pandasのDataFrameやSeriesがあったときに、ある列の値に基づいて数値に変換して、ユニークな整数IDを振りたい時がある。文字列の型のカテゴリを番号に変換したいという状況だ。
1行ずつ見ていけばできることはできるのだが、もっと簡単に速くできる方法は無いのか。
以下、StackOverflowや公式ドキュメントを参考に、検証結果をまとめておく。
- 問題設定と希望する出力
- 方法1 factorize()
- 方法2 df.groupby(['column_name']).ngroup()
- 方法3 Series.astype('category').cat.codes
- 関連質問
問題設定と希望する出力
例として、適当な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については以下も参照。
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
それでは。