七步搞定一個綜合案例,掌握pandas進階用法!

案例引入
現(xiàn)有一批銷售數(shù)據(jù),如下圖所示:

每個城市會銷售各種各樣的產品,現(xiàn)在想要統(tǒng)計每個城市各個子類別中,累計銷售數(shù)量篩選出每個城市每個子類別中銷量占比top 50%的至多3個產品。如果銷量排名前3種的產品未超過50%,則取Top3,如果超過50%,則取剛好大于50%的Top產品。輸出的結果為3列,分別為城市,子類別,產品列表(逗號隔開)。如下圖所示,①處有3種產品,是【上海-收納具】銷量Top3的產品,其銷售數(shù)量占比超過50%(或未達50%但已達到Top3,雖然這里后兩個產品都是文件夾,但觀察原始數(shù)據(jù)會發(fā)現(xiàn),這是兩種不同的文件夾);②處有兩件商品,說明【上海-桌子】中木桌,電腦桌銷量已超桌子的50%。

案例淺析
雖然在表述上有些繞,但其實需求還是比較明確的。仔細分析,從業(yè)務邏輯上,這里需要用到pandas的如下技巧。文件讀取-->分組求和-->分組排序-->計算各組累計百分比-->取Top3(需要與50%作比較)-->分組取列表-->文件保存。從具體實現(xiàn)上,可能還有其他處理技巧,如數(shù)據(jù)拼接(merge)等。下面結合代碼進行講解。
案例解答
0.必要包導入
正式開始前,需要引入相關包,主要是pandas。為過濾異常,這里也引入了warnings包。
import pandas as pdimport warningswarnings.filterwarnings('ignore')
1.數(shù)據(jù)讀取
讀文件是數(shù)據(jù)處理的第一步,pandas提供了read_xxx系列函數(shù),本次用到的是excel格式,因此使用read_excel即可,讀取成功后,用head查看數(shù)據(jù)樣例。
data = pd.read_excel('data.xlsx')#讀取數(shù)據(jù)文件data.head()#查看樣例

可以看到原始數(shù)據(jù)的列名為中文格式,為便于后續(xù)處理,我們統(tǒng)一改為英文,采用列名直接賦值的方式,如下面代碼。
# 直接賦值修改列名data.columns=['id', 'order_id', 'city', 'cate', 'sub_cate', 'prod_name', 'sale_amt', 'amt']data.head()

注意到prod_name包含的信息較多,逗號前是英文和中文名稱,逗號后是一些補充信息,我們使用split把它分隔開,因為分割出來是兩個字段,所以要寫成下面的形式,注意最后要加上str。
data['prod_full_name'], data['remark'] = data['prod_name'].str.split(',').strdata.head(10)

最后,我們可以篩選一些最終會用到的列,用于后面的處理。
#列子集篩選data_new = data[['city', 'sub_cate', 'prod_full_name', 'amt']]data_new.head()

2.分組聚合
按照需求,需要計算每個城市每個子類別下產品的銷售總量,因此需要按照city和sub_cate分組,并對amt求和。為計算占比,求得的和還需要和原始數(shù)據(jù)合在一塊作為新的一列。這里有兩種方式,可以先分組求和,再與原數(shù)據(jù)進行merge,也可以使用分組transform一步到位,在前面的文章Pandas tricks 之 transform的用法一文中有詳細的講解。這里采用第二種方式。計算的結果作為新的一列amt_sum添加到原數(shù)據(jù)上。
#分組求和并用transform與原數(shù)據(jù)合并amt_sum = data_new.groupby(['city', 'sub_cate'])['amt'].transform('sum')data_new['amt_sum'] = amt_sumdata_new.head(10)

為了驗證結果,我們取出city='杭州',sub_cate='用品'的所有樣本進行查看,這里用到了pandas多條件篩選數(shù)據(jù)操作。
#多條件篩選,用&連接data_new[(data_new['sub_cate']=='用品') & (data_new['city'] == '杭州')]

可以看到所有杭州,用品的銷售總量為18,和我們的計算結果一致。
3.分組排序
由于我們最終需要取排序Top3(或top50%)的產品,因此需要在各組內先按照銷售量降序排列,再計算百分比,最后求累計百分比。也可以先計算每個產品各自的占比,再排序之后求累計百分比。這里也采用第二種方式。
#計算占比:data_new['pct'] = data_new['amt'] / data_new['amt_sum']data_new.head(10)

各組內按銷售數(shù)量(或百分比)做降序。這里的排序有兩個層次的含義,第一種是組內實際順序不變,只給一個排序編號。代碼如下所示,method=first是保證序號是連續(xù)且唯一的。
data_new['group_rank'] = data_new.groupby(['city', 'sub_cate'])['amt'].rank(method='first', ascending=False)data_new.head(10)

同樣來看一下city='杭州',sub_cate='用品'的結果,如下圖所示,可以看到已經數(shù)量最多的編號為1,數(shù)量最少的編號為5,給出的序號并不是嚴格降序或升序的,說明數(shù)據(jù)的實際順序并沒有改變。

第二種是排序之后,改變數(shù)據(jù)的實際順序。我們使用lambda函數(shù)實現(xiàn):對每個分組按照上一步生成的rank值,升序排列。
data_sorted = data_new.groupby(['city', 'sub_cate']).apply(lambda x: x.sort_values('group_rank', ascending=True)).reset_index(drop=True)data_sorted.head(10)

結果如上圖,這樣銷量占比最高的產品就會出現(xiàn)在每組的第一行。同樣看一下city='杭州',sub_cate='用品'的結果,發(fā)現(xiàn)amt,pct是降序的,group_rank是升序的。

4.求累計占比
前一步之所以要改變數(shù)據(jù)的順序,就是為了在這里算累計占比時,可以直接累加。我們需要對pct列求累計值,最終用來與目標值50%作比較。注意同樣是在每組內進行,需要用cumsum函數(shù)求累計和。
#分組并用cumsum計算累計占比data_sorted['cum_pct'] = data_sorted.groupby(['city', 'sub_cate'])['pct'].cumsum()data_sorted.head(10)

再來看一下city='杭州',sub_cate='用品'的結果。

可以看到最后一列cum_pct已經按照pct列計算了累計百分比。其中累計到第二行的時候已經達到了61.1%,超過了50%,因此最終只需取前兩行即可。
5.目標篩選
經過了前面的數(shù)據(jù)準備,在這一步需要在每組內,篩選累計值達到50%的行,且最多三行。這里需要對每組內按行進行遍歷,用到了iterrows函數(shù),并判斷cum_pct與50%,group_rank與3的關系。我們自定義一個函數(shù)來實現(xiàn)。
def get_top_50_pct_rank(group_data, num=3, target=0.5):res = -1for index, grp in group_data.iterrows():if grp['group_rank'] <= num and grp['cum_pct'] >= target:res = grp['group_rank']breakif grp['cum_pct'] < target and grp['group_rank'] == num:res = numbreakreturn res
調用該函數(shù)之后,對每個組能得到符合條件的目標group_rank值,如下面代碼和圖片所示:
data_target_rank = data_sorted.groupby(['city', 'sub_cate']).apply(get_top_50_pct_rank).reset_index()data_target_rank.head()

上圖第三列就是我們需要的目標group_rank值,注意先要把默認的名字改過來,并將此結果與原始數(shù)據(jù)做一個合并。在此基礎上,就可以將每組內不超過目標group_rank值的行篩選出來。
data_target_rank.rename(columns={0: 'target_get'}, inplace=True)merge_data = pd.merge(data_sorted, data_target_rank,on=['city', 'sub_cate'], how='left')result_data = merge_data[merge_data['group_rank'] <= merge_data['target_get']]result_data.head()

還是看一下city='杭州',sub_cate='用品'的最終結果:

可以看出,該組內最初有5行數(shù)據(jù),篩選后剩下兩行,且銷售量占比超過50%,至此需求已基本實現(xiàn)。
6.分組拼接
在上一步篩選出了目標行,未達到最終目標,還需將每個分組內所有符合條件的產品名稱拼接起來,并用逗號隔開。這里采用分組對字符串求和的方式來實現(xiàn)。先在每個產品后面拼上一個逗號,然后“求和”,最后把末尾的逗號去掉。代碼如下:
#給每一個產品名稱末尾拼接一個逗號result_data['prod_full_name'] = result_data['prod_full_name'].apply(lambda x: x + ',')result_data.head()

#用求和的方式實現(xiàn)對產品名稱進行拼接result = result_data.groupby(['city', 'sub_cate'])['prod_full_name'].sum().reset_index()#去掉最末尾的逗號result['prod_full_name'] = result['prod_full_name'].apply(lambda x: x[:-1])
result就是我們想要的目標dataframe。最終的city='杭州',sub_cate='用品'的結果如下。

7.保存文件
將上一步得到的result保存成Excel,即可得到文中開頭截圖的結果,使用to_excel方法,指定文件名,忽略索引即可。
result.to_excel('result.xlsx', index=None)小結
本文使用pandas,通過7個步驟實現(xiàn)了一個綜合案例:篩選出每個城市每個子類別中銷量占比top 50%的至多3個產品。涉及到的操作依次有:數(shù)據(jù)讀取,列名修改,字段分割,列子集篩選;分組求和(transform);分組排序(編號),分組排序;累計求和;按行迭代,數(shù)據(jù)拼接,條件篩選,分組拼接,apply/lambda函數(shù);保存文件等。有一些是核心操作,有一些只是輔助。可以用下圖來總結,帶有五角星的是核心操作,其余是輔助操作,葉子節(jié)點是用到的函數(shù)。


