GZielinski commited on
Commit
cafd505
·
verified ·
1 Parent(s): c008e02

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +324 -1
app.py CHANGED
@@ -10,6 +10,7 @@ import pandas as pd
10
  import gradio as gr
11
  import papermill as pm
12
  import plotly.graph_objects as go
 
13
 
14
  # Optional LLM (HuggingFace Inference API)
15
  try:
@@ -603,6 +604,268 @@ def build_top_sellers_chart() -> go.Figure:
603
  return fig
604
 
605
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
606
  def refresh_dashboard():
607
  return render_kpi_cards(), build_sales_chart(), build_sentiment_chart(), build_top_sellers_chart()
608
 
@@ -748,11 +1011,71 @@ with gr.Blocks(title="AIBDM 2026 Workshop App") as demo:
748
  interactive=False,
749
  )
750
 
751
- user_input.submit(
 
752
  ai_chat,
753
  inputs=[user_input, chatbot],
754
  outputs=[chatbot, user_input, ai_figure, ai_table],
755
  )
756
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
757
 
758
  demo.launch(css=load_css(), allowed_paths=[str(BASE_DIR)])
 
10
  import gradio as gr
11
  import papermill as pm
12
  import plotly.graph_objects as go
13
+ from plotly.subplots import make_subplots
14
 
15
  # Optional LLM (HuggingFace Inference API)
16
  try:
 
604
  return fig
605
 
606
 
607
+ # ---- Data loaders (cached at import) -------------------------------------
608
+
609
+ def _load_lookup_data():
610
+ """Load Camille's analyzed Amazon dataset + time-series, cached on first call."""
611
+ main_path = BASE_DIR / "final_analyzed_amazon_dataset.csv"
612
+ ts_path = BASE_DIR / "synthetic_historical_sales_prices.csv"
613
+ if not main_path.exists() or not ts_path.exists():
614
+ return None, None
615
+ df = pd.read_csv(main_path)
616
+ ts = pd.read_csv(ts_path)
617
+ ts["date"] = pd.to_datetime(ts["date"])
618
+ # Group rare categories under "Other" — long tail is 6 categories with 1-2 products
619
+ cat_counts = df["main_category"].value_counts()
620
+ main_cats = cat_counts[cat_counts >= 10].index.tolist()
621
+ df["display_category"] = df["main_category"].where(
622
+ df["main_category"].isin(main_cats), "Other"
623
+ )
624
+ return df, ts
625
+
626
+
627
+ _LOOKUP_DF, _LOOKUP_TS = _load_lookup_data()
628
+
629
+
630
+ # ---- Helpers -------------------------------------------------------------
631
+
632
+ def _short_name(name, n=70):
633
+ s = str(name)
634
+ return s if len(s) <= n else s[: n - 1] + "…"
635
+
636
+
637
+ def _lookup_categories():
638
+ if _LOOKUP_DF is None:
639
+ return ["All categories"]
640
+ return ["All categories"] + sorted(_LOOKUP_DF["display_category"].unique().tolist())
641
+
642
+
643
+ def _lookup_products(category):
644
+ if _LOOKUP_DF is None:
645
+ return []
646
+ sub = _LOOKUP_DF if category == "All categories" else _LOOKUP_DF[_LOOKUP_DF["display_category"] == category]
647
+ return [f"{_short_name(r['product_name'])} | {r['product_id']}" for _, r in sub.iterrows()]
648
+
649
+
650
+ def _parse_pid(choice):
651
+ if not choice or "|" not in choice:
652
+ return None
653
+ return choice.split("|")[-1].strip()
654
+
655
+
656
+ # ---- Recommendation badge (matches BubbleBusters glass-morphism style) ----
657
+
658
+ REC_STYLES = {
659
+ "increase_price": ("#2ec4a0", "↑ INCREASE PRICE",
660
+ "Demand signals support a price increase"),
661
+ "maintain_price": ("#7c5cbf", "→ MAINTAIN PRICE",
662
+ "Current pricing is well-aligned with demand"),
663
+ "decrease_price": ("#e8537a", "↓ DECREASE PRICE",
664
+ "Demand softness suggests a price reduction"),
665
+ }
666
+
667
+
668
+ def _render_lookup_recommendation(rec):
669
+ color, label, sub = REC_STYLES.get(rec, ("#9d8fc4", "—", ""))
670
+ return f"""
671
+ <div style="background:linear-gradient(135deg,{color}f0,{color}c0);
672
+ color:white;padding:28px 24px;border-radius:20px;text-align:center;
673
+ box-shadow:0 8px 24px rgba(124,92,191,.18);
674
+ border:1.5px solid rgba(255,255,255,.4);
675
+ backdrop-filter:blur(16px);margin-bottom:20px;">
676
+ <div style="font-size:30px;font-weight:800;letter-spacing:1.2px;">{label}</div>
677
+ <div style="font-size:13px;opacity:0.92;margin-top:6px;font-weight:500;">{sub}</div>
678
+ </div>
679
+ """
680
+
681
+
682
+ def _render_lookup_kpi_cards(row):
683
+ cards = [
684
+ ("Current price", f"₹{row['discounted_price']:,.0f}",
685
+ f"List ₹{row['actual_price']:,.0f}", "#a48de8"),
686
+ ("Competitor price", f"₹{row['competitor_price']:,.0f}",
687
+ f"{((row['discounted_price'] - row['competitor_price']) / row['competitor_price'] * 100):+.1f}% vs us", "#7aa6f8"),
688
+ ("Customer rating", f"★ {row['rating']:.1f}",
689
+ f"{int(row['rating_count']):,} reviews", "#6ee7c7"),
690
+ ("Monthly sales", f"{int(row['monthly_sales']):,}",
691
+ f"Demand idx {row['demand_index']:.0f}", "#3dcba8"),
692
+ ("Sentiment", f"{row['sentiment_score']:+.2f}",
693
+ str(row['sentiment_label']).title(), "#e8a230"),
694
+ ("Customer segment", str(row['customer_segment']).replace('_', ' ').title(),
695
+ f"Return rate {row['return_rate']*100:.1f}%", "#c45ea8"),
696
+ ]
697
+ html = (
698
+ '<div style="display:grid;'
699
+ 'grid-template-columns:repeat(auto-fit,minmax(160px,1fr));'
700
+ 'gap:14px;margin-bottom:20px;">'
701
+ )
702
+ for label, value, sub, accent in cards:
703
+ html += f"""
704
+ <div style="background:rgba(255,255,255,.72);backdrop-filter:blur(16px);
705
+ border-radius:18px;padding:16px 14px;
706
+ border:1.5px solid rgba(255,255,255,.8);
707
+ box-shadow:0 4px 16px rgba(124,92,191,.08);
708
+ border-top:3px solid {accent};">
709
+ <div style="color:#9d8fc4;font-size:9.5px;font-weight:800;
710
+ text-transform:uppercase;letter-spacing:1.5px;
711
+ margin-bottom:6px;">{label}</div>
712
+ <div style="color:#2d1f4e;font-size:18px;font-weight:800;
713
+ margin-bottom:3px;">{value}</div>
714
+ <div style="color:#9d8fc4;font-size:11px;font-weight:500;">{sub}</div>
715
+ </div>"""
716
+ html += "</div>"
717
+ return html
718
+
719
+
720
+ def _render_lookup_review(row):
721
+ text = str(row.get("review_content", ""))[:380]
722
+ truncated = "…" if len(str(row.get("review_content", ""))) > 380 else ""
723
+ return f"""
724
+ <div style="background:rgba(255,255,255,.72);backdrop-filter:blur(16px);
725
+ border-radius:18px;padding:18px 22px;
726
+ border:1.5px solid rgba(255,255,255,.8);
727
+ border-left:4px solid #7c5cbf;
728
+ box-shadow:0 4px 16px rgba(124,92,191,.08);
729
+ margin-bottom:20px;">
730
+ <div style="color:#4b2d8a;font-weight:800;font-size:11px;
731
+ text-transform:uppercase;letter-spacing:1.5px;
732
+ margin-bottom:8px;">Customer voice</div>
733
+ <div style="color:#2d1f4e;font-size:13px;line-height:1.6;">{text}{truncated}</div>
734
+ </div>
735
+ """
736
+
737
+
738
+ # ---- Charts (use existing _styled_layout + CHART_PALETTE) ----------------
739
+
740
+ def build_lookup_history(product_id):
741
+ """18-month history: sales (left axis) + price (right axis)."""
742
+ if _LOOKUP_TS is None:
743
+ return _empty_chart("Place CSVs at Space root to enable Product Lookup")
744
+ sub = _LOOKUP_TS[_LOOKUP_TS["product_id"] == product_id].sort_values("date")
745
+ if sub.empty:
746
+ return _empty_chart("No history for this product")
747
+
748
+ fig = make_subplots(specs=[[{"secondary_y": True}]])
749
+ fig.add_trace(
750
+ go.Scatter(
751
+ x=sub["date"], y=sub["historical_sales"], name="Sales (units)",
752
+ mode="lines+markers", line=dict(color="#7c5cbf", width=2.5),
753
+ marker=dict(size=5),
754
+ hovertemplate="<b>Sales</b>: %{y:,.0f}<br>%{x|%b %Y}<extra></extra>",
755
+ ),
756
+ secondary_y=False,
757
+ )
758
+ fig.add_trace(
759
+ go.Scatter(
760
+ x=sub["date"], y=sub["historical_price"], name="Price (₹)",
761
+ mode="lines+markers", line=dict(color="#e8a230", width=2, dash="dot"),
762
+ marker=dict(size=4),
763
+ hovertemplate="<b>Price</b>: ₹%{y:,.0f}<br>%{x|%b %Y}<extra></extra>",
764
+ ),
765
+ secondary_y=True,
766
+ )
767
+ fig.update_layout(**_styled_layout(
768
+ height=400, hovermode="x unified",
769
+ title=dict(text="18-month history: sales and price"),
770
+ ))
771
+ fig.update_xaxes(gridcolor="rgba(124,92,191,0.15)")
772
+ fig.update_yaxes(title_text="Sales (units)", secondary_y=False,
773
+ gridcolor="rgba(124,92,191,0.15)")
774
+ fig.update_yaxes(title_text="Price (₹)", secondary_y=True, showgrid=False)
775
+ return fig
776
+
777
+
778
+ def build_lookup_forecast(product_id):
779
+ """6-month sales projection with confidence band (trend-based)."""
780
+ if _LOOKUP_TS is None:
781
+ return _empty_chart("Place CSVs at Space root to enable Product Lookup")
782
+ sub = _LOOKUP_TS[_LOOKUP_TS["product_id"] == product_id].sort_values("date").copy()
783
+ if sub.empty or len(sub) < 6:
784
+ return _empty_chart("Not enough history to forecast")
785
+
786
+ recent = sub.tail(6)["historical_sales"].values
787
+ trend = (recent[-1] - recent[0]) / 5
788
+ last_val = recent[-1]
789
+ last_date = sub["date"].iloc[-1]
790
+ future_dates = pd.date_range(last_date, periods=7, freq="ME")[1:]
791
+ future_vals = [max(0, last_val + trend * (i + 1)) for i in range(6)]
792
+ std = sub["historical_sales"].tail(12).std()
793
+ upper = [v + std for v in future_vals]
794
+ lower = [max(0, v - std) for v in future_vals]
795
+
796
+ fig = go.Figure()
797
+ fig.add_trace(go.Scatter(
798
+ x=sub["date"], y=sub["historical_sales"], name="Historical",
799
+ mode="lines", line=dict(color="#7c5cbf", width=2.5),
800
+ hovertemplate="<b>Historical</b>: %{y:,.0f}<extra></extra>",
801
+ ))
802
+ fig.add_trace(go.Scatter(x=future_dates, y=upper, mode="lines",
803
+ line=dict(width=0), showlegend=False, hoverinfo="skip"))
804
+ fig.add_trace(go.Scatter(
805
+ x=future_dates, y=lower, mode="lines", fill="tonexty",
806
+ fillcolor="rgba(46,196,160,0.18)", line=dict(width=0),
807
+ name="Forecast confidence", hoverinfo="skip",
808
+ ))
809
+ fig.add_trace(go.Scatter(
810
+ x=future_dates, y=future_vals, name="Forecast",
811
+ mode="lines+markers", line=dict(color="#2ec4a0", width=2.5, dash="dash"),
812
+ marker=dict(size=6),
813
+ hovertemplate="<b>Forecast</b>: %{y:,.0f}<extra></extra>",
814
+ ))
815
+ fig.update_layout(**_styled_layout(
816
+ height=400, hovermode="x unified",
817
+ title=dict(text="6-month sales forecast"),
818
+ ))
819
+ fig.update_xaxes(gridcolor="rgba(124,92,191,0.15)")
820
+ fig.update_yaxes(title_text="Sales (units)", gridcolor="rgba(124,92,191,0.15)")
821
+ return fig
822
+
823
+
824
+ # ---- Update callback (called on dropdown changes) ------------------------
825
+
826
+ def update_product_lookup(category, product_choice):
827
+ if _LOOKUP_DF is None:
828
+ msg = (
829
+ '<div style="background:rgba(255,255,255,.72);backdrop-filter:blur(16px);'
830
+ 'border-radius:18px;padding:24px;text-align:center;'
831
+ 'border:1.5px solid rgba(255,255,255,.8);'
832
+ 'box-shadow:0 4px 16px rgba(124,92,191,.08);">'
833
+ '<div style="font-size:32px;margin-bottom:10px;">⚙️</div>'
834
+ '<div style="color:#4b2d8a;font-weight:800;margin-bottom:6px;">Data not loaded</div>'
835
+ '<div style="color:#9d8fc4;font-size:12px;">'
836
+ 'Place <code>final_analyzed_amazon_dataset.csv</code> and '
837
+ '<code>synthetic_historical_sales_prices.csv</code> at the Space root.</div></div>'
838
+ )
839
+ return msg, "", "", _empty_chart(""), _empty_chart("")
840
+
841
+ pid = _parse_pid(product_choice)
842
+ if pid is None or pid not in _LOOKUP_DF["product_id"].values:
843
+ empty = ('<div style="padding:20px;color:#9d8fc4;text-align:center;">'
844
+ 'Select a product to see details</div>')
845
+ return empty, "", "", _empty_chart(""), _empty_chart("")
846
+
847
+ row = _LOOKUP_DF[_LOOKUP_DF["product_id"] == pid].iloc[0]
848
+ return (
849
+ _render_lookup_recommendation(row["pricing_recommendation"]),
850
+ _render_lookup_kpi_cards(row),
851
+ _render_lookup_review(row),
852
+ build_lookup_history(pid),
853
+ build_lookup_forecast(pid),
854
+ )
855
+
856
+
857
+ def update_product_choices(category):
858
+ """When category changes, refresh the product dropdown."""
859
+ products = _lookup_products(category)
860
+ return gr.update(
861
+ choices=products,
862
+ value=products[0] if products else None,
863
+ )
864
+
865
+
866
+ # ============================================================================
867
+
868
+
869
  def refresh_dashboard():
870
  return render_kpi_cards(), build_sales_chart(), build_sentiment_chart(), build_top_sellers_chart()
871
 
 
1011
  interactive=False,
1012
  )
1013
 
1014
+ user_input.submit(
1015
+
1016
  ai_chat,
1017
  inputs=[user_input, chatbot],
1018
  outputs=[chatbot, user_input, ai_figure, ai_table],
1019
  )
1020
 
1021
+
1022
+
1023
+ with gr.Tab("Product Lookup"):
1024
+ gr.Markdown(
1025
+ "### Single-product pricing deep dive\n"
1026
+ "Pick any product to see its pricing recommendation, key metrics, "
1027
+ "customer voice, 18-month history, and 6-month forecast."
1028
+ )
1029
+
1030
+ with gr.Row():
1031
+ with gr.Column(scale=1):
1032
+ lookup_cat = gr.Dropdown(
1033
+ choices=_lookup_categories(),
1034
+ value="All categories",
1035
+ label="Filter by category",
1036
+ )
1037
+ lookup_prod = gr.Dropdown(
1038
+ choices=_lookup_products("All categories"),
1039
+ value=(
1040
+ _lookup_products("All categories")[0]
1041
+ if _lookup_products("All categories") else None
1042
+ ),
1043
+ label="Select a product",
1044
+ )
1045
+ with gr.Column(scale=2):
1046
+ lookup_rec = gr.HTML()
1047
+
1048
+ lookup_kpis = gr.HTML()
1049
+ lookup_review = gr.HTML()
1050
+
1051
+ with gr.Row():
1052
+ lookup_history = gr.Plot()
1053
+ lookup_forecast = gr.Plot()
1054
+
1055
+ lookup_cat.change(
1056
+ fn=update_product_choices,
1057
+ inputs=lookup_cat,
1058
+ outputs=lookup_prod,
1059
+ )
1060
+ lookup_prod.change(
1061
+ fn=update_product_lookup,
1062
+ inputs=[lookup_cat, lookup_prod],
1063
+ outputs=[lookup_rec, lookup_kpis, lookup_review,
1064
+ lookup_history, lookup_forecast],
1065
+ )
1066
+ demo.load(
1067
+ fn=update_product_lookup,
1068
+ inputs=[lookup_cat, lookup_prod],
1069
+ outputs=[lookup_rec, lookup_kpis, lookup_review,
1070
+ lookup_history, lookup_forecast],
1071
+ )
1072
+
1073
+
1074
+
1075
+
1076
+
1077
+
1078
+
1079
+
1080
 
1081
  demo.launch(css=load_css(), allowed_paths=[str(BASE_DIR)])