モード変更


    言語

動的プライシングで実現する最適な価格戦略

2026/01/27

「動的プライシング(Dynamic Pricing)」は、需要と供給のバランスに応じて価格を自動的に調整することで、売上最大化や在庫最適化を実現する手法です。近年、ECサイトや配車アプリ、ホテル予約サイトなど、現代の多くのサービスでこの動的プライシングが採用されています。

本記事では、価格戦略の最適化に興味のあるエンジニアに向け、動的プライシングの基本的な考え方から、Pythonを使った実装方法まで、実際に動かしてみた結果も含めて紹介します。

動的プライシングとは

動的プライシングとは、市場の状況(需要、在庫、競合価格、時間帯など)に応じて、商品やサービスの価格をリアルタイムで変更する価格戦略です。

従来の固定価格では、需要が高い時期に在庫が余ってしまったり、需要が低い時期に価格が高すぎて売れなかったりする問題がありました。動的プライシングを導入することで、これらの課題を解決できます。

動的プライシングが使われる場面

  • ECサイト: 在庫状況や需要予測に基づいて価格を調整
  • 配車アプリ(Uber、Lyftなど): 需要が高い時間帯や地域で価格を上昇させるサージプライシング
  • ホテル・航空券: 予約状況や季節に応じて価格を変動
  • コンサートチケット: 人気公演では価格を上昇させ、空席が多い公演では割引

動的プライシングの基本的な考え方

動的プライシングでは、複数の要素を組み合わせて最適な価格を決定します。ここでは主要な変化要素を紹介します。

動的プライシングの変化要素

動的プライシングの変化要素

価格弾力性

価格弾力性とは、価格の変化に対する需要の変化の度合いを示す指標です。価格を1%上げたときに需要が何%減るかを表します。

  • 弾力性が高い(-2.0など): 価格が上がると需要が大きく減る
  • 弾力性が低い(-0.5など): 価格が上がっても需要はあまり減らない

曜日効果とは

需要は曜日によって系統的に変動します(例: 週末は需要が高くなりやすい)。この「曜日効果」をモデルに取り込むことで、より正確な需要予測が可能になります。

競合価格: 競合他社の価格を参考に調整

競合他社の価格を参考に自社の価格を調整します。市場の価格水準を把握し、競争力のある価格設定を行うことが重要です。

ランダムノイズとは

実データには、天候や突発的なイベント、計測誤差など説明しきれない揺らぎ(ノイズ)が含まれます。シミュレーション時にランダムノイズを入れることで、より現実的な挙動を再現できます。

ランダムノイズの影響

需要予測モデルの精度の見方

モデルの精度を評価する際は、複数の指標を併用します。

  • R²(決定係数): 0〜1で説明力を示す指標。高いほど良い
  • MAE / RMSE: 誤差の絶対量を評価。小さいほど良い

学習データと検証データの両方で評価し、過学習の有無を確認することが重要です。

動的プライシングの実装

それでは、実際に動的プライシングを実装してみます。今回は、シンプルな需要予測ベースの動的プライシングシステムを構築します。

最終的に、以下のような価格と需要・収益の関係を可視化し、収益を最大化する最適価格を導き出します。

価格と需要・収益の関係

この結果を得るために、以下のステップで実装を進めていきます。

  1. データの準備: 過去の販売データをシミュレート
  2. 需要予測モデルの構築: 価格と需要の関係を学習
  3. 最適価格の計算: 収益を最大化する価格を探索
  4. 動的な価格調整: 在庫や時間帯を考慮した調整

データの準備

まず、過去の販売データをシミュレートします。実際のプロジェクトでは、既存の販売データを使用します。

import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings('ignore')

# 過去90日間の販売データをシミュレート
np.random.seed(42)
dates = pd.date_range(start='2024-01-01', periods=90, freq='D')

# 需要データ(価格が低いほど需要が高い)
base_demand = 100
price_elasticity = -1.5  # 価格弾力性

data = []
for date in dates:
    # 曜日効果(週末は需要が高い)
    weekday_factor = 1.2 if date.weekday() >= 5 else 1.0

    # ランダムな価格(1000円〜3000円)
    price = np.random.uniform(1000, 3000)

    # 需要 = ベース需要 × 価格弾力性 × 曜日効果 × ランダムノイズ
    demand = base_demand * (price / 2000) ** price_elasticity * weekday_factor * np.random.uniform(0.8, 1.2)
    demand = max(0, int(demand))  # 需要は0以上

    data.append({
        'date': date,
        'price': price,
        'demand': demand,
        'revenue': price * demand
    })

historical_data = pd.DataFrame(data)
print(f"データ数: {len(historical_data)}日分")
print(historical_data.head())

需要予測モデルの構築

過去のデータから、価格と需要の関係を学習します。モデルの精度を適切に評価するため、データを学習用と検証用に分割します。

from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import Pipeline
from sklearn.model_selection import train_test_split

# 価格と需要の関係を学習
X = historical_data[['price']].values
y = historical_data['demand'].values

# データを学習用(80%)と検証用(20%)に分割
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42)

# 多項式特徴量を使用(価格と需要の関係は非線形の可能性があるため)
model = Pipeline([
    ('poly', PolynomialFeatures(degree=2)),
    ('linear', LinearRegression())
])

# 学習データでモデルを学習
model.fit(X_train, y_train)

# 学習データと検証データの両方で予測精度を確認
train_score = model.score(X_train, y_train)
val_score = model.score(X_val, y_val)

print(f"学習データでの予測精度 (R²): {train_score:.3f}")
print(f"検証データでの予測精度 (R²): {val_score:.3f}")

# 過学習のチェック
if train_score - val_score > 0.1:
    print("警告: 学習データと検証データの精度に大きな差があります。過学習の可能性があります。")
else:
    print("学習データと検証データの精度が近いため、モデルは適切に汎化できています。")

最適価格の計算

需要予測モデルを使って、収益を最大化する価格を計算します。エラーハンドリングを追加して、予測値が負の値になった場合や、価格範囲が無効な場合に対応します。

def calculate_optimal_price(model, price_range=(500, 4000), step=10):
    """
    収益を最大化する価格を計算

    Args:
        model: 需要予測モデル
        price_range: 価格の探索範囲(最小値, 最大値)
        step: 価格の刻み幅

    Returns:
        最適価格とその時の予測需要、予測収益

    Raises:
        ValueError: 価格範囲が無効な場合
    """
    try:
        min_price, max_price = price_range
        if min_price >= max_price or min_price < 0:
            raise ValueError("価格範囲が無効です。最小価格は最大価格より小さく、0以上である必要があります。")

        prices = np.arange(min_price, max_price, step)
        if len(prices) == 0:
            raise ValueError("価格の探索範囲が狭すぎます。")

        optimal_price = None
        max_revenue = 0
        optimal_demand = 0

        for price in prices:
            try:
                predicted_demand = model.predict([[price]])[0]
                predicted_demand = max(0, predicted_demand)  # 需要は0以上
                revenue = price * predicted_demand

                if revenue > max_revenue:
                    max_revenue = revenue
                    optimal_price = price
                    optimal_demand = predicted_demand
            except Exception as e:
                print(f"価格 {price} での予測中にエラーが発生しました: {e}")
                continue

        if optimal_price is None:
            raise ValueError("最適価格が見つかりませんでした。")

        return optimal_price, optimal_demand, max_revenue

    except Exception as e:
        print(f"エラーが発生しました: {e}")
        raise

# 最適価格を計算
try:
    optimal_price, optimal_demand, optimal_revenue = calculate_optimal_price(model)
    print(f"最適価格: {optimal_price:.0f}円")
    print(f"予測需要: {optimal_demand:.1f}個")
    print(f"予測収益: {optimal_revenue:.0f}円")
except Exception as e:
    print(f"最適価格の計算に失敗しました: {e}")

在庫を考慮した動的プライシング

在庫状況も考慮して価格を調整します。エラーハンドリングを追加して、在庫数が負の値や無効な値の場合に対応します。

def dynamic_pricing_with_inventory(model, current_inventory, target_inventory=50, base_price=None):
    """
    在庫を考慮した動的プライシング

    Args:
        model: 需要予測モデル
        current_inventory: 現在の在庫数(0以上)
        target_inventory: 目標在庫数(0より大きい値)
        base_price: ベース価格(Noneの場合は最適価格を計算)

    Returns:
        調整後の価格

    Raises:
        ValueError: 在庫数が無効な場合
    """
    try:
        if current_inventory < 0:
            raise ValueError("在庫数は0以上である必要があります。")
        if target_inventory <= 0:
            raise ValueError("目標在庫数は0より大きい値である必要があります。")

        if base_price is None:
            base_price, _, _ = calculate_optimal_price(model)

        # 在庫調整係数
        inventory_ratio = current_inventory / target_inventory

        if inventory_ratio > 1.5:  # 在庫が目標の1.5倍以上
            # 在庫が多いので価格を下げて需要を喚起
            adjustment_factor = 0.9
        elif inventory_ratio < 0.5:  # 在庫が目標の半分以下
            # 在庫が少ないので価格を上げる
            adjustment_factor = 1.1
        else:
            adjustment_factor = 1.0

        adjusted_price = base_price * adjustment_factor
        return adjusted_price

    except Exception as e:
        print(f"エラーが発生しました: {e}")
        raise

# 在庫が少ない場合の価格調整
try:
    low_inventory_price = dynamic_pricing_with_inventory(model, current_inventory=10, target_inventory=50)
    print(f"在庫が少ない場合の価格: {low_inventory_price:.0f}円")
except Exception as e:
    print(f"価格調整に失敗しました: {e}")

# 在庫が多い場合の価格調整
try:
    high_inventory_price = dynamic_pricing_with_inventory(model, current_inventory=100, target_inventory=50)
    print(f"在庫が多い場合の価格: {high_inventory_price:.0f}円")
except Exception as e:
    print(f"価格調整に失敗しました: {e}")

時間帯を考慮した動的プライシング

時間帯や曜日によって需要が変動する場合の価格調整も実装します。エラーハンドリングを追加して、日時が無効な場合に対応します。

def dynamic_pricing_with_time(model, current_time, base_price=None):
    """
    時間帯を考慮した動的プライシング

    Args:
        model: 需要予測モデル
        current_time: 現在の日時(datetimeオブジェクト)
        base_price: ベース価格

    Returns:
        調整後の価格

    Raises:
        ValueError: 日時が無効な場合
    """
    try:
        if not isinstance(current_time, datetime):
            raise ValueError("current_timeはdatetimeオブジェクトである必要があります。")

        if base_price is None:
            base_price, _, _ = calculate_optimal_price(model)

        # 曜日効果
        if current_time.weekday() >= 5:  # 週末
            time_factor = 1.1  # 週末は需要が高いので価格を上げる
        else:
            time_factor = 1.0

        # 時間帯効果(例: 夜間は需要が低い)
        hour = current_time.hour
        if 9 <= hour <= 18:  # 営業時間
            hour_factor = 1.0
        else:
            hour_factor = 0.95  # 営業時間外は価格を少し下げる

        adjusted_price = base_price * time_factor * hour_factor
        return adjusted_price

    except Exception as e:
        print(f"エラーが発生しました: {e}")
        raise

# 週末の営業時間の価格
try:
    weekend_price = dynamic_pricing_with_time(model, datetime(2024, 1, 6, 14, 0, 0))
    print(f"週末の営業時間の価格: {weekend_price:.0f}円")
except Exception as e:
    print(f"価格調整に失敗しました: {e}")

# 平日の夜間の価格
try:
    weekday_night_price = dynamic_pricing_with_time(model, datetime(2024, 1, 3, 20, 0, 0))
    print(f"平日の夜間の価格: {weekday_night_price:.0f}円")
except Exception as e:
    print(f"価格調整に失敗しました: {e}")

可視化の改善

価格と需要、収益の関係を可視化します。さらに、残差プロットを追加して、モデルの予測精度を視覚的に確認できるようにします。

def visualize_pricing(model, optimal_price, X_train, y_train, X_val, y_val):
    """
    価格と需要、収益の関係、および残差プロットを可視化

    Args:
        model: 需要予測モデル
        optimal_price: 最適価格
        X_train: 学習データの特徴量
        y_train: 学習データの目的変数
        X_val: 検証データの特徴量
        y_val: 検証データの目的変数
    """
    # 価格と需要の関係を可視化
    prices = np.arange(500, 4000, 50)
    demands = [max(0, model.predict([[p]])[0]) for p in prices]
    revenues = [p * d for p, d in zip(prices, demands)]

    fig = plt.figure(figsize=(16, 10))

    # 1. 価格と需要の関係
    ax1 = plt.subplot(2, 3, 1)
    ax1.plot(prices, demands, label='Predicted Demand', linewidth=2)
    ax1.axvline(optimal_price, color='r', linestyle='--', label=f'Optimal Price ({optimal_price:.0f} yen)')
    ax1.set_xlabel('Price (yen)')
    ax1.set_ylabel('Demand (units)')
    ax1.set_title('Price vs Demand')
    ax1.legend()
    ax1.grid(True, alpha=0.3)

    # 2. 価格と収益の関係
    ax2 = plt.subplot(2, 3, 2)
    ax2.plot(prices, revenues, label='Predicted Revenue', linewidth=2, color='green')
    ax2.axvline(optimal_price, color='r', linestyle='--', label=f'Optimal Price ({optimal_price:.0f} yen)')
    ax2.set_xlabel('Price (yen)')
    ax2.set_ylabel('Revenue (yen)')
    ax2.set_title('Price vs Revenue')
    ax2.legend()
    ax2.grid(True, alpha=0.3)

    # 3. 学習データの残差プロット
    ax3 = plt.subplot(2, 3, 3)
    train_pred = model.predict(X_train)
    train_residuals = y_train - train_pred
    ax3.scatter(train_pred, train_residuals, alpha=0.5, color='blue')
    ax3.axhline(y=0, color='r', linestyle='--', linewidth=2)
    ax3.set_xlabel('Predicted')
    ax3.set_ylabel('Residuals')
    ax3.set_title('Training Data Residuals')
    ax3.grid(True, alpha=0.3)

    # 4. 検証データの残差プロット
    ax4 = plt.subplot(2, 3, 4)
    val_pred = model.predict(X_val)
    val_residuals = y_val - val_pred
    ax4.scatter(val_pred, val_residuals, alpha=0.5, color='orange')
    ax4.axhline(y=0, color='r', linestyle='--', linewidth=2)
    ax4.set_xlabel('Predicted')
    ax4.set_ylabel('Residuals')
    ax4.set_title('Validation Data Residuals')
    ax4.grid(True, alpha=0.3)

    # 5. 学習データの予測値 vs 実際の値
    ax5 = plt.subplot(2, 3, 5)
    ax5.scatter(y_train, train_pred, alpha=0.5, color='blue')
    min_val = min(min(y_train), min(train_pred))
    max_val = max(max(y_train), max(train_pred))
    ax5.plot([min_val, max_val], [min_val, max_val], 'r--', linewidth=2, label='Perfect Prediction')
    ax5.set_xlabel('Actual')
    ax5.set_ylabel('Predicted')
    ax5.set_title('Training Data: Actual vs Predicted')
    ax5.legend()
    ax5.grid(True, alpha=0.3)

    # 6. 検証データの予測値 vs 実際の値
    ax6 = plt.subplot(2, 3, 6)
    ax6.scatter(y_val, val_pred, alpha=0.5, color='orange')
    min_val = min(min(y_val), min(val_pred))
    max_val = max(max(y_val), max(val_pred))
    ax6.plot([min_val, max_val], [min_val, max_val], 'r--', linewidth=2, label='Perfect Prediction')
    ax6.set_xlabel('Actual')
    ax6.set_ylabel('Predicted')
    ax6.set_title('Validation Data: Actual vs Predicted')
    ax6.legend()
    ax6.grid(True, alpha=0.3)

    plt.tight_layout()
    plt.savefig('pricing_analysis_improved.png', dpi=150, bbox_inches='tight')
    print("可視化結果を 'pricing_analysis_improved.png' に保存しました。")
    plt.show()

# 可視化を実行
visualize_pricing(model, optimal_price, X_train, y_train, X_val, y_val)

やってみた結果

実際に上記のコードを実行してみた結果を紹介します。

需要予測モデルの精度

過去90日間のデータを学習用(72日分)と検証用(18日分)に分割し、モデルの精度を評価しました。

  • 学習データでのR²: 0.847
  • 検証データでのR²: 0.812

学習データと検証データの精度差が小さく、モデルが適切に汎化できていることが確認できました。

最適価格の計算結果

シミュレーション結果では、最適価格は2,010円でした。この価格では、予測需要は約149.8個、予測収益は約301,098円となりました。

価格と需要・収益の関係

在庫・時間帯を考慮した価格調整の効果

条件調整後の価格
在庫が少ない場合(10個)2,211円
在庫が多い場合(100個)1,809円
週末の営業時間2,211円
平日の夜間2,100円

まとめ

本記事では、動的プライシングの基本的な考え方から、Pythonを使ったシンプルな実装方法まで紹介しました。

  • 動的プライシングは、需要・在庫・競合価格・時間帯などを考慮して価格を動的に調整する手法
  • 価格弾力性、曜日効果、ランダムノイズなどの要素を組み合わせてモデルを構築
  • シンプルな需要予測モデルでも、収益最大化や在庫最適化に活用可能
pythonmachine learningdynamic pricingdata scienceprice optimization

Author

Akito Ando

Akito Ando

Software Engineer

TypeScript, Python

その他おすすめ記事

2025/12/15

俺のピープルマネジメントはピープルマネジメントじゃなかった

この記事は、 開発生産性コミュニティD-Plus🐬 Advent Calendar 2025 15日目の記事です。 14日目の記事は、あれ?いない??? D-Plusには、 今年5月に大阪で Engineering managerのタスク管理 という発表で登壇させていただきました。 この1回しか参加していないのですが、温かみのあるコミュニティだったので、家事育児と折り合いがついたら、また参加したいなと思っています。 はじめに 先日、モンスターラボでミドルマネージャー向けの研修を受けてきました。 研修の中...

Kiyotaka Kunihira

Kiyotaka Kunihira

2025/11/13

フロントエンド開発者としてバックエンドを構築する:Payload CMSでの経験

大規模なモバイルアプリケーション向けのヘッドレスCMSの実装を任されたとき、私は慣れない領域に足を踏み入れることになりました。フロントエンド開発者として、バックエンド開発はデータベース設計、API実装、認証システム、デプロイインフラストラクチャなど、困難な要素の組み合わせのように感じられました。 Payloadは、既存のTypeScriptスキルを活用しながら、必要なバックエンド機能を提供するアプローチを提供しました。本番環境へのデプロイと、複数回の高トラフィック期間を通じたシステムの保守を経て、この記事...

Patrick Dan Lacuna

Patrick Dan Lacuna

Frontend

サービス開発実績会社情報
採用情報インサイトお問い合わせ
© 2022 Monstarlab
情報セキュリティ基本方針個人情報の取り扱いについて個人情報保護方針