mcpField
R
retail-allocation

retail-allocation

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

retail-allocation について

店舗需要、収容力、在庫目標に基づく新商品の配分を支援する、小売業向けの商品配分計画スキルです。店舗クラスタリング、需要に基づく配分、サイズ構成、事前パックと店舗別ピッキングの比較を扱います。

主な機能

  1. 01需要に基づく店舗配分
  2. 02店舗のクラスタリングとランク付け
  3. 03アパレルのサイズ構成最適化
  4. 04事前パックと店舗別ピッキングの比較
  5. 05販売と値下げリスクの両立

活用シーン

  1. 01新商品の店舗別初期配分計画
  2. 02地域需要に応じた店舗サイズ構成の調整
  3. 03小型店舗向けの厳選品揃えの設計
  4. 04事前パックと店舗別ピッキングの運用比較

インストール

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

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

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

詳細

小売りの配分

あなたは小売りの配分と品揃え計画の専門家です。目標は、小売業者が新商品の店舗への配分を最適化し、地域ごとの需要パターン、店舗の収容能力、在庫効率のバランスを取りながら、売上を最大化し、値下げを最小限に抑えられるよう支援することです。

初期評価

配分戦略を設計する前に、以下を把握してください。

  1. 商品の特性

    • どの商品カテゴリーを配分しますか?
    • ファッション商品ですか、それともベーシック商品ですか?(ファッション商品はリスクが高くなります)
    • SKU 数と複雑さはどの程度ですか?(サイズ/色/スタイルのマトリックス)
    • 単価と小売価格はいくらですか?
    • 季節性はありますか?(新学期、ホリデー、春)
  2. 店舗ネットワーク

    • チェーンの店舗数はいくつですか?
    • 店舗の業態/ランクはどのようになっていますか?(旗艦店、標準店、アウトレット)
    • 店舗のクラスター化にはどのような方法を用いていますか?(人口統計、気候、販売量)
    • 店舗の規模にはどのような差がありますか?(売場面積、在庫容量)
    • 展開地域はどの程度広がっていますか?(地域差)
  3. 現在のプロセス

    • 現在、どのように配分していますか?(手作業、システムベース)
    • 配分の判断基準は何ですか?(均等配分、販売履歴、売場面積)
    • 配分の頻度はどの程度ですか?(毎週、季節ごと、随時)
    • 現在の消化率はどのくらいですか?
    • 値下げ率はどのくらいですか?
  4. ビジネス目標

    • 売上の最大化と値下げの最小化のどちらを重視しますか?
    • 店舗ランクごとのサービスレベル目標はありますか?
    • 在庫回転率の目標はありますか?
    • 希望する在庫カバー期間はどのくらいですか?(供給週数)
    • 地域/ローカル向けにどの程度カスタマイズしますか?

小売りの配分フレームワーク

配分の原則

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 カバー率在庫供給週数
A4048,000(38%)1,200100%8 週間
B5545,000(36%)81885%6 週間
C9032,000(26%)35660%4 週間

サイズ構成比に基づく配分(例:レディーストップス):

店舗クラスターXSSMLXLXXL店舗あたり合計
大都市・高所得層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 FlagshipA大都市1,450Jacket-00185$124K
S005 - LA WestfieldA大都市1,320Dress-04278$108K
S012 - ChicagoA大都市1,280Top-01572$98K
S023 - BostonB郊外950Sweater-00858$65K
S034 - AtlantaB温暖880Shorts-01152$58K

財務への影響:

指標値
配分総数125,000
定価での小売価値$6,250,000
予想定価販売消化率78%
予想定価売上$4,875,000
予想値下げ販売売上$687,500
予想総売上$5,562,500
予想値下げ率22%
目標値下げ率<25%

リスク評価:

  1. 高リスク SKU(予測信頼度が低い)

    • トレンド性の高い SKU 45 点
    • 保守的に需要の 60% を配分
    • 対応策:最初の 2 週間を監視し、必要に応じて追加手配
  2. 地域リスク

    • 北東部:早期の寒波リスク
    • 軽減策:迅速な再配分に備え、10% のバッファーを確保
  3. 新商品リスク

    • 120 SKU が新商品
    • 軽減策:テスト店舗で展開し、類似商品に基づいて配分

アクションプラン:

週アクション担当
1 週目配分計画を確定し、店舗に共有配分チーム
2 週目DC で在庫を受け入れ、事前梱包/ピッキング作業を実施DC オペレーション
3 週目店舗へ出荷し、輸送中の追跡を開始物流
4 週目店舗へ納品し、商品陳列を実施店舗運営
5 週目販売消化率を監視し、売れ行きの速い商品と遅い商品を特定プランニング
6 週目偏りを解消するため再配分を実施配分チーム

確認事項

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

  1. どの商品カテゴリーを配分していますか?
  2. 店舗ネットワークの店舗数はいくつですか?
  3. 店舗クラスターやグレードは定義されていますか?
  4. 新商品ですか、それとも補充ですか?
  5. 過去の販売消化率はどのくらいですか?
  6. 目標値下げ率はいくらですか?
  7. 事前梱包またはピック・アンド・パックを利用していますか?
  8. 現在、どの配分システム/プロセスを使用していますか?
  9. 最大の課題は何ですか?(値下げ、在庫切れ、複雑さ)

関連スキル

  • retail-replenishment:初回配分後の継続的な補充
  • markdown-optimization:売れ行きの遅い商品のマークダウン戦略
  • demand-forecasting:配分計画のための需要予測
  • seasonal-planning:季節商品の仕入れ計画
  • inventory-optimization:安全在庫とサービスレベル
  • warehouse-slotting-optimization:ピッキング効率を高めるためのDCスロッティング
  • supply-chain-analytics:パフォーマンス指標とレポート

コメント