機械学習の特徴量として日付を使う際の考慮点
機械学習の特徴量として日付を使う際の考慮点

機械学習の特徴量として日付を使う際の考慮点

はじめに

DROBE ではユーザーとアイテムのマッチ度を算出するモデルを利用していますが、ユーザー側の特徴量として商品の提案日というものを使っています。

これは、例えば夏に商品を提案する際にコートを提案するのはあまり意味が無いという事から、ファッションにおける推薦には季節性を考慮する必要があるという所をモチベーションとしています。

この記事では日付を特徴量として使った際に線形モデルで季節を予測してアプリケーションで使う際の注意点について解説します。

特徴量としての配送日

配送日は日付なので、循環するものであると言えます。例えば12月31日と1月1日はとなり合わせの日付なので特徴としては非常に近いはずです。

ところが日付をそのまま入力とすると、機械学習のモデルは12月31日と1月1日の間に数字的な繋がりを見出す事が出来ません。

そこでモデルが日付を循環したものとして捉えられるように三角関数を使って日付を変換します。

(このコードは こちらの記事 を参考にさせていただきました)

import pandas as pd
import numpy as np
import datetime
import matplotlib.pyplot as plt
import seaborn as sns

def encode(df, col):
    """"
    この方法だと場合によって最大値が変化するデータでは正確な値は出ない
    例:月の日数が30日や31日の場合がある
    """"
    df[col + '_cos'] = np.cos(2 * np.pi * df[col] / df[col].max())
    df[col + '_sin'] = np.sin(2 * np.pi * df[col] / df[col].max())
    return df

SEASON = {
    1: "WINTER",
    2: "WINTER",
    3: "SPRING",
    4: "SPRING",
    5: "SPRING",
    6: "SUMMER",
    7: "SUMMER",
    8: "SUMMER",
    9: "FALL",
    10: "FALL",
    11: "FALL",
    12: "WINTER",
}

def make_dataset():
    values = []
    for i in range(0, 365):
        now = datetime.datetime(2021, 1, 1) + datetime.timedelta(days=i)
        season = SEASON[now.month]
        values.append((now.date(), i, season))

    df = pd.DataFrame(values, columns=["date", "yday", "season"])
    df = encode(df, "yday")
    return df
三角関数を使って日付をエンコードする

上記関数を使ってエンコードすると、以下のような dataframe を得る事が出来ます

df = make_dataset()
print(df)

date	yday	season	yday_cos	yday_sin
0	2021-01-01	0	WINTER	1.000000	0.000000e+00
1	2021-01-02	1	WINTER	0.999851	1.726064e-02
2	2021-01-03	2	WINTER	0.999404	3.451614e-02
3	2021-01-04	3	WINTER	0.998659	5.176135e-02
4	2021-01-05	4	WINTER	0.997617	6.899114e-02
...	...	...	...	...	...
360	2021-12-27	360	WINTER	0.997617	-6.899114e-02
361	2021-12-28	361	WINTER	0.998659	-5.176135e-02
362	2021-12-29	362	WINTER	0.999404	-3.451614e-02
363	2021-12-30	363	WINTER	0.999851	-1.726064e-02
364	2021-12-31	364	WINTER	1.000000	-2.449294e-16
上記の make_daatset 関数で作ったデータフレーム

Plot してみると、循環的な性質を持っている事が良くわかります

sns.scatterplot(data=df, x='yday_cos', y='yday_sin', hue='season')
image

エンコードされた日付を使って季節を推定してみる

このように sin cos にエンコードされた日付を使えば数字的な繫がりを加味した入力と出来そうですが、実はこのデータを線形モデルの入力としてそのまま使うと思わぬ落とし穴にハマる可能性があるので解説します。

作られたデータを使って以下のようなコードで季節を推定してみます。

from sklearn.metrics import roc_curve, auc

def solve(clf, target_seasons):
    df = make_dataset()

    X = df[["yday_sin", "yday_cos"]]
    y = np.where(df['season'].isin(target_seasons), 1, 0)
    clf.fit(X, y)

    preds = clf.predict_proba(X)[:, 1] # 検証データがクラス1に属する確率
    fpr, tpr, thresholds = roc_curve(y, preds)

    plt.plot(fpr, tpr, label='roc curve (area = %0.3f)' % auc(fpr, tpr))
    plt.plot([0, 1], [0, 1], linestyle='--', label='random')
    plt.legend()
    plt.xlabel('false positive rate')
    plt.ylabel('true positive rate')
    plt.show()

    print("{} accuracy={} target_seasons={}".format(clf.__class__.__name__, clf.score(X, y), target_seasons))

まずは季節が春である事を判断出来るかどうかを試してみます。推定には sklearn の LogisticRegression を使います。

from sklearn.linear_model import LogisticRegression

lr = LogisticRegression()
solve(lr, target_seasons=["SPRING"])

sin cos にエンコードされた日付データから、上手く春を推定できている事がわかります。

image

LogisticRegression accuracy=1.0 target_seasons=['SPRING']

次に季節が 春 or 秋 である事を判断できるかを試してみます。

lr = LogisticRegression()
solve(lr, target_seasons=["SPRING", "FALL"])
image

LogisticRegression accuracy=0.5506849315068493 target_seasons=['SPRING', 'FALL']

このように、全く判断出来ていない事がわかります。

ちなみに、冬 or 春 といった隣り合った季節の推定は問題なく出来ます。

lr = LogisticRegression()
solve(lr, target_seasons=["SPRING", "WINTER"])
image

LogisticRegression accuracy=1.0 target_seasons=['SPRING', 'WINTER']

これは直感的には以下のように考える事ができると思います。

単一の季節や、春と冬、といった隣り合わせの季節は直線を 1 本引けば分離可能なので線形モデルで判断が可能です。一方で、春と秋、といった隣り合っていない季節は分離するためには直線を 2 本引く必要があり、線形モデルでは判断が不可能であると考えられます。

隣り合った季節は 1 本の線で分離が可能
隣り合った季節は 1 本の線で分離が可能
隣り合っていない季節を分離するには 2 本の線が必要
隣り合っていない季節を分離するには 2 本の線が必要

このようにエンコードされた日付を入力として使った場合に、線形モデルを使ってしまうと上手く特徴が使えない事があり得るという事がわかります。

非線形モデルでの挙動

それでは非線形なモデルではどのような挙動になるかを試してみます。

from sklearn.ensemble import RandomForestClassifier

rfc = RandomForestClassifier()
solve(rfc, target_seasons=["SPRING", "FALL"])

このように、隣り合ってない季節でも正確に予測する事が出来ます。

image

RandomForestClassifier accuracy=1.0 target_seasons=['SPRING', 'FALL']

まとめ

ファッションにおける推薦に欠かせない季節性を考慮する際に重要となる日付データの取り扱いと、線形 / 非線形モデルによる挙動の違いについて解説しました。

データを上手く作っても、モデルの特性によっては望んだ性能が得られないといったケースがあり得てしまう事を検証しました。

アプリケーションやデータによって、どういったモデルを使うのか、また選択したモデルでデータを上手く扱う事が出来るのか、といった事を考慮に入れながら開発をしていくのが大事だと考えいます。

参考にした記事