featuretoolsの使い方メモ

# featuretoolsとは

機械学習を行う際に対象ドメインについての知識がある場合は適切な特徴量を考えて特徴量として与えることで精度向上を図ることができますが、ドメイン知識がない場合も足し合わせたりaggregateしたりして偶然当たりの特徴量を見出すことを期待するという作戦を取ることができます。あり得る組み合わせを片っ端から試すという総当たりアプローチであるので、総当り特徴量エンジニアリング(brute force feature engineering)と呼んだりするようです。

featuretoolsは手作業でやってると面倒な特徴量づくりを半自動化しちゃおうというpythonライブラリです。とても便利です。

featuretools公式チュートリアル
https://docs.featuretools.com/en/stable/

本記事ではこちらのブログのコードに沿ってやっていきます
https://blog.amedama.jp/entry/featuretools-brute-force-feature-engineering














# 1. install

pipで入ります

```terminal:terminal
pip install featuretools
```

addonを追加でインストールすることで別ライブラリと連携することもできます
https://docs.featuretools.com/en/stable/install.html

# 2. Deep Feature Synthesis

複数のDataFrameが与えられたとき、aggregateしたり統計量を算出したり特徴量間の四則演算をやって特徴量を作成しますが、これらの作業をいい塩梅にやってくれるのがDeep Feature Synthesisであり、これをやってくれる関数がfeaturetools.dfs()です。
https://docs.featuretools.com/en/stable/getting_started/afe.html

このいい塩梅の自動化を実現するためにpandas.DataFrameよりも詳細な細かいデータ型の指定が必要になります。例えば時間を表現するデータ型だけでもDatetime、DateOfBirth、DatetimeTimeIndex、NumericTimeIndexなどがあり、不適切な組み合わせが生じにくいようにしてくれます。
https://docs.featuretools.com/en/stable/getting_started/variables.html






# 3. entity1つの場合

featuretoolsは入力されたデータをentityと呼びます。pandas.DataFrameでデータをもってくる事が多いかと思いますが、その場合1つのpandas.DataFrameが1つのentityという事になります。

## 3-1. trans_primitivesのみ

trans_primitivesは特徴量間の計算を行います

#### 使うDataFrameをつくる

```python:python
import pandas as pd
data = {'name': ['a', 'b', 'c'],
        'x': [1, 2, 3],
        'y': [2, 4, 6],
        'z': [3, 6, 9],}
df = pd.DataFrame(data)
df
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/eb2535c5-fece-296b-43e5-80cad9e52992.png)

#### EntitySetをつくる

まず空のfeaturetools.EntitySetを作ります。EntitySetはentity同士の関係や処理する内容を定義しておく為のオブジェクトですが以下ではまだidしか書かれていません。idについては省略可能ですが、以下ではid='example'としています。

```python:python
import featuretools as ft
es = ft.EntitySet(id='example')
es
```

#### EntitySetにentityを追加

以下では先程作ったdfを登録して'locations'という名前で呼べるようにしています。index=はそのままindexを指定する為の引数で、省略するとDataFrameの最初の列をindexとして扱うようになっています。

```python:python
es = es.entity_from_dataframe(entity_id='locations',
                              dataframe=df,
                              index='name')
es
```

```terminal:output
Entityset: example
  Entities:
    locations [Rows: 3, Columns: 4]
  Relationships:
    No relationships
```

EntitySetに登録されているentityは以下のように呼び出すことができます

```python:python
es['locations']
```

```terminal:output
Entity: locations
  Variables:
    name (dtype: index)
    x (dtype: numeric)
    y (dtype: numeric)
    z (dtype: numeric)
  Shape:
    (Rows: 3, Columns: 4)
```


```python:python
es['locations'].df
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/faa2638d-f3ac-6233-2618-f8f321930678.png)

#### dfsを実行する

EntitySetができたので、後はft.dfs()に渡すだけで特徴量を作ってくれます。target_entityはメインとするentity、trans_primitivesが特徴量間の組み合わせに適用する計算方法、agg_primitivesがaggregateに使う計算方法です。

使用可能なprimitiveは以下にまとまっています
<a href="https://primitives.featurelabs.com/?__hstc=166560673.91938e9b06317968b92bf816be38bca2.1602997423211.1604225841773.1604228457443.16&__hssc=166560673.2.1604228457443&__hsfp=4146570320
">https://primitives.featurelabs.com/</a>

以下ではadd_numericで特徴量間の和を、subtract_numericで特徴量間の差を追加するように指示しています

```python:python
feature_matrix, feature_defs = ft.dfs(entityset=es,
                                      target_entity='locations',
                                      trans_primitives=['add_numeric', 'subtract_numeric'],
                                      agg_primitives=[],
                                      max_depth=1)
feature_matrix
```

元々あったのがx, y, zで、それらの和であるx+y, x+z, y+zと差のx-y, x-z, y-zが追加されています
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/18d16a3e-106a-a6a2-a83e-8c86a49e8d8c.png)























## 3-2. aggregateのみ

agg_primitivesにはaggregateをつくる計算方法を指定します

#### 使うDataFrameをつくる

```python:python
data = {'item_id': [1, 2, 3, 4, 5],
        'name': ['apple', 'broccoli', 'cabbage', 'dorian', 'eggplant'],
        'category': ['fruit', 'vegetable', 'vegetable', 'fruit', 'vegetable'],
        'price': [100, 200, 300, 4000, 500]}
item_df = pd.DataFrame(data)
item_df
```

aggregateに使うカテゴリー変数を2つもつDataFrameができました
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/67a4fe55-21a0-80c4-e35b-4e7180199150.png)

#### EntitySetをつくる

entityの追加までは先程と同じです

```python:python
es = ft.EntitySet(id='example')
es = es.entity_from_dataframe(entity_id='items',
                              dataframe=item_df,
                              index='item_id')
es
```

ここにaggregateに使う為のrelationshipを追加します。

以下ではitemsというentityをベースに、新たにcategoryというentityを作り、そのときのindexをcategoryとせよという指示になっています。

```python:python
es = es.normalize_entity(base_entity_id='items',
                         new_entity_id='category',
                         index='category')
es
```

```terminal:output
Entityset: example
  Entities:
    items [Rows: 5, Columns: 4]
    category [Rows: 2, Columns: 1]
  Relationships:
    items.category -> category.category
```

これでどうなっているかというと、まずitemsは入れたそのままになっています。

```terminal:output
es['items'].df
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/609390e4-1323-ee6e-67c6-934b357a8b7d.png)

一方、categoryというentityは新しく指定したcategory列でindexされているので以下のようになっています。

```python:python
es['category'].df
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/2ea2eaec-7993-741a-1dea-3824641f3a4d.png)

#### dfsを実行する

agg_primitivesにcount, sum, meanを指定して実行してみます

```python:python
feature_matrix, feature_defs = ft.dfs(entityset=es,
                                      target_entity='items',
                                      trans_primitives=[],
                                      agg_primitives=['count', 'sum', 'mean'],
                                      max_depth=2)
feature_matrix
```

aggregateできる列がitems.priceしかないのでCOUNT(), category.MEAN(), category.SUM()がそれぞれ算出されて、3列追加されたDataFrameができました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/28de728c-826e-ba95-07ed-8a1a1ab2fc26.png)







































# 4. entity2つの場合

#### DataFrameをつくる

```python:python
data = {'item_id': [1, 2, 3],
        'name': ['apple', 'banana', 'cherry'],
        'price': [100, 200, 300]}
item_df = pd.DataFrame(data)
item_df
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/4b9fb23f-37ca-1fef-771c-f01d7cdfd809.png)


```python:python
from datetime import datetime
data = {'transaction_id': [10, 20, 30, 40],
        'time': [
            datetime(2016, 1, 2, 3, 4, 5),
            datetime(2017, 2, 3, 4, 5, 6),
            datetime(2018, 3, 4, 5, 6, 7),
            datetime(2019, 4, 5, 6, 7, 8),
        ],
        'item_id': [1, 2, 3, 1],
        'amount': [1, 2, 3, 4]}
tx_df = pd.DataFrame(data)
tx_df
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/1321d5cb-d545-86c7-fe64-9c474c892a1b.png)

#### EntitySetをつくる

先ほどと同様にentityを足していきます

```python:python
es = ft.EntitySet(id='example')
es = es.entity_from_dataframe(entity_id='items',
                              dataframe=item_df,
                              index='item_id')
es = es.entity_from_dataframe(entity_id='transactions',
                              dataframe=tx_df,
                              index='transaction_id',
                              time_index='time')
es
```

2つのentityを繋ぐrelationshipをつくります。
itemsのitem_id列とtransactionsのitem_id列でマージするわけですね。

```python:python
relationship = ft.Relationship(es['items']['item_id'], es['transactions']['item_id'])
es = es.add_relationship(relationship)
es
```

```terminal:output
Entityset: example
  Entities:
    items [Rows: 3, Columns: 3]
    transactions [Rows: 4, Columns: 4]
  Relationships:
    transactions.item_id -> items.item_id
```

#### dfsを実行する

```python:python
feature_matrix, feature_defs = ft.dfs(entityset=es,
                                      target_entity='items',
                                      trans_primitives=['add_numeric', 'subtract_numeric'],
                                      agg_primitives=['count', 'sum', 'mean'],
                                      max_depth=2)
feature_matrix
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/80d90cb4-a6c9-c77d-4ffd-ff165cb3bb6f.png)
列見出しだけ書き出すと以下のような感じ。aggregateと特徴量間の計算の両方をやってくれてますね。

```terminal:output
['name',
 'price',
 'COUNT(transactions)',
 'MEAN(transactions.amount)',
 'SUM(transactions.amount)',
 'COUNT(transactions) + MEAN(transactions.amount)',
 'COUNT(transactions) + SUM(transactions.amount)',
 'COUNT(transactions) + price',
 'MEAN(transactions.amount) + SUM(transactions.amount)',
 'MEAN(transactions.amount) + price',
 'price + SUM(transactions.amount)',
 'COUNT(transactions) - MEAN(transactions.amount)',
 'COUNT(transactions) - SUM(transactions.amount)',
 'COUNT(transactions) - price',
 'MEAN(transactions.amount) - SUM(transactions.amount)',
 'MEAN(transactions.amount) - price',
 'price - SUM(transactions.amount)']
```

#### max_depthはどう効いてくるか

上記のコードでmax_depthをだんだん増やしていくとどうなるか見てみましょう

```terminal:max_depth=1
['name',
 'price',
 'COUNT(transactions)',
 'MEAN(transactions.amount)',
 'SUM(transactions.amount)']
```

```terminal:max_depth=1→2の増加分
['COUNT(transactions) + MEAN(transactions.amount)',
 'COUNT(transactions) + SUM(transactions.amount)',
 'COUNT(transactions) + price',
 'MEAN(transactions.amount) + SUM(transactions.amount)',
 'MEAN(transactions.amount) + price',
 'price + SUM(transactions.amount)',
 'COUNT(transactions) - MEAN(transactions.amount)',
 'COUNT(transactions) - SUM(transactions.amount)',
 'COUNT(transactions) - price',
 'MEAN(transactions.amount) - SUM(transactions.amount)',
 'MEAN(transactions.amount) - price',
 'price - SUM(transactions.amount)']
```


```terminal:max_depth=2→3の増加分
['MEAN(transactions.amount + items.price)',
 'MEAN(transactions.amount - items.price)',
 'SUM(transactions.amount + items.price)',
 'SUM(transactions.amount - items.price)']
```


```terminal:max_depth=3→4の増加分
['COUNT(transactions) + MEAN(transactions.amount + items.price)',
 'COUNT(transactions) + MEAN(transactions.amount - items.price)',
 'COUNT(transactions) + SUM(transactions.amount + items.price)',
 'COUNT(transactions) + SUM(transactions.amount - items.price)',
 'MEAN(transactions.amount + items.price) + MEAN(transactions.amount - items.price)',
 'MEAN(transactions.amount + items.price) + MEAN(transactions.amount)',
 'MEAN(transactions.amount + items.price) + SUM(transactions.amount + items.price)',
 'MEAN(transactions.amount + items.price) + SUM(transactions.amount - items.price)',
 'MEAN(transactions.amount + items.price) + SUM(transactions.amount)',
 'MEAN(transactions.amount + items.price) + price',
 'MEAN(transactions.amount - items.price) + MEAN(transactions.amount)',
 'MEAN(transactions.amount - items.price) + SUM(transactions.amount + items.price)',
 'MEAN(transactions.amount - items.price) + SUM(transactions.amount - items.price)',
 'MEAN(transactions.amount - items.price) + SUM(transactions.amount)',
 'MEAN(transactions.amount - items.price) + price',
 'MEAN(transactions.amount) + SUM(transactions.amount + items.price)',
 'MEAN(transactions.amount) + SUM(transactions.amount - items.price)',
 'SUM(transactions.amount + items.price) + SUM(transactions.amount - items.price)',
 'SUM(transactions.amount + items.price) + SUM(transactions.amount)',
 'SUM(transactions.amount - items.price) + SUM(transactions.amount)',
 'price + SUM(transactions.amount + items.price)',
 'price + SUM(transactions.amount - items.price)',
 'COUNT(transactions) - MEAN(transactions.amount + items.price)',
 'COUNT(transactions) - MEAN(transactions.amount - items.price)',
 'COUNT(transactions) - SUM(transactions.amount + items.price)',
 'COUNT(transactions) - SUM(transactions.amount - items.price)',
 'MEAN(transactions.amount + items.price) - MEAN(transactions.amount - items.price)',
 'MEAN(transactions.amount + items.price) - MEAN(transactions.amount)',
 'MEAN(transactions.amount + items.price) - SUM(transactions.amount + items.price)',
 'MEAN(transactions.amount + items.price) - SUM(transactions.amount - items.price)',
 'MEAN(transactions.amount + items.price) - SUM(transactions.amount)',
 'MEAN(transactions.amount + items.price) - price',
 'MEAN(transactions.amount - items.price) - MEAN(transactions.amount)',
 'MEAN(transactions.amount - items.price) - SUM(transactions.amount + items.price)',
 'MEAN(transactions.amount - items.price) - SUM(transactions.amount - items.price)',
 'MEAN(transactions.amount - items.price) - SUM(transactions.amount)',
 'MEAN(transactions.amount - items.price) - price',
 'MEAN(transactions.amount) - SUM(transactions.amount + items.price)',
 'MEAN(transactions.amount) - SUM(transactions.amount - items.price)',
 'SUM(transactions.amount + items.price) - SUM(transactions.amount - items.price)',
 'SUM(transactions.amount + items.price) - SUM(transactions.amount)',
 'SUM(transactions.amount - items.price) - SUM(transactions.amount)',
 'price - SUM(transactions.amount + items.price)',
 'price - SUM(transactions.amount - items.price)']
```

```terminal:max_depth=4→5の増加分
[]
```

```terminal:max_depth=5→6の増加分
[]
```

agg_primitives、trans_primitives、agg_primitives、trans_primitivesと適用して終了という仕様になっているようです。

# CUSTOM primitives

自前のprimitiveを追加して計算することもできるようです
https://docs.featuretools.com/en/stable/getting_started/primitives.html#simple-custom-primitives

# まとめ

便利！！！