pandasのDataFrameで整数型に欠損値を追加したくて〜2022年冬〜

NaNなんだ NaNの扱い NAやましい
2022.12.01

認めたくないものですが12月がやって来てしまいましたね。


▲ 私はスプラトゥーン3の新シーズンで12月を受け入れることにしました

こんにちは。データアナリティクス事業本部 インテグレーション部 機械学習チームのShirotaです。
今月も元気に(元気に?)日々学んだことをブログにしていこうと思います。
本日は、Pythonのデータ解析用のライブラリでお馴染み「pandas」の話をしていこうと思います。

データの前処理で楽しようとしたらやらかした話

「整数型(以下 int型)でNaNを使うぞ!」が今回の本題になっております。一旦心の隅に記録しておいてくれると嬉しいです。
ここに至るまでの経緯を簡単に説明していきます。

二つのデータを結合したら列のデータ型が変わってしまった

機械学習用の訓練データとテストデータの両方に、pandasで前処理を実施しようとしていました。
その際、両方のデータに同じ処理を別々にかけるのは面倒だと思い、一旦両データを結合しようと思いました。
訓練データとテストデータに存在するデータ型は以下のようになっていました。

train.csvのデータ型

id        object
source    object
text      object
label      int64
dtype: object

test.csvのデータ型

id        object
source    object
text      object
dtype: object

訓練データには、テストデータに存在しないlabelというint型の列が存在しました。
label列は、今回目的変数であるためテストデータには存在していなかったのです。
訓練データとテストデータは一時的に結合する予定なので、TrainFlagという列を追加して後でデータを元通り分離できるようにしてからこの二つのデータを結合して、データ型を確認してみました。

data-all.csvのデータ型

id            object
source        object
text          object
label        float64
TrainFlag       bool
dtype: object

label列が浮動小数点数型(以下 float型)になってしまっていました。

なぜこのようなことが起こったのか

二つのデータを結合したことによって、訓練データにしか存在しなかったlabel列が自動補完されました。
その際、テストデータはlabel列の値を持っていなかったため、欠損値としてNaNが補完されました。

NaN とは なのかと言いますと、NaNは 非数(数ではない値) を表現するものであり、主にfloat型の演算で用いられる値です。
Pythonにおいても、NaNはfloat型にのみ存在する概念となっています。

Wikipediaより、NaNの説明を引用しておきます。

NaN(Not a Number、非数、ナン)は、コンピュータにおいて、主に浮動小数点演算の結果として、不正なオペランドを与えられたために生じた結果を表す値またはシンボルである。

欠損値をどう扱うか

では、欠損値はどう扱えば良いのでしょうか。

例えば、fillnaメソッドを用いて欠損値を何らかの定数(平均値や中央値・最大値や最小値・その他固有値)で埋めるという手段があります。
しかし前述した通り、今回欠損値となっているところはテストデータの目的変数の値であるためこの手法は取らないこととします。

今回の場合、int型を保持したまま欠損値を扱いたいです。
厳密には違いますが、「 int型でNaNを使いたい 」という表現がしっくりくるかもしれません。

本題に辿り着いたので、ここから実際にint型でNaNを使いたいという夢を叶えていきましょう。

pandasを使ってint型で欠損値を扱っていく

PythonではNaNはfloat型でしか扱えません。どうしましょうか。

実は、pandas 1.0から NA というさまざまなデータ型で使える欠損値が利用できるようになりました。
以下、pandasの公式ドキュメントとDevelopersIOのブログを参考資料として紹介させて頂きます。

上記資料にも記載がありますが、現状、NA実験的に採用されている(pandas 1.5.2 時点) ようなので、今後予告なしに変更が入る可能性はあります。この点は注意して下さい。
今回私はGoogle Colaboratory(以下 Colab)上で以下作業を実施しており、Colab上で最新のバージョンであるpandas 1.3.5(2022年12月1日現在)を利用しました。

では早速、テストデータに空列のlabel列を追加していきます。

data_test["label"] = pd.NA

上記を実行後のデータセットは以下のようになりました。

id source text label
0 0238492 hoge 今日ずっと隣で猫が丸まって寝ていてかわいい <NA>
1 03942829 hogefuga ヒラメのショクワンも夢があるな <NA>
2 29342930 fuga ノヴァブラスターアドベントカレンダー開催します <NA>
3 375938 fuga 猫の寝息かと思ったら給水器の音だった <NA>
4 13858035 hoge よく見たら黒筐体だった <NA>

欠損値NAが入ったことが確認できました。
ここで、データ型を見てみます。

id        object
source    object
text      object
label     object
dtype: object

object型になってしまいました。
なので、データ型をint型に変更します。

data_test["label"] = data_test["label"].astype("Int64")

この際、 Int64 と Iを大文字にしないといけない ところに注意してください。
int64と指定すると、NAが利用できないデータ型として以下のようなエラーが出ます。

ValueError: cannot convert to 'int64'-dtype NumPy array with missing values. Specify an appropriate 'na_value' for this dtype.

上記を実行してからデータ型を確認すると、以下のようになります。

id        object
source    object
text      object
label      Int64
dtype: object

これで、int型でも無事欠損値が利用できました!

オマケ:int64とInt64のデータ型を結合するとどうなるか

NAを使えないint64とNAを使えるInt64、この二つのデータ型を結合するとどうなるでしょうか。

少しの間、考えてみてください。

考えましたか?

答えはこちらです。

objectになる

id        object
source    object
text      object
label     object
dtype: object

int型を維持したい場合は、int64(iが小文字)のデータ型をInt64(Iが大文字)のデータ型に変換してから二つのデータを結合するようにしましょう。

最後にもう一度だけお話ししておきますが、NA現状実験的に採用されている(pandas 1.5.2 時点) ため、本番環境での利用は避け一時的なデータ処理や検証などで利用するようにしましょう。