GroupBy pandas DataFrame и выберите наиболее распространенное значение


99

У меня есть фрейм данных с тремя строковыми столбцами. Я знаю, что единственное значение в 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. Что я могу сделать?

Ответы:


145

Вы можете использовать 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])

Если вам интересно, как выполнять другие функции agg в .agg (), попробуйте это.

# Let's add a new col,  account
source['account'] = [1,2,3,3]

source.groupby(['Country','City']).agg(mod  = ('Short name', \
                                        lambda x: x.value_counts().index[0]),
                                        avg = ('account', 'mean') \
                                      )

Я обнаружил, что stats.mode может показывать неверные ответы в случае строковых переменных. Так выглядит более надежно.
Вячеслав Нефедов

1
Разве этого не должно быть .value_counts(ascending=False)?
Рядовой

1
@Private: ascending=Falseуже является значением по умолчанию, поэтому явно устанавливать порядок не требуется.
Schmuddi

2
Как сказал Жако, pd.Series.modeтеперь это более уместно и быстрее.
Daisuke SHIBATO

Как я могу использовать это решение с несколькими различными функциями агрегирования, например, если у меня есть несколько столбцов, таких как «Краткое имя», и, кроме того, числовые столбцы, которые я хочу агрегировать с помощью функции суммы?
constiii

99

Панды> = 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приподнята. Об этом упоминается в документации:

Если данные пусты или нет одного наиболее распространенного значения, возникает ошибка StatisticsError.

Но вы сами видите ...

statistics.mode([1, 2])
# ---------------------------------------------------------------------------
# StatisticsError                           Traceback (most recent call last)
# ...
# StatisticsError: no unique mode; found 2 equally common values

@JoshFriedlander, df.groupby(cols).agg(pd.Series.mode)кажется, мне подходит . Если это не сработает, то второе предположение будет df.groupby(cols).agg(lambda x: pd.Series.mode(x).values[0]).
cs95

Спасибо (как всегда!) Ваш второй вариант улучшает ситуацию для меня, но я получаю IndexError: index 0 is out of bounds for axis 0 with size 0(вероятно, потому, что есть группы, в которых серия имеет только NaN). Добавление dropna=Falseрешает эту проблему , но, кажется, поднимает '<' not supported between instances of 'float' and 'str'(моя серия - это строки). (Счастлив превратить этот вопрос в новый вопрос, если хотите.)
Джош Фридлендер,

2
@JoshFriedlander Определите, 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.
cs95

1
Я должен добавить, что если вы хотите включить подсчет np.nan, это можно сделать с помощью df.groupy(cols).agg(lambda x: x.mode(dropna=False).iloc[0])режима, при условии, что вас не интересуют связи и вам нужен только один режим.
irene

17

Ибо aggфункция лямбба получает a 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

1
@ViacheslavNefedov - да, но возьмите решение @HYRY, которое использует чистые панды. Нет необходимости в scipy.stats.
eumiro 05

14

Немного поздно в игре, но у меня возникли проблемы с производительностью с решением 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

Надеюсь это поможет!


Это самый быстрый способ, которым я пришел .. Спасибо!
FtoTheZ

1
Есть ли способ использовать этот подход, но непосредственно внутри параметров agg?, Например. agg({'f1':mode,'f2':np.sum})
Пабло

1
@PabloA, к сожалению, нет, потому что интерфейс не совсем тот. Я рекомендую сделать это как отдельную операцию, а затем объединить ваши результаты. И, конечно, если производительность не является проблемой, вы можете использовать решение HYRY, чтобы ваш код был более кратким.
abw333 01

@ abw333 Я использовал решение HYRY, но у меня возникли проблемы с производительностью ... Я надеюсь, что команда разработчиков pandas поддерживает больше функций в этом aggметоде.
Пабло

Определенно лучший вариант для больших DataFrames. У меня было 83 миллиона строк и 2,5 миллиона уникальных групп. Это заняло 28 секунд на колонку, в то время как агрегация заняла более 11 минут на колонку.
ALollz

5

Два основных ответа здесь предполагают:

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].values

По сути, метод работает с одним столбцом за раз и выводит df, поэтому вместо того concat, что является интенсивным, вы обрабатываете первый как df, а затем итеративно добавляете выходной массив ( values.flatten()) как столбец в df.


3

Формально правильный ответ - Решение @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

1

Если вам нужен другой подход к его решению, который не зависит от 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/...
Ирен

1

Если вы не хотите включать значения 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 будет считаться отдельно.


0

Проблема здесь в производительности, если у вас много строк, это будет проблемой.

Если это ваш случай, попробуйте следующее:

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()

0

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

>>> 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
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.