ChatGPT、Claude、GitHub Copilot 這類 AI 助理普及之後,寫程式的門檻已經大幅降低了。
只要丟一句「用 pandas 處理這筆資料」,很快就能拿到一段像樣的程式碼。確實很方便。
不過,若直接拿來用在實務上,危險的程式碼其實不少。
能跑,但很慢。能跑,但很吃記憶體。能跑,但結果有問題。
這種「安靜的地雷」常常會混在 AI 生成的程式碼裡。
這篇文章會整理出在使用 pandas / NumPy 做資料處理時,如果是 AI 給出來,最好先停一下再看的 10 種程式碼。
每一項都會一起看「為什麼危險」以及「該怎麼改」。
iterrows() 一行一行跑ratings = []
for idx, row in df.iterrows():
if row['rating'] > 4:
ratings.append('Excellent')
elif row['rating'] > 2:
ratings.append('Average')
else:
ratings.append('Poor')
df['evaluation'] = ratings
iterrows() 會把 DataFrame 一列一列以 Python 物件的方式取出。
因此,pandas 的強項——向量化——幾乎發揮不了作用。資料越多,就會越慢。
像這種條件判斷,改用 np.select 一次處理會直觀得多。
import numpy as np
df['evaluation'] = np.select(
condlist=[df['rating'] > 4, df['rating'] > 2],
choicelist=['Excellent', 'Average'],
default='Poor'
)
只要開始寫列迴圈,最好先懷疑一下:「能不能改成對整欄寫法?」
apply()df['price_with_tax'] = df['price'].apply(lambda x: x * 1.1)
apply() 看起來很漂亮,但底層其實還是在跑 Python 迴圈。
也就是說,寫法只是變得像函式,效能瓶頸本質上並沒有太大改變。
如果只是單純計算,直接做運算會更快也更好讀。
df['price_with_tax'] = df['price'] * 1.1
apply() 很方便,但通常應該先想:「這個處理真的有必要嗎?」
df1、df2、df3 這種流水號一直增加df1 = pd.read_csv('data.csv')
df2 = df1[df1['status'] == 'active']
df3 = df2.groupby('category').sum()
df4 = df3.reset_index()
df5 = df4.rename(columns={'amount': 'total_amount'})
這種寫法回頭看時,會越來越難搞清楚每個變數到底是什麼。
在 Notebook 裡來回切換 cell 時,更容易混亂。
變數名稱最好盡量直接表達內容。
active_df = pd.read_csv('data.csv').query('status == "active"')
category_totals = (
active_df
.groupby('category', as_index=False)['amount']
.sum()
.rename(columns={'amount': 'total_amount'})
)
比起流水號,能傳達處理意義的名稱好用得多。
result = (
pd.read_csv('sales.csv')
.query('year == 2024')
.assign(tax=lambda df_: df_['price'] * 0.1)
.assign(total=lambda df_: df_['price'] + df_['tax'])
.groupby('category', as_index=False)['total']
.sum()
.merge(pd.read_csv('categories.csv'), on='category')
.sort_values('total', ascending=False)
.reset_index(drop=True)
.rename(columns={'total': 'total_sales', 'name': 'category_name'})
.head(20)
)
把方法一路串起來,第一眼看起來很俐落,但太長之後就很難追。
中途如果出錯,也不容易看出到底是在哪一步壞掉。
把處理依照區塊拆開,之後修改會方便很多。
sales = pd.read_csv('sales.csv').query('year == 2024')
sales['tax'] = sales['price'] * 0.1
sales['total'] = sales['price'] + sales['tax']
category_totals = (
sales
.groupby('category', as_index=False)['total']
.sum()
.rename(columns={'total': 'total_sales'})
)
categories = pd.read_csv('categories.csv')
result = (
category_totals
.merge(categories, on='category')
.sort_values('total_sales', ascending=False)
.head(20)
)
像「讀取與加工」「彙總」「合併與整理」這樣切開,整體可讀性會好很多。
SettingWithCopyWarningimport warnings
warnings.filterwarnings('ignore')
df[df['category'] == 'A']['price'] = 999
這種寫法相當危險。
像 df[條件]['欄位'] 這樣的鏈式指定,不能保證真的會反映到原本的 DataFrame。
會出現 Warning,正是因為它在提醒你這個風險。
如果要指定值,請直接用 loc 一次定位。
df.loc[df['category'] == 'A', 'price'] = 999
不要把警告關掉,而是改成不會出現警告的寫法,才比較安全。
.loc 其實也可能有風險subset = df.loc[df['region'] == 'Asia']
subset.loc[subset['sales'] > 1000, 'tier'] = 'Gold'
這也沒有看起來那麼安全。
第一次取出的 subset 到底是 View 還是 Copy 不夠明確,後面的指定也會變得不穩定。
最好是明確做出副本,或是直接對原始 DataFrame 寫條件。
subset = df.loc[df['region'] == 'Asia'].copy()
subset.loc[subset['sales'] > 1000, 'tier'] = 'Gold'
或者,如果要直接更新原始資料,可以這樣寫:
df.loc[(df['region'] == 'Asia') & (df['sales'] > 1000), 'tier'] = 'Gold'
只要會用到中間變數,明確加上 copy() 會安心很多。
df = pd.read_csv('huge_data.csv')
檔案小的時候這樣做沒問題,但資料一大就會很吃力。
不只會把不需要的欄位也讀進來,型別也交給 pandas 自行推斷,記憶體消耗會高很多。
如果先指定需要的欄位和型別,會輕量許多。
df = pd.read_csv(
'huge_data.csv',
usecols=['user_id', 'item_id', 'rating', 'timestamp'],
dtype={
'user_id': 'int32',
'item_id': 'int32',
'rating': 'float16',
'timestamp': 'int64',
}
)
資料欄位越多,usecols 和 dtype 的效果就越明顯。
raw = pd.read_csv('10gb_data.csv', ...)
processed = heavy_transform(raw)
# raw 已經不會再用到,但還留著
final = another_transform(processed)
在 Notebook 裡工作時,這種中間資料會一直堆積。
一不小心就會變成記憶體不足,這是很常見的情況。
把不需要的變數刪掉,後續處理通常會輕鬆一些。
import gc
raw = pd.read_csv('10gb_data.csv', ...)
processed = heavy_transform(raw)
del raw
gc.collect()
final = another_transform(processed)
當你串接很多層巨量資料處理時,也要順便想好要在哪裡釋放資料。
df = pd.read_csv('users.csv')
print(df['user_id'].dtype)
# 有時候會變成 float64
含有缺失值的 ID,可能會因為 pandas 的處理方式而變成 float64。
結果 1234567 會變成 1234567.0,或者前導 0 會消失。
ID 是識別碼,不是數值,最好一開始就當成字串讀進來,會比較安全。
df = pd.read_csv('users.csv', dtype={'user_id': str})
或者,如果你希望在保留缺失值的情況下仍以整數型別處理,可以使用 Nullable Integer 型別。
df = pd.read_csv('users.csv', dtype={'user_id': 'Int64'})
一旦 ID 的型別亂掉,JOIN 就可能默默壞掉,這點要特別注意。
concat()import glob
df = pd.DataFrame()
for file in glob.glob('data/*.csv'):
tmp = pd.read_csv(file)
df = pd.concat([df, tmp])
這種寫法通常會慢得很明顯。
因為每次 concat 都會把整體重新複製一次,檔案越多就越重。
先收集到 list 裡,最後一次合併會自然得多。
import glob
dfs = [pd.read_csv(f) for f in glob.glob('data/*.csv')]
df = pd.concat(dfs, ignore_index=True)
合併檔案時,基本上記得「先收集,再一次合併」就很實用。
iterrows() 迴圈太慢改成向量化 / np.select``apply() 用太多變成 Python 迴圈使用運算子或內建函式df1、df2… 一直增加難以閱讀改成有意義的變數名稱方法鏈接太長不好除錯以處理單位拆開鏈式指定有可能不會反映使用 .loc``.loc 中間變數不明確 Copy / View 不清楚明確使用 .copy()直接讀完整 CSV 很吃記憶體指定 usecols、dtype中間 DF 放著不管記憶體不會釋放使用 del 和 gc.collect()把 ID 當數值讀進來 JOIN 可能壞掉使用 str 或 Int64在迴圈裡 concat() 很慢先收集到 list 再一次合併結語pandas 本身並沒有問題。
相反地,只要用對了,它其實非常強大。
真正的問題是,直接照單全收 AI 回傳的程式碼。
「能跑」和「能安全地用在實務上」是完全不同的兩件事。
尤其是在處理大資料時,
快不快、對不對、之後還看不看得懂
每次只要檢查這三件事,就能大幅降低出問題的機率。