Repository: ibis-project/ibis

Issue:
Clean up potentially unreachable code in Window.agg (Pandas backend)
There is logic in `Window.agg` (in the Pandas backend) that I believe is unreachable:
https://github.com/ibis-project/ibis/blob/master/ibis/pandas/aggcontext.py#L459-L483

If `group_by` and `order_by` are both `None`, then a `Window` aggcontext **should not exist in the first place** because of this upstream logic that creates a `Summarize` aggcontext instead of a `Window` aggcontext:
https://github.com/ibis-project/ibis/blob/4fc17ab801b8d1b0b02a25551b4753247d837dfd/ibis/pandas/execution/window.py#L132-L133

It is definitely not reached by the tests, so possibly contains bugs.

We need to:
1. Determine whether it really is unreachable or not
2. If reachable, add tests for it and possibly fix broken logic
3. If unreachable, remove


Discussion:
This is logic that was split out into `Summarize` and `Transform` but never deleted. PR inbound.

Resolving patch:
diff --git a/ibis/backends/pandas/aggcontext.py b/ibis/backends/pandas/aggcontext.py
--- a/ibis/backends/pandas/aggcontext.py
+++ b/ibis/backends/pandas/aggcontext.py
@@ -574,121 +574,94 @@ def agg(
         group_by = self.group_by
         order_by = self.order_by
 
-        # if we don't have a grouping key, just call into pandas
-        if not group_by and not order_by:
-            # the result of calling .rolling(...) in pandas
-            windowed = self.construct_window(grouped_data)
-
-            # if we're a UD(A)F or a function that isn't a string (like the
-            # collect implementation) then call apply
-            if callable(function):
-                return windowed.apply(
-                    wrap_for_apply(function, args, kwargs), raw=True
-                )
-            else:
-                # otherwise we're a string and probably faster
-                assert isinstance(function, str)
-                method = getattr(windowed, function, None)
-                if method is not None:
-                    return method(*args, **kwargs)
-
-                # handle the case where we pulled out a name from an operation
-                # but it doesn't actually exist
-                return windowed.apply(
-                    wrap_for_apply(
-                        operator.methodcaller(function, *args, **kwargs)
-                    ),
-                    raw=True,
-                )
+        assert group_by or order_by
+
+        # Get the DataFrame from which the operand originated
+        # (passed in when constructing this context object in
+        # execute_node(ops.WindowOp))
+        parent = self.parent
+        frame = getattr(parent, 'obj', parent)
+        obj = getattr(grouped_data, 'obj', grouped_data)
+        name = obj.name
+        if frame[name] is not obj or name in group_by or name in order_by:
+            name = f"{name}_{ibis.util.guid()}"
+            frame = frame.assign(**{name: obj})
+
+        # set the index to our order_by keys and append it to the existing
+        # index
+        # TODO: see if we can do this in the caller, when the context
+        # is constructed rather than pulling out the data
+        columns = group_by + order_by + [name]
+        # Create a new frame to avoid mutating the original one
+        indexed_by_ordering = frame[columns].copy()
+        # placeholder column to compute window_sizes below
+        indexed_by_ordering['_placeholder'] = 0
+        indexed_by_ordering = indexed_by_ordering.set_index(order_by)
+
+        # regroup if needed
+        if group_by:
+            grouped_frame = indexed_by_ordering.groupby(group_by)
         else:
-            # Get the DataFrame from which the operand originated
-            # (passed in when constructing this context object in
-            # execute_node(ops.WindowOp))
-            parent = self.parent
-            frame = getattr(parent, 'obj', parent)
-            obj = getattr(grouped_data, 'obj', grouped_data)
-            name = obj.name
-            if frame[name] is not obj or name in group_by or name in order_by:
-                name = f"{name}_{ibis.util.guid()}"
-                frame = frame.assign(**{name: obj})
-
-            # set the index to our order_by keys and append it to the existing
-            # index
-            # TODO: see if we can do this in the caller, when the context
-            # is constructed rather than pulling out the data
-            columns = group_by + order_by + [name]
-            # Create a new frame to avoid mutating the original one
-            indexed_by_ordering = frame[columns].copy()
-            # placeholder column to compute window_sizes below
-            indexed_by_ordering['_placeholder'] = 0
-            indexed_by_ordering = indexed_by_ordering.set_index(order_by)
-
-            # regroup if needed
-            if group_by:
-                grouped_frame = indexed_by_ordering.groupby(group_by)
-            else:
-                grouped_frame = indexed_by_ordering
-            grouped = grouped_frame[name]
-
-            if callable(function):
-                # To compute the window_size, we need to contruct a
-                # RollingGroupby and compute count using construct_window.
-                # However, if the RollingGroupby is not numeric, e.g.,
-                # we are calling window UDF on a timestamp column, we
-                # cannot compute rolling count directly because:
-                # (1) windowed.count() will exclude NaN observations
-                #     , which results in incorrect window sizes.
-                # (2) windowed.apply(len, raw=True) will include NaN
-                #     obversations, but doesn't work on non-numeric types.
-                #     https://github.com/pandas-dev/pandas/issues/23002
-                # To deal with this, we create a _placeholder column
-
-                windowed_frame = self.construct_window(grouped_frame)
-                window_sizes = (
-                    windowed_frame['_placeholder']
-                    .count()
-                    .reset_index(drop=True)
-                )
-                mask = ~(window_sizes.isna())
-                window_upper_indices = pd.Series(range(len(window_sizes))) + 1
-                window_lower_indices = window_upper_indices - window_sizes
-                # The result Series of udf may need to be trimmed by
-                # timecontext. In order to do so, 'time' must be added
-                # as an index to the Series, if present. Here We extract
-                # time column from the parent Dataframe `frame`.
-                if get_time_col() in frame:
-                    result_index = construct_time_context_aware_series(
-                        obj, frame
-                    ).index
-                else:
-                    result_index = obj.index
-                result = window_agg_udf(
-                    grouped_data,
-                    function,
-                    window_lower_indices,
-                    window_upper_indices,
-                    mask,
-                    result_index,
-                    self.dtype,
-                    self.max_lookback,
-                    *args,
-                    **kwargs,
-                )
+            grouped_frame = indexed_by_ordering
+        grouped = grouped_frame[name]
+
+        if callable(function):
+            # To compute the window_size, we need to contruct a
+            # RollingGroupby and compute count using construct_window.
+            # However, if the RollingGroupby is not numeric, e.g.,
+            # we are calling window UDF on a timestamp column, we
+            # cannot compute rolling count directly because:
+            # (1) windowed.count() will exclude NaN observations
+            #     , which results in incorrect window sizes.
+            # (2) windowed.apply(len, raw=True) will include NaN
+            #     obversations, but doesn't work on non-numeric types.
+            #     https://github.com/pandas-dev/pandas/issues/23002
+            # To deal with this, we create a _placeholder column
+
+            windowed_frame = self.construct_window(grouped_frame)
+            window_sizes = (
+                windowed_frame['_placeholder'].count().reset_index(drop=True)
+            )
+            mask = ~(window_sizes.isna())
+            window_upper_indices = pd.Series(range(len(window_sizes))) + 1
+            window_lower_indices = window_upper_indices - window_sizes
+            # The result Series of udf may need to be trimmed by
+            # timecontext. In order to do so, 'time' must be added
+            # as an index to the Series, if present. Here We extract
+            # time column from the parent Dataframe `frame`.
+            if get_time_col() in frame:
+                result_index = construct_time_context_aware_series(
+                    obj, frame
+                ).index
             else:
-                # perform the per-group rolling operation
-                windowed = self.construct_window(grouped)
-                result = window_agg_built_in(
-                    frame,
-                    windowed,
-                    function,
-                    self.max_lookback,
-                    *args,
-                    **kwargs,
-                )
-            try:
-                return result.astype(self.dtype, copy=False)
-            except (TypeError, ValueError):
-                return result
+                result_index = obj.index
+            result = window_agg_udf(
+                grouped_data,
+                function,
+                window_lower_indices,
+                window_upper_indices,
+                mask,
+                result_index,
+                self.dtype,
+                self.max_lookback,
+                *args,
+                **kwargs,
+            )
+        else:
+            # perform the per-group rolling operation
+            windowed = self.construct_window(grouped)
+            result = window_agg_built_in(
+                frame,
+                windowed,
+                function,
+                self.max_lookback,
+                *args,
+                **kwargs,
+            )
+        try:
+            return result.astype(self.dtype, copy=False)
+        except (TypeError, ValueError):
+            return result
 
 
 class Cumulative(Window):


Test patch: