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це вже значення за замовчуванням, тому немає необхідності встановлювати порядок явно.
Шмудді

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те , що вона завжди повертає серію, що робить його дуже сумісний з 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

... це не працює добре, коли доводиться мати справу з декількома режимами; a 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трохи поспішайте з реалізацією . Якщо у вас все ще виникають проблеми, рекомендую відкрити нове
запитання

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

17

Адже aggфункція lambba отримує 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

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

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

Безумовно, шлях до великих фреймів даних. У мене було 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 Solution. Проблема рішення @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.