Repository navigation
feat: Add maintain_order to DataFrame.group_by
#4042
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ | |
| from narwhals._exceptions import issue_warning | ||
| from narwhals._expression_parsing import evaluate_output_names_and_aliases | ||
| from narwhals._pandas_like.utils import make_group_by_kwargs | ||
| from narwhals._utils import generate_temporary_column_name | ||
| from narwhals.dependencies import is_pandas_like_dataframe | ||
|
|
||
| if TYPE_CHECKING: | ||
|
|
@@ -262,6 +263,7 @@ def __init__( | |
| /, | ||
| *, | ||
| drop_null_keys: bool, | ||
| maintain_order: bool = False, | ||
| ) -> None: | ||
| self._original_columns = tuple(df.columns) | ||
| self._drop_null_keys = drop_null_keys | ||
|
|
@@ -270,14 +272,19 @@ def __init__( | |
| ) | ||
| self._exclude: tuple[str, ...] = (*self._keys, *self._output_key_names) | ||
| self._group_by_kwargs = make_group_by_kwargs(drop_null_keys=drop_null_keys) | ||
| self._row_index = ( | ||
| generate_temporary_column_name(n_bytes=8, columns=self.compliant.columns) | ||
| if maintain_order | ||
| else None | ||
| ) | ||
|
|
||
| # Drop index to avoid potential collisions: | ||
| # https://github.com/narwhals-dev/narwhals/issues/1907. | ||
| self._native = self.compliant.native | ||
| if set(self._native.index.names).intersection(self.compliant.columns): | ||
| self._native = self._native.reset_index(drop=True) | ||
|
|
||
| def agg(self, *exprs: PandasLikeExpr) -> PandasLikeDataFrame: # noqa: PLR0912 | ||
| def agg(self, *exprs: PandasLikeExpr) -> PandasLikeDataFrame: | ||
| all_aggs_are_simple = True | ||
| agg_exprs: list[AggExpr] = [] | ||
| order_by = () | ||
|
|
@@ -292,13 +299,7 @@ def agg(self, *exprs: PandasLikeExpr) -> PandasLikeDataFrame: # noqa: PLR0912 | |
| raise NotImplementedError(msg) | ||
| order_by = _current_order_by | ||
|
|
||
| if order_by: | ||
| grouped: NativeGroupBy = self._native.sort_values( | ||
| list(order_by), na_position="first" | ||
| ).groupby(self._keys.copy(), **self._group_by_kwargs) | ||
| else: | ||
| grouped = self._native.groupby(self._keys.copy(), **self._group_by_kwargs) | ||
| self._grouped = grouped | ||
| grouped = self._grouped = self._configure_grouped(order_by) | ||
|
|
||
| if all_aggs_are_simple: | ||
| result: pd.DataFrame | ||
|
|
@@ -314,6 +315,9 @@ def agg(self, *exprs: PandasLikeExpr) -> PandasLikeDataFrame: # noqa: PLR0912 | |
| else: | ||
| result = self._apply_aggs(grouped, exprs) | ||
|
|
||
| if order_by and self._row_index is not None: | ||
| result = self._restore_order(result, grouped, self._row_index) | ||
|
Comment on lines
+318
to
+319
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Restore group order when With 🤖 Prompt for AI Agents |
||
|
|
||
| impl = self.compliant._implementation | ||
| backend_version = impl._backend_version() | ||
| if impl.is_pandas() and backend_version < (3, 0): # pragma: no cover | ||
|
|
@@ -324,6 +328,32 @@ def agg(self, *exprs: PandasLikeExpr) -> PandasLikeDataFrame: # noqa: PLR0912 | |
|
|
||
| return self._select_results(result, agg_exprs) | ||
|
|
||
| def _configure_grouped(self, order_by: Sequence[str]) -> NativeGroupBy: | ||
| if not order_by: | ||
| return self._native.groupby(self._keys.copy(), **self._group_by_kwargs) | ||
| native = self._native | ||
| if self._row_index is not None: | ||
| # With `sort=False`, pandas returns groups in order of first appearance, | ||
| # but sorting by `order_by` changes which rows appear first. | ||
| native = ( | ||
| self.compliant._with_native(native, validate_column_names=False) | ||
| .with_row_index(self._row_index, order_by=None) | ||
| .native | ||
| ) | ||
| return native.sort_values(list(order_by), na_position="first").groupby( | ||
| self._keys.copy(), **self._group_by_kwargs | ||
| ) | ||
|
|
||
| def _restore_order( | ||
| self, result: pd.DataFrame, grouped: NativeGroupBy, row_index: str | ||
| ) -> pd.DataFrame: | ||
| # Align on the group keys rather than position: e.g. `last` returns its | ||
| # rows in order of last appearance. | ||
| ns = self.compliant.__narwhals_namespace__() | ||
| first_seen = grouped[row_index].min() | ||
| columns = list(result.columns) | ||
| return ns._concat_by_index([result, first_seen]).sort_values(row_index)[columns] | ||
|
|
||
| def _select_results( | ||
| self, df: pd.DataFrame, /, agg_exprs: Sequence[AggExpr] | ||
| ) -> PandasLikeDataFrame: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: narwhals-dev/narwhals
Length of output: 5742
🏁 Script executed:
Repository: narwhals-dev/narwhals
Length of output: 8627
🌐 Web query:
site:pandas.pydata.org/pandas-docs/version/1.5.3/reference/api/pandas.DataFrame.groupby.html sort=False ordered categorical group order first appearance category order💡 Result:
🏁 Script executed:
Repository: narwhals-dev/narwhals
Length of output: 9469
🏁 Script executed:
Repository: narwhals-dev/narwhals
Length of output: 940
🏁 Script executed:
Repository: narwhals-dev/narwhals
Length of output: 12776
Restore first-appearance order for categorical iteration.
When
maintain_order=Trueon pandas versions before 2.0,groupby(sort=False)can order an ordered categorical key by category order. For["b", "a", "b"]with categories["a", "b"],PandasLikeGroupBy.__iter__can therefore yield"a"before"b". The iterator groups the native frame directly and does not use the temporary row index. Attach that index before grouping, then order the yielded groups by its minimum position on these pandas versions.🐛 Suggested fix
🤖 Prompt for AI Agents