本文へ移動
今すぐ使えるSQLレシピ 更新日: 2026年5月1日 約28分で読めます

価格弾力性をSQLで計算する方法|値上げによる販売数変化を定量化する


「値上げしたら売れなくなる」を数字で言えるか

原材料費の高騰を受けて商品を10%値上げする。売上はどうなるか。

「売れにくくなるとは思うが、どれくらいかは…」という感覚論では意思決定が難しい。ここで使うのが価格弾力性(Price Elasticity of Demand:PED)だ。

価格弾力性 = 需要量の変化率 ÷ 価格の変化率
= (ΔQ / Q) ÷ (ΔP / P)

価格弾力性が -2 なら「価格が10%上がると需要量が20%減る」ことを意味する。弾力性が -0.3 なら「10%値上げで3%しか減らない」ので値上げしても利益が出やすい。

弾力性の絶対値解釈値上げへの対応
> 1弾力的(価格に敏感)値上げで販売数が大きく減る
= 1単位弾力的売上は変わらない
< 1非弾力的(価格に鈍感)値上げしても販売数はほとんど減らない
0完全非弾力的価格に無関係に需要が一定

今回は価格改定履歴と注文データを使って、商品ごとの価格弾力性をSQLで計算する。さらに対数回帰の近似式をウィンドウ関数で実装し、精度の高い弾力性推定を行う。


価格弾力性の計算方法

方法1:単純な弧弾力性(2時点の比較)

PED = ((Q2 - Q1) / ((Q2 + Q1) / 2)) ÷ ((P2 - P1) / ((P2 + P1) / 2))

中点法(Midpoint Method)とも呼ばれる。価格改定前後の2時点を比較する最もシンプルな方法だ。

方法2:対数回帰(複数時点を使った推定)

ln(Q) = α + β × ln(P) + ε
ここで β が価格弾力性の推定値

需要関数を対数変換することで、β が直接価格弾力性になる。複数の価格変動を使うため、単純な2時点比較より安定した推定ができる。SQLでは LN() 関数と、最小二乗法の公式をウィンドウ関数で近似実装する。


使用するテーブル

-- product_price_history(価格改定履歴)
-- product_id, price, valid_from, valid_to

-- order_items(注文明細)
-- order_id, product_id, quantity, unit_price

-- orders(注文テーブル)
-- order_id, order_date, status

STEP 1 ― 価格×期間ごとの販売数量を集計する

まず「この価格だったとき、月にどれだけ売れたか」というデータを作る。

-- STEP1: 価格帯別・月次の販売数量を集計する

WITH price_history AS (
    -- SCDテーブル。valid_toがNULLなら現在有効
    SELECT
        product_id,
        price,
        valid_from,
        COALESCE(valid_to, TIMESTAMP '2099-12-31 23:59:59')  AS valid_to_safe
    FROM product_price_history
),
monthly_sales AS (
    SELECT
        oi.product_id,
        DATE_TRUNC('month', o.order_date)  AS sales_month,
        SUM(oi.quantity)                    AS total_quantity,
        COUNT(DISTINCT o.order_id)          AS order_count,
        -- その月の適用価格(月初に有効だった価格)
        MAX(ph.price)                       AS price_in_month
    FROM orders       o
    JOIN order_items  oi ON o.order_id   = oi.order_id
    JOIN price_history ph
        ON  oi.product_id  = ph.product_id
        -- 月初日が価格の有効期間内にある
        AND DATE_TRUNC('month', o.order_date) >= CAST(ph.valid_from AS DATE)
        AND DATE_TRUNC('month', o.order_date) <  CAST(ph.valid_to_safe AS DATE)
    WHERE o.status = 'completed'
    AND o.order_date >= DATE '2023-01-01'
    AND o.order_date <  DATE '2025-01-01'
    GROUP BY oi.product_id, DATE_TRUNC('month', o.order_date)
)
SELECT *
FROM monthly_sales
ORDER BY product_id, sales_month;

出力イメージ(商品 P001)

product_idsales_monthtotal_quantityorder_countprice_in_month
P0012023-01-014122981,800
P0012023-02-013892711,800
P0012024-04-012842041,980
P0012024-05-012711981,980
P0012024-08-015213781,680
P0012024-09-014983611,680

価格が1,800円→1,980円(値上げ10%)で販売数が約30%減少し、1,680円(値下げ)で大きく増えている様子が見える。


STEP 2 ― 弧弾力性で価格改定前後を2時点比較する

-- STEP2: 弧弾力性(中点法)で価格改定前後を比較する

WITH price_history AS (
    SELECT product_id, price, valid_from,
           COALESCE(valid_to, TIMESTAMP '2099-12-31 23:59:59') AS valid_to_safe
    FROM product_price_history
),
monthly_sales AS (
    SELECT
        oi.product_id,
        DATE_TRUNC('month', o.order_date)  AS sales_month,
        SUM(oi.quantity)                    AS total_quantity,
        MAX(ph.price)                       AS price_in_month
    FROM orders o
    JOIN order_items  oi ON o.order_id   = oi.order_id
    JOIN price_history ph
        ON oi.product_id = ph.product_id
        AND DATE_TRUNC('month', o.order_date) >= CAST(ph.valid_from AS DATE)
        AND DATE_TRUNC('month', o.order_date) <  CAST(ph.valid_to_safe AS DATE)
    WHERE o.status = 'completed'
    AND o.order_date >= DATE '2023-01-01'
    AND o.order_date <  DATE '2025-01-01'
    GROUP BY oi.product_id, DATE_TRUNC('month', o.order_date)
),
price_change_periods AS (
    -- 価格改定が起きた前後の月を特定する
    SELECT
        product_id,
        price_in_month,
        sales_month,
        total_quantity,
        LAG(price_in_month) OVER (PARTITION BY product_id ORDER BY sales_month)
            AS prev_price,
        LAG(total_quantity) OVER (PARTITION BY product_id ORDER BY sales_month)
            AS prev_quantity,
        -- 価格が変化したか
        CASE WHEN price_in_month
             <> LAG(price_in_month) OVER (PARTITION BY product_id ORDER BY sales_month)
             THEN 1 ELSE 0 END  AS is_price_change
    FROM monthly_sales
),
arc_elasticity AS (
    SELECT
        product_id,
        sales_month                                           AS change_month,
        prev_price                                            AS price_before,
        price_in_month                                        AS price_after,
        prev_quantity                                         AS qty_before,
        total_quantity                                        AS qty_after,

        -- 価格変化率(中点法)
        ROUND(
            (price_in_month - prev_price) * 1.0
            / ((price_in_month + prev_price) / 2.0)
        , 4)  AS price_change_pct,

        -- 数量変化率(中点法)
        ROUND(
            (total_quantity - prev_quantity) * 1.0
            / ((total_quantity + prev_quantity) / 2.0)
        , 4)  AS qty_change_pct,

        -- 弧弾力性(価格弾力性)
        ROUND(
            ((total_quantity - prev_quantity) * 1.0
             / ((total_quantity + prev_quantity) / 2.0))
            /
            NULLIF(
                (price_in_month - prev_price) * 1.0
                / ((price_in_month + prev_price) / 2.0)
            , 0)
        , 4)  AS arc_elasticity

    FROM price_change_periods
    WHERE is_price_change = 1  -- 価格改定が発生した月のみ
)
SELECT
    product_id,
    change_month,
    price_before,
    price_after,
    qty_before,
    qty_after,
    ROUND(price_change_pct * 100, 2)  AS price_change_pct,
    ROUND(qty_change_pct * 100, 2)    AS qty_change_pct,
    arc_elasticity,
    -- 弾力性の解釈
    CASE
        WHEN arc_elasticity < -1   THEN '弾力的(価格敏感)'
        WHEN arc_elasticity BETWEEN -1 AND 0 THEN '非弾力的(価格鈍感)'
        WHEN arc_elasticity > 0    THEN '正の弾力性(ギッフェン財の可能性)'
        ELSE                            '計算不可'
    END  AS elasticity_type
FROM arc_elasticity
ORDER BY product_id, change_month;

出力イメージ

product_idchange_monthprice_beforeprice_afterprice_change_pctqty_change_pctarc_elasticityelasticity_type
P0012024-041,8001,980+9.52%-29.8%-3.13弾力的
P0012024-081,9801,680-16.40%+60.2%-3.67弾力的
P0032024-061,6001,750+8.97%-8.1%-0.90非弾力的
P0082024-032,4002,700+11.76%-4.2%-0.36非弾力的

P001(シャンプー)は弾力性 -3.13 と非常に弾力的だ。10%値上げで31%も需要が減る。競合製品に簡単に代替できる商品だと推測される。P008(日焼け止め)は -0.36 と非弾力的で、10%値上げでも4%しか減らない。季節需要が強く代替が難しい商品の特性だ。


STEP 3 ― 対数回帰による弾力性推定(複数時点版)

複数の価格変動を使って、より安定した弾力性を推定する。対数線形モデル ln(Q) = α + β × ln(P) の最小二乗法の公式は

β = (n × Σ(lnP × lnQ) - ΣlnP × ΣlnQ) / (n × Σ(lnP)² - (ΣlnP)²)

これは相関係数の計算と全く同じ構造だ。CORR() 関数でも計算できる。

-- STEP3: 対数回帰による弾力性(複数時点を使った安定推定)

WITH price_history AS (
    SELECT product_id, price, valid_from,
           COALESCE(valid_to, TIMESTAMP '2099-12-31 23:59:59') AS valid_to_safe
    FROM product_price_history
),
monthly_sales AS (
    SELECT
        oi.product_id,
        DATE_TRUNC('month', o.order_date)  AS sales_month,
        SUM(oi.quantity)                    AS total_quantity,
        MAX(ph.price)                       AS price_in_month
    FROM orders o
    JOIN order_items  oi ON o.order_id   = oi.order_id
    JOIN price_history ph
        ON oi.product_id = ph.product_id
        AND DATE_TRUNC('month', o.order_date) >= CAST(ph.valid_from AS DATE)
        AND DATE_TRUNC('month', o.order_date) <  CAST(ph.valid_to_safe AS DATE)
    WHERE o.status = 'completed'
    AND o.order_date >= DATE '2023-01-01'
    AND o.order_date <  DATE '2025-01-01'
    GROUP BY oi.product_id, DATE_TRUNC('month', o.order_date)
),
log_transformed AS (
    SELECT
        product_id,
        sales_month,
        total_quantity,
        price_in_month,
        LN(price_in_month)  AS ln_price,
        LN(total_quantity)  AS ln_quantity
    FROM monthly_sales
    WHERE total_quantity > 0  -- ゼロは対数変換不可
    AND price_in_month  > 0
)
SELECT
    product_id,
    COUNT(*)  AS data_points,

    -- CORR() で対数回帰の傾き(= 価格弾力性)を計算
    ROUND(CORR(ln_price, ln_quantity), 4)  AS log_elasticity,

    -- 方法1(手動計算):最小二乗法の公式
    ROUND(
        (COUNT(*) * SUM(ln_price * ln_quantity) - SUM(ln_price) * SUM(ln_quantity))
        / NULLIF(
            COUNT(*) * SUM(ln_price * ln_price) - SUM(ln_price) * SUM(ln_price)
        , 0)
    , 4)  AS log_elasticity_ols,

    -- 切片 α の計算
    ROUND(
        (SUM(ln_quantity) - (
            (COUNT(*) * SUM(ln_price * ln_quantity) - SUM(ln_price) * SUM(ln_quantity))
            / NULLIF(COUNT(*) * SUM(ln_price * ln_price) - SUM(ln_price) * SUM(ln_price), 0)
        ) * SUM(ln_price)
        ) / COUNT(*)
    , 4)  AS log_intercept,

    -- 推定の決定係数(R²)
    ROUND(POWER(CORR(ln_price, ln_quantity), 2), 4)  AS r_squared,

    -- 価格の範囲
    ROUND(MIN(price_in_month), 0)  AS min_price,
    ROUND(MAX(price_in_month), 0)  AS max_price,

    -- 弾力性の解釈
    CASE
        WHEN CORR(ln_price, ln_quantity) < -1 THEN '弾力的(価格敏感)'
        WHEN CORR(ln_price, ln_quantity) BETWEEN -1 AND 0 THEN '非弾力的'
        ELSE '正の弾力性(要確認)'
    END  AS elasticity_type
FROM log_transformed
GROUP BY product_id
HAVING COUNT(*) >= 6  -- 最低6時点のデータがある商品のみ
ORDER BY log_elasticity ASC;

出力イメージ

product_iddata_pointslog_elasticityr_squaredmin_pricemax_priceelasticity_type
P00118-3.240.88121,6801,980弾力的(価格敏感)
P00318-0.890.72431,6001,750非弾力的
P00818-0.340.61242,4002,700非弾力的
P01218-1.520.78919001,200弾力的(価格敏感)

CORR(ln_price, ln_quantity) が直接、対数線形モデルの価格弾力性推定値になる。R²(決定係数)が0.7以上であれば、このモデルが価格と販売数の関係をある程度説明できていると言える。


STEP 4 ― 価格弾力性を使って最適価格を推定する(完成版)

弾力性が分かると「どの価格なら売上(または利益)が最大になるか」を計算できる。

売上最大化の観点では、価格弾力性 = -1 のときが売上のピーク(これより弾力的なら値下げ、非弾力的なら値上げが有利)

-- STEP4: 価格弾力性から最適価格を推定する

WITH elasticity_data AS (
    -- STEP3の結果を使う(商品ごとの弾力性と切片)
    SELECT
        product_id,
        log_elasticity   AS beta,    -- 弾力性
        log_intercept    AS alpha,   -- 切片
        -- 現在価格(最新の価格)
        MAX(price_in_month)  AS current_price,
        -- 現在の月次販売数(最新2ヶ月平均)
        AVG(CASE WHEN sales_month >= DATE '2024-08-01' THEN total_quantity END)
            AS current_qty
    FROM (
        -- STEP3のlog_transformedとlog_elasticityの結果を結合(実際はCTEとして連結する)
        SELECT ms.product_id, ms.sales_month, ms.total_quantity, ms.price_in_month,
               ols.log_elasticity, ols.log_intercept
        FROM monthly_sales ms
        JOIN (SELECT product_id,
                ROUND((COUNT(*) * SUM(ln_price * ln_quantity) - SUM(ln_price) * SUM(ln_quantity))
                    / NULLIF(COUNT(*) * SUM(ln_price * ln_price) - SUM(ln_price) * SUM(ln_price), 0), 4) AS log_elasticity,
                ROUND((SUM(ln_quantity) - ((COUNT(*) * SUM(ln_price * ln_quantity) - SUM(ln_price) * SUM(ln_quantity))
                    / NULLIF(COUNT(*) * SUM(ln_price * ln_price) - SUM(ln_price) * SUM(ln_price), 0)) * SUM(ln_price)) / COUNT(*), 4) AS log_intercept
              FROM log_transformed GROUP BY product_id) ols ON ms.product_id = ols.product_id
    )
    GROUP BY product_id, log_elasticity, log_intercept
)
SELECT
    product_id,
    ROUND(beta, 3)          AS price_elasticity,
    ROUND(current_price, 0) AS current_price,
    ROUND(current_qty, 0)   AS current_monthly_qty,

    -- 現在の月次売上推定
    ROUND(current_price * current_qty, 0)  AS current_monthly_revenue,

    -- 価格別シナリオ分析(-20% / -10% / +10% / +20%)
    -- 数量予測:Q_new = Q_current × (P_new / P_current)^beta
    ROUND(current_price * 0.80, 0)  AS price_minus_20pct,
    ROUND(current_qty * POWER(0.80, beta), 0)  AS qty_at_minus_20pct,
    ROUND(current_price * 0.80 * current_qty * POWER(0.80, beta), 0)  AS rev_at_minus_20pct,

    ROUND(current_price * 0.90, 0)  AS price_minus_10pct,
    ROUND(current_qty * POWER(0.90, beta), 0)  AS qty_at_minus_10pct,
    ROUND(current_price * 0.90 * current_qty * POWER(0.90, beta), 0)  AS rev_at_minus_10pct,

    ROUND(current_price * 1.10, 0)  AS price_plus_10pct,
    ROUND(current_qty * POWER(1.10, beta), 0)  AS qty_at_plus_10pct,
    ROUND(current_price * 1.10 * current_qty * POWER(1.10, beta), 0)  AS rev_at_plus_10pct,

    ROUND(current_price * 1.20, 0)  AS price_plus_20pct,
    ROUND(current_qty * POWER(1.20, beta), 0)  AS qty_at_plus_20pct,
    ROUND(current_price * 1.20 * current_qty * POWER(1.20, beta), 0)  AS rev_at_plus_20pct

FROM elasticity_data
ORDER BY ABS(beta) DESC;

出力イメージ(P001: シャンプー、弾力性 -3.24)

価格シナリオ価格予測数量予測月次売上
現在1,680円498個837,240円
-20%1,344円1,108個1,489,152円
-10%1,512円734個1,109,808円
+10%1,848円337個622,776円
+20%2,016円229個461,664円

P001 は弾力性 -3.24 と非常に弾力的なため、20%値下げすると売上が78%増加するシミュレーション結果が出た。逆に10%値上げで売上が26%減少する。現在の価格は既に割引価格(セール価格1,680円)なので、定価に戻すことで売上が大きく落ちることが分かる。


実務での運用ヒント

① 競合価格・季節性の交絡要因に注意

価格変化の時期が季節需要のピーク(年末・夏)と重なっていると、価格弾力性が過大・過小評価される。「4月に値上げしたら5月のGW需要で数量が戻った」のような場合、季節性の影響を除去する必要がある。季節ダミー変数を加えた重回帰(SQLでは相関係数の拡張が必要)に移行するか、価格変化の前後を「同じ季節の年を比較する」設計にする。

② データ点数の確認

STEP3で HAVING COUNT(*) >= 6 を設定したのは、データ点数が少ないと回帰が不安定になるためだ。価格改定が1回しかない商品は弧弾力性(STEP2)のみを使い、対数回帰は使わない。

③ 価格弾力性は区間ごとに異なる

対数線形モデルは「すべての価格帯で弾力性が一定」という仮定を置いている。実際には高価格帯と低価格帯で弾力性が異なることが多い(高価格帯ではより弾力的になる傾向)。より精度を上げるには、価格を高・中・低の帯に分けて別々に弾力性を推定する。


まとめ

価格弾力性をSQLで計算するアプローチをまとめる。

  1. 弧弾力性(中点法) — 価格改定前後の2時点を比較する。実装が簡単で直感的
  2. 対数回帰(OLS)LN(price)LN(quantity) の相関係数または最小二乗法で計算。複数時点を使うため安定した推定ができる
  3. シナリオ分析 — 弾力性 β を使って Q_new = Q_current × (P_new / P_current)^β で価格別の予測数量・売上を計算する

弾力性が -3 なら値下げが有効、-0.3 なら値上げが有効という判断は、感覚論ではなくデータから導き出されたものだ。「値上げしたら売れなくなるか」という問いに、会議室でSQLの結果を見せながら答えられることが、このクエリの最大の価値だ。


次のアクション

SQLやデータ活用を、手元で試しながら理解する

記事で読んだ考え方を、SQL練習場や関連カテゴリの記事でさらに深掘りできます。相談やご依頼もお問い合わせページから受け付けています。

SQL練習場で試す お問い合わせ

この記事を書いた人:martechfarm

Treasure Data Top Lapidarist Award受賞。

SQL / Digdag / Python / CDP設計 / CRM設計を横断し、企業のデータ活用を支援。

実績・支援内容を見る →

MarTech Farmをもっと見る

今すぐ購読し、続きを読んで、すべてのアーカイブにアクセスしましょう。

続きを読む