mcpField
M
markdown-optimization

markdown-optimization

github.com/kishorkukrejaマーケティング・グロース8451 回インストール3 週間前に更新
リポジトリ

markdown-optimization について

動きの鈍い在庫の価格設定と処分戦略を検討する、小売業向けの値下げ最適化スキルです。価格弾力性、値下げ時期、複数SKUの判断、店舗別調整を扱います。

主な機能

  1. 01在庫と事業制約の評価
  2. 02価格弾力性と需要の分析
  3. 03値下げ時期の設定
  4. 04複数SKUの値下げ最適化
  5. 05店舗別の値下げ判断

活用シーン

  1. 01季節品や滞留在庫の値下げ計画
  2. 02在庫処分目標と利益制約の両立
  3. 03店舗在庫に応じた価格調整

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/kishorkukreja/awesome-supply-chain --skill markdown-optimization

インストール後、Claude Code で/markdown-optimizationを入力して読み込まれたか確認します。

詳細

値下げの最適化

小売業界における値下げの最適化とクリアランス価格戦略の専門家です。データに基づく価格決定を通じて、小売業者が販売不振在庫からの収益回収を最大化し、値下げコストを最小限に抑え、販売消化を加速できるよう支援します。

初期評価

値下げを最適化する前に、次の点を把握してください。

  1. 在庫状況

    • どの在庫を値下げする必要がありますか?(過剰在庫、季節商品、販売不振商品)
    • SKU/店舗ごとの現在の在庫数量は?
    • 在庫の経過期間は?(在庫保有週数)
    • 当初の原価と小売価格は?
    • 過去の販売消化率は?
  2. ビジネスの背景

    • 現在の値下げ率は?(売上に占める割合)
    • 目標とする値下げ率は?
    • 粗利益率の制約は?(許容できる最低粗利益率)
    • 季節性は?(シーズン終了の差し迫り具合)
    • 競合の価格環境は?
  3. 価格設定の柔軟性

    • 価格設定を管理するのは誰ですか?(本部、地域、店舗)
    • 値下げの頻度は?(毎週、毎月、継続的)
    • 値下げ幅のルールは?(10%、20%、30%刻み)
    • 割引率の下限/上限は?
    • 価格をパーソナライズできますか?(動的、ロイヤルティベース)
  4. チャネルと競合

    • 単一チャネルですか、それともオムニチャネルですか?
    • オンラインでの価格設定の柔軟性は?
    • 競合の価格を追跡していますか?
    • クリアランス用のアウトレット店舗はありますか?
    • 在庫処分の選択肢は?(卸売業者、寄付)

値下げ最適化フレームワーク

値下げ戦略の基本

主な目標:

  1. 収益回収を最大化する:クリアランスに移行する前に、可能な限り高い価格で販売する
  2. 販売消化を加速する:在庫が陳腐化する前に処分する
  3. 値下げ額を最小限に抑える:値下げの総費用を削減する
  4. ブランドイメージを維持する:過度な値引きを避ける
  5. タイミングを最適化する:早期と遅期の値下げのバランスを取る

値下げのライフサイクル:

Full Price → First Markdown (10-25%) → Second Markdown (30-50%) →
Final Clearance (50-75%) → Liquidation/Donation

意思決定フレームワーク:

  • いつ値下げするか? 販売消化率と在庫供給週数を監視する
  • いくら値下げするか? 価格弾力性と緊急性に基づいて判断する
  • どのSKUを値下げするか? 経過期間、数量、粗利益率に基づいて優先順位を付ける
  • どの店舗で値下げするか? 店舗ごと、またはチェーン全体で実施する

価格弾力性と需要モデリング

価格と需要の関係
import numpy as np
import pandas as pd
from scipy.optimize import minimize
from sklearn.linear_model import LinearRegression
import matplotlib.pyplot as plt

class PriceElasticityAnalyzer:
    """
    Analyze price elasticity of demand for markdown optimization

    Measure how demand changes with price reductions
    """

    def __init__(self, historical_pricing_data):
        """
        Parameters:
        - historical_pricing_data: DataFrame with historical price/sales
          columns: ['sku', 'date', 'price', 'units_sold', 'original_price']
        """
        self.data = historical_pricing_data

    def calculate_price_elasticity(self, sku):
        """
        Calculate price elasticity of demand

        Elasticity = % change in quantity / % change in price

        E > 1: Elastic (demand very sensitive to price)
        E = 1: Unit elastic
        E < 1: Inelastic (demand not very sensitive)
        """

        sku_data = self.data[self.data['sku'] == sku].copy()

        if len(sku_data) < 10:
            return {'error': 'Insufficient data', 'elasticity': None}

        # Calculate discount percentage
        sku_data['discount_pct'] = (
            (sku_data['original_price'] - sku_data['price']) /
            sku_data['original_price'] * 100
        )

        # Log transformation for elasticity calculation
        sku_data['log_price'] = np.log(sku_data['price'])
        sku_data['log_quantity'] = np.log(sku_data['units_sold'] + 1)

        # Linear regression: log(Q) = a + b * log(P)
        # b is the price elasticity
        X = sku_data[['log_price']].values
        y = sku_data['log_quantity'].values

        model = LinearRegression()
        model.fit(X, y)

        elasticity = model.coef_[0]
        r_squared = model.score(X, y)

        return {
            'sku': sku,
            'elasticity': abs(elasticity),  # Report as positive
            'r_squared': r_squared,
            'interpretation': self._interpret_elasticity(abs(elasticity)),
            'model': model
        }

    def _interpret_elasticity(self, elasticity):
        """Interpret elasticity value"""

        if elasticity > 2:
            return 'Highly elastic - very price sensitive'
        elif elasticity > 1:
            return 'Elastic - price sensitive'
        elif elasticity > 0.5:
            return 'Moderately elastic'
        else:
            return 'Inelastic - not very price sensitive'

    def estimate_demand_at_price(self, sku, target_price):
        """
        Estimate demand at a specific price point

        Uses fitted elasticity model
        """

        elasticity_result = self.calculate_price_elasticity(sku)

        if elasticity_result.get('error'):
            return None

        model = elasticity_result['model']
        log_price = np.log(target_price)

        log_quantity_pred = model.predict([[log_price]])[0]
        estimated_quantity = np.exp(log_quantity_pred) - 1

        return max(0, estimated_quantity)

    def find_optimal_markdown(self, sku, cost, current_price, current_inventory,
                             weeks_remaining=8):
        """
        Find optimal markdown to maximize revenue recovery

        Balances:
        - Higher prices = higher margin but slower sell-through
        - Lower prices = faster sell-through but lower margin
        """

        elasticity_result = self.calculate_price_elasticity(sku)

        if elasticity_result.get('error'):
            return {'error': 'Cannot optimize without elasticity data'}

        # Test different price points
        price_range = np.linspace(cost * 1.1, current_price, 20)  # Don't go below cost
        results = []

        for test_price in price_range:
            # Estimate weekly demand at this price
            estimated_weekly_demand = self.estimate_demand_at_price(sku, test_price)

            # Estimate total units sold over remaining weeks
            estimated_total_sold = min(
                estimated_weekly_demand * weeks_remaining,
                current_inventory
            )

            # Calculate revenue
            revenue = estimated_total_sold * test_price

            # Calculate margin
            margin = test_price - cost
            margin_pct = margin / test_price * 100 if test_price > 0 else 0

            # Calculate weeks to sell out
            if estimated_weekly_demand > 0:
                weeks_to_sellout = current_inventory / estimated_weekly_demand
            else:
                weeks_to_sellout = 999

            results.append({
                'price': test_price,
                'discount_pct': (current_price - test_price) / current_price * 100,
                'estimated_weekly_demand': estimated_weekly_demand,
                'estimated_total_sold': estimated_total_sold,
                'revenue': revenue,
                'margin_pct': margin_pct,
                'weeks_to_sellout': weeks_to_sellout
            })

        results_df = pd.DataFrame(results)

        # Find optimal price (maximize revenue)
        optimal_idx = results_df['revenue'].idxmax()
        optimal = results_df.iloc[optimal_idx]

        return {
            'current_price': current_price,
            'optimal_price': optimal['price'],
            'recommended_discount': optimal['discount_pct'],
            'expected_revenue': optimal['revenue'],
            'expected_sellthrough': optimal['estimated_total_sold'] / current_inventory * 100,
            'weeks_to_sellout': optimal['weeks_to_sellout'],
            'all_scenarios': results_df
        }

# Example usage
np.random.seed(42)

# Generate sample historical data
dates = pd.date_range('2024-01-01', periods=50, freq='W')
historical_data = []

for week in range(50):
    # Simulate price elasticity: lower prices → higher sales
    base_price = 100
    discount = min(week * 2, 60)  # Increasing discounts over time
    price = base_price * (1 - discount/100)

    # Demand increases as price decreases (elasticity ~1.5)
    base_demand = 20
    price_factor = (base_price / price) ** 1.5
    units_sold = base_demand * price_factor + np.random.normal(0, 5)
    units_sold = max(0, units_sold)

    historical_data.append({
        'sku': 'SKU123',
        'date': dates[week],
        'price': price,
        'units_sold': units_sold,
        'original_price': base_price
    })

historical_df = pd.DataFrame(historical_data)

# Analyze elasticity
analyzer = PriceElasticityAnalyzer(historical_df)
elasticity = analyzer.calculate_price_elasticity('SKU123')

print(f"Price Elasticity: {elasticity['elasticity']:.2f}")
print(f"Interpretation: {elasticity['interpretation']}")
print(f"R-squared: {elasticity['r_squared']:.2f}")

# Find optimal markdown
optimization = analyzer.find_optimal_markdown(
    sku='SKU123',
    cost=40,
    current_price=80,
    current_inventory=500,
    weeks_remaining=6
)

print(f"\nCurrent price: ${optimization['current_price']:.2f}")
print(f"Optimal price: ${optimization['optimal_price']:.2f}")
print(f"Recommended discount: {optimization['recommended_discount']:.1f}%")
print(f"Expected revenue: ${optimization['expected_revenue']:,.0f}")
print(f"Expected sell-through: {optimization['expected_sellthrough']:.1f}%")

値下げタイミングの最適化

動的な値下げスケジュール
class MarkdownScheduler:
    """
    Optimize markdown timing to maximize revenue recovery

    Decides when to markdown based on:
    - Current sell-through rate
    - Time remaining in season
    - Inventory level
    - Competitive pressure
    """

    def __init__(self, season_end_date, current_date):
        self.season_end = pd.to_datetime(season_end_date)
        self.current_date = pd.to_datetime(current_date)
        self.weeks_remaining = (self.season_end - self.current_date).days / 7

    def calculate_sell_through_rate(self, units_sold, initial_inventory, weeks_elapsed):
        """
        Calculate sell-through rate (STR)

        STR = units sold / initial inventory
        Weekly STR = STR / weeks
        """

        if initial_inventory == 0:
            return 0

        str_total = units_sold / initial_inventory * 100
        str_weekly = str_total / weeks_elapsed if weeks_elapsed > 0 else 0

        return {
            'total_str': str_total,
            'weekly_str': str_weekly,
            'units_sold': units_sold,
            'initial_inventory': initial_inventory
        }

    def should_markdown(self, sku_data):
        """
        Decide if SKU should be marked down now

        Factors:
        - Sell-through rate below target
        - Weeks of supply too high
        - End of season approaching
        """

        current_inventory = sku_data['current_inventory']
        weekly_sales_rate = sku_data['weekly_sales_rate']
        target_str = sku_data.get('target_str', 70)  # Target 70% sell-through

        # Calculate current trajectory
        projected_str = (weekly_sales_rate * self.weeks_remaining) / sku_data['initial_inventory'] * 100

        # Calculate weeks of supply
        if weekly_sales_rate > 0:
            weeks_of_supply = current_inventory / weekly_sales_rate
        else:
            weeks_of_supply = 999

        # Decision rules
        reasons = []
        should_md = False

        # Rule 1: Projected sell-through below target
        if projected_str < target_str:
            should_md = True
            reasons.append(f"Projected STR ({projected_str:.1f}%) below target ({target_str}%)")

        # Rule 2: Excessive weeks of supply
        if weeks_of_supply > self.weeks_remaining * 1.5:
            should_md = True
            reasons.append(f"Weeks of supply ({weeks_of_supply:.1f}) exceeds season ({self.weeks_remaining:.1f})")

        # Rule 3: End of season urgency
        if self.weeks_remaining < 4 and projected_str < 80:
            should_md = True
            reasons.append(f"Season ending soon ({self.weeks_remaining:.1f} weeks) with low STR")

        # Rule 4: No sales momentum
        if weekly_sales_rate < sku_data['initial_inventory'] * 0.01:  # <1% per week
            should_md = True
            reasons.append("Very slow sales rate")

        return {
            'should_markdown': should_md,
            'reasons': reasons,
            'projected_str': projected_str,
            'weeks_of_supply': weeks_of_supply,
            'weeks_remaining': self.weeks_remaining,
            'urgency': 'High' if self.weeks_remaining < 4 else 'Medium' if self.weeks_remaining < 8 else 'Low'
        }

    def recommend_markdown_depth(self, sku_data):
        """
        Recommend markdown percentage

        Deeper discounts for:
        - Lower sell-through
        - More weeks of supply
        - End of season
        - Low price elasticity (need big discount to move)
        """

        decision = self.should_markdown(sku_data)

        if not decision['should_markdown']:
            return {
                'recommended_markdown': 0,
                'reason': 'On track, no markdown needed'
            }

        weeks_of_supply = decision['weeks_of_supply']
        projected_str = decision['projected_str']
        elasticity = sku_data.get('price_elasticity', 1.0)

        # Base markdown on severity
        if projected_str < 40 or weeks_of_supply > 20:
            base_markdown = 50  # Aggressive
        elif projected_str < 55 or weeks_of_supply > 15:
            base_markdown = 30  # Moderate
        else:
            base_markdown = 20  # Conservative

        # Adjust for elasticity
        if elasticity < 0.8:  # Inelastic
            base_markdown *= 1.3  # Need bigger discount
        elif elasticity > 1.5:  # Elastic
            base_markdown *= 0.8  # Smaller discount works

        # Adjust for urgency
        if self.weeks_remaining < 3:
            base_markdown *= 1.4

        # Round to standard increments
        markdown_increments = [10, 20, 25, 30, 40, 50, 60, 70]
        recommended_markdown = min(markdown_increments, key=lambda x: abs(x - base_markdown))

        return {
            'recommended_markdown': recommended_markdown,
            'reason': f"Projected STR {projected_str:.1f}%, {weeks_of_supply:.1f} weeks supply",
            'urgency': decision['urgency']
        }

    def create_markdown_cadence(self, sku_data, markdown_stages=[20, 40, 60]):
        """
        Create multi-stage markdown plan

        Progressive markdowns over time
        """

        weeks_left = self.weeks_remaining
        cadence = []

        # Determine if we need aggressive or conservative cadence
        decision = self.should_markdown(sku_data)

        if decision['urgency'] == 'High':
            # Fast cadence - markdown every 1-2 weeks
            week_intervals = [0, 1, 3]
        elif decision['urgency'] == 'Medium':
            # Moderate cadence - markdown every 2-3 weeks
            week_intervals = [0, 2, 5]
        else:
            # Slow cadence - markdown every 3-4 weeks
            week_intervals = [0, 3, 7]

        for i, (markdown_pct, week_offset) in enumerate(zip(markdown_stages, week_intervals)):
            if week_offset < weeks_left:
                markdown_date = self.current_date + pd.Timedelta(weeks=week_offset)

                cadence.append({
                    'stage': i + 1,
                    'markdown_date': markdown_date,
                    'markdown_pct': markdown_pct,
                    'price': sku_data['original_price'] * (1 - markdown_pct/100),
                    'weeks_from_now': week_offset
                })

        return cadence

# Example
scheduler = MarkdownScheduler(
    season_end_date='2024-09-30',
    current_date='2024-08-15'
)

sku_data = {
    'sku': 'SKU456',
    'initial_inventory': 1000,
    'current_inventory': 720,
    'weekly_sales_rate': 25,
    'original_price': 89.99,
    'target_str': 75,
    'price_elasticity': 1.3
}

# Should we markdown?
decision = scheduler.should_markdown(sku_data)
print(f"Should markdown: {decision['should_markdown']}")
print(f"Reasons: {decision['reasons']}")
print(f"Projected STR: {decision['projected_str']:.1f}%")
print(f"Urgency: {decision['urgency']}")

# Recommend markdown depth
recommendation = scheduler.recommend_markdown_depth(sku_data)
print(f"\nRecommended markdown: {recommendation['recommended_markdown']}%")
print(f"Reason: {recommendation['reason']}")

# Create markdown cadence
cadence = scheduler.create_markdown_cadence(sku_data)
print(f"\nMarkdown Cadence:")
for stage in cadence:
    print(f"  Stage {stage['stage']}: {stage['markdown_pct']}% off on {stage['markdown_date'].date()} (${stage['price']:.2f})")

値下げ最適化モデル

複数SKUの値下げ最適化
from scipy.optimize import minimize, LinearConstraint

class MarkdownOptimizer:
    """
    Optimize markdowns across multiple SKUs

    Maximize total revenue while meeting clearance targets
    """

    def __init__(self, skus_data, constraints):
        """
        Parameters:
        - skus_data: DataFrame with SKU information
          columns: ['sku', 'inventory', 'cost', 'current_price', 'elasticity',
                   'weekly_demand_base']
        - constraints: Dict with budget/target constraints
        """
        self.skus = skus_data
        self.constraints = constraints

    def estimate_demand_at_discount(self, base_demand, elasticity, discount_pct):
        """
        Estimate demand at a given discount level

        Uses price elasticity to project demand lift
        """

        # Demand increases as price decreases
        # New_Demand = Base_Demand * (1 / (1 - discount))^elasticity
        if discount_pct >= 100:
            return base_demand * 10  # Arbitrary large number

        price_ratio = 1 / (1 - discount_pct/100)
        demand_multiplier = price_ratio ** elasticity

        return base_demand * demand_multiplier

    def calculate_revenue(self, discount_percentages):
        """
        Calculate total revenue for given discount strategy

        Parameters:
        - discount_percentages: Array of discount % for each SKU
        """

        total_revenue = 0

        for i, row in self.skus.iterrows():
            discount_pct = discount_percentages[i]

            # New price after discount
            new_price = row['current_price'] * (1 - discount_pct/100)

            # Estimated demand at this price
            estimated_demand = self.estimate_demand_at_discount(
                row['weekly_demand_base'],
                row['elasticity'],
                discount_pct
            )

            # Units sold (capped at inventory)
            units_sold = min(estimated_demand * 8, row['inventory'])  # 8 weeks

            # Revenue
            revenue = units_sold * new_price

            total_revenue += revenue

        return total_revenue

    def optimize_markdowns(self, max_avg_discount=40, min_clearance_rate=70):
        """
        Find optimal markdown strategy

        Constraints:
        - Average discount across SKUs <= max_avg_discount
        - Achieve min_clearance_rate sell-through
        """

        n_skus = len(self.skus)

        # Objective: Maximize revenue (minimize negative revenue)
        def objective(discounts):
            return -self.calculate_revenue(discounts)

        # Constraints
        constraints_list = []

        # Constraint 1: Average discount <= max
        def avg_discount_constraint(discounts):
            return max_avg_discount - np.mean(discounts)

        constraints_list.append({
            'type': 'ineq',
            'fun': avg_discount_constraint
        })

        # Constraint 2: Achieve minimum clearance rate
        def clearance_constraint(discounts):
            total_cleared = 0
            total_inventory = 0

            for i, row in self.skus.iterrows():
                discount_pct = discounts[i]
                new_price = row['current_price'] * (1 - discount_pct/100)

                estimated_demand = self.estimate_demand_at_discount(
                    row['weekly_demand_base'],
                    row['elasticity'],
                    discount_pct
                )

                units_sold = min(estimated_demand * 8, row['inventory'])
                total_cleared += units_sold
                total_inventory += row['inventory']

            clearance_rate = total_cleared / total_inventory * 100
            return clearance_rate - min_clearance_rate

        constraints_list.append({
            'type': 'ineq',
            'fun': clearance_constraint
        })

        # Bounds: 0% to 70% discount
        bounds = [(0, 70) for _ in range(n_skus)]

        # Initial guess: uniform discount
        x0 = np.full(n_skus, 30.0)

        # Optimize
        result = minimize(
            objective,
            x0,
            method='SLSQP',
            bounds=bounds,
            constraints=constraints_list
        )

        optimal_discounts = result.x

        # Calculate results
        results = []
        for i, row in self.skus.iterrows():
            discount = optimal_discounts[i]
            new_price = row['current_price'] * (1 - discount/100)

            estimated_demand = self.estimate_demand_at_discount(
                row['weekly_demand_base'],
                row['elasticity'],
                discount
            )

            units_sold = min(estimated_demand * 8, row['inventory'])
            revenue = units_sold * new_price
            clearance_rate = units_sold / row['inventory'] * 100

            results.append({
                'sku': row['sku'],
                'current_price': row['current_price'],
                'optimal_discount': discount,
                'new_price': new_price,
                'estimated_units_sold': units_sold,
                'revenue': revenue,
                'clearance_rate': clearance_rate
            })

        results_df = pd.DataFrame(results)

        return results_df, result

# Example
skus_data = pd.DataFrame({
    'sku': [f'SKU{i}' for i in range(1, 11)],
    'inventory': np.random.randint(100, 1000, 10),
    'cost': np.random.uniform(20, 50, 10),
    'current_price': np.random.uniform(50, 150, 10),
    'elasticity': np.random.uniform(0.8, 2.0, 10),
    'weekly_demand_base': np.random.uniform(5, 30, 10)
})

optimizer = MarkdownOptimizer(skus_data, {})

# Optimize
optimal_strategy, optimization_result = optimizer.optimize_markdowns(
    max_avg_discount=35,
    min_clearance_rate=75
)

print("Optimal Markdown Strategy:")
print(optimal_strategy[['sku', 'optimal_discount', 'new_price', 'estimated_units_sold', 'clearance_rate']])
print(f"\nTotal revenue: ${optimal_strategy['revenue'].sum():,.0f}")
print(f"Average discount: {optimal_strategy['optimal_discount'].mean():.1f}%")
print(f"Average clearance rate: {optimal_strategy['clearance_rate'].mean():.1f}%")

店舗レベルの値下げ判断

地域に合わせた値下げ戦略:

class StoreMarkdownManager:
    """
    Manage store-level markdown decisions

    Some stores need different markdown strategies than others
    """

    def __init__(self, stores_data):
        self.stores = stores_data

    def should_markdown_locally(self, sku, store_id, chain_markdown_pct=0):
        """
        Decide if store should markdown locally vs. follow chain

        Factors:
        - Local inventory level (excess?)
        - Local sell-through performance
        - Local competitive pressure
        """

        store = self.stores[self.stores['store_id'] == store_id].iloc[0]

        # Get SKU-specific data at store
        sku_inventory = store.get(f'{sku}_inventory', 0)
        sku_weekly_sales = store.get(f'{sku}_weekly_sales', 0)
        weeks_of_supply = sku_inventory / sku_weekly_sales if sku_weekly_sales > 0 else 999

        # Decision criteria
        local_markdown_needed = False
        reasons = []

        # Criterion 1: Excessive local inventory
        chain_avg_wos = 8  # Example chain average
        if weeks_of_supply > chain_avg_wos * 1.5:
            local_markdown_needed = True
            reasons.append(f"Excess inventory: {weeks_of_supply:.1f} weeks (chain avg: {chain_avg_wos})")

        # Criterion 2: Poor local performance
        chain_avg_str = 65  # Example
        local_str = store.get(f'{sku}_str', 50)
        if local_str < chain_avg_str * 0.8:
            local_markdown_needed = True
            reasons.append(f"Low STR: {local_str:.1f}% (chain avg: {chain_avg_str}%)")

        # Criterion 3: Competitive pressure
        if store.get('high_competition', False):
            local_markdown_needed = True
            reasons.append("High local competitive pressure")

        # Recommend local markdown depth
        if local_markdown_needed:
            # Go deeper than chain markdown
            recommended_markdown = max(chain_markdown_pct + 10, 30)
        else:
            # Follow chain markdown
            recommended_markdown = chain_markdown_pct

        return {
            'local_markdown_needed': local_markdown_needed,
            'reasons': reasons,
            'recommended_markdown': recommended_markdown,
            'weeks_of_supply': weeks_of_supply,
            'follow_chain': not local_markdown_needed
        }

    def identify_stores_for_clearance_transfer(self, sku, min_inventory=50):
        """
        Identify stores with excess inventory to transfer to outlet/clearance stores

        Better than heavy markdowns in regular stores
        """

        stores_with_excess = []

        for idx, store in self.stores.iterrows():
            sku_inventory = store.get(f'{sku}_inventory', 0)
            sku_weekly_sales = store.get(f'{sku}_weekly_sales', 0.1)

            if sku_inventory > min_inventory:
                weeks_of_supply = sku_inventory / sku_weekly_sales

                if weeks_of_supply > 15:  # Way too much
                    stores_with_excess.append({
                        'store_id': store['store_id'],
                        'inventory': sku_inventory,
                        'weeks_of_supply': weeks_of_supply,
                        'recommended_transfer_qty': int(sku_inventory * 0.5)  # Transfer half
                    })

        return pd.DataFrame(stores_with_excess)

# Example
stores_data = pd.DataFrame({
    'store_id': [f'S{i:03d}' for i in range(1, 21)],
    'high_competition': np.random.choice([True, False], 20, p=[0.3, 0.7])
})

# Add SKU-specific data
for idx, row in stores_data.iterrows():
    stores_data.at[idx, 'SKU001_inventory'] = np.random.randint(50, 300)
    stores_data.at[idx, 'SKU001_weekly_sales'] = np.random.randint(5, 25)
    stores_data.at[idx, 'SKU001_str'] = np.random.uniform(40, 80)

manager = StoreMarkdownManager(stores_data)

# Check if specific store needs local markdown
local_decision = manager.should_markdown_locally('SKU001', 'S001', chain_markdown_pct=20)
print(f"Store S001 - Local markdown needed: {local_decision['local_markdown_needed']}")
print(f"Recommended: {local_decision['recommended_markdown']}%")
print(f"Reasons: {local_decision['reasons']}")

# Find stores for clearance transfer
transfer_candidates = manager.identify_stores_for_clearance_transfer('SKU001')
print(f"\nStores with excess inventory for transfer:")
print(transfer_candidates)

ツールとライブラリ

Python ライブラリ

最適化:

  • scipy.optimize:価格最適化のための非線形最適化
  • pulp、pyomo:数理最適化
  • cvxpy:凸最適化

機械学習:

  • scikit-learn:価格弾力性の回帰分析
  • xgboost:価格と需要のモデリング
  • statsmodels:統計分析

データ処理:

  • pandas:データ操作
  • numpy:数値計算
商用ソフトウェア

価格設定と値下げ:

  • Revionics:価格最適化と値下げ管理
  • Blue Yonder (JDA) Price:AI を活用した価格設定
  • Oracle Retail Price Optimization:エンタープライズ向け値下げ最適化
  • Competera:動的価格設定プラットフォーム
  • Pricefx:クラウド型価格設定ソフトウェア

分析:

  • Tableau、Power BI:値下げの分析と可視化
  • SAP Analytics:エンタープライズ向け分析

よくある課題と解決策

課題:カニバリゼーションのリスク

問題:

  • 値下げによって通常価格での販売が奪われる
  • 顧客が割引を待つようになる
  • 通常価格での販売消化が損なわれる

解決策:

  • 対象を絞った値下げ(店舗単位、限定した店舗)
  • 広く告知しない
  • ロイヤルティプログラムを使ってパーソナライズした割引を提供する
  • 最も古い在庫から値下げする
  • 店舗で値下げする代わりにアウトレットへ移送する
  • クリアランス専用コーナーを設ける
課題:大幅な値下げによるブランド価値の毀損

問題:

  • 過度な値下げはブランドの認知を損ないます
  • 顧客がセールを待つようになります
  • 競合との値下げ競争に陥ります

解決策:

  • 戦略的な値下げ幅の上限(最大40~50%)を設定します
  • 大幅な値下げにはアウトレット/クリアランスストアを利用します
  • 大幅な値下げをする代わりに、寄付または清算を行います
  • 仕入れを改善して値下げを減らします
  • 定価での消化率を重視します
  • プレミアム商品とバリュー商品をセグメント化します
課題:価格弾力性データの不足

問題:

  • 新商品には値下げ履歴がありません
  • 最適な値下げ幅を推定できません
  • 推測に頼ると、最適でない判断につながります

解決策:

  • 類似商品を参考にします
  • パイロット店舗で異なる値下げ幅をテストします
  • 業界ベンチマークの弾力性を利用します
  • 出発点として、中程度の弾力性(1.0~1.5)を想定します
  • 商品属性に基づく機械学習を活用します
  • 時間をかけて弾力性データベースを構築します
課題:オムニチャネル価格設定の複雑さ

問題:

  • オンラインと店舗で価格が異なります
  • 顧客の混乱や苦情につながります
  • ショールーミング/ウェブルーミングの行動が見られます

解決策:

  • チャネル間で価格を統一します(推奨)
  • オンライン限定のクリアランスセクションを設けます
  • 価格マッチ保証を提供します
  • 地域別のオンライン価格設定を行います
  • 価格設定方針を明確に伝えます
  • 一律値下げではなく、ロイヤルティ施策を使ってパーソナライズします
課題:値下げのタイミングを誤る

問題:

  • 値下げが早すぎる → 定価での販売機会を失います
  • 値下げが遅すぎる → 在庫を処分できません
  • どちらも大きな損失につながります

解決策:

  • 消化率のしきい値を監視します
  • 値下げのトリガーを自動化します
  • 複数段階の値下げスケジュールを設定します
  • 地域ごとに時期を調整します(気候を考慮)
  • 競合の動向を監視します
  • シーズン終了後に分析し、翌年の改善につなげます

出力形式

マークダウン最適化レポート

エグゼクティブサマリー:

  • リスク対象在庫の合計:小売価格で820万ドル(42,000点)
  • 現在のマークダウン率:売上の28%
  • 目標マークダウン率:22%未満
  • 推奨するマークダウン戦略:段階的かつ対象を絞ったアプローチ
  • 予想マークダウンコスト:180万ドル(在庫価値の22%)
  • 予想売上回収額:640万ドル(78%)

SKU レベルのマークダウン推奨事項:

SKUカテゴリー在庫在庫供給週数現在の販売消化率推奨アクション割引率新価格予想売上
SKU001アウターウェア8501842%今すぐマークダウン40%$59.99$41K
SKU002ドレス1,2002238%今すぐマークダウン50%$39.99$38K
SKU003トップス6501258%3週目にマークダウン30%$34.99$18K
SKU004ボトムス420868%モニタリング、対応不要0%$49.99$21K
SKU005アクセサリー2,1002535%アウトレットへ移送60%$15.99$27K

SKU ごとのマークダウン実施計画:

SKUステージ 1ステージ 2ステージ 3クリアランス
SKU00130%引き(今すぐ)50%引き(3週目)60%引き(6週目)残りを処分
SKU00240%引き(今すぐ)60%引き(2週目)70%引き(5週目)残りを寄付
SKU00320%引き(2週目)40%引き(5週目)--

店舗レベルの調整:

店舗クラスターチェーンのマークダウン地域ごとの調整理由
都市部の高販売量店舗チェーンに従うなし販売消化は計画どおり
郊外のモールチェーンに従うさらに10%値下げ過剰在庫、競争圧力
小型店舗チェーンに従う大型店舗へ移送スペースと数量が限られている
アウトレット積極的に実施60~70%引きクリアランス販売先

財務への影響:

指標最適化なし最適化あり改善
売上回収額の合計$5.8M$6.4M+$600K
マークダウンコスト$2.4M (29%)$1.8M (22%)-$600K
販売済み点数28,000 (67%)34,000 (81%)+14ポイント
在庫処分までの週数12週8週-4週

導入計画:

週アクションSKU 数予想される効果
1週目第1弾マークダウン(30~40%引き)15 SKU8,000点を処分、売上48万ドル
2週目第2弾マークダウン(20~30%引き)12 SKU6,000点を処分、売上34万ドル
3週目販売不振商品の割引を拡大8 SKU5,000点を処分、売上18万ドル
4~6週目余剰在庫をアウトレットへ移送5 SKU10,000点を処分、売上25万ドル
7~8週目最終クリアランス(60~70%引き)残りすべて5,000点を処分、売上12万ドル

確認事項

さらに詳しい状況を把握する必要がある場合:

  1. どの在庫を値下げする必要がありますか?(在庫期間、数量、カテゴリー)
  2. 現在のマークダウン率はいくらですか?目標はどのくらいですか?
  3. どれくらいの時間がありますか?(シーズン終了まで、残り週数)
  4. 現在の販売消化率はいくらですか?
  5. 過去の価格弾力性データはありますか?
  6. マークダウンをどの程度管理できますか?(頻度、値下げ幅、店舗レベル)
  7. クリアランス販売用のアウトレット店舗はありますか?
  8. 競合他社はどのようなマークダウン施策を実施していますか?
  9. 粗利益率の制約はありますか?(許容可能な最低粗利益率)

関連スキル

  • retail-allocation:将来のマークダウンを最小限に抑えるための初期配分
  • demand-forecasting:さまざまな価格帯での需要予測
  • retail-replenishment:過剰在庫を防ぐための補充戦略
  • seasonal-planning:マークダウンのリスクを低減する季節商品の仕入れ計画
  • promotional-planning:販売促進戦略とマークダウン戦略の比較
  • inventory-optimization:マークダウンを最小限に抑える在庫管理
  • supply-chain-analytics:マークダウンのパフォーマンス指標

コメント