Более быстрая альтернатива для выполнения операции pandas groupby

У меня есть набор данных с именем (person_name), днем ​​и цветом (shirt_color) в виде столбцов.

Каждый человек носит рубашку определенного цвета в определенный день. Количество дней может быть произвольным.

Например. Вход:

name  day  color
----------------
John   1   White
John   2   White
John   3   Blue
John   4   Blue
John   5   White
Tom    2   White
Tom    3   Blue
Tom    4   Blue
Tom    5   Black
Jerry  1   Black
Jerry  2   Black
Jerry  4   Black
Jerry  5   White

Мне нужно найти наиболее часто используемый цвет каждым человеком.

Например. результат:

name    color
-------------
Jerry   Black
John    White
Tom     Blue

Для получения результатов я выполняю следующую операцию, которая работает нормально, но довольно медленно:

most_frquent_list = [[name, group.color.mode()[0]] 
                        for name, group in data.groupby('name')]
most_frquent_df = pd.DataFrame(most_frquent_list, columns=['name', 'color'])

Теперь предположим, что у меня есть набор данных с 5 миллионами уникальных имен. Каков самый лучший / самый быстрый способ выполнить указанную выше операцию?

Я надеюсь, что кто-нибудь проведет сравнительный анализ всех этих материалов. Я бы сделал это сейчас, но здесь уже поздно.

André C. Andersen 23.08.2018 00:10

@ AndréC.Andersen Я добавлю комментарии к каждому решению.

DYZ 23.08.2018 00:21

1,91 мс ± 2,35 мкс на цикл (среднее ± стандартное отклонение из 7 прогонов, по 1000 циклов в каждом)

DYZ 23.08.2018 00:23
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
9
3
10 347
7
Перейти к ответу Данный вопрос помечен как решенный

Ответы 7

ОБНОВИТЬ

Должно быть трудно превзойти это (~ в 10 раз быстрее на образце daraframe, чем любое предлагаемое решение pandas и в 1,5 раза быстрее, чем предлагаемое решение numpy). Суть в том, чтобы держаться подальше от панд и использовать itertools.groupby, который намного лучше справляется с нечисловыми данными.

from itertools import groupby
from collections import Counter

pd.Series({x: Counter(z[-1] for z in y).most_common(1)[0][0] for x,y 
          in groupby(sorted(df.values.tolist()), 
                            key=lambda x: x[0])})
# Jerry    Black
# John     White
# Tom       Blue

Старый ответ

Вот еще один способ. На самом деле он медленнее оригинального, но я оставлю его здесь:

data.groupby('name')['color']\
    .apply(pd.Series.value_counts)\
    .unstack().idxmax(axis=1)
# name
# Jerry    Black
# John     White
# Tom       Blue

Ха! Я только что это сделал. Я удалю

piRSquared 23.08.2018 00:12

@piRSquared Давай, держи! Пусть решает ОП.

DYZ 23.08.2018 00:13

@piRSquared Ваш Counter все еще был медленнее из-за apply. Дело здесь не в том, чтобы с пандами напортачить.

DYZ 23.08.2018 00:14

Я думаю, что решение collections в порядке, но называть его в 10 раз быстрее, чем pandas / numpy, вводит в заблуждение. На фреймворке данных даже с парой сотен строк решение piRSquared по факторизации легко превосходит его, а время для образца фрейма данных никогда не имеет большого значения.

user3483203 23.08.2018 00:16

@ user3483203 Согласен. Я добавил примечание, что 10-кратное ускорение наблюдается только в примере фрейма данных.

DYZ 23.08.2018 00:18

1-й: 252 мкс ± 1,87 мкс на цикл (среднее ± стандартное отклонение из 7 прогонов, по 1000 циклов в каждом)

DYZ 23.08.2018 00:26

2-й: 3,65 мс ± 26,2 мкс на цикл (среднее ± стандартное отклонение из 7 прогонов, по 100 циклов в каждом)

DYZ 23.08.2018 00:27
Ответ принят как подходящий

numpy.add.at и pandas.factorize Нумпи

Это должно быть быстро. Тем не менее, я попытался организовать его так, чтобы он был удобочитаемым.

i, r = pd.factorize(df.name)
j, c = pd.factorize(df.color)
n, m = len(r), len(c)

b = np.zeros((n, m), dtype=np.int64)

np.add.at(b, (i, j), 1)
pd.Series(c[b.argmax(1)], r)

John     White
Tom       Blue
Jerry    Black
dtype: object

groupby, size и idxmax

df.groupby(['name', 'color']).size().unstack().idxmax(1)

name
Jerry    Black
John     White
Tom       Blue
dtype: object

name
Jerry    Black
John     White
Tom       Blue
Name: color, dtype: object

Counter

¯\_(ツ)_/¯

from collections import Counter

df.groupby('name').color.apply(lambda c: Counter(c).most_common(1)[0][0])

name
Jerry    Black
John     White
Tom       Blue
Name: color, dtype: object

1-й: 362 мкс ± 1,47 мкс на цикл (среднее ± стандартное отклонение из 7 прогонов, по 1000 циклов в каждом)

DYZ 23.08.2018 00:24

2-й: 1,51 мс ± 4,67 мкс на цикл (среднее ± стандартное отклонение из 7 прогонов, по 1000 циклов в каждом)

DYZ 23.08.2018 00:25

3-й: 834 мкс ± 2,66 мкс на цикл (среднее ± стандартное отклонение из 7 прогонов, по 1000 циклов в каждом)

DYZ 23.08.2018 00:26

Решение от pd.Series.mode

df.groupby('name').color.apply(pd.Series.mode).reset_index(level=1,drop=True)
Out[281]: 
name
Jerry    Black
John     White
Tom       Blue
Name: color, dtype: object

Извините, я не понял вопрос и исправил.

BENY 23.08.2018 00:06

1,66 мс ± 3,48 мкс на цикл (среднее ± стандартное отклонение из 7 прогонов, по 1000 циклов в каждом)

DYZ 23.08.2018 00:22

Как насчет двух групп с transform(max)?

df = df.groupby(["name", "color"], as_index=False, sort=False).count()
idx = df.groupby("name", sort=False).transform(max)["day"] == df["day"]
df = df[idx][["name", "color"]].reset_index(drop=True)

Выход:

    name  color
0   John  White
1    Tom   Blue
2  Jerry  Black

12,2 мс ± 48,4 мкс на цикл (среднее ± стандартное отклонение из 7 прогонов, по 100 циклов в каждом)

DYZ 23.08.2018 00:21

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

André C. Andersen 23.08.2018 11:14

Подобно @ piRSquared pd.factorize и np.add.at ans.

Мы кодируем жала в столбцы, используя

i, r = pd.factorize(df.name)
j, c = pd.factorize(df.color)
n, m = len(r), len(c)
b = np.zeros((n, m), dtype=np.int64)

Но тогда вместо этого:

np.add.at(b, (i, j), 1)
max_columns_after_add_at = b.argmax(1)

Мы получаем max_columns_after_add_at с помощью функции jited, чтобы добавить и найти максимум в том же цикле:

@nb.jit(nopython=True, cache=True)
def add_at(x, rows, cols, val):
    max_vals = np.zeros((x.shape[0], ), np.int64)
    max_inds = np.zeros((x.shape[0], ), np.int64)
    for i in range(len(rows)):
        r = rows[i]
        c = cols[i]
        x[r, c]+=1
        if (x[r, c] > max_vals[r]):
            max_vals[r] = x[r, c]
            max_inds[r] = c
    return max_inds

А затем в конце получим фрейм данных,

ans = pd.Series(c[max_columns_after_add_at], r)

Итак, разница в том, как мы делаем argmax(axis=1) after np.add.at().

Временной анализ

import numpy as np
import numba as nb
m = 100000
n = 100000
rows = np.random.randint(low = 0, high = m, size=10000)
cols = np.random.randint(low = 0, high = n, size=10000)

Итак, это:

%%time
x = np.zeros((m,n))
np.add.at(x, (rows, cols), 1)
maxs = x.argmax(1)

дает:

CPU times: user 12.4 s, sys: 38 s, total: 50.4 s Wall time: 50.5 s

И это

%%time
x = np.zeros((m,n))
maxs2 = add_at(x, rows, cols, 1)

дает

CPU times: user 108 ms, sys: 39.4 s, total: 39.5 s Wall time: 38.4 s

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

l = """name  day  color
John   1   White
John   2   White
John   3   Blue
John   4   Blue
John   5   White
Tom    2   White
Tom    3   Blue
Tom    4   Blue
Tom    5   Black
Jerry  1   Black
Jerry  2   Black
Jerry  4   Black
Jerry  5   White""".split('\n')

Теперь нам нужно преобразовать этот список в список кортежей.

df = pd.DataFrame([tuple(i.split()) for i in l])
headers = df.iloc[0]
new_df  = pd.DataFrame(df.values[1:], columns=headers)

Используйте new_df сейчас, и вы можете сослаться на ответы выше @piRSquared

Большинство результатов тестов, обсуждаемых в других ответах, искажены из-за измерения с использованием тривиально небольшого тестового DataFrame в качестве входных данных. У Pandas есть фиксированное, но обычно незначительное время настройки, но оно будет значительным после обработки этого крошечного набора данных.

Для более крупного набора данных самый быстрый метод - использование pd.Series.mode() с agg():

df.groupby('name')['color'].agg(pd.Series.mode)

Испытательный стенд:

arr = np.array([
    ('John',   1,   'White'),
    ('John',   2,  'White'),
    ('John',   3,   'Blue'),
    ('John',   4,   'Blue'),
    ('John',   5,   'White'),
    ('Tom',    2,   'White'),
    ('Tom',    3,   'Blue'),
    ('Tom',    4,   'Blue'),
    ('Tom',    5,   'Black'),
    ('Jerry',  1,   'Black'),
    ('Jerry',  2,   'Black'),
    ('Jerry',  4,   'Black'),
    ('Jerry',  5,   'White')],
    dtype=[('name', 'O'), ('day', 'i8'), ('color', 'O')])

from timeit import Timer
from itertools import groupby
from collections import Counter

df = pd.DataFrame.from_records(arr).sample(100_000, replace=True)

def factorize():
    i, r = pd.factorize(df.name)
    j, c = pd.factorize(df.color)
    n, m = len(r), len(c)

    b = np.zeros((n, m), dtype=np.int64)

    np.add.at(b, (i, j), 1)
    return pd.Series(c[b.argmax(1)], r)

t_factorize = Timer(lambda: factorize())
t_idxmax = Timer(lambda: df.groupby(['name', 'color']).size().unstack().idxmax(1))
t_aggmode = Timer(lambda: df.groupby('name')['color'].agg(pd.Series.mode))
t_applymode = Timer(lambda: df.groupby('name').color.apply(pd.Series.mode).reset_index(level=1,drop=True))
t_aggcounter = Timer(lambda: df.groupby('name')['color'].agg(lambda c: Counter(c).most_common(1)[0][0]))
t_applycounter = Timer(lambda: df.groupby('name').color.apply(lambda c: Counter(c).most_common(1)[0][0]))
t_itertools = Timer(lambda: pd.Series(
    {x: Counter(z[-1] for z in y).most_common(1)[0][0] for x,y
      in groupby(sorted(df.values.tolist()), key=lambda x: x[0])}))

n = 100
[print(r) for r in (
    f"{t_factorize.timeit(number=n)=}",
    f"{t_idxmax.timeit(number=n)=}",
    f"{t_aggmode.timeit(number=n)=}",
    f"{t_applymode.timeit(number=n)=}",
    f"{t_applycounter.timeit(number=n)=}",
    f"{t_aggcounter.timeit(number=n)=}",
    f"{t_itertools.timeit(number=n)=}",
)]
t_factorize.timeit(number=n)=1.325189442
t_idxmax.timeit(number=n)=1.0613339019999999
t_aggmode.timeit(number=n)=1.0495010750000002
t_applymode.timeit(number=n)=1.2837302849999999
t_applycounter.timeit(number=n)=1.9432825890000007
t_aggcounter.timeit(number=n)=1.8283823839999993
t_itertools.timeit(number=n)=7.0855046380000015

Другие вопросы по теме