AdMob メディエーションの最適化は、本当のところ「やってみないと分からない」要素が大きい領域です。Network A を増やすと eCPM は上がるけれど ARPU は下がる、というのが平気で起きます。地域や端末層やセグメントごとに最適解が違うので、頭で組み立てた仮説より実測がだいたい強い。問題は、「やってみる」のコストが高いこと。1 つの組み合わせの A/B を 7 日回して、効かなければ次、効いたら更に深掘り、という地道さの先にしか答えがありません。
廣川政樹です。2014 年から個人開発で iOS/Android アプリを続けていて、累計 5,000 万 DL の壁紙アプリ群(綺麗な壁紙・浮世絵壁紙・クールな壁紙・イラスト壁紙・Dolice 壁紙)と、Lab 4 サイト・Blog 2 サイトを一人で運営しています。AdMob メディエーションの最適化は「本業」のほうの主要収益源で、長年手動で回してきました。
最近 Antigravity の並列エージェント機能で、この A/B 検証ループを自動化したらだいぶ楽になりました。このノートは、その実装と運用上のハマりどころ、そして本番アプリで事故を起こさないためのロールアウト設計を共有するメモです。「AdMob 収益を伸ばしたいが手動 A/B が回らない」「Antigravity の並列エージェントで実用的なものを作ってみたい」方向けです。
なぜ AdMob メディエーション最適化が手動で詰むか
メディエーションは、AdMob を最上位の Mediation Manager に置いて、その下に複数の広告ネットワーク(Meta Audience Network、AppLovin、Unity Ads、Vungle、IronSource など)をぶら下げる構成です。それぞれの eCPM フロアプライス をどう設定するか、Network ごとの優先順位 をどう決めるかが運用の本体です。
これがなぜ手動だと詰むかというと、変数が多すぎるからです。一例:
ネットワーク数: 7〜10 個
国別調整: 主要 20 カ国それぞれで挙動が違う
端末層: iOS / Android、tier 1 / tier 2 デバイスで eCPM が変わる
フロアプライス: $0.10 刻みで実質 30 段階
バナー / インタースティシャル / リワード の 3 種類で別々の最適化
組み合わせ爆発で、人間が頭で「次はこれを試そう」と決めるには複雑すぎます。私は半年ほど直感で回していましたが、「Network A のフロアを上げてみたら eCPM は上がったけど fill rate が落ちて ARPU は下がった」のようなトレードオフを毎回手動で確認する作業が、本気で疲れました。
Antigravity の並列エージェントは、複数のエージェントが互いに独立な実験を同時に走らせる構成が組めます。1 つの A/B 仮説に 1 エージェントを割り当てて、7 日間並列に検証して結果を持ち寄る、という流れが自然に書けます。
アーキテクチャ — 7 日サイクル 4 並列の検証ループ
最終的に組み上がった構成です。
┌──────────────────────────────────────────────────┐
│ Orchestrator Agent (週次, 月曜 00:00 JST) │
│ - AdMob Reporting API から先週の数字を集計 │
│ - 仮説を 4 つ立案 (eCPM フロア / Network 順位 等) │
│ - 4 つの Experimenter Agent に振り分け │
└──────────────────┬───────────────────────────────┘
│ 並列起動
┌──────────┼──────────┐
▼ ▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│ Exp #1 │ │ Exp #2 │ │ Exp #3 │ │ Exp #4 │
│ Banner │ │ Inters │ │ Reward │ │ Floor │
│ Network│ │ Order │ │ Order │ │ Up 10% │
│ 順位 │ │ 入替え │ │ 入替え │ │ │
└────┬───┘ └────┬───┘ └────┬───┘ └────┬───┘
│ │ │ │
│ Remote Config で 5% ロールアウト │
│ ↓ 7 日間ライブ計測 ↓ │
▼ ▼ ▼ ▼
┌────────────────────────────────────────────┐
│ Evaluator Agent (週次, 翌月曜 00:00 JST) │
│ - 4 実験の eCPM・ARPU・fill rate を比較 │
│ - 改善が確認できたものを 100% ロールアウト │
│ - 悪化したものは即座に巻き戻し │
└────────────────────────────────────────────┘
ポイントは 3 つです。(1) Orchestrator が仮説立案を担当 して、Experimenter は実行だけに集中させること。(2) 並列実験は Remote Config で 5% ずつのトラフィックに当てる こと(本番ユーザー 100% に当てない)。(3) Evaluator が悪化を検知したら即時に巻き戻す こと(Remote Config の Force Refresh で 1 時間以内に効きます)。
AdMob は変更の反映に最大 24 時間かかることがあるので、検証期間は最低 7 日確保しています。曜日効果(土日 vs 平日)と週内の広告需要変動を平均化するには 7 日が下限という経験則です。
Orchestrator Agent — 仮説立案
Orchestrator は週次で起動して、先週の AdMob レポートを取得し、改善余地のあるディメンションを 4 つ選び、仮説を 4 つ立てます。これを 4 並列の Experimenter に振り分けるのが役割です。
# orchestrator.py
from antigravity import Agent, AgentRunner
from admob import fetch_weekly_report
import json
ORCHESTRATOR_PROMPT = """あなたは AdMob メディエーションの最適化担当です。
先週の数字を見て、改善余地のある仮説を 4 つ立ててください。
入力データ:
{report_summary}
評価軸:
- eCPM (実効 CPM): $0.50 単位で意味のある差
- ARPU (Average Revenue Per User): セグメント別に評価
- fill rate: 90% を下回るネットワークは要注意
仮説の出力形式 (JSON 配列):
[
{
"id": "exp_001",
"hypothesis": "Banner 広告で Meta Audience Network を最上位に上げると eCPM 改善",
"format": "banner",
"change": {"network_order": ["meta", "applovin", "admob", "unity"]},
"target_segment": "iOS / 日本 / 30 代以上",
"expected_outcome": "eCPM +5% / fill rate 維持",
},
...
]
"""
def orchestrate (week_start: str ) -> list[ dict ]:
report = fetch_weekly_report(week_start)
report_summary = json.dumps(report, ensure_ascii = False , indent = 2 )
agent = Agent(
model = "gemini-3-pro" , # 仮説立案は Pro 推奨
system_prompt = ORCHESTRATOR_PROMPT .format( report_summary = report_summary),
)
response = agent.run( "先週のレポートから 4 つの仮説を JSON で出力してください。" )
hypotheses = json.loads(response.text)
assert len (hypotheses) == 4 , "Orchestrator は必ず 4 仮説を返すこと"
return hypotheses
Orchestrator には Gemini 3 Pro を使っています。仮説立案は数字を読んで複合的に判断する作業で、Flash では筋の良い仮説が出にくいことが分かりました。週 1 回 4 仮説で出力 2,000 トークン程度。Pro でも月 $1 程度のコストです。
仮説の質が高いほど検証ループ全体の精度が上がるので、ここはケチらず Pro を使うのが個人運用での実用的な判断です。
Experimenter Agent — Remote Config で 5% ロールアウト
Experimenter は受け取った仮説を Remote Config の A/B テストとして起動します。Firebase Remote Config の Conditions と Variants の組み合わせで「5% のユーザーにだけ新しい順序を当てる」のが標準的なやり方です。
# experimenter.py
from firebase_admin import remote_config
from antigravity import Agent
EXPERIMENTER_PROMPT = """あなたは AdMob メディエーション実験の実行担当です。
与えられた仮説を Firebase Remote Config の A/B テストとして起動してください。
仮説:
{hypothesis}
以下を生成:
1. Remote Config の parameter キーと値
2. Conditions (5% ロールアウト)
3. ロールバック判定の閾値 (eCPM 悪化 -20% / ARPU 悪化 -10%)
"""
def launch_experiment (hypothesis: dict ) -> dict :
agent = Agent(
model = "gemini-3-flash" ,
system_prompt = EXPERIMENTER_PROMPT .format(
hypothesis = json.dumps(hypothesis, ensure_ascii = False )
),
)
config = json.loads(agent.run( "Remote Config の設定を JSON で出力" ).text)
# Remote Config に書き込み
template = remote_config.get_template()
template.parameters[ f "network_order_ { hypothesis[ 'format' ] } " ] = \
remote_config.Parameter(
default_value = remote_config.ParameterValue(
value = json.dumps(config[ "control_value" ])
),
conditional_values = {
"experiment_5pct" : remote_config.ParameterValue(
value = json.dumps(config[ "variant_value" ])
),
},
)
template.conditions.append(remote_config.Condition(
name = "experiment_5pct" ,
expression = f "percent( { hypothesis[ 'id' ] } ) <= 5" ,
tag_color = "GREEN" ,
))
remote_config.publish_template(template)
return config
5% ロールアウトの根拠は、5,000 万 DL のうち日次アクティブユーザー約 8 万人に対して 4,000 人ずつ 4 実験 = 16,000 人がいずれかの実験群に入る、という設計です。コントロール群は残り 84% で、十分なサンプルサイズが確保できます。
アプリ側の実装 — Remote Config で広告順序を切り替える
iOS / Android のアプリ側では、起動時に Remote Config から network_order_banner 等のキーを取得して、AdMob のメディエーション設定を動的に組み替えます。
// iOS (Swift) — AdMobMediator.swift
import GoogleMobileAds
import FirebaseRemoteConfig
final class AdMobMediator {
static let shared = AdMobMediator ()
private let remoteConfig = RemoteConfig. remoteConfig ()
func setupBanner ( rootViewController : UIViewController) -> GADBannerView {
let order = networkOrder ( for : "banner" )
// メディエーションは AdMob 管理画面側の設定が本体だが、
// アプリ側で UnitID を切り替える形で実験対応
let unitId = adUnitId ( for : "banner" , order : order)
let bannerView = GADBannerView ( adSize : GADAdSizeBanner)
bannerView.adUnitID = unitId
bannerView.rootViewController = rootViewController
bannerView. load ( GADRequest ())
return bannerView
}
private func networkOrder ( for format: String ) -> [ String ] {
let key = "network_order_ \( format ) "
guard let json = remoteConfig. configValue ( forKey : key). stringValue ,
let data = json. data ( using : . utf8 ),
let order = try? JSONDecoder (). decode ([ String ]. self , from : data)
else {
return [ "admob" ] // フォールバック
}
return order
}
private func adUnitId ( for format: String , order : [ String ]) -> String {
// 実験ごとに事前に作っておいた UnitID をマッピング
let key = " \( format ) _ \( order. joined ( separator : "_" ) ) "
return adUnitMapping[key] ?? defaultUnitId ( format : format)
}
}
実装上の重要な工夫が 「事前に作っておいた UnitID をマッピング」 の部分です。AdMob はメディエーション設定を動的に書き換える API を公開していないので、アプリ側で UnitID を切り替えて疑似的に違うメディエーションを実現します。実験パターンを増やすには UnitID を事前に作っておく必要がありますが、これさえ用意すれば Remote Config だけで切り替えが効きます。
iOS と Android で同じロジックを 1 度ずつ実装すれば、以降の運用は Antigravity 側だけで完結します。
Evaluator Agent — eCPM だけでなく ARPU で判定する
7 日後に Evaluator が走って、4 実験の結果を比較します。ここで最も重要な実装上の判断が、「eCPM だけ見ない」 ことです。
# evaluator.py
EVALUATOR_PROMPT = """あなたは AdMob A/B 実験の結果評価担当です。
以下の 4 実験の 7 日間データを評価してください。
各実験データ:
{experiments_data}
評価ルール (重要):
- eCPM が +5% でも ARPU が -5% 以上ならロールバック
- fill rate が 90% を切ったら eCPM がいくら良くてもロールバック
- 統計的有意性: サンプル 16,000 人 × 7 日 = 11.2 万人で p < 0.05 を要求
- 改善が確認できた実験のみ 100% ロールアウト推奨
出力形式 (JSON):
{
"results": [
{
"experiment_id": "exp_001",
"outcome": "rollout" | "rollback" | "extend",
"rationale": "eCPM +8%, ARPU +6%, fill rate 92% で全体改善",
"next_action": "Remote Config の experiment_5pct を 100% に拡大",
},
...
]
}
"""
def evaluate (experiments: list[ dict ]) -> list[ dict ]:
data = []
for exp in experiments:
metrics = fetch_experiment_metrics(exp[ "id" ], days = 7 )
data.append({ "experiment" : exp, "metrics" : metrics})
agent = Agent(
model = "gemini-3-pro" ,
system_prompt = EVALUATOR_PROMPT .format(
experiments_data = json.dumps(data, ensure_ascii = False , indent = 2 )
),
)
decisions = json.loads(agent.run( "各実験の判定を JSON で出力" ).text)
return decisions[ "results" ]
eCPM だけ追って ARPU を見ないと、「高単価の広告は出ているが配信回数が減って総収益が下がる」という典型的な事故が起きます。実際に手動運用時代に経験しています。当時の自分は「eCPM が +20% なら勝ち」と思っていたのですが、その変更で fill rate が 95% から 80% に落ちて、ユーザー 1 人あたりの広告表示回数が減って、結果として ARPU は -8%。月で計算すると数十万円のマイナスでした。
それ以来、評価軸は eCPM × fill rate ≒ ARPU で考えるようになりました。Evaluator にはこれを明示的に判定ルールに書いています。
自動巻き戻し — 悪化検知時の安全装置
Evaluator が「ロールバック」と判定したら、即座に Remote Config を切り戻します。Force Refresh で全クライアントに 1 時間以内に効きます。
def rollback (experiment_id: str ):
template = remote_config.get_template()
# 該当 experiment の condition を削除
template.conditions = [
c for c in template.conditions if c.name != f "experiment_ { experiment_id } "
]
# 該当 parameter の conditional_value を削除
for key, param in template.parameters.items():
if f "experiment_ { experiment_id } " in param.conditional_values:
del param.conditional_values[ f "experiment_ { experiment_id } " ]
remote_config.publish_template(template)
# 強制 Refresh をトリガー
# アプリ側は次回起動時に新しい設定を取得
print ( f "⛔ Rolled back { experiment_id } " )
「即時に巻き戻せる」という安心感が、実験を積極的に回す上での心理的な支えになります。手動運用時代は「悪い変更を出したら戻すまでに半日かかる」というプレッシャーで保守的にしか動けませんでしたが、自動巻き戻しがあると「とりあえずやってみる」のコストが下がります。実験回数 × 期待値 = 改善総量 という公式で見ると、回数を増やせる設計こそが収益最適化の本体です。
ハマりどころ — 並列エージェント運用で気をつけたい 4 点
実装中に気づいた点を 4 つ共有します。
第一に、並列実験の独立性を保つ こと。4 つの Experimenter が同じ Remote Config パラメータを取り合うと、互いの効果を打ち消し合います。実験ごとに別パラメータ(network_order_banner、network_order_interstitial のように)を割り当てて、衝突しないように設計しています。
第二に、AdMob Reporting API のデータ遅延 です。リアルタイム API は遅延が小さいですが日次集計に使えず、ヒストリカル API は最大 48 時間遅延します。Evaluator は実験終了後 +2 日のタイミングで起動するように設定しています。
第三に、Gemini API のレート制限 です。並列に 4 つのエージェントを走らせると、特に同じプロンプトを連続実行する Evaluator の段で 429 エラーが頻発しました。asyncio.Semaphore(2) で同時実行数を絞り、Exponential Backoff でリトライする仕組みを入れています。
第四に、fill rate < 90% は事故のサイン です。「90% を切ったら eCPM がいくら良くてもロールバック」というルールは経験則ですが、これを下回ると広告非表示のセッションが増えて広告以外の体験設計も崩れます。Banner / Interstitial / Reward で許容下限が違うので、フォーマットごとに別閾値を設定するのが現実的です。
運用 6 ヶ月で見えてきた効果
定量的な変化を共有します。実装前と運用 6 ヶ月後の比較です。
月次平均 eCPM: $2.10 → $2.65(+26%)
月次平均 ARPU: $0.14 → $0.18(+29%)
A/B 実験回数: 月 2〜3 件(手動) → 月 16 件(自動)
1 実験あたりの運用工数: 4〜6 時間 → 5 分(結果レビューのみ)
ARPU が +29% に伸びたのが、5,000 万 DL という規模で見ると年間数百万円の差になります。手動運用時代は「変えてみたら悪化したかも」という不安で実験を絞っていましたが、自動巻き戻しがあると気軽に試せるので、結果として勝てる実験を多く拾えるようになりました。
もう一つ良かったのが、勝ち筋が言語化されること です。Evaluator の rationale フィールドに毎回理由が記録されるので、後から「どういう条件で何が効いたか」のパターンが蓄積されます。半年分のログを読み返すと、「日本市場では Meta Audience Network のフロアを $0.30 → $0.40 に上げると banner eCPM が安定して伸びる」のような知見が見えてきました。手動運用時代は感覚で「そんな気がする」で終わっていた話が、データと AI 判定で裏付けられる形で残ります。
次に手を入れる予定
このパイプラインでも残課題があります。
第一に、国別最適化の自動化 です。今は日本市場を中心に最適化していて、米国・東南アジアは粗いままです。国別に Orchestrator を分離して並列に回す構成を検討中です。
第二に、広告以外の収益ストリームとの統合 です。買い切り課金(StoreKit 2)とアフィリエイトの収益を AdMob レポートと統合して、「広告を強くすると買い切り課金が減る」のようなトレードオフを定量化したい。
第三に、LLM 出力の追跡可能性 です。Antigravity の並列エージェント間の通信ログを完全に保存して、後から「なぜ Evaluator がロールバックを推奨したか」を再生できる仕組みを追加予定です。コンプライアンス的な要求は個人開発では緩いのですが、自分自身の意思決定の質を上げる意味で重要だと感じています。
AdMob 収益化は派手な変化が起きにくい領域ですが、複利で 6 ヶ月運用して +29% は決して小さくない数字です。同じように個人で複数アプリを運営している方の参考になれば嬉しいです。本ノートで触れた実装パターンは応用が利くので、メディエーションだけでなく IAP の価格 A/B、オンボーディング A/B などにも転用できます。