import streamlit as st import pandas as pd import os import re import string from collections import Counter import plotly.graph_objects as go from datetime import datetime, timedelta from database import engine, get_tweet_count, get_latest_crawl_time from page_modules.table_utils import render_standard_table from timezone_utils import ( parse_dt_with_tz, parse_dt_with_source_tz, get_timezone_label, get_timezone_name, ) def parse_dt(series): return parse_dt_with_tz( series, st.session_state.get("user_timezone", "WIB (UTC+7)") ) def parse_crawled_dt(series): return parse_dt_with_source_tz( series, st.session_state.get("user_timezone", "WIB (UTC+7)"), os.getenv("APP_TIMEZONE", "Asia/Makassar") ) def format_dt(value): if value is None or pd.isna(value): return "Belum ada" try: tz_label = get_timezone_label( st.session_state.get("user_timezone", "WIB (UTC+7)") ) return f"{value.strftime('%d/%m/%Y %H:%M')} {tz_label}" except Exception: return "Belum ada" def user_today(): timezone_choice = st.session_state.get("user_timezone", "WIB (UTC+7)") return pd.Timestamp.now(tz=get_timezone_name(timezone_choice)).date() def _sync_dynamic_period(): mode = st.session_state.get("analysis_mode") today = user_today() configs = { "realtime": ( today - timedelta(days=6), today, "Tweet Terkini โ€” 7 Hari Terakhir", ), "30days": ( today - timedelta(days=29), today, "30 Hari Terakhir", ), "captured": ( today, today, "Tweet Hari Ini", ), } if mode not in configs: return start_day, end_day, mode_display = configs[mode] dt_start = datetime.combine(start_day, datetime.min.time()) dt_end = datetime.combine( end_day, datetime.max.time().replace(microsecond=0) ) st.session_state.filter_start_date = dt_start st.session_state.filter_end_date = dt_end st.session_state.filter_label = ( f"{dt_start.strftime('%d/%m/%Y')} s/d {dt_end.strftime('%d/%m/%Y')}" ) st.session_state.mode_display = mode_display st.session_state.filter_date_column = "created_at" def _load_stopwords(): stopword_file = "indonesian-stopwords-complete.txt" base = set() try: with open(stopword_file, "r", encoding="utf-8") as f: base = set(f.read().splitlines()) for kata in ["tidak", "bukan", "jangan", "kurang", "lebih"]: base.discard(kata) except FileNotFoundError: base = { "yang", "dan", "di", "ke", "dari", "ini", "itu", "dengan", "untuk", "pada", "adalah", "oleh", "ada", "ya", "akan", "atau", "juga", "sama", "karena", "jika", "sudah", "telah" } base.update({ "rt", "amp", "https", "http", "co", "t", "wkwk", "wkwkwk", "haha", "hehe", "yg", "dgn", "utk", "dr", "krn", "tp", "jd", "sdh", "aja", "doang", "banget", "bgt", "nih", "sih", "dong", "deh", }) return base def _load_stemmer(): try: from Sastrawi.Stemmer.StemmerFactory import StemmerFactory return StemmerFactory().create_stemmer() except Exception: return None NORMALISASI = { "gk": "tidak", "ga": "tidak", "gak": "tidak", "nggak": "tidak", "ngga": "tidak", "tdk": "tidak", "tak": "tidak", "yg": "yang", "dgn": "dengan", "utk": "untuk", "org": "orang", "krn": "karena", "dr": "dari", "tp": "tapi", "tpi": "tapi", "sm": "sama", "jd": "jadi", "sdh": "sudah", "blm": "belum", "emg": "memang", "emang": "memang", "gimana": "bagaimana", "gitu": "begitu", "gini": "begini", "bgt": "banget", "ongkir": "ongkos kirim", "freeongkir": "gratis ongkir", "free": "gratis", "ecommerce": "e commerce", "komdigi": "komdigi", } def step1_cleaning(text): text = str(text).lower() text = re.sub(r"http\S+|www\S+|https\S+", "", text) text = re.sub(r"@\w+", "", text) text = re.sub(r"#", "", text) text = re.sub(r"\d+", "", text) text = text.translate( str.maketrans("", "", string.punctuation) ) text = re.sub(r"[^a-zA-Z\s]", "", text) text = re.sub(r"\s+", " ", text).strip() return text def step2_normalization(text): return " ".join( NORMALISASI.get(word, word) for word in text.split() ) def step3_stopword_removal(text, stopwords): return " ".join( word for word in text.split() if word not in stopwords and len(word) > 2 ) def step4_stemming(text, stemmer): if stemmer is None: return text return stemmer.stem(text) def full_preprocessing(text, stopwords, stemmer): s1 = step1_cleaning(text) s2 = step2_normalization(s1) s3 = step3_stopword_removal(s2, stopwords) s4 = step4_stemming(s3, stemmer) return { "setelah_cleaning": s1, "setelah_normalisasi": s2, "setelah_stopword": s3, "clean_text": s4, } def _section_header(title, subtitle=""): """Render consistent section header card""" sub_html = ( f'
{subtitle}
' if subtitle else "" ) st.markdown(f"""
{title}
{sub_html}
""", unsafe_allow_html=True) def _render_preprocessing_styles(): st.markdown(""" """, unsafe_allow_html=True) def _section_gap(size="md"): st.markdown( f'
', unsafe_allow_html=True ) def _pill(col, icon, bg, color, dark, label, val, sub): """Render metric pill card with consistent styling""" with col: fs = "1.25rem" if len(str(val)) <= 8 else "1rem" st.markdown(f"""
{icon}
{label}
{val}
{sub}
""", unsafe_allow_html=True) def show(): st.markdown("""
๐Ÿงน

Bersihkan Data

""", unsafe_allow_html=True) _render_preprocessing_styles() st.markdown("""
๐Ÿ“– Apa yang dilakukan halaman ini?
Tweet dibersihkan melalui 4 tahap preprocessing:
โ‘  Cleaning โ€” hapus URL, mention, hashtag, angka, tanda baca  โ†’  โ‘ก Normalisasi โ€” ubah kata tidak baku  โ†’  โ‘ข Stopword Removal โ€” hapus kata tidak bermakna  โ†’  โ‘ฃ Stemming โ€” ubah kata ke bentuk dasar.
""", unsafe_allow_html=True) if "analysis_mode" not in st.session_state: st.warning("โš ๏ธ Silakan pilih mode tampilan di halaman Ambil Data Twitter terlebih dahulu.") return _sync_dynamic_period() start_date = st.session_state.get("filter_start_date") end_date = st.session_state.get("filter_end_date") filter_label = st.session_state.get("filter_label", "-") mode_display = st.session_state.get("mode_display", "-") if start_date is None or end_date is None: st.warning("โš ๏ธ Silakan buka halaman Ambil Data Twitter terlebih dahulu untuk memilih periode.") return mode_meta = { "realtime": ("#16a34a", "๐Ÿ“ก"), "30days": ("#3b6cf7", "๐Ÿ“…"), "captured": ("#0284c7", "๐Ÿ“†"), "custom": ("#d97706", "๐Ÿ”"), } mode_color, mode_icon = mode_meta.get( st.session_state.analysis_mode, ("#3b6cf7", "๐Ÿ“Š") ) st.markdown(f"""
{mode_icon}
{mode_display}
Periode: {filter_label}
""", unsafe_allow_html=True) try: df_all = pd.read_sql( "SELECT * FROM tweets ORDER BY created_at DESC", engine ) if df_all.empty: st.warning("โš ๏ธ Belum ada data. Kembali ke halaman Ambil Data Twitter.") return df_all["created_at"] = parse_dt(df_all["created_at"]) if "crawled_at" in df_all.columns: df_all["crawled_at"] = parse_crawled_dt(df_all["crawled_at"]) except Exception as e: st.error(f"โŒ {e}") return s_dt = pd.Timestamp(start_date) e_dt = pd.Timestamp(end_date) df = df_all[ (df_all["created_at"] >= s_dt) & (df_all["created_at"] <= e_dt) ].copy() if df.empty: st.warning( f"โš ๏ธ Tidak ada tweet dengan tanggal asli dalam periode {filter_label}." ) return total_tweets_in_db = get_tweet_count() latest_crawl_marker = get_latest_crawl_time() or "no-crawl" data_marker = (total_tweets_in_db, latest_crawl_marker) cache_key = ( f"pp_{st.session_state.analysis_mode}_" f"{start_date}_{end_date}_{total_tweets_in_db}_{latest_crawl_marker}" ) # Clear old cache entries for old_key in list(st.session_state.keys()): if old_key.startswith("pp_") and old_key != cache_key: del st.session_state[old_key] force_refresh = data_marker != st.session_state.get("_pp_last_data_marker") if cache_key not in st.session_state or force_refresh: stemmer = _load_stemmer() stopwords = _load_stopwords() with st.spinner("๐Ÿงน Sedang memproses 4 tahap preprocessing..."): results = [] for _, row in df.iterrows(): r = full_preprocessing( row["text"], stopwords, stemmer ) r["tweet_id"] = row.get("tweet_id", "") r["text_asli"] = row["text"] r["created_at"] = row["created_at"] r["crawled_at"] = row.get("crawled_at") results.append(r) df_c = pd.DataFrame(results) df_c = df_c[ df_c["clean_text"].str.strip().str.len() > 0 ].copy() st.session_state[cache_key] = df_c st.session_state[cache_key + "_stemmer_ok"] = stemmer is not None st.session_state["_pp_last_data_marker"] = data_marker df_c = st.session_state[cache_key] stemmer_ok = st.session_state.get( cache_key + "_stemmer_ok", False ) avg_b = df["text"].astype(str).str.len().mean() avg_a = df_c["clean_text"].astype(str).str.len().mean() red = ((avg_b - avg_a) / avg_b * 100) if avg_b > 0 else 0 rmv = len(df) - len(df_c) _section_header( "๐Ÿ“Œ Ringkasan Preprocessing", f"Berdasarkan tanggal asli tweet ยท {filter_label}" ) _section_gap("sm") c1, c2, c3, c4 = st.columns(4, gap="medium") _pill( c1, "๐Ÿ“Š", "#eef2ff", "#3b6cf7", "#1e3a8a", "Tweet Siap Dianalisis", f"{len(df_c):,}", "Setelah 4 tahap preprocessing" ) _pill( c2, "๐Ÿ“", "#f0fdf4", "#16a34a", "#14532d", "Panjang Sebelum", f"{avg_b:.0f} karakter", "Rata-rata per tweet" ) _pill( c3, "โœจ", "#fff7ed", "#ea580c", "#7c2d12", "Panjang Sesudah", f"{avg_a:.0f} karakter", "Rata-rata per tweet" ) _pill( c4, "๐Ÿ—‘๏ธ", "#fef2f2", "#ef4444", "#7f1d1d", "Reduksi Teks", f"{red:.1f}%", f"{rmv} tweet terlalu pendek dibuang" ) _section_gap("lg") steps = [ ( "#eef2ff", "#3b6cf7", "#1e3a8a", "โ‘  Cleaning", "Ubah ke huruf kecil ยท Hapus URL, mention, hashtag, angka, tanda baca, karakter non-alfabet" ), ( "#f0fdf4", "#16a34a", "#14532d", "โ‘ก Normalisasi Kata", "Ubah singkatan/kata gaul seperti gk โ†’ tidak, ongkir โ†’ ongkos kirim." ), ( "#fff7ed", "#ea580c", "#7c2d12", "โ‘ข Stopword Removal", "Hapus kata tidak bermakna, tetapi pertahankan kata negasi seperti tidak dan bukan." ), ( "#fefce8", "#ca8a04", "#713f12", "โ‘ฃ Stemming", f"Ubah kata ke bentuk dasar. {'โœ… Aktif' if stemmer_ok else 'โš ๏ธ Nonaktif'}" ), ] cols = st.columns(4, gap="medium") for col, (bg, color, dark, title, desc) in zip(cols, steps): with col: st.markdown(f"""
{title}
{desc}
""", unsafe_allow_html=True) _section_gap("lg") _section_header( "๐Ÿ“‹ Perbandingan Teks per Tahap Preprocessing", f"{len(df_c):,} tweet ยท {filter_label}" ) if "crawled_at" not in df_c.columns: df_c = df_c.copy() df_c["crawled_at"] = pd.NaT disp = df_c[ [ "tweet_id", "created_at", "crawled_at", "text_asli", "setelah_cleaning", "setelah_normalisasi", "setelah_stopword", "clean_text", ] ].copy() disp.columns = [ "ID Tweet", "Tanggal Tweet", "Masuk Database", "Teks Asli", "โ‘  Setelah Cleaning", "โ‘ก Setelah Normalisasi", "โ‘ข Setelah Stopword", "โ‘ฃ Hasil Akhir", ] disp["Tanggal Tweet"] = disp["Tanggal Tweet"].apply(format_dt) disp["Masuk Database"] = disp["Masuk Database"].apply(format_dt) render_standard_table( disp, height=350, min_width=1580, nowrap=["ID Tweet", "Tanggal Tweet", "Masuk Database"], wide_columns=[ "Teks Asli", "โ‘  Setelah Cleaning", "โ‘ก Setelah Normalisasi", "โ‘ข Setelah Stopword", "โ‘ฃ Hasil Akhir", ], column_widths={ "ID Tweet": "160px", "Tanggal Tweet": "170px", "Masuk Database": "170px", "Teks Asli": "300px", "โ‘  Setelah Cleaning": "260px", "โ‘ก Setelah Normalisasi": "260px", "โ‘ข Setelah Stopword": "260px", "โ‘ฃ Hasil Akhir": "260px", }, ) _section_gap("lg") with st.container(border=True, key="preprocessing_download_panel"): c1, c2 = st.columns(2, gap="medium", vertical_alignment="bottom") with c1: st.download_button( "๐Ÿ“ฅ Unduh Hasil Preprocessing Lengkap", df_c.to_csv(index=False).encode("utf-8"), f"clean_tweet_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv", "text/csv", width="stretch" ) with c2: out2 = df_c[ [ "tweet_id", "text_asli", "clean_text" ] ].copy() out2.columns = [ "tweet_id", "tweet", "clean_text" ] st.download_button( "๐Ÿ“ฅ Unduh Teks Bersih Saja", out2.to_csv(index=False).encode("utf-8"), f"teks_bersih_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv", "text/csv", width="stretch" ) _section_gap("lg") _section_header( "๐Ÿ“Š Kata-Kata yang Paling Sering Muncul", f"Dari {len(df_c):,} tweet yang sudah bersih" ) all_words = " ".join( df_c["clean_text"].fillna("") ).split() stop_extra = { "ongkos", "kirim", "gratis", "komdigi" } filtered_words = [ w for w in all_words if len(w) > 2 and w not in stop_extra ] word_freq = Counter( filtered_words ).most_common(20) if not word_freq: st.info("โš ๏ธ Kata tidak cukup untuk ditampilkan.") else: words = [w[0] for w in word_freq] counts = [w[1] for w in word_freq] fig = go.Figure(data=[ go.Bar( y=words[::-1], x=counts[::-1], orientation="h", marker=dict(color="#3b6cf7"), text=[str(c) for c in counts[::-1]], textposition="outside", hovertemplate="%{y}
Muncul %{x} kali", ) ]) fig.update_layout( height=500, margin=dict(l=0, r=60, t=8, b=8), paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)", xaxis=dict(showgrid=True, gridcolor="rgba(203,213,225,0.8)"), yaxis=dict(showgrid=False), ) with st.container(border=True, key="preprocessing_chart_panel"): st.plotly_chart( fig, width="stretch", config={"displayModeBar": False} ) _section_gap("lg") st.session_state["preprocessed_df"] = df_c if st.button( "๐Ÿ“ˆ Lanjut ke Analisis Sentimen โ†’", type="primary", width="stretch" ): st.session_state.current_page = "sentiment" st.rerun()