GA4・アクセス解析 / 2026.01.11

仮想イベントデータ×PythonでGA4を裏側から理解する|first_visit・セッション・PV・CVRを再現してみた


『GA4の数字をPythonで再現』というタイトル。左側にイベントログ、中央にPythonのコードアイコン、右側にGA4のダッシュボードを配置し、仮想データからGA4指標を算出するプロセスを表現したアイキャッチ画像

GA4の数字(ユーザー数・セッション数・PV・CVR)を見ていて、こう感じたことはありませんか?

  • 数字は見ているけど「結局、何を数えてるのか」が曖昧
  • レポート画面が多くて、どこを見ればいいか迷う
  • CVRを見ても、どの入口(ランディングページ)が効いてるのかピンとこない

そこでこの記事では、仮想イベントデータ(first_visit / session_start / page_view / conversion)を自作し、Python(pandas)でGA4の主要指標を再現します。
「GA4の数字=イベントの集計結果」という感覚を、自分の手で腹落ちさせるのがゴールです。


1 なぜ仮想データで練習するのか

1-1 GA4は「イベントの集計結果」だが、画面だけだと腹落ちしづらい

GA4は、もともと「イベント」を記録し、それを集計して指標として見せています。
しかし、GA4のUIは完成されているぶん、裏側の数え方(ロジック)が見えにくいのが難点です。

だからこそ、いったん裏側に戻って、

  • イベントログを作る
  • 自分で数える
  • その数字がGA4のどこに対応するかを確認する

という順番で学ぶと、理解が急に進みます。

1-2 実データが少ないと“検証の練習”ができない(特にCV周り)

小規模サイトほど、CVが少なく、LP別CVRなどがブレて判断しづらいです。
仮想データなら、母数を確保しながら「検証の型」を安全に練習できます。

1-3 仮想データ×Pythonで得られるメリット

  • 指標を「自分の言葉」で定義できる(何を数えているか説明できる)
  • GA4画面を見たときに「この指標は何の集計か」が想像できる
  • 数字を改善アクション(次の打ち手)につなげやすくなる

1-4 この記事のゴール

この記事の解説フロー。データ生成、Pythonによる集計、主要4指標の整理、GA4画面との対応付け、そして改善示唆への繋げ方という5つのステップを1枚にまとめた全体図

読了後に、次ができれば成功です。

  • ユーザー/新規ユーザー/セッション/PV/LP別CVRをPythonで再現できる
  • GA4で「どのレポートで何を見るか」を迷わない
  • “入口→成果”で改善の仮説が立てられる

2 仮想イベントデータの設計と作り方

2-1 今回の世界観(さくら整体院+ブログ/直近3ヶ月)

  • 対象サイト:さくら整体院+ブログ
  • 期間:直近3ヶ月想定
  • 最終KGI:新規予約(CV)を増やす
  • 主役KPI:初回訪問ベースのランディングページ別CVR
  • 施策レイヤー:流入最適化(Organic / Social など)

2-2 今回のイベント設計(GA4の基本イベント+CV)

  • first_visit:新規ユーザーの起点
  • session_start:セッション開始
  • page_view:ページ閲覧(PVの正体)
  • conversion/contact/ で発生する予約CVイベント(仮)

2-3 カラム定義(このデータで何が再現できるか)

GA4の指標とイベントの対応図。Usersはuser_id、New Usersはfirst_visit、Sessionsはsession_id(session_start)、PVはpage_view、CVは特定ページのconversionイベントといった、集計の根拠を整理したマップ

今回のCSVは次のカラムを持ちます。

  • event_timestamp:イベントの日時(順序の根拠。LP判定に必須)
  • date:日付(期間集計用)
  • user_id:ユーザー単位の集計
  • session_id:セッション単位の集計
  • event_name:first_visit / session_start / page_view / conversion
  • page_path:ページ別PV、LP判定に使う
  • channel_group:Organic / Social / Direct / Referral など

このあとのPython集計は、同じCSVを使って一緒に進めます。
まずは練習用の仮想イベントデータをダウンロードしてください。

ダウンロードできたら、次の章で pandasで読み込んで集計していきます。

2-4 ブレ防止の「定義宣言」

本文の途中で迷わないように、ここで定義を固定します。

  • 新規ユーザー:first_visit を持つユーザー
  • セッション:session_id(セッション数=session_idユニーク)
  • PV:page_view 件数
  • CV:conversion(/contact/で発生)
  • LP:セッション内で最初の page_viewpage_path(最小event_timestamp)
  • 初回訪問ベース:新規ユーザーの最初のセッションのみ対象

3 PythonでGA4の主要指標を再現してみる

準備:CSVを読み込み、分析しやすい形に整える

import pandas as pd

df = pd.read_csv("ga4_virtual_events_sakura_seitaiin_3months.csv")

# 日時型に変換(LP判定などで必須)
df["event_timestamp"] = pd.to_datetime(df["event_timestamp"])

# 必要なら期間で絞る(今回はCSVが3ヶ月想定なので省略してもOK)
# df = df[(df["date"] >= "2025-10-10") & (df["date"] <= "2026-01-09")]

指標1:ユーザー数・新規ユーザー数を再現する

ビジネスの問い

  • 直近3ヶ月で「どれくらい人が来たか?」(ユーザー数)
  • 「初めて来た人はどれくらいか?」(新規ユーザー数)
  • 初回流入チャネル(first touch)はどこが強いか?

Python集計ロジック

  • ユーザー数=期間内の user_id ユニーク数
  • 新規ユーザー数=event_name=="first_visit"user_id ユニーク数
  • 初回流入チャネル=first_visit 行の channel_group で集計(重複なし)

Pythonコード例

users = df["user_id"].nunique()

new_users = df.loc[df["event_name"]=="first_visit", "user_id"].nunique()

first_touch = (
    df[df["event_name"]=="first_visit"]
    .groupby("channel_group")["user_id"].nunique()
    .sort_values(ascending=False)
)

print(users, new_users)
print(first_touch)

結果(あなたの集計)

  • ユニークユーザー数:62
  • 新規ユーザー数:42
  • 初回流入チャネル(新規ユーザー42の内訳)
    • Organic Search 27 / Social 7 / Direct 5 / Referral 3

ミニ対応表(GA4対応の書き込み欄)

  • Python:user_idユニーク / first_visitユーザー
  • GA4:ユーザー / 新規ユーザー(獲得系)
GA4のユーザー獲得レポート画面。ディメンションに『最初のユーザーのデフォルトチャネルグループ』を選択し、Organic SearchやSocialなどの流入元ごとに、ユーザー数と新規ユーザー数の違いを表示しているスクリーンショット

指標1:ユーザー数・新規ユーザー数(ミニノック4問)

問1(選択式:定義)

Pythonで「新規ユーザー数」を今回の定義で最も正しく再現するのはどれ?
A. user_id のユニーク数
B. event_name=="first_visit"user_id ユニーク数
C. event_name=="session_start" の件数
D. event_name=="page_view" の件数

解答:B
解説: 今回は「新規ユーザー=first_visitを持つユーザー」と定義しているためです。


問2(記述:重複の理由)

チャネル別ユニークユーザー数を単純に集計すると、合計が全体ユーザー数を超えることがあります。なぜ?(1〜2行)

解答例: 同一ユーザーが期間中に複数チャネルで来訪し、チャネル別集計だと重複計上されるため。
解説: 迷いを減らすには「初回流入(first touch)で1ユーザー1チャネル」に固定します。


問3(実務:読み取り)

新規ユーザー(first touch)が Organic 27、Social 7、Direct 5、Referral 3 でした。
この結果から言えることとして最も妥当なのはどれ?
A. Socialが最重要なのでSEOは不要
B. 新規獲得はOrganic依存が大きく、Socialは補助的に効いている可能性が高い
C. Directが多いので広告が原因
D. Referralが少ないのでサイトが壊れている

解答:B
解説: 新規獲得はOrganicが主力、Socialは一定の補完。ここから「Organicの質(LP別CVR)」と「Socialの再訪(指標2)」を確認すると改善に直結します。


問4(実務:次に見るべきもの)

新規ユーザー数が増えた一方で、予約(CV)が増えません。次に優先して見るべき組み合わせはどれ?
A. PVだけを増やす
B. ランディングページ別CVR(初回訪問ベース)と、流入チャネル別CVR
C. サイトの色変更
D. 直帰率(このデータにはないが、それだけを見る)

解答:B
解説: “獲得(新規)”と“成果(CV)”が結びついているかを確認するには、入口(LP/チャネル)別のCVRが最短です。


指標2:セッション数・1ユーザーあたりセッション数を再現する

ビジネスの問い

  • 1人が何回くらい訪問しているか?(リピートの兆し)
  • 初回流入チャネル別に、再訪しやすいユーザー層はあるか?

Python集計ロジック(文章)

  • セッション数=session_id ユニーク数
  • 1ユーザーあたりセッション数=セッション数 ÷ ユーザー数
  • 初回流入チャネル別=first_touchでユーザーを固定し、そのユーザー群のセッション数を集計

Pythonコード例(全体)

sessions = df["session_id"].nunique()
sessions_per_user = sessions / users

print(sessions, sessions_per_user)

Pythonコード例(初回流入チャネル別:B案)

# first_touchチャネル(新規ユーザーのみ)
ft = df[df["event_name"]=="first_visit"][["user_id","channel_group"]].rename(columns={"channel_group":"first_touch_channel"})

# ユーザーにfirst_touchを付与
df2 = df.merge(ft, on="user_id", how="left")

# 新規ユーザー群(first_touchがあるユーザー)のみで、セッション/ユーザー
report = (
    df2[df2["first_touch_channel"].notna()]
    .groupby("first_touch_channel")
    .agg(unique_users=("user_id","nunique"),
         total_sessions=("session_id","nunique"))
)
report["sessions_per_user"] = report["total_sessions"] / report["unique_users"]
print(report.sort_values("sessions_per_user", ascending=False))

結果(あなたの集計)

  • 訪問回数の平均:1.53(中央値1、最大3)
  • 初回流入チャネル別(新規ユーザー群のリピート貢献度)
    • Social:1.57
    • Organic:1.26
    • Direct:1.00
    • Referral:1.00

ミニ対応表(GA4対応の書き込み欄)

  • Python:session_idユニーク / sessions ÷ users
  • GA4:セッション / セッション(またはセッション/ユーザー相当)
GA4のトラフィック獲得レポート画面。セッションのデフォルトチャネルグループ別に、セッション数やエンゲージメントセッション数などの指標を表示し、訪問ベースの流入傾向を確認しているスクリーンショット

指標2:セッション数・1ユーザーあたりセッション数(ミニノック4問)

問1(選択式:定義)

セッション数を最も安定して再現できるのはどれ?
A. event_name=="session_start" の件数
B. session_id のユニーク数
C. user_id のユニーク数
D. event_name=="page_view" の件数

解答:B
解説: セッションは session_id 単位。整合していればAでも同じになりがちですが、Bが基本です。


問2(計算:基礎)

ユーザー数62、セッション数95のとき、1ユーザーあたりセッション数は?(小数第2位)
解答:1.53
解説: 95÷62=1.53。平均訪問回数の目安です。


問3(実務:分布の解釈)

訪問回数別ユーザーが 1回:34、2回:23、3回:5 でした。最も妥当な解釈はどれ?
A. リピートが強いので新規施策は不要
B. 単発訪問が多く、再訪導線(関連記事・CTA・LINE等)に改善余地がある
C. 3回訪問が多いので問題ない
D. セッション数は意味がない

解答:B
解説: 中央値が1回に寄っているため、初回で終わるユーザーが多い。改善するなら「次に読む記事」「予約までの導線」「再訪動機」の設計です。


問4(実務:チャネル別の“質”)

初回流入チャネル別の sessions_per_user が Social 1.57、Organic 1.26 でした。ここから次のアクションとして妥当なのはどれ?
A. Socialは再訪が強い可能性があるので、SNSからの導線(プロフィール/固定投稿/CTA)を整備する
B. Socialは母数が少ないので全て無視する
C. Organicは弱いのでSEO記事を削除する
D. セッション/ユーザーは見ない

解答:A(※ただし母数注意)
解説: Socialが高いのは“関心が続きやすい層”の可能性。ただしサンプルが小さい場合は過信せず、同傾向が続くか確認します。


指標3:PV・ページ別PVを再現する

ビジネスの問い

  • どのページがよく見られているか?(人気コンテンツ)
  • ブログ記事は読まれているか?(入口候補の発見)

Python集計ロジック(文章)

  • PV=event_name=="page_view" の件数
  • ページ別PV=page_viewを page_path でcount
  • 併記:ページ別ユニークユーザー(そのページを見たユーザー数)

Pythonコード例

pv = df[df["event_name"]=="page_view"]

page_pv = (
    pv.groupby("page_path")
    .agg(pv_count=("event_name","count"),
         unique_users=("user_id","nunique"))
    .sort_values("pv_count", ascending=False)
)

print(page_pv.head(10))

結果(あなたの集計:TOP10例)

  • /contact/ が最も多い
  • /menu/、/(トップ)も上位
  • ブログ記事では user_engagement、page_view などが上位

ブログ配下に限定(主役へつなぐ)

  • ブログ配下総PV:115
  • ブログ記事別PVランキング
    • /blog/ga4-user-engagement/(23)
    • /blog/ga4-page-view/(18)
    • /blog/seo-basics/(16) …など

ミニ対応表(GA4対応の書き込み欄)

  • Python:page_view件数(Views相当)
  • GA4:ページ別の表示回数(Views相当)
GA4の『ページとスクリーン』レポート画面。ページパスごとの表示回数(PV)やユーザー数が一覧で表示されており、各コンテンツの閲覧ボリュームを確認しているスクリーンショット

同画面で「/blog/」配下にフィルタをかけ、ブログ記事ランキングが見える状態

GA4のページとスクリーンレポートで、フィルタ機能を使用して『/blog/』を含むページパスのみに絞り込んだ画面。ブログ記事ごとの表示回数(PV)やエンゲージメント時間を比較している様子

指標3:PV・ページ別PV(ミニノック4問)

問1(選択式:定義)

PVとして正しいのはどれ?
A. event_name=="page_view" の件数
B. session_id のユニーク数
C. event_name=="session_start" の件数
D. user_id のユニーク数

解答:A
解説: PVは page_view の回数です。


問2(記述:PVとユーザーの違い)

同じページで PVが多いがユニークユーザーが少ない場合、何が起きている可能性がありますか?(1〜2行)

解答例: 少数のユーザーが同じページを複数回見ている(フォーム入力で戻る、比較検討で見返す等)可能性。
解説: “人気”ではなく“迷い”や“再確認”の可能性もあるので、導線や内容の分かりやすさを疑います。


問3(実務:/contact/がPV上位の解釈)

ページ別PVで /contact/ が上位でした。最も適切な次の確認はどれ?
A. /contact/のPVが多い=成果が出ているので終了
B. /contact/到達は多いがCVが少ないなら、フォーム離脱(EFO)や入力負荷を疑う
C. /contact/をサイトから削除する
D. PVが多いページは全て成功

解答:B
解説: /contact/は終点になりやすいページ。到達が多くてもCVが増えないなら、フォーム周りの改善が最短です。


問4(実務:ブログPVから次に進む)

ブログ配下総PVが115で、記事別PV上位が分かりました。次に「成果につなげる」ために最優先で見るべきはどれ?
A. PVが多い記事だけ増やす
B. PV上位記事をランディングページとして見たときのCVR(指標4)
C. 画像を増やす
D. ページの色を変える

解答:B
解説: PV上位=成果ではありません。入口として機能しているかはLP別CVRで初めて判断できます。


指標4:初回訪問ベースのランディングページ別CVRを再現する(主役)

ビジネスの問い

  • 初回訪問の入口(ランディングページ)別に、どこが予約(CV)につながっているか?
  • “読まれている記事”と“成果につながる記事”は一致するのか?

Python集計ロジック(文章)

  1. 新規ユーザーの最初のセッションを取り出す
  2. そのセッション内の最初のpage_viewをLPとする
  3. セッション内にconversionがあればCV=1
  4. LP別に sessions / conversions / CVR(%) を出す
新規ユーザーのLP別CVRを算出する4つのステップ図。1.first_visitによる新規抽出、2.最初のセッション特定、3.LP(最初のpage_view)の確定、4.CV判定の順序を経て、最終的なCVRが計算される仕組みの解説

Pythonコード例

# 念のためソート(LP判定で最初のpage_viewを取りやすくする)
df = df.sort_values(["user_id", "session_id", "event_timestamp"]).reset_index(drop=True)

# 2) 新規ユーザー(first_visitを持つユーザー)を抽出
new_user_ids = df.loc[df["event_name"] == "first_visit", "user_id"].unique()

# 3) 新規ユーザーごとに「最初のセッション」を決める
#    ※ session_start の中から、user_idごとに最小event_timestampのsession_idを採用
first_sessions = (
    df[(df["event_name"] == "session_start") & (df["user_id"].isin(new_user_ids))]
    .sort_values(["user_id", "event_timestamp"])
    .drop_duplicates(subset=["user_id"], keep="first")[["user_id", "session_id"]]
)

# 4) 初回セッションのイベントログだけに絞る(初回訪問ベースの母集団)
first_session_events = df.merge(first_sessions, on=["user_id", "session_id"], how="inner")

# 5) 初回セッションの「ランディングページ(LP)」を確定
#    セッション内で最初の page_view の page_path をLPとする
landing_pages = (
    first_session_events[first_session_events["event_name"] == "page_view"]
    .sort_values(["session_id", "event_timestamp"])
    .drop_duplicates(subset=["session_id"], keep="first")[["session_id", "page_path"]]
    .rename(columns={"page_path": "landing_page"})
)

# 6) 初回セッション内でCVが起きたか(0/1)を作る
#    CVは conversion かつ /contact/ のみを対象(定義を明確化)
conv_sessions = (
    first_session_events[
        (first_session_events["event_name"] == "conversion")
        & (first_session_events["page_path"] == "/contact/")
    ][["session_id"]]
    .drop_duplicates()
)
conv_sessions["is_conversion"] = 1

# 7) LP×CV判定をセッション単位でまとめる
session_level = landing_pages.merge(conv_sessions, on="session_id", how="left")
session_level["is_conversion"] = session_level["is_conversion"].fillna(0).astype(int)

# 8) LP別に sessions / conversions / CVR% を算出
lp_cvr = (
    session_level.groupby("landing_page")
    .agg(
        sessions=("session_id", "nunique"),
        conversions=("is_conversion", "sum"),
    )
    .reset_index()
)

lp_cvr["cvr_pct"] = (lp_cvr["conversions"] / lp_cvr["sessions"] * 100).round(1)

# 見やすく:セッション数の多い順(またはCVR順でもOK)
lp_cvr = lp_cvr.sort_values(["sessions", "cvr_pct"], ascending=[False, False]).set_index("landing_page")

print("--- 初回訪問LP別CVRレポート(%表記) ---")
print(lp_cvr)

結果(あなたの集計:%表記統一)

  • /menu/acupuncture/:66.7%(ただし母数3)
  • /blog/seo-basics/:25.0%(母数4)
  • それ以外は0%が多い(CV総数が少ないため自然)

注意:sessionsが小さいLP(1〜2件)はCVRが跳ねるので参考値として扱う

ミニ対応表(GA4対応の書き込み欄)

  • Python:初回セッションLP別に(セッション数・CV数・CVR)
  • GA4:ランディングページ軸の「セッション」「コンバージョン」「セッションのコンバージョン率(相当)」
GA4のランディングページレポート画面。特定のページから流入したセッション数に対し、発生したコンバージョン数とコンバージョン率を一覧で表示し、入口としての貢献度を分析しているスクリーンショット

指標4:初回訪問ベースのランディングページ別CVR(ミニノック4問)

問1(選択式:定義)

初回訪問ベースのランディングページ(LP)の定義として正しいのはどれ?
A. ユーザー全期間で最初に見たページ
B. セッション内で最初に見たページ(session_id内の最初のpage_view)
C. PVが最も多いページ
D. /contact/ページ

解答:B
解説: LPは“セッションの入口”。初回訪問ベースでは「新規ユーザーの最初のセッション」に限定します。


問2(計算:基礎)

あるLPで初回訪問セッション数が4、CVが1ならCVR(%)はいくつ?
解答:25.0%
解説: 1÷4×100=25.0%。


問3(実務:高CVRだが母数が小さい)

/menu/acupuncture/sessions=3、CVR=66.7% と高い。最も妥当な判断はどれ?
A. すぐに広告費を全投入する
B. 良い兆候だが母数が小さいので、まずセッションを増やして再現性を確認する
C. たまたまなので必ず無視する
D. CVRが高い=全て正しいので検証不要

解答:B
解説: 母数が小さいとCVRは跳ねます。まず流入を増やすか期間を延ばし、同傾向が続くか確認します。


問4(実務:PV上位とCVRゼロのギャップ)

PV上位のブログ記事が、初回訪問LP別CVRでは0%でした。考えられる原因として適切なのはどれ?
A. その記事は読まれているが、予約導線(CTA・内部リンク)が弱い可能性
B. GA4が壊れている
C. ブログはすべて削除すべき
D. PVが多いならCVも必ず増えるはず

解答:A
解説: “読まれる”と“予約につながる”は別です。CTAの位置、予約ページへの導線、記事内容と施術メニューの接続が弱いとCVRは上がりません。


GA4のどのレポート・指標と対応しているか整理する

対応づけの考え方

迷ったら、次の3点で整理します。

  • 何を数えている?(イベント or ユニーク)
  • 単位は?(ユーザー / セッション / イベント)
  • 率なら分母と分子は?(例:CVR=CV÷セッション)

対応表(例)

指標Pythonで数えるものGA4で見る系統
ユーザーuser_idユニーク獲得(Acquisition)
新規ユーザーfirst_visitのuser_idユニーク獲得(Acquisition)
セッションsession_idユニークトラフィック獲得
PVpage_view件数エンゲージメント(ページ)
LP別CVR初回セッションLP別にCV÷セッションランディングページ軸(標準or探索)

まとめ:今回の学びを自分のGA4+Pythonにどう活かすか

まず自分のGA4で確認するチェックリスト

  • ユーザー/新規ユーザー:獲得系
  • セッション:トラフィック獲得系
  • PV:ページとスクリーン(ページ別)
  • LP別成果:ランディングページ軸(無ければ探索)

次にやる改善アクション例(さくら整体院の想定)

  • CVRが出たLP(例:特定のブログ記事)
    • 同テーマの記事を増やす、内部リンクで回遊を増やす、CTAを強化する
  • PVはあるがCVが出ないLP
    • 予約導線(/contact/)の誘導文、ボタン位置、フォーム入力(EFO)を見直す
  • 流入別の違い
    • Organicは獲得に強いが単発が多いなら、再訪導線(関連記事、メルマガ/LINE等)を設計する

自分のサイト版に置き換える手順

  • page_pathを自サイトの構造に合わせて置換
  • conversionイベントを自サイトのCV(予約完了/送信完了)に置換
  • 同じロジックで集計し、GA4画面の数字と見比べる

← 前の記事 次の記事 →