diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index f2cf1a4838c36..96f55ea6451e8 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -116,8 +116,8 @@ class CSSToExcelConverter: focusing on font styling, backgrounds, borders and alignment. Operates by first computing CSS styles in a fairly generic - way (see :meth:`compute_css`) then determining Excel style - properties from CSS properties (see :meth:`build_xlstyle`). + way (see :meth: `compute_css`) then determining Excel style + properties from CSS properties (see :meth: `build_xlstyle`). Parameters ---------- @@ -594,7 +594,11 @@ def _format_value(self, val): elif missing.isneginf_scalar(val): val = f"-{self.inf_rep}" elif self.float_format is not None: - val = float(self.float_format % val) + val = self.float_format % val + else: + # respecter l'affichage par défaut de pandas (console) + val = repr(val) + if getattr(val, "tzinfo", None) is not None: raise ValueError( "Excel does not support datetimes with " @@ -616,7 +620,20 @@ def _format_header_mi(self) -> Iterable[ExcelCell]: columns = self.columns merge_columns = self.merge_cells in {True, "columns"} - level_strs = columns._format_multi(sparsify=merge_columns, include_names=False) + + # Replace NaN column header values with a non-breaking space so + # Excel output matches console display (see user's _fix_headers). + NBSP = "\u00A0" + if isinstance(columns, MultiIndex): + fixed_levels = [] + for lvl in range(columns.nlevels): + vals = columns.get_level_values(lvl) + fixed_levels.append([NBSP if missing.isna(v) else str(v) for v in vals]) + fixed_columns = MultiIndex.from_arrays(fixed_levels, names=columns.names) + else: + fixed_columns = Index([NBSP if missing.isna(v) else str(v) for v in columns], name=columns.name) + + level_strs = fixed_columns._format_multi(sparsify=merge_columns, include_names=False) level_lengths = get_level_lengths(level_strs) coloffset = 0 lnum = 0 @@ -625,17 +642,24 @@ def _format_header_mi(self) -> Iterable[ExcelCell]: coloffset = self.df.index.nlevels - 1 for lnum, name in enumerate(columns.names): + val = NBSP if missing.isna(name) else str(name) yield ExcelCell( row=lnum, col=coloffset, - val=name, + val=val, style=None, ) - for lnum, (spans, levels, level_codes) in enumerate( - zip(level_lengths, columns.levels, columns.codes) + + + # Iterate the fixed_columns levels/codes so values already have + # NaNs replaced by NBSP (and are strings). + for lnum, (spans, level, codes) in enumerate( + zip(level_lengths, fixed_columns.levels, fixed_columns.codes) ): - values = levels.take(level_codes) + # level.take(codes) on fixed_columns.levels yields string values + values = level.take(codes).to_numpy() + for i, span_val in spans.items(): mergestart, mergeend = None, None if merge_columns and span_val > 1: @@ -652,6 +676,7 @@ def _format_header_mi(self) -> Iterable[ExcelCell]: mergestart=mergestart, mergeend=mergeend, ) + self.rowcounter = lnum def _format_header_regular(self) -> Iterable[ExcelCell]: @@ -673,6 +698,12 @@ def _format_header_regular(self) -> Iterable[ExcelCell]: ) colnames = self.header + # Normalize NaN column labels to a non-breaking space so Excel + # header output matches console display (same behavior as + # applied to MultiIndex headers in _format_header_mi). + NBSP = "\u00A0" + colnames = [NBSP if missing.isna(v) else str(v) for v in colnames] + for colindex, colname in enumerate(colnames): yield CssExcelCell( row=self.rowcounter,