Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/source/user_guide/io.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3745,6 +3745,7 @@ 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``).
* ``autofilter`` : A boolean indicating whether to add automatic filters to all columns (default ``False``).

.. note::

Expand Down
2 changes: 1 addition & 1 deletion doc/source/whatsnew/v3.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ Other enhancements
- :class:`Holiday` has gained the constructor argument and field ``exclude_dates`` to exclude specific datetimes from a custom holiday calendar (:issue:`54382`)
- :class:`Rolling` and :class:`Expanding` now support ``nunique`` (:issue:`26958`)
- :class:`Rolling` and :class:`Expanding` now support aggregations ``first`` and ``last`` (:issue:`33155`)
- :func:`DataFrame.to_excel` has a new ``autofilter`` parameter to add automatic filters to all columns (:issue:`61194`)
- :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`)
- :func:`to_numeric` on big integers converts to ``object`` datatype with python integers when not coercing. (:issue:`51295`)
- :meth:`.DataFrameGroupBy.transform`, :meth:`.SeriesGroupBy.transform`, :meth:`.DataFrameGroupBy.agg`, :meth:`.SeriesGroupBy.agg`, :meth:`.SeriesGroupBy.apply`, :meth:`.DataFrameGroupBy.apply` now support ``kurt`` (:issue:`40139`)
Expand Down Expand Up @@ -232,7 +233,6 @@ Other enhancements
- Support reading Stata 102-format (Stata 1) dta files (:issue:`58978`)
- Support reading Stata 110-format (Stata 7) dta files (:issue:`47176`)
- Switched wheel upload to **PyPI Trusted Publishing** (OIDC) for release-tag pushes in ``wheels.yml``. (:issue:`61718`)
-

.. ---------------------------------------------------------------------------
.. _whatsnew_300.notable_bug_fixes:
Expand Down
5 changes: 5 additions & 0 deletions pandas/core/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -2180,6 +2180,7 @@ def to_excel(
freeze_panes: tuple[int, int] | None = None,
storage_options: StorageOptions | None = None,
engine_kwargs: dict[str, Any] | None = None,
autofilter: bool = False,
) -> None:
"""
Write {klass} to an Excel sheet.
Expand Down Expand Up @@ -2240,6 +2241,9 @@ def to_excel(

.. versionadded:: {storage_options_versionadded}
{extra_parameters}
autofilter : bool, default False
If True, add automatic filters to all columns.

See Also
--------
to_csv : Write DataFrame to a comma-separated values (csv) file.
Expand Down Expand Up @@ -2312,6 +2316,7 @@ def to_excel(
index_label=index_label,
merge_cells=merge_cells,
inf_rep=inf_rep,
autofilter=autofilter,
)
formatter.write(
excel_writer,
Expand Down
3 changes: 3 additions & 0 deletions pandas/io/excel/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1209,6 +1209,7 @@ def _write_cells(
startrow: int = 0,
startcol: int = 0,
freeze_panes: tuple[int, int] | None = None,
autofilter_range: str | None = None,
) -> None:
"""
Write given formatted cells into Excel an excel sheet
Expand All @@ -1223,6 +1224,8 @@ def _write_cells(
startcol : upper left cell column to dump data frame
freeze_panes: int tuple of length 2
contains the bottom-most row and right-most column to freeze
autofilter_range: str, default None
column ranges to add automatic filters to, for example "A1:D5"
"""
raise NotImplementedError

Expand Down
1 change: 1 addition & 0 deletions pandas/io/excel/_odswriter.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ def _write_cells(
startrow: int = 0,
startcol: int = 0,
freeze_panes: tuple[int, int] | None = None,
autofilter_range: str | None = None,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to me we should be raising an error here if a user specifies autofilter=True and it is not supported with an engine rather than silently ignore the user's specification.

) -> None:
"""
Write the frame cells using odf
Expand Down
4 changes: 4 additions & 0 deletions pandas/io/excel/_openpyxl.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,7 @@ def _write_cells(
startrow: int = 0,
startcol: int = 0,
freeze_panes: tuple[int, int] | None = None,
autofilter_range: str | None = None,
) -> None:
# Write the frame cells using openpyxl.
sheet_name = self._get_sheet_name(sheet_name)
Expand Down Expand Up @@ -532,6 +533,9 @@ def _write_cells(
for k, v in style_kwargs.items():
setattr(xcell, k, v)

if autofilter_range:
wks.auto_filter.ref = autofilter_range


class OpenpyxlReader(BaseExcelReader["Workbook"]):
@doc(storage_options=_shared_docs["storage_options"])
Expand Down
4 changes: 4 additions & 0 deletions pandas/io/excel/_xlsxwriter.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ def _write_cells(
startrow: int = 0,
startcol: int = 0,
freeze_panes: tuple[int, int] | None = None,
autofilter_range: str | None = None,
) -> None:
# Write the frame cells using xlsxwriter.
sheet_name = self._get_sheet_name(sheet_name)
Expand Down Expand Up @@ -282,3 +283,6 @@ def _write_cells(
)
else:
wks.write(startrow + cell.row, startcol + cell.col, val, style)

if autofilter_range:
wks.autofilter(autofilter_range)
40 changes: 40 additions & 0 deletions pandas/io/formats/excel.py
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,8 @@ class ExcelFormatter:
Defaults to ``CSSToExcelConverter()``.
It should have signature css_declarations string -> excel style.
This is only called for body cells.
autofilter : bool, default False
If True, add automatic filters to all columns
"""

max_rows = 2**20
Expand All @@ -549,6 +551,7 @@ def __init__(
merge_cells: ExcelWriterMergeCells = False,
inf_rep: str = "inf",
style_converter: Callable | None = None,
autofilter: bool = False,
) -> None:
self.rowcounter = 0
self.na_rep = na_rep
Expand Down Expand Up @@ -584,6 +587,7 @@ def __init__(
raise ValueError(f"Unexpected value for {merge_cells=}.")
self.merge_cells = merge_cells
self.inf_rep = inf_rep
self.autofilter = autofilter

def _format_value(self, val):
if is_scalar(val) and missing.isna(val):
Expand Down Expand Up @@ -873,6 +877,34 @@ def get_formatted_cells(self) -> Iterable[ExcelCell]:
cell.val = self._format_value(cell.val)
yield cell

def _num2excel(self, index: int) -> str:
"""
Convert 0-based column index to Excel column name.

Parameters
----------
index : int
The numeric column index to convert to a Excel column name.

Returns
-------
column_name : str
The column name corresponding to the index.

Raises
------
ValueError
Index is negative
"""
if index < 0:
raise ValueError(f"Index cannot be negative: {index}")
column_name = ""
# while loop in case column name needs to be longer than 1 character
while index > 0 or not column_name:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you raise when index < 0? Also, what's the point of this being a while loop?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ignore my while loop comment - I supposed its so you can build column references like AA, BA, etc...

So makes sense, but still in need of a cleanup here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@WillAyd Thanks for the feedback - input validation added.

index, remainder = divmod(index, 26)
column_name = chr(65 + remainder) + column_name
return column_name

@doc(storage_options=_shared_docs["storage_options"])
def write(
self,
Expand Down Expand Up @@ -916,6 +948,13 @@ def write(
f"Max sheet size is: {self.max_rows}, {self.max_cols}"
)

if self.autofilter:
start = f"{self._num2excel(startcol)}{startrow + 1}"
end = f"{self._num2excel(startcol + num_cols)}{startrow + num_rows + 1}"
autofilter_range = f"{start}:{end}"
else:
autofilter_range = None

if engine_kwargs is None:
engine_kwargs = {}

Expand All @@ -938,6 +977,7 @@ def write(
startrow=startrow,
startcol=startcol,
freeze_panes=freeze_panes,
autofilter_range=autofilter_range,
)
finally:
# make sure to close opened file handles
Expand Down
2 changes: 2 additions & 0 deletions pandas/io/formats/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,7 @@ def to_excel(
verbose: bool = True,
freeze_panes: tuple[int, int] | None = None,
storage_options: StorageOptions | None = None,
autofilter: bool = False,
) -> None:
from pandas.io.formats.excel import ExcelFormatter

Expand All @@ -606,6 +607,7 @@ def to_excel(
index_label=index_label,
merge_cells=merge_cells,
inf_rep=inf_rep,
autofilter=autofilter,
)
formatter.write(
excel_writer,
Expand Down
17 changes: 17 additions & 0 deletions pandas/tests/io/excel/test_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,3 +350,20 @@ def test_format_hierarchical_rows_periodindex(merge_cells):
assert isinstance(cell.val, Timestamp), (
"Period should be converted to Timestamp"
)


@pytest.mark.parametrize("engine", ["xlsxwriter", "openpyxl"])
def test_autofilter(engine, tmp_excel):
# GH 61194
df = DataFrame.from_dict([{"A": 1, "B": 2, "C": 3}, {"A": 4, "B": 5, "C": 6}])

with ExcelWriter(tmp_excel, engine=engine) as writer:
df.to_excel(writer, autofilter=True, index=False)

openpyxl = pytest.importorskip("openpyxl") # test loading only with openpyxl
with contextlib.closing(openpyxl.load_workbook(tmp_excel)) as wb:
ws = wb.active

assert ws.auto_filter.ref is not None
print(ws.auto_filter.ref)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you remove the print statement?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Print statement removed - thanks for checking!

assert ws.auto_filter.ref == "A1:D3"
Loading