Pandas資料分析#
本章要介紹的方法是pandas的重點功能:
資料分組後運算
新增修改資料欄位
組合多個資料表
資料分組後運算#
資料分組後運算通常有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
可以使用不同方式將資料分組:
直接指定欄位名稱
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
一個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'])
的略寫版本。
一個function,輸入為index,輸出為分組標籤:
這種方式比較不直觀,但其實也只是為了創造index跟分組標籤的對應關係。這邊lambda x
的x
傳入的就是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相關方法,特別是aggregation和transformation對進行特徵工程很有幫助,可以把計算出來的結果併入資料集,給後續分析或是建模使用。
那麼如何新增欄位呢?
假設我們要新增一個欄位是近一期帳單金額相較於前一期帳單金額的增率,最簡單的做法:
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=True
或right_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和欄位都可以用來合併。
舉例情境:都可以。