GroupBy панды DataFrame и выберите наиболее распространенное значение
У меня есть кадр данных с тремя строковыми столбцами. Я знаю, что единственное значение в третьем столбце допустимо для каждой комбинации первых двух. Чтобы очистить данные, мне нужно сгруппировать по кадру данных первые два столбца и выбрать наиболее распространенное значение третьего столбца для каждой комбинации.
Мой код:
import pandas as pd
from scipy import stats
source = pd.DataFrame({'Country' : ['USA', 'USA', 'Russia','USA'],
'City' : ['New-York', 'New-York', 'Sankt-Petersburg', 'New-York'],
'Short name' : ['NY','New','Spb','NY']})
print source.groupby(['Country','City']).agg(lambda x: stats.mode(x['Short name'])[0])
Последняя строка кода не работает, она говорит "Ключевая ошибка" Сокращенное имя ", и если я пытаюсь группировать только по Сити, то у меня есть AssertionError. Что я могу исправить?
Ответы
Ответ 1
Вы можете использовать value_counts()
для получения серии счетчиков и получить первую строку:
import pandas as pd
source = pd.DataFrame({'Country' : ['USA', 'USA', 'Russia','USA'],
'City' : ['New-York', 'New-York', 'Sankt-Petersburg', 'New-York'],
'Short name' : ['NY','New','Spb','NY']})
source.groupby(['Country','City']).agg(lambda x:x.value_counts().index[0])
Ответ 2
2019 ответ, pd.Series.mode
доступен.
Используйте groupby
, GroupBy.agg
и примените функцию pd.Series.mode
к каждой группе:
source.groupby(['Country','City'])['Short name'].agg(pd.Series.mode)
Country City
Russia Sankt-Petersburg Spb
USA New-York NY
Name: Short name, dtype: object
Если это необходимо как DataFrame, используйте
source.groupby(['Country','City'])['Short name'].agg(pd.Series.mode).to_frame()
Short name
Country City
Russia Sankt-Petersburg Spb
USA New-York NY
Полезная вещь в Series.mode
заключается в том, что он всегда возвращает Series, что делает его очень совместимым с agg
и apply
, особенно при реконструкции вывода groupby. Это также быстрее.
# Accepted answer.
%timeit source.groupby(['Country','City']).agg(lambda x:x.value_counts().index[0])
# Proposed in this post.
%timeit source.groupby(['Country','City'])['Short name'].agg(pd.Series.mode)
5.56 ms ± 343 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
2.76 ms ± 387 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Series.mode
также хорошо работает, когда есть несколько режимов:
source2 = source.append(
pd.Series({'Country': 'USA', 'City': 'New-York', 'Short name': 'New'}),
ignore_index=True)
# Now 'source2' has two modes for the
# ("USA", "New-York") group, they are "NY" and "New".
source2
Country City Short name
0 USA New-York NY
1 USA New-York New
2 Russia Sankt-Petersburg Spb
3 USA New-York NY
4 USA New-York New
source2.groupby(['Country','City'])['Short name'].agg(pd.Series.mode)
Country City
Russia Sankt-Petersburg Spb
USA New-York [NY, New]
Name: Short name, dtype: object
Или, если вы хотите отдельную строку для каждого режима, вы можете использовать GroupBy.apply
:
source2.groupby(['Country','City'])['Short name'].apply(pd.Series.mode)
Country City
Russia Sankt-Petersburg 0 Spb
USA New-York 0 NY
1 New
Name: Short name, dtype: object
Если вам все равно, какой режим возвращается, если он является одним из них, вам понадобится лямбда, которая вызывает mode
и извлекает первый результат.
source2.groupby(['Country','City'])['Short name'].agg(
lambda x: pd.Series.mode(x)[0])
Country City
Russia Sankt-Petersburg Spb
USA New-York NY
Name: Short name, dtype: object
Вы также можете использовать statistics.mode
из python, но...
source.groupby(['Country','City'])['Short name'].apply(statistics.mode)
Country City
Russia Sankt-Petersburg Spb
USA New-York NY
Name: Short name, dtype: object
... он не работает хорошо, когда приходится иметь дело с несколькими режимами; Ошибка StatisticsError
. Это упоминается в документах:
Если данные пусты, или если нет одного наиболее распространенного значения, выдается StatisticsError.
Но вы можете убедиться сами...
statistics.mode([1, 2])
# ---------------------------------------------------------------------------
# StatisticsError Traceback (most recent call last)
# ...
# StatisticsError: no unique mode; found 2 equally common values
Ответ 3
Для agg
функция lambba получает Series
, которая не имеет атрибута 'Short name'
.
stats.mode
возвращает кортеж из двух массивов, поэтому вам нужно взять первый элемент первого массива в этом кортеже.
С этими двумя простыми заменами:
source.groupby(['Country','City']).agg(lambda x: stats.mode(x)[0][0])
возвращает
Short name
Country City
Russia Sankt-Petersburg Spb
USA New-York NY
Ответ 4
Немного поздно в игре здесь, но я столкнулся с некоторыми проблемами производительности с решением HYRY, поэтому мне пришлось придумать еще один.
Он работает, набирая частоту каждого значения ключа, а затем для каждой клавиши только сохраняя значение, которое появляется с ним чаще всего.
Также есть дополнительное решение, поддерживающее несколько режимов.
В тесте масштабирования, который представляет данные, с которыми я работаю, это сократило время выполнения от 37,4 до 0,5 с!
Здесь приведен код для решения, пример использования и масштабный тест:
import numpy as np
import pandas as pd
import random
import time
test_input = pd.DataFrame(columns=[ 'key', 'value'],
data= [[ 1, 'A' ],
[ 1, 'B' ],
[ 1, 'B' ],
[ 1, np.nan ],
[ 2, np.nan ],
[ 3, 'C' ],
[ 3, 'C' ],
[ 3, 'D' ],
[ 3, 'D' ]])
def mode(df, key_cols, value_col, count_col):
'''
Pandas does not provide a `mode` aggregation function
for its `GroupBy` objects. This function is meant to fill
that gap, though the semantics are not exactly the same.
The input is a DataFrame with the columns `key_cols`
that you would like to group on, and the column
`value_col` for which you would like to obtain the mode.
The output is a DataFrame with a record per group that has at least one mode
(null values are not counted). The `key_cols` are included as columns, `value_col`
contains a mode (ties are broken arbitrarily and deterministically) for each
group, and `count_col` indicates how many times each mode appeared in its group.
'''
return df.groupby(key_cols + [value_col]).size() \
.to_frame(count_col).reset_index() \
.sort_values(count_col, ascending=False) \
.drop_duplicates(subset=key_cols)
def modes(df, key_cols, value_col, count_col):
'''
Pandas does not provide a `mode` aggregation function
for its `GroupBy` objects. This function is meant to fill
that gap, though the semantics are not exactly the same.
The input is a DataFrame with the columns `key_cols`
that you would like to group on, and the column
`value_col` for which you would like to obtain the modes.
The output is a DataFrame with a record per group that has at least
one mode (null values are not counted). The `key_cols` are included as
columns, `value_col` contains lists indicating the modes for each group,
and `count_col` indicates how many times each mode appeared in its group.
'''
return df.groupby(key_cols + [value_col]).size() \
.to_frame(count_col).reset_index() \
.groupby(key_cols + [count_col])[value_col].unique() \
.to_frame().reset_index() \
.sort_values(count_col, ascending=False) \
.drop_duplicates(subset=key_cols)
print test_input
print mode(test_input, ['key'], 'value', 'count')
print modes(test_input, ['key'], 'value', 'count')
scale_test_data = [[random.randint(1, 100000),
str(random.randint(123456789001, 123456789100))] for i in range(1000000)]
scale_test_input = pd.DataFrame(columns=['key', 'value'],
data=scale_test_data)
start = time.time()
mode(scale_test_input, ['key'], 'value', 'count')
print time.time() - start
start = time.time()
modes(scale_test_input, ['key'], 'value', 'count')
print time.time() - start
start = time.time()
scale_test_input.groupby(['key']).agg(lambda x: x.value_counts().index[0])
print time.time() - start
Запуск этого кода напечатает что-то вроде:
key value
0 1 A
1 1 B
2 1 B
3 1 NaN
4 2 NaN
5 3 C
6 3 C
7 3 D
8 3 D
key value count
1 1 B 2
2 3 C 2
key count value
1 1 2 [B]
2 3 2 [C, D]
0.489614009857
9.19386196136
37.4375009537
Надеюсь, это поможет!
Ответ 5
Формально правильный ответ - решение @eumiro. Проблема решения @HYRY состоит в том, что когда у вас есть последовательность чисел типа [1,2,3,4], решение неверное, то есть у вас нет режима. Пример:
import pandas as pd
df = pd.DataFrame({'client' : ['A', 'B', 'A', 'B', 'B', 'C', 'A', 'D', 'D', 'E', 'E', 'E','E','E','A'], 'total' : [1, 4, 3, 2, 4, 1, 2, 3, 5, 1, 2, 2, 2, 3, 4], 'bla':[10, 40, 30, 20, 40, 10, 20, 30, 50, 10, 20, 20, 20, 30, 40]})
Если вы вычислите как @HYRY, вы получите:
df.groupby(['socio']).agg(lambda x: x.value_counts().index[0])
и вы получите:
![enter image description here]()
Что явно неверно (см . Значение A, которое должно быть 1, а не 4), потому что оно не может обрабатывать уникальные значения.
Таким образом, другое решение является правильным:
import scipy.stats
df3.groupby(['client']).agg(lambda x: scipy.stats.mode(x)[0][0])
получение:
![enter image description here]()
Ответ 6
Немного более неудобный, но более быстрый подход для более крупных наборов данных включает в себя получение подсчетов для столбца, представляющего интерес, сортировку наивысших до наименьших, а затем дедупликацию на подмножестве, чтобы сохранить только самые большие случаи.
import pandas as pd
source = pd.DataFrame({'Country' : ['USA', 'USA', 'Russia','USA'],
'City' : ['New-York', 'New-York', 'Sankt-Petersburg', 'New-York'],
'Short name' : ['NY','New','Spb','NY']})
grouped_df = source.groupby(['Country','City','Short name']
)[['Short name']].count().rename(columns={
'Short name':'count'}).reset_index()
grouped_df = grouped_df.sort_values('count',ascending=False)
grouped_df = grouped_df.drop_duplicates(subset=['Country','City']).drop('count', axis=1)
grouped_df
Ответ 7
Проблема здесь в производительности, если у вас много строк, это будет проблемой.
Если это ваш случай, попробуйте следующее:
import pandas as pd
source = pd.DataFrame({'Country' : ['USA', 'USA', 'Russia','USA'],
'City' : ['New-York', 'New-York', 'Sankt-Petersburg', 'New-York'],
'Short_name' : ['NY','New','Spb','NY']})
source.groupby(['Country','City']).agg(lambda x:x.value_counts().index[0])
source.groupby(['Country','City']).Short_name.value_counts().groupby['Country','City']).first()
Ответ 8
Если вам нужен другой подход для его решения, который не зависит от value_counts
или scipy.stats
вы можете использовать коллекцию Counter
from collections import Counter
get_most_common = lambda values: max(Counter(values).items(), key = lambda x: x[1])[0]
Который может быть применен к приведенному выше примеру, как это
src = pd.DataFrame({'Country' : ['USA', 'USA', 'Russia','USA'],
'City' : ['New-York', 'New-York', 'Sankt-Petersburg', 'New-York'],
'Short_name' : ['NY','New','Spb','NY']})
src.groupby(['Country','City']).agg(get_most_common)
Ответ 9
Два главных ответа здесь предлагают:
df.groupby(cols).agg(lambda x:x.value_counts().index[0])
или, предпочтительно
df.groupby(cols).agg(pd.Series.mode)
Однако оба они терпят неудачу в простых крайних случаях, как показано здесь:
df = pd.DataFrame({
'client_id':['A', 'A', 'A', 'A', 'B', 'B', 'B', 'C'],
'date':['2019-01-01', '2019-01-01', '2019-01-01', '2019-01-01', '2019-01-01', '2019-01-01', '2019-01-01', '2019-01-01'],
'location':['NY', 'NY', 'LA', 'LA', 'DC', 'DC', 'LA', np.NaN]
})
Первый:
df.groupby(['client_id', 'date']).agg(lambda x:x.value_counts().index[0])
IndexError
(из-за пустой серии, возвращаемой группой C
). Второй:
df.groupby(['client_id', 'date']).agg(pd.Series.mode)
возвращает ValueError: Function does not reduce
, так как первая группа возвращает список из двух (так как существует два режима). (Как задокументировано здесь, если первая группа вернула один режим, это будет работать!)
Два возможных решения для этого случая:
import scipy
x.groupby(['client_id', 'date']).agg(lambda x: scipy.stats.mode(x)[0])
И решение, данное мне cs95 в комментариях здесь:
def foo(x):
m = pd.Series.mode(x);
return m.values[0] if not m.empty else np.nan
df.groupby(['client_id', 'date']).agg(foo)
Тем не менее, все это медленно и не подходит для больших наборов данных. Решение, которое я в итоге использовал, которое а) может справиться с этими случаями и б) намного, намного быстрее, является слегка измененной версией ответа abw33 (который должен быть выше):
def get_mode_per_column(dataframe, group_cols, col):
return (dataframe.fillna(-1) # NaN placeholder to keep group
.groupby(group_cols + [col])
.size()
.to_frame('count')
.reset_index()
.sort_values('count', ascending=False)
.drop_duplicates(subset=group_cols)
.drop(columns=['count'])
.sort_values(group_cols)
.replace(-1, np.NaN)) # restore NaNs
group_cols = ['client_id', 'date']
non_grp_cols = list(set(df).difference(group_cols))
output_df = get_mode_per_column(df, group_cols, non_grp_cols[0]).set_index(group_cols)
for col in non_grp_cols[1:]:
output_df[col] = get_mode_per_column(df, group_cols, col)[col]
По сути, метод работает по одному столбцу за раз и выводит df, поэтому вместо concat
, который является интенсивным, вы обрабатываете первый как df, а затем итеративно добавляете выходной массив (values.flatten()
) как столбец в дф.