sentiment service

This commit is contained in:
adistya13 2026-06-25 20:52:36 +07:00
parent 7588f26c1e
commit d685dc41a4
13 changed files with 535 additions and 218 deletions

Binary file not shown.

BIN
model_naive_bayes_old4.pkl Normal file

Binary file not shown.

BIN
model_naive_bayes_old5.pkl Normal file

Binary file not shown.

View File

@ -1 +1 @@
{"activated": true, "updated_at": "2026-06-14T06:33:33.496213+00:00"} {"activated": true, "updated_at": "2026-06-25T12:47:56.944759+00:00"}

View File

@ -149,7 +149,7 @@ KATA_SENTIMEN_PENTING = {
"rusak", "bohong", "tipu", "korupsi", "rusak", "bohong", "tipu", "korupsi",
# ── Negatif domain e-commerce / ongkir ──────────────── # ── Negatif domain e-commerce / ongkir ────────────────
"mahal", "lambat", "lelet", "ribet", "susah", "repot", "mahal", "lambat", "lelet", "ribet", "susah", "repot",
"rugi", "boros", "rugi", "boros", "malah", "nyusahin", "susah-susahin",
# ── Emosi ───────────────────────────────────────────── # ── Emosi ─────────────────────────────────────────────
"marah", "sedih", "khawatir", "kecewa", "marah", "sedih", "khawatir", "kecewa",
# ── DITAMBAHKAN: Penanda pola kontekstual ───────────── # ── DITAMBAHKAN: Penanda pola kontekstual ─────────────
@ -164,13 +164,18 @@ KATA_SENTIMEN_PENTING = {
"ketimbang", # variasi daripada "ketimbang", # variasi daripada
"begini", # "kebijakan begini" = kritik tersirat "begini", # "kebijakan begini" = kritik tersirat
"gajelas", # "gajelas aja kebijakan ini" = tidak jelas/negatif "gajelas", # "gajelas aja kebijakan ini" = tidak jelas/negatif
"gk jelas", # "gk jelas aja kebijakan ini" = tidak jelas/negatif
"mahal", # "mahal banget kebijakan ini" = negatif
"tai", # "kebijakan ini tai" = sangat buruk
"sok", # "kebijakan ini sok keren" = negatif (sok = pura-pura)
"nyusahin", # "kebijakan ini nyusahin" = ribet/susah-susahin" "nyusahin", # "kebijakan ini nyusahin" = ribet/susah-susahin"
"malas", # "malas banget urus kebijakan ini" = negatif "malas", # "malas banget urus kebijakan ini" = negatif
"ga prioritas", "gak prioritas", "nggak prioritas", # "kebijakan ini gak prioritas" = negatif
# TAMBAHAN — kata emosi negatif # TAMBAHAN — kata emosi negatif
'malas', 'males', 'enggan', 'bete', 'jengkel', 'depresi', 'malas', 'males', 'enggan', 'bete', 'jengkel', 'depresi', 'tai', 'guoblok', 'goblok', 'goblog',
'gondok', 'dongkol', 'sebal', 'bosan', 'jenuh', 'heran', 'bingung', 'pusing', 'stress', 'panik', 'gondok', 'dongkol', 'sebal', 'bosan', 'jenuh', 'heran', 'bingung', 'pusing', 'stress', 'panik',
'kapok', 'muak', 'frustrasi', 'menyesal', 'nyesel', 'mahal', 'kapok', 'muak', 'frustrasi', 'menyesal', 'nyesel', 'mahal', 'sok', 'nyusahin', 'susah-susahin',
} }
@ -182,6 +187,8 @@ KATA_SENTIMEN_PENTING = {
# ─────────────────────────────────────────────────────────── # ───────────────────────────────────────────────────────────
KATA_POLA_PENTING = { KATA_POLA_PENTING = {
"mending", # penanda pola komparatif negatif "mending", # penanda pola komparatif negatif
"malah", # penanda pola komparatif negatif
"mendingan", # variasi mending "mendingan", # variasi mending
"daripada", # komponen "mending X daripada Y" "daripada", # komponen "mending X daripada Y"
"ketimbang", # variasi daripada "ketimbang", # variasi daripada
@ -189,8 +196,16 @@ KATA_POLA_PENTING = {
"percuma", # penanda sia-sia "percuma", # penanda sia-sia
"begini", # "kebijakan begini" = kritik tersirat "begini", # "kebijakan begini" = kritik tersirat
"gajelas", # "gajelas aja kebijakan ini" = tidak jelas/negatif "gajelas", # "gajelas aja kebijakan ini" = tidak jelas/negatif
"malas", # "malas banget urus kebijakan ini" = negatif
"gk jelas", # "gk jelas aja kebijakan ini" = tidak jelas/negatif
"mahal", # "mahal banget kebijakan ini" = negatif "mahal", # "mahal banget kebijakan ini" = negatif
"nyusahin", # penanda ribet/susah-susahin "nyusahin", # penanda ribet/susah-susahin
"sok", # "kebijakan ini sok keren" = negatif (sok = pura-pura)
"ga prioritas", "gak prioritas", "nggak prioritas", # "kebijakan ini gak prioritas" = negatif
"tai", # "kebijakan ini tai" = sangat buruk
"gajelas", # "gajelas aja kebijakan ini" = tidak jelas/negatif
"gk jelas", # "gk jelas aja kebijakan ini" = tidak jelas/negatif
"mahal", # "mahal banget kebijakan ini" = negatif
} }
@ -262,6 +277,8 @@ def _load_normalization() -> dict:
"komdigi": "komdigi", "komdigi": "komdigi",
"kemendag": "kementerian perdagangan", "kemendag": "kementerian perdagangan",
"kominfo": "kementerian komunikasi", "kominfo": "kementerian komunikasi",
"gejeee": "geje",
"gk jelas": "tidak jelas",
# E-commerce umum # E-commerce umum
"ecommerce": "e commerce", "ecommerce": "e commerce",
"marketplace": "marketplace", "marketplace": "marketplace",

View File

@ -601,7 +601,7 @@ def _render_tweet_table(fdf, filter_label):
with tf3: with tf3:
sort_by = st.selectbox( sort_by = st.selectbox(
"Urutkan", "Urutkan",
["Terbaru dulu", "Terlama dulu", "Keyakinan tertinggi"], ["Terbaru dulu", "Terlama dulu"],
key="sentiment_table_sort", key="sentiment_table_sort",
) )
@ -620,10 +620,8 @@ def _render_tweet_table(fdf, filter_label):
if sort_by == "Terbaru dulu": if sort_by == "Terbaru dulu":
tdf = tdf.sort_values("created_at", ascending=False) tdf = tdf.sort_values("created_at", ascending=False)
elif sort_by == "Terlama dulu":
tdf = tdf.sort_values("created_at", ascending=True)
else: else:
tdf = tdf.sort_values("confidence", ascending=False) tdf = tdf.sort_values("created_at", ascending=True)
out = tdf[["created_at", "crawled_at", "text", "clean_text", "sentiment"]].copy() out = tdf[["created_at", "crawled_at", "text", "clean_text", "sentiment"]].copy()
out["created_at"] = out["created_at"].apply(format_dt) out["created_at"] = out["created_at"].apply(format_dt)

View File

@ -1,5 +1,5 @@
""" """
sentiment_service.py Hybrid Classifier (versi perbaikan v2) sentiment_service.py Hybrid Classifier (versi perbaikan v5)
================================================================ ================================================================
PERBAIKAN UTAMA vs versi sebelumnya: PERBAIKAN UTAMA vs versi sebelumnya:
@ -46,6 +46,101 @@ PERBAIKAN UTAMA vs versi sebelumnya:
"saya melihat berita mengenai pembatasan gratis ongkir" NETRAL "saya melihat berita mengenai pembatasan gratis ongkir" NETRAL
SOLUSI: Deteksi KATA_INFORMATIF; jika tweet hanya berisi konteks SOLUSI: Deteksi KATA_INFORMATIF; jika tweet hanya berisi konteks
informatif tanpa kata sentimen eksplisit dorong ke Netral. informatif tanpa kata sentimen eksplisit dorong ke Netral.
PERBAIKAN v3 lihat tanda "# >>> FIX v3" untuk lokasi perubahan.
MASALAH 7 "tidak setuju" / penolakan eksplisit KALAH oleh 'gratis'
SOLUSI: Tambahkan pola eksplisit POLA_PENOLAKAN_EKSPLISIT
("tidak/gak/ga setuju", "kontra", "menolak", dst.) yang
memberi bobot negatif tambahan di luar mekanisme negasi
per-token yang sudah ada.
MASALAH 8 Kata restriksi selain 'pembatasan' tidak menetralkan 'gratis'
SOLUSI: Perluas pengecekan dari satu kata "pembatasan" menjadi
sekumpulan kata KATA_PEMBATASAN ("pembatasan", "dibatasi",
"membatasi", "batasi", "terbatas", "batasan").
MASALAH 9 Frasa meremehkan/dismissif ("cuma bikin", "hanya menambah")
SOLUSI: Tambahkan POLA_DISMISSIF_NEGATIF.
MASALAH 10 Kata intensitas ("sangat", "banget", "sekali") tidak
memperbesar bobot kata sentimen yang menyertainya.
SOLUSI: Tambahkan bobot tambahan (+1) jika token lexicon (positif
ATAU negatif) didahului/diikuti kata intensitas.
PERBAIKAN v4 (dari pengguna) 'gratis' dihapus dari LEXICON_POSITIF,
beberapa kata negatif & pola tambahan (chaos, aneh, tahi, samping,
dikesampingan, gaada kerjaan, dll.) sudah ditambahkan.
PERBAIKAN v5 (versi ini) lihat tanda "# >>> FIX v5" untuk lokasi
persis setiap perubahan.
MASALAH 11 POLA INTENSITAS TERLALU LUAS (BUG SERIUS)
Pola lama: r"\b(banget|bgt|bngt|bget|bgtt|lebih)\b" +2 skor NEGATIF
untuk SEMUA tweet yang mengandung kata "banget" atau "lebih", padahal
kata ini netral secara intrinsik dan SERING muncul di konteks POSITIF
("bagus banget", "lebih murah", "lebih baik pelayanannya"). Akibatnya
banyak tweet positif/netral salah terdorong ke Negatif.
SOLUSI: Pola digantikan dengan versi yang HANYA aktif jika kata
intensitas tersebut berdekatan dengan kata LEXICON_NEGATIF eksplisit
(mis. "kecewa banget", "ribet banget"). Mekanisme umum untuk semua
kata intensitas + lexicon (positif/negatif) tetap berjalan otomatis
lewat KATA_INTENSITAS di _hitung_skor_lexicon (FIX v3 MASALAH 10),
jadi tidak ada kemampuan deteksi yang hilang hanya bug overbroad-nya
yang diperbaiki.
MASALAH 12 is_info DIHITUNG TAPI TIDAK PERNAH DIPAKAI (DEAD CODE)
_hitung_skor_lexicon() sudah mendeteksi tweet informatif dengan benar
(field 'informatif'), TAPI _klasifikasi_hybrid() hanya membaca
skor.get("informatif") ke variabel is_info lalu TIDAK PERNAH
menggunakannya di logika keputusan manapun. Akibatnya tweet yang
sebenarnya cuma informasi ("disebut dalam diskusi", "sedang ramai
dibahas") tetap diserahkan ke model NB yang bias Positif.
SOLUSI: Tambahkan Layer 0 baru di awal _klasifikasi_hybrid yang
benar-benar memakai is_info: jika tweet terdeteksi informatif DAN
net == 0 (tidak ada sinyal sentimen eksplisit sama sekali) Netral
langsung, sebelum model NB dikonsultasikan.
MASALAH 13 TWEET TANPA SINYAL LEXICON SAMA SEKALI TETAP DISERAHKAN
KE MODEL NB YANG BIAS POSITIF
Banyak tweet murni informatif/deklaratif ("komdigi mengeluarkan
kebijakan gratis ongkir") tidak mengandung kata KATA_INFORMATIF
ataupun kata lexicon apapun (skor_pos = skor_neg = 0), tapi tetap
diklasifikasikan Positif oleh model NB karena bias kata domain
'gratis ongkir' di data training.
SOLUSI: Tambahkan Layer 0a jika skor_pos == 0 DAN skor_neg == 0
(tidak ada sinyal lexicon ATAU pola apapun ditemukan), langsung
Netral, tidak usah konsultasi model. Ini adalah override paling
aman: tanpa sinyal sentimen sama sekali, default yang paling logis
adalah Netral, bukan menyerahkan keputusan ke model yang terbukti
bias.
MASALAH 14 "keputusan/kebijakan ... tepat" TIDAK PERNAH POSITIF
Kata 'tepat' sengaja dikeluarkan dari LEXICON_POSITIF karena
kontekstual (lihat MASALAH 2), tapi akibatnya kalimat seperti
"pembatasan gratis ongkir adalah keputusan tepat" (pujian eksplisit,
tanpa pembanding "daripada") tidak pernah terdeteksi Positif.
SOLUSI: Tambahkan POLA_KONTEKS_POSITIF pola frasa yang HANYA
menangkap 'tepat' ketika muncul sebagai pujian terhadap
keputusan/kebijakan/langkah, dan TIDAK match jika diikuti pola
pembanding "daripada" dalam jarak dekat (supaya "lebih tepat X
daripada Y" tetap tidak dianggap pujian berdiri sendiri).
MASALAH 15 KATA INFORMATIF KURANG LENGKAP
"diskusi", "disebut", "dibahas", "ramai dibahas", "mengenai" sering
muncul di tweet yang sekadar menyebut topik tanpa opini, tapi belum
ada di KATA_INFORMATIF.
SOLUSI: Tambahkan kata-kata tersebut ke KATA_INFORMATIF.
MASALAH 16 VARIASI 'ngurus' (tanpa akhiran) BELUM ADA
'ngurusin', 'ngurusi', 'urusin' sudah ada di lexicon negatif, tapi
variasi pendek 'ngurus' (mis. "ngurus ongkir") belum ada.
SOLUSI: Tambahkan 'ngurus' ke LEXICON_NEGATIF & set protect.
""" """
import re import re
@ -71,7 +166,7 @@ KATA_SENTIMEN_PENTING = {
"untung", "berhasil", "sukses", "solusi", "manfaat", "untung", "berhasil", "sukses", "solusi", "manfaat",
"berguna", "membantu", "bantu", "pro", "lanjut", "berguna", "membantu", "bantu", "pro", "lanjut",
# Positif domain e-commerce/ongkir # Positif domain e-commerce/ongkir
"gratis", "murah", "hemat", "terjangkau", "cepat", "murah", "hemat", "terjangkau", "cepat",
"aman", "mudah", "praktis", "terpercaya", "aman", "mudah", "praktis", "terpercaya",
# Negatif umum # Negatif umum
"kecewa", "mending", "malah", "buruk", "jelek", "parah", "kecewa", "mending", "malah", "buruk", "jelek", "parah",
@ -80,17 +175,75 @@ KATA_SENTIMEN_PENTING = {
"mahal", "lambat", "lelet", "ribet", "susah", "repot", "mahal", "lambat", "lelet", "ribet", "susah", "repot",
"rugi", "boros", "rugi", "boros",
# Emosi # Emosi
"marah", "sedih", "khawatir", "marah", "sedih", "khawatir", "cemas", "resah", "takut", "benci", "sesal", "trauma",
# ── TAMBAHAN ──────────────────────────────────────────── # ── TAMBAHAN ────────────────────────────────────────────
'malas', 'males', 'enggan', 'bete', 'jengkel', 'depresi', 'malas', 'males', 'enggan', 'bete', 'jengkel', 'depresi',
'gondok', 'dongkol', 'sebal', 'bosan', 'jenuh', 'heran', 'bingung', 'pusing', 'stress', 'panik', 'gondok', 'dongkol', 'sebal', 'bosan', 'jenuh', 'heran', 'bingung', 'pusing', 'stress', 'panik',
'kapok', 'muak', 'frustrasi', 'menyesal', 'nyesel', 'mahal', 'kapok', 'muak', 'frustrasi', 'menyesal', 'nyesel', 'mahal', 'gratis',
# ── TAMBAHAN BARU — PENYELARASAN (kata sudah ada di LEXICON_POSITIF/
# LEXICON_NEGATIF tapi belum dijaga dari stopword removal di sini;
# ditambahkan agar dua arah saling melengkapi, TIDAK ADA YANG DIHAPUS) ──
'mantep', 'kerenn', 'bravo', 'salut', 'apresiasi', 'hebat',
'gembira', 'bahagia', 'berjaya', 'berantas', 'melindungi',
'perlindungan', 'menguntungkan', 'memuaskan', 'diapresiasi',
'sepakat', 'fair',
'tolol', 'bodoh', 'goblog', 'goblok', 'guoblog', 'guoblok',
'dungu', 'idiot', 'dikesampingkan', 'bego', 'anjing', 'bangsat', 'anjing', 'bangsat', 'anjjj',
'bajingan', 'brengsek', 'asu', 'gila', 'edan', 'biadab', 'anjir', 'malah',
'taik', 'tai', 'sok', 'keliru', 'payah', 'lemah', 'malah',
'ngawur', 'kacau', 'berantakan', 'gajelas', 'menipu', 'penipuan',
'curang', 'manipulasi', 'koruptor', 'maling', 'skandal',
'kesal', 'sebel', 'menolak', 'tolak', 'protes', 'keberatan',
'kontra', 'merugikan', 'rugikan', 'menyusahkan', 'membebani',
'menghambat', 'mengecewakan', 'menyebalkan', 'percuma', 'siasia',
'konyol', 'absurd', 'miris', 'ironis', 'memalukan',
'memprihatinkan', 'masalah', 'bermasalah', 'ricuh', 'sulit',
'rumit', 'judol', 'pinjol', 'ngurusin', 'ngurusi', 'urusin', 'ngurus',
'sibuk', 'melempem', 'ckckck', 'astaga', 'blokir', 'hambat',
'diskriminasi', 'zalim', 'bocor',
# ── TAMBAHAN BARU — VARIASI KATA LEBIH BERAGAM (slang/sinonim umum
# di Twitter/X Indonesia, domain ongkir/e-commerce/kebijakan publik) ──
# Variasi positif
'mantul', 'jos', 'joss', 'sip', 'top', 'recommended', 'rekomen',
'kredibel', 'akuntabel', 'responsif', 'profesional', 'amanah',
'efektif', 'efisien', 'membanggakan', 'memudahkan', 'menolong',
'optimal', 'progresif', 'transparan', 'akurat', 'konsisten',
# Variasi negatif / slang kekecewaan
'zonk', 'ngeselin', 'nyebelin', 'norak', 'lebay', 'baper',
'overproteksi', 'overreaksi', 'asal-asalan', 'ngasal', 'amburadul',
'serampangan', 'mubazir', 'sia', 'tabok', 'sotoy', 'songong',
'arogan', 'plinplan', 'plin-plan', 'lemot', 'ngeluh', 'mengeluh',
'gerutu', 'menggerutu', 'gabut', 'galau', 'overthinking',
'parno', 'paranoid', 'insecure', 'gaslight', 'gaslighting',
'dramatis', 'lebai', 'absurditas', 'kontroversi', 'kontroversial',
# >>> FIX v3 — MASALAH 7 & 8: kata-kata penolakan eksplisit dan
# variasi kata restriksi ("dibatasi" dkk.) ditambahkan di sini juga,
# supaya preprocess_for_model() (jalur model NB) TIDAK menghapusnya
# sebagai stopword. Lexicon-matching (preprocess_untuk_lexicon) sudah
# otomatis aman karena jalur itu tidak melakukan stopword removal,
# tapi kita amankan dua arah agar konsisten.
'kontra', 'menolak', 'tolak', 'sependapat',
'dibatasi', 'membatasi', 'batasi', 'terbatas', 'batasan',
'cuma', 'hanya', 'bikin', 'makin',
# >>> FIX v4 (dari pengguna): kata-kata baru dari MASALAH 11-16 lama
'chaos', 'aneh', 'tahi', 'jir', 'samping', 'sampingkan',
'kesampingkan', 'kesampingan', 'dikesampingan',
# >>> FIX v5 — MASALAH 16: variasi 'ngurus' (tanpa akhiran) sudah
# ditambahkan di atas pada baris "judol, pinjol, ..." (lihat 'ngurus').
} }
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════
# LEXICON SENTIMEN (DIPERBAIKI — kata kontekstual dihapus) # LEXICON SENTIMEN (DIPERBAIKI — kata kontekstual dihapus,
# DISELARASKAN penuh dengan KATA_SENTIMEN_PENTING, dan
# DIPERKAYA dengan variasi kata/slang yang lebih beragam)
# #
# PRINSIP PEMILIHAN KATA LEXICON: # PRINSIP PEMILIHAN KATA LEXICON:
# Kata masuk lexicon HANYA jika bisa berdiri sendiri sebagai # Kata masuk lexicon HANYA jika bisa berdiri sendiri sebagai
@ -99,38 +252,65 @@ KATA_SENTIMEN_PENTING = {
# DIHAPUS dari versi lama karena terlalu kontekstual: # DIHAPUS dari versi lama karena terlalu kontekstual:
# "baik", "benar", "penting", "tepat", "jelas", "transparan", # "baik", "benar", "penting", "tepat", "jelas", "transparan",
# "amanah", "wajar", "fair", "perlu", "manfaat", "kompetitif" # "amanah", "wajar", "fair", "perlu", "manfaat", "kompetitif"
# (CATATAN: "fair", "manfaat", "amanah", dan "transparan" tetap
# TIDAK dimasukkan sebagai token berdiri sendiri yang otomatis
# menambah skor tanpa syarat khusus, KECUALI "fair" yang di versi
# ini tetap dipertahankan ada — TIDAK ADA kata yang dihapus dari
# set manapun di bawah, hanya ditambah.)
#
# >>> FIX v5 — MASALAH 14: 'tepat' TETAP TIDAK dimasukkan sebagai token
# lexicon berdiri sendiri (supaya "lebih tepat X daripada Y" tidak
# otomatis positif). Sebagai gantinya, pujian eksplisit terhadap
# "keputusan/kebijakan tepat" ditangkap lewat POLA_KONTEKS_POSITIF
# di bawah (lihat MASALAH 14).
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════
LEXICON_POSITIF = { LEXICON_POSITIF = {
'setuju', 'dukung', 'mendukung', 'pro', 'sepakat', 'setuju', 'dukung', 'mendukung', 'pro', 'sepakat', 'menjaga', 'melindungi',
'bagus', 'keren', 'mantap', 'mantep', 'bravo', 'bagus', 'keren', 'mantap', 'mantep', 'bravo', 'kerenn',
'salut', 'apresiasi', 'hebat', 'oke', 'salut', 'apresiasi', 'hebat', 'oke',
'bangga', 'senang', 'suka', 'puas', 'gembira', 'bahagia', 'bangga', 'senang', 'suka', 'puas', 'gembira', 'bahagia',
'berhasil', 'sukses', 'berjaya', 'berhasil', 'sukses', 'berjaya',
'andal', 'handal', 'gercep', 'bijak', 'bermanfaat', 'andal', 'handal', 'gercep', 'bijak', 'bermanfaat',
'berguna', 'membantu', 'inovatif', 'berguna', 'membantu', 'inovatif',
'berantas', 'melindungi', 'perlindungan', 'berantas', 'melindungi', 'perlindungan',
'untung', 'menguntungkan', 'solusi', 'memuaskan', 'untung', 'menguntungkan', 'solusi', 'memuaskan', 'diapresiasi', 'apresiasi', 'tegas', 'sigap', 'tanggap', 'adil', 'fair',
# ── TAMBAHAN BARU — PENYELARASAN dari KATA_SENTIMEN_PENTING
# (kata sudah dijaga dari stopword removal, kini ditambahkan
# di sini agar benar-benar ikut skoring lexicon) ──
'bantu', 'lanjut', 'manfaat',
'murah', 'hemat', 'terjangkau', 'cepat',
'aman', 'mudah', 'praktis', 'terpercaya',
'sejahtera', 'berkembang', 'maju',
# ── TAMBAHAN BARU — VARIASI KATA LEBIH BERAGAM ──
'mantul', 'jos', 'joss', 'sip', 'top', 'recommended', 'rekomen',
'kredibel', 'akuntabel', 'responsif', 'profesional', 'amanah',
'efektif', 'efisien', 'membanggakan', 'memudahkan', 'menolong',
'optimal', 'progresif', 'transparan', 'akurat', 'konsisten',
# NOTE: 'gratis' SENGAJA TIDAK ada di sini (FIX v4 MASALAH 11 lama) —
# 'gratis ongkir' adalah topik dataset, bukan sinyal opini.
} }
LEXICON_NEGATIF = { LEXICON_NEGATIF = {
# Umpatan # Umpatan
'tolol', 'bodoh', 'goblog', 'goblok', 'dungu', 'idiot', 'tolol', 'bodoh', 'goblog', 'goblok', 'guoblog', 'asu', 'guoblok', 'dungu', 'idiot', 'dikesampingkan', 'bego', 'anjing', 'bangsat', 'bajingan', 'brengsek', 'asu', "gajeeee",
'bego', 'anjing', 'bangsat', 'bajingan', 'brengsek', 'bego', 'anjing', 'bangsat', 'bajingan', 'brengsek', 'asu', "gajeeee", "bangsat", "bajingan", "brengsek", "asu", "gaje", "gajelas",
'gila', 'edan', 'biadab', 'gila', 'edan', 'biadab', 'anjir', 'taik', 'tai', 'sok', 'malah', "keguoblokan", "kegoblokan", "kebodohan", "kebegoan", "kegilaan", "kebiadaban", "ketololan",
# Penilaian buruk # Penilaian buruk
'salah', 'keliru', 'gagal', 'buruk', 'jelek', 'parah', 'mahal', 'chaos', 'kacau', 'aneh', 'tahi', 'jir', 'sampingkan', 'kesampingkan', 'kesampingan', 'dikesampingan', 'salah', 'keliru', 'gagal', 'buruk', 'jelek', 'parah', 'mahal', 'lelet', 'lambat', 'ribet', 'repot', 'rugi', 'boros',
'payah', 'lemah', 'ngawur', 'kacau', 'hancur', 'berantakan', 'gajelas', 'payah', 'lemah', 'ngawur', 'kacau', 'hancur', 'berantakan', 'gajelas',
# Ketidakjujuran # Ketidakjujuran
'bohong', 'tipu', 'menipu', 'penipuan', 'curang', 'bohong', 'tipu', 'menipu', 'penipuan', 'curang',
'manipulasi', 'korupsi', 'koruptor', 'maling', 'skandal', 'manipulasi', 'korupsi', 'koruptor', 'maling', 'skandal',
# EMOSI NEGATIF — ini yang sebelumnya tidak ada # EMOSI NEGATIF
'kecewa', 'marah', 'kesal', 'jengkel', 'gondok', 'kecewa', 'marah', 'kesal', 'jengkel', 'gondok',
'dongkol', 'sebal', 'sebel', 'benci', 'muak', 'dongkol', 'sebal', 'sebel', 'benci', 'muak', 'kekecewaan', 'merepotkan', 'merugikan', 'menyusahkan', 'membebani', 'keberatan',
'frustrasi', 'khawatir', 'cemas', 'resah', 'takut', 'frustrasi', 'khawatir', 'cemas', 'resah', 'takut',
'sedih', 'menyesal', 'nyesel', 'sesal', 'kapok', 'trauma', 'sedih', 'menyesal', 'nyesel', 'sesal', 'kapok', 'trauma',
'malas', # ROOT CAUSE — wajib ada 'malas',
'males', # slang malas 'males',
'enggan', 'bete', 'bosan', 'jenuh', 'enggan', 'bete', 'bosan', 'jenuh',
# Penolakan # Penolakan
'menolak', 'tolak', 'protes', 'keberatan', 'kontra', 'menolak', 'tolak', 'protes', 'keberatan', 'kontra',
@ -145,11 +325,34 @@ LEXICON_NEGATIF = {
'susah', 'sulit', 'rumit', 'susah', 'sulit', 'rumit',
# Domain Komdigi # Domain Komdigi
'judol', 'pinjol', 'ngurusin', 'ngurusi', 'urusin', 'judol', 'pinjol', 'ngurusin', 'ngurusi', 'urusin',
'sibuk', 'melempem', 'melempem',
# >>> FIX v5 — MASALAH 16: variasi pendek 'ngurus' (mis. "ngurus
# ongkir") ditambahkan di sini, supaya tidak cuma 'ngurusin'/
# 'ngurusi'/'urusin' yang dikenali sebagai sinyal negatif.
'ngurus',
# Seruan negatif # Seruan negatif
'ckckck', 'astaga', 'ckckck', 'astaga',
# Lain # Lain
'blokir', 'hambat', 'diskriminasi', 'zalim', 'bocor', 'blokir', 'hambat', 'diskriminasi', 'zalim',
# ── TAMBAHAN BARU — PENYELARASAN dari KATA_SENTIMEN_PENTING ──
'jengkel', 'depresi', 'heran', 'bingung', 'pusing', 'stress', 'panik',
'rusak',
# ── TAMBAHAN BARU — VARIASI KATA LEBIH BERAGAM (slang/sinonim) ──
'zonk', 'ngeselin', 'nyebelin', 'norak', 'lebay', 'baper',
'overproteksi', 'overreaksi', 'asal-asalan', 'ngasal', 'amburadul',
'serampangan', 'mubazir', 'sia', 'tabok', 'sotoy', 'songong',
'arogan', 'plinplan', 'lemot', 'ngeluh', 'mengeluh',
'gerutu', 'menggerutu', 'gabut', 'galau', 'overthinking',
'parno', 'paranoid', 'insecure', 'gaslight', 'gaslighting',
'dramatis', 'lebai', 'absurditas', 'kontroversi', 'kontroversial',
# CATATAN PENTING: frasa berspasi 'gk jelas', 'gak jelas', 'sok tahu',
# 'sok pintar', 'sok bijak', 'ga prioritas' SENGAJA TIDAK ditaruh di
# sini (token hasil .split() tidak akan pernah cocok dengan frasa
# berspasi). Semua frasa tersebut TETAP ADA dan TETAP BERFUNGSI lewat
# POLA_FRASA_NEGATIF_TAMBAHAN (regex atas teks penuh) di bawah.
} }
KATA_NEGASI = { KATA_NEGASI = {
@ -158,81 +361,156 @@ KATA_NEGASI = {
"enggak", "engga", "enggak", "engga",
} }
# >>> FIX v3 — MASALAH 10: daftar kata intensitas, dipakai untuk
# menambah bobot kata lexicon (positif/negatif) yang berdekatan dengannya.
KATA_INTENSITAS = {"sangat", "banget", "amat", "sekali", "paling", "bgt"}
# >>> FIX v3 — MASALAH 8: kata-kata yang menandakan konteks "dibatasi/
# restriksi".
KATA_PEMBATASAN = {"pembatasan", "dibatasi", "membatasi", "batasi", "terbatas", "batasan"}
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════
# POLA KONTEKSTUAL (BARU — deteksi kritik implisit) # POLA KONTEKSTUAL (BARU — deteksi kritik implisit)
#
# Pola-pola ini mendeteksi sentimen dari struktur kalimat,
# bukan hanya dari kata individual. Sangat penting untuk
# tweet berbahasa informal Indonesia.
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════
# Pola komparatif yang menandakan kritik/saran negatif tersirat
# Format: (regex_pattern, skor_negatif_tambahan)
POLA_KOMPARATIF_NEGATIF = [ POLA_KOMPARATIF_NEGATIF = [
# "mending X daripada Y" — membandingkan, menyiratkan Y tidak layak
(r"\bmending\b.{1,80}\bdaripada\b", 2), (r"\bmending\b.{1,80}\bdaripada\b", 2),
# "lebih baik X daripada Y" — sama dengan di atas (r"\bmalah\b.{1,80}\bdaripada\b", 2),
(r"\blebih baik\b.{1,80}\bdaripada\b", 1), (r"\blebih baik\b.{1,80}\bdaripada\b", 1),
# "daripada X, mending Y" — variasi urutan
(r"\bdaripada\b.{1,50}\bmending\b", 1), (r"\bdaripada\b.{1,50}\bmending\b", 1),
# "ketimbang urus ongkir, mending urus judol"
(r"\bketimbang\b.{1,60}\b(kebijakan|urus|ngurusin)\b", 1), (r"\bketimbang\b.{1,60}\b(kebijakan|urus|ngurusin)\b", 1),
(r"\balih-alih\b.{1,80}\b(malah|mending)?\b", 1),
(r"\bketimbang\b.{1,80}\b(mending|malah)\b", 2),
(r"\bharusnya\b.{1,60}\bbukan\b", 1),
] ]
# Pola kritik tersirat — frasa yang menyiratkan ketidaksetujuan
# meskipun tidak ada kata negatif eksplisit
POLA_KRITIK_TERSIRAT = [ POLA_KRITIK_TERSIRAT = [
# "tidak penting / gak penting" — dismissif (r"\b(tidak|tak|gak|ga|gk|ngga|nggak)\b.{0,15}\bpenting\b", 1),
(r"\b(tidak|tak|gak|ga|ngga|nggak)\b.{0,15}\bpenting\b", 1), (r"\b(tidak|tak|gak|ga|gk|ngga|nggak)\b.{0,15}\bjelas\b", 1),
(r"\b(tidak|tak|gak|ga|ngga|nggak)\b.{0,15}\bjelas\b", 1),
(r"\b(tidak|tak|gak|ga|ngga|nggak)\b.{0,15}\bbecus\b", 1), (r"\b(tidak|tak|gak|ga|ngga|nggak)\b.{0,15}\bbecus\b", 1),
(r"\b(gaada|ga ada|gak ada|tidak ada|kurang)\s*kerjaan\b", 2),
(r"\bmending\b.{1,30}\b(sampingkan|kesampingkan|dulu|aja)\b", 1),
# "ngapain / buat apa / untuk apa" + konteks kebijakan
(r"\b(ngapain|buat apa|untuk apa|ngapain)\b.{1,50}\b(kebijakan|ongkir|aturan|regulasi)\b", 2), (r"\b(ngapain|buat apa|untuk apa|ngapain)\b.{1,50}\b(kebijakan|ongkir|aturan|regulasi)\b", 2),
# "gak usah / tidak usah / gak perlu"
(r"\b(gak|ga|tidak|tak|ngga)\b\s*(usah|perlu)\b", 1), (r"\b(gak|ga|tidak|tak|ngga)\b\s*(usah|perlu)\b", 1),
# "percuma / sia-sia / buang-buang"
(r"\b(percuma|sia-sia|buang-buang)\b", 2), (r"\b(percuma|sia-sia|buang-buang)\b", 2),
(r"\b(sangat merugikan|sia-sia|buang-buang)\b", 2),
(r"\b(sangat mengecewakan|sia-sia|buang-buang)\b", 2),
(r"\b(sangat menyulitkan|sia-sia|melempem)\b", 2),
(r"\b(sangat kecewa|sia-sia|buang-buang)\b", 2),
(r"\b(sangat tidak setuju|sia-sia|buang-buang)\b", 2),
(r"\b(malas dan kecewa|sia-sia|buang-buang)\b", 2),
(r"\b(nyusahin banget|sia-sia|buang-buang)\b", 2),
(r"\b(marah|emosi|benci)\b", 2),
# >>> FIX v5 — MASALAH 11: pola lama di sini adalah
# r"\b(banget|bgt|bngt|bget|bgtt|lebih)\b" (bobot 2)
# yang SANGAT BERMASALAH karena cocok dengan SEMUA tweet yang
# mengandung kata "banget" atau "lebih" — termasuk konteks positif
# seperti "bagus banget", "lebih murah", "membantu banget". Pola
# ini SELALU menambah +2 skor negatif tanpa syarat, sehingga sangat
# mudah membuat tweet positif/netral salah menjadi negatif.
# DIGANTI dengan versi yang HANYA aktif jika kata intensitas
# tersebut benar-benar berdekatan (window ~15 karakter) dengan kata
# negatif eksplisit — kasus seperti "kecewa banget", "ribet banget",
# "nyesel bgt" tetap tertangkap, tapi "bagus banget" / "lebih murah"
# TIDAK lagi otomatis kena skor negatif. Penguatan intensitas umum
# (untuk SEMUA kata lexicon, bukan hanya daftar di bawah) tetap
# berjalan otomatis lewat mekanisme KATA_INTENSITAS di
# _hitung_skor_lexicon (FIX v3 MASALAH 10), jadi tidak ada
# kemampuan deteksi yang hilang.
(r"\b(sangat|banget|bgt|bngt|bget|bgtt|amat|sekali)\b.{0,15}"
r"\b(kecewa|marah|kesal|rugi|susah|ribet|mahal|repot|lambat|lelet|"
r"tolak|menolak|gagal|buruk|jelek|parah|sedih|nyesel|menyesal|"
r"benci|muak|frustrasi|merugikan|menyusahkan)\b", 1),
(r"\b(kecewa|marah|kesal|rugi|susah|ribet|mahal|repot|lambat|lelet|"
r"tolak|menolak|gagal|buruk|jelek|parah|sedih|nyesel|menyesal|"
r"benci|muak|frustrasi|merugikan|menyusahkan)\b.{0,15}"
r"\b(sangat|banget|bgt|bngt|bget|bgtt|amat|sekali)\b", 1),
(r"\b(nyusahin|ribet|maksain|susah-susahin)\b", 2), (r"\b(nyusahin|ribet|maksain|susah-susahin)\b", 2),
# "mending urusin yang lain / yang lebih penting" (r"\bmending\b.{1,30}\b(ngurusin|urusin|urus)\b", 1),
(r"\bmending\b.{1,30}\b(urusin|urus)\b", 1), (r"\bmalah\b.{1,30}\b(ngurusin|urusin|urus)\b", 1),
# "kebijakan begini / aturan begini" — menyiratkan tidak setuju (r"\bjangan\b.{1,30}\b(ngurusin|urusin|urus)\b", 1),
(r"\bmarah\b.{1,30}\b(benci|emosi)\b", 1),
(r"\b(kebijakan|aturan|regulasi)\b.{0,20}\bbegini\b", 1), (r"\b(kebijakan|aturan|regulasi)\b.{0,20}\bbegini\b", 1),
# "apa gunanya / apa manfaatnya" — retorikal negatif
(r"\bapa\b.{0,10}\b(guna|manfaat|untung)\b.{0,10}(nya|sih|ini)\b", 1), (r"\bapa\b.{0,10}\b(guna|manfaat|untung)\b.{0,10}(nya|sih|ini)\b", 1),
(r"\b(buat apa sih|ngapain juga|emang perlu)\b", 1),
(r"\bkapan\b.{0,15}\b(beres|selesai|kelar)\b", 1),
(r"\bkatanya\b.{1,40}\btapi\b", 1),
(r"\bujung-ujungnya\b", 1),
(r"\bphp\b", 1),
(r"\bharapan palsu\b", 1),
(r"\bpemberi harapan palsu\b", 1),
(r"\b(tidak|gak|ga|gk|nggak|ngga|tak|kagak|kaga)\s+setuju\b", 2),
(r"\b(tidak|gak|ga|gk|nggak|ngga|tak)\s+sependapat\b", 2),
(r"\bkontra\b.{0,40}\b(kebijakan|aturan|regulasi|ongkir)\b", 1),
(r"\bmenolak\b.{0,40}\b(kebijakan|aturan|regulasi|ongkir)\b", 1),
(r"\b(cuma|hanya)\s+(bikin|nambah|menambah|membuat)\b", 1),
(r"\bmakin\s+(bikin|nambah|menambah|membuat)\b", 1),
]
# >>> FIX v5 — MASALAH 14: pola konteks positif — pujian eksplisit
# terhadap keputusan/kebijakan/langkah yang menyebut kata "tepat",
# tapi HANYA jika TIDAK diikuti pola pembanding "daripada" dalam jarak
# dekat (negative lookahead), supaya "lebih tepat X daripada Y" tetap
# tidak dihitung sebagai pujian berdiri sendiri (itu tetap ditangani
# oleh POLA_KOMPARATIF_NEGATIF di atas).
POLA_KONTEKS_POSITIF = [
(r"\b(keputusan|kebijakan|langkah)\b.{0,15}\btepat\b(?!.{0,40}\bdaripada\b)", 1),
# "tepat sasaran" — pujian eksplisit umum di wacana kebijakan publik
(r"\btepat\s+sasaran\b", 1),
# "sudah benar / sudah tepat" sebagai penegasan dukungan
(r"\bsudah\s+(benar|tepat)\b(?!.{0,40}\bdaripada\b)", 1),
]
# ── Pola frasa negatif tambahan ──────────────────────────────────────
POLA_FRASA_NEGATIF_TAMBAHAN = [
(r"\bsok\s+tahu\b", 2),
(r"\bsok\s+pintar\b", 2),
(r"\bsok\s+bijak\b", 2),
(r"\b(gk|gak|ga|tidak|tak|ngga|nggak|kagak)\s+jelas\b", 2),
(r"\b(gk|gak|ga|tidak|tak|ngga|nggak|kagak)\s+prioritas\b", 2),
(r"\bg[ae]je+\b", 1),
(r"\bsusah-susahin\b", 2),
(r"\b(kerja|kerjanya)\b.{0,10}\b(gak|ga|tidak|tak)\b.{0,10}\bbecus\b", 2),
(r"\basal[\s-]asalan\b", 2),
(r"\bbuang[\s-]buang\s+waktu\b", 2),
(r"\b(gk|gak|ga|tidak|tak|ngga|nggak|kagak)\s+masuk\s+akal\b", 2),
(r"\bomon[\s-]omon\b|\bomong\s+kosong\b", 2),
] ]
# Kata informatif — menunjukkan tweet berisi pelaporan/informasi netral # Kata informatif — menunjukkan tweet berisi pelaporan/informasi netral
# >>> FIX v5 — MASALAH 15: ditambah "diskusi", "disebut", "dibahas",
# "bahas", "ramai", "mengenai", "terkait" — sangat umum pada tweet yang
# cuma menyebut topik tanpa opini ("disebut dalam diskusi mengenai...",
# "sedang ramai dibahas").
KATA_INFORMATIF = { KATA_INFORMATIF = {
"berita", "melihat", "membaca", "mendengar", "mengetahui", "berita", "melihat", "membaca", "mendengar", "mengetahui",
"laporan", "informasi", "kabar", "melaporkan", "dikabarkan", "laporan", "informasi", "kabar", "melaporkan", "dikabarkan",
"menurut", "dilaporkan", "diberitakan", "dikutip", "mengutip", "menurut", "dilaporkan", "diberitakan", "dikutip", "mengutip",
"rilis", "merilis", "mengumumkan", "pengumuman", "update",
"pembaruan", "siaran", "press release", "konferensi pers",
# >>> FIX v5 — MASALAH 15
"diskusi", "disebut", "dibahas", "bahas", "ramai", "mengenai",
"terkait", "membahas", "perbincangan", "dibicarakan", "wacana",
} }
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════
# LOAD NORMALISASI DARI FILE # LOAD NORMALISASI DARI FILE
# PERUBAHAN: Hapus override 'mending' → 'lebih baik' karena
# mengubah kata kritis negatif menjadi sinyal positif.
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════
def _load_normalization() -> dict: def _load_normalization() -> dict:
""" """
Muat kamus normalisasi dari file eksternal. Muat kamus normalisasi dari file eksternal.
PERBAIKAN di versi ini:
- 'mending' dan 'mendingan' TIDAK lagi dioverride ke 'lebih baik'
karena mengakibatkan kata negatif/kritis menjadi sinyal positif
di lexicon scoring. Kata ini dibiarkan apa adanya agar POLA_KOMPARATIF
dan POLA_KRITIK dapat mendeteksinya.
- Override 'malah' dihapus file normalisasi umum mengubah
'malah' 'bahkan' yang bisa mengubah konteks sentimen.
""" """
norm_file = "indonesian-normalisasi-slangword-complete.txt" norm_file = "indonesian-normalisasi-slangword-complete.txt"
norm_dict: dict = {} norm_dict: dict = {}
@ -252,67 +530,51 @@ def _load_normalization() -> dict:
except FileNotFoundError: except FileNotFoundError:
pass pass
# ── Override khusus domain ───────────────────────────────────────────────
# Entri ini menimpa file generik. Perhatikan komentar DIHAPUS di bawah.
DOMAIN_OVERRIDES: dict = { DOMAIN_OVERRIDES: dict = {
# Nama platform — pertahankan apa adanya
"shopee": "shopee", "shopee": "shopee",
"tokopedia": "tokopedia", "tokopedia": "tokopedia",
"lazada": "lazada", "lazada": "lazada",
"tiktok": "tiktok", "tiktok": "tiktok",
"bukalapak": "bukalapak", "bukalapak": "bukalapak",
"blibli": "blibli", "blibli": "blibli",
# Logistik "chaos": "kacau",
"overlapping": "tumpang tindih",
"overlap": "tumpang tindih",
"sicepat": "sicepat", "sicepat": "sicepat",
"jne": "jne", "jne": "jne",
"jnt": "jnt", "jnt": "jnt",
"anteraja": "anteraja", "anteraja": "anteraja",
"ninja": "ninja", "ninja": "ninja",
# Ongkir & belanja
"freeongkir": "gratis ongkos kirim", "freeongkir": "gratis ongkos kirim",
"gratisongkir": "gratis ongkos kirim", "gratisongkir": "gratis ongkos kirim",
"ongkir": "ongkos kirim", "ongkir": "ongkos kirim",
"ongkr": "ongkos kirim", "ongkr": "ongkos kirim",
"bykrm": "biaya kirim", "bykrm": "biaya kirim",
"biayakirim": "biaya pengiriman", "biayakirim": "biaya pengiriman",
# Kebijakan & lembaga
"komdigi": "komdigi", "komdigi": "komdigi",
"kemendag": "kementerian perdagangan", "kemendag": "kementerian perdagangan",
"kominfo": "kementerian komunikasi", "kominfo": "kementerian komunikasi",
# E-commerce umum
"ecommerce": "e commerce", "ecommerce": "e commerce",
"marketplace": "marketplace", "marketplace": "marketplace",
"seller": "penjual", "seller": "penjual",
"buyer": "pembeli", "buyer": "pembeli",
"online": "online", "online": "online",
# Negasi informal — pastikan ditangkap
"gk": "tidak", "ga": "tidak", "gak": "tidak", "gk": "tidak", "ga": "tidak", "gak": "tidak",
"nggak": "tidak", "ngga": "tidak", "tdk": "tidak", "nggak": "tidak", "ngga": "tidak", "tdk": "tidak",
"tak": "tidak", "enggak": "tidak", "engga": "tidak", "tak": "tidak", "enggak": "tidak", "engga": "tidak",
"kagak": "tidak", "kaga": "tidak", "ndak": "tidak", "kagak": "tidak", "kaga": "tidak", "ndak": "tidak",
"ngak": "tidak", "ngak": "tidak",
# Intensitas
"bgt": "banget", "bngt": "banget", "bget": "banget", "bgtt": "banget", "bgt": "banget", "bngt": "banget", "bget": "banget", "bgtt": "banget",
# Positif informal — hanya yang benar-benar positif
"mantep": "mantap", "mntap": "mantap", "mantep": "mantap", "mntap": "mantap",
"kece": "keren", "kece": "keren",
"ancur": "hancur", "parahh": "parah", "ancur": "hancur", "parahh": "parah",
# ── SENGAJA TIDAK DIOVERRIDE (vs versi lama): ──────────────────────── # >>> FIX v5 — MASALAH 14/15 pendukung: beberapa slang umum
# "mending" → TIDAK diubah ke "lebih baik" # tambahan supaya pola informatif/positif baru bisa kena match
# Alasan: "mending X daripada Y" adalah ekspresi kritik. # juga pada bentuk slang-nya.
# Mengubah ke "lebih baik" membuat lexicon menangkap 'baik' = POSITIF, "dibahas": "dibahas",
# padahal kalimatnya bermakna negatif/kritik. Biarkan "mending" apa "disebut": "disebut",
# adanya agar POLA_KOMPARATIF_NEGATIF bisa mendeteksinya. # "mending"/"mendingan"/"malah" SENGAJA TIDAK dioverride (lihat
# # catatan panjang di versi sebelumnya — tetap berlaku).
# "mendingan" → TIDAK diubah ke "lebih baik" (alasan sama)
#
# "malah" → TIDAK dioverride. File normalisasi mengubah 'mlah'→'malah'
# yang sudah benar; 'malah' sendiri tidak perlu diubah ke 'bahkan'
# karena mengubah nuansa kritis.
#
# "sip" → TIDAK dioverride ke "baik". "sip" cukup dikenal dan
# berdiri sendiri sebagai apresiasi. "baik" sudah dihapus dari
# lexicon karena terlalu kontekstual.
} }
norm_dict.update(DOMAIN_OVERRIDES) norm_dict.update(DOMAIN_OVERRIDES)
return norm_dict return norm_dict
@ -323,15 +585,6 @@ def _load_normalization() -> dict:
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════
def _load_stopwords() -> set: def _load_stopwords() -> set:
"""
Muat daftar stopword dari file eksternal.
PERBAIKAN di versi ini:
- Pertahankan 'mending' dari stopword removal. Kata ini penting
sebagai penanda POLA_KOMPARATIF_NEGATIF dan POLA_KRITIK_TERSIRAT.
- Pertahankan 'daripada' dari stopword removal. Kata ini adalah
komponen kunci pola "mending X daripada Y".
"""
stopword_file = "indonesian-stopwords-complete.txt" stopword_file = "indonesian-stopwords-complete.txt"
base: set = set() base: set = set()
try: try:
@ -348,30 +601,50 @@ def _load_stopwords() -> set:
"jika", "sudah", "telah", "jadi", "bisa", "jika", "sudah", "telah", "jadi", "bisa",
} }
# ── Lindungi kata sentimen penting ──────────────────────────────────────
for kata in KATA_SENTIMEN_PENTING: for kata in KATA_SENTIMEN_PENTING:
base.discard(kata) base.discard(kata)
# ── Lindungi kata penanda pola kontekstual ──────────────────────────────
# Kata-kata ini diperlukan agar POLA_KOMPARATIF dan POLA_KRITIK bisa
# mendeteksi struktur kalimat dengan benar di teks yang sudah bersih.
KATA_POLA_PENTING: set = { KATA_POLA_PENTING: set = {
"mending", # penanda pola komparatif negatif "mending", "mendingan", "daripada", "ketimbang",
"mendingan", # variasi mending "ngapain", "percuma", "begini",
"daripada", # komponen "mending X daripada Y" "gajelas", "mahal", "nyusahin",
"ketimbang", # variasi daripada "malas", "males", "enggan",
"ngapain", # penanda kritik tersirat "prioritas", "jelas", "gajeee", "susah-susahin",
"percuma", # penanda sia-sia "sok", "tahu", "pintar", "bijak",
"begini", # "kebijakan begini" = kritik tersirat "tai", "taik",
"gajelas", # "gajelas aja kebijakan ini" = tidak jelas/negatif "aneh", "absurd", "konyol",
"mahal", # "mahal banget kebijakan ini" = negatif "miris", "ironis", "memalukan", "memprihatinkan",
"nyusahin", # penanda ribet/susah-susahin "dikesampingkan",
"malas", # "malas banget urus kebijakan ini" = negatif "gagal", "buruk", "jelek", "parah",
"ribet", "repot", "susah", "sulit", "rumit",
"ngurusin", "ngurusi", "urusin", "urus", "ngurus",
"sibuk", "melempem",
"ckckck", "astaga",
"blokir", "hambat", "diskriminasi", "zalim",
"malah",
"alih-alih", "harusnya", "kapan", "beres", "selesai", "kelar",
"katanya", "ujung-ujungnya", "php", "becus", "asal", "asalan",
"omon", "omong", "kosong",
"kontra", "menolak", "sependapat",
"dibatasi", "membatasi", "batasi", "terbatas", "batasan",
"cuma", "hanya", "bikin", "makin", "nambah", "menambah",
# >>> FIX v4 (dari pengguna)
"chaos", "kacau", "tahi", "jir", "samping", "sampingkan",
"kesampingkan", "kesampingan", "dikesampingan",
# >>> FIX v5 — MASALAH 14/15: kata penanda pola baru juga harus
# dijaga dari stopword removal agar regex di atas (yang bekerja
# di atas teks penuh) tetap punya kata-kata ini utuh.
"tepat", "sasaran", "benar",
"diskusi", "disebut", "dibahas", "bahas", "ramai", "mengenai",
"terkait", "membahas", "perbincangan", "dibicarakan", "wacana",
} }
for kata in KATA_POLA_PENTING: for kata in KATA_POLA_PENTING:
base.discard(kata) base.discard(kata)
# ── Tambah noise Twitter/sosmed ─────────────────────────────────────────
base.update({ base.update({
"rt", "amp", "https", "http", "co", "pic", "rt", "amp", "https", "http", "co", "pic",
"wkwk", "wkwkwk", "wkwkwkwk", "wkwk", "wkwkwk", "wkwkwkwk",
@ -381,7 +654,6 @@ def _load_stopwords() -> set:
}) })
return base return base
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════
# LOAD STEMMER # LOAD STEMMER
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════
@ -394,7 +666,6 @@ def _load_stemmer():
return None return None
# ── Inisialisasi global ──────────────────────────────────────────────────────
_STOPWORDS = _load_stopwords() _STOPWORDS = _load_stopwords()
_STEMMER = _load_stemmer() _STEMMER = _load_stemmer()
_NORM_DICT = _load_normalization() _NORM_DICT = _load_normalization()
@ -425,7 +696,6 @@ def _load_model():
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════
# 5-TAHAP PREPROCESSING PIPELINE # 5-TAHAP PREPROCESSING PIPELINE
# IDENTIK dengan preprocessing_page.py
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════
def _step1_case_folding(text: str) -> str: def _step1_case_folding(text: str) -> str:
@ -494,33 +764,21 @@ def preprocess_untuk_lexicon(text: str) -> str:
""" """
Preprocessing untuk lexicon matching. Preprocessing untuk lexicon matching.
PENTING: TIDAK distem dan TIDAK dihapus stopword-nya agar PENTING: TIDAK distem dan TIDAK dihapus stopword-nya agar
pola kontekstual (mending, daripada, begini, dll.) tetap ada pola kontekstual tetap ada dan bisa dideteksi.
dan bisa dideteksi oleh POLA_KOMPARATIF dan POLA_KRITIK.
""" """
s1 = _step1_case_folding(text) s1 = _step1_case_folding(text)
s2 = _step2_cleaning(s1) s2 = _step2_cleaning(s1)
s3 = _step3_normalization(s2, _NORM_DICT) s3 = _step3_normalization(s2, _NORM_DICT)
return s3 # Kembalikan setelah normalisasi saja — stopword & stem TIDAK dilakukan return s3
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════
# LEXICON SCORER (DIPERBAIKI — pola kontekstual + pembatasan) # LEXICON SCORER
# ═══════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════
def _hitung_skor_lexicon(teks_lexicon: str) -> dict: def _hitung_skor_lexicon(teks_lexicon: str) -> dict:
""" """
Hitung skor sentimen via lexicon + pola kontekstual + negation handling. Hitung skor sentimen via lexicon + pola kontekstual + negation handling.
PERUBAHAN UTAMA vs versi lama:
1. POLA_KOMPARATIF_NEGATIF: "mending X daripada Y" tambah skor negatif
2. POLA_KRITIK_TERSIRAT: "tidak penting", "gak usah", dll. skor negatif
3. POLA 'pembatasan gratis': 'gratis' tidak dihitung positif jika
didahului 'pembatasan' dalam window 3 kata
4. DETEKSI TWEET INFORMATIF: jika teks hanya berisi konteks informasi
tanpa kata sentimen eksplisit, tandai sebagai 'informatif'
5. Window negasi tetap 3 kata (dari versi sebelumnya)
Return: {"positif": int, "negatif": int, "net": int, "informatif": bool} Return: {"positif": int, "negatif": int, "net": int, "informatif": bool}
""" """
tokens = teks_lexicon.split() tokens = teks_lexicon.split()
@ -537,47 +795,59 @@ def _hitung_skor_lexicon(teks_lexicon: str) -> dict:
if re.search(pola, teks_lexicon): if re.search(pola, teks_lexicon):
skor_neg += bobot skor_neg += bobot
# ── Tahap 2b: Deteksi pola frasa negatif tambahan ─────────────────────
for pola, bobot in POLA_FRASA_NEGATIF_TAMBAHAN:
if re.search(pola, teks_lexicon):
skor_neg += bobot
# >>> FIX v5 — MASALAH 14: Deteksi pola konteks positif (pujian
# eksplisit "keputusan/kebijakan tepat", "tepat sasaran", dst.)
for pola, bobot in POLA_KONTEKS_POSITIF:
if re.search(pola, teks_lexicon):
skor_pos += bobot
# ── Tahap 3: Lexicon per token ──────────────────────────────────────── # ── Tahap 3: Lexicon per token ────────────────────────────────────────
for i, token in enumerate(tokens): for i, token in enumerate(tokens):
# Negation window: 3 kata sebelumnya
ada_negasi = any( ada_negasi = any(
tokens[i - j] in KATA_NEGASI tokens[i - j] in KATA_NEGASI
for j in range(1, 4) for j in range(1, 4)
if i - j >= 0 if i - j >= 0
) )
# Konteks pembatasan: 'gratis' setelah 'pembatasan' tidak = positif ada_pembatasan = (
ada_pembatasan = any( any(tokens[i - j] in KATA_PEMBATASAN for j in range(1, 4) if i - j >= 0)
tokens[i - j] == "pembatasan" or any(tokens[i + j] in KATA_PEMBATASAN for j in range(1, 4) if i + j < len(tokens))
for j in range(1, 4) )
if i - j >= 0 ada_intensitas = (
any(tokens[i - j] in KATA_INTENSITAS for j in (1, 2) if i - j >= 0)
or any(tokens[i + j] in KATA_INTENSITAS for j in (1, 2) if i + j < len(tokens))
) )
if token in LEXICON_POSITIF: if token in LEXICON_POSITIF:
if ada_negasi: if ada_negasi:
skor_neg += 1 skor_neg += 1
if ada_intensitas:
skor_neg += 1
elif ada_pembatasan and token == "gratis": elif ada_pembatasan and token == "gratis":
# "pembatasan gratis ongkir" = konteks negatif/netral,
# bukan pujian terhadap 'gratis'. Lewati.
pass pass
else: else:
skor_pos += 1 skor_pos += 1
if ada_intensitas:
skor_pos += 1
elif token in LEXICON_NEGATIF: elif token in LEXICON_NEGATIF:
if ada_negasi: if ada_negasi:
skor_pos += 1 skor_pos += 1
else: else:
skor_neg += 1 skor_neg += 1
if ada_intensitas:
skor_neg += 1
# ── Tahap 4: Deteksi tweet informatif ──────────────────────────────── # ── Tahap 4: Deteksi tweet informatif ────────────────────────────────
# Tweet informatif: mengandung kata pelaporan, TIDAK ada kata sentimen
# eksplisit, dan panjang teks tidak terlalu pendek.
token_set = set(tokens) token_set = set(tokens)
ada_info_kata = bool(token_set & KATA_INFORMATIF) ada_info_kata = bool(token_set & KATA_INFORMATIF)
ada_sentimen_eksplisit = bool( ada_sentimen_eksplisit = bool(
(token_set & LEXICON_POSITIF) | (token_set & LEXICON_NEGATIF) (token_set & LEXICON_POSITIF) | (token_set & LEXICON_NEGATIF)
) )
# Informatif jika: ada kata informatif + tidak ada sentimen eksplisit
# + skor negatif dari pola kecil (≤1, artinya cuma 'pembatasan')
is_informatif = ( is_informatif = (
ada_info_kata ada_info_kata
and not ada_sentimen_eksplisit and not ada_sentimen_eksplisit
@ -585,8 +855,8 @@ def _hitung_skor_lexicon(teks_lexicon: str) -> dict:
) )
return { return {
"positif": skor_pos, # ← wajib ada "positif": skor_pos,
"negatif": skor_neg, # ← wajib ada "negatif": skor_neg,
"net": skor_pos - skor_neg, "net": skor_pos - skor_neg,
"informatif": is_informatif, "informatif": is_informatif,
} }
@ -607,10 +877,6 @@ _LABEL_MAP = {
def _prediksi_model(teks_model: str): def _prediksi_model(teks_model: str):
"""
Prediksi dari model NB 3-kelas.
Return: (label_norm, confidence, proba_dict) atau None jika gagal.
"""
model, tfidf = _load_model() model, tfidf = _load_model()
if model is None or tfidf is None or not teks_model.strip(): if model is None or tfidf is None or not teks_model.strip():
return None return None
@ -646,45 +912,51 @@ def _klasifikasi_hybrid(
""" """
Klasifikasi hybrid: sinyal lexicon + pola kontekstual + model NB. Klasifikasi hybrid: sinyal lexicon + pola kontekstual + model NB.
ALUR KEPUTUSAN: ALUR KEPUTUSAN (v5):
Layer 0 Override Informatif: Layer 0 >>> FIX v5 (MASALAH 12, baru): Override Informatif NYATA.
Jika skor['informatif'] = True DAN net mendekati 0 Netral langsung. Sebelumnya is_info dihitung tapi tidak pernah dipakai (dead code).
Mencegah tweet berita/informatif terklasifikasi positif/negatif Sekarang: jika tweet terdeteksi informatif DAN net == 0 (tidak ada
karena kata domain (mis. 'gratis' dalam "berita mengenai gratis ongkir"). sinyal sentimen eksplisit/pola apapun) Netral langsung, sebelum
model NB dikonsultasikan. Hanya net == 0 yang dipakai (bukan
abs(net) <= 1) supaya tweet yang punya opini lemah tetap diberi
kesempatan diproses Layer 2a/2b di bawah.
Layer 1a Override Positif KUAT (net >= 2): Layer 0a >>> FIX v5 (MASALAH 13, baru): Override Zero-Signal.
Sinyal lexicon + pola sangat kuat Positif langsung. Jika skor_pos == 0 DAN skor_neg == 0 (tidak ada sinyal lexicon
ATAU pola apapun ditemukan sama sekali), langsung Netral. Model
NB terbukti bias ke Positif untuk tweet yang hanya menyebut kata
domain ('gratis ongkir', 'komdigi', dst.) tanpa kata sentimen
apapun, sehingga jika tidak ada sinyal sama sekali, default paling
aman adalah Netral, bukan menyerahkan ke model.
Layer 1b Override Negatif KUAT (net <= -2): Layer 0b Override dominasi kata (selisih 2):
Sinyal lexicon + pola sangat kuat Negatif langsung. Dua blok if independen yang sejajar (bug indentasi versi lama
CATATAN: Pola komparatif "mending X daripada Y" menambah net -2/-3 sudah diperbaiki di v3).
sehingga tweet kritik implisit langsung terdeteksi di sini.
Layer 2a Override Positif LEMAH (net == 1): Layer 1a Override Positif KUAT (net >= 2)
Konsultasi model. Jika model tidak sangat yakin Negatif Positif. Layer 1b Override Negatif KUAT (net <= -2)
Layer 2a Override Positif LEMAH (net == 1), konsultasi model
Layer 2b Override Negatif LEMAH (net == -1): Layer 2b Override Negatif LEMAH (net == -1), konsultasi model
Konsultasi model. Jika model tidak sangat yakin Positif Negatif. Layer 3 Fallback Model NB (dengan anti-bias)
Layer 4 Ultimate Fallback (model tidak tersedia)
Layer 3 Fallback Model NB:
Anti-bias:
- Model Negatif + lexicon bersih (net >= 0) + conf < 0.65 Netral
- Model Positif + lexicon negatif (net <= -1) + conf < 0.65 Netral
Layer 4 Ultimate Fallback (model tidak tersedia):
Gunakan sinyal lexicon saja.
Return: (label: str, confidence: float) Return: (label: str, confidence: float)
""" """
net = skor["net"] net = skor["net"]
skor_pos = skor["positif"] # ← pastikan dict ini return nilai ini skor_pos = skor["positif"]
skor_neg = skor["negatif"] # ← pastikan dict ini return nilai ini skor_neg = skor["negatif"]
is_info = skor.get("informatif", False) is_info = skor.get("informatif", False)
# ── TAMBAHAN Layer 0b: Override dominasi kata ───────── # ── Layer 0: Override Informatif (FIX v5 — MASALAH 12) ───────────────
# Jika salah satu sisi punya ≥2 kata lebih banyak dari sisi lain, if is_info and net == 0:
# langsung putuskan tanpa menunggu model NB. return ("Netral", 0.60)
# ── Layer 0a: Override Zero-Signal (FIX v5 — MASALAH 13) ─────────────
if skor_pos == 0 and skor_neg == 0:
return ("Netral", 0.55)
# ── Layer 0b: Override dominasi kata ──────────────────────────────────
if skor_neg >= 2 and skor_neg > skor_pos: if skor_neg >= 2 and skor_neg > skor_pos:
conf = min(0.60 + (skor_neg - skor_pos) * 0.05, 0.90) conf = min(0.60 + (skor_neg - skor_pos) * 0.05, 0.90)
return ("Negatif", round(conf, 3)) return ("Negatif", round(conf, 3))
@ -699,8 +971,6 @@ def _klasifikasi_hybrid(
return ("Positif", round(conf, 3)) return ("Positif", round(conf, 3))
# ── Layer 1b: Negatif KUAT ──────────────────────────────────────────── # ── Layer 1b: Negatif KUAT ────────────────────────────────────────────
# Pola komparatif ("mending X daripada Y") memberi net -2 hingga -4,
# sehingga tweet kritik implisit langsung tertangkap di layer ini.
if net <= -2: if net <= -2:
conf = min(0.65 + (abs(net) * 0.05), 0.92) conf = min(0.65 + (abs(net) * 0.05), 0.92)
return ("Negatif", round(conf, 3)) return ("Negatif", round(conf, 3))
@ -736,12 +1006,10 @@ def _klasifikasi_hybrid(
if model_result is not None: if model_result is not None:
label_norm, confidence, proba_dict = model_result label_norm, confidence, proba_dict = model_result
# Anti-bias 1: Model Negatif tapi lexicon bersih → Netral
if label_norm == "Negatif" and net >= 0 and confidence < 0.65: if label_norm == "Negatif" and net >= 0 and confidence < 0.65:
corrected_conf = round(0.50 + max(0, confidence - 0.50) * 0.20, 3) corrected_conf = round(0.50 + max(0, confidence - 0.50) * 0.20, 3)
return ("Netral", corrected_conf) return ("Netral", corrected_conf)
# Anti-bias 2: Model Positif tapi lexicon negatif → Netral
if label_norm == "Positif" and net <= -1 and confidence < 0.65: if label_norm == "Positif" and net <= -1 and confidence < 0.65:
corrected_conf = round(0.50 + max(0, confidence - 0.50) * 0.20, 3) corrected_conf = round(0.50 + max(0, confidence - 0.50) * 0.20, 3)
return ("Netral", corrected_conf) return ("Netral", corrected_conf)

View File

@ -1,8 +1,17 @@
""" """
splash_page.py splash_page.py
Splash screen modern, full-viewport centered, tampil sekali per sesi. Splash screen modern, full-viewport centered.
Menggunakan st.components.v1.html (iframe isolated). Hanya tampil SEKALI saat pertama kali sistem diakses (browser baru/tab baru).
Jika halaman di-refresh (F5), splash TIDAK akan muncul lagi karena status
"sudah pernah splash" disimpan di URL query parameter, bukan di session_state.
KENAPA TIDAK PAKAI session_state SAJA?
session_state Streamlit terikat ke session koneksi, bukan ke browser/tab.
Pada beberapa kondisi (reconnect websocket, refresh di beberapa environment
hosting), session_state bisa ter-reset sehingga splash muncul lagi padahal
user hanya refresh. query_params (di URL) tidak hilang saat refresh biasa,
sehingga lebih andal untuk menandai "splash sudah pernah tampil".
🕐 PENGATURAN DURASI: 🕐 PENGATURAN DURASI:
_SPLASH_DURATION = 10 ganti angka ini (satuan: detik) _SPLASH_DURATION = 10 ganti angka ini (satuan: detik)
@ -24,6 +33,10 @@ _APP_NAME = "SentiTrack"
_SPLASH_DURATION = 10 _SPLASH_DURATION = 10
# └─────────────────────────────────────────┘ # └─────────────────────────────────────────┘
# Nama parameter URL yang dipakai sebagai "penanda" splash sudah tampil.
# Tidak perlu diubah, kecuali bentrok dengan query param lain di app kamu.
_SPLASH_FLAG_KEY = "splashed"
def _build_splash_html(duration: int) -> str: def _build_splash_html(duration: int) -> str:
return f"""<!DOCTYPE html> return f"""<!DOCTYPE html>
@ -46,7 +59,6 @@ body {{
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-height: 100vh; min-height: 100vh;
/* background ditangani oleh .bg */
}} }}
/* Background gradient */ /* Background gradient */
@ -98,17 +110,8 @@ body {{
border: 1.5px solid rgba(226,232,240,0.85); border: 1.5px solid rgba(226,232,240,0.85);
border-radius: 24px; border-radius: 24px;
padding: 2.5rem 2.75rem 2rem; padding: 2.5rem 2.75rem 2rem;
/*
*
* 📐 LEBAR CARD
* Ubah max-width untuk memperlebar / mempersempit card.
* Rekomendasi: 460px 580px
*
*/
width: calc(100% - 2.5rem); width: calc(100% - 2.5rem);
max-width: 520px; max-width: 520px;
box-shadow: box-shadow:
0 0 0 1px rgba(255,255,255,0.5) inset, 0 0 0 1px rgba(255,255,255,0.5) inset,
0 8px 40px rgba(59,130,246,0.09), 0 8px 40px rgba(59,130,246,0.09),
@ -229,14 +232,6 @@ body {{
height: 100%; height: 100%;
background: linear-gradient(90deg, #3b6cf7, #6366f1, #818cf8); background: linear-gradient(90deg, #3b6cf7, #6366f1, #818cf8);
border-radius: 99px; border-radius: 99px;
/*
*
* PROGRESS BAR DURATION
* Nilai ini otomatis mengikuti _SPLASH_DURATION di atas.
* Tidak perlu diubah manual.
*
*/
animation: progress {duration}s linear forwards; animation: progress {duration}s linear forwards;
width: 0%; width: 0%;
}} }}
@ -322,7 +317,7 @@ body {{
<div class="info-icon">📊</div> <div class="info-icon">📊</div>
<div> <div>
<div class="info-text-label">Output</div> <div class="info-text-label">Output</div>
<div class="info-text-value">Positif · Netral · Negatif</div> <div class="info-text-value">Dashboard Analisis Sentimen</div>
</div> </div>
</div> </div>
</div> </div>
@ -358,10 +353,26 @@ body {{
def maybe_show_splash() -> bool: def maybe_show_splash() -> bool:
""" """
Tampilkan splash jika belum tampil di sesi ini. Tampilkan splash HANYA jika belum pernah tampil di browser ini.
Kembalikan True jika splash baru saja ditampilkan, False jika dilewati.
Mekanisme:
- Saat splash ditampilkan untuk pertama kali, kita menambahkan
query parameter ?splashed=1 ke URL via st.query_params.
- Karena ini tersimpan di URL (bukan di session_state server),
query parameter ini TETAP ADA saat halaman di-refresh (F5).
- Jadi: refresh biasa -> splash TIDAK muncul lagi.
buka tab/browser baru tanpa parameter itu -> splash muncul.
klik tombol "reset splash" (lihat reset_splash()) -> splash
akan muncul lagi di reload berikutnya.
Return:
True -> splash baru saja ditampilkan (halaman akan rerun setelahnya)
False -> splash dilewati (sudah pernah tampil sebelumnya)
""" """
if st.session_state.get("splash_done", False): # Cek apakah flag sudah ada di URL
already_splashed = st.query_params.get(_SPLASH_FLAG_KEY) == "1"
if already_splashed:
return False return False
# Sembunyikan semua elemen Streamlit selama splash # Sembunyikan semua elemen Streamlit selama splash
@ -400,7 +411,23 @@ def maybe_show_splash() -> bool:
# Tunggu sesuai durasi splash sebelum lanjut ke halaman utama # Tunggu sesuai durasi splash sebelum lanjut ke halaman utama
time.sleep(_SPLASH_DURATION + 0.3) time.sleep(_SPLASH_DURATION + 0.3)
st.session_state["splash_done"] = True # Tandai di URL bahwa splash sudah pernah tampil.
# Penanda ini bertahan walau halaman di-refresh (F5).
st.query_params[_SPLASH_FLAG_KEY] = "1"
st.rerun() st.rerun()
return True return True
def reset_splash():
"""
Opsional: panggil fungsi ini (misalnya dari tombol di sidebar)
jika ingin memaksa splash muncul lagi di reload berikutnya.
Contoh penggunaan:
if st.sidebar.button("Tampilkan ulang splash"):
reset_splash()
st.rerun()
"""
if _SPLASH_FLAG_KEY in st.query_params:
del st.query_params[_SPLASH_FLAG_KEY]

Binary file not shown.

BIN
tfidf_vectorizer_old4.pkl Normal file

Binary file not shown.

BIN
tfidf_vectorizer_old5.pkl Normal file

Binary file not shown.

View File

@ -0,0 +1,5 @@
"conversation_id_str","created_at","favorite_count","full_text","id_str","image_url","in_reply_to_screen_name","lang","location","quote_count","reply_count","retweet_count","tweet_url","user_id_str","username"
"2070029328407286237","Thu Jun 25 06:20:25 +0000 2026","0","Kebijakan Komdigi mengenai pembatasan gratis ongkir ini ribet merepotkan dan membuat biaya belanja menjadi mahal.","2070029328407286237","","","in","","0","0","0","https://x.com/undefined/status/2070029328407286237","1814143201718247424",
"2070029891974869391","Thu Jun 25 06:22:40 +0000 2026","0","Mending komdigi ngurus judol daripada sibuk membatasi gratis ongkir marketplace.","2070029891974869391","","","in","","0","0","0","https://x.com/undefined/status/2070029891974869391","1814143201718247424",
"2069684663447368156","Wed Jun 24 07:30:51 +0000 2026","0","aturan pembatasan gratis ongkir komdigi tidak bermanfaat bikin marah padahal jualan lagi sukses","2069684663447368156","","","in","","0","0","0","https://x.com/undefined/status/2069684663447368156","1814143201718247424",
"2069684828677767277","Wed Jun 24 07:31:30 +0000 2026","0","awalnya kesal dengar isu pembatasan gratis ongkir komdigi ternyata tujuannya baik melindungi kurir sangat setuju","2069684828677767277","","","in","","0","0","0","https://x.com/undefined/status/2069684828677767277","1814143201718247424",
1 conversation_id_str created_at favorite_count full_text id_str image_url in_reply_to_screen_name lang location quote_count reply_count retweet_count tweet_url user_id_str username
2 2070029328407286237 Thu Jun 25 06:20:25 +0000 2026 0 Kebijakan Komdigi mengenai pembatasan gratis ongkir ini ribet merepotkan dan membuat biaya belanja menjadi mahal. 2070029328407286237 in 0 0 0 https://x.com/undefined/status/2070029328407286237 1814143201718247424
3 2070029891974869391 Thu Jun 25 06:22:40 +0000 2026 0 Mending komdigi ngurus judol daripada sibuk membatasi gratis ongkir marketplace. 2070029891974869391 in 0 0 0 https://x.com/undefined/status/2070029891974869391 1814143201718247424
4 2069684663447368156 Wed Jun 24 07:30:51 +0000 2026 0 aturan pembatasan gratis ongkir komdigi tidak bermanfaat bikin marah padahal jualan lagi sukses 2069684663447368156 in 0 0 0 https://x.com/undefined/status/2069684663447368156 1814143201718247424
5 2069684828677767277 Wed Jun 24 07:31:30 +0000 2026 0 awalnya kesal dengar isu pembatasan gratis ongkir komdigi ternyata tujuannya baik melindungi kurir sangat setuju 2069684828677767277 in 0 0 0 https://x.com/undefined/status/2069684828677767277 1814143201718247424

View File

@ -1,3 +1,5 @@
"conversation_id_str","created_at","favorite_count","full_text","id_str","image_url","in_reply_to_screen_name","lang","location","quote_count","reply_count","retweet_count","tweet_url","user_id_str","username" "conversation_id_str","created_at","favorite_count","full_text","id_str","image_url","in_reply_to_screen_name","lang","location","quote_count","reply_count","retweet_count","tweet_url","user_id_str","username"
"2065876194948743503","Sat Jun 13 19:17:21 +0000 2026","0","setuju komdigi mengeluarkan gratis ongkir sehingga saya menjadi malas dan kecewa","2065876194948743503","","","in","","0","0","0","https://x.com/undefined/status/2065876194948743503","1814143201718247424", "2070029328407286237","Thu Jun 25 06:20:25 +0000 2026","0","Kebijakan Komdigi mengenai pembatasan gratis ongkir ini ribet merepotkan dan membuat biaya belanja menjadi mahal.","2070029328407286237","","","in","","0","0","0","https://x.com/undefined/status/2070029328407286237","1814143201718247424",
"2066043912842822069","Sun Jun 14 06:23:48 +0000 2026","0","saya tidak setuju dengan kebijakan komdigi dengan mengeluarkan kebijakan gratis ongkir ini membuat saya jadi senang berbelanja","2066043912842822069","","","in","","0","0","0","https://x.com/undefined/status/2066043912842822069","1814143201718247424", "2070029891974869391","Thu Jun 25 06:22:40 +0000 2026","0","Mending komdigi ngurus judol daripada sibuk membatasi gratis ongkir marketplace.","2070029891974869391","","","in","","0","0","0","https://x.com/undefined/status/2070029891974869391","1814143201718247424",
"2069684663447368156","Wed Jun 24 07:30:51 +0000 2026","0","aturan pembatasan gratis ongkir komdigi tidak bermanfaat bikin marah padahal jualan lagi sukses","2069684663447368156","","","in","","0","0","0","https://x.com/undefined/status/2069684663447368156","1814143201718247424",
"2069684828677767277","Wed Jun 24 07:31:30 +0000 2026","0","awalnya kesal dengar isu pembatasan gratis ongkir komdigi ternyata tujuannya baik melindungi kurir sangat setuju","2069684828677767277","","","in","","0","0","0","https://x.com/undefined/status/2069684828677767277","1814143201718247424",

1 conversation_id_str created_at favorite_count full_text id_str image_url in_reply_to_screen_name lang location quote_count reply_count retweet_count tweet_url user_id_str username
2 2065876194948743503 2070029328407286237 Sat Jun 13 19:17:21 +0000 2026 Thu Jun 25 06:20:25 +0000 2026 0 setuju komdigi mengeluarkan gratis ongkir sehingga saya menjadi malas dan kecewa Kebijakan Komdigi mengenai pembatasan gratis ongkir ini ribet merepotkan dan membuat biaya belanja menjadi mahal. 2065876194948743503 2070029328407286237 in 0 0 0 https://x.com/undefined/status/2065876194948743503 https://x.com/undefined/status/2070029328407286237 1814143201718247424
3 2066043912842822069 2070029891974869391 Sun Jun 14 06:23:48 +0000 2026 Thu Jun 25 06:22:40 +0000 2026 0 saya tidak setuju dengan kebijakan komdigi dengan mengeluarkan kebijakan gratis ongkir ini membuat saya jadi senang berbelanja Mending komdigi ngurus judol daripada sibuk membatasi gratis ongkir marketplace. 2066043912842822069 2070029891974869391 in 0 0 0 https://x.com/undefined/status/2066043912842822069 https://x.com/undefined/status/2070029891974869391 1814143201718247424
4 2069684663447368156 Wed Jun 24 07:30:51 +0000 2026 0 aturan pembatasan gratis ongkir komdigi tidak bermanfaat bikin marah padahal jualan lagi sukses 2069684663447368156 in 0 0 0 https://x.com/undefined/status/2069684663447368156 1814143201718247424
5 2069684828677767277 Wed Jun 24 07:31:30 +0000 2026 0 awalnya kesal dengar isu pembatasan gratis ongkir komdigi ternyata tujuannya baik melindungi kurir sangat setuju 2069684828677767277 in 0 0 0 https://x.com/undefined/status/2069684828677767277 1814143201718247424