Skip to content

Commit a3babac

Browse files
ulidomroeschkepre-commit-ci[bot]
authored
ENH: Add option to only merge column header cells in ExcelFormatter. (#59081)
* Add option to only merge column header cells in `ExcelFormatter`. * Add entry in the whatsnew document * Remove erroneous `:ref:` from docstring Co-authored-by: Matthew Roeschke <[email protected]> * Correct typo in docstring Co-authored-by: Matthew Roeschke <[email protected]> * Remove superfluous parentheses from if statement; better error message Co-authored-by: Matthew Roeschke <[email protected]> * Fix missing double quote. * Wording of whatsnew entry Co-authored-by: Matthew Roeschke <[email protected]> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: Matthew Roeschke <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 5719571 commit a3babac

File tree

5 files changed

+21
-9
lines changed

5 files changed

+21
-9
lines changed

doc/source/whatsnew/v3.0.0.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Other enhancements
3939
- Users can globally disable any ``PerformanceWarning`` by setting the option ``mode.performance_warnings`` to ``False`` (:issue:`56920`)
4040
- :meth:`Styler.format_index_names` can now be used to format the index and column names (:issue:`48936` and :issue:`47489`)
4141
- :class:`.errors.DtypeWarning` improved to include column names when mixed data types are detected (:issue:`58174`)
42+
- :func:`DataFrame.to_excel` argument ``merge_cells`` now accepts a value of ``"columns"`` to only merge :class:`MultiIndex` column header header cells (:issue:`35384`)
4243
- :meth:`DataFrame.corrwith` now accepts ``min_periods`` as optional arguments, as in :meth:`DataFrame.corr` and :meth:`Series.corr` (:issue:`9490`)
4344
- :meth:`DataFrame.cummin`, :meth:`DataFrame.cummax`, :meth:`DataFrame.cumprod` and :meth:`DataFrame.cumsum` methods now have a ``numeric_only`` parameter (:issue:`53072`)
4445
- :meth:`DataFrame.fillna` and :meth:`Series.fillna` can now accept ``value=None``; for non-object dtype the corresponding NA value will be used (:issue:`57723`)

pandas/_typing.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,7 @@ def closed(self) -> bool:
510510

511511
# ExcelWriter
512512
ExcelWriterIfSheetExists = Literal["error", "new", "replace", "overlay"]
513+
ExcelWriterMergeCells = Union[bool, Literal["columns"]]
513514

514515
# Offsets
515516
OffsetCalendar = Union[np.busdaycalendar, "AbstractHolidayCalendar"]

pandas/io/formats/excel.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252

5353
if TYPE_CHECKING:
5454
from pandas._typing import (
55+
ExcelWriterMergeCells,
5556
FilePath,
5657
IndexLabel,
5758
StorageOptions,
@@ -523,8 +524,11 @@ class ExcelFormatter:
523524
Column label for index column(s) if desired. If None is given, and
524525
`header` and `index` are True, then the index names are used. A
525526
sequence should be given if the DataFrame uses MultiIndex.
526-
merge_cells : bool, default False
527-
Format MultiIndex and Hierarchical Rows as merged cells.
527+
merge_cells : bool or 'columns', default False
528+
Format MultiIndex column headers and Hierarchical Rows as merged cells
529+
if True. Merge MultiIndex column headers only if 'columns'.
530+
.. versionchanged:: 3.0.0
531+
Added the 'columns' option.
528532
inf_rep : str, default `'inf'`
529533
representation for np.inf values (which aren't representable in Excel)
530534
A `'-'` sign will be added in front of -inf.
@@ -547,7 +551,7 @@ def __init__(
547551
header: Sequence[Hashable] | bool = True,
548552
index: bool = True,
549553
index_label: IndexLabel | None = None,
550-
merge_cells: bool = False,
554+
merge_cells: ExcelWriterMergeCells = False,
551555
inf_rep: str = "inf",
552556
style_converter: Callable | None = None,
553557
) -> None:
@@ -580,6 +584,9 @@ def __init__(
580584
self.index = index
581585
self.index_label = index_label
582586
self.header = header
587+
588+
if not isinstance(merge_cells, bool) and merge_cells != "columns":
589+
raise ValueError(f"Unexpected value for {merge_cells=}.")
583590
self.merge_cells = merge_cells
584591
self.inf_rep = inf_rep
585592

@@ -614,7 +621,7 @@ def _format_header_mi(self) -> Iterable[ExcelCell]:
614621

615622
columns = self.columns
616623
level_strs = columns._format_multi(
617-
sparsify=self.merge_cells, include_names=False
624+
sparsify=self.merge_cells in {True, "columns"}, include_names=False
618625
)
619626
level_lengths = get_level_lengths(level_strs)
620627
coloffset = 0
@@ -623,7 +630,7 @@ def _format_header_mi(self) -> Iterable[ExcelCell]:
623630
if self.index and isinstance(self.df.index, MultiIndex):
624631
coloffset = self.df.index.nlevels - 1
625632

626-
if self.merge_cells:
633+
if self.merge_cells in {True, "columns"}:
627634
# Format multi-index as a merged cells.
628635
for lnum, name in enumerate(columns.names):
629636
yield ExcelCell(
@@ -793,15 +800,17 @@ def _format_hierarchical_rows(self) -> Iterable[ExcelCell]:
793800
# with index names (blank if None) for
794801
# unambiguous round-trip, unless not merging,
795802
# in which case the names all go on one row Issue #11328
796-
if isinstance(self.columns, MultiIndex) and self.merge_cells:
803+
if isinstance(self.columns, MultiIndex) and (
804+
self.merge_cells in {True, "columns"}
805+
):
797806
self.rowcounter += 1
798807

799808
# if index labels are not empty go ahead and dump
800809
if com.any_not_none(*index_labels) and self.header is not False:
801810
for cidx, name in enumerate(index_labels):
802811
yield ExcelCell(self.rowcounter - 1, cidx, name, None)
803812

804-
if self.merge_cells:
813+
if self.merge_cells and self.merge_cells != "columns":
805814
# Format hierarchical rows as merged cells.
806815
level_strs = self.df.index._format_multi(
807816
sparsify=True, include_names=False

pandas/io/formats/style.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
Axis,
6767
AxisInt,
6868
Concatenate,
69+
ExcelWriterMergeCells,
6970
FilePath,
7071
IndexLabel,
7172
IntervalClosedType,
@@ -551,7 +552,7 @@ def to_excel(
551552
startrow: int = 0,
552553
startcol: int = 0,
553554
engine: str | None = None,
554-
merge_cells: bool = True,
555+
merge_cells: ExcelWriterMergeCells = True,
555556
encoding: str | None = None,
556557
inf_rep: str = "inf",
557558
verbose: bool = True,

pandas/tests/io/excel/test_writers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def frame(float_frame):
4949
return float_frame[:10]
5050

5151

52-
@pytest.fixture(params=[True, False])
52+
@pytest.fixture(params=[True, False, "columns"])
5353
def merge_cells(request):
5454
return request.param
5555

0 commit comments

Comments
 (0)