CausalImpactの理解と実装

はじめに

今回はCausalImpactについて書いていきたいと思います。 CausalImpactはgoogle製の効果測定用パッケージで、主に広告やキャンペーンの効果を測定するのに用いられます。

なぜ広告やキャンペーンの効果を測定するのにこういうものが必要なのかというと、 季節性や単なる流行の影響など測定したい項目以外にも様々な影響があるために キャンペーン前と後を単純比較するだけではおかしな結果になることが少なからずあります。 それを防ぐために因果推論を考える必要があり、CausalImpactはその因果推論の考えを使用したパッケージになります。

参考資料

causalimpactについて

論文を読んだ自分なりの理解をまとめます。
(解釈間違えてたらすみません)

従来のワークフロー(差分の差分法+synthetic control method)
  1. 1つ以上の当てはまりが良さそうな対照群を選択する
  2. synthetic control methodを用いて対照群を選択する(1が複数ある場合)
    ※synthetic control methodは複数の対照群の重みつけ和を処置群になるべく近づけることで、仮想の処置しなかった場合の処置群を作成する方法
  3. 差分の差分法を用いて処置群と2で作成した対照群を比較する
上記ワークフローの問題点
  • 時系列データだとたいてい自己回帰成分が発生するが、差分の差分法では各時間の点が独立同分布であるという仮定を置いている
  • 差分の差分法では2点間の違いしか見ることができないため、例えばキャンペーン期間が1週間あったとして、開始時や終了前の影響を個別に見ることができない。
  • synthetic control method→差分の差分法とおいう2段階の推計を行なっているので、どちらの段階の誤差の影響が大きいかがわかりづらい
CausalImpactの提案手法
  • 状態空間モデルを用いて対照群から処置をしなかった場合の処置群を作成し予測値を算出。それを実際の処置群の値と比較することで因果効果を算出。
提案手法による改善点
  • 状態空間モデルを使うことで自己回帰成分や季節性など時系列要因を考慮することができる
  • 時系列の1点ごとの影響を確認することができる
  • synthetic control methodの部分と差分の差分法でやっていたことを1つの状態空間モデルでカバーしているためどの部分で誤差が発生しているかがわかりやすい。モデルの不確実性がわかる。

実装

causalimpactのオリジナルはRで書かれているのですが、 それをpythonに移植したものがあったので今回はpython版を使用します。 python版のレポジトリは2つありますが、今回用いたのはdafitiさんの方です。

使用するデータは前回の差分の差分法をやったときと同じくRecruit Restaurant Visitor Forecastingのレストランの来店人数のデータを使い、ある店舗におけるお盆期間(8/11-20)の影響の効果を測定したいと思います。

2店舗のプロット図はこんな感じです。

f:id:rmizutaa:20190509080423p:plain

前処理をします。

df=pd.read_csv("air_visit_data.csv",parse_dates=["visit_date"])

#元のデータは欠損があるため日時のベースを作成
basedate=pd.DataFrame(list(pd.date_range('2016-05-01', '2016-08-20')))
basedate.columns=["visit_date"]

#店舗を指定、日時を限定
store1=df[df["air_store_id"]=="air_e55abd740f93ecc4"]
store1=store1[(store1["visit_date"]>="2016-05") & (store1["visit_date"]<="2016-08-20")]
store2=df[df["air_store_id"]=="air_1653a6c513865af3"]
store2=store2[(store2["visit_date"]>="2016-05") & (store2["visit_date"]<="2016-08-20")]

#データを結合
store1.rename(columns={"visitors":"visitors_store1"},inplace=True)
store2.rename(columns={"visitors":"visitors_store2"},inplace=True)
data=pd.merge(basedate,store1[["visit_date","visitors_store1"]],on="visit_date",how="left")
data=pd.merge(data,store2[["visit_date","visitors_store2"]],on="visit_date",how="left")
data=data.interpolate() #欠損値を線形補間
data=data.set_index("visit_date")

前処理後データはこんな感じです。

f:id:rmizutaa:20190509075343p:plain

CausalImpactに入れます。週の周期性があるためnseasonを7としています。

from causalimpact import CausalImpact
pre_period = ['2016-05-01', '2016-08-10']
post_period = ['2016-08-11', '2016-08-20']
ci = CausalImpact(data, pre_period, post_period,nseasons=[{'period': 7}],trend=True)
print(ci.summary())

f:id:rmizutaa:20190509075417p:plain

お盆の効果は平均すると13.7人、10日累積だと137人の効果があると出ました。 差分の差分法でやったときは13.5人と出ていたので同じくらいの値が出ています。 95%信頼区間も算出されていて、累積で見ると48.0〜228.3と結構モデルが安定していないことがわかります。 結果がどれだけぶれやすいかという不確実性が見れるのは良いですね。

グラフで見るとこのような形になります。

f:id:rmizutaa:20190509075444p:plain

1番目のグラフでyが処置群、Predictedが状態空間モデルによる予測値で、色がついている部分が予測値の信頼区間になります。 2番目のグラフは1番目のグラフのy-Predictedを示しています。 3番目のグラフは処置期間のy-Predictedの累積和を示します。

ちなみにCausalImpactはUnobservedComponentsを内包しており状態空間モデルそのものの結果を見ることもできます。

ci.trained_model.summary()

f:id:rmizutaa:20190509075601p:plain

作成した状態空間モデルの観測誤差が大きいために信頼区間が広めになっているようです。

CausalImpactは対照群を入力するのが通常ですが、対象群がない場合でも実行することができます。 この場合状態空間モデルは処置群の処置前の系列から作成されます。

pre_period = ['2016-05-01', '2016-08-10']
post_period = ['2016-08-11', '2016-08-20']
ci = CausalImpact(data.iloc[:,0], pre_period, post_period,nseasons=[{'period': 7}])
ci.plot(figsize=(12, 6))

f:id:rmizutaa:20190509203704p:plain 今回の例だと対照群を入れた場合と結果はあまり変わらなかったりします。 おそらく対照群が複数入力できる場合だと、そちらの方が結果が安定するんじゃないかと思います。

メモ

  • 効果の信頼区間が出せるのはうれしい
  • 1点ごとの推定ができるため処置区間内での変化も見ることができるのはよさそう
  • 対象群が複数入力できない場合にはあまり効果を発揮できない可能性はあるかも
  • 機会があれば使いたい

使用したコードは以下になります。

github.com