拯救pandas計劃(5)——獲取DataFrame分組topN數(shù)據(jù)
拯救pandas計劃(5)——獲取DataFrame分組topN數(shù)據(jù)
最近發(fā)現(xiàn)周圍的很多小伙伴們都不太樂意使用pandas,轉(zhuǎn)而投向其他的數(shù)據(jù)操作庫,身為一個數(shù)據(jù)工作者,基本上是張口pandas,閉口pandas了,故而寫下此系列以讓更多的小伙伴們愛上pandas。
系列文章說明:
系列名(系列文章序號)——此次系列文章具體解決的需求
平臺:
windows 10 python 3.8 pandas >=1.2.4
/ 數(shù)據(jù)需求
現(xiàn)有一組數(shù)據(jù),需要根據(jù)name進行分組,以date_col順序排序,獲取每組數(shù)據(jù)的前N項數(shù)據(jù)。
為考慮比較各方案間的耗時,此次數(shù)據(jù)采用數(shù)據(jù)類別多量小的數(shù)據(jù)集。
/ 需求拆解
整個數(shù)據(jù)框的前幾行或者后幾行都有相應(yīng)的方法可以調(diào)用,如head()、tail(),分組后的前幾行,只需要把整個數(shù)據(jù)框應(yīng)用到groupby上再對各個分組進行head()即可,而這里需要取得topN,則分組后不一定能夠按順序取得,故而需要對數(shù)據(jù)框進行排序。
/ 需求處理
方法一
正如需求拆解里提到過的,使用groupby來完成這部分任務(wù),在取得topN之前是需要對整個數(shù)據(jù)集進行排序的,這可以先嘗試下在groupby之前排序,還是之后排序是否會對整個任務(wù)執(zhí)行時間有影響。
先排序,后分組
df.sort_values(['name',?'date_col'],?inplace=True)
df.groupby(['name']).head(1)

先分組,后排序
由于groupby后面不能直接跟sort_values,所以需要調(diào)用apply來對每個分組進行排序。
分組后排序用時:
df.groupby(['name']).apply(lambda?x:?x.sort_values('date_col').head(1)).reset_index(drop=True)
看到這運行時間差了一個數(shù)量級,可能會懷疑是不是sort_values的問題,都知道pandas調(diào)用內(nèi)部函數(shù)時運行效率還算是過的去,怎么在這差了這么多,直接在groupby后面運行head()僅200ms,這會可以看看在apply里調(diào)用head()。
在上圖可以看出拖慢運行時間的主要原因不是sort_values,而是apply,雖然apply的工作機制方便了對數(shù)據(jù)框內(nèi)的數(shù)據(jù)進行各種各樣的處理操作,但當存在一種內(nèi)部函數(shù)可以滿足需求時再選擇使用apply就會稍顯雞肋。?
(手動水?。涸瓌?chuàng)CSDN宿者朽命,https://blog.csdn.net/weixin_46281427?spm=1011.2124.3001.5343,公眾號A11Dot派)?
簡言之,在這種方式處理上,先排序再分組取topN是能夠較快的得到目標數(shù)據(jù)。
方法二
在拯救pandas計劃(4)——DataFrame分組條件查找值中有提到過使用drop_duplicates(),同樣在這里分組取topN也可以一試,但有限制條件,其drop_duplicates()內(nèi)的keep參數(shù)決定了,僅能保留首個或尾個或者不保留重復(fù)數(shù)據(jù)。因此當只取top1時,可以試用此種方法,在處理時間上也過得去。
df.sort_values(['name',?'date_col'],?inplace=True)
df.drop_duplicates(['name'])??#?默認保留首個

方法三
雖然說有內(nèi)部函數(shù)直接能達成結(jié)果的優(yōu)先使用內(nèi)部函數(shù),但在這里不妨想一想如何在不使用groupby的方式求得分組topN。(可以先思考一會兒再繼續(xù)往下看)
闡述下我的想法,僅做拋磚引玉之用,既然是分組取topN,不就是一種變相的分組排序,取排序靠前的值。以這樣的思路,先對組中的每個類型進行計數(shù),再編號即可取得。
計數(shù):
除了groupby外對類型進行計數(shù)還有一個好的方法,value_counts,這里需要將sort參數(shù)設(shè)置為False,避免內(nèi)部排序影響外部排序,在計數(shù)前依然是先對整個數(shù)據(jù)框進行排序。
df.sort_values(['name',?'date_col'],?inplace=True)
name_count?=?df.value_counts('name',?sort=False)
編號:
而后對name_count進行編號,使用lambda調(diào)用range(x)。
name_count.map(lambda?x:?range(x))
從生成的結(jié)果看來,Series中的values是一個可迭代序列,這種結(jié)果不能直接對原始數(shù)據(jù)框設(shè)置編號,取出values,使用np.hstack以行方向組合,對每個分組編號組合成一個一維數(shù)組。
import?numpy?as?np
df.sort_values(['name',?'date_col'],?inplace=True)
np.hstack(df.value_counts('name',?sort=False).map(lambda?x:?range(x)).values)
ps: values中的每個值都是一維數(shù)組
取值:
再對生成的值與想要提取的topN的N進行對比,進行布爾索引提取即可得到想要的topN數(shù)據(jù)。運行結(jié)果如下,時間上也能接受:
以下是將這段代碼進行封裝成函數(shù):
import?numpy?as?np
import?pandas?as?pd
def?get_data_top(data:?pd.DataFrame,?group_cols:?list,?val_cols:?list,?ascending:?bool?=?True,?k:?int?=?1):
????"""
????自定義獲取數(shù)據(jù)框topN
????:param?data:?pd.DataFrame類型
????:param?group_cols:?list,?需要聚合的列名
????:param?val_cols:?list,?需要排序的列名
????:param?ascending:?排序方式,默認`True`,順序排序,接收bool或這個列表里全部為bool的列表
????:param?k:?取前k項值
????:return:?返回topN數(shù)據(jù)框
????"""
????#?為了能返回傳入數(shù)據(jù)框的原index,將index保存至values中
????datac?=?data.reset_index().copy()
????index_colname?=?datac.columns[0]
????#?對原數(shù)據(jù)框進行排序
????datac.sort_values(group_cols?+?val_cols,?ascending=ascending,?inplace=True)
????#?主要代碼:分組對組內(nèi)進行編號
????rank0?=?np.hstack(datac.value_counts(group_cols,?sort=False).map(lambda?x:?range(x)).values)
????#?取topN值
????datac?=?datac[rank0?????#?取出原index重置為index值
????datac.index?=?datac[index_colname].values
????#?刪除額外生成的index值的列
????del?datac[index_colname]
????return?datac
ps: 參數(shù)冒號后的類型僅做提示,輸入其他類型亦能入?yún)?,但需要傳入正確參數(shù)及類型才能正常輸出。
/ 總結(jié)
文中使用三種方法來取得數(shù)據(jù)集中的前N項值,過程上略有不同,總的結(jié)果呈現(xiàn)也基本相同,在想法及做法上對個人都一種提升。在寫這篇之前,我一直在詢問我自己,這篇值不值得寫下來,把方法三刪了改,改了刪,起初并沒有使用numpy.hstack,而是直接使用list強轉(zhuǎn)range,偶然一次運行時發(fā)現(xiàn)運行時間竟然比groupby.head短,當時還竊竊自喜,復(fù)盤發(fā)現(xiàn)原來是我的把.head()運用在apply中,在方法一也有提到過這樣做的耗時。經(jīng)過幾番修改,最終采用np.hstack組合編號,效率上能勉強達到方法一水平。
在書本中,在年長者口中,常常有一種聲音提醒我們現(xiàn)在站在了人生的十字路口,需要仔細思考,斟酌,推斷這樣做會有怎樣的結(jié)果,但現(xiàn)在還需要磨蹭啥呢,未來不是推斷出的未來,是創(chuàng)造的未來,敢于去想,敢于去做!
于二零二二年元月二十四日作
