CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: pandas で DataFrame を連結したら dtype が int から float になって驚いた話

今回は pandas を使っているときに二つの DataFrame を pd.concat() で連結したところ int のカラムが float になって驚いた、という話。 先に結論から書いてしまうと、これは片方の DataFrame に存在しないカラムがあったとき、それが全て NaN 扱いになることで発生する。 NaN は浮動小数点数型にしか存在しない概念なので、それが元で整数型と浮動小数点数型の演算になりキャストされてしまった。

使った環境は次の通り。

$ sw_vers  
ProductName:    Mac OS X
ProductVersion: 10.13.4
BuildVersion:   17E202
$ python -V
Python 3.6.5

下準備

まずは下準備として pandas と numpy をインストールしておく。

$ pip install pandas numpy

インストールできたら Python の REPL を起動する。

$ python

これで下準備はおわり。

問題の起こらないパターン

それでは早速 pandas.concat() を使って二つの DataFrame を連結してみよう。 最初は問題の起こらないパターンについて概要を説明する。

まずは pandas をインポートしておく。

>>> import pandas as pd

続いて、次のように同じカラムを持った DataFrame を二つ用意する。 カラム名は両者で順番が異なっている。 最初の DataFrame はユーザ名、年齢の順になっているが、二つ目は年齢、ユーザ名の順になっている。

>>> users_df1 = pd.DataFrame([
...   ['alice', 10],
...   ['bob', 20],
...   ['carol', 30]
... ], columns=['name', 'age'])
>>> users_df2 = pd.DataFrame([
...   [40, 'daniel'],
...   [50, 'emily'],
...   [60, 'frank'],
... ], columns=['age', 'name'])

それぞれの DataFrame に含まれる年齢 (age) の dtype は int64 になっている。

>>> users_df1.age.dtype
dtype('int64')
>>> users_df2.age.dtype
dtype('int64')

それでは pandas.concat() 関数を使って上記二つの DataFrame を縦に連結してみよう。 この関数は、カラム名さえ一致すれば順番が異なっていても上手いこと連結してくれることが分かる。

>>> concat_df = pd.concat([users_df1, users_df2])
>>> concat_df
   age    name
0   10   alice
1   20     bob
2   30   carol
0   40  daniel
1   50   emily
2   60   frank

カラムの dtype も、ちゃんと int64 のままだ。

>>> concat_df.age.dtype
dtype('int64')

問題の起こるパターン

では、続いて問題の起こるパターンについて見てみよう。

連結に使う DataFrame は次の通り。 先ほどとほとんど同じだが users_df2 に関しては user_id というカラムが追加されている。 そしてポイントは user_id というカラムが users_df1 には存在しないことだ。

>>> users_df1 = pd.DataFrame([
...   ['alice', 10],
...   ['bob', 20],
...   ['carol', 30]
... ], columns=['name', 'age'])
>>> users_df2 = pd.DataFrame([
...   [10, 40, 'daniel'],
...   [20, 50, 'emily'],
...   [30, 60, 'frank'],
... ], columns=['user_id', 'age', 'name'])

追加された users_df2user_id カラムの dtype は int64 になっている。

>>> users_df2.user_id.dtype
dtype('int64')

それでは二つの DataFrame を結合してみよう。

>>> concat_df = pd.concat([users_df1, users_df2])

結合された結果を確認してみよう。 すると user_id カラムについては一部に NaN が入っており、入っていないものについては小数点以下が表示されている。

>>> concat_df
   age    name  user_id
0   10   alice      NaN
1   20     bob      NaN
2   30   carol      NaN
0   40  daniel     10.0
1   50   emily     20.0
2   60   frank     30.0

おや、と思いながら dtype を確認すると、結合前の int64 が結合後には float64 に変換されている。

>>> concat_df.user_id.dtype
dtype('float64')

何が起こったのか

NumPy をインポートして numpy.nan を参照すると、たしかに型は float になっている。

>>> import numpy as np
>>> type(np.nan)
<class 'float'>

NaN を整数型にキャストしようとしても、上手くいかない。 そもそも、整数型には NaN という概念が存在しないため。

>>> np.int64(np.nan)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: cannot convert float NaN to integer

で、よくよく考えるとそもそも整数型と浮動小数点型の演算結果は浮動小数点型に合わせて出てくるものだった。

>>> 1 + 1.0
2.0
>>> type(1 + 1.0)
<class 'float'>

つまり、最初にやっていたことをよりシンプルな形で表すと、次のようになる。 まず、numpy.int64numpy.float64pandas.Series を用意する。

>>> s1 = pd.Series([0, 1, 2], dtype=np.int64)
>>> s2 = pd.Series([np.nan, np.nan, np.nan], dtype=np.float64)

そして、それらを結合する。 結果は浮動小数点型に合わせられる。

>>> pd.concat([s1, s2])
0    0.0
1    1.0
2    2.0
0    NaN
1    NaN
2    NaN
dtype: float64

なるほど、これなら違和感なし。

じゃあ、どうすれば良いのか?

これを解決するためのアプローチとしては次の二つの方法になると思う。 - あらかじめ存在しないカラムを追加しておく - 結合した上で値を置換、キャストする

最初の方法は自明として、二つ目に関して補足する。 まず、さきほどの DataFrame があったとする。

>>> concat_df
   age    name  user_id
0   10   alice      NaN
1   20     bob      NaN
2   30   carol      NaN
0   40  daniel     10.0
1   50   emily     20.0
2   60   frank     30.0

浮動小数点になっちゃったカラムについて NaN になっているところを Series#fillna() を使って適当な値に置換する。 DataFrame.fillna() するとカラムに関係なく埋められてしまう点に注意する。

>>> concat_df.user_id.fillna(-1, inplace=True)
>>> concat_df
   age    name  user_id
0   10   alice     -1.0
1   20     bob     -1.0
2   30   carol     -1.0
0   40  daniel     10.0
1   50   emily     20.0
2   60   frank     30.0

あとは Series#astype() で元々の型にキャストする。

>>> df.assign(user_id = df.user_id.astype(np.int64))
   age    name  user_id
0   10   alice       -1
1   20     bob       -1
2   30   carol       -1
0   40  daniel       10
1   50   emily       20
2   60   frank       30

いじょう。

めでたしめでたし。

Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理

Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理