sentiment service
This commit is contained in:
parent
7588f26c1e
commit
d685dc41a4
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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"}
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,51 +912,57 @@ 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))
|
||||||
|
|
||||||
if skor_pos >= 2 and skor_pos > skor_neg:
|
if skor_pos >= 2 and skor_pos > skor_neg:
|
||||||
conf = min(0.60 + (skor_pos - skor_neg) * 0.05, 0.90)
|
conf = min(0.60 + (skor_pos - skor_neg) * 0.05, 0.90)
|
||||||
return ("Positif", round(conf, 3))
|
return ("Positif", round(conf, 3))
|
||||||
|
|
||||||
# ── Layer 1a: Positif KUAT ────────────────────────────────────────────
|
# ── Layer 1a: Positif KUAT ────────────────────────────────────────────
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -16,7 +25,7 @@ import time
|
||||||
import streamlit as st
|
import streamlit as st
|
||||||
import streamlit.components.v1 as components
|
import streamlit.components.v1 as components
|
||||||
|
|
||||||
_APP_NAME = "SentiTrack"
|
_APP_NAME = "SentiTrack"
|
||||||
|
|
||||||
# ┌─────────────────────────────────────────┐
|
# ┌─────────────────────────────────────────┐
|
||||||
# │ ⏱ GANTI ANGKA INI UNTUK UBAH DURASI │
|
# │ ⏱ GANTI ANGKA INI UNTUK UBAH DURASI │
|
||||||
|
|
@ -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.
Binary file not shown.
Binary file not shown.
|
|
@ -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,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",
|
||||||
|
|
|
||||||
|
Loading…
Reference in New Issue