mathiassandnes Claude Opus 4.5 commited on
Commit
bcf0c50
·
1 Parent(s): 406a69d

Overhaul Strategy page with new economic model

Browse files

- Add 8 new sliders: linear price, lease price, mfg cost, fleet size,
utilization rate, logistics cost, repair cost/month, cycle length
- Add cumulative earnings over time chart (0-120 months)
- Add scenario comparison: save/clear scenarios feature
- Add breakeven analysis with marker on chart
- Add cycle transition markers (vertical dashed lines)
- Update hero metrics: breakeven month, monthly net revenue, 10yr profit
- Update Fleet Scale Economics to use new model
- Replace cycle sensitivity with cycle length analysis
- Switch Strategy view to NOK currency (other views remain EUR)
- Add model parameters summary expander

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Files changed (1) hide show
  1. app.py +421 -77
app.py CHANGED
@@ -123,19 +123,73 @@ if "logistics" not in st.session_state:
123
  if "max_cycles_override" not in st.session_state:
124
  st.session_state.max_cycles_override = max_cycles
125
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  # Render sidebar based on selected view
127
  if selected_view == "Strategy":
128
- st.sidebar.header("💼 Strategy Controls")
129
- st.session_state.lease_price = st.sidebar.slider(
130
- "Monthly Lease Price", 10, 60, st.session_state.lease_price, format="€%d"
 
 
 
 
131
  )
132
- st.session_state.mfg_cost = st.sidebar.slider(
133
- "Manufacturing Cost", 50, 250, st.session_state.mfg_cost, step=10, format="€%d"
 
 
 
 
 
 
 
 
 
 
 
134
  )
135
- st.session_state.max_cycles_override = st.sidebar.slider(
136
- "Max Reuse Cycles", 1, 5, st.session_state.max_cycles_override
 
137
  )
138
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  elif selected_view == "Design":
140
  st.sidebar.header("🎨 Design Controls")
141
  refurb_pct = st.sidebar.slider(
@@ -216,6 +270,68 @@ elif operating_margin >= 0:
216
  else:
217
  model_status, status_color = "Loss", "#e74c3c"
218
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  # ============================================================
220
  # SHARED PRODUCT HEADER (compact)
221
  # ============================================================
@@ -275,56 +391,218 @@ st.markdown("---")
275
  # ============================================================
276
  if selected_view == "Strategy":
277
  st.header("Strategy: Business Model at Scale")
278
- st.markdown("*For executives, investors, and board members*")
279
-
280
- # Hero metrics
281
- fleet_5k = 5000
282
- customers_5k = fleet_5k * max_cycles
283
- circ_cost_5k = fleet_5k * new_unit_cost + fleet_5k * (max_cycles - 1) * refurb_unit_cost
284
- circ_profit_5k = customers_5k * revenue_per_customer - circ_cost_5k
285
- lin_cost_5k = customers_5k * new_unit_cost
286
- lin_profit_5k = customers_5k * revenue_per_customer - lin_cost_5k
287
- profit_advantage_5k = circ_profit_5k - lin_profit_5k
288
- products_saved_5k = customers_5k - fleet_5k
289
- co2_per_unit = config["environmental"]["co2_manufacturing_kg"]
290
- co2_saved_5k = products_saved_5k * co2_per_unit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
 
292
  hero_cols = st.columns(4)
293
  with hero_cols[0]:
294
- st.metric("Profit Advantage", f"€{profit_advantage_5k:,.0f}", delta="vs linear model")
 
 
 
 
 
295
  with hero_cols[1]:
296
- st.metric("Products Saved", f"{products_saved_5k:,}", delta=f"at {fleet_5k:,} fleet")
 
 
 
 
 
297
  with hero_cols[2]:
298
- st.metric("CO2 Avoided", f"{co2_saved_5k:,.0f} kg", delta="fewer manufactured")
 
 
 
 
 
 
299
  with hero_cols[3]:
300
- st.metric("Customers Served", f"{customers_5k:,}", delta=f"{max_cycles} cycles")
 
 
 
 
 
 
 
 
 
301
 
302
  st.markdown("---")
303
 
304
  # Fleet Scale Economics
305
  st.subheader("Fleet Scale Economics: Volume Advantage")
306
 
 
307
  fleet_sizes = [500, 1000, 2000, 5000]
308
  scale_data = []
 
 
309
  for fleet in fleet_sizes:
310
- customers_served = fleet * max_cycles
311
- circular_total_cost = fleet * new_unit_cost + fleet * (max_cycles - 1) * refurb_unit_cost
312
- circular_revenue = customers_served * revenue_per_customer
313
- circular_profit = circular_revenue - circular_total_cost
314
- linear_cost = customers_served * new_unit_cost
315
- linear_revenue = customers_served * revenue_per_customer
316
- linear_profit = linear_revenue - linear_cost
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  products_saved = customers_served - fleet
318
  co2_saved = products_saved * co2_per_unit
319
- profit_advantage = circular_profit - linear_profit
320
 
321
  scale_data.append({
322
  "Fleet Size": fleet,
323
  "Fleet Label": f"{fleet:,}",
324
  "Customers Served": customers_served,
325
- "Circular Profit": circular_profit,
326
  "Linear Profit": linear_profit,
327
- "Profit Advantage": profit_advantage,
328
  "Products Saved": products_saved,
329
  "CO2 Saved (kg)": co2_saved,
330
  "Linear Products Needed": customers_served,
@@ -335,22 +613,20 @@ if selected_view == "Strategy":
335
  scale_col1, scale_col2 = st.columns(2)
336
 
337
  with scale_col1:
338
- # Fixed chart: Show Profit Advantage as the primary metric
339
  fig_advantage = go.Figure()
340
  fig_advantage.add_trace(go.Bar(
341
  x=scale_df["Fleet Label"],
342
  y=scale_df["Profit Advantage"],
343
  name="Circular Profit Advantage",
344
- marker_color="#2ecc71",
345
- text=[f"€{v:,.0f}" for v in scale_df["Profit Advantage"]],
346
  textposition="outside",
347
  ))
348
  fig_advantage.update_layout(
349
- title="Profit Advantage: Circular vs Linear",
350
  xaxis_title="Fleet Size",
351
- yaxis_title="Extra Profit from Circular Model (EUR)",
352
  showlegend=False,
353
- yaxis=dict(rangemode='tozero'),
354
  )
355
  st.plotly_chart(fig_advantage, use_container_width=True)
356
 
@@ -367,41 +643,86 @@ if selected_view == "Strategy":
367
  text=[f"{v:,}" for v in scale_df["Linear Products Needed"]], textposition="outside",
368
  ))
369
  fig_products.update_layout(
370
- title="Products Needed to Serve Same Customers",
371
  xaxis_title="Fleet Size Target", yaxis_title="Products to Manufacture",
372
  barmode="group", legend=dict(orientation="h", yanchor="bottom", y=1.02),
373
  yaxis=dict(rangemode='tozero'),
374
  )
375
  st.plotly_chart(fig_products, use_container_width=True)
376
 
377
- # Business case summary
378
- key_data = scale_df[scale_df["Fleet Size"] == 5000].iloc[0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
  st.success(f"""
380
- **The Business Case:** With **{5000:,} products** in our fleet, we serve **{key_data['Customers Served']:,} families**
381
- and generate **€{key_data['Profit Advantage']:,.0f} more profit** than a linear model.
382
 
383
- A linear model would need **{key_data['Linear Products Needed']:,} products** to serve the same customers.
384
 
385
- Environmental impact: **{key_data['CO2 Saved (kg)']:,.0f} kg CO2 saved**.
386
  """)
387
 
388
  st.markdown("---")
389
 
390
  # Cycle Sensitivity
391
- st.subheader("Sensitivity: Impact of Reuse Cycles")
392
 
 
 
393
  sensitivity_data = []
394
- for test_cycles in range(1, 6):
395
- circ_cost = new_unit_cost + (test_cycles - 1) * refurb_unit_cost
396
- circ_revenue = test_cycles * revenue_per_customer
397
- circ_profit = circ_revenue - circ_cost
398
- lin_cost = test_cycles * new_unit_cost
399
- lin_profit = test_cycles * revenue_per_customer - lin_cost
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
400
 
401
  sensitivity_data.append({
402
- "Cycles": test_cycles,
403
- "Profit Advantage": circ_profit - lin_profit,
404
- "Cost Saved": lin_cost - circ_cost,
 
 
 
405
  })
406
 
407
  sens_df = pd.DataFrame(sensitivity_data)
@@ -411,44 +732,67 @@ if selected_view == "Strategy":
411
  with sens_col1:
412
  fig_sens = go.Figure()
413
  fig_sens.add_trace(go.Bar(
414
- x=sens_df["Cycles"], y=sens_df["Profit Advantage"],
415
- marker_color=["#e74c3c" if x <= 0 else "#2ecc71" for x in sens_df["Profit Advantage"]],
416
- text=[f"€{v:,.0f}" for v in sens_df["Profit Advantage"]], textposition="outside",
417
  ))
418
  fig_sens.update_layout(
419
- title="Profit Advantage by # of Cycles",
420
- xaxis_title="Number of Use Cycles", yaxis_title="Profit Advantage (EUR)",
421
- yaxis=dict(rangemode='tozero'),
422
  )
423
- fig_sens.add_vline(x=max_cycles, line_dash="dash", line_color="blue",
424
- annotation_text=f"Current: {max_cycles}")
 
 
 
 
 
 
425
  st.plotly_chart(fig_sens, use_container_width=True)
426
 
427
  with sens_col2:
428
- fig_cost = go.Figure()
429
- fig_cost.add_trace(go.Bar(
430
- x=sens_df["Cycles"], y=sens_df["Cost Saved"],
431
  marker_color="#3498db",
432
- text=[f"€{v:,.0f}" for v in sens_df["Cost Saved"]], textposition="outside",
433
  ))
434
- fig_cost.update_layout(
435
- title="Cost Savings from Reuse",
436
- xaxis_title="Number of Use Cycles", yaxis_title="Cost Saved vs Linear (EUR)",
437
  yaxis=dict(rangemode='tozero'),
438
  )
439
- st.plotly_chart(fig_cost, use_container_width=True)
440
 
441
  # Summary table
442
  with st.expander("Full Scale Comparison Table"):
443
- display_df = scale_df[["Fleet Label", "Customers Served", "Circular Profit", "Linear Profit",
444
  "Profit Advantage", "Products Saved", "CO2 Saved (kg)"]].copy()
445
- display_df.columns = ["Fleet", "Customers", "Circular Profit", "Linear Profit",
446
  "Advantage", "Products Saved", "CO2 Saved"]
447
  st.dataframe(display_df.style.format({
448
- "Customers": "{:,}", "Circular Profit": "€{:,.0f}", "Linear Profit": "€{:,.0f}",
449
- "Advantage": "€{:,.0f}", "Products Saved": "{:,}", "CO2 Saved": "{:,.0f} kg",
450
  }), use_container_width=True, hide_index=True)
451
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
452
  # ============================================================
453
  # VIEW: DESIGN
454
  # ============================================================
 
123
  if "max_cycles_override" not in st.session_state:
124
  st.session_state.max_cycles_override = max_cycles
125
 
126
+ # Strategy view session state (NOK currency - approximately EUR × 10)
127
+ if "saved_scenarios" not in st.session_state:
128
+ st.session_state.saved_scenarios = []
129
+ if "strategy_linear_price" not in st.session_state:
130
+ st.session_state.strategy_linear_price = int(retail_price * 10) # EUR to NOK
131
+ if "strategy_lease_price" not in st.session_state:
132
+ st.session_state.strategy_lease_price = int(default_lease_price * 10) # EUR to NOK
133
+ if "strategy_mfg_cost" not in st.session_state:
134
+ st.session_state.strategy_mfg_cost = int(default_mfg_cost * 10) # EUR to NOK
135
+ if "strategy_fleet_size" not in st.session_state:
136
+ st.session_state.strategy_fleet_size = 1000
137
+ if "strategy_utilization" not in st.session_state:
138
+ st.session_state.strategy_utilization = 85
139
+ if "strategy_logistics_cost" not in st.session_state:
140
+ st.session_state.strategy_logistics_cost = int(default_logistics * 10) # EUR to NOK
141
+ if "strategy_repair_cost" not in st.session_state:
142
+ st.session_state.strategy_repair_cost = 50
143
+ if "strategy_cycle_length" not in st.session_state:
144
+ st.session_state.strategy_cycle_length = 24
145
+
146
  # Render sidebar based on selected view
147
  if selected_view == "Strategy":
148
+ st.sidebar.header("💼 Strategy Controls (NOK)")
149
+ st.sidebar.caption("Economic model for product lifespan profitability")
150
+
151
+ st.sidebar.subheader("Pricing")
152
+ st.session_state.strategy_linear_price = st.sidebar.slider(
153
+ "Linear Model Price", 500, 2000, st.session_state.strategy_linear_price, step=50, format="%d NOK",
154
+ help="What customers pay to buy outright"
155
  )
156
+ st.session_state.strategy_lease_price = st.sidebar.slider(
157
+ "Monthly Lease Price", 100, 600, st.session_state.strategy_lease_price, step=10, format="%d NOK",
158
+ help="Monthly revenue per leased product"
159
+ )
160
+
161
+ st.sidebar.subheader("Costs")
162
+ st.session_state.strategy_mfg_cost = st.sidebar.slider(
163
+ "Manufacturing Cost", 500, 2500, st.session_state.strategy_mfg_cost, step=50, format="%d NOK",
164
+ help="Cost to produce one unit"
165
+ )
166
+ st.session_state.strategy_logistics_cost = st.sidebar.slider(
167
+ "Logistics Cost", 50, 400, st.session_state.strategy_logistics_cost, step=10, format="%d NOK",
168
+ help="Cost between cycles (pickup + delivery)"
169
  )
170
+ st.session_state.strategy_repair_cost = st.sidebar.slider(
171
+ "Repair Cost/Month", 10, 100, st.session_state.strategy_repair_cost, step=5, format="%d NOK",
172
+ help="Monthly degradation cost (wear & tear)"
173
  )
174
 
175
+ st.sidebar.subheader("Fleet & Operations")
176
+ st.session_state.strategy_fleet_size = st.sidebar.slider(
177
+ "Fleet Size", 100, 10000, st.session_state.strategy_fleet_size, step=100,
178
+ help="Total products in fleet"
179
+ )
180
+ st.session_state.strategy_utilization = st.sidebar.slider(
181
+ "Utilization Rate", 50, 100, st.session_state.strategy_utilization, format="%d%%",
182
+ help="Percentage of fleet currently leased out"
183
+ )
184
+ st.session_state.strategy_cycle_length = st.sidebar.slider(
185
+ "Cycle Length", 12, 60, st.session_state.strategy_cycle_length, step=6, format="%d months",
186
+ help="Duration of one lease cycle"
187
+ )
188
+
189
+ # Keep EUR values in sync for other views (convert NOK back to EUR)
190
+ st.session_state.lease_price = st.session_state.strategy_lease_price // 10
191
+ st.session_state.mfg_cost = st.session_state.strategy_mfg_cost // 10
192
+
193
  elif selected_view == "Design":
194
  st.sidebar.header("🎨 Design Controls")
195
  refurb_pct = st.sidebar.slider(
 
270
  else:
271
  model_status, status_color = "Loss", "#e74c3c"
272
 
273
+
274
+ # ============================================================
275
+ # STRATEGY: EARNINGS CALCULATION FUNCTION
276
+ # ============================================================
277
+ def calculate_cumulative_earnings(
278
+ fleet_size: int,
279
+ utilization_rate: float,
280
+ lease_price: float,
281
+ mfg_cost: float,
282
+ logistics_cost: float,
283
+ repair_cost_per_month: float,
284
+ cycle_length_months: int,
285
+ max_months: int = 120,
286
+ ) -> pd.DataFrame:
287
+ """
288
+ Calculate cumulative earnings over time for a circular lease model.
289
+
290
+ Returns DataFrame with columns: month, cumulative_profit, cycle_transitions
291
+ """
292
+ products_in_use = fleet_size * utilization_rate
293
+
294
+ # Initial investment (manufacturing cost for entire fleet)
295
+ initial_investment = fleet_size * mfg_cost
296
+
297
+ data = []
298
+ cumulative_profit = -initial_investment # Start negative (investment)
299
+ cycle_transitions = 0
300
+
301
+ for month in range(max_months + 1):
302
+ if month == 0:
303
+ data.append({
304
+ "month": month,
305
+ "cumulative_profit": cumulative_profit,
306
+ "is_cycle_transition": False,
307
+ })
308
+ continue
309
+
310
+ # Monthly revenue from products in use
311
+ monthly_revenue = products_in_use * lease_price
312
+
313
+ # Monthly degradation/repair cost
314
+ monthly_degradation = products_in_use * repair_cost_per_month
315
+
316
+ # Check for cycle transition (every cycle_length_months)
317
+ is_transition = (month % cycle_length_months == 0) and (month > 0)
318
+ transition_cost = 0
319
+ if is_transition:
320
+ # Logistics cost for the entire fleet at cycle end
321
+ transition_cost = fleet_size * logistics_cost
322
+ cycle_transitions += 1
323
+
324
+ # Update cumulative profit
325
+ cumulative_profit += monthly_revenue - monthly_degradation - transition_cost
326
+
327
+ data.append({
328
+ "month": month,
329
+ "cumulative_profit": cumulative_profit,
330
+ "is_cycle_transition": is_transition,
331
+ })
332
+
333
+ return pd.DataFrame(data)
334
+
335
  # ============================================================
336
  # SHARED PRODUCT HEADER (compact)
337
  # ============================================================
 
391
  # ============================================================
392
  if selected_view == "Strategy":
393
  st.header("Strategy: Business Model at Scale")
394
+ st.markdown("*For executives, investors, and board members — All values in NOK*")
395
+
396
+ # Get Strategy slider values
397
+ s_fleet_size = st.session_state.strategy_fleet_size
398
+ s_utilization = st.session_state.strategy_utilization / 100 # Convert to decimal
399
+ s_lease_price = st.session_state.strategy_lease_price
400
+ s_mfg_cost = st.session_state.strategy_mfg_cost
401
+ s_logistics = st.session_state.strategy_logistics_cost
402
+ s_repair_cost = st.session_state.strategy_repair_cost
403
+ s_cycle_length = st.session_state.strategy_cycle_length
404
+ s_linear_price = st.session_state.strategy_linear_price
405
+
406
+ # ============================================================
407
+ # EARNINGS OVER TIME CHART
408
+ # ============================================================
409
+ st.subheader("Cumulative Earnings Over Time")
410
+
411
+ # Calculate current scenario
412
+ max_display_months = 120
413
+ current_earnings = calculate_cumulative_earnings(
414
+ fleet_size=s_fleet_size,
415
+ utilization_rate=s_utilization,
416
+ lease_price=s_lease_price,
417
+ mfg_cost=s_mfg_cost,
418
+ logistics_cost=s_logistics,
419
+ repair_cost_per_month=s_repair_cost,
420
+ cycle_length_months=s_cycle_length,
421
+ max_months=max_display_months,
422
+ )
423
+
424
+ # Find breakeven month
425
+ breakeven_month = None
426
+ for idx, row in current_earnings.iterrows():
427
+ if row["cumulative_profit"] >= 0 and idx > 0:
428
+ breakeven_month = row["month"]
429
+ break
430
+
431
+ # Create the chart
432
+ fig_earnings = go.Figure()
433
+
434
+ # Add saved scenarios first (so current is on top)
435
+ scenario_colors = ["#9b59b6", "#e67e22", "#1abc9c", "#e74c3c", "#3498db"]
436
+ for i, scenario in enumerate(st.session_state.saved_scenarios):
437
+ color = scenario_colors[i % len(scenario_colors)]
438
+ fig_earnings.add_trace(go.Scatter(
439
+ x=scenario["data"]["month"],
440
+ y=scenario["data"]["cumulative_profit"],
441
+ mode="lines",
442
+ name=scenario["name"],
443
+ line=dict(color=color, width=2, dash="dot"),
444
+ opacity=0.7,
445
+ ))
446
+
447
+ # Add current scenario
448
+ fig_earnings.add_trace(go.Scatter(
449
+ x=current_earnings["month"],
450
+ y=current_earnings["cumulative_profit"],
451
+ mode="lines",
452
+ name="Current Scenario",
453
+ line=dict(color="#2ecc71", width=3),
454
+ ))
455
+
456
+ # Add breakeven marker
457
+ if breakeven_month:
458
+ breakeven_profit = current_earnings[current_earnings["month"] == breakeven_month]["cumulative_profit"].values[0]
459
+ fig_earnings.add_trace(go.Scatter(
460
+ x=[breakeven_month],
461
+ y=[breakeven_profit],
462
+ mode="markers+text",
463
+ name=f"Breakeven (Month {breakeven_month})",
464
+ marker=dict(size=12, color="#f1c40f", symbol="star"),
465
+ text=[f"Month {breakeven_month}"],
466
+ textposition="top center",
467
+ ))
468
+
469
+ # Add cycle transition markers (vertical dashed lines)
470
+ for month in range(s_cycle_length, max_display_months + 1, s_cycle_length):
471
+ fig_earnings.add_vline(
472
+ x=month, line_dash="dash", line_color="rgba(255,255,255,0.3)",
473
+ annotation_text=f"Cycle {month // s_cycle_length}" if month <= s_cycle_length * 3 else None,
474
+ annotation_position="top",
475
+ )
476
+
477
+ # Add zero line (breakeven threshold)
478
+ fig_earnings.add_hline(y=0, line_color="rgba(255,255,255,0.5)", line_width=1)
479
+
480
+ fig_earnings.update_layout(
481
+ xaxis_title="Months",
482
+ yaxis_title="Cumulative Profit (NOK)",
483
+ legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="left", x=0),
484
+ hovermode="x unified",
485
+ )
486
+
487
+ st.plotly_chart(fig_earnings, use_container_width=True)
488
+
489
+ # Save/Clear scenario buttons
490
+ btn_col1, btn_col2, btn_col3 = st.columns([1, 1, 3])
491
+ with btn_col1:
492
+ if st.button("💾 Save Current Scenario", use_container_width=True):
493
+ scenario_num = len(st.session_state.saved_scenarios) + 1
494
+ st.session_state.saved_scenarios.append({
495
+ "name": f"Scenario {scenario_num}",
496
+ "params": {
497
+ "fleet_size": s_fleet_size,
498
+ "utilization": s_utilization,
499
+ "lease_price": s_lease_price,
500
+ "mfg_cost": s_mfg_cost,
501
+ "logistics": s_logistics,
502
+ "repair_cost": s_repair_cost,
503
+ "cycle_length": s_cycle_length,
504
+ },
505
+ "data": current_earnings.to_dict(),
506
+ })
507
+ st.rerun()
508
+ with btn_col2:
509
+ if st.button("🗑️ Clear All Scenarios", use_container_width=True):
510
+ st.session_state.saved_scenarios = []
511
+ st.rerun()
512
+
513
+ st.markdown("---")
514
+
515
+ # ============================================================
516
+ # HERO METRICS (updated for new model)
517
+ # ============================================================
518
+ # Calculate key metrics
519
+ products_in_use = s_fleet_size * s_utilization
520
+ monthly_net_revenue = products_in_use * (s_lease_price - s_repair_cost)
521
+ annual_profit = monthly_net_revenue * 12 - (s_fleet_size * s_logistics) # One cycle transition per year approx
522
+
523
+ # Linear model comparison: selling s_fleet_size products
524
+ linear_profit = s_fleet_size * (s_linear_price - s_mfg_cost)
525
+
526
+ # Minimum lifespan calculation (months to breakeven)
527
+ min_lifespan = breakeven_month if breakeven_month else "> 120"
528
 
529
  hero_cols = st.columns(4)
530
  with hero_cols[0]:
531
+ st.metric(
532
+ "Breakeven",
533
+ f"Month {breakeven_month}" if breakeven_month else "Not reached",
534
+ delta="profitable after" if breakeven_month else "needs adjustment",
535
+ help="When cumulative profit crosses zero"
536
+ )
537
  with hero_cols[1]:
538
+ st.metric(
539
+ "Monthly Net Revenue",
540
+ f"{monthly_net_revenue:,.0f} NOK",
541
+ delta=f"{products_in_use:,.0f} products in use",
542
+ help="(Lease price - repair cost) × products in use"
543
+ )
544
  with hero_cols[2]:
545
+ profit_at_120 = current_earnings[current_earnings["month"] == 120]["cumulative_profit"].values[0]
546
+ st.metric(
547
+ "Profit at Year 10",
548
+ f"{profit_at_120:,.0f} NOK",
549
+ delta="positive" if profit_at_120 > 0 else "loss",
550
+ help="Cumulative profit after 120 months"
551
+ )
552
  with hero_cols[3]:
553
+ # Compare circular vs linear (10-year horizon)
554
+ circular_10yr = profit_at_120
555
+ linear_10yr = linear_profit # One-time sale
556
+ advantage = circular_10yr - linear_10yr
557
+ st.metric(
558
+ "vs Linear (10yr)",
559
+ f"{advantage:+,.0f} NOK",
560
+ delta="circular wins" if advantage > 0 else "linear wins",
561
+ help="Circular 10yr profit minus one-time linear sale profit"
562
+ )
563
 
564
  st.markdown("---")
565
 
566
  # Fleet Scale Economics
567
  st.subheader("Fleet Scale Economics: Volume Advantage")
568
 
569
+ # Calculate 10-year profit for different fleet sizes using the new model
570
  fleet_sizes = [500, 1000, 2000, 5000]
571
  scale_data = []
572
+ co2_per_unit = config["environmental"]["co2_manufacturing_kg"]
573
+
574
  for fleet in fleet_sizes:
575
+ # Calculate 10-year (120 month) earnings for this fleet size
576
+ earnings_df = calculate_cumulative_earnings(
577
+ fleet_size=fleet,
578
+ utilization_rate=s_utilization,
579
+ lease_price=s_lease_price,
580
+ mfg_cost=s_mfg_cost,
581
+ logistics_cost=s_logistics,
582
+ repair_cost_per_month=s_repair_cost,
583
+ cycle_length_months=s_cycle_length,
584
+ max_months=120,
585
+ )
586
+ circular_profit_10yr = earnings_df[earnings_df["month"] == 120]["cumulative_profit"].values[0]
587
+
588
+ # Linear model: one-time sale
589
+ linear_profit = fleet * (s_linear_price - s_mfg_cost)
590
+
591
+ # Number of cycles in 10 years
592
+ num_cycles = 120 // s_cycle_length
593
+ customers_served = fleet * num_cycles
594
+
595
+ # Products saved (circular reuses, linear needs one per customer)
596
  products_saved = customers_served - fleet
597
  co2_saved = products_saved * co2_per_unit
 
598
 
599
  scale_data.append({
600
  "Fleet Size": fleet,
601
  "Fleet Label": f"{fleet:,}",
602
  "Customers Served": customers_served,
603
+ "Circular Profit (10yr)": circular_profit_10yr,
604
  "Linear Profit": linear_profit,
605
+ "Profit Advantage": circular_profit_10yr - linear_profit,
606
  "Products Saved": products_saved,
607
  "CO2 Saved (kg)": co2_saved,
608
  "Linear Products Needed": customers_served,
 
613
  scale_col1, scale_col2 = st.columns(2)
614
 
615
  with scale_col1:
 
616
  fig_advantage = go.Figure()
617
  fig_advantage.add_trace(go.Bar(
618
  x=scale_df["Fleet Label"],
619
  y=scale_df["Profit Advantage"],
620
  name="Circular Profit Advantage",
621
+ marker_color=["#2ecc71" if v > 0 else "#e74c3c" for v in scale_df["Profit Advantage"]],
622
+ text=[f"{v:,.0f} NOK" for v in scale_df["Profit Advantage"]],
623
  textposition="outside",
624
  ))
625
  fig_advantage.update_layout(
626
+ title="10-Year Profit Advantage: Circular vs Linear",
627
  xaxis_title="Fleet Size",
628
+ yaxis_title="Extra Profit from Circular Model (NOK)",
629
  showlegend=False,
 
630
  )
631
  st.plotly_chart(fig_advantage, use_container_width=True)
632
 
 
643
  text=[f"{v:,}" for v in scale_df["Linear Products Needed"]], textposition="outside",
644
  ))
645
  fig_products.update_layout(
646
+ title="Products Needed to Serve Same Customers (10yr)",
647
  xaxis_title="Fleet Size Target", yaxis_title="Products to Manufacture",
648
  barmode="group", legend=dict(orientation="h", yanchor="bottom", y=1.02),
649
  yaxis=dict(rangemode='tozero'),
650
  )
651
  st.plotly_chart(fig_products, use_container_width=True)
652
 
653
+ # Business case summary using current fleet size
654
+ current_fleet_data = None
655
+ for row in scale_data:
656
+ if row["Fleet Size"] == s_fleet_size or row == scale_data[-1]:
657
+ current_fleet_data = row
658
+ if row["Fleet Size"] == s_fleet_size:
659
+ break
660
+
661
+ # If current fleet size not in list, calculate it
662
+ if current_fleet_data["Fleet Size"] != s_fleet_size:
663
+ current_10yr = current_earnings[current_earnings["month"] == 120]["cumulative_profit"].values[0]
664
+ num_cycles = 120 // s_cycle_length
665
+ customers_served = s_fleet_size * num_cycles
666
+ products_saved = customers_served - s_fleet_size
667
+ current_fleet_data = {
668
+ "Fleet Size": s_fleet_size,
669
+ "Customers Served": customers_served,
670
+ "Circular Profit (10yr)": current_10yr,
671
+ "Linear Profit": s_fleet_size * (s_linear_price - s_mfg_cost),
672
+ "Profit Advantage": current_10yr - s_fleet_size * (s_linear_price - s_mfg_cost),
673
+ "Products Saved": products_saved,
674
+ "CO2 Saved (kg)": products_saved * co2_per_unit,
675
+ }
676
+
677
  st.success(f"""
678
+ **The Business Case:** With **{s_fleet_size:,} products** in our fleet at **{s_utilization*100:.0f}% utilization**,
679
+ we serve **{current_fleet_data['Customers Served']:,} families** over 10 years.
680
 
681
+ Circular model generates **{current_fleet_data['Profit Advantage']:+,.0f} NOK** compared to linear model.
682
 
683
+ Environmental impact: **{current_fleet_data['CO2 Saved (kg)']:,.0f} kg CO2 saved** ({current_fleet_data['Products Saved']:,} fewer products manufactured).
684
  """)
685
 
686
  st.markdown("---")
687
 
688
  # Cycle Sensitivity
689
+ st.subheader("Sensitivity: Impact of Cycle Length")
690
 
691
+ # Test different cycle lengths
692
+ cycle_lengths = [12, 18, 24, 36, 48, 60]
693
  sensitivity_data = []
694
+
695
+ for test_cycle in cycle_lengths:
696
+ # Calculate 10-year profit for this cycle length
697
+ earnings_df = calculate_cumulative_earnings(
698
+ fleet_size=s_fleet_size,
699
+ utilization_rate=s_utilization,
700
+ lease_price=s_lease_price,
701
+ mfg_cost=s_mfg_cost,
702
+ logistics_cost=s_logistics,
703
+ repair_cost_per_month=s_repair_cost,
704
+ cycle_length_months=test_cycle,
705
+ max_months=120,
706
+ )
707
+ profit_10yr = earnings_df[earnings_df["month"] == 120]["cumulative_profit"].values[0]
708
+
709
+ # Find breakeven
710
+ be_month = None
711
+ for idx, row in earnings_df.iterrows():
712
+ if row["cumulative_profit"] >= 0 and idx > 0:
713
+ be_month = row["month"]
714
+ break
715
+
716
+ num_cycles = 120 // test_cycle
717
+ num_transitions = num_cycles # Logistics cost at each cycle end
718
 
719
  sensitivity_data.append({
720
+ "Cycle Length": f"{test_cycle}mo",
721
+ "Cycle Months": test_cycle,
722
+ "10yr Profit": profit_10yr,
723
+ "Breakeven Month": be_month if be_month else 120,
724
+ "Num Cycles": num_cycles,
725
+ "Total Logistics": num_transitions * s_fleet_size * s_logistics,
726
  })
727
 
728
  sens_df = pd.DataFrame(sensitivity_data)
 
732
  with sens_col1:
733
  fig_sens = go.Figure()
734
  fig_sens.add_trace(go.Bar(
735
+ x=sens_df["Cycle Length"], y=sens_df["10yr Profit"],
736
+ marker_color=["#2ecc71" if x > 0 else "#e74c3c" for x in sens_df["10yr Profit"]],
737
+ text=[f"{v:,.0f} NOK" for v in sens_df["10yr Profit"]], textposition="outside",
738
  ))
739
  fig_sens.update_layout(
740
+ title="10-Year Profit by Cycle Length",
741
+ xaxis_title="Cycle Length", yaxis_title="Cumulative Profit (NOK)",
 
742
  )
743
+ # Mark current cycle length
744
+ current_cycle_label = f"{s_cycle_length}mo"
745
+ if current_cycle_label in sens_df["Cycle Length"].values:
746
+ fig_sens.add_vline(
747
+ x=sens_df[sens_df["Cycle Length"] == current_cycle_label].index[0],
748
+ line_dash="dash", line_color="#f1c40f",
749
+ annotation_text=f"Current: {s_cycle_length}mo"
750
+ )
751
  st.plotly_chart(fig_sens, use_container_width=True)
752
 
753
  with sens_col2:
754
+ fig_be = go.Figure()
755
+ fig_be.add_trace(go.Bar(
756
+ x=sens_df["Cycle Length"], y=sens_df["Breakeven Month"],
757
  marker_color="#3498db",
758
+ text=[f"{v:.0f}mo" for v in sens_df["Breakeven Month"]], textposition="outside",
759
  ))
760
+ fig_be.update_layout(
761
+ title="Breakeven Month by Cycle Length",
762
+ xaxis_title="Cycle Length", yaxis_title="Months to Breakeven",
763
  yaxis=dict(rangemode='tozero'),
764
  )
765
+ st.plotly_chart(fig_be, use_container_width=True)
766
 
767
  # Summary table
768
  with st.expander("Full Scale Comparison Table"):
769
+ display_df = scale_df[["Fleet Label", "Customers Served", "Circular Profit (10yr)", "Linear Profit",
770
  "Profit Advantage", "Products Saved", "CO2 Saved (kg)"]].copy()
771
+ display_df.columns = ["Fleet", "Customers (10yr)", "Circular Profit", "Linear Profit",
772
  "Advantage", "Products Saved", "CO2 Saved"]
773
  st.dataframe(display_df.style.format({
774
+ "Customers (10yr)": "{:,}", "Circular Profit": "{:,.0f} NOK", "Linear Profit": "{:,.0f} NOK",
775
+ "Advantage": "{:+,.0f} NOK", "Products Saved": "{:,}", "CO2 Saved": "{:,.0f} kg",
776
  }), use_container_width=True, hide_index=True)
777
 
778
+ # Model parameters summary
779
+ with st.expander("Current Model Parameters"):
780
+ params_col1, params_col2, params_col3 = st.columns(3)
781
+ with params_col1:
782
+ st.markdown("**Pricing**")
783
+ st.write(f"- Linear Price: {s_linear_price:,} NOK")
784
+ st.write(f"- Monthly Lease: {s_lease_price:,} NOK")
785
+ with params_col2:
786
+ st.markdown("**Costs**")
787
+ st.write(f"- Manufacturing: {s_mfg_cost:,} NOK")
788
+ st.write(f"- Logistics/cycle: {s_logistics:,} NOK")
789
+ st.write(f"- Repair/month: {s_repair_cost:,} NOK")
790
+ with params_col3:
791
+ st.markdown("**Operations**")
792
+ st.write(f"- Fleet Size: {s_fleet_size:,}")
793
+ st.write(f"- Utilization: {s_utilization*100:.0f}%")
794
+ st.write(f"- Cycle Length: {s_cycle_length} months")
795
+
796
  # ============================================================
797
  # VIEW: DESIGN
798
  # ============================================================