Подтвердить что ты не робот

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. Что я могу исправить?

4b9b3361

Ответ 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()) как столбец в дф.