Google Analytics4で見る広告効果:キャンペーン分析の基礎
Google Analytics4で見る広告効果:キャンペーン分析の基礎
統計と仕様を突き合わせると、広告効果の読み方は従来のUA時代から大きく変わっています。計測環境の変化によりクッキーの有効期限短縮や同意取得の影響が拡大し、推定モデルを前提とした評価が避けられなくなりました¹²。GA4ではデータ駆動型アトリビューション(各接点の貢献度を機械学習で按分する評価手法)が標準化され、ルックバックウィンドウ(評価対象に含める期間)やチャネル定義(トラフィック分類ルール)も刷新されています³。さらに、キャンペーン識別の土台であるUTMはsource・medium・campaign・term・contentの5要素に整理され、Google広告の自動タグ(gclid)と手動タグ付けの整合性が実務の肝になります⁴。意思決定の精度を上げるためには、UIレポートだけでなくデータAPIやBigQueryエクスポートを併用し、セッション粒度と収益粒度を一貫した軸で結ぶ必要があります。本稿ではCTOやエンジニアリーダーがチームに展開しやすい形で、設計の前提、実装、分析、運用改善までを技術的に掘り下げます。
なぜGA4で広告効果を測るのか:前提と設計思想
GA4はイベント中心モデルとユーザー単位の集約設計により、同じコンバージョンでもカウントや配分が状況によって変化する特性を持ちます⁵⁶。従来のラストクリック偏重から、機械学習による配分や再来訪の寄与を加味した評価へ移行しているため³、単一のレポートで真実に到達できるという発想は捨てた方が健全です。まずビジネスKPIを明確化し、評価単位を売上、粗利、LTV、サブスク継続などどこに置くのかをはっきりさせます。次にユーザー単位とセッション単位のどちらで意思決定するかを定め、アトリビューションモデルの選択とルックバック期間の整合性をとります。モデル比較は結論の幅を把握するための手段であり、組織内の合意形成を促すためにも複数モデルの差分を定期的に点検する運用が有効です³。
データロスを織り込んだKPI設計
同意モードやITPにより、完全なトラッキングは期待できません¹²。したがって、発注や請求といったバックエンドの真実データで最終指標を締め、GA4ではトレンド把握とチャネル間の相対比較に軸足を置くのが現実的です。UIの合計値を会社の公式数字にせず、BigQueryや基幹データと突合するデータパイプラインを用意して、差異の範囲を監視する体制を整えます。エンジニアリングの視点では、スキーマの進化や指標定義の変更に耐えるスナップショット設計が重要になります。
アトリビューションの前提を握る
GA4のプロパティ設定で定義したアトリビューションモデルとルックバック期間は、探索と広告レポートに反映されます³。データAPIは基本的にプロパティの既定設定に従いますが、モデル比較による意思決定はUIのモデル比較レポートか、BigQueryでの再集計が適しています³。評価の軸を変えると順位が入れ替わるのは自然な現象であり、CPAやROASを単一のモデルで最適化するのではなく、上流の獲得と下流の収益化を分けて評価すると合意が取りやすくなります。
キャンペーン分析の基礎設計:UTMと計測の土台
キャンペーン分析の正確性は、ほぼ命名規約の厳守で決まります。手動UTMではsourceとmediumの語彙をチームで固定し、広告プラットフォームの自動タグと衝突しないようにします。Google広告は自動タグ付けのgclidが優先されるため、GA4内のトラフィック分類では自動タグの値が使われます⁴。手動UTMを混在させたい場合は、媒体別にルールと例外を明文化して運用します。ディープリンクや短縮URLを使う場合はリダイレクトによるパラメータ欠損を検証し⁷、リファラ除外とクロスドメイン設定で自己リファラを除去することが漏れの抑止になります⁸。
計測コードの最小実装例(gtag.js)
フロント実装はコンバージョンイベントの正確な送信が要です。購入イベントを例に、通貨とアイテム情報を含めて送信します。実運用では同意状態の確認(Consent Mode)、重複送信の防止、サーバーサイド計測の併用を検討してください¹。
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXXXX');
// 購入完了時に実行
function trackPurchase(order) {
gtag('event', 'purchase', {
currency: 'JPY',
transaction_id: order.id,
value: order.total, // 税・送料を含むかは設計に合わせる
coupon: order.coupon || undefined,
items: order.items.map(function(i){
return {
item_id: i.sku,
item_name: i.name,
price: i.price,
quantity: i.qty,
item_category: i.category
};
})
});
}
</script>
GA4で広告効果を読む:UI、データAPI、BigQueryの使い分け
日次の異常検知やメディア別のトレンド把握はUIで十分に賄えます。広告のモデル比較レポートでアトリビューションを切り替え³、トラフィック獲得レポートでsession default channel group(GA推奨のチャネル分類)やsession source/medium(セッション単位の流入元/媒体)の推移を見れば、施策の成否は敏感に反映されます。一方で予算配分や入札調整といった責任のある意思決定には、指標定義を厳密にコントロールできるデータAPIとBigQueryが欠かせません。ここからは実装を具体的に示します。
Python(Data API)でキャンペーン別のKPIを取得する
Data APIは集計済みの数値を高速に取得でき、日次のダッシュボード更新に向きます。プロパティの既定アトリビューション設定が適用されること、クォータ上限とサンプリングの可能性を前提に設計します。例ではセッション基準のチャネルとキャンペーン名で、セッション数、コンバージョン、収益を取得しています。例外時はログを残し、バックオフして再試行するのが安全です。
from google.analytics.data_v1beta import BetaAnalyticsDataClient
from google.analytics.data_v1beta.types import DateRange, Dimension, Metric, RunReportRequest
from google.api_core.exceptions import GoogleAPIError
import os, time
PROPERTY_ID = os.getenv("GA4_PROPERTY_ID") # 形式: 123456789
client = BetaAnalyticsDataClient()
def run_campaign_report():
request = RunReportRequest(
property=f"properties/{PROPERTY_ID}",
dimensions=[
Dimension(name="sessionDefaultChannelGroup"),
Dimension(name="sessionCampaignName")
],
metrics=[
Metric(name="sessions"),
Metric(name="conversions"),
Metric(name="totalRevenue")
],
date_ranges=[DateRange(start_date="30daysAgo", end_date="yesterday")]
)
tries = 0
while True:
try:
response = client.run_report(request)
for row in response.rows:
channel = row.dimension_values[0].value
campaign = row.dimension_values[1].value
sessions = float(row.metric_values[0].value)
conversions = float(row.metric_values[1].value)
revenue = float(row.metric_values[2].value)
cvr = conversions / sessions if sessions else 0.0
print(channel, campaign, sessions, conversions, revenue, cvr)
break
except GoogleAPIError as e:
tries += 1
if tries > 3:
raise
time.sleep(2 ** tries)
if __name__ == "__main__":
run_campaign_report()
この取得は中規模のプロパティで行数も比較的コンパクトに収まり、短時間で完了します。レポート頻度が高い場合は集計結果をBigQueryに永続化してUIにはキャッシュを見せると、安定性とコストのバランスが取れます。
BigQueryでセッションと収益を結び、キャンペーン別に集計する
イベントエクスポートは未集計のため、自由度の高い再集計が可能です。ここではセッション開始イベントからセッション属性を取り出し、購入イベントの収益と突合します。あわせて注文数(transaction_idの重複排除)も算出し、後段のROAS/CAC計算に接続できる形にします。スキーマは更新されることがあるため、本番ではビュー化して参照側を安定させるのが定石です。
-- <PROJECT>.<DATASET>.events_* を適宜置換
WITH sessions AS (
SELECT
event_date,
(SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'ga_session_id') AS session_id,
user_pseudo_id,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'source') AS session_source,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'medium') AS session_medium,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'campaign') AS session_campaign
FROM `project.dataset.events_*`
WHERE event_name = 'session_start'
), revenue AS (
SELECT
event_date,
user_pseudo_id,
(SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'ga_session_id') AS session_id,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'currency') AS currency,
(SELECT value.double_value FROM UNNEST(event_params) WHERE key = 'value') AS revenue_value,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'transaction_id') AS transaction_id
FROM `project.dataset.events_*`
WHERE event_name = 'purchase'
)
SELECT
s.event_date,
COALESCE(s.session_source, '(not set)') AS source,
COALESCE(s.session_medium, '(not set)') AS medium,
COALESCE(s.session_campaign, '(not set)') AS campaign,
COUNT(DISTINCT CONCAT(s.user_pseudo_id, ':', CAST(s.session_id AS STRING))) AS sessions,
COUNT(DISTINCT IF(r.session_id IS NOT NULL, CONCAT(s.user_pseudo_id, ':', CAST(s.session_id AS STRING)), NULL)) AS converting_sessions,
COUNT(DISTINCT r.transaction_id) AS orders,
SUM(r.revenue_value) AS revenue
FROM sessions s
LEFT JOIN revenue r
ON s.user_pseudo_id = r.user_pseudo_id
AND s.session_id = r.session_id
AND s.event_date = r.event_date
GROUP BY 1,2,3,4
ORDER BY event_date DESC, revenue DESC;
標準的な月間大規模プロパティでも、適切なパーティションとクラスタリングを前提にすれば日次集計は現実的なコストと時間で完了します。実環境のイベント量やクエリ設計により前後する点は留意してください。
費用データを加えてROASとCACを算出する
費用は媒体側の真実ソースを使うのが原則です。まずはGoogle広告の費用をBigQueryに取り込み、キャンペーン名(またはID)と日付で結合してROASとCACを計算します。通貨と命名の差異を吸収する正規化層を用意しておくと、運用負荷を最小化できます。
-- ads_cost テーブルは日次・キャンペーン行で費用を保持
-- スキーマ例: date (YYYYMMDD), campaign (STRING), cost_jpy (NUMERIC)
WITH perf AS (
SELECT event_date AS date, campaign, SUM(orders) AS orders, SUM(revenue) AS revenue
FROM (
-- 直前の集計クエリをビュー化して参照するのが推奨
SELECT event_date, campaign, orders, revenue FROM `project.dataset.session_revenue_daily`
)
GROUP BY 1,2
)
SELECT
p.date,
p.campaign,
p.orders,
p.revenue,
c.cost_jpy,
SAFE_DIVIDE(p.revenue, c.cost_jpy) AS roas,
SAFE_DIVIDE(c.cost_jpy, p.orders) AS cac
FROM perf p
LEFT JOIN `project.dataset.ads_cost` c
ON p.date = c.date AND p.campaign = c.campaign
ORDER BY p.date DESC, roas DESC;
キャンペーン名は媒体側の改名で変わることがあるため、Google広告ではIDで結合し、表示名はディメンションとして扱う方が堅牢です。IDと名前の対応は履歴テーブルとして保持し、遡及時に正しい表示名で出力します。
Google Ads APIで費用を取得し、BigQueryに着地させる
費用の自動連携は安定運用に直結します。以下は最小限の実装例で、GAQLで日次費用を引き、BigQueryへロードします。実運用ではシークレットの保護、レート制御、冪等性の確保、スキーマのバージョニングを追加してください。
from google.ads.googleads.client import GoogleAdsClient
from google.cloud import bigquery
from datetime import date, timedelta
import os
CUSTOMER_ID = os.getenv("GOOGLE_ADS_CUSTOMER_ID") # 形式: 1234567890
GOOGLE_ADS_CONFIG = os.getenv("GOOGLE_ADS_YAML_PATH")
BQ_TABLE = "project.dataset.ads_cost"
client = GoogleAdsClient.load_from_storage(GOOGLE_ADS_CONFIG)
bq = bigquery.Client()
def fetch_cost_rows(start, end):
ga_service = client.get_service("GoogleAdsService")
query = f"""
SELECT
segments.date,
campaign.id,
campaign.name,
metrics.cost_micros
FROM campaign
WHERE segments.date BETWEEN '{start}' AND '{end}'
"""
rows = ga_service.search(customer_id=CUSTOMER_ID, query=query)
for r in rows:
d = r.segments.date
cid = r.campaign.id
name = r.campaign.name
cost = r.metrics.cost_micros / 1_000_000
yield {"date": d.replace("-", ""), "campaign_id": str(cid), "campaign": name, "cost_jpy": cost}
def load_to_bq(rows):
errors = bq.insert_rows_json(BQ_TABLE, list(rows))
if errors:
raise RuntimeError(errors)
if __name__ == "__main__":
end = date.today() - timedelta(days=1)
start = end - timedelta(days=30)
load_to_bq(fetch_cost_rows(start.isoformat(), end.isoformat()))
品質と意思決定の精度を高める運用の勘所
広告レポートで目立つ「Unassigned」や「(not set)」は、分析の解像度を落とします。原因はUTM欠損、リダイレクトによるパラメータ喪失、クロスドメイン設定の不足、ランディングに至る前のアプリ遷移など多岐にわたります⁹。影響度を常時可視化し、減少傾向にあるかどうかをSLOのように追うのが実務的です。次のクエリはセッションのうち直行や未設定が占める割合を日次で把握する例です。
WITH s AS (
SELECT
event_date,
(SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'ga_session_id') AS sid,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'source') AS source,
(SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'medium') AS medium
FROM `project.dataset.events_*`
WHERE event_name = 'session_start'
)
SELECT
event_date,
COUNT(DISTINCT sid) AS sessions,
COUNT(DISTINCT IF(source IS NULL OR medium IS NULL, sid, NULL)) AS unset_sessions,
COUNT(DISTINCT IF(source = '(direct)' AND medium = '(none)', sid, NULL)) AS direct_sessions,
SAFE_DIVIDE(COUNT(DISTINCT IF(source IS NULL OR medium IS NULL, sid, NULL)), COUNT(DISTINCT sid)) AS unset_ratio,
SAFE_DIVIDE(COUNT(DISTINCT IF(source = '(direct)' AND medium = '(none)', sid, NULL)), COUNT(DISTINCT sid)) AS direct_ratio
FROM s
GROUP BY event_date
ORDER BY event_date DESC;
モデル比較は恣意性の温床ではなく、ビジネス仮説の検証装置です。上流の動画やディスカバリーがデータ駆動で高く評価される一方、ラストクリックではブランド検索に寄りやすいという差分を定常的に観察し³、KPIに対する責任範囲を分解して議論すると、予算配分の意思決定が健全になります。UIのモデル比較レポートで傾向を掴み、施策レビューの場ではBigQueryで同一期間・同一定義の数値を固定して提示する運用がチームの信頼につながります。
パフォーマンスとガバナンスを両立させるために、イベントテーブルは日付パーティションとクラスタリングを活用し、必要なディメンションに合わせて派生ビューを用意します。Data APIはミニマムな指標で呼び出し回数を抑え、集計後の結果をキャッシュします。異常検知はアラートのしきい値を明示し、原因究明の手順をSOPとして文書化しておくと、属人化を防げます。なお、より詳細な実装指針や命名規約のテンプレートは、UTM命名規約ガイド、データパイプライン設計の基本、GA4×BigQuery実装ガイド、マーケとエンジニアの連携論点といった一般的な資料が参考になります。
ケースの当てはめと導入効果
仮に月額大規模な広告投資を行うD2Cで、CVを購入、評価軸を売上とした場合を考えます。Data APIで日次のチャネル別の基礎指標を更新し、週次ではBigQueryでセッションと収益、費用を結合したROASやCACを確認します。導入初期のベンチマークでは、UIとBigQueryの総売上差異を小さく抑え、Unassigned比率を段階的に低下させ、モデル比較で上流施策の寄与を加味した予算再配分により購買あたりの費用効率の改善が期待できます。もちろんビジネスによって収益構造や滞留期間は異なるため、KPIの定義と窓の長さは自社に最適化してください。
まとめ:データの不確実性を味方に、意思決定を速くする
GA4の広告効果は、単一の正解を探す旅ではありません。不確実性を認めたうえで、UTMと実装の品質を底上げし、UI・データAPI・BigQueryという三つの視点で数字を立体的に読むことが、組織の意思決定を速く強くします。今日できる一歩として、命名規約の棚卸し、クロスドメインとリファラ除外の再点検⁸、そして日次の自動レポート基盤の整備から始めてみてください。来月の予算会議で根拠のある配分を提案できるかどうかは、明日のデータ品質にかかっています。次にどのチャネルのどのキャンペーンを深掘りしますか。根拠を持って語れる状態を、チームで一緒に作っていきましょう。
参考文献
- Google サポート: 同意モード(Consent Mode)について. https://support.google.com/analytics/answer/11161109
- SiTest 企業ブログ: ITPの現状とCookie有効期限の変遷. https://sitest.jp/blog/?p=23241
- Google サポート: GA4 のアトリビューション レポートとモデル. https://support.google.com/analytics/answer/10596866
- Google サポート: 自動タグ設定と手動タグ設定の取り扱い(utm_content/utm_term を含む). https://support.google.com/analytics/answer/11242870
- data-be.at Magazine: UA と GA4 の最大の違い(イベント計測中心への移行). https://www.data-be.at/magazine/ga4-ua/
- iCAT 企業ブログ: GA4 レポートの考え方とユーザー軸分析. https://www.icat.co.th/ja/blog/report-ga4/
- e-Agency コラム: リダイレクトでの UTM パラメータ喪失と対策. https://googleanalytics360-suite.e-agency.co.jp/column/2235
- Google サポート: クロスドメイン計測と自己参照の自動検出. https://support.google.com/analytics/answer/10327750
- Stape 企業ブログ: GA4 の Unassigned / (not set) トラフィックの原因と対策. https://stape.io/blog/unassigned-and-not-set-traffic-source-in-ga4