Python
機械学習
MachineLearning
pandas

PandasのCategorical関係を調べてみた~慣れれば便利(と思う)

More than 1 year has passed since last update.

データ分析を支援する"pandas"だが,先日(2016/10/2) ver.0.19.0(stable) がリリースされた.いくつかの新機能の中に,Categorical データを走査(パース)するread_csv()のオプションがあるとのこと.Categorical データといえば,R言語のfactor型が頭に浮かぶが,これまで私自身,pandasのCategorical関係の機能を使うことはなかった.気になったので,ver. 0.19.0 を機に,少し調べてみた.

(動作環境は,pandas 0.19.0 (比較のため,一部 0.18.1使用),numpy 1.11.1, pythonは,3.5.2 になります.)

Pandasの 'Categorical' サポート状況とR言語との比較

R言語では,factor型(因子型)という変数型がサポートされている.これは汎用のプログラミング言語にはない型なので,なかなかなじみにくいが,カテゴリー型のデータを扱うために用意されている.但し,Rプログラマでも好き嫌いがあるようで,csvファイルからdata.frameに入力する際,stingAsFactor=FALSE を指定してデータをfactor型に変換するのを抑止する方も結構いるように見受けられる.(注.read.csv(), {data.table} fread()で(ユーザーのディフォルト変更がなければ)ディフォルトは stringAsFactor=TURE)

Pythonではもちろん言語仕様にfactor型はないが,pandasで(やはりRのfactor型に慣れたプログラマの機能リクエストなのか)ver 0.15.0 からCategoricalの型(dtype)がサポートされてきているようだ.(知らなかった...)

今回,ver 0.19.0では,read_csv()でCategoryをパースしたり,データの連結等でCategoryを考慮した操作ができるような機能拡張が行われている.

Documentationより引用
pandas_cat_1.PNG

(PandasのCategory型は,R言語のfactor型を意識した実装であるが,詳細部については,両者違いがあるようです.これについて私は分解能を持っていませんので,気になる方はpandas(ver 0.19.0)のドキュメントを参照ください.)

データセット"Mushroom"を用いた動作確認

さて,以下,動作を確認していく.データセットとしてUCI機械学習リポジトリから"Mushroom"を用意して使用した."Mushroom"は,内容としては,きのこの姿形などの特徴量から,きのこが毒きのこかそうでないかを分類するというものであるが,中身(ヘッダー部)は次のようになっている.

p,x,s,n,t,p,f,c,n,k,e,e,s,s,w,w,p,w,o,p,k,s,u
e,x,s,y,t,a,f,c,b,k,e,c,s,s,w,w,p,w,o,p,n,n,g
e,b,s,w,t,l,f,c,b,n,e,c,s,s,w,w,p,w,o,p,n,n,m
p,x,y,w,t,p,f,c,n,n,e,e,s,s,w,w,p,w,o,p,k,s,u
e,x,s,g,f,n,f,w,b,k,t,e,s,s,w,w,p,w,o,e,n,a,g
e,x,y,y,t,a,f,c,b,n,e,c,s,s,w,w,p,w,o,p,k,n,g
e,b,s,w,t,a,f,c,b,g,e,c,s,s,w,w,p,w,o,p,k,n,m
e,b,y,w,t,l,f,c,b,n,e,c,s,s,w,w,p,w,o,p,n,s,m
p,x,y,w,t,p,f,c,n,p,e,e,s,s,w,w,p,w,o,p,k,v,g
e,b,s,y,t,a,f,c,b,g,e,c,s,s,w,w,p,w,o,p,k,s,m

すべてアルファベットの文字となっているデータで,今回の記事にぴったりである.第1カラムは,"e" .. "edible" 「食用になる」,"p" .."poisonous"「毒の」を示すラベルである.

これを普通にファイル入力すると以下のようになる.

fn = '../Data/Mushroom/agaricus-lepiota.data'
# names for all columns
cols = ['label', 'cap-shape', 'cap-surface', 'cap-color', 'bruises', 'odor',
    'gill-attachment', 'gill-spacing', 'gill-size', 'gill-color', 'stalk-shape',
    'stalk-root', 'stalk-surface-above-ring', 'stalk-surface-below-ring',
    'stalk-color-above-ring', 'atalk-color-below-ring', 'veil-type', 
    'veil-color', 'ring-number', 'ring-type', 'spore-print-color', 
    'population', 'habitat']
# names for subset
col_subset = ['label', 'cap-shape', 'cap-surface', 'cap-color', 'bruises']

mr1 = pd.read_csv(fn, header=None, names=cols, usecols=col_subset)

In [1]: mr1.head()
Out[1]:
  label cap-shape cap-surface cap-color bruises
0     p         x           s         n       t
1     e         x           s         y       t
2     e         b           s         w       t
3     p         x           y         w       t
4     e         x           s         g       f

このときには,データ(p, x, s, n, t ...) は文字列型として扱われている.一方,pandas 0.19.0 では,以下のように入力することができる.(dtype='category' 追加)

mr2 = pd.read_csv(fn, header=None, names=cols, usecols=col_subset, dtype='category')

データのヘッダ部分をみても型はわからないので,dtypeを確認する.

In [4]: mr1.dtypes
Out[4]:
label          object
cap-shape      object
cap-surface    object
cap-color      object
bruises        object
dtype: object

In [5]: mr2.dtypes
Out[5]:
label          category
cap-shape      category
cap-surface    category
cap-color      category
bruises        category
dtype: object

mr1の個々のデータはstr型だが,pd.Series()のデータ型としては抽象オフジェクト型のobjectとなっている.一方,dtype='category' オプションをつけて入力したmr2の方は,きちんとcategoryに変換されているのが確認できる.

因みに,前バージョン(pandas 0.18.1)でもファイル入力後に,astypeで型変換を実施すれば,mr2と同じデータを得ることができる.

mr11 = mr1.apply(lambda x: x.astype('category'))

In [9]: mr11.dtypes
Out[9]:
label          category
cap-shape      category
cap-surface    category
cap-color      category
bruises        category
dtype: object

Category型のデータに対しては,cat(categoryの略)アクセサを通して,いくつかの関数(method)がサポートされている.
例えば,カテゴリーの種類については,以下で得られる.

In : mr2['cap-shape'].cat.categories
Out: Index(['b', 'c', 'f', 'k', 's', 'x'], dtype='object')

In : mr2['cap-color'].cat.categories
Out: Index(['b', 'c', 'e', 'g', 'n', 'p', 'r', 'u', 'w', 'y'], dtype='object')

上記の操作では,得られるカテゴリー種類(リスト)の並び,順番は特に決まっていない.同様の操作にpd.Seriesオブジェクトに対するunique()がある.

In : mr2['cap-shape'].unique()
Out:
[x, b, s, f, k, c]
Categories (6, object): [x, b, s, f, k, c]

ここで得られた結果セットは,上記の xx.cat.categories と同じであるが,この結果の順番はデータセット(pd.Seriesオブジェクト)を走査していく過程での登場順にならんでいるとのこと.(登場順序に特別な意味がある,というケースはあまりないと思いますが.)

Categoryの並び順序については,別のデータで確認してみたい.(後述)

ところでデータ分析の後過程で機械学習を行う場合,データセットは,数値型(int型,float型)に変換する必要がある.pandasのCategory型は以下の通り codes 関数(method)でint型に変換できる.

In : mr2_numeric = mr2['cap-shape'].cat.codes

In : mr2_numeric[:10]
Out:
0    5
1    5
2    0
3    5
4    5
5    5
6    0
7    0
8    5
9    0
dtype: int8

カテゴリーに含まれない異常値に対する戻り値は,以下のように -1 となる.

In : mr2.loc[3, 'cap-shape'] = np.nan

In : mr2.loc[6, 'cap-shape'] = np.nan

In : mr2['cap-shape'].cat.codes[:10]
Out:                                
0    5                                  
1    5                                  
2    0                                  
3   -1                                  
4    5                                  
5    5                                  
6   -1                                  
7    0                                  
8    5                                  
9    0                                  
dtype: int8                             

"Ordered" Categoryについての調査

pandas Category型には「順序あり」のオプションがある.ここでは元素記号の例で確認する.まずデータ(pd.Series)を用意する.

# データサンプルを作成するための関数
def mk_rand_elements():
    elem_dict = {1: 'H', 2: 'He', 3: 'Li', 4: 'Be', 5: 'B', 6: 'C', 7: 'N'}
    sz = 10
    r = np.random.randint(1, 7, size=sz)
    rand_el = [elem_dict[i] for i in r]

    return rand_el

elem_series = pd.Series(mk_rand_elements())

次に設定したい正しい順序を変数で用意する.(元素記号ですが,窒素くらいまでなら記憶は確かです...)

elem_ord = ['H', 'He', 'Li', 'Be', 'B', 'C', 'N']

上記のように,データシリーズと正しい順序のデータからカテゴリー型変数を作成する.

# convert to categorical and encoding 
elem_cat = elem_series.astype('category', categories=elem_ord, ordered=True)

# check
In : elem_cat
Out:
0     B
1     H
2    Li
3    Be
4    He
5    Li
6     B
7    Li
8    He
9     C
dtype: category
Categories (7, object): [H < He < Li < Be < B < C < N]

注目したいのは,一番下の行 Categories (7, object): [H < He < Li < Be < B < C < N] である.
不等号記号で表示された部分が,変数 'elem_cat' が順序ありのカテゴリー型(ordered category)のdtypeであることを示している.

これを数値型にエンコードした場合,きちんとカテゴリーの順番で数値が付与される.

# Encoding to numeric data
encoded = elem_cat.cat.codes

# pythonでは配列が index=0 からスタートするので,全体をオフセット
encoded = encoded + 1

# Encode前後をまとめて表示
result = pd.DataFrame(columns=['elem', 'num'])
result['elem'] = elem_series
result['num'] = encoded

In : result
Out:
  elem  num
0    B    5
1    H    1
2   Li    3
3   Be    4
4   He    2
5   Li    3
6    B    5
7   Li    3
8   He    2
9    C    6

上の通り,元素記号のデータ列がきちんと原子番号にエンコードされていることが分かる.このように外部からきちんと順番を設定し,ordered オプションを Trueにセットすることにより,カテゴリーの順番を保持することができる.データセット"Mushroom"でのきのこの形状特徴のようなデータに順番を意識したいものはないが,例えば学生の成績に['A', 'B', 'C', 'D']とつく場合や,投資会社がよく使う格付け['AAA', 'AA', 'A', 'BBB', 'BB', 'B']には,その順番自体に情報を持っている.このようなケースで 'ordered category' を使いたくなる場合があると思われる.

まとめ

機械学習の全体プロセスでは,文字列等から成るデータセットをファイルから読み込み,所定の処理を経て,それをモデル(分類モデル,回帰モデル)に入力する.モデルが扱えるのは数値データなので,文字列から数値に直接変換してしまえば,「カテゴリー型」にして処理する必要はない.

文字列から数値の変換には,自前の関数を用意してそれを適用したり,あるいはscikit-learn(preprocessing)の関数群が使えると思われる.しかし,今回調べたpandas "Categorical" 関係の機能も,pandas内部で処理できることから,jupyter notebookで使いたい,図形プロットしてながめてみたい,等,いろいろなところで使われると予想される.すなわち,知っていて「損はしない」便利な機能と考えられる.

(Feature requestでバージョンupに合わせて入る機能なので,一定の需要があるということでしょう.)

(追記) date.11/21/2016

PandasのCategorical型変換をサポートする関数に pd.factorize() というのもあるようです.

>>> myseq = ['a', 'b', 'c', 'a', 'b']
>>> encoded = pd.factorize(myseq)
>>> encoded
(array([0, 1, 2, 0, 1]), array(['a', 'b', 'c'], dtype=object))

戻り値としては,変換された数値型のデータ(indexer)と,元データの unique からなるtupleとなります.http://pandas.pydata.org/pandas-docs/stable/generated/pandas.factorize.html
(Pandas ドキュメント)

参考文献 / web site