python中實現數據分組統計的核心方法是pandas庫的groupby(),其核心機制為“split-apply-combine”。1. 首先使用groupby()按一個或多個列分組;2. 然后對每組應用聚合函數(如sum(), mean(), count()等)進行計算;3. 最后將結果合并成一個新的dataframe或series。通過groupby()可以實現單列分組、多列分組、多種聚合函數組合、自定義聚合函數、重置索引等操作,還能結合agg()實現多層聚合分析,配合apply()和transform()可執行更復雜的分組邏輯,如廣播結果回原始數據結構、填充缺失值、計算組內排名等。掌握groupby及其相關方法能顯著提升數據分析效率與靈活性。
python中實現數據分組統計,Pandas庫的groupby方法無疑是核心利器。它能讓你根據一個或多個列的值,將數據高效地拆分成若干組,然后對每組獨立進行聚合計算,比如求和、計數、平均值等等。這就像是把一大堆散亂的數據,按照某種內在的邏輯自動分類整理好,方便我們從不同維度去洞察數據背后的規律。
解決方案
要實現數據分組統計,最直接也是最常用的方法就是利用Pandas DataFrame的groupby()方法。它通常與一個或多個聚合函數(如sum(), mean(), count(), min(), max(), std(), var(), first(), last(), nth()等)結合使用。
首先,你需要一個DataFrame:
立即學習“Python免費學習筆記(深入)”;
import pandas as pd import numpy as np # 模擬一些銷售數據 data = { '產品': ['A', 'B', 'A', 'C', 'B', 'A', 'C', 'B', 'A'], '區域': ['華東', '華北', '華東', '華南', '華北', '華中', '華南', '華中', '華東'], '銷售額': [100, 150, 120, 80, 130, 90, 110, 160, 105], '銷量': [10, 15, 12, 8, 13, 9, 11, 16, 10] } df = pd.DataFrame(data) print("原始數據:") print(df) print("-" * 30) # 1. 單列分組并求和 # 統計每個產品的總銷售額 product_sales = df.groupby('產品')['銷售額'].sum() print("按產品統計總銷售額:") print(product_sales) print("-" * 30) # 2. 多列分組并計算平均值 # 統計每個區域、每個產品的平均銷售額和平均銷量 region_product_avg = df.groupby(['區域', '產品'])[['銷售額', '銷量']].mean() print("按區域和產品統計平均銷售額及銷量:") print(region_product_avg) print("-" * 30) # 3. 使用agg()進行多種聚合操作 # 統計每個區域的銷售額:總和、平均值、最大值 region_agg_sales = df.groupby('區域')['銷售額'].agg(['sum', 'mean', 'max']) print("按區域統計銷售額的多種指標:") print(region_agg_sales) print("-" * 30) # 4. 使用agg()對不同列應用不同聚合函數 # 統計每個產品:銷售額的總和,銷量的平均值 product_custom_agg = df.groupby('產品').agg( 總銷售額=('銷售額', 'sum'), 平均銷量=('銷量', 'mean') ) print("按產品統計銷售額總和與銷量平均值:") print(product_custom_agg) print("-" * 30) # 5. 分組后重置索引 # 如果想把分組鍵也作為普通列而不是索引,可以使用reset_index() reset_index_example = df.groupby('產品')['銷售額'].sum().reset_index() print("分組后重置索引:") print(reset_index_example)
groupby()方法的強大之處在于它的靈活性。你可以根據業務需求,選擇任意列作為分組鍵,然后對目標列應用各種聚合函數。我第一次接觸groupby的時候,覺得它有點像excel里的數據透視表,但又靈活得多,尤其是在處理大數據集時,那效率簡直是飛躍。
理解Pandas Groupby的“三板斧”:Split-Apply-Combine
說實話,很多人用groupby只是停留在表面,知道怎么寫,但不一定理解它背后的工作機制。Pandas的groupby操作,核心思想其實是“Split-Apply-Combine”(拆分-應用-合并)這個范式。理解這個,能幫你更好地優化代碼,解決更復雜的問題。
- Split(拆分): 這是groupby的第一步。Pandas會根據你指定的分組鍵(一個或多個列),將原始DataFrame邏輯上拆分成多個子DataFrame。每個子DataFrame都包含一個唯一分組鍵對應的所有行。這個過程是內存高效的,它通常不會真的創建很多小的DataFrame副本,而是內部維護一個指向原始數據視圖的索引映射。
- Apply(應用): 拆分完成后,Pandas會對每個獨立的子DataFrame應用一個函數。這個函數可以是內置的聚合函數(如sum(), mean()),也可以是你自定義的函數。這一步是計算的核心,比如你要求每組的平均值,就是在這里對每個子組的數據進行平均值計算。
- Combine(合并): 最后一步是將所有子DataFrame上應用函數得到的結果,合并成一個最終的DataFrame或Series。這個合并過程會根據分組鍵重新構建索引,并把聚合結果呈現出來。
掌握了“Split-Apply-Combine”這個概念,你就能更好地預判groupby的行為,也能在遇到性能瓶頸時,知道從哪個環節去優化。比如,如果你的apply步驟非常耗時,那可能需要考慮用矢量化操作替代,或者尋找Pandas內置的更優解。有時候,我們可能不自覺地寫出一些看似合理但效率低下的groupby操作,比如對每一組都用apply去跑一個復雜的自定義函數,這時候就得停下來想想,有沒有更Pandasic的辦法。
高級聚合與多層索引:解鎖復雜分析場景
在實際工作中,數據分析的需求往往比簡單求和復雜得多。groupby結合agg()方法,可以輕松應對多列分組和多層聚合的場景,甚至還能自定義聚合函數名,讓結果更清晰。
當我們需要同時對多個列進行分組,或者對同一列進行多種聚合操作時,agg()方法就顯得尤為重要了。
# 模擬更復雜的銷售數據,加入日期 df_complex = pd.DataFrame({ '日期': pd.to_datetime(['2023-01-01', '2023-01-01', '2023-01-02', '2023-01-02', '2023-01-03', '2023-01-03']), '產品': ['A', 'B', 'A', 'C', 'B', 'A'], '區域': ['華東', '華北', '華東', '華南', '華北', '華東'], '銷售額': [100, 150, 120, 80, 130, 90], '成本': [50, 70, 60, 40, 65, 45] }) print("n復雜原始數據:") print(df_complex) print("-" * 30) # 1. 多列分組,對不同列應用不同聚合函數,并自定義輸出列名 # 統計每天每個區域:總銷售額,平均成本,以及銷售額的最高值和最低值 daily_region_stats = df_complex.groupby(['日期', '區域']).agg( 總銷售額=('銷售額', 'sum'), 平均成本=('成本', 'mean'), 銷售額最大值=('銷售額', 'max'), 銷售額最小值=('銷售額', 'min') ) print("每天每區域的銷售統計:") print(daily_region_stats) print("-" * 30) # 2. 對同一列應用多個聚合函數,并使用多層列索引 # 統計每個產品的銷售額:總和、平均值、標準差 product_multi_agg = df_complex.groupby('產品')['銷售額'].agg(['sum', 'mean', 'std']) print("每個產品的銷售額多指標統計:") print(product_multi_agg) print("-" * 30) # 3. 如果需要更復雜的自定義聚合,可以傳入lambda函數 # 統計每個區域的銷售額,并計算一個自定義的“銷售額波動范圍”(最大值-最小值) region_custom_range = df_complex.groupby('區域')['銷售額'].agg( 總銷售額='sum', 銷售額波動范圍=lambda x: x.max() - x.min() ) print("每個區域的銷售額波動范圍:") print(region_custom_range)
通過agg(),我們可以構建出非常精細的聚合報表。我記得有次做用戶行為分析,需要同時按用戶ID和行為類型來統計不同時段的操作次數,groupby([‘user_id’, ‘action_type’]).count()就完美解決了,那種感覺就像是找到了數據里的“密碼”。而當需要更深入的分析,比如計算每個用戶每次會話的平均時長,或者特定行為的轉化率時,agg配合自定義函數就顯得尤為靈活了。
apply()與transform():突破簡單聚合的界限
groupby的強大不僅僅體現在簡單的聚合上,它還提供了apply()和transform()這兩個高級方法,它們能讓你在分組數據上執行更復雜、更靈活的操作,甚至可以將結果“廣播”回原始DataFrame的形狀。
-
apply():自由度最高的“瑞士軍刀”apply()方法會將每個分組作為一個獨立的DataFrame(或Series)傳遞給你定義的函數。這意味著你可以在每個組內執行幾乎任何Pandas操作,包括排序、篩選、合并、復雜的計算等等。它的返回值可以是Series、DataFrame,甚至是標量。
# 查找每個區域銷售額最高的兩條記錄 def top_n_sales(group, n=2): return group.sort_values(by='銷售額', ascending=False).head(n) top_sales_per_region = df_complex.groupby('區域').apply(top_n_sales) print("n每個區域銷售額最高的兩條記錄:") print(top_sales_per_region) print("-" * 30)
但apply也有它的“陷阱”,比如性能問題,如果你的函數能被矢量化操作替代,那最好不要用apply。它在處理大數據量時可能會比較慢,因為它需要迭代每個組并調用Python函數。
-
transform():結果形狀不變的“廣播器”transform()方法也對每個分組應用一個函數,但它的關鍵特性是:它返回的結果必須與原始分組的形狀(行數)相同,并且會自動將結果對齊回原始DataFrame的索引。這使得它非常適合用于在組內進行數據標準化、填充缺失值、計算組內排名等操作,而不會改變原始DataFrame的結構。
# 計算每個產品的銷售額占該產品總銷售額的比例 df_complex['銷售額占比'] = df_complex.groupby('產品')['銷售額'].transform(lambda x: x / x.sum()) print("添加銷售額占比列:") print(df_complex) print("-" * 30) # 填充每個區域的缺失銷售額為該區域的平均銷售額(假設有缺失值) df_with_nan = df_complex.copy() df_with_nan.loc[0, '銷售額'] = np.nan df_with_nan.loc[4, '銷售額'] = np.nan # 使用transform填充缺失值 df_with_nan['銷售額_填充'] = df_with_nan.groupby('區域')['銷售額'].transform(lambda x: x.fillna(x.mean())) print("n填充缺失銷售額后的數據:") print(df_with_nan)
transform則需要特別注意它的返回結果必須和輸入組的行數一致,否則會報錯。它的優點是性能通常比apply好,因為它更傾向于使用Pandas的內部優化。
掌握apply和transform,意味著你不再局限于簡單的聚合,可以進行更細粒度的組內操作,并將結果無縫集成回原始數據結構中。它們是處理復雜分組邏輯,尤其是需要返回與原始數據等長結果時的強大工具。