Spaces:
Sleeping
Sleeping
| """ | |
| 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, | |
| } |