Varriety commited on
Commit
b7ef13e
Β·
verified Β·
1 Parent(s): bf1409f

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +237 -193
src/streamlit_app.py CHANGED
@@ -30,143 +30,200 @@ img_batch = os.path.join(BASE_DIR, "slice3-1-1536x830.png")
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
170
  # ==============================
171
  @st.cache_resource
172
  def download_nltk_resources():
@@ -178,22 +235,19 @@ def download_nltk_resources():
178
  download_nltk_resources()
179
  stop_words = set(stopwords.words('english'))
180
 
181
- # ==============================
182
- # LOAD MODELS
183
- # ==============================
184
  @st.cache_resource
185
  def load_all_models():
186
- with st.spinner('Memuat model NLP...'):
187
- vader = SentimentIntensityAnalyzer()
188
- bertweet = pipeline("sentiment-analysis", model="finiteautomata/bertweet-base-sentiment-analysis", device=-1, truncation=True, max_length=128)
189
- roberta = pipeline("sentiment-analysis", model="cardiffnlp/twitter-roberta-base-sentiment", device=-1, truncation=True, max_length=512)
190
- roberta_large = pipeline("sentiment-analysis", model="siebert/sentiment-roberta-large-english", device=-1, truncation=True, max_length=512)
191
- return vader, bertweet, roberta, roberta_large
192
 
193
- vader, bertweet, roberta, roberta_large = load_all_models()
 
194
 
195
  # ==============================
196
- # CLEAN TEXT & MAPPING
197
  # ==============================
198
  def clean_text(text):
199
  text = str(text).lower()
@@ -221,50 +275,55 @@ def get_daily_label(score):
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>
239
  """, unsafe_allow_html=True)
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:
249
  st.image(img_hero, use_container_width=True)
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")
 
256
  try:
257
  if detect(user_input) != 'en':
258
- st.warning("⚠️ Peringatan: Teks sepertinya bukan bahasa Inggris. Hasil mungkin bias.")
259
  except:
260
  pass
261
 
262
  text = clean_text(user_input)
263
 
264
- with st.spinner("Menganalisis..."):
265
- try:
266
- v_score = vader.polarity_scores(text)['compound']
267
- v_label = "positive" if v_score > 0.05 else "negative" if v_score < -0.05 else "neutral"
268
  except: v_label = "neutral"
269
 
270
  try: t_label = classify_tb(TextBlob(text).sentiment.polarity)
@@ -285,55 +344,57 @@ if st.session_state.page == "uji_kalimat":
285
  return "βšͺ Neutral"
286
 
287
  data_test = {
288
- "Metode": ["VADER", "TextBlob", "BERTweet", "RoBERTa Base", "RoBERTa Large"],
289
- "Sentimen": [format_label(v_label), format_label(t_label), format_label(b_label), format_label(r_label), format_label(rl_label)]
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:
313
- try:
314
- st.image(img_batch, use_container_width=True)
315
- except Exception:
316
- st.empty()
317
 
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
 
326
  tweet_files = st.file_uploader("Pilih file Tweet (.txt)", type=['txt'], accept_multiple_files=True)
327
 
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)
338
 
339
  data = []
@@ -367,9 +428,7 @@ elif st.session_state.page == "analisis_batch":
367
  short_date = date_val[:10]
368
  text = clean_text(text_raw)
369
 
370
- try:
371
- v_score = vader.polarity_scores(text)['compound']
372
- vader_label = "positive" if v_score > 0.05 else "negative" if v_score < -0.05 else "neutral"
373
  except: vader_label = "neutral"
374
 
375
  try: tb_label = classify_tb(TextBlob(text).sentiment.polarity)
@@ -401,11 +460,14 @@ elif st.session_state.page == "analisis_batch":
401
  df = pd.DataFrame(data)
402
 
403
  if df.empty:
404
- st.error("❌ DataFrame kosong. Pastikan format TXT benar dan menggunakan bahasa Inggris.")
405
  else:
406
- col_metric1, col_metric2 = st.columns(2)
407
- col_metric1.metric("Total Tweet Berhasil Diproses", f"{total_tweets_uploaded} Tweets")
408
- col_metric2.metric("Total Tweet Dihapus (Non-EN/Invalid)", f"{total_tweets_skipped} Tweets")
 
 
 
409
 
410
  target_dates = sorted(df['date'].unique())
411
  start_unix = int(datetime.strptime(target_dates[0], "%Y-%m-%d").replace(tzinfo=timezone.utc).timestamp()) - 86400
@@ -440,14 +502,12 @@ elif st.session_state.page == "analisis_batch":
440
  df_price = df_price[df_price["date"].isin(pd.to_datetime(target_dates).date)]
441
 
442
  if df_price.empty:
443
- st.warning("⚠️ Data Harga API kosong setelah di-filter. Pastikan format penulisan tanggal di .txt sesuai (yyyy-mm-dd).")
444
  else:
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}
@@ -466,40 +526,30 @@ elif st.session_state.page == "analisis_batch":
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("---")
482
- st.markdown("### πŸ—‚οΈ Dataset Final (Tabel Terintegrasi Siap Uji)")
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")
489
 
490
  buffer = io.BytesIO()
491
  with pd.ExcelWriter(buffer, engine='xlsxwriter') as writer:
492
  df_merged.to_excel(writer, index=False)
493
- col_dl2.download_button("πŸ“₯ Download Excel", data=buffer.getvalue(), file_name="sentiment_volatility.xlsx", mime="application/vnd.ms-excel")
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"):
501
- st.write("- Jika `p-value < 0.05` maka korelasi dianggap **Signifikan** (Terdapat hubungan).")
502
- st.write("- Jika `p-value >= 0.05` maka korelasi dianggap **Tidak Signifikan**.")
503
 
504
  corr_data = []
505
  raw_corr_results = []
@@ -516,33 +566,31 @@ elif st.session_state.page == "analisis_batch":
516
  "p-value": f"{pval:.4f}",
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
 
528
  fig_line, ax_line = plt.subplots(figsize=(14, 6))
529
 
530
  ax_line.plot(df_merged["date"], df_merged["log_return"], label="BTC Log Return", color="#F7931A", linewidth=3, linestyle="-")
531
 
532
- colors = ["#4F46E5", "#10B981", "#F472B6", "#14B8A6", "#6366F1"]
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
 
547
  cols = st.columns(3)
548
  models_list = ["vader", "textblob", "bertweet", "roberta", "roberta_large"]
@@ -551,21 +599,17 @@ elif st.session_state.page == "analisis_batch":
551
  with cols[idx % 3]:
552
  fig_scatter, ax_scatter = plt.subplots(figsize=(5, 4))
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
 
562
- with st.expander("πŸ“– Panduan Membaca Grafik"):
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
 
570
  max_volatility_idx = df_merged["log_return"].idxmax()
571
  min_volatility_idx = df_merged["log_return"].idxmin()
@@ -576,17 +620,17 @@ elif st.session_state.page == "analisis_batch":
576
  strongest_model = max(raw_corr_results, key=lambda x: abs(x["r"]))
577
  arah_text = "berbanding lurus (positif)" if strongest_model["r"] > 0 else "berbanding terbalik (negatif)"
578
 
579
- st.markdown(f"Puncak lonjakan positif (*maximum log return*) terjadi pada **{date_max}**, sedangkan penurunan tajam paling ekstrem (*minimum log return*) terjadi pada **{date_min}**.")
580
 
581
  if len(sig_models) > 0:
582
  st.success(f"""
583
- βœ… **Hipotesis Diterima (H1):** Ditemukan korelasi linier yang signifikan pada metode **{', '.join(sig_models)}** (*p-value* < 0.05).
584
  Metode dengan pemetaan respons pasar terkuat adalah **{strongest_model['metode']}**, dengan sifat hubungan **{arah_text}**.
585
  """)
586
  else:
587
  st.warning("""
588
- ⚠️ **Hipotesis Ditolak (H0 Diterima):** Tidak ditemukan bukti empiris korelasi linier yang signifikan (seluruh *p-value* >= 0.05). Volatilitas harga Bitcoin pada rentang ini cenderung dipengaruhi oleh metrik atau faktor fundamental lain di luar sentimen platform X.
589
  """)
590
 
591
  except Exception as e:
592
- st.error(f"❌ Terjadi kesalahan saat memproses data/API: {e}")
 
30
  # ==============================
31
  # KONFIGURASI HALAMAN & STATE NAVIGASI
32
  # ==============================
33
+ st.set_page_config(page_title="SKRIPSI - Sentimen BTC", page_icon="β‚Ώ", layout="wide", initial_sidebar_state="collapsed")
34
 
35
  if 'page' not in st.session_state:
36
  st.session_state.page = "uji_kalimat"
37
 
38
  # ==============================
39
+ # GLOBAL CSS (MINIMALIST & PROFESSIONAL)
40
+ # ==============================
41
+ st.markdown("""
42
+ <style>
43
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
44
+
45
+ /* Menyembunyikan elemen default Streamlit agar terlihat seperti website asli */
46
+ #MainMenu {visibility: hidden;}
47
+ footer {visibility: hidden;}
48
+ header {visibility: hidden;}
49
+
50
+ /* Mengatur padding utama halaman */
51
+ .block-container {
52
+ padding-top: 2rem !important;
53
+ padding-bottom: 2rem !important;
54
+ max-width: 1200px !important;
55
+ }
56
+
57
+ /* Global Font */
58
+ html, body, [class*="css"] {
59
+ font-family: 'Inter', sans-serif !important;
60
+ }
61
+
62
+ /* Styling Navbar Container */
63
+ .nav-container {
64
+ display: flex;
65
+ justify-content: space-between;
66
+ align-items: center;
67
+ padding-bottom: 1.5rem;
68
+ margin-bottom: 2rem;
69
+ border-bottom: 1px solid rgba(128, 128, 128, 0.2);
70
+ }
71
+
72
+ /* Styling Tombol Standar Streamlit menjadi Minimalis */
73
+ div[data-testid="stButton"] > button {
74
+ border-radius: 30px;
75
+ font-weight: 600;
76
+ padding: 0.5rem 1.5rem;
77
+ transition: all 0.3s ease;
78
+ border: 1px solid transparent;
79
+ }
80
+
81
+ /* Menghilangkan border focus merah default Streamlit */
82
+ div[data-testid="stButton"] > button:focus:not(:active) {
83
+ border-color: transparent;
84
+ box-shadow: none;
85
+ color: inherit;
86
+ }
87
+ </style>
88
+ """, unsafe_allow_html=True)
89
+
90
+ # ==============================
91
+ # TEMA DINAMIS PER HALAMAN
92
  # ==============================
93
  if st.session_state.page == "uji_kalimat":
94
+ # TEMA DARK/ORANGE (Premium Landing Page Look)
95
  st.markdown("""
96
  <style>
 
 
97
  .stApp {
98
+ background-color: #0F172A; /* Dark Slate Minimalist */
99
+ color: #F8FAFC;
100
+ }
101
+ h1, h2, h3, h4, h5, h6, p, label {
102
+ color: #F8FAFC !important;
103
  }
104
 
105
+ /* Tombol Aksi Utama - Orange Bitcoin */
106
+ div[data-testid="stButton"] > button {
107
+ background-color: #F7931A !important;
108
  color: #FFFFFF !important;
109
+ border: none;
110
+ box-shadow: 0 4px 6px rgba(247, 147, 26, 0.2);
111
  }
112
+ div[data-testid="stButton"] > button:hover {
113
+ background-color: #E68310 !important;
114
+ transform: translateY(-2px);
115
+ }
116
+
117
+ /* Navigasi Button di Halaman 1 */
118
+ .nav-btn-active > button {
119
+ background-color: transparent !important;
120
+ border: 2px solid #F7931A !important;
121
+ color: #F7931A !important;
122
+ }
123
+ .nav-btn-inactive > button {
124
+ background-color: transparent !important;
125
+ border: 1px solid #475569 !important;
126
+ color: #94A3B8 !important;
127
  }
128
 
129
+ /* Styling Text Area Input */
130
+ .stTextArea textarea {
131
+ background-color: #1E293B !important;
132
+ color: #F8FAFC !important;
133
+ border: 1px solid #334155 !important;
134
+ border-radius: 12px;
135
+ font-size: 1rem;
136
+ padding: 1rem;
137
  }
138
+ .stTextArea textarea:focus {
139
+ border-color: #F7931A !important;
140
+ box-shadow: 0 0 0 1px #F7931A !important;
141
  }
142
  </style>
143
  """, unsafe_allow_html=True)
 
144
  else:
145
+ # TEMA TERANG (Dashboard Analisis Clean)
146
  st.markdown("""
147
  <style>
 
 
148
  .stApp {
149
+ background-color: #FFFFFF;
150
+ color: #0F172A;
 
151
  }
 
152
  h1, h2, h3, h4, h5, h6, label {
153
+ color: #0F172A !important;
154
  }
 
155
  p, span {
156
+ color: #475569 !important;
157
  }
158
 
159
+ /* Tombol Aksi Utama - Dark Navy */
160
+ div[data-testid="stButton"] > button {
161
+ background-color: #0F172A !important;
 
 
162
  color: #FFFFFF !important;
163
  border: none;
 
 
 
164
  }
165
+ div[data-testid="stButton"] > button:hover {
166
+ background-color: #1E293B !important;
167
+ transform: translateY(-2px);
168
+ }
169
+
170
+ /* Navigasi Button di Halaman 2 */
171
+ .nav-btn-inactive > button {
172
+ background-color: transparent !important;
173
+ border: 1px solid #CBD5E1 !important;
174
+ color: #64748B !important;
175
+ }
176
+ .nav-btn-active > button {
177
+ background-color: transparent !important;
178
+ border: 2px solid #0F172A !important;
179
+ color: #0F172A !important;
180
  }
181
 
182
+ /* Styling Dataframe / Table */
183
+ div[data-testid="stDataFrame"] {
184
+ border: 1px solid #E2E8F0;
185
+ border-radius: 12px;
186
+ overflow: hidden;
187
+ }
188
  </style>
189
  """, unsafe_allow_html=True)
190
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  # ==============================
192
+ # HEADER & NAVIGASI
193
  # ==============================
194
  def set_page(page_name):
195
  st.session_state.page = page_name
196
 
197
+ # Menggunakan Columns untuk Navbar yang rapi
198
+ col_logo, col_space, col_nav1, col_nav2 = st.columns([3, 4, 1.5, 1.5])
199
+
200
  with col_logo:
201
+ # Logo text
202
+ color_logo = "#F8FAFC" if st.session_state.page == "uji_kalimat" else "#0F172A"
203
+ st.markdown(f"<h3 style='margin:0; padding:10px 0; font-weight:700; color:{color_logo} !important;'>β‚Ώitcoin Sentimen</h3>", unsafe_allow_html=True)
204
 
205
  with col_nav1:
206
+ btn_class = "nav-btn-active" if st.session_state.page == "uji_kalimat" else "nav-btn-inactive"
207
+ st.markdown(f'<div class="{btn_class}">', unsafe_allow_html=True)
208
+ if st.button("Uji Kalimat πŸ“", use_container_width=True, key="nav_uji"):
209
  set_page("uji_kalimat")
210
  st.rerun()
211
  st.markdown('</div>', unsafe_allow_html=True)
212
 
213
  with col_nav2:
214
+ btn_class = "nav-btn-active" if st.session_state.page == "analisis_batch" else "nav-btn-inactive"
215
+ st.markdown(f'<div class="{btn_class}">', unsafe_allow_html=True)
216
+ if st.button("Analisis Batch πŸ“Š", use_container_width=True, key="nav_batch"):
217
  set_page("analisis_batch")
218
  st.rerun()
219
  st.markdown('</div>', unsafe_allow_html=True)
220
 
221
+ # Divider Navbar
222
+ st.markdown("<div style='border-bottom: 1px solid rgba(128,128,128,0.2); margin-top: 10px; margin-bottom: 40px;'></div>", unsafe_allow_html=True)
223
+
224
 
225
  # ==============================
226
+ # DOWNLOAD RESOURCES & LOAD MODELS (Tetap Sama)
227
  # ==============================
228
  @st.cache_resource
229
  def download_nltk_resources():
 
235
  download_nltk_resources()
236
  stop_words = set(stopwords.words('english'))
237
 
 
 
 
238
  @st.cache_resource
239
  def load_all_models():
240
+ vader = SentimentIntensityAnalyzer()
241
+ bertweet = pipeline("sentiment-analysis", model="finiteautomata/bertweet-base-sentiment-analysis", device=-1, truncation=True, max_length=128)
242
+ roberta = pipeline("sentiment-analysis", model="cardiffnlp/twitter-roberta-base-sentiment", device=-1, truncation=True, max_length=512)
243
+ roberta_large = pipeline("sentiment-analysis", model="siebert/sentiment-roberta-large-english", device=-1, truncation=True, max_length=512)
244
+ return vader, bertweet, roberta, roberta_large
 
245
 
246
+ with st.spinner('Mempersiapkan model NLP...'):
247
+ vader, bertweet, roberta, roberta_large = load_all_models()
248
 
249
  # ==============================
250
+ # FUNGSI CLEAN TEXT & MAPPING (Tetap Sama)
251
  # ==============================
252
  def clean_text(text):
253
  text = str(text).lower()
 
275
  elif score < -0.05: return 'Negative'
276
  else: return 'Neutral'
277
 
278
+
279
  # ==============================================================================
280
+ # HALAMAN 1: UJI KALIMAT (TEMA DARK/ORANGE)
281
  # ==============================================================================
282
  if st.session_state.page == "uji_kalimat":
283
+ col_text, col_img = st.columns([1.1, 1], gap="large")
284
 
285
  with col_text:
286
  st.markdown("""
287
+ <div style="padding-top: 2rem;">
288
+ <h1 style="font-size: 3.5rem; line-height: 1.1; margin-bottom: 1.5rem; font-weight: 700; letter-spacing: -1px;">
289
+ Bitcoin Volatility <br><span style="color: #F7931A;">vs Public Sentiment</span>
290
+ </h1>
291
+ <p style='font-size: 1.15rem; font-weight: 400; color: #94A3B8 !important; margin-bottom: 2rem;'>
292
+ Analisis Volatilitas Harga Bitcoin Terhadap Sentimen Publik Pada Platform X Berbasis Python.
293
  </p>
294
+ <div style="background-color: rgba(247, 147, 26, 0.1); border-left: 4px solid #F7931A; padding: 15px; border-radius: 4px; margin-bottom: 2rem;">
295
+ <p style="margin: 0; font-size: 0.95rem; color: #E2E8F0 !important;">
296
+ <b>Peneliti:</b> Arya Galuh Saputra (H1D022022)
297
+ </p>
298
+ </div>
299
  </div>
300
  """, unsafe_allow_html=True)
301
 
302
+ user_input = st.text_area("Masukkan Tweet (Bahasa Inggris):", "Great, Bitcoin just crashed another 10% today.", height=120)
303
 
304
+ st.markdown("<br>", unsafe_allow_html=True)
305
+ analyze_btn = st.button("πŸš€ Analisis Sentimen Sekarang", use_container_width=True)
 
306
 
307
  with col_img:
308
  try:
309
  st.image(img_hero, use_container_width=True)
310
+ except Exception:
311
+ st.info(f"Visualisasi Hero akan muncul di sini. (Pastikan file {os.path.basename(img_hero)} tersedia)")
312
 
313
  if analyze_btn:
314
+ st.markdown("<br><hr style='border-color: #334155;'><br>", unsafe_allow_html=True)
315
+ st.markdown("<h3 style='text-align: center; margin-bottom: 2rem;'>πŸ“‹ Hasil Deteksi Sentimen</h3>", unsafe_allow_html=True)
316
+
317
  try:
318
  if detect(user_input) != 'en':
319
+ st.warning("⚠️ Teks sepertinya bukan bahasa Inggris. Hasil prediksi mungkin memiliki bias.")
320
  except:
321
  pass
322
 
323
  text = clean_text(user_input)
324
 
325
+ with st.spinner("Mesin NLP sedang memproses..."):
326
+ try: v_label = "positive" if vader.polarity_scores(text)['compound'] > 0.05 else "negative" if vader.polarity_scores(text)['compound'] < -0.05 else "neutral"
 
 
327
  except: v_label = "neutral"
328
 
329
  try: t_label = classify_tb(TextBlob(text).sentiment.polarity)
 
344
  return "βšͺ Neutral"
345
 
346
  data_test = {
347
+ "Metode NLP": ["VADER", "TextBlob", "BERTweet", "RoBERTa Base", "RoBERTa Large"],
348
+ "Hasil Sentimen": [format_label(v_label), format_label(t_label), format_label(b_label), format_label(r_label), format_label(rl_label)]
349
  }
350
+
351
+ col_tabel, _ = st.columns([2, 1])
352
+ with col_tabel:
353
+ st.dataframe(pd.DataFrame(data_test), use_container_width=True, hide_index=True)
354
 
355
 
356
  # ==============================================================================
357
+ # HALAMAN 2: ANALISIS BATCH DATA (TEMA TERANG/KREM CLEAN)
358
  # ==============================================================================
359
  elif st.session_state.page == "analisis_batch":
360
+ # Tema Matplotlib
361
  plt.style.use('default')
362
  sns.set_theme(style="whitegrid", rc={
363
+ "axes.facecolor": "#F8FAFC",
364
+ "figure.facecolor": "#FFFFFF",
365
+ "axes.edgecolor": "#E2E8F0",
366
+ "text.color": "#0F172A",
367
+ "xtick.color": "#64748B",
368
+ "ytick.color": "#64748B",
369
+ "grid.color": "#F1F5F9"
370
  })
371
 
372
+ col_upload, col_img_batch = st.columns([1.5, 1], gap="large")
 
 
 
 
 
 
373
 
374
  with col_upload:
375
  st.markdown("""
376
+ <div style="padding-top: 1rem;">
377
+ <h2 style="font-size: 2.5rem; margin-bottom: 0.5rem; font-weight: 700; letter-spacing: -0.5px;">Analisis Batch Data</h2>
378
+ <p style='font-size: 1.1rem; margin-bottom: 2rem;'>Unggah file rekam jejak tweet (.txt) untuk diekstraksi dan dianalisis secara masal terhadap volatilitas pasar.</p>
379
  </div>
380
  """, unsafe_allow_html=True)
381
 
382
  tweet_files = st.file_uploader("Pilih file Tweet (.txt)", type=['txt'], accept_multiple_files=True)
383
 
384
+ with st.expander("πŸ“Œ Format TXT yang Didukung"):
385
  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")
386
 
387
+ st.markdown("<br>", unsafe_allow_html=True)
388
+ analyze_batch_btn = st.button("βš™οΈ Mulai Eksekusi Analisis", key="batch_btn")
389
+
390
+ with col_img_batch:
391
+ try:
392
+ st.image(img_batch, use_container_width=True)
393
+ except Exception:
394
+ st.empty()
395
 
396
  if tweet_files and analyze_batch_btn:
397
+ st.markdown("<hr style='border-color: #E2E8F0; margin: 3rem 0;'>", unsafe_allow_html=True)
398
  tweet_files = sorted(tweet_files, key=lambda x: x.name)
399
 
400
  data = []
 
428
  short_date = date_val[:10]
429
  text = clean_text(text_raw)
430
 
431
+ try: v_score = vader.polarity_scores(text)['compound']; vader_label = "positive" if v_score > 0.05 else "negative" if v_score < -0.05 else "neutral"
 
 
432
  except: vader_label = "neutral"
433
 
434
  try: tb_label = classify_tb(TextBlob(text).sentiment.polarity)
 
460
  df = pd.DataFrame(data)
461
 
462
  if df.empty:
463
+ st.error("❌ Data kosong. Pastikan format penulisan TXT benar dan tweet berbahasa Inggris.")
464
  else:
465
+ # Metrics Dashboard
466
+ st.markdown("### πŸ“Š Ringkasan Pemrosesan")
467
+ col_metric1, col_metric2, col_metric3 = st.columns(3)
468
+ col_metric1.metric("Tweet Diproses", f"{total_tweets_uploaded}", border=True)
469
+ col_metric2.metric("Tweet Diabaikan (Non-EN)", f"{total_tweets_skipped}", border=True)
470
+ col_metric3.metric("Total Model", "5 NLP Models", border=True)
471
 
472
  target_dates = sorted(df['date'].unique())
473
  start_unix = int(datetime.strptime(target_dates[0], "%Y-%m-%d").replace(tzinfo=timezone.utc).timestamp()) - 86400
 
502
  df_price = df_price[df_price["date"].isin(pd.to_datetime(target_dates).date)]
503
 
504
  if df_price.empty:
505
+ st.warning("⚠️ Data Harga API kosong. Pastikan rentang tanggal di .txt sesuai (yyyy-mm-dd).")
506
  else:
507
+ st.markdown("<hr style='border-color: #E2E8F0; margin: 3rem 0;'>", unsafe_allow_html=True)
 
508
 
509
  st.markdown("#### πŸ—£οΈ Data Sentimen Mentah")
510
+ raw_display_cols = ["date", "raw_tweet", "vader", "textblob", "bertweet", "roberta", "roberta_large"]
 
511
  st.dataframe(df[raw_display_cols], use_container_width=True, hide_index=True)
512
 
513
  sentiment_map = {"positive": 1, "neutral": 0, "negative": -1}
 
526
  for col in models:
527
  daily_display_cols.extend([col, f"{col}_label"])
528
 
529
+ st.markdown("<br>#### β‚Ώ Historis Harga & Volatilitas Bitcoin", unsafe_allow_html=True)
 
 
 
 
 
 
 
530
  st.dataframe(df_price[["date", "price", "pct_change", "log_return"]], use_container_width=True, hide_index=True)
531
 
532
  df_merged = pd.merge(df_price, df_sentiment_daily, on="date", how="inner")
533
 
534
+ st.markdown("<br>### πŸ—‚οΈ Dataset Final (Terintegrasi)", unsafe_allow_html=True)
 
535
  final_display_cols = ["date", "price", "pct_change", "log_return"] + [c for c in daily_display_cols if c != "date"]
536
  st.dataframe(df_merged[final_display_cols], use_container_width=True, hide_index=True)
537
 
538
+ # Tombol Download
539
+ col_dl1, col_dl2, _ = st.columns([1, 1, 3])
540
  csv_data = df_merged.to_csv(index=False).encode('utf-8')
541
+ col_dl1.download_button("πŸ“₯ Unduh CSV", data=csv_data, file_name="sentiment_volatility.csv", mime="text/csv", use_container_width=True)
542
 
543
  buffer = io.BytesIO()
544
  with pd.ExcelWriter(buffer, engine='xlsxwriter') as writer:
545
  df_merged.to_excel(writer, index=False)
546
+ col_dl2.download_button("πŸ“₯ Unduh Excel", data=buffer.getvalue(), file_name="sentiment_volatility.xlsx", mime="application/vnd.ms-excel", use_container_width=True)
547
 
548
+ st.markdown("<hr style='border-color: #E2E8F0; margin: 3rem 0;'>", unsafe_allow_html=True)
549
 
550
  # UJI KORELASI PEARSON
551
+ st.subheader("πŸ”¬ Uji Korelasi Pearson")
552
+ st.caption("Menganalisis hubungan statistik antara skor sentimen harian dan volatilitas log-return BTC.")
 
 
 
553
 
554
  corr_data = []
555
  raw_corr_results = []
 
566
  "p-value": f"{pval:.4f}",
567
  "Status": signifikansi
568
  })
 
569
  raw_corr_results.append({"metode": method.upper(), "r": corr, "p": pval})
570
 
571
  st.table(pd.DataFrame(corr_data))
572
 
573
  # LINE CHART
574
+ st.markdown("<br>", unsafe_allow_html=True)
575
  st.subheader("πŸ“ˆ Trend Analisis: Sentiment vs BTC Volatility")
576
 
577
  fig_line, ax_line = plt.subplots(figsize=(14, 6))
578
 
579
  ax_line.plot(df_merged["date"], df_merged["log_return"], label="BTC Log Return", color="#F7931A", linewidth=3, linestyle="-")
580
 
581
+ colors = ["#3B82F6", "#10B981", "#EC4899", "#14B8A6", "#6366F1"]
582
  for idx, method in enumerate(["vader", "textblob", "roberta", "roberta_large", "bertweet"]):
583
  ax_line.plot(df_merged["date"], df_merged[method], label=f"Sentiment: {method.upper()}", color=colors[idx], linewidth=1.5, linestyle="--", alpha=0.8)
584
 
585
+ ax_line.set_title("Pergerakan Sentimen vs Log Return Bitcoin", fontsize=14, pad=15, fontweight='bold')
586
+ ax_line.set_xlabel("Tanggal", fontsize=11)
587
+ ax_line.set_ylabel("Nilai Metrik", fontsize=11)
588
+ ax_line.legend(loc='upper left', bbox_to_anchor=(1, 1), frameon=True)
589
  plt.tight_layout()
590
  st.pyplot(fig_line)
591
 
592
  # SCATTER PLOT
593
+ st.markdown("<br>### πŸ”΅ Pola Distribusi Scatter", unsafe_allow_html=True)
 
594
 
595
  cols = st.columns(3)
596
  models_list = ["vader", "textblob", "bertweet", "roberta", "roberta_large"]
 
599
  with cols[idx % 3]:
600
  fig_scatter, ax_scatter = plt.subplots(figsize=(5, 4))
601
  sns.regplot(data=df_merged, x=method, y="log_return", ax=ax_scatter,
602
+ scatter_kws={"s": 40, "color": "#0F172A", "alpha": 0.5},
603
+ line_kws={"color": "#F7931A", "linewidth": 2})
604
+ ax_scatter.set_title(f"{method.upper()}", fontweight='bold')
605
+ ax_scatter.set_xlabel("Sentimen Score")
606
+ ax_scatter.set_ylabel("Log Return")
607
  plt.tight_layout()
608
  st.pyplot(fig_scatter)
609
 
 
 
 
 
610
  # KESIMPULAN
611
+ st.markdown("<hr style='border-color: #E2E8F0; margin: 3rem 0;'>", unsafe_allow_html=True)
612
+ st.subheader("πŸ“ Kesimpulan Otomatis")
613
 
614
  max_volatility_idx = df_merged["log_return"].idxmax()
615
  min_volatility_idx = df_merged["log_return"].idxmin()
 
620
  strongest_model = max(raw_corr_results, key=lambda x: abs(x["r"]))
621
  arah_text = "berbanding lurus (positif)" if strongest_model["r"] > 0 else "berbanding terbalik (negatif)"
622
 
623
+ st.write(f"Puncak lonjakan positif (*max log return*) terjadi pada **{date_max}**, sedangkan penurunan ekstrem terjadi pada **{date_min}**.")
624
 
625
  if len(sig_models) > 0:
626
  st.success(f"""
627
+ **Hipotesis Diterima (H1):** Ditemukan korelasi linier yang signifikan pada metode **{', '.join(sig_models)}** (*p-value* < 0.05).
628
  Metode dengan pemetaan respons pasar terkuat adalah **{strongest_model['metode']}**, dengan sifat hubungan **{arah_text}**.
629
  """)
630
  else:
631
  st.warning("""
632
+ **Hipotesis Ditolak (H0 Diterima):** Tidak ditemukan bukti empiris korelasi linier yang signifikan (seluruh *p-value* >= 0.05). Volatilitas harga cenderung dipengaruhi oleh faktor teknikal/fundamental di luar sentimen X.
633
  """)
634
 
635
  except Exception as e:
636
+ st.error(f"❌ Terjadi kesalahan sistem: {e}")