Tech カテゴリのイメージ

pandasでKeyErrorが出るときの原因と直し方。列名の空白と全角に気をつける[Python]

CSVを読み込んで列を取り出そうとしたら、確かにあるはずの列名でこれが出ることがある。

KeyError: 'price'

pandasだとこういう長いメッセージになることもある。

KeyError: "['price'] not in index"

画面で見ればちゃんとpriceという列がある。それなのに見つからないと言われる。原因の多くは、目に見えない文字が混ざっていることになる。

まず列名の実体を確認する

推測する前に、実際にどんな列名になっているかを確かめる。表示するだけだと空白が見えないので、リストにして出すのがコツになる。

print(df.columns.tolist())

これで['price ', ' name', 'price\n']のように、前後の空白や改行が見える形で出てくる。さらに厳密に見たいなら、1文字ずつ確認する。

for c in df.columns:
print(repr(c), [hex(ord(ch)) for ch in c])

reprを使うと'price 'のようにクォートが付いて表示されるので、空白の有無が確実にわかる。

よくある犯人はこのあたりになる。

・前後の半角スペース。'price ''price'は別物 ・全角スペース。 として混ざっていることがある ・BOM。UTF-8のCSVの先頭列にが付いて'id'になる ・改行。'price\n'のように末尾に残る ・全角の英字。'price''price'ではない

列名を一括で正規化する

原因を特定したら、読み込み直後に列名をそろえてしまうのが確実になる。

df.columns = df.columns.str.strip()

全角スペースも含めて落としたいなら、除去する文字を指定する。

df.columns = df.columns.str.strip().str.replace(" ", "", regex=False)

大文字小文字を統一しておくと、あとの参照が楽になる。

df.columns = df.columns.str.strip().str.lower()

BOMは読み込み時のエンコーディング指定で解決できる。

df = pd.read_csv("data.csv", encoding="utf-8-sig")

utf-8ではなくutf-8-sigにすると、BOMを自動的に取り除いてくれる。Excelで保存したCSVはBOM付きになりやすいので、日本語環境ではこちらを既定にしておくと事故が減る。

日本語のCSVで文字化けする場合は、そもそものエンコーディングが原因のこともある。その話はUnicodeDecodeErrorの対処にまとめた。

ヘッダー行がずれている

先頭に説明行やタイトル行が入っているファイルだと、1行目がヘッダーとして読まれず、列名がUnnamed: 0のようになる。

print(df.columns.tolist())
# ['売上データ', 'Unnamed: 1', 'Unnamed: 2']

この場合は、どの行がヘッダーかを指定する。

df = pd.read_csv("data.csv", header=2) # 3行目をヘッダーにする

Excelファイルならskiprowsを使うこともある。

df = pd.read_excel("data.xlsx", skiprows=2)

存在しない列を安全に扱う

列があるかどうかわからない状態で処理するなら、事前に確認する。

if "price" in df.columns:
total = df["price"].sum()

複数の列をまとめて選ぶとき、一部が無いだけで落ちるのを避けたいなら、積集合を取る。

want = ["id", "name", "price"]
cols = [c for c in want if c in df.columns]
sub = df[cols]

無い列を欠損として作りたい場合はreindexが使える。

sub = df.reindex(columns=want) # 無い列は NaN で作られる

loc と iloc の取り違え

列名ではなく行の指定でKeyErrorが出ることもある。locはラベル、ilocは位置を指す。

df.loc[0] # インデックスラベルが 0 の行。ラベルが無いと KeyError
df.iloc[0] # 先頭の行。常に動く

フィルタやsort_valuesをしたあとはインデックスが飛び飛びになるので、loc[0]が存在しないことがある。位置で取りたいならilocを使うか、インデックスを振り直す。

df = df.reset_index(drop=True)

辞書のKeyErrorの場合

pandasではなく素の辞書でKeyErrorが出ているなら、getを使うと既定値を返せる。

value = d.get("price", 0) # 無ければ 0

まとめ

・列があるのにKeyErrorが出るときは、まずdf.columns.tolist()で実体を確認する。reprで見ると空白がわかる ・原因の多くは前後の空白、全角スペース、BOM、改行、全角英字 ・読み込み直後にdf.columns = df.columns.str.strip()で正規化しておくと事故が減る ・Excel由来のCSVはencoding="utf-8-sig"にするとBOMが消える ・タイトル行があるファイルはheader=skiprows=でヘッダー位置を指定する ・複数列の選択はreindex(columns=...)にすると、無い列があっても落ちない ・行のKeyErrorはlocilocの取り違えが多い。フィルタ後はreset_index(drop=True)を挟む