TIF_E41253310/Model/preprocessing/text_preprocessing.py

436 lines
13 KiB
Python

"""
=============================================================
TEXT PREPROCESSING - TEKS PEKERJAAN
=============================================================
Tujuan:
- Membentuk teks_pekerjaan dari jenis_perusahaan + nama_perusahaan + jabatan
- Melakukan text preprocessing: case folding, cleaning, tokenizing, stopword, stemming
- Menyimpan hasil preprocessing
PRD Reference: Section 12, 13
=============================================================
"""
import pandas as pd
import re
import os
import json
from datetime import datetime
# Sastrawi untuk stemming Bahasa Indonesia
try:
from Sastrawi.Stemmer.StemmerFactory import StemmerFactory
factory = StemmerFactory()
stemmer = factory.create_stemmer()
SASTRAWI_AVAILABLE = True
except ImportError:
SASTRAWI_AVAILABLE = False
print("[WARNING] Sastrawi tidak tersedia. Stemming tidak akan dilakukan.")
# NLTK untuk stopwords
try:
import nltk
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize
nltk.download('stopwords', quiet=True)
nltk.download('punkt', quiet=True)
nltk.download('punkt_tab', quiet=True)
NLTK_AVAILABLE = True
except:
NLTK_AVAILABLE = False
# Path configuration
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DATA_INPUT = os.path.join(BASE_DIR, '..', 'data', 'standardized', 'unified_dataset.csv')
DATA_OUTPUT = os.path.join(BASE_DIR, '..', 'data', 'processed')
# Mapping status pekerjaan dari angka ke teks
STATUS_MAPPING = {
1: "bekerja",
2: "belum_kerja",
3: "wiraswasta",
4: "melanjutkan_studi",
5: "mencari_kerja"
}
# Mapping jenis_perusahaan dari angka ke teks
JENIS_PERUSAHAAN_MAPPING = {
1: "pemerintah",
2: "bnpp",
3: "swasta",
4: "wiraswasta",
5: "lainnya",
6: "bumn",
7: "nirlaba",
0: ""
}
# Stopwords Bahasa Indonesia (extended)
INDONESIAN_STOPWORDS = {
'yang', 'dan', 'di', 'dari', 'ini', 'itu', 'dengan', 'untuk', 'pada',
'adalah', 'ke', 'atau', 'akan', 'juga', 'tidak', 'dalam', 'oleh',
'sebagai', 'lebih', 'sudah', 'ada', 'bisa', 'telah', 'antara', 'maka',
'serta', 'namun', 'secara', 'seperti', 'jika', 'hanya', 'tetapi', 'saat',
'sedang', 'sebelumnya', 'tersebut', 'yaitu', 'saya', 'kami', 'kita',
'anda', 'dia', 'mereka', 'kami', 'kita', 'nya', 'lah', 'kah', 'pun',
'sih', 'deh', 'dong', 'gitu', 'aja', 'emang', 'nih', 'tuh', 'kok',
'yg', 'dg', 'dr', 'dlm', 'dgn', 'utk', 'pd', 'sdh', 'blm', 'jg',
'krn', 'sm', 'tp', 'jd', 'sd', 'bt', 'spy', 'msh', 'dl', 'bs', 'bsa',
'pt', 'cv', 'tdk', 'bg', 'smua', 'atl', 'krn', 'sprti', 'lngkp', 'sm',
# Tambahan umum
'per', 'bagian', 'ibu', 'bapak', 'kakak', 'adik', 'pak', 'bu', 'mas',
'mbak', 'pak', 'bu', 'pak', 'pak', 'dan', 'atau', 'maupun', 'hal', 'satu',
'dua', 'tiga', 'empat', 'lima', 'enam', 'tujuh', 'delapan', 'sembilan', 'sepuluh'
}
# Pattern untuk cleaning
CLEANING_PATTERNS = {
'email': r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b',
'url': r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+',
'phone': r'\b\d{10,15}\b',
'special_chars': r'[^\w\s]',
'extra_spaces': r'\s+',
'numbers': r'\b\d+\b'
}
def clean_text(text):
"""
Basic text cleaning:
- Hapus email, URL, phone
- Hapus special characters
- Hapus standalone numbers (tidak bermakna)
- Hapus extra spaces
"""
if pd.isna(text) or text == '' or text == 'nan':
return ''
text = str(text).lower()
# Hapus email
text = re.sub(CLEANING_PATTERNS['email'], ' ', text)
# Hapus URL
text = re.sub(CLEANING_PATTERNS['url'], ' ', text)
# Hapus phone
text = re.sub(CLEANING_PATTERNS['phone'], ' ', text)
# Hapus special chars (simpan spasi dan alfanumerik)
text = re.sub(r'[^\w\s]', ' ', text)
# Hapus extra spaces
text = re.sub(r'\s+', ' ', text)
# Trim
text = text.strip()
return text
def remove_stopwords(tokens):
"""
Hapus stopwords dari list tokens.
Also removes standalone numbers.
"""
result = []
for token in tokens:
t = token.lower()
# Skip if it's a stopword
if t in INDONESIAN_STOPWORDS:
continue
# Skip if it's just a number
if t.isdigit():
continue
# Skip if it's too short (1-2 chars)
if len(t) <= 2:
continue
result.append(token)
return result
_STEM_CACHE: dict = {}
def stemming(tokens):
"""
Apply stemming menggunakan Sastrawi dengan memoization cache.
"""
if not SASTRAWI_AVAILABLE:
return tokens
stemmed = []
for token in tokens:
if token in _STEM_CACHE:
stemmed.append(_STEM_CACHE[token])
continue
try:
stemmed_token = stemmer.stem(token)
_STEM_CACHE[token] = stemmed_token
stemmed.append(stemmed_token)
except Exception:
_STEM_CACHE[token] = token
stemmed.append(token)
return stemmed
def tokenize(text):
"""
Tokenize text menjadi list of words.
"""
if not text or text == '':
return []
# Split berdasarkan whitespace
tokens = text.split()
return tokens
def form_teks_pekerjaan(row):
"""
Membentuk teks_pekerjaan dari:
status_pekerjaan + jenis_perusahaan + nama_perusahaan + jabatan + tingkatan_tempat_kerja
PRD Section 11:
'bekerja perusahaan swasta pt abc teknologi software engineer nasional wiraswasta berbadan hukum'
"""
parts = []
def clean_val(v):
if pd.isna(v) or not v:
return ''
s = str(v).strip()
if s.lower() in ['-', 'none', 'null', 'kosong', 'tidak ada', '0']:
return ''
return s
# 1. status_pekerjaan
status = clean_val(row.get('status_pekerjaan', row.get('status_kerja', '')))
if status:
parts.append(status)
# 2. jenis_perusahaan
jenis = row.get('jenis_perusahaan', '')
if pd.notna(jenis) and str(jenis).strip() != '':
try:
jenis_num = int(float(str(jenis)))
jenis_text = JENIS_PERUSAHAAN_MAPPING.get(jenis_num, str(jenis))
except (ValueError, TypeError):
jenis_text = str(jenis)
j_clean = clean_val(jenis_text)
if j_clean:
parts.append(j_clean)
# 3. nama_perusahaan
nama = clean_val(row.get('nama_perusahaan', ''))
if nama:
parts.append(nama)
# 4. jabatan
jabatan = clean_val(row.get('jabatan', ''))
if jabatan:
parts.append(jabatan)
# Gabungkan 4 kolom dengan spasi
teks_pekerjaan = ' '.join(parts)
return teks_pekerjaan
def preprocess_text(text):
"""
Full text preprocessing pipeline:
1. Case Folding
2. Cleaning
3. Tokenizing
4. Stopword Removal
5. Stemming
PRD Section 13:
Urutan: Case Folding -> Cleaning -> Tokenizing -> Stopword Removal -> Stemming
"""
# 1. Case Folding (sudah di dalam clean_text)
# 2. Cleaning
text = clean_text(text)
if not text:
return ''
# 3. Tokenizing
tokens = tokenize(text)
# 4. Stopword Removal
tokens = remove_stopwords(tokens)
# 5. Stemming
if SASTRAWI_AVAILABLE:
tokens = stemming(tokens)
# Join kembali
processed_text = ' '.join(tokens)
return processed_text
def preprocess_dataframe(df, text_column='teks_pekerjaan'):
"""
Menjalankan preprocessing pada kolom teks di seluruh DataFrame.
"""
df_copy = df.copy()
if text_column in df_copy.columns:
df_copy[text_column] = df_copy[text_column].apply(lambda x: preprocess_text(str(x)) if pd.notna(x) else '')
return df_copy
def classify_has_pekerjaan(teks_pekerjaan, status):
"""
Klasifikasi apakah alumni memiliki informasi pekerjaan yang cukup.
PRD Section 10:
- Memiliki informasi pekerjaan jika: nama perusahaan ADA ATAU jabatan ADA ATAU jenis perusahaan ADA
"""
if status in ['belum_kerja', 'belum_bekerja', 'mencari_kerja', 'tidak_kerja']:
return 'tidak_berpengalaman'
if not teks_pekerjaan or teks_pekerjaan == '':
return 'tidak_berpengalaman'
# Cek apakah teks terlalu pendek (hanya 1-2 kata)
words = teks_pekerjaan.split()
if len(words) < 2:
return 'tidak_berpengalaman'
return 'berpengalaman'
def main():
"""
Main function untuk text preprocessing.
"""
print("=" * 60)
print("TEXT PREPROCESSING - TEKS PEKERJAAN")
print("=" * 60)
# Load unified dataset
print("\n>> Loading unified dataset...")
df = pd.read_csv(DATA_INPUT, encoding='utf-8-sig')
print(f" Total data: {len(df)} alumni")
# 1. Form teks_pekerjaan
print("\n>> Membentuk teks_pekerjaan...")
df['teks_pekerjaan_raw'] = df.apply(form_teks_pekerjaan, axis=1)
# Statistik
empty_teks = (df['teks_pekerjaan_raw'] == '').sum()
print(f" Teks pekerjaan kosong: {empty_teks}")
print(f" Teks pekerjaan terisi: {len(df) - empty_teks}")
# 2. Text Preprocessing
print("\n>> Melakukan text preprocessing...")
print(f" Sastrawi available: {SASTRAWI_AVAILABLE}")
print(f" NLTK available: {NLTK_AVAILABLE}")
df['teks_pekerjaan'] = df['teks_pekerjaan_raw'].apply(preprocess_text)
# 3. Klasifikasi pengalaman kerja
print("\n>> Mengklasifikasi pengalaman kerja...")
df['kategori_pengalaman'] = df.apply(
lambda row: classify_has_pekerjaan(row['teks_pekerjaan'], row['status_pekerjaan']),
axis=1
)
# Statistik
pengalaman_counts = df['kategori_pengalaman'].value_counts()
print("\n Distribusi Kategori Pengalaman:")
for cat, count in pengalaman_counts.items():
pct = (count / len(df)) * 100
print(f" {cat}: {count} ({pct:.1f}%)")
# 4. Pisahkan data untuk clustering
print("\n>> Memisahkan data untuk clustering...")
# Data yang akan di-cluster: memiliki teks pekerjaan yang cukup
df_untuk_clustering = df[
(df['kategori_pengalaman'] == 'berpengalaman') &
(df['teks_pekerjaan'].str.len() > 0)
].copy()
# Data C3: belum/tidak bekerja
df_c3 = df[
(df['status_pekerjaan'].isin(['belum_kerja', 'mencari_kerja'])) |
(df['kategori_pengalaman'] == 'tidak_berpengalaman')
].copy()
print(f" Data untuk clustering: {len(df_untuk_clustering)}")
print(f" Data C3 (tidak untuk clustering): {len(df_c3)}")
# 5. Generate Summary
print("\n" + "=" * 60)
print("SUMMARY")
print("=" * 60)
print(f"Total alumni: {len(df)}")
print(f"Data untuk clustering: {len(df_untuk_clustering)}")
print(f"Data C3: {len(df_c3)}")
print("\n>> Distribusi Status Pekerjaan:")
for status, count in df['status_pekerjaan'].value_counts().items():
pct = (count / len(df)) * 100
print(f" {status}: {count} ({pct:.1f}%)")
print("\n>> Distribusi Prodi:")
for prodi, count in df['kode_prodi'].value_counts().items():
pct = (count / len(df)) * 100
print(f" {prodi}: {count} ({pct:.1f}%)")
print("\n>> Sample teks_pekerjaan (raw):")
for idx, row in df_untuk_clustering.head(5).iterrows():
print(f" [{row['nama']}] {row['teks_pekerjaan_raw'][:80]}...")
print("\n>> Sample teks_pekerjaan (processed):")
for idx, row in df_untuk_clustering.head(5).iterrows():
print(f" [{row['nama']}] {row['teks_pekerjaan'][:80]}...")
# 6. Save results
print("\n>> Saving results...")
os.makedirs(DATA_OUTPUT, exist_ok=True)
# Save full dataset
full_output = os.path.join(DATA_OUTPUT, 'processed_dataset.csv')
df.to_csv(full_output, index=False, encoding='utf-8-sig')
print(f" Full dataset: {full_output}")
# Save dataset untuk clustering
clustering_output = os.path.join(DATA_OUTPUT, 'dataset_untuk_clustering.csv')
df_untuk_clustering.to_csv(clustering_output, index=False, encoding='utf-8-sig')
print(f" Dataset clustering: {clustering_output}")
# Save C3 data
c3_output = os.path.join(DATA_OUTPUT, 'dataset_c3.csv')
df_c3.to_csv(c3_output, index=False, encoding='utf-8-sig')
print(f" Dataset C3: {c3_output}")
# Save summary
summary = {
'tanggal': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'total_alumni': len(df),
'total_untuk_clustering': len(df_untuk_clustering),
'total_c3': len(df_c3),
'sastrawi_available': SASTRAWI_AVAILABLE,
'distribusi_status': df['status_pekerjaan'].value_counts().to_dict(),
'distribusi_prodi': df['kode_prodi'].value_counts().to_dict()
}
summary_output = os.path.join(DATA_OUTPUT, 'preprocessing_summary.json')
with open(summary_output, 'w', encoding='utf-8') as f:
json.dump(summary, f, indent=2, ensure_ascii=False)
print(f" Summary: {summary_output}")
print("\n" + "=" * 60)
print("[SUCCESS] TEXT PREPROCESSING SELESAI!")
print("=" * 60)
return df, df_untuk_clustering, df_c3
if __name__ == "__main__":
df, df_clustering, df_c3 = main()