環境
python, pandas, jupyter のバージョン
> python --version # Python 3.7.4 import pandas as pd pd.__version__ # '0.25.2' > jupyter notebook --version # 6.0.1
発生事象
データ分析中にエラーに遭遇した。簡単なデータを使って再現すると、以下のようになる。
以下、jupyter notebook上で実行した。出力された結果は# ---
の後に記載している。
df = pd.DataFrame({ 'float_col' : [1.1, 2.2, 3.3, 4.4, 5.5, 6.6], 'int_col' : [0, 2, 4, 1, 3, 5] })
df --- float_col int_col 0 1.1 0 1 2.2 2 2 3.3 4 3 4.4 1 4 5.5 3 5 6.6 5
arr = list(range(10, 61, 10)) arr --- [10, 20, 30, 40, 50, 60]
idx = df.loc[2]["int_col"] arr[idx] --- --------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-6-90a797f8b25e> in <module> 1 idx = df.loc[2]["int_col"] ----> 2 arr[idx] TypeError: list indices must be integers or slices, not numpy.float64
idxには4が入っていると思っていたが、なぜか配列の添え字に指定するとエラーになった。
エラーメッセージはTypeError: list indices must be integers or slices, not numpy.float64
である。すなわち、
listのインデックスに指定できるのは整数かスライスだけで、numpy.float64 はダメだ、という意味だ。
え?整数のはずなのに、何でnumpy.float64になったの?
idx
---
4.0
type(idx)
---
numpy.float64
idxはnumpy.float64型の浮動小数点数4.0だったので、配列の添え字に使うとエラーになったというわけだ。では、なぜfloatになっていたのか?
仕組み、理由
実は、さっきの例では、少し変なlocの使い方をしていた。
df.loc[行名, 列名]の形で指定要素を取り出せるのに、さっきはdf.loc[行名][列名]と2回添え字を使っていた。
確かに、これでも結果として指定される要素は同じだ。しかしこの違いが、さっきのエラーを引き起こしたのだ。
df.loc[2]の時点で、単独の行をいったん選択・抽出している。単独の行とはすなわちSeriesであり、それは単一の型を持たねばならない。
ではその型は何か?
floatとintを両方格納できる型、すなわちfloatである。そしてintの4はこのときfloatの4.0へと自動的に型変換(キャスト)されたのだ。
df.dtypes
---
float_col float64
int_col int64
dtype: object
df.loc[2] --- float_col 3.3 int_col 4.0 Name: 2, dtype: float64
したがって、整数値を整数のまま取り出しエラーを回避するには、df.loc[行名, 列名]の形で要素を指定すればよい。こうすれば途中でSeriesを経由しないので、型変換も実行されず、整数の4は整数のままである。
idx2 = df.loc[2, "int_col"] arr[idx2] --- 50
同様の事象
仕組みが分かれば簡単な話である。この事象が発生する仕組みを改めて書くと、以下のようになるだろう。
- floatの列とint型の列が混在したDataFrameから
- 1行をSeriesとして抜き出すと
- int型の値が暗黙のうちに型変換(キャスト)されて、float型になる
したがって、locに限らず1行を選択・抽出すると同様の事象が発生する。
思いつくままに並べると、次のようになるだろう。
- loc, iloc
- iterrows
- 転置
locが行の名称を指定して行を選択するのに対して、ilocは行の位置(何行目か)を指定して行を選択する。ilocの場合もほとんど同様なので、説明は割愛する。
iterrows
iterrowsはDataFrameの各行を順番に抜き出す。listに対してforを使うのと同じような働きをする。
予想通り、int_colの値はすべてfloatになっていた。
for _, row in df.iterrows(): print(row["int_col"]) --- 0.0 2.0 4.0 1.0 3.0 5.0
なお、iterrowsのほうは公式ドキュメントに注意事項として書いてある。
pandas.DataFrame.iterrows — pandas 0.25.3 documentation
Because iterrows returns a Series for each row, it does not preserve dtypes across the rows (dtypes are preserved across columns for DataFrames).
拙訳:iterrowsは各行についてSeriesを返すので、行の中のdtypeは保存されない。(dtypesはDataFrameの列の中で保存されている)*1
転置
DataFrameの転置は、1行を選択・抽出するわけではない。 しかし、行と列を入れ替えるので、既存のDataFrameの各行をSeriesとして扱うことになり、結果としてlocやiterrowsなどと同じ状況が発生する。
df_t = df.transpose() df_t # --- 0 1 2 3 4 5 float_col 1.1 2.2 3.3 4.4 5.5 6.6 int_col 0.0 2.0 4.0 1.0 3.0 5.0
転置後のint_colが行になるが、これらの数値が浮動小数点数で表記されていることに注意。 なお、転置のほうは公式ドキュメントに注意事項として書いてある。
pandas.DataFrame.transpose — pandas 0.25.3 documentation
Notes Transposing a DataFrame with mixed dtypes will result in a homogeneous DataFrame with the object dtype. In such a case, a copy of the data is always made.
拙訳: 複数のdtypeからなるDataFrameを転置すると、objectのdtypeを持つ同質的なDataFrameが出来上がる。このような場合、常にデータのコピーができる。
関連
strが入っているとobjectになるから結果的に大丈夫ぽい。あとで。
型に関する公式ドキュメントでちょうど良いページが見つからなくて、困っている。
素直に「pandas dtype」で検索すると出るページはこれ。
pandas.DataFrame.dtypes — pandas 0.25.3 documentation
しかし、このページの説明はとても簡単なことしか書いていない。このページから行ける下記のページが、dtypeに関する公式情報かな?
Essential basic functionality — pandas 0.25.3 documentation
以下は、この記事で扱ってきた内容と似ているが少し違う例である。DataFrameを結合したらintの列にNaNが混じり、floatに自動的に型変換されたというものである。
Python: pandas で DataFrame を連結したら dtype が int から float になって驚いた話 - CUBE SUGAR CONTAINER
*1:acrossの訳し方むずい……