Article

競合分析で差をつける広告運用:他社の出稿をヒントにする方法

高田晃太郎
競合分析で差をつける広告運用:他社の出稿をヒントにする方法

iOSのATT(App Tracking Transparency)同意率は市場・国・カテゴリで幅があり、AppsFlyerの分析では全体平均で約41%(2021年時点)と初期予想を上回り¹、国別でも30〜50%台のレンジが報告されています⁶。Adjustの継続レポートでも業種によって30%超のケースが示されています⁷。また、一部の独立調査では「Allow」を選ぶユーザーが想定より多い傾向も指摘されています²。こうした背景から、ポストCookie時代の計測は引き続き不確実性が高い領域と言えます。 一方で、Googleのレスポンシブ検索広告(RSA)は最大15個の見出しと4個の説明文を登録でき³、組み合わせ空間は人間の目視では捌き切れません。さらにMeta広告ライブラリAPIはクエリあたり最大100件の取得とページングを備え、クリエイティブ本文やリンクタイトル、配信期間、プラットフォームといったフィールドを取得できます⁴。公開情報から数千件規模のクリエイティブを機械的に収集するための足回りは整っています。一般に、単純な模倣ではなく、競合のメッセージ変数を抽出→自社のキーワード戦略と実験に翻訳→効果検証という流れにすると学習速度が上がりやすく、媒体横断の一貫性も維持しやすくなります。本稿ではCTO・エンジニアリーダーがチームに提供できる再現可能なデータパイプラインと、KPI(事業目標指標)に直結する実験設計をコード付きで提示します。

競合分析を広告運用に結びつける設計図

競合の広告を眺める行為は情報収集では終わりがちです。そこでまず、収集の前に設計を置きます。どの市場仮説を検証したいのか、例えば価格訴求と体験価値訴求のどちらが効いているのか、あるいは機能の列挙よりも導入事例の具体性が反応を高めるのかといった問いを明確にし、その問いに答えるためのデータスキーマを設計します。テキスト、CTA、オファー、プラットフォーム、配信期間といった変数を正規化し、メッセージの意味論的な近さを数値化するための埋め込み表現(embedding:文の意味をベクトルで表現)を用意すると、後段の分析が一気に体系化されます。これにより、検索意図に紐づくキーワード群の優先度づけや除外キーワードの見直しも、クリエイティブ分析と同じ土台で一貫して進められます。

取得元は原則として利用規約に準拠した公式APIと公開ライブラリに限定するのがエンジニアリングのガバナンスとして安全です。Meta広告ライブラリは公式APIが提供されており、クリエイティブ本文、リンクタイトル、配信期間、プラットフォームといったフィールドを取得できます⁴。GoogleのAds Transparency CenterやTikTokのCreative Centerは検索やエクスポート機能を通じた人手・半自動の取得が中心になります。検索広告の競合出稿はSerpApiのような検索結果APIでスポンサー枠を構造化データとして取得する方法が現実的です⁵。自社アカウントの入札競合状況はUI限定のケースがあるため、そこはパネルデータやサードパーティのリサーチを補助的に使います。

設計時点でSLO(Service Level Objective:サービス運用の目標値)としてのパフォーマンス指標を置いておくと運用が安定します。例えばAPI収集はp95レイテンシ800ms未満、失敗時の自動リトライ上限は3回以内、埋め込み生成はGPU環境で毎秒数百レコードを目標、クラスタリングは1万レコードを数秒台で終了といった具合です。数値は環境に依存するため、メトリクス収集とアラートで運用の健全性を担保します。

データソースと法務・ガバナンスの原則

法務・コンプライアンスの観点では、各サービスの利用規約とロボッツ規約に反しない取得に限定します。APIキーやトークンはKMSやSecrets Managerで保護し、ローテーションを自動化します。個人情報やユーザー追跡に関わるデータは収集対象に含めず、公開クリエイティブや広告主名といったパブリック情報のみにフォーカスします。社内共有は必要最小限のメタデータに留め、創作物のスクリーンショット転載は社内ナレッジに限定する運用が無難です。

変数設計:メッセージを解像度高く分解する

分解の単位は、ベネフィット、証拠、差別化フレーズ、リスク低減要素、オファー、CTA、ビジュアルモチーフ、プラットフォーム、期間という九つに整理すると分析しやすくなります。例えば「導入1週間でデプロイ時間を50%短縮」ならベネフィットは時間短縮、証拠は具体的なパーセンテージ、差別化はスピード、リスク低減は無料トライアル、オファーは期間限定の割引、CTAは今すぐ試す、ビジュアルはダッシュボード、プラットフォームはLinkedIn、期間はQ1という具合に分解できます。後段のクラスタリングやラベリングでこの構造を保つことが、施策への翻訳を容易にします。

実装編:収集→加工→分析→可視化→実験

ここからは再現可能な実装を示します。最小構成はデータ収集モジュール、前処理・正規化、意味ベクトル化とクラスタリング、メッセージラベリング、可視化、そして実験キットです。計測困難な時代ほど、パイプラインの健全性とログが学習速度を決めます。

Meta広告ライブラリAPIでクリエイティブを取得する

import os
import time
import requests
import pandas as pd
from urllib.parse import urlencode

ACCESS_TOKEN = os.environ.get("META_ADLIB_TOKEN")
BASE = "https://graph.facebook.com/v19.0/ads_archive"

params = {
    "access_token": ACCESS_TOKEN,
    "search_type": "KEYWORD_UNORDERED",
    "search_terms": "SaaS データ基盤",
    "ad_reached_countries": "JP",
    "ad_type": "ALL",
    "ad_active_status": "ALL",
    "limit": 100,
    "fields": ",".join([
        "ad_creative_body",
        "ad_creative_link_title",
        "ad_delivery_start_time",
        "ad_delivery_stop_time",
        "publisher_platforms",
        "page_name",
        "ad_snapshot_url"
    ])
}

def fetch_all(params):
    url = f"{BASE}?{urlencode(params)}"
    rows = []
    while url:
        resp = requests.get(url, timeout=30)
        if resp.status_code != 200:
            time.sleep(1)
            resp.raise_for_status()
        j = resp.json()
        rows.extend(j.get("data", []))
        url = j.get("paging", {}).get("next")
        time.sleep(0.2)
    return pd.DataFrame(rows)

if __name__ == "__main__":
    df = fetch_all(params)
    df.to_csv("meta_ads.csv", index=False)
    print({"records": len(df)})

取得件数はクエリに依存します。コードではページングを自動追跡し、API失敗時は例外で検知しつつ退避できるように設計しています。必要に応じてプロキシやバックオフ、レートリミット制御を追加してください。なお、取得できるフィールド仕様はMetaの開発者ドキュメントを参照してください⁴。

検索連動型のスポンサー枠をSerpApiで取得する

import os
import requests
import pandas as pd

SERP_API_KEY = os.environ.get("SERPAPI_KEY")
q = "データ統合 プラットフォーム"
url = "https://serpapi.com/search.json"
params = {
    "engine": "google",
    "q": q,
    "hl": "ja",
    "gl": "jp",
    "location": "Tokyo,Japan",
    "num": 20,
    "api_key": SERP_API_KEY
}
resp = requests.get(url, params=params, timeout=30)
resp.raise_for_status()
ads = resp.json().get("ads", [])
df = pd.json_normalize(ads)
df[["position", "title", "displayed_link", "link", "snippet"]].to_csv("search_ads.csv", index=False)

検索広告の実際のインプレッションや入札は取得できませんが、出稿面とメッセージの傾向、LPのアングルを把握するには十分です。場所や言語を固定し、定点観測として日次で同一キーワード群を計測すると変化点が掴めます。Google検索結果のスポンサー枠を構造化で取得する方法としてSerpApiの利用は実務上の選択肢です⁵。

前処理:正規化・n-gram抽出・期間重み付け

import pandas as pd
import numpy as np
from datetime import datetime
import re

meta = pd.read_csv("meta_ads.csv")
meta["body"] = meta["ad_creative_body"].fillna("")
meta["title"] = meta["ad_creative_link_title"].fillna("")

for col in ["ad_delivery_start_time", "ad_delivery_stop_time"]:
    meta[col] = pd.to_datetime(meta[col], errors="coerce")

now = pd.Timestamp.utcnow()
meta["days_active"] = (meta["ad_delivery_stop_time"].fillna(now) - meta["ad_delivery_start_time"]).dt.days.clip(lower=0)
meta["text"] = (meta["title"] + " \n" + meta["body"]).str.replace(r"\s+", " ", regex=True)

from sklearn.feature_extraction.text import CountVectorizer
cv = CountVectorizer(analyzer="word", ngram_range=(1,2), min_df=2)
X = cv.fit_transform(meta["text"])

# 広告の配信期間で重み付けしてn-gramスコアを算出
weights = 1 + np.log1p(meta["days_active"].fillna(0).values)  # shape: (n_samples,)
ngram_scores = (X.T @ weights).A1  # スパース行列演算で加重カウントを求める
terms = np.array(cv.get_feature_names_out())
ngram_df = pd.DataFrame({"term": terms, "score": ngram_scores}).sort_values("score", ascending=False).head(50)
print(ngram_df.head(10))

期間重み付けは単純化ですが、短期で消えたメッセージより長期配信されたメッセージにやや重みを置くことで、仮説の優先度付けに寄与します。正規化の仕様はチーム内で固定し、解析の再現性を担保します。

埋め込みとクラスタリングでメッセージの“型”を見つける

from sentence_transformers import SentenceTransformer
from sklearn.cluster import KMeans
import numpy as np

model = SentenceTransformer("sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
texts = meta["text"].tolist()
emb = model.encode(texts, batch_size=64, show_progress_bar=True, convert_to_numpy=True, normalize_embeddings=True)

k = 12
km = KMeans(n_clusters=k, n_init=10, random_state=42)
labels = km.fit_predict(emb)
meta["cluster"] = labels

centroids = km.cluster_centers_
closest = np.argmin(((emb[:, None, :] - centroids[None, :, :]) ** 2).sum(axis=2), axis=0)
representatives = meta.iloc[closest][["text", "cluster"]]
print(representatives.sort_values("cluster").head())

クラスタはメッセージの“型”を表します。代表文面を読むと、価格訴求、導入の容易さ、実績、技術優位性、セキュリティ保証といった分布が見えてきます。クラスタ数はシルエット係数や実務上の可読性で決めます。

LLMで短い分類ラベルを自動付与する

import os
import time
import openai

openai.api_key = os.environ.get("OPENAI_API_KEY")

SYSTEM = "与えられた広告文面を短いラベル(6語以内)で分類してください。価格、事例、技術優位、無料トライアル、セキュリティ、スピード、統合、サポート等の軸で。出力はラベルのみ。"

def classify(batch):
    out = []
    for t in batch:
        for _ in range(3):
            try:
                resp = openai.ChatCompletion.create(
                    model="gpt-4o-mini",
                    messages=[{"role": "system", "content": SYSTEM}, {"role": "user", "content": t[:800]}],
                    temperature=0.1,
                    timeout=30,
                )
                out.append(resp.choices[0].message["content"].strip())
                break
            except Exception as e:
                time.sleep(1)
        else:
            out.append("分類失敗")
    return out

sample = meta["text"].head(20).tolist()
labels = classify(sample)
print(list(zip(labels, sample[:5])))

ラベルは短く、解釈のぶれを抑えるプロンプトにします。人手によるスポットチェックで精度を確認し、誤分類が多いクラスタは手動で上書きする運用が現実的です。個人情報を含まない短文のみを送る、ロギングを制御するといったプライバシー配慮も忘れないでください。

可視化と共有:経営への説明可能性を高める

import plotly.express as px
import pandas as pd
from sklearn.decomposition import PCA

pca = PCA(n_components=2, random_state=42)
xy = pca.fit_transform(emb)
vis = pd.DataFrame({"x": xy[:,0], "y": xy[:,1], "cluster": meta["cluster"], "page": meta["page_name"], "text": meta["text"]})

fig = px.scatter(vis, x="x", y="y", color="cluster", hover_name="page", hover_data=["text"], title="競合メッセージ空間の分布")
fig.write_html("clusters.html")

二次元に射影したプロットは、経営・事業側への説明で強力です。どの“型”が密集し、どの“型”が空白なのかが一目で伝わります。レポートはHTMLで書き出してナレッジベースに添付し、再現手順と一緒に保管します。

保存と再現:BigQueryで生ログと特徴量を管理する

from google.cloud import bigquery
import os

client = bigquery.Client(project=os.environ.get("GCP_PROJECT"))
meta_out = meta.copy()
meta_out["emb"] = emb.tolist()
job = client.load_table_from_dataframe(meta_out, "adintel.raw_meta_ads")
job.result()
print(client.get_table("adintel.raw_meta_ads").num_rows)

生ログ、正規化済みテーブル、特徴量、可視化用射影の四層を分けて保存すると、スキーマ変更やモデル更新の影響を局所化できます。データセットや権限は最小権限原則で付与します。

学びを施策に翻訳する:RSA生成と実験設計

分析は意思決定に変換されて価値になります。検索広告なら、クラスタとラベルから導かれる有望な“型”をレスポンシブ検索広告(RSA:検索キーワードに応じて見出しや説明文を動的に組み合わせるフォーマット)のアセットに翻訳します。このとき、ブランドと法務のガイドラインに沿った言い換え辞書を用意し、競合表現の直写を避けます。LPはCTAや証拠の位置、導入手順の具体性をメッセージの“型”に合わせて調整します。入札・配分はクエリの意図に沿って、情報収集段階と購買段階でメッセージのトーンを切り替え、無理のないステップで誘導します。RSAのアセット仕様(見出しと説明文の上限)はGoogleの公式ドキュメントを随時確認し、最新のルールに従って運用してください³。あわせて、キーワード単位の検索意図マップを作り、各“型”と対応づけてA/Bテストの設計を行うと、運用の一貫性が高まります。

テンプレートからRSAアセット案を機械生成する

import itertools

label_to_phrases = {
    "価格訴求": ["コストを最適化", "初期費用を抑制", "従量課金で柔軟に"],
    "導入容易": ["短期間で稼働", "ノーコード設定", "既存SaaSと連携"],
    "実績": ["多様な企業で導入", "豊富な導入実績", "導入事例を公開"],
}

headlines = list(set(itertools.chain.from_iterable(label_to_phrases.values())))
descriptions = [
    "無料トライアルで評価。小さく始められる",
    "主要なセキュリティ標準に準拠",
    "データ統合作業を自動化。工数を削減",
]

rsa = {
    "headlines": headlines[:15],
    "descriptions": descriptions[:4]
}
print(rsa)

生成した案は人手で磨き、ピン留め設定も含めて検証計画に落とし込みます。広告管理画面のアセットレポートで貢献度を把握し、次の世代にフィードバックします。クリエイティブとLPを二重に変えると因果が崩れやすいため、変更は一度に一軸を基本とします。なお、自社の実績や数値を裏づけできない表現は避け、一般化した表現に置き換えるのが安全です。

検証デザイン:計測不確実性に強い枠組み

ATTやCookie制限の影響でポストクリック計測は揺れやすい状況です¹⁶⁷。そこで、地域や時間帯を使った擬似ホールドアウト、クリエイティブのみを変えた広告セット比較、媒体横断のブランド検索リフトといった複線的な検証を併用します。さらにSQLで差分の差分(DiD:施策群と対照群の前後差の差)や事前トレンドのチェックを取り入れると、外乱要因に強い推定が可能になります。社内のBIではp50/p90の反応速度、CPxの信頼区間、頻度制御の健全性をダッシュボードに常設し、意思決定を遅らせない情報設計を意識します。

運用の現実解:SLO、コスト、導入期間の目安

収集パイプラインのSLOは日次の完了率99%を目標にし、欠損時は前回値で埋めるか、分析ジョブを自動スキップします。埋め込みとクラスタリングのジョブはデータ量に比例するため、1万件規模であればマネージドGPUを短時間だけ起動する構成がコスト最適です。可視化とレポーティングは静的HTMLの生成とナレッジベースへの添付を標準化すると、属人化を防げます。初期導入は要件定義とガバナンス確認に一週間、実装と社内レビューに一週間、パイロットの計測に一週間という三週間前後をひとつの目安にできます。ROIは新規の純増というよりも、無駄弾の削減と学習速度の向上で説明し、採用する“型”の更新頻度をOKRとして置くと、チームの集中が維持されます。

重要なのは「競合の“勝ち筋”を見つける」のではなく、「自社にとって再現可能な“型”を育てる」ことです。競合の成功は参照点ですが、プロダクト、価格、チャネルの違いがそのまま結果に反映されるわけではありません。だからこそ、収集から検証までをコードとSLOで固め、仮説検証の速度を上げることが差になります。

まとめ:仮説を早く、安く、正しく回す

不確実性が高まるほど、競合の公開情報は貴重な“外部実験”として機能します。ただし、真似るのではなく、変数に分解して自社の仮説(キーワード戦略・クリエイティブ戦略)に翻訳し、実験で確かめる姿勢が欠かせません。本稿のパイプラインは、公式APIを中心にデータを収集し、埋め込みとクラスタリングでメッセージの型を抽出し、ラベリングと可視化で関係者の合意形成を助け、RSAやLPに落とすところまでを一気通貫で支えます。次にやるべきことは明確です。自社の優先市場を一つ決め、キーワード群を定義し、定点観測を今日から始めること。最初の二週間でメッセージの上位“型”を三つ選び、小さな実験を設計してください。どの“型”があなたの事業にとって再現性ある成長を生むのか、今のチームなら必ず見つけられるはずです。

参考文献

  1. AppsFlyer. ATTのオプトイン率は予想を大きく上回る(2021年)
  2. MarketingCharts. Many users are tapping on the “Allow” button.
  3. Google 広告ヘルプ. レスポンシブ検索広告の仕組み(見出し最大15、説明文4)
  4. Meta for Developers. Ads Archive(広告ライブラリAPIのフィールドと仕様)
  5. SerpApi. Google Ads(スポンサー枠の取得機能の概要)
  6. AppsFlyer Newsroom. ATT data findings(国別などのオプトイン率データ)
  7. Adjust Blog. App Tracking Transparency opt-in rates(業種別のオプトイン率)