LIFULL Creators Blog

LIFULL Creators Blogとは、株式会社LIFULLの社員が記事を共有するブログです。自分の役立つ経験や知識を広めることで世界をもっとFULLにしていきます。

広告宣伝費最適化に向けた最適化問題の活用

AI戦略室の椎橋です。LIFULLで取り組んでいる広告費配分のポートフォリオ最適化を紹介します。

LIFULLは広告宣伝費に年間100億近く使っており、決算説明会の質疑応答でも頻出なテーマで削減することが求めらています。広告にはTVCMや電車のつり革広告、リスティング広告や、リターゲティング広告など広告配信する場所やターゲットユーザー層もさまざまな種類があります。これらの広告媒体にそれぞれいくらの金額を投資すべきかというポートフォリオ最適化を計算するのが本記事のメインになります。

社内システムMAM

広告運用を自動化するためにMAMという社内システムがあります。広告を運用するマーケターが操作するためのフロントエンド、広告実績を蓄積するDB、取得・集計する定期バッチ処理などの機能をすべてまとめてMAMと呼んでいます。ポートフォリオ最適化におけるデータの流れを簡易的に図示すると以下のようになります。

f:id:LIFULL-shiibass:20200812104242j:plain
ポートフォリオ最適化のためのデータの流れ簡易図

広告実績データをBigQueryに保存し、そこからデータ抽出、機械学習計算、計算結果のポートフォリオで広告入稿、その結果がBigqueryに保存される、というサイクルになっています。

アトリビューションモデル

ポートフォリオ計算の前に広告を価値を定量化してサイエンスの問題に落とし込みやすくします。ユーザーはコンバージョンまでに多くの媒体に接触しており、接触した媒体はそれぞれどれくらい貢献しているかというのを計算するモデルになります。下図の接触例で、代表的なアトリビューションモデルで評価したときの広告評価値を表にまとめると以下のようになります。

f:id:LIFULL-shiibass:20200812103742j:plain
コンバージョンまでの媒体接触例

f:id:LIFULL-shiibass:20200812105415j:plain
さまざまなアトリビューションモデルとそのときの広告価値見積もり

弊社では独自のアトリビューションモデルを開発していて、そのモデルで各広告の売上換算価値を見積もることができます。 計算方法は今回は省略します。この価値を使ってポートフォリオ計算を行います。

ポートフォリオ最適化アルゴリズム

ポートフォリオを計算するために最適化問題を解くのですが、必要なパーツをサブセクションに分けて説明します。

 広告効果モデル

各広告に対していくらの金額を使うといくらの売上価値が見込めるかという回帰モデルを作ります。扱いやすいように凸の性質を持つような関数で回帰します。一般的に広告予算を増やせば増やすほどコンバージョン率の低い層にも広告配信していくようになるため、以下の図に示すように減衰効果をモデルに組み込むことには妥当性があります。

f:id:LIFULL-shiibass:20200825162410j:plain
広告効果モデル(横軸はコスト、縦軸は売上)

入口出口問題

これは社内独自の問題で私はそう呼んでいます。LIFULL HOME'Sは賃貸、新築マンション、中古戸建などのセクターごとに管理する部署が異なり、予算も目標売上も各セクターごとに割り当てられています。一方で賃貸と売買のどちらにするか悩むユーザーは多くおり、賃貸想定の広告のつもりが最終的に中古マンションを購入するということは珍しくありません。なので発生したセクターごとの売上の割合で各部署で予算を出し合って広告出稿したとみなすようにしています。リスティング広告の例がわかりやすく、以下のように"東京 ほーむず マンション"という検索広告は賃貸ユーザー向きか売買ユーザー向きかはっきりしません。この広告に100万円使って売上価値が賃貸200万、新築マンションが600万、中古戸建が200万の売上になったら、それぞれの予算を20万、60万、20万使ったことにするということです。 この配分ルールによって、賃貸の予算を使おうとしても売買の予算も使ってしまうことになり、トレードオフがある中でポートフォリオを計算する必要があります。

f:id:LIFULL-shiibass:20200812103754j:plain
"東京 ほーむず マンション"の検索広告

定式化

問題を簡単にするために本記事ではセクターを賃貸と売買の2セクターとし、広告効果モデルはy=a\log(x+1),\ \ \ a>0の形式で書けるものとします。 定式化は以下のようになります。

f:id:LIFULL-shiibass:20200812110023j:plain
最適化問題

ここでMは大きな定数、添え字iは広告IDを表し、a_iは広告効果モデルのパラメータです。式7式8ではダミー変数を用いていますが、これは実運用では残り予算がマイナスになるときがあり、そのときでも実行可能解を出力するための式変形です。BUDGET_CHINTAI=100, BUDGET_BAIBAI=200は賃貸部署、売買部署の予算です。 サンプルコードを用意しました。

import cvxpy
import numpy

BUDGET_CHINTAI = 100
BUDGET_BAIBAI = 200

AD_MODELS = [
    {"id": 1, "a": 20, "chintai_rate": 0.2, "baibai_rate": 0.8},
    {"id": 2, "a": 10, "chintai_rate": 0.3, "baibai_rate": 0.7},
    {"id": 3, "a": 5, "chintai_rate": 0.5, "baibai_rate": 0.5},
    {"id": 4, "a": 30, "chintai_rate": 0.9, "baibai_rate": 0.1},
]

def _predict_sale_and_cost(cost, ad_model):
    sale = ad_model["a"] * cvxpy.log(cost + 1)
    sale_chintai = sale * ad_model["chintai_rate"]
    sale_baibai = sale * ad_model["baibai_rate"]
    cost_chintai = cost * ad_model["chintai_rate"]
    cost_baibai = cost * ad_model["baibai_rate"]
    return sale_chintai, sale_baibai, cost_chintai, cost_baibai

def objective_function(costs, dummys):
    M = 1000
    sum_sale_chintai = 0
    sum_sale_baibai = 0
    for cost, ad_model in zip(costs, AD_MODELS):
        sale_chintai, sale_baibai, _, _ = _predict_sale_and_cost(cost, ad_model)
        sum_sale_chintai += sale_chintai
        sum_sale_baibai += sale_baibai
    return sum_sale_chintai + sum_sale_baibai - M * (dummys[0] + dummys[1])

def constraint_function(costs, dummys):
    const_list = []
    sum_cost_chintai = 0
    sum_cost_baibai = 0
    for cost, ad_model in zip(costs, AD_MODELS):
        _, _, cost_chintai, cost_baibai = _predict_sale_and_cost(cost, ad_model)
        sum_cost_chintai += cost_chintai
        sum_cost_baibai += cost_baibai
    const_list.append(sum_cost_chintai <= BUDGET_CHINTAI + dummys[0])
    const_list.append(sum_cost_baibai <= BUDGET_BAIBAI + dummys[1])
    
    for cost in costs:
        const_list.append(cost >= 0)
    
    for dummy in dummys:
        const_list.append(dummy >= 0)
        
    return const_list

def calc_sum_cost(costs):
    cost_chintai = numpy.array([c * m["chintai_rate"] for c, m in zip(costs.value, AD_MODELS)]).sum()
    cost_baibai = numpy.array([c * m["baibai_rate"] for c, m in zip(costs.value, AD_MODELS)]).sum()
    print("賃貸コスト ", cost_chintai)
    print("売買コスト ", cost_baibai)
    
costs = cvxpy.Variable(len(AD_MODELS))
dummys = cvxpy.Variable(2)
prob = cvxpy.Problem(cvxpy.Maximize(objective_function(costs, dummys)), constraint_function(costs, dummys))
prob.solve(verbose=False, solver="ECOS_BB", mi_max_iters=50000)
assert prob.status == "optimal"
costs.value
calc_sum_cost(costs)

実行すると以下の解を得ます。広告1,2,3,4にそれぞれ155,51,14,51の予算を割り当てるという結果で、このとき賃貸部署が負担する予算は100、売買部署が負担する予算は172です。これは定式化の性質的には大域的最適解に収束しているはずなのでこれ以上多く売上を出せる解は存在しないはずです。

f:id:LIFULL-shiibass:20200812103805j:plain
最適化計算結果

事業の成長に合わせて目的関数を変更できる

サンプルコードでの売上最大化は一見腑に落ちる式に見えますが、売買部署の立場から見ると予算が200あるのに172しか使っていないという点で最適ではないかもしれません。利益の最大化を目的としたポートフォリオはまた別の解が出てきます。このように立場や事業の成長期によって目的が変わるという意味ではある意味多目的最適化の側面を持っており、例えば想定されるシチュエーションごとに対策を考えておくとこうなります。

f:id:LIFULL-shiibass:20200812103808j:plain
目的関数のバリエーション

 説明責任

このアルゴリズムの強みは細かい運用における制約を記述できることですが、説明責任を果たせることも大きなメリットです。最近の金融工学の研究ではニューラルネットワークや強化学習を用いたポートフォリオ最適化もあるのですが、ブラックボックス性の強さが安定運用の妨げになりかねません。出力値に対して説明ができず、ポートフォリオを採用してくれる社内のマーケターに安心感を与えられません。機械学習は広告効果の予測のみに使うように切り分けることで、広告効果モデルの予測が正しいと仮定すればそのあとの計算については納得してもらえます(もちろん数理最適化の説明は平易な言葉に置き換えます)。

まとめ

広告宣伝費最適化に向けた数理最適化の活用事例を紹介しました。最適化問題は非常に強力とは言えないのですが、かゆいところに手が届く技術で、業務上必要になる細かいロジックを洗練させたいときに役立ちます。

AI戦略室の事例をコンスタントに発信できるようにがんばります。ありがとうございました。