mcpField
S
sales-operations-planning

sales-operations-planning

github.com/kishorkukrejaその他8464 回インストール3 週間前に更新
リポジトリ

sales-operations-planning について

Sales & Operations Planning(S&OP)と Integrated Business Planning(IBP)の設計・改善を支援するスキルです。部門横断の計画サイクル、意思決定、成熟度モデル、指標、関連ツールを解説します。

主な機能

  1. 01計画プロセスと組織状況の評価
  2. 02月次 S&OP の5段階サイクルの構成
  3. 03需要・供給・財務計画の調整
  4. 04シナリオと経営判断の準備
  5. 05予測・供給・在庫・財務指標の追跡
  6. 06成熟度レベルと計画ツールの確認

活用シーン

  1. 01定期的な部門横断 S&OP サイクルの確立
  2. 02需要と生産能力のギャップ解消
  3. 03経営層向けシナリオとトレードオフの準備
  4. 04KPI による計画パフォーマンスの監視

インストール

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

$npx skills add https://github.com/kishorkukreja/awesome-supply-chain --skill sales-operations-planning

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

詳細

Sales & Operations Planning(S&OP)

Sales & Operations Planning(S&OP)およびIntegrated Business Planning(IBP)の専門家です。需要、供給、財務、戦略を連携させ、より良い経営判断につながる効果的なS&OPプロセスを組織が導入できるよう支援します。

初期評価

S&OPの導入や改善に取り組む前に、以下を把握します。

  1. 現状

    • 既存の計画プロセスはどのようなものですか?(非公式、Excelベース、システム主導)
    • 計画サイクルと頻度はどのようなものですか?(月次、週次)
    • 誰が参加していますか?(営業、オペレーション、財務、経営陣)
    • 現在の課題や問題点は何ですか?
  2. 組織の状況

    • 会社の規模や複雑さはどの程度ですか?
    • 製品ファミリー/SKUはいくつありますか?
    • 対象地域はどの範囲ですか?(単一拠点、地域、グローバル)
    • 業界特有の特徴は何ですか?(季節性、販促、プロジェクトベース)
  3. 計画の成熟度

    • 予測精度はどの程度ですか?
    • 部門横断的な連携の質はどの程度ですか?
    • データシステムと統合状況はどうなっていますか?
    • パフォーマンス測定はどのように行っていますか?
  4. 事業目標

    • 戦略目標は何ですか?(成長、利益率、サービス、在庫)
    • 主要な指標と目標値は何ですか?
    • 意思決定権限とガバナンスはどのようになっていますか?
    • ステークホルダーの期待は何ですか?

S&OPフレームワーク

S&OPの定義

**Sales & Operations Planning(S&OP)**とは、顧客、営業、マーケティング、オペレーション、エンジニアリング、財務、製品管理など、事業に関するすべての計画をひとつの統合計画にまとめる、月次の統合経営プロセスです。

主な原則:

  1. 部門横断的な統合:サイロ化を解消します
  2. 将来志向:18~24か月のローリング計画期間を設定します
  3. 経営陣が主導:リーダーシップが説明責任を担います
  4. 唯一の信頼できる情報源:合意に基づく計画をひとつにまとめます
  5. 意思決定を重視:報告にとどまらず、具体的な行動につなげます
S&OPとIBP(Integrated Business Planning)の比較
項目S&OPIBP
対象範囲需要と供給のバランス戦略と財務の統合
計画期間12~18か月24~36か月以上
頻度月次月次+四半期レビュー
重点オペレーションの調整戦略+オペレーション
参加者部門横断型財務、戦略部門を含む
成果物数量計画数量計画+財務計画

S&OPプロセスのステップ

月次S&OPサイクル

5段階のプロセス:

Week 1: Data Gathering & Demand Review
Week 2: Supply/Capacity Review
Week 3: Pre-S&OP Meeting
Week 4: Executive S&OP Meeting
Week 5: Implementation & Monitoring
ステップ1:データ収集と予測

目的:

  • 実績データを収集します
  • 統計的なベースライン予測を作成します
  • 例外や異常を特定します

活動:

  • 実績販売、生産、在庫を更新します
  • 予測モデルを実行します
  • 予測精度を算出します
  • 例外(大幅な変動、新製品)にフラグを付けます

成果物:

  • 製品ファミリー別の統計予測
  • 予測精度指標(MAPE、バイアス)
  • 例外レポート
  • データ品質の問題ログ
import pandas as pd
import numpy as np
from datetime import datetime, timedelta

class SopDataGathering:
    """Data gathering and baseline forecasting for S&OP"""

    def __init__(self, historical_data):
        """
        Parameters:
        - historical_data: DataFrame with 'date', 'product_family', 'actual_sales'
        """
        self.data = historical_data

    def calculate_forecast_accuracy(self, forecast_col='forecast',
                                    actual_col='actual_sales'):
        """Calculate forecast accuracy metrics"""

        df = self.data.copy()

        # MAPE (Mean Absolute Percentage Error)
        df['ape'] = np.abs((df[actual_col] - df[forecast_col]) / df[actual_col]) * 100
        mape = df['ape'].mean()

        # Bias (average error)
        df['error'] = df[forecast_col] - df[actual_col]
        bias = df['error'].mean()
        bias_pct = (bias / df[actual_col].mean()) * 100

        # MAD (Mean Absolute Deviation)
        mad = np.abs(df['error']).mean()

        # Tracking signal
        cumulative_error = df['error'].sum()
        tracking_signal = cumulative_error / mad if mad > 0 else 0

        return {
            'mape': mape,
            'bias': bias,
            'bias_pct': bias_pct,
            'mad': mad,
            'tracking_signal': tracking_signal
        }

    def identify_exceptions(self, threshold_pct=20):
        """
        Identify forecast exceptions requiring attention

        Parameters:
        - threshold_pct: % change threshold for flagging
        """

        df = self.data.copy()

        # Calculate month-over-month change
        df = df.sort_values(['product_family', 'date'])
        df['prev_forecast'] = df.groupby('product_family')['forecast'].shift(1)
        df['forecast_change_pct'] = (
            (df['forecast'] - df['prev_forecast']) / df['prev_forecast'] * 100
        )

        # Flag large changes
        exceptions = df[
            np.abs(df['forecast_change_pct']) > threshold_pct
        ].copy()

        return exceptions[['product_family', 'date', 'forecast',
                          'prev_forecast', 'forecast_change_pct']]

    def generate_baseline_forecast(self, periods_ahead=18):
        """
        Generate statistical baseline forecast

        Simple approach using moving average with trend
        """

        forecasts = []

        for product in self.data['product_family'].unique():
            product_data = self.data[
                self.data['product_family'] == product
            ].sort_values('date')

            # Calculate moving average and trend
            recent_avg = product_data['actual_sales'].tail(3).mean()
            older_avg = product_data['actual_sales'].tail(6).head(3).mean()
            trend = (recent_avg - older_avg) / 3  # Per period trend

            # Generate forecast
            last_date = product_data['date'].max()

            for i in range(1, periods_ahead + 1):
                forecast_date = last_date + pd.DateOffset(months=i)
                forecast_value = recent_avg + (trend * i)
                forecast_value = max(0, forecast_value)  # No negative forecasts

                forecasts.append({
                    'product_family': product,
                    'date': forecast_date,
                    'baseline_forecast': forecast_value,
                    'method': 'MA_with_trend'
                })

        return pd.DataFrame(forecasts)

# Example usage
historical = pd.DataFrame({
    'date': pd.date_range('2024-01-01', periods=12, freq='MS'),
    'product_family': ['Electronics'] * 12,
    'actual_sales': [1000, 1100, 1050, 1200, 1250, 1300, 1280, 1350, 1400, 1450, 1500, 1550],
    'forecast': [980, 1120, 1000, 1180, 1300, 1250, 1300, 1320, 1380, 1480, 1520, 1500]
})

sop_data = SopDataGathering(historical)

# Calculate accuracy
accuracy = sop_data.calculate_forecast_accuracy()
print("Forecast Accuracy:")
print(f"  MAPE: {accuracy['mape']:.1f}%")
print(f"  Bias: {accuracy['bias_pct']:.1f}%")

# Identify exceptions
exceptions = sop_data.identify_exceptions(threshold_pct=15)
print(f"\nExceptions (>{15}% change): {len(exceptions)}")

# Generate baseline
baseline = sop_data.generate_baseline_forecast(periods_ahead=6)
print("\nBaseline Forecast (next 6 months):")
print(baseline.head())
ステップ2:需要レビュー

目的:

  • 統計予測をレビューし、調整します
  • 市場情報を反映します
  • 合意に基づく需要計画を作成します

参加者:

  • 営業部門のリーダー
  • マーケティング
  • 製品管理
  • 需要計画
  • 財務(オブザーバー)

活動:

  • 統計予測をレビューします
  • 今後の販促や製品投入について話し合います
  • 市場動向や競合他社の動きを考慮します
  • ビジネスインテリジェンスに基づいて予測を調整します
  • 前提条件とリスクを記録します

主な質問:

  • 先月から何が変わりましたか?
  • 新規顧客の獲得や顧客の喪失はありましたか?
  • 販促計画は確定しましたか?
  • 価格変更はどのような影響を及ぼしますか?
  • 競争環境に変化はありましたか?

成果物:

  • 製品ファミリー別の合意済み需要予測
  • 記録された需要の前提条件
  • 上振れ/下振れシナリオ
  • 特定されたリスクと機会
class DemandReview:
    """Demand review and consensus building"""

    def __init__(self, statistical_forecast):
        self.statistical_fcst = statistical_forecast
        self.adjustments = []
        self.assumptions = []

    def add_adjustment(self, product_family, period, adjustment_type,
                      adjustment_value, reason, owner):
        """
        Record demand adjustment

        Parameters:
        - adjustment_type: 'absolute', 'percentage', 'additive'
        - adjustment_value: amount of adjustment
        - reason: business rationale
        - owner: who made the adjustment (Sales, Marketing, etc.)
        """

        self.adjustments.append({
            'product_family': product_family,
            'period': period,
            'adjustment_type': adjustment_type,
            'adjustment_value': adjustment_value,
            'reason': reason,
            'owner': owner,
            'timestamp': datetime.now()
        })

    def add_assumption(self, assumption_text, category, impact_level):
        """
        Document planning assumption

        Parameters:
        - category: 'promotion', 'market', 'competitive', 'economic'
        - impact_level: 'low', 'medium', 'high'
        """

        self.assumptions.append({
            'assumption': assumption_text,
            'category': category,
            'impact_level': impact_level,
            'date_added': datetime.now()
        })

    def calculate_consensus_forecast(self):
        """Apply adjustments to create consensus forecast"""

        consensus = self.statistical_fcst.copy()

        for adj in self.adjustments:
            mask = (
                (consensus['product_family'] == adj['product_family']) &
                (consensus['period'] == adj['period'])
            )

            if adj['adjustment_type'] == 'percentage':
                consensus.loc[mask, 'consensus_forecast'] = (
                    consensus.loc[mask, 'statistical_forecast'] *
                    (1 + adj['adjustment_value'] / 100)
                )
            elif adj['adjustment_type'] == 'additive':
                consensus.loc[mask, 'consensus_forecast'] = (
                    consensus.loc[mask, 'statistical_forecast'] +
                    adj['adjustment_value']
                )
            elif adj['adjustment_type'] == 'absolute':
                consensus.loc[mask, 'consensus_forecast'] = adj['adjustment_value']

        # Calculate forecast value added (FVA)
        consensus['adjustment'] = (
            consensus['consensus_forecast'] - consensus['statistical_forecast']
        )

        return consensus

    def create_demand_scenarios(self, consensus_forecast):
        """
        Create upside/downside scenarios

        Typically: Base, Optimistic (+15%), Pessimistic (-15%)
        """

        scenarios = consensus_forecast.copy()

        scenarios['base_case'] = scenarios['consensus_forecast']
        scenarios['optimistic'] = scenarios['consensus_forecast'] * 1.15
        scenarios['pessimistic'] = scenarios['consensus_forecast'] * 0.85

        return scenarios

    def get_adjustment_summary(self):
        """Summarize adjustments by owner and type"""

        if not self.adjustments:
            return pd.DataFrame()

        df = pd.DataFrame(self.adjustments)

        summary = df.groupby(['owner', 'adjustment_type']).agg({
            'adjustment_value': ['count', 'sum', 'mean']
        }).reset_index()

        return summary

# Example
statistical_fcst = pd.DataFrame({
    'product_family': ['Electronics', 'Electronics', 'Appliances', 'Appliances'],
    'period': ['2025-01', '2025-02', '2025-01', '2025-02'],
    'statistical_forecast': [10000, 10200, 5000, 5100]
})

demand_review = DemandReview(statistical_fcst)

# Add adjustments
demand_review.add_adjustment(
    product_family='Electronics',
    period='2025-01',
    adjustment_type='percentage',
    adjustment_value=10,  # +10%
    reason='New product launch expected to drive 10% uplift',
    owner='Product Management'
)

demand_review.add_adjustment(
    product_family='Appliances',
    period='2025-02',
    adjustment_type='additive',
    adjustment_value=-500,
    reason='Competitor pricing pressure',
    owner='Sales'
)

# Add assumptions
demand_review.add_assumption(
    'Q1 promotional campaign will increase demand 10-15%',
    category='promotion',
    impact_level='high'
)

# Calculate consensus
consensus = demand_review.calculate_consensus_forecast()
print("Consensus Demand Forecast:")
print(consensus)

# Scenarios
scenarios = demand_review.create_demand_scenarios(consensus)
print("\nDemand Scenarios:")
print(scenarios[['product_family', 'period', 'pessimistic', 'base_case', 'optimistic']])
ステップ 3:供給レビュー

目的:

  • 需要を満たす供給能力を評価する
  • 制約と不足を特定する
  • 供給の代替案を策定する

参加者:

  • オペレーション部門のリーダー
  • 製造部門
  • 調達部門
  • 供給計画部門
  • エンジニアリング部門

活動:

  • 生産能力と需要を比較して確認する
  • ボトルネックと制約を特定する
  • サプライヤーの能力を評価する
  • 在庫状況を評価する
  • 供給ソリューションを提案する

主な質問:

  • 需要計画を満たせますか?
  • 制約は何ですか?
  • 能力増強に必要なリードタイムはどれくらいですか?
  • サプライチェーンのリスクはありますか?
  • コストへの影響は何ですか?

成果物:

  • 製品ファミリー別の供給計画
  • 特定された能力不足
  • 供給の代替シナリオ
  • 在庫戦略
  • 投資要件
class SupplyReview:
    """Supply review and capacity analysis"""

    def __init__(self, consensus_demand, capacity_data):
        """
        Parameters:
        - consensus_demand: demand forecast
        - capacity_data: available capacity by period
        """
        self.demand = consensus_demand
        self.capacity = capacity_data

    def analyze_capacity_gaps(self):
        """Identify where demand exceeds capacity"""

        # Merge demand and capacity
        analysis = self.demand.merge(
            self.capacity,
            on=['product_family', 'period'],
            how='left'
        )

        # Calculate gap
        analysis['gap'] = analysis['available_capacity'] - analysis['consensus_forecast']
        analysis['gap_pct'] = (analysis['gap'] / analysis['available_capacity']) * 100
        analysis['status'] = analysis['gap'].apply(
            lambda x: 'OK' if x >= 0 else 'CONSTRAINED'
        )

        # Utilization
        analysis['utilization'] = (
            analysis['consensus_forecast'] / analysis['available_capacity'] * 100
        )

        return analysis

    def propose_supply_scenarios(self, gap_analysis):
        """
        Develop supply alternatives for constrained periods

        Scenarios:
        1. Do Nothing (accept stockouts)
        2. Add Overtime
        3. Outsource
        4. Build Ahead
        """

        constrained = gap_analysis[gap_analysis['status'] == 'CONSTRAINED'].copy()

        if constrained.empty:
            return pd.DataFrame()

        scenarios = []

        for idx, row in constrained.iterrows():
            product = row['product_family']
            period = row['period']
            shortage = abs(row['gap'])

            # Scenario 1: Do Nothing
            scenarios.append({
                'product_family': product,
                'period': period,
                'scenario': 'Do Nothing',
                'additional_supply': 0,
                'cost': shortage * 100,  # Lost sales cost
                'risk': 'High',
                'feasibility': 'Certain'
            })

            # Scenario 2: Overtime
            overtime_capacity = row['available_capacity'] * 0.15  # 15% OT
            overtime_supply = min(shortage, overtime_capacity)
            scenarios.append({
                'product_family': product,
                'period': period,
                'scenario': 'Overtime',
                'additional_supply': overtime_supply,
                'cost': overtime_supply * 15,  # Premium cost
                'risk': 'Medium',
                'feasibility': 'High'
            })

            # Scenario 3: Outsource
            scenarios.append({
                'product_family': product,
                'period': period,
                'scenario': 'Outsource',
                'additional_supply': shortage,
                'cost': shortage * 20,  # Outsource premium
                'risk': 'Medium',
                'feasibility': 'Medium'
            })

            # Scenario 4: Build Ahead
            # Produce in prior period
            scenarios.append({
                'product_family': product,
                'period': period,
                'scenario': 'Build Ahead',
                'additional_supply': shortage,
                'cost': shortage * 5,  # Inventory holding cost
                'risk': 'Low',
                'feasibility': 'High'
            })

        return pd.DataFrame(scenarios)

    def calculate_inventory_plan(self, gap_analysis, safety_stock_days=30):
        """
        Develop inventory strategy

        Target: Safety stock + cycle stock
        """

        inventory_plan = gap_analysis.copy()

        # Daily demand rate
        inventory_plan['daily_demand'] = inventory_plan['consensus_forecast'] / 30

        # Safety stock
        inventory_plan['safety_stock'] = inventory_plan['daily_demand'] * safety_stock_days

        # Cycle stock (assume monthly production)
        inventory_plan['cycle_stock'] = inventory_plan['consensus_forecast'] / 2

        # Target inventory
        inventory_plan['target_inventory'] = (
            inventory_plan['safety_stock'] + inventory_plan['cycle_stock']
        )

        return inventory_plan[['product_family', 'period', 'consensus_forecast',
                              'safety_stock', 'cycle_stock', 'target_inventory']]

# Example
consensus_demand = pd.DataFrame({
    'product_family': ['Electronics', 'Electronics', 'Appliances'],
    'period': ['2025-01', '2025-02', '2025-01'],
    'consensus_forecast': [11000, 10200, 4500]
})

capacity_data = pd.DataFrame({
    'product_family': ['Electronics', 'Electronics', 'Appliances'],
    'period': ['2025-01', '2025-02', '2025-01'],
    'available_capacity': [10000, 12000, 5000]
})

supply_review = SupplyReview(consensus_demand, capacity_data)

# Analyze gaps
gaps = supply_review.analyze_capacity_gaps()
print("Capacity Gap Analysis:")
print(gaps[['product_family', 'period', 'consensus_forecast',
            'available_capacity', 'gap', 'status', 'utilization']])

# Propose scenarios
scenarios = supply_review.propose_supply_scenarios(gaps)
if not scenarios.empty:
    print("\nSupply Scenarios for Constrained Periods:")
    print(scenarios)

# Inventory plan
inventory = supply_review.calculate_inventory_plan(gaps)
print("\nInventory Plan:")
print(inventory)
ステップ 4:S&OP 事前会議

目的:

  • 需要と供給の整合を図る
  • 経営陣向けの推奨事項を策定する
  • シナリオとトレードオフを準備する

参加者:

  • S&OP プロセスオーナー/ファシリテーター
  • 需要計画リーダー
  • 供給計画リーダー
  • 財務部門
  • 選定された事業部門のリーダー

活動:

  • 需要と供給のバランスを確認する
  • 不足と競合を特定する
  • メリット・デメリット・コストを含むシナリオを策定する
  • 財務への影響を整合させる
  • 経営会議向けのプレゼンテーションを準備する

主な成果物:

  • 需要と供給のバランスが取れた計画(基本ケース)
  • トレードオフを含む代替シナリオ
  • 財務面での整合
  • 根拠を添えた推奨事項
  • 経営陣の判断が必要な未解決事項
class PreSopMeeting:
    """Pre-S&OP meeting preparation and analysis"""

    def __init__(self, demand_plan, supply_plan, financial_data):
        self.demand = demand_plan
        self.supply = supply_plan
        self.financial = financial_data

    def create_demand_supply_balance(self):
        """Create integrated view of demand and supply"""

        balance = self.demand.merge(
            self.supply,
            on=['product_family', 'period'],
            how='outer'
        )

        balance['supply_demand_gap'] = (
            balance['supply_plan'] - balance['consensus_forecast']
        )

        balance['inventory_impact'] = balance['supply_demand_gap'].cumsum()

        return balance

    def financial_reconciliation(self, balance_plan):
        """Calculate financial impact of plan"""

        financial = balance_plan.merge(
            self.financial,
            on='product_family',
            how='left'
        )

        # Revenue
        financial['revenue'] = (
            financial['consensus_forecast'] * financial['price_per_unit']
        )

        # COGS
        financial['cogs'] = (
            financial['supply_plan'] * financial['cost_per_unit']
        )

        # Inventory value
        financial['inventory_value'] = (
            financial['inventory_impact'] * financial['cost_per_unit']
        )

        # Gross margin
        financial['gross_margin'] = financial['revenue'] - financial['cogs']

        # Aggregate by period
        summary = financial.groupby('period').agg({
            'revenue': 'sum',
            'cogs': 'sum',
            'gross_margin': 'sum',
            'inventory_value': 'sum'
        }).reset_index()

        summary['gross_margin_pct'] = (
            summary['gross_margin'] / summary['revenue'] * 100
        )

        return summary

    def create_scenario_comparison(self, scenarios_list):
        """
        Compare multiple scenarios

        Parameters:
        - scenarios_list: list of dicts with scenario details
        """

        comparison = pd.DataFrame(scenarios_list)

        # Rank scenarios
        comparison['total_score'] = (
            comparison['service_level'] * 0.4 +
            (100 - comparison['cost_impact_pct']) * 0.3 +
            comparison['feasibility'] * 0.3
        )

        comparison = comparison.sort_values('total_score', ascending=False)

        return comparison

    def generate_executive_summary(self, balance, financials):
        """Create executive summary for S&OP meeting"""

        summary = {
            'planning_period': balance['period'].min(),
            'total_demand': balance['consensus_forecast'].sum(),
            'total_supply': balance['supply_plan'].sum(),
            'net_inventory_change': balance['supply_demand_gap'].sum(),
            'revenue_plan': financials['revenue'].sum(),
            'gross_margin_pct': (
                financials['gross_margin'].sum() /
                financials['revenue'].sum() * 100
            ),
            'constraints': balance[balance['supply_demand_gap'] < 0].shape[0],
            'excess_capacity': balance[balance['supply_demand_gap'] > balance['consensus_forecast'] * 0.2].shape[0]
        }

        return summary

# Example
demand_plan = pd.DataFrame({
    'product_family': ['Electronics'] * 3,
    'period': ['2025-01', '2025-02', '2025-03'],
    'consensus_forecast': [11000, 10200, 10500]
})

supply_plan = pd.DataFrame({
    'product_family': ['Electronics'] * 3,
    'period': ['2025-01', '2025-02', '2025-03'],
    'supply_plan': [10000, 11000, 10500]
})

financial_data = pd.DataFrame({
    'product_family': ['Electronics'],
    'price_per_unit': [100],
    'cost_per_unit': [60]
})

pre_sop = PreSopMeeting(demand_plan, supply_plan, financial_data)

# Create balance
balance = pre_sop.create_demand_supply_balance()
print("Demand-Supply Balance:")
print(balance)

# Financial reconciliation
financials = pre_sop.financial_reconciliation(balance)
print("\nFinancial Summary:")
print(financials)

# Executive summary
exec_summary = pre_sop.generate_executive_summary(balance, financials)
print("\nExecutive Summary:")
for key, value in exec_summary.items():
    print(f"  {key}: {value}")

# Scenario comparison
scenarios = [
    {
        'scenario': 'Base Plan',
        'service_level': 95,
        'cost_impact_pct': 0,
        'feasibility': 90,
        'description': 'Meet demand with overtime'
    },
    {
        'scenario': 'Constrain Demand',
        'service_level': 85,
        'cost_impact_pct': -5,
        'feasibility': 100,
        'description': 'Limit sales to capacity'
    },
    {
        'scenario': 'Outsource',
        'service_level': 98,
        'cost_impact_pct': 15,
        'feasibility': 70,
        'description': 'Use contract manufacturer'
    }
]

scenario_comparison = pre_sop.create_scenario_comparison(scenarios)
print("\nScenario Comparison:")
print(scenario_comparison)
ステップ 5:経営会議での S&OP

目的:

  • 計画に関する最終決定を行う
  • 不足とトレードオフを解消する
  • リソースの割り当てを承認する
  • 戦略上の優先事項について認識を合わせる

参加者:

  • CEO または COO(議長)
  • 営業担当 VP
  • オペレーション担当 VP
  • CFO
  • サプライチェーン担当 VP
  • 製品/マーケティング担当 VP
  • 事業部門の責任者

所要時間: 2~4 時間

議題:

  1. 実績の確認(15 分)

    • 前月の実績と計画の比較
    • 主な指標と KPI
    • 予測精度
  2. 需要レビュー(30 分)

    • 製品ファミリー別のコンセンサス需要
    • 主な変更点と前提条件
    • リスクと機会
  3. 供給レビュー(30 分)

    • 生産能力と制約
    • 供給の代替案
    • 在庫戦略
  4. 財務レビュー(20 分)

    • 売上高と利益率への影響
    • 運転資本
    • 予算との整合
  5. シナリオとトレードオフ(45 分)

    • 代替案を提示する
    • 影響を検討する
    • 決定を下す
  6. 戦略的課題(30 分)

    • 新製品の発売
    • 生産能力への投資
    • 市場機会
    • リスク軽減策
  7. 決定事項とアクション(20 分)

    • 決定事項を記録する
    • アクション項目の担当者を割り当てる
    • フォローアップを設定する

主な決定事項:

  • 需要計画の承認
  • 供給・在庫計画の承認
  • リソース配分に関する決定
  • 投資の承認
  • 方針の変更

成果物:

  • 承認済みの S&OP 計画
  • 決定事項の記録
  • 担当者付きのアクション項目
  • リスクと緊急時対応策
from dataclasses import dataclass
from typing import List
from datetime import datetime

@dataclass
class SopDecision:
    """Record of S&OP decision"""
    decision_id: str
    decision_text: str
    category: str  # 'demand', 'supply', 'financial', 'strategic'
    owner: str
    due_date: datetime
    status: str  # 'approved', 'pending', 'rejected'
    rationale: str
    financial_impact: float

@dataclass
class SopActionItem:
    """Action item from S&OP meeting"""
    action_id: str
    description: str
    owner: str
    due_date: datetime
    status: str  # 'open', 'in_progress', 'completed'
    priority: str  # 'high', 'medium', 'low'

class ExecutiveSopMeeting:
    """Executive S&OP meeting management"""

    def __init__(self, meeting_date):
        self.meeting_date = meeting_date
        self.decisions = []
        self.action_items = []
        self.approved_plan = None

    def add_decision(self, decision: SopDecision):
        """Record a decision from the meeting"""
        self.decisions.append(decision)

    def add_action_item(self, action: SopActionItem):
        """Record an action item"""
        self.action_items.append(action)

    def approve_plan(self, plan_data):
        """Approve the S&OP plan"""
        self.approved_plan = {
            'approval_date': self.meeting_date,
            'plan': plan_data,
            'status': 'approved'
        }

    def generate_meeting_minutes(self):
        """Create meeting minutes document"""

        minutes = {
            'meeting_date': self.meeting_date,
            'decisions_count': len(self.decisions),
            'action_items_count': len(self.action_items),
            'decisions': [
                {
                    'id': d.decision_id,
                    'decision': d.decision_text,
                    'owner': d.owner,
                    'impact': d.financial_impact
                }
                for d in self.decisions
            ],
            'action_items': [
                {
                    'id': a.action_id,
                    'action': a.description,
                    'owner': a.owner,
                    'due': a.due_date,
                    'priority': a.priority
                }
                for a in self.action_items
            ]
        }

        return minutes

    def get_decision_summary(self):
        """Summarize decisions by category"""

        if not self.decisions:
            return pd.DataFrame()

        decisions_df = pd.DataFrame([
            {
                'category': d.category,
                'status': d.status,
                'financial_impact': d.financial_impact
            }
            for d in self.decisions
        ])

        summary = decisions_df.groupby(['category', 'status']).agg({
            'financial_impact': 'sum'
        }).reset_index()

        return summary

# Example
exec_sop = ExecutiveSopMeeting(meeting_date=datetime(2025, 1, 15))

# Record decisions
exec_sop.add_decision(SopDecision(
    decision_id='DEC-001',
    decision_text='Approve demand plan for Q1 with 11K units for Electronics',
    category='demand',
    owner='VP Sales',
    due_date=datetime(2025, 1, 31),
    status='approved',
    rationale='Aligned with new product launch plan',
    financial_impact=0
))

exec_sop.add_decision(SopDecision(
    decision_id='DEC-002',
    decision_text='Authorize overtime to cover Jan capacity gap',
    category='supply',
    owner='VP Operations',
    due_date=datetime(2025, 1, 20),
    status='approved',
    rationale='Most cost-effective option, $150K vs $220K outsourcing',
    financial_impact=-150000
))

exec_sop.add_decision(SopDecision(
    decision_id='DEC-003',
    decision_text='Approve $3.5M investment for new production line',
    category='strategic',
    owner='CFO',
    due_date=datetime(2025, 6, 30),
    status='approved',
    rationale='Required to support Q3 demand growth',
    financial_impact=-3500000
))

# Add action items
exec_sop.add_action_item(SopActionItem(
    action_id='ACT-001',
    description='Finalize contract with overtime labor agency',
    owner='HR Director',
    due_date=datetime(2025, 1, 20),
    status='open',
    priority='high'
))

exec_sop.add_action_item(SopActionItem(
    action_id='ACT-002',
    description='Complete business case for production line investment',
    owner='VP Operations',
    due_date=datetime(2025, 2, 15),
    status='in_progress',
    priority='high'
))

# Generate minutes
minutes = exec_sop.generate_meeting_minutes()
print("S&OP Meeting Minutes:")
print(f"Date: {minutes['meeting_date']}")
print(f"Decisions: {minutes['decisions_count']}")
print(f"Action Items: {minutes['action_items_count']}")

# Decision summary
decision_summary = exec_sop.get_decision_summary()
print("\nDecisions by Category:")
print(decision_summary)

S&OP 成熟度モデル

レベル 1:場当たり的(非定型)

特徴:

  • 正式な S&OP プロセスがない
  • Excel ベースで手作業
  • 営業とオペレーションの間に連携がない
  • その場しのぎの対応に追われている

兆候:

  • 緊急の手配が頻発する
  • 予測精度が低い(60% 未満)
  • 過剰在庫または欠品
  • 約束した納期を守れない
レベル 2:初歩的

特徴:

  • 月次の S&OP 会議を開始している
  • 部門横断的な参加が一部ある
  • 短期(1~3 か月)に焦点を当てている
  • データ統合が限定的

能力:

  • 基本的な需要レビュー
  • 生産能力の確認
  • 経営陣の認識
レベル 3:標準

特徴:

  • 体系化された 5 ステップのプロセス
  • 12~18 か月の計画期間
  • 良好なデータ統合
  • 定期的な実施サイクル

能力:

  • コンセンサス予測
  • 生産能力計画
  • 財務面での整合
  • シナリオ分析
レベル 4:高度

特徴:

  • 統合事業計画(IBP)
  • 24 か月以上の計画期間
  • 戦略との整合
  • リアルタイム更新

能力:

  • ポートフォリオ管理
  • 収益性の最適化
  • What-if シミュレーション
  • 分析の自動化
レベル 5:先を見越した最上級の取り組み

特徴:

  • 予測的かつ処方的
  • AI/MLを活用したインサイト
  • 動的な計画策定
  • バリューチェーンの連携

機能:

  • 予測分析
  • 最適化アルゴリズム
  • デジタルツインのモデリング
  • 社外との連携(サプライヤー、顧客)

S&OP の指標と KPI

予測精度

MAPE(平均絶対パーセント誤差)

  • 目標:集計レベルでは <20%、詳細レベルでは <30%
  • 製品ファミリーごとに毎月測定
  • 経時的な傾向を追跡

バイアス

  • 目標:±5%
  • 正のバイアス = 予測過多
  • 負のバイアス = 予測過少

予測付加価値(FVA)

  • 手動の修正によって精度が向上しているか?
  • 統計的な予測と合意に基づく予測のパフォーマンスを測定
供給パフォーマンス

計画達成率

  • 達成した生産計画の割合
  • 目標:>95%

設備稼働率

  • 使用した生産能力の割合
  • 目標:80~90%(柔軟性を確保)

供給の柔軟性

  • 生産能力の調整に要するリードタイム
  • 切り替え時間の割合
在庫指標

在庫回転率

  • 売上原価(COGS)/平均在庫
  • 目標:業界によって異なります(一般的には 6~12 回転)

在庫日数

  • 在庫/1 日あたりの売上
  • 目標:30~60 日

在庫精度

  • 許容範囲内にある SKU の割合
  • 目標:>95%
財務指標

売上計画達成率

  • 実際の売上/計画
  • 目標:95~105%

粗利益率

  • 計画に対する利益率
  • 差異の要因を追跡

運転資本

  • 在庫に拘束されている資金
  • キャッシュ・コンバージョン・サイクルを最適化
プロセス指標

会議の有効性

  • 期限内に決定された事項の割合
  • アクションアイテムの完了率
  • 参加者のエンゲージメントスコア

サイクルタイム

  • データ収集から意思決定までの時間
  • 目標:<4 週間

計画の安定性

  • 計画の変更頻度
  • 計画の不安定さを示す指標
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

class SopMetrics:
    """S&OP performance metrics tracking"""

    def __init__(self):
        self.metrics_history = []

    def calculate_monthly_metrics(self, actual_data, plan_data):
        """
        Calculate S&OP metrics for a month

        Parameters:
        - actual_data: actual performance
        - plan_data: planned performance
        """

        # Forecast accuracy (MAPE)
        mape = np.mean(
            np.abs((actual_data['demand'] - plan_data['forecast']) /
                   actual_data['demand'])
        ) * 100

        # Bias
        bias = np.mean(plan_data['forecast'] - actual_data['demand'])
        bias_pct = (bias / np.mean(actual_data['demand'])) * 100

        # Plan attainment
        plan_attainment = (
            actual_data['production'].sum() / plan_data['production_plan'].sum()
        ) * 100

        # Inventory metrics
        inventory_turns = (
            actual_data['cogs'].sum() /
            actual_data['avg_inventory'].mean()
        )

        days_of_supply = (
            actual_data['ending_inventory'].iloc[-1] /
            (actual_data['demand'].sum() / 30)
        )

        # Revenue attainment
        revenue_attainment = (
            actual_data['revenue'].sum() / plan_data['revenue_plan'].sum()
        ) * 100

        metrics = {
            'period': actual_data['period'].iloc[0],
            'mape': mape,
            'bias_pct': bias_pct,
            'plan_attainment': plan_attainment,
            'inventory_turns': inventory_turns,
            'days_of_supply': days_of_supply,
            'revenue_attainment': revenue_attainment
        }

        self.metrics_history.append(metrics)

        return metrics

    def plot_metrics_dashboard(self):
        """Create S&OP metrics dashboard"""

        if not self.metrics_history:
            return None

        df = pd.DataFrame(self.metrics_history)

        fig, axes = plt.subplots(2, 3, figsize=(16, 10))
        fig.suptitle('S&OP Performance Dashboard', fontsize=16, fontweight='bold')

        # MAPE
        axes[0, 0].plot(df['period'], df['mape'], marker='o', linewidth=2)
        axes[0, 0].axhline(y=20, color='r', linestyle='--', label='Target')
        axes[0, 0].set_title('Forecast Accuracy (MAPE)')
        axes[0, 0].set_ylabel('MAPE (%)')
        axes[0, 0].legend()
        axes[0, 0].grid(True, alpha=0.3)

        # Bias
        axes[0, 1].plot(df['period'], df['bias_pct'], marker='o', linewidth=2, color='orange')
        axes[0, 1].axhline(y=5, color='r', linestyle='--', alpha=0.5)
        axes[0, 1].axhline(y=-5, color='r', linestyle='--', alpha=0.5)
        axes[0, 1].set_title('Forecast Bias')
        axes[0, 1].set_ylabel('Bias (%)')
        axes[0, 1].grid(True, alpha=0.3)

        # Plan Attainment
        axes[0, 2].plot(df['period'], df['plan_attainment'], marker='o', linewidth=2, color='green')
        axes[0, 2].axhline(y=95, color='r', linestyle='--', label='Min Target')
        axes[0, 2].axhline(y=105, color='r', linestyle='--')
        axes[0, 2].set_title('Plan Attainment')
        axes[0, 2].set_ylabel('Attainment (%)')
        axes[0, 2].legend()
        axes[0, 2].grid(True, alpha=0.3)

        # Inventory Turns
        axes[1, 0].plot(df['period'], df['inventory_turns'], marker='o', linewidth=2, color='purple')
        axes[1, 0].axhline(y=8, color='g', linestyle='--', label='Target')
        axes[1, 0].set_title('Inventory Turns')
        axes[1, 0].set_ylabel('Turns')
        axes[1, 0].legend()
        axes[1, 0].grid(True, alpha=0.3)

        # Days of Supply
        axes[1, 1].plot(df['period'], df['days_of_supply'], marker='o', linewidth=2, color='brown')
        axes[1, 1].axhline(y=45, color='g', linestyle='--', label='Target')
        axes[1, 1].set_title('Days of Supply')
        axes[1, 1].set_ylabel('Days')
        axes[1, 1].legend()
        axes[1, 1].grid(True, alpha=0.3)

        # Revenue Attainment
        axes[1, 2].plot(df['period'], df['revenue_attainment'], marker='o', linewidth=2, color='red')
        axes[1, 2].axhline(y=95, color='r', linestyle='--', alpha=0.5)
        axes[1, 2].axhline(y=105, color='r', linestyle='--', alpha=0.5)
        axes[1, 2].axhspan(95, 105, alpha=0.2, color='green')
        axes[1, 2].set_title('Revenue Attainment')
        axes[1, 2].set_ylabel('Attainment (%)')
        axes[1, 2].grid(True, alpha=0.3)

        plt.tight_layout()

        return fig

# Example
metrics = SopMetrics()

# Simulate 6 months of data
for month in range(1, 7):
    actual = pd.DataFrame({
        'period': [f'2025-{month:02d}'],
        'demand': [10000 + np.random.randint(-500, 500)],
        'production': [10000 + np.random.randint(-300, 300)],
        'revenue': [1000000 + np.random.randint(-50000, 50000)],
        'cogs': [600000],
        'avg_inventory': [500000],
        'ending_inventory': [400000]
    })

    plan = pd.DataFrame({
        'forecast': [10000],
        'production_plan': [10000],
        'revenue_plan': [1000000]
    })

    monthly_metrics = metrics.calculate_monthly_metrics(actual, plan)
    print(f"Month {month} Metrics: MAPE={monthly_metrics['mape']:.1f}%")

# Plot dashboard
metrics.plot_metrics_dashboard()

ツールとライブラリ

Python ライブラリ

データ分析:

  • pandas:データ操作
  • numpy:数値計算
  • scipy:統計分析

最適化:

  • pulp:線形計画法
  • pyomo:高度な最適化

可視化:

  • matplotlib、seaborn:グラフ
  • plotly:インタラクティブなダッシュボード
  • dash:ウェブアプリケーション
商用 S&OP ソフトウェア

エンタープライズプラットフォーム:

  • Kinaxis RapidResponse:S&OP/IBP をリードするプラットフォーム
  • o9 Solutions:AI を活用したデジタルプラットフォーム
  • SAP IBP:統合事業計画
  • Oracle Cloud Supply Chain Planning:S&OP モジュール
  • Blue Yonder:S&OP と需要・供給のマッチング
  • Anaplan:連携型計画プラットフォーム
  • Logility:サプライチェーン計画スイート

コラボレーションツール:

  • Microsoft Teams:会議とコラボレーション
  • Slack:非同期コミュニケーション
  • Miro / Mural:オンラインホワイトボード
  • Power BI / Tableau:可視化
Excel / Google Sheets

中規模企業では、現在も S&OP に広く使用されています。

  • 計画テンプレート
  • シナリオモデリング
  • 財務の照合
  • 経営幹部向けダッシュボード

よくある課題と解決策

課題:部門横断の連携不足

問題:

  • 営業とオペレーションの間にあるサイロ化
  • 責任のなすり合い
  • 会議への参加意欲の低さ

解決策:

  • 経営幹部の支援と説明責任
  • 明確な役割と責任(RACI)
  • 共通の指標とインセンティブ
  • 信頼関係を築く活動
  • 専門的なファシリテーション
課題:データ品質と統合

問題:

  • 信頼できる情報源が複数存在する
  • 手作業によるデータ収集
  • エラーや不整合
  • 準備に時間がかかる

解決策:

  • 単一の統合システム
  • データフィードの自動化
  • データガバナンスのプロセス
  • マスターデータ管理
  • 例外に基づくレビュー
課題:短期的な視点

問題:

  • 先の見通しが 1~3 か月に限られている
  • 事後対応に偏り、先を見越した対応ができていない
  • 戦略上の課題を見落とす

解決策:

  • 計画期間を 18~24 か月に延長
  • 毎月のローリング予測
  • 四半期ごとの戦略レビュー
  • 長期的な生産能力計画
  • 新製品の統合
課題:会議の過多

問題:

  • 会議が多すぎます
  • 議論が繰り返されます
  • 意思決定疲れが生じます

解決策:

  • 5ステップのプロセスに簡素化します
  • 事前課題と準備を行います
  • 明確な議題と時間制限を設定します
  • 戦術的な意思決定を委任します
  • 例外ベースでレビューします
課題:経営幹部の関与不足

問題:

  • 経営幹部が会議を欠席します
  • 決定を形式的に承認します
  • 価値を見いだせません

解決策:

  • 適切な詳細レベルにします(戦略的であり、戦術的ではない)
  • 必要な意思決定を明確にします
  • ビジネスへの影響を示します
  • 財務結果と結び付けます
  • 成功事例や成果を共有します

出力形式

S&OP レポートの構成

エグゼクティブダッシュボード(1ページ):

指標現在目標状況
予測精度(MAPE)18.5%<20%✓
計画達成率96%>95%✓
在庫回転率7.28.0⚠
売上達成率102%95-105%✓

製品ファミリー別需要計画:

ファミリーJanFebMarQ1 合計前月比前年比
エレクトロニクス11,00010,20010,50031,700+5%+12%
家電製品4,5004,8005,00014,300+2%+8%

供給計画と制約:

ファミリー需要生産能力ギャップ状況解決策
エレクトロニクス11,00010,000(1,000)制約あり残業を承認
家電製品4,5005,000+500問題なし通常生産

財務サマリー:

指標今月来月Q1 計画
売上高$1.5M$1.4M$4.3M
売上原価$0.9M$0.85M$2.6M
売上総利益率40%39%39.5%
在庫価値$5.2M$5.0M$5.0M

決定事項:

  1. 注記された前提条件に基づき、Q1 需要計画を承認する
  2. 1月のエレクトロニクス部門の残業を承認する
  3. 生産能力拡大の事業計画を進める
  4. 3月までに家電製品の価格戦略を見直す

アクション項目:

  • 残業派遣会社との契約を確定する(人事、1月20日)
  • 生産能力拡大の事業計画を完成させる(オペレーション、2月15日)
  • 家電製品の価格分析を行う(マーケティング、3月1日)

確認事項

さらに詳しい情報が必要な場合:

  1. 現在の S&OP プロセスの成熟度はどの程度ですか?
  2. 現在、誰が S&OP に参加していますか?
  3. 計画期間はどのくらいですか?(何か月先まで)
  4. どのようなシステム/ツールを使用していますか?
  5. 最大の課題や問題点は何ですか?
  6. S&OP プロセスの責任者は誰ですか?
  7. 現在、予測はどのように作成していますか?
  8. 通常、どのような意思決定が必要ですか?

関連スキル

  • demand-forecasting:S&OP への需要計画のインプット
  • capacity-planning:供給能力の分析
  • master-production-scheduling:詳細な実行計画
  • scenario-planning:リスク分析と代替案
  • inventory-optimization:在庫戦略
  • supply-chain-analytics:指標と KPI
  • network-design:戦略的なネットワークに関する意思決定
  • financial-planning:財務の調整

コメント