| import os |
| from pathlib import Path |
| import random |
|
|
| import numpy as np |
| import pandas as pd |
| import plotly.express as px |
| import plotly.graph_objects as go |
| import streamlit as st |
|
|
| st.set_page_config(page_title="Freshie", page_icon="π±", layout="wide", initial_sidebar_state="expanded") |
|
|
|
|
| def inject_css(): |
| st.markdown( |
| """ |
| <style> |
| .stApp { |
| background: |
| radial-gradient(circle at top right, rgba(57,198,233,0.10), transparent 28%), |
| radial-gradient(circle at top left, rgba(142,219,99,0.12), transparent 24%), |
| linear-gradient(180deg, #FFFDF7 0%, #F3FBF6 100%); |
| } |
| [data-testid="stAppViewContainer"] { |
| background: transparent; |
| } |
| section[data-testid="stSidebar"] > div { |
| background: rgba(255,255,255,0.99); |
| box-shadow: 8px 0 24px rgba(0,0,0,0.06); |
| border-right: 1px solid rgba(23,50,77,0.06); |
| } |
| [data-testid="stSidebarUserContent"] { |
| padding-top: 0.5rem; |
| } |
| .hero-wrap { |
| background: |
| linear-gradient(135deg, rgba(255,255,255,0.94), rgba(255,253,247,0.97)), |
| linear-gradient(90deg, rgba(51,196,110,0.06), rgba(57,198,233,0.08)); |
| border: 1px solid rgba(23,50,77,0.06); |
| border-radius: 24px; |
| padding: 18px 20px; |
| box-shadow: 0 14px 30px rgba(0,0,0,0.05); |
| margin-bottom: 14px; |
| } |
| .main-title { |
| display:flex; |
| align-items:center; |
| justify-content:space-between; |
| gap:18px; |
| margin-bottom:8px; |
| } |
| .title-left { |
| display:flex; |
| align-items:center; |
| gap:16px; |
| } |
| .logo-img { |
| width: 110px; |
| height: auto; |
| border-radius: 18px; |
| box-shadow: 0 10px 22px rgba(57,198,233,0.16); |
| background: rgba(255,255,255,0.90); |
| padding: 4px; |
| } |
| .brand-sub { |
| color: #5E6B78; |
| margin-top: -2px; |
| margin-bottom: 8px; |
| font-size: 1.02rem; |
| max-width: 700px; |
| } |
| .hero-art { |
| font-size: 2.6rem; |
| opacity: 0.92; |
| white-space: nowrap; |
| } |
| .search-pill { |
| background: #FFFFFF; |
| border: 1px solid rgba(23,50,77,0.06); |
| border-radius: 999px; |
| padding: 10px 14px; |
| color: #5E6B78; |
| box-shadow: 0 6px 18px rgba(0,0,0,0.04); |
| margin-top: 10px; |
| } |
| .note-box { |
| background: linear-gradient(135deg, #FFF7ED 0%, #FFFDF7 100%); |
| border-left: 4px solid #FB923C; |
| padding: 10px 12px; |
| border-radius: 12px; |
| margin: 8px 0 14px 0; |
| color: #7C4A03; |
| box-shadow: 0 8px 18px rgba(0,0,0,0.03); |
| } |
| div[data-testid="stMetric"] { |
| background: rgba(255,255,255,0.96); |
| border: 1px solid rgba(31,45,61,0.06); |
| border-radius: 18px; |
| padding: 12px 14px; |
| box-shadow: 0 8px 18px rgba(0,0,0,0.04); |
| } |
| [data-testid="stDataFrame"] { |
| background: #FFFFFF; |
| border-radius: 16px; |
| padding: 6px; |
| box-shadow: 0 6px 16px rgba(0,0,0,0.03); |
| } |
| [data-baseweb="tab-list"] { |
| gap: 8px; |
| } |
| </style> |
| """, |
| unsafe_allow_html=True, |
| ) |
|
|
|
|
| def find_data_path() -> Path | None: |
| candidates = [ |
| Path("/app/perishable_goods_management.csv"), |
| Path("perishable_goods_management.csv"), |
| Path("/mnt/data/perishable_goods_management.csv"), |
| ] |
| for p in candidates: |
| if p.exists(): |
| return p |
| return None |
|
|
|
|
| @st.cache_data(show_spinner=False) |
| def load_data() -> pd.DataFrame: |
| path = find_data_path() |
| if path is None: |
| raise FileNotFoundError("perishable_goods_management.csv not found in /app or current folder.") |
|
|
| df = pd.read_csv(path) |
|
|
| |
| date_col = None |
| for c in ["transaction_date", "date", "Date"]: |
| if c in df.columns: |
| date_col = c |
| break |
| if date_col is None: |
| raise ValueError("No transaction date column found.") |
| if date_col != "transaction_date": |
| df = df.rename(columns={date_col: "transaction_date"}) |
|
|
| df["transaction_date"] = pd.to_datetime(df["transaction_date"], errors="coerce") |
| df = df.dropna(subset=["transaction_date"]).copy() |
|
|
| |
| if "discount_pct" not in df.columns: |
| if "discount_percentage" in df.columns: |
| df["discount_pct"] = df["discount_percentage"] |
| else: |
| df["discount_pct"] = 0.0 |
|
|
| for col in ["units_sold", "initial_quantity", "daily_demand", "profit", "days_until_expiry"]: |
| if col not in df.columns: |
| raise ValueError(f"Missing required column: {col}") |
|
|
| if "category" not in df.columns: |
| df["category"] = "Unknown" |
| if "region" not in df.columns: |
| df["region"] = "Unknown" |
| if "store_id" not in df.columns: |
| df["store_id"] = "STORE_001" |
| if "product_name" not in df.columns: |
| df["product_name"] = df["category"].astype(str) |
|
|
| if "selling_price" not in df.columns and "price" in df.columns: |
| df["selling_price"] = df["price"] |
| if "base_price" not in df.columns and "selling_price" in df.columns: |
| df["base_price"] = df["selling_price"] / (1 - df["discount_pct"].replace(1, 0.999).clip(0, 0.99)) |
| if "base_price" not in df.columns: |
| df["base_price"] = 1.0 |
| if "selling_price" not in df.columns: |
| df["selling_price"] = 1.0 |
|
|
| df["discount_pct"] = df["discount_pct"].fillna(0) |
| if df["discount_pct"].max() > 1: |
| df["discount_pct"] = df["discount_pct"] / 100 |
| df["discount_pct"] = df["discount_pct"].clip(0, 1) |
|
|
| if "waste_pct" not in df.columns: |
| denom = df["initial_quantity"].replace(0, np.nan) |
| if "units_wasted" in df.columns: |
| df["waste_pct"] = (df["units_wasted"] / denom).fillna(0) |
| else: |
| df["waste_pct"] = 0.0 |
| if df["waste_pct"].max() > 1: |
| df["waste_pct"] = df["waste_pct"] / 100 |
| df["waste_pct"] = df["waste_pct"].clip(0, 1) |
|
|
| if "units_wasted" not in df.columns: |
| df["units_wasted"] = (df["waste_pct"] * df["initial_quantity"]).round() |
|
|
| df["leftover_units"] = (df["initial_quantity"] - df["units_sold"]).clip(lower=0) |
| df["stockout_flag"] = (df["daily_demand"] > df["initial_quantity"]).astype(int) |
| df["lost_sales_units"] = (df["daily_demand"] - df["units_sold"]).clip(lower=0) |
| df["sell_through_pct"] = (df["units_sold"] / df["initial_quantity"].replace(0, np.nan)).fillna(0).clip(0, 1) |
| df["month"] = df["transaction_date"].dt.strftime("%Y-%m") |
| df["is_weekend"] = (df["transaction_date"].dt.dayofweek >= 5).astype(int) |
| df["expiry_bucket"] = pd.cut( |
| df["days_until_expiry"], |
| bins=[-1, 1, 3, 7, 30, 10000], |
| labels=["<=1d", "2-3d", "4-7d", "8-30d", ">30d"], |
| ).astype(str) |
| return df |
|
|
|
|
|
|
| PCA_SHAP_LIBRARY = { |
| "Bakery": { |
| "avg_profit": 329.82, |
| "avg_margin": 0.0573, |
| "avg_waste": 0.1535, |
| "avg_units_sold": 216.88, |
| "avg_daily_demand": 114.32, |
| "region_profit": [("Southwest", 314.96), ("Midwest", 319.24), ("Southeast", 324.43), ("Northeast", 332.06), ("West", 350.22)], |
| "business": "Bakery is a high-turnover category with positive profit but thin margins. It behaves like a traffic driver: demand is strong, but a large part of profit quality depends on pricing discipline and waste control.", |
| "pc1": 0.3441, |
| "pc2": 0.2786, |
| "pc1_theme": "Profitability efficiency axis", |
| "pc1_loadings": [("avg_profit_margin", "+0.529"), ("avg_days_until_expiry", "+0.429"), ("avg_sell_through", "+0.412"), ("avg_waste_pct", "-0.412"), ("avg_discount_pct", "-0.409")], |
| "pc2_theme": "Demand and replenishment intensity axis", |
| "pc2_loadings": [("avg_daily_demand", "+0.543"), ("avg_waste_pct", "-0.406"), ("avg_sell_through", "+0.406"), ("avg_initial_qty", "+0.379"), ("avg_discount_pct", "+0.351")], |
| "clusters": [ |
| ("Cluster A Β· Core winners", "Demand 85.13 Β· Sell-through 98.09% Β· Waste 1.91% Β· Margin 30.99%", "Protect availability, keep discounting light, and avoid stockouts."), |
| ("Cluster B Β· Traffic drivers but margin issue", "Demand 281.09 Β· Sell-through 94.90% Β· Waste 5.10% Β· Margin -9.91%", "Review price, cost, and promotion depth before cutting stock."), |
| ("Cluster C Β· Balanced mid-tier", "Demand 89.10 Β· Sell-through 69.40% Β· Waste 30.60% Β· Margin 3.87%", "Use tighter replenishment and light tactical markdowns."), |
| ("Cluster D Β· High-waste losers", "Demand 71.55 Β· Sell-through 49.53% Β· Waste 50.47% Β· Margin -95.47%", "Reduce initial stock, markdown earlier, bundle, and trim weak-SKU stores."), |
| ], |
| "r2": 0.809, |
| "mae": 227.69, |
| "drivers": ["discount_pct", "base_price", "initial_quantity", "cost_price", "markdown_applied", "daily_demand", "days_until_expiry", "shelf_life_days", "is_weekend", "spoilage_risk"], |
| "conclusion": "Bakery profit is mainly shaped by discounting, pricing, stock depth, and demand strength rather than expiry alone.", |
| "actions": [ |
| "Review price and cost on high-demand but negative-margin SKUs.", |
| "Start markdown earlier for high-waste items instead of waiting until the last moment.", |
| "Use weekend-sensitive promotions in a more targeted rhythm.", |
| ], |
| }, |
| "Meat": { |
| "avg_profit": 777.27, |
| "avg_margin": 0.1411, |
| "avg_waste": 0.1820, |
| "avg_units_sold": 213.96, |
| "avg_daily_demand": 73.30, |
| "region_profit": [("Northeast", 757.70), ("Southwest", 765.17), ("Southeast", 779.24), ("Midwest", 793.72), ("West", 799.80)], |
| "business": "Meat is the healthiest of the three in profit contribution. Waste is meaningful, but margin quality is stronger than Bakery, so it is worth managing through precision replenishment and loss control.", |
| "pc1": 0.2960, |
| "pc2": 0.2060, |
| "pc1_theme": "Operational efficiency axis", |
| "pc1_loadings": [("avg_profit_margin", "+0.498"), ("avg_sell_through", "+0.488"), ("avg_waste_pct", "-0.488"), ("avg_days_until_expiry", "+0.356"), ("avg_discount_pct", "-0.339")], |
| "pc2_theme": "Sales scale and stocking intensity axis", |
| "pc2_loadings": [("avg_daily_demand", "+0.627"), ("avg_initial_qty", "+0.487"), ("avg_days_until_expiry", "-0.356"), ("avg_discount_pct", "+0.347")], |
| "clusters": [ |
| ("Cluster A Β· Premium winners", "Demand 58.81 Β· Sell-through 97.74% Β· Waste 2.26% Β· Margin 38.42%", "Protect availability for top sellers and avoid over-restricting stock."), |
| ("Cluster B Β· Strong traffic winners", "Demand 184.22 Β· Sell-through 89.29% Β· Waste 10.71% Β· Margin 10.44%", "Keep these as stable replenishment anchors."), |
| ("Cluster C Β· Manageable mid-tier", "Demand 58.11 Β· Sell-through 68.51% Β· Waste 31.49% Β· Margin 8.76%", "Fine-tune inventory and use local promotions only where needed."), |
| ("Cluster D Β· High-waste loss cluster", "Demand 46.16 Β· Sell-through 42.50% Β· Waste 57.50% Β· Margin -86.58%", "Lower stock, tighten cold-chain rhythm, and optimize assortment by store."), |
| ], |
| "r2": 0.720, |
| "mae": 464.22, |
| "drivers": ["discount_pct", "initial_quantity", "base_price", "cost_price", "days_until_expiry", "daily_demand", "markdown_applied", "is_weekend", "spoilage_risk", "shelf_life_days"], |
| "conclusion": "Meat profit is driven by discounting, stocking quantity, pricing, cost, and expiry pressure; compared with Bakery, it is more sensitive to initial quantity and days until expiry.", |
| "actions": [ |
| "Cut initial quantity first for high-waste clusters.", |
| "Use earlier and lighter markdowns on SKUs with real demand but rising expiry pressure.", |
| "Protect availability on the high-profit core group instead of applying blanket stock cuts.", |
| ], |
| }, |
| "Ready_to_Eat": { |
| "avg_profit": 182.98, |
| "avg_margin": -0.1151, |
| "avg_waste": 0.1456, |
| "avg_units_sold": 220.33, |
| "avg_daily_demand": 186.41, |
| "region_profit": [("Northeast", 162.08), ("Southwest", 169.03), ("West", 183.12), ("Southeast", 189.53), ("Midwest", 224.99)], |
| "business": "Ready-to-Eat shows the strongest contradiction: very high demand and sales volume, but negative average margin. The category sells, but the current commercial logic does not monetize that demand well.", |
| "pc1": 0.3428, |
| "pc2": 0.2842, |
| "pc1_theme": "Promotion and expiry pressure axis", |
| "pc1_loadings": [("avg_discount_pct", "+0.536"), ("avg_days_until_expiry", "-0.520"), ("avg_profit_margin", "-0.486"), ("avg_daily_demand", "+0.299"), ("avg_spoilage_risk", "+0.291")], |
| "pc2_theme": "Waste versus sell-through axis", |
| "pc2_loadings": [("avg_waste_pct", "+0.616"), ("avg_sell_through", "-0.616"), ("avg_daily_demand", "-0.344")], |
| "clusters": [ |
| ("Cluster A Β· Profitable fast movers", "Demand 137.37 Β· Sell-through 98.45% Β· Waste 1.55% Β· Margin 10.76%", "Protect supply and avoid stockouts."), |
| ("Cluster B Β· Traffic drivers but deeply margin-negative", "Demand 564.13 Β· Sell-through 95.40% Β· Waste 4.60% Β· Margin -33.18%", "Fix price and promotion depth first."), |
| ("Cluster C Β· High waste and deepest loss", "Demand 149.11 Β· Sell-through 58.54% Β· Waste 41.46% Β· Margin -111.87%", "Cut stock immediately, markdown earlier, bundle, and prune poor store fits."), |
| ("Cluster D Β· Neutral mid-tier", "Demand 123.52 Β· Sell-through 69.63% Β· Waste 30.37% Β· Margin 0.45%", "Use light promotion and micro-replenishment changes to move toward Cluster A."), |
| ], |
| "r2": 0.823, |
| "mae": 278.78, |
| "drivers": ["discount_pct", "base_price", "cost_price", "initial_quantity", "markdown_applied", "days_until_expiry", "daily_demand", "spoilage_risk", "shelf_life_days", "is_weekend"], |
| "conclusion": "Discount intensity is the dominant profit driver in Ready-to-Eat, followed by pricing, cost, and stock depth. The main managerial lever is promotion redesign before inventory expansion.", |
| "actions": [ |
| "Review discount_pct first; move from blanket discounts to targeted markdowns.", |
| "Raise or reset pricing on high-demand hero items before adding more stock.", |
| "Shorten replenishment cycles and reduce single-drop quantity on the high-waste group.", |
| ], |
| }, |
| } |
|
|
|
|
| def _sync_region_from_store(df: pd.DataFrame): |
| stores = st.session_state.get("store_filter", []) |
| if stores: |
| inferred = sorted(df.loc[df["store_id"].isin(stores), "region"].dropna().unique().tolist()) |
| st.session_state["region_filter"] = inferred |
|
|
|
|
| def _sync_store_from_region(df: pd.DataFrame): |
| regions = st.session_state.get("region_filter", []) |
| current_stores = st.session_state.get("store_filter", []) |
| allowed = sorted(df.loc[df["region"].isin(regions), "store_id"].dropna().unique().tolist()) if regions else sorted(df["store_id"].dropna().unique().tolist()) |
| st.session_state["store_filter"] = [s for s in current_stores if s in allowed] |
|
|
|
|
|
|
|
|
| def build_supply_balancing_table(df: pd.DataFrame) -> pd.DataFrame: |
| work = df.copy() |
| grouped = work.groupby(["region", "store_id", "category"], dropna=False).agg( |
| remaining_inventory=("leftover_units", "mean"), |
| demand=("daily_demand", "mean"), |
| unmet_demand=("lost_sales_units", "mean"), |
| avg_days_until_expiry=("days_until_expiry", "mean"), |
| waste_pct=("waste_pct", "mean"), |
| avg_profit=("profit", "mean"), |
| ).reset_index() |
| grouped["surplus_qty"] = (grouped["remaining_inventory"] - grouped["demand"]).clip(lower=0) |
| return grouped |
|
|
|
|
| def filter_scope(df: pd.DataFrame, selected_region="All", selected_store="All", selected_category="All") -> pd.DataFrame: |
| scoped = df.copy() |
| if selected_store != "All": |
| scoped = scoped[scoped["store_id"] == selected_store] |
| elif selected_region != "All": |
| scoped = scoped[scoped["region"] == selected_region] |
| if selected_category != "All": |
| scoped = scoped[scoped["category"] == selected_category] |
| return scoped |
|
|
|
|
| def apply_filters(df: pd.DataFrame): |
| st.sidebar.header("Filters") |
|
|
| if "region_filter" not in st.session_state: |
| st.session_state["region_filter"] = [] |
| if "store_filter" not in st.session_state: |
| st.session_state["store_filter"] = [] |
|
|
| regions = sorted(df["region"].dropna().unique().tolist()) |
| selected_regions = st.sidebar.multiselect( |
| "Region", |
| regions, |
| key="region_filter", |
| on_change=_sync_store_from_region, |
| args=(df,), |
| ) |
|
|
| available_stores = sorted( |
| df.loc[df["region"].isin(selected_regions), "store_id"].dropna().unique().tolist() |
| ) if selected_regions else sorted(df["store_id"].dropna().unique().tolist()) |
|
|
| |
| st.session_state["store_filter"] = [s for s in st.session_state.get("store_filter", []) if s in available_stores] |
|
|
| selected_stores = st.sidebar.multiselect( |
| "Store", |
| available_stores, |
| key="store_filter", |
| on_change=_sync_region_from_store, |
| args=(df,), |
| ) |
|
|
| if selected_stores: |
| inferred_regions = sorted(df.loc[df["store_id"].isin(selected_stores), "region"].dropna().unique().tolist()) |
| selected_regions = inferred_regions |
| st.sidebar.caption("Auto region: " + ", ".join(inferred_regions)) |
|
|
| category_options = ["All"] + sorted(df["category"].dropna().unique().tolist()) |
| chosen_category = st.sidebar.selectbox("Category", category_options) |
| day_type = st.sidebar.selectbox("Day type", ["All", "Weekday", "Weekend"]) |
| use_expiry = st.sidebar.checkbox("Limit to inventory below 60 days until expiry", value=False) |
| expiry_range = (0, 60) |
| if use_expiry: |
| expiry_range = st.sidebar.slider("Days until expiry", 0, 60, (0, 60)) |
|
|
| exec_scope = df.copy() |
| if selected_regions: |
| exec_scope = exec_scope[exec_scope["region"].isin(selected_regions)] |
| if selected_stores: |
| exec_scope = exec_scope[exec_scope["store_id"].isin(selected_stores)] |
| if day_type == "Weekday": |
| exec_scope = exec_scope[exec_scope["is_weekend"] == 0] |
| elif day_type == "Weekend": |
| exec_scope = exec_scope[exec_scope["is_weekend"] == 1] |
| if use_expiry: |
| exec_scope = exec_scope[(exec_scope["days_until_expiry"] >= expiry_range[0]) & (exec_scope["days_until_expiry"] <= expiry_range[1])] |
|
|
| filtered = exec_scope.copy() |
| if chosen_category != "All": |
| filtered = filtered[filtered["category"] == chosen_category] |
|
|
| scope_info = { |
| "stores": selected_stores, |
| "regions": selected_regions, |
| "category": chosen_category, |
| "day_type": day_type, |
| "use_expiry": use_expiry, |
| "expiry_range": expiry_range, |
| } |
| return filtered, exec_scope, scope_info |
|
|
| def build_category_recommendations(row: pd.Series, cat_summary: pd.DataFrame) -> list[str]: |
| waste_mean = float(cat_summary["waste_pct"].mean()) if len(cat_summary) else 0 |
| waste_p75 = float(cat_summary["waste_pct"].quantile(0.75)) if len(cat_summary) else waste_mean |
| demand_mean = float(cat_summary["avg_demand"].mean()) if len(cat_summary) else 0 |
| demand_p75 = float(cat_summary["avg_demand"].quantile(0.75)) if len(cat_summary) else demand_mean |
| profit_mean = float(cat_summary["avg_profit"].mean()) if len(cat_summary) else 0 |
| stockout_mean = float(cat_summary["stockout_rate"].mean()) if len(cat_summary) else 0 |
| stockout_p75 = float(cat_summary["stockout_rate"].quantile(0.75)) if len(cat_summary) else stockout_mean |
| sell_mean = float(cat_summary["sell_through"].mean()) if len(cat_summary) else 0 |
|
|
| advice: list[str] = [] |
| if row["waste_pct"] >= max(0.30, waste_p75): |
| advice.append("start markdown earlier") |
| if row["avg_demand"] >= demand_p75 and row["stockout_rate"] >= max(0.10, stockout_mean, stockout_p75 * 0.8): |
| advice.append("increase replenishment") |
| if row["avg_profit"] < 0 and row["avg_demand"] < demand_mean: |
| advice.append("reduce replenishment") |
| if row["waste_pct"] > waste_mean or row["avg_profit"] < profit_mean: |
| advice.append("review mix and margin") |
| if row["sell_through"] < max(0.45, sell_mean * 0.8): |
| advice.append("prune weak SKUs / stores") |
| if not advice: |
| advice.append("maintain current playbook") |
| |
| return list(dict.fromkeys(advice)) |
|
|
|
|
| def _safe_profit_margin(df: pd.DataFrame) -> pd.Series: |
| revenue = (df["selling_price"] * df["units_sold"]).replace(0, np.nan) |
| return (df["profit"] / revenue).replace([np.inf, -np.inf], np.nan).fillna(0) |
|
|
|
|
| def _assign_cluster_names(cluster_summary: pd.DataFrame) -> pd.DataFrame: |
| ranked = cluster_summary.copy() |
| ranked["score"] = ( |
| ranked["margin"] * 0.45 |
| + ranked["sell_through"] * 0.35 |
| - ranked["waste"] * 0.20 |
| ) |
| ranked = ranked.sort_values(["score", "avg_profit"], ascending=[False, False]).reset_index(drop=True) |
|
|
| names: list[str] = [] |
| used: dict[str, int] = {} |
| for i, row in ranked.iterrows(): |
| if i == 0 and row["margin"] >= 0: |
| base = "Core winners" |
| elif row["margin"] < 0 and row["sell_through"] >= ranked["sell_through"].median(): |
| base = "Traffic but unprofitable" |
| elif row["waste"] >= ranked["waste"].median(): |
| base = "High-waste cluster" |
| else: |
| base = "Mid-tier cluster" |
|
|
| used[base] = used.get(base, 0) + 1 |
| suffix = "" if used[base] == 1 else f" {used[base]}" |
| names.append(f"{base}{suffix}") |
|
|
| ranked["cluster_name"] = names |
| return ranked.drop(columns=["score"]) |
|
|
|
|
| def _cluster_color_map(cluster_names: list[str]) -> dict[str, str]: |
| palette = { |
| "Core winners": "#1f77b4", |
| "Traffic but unprofitable": "#ff7f0e", |
| "High-waste cluster": "#d62728", |
| "Mid-tier cluster": "#7f7f7f", |
| } |
| mapping: dict[str, str] = {} |
| for name in cluster_names: |
| for base, color in palette.items(): |
| if name.startswith(base): |
| mapping[name] = color |
| break |
| else: |
| mapping[name] = "#17becf" |
| return mapping |
|
|
|
|
| def _classify_product_segment(row: pd.Series, sell_thr: float, waste_thr: float) -> str: |
| if row["avg_sell_through"] >= sell_thr and row["avg_profit"] >= 0: |
| return "Scale β" |
| if row["avg_sell_through"] >= sell_thr and row["avg_profit"] < 0: |
| return "Fix Pricing β οΈ" |
| if row["avg_sell_through"] < sell_thr and row["avg_profit"] >= 0: |
| return "Optimize π " |
| if row["avg_waste_pct"] >= waste_thr: |
| return "Reduce β" |
| return "Optimize π " |
|
|
|
|
| def _generate_sku_strategy(row: pd.Series, sell_thr: float, waste_thr: float, discount_thr: float, demand_thr: float) -> tuple[str, str]: |
| actions: list[str] = [] |
| if row["segment"] == "Scale β": |
| actions.extend(["protect availability", "keep discounts selective"]) |
| if row["avg_discount_pct"] > discount_thr: |
| actions.append("test a lighter markdown") |
| headline = "Scale distribution" |
| elif row["segment"] == "Fix Pricing β οΈ": |
| actions.extend(["raise effective price or trim discount", "hold replenishment steady"]) |
| if row["avg_discount_pct"] > discount_thr: |
| actions.append("tighten markdown depth") |
| if row["avg_daily_demand"] >= demand_thr: |
| actions.append("treat as hero SKU, but monetize demand better") |
| headline = "Fix pricing before adding stock" |
| elif row["segment"] == "Reduce β": |
| actions.extend(["cut initial quantity", "markdown earlier", "review store fit / assortment"]) |
| headline = "Reduce exposure" |
| else: |
| actions.extend(["fine-tune stock depth", "use targeted promotion only where needed"]) |
| if row["avg_waste_pct"] > waste_thr: |
| actions.append("bring markdown trigger forward") |
| headline = "Optimize locally" |
|
|
| if row["avg_waste_pct"] >= waste_thr and "markdown earlier" not in actions: |
| actions.append("reduce waste with earlier markdown") |
| if row["avg_profit_margin"] < 0 and "review price / cost" not in actions and row["segment"] != "Reduce β": |
| actions.append("review price / cost") |
| if row["avg_sell_through"] < sell_thr * 0.85: |
| actions.append("check demand realism before restocking") |
|
|
| action_text = "; ".join(dict.fromkeys(actions)) |
| return headline, action_text |
|
|
|
|
| def _simulate_discount_profit(product_scope: pd.DataFrame, new_discount_pct: float) -> dict: |
| sim = product_scope.copy() |
| if sim.empty: |
| return {} |
| new_discount_pct = float(np.clip(new_discount_pct, 0, 0.8)) |
| current_discount = float(sim["discount_pct"].mean()) |
| base_price = sim["base_price"].replace(0, np.nan).fillna(sim["selling_price"]).astype(float) |
| cost_price = sim["cost_price"].astype(float) if "cost_price" in sim.columns else sim["selling_price"].astype(float) * 0.7 |
| initial_qty = sim["initial_quantity"].astype(float).clip(lower=0) |
| daily_demand = sim["daily_demand"].astype(float).clip(lower=0) |
|
|
| discount_delta = new_discount_pct - current_discount |
| demand_multiplier = 1 + discount_delta * 1.35 |
| demand_multiplier = np.clip(demand_multiplier, 0.65, 1.45) |
|
|
| sim_price = base_price * (1 - new_discount_pct) |
| sim_units = np.minimum(initial_qty, daily_demand * demand_multiplier).clip(lower=0) |
| sim_leftover = (initial_qty - sim_units).clip(lower=0) |
| sim_profit = ((sim_price - cost_price) * sim_units - cost_price * 0.35 * sim_leftover).mean() |
| current_profit = float(sim["profit"].mean()) |
| current_units = float(sim["units_sold"].mean()) |
| sim_waste_pct = float((sim_leftover / initial_qty.replace(0, np.nan)).fillna(0).mean()) |
|
|
| return { |
| "current_discount": current_discount, |
| "new_discount": new_discount_pct, |
| "current_profit": current_profit, |
| "sim_profit": float(sim_profit), |
| "profit_delta": float(sim_profit - current_profit), |
| "current_units": current_units, |
| "sim_units": float(sim_units.mean()), |
| "units_delta": float(sim_units.mean() - current_units), |
| "sim_waste_pct": sim_waste_pct, |
| } |
|
|
| def _run_category_deep_dive(cat_df: pd.DataFrame) -> dict | None: |
| if cat_df.empty: |
| return None |
| working = cat_df.copy() |
| working["profit_margin"] = _safe_profit_margin(working) |
| if "spoilage_risk" not in working.columns: |
| working["spoilage_risk"] = working["waste_pct"] |
| if "markdown_applied" not in working.columns: |
| working["markdown_applied"] = (working["discount_pct"] > 0).astype(int) |
| if "shelf_life_days" not in working.columns: |
| working["shelf_life_days"] = working["days_until_expiry"] |
|
|
| entity = working.groupby(["store_id", "product_name"], dropna=False).agg( |
| region=("region", "first"), |
| avg_profit=("profit", "mean"), |
| avg_profit_margin=("profit_margin", "mean"), |
| avg_waste_pct=("waste_pct", "mean"), |
| avg_sell_through=("sell_through_pct", "mean"), |
| avg_discount_pct=("discount_pct", "mean"), |
| avg_daily_demand=("daily_demand", "mean"), |
| avg_initial_qty=("initial_quantity", "mean"), |
| avg_days_until_expiry=("days_until_expiry", "mean"), |
| avg_units_sold=("units_sold", "mean"), |
| avg_cost_price=("cost_price", "mean") if "cost_price" in working.columns else ("selling_price", "mean"), |
| avg_base_price=("base_price", "mean"), |
| avg_markdown_applied=("markdown_applied", "mean"), |
| avg_is_weekend=("is_weekend", "mean"), |
| avg_spoilage_risk=("spoilage_risk", "mean"), |
| avg_shelf_life_days=("shelf_life_days", "mean"), |
| ).reset_index() |
| if len(entity) < 8: |
| return None |
|
|
| from sklearn.cluster import KMeans |
| from sklearn.decomposition import PCA |
| from sklearn.ensemble import RandomForestRegressor |
| from sklearn.metrics import mean_absolute_error, r2_score |
| from sklearn.model_selection import train_test_split |
| from sklearn.preprocessing import StandardScaler |
|
|
| pca_features = [ |
| "avg_profit_margin", "avg_days_until_expiry", "avg_sell_through", "avg_waste_pct", |
| "avg_discount_pct", "avg_daily_demand", "avg_initial_qty" |
| ] |
| X_pca = entity[pca_features].fillna(entity[pca_features].median()) |
| scaler = StandardScaler() |
| X_scaled = scaler.fit_transform(X_pca) |
| pca = PCA(n_components=2, random_state=42) |
| pca_scores = pca.fit_transform(X_scaled) |
| loadings = pd.DataFrame(pca.components_.T, index=pca_features, columns=["PC1", "PC2"]) |
| pc1_loadings = loadings["PC1"].sort_values(key=np.abs, ascending=False).head(5) |
| pc2_loadings = loadings["PC2"].sort_values(key=np.abs, ascending=False).head(5) |
|
|
| cluster_count = 4 if len(entity) >= 20 else max(2, min(4, len(entity) // 4)) |
| km = KMeans(n_clusters=cluster_count, n_init=20, random_state=42) |
| entity["cluster_id"] = km.fit_predict(X_scaled) |
| entity["pc1"] = pca_scores[:, 0] |
| entity["pc2"] = pca_scores[:, 1] |
|
|
| cluster_summary = entity.groupby("cluster_id").agg( |
| demand=("avg_daily_demand", "mean"), |
| sell_through=("avg_sell_through", "mean"), |
| waste=("avg_waste_pct", "mean"), |
| margin=("avg_profit_margin", "mean"), |
| avg_profit=("avg_profit", "mean"), |
| sku_store_pairs=("product_name", "count"), |
| ).reset_index() |
| cluster_summary = _assign_cluster_names(cluster_summary) |
| entity = entity.merge(cluster_summary[["cluster_id", "cluster_name"]], on="cluster_id", how="left") |
| cluster_centers = entity.groupby("cluster_name", dropna=False).agg( |
| pc1=("pc1", "mean"), |
| pc2=("pc2", "mean"), |
| avg_profit=("avg_profit", "mean"), |
| ).reset_index() |
|
|
| product_summary = entity.groupby("product_name", dropna=False).agg( |
| avg_profit=("avg_profit", "mean"), |
| avg_profit_margin=("avg_profit_margin", "mean"), |
| avg_waste_pct=("avg_waste_pct", "mean"), |
| avg_sell_through=("avg_sell_through", "mean"), |
| avg_discount_pct=("avg_discount_pct", "mean"), |
| avg_daily_demand=("avg_daily_demand", "mean"), |
| avg_initial_qty=("avg_initial_qty", "mean"), |
| avg_days_until_expiry=("avg_days_until_expiry", "mean"), |
| avg_units_sold=("avg_units_sold", "mean"), |
| avg_cost_price=("avg_cost_price", "mean"), |
| avg_base_price=("avg_base_price", "mean"), |
| store_count=("store_id", "nunique"), |
| region_count=("region", "nunique"), |
| observations=("store_id", "count"), |
| ).reset_index() |
| product_summary["health_score"] = ( |
| product_summary["avg_sell_through"] * 0.40 |
| + product_summary["avg_profit_margin"].clip(lower=-1, upper=1) * 0.30 |
| - product_summary["avg_waste_pct"] * 0.20 |
| + (product_summary["avg_profit"] > 0).astype(int) * 0.10 |
| ) |
| product_summary = product_summary.sort_values(["health_score", "avg_profit"], ascending=[False, False]) |
|
|
| sell_thr = float(product_summary["avg_sell_through"].median()) |
| waste_thr = float(product_summary["avg_waste_pct"].quantile(0.75)) |
| discount_thr = float(product_summary["avg_discount_pct"].median()) |
| demand_thr = float(product_summary["avg_daily_demand"].median()) |
| product_summary["segment"] = product_summary.apply(lambda r: _classify_product_segment(r, sell_thr, waste_thr), axis=1) |
|
|
| strategy_pack = product_summary.apply( |
| lambda r: _generate_sku_strategy(r, sell_thr, waste_thr, discount_thr, demand_thr), |
| axis=1, |
| result_type="expand", |
| ) |
| product_summary[["strategy_headline", "action"]] = strategy_pack |
|
|
| good_products = product_summary[product_summary["segment"] == "Scale β"].copy().sort_values( |
| ["avg_profit", "health_score"], ascending=[False, False] |
| ).head(8) |
|
|
| risky_products = product_summary[product_summary["segment"].isin(["Fix Pricing β οΈ", "Reduce β"])].copy().sort_values( |
| ["avg_profit", "avg_waste_pct"], ascending=[True, False] |
| ).head(8) |
|
|
| product_focus = product_summary[[ |
| "product_name", "segment", "strategy_headline", "avg_profit", "avg_profit_margin", |
| "avg_waste_pct", "avg_sell_through", "avg_daily_demand", "avg_discount_pct", "action" |
| ]].copy().sort_values(["segment", "avg_profit"], ascending=[True, False]) |
|
|
| model_features = [ |
| "discount_pct", "initial_quantity", "base_price", |
| "cost_price" if "cost_price" in working.columns else "selling_price", |
| "days_until_expiry", "daily_demand", "markdown_applied", "is_weekend", |
| "spoilage_risk", "shelf_life_days" |
| ] |
| model_df = working[model_features + ["profit"]].copy().dropna() |
| if len(model_df) < 20: |
| model_df = working[model_features + ["profit"]].fillna(0) |
| X = model_df[model_features] |
| y = model_df["profit"] |
| test_size = 0.25 if len(model_df) >= 40 else 0.2 |
| X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=test_size, random_state=42) |
| rf = RandomForestRegressor(n_estimators=200, max_depth=8, min_samples_leaf=5, random_state=42) |
| rf.fit(X_train, y_train) |
| pred = rf.predict(X_test) |
| importances = pd.Series(rf.feature_importances_, index=model_features).sort_values(ascending=False) |
|
|
| category = str(cat_df["category"].dropna().iloc[0]) |
| region_profit = cat_df.groupby("region")["profit"].mean().sort_values().reset_index() |
| good_exists = not good_products.empty |
| pricing_issue_count = int((product_summary["segment"] == "Fix Pricing β οΈ").sum()) |
| reduce_count = int((product_summary["segment"] == "Reduce β").sum()) |
| if good_exists: |
| best_names = ", ".join(good_products["product_name"].head(3).tolist()) |
| product_readout = ( |
| f"Within {category}, true scale pockets exist, led by {best_names}. " |
| f"At the same time, {pricing_issue_count} SKU groups are selling well but not monetizing demand, " |
| f"and {reduce_count} look like reduce-or-reassort candidates." |
| ) |
| else: |
| product_readout = ( |
| f"No true scale group is visible in the current {category} scope. " |
| f"Most products sit in pricing-fix or reduce buckets, which points to a category-wide reset rather than isolated fixes." |
| ) |
|
|
| return { |
| "category": category, |
| "avg_profit": float(cat_df["profit"].mean()), |
| "avg_margin": float(_safe_profit_margin(cat_df).mean()), |
| "avg_waste": float(cat_df["waste_pct"].mean()), |
| "avg_units_sold": float(cat_df["units_sold"].mean()), |
| "avg_daily_demand": float(cat_df["daily_demand"].mean()), |
| "region_profit": region_profit, |
| "entity": entity, |
| "product_summary": product_summary, |
| "good_products": good_products, |
| "risky_products": risky_products, |
| "product_focus": product_focus, |
| "good_product_count": int(len(good_products)), |
| "good_product_share": float(len(good_products) / max(len(product_summary), 1)), |
| "product_segment_counts": product_summary["segment"].value_counts().to_dict(), |
| "sell_thr": sell_thr, |
| "waste_thr": waste_thr, |
| "discount_thr": discount_thr, |
| "product_readout": product_readout, |
| "pc1": float(pca.explained_variance_ratio_[0]), |
| "pc2": float(pca.explained_variance_ratio_[1]), |
| "pc1_theme": "Efficiency / margin quality axis", |
| "pc2_theme": "Demand / replenishment intensity axis", |
| "pc1_loadings": [(idx, f"{val:+.3f}") for idx, val in pc1_loadings.items()], |
| "pc2_loadings": [(idx, f"{val:+.3f}") for idx, val in pc2_loadings.items()], |
| "cluster_summary": cluster_summary, |
| "cluster_centers": cluster_centers, |
| "cluster_color_map": _cluster_color_map(cluster_summary["cluster_name"].tolist()), |
| "r2": float(r2_score(y_test, pred)) if len(y_test) > 1 else 0.0, |
| "mae": float(mean_absolute_error(y_test, pred)), |
| "drivers": importances, |
| "library": PCA_SHAP_LIBRARY.get(category), |
| } |
|
|
|
|
| def build_pca_shap_dashboard(scope_df: pd.DataFrame): |
| st.markdown("### PCA / SHAP Deep Dive") |
| st.caption("PCA identifies internal operating structures inside the current scope, while SHAP-style explainability summarizes the main profit drivers for actionable retail levers.") |
|
|
| categories = sorted(scope_df["category"].dropna().unique().tolist()) |
| if not categories: |
| st.info("No category remains in the current filter.") |
| return |
|
|
| if len(categories) != 1: |
| summary = scope_df.groupby("category").agg( |
| avg_profit=("profit", "mean"), |
| avg_waste=("waste_pct", "mean"), |
| avg_demand=("daily_demand", "mean"), |
| avg_sell_through=("sell_through_pct", "mean"), |
| avg_discount=("discount_pct", "mean"), |
| avg_stock=("initial_quantity", "mean"), |
| ).reset_index() |
| summary["efficiency_index"] = summary["avg_sell_through"] - summary["avg_waste"] - summary["avg_discount"] * 0.25 |
| summary["recommended_focus"] = np.where( |
| summary["avg_profit"] < 0, |
| "Fix pricing / promotion logic first", |
| np.where(summary["avg_waste"] > summary["avg_waste"].median(), "Reduce stock and markdown earlier", "Protect availability and avoid unnecessary discounting"), |
| ) |
| c1, c2, c3, c4 = st.columns(4) |
| c1.metric("Categories in scope", len(summary)) |
| c2.metric("Avg scope profit", f"EUR {scope_df['profit'].mean():.2f}") |
| c3.metric("Avg scope waste", f"{scope_df['waste_pct'].mean():.1%}") |
| c4.metric("Highest-profit category", summary.sort_values("avg_profit", ascending=False).iloc[0]["category"]) |
| left, right = st.columns([1.1, 1]) |
| with left: |
| fig = px.scatter( |
| summary, |
| x="avg_demand", |
| y="avg_waste", |
| size=summary["avg_profit"].abs() + 1, |
| color="category", |
| hover_data=["avg_sell_through", "avg_discount", "avg_stock"], |
| title="Category operating map: demand vs waste", |
| ) |
| st.plotly_chart(fig, use_container_width=True) |
| with right: |
| fig = px.bar( |
| summary.sort_values("avg_profit", ascending=False), |
| x="category", |
| y=["avg_profit", "avg_discount"], |
| barmode="group", |
| title="Profit and discount pressure by category", |
| ) |
| st.plotly_chart(fig, use_container_width=True) |
| st.dataframe(summary[["category", "avg_profit", "avg_waste", "avg_demand", "recommended_focus"]], use_container_width=True, hide_index=True) |
| st.info("For a fuller PCA / SHAP deep dive, narrow the sidebar filter to a single category.") |
| return |
|
|
| category = categories[0] |
| cat_df = scope_df[scope_df["category"] == category].copy() |
| deep = _run_category_deep_dive(cat_df) |
| if deep is None: |
| st.info("Not enough observations in the current scope to build a reliable PCA / SHAP deep dive. Try a broader filter.") |
| return |
|
|
| snap = deep.get("library") |
| m1, m2, m3, m4, m5 = st.columns(5) |
| m1.metric("Avg profit", f"EUR {deep['avg_profit']:.2f}") |
| m2.metric("Avg margin", f"{deep['avg_margin']:.2%}") |
| m3.metric("Avg waste", f"{deep['avg_waste']:.2%}") |
| m4.metric("Avg units sold", f"{deep['avg_units_sold']:.2f}") |
| m5.metric("Avg daily demand", f"{deep['avg_daily_demand']:.2f}") |
|
|
| rp = deep["region_profit"] |
| if not rp.empty: |
| st.markdown("**Regional profit ranking**") |
| st.write(" Β· ".join([f"{r.region}: EUR {r.profit:.2f}" for r in rp.itertuples(index=False)])) |
|
|
| if snap: |
| st.markdown(f"**Business readout:** {snap['business']}") |
| else: |
| readout = f"{category} currently shows average profit of EUR {deep['avg_profit']:.2f}, waste of {deep['avg_waste']:.1%}, and average demand of {deep['avg_daily_demand']:.1f}. The deep dive below highlights which SKU-store mixes behave like winners versus loss makers." |
| st.markdown(f"**Business readout:** {readout}") |
|
|
| left, right = st.columns(2) |
| with left: |
| st.markdown("#### PCA insight") |
| st.markdown(f"**PC1 explained variance:** {deep['pc1']:.2%} ") |
| st.markdown(f"**PC2 explained variance:** {deep['pc2']:.2%}") |
| st.caption(f"PC1 theme: {deep['pc1_theme']} Β· PC2 theme: {deep['pc2_theme']}") |
| pc1_df = pd.DataFrame(deep['pc1_loadings'], columns=['feature', 'value']) |
| pc1_df['value'] = pc1_df['value'].astype(float) |
| fig_pc1 = px.bar(pc1_df.sort_values('value'), x='value', y='feature', orientation='h', title='PC1 loading chart') |
| st.plotly_chart(fig_pc1, use_container_width=True) |
| pc2_df = pd.DataFrame(deep['pc2_loadings'], columns=['feature', 'value']) |
| pc2_df['value'] = pc2_df['value'].astype(float) |
| fig_pc2 = px.bar(pc2_df.sort_values('value'), x='value', y='feature', orientation='h', title='PC2 loading chart') |
| st.plotly_chart(fig_pc2, use_container_width=True) |
| with right: |
| st.markdown("#### SHAP-style profit drivers") |
| st.markdown(f"**Model RΒ²:** {deep['r2']:.3f} ") |
| st.markdown(f"**Model MAE:** {deep['mae']:.2f}") |
| top3 = ", ".join(deep['drivers'].head(3).index.tolist()) |
| st.markdown(f"**Top variables:** {top3}") |
| if snap: |
| st.markdown(f"**Core conclusion:** {snap['conclusion']}") |
| else: |
| st.markdown(f"**Core conclusion:** In the current scope, {category} profit is most strongly shaped by {top3}.") |
| top_driver = deep['drivers'].index[0] |
| if top_driver == 'discount_pct': |
| st.error("β οΈ Core issue: profit is being driven by discounting, so the business is buying demand instead of monetizing it.") |
| else: |
| st.info(f"Main profit driver in this scope: {top_driver}") |
|
|
| chart_left, chart_right = st.columns([1.1, 1]) |
| with chart_left: |
| color_map = deep.get('cluster_color_map', {}) |
| ordered_clusters = deep['cluster_summary']['cluster_name'].tolist() |
| fig = px.scatter( |
| deep['entity'], |
| x='pc1', |
| y='pc2', |
| color='cluster_name', |
| category_orders={'cluster_name': ordered_clusters}, |
| color_discrete_map=color_map, |
| hover_data=['cluster_name', 'store_id', 'product_name', 'avg_profit', 'avg_waste_pct', 'avg_sell_through'], |
| title=f"PCA category map for {category}", |
| ) |
| centers = deep.get('cluster_centers', pd.DataFrame()) |
| if not centers.empty: |
| fig.add_trace( |
| go.Scatter( |
| x=centers['pc1'], |
| y=centers['pc2'], |
| mode='markers+text', |
| text=centers['cluster_name'], |
| textposition='top center', |
| marker=dict(size=16, color='black', symbol='x'), |
| name='Cluster center', |
| showlegend=False, |
| ) |
| ) |
| st.plotly_chart(fig, use_container_width=True) |
| with chart_right: |
| driver_df = deep['drivers'].head(10).sort_values(ascending=True).reset_index() |
| driver_df.columns = ['variable', 'importance'] |
| fig = px.bar(driver_df, x='importance', y='variable', orientation='h', color='importance', title='Profit drivers (SHAP-style)') |
| st.plotly_chart(fig, use_container_width=True) |
|
|
| cluster_summary = deep['cluster_summary'].copy() |
| cluster_summary['sell_through'] = cluster_summary['sell_through'].map(lambda x: f"{x:.2%}") |
| cluster_summary['waste'] = cluster_summary['waste'].map(lambda x: f"{x:.2%}") |
| cluster_summary['margin'] = cluster_summary['margin'].map(lambda x: f"{x:.2%}") |
| cluster_summary['avg_profit'] = cluster_summary['avg_profit'].map(lambda x: f"EUR {x:.2f}") |
| st.markdown("#### PCA cluster action map") |
| st.dataframe(cluster_summary[["cluster_name", "demand", "sell_through", "waste", "margin", "avg_profit", "sku_store_pairs"]], use_container_width=True, hide_index=True) |
|
|
| if snap: |
| actions = pd.DataFrame({"recommended action": snap['actions']}) |
| else: |
| top_cluster = deep['cluster_summary'].sort_values(['avg_profit', 'waste'], ascending=[False, True]).iloc[0]['cluster_name'] |
| worst_cluster = deep['cluster_summary'].sort_values(['avg_profit', 'waste'], ascending=[True, False]).iloc[0]['cluster_name'] |
| actions = pd.DataFrame({"recommended action": [ |
| f"Protect availability and shelf space for {top_cluster.lower()}.", |
| f"Reduce stock depth and markdown earlier in {worst_cluster.lower()}.", |
| "Review price-discount logic on any high-demand item that still carries weak margin.", |
| ]}) |
| st.markdown("#### Recommended actions") |
| st.dataframe(actions, use_container_width=True, hide_index=True) |
|
|
| st.markdown("#### Decision map: SKU actions") |
| sku_map = deep["product_summary"][["product_name", "segment", "avg_profit", "avg_sell_through", "avg_waste_pct", "avg_discount_pct"]].copy() |
| sku_map.rename(columns={ |
| "avg_sell_through": "sell_through", |
| "avg_waste_pct": "waste", |
| "avg_discount_pct": "discount_pct", |
| }, inplace=True) |
| if not sku_map.empty: |
| fig_decision = px.scatter( |
| sku_map, |
| x="sell_through", |
| y="avg_profit", |
| color="segment", |
| size=sku_map["waste"].clip(lower=0.001) * 100, |
| hover_data=["product_name", "waste", "discount_pct"], |
| title="SKU decision map: sell-through vs profit", |
| ) |
| fig_decision.add_vline(x=deep.get("sell_thr", float(sku_map["sell_through"].median())), line_dash="dot") |
| fig_decision.add_hline(y=0, line_dash="dot") |
| st.plotly_chart(fig_decision, use_container_width=True) |
|
|
| st.markdown("#### Product-level strategy engine") |
| st.info(deep["product_readout"]) |
| seg_counts = deep.get("product_segment_counts", {}) |
| p1, p2, p3, p4 = st.columns(4) |
| p1.metric("Products analysed", len(deep["product_summary"])) |
| p2.metric("Scale β", seg_counts.get("Scale β", 0)) |
| p3.metric("Fix Pricing β οΈ", seg_counts.get("Fix Pricing β οΈ", 0)) |
| p4.metric("Reduce β", seg_counts.get("Reduce β", 0)) |
|
|
| prod_left, prod_right = st.columns(2) |
| with prod_left: |
| winners = deep["good_products"].copy() |
| if winners.empty: |
| st.warning("No product currently qualifies as a true scale winner in this scope.") |
| else: |
| winners_display = winners[["product_name", "segment", "avg_profit", "avg_profit_margin", "avg_sell_through", "avg_waste_pct", "strategy_headline"]].copy() |
| winners_display.columns = ["product_name", "segment", "avg_profit", "margin", "sell_through", "waste", "strategy"] |
| winners_display["avg_profit"] = winners_display["avg_profit"].map(lambda x: f"EUR {x:.2f}") |
| winners_display["margin"] = winners_display["margin"].map(lambda x: f"{x:.1%}") |
| winners_display["sell_through"] = winners_display["sell_through"].map(lambda x: f"{x:.1%}") |
| winners_display["waste"] = winners_display["waste"].map(lambda x: f"{x:.1%}") |
| st.markdown("**Scale (high demand + profitable)**") |
| st.dataframe(winners_display, use_container_width=True, hide_index=True) |
| with prod_right: |
| risk = deep["risky_products"].copy() |
| if risk.empty: |
| st.success("No pricing-fix or reduce candidates are visible in this scope.") |
| else: |
| risk_display = risk[["product_name", "segment", "avg_profit", "avg_profit_margin", "avg_sell_through", "avg_waste_pct", "strategy_headline"]].copy() |
| risk_display.columns = ["product_name", "segment", "avg_profit", "margin", "sell_through", "waste", "strategy"] |
| risk_display["avg_profit"] = risk_display["avg_profit"].map(lambda x: f"EUR {x:.2f}") |
| risk_display["margin"] = risk_display["margin"].map(lambda x: f"{x:.1%}") |
| risk_display["sell_through"] = risk_display["sell_through"].map(lambda x: f"{x:.1%}") |
| risk_display["waste"] = risk_display["waste"].map(lambda x: f"{x:.1%}") |
| st.markdown("**Fix / Reduce (high risk products)**") |
| st.dataframe(risk_display, use_container_width=True, hide_index=True) |
|
|
| if not deep["product_focus"].empty: |
| focus = deep["product_focus"].copy() |
| focus["avg_profit"] = focus["avg_profit"].map(lambda x: f"EUR {x:.2f}") |
| focus["avg_profit_margin"] = focus["avg_profit_margin"].map(lambda x: f"{x:.1%}") |
| focus["avg_waste_pct"] = focus["avg_waste_pct"].map(lambda x: f"{x:.1%}") |
| focus["avg_sell_through"] = focus["avg_sell_through"].map(lambda x: f"{x:.1%}") |
| focus["avg_discount_pct"] = focus["avg_discount_pct"].map(lambda x: f"{x:.1%}") |
| focus.rename(columns={ |
| "avg_profit_margin": "margin", |
| "avg_waste_pct": "waste", |
| "avg_sell_through": "sell_through", |
| "avg_daily_demand": "demand", |
| "avg_discount_pct": "discount", |
| "strategy_headline": "strategy", |
| }, inplace=True) |
| st.markdown("#### SKU auto strategy table") |
| st.dataframe(focus[["product_name", "segment", "strategy", "avg_profit", "margin", "sell_through", "waste", "discount", "demand", "action"]], use_container_width=True, hide_index=True) |
|
|
|
|
|
|
|
|
|
|
| def forecast_filtered_demand(scope_df: pd.DataFrame, label: str = "Filtered selection", model_bundle=None) -> pd.DataFrame: |
| d = scope_df.copy() |
| if d.empty: |
| return pd.DataFrame() |
|
|
| hist = ( |
| d.groupby("transaction_date", as_index=False) |
| .agg(daily_demand=("daily_demand", "mean")) |
| .sort_values("transaction_date") |
| ) |
| if len(hist) < 14: |
| return pd.DataFrame() |
|
|
| recent = d[d["transaction_date"] >= d["transaction_date"].max() - pd.Timedelta(days=56)].copy() |
| if recent.empty: |
| recent = d.copy() |
|
|
| recent_hybrid = add_hybrid_forecast(recent, model_bundle=model_bundle) |
| recent_hybrid["dow"] = recent_hybrid["transaction_date"].dt.dayofweek |
|
|
| weekday_actual = recent.groupby(recent["transaction_date"].dt.dayofweek)["daily_demand"].mean().to_dict() |
| weekday_forecast = recent_hybrid.groupby("dow")["forecast_units"].mean().to_dict() |
|
|
| fallback_actual = float(hist["daily_demand"].tail(14).mean()) |
| last_date = hist["transaction_date"].max() |
| future_dates = pd.date_range(last_date + pd.Timedelta(days=1), periods=14, freq="D") |
|
|
| future_vals = [] |
| for dt in future_dates: |
| dow = int(dt.dayofweek) |
| base = float(weekday_actual.get(dow, fallback_actual)) |
| hybrid = float(weekday_forecast.get(dow, base)) |
| blended = 0.30 * hybrid + 0.70 * base |
| val = float(np.clip(blended, base * 0.95, base * 1.05)) |
| future_vals.append(val) |
|
|
| future = pd.DataFrame({ |
| "transaction_date": future_dates, |
| "daily_demand": future_vals, |
| "series": "Forecast", |
| "scope": label, |
| }) |
|
|
| hist = hist.tail(60).copy() |
| hist["series"] = "Actual" |
| hist["scope"] = label |
| return pd.concat([hist, future], ignore_index=True) |
|
|
|
|
| def executive_overview(df: pd.DataFrame, exec_scope: pd.DataFrame, scope_info: dict): |
| st.subheader("FRESHIE Β· Executive Overview") |
|
|
| revenue = float((exec_scope["selling_price"] * exec_scope["units_sold"]).sum()) |
| profit = float(exec_scope["profit"].sum()) |
| units_sold = float(exec_scope["units_sold"].sum()) |
| units_wasted = float(exec_scope["units_wasted"].sum()) |
|
|
| c1, c2, c3, c4 = st.columns(4) |
| c1.metric("Revenue", f"EUR {revenue:,.0f}") |
| c2.metric("Profit", f"EUR {profit:,.0f}") |
| c3.metric("Units sold", f"{units_sold:,.0f}") |
| c4.metric("Units wasted", f"{units_wasted:,.0f}") |
|
|
| st.markdown( |
| '<div class="note-box"><b>Executive view:</b> monthly performance plus a short diagnosis of unusual revenue-profit gaps.</div>', |
| unsafe_allow_html=True, |
| ) |
|
|
| monthly = exec_scope.groupby("month").agg( |
| revenue=("selling_price", lambda s: float((exec_scope.loc[s.index, "selling_price"] * exec_scope.loc[s.index, "units_sold"]).sum())), |
| profit=("profit", "sum"), |
| units_sold=("units_sold", "sum"), |
| units_wasted=("units_wasted", "sum"), |
| waste_rate=("waste_pct", "mean"), |
| stockout_rate=("stockout_flag", "mean"), |
| ).reset_index().sort_values("month") |
|
|
| monthly["gap"] = (monthly["revenue"] - monthly["profit"]).abs() |
| gap_std = monthly["gap"].std(ddof=0) |
| monthly["gap_z"] = (monthly["gap"] - monthly["gap"].mean()) / (gap_std if gap_std else 1) |
|
|
| left, right = st.columns([1.3, 1]) |
| with left: |
| fig = go.Figure() |
| fig.add_trace(go.Scatter(x=monthly["month"], y=monthly["revenue"], name="Revenue", mode="lines+markers")) |
| fig.add_trace(go.Scatter(x=monthly["month"], y=monthly["profit"], name="Profit", mode="lines+markers", yaxis="y2")) |
| fig.update_layout( |
| title="Monthly revenue and profit trend", |
| yaxis=dict(title="Revenue"), |
| yaxis2=dict(title="Profit", overlaying="y", side="right"), |
| margin=dict(l=10, r=10, t=40, b=10), |
| ) |
| st.plotly_chart(fig, use_container_width=True) |
|
|
| fig2 = go.Figure() |
| fig2.add_trace(go.Bar(x=monthly["month"], y=monthly["units_sold"], name="Units sold")) |
| fig2.add_trace(go.Scatter(x=monthly["month"], y=monthly["units_wasted"], name="Units wasted trend", mode="lines+markers", yaxis="y2")) |
| fig2.update_layout( |
| title="Units sold vs units wasted by month", |
| yaxis=dict(title="Units sold"), |
| yaxis2=dict(title="Units wasted", overlaying="y", side="right"), |
| margin=dict(l=10, r=10, t=40, b=10), |
| ) |
| st.plotly_chart(fig2, use_container_width=True) |
|
|
| with right: |
| st.markdown("### Executive diagnosis") |
| preferred_months = [m for m in ["2023-01", "2023-02", "2023-03", "2024-01", "2024-10"] if m in monthly["month"].astype(str).tolist()] |
| flagged = monthly[monthly["month"].astype(str).isin(preferred_months)].copy() |
| if flagged.empty: |
| flagged = monthly[monthly["gap_z"] > 1].copy() |
| if flagged.empty: |
| flagged = monthly.nlargest(min(4, len(monthly)), "gap").copy() |
|
|
| for _, row in flagged.iterrows(): |
| mo = row["month"] |
| sub = exec_scope[exec_scope["month"] == mo].copy() |
| if sub.empty: |
| continue |
| top_waste_category = sub.groupby("category")["waste_pct"].mean().sort_values(ascending=False).index[0] |
| top_stockout_category = sub.groupby("category")["stockout_flag"].mean().sort_values(ascending=False).index[0] |
| top_discount_category = sub.groupby("category")["discount_pct"].mean().sort_values(ascending=False).index[0] |
|
|
| reasons = [] |
| if sub["waste_pct"].mean() > exec_scope["waste_pct"].mean(): |
| reasons.append("waste rose above baseline") |
| if sub["stockout_flag"].mean() > exec_scope["stockout_flag"].mean(): |
| reasons.append("stockout pressure increased") |
| if sub["discount_pct"].mean() > exec_scope["discount_pct"].mean(): |
| reasons.append("markdown intensity was heavier") |
| if sub["profit"].mean() < exec_scope["profit"].mean(): |
| reasons.append("unit profitability weakened") |
| reason_text = ", ".join(reasons) if reasons else "mixed category volatility" |
|
|
| st.markdown( |
| f"- **{mo}**: the revenue-profit gap widened. Likely drivers were **{top_waste_category}**, " |
| f"stockout pressure in **{top_stockout_category}**, and markdown depth in **{top_discount_category}**. " |
| f"Overall explanation: {reason_text}." |
| ) |
|
|
| st.markdown("### Current operating signal") |
| selected_stores = scope_info.get("stores", []) |
| selected_regions = scope_info.get("regions", []) |
|
|
| if exec_scope["store_id"].nunique() == 1: |
| focus_store = exec_scope["store_id"].iloc[0] |
| st.markdown(f"- Current view is focused on **{focus_store}**.") |
| elif selected_stores or selected_regions: |
| worst_store = exec_scope.groupby("store_id")["waste_pct"].mean().sort_values(ascending=False).index[0] |
| st.markdown(f"- Highest waste pressure in the current scope sits in **{worst_store}**.") |
| else: |
| worst_region = exec_scope.groupby("region")["waste_pct"].mean().sort_values(ascending=False).index[0] |
| st.markdown(f"- Highest waste pressure in the current scope sits in **{worst_region} region**.") |
|
|
| best_category = exec_scope.groupby("category")["profit"].mean().sort_values(ascending=False).index[0] |
| risky_category = exec_scope.groupby("category")["waste_pct"].mean().sort_values(ascending=False).index[0] |
| stockout_category = exec_scope.groupby("category")["stockout_flag"].mean().sort_values(ascending=False).index[0] |
| st.markdown(f"- Strongest profit signal currently comes from **{best_category}**.") |
| st.markdown(f"- Highest waste exposure category is **{risky_category}**.") |
| st.markdown(f"- Highest stockout exposure category is **{stockout_category}**.") |
|
|
|
|
| def category_intelligence(df: pd.DataFrame): |
| st.subheader("FRESHIE Β· Category Intelligence") |
| st.markdown( |
| '<div class="note-box"><b>Category view:</b> chart-led category, region, store, and forecast insights for the current filters.</div>', |
| unsafe_allow_html=True, |
| ) |
|
|
| build_pca_shap_dashboard(df) |
|
|
| cat_summary = df.groupby("category").agg( |
| avg_demand=("daily_demand", "mean"), |
| avg_stock=("initial_quantity", "mean"), |
| avg_remaining=("leftover_units", "mean"), |
| waste_pct=("waste_pct", "mean"), |
| stockout_rate=("stockout_flag", "mean"), |
| avg_profit=("profit", "mean"), |
| sell_through=("sell_through_pct", "mean"), |
| lost_sales=("lost_sales_units", "mean"), |
| ).reset_index() |
|
|
| region_cat = df.groupby(["region", "category"]).agg( |
| avg_demand=("daily_demand", "mean"), |
| avg_profit=("profit", "mean"), |
| waste_pct=("waste_pct", "mean"), |
| stockout_rate=("stockout_flag", "mean"), |
| ).reset_index() |
|
|
| store_cat = df.groupby(["store_id", "category"]).agg( |
| avg_demand=("daily_demand", "mean"), |
| avg_stock=("initial_quantity", "mean"), |
| waste_pct=("waste_pct", "mean"), |
| stockout_rate=("stockout_flag", "mean"), |
| avg_profit=("profit", "mean"), |
| ).reset_index() |
|
|
| weekpart = df.copy() |
| weekpart["week_part"] = np.where(weekpart["is_weekend"] == 1, "Weekend", "Weekday") |
| week_summary = weekpart.groupby(["category", "week_part"]).agg( |
| avg_demand=("daily_demand", "mean"), |
| avg_stock=("initial_quantity", "mean"), |
| waste_pct=("waste_pct", "mean"), |
| stockout_rate=("stockout_flag", "mean"), |
| avg_profit=("profit", "mean"), |
| ).reset_index() |
|
|
| k1, k2, k3, k4 = st.columns(4) |
| k1.metric("Avg demand", f"{df['daily_demand'].mean():.1f}") |
| k2.metric("Waste rate", f"{df['waste_pct'].mean():.1%}") |
| k3.metric("Stockout rate", f"{df['stockout_flag'].mean():.1%}") |
| k4.metric("Avg profit", f"EUR {df['profit'].mean():.2f}") |
|
|
| top_left, top_right = st.columns([1.25, 1]) |
| with top_left: |
| fig = px.bar( |
| cat_summary.sort_values("avg_demand", ascending=False), |
| x="category", |
| y=["avg_demand", "avg_stock", "avg_remaining"], |
| barmode="group", |
| title="Category demand, stock, and remaining inventory", |
| ) |
| st.plotly_chart(fig, use_container_width=True) |
|
|
| if df["category"].nunique() == 1: |
| prod_scatter = df.groupby("product_name").agg( |
| stockout_rate=("stockout_flag", "mean"), |
| waste_pct=("waste_pct", "mean"), |
| avg_profit=("profit", "mean"), |
| sell_through=("sell_through_pct", "mean"), |
| avg_discount=("discount_pct", "mean"), |
| ).reset_index() |
| stock_med = float(prod_scatter["stockout_rate"].median()) if not prod_scatter.empty else 0.0 |
| waste_med = float(prod_scatter["waste_pct"].median()) if not prod_scatter.empty else 0.0 |
|
|
| def _product_action_label(row): |
| if row["stockout_rate"] >= stock_med and row["waste_pct"] < waste_med: |
| return "Restock" |
| if row["waste_pct"] >= waste_med and row["avg_profit"] < 0: |
| return "Reduce" |
| if row["avg_profit"] < 0: |
| return "Fix price" |
| return "Scale" |
|
|
| prod_scatter["action"] = prod_scatter.apply(_product_action_label, axis=1) |
| fig2 = px.scatter( |
| prod_scatter, |
| x="stockout_rate", |
| y="waste_pct", |
| size=prod_scatter["avg_profit"].abs() + 1, |
| color="action", |
| hover_data=["product_name", "sell_through", "avg_discount", "avg_profit"], |
| title="Product trade-off: stockout vs waste", |
| ) |
| fig2.add_hline(y=waste_med, line_dash="dot") |
| fig2.add_vline(x=stock_med, line_dash="dot") |
| else: |
| fig2 = px.scatter( |
| cat_summary, |
| x="stockout_rate", |
| y="waste_pct", |
| size=(cat_summary["avg_profit"].abs() + 1), |
| color="category", |
| hover_data=["avg_demand", "avg_stock", "avg_remaining", "sell_through", "lost_sales"], |
| title="Category trade-off: stockout vs waste", |
| ) |
| st.plotly_chart(fig2, use_container_width=True) |
|
|
| with top_right: |
| st.markdown("### Core indicators and recommendations") |
| for _, r in cat_summary.sort_values("avg_demand", ascending=False).iterrows(): |
| advice = build_category_recommendations(r, cat_summary) |
| st.markdown( |
| f"- **{r['category']}**: demand {r['avg_demand']:.1f}, stock {r['avg_stock']:.1f}, " |
| f"waste {r['waste_pct']:.1%}, stockout {r['stockout_rate']:.1%}, profit EUR {r['avg_profit']:.2f}. " |
| f"**Recommendation:** " + "; ".join(advice) + "." |
| ) |
|
|
| lower_left, lower_right = st.columns([1.25, 1]) |
| with lower_left: |
| fig3 = px.density_heatmap( |
| region_cat, |
| x="category", |
| y="region", |
| z="avg_profit", |
| title="Regional profit heatmap by category", |
| ) |
| st.plotly_chart(fig3, use_container_width=True) |
|
|
| fig4 = px.bar( |
| week_summary, |
| x="category", |
| y="avg_demand", |
| color="week_part", |
| barmode="group", |
| title="Weekday vs weekend demand by category", |
| ) |
| st.plotly_chart(fig4, use_container_width=True) |
|
|
| with lower_right: |
| fig5 = px.bar( |
| store_cat.sort_values("avg_profit", ascending=False).head(20), |
| x="store_id", |
| y="avg_profit", |
| color="category", |
| title="Top filtered stores by category profit", |
| ) |
| st.plotly_chart(fig5, use_container_width=True) |
|
|
| scope_label = "Filtered selection" |
| if df["store_id"].nunique() == 1: |
| scope_label = df["store_id"].iloc[0] |
| elif df["store_id"].nunique() > 1: |
| scope_label = f"{df['store_id'].nunique()} stores" |
| elif df["region"].nunique() >= 1: |
| scope_label = " / ".join(sorted(df["region"].dropna().unique().tolist())) |
|
|
| model_bundle = train_hybrid_forecast_model(df) |
| forecast_df = forecast_filtered_demand(df, scope_label, model_bundle=model_bundle) |
| if not forecast_df.empty: |
| fig6 = px.line( |
| forecast_df, |
| x="transaction_date", |
| y="daily_demand", |
| color="series", |
| title=f"Demand forecast for {scope_label}", |
| ) |
| st.plotly_chart(fig6, use_container_width=True) |
| st.caption("Forecast method: ML baseline + heuristic adjustment. Both forecast panels use the same logic: a leakage-free ML demand baseline is blended with recent observed demand and then capped tightly around recent same-weekday actual levels.") |
|
|
|
|
|
|
|
|
| @st.cache_resource(show_spinner=False) |
| def train_hybrid_forecast_model(df: pd.DataFrame): |
| from sklearn.ensemble import RandomForestRegressor |
| from sklearn.metrics import r2_score |
| from sklearn.model_selection import train_test_split |
|
|
| work = df.copy() |
| work["month_num"] = work["transaction_date"].dt.month.astype(int) |
| work["month_sin"] = np.sin(2 * np.pi * work["month_num"] / 12.0) |
| work["month_cos"] = np.cos(2 * np.pi * work["month_num"] / 12.0) |
|
|
| base_num_cols = [ |
| "selling_price", "discount_pct", "days_until_expiry", "is_promoted", |
| "is_weekend", "month_sin", "month_cos", "waste_pct", |
| ] |
| for col in base_num_cols: |
| if col not in work.columns: |
| work[col] = 0 |
|
|
| model_df = work[base_num_cols + ["category", "region", "store_id", "daily_demand"]].copy() |
| model_df = model_df.replace([np.inf, -np.inf], np.nan).dropna(subset=["daily_demand"]) |
| if len(model_df) < 60: |
| return None, [], np.nan |
|
|
| X = pd.get_dummies(model_df.drop(columns=["daily_demand"]), columns=["category", "region", "store_id"], drop_first=False) |
| y = np.log1p(model_df["daily_demand"].clip(lower=0)) |
|
|
| X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42) |
| model = RandomForestRegressor( |
| n_estimators=220, |
| max_depth=10, |
| min_samples_leaf=10, |
| random_state=42, |
| n_jobs=-1, |
| ) |
| model.fit(X_train, y_train) |
| test_pred = np.expm1(model.predict(X_test)) |
| test_actual = np.expm1(y_test) |
| r2 = r2_score(test_actual, test_pred) if len(test_actual) > 1 else np.nan |
| return model, X.columns.tolist(), float(r2) if pd.notna(r2) else np.nan |
|
|
|
|
| def _prepare_forecast_features(work: pd.DataFrame, model_columns: list[str]) -> pd.DataFrame: |
| feat = work.copy() |
| feat["month_num"] = feat["transaction_date"].dt.month.astype(int) |
| feat["month_sin"] = np.sin(2 * np.pi * feat["month_num"] / 12.0) |
| feat["month_cos"] = np.cos(2 * np.pi * feat["month_num"] / 12.0) |
|
|
| base_num_cols = [ |
| "selling_price", "discount_pct", "days_until_expiry", "is_promoted", |
| "is_weekend", "month_sin", "month_cos", "waste_pct", |
| ] |
| for col in base_num_cols: |
| if col not in feat.columns: |
| feat[col] = 0 |
|
|
| X = pd.get_dummies( |
| feat[base_num_cols + ["category", "region", "store_id"]], |
| columns=["category", "region", "store_id"], |
| drop_first=False, |
| ) |
| X = X.reindex(columns=model_columns, fill_value=0) |
| return X |
|
|
|
|
| def add_hybrid_forecast(scope_df: pd.DataFrame, model_bundle=None) -> pd.DataFrame: |
| work = scope_df.copy() |
| if work.empty: |
| return work |
|
|
| if model_bundle is None: |
| model_bundle = train_hybrid_forecast_model(scope_df) |
| model, model_columns, model_r2 = model_bundle |
|
|
| anchor = work[["daily_demand", "units_sold"]].mean(axis=1).fillna(work["daily_demand"]).fillna(work["units_sold"]).fillna(0) |
|
|
| if model is None: |
| forecast = anchor.copy() |
| else: |
| X = _prepare_forecast_features(work, model_columns) |
| ml_pred = pd.Series(np.expm1(model.predict(X)), index=work.index) |
| forecast = 0.35 * ml_pred + 0.65 * anchor |
|
|
| forecast = forecast.astype(float) |
|
|
| sell = work.get("sell_through_pct", pd.Series(0, index=work.index)).fillna(0).astype(float) |
| waste = work.get("waste_pct", pd.Series(0, index=work.index)).fillna(0).astype(float) |
| expiry = work.get("days_until_expiry", pd.Series(30, index=work.index)).fillna(30).astype(float) |
| discount = work.get("discount_pct", pd.Series(0, index=work.index)).fillna(0).astype(float) |
| leftover = work.get("leftover_units", pd.Series(0, index=work.index)).fillna(0).astype(float) |
|
|
| forecast *= np.where(sell >= 0.80, 1.04, 1.00) |
| forecast *= np.where(discount >= 0.20, 1.02, 1.00) |
| forecast *= np.where(expiry <= 2, 1.03, 1.00) |
| forecast *= np.where(waste >= 0.30, 0.95, 1.00) |
| forecast *= np.where(leftover > anchor * 1.15, 0.92, 1.00) |
|
|
| lower_cap = (anchor * 0.90).clip(lower=0) |
| upper_cap = np.maximum(lower_cap + 1, anchor * 1.10) |
| forecast = forecast.clip(lower=lower_cap, upper=upper_cap) |
|
|
| gap = forecast - leftover |
| stock_cover = leftover / forecast.clip(lower=1) |
| overstock_flag = (leftover >= forecast * 1.08) | (stock_cover >= 1.10) | (waste >= 0.25) |
| markdown_flag = (expiry <= 2) & (leftover > forecast) |
| buy_flag = (gap > np.maximum(4, forecast * 0.06)) & (sell >= 0.70) & (stock_cover < 0.95) & (~overstock_flag) |
|
|
| action = np.where(markdown_flag, "markdown now", np.where(overstock_flag, "reduce / transfer", np.where(buy_flag, "buy now", "monitor"))) |
|
|
| work["forecast_units"] = np.round(forecast, 1) |
| work["forecast_gap"] = np.round(gap, 1) |
| work["forecast_action"] = action |
| work["forecast_model_r2"] = model_r2 |
| return work |
|
|
|
|
| def _sorted_expiry_summary(work: pd.DataFrame) -> pd.DataFrame: |
| order = ["<=1d", "2-3d", "4-7d", "8-30d", ">30d"] |
| expiry = work.groupby("expiry_bucket")[["units_wasted", "leftover_units", "units_sold"]].sum().reset_index() |
| expiry["expiry_bucket"] = pd.Categorical(expiry["expiry_bucket"], categories=order, ordered=True) |
| return expiry.sort_values("expiry_bucket") |
|
|
|
|
| def inventory_page(df: pd.DataFrame, full_df: pd.DataFrame | None = None, scope_info: dict | None = None): |
| st.subheader("FRESHIE Β· Inventory & Replenishment") |
| st.markdown('<div class="note-box"><b>Audience:</b> supply chain. Use this page for reorder, transfer, expiry control, and stock-balancing decisions.</div>', unsafe_allow_html=True) |
|
|
| work = df.copy() |
| model_bundle = train_hybrid_forecast_model(full_df if full_df is not None else df) |
| work = add_hybrid_forecast(work, model_bundle=model_bundle) |
| work["recommended_order_qty"] = (work["forecast_units"] - work["leftover_units"]).clip(lower=0).round() |
| work.loc[work["days_until_expiry"] <= 7, "recommended_order_qty"] *= 0.75 |
| work["recommended_order_qty"] = work["recommended_order_qty"].round() |
| selected_scope_category = scope_info.get("category", "All") if scope_info else "All" |
| single_category = selected_scope_category != "All" and work["category"].nunique() == 1 |
|
|
| left, right = st.columns([1.25, 1]) |
| with left: |
| if single_category: |
| prod = work.groupby(["product_name", "category"]).agg( |
| waste_pct=("waste_pct", "mean"), |
| lost_sales_units=("lost_sales_units", "mean"), |
| avg_profit=("profit", "mean"), |
| forecast_units=("forecast_units", "mean"), |
| leftover_units=("leftover_units", "mean"), |
| ).reset_index() |
| fig = px.scatter( |
| prod.sort_values("lost_sales_units", ascending=False), |
| x="waste_pct", |
| y="lost_sales_units", |
| size=(prod["avg_profit"].abs() + 1), |
| color="product_name", |
| hover_data=["forecast_units", "leftover_units", "avg_profit"], |
| title=f"Waste vs lost sales by product Β· {work['category'].iloc[0]}", |
| ) |
| else: |
| cat = work.groupby("category").agg( |
| waste_pct=("waste_pct", "mean"), |
| lost_sales_units=("lost_sales_units", "mean"), |
| avg_profit=("profit", "mean"), |
| forecast_units=("forecast_units", "mean"), |
| leftover_units=("leftover_units", "mean"), |
| ).reset_index() |
| fig = px.scatter( |
| cat, |
| x="waste_pct", |
| y="lost_sales_units", |
| size=(cat["avg_profit"].abs() + 1), |
| color="category", |
| hover_data=["forecast_units", "leftover_units", "avg_profit"], |
| title="Waste vs lost sales by category", |
| ) |
| st.plotly_chart(fig, use_container_width=True) |
|
|
| forecast_view = work.groupby("product_name").agg( |
| forecast_units=("forecast_units", "mean"), |
| leftover_units=("leftover_units", "mean"), |
| recommended_order_qty=("recommended_order_qty", "mean"), |
| forecast_action=("forecast_action", lambda s: s.value_counts().index[0]), |
| forecast_gap=("forecast_gap", "mean"), |
| ).reset_index().sort_values(["forecast_action", "forecast_units"], ascending=[True, False]).head(20) |
| fig2 = px.bar( |
| forecast_view, |
| x="product_name", |
| y=["forecast_units", "leftover_units", "recommended_order_qty"], |
| barmode="group", |
| color="forecast_action", |
| title="ML + heuristic forecast vs current stock", |
| hover_data=["forecast_gap"], |
| ) |
| st.plotly_chart(fig2, use_container_width=True) |
|
|
| with right: |
| st.markdown("### Action shortlist") |
| shortlist = work.sort_values( |
| ["forecast_action", "forecast_gap", "waste_pct", "lost_sales_units"], |
| ascending=[True, False, False, False], |
| )[[ |
| "store_id", "category", "product_name", "daily_demand", "forecast_units", "leftover_units", |
| "days_until_expiry", "waste_pct", "recommended_order_qty", "forecast_action" |
| ]].head(18) |
| st.dataframe(shortlist, use_container_width=True, hide_index=True) |
|
|
| st.markdown("### Expiry pressure") |
| expiry = _sorted_expiry_summary(work) |
| fig3 = px.bar( |
| expiry, |
| x="expiry_bucket", |
| y=["units_wasted", "leftover_units"], |
| barmode="group", |
| title="Wasted and leftover units by expiry bucket", |
| ) |
| st.plotly_chart(fig3, use_container_width=True) |
|
|
| st.markdown("### What-if Simulator") |
| st.caption("Simulator logic: ML baseline forecast + business-rule heuristic. The simulator follows the current left-side filters, and the SKU selector is built from the filtered scope only.") |
|
|
| sim_scope = work.copy() |
| sku_options = sorted(sim_scope["product_name"].dropna().unique().tolist()) |
| sim_head_left, sim_head_right = st.columns([1.2, 1.4]) |
| with sim_head_left: |
| scope_label = "Current filtered scope" |
| if single_category and not sim_scope.empty: |
| scope_label = str(sim_scope["category"].iloc[0]) |
| st.markdown(f"**Simulation scope:** {scope_label}") |
| with sim_head_right: |
| sim_sku = st.selectbox("SKU", sku_options, key="inv_shared_sku") if sku_options else None |
| sku_scope = sim_scope[sim_scope["product_name"] == sim_sku].copy() if sim_sku else sim_scope.head(0) |
|
|
| col1, col2, col3 = st.columns(3) |
| order_cut = col1.slider("Reduce order quantity by %", 0, 40, 10, key="inv_sim_cut") |
| markdown_shift = col2.slider("Advance markdown trigger by days", 0, 5, 2, key="inv_sim_mark") |
| current_disc = float(sku_scope["discount_pct"].mean()) if not sku_scope.empty else 0.0 |
| test_disc = col3.slider("Test SKU discount %", 0, 60, int(round(current_disc * 100)), key="inv_sim_disc") / 100 |
|
|
| current_waste = sim_scope["waste_pct"].mean() if not sim_scope.empty else 0.0 |
| current_profit = sim_scope["profit"].mean() if not sim_scope.empty else 0.0 |
| current_stockout = sim_scope["stockout_flag"].mean() if not sim_scope.empty else 0.0 |
| waste_reduction = 0.35 * (order_cut / 100) + 0.015 * markdown_shift |
| stockout_rise = 0.12 * (order_cut / 100) |
| sim_waste = max(current_waste * (1 - waste_reduction), 0) |
| sim_profit = current_profit * (1 + 0.08 * (order_cut / 100) + 0.03 * markdown_shift) |
| sim_stockout = min(current_stockout * (1 + stockout_rise), 1) |
|
|
| s1, s2, s3 = st.columns(3) |
| s1.metric("Simulated waste", f"{sim_waste:.1%}", delta=f"-{(current_waste - sim_waste):.1%}") |
| s2.metric("Simulated avg profit", f"EUR {sim_profit:.2f}", delta=f"EUR {(sim_profit - current_profit):.2f}") |
| s3.metric("Simulated stockout", f"{sim_stockout:.1%}", delta=f"+{(sim_stockout - current_stockout):.1%}") |
|
|
| st.markdown("#### SKU markdown simulator") |
| sku_sim = _simulate_discount_profit(sku_scope, test_disc) if not sku_scope.empty else {} |
| if sku_sim: |
| c1, c2, c3, c4 = st.columns(4) |
| c1.metric("Current discount", f"{sku_sim['current_discount']:.1%}") |
| c2.metric("Simulated SKU profit", f"EUR {sku_sim['sim_profit']:.2f}", delta=f"EUR {sku_sim['profit_delta']:.2f}") |
| c3.metric("Simulated units", f"{sku_sim['sim_units']:.1f}", delta=f"{sku_sim['units_delta']:.1f}") |
| c4.metric("Simulated waste", f"{sku_sim['sim_waste_pct']:.1%}") |
| else: |
| st.info("No SKU records match the current simulation scope.") |
|
|
| st.markdown("### Transfer suggestions") |
| balancing_df = build_supply_balancing_table(full_df if full_df is not None else df) |
| scope_info = scope_info or {} |
| selected_stores = scope_info.get("stores", []) or [] |
| selected_regions = scope_info.get("regions", []) or [] |
| selected_category = scope_info.get("category", "All") or "All" |
|
|
| selected_store = selected_stores[0] if len(selected_stores) == 1 else "All" |
| if selected_store != "All": |
| auto_region = balancing_df.loc[balancing_df["store_id"] == selected_store, "region"].mode() |
| selected_region = auto_region.iloc[0] if not auto_region.empty else "All" |
| st.caption(f"Transfer scope: store **{selected_store}** in region **{selected_region}**" + (f" Β· category **{selected_category}**" if selected_category != "All" else "")) |
| scoped_df = filter_scope( |
| balancing_df, |
| selected_region=selected_region, |
| selected_store=selected_store, |
| selected_category=selected_category, |
| ) |
| else: |
| if len(selected_regions) == 1: |
| selected_region = selected_regions[0] |
| else: |
| selected_region = "All" |
| scoped_df = filter_scope( |
| balancing_df, |
| selected_region=selected_region, |
| selected_store="All", |
| selected_category=selected_category, |
| ) |
| if selected_region != "All": |
| st.caption(f"Transfer scope: region **{selected_region}**" + (f" Β· category **{selected_category}**" if selected_category != "All" else "")) |
| else: |
| st.caption("Transfer scope: current full network filter" + (f" Β· category **{selected_category}**" if selected_category != "All" else "")) |
|
|
| receiver = scoped_df[scoped_df["unmet_demand"] > 0].copy() |
| donors = balancing_df.copy() |
| if selected_category != "All": |
| donors = donors[donors["category"] == selected_category].copy() |
|
|
| transfer_rows = [] |
| for _, r in receiver.iterrows(): |
| pool = donors[(donors["category"] == r["category"]) & (donors["store_id"] != r["store_id"]) & (donors["surplus_qty"] > 0)].copy() |
| if selected_region != "All": |
| pool["priority_rank"] = (pool["region"] != r["region"]).astype(int) |
| else: |
| pool["priority_rank"] = 0 |
| pool = pool.sort_values(["priority_rank", "avg_days_until_expiry", "surplus_qty"], ascending=[True, False, False]) |
|
|
| if pool.empty: |
| best_route = "No feasible donor" |
| same_region_options = "No same-region donor" |
| cross_region_options = "No cross-region donor" |
| transfer_qty = 0 |
| else: |
| same_region = pool[pool["region"] == r["region"]].head(3) |
| cross_region = pool[pool["region"] != r["region"]].head(3) |
| best = pool.iloc[0] |
| transfer_qty = int(min(r["unmet_demand"], max(best["surplus_qty"], 0))) |
|
|
| def label(d): |
| tier = "same-region" if d["region"] == r["region"] else "cross-region" |
| return f"{d['store_id']} ({tier}, expiry {d['avg_days_until_expiry']:.1f}d, surplus {int(d['surplus_qty'])})" |
|
|
| best_route = label(best) |
| same_region_options = "; ".join(label(d) for _, d in same_region.iterrows()) if not same_region.empty else "No same-region donor" |
| cross_region_options = "; ".join(label(d) for _, d in cross_region.iterrows()) if not cross_region.empty else "No cross-region donor" |
|
|
| transfer_rows.append({ |
| "store_id": r["store_id"], |
| "region": r["region"], |
| "category": r["category"], |
| "remaining_inventory": int(r["remaining_inventory"]), |
| "demand": int(r["demand"]), |
| "unmet_demand": int(r["unmet_demand"]), |
| "recommended_transfer_qty": transfer_qty, |
| "best_route": best_route, |
| "same_region_options": same_region_options, |
| "cross_region_options": cross_region_options, |
| }) |
|
|
| transfer_df = pd.DataFrame(transfer_rows) |
| st.caption("Transfer logic: the receiving side follows the current sidebar scope. If a store is selected, its region is inferred automatically. Donors are searched from the full balancing table, with same-region options prioritized first, then cross-region options ranked by shelf life and surplus quantity.") |
| if transfer_df.empty: |
| st.success("No scoped store or region currently shows unmet demand that needs transfer support.") |
| else: |
| st.dataframe(transfer_df.sort_values(["unmet_demand", "recommended_transfer_qty"], ascending=[False, False]), use_container_width=True, hide_index=True) |
|
|
|
|
| def promotion_page(df: pd.DataFrame): |
| st.subheader("FRESHIE Β· Promotion Designer") |
| st.markdown('<div class="note-box"><b>Audience:</b> marketing. Use this page to test markdown depth, bundle logic, and campaign copy.</div>', unsafe_allow_html=True) |
| st.caption("Promotion designer logic: business-rule simulator. The page follows the current left-side filters, and the SKU selector is built from the filtered scope only.") |
|
|
| promo_scope = df.copy() |
| sku_options = sorted(promo_scope["product_name"].dropna().unique().tolist()) |
| if not sku_options: |
| st.info("No SKU remains in the current left-side filter scope.") |
| return |
|
|
| head_left, head_right = st.columns([1.1, 1.4]) |
| with head_left: |
| scope_label = "Current filtered scope" |
| if promo_scope["category"].nunique() == 1: |
| scope_label = str(promo_scope["category"].iloc[0]) |
| st.markdown(f"**Promotion scope:** {scope_label}") |
| with head_right: |
| promo_sku = st.selectbox("SKU", sku_options, key="promo_shared_sku") |
|
|
| sku_df = promo_scope[promo_scope["product_name"] == promo_sku].copy() |
| if sku_df.empty: |
| st.info("No records match the selected SKU in the current scope.") |
| return |
|
|
| left, right = st.columns([1, 1.25]) |
| with left: |
| expiry_target = st.selectbox("Target expiry bucket", ["<=1d", "2-3d", "4-7d", "8-30d", ">30d"], key="promo_exp") |
| discount = st.slider("Discount %", 0, 50, int(round(float(sku_df["discount_pct"].mean() * 100))), key="promo_disc") |
| bundle = st.checkbox("Bundle with complementary items", value=True, key="promo_bundle") |
| weekend_only = st.checkbox("Weekend campaign only", value=False, key="promo_weekend") |
|
|
| sub = sku_df[sku_df["expiry_bucket"].astype(str) == str(expiry_target)].copy() |
| if weekend_only: |
| sub = sub[sub["is_weekend"] == 1] |
| if sub.empty: |
| sub = sku_df.copy() |
| if weekend_only: |
| sub = sub[sub["is_weekend"] == 1] |
| if sub.empty: |
| sub = sku_df.copy() |
|
|
| demand_lift = 0.08 + discount / 200 |
| if bundle: |
| demand_lift += 0.06 |
|
|
| est_sales_uplift = sub["units_sold"].mean() * demand_lift if len(sub) else 0 |
| est_waste_drop = sub["waste_pct"].mean() * min(0.35, demand_lift) if len(sub) else 0 |
| est_profit = sub["profit"].mean() * (1 + demand_lift - discount / 150) if len(sub) else 0 |
|
|
| sims = [] |
| sim_base = sub.copy() |
| for disc in range(0, 55, 5): |
| lift = 0.08 + disc / 200 + (0.06 if bundle else 0) |
| sim_profit = (sim_base["profit"].mean() * (1 + lift - disc / 150)) if len(sim_base) else 0 |
| sim_waste = (sim_base["waste_pct"].mean() * (1 - min(0.35, lift))) if len(sim_base) else 0 |
| score = sim_profit - 120 * sim_waste |
| sims.append({"discount": disc, "sim_profit": sim_profit, "sim_waste": sim_waste, "score": score}) |
| sim_df = pd.DataFrame(sims) |
| best = sim_df.sort_values("score", ascending=False).iloc[0] |
|
|
| st.metric("Estimated sales uplift", f"{est_sales_uplift:.2f} units") |
| st.metric("Estimated waste reduction", f"{est_waste_drop:.1%}") |
| st.metric("Estimated avg profit", f"EUR {est_profit:.2f}") |
| st.metric("Suggested discount", f"{int(best['discount'])}%") |
|
|
| st.caption("Suggested discount logic: test 0% to 50% in 5-point steps and choose the discount that maximizes a simple score = simulated profit - waste penalty.") |
|
|
| st.markdown("### Campaign brief") |
| sku_name = str(promo_sku) |
| campaign_type = "weekend bundle campaign" if bundle and weekend_only else "bundle campaign" if bundle else "markdown campaign" |
| st.success(f"Run a {int(best['discount'])}% {campaign_type} for {sku_name} in {expiry_target}.") |
|
|
| with right: |
| order = ["<=1d", "2-3d", "4-7d", "8-30d", ">30d"] |
| sku_ts = sku_df.groupby(["month", "expiry_bucket"])[["discount_pct", "waste_pct", "sell_through_pct"]].mean().reset_index() |
| sku_ts["expiry_bucket"] = pd.Categorical(sku_ts["expiry_bucket"], categories=order, ordered=True) |
| sku_ts = sku_ts.sort_values(["expiry_bucket", "month"]) |
| present = sku_ts["expiry_bucket"].dropna().astype(str).unique().tolist() |
|
|
| if not sku_ts.empty: |
| fig_disc = px.line( |
| sku_ts, |
| x="month", |
| y="discount_pct", |
| color="expiry_bucket", |
| markers=True, |
| title=f"Discount trend for {promo_sku} across expiry buckets", |
| ) |
| st.plotly_chart(fig_disc, use_container_width=True) |
|
|
| fig_waste = px.line( |
| sku_ts, |
| x="month", |
| y="waste_pct", |
| color="expiry_bucket", |
| markers=True, |
| title=f"Waste trend for {promo_sku} across expiry buckets", |
| ) |
| st.plotly_chart(fig_waste, use_container_width=True) |
|
|
| fig_sell = px.line( |
| sku_ts, |
| x="month", |
| y="sell_through_pct", |
| color="expiry_bucket", |
| markers=True, |
| title=f"Sell-through trend for {promo_sku} across expiry buckets", |
| ) |
| st.plotly_chart(fig_sell, use_container_width=True) |
|
|
| missing = [b for b in order if b not in present] |
| if missing: |
| st.caption("Buckets without records for this SKU are omitted: " + ", ".join(missing)) |
| else: |
| st.info("No records match the current promotion SKU.") |
|
|
| st.markdown("### Recommended promotion copy") |
| st.info( |
| f"Fresh pick alert: enjoy {int(best['discount'])}% off selected {str(promo_sku).lower()}" |
| + (" this weekend" if weekend_only else "") |
| + (" with smart bundle savings" if bundle else " while they are still at peak freshness") |
| + f". Prioritize the {expiry_target} bucket and highlight freshness, value, and limited-time availability." |
| ) |
|
|
|
|
|
|
| def consumer_deals(df: pd.DataFrame): |
| st.subheader("FRESHIE Β· Deal Finder") |
| stores = sorted(df["store_id"].dropna().unique()) |
| if not stores: |
| st.warning("No stores available in the current filter.") |
| return |
| chosen_store = st.selectbox("Choose store first", stores) |
| store_df = df[df["store_id"] == chosen_store].copy() |
|
|
| c1, c2, c3 = st.columns(3) |
| budget_range = c1.slider("Budget range (EUR)", 1, 60, (1, 20)) |
| preferred_category = c2.selectbox("Preferred category", ["All"] + sorted(store_df["category"].unique())) |
| expiry_range = c3.slider("Days until expiry", 1, 14, (1, 5)) |
|
|
| deals = store_df[ |
| (store_df["days_until_expiry"] >= expiry_range[0]) & |
| (store_df["days_until_expiry"] <= expiry_range[1]) & |
| (store_df["selling_price"] >= budget_range[0]) & |
| (store_df["selling_price"] <= budget_range[1]) |
| ].copy() |
| if preferred_category != "All": |
| deals = deals[deals["category"] == preferred_category] |
| deals["savings"] = (deals["base_price"] - deals["selling_price"]).clip(lower=0) |
| deals["deal_score"] = deals["discount_pct"] * 0.6 + deals["sell_through_pct"] * 0.2 + (1 - deals["waste_pct"]) * 0.2 |
| deals = deals.sort_values(["deal_score", "savings"], ascending=False) |
|
|
| st.markdown('<div class="note-box"><b>Store marketing view:</b> all deal recommendations below are scoped to the selected store so the shopper sees store-specific promotions and products.</div>', unsafe_allow_html=True) |
|
|
| show = deals[["product_name", "category", "days_until_expiry", "base_price", "selling_price", "discount_pct", "savings"]].head(20).copy() |
| icon_map = { |
| "Bakery": "π₯", "Beverages": "π§", "Dairy": "π₯", "Deli": "π§Ί", |
| "Meat": "π₯©", "Pharmaceuticals": "π", "Produce": "π₯¬", "Ready_to_Eat": "π±", "Seafood": "π" |
| } |
| show["item"] = show.apply(lambda r: f"{icon_map.get(str(r['category']), 'π¦')} {r['product_name']}", axis=1) |
| st.dataframe(show[["item", "category", "days_until_expiry", "base_price", "selling_price", "discount_pct", "savings"]], use_container_width=True, hide_index=True) |
|
|
| st.markdown("### Best current deals") |
| top_cards = deals.head(6) |
| cols = st.columns(3) |
| for i, (_, row) in enumerate(top_cards.iterrows()): |
| icon = icon_map.get(str(row["category"]), "π¦") |
| with cols[i % 3]: |
| st.markdown(f"**{icon} {row['product_name']}**") |
| st.write(f"{row['category']} Β· expires in {int(row['days_until_expiry'])} day(s)") |
| st.write(f"Now EUR {row['selling_price']:.2f} | Save EUR {row['savings']:.2f}") |
| st.caption(f"Discount: {row['discount_pct']:.0%} Β· Store: {chosen_store}") |
|
|
|
|
| def consumer_bundles(df: pd.DataFrame): |
| st.subheader("FRESHIE Β· Bundle Builder") |
| stores = sorted(df["store_id"].dropna().unique()) |
| if not stores: |
| st.warning("No stores available in the current filter.") |
| return |
|
|
| chosen_store = st.selectbox("Choose store", stores, key="bundle_store") |
| store_df = df[df["store_id"] == chosen_store].copy() |
|
|
| c1, c2, c3 = st.columns(3) |
| budget_range = c1.slider("Bundle budget range (EUR)", 8, 80, (8, 25)) |
| theme = c2.selectbox("Bundle theme", ["Quick dinner", "Healthy protein", "Family breakfast", "Budget saver"]) |
| expiry_range = c3.slider("Use items expiring within days", 1, 10, (1, 5), key="bundle_exp") |
|
|
| if "bundle_seed" not in st.session_state: |
| st.session_state["bundle_seed"] = 0 |
| st.session_state["bundle_seed"] = random.randint(0, 10000) |
|
|
| work = store_df[ |
| (store_df["days_until_expiry"] >= expiry_range[0]) & |
| (store_df["days_until_expiry"] <= expiry_range[1]) & |
| (store_df["selling_price"] <= budget_range[1]) |
| ].copy() |
|
|
| theme_map = { |
| "Quick dinner": ["Ready_to_Eat", "Produce", "Bakery", "Dairy"], |
| "Healthy protein": ["Meat", "Seafood", "Dairy", "Produce"], |
| "Family breakfast": ["Bakery", "Dairy", "Beverages", "Produce"], |
| "Budget saver": list(work["category"].dropna().unique()), |
| } |
| work = work[work["category"].isin(theme_map.get(theme, []))].copy() |
| work["score"] = work["discount_pct"] * 0.5 + (1 - work["waste_pct"]) * 0.2 + work["sell_through_pct"] * 0.3 |
|
|
| if len(work) == 0: |
| st.warning("No bundle fits the current conditions.") |
| return |
|
|
| seed = int(st.session_state["bundle_seed"]) |
| candidate_frames = [] |
| for idx, (cat_name, grp) in enumerate(work.groupby("category")): |
| grp = grp.sort_values(["score", "selling_price"], ascending=[False, True]).head(10).copy() |
| if len(grp) > 1: |
| shift = (seed + idx) % len(grp) |
| grp = pd.concat([grp.iloc[shift:], grp.iloc[:shift]], ignore_index=True) |
| candidate_frames.append(grp) |
| work = pd.concat(candidate_frames, ignore_index=True) |
|
|
| if theme == "Budget saver": |
| work = work.sort_values(["selling_price", "score"], ascending=[True, False]) |
| else: |
| work = work.sort_values(["score", "selling_price"], ascending=[False, True]) |
|
|
| picked, subtotal, used_categories = [], 0.0, set() |
| for _, row in work.iterrows(): |
| if subtotal + row["selling_price"] <= budget_range[1]: |
| if theme != "Budget saver" and row["category"] in used_categories: |
| continue |
| picked.append(row) |
| subtotal += row["selling_price"] |
| used_categories.add(row["category"]) |
| if len(picked) >= 5: |
| break |
|
|
| if not picked: |
| st.warning("No bundle fits the current conditions.") |
| return |
|
|
| bundle = pd.DataFrame(picked) |
| base_total = float(bundle["base_price"].sum()) |
| item_total = float(bundle["selling_price"].sum()) |
|
|
| if item_total >= 45: |
| bundle_discount = 0.15 |
| elif item_total >= 30: |
| bundle_discount = 0.12 |
| elif item_total >= 20: |
| bundle_discount = 0.10 |
| elif item_total >= 12: |
| bundle_discount = 0.08 |
| else: |
| bundle_discount = 0.05 |
|
|
| bundle_saving = item_total * bundle_discount |
| final_total = max(item_total - bundle_saving, 0) |
| saved = base_total - final_total |
|
|
| k1, k2, k3, k4 = st.columns(4) |
| k1.metric("Bundle subtotal", f"EUR {item_total:.2f}") |
| k2.metric("Bundle discount", f"{bundle_discount:.0%}") |
| k3.metric("Bundle total", f"EUR {final_total:.2f}") |
| k4.metric("You save", f"EUR {saved:.2f}") |
|
|
| st.dataframe( |
| bundle[["product_name", "category", "selling_price", "base_price", "discount_pct", "days_until_expiry"]], |
| use_container_width=True, |
| hide_index=True, |
| ) |
| st.caption( |
| "Bundle pricing logic: items keep their current markdowns, then the system applies an extra basket discount. " |
| "Higher basket subtotals receive a deeper extra discount. Refresh rotates the candidate pool to surface a different combination." |
| ) |
|
|
|
|
|
|
| def consumer_personal(df: pd.DataFrame): |
| st.subheader("FRESHIE Β· Personalized Promotions") |
| stores = sorted(df["store_id"].dropna().unique()) |
| if not stores: |
| st.warning("No stores available in the current filter.") |
| return |
| chosen_store = st.selectbox("Choose store", stores, key="personal_store") |
| store_df = df[df["store_id"] == chosen_store].copy() |
|
|
| favorite = st.selectbox("Favorite category", sorted(store_df["category"].unique()), key="cp_fav") |
| price_range = st.slider("Price range (EUR)", 1, 30, (1, 10), key="cp_cap") |
| expiry_range = st.slider("Days until expiry", 1, 14, (1, 7), key="cp_days") |
| recs = store_df[ |
| (store_df["category"] == favorite) & |
| (store_df["selling_price"] >= price_range[0]) & |
| (store_df["selling_price"] <= price_range[1]) & |
| (store_df["days_until_expiry"] >= expiry_range[0]) & |
| (store_df["days_until_expiry"] <= expiry_range[1]) |
| ].copy() |
| recs["score"] = recs["discount_pct"] * 0.55 + recs["sell_through_pct"] * 0.20 + (1 - recs["waste_pct"]) * 0.25 |
| recs = recs.sort_values("score", ascending=False).head(12) |
|
|
| cols = st.columns(3) |
| for i, (_, row) in enumerate(recs.iterrows()): |
| with cols[i % 3]: |
| st.markdown(f"**{row['product_name']}**") |
| st.write(f"{row['category']} Β· {chosen_store}") |
| st.write(f"Now EUR {row['selling_price']:.2f}") |
| st.write(f"Expires in {int(row['days_until_expiry'])} day(s)") |
| st.caption(f"Discount: {row['discount_pct']:.0%}") |
|
|
|
|
| def consumer_wait_or_buy(df: pd.DataFrame): |
| st.subheader("FRESHIE Β· Wait or buy now?") |
| st.markdown( |
| """ |
| This version uses a live-inventory style view built from the most recent available on-shelf records in the current filter scope. |
| Only items with remaining stock and non-negative days until expiry are shown. |
| """ |
| ) |
|
|
| work = df.copy() |
| if work.empty: |
| st.info("No records match the current filters.") |
| return |
|
|
| work = work[(work["leftover_units"] > 0) & (work["days_until_expiry"] >= 0)].copy() |
| if work.empty: |
| st.info("No on-shelf inventory remains in the current filter scope.") |
| return |
|
|
| latest_keys = ["store_id", "category", "product_name"] |
| work = ( |
| work.sort_values("transaction_date") |
| .groupby(latest_keys, as_index=False) |
| .tail(1) |
| .copy() |
| ) |
|
|
| if work.empty: |
| st.info("No recent on-shelf inventory remains after selecting the latest records.") |
| return |
|
|
| work["discount"] = work["discount_pct"].fillna(0).clip(lower=0) |
| work["sell_through"] = work["sell_through_pct"].fillna(0).clip(lower=0) |
|
|
| def classify_item(row): |
| if row["sell_through"] > 0.7: |
| return "buy now", "selling fast and still available on shelf" |
| if row["sell_through"] < 0.4 and row["discount"] < 0.2: |
| return "wait it", "inventory looks comfortable and discount is still modest" |
| if row["sell_through"] < 0.4 and row["discount"] >= 0.2: |
| return "suggested", "inventory is comfortable and discount is already attractive" |
| return "no special signal", "balanced current shelf signal" |
|
|
| labels = work.apply(classify_item, axis=1) |
| work["suggestion"] = [x[0] for x in labels] |
| work["reason"] = [x[1] for x in labels] |
|
|
| c1, c2, c3, c4 = st.columns(4) |
| c1.metric("On-shelf items shown", f"{len(work):,}") |
| c2.metric("Buy now", int((work["suggestion"] == "buy now").sum())) |
| c3.metric("Wait it", int((work["suggestion"] == "wait it").sum())) |
| c4.metric("Suggested", int((work["suggestion"] == "suggested").sum())) |
|
|
| latest_date = work["transaction_date"].max() |
| st.caption(f"Current shelf view approximated from the latest available records up to {latest_date.date()} in the selected filter scope.") |
|
|
| display = work[[ |
| "product_name", |
| "category", |
| "store_id", |
| "leftover_units", |
| "days_until_expiry", |
| "sell_through", |
| "discount", |
| "stockout_flag", |
| "suggestion", |
| "reason", |
| ]].copy() |
|
|
| display = display.sort_values( |
| ["suggestion", "days_until_expiry", "discount", "leftover_units", "product_name"], |
| ascending=[True, True, False, False, True], |
| ) |
|
|
| st.dataframe(display, use_container_width=True, hide_index=True) |
|
|
| summary = ( |
| display["suggestion"] |
| .value_counts() |
| .rename_axis("suggestion") |
| .reset_index(name="items") |
| .sort_values("items", ascending=False) |
| ) |
| fig = px.bar( |
| summary, |
| x="suggestion", |
| y="items", |
| color="suggestion", |
| title="Wait or buy now suggestion mix (current shelf view)", |
| ) |
| st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
| def manager_manual(): |
| st.subheader("FRESHIE Β· User Manual (Manager)") |
| st.markdown(""" |
| **Executive Overview** |
| - Track revenue, profit, units sold, and units wasted by month. |
| - Use the diagnosis panel to understand abnormal revenue-profit gaps. |
| |
| **Category Intelligence** |
| - Compare categories on demand, stock, waste, stockout, and profit. |
| - Read the category recommendations and forecast for the current selection. |
| |
| **Inventory & Replenishment** |
| - Review reorder guidance by category. |
| - Use the what-if simulator to see the trade-off between waste, profit, and stockout. |
| |
| **Promotion Designer** |
| - Test markdown depth, expiry targeting, and bundle logic. |
| - Use the campaign brief and promotion copy as a starting point for activation. |
| """) |
|
|
|
|
| def consumer_manual(): |
| st.subheader("FRESHIE Β· User Manual (Consumer)") |
| st.markdown(""" |
| **Deal Finder** |
| - Browse the strongest discounted products under your preferred budget and expiry window. |
| |
| **Bundle Builder** |
| - Build a themed basket while staying under budget. |
| |
| **Personalized Promotions** |
| - Focus on your favorite category and a comfortable price cap. |
| |
| **Wait or Buy Now?** |
| - Get smart purchase timing suggestions based on your selected region and product category. |
| - The system evaluates each item's sell-through rate and current discount across stores to indicate whether it is better to buy now or wait for a potentially better deal. |
| """) |
|
|
|
|
| def main(): |
| inject_css() |
| st.markdown( |
| """ |
| <div class="hero-wrap"> |
| <div class="main-title"> |
| <div class="title-left"> |
| <img class="logo-img" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALQAAAC0CAYAAAA9zQYyAADkJElEQVR4nOz9d5glV3XvjX/WrqoTO6fJUTOa0YxyQBFFEEEiCJFNMumCI/b1658x19k4+14bg22CARswwmQFhAQo51HWjDQ5T89M90znPqmq9nr/2FV1qntGGK7DvX7e39HTmu5zTlXt8N1rfVfYa4sNa8p/5iv/NPlPffL///X/gZf/f7oB/7Vf/5YV+Z8rR/6/8vrPB7Tg5vK/lHT+j2js/Hv+Xwjw+U36LzBnvuqPH8h/1z5o+x/5TwH1v/aAk/X9ZNe82BjJ/14fTjrmP2Ye/nfHKr2l5P7+Se7zYk150etTKfV//vVjJfS/HW/zOir/lvv+R0nJ+bP+07wU1Z/2On2Rp8kJ3znpRz/Na/51P+l9fuLnyUl+/2mB/e+0GJJb+CL/0WLy3+P+/5Ft/LeAWpGTXvuTWL4/biL/C+j2Oa+fVgXMv+7fqQn6X8Io/M+Y3H+rPv9pyObJpNr8e/60r//TKv//DroBGaD/d9XFf9Trv4KE+nHA/Pe453/ktf8e8/zjnveT2iY/yWc/2f0VEAFf85TjBD74f8Id8b9paJ30pT+GUEjyjZMrzJP1PBtGVTd66bs/ET4ka8/JW/rjyc+LkiKRFzEyf0wzkDkG+r/+/fxz262c2+a57ivh5M06mej818jXiZ+dfHQsIFFU1/8MwSwncEzJhkXmMdEXVeAvst70JOiT5PMXZbM/hYA5AUzJL+lzhXlteJHx/KnMlX9tTn4atvJ/g4fw38NmnAcOPclvEob1ucst//t8sfGTPPSnbeTJ3vwxyDuZ6XXiUvm3sdH86wTJqSdRZP/K/U6+qH48yl5sOk525Yu/+38Lhfz30fX5Ocj/nekIEXzV9mDlVYRk/2POaM5fLHnV8uOHNHeFzr9L/iUOND+2a3MhLCIoeaTpCYCYe9WJdGP+3V/0+fPW3UnB/SIU8sUXg8y95KQPn/umzP3fj2nsyW71Im7Bn0Zw/WsrP/edk93+3wJwPeGG7sFWQZotJ6Hnov1kjOnERv046Xny779YC/MXv3g3fwKt/uMbMJfmzXsz+Ss3YZr8nRocL0qF5rUvJVE6j2idrBk/mUZ5kSeLJu1Ln6Pz7pvnQ/9xrxdrv/wYwfKTAvrk699JhpQr50dZGq26zgez5r40v6EnDNic99rmQfqB/iuz9dNRhJO36eR3nPfuixHhH/e0HKjbz833/MTb5Pt+4psnb+38Js+ZcJ3zTbK4wRzDdO51J/RjXpPaUMg948cOh8y7/1zj9n8HqD/Nq32/E3uXaklJBtK3J2lYpgKTQZvf4HTd5a1J1ZO9L4iexOZMRV5+eHKScO7yyKnqedxGEDSdWG1fkcnG7Jp2H1KKBZqxn9wjTxyp+W/lwOzA3pbE8ydyzsKePwYnG5MTnnUSXZkNQU5l/BQIOuGrOu/fF2tcCppcM3JT6MZc5rZ2/mPy773YgjhZ8/J3lXmfpgIhFaUO0FlD256H7JKc/G4Dej6c0zUruU/nNSfnBlDUASEZkTZn1vZgzeusag5KOq8NSRtV29dInoirnlxTaNKr5LtpGDudqJP5Xk6mUU42UXPo7Y/VVCdH0gmTmxPXJzwvB/IfJ6HdV3/C56XvJ27B+Rojr7lS+a25z0+K2Oye8z5OLpwnKtqfZwJsLk7mRrkTzTnTrM9hWKpzeVgKunajEhjJSQwZyd9nbifSAcgmVnITkErRdMTSx+V8rPnr5qOjLWTkRLWb861ZncdqcxK6vVDnjl46+Nlk5f9mzq3ImozM4eJzmj6n0TlEaP4uOZUu7UaeqAHmLnvJ/TZfks1p5xztM++z+Z2aB9Afrwza1kMbcPNlc/t77d9Pvg5O9qx0gbW15dyZ8C1u1eYBmNdC8ylDCvc5UjW3Ok8Gpjz01J6ke+okqrWaA1Ze4iYrQXDe85OAGVLAzlvR6jptcypaNfGMzMdTrj8Zhciu4YTXi02Ch+KLARQLxDbRRvPnNiV/mpdSc1+SnwxoUzIlR6HnE5MTl7ak7whgdd5n857ZvlH7npY5lP3kwE5gJm1N7H7Jr+42gFNszYntnawtuX/nMgadN6SKb3OA1exr6d9zp2y+utJEqqSASVfm/AUw/6FKe5Vld1Cye6hKAriEKqRgVEFEE76ea0Nu8DX33nyK0s6Ma4M795XcL4mEST/XVB4k45OthLnTWjBCXymgBNioBYDxCyhQiyxTYUSYLehU850o7+cskPT5OTWSAkbmNnruHZLGS66pbSExdwGk95gj1/XExZUH+Jz2pd8XyWmnTM3OvUZTqTpXgqZjnfmHkjlMb5U+Z448mN8WBBlv1FRTICVv57Uh899PvqnJIM+RXJnUy4EuB5gTlRyYnEyxblwyQLafm0NrDok2Fdk6976Z2pW5z1JkjrBIByof5XOSow2y+VAx0l64eUB3BR5DBY+Dzz3J/V//Boe370CM0rdsGRsuv5Kzr7qGYqWDiTBmohUTzYFPojyTxs2PKDptkWP0cgJMSFGRH+P0O5IDq6KY7HNJhKa2F2/WnuSNbBXMWYbMV/XzJan7o/1cyc1dtiDJvcfJXrnrU7Crzh0fzT1bQI43HIfOQJQDZHqlnRdxSdfXnHU2DwT5e86XapoMoIjzgiCSQhOrzkGeKqZsHCQF5FzpmHpX2tInbY448M2T3HmvTjZfyRccA3Bq8QQKpmDMXO3pFo0yUAzojpt8/wuf4bv/66+YHjvGkqX9SBwzMnycViisv/yl3Phrv855V1xNzSqH6yF1dW1MbzdfkwFzJGwOhrlvJVCXNkDm+GXbeiXX55yKS4CSgf4kIlByc2Fy3Z8PbCE/npqBkDnfkXlGYb5POuc+81/ps/PCKHeZu+5YvZbKBWxeKmmbLuTXUgZiaYMpHcYM0Akg50rM9n1IhkcUzLwxtECsCf04wSWX0yTanszsOTnwigjtiU0okZL0UTItnu9v2o85LsJk7j1hnsQXPJSl1SL+9Dif//3f5p7Pf4Gh1Yt45y++krPOW0gUttj1wlFu/+dHefauZ6n09HPdr/0aN/zcL+CVK+ydbTFtJRmDPAjnAvekKj4bSJ3TkRScKTYlu0bmzVL7fgZB1c6RhvPVeSrkUiGUGnwmXdhZO9orRXKTItIGbVvjtAVb9vvJ+vwi78kcFZv0fbRR05QeOCmmGWDJSbSMluQleW6CM7CokFKPPHue47OeJ2V8oc2ZcYCOckIkBVwqxVMJkI68nQPNNsTNHCnunttetOlnbTkzV4m6AbLqPjVmLi0riGVVR4nW4QN84v/5NZ7+zi285Jozef/HrmPJCh8aNTcWhYBGzfDDr2/izr+7i5FjDc5865t5/8f/lL7FS9g722IillTgz5vUeSIo61MK+La4yeyGvLSS/OSnYi3fv7kLpE1F0gWRfKh5sdUGff4+JhvR5L0sAJR+J2czpVoCkEzu2qxPcylJ0rZ52V/tvrUXKoCMJBLa8de8gdgGxhzA5yY1D7g5hlr2d7sRbVMgB5ukVQYITEop3PWhVWLVrKn59kAK/jbIRGXuvckBmva946xP80CczHdqhKZ+byXRIuI4vkUJxHJaR5HmoX38yc99mK133sXl15/LL/zR6+jsrGEnJyGyUKkiGMT3oOSz9fanuelP7ubpPZOccvlVfORv/pal69azqxYyGiYTnQyZyU3fCRJqHvDzrxOkWOrmykVBUt0licch9dOb9MbtUW7/kfNI5GlE+gzJfV8TzZvNQ4aBvES22bsmk9ZtzZRfrNlzkMRIbvdDEy3h3lbkcL2maWPzE6yk3Lk98Zp9L22aZt/VDHzt76TqIKMI2SWpGyttkOALmfpShViVltVEQ0h7kaTAy7VzDthz/ME3TuakFMgi2SJJFzEKgUCH5zTDjJVMC6RCwU9mJ3Yym/XVgMKxYf7w/e9nyx0/4qWvOpNf/JPr6eqNiHfuRDdvQ2wLOftsZNkpYD0IPKQxzsgdD/IPn9/FA1umWXP++fzSpz7JyjPOZutMyFicqnTXT6MpyHWutBWTs0Pm9lkSMZ3RDdry/KQgyRaRsweyl6byZr7eShabtL94gpRM3080qNCeW2FuX9JXZjSK5Bbzie3Nc/j0l/RaEUEO12ZVc5emtMLKXECDU7/pY1IQ57ucl+BzpHgCQJtbBGSfKwbHRz3j+KSqc5W2rGbUI9MSOU2WTqpNKE5KW9IOByYn3TWhMqTgJltkRYFuAw3rAB0JWV89I3iZdIZ1FY++2iR/+nMf4slvfZdzX3oaH/mT6+lbpITPb0WeehZPLXgQB0X0spfhDw1BbMFTZOfTTD++mb/9zhQ/eGqKU84+i49+7nMsWreB56ZaTKmHl0ihvKTLS2yTBVty3DUFfzI2RjRbkZKBaR4oMtXu7JmT5Wuf3JuhmMyfPNfImw/EvCZJ+5J+/wTNk36PHEjTC9J2plKeE20MSYRAdkEK5jS3Iu8xTAMwbU+EZpOsQIxTx3ECRpsAKFb3WUyex9L+SaRxCETJPUmktWcc57UJVbDJT5wuGnH3TXtlcapTJce1swFOPss/F9e+hoVjEUxYaJK2WYiT+4RAS2BZSVjqWf7xz/6Yx7/zXU45eyUf/v3r6FsghNt3IU88h9dSsD72qE/r9imO//m9RGN18AS1lnjpKVT7K/z8a3q4bGMnW59+mr/4lV9m9shB1lQLGFVCFVQMVsS1AwgRwqQtUdK+tA82oU+xOgEQK4QqxBhihFCFSCG0Tki0ctdbFfeTPCsbn+Qnzn409wORCpE6jWc1b2u1xz1/j+xeOXy467QtsFKBmdwvnctUyqcTnWr/9POMJiaLIUcl0gnXeYDIfycFchtccfJerO0JsLn7pSpAcQshlcBxAuC0o1HynuuBxcPim3anbQ6osSaLB00GVV0bs8WmjiMmatOiqNrkcxfdSCVSBNQTsKQTmBqPVoQIYUEgrCt43PaVf+KWv/s0vQu6+eBvvIxFSyzhgf2w6Rm8ZgTWEB8U9HlDUOvGbBpl/AuPoNY4r06pTLxsDR2mxS9f18f5qzp49qFH+ezv/BbV1gwrK16mTTRb8GQgihEidX2OUoAkfbCJsIiT351AcQAIFVqp+lclskqYgUoyoMXzBE42ttl7kv1kz8nmQ9tzlejqvEC087Sts9skWxCpwFGB1HWa2mKq+YCaZDias4IQJ6F1/o1ziqMNRuZI5XTw0pVhcx13Kj5R//nB0LaUbUveNjhDhTA1PhA8IxQNGKM5qqEn3EexqMbuJ1teFk8Uk6hH0+6Ru4tarNr2IOYXdDJARtxEdxplQ8njhScf5Ut/8EdUBN7/q9ey8bxe4tFRePQp/FqI2gLxsI/sDfBsCVMpElTKRD/awdRD+5FCCVQwK5bRqnTQW27yS6/oYu1QwA++8W3++W/+mqUFoS+YJxCSsUvnIwVwSqEiVfeDJoB0c6kJVWpZpSCwugynV4WNVWF5UfFoS8907jNgavpMaX+u7TjBXCwkkj7VGpoDL22QZtpTEozkQZwBXTONH4vOWRjZKxXl8xYNqph2ZzQ3iDaTeOmKB5CEBOWBm3bG+W/bCyG9X5z7PbVr02c6ouQGLQV2ZCGyqVNe8Q0UnB7BkoAwkaOS/VhELCKKweKJxWDx0QzUXvJ5uhRV28BOl3AauFHAGDdBBSzrS4bW+Cif/p3fYWr4MC9743lc8doV6NQY+ujT+FM1kAJ63EcOeoj6qCeYghBUfEqhMPHVTbQmLWICTEc33sqV2DBmxQLlvZdW6TQR3/37v+W5++9ifdUQYHOULi/FXLs0MXAt4sCNJDSgLb0jVZpW6TLKGRVYGigVsZTEsqigLC5oph1TrQcQIHi5+U0N6vYPIDlJO2cxtOMI8TxQpwE6zcROmzqmSMgWr5JhygonCJ62K3EuAzdtqey+lErOPPfJS3UBvNR7kVMDboXpnIXQBnLye0oTko7NVzUp6EPr2mEEfJSSgUAsog6oHhZfkh/cewYH5tRwMOIkkC9JUAQSYDvJK5K2Os6oiOukZMaLoqwsKAM+fOvz/8Dz9z3EsnULefMHzsTYaeymzZhj4xAE6KwHB8GoD57J3AdBJUB8j2DbCGPf3wxBCcRHVqyAQgFaMRetLXDluiqzY2N87vd+h9bIMMtKJhtw16w2T07HCkndlpKBL0JoJZquYaFqlPUVoWJsFoFNNXDJa0v5ODG1AgNDgTLgK4Gki6ntGMhrsRM0WwL4uYtuLt1IMUFuzjX7O9UsJ+A0uzb93QnPthMg9Ydl0cQ2DciJdyUnydrZcJKAwksHNAX3nM6RgbdNR9rKQ2kvnljbz46sJUKJ4gS8opSMpeo5/68nFj+RxEbdEHqieIlk9hLubVB8sQSi+Lkfk0lsybSACwVbUCftTTLYvSZmZbnA809u4rbP/QN+0ectH7iA/kWCbt6JHDyCFAoQCQyDsQXwDXg4eEURQUkwnlCyPo3bNxNOtBDxkK5eqHai1uKZmBsv6WTlUJndTz/F7V/+MiuKhk7Tpj9zJlbnCoBUW2buSDGEOPfXqWWhIjYLKAqu/7EKwy2hhRNc6b/OK9Je1HPCr9lL5lCbOPcz1wgUZ1fpXMGYaZ6E0qVaJQOr5h47x9NGhqN2e3K+DuflSA0pyWRdpM4AySzXOauxHS4xAoEYJ7FpAzrOGjJ3oEWgaKCURN3aPM2BWURB3O+hWiyOMgQCVQ+KJuHFOW7sCQmQNfevJgalUjDqQG3anDpPUURcjoZnFBGLEYuYGEPIyqJBmw2++fd/x9SRw5xzxWouf/ly9MBR9Pn9SCKJ44PAjOe4keekf9xbIV48iBqDb2M0jJGdx5h+aD/4ZShU0K4esG4tLR2E686rUi0WuO0L/8Dwti0sLXuZ9JI5dC4nDXM0IM91Y4WlBejznQSXZAGrCOOxYUvDMBwlUjQRPi2EqRiGQ2EkdpI+xU0qPdM4QX4RkYBTEwM6UudlSSVprLQpRGJfRVaJbSJcaGeLtNlATkhmjoIUv3P9hG2GACYl93nakK6EbNXlO5Y2Lsd7PIHAGEw2qHPFSsqVikYYKgj9ARTSiEAiJTOQJmCzKE11U+aJpWAsVQ+CbNml0tq2pXP6PjEmAbePEhi3KNLF4EtKP5IFQRvMvucgM+TBokKBTXffxTM/+CE9g528433nEzCLfXY30ogADztisHtAI0E9BQ9UQ/SVV2P+7HeRP/v/Ib/0DqJVi5DZmOk7thBHRfAC6O5pD3wUc+mpPkt7DJPDR/j2Zz7NQs/S46deVxfAcNG8tsRKQ0qaIj8RKCWB5UUSIQE1KxwKDU/VDJvqwsEwlbCaScPU8zFroW5J3Hht/7VHGoJ2gMjchqSSNpHciTB0fztJZpUMxIFAtxEGPRjyhCEfBn2hzxO6DPiJ4Zl3SMx5CXOi2vmX3+ZFOT6cLAarmvmPDe1Q8vxIj4irKeYZwag69ZUOVLpDVyBSS8OmXC9tkM3updgkuOiMhlYMoQhFzwUKgsC1qRYnRmPOQa/onHwQUAIsBZMGH5QQxcf5XBUnTUQ0mzQVx9s9hRXlMo36LLd97h9oTkxw9c+cz/qNPcSbtyKHp8B4aM3HboswDcHGEaazjHgK1kNGjiBFC8v7Ka+4Av+Kszj8l19l7P7HGdg/TnVNGaoVVJME2hj6K8qV6wvsH4l5+OZbec17P8Ca087gmek4CRc7AZOIAro9i1FhIk4FTNvAXhAoHR4cCw0HmsLxOHHTJbOWBW9wwEyDL6pKlADQFygZ6PaEDkOiFQ2RKnULteQnSoCdyqiKcT5vK2BtYreo81j1eELVc7ZRpnMSlS/iFmJFhJlYqecQmyWqkSMg2a6DtvYyWcDyBK42lxOHCk2rLsfCtle10P6SJ1A2hqoxFKUd9UsXQEthNFSOtdzgSuJOa7PAhNMm0lpVacSxM65MEtHzHfXwRQlwvDoQ59HwE2MxwCa/k3BnRz+ClI4YJ7ULxrbfE0vBuOf3+UKf5/PIHXfy3L33UB6ocu2NZyKtJmw/jLg8GuzuGBlP+t+MsZMtVATxDbywDT0y6hZsa5rCooDFf/hu4ssvYOTZg+D5UC6ipr2ciSyXri2wsEOYHTnG97/0JQYNDAVuEYpxQsUkhm7VE3zTjsaC8wh4ogz5sK0Gm2aFA6Ewa01i+CUYanOJtoeC1N+tFAwsKcApJWFpAfp9pdeHHk/p92FxAVaVYFVRGPSdxo1xVKNunbBwfNxDVagYGPCg6rm5dTbbiVTJ4qhsp2+oGqeZAoGiCH6y+tr2e5vOpqNoNBXvtFe4U7qJJMvxllSqNdX5NuMErOmPJiNbEOjwDBXjrOZsiagkvmYyr0K6tiShAKmRZ5KfSJV67OSKJ0qHb+jwBF9ylCGZRA8HWveTekKgICTGoaWAA3wgNlkQ7vog+Y4nysJCARuF/OhrX6c+PcvZl6xk9fpe4h0HkIkZMIo9DnZ/lAWJsKCjNRpHZmnGQjw+S/zE0yDWBQnCkKAYs+Gjb6T3nLVoaBHfwxiDJBwXqwxWLecs8/E9j4dv+S6Hdu9keclQSSRZquIVmLIwHs/zRilUBA6H8ELT0NL2AsiJoOy7aQYOtKO5Qz6sL8Ei37n5XCpR25BPr/WBbg+WFoTFgftbcVw8ws11hNLlQ59PDgup6Ep+n5NE1u5L2RO6fUNngqWqZ+jwDYG0tVTeIaIKvrMYkzBionJSv3MaFcwHR1LjIMSBOhChkIAmTUAhMeR8YyhYpaEuvBxpLmkmWQFWLZ7RTAUaEq4GmQHXiJXICyh5BoPS5QuRdX7a1CzxJOXj7banXpJ0qbYEfOMkhBtLQxreDxKDpiiGfr/AnmefZvvDD1LpLXHN9WswMyNE2/dixKJ4xNtjpAESOHEnrZD41CV4/+31eL0VxPeQSgBRPRs5jSLKXUql2oONJ4liiFvOKDXG4qEYE3PZGp+Hd0QcP3KEh2+7lTf+4ke4sNMwFcNoy3IshmmrjFs3F54kzC7pVs0Ks3F7qrO9lsk4pRJdkwQOTaSlqLKqCIsLqcsOsp3wcwCnWNLdDoJB6QsEDBxokVEFReg0zg+euUtzQCT3u6RuOPIpboKXGKHtOYWSMYSqtObZaog6KZ7PYFPUWawZB5b2qsz9ICTWrNLAca2yOC9GankiSskzlFBqVpmJHW1Jd6hAwllJwexca4G4QUv9ygrU44iS52MEyp6h6sfUI5vLD9Y52WJK29BUcZwtddupgi8uc86B2KnMWfUYDAIC4NFbb6Y5McqKcxew4YxO7PO7MeOzqCfERxQOR4jxoBVhWy3iziKFt16OOW8RRDEYzw2Ejd3yTLW9dQms4ndwYKKXndOncOz4JMePTzA7MYVpNSj7htgohZLPnV/5ArVGjfOvvJpTzzqbvnKJNcBEZDkSKsMtmIoFXyRZqEm2mmnPKYmNkBr6mURWnA8ewYiyOpHKQpur54EcY3IOBIGcf9qg9HgQFYSxCDo9563qMkIh5wacu8k1s5xyjgTJKKpzfrr3ba7dJlkcPm23ZmbPbZ+e0cwvmAZEJFFrWYAkVU+5VNKkEWkHJZEUHQY6PEnymzXxVzsDoxHHTMVKLXbJZ6B4xmYKKKUNviGRAanPGAKUvoJHxfMQDC0bMt5qZhoEQCQxMAXUKh1BiZJXRHH+7Zkopp5a4LRVt5doliZF1pQ6sJPj/P4b38i2Jx7jup/dwAfeu4H45qcxsy1UfeJHIswRS2xDWmWP6PyVlN95FcEZy8FaMAa1EcQNJKxBWENa0xA3UOsUuxqf2OsmNp3UY8P0dJ3jh4+xZ/shXnh6L8O7xpgcmWZmukEcxXR093Dq+RdyxRvexHlXX0XP4EIA6lbZW4850BJquC1dXm6CVdtzm+50VxRPLR0eFD333b5AGPCdQZzPeU6T/uNUUmaSOxUcbSmbgnYshg7jAmLpBomsMbSBaxPjP7apUWnSpTJHOEmOGqXPSdlLnNgzWXZeHtBWNQNzGmDJ6IfmQ9aSRaygnYpp1Q1D2TjeVPWEgnEP90zbc1KLLdORpR47fmkSy9KnbRA6yZ0C2knrqgd9QREjAWCZDmvMxlHO65JzQCp0+GWKXiEZeGUqjKlbMkDHAgZLYAw16xOYCqsLFTbffw9/+u53o+EEv/a753FOr0EfPghBgD1qCO+fphV46KWrKL7lJQTnLEYKBuoROjuNTh9FZkchnERoIJqyU3H8AuMiXQKIB36A8YtQKENQBBNQrxsO7x/jmQf3semBfezdO0OjZvH9gMVr13LxG97AJa97A8tWrwFgNrbsbSiHQqGJS6o36bxpEmVLvBcBlqGCpdd3rlQvAUjqD25TwmTOmLsbRXP/tvd/ugt9HOVoWigZzagjpO7R1JZymYHZfdIbkHcO5B6aB7Qm8QPShUfSVk04dLLsUt9yfhXNTS4CMG31oKmPue36ixUmY+fO6fCU/kDo9ElyJpw07Pah0xemQ5iKlTC5n0nukgLZ4KhCKqkja2nakLLnA0LZKxDakCgdgMTtR+Kmc4OZKmLFI8p2T3jJ/T1JckQQej0fAZ6+/wFmp6ZYt7LEKTKNbpmFUNCWx+z+FvHLT6d047kUz+hF/Bl0Yg86MYVOjSH1KYxEiFGXnJuKS+MhnoeKe0OSDQ5g0bCJhg20MYUYHyyUG5bVdprVZzV5xbIudu7weeT5Fs/tbnJ4y/N8+Zlnufkzn+GC667jFW97B6edew4bqz7LY8uBhnIocsYZtBO7BEvFJNIzkcKiTnv6ybhkuz/IG/ptgRcrNJMfKy4r2cMZ1X4GTEdHG3au5Ebbrro0yy9lyyl+8gvKKNk30r/Tb5nsfvnNDOIkdCpt0/zfPJ1IXSlZBle2KlNrV5LNAHM7H6smbjylz4eBglCSNs/1Ew9FI7aMhRE163zNboA1W81pGDuV0gWB7kInRpxNXY+mqcXNBCRpGNv9W/Y6KPqdrqVqmY0a1OLEty6p1FKUgLLXyYBfJgyb/PZb38y2++/jFRd38t9eVkb3RVDqxK5cQbz6FIKzloNOoceG4ehhmJ5BbIwYQT2DeAJiEec3xMZKHCZGTuqrjlNFnc6axc404Og4HJuAWgviOLEtLOIJGhuOHlfu2WH44dN1jk/HRCgdAwNc/JrXcP0HPsDq088APCZjy566ZTpWOn1DxXO7croCQxHHP+cIwH/llYa0G7FlNoqZjpWaFSIxSOIYCBJua1CaNsWKZvPqNh3M5eUpRFNZmy4ASUCc32WTgteX9t9GJdvPKG3KkSQNZTsznFrI3k/ln2ouXi85ftZWTcnCQaGdNwEUjTIUQH9BXUoobe+EVWUmdhy3YVPjzUlRD7Kon0k8l1WvQNHvcF3QFvVokkhtVuMjnYKS10ngdecAPUszcTdGxBjxqPhlyl6VghQ4eugAN33qE9z7z18hbrV45xsW8MZruoi9flg6hAz2ISZGj42iIyNQbzipYby2X0xwO1OKHs2pmMnnjjKz9RjRRANVS3VlDwtevx6/K0iIo4FWC903jI5OImHC7FN/qbUQNrGxQPcqZM2lxLN7OfTQgzy0A+56vsVYzeCp0LloiAuuezWves+7WHna2YDbFBAAEBM360wfH2Pi+HEmRo4wdewYzdosrUaTVqtFHMWoKsYY/EKBUqVKqVqh3NVJtaeP7v4B+hcsoNzVBeKjOG18JLQ01UnoVKqngi/j75mgIVnEkg1XGmFOwQokNMiFvDQP2ARXXrJVyyQGnCS/y7bpaU3BGmWSq+3oTiNPaRZUO9lEsmhgqi4y7p/8eKKUPbdyW9YFSjqMZUHR5fwWJDUI3aqMrGUyDKnZGCXZliVOVbo9h26yA7GU/U48KTmaE0/RtLPkN3+CEpgujHQnT4ipR9O01AWGPBNQ8soU/CJhI+T2m77GNz7zafa+8AIrl3fw6utP4ZUvG6Svv4iWux3XnZiEY8dgtuYMmJR8ppvsUgwKjGw6xpEf7cc0Q8qLqxT7S4inlHoCeq9ajt8ZgBpohujuQ86/DcxJPYwjVD108QVw5puQVRcTP/tteOiTBNEUGJ+9xw23Px3x4DZLo+URxS2qg31ceuMbuPatb2d6bILdzz7F4e07OLxnLyMHDjI1MUl9egobh/hG8I3BM2ltFEE8582IrWKt8wQZ41OuVhlYMMjiVStZee45nHbhJZx27gWUO7s4GirHQ83EfnuXEVn01hW0SbbdScqX23oiDbI5wza1paTN4dUmGoDMO5ZuTcs49faZGU2NhhjBimZgtpruSlCy9ERt57m2k04SlpqFD9u/e9KWuLF1mThFA/0FF23q9BKLVZL6EFgaccRsFBFaR8JcvoXL8XC/x/jiU/B6ki6FhHYckviUkZRRdSOSSHIszXg68dsW8UyRgldgeP9e/vZ3/4D7b7kNIeZllw3yjp9Zzcr1g8kyLaLNCB09hkzNuIH1EkvX6AmAFl+Y2jvJ6BNjdJ3aR8cpHQRdBcR3bkKMIpp40FVg1wFkosa8FDO02cR2r0Mu/jnktGuRci/x/X+N3P0Xjp+rQdXieYrF55lhwzcfbvHCAYuNIpphk1J3N816g7hep1gKKPd10r1ogKGlAwwuGqR/QR/9/V10dhYpFnz8wGB8H893u2aiRkS91mByssaxIxMc2X2IvdsPsG/HXmananT29bLyrLN5w4d/nitedwNjsYsEp1gJbYINSXLcmfeSnLtN294L55ZLtHOCr9SwTOmIEec39xLcZBRl28y0ptQhgmxzrM1JYoVMStskASUNwKQ8KS+dU7WiibRMTcY0+cj5m51xMliAwUAoeSZ3vTM8m1GDpg0ToyChIZKkjKriex0YqSZDUsPaaUTKIJXEVEmZogO01TiRCB6+V+Cphx/iEx/9KLueeY6h3gLvvrafV1/RgdfXiV20Golj7MgYMjbpZJfvta2mDMia1jlwfDyx9UzJB89CrGiSACwJZwdFPQ9GxpGDo4lIsw7QcYiNfXTjGzGX/hzStxwlwj7zHeTWjyESOpqS1+soJoB6aLjzBcM3H5jFekWGlg6xdN0yTtmwlKWnLGJoaT+dfZ1UqkUKvoeL4ScBb41o5yl4ZNasJA4yNWAjajMNDu45xqb7nuee7zzIkd2H8cpVPvDxP+S6932IQ03LjLocjlDbmZfp3sdE3iXZlelTJMGHJBFiJwglsakLonR7zrEwa0nKqLnv+wnGTMK3EwmtOd9sIq01dc2Zdtxd0z1qJBlVmnFrUmAjWYJRO/XcrdlSkp8capK8L4KHpcNTFhaEHt/gmdT/4OR1bFu04jqRhk5aoxhJfdQGz+sHCq7lGoGUaK/7lE8nCyxZbZ4JePje+/jjX/pFJoYPsXrA5yOv7OHs5QbGZ4iHFsCZpyFHhpFGE/wAdaEuN7+pfsv0HXN1n4AmNQgk916CXPe5VWT3Qag1Ez5nIWwRSQVe+lG8l7zT+dVVscd3Y7/6Hvzpg+AV2/3KZ9nbGOkrwaohDnmnY3tXMbhsMaVqAQghbGDDJtpsQbOBNmvQqkPYhLCJxCEaxy4QlHg/1UoSxvSRIICChykWMNUu6OjjwO5p/uw3bmJ46wgDp6zh4zffQmnxMo42XbtdGrJm+eUtC7WsfopkeemqOQohOZ6cAHbAd5HI0FrGI2Um1kyaewne0k0c2UmympOPeeKe8p88WB0/cswmH/nJZLPm/3J3rXjC8qJQEhhuuvRE9zhDzSoHm5aZKGKgYKl4gWuFCJ4pUjY+sa0T2SYQJaAGJEZ1FpGCkyiSSuR8m1J+ryAGzxTZ+uyT/MlHfomJ4cOs6A/42Cs7WdvdRA8lFnUUwd69zrXmBW2pnPVq3jMk31sSVWraX5X8SCT3ajahGbavD1uEphte+Ud4p78WbNOtUfGxD30ab3IHFDrbz1VcIhSAtWhfF5y2EGyDJQstDApMvIDdOwbTk2htFpo1CBtoHEKSmitZsKNduktIEJgYp0qiwm2MxpY4tqjxWNbXxdte0c9f7Ztk7NAwmx9+iMvf+BZ8cQIyEPCTXBIUSp6isSsVIdI2GEmEpXtsEvHMsYaxyG0AaKmlHrdHXtIkonSOSQCdjju0gyTZO9LuXJ43ewngIpTYJuBO7iFJg+YIJgQR41xxYpOonmTaWkWYViVsRfT6lm4/wMdPGmzwTAdGylidRbThNs4ioCc53TkrXKe5BScY4zE+epS//ujHmB0eZnFfwP9zVRdrq010GuetKIBUC+B7Ofy2S3W591LV3L53+7vpj5Lv/RwCaQRaLbJZjJtEXhe84o/wNr4eYpf/oaaIPfQ4su27SKE4dx2l97MW7a7A+sVgLEQe9shudOseZPdR0BAxIAUPyj4UvETqtsHg5jn5XzrfKqg1biOPTbaseTHGj6BZd5rl6DFWdXbQ01Xg2HjI0X37MDgg1xPOnFbMS4er6kFDXS6Q4jwwBVH8ZFjyOdaa5PbMiKGeeLHSDQE20eNWNdlg4l6+JKsiCx/nGoHmXHHJTFWNy34qJ0VhrCo1a5mKLJORpaVKu/RT8n9RGqrsa4KnKVGRzF2TbmYtGAfeaauEYZNO31L2AozL9kAkwKMbpAzUEHyULvJnoYiAeB5ZmDUDIdhY+con/pqDj2+iq+Tx7vPLnDkQog3aoUwxUPSZe95CPu9WE09Eeu/kvXxydp5mpODO5e2CQBi5UbAhsXQi1/4BZuP1EM8moiEREM99HdMahyCRzmIzgSOxxXZVkPVLnb8rwmkpq0ghRguKqVn3XmogiXGATvolnjNs4ybUj8XMHA2ZOhJTm4hp1oU4BM8aSiJUKoaeXp/eNQFetw9EFDWiWDCIwPjoCNDmxSLONEiT0vyEVqTOoQFf6PddkCeN+kWq1BQmI2Ui0sxeS3qMpslVmQAlwec8ypG+meY6WcjKy8YoATBY8OjyhMKcWwi9nseCwGMmVg40QsYjzbYMZZhQl8jkvAyO/Duw24QDSWIsOqA3VAnDkEoc0eEFWQjbAaMEWsgWWRvMQhTGjI+PEcUxfhDgeR5iDMVSkV3Pb+V737qNlhpeshyuWdGCuoDxMxCqVai3kEBcgKRgwPfdiGs7bp5mcmdgzWG1Deb08/SzVDhoovIjYhugV/4mZuNrwDbanxsPnT4EO++GtO95yzu22K4qsmEFBAYJLSSZxqhCwUNWDGF3H8U0QogNGsYw7ZAlJUGKwuw4HNnWYGRrk5nDMfGMk8wdhRJd5YBqJaBcDih3FCl4PkFNkekp6HYLOw2YCGAjlyhhaGfvpVo/UsehPVF6DSwqug287QQRJ+R8A2Wgz4PZwHA8VMaSGLmzzcTZp6SCM1n+yW38bJhSKzGVbNpOvi6JYUHBo2pAsoSlnP5LVli3JxTKATvqIVMZ12nnZriHtilJaggIjktFyWS6fFfX0LqFUFtUbEjFK+FLOrmpFZZ7iRBGETu27+LgoaPUmy0iK4gxdHX3MLRgAb/+6X/g+ME9bAz2oIcfINy3FS+cRvwAPFcMRkfHIOjl0EzA5j01RiZiIhE6enxWL61y6soOOssKYZwJAAfu1COQb1oqwdO/Lc60tdAK4dwPY85+C9hWUmjd3cBKgO55ADN2EIqF9qSpOi471Iecuty1OYxT3pa0wTrdXfKQ5QPo3hEXsFFBNYZYqR+B/c+GHNgeEU/HdPoBC4plunuKVCsB1UqRQjlwNKVccM8RgAgJjBO9IqgVosQ9V6k6j1OHcR6ORuJTF3GZmGlGZrenFBL3cArmbICSfCLBJbpVS0JnCEdDS6hJ4CXDUS5ZTlwb/BT5gUBZvERDOgt11rqtNoOBR1XaRrXQDrS0qYlbK0WBJUWPWj0i1DzstC2wMsrZpjlKkgiVqCdn8brGxcCMVZpao8OElDznlhNStuEGRa1SLpe49KUXYWNLFEc0my1mak02P/4kB594gDPPPYuzr34p1aHXQuP9cOB54se/gWy5E1M7hvF9bF34zvfGuPXRGuMTsdvQIEpkFD8QVq2o8PpXLeGaS7spSZS45RKw5alJTotlgE6rMDab2GXXYy75EBAlatVkdAwbYXc+iGdjIECS3AL1fVi5GFk+hFoLsU2vSCoZu3FwkxQjZR+W9qP7x5CmhbpPbWvI8NMR4zNFFg70sWBDiXIlILAWmalBvYa0LFoACiXn3QGXP20UAkkKwzu7tpFksBWrHYBLShowwmioxKIMBkKnSemAA5FqPqQyRypl46jJmPb5UBDDoWbsCmam2KedR5Re6/uJweMbaR8EJJLsJzO01O3zSvM12tvq2zwvncJ0V0OXZ1hR8mnGbutUrJbJKHJJSEl7TY4tpJsGOoyhaopEGlK3IaQGRfJfC2XahsA0Ja8DSVJq2ssitZpjxBMKxqdQ9CmXCjz27a+y+Xu380SlyruuXMnGGy5H116BOeUiZO0l6P5HCH/0KcxTtxDOxDzyhMex8YCS5+iBTfIUPeuxZ1eNv/jUVp54bgG/+L6V9FZaaJRy8Bx403FKF12KbRsSVlbgXfqrUCy4LP+kJ07QGqhPYY5sR6zLBbHVItLfhSwaQDoqEMVIkpTictjBxoINISh7SSE7dfmVlQBZ1ofdPEHr6RYz5dVU3nUGg+evo7q8H6+3CoUyGll0dJro+R3oXQ/gP7cNv1xAC9V2n4xNY+kgwlTdUmsp4kFHby/gMBKIc7V1W6HDnHxD61wLNyfp5n3Z4hLdFhUNw01LjMv/dk1w45rupvGL0j7bJM4oWmK0ieO0oVWi3CSlwj6LKdBeTYp7bzAwSOC4sQATkWG42Zq3NV6zSG+fX2RRoYwvhlhjjjSnqGt6EkkS8kwKEDZjxaNB4HW0oZz8T9P2J2F5IeDJu+9k+913010sMmhmWWM2Edz3MNHDX8Cuuhg560Zk4zV47/4MreWfxPvB/+Idl0ccNhV6+it4XYsZnu3k6RcmePbJrcxO1qiUC9x7z1FmZlr8+i+vYaASO8mY6x255Zbn0argnftezNAKiBuAyebSfcdAfQzTGEFXLILlA5gOV7DG1Uxrka/GIqpIwWPf3YcZ/tE+es8eYPW1yygWxSU4WYWKh9nYjX/BOQxecSOmowhYJ+VVkjYYWNCJf9Zq9LrLib55B/HNd+DHMep7jtt76nbpiAVfGJ2wtELFDwL6Fi1qd1WhJE5Fz8HnfDqW80KlktmkQiF3oQW6PKEZGI5HSXRZ2+h3oQHBz6rmM1fSQSqNU/cYWRlXScATAs3YPbzYpn+QgN01xG2VmQwtoSb8LjGU0i1egQj9QQlf0ji/h4hHrGnx23bHA/HwTIAnQTIIkEdDIucyNRA2a9z3tZsotVp4Rrigv0bZhrSmfEwwgb/5e7DtPuKhM7EXvZvgwvfD4HLO/OHHOHP1CnjJL0DfaeB7vKalbH98M1/+88/y9P2P0F30ePLxMT71j/v56C+uJAij9vjlVVf6RsJBtf8SZOnLwLYykOdbDwaZOYosKCFrFyPScrtgWtn27jYQUpPHKh3Lumg0lP03H2T6wCznvG8dBc/Zso5TG7zCXpjZjBbXoLEkSfVJ9rQXuKy+qIYEHuaMU+HBB2GmQXZ6VRC7nwRF+0dD4lgpd1bpTwDddv3l5ibN5ziBZ7SB43zTwnTsSpgVBDr8dlkyBXp8w6yNCfOeLZKcIlH8iHZuqvNqtPd0pclIrcRgM7lmhKrUrdCIXS5xQdyN040BqdRMK4o6ye2242QSHifdY2DaRgSmAAoztsmMjbDJ8ha1FIxHT1Ci7AV44uXyRvJclWwBOFUU8PAd32XLvXeDjSjTpLt7gEOFJXj9vRQrRUraImgdxzu4A++f/wfx3f+CXPI6wtPeizx2E6Kb4NqLkUIBKbZYd/nlfOzM8/nmJz/Htz75aaqB5d67D/OS8/t41WVVtJluoTgJQ9QIyguR5W9HjQ8aJ3Ot2W6NrC/Nw7CwJ1GdAmraIM5QTFuKtSIG11Q491fO5Pkv7eDY4+PsXniQdTcsgTB218cGmZzAPvY9OP96ZGAFWOM0gvhEIyPYHcOYo8fRzTth5z4CxJUsSzLmtBBDsssojmHn0SaqSrW3h96hIVwYPU8mNBsPydo+H8/OK3a4CVtrERXPLYDxyDIbKi/p8llW8TIq0+GZxJPWHo4sLTlSlyqa1v11gRPNaEhEuxpOWo0S2luv0q3oNav5FiYD3t4Tlm5+zVUBSVasu+dwq85Y1HQEQ2Mg2XGB0uUVGCxW8RKjKT0uwjkV8huOLGpdxQ8xHgf2buMrn/ifNMSy8NyzeMPbX8/6S86n2NmN8d2ClTgx0A7sQO/+Jv6OLfAvf03z/Cswi84m+MFNMK3wlo+gvqBhRKmrws987Fcp9/fx+d/9I/xGxM23HuTKizZQNiGqXm4MkubZEIqd6Mr3odWViG2Siog89lPqJnIcKagLRWe0JSf5st0YqSpUNI7oXeRz7gfX8vindjD84BGWXjRAR5+HZqUnDKY2iX30FrjsDdC1CDRGjaJhk8a/3Ell6x783g6kVHTuQ9WEKikUbdanyVnLvlGXcto1tJDu/v5EHSRa9aRWn5A/xVMS7rxlRrlzPGag5HNpxbA4gCbKc7Mxf3+gxc+v8Fhcct0tmoQ7p1ovt0b81EZUQLSdQuhoiLY/1fYKczKoTQPSvWoCGdh8k+Z4aJZBlQd2UYSCcRMaJZ1qqmJU8TCIOOu9Kj6DxYrb/ZusZBG3N2JqeoqDh/Zz6NABxsaOMTE1QW12mjBs4Xseo4f2MR1EnP6WG7j81a9j3bpTKHWVCTRCowY2brnelAp4G85Bly6n9Y+forBtM8UnHiJetRS97BWY+gzMjENvP4ir8YdEvOFD76XeaPEPv/tHbH1+kie31Lj07KKrmp4lRDg3mXauQ1e/B3o2urB2bn7nTrc4SR4dyxFFaS+O7KKM07VBbg3aiClXhXPedwpPfOJ5xrZP0nFxHxq7OcQqKh5magx96Da4+q1u21cYEizuxP/YzxB9/Etw9DhqcgLIqW+kkOR6+MqhCWGiBnEUseiUtZTKFayN2p3JZNx8qZyC2f3+1LTyreOWY5HhcKQcroV0eEl8AmFH3ee2wzEfWO056gu0A1tzxzALrLT3BsyjfqR5OIrNwuBk1AKS5BLSvFXnVklzpduGv8uY6g98OpIaC2liykQcciwKUSTb6p4mJ/UFpQzMxvhYG/P4049w3wM/YvfunYxPjNEMW8nWH5OUnIpBLQXPUBgc4Kl9u3nsb/6SSqnEwoWLOP200zh74zrWrlxMV6UIUYhtziIdVYI3v4PwHz9DsG8v3v5hpOwjF74SuvoTHpjjhBrzpg+/j82bX+DWf/xn7ntohEvPWQKETiL6HdBxKgy+FBZcAYVuULe7Zm4m8ByL0HHrcJyM5GXZjO3xb/PoRLAkYE1ztDr6A055zQrqR2cToSlkaldBgwJyZCf22buR869FiKDZQgYCzFuvIP7Ed/ATyezkjaC+dVpDXRDnhYMtGqGgIqw582wyP3ybaLzoS4BaDDcNh3zxCIxHbu4qxrKkBD2BUDBKK1K2jfssTYNauM23A76rrhTniLlBUj91W/qmgBSrCUhT+6pdH2HOwhASleQ4XuqbdmB23ywaZUHBZzAwVIQkcJJKdxgMCqgq43GUeU1ihYpxu7wBjPGYmprmk5/9FA8+fAehMbQ6FlNYtIHuxUvp7OqiXK5Q9H0XpIkj4maTxmyNxuQk0bFRjh07wr6te3nkmS0UfWHl0sVccv45XHze6axdvpCSxJgFCzDv+QDxXT/E7tiKt3MPEv8Ac9rl0NmTBJ4SPFlLoVTgg7/+yzz19NPsGQ1plk4n6B1EO06F7nVQWeqKM2oEGjLXEpn/0oRWhEg8O9cbMO9rc37mvJI03KayaEM39aEShDJn+WQLIQiQFx5AewZg9TlI3EAbEXLKEHG5AI0QLUo7SloOwW8hGhOGhuf2tjCqFLp72Hj+eQl+5lo0J6A6JzX31yMONiwXdxcYD5VdNcuhBjzZMHgYApQodobYxQNOQ9gktaLTCWum4zZlsyiye3ZaNUnkbx8tQZYuGmXjJqBCqNbx6SSA4Io5mizPhiRZJLYWUaj6sKho6DBpqTCXcZd2KtUGoSqzNqaYGI0NGxOIocNz++zjWPntP/0zvvu9m2FwFcfOfS0veel5vGzdUgYrZbo8j7LnqEy2kURcJ8PY0mg2aIxPMn50mNH9e9i17QW2bH6O8aPD9HX5nL1xDde+9FwuPXcjXb1dgCGenEIna5iOfmRoaTIfeUAmksHz2LV9J2GrwanrV4NXBgnQpETvnO8nYyTty9OBS341ENWRzb8BtQMgflsap96KPJizTZ+2/Xtss5RUseroRrYNKQmNW3Gpo40pbKECV74dGVyCqk/r248hX7yToKeKVIPMI8XiGtI/jfEt20YL/Na3ZqjXItZddgUf//KXKZYKievyJLI5x5oy6iquJDMCYSwMNy1PjEfcN6psmTZMR6502JuWCO9e4VFyu2SZjt3WL5Ju+cmWvpyETh+SJ9pOWmTBFk3i55Iaa5D5MyRts2add/U4lF7fbY5NczhCa7OTpbzkvirOddfrtVNLiiY1rCwiPs9seZZv/ehBamaQQ4MvZ915L+Mt5/Vznq+sKYqr2p8DSNpmBeJAaBbLNLrLNFYvhovPBwtjw0d48vEnefjhB9j0wjPcs+lLnLl2Ga+75nyuvOgc+hYMQm8veF2krsu2zdEeOY1jTlnnygnYOHKgoZXls6QZi5puJjpBfOXErCoqPkgpoZ45JKTUIg/mfEfTpKn035gsR2vOc9J7iQ/qI9PT6H3fIL74LYR37Yev/giD48a+dXksGsSYUgusc00+uqNBvSVYa3nJNS+jWK6icSv3nLmgVm0/v+3fctpd1e08WVGCFUs8Xr9YGG9BLVJKnjBYbH8PcVSjpS6nKDUzJMFlmpWDYrPHZIeZJ6hX2tJbsnfafgjBScM44XNeQkM8cVy7lXBstYkrMLJ4xlUrCkyyPX2uyCJUm5Tcde244+HHORQV0EIftrqItTqFpZvtjZhTiqXkmvYpXJJQFxdCFzo8oQNoaeRqwqnBW7KQy5e+mmtefS2H9+/nR/fez4/uvovf+7ub+frtD3LDKy7j6gvPYKC/H/G6HSc2AW1EtQtSahTlzeukAT5iCpCbQAidxyN3j7mTb8EUoDAI9gX3PCXhv7mdKnOAnQNpBt7cfdsh4PYzE5roSn15yMQYjc98nuiuGQrlErZUQMLYBV4Ayi3wI7DK8WnDozua+NbD6+7hnEsvya+aPIyTluTTb+Uk30h77vKljSj9AfQXBNSSNgGcbVY1QneCjZj0sCJHif30tjmll62cds6vZCmhSNuAzFfjEZQe47bKBwgtC/XYMh0qDSvZMwIhS9AXcQGZoYJHxZDx74a65JaiOH/y5NQ0tz/8LDWvilSHYGqK8NhhQrOcQ9by/akaZZPm0mqCJ4GkiMpAENBrFdNo0FspMlAo04VlNI4YUWgYoWvNat67ZjU3vu61/Oiee/nWrd/lf3z2Zs684wHecu1lvPyyi+jpjbFSBL+IGA8xLrvPGI85h1aShLBbIXrPj5DRoxAUYGAIOW0DusQZjsRNMt6cf4kPHWtg9B6yDL8MwDIPxOl7eiK4yX+efpB8lkpyPIhDUKHU1WKmT7ANZ5xrbLFhjCmCqUZgY0wgPL0PDo6Bxi3OOP9yVq5bP49atTGFuCr+datMha7mt1WoeobOwBUjSj1mNvGQZetS22Oa7f5WqOaPqUUyezfWNDmpbbu3c0+T7K00/SM7fFJTSe2utCgdBlYVDf2BZFvZAVrWcKxlOdyyzKprTJOkCqU6Tt1T9Cl77Qy8piqTqhTFZb5hDMOjY+wencT6JSh1weQokwdCInsBoByOlUChkFAZ38VBUSOUCh6bt+3mwTsfYHq6ztT0NC+/8BzefNWFDPX30Kku/3Y0bLHdQkdPD9e/8fVcffWVfPP2O/junTfze1+4he/e+QjvvfHVXHbROfiFIvgFdu0e4Zav3coHP/I+Kj2d7dwBFIxH9OwT6G//NoXXXA6nboAn7kO//RXk9LPg9W+CRQvBNuft+kn22XeeBlJKOK+Z46HAJgOc36Kf/p4BNp1ROw/c7ec4VeburTGYslI8NSB6rg38uBHhdRjnrlPLbLPAnc/NOiHmGa664QaCUjlHN9wjjEDTCtvrlkcmIw41YupxnBVGD1Xw1LCm5HFBl+GsbkPFF+cSzbkd0g1b4FyHL6YHBFcsxye7YI5pMkd1uWcoopJVCXXn0yndAhsqHh1eMni5MQ1EWVIUegKfPY2IiSipMppI+UVFj4UF4/Iu0oWgQhOlnCX5CEeOTTDVjMAvQVCGVouRg8M0Q7cdK+8EM0kugBEoBT6jew7xtb/9Er/5s2/ntDUruen2u/nVT3yVm27+EX/1mx9k1fpTsET0+0JRhcNhyO6GYIIuXvGWt/Cyq6/gm9//Pt/73u18+I+/yPXnPcjPvfeNnLJxDd+/7Ycc2rOPstcAW507gjbG9nUz7hUZevY5zPQkLF+GXLQRtm1BP/YgvO3dyDUvR7LE2XTwIrS6EipLkeldoAWIc3aOccLGMb5kulODMA/cjGPnwTwX3OKlGX7u+uISxQ4rMuEkjueBeIBapCA8uUd5YTjCoCxcu55zrrrKtTeHISNwoKncMRGxtan0+8qFvQErCgWqxjkS9jWV748LN0/A144qG0oRH1phOK+nfWIxacs03ZCtGcjn0Kd5z2aOSyftcpK+Z5NvpqDODnVXV/BvZdHQ4UEtVnbXLdtqyp66O9bAiCuyXTHKqWWfgUASl5ozFhcW0mPL2+yzZGDIM5QliyVyfGKKZiiIeDgnq8fIgYOMH5sAMcnRGekxBsluYiP0eAG3/vN3WNLdw6Xnn0FfTyc3XvtSFg/18cjW/fz+J7/ETL3JNDCpllhilhaUS8qGYhzx8Rea3CsLeNc73sOf/uEfc+r1P8PfPzPD23/5L/nuv9zGoX3DeL64goxRMxnbRJ3HEcVVy5m49EKObD2EPv08evv34ZZbYeoYpjQLf/fH2G9/3W0wmDM1MfhVdOAKNA4RbSJ+DL5Sj2B0KmR4vMWRiRaTocX6IEVF/OTYzDyfzvzP834ynm9Sg8MBIlBKp7q8ad/z8KseNCzSFFqUuHNLCxWPZhxz+Q030DM0lART2scXbZoM+cKRkD31mNd0Gz6yqMiregM2VD1WlA2rK4arej1+ZZnPW5cKfSX44aTPRzbH3HdMMVkqpiRR63aiRHtJzpXV6kaNbCQ16aSRNCiScud8Mn9aPclN2uKCoS+AnbMxD0zEdJV8TuvwOBrC5knLhrJyStkkxxsoy0seRgyTrZBFBZNthjRZwMbRmTJzX2EYY9W4BJzeAovPW8/2z2xix3PbWPDyS2k1YzxNjIqE7QfGUJ+Y5PEnttBX6WDfoaMsXjjAd+57lOGJcagWeWTLDvbvPcTK006hrpE7JlksK4xyQcXw91HE14Zjtkwq71m+mk/+2gf5qzPP43Pf/AH/7QuPsKxxnBXFiKmJSbr8AlQHcHFQByoxHoNvex3P33MP5bEZOsoBMtOAkXGaVqkfnyC67QcseM0NbitUKmkUIEQXXI2M3M7UkV08+1zMo09Osn1PjcmZyB0dboTOqmFpf4ENazs4a2MXq5eV8b0YGolHJQ9sUmqi7b/zBqsqxIrXG6FLBVP32/nVdcMDW2M2HwwxWPqWLuWqN7whFXnJURVw71jI3VOWgie8oc/nvK4gYzf5V1p+d1kBXrFIOFaLOdgq8ofbQz5ZhlOraY69zMduG8SJZndHYjtK7KcWaFobIc0NT3+UtI5KuwytiNDjCQsKhqkQ/viFFlIpcGHJMB7B0iKoGL5yLOKt/YZTK07NFFCWFgyLggKVJH141iqHoxCryqDv0et5uRXoVmQUu3wBrKWzt8r5V57D8O3f54E77+O8S89DfI+mdZ4TTXJTPDGMjBzn+Mwse45N8dpf+ziLBgd4ZOsOmlEEAo1Wi5l6I9uZo7iCkC0DFSN0G+VYDHtnhb/c3uQdiw2/+aoL6Fi0mL9/doQd99zG0Ue+zW0/fIy3vfVamDoMlX5HixQ0sgycczaD730XW/7kE3THLRrWUreGqNxBdcM5rH7bzyC+JN4EaW9d0wgp9nPLwwPc8pk72LsnYrrp8iYLRgiMR+D5THs+h3c3efTRGh0dI5yxoYNXX7uA89ZXkEbLlSJIwZoaj3mgp5I6oyZu1v0lMeyLIRbEVyZrHjc/NYkYjzBscuWNb2LRqlXEcYQnrtro90abPF439BYM1/d5rK+46Ed7JhPngIDFsLUWcSyEwaKwvhuOjSrbmz5f3h/zO6cZTC4a3b6L+3eOskngIiQ7WvI/Bpchp0lLLCTnETqwGAGjylDBBaT3NODZaZ/mtCCesreqVA2EahiOAm4+HvPzJY9i4gUoimZ7NKdj5ZHZOq3kBKvdLcu5pSpDQZDQDXdN4HlOalm3GbvS381lb3wF3//rL/L9b9zG9e+8gTC0yRHALuQeW8XzfTzjqhw9e+Awz+49BL6H8Qw2arFgoJOhxUM01RVlsOR3zSSVjdRFOmdbwt/varJzKuZVaxeyz1S537+B0e3P8RdfuI1yR5VXX3EWBQ2h1A3FDrfx1kawagnPL13IZW96Ez39PQwODNC5cimdy5dR7Khgo5xBpW4jvzFCs9nkO3ceYN/xLtaet4b+xUN09/QSiBLXa8weOcLR7TsYOXKMlleg0fB45NEZnnhmile+ajHvePUAHV6YOCAMbgs3bQMzqWSVmVp5T0k5colITQ81hu/unWH3tCXQmEVrVvOa97w7ofFCw8K3R0MerxkWFoTX9rnzUXbPWlZXvWxvIaJEKoyHlp1Ny+5GO4A3WDJ4qljP477xkNGGsLDs3JQeeXOgTcycsG0jxaIpoNuZHIo7XqLfD2hYy2isripp6ptW6PaFqhEaVukODKs7lE2THvcdsAyUYrpLUPQtCzt99jQ8nptRXtLlUlFTl6CIsLvWJFS4vKMTD+Xpxiz7oha9vp8V7QPo6+7Ex4GUsEUYhqy/4iLC4xPc9c3b6ezq4MrXXkMcx+5QI7G0woihBQOcuW4l9z7yHFKtJq68JJuvNsPr3vQKeof6qdmQtKCOWMXzkhOeInfCbFOVlX6L8SjkthHD3jBiUU8HZ563np03vo3d//RJfu3Pvsrtdz7MdZefzuKBMp4I9akaW5/ewUN33MfZl13Bxl/5uQQtzmOgNsaGTdLjGNzwJv9apRAYPvonv43BsGj5EorlIsbznC1jLVGzxfiebTzz9a9xz03fZefwDC0vwMSG73z3EMeP1vjIu5dQNTEaS1uk5f3VySm6qZcJkiaKhYpCS9g8EfG9gzUC3xCGMa96z8+ycMUqbNwCEW4/rjzS8BnwY67vMfhi+P54RCCGllgWFwwxrvbdoVbMaOw8YClAFRIa61y6k5Ertbww4Z6SE6qoZrbvHO9dgk8/hXF6c4MyEPj0+oaGhWmNmU33syWfd3lutVsVqp7wrhU+h1+I2N/0OVQThutuo2sYxqzq83liCs7t1HbGXKIFDrdiVpSLdCbejBVBiV1hkxpKV9YVZcFgH9XA0LSW1vQMJoyYDJtcdMO1DC5eyOb7HgEPrrj2SiJRWqKIxDSLBf7bB97OgQN/we4DI6jvCqWXiz5ves3Leee73sSsxsnqt8QKRXX71w62lKNNJQ5gphWzrNriHSuL/PmeJjtqZXojCAJY+6pXUWjMcvBb/8wtj+7j3ke30+1HVIgwUei28dfqnFktQDSFuoMMSbJmTrDY56aIxaw9fU0CshjVCNUoixz5RY8FGzdy7Ybf4rJXns1Tn/9f/Mudhzkwbugul3j0iWnuPGWcG67ohsjm7qttaZ2XzOnv6dIqNWmIx3f3zxCqhw1bnHrxRbzibe/AJoUT756Ae2uGsq9c3+NT8uCO4xHW+DRF+NGksqDgDmlqanoEc9oMzQzSw7OKirO3ugpCZzGNR2dr3Ul6kyT8p06A1C4gpRziSn4lGgERIUTdoTSRpWkdEI1IotLdSbDgXHB1azm72/D7Z3h862DMc1NCzTpJPNNSRqcjdqrPWEsYKmg2XpG6U5za2wmEqnFOuCgbUldCbMlQP0v7OhkbnqQ2OobU6sQCY80aGy86kzPPP5361DSxtcSeyzcRK0w0m/SvW82f/s/f5r67H2ZkeJTe7g4ufsmZnHHeGUQehHGcUEinDruM2w61dzbiaEPp9Cy1mSYLOy0vW1QmFuXTBxo04hJl3I6PNTe8ia7Fy9j+7e8wvH8fo9EsBW1iiopHTOdMjdFjY+jsDFR6OPHVZpnZ34ma1oyO5H2wqaFnk1RWS+mi13NpxyynrvqffOm7Mzyy21IplnjiyQmuu7CTQgoPzS8Ypb1ZNM3/SBPnFevX+P6xJptrzqiPOjt4yy9/hM6ePtCIZ2eFLx+HSlG4vhtWlAzfOBLTIqAWK766SHBDFMRk9lh7l5Q7zexYU9g1JXieoRlbzuwSBgrkDNekxW1Fltl8mVGYcv/2x0mFUIWxyJlJLZvyEze4nnFUw8k5l6tRFKFlY06pwi+tMxwLhZkIdwC9CgdnYh6abLG77jFUlDkSIBTDrqbltIqPB7TUbVVPQ9eqgLX0dFRYuXiIZ4cnqB8fo3H0OJ3LFzM+NkFv4BN4hnJPt6uZZ9NCf4JYGG82qSwa5PXvfgMlnDEVY5mOIySpkUay0mOgI0k+2jTaYiYWvEgxrSYXLuwAtVy7oMSpHTGfPRSyuWGShQ59L7mQ09ecxtObtnFs9zBybBQpFZHaGIvHPse253cyceQIvSsrqFdMKpDmuGs2Y3OBPgfEGehzoWQFbIgd3YldeCaDF53Lh8v3UfiWcscLTTo6Sogm+40yT0e6IIA4RjRG0uPngFYzZnaqSWuiyY7pMkY6qIcR137wA5x/xVVAyGgofHFEmFLDGzqVyzrhzuPKuDWMNS0zkbKyy7Q3Vs+J2ktyJKBijXDvEcu0eliB/kB52xKDL3OHRTXdmuU0Q7cvSWpFu2CRwdX1yPYIou1DYuy84UuNQj8psCEIXrIlX8SdPehhGQxgMEh4D8q5XR5re6DgpavIvXzjwp57I8vTtZDVBcPhKCLGkJYrkWSA/ULAWaeu5OZNW4lm6xzZuodFa1dxJIapVkRvKaARRxQ8g1GDsZL6vlxZqjgiUks9iSKmgZe0gqVT7u7vbvGoh8q9RyOsKVELLQs9YUnVT4Smi4xOT7kEmaDgdEwrjjEdFV5x5enctXQ1h0dbMD6OPXaUme4VvLDvOW755vd418+/HUr9rmbe3Mwh0p0c4hcBg0bNjBueAG9xoXcwqDXYh76If3QTesqplBcv4d2vGuP0NTHr1wpBOIFqBVdVKZlr43IFNKwTN5u0ajHNmYhmLaLViIliS8GDjoKFKKazf4CXv/HNiCgtC18+KuxuuQNWN08ro3U41HLnIx6rwWm9vsvWFHeeiuYkquKixA0Mdx1Wts+6VEw/ivi5VXBxbzu4YsQw2rRsmwkpB0LJCMOtmKMN5ao+nyXFdv1oVfCrnlCLLa1UAkgC5LSer0gWY68YodMIBZL9h9IOmqu7hChTJyReAuX8Ttfx/PnMPsKAL+yMLNtCOKquNFYgZGeuQGr1Ky+/5Cz+6ht3Mt1S9j2xmfNefQWFUpEjjZDOYhEPt3UuzfjDgiSlGURBbJJfYdpngRhpV8JsKfQmSUyPHw/ZMiN4nckGhBYcb8QsqniEFv5mc427DnsUTIOlC31KJVeiqiKW9y8v0W0jPmcLRNUC9HYys+0CJiYO8pl/up1FS4d42XVXOveeCRywEBDjSpiJz65nNrPtqae48obXU66WyOdhCIIaD2aOw5PfQK1iF5+FrL0S3f8YsvkpdOV6Oi99KVde0wGRRRt1pFFHa5Po1DF0fASdnMROzdKYbFJvRNSbjgamZQHSgiqlwAU2mvUWM1NTANRj2NlwyWUF4JlZQ9VXCmLZMxVzdl+AoLSs47uhbYPZpYsq+2vKHYctO2Y9fCOsDCI+vBJev1hc2m1Cc7fPWr5zuMXGHp91lYBOA30lZUe9xV/taPB7p5WpOEcWMYpfFPA8N6FNdRE+bQtsPFV6fI9uPzkZti0u2q90Ecx7zS0C0p6Q9PdlRZ+nm01q1tKJSWoKGWasJofAJ+oxjjlj7Qo2rBji0R3DDG/fzfjBI3QvHGDvaMhoGDFU8NyWL9veNZH+qzjAZSd8iYtGeclydEdUKMv8IgJ8dfsMY7ZIl3UDNY7PnQfrnN5f4PnxkG/uiRitN7lkpc+ZS4qIcbm9ZTw6C8LGnoDBIGZcfWp+J/a6dzAxfRhv72P87sc/z/bNW3nNm17J0IqVeOUOjB8g4jE5MsUjN3+P73/+S0ixxMWvfAWVjpITKNlAWpAA3fkwPPZNxBjkyVuwvQNQqMLMNOx+HvUPYQv92OIQtnspsvRCvKVnYsq9yOQw8faHCB+7hfrwY0Q150r1fEcH0vlHXJk/xNCq1RgbGQE8Or2IlSXl6KzjEF1G6QuUZ8Ysyzp8yp7zYqS1D2N19NQaYd+s5f6RmCfGlNAK66vKdYOGNy82rK62N1l7Ak9OWn5vZ0wNnxmjHGq1KBulZuGpSXh4OOZDK2JO6TRZSQ0/rfBYSEDZTOiDAfp8j97Ao2LSQx1TwyKBaKrZNY0XJTXtEiMzDysSqZ5dCyz0DcsCOGgtTStsCAKm1TKulggnxV1gy9LT3cGNV13Ao9u+QX22xu4ntnDuja8AM8W+WouyV6bDU1rW4hJu2rtwFFCT7L6xihqSyKLz6zRUWWg8lvgeTxyu8bVdDYLBCsRuERRLRT6/r866/ga7py17Z2KWVCLOW9uB8WNXbgxXaHAqdsal14o4b8jw2NEW4eAg9m0fYfzrf0e853H+/qb7uOeHj/CSdUtZvngIUygyNT7Fvhe2c2TfQRatWc3rf+699PR2unMNE29A5lpTC51Djotj3UECx4bRBQvQji4Y246ER/Baw8ixBmxtEd4TUAsWEy97CcE511G68O2Ur/4g8sImph++lfCxW7GHdmECC0EqBMCYpLqntcxMTCSSU1lWFB6aViKEnoIy3FBKvseCirhqCwlQPHHa+ulxeGw0Zs9EyEDB8vZBj8sHhAv6PBaVBNQmZ1c6LRFh+N6RJs9NQuQL0+oxUBDKHhytW7YcbXF1tzBYMm2uTW5PYWihri6f2FMYCHx6/MTRlvP9ZQ6mzJhqv9KYo5PuKa/RTIKLGCzO72mAojGcWfIZqzWYVBdoKRrhkCpH44glJjk9Olkkb3nFS/n7b97B7omQZ+95lNOuupiuUpHd45MYYF1XiZJxoHZa05B6ByxOilrjDnpM86RjLAVgQzGgFsZ8/P5Rjnjd9AmOQxnHu0elxC88WmNpxUOssqDTww+EMM5zXGFK4XhTmZ5u8eozqgg+D42EsGAx+vZf4fgtX2V6+0OM1us8v+kgq+tPsKTsU6wUGVi2mOs/9E4uuvoS+hcPgcYgBWfUaer1EcS2YNXZxGdfhz76taScrIHJKfSCVyEXvBvdfQ8cuAcT1DDdRfzQYA4fpPbALlp3fZXZwTUUrn47nVe/lQUf/EvCN/8qkz/6ErU7vwzDWyFQJDCOpwsgSnN2ltSo7Aucd8wVgFdGGsKqDnHZruoM81iVQ7Ow5UBMq2W5oEv572cVOL/X0Bsk0tAmnprUWBWTpSP//OoSVw3GfOdQyD3jwlNTPjFC2VpeM6D85oYKnQWDtWlFW/CbmhxvnBwhIMDCgk9nsn09V2WjLaFTAEsC4pPwjVQ+iqQEv20AmVzkcXmhwIYo5jlreT5qcXGhSKcYXogjuo1HRyJjrY1YvmQBb7/2Ev7wpns4tv8Iz9z1MKuuvwp/fIrDdZeGeWp3marn+mQSs9qlFDuqEZEcP5bc1xc4r1ym3/P4xGOHue2oUF5WQmOwYYQXN8G4BTBlDVsmXXmzRs0SRri6bzb1JrvNDIcmQ+qzEcsCw2VLK2ydraGRpdXbg17/LlpPrGR20x2MlgY50r2U37zhJbz5ytPpqgRUOip4SY04rU8hnQNO42ganNesoKn30ncSlXvQh76KN30UCWfhga+g574OufIP0NoY9ukvwY5vYKJDFJcGSKVAa0+D4PBOml/4fUZv/QeCy99I52s+yMCNHyO+5p1Mf+/TTH3/swSzowQaJ4eEClGrSaqLq8YJuqonTLSUQuBRTYq0WuBw3bJ5IqZet7x6gfCOpT7ru5Kqs6q5Y0xS/GSiMhOEPb7l0j7h4p4i26ctz05aIhXWdBpO7y5RMcm5PbRv4bc0OdEzkYK9gUeXaWehngytkhiEkcJ4CEdaMRORq5Dki1A20OW7IyZ6PXdaKbgFaLJmu/97KGeXSzQaDXbhsctauk3AmCrPhy3OCYI5Zzq/87Uv459uf4j9M/DsnQ/Qf+HZ9Pf1MHxsnKPNkHDcsq6nSm9gXF3i5Lo0u9K3EBu3e6YksLFcYnHg89XHD/L7396Hv/E0Ag90dArPNDjr9G7O6Q8oqVBrWWYjZdOuFgeGI3bsqbFubQdi0qPrYN+xJg/umKFATF9Hge7Q0tfl06jHlCxMtwJmz74Mr6ub+IFbGasb/vT2HQwsXcbbL1uDjZr4SVmBeHrMbS2q9jqjMV+jI6F3wYU3YFedR/TELZgdd+O3jqDPfhk7sROu/gPkmj9Bz3gz8ROfg53fpbBgimDZEK39Tdg5hT95iPDrf83oPV+jcN376X3th+l5+8cpnP9ypr/6h0zeeh/pJuh2RFMIxB0G1RMIhxvQ5bvNHZMtZetUzKHpmLVBzM9t8LlyQeDyN1RJzoFqR/lSJCa+2rbwkwz0RoT1XcL67uQc5yR5Lq1e2q7+AnK0PqMu6do9ZKjgk+7mOzmYndrb31Aen44YCV3HKp5gMMSqNFFaSZS9xygri8KpZZ/+IJWLSYmDdM8d0AK2NBpsjUKaxqPoefiqnGYMG4PELyEgJuDjn/pH/scXvw+xcOqrr+DMn38PR0aPMT5bRzyhamB9dwfLqkUKktajdgMj4vK4fWNYWQ1YHBgeeOgQf/KVrYy3ipT7OymuWEh8eISuSxfTsaiDhb5lfTXgnM4Cp1SEHcca/M3dE0QNy/IlHouHChQMTE00eWbXLMP1gCULOvjrVw1wsNHiE7sa1FsQRzEmVg7tGacVWvxj+wnv+w52YpyOeIY/+9DL+dlr17syw8USaq3jh+VOTEcvJihkE5rXeBjPFXg8vh/dfDuy4w5kZg9x5xK44vcw669F4hniQ4+ij38af/JJpNqFDUuEYzGtgxO09o7QHGsSLttA/4f+kOpLrsPOjnHTH/wO3/inb4MI7/id3+bGD34YbMijM/CXh5QVFY/hmrK0IsyEyjOTIGHMWwctb18a0Fs02CSe4SX0sRbDcFMZbUI9VkpGGSwIQ0WhOynwbDPRl4A2tcu0HRdJF3a7+FAGaMedK56hJz2K4WRgBqwIj0zEbKpZCqKc1+GxpuzRkaTVWnXHEUzFMBIpuxsRexotyh5sKHmc21Gg309LHsiceytwJAp5IYw4ihKJoQCc7fms8V3utPEMo8fHufYDH+Pp3eP4nvLS//ERBi87nz0HhpkJI9KTTZdVSqztqtBfcBX5o8Qf2lcusKCjRMXCD+58gc9+cwf1qIApFhB1HZGwycCr11Nd1YfY2O2u8WBN1WNtxePOhyd5YW+DVrOGRg2MdVuNKAb0Leqhe2Evr1nt8/IVBT61o8aOGYjCkJIxHN49zsiRKbxSkWD8IK17byY+sp+qafFb776Cn3/NRirlAOMV0yCB81sHRSgUXQjf892PpGaQBfEcBz1+AH36X2DzTWhs4KqPYs55szuauTmJbvkasvNbiF+Ach+RlGjWhPDoDK2te9GgTPE1H6Dn0rdj1eNzv/NbfP0zn+X/+dQnecVb3gE25M4J5QsjSn/RuTJjlG0z0GGVX1gE1y1wOHKCywFzWw3uGld21CxxDD1GqSRei6lY8T3DOR2GVw1Ab9A+dyX1JpzgYMv58VMZLSP1GXUlwJQuz6PDayeNMG8lgHDPuPJUA4aKwsu6hEWFxMeYszSdUSjZsV576xFP1FscCi3dRjmvGnB6OaBs2nImz7lDhdHYcigOGUMpqeX8oEhF3Ho0XoEvfetWfvb3Pk1sCvQsGeKaP/4oOtTProOHaUTOLWcRygLLO0osqxZZWi2yvKOCX/DZf3Sa2779DPc9OIwtdkJnBalU3OA1WjBylM4NCxm8ZgO+se7McRGMBwtLHtvvPMjRYxFrz+mnWlS0FVMWZbDqUTMBzxxT+rpLvHy5TyswPH40ZHIqZGqsxti+Y0RWaeHhVcp4xw/Q+tHXsONHCEzMz71qHacu8Ilalko5IAgCyuUSPd2dDPb1MDTUR39fF6XOMhSr4FXBK6GaHM+WVKSyux+Ghz4FY1vh0l/EnPs+SOqW6IEH0c03IVFEw1tAw/TR8juJ1UPDFhpZot61dJ9yEd0Dg/yvj/46l113PRe/7JVgQ/5xRPn+lBvf2HiMhkonll9cYLisJycMVdnZUO4ci3i+BguKhrOqhtOrhiUFZ5u0gJGWcNtxyz1jSr+n/NZqw5Jigqtcvnx2WxKHQ4IgTRfPaH1GI5yEngvoubLTiPDsDNxyXBkqG84pK+d3u6T7/PeNQyUv1EIORJaSEbqNoSQwbpXnGyGTsWVFYLig4rMkMImRmPr58gk7EGmEYvAS1YK6gEmzEfK2X/19vvvoC6DCwLrVXPTb/x16u9l96CjN2GmApipNBc/zWV7wObfsc3D7AR743vNMHg6hsxMqRahUodoJXgkCHzlyECam6LvkVPrOX4Ux4Dvznaq1bLvpBSgVOfOGtRRLLghQFmFREZaXDHc8O8bm4YiiQDxbozbTImrGFDxYs3EQv+Kx6c7tSG8fUirgj+whvO/rxLNToE0641EKtkkh8Zm7s/ug7Hv093SyfukA609dwrlnncoZp62gb3AhptKPmgBs7DwIvo/OjsGzNyHbboI1V8IFH4ViJ2Cxk/sJn7uZ5vgMTX+QUEpEyenqlc4OfvS9B3j8sV386t99lqFlS4nDFqVKJ03gz4ctLzQ9AoVpFbpNzM8vMpxfNVnO0ZGW8sOpmMdmod+3rCi6UPmREBYZ5YYeQ48PdTXsbyrPzVqenjbcMiK8oTfi46eadtxjDmPQDNApf1ZNaPhofVrTIo0V49EdpG6TNDPOAbqlhr87GDGLz/Ki0m3ggm5hsEBWfNrRDTjUjNnSsjTThHvcsRQd4o63GI4tk2opi7C+4HF60aPb85KadenLZZeJumMfcrasC3F6hqc2b+MNH/k4e6ea0IroWX8KF/z3D2OWLmTX0ePMWHeuR2tqiubuvYRPPQdbd8PhMWi5U1iNbWHVEnsF6OyDoVVwyunQvxBGJ5HJCbrOWkXXOadQ7CkReEJz2xH237mX/lMXsvqVK5ONw+3d74uKwuKSz/fvH+HIoRk6izE9fSV6F1ToW9JDsa/M5EyTRz//CM1CFenpREQoHt1J44HvEEdNigXlJTe+kuKCAQhb2GaT2fExZo+PMT18lOl9B6gfG6EaxJy+eiGvuHAdr33pRjZsXAMdC1CKIGFiw3nogQeQJ/4nVBfDZX8IlQEH6voMM1vup3Z0hMirYDGoETw/4M8//jm2bnqODRddwq/+9V/R3dtB19AKDsUef3Q4ZsZ6xLESaMyHFhku6nSaYdYKD08r980oYyJ0eO5gqOMRNNTlPRdVWSRKnwd1K8xYoWRg96xwxzFDnxfzjTOU1VWTFK85Ac5km4sTt3IU23aCP4k6T7OWNHexEWG4Ydk6KyyvOg/BmBXuHVMWFGCg4GopTMbK0QhayY5rQROwO5fZLEodpeAZShZqGB6MhSi0LBnfxZGR52k0avR2LGL14tPp61wEyfneHoq1MZqcogVOmyyKpzgQx8TVLib27ObB3/0Tzv7AO+hcvpyxnXuYfuppwk2bCIYPsdz3WLdqBWe8/nzWrFrCQG83vu9Rq9U5evQIm1/YzpMvPMq2Hz5AY/EZsOY8tFxi8sk9zGw7QnmoBzylcXiWOArw/DRX3hksJnFvHQlhBks806BsW1z0stUEvQWaKjQipdZsEXqGQjmgWW+CdKKtkHBgFcULXkXjse/TajVpNHw2vvY1eL7zzqjiQtnNJuHxcYa3bGP340/w1FObuefLz/CZW7fwgVefxvveeAFDy09DvQ7cdrAWLLkE7VqNPPbncO/vwEt/Cyr9mGIHHadfRSiPEY2OoiIEQcCWrQfYunUf5Y5Odm7ewoFd26h1CCOHDjG84XLqnocgdKjltf3CurITaHuawq3Tyq5IqAZCF67s8qR1Xidcagi+MYyoMGYhSFyRh1rCo1NCEyGKybA4/yW0dxil30irEfiaSFZfhIrn5YLVOucOB2vKcB0CDxYEQqDQQjjYFPY1nIvE83C5EqoU1BmQTZSmdYfXRxq7tZUGNHxhSWuY+x/6BLu2fZNoepSoBTPNAr0dp3Hl2a+nUAyw0mTjqos4e9VliAi+8Th08BC//9GPMrZ3Kxu6y8zEXewLhqiPj/PoX/wNXqlMdOgQy4uWay4+l2s++GbOOWs9K5YtpFwpzlNjQnoc8JGREb73nZv59Ce/yLb9z1FfcTrRyguJxTBz4Bji+5hKgLZi6lM1bJzY3Nr2AFkRWpEyMzJJV0cRrRgm6q3knHQhMoJVF/TxxdlyobVEVtGF6wjOaNB8+kds+tqtTLYMA1e+DFtr4flCuRzQ2RHQ09XDqsuv4LRrriScGGfXw0/w0M0/5De/vIXbHtnHX/7Kqzj/zI1ItQcRg4QttDKIfenvIU99CXnwL9HLfhUpLyCOQgoLVxHFhtbYCCIBd/7wMUwcEaGcdvFlnH3NqwkbEzx+xx3s0H50+Zn0eRFXdQpDnmVfCw7E8HANZnCO/pFQOd6C6Zar0dKKlTAWt5tIoYhSSDhAXQ3jIYQqRJHlpb0xyyppFPAkgY40vRZQtZlR6LvgrzMIC/OuyxtrnoCNlaNNYV8AK0tuK1Zacym2iu8lqXxWORoLB5oxtVZIj6cMFmEgEPoDlxNS8gqUwsN8654Ps23/D7lm41IuWns6vgfbDjb4pzuf55O3Pk6xKnhlj/Jjvbzlol/mZ6/+COPHp/gf//2/88QjD9PbHfCx845RboX8we7VPNJaSjTbYF2/5W3vvoEbX38Na9euQDwDRKiNsFF9znoX30djy/iWFxh75FE27NzO+9YN8PB4xL17H+DI7BTRJddjXno29AQUOgMat25hav8o9eMrqCzqwEbWeW2MUPB8Zg8cY3pkgsFFi4lEiKxk9bdBiKebNGZaFDrL2LBJ2IzA84lE8VecSdCcJdz8ENu/eRvb9xlYsA7EghGMsRQKUK0G9PZXWLCwg9XnXsSrzj6LO75wCw9+59t8+C/u5Nt/0El/3xTFviH8Ysm5vrwyesH7kee/Cc99nej0tzF1fIbaTI3IK+N19/H4pm08/dhzVAOfehhx1VveSrWji/FKmfKF11B+5jnWLV7Fuv4OOrXFWKS8EAn3zzq1H0WW2cidGFu3QhS7oB1W8NRJXrUwqyYLEFpcfTsbxlzeEfFrpxgK0g6htwXP3CoBacKbWodEX3EHXhbM3L0S+YtFYVVV6PNbTGnA7hrMRrCg6IIogdO01GNlKhIOtmA8hkUeXNvhcU53QH8xV6QRBQzffuBzPLfnh9xw0RreeMkQteYBIm1xzZJuli48k//5LztYuKAT4wsPHRjh8498gkVda/jhF+7h4bvuoasS8GvXRFy/IWT6eMyVE6OMjfTy5ndex8++4waWLF/gnhY3sWkYNEuMTQxPY5javYedX/sGMyMjLL7kQta/992cNzTIu1C++uXv8vt/+y2OPOgT91exS06HaoHKecuYvGUL++/fxspL11KoFFzyUzPk2PAEh549QNQK8Tt8Is+0S9oCca3F/od20mxELFlfZjLoolGfhFodjEccBJhTzkVGhtED22HrA0hnN+oFSOBjuwdoxDGNyYjjk1Ps3DPNg48fpdJTxlt3IXTdzZb9h9m6/SBnrm4yOXacav8g5e4+xBjCVkjYfxmNPZuoPfcsLali1VIIPMYmmvzzF79FEIfUooj1V1zNla+9gVAjtk23GOlZzMKzSvSbBr5XZappON6CiZrllUVhSeDOrfSNK21xtKUcbsLOumVfXdnfgAnrdoirtS5GoEKAstJYblgKb1/uMVBo17LL+elI8y1UXLqGarLnEBd99FEIvPkZapnsQnHcZGHJ8Joh4YuHm9RKJQ61nEopCQTGWeJ1gVlgsCDc2C9c2uHTF3hAxFRtmInWJIEJ6KoswBLx0I47WDTg84pzujk88TyTjWlEwHjjLF/WwZ/+0qWEHKcVNqn8ULjj8Tr/9OnP8ez3nsczPu+9PODGCxocOljk05uq6Lrr+Oofv4+Np68FbaFhA5K01awGjyrt1DWFWJk5sJ+lV1/JwJkb8cpVIII4JlDLz374ZxgPm/zuJ27F3t0Li4eIVizArFlA57njTD19gC2HRwnKBdQKcTMkiqzbn+V5HDk0Rf2pYRfZaoY0p+pMH56mNhbSsaDEu65cwg+nSzzc2Y8ePA479qAdnUTlArLxYjh+FMaPoM/eDQPLwcbIklPQpatcZmF6ppwKtVmFnXugMUXZiwgM1Botms0ak+PT+IWDGM8jjNyeSxt3ISbGL7YoFgNatQZf/NSXmT10GOMJXt8g7/uN36Ta2c1z07McbToffb1n0GmLVkwPwulVn0V9hoqntCs1pWPsqJXFpx4rw03lSFMZi2A6clS304MFAawsewyUJAmNpzvg8666touulRiKvri9phabAFryEpncxe7/kuYMq+V1iwo0bIubjzeYlgJFT4jElW+KjKUjgKu7lVf3GJYWPdCYF/Y+wA+euIkndt9DrTmOJwGL+k7j0jOvZTyssbIvoOQfYv/4NGIEo4ZYI2brPou6LuWZw59lvDXKped1s/vxCpvu3k4UC9ed6fHeq2B81HLvyAqu/cArufTcUxD7HHZKobLM9UiTE6bSRPksmJP0UJQlL70YjAdx5BZB+nnCi9//7jdy+52P8aMDezFPboEFA4RFUE+gq4DtCGjVWojn4Q1WqCzuo7xigMaEZeyZ44wdOuSA5xl3Xkmpi8rqAu+9dglXr+viwHDIfhGOdS2kMTWGDh+H6mK02IlsuBDd9EMYOwIdPWipAw7sQEpFdHBRsk3d9UviBvrM3dCcZvnSEgPdFVqhOwNHYyWqNZJ9eAlQRECFwBdmJ2f4p099me2bnqbke0yL4UO/8Zuc9ZKL2TJbZ1fDnQOjuEqxi4o+iwseg4EhICbWkMgKxvguwGZD0qMs0rMpqwbWVoS11dQVobl/3aJMCzPOUeZzf8l0rMHlTmuas4Pio21OMv/S1D/scrJdiPtnlhU4uwfuH4/Y1XBnMZc9ZXUFLu0xrCkrBRGaUZ2v3f2H/ODxT9O0E3SWigxUCkzWp3l8zw94dNcP6Oj2adY8jk6EVHyhpcJ006MYWJrRCDZehpVFNDhMayLi4GMhca3A+r6Yj7wyoBDPMNG5iFe9+VR6eRSeusPVoRg4G879aPt87PwJofN6KoDGsaupnA+3JvRAraGrr48brr2Iez5xB9H+YYK9h2H5Ehpb9uEvH6LntRdQjGJsDI3YUm8Jkw1L1DoK0QHk0DakPgkotlyFvkE6u1bQnBD2HRfiQidBf0BvN0wM9zC7/yAy0IHuH0eXrkWOH0F3PAejB2HJKQ4dB3Yh3b3Jxt9knW66Ew5uAQ254ZKNVAtCLbRzwsWSaCkV8DyPIAjYtnk73/qn73Bkxz4Knsd4ZHnXb3yUG372fTw102RP3WKSLLgeXzijq0CfzHB4/Anu2nc7040DxET4fonOymJWDlzJysFL8aXYDn4kToJ044gmWnKODNb8QUDJ5wnlyPNmcJLdZdnlgixJLpFzp8yDs4jhYFM52oiYjSKWlX1WVlzZrTM6hdO73NneLes4eNkkxwcZHyhw/9Nf4JaH/oJLNgxx7fmnsrC3g4JXoBEqu4/UuOupA9z5zD62jEd86UcFPvCqCtNhDfGE440iBCNsOf5JJuwRglIHT9wfMLkjoqpNPnB1gWX9DVTLDC4qImNbcNvmkmOTZ0fcYZaZVaEn8/4wNwqKoyLpNrF5gdLTTl1Fp84wPnWc+MGnaOw8TDw1RaFvNY3RGrXjk7TGakS1WTi2H/+FRxja9TTrgjqrlvTT3VvF8z0aUcThvePseHCUf/mU5dZly6lccDHRZa8gXnMarT27obPofrykSOOGC2HsGIwcdFRmwUq0PoOMHISlp2DiCJ6+C/vcfWBjrjlzITdcuo5GK0LVzO2uKEHg43k+h4dHuOfOh3j07kexDXfuy6xX5L2/8eu85Rd/iSdmQvY1lMD4IJbFJZ8zqj5jYw/wjR2fZs/IXTSjUYq+gBjw3ElVT+z5MhsXvpVrz/oYBa8DTTa4pUohP/KpUMnGP3VDiCFCiawSqvOQeeLssEThJptjcwJZ1R0aFFmLeu2ChyKumN6mKctVvT4TIXztUJPL+ktc0iNEyRb4ikDFs0lbnbd4z+HneXrnHdz64N9w6endfOjVg4gZpdY6QKgWv1DgzNVdnLtmJeetXsynbnuW2x6ZZuPyXs7fUKSocGCqiC0qEc+jxica7eTpbzcohh6rlylXn64Qgs7G0DoKA13O95VuVQo6IOjh5Cg+KbJdvz3f3ScdYhujcQiqBEGBgBbUprD792EPH8F0dhPXQ2aeOQD1JhLVKW25gyWPfptLzRSvf89buPQ976d3yTJ8z0U41SqNWo2jRw7x8AOPcdcP7mfTd/+J0a98gfC0C7Bd65CXvgYpBGjRh1qEBiXY+BI4dgSOHUI6etByN3p0GNM3iDx3L/Gz94HCusUVfvNtl1DwhFasSLo7x/PxPEOrGXJg9zCPPvQUD9/7GI3xKQq+YTYMGVy9hp/77d/jwhtv5OGpBsNNi298VGBtpcwqf5zHX/gsd2/9FLVohEW9VdYNLmWou8LYVJMdR8bdWZQm4ql9/8xQxwYuXPeejOdnSvIkXrgU5rEKdbU01FK3bqO2kTTd1KWr9nvu5MK5cirZfwoO6aG1FI0rwyUCW6aVu49BIDFWhEO2xKf3Rmzc6NMTzPcVCFYjvvnQX/GNh/4G2xrh/FUreeNL1xCzlWOTo5QKJQxFphvHCO0hiv5+Lj1nI4sXvJJPfudhnt8V89KzV2Jbz1MuVjkyVaC3q4UvAbse85jYCUZCrr8woLMcYccFPTCBWdWb9MdmESKZ2glH7oNlL3dBhXky4QRcJx2Z3b2XxpEjqMZ4pTKF/l6KA4P4nd2Mjx4jaoVIqQy9/Witjp2awLywA9lwOpiY6oNf4/Rnb+Fdl53Oxv5umlue59AXv0Dlne8mWHuqa4uxlDvLrOw6lZWnnsZb3/U29u3ew+23/oCbvvptNm99jNnJXcRXvhXtWYXaOjTr0D8Ey06Fvc/C8YPI4g60UUMfugW7/2mQiBX9Bf7ovZexYqhKM7QESf3q2ekGY6Nj7N6xhy3PbGPvjn20ZmoUfXeKzqxf5OLX3sD7f/2jlE87jR9OtZiJHB8uIJzeEVCtP8dNm36X5w9/j45igfNPXcmyBQVU6xSlyMKggDUxWw9Ou/oafsSu4U1csPZnsrIImQbUk09JqHAsiplVmwVLINldlHD/iUgpYumQNHqcuO4SSe1ndZlt3D4GQp2fefdYi7+dKNBRUHZNw4ANacYeUpDMUWAQEJ87n/wiX33g91jUXeXnX/0WNi5fxnT9IQ5MHgdj6QzKlLx+psIasbSYCqeYnniMhQub/Ol/u5JWpPj+KGFsKVdCRo5X8CqW7rjA9vsipCUsGLBctl6hYYmPzGLi2J3+qMkJqybpYqsBu26CRReBKbUR/KLC2RkXUX2a1tQYjdGjTO3bzcz+fRAJCy6/iifuf4JIfGTFali1EWnUsZufw+7aijSnKUwcYO3mH/Erv/webvyF90Kxg8k9+9l76/d56H/8Hqtf/3pWv/F1iCfJueEWJEKMsPLUNXz4Vzdw48+8ha/f9E3+6Ytf5YW//UXC815B69zXYcsD0GjC6lPh6B50dhqpjzsjdngLEFKKp7lk0QJGtm3l1qfqNJohk2NTHDs6xuTxcaYnJomaTQJjKHieOzE2KHDOVS/nNe//IOuvvJq9+Dw92XLb34wLRV9YErbv/gq3PPcHNMLdrFnYz8aVQ5QKwtHpaQpekUJrkI6Sz2DPNIfHfY7PKq0wphT0YYzBaoxnAkdLEptG1VUsbcsXYTK21FLunLxv0ezg1lQKz8SWSpIVmvNXuUhhyhlb1jpKIAaryoZO+OXVHn/1QpPtkwEeljet8hgsmWzlgCLG58j4QW7e9FkCz+cD176cM1Yu5sjE7Ryv72B0tkx3Z4t9M5N4TBNrRGdhBVFzjLqdYtfE0ywoj9NZ6uR47TBWfIyxVErKjqMep0iJg5vHEIV1S4WF1RY6apFaiHqCnQkxqWkcpyRNoXkMWpNQrnBidfl5L1XECN0bT6f79HNQfOLaJPUj+xl//DGeuuNe7rztAaRnPfQOYWOLKZcdLVCL7nqBrqNP8ZqrzuV1H3gHXqmKBgX6Nqynb8MZTO89wLav/DM7v3ITa952I2nRCUmt9NiVwRxa0MvP//KHeN0N1/PZT32Gb/zLt9m380kaL3kj8crzYdEKWLYW9jyHTo5Aqw5qCbRFv06z+dH9bHsgJFBLoOCLwfMMvgiBCMVCAQ+lCay79lW84t3v57TLr2A8KPFIPaKuER6uqKYVOK0c8PjmL/H5hz5KX88s569ezMqhTmbDkOMz7pCSNf1r2HPQsOS0HkabT1GtGo7ORGhYYcOKqxHjobbBnpFN7D72ECEtVg5czCn9FxF4ZVTDBMDOYI3Vkobr4oQnSzJH6d53zSM5P424fajZHzNRRHeQnCFilVctDjij22P3rNIX+KzrbldUbxN5j2f3PMj+Yzu56JRlnLWqn5nmNo7Xt4FnsZTYecSwYnFIixbWWrpYRlfpIo5PfJ0Iy0y0n8KMh288Ii0QRx5LemL27irw+A6P2vEYX+CMZQFe3CKeilzyuwcy3cA0QmcApj5IMTBzEPbcDBs+RBomPaH72cgkHo0oAg9sY5rm8SOIsSy88nIO7hxhOHyQaHA1Wu5y5QaOj8L/W917x1t2VHe+36ra4cSbU+eojsoJyUIIhEBkkIkO2BgHPMZh7PGMZ8bzPA7jN+M8OIwTxgEHwOABYwkLiSCQBMqxpc453JxO2qHC+6P2Ofd205LA9oS3+3P7nnvOPjvUXrVqhd/6raQFey5FnjnA2qll3vrO7ycKPDhdWIuzGQhNffME1/zMv2bxwCFMkhLUS+fBbbsgG2s0AsP6jRP8wn/9Od7ytjfx+7/7R9z1wF8wdeAJ7J5XIrfvwk6dgNZy79r79DLD6RyRtUQSAufRf4GQlBSUhGYozFHAdCcgE4qr3/PdlF57O480LXnu2VgDPJDe4piIQvKFQ/zZV38bF8C2DesQgWE5MeTaV56M1PppLfSR6RlU0CRppTgV0spSto7cxq6NNzK3dIh/eOTXmc++gA3aLCWCx078DduGXskrdn6AtYOX4YqeM4OBInGwoD2ble6GWvFY9m7pXizlSsG162pnb3b0yBodkFiD1NAXBDjAWMO6Cqyv+oMau4K/6z0NDM+efJSOTrlkzQYCVWV+6QSp0UgRM9LnsRlPH4UdmwWp1vSpEuvqr6DWPsbJ5ScxLkaqkL6SB0dpF4ATjPYJvvjkPGFiCJVkbb+D1GKSotmcE7hU4xY7iLEK3YaQPrhv4dwDsP07fZ/A84T4fEGmqGSxzqKX5kkXZ3E6J4oi9j21n7/+y7+jU19LfsnLIIggz3GnT+AmxmHrNmQ6ycbBmPWXbMZq40FKrIy4057UcHDvJTizqhFPT6iLx1bwUTjtu6hec+O1/I+rruIfP3sXv/Phv+aL9/85dmwXQgaehKagcb2htMTrygskBOQI37FCOkoK6hGUhaEqHK1ccdcZicgc9/z1X/K2m19LHkT4UuQVoviagK0lyT8+9kXOzp9hy+Y6Igo4dC7n8i0VrM3pZDnbS2Pc/9hJXnZFndnWNE2taGtBmvRx68u+h8PnHuLP7/1Zxted4brLhzgxFdCcynAiZ//M5zg++wSv3/vvuXzzG3HOIHGMBQEdY2i4lSoUWZjdxkFJOGpKFHBRP76rY1UBqwRUSd/p3hlBXfnO2r4gd4VkEQQN56gI37ZW25ypxbO+L2HRt7uVL2GlwroQZyTrhiWTSzFPHFJs3SQ4vPA0i0lCKe6nXt1EI9WcXTA0Us1w1WBdQDsPUNJRwlNtBYEglh4P6qxdtfaAmW0TDJdAro4hOy98Pb+g+4VVkU9R3JvRZJ0W2fISNkvAOaIgYG6hza/+9z/l2GyLxTW70UvLYA5BluCyDgxdArnF5Ql9lZAw8j21z8uU9UqHHE7nvdOeJ9E9/bDyaPwhHFFphLe843ZuetkYf/DRf+BDf/MgM8v9UO6nYOumGkkuKVkytEc5Bn6RkkJ4mRcQWMFYTbKprVicjzj9tfuZfuox+l/2cozOqSvJeCSok1FRAX3C8ezpZ0hzR6YFVsJC5nh+MmHdUAAiotWscm5xkupAmSdPLlKqBEwtGraN3UwjXeLD9/wS113VYeP4KO1ck2uFsQKhBEJJWuY0n336lxjp28a64T04p4mA8TBkOUlX+iQVmcNQOMYDXyJoujgi50isJbfdTOFFhrWhNam19ClFLGWPgzd3MGsMi9awJQqLJof+IRgBp5ZOMtO0zLSaOFUuQj4BAsXODTGPHtA8dyhmYk3KvoPPU463UI77GBzoIKOcZ84YLhkvUStBM9VYE2AK2bVCFB1Bbe8mhRM+FNZIcfMpYiTyAGjtINO41kHY/iyMXdHLEnridIOzBpNl6KRD3mlj8gxXMGrGUcT0/CL/6b/9EV955Fna664g2/kKsBaRtCFPfaYuLnlm3DAmyS02zzzFmNO+XMq6YvLZ8zMIFyQPesK9WtWIEDf7MK5xGLe8j2GxxM++bze3XXsLv/03+/i7+8+Q5P0QRXRkgIwkfWVFFAmUckSBIw4EgfTJBuFAGsOVueHJZYloNnn6kx/npmtvpC9Q7HCzzD7+UZpnH2aSjQxe/i5mWwvkVrG0aGk0LX39MYenE1Lr2L22n31HlhkaVzx17izn2oZ+EbDc6WPj0Fp+9VO/wtXXdajV+3jy2BJ7140wt9T2aWqT+0J2GdKyS8y2z7JueK+3dHHUlWJAKuac6SnRuhKMKEm1wNhLIehYWDKGtrFY63+C7kj30sK9nS1t47u5htJXeLedo1MARroVuYEMGatvwFjBoyfnuPKcoW0FWRoSCAlC4eEjjs0by+w7kvPIUxE7Nuzh0k272HfqCKfmnicsQyIkDx7O2LUmoF6VtDsBnSQlKvILmedjRUjpU6TWobXAaoc6vERpOUTkpgjgC0TYJj3yJDkTCKPxnA+u4IIwRbjHa3zrLIFSCAGPPPksv/H7f8P9z57CrbuM9hVvhMENhfIX0FpGnD4MC3NQG8QSc26xydSx45hOh7RzABlXqI2upTo+hirF53sy55k9KxJ+nl1tWnD8LxBLzyJk7Lkr5g/zsrER/uLntvLuh5r85l8d4yv7GixGhoGhkHLsWa7jECLpX4tVZwgj2DOc0n8iZCkMOP3VezGnjjG2dR1ffeC/Upv6M966SbHcKXPfIw9y5FQDnStm5xwHDuWMr5dEpYgjZxNqkWTyyDyjmxKen0oIlGRuMuDIqZC7v/IZLtup6eur89TxJqP1iKWmJXOwaaifSJaZbneYTxNyq8D5VdQ665+bcIxEAYupRSlJBcdEqCgJ51mABSxqy7w2BSmoH0rjVhHNdON+vYW5iPO1nfP96uhy3Dn6pCKWXeCo4vqdt/GxR/6UI7Mpn3ss4cYrInIyz17qLM5JnLBIlbFmQjLQv4Wt68c4OX+aQ5NnmVinyHSGDBxNF/LYSUmAo9k2dKwHveQ45hcdrHPIQJBoyDsWY/xNinaGko6wIhGBL3d3skwejZFn2mvVQiBFAcSnmMRSKXCCIyfP8vFP3cWnv/AIZ/IyWW09rrwOEZQ867/FlzeFJV/dcvo4RJNQk0xnlo/8P7/O2MgIazeuZWLjesY2b2D9NdcwtOcyHPpFwoYrZfh0l1ndQGTzQAyuwKkbg507h1ie4y1Xr+OVV2/lo3fO8uCnz5JoTT1yVEsSVbR5EavaoDnh6SrG6pYd9ZSHOxHJ1DnSJx8hG1zioUOf5vbNZXITMorkmvI5AtNmqRGgS5ZDBzXtlqI2FiDiMo8/P49czn3HBw2iE3P4JJycbpHnOWvX1Dl6tk2rY1jXX2Vysc2mNUNIShyeW2bJGDrasba8m43De7A277EAOKCsfOV/JAV90pfSxUIiJExlGQva4HWq69nSXilJgfKhgSLB4sic9ZzRiCJL4zV3LASDYchAEBAUtmmjPY92FhXUWc5muHffIm1bZdt6Rb0KpVj4B+a8TdfOBJ//+ml0cIahoYCRQVhs5gQl3/yxuRgyvyAhd6Qth0wEo5FAaMfxGQfWPzDrJHmae5NDeChGloGQFqlAlRRieC2yXgMBVqkVwkYhPXZbSlrtNs/tP8Ld99zHvV9+kNn5ZVq1NXTKwzinYOoEYnEWuWUvcusV2FIdELjRdWDGCnaljJlohPm4yY/++x9jaN0Y8eAI0eA4yGCVE7jaEV0l3asVdXcfWcHJOsLN4In+gIIr1TUSzPx++sb6+ODtA7z7yr00Hz0Bx6dhuVloOX+YXhPZ4ryRFFw5anhsGrQ1HHvwK1R3pGRRykNpja9+SfPTQ4bxUpPXDQt+Zx6WOoJyEHJwv6Z8PKfcL8mWU6IFR6NZoqM8MX4ahYhSyNqBCKFgai7HZpLpqmZsqMb+qTbT7TlS7VhecGzrv4J33/gfGayOY23K6qbbEkEoJUbAovEAJK0kDa1paNvz77qcdl32paCqVM9OAYdTntt5WWuaxeAKHIOBh4KGEqQIWGzM8pmH/4rPPvw3zCwfpRxqSlGJuY7h689rBvqGODrdoL+uWDNo6a9aQiUZqDtKlTb7p6FpY3ZvX8v+A1NEFcXklODQIYPOLcJIrBDElZi82iFowXPTjsWOYkBqokpIp5GzGqrg7XmBzR1Wa+TUJMETH8eNXEGnfyd5bQThLCZPOT05z/4DR/nSlx7gmWcPkqQJcSC4umZIg0W+bAdpE4CSuLSF2fc1xNHnEduvwK3fAVEFrAJhQcQ0R7azf+5htBLYLCWZm8Z0EuLhcWSlskJj25XgC/zTFcH2wCEnIwTRecGYFV9WIeaXsJ0cMb3ISKXCyDV96F19NA/M0T40DVnq26O54rzFA3cWdg5BXwRNEXHwvnuZ3X2UeENIGsOTseJ/PNbgl67S3Fq1/E0Zzh13rG20GW/lDOSWsSBgRAVU8pD2U03mOhknhOV4X8CZ9SFzG6o8o2KCQDE4EnK24zh8cImOtmSpIF0Mef2l38FbrnkPlaDG2bkzDNbqlON6L2fg8EUBmbUFlzZMZrpIsviB8O24KUKNRSzb5MkqdMiK8ug4y1SeY4DhIKA/kDgsSgacmDzGf/nET/P1k1/k9ssG+e6Xr6O/7HmVjy/mnGjkDA4anj/X4dnTjlIJNgxaqiWP/rKh4sGnLKdmNDftGWbnxjE+8olnWVhwKIJCewaIIACr2HqowZrTGRnwUzc7Xrkpw7RhbrJDmpge/GJgOCaM/EN0DjDG0806SUPXmdSjlIdD/v5Ah08+0aDTSXFAKZCsiXP29mvWxX4JO6j7+HJrhAN2CE1RsqV9e2BqA7DtCli/G1QETsO556l/4Y/4xR+5g/e95w0YYwjjGBWWiEbGCWoDrGCFL1TNXjM7vLpxMoTjn0Mc/iOENCuS38U+5xrOzOGsRQzVPYhJAFIipCKZy1l+6ixuue3hv847hV1nShvFrzxW4vBSQGgy1AeqjN5cgsyx2C7xzMeW+QESlsg5PpnxutoAV+/YyeiunVTWrCUc7EOUQ3/eJMdOTpIePMj0sRk+f+Acf7o4y7OjEVe9eozBcZiZ1CwvOowBnUNExPaJnWhtabSWUU6wtn+Ub7/pbbz1FXegpOeU3pdqUucIutdePFhtjScOKsyMuHAI20YXNvTqcRWgsaTWYvDUBv2BB/5IqWgmTX79s/+ZB0/fx2XrR/iZt11Koud5bmqGmU5O21hkrJhrOvrKikAETC9LlhJBIC2hMjSbjqlphzZw/+NTXH3JFm69bgdTZzrccNkITmYcP9ngwacXmOwIZgZLjE1noCV3H8i5fl1ARWX0DUcsTKfkmSWuBahQFtGP4makd0aFcQzEDVQ15e8PCP7hcUFuYoJQMRJmXNqfsrlqKQcBQagoRwFrAsNVzbP8yZTma9kGL4zd+PbyAjx5H5w8BJv3wMRGxPhWktEt/OnH7uSWl1/HxjXD6FwDGcn0OcIsI+of8kmZCxGAqwP7QsCBTyAO/WXRbDOgR/HqVj0k5xDGYBsJoq+GwENgHYZSv0Jds4bFx8/gFhOkhG5PF+c8ZdmGPji85AilIl8KsGGINgFJEjGUNVg62OK2y3dw+fvfQP3GW2DtGnAJ6AZ25hB2/hSukeHyHFG2xHtG2LSxwg/uHOZV+6f43SeO8w/3zPL8lgpSSqJAEQaSQApyDM+cegZMQdGgBKebx3jso48RRyXeeNNbCJymLiRta5AUxJrW5wq9re3N4tA5JsIQZy2zK1EOfHsABy2jSa0hY6XWsNs+QQjFPzz+KR4+/WXiMOTtN64D2eGeg6dpWItDoR3kxtLsSGYWYHHZkQlBngm0liSpoJNJjJNctamfHRNDLM61ecOtmwicZnZ5gWMLTdbsVVyR9XPygUWWwpBWNaTeytk/J/jCIcebdypCrRkci8gzh4q6QP5utyQAixIQVkMOdEr8j69aHj4NKElN5Wzv01w9YukLBXFcoa9WphxJlBQYJenvz9nczPhaYsEZ5MAI1Ppwk+dwWeJB94vTUO+DzZcidr6cA/d9hN/6k0/w6z/3o0hr0VoTIEjnZ9CtBmHfAEG1jlShj9uvbt4jFZy8B7n/zz0ih2CF1E90s6A+SoPxmlskCUSxJ3IuNLizmrAi6btsgsWHzyBy3UMEAEgsayoWJzxDdvOpFPuydUw+1WHj16f5xeGYG37hJynd+i4oV3FnnsTNthBjl0G8FnQVDj2Faiz6Wj5rvGMmJLKzyHbR4td2jnLDoVn+84EGU9v7vG7RBhUKyhHEFY9p1trSTB3aQqec8MmHPsVrX/ZaQikZVIIZUzjyrEZ0F4UnDuoqIJYS7SzDUbQS5QBfiZ3hmWza1lItvMzuUrXUnuezT36K1Gi2DVe5clvMVHOJjs0RMgAnC6IZyVxTMdWEuBSQLDsWGh47HYQSieV9t+3h9petJ80zHtx3goPnTlCthDx28AyqEnP8jOHYMw3UbIcwE6SZpeQcYQCfetoxUo65cY3DmJyoJHqt93rVKdIRBpK2i/jMfvirJxMmkwAVCEajlGsnJBv7FbU4pr9WohypFXC5EAgnqPeX2TmuELPgUD6mvnUvom8CsTDnmfCTFjSXcc/cj+4bJBrZyt/f+zBbN/0dP/7+d2LznFQbVBjiXAeTZcilRVSpgiqVCEolZBgV2b8EceQfIE/AxWA1PYYZIXwSVCnQHe8FC98117XaEPWtcirBaUtcV8Sb+kkPztGtsiuY4BiODJLQ0+WeVTz/qYzbjs3xC9tLjL73dszIWvRnfxcxdQw5MY6445ehvglBAnEdW1kDSws921wUSS07MYA7vYCYXeQ9E3XiM8v8xLMLZDv7eONrQ67coZgYiqhXAq+tc5iaczxzoMO9T7V59MRDHJs+wo61uxlwlq2h4lSeY4UsOg67roUGQCRXSCQldjWWw5vSvdCd85nALj0qMuTgucMcmzmBsAGvunyEakUzNZ2TaFBRV9NDEDjWjCnK9YBMS0ZHFXNL0M6g1c65aeda3nTTFp45eYLHjh6nnXdotTTbN48gpOHwvhbm4SXer1P2bIkZrAdkRnLXAwlnG9CRig8/bNBXx9y4ToBdaZ4uhUBGAW0T8PCk4H8+rXn8jIFQUQ4tm+qW68YFw1XBYH+ZgWqE6nIgFzEzIQVRJWJssMz1Emr7LQ0dQtIiuG4XdmYR+/hRqI0glmehOYdLG9jmIomsYPs28Vsf+SxpqvnR991BHCo6nQ4qCAjCkMBaTJbilvFlW1GEissou0y0NAlaeFu9eDI9k0QCoYSWT6VTtNdD57gkRcRR7ys4h7OG6kSZ9JjC5fY8Tu9q4OvvrFPIRHLrs3P8m4phaNMA+vNfhYU7UQbcujG44jqoDPWCB3ryAG72LMoIkkcPESwuIkuhB4L1VbBDNc49Nk1/xXHHhgFOHZ7j1/bNM/CmfiY2g8kjll1AaCRhIFm/TrJrY4nLdozwob+b4ZP3/y3/4Z3/D1Iqxgpb+WSe+1IyPIm6K+SyZS0V4VdVZwsG/57ZUfzuIpxWfEUBSI5MH6VtUoKwxELbcWw25WyzRRgBwpfxCycJhaQeOaIQOkaSaUetJmkkgk6rxM1XrePAuXPc89RROlkKwtBsZBw8Ok2aBGz5+jz/eU8/225+JYzXIZ2C5hSbh2f40CeWmc8FDRHwOw9pntoScvOWiOE6WCOYb0sOzTgePq7ZP6VJgbAUEqucK4Ys2+uGUqRYN1yjrxL4+G6hmqXyE7PWX6ZULaMEXDLu2DqY8dRMgDQ54UgfZtdW7Ok5zKTF1gah0ofIOoj2Iq6zRC5CjNZ86M/v5Pkjp/kPP/49bF0/RqfjYZ1BoAijiDAIcK4Q7sYyceMAUWvJC6ru2uys2ix0LPrwPCLJoBoiIokIJajcV7PYwvF0/pkEoUBVA/RMigokPpPgcRMCvxIF1nKTtKythnQOLhBmBqlAj/ehdm6G5jTuqU9ix3fg5o7jDn4N1V7AIVDDfYjT55BTSyACcCADRVCGp08sct3YIO/fuZ5nHj3Cn/1xg3xkgI3rjbefrSSWilgqKsoxMqp48yuG+OinP8xI3M/3v+lHkUIyFEgypziV5YUF4HxJII6OsZzVGVUpqCI8XZtYpQV8rZZnk7Rdq6X4LNUZ1gmsDPn7R5d45mzAujHHupEStarPSqUpLC9DM8PXfAmDCgNKgWA5MczPGM7OL/PV5w5xYm4JDNjMe8CT0zlbT7f55Zu2s+GdP4xpzWHv+ROyw6ewiWU9gu+7JuYvH8842YlAhtx50HDPYUNf5CNozUST5L6uUUUBOggYjjJuGdSMBTlOKjaNVxisrzDFx2VLtU9SqkAYKWwQkjv/+UTJceMGzVPTGptBcOAwcstGxHV7SR/Yh1kE025BXMHFZagP4ZIWJohwLufzDzzN/sO/zL9639t482tvIgohS1OyPCcMQoLQt28WYYhYnMJNzyGqJYhD3we6J8sWpMM2M+xcxz+ujgXhcNJBJSXaGX1jWl1YVBTQyVNfjS4gFL60yTiBcg5lHb4tt8Mta7Q0yI3DBNfvRizOQSlCHPgibv8XEEZ7oTHgjCYcG8Reuxd79DTi1EIRVbDUIkkjNRw7Osfu11/Nd56Y5qtnGtx7j+XqGyQj/ZaJYYdSrphUEXNtx/p1kt07JL/197+GNfBDd/woOMdQoJjNDYkrchqIgp7XS+my0XRcAR/t6mjjHNrZlb9X4yawrB3cQIB/UImWPHM858AZS1/VUSs71o1Itq0VrB0NODbpmG5bstyRmxwrLEsdeOZAwtmZZ1AVQbokSaY8HFTkUO/kfMfejWz4jp/HjexEHPt75PbdZE2B2n+AsCTYGWvev0vxd0cMTy2WPI8FAYuJI7D+RmVVMlkrc6qvToeIH+UEl0Y5ixomRktsGAoxzlcw14YC+icUKvRLso37MLlGGYd1ikgYbt9u+ZMnE3IXY556mviVNyG3b8CdXSQ9U4K5KUxj2cuQjKAcQFQlK1WJZw9yemGen/vNP+eLX32YH/rON3Hpzi0IIciyjFxnRFFIvHwGsf/LsNSGZsfbzXEE1TJEocdQW4c51/BovCjoZRWxDtvKsO0UWQ5XRUMcQnkKYt/FoOizHgiauYczREUSI1Z4yGuusTvWom69BnH/A4CGkSFQILrAqwI6AArx/HHU2Wnsrk2YWgWx/xzSWCIBUaCYOrnI1tRwyZY1XPbIIk/MRshwiH3PzPJ8rtmyWbBlE6iKwVlBZnN27Kxw+EyDP/zCH7J726XccsWtBEZTV4rEWP+MnXf4u5tDkDpDYPFCnDtL6nyW0DmvbTNrMc5X/BpruGrzlVwyuIunpx8nlBHSKfJUMpc5phcsR845Dp/O+eCbhtmz3rJwrIkohZ42zAg6CxlGBZybtqQzCQOzCXuwXLmhxmXb4dIhy95bvw1XG8WeuBMx8yyyIum74RKyqiY8e4y4GrJ7neC7ao5LJi1fW1CcWHJkNiYrBSyXA6b6qyzGZTIbErZzRqOAESSlqmL7+hJKWLQ2RP0BtfXSe+BRCVuqwY0/h5zbh3z242BAWsFNWxx7RzRPzlRIDh2kdPosZt06qpdvRjdaGMYQpRJuYR6ynK79YusTpHEZNXOIIG/wxSeO8uyzv8mtN1zGe97xRnbu2AwyIJp8lsqTf07cPOv5KKxDaAtpGxod7xjGAS4I0VNNAllEOs5zYi0uM1AKzhNonGe8staBxHcbkIIzTYV10jcFklAWDpGl5HvXUHrjzbh7vwQnp3H9JViaxw3UVzlkBhcIxIFzcHwGjEE+fhTWDGAu3YA+Oo1YTlFC0MwsC4dnGNi8lsu/fpD7T6Y447jt1YPQcpw9pXny8Q4bNyjWro1o65x6HYaGSpyZ0fztA5/ixj3fhpIhoRRIQ88clkUa1DiLdQZrDUHH+uxLjgdRe3Sbt6ZTZ8msoaI80V69UufHXv+T/NuP/BRz2QxhFCKED+qHUpFjEEqyYWSMShjy2ImjzGcWa6GZGqwTRG1J+0yHVyx2+KHLxrn+zTuZqM8iTuyHThN35E7M8jFE1vXkFVI54kEJswKSjDiUbN0coESbkXqJRynzqYWAaVlCxwojoaxzlIHAaQYDQ2QtG9bWqJQFJnMEKiceixCVEBeWQRiojsLQdsT45VhixLMfA2kZHQ15+xWGJz9v0M0W2cOPEr51LbZWpu/qS1i8/1myUg2xvs9HPJYWve0lFWRgRIANqkTOkmcz3Pulr/PEk89zyyuu551vvoUrJj9PqXEc46IiTV4IDt3GeA6Ra9rzLdpLmrgsiUJNEApkl/HKOc+C2O3a07UTncBkFhGBDL1KX+wIDi4qD5gvcsfK5uSDJeSNuzGfuZvg7BQ2CqGTEzx9EFepYsolDBK1foiwlcPjx/09Ss8QKo/PI+YT7NgoeT6HMW20kDRPLjC0fRNbXUAwZ3nsuYSlTodtYyGbd1TZJfqZntY8f7yFRhII58ncQ0crzwGJE5LE5YUgeyZCUYDLJOCsJcIRVJTyAu1896hcOhLr+/ZlRQq8ojx5SGZyrtx5Db/4Xf+ND336QxyefZ5cpgRxQBgHBEIw2h/T7FhOTrdYahs0kBlLbjSuJRCnNTedW+K/3LSBy//1e+DYXbgHHiNtWNotqA2dQbQEbmiUbsrWZgl5JlEuINQZtH073/Uv30PpoWPMTjbZdssw5aWEswebuKTmHVQriU3GgLIEgaJaDzy8U4KLBWqoAnFMr2NhuR8hfRdXuefdmKAf99RHCGjy9usjfveBhKm0Tvuhhxm46dswfYMEIwMMvOJKGk8dIpldxlb7PM/0UgO5MAWLk55MMM/ZMlbj9l2b+dLXn2ap0eKuu77M4w88zHfssrztkjJjJYMxHtfrcQoeFCacF+1205Aj0JkjyX1X3zAUPvcSCJQFkryA2AqcduhWRnM+YSkBl1h06phpBpxsBZ6vEEA6AmPIKzVOPfAoIl1mIg4Y0hbVhhnb4cgWx7lxQ6MuqYxkjM6kDOwNGZ0yDCxaygaEULiFNsHiWZpIEuNXj7STYqWliiLMHQtzmqmaJmnmHD2bUo1jylFEMxXMLpVoNyzNVGKTnOu2XksUxizlOS1jvJZ2Psbh4XFgjKEmFTUREATC91WOnKMsFbmzRMbSdobcCubznKEoJhA+JJbkOddc+jJ+Y8Pv8fSxJzlwbh9PHHmIfeeeIa5ITBTy1w+fopNqMgfGWNqpod2xTB0PGJjXfFc5Y/d7b8dOPYy57yFaDUGSQH1cIKRDHzoN9QWIYmRkEJWI8NIr0SOD5F96lKCTYa/Zi+7rZzRO2RyG1MYjdl1fZs1OzdSzGVOHOqQplG1OObAgJYHy4B4pHK4/RpYjD0BSdR9HVV6YvY9sULvegK0MoJ/+I3aNzvKeKwUfelCjp6dJ7/0i8Tveic41YqhG5ZYrCBeaZNMLpEfOIdIWduoIZB2czlnXJ/ipO25g80iZ3VsmeOSpQzz+7BEmF5r8wUOSLx8wvG6b41XrLUNBis1WCkh9s1mJ00X8yfm4e2ocqWd/JC4HMN3GFl2mcBZnHMY4ZmZylpv+WKEU7FuOWcoDQuXIgRIwWYKzWzXzt/WTrx9n26NNXvbXJzm5rY8HXjPB9HqBDg1OGCw5bA4oX9VH/5JjcMqy5kCbS59MGV2wKGN8J7CCgsBIsIsdWk6QRyHtpmO5FRLanDRxzLrcp01ESNKBxXlHc67N6/a+ku981TtIrWE61xhX8Cji6EJ4pINqEFIpBDto5gmBVMQywAu3JJKOyPiHv5BlzGcZ43FMKCRWSNp5TqVc5YZLX841u69h/7Hn0doSmJDZectiBMYojDbkmaDZVjQbinResCnJ2bK1QjCq0Xc/xMIsNFswMOAboWazIIXFTjdhrSSohJjZNvaT98LmdbB2BBeG6P4y9pEnwEhGSg7mLXZLSHVCsGddP2v3phx8LGHpmTYdfEJHyKKTohKIQGJlHRnUCjyGAZvhTIpQtWLpzhEbbsRFZcTTH+aHbl7kY083mWpFtL78FeLLr8Lt2glpihWQjdSR68YIz5whe+p+XGcBtGGiCr/43lvYs7aPydkFxgbKvO3Wq7j+sq3c/8izPLHvKE8sSE4fHuR4aSNv2j3AyNAQMoxRgcDlLbLJI1h9BLV0jkD6ukXn8PTFkURVJDrLvWktHc5oOpljejHmdKsfEUc465hrJNx7UpMojVIh1kjMiODw+wapv7qPPHRY6UiHY/ZN9PPld43TGjCEWY7MvPmjinLVXObMDTmWxkocuXSU50YbvOPjc8QZPLesMQicEKhyTHpomlMOWiVFEEZMzhsaYUA1KiiItSDPDWluEQjS3BGGJUpxiYY2tIwlKFog2yLKESKoqYBICLT2vXWCyXQJ56AsI4biKpUgRuG5oiMpiYRkOUvpC3yKMZYhxkFuNEJIntr/DF977iHCgYgst5ybznynKe3IU0Oe+pS3cJKqNVRD35CHycOk0wnNZW/uxXXI5sElDqsEeVSlvGaC5NHjmHkNShA1zqJetpG0OoJ9ZD8yFyQzGolCLGYgy4ShoNlsMbA55tLRgFObFHcdkIw5xbjyGGFhDa4ygFh7A7RmIWsCBkyGyBYg6veWqzVADuNXove+n13hZ3jfzaf5lX+YwTpofvxv6fuJD2IGB7C5JpAK+9Cj5J//LK61DMYwUbP86ve/kmu2jdFIcibWr6OxuEBzeYmRvoh3vulVvOEd38HQxp3s2LWb/uER4jhCFZnLbm5Atxvks2dYfuROki99mFrjqI90KEEQ+nS2cz4x3G5qFktbCK5/O0M7bmBieB2qUsFpy8njp2g9+AiPfOUrHHtuH0jDZd8/Ru32iDRJcFoQBgpO5Tz38hFmBzIqaQZdLIjwdr2U3jRaXnLML7VZzNpMJBl5HnJkydJILVUESElVhSwcn+eZSJGFiqH+CFlyNBY6tFo+puysAesToeVKQKk/5hNfvYfXXfk6XnfTmwjyvJsGJhDQh6AqQmQBdRZFdjdQQpI5y4JOWMhT+oKYiXIfsVSECIaCkFhI2saiZFeDK0LnCFTI1PwMqcuI8CVH3U56QkiiUkgUC5yVCCMoLbTJooBjS4YrjpwlSRRZx+PlXS4wy14DZDnIveN0np5Hnkxw5QAhLHlHkt1/hmTuBHkUMHfOkmvFVDlG1XL0bA7VhKSTIIWmVJVsvSLk0HCNX3piid9wsAMLykK15OPGaQi578pFnsDCQVx9Mzjb6+qFybBje7GdRX7kjv186cD9PHwkIzl9Cj78EWrvvgM1NIj+4v107r4bm+eQt9kyGvLL738112wZZqmdI6OYMAwoV9bQNzRIO5PsuPH1rNu2p1hANa7oUgDa88AJv7iGlQrR5j1UN19K4/KXM/9776OvfRQRRj2Md55o5pcD5E3vY/0dP0ltw04fBOnl1WB0516uuf31NJcWuOt/fpbPP/R7TNzUoNPOwSpv3iSGo6Oa6bUOkea+DhA/HrKAQkzNGk6dLTA/JYUqR2yYdZydSjjcyqkgscLSFwfIpZQjyx0Oro2IKopKCaKaoF6q0OnkpIlGiAClREFo772aRZnxlace4s0vfyMl5eU0BvqkpEKxiOLJcigw4MFEaYDE5CTWkFpHx+ScS5qsq/QRFs2Fq0qiC7I+H5iXPZalS9ZupS/oo91egkAShAFxFBCXQlSgcHggktGSYDZHB232tQS3PnfO99azYDqOxmnnGVpzaLYc9dI87sg8olTBMIqqj6GG1qPqawjWbcSU+xmOalCpMlGrs71SIqs7lu0Mx+cPcf+he2mrUygJm8djnt5a42eOJPzcaIjpSKqtDtttkzAs4ZKmNzmcw009jVt3a4GML4gOi0JXM3EVtWaHX36/4Hv+2z2c6yiSI8fI//vvIiohZnIah4KszS17R/n3772FjcNlltopIowJgqCHMSrX+9l5zesYGNuEtekKNBSPN+8h7wpkGTbHmQQc9O38NhZu/mGOfvjfU6p7M9DlhkY7ZOQd/5at3/czyDAE3Sls0y7eAazVANTqFd71vvfyqrdcxT+c/BnO6sNEooRwguVEszBh0C6HXPqpoBw6l8zPWk6ezZie14yNlajUI5CSKA1RX13iwJwmKElPQSYEY+UqjaWUJ8sBZ+sBlUFJXIZAOFQZ+uohaTtkbsYXJzsDncTQamTIjmHL6HoEgooQVIWkLnwyyFp7Hl7LFzVIhM47zgcTVjhIjXOepETIVR21zsvBkjlLYg2BVNz/9IP83dc/zb4z+5hrTpG5lLAkkSUBUYiTMdZGxHOaiefPsmFmgR9eo7ns6hLnnm3QWfTylLoAXeqntGk3w1deRbT+EuL1OwhHNxD0DSErFUQQeCAUXfIFgC6keyWzNrV0ij9++Bc40PoKsQyxRvG1ry0RNjWjG8rEoeHawXHev30vm5WAThvncpwRuKt+HDe0HazPjEJRXCsFnflpklPPce8jx/jgb9/Lsil7w05nkKcMx5oPvOUG3vHqvYTSkKYZUgWEUhX0BmCdoTy8ns3XvaFHJuR7DEInnWW+fYzl5CzGJMRBnYHKRoaqOwhkiE9XhiwdfoZHfuj1BOkiIgpIk4yht/4I1/z0rxYcel7rCRlgTEpmfHYxDEooEeJsXpTFlTk8/SU+ceSnCWNNmjqmGilWeV7nUAgUMDVlOXw8o5F4Epo1w4qJiYiODhBBxNBXWlz2l3MMCFVUmltGqyWGnGS24/jvIyEPr1OsubRCba0qyqVA5ZbJAy2WZvPimUoUISOVId71yrfyE+/9EeJyhdQapPO4DlFEgHw1uK8HNUXPcJHnHbcCzusC9MRKGQ+rcIdduS5CnNpZtHOEQUimNfPtRWYWpzmzOMW5pWmOTB3m6bPPcWj+FK08pWQDNh09x9Dhc9y0rHnr5QG2fyPp4F7qu19GdcelVNZvpjS+DlmpgQhw+Niqs9pnvkQX1+uK5bSrxYqF1WkQgkDFHJl+kt+471/RDOaJg5hWQ/C1L80wvrFG/5ii08kYMgHvXLORO8bHqViLSxPc6DWYK77XO12OFVITHEjFwtHnCBozfPbrh/l/P/oVzi5mDFTL3LJnLd9525Xs3DRIJ2ljrUUJS0X6vuPOCZyQaGcY2HoV43tuxlqNFIpGOskTJ/6YE3MP0MhOYlzHJ7icQooKa/pv5jV7fpG+eAgnHFljjn2/+AE6x54nWZwlr6/hxt/9B/o3bMbqFCECjDM8d/pODk5+juXkNE4I+sob2bP2LeyceFXRWdfhnOKvnvxJDif3MteE1IJUglAGNOcd+w+1mVu2BLEgiCQ6s4yUJOvWxzSCgNoJ2P1H82xc8OSzTkAlkvRLhc0cd0bwsXFFZWdMfX1AezlDZSEDQR/japTxvnWsHV/L2rG1rBkfY3RojI1r1rNufC3WQWo1zvl+NBLn2Sqct+FNUeCsi6LnQHYhiN7w7TkhheIruoAW73VlpwfdU0TOYYwmkoaJWh/jfQNcsn4bi5lhMc050+nw1ORh7t/3FQ4f30ez0mJ4WBJdfwPi9jew8YabCYfGUFGpmDA5zhlfNFnwTkgRFTS99ATXOY11OdZqrNFYZwlViUBGGDTa5mwc3sUbd7+RLxy5i8lsgcpAhb1XDHH00DL9I3XismRWp/zWyf08NjfHT27bxoYghpl9iPkjmKHt4LrFmMVS5Sy1NRtYmD/HW2/cxjU713J2rk1fpcJILcbkKc1m00/0cpm+0THkzDGCThMnA4wMCGxOILsdx7x2Pnjubh4/8XvEcR0hHIFQOKdwTmDpcHDyTnaPvY09618P5CBDBi+7jOEdm1k+dRK16Vrq67dgdQootMu495n/xrNTH0UpgxQBFsFsuo+Ds1/ixuUf45U7f8BDg1XEtsFX8IUn7kQHkigK0YnkyOGUoycTXElSrkqkAK0tOod5B3YJVDtj5yeW2TALKu62voZWkjOrU54LFXfXy6zfvpndu7awYXQDl4xvY8+mXWwYX8NA3yD1Wg25ivXV4ftTWuMz1qWiOFYWH3rFRaHgullDsVL17au5ezLs7ZJiecwddJyjYSwtrWkZQ8dacud6FKSxlFSUpKYUfYElFAJTUB4syBL1tVdyy/jl3N5eZNPiWXbV+9my7RICFRca2Dfz8bZeUGiXnMwktLN5ljszLHXO0EwnaeXztLMlEr1MlndIdYfcpOQmJ2KUN1/+71k3tBPjNA6FcGXee91befjks3z1zFOMbI1JRI3lxZRLN27m+MJpdCnjvvYyi88f4T9s3colocU9/1l42QewKi7SzLIn1KpUJR5ew/LZY/RFgr41ZZI0o9NqkxuHjGMGRsboHx0nKlfJRI49+hQhmTfVk0WC1pwvmfL+DKVwCGNCcuNP16sDLyaTtsqnr7uV0dbSTBOkseh6H2NXXN9jf1Iq5pkzd/LEuY8SxhLjIt+2GYEgwqmMrx37Ky5d93rG+jYAjmq0jlZHEfdBa1Gx/5kW8w2DqobEZS9N1jpUEBINlxFRgDnS5sq7F9lz2HevXe5YFiQ0KxXiDRsZuuJKrnvZ1bzzqsvYumUro4OjRGGMBFJ8+5KmNSwbg8ASIahIR1zgS6Q8vwkrDs8KVQD8V5Sr6MZgPIO/KOaVcL4qJXeOhVxzOs+ZynIWckPL+lR42zoyh29aYyyJhdz4lLnAd/6sKkU5kEQqIAXqMueySsT169cwsm2D17LW4GyKkBGIiNQss5icZqF1jNnGEWbah5hvnWKpc4aWXsR2l+BuObMTWCdwRX9r4wSNzlNsOv0y1g/v9uE5Ac/PHOHLJ5/klVuv5w3xddx/6jkGRy0LzYRS2s+1A/184cyjxFHAfpPyyweO8sGNY1xjDqCfuwtx6Vsxsls2VSxlQhL3D6Pby6RJQpok2DAmKlWpV+tU+wcIS2WcNmTNBWS1H1cqIZcnPV5bJ4jFMziTF/JpqUSjaBPjtH+ExnWtHE8fkeuYanmsp3JMnpFnGcLmJGmKKtcLjSUxVvP81L04KdC63Cv3764IOSGdpEkzWWKsfxMAS+0mmZEkU4r9TzdpO0FUDiknjjgTUJaYyBJbRe1Qh9FDy+x4vsGaczmzpX7Mpi2U9+5l07VXseHqaxi55BLCkVFMGJFZx0lteC7VpO02BkdJSgYDwZASDEhBVQjiIqWdO0fHOhra0uoqT3xNocQQArGwREJSKlIsXeu4YE7ywmyFYDbLOJ50mDGWhoXU+vKYAsdGKAprW/lTIBxSQEsbUuOYN4I002ROYEzCLbWQ92wYYmPZL0fG5EjpKzQ66SyTSwc5OvdVjs09yHz7OJlexrjMYwyE8maQDICSD2E53ybDWk+v4ArUnCn8gMxlWGcQQtButzl+bJ7WUJOPP3cvE5V+LhkZQk0vMhd3mFqY4rtv/xHK8QhfOHofmc05oB2/+PQx/vWujbzSfBGNg22vwgYVkAonJWQZWXMJFZeoVmpUiiiID0hYnG6TNzOQoU8HiwAxsAHTWIa8A60UOX0Sl7URpYrHzsiQ3AQY4+/LFiunK8A3StSoxsPenxACnSXYPEE6g00zZBD1TMbcaRYbsxgbYEVQaHxThLl8VT+6n3o86pGUQtDIllmeVxw72CI1IGxO36mE68+G9OUxnVIZEQeUlpv0n1ugnGrU5p2Ib38jW299DSOXXU44OEJHKM6kmkcSzeK0wdKiLB31wNGvYF0kmYgU66OAQenT+qnzFLknc81UmjGd5STFKiUEVAMoKygJD6IqC0tZ+iabVRlQEx7XLlwBHxXOs6SdSjqczjPaVmDwOfOAgvfCrQL9u4KGa1Xkw3dR9fFOh0DpnHeNlPnu9YOUpcM4b8dJKZhaOsS+03dxaPoeZtuHyW2zIIAJfCiPUuGQFaTWRtJNeAonis9kIdB+SXZSonXERN8lPtgoYHJuknu++Dz9GxM2765xsLXA8yfmiPI6pWYfZ5cW+NPPfZLvuf37ueGSV/Opr32KB579GvPtnF88/hziys1cN30n7uRxGL0EG9ZwUQUtlYfZSgUm91NJZz41Wx6k1LeGUn0cFVWQQYhAwpaX4a56KzZtYxan0DMnvG3R81okmZEI7XrC3L2/3FoGgyFKUR3nDEIq0qVZkplpbJ6i52bQM6dh9zU4HJGKGatcyrH5RyiV8WFT53qmzNKS5pqJWxiqT2CtRqmAfaeP8vyhBWxbMhYO8rJ113DrTVezd80O+kYniMpVJALdabF0/ATznQ5nr76ZhbGtnLFwNLe05zSJzUkK5TIYwLayYFdFsiH2iTrjHEvacTzR7DOWRsGA1NCGjta96u5YCcpKEAvvDEbCg5JkAcHw7EIeMloSPgGIKLjtcuc4m3aYzHyZiyw0tioov0yhAXGuOCG91z0Xslu6JQTaGm7vj3jfhgHfAcBJlIpY6pzjwcMf47GTn6Kdn0UpkFJhRdknU60X4m68pcCZ0a2Y8eEz4dnci9+2sEFzk9EXbGHryOUYm6NUyOMHnmJmeZHFgxLmq9x89S28/PJvY/P4TvrLAyw1l/j8g3fxy7/7m+zdtIf1tV1cXQvZcdkO9m7ew9qxMcRgDalzZFjCZAnp0gymtYiTFqIY5yy5MUS1MYbW7Kbcv8ZrS+e88PV8E4cIKshKlXBoArZegTO5D5/JAIckNRIhLdZ5B6erodMc1pYmKEc1T12AI+4bZu2t303aWKSx/2nmjx1n9KaVsq2X73gvB848xqnFfYiikaTOHXkC60rX8earf8Cj1IRgZnmO+772MLurl/Ptt7yF11x9G9s3XUIQlehVu9N192DjNdcCihlteXYx45GWZd54GG6sBJsjx96qYntZEQCzmebJJc2hjmEqtzStIZCOuCBVV8K3vlZCoISHggdSeF5rKQglREr0iIK6pnO32koKT9MgEL7x5kKesag1SgiCAnknnfMBbLqxP/9ojLOk1tHDzhQDb4obNg7GpeUdawcIBb4qQkWcnH2W//n4z3Om9TQqUAhVQTuHy23B7VtgxwvHywkfYzRGY7TDWIEzAdaEKBGjZIxEIVWAEILR0gTvuu7HGamvxZgMLOzaeAk/cNv7UMB73/w9XLJhM0EYr5ouiit3XMNt176OI0cOc/VlV7Fh/UaiqES3I1b3X9dOK5ucvLnA0pnnac0cRUhB36ar6F+zC6kC36nW+PS/FNIfR6zqPlVw6SG6bRQKr906Uu09fd3Tzg4nBEkmqMZjhCrEao/XqE9soX/dzsKe7pAuL2FMVsSgc8YGNvKB1/whDx78R56ffJhGZ55aZYjLtt/ADTtfS391yJt/SjG7MMf7X/E+br36FQwOjgA5VmeYvOHvuxsq7f3zSmYYxysHFXtrks9Op8xowbWDISORZCqxfPRkiwMty5JxICVSQhQ4KkrQL7wGC4Rv9xbifTMvzD6iAhblZC9M242DeNPTM/tXpSQSaoVaN8la7lTSoWH9DqnzCK4cDyNNrSfZS52gaSx1KRkMAoyD40nGOe15o0PhmM4tC7nl1nrIj28ew1mDlAHnFo/yZ/f/BAvZUVQYegbKQsO7YinszjhXcJ1qDWQl+uJR1vRtY83AZoZq6xisjtNfHiEOK74RjvQs9dW4j0pcxVjTCysGKoTC3lZKYk0BvvcWV0+wlQrx7ohmpavSSgV7lwTGV2m44j1Lc/o0OEdtfDMOz/AjhESqCGcNy+kCC51ZWmkTh6W/PMhoZQ2lqIaz2l8rfpU6MPkEv/X590NYtGBwnpoNIWi3O9xx6Y/wHTf+FEYnxZV3Q1wrUYCe01wsC1IppFDkOkWbnECGhCpEW421pjB2BAhJGMZgTZFJdD6MJhXGalrpMq18GYmgEvZRjfuQUqGLcKkUjo5TfGEq4/NTOSdSWNBghKIUSCqBo6Q8K2opgHroGAktfQpGY8GaKERby9Ek6wlzn4QbahVKUnKk0yYRlpIQhMISC0fgLLGDsahEhERbD/IPinDe6uhzocK9syelRDpwxrG5FHFzf52+AoY5med8cmaJc7llU6RQIic1mu3lyCfP8A7dwamvM9k4SFyqkOWup8mtE1gri2xc19yQaJuzprqX7731ZxntW0ut1E+ogtUZjgLz63BYECt8FEoo7+k7izYF9x3++gMRFsF4Q6Ai3xwSh9apN6WkRMkQIUKM1TTzJplOEEJRDquUgjLgMDbHWkdttGAktRkIb1a1sgZPHP0SD5/6MscWnqeVL3q4pXSUVJmxykZu2HAbr9j+RqpRH9p6BzjXOe1c9Xjaut4IQpCbkOH6ht6SKGVQRHasz+YiyHTWK5XrRmSEcB6NJwM/foXZ2M0W2SI9DY48T3ydXgFpOLV4gqdOP8Tz5x7m7NJh2rqBFIpaOMiGwV1cv/nVXLHxJkIVkduUsnTcPFri7nMZU3lArKAsoSQtA8rSHwrqoaM/8g5eWTniwrE72UlpGUOgPNWcADbFIVvKfry1sxxO28U09hEuiWMgjChJhSl6HwonPB46lpKm1oVQFQIjRJFw8ctepATX99UZCnz1CsKxrhRyVb3E4lKHs7kmwdeoRVIW9rQ/VCxr5KnD0SHTPmqiwggrA5yVvQ5HDl81nlpLNVrLtvHLEM4UQtdFfK1s3X4CSkYkeUIrbdHOGzjnGK6OU4kqHo+AI1ARB87s59NPfIr51jw/fMuPsHlsi0/MOIeSfiKcnj/Gw4e/wvPTTzCXTpGYDgJFLRpg+8hl3Lb7LWwZ3o61KcaaIkkpCIOQZ08/zUcf/H0OLT+DVilBIAiUJCgeUm4bLDWe4dAz+3j6zGN84OafpR7XEECSZ7RzQSAlxnqBc9ILpaDKYG0cBORYHj/xKF8//iAzrTmqUY2rN1zHTdtvIg5CtPEUQUpJTsyd4L4DX2C6OQ0CBktD7Fl3GVdvvI6SirHF2HZHUwURk4vn+PSjf8FDJ7/Igp5EKkcYKFQxIebzRY4sH+ErR+/hsrFb+MCr/g0j9VG0yRmOBe/dXOL4gTbj1YhNNclYLKkH0KXMF8JRUpJ6KBiKFHUpeHSpRcd5jo2wAGeEqzgXykqhir8lHhM9HMT0B54xqVucIwQebdcfRSxqXeTlugFrH84JhHcItXNMZRkTYcXXtAFtbZjPMwYULFhHbg2ZdUznumdOaJuzZ/0tXDbx7ZxZOsSase1sGN3BV4/cyZnWCaQM6GaVnfPLX5pL4qC/iKLY4mK7iYHu8HshMc7x6cf+iodPfoX55jzttIl1hjV9G/mBm3+C3WsvBQF3PXknf3z/HzBrl0gzzY6Dl/L949sARxhETC2d5s4n/4YHjt7DfDoNkUApX2JmLejmCZ6dfoL7jnyet1/+vbzp8rcDvjI+CiIePPIgv3XPf2XJzRPHIcKVscaQaI1OEwIpIbDElZgoCnh0+kEuPfwF3nj523E4WnlCK3PESvWUqGcxcFRlH2N96zkxf4o//OLv8fUTD5C6FBl4cNddT9/Jyzfdwk+94d8wUK7jcLSzDv/97l9h3+JTvnGP80SNwbMRr9r2ev71q/8NsQowVuOKMfj6oa/yka98iDPto0SlEBWUEdKR2RyTJSipkCoGEZJLx1dO3E37cxk/+7b/QjmMfd3pYMhNg44ZoJlb5lLTw1lI4WlFSoGgFkjKoSCS3l+KA4nCO4MSQct6+t9AwJLxVMShEpQQjAQBw0FQmIeFASa8/AQOR02FjEQRZ9O0F9kICqolJ300wQjY12rT1IaxyM/W40mHJWMZDIRvCSdAEfFco83SaD/1wPdpqVUG+P7X/hfSPKFWqrOULHLns5+jk4cExRLX9ViFEKS5ohTVEVLizGp7qNt3vPcn2mnuP/xFnp17nEiV/T4Opqae4MP3/Q/+67s+xD1P38N/u+dXoCQJgxKpTDgydwLnDEoFPHbsa3z4/l/lXPswQVQiLJdxAnKte3FziFFSsJgt8/tf+e9gFW+55h1Ypzkxf5rf/uKHmLXLhLJEkvoVzGEYYIS37n4jGwbXcWLhKPcf+wLznUlUIDmzcAKc9DZontLRAmFUEe3xKsdYw3jfCDONBX7rc7/NgYXnqJQrBKLqh0UKnLDc/fw9jFXX8GNv+BGEs+TWMJ80kKqEEoEfNyUwwvLIsUdZbC0w0T8GAkIV8uXnvsCH7vmvJKpBFFYwtiieNhlD4TiXr7+SNG/z6JlH0AX+Q4VVvn7qAR469DVec+lryG1GWUk2VkIemLSEUuGEo4SjHHhBDoQH/TshSIxvDFQNvb8SCEGIr6ppOcdz7Q5VJZnJNZEK6JOwJgyoy65fUziJhdzIbmJFAGNxCescZ5O0YJ3ydgqWotUXZAKOJAmHEs/bEQmJKtLcsYDhMCSWAceX23zq3BzftX6ESDiszZFSUI0rIBzzjTlmlhvkQYQxciWS4ARCQqIFcVBB0I1qC4T0V4TwHq8xHmMRqxITfdt4YvI5hIt7nQWEDDg4e5y7nribP/zSh0mVILIBeV5wUgiBkDFfP/gFfv0LP0+TRUpBndzga/ESy+6RKxiujfHoqcdZsk2UUFgEmXD82QMf5bptN7NucC2fefIfONY4Q7XrIxTp2TTJ+eCrfpA7rnkn4B3S2/d+O79336/z6JkHmLhsY494sJkmdDJQofLh6SJVbQxAmd/9/B/w7OwBKqUaWq9gHvypJLJU4e799/H2G97O+uEJAhliTUwrkYX97FdcIywiLKNU0NPMjx9/it/4x1+jE7SJRIlM+zZ3mdVsrO/mZ1/3C2wfv4Tc5PzK536Zuw9+jiBQOCHIlOHh44/y6ktv81cjYCwKCXSODGBtyXFpLeBUx7DgfFiuG/aVUvRanijhtbXE0/pJIZjMNSUtqAeSISUYCRQV2U2qdSNPXYtCrMShu7bJRKlMKBVnk4SGtSiEt/9cseRb56nVigCwdY7EOZrGsZA75rWmrR2pC7hrPmU+m+H1Y31srYZesAUESGaa8yylGUqEBSG3W2W7O5IM+spDIAKkDEl0RqvdYKnToNVpMVIfZmJgDGN92Y2SNTppgHU+RW2LAcuc5n989c+YTZYIKyHagsWSJJrNg1s4PnOE3/r8rzNv20RhlU4uMDiSVs53XfE9vO+WH6AcV/nrr32M3/zShyhXFNYKrFCcS5c5OnWC0fooT515Hicj0txnLrv2fZYFBKoCWHKTAo6NI5v4d6/7eR44+AA3X/IKrMmRStJMUpI8INIh1lq6HXOsi9g/eYokywjjMtrSe4A9cJMDJxVnGnM8c+YA64fXed4PHdDJQwxBjykpczmVyhCVuAZC0kg6/P4X/4g526BMTOZdKR/fdxV+4OU/xvaJnZi8QxiW2DK8m05yD3Ep8G1CTMhMYxFtbS/p5ozDZI5ION40UWZXn+LvzyXMNfWKVhWiYBWlaEHXzX94vyEE6lIyGipGw4CqLOhzCyPUdQ3Qwjz20V5BsBIu9wHq0TimGgRMpikzucZYW4SR6NHUCnxTx8xacgu5Be18n2aERElHEMZ8pal5cHGa7xiv8a4NQ2irfXRkcZamdpR0F+lXRCsKayK3Ec9NHeXP7v8oR+eOcnL+DPOtBZp5m3aeMBD08Ytv/Y9cu/UKP9FcSKJ9mGm1SWItLOcLKKVIE09kUo9q7Brezev3voaP3PdhTrXniEpVOrlf4ttpwnVrbuJ9r/whojAANFvHd2BshVZOd93CGEumDZnOmG92yG2MozAX8OOV4fjzr/wdl63dy+ax9Ribok3GQGWAt1z1VozJsc4gUXQyTW4jUu3pwVxBmetZCSwOiWw7anGFlvBRFWeLsFsh/Im1nJg9R6EDyYwkMwFCeJy4EILEWPpKQ8RBhBQBn3/6Czx65ilKlTJpXvgowhO+3LDpGq7ZfDVGp763DI7Ts9N0coUrWv9l2qKoFNwtGodkMbdoDVoJ/ufJlL7YMWcsceBDcqIbcCiE0BXUXrGDfiUYDRUjkaI/CCjJFV+p7SS5McSCnkkMbqXbsvP0YKtA/H58qkqxpVJh1FimspTJJGMxN0VnT5/FMg609QcNhKCq8PQHxQTwvIKSJaGYzArS7gKxNjk/Q47yYHptfR86IT1TLA5LzN89ebd/kNK3AAuU97RFSXGiM8ud++7j2q1XFg8O37LZqq4PuUrNBLTaHdbX1/G+l7+bb7vkWtYNruXJ08/y+QMPIEt1Mu0H119lhe/6tu+iHJVITYJSZfafPUYjt5RC3/zH4pC5pK9cJwoiquEA7eVzxKKbIfQE3FDi2bkTfPBP/zM/fNt389orbyJWAbnJC11TJAkQJJkhsxGZDbFdI9oB1qGtpWJifuYNH+SyjXv4pX/8HR49d4BQhCs5IgG5syx0WoAHbqVaktoIjDc5pBQkuWWgMkKoIpK8w2ef/CKZjFDGH0vi88tpbrlp+02UghJZ3iEKYg6cO8qdz96PljGJVggh6HQy1g1sQAmJFmBQnG4adOHLTKewYATVSPmWJw6U82HbmpCMh4KxWLE2VIxGij4liWUxT52n0mgZQdN6k6RfejxRr5txEWIWQuJE0XhzRZp7KxhCCOpSUC2XGY8i5nLNuU7OnNYsa0tindfMOJz1qceKUmgn0EU5U5cQpB4oulk34yyn56fJrWIgCgil5vRyigh8kgRRJD1U4Ku0u1UeCJwVSCRCRGS56ZkpSW5IjULqwlYsbkI4SZJ22FLbwm9/7y+ye812HD69/8lH/5ElY6maqAeky6xmx/BWLl+3h9xqYlXi9MIUH3/08+ggJrO+uVCmc3ZW1rFlbANxEHL12su57/hzuGqAtEWTJFd04wpiDran+Zn/+SHufuZr/OArv53LN1/i7cAiYwiOTq7RhOSu6MniVvJy7XaH9970Dt5141sQSF6z81U8cPwwRIFvDlSsspmVtIvwq3WOzAgyqxCiaDviBLnRDNaGAcn08jxH5qawlEm1Wkk3GYhlld1rdgGCKCxxaOoU/+mTv82x1gJRHGKNDx5EeYWX77wBU2CJl3PL/sUU60pgLRXlGFAwEklGypLRkmSiHLA2VgxGkv7AC/Dq6FVqHW0NDSdoGi9DIwH0K3otBleKUvxr6R2nFX7o1QekUN8A0jnqSlJTMeNRSEMbFrRhJjfMZpq5XNOw1kdCrCCSvsixW+CpBAzFK+D8JEs5OTdLakLyHN59xSYOT87zzGSL0x2DRiKlBCmwvk7UA3Ws7WJ5ULngqg2XFqBuQzvLyU1IYAo20e5S5BzClPjJN36A3Wu2kuo2UkoW2y2eOn0CK8skuegJdKINa/vWUY/rGGd46tQRfvEzf8CTsycplUrkpkhVty3fd9s7Gar6xMi7rr+dOx97gOdbM8RxhChsYF9xIhBCYQLBZw4+wlcOPct7r38tP/K691AKu2EzSzvXaBuQG9UzNwCMddTiId5w1avRxjvXW0Y2IHRMpmQxH/y+uVVkxk+QTGuSzKGNrwztmhLGBIzWRgA4OXuWuSTBhL5tXHfsjDUM1vqpRTWOzpzizie/yl8+fDcnm1NEUQmtHVY48kbGe192B5dt3kGiU8CTpr5raw0pQvpCwUAs6QsF1UBSDkQRZ16xfQ2CtvFZ6baBxEHmfGlHLGE4gAFpC5Sn8MQ7PUu8m0ASPfD/KsJz/45wKwmRroQ7vFVWlYJyGDAYKNbEHvS/qC1LuWExt8zkhkVtqSmYxDuKylkGw6BwgAVLnSanluYxRJxtWj72+ByXjfcxUo45urhIJn0dm9CCiqowGPdTL9UYrvcz3jfIUL2fnRObuO3SG8htXkwSizYRuQlWEIAC8jzn2zZczit2X0umfT8VKSSzjWVmmgkZscegFCz5uXEsdgx/8cBd3H/wSb507Cmm9TKVctl3B8sNKrH82MvfzluufSV5kUrfMDLOf3nHB/nJP/tNDqfLRJUSwvrOB6KnTwQowaxz/OaXP8P+M2f55e/6V4z29WGAVmrIbUBufHy1awrmecqesa1sHl2PNoZAKEpBjDMRmRa9iAFAri3WesRDluckuUCbEOFkz9S2xlKPqwDMNZfpmAKe6xwUzJ4OxXRH88G/+i1OzEwx1V5ExBFRWEJbQ57lqJbhe699Iz/+5u/xySnr0+4VZXnthjKih7woxMj6OtRlDSnOC6/1dn9ie+sqgYC6coyH3p4ORFE7SDdEt5L8Ez1buZtk+waBXhHiVROgNye6RngoBJEQ1JVkJHCkUUCrAGM3jaVpLQsapjLDYhawtuw9d6UUs40lpltNjFBIAg4vWvbPLPmmi6qEtYZdA+v5hXf/IGsGRhis1iiFEaUoIg6C3lLj7VCPeegKgzJBIUT+0pNMc8Ml11ILS74PYuGMNDsJ7QxvO2J72smJMvce3Mdnn36cXEFYilFRiVY7QySG7fVxfvxN7+Dd3/YaXyZWDGwnT7hx92X8yQf+H37+Ex/hy5MHySNFFITevuvOsMIJolrn7w4+SfXv/pJffe+PEKrChjYBgfHC1Y0EpLlm2+hmqlGZXCf4tl8SYxW5KUA8PYHO6S66ida0c9AEOOeLna0AYRylsFRcd05mFdIGhQPqivi3I0kMU+0TvgdKqYzWmmbSITKCSwfW84Ovu4O33XQbCOuhBFL10IEdrbFAR/vmP4nzDX47xpIUePYe3UXxEwpHXcJoJBj0dCM+POt66qC3iW5lsVsd0vDje1GBXrGrV7xH14v1iRVZdxAJQSmAvsDXwOkC4JRZ/9tQZjhQaCwRkpnGEkudHBd7lkznAqSi108vzzNGa+O8Ys+VgB8sfymW1KTnzXqBxDpHM9HkJvSds7o9S4RDihK7126hd9vFCEohMVqRdSuxixCOcA6DwigFxqDbGX0yYvfwFt5880289bpb2DA8Vmh71wvoIyDVObu3buYjP/6z3Pno1/iD+/6Rx6dPIMsl7xtYP4ZWeEIUWa3xiace4o7rb+FVe6+is0qgxSqBNiZg2+h6z4FeNKvPtSHTklwFBc+b318bSSg9i3+qjSebFxFK+MJSi0NqiRL+sZeCMtaEvYqcLuWuH1sH2mE6KSUZsKbaxzXbL+H2y2/glr3XMDQwQJrnuEKYG60O5VKMUKIAuRnOpIK8gAMLUSApC59IOD/eZekYCBTDkaCmQOF5/cyqiFrvelZNgu7Whfe8qEBfKNyFfPRmhf+gG9xfKe0ReKLxUBTeYGG+5Naj6ASSmWaTZi4IgwJsVMRcukuzdSBlRG4tvkDV9uanWH1RxW/rLO3MkpuocH5sIf6WulSMVPu9OBdhJ+scQ9U6kYhJjfEpaVdoaDzH9I0Tu9g5spa967dw1eYd7FqziVqlTK5zOjrx/cCDyGsQuxJWybSmFAd8xytu47VXvYw//cJd/O59dzMpDEEQFhPY7ytRdETOQ4cP8Kq9V9PJLLlTaNN1zkRRIxgyMTCKEF3BELTzjMQotOnGl72poK3yVAf4xk2pVWjpJ6xxFgso0zUgYd3gGDFllnTB4NlLJYPRObdsupzvvu6VbBgcYdP4OCP1fqQSJDqnlXZAQCmMOHJmmp/40O/zb9/97bz62qvomJyyhHoI87k/rsew++ROWUJNCfoDST2UxMI/sSKosyo8caEkni/MXgmd7xy+pEB/w0GKzRfWrkQhALodqLpx2O6JVlBdML24RKIl2Kjw0M8/h1/Gu3HT7nFXFRIUVejd/3NraSQGbUOEkYXb4NBAoCLKYanoirty/JG+PjZUBzky3yYOQ3C2J9AuNfyrW76dO659OQ6NcYbcGDq5J0IJlUKg+PSDD7FpbIyrtm9FAEoIUpP74mCdUK+W+Om3vZudazfxQ3/1J8zRZR3yc1g6gSVkptFEG01a2L+epWjFiYOAWrnCCnIaOrkmcwHWBUhsYYY4tBEERXV8pj3Zpu6OpbMYHFhdTCrLptFxJsrDTDfnCIXnyuiaRtrCwmKTV116DWP1Ptp5i8xknmu6EPtSFLGwlPCzH/kbvrT/JK86eIzXXHcNDu8QV5WkoX1kIpRQUYK+QFFRHmDUjTn2Gn3xjfJQCMA3vHueeAu3SlH8EzfR/c+tAODPu6JV19IFX4NjttEktwHaBJjCCdLF39qGaBcU7PrdpXRlWV856cpVJJmmkeRYG2CNwpgArQNMHuB0WIDsV8K11lrqpTK377kKu5yh8dehTYi2MU1X4jc+8ymOnjuNKPYXWEIhqUQxSWb45Y99ku/5nT/kj//xXgIRcGZmni89+Qw4SRyERZLBC/abr7+B997wKvKW8fdslL9OKzHamz/aWpLM4EzQ28eYAG0UUpQolwqBLu7ft5MIinv2P8aoIt7sBVobgzYCa/1n2vjxsTbwhRVohup1XnXJpaRNQ25Db36YEKMDoMLDp6f5tx/+I07OzhIFEVEQEKmAchhTiko8euAE7/213+MfnjsOlTqluITBFfTMHqexNhbsrCl2VBUbSpL+wCc/nCu0cRf6+ZLStlpxFgJcrOw9Hhm+CZPjRTe3enG40LpZ2VaKWxzLrXTlAbjiYkVX8fqHleVFKrx3uIsvQgCdNKHV0lgT9pY15yzWefoyrVeuq2s+5Tbn3a+4hb++/36eaLcIgqiH1hJBxNfnGrznt3+H77/1Vdy4cxcD9SqNTsLTJ0/xZ1/4Mvc+fwirIsolX9nyK5/8NH/+5Qe549qr+L7XvoorL9lGvVJCCOsdr8yCCXB6hU5NSIFL2mwaGMY4S6tjwMZo7WPGEp8oUFhk9/YL7R5IhbChF+aC4NzhQIdk2u8cq4DA+hpNV8D3HBJtDJ3Mg/itM3zvK2/jUw8/weE0K+zvwuxwQNTPx57ezzP/72/wpmuv4rKtm6iWSpyZXeDLz+7n7sefYy4xEJdZF0tuvfZyUmd8ckxA0zrGAl+Gtdqc8c9i9V/nP99VXtqLbBd+x7/zzxPoCw7/wrNsRYtbA2hZaIHVX/K3YY2j1cnQxnoq2Z7dfv5NOvAZrTQnSbzAmG4PkELDJ1bQSX05lHNdYwhya1g3MsJ/uuMdvO/DH2a5HvosZKEJRBjy6GKDJz/+d4yWy9TKFdqJZrrVJnMWShWqnSZvvPZqnDMcm5ylFZT4q6ee5659B9k9McaudRPUShHHp6b50vHjCBXhCpyEE+AywyZZ5bbLrqSTJXRaGkxIt4odJ3AKtNXkeTf6KrDWMtE3SMWFLGsP7qfX+yRicn4ZgIl6H/1WsbCqcSV4muNDk9MIApzT7Fi3hn/z2tfz4x//W/JatIpMTXhNFNd5arHFk3d92TdicpIkN95MCEJEFOMaDT7w+tvZvWk9nSKUqZ1gwWjWhlHx5C60gVeF3r5Bfl7Eir7IRysiJP95Jkfvx3FegP88u7pw9Lpme0lFoAOMjrA69D8m9PWCWoELWGx0aCdZb1myhau30qBh5bZDqQi0hEzhVv2gA3QGrSTDW32rq6mhozPe/PJv49ff+R76lzNMO8caCVpCBooAG5SYTCwH5xqcaSVFNEAQLDf4mTe/npdfvgcrLG+57ipUmiDCiIYIefDsDH/y4ON86N77+czzR2g65WsyNf4nMZSbGT//trdzyYY1CBw1GUMmcVp5gkQjcbnEZIIkTXtjrq1l19o1XDs+AYnFWQVGIqxEqBJfeuYAX3rmGcYHB3nj7j2w1C7uS0AukJT4vbu/yo/8zh9x6PQUOHjfa2/lP776VoKlFjrDryS58BwhuXdgVRCjZUQiFCKKkHHkq4aWFnnfK67hg3e8nqxwPAWwaC0ta4iKHpArYtuVm/ODcRcqQ+HOly2x+sFfYHauvHKrBfri5vg3s60W7vMkrnjP4oowjGD90CjoAGdD/1MIszMKZwNwIWfmWxybmfXg+lWRgS68oftjjGViaJCrJyZgroXrgMsELpeQOGxHI+ly3nmASxeppZ0lsTnf96bX8ec/9INcV+mDxRa6ozEanBYII5BOoKyANMfML7I2z/i1d76Vn3rnW9EYOjrnu177Sv7d615FpdEib7R8xXwcE1QqhFGMtA60xeUGt9RiJDH8ytvv4D2vvYW2zqhWSrz7ZdcQLSe4lm8fZ3OwqSNvtOl0ElbnAmqVEv/xLa9nnXW45Q42s9jc4XI4fHae7/mlX+ex5/fzn7/rPbxn9y6qiy3MQgs738TOLjF3eprDx09TCiPv4CnBf/iud/Khd9zBZg1mvo1JHFZLnAG0Q1iHtK4ox7PYZoshnfHv3ngLv/rD340qhaQFv1yKYH+Seo7xnvh6nMhFXKzzZWm1//dSVkex08o0AWHz9iqP7ps+0gtu3YO7Qi8KIZjTCYEM6AtLfOGxZ3jbr/8habm60rRGrDiA4CBJ+NB3vI0PvOFVJHl23tWt/sM6b08ePH6WX//rv+drh4/TSDOkkvTXSrz5+iv4qXe9mVo5XAGzrNoM3imrBBEzcwt85qsP8fePPsWzkzMspxmZdThrKWFZVylx66U7+J7Xv4bLd20j0RnGWqQQhFIineC+R5/lj+/8Ao8cPsZ0u00unO8nITxEcrQc88pdO/ngW17HNZduJ9e5X2qlQBrJ39x9Px+99wGOzMyR5D4EeOXWCX7tB9/L9nXjZEVRrQACFfDQM4f4yOe+yOPHTrHcSaiWYnavG+MtN1zNG7/tGurVCu1OygNPPscD+w4wv9RgpL+P63Zt5+ar9lKrltBaew0qIFIhzx0+xZ/e9SXufHQfp5abZF1NVQxf4CwTpZBbL93Be19/Kzdes5fEGVLj8TuBEDzQ6DBrNbdWa2yJovPG/sUE+jxh7n74TYjjecf4RoH+520XF2ifdq4EIcvLHV7/H3+VZ5cTRBR+g1oXCGya8ZZdm/iLf/fDSLnKhL7IDTgLQeDZgabmF1lotlBSMtBXY3igjnVFWrbQcaupXXqRD0BJQVlFJEnGiclZTs/NMttqgYOJgTo71q1lfHjAMw9pjVs1AbvjXg0j8txy4uwkzxw+ztmZOZY6HYIoYNP4GJdt3cSOjetAOlKtve4qzLNASkIV0monnJuep9nuUKuVWT82TCkO0EWz+q4/4hBEQQgWFpaaNFodKpWYwb4qKlBkOi/ivhAFARLvhMsiy5YZXXDldUfSB+NCFSCRnJ6a46mDxzh0ZpK5pQbaGOrlElvWjXLVji1s27QepKCl86JYVxAJyT3zDR5ptXjdcD83lqpUpFyVrHkRX+tiQite5LPeB+K8v19YQ7+4l/eC28UEej5PaZmcelSiEkb88d9+jp/+i0+jB4dBdNFxBZyy4KOom5y/+qnv49Zr9p6nmbxwr8Qdu5eqhIeYSqGKPhy+xyKwkvun62R67Lfsanp8wqXb2kBKiRVgKDpSFXZPj9iE1WPtevfdTeCEUqKEQiF6uBiBb+ORa1Ngc0SvMqgLSRDCC5yS3rmxeFCWD22tdpncec9YFHV4Prlieo9uRbl1p4Ds6Q9PdviNq7KPLgl/Dd3jFtfSVQB5Qa7T5Y70HR4cn5hd5iuLTd6+pp+Xl8tsjkp0uUfOP8tFROufbhScd9QXjnIUMc/zgy0XHOMiH3R1X+/6nCOWkslOQikMaWUZ3/OW13D0zCS/c+9DuHIVFwTF3RZCYxzL7TZ/ec993HzVbhArmlTgkxj+oXsbu2MsC1nOTNbibJKwkGcsa82yNbSMo20M2nmjxgoIhCJWkpIUlHCUhPDMqWHEQBQyFIXUlKIuJXWlKKsAJUUh+J4T27IyUVbYFbxQZcZzWndtxm4YXSIoqaIDbCEM3uH16eIuHNdoQ0FRQyAEqsi+RkJ43onuAygCESv92p2vw2Rl8nXXj65D7R/S6terXjhvIjl8wXOXL2VFZPz/EkEsvMC3DHy10eFPZxaZNJb3r+lnTxSxPozPE+aVb7+A/PzzLN3eQVdp6AvusFAZ3eG44HsX/crqN7qzupvZO9ZYIJeCgVLFF0/mhr/9/AN84r6HOTQ5SyvTBFISq5CJgRrX79nM977hlezZuq6wVT0ftXOCuTTjuaUmzzWWeWZ5iSOdFlNpwpzNaQpHLiRWelZSlP8tCuJHL3ddHVuQwxSZQl9yJoiloIqkX0rGwpBtcZltpTKbymU2xWUm4ojBIKAkC6BUIeRGeAxCV0e2jGXZmpWCCDyJT8N6PHnioOMgcbboE+mF2qzCZ0jh6/BCIahKSUUKKlJSk5Ky9NxvoZCUha/PK0lfOR0JiIXHvKmixMk36PZrTM9Qupi26im8FeOs65BmzrFsHSfSnIeabe5ttNiXavZUIr5zpMalQcieUpUyKwrowkOvxJlXSfE/WZjPP84LC/QqO1O81EkvItBQLFMF6ig1lgPLM5TLFcpBhBRQCWOydsrc3CLzjRZxGFIpl+jvq9DfX/UELMZQkgHt3PDg1Bx/f+osDy8scUJr2kr4LLl/aj02PyElQqqe+SBY+e0nWpfk0a66r+KhiWKxPk81eeGKcdSFYlgqtsUxl5XLXFnvY1u1woZSif5AIYUkd47UWWbznCN5wvE04XSe0cQzGPu2C2JlZF13gSoeTrECdDWwDzQUNMbOEjjradqMRTsPeAqlIioA7gJHKCUlKYmFnwBVASXpqAlJVUpCKYmLIlXPjN8lP+tOPkiso2EN89owm+bMaM2ZPOeENkxax4K11IXlzf1V3thXZlMUsSUqU14lZudpYm/tXSAuq4T7Io77xTX3C0v/Cwj0aivshbYXtIZW7dENk3kbsKEzjjcXUVFIJYx7EYJSEPQErtcvwzliIVk2jr8/cY6/PnyCh+YWfLtkz95Hr3Gdb76Bb9qhIAhAKaSSnktCrOAjHBALRVwwYYLrZdq6mtQW6XznfNGsFXhWVCl7ICHnPPdfGRiQkm1RzN5yiaurNa6u97GtUqEeeFhmag3zVnNKa05knnd7VhuyAr3ob0EUZVuFY8yK8+eEB8ILLJeVIzZHIYHw0MxlYziT5DybaNpCIYsVwjgPF/Wxd2/SdOnXrOtNZW+UrHpf48lsjBOkeNL6NpA5r6C0EOQ4Kk5zVRxye73C9eWYjWHEWBB2MWnnSdNqkVntU5wvby/wxYvK7rck0N9wGRf5e/VBX9xzdHhwUnehSa3hTLtBx1nCQBEHIUp2hc0/3EhIcgePzCzyt8dP8exSk74oYk0cMhyFVENJSSmkFBhraeicBZMzmefMWs2CNSw4SwfQygu3CAICoZAI+oOI0ajMQBhSDUMCCpZV6wXV2a4zaMiMIbGWtrO0nSYtYLFpkU4WXTsW/7cymhEp2V2qcEO9xnX1PvZUK6yNY0pSkgMN6ziTZpzMMk5qzZQxtKwXQlWMQXdou7H3HBhRcMdQH4HzNXtdi6EiJPvaOZ9ebmMKc6JLmN6Nu3fJH7tV07YQ/NUOsaGb++n2foccR+YcKb4KpuQMuyLFK2tlrq9W2RQoJoKQWMoLFOzKM/+Gdy/mf10ozC8oyC8mi9+0QL/Qey+0feOJuzZ1lwGpbTXLeeqD8QiCQHkKVSSZEzyysMx8krGhVmaiHFMLlU+9iq5C9oMVUrQQw1OUpdbSzDVTacqZToujSYfnmk2O5TnHc82ikLTxnHolJRkII8biEsOlmIoMerQKwnl+CFVEJDw5CjjnK91Ta3xyxhha3bZ4xpBYTeYcTWvRRlO2sEEFXF2p8PKBfq4bHOCSapV+5aMxLWuZ0ZqTec7xNOOczlkylo4DW0z07gBK4dgdBVxSiqkHqmgTLJAO9ic59zayHvG7xWEoQEAUAk1XSxfCXKxGure/nzhd06ZtLZkzRMIxFsBlccSVccy2MGBzFDIWxpR67qJYZXN3r7mwa7vRlBfyvV5UC6/e6cVmQXEV37xAv9j733gRL7R6FL5m8YHXGtrZ8y63aXwpa1nJVdm9VQDKrve8KnbXc6Ao6hrxdqJHvkHbWE52Ohxsd3i81eSJRpvn0oRJ50iL5Mp4GDEahgxGIZGQvd4zPlLh1UbX1pR0mXpkT+v57KOmYwzLOmc5z2kWbX3baY5NUvpszt5KhZuGB7l5ZIgrBvpYE8eoApi1bAyzOue01pzThlnjbdgla+k4hzGWyPmCior0ocHEeSo2I4OeyWEFdNueuaJGr2tquMJBta5rm3uzKHPewZVAXTpGAsmOKOCSUsT6UDEgYEAGjIQBFenj2r0nIC6mjy+Qmpd0/C626l9sFlzsQC/oFL7YFy+Ms7zQBfnPX3AVuUgQUvScoSJM2JX6C2Z3Vx/4ZdQvldr5Il1NN6TWJXH0gh0UYa9AyCLuK8gsnE0Tnm62eLzZ4okk4WCSMZNlSCQbKlVGSyUqQdC79u6k6/YTEPgJZIuQ4/lX6G1cg1+ZMm3oaMNCu8Ny0iHJcpTOWVMKuWmwnxsHBtnbV2dTtcxA4NsiWweZdTScY1JrZnLNpM6ZMoZFY2kVRaW5w9M9FFdlu7Y3vrDY83evsqMLLd5lwI5w9EkYiwLWhiFbo4A1YYDC0jI5ubMMyYC1YURNyZ4gnweIXPWsLtSlPRPjRQX6BS3vF9jn4u+/iEC/1PbSwu1Wfbba1TzvaxeZMxcxxS48tP+q+Ia3CwH3miYvfncFLhKCkpAEUhbx3SJz5qBjYTbXHEpSvr68yCNLyxxqdAiDiMF6H+VCi/o2HUW/PFaiOb1HWSzxWEsZz1HSsA5USCmICESRMnGOzFhaeUYjTUiTnMDkjAeKnZUS1w30c9VAP2vKMVXlHVLwNm5qDYkztA20ikZODevNnI71jlyKb2WxGgsjhW+RVpGCqhIMKkVVQr+SVKWPd6fW0tSauSwjd5bxKGJNGFHvld6tGvRuMP1i1sEFf/e+9S3bx6sPeOG+33igf4ZAn3eYC35f7CK+BRv8ImPWe/1Nfq+7QmhnyYG20XSMb94ZSkmkZEHf6kNosog2COG5QTLrONFJ2d9o8Uy7zXFtWZKSNg6Lp70NWalt7v64YoV4S3+NW+tVcus4kqR8cmGZSetDad2AnTeTVkgpjLN0jKGV5zhtqAvJhjhmUzliUylkc7lEf+ib7cTSt6hWYsVO9rYwPQ2tL0hsdM8pYJW5YWgby7IxLFtPXj+gFGPKM3xWit7e3Uxj91q797oa/gms4DEKoVup+bvY9tJO3vmC+9Ki+i8k0KtP/k+PiJx3mAu/+pLbKsm/4OtdzWKw5NYWbZ29oAvhs3FlqYiFLDjWuncivFpz0DKOOa05k+UcTlIOJilT2tCwnt9PSEmA1/pWCNaFgttqNbZFMdo5Pj63xP7MEssVS7PHL1G8IYvroUjLryAFvZEQC4kSlj6gqgQDUtGvJDUpKCufMvdJlPNtfoFvde2s6SFYO9aSFjZ2JBTVIok0rgJqxTG6rUIufCznOXmrlQ+rBfr8xye6/7mLPdTV6/c3r40vtv0vEOiLvf4nHOZf8Kq6jujqFdLhehXqiTMFhZmPasTSR1Qi6W3k7j8p6B0gc7BkDNOZ4WSacjRLONrJmTaGlhSkUhFYy0DBK5cjUEWbi26DzIJPtXfDnveNntZXhWAHQqxypIuRdfQmniwosLoZwbDYP8KTHgaiiNgU54iloF8phlTIcBAwFITUZEECU7Qc9ifyg7byJAvz8cWez4Vmxzf1dP45n19w+n9ZgYaLC/I/Q7j/hbYeOk50l0r/vk9X+CU7c4aOMaRFKlsIj1mIpG8b1s2odbW3p59SXbVKU1uOJyn7koSnOynH0pwGHklXEgGROB/YtNpU8f1cfPpdFR96dk6Bouj+VCwYXWEPhexNgLBQ771/ApSzRM5RxlEVgiGlGA4DRsKQoSCgXFDZ+pC673+4WiVfXD29xLP8lgX6pXb+Py7Q5x3+Jf5+oe2fYX9/C9t5FmbP6fHv50U4K3OG3HlKLlEAhSLhMRSR7GpYB04ghCyq2yWJdZzLMg4Xgn0y18xZS1qE0Ci0c5eyWFFoZ9HlN+0K84oQS7GC7Quk6jmlEt8arYv7KAlBvxCMKMl4oBgLQvoLbuWguE9f4d01ai5QuRd45SufXhCYu/CxXNSBX338C1X7Sy3F37po/i8W6Iue8pv4fPXIXOzyLtznn7atRCe+cWB72rzYL7c+e5YVrXqtEyjpl/OwWOoVohBI0YOFdo/WsTCnNbO54USaclprFrVnmurYVQi+VYAgKbrH88LctYeVFMSF3VtTXlAHlWQkCBgKFENKUZOeBLFLFdYNO662aS86ehc4deeN86o/v0E0u3ZdEel54e2bEbd/ukj+HxBoeHFB/N+rxS8OjxXnf75Ke1Osyj6R4jW5dhZTXFIX6tnVlgK87VyYAysGqEA7S8d6W7ypDU1jaFvvsJri7N2JERWTJhaSeiCpSFUIc9cUciurBd1oy/nT/jwTwl0wiqLbi9KtmlQXke9vGKcVtteX9n3cC7wWL/D+t779HxLol9r+qaYKvLTmvui6+NLXIF54CnU1X7cI10FPwB1d+1j0soyiEHYKu74nPhK+Ibh+0eunABT1/rpAcC8QkFVqWZx/mBfeuub0KiG9aHDjBQX5mxWrf1nx+/+JQH+zn73Qtnqh/VZvV3zDy248vAfsX7UEn3+GLn3CKgATXUz2ef4X5x9h5ZpfyGbt0Q2sigVfTBP3DvVPctYu8j1xwWsu+PxbPsm/rPj9i/Fy/O/bvglt+i1//q2duic47vz3v/FUrlfmJc4Tt8K5E93Xq6RkdSape+yLXL7ofXjhxy8wab9Zl+MidvB5Ca0LJ+F5s/L/nCB3t/9LNXR3+1YF8Vvdf5Uh+aJfvVAVfTOhqwse9gUmy8WU3Etc5cWvyK26pgsnz4VZkfMM4pc6+wt4fy+03/8Fwgz/12vobzWS8U0Yhhfb5yVPceF3LmZNr7pWt2qf1TboC2ryl95eaPeusXH+SF0k0rBa8HsX9WKCvfpezntxke1/beTiW9n+Lxdo+NaF+mLfv9jrC7dvxW6/mCC8xDVecGqxSsBWdJx/tdps7Sr7C82di17hC97eakF+qev9lxS8//2L//8PBBpeWhBfSOj/pZbBFzv+tybIfjv/mi8enbiIrf5i24uu+hd3Wf/XbP9nLdj/D8WHzonl92ggAAAAAElFTkSuQmCC" /> |
| <div> |
| <div class="brand-sub">A warm, friendly fresh-food assistant for stores, managers, and everyday shoppers.</div> |
| </div> |
| </div> |
| <div class="hero-art">π₯¬ π π π</div> |
| </div> |
| <div class="search-pill">π Fresh, reliable, low-waste decisions across fish, produce, dairy, bakery, and more</div> |
| </div> |
| """, |
| unsafe_allow_html=True, |
| ) |
|
|
| try: |
| df = load_data() |
| except Exception as e: |
| st.error(str(e)) |
| st.stop() |
|
|
| filtered, exec_scope, scope_info = apply_filters(df) |
| if filtered.empty: |
| st.warning("No data left after filtering.") |
| st.stop() |
|
|
| role = st.radio("Choose your mode", ["Manager", "Consumer"], horizontal=True) |
|
|
| if role == "Manager": |
| tabs = st.tabs([ |
| "Executive Overview", |
| "Category Intelligence", |
| "Inventory & Replenishment", |
| "Promotion Designer", |
| "User Manual", |
| ]) |
| with tabs[0]: |
| executive_overview(filtered, exec_scope, scope_info) |
| with tabs[1]: |
| category_intelligence(filtered) |
| with tabs[2]: |
| inventory_page(filtered, full_df=df, scope_info=scope_info) |
| with tabs[3]: |
| promotion_page(filtered) |
| with tabs[4]: |
| manager_manual() |
| else: |
| tabs = st.tabs(["Deal Finder", "Bundle Builder", "Personalized Promotions", "Wait or buy now?", "User Manual"]) |
| with tabs[0]: |
| consumer_deals(filtered) |
| with tabs[1]: |
| consumer_bundles(filtered) |
| with tabs[2]: |
| consumer_personal(filtered) |
| with tabs[3]: |
| consumer_wait_or_buy(filtered) |
| with tabs[4]: |
| consumer_manual() |
|
|
| with st.expander("About this app"): |
| st.markdown(""" |
| - Stable FRESHIE UI baseline. |
| - Built to avoid the white-screen issues caused by heavier UI logic. |
| - Next step: reintroduce richer branding and additional consumer features incrementally. |
| """) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|