""" 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, }