567 lines
25 KiB
Python
567 lines
25 KiB
Python
"""
|
|
Functions untuk parsing dan konversi nilai dari berbagai format CSV tracer study.
|
|
Modul ini bertugas menerjemahkan kode-kode numerik kuesioner baku DIKTI (seperti F8, F504, F505, F1101, F14, F15, dll)
|
|
menjadi teks deskriptif yang manusiawi untuk disimpan ke database dan ditampilkan di frontend.
|
|
"""
|
|
|
|
# Mengimpor modul ekspresi reguler (re) untuk pencocokan pola string (misal: "1 - Teks")
|
|
import re
|
|
# Mengimpor pustaka pandas untuk operasi analisis data dan pengecekan nilai NaN (pd.isna)
|
|
import pandas as pd
|
|
# Mengimpor datetime untuk manipulasi tanggal dan timedelta untuk perhitungan selisih hari tanggal serial Excel
|
|
from datetime import datetime, timedelta
|
|
# Mengimpor tipe hint dari modul typing: Optional (bisa None), Tuple (pasangan data), Any (tipe fleksibel)
|
|
from typing import Optional, Tuple, Any
|
|
# Mengimpor objek Session dari SQLAlchemy ORM untuk interaksi sesi database
|
|
from sqlalchemy.orm import Session
|
|
|
|
|
|
# =========================================================================
|
|
# MAPPING KODE KE TEKS JAWABAN (Standar Kuesioner Tracer Study DIKTI)
|
|
# =========================================================================
|
|
|
|
# Kamus terpusat yang memetakan kode pertanyaan kuesioner DIKTI ke teks pilihan jawabannya
|
|
JAWABAN_MAPPING = {
|
|
# F8: Pertanyaan status aktivitas alumni saat ini
|
|
'F8': {
|
|
1: 'Bekerja (full time/part time)', # Kode 1: Alumni sudah bekerja penuh/paruh waktu
|
|
2: 'Belum memungkinkan bekerja', # Kode 2: Belum dapat bekerja karena alasan tertentu
|
|
3: 'Wiraswasta', # Kode 3: Membuka usaha sendiri / wirausaha
|
|
4: 'Melanjutkan Pendidikan', # Kode 4: Sedang menempuh studi lanjut S2/Spesialis
|
|
5: 'Tidak kerja tetapi sedang mencari kerja', # Kode 5: Pengangguran aktif mencari lowongan
|
|
},
|
|
|
|
# F504: Apakah mendapatkan pekerjaan dalam kurun waktu <= 6 bulan setelah lulus?
|
|
'F504': {
|
|
1: 'Ya', # Kode 1: Ya, dapat kerja <= 6 bulan
|
|
2: 'Tidak', # Kode 2: Tidak, dapat kerja > 6 bulan
|
|
},
|
|
|
|
# F505: Kisaran rata-rata pendapatan/gaji alumni per bulan
|
|
'F505': {
|
|
1000000: 'Rp 1.000.000', # Range nominal 1 juta
|
|
1500000: 'Rp 1.500.000', # Range nominal 1.5 juta
|
|
2000000: 'Rp 2.000.000', # Range nominal 2 juta
|
|
2500000: 'Rp 2.500.000', # Range nominal 2.5 juta
|
|
3000000: 'Rp 3.000.000', # Range nominal 3 juta
|
|
3500000: 'Rp 3.500.000', # Range nominal 3.5 juta
|
|
4000000: 'Rp 4.000.000', # Range nominal 4 juta
|
|
4500000: 'Rp 4.500.000', # Range nominal 4.5 juta
|
|
5000000: 'Rp 5.000.000', # Range nominal 5 juta
|
|
6000000: 'Rp 6.000.000', # Range nominal 6 juta
|
|
7000000: 'Rp 7.000.000', # Range nominal 7 juta
|
|
8000000: '>Rp 8.000.000', # Range nominal di atas 8 juta
|
|
},
|
|
|
|
# F1101: Jenis atau bentuk institusi / perusahaan tempat alumni bekerja
|
|
'F1101': {
|
|
1: 'Instansi pemerintah', # Kode 1: Instansi pemerintah / PNS
|
|
2: 'Organisasi non-profit/Lembaga Swadaya Masyarakat', # Kode 2: LSM / Non-profit / Yayasan
|
|
3: 'Perusahaan swasta', # Kode 3: Korporasi swasta
|
|
4: 'Wiraswasta/perusahaan sendiri', # Kode 4: Usaha mandiri
|
|
5: 'Lainnya', # Kode 5: Bentuk instansi lainnya
|
|
6: 'BUMN/BUMD', # Kode 6: Badan Usaha Milik Negara / Daerah
|
|
7: 'Institusi/Organisasi Multilateral', # Kode 7: Lembaga internasional / PBB dsb.
|
|
},
|
|
|
|
# F5d / F5D: Skala tingkatan tempat kerja (wilayah operasional perusahaan)
|
|
'F5D': {
|
|
1: 'Lokal/wilayah/wiraswasta tidak berbadan hukum', # Kode 1: Skala lokal / UKM non-badan hukum
|
|
2: 'Nasional/wiraswasta berbadan hukum', # Kode 2: Skala nasional (PT, CV)
|
|
3: 'Multinasional/Internasional', # Kode 3: Skala internasional lintas negara
|
|
},
|
|
'F5d': {
|
|
1: 'Lokal/wilayah/wiraswasta tidak berbadan hukum', # Versi huruf kecil F5d
|
|
2: 'Nasional/wiraswasta berbadan hukum', # Versi huruf kecil F5d
|
|
3: 'Multinasional/Internasional', # Versi huruf kecil F5d
|
|
},
|
|
|
|
# F5c / F5C: Posisi tingkatan jabatan alumni jika berwirausaha
|
|
'F5C': {
|
|
1: 'Founder', # Pendiri usaha
|
|
2: 'Co-Founder', # Rekan pendiri
|
|
3: 'Staff', # Staf pelaksana
|
|
4: 'Freelance/Kerja Lepas', # Pekerja lepas
|
|
},
|
|
'F5c': {
|
|
1: 'Founder',
|
|
2: 'Co-Founder',
|
|
3: 'Staff',
|
|
4: 'Freelance/Kerja Lepas',
|
|
},
|
|
|
|
# F18a / F18A: Sumber pembiayaan untuk studi lanjut
|
|
'F18A': {
|
|
1: 'Biaya Sendiri', # Biaya mandiri/keluarga
|
|
2: 'Beasiswa', # Beasiswa pendidikan
|
|
},
|
|
'F18a': {
|
|
1: 'Biaya Sendiri',
|
|
2: 'Beasiswa',
|
|
},
|
|
|
|
# F1201: Sumber utama biaya kuliah selama menempuh diploma/sarjana
|
|
'F1201': {
|
|
1: 'Biaya Sendiri/Keluarga', # Biaya orang tua / pribadi
|
|
2: 'Beasiswa ADIK', # Beasiswa Afirmasi Dikti
|
|
3: 'Beasiswa BIDIKMISI', # Beasiswa Bidikmisi / KIP-Kuliah
|
|
4: 'Beasiswa PPA', # Beasiswa Peningkatan Prestasi Akademik
|
|
5: 'Beasiswa AFIRMASI', # Beasiswa Afirmasi daerah 3T
|
|
6: 'Beasiswa Perusahaan/Swasta', # Beasiswa industri (Djarum, Tanoto, dll)
|
|
7: 'Lainnya', # Sumber bantuan biaya lain
|
|
},
|
|
|
|
# F14: Tingkat keeratan hubungan antara program studi dengan bidang pekerjaan
|
|
'F14': {
|
|
1: 'Sangat Erat', # Relevansi sangat tinggi
|
|
2: 'Erat', # Relevansi tinggi
|
|
3: 'Cukup Erat', # Relevansi sedang
|
|
4: 'Kurang Erat', # Relevansi rendah
|
|
5: 'Tidak sama sekali', # Sama sekali tidak relevan
|
|
},
|
|
|
|
# F15: Kesesuaian tingkat pendidikan yang dituntut oleh pekerjaan
|
|
'F15': {
|
|
1: 'Setingkat lebih tinggi', # Butuh strata lebih tinggi (misal butuh S2)
|
|
2: 'Tingkat yang sama', # Tepat sesuai jenjang (D4/S1)
|
|
3: 'Setingkat lebih rendah', # Cukup jenjang D3/SMA
|
|
4: 'Tidak perlu pendidikan tinggi', # Tidak mensyaratkan gelar kuliah
|
|
},
|
|
|
|
# F301: Waktu mulai aktif mencari pekerjaan
|
|
'F301': {
|
|
1: 'Saya mencari kerja sebelum lulus', # Sudah berburu kerja sebelum wisuda
|
|
2: 'Saya mencari kerja sesudah wisuda', # Baru mencari setelah resmi diwisuda
|
|
3: 'Saya tidak mencari kerja', # Langsung usaha / belum ingin bekerja
|
|
},
|
|
|
|
# F1001: Keaktifan mencari pekerjaan dalam kurun 4 minggu terakhir
|
|
'F1001': {
|
|
1: 'Tidak', # Tidak mencari kerja
|
|
2: 'Tidak, tapi saya sedang menunggu hasil lamaran kerja', # Menunggu pengumuman
|
|
3: 'Ya, saya akan mulai bekerja dalam 2 minggu ke depan', # Sudah diterima dan siap masuk
|
|
4: 'Ya, tapi saya belum pasti akan bekerja dalam 2 minggu ke depan', # Masih proses tes
|
|
5: 'Lainnya', # Alasan keaktifan lainnya
|
|
},
|
|
|
|
# F1601: Kesesuaian pekerjaan dengan latar belakang pendidikan
|
|
'F1601': {
|
|
0: 'Tidak sesuai',
|
|
1: 'Pekerjaan saya sekarang sudah sesuai dengan pendidikan saya',
|
|
},
|
|
|
|
# F1602: Alasan mengapa pekerjaan belum sesuai
|
|
'F1602': {
|
|
0: 'Pekerjaan saya sudah sesuai',
|
|
1: 'Saya belum mendapatkan pekerjaan yang lebih sesuai',
|
|
},
|
|
|
|
# F1603 - F1613: Berbagai pertimbangan alasan memilih pekerjaan yang tidak sesuai
|
|
'F1603': {
|
|
0: 'Pekerjaan saya sudah sesuai',
|
|
1: 'Di pekerjaan ini saya memeroleh prospek karir yang baik',
|
|
},
|
|
'F1604': {
|
|
0: 'Pekerjaan saya sudah sesuai',
|
|
1: 'Saya lebih suka bekerja di area pekerjaan yang tidak ada hubungannya dengan pendidikan saya',
|
|
},
|
|
'F1605': {
|
|
0: 'Pekerjaan saya sudah sesuai',
|
|
1: 'Saya dipromosikan ke posisi yang kurang berhubungan dengan pendidikan saya',
|
|
},
|
|
'F1606': {
|
|
0: 'Pekerjaan saya sudah sesuai',
|
|
1: 'Saya dapat memeroleh pendapatan yang lebih tinggi di pekerjaan ini',
|
|
},
|
|
'F1607': {
|
|
0: 'Pekerjaan saya sudah sesuai',
|
|
1: 'Pekerjaan saya saat ini lebih aman/terjamin/secure',
|
|
},
|
|
'F1608': {
|
|
0: 'Pekerjaan saya sudah sesuai',
|
|
1: 'Pekerjaan saya saat ini lebih menarik',
|
|
},
|
|
'F1609': {
|
|
0: 'Pekerjaan saya sudah sesuai',
|
|
1: 'Pekerjaan saya saat ini lebih memungkinkan saya mengambil pekerjaan',
|
|
},
|
|
'F1610': {
|
|
0: 'Pekerjaan saya sudah sesuai',
|
|
1: 'Pekerjaan saya saat ini lokasinya lebih dekat dari rumah saya',
|
|
},
|
|
'F1611': {
|
|
0: 'Pekerjaan saya sudah sesuai',
|
|
1: 'Pekerjaan saya saat ini dapat lebih menjamin kebutuhan keluarga saya',
|
|
},
|
|
'F1612': {
|
|
0: 'Pekerjaan saya sudah sesuai',
|
|
1: 'Pada awal meniti karir ini, saya harus menerima pekerjaan yang tidak berhubungan dengan pendidikan saya',
|
|
},
|
|
'F1613': {
|
|
0: 'Pekerjaan saya sudah sesuai',
|
|
1: 'Lainnya',
|
|
},
|
|
}
|
|
|
|
# Mapping resmi kode numerik wilayah provinsi Indonesia (F5a1)
|
|
PROVINSI_MAPPING = {
|
|
10000: 'D.K.I. Jakarta',
|
|
20000: 'Jawa Barat',
|
|
30000: 'Jawa Tengah',
|
|
40000: 'D.I. Yogyakarta',
|
|
50000: 'Jawa Timur',
|
|
60000: 'Aceh',
|
|
70000: 'Sumatera Utara',
|
|
80000: 'Sumatera Barat',
|
|
90000: 'Riau',
|
|
100000: 'Jambi',
|
|
110000: 'Sumatera Selatan',
|
|
120000: 'Lampung',
|
|
130000: 'Kalimantan Barat',
|
|
140000: 'Kalimantan Tengah',
|
|
150000: 'Kalimantan Selatan',
|
|
160000: 'Kalimantan Timur',
|
|
170000: 'Sulawesi Utara',
|
|
180000: 'Sulawesi Tengah',
|
|
190000: 'Sulawesi Selatan',
|
|
200000: 'Sulawesi Tenggara',
|
|
210000: 'Maluku',
|
|
220000: 'Bali',
|
|
230000: 'Nusa Tenggara Barat',
|
|
240000: 'Nusa Tenggara Timur',
|
|
250000: 'Papua',
|
|
260000: 'Bengkulu',
|
|
270000: 'Maluku Utara',
|
|
280000: 'Banten',
|
|
290000: 'Kepulauan Bangka Belitung',
|
|
300000: 'Gorontalo',
|
|
310000: 'Kepulauan Riau',
|
|
320000: 'Papua Barat',
|
|
330000: 'Sulawesi Barat',
|
|
340000: 'Kalimantan Utara',
|
|
350000: 'Luar Negeri',
|
|
}
|
|
|
|
# Mapping beberapa kode utama kabupaten / kota wilayah Jawa Timur (F5a2)
|
|
KABUPATEN_MAPPING = {
|
|
52000: 'Kota Surabaya',
|
|
52100: 'Kota Malang',
|
|
52200: 'Kota Probolinggo',
|
|
52300: 'Kota Pasuruan',
|
|
52400: 'Kab. Jember',
|
|
52500: 'Kab. Banyuwangi',
|
|
}
|
|
|
|
# Mapping skala likert 1-5 untuk tingkat kompetensi Dikti (F1761 - F1774)
|
|
KOMPETENSI_LEVEL = {
|
|
1: '1 - Sangat Rendah',
|
|
2: '2 - Rendah',
|
|
3: '3 - Netral',
|
|
4: '4 - Tinggi',
|
|
5: '5 - Sangat Tinggi',
|
|
}
|
|
|
|
# Mapping skala likert 1-5 untuk efektivitas metode pembelajaran di kampus (F21 - F27)
|
|
METODE_LEVEL = {
|
|
1: '1 - Sangat Besar',
|
|
2: '2 - Besar',
|
|
3: '3 - Cukup Besar',
|
|
4: '4 - Kurang',
|
|
5: '5 - Tidak Sama Sekali',
|
|
}
|
|
|
|
# Mapping nilai biner 0 dan 1 untuk saluran pencarian kerja Ya/Tidak (F401 - F415)
|
|
YA_TIDAK = {
|
|
0: '0 - Tidak',
|
|
1: '1 - Ya',
|
|
}
|
|
|
|
|
|
|
|
# =========================================================================
|
|
# FUNGSI-FUNGSI PARSING DATA MENTAH
|
|
# =========================================================================
|
|
|
|
# Fungsi untuk memisahkan kode angka dan teks deskripsi dari nilai kolom CSV (khususnya format kuesioner tahun 2024)
|
|
def parse_2024_value(val) -> Tuple[Optional[int], Optional[str]]:
|
|
"""
|
|
Mengurai (parse) nilai kolom dari format ekspor CSV 2024 yang memadukan kode angka dan teks.
|
|
|
|
Contoh pola yang ditangani:
|
|
- "3 - Cukup Erat" -> menghasilkan tuple: (3, "Cukup Erat")
|
|
- "1 - Ya" -> menghasilkan tuple: (1, "Ya")
|
|
- "Rp 3.000.000,- s/d Rp 4.000.000,-" -> menghasilkan tuple: (3000000, teks_asli)
|
|
- "Bekerja (full time/part time)" -> menghasilkan tuple: (None, teks_asli)
|
|
|
|
Returns:
|
|
Tuple (kode_as_int, teks_bersih):
|
|
- kode_as_int: integer angka kode jika berhasil diekstrak, None jika tidak ada
|
|
- teks_bersih: string teks deskripsi bersih tanpa kode awalan
|
|
"""
|
|
# Memeriksa apakah nilai input kosong atau bernilai NaN (Not a Number dari Pandas)
|
|
if pd.isna(val) or val == '':
|
|
# Kembalikan None untuk kedua elemen jika tidak ada data
|
|
return None, None
|
|
|
|
# Jika nilai sudah bertipe numerik langsung (int atau float murni)
|
|
if isinstance(val, (int, float)):
|
|
# Konversi ke int jika nilai bulat, jika desimal return None untuk kode
|
|
return int(val) if val == int(val) else None, None
|
|
|
|
# Mengubah nilai menjadi tipe string dan membuang spasi kosong di awal/akhir
|
|
val_str = str(val).strip()
|
|
|
|
# Memeriksa jika string hasil strip adalah string kosong atau representasi literal null ('nan', 'None')
|
|
if val_str in ['', 'nan', 'None']:
|
|
return None, None
|
|
|
|
# Mencocokkan dengan pola Regex: Angka di awal diikuti tanda strip/dash lalu teks (misal: "1 - Teks" atau "12 - Teks")
|
|
match = re.match(r'^\s*(\d+)\s*[-–—]\s*(.+)$', val_str)
|
|
# Jika pola angka strip teks cocok
|
|
if match:
|
|
# Mengambil grup angka pertama dan mengonversinya ke integer
|
|
kode = int(match.group(1))
|
|
# Mengambil grup teks kedua dan membersihkan spasi
|
|
teks = match.group(2).strip()
|
|
# Mengembalikan pasangan (kode, teks)
|
|
return kode, teks
|
|
|
|
# Pola khusus untuk teks rentang nominal gaji (contoh: "Rp 3.000.000,- s/d Rp 4.000.000,-")
|
|
if 's/d' in val_str or 'sd' in val_str.lower():
|
|
# Mengekstrak seluruh digit angka dan titik sebelum pemisah 's/d'
|
|
numbers = re.findall(r'[\d.]+', val_str.split('s/d')[0] if 's/d' in val_str else val_str.split('sd')[0])
|
|
# Jika digit angka berhasil ditemukan
|
|
if numbers:
|
|
try:
|
|
# Mengambil angka batas bawah, menghapus tanda pemisah titik, dan mengonversinya ke integer
|
|
num = int(numbers[-1].replace('.', ''))
|
|
# Mengembalikan angka nominal batas bawah bersama string teks aslinya
|
|
return num, val_str
|
|
except:
|
|
# Jika konversi gagal, abaikan dan lanjut ke fallback
|
|
pass
|
|
|
|
# Jika sudah berupa string teks bebas tanpa format kode di depannya, kembalikan teks apa adanya
|
|
return None, val_str
|
|
|
|
|
|
# Fungsi untuk memvalidasi dan mengonversi format tanggal string atau serial Excel ke standar format tanggal MySQL (YYYY-MM-DD)
|
|
def parse_date(val) -> Optional[str]:
|
|
"""
|
|
Mengonversi berbagai variasi format penanggalan dari file CSV atau format angka serial Excel menjadi string tanggal baku ISO (YYYY-MM-DD).
|
|
"""
|
|
# Memeriksa jika nilai tanggal bernilai kosong atau None
|
|
if pd.isna(val) or val is None or val == '':
|
|
return None
|
|
|
|
# Jika nilai input sudah merupakan instance objek datetime atau pandas Timestamp
|
|
if isinstance(val, (datetime, pd.Timestamp)):
|
|
# Format langsung menjadi 'YYYY-MM-DD'
|
|
return val.strftime('%Y-%m-%d')
|
|
|
|
# Mengubah nilai menjadi string bersih
|
|
val_str = str(val).strip()
|
|
# Mengabaikan string kosong atau karakter placeholder umum seperti tanda strip
|
|
if val_str in ['', 'nan', 'None', '-', '_', '0']:
|
|
return None
|
|
|
|
# Memeriksa kemungkinan format angka serial tanggal bawaan Microsoft Excel (misal: angka 44794 merepresentasikan hari sejak 30-12-1899)
|
|
try:
|
|
float_val = float(val_str)
|
|
# Rentang serial number tanggal Excel valid (antara rentang tahun 1980 - 2064)
|
|
if 30000 <= float_val <= 60000:
|
|
# Menghitung tanggal kalender dengan menambahkan selisih hari ke basis kalender Excel (30 Desember 1899)
|
|
excel_date = datetime(1899, 12, 30) + timedelta(days=float_val)
|
|
# Mengembalikan tanggal dalam format string 'YYYY-MM-DD'
|
|
return excel_date.strftime('%Y-%m-%d')
|
|
except (ValueError, TypeError):
|
|
# Jika bukan angka serial Excel, lanjutkan ke parsing format tanggal teks
|
|
pass
|
|
|
|
# Daftar format pola string penanggalan umum yang sering muncul pada spreadsheet kuesioner
|
|
formats = [
|
|
'%Y-%m-%d', '%Y/%m/%d',
|
|
'%d-%m-%Y', '%d/%m/%Y',
|
|
'%m-%d-%Y', '%m/%d/%Y',
|
|
'%d-%m-%y', '%d/%m/%y',
|
|
'%m-%d-%y', '%m/%d/%y',
|
|
'%Y-%m-%d %H:%M:%S', '%Y/%m/%d %H:%M:%S',
|
|
'%d/%m/%Y %H:%M:%S', '%d-%m-%Y %H:%M:%S'
|
|
]
|
|
# Mencoba mencocokkan nilai string dengan setiap format tanggal yang tersedia
|
|
for fmt in formats:
|
|
try:
|
|
# Jika format cocok, kembalikan tanggal terformat ISO
|
|
return datetime.strptime(val_str, fmt).strftime('%Y-%m-%d')
|
|
except ValueError:
|
|
# Jika tidak cocok, coba format berikutnya di list
|
|
continue
|
|
|
|
# Kembalikan None jika tidak ada satupun pola tanggal yang cocok
|
|
return None
|
|
|
|
|
|
# =========================================================================
|
|
# FUNGSI-FUNGSI KONVERSI NILAI KUESIONER
|
|
# =========================================================================
|
|
|
|
# Fungsi utama untuk menerjemahkan nilai kode jawaban mentah menjadi teks deskriptif
|
|
def konversi_jawaban(field_code: Optional[str], nilai, db: Optional[Session] = None) -> Any:
|
|
"""
|
|
Mengonversi kode angka kuesioner ke teks label jawaban yang sesuai.
|
|
|
|
PENTING: Nilai 0 ADALAH jawaban valid untuk beberapa field kuesioner:
|
|
- F502: 0 bulan (artinya langsung bekerja saat lulus kuliah)
|
|
- F302/F303: 0 bulan (mulai mencari saat lulus)
|
|
- F401-F415: 0 bernilai "Tidak", 1 bernilai "Ya"
|
|
|
|
Args:
|
|
field_code: Kode butir pertanyaan (contoh: 'F14', 'F504')
|
|
nilai: Nilai mentah dari sel CSV
|
|
db: Sesi koneksi database opsional
|
|
|
|
Returns:
|
|
Nilai teks yang telah dikonversi, atau None jika sel kosong
|
|
"""
|
|
# Memeriksa sel kosong atau NaN
|
|
if pd.isna(nilai) or nilai == '':
|
|
return None
|
|
|
|
# Memeriksa string kosong setelah pembersihan spasi
|
|
if isinstance(nilai, str) and nilai.strip() in ['', 'nan', 'None']:
|
|
return None
|
|
|
|
# Penanganan data kuesioner format 2024 yang sudah memuat "KODE - TEKS"
|
|
if isinstance(nilai, str):
|
|
# Mencocokkan regex pola angka strip teks
|
|
match = re.match(r'^\s*(\d+)\s*[-–—]\s*(.+)$', nilai)
|
|
if match:
|
|
kode = int(match.group(1))
|
|
teks = match.group(2).strip()
|
|
|
|
# Jika teks memiliki deskripsi yang cukup panjang (>5 karakter), gunakan langsung teks tersebut
|
|
if len(teks) > 5:
|
|
return teks
|
|
|
|
# Jika nilai diawali huruf (sudah berbentuk teks naratif penuh tanpa angka kode)
|
|
if not nilai[0].isdigit() if nilai else True:
|
|
return nilai.strip()
|
|
|
|
# Menyimpan nilai asli sebelum dikonversi
|
|
original_nilai = nilai
|
|
# Konversi string angka menjadi integer
|
|
if isinstance(nilai, str):
|
|
try:
|
|
nilai = int(nilai)
|
|
except ValueError:
|
|
pass
|
|
|
|
# Konversi float murni (misal 1.0) menjadi integer (1)
|
|
if isinstance(nilai, float):
|
|
if nilai != int(nilai):
|
|
return None # Mengabaikan bilangan desimal yang tidak valid untuk kode kuesioner
|
|
nilai = int(nilai)
|
|
|
|
# Menentukan nilai kunci pencarian (key)
|
|
nilai_key: Any = int(nilai) if isinstance(nilai, (int, float)) else nilai
|
|
# Mengubah kode field menjadi huruf kapital bersih
|
|
code_upper = field_code.upper().strip() if field_code else ''
|
|
|
|
# ================= Konversi Berdasarkan Kode Pertanyaan Spesifik =================
|
|
|
|
# F1761-F1774: Penilaian 7 kelompok kompetensi saat lulus & saat ini (skala 1-5, nilai 0 diabaikan menjadi NULL)
|
|
if code_upper in ['F1761', 'F1762', 'F1763', 'F1764', 'F1765', 'F1766', 'F1767', 'F1768', 'F1769', 'F1770', 'F1771', 'F1772', 'F1773', 'F1774']:
|
|
if nilai_key == 0:
|
|
return None
|
|
return KOMPETENSI_LEVEL.get(nilai_key)
|
|
|
|
# F21-F27: Evaluasi metode pembelajaran di kampus (skala 1-5, nilai 0 diabaikan menjadi NULL)
|
|
if code_upper in ['F21', 'F22', 'F23', 'F24', 'F25', 'F26', 'F27']:
|
|
if nilai_key == 0:
|
|
return None
|
|
return METODE_LEVEL.get(nilai_key)
|
|
|
|
# F401-F415: Saluran cara mencari pekerjaan (jawaban biner: 0 = "Tidak", 1 = "Ya")
|
|
if code_upper in ['F401', 'F402', 'F403', 'F404', 'F405', 'F406', 'F407',
|
|
'F408', 'F409', 'F410', 'F411', 'F412', 'F413', 'F414', 'F415']:
|
|
return YA_TIDAK.get(nilai_key)
|
|
|
|
# F504: Status mendapatkan pekerjaan dalam <= 6 bulan (1 = Ya, 2 = Tidak)
|
|
if code_upper == 'F504':
|
|
return JAWABAN_MAPPING.get('F504', {}).get(nilai_key)
|
|
|
|
# F505: Rata-rata pendapatan gaji per bulan (pencocokan ke range atau format mata uang Rupiah)
|
|
if code_upper == 'F505':
|
|
if nilai_key in JAWABAN_MAPPING.get('F505', {}):
|
|
return JAWABAN_MAPPING['F505'][nilai_key]
|
|
if isinstance(nilai_key, (int, float)) and nilai_key > 0:
|
|
return f"Rp {int(nilai_key):,}".replace(',', '.')
|
|
if isinstance(nilai, str) and nilai.strip() and nilai.strip() not in ['0', '0.0']:
|
|
return nilai.strip()
|
|
return None
|
|
|
|
# F1601-F1613: Pertimbangan alasan memilih pekerjaan yang tidak sesuai bidang
|
|
if code_upper in ['F1601', 'F1602', 'F1603', 'F1604', 'F1605', 'F1606',
|
|
'F1607', 'F1608', 'F1609', 'F1610', 'F1611', 'F1612', 'F1613']:
|
|
if code_upper in JAWABAN_MAPPING:
|
|
return JAWABAN_MAPPING[code_upper].get(nilai_key)
|
|
return YA_TIDAK.get(nilai_key)
|
|
|
|
# F502, F302, F303: Jumlah durasi bulan masa tunggu mencari kerja (nilai 0 s.d. 12 valid berupa angka murni)
|
|
if code_upper in ['F502', 'F302', 'F303']:
|
|
return nilai_key # Dikembalikan sebagai angka numerik murni
|
|
|
|
# F5A1: Wilayah provinsi tempat bekerja (konversi kode provinsi ke nama provinsi)
|
|
if code_upper == 'F5A1':
|
|
nama_provinsi = PROVINSI_MAPPING.get(nilai_key)
|
|
if nama_provinsi:
|
|
return f"{nilai_key} - {nama_provinsi}"
|
|
return nilai_key
|
|
|
|
# F5A2: Wilayah kabupaten atau kota tempat bekerja
|
|
if code_upper == 'F5A2':
|
|
nama_kabupaten = KABUPATEN_MAPPING.get(nilai_key)
|
|
if nama_kabupaten:
|
|
return f"{nilai_key} - {nama_kabupaten}"
|
|
return nilai_key
|
|
|
|
# Pengecekan pada kamus umum JAWABAN_MAPPING untuk kode lainnya
|
|
if code_upper in JAWABAN_MAPPING:
|
|
return JAWABAN_MAPPING[code_upper].get(nilai_key)
|
|
if field_code in JAWABAN_MAPPING:
|
|
return JAWABAN_MAPPING[field_code].get(nilai_key)
|
|
|
|
# Nilai bawaan jika tidak ada kamus pencocokan: kembalikan nilai asli
|
|
return original_nilai
|
|
|
|
|
|
# Fungsi pembantu untuk memilih apakah menyimpan kode numerik atau teks deskripsi sesuai tipe kolom di database
|
|
def convert_value_for_field(field_code: str, kode: int, teks: str, is_numeric_field: bool) -> Any:
|
|
"""
|
|
Menentukan representasi data akhir yang akan disimpan ke database berdasarkan tipe kolom tabel target.
|
|
|
|
Args:
|
|
field_code: Kode butir pertanyaan (contoh: 'F14', 'F504')
|
|
kode: Angka kode jawaban hasil parsing
|
|
teks: Teks label jawaban hasil parsing
|
|
is_numeric_field: Bernilai True jika kolom database bertipe integer (misal: bulan tunggu)
|
|
|
|
Returns:
|
|
Nilai yang telah disesuaikan dengan tipe kolom database
|
|
"""
|
|
# Jika kolom database adalah tipe numerik integer, prioritaskan mengembalikan kode angka
|
|
if is_numeric_field:
|
|
return kode
|
|
|
|
# Jika teks deskripsi tersedia dan panjangnya memadai, gunakan teks deskriptif
|
|
if teks and len(teks) > 5:
|
|
return teks
|
|
|
|
# Jika kode angka ada, coba konversikan kode tersebut ke teks melalui fungsi konversi_jawaban
|
|
if kode is not None:
|
|
return konversi_jawaban(field_code, kode)
|
|
|
|
# Opsi fallback terakhir: kembalikan teks
|
|
return teks
|
|
|