pandas 特定列の値をユニークな数値IDに変換する3つの方法

pandasのDataFrameやSeriesがあったときに、ある列の値に基づいて数値に変換して、ユニークな整数IDを振りたい時がある。文字列の型のカテゴリを番号に変換したいという状況だ。
1行ずつ見ていけばできることはできるのだが、もっと簡単に速くできる方法は無いのか。

以下、StackOverflowや公式ドキュメントを参考に、検証結果をまとめておく。

f:id:soratokimitonoaidani:20200418212154p:plain

問題設定と希望する出力

例として、適当なDataFrameを作成する。

import pandas as pd
df = pd.DataFrame({
    'name'    : ['Alice', 'Bob', 'Charlie', 'Charlie', 'Alice', 'Bob'],
    'item' : ['aaa', 'bbb', 'ccc', 'ddd', 'eee', 'fff'],
    'number'    : [3, 2, 4, 3, 2, 1],
})
df
name item number
0 Alice aaa 3
1 Bob bbb 2
2 Charlie ccc 4
3 Charlie ddd 3
4 Alice eee 2
5 Bob fff 1

このデータは、通販の商品注文なのか、レストランに入って注文してるのか、どういうシチュエーションなんだろう。
まぁ良いや。あんまり考えずに作ったデータなので。
で、nameの値に応じて番号を振りたいとしよう。こんなふうに。

(注意:name_idの順序付けには条件が無いものとする。すなわち、出現順やアルファベット順でなくても良いとする。)

df['name_id'] = [0, 1, 2, 2, 0, 1]
df
name item number name_id
0 Alice aaa 3 0
1 Bob bbb 2 1
2 Charlie ccc 4 2
3 Charlie ddd 3 2
4 Alice eee 2 0
5 Bob fff 1 1

今回は手動で列の値を指定して追加したけど、もちろん実際のデータでこんなことはできない。
これを自動で実行するにはどうすればよいか。
3つの方法があることが分かったので、まとめて書いておく。

(※以下の各方法について説明する前に、最初に書いたdfを作成しているものとして読んでください。)

方法1 factorize()

1つ目の方法はfactorize()関数を使用するものだ。   factorizeという単語の意味は、英和辞典を引くと「因数分解する」と書いてある。しかし、この操作は別に多項式因数分解ではないよな。データ分析だと別の意味があるのだろうか。

This method is useful for obtaining a numeric representation of an array when all that matters is identifying distinct values.
このメソッドは,異なる値を識別することだけが重要な場合に,配列の数値表現を得るのに役立ちます. (DeepL翻訳)
https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.factorize.html

同じ値が同じ数字になり、違う値が違う数値になるように、数値に変換するよ、という話なので、今回の目的にピッタリあう。

df = pd.DataFrame({
    'name'    : ['Alice', 'Bob', 'Charlie', 'Charlie', 'Alice', 'Bob'],
    'item' : ['aaa', 'bbb', 'ccc', 'ddd', 'eee', 'fff'],
    'number'    : [3, 2, 4, 3, 2, 1],
})
df['name_id'] = df['name'].factorize()

→エラー(内容はクリックすると展開されます)

    ---------------------------------------------------------------------------

    ValueError                                Traceback (most recent call last)

    <ipython-input-27-52d75a2b1282> in <module>
    ----> 1 df['name_id'] = df['name'].factorize()
    

    /usr/local/lib/python3.7/site-packages/pandas/core/frame.py in __setitem__(self, key, value)
       2936         else:
       2937             # set column
    -> 2938             self._set_item(key, value)
       2939 
       2940     def _setitem_slice(self, key, value):


    /usr/local/lib/python3.7/site-packages/pandas/core/frame.py in _set_item(self, key, value)
       2998 
       2999         self._ensure_valid_index(value)
    -> 3000         value = self._sanitize_column(key, value)
       3001         NDFrame._set_item(self, key, value)
       3002 


    /usr/local/lib/python3.7/site-packages/pandas/core/frame.py in _sanitize_column(self, key, value, broadcast)
       3634 
       3635             # turn me into an ndarray
    -> 3636             value = sanitize_index(value, self.index, copy=False)
       3637             if not isinstance(value, (np.ndarray, Index)):
       3638                 if isinstance(value, list) and len(value) > 0:


    /usr/local/lib/python3.7/site-packages/pandas/core/internals/construction.py in sanitize_index(data, index, copy)
        609 
        610     if len(data) != len(index):
    --> 611         raise ValueError("Length of values does not match length of index")
        612 
        613     if isinstance(data, ABCIndexClass) and not copy:


    ValueError: Length of values does not match length of index

あれ。ValueErrorになってしまった。何でだろう。

df['name'].factorize()
(array([0, 1, 2, 2, 0, 1]), Index(['Alice', 'Bob', 'Charlie'], dtype='object'))
type(df['name'].factorize())
tuple
df['name'].factorize()[0]
array([0, 1, 2, 2, 0, 1])

上記の通り、factorize関数はSeries自体ではなくtupleを返してくる。
tupleの1番目は「Seriesのどの位置に、どの要素が入っているか」を示す数値のndarrayが返る。
2番めは「使われている要素一覧」を示すIndexが返る。
(公式ドキュメントに書いてあるとおりだが。)
なるほど、Seriesを上記の2つに「分解して」返してくるので、factorizeという関数名なのであろう。

というわけで、今回ほしいのはtupleの1番目なので、factorize()したあとに[0]を指定すればよい。正しいコードは以下のようになる。

df['name_id'] = df['name'].factorize()[0]
df
name item number name_id
0 Alice aaa 3 0
1 Bob bbb 2 1
2 Charlie ccc 4 2
3 Charlie ddd 3 2
4 Alice eee 2 0
5 Bob fff 1 1

方法2 df.groupby(['column_name']).ngroup()

2番めの方法はgroupby関数を使うものだ。

df['name_id'] = df.groupby(['name']).ngroup()
df
name item number name_id
0 Alice aaa 3 0
1 Bob bbb 2 1
2 Charlie ccc 4 2
3 Charlie ddd 3 2
4 Alice eee 2 0
5 Bob fff 1 1

pandas.core.groupby.GroupBy.ngroupのドキュメントはこちら。(関数の正式名称、長いな!)

pandas.core.groupby.GroupBy.ngroup — pandas 1.0.3 documentation

groupの番号を返す関数である。 groupbyを使って特定列の値でグループ化して、その番号を取ることで、ユニークなIDを付与している。わかりやすい。

方法3 Series.astype('category').cat.codes

3番目の方法は、astypeを使ってcategory型に変換して、その番号を取得するものだ。

df['name_id'] = df['name'].astype('category').cat.codes
df
name item number name_id
0 Alice aaa 3 0
1 Bob bbb 2 1
2 Charlie ccc 4 2
3 Charlie ddd 3 2
4 Alice eee 2 0
5 Bob fff 1 1
df['name'].astype('category').cat.codes
0    0
1    1
2    2
3    2
4    0
5    1
dtype: int8

確かにできているけど、何でこの方法で実現できるのか分からなかった。ので、調べた。

df['name']
0      Alice
1        Bob
2    Charlie
3    Charlie
4      Alice
5        Bob
Name: name, dtype: object
df['name'].astype('category')
0      Alice
1        Bob
2    Charlie
3    Charlie
4      Alice
5        Bob
Name: name, dtype: category
Categories (3, object): [Alice, Bob, Charlie]

astype()を使ってdtypeを変換しているので、dtypeがobjectからcategoryに変わっている。 dtypeについては以下も参照。

linus-mk.hatenablog.com

categoryとはどういうdtypeなのか? を調べようとしたけど、 公式ドキュメントを見ても長い説明が書いてあったので諦めた。
Categorical data — pandas 1.0.3 documentation

カテゴリ型のSeriesはcatというアクセサを通じて色々な情報を取得できる、らしい。すなわち、Series.cat.xxxx という属性で、そのカテゴリ型の情報を取得できる。
Pythonデータ分析/機械学習のための基本コーディング! pandasライブラリ活用入門 p.159)
pandas公式ドキュメントだと多分ここかな。
https://pandas.pydata.org/pandas-docs/stable/reference/series.html#api-series-cat

その中で、カテゴリのIDを取得するには、Series.cat.codesとすれば良いようだ。

関連質問

関連質問:

python - Pandas: convert categories to numbers - Stack Overflow
一番閲覧数が多いのはこれみたいだ。1つの列を基準にカテゴリーを数値に変換する。(ただし1始まりで番号をふろうとしていることだけ注意)

python - Q: [Pandas] How to efficiently assign unique ID to individuals with multiple entries based on name in very large df - Stack Overflow
FirstName列とLastName列という2つの列を連結した値に対して、ユニークな数値IDを付与したいという質問。

この2つを見れば用は足りるだろうと思うが、同様の質問がまだあったので載せておく。

python - Factorize a column of strings in pandas - Stack Overflow

python - Convert pandas series from string to unique int ids - Stack Overflow

それでは。