Skip to content

PERF: operate on arrays instead of Series in DataFrame/DataFrame ops #33561

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

Merged

Conversation

jorisvandenbossche
Copy link
Member

xref #32779

@jbrockmendel
Copy link
Member

While you're doing this, want to get the other .iloc[:, i] cases within dispatch_to_series?

@jorisvandenbossche
Copy link
Member Author

Yes, will do.

(note, I would also be fine with you including this in #33561, if we decide to have a switch between column/blockwise. But it's mainly because you seemed quite hesitant to include something like this, that I thought to do this PR separately)


else:
# Remaining cases have less-obvious dispatch rules
raise NotImplementedError(right)

new_data = expressions.evaluate(column_op, str_rep, left, right)
Copy link
Member

Choose a reason for hiding this comment

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

does this have a perf impact? IIRC a while back I tried removing it because I thought it shouldn't, but found that it did

Copy link
Member Author

Choose a reason for hiding this comment

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

As far as I understand, the get_array_op should take care of this?

For example, the arrays_ops.py::na_arithmetic_op also calls expressions.evaluate to perform the actual op.

Copy link
Member

Choose a reason for hiding this comment

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

That was what i thought too, and I could be remembering incorrectly. But this is worth double-checking.


def column_op(a, b):
return {i: func(a.iloc[:, i], b.iloc[i]) for i in range(len(a.columns))}
Copy link
Member

Choose a reason for hiding this comment

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

doesnt need to be in this PR, but since you're attention is already focused on this line: this can be incorrect if b is e.g. Categorical, since b.iloc[i] will have the scalar behavior instead of the Categorical (usually raising) behavior. (This is also a place where NaT causes issues, and the idea of only-one-NA causes me nightmares)

The main idea I've had here is to use b.iloc[[i]] instead of b.iloc[i], haven't gotten around to implementing it. Does this change make it easier to address the issue?

Copy link
Member Author

Choose a reason for hiding this comment

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

when using something like b.iloc[[i]], you would need to handle broadcasting here as well (since the array ops expect either same length array or scalar). So since that is a pre-existing issue, I would leave that for a dedicated PR.
Is there an issue for this?

Copy link
Member

Choose a reason for hiding this comment

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

I dont think there's an issue for this. If I had found one, i would have added the Numeric label

@jorisvandenbossche jorisvandenbossche added the Performance Memory or execution speed performance label Apr 17, 2020
@jorisvandenbossche jorisvandenbossche added this to the 1.1 milestone Apr 17, 2020
@jreback
Copy link
Contributor

jreback commented Apr 17, 2020

can you point to some asv's that change with this? (nice cleanup anyhow)

@jorisvandenbossche
Copy link
Member Author

jorisvandenbossche commented May 22, 2020

can you point to some asv's that change with this? (nice cleanup anyhow)

There is already a MixedFrameWithSeriesAxis benchmark that captures this partly:

Running that benchmark specifically, on master:

[100.00%] ··· arithmetic.MixedFrameWithSeriesAxis.time_frame_op_with_series_axis1                                                                                                                                ok
[100.00%] ··· ========== ==========
                opname             
              ---------- ----------
                  eq      194±4ms  
                  ne      191±3ms  
                  lt      189±3ms  
                  le      190±4ms  
                  ge      209±50ms 
                  gt      225±20ms 
                 add      4.31±1ms 
                 sub      4.68±1ms 
               truediv    4.64±1ms 
               floordiv   54.8±2ms 
                 mul      4.73±1ms 
                 pow      15.4±2ms 
              ========== ==========

and with this branch:

[100.00%] ··· arithmetic.MixedFrameWithSeriesAxis.time_frame_op_with_series_axis1                                                                                                                                ok
[100.00%] ··· ========== ============
                opname               
              ---------- ------------
                  eq       68.5±9ms  
                  ne       70.3±9ms  
                  lt       62.7±6ms  
                  le      68.1±10ms  
                  ge       70.5±8ms  
                  gt       70.7±9ms  
                 add       4.44±1ms  
                 sub       5.52±1ms  
               truediv    5.45±0.8ms 
               floordiv    60.7±3ms  
                 mul       4.42±1ms  
                 pow       15.1±4ms  
              ========== ============

The comparison ops take the code path I changed here, and those show a consistent speed-up (the arithmetic ones actually take a different code path)


else:
# Remaining cases have less-obvious dispatch rules
raise NotImplementedError(right)

new_data = expressions.evaluate(column_op, str_rep, left, right)
return new_data
return type(left)._from_arrays(
Copy link
Member

Choose a reason for hiding this comment

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

i think you can just return arrays here, since that will be passed to left._construct_result (though the docstring does say DataFrame, i guess thats wrong ATM)

Copy link
Member Author

Choose a reason for hiding this comment

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

I want to explicitly use _from_arrays, as that is part of the reason that makes this PR a perf improvement (the _from_arrays constructor specifically handles the case of a list of arrays corresponding to columns). Also, the default DataFrame constructor handles a list of arrays differently.

Copy link
Member

Choose a reason for hiding this comment

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

sounds good

@jorisvandenbossche jorisvandenbossche merged commit 64859ec into pandas-dev:master May 25, 2020
@jorisvandenbossche jorisvandenbossche deleted the perf-frame-op-array branch May 25, 2020 14:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Performance Memory or execution speed performance
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants