§6
動手跑跑看:下載模擬資料,兩個 pandas 版本都跑一次
先下載本頁的模擬資料 driver_daily_stats.csv(2000 列 × 5 欄,約 62 KB),和下面的程式放在同一個資料夾,在那個資料夾開終端機執行 python 檔名.py。用 Google Colab 的話,先從左側「檔案」面板把 CSV 上傳到工作階段。不確定自己裝哪一版,先跑 import pandas as pd; print(pd.__version__)。
程式 1:題目的程式,再把第 4 行拆開看
import pandas as pd
df = pd.read_csv('driver_daily_stats.csv')
print(df['daily_earnings'].describe()[['mean', '50%', 'max']])
# ---- 加碼:把第 4 行拆開,一步一步看 ----
print('pandas 版本:', pd.__version__)
print('表格大小(列, 欄):', df.shape)
print(df.head()) # 前 5 列
print(df.dtypes) # 每一欄的資料型別
col = df['daily_earnings'] # ① 取出一欄
print('這一欄有幾格:', len(col), '其中空格:', col.isna().sum())
desc = col.describe() # ② 算出 8 個統計量
print(desc)
print(type(col))
print(type(desc))
print(type(desc['mean']))
實際跑出來的結果(兩版相同的部分)
mean 223.411306
50% 128.552462
max 4500.000000
Name: daily_earnings, dtype: float64
pandas 版本: 2.2.3 ← pandas 3 這行是 3.0.6
表格大小(列, 欄): (2000, 5)
driver_id daily_earnings region app_version gps_ping_rate
0 D0001 NaN A 5.3.1 1.000
1 D0002 21.438655 C 5.4.0 0.943
2 D0003 255.924079 B 5.3.1 1.017
3 D0004 133.313945 C 5.2.0 0.948
4 D0005 116.975976 B 5.3.1 0.827
前 4 行就是題目的輸出,三個數字和考卷一模一樣。接下來 df.dtypes 和三個 type(...) 兩版不同:
pandas 2.2.3 後半段
driver_id object
daily_earnings float64
region object
app_version object
gps_ping_rate float64
dtype: object
這一欄有幾格: 2000 其中空格: 100
count 1900.000000
mean 223.411306
std 314.383784
min 2.714220
25% 63.244931
50% 128.552462
75% 268.723376
max 4500.000000
Name: daily_earnings, dtype: float64
<class 'pandas.core.series.Series'>
<class 'pandas.core.series.Series'>
<class 'numpy.float64'>
pandas 3.0.6 後半段
driver_id str
daily_earnings float64
region str
app_version str
gps_ping_rate float64
dtype: object
這一欄有幾格: 2000 其中空格: 100
count 1900.000000
mean 223.411306
std 314.383784
min 2.714220
25% 63.244931
50% 128.552462
75% 268.723376
max 4500.000000
Name: daily_earnings, dtype: float64
<class 'pandas.Series'>
<class 'pandas.Series'>
<class 'numpy.float64'>
「← pandas 3 這行是 3.0.6」是本頁加的註解,程式不會印出來。
程式 2:四種處理,做成一張比較表
import pandas as pd
df = pd.read_csv('driver_daily_stats.csv')
col = df['daily_earnings']
# 四種處理,各做出一個「處理後的欄位」(都不會改到 df)
versions = {
'original': col, # 原始:統計時自動跳過 NaN
'A_mean': col.fillna(col.mean()), # A:用平均數補
'B_median': col.fillna(col.median()), # B:用中位數補
'C_max': col.fillna(col.max()), # C:用最大值補
'D_drop': df.dropna()['daily_earnings'], # D:整列刪掉
}
rows = []
for name, s in versions.items():
rows.append({'name': name, 'count': s.count(), 'mean': s.mean(),
'median': s.median(), 'std': s.std(),
'q25': s.quantile(0.25), 'q75': s.quantile(0.75)})
table = pd.DataFrame(rows).set_index('name') # 用 DataFrame 設計圖做一張新表格
print(table.round(2))
print('D 刪除後剩', len(df.dropna()), '列,原本', len(df), '列')
實際跑出來的結果(兩版完全相同)
count mean median std q25 q75
name
original 1900 223.41 128.55 314.38 63.24 268.72
A_mean 2000 223.41 138.34 306.42 65.21 253.03
B_median 2000 218.67 128.55 307.12 65.21 253.03
C_max 2000 437.24 138.34 981.36 65.21 302.32
D_drop 1900 223.41 128.55 314.38 63.24 268.72
D 刪除後剩 1900 列,原本 2000 列
注意這裡的 col.fillna(…) 都沒有 inplace:每一次都做一個新的 Series 交出來,col 和 df 完全不受影響(實測:新 Series 是 0 個 NaN,col 和 df 還是 100 個),所以四種處理可以從同一份原始資料各算各的。
程式 3:選項的 inplace 寫法 vs 兩種安全寫法
import pandas as pd
print('pandas 版本:', pd.__version__)
# 選項 B 的原文寫法:df[...] 先交出一欄,再叫那一欄 inplace 補值
df = pd.read_csv('driver_daily_stats.csv')
print('補值前 NaN:', df['daily_earnings'].isna().sum())
result = df['daily_earnings'].fillna(df['daily_earnings'].median(), inplace=True)
print('選項寫法之後 NaN:', df['daily_earnings'].isna().sum())
print('fillna 回傳的東西:', type(result))
# 安全寫法 1:算出補好的新欄位,再用等號放回 df
df = pd.read_csv('driver_daily_stats.csv')
df['daily_earnings'] = df['daily_earnings'].fillna(df['daily_earnings'].median())
print('安全寫法 1 之後 NaN:', df['daily_earnings'].isna().sum())
# 安全寫法 2:直接叫 df 自己補,用字典指定「哪一欄補什麼」
df = pd.read_csv('driver_daily_stats.csv')
df.fillna({'daily_earnings': df['daily_earnings'].median()}, inplace=True)
print('安全寫法 2 之後 NaN:', df['daily_earnings'].isna().sum())
實際跑出來的結果:兩版不一樣
警告訊息開頭會印出程式檔的完整路徑,每台電腦不同,下面用 …\ 代替;警告後面還會再印一次出問題的那一行程式。
pandas 2.2.3:有補上,但有 FutureWarning
pandas 版本: 2.2.3
補值前 NaN: 100
…\demo3.py:7: FutureWarning: A value is trying to be set on a copy of a DataFrame or Series through chained assignment using an inplace method.
The behavior will change in pandas 3.0. This inplace method will never work because the intermediate object on which we are setting values always behaves as a copy.
For example, when doing 'df[col].method(value, inplace=True)', try using 'df.method({col: value}, inplace=True)' or df[col] = df[col].method(value) instead, to perform the operation inplace on the original object.
result = df['daily_earnings'].fillna(df['daily_earnings'].median(), inplace=True)
選項寫法之後 NaN: 0
fillna 回傳的東西: <class 'NoneType'>
安全寫法 1 之後 NaN: 0
安全寫法 2 之後 NaN: 0
pandas 3.0.6:選項寫法沒有補到 df
pandas 版本: 3.0.6
補值前 NaN: 100
…\demo3.py:7: ChainedAssignmentError: A value is being set on a copy of a DataFrame or Series through chained assignment using an inplace method.
Such inplace method never works to update the original DataFrame or Series, because the intermediate object on which we are setting values always behaves as a copy (due to Copy-on-Write).
For example, when doing 'df[col].method(value, inplace=True)', try using 'df.method({col: value}, inplace=True)' instead, to perform the operation inplace on the original object, or try to avoid an inplace operation using 'df[col] = df[col].method(value)'.
See the documentation for a more detailed explanation: https://pandas.pydata.org/pandas-docs/stable/user_guide/copy_on_write.html
result = df['daily_earnings'].fillna(df['daily_earnings'].median(), inplace=True)
選項寫法之後 NaN: 100
fillna 回傳的東西: <class 'pandas.Series'>
安全寫法 1 之後 NaN: 0
安全寫法 2 之後 NaN: 0
- pandas 3 的 fillna 確實把 100 格補好了,回傳的就是那個補好的 Series;只是它是
df['daily_earnings'] 交出來的「另一個物件」,而且寫入時已經複製過,和 df 無關。
- 兩種安全寫法在兩版都是 0 個 NaN,而且完全沒有警告。
- 本頁把這支程式存成
demo3.py 執行,所以警告寫的是 demo3.py:7:第 7 行就是選項寫法那一行。
| 新出現的語法 | 是什麼 | 白話 |
{'original': col, …} | 字典 dict | 大括號裡一對一對的「鑰匙: 值」,用鑰匙找值 |
for name, s in versions.items(): | for 迴圈 | 把字典裡的每一對拿出來,鑰匙暫時叫 name、值暫時叫 s,各跑一次縮排裡的程式 |
rows.append({...}) | list 的方法 | 在清單尾巴加一個東西(這裡加的是一列的資料) |
pd.DataFrame(rows) | 類別 → 物件 | 拿 DataFrame 設計圖,把「一列一個字典」的清單做成一張表格 |
.set_index('name') | DataFrame 的方法 | 把 name 那一欄拿去當列標籤,交出一張新表格 |
table.round(2) | DataFrame 的方法 | 每個數字四捨五入到小數點後 2 位,交出新表格 |
s.quantile(0.25) | Series 的方法 | 第 25 百分位數,線性內插,和 describe 的 25% 一樣 |
df.dropna()['daily_earnings'] | 方法串接+取欄位 | 先整列刪掉有 NaN 的列,交出新表格,再從新表格取一欄 |
type(result) | 內建函式 | 看 fillna 交回來的是哪一種東西 |