Skip to content

PERF: block-wise arithmetic for frame-with-frame #32779

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
merged 63 commits into from
May 19, 2020
Merged
Show file tree
Hide file tree
Changes from 51 commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
1697252
PERF: block-wise arithmetic for frame-with-frame
jbrockmendel Mar 17, 2020
a7764d6
Merge branch 'master' of https://github.com/pandas-dev/pandas into pe…
jbrockmendel Mar 17, 2020
30a836d
lint fixup
jbrockmendel Mar 17, 2020
3559698
Merge branch 'master' of https://github.com/pandas-dev/pandas into pe…
jbrockmendel Mar 18, 2020
4334353
troubleshoot npdev build
jbrockmendel Mar 18, 2020
cb40b0c
Merge branch 'master' of https://github.com/pandas-dev/pandas into pe…
jbrockmendel Mar 24, 2020
713a776
comment
jbrockmendel Mar 25, 2020
95ef3ad
checkpoint passing
jbrockmendel Mar 25, 2020
61e5cd6
checkpoint passing
jbrockmendel Mar 25, 2020
89c3d7b
refactor
jbrockmendel Mar 25, 2020
e348e46
blackify
jbrockmendel Mar 25, 2020
519c757
Merge branch 'master' of https://github.com/pandas-dev/pandas into pe…
jbrockmendel Mar 31, 2020
2b1ba18
disable assertions for perf
jbrockmendel Mar 31, 2020
53e93fc
Merge branch 'master' of https://github.com/pandas-dev/pandas into pe…
jbrockmendel Apr 1, 2020
91c86a3
asv
jbrockmendel Apr 1, 2020
2034084
whatsnew
jbrockmendel Apr 1, 2020
8aedf35
Merge branch 'master' of https://github.com/pandas-dev/pandas into pe…
jbrockmendel Apr 3, 2020
0c12d35
revert warning suppression
jbrockmendel Apr 3, 2020
9727562
Fixupm indentation
jbrockmendel Apr 3, 2020
6661dd3
Merge branch 'master' of https://github.com/pandas-dev/pandas into pe…
jbrockmendel Apr 4, 2020
42bbbf3
suppress warning
jbrockmendel Apr 4, 2020
65ab023
Merge branch 'master' of https://github.com/pandas-dev/pandas into pe…
jbrockmendel Apr 6, 2020
0d958a3
update asv
jbrockmendel Apr 6, 2020
7f91e74
Merge branch 'master' of https://github.com/pandas-dev/pandas into pe…
jbrockmendel Apr 6, 2020
56eef51
_data->_mgr
jbrockmendel Apr 6, 2020
4baea6f
Merge branch 'master' of https://github.com/pandas-dev/pandas into pe…
jbrockmendel Apr 7, 2020
41a4e7a
Merge branch 'master' of https://github.com/pandas-dev/pandas into pe…
jbrockmendel Apr 7, 2020
b23144e
Merge branch 'master' of https://github.com/pandas-dev/pandas into pe…
jbrockmendel Apr 9, 2020
7f24d57
Merge branch 'master' of https://github.com/pandas-dev/pandas into pe…
jbrockmendel Apr 9, 2020
ae744b7
update to use faspath constructor
jbrockmendel Apr 9, 2020
b14a98c
Merge branch 'master' of https://github.com/pandas-dev/pandas into pe…
jbrockmendel Apr 10, 2020
f42c403
update import
jbrockmendel Apr 10, 2020
8a2807e
remove unused import
jbrockmendel Apr 10, 2020
fa046f0
Merge branch 'master' of https://github.com/pandas-dev/pandas into pe…
jbrockmendel Apr 10, 2020
fd10fb6
rebase compat
jbrockmendel Apr 10, 2020
7ea5d3a
Merge branch 'master' of https://github.com/pandas-dev/pandas into pe…
jbrockmendel Apr 11, 2020
7150e87
slice instead of take
jbrockmendel Apr 12, 2020
bddfbb0
Merge branch 'master' of https://github.com/pandas-dev/pandas into pe…
jbrockmendel Apr 13, 2020
25f83d6
Merge branch 'master' of https://github.com/pandas-dev/pandas into pe…
jbrockmendel Apr 13, 2020
2142d29
Merge branch 'master' of https://github.com/pandas-dev/pandas into pe…
jbrockmendel Apr 14, 2020
0ca2125
Dummy commit to force CI
jbrockmendel Apr 14, 2020
1ea0cc0
Merge branch 'master' of https://github.com/pandas-dev/pandas into pe…
jbrockmendel Apr 15, 2020
2bfc308
update call bound
jbrockmendel Apr 15, 2020
d5ad2a0
update max_len
jbrockmendel Apr 15, 2020
108004b
Merge branch 'master' of https://github.com/pandas-dev/pandas into pe…
jbrockmendel Apr 16, 2020
7ca5f9a
Merge branch 'master' of https://github.com/pandas-dev/pandas into pe…
jbrockmendel Apr 17, 2020
e78570d
never take
jbrockmendel Apr 17, 2020
33dfbdf
Merge branch 'master' of https://github.com/pandas-dev/pandas into pe…
jbrockmendel Apr 25, 2020
30f655b
REF: move operate_blockwise to new file
jbrockmendel Apr 25, 2020
65a4eaf
Merge branch 'master' of https://github.com/pandas-dev/pandas into pe…
jbrockmendel Apr 25, 2020
30d6580
ndim compat
jbrockmendel Apr 25, 2020
fe21f9c
Merge branch 'master' of https://github.com/pandas-dev/pandas into pe…
jbrockmendel Apr 26, 2020
f86deb4
separate out helper function
jbrockmendel Apr 26, 2020
f3dc465
Merge branch 'master' of https://github.com/pandas-dev/pandas into pe…
jbrockmendel Apr 27, 2020
b57f52c
Merge branch 'master' of https://github.com/pandas-dev/pandas into pe…
jbrockmendel May 11, 2020
0c46531
update per comments
jbrockmendel May 11, 2020
463a145
Merge branch 'master' of https://github.com/pandas-dev/pandas into pe…
jbrockmendel May 12, 2020
32e70d8
update per comments
jbrockmendel May 12, 2020
7989251
Merge branch 'master' of https://github.com/pandas-dev/pandas into pe…
jbrockmendel May 14, 2020
455e45e
Merge branch 'master' of https://github.com/pandas-dev/pandas into pe…
jbrockmendel May 16, 2020
41e8e78
update asv
jbrockmendel May 16, 2020
ac8eea8
Merge branch 'master' of https://github.com/pandas-dev/pandas into pe…
jbrockmendel May 17, 2020
8c4f951
requested edits
jbrockmendel May 17, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions asv_bench/benchmarks/arithmetic.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,56 @@ def time_frame_op_with_series_axis1(self, opname):
getattr(operator, opname)(self.df, self.ser)


class FrameWithFrameWide:
# Many-columns, mixed dtypes

params = [
[
operator.add,
operator.sub,
operator.mul,
operator.truediv,
operator.floordiv,
operator.pow,
operator.mod,
operator.eq,
operator.ne,
operator.gt,
operator.ge,
operator.lt,
operator.le,
Copy link
Member

Choose a reason for hiding this comment

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

Is it needed to test it with all those different ops?
I think what we are interested in catching with the benchmark, is the general code handling wide dataframes (how the dispatching to the actual op is done, dispathing to block/column instead of series, etc), not the actual op right? So for those aspects, they all use the same code, and testing all ops seems overkill? (it just adds to the number of benchmarks needlessly, making it harder to run the benchmark suite / interpret the results)

Copy link
Member Author

Choose a reason for hiding this comment

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

Is it needed to test it with all those different ops?

No strong opinion here.

making it harder to run the benchmark suite

Yah, this is a hassle. Best guess is eventually we're going to land on a "throw hardware at the problem" solution, but that doesn't help the local-running troubles.

Copy link
Member

Choose a reason for hiding this comment

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

No strong opinion here.

Then let's keep only 2 of them or so, I would say

Copy link
Member Author

Choose a reason for hiding this comment

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

how about four: one comparison, one logical, floordiv (for which we have special handling), and one other arithmetic

]
]
param_names = ["op"]

def setup(self, op):
# we choose dtypes so as to make the blocks
# a) not perfectly match between right and left
# b) appreciably bigger than single columns
arr = np.random.randn(10 ** 6).reshape(500, 2000).astype(np.float64)
df = pd.DataFrame(arr)
df[1000] = df[1000].astype(np.float32)
df.iloc[:, 1000:] = df.iloc[:, 1000:].astype(np.float32)
df._consolidate_inplace()

# TODO: GH#33198 the setting here shoudlnt need two steps
df2 = pd.DataFrame(arr)
df2[1000] = df2[1000].astype(np.int64)
df2.iloc[:, 500:1500] = df2.iloc[:, 500:1500].astype(np.int64)
df2._consolidate_inplace()

self.left = df
self.right = df

def time_op_different_blocks(self, op):
# blocks (and dtypes) are not aligned
op(self.left, self.right)

def time_op_same_blocks(self, op):
# blocks (and dtypes) are aligned
op(self.left, self.left)


class Ops:

params = [[True, False], ["default", 1]]
Expand Down
2 changes: 1 addition & 1 deletion doc/source/whatsnew/v1.1.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ Performance improvements
:meth:`DataFrame.sparse.from_spmatrix` constructor (:issue:`32821`,
:issue:`32825`, :issue:`32826`, :issue:`32856`, :issue:`32858`).
- Performance improvement in reductions (sum, prod, min, max) for nullable (integer and boolean) dtypes (:issue:`30982`, :issue:`33261`, :issue:`33442`).

- Performance improvement in arithmetic operations between two :class:`DataFrame` objects (:issue:`32779`)

.. ---------------------------------------------------------------------------

Expand Down
6 changes: 4 additions & 2 deletions pandas/core/arrays/datetimelike.py
Original file line number Diff line number Diff line change
Expand Up @@ -1360,18 +1360,20 @@ def _addsub_object_array(self, other: np.ndarray, op):
"""
assert op in [operator.add, operator.sub]
if len(other) == 1:
# If both 1D then broadcasting is unambiguous
# TODO(EA2D): require self.ndim == other.ndim here
return op(self, other[0])

warnings.warn(
"Adding/subtracting array of DateOffsets to "
"Adding/subtracting object-dtype array to "
f"{type(self).__name__} not vectorized",
PerformanceWarning,
)

# Caller is responsible for broadcasting if necessary
assert self.shape == other.shape, (self.shape, other.shape)

res_values = op(self.astype("O"), np.array(other))
res_values = op(self.astype("O"), np.asarray(other))
result = array(res_values.ravel())
result = extract_array(result, extract_numpy=True).reshape(self.shape)
return result
Expand Down
4 changes: 3 additions & 1 deletion pandas/core/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,7 @@ def __init__(
mgr = self._init_mgr(
data, axes=dict(index=index, columns=columns), dtype=dtype, copy=copy
)

elif isinstance(data, dict):
mgr = init_dict(data, index, columns, dtype=dtype)
elif isinstance(data, ma.MaskedArray):
Expand Down Expand Up @@ -5654,10 +5655,11 @@ def _construct_result(self, result) -> "DataFrame":
-------
DataFrame
"""
out = self._constructor(result, index=self.index, copy=False)
out = self._constructor(result, copy=False)
# Pin columns instead of passing to constructor for compat with
# non-unique columns case
out.columns = self.columns
out.index = self.index
return out

def combine(
Expand Down
35 changes: 29 additions & 6 deletions pandas/core/internals/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1233,12 +1233,21 @@ def reindex_indexer(

return type(self).from_blocks(new_blocks, new_axes)

def _slice_take_blocks_ax0(self, slice_or_indexer, fill_value=lib.no_default):
def _slice_take_blocks_ax0(
self, slice_or_indexer, fill_value=lib.no_default, only_slice: bool = False
):
"""
Slice/take blocks along axis=0.

Overloaded for SingleBlock

Parameters
----------
slice_or_indexer : slice, ndarray[bool], or list-like of ints
fill_value : scalar, default lib.no_default
only_slice : bool, default False
If True, we always return views on existing arrays, never copies.

Returns
-------
new_blocks : list of Block
Expand Down Expand Up @@ -1309,11 +1318,25 @@ def _slice_take_blocks_ax0(self, slice_or_indexer, fill_value=lib.no_default):
blocks.append(newblk)

else:
blocks.append(
blk.take_nd(
blklocs[mgr_locs.indexer], axis=0, new_mgr_locs=mgr_locs,
)
)
# GH#32779 to avoid the performance penalty of copying,
# we may try to only slice
taker = blklocs[mgr_locs.indexer]
Copy link
Contributor

Choose a reason for hiding this comment

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

can you add some comments here about the perf implications of these strategies

max_len = max(len(mgr_locs), taker.max() + 1)
if only_slice:
taker = lib.maybe_indices_to_slice(taker, max_len)

if isinstance(taker, slice):
nb = blk.getitem_block(taker, new_mgr_locs=mgr_locs)
blocks.append(nb)
elif only_slice:
# GH#33597 slice instead of take, so we get
# views instead of copies
for i, ml in zip(taker, mgr_locs):
Copy link
Contributor

Choose a reason for hiding this comment

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

can you use a list comprehesnion

Copy link
Member Author

Choose a reason for hiding this comment

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

above on 1317 i can, here it is less clean

nb = blk.getitem_block([i], new_mgr_locs=ml)
blocks.append(nb)
else:
nb = blk.take_nd(taker, axis=0, new_mgr_locs=mgr_locs)
blocks.append(nb)

return blocks

Expand Down
6 changes: 4 additions & 2 deletions pandas/core/ops/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
logical_op,
)
from pandas.core.ops.array_ops import comp_method_OBJECT_ARRAY # noqa:F401
from pandas.core.ops.blockwise import operate_blockwise
from pandas.core.ops.common import unpack_zerodim_and_defer
from pandas.core.ops.dispatch import should_series_dispatch
from pandas.core.ops.docstrings import (
Expand Down Expand Up @@ -325,8 +326,9 @@ def dispatch_to_series(left, right, func, str_rep=None, axis=None):
elif isinstance(right, ABCDataFrame):
assert right._indexed_same(left)

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.

An alternative would be to improve the performance of this line, and avoid the complexity that is added in operate_blockwise.
It will not give the same performance boost as what you now have with the blockwise operation, but it might already be a nice speedup with much less complex code, so a trade-off to consider IMO.

Copy link
Member Author

Choose a reason for hiding this comment

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

regardless this is worth doing on L343/424 and L348/429

array_op = get_array_op(func, str_rep=str_rep)
bm = operate_blockwise(left, right, array_op)
return type(left)(bm)

elif isinstance(right, ABCSeries) and axis == "columns":
# We only get here if called via _combine_series_frame,
Expand Down
12 changes: 9 additions & 3 deletions pandas/core/ops/array_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from functools import partial
import operator
from typing import Any, Optional, Tuple
import warnings

import numpy as np

Expand Down Expand Up @@ -120,7 +121,7 @@ def masked_arith_op(x: np.ndarray, y, op):
return result


def define_na_arithmetic_op(op, str_rep: str):
def define_na_arithmetic_op(op, str_rep: Optional[str]):
def na_op(x, y):
return na_arithmetic_op(x, y, op, str_rep)

Expand Down Expand Up @@ -254,8 +255,13 @@ def comparison_op(
res_values = comp_method_OBJECT_ARRAY(op, lvalues, rvalues)

else:
with np.errstate(all="ignore"):
res_values = na_arithmetic_op(lvalues, rvalues, op, str_rep, is_cmp=True)
with warnings.catch_warnings():
# suppress warnings from numpy about element-wise comparison
warnings.simplefilter("ignore", DeprecationWarning)
with np.errstate(all="ignore"):
res_values = na_arithmetic_op(
lvalues, rvalues, op, str_rep, is_cmp=True
)

return res_values

Expand Down
91 changes: 91 additions & 0 deletions pandas/core/ops/blockwise.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from typing import TYPE_CHECKING, List, Tuple

import numpy as np

from pandas._typing import ArrayLike

if TYPE_CHECKING:
from pandas.core.internals.blocks import Block # noqa:F401


def operate_blockwise(left, right, array_op):
assert right._indexed_same(left)

def get_same_shape_values(
Copy link
Contributor

Choose a reason for hiding this comment

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

this should be moved out to a function in the module; this is complex (partialy) because of this

Copy link
Member Author

Choose a reason for hiding this comment

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

updated

lblk: "Block", rblk: "Block", left_ea: bool, right_ea: bool
) -> Tuple[ArrayLike, ArrayLike]:
"""
Slice lblk.values to align with rblk. Squeeze if we have EAs.
"""
lvals = lblk.values
rvals = rblk.values

# TODO(EA2D): with 2D EAs pnly this first clause would be needed
if not (left_ea or right_ea):
lvals = lvals[rblk.mgr_locs.indexer, :]
assert lvals.shape == rvals.shape, (lvals.shape, rvals.shape)
elif left_ea and right_ea:
assert lvals.shape == rvals.shape, (lvals.shape, rvals.shape)
elif right_ea:
# lvals are 2D, rvals are 1D
lvals = lvals[rblk.mgr_locs.indexer, :]
assert lvals.shape[0] == 1, lvals.shape
lvals = lvals[0, :]
else:
# lvals are 1D, rvals are 2D
assert rvals.shape[0] == 1, rvals.shape
rvals = rvals[0, :]

return lvals, rvals

res_blks: List["Block"] = []
rmgr = right._mgr
for n, blk in enumerate(left._mgr.blocks):
locs = blk.mgr_locs
blk_vals = blk.values

left_ea = not isinstance(blk_vals, np.ndarray)

rblks = rmgr._slice_take_blocks_ax0(locs.indexer, only_slice=True)

# Assertions are disabled for performance, but should hold:
# if left_ea:
# assert len(locs) == 1, locs
# assert len(rblks) == 1, rblks
# assert rblks[0].shape[0] == 1, rblks[0].shape

for k, rblk in enumerate(rblks):
right_ea = not isinstance(rblk.values, np.ndarray)

lvals, rvals = get_same_shape_values(blk, rblk, left_ea, right_ea)

res_values = array_op(lvals, rvals)
if left_ea and not right_ea and hasattr(res_values, "reshape"):
res_values = res_values.reshape(1, -1)
nbs = rblk._split_op_result(res_values)

# Assertions are disabled for performance, but should hold:
# if right_ea or left_ea:
# assert len(nbs) == 1
# else:
# assert res_values.shape == lvals.shape, (res_values.shape, lvals.shape)

for nb in nbs:
Copy link
Contributor

Choose a reason for hiding this comment

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

this should be in a function again this is way too complex to grok

Copy link
Contributor

Choose a reason for hiding this comment

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

can you do this here

Copy link
Member Author

Choose a reason for hiding this comment

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

sure

# Reset mgr_locs to correspond to our original DataFrame
nblocs = locs.as_array[nb.mgr_locs.indexer]
nb.mgr_locs = nblocs
# Assertions are disabled for performance, but should hold:
# assert len(nblocs) == nb.shape[0], (len(nblocs), nb.shape)
# assert all(x in locs.as_array for x in nb.mgr_locs.as_array)

res_blks.extend(nbs)

# Assertions are disabled for performance, but should hold:
# slocs = {y for nb in res_blks for y in nb.mgr_locs.as_array}
# nlocs = sum(len(nb.mgr_locs.as_array) for nb in res_blks)
# assert nlocs == len(left.columns), (nlocs, len(left.columns))
# assert len(slocs) == nlocs, (len(slocs), nlocs)
# assert slocs == set(range(nlocs)), slocs

new_mgr = type(rmgr)(res_blks, axes=rmgr.axes, do_integrity_check=False)
return new_mgr
9 changes: 8 additions & 1 deletion pandas/tests/arithmetic/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,14 @@ def assert_invalid_comparison(left, right, box):
result = right != left
tm.assert_equal(result, ~expected)

msg = "Invalid comparison between|Cannot compare type|not supported between"
msg = "|".join(
[
"Invalid comparison between",
"Cannot compare type",
"not supported between",
"invalid type promotion",
]
)
with pytest.raises(TypeError, match=msg):
left < right
with pytest.raises(TypeError, match=msg):
Expand Down
7 changes: 4 additions & 3 deletions pandas/tests/arithmetic/test_datetime64.py
Original file line number Diff line number Diff line change
Expand Up @@ -964,7 +964,9 @@ def test_dt64arr_sub_dt64object_array(self, box_with_array, tz_naive_fixture):
obj = tm.box_expected(dti, box_with_array)
expected = tm.box_expected(expected, box_with_array)

warn = PerformanceWarning if box_with_array is not pd.DataFrame else None
warn = None
if box_with_array is not pd.DataFrame or tz_naive_fixture is None:
warn = PerformanceWarning
Copy link
Member

Choose a reason for hiding this comment

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

Do you know why this changed? We do / do not raise a warning on the array-level?

Copy link
Member Author

Choose a reason for hiding this comment

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

We don't do a warning when operating op(arr, length_1_object_array), which turns out to be the cases described here (in reverse)

with tm.assert_produces_warning(warn):
result = obj - obj.astype(object)
tm.assert_equal(result, expected)
Expand Down Expand Up @@ -1444,8 +1446,7 @@ def test_dt64arr_add_mixed_offset_array(self, box_with_array):
s = DatetimeIndex([Timestamp("2000-1-1"), Timestamp("2000-2-1")])
s = tm.box_expected(s, box_with_array)

warn = None if box_with_array is pd.DataFrame else PerformanceWarning
with tm.assert_produces_warning(warn):
with tm.assert_produces_warning(PerformanceWarning):
other = pd.Index([pd.offsets.DateOffset(years=1), pd.offsets.MonthEnd()])
other = tm.box_expected(other, box_with_array)
result = s + other
Expand Down
8 changes: 5 additions & 3 deletions pandas/tests/frame/test_arithmetic.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,11 @@ def check(df, df2):
)
tm.assert_frame_equal(result, expected)

msg = re.escape(
"Invalid comparison between dtype=datetime64[ns] and ndarray"
)
msgs = [
r"Invalid comparison between dtype=datetime64\[ns\] and ndarray",
"invalid type promotion",
]
msg = "|".join(msgs)
with pytest.raises(TypeError, match=msg):
x >= y
with pytest.raises(TypeError, match=msg):
Expand Down