Skip to content

Commit 5cffe0c

Browse files
ENH: adding autofilter when writing to excel (#61194) (#62994)
1 parent adb39e4 commit 5cffe0c

File tree

10 files changed

+256
-1
lines changed

10 files changed

+256
-1
lines changed

doc/source/user_guide/io.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3717,6 +3717,7 @@ The look and feel of Excel worksheets created from pandas can be modified using
37173717

37183718
* ``float_format`` : Format string for floating point numbers (default ``None``).
37193719
* ``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``).
3720+
* ``autofilter`` : A boolean indicating whether to add automatic filters to all columns (default ``False``).
37203721

37213722
.. note::
37223723

doc/source/whatsnew/v3.0.0.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ Other enhancements
202202
- :class:`Holiday` has gained the constructor argument and field ``exclude_dates`` to exclude specific datetimes from a custom holiday calendar (:issue:`54382`)
203203
- :class:`Rolling` and :class:`Expanding` now support ``nunique`` (:issue:`26958`)
204204
- :class:`Rolling` and :class:`Expanding` now support aggregations ``first`` and ``last`` (:issue:`33155`)
205+
- :func:`DataFrame.to_excel` has a new ``autofilter`` parameter to add automatic filters to all columns (:issue:`61194`)
205206
- :func:`read_parquet` accepts ``to_pandas_kwargs`` which are forwarded to :meth:`pyarrow.Table.to_pandas` which enables passing additional keywords to customize the conversion to pandas, such as ``maps_as_pydicts`` to read the Parquet map data type as python dictionaries (:issue:`56842`)
206207
- :func:`to_numeric` on big integers converts to ``object`` datatype with python integers when not coercing. (:issue:`51295`)
207208
- :meth:`.DataFrameGroupBy.transform`, :meth:`.SeriesGroupBy.transform`, :meth:`.DataFrameGroupBy.agg`, :meth:`.SeriesGroupBy.agg`, :meth:`.SeriesGroupBy.apply`, :meth:`.DataFrameGroupBy.apply` now support ``kurt`` (:issue:`40139`)
@@ -232,7 +233,6 @@ Other enhancements
232233
- Support reading Stata 102-format (Stata 1) dta files (:issue:`58978`)
233234
- Support reading Stata 110-format (Stata 7) dta files (:issue:`47176`)
234235
- Switched wheel upload to **PyPI Trusted Publishing** (OIDC) for release-tag pushes in ``wheels.yml``. (:issue:`61718`)
235-
-
236236

237237
.. ---------------------------------------------------------------------------
238238
.. _whatsnew_300.notable_bug_fixes:

pandas/core/generic.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2178,6 +2178,7 @@ def to_excel(
21782178
freeze_panes: tuple[int, int] | None = None,
21792179
storage_options: StorageOptions | None = None,
21802180
engine_kwargs: dict[str, Any] | None = None,
2181+
autofilter: bool = False,
21812182
) -> None:
21822183
"""
21832184
Write {klass} to an Excel sheet.
@@ -2238,6 +2239,9 @@ def to_excel(
22382239
22392240
.. versionadded:: {storage_options_versionadded}
22402241
{extra_parameters}
2242+
autofilter : bool, default False
2243+
If True, add automatic filters to all columns.
2244+
22412245
See Also
22422246
--------
22432247
to_csv : Write DataFrame to a comma-separated values (csv) file.
@@ -2310,6 +2314,7 @@ def to_excel(
23102314
index_label=index_label,
23112315
merge_cells=merge_cells,
23122316
inf_rep=inf_rep,
2317+
autofilter=autofilter,
23132318
)
23142319
formatter.write(
23152320
excel_writer,

pandas/io/excel/_base.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1204,6 +1204,7 @@ def _write_cells(
12041204
startrow: int = 0,
12051205
startcol: int = 0,
12061206
freeze_panes: tuple[int, int] | None = None,
1207+
autofilter_range: str | None = None,
12071208
) -> None:
12081209
"""
12091210
Write given formatted cells into Excel an excel sheet
@@ -1218,6 +1219,8 @@ def _write_cells(
12181219
startcol : upper left cell column to dump data frame
12191220
freeze_panes: int tuple of length 2
12201221
contains the bottom-most row and right-most column to freeze
1222+
autofilter_range: str, default None
1223+
column ranges to add automatic filters to, for example "A1:D5"
12211224
"""
12221225
raise NotImplementedError
12231226

pandas/io/excel/_odswriter.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,15 @@ def _write_cells(
9999
startrow: int = 0,
100100
startcol: int = 0,
101101
freeze_panes: tuple[int, int] | None = None,
102+
autofilter_range: str | None = None,
102103
) -> None:
103104
"""
104105
Write the frame cells using odf
105106
"""
107+
108+
if autofilter_range:
109+
raise ValueError("Autofilter is not supported with odf!")
110+
106111
from odf.table import (
107112
Table,
108113
TableCell,

pandas/io/excel/_openpyxl.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,7 @@ def _write_cells(
449449
startrow: int = 0,
450450
startcol: int = 0,
451451
freeze_panes: tuple[int, int] | None = None,
452+
autofilter_range: str | None = None,
452453
) -> None:
453454
# Write the frame cells using openpyxl.
454455
sheet_name = self._get_sheet_name(sheet_name)
@@ -532,6 +533,9 @@ def _write_cells(
532533
for k, v in style_kwargs.items():
533534
setattr(xcell, k, v)
534535

536+
if autofilter_range:
537+
wks.auto_filter.ref = autofilter_range
538+
535539

536540
class OpenpyxlReader(BaseExcelReader["Workbook"]):
537541
@doc(storage_options=_shared_docs["storage_options"])

pandas/io/excel/_xlsxwriter.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ def _write_cells(
245245
startrow: int = 0,
246246
startcol: int = 0,
247247
freeze_panes: tuple[int, int] | None = None,
248+
autofilter_range: str | None = None,
248249
) -> None:
249250
# Write the frame cells using xlsxwriter.
250251
sheet_name = self._get_sheet_name(sheet_name)
@@ -282,3 +283,6 @@ def _write_cells(
282283
)
283284
else:
284285
wks.write(startrow + cell.row, startcol + cell.col, val, style)
286+
287+
if autofilter_range:
288+
wks.autofilter(autofilter_range)

pandas/io/formats/excel.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,8 @@ class ExcelFormatter:
532532
Defaults to ``CSSToExcelConverter()``.
533533
It should have signature css_declarations string -> excel style.
534534
This is only called for body cells.
535+
autofilter : bool, default False
536+
If True, add automatic filters to all columns.
535537
"""
536538

537539
max_rows = 2**20
@@ -549,6 +551,7 @@ def __init__(
549551
merge_cells: ExcelWriterMergeCells = False,
550552
inf_rep: str = "inf",
551553
style_converter: Callable | None = None,
554+
autofilter: bool = False,
552555
) -> None:
553556
self.rowcounter = 0
554557
self.na_rep = na_rep
@@ -584,6 +587,7 @@ def __init__(
584587
raise ValueError(f"Unexpected value for {merge_cells=}.")
585588
self.merge_cells = merge_cells
586589
self.inf_rep = inf_rep
590+
self.autofilter = autofilter
587591

588592
def _format_value(self, val):
589593
if is_scalar(val) and missing.isna(val):
@@ -873,6 +877,34 @@ def get_formatted_cells(self) -> Iterable[ExcelCell]:
873877
cell.val = self._format_value(cell.val)
874878
yield cell
875879

880+
def _num2excel(self, index: int) -> str:
881+
"""
882+
Convert 0-based column index to Excel column name.
883+
884+
Parameters
885+
----------
886+
index : int
887+
The numeric column index to convert to a Excel column name.
888+
889+
Returns
890+
-------
891+
column_name : str
892+
The column name corresponding to the index.
893+
894+
Raises
895+
------
896+
ValueError
897+
Index is negative
898+
"""
899+
if index < 0:
900+
raise ValueError(f"Index cannot be negative: {index}")
901+
column_name = ""
902+
# while loop in case column name needs to be longer than 1 character
903+
while index > 0 or not column_name:
904+
index, remainder = divmod(index, 26)
905+
column_name = chr(65 + remainder) + column_name
906+
return column_name
907+
876908
@doc(storage_options=_shared_docs["storage_options"])
877909
def write(
878910
self,
@@ -916,6 +948,46 @@ def write(
916948
f"Max sheet size is: {self.max_rows}, {self.max_cols}"
917949
)
918950

951+
if self.autofilter:
952+
# default offset for header row
953+
startrowsoffset = 1
954+
endrowsoffset = 1
955+
956+
if num_cols == 0:
957+
indexoffset = 0
958+
elif self.index:
959+
indexoffset = 0
960+
if isinstance(self.df.index, MultiIndex):
961+
if self.merge_cells:
962+
raise ValueError(
963+
"Excel filters merged cells by showing only the first row. "
964+
"'autofilter' and 'merge_cells' cannot "
965+
"be used simultaneously."
966+
)
967+
else:
968+
indexoffset = self.df.index.nlevels - 1
969+
970+
if isinstance(self.columns, MultiIndex):
971+
if self.merge_cells:
972+
raise ValueError(
973+
"Excel filters merged cells by showing only the first row. "
974+
"'autofilter' and 'merge_cells' cannot "
975+
"be used simultaneously."
976+
)
977+
else:
978+
startrowsoffset = self.columns.nlevels
979+
# multindex columns add a blank row between header and data
980+
endrowsoffset = self.columns.nlevels + 1
981+
else:
982+
# no index column
983+
indexoffset = -1
984+
start = f"{self._num2excel(startcol)}{startrow + startrowsoffset}"
985+
autofilter_end_column = self._num2excel(startcol + num_cols + indexoffset)
986+
end = f"{autofilter_end_column}{startrow + num_rows + endrowsoffset}"
987+
autofilter_range = f"{start}:{end}"
988+
else:
989+
autofilter_range = None
990+
919991
if engine_kwargs is None:
920992
engine_kwargs = {}
921993

@@ -938,6 +1010,7 @@ def write(
9381010
startrow=startrow,
9391011
startcol=startcol,
9401012
freeze_panes=freeze_panes,
1013+
autofilter_range=autofilter_range,
9411014
)
9421015
finally:
9431016
# make sure to close opened file handles

pandas/io/formats/style.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,7 @@ def to_excel(
579579
verbose: bool = True,
580580
freeze_panes: tuple[int, int] | None = None,
581581
storage_options: StorageOptions | None = None,
582+
autofilter: bool = False,
582583
) -> None:
583584
from pandas.io.formats.excel import ExcelFormatter
584585

@@ -592,6 +593,7 @@ def to_excel(
592593
index_label=index_label,
593594
merge_cells=merge_cells,
594595
inf_rep=inf_rep,
596+
autofilter=autofilter,
595597
)
596598
formatter.write(
597599
excel_writer,

0 commit comments

Comments
 (0)