Membandingkan daftar dalam dua kolom dengan bijaksana secara efisien

16

Saat memiliki Pandaf DataFrame seperti ini:

import pandas as pd
import numpy as np
df = pd.DataFrame({'today': [['a', 'b', 'c'], ['a', 'b'], ['b']], 
                   'yesterday': [['a', 'b'], ['a'], ['a']]})
                 today        yesterday
0      ['a', 'b', 'c']       ['a', 'b']
1           ['a', 'b']            ['a']
2                ['b']            ['a']                          
... etc

Tetapi dengan sekitar 100.000 entri, saya mencari untuk menemukan penambahan dan penghapusan daftar itu di dua kolom berdasarkan baris-bijaksana.

Ini sebanding dengan pertanyaan ini: Pandas: Bagaimana Membandingkan Kolom Daftar Baris-bijaksana dalam DataFrame dengan Pandas (bukan untuk loop)? tapi saya melihat perbedaannya, dan Pandas.applymetode sepertinya tidak secepat itu untuk banyak entri. Ini adalah kode yang saya gunakan saat ini. Pandas.applydengan numpy's setdiff1dmetode:

additions = df.apply(lambda row: np.setdiff1d(row.today, row.yesterday), axis=1)
removals  = df.apply(lambda row: np.setdiff1d(row.yesterday, row.today), axis=1)

Ini berfungsi dengan baik, namun dibutuhkan sekitar satu menit untuk 120 000 entri. Jadi adakah cara yang lebih cepat untuk mencapai ini?

MegaCookie
sumber
Berapa banyak item maksimum (dalam satu baris) yang dimiliki oleh salah satu kolom ini?
thushv89
2
sudahkah Anda mencoba metode di pos yang Anda tautkan? khususnya yang menggunakan set persimpangan, yang harus Anda lakukan adalah menggunakan set difference sebagai gantinya, bukan?
gold_cy
1
@aws_apprentice solusi itu pada dasarnya adalah apa yang OP miliki di sini.
Quang Hoang
DataFrame Pandas mungkin bukan struktur data yang tepat untuk ini. Bisakah Anda berbagi sedikit lebih banyak latar belakang pada program dan data?
AMC

Jawaban:

14

Tidak yakin tentang kinerja, tetapi pada kurangnya solusi yang lebih baik ini mungkin berlaku:

temp = df[['today', 'yesterday']].applymap(set)
removals = temp.diff(periods=1, axis=1).dropna(axis=1)
additions = temp.diff(periods=-1, axis=1).dropna(axis=1) 

Pemindahan:

  yesterday
0        {}
1        {}
2       {a}

Tambahan:

  today
0   {c}
1   {b}
2   {b}
benteng
sumber
2
Ini sangat cepat.
rpanai
2
Ini memang sangat cepat. Itu turun menjadi sekitar 2 detik!
MegaCookie
2
Wow, saya terkejut dengan kinerja juga karena applymap, tapi senang itu berhasil untuk Anda!
mulai
2
Sekarang, seperti yang kita tahu solusi benteng cepat, Bisakah seseorang menjelaskan kepada saya. Kenapa lebih cepat?
Grijesh Chauhan
7
df['today'].apply(set) - df['yesterday'].apply(set)
Andreas K.
sumber
Terima kasih! Ini saya pikir solusi yang paling mudah dibaca, namun solusi r.ook sedikit lebih cepat.
MegaCookie
5

Saya akan menyarankan Anda untuk menghitung additionsdan removalsmenerapkan yang sama.

Hasilkan contoh yang lebih besar

import pandas as pd
import numpy as np
df = pd.DataFrame({'today': [['a', 'b', 'c'], ['a', 'b'], ['b']], 
                   'yesterday': [['a', 'b'], ['a'], ['a']]})
df = pd.concat([df for i in range(10_000)], ignore_index=True)

Solusi Anda

%%time
additions = df.apply(lambda row: np.setdiff1d(row.today, row.yesterday), axis=1)
removals  = df.apply(lambda row: np.setdiff1d(row.yesterday, row.today), axis=1)
CPU times: user 10.9 s, sys: 29.8 ms, total: 11 s
Wall time: 11 s

Solusi Anda pada satu aplikasi berlaku

%%time
df["out"] = df.apply(lambda row: [np.setdiff1d(row.today, row.yesterday),
                                  np.setdiff1d(row.yesterday, row.today)], axis=1)
df[['additions','removals']] = pd.DataFrame(df['out'].values.tolist(), columns=['additions','removals'])
df = df.drop("out", axis=1)

CPU times: user 4.97 s, sys: 16 ms, total: 4.99 s
Wall time: 4.99 s

Menggunakan set

Kecuali jika daftar Anda sangat besar, Anda dapat menghindari numpy

def fun(x):
    a = list(set(x["today"]).difference(set(x["yesterday"])))
    b = list((set(x["yesterday"])).difference(set(x["today"])))
    return [a,b]

%%time
df["out"] = df.apply(fun, axis=1)
df[['additions','removals']] = pd.DataFrame(df['out'].values.tolist(), columns=['additions','removals'])
df = df.drop("out", axis=1)

CPU times: user 1.56 s, sys: 0 ns, total: 1.56 s
Wall time: 1.56 s

solusi @ r.ook

Jika Anda senang memiliki set alih-alih daftar sebagai output, Anda dapat menggunakan kode @ r.ook

%%time
temp = df[['today', 'yesterday']].applymap(set)
removals = temp.diff(periods=1, axis=1).dropna(axis=1)
additions = temp.diff(periods=-1, axis=1).dropna(axis=1) 
CPU times: user 93.1 ms, sys: 12 ms, total: 105 ms
Wall time: 104 ms

Solusi @Andreas K.

%%time
df['additions'] = (df['today'].apply(set) - df['yesterday'].apply(set))
df['removals'] = (df['yesterday'].apply(set) - df['today'].apply(set))

CPU times: user 161 ms, sys: 28.1 ms, total: 189 ms
Wall time: 187 ms

dan pada akhirnya Anda dapat menambahkan .apply(list)untuk mendapatkan hasil yang sama

rpanai
sumber
1
Perbandingan keren yang Anda lakukan!
MegaCookie
1

Inilah salah satu dengan gagasan offloading compute part ke alat NumPy vektor. Kami akan mengumpulkan semua data ke array tunggal untuk setiap header, melakukan semua pencocokan yang diperlukan pada NumPy dan akhirnya memotong kembali ke entri baris yang diperlukan. Pada NumPy yang melakukan bagian pengangkatan berat, kami akan menggunakan hashing berdasarkan ID grup dan ID dalam setiap grup yang menggunakan np.searchsorted. Kami juga memanfaatkan angka-angka karena lebih cepat dengan NumPy. Implementasinya akan terlihat seperti ini -

t = df['today']
y = df['yesterday']
tc = np.concatenate(t)
yc = np.concatenate(y)

tci,tcu = pd.factorize(tc)

tl = np.array(list(map(len,t)))
ty = np.array(list(map(len,y)))

grp_t = np.repeat(np.arange(len(tl)),tl)
grp_y = np.repeat(np.arange(len(ty)),ty)

sidx = tcu.argsort()
idx = sidx[np.searchsorted(tcu,yc,sorter=sidx)]

s = max(tci.max(), idx.max())+1
tID = grp_t*s+tci
yID = grp_y*s+idx

t_mask = np.isin(tID, yID, invert=True)
y_mask = np.isin(yID, tID, invert=True)

t_se = np.r_[0,np.bincount(grp_t,t_mask).astype(int).cumsum()]
y_se = np.r_[0,np.bincount(grp_y,y_mask).astype(int).cumsum()]

Y = yc[y_mask].tolist()
T = tc[t_mask].tolist()

A = pd.Series([T[i:j] for (i,j) in zip(t_se[:-1],t_se[1:])])
R = pd.Series([Y[i:j] for (i,j) in zip(y_se[:-1],y_se[1:])])

Optimalisasi lebih lanjut dimungkinkan pada langkah-langkah untuk menghitung t_maskdan y_mask, di mana np.searchsorteddapat digunakan lagi.

Kita juga bisa menggunakan penugasan array sederhana sebagai alternatif untuk isinlangkah t_maskdan y_mask, seperti -

M = max(tID.max(), yID.max())+1
mask = np.empty(M, dtype=bool)

mask[tID] = True
mask[yID] = False
t_mask = mask[tID]

mask[yID] = True
mask[tID] = False
y_mask = mask[yID]
Divakar
sumber