GroupBy gấu trúc DataFrame và chọn giá trị phổ biến nhất


99

Tôi có một khung dữ liệu với ba cột chuỗi. Tôi biết rằng một giá trị duy nhất trong cột thứ 3 là hợp lệ cho mọi kết hợp của hai giá trị đầu tiên. Để làm sạch dữ liệu, tôi phải nhóm theo khung dữ liệu bởi hai cột đầu tiên và chọn giá trị chung nhất của cột thứ ba cho mỗi kết hợp.

Mã của tôi:

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

Dòng cuối cùng của mã không hoạt động, nó cho biết "Lỗi khóa 'Tên ngắn'" và nếu tôi cố gắng chỉ nhóm theo Thành phố, thì tôi nhận được lỗi AssertionError. Tôi có thể làm gì để sửa chữa nó?

Câu trả lời:


145

Bạn có thể sử dụng value_counts()để lấy chuỗi số và lấy hàng đầu tiên:

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

Trong trường hợp bạn đang thắc mắc về việc thực hiện các hàm agg khác trong .agg (), hãy thử điều này.

# 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') \
                                      )

Tôi nhận thấy rằng stats.mode có thể hiển thị câu trả lời không chính xác trong trường hợp biến chuỗi. Cách này trông đáng tin cậy hơn.
Viacheslav Nefedov

1
Điều này có nên không .value_counts(ascending=False)?
Riêng tư

1
@Private: ascending=Falseđã là giá trị mặc định, vì vậy không cần đặt thứ tự một cách rõ ràng.
Schmuddi

2
Như Jacquot đã nói, pd.Series.modebây giờ là thích hợp hơn và nhanh hơn.
Daisuke SHIBATO

Làm cách nào tôi có thể sử dụng giải pháp này với nhiều hàm tổng hợp khác nhau, ví dụ: nếu tôi có nhiều cột như "Tên viết tắt" và thêm các cột số mà tôi muốn tổng hợp bằng một hàm tổng?
constiii

99

Gấu trúc> = 0,16

pd.Series.mode có sẵn!

Sử dụng groupby, GroupBy.aggvà áp dụng các pd.Series.modechức năng cho từng nhóm:

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

Nếu điều này là cần thiết làm DataFrame, hãy sử dụng

source.groupby(['Country','City'])['Short name'].agg(pd.Series.mode).to_frame()

                         Short name
Country City                       
Russia  Sankt-Petersburg        Spb
USA     New-York                 NY

Điều hữu ích Series.modelà nó luôn trả về một Series, làm cho nó rất tương thích với aggapply, đặc biệt là khi tạo lại đầu ra theo nhóm. Nó cũng nhanh hơn.

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

Xử lý với nhiều chế độ

Series.modecũng hoạt động tốt khi có nhiều chế độ:

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

Hoặc, nếu bạn muốn một hàng riêng biệt cho từng chế độ, bạn có thể sử dụng 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

Nếu bạn không quan tâm chế độ nào được trả về miễn là một trong hai chế độ đó, thì bạn sẽ cần một lambda gọi modevà trích xuất kết quả đầu tiên.

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

Các lựa chọn thay thế để (không) xem xét

Bạn cũng có thể sử dụng statistics.modetừ python, nhưng ...

source.groupby(['Country','City'])['Short name'].apply(statistics.mode)

Country  City            
Russia   Sankt-Petersburg    Spb
USA      New-York             NY
Name: Short name, dtype: object

... nó không hoạt động tốt khi phải xử lý nhiều chế độ; a StatisticsErrorđược nâng lên. Điều này được đề cập trong tài liệu:

Nếu dữ liệu trống hoặc nếu không có chính xác một giá trị phổ biến nhất, thì StatisticsError sẽ xuất hiện.

Nhưng bạn có thể tự mình thấy ...

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)dường như hiệu quả với tôi. Nếu điều đó không hiệu quả, dự đoán thứ hai của tôi sẽ là df.groupby(cols).agg(lambda x: pd.Series.mode(x).values[0]).
cs95

Cảm ơn (như mọi khi!) Tùy chọn thứ hai của bạn cải thiện mọi thứ cho tôi, nhưng tôi nhận được một IndexError: index 0 is out of bounds for axis 0 with size 0(có thể là vì có những nhóm trong đó một loạt chỉ có NaN). Thêm dropna=Falsegiải quyết điều này , nhưng dường như tăng '<' not supported between instances of 'float' and 'str'(chuỗi của tôi là chuỗi). (Rất vui khi đặt câu hỏi này thành một câu hỏi mới nếu bạn thích.)
Josh Friedlander

2
@JoshFriedlander Xác định def foo(x): m = pd.Series.mode(x); return m.values[0] if not m.empty else np.nanvà sau đó sử dụng df.groupby(cols).agg(foo). Nếu điều đó không hiệu quả, hãy thử thực hiện foomột chút. Nếu bạn vẫn gặp sự cố khi bắt đầu, tôi khuyên bạn nên mở Q.
cs95

1
Tôi nên nói thêm rằng nếu bạn muốn bao gồm việc đếm np.nan, người ta có thể làm điều đó thông qua df.groupy(cols).agg(lambda x: x.mode(dropna=False).iloc[0])chế độ này, giả sử bạn không quan tâm đến các mối quan hệ và chỉ muốn một chế độ.
irene

17

Đối với agg, hàm lambba nhận được một Series, không có 'Short name'thuộc tính.

stats.mode trả về một bộ gồm hai mảng, vì vậy bạn phải lấy phần tử đầu tiên của mảng đầu tiên trong bộ này.

Với hai thay đổi đơn giản sau:

source.groupby(['Country','City']).agg(lambda x: stats.mode(x)[0][0])

trả lại

                         Short name
Country City                       
Russia  Sankt-Petersburg        Spb
USA     New-York                 NY

1
@ViacheslavNefedov - có, nhưng hãy sử dụng giải pháp của @ HYRY, sử dụng gấu trúc thuần chủng. Không cần scipy.stats.
eumiro

14

Trận đấu ở đây hơi muộn, nhưng tôi đã gặp phải một số vấn đề về hiệu suất với giải pháp của HYRY, vì vậy tôi phải đưa ra một giải pháp khác.

Nó hoạt động bằng cách tìm tần suất xuất hiện của mỗi khóa-giá trị và sau đó, đối với mỗi khóa, chỉ giữ giá trị xuất hiện với nó thường xuyên nhất.

Ngoài ra còn có một giải pháp bổ sung hỗ trợ nhiều chế độ.

Trong một bài kiểm tra quy mô đại diện cho dữ liệu mà tôi đang làm việc, điều này đã giảm thời gian chạy từ 37,4 giây xuống 0,5 giây!

Đây là mã cho giải pháp, một số ví dụ sử dụng và kiểm tra quy mô:

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

Chạy mã này sẽ in ra một cái gì đó như:

   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

Hi vọng điêu nay co ich!


Đó là cách nhanh nhất tôi đến cùng .. Cảm ơn!
FtoTheZ

1
Có cách nào để sử dụng aproach này nhưng trực tiếp bên trong các tham số agg không?, Ví dụ. agg({'f1':mode,'f2':np.sum})
Pablo

1
@PabloA rất tiếc là không, vì giao diện không hoàn toàn giống nhau. Tôi khuyên bạn nên thực hiện điều này như một hoạt động riêng biệt, sau đó kết hợp kết quả của bạn vào. Và tất nhiên, nếu hiệu suất không phải là vấn đề đáng lo ngại, bạn có thể sử dụng giải pháp của HYRY để giữ cho mã của bạn ngắn gọn hơn.
abw333

@ abw333 Tôi đã sử dụng giải pháp của HYRY, nhưng tôi gặp phải vấn đề về hiệu suất ... Tôi hy vọng rằng nhóm phát triển gấu trúc hỗ trợ nhiều chức năng hơn trong aggphương pháp này.
Pablo

Chắc chắn là cách để đi cho các DataFrame lớn. Tôi đã có 83 triệu hàng và 2,5 triệu nhóm duy nhất. Quá trình này mất 28 giây cho mỗi cột, trong khi tổng thời gian mất hơn 11 phút cho mỗi cột.
ALollz

5

Hai câu trả lời hàng đầu ở đây gợi ý:

df.groupby(cols).agg(lambda x:x.value_counts().index[0])

hoặc, tốt hơn là

df.groupby(cols).agg(pd.Series.mode)

Tuy nhiên, cả hai đều không thành công trong các trường hợp cạnh đơn giản, như được minh họa ở đây:

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]
})

Đầu tiên:

df.groupby(['client_id', 'date']).agg(lambda x:x.value_counts().index[0])

sản lượng IndexError(vì Dòng trống được trả về theo nhóm C). Thư hai:

df.groupby(['client_id', 'date']).agg(pd.Series.mode)

trả về ValueError: Function does not reduce, vì nhóm đầu tiên trả về một danh sách gồm hai (vì có hai chế độ). (Theo tài liệu ở đây , nếu nhóm đầu tiên trả về một chế độ duy nhất, điều này sẽ hoạt động!)

Hai giải pháp khả thi cho trường hợp này là:

import scipy
x.groupby(['client_id', 'date']).agg(lambda x: scipy.stats.mode(x)[0])

Và giải pháp được cs95 đưa ra cho tôi trong phần bình luận ở đây :

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)

Tuy nhiên, tất cả những điều này đều chậm và không phù hợp với các bộ dữ liệu lớn. Một giải pháp mà tôi đã kết thúc bằng cách sử dụng a) có thể giải quyết những trường hợp này và b) nhanh hơn rất nhiều, là một phiên bản được sửa đổi nhẹ của câu trả lời abw33 (phải cao hơn):

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

Về cơ bản, phương thức này hoạt động trên một col tại một thời điểm và xuất ra một df, vì vậy, thay vì concatchuyên sâu, bạn xử lý đầu tiên như một df, và sau đó thêm lặp đi lặp lại mảng đầu ra ( values.flatten()) dưới dạng một cột trong df.


3

Về mặt hình thức, câu trả lời chính xác là Giải pháp @eumiro. Vấn đề của giải pháp @HYRY là khi bạn có một dãy số như [1,2,3,4] thì lời giải là sai, tức là bạn không có chế độ . Thí dụ:

>>> 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]
        }
    )

Nếu bạn tính toán như @HYRY, bạn nhận được:

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

Điều này rõ ràng là sai (xem giá trị A phải là 1 chứ không phải 4 ) vì nó không thể xử lý với các giá trị duy nhất.

Do đó, giải pháp khác là đúng:

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

Nếu bạn muốn một cách tiếp cận khác để giải quyết nó mà không phụ thuộc vào value_countshoặc scipy.statsbạn có thể sử dụng Counterbộ sưu tập

from collections import Counter
get_most_common = lambda values: max(Counter(values).items(), key = lambda x: x[1])[0]

Có thể áp dụng cho ví dụ trên như thế này

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)

Điều này nhanh hơn pd.Series.modehoặc pd.Series.value_counts().iloc[0]- nhưng nếu bạn có các giá trị NaN mà bạn muốn đếm, điều này sẽ không thành công. Mỗi lần xuất hiện NaN sẽ được xem là khác với các lần NaN khác, vì vậy mỗi lần NaN được tính để có số lượng 1. Xem stackoverflow.com/questions/61102111/…
irene

1

Nếu bạn không muốn bao gồm các giá trị NaN , việc sử dụng Countersẽ nhanh hơn nhiều so với pd.Series.modehoặc 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)

nên làm việc. Điều này sẽ không thành công khi bạn có các giá trị NaN, vì mỗi NaN sẽ được tính riêng biệt.


0

Vấn đề ở đây là hiệu suất, nếu bạn có nhiều hàng sẽ là một vấn đề.

Nếu đó là trường hợp của bạn, hãy thử với điều này:

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

Một cách tiếp cận hơi vụng về nhưng nhanh hơn đối với các bộ dữ liệu lớn hơn bao gồm việc lấy số lượng cho một cột quan tâm, sắp xếp số lượng từ cao nhất đến thấp nhất, sau đó khử trùng lặp trên một tập hợp con để chỉ giữ lại các trường hợp lớn nhất. Ví dụ mã như sau:

>>> 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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.