GroupBy pandas DataFrame dan pilih nilai paling umum

100

Saya memiliki bingkai data dengan tiga kolom string. Saya tahu bahwa hanya satu nilai di kolom ke-3 yang valid untuk setiap kombinasi dari dua yang pertama. Untuk membersihkan data, saya harus mengelompokkan berdasarkan bingkai data dengan dua kolom pertama dan memilih nilai paling umum dari kolom ketiga untuk setiap kombinasi.

Kode saya:

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

Baris terakhir kode tidak berfungsi, ia mengatakan "Kesalahan kunci 'Nama pendek'" dan jika saya mencoba mengelompokkan hanya menurut Kota, maka saya mendapat AssertionError. Apa yang bisa saya lakukan untuk memperbaikinya?

Viacheslav Nefedov
sumber

Jawaban:

145

Anda bisa menggunakan value_counts()untuk mendapatkan rangkaian hitungan, dan mendapatkan baris pertama:

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

Jika Anda bertanya-tanya tentang melakukan fungsi agg lainnya di .agg () coba ini.

# 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') \
                                      )
HYRY
sumber
Saya telah menemukan bahwa stats.mode dapat menunjukkan jawaban yang salah dalam kasus variabel string. Cara ini terlihat lebih andal.
Viacheslav Nefedov
1
Bukankah seharusnya demikian .value_counts(ascending=False)?
Prajurit
1
@Private: ascending=Falsesudah menjadi nilai default, jadi tidak perlu menyetel urutan secara eksplisit.
Schmuddi
2
Seperti yang dikatakan Jacquot, pd.Series.modesekarang lebih tepat dan lebih cepat.
Daisuke SHIBATO
Bagaimana saya dapat menggunakan solusi ini dengan beberapa fungsi agregasi yang berbeda, misalnya jika saya memiliki beberapa kolom seperti "Nama pendek" dan kolom numerik tambahan yang ingin saya gabungkan dengan fungsi penjumlahan?
constiii
100

Panda> = 0,16

pd.Series.mode tersedia!

Gunakan groupby,, GroupBy.aggdan terapkan pd.Series.modefungsi ke setiap grup:

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

Jika ini diperlukan sebagai DataFrame, gunakan

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

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

Hal yang berguna tentang itu Series.modeadalah selalu mengembalikan Seri, membuatnya sangat kompatibel dengan aggdan apply, terutama saat merekonstruksi keluaran groupby. Ini juga lebih cepat.

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

Berurusan dengan Berbagai Mode

Series.modejuga berfungsi dengan baik bila ada beberapa 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

Atau, jika Anda menginginkan baris terpisah untuk setiap mode, Anda dapat menggunakan 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

Jika Anda tidak peduli mode mana yang dikembalikan selama salah satunya, maka Anda memerlukan lambda yang memanggil modedan mengekstrak hasil pertama.

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

Alternatif untuk (tidak) dipertimbangkan

Anda juga dapat menggunakan statistics.modedari python, tetapi ...

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

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

... itu tidak bekerja dengan baik ketika harus berurusan dengan banyak mode; a StatisticsErrordibesarkan. Ini disebutkan di dokumen:

Jika data kosong, atau jika tidak ada persis satu nilai yang paling umum, StatisticsError dimunculkan.

Tapi Anda bisa lihat sendiri ...

statistics.mode([1, 2])
# ---------------------------------------------------------------------------
# StatisticsError                           Traceback (most recent call last)
# ...
# StatisticsError: no unique mode; found 2 equally common values
cs95
sumber
@JoshFriedlander df.groupby(cols).agg(pd.Series.mode)tampaknya bekerja untuk saya. Jika itu tidak berhasil, tebakan kedua saya adalah df.groupby(cols).agg(lambda x: pd.Series.mode(x).values[0]).
cs95
Terima kasih (seperti biasa!) Opsi kedua Anda meningkatkan banyak hal untuk saya, tetapi saya mendapatkan IndexError: index 0 is out of bounds for axis 0 with size 0(mungkin karena ada grup di mana rangkaian hanya memiliki NaN). Menambahkan dropna=Falsememecahkan ini , tetapi tampaknya menaikkan '<' not supported between instances of 'float' and 'str'(seri saya adalah string). (Senang membuat ini menjadi pertanyaan baru jika Anda mau.)
Josh Friedlander
2
@JoshFriedlander Tentukan def foo(x): m = pd.Series.mode(x); return m.values[0] if not m.empty else np.nandan kemudian gunakan df.groupby(cols).agg(foo). Jika itu tidak berhasil, atur implementasi foosebentar. Jika Anda masih mengalami masalah, saya sarankan membuka Q baru
cs95
1
Saya harus menambahkan bahwa jika Anda ingin memasukkan penghitungan np.nan, seseorang dapat melakukannya melalui df.groupy(cols).agg(lambda x: x.mode(dropna=False).iloc[0])mode, dengan asumsi Anda tidak peduli tentang ikatan dan hanya menginginkan satu mode.
irene
17

Karena agg, fungsi lambba mendapat a Series, yang tidak memiliki 'Short name'atribut.

stats.mode mengembalikan tupel dari dua larik, jadi Anda harus mengambil elemen pertama dari larik pertama dalam tupel ini.

Dengan dua perubahan sederhana ini:

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

kembali

                         Short name
Country City                       
Russia  Sankt-Petersburg        Spb
USA     New-York                 NY
eumiro
sumber
1
@ViacheslavNefedov - ya, tetapi gunakan solusi @ HYRY, yang menggunakan panda murni. Tidak perlu scipy.stats.
eumiro
14

Sedikit terlambat untuk permainan di sini, tapi saya mengalami beberapa masalah kinerja dengan solusi HYRY, jadi saya harus memikirkan yang lain.

Ini bekerja dengan menemukan frekuensi setiap nilai kunci, dan kemudian, untuk setiap kunci, hanya menyimpan nilai yang paling sering muncul bersamanya.

Ada juga solusi tambahan yang mendukung banyak mode.

Pada uji skala yang mewakili data yang saya kerjakan, runtime ini berkurang dari 37,4s menjadi 0,5s!

Berikut kode solusinya, beberapa contoh penggunaan, dan uji skala:

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

Menjalankan kode ini akan mencetak sesuatu seperti:

   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

Semoga ini membantu!

abw333
sumber
Itulah cara tercepat saya ikut .. Terima kasih!
FtoTheZ
1
Apakah ada cara untuk menggunakan pendekatan ini tetapi langsung di dalam parameter agg ?, mis. agg({'f1':mode,'f2':np.sum})
Pablo
1
@PabloA sayangnya tidak, karena antarmukanya tidak persis sama. Saya sarankan melakukan ini sebagai operasi terpisah, dan kemudian menggabungkan hasil Anda. Dan, tentu saja, jika kinerja bukan masalah, Anda dapat menggunakan solusi HYRY untuk menjaga kode Anda lebih ringkas.
abw333
@ abw333 Saya menggunakan solusi HYRY, tetapi saya mengalami masalah kinerja ... Saya harap tim pandas dev mendukung lebih banyak fungsi dalam aggmetode ini.
Pablo
Jelas cara untuk menggunakan DataFrames besar. Saya memiliki 83 juta baris dan 2,5 juta grup unik. Ini membutuhkan waktu 28 detik per kolom, sedangkan agg membutuhkan waktu lebih dari 11 menit per kolom.
ALollz
5

Dua jawaban teratas di sini menyarankan:

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

atau, lebih disukai

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

Namun keduanya gagal dalam kasus tepi sederhana, seperti yang ditunjukkan di sini:

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

Pertama:

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

hasil IndexError(karena Seri kosong dikembalikan oleh grup C). Kedua:

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

kembali ValueError: Function does not reduce, karena grup pertama mengembalikan daftar dua (karena ada dua mode). (Seperti yang didokumentasikan di sini , jika grup pertama mengembalikan mode tunggal, ini akan berhasil!)

Dua solusi yang mungkin untuk kasus ini adalah:

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

Dan solusi yang diberikan kepada saya oleh cs95 di komentar di sini :

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)

Namun, semua ini lambat dan tidak cocok untuk kumpulan data besar. Solusi yang akhirnya saya gunakan yang a) dapat menangani kasus-kasus ini dan b) jauh, jauh lebih cepat, adalah versi jawaban abw33 yang sedikit dimodifikasi (yang seharusnya lebih tinggi):

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

Pada dasarnya, metode ini bekerja pada satu col pada satu waktu dan mengeluarkan df, jadi alih-alih concat, yang intensif, Anda memperlakukan yang pertama sebagai df, dan kemudian menambahkan larik keluaran ( values.flatten()) secara berulang sebagai kolom di df.

Josh Friedlander
sumber
3

Secara formal, jawaban yang benar adalah Solusi @eumiro. Masalah dari solusi @HYRY adalah ketika Anda memiliki urutan angka seperti [1,2,3,4] solusinya salah, yaitu, Anda tidak memiliki mode . Contoh:

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

Jika Anda menghitung seperti @HYRY Anda mendapatkan:

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

Yang jelas salah (lihat nilai A yang seharusnya 1 dan bukan 4 ) karena tidak dapat menangani dengan nilai unik.

Jadi, solusi lainnya benar:

>>> 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
nunodsousa
sumber
1

Jika Anda ingin pendekatan lain untuk menyelesaikannya yang tidak bergantung pada value_countsatau scipy.statsAnda dapat menggunakan Counterkoleksi

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

Yang bisa diterapkan pada contoh di atas seperti ini

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)
kmader
sumber
Ini lebih cepat dari pd.Series.modeatau pd.Series.value_counts().iloc[0]- tetapi jika Anda memiliki nilai NaN yang ingin Anda hitung, ini akan gagal. Setiap kejadian NaN akan terlihat berbeda dengan NaN lainnya, sehingga setiap NaN dihitung memiliki hitungan 1. Lihat stackoverflow.com/questions/61102111/…
irene
1

Jika Anda tidak ingin memasukkan nilai NaN , menggunakan Counterjauh lebih cepat daripada pd.Series.modeatau 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)

harus bekerja. Ini akan gagal jika Anda memiliki nilai NaN, karena setiap NaN akan dihitung secara terpisah.

irene
sumber
0

Masalahnya di sini adalah kinerjanya, jika Anda memiliki banyak baris maka akan menjadi masalah.

Jika itu kasus Anda, silakan coba dengan ini:

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()
Diego Perez Sastre
sumber
0

Pendekatan yang sedikit lebih ceroboh tetapi lebih cepat untuk kumpulan data yang lebih besar melibatkan mendapatkan penghitungan untuk kolom yang diminati, mengurutkan jumlah dari tertinggi ke terendah, dan kemudian menghilangkan duplikasi pada subset untuk hanya mempertahankan kasus terbesar. Contoh kodenya adalah sebagai berikut:

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