promotional-planning について
消費財の販促計画を支援するスキルです。販促効果と供給準備の両立に向け、リフトと ROI の分析、カレンダーや価格の最適化、需要予測、在庫・生産能力の計画を扱います。
主な機能
- 01販促リフトと ROI の分析
- 02販促カレンダーの最適化
- 03販促価格の最適化
- 04需要と在庫の予測
- 05生産・配送能力の評価
活用シーン
- 01予定する販促施策の収益性比較
- 02予算内での販促カレンダー作成
- 03小売イベント向け在庫の予測
- 04販促前の供給能力確認
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ kishorkukreja/ awesome-supply-chain --skill promotional-planningインストール後、Claude Code で/promotional-planningを入力して読み込まれたか確認します。
詳細
販促計画
CPGの販促計画とトレードプロモーションの最適化に精通しています。目標は、販促の実施を支えるサプライチェーンの準備を確保しながら、ROIを最大化する効果的な販促戦略の立案を支援することです。
初期評価
販促を計画する前に、以下を把握します。
-
ビジネスの背景
- 販促対象の商品/カテゴリーは何ですか?
- 小売業者/チャネルはどこですか?(食料品店、会員制倉庫型店舗、コンビニエンスストア、オンライン)
- 販促予算と目標は何ですか?
- 過去の販促効果(ROI、売上増加、純増効果)はどうでしたか?
-
販促戦略
- どのような販促タイプを利用していますか?(TPR、BOGO、まとめ買い、クーポン)
- 一般的な販促手法は何ですか?(価格割引率、特売広告、ディスプレイ)
- 販促頻度はどれくらいですか?(SKUごとの年間イベント数)
- 競合他社の販促活動はどうなっていますか?
-
データの利用可否
- 過去の販促実績データはありますか?
- 通常時と販促時の売上データはありますか?
- シンジケートデータ(Nielsen、IRI)はありますか?
- 小売業者のPOSデータにアクセスできますか?
- コストデータ(トレード支出、棚割り、ディスプレイ)はありますか?
-
サプライチェーンの準備状況
- 販促用の生産に必要なリードタイムはどれくらいですか?
- 生産能力に制約はありますか?
- 販促用在庫はどこにありますか?(先行購入)
- 需要急増に対応するための配送センターの処理能力はありますか?
販促計画のフレームワーク
トレードプロモーションの種類
1. 期間限定の価格引き下げ(TPR)
- 通常の棚価格から値引きします
- 一般的な値引き幅:15~30%
- 最も一般的な販促タイプです
- 小売業者が値引きを消費者に還元します
2. 特売広告
- 小売業者のチラシ/広告に商品を掲載します
- 通常はTPRと組み合わせます
- 高い認知度と強い売上増加が見込めます
- コスト:小売業者への広告協賛金
3. ディスプレイ
- 通常の棚以外に商品を配置します(エンド陳列、フロアスタンド)
- 視認性と衝動買いを高めます
- TPRや特売広告と組み合わせることがよくあります
- コスト:ディスプレイ協賛金
4. まとめ買いオファー
- 「2個買うと1個無料」「3個で5ドル」
- 買いだめを促します
- 1回あたりの平均購入額を高めます
- 将来の売上を共食いする可能性があります
5. クーポン
- メーカーまたは小売業者のクーポン
- 特定の消費者を対象にします
- 利用状況を追跡できます
- デジタルクーポンやモバイルクーポンが増加しています
6. 店頭イベント
- サンプリング、実演
- 商品についての情報提供
- 高いエンゲージメントが得られますが、コストも高くなります
- 新商品に適しています
7. ロイヤルティプログラム
- ポイント、特典、パーソナライズされたオファー
- 重要性が高まっています(デジタル化)
- 小売業者ごとに異なります
- イベント型ではなく継続的に実施します
販促による売上増加のモデリング
ベースライン売上と純増売上
ベースライン売上:
- 販促がなくても発生する売上です
- 真の売上増加を算出するうえで重要です
- 方法:販促のない週の平均、時系列分解
純増売上:
- 販促によって発生した追加売上です
- 純増売上 = 販促時の売上 - ベースライン売上
- ROI算出の主要指標です
売上増加率:
Lift = (Promotional Sales - Baseline Sales) / Baseline Sales
販促反応モデル
1. 価格弾力性モデル
import numpy as np
import pandas as pd
from sklearn.linear_model import LinearRegression
import statsmodels.api as sm
def estimate_price_elasticity(sales_data):
"""
Estimate price elasticity from historical data
Parameters:
- sales_data: DataFrame with columns ['price', 'volume', 'feature',
'display', 'week', 'sku']
Returns:
- elasticity coefficient and model
"""
# Log-log model for elasticity
sales_data['log_price'] = np.log(sales_data['price'])
sales_data['log_volume'] = np.log(sales_data['volume'])
# Add promotional variables
X = sales_data[['log_price', 'feature', 'display']]
X = sm.add_constant(X)
y = sales_data['log_volume']
# OLS regression
model = sm.OLS(y, X).fit()
elasticity = {
'price_elasticity': model.params['log_price'],
'feature_lift': np.exp(model.params['feature']) - 1,
'display_lift': np.exp(model.params['display']) - 1,
'r_squared': model.rsquared,
'model': model
}
return elasticity
# Example usage
sales_data = pd.DataFrame({
'price': [3.99, 3.99, 2.99, 2.99, 3.99, 2.99],
'volume': [1000, 1100, 1800, 2000, 950, 1850],
'feature': [0, 0, 1, 1, 0, 1],
'display': [0, 0, 0, 1, 0, 1]
})
elasticity = estimate_price_elasticity(sales_data)
print(f"Price Elasticity: {elasticity['price_elasticity']:.2f}")
print(f"Feature Lift: {elasticity['feature_lift']:.1%}")
print(f"Display Lift: {elasticity['display_lift']:.1%}")
2. 乗法的販促モデル
def promotional_uplift_model(baseline, price_discount, feature, display):
"""
Calculate promotional uplift using multiplicative model
Parameters:
- baseline: baseline sales volume
- price_discount: % discount (e.g., 0.20 for 20% off)
- feature: 1 if featured, 0 otherwise
- display: 1 if on display, 0 otherwise
Returns:
- predicted promotional sales
"""
# Typical lift factors (calibrate from historical data)
PRICE_ELASTICITY = -2.5 # 10% price cut → 25% volume increase
FEATURE_LIFT = 1.5 # Feature alone increases sales 50%
DISPLAY_LIFT = 2.0 # Display alone doubles sales
# Price effect
price_effect = (1 + price_discount) ** PRICE_ELASTICITY
# Feature effect
feature_effect = FEATURE_LIFT if feature else 1.0
# Display effect
display_effect = DISPLAY_LIFT if display else 1.0
# Multiplicative model
promotional_sales = baseline * price_effect * feature_effect * display_effect
return promotional_sales
# Example: 20% discount with feature and display
baseline = 1000
promo_sales = promotional_uplift_model(
baseline=baseline,
price_discount=0.20,
feature=1,
display=1
)
lift = (promo_sales - baseline) / baseline
print(f"Baseline: {baseline}")
print(f"Promotional Sales: {promo_sales:.0f}")
print(f"Lift: {lift:.1%}")
3. 機械学習による販促モデル
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_absolute_error, r2_score
def train_promotional_model(historical_data):
"""
Train ML model to predict promotional sales
Parameters:
- historical_data: DataFrame with promotional events and outcomes
Returns:
- trained model and performance metrics
"""
# Features
feature_cols = [
'regular_price',
'promotional_price',
'discount_pct',
'feature',
'display',
'brand',
'category',
'retailer',
'week_of_year',
'holiday_week',
'competitive_promos',
'inventory_level',
'days_since_last_promo'
]
X = historical_data[feature_cols]
y = historical_data['promotional_sales']
# Train/test split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# Gradient Boosting model
model = GradientBoostingRegressor(
n_estimators=200,
learning_rate=0.1,
max_depth=5,
random_state=42
)
model.fit(X_train, y_train)
# Evaluate
y_pred = model.predict(X_test)
mae = mean_absolute_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)
# Feature importance
feature_importance = pd.DataFrame({
'feature': feature_cols,
'importance': model.feature_importances_
}).sort_values('importance', ascending=False)
return {
'model': model,
'mae': mae,
'r2': r2,
'feature_importance': feature_importance
}
販促ROI分析
ROI算出のフレームワーク
def calculate_promotional_roi(promo_data):
"""
Calculate comprehensive promotional ROI
Parameters:
- promo_data: dict with promotional details
Returns:
- ROI metrics and profitability analysis
"""
# Sales metrics
baseline_units = promo_data['baseline_units']
promo_units = promo_data['promo_units']
incremental_units = promo_units - baseline_units
regular_price = promo_data['regular_price']
promo_price = promo_data['promo_price']
# Revenue
baseline_revenue = baseline_units * regular_price
promo_revenue = promo_units * promo_price
incremental_revenue = promo_revenue - baseline_revenue
# Costs
cogs = promo_data['cogs_per_unit']
baseline_cogs = baseline_units * cogs
promo_cogs = promo_units * cogs
incremental_cogs = promo_cogs - baseline_cogs
# Trade spending
trade_spend = {
'price_discount': baseline_units * (regular_price - promo_price),
'off_invoice': promo_data.get('off_invoice', 0),
'feature_allowance': promo_data.get('feature_cost', 0),
'display_allowance': promo_data.get('display_cost', 0),
'slotting_fees': promo_data.get('slotting', 0),
'coop_advertising': promo_data.get('coop_ad', 0)
}
total_trade_spend = sum(trade_spend.values())
# Incremental profit
baseline_profit = baseline_revenue - baseline_cogs
promo_gross_profit = promo_revenue - promo_cogs
incremental_gross_profit = promo_gross_profit - baseline_profit
# Net incremental profit (after trade spend)
net_incremental_profit = incremental_gross_profit - total_trade_spend
# ROI
roi = net_incremental_profit / total_trade_spend if total_trade_spend > 0 else 0
# Payback
payback = total_trade_spend / incremental_gross_profit if incremental_gross_profit > 0 else float('inf')
return {
'baseline_units': baseline_units,
'promo_units': promo_units,
'incremental_units': incremental_units,
'lift_pct': (incremental_units / baseline_units * 100),
'baseline_revenue': baseline_revenue,
'promo_revenue': promo_revenue,
'incremental_revenue': incremental_revenue,
'baseline_profit': baseline_profit,
'promo_profit': promo_gross_profit,
'incremental_profit': incremental_gross_profit,
'trade_spend': total_trade_spend,
'trade_spend_breakdown': trade_spend,
'net_profit': net_incremental_profit,
'roi': roi,
'roi_pct': roi * 100,
'payback': payback
}
# Example
promo_result = calculate_promotional_roi({
'baseline_units': 1000,
'promo_units': 2500,
'regular_price': 4.99,
'promo_price': 3.99,
'cogs_per_unit': 2.50,
'off_invoice': 500,
'feature_cost': 1000,
'display_cost': 800,
'slotting': 0,
'coop_ad': 500
})
print(f"Lift: {promo_result['lift_pct']:.0f}%")
print(f"Incremental Units: {promo_result['incremental_units']}")
print(f"Trade Spend: ${promo_result['trade_spend']:,.0f}")
print(f"Net Profit: ${promo_result['net_profit']:,.0f}")
print(f"ROI: {promo_result['roi_pct']:.0f}%")
販促後の分析
def post_promotion_analysis(sales_data, promo_start_week, promo_duration):
"""
Analyze post-promotion effects including cannibalization
Parameters:
- sales_data: weekly sales data
- promo_start_week: week number promotion started
- promo_duration: number of weeks promotion ran
Returns:
- analysis of pre, during, and post promotion periods
"""
promo_end_week = promo_start_week + promo_duration
# Define periods
pre_promo = sales_data[
(sales_data['week'] >= promo_start_week - 4) &
(sales_data['week'] < promo_start_week)
]
during_promo = sales_data[
(sales_data['week'] >= promo_start_week) &
(sales_data['week'] < promo_end_week)
]
post_promo = sales_data[
(sales_data['week'] >= promo_end_week) &
(sales_data['week'] < promo_end_week + 4)
]
# Calculate averages
avg_pre = pre_promo['sales'].mean()
avg_during = during_promo['sales'].mean()
avg_post = post_promo['sales'].mean()
# Pantry loading effect (post-promo dip)
pantry_loading = avg_pre - avg_post if avg_post < avg_pre else 0
# True incrementality (accounting for pantry loading)
total_promo_sales = during_promo['sales'].sum()
expected_baseline = avg_pre * promo_duration
post_promo_impact = pantry_loading * len(post_promo)
true_incremental = total_promo_sales - expected_baseline - post_promo_impact
return {
'avg_pre_promo': avg_pre,
'avg_during_promo': avg_during,
'avg_post_promo': avg_post,
'lift_during': (avg_during - avg_pre) / avg_pre * 100,
'post_promo_dip': (avg_pre - avg_post) / avg_pre * 100,
'pantry_loading': pantry_loading,
'true_incremental_sales': true_incremental,
'true_incrementality_pct': true_incremental / expected_baseline * 100
}
販促の最適化
最適な販促カレンダー
from pulp import *
import pandas as pd
import numpy as np
def optimize_promotional_calendar(products, budget, constraints):
"""
Optimize promotional calendar to maximize ROI within budget
Parameters:
- products: DataFrame with product promotional opportunities
- budget: total promotional budget
- constraints: dict with business constraints
Returns:
- optimal promotional calendar
"""
# Create problem
prob = LpProblem("Promotional_Optimization", LpMaximize)
# Decision variables: promote product i in week w
promo_vars = {}
for idx, row in products.iterrows():
for week in range(1, 53):
var_name = f"promo_{row['sku']}_{week}"
promo_vars[row['sku'], week] = LpVariable(
var_name,
cat='Binary'
)
# Objective: Maximize total incremental profit
prob += lpSum([
promo_vars[row['sku'], week] * row['incremental_profit']
for idx, row in products.iterrows()
for week in range(1, 53)
])
# Constraint 1: Total budget
prob += lpSum([
promo_vars[row['sku'], week] * row['trade_spend']
for idx, row in products.iterrows()
for week in range(1, 53)
]) <= budget
# Constraint 2: Minimum weeks between promotions per SKU
min_gap = constraints.get('min_weeks_between_promos', 8)
for idx, row in products.iterrows():
sku = row['sku']
for week in range(1, 53):
# If promoting in week w, can't promote in next min_gap weeks
prob += lpSum([
promo_vars[sku, w]
for w in range(week, min(week + min_gap, 53))
]) <= 1
# Constraint 3: Maximum promotions per week (capacity)
max_promos_per_week = constraints.get('max_promos_per_week', 10)
for week in range(1, 53):
prob += lpSum([
promo_vars[row['sku'], week]
for idx, row in products.iterrows()
]) <= max_promos_per_week
# Constraint 4: Minimum promotions per SKU per year
min_promos_per_sku = constraints.get('min_promos_per_sku', 2)
for idx, row in products.iterrows():
sku = row['sku']
prob += lpSum([
promo_vars[sku, week]
for week in range(1, 53)
]) >= min_promos_per_sku
# Solve
prob.solve(PULP_CBC_CMD(msg=0))
# Extract solution
calendar = []
for idx, row in products.iterrows():
for week in range(1, 53):
if promo_vars[row['sku'], week].varValue > 0.5:
calendar.append({
'sku': row['sku'],
'week': week,
'trade_spend': row['trade_spend'],
'incremental_profit': row['incremental_profit'],
'roi': row['incremental_profit'] / row['trade_spend']
})
calendar_df = pd.DataFrame(calendar)
results = {
'status': LpStatus[prob.status],
'total_profit': value(prob.objective),
'total_spend': calendar_df['trade_spend'].sum(),
'avg_roi': calendar_df['roi'].mean(),
'num_promotions': len(calendar_df),
'calendar': calendar_df.sort_values(['week', 'sku'])
}
return results
販促価格の最適化
def optimize_promotional_price(baseline_volume, regular_price, cogs,
elasticity, fixed_costs):
"""
Find optimal promotional price to maximize profit
Parameters:
- baseline_volume: baseline unit sales at regular price
- regular_price: regular shelf price
- cogs: cost of goods sold per unit
- elasticity: price elasticity coefficient
- fixed_costs: fixed promotional costs (feature, display, etc.)
Returns:
- optimal promotional price and expected profit
"""
def profit_function(promo_price):
"""Calculate profit at given promotional price"""
# Price change
price_change = (promo_price - regular_price) / regular_price
# Volume with price elasticity
volume = baseline_volume * (1 + elasticity * price_change)
# Revenue and costs
revenue = volume * promo_price
variable_costs = volume * cogs
total_costs = variable_costs + fixed_costs
# Profit
profit = revenue - total_costs
return -profit # Negative for minimization
from scipy.optimize import minimize_scalar
# Optimize price (bounded between 50% and 90% of regular price)
result = minimize_scalar(
profit_function,
bounds=(regular_price * 0.5, regular_price * 0.9),
method='bounded'
)
optimal_price = result.x
optimal_profit = -result.fun
# Calculate metrics at optimal price
price_change = (optimal_price - regular_price) / regular_price
optimal_volume = baseline_volume * (1 + elasticity * price_change)
discount_pct = (1 - optimal_price / regular_price) * 100
return {
'optimal_price': optimal_price,
'discount_pct': discount_pct,
'expected_volume': optimal_volume,
'expected_profit': optimal_profit,
'lift': (optimal_volume - baseline_volume) / baseline_volume * 100
}
# Example
result = optimize_promotional_price(
baseline_volume=1000,
regular_price=4.99,
cogs=2.50,
elasticity=-2.5,
fixed_costs=2000
)
print(f"Optimal Price: ${result['optimal_price']:.2f}")
print(f"Discount: {result['discount_pct']:.0f}%")
print(f"Expected Volume: {result['expected_volume']:.0f}")
print(f"Expected Profit: ${result['expected_profit']:.0f}")
販促に向けたサプライチェーン
販促需要予測
def forecast_promotional_demand(sku, retailer, promo_mechanics,
historical_promos):
"""
Forecast demand for upcoming promotion
Parameters:
- sku: product SKU
- retailer: retailer identifier
- promo_mechanics: dict with discount, feature, display
- historical_promos: DataFrame with past promotional performance
Returns:
- promotional forecast
"""
# Filter to similar promotions
similar_promos = historical_promos[
(historical_promos['sku'] == sku) &
(historical_promos['retailer'] == retailer)
]
if len(similar_promos) == 0:
# No history, use category average
similar_promos = historical_promos[
historical_promos['category'] == get_category(sku)
]
# Calculate average lift for similar mechanics
discount_range = promo_mechanics['discount_pct']
has_feature = promo_mechanics['feature']
has_display = promo_mechanics['display']
similar_mechanics = similar_promos[
(similar_promos['discount_pct'].between(discount_range - 5, discount_range + 5)) &
(similar_promos['feature'] == has_feature) &
(similar_promos['display'] == has_display)
]
if len(similar_mechanics) > 0:
avg_lift = similar_mechanics['lift'].mean()
std_lift = similar_mechanics['lift'].std()
else:
# Use regression model
avg_lift = predict_lift_from_model(promo_mechanics)
std_lift = 0.3 # Assume 30% std dev
# Get baseline forecast
baseline = get_baseline_forecast(sku, retailer)
# Promotional forecast
forecast = {
'baseline': baseline,
'expected_lift': avg_lift,
'promo_forecast': baseline * (1 + avg_lift),
'low_forecast': baseline * (1 + avg_lift - std_lift), # P10
'high_forecast': baseline * (1 + avg_lift + std_lift), # P90
'confidence': 'high' if len(similar_mechanics) >= 3 else 'low'
}
return forecast
販促用在庫計画
def plan_promotional_inventory(promo_forecast, lead_time_days,
safety_factor=1.2):
"""
Calculate promotional inventory requirements
Parameters:
- promo_forecast: dict from forecast_promotional_demand
- lead_time_days: production/procurement lead time
- safety_factor: buffer multiplier (1.2 = 20% safety stock)
Returns:
- inventory plan with build schedule
"""
# Expected promotional volume
expected_volume = promo_forecast['promo_forecast']
# Safety stock for uncertainty
forecast_uncertainty = (
promo_forecast['high_forecast'] - promo_forecast['low_forecast']
) / 2
safety_stock = forecast_uncertainty * safety_factor
# Total inventory needed
total_needed = expected_volume + safety_stock
# Build schedule (when to produce)
promo_start_date = promo_forecast.get('start_date')
build_start_date = promo_start_date - pd.Timedelta(days=lead_time_days + 7)
# Pre-build strategy
inventory_plan = {
'promotional_forecast': expected_volume,
'safety_stock': safety_stock,
'total_inventory_needed': total_needed,
'build_start_date': build_start_date,
'promo_start_date': promo_start_date,
'lead_time_days': lead_time_days,
'recommended_locations': determine_forward_stock_locations(
promo_forecast
)
}
return inventory_plan
def determine_forward_stock_locations(promo_forecast):
"""
Determine where to pre-position promotional inventory
"""
retailer = promo_forecast.get('retailer')
volume = promo_forecast['promo_forecast']
# Strategy based on volume
if volume > 10000: # Large promotion
return {
'strategy': 'forward_stock',
'locations': ['retailer_dc', 'manufacturer_dc'],
'split': {'retailer_dc': 0.7, 'manufacturer_dc': 0.3}
}
elif volume > 5000: # Medium promotion
return {
'strategy': 'partial_forward',
'locations': ['retailer_dc'],
'split': {'retailer_dc': 1.0}
}
else: # Small promotion
return {
'strategy': 'pull_from_stock',
'locations': ['manufacturer_dc'],
'split': {'manufacturer_dc': 1.0}
}
販促対応能力の計画
def assess_promotional_capacity(promotional_calendar, production_capacity,
dc_capacity):
"""
Assess if supply chain can support promotional calendar
Parameters:
- promotional_calendar: DataFrame with planned promotions by week
- production_capacity: available production capacity by week
- dc_capacity: DC storage capacity
Returns:
- capacity analysis and bottlenecks
"""
# Aggregate demand by week
weekly_demand = promotional_calendar.groupby('week').agg({
'forecasted_volume': 'sum',
'num_promos': 'count'
})
# Add production lead time offset
LEAD_TIME_WEEKS = 4
weekly_demand['production_week'] = weekly_demand.index - LEAD_TIME_WEEKS
# Check production capacity
capacity_issues = []
for week, row in weekly_demand.iterrows():
prod_week = row['production_week']
if prod_week < 1:
continue
required = row['forecasted_volume']
available = production_capacity.get(prod_week, 0)
if required > available:
capacity_issues.append({
'week': prod_week,
'type': 'production',
'required': required,
'available': available,
'gap': required - available,
'utilization': required / available * 100
})
# Check DC capacity
# Calculate cumulative inventory build-up
inventory_profile = calculate_inventory_profile(
promotional_calendar,
production_capacity
)
for week, inventory in inventory_profile.items():
if inventory > dc_capacity:
capacity_issues.append({
'week': week,
'type': 'storage',
'required': inventory,
'available': dc_capacity,
'gap': inventory - dc_capacity,
'utilization': inventory / dc_capacity * 100
})
return {
'capacity_ok': len(capacity_issues) == 0,
'issues': capacity_issues,
'max_production_utilization': max(
i['utilization'] for i in capacity_issues
if i['type'] == 'production'
) if capacity_issues else 0,
'recommendations': generate_capacity_recommendations(capacity_issues)
}
販促分析
販促実績ダッシュボード
def generate_promotional_scorecard(promotional_data):
"""
Generate comprehensive promotional performance scorecard
Parameters:
- promotional_data: DataFrame with promotional events and results
Returns:
- scorecard metrics
"""
scorecard = {}
# Overall metrics
scorecard['total_promotions'] = len(promotional_data)
scorecard['total_trade_spend'] = promotional_data['trade_spend'].sum()
scorecard['total_incremental_profit'] = promotional_data['incremental_profit'].sum()
# ROI metrics
scorecard['avg_roi'] = (
promotional_data['incremental_profit'].sum() /
promotional_data['trade_spend'].sum()
)
scorecard['roi_pct'] = scorecard['avg_roi'] * 100
positive_roi = promotional_data[promotional_data['incremental_profit'] > 0]
scorecard['pct_positive_roi'] = len(positive_roi) / len(promotional_data) * 100
# Lift metrics
scorecard['avg_lift'] = promotional_data['lift_pct'].mean()
scorecard['median_lift'] = promotional_data['lift_pct'].median()
# Effectiveness by mechanic
scorecard['mechanics'] = promotional_data.groupby('mechanic').agg({
'roi': 'mean',
'lift_pct': 'mean',
'trade_spend': 'sum',
'promo_id': 'count'
}).rename(columns={'promo_id': 'count'})
# Top performers
scorecard['top_10_promos'] = promotional_data.nlargest(10, 'incremental_profit')[
['sku', 'retailer', 'mechanic', 'incremental_profit', 'roi']
]
# Bottom performers
scorecard['bottom_10_promos'] = promotional_data.nsmallest(10, 'incremental_profit')[
['sku', 'retailer', 'mechanic', 'incremental_profit', 'roi']
]
# Efficiency metrics
scorecard['spend_per_incremental_unit'] = (
promotional_data['trade_spend'].sum() /
promotional_data['incremental_units'].sum()
)
return scorecard
ツールとライブラリ
商用ソフトウェア
トレードプロモーション管理(TPM):
- SAP TPM:SAP ERP と統合
- Oracle TPM:トレードプロモーションの管理と最適化
- AFS TPM:Analytical Frameworks Solutions
- Wipro Promax:クラウドベースの TPM
- Blacksmith Applications:TPM/TPO ソフトウェア
- IRI PromoPlanner:分析主導のプランニング
トレードプロモーション最適化(TPO):
- Anaplan:プロモーション最適化機能を備えたクラウドプランニング
- o9 Solutions:AI 主導のプロモーションプランニング
- Blue Yonder:価格とプロモーションの最適化
- Revionics:価格とプロモーションの最適化(Oracle)
- TABS Analytics:プロモーションの予測とプランニング
分析プラットフォーム:
- Nielsen:シンジケート販売データとプロモーション分析
- IRI:市場測定とプロモーションに関するインサイト
- Numerator:消費者パネルとプロモーションの追跡
- 84.51° (Kroger):小売データとプロモーション分析
Python ライブラリ
# Core libraries for promotional analysis
# Data manipulation and analysis
import pandas as pd
import numpy as np
# Statistical modeling
import statsmodels.api as sm
from statsmodels.tsa.seasonal import seasonal_decompose
from scipy import stats
from scipy.optimize import minimize
# Machine learning
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
from sklearn.linear_model import LinearRegression, Ridge, Lasso
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import cross_val_score
# Optimization
from pulp import *
import pyomo.environ as pyo
# Visualization
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
import plotly.graph_objects as go
# Time series
from prophet import Prophet
import pmdarima as pm
プロモーション分析テンプレート
class PromotionalAnalyzer:
"""
Comprehensive promotional analysis framework
"""
def __init__(self, sales_data, promotional_events):
self.sales_data = sales_data
self.promo_events = promotional_events
self.baseline_model = None
self.lift_model = None
def calculate_baseline(self, method='moving_average', window=4):
"""Calculate baseline sales (non-promotional)"""
if method == 'moving_average':
# Use non-promotional weeks only
non_promo = self.sales_data[self.sales_data['promo'] == 0]
baseline = non_promo.rolling(window=window)['sales'].mean()
elif method == 'regression':
# Time series regression
X = np.arange(len(self.sales_data)).reshape(-1, 1)
y = self.sales_data[self.sales_data['promo'] == 0]['sales']
model = LinearRegression().fit(X, y)
baseline = model.predict(X)
return baseline
def measure_lift(self):
"""Measure promotional lift for each event"""
results = []
for idx, promo in self.promo_events.iterrows():
# Get sales during promotion
promo_sales = self.sales_data[
(self.sales_data['week'] >= promo['start_week']) &
(self.sales_data['week'] <= promo['end_week'])
]['sales'].sum()
# Get baseline
baseline = self.sales_data[
(self.sales_data['week'] >= promo['start_week']) &
(self.sales_data['week'] <= promo['end_week'])
]['baseline'].sum()
lift = (promo_sales - baseline) / baseline
results.append({
'promo_id': promo['promo_id'],
'baseline': baseline,
'promo_sales': promo_sales,
'incremental': promo_sales - baseline,
'lift_pct': lift * 100
})
return pd.DataFrame(results)
def build_lift_model(self):
"""Build predictive model for promotional lift"""
# Feature engineering
features = self.promo_events[[
'discount_pct', 'feature', 'display', 'price'
]]
# Target
lift_results = self.measure_lift()
target = lift_results['lift_pct']
# Train model
model = RandomForestRegressor(n_estimators=100, random_state=42)
model.fit(features, target)
self.lift_model = model
return model
def forecast_promotion(self, promo_mechanics):
"""Forecast results for planned promotion"""
if self.lift_model is None:
self.build_lift_model()
# Predict lift
features = pd.DataFrame([promo_mechanics])
predicted_lift = self.lift_model.predict(features)[0]
# Apply to baseline
baseline = self.calculate_baseline()
forecast = baseline * (1 + predicted_lift / 100)
return {
'predicted_lift_pct': predicted_lift,
'baseline_forecast': baseline,
'promotional_forecast': forecast
}
よくある課題と解決策
課題:プロモーション予測の精度が低い
問題:
- プロモーションの実績に大きなばらつきがある
- 予測が 50~100%ずれる
- 品切れ、または過剰在庫が発生する
解決策:
- プロモーションデータベースを構築する(過去のすべてのイベント)
- プロモーションの仕組み(TPR、TPR+F、TPR+F+D)ごとに分類する
- 新商品には類似商品を使用する
- 直近のデータでモデルを調整する
- 不確実性の高いイベントには安全在庫を追加する
- 需要感知を導入する(初期結果に基づいて予測を更新する)
def adaptive_promotional_forecast(initial_forecast, actual_sales_to_date,
days_into_promo, total_promo_days):
"""
Update promotional forecast based on early performance
"""
# Typical promotional curve (front-loaded)
expected_pct_complete = {
1: 0.30, # Day 1: 30% of sales
2: 0.50, # Day 2: 50% cumulative
3: 0.65,
4: 0.75,
5: 0.83,
6: 0.90,
7: 1.00
}
expected_complete = expected_pct_complete.get(days_into_promo, days_into_promo / total_promo_days)
expected_sales = initial_forecast * expected_complete
# Actual vs expected
performance_ratio = actual_sales_to_date / expected_sales
# Update forecast
updated_forecast = initial_forecast * performance_ratio
return {
'initial_forecast': initial_forecast,
'expected_to_date': expected_sales,
'actual_to_date': actual_sales_to_date,
'performance_ratio': performance_ratio,
'updated_forecast': updated_forecast
}
課題:ROI の低い、またはマイナスのプロモーション
問題:
- 多くのプロモーションで利益が出ない
- トレード支出が大きく、純増効果が小さい
- いずれにせよ購入するロイヤル顧客に値引きを提供している
解決策:
- プロモーションを事前テストする(小規模な市場、テスト店舗)
- プロモーションの値引き幅を最適化する(価格テスト)
- プロモーションの対象を絞る(ロイヤルティプログラム、クーポン、TPR の比較)
- プロモーションの実施品質を高める(ディスプレイが設置されていることを確認する)
- 慢性的に成果の悪い施策をやめる(下位 20%のプロモーションを打ち切る)
- 既存顧客より新規顧客の獲得を重視する
課題:プロモーションの競合と共食い
問題:
- 同じ週に複数の商品がプロモーション対象となる
- ポートフォリオ内で共食いが発生する
- 小売業者がカレンダーに難色を示す
解決策:
- ポートフォリオ全体でプロモーションカレンダーを調整する
- 商品ラインごとにプロモーション時期をずらす
- 共食いの影響をモデル化する
- 個別の SKU ではなく、ポートフォリオ全体で最適化する
- 小売業者と早い段階でプロモーションカレンダーを交渉する
課題:サプライチェーンの実行
問題:
- プロモーション中に品切れが発生する
- プロモーション用の在庫が届くのが遅れる
- DC の処理能力を超える
解決策:
- プロモーション用の生産計画ではリードタイムを延長する(6~8 週間)
- イベントの 4 週間前にプロモーション予測を確定する
- 在庫を事前に配置する(小売業者の DC に前倒しで在庫を送る)
- プロモーション用の生産能力を確保する
- ネットワークにプロモーション需要の急増に対応できる能力を組み込む
- 大規模なイベントには、二次サプライヤーや委託製造業者を利用する
課題:プロモーション後の販売低迷(買いだめ)
問題:
- プロモーション後に売上が急落する
- 消費者が買いだめしている
- プロモーションが失敗したように見える
解決策:
- 真の純増効果を測定する(プロモーション後の期間も含める)
- プロモーション期間を短くする(最長 1~2 週間)
- 購入数に上限を設ける
- 値引きしすぎない
- 買いだめではなく、試用や新規顧客の獲得を重視する
出力形式
販促プランのテンプレート
販促概要:
- 製品:Brand X シリアル 18oz
- 小売業者:大手食品スーパー
- 販促期間:第15~16週(4月10~23日)
- 販促施策:一時的な値下げ+広告掲載+店頭ディスプレイ
- 通常価格:$4.99
- 販促価格:$3.49(30%オフ)
予測実績:
| 指標 | ベースライン | 販促時 | 増分 | 上昇率 |
|---|---|---|---|---|
| 販売数量 | 1,000 | 3,200 | 2,200 | 220% |
| 売上高 | $4,990 | $11,168 | $6,178 | 124% |
| 粗利益 | $2,490 | $3,168 | $678 | 27% |
販促費:
| 項目 | 費用 |
|---|---|
| 請求書控除割引 | $1,500 |
| 広告掲載費 | $1,200 |
| ディスプレイ設置費 | $800 |
| 共同広告費 | $500 |
| 販促費合計 | $4,000 |
ROI 分析:
- 増分粗利益:$678
- 販促費:$4,000
- 純利益:-$3,322
- ROI:-83%
推奨:実施しないでください
- 販促による損失が見込まれます
- 検討事項:販促費の削減、施策の改善、または中止
サプライチェーン計画:
- 生産計画:第11週(販促の4週間前)
- 数量:3,500個(予測数量+安全在庫10%)
- 小売業者のDCへの出荷:第13週
- 店頭設置:第14週
確認すべき質問
追加の情報が必要な場合:
- どの製品/カテゴリーを販促しますか?
- 対象の小売業者とチャネルはどこですか?
- どのような販促施策を実施しますか?(割引率、広告掲載、ディスプレイ)
- 販促予算と目的は何ですか?
- 過去の販促データはありますか?
- 通常の販促ROIと上昇率はどの程度ですか?
- 生産リードタイムと生産能力の制約はありますか?
- 販促カレンダーはどのくらい前に確定しますか?
関連スキル
- demand-forecasting:ベースライン需要と販促需要の予測
- markdown-optimization:価格設定と在庫処分の最適化
- retail-replenishment:販促在庫の調整
- dsd-route-optimization:DSD 販促の実行
- capacity-planning:販促に向けた生産能力の計画
- inventory-optimization:販促用安全在庫の最適化
- sales-operations-planning:S&OP との連携
- supply-chain-analytics:販促実績の追跡
- seasonal-planning:祝祭日および季節ごとの販促
コメント
Markdown 対応