Spaces:
Running
Running
Download strategy_v2.py from Liisdotcom/simulator: direct link, hf CLI and curl.
- Browser
- Download file 16.9 kB
-
https://huggingface.co/spaces/Liisdotcom/simulator/resolve/main/strategy_v2.py
- Command line
-
hf download hf://spaces/Liisdotcom/simulator/strategy_v2.py
-
curl -L -o strategy_v2.py https://huggingface.co/spaces/Liisdotcom/simulator/resolve/main/strategy_v2.py
16.9 kB
| """ | |
| LiiS Circular Economy Simulator - C-Level View (Draft) | |
| Simplified executive dashboard focused on portfolio-level economics. | |
| Run with: streamlit run strategy_v2.py | |
| """ | |
| import streamlit as st | |
| import plotly.graph_objects as go | |
| import pandas as pd | |
| import plotly.io as pio | |
| # ── LiiS Brand Palette ── | |
| COLORS = { | |
| "bg": "#f9fcf3", | |
| "surface": "#ffffff", | |
| "surface_alt": "#eef5e6", | |
| "primary": "#124b5f", | |
| "secondary": "#785964", | |
| "text": "#1a1a2e", | |
| "text_muted": "#4a5568", | |
| "text_subtle": "#718096", | |
| "border": "#d1ddc8", | |
| "positive": "#124b5f", | |
| "negative": "#a0444a", | |
| "warning": "#c87941", | |
| "info": "#4a7c9b", | |
| } | |
| CHART_COLORS = ["#124b5f", "#785964", "#2a7f62", "#c87941", "#4a7c9b", "#a0444a"] | |
| LIIS_LAYOUT = dict( | |
| paper_bgcolor=COLORS["bg"], | |
| plot_bgcolor=COLORS["surface"], | |
| font=dict(family="Futura, Century Gothic, Arial, sans-serif", color=COLORS["text"]), | |
| xaxis=dict(gridcolor="#e2e8f0", zerolinecolor=COLORS["border"]), | |
| yaxis=dict(gridcolor="#e2e8f0", zerolinecolor=COLORS["border"]), | |
| colorway=CHART_COLORS, | |
| legend=dict(orientation="h", yanchor="bottom", y=1.02), | |
| hovermode="x unified", | |
| ) | |
| liis_template = pio.templates["plotly"] | |
| liis_template.layout.update(LIIS_LAYOUT) | |
| pio.templates["liis"] = liis_template | |
| pio.templates.default = "liis" | |
| # Page config | |
| st.set_page_config( | |
| page_title="LiiS Strategy", | |
| page_icon="💼", | |
| layout="wide", | |
| ) | |
| # Custom CSS — LiiS brand | |
| st.markdown(f""" | |
| <style> | |
| .main-header {{ | |
| background: linear-gradient(135deg, {COLORS['primary']} 0%, #1a6b7a 100%); | |
| padding: 1.5rem 2rem; | |
| border-radius: 12px; | |
| margin-bottom: 1.5rem; | |
| }} | |
| .main-header h1 {{ color: white; margin: 0; font-size: 2rem; font-family: Futura, Century Gothic, Arial, sans-serif; }} | |
| .main-header p {{ color: rgba(255,255,255,0.9); margin: 0.5rem 0 0 0; }} | |
| [data-testid="stMetricValue"] {{ font-size: 2rem; color: {COLORS['primary']}; }} | |
| .hero-metric {{ | |
| background: {COLORS['surface']}; | |
| padding: 1.5rem; | |
| border-radius: 12px; | |
| border-left: 4px solid {COLORS['primary']}; | |
| border: 1px solid {COLORS['border']}; | |
| border-left: 4px solid {COLORS['primary']}; | |
| margin-bottom: 1rem; | |
| }} | |
| .hero-metric h2 {{ color: {COLORS['primary']}; margin: 0; font-size: 2.5rem; }} | |
| .hero-metric p {{ color: {COLORS['text_subtle']}; margin: 0.3rem 0 0 0; font-size: 0.9rem; }} | |
| .tier-header {{ | |
| color: {COLORS['text_subtle']}; | |
| font-size: 0.75rem; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| margin-bottom: 0.5rem; | |
| }} | |
| .comparison-card {{ | |
| background: {COLORS['surface']}; | |
| padding: 1.5rem; | |
| border-radius: 12px; | |
| text-align: center; | |
| border: 1px solid {COLORS['border']}; | |
| }} | |
| .winner {{ border: 2px solid {COLORS['primary']}; }} | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # Header | |
| st.markdown(""" | |
| <div class="main-header"> | |
| <h1>LiiS Strategy Dashboard</h1> | |
| <p>C-Level View — Portfolio Economics & Break-Even Analysis</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # ============================================================ | |
| # SIDEBAR - INPUT CONTROLS (4 Sliders) | |
| # ============================================================ | |
| st.sidebar.header("Portfolio Parameters") | |
| st.sidebar.caption("Adjust to see impact on profitability") | |
| portfolio_size = st.sidebar.slider( | |
| "Product Portfolio", | |
| min_value=50, max_value=1000, value=300, step=50, | |
| help="Number of units in your fleet" | |
| ) | |
| monthly_lease = st.sidebar.slider( | |
| "Monthly Lease Price", | |
| min_value=250, max_value=650, value=250, step=25, | |
| format="%d NOK", | |
| help="What customers pay per month" | |
| ) | |
| mfg_cost = st.sidebar.slider( | |
| "Manufacturing Cost", | |
| min_value=2500, max_value=7500, value=2500, step=250, | |
| format="%d NOK", | |
| help="Cost to produce one unit" | |
| ) | |
| reuse_cycles = st.sidebar.slider( | |
| "Reuse Cycles", | |
| min_value=2, max_value=10, value=3, step=1, | |
| help="How many families use each product" | |
| ) | |
| # ============================================================ | |
| # CALCULATIONS | |
| # ============================================================ | |
| # Per-product economics | |
| lease_term_months = 12 # Each cycle is 12 months | |
| revenue_per_product = monthly_lease * lease_term_months * reuse_cycles | |
| # Costs per product (simplified model) | |
| logistics_per_cycle = 150 # NOK - pickup + delivery per cycle | |
| repair_per_cycle = 200 # NOK - average repair/refurb per cycle | |
| total_cost_per_product = mfg_cost + (logistics_per_cycle + repair_per_cycle) * reuse_cycles | |
| blended_cost_per_product = total_cost_per_product / reuse_cycles | |
| # Profit per product | |
| profit_per_product = revenue_per_product - total_cost_per_product | |
| operating_margin = (profit_per_product / revenue_per_product) * 100 if revenue_per_product > 0 else 0 | |
| # Portfolio level | |
| revenue_portfolio = portfolio_size * revenue_per_product | |
| cost_portfolio = portfolio_size * total_cost_per_product | |
| blended_cost_portfolio = portfolio_size * blended_cost_per_product | |
| profit_portfolio = portfolio_size * profit_per_product | |
| # Break-even calculation (months) | |
| monthly_revenue_portfolio = portfolio_size * monthly_lease | |
| monthly_cost_portfolio = portfolio_size * (repair_per_cycle / lease_term_months) # Monthly degradation | |
| initial_investment = portfolio_size * mfg_cost | |
| # Cumulative profit over time to find break-even | |
| breakeven_month = None | |
| cumulative = -initial_investment | |
| for month in range(1, 121): | |
| # Add logistics cost at cycle transitions | |
| if month % lease_term_months == 0: | |
| cumulative -= portfolio_size * logistics_per_cycle | |
| cumulative += monthly_revenue_portfolio - monthly_cost_portfolio | |
| if cumulative >= 0 and breakeven_month is None: | |
| breakeven_month = month | |
| break | |
| if breakeven_month is None: | |
| breakeven_month = "> 120" | |
| # Linear model comparison | |
| linear_margin = 0.20 # 20% typical retail margin | |
| linear_profit_per_sale = mfg_cost * linear_margin / (1 - linear_margin) # Selling at cost + margin | |
| linear_revenue_per_product = mfg_cost / (1 - linear_margin) | |
| linear_breakeven_units = int(initial_investment / linear_profit_per_sale) if linear_profit_per_sale > 0 else 999 | |
| # Time to sell that many units (assume 1 sale per unit in portfolio per cycle) | |
| linear_breakeven_months = max(24, int(linear_breakeven_units / portfolio_size * lease_term_months)) | |
| # ============================================================ | |
| # TIER 1: PRIMARY METRICS | |
| # ============================================================ | |
| st.markdown('<p class="tier-header">Tier 1 — Primary Metrics</p>', unsafe_allow_html=True) | |
| hero_col1, hero_col2 = st.columns(2) | |
| with hero_col1: | |
| st.markdown(f""" | |
| <div class="hero-metric"> | |
| <p>BREAK EVEN</p> | |
| <h2>{breakeven_month} MONTHS</h2> | |
| <p>For entire portfolio of {portfolio_size:,} units</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| with hero_col2: | |
| profit_color = COLORS["positive"] if profit_portfolio > 0 else COLORS["negative"] | |
| st.markdown(f""" | |
| <div class="hero-metric"> | |
| <p>TOTAL PORTFOLIO PROFIT</p> | |
| <h2 style="color: {profit_color}">{profit_portfolio:,.0f} NOK</h2> | |
| <p>Net revenue across {portfolio_size:,} units over {reuse_cycles} cycles</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown("---") | |
| # ============================================================ | |
| # TIER 2: SUPPORTING METRICS | |
| # ============================================================ | |
| st.markdown('<p class="tier-header">Tier 2 — Unit Economics</p>', unsafe_allow_html=True) | |
| metric_col1, metric_col2, metric_col3 = st.columns(3) | |
| with metric_col1: | |
| st.metric( | |
| "Revenue per Product", | |
| f"{revenue_per_product:,.0f} NOK", | |
| delta=f"{monthly_lease} × {lease_term_months}mo × {reuse_cycles} cycles", | |
| help="Total revenue generated by one product over its lifetime" | |
| ) | |
| st.metric( | |
| "Revenue per Portfolio", | |
| f"{revenue_portfolio:,.0f} NOK", | |
| delta=f"{portfolio_size:,} units", | |
| help="Total portfolio revenue" | |
| ) | |
| with metric_col2: | |
| st.metric( | |
| "Blended Cost per Product", | |
| f"{blended_cost_per_product:,.0f} NOK", | |
| delta=f"Total {total_cost_per_product:,.0f} ÷ {reuse_cycles} cycles", | |
| help="Average cost per cycle (manufacturing + logistics + repair)" | |
| ) | |
| st.metric( | |
| "Blended Cost per Portfolio", | |
| f"{blended_cost_portfolio:,.0f} NOK", | |
| delta=f"{portfolio_size:,} units", | |
| help="Total portfolio blended cost" | |
| ) | |
| with metric_col3: | |
| margin_color = "normal" if operating_margin >= 50 else "off" | |
| st.metric( | |
| "Operating Margin", | |
| f"{operating_margin:.0f}%", | |
| delta="healthy" if operating_margin >= 50 else "review pricing", | |
| delta_color=margin_color, | |
| help="Profit / Revenue - same for all units" | |
| ) | |
| st.metric( | |
| "Profit per Product", | |
| f"{profit_per_product:,.0f} NOK", | |
| delta="lifetime profit", | |
| help="Revenue - Total Cost per product" | |
| ) | |
| st.markdown("---") | |
| # ============================================================ | |
| # CHART 1: Break Even Comparison | |
| # ============================================================ | |
| st.markdown('<p class="tier-header">Tier 2 — Model Comparison</p>', unsafe_allow_html=True) | |
| st.subheader("Break Even: Circular vs Linear Model") | |
| comp_col1, comp_col2 = st.columns(2) | |
| with comp_col1: | |
| st.markdown(f""" | |
| <div class="comparison-card"> | |
| <p style="color: {COLORS['text_subtle']}; margin: 0;">LINEAR MODEL</p> | |
| <h2 style="color: {COLORS['negative']}; margin: 0.5rem 0;">{linear_breakeven_months} MONTHS</h2> | |
| <p style="color: {COLORS['text_muted']}; margin: 0;">{linear_margin*100:.0f}% margin</p> | |
| <p style="color: {COLORS['text_subtle']}; font-size: 0.8rem; margin-top: 0.5rem;">Sell once, done</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| with comp_col2: | |
| st.markdown(f""" | |
| <div class="comparison-card winner"> | |
| <p style="color: {COLORS['text_subtle']}; margin: 0;">CIRCULAR MODEL</p> | |
| <h2 style="color: {COLORS['primary']}; margin: 0.5rem 0;">{breakeven_month} MONTHS</h2> | |
| <p style="color: {COLORS['primary']}; margin: 0;">{operating_margin:.0f}% margin</p> | |
| <p style="color: {COLORS['text_subtle']}; font-size: 0.8rem; margin-top: 0.5rem;">Reuse {reuse_cycles}× per product</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown("---") | |
| # ============================================================ | |
| # CHART 2: Blended Cost vs Revenue Across Cycles | |
| # ============================================================ | |
| st.subheader("Cost & Revenue Trajectory by Reuse Cycles") | |
| cycles_range = list(range(2, 11)) | |
| chart_data = [] | |
| for c in cycles_range: | |
| rev = monthly_lease * lease_term_months * c | |
| total_cost = mfg_cost + (logistics_per_cycle + repair_per_cycle) * c | |
| blended = total_cost / c | |
| profit = rev - total_cost | |
| margin = (profit / rev) * 100 if rev > 0 else 0 | |
| chart_data.append({ | |
| "Cycles": c, | |
| "Revenue": rev, | |
| "Total Cost": total_cost, | |
| "Blended Cost": blended, | |
| "Profit": profit, | |
| "Margin": margin, | |
| }) | |
| chart_df = pd.DataFrame(chart_data) | |
| fig_cycles = go.Figure() | |
| # Revenue line (constant per cycle × cycles) | |
| fig_cycles.add_trace(go.Scatter( | |
| x=chart_df["Cycles"], | |
| y=chart_df["Revenue"], | |
| mode="lines+markers", | |
| name="Revenue per Product", | |
| line=dict(color=COLORS["positive"], width=3), | |
| marker=dict(size=8), | |
| )) | |
| # Blended cost line (decreasing) | |
| fig_cycles.add_trace(go.Scatter( | |
| x=chart_df["Cycles"], | |
| y=chart_df["Blended Cost"], | |
| mode="lines+markers", | |
| name="Blended Cost per Product", | |
| line=dict(color=COLORS["negative"], width=3), | |
| marker=dict(size=8), | |
| )) | |
| # Total cost line | |
| fig_cycles.add_trace(go.Scatter( | |
| x=chart_df["Cycles"], | |
| y=chart_df["Total Cost"], | |
| mode="lines+markers", | |
| name="Total Cost per Product", | |
| line=dict(color=COLORS["negative"], width=2, dash="dot"), | |
| marker=dict(size=6), | |
| opacity=0.6, | |
| )) | |
| # Mark current cycles | |
| fig_cycles.add_vline( | |
| x=reuse_cycles, line_dash="dash", line_color=COLORS["warning"], | |
| annotation_text=f"Current: {reuse_cycles} cycles", | |
| annotation_position="top" | |
| ) | |
| fig_cycles.update_layout( | |
| xaxis_title="Reuse Cycles", | |
| yaxis_title="NOK per Product", | |
| legend=dict(orientation="h", yanchor="bottom", y=1.02), | |
| hovermode="x unified", | |
| ) | |
| st.plotly_chart(fig_cycles, use_container_width=True) | |
| st.info(f"**Key insight:** More cycles = lower blended cost = higher profit. At {reuse_cycles} cycles, blended cost is {blended_cost_per_product:,.0f} NOK vs {mfg_cost:,} NOK manufacturing cost.") | |
| st.markdown("---") | |
| # ============================================================ | |
| # TIER 3: AT SCALE | |
| # ============================================================ | |
| st.markdown('<p class="tier-header">Tier 3 — Scale Understanding</p>', unsafe_allow_html=True) | |
| st.subheader("At Scale: Customer Base Projection") | |
| scale_col1, scale_col2 = st.columns([1, 2]) | |
| with scale_col1: | |
| customer_base = st.slider( | |
| "Customer Base", | |
| min_value=500, max_value=10000, value=1000, step=100, | |
| help="Target number of active customers" | |
| ) | |
| utilization_rate = 0.80 # 80% of fleet in use at any time | |
| fleet_needed = int(customer_base / utilization_rate) | |
| annual_revenue_scale = customer_base * monthly_lease * lease_term_months | |
| total_cost_scale = fleet_needed * blended_cost_per_product | |
| annual_profit_scale = annual_revenue_scale - total_cost_scale | |
| st.metric("Fleet Size Needed", f"{fleet_needed:,} units", delta=f"{utilization_rate*100:.0f}% utilization") | |
| st.metric("Annual Revenue", f"{annual_revenue_scale:,.0f} NOK", delta=f"{customer_base:,} customers") | |
| st.metric("Total Blended Cost", f"{total_cost_scale:,.0f} NOK", delta=f"{fleet_needed:,} units") | |
| profit_delta = "profit" if annual_profit_scale > 0 else "loss" | |
| st.metric("Annual Profit", f"{annual_profit_scale:,.0f} NOK", delta=profit_delta) | |
| with scale_col2: | |
| # Chart 3: 5-Year Cumulative Profit | |
| st.markdown("**5-Year Cumulative Profit Trajectory**") | |
| # Calculate with 10% utilization decline per cycle | |
| months = list(range(0, 61)) | |
| cumulative_profit = [] | |
| cumulative = -fleet_needed * mfg_cost # Initial investment | |
| current_utilization = utilization_rate | |
| for month in months: | |
| if month == 0: | |
| cumulative_profit.append(cumulative) | |
| continue | |
| # Decline utilization at each cycle end | |
| if month > 0 and month % lease_term_months == 0: | |
| current_utilization = max(0.5, current_utilization - 0.10) | |
| cumulative -= fleet_needed * logistics_per_cycle # Logistics at cycle transition | |
| active_customers = int(fleet_needed * current_utilization) | |
| monthly_rev = active_customers * monthly_lease | |
| monthly_cost = active_customers * (repair_per_cycle / lease_term_months) | |
| cumulative += monthly_rev - monthly_cost | |
| cumulative_profit.append(cumulative) | |
| fig_trajectory = go.Figure() | |
| fig_trajectory.add_trace(go.Scatter( | |
| x=months, | |
| y=cumulative_profit, | |
| mode="lines", | |
| name="Cumulative Profit", | |
| line=dict(color=COLORS["positive"], width=3), | |
| fill="tozeroy", | |
| fillcolor="rgba(18, 75, 95, 0.15)", | |
| )) | |
| # Add zero line | |
| fig_trajectory.add_hline(y=0, line_color=COLORS["border"], line_width=1) | |
| # Mark cycle transitions | |
| for cycle_month in range(lease_term_months, 61, lease_term_months): | |
| fig_trajectory.add_vline( | |
| x=cycle_month, line_dash="dot", line_color=COLORS["border"], | |
| ) | |
| # Find and mark breakeven | |
| for i, cp in enumerate(cumulative_profit): | |
| if cp >= 0 and i > 0: | |
| fig_trajectory.add_trace(go.Scatter( | |
| x=[i], y=[cp], | |
| mode="markers+text", | |
| name="Breakeven", | |
| marker=dict(size=12, color=COLORS["warning"], symbol="star"), | |
| text=["Breakeven"], | |
| textposition="top center", | |
| )) | |
| break | |
| fig_trajectory.update_layout( | |
| xaxis_title="Months", | |
| yaxis_title="Cumulative Profit (NOK)", | |
| showlegend=False, | |
| yaxis=dict(tickformat=","), | |
| ) | |
| st.plotly_chart(fig_trajectory, use_container_width=True) | |
| final_profit = cumulative_profit[-1] | |
| st.caption(f"5-year projection with {fleet_needed:,} unit fleet. Final cumulative profit: **{final_profit:,.0f} NOK**") | |
| # ============================================================ | |
| # FOOTER | |
| # ============================================================ | |
| st.markdown("---") | |
| st.caption(f"Portfolio: {portfolio_size:,} units | Lease: {monthly_lease} NOK/mo | Mfg: {mfg_cost:,} NOK | Cycles: {reuse_cycles}") | |