Article

PPC広告代理店に聞いた!効果が出る企業と出ない企業の違い

高田晃太郎
PPC広告代理店に聞いた!効果が出る企業と出ない企業の違い

PPC広告(リスティング広告/Google 広告)で成果が分かれる理由を、CTO視点で整理します。PPC広告代理店がよく挙げる論点と、エンジニアリングで支えられる実装・運用の勘所を、一般読者にも伝わる形で噛み砕いて解説します。

検索広告の平均コンバージョン率は概ね3〜4%台、上位四分位は約11%前後に達する一方¹、同じ業種・同予算でも成約まで進まないアカウントは珍しくありません。研究データではラストクリック(最後に触れた広告だけで評価する方法)での評価は価値の見落としが最大で約30%生じ得るとされ²、広告の“効く・効かない”はメディアというより、計測と運用の成熟度に強く依存します。業界の運用事例を横断的にみると、差を分けるのは広告プラットフォームの選定よりも、CRMと計測の結合度(顧客データと広告のつながり)、ランディング体験の速度・摩擦、そしてLTV基準の意思決定に集約されます。CTOやエンジニアリングリーダーが関与するだけで、費用対効果が大きく変わるのはこの領域です。

効果が出る企業は「計測の完成度」が高い

成果を出す企業の共通項は、クリックから受注までのデータが損失なく接続されている点にあります。広告経由のリードがMQL(Marketing Qualified Lead: マーケティング合格リード)に昇格し、SQL(Sales Qualified Lead: 商談化見込み)を経て受注化した瞬間に、その価値がプラットフォームへ正しくフィードバックされているかどうか。ここが崩れると入札は短期のフォーム送信(CV)最適化に偏り、低質リードが増えて顧客獲得単価(CAC)が悪化します。反対に、オフラインコンバージョンの自動連携、Enhanced Conversions for Leads(同意に基づきハッシュ化された一次データでマッチングする仕組み)、サーバーサイド計測まで含めて一次データ主導の最適化サイクルを作ると、学習は一段深くなり媒体側の自動化が活きます³⁴。

オフラインコンバージョン連携とエラーハンドリング

営業の受注や契約更新など、真の価値発生点を広告に返すために、Google Ads APIでGCLID/GBRAID/WBRAIDをキーにコンバージョンをアップロードします。スキーマのずれやタイムゾーン、重複判定での失敗が頻出するため、再試行設計と監査ログが不可欠です⁴。

# offline_conversion_upload.py
import os
import sys
import logging
from datetime import datetime, timezone
from google.ads.googleads.client import GoogleAdsClient
from google.ads.googleads.errors import GoogleAdsException

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")

def load_client():
    try:
        return GoogleAdsClient.load_from_storage(path=os.getenv("GOOGLE_ADS_YAML", "google-ads.yaml"))
    except Exception as e:
        logging.exception("Failed to load Google Ads client")
        sys.exit(1)

def build_conversion(gclid: str, conv_name: str, value: float, currency: str, event_time: str):
    # event_time: ISO 8601 (e.g., 2025-08-30T10:23:00Z)
    return {
        "gclid": gclid,
        "conversion_action": conv_name,
        "conversion_date_time": event_time,
        "conversion_value": value,
        "currency_code": currency,
        "order_id": f"{gclid}-{int(datetime.now(timezone.utc).timestamp())}"
    }

def upload(conversions):
    client = load_client()
    service = client.get_service("ConversionUploadService")
    request = client.get_type("UploadClickConversionsRequest")
    request.customer_id = os.environ["GOOGLE_ADS_CUSTOMER_ID"]
    request.validate_only = False
    request.partial_failure = True

    for c in conversions:
        cc = request.conversions.add()
        cc.gclid = c["gclid"]
        cc.conversion_action = client.get_service("ConversionActionService").conversion_action_path(
            request.customer_id, c["conversion_action"]
        )
        cc.conversion_date_time = c["conversion_date_time"]
        cc.conversion_value = c["conversion_value"]
        cc.currency_code = c["currency_code"]
        cc.order_id = c["order_id"]

    try:
        response = service.upload_click_conversions(request=request)
        if response.partial_failure_error.code != 0:
            logging.error("Partial failure: %s", response.partial_failure_error)
        for r in response.results:
            logging.info("Uploaded: gclid=%s, success=%s", r.gclid, r.always_identify_user)
    except GoogleAdsException as ex:
        for error in ex.failure.errors:
            logging.error("Error: %s", error.message)
        raise

if __name__ == "__main__":
    sample = [build_conversion(
        gclid="CjwK...", conv_name=os.environ["CONV_ACTION_ID"],
        value=12000.0, currency="JPY", event_time="2025-08-30T10:23:00Z"
    )]
    upload(sample)

リードのハッシュ化とEnhanced Conversions

フォームに入力されたメールアドレスや電話番号はSHA-256でハッシュ化し、ユーザーの同意に基づいて送信します。広告主ドメインから直接送る設計にするとマッチ率が上がりやすくなります³。

<script async src="https://www.googletagmanager.com/gtag/js?id=AW-XXXX"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);} gtag('js', new Date());
  gtag('config', 'AW-XXXX');

  async function sha256(str){
    const buf = new TextEncoder().encode(str.trim().toLowerCase());
    const digest = await crypto.subtle.digest('SHA-256', buf);
    return Array.from(new Uint8Array(digest)).map(b => b.toString(16).padStart(2,'0')).join('');
  }

  async function sendEnhanced(email, phone){
    const hashed_email = await sha256(email);
    const hashed_phone = await sha256(phone.replace(/\D/g, ''));
    gtag('set', 'user_data', { email: hashed_email, phone_number: hashed_phone });
    gtag('event', 'generate_lead', { 'send_to': 'AW-XXXX/YYYY' });
  }
</script>

伸び悩む企業は「ランディングと営業プロセス」で損をする

効果が出ないアカウントの多くは、広告そのものよりもランディング体験の摩擦と営業プロセスの遅延で歩留まりを落としています。LCP(Largest Contentful Paint: 主要コンテンツの描画時間)が4秒を超える⁶、INP(Interaction to Next Paint: 反応の速さ)が400msを超える⁷、ファーストビューの訴求が曖昧、必須項目が過剰なフォームなど、どれも単体では小さな阻害要因ですが、合算すると離脱率が悪化しコンバージョン率(CVR)に大きな負影響が生じます⁵。さらに、インバウンドSLA(問い合わせ対応の合意時間)が曖昧で初動の架電やメールが数時間遅れると、見込み客の温度は急速に低下します⁸。技術チームがWebパフォーマンスと営業システムの連携を改善すると、媒体や入札戦略を変えずともCVRの基礎体力が上がり、CACの許容幅が広がるのです。

Web Vitalsの計測とサーバー送信

Core Web Vitalsの計測値をサーバーに送ると、ページやクリエイティブ単位でCVRとの相関を即座に確認できます。INPが200msを切ると体感操作性が大幅に改善されます⁷。

// web-vitals-report.js
import {onLCP, onINP, onCLS} from 'web-vitals';

function sendToEndpoint(metric){
  const body = JSON.stringify({
    name: metric.name,
    value: metric.value,
    id: metric.id,
    url: location.href,
    ts: Date.now()
  });
  navigator.sendBeacon('/vitals', body);
}

onLCP(sendToEndpoint);
onINP(sendToEndpoint);
onCLS(sendToEndpoint);

Lighthouse CIでLPを継続監視

リリースのたびにLPのLCP/INP/CLS(累積レイアウトシフト)が劣化していないか自動で検知します。スコアの下振れを検知してから運用に気づくのではなく、開発段階で差し戻す文化が重要です。

// package.json(抜粋)
{
  "scripts": {
    "lhci:collect": "lhci collect --url=https://example.com/lp --numberOfRuns=5",
    "lhci:assert": "lhci assert --config=./lighthouserc.json"
  },
  "devDependencies": {
    "@lhci/cli": "0.13.0"
  }
}
// lighthouserc.json
{
  "ci": {
    "collect": {
      "settings": {
        "preset": "desktop"
      }
    },
    "assert": {
      "assertions": {
        "performance": ["error", {"minScore": 0.9}],
        "first-contentful-paint": ["warn", {"maxNumericValue": 1800}],
        "largest-contentful-paint": ["error", {"maxNumericValue": 2500}]
      }
    }
  }
}

速度改善は、多くの事例で直帰率の低下やフォーム到達率の上昇につながることが報告されています。相関であり因果の単独証明ではありませんが、Web Vitalsの改善が下流KPIの底上げに寄与する傾向は、A/Bの同時期比較でも観察されます⁵⁶⁷。

メディア運用は「クエリ×クリエイティブ×実験設計」の科学

アカウント構成で伸びる企業は、クエリ(検索語句)の被覆とノイズ除去、アセットとメッセージの探索、そして実験デザインが精緻です。広範囲一致を使う場合でも除外語(ネガティブキーワード)の衛生管理とファネル別の意図設計が徹底され、レスポンシブ検索広告のアセットは強い訴求の仮説に基づいて体系的に入れ替わります。媒体機械学習に任せるのではなく、学習を良質なデータで“しつける”姿勢が成果の差を生みます。地理や時間帯、デバイス、オーディエンスのセグメントで並列のホールドアウト(同時期に意図的に広告を止める対象を設ける設計)を行い、インクリメンタリティ(純増効果)を測ると、現実的な最適化の幅が見えてきます。

検索語句の品質監査をデータで回す

人手の目視レビューに頼ると検知漏れが増えます。APIで検索語句のパフォーマンスを定期取得し、閾値でレビューキューに自動登録します。

# search_terms_audit.py
import os
import csv
from google.ads.googleads.client import GoogleAdsClient
from google.ads.googleads.errors import GoogleAdsException

QUERY = """
SELECT search_term_view.search_term, 
       metrics.impressions, metrics.clicks, metrics.conversions, 
       metrics.cost_micros, segments.date
FROM search_term_view
WHERE segments.date DURING LAST_30_DAYS
"""

def fetch():
    client = GoogleAdsClient.load_from_storage()
    ga_service = client.get_service("GoogleAdsService")
    stream = ga_service.search_stream(customer_id=os.environ["CID"], query=QUERY)
    for batch in stream:
        for row in batch.results:
            yield (
                row.search_term_view.search_term,
                row.metrics.impressions,
                row.metrics.clicks,
                row.metrics.conversions,
                row.metrics.cost_micros / 1_000_000,
                row.segments.date
            )

def export_csv(path="search_terms.csv"):
    with open(path, "w", newline="") as f:
        writer = csv.writer(f)
        writer.writerow(["term", "impr", "clicks", "conv", "cost", "date"])
        for r in fetch():
            writer.writerow(r)

if __name__ == "__main__":
    try:
        export_csv()
    except GoogleAdsException as e:
        for err in e.failure.errors:
            print("API Error:", err.message)
        raise

CRMのステージ遷移を広告に戻す

SQL化、商談化、受注の時点で価値を付与し、コンバージョンの重みを更新すると、媒体の最適化は短期CVから事業価値(LTV)に寄っていきます。WebhookでCRMからイベントを受け、Pub/Subやキューでバッファし、重複排除してからアップロードします。

// functions/index.js (Firebase Functions)
import functions from 'firebase-functions';
import {PubSub} from '@google-cloud/pubsub';

const pubsub = new PubSub();

export const crmWebhook = functions.https.onRequest(async (req, res) => {
  try {
    const event = req.body; // validate signature in production
    await pubsub.topic('crm-events').publishMessage({ json: event });
    res.status(202).send({ status: 'queued' });
  } catch (e) {
    console.error(e);
    res.status(500).send({ error: 'internal_error' });
  }
});

経営視点のKPI設計が入札と予算を決める

媒体のKPIをフォーム送信やMQLに置いたままだと、入札の上限はすぐに壁に当たります。許容CACはLTV(顧客生涯価値)×粗利×回収期間係数で定義し、広告ごとに異なるLTVを考慮した重みづけで最適化すると、規模と効率を同時に追えます。再現可能性のあるルールで予算を動かし、広告の役割を明確にすることで、アロケーションの議論は感覚論から卒業します。

BigQueryでCACとペイバックを可視化

受注金額、継続率、粗利率を組み合わせ、チャネル×キャンペーン×クエリの粒度でCACとペイバックを算出します。

-- cac_payback.sql
WITH cost AS (
  SELECT date, campaign, SUM(cost) AS cost
  FROM ad_cost_daily
  GROUP BY 1,2
),
lead AS (
  SELECT date, campaign, lead_id
  FROM leads
),
revenue AS (
  SELECT l.campaign, SUM(o.amount * o.gross_margin) AS gm
  FROM opportunities o
  JOIN leads l ON o.lead_id = l.lead_id
  WHERE o.is_won = TRUE
  GROUP BY 1
)
SELECT c.campaign,
       SUM(c.cost) AS spend,
       COUNT(DISTINCT l.lead_id) AS leads,
       SAFE_DIVIDE(SUM(c.cost), COUNT(DISTINCT l.lead_id)) AS cac_per_lead,
       r.gm AS gross_margin_value,
       SAFE_DIVIDE(r.gm, SUM(c.cost)) AS gm_roas
FROM cost c
LEFT JOIN lead l USING(date, campaign)
LEFT JOIN revenue r USING(campaign)
GROUP BY 1, r.gm

許容CACの計算と入札上限の自動提案

単純化したモデルでも、LTVと粗利から許容CACを求め、現在のCVR・CTR・CPCから入札上限を推定できます。実運用では入札戦略の自動化に直接介入しない方針でも、意思決定のガードレールとして機能します。

# bid_cap.py
import math
from dataclasses import dataclass

@dataclass
class Inputs:
    ltv: float           # 顧客生涯売上
    gross_margin: float  # 粗利率 0-1
    payback_months: int  # 目標回収期間
    cvr: float           # LP→CVのコンバージョン率
    ctr: float           # クリック率


def allowable_cac(i: Inputs) -> float:
    # 期間を短くするほど許容CACは厳しくなる(単純モデル)
    factor = max(0.3, min(1.0, 12 / i.payback_months))
    return i.ltv * i.gross_margin * factor


def bid_cap(i: Inputs, target_cac: float) -> float:
    # 単純化: CPC ≈ CAC × CTR × CVR
    return target_cac * i.ctr * i.cvr

if __name__ == "__main__":
    ins = Inputs(ltv=300000, gross_margin=0.7, payback_months=12, cvr=0.05, ctr=0.06)
    cac = allowable_cac(ins)
    cap = bid_cap(ins, cac)
    print({"allowable_cac": round(cac), "bid_cap_jpy": round(cap, 2)})

この枠組みを使えば、営業サイドの受注率や平均受注額が改善したとき、広告側がどれだけ攻められるかを即座に判断できます。逆にLPのCVRや速度が落ちた場合は、許容CACが縮み入札上限が下がるため、媒体で無理に踏まず品質改善を優先する合図になります。意思決定が事実と一貫している限り、短期のブレに振り回されることは減ります。

より実践的な深掘りとして、B2Bのリード評価やSLA設計の基礎はこちらの解説、ファーストパーティデータの活用は同意管理と計測の特集、アトリビューション比較はモデル選定ガイドが参考になります。

まとめ:技術が運用を押し上げる

成果が出る企業と出ない企業の差は、媒体選びよりも計測・データ連携・ランディング体験・経営KPIの一貫性にあります。広告の自動化は、良質な一次データと高速なLP、明確な許容CACというレールが整って初めて最大化します。今日できる最小の一歩として、Enhanced Conversionsとオフラインコンバージョンのセットアップ³⁴、Web Vitalsの収集とLighthouse CIの導入、そしてLTVと粗利から許容CACを定義する作業から始めてみてください。技術チームがこの土台を作れば、運用の試行錯誤は学習速度を増し、次の四半期には指標が揃って動き始めるはずです。あなたの組織では、どのボトルネックから解きほぐしますか。

参考文献

  1. Search Engine Journal. Data: What’s a Good CTR, CPA, Conversion Rate in AdWords? (2018). https://www.searchenginejournal.com/data-whats-good-ctr-cpa-conversion-rate-adwords-2018/248947/
  2. Chief Marketer. Study: Last-Touch Ad Attribution Undervalues Facebook by as Much as 30%. https://chiefmarketer.com/study-last-touch-ad-attribution-undervalues-facebook-by-as-much-as-30/
  3. Google 広告ヘルプ. リードの拡張コンバージョンについて(Enhanced Conversions for Leads). https://support.google.com/google-ads/answer/15713840?hl=ja
  4. Google 広告ヘルプ. オフライン コンバージョンのインポートに関するガイド. https://support.google.com/google-ads/answer/15479791?hl=ja
  5. Think with Google. Mobile page speed data: Why speed matters. https://www.thinkwithgoogle.com/marketing-strategies/app-and-mobile/mobile-page-speed-data/
  6. web.dev. Largest Contentful Paint (LCP). https://web.dev/articles/lcp
  7. web.dev. Interaction to Next Paint (INP). https://web.dev/inp/
  8. Workato. Lead Response Time Study: Why speed to lead matters. https://www.workato.com/the-connector/lead-response-time-study/