{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# ๐ŸŒŠ The Bullwhip Effect: Why Small Ripples Become Giant Waves\n", "\n", "**BCOR 440: Operations & Supply Chain Management**\n", "\n", "---\n", "\n", "## What You'll Learn Today\n", "\n", "By the end of this notebook, you'll be able to:\n", "- Explain why a 10% change in customer demand can cause a 40%+ swing at the supplier level\n", "- Identify the four main causes of the bullwhip effect\n", "- Describe strategies companies use to reduce demand amplification\n", "- Connect this to your Practice Operations simulation decisions\n", "\n", "---\n", "\n", "## Why Python Instead of Excel?\n", "\n", "Great question! We're using Python here because:\n", "1. **We'll run 500+ simulations** to see patterns emerge โ€” try doing that in Excel!\n", "2. **Complex supply chain dynamics** โ€” multiple tiers reacting simultaneously\n", "3. **Animated visualizations** โ€” watch the chaos unfold in real-time\n", "\n", "You don't need to write any code. Just run each cell and observe what happens." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## Part 1: The Setup โ€” Meet Your Supply Chain\n", "\n", "Imagine you're managing a **4-tier supply chain** for smartphones:\n", "\n", "```\n", "๐Ÿ“ฑ Retailer (Best Buy) โ†’ ๐Ÿ“ฆ Distributor โ†’ ๐Ÿญ Factory โ†’ โ›๏ธ Raw Materials Supplier\n", "```\n", "\n", "Each level makes ordering decisions based on what they *think* demand will be.\n", "\n", "Let's set up our simulation:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Run this cell first โ€” it loads our tools\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "from matplotlib.animation import FuncAnimation\n", "from IPython.display import HTML, display\n", "import warnings\n", "warnings.filterwarnings('ignore')\n", "\n", "# Set a nice visual style\n", "plt.style.use('seaborn-v0_8-whitegrid')\n", "plt.rcParams['figure.figsize'] = [12, 6]\n", "plt.rcParams['font.size'] = 12\n", "\n", "print(\"โœ… Tools loaded! Ready to simulate.\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## Part 2: Creating Realistic Demand Data\n", "\n", "Real-world demand has three components:\n", "- **Trend** โ€” overall growth or decline\n", "- **Seasonality** โ€” predictable patterns (holiday spikes, summer slowdowns)\n", "- **Random noise** โ€” unpredictable day-to-day variation\n", "\n", "We'll create demand data that mirrors what companies actually see." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def generate_realistic_demand(periods=52, base_demand=100, seed=42):\n", " \"\"\"\n", " Generate realistic demand with trend, seasonality, and noise.\n", " Mirrors patterns seen in real retail/manufacturing data.\n", " \"\"\"\n", " np.random.seed(seed)\n", " \n", " # Time index\n", " t = np.arange(periods)\n", " \n", " # Component 1: Slight upward trend (2% annual growth)\n", " trend = base_demand * (1 + 0.02 * t / 52)\n", " \n", " # Component 2: Seasonality (peaks in Q4 for retail)\n", " seasonality = 15 * np.sin(2 * np.pi * t / 52 - np.pi/2) # Peak around week 39 (Q4)\n", " \n", " # Component 3: Random variation (demand uncertainty)\n", " noise = np.random.normal(0, 10, periods)\n", " \n", " # Combine components\n", " demand = trend + seasonality + noise\n", " \n", " # Ensure demand is positive\n", " demand = np.maximum(demand, base_demand * 0.5)\n", " \n", " return demand\n", "\n", "# Generate our demand pattern\n", "customer_demand = generate_realistic_demand(periods=52)\n", "\n", "# Visualize it\n", "fig, ax = plt.subplots(figsize=(12, 5))\n", "ax.plot(customer_demand, color='#2ecc71', linewidth=2, marker='o', markersize=4, label='Customer Demand')\n", "ax.axhline(y=100, color='gray', linestyle='--', alpha=0.5, label='Baseline (100 units)')\n", "ax.fill_between(range(len(customer_demand)), customer_demand, 100, alpha=0.3, color='#2ecc71')\n", "ax.set_xlabel('Week')\n", "ax.set_ylabel('Units Demanded')\n", "ax.set_title('๐Ÿ“Š Customer Demand Over 52 Weeks', fontsize=14, fontweight='bold')\n", "ax.legend()\n", "\n", "# Add annotations\n", "peak_week = np.argmax(customer_demand)\n", "ax.annotate(f'Q4 Peak\\n({customer_demand[peak_week]:.0f} units)', \n", " xy=(peak_week, customer_demand[peak_week]),\n", " xytext=(peak_week-8, customer_demand[peak_week]+15),\n", " fontsize=10,\n", " arrowprops=dict(arrowstyle='->', color='gray'))\n", "\n", "plt.tight_layout()\n", "plt.show()\n", "\n", "print(f\"\\n๐Ÿ“ˆ Demand Statistics:\")\n", "print(f\" Average: {np.mean(customer_demand):.1f} units/week\")\n", "print(f\" Std Dev: {np.std(customer_demand):.1f} units (this is the 'noise' level)\")\n", "print(f\" Range: {np.min(customer_demand):.0f} to {np.max(customer_demand):.0f} units\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## Part 3: The Bullwhip Effect Engine\n", "\n", "Here's where the magic (or chaos) happens. Each tier in our supply chain:\n", "\n", "1. **Observes** demand from downstream\n", "2. **Forecasts** future demand (using simple moving average)\n", "3. **Orders** based on forecast + safety stock\n", "4. **Waits** for orders to arrive (lead time delay)\n", "\n", "The problem? **Each tier only sees orders from the tier below, not actual customer demand.**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class SupplyChainSimulator:\n", " \"\"\"\n", " Simulates a 4-tier supply chain to demonstrate the bullwhip effect.\n", " \n", " The bullwhip effect occurs because:\n", " 1. Demand signal processing (forecasting adds lag)\n", " 2. Order batching (orders come in lumps, not streams)\n", " 3. Price fluctuations (buying more when prices are low)\n", " 4. Rationing & shortage gaming (over-ordering when supply is tight)\n", " \"\"\"\n", " \n", " def __init__(self, \n", " periods=52,\n", " base_demand=100,\n", " demand_std=10,\n", " lead_time=2,\n", " forecast_periods=4,\n", " safety_stock_factor=1.5):\n", " \n", " self.periods = periods\n", " self.base_demand = base_demand\n", " self.demand_std = demand_std\n", " self.lead_time = lead_time\n", " self.forecast_periods = forecast_periods\n", " self.safety_stock_factor = safety_stock_factor\n", " \n", " # Supply chain tiers\n", " self.tiers = ['Customer Demand', 'Retailer Orders', \n", " 'Distributor Orders', 'Factory Orders', \n", " 'Supplier Orders']\n", " \n", " def generate_customer_demand(self, external_data=None):\n", " \"\"\"Generate realistic customer demand with trend and seasonality\"\"\"\n", " if external_data is not None:\n", " return external_data\n", " \n", " return generate_realistic_demand(\n", " periods=self.periods,\n", " base_demand=self.base_demand\n", " )\n", " \n", " def forecast_demand(self, history):\n", " \"\"\"Simple moving average forecast\"\"\"\n", " if len(history) < self.forecast_periods:\n", " return np.mean(history) if history else self.base_demand\n", " return np.mean(history[-self.forecast_periods:])\n", " \n", " def calculate_order(self, forecast, current_inventory, incoming_orders):\n", " \"\"\"\n", " Order-up-to policy: Order enough to meet forecasted demand plus safety stock.\n", " This is where the bullwhip amplification happens!\n", " \"\"\"\n", " # Target inventory = forecasted demand during lead time + safety stock\n", " target = forecast * (self.lead_time + 1) + self.safety_stock_factor * self.demand_std\n", " \n", " # Order the gap between target and current position\n", " order_quantity = max(0, target - current_inventory - incoming_orders)\n", " \n", " # Order batching: round to nearest 10 units (realistic constraint)\n", " order_quantity = round(order_quantity / 10) * 10\n", " \n", " return order_quantity\n", " \n", " def run_simulation(self, external_demand=None):\n", " \"\"\"Run the full supply chain simulation\"\"\"\n", " # Initialize storage for each tier\n", " results = {tier: [] for tier in self.tiers}\n", " \n", " # Generate customer demand\n", " customer_demand = self.generate_customer_demand(external_demand)\n", " results['Customer Demand'] = list(customer_demand)\n", " \n", " # Initialize each tier's state\n", " num_tiers = len(self.tiers) - 1 # Exclude customer demand\n", " inventories = [self.base_demand * 2] * num_tiers\n", " incoming = [[0] * self.lead_time for _ in range(num_tiers)]\n", " \n", " # Simulate each period\n", " for period in range(self.periods):\n", " orders_this_period = [customer_demand[period]] # Start with customer demand\n", " \n", " # Each tier reacts to the tier below it\n", " for tier_idx in range(num_tiers):\n", " tier_name = self.tiers[tier_idx + 1]\n", " \n", " # Get demand signal from downstream tier\n", " downstream_demand = orders_this_period[-1]\n", " \n", " # Update inventory: receive incoming, subtract demand\n", " inventories[tier_idx] += incoming[tier_idx].pop(0)\n", " inventories[tier_idx] -= downstream_demand\n", " \n", " # Forecast based on recent downstream orders\n", " history = results[tier_name][-self.forecast_periods:] if results[tier_name] else [self.base_demand]\n", " forecast = self.forecast_demand(history + [downstream_demand])\n", " \n", " # Calculate order quantity\n", " incoming_sum = sum(incoming[tier_idx])\n", " order = self.calculate_order(forecast, inventories[tier_idx], incoming_sum)\n", " \n", " # Record the order\n", " results[tier_name].append(order)\n", " orders_this_period.append(order)\n", " \n", " # Add to incoming pipeline (will arrive after lead time)\n", " incoming[tier_idx].append(order)\n", " \n", " return pd.DataFrame(results)\n", " \n", " def calculate_amplification(self, results):\n", " \"\"\"Calculate how much demand variability amplifies at each tier\"\"\"\n", " customer_std = results['Customer Demand'].std()\n", " \n", " amplification = {}\n", " for tier in self.tiers:\n", " tier_std = results[tier].std()\n", " amplification[tier] = tier_std / customer_std\n", " \n", " return amplification\n", "\n", "print(\"โœ… Supply Chain Simulator loaded!\")\n", "print(\"\\n๐Ÿ“‹ Key settings:\")\n", "print(\" โ€ข 4 tiers: Customer โ†’ Retailer โ†’ Distributor โ†’ Factory โ†’ Supplier\")\n", "print(\" โ€ข Lead time: 2 weeks\")\n", "print(\" โ€ข Forecast method: 4-period moving average\")\n", "print(\" โ€ข Safety stock: 1.5x demand standard deviation\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## Part 4: Watch the Bullwhip in Action! ๐ŸŒŠ\n", "\n", "Now let's run the simulation and see what happens. Pay attention to how the **variability increases** as you move upstream." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Create and run the simulation\n", "sim = SupplyChainSimulator(\n", " periods=52,\n", " base_demand=100,\n", " demand_std=10,\n", " lead_time=2,\n", " forecast_periods=4,\n", " safety_stock_factor=1.5\n", ")\n", "\n", "results = sim.run_simulation()\n", "\n", "# Calculate amplification\n", "amplification = sim.calculate_amplification(results)\n", "\n", "# Create the visualization\n", "fig, axes = plt.subplots(2, 1, figsize=(14, 10))\n", "\n", "# Plot 1: All tiers over time\n", "colors = ['#2ecc71', '#3498db', '#9b59b6', '#e74c3c', '#e67e22']\n", "for i, tier in enumerate(sim.tiers):\n", " axes[0].plot(results[tier], label=tier, color=colors[i], linewidth=2, alpha=0.8)\n", "\n", "axes[0].set_xlabel('Week')\n", "axes[0].set_ylabel('Order Quantity')\n", "axes[0].set_title('๐ŸŒŠ The Bullwhip Effect: Watch Variability Amplify Upstream', \n", " fontsize=14, fontweight='bold')\n", "axes[0].legend(loc='upper right')\n", "axes[0].axhline(y=100, color='gray', linestyle='--', alpha=0.5)\n", "\n", "# Plot 2: Amplification factor by tier\n", "tiers = list(amplification.keys())\n", "factors = [amplification[t] for t in tiers]\n", "\n", "bars = axes[1].bar(tiers, factors, color=colors, edgecolor='white', linewidth=2)\n", "axes[1].set_ylabel('Amplification Factor\\n(relative to customer demand)')\n", "axes[1].set_title('๐Ÿ“Š How Much Does Variability Amplify at Each Tier?', \n", " fontsize=14, fontweight='bold')\n", "\n", "# Add value labels on bars\n", "for bar, factor in zip(bars, factors):\n", " axes[1].annotate(f'{factor:.1f}x',\n", " xy=(bar.get_x() + bar.get_width() / 2, bar.get_height()),\n", " ha='center', va='bottom', fontsize=12, fontweight='bold')\n", "\n", "# Add a reference line at 1.0x\n", "axes[1].axhline(y=1, color='gray', linestyle='--', alpha=0.7)\n", "axes[1].annotate('No amplification (1.0x)', xy=(0.5, 1.1), fontsize=10, color='gray')\n", "\n", "plt.tight_layout()\n", "plt.show()\n", "\n", "# Print summary statistics\n", "print(\"\\n๐Ÿ“Š VARIABILITY BY TIER:\")\n", "print(\"=\" * 50)\n", "for tier in sim.tiers:\n", " std = results[tier].std()\n", " amp = amplification[tier]\n", " print(f\"{tier:25} | Std Dev: {std:6.1f} | Amplification: {amp:.1f}x\")\n", "\n", "print(\"\\n๐Ÿ’ก KEY INSIGHT:\")\n", "print(f\" Customer demand varies by ยฑ{results['Customer Demand'].std():.0f} units\")\n", "print(f\" But supplier orders vary by ยฑ{results['Supplier Orders'].std():.0f} units!\")\n", "print(f\" That's a {amplification['Supplier Orders']:.1f}x amplification!\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## โธ๏ธ STOP AND THINK\n", "\n", "**Discussion Questions:**\n", "\n", "1. Look at the top chart. Which line (tier) has the most \"chaos\"? Why does this happen?\n", "\n", "2. If you were the supplier (orange line), how would this variability affect your business?\n", " - Your hiring decisions?\n", " - Your inventory costs?\n", " - Your machine utilization?\n", "\n", "3. Notice that customer demand (green) is relatively stable. Why don't the other tiers just copy it?\n", "\n", "---" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Part 5: Monte Carlo โ€” Is This a Fluke or a Pattern?\n", "\n", "One simulation might be random luck. Let's run **500 simulations** to see if the bullwhip effect is consistent.\n", "\n", "**This is why we use Python instead of Excel!** ๐Ÿ" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Run 500 simulations with different random demand patterns\n", "n_simulations = 500\n", "all_amplifications = []\n", "\n", "print(f\"Running {n_simulations} simulations...\")\n", "for i in range(n_simulations):\n", " # Create new simulator with different random seed\n", " np.random.seed(i)\n", " demand = generate_realistic_demand(periods=52, base_demand=100, seed=i)\n", " results = sim.run_simulation(external_demand=demand)\n", " amp = sim.calculate_amplification(results)\n", " all_amplifications.append(amp)\n", " \n", " # Progress indicator\n", " if (i + 1) % 100 == 0:\n", " print(f\" Completed {i + 1} simulations...\")\n", "\n", "# Convert to DataFrame for analysis\n", "amp_df = pd.DataFrame(all_amplifications)\n", "\n", "print(\"\\nโœ… All simulations complete!\")\n", "\n", "# Create visualization\n", "fig, axes = plt.subplots(1, 2, figsize=(14, 5))\n", "\n", "# Box plot of amplification factors\n", "colors = ['#2ecc71', '#3498db', '#9b59b6', '#e74c3c', '#e67e22']\n", "bp = axes[0].boxplot([amp_df[tier] for tier in sim.tiers], \n", " labels=['Customer', 'Retailer', 'Distributor', 'Factory', 'Supplier'],\n", " patch_artist=True)\n", "\n", "for patch, color in zip(bp['boxes'], colors):\n", " patch.set_facecolor(color)\n", " patch.set_alpha(0.7)\n", "\n", "axes[0].set_ylabel('Amplification Factor')\n", "axes[0].set_title(f'Bullwhip Amplification Across {n_simulations} Simulations', fontweight='bold')\n", "axes[0].axhline(y=1, color='gray', linestyle='--', alpha=0.5)\n", "\n", "# Histogram of supplier amplification\n", "axes[1].hist(amp_df['Supplier Orders'], bins=30, color='#e67e22', edgecolor='white', alpha=0.7)\n", "axes[1].axvline(x=amp_df['Supplier Orders'].mean(), color='red', linestyle='-', linewidth=2,\n", " label=f'Mean: {amp_df[\"Supplier Orders\"].mean():.1f}x')\n", "axes[1].axvline(x=1, color='gray', linestyle='--', linewidth=1, label='No amplification (1.0x)')\n", "axes[1].set_xlabel('Amplification Factor')\n", "axes[1].set_ylabel('Frequency')\n", "axes[1].set_title('Distribution of Supplier Amplification', fontweight='bold')\n", "axes[1].legend()\n", "\n", "plt.tight_layout()\n", "plt.show()\n", "\n", "print(\"\\n๐Ÿ“Š STATISTICAL SUMMARY (500 simulations):\")\n", "print(\"=\" * 55)\n", "print(f\"{'Tier':<20} {'Mean Amp':>12} {'Min':>10} {'Max':>10}\")\n", "print(\"-\" * 55)\n", "for tier in sim.tiers:\n", " mean_amp = amp_df[tier].mean()\n", " min_amp = amp_df[tier].min()\n", " max_amp = amp_df[tier].max()\n", " print(f\"{tier:<20} {mean_amp:>12.1f}x {min_amp:>10.1f}x {max_amp:>10.1f}x\")\n", "\n", "print(\"\\n๐Ÿ’ก CONCLUSION:\")\n", "print(f\" The bullwhip effect is NOT random โ€” it happened in ALL {n_simulations} simulations!\")\n", "print(f\" Supplier variability averages {amp_df['Supplier Orders'].mean():.1f}x customer variability.\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## Part 6: What Causes This? The Four Culprits\n", "\n", "The bullwhip effect isn't one problem โ€” it's four problems combined:\n", "\n", "| Cause | What Happens | Example |\n", "|-------|--------------|----------|\n", "| **1. Demand Signal Processing** | Each tier forecasts based on orders, not true demand | Retailer sees a spike, assumes trend, over-orders |\n", "| **2. Order Batching** | Companies order in batches, not continuous flow | \"We only order on Mondays\" or \"minimum 100 units\" |\n", "| **3. Price Fluctuations** | Promotions cause forward-buying | \"20% off this week!\" โ†’ everyone stocks up |\n", "| **4. Rationing & Shortage Gaming** | When supply is short, everyone over-orders | Toilet paper 2020: expected shortage โ†’ panic buying |\n", "\n", "Let's test what happens when we change the lead time (one key factor):" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Experiment: How does lead time affect amplification?\n", "lead_times = [1, 2, 3, 4, 5]\n", "amp_by_lead_time = []\n", "\n", "# Use consistent demand for fair comparison\n", "consistent_demand = generate_realistic_demand(periods=52, base_demand=100, seed=42)\n", "\n", "for lt in lead_times:\n", " test_sim = SupplyChainSimulator(\n", " periods=52,\n", " base_demand=100,\n", " lead_time=lt,\n", " forecast_periods=4,\n", " safety_stock_factor=1.5\n", " )\n", " results = test_sim.run_simulation(external_demand=consistent_demand)\n", " amp = test_sim.calculate_amplification(results)\n", " amp_by_lead_time.append(amp['Supplier Orders'])\n", "\n", "# Visualize\n", "fig, ax = plt.subplots(figsize=(10, 5))\n", "\n", "bars = ax.bar(lead_times, amp_by_lead_time, color='#e74c3c', edgecolor='white', linewidth=2)\n", "ax.set_xlabel('Lead Time (weeks)', fontsize=12)\n", "ax.set_ylabel('Supplier Amplification Factor', fontsize=12)\n", "ax.set_title('๐Ÿšš Longer Lead Times = Worse Bullwhip Effect', fontsize=14, fontweight='bold')\n", "ax.set_xticks(lead_times)\n", "\n", "# Add value labels\n", "for bar, amp in zip(bars, amp_by_lead_time):\n", " ax.annotate(f'{amp:.1f}x',\n", " xy=(bar.get_x() + bar.get_width() / 2, bar.get_height()),\n", " ha='center', va='bottom', fontsize=12, fontweight='bold')\n", "\n", "plt.tight_layout()\n", "plt.show()\n", "\n", "print(\"\\n๐Ÿ’ก KEY INSIGHT:\")\n", "print(f\" โ€ข 1-week lead time: {amp_by_lead_time[0]:.1f}x amplification\")\n", "print(f\" โ€ข 5-week lead time: {amp_by_lead_time[4]:.1f}x amplification\")\n", "print(f\"\\n Reducing lead time is one of the most effective bullwhip remedies!\")\n", "print(f\" This is why Amazon invests billions in faster delivery infrastructure.\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## Part 7: The Solution โ€” Information Sharing\n", "\n", "What if every tier could see **actual customer demand** instead of just orders from below?\n", "\n", "This is what happens when companies share Point-of-Sale (POS) data with suppliers." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class TransparentSupplyChain(SupplyChainSimulator):\n", " \"\"\"\n", " A supply chain where ALL tiers can see actual customer demand.\n", " This represents information sharing / POS data transparency.\n", " \"\"\"\n", " \n", " def run_simulation(self, external_demand=None):\n", " \"\"\"Run simulation where all tiers see customer demand\"\"\"\n", " results = {tier: [] for tier in self.tiers}\n", " \n", " customer_demand = self.generate_customer_demand(external_demand)\n", " results['Customer Demand'] = list(customer_demand)\n", " \n", " num_tiers = len(self.tiers) - 1\n", " inventories = [self.base_demand * 2] * num_tiers\n", " incoming = [[0] * self.lead_time for _ in range(num_tiers)]\n", " \n", " for period in range(self.periods):\n", " # KEY DIFFERENCE: All tiers react to CUSTOMER demand, not tier below\n", " actual_customer_demand = customer_demand[period]\n", " \n", " for tier_idx in range(num_tiers):\n", " tier_name = self.tiers[tier_idx + 1]\n", " \n", " inventories[tier_idx] += incoming[tier_idx].pop(0)\n", " inventories[tier_idx] -= actual_customer_demand\n", " \n", " # Forecast based on ACTUAL customer demand (not distorted orders)\n", " history = results['Customer Demand'][-self.forecast_periods:] if len(results['Customer Demand']) >= self.forecast_periods else [self.base_demand]\n", " forecast = self.forecast_demand(history)\n", " \n", " incoming_sum = sum(incoming[tier_idx])\n", " order = self.calculate_order(forecast, inventories[tier_idx], incoming_sum)\n", " \n", " results[tier_name].append(order)\n", " incoming[tier_idx].append(order)\n", " \n", " return pd.DataFrame(results)\n", "\n", "# Compare traditional vs transparent supply chain\n", "consistent_demand = generate_realistic_demand(periods=52, base_demand=100, seed=42)\n", "\n", "# Traditional (distorted signal)\n", "traditional = SupplyChainSimulator(periods=52, lead_time=2)\n", "trad_results = traditional.run_simulation(external_demand=consistent_demand)\n", "trad_amp = traditional.calculate_amplification(trad_results)\n", "\n", "# Transparent (shared information)\n", "transparent = TransparentSupplyChain(periods=52, lead_time=2)\n", "trans_results = transparent.run_simulation(external_demand=consistent_demand)\n", "trans_amp = transparent.calculate_amplification(trans_results)\n", "\n", "# Visualize comparison\n", "fig, axes = plt.subplots(1, 2, figsize=(14, 5))\n", "\n", "# Traditional\n", "axes[0].plot(trad_results['Customer Demand'], label='Customer', color='#2ecc71', linewidth=2)\n", "axes[0].plot(trad_results['Supplier Orders'], label='Supplier', color='#e74c3c', linewidth=2)\n", "axes[0].set_title(f'Traditional: Supplier Amp = {trad_amp[\"Supplier Orders\"]:.1f}x', fontweight='bold')\n", "axes[0].set_xlabel('Week')\n", "axes[0].set_ylabel('Units')\n", "axes[0].legend()\n", "axes[0].set_ylim(0, 300)\n", "\n", "# Transparent\n", "axes[1].plot(trans_results['Customer Demand'], label='Customer', color='#2ecc71', linewidth=2)\n", "axes[1].plot(trans_results['Supplier Orders'], label='Supplier', color='#3498db', linewidth=2)\n", "axes[1].set_title(f'With POS Sharing: Supplier Amp = {trans_amp[\"Supplier Orders\"]:.1f}x', fontweight='bold')\n", "axes[1].set_xlabel('Week')\n", "axes[1].set_ylabel('Units')\n", "axes[1].legend()\n", "axes[1].set_ylim(0, 300)\n", "\n", "plt.tight_layout()\n", "plt.show()\n", "\n", "reduction = (1 - trans_amp['Supplier Orders'] / trad_amp['Supplier Orders']) * 100\n", "print(\"\\n๐Ÿ“Š INFORMATION SHARING RESULTS:\")\n", "print(\"=\" * 50)\n", "print(f\" Traditional supply chain: {trad_amp['Supplier Orders']:.1f}x amplification\")\n", "print(f\" With POS data sharing: {trans_amp['Supplier Orders']:.1f}x amplification\")\n", "print(f\"\\n ๐ŸŽฏ Reduction: {reduction:.0f}%\")\n", "print(\"\\n๐Ÿ’ก This is why Walmart pioneered sharing POS data with P&G in the 1980s!\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## Part 8: Real-World Case Studies ๐Ÿ“š\n", "\n", "The bullwhip effect isn't just theory โ€” it's caused massive problems for real companies:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "case_studies = \"\"\"\n", "โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”\n", "โ”‚ REAL-WORLD BULLWHIP CASES โ”‚\n", "โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค\n", "โ”‚ โ”‚\n", "โ”‚ ๐Ÿงป TOILET PAPER (2020) โ”‚\n", "โ”‚ โ€ข Customer demand: Up ~40% initially โ”‚\n", "โ”‚ โ€ข Retailer orders: Up 200-400% โ”‚\n", "โ”‚ โ€ข Result: Empty shelves, then massive excess inventory โ”‚\n", "โ”‚ โ”‚\n", "โ”‚ ๐Ÿ’ป CISCO (2001) โ”‚\n", "โ”‚ โ€ข Customers doubled orders during dot-com boom โ”‚\n", "โ”‚ โ€ข Cisco's suppliers ramped up even more โ”‚\n", "โ”‚ โ€ข Result: $2.2 BILLION inventory write-off when demand crashed โ”‚\n", "โ”‚ โ”‚\n", "โ”‚ ๐Ÿš— AUTOMOTIVE CHIPS (2021) โ”‚\n", "โ”‚ โ€ข COVID lockdowns: Automakers cancelled chip orders โ”‚\n", "โ”‚ โ€ข Recovery: Everyone ordered at once โ”‚\n", "โ”‚ โ€ข Result: 6-12 month chip shortage, production halts โ”‚\n", "โ”‚ โ”‚\n", "โ”‚ ๐Ÿ BARILLA PASTA (1990s) โ”‚\n", "โ”‚ โ€ข Weekly promotions caused 200-300% order spikes โ”‚\n", "โ”‚ โ€ข Solution: Everyday Low Pricing + VMI program โ”‚\n", "โ”‚ โ€ข Result: Inventory costs cut by 50% โ”‚\n", "โ”‚ โ”‚\n", "โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜\n", "\"\"\"\n", "\n", "print(case_studies)\n", "\n", "print(\"\\n๐Ÿ”ง STRATEGIES TO REDUCE THE BULLWHIP EFFECT:\")\n", "print(\"=\"*60)\n", "strategies = [\n", " (\"Share POS Data\", \"Give all tiers visibility to actual customer demand\"),\n", " (\"Reduce Lead Times\", \"Faster delivery = less forecasting needed\"),\n", " (\"Smaller, Frequent Orders\", \"Order daily instead of weekly batches\"),\n", " (\"Everyday Low Pricing\", \"Eliminate promotions that cause forward-buying\"),\n", " (\"Vendor Managed Inventory\", \"Let suppliers manage retail inventory levels\"),\n", " (\"Collaborative Forecasting\", \"CPFR: Share forecasts across the supply chain\")\n", "]\n", "\n", "for strategy, description in strategies:\n", " print(f\"\\n โœ“ {strategy}\")\n", " print(f\" {description}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## Part 9: Connect to Practice Operations ๐ŸŽฎ\n", "\n", "How does this apply to your simulation?" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(\"\"\"\n", "โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”\n", "โ”‚ PRACTICE OPERATIONS CONNECTION โ”‚\n", "โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค\n", "โ”‚ โ”‚\n", "โ”‚ MODULE 2: Managing Suppliers โ”‚\n", "โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚\n", "โ”‚ โ€ข You're the RETAILER in the bullwhip chain โ”‚\n", "โ”‚ โ€ข Your ordering decisions affect supplier stability โ”‚\n", "โ”‚ โ€ข Steady orders = lower costs, better quality โ”‚\n", "โ”‚ โ”‚\n", "โ”‚ MODULE 3: Forecasting and Contracts โ”‚\n", "โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚\n", "โ”‚ โ€ข This is where demand signal processing happens! โ”‚\n", "โ”‚ โ€ข The \"hot\" products forecast = potential bullwhip trigger โ”‚\n", "โ”‚ โ€ข Don't over-react to short-term trends โ”‚\n", "โ”‚ โ”‚\n", "โ”‚ MODULE 4: Capacity Planning โ”‚\n", "โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚\n", "โ”‚ โ€ข Bullwhip makes capacity planning harder โ”‚\n", "โ”‚ โ€ข Do you hire for peak or average demand? โ”‚\n", "โ”‚ โ€ข Hint: Smooth your production, buffer with inventory โ”‚\n", "โ”‚ โ”‚\n", "โ”‚ MODULE 6: Maximize Net Worth (Capstone) โ”‚\n", "โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚\n", "โ”‚ โ€ข Your purchasing decisions propagate downstream โ”‚\n", "โ”‚ โ€ข Manage the bullwhip to minimize inventory costs โ”‚\n", "โ”‚ โ€ข Balance responsiveness vs. stability โ”‚\n", "โ”‚ โ”‚\n", "โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜\n", "\"\"\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## Part 10: Your Turn โ€” Experiment! ๐Ÿงช\n", "\n", "Adjust the parameters below and see how they affect the bullwhip:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•\n", "# ๐ŸŽฎ ADJUST THESE PARAMETERS AND RE-RUN THIS CELL!\n", "# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•\n", "\n", "MY_LEAD_TIME = 2 # Try: 1, 2, 3, 4, 5 weeks\n", "MY_FORECAST_PERIODS = 4 # Try: 2, 4, 6, 8 periods for moving average\n", "MY_SAFETY_STOCK = 1.5 # Try: 1.0, 1.5, 2.0, 2.5 (multiplier)\n", "MY_DEMAND_VARIABILITY = 10 # Try: 5, 10, 15, 20 (std deviation)\n", "\n", "# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•\n", "\n", "# Run with your parameters\n", "my_sim = SupplyChainSimulator(\n", " periods=52,\n", " base_demand=100,\n", " demand_std=MY_DEMAND_VARIABILITY,\n", " lead_time=MY_LEAD_TIME,\n", " forecast_periods=MY_FORECAST_PERIODS,\n", " safety_stock_factor=MY_SAFETY_STOCK\n", ")\n", "\n", "my_results = my_sim.run_simulation()\n", "my_amp = my_sim.calculate_amplification(my_results)\n", "\n", "# Quick visualization\n", "fig, ax = plt.subplots(figsize=(12, 5))\n", "ax.plot(my_results['Customer Demand'], label='Customer', color='#2ecc71', linewidth=2)\n", "ax.plot(my_results['Supplier Orders'], label='Supplier', color='#e74c3c', linewidth=2)\n", "ax.set_title(f'Your Settings: Lead Time={MY_LEAD_TIME}, Forecast={MY_FORECAST_PERIODS}, Safety={MY_SAFETY_STOCK}x', fontweight='bold')\n", "ax.set_xlabel('Week')\n", "ax.set_ylabel('Units')\n", "ax.legend()\n", "plt.tight_layout()\n", "plt.show()\n", "\n", "print(f\"\\n๐Ÿ“Š YOUR RESULTS:\")\n", "print(f\" Supplier amplification: {my_amp['Supplier Orders']:.1f}x\")\n", "print(f\" (Baseline with default settings was ~6-8x)\")\n", "print(f\"\\n {'๐Ÿ˜Š Lower is better!' if my_amp['Supplier Orders'] < 6 else '๐Ÿ“ˆ Try reducing lead time or forecast periods!'} \")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽ“ Summary: Key Takeaways\n", "\n", "| Concept | What You Learned |\n", "|---------|------------------|\n", "| **Bullwhip Effect** | Small demand changes amplify upstream |\n", "| **4 Causes** | Signal processing, batching, price fluctuations, shortage gaming |\n", "| **Lead Time Impact** | Longer lead times = worse amplification |\n", "| **Information Sharing** | POS data visibility dramatically reduces the effect |\n", "| **Python Advantage** | 500 simulations in seconds; Excel can't do this |\n", "\n", "---\n", "\n", "## ๐Ÿ“ Reflection Questions\n", "\n", "1. Why doesn't increasing safety stock reduce the bullwhip effect?\n", "\n", "2. How does Amazon's Prime 2-day shipping help reduce the bullwhip effect?\n", "\n", "3. In your Practice Operations simulation, what's one thing you'll do differently now?\n", "\n", "4. A supplier asks you to share your sales data. Based on what you learned, why should you agree?\n", "\n", "---\n", "\n", "**Great work! You've just learned one of the most important concepts in supply chain management.** ๐ŸŽ‰" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.10.0" } }, "nbformat": 4, "nbformat_minor": 4 }