importmathfromtypingimportIterableimportnumpyasnpimportpandasaspddef_add_trailing_zero(formatted_number:str)->str:"""Ensure there is at least one decimal digit."""ifnotformatted_number:return"0.0"if"."informatted_number:ifformatted_number[-1]==".":returnf"{formatted_number}0"returnformatted_numberreturnf"{formatted_number}.0"def_format_real_number(value:float)->str:"""Format a real number like Biogeme, for LaTeX tables. - Use .3g formatting. - Avoid losing the decimal point. - Preserve scientific notation if used. """formatted_value=f"{value:.3g}"# If scientific notation is used, protect the mantissaif"e"informatted_value:left,right=formatted_value.split("e")returnf"{_add_trailing_zero(left)}e{right}"if"E"informatted_value:left,right=formatted_value.split("E")returnf"{_add_trailing_zero(left)}E{right}"return_add_trailing_zero(formatted_value)
[docs]defdataframe_to_latex_decimal(df:pd.DataFrame,float_columns:Iterable[str]|None=None,include_index:bool=True,caption:str|None=None,label:str|None=None,)->str:"""Generate a LaTeX tabular with r@{.}l alignment for float columns. Parameters ---------- df Input DataFrame. float_columns Names of columns to treat as numeric with decimal alignment. If None, all columns with float dtype are used. include_index If True, include the index as the first column (left aligned). caption Optional LaTeX caption (without \\caption{} wrapper). label Optional LaTeX label, used as \\label{...} if provided. Returns ------- latex A LaTeX string with \\begin{tabular} ... \\end{tabular}. """iffloat_columnsisNone:float_columns=[cforcindf.columnsifnp.issubdtype(df[c].dtype,np.floating)]float_columns=list(float_columns)# Column alignment specificationcol_specs:list[str]=[]ifinclude_index:col_specs.append("l")forcolindf.columns:ifcolinfloat_columns:col_specs.append("r@{.}l")else:col_specs.append("l")col_spec_str="".join(col_specs)lines:list[str]=[]lines.append(f"\\begin{{tabular}}{{{col_spec_str}}}")# Optional caption/label for a standalone table environmentifcaptionisnotNoneorlabelisnotNone:lines.append("\\hline")ifcaptionisnotNone:lines.append(f"\\multicolumn{{{len(col_specs)}}}{{c}}{{{caption}}}\\\\")iflabelisnotNone:lines.append(f"\\multicolumn{{{len(col_specs)}}}{{c}}{{\\label{{{label}}}}}\\\\")lines.append("\\hline")# Header rowheader_cells:list[str]=[]ifinclude_index:header_cells.append("")# index column has no headerforcolindf.columns:safe_name=str(col).replace("_",r"\_")ifcolinfloat_columns:# Span the two decimal-aligned columnsheader_cells.append(f"\\multicolumn{{2}}{{c}}{{{safe_name}}}")else:header_cells.append(safe_name)lines.append(" & ".join(header_cells)+r" \\")lines.append(r"\hline")# Data rowsforidx,rowindf.iterrows():row_cells:list[str]=[]ifinclude_index:idx_str=str(idx).replace("_",r"\_")row_cells.append(idx_str)forcolindf.columns:val=row[col]ifcolinfloat_columns:ifvalisNoneor(isinstance(val,float)andmath.isnan(val)):# Empty numeric cell: keep both parts emptyrow_cells.append("")# integer partrow_cells.append("")# fractional partelse:formatted=_format_real_number(float(val))# r@{.}l: replace the dot by '&' so we supply two cellsleft_right=formatted.split(".",maxsplit=1)iflen(left_right)==2:left,right=left_rightelse:# Should not happen thanks to _format_real_number, but be safeleft,right=formatted,""row_cells.append(left)row_cells.append(right)else:cell=str(val)cell=cell.replace("_",r"\_")row_cells.append(cell)lines.append(" & ".join(row_cells)+r" \\")lines.append(r"\end{tabular}")return"\n".join(lines)