У меня есть набор данных с именем (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 Я добавлю комментарии к каждому решению.
1,91 мс ± 2,35 мкс на цикл (среднее ± стандартное отклонение из 7 прогонов, по 1000 циклов в каждом)






ОБНОВИТЬ
Должно быть трудно превзойти это (~ в 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 Давай, держи! Пусть решает ОП.
@piRSquared Ваш Counter все еще был медленнее из-за apply. Дело здесь не в том, чтобы с пандами напортачить.
Я думаю, что решение collections в порядке, но называть его в 10 раз быстрее, чем pandas / numpy, вводит в заблуждение. На фреймворке данных даже с парой сотен строк решение piRSquared по факторизации легко превосходит его, а время для образца фрейма данных никогда не имеет большого значения.
@ user3483203 Согласен. Я добавил примечание, что 10-кратное ускорение наблюдается только в примере фрейма данных.
1-й: 252 мкс ± 1,87 мкс на цикл (среднее ± стандартное отклонение из 7 прогонов, по 1000 циклов в каждом)
2-й: 3,65 мс ± 26,2 мкс на цикл (среднее ± стандартное отклонение из 7 прогонов, по 100 циклов в каждом)
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 и idxmaxdf.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 циклов в каждом)
2-й: 1,51 мс ± 4,67 мкс на цикл (среднее ± стандартное отклонение из 7 прогонов, по 1000 циклов в каждом)
3-й: 834 мкс ± 2,66 мкс на цикл (среднее ± стандартное отклонение из 7 прогонов, по 1000 циклов в каждом)
Решение от 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
Извините, я не понял вопрос и исправил.
1,66 мс ± 3,48 мкс на цикл (среднее ± стандартное отклонение из 7 прогонов, по 1000 циклов в каждом)
Как насчет двух групп с 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 циклов в каждом)
Спасибо. Не очень хорошо, судя по тому, что, как я вижу, получают другие. В качестве обратной связи по тестированию времени, если вы повторяете один и тот же небольшой набор данных много раз, это может быть несравнимо с тем, как сделать это один раз для большого набора данных. Многие подобные решения имеют высокую начальную стоимость, но будут работать хорошо, как только начнется обработка. Повторение небольшого набора данных много раз означает, что вы, возможно, просто измеряете начальную стоимость, которая должна быть единовременной. Я предлагаю вам увеличить размер тестируемого набора данных до тех пор, пока выполнение одного цикла не займет несколько секунд.
Подобно @ 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
Я надеюсь, что кто-нибудь проведет сравнительный анализ всех этих материалов. Я бы сделал это сейчас, но здесь уже поздно.