Repository: ibis-project/ibis

Issue:
perf(pandas): `Alias` executes its children twice 
On the Pandas backend, `Alias` executes the expression it is renaming (and the tree under it) twice

See [this notebook](https://gist.github.com/timothydijamco/b3d6e1cceb1e6f3fb9cf7ddb8d1db7eb)


Discussion:
@timothydijamco We're going to release 3.1 tomorrow (July 22, 2022) so I don't think a fix for this will make it in! However, definitely for 4.0.0 or perhaps a bugfix 3.1.1 in between.
Given the large changes for 4.0, we'd strongly prefer this in a 3.1.1 patch version if possible. 
@gerrymanoim That makes sense! Happy to review PRs for this. Is this something that's high priority for y'all?
thanks! It's somewhat significant for us. I think I have a fix and can open a PR this week.

I believe this just needs to be changed to `return data` (`data` being the second argument) but I'll check it against the Ibis tests and add a re-execution test
https://github.com/ibis-project/ibis/blob/00c83e6912263d58cbf04d7904bc0e96ccb03af0/ibis/backends/pandas/execution/generic.py#L926-L930
@timothydijamco Sounds good! I'll review the PR as soon as it's up.
Thanks! We're doing another round of updates/patches, my main point was that we want to get these fixes relating to 3.0->3.1 in before we try to tackle the breaking changes slated for 4.0. 
Yep that makes sense. We'll do a 3.2 release before 4.0 lands.
@gerrymanoim We'd like to release this week. Any chance of getting this fix in for 3.2? Otherwise, we'll have to leave it for 4.0.
@timothydijamco if u have a chance to do this

@gerrymanoim is out of town
@jreback Thanks!

Resolving patch:
diff --git a/ibis/backends/pandas/execution/generic.py b/ibis/backends/pandas/execution/generic.py
--- a/ibis/backends/pandas/execution/generic.py
+++ b/ibis/backends/pandas/execution/generic.py
@@ -989,10 +989,10 @@ def execute_node_self_reference_dataframe(op, data, **kwargs):
 
 
 @execute_node.register(ops.Alias, object)
-def execute_alias(op, _, **kwargs):
-    # just compile the underlying argument because the naming is handled
+def execute_alias(op, data, **kwargs):
+    # just return the underlying argument because the naming is handled
     # by the translator for the top level expression
-    return execute(op.arg, **kwargs)
+    return data
 
 
 @execute_node.register(ops.ValueList, collections.abc.Sequence)


Test patch:
diff --git a/ibis/backends/pandas/tests/execution/test_window.py b/ibis/backends/pandas/tests/execution/test_window.py
--- a/ibis/backends/pandas/tests/execution/test_window.py
+++ b/ibis/backends/pandas/tests/execution/test_window.py
@@ -612,10 +612,7 @@ def test_pre_execute(op, client, **kwargs):
     # once in window op at the top to pickup any scope changes before computing
     # twice in window op when calling execute on the ops.Lag node at the
     # beginning of execute and once before the actual computation
-    #
-    # this process happens twice because of the pre_execute call on the Alias
-    # operation
-    assert called[0] == 3 + 3
+    assert called[0] == 3
 
 
 def test_window_grouping_key_has_scope(t, df):
diff --git a/ibis/backends/pandas/tests/test_udf.py b/ibis/backends/pandas/tests/test_udf.py
--- a/ibis/backends/pandas/tests/test_udf.py
+++ b/ibis/backends/pandas/tests/test_udf.py
@@ -237,6 +237,21 @@ def error_udf(s):
         error_udf(t.c).execute()
 
 
+def test_udf_no_reexecution(t2):
+    execution_count = 0
+
+    @udf.elementwise(input_type=[dt.double], output_type=dt.double)
+    def times_two_count_executions(x):
+        nonlocal execution_count
+        execution_count += 1
+        return x * 2.0
+
+    expr = t2.mutate(doubled=times_two_count_executions(t2.a))
+    expr.execute()
+
+    assert execution_count == 1
+
+
 def test_compose_udfs(t2, df2):
     expr = times_two(add_one(t2.a))
     result = expr.execute()