Почему «groupby` с `as_index=False» даже медленнее, чем » groupby` с `reset_index`

#python #pandas #dataframe #performance #pandas-groupby

Вопрос:

Я только недавно столкнулся с этим странным поведением Панд groupby .

У меня есть этот фрейм данных:

 >>> df = pd.DataFrame({'a': [1, 2, 3, 1, 2, 3], 'b': [4, 5, 6, 7, 8, 9]})
>>> df
   a  b
0  1  4
1  2  5
2  3  6
3  1  7
4  2  8
5  3  9
>>> 
 

И я хочу в groupby колонку a и sum в колонку b .

Обычно groupby столбец будет иметь значение индекса, но as_index=False не будет:

 >>> df.groupby('a')['b'].sum()
a
1    11
2    13
3    15
Name: b, dtype: int64
>>> 
 

Но когда я засекаю их время:

 >>> timeit(lambda: df.groupby('a')['b'].sum(), number=1000)
0.5426476000000093
>>> timeit(lambda: df.groupby('a')['b'].sum(), number=10000)
4.912795499999902
>>> timeit(lambda: df.groupby('a', as_index=False)['b'].sum(), number=1000)
1.419923899999958
>>> timeit(lambda: df.groupby('a', as_index=False)['b'].sum(), number=10000)
11.907147600000144
>>> 
 

Вы можете видеть, что по какой-то причине, groupby с as_index=False 2,75 раза медленнее!

Не только это! Это даже медленнее, чем reset_index !

 >>> timeit(lambda: df.groupby('a', as_index=False)['b'].sum(), number=1000)
1.419923899999958
>>> timeit(lambda: df.groupby('a', as_index=False)['b'].sum(), number=10000)
11.907147600000144
>>> timeit(lambda: df.groupby('a')['b'].sum().reset_index(), number=1000)
1.0641113000001496
>>> timeit(lambda: df.groupby('a')['b'].sum().reset_index(), number=10000)
10.01520289999985
>>> 
 

И reset_index , очевидно, также дает тот же результат, что и as_index=False :

 >>> df.groupby('a')['b'].sum().reset_index()
   a   b
0  1  11
1  2  13
2  3  15
>>> 
 

as_index=False :

 >>> df.groupby('a', as_index=False)['b'].sum()
   a   b
0  1  11
1  2  13
2  3  15
>>> 
 

Я могу понять, что as_index=False это может быть медленнее, но не настолько… Также главное, что я не могу понять, почему это reset_index быстрее? Это дополнительная функция…

Почему это так? В чем заключается реализация as_index ?

Я действительно удивлен, я даже подумал , что очень возможно, что as_index=False это будет быстрее, чем as_index=True , так как для этого не нужно устанавливать столбец в качестве индекса.

Но все наоборот, на самом деле as_index=True он в 2,75 раза быстрее… И даже reset_index быть быстрее, чем as_index=False .

Если это так, почему as_index=False бы также просто не использовать reset_index ?

Комментарии:

1. это будет отличная возможность для вас взглянуть на исходный код и, возможно, отправить PR, чтобы исправить это. питание с открытым исходным кодом!

2. @sammywemmy Правда, разберусь в этом!

3. @sammywemmy Это, честно говоря, для меня интересный вопрос 😛

Ответ №1:

as_index=True является значением по умолчанию, так как группер использует внутренний индекс и сбрасывает его, если as_index установлено значение False :

см. core.groupby.py источник

         if not self.as_index:
            self._insert_inaxis_grouper_inplace(result)
 

Разница во времени между True / False на самом деле минимальна, вы должны использовать больший кадр данных для проверки скорости.

Здесь на 600 тысяч строк:

True : 30.7 ms ± 3.05 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

False : 32.5 ms ± 2.66 ms per loop (mean ± std. dev. of 7 runs, 10 loops each

Таким образом, разница не пропорциональна (в 2 раза медленнее), а скорее фиксирована (на 2,5 мс медленнее), что является меньшим бременем.

Теперь о том , почему исходный код не используется reset_index , ну, панды многое делают внутри, я только предполагаю, поскольку код сложный, но, вероятно, существует множество проверок, которые делают больше, чем просто сброс индекса.

Комментарии:

1. Круто, но было бы неплохо узнать, почему это reset_index быстрее.

2. Только записка… Где ты провел эту черту? какая строка 🙂

3. Добавлен пример _insert_inaxis_grouper_inplace функции.

Ответ №2:

Назначьте ответу @mozway _insert_inaxis_grouper_inplace функцию:

 def _insert_inaxis_grouper_inplace(self, result: DataFrame) -> None:
    # zip in reverse so we can always insert at loc 0
    columns = result.columns
    for name, lev, in_axis in zip(
        reversed(self.grouper.names),
        reversed(self.grouper.get_group_levels()),
        reversed([grp.in_axis for grp in self.grouper.groupings]),
    ):
        # GH #28549
        # When using .apply(-), name will be in columns already
        if in_axis and name not in columns:
            result.insert(0, name, lev)
 

Как вы можете видеть, он использует insert , а не сбрасывает индекс.

Поэтому я подозреваю, что если он использует:

 def _insert_inaxis_grouper_inplace(self, result: DataFrame) -> None:
    # zip in reverse so we can always insert at loc 0
    columns = result.columns
    for name, lev, in_axis in zip(
        reversed(self.grouper.names),
        reversed(self.grouper.get_group_levels()),
        reversed([grp.in_axis for grp in self.grouper.groupings]),
    ):
        # GH #28549
        # When using .apply(-), name will be in columns already
        if in_axis and name not in columns:
            result = result.reset_index()
 

Хотя я не совсем уверен в настройке, но я ожидаю, что все вышеперечисленное сработает…

Тогда, вероятно, это было бы быстрее.