データ分析を支援する"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を考慮した操作ができるような機能拡張が行われている.
(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
- Categorical Data - Pandas documentation
http://pandas.pydata.org/pandas-docs/stable/categorical.html - Convert categorical data in pandas dataframe - stack overflow
http://stackoverflow.com/questions/32011359/convert-categorical-data-in-pandas-dataframe - R: A Language and Environment for Statistical Computing - CRAN
- UCI Machine Learning Repository: Mushroom Data Set
https://archive.ics.uci.edu/ml/datasets/Mushroom