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",
# ── Negatif domain e-commerce / ongkir ────────────────
"mahal", "lambat", "lelet", "ribet", "susah", "repot",
"rugi", "boros",
"rugi", "boros", "malah", "nyusahin", "susah-susahin",
# ── Emosi ─────────────────────────────────────────────
"marah", "sedih", "khawatir", "kecewa",
# ── DITAMBAHKAN: Penanda pola kontekstual ─────────────
@ -164,13 +164,18 @@ KATA_SENTIMEN_PENTING = {
"ketimbang", # variasi daripada
"begini", # "kebijakan begini" = kritik tersirat
"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"
"malas", # "malas banget urus kebijakan ini" = negatif
"ga prioritas", "gak prioritas", "nggak prioritas", # "kebijakan ini gak prioritas" = 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',
'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 = {
"mending", # penanda pola komparatif negatif
"malah", # penanda pola komparatif negatif
"mendingan", # variasi mending
"daripada", # komponen "mending X daripada Y"
"ketimbang", # variasi daripada
@ -189,8 +196,16 @@ KATA_POLA_PENTING = {
"percuma", # penanda sia-sia
"begini", # "kebijakan begini" = kritik tersirat
"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
"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",
"kemendag": "kementerian perdagangan",
"kominfo": "kementerian komunikasi",
"gejeee": "geje",
"gk jelas": "tidak jelas",
# E-commerce umum
"ecommerce": "e commerce",
"marketplace": "marketplace",

View File

@ -601,7 +601,7 @@ def _render_tweet_table(fdf, filter_label):
with tf3:
sort_by = st.selectbox(
"Urutkan",
["Terbaru dulu", "Terlama dulu", "Keyakinan tertinggi"],
["Terbaru dulu", "Terlama dulu"],
key="sentiment_table_sort",
)
@ -620,10 +620,8 @@ def _render_tweet_table(fdf, filter_label):
if sort_by == "Terbaru dulu":
tdf = tdf.sort_values("created_at", ascending=False)
elif sort_by == "Terlama dulu":
tdf = tdf.sort_values("created_at", ascending=True)
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["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:
@ -46,6 +46,101 @@ PERBAIKAN UTAMA vs versi sebelumnya:
"saya melihat berita mengenai pembatasan gratis ongkir" NETRAL
SOLUSI: Deteksi KATA_INFORMATIF; jika tweet hanya berisi konteks
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
@ -71,7 +166,7 @@ KATA_SENTIMEN_PENTING = {
"untung", "berhasil", "sukses", "solusi", "manfaat",
"berguna", "membantu", "bantu", "pro", "lanjut",
# Positif domain e-commerce/ongkir
"gratis", "murah", "hemat", "terjangkau", "cepat",
"murah", "hemat", "terjangkau", "cepat",
"aman", "mudah", "praktis", "terpercaya",
# Negatif umum
"kecewa", "mending", "malah", "buruk", "jelek", "parah",
@ -80,17 +175,75 @@ KATA_SENTIMEN_PENTING = {
"mahal", "lambat", "lelet", "ribet", "susah", "repot",
"rugi", "boros",
# Emosi
"marah", "sedih", "khawatir",
"marah", "sedih", "khawatir", "cemas", "resah", "takut", "benci", "sesal", "trauma",
# ── TAMBAHAN ────────────────────────────────────────────
'malas', 'males', 'enggan', 'bete', 'jengkel', 'depresi',
'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:
# Kata masuk lexicon HANYA jika bisa berdiri sendiri sebagai
@ -99,38 +252,65 @@ KATA_SENTIMEN_PENTING = {
# DIHAPUS dari versi lama karena terlalu kontekstual:
# "baik", "benar", "penting", "tepat", "jelas", "transparan",
# "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 = {
'setuju', 'dukung', 'mendukung', 'pro', 'sepakat',
'bagus', 'keren', 'mantap', 'mantep', 'bravo',
'setuju', 'dukung', 'mendukung', 'pro', 'sepakat', 'menjaga', 'melindungi',
'bagus', 'keren', 'mantap', 'mantep', 'bravo', 'kerenn',
'salut', 'apresiasi', 'hebat', 'oke',
'bangga', 'senang', 'suka', 'puas', 'gembira', 'bahagia',
'berhasil', 'sukses', 'berjaya',
'andal', 'handal', 'gercep', 'bijak', 'bermanfaat',
'berguna', 'membantu', 'inovatif',
'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 = {
# Umpatan
'tolol', 'bodoh', 'goblog', 'goblok', 'dungu', 'idiot',
'bego', 'anjing', 'bangsat', 'bajingan', 'brengsek',
'gila', 'edan', 'biadab',
'tolol', 'bodoh', 'goblog', 'goblok', 'guoblog', 'asu', 'guoblok', 'dungu', 'idiot', 'dikesampingkan', 'bego', 'anjing', 'bangsat', 'bajingan', 'brengsek', 'asu', "gajeeee",
'bego', 'anjing', 'bangsat', 'bajingan', 'brengsek', 'asu', "gajeeee", "bangsat", "bajingan", "brengsek", "asu", "gaje", "gajelas",
'gila', 'edan', 'biadab', 'anjir', 'taik', 'tai', 'sok', 'malah', "keguoblokan", "kegoblokan", "kebodohan", "kebegoan", "kegilaan", "kebiadaban", "ketololan",
# 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',
# Ketidakjujuran
'bohong', 'tipu', 'menipu', 'penipuan', 'curang',
'manipulasi', 'korupsi', 'koruptor', 'maling', 'skandal',
# EMOSI NEGATIF — ini yang sebelumnya tidak ada
# EMOSI NEGATIF
'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',
'sedih', 'menyesal', 'nyesel', 'sesal', 'kapok', 'trauma',
'malas', # ROOT CAUSE — wajib ada
'males', # slang malas
'malas',
'males',
'enggan', 'bete', 'bosan', 'jenuh',
# Penolakan
'menolak', 'tolak', 'protes', 'keberatan', 'kontra',
@ -145,11 +325,34 @@ LEXICON_NEGATIF = {
'susah', 'sulit', 'rumit',
# Domain Komdigi
'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
'ckckck', 'astaga',
# 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 = {
@ -158,81 +361,156 @@ KATA_NEGASI = {
"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-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 = [
# "mending X daripada Y" — membandingkan, menyiratkan Y tidak layak
(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),
# "daripada X, mending Y" — variasi urutan
(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"\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 = [
# "tidak penting / gak penting" — dismissif
(r"\b(tidak|tak|gak|ga|ngga|nggak)\b.{0,15}\bpenting\b", 1),
(r"\b(tidak|tak|gak|ga|ngga|nggak)\b.{0,15}\bjelas\b", 1),
(r"\b(tidak|tak|gak|ga|gk|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}\bbecus\b", 1),
# "ngapain / buat apa / untuk apa" + konteks kebijakan
(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),
(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),
# "percuma / sia-sia / buang-buang"
(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),
# "mending urusin yang lain / yang lebih penting"
(r"\bmending\b.{1,30}\b(urusin|urus)\b", 1),
# "kebijakan begini / aturan begini" — menyiratkan tidak setuju
(r"\bmending\b.{1,30}\b(ngurusin|urusin|urus)\b", 1),
(r"\bmalah\b.{1,30}\b(ngurusin|urusin|urus)\b", 1),
(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),
# "apa gunanya / apa manfaatnya" — retorikal negatif
(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
# >>> 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 = {
"berita", "melihat", "membaca", "mendengar", "mengetahui",
"laporan", "informasi", "kabar", "melaporkan", "dikabarkan",
"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
# PERUBAHAN: Hapus override 'mending' → 'lebih baik' karena
# mengubah kata kritis negatif menjadi sinyal positif.
# ═══════════════════════════════════════════════════════════
def _load_normalization() -> dict:
"""
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_dict: dict = {}
@ -252,67 +530,51 @@ def _load_normalization() -> dict:
except FileNotFoundError:
pass
# ── Override khusus domain ───────────────────────────────────────────────
# Entri ini menimpa file generik. Perhatikan komentar DIHAPUS di bawah.
DOMAIN_OVERRIDES: dict = {
# Nama platform — pertahankan apa adanya
"shopee": "shopee",
"tokopedia": "tokopedia",
"lazada": "lazada",
"tiktok": "tiktok",
"bukalapak": "bukalapak",
"blibli": "blibli",
# Logistik
"chaos": "kacau",
"overlapping": "tumpang tindih",
"overlap": "tumpang tindih",
"sicepat": "sicepat",
"jne": "jne",
"jnt": "jnt",
"anteraja": "anteraja",
"ninja": "ninja",
# Ongkir & belanja
"freeongkir": "gratis ongkos kirim",
"gratisongkir": "gratis ongkos kirim",
"ongkir": "ongkos kirim",
"ongkr": "ongkos kirim",
"bykrm": "biaya kirim",
"biayakirim": "biaya pengiriman",
# Kebijakan & lembaga
"komdigi": "komdigi",
"kemendag": "kementerian perdagangan",
"kominfo": "kementerian komunikasi",
# E-commerce umum
"ecommerce": "e commerce",
"marketplace": "marketplace",
"seller": "penjual",
"buyer": "pembeli",
"online": "online",
# Negasi informal — pastikan ditangkap
"gk": "tidak", "ga": "tidak", "gak": "tidak",
"nggak": "tidak", "ngga": "tidak", "tdk": "tidak",
"tak": "tidak", "enggak": "tidak", "engga": "tidak",
"kagak": "tidak", "kaga": "tidak", "ndak": "tidak",
"ngak": "tidak",
# Intensitas
"bgt": "banget", "bngt": "banget", "bget": "banget", "bgtt": "banget",
# Positif informal — hanya yang benar-benar positif
"mantep": "mantap", "mntap": "mantap",
"kece": "keren",
"ancur": "hancur", "parahh": "parah",
# ── SENGAJA TIDAK DIOVERRIDE (vs versi lama): ────────────────────────
# "mending" → TIDAK diubah ke "lebih baik"
# Alasan: "mending X daripada Y" adalah ekspresi kritik.
# Mengubah ke "lebih baik" membuat lexicon menangkap 'baik' = POSITIF,
# padahal kalimatnya bermakna negatif/kritik. Biarkan "mending" apa
# adanya agar POLA_KOMPARATIF_NEGATIF bisa mendeteksinya.
#
# "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.
# >>> FIX v5 — MASALAH 14/15 pendukung: beberapa slang umum
# tambahan supaya pola informatif/positif baru bisa kena match
# juga pada bentuk slang-nya.
"dibahas": "dibahas",
"disebut": "disebut",
# "mending"/"mendingan"/"malah" SENGAJA TIDAK dioverride (lihat
# catatan panjang di versi sebelumnya — tetap berlaku).
}
norm_dict.update(DOMAIN_OVERRIDES)
return norm_dict
@ -323,15 +585,6 @@ def _load_normalization() -> dict:
# ═══════════════════════════════════════════════════════════
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"
base: set = set()
try:
@ -348,30 +601,50 @@ def _load_stopwords() -> set:
"jika", "sudah", "telah", "jadi", "bisa",
}
# ── Lindungi kata sentimen penting ──────────────────────────────────────
for kata in KATA_SENTIMEN_PENTING:
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 = {
"mending", # penanda pola komparatif negatif
"mendingan", # variasi mending
"daripada", # komponen "mending X daripada Y"
"ketimbang", # variasi daripada
"ngapain", # penanda kritik tersirat
"percuma", # penanda sia-sia
"begini", # "kebijakan begini" = kritik tersirat
"gajelas", # "gajelas aja kebijakan ini" = tidak jelas/negatif
"mahal", # "mahal banget kebijakan ini" = negatif
"nyusahin", # penanda ribet/susah-susahin
"malas", # "malas banget urus kebijakan ini" = negatif
"mending", "mendingan", "daripada", "ketimbang",
"ngapain", "percuma", "begini",
"gajelas", "mahal", "nyusahin",
"malas", "males", "enggan",
"prioritas", "jelas", "gajeee", "susah-susahin",
"sok", "tahu", "pintar", "bijak",
"tai", "taik",
"aneh", "absurd", "konyol",
"miris", "ironis", "memalukan", "memprihatinkan",
"dikesampingkan",
"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:
base.discard(kata)
# ── Tambah noise Twitter/sosmed ─────────────────────────────────────────
base.update({
"rt", "amp", "https", "http", "co", "pic",
"wkwk", "wkwkwk", "wkwkwkwk",
@ -381,7 +654,6 @@ def _load_stopwords() -> set:
})
return base
# ═══════════════════════════════════════════════════════════
# LOAD STEMMER
# ═══════════════════════════════════════════════════════════
@ -394,7 +666,6 @@ def _load_stemmer():
return None
# ── Inisialisasi global ──────────────────────────────────────────────────────
_STOPWORDS = _load_stopwords()
_STEMMER = _load_stemmer()
_NORM_DICT = _load_normalization()
@ -425,7 +696,6 @@ def _load_model():
# ═══════════════════════════════════════════════════════════
# 5-TAHAP PREPROCESSING PIPELINE
# IDENTIK dengan preprocessing_page.py
# ═══════════════════════════════════════════════════════════
def _step1_case_folding(text: str) -> str:
@ -494,33 +764,21 @@ def preprocess_untuk_lexicon(text: str) -> str:
"""
Preprocessing untuk lexicon matching.
PENTING: TIDAK distem dan TIDAK dihapus stopword-nya agar
pola kontekstual (mending, daripada, begini, dll.) tetap ada
dan bisa dideteksi oleh POLA_KOMPARATIF dan POLA_KRITIK.
pola kontekstual tetap ada dan bisa dideteksi.
"""
s1 = _step1_case_folding(text)
s2 = _step2_cleaning(s1)
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:
"""
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}
"""
tokens = teks_lexicon.split()
@ -537,47 +795,59 @@ def _hitung_skor_lexicon(teks_lexicon: str) -> dict:
if re.search(pola, teks_lexicon):
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 ────────────────────────────────────────
for i, token in enumerate(tokens):
# Negation window: 3 kata sebelumnya
ada_negasi = any(
tokens[i - j] in KATA_NEGASI
for j in range(1, 4)
if i - j >= 0
)
# Konteks pembatasan: 'gratis' setelah 'pembatasan' tidak = positif
ada_pembatasan = any(
tokens[i - j] == "pembatasan"
for j in range(1, 4)
if i - j >= 0
ada_pembatasan = (
any(tokens[i - j] in KATA_PEMBATASAN for j in range(1, 4) if i - j >= 0)
or any(tokens[i + j] in KATA_PEMBATASAN for j in range(1, 4) if i + j < len(tokens))
)
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 ada_negasi:
skor_neg += 1
if ada_intensitas:
skor_neg += 1
elif ada_pembatasan and token == "gratis":
# "pembatasan gratis ongkir" = konteks negatif/netral,
# bukan pujian terhadap 'gratis'. Lewati.
pass
else:
skor_pos += 1
if ada_intensitas:
skor_pos += 1
elif token in LEXICON_NEGATIF:
if ada_negasi:
skor_pos += 1
else:
skor_neg += 1
if ada_intensitas:
skor_neg += 1
# ── 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)
ada_info_kata = bool(token_set & KATA_INFORMATIF)
ada_sentimen_eksplisit = bool(
(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 = (
ada_info_kata
and not ada_sentimen_eksplisit
@ -585,8 +855,8 @@ def _hitung_skor_lexicon(teks_lexicon: str) -> dict:
)
return {
"positif": skor_pos, # ← wajib ada
"negatif": skor_neg, # ← wajib ada
"positif": skor_pos,
"negatif": skor_neg,
"net": skor_pos - skor_neg,
"informatif": is_informatif,
}
@ -607,10 +877,6 @@ _LABEL_MAP = {
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()
if model is None or tfidf is None or not teks_model.strip():
return None
@ -646,51 +912,57 @@ def _klasifikasi_hybrid(
"""
Klasifikasi hybrid: sinyal lexicon + pola kontekstual + model NB.
ALUR KEPUTUSAN:
ALUR KEPUTUSAN (v5):
Layer 0 Override Informatif:
Jika skor['informatif'] = True DAN net mendekati 0 Netral langsung.
Mencegah tweet berita/informatif terklasifikasi positif/negatif
karena kata domain (mis. 'gratis' dalam "berita mengenai gratis ongkir").
Layer 0 >>> FIX v5 (MASALAH 12, baru): Override Informatif NYATA.
Sebelumnya is_info dihitung tapi tidak pernah dipakai (dead code).
Sekarang: jika tweet terdeteksi informatif DAN net == 0 (tidak ada
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):
Sinyal lexicon + pola sangat kuat Positif langsung.
Layer 0a >>> FIX v5 (MASALAH 13, baru): Override Zero-Signal.
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):
Sinyal lexicon + pola sangat kuat Negatif langsung.
CATATAN: Pola komparatif "mending X daripada Y" menambah net -2/-3
sehingga tweet kritik implisit langsung terdeteksi di sini.
Layer 0b Override dominasi kata (selisih 2):
Dua blok if independen yang sejajar (bug indentasi versi lama
sudah diperbaiki di v3).
Layer 2a Override Positif LEMAH (net == 1):
Konsultasi model. Jika model tidak sangat yakin Negatif Positif.
Layer 2b Override Negatif LEMAH (net == -1):
Konsultasi model. Jika model tidak sangat yakin Positif Negatif.
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.
Layer 1a Override Positif KUAT (net >= 2)
Layer 1b Override Negatif KUAT (net <= -2)
Layer 2a Override Positif LEMAH (net == 1), konsultasi model
Layer 2b Override Negatif LEMAH (net == -1), konsultasi model
Layer 3 Fallback Model NB (dengan anti-bias)
Layer 4 Ultimate Fallback (model tidak tersedia)
Return: (label: str, confidence: float)
"""
net = skor["net"]
skor_pos = skor["positif"] # ← pastikan dict ini return nilai ini
skor_neg = skor["negatif"] # ← pastikan dict ini return nilai ini
skor_pos = skor["positif"]
skor_neg = skor["negatif"]
is_info = skor.get("informatif", False)
# ── TAMBAHAN Layer 0b: Override dominasi kata ─────────
# Jika salah satu sisi punya ≥2 kata lebih banyak dari sisi lain,
# langsung putuskan tanpa menunggu model NB.
# ── Layer 0: Override Informatif (FIX v5 — MASALAH 12) ───────────────
if is_info and net == 0:
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:
conf = min(0.60 + (skor_neg - skor_pos) * 0.05, 0.90)
return ("Negatif", round(conf, 3))
if skor_pos >= 2 and skor_pos > skor_neg:
conf = min(0.60 + (skor_pos - skor_neg) * 0.05, 0.90)
if skor_pos >= 2 and skor_pos > skor_neg:
conf = min(0.60 + (skor_pos - skor_neg) * 0.05, 0.90)
return ("Positif", round(conf, 3))
# ── Layer 1a: Positif KUAT ────────────────────────────────────────────
@ -699,8 +971,6 @@ def _klasifikasi_hybrid(
return ("Positif", round(conf, 3))
# ── 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:
conf = min(0.65 + (abs(net) * 0.05), 0.92)
return ("Negatif", round(conf, 3))
@ -736,12 +1006,10 @@ def _klasifikasi_hybrid(
if model_result is not None:
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:
corrected_conf = round(0.50 + max(0, confidence - 0.50) * 0.20, 3)
return ("Netral", corrected_conf)
# Anti-bias 2: Model Positif tapi lexicon negatif → Netral
if label_norm == "Positif" and net <= -1 and confidence < 0.65:
corrected_conf = round(0.50 + max(0, confidence - 0.50) * 0.20, 3)
return ("Netral", corrected_conf)

View File

@ -1,8 +1,17 @@
"""
splash_page.py
Splash screen modern, full-viewport centered, tampil sekali per sesi.
Menggunakan st.components.v1.html (iframe isolated).
Splash screen modern, full-viewport centered.
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:
_SPLASH_DURATION = 10 ganti angka ini (satuan: detik)
@ -16,7 +25,7 @@ import time
import streamlit as st
import streamlit.components.v1 as components
_APP_NAME = "SentiTrack"
_APP_NAME = "SentiTrack"
# ┌─────────────────────────────────────────┐
# │ ⏱ GANTI ANGKA INI UNTUK UBAH DURASI │
@ -24,6 +33,10 @@ _APP_NAME = "SentiTrack"
_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:
return f"""<!DOCTYPE html>
@ -46,7 +59,6 @@ body {{
align-items: center;
justify-content: center;
min-height: 100vh;
/* background ditangani oleh .bg */
}}
/* Background gradient */
@ -98,17 +110,8 @@ body {{
border: 1.5px solid rgba(226,232,240,0.85);
border-radius: 24px;
padding: 2.5rem 2.75rem 2rem;
/*
*
* 📐 LEBAR CARD
* Ubah max-width untuk memperlebar / mempersempit card.
* Rekomendasi: 460px 580px
*
*/
width: calc(100% - 2.5rem);
max-width: 520px;
box-shadow:
0 0 0 1px rgba(255,255,255,0.5) inset,
0 8px 40px rgba(59,130,246,0.09),
@ -229,14 +232,6 @@ body {{
height: 100%;
background: linear-gradient(90deg, #3b6cf7, #6366f1, #818cf8);
border-radius: 99px;
/*
*
* PROGRESS BAR DURATION
* Nilai ini otomatis mengikuti _SPLASH_DURATION di atas.
* Tidak perlu diubah manual.
*
*/
animation: progress {duration}s linear forwards;
width: 0%;
}}
@ -322,7 +317,7 @@ body {{
<div class="info-icon">📊</div>
<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>
@ -358,10 +353,26 @@ body {{
def maybe_show_splash() -> bool:
"""
Tampilkan splash jika belum tampil di sesi ini.
Kembalikan True jika splash baru saja ditampilkan, False jika dilewati.
Tampilkan splash HANYA jika belum pernah tampil di browser ini.
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
# Sembunyikan semua elemen Streamlit selama splash
@ -400,7 +411,23 @@ def maybe_show_splash() -> bool:
# Tunggu sesuai durasi splash sebelum lanjut ke halaman utama
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()
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"
"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",
"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",
"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 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