【後編】GA4×BigQuery セッションのズレを補正する実践ガイド|チャネルの再現とSQL実装のポイント

GA4のセッション数と、BigQueryにエクスポートされたデータによるセッション数がズレる——これは多くの分析担当者が直面する悩みのひとつです。
また、GA4画面でのデータと比べて、BigQueryにエクスポートしたデータではダイレクト流入が異常に多くなってしまう、という事例もよく聞きますが、これらの差異は両者の仕様の違いによるものです。

本記事では、そうしたズレが発生する背景や構造を踏まえ、BigQuery上でのGA4デフォルトチェネルグループの再現方法や、流入元別セッションをGA4の数値の定義に近づけるよう補正するためのテクニックを紹介します。
また、2024年7月17日に追加された新しいフィールド「session_traffic_source_last_click」についてもご紹介します。

※GA4とBigQueryの構造的な違いにフォーカスした前編記事もあわせてご参照ください:
👉 GA4とBigQueryで流入元セッションがズレる理由|構造の違いを理解する – 前編

⚠️ 本記事に引用しているSQLクエリは未検証のため、正常な動作は保証できません。検証・デバッグ済みのSQLクエリ全文は、次回の検証記事に掲載しますので、そちらをご参照ください。

1. チャネル分類の補正と整合性の確保

前編でも触れた通り、GA4ではデフォルトでチャネルが分類(デフォルトチャネルグループ)されていますが、BigQueryのエクスポートデータにはこの分類情報が含まれていません。そのため、チャネル分類ロジックをBigQuery上で再現する必要があります。

以下は、チャネル再分類を含む簡易的なSQL例です(詳細は前回記事を参照→ GA4チャネルグループ分類の正規表現まとめ|BigQuery用サンプルSQLつき):

CASE
  WHEN REGEXP_CONTAINS(medium, r"^organic$") AND REGEXP_CONTAINS(source, r'(google|bing|yahoo|baidu|duckduckgo|search\.brave\.com)') THEN 'Organic Search'
  WHEN REGEXP_CONTAINS(source, r'(facebook|instagram|twitter|t.co|linkedin|pinterest)')
       AND REGEXP_CONTAINS(medium, r'^(social|social-organic)$') THEN 'Organic Social'
  WHEN REGEXP_CONTAINS(source, r'(facebook|instagram|twitter|t.co|linkedin|pinterest)')
       AND REGEXP_CONTAINS(medium, r'^(.*cp.*|ppc|retargeting|paid.*)$') THEN 'Paid Social'
  WHEN REGEXP_CONTAINS(medium, r'^(cpc|ppc|paidsearch)$')
       AND REGEXP_CONTAINS(source, r'(google|bing|yahoo|baidu|duckduckgo|search\.brave\.com)') THEN 'Paid Search'
  WHEN REGEXP_CONTAINS(medium, r'^(email|e-mail)$') THEN 'Email'
  WHEN REGEXP_CONTAINS(medium, r'^(affiliate)$') THEN 'Affiliates'
  WHEN REGEXP_CONTAINS(medium, r'^(display|banner|cpm)$') THEN 'Display'
  WHEN REGEXP_CONTAINS(medium, r'^(video|paidvideo)$') THEN 'Paid Video'
  WHEN REGEXP_CONTAINS(medium, r'^(organic-video)$') THEN 'Organic Video'
  WHEN REGEXP_CONTAINS(medium, r'^(audio)$') THEN 'Audio'
  WHEN REGEXP_CONTAINS(medium, r'^(sms|text)$') THEN 'SMS'
  WHEN REGEXP_CONTAINS(medium, r'^(push|notification)$') THEN 'Mobile Push Notifications'
  WHEN source = '(direct)' AND (medium = '(not set)' OR medium = '(none)') THEN 'Direct'
  ELSE 'Referral'
END AS default_channel_group

2. セッションの補正と再現テクニック

GA4とBigQueryで流入元別セッションがズレる大きな要因の1つに「セッションの属性処理」があります。ダイレクト訪問した再訪ユーザーに対して、GA4画面には前回の流入元からの訪問としてカウントするロジックがありますが、BigQueryのエクスポートデータには2024年7月17日まで反映されていなかったため、手動でロジックを再現しなければなりません。

以下は、代表的な再現方法です:

1)2024年7月17日以降のデータ

2024年7月17日以降のデータは、GA4のロジックを反映したフィールド「session_traffic_source_last_click」を使えばいいので、比較的簡単に実装できます。

SELECT
  user_pseudo_id,
  (SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'ga_session_id') AS session_id,
  session_traffic_source_last_click.source AS session_source,
  session_traffic_source_last_click.medium AS session_medium,
  session_traffic_source_last_click.campaign AS session_campaign
FROM
  `your_project.your_dataset.ga4_events_*`
WHERE
  _TABLE_SUFFIX BETWEEN '20240717' AND '20241231'
  AND event_name = 'session_start'

2)ラストノンダイレクトクリック属性の再現(2024年7月16日以前のデータ)

ただし、2024年7月16日以前のデータには「session_traffic_source_last_click」フィールドが存在しないため、イベントレベルのトラフィックソース情報をもとに、ロジック(ラストノンダイレクトクリック属性)を手動で再現する必要があります。

以下は、Napkyn社によって公開されている再現クエリの一例です:

出典:
Session-Level Attribution in GA4 with BigQuery: A Practical Guide

WITH SessionData AS (
  SELECT
    user_pseudo_id,
    FORMAT_TIMESTAMP('%Y%m%d', TIMESTAMP_MICROS(event_timestamp)) AS event_date,
    event_timestamp,
    (SELECT value.int_value FROM UNNEST(event_params) WHERE key = 'ga_session_id') AS session_id,
    collected_traffic_source.source AS source,
    collected_traffic_source.medium AS medium,
    collected_traffic_source.campaign AS campaign,
    CASE 
      WHEN (collected_traffic_source.source IS NULL OR collected_traffic_source.source = 'direct') 
           AND (collected_traffic_source.medium IS NULL OR collected_traffic_source.medium = '(none)') 
      THEN TRUE ELSE FALSE 
    END AS is_direct
  FROM
    `your_project.your_dataset.ga4_events_*`
  WHERE
    _TABLE_SUFFIX BETWEEN '20230101' AND '20231231'
  GROUP BY
    user_pseudo_id, event_date, event_timestamp, session_id, source, medium, campaign, is_direct
),

UserJourneys AS (
  SELECT
    user_pseudo_id,
    session_id,
    event_date,
    MIN(event_timestamp) AS session_start_time,
    MAX(is_direct) AS is_direct_session,
    CASE 
      WHEN MAX(is_direct) = FALSE THEN MAX(IF(is_direct = FALSE, source, NULL)) 
      ELSE NULL 
    END AS non_direct_source,
    CASE 
      WHEN MAX(is_direct) = FALSE THEN MAX(IF(is_direct = FALSE, medium, NULL)) 
      ELSE NULL 
    END AS non_direct_medium,
    CASE 
      WHEN MAX(is_direct) = FALSE THEN MAX(IF(is_direct = FALSE, campaign, NULL)) 
      ELSE NULL 
    END AS non_direct_campaign
  FROM
    SessionData
  GROUP BY
    user_pseudo_id, session_id, event_date
),

LastNonDirectAttribution AS (
  SELECT
    a.user_pseudo_id,
    a.session_id,
    a.event_date,
    a.session_start_time,
    a.is_direct_session,
    CASE
      WHEN a.is_direct_session THEN
        LAST_VALUE(b.non_direct_source IGNORE NULLS) OVER (
          PARTITION BY a.user_pseudo_id 
          ORDER BY a.session_start_time 
          ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
        )
      ELSE a.non_direct_source
    END AS attributed_source,
    CASE
      WHEN a.is_direct_session THEN
        LAST_VALUE(b.non_direct_medium IGNORE NULLS) OVER (
          PARTITION BY a.user_pseudo_id 
          ORDER BY a.session_start_time 
          ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
        )
      ELSE a.non_direct_medium
    END AS attributed_medium,
    CASE
      WHEN a.is_direct_session THEN
        LAST_VALUE(b.non_direct_campaign IGNORE NULLS) OVER (
          PARTITION BY a.user_pseudo_id 
          ORDER BY a.session_start_time 
          ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
        )
      ELSE a.non_direct_campaign
    END AS attributed_campaign
  FROM
    UserJourneys a
  LEFT JOIN
    UserJourneys b
  ON
    a.user_pseudo_id = b.user_pseudo_id
    AND b.session_start_time <= a.session_start_time
)

SELECT
  user_pseudo_id,
  session_id,
  event_date,
  is_direct_session,
  COALESCE(attributed_source, 'direct') AS final_source,
  COALESCE(attributed_medium, '(none)') AS final_medium,
  attributed_campaign AS final_campaign
FROM
  LastNonDirectAttribution
ORDER BY
  user_pseudo_id, session_start_time

上記の再現SQLは複雑かつ手間がかかりますが、正確な流入元評価には欠かせません。再現プロセスや参考スクリプトについては、Napkyn社やOptimize Smartのブログに詳しい事例があります。

⚠️ 本記事に引用しているSQLクエリは未検証のため、正常な動作は保証できません。検証・デバッグ済みのSQLクエリ全文は、次回の検証記事に掲載しますので、そちらをご参照ください。

セッションのズレを補正するには、構造への理解が欠かせません。
“なぜズレるのか”という構造面の視点については、前編で詳しく解説しています。あわせてご覧ください。

👉 構造理解を扱った前編はこちら:
GA4とBigQueryで流入元セッションがズレる理由|構造の違いを理解する – 前編


📌 この記事は、GA4 × BigQuery の実務分析を支援する「サイトウオンライン」の実験室よりお届けしました。