pandasでDataFrameから行をSeriesとして抽出した場合、暗黙の型変換が実行されるので注意

環境

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の訳し方むずい……