Выберите максимальное количество строк в группе - проблема с производительностью панд - PullRequest
0 голосов
/ 17 мая 2018

Я выбираю одну максимальную строку для каждой группы и использую groupby / agg, чтобы вернуть значения индекса и выбрать строки, используя loc.

Например, для группировки по "Id", а затем выберите строку с наибольшим значением "delta":

selected_idx = df.groupby("Id").apply(lambda df: df.delta.argmax())
selected_rows = df.loc[selected_idx, :]

Однако так медленно. На самом деле, мой ноутбук i7 / 16G RAM зависает, когда я использую этот запрос на 13 миллионов строк.

У меня есть два вопроса к экспертам:

  1. Как я могу заставить этот запрос быстро выполняться в пандах? Что я делаю не так?
  2. Почему эта операция такая дорогая?

[Обновление] Большое спасибо за анализ @unutbu! sort_drop это так! На моей машине i7 / 32GRAM groupby + idxmax зависает почти 14 часов (никогда ничего не возвращает), однако sort_drop справился с этим МЕНЬШЕ, ЧЕМ МИНУТУ!

Мне все еще нужно посмотреть, как pandas реализует каждый метод, но проблемы пока решены! Я люблю StackOverflow.

Ответы [ 2 ]

0 голосов
/ 26 июня 2018

Используя джит Нумбы

from numba import njit
import numpy as np

@njit
def nidxmax(bins, k, weights):
    out = np.zeros(k, np.int64)
    trk = np.zeros(k)
    for i, w in enumerate(weights - (weights.min() - 1)):
        b = bins[i]
        if w > trk[b]:
            trk[b] = w
            out[b] = i
    return np.sort(out)

def with_numba_idxmax(df):
    f, u = pd.factorize(df.Id)
    return df.iloc[nidxmax(f, len(u), df.delta.values)]

Заимствование у @ unutbu

def make_df(N):
    # lots of small groups
    df = pd.DataFrame(np.random.randint(N//10+1, size=(N, 2)), columns=['Id','delta'])
    # few large groups
    # df = pd.DataFrame(np.random.randint(10, size=(N, 2)), columns=['Id','delta'])
    return df

Prime jit

with_numba_idxmax(make_df(10));

Тест

df = make_df(2**20)


%timeit with_numba_idxmax(df)
%timeit using_sort_drop(df)

47.4 ms ± 99.8 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
194 ms ± 451 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
0 голосов
/ 17 мая 2018

Самый быстрый вариант зависит не только от длины DataFrame (в данном случае около 13M строк), но и от количества групп. Ниже приведены перфлоплоты, которые сравнивают несколько способов нахождения максимума в каждой группе:

Если существует только несколько (больших) групп , using_idxmax может быть самым быстрым вариантом: enter image description here

Если имеется много (маленьких) групп и размер Фрейма данных не слишком велик , using_sort_drop может быть самым быстрым вариантом: enter image description here

Имейте в виду, однако, что хотя using_sort_drop, using_sort и using_rank начинают выглядеть очень быстро, по мере увеличения N = len(df) их скорость относительно других параметров быстро исчезает. Для достаточно больших N, using_idxmax становится самым быстрым вариантом , даже если есть много групп.

using_sort_drop, using_sort и using_rank сортирует DataFrame (или группы в DataFrame). В среднем сортировка составляет O(N * log(N)), тогда как другие методы используют операции O(N). Вот почему такие методы, как using_idxmax, бьют using_sort_drop для очень больших фреймов данных.

Имейте в виду, что результаты тестов могут отличаться по ряду причин, включая характеристики компьютера, ОС и версии программного обеспечения. Поэтому важно запускать тесты на своем компьютере и с тестовыми данными, адаптированными к вашей ситуации.

Исходя из вышеприведенных перфлотов, using_sort_drop может быть - вариант, который стоит рассмотреть для вашего DataFrame из 13M строк, особенно если в нем много (маленьких) групп. В противном случае, я подозреваю, что using_idxmax будет самым быстрым вариантом, но, опять же, важно проверить эталонные тесты на вашем компьютере.


Вот настройка, которую я использовал для создания перфлоплот :

import numpy as np
import pandas as pd 
import perfplot

def make_df(N):
    # lots of small groups
    df = pd.DataFrame(np.random.randint(N//10+1, size=(N, 2)), columns=['Id','delta'])
    # few large groups
    # df = pd.DataFrame(np.random.randint(10, size=(N, 2)), columns=['Id','delta'])
    return df


def using_idxmax(df):
    return df.loc[df.groupby("Id")['delta'].idxmax()]

def max_mask(s):
    i = np.asarray(s).argmax()
    result = [False]*len(s)
    result[i] = True
    return result

def using_custom_mask(df):
    mask = df.groupby("Id")['delta'].transform(max_mask)
    return df.loc[mask]

def using_isin(df):
    idx = df.groupby("Id")['delta'].idxmax()
    mask = df.index.isin(idx)
    return df.loc[mask]

def using_sort(df):
    df = df.sort_values(by=['delta'], ascending=False, kind='mergesort')
    return df.groupby('Id', as_index=False).first()

def using_rank(df):
    mask = (df.groupby('Id')['delta'].rank(method='first', ascending=False) == 1)
    return df.loc[mask]

def using_sort_drop(df):
    # Thanks to jezrael
    # /11039184/vyberite-maksimalnoe-kolichestvo-strok-v-gruppe-problema-s-proizvoditelnosty-pandcomment87795818_50389889
    return df.sort_values(by=['delta'], ascending=False, kind='mergesort').drop_duplicates('Id')

def using_apply(df):
    selected_idx = df.groupby("Id").apply(lambda df: df.delta.argmax())
    return df.loc[selected_idx]

def check(df1, df2):
    df1 = df1.sort_values(by=['Id','delta'], kind='mergesort').reset_index(drop=True)
    df2 = df2.sort_values(by=['Id','delta'], kind='mergesort').reset_index(drop=True)
    return df1.equals(df2)

perfplot.show(
    setup=make_df,
    kernels=[using_idxmax, using_custom_mask, using_isin, using_sort, 
             using_rank, using_apply, using_sort_drop],
    n_range=[2**k for k in range(2, 20)],
    logx=True,
    logy=True,
    xlabel='len(df)',
    repeat=75,
    equality_check=check)

Другой способ оценки производительности - использовать IPython% timeit :

In [55]:  df = make_df(2**20)

In [56]: %timeit using_sort_drop(df)
1 loop, best of 3: 403 ms per loop

In [57]: %timeit using_rank(df)
1 loop, best of 3: 1.04 s per loop

In [58]: %timeit using_idxmax(df)
1 loop, best of 3: 15.8 s per loop
...