Repository: narwhals-dev/narwhals
Pull request #397 (2024-07-04 10:12:02)

fix: raise if complex pandas aggregation would return empty results
fixes the example from https://github.com/narwhals-dev/narwhals/issues/282#issuecomment-2208586002

<!--
# Thanks for contributing a pull request! 
## Please make sure you see our contribution guidelines: https://github.com/narwhals-dev/narwhals/blob/main/CONTRIBUTING.md
-->

## What type of PR is this? (check all applicable)

- [ ] 💾 Refactor
- [ ] ✨ Feature
- [ ] 🐛 Bug Fix
- [ ] 🔧 Optimization
- [ ] 📝 Documentation
- [ ] ✅ Test
- [ ] 🐳 Other

## Related issues 

- Related issue # 
- Closes #

## Checklist

- [ ] Code follows style guide (ruff)
- [ ] Tests added 
- [ ] Documented the changes

## If you have comments or can explain your changes, please do so below.




Patch:
diff --git a/narwhals/_pandas_like/group_by.py b/narwhals/_pandas_like/group_by.py
index 2422df8a..e2ac36c6 100644
--- a/narwhals/_pandas_like/group_by.py
+++ b/narwhals/_pandas_like/group_by.py
@@ -65,6 +65,7 @@ class PandasGroupBy:
             output_names,
             self._from_dataframe,
             implementation,
+            dataframe_is_empty=self._df._dataframe.empty,
         )
 
     def _from_dataframe(self, df: PandasDataFrame) -> PandasDataFrame:
@@ -86,6 +87,8 @@ def agg_pandas(  # noqa: PLR0913
     output_names: list[str],
     from_dataframe: Callable[[Any], PandasDataFrame],
     implementation: Any,
+    *,
+    dataframe_is_empty: bool,
 ) -> PandasDataFrame:
     """
     This should be the fastpath, but cuDF is too far behind to use it.
@@ -140,6 +143,20 @@ def agg_pandas(  # noqa: PLR0913
         result_simple = result_simple.rename(columns=name_mapping).reset_index()
         return from_dataframe(result_simple.loc[:, output_names])
 
+    if dataframe_is_empty:
+        # Don't even attempt this, it's way too inconsistent across pandas versions.
+        msg = (
+            "No results for group-by aggregation.\n\n"
+            "Hint: you were probably trying to apply a non-elementary aggregation with a "
+            "pandas-like API.\n"
+            "Please rewrite your query such that group-by aggregations "
+            "are elementary. For example, instead of:\n\n"
+            "    df.group_by('a').agg(nw.col('b').round(2).mean())\n\n"
+            "use:\n\n"
+            "    df.with_columns(nw.col('b').round(2)).group_by('a').agg(nw.col('b').mean())\n\n"
+        )
+        raise ValueError(msg)
+
     warnings.warn(
         "Found complex group-by expression, which can't be expressed efficiently with the "
         "pandas API. If you can, please rewrite your query such that group-by aggregations "
@@ -171,4 +188,5 @@ def agg_pandas(  # noqa: PLR0913
         result_complex = grouped.apply(func)
 
     result = result_complex.reset_index()
+
     return from_dataframe(result.loc[:, output_names])


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 9a2fa2f3..00b9274c 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -52,7 +52,7 @@ def polars_constructor(obj: Any) -> IntoDataFrame:
     return pl.DataFrame(obj)
 
 
-if parse_version(pd.__version__) >= parse_version("1.5.0"):
+if parse_version(pd.__version__) >= parse_version("2.0.0"):
     params = [pandas_constructor, pandas_nullable_constructor, pandas_pyarrow_constructor]
 else:  # pragma: no cover
     params = [pandas_constructor]
@@ -94,7 +94,7 @@ def polars_series_constructor(obj: Any) -> Any:
     return pl.Series(obj)
 
 
-if parse_version(pd.__version__) >= parse_version("1.5.0"):
+if parse_version(pd.__version__) >= parse_version("2.0.0"):
     params_series = [
         pandas_series_constructor,
         pandas_series_nullable_constructor,
diff --git a/tests/test_group_by.py b/tests/test_group_by.py
index 12fbaa42..7c3f4410 100644
--- a/tests/test_group_by.py
+++ b/tests/test_group_by.py
@@ -70,3 +70,12 @@ def test_group_by_len(constructor: Any) -> None:
     )
     expected = {"a": [1, 3], "b": [2, 1]}
     compare_dicts(result, expected)
+
+
+def test_group_by_empty_result_pandas() -> None:
+    df_any = pd.DataFrame({"a": [1, 2, 3], "b": [4, 3, 2]})
+    df = nw.from_native(df_any, eager_only=True)
+    with pytest.raises(ValueError, match="No results"):
+        df.filter(nw.col("a") < 0).group_by("a").agg(
+            nw.col("b").sum().round(2).alias("c")
+        )