From 620dc406a2504227a4edac9bcbaa621fb52513c1 Mon Sep 17 00:00:00 2001 From: Richard Howe Date: Fri, 28 Jul 2023 23:44:25 -0400 Subject: [PATCH 1/8] Updated default styling logic for to_excel and added unit tests. --- doc/source/whatsnew/v2.1.0.rst | 1 + pandas/io/formats/excel.py | 33 ++++++----------- pandas/tests/io/excel/test_style.py | 55 +++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 23 deletions(-) diff --git a/doc/source/whatsnew/v2.1.0.rst b/doc/source/whatsnew/v2.1.0.rst index 0fdec3175f635..9162d47807d6c 100644 --- a/doc/source/whatsnew/v2.1.0.rst +++ b/doc/source/whatsnew/v2.1.0.rst @@ -176,6 +176,7 @@ Other enhancements - Performance improvement in :func:`concat` with homogeneous ``np.float64`` or ``np.float32`` dtypes (:issue:`52685`) - Performance improvement in :meth:`DataFrame.filter` when ``items`` is given (:issue:`52941`) - Reductions :meth:`Series.argmax`, :meth:`Series.argmin`, :meth:`Series.idxmax`, :meth:`Series.idxmin`, :meth:`Index.argmax`, :meth:`Index.argmin`, :meth:`DataFrame.idxmax`, :meth:`DataFrame.idxmin` are now supported for object-dtype objects (:issue:`4279`, :issue:`18021`, :issue:`40685`, :issue:`43697`) +- Updated :meth:`DataFrame.to_excel` so that the output spreadsheet has no styling. (:issue:`54154`) - .. --------------------------------------------------------------------------- diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index 9970d465ced9d..9713929e958ee 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -580,19 +580,6 @@ def __init__( self.merge_cells = merge_cells self.inf_rep = inf_rep - @property - def header_style(self) -> dict[str, dict[str, str | bool]]: - return { - "font": {"bold": True}, - "borders": { - "top": "thin", - "right": "thin", - "bottom": "thin", - "left": "thin", - }, - "alignment": {"horizontal": "center", "vertical": "top"}, - } - def _format_value(self, val): if is_scalar(val) and missing.isna(val): val = self.na_rep @@ -640,7 +627,7 @@ def _format_header_mi(self) -> Iterable[ExcelCell]: row=lnum, col=coloffset, val=name, - style=self.header_style, + style=None, ) for lnum, (spans, levels, level_codes) in enumerate( @@ -655,7 +642,7 @@ def _format_header_mi(self) -> Iterable[ExcelCell]: row=lnum, col=coloffset + i + 1, val=values[i], - style=self.header_style, + style=None, css_styles=getattr(self.styler, "ctx_columns", None), css_row=lnum, css_col=i, @@ -671,7 +658,7 @@ def _format_header_mi(self) -> Iterable[ExcelCell]: row=lnum, col=coloffset + i + 1, val=v, - style=self.header_style, + style=None, css_styles=getattr(self.styler, "ctx_columns", None), css_row=lnum, css_col=i, @@ -704,7 +691,7 @@ def _format_header_regular(self) -> Iterable[ExcelCell]: row=self.rowcounter, col=colindex + coloffset, val=colname, - style=self.header_style, + style=None, css_styles=getattr(self.styler, "ctx_columns", None), css_row=0, css_col=colindex, @@ -727,7 +714,7 @@ def _format_header(self) -> Iterable[ExcelCell]: ] * len(self.columns) if functools.reduce(lambda x, y: x and y, (x != "" for x in row)): gen2 = ( - ExcelCell(self.rowcounter, colindex, val, self.header_style) + ExcelCell(self.rowcounter, colindex, val, None) for colindex, val in enumerate(row) ) self.rowcounter += 1 @@ -761,7 +748,7 @@ def _format_regular_rows(self) -> Iterable[ExcelCell]: self.rowcounter += 1 if index_label and self.header is not False: - yield ExcelCell(self.rowcounter - 1, 0, index_label, self.header_style) + yield ExcelCell(self.rowcounter - 1, 0, index_label, None) # write index_values index_values = self.df.index @@ -773,7 +760,7 @@ def _format_regular_rows(self) -> Iterable[ExcelCell]: row=self.rowcounter + idx, col=0, val=idxval, - style=self.header_style, + style=None, css_styles=getattr(self.styler, "ctx_index", None), css_row=idx, css_col=0, @@ -809,7 +796,7 @@ def _format_hierarchical_rows(self) -> Iterable[ExcelCell]: # if index labels are not empty go ahead and dump if com.any_not_none(*index_labels) and self.header is not False: for cidx, name in enumerate(index_labels): - yield ExcelCell(self.rowcounter - 1, cidx, name, self.header_style) + yield ExcelCell(self.rowcounter - 1, cidx, name, None) if self.merge_cells: # Format hierarchical rows as merged cells. @@ -836,7 +823,7 @@ def _format_hierarchical_rows(self) -> Iterable[ExcelCell]: row=self.rowcounter + i, col=gcolidx, val=values[i], - style=self.header_style, + style=None, css_styles=getattr(self.styler, "ctx_index", None), css_row=i, css_col=gcolidx, @@ -854,7 +841,7 @@ def _format_hierarchical_rows(self) -> Iterable[ExcelCell]: row=self.rowcounter + idx, col=gcolidx, val=indexcolval, - style=self.header_style, + style=None, css_styles=getattr(self.styler, "ctx_index", None), css_row=idx, css_col=gcolidx, diff --git a/pandas/tests/io/excel/test_style.py b/pandas/tests/io/excel/test_style.py index 710f1f272cd7f..b6ffd9e379269 100644 --- a/pandas/tests/io/excel/test_style.py +++ b/pandas/tests/io/excel/test_style.py @@ -31,6 +31,28 @@ def assert_equal_cell_styles(cell1, cell2): assert cell1.protection.__dict__ == cell2.protection.__dict__ +def test_styler_default_values(): + # GH 54154 + openpyxl = pytest.importorskip("openpyxl") + df = DataFrame([{"A": 1, "B": 2, "C": 3}, {"A": 1, "B": 2, "C": 3}]) + + with tm.ensure_clean(".xlsx") as path: + with ExcelWriter(path, engine="openpyxl") as writer: + df.to_excel(writer, sheet_name="custom") + + with contextlib.closing(openpyxl.load_workbook(path)) as wb: + # Check font, spacing, indentation + assert wb["custom"].cell(1, 1).font.bold is False + assert wb["custom"].cell(1, 1).alignment.horizontal is None + assert wb["custom"].cell(1, 1).alignment.vertical is None + + # Check border + assert wb["custom"].cell(1, 1).border.bottom.color is None + assert wb["custom"].cell(1, 1).border.top.color is None + assert wb["custom"].cell(1, 1).border.left.color is None + assert wb["custom"].cell(1, 1).border.right.color is None + + @pytest.mark.parametrize( "engine", ["xlsxwriter", "openpyxl"], @@ -123,6 +145,39 @@ def test_styler_to_excel_unstyled(engine): ] +@pytest.mark.parametrize( + "css", + ["background-color: #111222"], +) +def test_styler_custom_style(css): + # GH 54154 + openpyxl = pytest.importorskip("openpyxl") + df = DataFrame([{"A": 1, "B": 2}, {"A": 1, "B": 2}]) + + with tm.ensure_clean(".xlsx") as path: + with ExcelWriter(path, engine="openpyxl") as writer: + styler = df.style.map(lambda x: css) + styler.to_excel(writer, sheet_name="custom", index=False) + + with contextlib.closing(openpyxl.load_workbook(path)) as wb: + # Check font, spacing, indentation + assert wb["custom"].cell(1, 1).font.bold is False + assert wb["custom"].cell(1, 1).alignment.horizontal is None + assert wb["custom"].cell(1, 1).alignment.vertical is None + + # Check border + assert wb["custom"].cell(1, 1).border.bottom.color is None + assert wb["custom"].cell(1, 1).border.top.color is None + assert wb["custom"].cell(1, 1).border.left.color is None + assert wb["custom"].cell(1, 1).border.right.color is None + + # Check background color + assert wb["custom"].cell(2, 1).fill.fgColor.index == "00111222" + assert wb["custom"].cell(3, 1).fill.fgColor.index == "00111222" + assert wb["custom"].cell(2, 2).fill.fgColor.index == "00111222" + assert wb["custom"].cell(3, 2).fill.fgColor.index == "00111222" + + @pytest.mark.parametrize( "engine", ["xlsxwriter", "openpyxl"], From a44242a0110125db56858880ac32b7837ea7f218 Mon Sep 17 00:00:00 2001 From: Richard Howe Date: Sat, 29 Jul 2023 10:28:00 -0400 Subject: [PATCH 2/8] Adding documentation to the Pandas User Guide. --- doc/source/user_guide/io.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/doc/source/user_guide/io.rst b/doc/source/user_guide/io.rst index 3f986cd803b10..ef8d1c28dad02 100644 --- a/doc/source/user_guide/io.rst +++ b/doc/source/user_guide/io.rst @@ -3924,6 +3924,20 @@ The look and feel of Excel worksheets created from pandas can be modified using * ``float_format`` : Format string for floating point numbers (default ``None``). * ``freeze_panes`` : A tuple of two integers representing the bottommost row and rightmost column to freeze. Each of these parameters is one-based, so (1, 1) will freeze the first row and first column (default ``None``). +.. note:: + + As of Pandas 3.0, by default spreadsheets created with the ``to_excel`` method + will not contain any styling. Users wishing to bold text, add bordered styles, + etc in a worksheet output by ``to_excel`` can do so by using ``Styler.to_excel`` + to create styled excel files. + + +.. code-block:: python + + css = "border: 1pt solid #111222" + styler = df.style.map(lambda x: css) + styler.to_excel("path_to_file.xlsx", sheet_name="custom", index=False) + Using the `Xlsxwriter`_ engine provides many options for controlling the format of an Excel worksheet created with the ``to_excel`` method. Excellent examples can be found in the `Xlsxwriter`_ documentation here: https://xlsxwriter.readthedocs.io/working_with_pandas.html From ac25ee345107e552a310d590ef19895aea7a58ad Mon Sep 17 00:00:00 2001 From: Richard Howe Date: Wed, 14 Feb 2024 22:16:07 -0500 Subject: [PATCH 3/8] Updating whatsnew --- doc/source/whatsnew/v2.1.0.rst | 1 - doc/source/whatsnew/v3.0.0.rst | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/source/whatsnew/v2.1.0.rst b/doc/source/whatsnew/v2.1.0.rst index 5385fb93e61c3..3cdefcc12f6f6 100644 --- a/doc/source/whatsnew/v2.1.0.rst +++ b/doc/source/whatsnew/v2.1.0.rst @@ -272,7 +272,6 @@ Other enhancements - Performance improvement in :func:`concat` with homogeneous ``np.float64`` or ``np.float32`` dtypes (:issue:`52685`) - Performance improvement in :meth:`DataFrame.filter` when ``items`` is given (:issue:`52941`) - Reductions :meth:`Series.argmax`, :meth:`Series.argmin`, :meth:`Series.idxmax`, :meth:`Series.idxmin`, :meth:`Index.argmax`, :meth:`Index.argmin`, :meth:`DataFrame.idxmax`, :meth:`DataFrame.idxmin` are now supported for object-dtype objects (:issue:`4279`, :issue:`18021`, :issue:`40685`, :issue:`43697`) -- Updated :meth:`DataFrame.to_excel` so that the output spreadsheet has no styling. (:issue:`54154`) - .. --------------------------------------------------------------------------- diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 35769d9c5f0d8..54f54c419ac49 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -32,7 +32,7 @@ Other enhancements - :func:`read_stata` now returns ``datetime64`` resolutions better matching those natively stored in the stata format (:issue:`55642`) - Allow dictionaries to be passed to :meth:`pandas.Series.str.replace` via ``pat`` parameter (:issue:`51748`) - Support passing a :class:`Series` input to :func:`json_normalize` that retains the :class:`Series` :class:`Index` (:issue:`51452`) -- +- Updated :meth:`DataFrame.to_excel` so that the output spreadsheet has no styling. (:issue:`54154`) .. --------------------------------------------------------------------------- .. _whatsnew_300.notable_bug_fixes: From 505b4a8bff7ba8fd028511fc9dfdaf8952ef9763 Mon Sep 17 00:00:00 2001 From: Richard Howe Date: Wed, 14 Feb 2024 22:21:03 -0500 Subject: [PATCH 4/8] Fixing merge conflict. --- doc/source/whatsnew/v2.1.0.rst | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/doc/source/whatsnew/v2.1.0.rst b/doc/source/whatsnew/v2.1.0.rst index 3cdefcc12f6f6..d4eb5742ef928 100644 --- a/doc/source/whatsnew/v2.1.0.rst +++ b/doc/source/whatsnew/v2.1.0.rst @@ -266,32 +266,6 @@ Other enhancements - Let :meth:`DataFrame.to_feather` accept a non-default :class:`Index` and non-string column names (:issue:`51787`) - Added a new parameter ``by_row`` to :meth:`Series.apply` and :meth:`DataFrame.apply`. When set to ``False`` the supplied callables will always operate on the whole Series or DataFrame (:issue:`53400`, :issue:`53601`). - :meth:`DataFrame.shift` and :meth:`Series.shift` now allow shifting by multiple periods by supplying a list of periods (:issue:`44424`) -- Groupby aggregations (such as :meth:`DataFrameGroupby.sum`) now can preserve the dtype of the input instead of casting to ``float64`` (:issue:`44952`) -- Improved error message when :meth:`DataFrameGroupBy.agg` failed (:issue:`52930`) -- Many read/to_* functions, such as :meth:`DataFrame.to_pickle` and :func:`read_csv`, support forwarding compression arguments to lzma.LZMAFile (:issue:`52979`) -- Performance improvement in :func:`concat` with homogeneous ``np.float64`` or ``np.float32`` dtypes (:issue:`52685`) -- Performance improvement in :meth:`DataFrame.filter` when ``items`` is given (:issue:`52941`) -- Reductions :meth:`Series.argmax`, :meth:`Series.argmin`, :meth:`Series.idxmax`, :meth:`Series.idxmin`, :meth:`Index.argmax`, :meth:`Index.argmin`, :meth:`DataFrame.idxmax`, :meth:`DataFrame.idxmin` are now supported for object-dtype objects (:issue:`4279`, :issue:`18021`, :issue:`40685`, :issue:`43697`) -- - -.. --------------------------------------------------------------------------- -.. _whatsnew_210.notable_bug_fixes: - -Notable bug fixes -~~~~~~~~~~~~~~~~~ - -These are bug fixes that might have notable behavior changes. - -.. _whatsnew_210.notable_bug_fixes.notable_bug_fix1: - -notable_bug_fix1 -^^^^^^^^^^^^^^^^ - -.. _whatsnew_210.notable_bug_fixes.notable_bug_fix2: - -notable_bug_fix2 -^^^^^^^^^^^^^^^^ -======= - Groupby aggregations with ``numba`` (such as :meth:`.DataFrameGroupBy.sum`) now can preserve the dtype of the input instead of casting to ``float64`` (:issue:`44952`) - Improved error message when :meth:`.DataFrameGroupBy.agg` failed (:issue:`52930`) - Many read/to_* functions, such as :meth:`DataFrame.to_pickle` and :func:`read_csv`, support forwarding compression arguments to ``lzma.LZMAFile`` (:issue:`52979`) From 86662c46e75bdcbae4a443123b1ee0f7597b747f Mon Sep 17 00:00:00 2001 From: Richard Howe Date: Wed, 14 Feb 2024 22:53:59 -0500 Subject: [PATCH 5/8] Updating user guide documentation. --- doc/source/user_guide/io.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/source/user_guide/io.rst b/doc/source/user_guide/io.rst index d18541ba459a1..974fd380ab6c7 100644 --- a/doc/source/user_guide/io.rst +++ b/doc/source/user_guide/io.rst @@ -3912,15 +3912,15 @@ The look and feel of Excel worksheets created from pandas can be modified using As of Pandas 3.0, by default spreadsheets created with the ``to_excel`` method will not contain any styling. Users wishing to bold text, add bordered styles, - etc in a worksheet output by ``to_excel`` can do so by using ``Styler.to_excel`` - to create styled excel files. + etc in a worksheet output by ``to_excel`` can do so by using :meth:`Styler.to_excel` + to create styled excel files. For documentation on styling spreadsheets, see +`here `__. .. code-block:: python - css = "border: 1pt solid #111222" - styler = df.style.map(lambda x: css) - styler.to_excel("path_to_file.xlsx", sheet_name="custom", index=False) + css = "border: 1px solid black; font-weight: bold;" + df.style.map_index(lambda x: css).map_index(lambda x: css, axis=1).to_excel("myfile.xlsx") Using the `Xlsxwriter`_ engine provides many options for controlling the format of an Excel worksheet created with the ``to_excel`` method. Excellent examples can be found in the From 024a162c1a459dc093f55e7bda39bd53d1fdb81f Mon Sep 17 00:00:00 2001 From: Richard Howe Date: Wed, 14 Feb 2024 23:23:20 -0500 Subject: [PATCH 6/8] Fixing syntax error. --- doc/source/user_guide/io.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/user_guide/io.rst b/doc/source/user_guide/io.rst index 974fd380ab6c7..060db772b9682 100644 --- a/doc/source/user_guide/io.rst +++ b/doc/source/user_guide/io.rst @@ -3914,7 +3914,7 @@ The look and feel of Excel worksheets created from pandas can be modified using will not contain any styling. Users wishing to bold text, add bordered styles, etc in a worksheet output by ``to_excel`` can do so by using :meth:`Styler.to_excel` to create styled excel files. For documentation on styling spreadsheets, see -`here `__. + `here `__. .. code-block:: python From c1731fd39a7d236e2a21051f56ed339250b0148e Mon Sep 17 00:00:00 2001 From: Richard Howe Date: Thu, 15 Feb 2024 15:05:59 -0500 Subject: [PATCH 7/8] Updating implementation based on reviewer feedback. --- doc/source/whatsnew/v3.0.0.rst | 3 ++- pandas/tests/io/excel/test_style.py | 9 +++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 54f54c419ac49..ac802a5fcfa6c 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -32,7 +32,7 @@ Other enhancements - :func:`read_stata` now returns ``datetime64`` resolutions better matching those natively stored in the stata format (:issue:`55642`) - Allow dictionaries to be passed to :meth:`pandas.Series.str.replace` via ``pat`` parameter (:issue:`51748`) - Support passing a :class:`Series` input to :func:`json_normalize` that retains the :class:`Series` :class:`Index` (:issue:`51452`) -- Updated :meth:`DataFrame.to_excel` so that the output spreadsheet has no styling. (:issue:`54154`) +- .. --------------------------------------------------------------------------- .. _whatsnew_300.notable_bug_fixes: @@ -90,6 +90,7 @@ Other API changes - 3rd party ``py.path`` objects are no longer explicitly supported in IO methods. Use :py:class:`pathlib.Path` objects instead (:issue:`57091`) - :attr:`MultiIndex.codes`, :attr:`MultiIndex.levels`, and :attr:`MultiIndex.names` now returns a ``tuple`` instead of a ``FrozenList`` (:issue:`53531`) - Made ``dtype`` a required argument in :meth:`ExtensionArray._from_sequence_of_strings` (:issue:`56519`) +- Updated :meth:`DataFrame.to_excel` so that the output spreadsheet has no styling. (:issue:`54154`) - pickle and HDF (``.h5``) files created with Python 2 are no longer explicitly supported (:issue:`57387`) - diff --git a/pandas/tests/io/excel/test_style.py b/pandas/tests/io/excel/test_style.py index 7748d975be390..58b297e4520bb 100644 --- a/pandas/tests/io/excel/test_style.py +++ b/pandas/tests/io/excel/test_style.py @@ -145,18 +145,15 @@ def test_styler_to_excel_unstyled(engine): ] -@pytest.mark.parametrize( - "css", - ["background-color: #111222"], -) -def test_styler_custom_style(css): +def test_styler_custom_style(): # GH 54154 + css_style = "background-color: #111222" openpyxl = pytest.importorskip("openpyxl") df = DataFrame([{"A": 1, "B": 2}, {"A": 1, "B": 2}]) with tm.ensure_clean(".xlsx") as path: with ExcelWriter(path, engine="openpyxl") as writer: - styler = df.style.map(lambda x: css) + styler = df.style.map(lambda x: css_style) styler.to_excel(writer, sheet_name="custom", index=False) with contextlib.closing(openpyxl.load_workbook(path)) as wb: From ebdef70cd971e3eaf4b22a6b9cfb4328a2a3f5df Mon Sep 17 00:00:00 2001 From: Richard Howe Date: Thu, 15 Feb 2024 15:09:19 -0500 Subject: [PATCH 8/8] Updating documentation. --- doc/source/whatsnew/v3.0.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index ac802a5fcfa6c..4724ad5e14eba 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -90,7 +90,7 @@ Other API changes - 3rd party ``py.path`` objects are no longer explicitly supported in IO methods. Use :py:class:`pathlib.Path` objects instead (:issue:`57091`) - :attr:`MultiIndex.codes`, :attr:`MultiIndex.levels`, and :attr:`MultiIndex.names` now returns a ``tuple`` instead of a ``FrozenList`` (:issue:`53531`) - Made ``dtype`` a required argument in :meth:`ExtensionArray._from_sequence_of_strings` (:issue:`56519`) -- Updated :meth:`DataFrame.to_excel` so that the output spreadsheet has no styling. (:issue:`54154`) +- Updated :meth:`DataFrame.to_excel` so that the output spreadsheet has no styling. Custom styling can still be done using :meth:`Styler.to_excel` (:issue:`54154`) - pickle and HDF (``.h5``) files created with Python 2 are no longer explicitly supported (:issue:`57387`) -