Therdpoom commited on
Commit
09a7a53
·
verified ·
1 Parent(s): 320b93f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +281 -1
app.py CHANGED
@@ -36,7 +36,8 @@ def index():
36
  "POST /api/coffee/forecast",
37
  "POST /api/coffee/menu-analysis",
38
  "POST /api/hr/classify",
39
- "POST /api/lottery/analyze"
 
40
  ]
41
  })
42
 
@@ -600,6 +601,285 @@ def hr_classify():
600
  return jsonify({"error": str(e)}), 500
601
 
602
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
603
  if __name__ == "__main__":
604
  port = int(os.environ.get("PORT", 7860))
605
  app.run(host="0.0.0.0", port=port)
 
36
  "POST /api/coffee/forecast",
37
  "POST /api/coffee/menu-analysis",
38
  "POST /api/hr/classify",
39
+ "POST /api/lottery/analyze",
40
+ "POST /api/sentiment/analyze"
41
  ]
42
  })
43
 
 
601
  return jsonify({"error": str(e)}), 500
602
 
603
 
604
+ # ─────────────────────────────────────
605
+ # POST /api/sentiment/analyze
606
+ # วิเคราะห์ความรู้สึกลูกค้าจาก reviews
607
+ # models: logistic | random_forest | xgboost | all
608
+ # ─────────────────────────────────────
609
+ @app.route("/api/sentiment/analyze", methods=["POST", "OPTIONS"])
610
+ def sentiment_analyze():
611
+ if request.method == "OPTIONS":
612
+ return jsonify({}), 200
613
+ try:
614
+ import time
615
+ t0 = time.time()
616
+ from sklearn.feature_extraction.text import TfidfVectorizer
617
+ from sklearn.linear_model import LogisticRegression
618
+ from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
619
+ from sklearn.model_selection import train_test_split
620
+ from sklearn.metrics import accuracy_score, f1_score, confusion_matrix
621
+ from sklearn.pipeline import Pipeline
622
+ from collections import Counter
623
+
624
+ body = request.get_json()
625
+ data = body.get("data", [])
626
+ model_type = body.get("model", "logistic").lower()
627
+
628
+ if len(data) < 5:
629
+ return jsonify({"error": "ต้องการข้อมูลอย่างน้อย 5 reviews"}), 400
630
+
631
+ df = pd.DataFrame(data)
632
+ if "review_text" not in df.columns:
633
+ return jsonify({"error": "Missing column: review_text"}), 400
634
+
635
+ df["review_text"] = df["review_text"].fillna("").astype(str)
636
+ has_label = "sentiment" in df.columns and df["sentiment"].notna().sum() > 0
637
+
638
+ X = df["review_text"]
639
+ results = {}
640
+ pipes = {}
641
+ models_to_run = (["logistic","random_forest","xgboost"]
642
+ if model_type == "all" else [model_type])
643
+
644
+ # Train/test split ถ้ามี label
645
+ if has_label:
646
+ y = df["sentiment"].fillna("Neutral")
647
+ if len(df) >= 20:
648
+ X_tr, X_te, y_tr, y_te = train_test_split(
649
+ X, y, test_size=0.2, random_state=42,
650
+ stratify=y if y.value_counts().min() >= 2 else None)
651
+ else:
652
+ X_tr, X_te, y_tr, y_te = X, X, y, y
653
+ else:
654
+ # ไม่มี label → train ด้วย rule-based pseudo labels
655
+ def rule_label(text):
656
+ tl = text.lower()
657
+ pos_w = ["ดี","เยี่ยม","ประทับใจ","แนะนำ","คุ้ม","ชอบ",
658
+ "excellent","great","perfect","love","good","satisfied"]
659
+ neg_w = ["แย่","ผิดหวัง","ห่วย","ช้า","เสีย","ไม่ดี",
660
+ "terrible","worst","poor","bad","horrible","disappointed"]
661
+ p = sum(1 for w in pos_w if w in tl)
662
+ n = sum(1 for w in neg_w if w in tl)
663
+ return "Positive" if p > n else "Negative" if n > p else "Neutral"
664
+ y = X.apply(rule_label)
665
+ X_tr, X_te, y_tr, y_te = X, X, y, y
666
+
667
+ for m_name in models_to_run:
668
+ if m_name == "logistic":
669
+ clf = LogisticRegression(max_iter=1000, C=0.5, random_state=42)
670
+ label = "Logistic + TF-IDF"
671
+ elif m_name == "random_forest":
672
+ clf = RandomForestClassifier(n_estimators=100, random_state=42)
673
+ label = "Random Forest + TF-IDF"
674
+ else:
675
+ clf = GradientBoostingClassifier(n_estimators=100, random_state=42)
676
+ label = "XGBoost + TF-IDF"
677
+
678
+ pipe = Pipeline([
679
+ ("tfidf", TfidfVectorizer(max_features=1500, ngram_range=(1,2),
680
+ sublinear_tf=True, min_df=1)),
681
+ ("clf", clf)
682
+ ])
683
+ pipe.fit(X_tr, y_tr)
684
+ y_pred = pipe.predict(X_te)
685
+
686
+ perf = {}
687
+ if has_label and len(set(y_te)) > 1:
688
+ perf = {
689
+ "accuracy_pct": round(accuracy_score(y_te,y_pred)*100, 1),
690
+ "f1_macro_pct": round(f1_score(y_te,y_pred,average="macro",zero_division=0)*100, 1),
691
+ "f1_weighted_pct": round(f1_score(y_te,y_pred,average="weighted",zero_division=0)*100, 1),
692
+ }
693
+ # Confusion matrix
694
+ labels_order = ["Positive","Neutral","Negative"]
695
+ labels_present = [l for l in labels_order if l in set(y_te)]
696
+ cm = confusion_matrix(y_te, y_pred, labels=labels_present)
697
+ perf["confusion_matrix"] = {
698
+ "labels": labels_present,
699
+ "matrix": cm.tolist()
700
+ }
701
+
702
+ # Top keywords per class via TF-IDF
703
+ tfidf_step = pipe.named_steps["tfidf"]
704
+ feature_names = tfidf_step.get_feature_names_out()
705
+ keywords = {}
706
+ for cls in ["Positive","Negative","Neutral"]:
707
+ mask = y_tr == cls
708
+ if mask.sum() > 0:
709
+ vecs = tfidf_step.transform(X_tr[mask])
710
+ mean_sc = vecs.mean(axis=0).A1
711
+ top_idx = mean_sc.argsort()[-10:][::-1]
712
+ keywords[cls] = [str(feature_names[i]) for i in top_idx
713
+ if len(str(feature_names[i])) > 1][:8]
714
+
715
+ results[m_name] = {
716
+ "model_name": label,
717
+ "performance": perf,
718
+ "keywords": keywords,
719
+ }
720
+ pipes[m_name] = pipe
721
+
722
+ # ── Best model → predict all ──
723
+ best_name = (model_type if model_type != "all" else "logistic")
724
+ if best_name not in pipes:
725
+ best_name = list(pipes.keys())[0]
726
+ best_pipe = pipes[best_name]
727
+
728
+ all_preds = best_pipe.predict(X)
729
+ all_proba = best_pipe.predict_proba(X)
730
+ all_classes = best_pipe.classes_
731
+
732
+ # ── Build predictions list ──
733
+ predictions = []
734
+ for i, row in df.iterrows():
735
+ prob_arr = all_proba[i]
736
+ pred = all_preds[i]
737
+ conf = float(prob_arr.max())
738
+ prob_dict = {c: round(float(p)*100,1)
739
+ for c,p in zip(all_classes, prob_arr)}
740
+ predictions.append({
741
+ "review_id": str(row.get("review_id", f"REV{i:04d}")),
742
+ "review_text": str(row.get("review_text",""))[:200],
743
+ "channel": str(row.get("channel","")),
744
+ "branch": str(row.get("branch","")),
745
+ "category": str(row.get("category","")),
746
+ "date": str(row.get("date","")),
747
+ "rating": int(row["rating"]) if "rating" in row and pd.notna(row.get("rating")) else None,
748
+ "actual_sentiment": str(row.get("sentiment","")) if has_label else None,
749
+ "predicted_sentiment": pred,
750
+ "confidence": round(conf, 3),
751
+ "probabilities": prob_dict,
752
+ })
753
+
754
+ # ── Summary stats ──
755
+ pred_counts = Counter(all_preds)
756
+ total = len(predictions)
757
+ sentiment_summary = {
758
+ "Positive": pred_counts.get("Positive",0),
759
+ "Neutral": pred_counts.get("Neutral",0),
760
+ "Negative": pred_counts.get("Negative",0),
761
+ "positive_pct": round(pred_counts.get("Positive",0)/total*100,1),
762
+ "neutral_pct": round(pred_counts.get("Neutral",0)/total*100,1),
763
+ "negative_pct": round(pred_counts.get("Negative",0)/total*100,1),
764
+ "net_sentiment_score": round(
765
+ (pred_counts.get("Positive",0) - pred_counts.get("Negative",0))/total*100, 1)
766
+ }
767
+
768
+ # ── Channel breakdown ──
769
+ channel_breakdown = {}
770
+ if "channel" in df.columns:
771
+ for ch in df["channel"].dropna().unique():
772
+ mask = df["channel"] == ch
773
+ ch_preds = [p["predicted_sentiment"] for p, m in
774
+ zip(predictions, mask) if m]
775
+ c2 = Counter(ch_preds)
776
+ n = len(ch_preds)
777
+ channel_breakdown[ch] = {
778
+ "total": n,
779
+ "Positive": c2.get("Positive",0),
780
+ "Negative": c2.get("Negative",0),
781
+ "Neutral": c2.get("Neutral",0),
782
+ "positive_pct": round(c2.get("Positive",0)/n*100,1) if n else 0,
783
+ "negative_pct": round(c2.get("Negative",0)/n*100,1) if n else 0,
784
+ }
785
+
786
+ # ── Branch breakdown ──
787
+ branch_breakdown = {}
788
+ if "branch" in df.columns:
789
+ for br in df["branch"].dropna().unique():
790
+ mask = df["branch"] == br
791
+ br_preds = [p["predicted_sentiment"] for p, m in
792
+ zip(predictions, mask) if m]
793
+ c3 = Counter(br_preds)
794
+ n = len(br_preds)
795
+ branch_breakdown[br] = {
796
+ "total": n,
797
+ "Positive": c3.get("Positive",0),
798
+ "Negative": c3.get("Negative",0),
799
+ "Neutral": c3.get("Neutral",0),
800
+ "negative_pct": round(c3.get("Negative",0)/n*100,1) if n else 0,
801
+ }
802
+
803
+ # ── Monthly trend ──
804
+ monthly_trend = {}
805
+ if "month" in df.columns:
806
+ for mo in sorted(df["month"].dropna().unique()):
807
+ mask = df["month"] == mo
808
+ mo_preds = [p["predicted_sentiment"] for p, m in
809
+ zip(predictions, mask) if m]
810
+ c4 = Counter(mo_preds)
811
+ monthly_trend[mo] = {
812
+ "Positive": c4.get("Positive",0),
813
+ "Negative": c4.get("Negative",0),
814
+ "Neutral": c4.get("Neutral",0),
815
+ }
816
+
817
+ # ── Avg rating by sentiment ──
818
+ avg_rating = {}
819
+ if "rating" in df.columns:
820
+ for s in ["Positive","Negative","Neutral"]:
821
+ mask = [p["predicted_sentiment"]==s for p in predictions]
822
+ ratings = [p["rating"] for p, m in zip(predictions, mask)
823
+ if m and p["rating"] is not None]
824
+ avg_rating[s] = round(np.mean(ratings),2) if ratings else None
825
+
826
+ elapsed = round(time.time()-t0, 1)
827
+ print(f"✅ sentiment/analyze: {total}reviews model={model_type} t={elapsed}s")
828
+
829
+ # Comparison (if all)
830
+ comparison = []
831
+ for m_name, m_res in results.items():
832
+ if m_res.get("performance"):
833
+ comparison.append({
834
+ "model": m_res["model_name"],
835
+ "accuracy_pct": m_res["performance"].get("accuracy_pct",0),
836
+ "f1_macro_pct": m_res["performance"].get("f1_macro_pct",0),
837
+ })
838
+
839
+ # Rule-based baseline
840
+ pos_w = ["ดี","เยี่ยม","ประทับใจ","excellent","great","love","good"]
841
+ neg_w = ["แย่","ผิดหวัง","ห่วย","terrible","worst","poor","bad"]
842
+ rule_preds = []
843
+ for text in X:
844
+ tl = text.lower()
845
+ p = sum(1 for w in pos_w if w in tl)
846
+ n = sum(1 for w in neg_w if w in tl)
847
+ rule_preds.append("Positive" if p>n else "Negative" if n>p else "Neutral")
848
+ if has_label:
849
+ rule_acc = round(accuracy_score(y, rule_preds)*100,1)
850
+ comparison.append({
851
+ "model": "Rule-based (Baseline)",
852
+ "accuracy_pct": rule_acc,
853
+ "f1_macro_pct": round(f1_score(y, rule_preds, average="macro",zero_division=0)*100,1)
854
+ })
855
+
856
+ return jsonify({
857
+ "total_reviews": total,
858
+ "model_requested": model_type,
859
+ "best_model": best_name,
860
+ "processing_time_sec": elapsed,
861
+ "has_label": has_label,
862
+ "sentiment_summary": sentiment_summary,
863
+ "channel_breakdown": channel_breakdown,
864
+ "branch_breakdown": branch_breakdown,
865
+ "monthly_trend": monthly_trend,
866
+ "avg_rating_by_sentiment": avg_rating,
867
+ "model_results": results,
868
+ "comparison": comparison,
869
+ "predictions": predictions,
870
+ "top_negative": sorted(
871
+ [p for p in predictions if p["predicted_sentiment"]=="Negative"],
872
+ key=lambda x: x["confidence"], reverse=True)[:10],
873
+ "top_positive": sorted(
874
+ [p for p in predictions if p["predicted_sentiment"]=="Positive"],
875
+ key=lambda x: x["confidence"], reverse=True)[:5],
876
+ })
877
+
878
+ except Exception as e:
879
+ import traceback; print(traceback.format_exc())
880
+ return jsonify({"error": str(e)}), 500
881
+
882
+
883
  if __name__ == "__main__":
884
  port = int(os.environ.get("PORT", 7860))
885
  app.run(host="0.0.0.0", port=port)