AdMob のレポートからフロア(eCPM 下限値)を決める作業を自動化するとき、多くの人が最初に思いつくのは「レポートを丸ごと Gemini に渡して、各グループの新しいフロアを答えさせる」です。私も最初はそうしかけたのですが、すぐにやめました。しきい値の計算と判定を LLM にやらせると、その判定が監査できなくなる からです。フロアの設定は収益に直結する不可逆な操作で、「なぜこの値になったか」を後から再現できない仕組みは、運用に乗せてはいけません。
私は 2014 年からの個人開発で累計 5,000万ダウンロードのアプリ事業を運営し、AdMob は月間ピークで 150 万円ほどの収益源です。国際芸術賞 17冠のアート活動と並行してこの規模を一人で回す中で、毎月 42 のメディエーショングループのフロアを見直しています。その経験から言えるのは、Gemini の構造化出力は「判定」ではなく「抽出」にこそ強い、ということです。以下では、構造化出力を抽出工程だけに限定し、判定をコードに分離する設計を、実際に運用している判定ルールとともに示していきます。
なぜ判定を LLM にやらせてはいけないのか
フロアの判定ルール自体は、実は完全に決定論的です。私が使っているルールはこうです。iOS のフロアは実勢 eCPM × 55% を基準にし、現フロア / eCPM の比率が 65% を超えていたら引き下げ、比率が 40% 未満かつマッチ率が 95% を超えていたら引き上げ、その中間(40〜65%)は維持。最小単位は $0.50。Android は実勢比ではなく一律 $0.50 固定です。
このルールは四則演算としきい値比較だけで完結します。つまり、LLM に「判断」させる余地は本来ありません。にもかかわらず LLM にプロンプトでルールごと渡すと、3 つの問題が出ます。第一に、しきい値の境界(比率がちょうど 65.0% のとき等)で出力がぶれます。第二に、同じ入力でも実行ごとに微妙に違う値が返る可能性があり、再現性が崩れます。第三に、最大の問題として、「比率 64% だから維持」という判断の根拠を、出力からは検証できない 。コードなら ratio = floor / ecpm の 1 行を見れば誰でも追えますが、LLM の内部推論は追えません。
フロアの誤設定は、高すぎればフィル率が落ちて no-fill が多発し、低すぎれば eCPM が崩れます。実際、過去に Android INT のフロアを実勢に対して過剰に高く設定していたために、マッチ率が 70% 台で頭打ちになっていたことがありました。こういう「効きすぎ・効かなすぎ」を後から検証して直すには、判定が決定論的でなければなりません。
では Gemini に何をやらせるのか — 「抽出」だけ
一方で、AdMob のレポートを CSV やコピペで受け取ると、データは驚くほど乱れています。グループ名の表記ゆれ、通貨記号の有無、マッチ率が「47.53%」だったり「0.4753」だったり、列順がエクスポート設定で変わる。この乱れた入力を、型の揃った行データに整える工程 こそ、Gemini の構造化出力が圧倒的に強い領域です。
ここでのコツは、出力スキーマに「判定結果」を一切含めないことです。スキーマには生の観測値(グループ名・グループ ID・実勢 eCPM・マッチ率・現フロア)だけを定義し、新フロアや「引き上げ/引き下げ」といった判断は絶対にスキーマに入れません。LLM の責務を「乱れた入力 → 型付きの観測値」に閉じ込めるわけです。
import os
from google import genai
from pydantic import BaseModel
class GroupRow ( BaseModel ):
group_name: str
group_id: str
ecpm_usd: float # 実勢 eCPM(観測値のみ)
match_rate: float # 0.0〜1.0 に正規化
current_floor_usd: float
class ExtractedReport ( BaseModel ):
rows: list[GroupRow]
client = genai.Client( api_key = os.environ[ "YOUR_GEMINI_API_KEY" ])
raw_report = open ( "admob_mediation_report.txt" , encoding = "utf-8" ).read()
resp = client.models.generate_content(
model = "gemini-2.5-pro" ,
contents = (
"次の AdMob メディエーションレポートを、各グループ 1 行に正規化して抽出してください。"
"match_rate は 0.0〜1.0 に変換。判定や推奨値は出力しないこと。観測値のみ。 \n\n "
+ raw_report
),
config = {
"response_mime_type" : "application/json" ,
"response_schema" : ExtractedReport,
},
)
report: ExtractedReport = resp.parsed
プロンプトに「判定や推奨値は出力しないこと」と明示しているのが肝です。スキーマで縛ったうえで、自然言語でも責務を限定する。これで Gemini は「抽出器」として安定して動きます。
判定はコードに置く — 監査可能な決定関数
抽出された型付きデータに対して、判定は普通の Python 関数で行います。ここが監査の要です。
from dataclasses import dataclass
@dataclass
class FloorDecision :
group_id: str
current: float
proposed: float
action: str # "raise" / "lower" / "hold"
reason: str # 根拠を文字列で残す(監査用)
def decide_floor_ios (row) -> FloorDecision:
ratio = row.current_floor_usd / row.ecpm_usd if row.ecpm_usd else 0
target = round (row.ecpm_usd * 0.55 / 0.5 ) * 0.5 # 55% 基準・$0.50 刻み
if ratio > 0.65 :
return FloorDecision(row.group_id, row.current_floor_usd,
max (target, 0.5 ), "lower" ,
f "ratio { ratio :.0% } > 65%" )
if ratio < 0.40 and row.match_rate > 0.95 :
return FloorDecision(row.group_id, row.current_floor_usd,
max (target, 0.5 ), "raise" ,
f "ratio { ratio :.0% } < 40% & mr { row.match_rate :.0% } > 95%" )
return FloorDecision(row.group_id, row.current_floor_usd,
row.current_floor_usd, "hold" ,
f "ratio { ratio :.0% } in 40-65% band" )
decisions = [decide_floor_ios(r) for r in report.rows]
reason フィールドに判断根拠を文字列で残しているのがポイントです。後から「このグループはなぜ維持だったのか」を問われたら、ratio 64% in 40-65% band という機械的な根拠が即座に出ます。LLM にやらせていたら、この一行は決して得られません。判定ロジックを変えたいときも、テストを書いて関数を直すだけで済み、プロンプトを「育てる」必要がありません。
抽出の正しさをどう担保するか
判定をコードに寄せても、抽出が間違っていたら元も子もありません。そこで抽出結果には、コード側で軽いサニティチェックを通します。
def sanity_check (row) -> list[ str ]:
issues = []
if not ( 0.0 <= row.match_rate <= 1.0 ):
issues.append( f " { row.group_id } : match_rate 異常 { row.match_rate } " )
if row.ecpm_usd < 0 or row.ecpm_usd > 200 :
issues.append( f " { row.group_id } : eCPM 異常 { row.ecpm_usd } " )
if row.current_floor_usd < 0 :
issues.append( f " { row.group_id } : 負のフロア" )
return issues
マッチ率が 1.0 を超えていたら、Gemini が「47.53%」を 47.53 のまま入れた可能性が高い。eCPM が異常値なら通貨記号の誤読を疑う。抽出は LLM、その妥当性検証はコード という、前のセクションと同じ「LLM は曖昧な入力に、コードは決定論的な検証に」という役割分担です。インプレッションが 20 未満のグループは判定対象から外す、といった運用上のフィルタも、ここでコードとして明示します。
書き込みは人間に残す — パイプラインの終端設計
抽出と判定が終わっても、AdMob の編集画面へフロア値を打ち込む最後の一手は人間に残しています。AdMob の編集画面はボタン ID が動的で、自動入力は入力欄の取り違え事故を起こしやすいためです。パイプラインの出力は「グループごとの提案値と根拠の一覧」までとし、人間がそれを見て入力する。入力後に、抽出と同じ仕組みで編集画面の実値を読み返し、提案値と一致するかを照合します。
この終端設計により、「Gemini が抽出 → コードが判定 → 人間が書き込み → コードが実値を照合」という、各工程の責務が明確に分かれたパイプラインになります。LLM が関わるのは最初の抽出だけで、収益に直結する判定と不可逆な書き込みは、それぞれコードと人間が握る。これが、収益データを扱う自動化で私が推奨する基本形です。
実際の運用ではどれくらいの規模を捌くか
この分離設計で月次に処理しているのは、iOS・Android 合わせて 42 グループ分のレポートです。Gemini への投入は 1 回で全グループ分のテキストを渡し、抽出は数秒で 1 グループ 1 行に正規化されます。そこから先の判定はコードなので一瞬で、42 グループ全件の「提案値+根拠」一覧が即座に揃います。
直近の月次では、この一覧をもとに引き上げ 3 件・引き下げ 2 件・残り維持、という結果になりました。インプレッションが 20 未満のグループは判定対象から外しているため、データの薄いグループに振り回されません。重要なのは、この「5 件だけ動かす」という判断が、すべて ratio の数値根拠付きで残っていることです。来月、別の人(あるいは未来の自分)がこの判断を見直すとき、根拠の文字列をたどるだけで再現できます。LLM に丸投げしていたら、この再現性は得られませんでした。
この設計が効く範囲
最後に、この「抽出と判定を分ける」設計が効くのは、AdMob のフロア決定に限りません。判定ルールが決定論的に書ける一方で、入力データが乱れている——この条件を満たす業務は、構造化出力で抽出し、判定をコードに置くと、速くて監査可能な自動化になります。レビューのセンチメント分類のように判定自体が曖昧なタスクは LLM に寄せ、しきい値や四則演算で決まる判定はコードに寄せる。この線引きを最初に決めることが、LLM を業務に組み込むときの設計の出発点だと考えています。
まずは自分が LLM に投げているプロンプトを 1 つ見直して、「これは抽出か、判定か」を切り分けてみてください。判定だった部分をコードに移すだけで、出力の再現性と監査可能性が一気に上がるはずです。