Varriety commited on
Commit
3bdb541
Β·
verified Β·
1 Parent(s): f9e27f4

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +381 -279
src/streamlit_app.py CHANGED
@@ -432,6 +432,9 @@ with st.spinner('Mempersiapkan model AI...'):
432
  def clean_text(text):
433
  text = str(text)
434
 
 
 
 
435
  text = re.sub(
436
  r'^.*?Β·\s*\d+\s*(?:dtk|mnt|jam|s|h|sec|min)\s*(?:Membalas\s+@\w+\s*)?',
437
  '',
@@ -439,25 +442,30 @@ def clean_text(text):
439
  flags=re.IGNORECASE
440
  )
441
 
 
442
  text = re.sub(r'Tampilkan lebih banyak.*$', '', text, flags=re.IGNORECASE)
443
 
 
 
444
  text = re.sub(r'(\s+\d+)+\s*$', '', text).strip()
445
 
 
446
  text = text.lower()
447
- text = re.sub(r"http\S+", "", text)
448
- text = re.sub(r"@\w+", "", text)
449
- text = re.sub(r"#\w+", "", text)
450
- text = re.sub(r"[^\w\s]", "", text)
451
- text = re.sub(r"\b\d+\b", "", text)
452
  text = re.sub(r"\s+", " ", text).strip()
453
 
 
454
  tokens = text.split()
455
  tokens = [word for word in tokens if word not in stop_words]
456
  return " ".join(tokens)
457
 
458
 
459
  # ==============================================================================
460
- # THRESHOLD TEXTBLOB
461
  # ==============================================================================
462
  TEXTBLOB_THRESHOLD = 0.10
463
 
@@ -473,6 +481,10 @@ def map_roberta(label):
473
  def map_bertweet(label):
474
  return {"pos": "positive", "neu": "neutral", "neg": "negative"}.get(label.lower(), "neutral")
475
 
 
 
 
 
476
  def get_daily_label(score):
477
  if score > TEXTBLOB_THRESHOLD: return 'Positive'
478
  elif score < -TEXTBLOB_THRESHOLD: return 'Negative'
@@ -607,7 +619,6 @@ if st.session_state.page == "uji_kalimat":
607
 
608
  for i, (method, label, detail) in enumerate(results):
609
  col = col_a if i % 2 == 0 else col_b
610
-
611
  bg = badge_color(label)
612
  tc = badge_text_color(label)
613
  icon = "β†—" if label == "positive" else ("β†˜" if label == "negative" else "β†’")
@@ -834,295 +845,386 @@ elif st.session_state.page == "analisis_batch":
834
  start_unix = int(datetime.strptime(target_dates[0], "%Y-%m-%d").replace(tzinfo=timezone.utc).timestamp()) - 86400
835
  end_unix = int(datetime.strptime(target_dates[-1], "%Y-%m-%d").replace(tzinfo=timezone.utc).timestamp()) + 86400
836
 
837
- with st.spinner("πŸ“‘ Mengambil data harga Bitcoin dari CoinGecko API..."):
 
 
 
 
838
  url = "https://api.coingecko.com/api/v3/coins/bitcoin/market_chart/range"
839
- params = {"vs_currency": "usd", "from": start_unix, "to": end_unix}
840
  headers = {"accept": "application/json", "User-Agent": "Mozilla/5.0"}
841
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
842
  try:
843
- time.sleep(2)
844
- res = requests.get(url, params=params, headers=headers)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
845
 
846
- if res.status_code != 200:
847
- st.error(f"API Error {res.status_code}: {res.text}")
848
  else:
849
- data_json = res.json()
850
-
851
- if "prices" not in data_json:
852
- st.error("Data harga tidak ditemukan di respons API.")
853
- else:
854
- prices = data_json["prices"]
855
-
856
- df_price = pd.DataFrame(prices, columns=["timestamp", "price"])
857
- df_price["date"] = pd.to_datetime(df_price["timestamp"], unit="ms").dt.date
858
- df_price = df_price.groupby("date")["price"].mean().reset_index()
859
- df_price["pct_change"] = df_price["price"].pct_change() * 100
860
- df_price["log_return"] = np.log(df_price["price"] / df_price["price"].shift(1))
861
- df_price.dropna(inplace=True)
862
- df_price = df_price[df_price["date"].isin(pd.to_datetime(target_dates).date)]
863
-
864
- if df_price.empty:
865
- st.warning("⚠️ Data Harga API kosong. Pastikan rentang tanggal di .txt sesuai (yyyy-mm-dd).")
866
- else:
867
- st.markdown("<hr class='vbc-divider'>", unsafe_allow_html=True)
868
-
869
- # ── Tabel Data Sentimen ──────────────────────────────
870
- st.markdown("πŸ—£οΈ Data Sentimen")
871
- raw_display_cols = ["date","raw_tweet","vader","textblob","bertweet","roberta","roberta_large","vader_score","tb_score"]
872
- st.dataframe(df[raw_display_cols], use_container_width=True, hide_index=True)
873
-
874
- # ==================================================
875
- # AGREGASI HARIAN DUAL-MODE
876
- # ==================================================
877
- sentiment_map = {"positive": 1, "neutral": 0, "negative": -1}
878
- df_score = df.copy()
879
- models_cat = ["vader","textblob","bertweet","roberta","roberta_large"]
880
- for col in models_cat:
881
- df_score[col] = df_score[col].map(sentiment_map)
882
-
883
- # Agregasi kategorik (βˆ’1/0/1 mean)
884
- df_sentiment_daily = df_score.groupby("date")[models_cat].mean().reset_index()
885
- df_sentiment_daily["date"] = pd.to_datetime(df_sentiment_daily["date"]).dt.date
886
-
887
- # Agregasi numerik VADER compound & TextBlob polarity
888
- df_numeric_daily = df.groupby("date")[["vader_score","tb_score"]].mean().reset_index()
889
- df_numeric_daily["date"] = pd.to_datetime(df_numeric_daily["date"]).dt.date
890
-
891
- for col in models_cat:
892
- df_sentiment_daily[f"{col}_label"] = df_sentiment_daily[col].apply(get_daily_label)
893
-
894
- daily_display_cols = ["date"]
895
- for col in models_cat:
896
- daily_display_cols.extend([col, f"{col}_label"])
897
-
898
- # ── Tabel Harga Bitcoin ───────────────────────────
899
- st.markdown("β‚Ώ Data Harga & Volatilitas Bitcoin")
900
- st.dataframe(df_price[["date","price","pct_change","log_return"]], use_container_width=True, hide_index=True)
901
-
902
- # Merge data
903
- df_merged = pd.merge(df_price, df_sentiment_daily, on="date", how="inner")
904
- df_merged = pd.merge(df_merged, df_numeric_daily, on="date", how="inner")
905
-
906
- # ── Tabel Data Final ──────────────────────────────
907
- st.markdown("πŸ—‚οΈ Data Final")
908
- final_display_cols = (
909
- ["date","price","pct_change","log_return"]
910
- + [c for c in daily_display_cols if c != "date"]
911
- + ["vader_score","tb_score"]
912
- )
913
- st.dataframe(df_merged[final_display_cols], use_container_width=True, hide_index=True)
914
-
915
- # Download buttons
916
- col_dl1, col_dl2, _ = st.columns([1, 1, 3])
917
- csv_data = df_merged.to_csv(index=False).encode('utf-8')
918
- col_dl1.download_button("πŸ“₯ Unduh CSV", data=csv_data, file_name="bitcoin_volatility_sentiment.csv", mime="text/csv", use_container_width=True)
919
- buffer = io.BytesIO()
920
- with pd.ExcelWriter(buffer, engine='xlsxwriter') as writer:
921
- df_merged.to_excel(writer, index=False)
922
- col_dl2.download_button("πŸ“₯ Unduh Excel", data=buffer.getvalue(), file_name="bitcoin_volatility_sentiment.xlsx", mime="application/vnd.ms-excel", use_container_width=True)
923
-
924
- st.markdown("<hr class='vbc-divider'>", unsafe_allow_html=True)
925
-
926
- # ==================================================
927
- # UJI KORELASI PEARSON DENGAN LAG
928
- # ==================================================
929
- st.subheader("πŸ”¬ Uji Korelasi Pearson")
930
- st.caption(
931
- "Menganalisis hubungan statistik antara skor sentimen harian dan "
932
- "volatilitas log-return BTC pada lag 0 (hari sama), +1 hari, dan +2 hari. "
933
- "VADER & TextBlob menggunakan skor numerik kontinu; model BERT menggunakan "
934
- "rata-rata kategorikal (βˆ’1/0/1)."
935
- )
936
-
937
- corr_columns = {
938
- "VADER (numerik)": "vader_score",
939
- "TextBlob (numerik)": "tb_score",
940
- "BERTweet (kategorik)": "bertweet",
941
- "RoBERTa Base (kategorik)": "roberta",
942
- "RoBERTa Large (kategorik)":"roberta_large",
943
- }
944
-
945
- corr_rows = []
946
- raw_results = []
947
-
948
- n_obs = len(df_merged)
949
-
950
- for label_name, col_key in corr_columns.items():
951
- for lag in [0, 1, 2]:
952
- log_ret_shifted = df_merged["log_return"].shift(-lag)
953
- valid_mask = log_ret_shifted.notna()
954
-
955
- x_vals = df_merged.loc[valid_mask, col_key]
956
- y_vals = log_ret_shifted[valid_mask]
957
-
958
- if len(x_vals) < 4:
959
- corr, pval = np.nan, np.nan
960
- else:
961
- try:
962
- corr, pval = pearsonr(x_vals, y_vals)
963
- except Exception:
964
- corr, pval = np.nan, np.nan
965
-
966
- arah = "Positif" if (corr is not np.nan and corr > 0) else "Negatif"
967
- sig = "βœ… Signifikan" if (pval is not np.nan and pval < 0.05) else "Tidak Signifikan"
968
-
969
- corr_rows.append({
970
- "Metode": label_name,
971
- "Lag": f"t+{lag}",
972
- "r": f"{corr:.4f}" if not np.isnan(corr) else "N/A",
973
- "Arah": arah,
974
- "p-value": f"{pval:.4f}" if not np.isnan(pval) else "N/A",
975
- "Signifikansi": sig,
976
- })
977
- raw_results.append({
978
- "metode": label_name,
979
- "col": col_key,
980
- "lag": lag,
981
- "r": corr if not np.isnan(corr) else 0.0,
982
- "p": pval if not np.isnan(pval) else 1.0,
983
- })
984
-
985
- df_corr_table = pd.DataFrame(corr_rows)
986
-
987
- def highlight_best(row):
988
  try:
989
- r_abs = abs(float(row["r"]))
990
- except:
991
- r_abs = 0
992
- sig_ok = "βœ…" in str(row["Signifikansi"])
993
- if sig_ok:
994
- return ["background-color:#e6fff1; font-weight:600"] * len(row)
995
- return [""] * len(row)
996
-
997
- styled = df_corr_table.style.apply(highlight_best, axis=1)
998
- st.dataframe(styled, use_container_width=True, hide_index=True)
999
-
1000
- # ── Heatmap Korelasi (r) per Metode Γ— Lag ─────────
1001
- st.subheader("🌑️ Heatmap Korelasi (r) β€” Metode Γ— Lag")
1002
- pivot_r = pd.DataFrame(raw_results).pivot_table(
1003
- index="metode", columns="lag", values="r"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1004
  )
1005
- pivot_r.columns = [f"Lag t+{c}" for c in pivot_r.columns]
1006
- pivot_r.index = [i.replace(" (numerik)","").replace(" (kategorik)","") for i in pivot_r.index]
1007
-
1008
- fig_hm, ax_hm = plt.subplots(figsize=(8, 4))
1009
- sns.heatmap(
1010
- pivot_r, annot=True, fmt=".3f", cmap="RdYlGn",
1011
- center=0, vmin=-1, vmax=1, linewidths=0.5,
1012
- ax=ax_hm, cbar_kws={"label": "Pearson r"}
1013
- )
1014
- ax_hm.set_title("Koefisien Korelasi Pearson (r) per Metode & Lag", fontweight='bold', pad=12)
1015
- ax_hm.set_ylabel("")
1016
- ax_hm.set_xlabel("Lag")
1017
  plt.tight_layout()
1018
- st.pyplot(fig_hm)
1019
-
1020
- # ── Scatter Plot ──────────────────────────────────
1021
- st.subheader("πŸ”΅ Pola Distribusi Scatter Plot (Lag t+0)")
1022
- scatter_cols = list(corr_columns.values())
1023
- cols_sc = st.columns(3)
1024
- for idx2, (disp_name, col_key) in enumerate(corr_columns.items()):
1025
- with cols_sc[idx2 % 3]:
1026
- fig_s, ax_s = plt.subplots(figsize=(5, 4))
1027
- sns.regplot(
1028
- data=df_merged, x=col_key, y="log_return", ax=ax_s,
1029
- scatter_kws={"s": 40, "color": "#10b981", "alpha": 0.5},
1030
- line_kws={"color": "#0f172a", "linewidth": 2}
1031
- )
1032
-
1033
- try:
1034
- r_val, p_val = pearsonr(df_merged[col_key], df_merged["log_return"])
1035
- ax_s.set_title(f"{disp_name.split(' ')[0]}\nr={r_val:.3f}, p={p_val:.3f}", fontweight='bold', fontsize=9)
1036
- except:
1037
- ax_s.set_title(disp_name.split(' ')[0], fontweight='bold')
1038
- ax_s.set_xlabel("Sentimen Score")
1039
- ax_s.set_ylabel("Log Return")
1040
- plt.tight_layout()
1041
- st.pyplot(fig_s)
1042
-
1043
- # ── Line Chart ────────────────────────────────────
1044
- st.subheader("πŸ“ˆ Trend Analisis: Sentiment vs BTC Volatility")
1045
- fig_line, ax_line = plt.subplots(figsize=(14, 6))
1046
- ax_line.plot(
1047
- df_merged["date"], df_merged["log_return"],
1048
- label="BTC Log Return", color="#f7931a", linewidth=3
 
 
 
 
 
 
 
1049
  )
1050
-
1051
- colors_line = ["#3B82F6","#10B981","#EC4899","#14B8A6","#6366F1"]
1052
- for i, (disp_name, col_key) in enumerate(corr_columns.items()):
1053
- ax_line.plot(
1054
- df_merged["date"], df_merged[col_key],
1055
- label=f"Sentimen: {disp_name.split(' ')[0]}",
1056
- color=colors_line[i], linewidth=1.5, linestyle="--", alpha=0.8
1057
- )
1058
- ax_line.set_title("Pergerakan Sentimen vs Log Return Bitcoin", fontsize=14, pad=15, fontweight='bold')
1059
- ax_line.set_xlabel("Tanggal", fontsize=11)
1060
- ax_line.set_ylabel("Nilai Metrik", fontsize=11)
1061
- ax_line.legend(loc='upper left', bbox_to_anchor=(1, 1), frameon=True)
1062
- plt.tight_layout()
1063
- st.pyplot(fig_line)
1064
-
1065
- # ── Distribusi Sentimen Harian (Bar) ──────────────
1066
- st.subheader("πŸ“Š Distribusi Sentimen Harian per Model")
1067
- label_cols = [c for c in df_merged.columns if c.endswith("_label")]
1068
- if label_cols:
1069
- fig_dist, axes = plt.subplots(1, len(label_cols), figsize=(14, 4), sharey=False)
1070
- if len(label_cols) == 1:
1071
- axes = [axes]
1072
- palette = {"Positive": "#10b981", "Neutral": "#94a3b8", "Negative": "#f43f5e"}
1073
- for ax_d, lc in zip(axes, label_cols):
1074
- counts = df_merged[lc].value_counts()
1075
- bars = ax_d.bar(
1076
- counts.index,
1077
- counts.values,
1078
- color=[palette.get(x, "#94a3b8") for x in counts.index]
1079
- )
1080
- ax_d.bar_label(bars, fontsize=9)
1081
- ax_d.set_title(lc.replace("_label", "").upper(), fontweight='bold', fontsize=9)
1082
- ax_d.set_xlabel("")
1083
- ax_d.set_ylabel("Jumlah Hari")
1084
- plt.tight_layout()
1085
- st.pyplot(fig_dist)
1086
-
1087
- # ── KESIMPULAN ────────────────────────────────────
1088
- st.markdown("<hr class='vbc-divider'>", unsafe_allow_html=True)
1089
- st.subheader("πŸ“ Kesimpulan")
1090
-
1091
- max_idx = df_merged["log_return"].idxmax()
1092
- min_idx = df_merged["log_return"].idxmin()
1093
- date_max = df_merged.loc[max_idx, "date"]
1094
- date_min = df_merged.loc[min_idx, "date"]
1095
-
1096
- st.write(
1097
- f"Puncak lonjakan positif (*max log return*) terjadi pada **{date_max}**, "
1098
- f"sedangkan penurunan ekstrem terjadi pada **{date_min}**. "
1099
- f"Dataset mencakup **{n_obs} hari** pengamatan."
1100
- )
1101
-
1102
- sig_results = [r for r in raw_results if r["p"] < 0.05]
1103
- all_results = raw_results
1104
-
1105
- if sig_results:
1106
- best = max(sig_results, key=lambda x: abs(x["r"]))
1107
- arah_text = "berbanding lurus (positif)" if best["r"] > 0 else "berbanding terbalik (negatif)"
1108
- sig_summary = ", ".join(
1109
- set(f"{r['metode'].split(' ')[0]} (lag t+{r['lag']})" for r in sig_results)
1110
- )
1111
- st.success(f"""
1112
  **Hipotesis Diterima (H1):** Ditemukan korelasi linier yang signifikan (*p-value* < 0.05) pada: **{sig_summary}**.
1113
 
1114
  Metode & lag dengan korelasi terkuat adalah **{best['metode']} (lag t+{best['lag']})** dengan r = **{best['r']:.4f}**, sifat hubungan **{arah_text}**.
1115
- """)
1116
- else:
1117
- best_overall = max(all_results, key=lambda x: abs(x["r"]))
1118
- st.warning(f"""
 
1119
  **Hipotesis Ditolak (H0 Diterima):** Belum ditemukan bukti empiris korelasi linier yang signifikan pada seluruh metode dan lag yang diuji (*p-value* β‰₯ 0.05).
1120
 
1121
  Korelasi terbesar ditemukan pada **{best_overall['metode']} (lag t+{best_overall['lag']})** dengan r = **{best_overall['r']:.4f}**. Volatilitas harga kemungkinan dipengaruhi faktor teknikal/fundamental di luar sentimen X, atau jumlah observasi belum mencukupi (n = {n_obs}, butuh minimal 30 hari agar r β‰₯ 0.35 bisa signifikan).
1122
- """)
1123
 
1124
  except Exception as e:
1125
- st.error(f"⚠️ Terjadi kesalahan saat mengambil atau memproses data API CoinGecko: {e}")
1126
 
1127
  elif analyze_batch_btn and not tweet_files:
1128
  st.warning("⚠️ Silakan unggah minimal satu file .txt terlebih dahulu.")
 
432
  def clean_text(text):
433
  text = str(text)
434
 
435
+ # Hapus prefix metadata Twitter/X
436
+ # Format: "DisplayName @handle Β· Xdtk/mnt/jam [Membalas @target]"
437
+ # Contoh: "Botanika Intern @botanika_intern Β· 7dtk Membalas @AshCrypto"
438
  text = re.sub(
439
  r'^.*?Β·\s*\d+\s*(?:dtk|mnt|jam|s|h|sec|min)\s*(?:Membalas\s+@\w+\s*)?',
440
  '',
 
442
  flags=re.IGNORECASE
443
  )
444
 
445
+ # Hapus "Tampilkan lebih banyak" (artefak UI Twitter)
446
  text = re.sub(r'Tampilkan lebih banyak.*$', '', text, flags=re.IGNORECASE)
447
 
448
+ # Hapus angka trailing engagement (like/retweet count)
449
+ # Contoh akhir tweet: "...Bitcoin drops. 1 6" β†’ angka dihapus
450
  text = re.sub(r'(\s+\d+)+\s*$', '', text).strip()
451
 
452
+ # Cleaning standar
453
  text = text.lower()
454
+ text = re.sub(r"http\S+", "", text) # hapus URL
455
+ text = re.sub(r"@\w+", "", text) # hapus @mention
456
+ text = re.sub(r"#\w+", "", text) # hapus #hashtag
457
+ text = re.sub(r"[^\w\s]", "", text) # hapus tanda baca
458
+ text = re.sub(r"\b\d+\b", "", text) # hapus angka sisa
459
  text = re.sub(r"\s+", " ", text).strip()
460
 
461
+ # --- LANGKAH 5: Hapus stopwords ---
462
  tokens = text.split()
463
  tokens = [word for word in tokens if word not in stop_words]
464
  return " ".join(tokens)
465
 
466
 
467
  # ==============================================================================
468
+ # THRESHOLD TEXTBLOB β€” dinaikkan dari Β±0.05 ke Β±0.10
469
  # ==============================================================================
470
  TEXTBLOB_THRESHOLD = 0.10
471
 
 
481
  def map_bertweet(label):
482
  return {"pos": "positive", "neu": "neutral", "neg": "negative"}.get(label.lower(), "neutral")
483
 
484
+
485
+ # ==============================================================================
486
+ # get_daily_label β€” threshold diselaraskan dengan TEXTBLOB_THRESHOLD
487
+ # ==============================================================================
488
  def get_daily_label(score):
489
  if score > TEXTBLOB_THRESHOLD: return 'Positive'
490
  elif score < -TEXTBLOB_THRESHOLD: return 'Negative'
 
619
 
620
  for i, (method, label, detail) in enumerate(results):
621
  col = col_a if i % 2 == 0 else col_b
 
622
  bg = badge_color(label)
623
  tc = badge_text_color(label)
624
  icon = "β†—" if label == "positive" else ("β†˜" if label == "negative" else "β†’")
 
845
  start_unix = int(datetime.strptime(target_dates[0], "%Y-%m-%d").replace(tzinfo=timezone.utc).timestamp()) - 86400
846
  end_unix = int(datetime.strptime(target_dates[-1], "%Y-%m-%d").replace(tzinfo=timezone.utc).timestamp()) + 86400
847
 
848
+ # ==============================================================
849
+ # FUNGSI FETCH HARGA BTC β€” Retry + Fallback ke Binance
850
+ # ==============================================================
851
+ def fetch_via_coingecko(start_ts, end_ts):
852
+ """Coba CoinGecko dengan retry eksponensial. Return list [[ts_ms, price]] atau raise."""
853
  url = "https://api.coingecko.com/api/v3/coins/bitcoin/market_chart/range"
854
+ params = {"vs_currency": "usd", "from": start_ts, "to": end_ts}
855
  headers = {"accept": "application/json", "User-Agent": "Mozilla/5.0"}
856
+ wait_times = [5, 15, 30]
857
+ for attempt, wait in enumerate(wait_times, 1):
858
+ time.sleep(wait)
859
+ res = requests.get(url, params=params, headers=headers, timeout=20)
860
+ if res.status_code == 200:
861
+ data_j = res.json()
862
+ if "prices" in data_j:
863
+ return data_j["prices"]
864
+ raise ValueError("Key 'prices' tidak ada di respons CoinGecko.")
865
+ if res.status_code == 429:
866
+ if attempt < len(wait_times):
867
+ continue
868
+ raise ConnectionError(f"CoinGecko 429 setelah {attempt} percobaan.")
869
+ raise ConnectionError(f"CoinGecko error {res.status_code}: {res.text[:200]}")
870
+ raise ConnectionError("CoinGecko gagal setelah semua percobaan.")
871
+
872
+ def fetch_via_binance(start_ts, end_ts):
873
+ """
874
+ Fallback: Binance Public API β€” BTCUSDT daily klines.
875
+ Endpoint bebas API key, limit 1000 candles per request.
876
+ """
877
+ url = "https://api.binance.com/api/v3/klines"
878
+ prices = []
879
+ cur_start = start_ts * 1000
880
+ end_ms = end_ts * 1000
881
+ while cur_start < end_ms:
882
+ params = {
883
+ "symbol": "BTCUSDT",
884
+ "interval": "1d",
885
+ "startTime": cur_start,
886
+ "endTime": end_ms,
887
+ "limit": 1000,
888
+ }
889
+ res = requests.get(url, params=params, timeout=20)
890
+ if res.status_code != 200:
891
+ raise ConnectionError(f"Binance error {res.status_code}: {res.text[:200]}")
892
+ batch = res.json()
893
+ if not batch:
894
+ break
895
+ for candle in batch:
896
+ open_time = int(candle[0])
897
+ close_price = float(candle[4])
898
+ prices.append([open_time, close_price])
899
+ cur_start = int(batch[-1][0]) + 1
900
+ if len(batch) < 1000:
901
+ break
902
+ if not prices:
903
+ raise ValueError("Binance tidak mengembalikan data.")
904
+ return prices
905
+
906
+ def build_df_price(raw_prices, target_date_list):
907
+ """Bersihkan raw [[ts_ms, price]] β†’ df_price dengan log_return."""
908
+ df_p = pd.DataFrame(raw_prices, columns=["timestamp", "price"])
909
+ df_p["date"] = pd.to_datetime(df_p["timestamp"], unit="ms").dt.date
910
+ df_p = df_p.groupby("date")["price"].mean().reset_index()
911
+ df_p["pct_change"] = df_p["price"].pct_change() * 100
912
+ df_p["log_return"] = np.log(df_p["price"] / df_p["price"].shift(1))
913
+ df_p.dropna(inplace=True)
914
+ df_p = df_p[df_p["date"].isin(pd.to_datetime(target_date_list).date)]
915
+ return df_p
916
+
917
+ # ── Eksekusi fetch dengan fallback ─────────────────────────────
918
+ raw_prices = None
919
+ api_source = None
920
+
921
+ with st.spinner("πŸ“‘ Mengambil data harga Bitcoin..."):
922
  try:
923
+ raw_prices = fetch_via_coingecko(start_unix, end_unix)
924
+ api_source = "CoinGecko"
925
+ except Exception as cg_err:
926
+ st.warning(
927
+ f"⚠️ CoinGecko tidak tersedia ({cg_err}). "
928
+ "Beralih ke **Binance Public API** sebagai fallback..."
929
+ )
930
+ try:
931
+ raw_prices = fetch_via_binance(start_unix, end_unix)
932
+ api_source = "Binance"
933
+ except Exception as bn_err:
934
+ st.error(
935
+ f"❌ Kedua sumber data gagal.\n"
936
+ f"- CoinGecko: {cg_err}\n"
937
+ f"- Binance: {bn_err}\n\n"
938
+ "Coba lagi beberapa menit kemudian atau periksa koneksi internet."
939
+ )
940
+
941
+ if raw_prices is not None:
942
+ try:
943
+ df_price = build_df_price(raw_prices, target_dates)
944
+ st.info(f"βœ… Data harga BTC berhasil diambil dari **{api_source}**.")
945
 
946
+ if df_price.empty:
947
+ st.warning("⚠️ Data Harga API kosong. Pastikan rentang tanggal di .txt sesuai (yyyy-mm-dd).")
948
  else:
949
+ st.markdown("<hr class='vbc-divider'>", unsafe_allow_html=True)
950
+
951
+ # ── Tabel Data Sentimen ──────────────────────────────
952
+ st.markdown("πŸ—£οΈ Data Sentimen")
953
+ raw_display_cols = ["date","raw_tweet","vader","textblob","bertweet","roberta","roberta_large","vader_score","tb_score"]
954
+ st.dataframe(df[raw_display_cols], use_container_width=True, hide_index=True)
955
+
956
+ # ==================================================
957
+ # AGREGASI HARIAN DUAL-MODE
958
+ # Mode A (Kategorik): konversi {pos:1, neu:0, neg:-1}
959
+ # β†’ tetap ada untuk tabel display & model BERT/RoBERTa
960
+ # Mode B (Numerik): rata-rata vader_score & tb_score
961
+ # β†’ dipakai sebagai variabel korelasi utama karena
962
+ # mempertahankan intensitas sentimen
963
+ # ==================================================
964
+ sentiment_map = {"positive": 1, "neutral": 0, "negative": -1}
965
+ df_score = df.copy()
966
+ models_cat = ["vader","textblob","bertweet","roberta","roberta_large"]
967
+ for col in models_cat:
968
+ df_score[col] = df_score[col].map(sentiment_map)
969
+
970
+ # Agregasi kategorik (βˆ’1/0/1 mean)
971
+ df_sentiment_daily = df_score.groupby("date")[models_cat].mean().reset_index()
972
+ df_sentiment_daily["date"] = pd.to_datetime(df_sentiment_daily["date"]).dt.date
973
+
974
+ # Agregasi numerik VADER compound & TextBlob polarity
975
+ df_numeric_daily = df.groupby("date")[["vader_score","tb_score"]].mean().reset_index()
976
+ df_numeric_daily["date"] = pd.to_datetime(df_numeric_daily["date"]).dt.date
977
+
978
+ for col in models_cat:
979
+ df_sentiment_daily[f"{col}_label"] = df_sentiment_daily[col].apply(get_daily_label)
980
+
981
+ daily_display_cols = ["date"]
982
+ for col in models_cat:
983
+ daily_display_cols.extend([col, f"{col}_label"])
984
+
985
+ # ── Tabel Harga Bitcoin ───────────────────────────
986
+ st.markdown("β‚Ώ Data Harga & Volatilitas Bitcoin")
987
+ st.dataframe(df_price[["date","price","pct_change","log_return"]], use_container_width=True, hide_index=True)
988
+
989
+ # Merge data
990
+ df_merged = pd.merge(df_price, df_sentiment_daily, on="date", how="inner")
991
+ df_merged = pd.merge(df_merged, df_numeric_daily, on="date", how="inner")
992
+
993
+ # ── Tabel Data Final ──────────────────────────────
994
+ st.markdown("πŸ—‚οΈ Data Final")
995
+ final_display_cols = (
996
+ ["date","price","pct_change","log_return"]
997
+ + [c for c in daily_display_cols if c != "date"]
998
+ + ["vader_score","tb_score"]
999
+ )
1000
+ st.dataframe(df_merged[final_display_cols], use_container_width=True, hide_index=True)
1001
+
1002
+ # Download buttons
1003
+ col_dl1, col_dl2, _ = st.columns([1, 1, 3])
1004
+ csv_data = df_merged.to_csv(index=False).encode('utf-8')
1005
+ col_dl1.download_button("πŸ“₯ Unduh CSV", data=csv_data, file_name="bitcoin_volatility_sentiment.csv", mime="text/csv", use_container_width=True)
1006
+ buffer = io.BytesIO()
1007
+ with pd.ExcelWriter(buffer, engine='xlsxwriter') as writer:
1008
+ df_merged.to_excel(writer, index=False)
1009
+ col_dl2.download_button("πŸ“₯ Unduh Excel", data=buffer.getvalue(), file_name="bitcoin_volatility_sentiment.xlsx", mime="application/vnd.ms-excel", use_container_width=True)
1010
+
1011
+ st.markdown("<hr class='vbc-divider'>", unsafe_allow_html=True)
1012
+
1013
+ # ==================================================
1014
+ # UJI KORELASI PEARSON DENGAN LAG
1015
+ # Lag 0 : sentimen hari t vs harga hari t (same-day)
1016
+ # Lag +1: sentimen hari t vs harga hari t+1
1017
+ # Lag +2: sentimen hari t vs harga hari t+2
1018
+ #
1019
+ # Kolom korelasi yang diuji:
1020
+ # - vader_score (numerik compound, βˆ’1 s.d. 1)
1021
+ # - tb_score (numerik polarity, βˆ’1 s.d. 1)
1022
+ # - bertweet (kategorik βˆ’1/0/1 mean)
1023
+ # - roberta (kategorik βˆ’1/0/1 mean)
1024
+ # - roberta_large(kategorik βˆ’1/0/1 mean)
1025
+ # ==================================================
1026
+ st.subheader("πŸ”¬ Uji Korelasi Pearson")
1027
+ st.caption(
1028
+ "Menganalisis hubungan statistik antara skor sentimen harian dan "
1029
+ "volatilitas log-return BTC pada lag 0 (hari sama), +1 hari, dan +2 hari. "
1030
+ "VADER & TextBlob menggunakan skor numerik kontinu; model BERT menggunakan "
1031
+ "rata-rata kategorikal (βˆ’1/0/1)."
1032
+ )
1033
+
1034
+ corr_columns = {
1035
+ "VADER (numerik)": "vader_score",
1036
+ "TextBlob (numerik)": "tb_score",
1037
+ "BERTweet (kategorik)": "bertweet",
1038
+ "RoBERTa Base (kategorik)": "roberta",
1039
+ "RoBERTa Large (kategorik)":"roberta_large",
1040
+ }
1041
+
1042
+ corr_rows = []
1043
+ raw_results = []
1044
+
1045
+ n_obs = len(df_merged)
1046
+
1047
+ for label_name, col_key in corr_columns.items():
1048
+ for lag in [0, 1, 2]:
1049
+ # shift(-lag): harga mundur lag hari ke depan
1050
+ # artinya: sentimen t berkorelasi dengan harga t+lag
1051
+ log_ret_shifted = df_merged["log_return"].shift(-lag)
1052
+ valid_mask = log_ret_shifted.notna()
1053
+
1054
+ x_vals = df_merged.loc[valid_mask, col_key]
1055
+ y_vals = log_ret_shifted[valid_mask]
1056
+
1057
+ if len(x_vals) < 4:
1058
+ corr, pval = np.nan, np.nan
1059
+ else:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1060
  try:
1061
+ corr, pval = pearsonr(x_vals, y_vals)
1062
+ except Exception:
1063
+ corr, pval = np.nan, np.nan
1064
+
1065
+ arah = "Positif" if (corr is not np.nan and corr > 0) else "Negatif"
1066
+ sig = "βœ… Signifikan" if (pval is not np.nan and pval < 0.05) else "Tidak Signifikan"
1067
+
1068
+ corr_rows.append({
1069
+ "Metode": label_name,
1070
+ "Lag": f"t+{lag}",
1071
+ "r": f"{corr:.4f}" if not np.isnan(corr) else "N/A",
1072
+ "Arah": arah,
1073
+ "p-value": f"{pval:.4f}" if not np.isnan(pval) else "N/A",
1074
+ "Signifikansi": sig,
1075
+ })
1076
+ raw_results.append({
1077
+ "metode": label_name,
1078
+ "col": col_key,
1079
+ "lag": lag,
1080
+ "r": corr if not np.isnan(corr) else 0.0,
1081
+ "p": pval if not np.isnan(pval) else 1.0,
1082
+ })
1083
+
1084
+ df_corr_table = pd.DataFrame(corr_rows)
1085
+
1086
+ # Tandai baris terbaik per metode (|r| terbesar)
1087
+ def highlight_best(row):
1088
+ try:
1089
+ r_abs = abs(float(row["r"]))
1090
+ except:
1091
+ r_abs = 0
1092
+ sig_ok = "βœ…" in str(row["Signifikansi"])
1093
+ if sig_ok:
1094
+ return ["background-color:#e6fff1; font-weight:600"] * len(row)
1095
+ return [""] * len(row)
1096
+
1097
+ styled = df_corr_table.style.apply(highlight_best, axis=1)
1098
+ st.dataframe(styled, use_container_width=True, hide_index=True)
1099
+
1100
+ # ── Heatmap Korelasi (r) per Metode Γ— Lag ─────────
1101
+ st.subheader("🌑️ Heatmap Korelasi (r) β€” Metode Γ— Lag")
1102
+ pivot_r = pd.DataFrame(raw_results).pivot_table(
1103
+ index="metode", columns="lag", values="r"
1104
+ )
1105
+ pivot_r.columns = [f"Lag t+{c}" for c in pivot_r.columns]
1106
+ pivot_r.index = [i.replace(" (numerik)","").replace(" (kategorik)","") for i in pivot_r.index]
1107
+
1108
+ fig_hm, ax_hm = plt.subplots(figsize=(8, 4))
1109
+ sns.heatmap(
1110
+ pivot_r, annot=True, fmt=".3f", cmap="RdYlGn",
1111
+ center=0, vmin=-1, vmax=1, linewidths=0.5,
1112
+ ax=ax_hm, cbar_kws={"label": "Pearson r"}
1113
+ )
1114
+ ax_hm.set_title("Koefisien Korelasi Pearson (r) per Metode & Lag", fontweight='bold', pad=12)
1115
+ ax_hm.set_ylabel("")
1116
+ ax_hm.set_xlabel("Lag")
1117
+ plt.tight_layout()
1118
+ st.pyplot(fig_hm)
1119
+
1120
+ # ── Scatter Plot ──────────────────────────────────
1121
+ st.subheader("πŸ”΅ Pola Distribusi Scatter Plot (Lag t+0)")
1122
+ scatter_cols = list(corr_columns.values())
1123
+ cols_sc = st.columns(3)
1124
+ for idx2, (disp_name, col_key) in enumerate(corr_columns.items()):
1125
+ with cols_sc[idx2 % 3]:
1126
+ fig_s, ax_s = plt.subplots(figsize=(5, 4))
1127
+ sns.regplot(
1128
+ data=df_merged, x=col_key, y="log_return", ax=ax_s,
1129
+ scatter_kws={"s": 40, "color": "#10b981", "alpha": 0.5},
1130
+ line_kws={"color": "#0f172a", "linewidth": 2}
1131
  )
1132
+ # Tampilkan r dan p pada plot
1133
+ try:
1134
+ r_val, p_val = pearsonr(df_merged[col_key], df_merged["log_return"])
1135
+ ax_s.set_title(f"{disp_name.split(' ')[0]}\nr={r_val:.3f}, p={p_val:.3f}", fontweight='bold', fontsize=9)
1136
+ except:
1137
+ ax_s.set_title(disp_name.split(' ')[0], fontweight='bold')
1138
+ ax_s.set_xlabel("Sentimen Score")
1139
+ ax_s.set_ylabel("Log Return")
 
 
 
 
1140
  plt.tight_layout()
1141
+ st.pyplot(fig_s)
1142
+
1143
+ # ── Line Chart ────────────────────────────────────
1144
+ st.subheader("πŸ“ˆ Trend Analisis: Sentiment vs BTC Volatility")
1145
+ fig_line, ax_line = plt.subplots(figsize=(14, 6))
1146
+ ax_line.plot(
1147
+ df_merged["date"], df_merged["log_return"],
1148
+ label="BTC Log Return", color="#f7931a", linewidth=3
1149
+ )
1150
+ # Plot semua kolom sentimen (numerik + kategorik)
1151
+ colors_line = ["#3B82F6","#10B981","#EC4899","#14B8A6","#6366F1"]
1152
+ for i, (disp_name, col_key) in enumerate(corr_columns.items()):
1153
+ ax_line.plot(
1154
+ df_merged["date"], df_merged[col_key],
1155
+ label=f"Sentimen: {disp_name.split(' ')[0]}",
1156
+ color=colors_line[i], linewidth=1.5, linestyle="--", alpha=0.8
1157
+ )
1158
+ ax_line.set_title("Pergerakan Sentimen vs Log Return Bitcoin", fontsize=14, pad=15, fontweight='bold')
1159
+ ax_line.set_xlabel("Tanggal", fontsize=11)
1160
+ ax_line.set_ylabel("Nilai Metrik", fontsize=11)
1161
+ ax_line.legend(loc='upper left', bbox_to_anchor=(1, 1), frameon=True)
1162
+ plt.tight_layout()
1163
+ st.pyplot(fig_line)
1164
+
1165
+ # ── Distribusi Sentimen Harian (Bar) ──────────────
1166
+ st.subheader("πŸ“Š Distribusi Sentimen Harian per Model")
1167
+ label_cols = [c for c in df_merged.columns if c.endswith("_label")]
1168
+ if label_cols:
1169
+ fig_dist, axes = plt.subplots(1, len(label_cols), figsize=(14, 4), sharey=False)
1170
+ if len(label_cols) == 1:
1171
+ axes = [axes]
1172
+ palette = {"Positive": "#10b981", "Neutral": "#94a3b8", "Negative": "#f43f5e"}
1173
+ for ax_d, lc in zip(axes, label_cols):
1174
+ counts = df_merged[lc].value_counts()
1175
+ bars = ax_d.bar(
1176
+ counts.index,
1177
+ counts.values,
1178
+ color=[palette.get(x, "#94a3b8") for x in counts.index]
1179
  )
1180
+ ax_d.bar_label(bars, fontsize=9)
1181
+ ax_d.set_title(lc.replace("_label", "").upper(), fontweight='bold', fontsize=9)
1182
+ ax_d.set_xlabel("")
1183
+ ax_d.set_ylabel("Jumlah Hari")
1184
+ plt.tight_layout()
1185
+ st.pyplot(fig_dist)
1186
+
1187
+ # ── KESIMPULAN ────────────────────────────────────
1188
+ st.markdown("<hr class='vbc-divider'>", unsafe_allow_html=True)
1189
+ st.subheader("πŸ“ Kesimpulan")
1190
+
1191
+ max_idx = df_merged["log_return"].idxmax()
1192
+ min_idx = df_merged["log_return"].idxmin()
1193
+ date_max = df_merged.loc[max_idx, "date"]
1194
+ date_min = df_merged.loc[min_idx, "date"]
1195
+
1196
+ st.write(
1197
+ f"Puncak lonjakan positif (*max log return*) terjadi pada **{date_max}**, "
1198
+ f"sedangkan penurunan ekstrem terjadi pada **{date_min}**. "
1199
+ f"Dataset mencakup **{n_obs} hari** pengamatan."
1200
+ )
1201
+
1202
+ # Cari lag & metode terbaik (|r| terbesar + signifikan)
1203
+ sig_results = [r for r in raw_results if r["p"] < 0.05]
1204
+ all_results = raw_results
1205
+
1206
+ if sig_results:
1207
+ best = max(sig_results, key=lambda x: abs(x["r"]))
1208
+ arah_text = "berbanding lurus (positif)" if best["r"] > 0 else "berbanding terbalik (negatif)"
1209
+ sig_summary = ", ".join(
1210
+ set(f"{r['metode'].split(' ')[0]} (lag t+{r['lag']})" for r in sig_results)
1211
+ )
1212
+ st.success(f"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1213
  **Hipotesis Diterima (H1):** Ditemukan korelasi linier yang signifikan (*p-value* < 0.05) pada: **{sig_summary}**.
1214
 
1215
  Metode & lag dengan korelasi terkuat adalah **{best['metode']} (lag t+{best['lag']})** dengan r = **{best['r']:.4f}**, sifat hubungan **{arah_text}**.
1216
+ """)
1217
+ else:
1218
+ # Tampilkan tetap hasil terbaik meskipun belum signifikan
1219
+ best_overall = max(all_results, key=lambda x: abs(x["r"]))
1220
+ st.warning(f"""
1221
  **Hipotesis Ditolak (H0 Diterima):** Belum ditemukan bukti empiris korelasi linier yang signifikan pada seluruh metode dan lag yang diuji (*p-value* β‰₯ 0.05).
1222
 
1223
  Korelasi terbesar ditemukan pada **{best_overall['metode']} (lag t+{best_overall['lag']})** dengan r = **{best_overall['r']:.4f}**. Volatilitas harga kemungkinan dipengaruhi faktor teknikal/fundamental di luar sentimen X, atau jumlah observasi belum mencukupi (n = {n_obs}, butuh minimal 30 hari agar r β‰₯ 0.35 bisa signifikan).
1224
+ """)
1225
 
1226
  except Exception as e:
1227
+ st.error(f"⚠️ Terjadi kesalahan saat memproses data harga Bitcoin: {e}")
1228
 
1229
  elif analyze_batch_btn and not tweet_files:
1230
  st.warning("⚠️ Silakan unggah minimal satu file .txt terlebih dahulu.")