У меня есть фрейм данных с тремя строковыми столбцами. Я знаю, что единственное значение в 3-м столбце действительно для каждой комбинации первых двух. Чтобы очистить данные, мне нужно сгруппировать данные по фреймам по первым двум столбцам и выбрать наиболее распространенное значение третьего столбца для каждой комбинации.
Мой код:
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. Что я могу сделать?
.value_counts(ascending=False)
?ascending=False
уже является значением по умолчанию, поэтому явно устанавливать порядок не требуется.pd.Series.mode
теперь это более уместно и быстрее.Панды> = 0,16
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
приподнята. Об этом упоминается в документации:Но вы сами видите ...
statistics.mode([1, 2]) # --------------------------------------------------------------------------- # StatisticsError Traceback (most recent call last) # ... # StatisticsError: no unique mode; found 2 equally common values
источник
df.groupby(cols).agg(pd.Series.mode)
кажется, мне подходит . Если это не сработает, то второе предположение будетdf.groupby(cols).agg(lambda x: pd.Series.mode(x).values[0])
.IndexError: index 0 is out of bounds for axis 0 with size 0
(вероятно, потому, что есть группы, в которых серия имеет только NaN). Добавлениеdropna=False
решает эту проблему , но, кажется, поднимает'<' not supported between instances of 'float' and 'str'
(моя серия - это строки). (Счастлив превратить этот вопрос в новый вопрос, если хотите.)def foo(x): m = pd.Series.mode(x); return m.values[0] if not m.empty else np.nan
а затем используйтеdf.groupby(cols).agg(foo)
. Если это не сработает,foo
немного поиграйте с реализацией . Если у вас все еще возникают проблемы с запуском, я рекомендую открыть новый Q.np.nan
, это можно сделать с помощьюdf.groupy(cols).agg(lambda x: x.mode(dropna=False).iloc[0])
режима, при условии, что вас не интересуют связи и вам нужен только один режим.Ибо
agg
функция лямбба получает aSeries
, у которого нет'Short name'
атрибута.stats.mode
возвращает кортеж из двух массивов, поэтому вам нужно взять первый элемент первого массива в этом кортеже.С помощью этих двух простых изменений:
source.groupby(['Country','City']).agg(lambda x: stats.mode(x)[0][0])
возвращается
источник
scipy.stats
.Немного поздно в игре, но у меня возникли проблемы с производительностью с решением 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
Надеюсь это поможет!
источник
agg({'f1':mode,'f2':np.sum})
agg
методе.Два основных ответа здесь предполагают:
df.groupby(cols).agg(lambda x:x.value_counts().index[0])
или, предпочтительно
Однако оба из них не работают в простых крайних случаях, как показано здесь:
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].values
По сути, метод работает с одним столбцом за раз и выводит df, поэтому вместо того
concat
, что является интенсивным, вы обрабатываете первый как df, а затем итеративно добавляете выходной массив (values.flatten()
) как столбец в df.источник
Формально правильный ответ - Решение @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, вы получите:
>>> print(df.groupby(['client']).agg(lambda x: x.value_counts().index[0])) total bla client A 4 30 B 4 40 C 1 10 D 3 30 E 2 20
Что явно неверно (см . Значение A, которое должно быть 1, а не 4 ), потому что оно не может обрабатывать уникальные значения.
Таким образом, верно другое решение:
>>> import scipy.stats >>> print(df.groupby(['client']).agg(lambda x: scipy.stats.mode(x)[0][0])) total bla client A 1 10 B 4 40 C 1 10 D 3 30 E 2 20
источник
Если вам нужен другой подход к его решению, который не зависит от
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)
источник
pd.Series.mode
илиpd.Series.value_counts().iloc[0]
- но если у вас есть значения NaN, которые вы хотите посчитать, это не удастся. Каждое появление NaN будет рассматриваться как отличное от других NaN, поэтому каждое NaN считается имеющим счет1
. См stackoverflow.com/questions/61102111/...Если вы не хотите включать значения NaN , использование
Counter
намного быстрее, чемpd.Series.mode
илиpd.Series.value_counts()[0]
:def get_most_common(srs): x = list(srs) my_counter = Counter(x) return my_counter.most_common(1)[0][0] df.groupby(col).agg(get_most_common)
должно сработать. Это не удастся, если у вас есть значения NaN, поскольку каждое NaN будет считаться отдельно.
источник
Проблема здесь в производительности, если у вас много строк, это будет проблемой.
Если это ваш случай, попробуйте следующее:
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()
источник
Немного неуклюжий, но более быстрый подход для больших наборов данных включает в себя получение счетчиков для интересующего столбца, сортировку счетчиков от наибольшего к наименьшему, а затем дедупликацию на подмножестве, чтобы сохранить только наибольшие наблюдения. Пример кода следующий:
>>> 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()\ .sort_values('count', ascending=False)\ .drop_duplicates(subset=['Country', 'City'])\ .drop('count', axis=1) >>> print(grouped_df) Country City Short name 1 USA New-York NY 0 Russia Sankt-Petersburg Spb
источник