NO AD NO LIFE

ベンチャー企業の広告エンジニアのブログ

ITP 2.0の機能の検証

はじめに

ITP(Inteligent Tracking Prevention)はブラウザ(Safari)上のユーザ行動の追跡を抑止する機能で、2017年に発表された最初のバージョンであるITP1.0は広告業界を震撼させました。 そして、2018年6月4日に新しいバージョンであるITP2.0が発表されました。ITP2.0ではさらに厳しいユーザ行動の追跡抑止機能が追加されています。

ITP2.0の主な機能として、

  • トラッカー(ユーザ行動の追跡者)により付与された3rd Party Cookieの即時的な排除
  • Storage Access APIを利用したCookieの管理
  • ファーストパーティバウンストラッカーに対する保護
  • リファラヘッダからドメイン以外部分の排除

があります。*1

今回はこれらの機能について、トラッキング環境を擬似的に構築し、動作検証を行なっていきます。

検証環境

今回想定するトラッキング環境

ユーザのトラッキングにはいくつかの方法がありますが、今回はトラッキングサーバのリダイレクトを利用した形式を想定します。

f:id:inchom:20180617005859p:plain
リダイレクタを利用したトラッキング

ユーザが広告をクリックすると、一度広告ベンダの計測サーバへリダイレクトされ、そこで広告ベンダのドメインへのCookieの付与が行われます。 その後、ユーザが購買ページへ到達すると、計測タグが発火してリダイレクト時に付与されたCookieを読み込み、広告経由のユーザかどうかを判定するという流れになります。

環境構築

想定する環境をローカルのPC上に作っていきます。本論だけ読みたい場合は読み飛ばしても大丈夫です。 ここで紹介するコードについてはこちらGitHub - satoshi03/ITP2-Investigation: For ITP2.0 Investigationソースコードがありますので、気になる人は参照してください。

仮想的なドメインの設定

/etc/hostsを編集して、仮想的にドメインを作ります。

# for ITP2.0 test
127.0.0.1       www.media1.com
127.0.0.1       www.media2.com
127.0.0.1       www.media3.com
127.0.0.1       www.media4.com
127.0.0.1       www.ad-vendor.com
127.0.0.1       www.advertiser.com

メディアページ

メディアページには、広告が貼られているという想定で、クリックするとリダイレクタへ遷移するリンクが貼られています。

<html>
  <body>
    <h1>Media Page</h1>
    <a href='http://www.ad-vendor.com:8888/redirect?to=http://www.advertiser.com/advertiser/lp.html' target='_self'>Click to redirect</a>
  </body>
</html>

今回はpython3を利用して簡易的なWebサーバを80番ポートで立ち上げています。

$ sudo python -m http.server 80

広告ベンダトラッキングサーバ

メディアの広告から遷移してきたユーザにCookieを付与し、遷移先の広告主サイトのランディングページへリダイレクトします。

package main

import (
        "fmt"
        "log"
        "time"

        "github.com/buaazp/fasthttprouter"
        "github.com/pborman/uuid"
        "github.com/valyala/fasthttp"
)

func redirectHandler(c *fasthttp.RequestCtx) {
        // Get referer
        referer := c.Referer()
        log.Printf("redirect from %s", string(referer))

        // Set tracking cookie
        cookie := fasthttp.Cookie{}
        cookie.SetKey("ADUID")
        cookie.SetValue(uuid.NewRandom().String())
        cookie.SetExpire(time.Now().AddDate(1, 0, 0))
        cookie.SetDomain("ad-vendor.com")
        cookie.SetPath("/")
        c.Response.Header.SetCookie(&cookie)

        // Redirect to designated page
        redirectTo := string(c.QueryArgs().Peek("to"))
        if redirectTo != "" {
                log.Printf("redirect to %s", redirectTo)
                c.Redirect(redirectTo, 302) //use 302 to avoid browser cache
        } else {
                fmt.Fprintf(c, "error. not redirect")
                c.SetStatusCode(fasthttp.StatusNotFound)
        }
}

func retargetHandler(c *fasthttp.RequestCtx) {
        // Get referer
        referer := c.Referer()
        log.Printf("user views: %s ", string(referer))
        c.SetStatusCode(fasthttp.StatusOK)
}

func main() {
        router := fasthttprouter.New()

        router.GET("/redirect", redirectHandler)
        router.GET("/retarget", retargetHandler)

        log.Printf("Start HTTP server")
        panic(fasthttp.ListenAndServe(":8888", router.Handler))
}

8888番ポートでWebサーバを立ち上げています。

$ go run server.go

広告主ページ

広告主ページは、ランディングページと購買ページのそれぞれを作成しています。

ランディンページ

ランディングページには、購買ページへのリンクが貼られています。

<html>
  <body>
    <h1>Advertiser Landing Page</h1>
    <a href="http://www.advertiser.com/advertiser/cvp.html">Go to conversion page</a>
    <iframe height="0" width="0" frameBorder="0" src="http://www.ad-vendor.com:8888/retarget"></iframe>
  </body>
</html>

購買ページ

購買ページでは、iframeで広告ベンダが提供するcv用のタグを呼び出しています。

<html>
  <body>
    <h1>Advertiser Conversion Page</h1>
    <iframe src="http://www.ad-vendor.com/ad-vendor/cv.html"></iframe>
  </body>
</html>

メディアと同様にpythonのwebサーバ上で動作します。

計測タグ(広告ベンダ)

計測タグは、広告ベンダのドメインの3rd Party Cookieを読み込み、検証のためにページ上に表示するようにしています。

<html>
  <body>
    <p id="tracking-cookie"></p>
    <script src="http://www.ad-vendor.com/ad-vendor/cv-tag.js"></script>
  </body>
</html>
function getCookie(cname) {
  var name = cname + "=";
  var decodedCookie = decodeURIComponent(document.cookie);
  var ca = decodedCookie.split(';');
  console.log(document.cookie);
  console.log(ca);
  for(var i = 0; i < ca.length; i++) {
    var c = ca[i];
    while (c.charAt(0) == ' ') {
      c = c.substring(1);
    }
    if (c.indexOf(name) == 0) {
      return c.substring(name.length, c.length);
    }
    return "";
  }
}

var uid = getCookie("ADUID");
var el = document.getElementById("tracking-cookie");
console.log(uid);
el.innerHTML = "tracking cookie uid:" + uid;

検証

ITP2.0の検証を行っていきます。この検証では主に従来のバージョンのITPとの差分に着目し、以下の特徴の検証を行います。

  • トラッカーの判定の改善
  • トラッカーにより付与された3rd Party Cookieの即時的な排除
  • リファラヘッダからドメイン以外の情報の排除

Storage Access APIに関しては、現在(2018/6/16)調査中です。詳細が判明し次第更新したいと思います。

検証1. トラッカーの判定の改善

TP 2.0でどのように同様にトラッカーの判定がなされているのかを検証してみます。

ITPでは、ユーザが訪問したサイトに埋め込まれたフレームや、そのサイトからロードされたタグなどのリソースを監視・収集し、トラッカーとしての機能を持つかどうかを分類することで、トラッカー判定を行います。 ITP2.0では、リソースアクセスを行わないようなリダイレクタによる追跡も対象になるなど、さらにトラッカーの判定が厳しくなっています。

トラッカーの判定

ITPでは、トラッカーとして判定する際には以下の要素を利用しているようです。*2

最後の要素に関しては、公式のブログで明言されている訳ではありませんが、ITP2.0から追加された要素で、下の図のようにタグやフレームの埋め込みを利用せず純粋なナビゲーションリダイレクトのみを利用してユーザを追跡をする場合にトラッカーと判定するために利用されていると思われます。

f:id:inchom:20180617002812p:plain

これらの要素のドメインの数が4以上となるとトラッカーとして判定されるようです。

検証手順

検証の手順としては、一般的な広告のトラッキングの流れで行います。

  1. メディアのページ(http://www.media1.com/media/)にアクセス
  2. 広告リンクをクリックして広告ベンダトラッキングサーバへ遷移(この時のCookieを付与)
  3. ラッキングサーバが広告主ページのランディングページ(http://www.advertiser.com/advertiser/lp.html)へリダイレクト
  4. 購買ページ(http://www.advertiser.com/advertiser/cv.html)へのリンクをクリック
  5. 購買ページからCVタグをロードしてCookieが参照できているかを確認
  6. メディアを変更してトラッカーと判定されるまで1-5を繰り返す

f:id:inchom:20180617005758p:plain
ITP2.0によるCookieのブロック

余談ですが、今回のケースはiframeやタグなどのリソースを貼っていないリダイレクト元ドメイン(メディア)を変更していくことになります。そのため、ITP1.1ではドメイン数が増えずトラッカーとして判定されないことは確認済みです。

メディア1(www.media1.com)

まずは、1から5までの手順を行います。

対象のドメインは、

  • www.media1.com
  • www.advertiser.com

となり、ドメイン数が2となるため、まだトラッカーとしては判定されていないようです。

f:id:inchom:20180617001244p:plain

メディア2(www.media2.com)

メディアをwww.media2.comに変更して手順6を行います。

  • www.media1.com
  • www.media2.com
  • www.advertiser.com

ドメイン数が3となるため、まだトラッカーとしては判定されていないようです。

f:id:inchom:20180617001710p:plain

メディア3(www.media3.com)

メディアをwww.media3.comに変更して手順6を行います。

  • www.media1.com
  • www.media2.com
  • www.media3.com
  • www.advertiser.com

この段階ではCookieを参照することができていますが、ドメイン数が4となるため、これ以降トラッカーとして判定されると思われます。

f:id:inchom:20180617001320p:plain

メディア4(www.media4.com)

メディアをwww.media4.comに変更して手順6を行います。

トラッカーとして判定されているため、Cookieを参照することができなくなりました。

f:id:inchom:20180617001515p:plain

この結果から、ITP2.0では判定に利用する要素のドメイン数が4以上となったときにトラッカーと判定され、純粋なナビゲーションリダイレクトも含まれるようです。

検証2.トラッカーにより付与された3rd Party Cookieの即時的な排除

ITP 1.1では、ユーザがトラッカーのドメインのページで何らかのインタラクション(クリックやテキスト入力など)を行った場合は、そのインタラクションから24時間は3rd Party Cookieの付与を許可するようになっていました。*3

f:id:inchom:20180617005441p:plain
ITP1.1でのユーザインタラクションによる3rd Party Cookieの一時的な許可

ITP 2.0では、ユーザがトラッカーのドメインのページで何らかのインタラクションを行った場合であっても、即時的に3rd Party Cookieを排除するように変更されています。

f:id:inchom:20180617005529p:plain
ITP2.0ではユーザインタラクション後も3rd Party Cookieを排除

ユーザインタラクション後も3rd Party Cookieのブロックされるか検証を行っていきます。

先ほどの検証でトラッカーとして判定された www.ad-vendor.com というドメインのページを用意します。

  • 広告ベンダインタラクションページ
<html>
  <body>
    <h1>Ad Vendor Page</h1>
    <form action="/">
      <input type="text" name="hoge"><br>
      <input type="submit" value="Submit">
    </form>
  </body>
</html>

インタラクション(テキスト入力とクリック)を行います。

f:id:inchom:20180616033848p:plain

その後、メディアページに戻って、同様の手順で購買ページへ行ってみます。 やはりCookieを取得できていません。*4

f:id:inchom:20180616223817p:plain

仕様と同様にITP2.0では、インタラクションを行った後でもCookieが即時的に排除されることが確認されました。

検証3. リファラヘッダにドメイン以外の情報を排除

ITP2.0 ではドメインがトラッカーとして判定された場合、リファラヘッダからドメイン以外の部分が排除されるようになりました。 これにより広告主ページ上にリターゲット用のタグなどを貼っていても、どのユーザがどのページを踏んだかを判定できなくなる可能性があります。

f:id:inchom:20180616032433p:plain
ITP2.0によるリファラからドメイン以外の部分の排除

実際に、広告主ページ上にiframeでリターゲット用のiframeを置き、サーバ側でリファラがどのような形で取得されるかを検証します。

  • 広告主ランディングページ
<html>
  <body>
    <h1>Advertiser Landing Page</h1>
    <a href="http://www.advertiser.com/advertiser/cvp.html">Go to conversion page</a>
    <iframe height="0" width="0" frameBorder="0" src="http://www.ad-vendor.com:8888/retarget"></iframe>
  </body>
</html>

手順は、検証1で行ったやり方と同様に、トラッカーと認定されるまでリダイレクトを繰り返します。 トラッカーとして判定された時に、サーバ側に送られる情報を調べます。

結果としては、トラッカーとして判定された瞬間から、リファラヘッダからドメイン名以外の部分を排除することが確認されました。

f:id:inchom:20180616031239p:plain
取得可能なリファラ

この部分に関しては、サーバ側に送られるリファラヘッダが削られるだけであるため、リクエスト内のURLパラメータにリファラを含めて送信することで回避可能かと思います。

おわりに

今回は、6月4日に公開されたITP2.0について、調査・検証を行いました。

結果として、ITP2.0では以下のものが更新されていることが分かりました。

  • 判定に利用する要素ドメイン数が4以上となったときにトラッカーと判定され、純粋なナビゲーションリダイレクトも含まれる
  • ユーザがトラッカーのドメインのページで何らかのインタラクションを行った場合であっても即時的に3rd Party Cookieを排除する
  • トラッカーとして判定された瞬間からリファラヘッダからドメイン名以外の部分を排除する

所感ですが、ITP2.0ではより3rd Party Cookieに対する制約が厳しくなったものの、これまでに1st Party Cookieを利用した形式に計測を変更しているベンダーの場合は、それほど影響が大きくないのではないかと感じました。

それよりもあまりクローズアップされていないですが、リファラヘッダからドメイン以外の部分を排除する修正は、気づかない内に配信ボリュームに影響を与える可能性があるため注意が必要かと思います。

Storage Access APIに関しては現在調査中ですが、詳細が判明し次第アップデートをする予定です。

今回の検証内容に関するコメントや質問がありましたら、下のコメント欄に書いていただけたらと思います。

*1:機能のより詳細な情報については、Intelligent Tracking Prevention 2.0 | WebKitを参照してください。

*2:ITPの仕様と挙動について、あまり知られていないことを簡単に整理する – マーケティングメトリックス研究所/MARKETING METRICS Lab.のサイトを参考にさせて頂きました

*3:ここでのインタラクションはページのリダイレクトや表示は含まれません。

*4:ITP1.1では同様の手順でCookieを取得できるようになることを確認済みです。

作りながら学ぶ広告サーバ - DSP その2

はじめに

DSPを作りながら詳細について解説しております.

前回は,RTBとDSPの概要,インフラ設計,データ設計の説明をしました.

今回は,入札機能の詳細と実装の説明をします.記事内で紹介しているコードは,Githubに公開しております.

前回のエントリーを読んでいない方は,下記からお進みください.

inchom.hatenadiary.jp

(注意)共通標準であるOpenRTBの仕様をベースに開発を進めるので,現状動いているシステムのRTBの仕様とは異なっていたり,一部私の認識が間違っている箇所もあるかもしれません,そういった場合はコメント等いただけたらと思います.

入札機能の概要

入札機能の概要について説明していきます.下図の赤枠で囲んだ部分が入札機能です.

f:id:inchom:20160507110441p:plain

DSP視点での入札機能の処理の流れになります.

  1. SSPから送信されるRTBリクエストを取得する
  2. 広告枠情報から入札するかを決めて,入札する場合は入札価格を決めてRTBレスポンスを返す
  3. SSPから落札通知が送られてきた場合は広告マークアップSSPに送信する

以下,順を追って説明していきます.

1. SSPから送信されるRTBリクエストを取得する

SSPは,ユーザがメディアを開いたタイミングで,RTBリクエストをDSPに送信します.

RTBリクエストとは

RTBリクエストとは,「このような広告枠に広告を出したい人は入札してください!ただし,最低入札単価は○○円です!」というような広告枠の情報を持った入札のリクエストです.

実際のRTBリクエストのサンプルを張っておきます.

{
    "id": "IxexyLDIIk",
    "imp": [
        {
            "id": "1",
            "banner": {
                "w": 728,
                "h": 90,
                "pos": 1
            },
            "bidfloor": 50,
            "bidfloorcur": "JPY"
        }
    ],
    "app": {
        "id": "agltb3B1Yi1pbmNyDAsSA0FwcBiJkfIUDA",
        "name": "Yahoo Weather",
        "cat": [
            "IAB15"
        ],
        "ver": "1.0.2",
        "bundle": "628677149",
        "publisher": {
            "id": "agltb3B1Yi1pbmNyDAsSA0FwcBiJkfTUCV",
            "name": "yahoo",
            "domain": "www.yahoo.com"
        },
        "storeurl": "https://itunes.apple.com/id628677149"
    },
    "device": {
        "ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 6_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3",
        "ip": "123.145.167.189",
        "model": "iPhone",
        "os": "iOS"
    },
    "user": {
        "id": "ffffffd5135596709273b3a1a07e466ea2bf4fff",
        "yob": 1984,
        "gender": "M"
    }
}
  • impフィールドには,広告枠の詳細情報が入ります.
    • 最低入札価格(imp.bidfloor)*1
    • 最低入札価格の通貨(imp.bidfloorcur)*2
    • imp.bannerフィールドは,バナー広告のみが持つフィールドで,バナーの情報が入ります.
      • バナーの大きさ(imp.banner.w, imp.banner.h)
      • 位置(imp.banner.pos)
  • appフィールドは,メディアがアプリの場合のみ持つフィールドで,アプリメディアの情報が入ります.
    • アプリのカテゴリ(app.cat)
    • バージョン(app.ver)
    • AppStoreのURL(app.storeurl)
  • deviceフィールドには,ユーザのデバイスの詳細情報が入ります.
    • アクセス元のIP(device.ip)
    • モバイルOS(device.os)
    • 位置情報(device.geo)
  • userフィールド,ユーザの情報が入ります.
    • 生まれ年(user.yob)
    • 性別(user.gender)

上記のものは,ごく一部のものですので,詳細を知りたい方はOpenRTB v2.3 Specificationをご覧下さい.

DSPは上記のような情報を参照しつつ,広告をクリックされやすい広告枠,あるいはクリックしやすいユーザーかを推定して,枠に対して入札を行います.

実装

RTBリクエストを取得・検証する部分の実装を見ていきましょう.

func parseRequest(r *http.Request) (*Request, error) {
    // Read bid request in json format
    body, err := ioutil.ReadAll(r.Body)
    if err != nil {
        return nil, err
    }

    // Unmarshal json to BidRequest
    var br openrtb.BidRequest
    err = json.Unmarshal(body, &br)
    if err != nil {
        return nil, err
    }

    return &Request{
        BidRequest: &br,
    }, nil
}

RTBリクエストの取得は,POSTリクエストでBody部分にJSON形式のデータが入ってくるため,それをパースするだけです*3

パース先の構造体は,OpenRTBの仕様に沿ったものを,こちらで公開されていたため,それをインポートする形で使っています.

Bodyのデータが読めなかったり,JSONが壊れていたり,JSONのデータがOpenRTBの仕様に沿っていない場合は,エラーが返ります.

正常に処理が成功した場合は,RequestオブジェクトにBidRequestオブジェクトを持たせて返します.

次に,取得したリクエストの検証部分です.

func (r *Request) validate() error {
    // Required attributes must be exsit
    if r.BidRequest.ID == "" {
        return errors.InvalidRequestParamError{"BidRequest.ID", ""}
    }
    if r.BidRequest.Imp == nil {
        return errors.InvalidRequestParamError{"BidRequest.Imp", ""}
    }

    // Currency type is valid
    if !r.validCurrency() {
        return errors.InvalidCurError
    }
    return nil
}

func (r *Request) validCurrency() bool {
    // currency not exist
    if len(r.BidRequest.Cur) == 0 {
        return true
    }
    // currency exist
    for _, cur := range r.BidRequest.Cur {
        if cur == consts.DefaultBidCur {
            // currency including default bid currency
            return true
        }
    }
    // currency NOT including default bid currency
    return false
}

最低限の検証として,必須フィールドが無かったり,DSPで扱っているデフォルトの通貨とは違うリクエストがきた場合は,検証エラーを返します.

実際にプロダクションで扱う場合には,ホワイトリストやブロックリストの値を見るような検証も必要かと思います.

2. 広告枠情報から入札するかを決めて,入札する場合は入札価格を決めてRTBレスポンスを返す

RTBリクエストを受け取ったDSPは,広告枠情報を参照して,配信可能な広告候補から入札するかを決定します.入札する場合は入札価格を決めてRTBレスポンスを返します.

入札の決定

1で説明した広告枠の情報を参照しつつ,入札するべきかを決めます. 入札を行うかどうかの基本的な考え方は,「枠ごとに配信可能な広告のeCPMを考慮して入札価格を計算し,最低入札単価を上回っている場合は,入札を行う」かと思います.

下図で説明すると,最低入札価格が50円となっているときに,手元の広告のこの広告枠に対してのeCPMを計算します. 結果,eCPMが最も高いものが100円ですので,この枠に対して入札することになります.

f:id:inchom:20160507122028p:plain:w400

実装

送信されてRTBリクエスト内のimpが正しいかを検証します.

func validateImp(imp *openrtb.Imp) error {
    // Native Ad is not supported
    if imp.Native != nil {
        return errors.NoSupportError{"native"}
    }
    // Video Ad is not supported
    if imp.Video != nil {
        return errors.NoSupportError{"video"}
    }
    // Check bid currency
    if imp.BidFloorCur != "" && imp.BidFloorCur != consts.DefaultBidCur {
        return errors.InvalidCurError
    }
    return nil
}

ネイティブ広告やビデオ広告はサポートしておりませんので,それらのimpだった場合は入札しないようにしています. また,impごとに設定されている入札通貨がDSPのデフォルトと異なっている場合は入札しません.

impが正しい場合は,枠ごとに入札候補(Index)となる広告を取得します.

func GetIndex(ctx context.Context, imp *openrtb.Imp) (Index, error) {
    // Get key for getting value in redis
    key, err := makeKey(imp)
    if err != nil {
        return nil, err
    }

    // Get value from redis
    cli := redis.GetConn(ctx, consts.CtxRedisKey)
    value, _ := redis.GetCmd(cli, key)
    var out Index
    err = msgpack.Unmarshal([]byte(value), &out)
    if err != nil {
        // Failed to get valid value
        return nil, err
    }
    return out, nil
}

入札候補は,枠ごとに作成され,データストアであるRedis内にMsgpackでパックされた形で保存されています.Indexのデータ構造は第一回で説明していますのでご覧下さい.

入札候補には,広告情報のリストが入っています. 具体的には,広告IDや遷移先URL,eCPMなどです.

下記,入札候補データのイメージです*4

key => value

index:banner => [ 
  {
    eCPM: 200.0,
    CampaignID: "1234",
    CreativeID: "12345",
    Nurl: "http://xxxx.xxx/xxx/win",
    Adm: ...
  },
  {
    eCPM: 180.0,
    CampaignID: "1235",
    CreativeID: "12346",
    Nurl: "http://xxxx.xxx/xxx/win",
    Adm: ...
  }
]

Redisからデータを取得できなかったり,Msgpackでアンパックできなかったときは,エラーを返します.

現状,Redisから直接取得する形になっていますが,IOコストかかるため実際のプロダクション環境ではプロセスキャッシュを間に挟む方よいです.

次に,取得した候補から一つずつ広告を取り出し,広告の検証を行います.

func validateAd(imp *openrtb.Imp, ad *data.Ad) error {
    // Check bid price greater than bid floor price
    if ad.CalcBidPrice() <= imp.BidFloor {
        return errors.LowPriceError
    }
    return nil
}

入札価格が最低入札価格を上回っていることのみを条件としています.

将来的には,ブロックされているカテゴリやドメインでないかをチェックする必要があります.

この検証を通った広告が一つ以上あれば入札することになります.

入札価格の計算

入札する場合は,入札価格を決める必要があります. 上の図のケースで,100円で入札してしまっては,DSP側に利益がでません.(もちろん推定以上にeCPMが高いケースは別ですが)

DSP側の利益を最大化することを考えると,入札できる価格で最も低く入札することが必要になります.

f:id:inchom:20160507122827p:plain:w300

入札できる価格の推定は,複数DSPが関わってくるので常に変動することになります.また,入札価格を下げては配信ボリュームがでませんし,入札価格を上げすぎては利益をつぶすことになります. まだ,私自身不勉強で入札価格をどのように決定するかは調べきれていませんが,入札価格と落札結果をフィードバックさせて決めているのではないかと妄想しています.

実装

検証に通った広告の中で,最も収益性の高い広告を選びます.

func choiceBestAd(va []data.Ad) (data.Ad, bool) {
    var revenue, maxRevenue float64
    index := -1
    for i := range va {
        revenue = va[i].PeCPM - va[i].CalcBidPrice()
        if revenue > maxRevenue {
            maxRevenue = revenue
            index = i
        }
    }
    if index == -1 {
        return data.Ad{}, false
    }
    return va[index], true
}

収益性はeCPMから入札価格を引いた値として,広告の中で最も値の大きいものを選択します.

入札価格の計算は,かなり簡易的なものにしています.

func (ad *Ad) CalcBidPrice() float64 {
    return ad.PeCPM * (1.0 - consts.RevenueRatio)
}

eCPMに対する収益比率を決めて,その分を差し引いて入札するだけです.

例えば,収益比率が10%とした場合には,eCPMが100円だったときに90円で入札して10円は収益になる,という感じです.

eCPMの推定

今回はあまり深く触れませんが,重要な話として,eCPMをどのように推定するかを考える必要があります. eCPMの推定の精度が低いと,入札すべきかの判定や入札価格を決めるのが難しく,また,収益性を担保することが難しくなります.

eCPMの推定は,基本的には過去の広告枠の配信ログからクリック率を推定し,計算することになるかと思います. バッチで定期的に計算されているものが配信候補リスト内に入っていることを前提としています.

実装

配信ログとして,RTBリクエストに対して何の広告を返したかの情報を作成し,fluentd経由で送るようにしています.

func sendLog(ctx context.Context, req *openrtb.BidRequest, resp *openrtb.BidResponse) {
        fluent.Send(ctx, consts.CtxFluentKey, "request", makeRequestLog(req, resp))
        for _, bid := range resp.SeatBid[0].Bid {
                fluent.Send(ctx, consts.CtxFluentKey, "bid", makeBidLog(&bid))
        }
}

func makeRequestLog(r *openrtb.BidRequest, br *openrtb.BidResponse) map[string]interface{} {
        return map[string]interface{}{
                "request_id": r.ID,
                "cur":        br.Cur,
                "bid_num":    len(br.SeatBid[0].Bid),
        }
}

func makeBidLog(bid *openrtb.Bid) map[string]interface{} {
        return map[string]interface{}{
                "bid_id":      bid.ID,
                "imp_id":      bid.ImpID,
                "price":       bid.Price,
                "ad_id":       bid.AdID,
                "campaign_id": bid.CID,
                "creative_id": bid.CrID,
        }
}

BidRequestとBidResponseから,「どの枠に対してどの広告をいくらで入札したか」のログデータを構築しています.

eCPM推定の精度を上げるためには,メディア情報やユーザ,デバイス情報などもログするようにしていく必要があるかと思います.

RTBレスポンスの返送

RTBレスポンスは,RTBリクエストに対して入札するかどうか,入札する場合はどんな広告をいくらで入札するかといった情報を持ちます.

以下に簡単サンプルを張っておきます.

{
    "cur": "JPY", 
    "id": "IxexyLDIIk", 
    "seatbid": [
        {
            "bid": [
                {
                    "adid": "1234", 
                    "adm": "<a href=\"http://test.noadnolife.com/v1/click/12345?impid=${AUCTION_IMP_ID}&price=${AUCTION_PRICE}\"><img src=\"http://test.noadnolife.com/img/12345?impid=${AUCTION_IMP_ID}&price=${AUCTION_PRICE}\" width=\"728\" height=\"90\" border=\"0\" alt=\"Advertisement\" /></a>", 
                    "cid": "1234", 
                    "crid": "12345", 
                    "id": "ed5dd1c0-05f1-4166-9370-4b07864d1136", 
                    "impid": "1", 
                    "iurl": "http://test.noadnolife.com/img/12345.png", 
                    "nurl": "http://test.noadnolife.com/v1/win/12345?impid=${AUCTION_IMP_ID}&price=${AUCTION_PRICE}", 
                    "price": 225
                }
            ]
        }
    ]
}
  • idフィールドにRTBリクエストIDが入ります
  • seatbid.bidフィールドに入札情報が入ります
    • キャンペーンID(seatbid.bid.cid)
    • クリエイティブID(seatbid.bid.crid)
    • インプレッションID(seatbid.bid.impid)
    • 落札通知の送信用URL(seatbid.bid.nurl)
    • 広告マークアップ(seatbid.bid.adm)
    • 入札価格(seatbid.bid.price)

SSPに対して,「どの枠にどの広告をいくらで入札します」ということを上記の情報を送ることで実現します.

実装

入札した結果取得した広告が一つ以上あれば,BidReponseの構造体にマッピングします.

func makeBidResponse(br *openrtb.BidRequest, ads []*data.Ad) *openrtb.BidResponse {
        sb := makeSeatBid(ads)
        return &openrtb.BidResponse{
                ID:      br.ID,
                SeatBid: sb,
                Cur:     consts.DefaultBidCur,
        }
}

func makeSeatBid(ads []*data.Ad) []openrtb.SeatBid {
        bid := make([]openrtb.Bid, len(ads))
        for i, ad := range ads {
                bid[i] = *makeBid(ad)
        }
        return []openrtb.SeatBid{
                openrtb.SeatBid{
                        Bid: bid,
                },
        }
}

func makeBid(ad *data.Ad) *openrtb.Bid {
        return &openrtb.Bid{
                ID:    uuid.NewRandom().String(),
                ImpID: ad.ImpID,
                Price: ad.CalcBidPrice(),
                AdID:  ad.AdID,
                CID:   ad.CampaignID,
                CrID:  ad.CreativeID,
                NURL:  ad.NURL,
                IURL:  ad.IURL,
                AdM:   ad.AdM,
        }
}

レスポンスのhttpヘッダーに,OpenRTB用の拡張ヘッダーを入れておき,BidResponseのオブジェクトをJSONに書き換えてレスポンスを送ります.

func WriteResponse(w http.ResponseWriter, resp interface{}, code int) {
        w.Header().Set("Content-Type", "application/json; charset=utf-8")
        w.Header().Set("x-openrtb-version", "2.3")
        w.WriteHeader(code)
        json.NewEncoder(w).Encode(resp)
}

OpenRTB用拡張ヘッダー

X-Openrtb-Version: 2.3

一つも広告が無い場合は,ステータスコード204を返します.

3. SSPから落札通知が送られてきた場合は広告マークアップSSPに送信する

リクエストの取得

2で返送したRTBレスポンスが落札された場合,SSPから落札通知(WinNotice)が送られてきます.

送信先のエンドポイントは,RTBリクエスト内のseatbid.bid.nurlに定義します.そこで定義しているurlとパラメータでリクエストが送信されてきます.

今回は,以下のようなnurlを定義しています.

http://test.noadnolife.com/v1/win/12345?impid=${AUCTION_IMP_ID}&price=${AUCTION_PRICE}

nurl内には,マクロを定義することができ,SSPが置き換えたものを送信してきます.

詳細は,OpenRTBv2.3の仕様の4.4に書かれていますので気になる方は参照してください.

実装

送られてくるパラメータを取得,検証します.

func parseRequest(ctx context.Context, r *http.Request) *Request {
        wonPriceStr := r.FormValue("price")
        wonPrice, err := strconv.ParseFloat(wonPriceStr, 64)
        if err != nil {
                wonPrice = 0.0
        }

        impID := r.FormValue("impid")
        creativeID := kami.Param(ctx, "crid")

        return &Request{
                WonPrice:   wonPrice,
                CreativeID: creativeID,
                ImpID:      impID,
        }
}

func (r *Request) validate() error {
        if r.WonPrice <= 0.0 {
                return errors.InvalidRequestParamError{"price", fmt.Sprintf("%f", r.WonPrice)}
        }

        if r.ImpID == "" {
                return errors.InvalidRequestParamError{"impid", r.ImpID}
        }

        if r.CreativeID == "" {
                return errors.InvalidRequestParamError{"crid", r.CreativeID}
        }
        // validation ok
        return nil
}

crid,impid,priceにそれぞれ値が存在し,落札価格が0より大きい場合は有効な落札通知と判定しています.

広告マークアップの返送

WinNoticeを受け取ったDSPは広告マークアップを返します*5.広告マークアップとは,広告を表示するためのマークアップ情報です.

実装

今回は,広告マークアップはRTBレスポンスに含めているため,送らないこととしています.

utils.WriteResponse(w, map[string]interface{}{"message": "ok"}, 200)

ステータスコード200で{"message": "ok"}のみ返します.

落札価格のフィードバック

落札価格は,一般的に二つの方法のいずれかで決められています.

  • ファーストプライスオークション
    • 最も高い入札価格が落札価格となる
  • セカンドプライスオークション
    • 2番目に高い入札価格(+1円)が落札価格となる

どちらの方法でオークションが行われているかは,RTBリクエストのatフィールドで決められます(デフォルトはセカンドプライスオークション).

いずれのオークションであれ,落札できた入札価格をログしておき,次の入札を最適化する必要があります.

実装

fluentd経由で落札したクリエイティブ,インプレッションID,落札金額のログを送ります.

fluent.Send(ctx, "fluent", "win", map[string]interface{}{
     "WonPrice":   request.WonPrice,
     "CreativeID": request.CreativeID,
     "ImpID":      request.ImpID,
})

集計時に,入札時のログのImpIDと落札通知のImpIDを結びつけて,入札価格と落札価格の差や,入札成功,失敗のフィードバックをかけていきます.(現状はその機構は存在しないのでとりあえず溜めていきます)

その他

eCPMを推定するためには,クリック率が測定できる必要があるためクリックログを保存する必要があります. また,CVRを計測するためには,コンバージョンログを保存する必要があります.

上記は入札機能に付随するものとして今後どこかで説明したいと思います.

おわりに

長くなってしまいましたが,DSPの入札機能の実装について説明しました.記事内で紹介しているコードは,Githubに公開しております.

今回説明した内容は最小限のものですので,実際にはより高精度にeCPMを推定するためや入札価格を決めるため,より細かな情報や高度なアルゴリズムを使う必要があるかと思います.

実装に関しても,非同期にすべき箇所やキャッシュ化すべき箇所などがありますが,簡易的な実装として理解していただけたら幸いです.

次回は,構築について説明していきます.

参考

RTBリクエストの簡単な表(一部)を作りました.

属性 説明
id string; 必須 ビッドリクエストに付与されるユニークなID
imp obeject array; 必須 インプレッション情報; 1要素以上必須
site object; 推奨 メディアのWebサイトの情報; Webメディアのみ適用可能/推奨
app object; 推奨 メディアのアプリの情報; アプリメディアのみ適用可能/推奨
device object; 推奨 インプレッションするユーザのデバイスの詳細情報
user object; 推奨 ユーザ情報
test integer; default 0 テストモード(課金不可)のフラグ; 1=テストモード
at integer; default 2 オークション種別; 1 = 1stプライスオークション, 2 = 2ndプライスオークション
tmax integer 入札のタイムアウト時間(ms)
wseat string array 入札可能なバイヤーのホワイトリスト
cur string array 入札用の通貨の配列; ISO-4217のコードを使用
bcat string array ブロックする広告主のカテゴリ
badv string array ブロックする広告主のドメイン
  • impオブジェクトのフィールド (一部)
属性 説明
id string; required インプレッションに付与されるビッドリクエスト内で一意のID
banner object バナー広告枠情報
native object ネイティブ広告枠情報
video object ビデオ広告枠情報
instl integer; default 0 1の場合は全画面あるいはインタースティシャル広告; 1のときはインタースティシャルではない
tagid string 特定の広告の位置を示すタグID
bidfloor float; default 0 最低入札価格
bidfloorcur string; default "USD" 最低入札価格通
  • bannerオブジェクトのフィールド (一部)
属性 説明
w integer; recommended 広告枠の表示横幅
h integer; recommended 広告枠の表示縦幅
pos integer 広告枠の表示位置
btype integer array ブロックするバナータイプ
battr integer array ブロックするクリエイティブ
api integer array サポートするAPIフレームワーク

*1:CPMベースで1000インプレッションあたりの単価が入ります

*2:最低入札単価はISO-4217で決められているアルファベットで指定されます

*3:go標準パッケージのjsonはクソ説がありますが,今回は簡単に作っているので使っています.気になる方はより速いffjsonを使うのがよいかと思います

*4:実際にはMsgPackでパックされた状態で入っています

*5:2のRTBレスポンスに含めることもできるため,その場合は送る必要はありません

作りながら学ぶ広告サーバ - DSP その1

はじめに

今回は,近年の広告業界に定着したRTB(Real Time Bidding)について説明しようと思います.

残念ながら筆者はRTBの広告サーバを作ったことがないのですが,GW休みで暇なので詳細まで皆さんと一緒に勉強していきたいので,実際にRTBのシステムを作りながら解説していこうと思います.

RTBの広告サーバだけでなく広告サーバ共通で気を付けるポイントもあるので,その辺りにも触れていきます.(大雑把な話は以下のエントリにまとめていますので,興味のある方はご覧下さい)

inchom.hatenadiary.jp

今回は,RTBシステムの広告主側プラットフォームであるDSP (Demand Side Platform)について説明していきます.

(注意)共通標準であるOpenRTBの仕様をベースに開発を進めるので,現状動いているシステムのRTBの仕様とは異なっていたり,一部私の認識が間違っている箇所もあるかもしれません,そういった場合はコメント等いただけたらと思います.

RTBシステムとは?

RTBシステムとは,広告枠の売買を目的としたシステムです. 具体的には,ユーザがメディアのページを表示しようとしたときに,そのページに表示する広告枠に対して入札を行い,最も高くその広告枠を買った広告サーバの広告を表示するシステムです.

ユーザがメディアのページを表示を開始してから表示が完了するまでの1秒足らずの間に入札を行いますから,一般的な入札のように人が落札している時間なんてありません.(熟練の広告運用担当者はそれをやっているという噂はありますがw)

ではどのように実現しているのでしょうか? RTBシステムの概要図を使って説明します.

f:id:inchom:20160430184724p:plain

  1. ユーザが広告タグが張ってあるメディアのページを踏みます
  2. メディアのページから広告リクエストがSSP(Supply Side Platform)に対して飛びます
  3. SSP複数DSPに対して広告枠の入札リクエスト(RTBリクエスト)を送ります
  4. DSPは広告枠の情報から入札するかを決めて,入札する場合は入札価格を含めてRTBレスポンスを返します
  5. SSPは最も高く入札したDSPに対して落札通知を行います
  6. 落札通知を受け取ったDSPは広告の表示情報(広告マークアップ)を送ります*1
  7. SSPはメディアに対して広告表示情報を返します
  8. ユーザに広告が表示されます

上記2-8までの手順がメディアのページを表示するまでの間に行われます. このように,広告枠が1インプレッション単位で逐次入札されるため,Real Time Biddingと呼ばれます.

上記を実現する仕組みがRTBシステムです.

DSPとは?

DSPとは,SSPから送られた広告枠の入札リクエストに対して,自分の広告と広告枠の情報を考慮して入札すべきかどうか判断するプラットフォームです.

DSPは一般的に以下のような機能を持ちます.

広告枠入札機能

SSPから与えられた広告枠情報から広告枠を入札する機能です.収益性の高い広告枠を安価に入札することが目的になります. そのため,メディアやデバイス,ユーザと配信可能な広告との親和性を考慮して,可能な限りクリックやコンバージョンしやすい広告枠をできるだけ安く入札することになります. 以前紹介したターゲティングの技術も積極的に使われます. 一般的に50msec程度の応答性能を求められるため,大量のデータを高速に処理できるシステム設計.データ設計が重要になります.

広告入稿機能

広告主や代理店が広告を入稿する機能です. 予算や単価,ターゲティング用の設定などを行うことができます. 初期の広告サーバはこのような機能は持たずメールベースで入稿する(エンジニアがDBに入れる)ケースもあります.使いやすいUI/UXにする必要があるため,作り込むと非常に時間がかかる機能です.

広告効果レポート機能

広告主や代理店が入稿した広告の効果を閲覧する機能です. 1コンバージョンあたりに使われた予算を示すCPAやクリック率やコンバージョン率を表示することができます. レポートは大容量のログデータを高速に処理するための基盤作りやUI/UX設計が必要になるため,こちらも時間のかかる機能になります.

全ての機能を作ろうとすると時間的に終わらないので,今回はコアの機能である広告入札機能に焦点を当てて開発していきますが,データ設計やインフラ設計はその他の機能を考慮したものにします.

DSPの開発

前置きが長かったですが,これから実際にDSPを作っていきたいと思います.

ベースとなるRTBの仕様は,OpenRTB v2.3を参照します.こちらに仕様のPDFがあります.

開発は,以下の手順で進めていきます.

  • システム設計
  • データ設計
  • 実装
  • システム構築

全てやっているとかなり長くなってしまうため,今回は,データ設計まで説明します.

システム設計

一般的な広告サーバの設計方針に併せて以下のようなことを意識しながら設計を行っていきます.

  • 大量リクエストを高速に処理できる
  • 停止しない
  • 障害復旧が早い
  • 収益性の高い広告を選択できる

今回は,AWSの上で動くシステムを前提としました.

f:id:inchom:20160501102805p:plain

DSP API Server

広告枠の入札を行うサーバです. このAPIサーバはロードバランサに接続され,リクエストが増えるとAPIサーバを増やして対応することを想定しています.(一般的なAPIサーバと同じです)

Redis

配信可能な広告データを持ちます. 処理性能を考慮してオンメモリ型のKVSであるRedisを参照します. Redisに対してWriteをしないようにすることで,リクエストが大量に増加した場合も,Redis Slaveを増やすことで対応できるためスケールするようになります.これによって大量リクエストを高速に処理できるになります.

ただ,Redisのマスターサーバが単一障害点になり得るため,AaroSpikeのような分散KVSを使ったり,LevelDBやKyoto TycoonなどのFile型KVSを各APIインスタンスが保持する方法でもよいかと思います.(今回は実装コストを考慮してRedisを採用しています.ElastiCacheを使えばフェイルオーバーをしてくれるため,無停止ではないにしろ障害復旧は早い設計にしています)

S3

配信ログを保存します. APIサーバ側から送られてくる配信ログ(入札,クリック,コンバージョンログなど)を定期的に受け取り,保存します. ログ集計のためBackendサーバ側から定期的に読み込まれます.

DSP Backend Server

配信可能な広告データの更新,配信レポートの更新を行います. 配信可能な広告データの更新には,MySQLに存在する広告データを取得し,S3の配信ログ(あるいは配信レポート)を読み込んで予算超過しているものを排除して,Redisにある配信可能広告リストを更新します.

配信レポートの更新には,S3の配信ログを集計して広告主や代理店が必要とする広告配信結果をMySQLに保存します.

処理速度の観点から配信量が増えるとS3から直接読み込むのは難しくなると思われるため,懐に余裕があればRedshiftやBigQueryを間に挟んだ方がよいかと思います.

MySQL

広告主や代理店から入稿された広告情報,また,広告の配信結果を保持します. 広告情報は,クリエイティブの情報や予算や単価などを保持します. 広告の配信結果は,キャンペーン別,クリエイティブ別,セグメント別などでインプレッション数,クリック数,CTR,CVR,CPAなどを保持します.

CDN (Cloud Front)

S3に保存された広告のクリエイティブを保持します. 今回はCloud Frontにしていますが,配信量が増えるとシステムコストとして効いてくるのがこの部分のため,実際には他のCDNとコストや安定性などから検討するのがよいかと思います.

データ設計

DSPで使用するデータの設計を行っていきます.今回は,広告入札機能の実装がメインであるため,DSP API Serverで扱うデータ型を決めていきます.

DSP API Serverで扱うデータ型には以下のものがあります.

  • RTBの応答に使うデータ型
  • DSP API Serverで内部的に扱うデータ型

RTBの応答に使われるデータ型は,OpenRTBの仕様に従う形になります. そのため,内部的に扱うデータ型のみ仕様を決めます.

DSP API Serverで内部的に扱うデータ

DSP API Serverで内部的に扱うデータは仕様を極力小さくするため以下に絞りました.

  • 広告配信候補リスト
  • 広告データ

詳細を下で説明します.

予算消化を厳密に管理するためには予算上限や消化予算などのデータを持たせた方がよいですが,今回は実装コストやシステムの複雑性を考慮して,広告配信候補リストにあるものは配信可能というようにしています.(集計バッチが高頻度動くことを期待している)

広告配信候補リスト

Indexと呼ばれる,どの枠にどの広告を出すかを持つデータです.

詳細は,広告の基本 - 広告サーバの機能 - NO AD NO LIFE広告配信候補リストの作成の部分で触れています.

Indexは,配信可能な広告データのリストを所持しています. Indexはリスト型であるためMessagePackでパッキングした後のデータを入れておきます.RedisのSET型を使うこともできますが,RedisにロックインされないためにKey => Valueの形式を保つようにしています.

(Key => Value)

index:<view> => Index
項目 内容
view どの広告クリエイティブの種類かを示す.バナーやネイティブ,動画などを判別する. banner, banner_rect, native

ここで定義した項目は最低限のものなので,実際にはさらに細かく作っていく必要があります.

広告データ

広告データは,広告の入札,表示,クリック等をするために必要な情報です.

項目 内容
CampaignID 広告キャンペーンID 123
CreativeID 広告クリエイティブID 1234
Price 単価 30
NURL 落札通知を受けるURL
IURL 広告の画像URL.広告の品質・安全性のチェックに使われる
AdM 広告マークアップ
Adomain ブロックするドメイン
PeCPM eCPMの期待値

ここで定義されているBidPriceを見て,最低入札単価を満たしているものがあれば,対象の広告枠に入札するようにします. また,PeCPMにeCPMの期待値を入れておき,広告効果を考慮した入札ができるようにしておきます.

その他のデータ

FQやリタゲ用のデータなども将来的には必要だと思いますが,時間的に実装できないので排除しています.

おわりに

今回は,RTBシステムについての概要,DSPの概要,システム設計,データ設計について説明しました. 次回は,実装,システム構築を行っていきたいと思います.

*1:広告の表示情報は4のRTBレスポンスに含めることができるので,その場合は送る必要はありません.

広告の基本 - ターゲティング

はじめに

今回は,広告の収益性を高めるために重要な位置付けとなるターゲティングの各手法についてまとめたいと思います.

ターゲティングとは?

ターゲティングとは,ユーザと広告のマッチングです. つまり,ユーザと相性の良い広告を見つけることです.相性が良いとは,興味を持ちやすくクリックやコンバージョンをしやすい(=効果の良い広告)ということになります.

直感的な例を示します.

あなたがより良い効果の広告を,どこかの知らない人に見せたいと思ったときにどちらの広告を選ぶでしょうか?

(1) 化粧品の広告

(2) ゲームアプリの広告

選べない人が多かったのではないでしょうか?あなたは,その人がどんな人なのかを知らないからです.

それでは,ある人が実は男性だと分かりました.その場合はどうでしょうか?

おそらく,殆どの人は(2)を選択したはずです.男性の場合,一般的に化粧品よりもゲームアプリに興味を持つ場合が多いからです.

ですが,実はその男性があるショッピングサイトで,同じ種類の化粧品をカートに入れたけれど購入していなかったことが分かりました.その場合はどうでしょうか?

今度は,(1)を選択するはずです.

このように,ユーザの情報を集めて,より興味を持ちやすいと思われる広告を選択する手法がターゲティングです.

ターゲティングの種類

ターゲティングには,行動履歴やユーザの属性を使うものなど様々な手法が存在しています.

今回はその中でも代表的なものとして以下の手法を解説していきます.

  • リターゲティング
  • 属性ターゲティング
  • オーディエンスターゲティング
  • コンテンツターゲティング

リターゲティング

リターゲティングとは,広告主のサイトの閲覧履歴のあるユーザに,閲覧した商品と関連する広告を配信したり,検索エンジンで検索したワードに関連する広告を配信するものです.

サイトの閲覧履歴を使うものをサイトリターゲティング,検索エンジンを使うものサーチリターゲティングと呼びます.

例えば,ショッピングサイトで化粧品を閲覧したユーザにその化粧品の広告を出したり,検索エンジンでゲームアプリを検索したユーザにそのゲームアプリの広告を出すといったものです.

最近では,アプリをインストールした後に一定期間起動していないユーザに対してターゲティングを行うリエンゲージメント広告もリターゲティングの一種とされています.

この手法の利点は,購買意欲が高いユーザを厳選できるため,クリックやコンバージョンされやすく効果が高い点です.(リターゲティングを導入することでCVRが数倍になるケースも存在します)

欠点は,閲覧履歴や検索履歴があるユーザにしぼられるため配信ボリュームが出ない場合があるという点です.また,カートに商品を入れていたようなユーザは広告を出さなくても購入していた可能性もあり,効果が過度に評価されることもあります.さらに,しつこく同じ商品が出続けることも多いため,ユーザに嫌悪感を与えてしまう可能性もあります.

属性ターゲティング

ユーザの年齢,性別といった情報(デモグラフィックデータ)を元にクリックしやすそうな広告を配信することを,属性ターゲティングと呼びます.

属性情報は以下のようなものがあります.

  • 年齢
  • 性別
  • 地域
  • 所得
  • 職業

こういった情報を使うことで,育毛剤の広告を40代以上の男性に配信したり,東京の賃貸マンションの広告を東京に住んでいるユーザに配信したりするといったことが可能になります.地域データを使う場合は,ジオグラフィックターゲティングと呼ばれます.

また,近年では,ユーザのリアルタイムの位置情報を取得して,付近に存在する店の広告を配信する技術も存在しています(この分野のことはO2Oと呼ばれています).

これらのユーザの属性は,例えばFacebookなどのSNSで入力されたプロフィール情報を使用したり,アンケートなどから取得する場合もあります. 現状では,CookieやデバイスID(IDFAやAdvertisingID)とそれらの属性情報を紐づけることで,ターゲティングを実現しています.

利点は,直感的であるため,広告主自身が配信設定できる場合が多いことや,リターゲティングなどと比較して配信ボリュームを出しやすい点が挙げられます.

欠点は,属性情報だけでは広告に関心があるかを判断できない場合が多いことや,属性情報を取得してユーザと紐付けることが難しいことなどが挙げられます.

オーディエンスターゲティング

ユーザのオンライン上での行動履歴などから効果の良い広告を配信することを,オーディエンスターゲティングと呼びます.

例を示します.EC2サイトやブログなど複数のサイトに股がってユーザの行動履歴を収集し,その行動履歴に従ってユーザをいくつかのグループに分割します.それらのグループごとに興味があると思われる広告を配信する手法です.

このグループは,例えばファッションに興味のあったり,あるいはスポーツに興味のあるグループなど,粒度は様々ですが,あるものに関心のあるユーザをまとめたものになります.このグループのことを,一般的にセグメントと呼びます.

この手法は,リターゲティングや属性ターゲティングと併せて活用されることもあります.

利点は,リターゲティングと比較して,オンライン上での行動履歴を横断できるので直近のユーザの欲求を反映できることです.

欠点は,配信ボリュームが出ない場合があることや,複数のシステムの連携が必要になるため複雑になることが多い点です.

コンテンツターゲティング

コンテンツターゲティングとは,Webコンテンツに併せた広告を配信するターゲティング手法です.

例えば,料理のブログに,そこで紹介されている調味料の広告を出すことでユーザの興味を引くといったものになります.

Webコンテンツを予め解析しておき,文字列やタグ情報から類似度の高い広告を配信することで実現します.

利点は,サーチリターゲティングと相性が良いため組み合わせることで高い効果を得られやすいという点です.

欠点は,予めコンテンツを解析する必要性があることや,同じページに来るユーザでも興味が違う場合も多いため,リターゲティングと比較して効果が得られにくいという点です.

ターゲティングは万能ではない

ターゲティングは,ユーザに興味がありそうな広告を選択して配信できるため,うまくハマった場合には高い効果が得られやすいです.そのためか,過剰にターゲティング手法にスポットライトが当てられているという感覚があります.

しかし,ターゲティングは実際のところ,広告配信可能なユーザに対して配信可能な広告の中から最適と思われるものを選択する,ということに過ぎません.

つまり,広告配信可能なユーザの数が少なかったり,配信可能な広告の数が少ないと,精度が高かったとしても,「配信ボリュームが全然出ない」「最適な広告が存在しない」,というケースが往々にして存在します.

広告ユーザ数を確保することや広告数を確保することは,ターゲティングの精度を高めること以上に効果に重要な影響を及ぼす場合が多いため,ターゲティング手法にとらわれず,まずはユーザ(枠)や広告数の確保に力を入れることが必要だと感じています.

おわりに

今回は,ターゲティング手法についてまとめました.

それぞれの手法に,利点や欠点が存在するため,配信可能ユーザや配信可能広告数を考慮して最適な手法を選択する必要があるかと思います.

広告の基本 - 広告サーバの機能

はじめに

今回は,どのように広告配信が行われるのかについて,主な機能の紹介とともにまとめました. 広告サーバの目的については,以下のエントリを確認してください.

inchom.hatenadiary.jp

広告サーバの定義

前回のエントリで広告サーバの定義ができていなかったため,ここで広告サーバの定義を明確にしておきます. 広告サーバとは,広告を入稿でき,広告を配信でき,配信記録をレポートできるシステム(サーバ群)のことを指しています.

より具体的には,広告管理画面,配信リスト作成サーバ,広告配信サーバ,ログ集計サーバをまとめたシステムとして定義しています.

広告配信の流れ

下図に広告が入稿されてから配信記録がレポートされるまでのもっとも基本的な流れをまとめています.

f:id:inchom:20160214194318p:plain

以下に,それぞれについて細かく説明していきます.

広告を入稿

広告主あるいは代理店は,配信したい広告を広告管理画面を通して入稿します. ここで設定する情報は一般的に以下のようなものになります,

  • 広告名
  • 課金種別(インプレッション課金/クリック課金/コンバージョン課金)
  • 単価
  • 遷移先URL
  • 広告タイトル文
  • 広告説明文
  • 画像URL

上に挙げたものは最低限のものですので,実際にはターゲティング用の設定や,その他広告サーバごとに個別の設定をすることができます.

広告を入稿すると,コンバージョンタグと呼ばれるものが発行されます. コンバージョンタグは,コンバージョンポイントと呼ばれる商品購入時などに表示されるページ(「購入ありがとうございました」というようなページ)に貼付けることで,タグ経由でコンバージョンの計測ができます.

広告配信候補リスト作成/更新

入稿された広告情報を参照して,一定時間おきに広告配信候補リストの作成/更新が行われます. 広告配信候補リストとは,下図のように,入稿された広告の集合から,どのメディアのどの枠,どのセグメントのユーザに何の広告を配信するかを定義したものです.(Indexと呼ばれることもあります)

f:id:inchom:20160214212159p:plain

このような処理をする理由は,広告配信サーバは極めて高速に広告を選択/配信する必要があるため,可能な限り配信サーバ側で処理をさせないようにするためです. 配信サーバ側でどのメディアに配信するかを広告情報や統計データから逐一計算するのは計算コストが高すぎるため,この処理を間に挟まなければ最大でも50msec程度の応答性能が必要な配信サーバでは全く使い物にならないものになってしまいます.

この処理は,広告サーバの肝の部分でありシステム全体の性能に極めて大きな影響を及ぼします.

処理性能という意味では,リストのデータ構造が複雑であったり,複数のデータを読み込まなければ最終的な結果が分からない,などとなってしまうと配信サーバ側の計算コストが上がってしまい,応答遅延に繋がります.反対に,データ構造を簡潔にしすぎると,枠やセグメントのパターンごとにデータ量が倍々で増えていってしまい,データ容量を圧迫するといった問題が起きる可能性があります.

広告自体の収益性という意味では,統計データの更新頻度が遅かったり,データ量が足りない,あるいは,収益性の予測ロジックの精度が低かったり,そもそも配信候補リストに載せづらい設計になっている場合は,広告枠やユーザに対して収益性の高い広告を選択できません. 統計データの更新頻度は理想的には5分以内にしたいところですが,これはシステム全体のサイクルを5分で回すことと同義であるため,リリース後に頻度を高めるのはなかなか難しいです.そのため,システム設計時に慎重に検討する必要があります.

広告配信

広告配信では,広告候補リストを参照して,広告配信サーバが,枠やユーザごとにどの広告を返すかを選択します. ここは,可能な限り処理をシンプルにすることや,並列処理可能にすることで,処理性能を高める必要があります. また,重要な点として,処理性能が追いつかない場合には,サーバの台数を増やすことで簡単に対応できるような設計にしておく必要があります.

消化予算やユーザ情報(フリークエンシーなど)といったどうしてもリアルタイムで参照する必要性がある情報に関しては,ボトルネックならないように注意し,プロセスキャッシュや同一サーバ内にオンメモリ型のDBを立てるなどして頑張りましょう.

また,配信が絶対に止まらないように,単一障害点をなくしていくことも必要になります. 具体的には,データストア周りを冗長化させておいたり(大抵の問題はデータストア周りで起きます),資金や体力的に余裕があれば常に別系統のシステムをスタンバイさせておくなどといった対処をするのが理想的です.

同様に,配信に問題が起きた場合に,問題が起きたことに気づけるようにしたり,どこが問題なのかをすぐに発見できるようにするツール群の整備もしておく必要があります.

統計データを更新

ここでは,ユーザの課金行動(インプレッション,クリック,コンバージョン)のログデータを集計します. ここでの統計データとは,主に二つのデータのことを指しています.

一つは,管理画面のレポート用に,広告ごとのインプレッション数,クリック数,コンバージョン数などといったものの集計データです.これは,主に広告主や代理店が広告の効果を把握するために使われます.より具体的には,効果の悪い広告を停止して,新しい広告を入稿したり,広告の良い広告の単価を上げることで配信量を増やすなどといったことに使われます.ここでの効果は,CTR(クリック率)やCVR(コンバージョン率),CPAなどで測られることが多いため,これらの情報も集計されます.

また,コンバージョン数が増えると単価が上がり,収益性が高まることから,コンバージョンログを可能な限り多く集め,クリックなどに紐付けるように工夫する必要があります. 間接コンバージョンを取る仕組みを作ったり,オフラインの購買データを取得できるようにすることで,測定可能なコンバージョンを増やすことができます.

もう一つは,上記よりもより細かい統計データです.広告単位だけでなく,どのセグメントのユーザやコンテンツ,あるいはより細かい単位で,効果測定を行うために使用されます.このデータは,広告配信候補リストを作成/更新するために使われます.管理画面のレポートとは,全く別系統の集計システムで動作している場合もあります.

レポートを閲覧

広告主や代理店は,管理画面上の配信結果のレポートを閲覧することで,広告の効果を把握します. 広告の効果がコストに見合わない場合は,対象の広告の配信を停止し,別の画像や文言を使った広告を再度入稿するなどといったことをします. 一般的に,広告入稿の頻度は高い方が良いとされ,管理画面のユーザビリティやレポートの更新頻度が,この頻度に影響するため,力を入れる必要がある部分でもあります.

おわりに

今回は,どのように広告配信が行われるのかについて,主な機能の紹介とともにまとめています.

前回エントリで挙げた重要な要素をより具体的に書くと,以下のようになります.

  • 収益の機会損失を失くすこと
    • (ほぼ)絶対に止まらない
    • 障害復旧が早い
    • 高速な配信
  • 収益性の高い広告の配信
    • 統計データの更新頻度を高める
    • 収益性の予測精度を高める
    • 広告入稿の頻度を高める
  • 高精度の成果計測
    • 取得可能なコンバージョンログの数を増やす

実際には,全てを実現するのは,資金的,時間的に難しいと思うので,優先度を付けて設計していくことが必要になるかと思います.

広告の基本 - 広告サーバの目的

はじめに

広告を配信する仕組みを一般的に広告サーバと呼びますが,今回は,広告サーバの目的と重要な要素について簡単に説明したいと思います. 広告用語について分からない場合は下記エントリを確認してください.

inchom.hatenadiary.jp

広告サーバとは?

広告サーバ(Ad Server)とは,広告主が入稿した広告を,メディア上に表示させて,それをユーザが見て購買行動(コンバージョン)を起こすことで収益を得るシステムのことです.

以下に簡略化した仕組みを図にしています.

f:id:inchom:20160213174611p:plain

広告主は予め広告サーバに広告を入稿しておきます.その後,ユーザがコンテンツ(Webページ/アプリページ)へのアクセスをすると,ページ内に埋め込まれた情報から広告サーバにアクセスして広告を取得する,といった流れになります.

広告サーバの目的

広告サーバは広告による収益を最大化することが目的になります.

具体的には,広告の1000回表示あたりの収益(eCPM)を最大化することにフォーカスすることになります.

広告サーバに必要な要素

eCPMを最大化するためには,以下の3点が重要になります.

  • 収益の機会損失を失くす
  • 収益性の高い広告の配信
  • 高精度の成果計測

以下にそれぞれの要素について説明します.

機会損失を失くす

収益の機会損失を失くすためには,それらの原因を一つずつ取り除く根気のいる作業が必要になります. 機会損失の原因は様々なケースが考えられますが,代表的には以下のようなものになります.

広告サーバの停止による広告表示不能

広告サーバの停止による広告表示不能とは,システムに何らかの障害が起きるなどして,広告サーバ全体あるいは一部がダウンしてしまい,システムが広告を返せなくなることです.これにより,広告収益がなくなり収益性が下がってしまいます.

また,メディア側で広告が表示されるはずだった箇所が空白で表示されることで,ユーザ体験を阻害することもあります.

過去に,Googleの広告サーバが障害により停止して,約2時間で1億円近い損失が出たなどの事例があります.

このような問題を完璧に防ぐのは不可能に近いですが,そのような問題が起きる確率を極限まで減らすために,単一障害点を一つずつ取り除くことや,問題が起きた場合の原因究明,復旧のスピードを上げることが重要になります.

応答性能劣化による広告表示遅延

応答性能劣化による広告表示遅延とは,システムに対するリクエストが増えるなどして,システム側で処理が追いつかなくなり,応答に遅延が発生してしまうことでメディア側の広告表示が遅れることです.これにより,本来ユーザが見るはずだった広告を見逃す確率が高まり,クリックやコンバージョンされる確率が低くなることで収益性が低下してしまいます.

また,広告を表示するメディアのページの広告のロードが遅れることで,ユーザ体験を阻害することもあります.

このような問題を防ぐためには,スケールアウト可能なシステム設計にすること,リクエスト増加を見積もることで事前にシステムの応答性能を高めることが重要になります.

恒常的に100msec程度の応答性能しかでていない場合は,応答性能を高めることで機会損失がなくなり,広告選定ロジック改善以上の効果が得られるケースもあります.

収益性の高い広告の配信

収益性の高い広告とは,単価が高く,クリックやコンバージョンされやすい広告のことをいいます.つまり,eCPMが高くなる広告ということなります. eCPMが高くなる広告をより多くだせば,同じ量のインプレッションでも収益が高くなります.

どの広告がクリックやコンバージョンがされやすいかどうかについては,広告を見るユーザによって異なるため,ユーザごとにどの広告をクリックしやすいかを見つける必要があります.クリックしやすいと思われるユーザに対して特定の広告を配信することをターゲティングと呼び,現在様々な手法が存在しています.

細かくは説明しませんが,以下のような手法が一般的です.(どこかでまた細かく説明します)

  • リターゲティング
  • ユーザ属性ターゲティング
  • オーディエンスターゲティング
  • 拡張オーディエンスターゲンティング
  • コンテンツターゲティング

また,同様に,ユーザに対してクリックされにくい広告を配信しないことも重要になります.

これはターゲティングと対比して,ネガティブターゲティングと呼ばれます.

ネガティブターゲティングでは,以下のような手法が一般的です.

  • ユーザフリークエンシー(FQ)制限
  • デリターゲティング

上記のように様々な手法が存在しているのですが,どれも銀の弾丸ではなく,処理速度とのトレードオフや縮小最適化(効果が良い部分のみに注力することで配信ボリュームがでなくなる)問題が存在します.

そのため,地道なA/Bテストで効果検証をしつつ(実際にはA/Bテストも難しいのですが),最適な手法を見つける必要があります.

高精度の成果計測

高精度の効果計測とは,コンバージョンの計測の精度を高めることです. 広告クリックからコンバージョン計測までの一連の流れを計測ツールを使わずに自社製にすることで計測漏れをなくしたり,広告をクリックしたとしても,すぐにコンバージョンしなかったり,オフラインで購買するなどで,今までは計測できていないものを計測できるようにすることで,広告のクリックの価値を間接的に高めることです.

クリックしてもすぐにコンバージョンしなかったりして,後日検索するなどしてコンバージョンすることを間接コンバージョンと呼び,Cookieなどで追跡する手法が存在します.

ロジック改善のようにあまり注力される点ではないのですが,Facebookなどでは成果計測の精度を上げることで1ユーザあたりの収益性を高めたケースが存在します.

jp.techcrunch.com

終わりに

今回は,広告サーバについて,その目的と重要な要素についてまとめました.

簡単に箇条書きにすると以下のようになります.

  • 広告サーバの目的

    • 収益性を最大化すること
  • 重要な要素

    • 収益の機会損失を失くすこと
    • 収益性の高い広告の配信
    • 高精度の成果計測

重要な要素を満たすために具体的にどのようなシステム設計するべきかや,今回説明しきれなかった部分については後日まとめたいと思います.

広告の基本 - 広告用語

広告についての内容を書く上で前提となる用語についてまとめています.

登場人物 (プレイヤー)

メディア/媒体

広告主

  • 広告を出稿する元/スポンサー

ユーザ

  • メディアを閲覧する人

広告代理店/Rep.

  • 広告主に代わって広告を出稿/管理する代理店
  • ネット広告専門の場合にメディアレップと呼ばれることがある

指標系

広告効果をはかる指標に使われるもの

インプレッション (impression)

  • ブラウザ上に広告を読込/表示すること
  • ブラウザに読み込まれたが表示されていない(=ユーザが広告を見ていない)インプレッションも含まれる場合がある
  • 上記と区別するためブラウザに表示されたもの(=in view)のみを指したビューアブルインプレッション(Viewable Impression)という指標がある

クリック (click)

  • ユーザが広告がクリックすること

コンバージョン (conversion)

  • 広告をクリックしたユーザがコンバージョン行動(購買,アプリインストールなど)を行うこと

クリックスルーレート(Click Through Rate/CTR)

  • インプレッション数に対するクリック数の割合
  • CTRが高い広告を選択することでメディア側の(短期的な)収益を高めることができる
  • CTR(%) = number of clicks / number of impressions * 100

コンバージョンレート (ConVersion Rate/CVR)

  • クリック数に対するコンバージョン数の割合
  • CVR(%) = number of conversions / number of clicks * 100

CPA (Cost Per Acquisition/Cost Per Action)

  • 1コンバージョンにかかったコスト(金額)
  • CPA = total cost / number of conversions

CPC (Cost Per Click)

  • 1クリックにかかったコスト

CPM (Cost Per Mills)/eCPM (effective Cost Per Mills)

  • 1000インプレッションにかかったコスト
  • eCPMはインプレッション課金以外のものも含んで計算する場合に使われることが多い
    • eCPM = total cost / number of impressions * 1000

CPV (Cost Per View)

  • 動画広告で1回視聴にかかったコスト
    • どのタイミングで視聴にカウントするかはメディアやADNによって異なる
  • 主にFacebookで使われる

システム系

メディアアドサーバ

  • メディア側が管理している広告サーバ

アドネットワーク (Adnetwork)

  • 複数の広告配信可能なメディアを束ねて広告配信するネットワーク

DSP (Demand Side Platform)

  • 広告主側の収益を最大化するためのプラットフォーム

SSP (Supply Side Platform)

  • メディア側の収益を最大化するためのプラットフォーム

RTB (Real Time Bidding)

  • DSPSSPの間で1インプレッション単位で広告を入札する仕組み

広告表示形式系

検索連動型広告

  • 検索した単語に関連して表示される広告
  • Google AdWordsなど

ディスプレイ広告

  • Webページ上の一部に表示される広告
  • Google Ad Senseなど

ネイティブ広告

  • メディアで表示されるページに溶け込む形で表示される広告
  • 記事そのものが広告の場合も含む

インフィード広告

  • リスト型やカード型のコンテンツ間に表示される広告
  • FacebookTwitterなど

ルーセル広告

  • スライドすることで複数の広告画像をできたりや遷移先に飛ぶことができる広告
  • Facebookなど

動画広告

  • 動画形式で再生される広告
  • 動画コンテンツ再生時に再生されるインストリーム型,動画コンテンツと関係なく再生されるアウトストリーム型が存在する
  • インストリーム型:YouTube(Google)など
  • アウトストリーム型:Facebookなど

その他

メディア在庫/広告枠在庫

  • メディアに掲載可能な広告枠

広告在庫/広告主在庫

  • 表示可能な(=消化可能な予算のある)広告