54
53

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-10-14

データ分析を支援する"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

54
53
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
54
53