Pandas資料分析#

本章要介紹的方法是pandas的重點功能:

  1. 資料分組後運算

  2. 新增修改資料欄位

  3. 組合多個資料表

資料分組後運算#

資料分組後運算通常有3大步驟:groupby → 運算 → 合併。

[step 1] groupby是將資料分組,例如依據性別、學歷、年齡級距等等。

[step 2] 資料分組後的運算通常會是這三種動作之一:聚合(aggregation)、轉換(transformation)、過濾(filteration)

  • 聚合是將分組後的資料,一組一組個別計算統計值。例如原始資料根據性別,計算各性別收入平均數。

  • 轉換是將分組後的資料,一組一組進行邏輯運算,產生出新的欄位。例如原始資料根據日期,累加每日的營業額。

  • 過濾是將分組後的資料,一組一組根據指定的條件做篩選,產生篩選後的資料。例如原始資料根據選區分組,篩選出執政黨得票率超過50%的選區。

[step 3] 合併的步驟就是把上一步產出的資料合併起來。

  • 假設原始資料筆數是1000筆、性別紀錄有2種,groupby + aggregation後,資料就是2筆。

  • 假設原始資料筆數是1000筆,groupby + transformation後,資料仍然是1000筆。

  • 假設原始資料筆數是1000筆,groupby + filteration後的資料筆數不確定,要看符合篩選條件的資料筆數。

首先介紹groupby方法:

Groupby#

本節主要使用的資料集為台灣2005年信用卡客戶的繳款及還款資訊,內含基本的性別、年齡、婚姻狀況等資訊。

變數的定義及說明請參考資料來源:Default of Credit Card Clients Dataset (kaggle.com)

首先讀入套件和資料:

import pandas as pd
df = pd.read_csv('./data/UCI_Credit_Card.csv')

檢視資料的基本資訊:

df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 30000 entries, 0 to 29999
Data columns (total 25 columns):
 #   Column                      Non-Null Count  Dtype  
---  ------                      --------------  -----  
 0   ID                          30000 non-null  int64  
 1   LIMIT_BAL                   30000 non-null  float64
 2   SEX                         30000 non-null  int64  
 3   EDUCATION                   30000 non-null  int64  
 4   MARRIAGE                    30000 non-null  int64  
 5   AGE                         30000 non-null  int64  
 6   PAY_0                       30000 non-null  int64  
 7   PAY_2                       30000 non-null  int64  
 8   PAY_3                       30000 non-null  int64  
 9   PAY_4                       30000 non-null  int64  
 10  PAY_5                       30000 non-null  int64  
 11  PAY_6                       30000 non-null  int64  
 12  BILL_AMT1                   30000 non-null  float64
 13  BILL_AMT2                   30000 non-null  float64
 14  BILL_AMT3                   30000 non-null  float64
 15  BILL_AMT4                   30000 non-null  float64
 16  BILL_AMT5                   30000 non-null  float64
 17  BILL_AMT6                   30000 non-null  float64
 18  PAY_AMT1                    30000 non-null  float64
 19  PAY_AMT2                    30000 non-null  float64
 20  PAY_AMT3                    30000 non-null  float64
 21  PAY_AMT4                    30000 non-null  float64
 22  PAY_AMT5                    30000 non-null  float64
 23  PAY_AMT6                    30000 non-null  float64
 24  default.payment.next.month  30000 non-null  int64  
dtypes: float64(13), int64(12)
memory usage: 5.7 MB

groupby的基本用法:

df.groupby('SEX')['LIMIT_BAL'].mean()
SEX
1    163519.825034
2    170086.462014
Name: LIMIT_BAL, dtype: float64

上述語法把客戶根據性別分組,分別計算信用卡額度平均數。

語法應該很直觀好懂,讓我們來進一步解析。

groupby可以使用不同方式將資料分組:

  1. 直接指定欄位名稱

df.groupby('EDUCATION')['LIMIT_BAL'].mean()
EDUCATION
0    217142.857143
1    212956.069910
2    147062.437634
3    126550.270490
4    220894.308943
5    168164.285714
6    148235.294118
Name: LIMIT_BAL, dtype: float64
  1. 一個mapping index跟分組標籤的物件,可以是pd.Series或dictionary:

df.groupby(df['EDUCATION'])['LIMIT_BAL'].mean()
EDUCATION
0    217142.857143
1    212956.069910
2    147062.437634
3    126550.270490
4    220894.308943
5    168164.285714
6    148235.294118
Name: LIMIT_BAL, dtype: float64

因為pd.Series有index跟分組標籤的對應關係:

df['EDUCATION']
0        2
1        2
2        2
3        2
4        2
        ..
29995    3
29996    3
29997    2
29998    3
29999    2
Name: EDUCATION, Length: 30000, dtype: int64

事實上df.groupby('EDUCATION')df.groupby(df['EDUCATION'])的略寫版本。

  1. 一個function,輸入為index,輸出為分組標籤:

這種方式比較不直觀,但其實也只是為了創造index跟分組標籤的對應關係。這邊lambda xx傳入的就是df的index。 原始index是0~29999,將index除以10000再四捨五入到整數,就變成0~3了。

df.groupby(lambda x: round(x / 10000))['LIMIT_BAL'].mean()
0    165640.871826
1    166523.020302
2    166337.366263
3    173545.909182
Name: LIMIT_BAL, dtype: float64

如果忘記lambda function的用法可以回去參考前面python的章節。

簡而言之,groupby的功能就是要把資料從原本index的層級轉換到分組標籤的層級。

接下來就進入分組後的運算,首先是Aggregation。

Aggregation#

上面的語法其實就屬於aggregation類的運算。

單一統計值計算

例如想看信用卡額度有沒有性別差異(1=male, 2=female),可以把客戶根據性別分組,分別計算信用卡額度平均數。

使用方式如下:

df.groupby('SEX')['LIMIT_BAL'].mean()
SEX
1    163519.825034
2    170086.462014
Name: LIMIT_BAL, dtype: float64

注意到這邊回傳的是一個Series。

如果改用以下方式的話,回傳的會是一個DataFrame。

df.groupby('SEX')[['LIMIT_BAL']].mean()
LIMIT_BAL
SEX
1 163519.825034
2 170086.462014

差別在於被計算的欄位在選用時,是[]還是[[]]

其中.mean()是pandas內建的統計方法,還有許多可以用:

例如:

df.groupby('SEX')['LIMIT_BAL'].median()
SEX
1    130000.0
2    150000.0
Name: LIMIT_BAL, dtype: float64
df.groupby('SEX')['LIMIT_BAL'].std()
SEX
1    136250.481355
2    125231.777960
Name: LIMIT_BAL, dtype: float64

如果要一次算多個欄位,就只要在groupby後指定多個欄位名稱:

df.groupby('SEX')[['LIMIT_BAL', 'BILL_AMT1']].mean()
LIMIT_BAL BILL_AMT1
SEX
1 163519.825034 54281.004038
2 170086.462014 49216.395263

如果分組要依據的欄位不只一個,就只要把要分組的欄位名稱包成一個list放入groupby中:

df.groupby(['SEX', 'EDUCATION'])[['LIMIT_BAL']].count()
LIMIT_BAL
SEX EDUCATION
1 0 8
1 4354
2 5374
3 1990
4 42
5 95
6 25
2 0 6
1 6231
2 8656
3 2927
4 81
5 185
6 26

多個統計值計算

如果要計算多個的統計值的話該怎麼做?

可以透過.agg()方法,方法內指定要計算的統計值名稱:

df.groupby('SEX')['LIMIT_BAL'].agg(['mean','std'])
mean std
SEX
1 163519.825034 136250.481355
2 170086.462014 125231.777960

常用的還有:'min''max''median''count''nunique'等等。

一次算多個欄位也可以:

df.groupby('SEX')[['LIMIT_BAL', 'BILL_AMT1']].agg(['mean','std'])
LIMIT_BAL BILL_AMT1
mean std mean std
SEX
1 163519.825034 136250.481355 54281.004038 77359.281251
2 170086.462014 125231.777960 49216.395263 71016.668364

但會注意到欄位名稱會變成有兩個level。

DataFrame的欄位有兩個level的話,在後續處理會比較麻煩。

因此,建議是用以下的寫法,雖然要打比較多字,但寫法比較彈性,可以針對不同欄位計算不同統計值,也可避免產生多個level。

(df
 .groupby('SEX')
 .agg(
    LIMIT_BAL_mean = ('LIMIT_BAL', 'mean'), 
    LIMIT_BAL_std = ('LIMIT_BAL', 'std'), 
    BILL_AMT1_mean = ('BILL_AMT1', 'mean'), 
    BILL_AMT1_std = ('BILL_AMT1', 'std')
))
LIMIT_BAL_mean LIMIT_BAL_std BILL_AMT1_mean BILL_AMT1_std
SEX
1 163519.825034 136250.481355 54281.004038 77359.281251
2 170086.462014 125231.777960 49216.395263 71016.668364

注意到上面的語法有()包在最外層,否則無法在.前換行,這樣換行會讓結構比較清楚易讀。

其實aggregation還有很多種寫法,可參考:Pandas GroupBy Applications that Everyone Should Know | by Pradeep | Medium

Transformation#

直接看例子就可以知道Transformation跟Aggregation的差異:

transformation

df.groupby('SEX')['LIMIT_BAL'].transform('mean')
0        170086.462014
1        170086.462014
2        170086.462014
3        170086.462014
4        163519.825034
             ...      
29995    163519.825034
29996    163519.825034
29997    163519.825034
29998    163519.825034
29999    163519.825034
Name: LIMIT_BAL, Length: 30000, dtype: float64

aggregation

df.groupby('SEX')['LIMIT_BAL'].agg('mean')
SEX
1    163519.825034
2    170086.462014
Name: LIMIT_BAL, dtype: float64

屬於transformation類的方法還有:cumsum, cummax, cummin, rank等等。

假設紀錄了兩位客戶今天的交易資料(已照交易順序排序),使用方式如下:

txn_df = pd.DataFrame({
    'cust_id': [0, 0, 0, 1, 1],
    'nth_txn': [1, 2, 3, 1, 2],
    'txn_amt': [15, 25, 100, 80, 30]
})
txn_df
cust_id nth_txn txn_amt
0 0 1 15
1 0 2 25
2 0 3 100
3 1 1 80
4 1 2 30

以下語法可以計算每位客戶截至每一筆交易的累計金額:

txn_df.groupby('cust_id')['txn_amt'].cumsum()
0     15
1     40
2    140
3     80
4    110
Name: txn_amt, dtype: int64

或是計算每位客戶截至每一筆交易的最大金額:

txn_df.groupby('cust_id')['txn_amt'].cummax()
0     15
1     25
2    100
3     80
4     80
Name: txn_amt, dtype: int64

或是計算排序:

txn_df.groupby('cust_id')['txn_amt'].rank()
0    1.0
1    2.0
2    3.0
3    2.0
4    1.0
Name: txn_amt, dtype: float64

說到排序當然也可以用降冪排序:

txn_df.groupby('cust_id')['txn_amt'].rank(ascending=False)
0    3.0
1    2.0
2    1.0
3    1.0
4    2.0
Name: txn_amt, dtype: float64

Filteration#

filteration的主要功能是篩選資料,功能跟.loc[]query()類似,只是並非直接根據欄位值做篩選,而是根據聚合後的統計值。

最簡單的使用方式是搭配.sort_values方法,例如想要取各學歷組別中,額度(LIMIT_BAL)最高的人的資料:

df.sort_values(by='LIMIT_BAL').groupby('EDUCATION').tail(1)
ID LIMIT_BAL SEX EDUCATION MARRIAGE AGE PAY_0 PAY_2 PAY_3 PAY_4 ... BILL_AMT4 BILL_AMT5 BILL_AMT6 PAY_AMT1 PAY_AMT2 PAY_AMT3 PAY_AMT4 PAY_AMT5 PAY_AMT6 default.payment.next.month
6876 6877 360000.0 1 0 2 30 0 0 -1 0 ... 34345.0 36777.0 30.0 23000.0 12280.0 25007.0 25008.0 1767.0 3300.0 0
20843 20844 540000.0 1 6 2 49 0 0 0 0 ... 20879.0 3698.0 209167.0 3538.0 4380.0 2067.0 564.0 207019.0 6716.0 0
21239 21240 550000.0 1 5 1 37 0 0 0 0 ... 91903.0 91010.0 70045.0 16025.0 14407.0 3512.0 3506.0 2512.0 125020.0 1
23091 23092 710000.0 2 4 2 32 0 0 0 0 ... 195642.0 189101.0 182062.0 11000.0 12000.0 8000.0 7000.0 7000.0 6500.0 0
21375 21376 760000.0 1 3 1 54 0 0 0 0 ... 469931.0 474421.0 480598.0 20011.0 19500.0 19019.0 18003.0 20000.0 22000.0 0
14919 14920 800000.0 1 2 1 53 -1 -1 -1 0 ... 40814.0 12590.0 9339.0 11145.0 64657.0 131.0 247.0 9366.0 63482.0 0
2197 2198 1000000.0 2 1 1 47 0 0 0 -1 ... 891586.0 927171.0 961664.0 50784.0 50723.0 896040.0 50000.0 50000.0 50256.0 0

7 rows × 25 columns

tail()當然也就有head()

df.sort_values(by='LIMIT_BAL').groupby('EDUCATION').head(1)
ID LIMIT_BAL SEX EDUCATION MARRIAGE AGE PAY_0 PAY_2 PAY_3 PAY_4 ... BILL_AMT4 BILL_AMT5 BILL_AMT6 PAY_AMT1 PAY_AMT2 PAY_AMT3 PAY_AMT4 PAY_AMT5 PAY_AMT6 default.payment.next.month
3007 3008 10000.0 2 2 1 24 1 2 2 2 ... 4837.0 5208.0 5475.0 0.0 1600.0 0.0 600.0 500.0 0.0 0
18340 18341 10000.0 2 1 1 23 0 0 0 0 ... 1410.0 9508.0 8997.0 1262.0 1000.0 1115.0 9329.0 331.0 1000.0 0
27483 27484 10000.0 1 3 2 56 2 2 0 0 ... 6711.0 7258.0 7777.0 0.0 1500.0 2000.0 2000.0 2000.0 2000.0 0
13148 13149 10000.0 1 6 2 47 0 0 0 -1 ... 2640.0 695.0 738.0 1000.0 0.0 2640.0 695.0 738.0 0.0 0
9503 9504 10000.0 1 5 2 35 3 2 0 0 ... 8838.0 0.0 0.0 25.0 4000.0 0.0 1000.0 0.0 0.0 0
11372 11373 20000.0 2 4 2 22 0 0 0 0 ... 18657.0 8539.0 0.0 1500.0 1200.0 1000.0 1000.0 0.0 0.0 0
19920 19921 50000.0 2 0 1 40 0 0 0 0 ... 47647.0 40500.0 41921.0 2229.0 2298.0 2100.0 2500.0 1921.0 8432.0 0

7 rows × 25 columns

或是第n筆(起始點為0):

df.sort_values(by='LIMIT_BAL').groupby('EDUCATION').nth(0)
ID LIMIT_BAL SEX EDUCATION MARRIAGE AGE PAY_0 PAY_2 PAY_3 PAY_4 ... BILL_AMT4 BILL_AMT5 BILL_AMT6 PAY_AMT1 PAY_AMT2 PAY_AMT3 PAY_AMT4 PAY_AMT5 PAY_AMT6 default.payment.next.month
3007 3008 10000.0 2 2 1 24 1 2 2 2 ... 4837.0 5208.0 5475.0 0.0 1600.0 0.0 600.0 500.0 0.0 0
18340 18341 10000.0 2 1 1 23 0 0 0 0 ... 1410.0 9508.0 8997.0 1262.0 1000.0 1115.0 9329.0 331.0 1000.0 0
27483 27484 10000.0 1 3 2 56 2 2 0 0 ... 6711.0 7258.0 7777.0 0.0 1500.0 2000.0 2000.0 2000.0 2000.0 0
13148 13149 10000.0 1 6 2 47 0 0 0 -1 ... 2640.0 695.0 738.0 1000.0 0.0 2640.0 695.0 738.0 0.0 0
9503 9504 10000.0 1 5 2 35 3 2 0 0 ... 8838.0 0.0 0.0 25.0 4000.0 0.0 1000.0 0.0 0.0 0
11372 11373 20000.0 2 4 2 22 0 0 0 0 ... 18657.0 8539.0 0.0 1500.0 1200.0 1000.0 1000.0 0.0 0.0 0
19920 19921 50000.0 2 0 1 40 0 0 0 0 ... 47647.0 40500.0 41921.0 2229.0 2298.0 2100.0 2500.0 1921.0 8432.0 0

7 rows × 25 columns

或是可以客製化依需求過濾,舉一個案例如下:

回到信用卡的資料集,假設我們想查看那些違約率高的年齡組別的人:

df.groupby('AGE').filter(lambda d: d['default.payment.next.month'].mean() > 0.5 )
ID LIMIT_BAL SEX EDUCATION MARRIAGE AGE PAY_0 PAY_2 PAY_3 PAY_4 ... BILL_AMT4 BILL_AMT5 BILL_AMT6 PAY_AMT1 PAY_AMT2 PAY_AMT3 PAY_AMT4 PAY_AMT5 PAY_AMT6 default.payment.next.month
387 388 80000.0 1 3 1 73 -1 0 0 2 ... 70646.0 72870.0 75018.0 3500.0 4330.0 3500.0 3500.0 3500.0 5100.0 1
10699 10700 140000.0 1 2 1 73 0 0 0 0 ... 135772.0 138675.0 137197.0 5082.0 5008.0 5000.0 6016.0 5018.0 5264.0 0
29163 29164 360000.0 2 3 1 73 -2 -2 -2 -2 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1
29179 29180 500000.0 2 2 1 73 -2 -2 -2 -2 ... 2835.0 8896.0 3850.0 2826.0 2652.0 2835.0 8896.0 3850.0 711.0 1

4 rows × 25 columns

(’default.payment.next.month’是下個月繳款是否違約,1為違約,0為正常繳款,所以取平均數即為違約率)

新增修改資料欄位#

以上groupby相關方法,特別是aggregationtransformation對進行特徵工程很有幫助,可以把計算出來的結果併入資料集,給後續分析或是建模使用。

那麼如何新增欄位呢?

假設我們要新增一個欄位是近一期帳單金額相較於前一期帳單金額的增率,最簡單的做法:

df['billamt_gwr'] = (df['BILL_AMT1'] - df['BILL_AMT2']) / df['BILL_AMT2']

但這樣的做法有幾個缺點:

原始資料集會被異動,要回溯異動前的狀態就必須重新產一次異動前的資料。

解決方式是在進行欄位新增或修改的操作前,先把DataFrame複製下來。

複製的方法要使用.copy(),不使用.copy()的話並不會真的複製一個物件,只是多了一個變數名稱指向原始資料物件而已:

df_processing = df.copy()

另外,這種做法每次新增或修改一個欄位就要獨立寫一行,多行之間可能會沒有清楚的聯繫,語法會比較難以閱讀。

所以推薦的作法是使用.assign()方法:

df_processing = (df
                .assign(
                    billamt_gwr1 = lambda d: (d['BILL_AMT1'] - d['BILL_AMT2']) / d['BILL_AMT2'],
                    billamt_gwr2 = lambda d: (d['BILL_AMT2'] - d['BILL_AMT3']) / d['BILL_AMT3'],
                    billamt_gwr3 = lambda d: (d['BILL_AMT3'] - d['BILL_AMT4']) / d['BILL_AMT4'],
                ))

其中lambda d敘述句是產生一個匿名函數,以d作為參數,d可以任意命名,這邊習慣命名為d是為了代稱data,因為傳入這個匿名函數的input就是前面的df這個DataFrame。

.assign()方法會直接產生一個新的物件,不會影響到原有的資料集。

新建的DataFrame:

df_processing[['billamt_gwr1', 'billamt_gwr2', 'billamt_gwr']]
billamt_gwr1 billamt_gwr2 billamt_gwr
0 0.261444 3.502177 0.261444
1 0.554783 -0.356823 0.554783
2 1.084480 0.034516 1.084480
3 -0.025771 -0.021464 -0.025771
4 0.519753 -0.841775 0.519753
... ... ... ...
29995 -0.020055 -0.074629 -0.020055
29996 -0.079322 -0.478013 -0.079322
29997 0.062277 0.216824 0.062277
29998 -1.020988 0.027194 -1.020988
29999 -0.019957 -0.017261 -0.019957

30000 rows × 3 columns

原本的DataFrame:

df[['billamt_gwr1', 'billamt_gwr2', 'billamt_gwr']]
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[34], line 1
----> 1 df[['billamt_gwr1', 'billamt_gwr2', 'billamt_gwr']]

File ~/Library/Caches/pypoetry/virtualenvs/ntu-ps-intro-ml-GyDE60mE-py3.11/lib/python3.11/site-packages/pandas/core/frame.py:3899, in DataFrame.__getitem__(self, key)
   3897     if is_iterator(key):
   3898         key = list(key)
-> 3899     indexer = self.columns._get_indexer_strict(key, "columns")[1]
   3901 # take() does not accept boolean indexers
   3902 if getattr(indexer, "dtype", None) == bool:

File ~/Library/Caches/pypoetry/virtualenvs/ntu-ps-intro-ml-GyDE60mE-py3.11/lib/python3.11/site-packages/pandas/core/indexes/base.py:6115, in Index._get_indexer_strict(self, key, axis_name)
   6112 else:
   6113     keyarr, indexer, new_indexer = self._reindex_non_unique(keyarr)
-> 6115 self._raise_if_missing(keyarr, indexer, axis_name)
   6117 keyarr = self.take(indexer)
   6118 if isinstance(key, Index):
   6119     # GH 42790 - Preserve name from an Index

File ~/Library/Caches/pypoetry/virtualenvs/ntu-ps-intro-ml-GyDE60mE-py3.11/lib/python3.11/site-packages/pandas/core/indexes/base.py:6179, in Index._raise_if_missing(self, key, indexer, axis_name)
   6176     raise KeyError(f"None of [{key}] are in the [{axis_name}]")
   6178 not_found = list(ensure_index(key)[missing_mask.nonzero()[0]].unique())
-> 6179 raise KeyError(f"{not_found} not in index")

KeyError: "['billamt_gwr1', 'billamt_gwr2'] not in index"

.assign()方法很方便,幾乎是處理欄位首選的方法。

無論填補遺漏值、改名、格式轉換、transformation等等各種操作都可以寫在``.assign()```裡面。

而且當下新增的欄位也可以在同一個.assign中使用(看下面age_group_mean)的例子。

例如以下的語法展示了各種欄位的操作方式:

df_processing = (df
                .assign(
                    # 遺漏值填補
                    billamt_gwr1 = lambda d: ((d['BILL_AMT1'] - d['BILL_AMT2']) / d['BILL_AMT2']).fillna(0),
                    # 更改名稱(實際上是新建欄位)
                    target = lambda d: d['default.payment.next.month'],
                    # 格式轉換
                    limit_bal = lambda d: d['LIMIT_BAL'].astype(int),
                    # 切割
                    age_group = lambda d: pd.qcut(d['AGE'], 7, duplicates='drop'),
                    # transformation
                    age_group_mean = lambda d: d.groupby('age_group', observed=True)['limit_bal'].transform('mean'),
                    # 新增固定值
                    cnt = 1
                ))

查看結果:

df_processing.sample(5)
ID LIMIT_BAL SEX EDUCATION MARRIAGE AGE PAY_0 PAY_2 PAY_3 PAY_4 ... PAY_AMT5 PAY_AMT6 default.payment.next.month billamt_gwr billamt_gwr1 target limit_bal age_group age_group_mean cnt
7785 7786 100000.0 1 3 3 48 2 -1 0 0 ... 102.0 0.0 1 0.250476 0.250476 1 100000 (46.0, 79.0] 167830.202488 1
24154 24155 110000.0 1 2 2 29 2 2 2 2 ... 2000.0 1854.0 1 -0.045612 -0.045612 1 110000 (26.0, 29.0] 166401.692273 1
20612 20613 70000.0 1 1 2 26 0 0 0 0 ... 0.0 2448.0 0 -0.001232 -0.001232 0 70000 (20.999, 26.0] 87249.853716 1
12785 12786 240000.0 2 2 1 40 -1 -1 -1 -1 ... 3927.0 2529.0 0 1.597101 1.597101 0 240000 (36.0, 40.0] 197235.494881 1
6621 6622 50000.0 2 2 1 41 2 2 2 0 ... 296.0 60.0 1 -0.010406 -0.010406 1 50000 (40.0, 46.0] 183018.845509 1

5 rows × 32 columns

結合多個DataFrame#

進行資料分析時常常需要使用的欄位是散落在各個資了表中,因此合併資料表是一個常用的功能。

這邊介紹pandas主要的3種方法:pd.concat().join().merge()

pd.concat()#

pd.concat()通常用在多個相同的資料表上

例如信用卡業務統計數據每個月都會有一份,這時候就很適合使用pd.concat()

使用方式舉例如下:

df_202312 = pd.DataFrame({
    'yyyymm': ['202312', '202312', '202312'],
    'bank_no': ['001', '002', '003'],
    'new_card': [12198, 17390, 23209],
})
df_202401 = pd.DataFrame({
    'yyyymm': ['202401', '202401', '202401'],
    'bank_no': ['001', '002', '003'],
    'new_card': [14692, 16729, 27390],
})
df_202312
yyyymm bank_no new_card
0 202312 001 12198
1 202312 002 17390
2 202312 003 23209
df_202401
yyyymm bank_no new_card
0 202401 001 14692
1 202401 002 16729
2 202401 003 27390

上面兩個資料集基本上是同樣的資料集,只是記錄的是不同月份的數據,因此適合根據列的方向來合併(axis=0):

pd.concat([df_202312, df_202401], axis=0)
yyyymm bank_no new_card
0 202312 001 12198
1 202312 002 17390
2 202312 003 23209
0 202401 001 14692
1 202401 002 16729
2 202401 003 27390

注意到index的部分也一起合併起來了,造成index重複。

index重複會讓後續計算產生很多bug,千萬要注意不能讓index重複,所以根據axis=0合併資料表的話通常要加上ignore_index=True參數:

pd.concat([df_202312, df_202401], axis=0, ignore_index=True)
yyyymm bank_no new_card
0 202312 001 12198
1 202312 002 17390
2 202312 003 23209
3 202401 001 14692
4 202401 002 16729
5 202401 003 27390

或是使用keys參數,指定可以區別資料的index:

pd.concat([df_202312, df_202401], axis=0, keys=['202312', '202401'])
yyyymm bank_no new_card
202312 0 202312 001 12198
1 202312 002 17390
2 202312 003 23209
202401 0 202401 001 14692
1 202401 002 16729
2 202401 003 27390

當然,合併資料前盡量要注意欄位是否一致,雖然pd.concat會比對欄位名稱來合併資料:

df_202402 = pd.DataFrame({
    'yyyymm': ['202402', '202402', '202402'],
    'new_card': [12920, 13028, 18921],
    'bank_no': ['001', '002', '003'],
})

注意到這個資料集的欄位順序不一樣。

df_202402
yyyymm new_card bank_no
0 202402 12920 001
1 202402 13028 002
2 202402 18921 003
pd.concat([df_202312, df_202401, df_202402], axis=0, ignore_index=True)
yyyymm bank_no new_card
0 202312 001 12198
1 202312 002 17390
2 202312 003 23209
3 202401 001 14692
4 202401 002 16729
5 202401 003 27390
6 202402 001 12920
7 202402 002 13028
8 202402 003 18921

以上的情境是把資料表縱向地合併起來,但也可以橫向合併,這時要比對的就不是欄位名稱而是index了。

.join()#

.join()通常使用的情境是兩個資料表的層級不一樣,一張資料表用欄位值去串另一張資料表的index

例如一個資料表的一個列代表的是交易,另一個資料表的一列代表的是客戶。

因為一個客戶會有多筆交易,所以我們說交易資料表有比較細的層級,而客戶資料表是比較粗的層級。

使用.join()的情境就是,使用較細的資料表的欄位,去結合比較粗的資料表的index。

如下範例:

df1 = pd.DataFrame({
    'user_id': ['a', 'b',  'b', 'c', 'd', 'e', 'e'], 
    'txn_amt': [2821, 320, 1238, 8912, 7628, 1212, 2443],
})
df2 = pd.DataFrame({
    'age': [22, 30, 28, 34, 26],
    'gender': ['F', 'M', 'F', 'F', 'M'],
},  index=['a', 'b', 'c', 'd', 'e'])
df1
user_id txn_amt
0 a 2821
1 b 320
2 b 1238
3 c 8912
4 d 7628
5 e 1212
6 e 2443
df2
age gender
a 22 F
b 30 M
c 28 F
d 34 F
e 26 M

.join()是DataFrame方法,所以必須在DataFrame後面呼叫它,並指定要join的欄位。

指定的欄位名稱是df1的欄位,df2不需要指定,因為是在index。

df1.join(df2, on=['user_id'])
user_id txn_amt age gender
0 a 2821 22 F
1 b 320 30 M
2 b 1238 30 M
3 c 8912 28 F
4 d 7628 34 F
5 e 1212 26 M
6 e 2443 26 M

.join()通常只用在橫向合併。

.merge()#

.merge()則是可以根據欄位也可以根據index做結合,但一次只能合併兩張資料表。

放在左側的資料表稱作left table,右側當然就叫right table。

用欄位合併的話參數名稱是left_on=[’欄位名稱’]right_on=[’欄位名稱’]

用index合併的話,是left_index=Trueright_index=True

要將df1跟df2合併的話,是使用df1的欄位跟df2的index,所以語法是這樣寫:

df1.merge(df2, left_on='user_id', right_index=True)
user_id txn_amt age gender
0 a 2821 22 F
1 b 320 30 M
2 b 1238 30 M
3 c 8912 28 F
4 d 7628 34 F
5 e 1212 26 M
6 e 2443 26 M

注意到上面的結果跟使用.join()是一樣的,但.join()會比較快一些。

.merge()通常也只用在橫向合併。

統整比較#

  • pd.concat

    • pandas function

    • 可以縱向或橫向合併

    • 可以一次合併多個資料表

    • 只能根據欄位名稱(縱向)或是根據index(橫向)合併。

    • 舉例情境:不同月份的相同資料表。

  • .join()

    • DataFrame method

    • 只能橫向合併

    • 可以一次合併多個資料表

    • left table可以使用index或欄位,但right table只能根據index來合併。

    • 舉例情境:客戶交易資料串入客戶特徵資料。

  • .merge()

    • DataFrame method

    • 只能橫向合併

    • 一次只能合併兩張資料表

    • 無論left or right,index和欄位都可以用來合併。

    • 舉例情境:都可以。