sales-operations-planning について
Sales & Operations Planning(S&OP)と Integrated Business Planning(IBP)の設計・改善を支援するスキルです。部門横断の計画サイクル、意思決定、成熟度モデル、指標、関連ツールを解説します。
主な機能
- 01計画プロセスと組織状況の評価
- 02月次 S&OP の5段階サイクルの構成
- 03需要・供給・財務計画の調整
- 04シナリオと経営判断の準備
- 05予測・供給・在庫・財務指標の追跡
- 06成熟度レベルと計画ツールの確認
活用シーン
- 01定期的な部門横断 S&OP サイクルの確立
- 02需要と生産能力のギャップ解消
- 03経営層向けシナリオとトレードオフの準備
- 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の導入や改善に取り組む前に、以下を把握します。
-
現状
- 既存の計画プロセスはどのようなものですか?(非公式、Excelベース、システム主導)
- 計画サイクルと頻度はどのようなものですか?(月次、週次)
- 誰が参加していますか?(営業、オペレーション、財務、経営陣)
- 現在の課題や問題点は何ですか?
-
組織の状況
- 会社の規模や複雑さはどの程度ですか?
- 製品ファミリー/SKUはいくつありますか?
- 対象地域はどの範囲ですか?(単一拠点、地域、グローバル)
- 業界特有の特徴は何ですか?(季節性、販促、プロジェクトベース)
-
計画の成熟度
- 予測精度はどの程度ですか?
- 部門横断的な連携の質はどの程度ですか?
- データシステムと統合状況はどうなっていますか?
- パフォーマンス測定はどのように行っていますか?
-
事業目標
- 戦略目標は何ですか?(成長、利益率、サービス、在庫)
- 主要な指標と目標値は何ですか?
- 意思決定権限とガバナンスはどのようになっていますか?
- ステークホルダーの期待は何ですか?
S&OPフレームワーク
S&OPの定義
**Sales & Operations Planning(S&OP)**とは、顧客、営業、マーケティング、オペレーション、エンジニアリング、財務、製品管理など、事業に関するすべての計画をひとつの統合計画にまとめる、月次の統合経営プロセスです。
主な原則:
- 部門横断的な統合:サイロ化を解消します
- 将来志向:18~24か月のローリング計画期間を設定します
- 経営陣が主導:リーダーシップが説明責任を担います
- 唯一の信頼できる情報源:合意に基づく計画をひとつにまとめます
- 意思決定を重視:報告にとどまらず、具体的な行動につなげます
S&OPとIBP(Integrated Business Planning)の比較
| 項目 | S&OP | IBP |
|---|---|---|
| 対象範囲 | 需要と供給のバランス | 戦略と財務の統合 |
| 計画期間 | 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 時間
議題:
-
実績の確認(15 分)
- 前月の実績と計画の比較
- 主な指標と KPI
- 予測精度
-
需要レビュー(30 分)
- 製品ファミリー別のコンセンサス需要
- 主な変更点と前提条件
- リスクと機会
-
供給レビュー(30 分)
- 生産能力と制約
- 供給の代替案
- 在庫戦略
-
財務レビュー(20 分)
- 売上高と利益率への影響
- 運転資本
- 予算との整合
-
シナリオとトレードオフ(45 分)
- 代替案を提示する
- 影響を検討する
- 決定を下す
-
戦略的課題(30 分)
- 新製品の発売
- 生産能力への投資
- 市場機会
- リスク軽減策
-
決定事項とアクション(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.2 | 8.0 | ⚠ |
| 売上達成率 | 102% | 95-105% | ✓ |
製品ファミリー別需要計画:
| ファミリー | Jan | Feb | Mar | Q1 合計 | 前月比 | 前年比 |
|---|---|---|---|---|---|---|
| エレクトロニクス | 11,000 | 10,200 | 10,500 | 31,700 | +5% | +12% |
| 家電製品 | 4,500 | 4,800 | 5,000 | 14,300 | +2% | +8% |
供給計画と制約:
| ファミリー | 需要 | 生産能力 | ギャップ | 状況 | 解決策 |
|---|---|---|---|---|---|
| エレクトロニクス | 11,000 | 10,000 | (1,000) | 制約あり | 残業を承認 |
| 家電製品 | 4,500 | 5,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 |
決定事項:
- 注記された前提条件に基づき、Q1 需要計画を承認する
- 1月のエレクトロニクス部門の残業を承認する
- 生産能力拡大の事業計画を進める
- 3月までに家電製品の価格戦略を見直す
アクション項目:
- 残業派遣会社との契約を確定する(人事、1月20日)
- 生産能力拡大の事業計画を完成させる(オペレーション、2月15日)
- 家電製品の価格分析を行う(マーケティング、3月1日)
確認事項
さらに詳しい情報が必要な場合:
- 現在の S&OP プロセスの成熟度はどの程度ですか?
- 現在、誰が S&OP に参加していますか?
- 計画期間はどのくらいですか?(何か月先まで)
- どのようなシステム/ツールを使用していますか?
- 最大の課題や問題点は何ですか?
- S&OP プロセスの責任者は誰ですか?
- 現在、予測はどのように作成していますか?
- 通常、どのような意思決定が必要ですか?
関連スキル
- demand-forecasting:S&OP への需要計画のインプット
- capacity-planning:供給能力の分析
- master-production-scheduling:詳細な実行計画
- scenario-planning:リスク分析と代替案
- inventory-optimization:在庫戦略
- supply-chain-analytics:指標と KPI
- network-design:戦略的なネットワークに関する意思決定
- financial-planning:財務の調整