電通総研 テックブログ

電通総研が運営する技術ブログ

Oura Ringで就寝時間をレコメンドする

こんにちは。ISID 金融ソリューション事業部の若本です。

1ヶ月ほど前にOura Ringというスマートデバイスを購入しまして、生体データを色々取り溜めていました。せっかくデータを取るなら何かに使いたいということで、機械学習モデルと組み合わせて就寝時間をレコメンドしてみます。

Oura Ringとは?

Oura Ring指輪型の健康トラッカーです。多種のセンサーを内蔵しており、運動や睡眠、心拍数に関するデータを日々蓄積しています。完全に個人の感想ですが、充電持ちがよく着用の違和感もないので重宝しています。

なお、蓄積したデータはAPIを使ってアクセスできます。APIは2種類ありますが、今回はOura API V1のデータのみを使いました。より詳細なデータを取得できるOura API V2もありますが、こちらは運動や心拍のデータがメインとなっており、コンディションや睡眠に関するデータは記事執筆時点で提供されていません。今回はAPI V1で取得できるデータのうち、機械学習モデルへの入力として「前日の睡眠時間・起床時間・就寝時間」を、出力として「翌日のReadinessスコア」を使用しました。

※V1の機能はV2に順次移行していくようですので、仕様は変わる可能性があります。適宜APIリファレンスをご参照ください。

Oura Ringでやりたいこと

Oura Ringでは平均的な就寝時間の範囲を把握することはできますが、就寝時間を変えることによって体調がどれほど変化するのかはわかりません。そこで、Oura Ringのデータから「何時に寝ればいいのか」「それによってどれくらい次の日のコンディションがよくなるのか」を教えてくれるAIモデルを作成します。
Oura Ringでは体が準備できているかを表す指標としてReadinessスコアを算出しており、これを疑似的にコンディションとみなすことができそうです。このReadinessスコアを高くするために、前日の睡眠実績から「今日は何時に寝るべきか」を推論することがゴールです。

機械学習モデルによる予測

Oura Ringで取得している睡眠実績データを基に、同じくOura Ringで算出されるReadinessスコアを予測します。取り貯めた1ヶ月のデータをOura API V1で取得して得られた前日の睡眠時間、起床時間、就寝時間を基に、翌日のReadinessスコアを予測する機械学習モデルを作成しましょう。

ここで、Oura APIで取得したデータについて前処理を行います。取得した起床時間/就寝時間はタイムゾーンなのでそのまま機械学習モデルで扱うことができません。floatへ型変換を行いましょう。具体的には、「何時何分か」の情報を数値情報に変換します。一番簡単なのは1日を0~1の数値に落とし込むことですが、単純に24で除算してしまうと24時以降の値が不連続になってしまうので、14時までのデータは24を足すことで連続するようにしました。睡眠時間とスコアについても適当にスケーリングします。

def time_to_float_converter(x):
    time = pd.to_datetime(x)
    hour = (time.hour + 24 if time.hour < 14 else time.hour) / 24 - 0.5
    minute = time.minute / (24 * 60)
    return (hour+minute)
    
info_df['sleep_bedtime_start'] = info_df['sleep_bedtime_start'].apply(lambda x: time_to_float_converter(x))
info_df['sleep_bedtime_end'] = info_df['sleep_bedtime_end'].apply(lambda x: time_to_float_converter(x))
info_df['sleep_duration'] = info_df['sleep_duration'] / 3600 / 12
info_df['readiness_score'] /= 100

前処理後のデータは以下のようになります。全てのデータをfloat型に落とし込めていることが確認できました。

Readinessスコアの説明によると、Readinessスコアは運動・睡眠・心拍数の長期的な推移で算出されているそうですが、今回は睡眠データの一部しか使わないため、正確にReadinessスコアを予測することは不可能です。さらに期間も短いため、外れ値がモデルの学習に影響を及ぼしやすい条件になっています。そこで、予測モデルは外れ値の影響を受けづらいHuber Regressorを選定しました。なお、モデルの作成時にはPyCaretでチューニングしたパラメータを用いています。

X_train = info_df.drop(['readiness_score'], axis=1)
y_train = info_df['readiness_score']
model = HuberRegressor(**params)
model.fit(X_train, y_train)

DiCEで就寝時間をレコメンド

上記のモデルで翌日のReadinessスコアを予測したい場合、就寝時間の入力を使うことができません。なぜなら、予測時点でまだ就寝時間が決まっていないからです。

なので、モデルの理想の出力に合わせて就寝時間を計算し、就寝時間をレコメンドします。反実仮想説明(CE: Counterfactual Explanation)に用いられるDiCE1でこれを実現します。
DiCEの詳細な説明は省きますが、DiCEでは「もしも~の入力とその予測結果」(反実仮想サンプル)を生成します。詳細は原著論文をご参照ください。DiCEを使用することで、予測を変えるために入力をどれだけ変えるべきかがわかります。
まず先ほど学習済みの機械学習モデルをDiCEに取り込みます。

d = dice_ml.Data(dataframe=info_df,
                 continuous_features = list(info_df.drop(['readiness_score'], axis=1).columns), # 連続変数の指定(今回はすべて該当)
                 outcome_name = 'readiness_score')
m = dice_ml.Model(model=model, backend="sklearn", model_type='regressor')
exp = dice_ml.Dice(d, m, method="random")

次にDiCEを用いて、学習データから反実仮想サンプルを生成してみます。今回は2つ生成してみましょう。21時~27時の間に就寝するという制約を付け、Readinessスコアが80点以上となるようなサンプルを生成します。

index = 0
counterfactuals_num = 2
conf = exp.generate_counterfactuals(X_train.iloc[index:index+1, :], 
                                  total_CFs=counterfactuals_num, 
                                  features_to_vary=["sleep_bedtime_start"],
                                  permitted_range={'sleep_bedtime_start': [0.417, 0.625]}, # 21時~27時の間
                                  desired_range=[0.8, 1.0]) # スコアが80~100点ならOK
conf.visualize_as_dataframe(show_only_changes=True)

上記のような結果が得られました。sleep_duration(前日の睡眠時間)やsleep_bedtime_end(起床時間)は変わらず、sleep_bedtime_start(就寝時間)のみ変化していることが分かります。最後に、得られた反実仮想サンプルを時刻に戻して出力してみます。

def float_to_time_converter(x: float):
    time_tmp = (x + 0.5) * 24
    f, i = math.modf(time_tmp)
    pred_hour = int(i)
    pred_minites = int(f * 60)
    return pred_hour, pred_minites
    
def messenger(df):
    for i, row in df.iterrows():
        pred_hour, pred_minites = float_to_time_converter(row['sleep_bedtime_start'])
        score = row['readiness_score']
        print(f'<候補{i+1}> {pred_hour}:{pred_minites}に就寝すると{score*100:.1f}点のReadinessスコアが見込めます。')


conf_df = conf.cf_examples_list[0].final_cfs_df_sparse # DiCEの結果をpandasに変換
messenger(conf_df)

前日の睡眠状態から、「24時過ぎに寝ること」をレコメンドされました。現実的な数値なので納得感はあります。23時過ぎの就寝もレコメンドされていますが、1時間早く寝ても翌日の予測Readinessスコアは0.2点しか上がらないようです。
思ったよりも就寝時間の差による影響が少なかった印象ですが、より長い期間のデータを基にモデルを作ると顕著な傾向が見られるかもしれません。もしくは就寝時間だけでなく、今回使用しなかった運動データなどについてもレコメンド対象とすれば、より大幅にReadinessスコアをコントロールできそうです。

おわりに

今回は、Oura Ringで取得したデータを用いた就寝時間のレコメンドについて紹介しました。今回は簡易的な検証として睡眠データとReadinessスコアのみを用いましたが、好みに合わせて入力を変えるのも面白そうです。予測したい変数についても1日の総タイピング数、Gitのcommit行数などに変えてもいいかもしれません。
さらに、LINE NotifyHerokuなどのサービスを組み合わせることで就寝時間を毎日通知してくれるようにもできます。この指輪1つで色々と面白いことができそうですね。

おまけ

今回使用しているコードはこちらに公開しています。Oura Ringをお持ちの方はぜひお試しください。

執筆:@wakamoto.ryosuke、レビュー:@higaShodoで執筆されました


  1. DiCE: Diverse Counterfactual Explanations(https://github.com/interpretml/DiCE