Update src/streamlit_app.py
Browse files- src/streamlit_app.py +167 -167
src/streamlit_app.py
CHANGED
|
@@ -28,115 +28,142 @@ img_hero = os.path.join(BASE_DIR, "crypto-currency-concept-830px.png")
|
|
| 28 |
img_batch = os.path.join(BASE_DIR, "slice3-1-1536x830.png")
|
| 29 |
|
| 30 |
# ==============================
|
| 31 |
-
# KONFIGURASI HALAMAN &
|
| 32 |
# ==============================
|
| 33 |
st.set_page_config(page_title="SKRIPSI - BTS", page_icon="βΏ", layout="wide")
|
| 34 |
|
| 35 |
-
st.
|
| 36 |
-
|
| 37 |
-
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
| 38 |
-
|
| 39 |
-
:root {
|
| 40 |
-
--bitcoin-orange: #F7931A;
|
| 41 |
-
--bitcoin-orange-hover: #E8830C;
|
| 42 |
-
--bg-main: #0B0E14;
|
| 43 |
-
--bg-secondary: #151A22;
|
| 44 |
-
--text-dark: #FFFFFF;
|
| 45 |
-
--text-muted: #9CA3AF;
|
| 46 |
-
--border-color: #2D3748;
|
| 47 |
-
}
|
| 48 |
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
.stTabs [aria-selected="true"] {
|
| 102 |
-
border-bottom: 3px solid var(--bitcoin-orange) !important;
|
| 103 |
-
color: var(--text-dark) !important;
|
| 104 |
-
font-weight: 700;
|
| 105 |
-
}
|
| 106 |
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
[data-testid="stMetricValue"] {
|
| 115 |
-
color: var(--bitcoin-orange);
|
| 116 |
-
font-weight: 700;
|
| 117 |
}
|
| 118 |
-
|
| 119 |
-
color:
|
| 120 |
-
|
|
|
|
|
|
|
|
|
|
| 121 |
}
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
margin: 2rem 0;
|
| 126 |
}
|
| 127 |
</style>
|
| 128 |
""", unsafe_allow_html=True)
|
| 129 |
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
|
| 141 |
# ==============================
|
| 142 |
# DOWNLOAD REQUIRED RESOURCES
|
|
@@ -194,28 +221,18 @@ def get_daily_label(score):
|
|
| 194 |
elif score < -0.05: return 'Negative'
|
| 195 |
else: return 'Neutral'
|
| 196 |
|
| 197 |
-
# ==============================
|
| 198 |
-
#
|
| 199 |
-
# ==============================
|
| 200 |
-
st.
|
| 201 |
-
|
| 202 |
-
tab1, tab2 = st.tabs(["π§ͺ Uji Kalimat", "π Analisis Batch Data"])
|
| 203 |
-
|
| 204 |
-
# ==============================
|
| 205 |
-
# TAB 1: UJI KALIMAT (Layout Web Hero)
|
| 206 |
-
# ==============================
|
| 207 |
-
with tab1:
|
| 208 |
-
st.markdown("<br>", unsafe_allow_html=True)
|
| 209 |
-
|
| 210 |
-
# Kolom teks & input di Kiri, Gambar di Kanan
|
| 211 |
col_text, col_img = st.columns([1.2, 1], gap="large")
|
| 212 |
|
| 213 |
with col_text:
|
| 214 |
st.markdown("""
|
| 215 |
<div style="padding-top: 0.5rem;">
|
| 216 |
-
<h1 style="font-size:
|
| 217 |
-
<p style='
|
| 218 |
-
<p style='
|
| 219 |
<b>Peneliti:</b> Arya Galuh Saputra (H1D022022)
|
| 220 |
</p>
|
| 221 |
</div>
|
|
@@ -223,12 +240,9 @@ with tab1:
|
|
| 223 |
|
| 224 |
user_input = st.text_area("Masukkan Tweet (Bahasa Inggris):", "Great, Bitcoin just crashed another 10% today.", height=130)
|
| 225 |
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
with col_info:
|
| 230 |
-
with st.expander("βΉοΈ Info Model NLP"):
|
| 231 |
-
st.caption("5 Metode: VADER, TextBlob, BERTweet, RoBERTa, dan RoBERTa Large.")
|
| 232 |
|
| 233 |
with col_img:
|
| 234 |
try:
|
|
@@ -236,7 +250,6 @@ with tab1:
|
|
| 236 |
except Exception as e:
|
| 237 |
st.error(f"β³ Menunggu gambar diunggah... (Pastikan file {img_hero} tersedia)")
|
| 238 |
|
| 239 |
-
# Menampilkan hasil di bawahnya secara full width jika tombol ditekan
|
| 240 |
if analyze_btn:
|
| 241 |
st.markdown("<hr>", unsafe_allow_html=True)
|
| 242 |
st.subheader("π Hasil Deteksi Sentimen")
|
|
@@ -277,13 +290,23 @@ with tab1:
|
|
| 277 |
}
|
| 278 |
st.dataframe(pd.DataFrame(data_test), use_container_width=True, hide_index=True)
|
| 279 |
|
| 280 |
-
|
| 281 |
-
#
|
| 282 |
-
#
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 287 |
col_img_batch, col_upload = st.columns([1, 1.5], gap="large")
|
| 288 |
|
| 289 |
with col_img_batch:
|
|
@@ -295,8 +318,8 @@ with tab2:
|
|
| 295 |
with col_upload:
|
| 296 |
st.markdown("""
|
| 297 |
<div style="padding-top: 1.5rem;">
|
| 298 |
-
<h2 style="font-size: 2.
|
| 299 |
-
<p style='
|
| 300 |
</div>
|
| 301 |
""", unsafe_allow_html=True)
|
| 302 |
|
|
@@ -305,9 +328,10 @@ with tab2:
|
|
| 305 |
with st.expander("π Lihat Format TXT yang Benar"):
|
| 306 |
st.code("username | 2024-03-01 14:00:00\nIsi tweet baris pertama di sini\n\nusername2 | 2024-03-01 15:30:00\nIsi tweet baris kedua di sini", language="text")
|
| 307 |
|
|
|
|
| 308 |
analyze_batch_btn = st.button("βοΈ Eksekusi Analisis", key="batch_btn")
|
|
|
|
| 309 |
|
| 310 |
-
# Bagian pemrosesan data, dieksekusi secara full-width di bawah layout gambar & form
|
| 311 |
if tweet_files and analyze_batch_btn:
|
| 312 |
st.markdown("---")
|
| 313 |
tweet_files = sorted(tweet_files, key=lambda x: x.name)
|
|
@@ -325,12 +349,10 @@ with tab2:
|
|
| 325 |
|
| 326 |
for tweet in tweets:
|
| 327 |
parts = tweet.strip().split("\n", 1)
|
| 328 |
-
if len(parts) != 2:
|
| 329 |
-
continue
|
| 330 |
|
| 331 |
meta, text_raw = parts
|
| 332 |
|
| 333 |
-
# FILTER BAHASA INGGRIS
|
| 334 |
try:
|
| 335 |
DetectorFactory.seed = 0
|
| 336 |
lang = detect(text_raw)
|
|
@@ -392,11 +414,7 @@ with tab2:
|
|
| 392 |
st.info("π‘ Mengambil data harga Bitcoin dari CoinGecko API...")
|
| 393 |
|
| 394 |
url = "https://api.coingecko.com/api/v3/coins/bitcoin/market_chart/range"
|
| 395 |
-
params = {
|
| 396 |
-
"vs_currency": "usd",
|
| 397 |
-
"from": start_unix,
|
| 398 |
-
"to": end_unix
|
| 399 |
-
}
|
| 400 |
headers = {"accept": "application/json", "User-Agent": "Mozilla/5.0"}
|
| 401 |
|
| 402 |
try:
|
|
@@ -427,13 +445,11 @@ with tab2:
|
|
| 427 |
st.markdown("---")
|
| 428 |
st.header("π Ringkasan Data")
|
| 429 |
|
| 430 |
-
# TABEL 1: SENTIMEN MENTAH
|
| 431 |
st.markdown("#### π£οΈ Data Sentimen Mentah")
|
| 432 |
st.caption("Tweet asli, hasil preprocessing, dan label prediksi dari 5 model sebelum konversi ke numerik.")
|
| 433 |
raw_display_cols = ["date", "raw_tweet", "cleaned_tweet", "vader", "textblob", "bertweet", "roberta", "roberta_large"]
|
| 434 |
st.dataframe(df[raw_display_cols], use_container_width=True, hide_index=True)
|
| 435 |
|
| 436 |
-
# KONVERSI LABEL SENTIMEN KE ANGKA
|
| 437 |
sentiment_map = {"positive": 1, "neutral": 0, "negative": -1}
|
| 438 |
df_score = df.copy()
|
| 439 |
for col in ["vader", "textblob", "bertweet", "roberta", "roberta_large"]:
|
|
@@ -450,19 +466,16 @@ with tab2:
|
|
| 450 |
for col in models:
|
| 451 |
daily_display_cols.extend([col, f"{col}_label"])
|
| 452 |
|
| 453 |
-
# TABEL 2: SKOR SENTIMEN HARIAN
|
| 454 |
st.markdown("<br>", unsafe_allow_html=True)
|
| 455 |
st.markdown("#### π’ Skor Sentimen Harian")
|
| 456 |
st.caption("Rata-rata skor sentimen harian yang dikonversi ke representasi metrik.")
|
| 457 |
st.dataframe(df_sentiment_daily[daily_display_cols], use_container_width=True, hide_index=True)
|
| 458 |
|
| 459 |
-
# TABEL 3: HARGA BITCOIN
|
| 460 |
st.markdown("<br>", unsafe_allow_html=True)
|
| 461 |
st.markdown("#### βΏ Historis Harga & Volatilitas Bitcoin")
|
| 462 |
st.caption("Data pergerakan rata-rata harga, persentase perubahan, dan Log Return (CoinGecko API).")
|
| 463 |
st.dataframe(df_price[["date", "price", "pct_change", "log_return"]], use_container_width=True, hide_index=True)
|
| 464 |
|
| 465 |
-
# MERGE KEDUA TABEL
|
| 466 |
df_merged = pd.merge(df_price, df_sentiment_daily, on="date", how="inner")
|
| 467 |
|
| 468 |
st.markdown("---")
|
|
@@ -470,7 +483,6 @@ with tab2:
|
|
| 470 |
final_display_cols = ["date", "price", "pct_change", "log_return"] + [c for c in daily_display_cols if c != "date"]
|
| 471 |
st.dataframe(df_merged[final_display_cols], use_container_width=True, hide_index=True)
|
| 472 |
|
| 473 |
-
# Export Buttons
|
| 474 |
col_dl1, col_dl2, _ = st.columns([1, 1, 2])
|
| 475 |
csv_data = df_merged.to_csv(index=False).encode('utf-8')
|
| 476 |
col_dl1.download_button("π₯ Download CSV", data=csv_data, file_name="sentiment_volatility.csv", mime="text/csv")
|
|
@@ -482,9 +494,7 @@ with tab2:
|
|
| 482 |
|
| 483 |
st.markdown("---")
|
| 484 |
|
| 485 |
-
# ==============================
|
| 486 |
# UJI KORELASI PEARSON
|
| 487 |
-
# ==============================
|
| 488 |
st.subheader("π¬ Uji Korelasi Pearson (Sentiment vs Log Return)")
|
| 489 |
|
| 490 |
with st.expander("π‘ Dasar Pengambilan Keputusan"):
|
|
@@ -507,17 +517,11 @@ with tab2:
|
|
| 507 |
"Status": signifikansi
|
| 508 |
})
|
| 509 |
|
| 510 |
-
raw_corr_results.append({
|
| 511 |
-
"metode": method.upper(),
|
| 512 |
-
"r": corr,
|
| 513 |
-
"p": pval
|
| 514 |
-
})
|
| 515 |
|
| 516 |
st.table(pd.DataFrame(corr_data))
|
| 517 |
|
| 518 |
-
# ==============================
|
| 519 |
# LINE CHART
|
| 520 |
-
# ==============================
|
| 521 |
st.markdown("---")
|
| 522 |
st.subheader("π Trend Analisis: Sentiment vs BTC Volatility")
|
| 523 |
|
|
@@ -529,16 +533,14 @@ with tab2:
|
|
| 529 |
for idx, method in enumerate(["vader", "textblob", "roberta", "roberta_large", "bertweet"]):
|
| 530 |
ax_line.plot(df_merged["date"], df_merged[method], label=f"Sentiment: {method.upper()}", color=colors[idx], linewidth=1.5, linestyle="--", alpha=0.8)
|
| 531 |
|
| 532 |
-
ax_line.set_title("Pergerakan Rata-Rata Sentimen Harian Terhadap Volatilitas Harga Bitcoin", color="#
|
| 533 |
-
ax_line.set_xlabel("Tanggal", color="#
|
| 534 |
-
ax_line.set_ylabel("Nilai (Value)", color="#
|
| 535 |
-
ax_line.legend(loc='upper left', bbox_to_anchor=(1, 1), frameon=True, facecolor='#
|
| 536 |
plt.tight_layout()
|
| 537 |
st.pyplot(fig_line)
|
| 538 |
|
| 539 |
-
#
|
| 540 |
-
# SCATTER PLOT + TRENDLINE
|
| 541 |
-
# ==============================
|
| 542 |
st.markdown("---")
|
| 543 |
st.subheader("π΅ Pola Distribusi (Scatter Plot & Trendline)")
|
| 544 |
|
|
@@ -551,9 +553,9 @@ with tab2:
|
|
| 551 |
sns.regplot(data=df_merged, x=method, y="log_return", ax=ax_scatter,
|
| 552 |
scatter_kws={"s": 50, "color": "#4F46E5", "alpha": 0.6},
|
| 553 |
line_kws={"color": "#F7931A", "linewidth": 2.5})
|
| 554 |
-
ax_scatter.set_title(f"{method.upper()} vs Log Return", color="#
|
| 555 |
-
ax_scatter.set_xlabel("Skor Sentimen", color="#
|
| 556 |
-
ax_scatter.set_ylabel("Log Return", color="#
|
| 557 |
plt.tight_layout()
|
| 558 |
st.pyplot(fig_scatter)
|
| 559 |
|
|
@@ -561,9 +563,7 @@ with tab2:
|
|
| 561 |
st.write("- **Garis Orange (Trendline):** Menunjukkan arah korelasi (Naik = Positif, Turun = Negatif).")
|
| 562 |
st.write("- **Titik Ungu:** Sebaran data, semakin merapat ke garis orange berarti korelasi semakin kuat.")
|
| 563 |
|
| 564 |
-
#
|
| 565 |
-
# KESIMPULAN & PEMBAHASAN AKHIR
|
| 566 |
-
# ==============================
|
| 567 |
st.markdown("---")
|
| 568 |
st.header("π Ekstraksi Kesimpulan")
|
| 569 |
|
|
|
|
| 28 |
img_batch = os.path.join(BASE_DIR, "slice3-1-1536x830.png")
|
| 29 |
|
| 30 |
# ==============================
|
| 31 |
+
# KONFIGURASI HALAMAN & STATE NAVIGASI
|
| 32 |
# ==============================
|
| 33 |
st.set_page_config(page_title="SKRIPSI - BTS", page_icon="βΏ", layout="wide")
|
| 34 |
|
| 35 |
+
if 'page' not in st.session_state:
|
| 36 |
+
st.session_state.page = "uji_kalimat"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
+
# ==============================
|
| 39 |
+
# INJEKSI CSS DINAMIS (TEMA HALAMAN)
|
| 40 |
+
# ==============================
|
| 41 |
+
if st.session_state.page == "uji_kalimat":
|
| 42 |
+
# TEMA ORANYE (Mirip Vancouver Bitcoin Landing Page)
|
| 43 |
+
st.markdown("""
|
| 44 |
+
<style>
|
| 45 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
| 46 |
+
|
| 47 |
+
.stApp {
|
| 48 |
+
background-color: #F38220; /* Oranye */
|
| 49 |
+
color: #FFFFFF;
|
| 50 |
+
font-family: 'Inter', sans-serif;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
h1, h2, h3, h4, h5, h6, p, span, label {
|
| 54 |
+
color: #FFFFFF !important;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
/* Styling Text Area & Inputs agar kontras di latar oranye */
|
| 58 |
+
.stTextArea textarea {
|
| 59 |
+
background-color: #FFFFFF;
|
| 60 |
+
color: #111827 !important;
|
| 61 |
+
border-radius: 8px;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
/* Tombol Aksi */
|
| 65 |
+
.action-btn > button {
|
| 66 |
+
background-color: #1E1E24;
|
| 67 |
+
color: #FFFFFF !important;
|
| 68 |
+
border: none;
|
| 69 |
+
border-radius: 25px;
|
| 70 |
+
padding: 0.6rem 2rem;
|
| 71 |
+
font-weight: 600;
|
| 72 |
+
}
|
| 73 |
+
.action-btn > button:hover {
|
| 74 |
+
background-color: #33333C;
|
| 75 |
+
}
|
| 76 |
+
</style>
|
| 77 |
+
""", unsafe_allow_html=True)
|
| 78 |
|
| 79 |
+
else:
|
| 80 |
+
# TEMA PUTIH/KREM (Mirip Vancouver Bitcoin Info Page)
|
| 81 |
+
st.markdown("""
|
| 82 |
+
<style>
|
| 83 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
| 84 |
+
|
| 85 |
+
.stApp {
|
| 86 |
+
background-color: #FFFDF5; /* Krem / Putih Gading */
|
| 87 |
+
color: #111827;
|
| 88 |
+
font-family: 'Inter', sans-serif;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
h1, h2, h3, h4, h5, h6, label {
|
| 92 |
+
color: #111827 !important;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
p, span {
|
| 96 |
+
color: #4B5563 !important;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
hr { border-color: #E5E7EB; }
|
| 100 |
+
|
| 101 |
+
/* Tombol Aksi */
|
| 102 |
+
.action-btn > button {
|
| 103 |
+
background-color: #312E81;
|
| 104 |
+
color: #FFFFFF !important;
|
| 105 |
+
border: none;
|
| 106 |
+
border-radius: 25px;
|
| 107 |
+
padding: 0.6rem 2rem;
|
| 108 |
+
font-weight: 600;
|
| 109 |
+
}
|
| 110 |
+
.action-btn > button:hover {
|
| 111 |
+
background-color: #4338CA;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
/* Expander Header Text */
|
| 115 |
+
.streamlit-expanderHeader { color: #111827 !important; }
|
| 116 |
+
</style>
|
| 117 |
+
""", unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
|
| 119 |
+
# CSS Global untuk Navigasi (Tetap sama di kedua tema)
|
| 120 |
+
st.markdown("""
|
| 121 |
+
<style>
|
| 122 |
+
.nav-container {
|
| 123 |
+
padding: 10px 0;
|
| 124 |
+
border-bottom: 2px solid rgba(255, 255, 255, 0.2);
|
| 125 |
+
margin-bottom: 20px;
|
|
|
|
|
|
|
|
|
|
| 126 |
}
|
| 127 |
+
.nav-btn > button {
|
| 128 |
+
background-color: transparent !important;
|
| 129 |
+
border: 2px solid currentColor !important;
|
| 130 |
+
border-radius: 20px !important;
|
| 131 |
+
font-weight: 600 !important;
|
| 132 |
+
transition: all 0.3s ease;
|
| 133 |
}
|
| 134 |
+
.nav-btn > button:hover {
|
| 135 |
+
opacity: 0.8;
|
| 136 |
+
transform: translateY(-2px);
|
|
|
|
| 137 |
}
|
| 138 |
</style>
|
| 139 |
""", unsafe_allow_html=True)
|
| 140 |
|
| 141 |
+
# ==============================
|
| 142 |
+
# FUNGSI NAVIGASI
|
| 143 |
+
# ==============================
|
| 144 |
+
def set_page(page_name):
|
| 145 |
+
st.session_state.page = page_name
|
| 146 |
+
|
| 147 |
+
# Header Navigasi (Sistem Berpindah Halaman)
|
| 148 |
+
col_logo, col_nav1, col_nav2, col_space = st.columns([2, 1.5, 1.5, 4])
|
| 149 |
+
with col_logo:
|
| 150 |
+
st.markdown("<h3 style='margin:0; padding:0;'>βΏitcoin Sentimen</h3>", unsafe_allow_html=True)
|
| 151 |
+
|
| 152 |
+
with col_nav1:
|
| 153 |
+
st.markdown('<div class="nav-btn">', unsafe_allow_html=True)
|
| 154 |
+
if st.button("Uji Kalimat π", use_container_width=True):
|
| 155 |
+
set_page("uji_kalimat")
|
| 156 |
+
st.rerun()
|
| 157 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 158 |
+
|
| 159 |
+
with col_nav2:
|
| 160 |
+
st.markdown('<div class="nav-btn">', unsafe_allow_html=True)
|
| 161 |
+
if st.button("Analisis Batch π", use_container_width=True):
|
| 162 |
+
set_page("analisis_batch")
|
| 163 |
+
st.rerun()
|
| 164 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 165 |
+
|
| 166 |
+
st.markdown("<div style='margin-bottom: 40px;'></div>", unsafe_allow_html=True)
|
| 167 |
|
| 168 |
# ==============================
|
| 169 |
# DOWNLOAD REQUIRED RESOURCES
|
|
|
|
| 221 |
elif score < -0.05: return 'Negative'
|
| 222 |
else: return 'Neutral'
|
| 223 |
|
| 224 |
+
# ==============================================================================
|
| 225 |
+
# HALAMAN 1: UJI KALIMAT (BACKGROUND ORANYE)
|
| 226 |
+
# ==============================================================================
|
| 227 |
+
if st.session_state.page == "uji_kalimat":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
col_text, col_img = st.columns([1.2, 1], gap="large")
|
| 229 |
|
| 230 |
with col_text:
|
| 231 |
st.markdown("""
|
| 232 |
<div style="padding-top: 0.5rem;">
|
| 233 |
+
<h1 style="font-size: 3rem; line-height: 1.2; margin-bottom: 1rem; color: white;">Bitcoin Volatility vs Public Sentiment</h1>
|
| 234 |
+
<p style='font-size: 1.15rem; font-weight: 500;'>Analisis Volatilitas Harga Bitcoin Terhadap Sentimen Publik Pada Platform X Berbasis Python</p>
|
| 235 |
+
<p style='font-size: 0.95rem; margin-top: 1.5rem; margin-bottom: 2rem; border-left: 4px solid white; padding-left: 10px;'>
|
| 236 |
<b>Peneliti:</b> Arya Galuh Saputra (H1D022022)
|
| 237 |
</p>
|
| 238 |
</div>
|
|
|
|
| 240 |
|
| 241 |
user_input = st.text_area("Masukkan Tweet (Bahasa Inggris):", "Great, Bitcoin just crashed another 10% today.", height=130)
|
| 242 |
|
| 243 |
+
st.markdown('<div class="action-btn">', unsafe_allow_html=True)
|
| 244 |
+
analyze_btn = st.button("π Analisis Sentimen", use_container_width=True)
|
| 245 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|
| 246 |
|
| 247 |
with col_img:
|
| 248 |
try:
|
|
|
|
| 250 |
except Exception as e:
|
| 251 |
st.error(f"β³ Menunggu gambar diunggah... (Pastikan file {img_hero} tersedia)")
|
| 252 |
|
|
|
|
| 253 |
if analyze_btn:
|
| 254 |
st.markdown("<hr>", unsafe_allow_html=True)
|
| 255 |
st.subheader("π Hasil Deteksi Sentimen")
|
|
|
|
| 290 |
}
|
| 291 |
st.dataframe(pd.DataFrame(data_test), use_container_width=True, hide_index=True)
|
| 292 |
|
| 293 |
+
|
| 294 |
+
# ==============================================================================
|
| 295 |
+
# HALAMAN 2: ANALISIS BATCH DATA (BACKGROUND PUTIH/KREM)
|
| 296 |
+
# ==============================================================================
|
| 297 |
+
elif st.session_state.page == "analisis_batch":
|
| 298 |
+
# Ubah tema grafik matplotlib agar sesuai dengan background terang
|
| 299 |
+
plt.style.use('default')
|
| 300 |
+
sns.set_theme(style="whitegrid", rc={
|
| 301 |
+
"axes.facecolor": "#FFFFFF",
|
| 302 |
+
"figure.facecolor": "#FFFDF5",
|
| 303 |
+
"axes.edgecolor": "#E5E7EB",
|
| 304 |
+
"text.color": "#111827",
|
| 305 |
+
"xtick.color": "#4B5563",
|
| 306 |
+
"ytick.color": "#4B5563",
|
| 307 |
+
"grid.color": "#F3F4F6"
|
| 308 |
+
})
|
| 309 |
+
|
| 310 |
col_img_batch, col_upload = st.columns([1, 1.5], gap="large")
|
| 311 |
|
| 312 |
with col_img_batch:
|
|
|
|
| 318 |
with col_upload:
|
| 319 |
st.markdown("""
|
| 320 |
<div style="padding-top: 1.5rem;">
|
| 321 |
+
<h2 style="font-size: 2.5rem; margin-bottom: 0.5rem; color: #111827;">π Analisis Batch Data</h2>
|
| 322 |
+
<p style='font-size: 1.1rem; margin-bottom: 1.5rem;'>Unggah file ekstensi .txt yang berisi history tweet untuk dianalisis secara masal.</p>
|
| 323 |
</div>
|
| 324 |
""", unsafe_allow_html=True)
|
| 325 |
|
|
|
|
| 328 |
with st.expander("π Lihat Format TXT yang Benar"):
|
| 329 |
st.code("username | 2024-03-01 14:00:00\nIsi tweet baris pertama di sini\n\nusername2 | 2024-03-01 15:30:00\nIsi tweet baris kedua di sini", language="text")
|
| 330 |
|
| 331 |
+
st.markdown('<div class="action-btn">', unsafe_allow_html=True)
|
| 332 |
analyze_batch_btn = st.button("βοΈ Eksekusi Analisis", key="batch_btn")
|
| 333 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 334 |
|
|
|
|
| 335 |
if tweet_files and analyze_batch_btn:
|
| 336 |
st.markdown("---")
|
| 337 |
tweet_files = sorted(tweet_files, key=lambda x: x.name)
|
|
|
|
| 349 |
|
| 350 |
for tweet in tweets:
|
| 351 |
parts = tweet.strip().split("\n", 1)
|
| 352 |
+
if len(parts) != 2: continue
|
|
|
|
| 353 |
|
| 354 |
meta, text_raw = parts
|
| 355 |
|
|
|
|
| 356 |
try:
|
| 357 |
DetectorFactory.seed = 0
|
| 358 |
lang = detect(text_raw)
|
|
|
|
| 414 |
st.info("π‘ Mengambil data harga Bitcoin dari CoinGecko API...")
|
| 415 |
|
| 416 |
url = "https://api.coingecko.com/api/v3/coins/bitcoin/market_chart/range"
|
| 417 |
+
params = {"vs_currency": "usd", "from": start_unix, "to": end_unix}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 418 |
headers = {"accept": "application/json", "User-Agent": "Mozilla/5.0"}
|
| 419 |
|
| 420 |
try:
|
|
|
|
| 445 |
st.markdown("---")
|
| 446 |
st.header("π Ringkasan Data")
|
| 447 |
|
|
|
|
| 448 |
st.markdown("#### π£οΈ Data Sentimen Mentah")
|
| 449 |
st.caption("Tweet asli, hasil preprocessing, dan label prediksi dari 5 model sebelum konversi ke numerik.")
|
| 450 |
raw_display_cols = ["date", "raw_tweet", "cleaned_tweet", "vader", "textblob", "bertweet", "roberta", "roberta_large"]
|
| 451 |
st.dataframe(df[raw_display_cols], use_container_width=True, hide_index=True)
|
| 452 |
|
|
|
|
| 453 |
sentiment_map = {"positive": 1, "neutral": 0, "negative": -1}
|
| 454 |
df_score = df.copy()
|
| 455 |
for col in ["vader", "textblob", "bertweet", "roberta", "roberta_large"]:
|
|
|
|
| 466 |
for col in models:
|
| 467 |
daily_display_cols.extend([col, f"{col}_label"])
|
| 468 |
|
|
|
|
| 469 |
st.markdown("<br>", unsafe_allow_html=True)
|
| 470 |
st.markdown("#### π’ Skor Sentimen Harian")
|
| 471 |
st.caption("Rata-rata skor sentimen harian yang dikonversi ke representasi metrik.")
|
| 472 |
st.dataframe(df_sentiment_daily[daily_display_cols], use_container_width=True, hide_index=True)
|
| 473 |
|
|
|
|
| 474 |
st.markdown("<br>", unsafe_allow_html=True)
|
| 475 |
st.markdown("#### βΏ Historis Harga & Volatilitas Bitcoin")
|
| 476 |
st.caption("Data pergerakan rata-rata harga, persentase perubahan, dan Log Return (CoinGecko API).")
|
| 477 |
st.dataframe(df_price[["date", "price", "pct_change", "log_return"]], use_container_width=True, hide_index=True)
|
| 478 |
|
|
|
|
| 479 |
df_merged = pd.merge(df_price, df_sentiment_daily, on="date", how="inner")
|
| 480 |
|
| 481 |
st.markdown("---")
|
|
|
|
| 483 |
final_display_cols = ["date", "price", "pct_change", "log_return"] + [c for c in daily_display_cols if c != "date"]
|
| 484 |
st.dataframe(df_merged[final_display_cols], use_container_width=True, hide_index=True)
|
| 485 |
|
|
|
|
| 486 |
col_dl1, col_dl2, _ = st.columns([1, 1, 2])
|
| 487 |
csv_data = df_merged.to_csv(index=False).encode('utf-8')
|
| 488 |
col_dl1.download_button("π₯ Download CSV", data=csv_data, file_name="sentiment_volatility.csv", mime="text/csv")
|
|
|
|
| 494 |
|
| 495 |
st.markdown("---")
|
| 496 |
|
|
|
|
| 497 |
# UJI KORELASI PEARSON
|
|
|
|
| 498 |
st.subheader("π¬ Uji Korelasi Pearson (Sentiment vs Log Return)")
|
| 499 |
|
| 500 |
with st.expander("π‘ Dasar Pengambilan Keputusan"):
|
|
|
|
| 517 |
"Status": signifikansi
|
| 518 |
})
|
| 519 |
|
| 520 |
+
raw_corr_results.append({"metode": method.upper(), "r": corr, "p": pval})
|
|
|
|
|
|
|
|
|
|
|
|
|
| 521 |
|
| 522 |
st.table(pd.DataFrame(corr_data))
|
| 523 |
|
|
|
|
| 524 |
# LINE CHART
|
|
|
|
| 525 |
st.markdown("---")
|
| 526 |
st.subheader("π Trend Analisis: Sentiment vs BTC Volatility")
|
| 527 |
|
|
|
|
| 533 |
for idx, method in enumerate(["vader", "textblob", "roberta", "roberta_large", "bertweet"]):
|
| 534 |
ax_line.plot(df_merged["date"], df_merged[method], label=f"Sentiment: {method.upper()}", color=colors[idx], linewidth=1.5, linestyle="--", alpha=0.8)
|
| 535 |
|
| 536 |
+
ax_line.set_title("Pergerakan Rata-Rata Sentimen Harian Terhadap Volatilitas Harga Bitcoin", color="#111827", fontsize=14, pad=15, fontweight='bold')
|
| 537 |
+
ax_line.set_xlabel("Tanggal", color="#4B5563", fontsize=11)
|
| 538 |
+
ax_line.set_ylabel("Nilai (Value)", color="#4B5563", fontsize=11)
|
| 539 |
+
ax_line.legend(loc='upper left', bbox_to_anchor=(1, 1), frameon=True, facecolor='#FFFFFF', edgecolor='#E5E7EB')
|
| 540 |
plt.tight_layout()
|
| 541 |
st.pyplot(fig_line)
|
| 542 |
|
| 543 |
+
# SCATTER PLOT
|
|
|
|
|
|
|
| 544 |
st.markdown("---")
|
| 545 |
st.subheader("π΅ Pola Distribusi (Scatter Plot & Trendline)")
|
| 546 |
|
|
|
|
| 553 |
sns.regplot(data=df_merged, x=method, y="log_return", ax=ax_scatter,
|
| 554 |
scatter_kws={"s": 50, "color": "#4F46E5", "alpha": 0.6},
|
| 555 |
line_kws={"color": "#F7931A", "linewidth": 2.5})
|
| 556 |
+
ax_scatter.set_title(f"{method.upper()} vs Log Return", color="#111827", fontweight='bold')
|
| 557 |
+
ax_scatter.set_xlabel("Skor Sentimen", color="#4B5563")
|
| 558 |
+
ax_scatter.set_ylabel("Log Return", color="#4B5563")
|
| 559 |
plt.tight_layout()
|
| 560 |
st.pyplot(fig_scatter)
|
| 561 |
|
|
|
|
| 563 |
st.write("- **Garis Orange (Trendline):** Menunjukkan arah korelasi (Naik = Positif, Turun = Negatif).")
|
| 564 |
st.write("- **Titik Ungu:** Sebaran data, semakin merapat ke garis orange berarti korelasi semakin kuat.")
|
| 565 |
|
| 566 |
+
# KESIMPULAN
|
|
|
|
|
|
|
| 567 |
st.markdown("---")
|
| 568 |
st.header("π Ekstraksi Kesimpulan")
|
| 569 |
|