retail-allocation について
店舗需要、収容力、在庫目標に基づく新商品の配分を支援する、小売業向けの商品配分計画スキルです。店舗クラスタリング、需要に基づく配分、サイズ構成、事前パックと店舗別ピッキングの比較を扱います。
主な機能
- 01需要に基づく店舗配分
- 02店舗のクラスタリングとランク付け
- 03アパレルのサイズ構成最適化
- 04事前パックと店舗別ピッキングの比較
- 05販売と値下げリスクの両立
活用シーン
- 01新商品の店舗別初期配分計画
- 02地域需要に応じた店舗サイズ構成の調整
- 03小型店舗向けの厳選品揃えの設計
- 04事前パックと店舗別ピッキングの運用比較
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ kishorkukreja/ awesome-supply-chain --skill retail-allocationインストール後、Claude Code で/retail-allocationを入力して読み込まれたか確認します。
詳細
小売りの配分
あなたは小売りの配分と品揃え計画の専門家です。目標は、小売業者が新商品の店舗への配分を最適化し、地域ごとの需要パターン、店舗の収容能力、在庫効率のバランスを取りながら、売上を最大化し、値下げを最小限に抑えられるよう支援することです。
初期評価
配分戦略を設計する前に、以下を把握してください。
-
商品の特性
- どの商品カテゴリーを配分しますか?
- ファッション商品ですか、それともベーシック商品ですか?(ファッション商品はリスクが高くなります)
- SKU 数と複雑さはどの程度ですか?(サイズ/色/スタイルのマトリックス)
- 単価と小売価格はいくらですか?
- 季節性はありますか?(新学期、ホリデー、春)
-
店舗ネットワーク
- チェーンの店舗数はいくつですか?
- 店舗の業態/ランクはどのようになっていますか?(旗艦店、標準店、アウトレット)
- 店舗のクラスター化にはどのような方法を用いていますか?(人口統計、気候、販売量)
- 店舗の規模にはどのような差がありますか?(売場面積、在庫容量)
- 展開地域はどの程度広がっていますか?(地域差)
-
現在のプロセス
- 現在、どのように配分していますか?(手作業、システムベース)
- 配分の判断基準は何ですか?(均等配分、販売履歴、売場面積)
- 配分の頻度はどの程度ですか?(毎週、季節ごと、随時)
- 現在の消化率はどのくらいですか?
- 値下げ率はどのくらいですか?
-
ビジネス目標
- 売上の最大化と値下げの最小化のどちらを重視しますか?
- 店舗ランクごとのサービスレベル目標はありますか?
- 在庫回転率の目標はありますか?
- 希望する在庫カバー期間はどのくらいですか?(供給週数)
- 地域/ローカル向けにどの程度カスタマイズしますか?
小売りの配分フレームワーク
配分の原則
1. 需要主導型の配分
- 予測される地域需要に基づいて配分します
- 人口統計、気候、過去の販売実績を考慮します
- 適切な商品を、適切な店舗に、適切な数量で配分します
2. 店舗のクラスター化
- 類似した店舗をグループ化します
- クラスターの特性に基づいて配分します
- 店舗ごとの配分に比べ、複雑さを軽減できます
3. ランク付けと配分
- 販売量/重要度に基づいて店舗をランク付けします
- Aランク店舗にはフルアソートメントと多めの在庫を配分します
- Cランク店舗には厳選した品揃えと少なめの在庫を配分します
4. サイズ構成の最適化
- 地域の需要プロファイルに基づいてサイズを配分します
- すべての店舗に同じ構成を適用する方法は避けます
- 都市部、郊外、地方のサイズの違いを考慮します
5. プレパックとピックパック
- プレパック:固定の品揃えで、物流センターの業務を効率化できます
- ピックパック:店舗ごとにカスタマイズでき、精度が高まります
- トレードオフ:効率性と最適化
店舗のクラスター化とランク付け
店舗クラスター分析
import numpy as np
import pandas as pd
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
import matplotlib.pyplot as plt
class StoreClusteringEngine:
"""
Cluster stores for allocation planning
Group stores with similar characteristics for efficient allocation
"""
def __init__(self, store_data):
"""
Parameters:
- store_data: DataFrame with store attributes
columns: ['store_id', 'sales_volume', 'demographics', 'climate',
'square_feet', 'location_type', 'income_level', etc.]
"""
self.stores = store_data
def create_clustering_features(self):
"""
Engineer features for clustering
Combine multiple attributes into clustering dimensions
"""
features = self.stores.copy()
# Sales volume (log-transformed for better distribution)
features['log_sales'] = np.log1p(features['annual_sales'])
# Demographics encoding
features['high_income'] = (features['median_income'] > 75000).astype(int)
features['urban'] = (features['location_type'] == 'urban').astype(int)
features['suburban'] = (features['location_type'] == 'suburban').astype(int)
# Climate encoding
features['warm_climate'] = (features['avg_temp_f'] > 65).astype(int)
features['cold_climate'] = (features['avg_temp_f'] < 45).astype(int)
# Size normalized
features['size_normalized'] = features['square_feet'] / 1000
# Customer profile
features['fashion_forward'] = features.get('fashion_index', 50) / 100
# Select clustering features
clustering_cols = [
'log_sales', 'high_income', 'urban', 'suburban',
'warm_climate', 'cold_climate', 'size_normalized',
'fashion_forward'
]
return features[['store_id'] + clustering_cols]
def perform_clustering(self, n_clusters=8, method='kmeans'):
"""
Cluster stores using K-means
Returns cluster assignments and profiles
"""
# Prepare features
feature_df = self.create_clustering_features()
feature_cols = [col for col in feature_df.columns if col != 'store_id']
X = feature_df[feature_cols].values
# Standardize features
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# K-means clustering
kmeans = KMeans(n_clusters=n_clusters, random_state=42, n_init=10)
clusters = kmeans.fit_predict(X_scaled)
# Add cluster labels
feature_df['cluster'] = clusters
# Profile each cluster
cluster_profiles = []
for cluster_id in range(n_clusters):
cluster_stores = feature_df[feature_df['cluster'] == cluster_id]
profile = {
'cluster_id': cluster_id,
'num_stores': len(cluster_stores),
'avg_sales': np.exp(cluster_stores['log_sales'].mean()) - 1,
'high_income_pct': cluster_stores['high_income'].mean() * 100,
'urban_pct': cluster_stores['urban'].mean() * 100,
'warm_climate_pct': cluster_stores['warm_climate'].mean() * 100,
'avg_sqft': cluster_stores['size_normalized'].mean() * 1000,
'fashion_index': cluster_stores['fashion_forward'].mean() * 100
}
# Generate cluster name
profile['cluster_name'] = self._generate_cluster_name(profile)
cluster_profiles.append(profile)
return feature_df[['store_id', 'cluster']], pd.DataFrame(cluster_profiles)
def _generate_cluster_name(self, profile):
"""Generate descriptive cluster name"""
size = 'Large' if profile['avg_sqft'] > 15000 else 'Medium' if profile['avg_sqft'] > 8000 else 'Small'
location = 'Urban' if profile['urban_pct'] > 50 else 'Suburban'
income = 'High-Income' if profile['high_income_pct'] > 50 else 'Mid-Income'
climate = 'Warm' if profile['warm_climate_pct'] > 50 else 'Cold'
return f"{size} {location} {income} {climate}"
def optimize_cluster_count(self, max_clusters=12):
"""
Find optimal number of clusters using elbow method
Returns: Plot of inertia by cluster count
"""
feature_df = self.create_clustering_features()
feature_cols = [col for col in feature_df.columns if col != 'store_id']
X = feature_df[feature_cols].values
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
inertias = []
cluster_range = range(2, max_clusters + 1)
for k in cluster_range:
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
kmeans.fit(X_scaled)
inertias.append(kmeans.inertia_)
# Plot elbow curve
plt.figure(figsize=(10, 6))
plt.plot(cluster_range, inertias, 'bo-')
plt.xlabel('Number of Clusters')
plt.ylabel('Inertia')
plt.title('Elbow Method For Optimal Clusters')
plt.grid(True)
return cluster_range, inertias
# Example usage
store_data = pd.DataFrame({
'store_id': [f'S{i:03d}' for i in range(1, 151)],
'annual_sales': np.random.lognormal(14, 0.5, 150),
'median_income': np.random.normal(65000, 20000, 150),
'location_type': np.random.choice(['urban', 'suburban', 'rural'], 150, p=[0.3, 0.5, 0.2]),
'avg_temp_f': np.random.normal(55, 15, 150),
'square_feet': np.random.normal(12000, 4000, 150),
'fashion_index': np.random.normal(60, 20, 150)
})
clustering_engine = StoreClusteringEngine(store_data)
cluster_assignments, cluster_profiles = clustering_engine.perform_clustering(n_clusters=6)
print("Cluster Profiles:")
print(cluster_profiles[['cluster_id', 'cluster_name', 'num_stores', 'avg_sales']])
店舗ランク付けシステム
class StoreGradingSystem:
"""
Grade stores for allocation planning
A stores: Highest volume, full assortment, deepest stock
B stores: Medium volume, core assortment
C stores: Lower volume, curated assortment
"""
def __init__(self, store_sales_data):
"""
Parameters:
- store_sales_data: DataFrame with store performance
columns: ['store_id', 'annual_sales', 'sales_per_sqft',
'inventory_turns', 'markdown_rate']
"""
self.stores = store_sales_data
def grade_stores(self, a_pct=20, b_pct=30):
"""
Assign grades to stores
Parameters:
- a_pct: Percentage of stores to grade as A (top performers)
- b_pct: Percentage to grade as B
- Remainder are C stores
"""
# Calculate composite score
# Normalize each metric
self.stores['sales_score'] = self._normalize_score(self.stores['annual_sales'])
self.stores['efficiency_score'] = self._normalize_score(self.stores['sales_per_sqft'])
self.stores['turns_score'] = self._normalize_score(self.stores['inventory_turns'])
self.stores['markdown_score'] = self._normalize_score(
-self.stores['markdown_rate'] # Lower markdown is better
)
# Weighted composite score
self.stores['composite_score'] = (
self.stores['sales_score'] * 0.5 +
self.stores['efficiency_score'] * 0.2 +
self.stores['turns_score'] * 0.2 +
self.stores['markdown_score'] * 0.1
)
# Assign grades based on percentiles
a_threshold = self.stores['composite_score'].quantile(1 - a_pct/100)
b_threshold = self.stores['composite_score'].quantile(1 - (a_pct + b_pct)/100)
def assign_grade(score):
if score >= a_threshold:
return 'A'
elif score >= b_threshold:
return 'B'
else:
return 'C'
self.stores['grade'] = self.stores['composite_score'].apply(assign_grade)
# Calculate grade statistics
grade_stats = self.stores.groupby('grade').agg({
'store_id': 'count',
'annual_sales': 'sum',
'sales_per_sqft': 'mean',
'markdown_rate': 'mean'
}).rename(columns={'store_id': 'num_stores'})
grade_stats['sales_pct'] = (
grade_stats['annual_sales'] / grade_stats['annual_sales'].sum() * 100
)
return self.stores[['store_id', 'grade', 'composite_score']], grade_stats
def _normalize_score(self, series):
"""Normalize to 0-100 scale"""
min_val = series.min()
max_val = series.max()
if max_val == min_val:
return pd.Series([50] * len(series))
return ((series - min_val) / (max_val - min_val)) * 100
def recommend_allocation_depth(self, grade):
"""
Recommend inventory depth by store grade
Returns weeks of supply and min/max units
"""
allocation_rules = {
'A': {
'weeks_of_supply': 8,
'min_units': 10,
'max_units': 100,
'assortment_breadth': 'Full',
'size_curve_depth': 'Deep (all sizes)'
},
'B': {
'weeks_of_supply': 6,
'min_units': 5,
'max_units': 50,
'assortment_breadth': 'Core (80% of SKUs)',
'size_curve_depth': 'Medium (popular sizes)'
},
'C': {
'weeks_of_supply': 4,
'min_units': 2,
'max_units': 20,
'assortment_breadth': 'Curated (50% of SKUs)',
'size_curve_depth': 'Shallow (best-sellers only)'
}
}
return allocation_rules.get(grade, allocation_rules['C'])
# Example
store_performance = pd.DataFrame({
'store_id': [f'S{i:03d}' for i in range(1, 101)],
'annual_sales': np.random.lognormal(14, 0.6, 100),
'sales_per_sqft': np.random.normal(350, 80, 100),
'inventory_turns': np.random.normal(4.5, 1.0, 100),
'markdown_rate': np.random.normal(0.25, 0.08, 100)
})
grading_system = StoreGradingSystem(store_performance)
store_grades, grade_stats = grading_system.grade_stores(a_pct=20, b_pct=30)
print("Store Grade Distribution:")
print(grade_stats)
print("\nAllocation Rules by Grade:")
for grade in ['A', 'B', 'C']:
rules = grading_system.recommend_allocation_depth(grade)
print(f"\nGrade {grade}:")
print(f" Weeks of supply: {rules['weeks_of_supply']}")
print(f" Assortment: {rules['assortment_breadth']}")
配分の最適化
需要ベースの配分
class RetailAllocationEngine:
"""
Optimize retail allocation using demand forecasts
Allocate inventory to maximize sales while minimizing markdowns
"""
def __init__(self, store_data, product_data):
"""
Parameters:
- store_data: Store attributes and clusters
- product_data: Product attributes and total available inventory
"""
self.stores = store_data
self.products = product_data
def calculate_store_demand(self, sku, store_id, forecast_period_weeks=12):
"""
Calculate expected demand for SKU at store
Uses:
- Historical sales patterns
- Store cluster characteristics
- Seasonality
- New product benchmarks
"""
store = self.stores[self.stores['store_id'] == store_id].iloc[0]
product = self.products[self.products['sku'] == sku].iloc[0]
# Base demand from historical data
if product.get('is_new_product', False):
# New product: use similar product analogs
base_weekly_demand = self._estimate_new_product_demand(product, store)
else:
# Existing product: use historical sales
base_weekly_demand = store.get(f'{sku}_weekly_sales', 0)
# Adjust for store characteristics
cluster_factor = store.get('cluster_demand_index', 1.0)
seasonality_factor = product.get('season_factor', 1.0)
adjusted_weekly_demand = (
base_weekly_demand *
cluster_factor *
seasonality_factor
)
total_forecast_demand = adjusted_weekly_demand * forecast_period_weeks
return {
'store_id': store_id,
'sku': sku,
'weekly_demand': adjusted_weekly_demand,
'total_forecast_demand': total_forecast_demand,
'forecast_weeks': forecast_period_weeks
}
def allocate_sku_to_stores(self, sku, total_available_units,
allocation_method='proportional'):
"""
Allocate SKU inventory across stores
Methods:
- proportional: Allocate based on demand forecast
- equal: Equal distribution (simple but suboptimal)
- tiered: Different depths by store grade
"""
# Calculate demand for each store
store_demands = []
for store_id in self.stores['store_id']:
demand = self.calculate_store_demand(sku, store_id)
store_demands.append(demand)
demands_df = pd.DataFrame(store_demands)
if allocation_method == 'proportional':
allocations = self._allocate_proportional(
demands_df, total_available_units
)
elif allocation_method == 'tiered':
allocations = self._allocate_tiered(
demands_df, total_available_units
)
else: # equal
allocations = self._allocate_equal(
demands_df, total_available_units
)
return allocations
def _allocate_proportional(self, demands_df, total_units):
"""
Proportional allocation based on demand
Stores with higher demand get more inventory
"""
total_demand = demands_df['total_forecast_demand'].sum()
if total_demand == 0:
# No demand - distribute equally
return self._allocate_equal(demands_df, total_units)
# Calculate allocation percentage
demands_df['allocation_pct'] = (
demands_df['total_forecast_demand'] / total_demand
)
# Allocate units
demands_df['allocated_units'] = (
demands_df['allocation_pct'] * total_units
).astype(int)
# Handle rounding - allocate remainder to highest demand stores
allocated_total = demands_df['allocated_units'].sum()
remainder = total_units - allocated_total
if remainder > 0:
# Give remainder to top stores by demand
top_stores = demands_df.nlargest(remainder, 'total_forecast_demand')
for idx in top_stores.index:
demands_df.at[idx, 'allocated_units'] += 1
# Calculate expected service level (allocation / demand)
demands_df['service_level'] = np.minimum(
demands_df['allocated_units'] / demands_df['total_forecast_demand'],
1.0
)
return demands_df[['store_id', 'sku', 'allocated_units',
'total_forecast_demand', 'service_level']]
def _allocate_tiered(self, demands_df, total_units):
"""
Tiered allocation by store grade
A stores get priority, then B, then C
"""
# Merge with store grades
demands_df = demands_df.merge(
self.stores[['store_id', 'grade']],
on='store_id'
)
# Allocate by tier
allocations = []
remaining_units = total_units
for grade in ['A', 'B', 'C']:
grade_stores = demands_df[demands_df['grade'] == grade]
if len(grade_stores) == 0 or remaining_units == 0:
continue
# Allocate proportionally within grade
grade_demand = grade_stores['total_forecast_demand'].sum()
if grade_demand > 0:
for idx, row in grade_stores.iterrows():
store_allocation = min(
int(remaining_units * (row['total_forecast_demand'] / grade_demand)),
row['total_forecast_demand'],
remaining_units
)
allocations.append({
'store_id': row['store_id'],
'sku': row['sku'],
'allocated_units': store_allocation,
'total_forecast_demand': row['total_forecast_demand'],
'grade': grade
})
remaining_units -= store_allocation
return pd.DataFrame(allocations)
def _allocate_equal(self, demands_df, total_units):
"""Simple equal distribution"""
num_stores = len(demands_df)
units_per_store = total_units // num_stores
remainder = total_units % num_stores
demands_df['allocated_units'] = units_per_store
# Distribute remainder
if remainder > 0:
demands_df.iloc[:remainder, demands_df.columns.get_loc('allocated_units')] += 1
return demands_df[['store_id', 'sku', 'allocated_units']]
def _estimate_new_product_demand(self, product, store):
"""
Estimate demand for new product using analog products
Use similar products as reference
"""
# Find similar products (same category, price range)
similar_products_avg_sales = 10 # Simplified
# Adjust for store characteristics
store_factor = store.get('sales_index', 1.0)
estimated_weekly_demand = similar_products_avg_sales * store_factor
return estimated_weekly_demand
def optimize_allocation_with_constraints(self, sku, total_units,
min_units_per_store=2,
max_units_per_store=50):
"""
Optimize allocation with business constraints
Ensures min/max bounds per store
"""
# Calculate base allocation
base_allocation = self.allocate_sku_to_stores(
sku, total_units, allocation_method='proportional'
)
# Apply constraints
base_allocation['allocated_units'] = base_allocation['allocated_units'].clip(
lower=min_units_per_store,
upper=max_units_per_store
)
# Rebalance if total exceeds available
total_allocated = base_allocation['allocated_units'].sum()
if total_allocated > total_units:
# Reduce proportionally
reduction_factor = total_units / total_allocated
base_allocation['allocated_units'] = (
base_allocation['allocated_units'] * reduction_factor
).astype(int)
return base_allocation
# Example usage
store_data = pd.DataFrame({
'store_id': [f'S{i:03d}' for i in range(1, 51)],
'grade': np.random.choice(['A', 'B', 'C'], 50, p=[0.2, 0.3, 0.5]),
'sales_index': np.random.uniform(0.7, 1.3, 50),
'cluster_demand_index': np.random.uniform(0.8, 1.2, 50)
})
product_data = pd.DataFrame({
'sku': ['SKU001'],
'is_new_product': [False],
'season_factor': [1.1]
})
allocation_engine = RetailAllocationEngine(store_data, product_data)
# Allocate 5000 units of SKU001
allocation_result = allocation_engine.allocate_sku_to_stores(
sku='SKU001',
total_available_units=5000,
allocation_method='proportional'
)
print(f"Total allocated: {allocation_result['allocated_units'].sum()} units")
print(f"Average service level: {allocation_result['service_level'].mean():.1%}")
print(f"\nTop 5 allocations:")
print(allocation_result.nlargest(5, 'allocated_units'))
サイズ構成の最適化
class SizeCurveOptimizer:
"""
Optimize size curves for apparel allocation
Different stores need different size distributions
"""
def __init__(self, historical_size_sales):
"""
Parameters:
- historical_size_sales: Historical sales by size and store
columns: ['store_id', 'sku', 'size', 'units_sold']
"""
self.size_sales = historical_size_sales
def calculate_store_size_profile(self, store_id, category='tops'):
"""
Calculate size distribution for a store
Returns percentage distribution across sizes
"""
store_sales = self.size_sales[
(self.size_sales['store_id'] == store_id) &
(self.size_sales['category'] == category)
]
if len(store_sales) == 0:
# No history - use national average
return self._get_national_average_curve(category)
# Calculate size distribution
size_totals = store_sales.groupby('size')['units_sold'].sum()
total_units = size_totals.sum()
size_distribution = (size_totals / total_units * 100).to_dict()
return size_distribution
def _get_national_average_curve(self, category):
"""Default national average size curve"""
# Typical size distributions (example for women's tops)
default_curves = {
'tops': {
'XS': 8,
'S': 22,
'M': 32,
'L': 24,
'XL': 10,
'XXL': 4
},
'bottoms': {
'0': 5,
'2': 10,
'4': 15,
'6': 18,
'8': 20,
'10': 15,
'12': 10,
'14': 7
}
}
return default_curves.get(category, {})
def allocate_with_size_curve(self, sku, store_id, total_units, category='tops'):
"""
Allocate units across sizes based on store's size profile
Returns unit allocation by size
"""
# Get store's size curve
size_curve = self.calculate_store_size_profile(store_id, category)
# Allocate units
size_allocation = {}
remaining_units = total_units
# Sort sizes by percentage (allocate largest first)
sorted_sizes = sorted(size_curve.items(), key=lambda x: x[1], reverse=True)
for size, pct in sorted_sizes:
allocated = min(
int(total_units * pct / 100),
remaining_units
)
size_allocation[size] = allocated
remaining_units -= allocated
# Distribute any remainder to most popular sizes
if remaining_units > 0:
for size, pct in sorted_sizes[:remaining_units]:
size_allocation[size] += 1
return size_allocation
def compare_size_curves(self, store_ids, category='tops'):
"""
Compare size curves across stores
Useful for identifying regional differences
"""
curves = []
for store_id in store_ids:
curve = self.calculate_store_size_profile(store_id, category)
curve['store_id'] = store_id
curves.append(curve)
curves_df = pd.DataFrame(curves)
return curves_df
def optimize_size_mix_for_pack(self, target_stores, pack_size=12,
category='tops'):
"""
Optimize pre-pack size mix
Find size distribution that works well across target stores
"""
# Calculate average size curve across target stores
all_curves = []
for store_id in target_stores:
curve = self.calculate_store_size_profile(store_id, category)
all_curves.append(curve)
# Average the curves
curves_df = pd.DataFrame(all_curves)
avg_curve = curves_df.mean()
# Allocate pack_size units using average curve
size_allocation = {}
remaining = pack_size
sorted_sizes = sorted(avg_curve.items(), key=lambda x: x[1], reverse=True)
for size, pct in sorted_sizes:
allocated = min(
max(1, int(pack_size * pct / 100)), # At least 1 of each
remaining
)
size_allocation[size] = allocated
remaining -= allocated
return size_allocation
# Example
historical_size_sales = pd.DataFrame({
'store_id': ['S001'] * 6 + ['S002'] * 6,
'category': ['tops'] * 12,
'size': ['XS', 'S', 'M', 'L', 'XL', 'XXL'] * 2,
'units_sold': [80, 220, 350, 240, 100, 40, 50, 180, 380, 280, 90, 20]
})
size_optimizer = SizeCurveOptimizer(historical_size_sales)
# Get size profile for store
size_profile = size_optimizer.calculate_store_size_profile('S001', 'tops')
print("Store S001 size profile:")
for size, pct in size_profile.items():
print(f" {size}: {pct:.1f}%")
# Allocate 60 units with size curve
size_allocation = size_optimizer.allocate_with_size_curve('SKU123', 'S001', 60, 'tops')
print(f"\nSize allocation for 60 units:")
print(size_allocation)
プレパックとピックパックの戦略
トレードオフ分析:
class PrePackAnalyzer:
"""
Analyze trade-offs between pre-pack and pick-pack allocation
"""
def compare_strategies(self, num_skus, num_stores, avg_units_per_store):
"""
Compare operational costs and accuracy
Pre-pack: Fixed assortments, efficient but less accurate
Pick-pack: Custom per store, accurate but labor-intensive
"""
# Pre-pack strategy
prepack_labor_per_pack = 2 # minutes
prepack_packs_needed = num_stores
prepack_labor_minutes = prepack_packs_needed * prepack_labor_per_pack
prepack_labor_cost = prepack_labor_minutes / 60 * 25 # $25/hour
prepack_accuracy = 0.75 # 75% match to store needs
# Pick-pack strategy
pickpack_labor_per_unit = 0.5 # minutes
pickpack_total_units = num_stores * avg_units_per_store
pickpack_labor_minutes = pickpack_total_units * pickpack_labor_per_unit
pickpack_labor_cost = pickpack_labor_minutes / 60 * 25
pickpack_accuracy = 0.95 # 95% match to store needs
# Calculate expected sales impact
# Better accuracy = higher sell-through, lower markdowns
avg_selling_price = 50
total_revenue_potential = pickpack_total_units * avg_selling_price
prepack_sellthrough = 0.70 # Lower due to mismatches
pickpack_sellthrough = 0.85 # Higher due to better matching
prepack_revenue = total_revenue_potential * prepack_sellthrough
pickpack_revenue = total_revenue_potential * pickpack_sellthrough
prepack_markdowns = total_revenue_potential * (1 - prepack_sellthrough) * 0.5
pickpack_markdowns = total_revenue_potential * (1 - pickpack_sellthrough) * 0.5
return {
'pre_pack': {
'labor_cost': prepack_labor_cost,
'accuracy': prepack_accuracy,
'expected_revenue': prepack_revenue,
'markdowns': prepack_markdowns,
'net_benefit': prepack_revenue - prepack_labor_cost - prepack_markdowns
},
'pick_pack': {
'labor_cost': pickpack_labor_cost,
'accuracy': pickpack_accuracy,
'expected_revenue': pickpack_revenue,
'markdowns': pickpack_markdowns,
'net_benefit': pickpack_revenue - pickpack_labor_cost - pickpack_markdowns
}
}
def recommend_strategy(self, num_skus, num_stores, product_type):
"""
Recommend pre-pack or pick-pack
Factors:
- Product type (fashion vs. basic)
- Store count
- Store diversity
"""
comparison = self.compare_strategies(num_skus, num_stores, avg_units_per_store=20)
prepack_net = comparison['pre_pack']['net_benefit']
pickpack_net = comparison['pick_pack']['net_benefit']
# Decision rules
if product_type == 'fashion' and pickpack_net > prepack_net * 1.1:
return {
'recommendation': 'Pick-Pack',
'reason': 'Fashion products benefit from customization',
'incremental_benefit': pickpack_net - prepack_net
}
elif num_stores > 500 and prepack_net > pickpack_net * 0.9:
return {
'recommendation': 'Pre-Pack',
'reason': 'Large store count makes pick-pack prohibitive',
'cost_savings': prepack_net - pickpack_net
}
elif product_type == 'basics':
return {
'recommendation': 'Pre-Pack',
'reason': 'Basic products have consistent demand across stores',
'efficiency_gain': 'Significant'
}
else:
return {
'recommendation': 'Pick-Pack',
'reason': 'Better accuracy justifies additional cost',
'incremental_benefit': pickpack_net - prepack_net
}
# Example
analyzer = PrePackAnalyzer()
comparison = analyzer.compare_strategies(num_skus=100, num_stores=200, avg_units_per_store=20)
print("Pre-Pack Strategy:")
print(f" Labor cost: ${comparison['pre_pack']['labor_cost']:,.0f}")
print(f" Net benefit: ${comparison['pre_pack']['net_benefit']:,.0f}")
print("\nPick-Pack Strategy:")
print(f" Labor cost: ${comparison['pick_pack']['labor_cost']:,.0f}")
print(f" Net benefit: ${comparison['pick_pack']['net_benefit']:,.0f}")
recommendation = analyzer.recommend_strategy(100, 200, 'fashion')
print(f"\nRecommendation: {recommendation['recommendation']}")
print(f"Reason: {recommendation['reason']}")
ツールとライブラリ
Python ライブラリ
最適化:
pulp、pyomo:配分最適化のための線形計画法scipy.optimize:最適化アルゴリズムortools:Google OR-Tools
クラスター化と ML:
scikit-learn:K-means クラスタリング、分類scipy:統計分析pandas:データ操作
可視化:
matplotlib、seaborn:プロット作成plotly:インタラクティブな可視化geopandas:地理情報の可視化
商用ソフトウェア
配分と計画:
- Blue Yonder Luminate:小売計画スイート
- o9 Solutions:デジタル計画プラットフォーム
- SAP Integrated Business Planning:エンタープライズ計画
- Oracle Retail:マーチャンダイジングと配分
- Aptos Retail:配分の最適化
特化型ツール:
- Infor Allocation:小売りの配分
- JustEnough:品揃えと配分
- TXT Retail:ファッション商品の配分
- 板Assortment Optimization by SAS:分析主導型の配分
よくある課題と解決策
課題:新商品の配分(販売履歴なし)
問題:
- 新商品の販売履歴がない
- 店舗ごとの需要予測が難しい
- 過剰在庫や品切れのリスクがある
解決策:
- 類似商品(スタイル、価格、カテゴリが近い商品)を参考にする
- テスト店舗で試験的に販売する
- 少量から始め、迅速に補充する
- 予測に役立つ属性(色、スタイル、価格)を活用する
- 販売初期の売れ行きを監視し、調整する
- 店舗をクラスター化し、各クラスターから1店舗でテストする
課題:サイズ/カラーの偏り
問題:
- 一部のサイズやカラーがすぐに売り切れる
- それ以外の商品は動きの遅い商品として残る
- 動きの遅いカラーは値下げのリスクがある
解決策:
- 地域ごとにサイズ構成を最適化する
- 地域ごとのカラーの好みを分析する
- トレンドカラーは少量でテストする
- 定番カラーは厚く、ファッションカラーは薄く配分する
- 偏りがある場合は店舗間で在庫を移動する
- 販売初期の消化率に基づいて動的に配分する
課題:店舗の収容能力の制約
問題:
- 小型店舗では全商品の品揃えを維持できない
- 在庫がバックヤードや売場のスペースを超える
- 混雑によって顧客体験が損なわれる
解決策:
- 小型店舗向けに厳選した品揃えを用意する
- 店舗を規模別に分類し、それに応じて調整する
- 売れ筋商品はクロスドッキングを活用する(バックヤードの在庫を削減)
- 一括納品ではなく、少量を頻繁に納品する
- 品切れ時はデジタルのエンドレスアイルを活用する
- 店舗フォーマットに合わせた品揃えにする
課題:地域ごとの需要の違い
問題:
- 全店舗一律の配分ではうまくいかない
- 気候の違い(フロリダでコート?)
- 人口統計上の違い
- 文化的な好み
解決策:
- 地理的なクラスター化
- 気候に基づく配分ルール
- 人口統計によるセグメント化
- 一部のカテゴリでは地域の購買担当者に権限を委譲する
- 地域別の配分計画
- 地域ごとにテストと学習を行う
課題:過剰在庫と値下げ
問題:
- 配分した在庫が多すぎる
- 消化率が低い
- 在庫を処分するため大幅な値下げが必要になる
解決策:
- 初回配分を控えめにし、追加発注で対応する
- 週次で消化率を監視する
- 早期に値下げを実施する
- 店舗間で在庫を移動する
- アウトレット店舗に配分する
- 需要予測を改善する
出力形式
配分計画レポート
エグゼクティブサマリー:
- 商品:2025年秋アパレルコレクション
- 配分総数:850 SKU にわたり、合計 125,000 点
- 店舗ネットワーク:185 店舗(A グレード 40 店舗、B グレード 55 店舗、C グレード 90 店舗)
- 配分方法:店舗クラスター分けに基づく需要配分
- 予想定価販売消化率:78%
店舗クラスターの概要:
| クラスター | 店舗数 | 特徴 | 週平均売上 | 配分戦略 |
|---|---|---|---|---|
| 1 - 大都市・高所得層 | 25 | 都心の旗艦店、富裕層 | $85K | 全商品を展開し、在庫を厚めに確保 |
| 2 - 郊外のモール | 60 | 地域のモール、ファミリー層 | $45K | 中核商品を展開し、在庫は中程度に確保 |
| 3 - 温暖な気候 | 40 | 南部の州、通年営業 | $38K | 季節性を調整し、軽量素材を中心に展開 |
| 4 - 小型店舗 | 35 | ストリップセンター、地方 | $22K | 厳選した商品を展開し、在庫は少なめに確保 |
| 5 - アウトレット | 25 | クリアランス中心 | $32K | 過剰在庫の配分先 |
グレード別の配分概要:
| グレード | 店舗数 | 配分数 | 1 店舗あたり平均点数 | SKU カバー率 | 在庫供給週数 |
|---|---|---|---|---|---|
| A | 40 | 48,000(38%) | 1,200 | 100% | 8 週間 |
| B | 55 | 45,000(36%) | 818 | 85% | 6 週間 |
| C | 90 | 32,000(26%) | 356 | 60% | 4 週間 |
サイズ構成比に基づく配分(例:レディーストップス):
| 店舗クラスター | XS | S | M | L | XL | XXL | 店舗あたり合計 |
|---|---|---|---|---|---|---|---|
| 大都市・高所得層 | 12% | 24% | 30% | 22% | 9% | 3% | 120 点 |
| 郊外 | 8% | 22% | 32% | 24% | 10% | 4% | 80 点 |
| 小型店舗 | 6% | 20% | 34% | 26% | 11% | 3% | 35 点 |
上位 10 店舗 - 配分例:
| 店舗 | グレード | クラスター | 合計点数 | 主力 SKU | 点数 | 予想売上 |
|---|---|---|---|---|---|---|
| S001 - NYC Flagship | A | 大都市 | 1,450 | Jacket-001 | 85 | $124K |
| S005 - LA Westfield | A | 大都市 | 1,320 | Dress-042 | 78 | $108K |
| S012 - Chicago | A | 大都市 | 1,280 | Top-015 | 72 | $98K |
| S023 - Boston | B | 郊外 | 950 | Sweater-008 | 58 | $65K |
| S034 - Atlanta | B | 温暖 | 880 | Shorts-011 | 52 | $58K |
財務への影響:
| 指標 | 値 |
|---|---|
| 配分総数 | 125,000 |
| 定価での小売価値 | $6,250,000 |
| 予想定価販売消化率 | 78% |
| 予想定価売上 | $4,875,000 |
| 予想値下げ販売売上 | $687,500 |
| 予想総売上 | $5,562,500 |
| 予想値下げ率 | 22% |
| 目標値下げ率 | <25% |
リスク評価:
-
高リスク SKU(予測信頼度が低い)
- トレンド性の高い SKU 45 点
- 保守的に需要の 60% を配分
- 対応策:最初の 2 週間を監視し、必要に応じて追加手配
-
地域リスク
- 北東部:早期の寒波リスク
- 軽減策:迅速な再配分に備え、10% のバッファーを確保
-
新商品リスク
- 120 SKU が新商品
- 軽減策:テスト店舗で展開し、類似商品に基づいて配分
アクションプラン:
| 週 | アクション | 担当 |
|---|---|---|
| 1 週目 | 配分計画を確定し、店舗に共有 | 配分チーム |
| 2 週目 | DC で在庫を受け入れ、事前梱包/ピッキング作業を実施 | DC オペレーション |
| 3 週目 | 店舗へ出荷し、輸送中の追跡を開始 | 物流 |
| 4 週目 | 店舗へ納品し、商品陳列を実施 | 店舗運営 |
| 5 週目 | 販売消化率を監視し、売れ行きの速い商品と遅い商品を特定 | プランニング |
| 6 週目 | 偏りを解消するため再配分を実施 | 配分チーム |
確認事項
さらに詳しい情報が必要な場合:
- どの商品カテゴリーを配分していますか?
- 店舗ネットワークの店舗数はいくつですか?
- 店舗クラスターやグレードは定義されていますか?
- 新商品ですか、それとも補充ですか?
- 過去の販売消化率はどのくらいですか?
- 目標値下げ率はいくらですか?
- 事前梱包またはピック・アンド・パックを利用していますか?
- 現在、どの配分システム/プロセスを使用していますか?
- 最大の課題は何ですか?(値下げ、在庫切れ、複雑さ)
関連スキル
- retail-replenishment:初回配分後の継続的な補充
- markdown-optimization:売れ行きの遅い商品のマークダウン戦略
- demand-forecasting:配分計画のための需要予測
- seasonal-planning:季節商品の仕入れ計画
- inventory-optimization:安全在庫とサービスレベル
- warehouse-slotting-optimization:ピッキング効率を高めるためのDCスロッティング
- supply-chain-analytics:パフォーマンス指標とレポート
コメント
Markdown 対応