PandasのCategorical関係を調べてみた～慣れれば便利（と思う）

データ分析を支援する"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](https://qiita-image-store.s3.amazonaws.com/0/74152/9ee9fbcb-f3f1-da27-9dd7-8d3916cfdef9.png)

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

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

```text
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"「毒の」を示すラベルである．  
  
これを普通にファイル入力すると以下のようになる．

```py
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'` 追加）

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

```

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


```py
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と同じデータを得ることができる．

```py
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）がサポートされている．
例えば，カテゴリーの種類については，以下で得られる．

```py
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()がある．

```py
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型に変換できる．

```py
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` となる．

```py
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)を用意する．


```py
# データサンプルを作成するための関数
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())

```

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


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

```

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

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

```

```text
# 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であることを示している．

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

```py
# 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

```

```text
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()` というのもあるようです．

```py
>>> 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