Skip to content

PERF: Severe performance hit on DataFrame.sum with multi index and 'skipna=False' #37976

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

Closed
2 of 3 tasks
miccoli opened this issue Nov 20, 2020 · 8 comments
Closed
2 of 3 tasks
Labels
Duplicate Report Duplicate issue or pull request Groupby Performance Memory or execution speed performance

Comments

@miccoli
Copy link
Contributor

miccoli commented Nov 20, 2020

Series.sum(..., skipna=False) and DataFrame.sum(..., skipna=False) are very slow on a MultiIndex,
if compared to the same method called with (..., skipna=True)


  • I have checked that this issue has not already been reported.

  • I have confirmed this bug exists on the latest version of pandas.

  • (optional) I have confirmed this bug exists on the master branch of pandas.


Code Sample, a copy-pastable example

import pandas as pd
import numpy as np

dims = (1000, 100, 10)

# make random data
rg = np.random.default_rng(0)
x = rg.random(dims)

# construct dataframe with multindex
idx = pd.MultiIndex.from_product(map(range, dims), names=["i0", "i1", "i2"])
df = pd.Series(data=x.ravel(), index=idx, name="x")

# comupute sum over "i2" level in pandas
s1 = df.sum(level=["i0", "i1"], skipna=False)
s2 = df.sum(level=["i0", "i1"], skipna=True)

# compute sum over "axis=2" in numpy
s_ref = x.sum(axis=2)

# check results
s_ref_norm = np.linalg.norm(s_ref)
for s in (s1, s2):
    print(np.linalg.norm(s.to_numpy().reshape(s_ref.shape) - s_ref) / s_ref_norm)

Problem description

The computation of s1 = df.sum(level=["i0", "i1"], skipna=False) takes forever in the sample above:

In [2]: %timeit df.sum(level=["i0", "i1"], skipna=False)
8.44 s ± 47.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [3]: %timeit df.sum(level=["i0", "i1"], skipna=True)
52.1 ms ± 266 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [4]: %timeit x.sum(axis=2)
1.09 ms ± 13.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Here we see a severe performance hit (8.61 s vs 0.05 s) for the very same DataFrame.sum if skipna=False.
Maybe this is related to #16788 and #37622

The performance of df.sum(level=["i0", "i1"], skipna=True) with respect to baseline numpy is not great, but I think this is not an issue here.

Results are correct, fortunately. Curiously the slow result (skipna=False) is identical with the reference numpy sum, while the faster result (skipna=True) is within ±2 ULP, but of course this is not a concern.

Almost the same results hold if we use a DataFrame instead of a Series

Output of pd.show_versions()

INSTALLED VERSIONS

commit : 67a3d42
python : 3.8.2.final.0
python-bits : 64
OS : Darwin
OS-release : 19.6.0
Version : Darwin Kernel Version 19.6.0: Thu Oct 29 22:56:45 PDT 2020; root:xnu-6153.141.2.2~1/RELEASE_X86_64
machine : x86_64
processor : i386
byteorder : little
LC_ALL : None
LANG : None
LOCALE : None.UTF-8

pandas : 1.1.4
numpy : 1.19.4
pytz : 2020.1
dateutil : 2.8.1
pip : 20.2.4
setuptools : 49.6.0
Cython : None
pytest : 6.0.1
hypothesis : None
sphinx : None
blosc : None
feather : None
xlsxwriter : None
lxml.etree : None
html5lib : 1.0.1
pymysql : None
psycopg2 : None
jinja2 : 2.11.2
IPython : 7.17.0
pandas_datareader: None
bs4 : None
bottleneck : 1.3.2
fsspec : 0.8.0
fastparquet : None
gcsfs : None
matplotlib : 3.3.2
numexpr : 2.7.1
odfpy : None
openpyxl : 3.0.3
pandas_gbq : None
pyarrow : 1.0.1
pytables : None
pyxlsb : None
s3fs : 0.5.0
scipy : 1.5.3
sqlalchemy : None
tables : 3.6.1
tabulate : 0.8.7
xarray : 0.16.0
xlrd : 1.2.0
xlwt : None
numba : 0.51.1

@miccoli miccoli added Bug Needs Triage Issue that has not been reviewed by a pandas team member labels Nov 20, 2020
@miccoli miccoli changed the title BUG: Severe performance hit on DataFrame.sum with multi index and 'skipna=False' PERF: Severe performance hit on DataFrame.sum with multi index and 'skipna=False' Nov 20, 2020
@miccoli
Copy link
Contributor Author

miccoli commented Nov 20, 2020

A side note on performance. After reading #34773, I checked that timing is not influenced by pd.options.compute.use_bottleneck=False or pd.options.compute.use_bottleneck=True

In fact this is the result of prun df.sum(level=["i0", "i1"], skipna=False), from which it is evident that the current implementation is iterating on each element of the data, and the algorithm is not vectorised.

         34102203 function calls (33702182 primitive calls) in 16.093 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
  4600460    0.838    0.000    1.388    0.000 {built-in method builtins.isinstance}
   100001    0.534    0.000    0.534    0.000 {method 'reduce' of 'numpy.ufunc' objects}
   400000    0.475    0.000    1.134    0.000 _ufunc_config.py:32(seterr)
        1    0.401    0.401   16.032   16.032 ops.py:672(_aggregate_series_pure_python)
  1900114    0.394    0.000    0.550    0.000 generic.py:10(_check)
   100002    0.385    0.000    4.166    0.000 multi.py:251(__new__)
   400000    0.377    0.000    0.426    0.000 _ufunc_config.py:132(geterr)
   100000    0.373    0.000    7.695    0.000 series.py:4223(_reduce)
   100000    0.359    0.000    5.216    0.000 multi.py:1899(__getitem__)
   300014    0.353    0.000    0.892    0.000 base.py:498(_shallow_copy)
   100002    0.351    0.000    0.712    0.000 multi.py:1314(_set_names)
   100000    0.346    0.000    6.258    0.000 managers.py:1589(get_slice)
   100000    0.331    0.000    6.379    0.000 nanops.py:61(_f)
   100000    0.327    0.000    3.722    0.000 nanops.py:237(_get_values)
   300005    0.296    0.000    0.678    0.000 multi.py:3682(_coerce_indexer_frozen)
   600030    0.272    0.000    0.499    0.000 common.py:1600(_is_dtype_type)
   300015    0.259    0.000    0.401    0.000 base.py:463(_simple_new)
1600088/1300069    0.249    0.000    0.321    0.000 {built-in method builtins.len}
  2800188    0.246    0.000    0.246    0.000 {built-in method builtins.getattr}
   100002    0.241    0.000    1.762    0.000 multi.py:705(_set_levels)
   400007    0.216    0.000    1.390    0.000 multi.py:720(<genexpr>)
   100002    0.215    0.000    1.225    0.000 multi.py:870(_set_codes)
   100000    0.215    0.000    4.954    0.000 nanops.py:470(nansum)
   200012    0.208    0.000    0.791    0.000 common.py:1330(is_bool_dtype)
   400007    0.196    0.000    0.942    0.000 multi.py:880(<genexpr>)
   100002    0.191    0.000    0.479    0.000 generic.py:5141(__setattr__)
 100001/1    0.183    0.000   16.092   16.092 generic.py:11389(stat_func)
   300014    0.183    0.000    1.074    0.000 numeric.py:105(_shallow_copy)
   100000    0.173    0.000    7.165    0.000 ops.py:955(_chop)
   400021    0.170    0.000    0.779    0.000 base.py:256(is_dtype)
   400000    0.161    0.000    0.161    0.000 {built-in method numpy.seterrobj}
   100002    0.156    0.000    0.352    0.000 blocks.py:124(__init__)
   400000    0.154    0.000    0.312    0.000 nanops.py:64(<genexpr>)
   200005    0.154    0.000    0.547    0.000 dtypes.py:906(is_dtype)
   200000    0.143    0.000    1.687    0.000 common.py:1180(needs_i8_conversion)
   100001    0.139    0.000    7.328    0.000 ops.py:934(__iter__)
   300006    0.138    0.000    0.207    0.000 base.py:66(_reset_cache)
   200000    0.136    0.000    0.334    0.000 common.py:1025(is_datetime_or_timedelta_dtype)
   200036    0.133    0.000    0.339    0.000 common.py:1460(is_extension_array_dtype)
   200001    0.129    0.000    0.341    0.000 common.py:696(is_integer_dtype)
   200004    0.127    0.000    0.213    0.000 series.py:442(name)
   300005    0.127    0.000    0.382    0.000 cast.py:856(coerce_indexer_dtype)
   200000    0.127    0.000    0.745    0.000 _ufunc_config.py:433(__enter__)
   200018    0.125    0.000    0.265    0.000 {pandas._libs.lib.is_list_like}
   200000    0.123    0.000    0.639    0.000 _ufunc_config.py:438(__exit__)
  1200145    0.122    0.000    0.122    0.000 {built-in method builtins.issubclass}
   800000    0.121    0.000    0.121    0.000 {built-in method numpy.geterrobj}
   200001    0.118    0.000    0.209    0.000 managers.py:1613(internal_values)
   200036    0.117    0.000    0.170    0.000 base.py:413(find)
   100002    0.115    0.000    0.115    0.000 generic.py:195(__init__)
   200004    0.112    0.000    0.536    0.000 common.py:381(is_datetime64tz_dtype)
   200015    0.112    0.000    0.112    0.000 {pandas._libs.lib.is_scalar}
   100002    0.112    0.000    0.714    0.000 series.py:201(__init__)
   200003    0.112    0.000    0.296    0.000 common.py:1296(is_float_dtype)
   200008    0.108    0.000    0.674    0.000 common.py:456(is_period_dtype)
   100000    0.108    0.000    8.009    0.000 generic.py:10257(<lambda>)
   100000    0.107    0.000    0.459    0.000 blocks.py:256(make_block_same_class)
   300000    0.104    0.000    0.158    0.000 nanops.py:57(check)
   100000    0.104    0.000    0.104    0.000 multi.py:1922(<listcomp>)
   400014    0.103    0.000    0.144    0.000 inference.py:322(is_hashable)
   100002    0.103    0.000    0.126    0.000 managers.py:1532(__init__)
   200006    0.092    0.000    0.405    0.000 {built-in method builtins.any}
   400017    0.092    0.000    0.092    0.000 base.py:544(_reset_identity)
   200019    0.091    0.000    0.091    0.000 {built-in method _abc._abc_instancecheck}
   100000    0.089    0.000    0.412    0.000 common.py:97(is_bool_indexer)
   100002    0.086    0.000    0.086    0.000 generic.py:5123(__getattr__)
   200009    0.085    0.000    0.085    0.000 multi.py:1311(_get_names)
   200001    0.084    0.000    0.293    0.000 series.py:540(_values)
   100002    0.083    0.000    0.098    0.000 blocks.py:237(mgr_locs)
   100000    0.079    0.000    0.698    0.000 nanops.py:193(_maybe_get_mask)
   100000    0.078    0.000    0.641    0.000 {method 'sum' of 'numpy.ndarray' objects}
   400019    0.077    0.000    0.118    0.000 common.py:180(<lambda>)
   300003    0.074    0.000    0.074    0.000 managers.py:1575(_block)
   200008    0.073    0.000    0.090    0.000 common.py:1565(_get_dtype)
   300011    0.072    0.000    0.110    0.000 base.py:567(__len__)
   300006    0.071    0.000    0.100    0.000 base.py:5559(ensure_index)
   400019    0.070    0.000    0.070    0.000 {built-in method __new__ of type object at 0x10b204960}
   300005    0.067    0.000    0.067    0.000 {method 'view' of 'numpy.ndarray' objects}
   100000    0.062    0.000    0.861    0.000 nanops.py:322(_na_ok_dtype)
   400019    0.060    0.000    0.060    0.000 common.py:178(classes)
   100000    0.060    0.000    0.166    0.000 nanops.py:328(_wrap_results)
   200000    0.060    0.000    0.060    0.000 _ufunc_config.py:429(__init__)
   100000    0.060    0.000    8.069    0.000 groupby.py:1062(<lambda>)
   100006    0.056    0.000    0.068    0.000 common.py:905(is_datetime64_any_dtype)
   100003    0.056    0.000    0.094    0.000 series.py:492(name)
   100000    0.055    0.000    0.055    0.000 blocks.py:287(_slice)
   100002    0.052    0.000    0.052    0.000 blocks.py:135(_check_ndim)
   300012    0.052    0.000    0.052    0.000 {method 'copy' of 'dict' objects}
   100000    0.050    0.000    0.201    0.000 base.py:626(shape)
   200019    0.049    0.000    0.140    0.000 abc.py:96(__instancecheck__)
   200004    0.047    0.000    0.062    0.000 multi.py:849(nlevels)
   100000    0.047    0.000    0.066    0.000 nanops.py:1258(_maybe_null_out)
   100008    0.046    0.000    0.243    0.000 construction.py:339(extract_array)
   300013    0.045    0.000    0.045    0.000 base.py:1175(name)
   200011    0.042    0.000    0.061    0.000 common.py:188(<lambda>)
   400016    0.041    0.000    0.041    0.000 {built-in method builtins.hash}
   300015    0.040    0.000    0.040    0.000 base.py:3870(_values)
   300033    0.039    0.000    0.039    0.000 {built-in method builtins.hasattr}
   200011    0.036    0.000    0.036    0.000 common.py:183(classes_and_not_datetimelike)
   200001    0.033    0.000    0.033    0.000 blocks.py:201(internal_values)
   100010    0.031    0.000    0.038    0.000 common.py:422(is_timedelta64_dtype)
   300006    0.029    0.000    0.029    0.000 {method 'clear' of 'dict' objects}
   100000    0.029    0.000    0.563    0.000 _methods.py:45(_sum)
   200003    0.028    0.000    0.028    0.000 {pandas._libs.algos.ensure_int8}
   100004    0.028    0.000    0.028    0.000 generic.py:365(_get_axis_number)
   100002    0.027    0.000    0.171    0.000 inference.py:185(is_array_like)
   100003    0.026    0.000    0.026    0.000 numba_.py:17(maybe_use_numba)
   100003    0.025    0.000    0.025    0.000 function.py:48(__call__)
        8    0.024    0.003    0.024    0.003 {method 'take' of 'numpy.ndarray' objects}
   100012    0.024    0.000    0.024    0.000 multi.py:866(codes)
   100000    0.022    0.000    0.022    0.000 managers.py:1598(index)
   100000    0.019    0.000    0.019    0.000 nanops.py:1290(check_below_min_count)
   100002    0.017    0.000    0.017    0.000 {pandas._libs.algos.ensure_int16}
   100000    0.015    0.000    0.015    0.000 {method 'values' of 'dict' objects}
   100000    0.015    0.000    0.015    0.000 nanops.py:168(_get_fill_value)
        2    0.013    0.007    0.013    0.007 {method 'factorize' of 'pandas._libs.hashtable.Int64HashTable' objects}
   100002    0.012    0.000    0.012    0.000 blocks.py:233(mgr_locs)
        1    0.011    0.011    0.011    0.011 {method 'get_labels_groupby' of 'pandas._libs.hashtable.Int64HashTable' objects}
        1    0.006    0.006    0.006    0.006 {pandas._libs.lib.maybe_convert_objects}
        1    0.005    0.005    0.005    0.005 sorting.py:23(get_group_index)
        4    0.005    0.001    0.005    0.001 {method 'searchsorted' of 'numpy.ndarray' objects}
        1    0.004    0.004    0.004    0.004 {pandas._libs.algos.groupsort_indexer}
        3    0.004    0.001    0.004    0.001 {pandas._libs.algos.take_1d_int64_int64}
        1    0.004    0.004    0.004    0.004 sorting.py:139(decons_group_index)
        2    0.003    0.001    0.021    0.011 algorithms.py:508(factorize)
        1    0.002    0.002    0.002    0.002 {pandas._libs.lib.generate_slices}
       11    0.002    0.000    0.002    0.000 {pandas._libs.algos.ensure_int64}
        2    0.001    0.001    0.030    0.015 multi.py:1378(_get_grouper_for_level)
        1    0.001    0.001   16.093   16.093 <string>:1(<module>)
        1    0.001    0.001   16.032   16.032 ops.py:612(agg_series)
        1    0.001    0.001    0.017    0.017 ops.py:286(_get_compressed_codes)
        1    0.000    0.000    0.017    0.017 ops.py:269(group_info)
        6    0.000    0.000    0.000    0.000 {built-in method numpy.empty}
        1    0.000    0.000   16.054   16.054 groupby.py:1057(_python_agg_general)
        6    0.000    0.000    0.000    0.000 {built-in method numpy.core._multiarray_umath.implement_array_function}
        1    0.000    0.000   16.093   16.093 {built-in method builtins.exec}
        1    0.000    0.000   16.054   16.054 generic.py:228(aggregate)
        1    0.000    0.000    0.035    0.035 grouper.py:610(get_grouper)
      3/1    0.000    0.000    0.000    0.000 base.py:293(__new__)
        2    0.000    0.000    0.030    0.015 grouper.py:413(__init__)
       12    0.000    0.000    0.000    0.000 _dtype.py:321(_name_get)
        3    0.000    0.000    0.004    0.001 algorithms.py:1616(take_nd)
        2    0.000    0.000    0.013    0.007 algorithms.py:468(_factorize_array)
        3    0.000    0.000    0.000    0.000 {pandas._libs.lib.infer_dtype}
        4    0.000    0.000    0.002    0.000 algorithms.py:69(_ensure_data)
        4    0.000    0.000    0.000    0.000 {method 'argsort' of 'numpy.ndarray' objects}
        2    0.000    0.000    0.005    0.002 multi.py:2888(_get_level_indexer)
        2    0.000    0.000    0.003    0.002 algorithms.py:1991(safe_sort)
       18    0.000    0.000    0.000    0.000 frozen.py:66(__getitem__)
       12    0.000    0.000    0.000    0.000 {built-in method numpy.array}
        3    0.000    0.000    0.000    0.000 algorithms.py:1487(_get_take_nd_function)
        1    0.000    0.000    0.000    0.000 {built-in method numpy.zeros}
        2    0.000    0.000    0.000    0.000 cast.py:442(maybe_promote)
        2    0.000    0.000    0.005    0.002 grouper.py:748(is_in_axis)
        1    0.000    0.000    0.035    0.035 groupby.py:483(__init__)
        2    0.000    0.000    0.000    0.000 algorithms.py:272(_check_object_for_strings)
       12    0.000    0.000    0.000    0.000 _dtype.py:307(_name_includes_bit_suffix)
       22    0.000    0.000    0.000    0.000 numerictypes.py:286(issubclass_)
        1    0.000    0.000    0.011    0.011 sorting.py:521(compress_group_index)
        1    0.000    0.000    0.016    0.016 series.py:820(take)
       11    0.000    0.000    0.000    0.000 numerictypes.py:360(issubdtype)
        9    0.000    0.000    0.000    0.000 common.py:194(is_object_dtype)
        1    0.000    0.000    0.004    0.004 sorting.py:496(get_group_index_sorter)
       11    0.000    0.000    0.000    0.000 {method 'format' of 'str' objects}
        1    0.000    0.000    0.035    0.035 series.py:1622(groupby)
       19    0.000    0.000    0.000    0.000 common.py:1733(pandas_dtype)
        2    0.000    0.000    0.000    0.000 cast.py:300(maybe_cast_result_dtype)
        2    0.000    0.000    0.000    0.000 base.py:1193(_validate_names)
       12    0.000    0.000    0.000    0.000 common.py:530(is_categorical_dtype)
        2    0.000    0.000    0.000    0.000 blocks.py:2655(get_block_type)
        2    0.000    0.000    0.008    0.004 base.py:701(take)
        1    0.000    0.000    0.012    0.012 multi.py:1932(take)
        2    0.000    0.000    0.000    0.000 algorithms.py:178(_reconstruct_data)
        4    0.000    0.000    0.000    0.000 common.py:348(is_datetime64_dtype)
        2    0.000    0.000    0.000    0.000 ops.py:111(shape)
        2    0.000    0.000    0.000    0.000 base.py:793(copy)
        1    0.000    0.000    0.000    0.000 fromnumeric.py:70(_wrapreduction)
        1    0.000    0.000    0.000    0.000 generic.py:329(_wrap_series_output)
        1    0.000    0.000    0.021    0.021 ops.py:305(result_index)
        1    0.000    0.000   16.090   16.090 generic.py:10249(_agg_by_level)
       10    0.000    0.000    0.000    0.000 _asarray.py:14(asarray)
        2    0.000    0.000    0.005    0.002 multi.py:2639(get_loc)
        1    0.000    0.000    0.021    0.021 ops.py:299(reconstructed_codes)
        1    0.000    0.000    0.000    0.000 typing.py:865(__new__)
        6    0.000    0.000    0.000    0.000 multi.py:686(__len__)
        2    0.000    0.000    0.000    0.000 blocks.py:2701(make_block)
        6    0.000    0.000    0.000    0.000 common.py:750(is_signed_integer_dtype)
        2    0.000    0.000    0.000    0.000 base.py:1216(_set_names)
        2    0.000    0.000    0.000    0.000 cast.py:257(maybe_cast_result)
        3    0.000    0.000    0.000    0.000 {method 'astype' of 'numpy.ndarray' objects}
        2    0.000    0.000    0.000    0.000 common.py:224(is_sparse)
        1    0.000    0.000    0.000    0.000 ops.py:311(<listcomp>)
        2    0.000    0.000    0.000    0.000 cast.py:598(_ensure_dtype_type)
        3    0.000    0.000    0.000    0.000 common.py:218(asarray_tuplesafe)
        1    0.000    0.000    0.020    0.020 ops.py:947(_get_sorted_data)
        4    0.000    0.000    0.000    0.000 missing.py:130(_isna)
       12    0.000    0.000    0.000    0.000 _dtype.py:24(_kind_name)
        2    0.000    0.000    0.000    0.000 algorithms.py:263(_get_data_algo)
        2    0.000    0.000    0.000    0.000 <__array_function__ internals>:2(can_cast)
        2    0.000    0.000    0.000    0.000 managers.py:1564(from_array)
        8    0.000    0.000    0.000    0.000 common.py:492(is_interval_dtype)
        2    0.000    0.000    0.000    0.000 base.py:1246(set_names)
        2    0.000    0.000    0.000    0.000 generic.py:165(_iterate_slices)
        5    0.000    0.000    0.000    0.000 dtypes.py:1119(is_dtype)
        1    0.000    0.000    0.000    0.000 construction.py:390(sanitize_array)
        1    0.000    0.000    0.021    0.021 ops.py:295(ngroups)
        2    0.000    0.000    0.000    0.000 cast.py:116(maybe_downcast_to_dtype)
        1    0.000    0.000    0.000    0.000 ops.py:88(__init__)
        2    0.000    0.000    0.000    0.000 grouper.py:834(_convert_grouper)
        1    0.000    0.000    0.004    0.004 ops.py:929(sort_idx)
        3    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:1017(_handle_fromlist)
        1    0.000    0.000    0.012    0.012 multi.py:1969(<listcomp>)
        6    0.000    0.000    0.000    0.000 ops.py:113(<genexpr>)
        4    0.000    0.000    0.000    0.000 {built-in method pandas._libs.missing.checknull}
        1    0.000    0.000    0.000    0.000 cast.py:1570(construct_1d_object_array_from_listlike)
        1    0.000    0.000    0.000    0.000 multi.py:692(levels)
       18    0.000    0.000    0.000    0.000 {function FrozenList.__getitem__ at 0x122f3b1f0}
        1    0.000    0.000    0.000    0.000 generic.py:377(_get_axis)
        7    0.000    0.000    0.000    0.000 {method 'get' of 'dict' objects}
        1    0.000    0.000    0.000    0.000 generic.py:5095(__finalize__)
        2    0.000    0.000    0.000    0.000 <__array_function__ internals>:2(putmask)
        1    0.000    0.000    0.000    0.000 numeric.py:268(full)
        1    0.000    0.000    0.000    0.000 base.py:5726(_maybe_cast_data_without_dtype)
        1    0.000    0.000    0.000    0.000 generic.py:370(_wrap_aggregated_output)
        1    0.000    0.000    0.000    0.000 cast.py:1187(maybe_castable)
        2    0.000    0.000    0.000    0.000 common.py:1223(is_numeric_dtype)
        2    0.000    0.000    0.000    0.000 managers.py:1602(dtype)
        2    0.000    0.000    0.000    0.000 multi.py:2615(_get_loc_single_level_index)
        2    0.000    0.000    0.000    0.000 cast.py:184(maybe_downcast_numeric)
        2    0.000    0.000    0.000    0.000 algorithms.py:255(_get_values_for_rank)
        4    0.000    0.000    0.000    0.000 missing.py:47(isna)
        2    0.000    0.000    0.000    0.000 series.py:810(axes)
        1    0.000    0.000    0.001    0.001 ops.py:924(slabels)
        4    0.000    0.000    0.000    0.000 grouper.py:549(ngroups)
        1    0.000    0.000    0.000    0.000 fromnumeric.py:2881(prod)
        2    0.000    0.000    0.000    0.000 base.py:1213(_get_names)
        4    0.000    0.000    0.000    0.000 base.py:5656(maybe_extract_name)
       11    0.000    0.000    0.000    0.000 {pandas._libs.algos.ensure_platform_int}
        2    0.000    0.000    0.000    0.000 series.py:398(_set_axis)
        2    0.000    0.000    0.000    0.000 sorting.py:131(is_int64_overflow_possible)
        2    0.000    0.000    0.000    0.000 algorithms.py:213(_ensure_arraylike)
        1    0.000    0.000    0.000    0.000 sorting.py:56(_int64_cut_off)
        6    0.000    0.000    0.000    0.000 grouper.py:579(group_index)
        1    0.000    0.000    0.004    0.004 sorting.py:159(decons_obs_group_ids)
        1    0.000    0.000    0.012    0.012 multi.py:1947(_assert_take_fillable)
        2    0.000    0.000    0.000    0.000 ops.py:232(<listcomp>)
        1    0.000    0.000    0.000    0.000 groupby.py:697(__getattr__)
        1    0.000    0.000    0.000    0.000 multi.py:697(<listcomp>)
        1    0.000    0.000    0.000    0.000 groupby.py:2672(_reindex_output)
        1    0.000    0.000    0.000    0.000 <__array_function__ internals>:2(prod)
        8    0.000    0.000    0.000    0.000 ops.py:107(groupings)
        2    0.000    0.000    0.000    0.000 ops.py:230(codes)
        2    0.000    0.000    0.000    0.000 grouper.py:761(is_in_obj)
        1    0.000    0.000    0.000    0.000 base.py:4083(__getitem__)
        2    0.000    0.000    0.000    0.000 series.py:427(dtype)
        2    0.000    0.000    0.000    0.000 grouper.py:712(<genexpr>)
        1    0.000    0.000    0.000    0.000 ops.py:916(__init__)
        2    0.000    0.000    0.000    0.000 grouper.py:830(_is_label_like)
        2    0.000    0.000    0.000    0.000 grouper.py:714(<genexpr>)
        1    0.000    0.000    0.000    0.000 <__array_function__ internals>:2(copyto)
        3    0.000    0.000    0.000    0.000 groupby.py:2710(<genexpr>)
        1    0.000    0.000    0.000    0.000 ops.py:977(get_splitter)
        1    0.000    0.000    0.000    0.000 managers.py:306(__len__)
        2    0.000    0.000    0.000    0.000 common.py:806(is_unsigned_integer_dtype)
        1    0.000    0.000    0.000    0.000 base.py:569(_is_builtin_func)
        2    0.000    0.000    0.000    0.000 grouper.py:573(result_index)
        1    0.000    0.000    0.000    0.000 {built-in method builtins.min}
        1    0.000    0.000    0.000    0.000 series.py:595(__len__)
        4    0.000    0.000    0.000    0.000 grouper.py:567(codes)
        1    0.000    0.000    0.000    0.000 base.py:563(_get_cython_func)
        1    0.000    0.000    0.000    0.000 base.py:854(empty)
        1    0.000    0.000    0.000    0.000 base.py:5672(_maybe_cast_with_dtype)
        2    0.000    0.000    0.000    0.000 blocks.py:315(dtype)
        1    0.000    0.000    0.000    0.000 fromnumeric.py:71(<dictcomp>)
        1    0.000    0.000    0.000    0.000 construction.py:520(_try_cast)
        4    0.000    0.000    0.000    0.000 {method 'append' of 'list' objects}
        2    0.000    0.000    0.000    0.000 series.py:381(_constructor)
        1    0.000    0.000    0.000    0.000 ops.py:118(nkeys)
        1    0.000    0.000    0.000    0.000 {method 'ravel' of 'numpy.ndarray' objects}
        1    0.000    0.000    0.000    0.000 generic.py:353(<dictcomp>)
        1    0.000    0.000    0.000    0.000 managers.py:259(items)
        1    0.000    0.000    0.000    0.000 groupby.py:632(_selected_obj)
        1    0.000    0.000    0.000    0.000 ops.py:238(names)
        2    0.000    0.000    0.000    0.000 base.py:590(dtype)
        3    0.000    0.000    0.000    0.000 {method 'items' of 'dict' objects}
        1    0.000    0.000    0.000    0.000 base.py:669(size)
        1    0.000    0.000    0.000    0.000 common.py:149(cast_scalar_indexer)
        3    0.000    0.000    0.000    0.000 {pandas._libs.lib.is_float}
        2    0.000    0.000    0.000    0.000 base.py:424(<genexpr>)
        1    0.000    0.000    0.000    0.000 construction.py:580(is_empty_data)
        2    0.000    0.000    0.000    0.000 {method 'index' of 'list' objects}
        2    0.000    0.000    0.000    0.000 grouper.py:713(<genexpr>)
        1    0.000    0.000    0.000    0.000 ops.py:240(<listcomp>)
        1    0.000    0.000    0.000    0.000 common.py:72(ensure_float)
        1    0.000    0.000    0.000    0.000 <string>:1(__new__)
        2    0.000    0.000    0.000    0.000 generic.py:354(<genexpr>)
        2    0.000    0.000    0.000    0.000 multiarray.py:1078(putmask)
        2    0.000    0.000    0.000    0.000 multiarray.py:468(can_cast)
        1    0.000    0.000    0.000    0.000 generic.py:232(attrs)
        1    0.000    0.000    0.000    0.000 {built-in method builtins.all}
        2    0.000    0.000    0.000    0.000 multi.py:1673(is_all_dates)
        2    0.000    0.000    0.000    0.000 {pandas._libs.lib.is_integer}
        2    0.000    0.000    0.000    0.000 {pandas._libs.lib.is_bool}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
        1    0.000    0.000    0.000    0.000 fromnumeric.py:2876(_prod_dispatcher)
        1    0.000    0.000    0.000    0.000 multiarray.py:1043(copyto)
        1    0.000    0.000    0.000    0.000 base.py:637(ndim)
        1    0.000    0.000    0.000    0.000 multi.py:1446(_has_complex_internals)
        1    0.000    0.000    0.000    0.000 {built-in method builtins.callable}
        1    0.000    0.000    0.000    0.000 {pandas._libs.lib.is_iterator}sidM

For reference here the output of %prun df.sum(level=["i0", "i1"], skipna=True)

         2049 function calls (2033 primitive calls) in 0.063 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        2    0.013    0.007    0.013    0.007 {method 'factorize' of 'pandas._libs.hashtable.Int64HashTable' objects}
        1    0.011    0.011    0.011    0.011 {method 'get_labels_groupby' of 'pandas._libs.hashtable.Int64HashTable' objects}
        4    0.008    0.002    0.008    0.002 {method 'take' of 'numpy.ndarray' objects}
        1    0.005    0.005    0.005    0.005 sorting.py:23(get_group_index)
        4    0.004    0.001    0.004    0.001 {method 'searchsorted' of 'numpy.ndarray' objects}
        1    0.004    0.004    0.004    0.004 ops.py:591(_aggregate)
        1    0.004    0.004    0.004    0.004 sorting.py:139(decons_group_index)
        2    0.003    0.001    0.021    0.011 algorithms.py:508(factorize)
        2    0.003    0.001    0.003    0.001 {pandas._libs.algos.take_1d_int64_int64}
        1    0.002    0.002    0.063    0.063 generic.py:11389(stat_func)
        8    0.001    0.000    0.001    0.000 {pandas._libs.algos.ensure_int64}
        2    0.001    0.000    0.030    0.015 multi.py:1378(_get_grouper_for_level)
        1    0.001    0.001    0.016    0.016 ops.py:286(_get_compressed_codes)
        1    0.000    0.000    0.017    0.017 ops.py:269(group_info)
      8/6    0.000    0.000    0.000    0.000 {built-in method numpy.core._multiarray_umath.implement_array_function}
      456    0.000    0.000    0.000    0.000 {built-in method builtins.isinstance}
        2    0.000    0.000    0.000    0.000 {method 'reduce' of 'numpy.ufunc' objects}
        2    0.000    0.000    0.013    0.007 algorithms.py:468(_factorize_array)
       35    0.000    0.000    0.000    0.000 common.py:1600(_is_dtype_type)
      3/1    0.000    0.000    0.000    0.000 base.py:293(__new__)
        1    0.000    0.000    0.063    0.063 {built-in method builtins.exec}
        1    0.000    0.000    0.000    0.000 {pandas._libs.algos.ensure_int8}
        1    0.000    0.000    0.025    0.025 ops.py:436(_cython_operation)
       11    0.000    0.000    0.000    0.000 _dtype.py:321(_name_get)
        1    0.000    0.000    0.000    0.000 {pandas._libs.algos.ensure_int16}
        1    0.000    0.000    0.035    0.035 grouper.py:610(get_grouper)
        1    0.000    0.000    0.063    0.063 <string>:1(<module>)
       35    0.000    0.000    0.000    0.000 common.py:1460(is_extension_array_dtype)
      118    0.000    0.000    0.000    0.000 generic.py:10(_check)
      193    0.000    0.000    0.000    0.000 {built-in method builtins.getattr}
        3    0.000    0.000    0.000    0.000 {pandas._libs.lib.infer_dtype}
        2    0.000    0.000    0.030    0.015 grouper.py:413(__init__)
        1    0.000    0.000    0.000    0.000 {method 'fill' of 'numpy.ndarray' objects}
       35    0.000    0.000    0.000    0.000 base.py:413(find)
       17    0.000    0.000    0.000    0.000 {pandas._libs.lib.is_list_like}
        8    0.000    0.000    0.000    0.000 base.py:498(_shallow_copy)
        4    0.000    0.000    0.002    0.000 algorithms.py:69(_ensure_data)
      144    0.000    0.000    0.000    0.000 {built-in method builtins.issubclass}
        2    0.000    0.000    0.003    0.002 algorithms.py:1616(take_nd)
        2    0.000    0.000    0.000    0.000 {built-in method numpy.zeros}
    64/52    0.000    0.000    0.000    0.000 {built-in method builtins.len}
        2    0.000    0.000    0.003    0.002 algorithms.py:1991(safe_sort)
        2    0.000    0.000    0.005    0.002 multi.py:2888(_get_level_indexer)
        4    0.000    0.000    0.000    0.000 {method 'argsort' of 'numpy.ndarray' objects}
        9    0.000    0.000    0.000    0.000 base.py:463(_simple_new)
       10    0.000    0.000    0.000    0.000 {built-in method numpy.array}
       16    0.000    0.000    0.000    0.000 frozen.py:66(__getitem__)
       14    0.000    0.000    0.000    0.000 common.py:1330(is_bool_dtype)
        2    0.000    0.000    0.000    0.000 cast.py:442(maybe_promote)
        1    0.000    0.000    0.026    0.026 groupby.py:1020(_cython_agg_general)
        8    0.000    0.000    0.000    0.000 numeric.py:105(_shallow_copy)
        1    0.000    0.000    0.035    0.035 groupby.py:483(__init__)
       11    0.000    0.000    0.000    0.000 _dtype.py:307(_name_includes_bit_suffix)
       20    0.000    0.000    0.000    0.000 numerictypes.py:286(issubclass_)
        1    0.000    0.000    0.011    0.011 sorting.py:521(compress_group_index)
       23    0.000    0.000    0.000    0.000 base.py:256(is_dtype)
       17    0.000    0.000    0.000    0.000 {built-in method _abc._abc_instancecheck}
       10    0.000    0.000    0.000    0.000 numerictypes.py:360(issubdtype)
        2    0.000    0.000    0.005    0.002 grouper.py:748(is_in_axis)
       22    0.000    0.000    0.000    0.000 common.py:1733(pandas_dtype)
        2    0.000    0.000    0.000    0.000 algorithms.py:272(_check_object_for_strings)
       10    0.000    0.000    0.000    0.000 {method 'format' of 'str' objects}
        1    0.000    0.000    0.000    0.000 generic.py:329(_wrap_series_output)
        1    0.000    0.000    0.035    0.035 series.py:1622(groupby)
        1    0.000    0.000    0.000    0.000 series.py:201(__init__)
        4    0.000    0.000    0.000    0.000 {built-in method numpy.empty}
        1    0.000    0.000    0.061    0.061 generic.py:10249(_agg_by_level)
        2    0.000    0.000    0.000    0.000 algorithms.py:1487(_get_take_nd_function)
        1    0.000    0.000    0.000    0.000 ops.py:366(_get_cython_function)
        1    0.000    0.000    0.000    0.000 multi.py:251(__new__)
        9    0.000    0.000    0.000    0.000 common.py:194(is_object_dtype)
       10    0.000    0.000    0.000    0.000 common.py:1565(_get_dtype)
       12    0.000    0.000    0.000    0.000 common.py:530(is_categorical_dtype)
        1    0.000    0.000    0.021    0.021 ops.py:305(result_index)
        1    0.000    0.000    0.000    0.000 cast.py:300(maybe_cast_result_dtype)
        6    0.000    0.000    0.000    0.000 common.py:381(is_datetime64tz_dtype)
       11    0.000    0.000    0.000    0.000 common.py:422(is_timedelta64_dtype)
        2    0.000    0.000    0.000    0.000 algorithms.py:178(_reconstruct_data)
        2    0.000    0.000    0.000    0.000 base.py:1193(_validate_names)
        2    0.000    0.000    0.000    0.000 multi.py:3682(_coerce_indexer_frozen)
        2    0.000    0.000    0.000    0.000 base.py:793(copy)
        2    0.000    0.000    0.000    0.000 ops.py:111(shape)
       14    0.000    0.000    0.000    0.000 common.py:188(<lambda>)
        4    0.000    0.000    0.000    0.000 common.py:696(is_integer_dtype)
       36    0.000    0.000    0.000    0.000 {built-in method builtins.hasattr}
        3    0.000    0.000    0.000    0.000 common.py:218(asarray_tuplesafe)
        1    0.000    0.000    0.021    0.021 ops.py:299(reconstructed_codes)
        1    0.000    0.000    0.000    0.000 fromnumeric.py:70(_wrapreduction)
        2    0.000    0.000    0.008    0.004 base.py:701(take)
        1    0.000    0.000    0.000    0.000 ops.py:311(<listcomp>)
        6    0.000    0.000    0.000    0.000 dtypes.py:906(is_dtype)
       17    0.000    0.000    0.000    0.000 abc.py:96(__instancecheck__)
        1    0.000    0.000    0.000    0.000 numeric.py:75(zeros_like)
        4    0.000    0.000    0.000    0.000 common.py:348(is_datetime64_dtype)
        5    0.000    0.000    0.000    0.000 missing.py:130(_isna)
        1    0.000    0.000    0.000    0.000 typing.py:865(__new__)
        9    0.000    0.000    0.000    0.000 _asarray.py:14(asarray)
        1    0.000    0.000    0.000    0.000 multi.py:705(_set_levels)
        2    0.000    0.000    0.000    0.000 base.py:1216(_set_names)
        6    0.000    0.000    0.000    0.000 common.py:750(is_signed_integer_dtype)
        1    0.000    0.000    0.000    0.000 multi.py:1314(_set_names)
        2    0.000    0.000    0.000    0.000 cast.py:598(_ensure_dtype_type)
       14    0.000    0.000    0.000    0.000 {pandas._libs.lib.is_scalar}
       11    0.000    0.000    0.000    0.000 _dtype.py:24(_kind_name)
        2    0.000    0.000    0.005    0.002 multi.py:2639(get_loc)
        2    0.000    0.000    0.000    0.000 common.py:224(is_sparse)
        9    0.000    0.000    0.000    0.000 common.py:456(is_period_dtype)
       21    0.000    0.000    0.000    0.000 common.py:178(classes)
       12    0.000    0.000    0.000    0.000 {built-in method __new__ of type object at 0x10b204960}
       21    0.000    0.000    0.000    0.000 common.py:180(<lambda>)
       10    0.000    0.000    0.000    0.000 inference.py:322(is_hashable)
        3    0.000    0.000    0.000    0.000 {method 'astype' of 'numpy.ndarray' objects}
        4    0.000    0.000    0.000    0.000 multi.py:686(__len__)
        2    0.000    0.000    0.000    0.000 <__array_function__ internals>:2(can_cast)
        7    0.000    0.000    0.000    0.000 multi.py:1311(_get_names)
        1    0.000    0.000    0.000    0.000 construction.py:390(sanitize_array)
        2    0.000    0.000    0.000    0.000 algorithms.py:263(_get_data_algo)
        3    0.000    0.000    0.000    0.000 multi.py:880(<genexpr>)
        1    0.000    0.000    0.025    0.025 ops.py:581(aggregate)
        1    0.000    0.000    0.000    0.000 cast.py:1570(construct_1d_object_array_from_listlike)
        1    0.000    0.000    0.000    0.000 cast.py:257(maybe_cast_result)
       10    0.000    0.000    0.000    0.000 base.py:544(_reset_identity)
        1    0.000    0.000    0.021    0.021 ops.py:295(ngroups)
        8    0.000    0.000    0.000    0.000 base.py:567(__len__)
        2    0.000    0.000    0.000    0.000 base.py:1246(set_names)
        2    0.000    0.000    0.000    0.000 managers.py:1602(dtype)
        3    0.000    0.000    0.000    0.000 base.py:66(_reset_cache)
        7    0.000    0.000    0.000    0.000 construction.py:339(extract_array)
        1    0.000    0.000    0.000    0.000 multi.py:870(_set_codes)
        3    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:1017(_handle_fromlist)
       14    0.000    0.000    0.000    0.000 common.py:183(classes_and_not_datetimelike)
        2    0.000    0.000    0.000    0.000 cast.py:856(coerce_indexer_dtype)
        1    0.000    0.000    0.026    0.026 groupby.py:987(_agg_general)
        7    0.000    0.000    0.000    0.000 common.py:492(is_interval_dtype)
        2    0.000    0.000    0.000    0.000 generic.py:165(_iterate_slices)
        4    0.000    0.000    0.000    0.000 dtypes.py:1119(is_dtype)
        3    0.000    0.000    0.000    0.000 multi.py:720(<genexpr>)
        1    0.000    0.000    0.000    0.000 blocks.py:2655(get_block_type)
        5    0.000    0.000    0.000    0.000 {built-in method pandas._libs.missing.checknull}
        1    0.000    0.000    0.000    0.000 base.py:5726(_maybe_cast_data_without_dtype)
        1    0.000    0.000    0.000    0.000 common.py:1509(is_complex_dtype)
        1    0.000    0.000    0.000    0.000 blocks.py:2701(make_block)
        5    0.000    0.000    0.000    0.000 {built-in method builtins.any}
        1    0.000    0.000    0.000    0.000 <__array_function__ internals>:2(zeros_like)
        4    0.000    0.000    0.000    0.000 base.py:5656(maybe_extract_name)
        2    0.000    0.000    0.000    0.000 <__array_function__ internals>:2(putmask)
        1    0.000    0.000    0.000    0.000 generic.py:370(_wrap_aggregated_output)
        5    0.000    0.000    0.000    0.000 missing.py:47(isna)
        7    0.000    0.000    0.000    0.000 common.py:905(is_datetime64_any_dtype)
        1    0.000    0.000    0.000    0.000 cast.py:1187(maybe_castable)
        1    0.000    0.000    0.000    0.000 ops.py:88(__init__)
        2    0.000    0.000    0.000    0.000 series.py:427(dtype)
        1    0.000    0.000    0.000    0.000 generic.py:5141(__setattr__)
        1    0.000    0.000    0.000    0.000 generic.py:377(_get_axis)
        2    0.000    0.000    0.000    0.000 grouper.py:834(_convert_grouper)
        1    0.000    0.000    0.000    0.000 blocks.py:124(__init__)
        6    0.000    0.000    0.000    0.000 ops.py:113(<genexpr>)
        1    0.000    0.000    0.000    0.000 missing.py:507(_maybe_fill)
       10    0.000    0.000    0.000    0.000 base.py:1175(name)
        8    0.000    0.000    0.000    0.000 multi.py:866(codes)
        2    0.000    0.000    0.000    0.000 common.py:1223(is_numeric_dtype)
        6    0.000    0.000    0.000    0.000 {method 'copy' of 'dict' objects}
       16    0.000    0.000    0.000    0.000 {function FrozenList.__getitem__ at 0x122f3b1f0}
        2    0.000    0.000    0.000    0.000 series.py:810(axes)
        1    0.000    0.000    0.000    0.000 groupby.py:697(__getattr__)
        1    0.000    0.000    0.000    0.000 generic.py:5095(__finalize__)
        1    0.000    0.000    0.004    0.004 sorting.py:159(decons_obs_group_ids)
        1    0.000    0.000    0.000    0.000 managers.py:1564(from_array)
        1    0.000    0.000    0.000    0.000 cast.py:184(maybe_downcast_numeric)
        1    0.000    0.000    0.000    0.000 base.py:4083(__getitem__)
        1    0.000    0.000    0.026    0.026 groupby.py:1539(sum)
        2    0.000    0.000    0.000    0.000 multi.py:2615(_get_loc_single_level_index)
        6    0.000    0.000    0.000    0.000 {method 'get' of 'dict' objects}
        1    0.000    0.000    0.000    0.000 missing.py:341(_isna_compat)
        1    0.000    0.000    0.000    0.000 <__array_function__ internals>:2(empty_like)
        1    0.000    0.000    0.000    0.000 cast.py:116(maybe_downcast_to_dtype)
        6    0.000    0.000    0.000    0.000 grouper.py:579(group_index)
        3    0.000    0.000    0.000    0.000 managers.py:1575(_block)
        1    0.000    0.000    0.000    0.000 sorting.py:56(_int64_cut_off)
        2    0.000    0.000    0.000    0.000 ops.py:232(<listcomp>)
        4    0.000    0.000    0.000    0.000 grouper.py:549(ngroups)
        1    0.000    0.000    0.000    0.000 fromnumeric.py:2881(prod)
        1    0.000    0.000    0.000    0.000 generic.py:195(__init__)
        1    0.000    0.000    0.000    0.000 groupby.py:2672(_reindex_output)
       12    0.000    0.000    0.000    0.000 {built-in method builtins.hash}
        2    0.000    0.000    0.000    0.000 algorithms.py:255(_get_values_for_rank)
        2    0.000    0.000    0.000    0.000 algorithms.py:213(_ensure_arraylike)
        2    0.000    0.000    0.000    0.000 sorting.py:131(is_int64_overflow_possible)
        8    0.000    0.000    0.000    0.000 ops.py:107(groupings)
        3    0.000    0.000    0.000    0.000 generic.py:365(_get_axis_number)
        1    0.000    0.000    0.000    0.000 blocks.py:237(mgr_locs)
        2    0.000    0.000    0.000    0.000 base.py:1213(_get_names)
        8    0.000    0.000    0.000    0.000 {pandas._libs.algos.ensure_platform_int}
        1    0.000    0.000    0.000    0.000 groupby.py:655(_set_group_selection)
        3    0.000    0.000    0.000    0.000 common.py:1296(is_float_dtype)
        1    0.000    0.000    0.000    0.000 <__array_function__ internals>:2(prod)
        3    0.000    0.000    0.000    0.000 series.py:442(name)
        1    0.000    0.000    0.000    0.000 {method 'all' of 'numpy.ndarray' objects}
        1    0.000    0.000    0.000    0.000 managers.py:1532(__init__)
        2    0.000    0.000    0.000    0.000 grouper.py:761(is_in_obj)
        9    0.000    0.000    0.000    0.000 base.py:3870(_values)
        3    0.000    0.000    0.000    0.000 base.py:5559(ensure_index)
        2    0.000    0.000    0.000    0.000 grouper.py:712(<genexpr>)
        3    0.000    0.000    0.000    0.000 groupby.py:2710(<genexpr>)
        1    0.000    0.000    0.000    0.000 base.py:5672(_maybe_cast_with_dtype)
        2    0.000    0.000    0.000    0.000 grouper.py:714(<genexpr>)
        1    0.000    0.000    0.000    0.000 common.py:1180(needs_i8_conversion)
        2    0.000    0.000    0.000    0.000 inference.py:185(is_array_like)
        2    0.000    0.000    0.000    0.000 ops.py:230(codes)
        2    0.000    0.000    0.000    0.000 common.py:806(is_unsigned_integer_dtype)
        2    0.000    0.000    0.000    0.000 series.py:492(name)
        1    0.000    0.000    0.000    0.000 generic.py:5123(__getattr__)
        2    0.000    0.000    0.000    0.000 grouper.py:830(_is_label_like)
        1    0.000    0.000    0.000    0.000 series.py:398(_set_axis)
        2    0.000    0.000    0.000    0.000 grouper.py:573(result_index)
        1    0.000    0.000    0.000    0.000 <__array_function__ internals>:2(copyto)
        1    0.000    0.000    0.000    0.000 ops.py:402(_get_cython_func_and_vals)
        1    0.000    0.000    0.000    0.000 fromnumeric.py:71(<dictcomp>)
        2    0.000    0.000    0.000    0.000 {method 'view' of 'numpy.ndarray' objects}
        1    0.000    0.000    0.000    0.000 managers.py:1613(internal_values)
        1    0.000    0.000    0.000    0.000 base.py:669(size)
        1    0.000    0.000    0.000    0.000 ops.py:238(names)
        1    0.000    0.000    0.000    0.000 common.py:1025(is_datetime_or_timedelta_dtype)
        4    0.000    0.000    0.000    0.000 grouper.py:567(codes)
        1    0.000    0.000    0.000    0.000 construction.py:520(_try_cast)
        2    0.000    0.000    0.000    0.000 blocks.py:315(dtype)
        1    0.000    0.000    0.000    0.000 _methods.py:56(_all)
        1    0.000    0.000    0.000    0.000 base.py:854(empty)
        1    0.000    0.000    0.000    0.000 {built-in method builtins.min}
        1    0.000    0.000    0.000    0.000 generic.py:353(<dictcomp>)
        4    0.000    0.000    0.000    0.000 {method 'append' of 'list' objects}
        2    0.000    0.000    0.000    0.000 base.py:424(<genexpr>)
        1    0.000    0.000    0.000    0.000 construction.py:580(is_empty_data)
        1    0.000    0.000    0.000    0.000 common.py:149(cast_scalar_indexer)
        1    0.000    0.000    0.000    0.000 series.py:540(_values)
        1    0.000    0.000    0.000    0.000 groupby.py:632(_selected_obj)
        2    0.000    0.000    0.000    0.000 generic.py:354(<genexpr>)
        2    0.000    0.000    0.000    0.000 base.py:590(dtype)
        2    0.000    0.000    0.000    0.000 multi.py:849(nlevels)
        3    0.000    0.000    0.000    0.000 {pandas._libs.lib.is_float}
        1    0.000    0.000    0.000    0.000 blocks.py:135(_check_ndim)
        1    0.000    0.000    0.000    0.000 ops.py:240(<listcomp>)
        2    0.000    0.000    0.000    0.000 {method 'items' of 'dict' objects}
        2    0.000    0.000    0.000    0.000 {method 'index' of 'list' objects}
        1    0.000    0.000    0.000    0.000 <string>:1(__new__)
        2    0.000    0.000    0.000    0.000 multiarray.py:1078(putmask)
        2    0.000    0.000    0.000    0.000 multiarray.py:468(can_cast)
        3    0.000    0.000    0.000    0.000 {method 'clear' of 'dict' objects}
        1    0.000    0.000    0.000    0.000 function.py:48(__call__)
        2    0.000    0.000    0.000    0.000 base.py:637(ndim)
        2    0.000    0.000    0.000    0.000 grouper.py:713(<genexpr>)
        1    0.000    0.000    0.000    0.000 {built-in method builtins.all}
        2    0.000    0.000    0.000    0.000 {pandas._libs.lib.is_integer}
        1    0.000    0.000    0.000    0.000 generic.py:232(attrs)
        1    0.000    0.000    0.000    0.000 {pandas._libs.algos.ensure_float64}
        1    0.000    0.000    0.000    0.000 series.py:381(_constructor)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
        2    0.000    0.000    0.000    0.000 {pandas._libs.lib.is_bool}
        1    0.000    0.000    0.000    0.000 numeric.py:71(_zeros_like_dispatcher)
        1    0.000    0.000    0.000    0.000 fromnumeric.py:2876(_prod_dispatcher)
        1    0.000    0.000    0.000    0.000 multi.py:1673(is_all_dates)
        1    0.000    0.000    0.000    0.000 multiarray.py:75(empty_like)
        1    0.000    0.000    0.000    0.000 blocks.py:233(mgr_locs)
        1    0.000    0.000    0.000    0.000 blocks.py:201(internal_values)
        1    0.000    0.000    0.000    0.000 {built-in method builtins.callable}
        1    0.000    0.000    0.000    0.000 multiarray.py:1043(copyto)
        1    0.000    0.000    0.000    0.000 {pandas._libs.lib.is_iterator}sidM

@jorisvandenbossche
Copy link
Member

@miccoli thanks for your analysis. You are correct that in the case of skipna=False it is doing the grouped sum using the general pure python implementation of groupby. The underlying reason is that the skipna keyword is not yet implemented for the fast grouped reductions written in cython. There is an older issue that covers this enhancement: #15675

@jorisvandenbossche jorisvandenbossche added Performance Memory or execution speed performance and removed Bug Needs Triage Issue that has not been reviewed by a pandas team member labels Nov 20, 2020
@miccoli
Copy link
Contributor Author

miccoli commented Nov 20, 2020

@jorisvandenbossche I see... it was hard to catch that old issue!

Basically this means that

  • df.sum(level=["i0", "i1"], skipna=True)) maps to the (fast) implementation of
    df.groupby(level=["i0", "i1"]).sum()

while

  • df.sum(level=["i0", "i1"], skipna=True)) has no .groupby equivalent, and is implemented in slow pure python.

So, if I got it right, introducing some NaN's in my tests I have:

In [16]: pos = (123,4,5)
    ...: df.loc[pos] = np.NaN
    ...: assert np.isnan(x[pos]).all()

In [17]: s1 = df.sum(level=["i0", "i1"], skipna=False)
    ...: s2 = df.sum(level=["i0", "i1"], skipna=True)

In [18]: s1_ref = np.sum(x, axis=2)
    ...: s2_ref = np.nansum(x, axis=2)

In [19]: np.testing.assert_equal(s1.to_numpy().reshape(s1_ref.shape), s1_ref)

In [20]: np.testing.assert_array_almost_equal_nulp(
    ...:     s2.to_numpy().reshape(s2_ref.shape), s2_ref, nulp=2
    ...: )

Now can compute s2 also with .groupby:

In [23]: sg2 = df.groupby(level=["i0", "i1"]).sum()

In [24]: assert s2.equals(sg2)

In [25]: %timeit df.groupby(level=["i0", "i1"]).sum()
59.2 ms ± 248 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

but there is no way to compute s1 (skipna=False) with groupby.

I have a final comment/question: this implies that the preferred way forward would be to implement the skipna argument in .groupby.

@jorisvandenbossche
Copy link
Member

Basically this means that ..

Indeed. Only with the detail that the skipna=False variant still uses a groupby under the hood, just a slower non-optimized version with a lambda function:

pandas/pandas/core/generic.py

Lines 10518 to 10523 in 7077a08

if hasattr(grouped, name) and skipna:
return getattr(grouped, name)(**kwargs)
axis = self._get_axis_number(axis)
method = getattr(type(self), name)
applyf = lambda x: method(x, axis=axis, skipna=skipna, **kwargs)
return grouped.aggregate(applyf)

So that is also how you can do the skipna=False using groupby in practice at the moment; df.groupby(level=[...]).aggregate(lambda group: group.sum(skipna=False))

I have a final comment/question: this implies that the preferred way forward would be to implement the skipna argument in .groupby.

Indeed (all DataFrame/Series reductions with a level= keyword are basically a short-cut for a groupby operation), and that is what's covered by #15675

@jorisvandenbossche
Copy link
Member

BTW, there is an old stalled PR for this (#15772), contributions to implement this are always welcome! (but no pressure, it's a relatively big chunk of work)

@miccoli
Copy link
Contributor Author

miccoli commented Nov 20, 2020

BTW, there is an old stalled PR for this (#15772), contributions to implement this are always welcome! (but no pressure, it's a relatively big chunk of work)

I would love to do it, but for now I have no spare time...

Please feel free to close this issue, if you think that it overlaps to much with #15675, or to keep it open if you think that it could be helpful to others.

@jorisvandenbossche
Copy link
Member

I would love to do it, but for now I have no spare time...

No problem! (and thanks for taking the time to look into this and open an issue)

Yes, going to close this as a duplicate of #15675, we have enough open issues ;)

@jorisvandenbossche
Copy link
Member

Duplicate of #15675

@jorisvandenbossche jorisvandenbossche marked this as a duplicate of #15675 Nov 20, 2020
@jorisvandenbossche jorisvandenbossche added Duplicate Report Duplicate issue or pull request Groupby labels Nov 20, 2020
@jorisvandenbossche jorisvandenbossche added this to the No action milestone Nov 20, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate Report Duplicate issue or pull request Groupby Performance Memory or execution speed performance
Projects
None yet
Development

No branches or pull requests

2 participants