Skip to content

Commit 90f75a5

Browse files
committed
ENH: adding autofilter when writing to excel (#61194)
1 parent 9f66b81 commit 90f75a5

File tree

8 files changed

+52
-1
lines changed

8 files changed

+52
-1
lines changed

doc/source/user_guide/io.rst

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

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

37493750
.. note::
37503751

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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2180,6 +2180,7 @@ def to_excel(
21802180
freeze_panes: tuple[int, int] | None = None,
21812181
storage_options: StorageOptions | None = None,
21822182
engine_kwargs: dict[str, Any] | None = None,
2183+
autofilter: bool = False,
21832184
) -> None:
21842185
"""
21852186
Write {klass} to an Excel sheet.
@@ -2312,6 +2313,7 @@ def to_excel(
23122313
index_label=index_label,
23132314
merge_cells=merge_cells,
23142315
inf_rep=inf_rep,
2316+
autofilter=autofilter,
23152317
)
23162318
formatter.write(
23172319
excel_writer,

pandas/io/excel/_odswriter.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ 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

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: 22 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,16 @@ 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+
column_name = ""
885+
while index > 0 or not column_name:
886+
index, remainder = divmod(index, 26)
887+
column_name = chr(65 + remainder) + column_name
888+
return column_name
889+
876890
@doc(storage_options=_shared_docs["storage_options"])
877891
def write(
878892
self,
@@ -916,6 +930,13 @@ def write(
916930
f"Max sheet size is: {self.max_rows}, {self.max_cols}"
917931
)
918932

933+
if self.autofilter:
934+
start = f"{self._num2excel(startcol)}{startrow + 1}"
935+
end = f"{self._num2excel(startcol + num_cols)}{startrow + num_rows + 1}"
936+
autofilter_range = f"{start}:{end}"
937+
else:
938+
autofilter_range = None
939+
919940
if engine_kwargs is None:
920941
engine_kwargs = {}
921942

@@ -938,6 +959,7 @@ def write(
938959
startrow=startrow,
939960
startcol=startcol,
940961
freeze_panes=freeze_panes,
962+
autofilter_range=autofilter_range,
941963
)
942964
finally:
943965
# make sure to close opened file handles

pandas/tests/io/excel/test_style.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,3 +350,20 @@ def test_format_hierarchical_rows_periodindex(merge_cells):
350350
assert isinstance(cell.val, Timestamp), (
351351
"Period should be converted to Timestamp"
352352
)
353+
354+
355+
@pytest.mark.parametrize("engine", ["xlsxwriter", "openpyxl"])
356+
def test_autofilter(engine, tmp_excel):
357+
# GH 61194
358+
df = DataFrame.from_dict([{"A": 1, "B": 2, "C": 3}, {"A": 4, "B": 5, "C": 6}])
359+
360+
with ExcelWriter(tmp_excel, engine=engine) as writer:
361+
df.to_excel(writer, autofilter=True, index=False)
362+
363+
openpyxl = pytest.importorskip("openpyxl") # test loading only with openpyxl
364+
with contextlib.closing(openpyxl.load_workbook(tmp_excel)) as wb:
365+
ws = wb.active
366+
367+
assert ws.auto_filter.ref is not None
368+
print(ws.auto_filter.ref)
369+
assert ws.auto_filter.ref == "A1:D3"

0 commit comments

Comments
 (0)