Update src/streamlit_app.py
Browse files- 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 838 |
url = "https://api.coingecko.com/api/v3/coins/bitcoin/market_chart/range"
|
| 839 |
-
params = {"vs_currency": "usd", "from":
|
| 840 |
headers = {"accept": "application/json", "User-Agent": "Mozilla/5.0"}
|
| 841 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 842 |
try:
|
| 843 |
-
|
| 844 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 845 |
|
| 846 |
-
if
|
| 847 |
-
st.
|
| 848 |
else:
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
|
| 871 |
-
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
|
| 875 |
-
|
| 876 |
-
|
| 877 |
-
|
| 878 |
-
|
| 879 |
-
|
| 880 |
-
|
| 881 |
-
|
| 882 |
-
|
| 883 |
-
|
| 884 |
-
|
| 885 |
-
|
| 886 |
-
|
| 887 |
-
|
| 888 |
-
|
| 889 |
-
|
| 890 |
-
|
| 891 |
-
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
|
| 895 |
-
|
| 896 |
-
|
| 897 |
-
|
| 898 |
-
|
| 899 |
-
|
| 900 |
-
|
| 901 |
-
|
| 902 |
-
|
| 903 |
-
|
| 904 |
-
|
| 905 |
-
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
|
| 909 |
-
|
| 910 |
-
|
| 911 |
-
|
| 912 |
-
|
| 913 |
-
|
| 914 |
-
|
| 915 |
-
|
| 916 |
-
|
| 917 |
-
|
| 918 |
-
|
| 919 |
-
|
| 920 |
-
|
| 921 |
-
|
| 922 |
-
|
| 923 |
-
|
| 924 |
-
|
| 925 |
-
|
| 926 |
-
|
| 927 |
-
|
| 928 |
-
|
| 929 |
-
|
| 930 |
-
|
| 931 |
-
|
| 932 |
-
|
| 933 |
-
|
| 934 |
-
|
| 935 |
-
|
| 936 |
-
|
| 937 |
-
|
| 938 |
-
|
| 939 |
-
|
| 940 |
-
|
| 941 |
-
|
| 942 |
-
|
| 943 |
-
|
| 944 |
-
|
| 945 |
-
|
| 946 |
-
|
| 947 |
-
|
| 948 |
-
|
| 949 |
-
|
| 950 |
-
|
| 951 |
-
|
| 952 |
-
|
| 953 |
-
|
| 954 |
-
|
| 955 |
-
|
| 956 |
-
|
| 957 |
-
|
| 958 |
-
|
| 959 |
-
|
| 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 |
-
|
| 990 |
-
except:
|
| 991 |
-
|
| 992 |
-
|
| 993 |
-
|
| 994 |
-
|
| 995 |
-
|
| 996 |
-
|
| 997 |
-
|
| 998 |
-
|
| 999 |
-
|
| 1000 |
-
|
| 1001 |
-
|
| 1002 |
-
|
| 1003 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1004 |
)
|
| 1005 |
-
|
| 1006 |
-
|
| 1007 |
-
|
| 1008 |
-
|
| 1009 |
-
|
| 1010 |
-
|
| 1011 |
-
|
| 1012 |
-
|
| 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(
|
| 1019 |
-
|
| 1020 |
-
|
| 1021 |
-
|
| 1022 |
-
|
| 1023 |
-
|
| 1024 |
-
|
| 1025 |
-
|
| 1026 |
-
|
| 1027 |
-
|
| 1028 |
-
|
| 1029 |
-
|
| 1030 |
-
|
| 1031 |
-
|
| 1032 |
-
|
| 1033 |
-
|
| 1034 |
-
|
| 1035 |
-
|
| 1036 |
-
|
| 1037 |
-
|
| 1038 |
-
|
| 1039 |
-
|
| 1040 |
-
|
| 1041 |
-
|
| 1042 |
-
|
| 1043 |
-
|
| 1044 |
-
|
| 1045 |
-
|
| 1046 |
-
|
| 1047 |
-
|
| 1048 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1049 |
)
|
| 1050 |
-
|
| 1051 |
-
|
| 1052 |
-
|
| 1053 |
-
|
| 1054 |
-
|
| 1055 |
-
|
| 1056 |
-
|
| 1057 |
-
|
| 1058 |
-
|
| 1059 |
-
|
| 1060 |
-
|
| 1061 |
-
|
| 1062 |
-
|
| 1063 |
-
|
| 1064 |
-
|
| 1065 |
-
|
| 1066 |
-
|
| 1067 |
-
|
| 1068 |
-
|
| 1069 |
-
|
| 1070 |
-
|
| 1071 |
-
|
| 1072 |
-
|
| 1073 |
-
|
| 1074 |
-
|
| 1075 |
-
|
| 1076 |
-
|
| 1077 |
-
|
| 1078 |
-
|
| 1079 |
-
|
| 1080 |
-
|
| 1081 |
-
|
| 1082 |
-
|
| 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 |
-
|
| 1117 |
-
|
| 1118 |
-
|
|
|
|
| 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
|
| 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.")
|