Python
pandas
特徴量
前処理
feature-engineering

DataFrameで特徴量作るのめんどくさ過ぎる。。featuretoolsを使って自動生成したろ

前にSQLで言う所のcase when x then y else z end的な処理をpandasでやる時にすぐやり方を忘れるから記事にした。あれはあれでいいのだけれど、まだまだ前処理にすごく時間がかかる!!めっちゃめんどい:anger:

なんとかしたい...

今までpandas.DataFrameで色々特徴量生成(feature creationとかfeature engineering)する時に、ごちゃごちゃpandasのネイティブな機能を使って生成してたけど、kagglerのエレガントなデータの前処理を見ていると下記モジュールを使っている人が多い印象。

特に大量に特徴量を生成したい場合、featuretoolsがすごく便利そうな予感!!!

よっしゃ!! 使ってみよ!!!

Featuretoolsで出来ること

具体的にFeaturetoolsを使う前にちょっとだけこのモジュールで出来ることをみておく

公式リファレンスによれば

Featuretools is a framework to perform automated feature engineering. It excels at transforming transactional and relational datasets into feature matrices for machine learning.

  • 自動で特徴量を生成するフレームワークを提供!!
  • RDBや表形式(tabular data)のデータの変換に使える!!

ウリは上記のようなところらしい。
pandas.DataFrameで保持している表形式のデータをEntityオブジェクトとして扱い、Entity間の関係をRelationオブジェクトとして扱うことでER図っぽいイメージでデータを扱えるところがいい感じ。

実際に使ってみる

ま、とりあえず使ってみればええところも悪いところもわかるやろ、ということで手を動かす。

デモ用のデータが用意されているみたいなのでそれをそのまま使うことにする。
productsという名称のDataFrameがあるけど、今回はスルー。

まずはデータの中身を確認

# import module
import featuretools as ft
import pandas as pd

# load demo data
# demo data include {customers,sessions,transactions,products} DataFrame
data = ft.demo.load_mock_customer()

data['customers'].head()
data['sessions'].head()
data['transactions'].head()

customers

index customer_id zip_code join_date
0 1 60091 2008-01-01
1 2 02139 2008-02-20
2 3 02139 2008-04-10
3 4 60091 2008-05-30
4 5 02139 2008-07-19

sessions

index session_id customer_id device session_start
0 1 1 desktop 2014-01-01 00:00:00
1 2 1 desktop 2014-01-01 00:17:20
2 3 5 mobile 2014-01-01 00:28:10
3 4 3 mobile 2014-01-01 00:43:20
4 5 2 tablet 2014-01-01 01:10:25

transactions

index transaction_id session_id transaction_time product_id amount
0 352 1 2014-01-01 00:00:00 4 7.39
1 186 1 2014-01-01 00:01:05 4 147.23
2 319 1 2014-01-01 00:02:10 2 111.34
3 256 1 2014-01-01 00:03:15 4 78.15
4 449 1 2014-01-01 00:04:20 3 33.93

customer_idとかsession_idとかでそれぞれのテーブルをJoinする感じね。。。

transactionsのamountがcustomerの商品購入額を表している感じかな。どのsessionでのtransactionかを識別出来るところを見るとECサイトでの商品購入を想定したデータといったところか。

featuretoolsは以下が大きな流れになっているっぽい。

  1. EntitySetの生成
  2. Entityの生成
  3. Relationshipの生成
  4. EntitySetにEntity,Relationshipを追加
  5. DFSの実行

Entityを生成する

この3つのpd.DataFrameをEntityとして登録していく。

# generate EntitySet
es = ft.EntitySet(id='demodat')

# add Entity
es.entity_from_dataframe(entity_id='cust',dataframe=data['customers'],index='customer_id')
es.entity_from_dataframe(entity_id='session',dataframe=data['sessions'], index='session_id',time_index='session_start')
es.entity_from_dataframe(entity_id='trans',dataframe=data['transactions'], index='transaction_id',time_index='transaction_time')

Entityの名称を決めて、レコードを一意に特定出来る列を指定してentity_from_dataframeでEntityとして登録する。この時に、時系列を認識させるためのtime_indexとかも指定出来る。

まぁこれだけだと、別に何も恩恵はない。。。
Entity間のRelationを定義して紐付けるあたりからいい感じなってくる。

ちなみにEntityは必ずユニークに識別出来る列を持っておく必要があるので、そこは注意が必要。

Relationshipを生成する

3つのEntityをそれぞれRelationshipを定義してくっつける。

ft.Relatinoship([ 紐付け先Entity[紐付けKey], 紐付け元Entity[紐付けKey] ])でRelationを定義出来る。

紐付け先:親Entity
紐付け元:子Entity

# generate relationship
r_cust_session = ft.Relationship(es['cust']['customer_id'], es['session']['customer_id'])
r_session_trans = ft.Relationship(es['session']['session_id'], es['trans']['session_id'])

#link(add) relationship
es.add_relationships(relationships=[r_cust_session,r_session_trans])

今回の場合は以下の構造となっている。

  • (親Entity) cust
    • (子Entity) session
      • (孫Entity) trans

Entity setはこんな感じになってる。

Entityset(demodat)
Entityset: demodat
  Entities:
    cust [Rows: 5, Columns: 3]
    session [Rows: 35, Columns: 4]
    trans [Rows: 500, Columns: 5]
  Relationships:
    session.customer_id -> cust.customer_id
    trans.session_id -> session.session_id

3つのEntityと2つのRelationshipsが生成され追加されているのがわかる。

DataTypeを確認する

自動で変数を作成する処理に移る前に、featuretoolsが定義するデータのタイプについて把握しておく必要がある。

いや、pandasの定義と同じやろ!!と思っていたのだが、意外と細かく設定されている。

公式リファレンス

以下リファレンスからの引用

データタイプ 概要 つまり。。。
Index(id, entity[, name]) Represents variables that uniquely identify an instance of an entity EntityのユニークなIndex Key
Id(id, entity[, name]) Represents variables that identify another entity 他のテーブルとひもづける時のKey
TimeIndex(id, entity[, name]) Represents time index of entity 時系列Index
DatetimeTimeIndex(id, entity[, format, name]) Represents time index of entity that is a datetime Datetimeの時系列Index
NumericTimeIndex(id, entity[, name]) Represents time index of entity that is numeric 数値型の時系列Index
Datetime(id, entity[, format, name]) Represents variables that are points in time indexではない時系列データ
Numeric(id, entity[, name]) Represents variables that contain numeric values 数値型の変数(特徴量に使うつもりの変量)だいたいこれ
Categorical(id, entity[, name]) Represents variables that can take an unordered discrete values 文字列型や非数値型の変数(種類が知れてるカテゴリデータは大抵これ)
Ordinal(id, entity[, name]) Represents variables that take on an ordered discrete value 順序関係のある非数値型の変数(線形回帰系のモデリング時にこの型を用いると意図しない挙動になる可能性があるので注意!!)
Boolean(id, entity[, name]) Represents variables that take on one of two values 2値の論理型データ
Text(id, entity[, name]) Represents variables that are arbitary strings テキスト型(おそらく文字列のパターンが多いとCategoricalでなくてこちらが採用される)
LatLong(id, entity[, name]) Represents an ordered pair (Latitude, Longitude) 緯度経度

ポイントとしてはIdやIndexのようなデータ型が存在し、Entityのユニークキーやその他のEntityと紐づけるためにキーを明示的定義しているところ。IndexやIdのようなインデックス系のデータ定義がされているものは集約関数が適用されない。

ちなみにこんな感じで確認出来る

es['trans'].variables


[<Variable: session_id (dtype = id, count = 500)>,
 <Variable: transaction_time (dtype: datetime_time_index, format: None)>,
 <Variable: amount (dtype = numeric, count = 500)>,
 <Variable: transaction_id (dtype = index, count = 500)>,
 <Variable: product_id (dtype = categorical, count = 500)>]

DFSを実行する(いや、待て!! DFSってなんや??)

さて、本題の特徴量の自動生成に取り掛かろう:muscle:

公式リファレンスによるとDFSを実行すればいいとのこと。。。

なるほど。。。

DFS。。

Deep Feature Synthesis

ん?

何それ?

ググろ

MITの人がなんかそれっぽくまとめているpdfによると複数のRetionshipを持ったテーブルを横断的に集計したり変換したりする行為をDeep Feature Synthesisと呼んでいるっぽい。。。

ふーーん:no_mouth: まぁ、モデルに突っ込む特徴量をいろんな形で作成してくれるという理解でええやろ。

とりあえず、sessionと定義したEntityでDFSやってみる。

# define aggregate functions
list_agg = ['sum','min','max','count']

# define transfer functions
list_trans = ['year','month','day']

# run dfs
df_feature, features_defs = ft.dfs(
                                     entityset=es, #specify entityset
                                     target_entity='session', #specify entity
                                     agg_primitives=list_agg, #specify agg functions
                                     trans_primitives =list_trans, #specify transform functions
                                     max_depth=1 #specify depth
                                   )
# head
df_feature.head()

head 結果

session_id(Index) customer_id device SUM(trans.amount) MIN(trans.amount) MAX(trans.amount) COUNT(trans) YEAR(session_start) MONTH(session_start) DAY(session_start) cust.zip_code
1 1 desktop 1245.54 5.60 147.23 16 2014 1 1 60091
2 1 desktop 895.33 8.67 148.14 10 2014 1 1 60091
3 5 mobile 939.82 20.91 141.66 14 2014 1 1 02139
4 3 mobile 2054.32 8.70 147.73 25 2014 1 1 02139
5 2 tablet 715.35 6.29 124.29 11 2014 1 1 02139

おおぉぉーーーー!!!:pray:なんかいっぱい特徴量できとるゥ!!!

具体的な手順を書いておくと

  1. 適用したい集約関数を定義する
  2. 適用したい変換関数を定義する
  3. ft.dfs()にEntitysetを定義し、関数を適用したいEntityを指定する
  4. 適用する関数の深さ(depth)を指定する
  5. returnされるオブジェクトは(feature_matrix,featurelist)で変換されたDataFrame、Featureのリスト

返却されたfeature_matrixを見てみるとsession Entityの子Entityとなっているtrans Entityのうち、Numeric型の変数が指定した集約関数が適用されて特徴量として作成されている。

sessionの親EntityであるcustEntityの変数となるzip_codeも紐づいている。

追加された特徴量は以下の3タイプ

  1. 自分の親Entity(cust)の変数は子Entityに継承されてfeatureとして追加
  2. 自分の子Entity(trans)の変数は指定された集計関数分だけfeatureとして追加
  3. 自分のEntity(session)の変数に変換関数が適用されてfeatureとして追加

え。。。

すごく便利じゃね、これ!!:thumbsup:

ちなみにmax_depth=2を指定すると親Entity粒度で集計した結果を紐づけた変数が追加される。

一応、featuretoolsで集計関数として用意されているものを確認しておく。

集計関数一覧 -公式リファレンス-から抜粋)

集計関数 概要
Count(id_feature, parent_entity[, count_null]) Counts the number of non null values.
Mean(base_features, parent_entity[, …]) Computes the average value of a numeric feature.
Sum(base_features, parent_entity[, …]) Counts the number of elements of a numeric or boolean feature.
Min alias of min
Max(base_features, parent_entity[, …]) Finds the maximum non-null value of a numeric feature.
Std(base_features, parent_entity[, …]) Finds the standard deviation of a numeric feature ignoring null values.
Median(base_features, parent_entity[, …]) Finds the median value of any feature with well-ordered values.
Mode(base_features, parent_entity[, …]) Finds the most common element in a categorical feature.
AvgTimeBetween(base_features, parent_entity) Computes the average time between consecutive events.
TimeSinceLast(base_features, parent_entity) Time since last related instance.
NUnique(base_features, parent_entity[, …]) Returns the number of unique categorical variables.
PercentTrue(base_features, parent_entity[, …]) Finds the percent of ‘True’ values in a boolean feature.
All(base_features, parent_entity[, …]) Test if all values are ‘True’.
Any(base_features, parent_entity[, …]) Test if any value is ‘True’.
Last(base_features, parent_entity[, …]) Returns the last value.
Skew(base_features, parent_entity[, …]) Computes the skewness of a data set.
Trend(base_features, parent_entity, **kwargs) Calculates the slope of the linear trend of variable overtime.

まぁ、AVGとかSUM覚えとけばいいかな。。

変換関数は数も多いので公式リファレンスを直接確認してください。

depthの違いによって自動作成される変数がどう変わるか?

自動作成される変数がどう変化するかを見るために、cust Entityを基準に変数の作成を行いたい。

ちなみにcust Entityが基準となる実際の問題設定としては
「既存の顧客のうち、過去の取引傾向からXヶ月後に取引をやめてしまう顧客を検知したい」
といった離反予測問題や
「過去の取引傾向から来月の取引額を予測し、来月の仕入在庫量を適正化したい」
といった需要予測・在庫最適化問題の時のFeatureEngineeringで活用しそうな予感。。。

depth=0のケース

#depth=0
# define aggregate functions
list_agg = ['sum','median','count','std']
# define transfer functions
list_trans = ['year','month','day']
# run dfs
df_feature_depth0, _ = ft.dfs(
                                     entityset=es,
                                     target_entity='cust',
                                     agg_primitives=list_agg,
                                     trans_primitives =list_trans,
                                     max_depth=0)
# count features
print(len(df_feature_depth0.columns))

>> 1 #zip_code

depth=0の場合には、自分自身のEntityのうち、IndexやIdでないものだけが特徴量として生成される。

depth=1のケース

# depth=1
# define aggregate functions
list_agg = ['sum','median','count','std']
# define transfer functions
list_trans = ['year','month','day']
# run dfs
df_feature_depth1, _ = ft.dfs(
                                     entityset=es,
                                     target_entity='cust',
                                     agg_primitives=list_agg,
                                     trans_primitives =list_trans,
                                     max_depth=1)
# count features
print(len(df_feature_depth1.columns))
print(list(df_feature_depth1.columns))

>> 5
>> ['zip_code', 'COUNT(session)', 'YEAR(join_date)', 'MONTH(join_date)', 'DAY(join_date)']

depth=1の場合には、自分自身のEntityのうち、Transformの対象となっている変数と1階層下の子Entityに集計関数を適用した結果が特徴量として生成されている。

depth=2のケース

depth=2の場合には、当然孫Entityまで含めて作成される。

# depth=2
# define aggregate functions
list_agg = ['sum','median','count','std']
# define transfer functions
list_trans = ['year','month','day']
# run dfs
df_feature_depth2, _ = ft.dfs(
                                     entityset=es,
                                     target_entity='cust',
                                     agg_primitives=list_agg,
                                     trans_primitives =list_trans,
                                     max_depth=2)
# count features
print(len(df_feature_depth2.columns))
print(list(df_feature_depth2.columns))


>> 18
>> ['zip_code', 'COUNT(session)', 'SUM(trans.amount)', 'MEDIAN(trans.amount)', 'COUNT(trans)', 'STD(trans.amount)', 'YEAR(join_date)', 'MONTH(join_date)', 'DAY(join_date)', 'SUM(session.MEDIAN(trans.amount))', 
'SUM(session.STD(trans.amount))', 'MEDIAN(session.SUM(trans.amount))', 'MEDIAN(session.MEDIAN(trans.amount))', 'MEDIAN(session.COUNT(trans))',
'MEDIAN(session.STD(trans.amount))', 'STD(session.SUM(trans.amount))', 'STD(session.MEDIAN(trans.amount))', 'STD(session.COUNT(trans))']

18特徴量か。。めっちゃ多くなってきたな。。。ここまでの変数を自分で作ろうと思ったらかなり大変やし、これはマジで使えるかもしらんなぁ。。
よく見るとtrans.amountの偏差の合計やら合計の偏差やら色んな組み合わせを自動作成してくれてるし。

凄イィ。。。

これを使いこなせれば、集計地獄から解放される予感:kissing_heart:

ちなみに、depth=-1を指定すると最も深い階層まで潜って変数を作ってくれるみたいなので、Entityの階層が浅くて集計関数をそれほど設定してなければ(メモリを食いつぶすリスクも小さいので)、-1を指定しておくとお手軽でいいかも!!(リファレンスにはそんなこと書かれていないけど多分そう)

まとめ

試しにfeaturetoolsを使ってみたが、こいつぁ凄い。。個人的には今後のfeature creationの際にはこれを使えそうか第一に検討するので良さそう。

少なくとも構造化された表形式のデータを使って回帰やら判別モデルを作成するときには、非常に強力なツールとなってくれそうだし。リファレンスを見る限りタイムシフト的な変数も作れそうだし夢が広がりますなぁ!!

pd.merge()みたいな複雑で可読性の低い処理はあんまり書きたくないしね。

使いこなせるようになったら、もうちょっと細かいtipsの記事書けるとなお良いのだが。。

まぁいいや、いったん、おしまい