はじめに:値上げ・値下げの判断を感覚で終わらせない
仕入れ値の上昇、送料の改定、競合の動き。ECを運営していると、価格を見直さなければならない場面はたびたび訪れます。そして価格を変えたあと、多くの現場でこんな会話が交わされます。
「値上げしたけど、思ったより売れ行きは落ちていない気がする」 「値下げしたら売れたけど、これって本当に得だったんだろうか」
どちらも「気がする」「だろうか」で止まっています。価格は売上を構成するもっとも基本的な要素なのに、その変更の効果は驚くほど感覚で語られがちです。値上げで客が離れたのか、値下げで利益を削っただけなのか。これらは本来、データで確かめられるはずの問いです。
この記事では、商品ごとの価格履歴とGA4の行動データをBigQueryで結合し、価格変更がCVR(購入率)と売上、そして粗利にどう影響したかを検証する手順を紹介します。具体的には、商品ページの閲覧(view_item)と購入(purchase)を価格の時点と突き合わせ、価格帯ごとにCVRや売上を並べて見ていきます。
ただし、先に大事なことをお伝えしておきます。価格変更の効果測定には交絡要因が非常に多く、「値上げしたからCVRが下がった」と単純に断定するのは危険です。本記事は因果を断定する手法ではなく、判断材料を一枚追加するための分析と位置づけてください。施策の前後比較の考え方については、以下の記事も合わせて参考になります。
なお本記事は、GA4からBigQueryへのエクスポートが設定済みで、events_YYYYMMDD テーブルが揃っていること、そして商品ごとの価格変更履歴を別途用意できることを前提にしています。
検証に必要な2つのデータ
価格変更の影響を見るには、最低限2種類のデータが必要です。
ひとつは、GA4から取れる行動データです。誰がどの商品ページを見て(view_item)、最終的に購入したか(purchase)。この2つのイベントがあれば、商品単位のCVRを計算できます。
もうひとつは、価格履歴です。これがGA4だけでは取れません。GA4の view_item や purchase には、そのイベント発生時点の価格が入っていることもありますが、「いつからいつまで、いくらで売っていたか」という価格の区間情報は、自分で管理する必要があります。
そこで、価格の変更履歴を次のような形でBigQueryに用意します。商品コードごとに、価格と適用期間を持たせたテーブルです。
-- 価格履歴テーブルのイメージ(事前に用意しておく)
-- item_id : 商品コード
-- price : その期間の販売価格(税抜・円)
-- unit_cost : その期間の仕入原価(円)粗利計算用
-- valid_from : 価格適用の開始日
-- valid_to : 価格適用の終了日(現行価格はNULLや遠い未来日)
CREATE TABLE IF NOT EXISTS `myproject.ec.price_history` (
item_id STRING,
price INT64,
unit_cost INT64,
valid_from DATE,
valid_to DATE
);
この価格履歴は、ネットショップの管理画面の変更ログや、自社の商品マスタの更新履歴から作るのが現実的です。手作業でも、主要商品の値上げ・値下げのタイミングを表に起こすだけで十分に使えます。
価格履歴とGA4のview_item→purchaseを結合する
それでは、GA4の view_item と purchase を、その発生時点の価格に紐づけていきます。考え方はシンプルで、「イベントが起きた日が、どの価格区間に含まれるか」で価格履歴とつなぎます。
まずGA4のイベントから、商品単位の閲覧と購入を取り出します。GA4のEC計測では、商品情報が items 配列に入っているため、UNNEST で展開します。
-- view_item と purchase を商品単位で取り出す
WITH item_events AS (
SELECT
PARSE_DATE('%Y%m%d', event_date) AS event_dt,
event_name,
item.item_id AS item_id,
-- 購入件数の重複排除に使う取引ID(purchase のときだけ入る)
(SELECT ep.value.string_value FROM UNNEST(event_params) AS ep
WHERE ep.key = 'transaction_id') AS transaction_id,
-- purchase 時の数量と売上はイベント側の値を使う
item.quantity AS quantity,
item.item_revenue AS item_revenue
FROM `myproject.analytics_000000.events_*`,
UNNEST(items) AS item
WHERE _TABLE_SUFFIX BETWEEN '20260401' AND '20260831'
AND event_name IN ('view_item', 'purchase')
AND item.item_id IS NOT NULL
)
SELECT * FROM item_events;
ここで transaction_id を一緒に取り出しているのには理由があります。UNNEST(items) で展開すると、1回の購入でも商品(明細)の数だけ行が増えます。このまま COUNTIF(event_name = 'purchase') で数えると、「購入件数」ではなく「購入された明細の数」を数えてしまい、CVRの分子が水増しされます。後の集計では、購入件数は transaction_id で重複を除いて数え、売上・数量は明細粒度のまま合計する、というように定義を分けます。
次に、このイベントを価格履歴と結合します。event_dt が価格の適用区間(valid_from 以上、valid_to 未満)に入っているレコードを引き当てます。
-- イベントを発生時点の価格区間に紐づける
WITH item_events AS (
SELECT
PARSE_DATE('%Y%m%d', event_date) AS event_dt,
event_name,
item.item_id AS item_id,
(SELECT ep.value.string_value FROM UNNEST(event_params) AS ep
WHERE ep.key = 'transaction_id') AS transaction_id,
item.quantity AS quantity,
item.item_revenue AS item_revenue
FROM `myproject.analytics_000000.events_*`,
UNNEST(items) AS item
WHERE _TABLE_SUFFIX BETWEEN '20260401' AND '20260831'
AND event_name IN ('view_item', 'purchase')
AND item.item_id IS NOT NULL
)
SELECT
e.event_dt,
e.event_name,
e.item_id,
e.transaction_id,
e.quantity,
e.item_revenue,
p.price,
p.unit_cost
FROM item_events AS e
LEFT JOIN `myproject.ec.price_history` AS p
ON e.item_id = p.item_id
AND e.event_dt >= p.valid_from
AND e.event_dt < COALESCE(p.valid_to, DATE '2999-12-31');
この結合により、それぞれの閲覧・購入が「そのとき何円だった商品か」という情報を持つようになります。これが価格変更の検証の土台です。
価格を変えた日をまたいで集計すれば、同じ商品でも「変更前の価格区間」と「変更後の価格区間」でCVRや売上を分けて比較できるようになります。
価格帯別のCVR・売上・粗利を集計する
土台ができたら、価格区間ごとに指標を集計します。ここでは、同じ商品の価格変更前後を比べることを想定し、item_id と price(価格区間)でまとめます。
-- 価格区間ごとの閲覧数・購入数・CVR・売上・粗利
WITH joined AS (
-- 上記の結合結果を joined として受け取る想定
SELECT * FROM `myproject.ec.events_with_price`
)
SELECT
item_id,
price,
COUNTIF(event_name = 'view_item') AS view_cnt,
-- 購入件数は取引IDで重複排除(明細の数ではなく購入回数を数える)
COUNT(DISTINCT IF(event_name = 'purchase', transaction_id, NULL))
AS purchase_cnt,
SAFE_DIVIDE(
COUNT(DISTINCT IF(event_name = 'purchase', transaction_id, NULL)),
COUNTIF(event_name = 'view_item')) AS cvr,
-- 売上・粗利は明細粒度のまま合計してよい
SUM(IF(event_name = 'purchase', item_revenue, 0)) AS revenue,
-- 粗利 = 売上 - (原価 × 購入数量)
SUM(IF(event_name = 'purchase',
item_revenue - unit_cost * quantity, 0)) AS gross_profit
FROM joined
GROUP BY item_id, price
ORDER BY item_id, price;
このクエリで、商品ごとに価格区間別の閲覧数・購入数・CVR・売上・粗利が一覧になります。購入数(purchase_cnt)は transaction_id で重複を除いた購入回数、売上と粗利は明細を合計した金額、というように分子の定義を使い分けている点に注意してください。値上げした商品なら、安かった区間と高くなった区間が並ぶので、CVRがどう動いたか、売上と粗利がそれぞれどうなったかを横並びで見られます。
ここでひとつ強調したいのが、CVRと売上と粗利を必ずセットで見ることです。値下げをすればCVRは上がりやすく、売上も伸びることが多いものです。しかし粗利の列を見ると、売れた割に手元に残っていない、ということがしばしば起こります。逆に値上げでCVRが少し下がっても、1件あたりの粗利が増えて、トータルの粗利はむしろ改善していることもあります。CVRだけ、売上だけを見ると、判断を誤ります。
価格弾力性の見方
もう一歩踏み込みたい場合、価格弾力性という考え方が参考になります。これは「価格を1%変えたとき、需要(販売数量)が何%変わるか」をざっくり捉える指標です。
ここで前後比較の「前」をどう取るかに注意が必要です。価格変更の効果を時系列で見たいのですから、並べる順番は価格の高い・安いではなく、価格を適用した時間順(valid_from)でなければなりません。価格で並べてしまうと、値上げ→値下げのように価格が行き来したケースで時間の前後が崩れ、「前の価格区間」が実際の直前の区間と一致しなくなります。そこで、価格区間ごとの数量に価格履歴の適用開始日(valid_from)を持たせ、その時間順で直前の区間を引き当てます。
-- 価格変更前後(時間順)の数量変化から、おおまかな弾力性の目安を見る
WITH per_price AS (
SELECT
e.item_id,
e.price,
-- 価格区間の適用開始日を持たせて時間順に並べられるようにする
p.valid_from,
SUM(IF(e.event_name = 'purchase', e.quantity, 0)) AS qty
FROM `myproject.ec.events_with_price` AS e
JOIN `myproject.ec.price_history` AS p
ON e.item_id = p.item_id
AND e.price = p.price
GROUP BY e.item_id, e.price, p.valid_from
),
paired AS (
SELECT
item_id,
valid_from,
price,
qty,
-- 価格昇順ではなく valid_from(時間順)で直前の区間を取る
LAG(price) OVER (PARTITION BY item_id ORDER BY valid_from) AS prev_price,
LAG(qty) OVER (PARTITION BY item_id ORDER BY valid_from) AS prev_qty
FROM per_price
)
SELECT
item_id,
valid_from,
prev_price,
price,
prev_qty,
qty,
-- 弾力性の目安:数量変化率 ÷ 価格変化率
SAFE_DIVIDE(
SAFE_DIVIDE(qty - prev_qty, prev_qty),
SAFE_DIVIDE(price - prev_price, prev_price)
) AS price_elasticity_rough
FROM paired
WHERE prev_price IS NOT NULL;
なお、これはあくまで「時系列で直前の価格区間と比べた断面」です。価格帯そのものの高低で需要を比べたい(価格帯別の横断面を見たい)場合は別の整理になります。ここでは「値上げ・値下げという時間上の変更の前後で数量がどう動いたか」を見たいので、時間順での前後比較に揃えています。
弾力性の目安がマイナスで絶対値が大きいほど、価格に敏感な商品ということになります。値上げで数量が大きく減る商品は、値上げ幅に注意が必要だと推測できます。逆に絶対値が小さければ、多少の値上げでも数量があまり動かない、価格に鈍感な商品の可能性があります。
ただし、この数字はあくまで「目安」です。次の章で述べるとおり、価格以外の要因が数量に影響している可能性が常にあるため、弾力性の値を額面どおりに信じて価格を決めるのは避けてください。傾向を掴むための補助線、くらいに捉えるのが安全です。
⚠️ 交絡要因が多く、厳密な因果は難しい
ここまで集計の手順を紹介してきましたが、もっとも大切なのはこの章です。価格変更の効果測定は、交絡要因(結果に影響する別の原因)が非常に多く、「価格を変えたから売れ行きが変わった」と断定するのは簡単ではありません。
代表的な交絡要因を挙げます。
- 季節・需要の波:値上げと同時に閑散期に入っただけかもしれません。CVRの低下が価格のせいなのか季節のせいなのか、区別がつきにくくなります。
- 在庫・欠品:値下げした商品が途中で品切れしていれば、購入数は実力より低く出ます。逆に在庫を潤沢にしたタイミングと価格変更が重なることもあります。
- 競合の動き:同じ時期に競合が値下げやセールをしていれば、自社の数字はその影響を受けます。これは自社データだけでは見えません。
- 販促・露出の変化:価格変更と前後して、メルマガ配信や広告出稿、特集ページへの掲載があれば、それが流入とCVRを動かします。
これらが絡み合うため、価格変更だけの純粋な効果を取り出すのは、現実にはかなり難しいと考えておくべきです。クーポンの併用がある場合は、効果が二重に重なってさらに解釈が複雑になります。割引施策同士の食い合いについては、以下の記事も参考になります。
それでも、解釈の精度を上げる工夫はあります。
ひとつは、対照(比較対象)を持つことです。価格を変えなかった類似商品や、同じカテゴリ全体の動きを並べて見れば、「価格を変えていない商品も同じように落ちている」のか「価格を変えた商品だけが落ちている」のかを区別する手がかりになります。市場全体の地合いを差し引いて考えられるわけです。
もうひとつは、期間設計です。価格変更の直後だけを見ると、新価格への一時的な反応(駆け込み購入やその反動)に引きずられます。十分な日数を取り、できれば前年同期や直近の同じ曜日構成の期間と比べることで、季節や曜日の影響をならせます。
そして、これらを尽くしても残るのは「断定」ではなく「示唆」です。データはあくまで判断を支える材料であり、最終的な価格判断には、原価や競合状況、ブランドの方針といった、データに乗らない情報も合わせて考える必要があります。
まとめ
価格変更の効果は感覚で語られがちですが、GA4の view_item と purchase を価格履歴に結合すれば、価格帯ごとのCVR・売上・粗利を並べて検証できます。本記事の要点を振り返ります。
- 検証には、GA4の行動データに加えて、自分で用意する価格履歴(価格と適用期間)が必要です。
- イベントの発生日を価格の適用区間に紐づけることで、各閲覧・購入を「そのとき何円だった商品か」に対応づけられます。
- CVR・売上・粗利は必ずセットで見ます。値下げで売れても粗利が削れている、値上げでCVRが下がってもトータル粗利は改善、といった逆転がよく起こるためです。
- 価格弾力性は傾向を掴む補助線として使えますが、額面どおりに信じて価格を決めるのは避けるべきです。
- 季節・在庫・競合・販促などの交絡要因が多く、価格だけの純粋な効果を取り出すのは難しいものです。対照商品の併置と十分な期間設計で、解釈の精度を上げられます。
データで分かるのは「断定」ではなく「示唆」です。それでも、感覚だけで価格を動かしていた状態から、根拠を一枚添えて判断できる状態へ進むだけで、価格戦略の確からしさは確実に変わってきます。まずは主要な数商品の価格履歴を表に起こすところから始めてみてください。