Sentimen-Analysis / services /preprocessing.py
noranisa's picture
Update services/preprocessing.py
bb0c650 verified
"""
services/preprocessing_id.py
Preprocessing Bahasa Indonesia yang komprehensif untuk media sosial.
Mencakup:
1. Normalisasi slang & singkatan (~200 kata)
2. Handling emoji → sentimen kata
3. Code-switching ID-EN
4. Deteksi & normalisasi kata berulang
5. Stopwords gabungan ID+EN
"""
import re
from typing import Optional
# ─────────────────────────────────────────────
# 1. STOPWORDS GABUNGAN ID + EN
# ─────────────────────────────────────────────
STOPWORDS = {
# Bahasa Indonesia - fungsi
'yang','dan','di','ke','dari','ini','itu','dengan','untuk','adalah','ada',
'pada','juga','tidak','bisa','sudah','saya','kamu','kami','mereka','kita',
'nya','pun','aja','ya','yah','mau','jadi','buat','kalau','banget','sangat',
'lebih','nih','sih','dong','lah','lagi','terus','sama','atau','karena',
'oleh','akan','dapat','harus','boleh','perlu','agar','supaya','namun',
'tetapi','tapi','sedang','sedangkan','setelah','sebelum','ketika','saat',
'antara','dalam','luar','atas','bawah','depan','belakang','kiri','kanan',
'sekitar','tentang','terhadap','selama','karena','sebab','hingga','sampai',
'bahwa','agar','jika','bila','apabila','maka','sehingga','walaupun','meski',
'justru','malah','bahkan','hanya','saja','paling','makin','semakin',
'begitu','seperti','misalnya','contoh','yaitu','yakni','antara','lain',
'hal','cara','jenis','macam','bagian','sistem','proses',
# Bahasa gaul/informal
'tuh','tuu','gitu','gini','gituu','ginii','doang','woi','hoi','hai','hey',
'nah','neh','deh','kok','sih','kan','kaan','si','bu','pak','mas','mbak',
# Bahasa Inggris - fungsi
'the','is','in','of','to','a','an','and','it','for','that','this',
'was','are','be','has','have','had','do','does','did','will','would',
'could','should','may','might','shall','must','can','need','dare',
'with','at','by','from','up','about','into','through','during',
'before','after','above','below','between','out','off','over','under',
'again','further','then','once','here','there','when','where','why',
'how','all','both','each','few','more','most','other','some','such',
'no','not','only','own','same','so','than','too','very','just','but',
'if','or','because','as','until','while','although','though','since',
'i','you','he','she','we','they','me','him','her','us','them',
'my','your','his','its','our','their',
}
# ─────────────────────────────────────────────
# 2. SLANG & SINGKATAN (~200 entri)
# ─────────────────────────────────────────────
SLANG_MAP = {
# Negasi
'gak':'tidak','ga':'tidak','nggak':'tidak','ngga':'tidak','enggak':'tidak',
'gk':'tidak','ndk':'tidak','ndak':'tidak','kagak':'tidak','kaga':'tidak',
'bkn':'bukan','bukan':'bukan',
# Kata ganti
'gue':'saya','gw':'saya','w':'saya','aq':'saya','ak':'saya',
'lo':'kamu','lu':'kamu','elo':'kamu','elu':'kamu','loe':'kamu',
'dy':'dia','doi':'dia','dya':'dia',
'kt':'kita','qta':'kita','qt':'kita',
'mrk':'mereka','mrek':'mereka',
# Konjungsi & partikel
'tp':'tapi','tpi':'tapi','ttpi':'tetapi',
'jg':'juga','jga':'juga','jg':'juga',
'krn':'karena','karna':'karena','kren':'karena',
'dgn':'dengan','dg':'dengan','d':'dengan',
'yg':'yang','y':'yang',
'utk':'untuk','buat':'untuk','bt':'buat',
'dr':'dari','dri':'dari',
'sm':'sama','ama':'sama','bareng':'bersama',
'kl':'kalau','kalo':'kalau','klw':'kalau',
'spy':'supaya','biar':'supaya',
'sdgkan':'sedangkan','pdhl':'padahal','pdhal':'padahal',
'mk':'maka','jd':'jadi','jdi':'jadi',
# Aspek waktu
'udah':'sudah','udh':'sudah','dah':'sudah','sdh':'sudah','uda':'sudah',
'blm':'belum','blum':'belum','belom':'belum',
'msh':'masih','masi':'masih','msih':'masih',
'lg':'lagi','lgi':'lagi','lg':'lagi',
'skrg':'sekarang','skrng':'sekarang','skg':'sekarang',
'stlh':'setelah','sblm':'sebelum',
'trs':'terus','trus':'terus','teros':'terus',
# Kemampuan & keharusan
'bs':'bisa','bsa':'bisa','biz':'bisa',
'hrs':'harus','hrus':'harus',
'prlu':'perlu','prl':'perlu',
'mau':'ingin','mw':'ingin','mo':'ingin',
# Kuantitas & kualitas
'bgt':'banget','bngt':'banget','bngt':'banget',
'sgt':'sangat','bgt':'banget',
'krg':'kurang','krang':'kurang',
'lbh':'lebih','lbih':'lebih',
'plg':'paling','paling':'paling',
'byk':'banyak','bnyk':'banyak',
'dkt':'dekat','dkt':'dekat',
'cpt':'cepat','cpet':'cepat',
'lmbt':'lambat','lmbat':'lambat',
# Afirmatif & konfirmasi
'iya':'ya','iyes':'ya','iyess':'ya',
'ok':'oke','okay':'oke','oke':'oke','okey':'oke',
'sip':'bagus','siip':'bagus',
'bnr':'benar','bner':'benar','bnar':'benar',
'emg':'memang','emang':'memang','mmg':'memang','memg':'memang',
'kyk':'kayak','kek':'seperti','kyak':'kayak',
# Ekspresi umum medsos
'wkwk':'haha','wkwkwk':'haha','wkwkwkwk':'haha',
'hehe':'haha','hihi':'haha','huhu':'sedih','hiks':'sedih',
'lol':'tertawa','lmao':'tertawa','rofl':'tertawa',
'omg':'terkejut','omgg':'terkejut',
'btw':'ngomong-ngomong','fyi':'informasi',
'asap':'segera','imo':'menurutku','imho':'menurutku',
'tbh':'jujurnya','ngl':'jujur',
# Informasi & pertanyaan
'info':'informasi','inpo':'informasi',
'kmn':'kemana','kmana':'kemana',
'dmn':'dimana','dmana':'dimana',
'gmn':'bagaimana','gmana':'bagaimana','bgmn':'bagaimana',
'knp':'kenapa','ngp':'kenapa','napa':'kenapa',
'brp':'berapa','brapa':'berapa',
'ttg':'tentang','mnrt':'menurut',
'mksud':'maksud','mksd':'maksud',
# Platform-spesifik
'fyp':'for you page','foryou':'untuk kamu',
'collab':'kolaborasi','ft':'bersama',
'dm':'pesan langsung','pm':'pesan pribadi',
'fr':'sungguh','frfr':'sungguh-sungguh',
'ngl':'jujur','idk':'tidak tahu','idk':'tidak tahu',
'ootd':'outfit hari ini','grwm':'ikut siap-siapku',
# Sapaan & gelar
'kk':'kakak','kak':'kakak','bro':'saudara','sis':'saudari',
'bang':'abang','bg':'abang','abg':'abang',
'om':'paman','tante':'bibi','bund':'bunda',
# Kata-kata umum yang sering disingkat
'prt':'pemerintah','pmrth':'pemerintah',
'jkt':'jakarta','sby':'surabaya','bdg':'bandung',
'indo':'indonesia','id':'indonesia',
'pns':'pegawai negeri','asn':'aparatur sipil negara',
'umkm':'usaha mikro kecil menengah',
'pdp':'pendapatan','gdp':'produk domestik bruto',
'bbm':'bahan bakar minyak','lpg':'gas elpiji',
'rs':'rumah sakit','puskesmas':'pusat kesehatan masyarakat',
'sd':'sekolah dasar','smp':'sekolah menengah pertama',
'sma':'sekolah menengah atas','pt':'perguruan tinggi',
}
# ─────────────────────────────────────────────
# 3. EMOJI → SENTIMEN KATA
# ─────────────────────────────────────────────
EMOJI_MAP = {
# Positif
'😀':'senang','😃':'senang','😄':'senang','😁':'senang',
'😊':'senang','🥰':'cinta','😍':'cinta','❤':'cinta',
'❤️':'cinta','💕':'sayang','💖':'sayang','💗':'sayang',
'👍':'bagus','👌':'oke','✅':'benar','💯':'sempurna',
'🔥':'keren','⭐':'bagus','🌟':'luar biasa','✨':'indah',
'🎉':'selamat','🎊':'perayaan','🏆':'menang','🥇':'juara',
'😂':'lucu','🤣':'lucu','😆':'lucu',
'🙏':'terima kasih','👏':'tepuk tangan','💪':'kuat','🚀':'maju',
'😎':'keren','🤩':'kagum','😇':'baik','🤗':'pelukan',
# Negatif
'😢':'sedih','😭':'menangis','😔':'kecewa','😞':'kecewa',
'😡':'marah','😠':'marah','🤬':'sangat marah','💢':'marah',
'👎':'jelek','❌':'salah','🚫':'dilarang','⛔':'berhenti',
'😱':'terkejut','😨':'takut','😰':'khawatir','😟':'khawatir',
'🤮':'jijik','🤢':'mual','💔':'patah hati','😤':'kesal',
'😣':'susah','😖':'frustrasi','😩':'lelah','😫':'capek',
# Netral/Informasi
'🤔':'berpikir','🧐':'mengamati','💭':'berpikir',
'📢':'pengumuman','📣':'pengumuman','ℹ':'informasi',
'⚠':'peringatan','❓':'pertanyaan','❗':'penting',
'📊':'data','📈':'naik','📉':'turun','💰':'uang',
'🏥':'rumah sakit','🏫':'sekolah','🏛':'pemerintah',
}
# ─────────────────────────────────────────────
# 4. PREPROCESSING PIPELINE
# ─────────────────────────────────────────────
def normalize_repeated_chars(text: str) -> str:
"""
'baguuuus' → 'bagus', 'hahahaha' → 'haha'
Pertahankan max 2 karakter berulang.
"""
return re.sub(r'(.)\1{2,}', r'\1\1', text)
def extract_emoji_sentiment(text: str) -> tuple[str, list[str]]:
"""
Ekstrak emoji dan ganti dengan kata sentimen.
Return: (text_tanpa_emoji, list_kata_sentimen)
"""
sentiment_words = []
result = text
for emoji, word in EMOJI_MAP.items():
if emoji in result:
count = result.count(emoji)
sentiment_words.extend([word] * min(count, 2))
result = result.replace(emoji, f' {word} ')
# Hapus emoji yang tidak ada di map menggunakan regex
result = re.sub(
r'[\U00010000-\U0010ffff'
r'\U0001F600-\U0001F64F'
r'\U0001F300-\U0001F5FF'
r'\U0001F680-\U0001F6FF'
r'\U0001F1E0-\U0001F1FF'
r'\u2600-\u26FF\u2700-\u27BF]',
' ', result
)
return result, sentiment_words
def normalize_slang(tokens: list[str]) -> list[str]:
"""Normalisasi slang per token."""
return [SLANG_MAP.get(t, t) for t in tokens]
def clean_text_deep(text: str, keep_emoji_words: bool = True) -> str:
"""
Full preprocessing pipeline:
1. Lowercase
2. Hapus URL, mention, hashtag (simpan kata hashtag)
3. Ekstrak emoji → kata sentimen
4. Normalisasi karakter berulang
5. Normalisasi slang
6. Bersihkan karakter non-alfanumerik
7. Filter stopwords & token pendek
"""
if not text or not isinstance(text, str):
return ""
# 1. Lowercase
text = text.lower().strip()
# 2. Hapus URL
text = re.sub(r'https?://\S+|www\.\S+', '', text)
# 3. Hapus mention (@user)
text = re.sub(r'@\w+', '', text)
# 4. Hashtag → simpan kata
text = re.sub(r'#(\w+)', r' \1 ', text)
# 5. Emoji → kata sentimen
text, emoji_words = extract_emoji_sentiment(text)
# 6. Normalisasi karakter berulang
text = normalize_repeated_chars(text)
# 7. Hapus karakter non-alfanumerik (simpan spasi)
text = re.sub(r'[^a-z0-9\s]', ' ', text)
# 8. Tokenisasi
tokens = text.split()
# 9. Normalisasi slang
tokens = normalize_slang(tokens)
# 10. Filter: hapus stopwords dan token terlalu pendek
tokens = [t for t in tokens if len(t) > 2 and t not in STOPWORDS]
# 11. Tambahkan kata dari emoji
if keep_emoji_words and emoji_words:
tokens.extend(emoji_words)
return ' '.join(tokens)
def is_valid(text: str, min_words: int = 3) -> bool:
"""Cek apakah teks valid untuk dianalisis."""
if not text or not isinstance(text, str):
return False
cleaned = clean_text_deep(text)
return len(cleaned.split()) >= min_words
def clean_text(text: str) -> str:
"""
Alias untuk kompatibilitas dengan kode lama.
Memanggil clean_text_deep.
"""
return clean_text_deep(text)
def batch_clean(texts: list[str]) -> list[str]:
"""Batch preprocessing untuk list teks."""
return [clean_text_deep(t) for t in texts]
def get_text_stats(text: str) -> dict:
"""
Statistik linguistik untuk satu teks.
Berguna untuk feature engineering.
"""
original_tokens = text.lower().split()
cleaned = clean_text_deep(text)
cleaned_tokens = cleaned.split()
# Deteksi code-switching (campuran ID-EN)
en_words = {'the','is','in','of','a','an','and','it','for','that',
'this','good','bad','great','nice','love','hate','really',
'very','just','like','get','can','go','make','know','think',
'want','need','people','time','work','life','day','way'}
en_count = sum(1 for t in original_tokens if t in en_words)
cs_ratio = en_count / max(len(original_tokens), 1)
# Deteksi emoji
emoji_count = sum(1 for c in text if c in EMOJI_MAP)
# Deteksi pengulangan kata
repeat_count = len(re.findall(r'(.)\1{2,}', text.lower()))
return {
'original_len': len(original_tokens),
'cleaned_len': len(cleaned_tokens),
'emoji_count': emoji_count,
'cs_ratio': round(cs_ratio, 3),
'repeat_count': repeat_count,
'is_codeswitched': cs_ratio > 0.2,
}