はじめに
ChatGPTやClaude、GitHub CopilotのようなAIアシスタントが普及して、コードを書くハードルはかなり下がりました。
「pandasでこのデータを処理して」と投げれば、それなりのコードがすぐ返ってきます。たしかに便利です。
ただ、実務でそのまま使うには危ないコードも少なくありません。
動くけれど遅い。動くけれどメモリを食う。動くけれど結果が怪しい。
こういう“静かな地雷”が、AI生成コードには普通に混ざります。
この記事では、pandas / NumPy でデータ処理をするときに、AIが出してきたら一度止めたいコードを10個まとめます。
それぞれ「なぜ危ないのか」「どう直すのか」をセットで見ていきます。
1. iterrows() で1行ずつ回す
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 を1行ずつ Python オブジェクトとして取り出します。
そのため、pandas の強みであるベクトル化がほぼ効きません。データが増えるほど遅くなります。
こういう条件分岐なら、np.select でまとめて処理したほうがずっと素直です。
import numpy as np
df['evaluation'] = np.select(
condlist=[df['rating'] > 4, df['rating'] > 2],
choicelist=['Excellent', 'Average'],
default='Poor'
)
行ループを書き始めたら、まず「列全体で書けないか」を疑うのがよさそうです。
2. apply() をとりあえず使う
df['price_with_tax'] = df['price'].apply(lambda x: x * 1.1)
apply() は見た目こそきれいですが、中では Python のループが回っています。
つまり、書き方が関数っぽくなっただけで、重さの本質はあまり変わりません。
単純な計算なら、そのまま演算したほうが速くて読みやすいです。
df['price_with_tax'] = df['price'] * 1.1
apply() は便利ですが、まずは「その処理、本当に必要か」を見たほうがいいです。
3. 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 でセルを行き来すると、なおさら混乱しやすいです。
変数には、できるだけ中身がわかる名前をつけたほうがいいです。
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'})
)
連番よりも、処理の意味が伝わる名前のほうがずっと扱いやすいです。
4. メソッドチェーンをつなげすぎる
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)
)
「読込と加工」「集計」「結合と整形」くらいで区切ると、だいぶ見通しがよくなります。
5. SettingWithCopyWarning を無視する
import warnings
warnings.filterwarnings('ignore')
df[df['category'] == 'A']['price'] = 999
これはかなり危ない書き方です。
df[条件]['列'] のような連鎖代入は、元の DataFrame に反映される保証がありません。
Warning が出ているのは、まさにその危険を教えてくれているからです。
代入したいなら、loc を使って一発で指定します。
df.loc[df['category'] == 'A', 'price'] = 999
警告を消すのではなく、警告が出ない書き方に直すほうが安全です。
6. .loc も実は危ない
subset = df.loc[df['region'] == 'Asia']
subset.loc[subset['sales'] > 1000, 'tier'] = 'Gold'
これも、見た目ほど安全ではありません。
1回目の 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() を明示しておくと安心です。
7. CSVをそのまま全部読む
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 の効果は大きいです。
8. 使い終わった巨大 DataFrame を放置する
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)
巨大なデータを何段もつなぐときは、どこで捨てるかも考えたほうがいいです。
9. ID カラムを文字列として扱わない
df = pd.read_csv('users.csv')
print(df['user_id'].dtype)
# float64 になることがある
欠損を含む ID は、pandas の都合で float64 扱いになってしまうことがあります。
その結果、1234567 が 1234567.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 が静かに壊れるので注意が必要です。
10. concat() をループの中で何度も呼ぶ
import glob
df = pd.DataFrame()
for file in glob.glob('data/*.csv'):
tmp = pd.read_csv(file)
df = pd.concat([df, tmp])
これはかなり遅くなりがちな書き方です。
毎回 concat するたびに全体をコピーし直すので、ファイル数が増えるほど重くなります。
先にリストにためて、最後にまとめて結合するほうが自然です。
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()
|
かなり遅い | リストにためて最後に結合 |
おわりに
pandas 自体が悪いわけではありません。
むしろ、ちゃんと使えばかなり強力です。
問題は、AIが返してきたコードをそのまま受け入れてしまうことです。
「動く」ことと「実務で安全に使える」ことは、まったく別です。
特に大きいデータを扱うときは、
速いか、正しいか、あとで読めるか
この3つを毎回見るだけでも、事故はかなり減ります。