feat: initial commit project tracer study TA D4 TIF
This commit is contained in:
commit
8e94efc788
|
|
@ -0,0 +1,46 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
.claude
|
||||||
|
|
||||||
|
# Python & Virtual Environments
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env/
|
||||||
|
.venv/
|
||||||
|
**/venv/
|
||||||
|
**/__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
Backend/venv/
|
||||||
|
|
||||||
|
# Environment Variables & Secrets
|
||||||
|
.env
|
||||||
|
*.env
|
||||||
|
!.env.example
|
||||||
|
Backend/.env
|
||||||
|
|
||||||
|
# Local logs
|
||||||
|
*.log
|
||||||
|
Backend/*.log
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||||
|
"plugins": ["react", "oxc"],
|
||||||
|
"rules": {
|
||||||
|
"react/rules-of-hooks": "error",
|
||||||
|
"react/only-export-components": ["warn", { "allowConstantExport": true }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { Routes, Route, Navigate, useNavigate, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
|
import LoginPage from "./src/Login/login";
|
||||||
|
import Dashboard from "./src/dashboard";
|
||||||
|
import AlumniForm from "./src/form-alumni";
|
||||||
|
import ExportKlasifikasi from "./src/Export-Klasifikasi";
|
||||||
|
import ForgotPassword from "./src/forgot-password";
|
||||||
|
import ProsesKlasifikasi from "./src/proses-klasifikasi";
|
||||||
|
import ResetPassword from "./src/reset-password";
|
||||||
|
import TahunAngkatan from "./src/tahun-angkatan";
|
||||||
|
import ImportData from "./src/ImportData";
|
||||||
|
import KuesionerAlumni from "./src/kuesioner-alumni";
|
||||||
|
import KamusKataKunci from "./src/kamus-kata-kunci";
|
||||||
|
|
||||||
|
function ProtectedRoute({ children }) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkAuth = () => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
const username = localStorage.getItem('username');
|
||||||
|
if (!token || !username) {
|
||||||
|
navigate('/', { replace: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkAuth();
|
||||||
|
|
||||||
|
// Dengarkan saat logout dari tab browser lain
|
||||||
|
const handleStorageChange = (e) => {
|
||||||
|
if (e.key === 'token' || e.key === 'username' || e.key === null) {
|
||||||
|
checkAuth();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('storage', handleStorageChange);
|
||||||
|
return () => window.removeEventListener('storage', handleStorageChange);
|
||||||
|
}, [navigate, location]);
|
||||||
|
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
const username = localStorage.getItem('username');
|
||||||
|
|
||||||
|
if (!token || !username) {
|
||||||
|
return <Navigate to="/" replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
{/* Rute Publik */}
|
||||||
|
<Route path="/" element={<LoginPage />} />
|
||||||
|
<Route path="/register" element={<Navigate to="/" replace />} />
|
||||||
|
<Route path="/forgot-password" element={<ForgotPassword />} />
|
||||||
|
<Route path="/reset-password" element={<ResetPassword />} />
|
||||||
|
<Route path="/kuesioner" element={<KuesionerAlumni />} />
|
||||||
|
<Route path="/tracer-study" element={<KuesionerAlumni />} />
|
||||||
|
|
||||||
|
{/* Rute Terproteksi (Wajib Login) */}
|
||||||
|
<Route path="/dashboard" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
|
||||||
|
<Route path="/alumni" element={<ProtectedRoute><AlumniForm /></ProtectedRoute>} />
|
||||||
|
<Route path="/export-klasifikasi" element={<ProtectedRoute><ExportKlasifikasi /></ProtectedRoute>} />
|
||||||
|
<Route path="/proses-klasifikasi" element={<ProtectedRoute><ProsesKlasifikasi /></ProtectedRoute>} />
|
||||||
|
<Route path="/tahun-angkatan" element={<ProtectedRoute><TahunAngkatan /></ProtectedRoute>} />
|
||||||
|
<Route path="/import" element={<ProtectedRoute><ImportData standalone={true} /></ProtectedRoute>} />
|
||||||
|
<Route path="/keywords" element={<ProtectedRoute><KamusKataKunci standalone={true} /></ProtectedRoute>} />
|
||||||
|
<Route path="/kamus-kata-kunci" element={<ProtectedRoute><KamusKataKunci standalone={true} /></ProtectedRoute>} />
|
||||||
|
|
||||||
|
{/* Fallback jika route tidak ditemukan */}
|
||||||
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
|
</Routes>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Application Config
|
||||||
|
APP_NAME="FastAPI TA Alumni"
|
||||||
|
DEBUG=True
|
||||||
|
API_PREFIX="/api/v1"
|
||||||
|
|
||||||
|
# Database Connection
|
||||||
|
# SQLite (Local Dev Default):
|
||||||
|
DATABASE_URL="sqlite:///./ta_database.db"
|
||||||
|
# PostgreSQL Example:
|
||||||
|
# DATABASE_URL="postgresql://username:password@localhost:5432/dbname"
|
||||||
|
# MySQL Example:
|
||||||
|
# DATABASE_URL="mysql+pymysql://username:password@localhost:3306/dbname"
|
||||||
|
|
||||||
|
# Security & JWT Token
|
||||||
|
# Generate a secure key: openssl rand -hex 32
|
||||||
|
JWT_SECRET_KEY="replace_this_with_a_very_long_random_hex_string_in_production"
|
||||||
|
JWT_ALGORITHM="HS256"
|
||||||
|
ACCESS_TOKEN_EXPIRE_MINUTES=1440
|
||||||
|
|
@ -0,0 +1,209 @@
|
||||||
|
# 🤖 Machine Learning Model
|
||||||
|
|
||||||
|
## Deskripsi
|
||||||
|
|
||||||
|
Model Machine Learning untuk clustering alumni berdasarkan kompetensi menggunakan Text Mining (TF-IDF) dan K-Means Clustering.
|
||||||
|
|
||||||
|
## Struktur Folder
|
||||||
|
|
||||||
|
```
|
||||||
|
Model/
|
||||||
|
├── __init__.py
|
||||||
|
├── main.py # Pipeline utama
|
||||||
|
├── preprocessing/
|
||||||
|
│ └── text_preprocessing.py # Text preprocessing
|
||||||
|
├── feature_engineering/
|
||||||
|
│ └── tfidf.py # TF-IDF extraction
|
||||||
|
├── clustering/
|
||||||
|
│ └── kmeans.py # K-Means clustering
|
||||||
|
├── utils/
|
||||||
|
├── data/
|
||||||
|
│ ├── raw/ # Data mentah
|
||||||
|
│ ├── processed/ # Data sudah diproses
|
||||||
|
│ └── result/ # Hasil clustering
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Instalasi Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install pandas scikit-learn nltk sastrawi
|
||||||
|
```
|
||||||
|
|
||||||
|
## Penggunaan Dasar
|
||||||
|
|
||||||
|
### 1. Import Pipeline
|
||||||
|
|
||||||
|
```python
|
||||||
|
import pandas as pd
|
||||||
|
from Model.main import AlumniClusteringPipeline
|
||||||
|
|
||||||
|
# Siapkan data
|
||||||
|
data = pd.DataFrame({
|
||||||
|
'nama': ['Ali', 'Budi', 'Citra'],
|
||||||
|
'perusahaan': ['Google', 'Tokopedia', 'Gojek'],
|
||||||
|
'jabatan': ['Engineer', 'Analyst', 'Manager']
|
||||||
|
})
|
||||||
|
|
||||||
|
# Jalankan pipeline
|
||||||
|
pipeline = AlumniClusteringPipeline(n_clusters=3)
|
||||||
|
result = pipeline.fit_transform(data)
|
||||||
|
|
||||||
|
print(result)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Auto-Detect K Optimal
|
||||||
|
|
||||||
|
```python
|
||||||
|
pipeline = AlumniClusteringPipeline(n_clusters=3, auto_k=True)
|
||||||
|
result = pipeline.fit_transform(data)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Simpan Hasil
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Simpan ke CSV
|
||||||
|
result.to_csv('hasil_clustering.csv', index=False)
|
||||||
|
|
||||||
|
# Lihat distribusi cluster
|
||||||
|
print(result['cluster_label'].value_counts())
|
||||||
|
|
||||||
|
# Lihat ringkasan
|
||||||
|
summary = pipeline.get_cluster_summary(result)
|
||||||
|
print(summary)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Input Model
|
||||||
|
|
||||||
|
Model menerima DataFrame dengan kolom:
|
||||||
|
|
||||||
|
| Kolom | Deskripsi |
|
||||||
|
|-------|-----------|
|
||||||
|
| `nama` | Nama lengkap alumni |
|
||||||
|
| `perusahaan` | Nama perusahaan/instansi |
|
||||||
|
| `jabatan` | Jabatan/posisi |
|
||||||
|
|
||||||
|
## Output Model
|
||||||
|
|
||||||
|
DataFrame dengan kolom tambahan:
|
||||||
|
|
||||||
|
| Kolom | Deskripsi |
|
||||||
|
|-------|-----------|
|
||||||
|
| `cluster` | Nomor cluster (0, 1, 2, ...) |
|
||||||
|
| `cluster_label` | Label cluster ("Cluster_0", "Cluster_1", ...) |
|
||||||
|
|
||||||
|
## CLI Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Model
|
||||||
|
python main.py [input.csv] [n_clusters] [output.csv]
|
||||||
|
```
|
||||||
|
|
||||||
|
Contoh:
|
||||||
|
```bash
|
||||||
|
python main.py ../data/raw/alumni.csv 3 ../data/result/hasil.csv
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integrasi dengan Backend
|
||||||
|
|
||||||
|
Model sudah diintegrasikan dengan Backend FastAPI melalui `ml_service.py`.
|
||||||
|
|
||||||
|
Endpoint untuk clustering tersedia di `/api/v1/clustering`.
|
||||||
|
|
||||||
|
Lihat `Backend/app/services/ml_service.py` untuk detail.
|
||||||
|
|
||||||
|
----------------------------------------
|
||||||
|
|
||||||
|
## Update Progress (2026-08-28)
|
||||||
|
|
||||||
|
### Pipeline ML Selesai (6 Tahap)
|
||||||
|
|
||||||
|
| Tahap | Script | Fungsi |
|
||||||
|
|-------|--------|--------|
|
||||||
|
| 1 | `column_mapping.json` | Mapping kolom 2019-2025 |
|
||||||
|
| 2 | `merge_data.py` | Merge data multi-file CSV |
|
||||||
|
| 3 | `text_preprocessing.py` | Text preprocessing + Sastrawi |
|
||||||
|
| 4 | `clustering.py` | TF-IDF + K-Means K=4 |
|
||||||
|
| 5 | `interpretasi.py` | Interpretasi C0-C3 |
|
||||||
|
| 6 | `dashboard.py` | Generate visualisasi |
|
||||||
|
|
||||||
|
### Cara Menjalankan Pipeline
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Model/preprocessing
|
||||||
|
python merge_data.py # Tahap 1-2
|
||||||
|
python text_preprocessing.py # Tahap 3
|
||||||
|
python clustering.py # Tahap 4
|
||||||
|
python interpretasi.py # Tahap 5
|
||||||
|
python dashboard.py # Tahap 6
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data Sources
|
||||||
|
|
||||||
|
| Tahun | File CSV | Records |
|
||||||
|
|-------|----------|---------|
|
||||||
|
| 2019 | Data_2019_Tracer_study.csv | 30 |
|
||||||
|
| 2020 | Data_2020_Tracer_study.csv | 31 |
|
||||||
|
| 2021 | Data_2021_Tracer_study.csv | 237 |
|
||||||
|
| 2022 | Data_2022_Tracer_study.csv | 211 |
|
||||||
|
| 2023 | Data_2023_Tracer_study.csv | 268 |
|
||||||
|
| 2024 | Data_2024_Tracer_study.csv | 361 |
|
||||||
|
| 2025 | Data_2025_Tracer_study_*.csv (5 file) | 458 |
|
||||||
|
| **Total** | | **1596** |
|
||||||
|
|
||||||
|
### Hasil
|
||||||
|
|
||||||
|
- **Total Alumni:** 1596
|
||||||
|
- **Data untuk Clustering:** 1114
|
||||||
|
- **K-Means K:** 4 (sesuai PRD)
|
||||||
|
- **Silhouette Score:** 0.1034
|
||||||
|
- **TF-IDF Features:** 500
|
||||||
|
|
||||||
|
### Distribusi Cluster
|
||||||
|
|
||||||
|
| Cluster | Jumlah | % | Top Terms |
|
||||||
|
|---------|--------|---|-----------|
|
||||||
|
| 0 | 587 | 52.7% | perintah, bumn, bnpp, jember, staff |
|
||||||
|
| 1 | 146 | 13.1% | freelance, kerja lepas |
|
||||||
|
| 2 | 183 | 16.4% | wiraswasta, usaha sendiri |
|
||||||
|
| 3 | 198 | 17.8% | swasta, usaha swasta, indonesia |
|
||||||
|
|
||||||
|
### Distribusi Kategori C0-C3
|
||||||
|
|
||||||
|
| Kategori | Deskripsi | Jumlah | % |
|
||||||
|
|----------|-----------|--------|---|
|
||||||
|
| C0 | Sesuai Profil Lulusan | 223 | 13.97% |
|
||||||
|
| C1 | Bidang Diketahui (BUMN, pemerintah, dll) | 385 | 24.12% |
|
||||||
|
| C2 | Bidang Tidak Jelas | 506 | 31.70% |
|
||||||
|
| C3 | Belum/Tidak Bekerja | 482 | 30.20% |
|
||||||
|
|
||||||
|
### Catatan Hasil
|
||||||
|
|
||||||
|
- **C0 (14%)**: Alumni bekerja di bidang sesuai prodi (IT, jaringan, dll)
|
||||||
|
- **C1 (24%)**: Bekerja di bidang non-teknis (BUMN, pemerintah, dll)
|
||||||
|
- **C2 (32%)**: Bidang pekerjaan tidak teridentifikasi jelas
|
||||||
|
- **C3 (30%)**: Alumni belum/tidak bekerja saat tracer study
|
||||||
|
|
||||||
|
### Distribusi per Prodi
|
||||||
|
|
||||||
|
| Prodi | Kode | C0 | C1 | C2 | C3 |
|
||||||
|
|-------|------|----|----|----|----|
|
||||||
|
| TIF | 55301 | 13 | 135 | 365 | 169 |
|
||||||
|
| MIF | 57401 | 10 | 66 | 226 | 136 |
|
||||||
|
| TKK | 56401 | 13 | 48 | 217 | 122 |
|
||||||
|
|
||||||
|
### Output Files
|
||||||
|
|
||||||
|
```
|
||||||
|
Model/data/
|
||||||
|
├── standardized/
|
||||||
|
│ └── unified_dataset.csv # Data setelah merge
|
||||||
|
├── processed/
|
||||||
|
│ ├── dataset_untuk_clustering.csv
|
||||||
|
│ ├── clustering_results.csv
|
||||||
|
│ ├── model_info.json
|
||||||
|
│ └── dashboard/
|
||||||
|
│ └── dashboard.html # Dashboard interaktif
|
||||||
|
└── result/
|
||||||
|
└── interpretasi_results.csv # Hasil C0-C3
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
# Model Module
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
# Clustering module
|
||||||
|
|
@ -0,0 +1,176 @@
|
||||||
|
"""
|
||||||
|
K-Means Clustering Module
|
||||||
|
Mengelompokkan alumni berdasarkan kompetensi menggunakan algoritma K-Means Clustering.
|
||||||
|
Modul ini mencakup wrapper class untuk scikit-learn KMeans, pencarian k optimal (Elbow Method & Silhouette),
|
||||||
|
serta ringkasan statistik setiap kelompok (cluster).
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Mengimpor modul Any untuk type hint fleksibel
|
||||||
|
from typing import Any
|
||||||
|
# Mengimpor modul KMeans dari pustaka scikit-learn (sklearn.cluster) untuk implementasi algoritma clustering berbasis centroid
|
||||||
|
from sklearn.cluster import KMeans
|
||||||
|
# Mengimpor fungsi silhouette_score untuk mengukur seberapa baik data terkelompok dalam cluster-nya dibandingkan cluster tetangga
|
||||||
|
from sklearn.metrics import silhouette_score
|
||||||
|
# Mengimpor pustaka numpy untuk operasi array numerik dan manipulasi matriks fitur
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Mendefinisikan class KMeansClustering sebagai pembungkus (wrapper) model KMeans Scikit-Learn
|
||||||
|
class KMeansClustering:
|
||||||
|
"""K-Means Clustering wrapper: Memudahkan inisialisasi, pelatihan (fit), dan prediksi klaster."""
|
||||||
|
|
||||||
|
# Metode konstruktor class yang dijalankan saat objek pertama kali diinisialisasi
|
||||||
|
def __init__(self, n_clusters: int = 3, random_state: int = 42):
|
||||||
|
"""
|
||||||
|
Inisialisasi hyperparameter algoritma K-Means:
|
||||||
|
Args:
|
||||||
|
n_clusters: Jumlah cluster (k) yang diinginkan (default: 3)
|
||||||
|
random_state: Seed acak untuk memastikan hasil inisialisasi centroid selalu sama/reproducible (default: 42)
|
||||||
|
"""
|
||||||
|
# Menyimpan nilai jumlah cluster ke dalam atribut instance objek
|
||||||
|
self.n_clusters = n_clusters
|
||||||
|
# Menyimpan nilai seed acak ke dalam atribut instance objek
|
||||||
|
self.random_state = random_state
|
||||||
|
# Menginstansiasi objek model KMeans dari Scikit-Learn
|
||||||
|
# n_init='auto': Algoritma secara otomatis menentukan jumlah inisialisasi centroid awal berbasis heuristik Scikit-Learn modern
|
||||||
|
self.model = KMeans(
|
||||||
|
n_clusters=n_clusters,
|
||||||
|
random_state=random_state,
|
||||||
|
n_init='auto'
|
||||||
|
)
|
||||||
|
# Menyiapkan atribut untuk menyimpan label cluster dari tiap data setelah model dilatih (default None)
|
||||||
|
self.labels_ = None
|
||||||
|
# Menyiapkan atribut untuk menyimpan nilai inertia (Within-Cluster Sum of Squares) setelah dilatih (default None)
|
||||||
|
self.inertia_ = None
|
||||||
|
|
||||||
|
# Metode fit untuk melatih model K-Means pada dataset masukan X
|
||||||
|
def fit(self, X) -> 'KMeansClustering':
|
||||||
|
"""Melatih (fit) model K-Means terhadap matriks data/fitur X"""
|
||||||
|
# Menjalankan proses clustering pada matriks fitur X hingga posisi centroid konvergen
|
||||||
|
self.model.fit(X)
|
||||||
|
# Menyimpan label cluster hasil fitting (0, 1, 2, ...) ke atribut labels_
|
||||||
|
self.labels_ = self.model.labels_
|
||||||
|
# Mengambil nilai inertia (jumlah kuadrat jarak titik ke centroid terdekat) dari model terlatih
|
||||||
|
self.inertia_ = self.model.inertia_
|
||||||
|
# Mengembalikan instance objek diri sendiri (self) untuk chaining method jika diperlukan
|
||||||
|
return self
|
||||||
|
|
||||||
|
# Metode predict untuk memprediksi cluster data baru tanpa melatih ulang posisi centroid
|
||||||
|
def predict(self, X) -> Any:
|
||||||
|
"""Memprediksi label cluster untuk data baru berdasarkan centroid yang telah terbentuk"""
|
||||||
|
# Mengembalikan array numpy berisi label cluster hasil prediksi jarak Euclidean terdekat ke centroid
|
||||||
|
return self.model.predict(X)
|
||||||
|
|
||||||
|
# Metode fit_predict untuk melatih model sekaligus menghasilkan prediksi label cluster data dalam satu langkah
|
||||||
|
def fit_predict(self, X) -> Any:
|
||||||
|
"""Melatih model sekaligus menghasilkan label cluster secara langsung"""
|
||||||
|
# Menjalankan proses fitting dan langsung mengembalikan array label cluster
|
||||||
|
self.labels_ = self.model.fit_predict(X)
|
||||||
|
# Menyimpan nilai inersia akhir dari proses klastering
|
||||||
|
self.inertia_ = self.model.inertia_
|
||||||
|
# Mengembalikan array label cluster numerik
|
||||||
|
return self.labels_
|
||||||
|
|
||||||
|
|
||||||
|
# Fungsi untuk mencari nilai k optimal menggunakan kombinasi Elbow Method dan Silhouette Score
|
||||||
|
def find_optimal_k(X, k_range: range = range(2, 11)) -> dict:
|
||||||
|
"""
|
||||||
|
Mencari jumlah cluster optimal (k) menggunakan Elbow Method dan Silhouette Analysis.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
X: Matriks fitur (feature matrix) yang telah melalui tahap preprocessing dan feature engineering
|
||||||
|
k_range: Rentang nilai k yang akan diuji (default: dari k=2 hingga k=10)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary berisi daftar nilai k, inertia, dan silhouette score untuk setiap k yang diuji
|
||||||
|
"""
|
||||||
|
# Menyiapkan struktur data dictionary untuk menampung riwayat evaluasi
|
||||||
|
results = {
|
||||||
|
'k': [], # Menyimpan nilai k yang diuji
|
||||||
|
'inertia': [], # Menyimpan nilai inertia untuk grafik Elbow
|
||||||
|
'silhouette': [] # Menyimpan nilai rata-rata Silhouette Coefficient
|
||||||
|
}
|
||||||
|
|
||||||
|
# Melakukan iterasi untuk setiap nilai k dalam rentang k_range
|
||||||
|
for k in k_range:
|
||||||
|
# Menginisialisasi objek KMeans dengan jumlah cluster k saat ini (n_init='auto' sesuai standar Scikit-Learn terbaru)
|
||||||
|
kmeans = KMeans(n_clusters=k, random_state=42, n_init='auto')
|
||||||
|
# Melatih model dan memperoleh label pengelompokan cluster untuk matriks data X
|
||||||
|
labels = kmeans.fit_predict(X)
|
||||||
|
|
||||||
|
# Menambahkan nilai k saat ini ke list hasil
|
||||||
|
results['k'].append(k)
|
||||||
|
# Menambahkan nilai inertia (WCSS - Within-Cluster Sum of Squares) ke list hasil
|
||||||
|
results['inertia'].append(kmeans.inertia_)
|
||||||
|
|
||||||
|
# Silhouette score hanya dapat dihitung jika k > 1 dan terdapat lebih dari 1 kelompok cluster unik yang terbentuk
|
||||||
|
if k > 1 and len(set(labels)) > 1:
|
||||||
|
# Menghitung skor silhouette dan menambahkannya ke list hasil
|
||||||
|
results['silhouette'].append(silhouette_score(X, labels))
|
||||||
|
else:
|
||||||
|
# Jika kondisi tidak terpenuhi, set nilai skor silhouette menjadi 0
|
||||||
|
results['silhouette'].append(0)
|
||||||
|
|
||||||
|
# Mengembalikan dictionary lengkap berisi hasil pengujian seluruh nilai k
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
# Fungsi untuk menghitung ringkasan statistik dan distribusi data di setiap kelompok klaster
|
||||||
|
def get_cluster_summary(df, labels: np.ndarray) -> dict:
|
||||||
|
"""
|
||||||
|
Mendapatkan ringkasan profil statistik setiap cluster hasil pengelompokan.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
df: DataFrame pandas berisi data asli alumni sebelum ditransformasi
|
||||||
|
labels: Array numpy yang berisi label cluster dari setiap baris data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary yang berisi jumlah data (count), persentase, dan contoh data per cluster
|
||||||
|
"""
|
||||||
|
# Membuat salinan (copy) DataFrame asli agar tidak memodifikasi data induk
|
||||||
|
df_result = df.copy()
|
||||||
|
# Menambahkan kolom baru 'cluster' yang berisi label klaster hasil prediksi K-Means
|
||||||
|
df_result['cluster'] = labels
|
||||||
|
|
||||||
|
# Menyiapkan dictionary untuk menampung ringkasan tiap cluster
|
||||||
|
summary = {}
|
||||||
|
# Melakukan perulangan untuk setiap ID klaster unik yang sudah diurutkan (0, 1, 2, ...)
|
||||||
|
for cluster_id in sorted(df_result['cluster'].unique()):
|
||||||
|
# Memfilter baris data yang masuk ke dalam ID cluster saat ini
|
||||||
|
cluster_data = df_result[df_result['cluster'] == cluster_id]
|
||||||
|
# Mengisi ringkasan informasi cluster (jumlah baris, persentase dari total data, dan 3 sampel data teratas)
|
||||||
|
summary[f'Cluster_{cluster_id}'] = {
|
||||||
|
'count': len(cluster_data), # Jumlah anggota klaster
|
||||||
|
'percentage': round(len(cluster_data) / len(df_result) * 100, 2), # Persentase terhadap total populasi
|
||||||
|
'sample_data': cluster_data.head(3).to_dict('records') # 3 sampel baris data berformat list of dict
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mengembalikan dictionary ringkasan klaster
|
||||||
|
return summary
|
||||||
|
|
||||||
|
|
||||||
|
# Blok pengetesan lokal yang hanya dijalankan ketika file ini dieksekusi langsung di terminal (bukan diimpor)
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Mengimpor modul make_blobs untuk membuat dataset sintesis tiruan untuk keperluan unit testing
|
||||||
|
from sklearn.datasets import make_blobs
|
||||||
|
|
||||||
|
# Membuat 100 sampel data tiruan dengan 3 pusat klaster untuk menguji fungsionalitas modul
|
||||||
|
blob_data = make_blobs(n_samples=100, centers=3, random_state=42)
|
||||||
|
# Mengambil matriks fitur X dari data sintesis
|
||||||
|
X = blob_data[0]
|
||||||
|
|
||||||
|
# Menjalankan fungsi pencarian k optimal
|
||||||
|
results = find_optimal_k(X)
|
||||||
|
print("Elbow Method Results:")
|
||||||
|
# Menampilkan hasil evaluasi inertia dan silhouette score untuk setiap nilai k
|
||||||
|
for i in range(len(results['k'])):
|
||||||
|
print(f" K={results['k'][i]}: Inertia={results['inertia'][i]:.2f}, Silhouette={results['silhouette'][i]:.4f}")
|
||||||
|
|
||||||
|
# Menguji proses klastering akhir dengan k=3
|
||||||
|
kmeans = KMeansClustering(n_clusters=3)
|
||||||
|
# Melakukan fitting dan prediksi
|
||||||
|
labels = kmeans.fit_predict(X)
|
||||||
|
# Menampilkan label unik yang terbentuk
|
||||||
|
print(f"\nClustering complete! Labels: {np.unique(labels)}")
|
||||||
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
from .default_keywords import (
|
||||||
|
PRODI_CODES,
|
||||||
|
DEFAULT_KEYWORD_DICTIONARY,
|
||||||
|
DEFAULT_BIDANG_KERJA_UMUM,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
"""
|
||||||
|
=============================================================
|
||||||
|
MASTER CONFIGURATION: KAMUS PROFIL LULUSAN & SEKTOR PEKERJAAN
|
||||||
|
=============================================================
|
||||||
|
PRD Reference: Section 7, 18, & 24
|
||||||
|
Dokumen ini berisi kamus acuan default untuk profil kurikulum
|
||||||
|
program studi Jurusan Teknologi Informasi (D4 TIF, D3 MIF, D3 TKK)
|
||||||
|
dan daftar sektor industri kerja umum.
|
||||||
|
=============================================================
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Pemetaan Kode Program Studi ke Singkatan Resmi
|
||||||
|
PRODI_CODES = {
|
||||||
|
# Kode Resmi Nasional (PDDIKTI)
|
||||||
|
'55301': 'TIF',
|
||||||
|
'57401': 'MIF',
|
||||||
|
'56401': 'TKK',
|
||||||
|
# Kode Internal & PDD
|
||||||
|
'21': 'TIF',
|
||||||
|
'19': 'MIF',
|
||||||
|
'20': 'TKK',
|
||||||
|
'22': 'TIF', # Kampus Nganjuk
|
||||||
|
'23': 'TIF', # Kampus Sidoarjo
|
||||||
|
# Format Deskriptif Teks
|
||||||
|
'Teknik Informatika (55301)': 'TIF',
|
||||||
|
'Manajemen Informatika (57401)': 'MIF',
|
||||||
|
'Teknik Komputer (56401)': 'TKK',
|
||||||
|
'Teknik Informatika Kampus Kab. Nganjuk (55302)': 'TIF',
|
||||||
|
'Teknik Informatika Kampus Kab. Sidoarjo (55304)': 'TIF',
|
||||||
|
'D4 Teknik Informatika': 'TIF',
|
||||||
|
'D3 Manajemen Informatika': 'MIF',
|
||||||
|
'D3 Teknik Komputer': 'TKK',
|
||||||
|
'TIF': 'TIF',
|
||||||
|
'MIF': 'MIF',
|
||||||
|
'TKK': 'TKK',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Kamus Profil Lulusan Per Program Studi (Untuk Kategori C0 - Sesuai Profil)
|
||||||
|
DEFAULT_KEYWORD_DICTIONARY = {
|
||||||
|
'TIF': [
|
||||||
|
'software engineer', 'software developer', 'software development', 'software',
|
||||||
|
'programmer', 'programming', 'coder', 'coding', 'koding', 'code', 'dev', 'developer', 'development',
|
||||||
|
'quality assurance', 'qa engineer', 'qa tester', 'software tester', 'tester', 'testing', 'qa', 'qc software',
|
||||||
|
'it consultant', 'konsultan it', 'konsultan perangkat lunak', 'konsultan software',
|
||||||
|
'project manager it', 'manajemen proyek it', 'manajemen proyek', 'project manager',
|
||||||
|
'scrum master', 'system analyst', 'analis sistem', 'analisis sistem', 'teknik informatika',
|
||||||
|
'algoritma', 'pengembangan software', 'pengembangan aplikasi', 'app developer',
|
||||||
|
'aplikasi', 'sistem informasi', 'teknologi informasi', 'informatika',
|
||||||
|
'mobile developer', 'web developer', 'android developer', 'ios developer',
|
||||||
|
'frontend', 'frontend developer', 'backend', 'backend developer', 'fullstack', 'fullstack developer',
|
||||||
|
'game developer', 'game programmer', 'game', 'gaming', 'cyber security',
|
||||||
|
'cyber', 'keamanan sistem', 'keamanan siber', 'keamanan jaringan',
|
||||||
|
'machine learning', 'artificial intelligence', 'data scientist', 'data science',
|
||||||
|
'cloud', 'cloud computing', 'devops', 'devops engineer', 'site reliability engineer', 'sre',
|
||||||
|
'prompt engineer', 'ai engineer', 'nlp engineer', 'computer vision', 'cloud architect',
|
||||||
|
'it specialist', 'it staff', 'staff it', 'it officer', 'it programmer', 'lead developer',
|
||||||
|
'tech lead', 'solutions architect', 'software architect', 'security engineer',
|
||||||
|
'soc analyst', 'penetration tester', 'pentester', 'flutter', 'flutter developer',
|
||||||
|
'react developer', 'laravel developer', 'golang', 'python developer', 'java developer',
|
||||||
|
'automation engineer', 'automation tester', 'it auditor',
|
||||||
|
'startup teknologi', 'tech startup', 'software house', 'it entrepreneur',
|
||||||
|
'tokopedia', 'shopee', 'bukalapak', 'sevima', 'sentra vidya utama', 'traveloka',
|
||||||
|
'gojek', 'grab', 'tiket', 'edutech', 'fintech', 'edtech', 'dot indonesia',
|
||||||
|
'tech', 'technology', 'teknologi', 'technocraft', 'solusindo', 'solusitama', 'infomedia', 'telkom',
|
||||||
|
'entrepreneur', 'startup', 'technopreneur', 'founder', 'owner', 'co founder', 'ceo', 'cto', 'cio', 'wirausaha'
|
||||||
|
],
|
||||||
|
'MIF': [
|
||||||
|
'programmer', 'programming', 'coder', 'coding', 'koding', 'code', 'dev', 'development',
|
||||||
|
'software developer', 'software developer desktop', 'software development',
|
||||||
|
'software developer web', 'software developer mobile', 'web developer', 'mobile developer',
|
||||||
|
'frontend developer', 'backend developer', 'fullstack developer', 'database administrator',
|
||||||
|
'developer', 'database', 'dba', 'sql', 'mysql', 'postgresql', 'oracle', 'mongodb',
|
||||||
|
'data analyst', 'analis data', 'analisis data', 'data warehouse', 'etl',
|
||||||
|
'business intelligence', 'bi analyst', 'bi developer', 'data visualization', 'visualisasi data',
|
||||||
|
'data engineer', 'database engineer', 'data specialist', 'power bi', 'tableau',
|
||||||
|
'business analyst', 'analis bisnis', 'product owner', 'product manager',
|
||||||
|
'erd', 'manajemen database', 'database management', 'backend', 'frontend',
|
||||||
|
'fullstack', 'aplikasi', 'sistem informasi', 'manajemen informatika', 'pemrograman',
|
||||||
|
'teknologi informasi', 'komputer', 'android developer', 'ios developer',
|
||||||
|
'android', 'ios', 'mobile', 'ui ux', 'ui ux designer', 'ui designer', 'ux designer',
|
||||||
|
'product designer', 'graphic designer', 'desain grafis', 'desain logo', 'desain',
|
||||||
|
'design', 'multimedia', 'animasi', 'web designer', 'web master', 'wordpress developer',
|
||||||
|
'erp specialist', 'sap consultant', 'odoo', 'odoo developer',
|
||||||
|
'software house', 'tokopedia', 'shopee', 'bukalapak', 'sevima',
|
||||||
|
'sentra vidya utama', 'traveloka', 'gojek', 'grab', 'edutech', 'fintech', 'edtech',
|
||||||
|
'tech', 'technology', 'teknologi',
|
||||||
|
'it entrepreneur', 'founder', 'owner', 'co founder', 'ceo', 'wirausaha'
|
||||||
|
],
|
||||||
|
'TKK': [
|
||||||
|
'jaringan', 'network', 'networking', 'network administrator', 'network engineer',
|
||||||
|
'network security', 'administrator jaringan', 'jaringan komputer', 'teknisi jaringan komputer',
|
||||||
|
'lan wan', 'lan', 'wan', 'cloud computing', 'cloud', 'iot', 'internet of things',
|
||||||
|
'iot engineer', 'embedded', 'embedded system', 'firmware', 'hardware', 'sysadmin',
|
||||||
|
'system administrator', 'server administrator', 'datacenter engineer', 'noc engineer',
|
||||||
|
'noc technician', 'field engineer it', 'fiber optic', 'teknisi fiber',
|
||||||
|
'robotika', 'robotics', 'plc', 'scada', 'cctv technician', 'teknisi cctv',
|
||||||
|
'server', 'mikrotik', 'cisco', 'komputer', 'teknisi komputer', 'service komputer',
|
||||||
|
'infrastruktur', 'datacenter', 'teknik komputer', 'it support', 'it technician', 'helpdesk it',
|
||||||
|
'teknisi jaringan', 'admin jaringan', 'computer', 'laptop', 'pc',
|
||||||
|
'cell', 'cellular', 'seluler', 'telekomunikasi', 'telecommunication', 'teknisi',
|
||||||
|
'telkomsel', 'telkom', 'nexwave', 'huawei', 'zte', 'indosat', 'xl axiata', 'smartfren',
|
||||||
|
'fiber', 'optik', 'fiberstar', 'biznet', 'cbn', 'myrepublic', 'noc', 'tower', 'bts',
|
||||||
|
'tech', 'technology', 'teknologi', 'technopreneur',
|
||||||
|
'wirausaha bidang teknik komputer', 'founder', 'owner', 'co founder', 'ceo', 'wirausaha'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Daftar Sektor Pekerjaan Formal Non-IT (Untuk Kategori C1 - Bidang Diketahui)
|
||||||
|
DEFAULT_BIDANG_KERJA_UMUM = [
|
||||||
|
# Perbankan & Lembaga Keuangan
|
||||||
|
'bank', 'bni', 'bri', 'mandiri', 'bca', 'btn', 'btpn', 'bank jatim', 'bsi', 'mega', 'muamalat', 'cimb',
|
||||||
|
'perbankan', 'koperasi', 'leasing', 'asuransi', 'pegadaian', 'akuntan', 'accounting',
|
||||||
|
'finance', 'keuangan', 'perpajakan', 'pajak', 'teller', 'kasir', 'finansial',
|
||||||
|
# Manajemen, Administrasi & Staf Formal
|
||||||
|
'manager', 'supervisor', 'staff', 'admin', 'administrasi', 'hrd', 'personalia',
|
||||||
|
'receptionist', 'customer service', 'cs', 'operator', 'sekretaris', 'notaris',
|
||||||
|
# Pemasaran, Penjualan & Retail
|
||||||
|
'marketing', 'sales', 'sales promotion', 'spg', 'spb', 'telemarketing', 'account executive',
|
||||||
|
'toko', 'minimarket', 'indomaret', 'alfamart', 'supermarket', 'mart', 'distributor', 'agen',
|
||||||
|
# Industri Manufaktur, Migas, Energi, Pertanian
|
||||||
|
'migas', 'pelangi migas', 'oil', 'gas', 'pertamina', 'pln', 'pdam', 'bumn', 'bumd',
|
||||||
|
'tjiwi kimia', 'pabrik', 'manufaktur', 'industri', 'produksi', 'qc', 'qa qc', 'gudang', 'warehouse',
|
||||||
|
'east west seed', 'seed', 'benih', 'pertanian', 'perkebunan', 'peternakan', 'agro', 'pupuk',
|
||||||
|
'cosmetindo', 'moro ayu', 'kosmetik', 'farmasi', 'apotek', 'apoteker', 'kimia', 'tekstil',
|
||||||
|
# Pendidikan & Kesehatan
|
||||||
|
'guru', 'dosen', 'pengajar', 'instruktur', 'tutor', 'trainer', 'sekolah', 'kampus',
|
||||||
|
'politeknik', 'universitas', 'sd', 'smp', 'sma', 'smk', 'madrasah', 'pesantren',
|
||||||
|
'perawat', 'bidan', 'dokter', 'rumah sakit', 'rsud', 'klinik', 'puskesmas', 'laboratorium',
|
||||||
|
# Jasa Lapangan, Logistik, Kuliner & Instansi Publik
|
||||||
|
'driver', 'kurir', 'pengemudi', 'security', 'satpam', 'mekanik', 'bengkel',
|
||||||
|
'barista', 'koki', 'chef', 'waiter', 'pelayan', 'restoran', 'cafe', 'kafe', 'hotel', 'catering',
|
||||||
|
'penulis', 'editor', 'fotografer', 'videografer',
|
||||||
|
'pns', 'asn', 'polisi', 'tni', 'pemerintah', 'dinas', 'kelurahan', 'kecamatan', 'balai', 'kantor desa'
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
{
|
||||||
|
"0": {
|
||||||
|
"jumlah": 587,
|
||||||
|
"persentase": 52.69299820466786,
|
||||||
|
"top_terms": [
|
||||||
|
"perintah",
|
||||||
|
"bumn",
|
||||||
|
"bnpp",
|
||||||
|
"jember",
|
||||||
|
"staff",
|
||||||
|
"swasta",
|
||||||
|
"instansi perintah",
|
||||||
|
"instansi",
|
||||||
|
"founder",
|
||||||
|
"negeri"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"1": {
|
||||||
|
"jumlah": 146,
|
||||||
|
"persentase": 13.10592459605027,
|
||||||
|
"top_terms": [
|
||||||
|
"kerja",
|
||||||
|
"freelance",
|
||||||
|
"kerja lepas",
|
||||||
|
"lepas",
|
||||||
|
"freelance kerja",
|
||||||
|
"usaha",
|
||||||
|
"usaha sendiri",
|
||||||
|
"sendiri",
|
||||||
|
"wiraswasta usaha",
|
||||||
|
"lain tulis"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"2": {
|
||||||
|
"jumlah": 183,
|
||||||
|
"persentase": 16.427289048473966,
|
||||||
|
"top_terms": [
|
||||||
|
"wiraswasta",
|
||||||
|
"wiraswasta usaha",
|
||||||
|
"usaha sendiri",
|
||||||
|
"sendiri",
|
||||||
|
"usaha",
|
||||||
|
"toko",
|
||||||
|
"founder",
|
||||||
|
"wiraswasta toko",
|
||||||
|
"jaya",
|
||||||
|
"wiraswasta freelance"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"3": {
|
||||||
|
"jumlah": 198,
|
||||||
|
"persentase": 17.7737881508079,
|
||||||
|
"top_terms": [
|
||||||
|
"swasta",
|
||||||
|
"usaha swasta",
|
||||||
|
"usaha",
|
||||||
|
"staff",
|
||||||
|
"indonesia",
|
||||||
|
"swasta staff",
|
||||||
|
"founder",
|
||||||
|
"swasta founder",
|
||||||
|
"digital",
|
||||||
|
"media"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,32 @@
|
||||||
|
============================================================
|
||||||
|
CLUSTERING SUMMARY
|
||||||
|
Tanggal: 2026-08-28 04:27:42
|
||||||
|
============================================================
|
||||||
|
|
||||||
|
[MODEL INFO]
|
||||||
|
K = 4 (sesuai PRD)
|
||||||
|
Inertia: 893.89
|
||||||
|
Silhouette Score: 0.1034
|
||||||
|
Total Samples: 1114
|
||||||
|
Total Features: 500
|
||||||
|
|
||||||
|
[EVALUATION RESULTS]
|
||||||
|
K=2: Inertia=992.52, Silhouette=0.0595
|
||||||
|
K=3: Inertia=937.51, Silhouette=0.0833
|
||||||
|
K=4: Inertia=893.89, Silhouette=0.1034
|
||||||
|
K=5: Inertia=868.63, Silhouette=0.1151
|
||||||
|
K=6: Inertia=854.48, Silhouette=0.1154
|
||||||
|
|
||||||
|
[CLUSTER DISTRIBUTION]
|
||||||
|
Cluster 0: 587 (52.7%)
|
||||||
|
Cluster 1: 146 (13.1%)
|
||||||
|
Cluster 2: 183 (16.4%)
|
||||||
|
Cluster 3: 198 (17.8%)
|
||||||
|
|
||||||
|
[TOP TERMS PER CLUSTER]
|
||||||
|
Cluster 0: perintah, bumn, bnpp, jember, staff
|
||||||
|
Cluster 1: kerja, freelance, kerja lepas, lepas, freelance kerja
|
||||||
|
Cluster 2: wiraswasta, wiraswasta usaha, usaha sendiri, sendiri, usaha
|
||||||
|
Cluster 3: swasta, usaha swasta, usaha, staff, indonesia
|
||||||
|
|
||||||
|
============================================================
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 90 KiB |
|
|
@ -0,0 +1,350 @@
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Dashboard Clustering Alumni - Politeknik Negeri Jember</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 30px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 28px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header p {
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 25px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-value {
|
||||||
|
font-size: 42px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-c0 { color: #2ecc71; }
|
||||||
|
.card-c1 { color: #3498db; }
|
||||||
|
.card-c2 { color: #f39c12; }
|
||||||
|
.card-c3 { color: #e74c3c; }
|
||||||
|
.card-tif { color: #9b59b6; }
|
||||||
|
.card-mif { color: #1abc9c; }
|
||||||
|
.card-tkk { color: #e67e22; }
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 25px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-wrapper {
|
||||||
|
position: relative;
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.definitions {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 25px;
|
||||||
|
margin-top: 20px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.definitions h2 {
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.definition-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.definition-badge {
|
||||||
|
padding: 8px 15px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
margin-right: 15px;
|
||||||
|
min-width: 50px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-c0 { background: #2ecc71; }
|
||||||
|
.badge-c1 { background: #3498db; }
|
||||||
|
.badge-c2 { background: #f39c12; }
|
||||||
|
.badge-c3 { background: #e74c3c; }
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 30px;
|
||||||
|
color: white;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>Dashboard Clustering Alumni</h1>
|
||||||
|
<p>Sistem Pengelompokan Pekerjaan Alumni dengan Kompetensi Lulusan - Jurusan Teknologi Informasi, Politeknik Negeri Jember</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cards">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-value card-tif">347</div>
|
||||||
|
<div class="card-label">Teknik Informatika (TIF)</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-value card-mif">236</div>
|
||||||
|
<div class="card-label">Manajemen Informatika (MIF)</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-value card-tkk">194</div>
|
||||||
|
<div class="card-label">Teknik Komputer (TKK)</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-value" style="color:#2c3e50">1596</div>
|
||||||
|
<div class="card-label">Total Alumni</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cards">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-value card-c0">223</div>
|
||||||
|
<div class="card-label">Sesuai Profil (C0)</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-value card-c1">385</div>
|
||||||
|
<div class="card-label">Bidang Diketahui (C1)</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-value card-c2">506</div>
|
||||||
|
<div class="card-label">Bidang Tidak Jelas (C2)</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-value card-c3">482</div>
|
||||||
|
<div class="card-label">Belum Bekerja (C3)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<div class="chart-container">
|
||||||
|
<div class="chart-title">Distribusi Kategori Alumni (C0-C3)</div>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="kategoriChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="chart-container">
|
||||||
|
<div class="chart-title">Distribusi per Program Studi</div>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="prodiChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid" style="margin-top:20px">
|
||||||
|
<div class="chart-container">
|
||||||
|
<div class="chart-title">Distribusi Cluster K-Means (K=4)</div>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="clusterChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="chart-container">
|
||||||
|
<div class="chart-title">Match Rate per Program Studi</div>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="matchChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="definitions">
|
||||||
|
<h2>Definisi Kategori</h2>
|
||||||
|
<div class="definition-item">
|
||||||
|
<span class="definition-badge badge-c0">C0</span>
|
||||||
|
<span><strong>Sesuai Profil Lulusan:</strong> Alumni bekerja dan pekerjaan match dengan kamus kata kunci profil lulusannya.</span>
|
||||||
|
</div>
|
||||||
|
<div class="definition-item">
|
||||||
|
<span class="definition-badge badge-c1">C1</span>
|
||||||
|
<span><strong>Bidang Diketahui:</strong> Alumni bekerja, bidang pekerjaan jelas, tapi tidak match kamus prodi.</span>
|
||||||
|
</div>
|
||||||
|
<div class="definition-item">
|
||||||
|
<span class="definition-badge badge-c2">C2</span>
|
||||||
|
<span><strong>Bidang Tidak Teridentifikasi:</strong> Alumni bekerja tapi informasi pekerjaan kurang jelas atau terlalu umum.</span>
|
||||||
|
</div>
|
||||||
|
<div class="definition-item">
|
||||||
|
<span class="definition-badge badge-c3">C3</span>
|
||||||
|
<span><strong>Belum/Tidak Bekerja:</strong> Alumni belum bekerja atau tidak bekerja saat tracer study.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>Generated: 2026-08-28 04:48:41</p>
|
||||||
|
<p>Sistem Clustering Alumni - Politeknik Negeri Jember</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Kategori Chart
|
||||||
|
new Chart(document.getElementById('kategoriChart'), {
|
||||||
|
type: 'doughnut',
|
||||||
|
data: {
|
||||||
|
labels: ['C0 - Sesuai Profil', 'C1 - Bidang Diketahui', 'C2 - Bidang Tidak Jelas', 'C3 - Belum Bekerja'],
|
||||||
|
datasets: [{
|
||||||
|
data: [
|
||||||
|
223,
|
||||||
|
385,
|
||||||
|
506,
|
||||||
|
482
|
||||||
|
],
|
||||||
|
backgroundColor: ['#2ecc71', '#3498db', '#f39c12', '#e74c3c']
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: { position: 'bottom' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prodi Chart
|
||||||
|
new Chart(document.getElementById('prodiChart'), {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: ['TIF', 'MIF', 'TKK'],
|
||||||
|
datasets: [{
|
||||||
|
label: 'Jumlah Alumni',
|
||||||
|
data: [
|
||||||
|
347,
|
||||||
|
236,
|
||||||
|
194
|
||||||
|
],
|
||||||
|
backgroundColor: ['#9b59b6', '#1abc9c', '#e67e22']
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cluster Chart
|
||||||
|
new Chart(document.getElementById('clusterChart'), {
|
||||||
|
type: 'pie',
|
||||||
|
data: {
|
||||||
|
labels: ['Cluster 0', 'Cluster 1', 'Cluster 2', 'Cluster 3'],
|
||||||
|
datasets: [{
|
||||||
|
data: [326, 27, 66, 100],
|
||||||
|
backgroundColor: ['#3498db', '#2ecc71', '#f39c12', '#9b59b6']
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Match Rate Chart
|
||||||
|
new Chart(document.getElementById('matchChart'), {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: ['TIF', 'MIF', 'TKK'],
|
||||||
|
datasets: [{
|
||||||
|
label: 'Match Rate (%)',
|
||||||
|
data: [2.22, 1.82, 6.90],
|
||||||
|
backgroundColor: ['#9b59b6', '#1abc9c', '#e67e22']
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
max: 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,455 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Dashboard Clustering Alumni - Politeknik Negeri Jember</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 40px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 32px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background: linear-gradient(90deg, #00d9ff, #00ff88);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header p {
|
||||||
|
color: #a0a0a0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 25px;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
border-color: rgba(255,255,255,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-value {
|
||||||
|
font-size: 38px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #a0a0a0;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-c0 { color: #2ecc71; }
|
||||||
|
.card-c1 { color: #3498db; }
|
||||||
|
.card-c2 { color: #f39c12; }
|
||||||
|
.card-c3 { color: #e74c3c; }
|
||||||
|
.card-tif { color: #a855f7; }
|
||||||
|
.card-mif { color: #14b8a6; }
|
||||||
|
.card-tkk { color: #f97316; }
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(380px, 1fr));
|
||||||
|
gap: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 30px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-wrapper {
|
||||||
|
position: relative;
|
||||||
|
height: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.definitions {
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 35px;
|
||||||
|
margin-top: 30px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.definitions h2 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 22px;
|
||||||
|
background: linear-gradient(90deg, #00d9ff, #00ff88);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.definition-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.definition-badge {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
margin-right: 20px;
|
||||||
|
min-width: 50px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-c0 { background: #2ecc71; }
|
||||||
|
.badge-c1 { background: #3498db; }
|
||||||
|
.badge-c2 { background: #f39c12; }
|
||||||
|
.badge-c3 { background: #e74c3c; }
|
||||||
|
|
||||||
|
.definition-text {
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.definition-text strong {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-box {
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #00d9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #a0a0a0;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 40px;
|
||||||
|
padding: 20px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.header h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>Sistem Clustering Alumni</h1>
|
||||||
|
<p>Jurusan Teknologi Informasi - Politeknik Negeri Jember</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cards">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-value card-tif">225</div>
|
||||||
|
<div class="card-label">Teknik Informatika</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-value card-mif">165</div>
|
||||||
|
<div class="card-label">Manajemen Informatika</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-value card-tkk">145</div>
|
||||||
|
<div class="card-label">Teknik Komputer</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-value" style="color:#fff">535</div>
|
||||||
|
<div class="card-label">Total Alumni</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cards">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-value card-c0">18</div>
|
||||||
|
<div class="card-label">C0 - Sesuai Profil</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-value card-c1">85</div>
|
||||||
|
<div class="card-label">C1 - Bidang Diketahui</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-value card-c2">416</div>
|
||||||
|
<div class="card-label">C2 - Bidang Tidak Jelas</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-value card-c3">16</div>
|
||||||
|
<div class="card-label">C3 - Belum Bekerja</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-row">
|
||||||
|
<div class="stat-box">
|
||||||
|
<div class="stat-number">519</div>
|
||||||
|
<div class="stat-label">Data di-Cluster</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-box">
|
||||||
|
<div class="stat-number">4</div>
|
||||||
|
<div class="stat-label">Jumlah Cluster (K)</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-box">
|
||||||
|
<div class="stat-number">0.16</div>
|
||||||
|
<div class="stat-label">Silhouette Score</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-box">
|
||||||
|
<div class="stat-number">3.36%</div>
|
||||||
|
<div class="stat-label">Match Rate C0</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid" style="margin-top:30px">
|
||||||
|
<div class="chart-container">
|
||||||
|
<div class="chart-title">Distribusi Kategori Alumni (C0-C3)</div>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="kategoriChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="chart-container">
|
||||||
|
<div class="chart-title">Distribusi per Program Studi</div>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="prodiChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid" style="margin-top:25px">
|
||||||
|
<div class="chart-container">
|
||||||
|
<div class="chart-title">Distribusi Cluster K-Means (K=4)</div>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="clusterChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="chart-container">
|
||||||
|
<div class="chart-title">Distribusi C0-C3 per Prodi</div>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="prodiKategoriChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="definitions">
|
||||||
|
<h2>Definisi Kategori C0-C3</h2>
|
||||||
|
<div class="definition-item">
|
||||||
|
<span class="definition-badge badge-c0">C0</span>
|
||||||
|
<span class="definition-text"><strong>Sesuai Profil Lulusan:</strong> Alumni bekerja dan pekerjaan memiliki kecocokan dengan kamus kata kunci profil lulusannya berdasarkan prodi asal.</span>
|
||||||
|
</div>
|
||||||
|
<div class="definition-item">
|
||||||
|
<span class="definition-badge badge-c1">C1</span>
|
||||||
|
<span class="definition-text"><strong>Bidang Diketahui:</strong> Alumni bekerja dan bidang pekerjaan dapat diidentifikasi dengan jelas, tetapi tidak match dengan kamus prodi.</span>
|
||||||
|
</div>
|
||||||
|
<div class="definition-item">
|
||||||
|
<span class="definition-badge badge-c2">C2</span>
|
||||||
|
<span class="definition-text"><strong>Bidang Tidak Teridentifikasi:</strong> Alumni bekerja tetapi informasi pekerjaan tidak cukup jelas untuk menentukan bidang pekerjaan.</span>
|
||||||
|
</div>
|
||||||
|
<div class="definition-item">
|
||||||
|
<span class="definition-badge badge-c3">C3</span>
|
||||||
|
<span class="definition-text"><strong>Belum/Tidak Bekerja:</strong> Alumni belum bekerja atau berstatus tidak bekerja saat tracer study.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>Sistem Pengelompokan Pekerjaan Alumni dengan Kompetensi Lulusan</p>
|
||||||
|
<p>Generated: 2026-08-28</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Kategori Chart
|
||||||
|
new Chart(document.getElementById('kategoriChart'), {
|
||||||
|
type: 'doughnut',
|
||||||
|
data: {
|
||||||
|
labels: ['C0 - Sesuai Profil', 'C1 - Bidang Diketahui', 'C2 - Bidang Tidak Jelas', 'C3 - Belum Bekerja'],
|
||||||
|
datasets: [{
|
||||||
|
data: [18, 85, 416, 16],
|
||||||
|
backgroundColor: ['#2ecc71', '#3498db', '#f39c12', '#e74c3c'],
|
||||||
|
borderWidth: 0
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: 'bottom',
|
||||||
|
labels: { color: '#fff', padding: 15 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prodi Chart
|
||||||
|
new Chart(document.getElementById('prodiChart'), {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: ['TIF', 'MIF', 'TKK'],
|
||||||
|
datasets: [{
|
||||||
|
label: 'Jumlah Alumni',
|
||||||
|
data: [225, 165, 145],
|
||||||
|
backgroundColor: ['#a855f7', '#14b8a6', '#f97316'],
|
||||||
|
borderRadius: 8
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false }
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
grid: { color: 'rgba(255,255,255,0.1)' },
|
||||||
|
ticks: { color: '#fff' }
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
grid: { display: false },
|
||||||
|
ticks: { color: '#fff' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cluster Chart
|
||||||
|
new Chart(document.getElementById('clusterChart'), {
|
||||||
|
type: 'pie',
|
||||||
|
data: {
|
||||||
|
labels: ['Cluster 0 (62.8%)', 'Cluster 1 (5.2%)', 'Cluster 2 (12.7%)', 'Cluster 3 (19.3%)'],
|
||||||
|
datasets: [{
|
||||||
|
data: [326, 27, 66, 100],
|
||||||
|
backgroundColor: ['#3498db', '#2ecc71', '#f39c12', '#9b59b6'],
|
||||||
|
borderWidth: 0
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: 'bottom',
|
||||||
|
labels: { color: '#fff', padding: 10 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prodi Kategori Chart
|
||||||
|
new Chart(document.getElementById('prodiKategoriChart'), {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: ['TIF', 'MIF', 'TKK'],
|
||||||
|
datasets: [
|
||||||
|
{ label: 'C0', data: [5, 3, 10], backgroundColor: '#2ecc71' },
|
||||||
|
{ label: 'C1', data: [53, 20, 12], backgroundColor: '#3498db' },
|
||||||
|
{ label: 'C2', data: [162, 134, 120], backgroundColor: '#f39c12' },
|
||||||
|
{ label: 'C3', data: [5, 8, 3], backgroundColor: '#e74c3c' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: 'bottom',
|
||||||
|
labels: { color: '#fff', padding: 10 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
stacked: true,
|
||||||
|
grid: { color: 'rgba(255,255,255,0.1)' },
|
||||||
|
ticks: { color: '#fff' }
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
stacked: true,
|
||||||
|
grid: { display: false },
|
||||||
|
ticks: { color: '#fff' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 98 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
|
|
@ -0,0 +1,483 @@
|
||||||
|
kode_prodi,nim,nama,tahun_lulus,status_pekerjaan,jenis_perusahaan,nama_perusahaan,jabatan,sumber_file,teks_pekerjaan_raw,teks_pekerjaan,kategori_pengalaman
|
||||||
|
57401,E31160678,RIZKA NURUL KHAKIKI,2019.0,bekerja,3,,,Data_2019_Tracer_study.csv,swasta,swasta,tidak_berpengalaman
|
||||||
|
55301,E41150211,DEWI AYU ITIKAF,2019.0,mencari_kerja,0,0,0.0,Data_2019_Tracer_study.csv,0 0.0,,tidak_berpengalaman
|
||||||
|
57401,E31151401,Aza Pascaeka,2019.0,belum_kerja,4,,,Data_2019_Tracer_study.csv,wiraswasta,wiraswasta,tidak_berpengalaman
|
||||||
|
57401,E31130845,MOCH. RIVALDI AFTONI,2019.0,bekerja,5,,,Data_2019_Tracer_study.csv,lainnya,lain,tidak_berpengalaman
|
||||||
|
57401,E31160328,Afega Wira Pradana,2019.0,bekerja,3,,,Data_2019_Tracer_study.csv,swasta,swasta,tidak_berpengalaman
|
||||||
|
57401,E31161719,Reza Septia Safirah,2019.0,belum_kerja,4,,,Data_2019_Tracer_study.csv,wiraswasta,wiraswasta,tidak_berpengalaman
|
||||||
|
56401,E32160904,ONGKY OKTAFIAN DWI JOHANSYAH,2019.0,bekerja,3,,,Data_2019_Tracer_study.csv,swasta,swasta,tidak_berpengalaman
|
||||||
|
55301,E41150068,MUHAMMAD HAFIDH FIRMANSYAH,2019.0,bekerja,5,,,Data_2019_Tracer_study.csv,lainnya,lain,tidak_berpengalaman
|
||||||
|
55301,E41151584,Risqi Ahmad Sultoni,2019.0,bekerja,3,,,Data_2019_Tracer_study.csv,swasta,swasta,tidak_berpengalaman
|
||||||
|
57401,E31151214,Nita Wahyu Lianti,2019.0,belum_kerja,4,,,Data_2019_Tracer_study.csv,wiraswasta,wiraswasta,tidak_berpengalaman
|
||||||
|
57401,E31160306,Faiqotul Hikmah,2019.0,bekerja,3,,,Data_2019_Tracer_study.csv,swasta,swasta,tidak_berpengalaman
|
||||||
|
57401,E31160852,MOHAMMAD IMRAN,2020.0,mencari_kerja,0,0,0.0,Data_2020_Tracer_study.csv,0 0.0,,tidak_berpengalaman
|
||||||
|
57401,E31171030,Nur Intan Nida,2020.0,mencari_kerja,0,0,0.0,Data_2020_Tracer_study.csv,0 0.0,,tidak_berpengalaman
|
||||||
|
56401,E32170720,Sisca Nurmala,2020.0,mencari_kerja,0,0,0.0,Data_2020_Tracer_study.csv,0 0.0,,tidak_berpengalaman
|
||||||
|
56401,E32170564,M Ibnu Wildan,2020.0,mencari_kerja,4,Seblak Preanger,3.0,Data_2020_Tracer_study.csv,wiraswasta Seblak Preanger 3.0,wiraswasta seblak preanger,tidak_berpengalaman
|
||||||
|
57401,E31170717,M. Fuat,2020.0,mencari_kerja,0,0,0.0,Data_2020_Tracer_study.csv,0 0.0,,tidak_berpengalaman
|
||||||
|
56401,E32162050,Akhmad Rizky Sangaji,2020.0,belum_kerja,4,,,Data_2020_Tracer_study.csv,wiraswasta,wiraswasta,tidak_berpengalaman
|
||||||
|
56401,E32161727,Moch Samsy,2020.0,belum_kerja,4,,,Data_2020_Tracer_study.csv,wiraswasta,wiraswasta,tidak_berpengalaman
|
||||||
|
55301,E41190053,DIAN WAHYUNI,2020.0,bekerja,1,0,3.0,Data_2020_Tracer_study.csv,pemerintah 0 3.0,perintah,tidak_berpengalaman
|
||||||
|
57401,E31171653,Ahmad Faqih Hamdani,2020.0,bekerja,7,0,3.0,Data_2020_Tracer_study.csv,nirlaba 0 3.0,nirlaba,tidak_berpengalaman
|
||||||
|
55301,E41170333 ,CICILIA SELVY ERIANTHY,2021.0,wiraswasta,4,tidak ada,1.0,Data_2021_Tracer_study.csv,wiraswasta tidak ada 1.0,wiraswasta,tidak_berpengalaman
|
||||||
|
57401,E31182203 ,Rofi Berliana Mahanani,2021.0,melanjutkan_studi,3,-,,Data_2021_Tracer_study.csv,swasta -,swasta,tidak_berpengalaman
|
||||||
|
56401,E32180554 ,Akhmad Farizi,2021.0,melanjutkan_studi,3,tidak ada,4.0,Data_2021_Tracer_study.csv,swasta tidak ada 4.0,swasta,tidak_berpengalaman
|
||||||
|
55301,E41170562 ,KRISOPRAS EPIKUROS,2021.0,wiraswasta,3,PT.,4.0,Data_2021_Tracer_study.csv,swasta PT. 4.0,swasta,tidak_berpengalaman
|
||||||
|
56401,E32182061 ,OZSA DEWANGGA PRADANA,2021.0,melanjutkan_studi,2,-,,Data_2021_Tracer_study.csv,bnpp -,bnpp,tidak_berpengalaman
|
||||||
|
57401,E31200908,ROBBY ASY'ARI,2022.0,wiraswasta,4,,3.0,Data_2022_Tracer_study.csv,wiraswasta 3.0,wiraswasta,tidak_berpengalaman
|
||||||
|
57401,E31191006,AGUS ARDIANSYAH,2022.0,bekerja,4,PT PN,4.0,Data_2022_Tracer_study.csv,wiraswasta PT PN 4.0,wiraswasta,tidak_berpengalaman
|
||||||
|
56401,E32191199,RIVAI NUR ROHMAN,2022.0,wiraswasta,6,,3.0,Data_2022_Tracer_study.csv,bumn 3.0,bumn,tidak_berpengalaman
|
||||||
|
57401,E31192382,AINUR KARINA ABI RAHMADIYAN,2022.0,wiraswasta,3,,3.0,Data_2022_Tracer_study.csv,swasta 3.0,swasta,tidak_berpengalaman
|
||||||
|
57401,E31191195,NOVIEKA MUTIARA FIRDAUSIAH,2022.0,wiraswasta,4,,1.0,Data_2022_Tracer_study.csv,wiraswasta 1.0,wiraswasta,tidak_berpengalaman
|
||||||
|
56401,E32191236,DAFFA PUTRA JAYA PERMANA,2022.0,wiraswasta,4,,1.0,Data_2022_Tracer_study.csv,wiraswasta 1.0,wiraswasta,tidak_berpengalaman
|
||||||
|
55301,E41182080,MUHAMMAD ILMI QOLBI IMANNI,2022.0,wiraswasta,7,,4.0,Data_2022_Tracer_study.csv,nirlaba 4.0,nirlaba,tidak_berpengalaman
|
||||||
|
55301,E41201788,ARIF RAHMAN HAKIM,2022.0,wiraswasta,3,,3.0,Data_2022_Tracer_study.csv,swasta 3.0,swasta,tidak_berpengalaman
|
||||||
|
55301,E41200889,AHMAD FAQIH ARDINATA,2022.0,wiraswasta,3,,4.0,Data_2022_Tracer_study.csv,swasta 4.0,swasta,tidak_berpengalaman
|
||||||
|
55301,E41180632,FRENGKI ELISTANTO,2022.0,wiraswasta,1,,4.0,Data_2022_Tracer_study.csv,pemerintah 4.0,perintah,tidak_berpengalaman
|
||||||
|
57401,E31192117,AULIA DININ PUTRI ALIFIAH,2022.0,melanjutkan_studi,1,,4.0,Data_2022_Tracer_study.csv,pemerintah 4.0,perintah,tidak_berpengalaman
|
||||||
|
57401,E31191120,SHINDI FATIKA SARI,2022.0,melanjutkan_studi,3,,4.0,Data_2022_Tracer_study.csv,swasta 4.0,swasta,tidak_berpengalaman
|
||||||
|
56401,E32190383,GALANG PAHALA,2022.0,melanjutkan_studi,3,,3.0,Data_2022_Tracer_study.csv,swasta 3.0,swasta,tidak_berpengalaman
|
||||||
|
57401,E31191849,Silfi Gabrilia Choirun Nisa,2022.0,melanjutkan_studi,4,,2.0,Data_2022_Tracer_study.csv,wiraswasta 2.0,wiraswasta,tidak_berpengalaman
|
||||||
|
56401,E32190887,Amirul Faisal,2022.0,melanjutkan_studi,3,,4.0,Data_2022_Tracer_study.csv,swasta 4.0,swasta,tidak_berpengalaman
|
||||||
|
55301,E41171991,FEBRERO ARAYA KUSUMATMAJA,2022.0,wiraswasta,7,,4.0,Data_2022_Tracer_study.csv,nirlaba 4.0,nirlaba,tidak_berpengalaman
|
||||||
|
57401,E31191300,IWANG PRADIPTA DINOTO,2022.0,melanjutkan_studi,6,,3.0,Data_2022_Tracer_study.csv,bumn 3.0,bumn,tidak_berpengalaman
|
||||||
|
57401,E31191982,GALUH LAKSITA NARASWARI,2022.0,melanjutkan_studi,6,,3.0,Data_2022_Tracer_study.csv,bumn 3.0,bumn,tidak_berpengalaman
|
||||||
|
57401,E31191433,Muhammad Naufal Zuhdi,2022.0,melanjutkan_studi,6,,1.0,Data_2022_Tracer_study.csv,bumn 1.0,bumn,tidak_berpengalaman
|
||||||
|
56401,E32191840,Wahyu Musthofa,2022.0,melanjutkan_studi,1,,2.0,Data_2022_Tracer_study.csv,pemerintah 2.0,perintah,tidak_berpengalaman
|
||||||
|
55301,E41161215,HENDRA LAKSMANA,2022.0,melanjutkan_studi,6,,1.0,Data_2022_Tracer_study.csv,bumn 1.0,bumn,tidak_berpengalaman
|
||||||
|
55301,E41182248,ABDUR RAZAK SEBASTIAN,2022.0,melanjutkan_studi,7,,4.0,Data_2022_Tracer_study.csv,nirlaba 4.0,nirlaba,tidak_berpengalaman
|
||||||
|
55301,E41181028,AISHA RAHMAYANTI,2022.0,melanjutkan_studi,4,,3.0,Data_2022_Tracer_study.csv,wiraswasta 3.0,wiraswasta,tidak_berpengalaman
|
||||||
|
55301,E41182048,MUHAMMAD ANSORI,2022.0,melanjutkan_studi,4,,4.0,Data_2022_Tracer_study.csv,wiraswasta 4.0,wiraswasta,tidak_berpengalaman
|
||||||
|
55301,E41212442,AFDUL ANWAR ADANI,2022.0,melanjutkan_studi,3,,4.0,Data_2022_Tracer_study.csv,swasta 4.0,swasta,tidak_berpengalaman
|
||||||
|
55301,E41212449,RIZAL WAHYU UTAMA,2022.0,melanjutkan_studi,4,,4.0,Data_2022_Tracer_study.csv,wiraswasta 4.0,wiraswasta,tidak_berpengalaman
|
||||||
|
55301,E41212470,RIZQIAH DWI AGUSTIN,2022.0,melanjutkan_studi,3,,3.0,Data_2022_Tracer_study.csv,swasta 3.0,swasta,tidak_berpengalaman
|
||||||
|
57401,E31192264,ALVIN YUDHA EKA PRADISTYA,2022.0,melanjutkan_studi,6,,2.0,Data_2022_Tracer_study.csv,bumn 2.0,bumn,tidak_berpengalaman
|
||||||
|
55301,E41171763,TABHRANY ODI ASMORO,2022.0,melanjutkan_studi,,,,Data_2022_Tracer_study.csv,,,tidak_berpengalaman
|
||||||
|
55301,E41181135,Harisman Siihombing,2022.0,bekerja,3,-,3.0,Data_2022_Tracer_study.csv,swasta - 3.0,swasta,tidak_berpengalaman
|
||||||
|
55301,E41180749,MUHAMMAD HANAFI ABROR,2022.0,melanjutkan_studi,,,,Data_2022_Tracer_study.csv,,,tidak_berpengalaman
|
||||||
|
57401,E31191250,MUHAMMAD PUTRA ADIKUSUMA,2022.0,bekerja,3,,3.0,Data_2022_Tracer_study.csv,swasta 3.0,swasta,tidak_berpengalaman
|
||||||
|
55301,E41181773,MUHAMMAD FARHAN NUR PRATAMA,2022.0,melanjutkan_studi,,,,Data_2022_Tracer_study.csv,,,tidak_berpengalaman
|
||||||
|
55301,E41180879,YUDISTIONO,2022.0,wiraswasta,,,,Data_2022_Tracer_study.csv,,,tidak_berpengalaman
|
||||||
|
55301,E41212443,GALUH MAHENDRA,2022.0,wiraswasta,,,4.0,Data_2022_Tracer_study.csv,4.0,,tidak_berpengalaman
|
||||||
|
57401,E31192064,LUSY DWI WILUJENG,2022.0,wiraswasta,,,,Data_2022_Tracer_study.csv,,,tidak_berpengalaman
|
||||||
|
Manajemen Informatika (57401),E31191738,ABDULLAH SYIHABUDIN AL ACHYAR,2024.0,belum_kerja,"5 - Lainnya, Tuliskan",,3 - Staff,Data_2024_Tracer_study.csv,"5 - Lainnya, Tuliskan 3 - Staff",lain tulis staff,tidak_berpengalaman
|
||||||
|
Teknik Informatika (55301),E41201747,M. YOGA ANDRIAN PUTRA,2024.0,belum_kerja,Belum Bekerja,,4 - Freelance/Kerja Lepas,Data_2024_Tracer_study.csv,Belum Bekerja 4 - Freelance/Kerja Lepas,belum kerja freelance kerja lepas,tidak_berpengalaman
|
||||||
|
Manajemen Informatika (57401),E31210257,MUHAMMAD ZIDAN PRASETYO,2024.0,belum_kerja,1 - Instansi pemerintah,,3 - Staff,Data_2024_Tracer_study.csv,1 - Instansi pemerintah 3 - Staff,instansi perintah staff,tidak_berpengalaman
|
||||||
|
Manajemen Informatika (57401),E31211910,DAVIN FADHLILLAH YUDANTO,2024.0,belum_kerja,3 - Perusahaan swasta,,4 - Freelance/Kerja Lepas,Data_2024_Tracer_study.csv,3 - Perusahaan swasta 4 - Freelance/Kerja Lepas,usaha swasta freelance kerja lepas,tidak_berpengalaman
|
||||||
|
Teknik Informatika (55301),E41201781,WIDYA OCTAVIOLA NURYA,2024.0,belum_kerja,6 - BUMN/BUMD,,2 - Co-Founder,Data_2024_Tracer_study.csv,6 - BUMN/BUMD 2 - Co-Founder,bumn bumd founder,tidak_berpengalaman
|
||||||
|
Manajemen Informatika (57401),E31211936,RAHMANDA RICO LUTFIANSYAH,2024.0,belum_kerja,7 - Institusi/Organisasi Multilateral,,1 - Founder,Data_2024_Tracer_study.csv,7 - Institusi/Organisasi Multilateral 1 - Founder,institusi organisasi multilateral founder,tidak_berpengalaman
|
||||||
|
Teknik Informatika (55301),E41202091,NUR HASANAH,2024.0,belum_kerja,6 - BUMN/BUMD,,1 - Founder,Data_2024_Tracer_study.csv,6 - BUMN/BUMD 1 - Founder,bumn bumd founder,tidak_berpengalaman
|
||||||
|
Teknik Informatika (55301),E41200569,NURILLAHILWAFI,2024.0,belum_kerja,"5 - Lainnya, Tuliskan",,1 - Founder,Data_2024_Tracer_study.csv,"5 - Lainnya, Tuliskan 1 - Founder",lain tulis founder,tidak_berpengalaman
|
||||||
|
Manajemen Informatika (57401),E31211906,STEVEN RIO CHANDRA PURNAMA,2024.0,belum_kerja,"5 - Lainnya, Tuliskan",,4 - Freelance/Kerja Lepas,Data_2024_Tracer_study.csv,"5 - Lainnya, Tuliskan 4 - Freelance/Kerja Lepas",lain tulis freelance kerja lepas,tidak_berpengalaman
|
||||||
|
Teknik Informatika (55301),E41202429,AZRIL NUR RAHMAN,2024.0,belum_kerja,3 - Perusahaan swasta,,4 - Freelance/Kerja Lepas,Data_2024_Tracer_study.csv,3 - Perusahaan swasta 4 - Freelance/Kerja Lepas,usaha swasta freelance kerja lepas,tidak_berpengalaman
|
||||||
|
Manajemen Informatika (57401),E31210717,FARA ISTIGHFAR SARI,2024.0,belum_kerja,belum bekerja,,4 - Freelance/Kerja Lepas,Data_2024_Tracer_study.csv,belum bekerja 4 - Freelance/Kerja Lepas,belum kerja freelance kerja lepas,tidak_berpengalaman
|
||||||
|
Teknik Informatika (55301),E41202281,BAHRUL AJI SANTOSO,2024.0,belum_kerja,4 - Wiraswasta/perusahaan sendiri,,1 - Founder,Data_2024_Tracer_study.csv,4 - Wiraswasta/perusahaan sendiri 1 - Founder,wiraswasta usaha sendiri founder,tidak_berpengalaman
|
||||||
|
Manajemen Informatika (57401),E31211900,MARSHA ARLINDA PUTRI,2024.0,belum_kerja,3 - Perusahaan swasta,,4 - Freelance/Kerja Lepas,Data_2024_Tracer_study.csv,3 - Perusahaan swasta 4 - Freelance/Kerja Lepas,usaha swasta freelance kerja lepas,tidak_berpengalaman
|
||||||
|
Manajemen Informatika (57401),E31211896,REIHAN JANUWISESA FATIH ATTARIANSYAH,2024.0,belum_kerja,1 - Instansi pemerintah,,3 - Staff,Data_2024_Tracer_study.csv,1 - Instansi pemerintah 3 - Staff,instansi perintah staff,tidak_berpengalaman
|
||||||
|
Manajemen Informatika (57401),E31210592,YUYUN INE FEBRIANI,2024.0,belum_kerja,"5 - Lainnya, Tuliskan",,4 - Freelance/Kerja Lepas,Data_2024_Tracer_study.csv,"5 - Lainnya, Tuliskan 4 - Freelance/Kerja Lepas",lain tulis freelance kerja lepas,tidak_berpengalaman
|
||||||
|
Teknik Informatika (55301),E41201153,BIMA ANGGARA WIRASATYA,2024.0,belum_kerja,3 - Perusahaan swasta,,4 - Freelance/Kerja Lepas,Data_2024_Tracer_study.csv,3 - Perusahaan swasta 4 - Freelance/Kerja Lepas,usaha swasta freelance kerja lepas,tidak_berpengalaman
|
||||||
|
Manajemen Informatika (57401),E31210385,ELA ERWITA SISILIA,2024.0,belum_kerja,6 - BUMN/BUMD,,3 - Staff,Data_2024_Tracer_study.csv,6 - BUMN/BUMD 3 - Staff,bumn bumd staff,tidak_berpengalaman
|
||||||
|
Teknik Informatika (55301),E41201302,FAHMI DAFRIN MAULANA,2024.0,belum_kerja,4 - Wiraswasta/perusahaan sendiri,,4 - Freelance/Kerja Lepas,Data_2024_Tracer_study.csv,4 - Wiraswasta/perusahaan sendiri 4 - Freelance/Kerja Lepas,wiraswasta usaha sendiri freelance kerja lepas,tidak_berpengalaman
|
||||||
|
Teknik Informatika (55301),E41202383,ARIEF ANASTIAR SUHARSONO,2024.0,belum_kerja,6 - BUMN/BUMD,,1 - Founder,Data_2024_Tracer_study.csv,6 - BUMN/BUMD 1 - Founder,bumn bumd founder,tidak_berpengalaman
|
||||||
|
Teknik Komputer (56401),E32211535,MOHAMMAD IRNANDA,2024.0,belum_kerja,Belum Bekerja,,1 - Founder,Data_2024_Tracer_study.csv,Belum Bekerja 1 - Founder,belum kerja founder,tidak_berpengalaman
|
||||||
|
Teknik Informatika (55301),E41202280,ASIF MAHARDHIKA RAMADAN,2024.0,belum_kerja,4 - Wiraswasta/perusahaan sendiri,,4 - Freelance/Kerja Lepas,Data_2024_Tracer_study.csv,4 - Wiraswasta/perusahaan sendiri 4 - Freelance/Kerja Lepas,wiraswasta usaha sendiri freelance kerja lepas,tidak_berpengalaman
|
||||||
|
Teknik Informatika (55301),E41202254,NIKI MUHAMMAD NAFIS,2024.0,belum_kerja,Belum Bekerja,,4 - Freelance/Kerja Lepas,Data_2024_Tracer_study.csv,Belum Bekerja 4 - Freelance/Kerja Lepas,belum kerja freelance kerja lepas,tidak_berpengalaman
|
||||||
|
Manajemen Informatika (57401),E31212365,BAGUS DAFFA FIRMANSYAH,2024.0,belum_kerja,6 - BUMN/BUMD,,4 - Freelance/Kerja Lepas,Data_2024_Tracer_study.csv,6 - BUMN/BUMD 4 - Freelance/Kerja Lepas,bumn bumd freelance kerja lepas,tidak_berpengalaman
|
||||||
|
Manajemen Informatika (57401),E31212295,DAFFA ATHALA ZAGHAZY,2024.0,belum_kerja,3 - Perusahaan swasta,,2 - Co-Founder,Data_2024_Tracer_study.csv,3 - Perusahaan swasta 2 - Co-Founder,usaha swasta founder,tidak_berpengalaman
|
||||||
|
Teknik Komputer (56401),E32210989,NARARGA MAULANA,2024.0,belum_kerja,3 - Perusahaan swasta,,3 - Staff,Data_2024_Tracer_study.csv,3 - Perusahaan swasta 3 - Staff,usaha swasta staff,tidak_berpengalaman
|
||||||
|
Teknik Komputer (56401),E32210755,SAHRUL GUNAWAN,2024.0,belum_kerja,3 - Perusahaan swasta,,1 - Founder,Data_2024_Tracer_study.csv,3 - Perusahaan swasta 1 - Founder,usaha swasta founder,tidak_berpengalaman
|
||||||
|
Teknik Informatika (55301),E41201212,MUHAMMAD AKBAR RAYYAN AL-FATH,2024.0,belum_kerja,4 - Wiraswasta/perusahaan sendiri,,4 - Freelance/Kerja Lepas,Data_2024_Tracer_study.csv,4 - Wiraswasta/perusahaan sendiri 4 - Freelance/Kerja Lepas,wiraswasta usaha sendiri freelance kerja lepas,tidak_berpengalaman
|
||||||
|
Teknik Komputer (56401),E32210897,MUFIDAH EKA ALVA NADYA,2024.0,belum_kerja,6 - BUMN/BUMD,Bumn,1 - Founder,Data_2024_Tracer_study.csv,6 - BUMN/BUMD Bumn 1 - Founder,bumn bumd bumn founder,tidak_berpengalaman
|
||||||
|
Teknik Informatika (55301),E41202411,LUQMAN HAKIM,2024.0,belum_kerja,3 - Perusahaan swasta,,1 - Founder,Data_2024_Tracer_study.csv,3 - Perusahaan swasta 1 - Founder,usaha swasta founder,tidak_berpengalaman
|
||||||
|
Teknik Informatika (55301),E41200415,NOGA MUKTIWATI,2024.0,belum_kerja,6 - BUMN/BUMD,,4 - Freelance/Kerja Lepas,Data_2024_Tracer_study.csv,6 - BUMN/BUMD 4 - Freelance/Kerja Lepas,bumn bumd freelance kerja lepas,tidak_berpengalaman
|
||||||
|
Manajemen Informatika (57401),E31212340,AHMAD SAIFUL SEPTIAWAN,2024.0,belum_kerja,4 - Wiraswasta/perusahaan sendiri,,1 - Founder,Data_2024_Tracer_study.csv,4 - Wiraswasta/perusahaan sendiri 1 - Founder,wiraswasta usaha sendiri founder,tidak_berpengalaman
|
||||||
|
Teknik Komputer (56401),E32210743,GUSTI ARLIZ NANDITO,2024.0,belum_kerja,"5 - Lainnya, Tuliskan",,4 - Freelance/Kerja Lepas,Data_2024_Tracer_study.csv,"5 - Lainnya, Tuliskan 4 - Freelance/Kerja Lepas",lain tulis freelance kerja lepas,tidak_berpengalaman
|
||||||
|
Teknik Komputer (56401),E32211375,NELSEN ARDIANSAH,2024.0,belum_kerja,Belum bekerja,,4 - Freelance/Kerja Lepas,Data_2024_Tracer_study.csv,Belum bekerja 4 - Freelance/Kerja Lepas,belum kerja freelance kerja lepas,tidak_berpengalaman
|
||||||
|
Teknik Komputer (56401),E32210111,DANY FERDYANSYAH,2024.0,belum_kerja,2 - Organisasi non-profit/Lembaga Swadaya Masyarakat,,4 - Freelance/Kerja Lepas,Data_2024_Tracer_study.csv,2 - Organisasi non-profit/Lembaga Swadaya Masyarakat 4 - Freelance/Kerja Lepas,organisasi non profit lembaga swadaya masyarakat freelance kerja lepas,tidak_berpengalaman
|
||||||
|
Teknik Informatika (55301),E41202285,ALI IMRON SAEFULLAH,2024.0,belum_kerja,3 - Perusahaan swasta,,4 - Freelance/Kerja Lepas,Data_2024_Tracer_study.csv,3 - Perusahaan swasta 4 - Freelance/Kerja Lepas,usaha swasta freelance kerja lepas,tidak_berpengalaman
|
||||||
|
Teknik Komputer (56401),E32210675,SYLVA ADITYA ARFIANSYAH,2024.0,belum_kerja,"5 - Lainnya, Tuliskan",,3 - Staff,Data_2024_Tracer_study.csv,"5 - Lainnya, Tuliskan 3 - Staff",lain tulis staff,tidak_berpengalaman
|
||||||
|
Teknik Komputer (56401),E32211395,LUTFI ARDIANSYAH,2024.0,belum_kerja,4 - Wiraswasta/perusahaan sendiri,,4 - Freelance/Kerja Lepas,Data_2024_Tracer_study.csv,4 - Wiraswasta/perusahaan sendiri 4 - Freelance/Kerja Lepas,wiraswasta usaha sendiri freelance kerja lepas,tidak_berpengalaman
|
||||||
|
Teknik Komputer (56401),E32210736,MUHAMMAD NAUFAL AFFAN SAMUDRA,2024.0,belum_kerja,"5 - Lainnya, Tuliskan",,4 - Freelance/Kerja Lepas,Data_2024_Tracer_study.csv,"5 - Lainnya, Tuliskan 4 - Freelance/Kerja Lepas",lain tulis freelance kerja lepas,tidak_berpengalaman
|
||||||
|
Manajemen Informatika (57401),E31211386,REFYAN GIGAS ZIDANE HERDINAND PRATAMA,2024.0,belum_kerja,"5 - Lainnya, Tuliskan",,4 - Freelance/Kerja Lepas,Data_2024_Tracer_study.csv,"5 - Lainnya, Tuliskan 4 - Freelance/Kerja Lepas",lain tulis freelance kerja lepas,tidak_berpengalaman
|
||||||
|
Teknik Komputer (56401),E32210813,MUTYARA PERTIWI,2024.0,belum_kerja,"5 - Lainnya, Tuliskan",,4 - Freelance/Kerja Lepas,Data_2024_Tracer_study.csv,"5 - Lainnya, Tuliskan 4 - Freelance/Kerja Lepas",lain tulis freelance kerja lepas,tidak_berpengalaman
|
||||||
|
Teknik Informatika (55301),E41202298,FIRDAUS FIRMANSYAH,2024.0,belum_kerja,3 - Perusahaan swasta,,4 - Freelance/Kerja Lepas,Data_2024_Tracer_study.csv,3 - Perusahaan swasta 4 - Freelance/Kerja Lepas,usaha swasta freelance kerja lepas,tidak_berpengalaman
|
||||||
|
Teknik Informatika (55301),E41200363,ALFIAN EZZA RADITYA,2024.0,belum_kerja,6 - BUMN/BUMD,,4 - Freelance/Kerja Lepas,Data_2024_Tracer_study.csv,6 - BUMN/BUMD 4 - Freelance/Kerja Lepas,bumn bumd freelance kerja lepas,tidak_berpengalaman
|
||||||
|
Teknik Informatika (55301),E32211505,MUHAMMAD AMAN AHCYAD,2024.0,belum_kerja,Belum bekerja,,4 - Freelance/Kerja Lepas,Data_2024_Tracer_study.csv,Belum bekerja 4 - Freelance/Kerja Lepas,belum kerja freelance kerja lepas,tidak_berpengalaman
|
||||||
|
Teknik Komputer (56401),E32212364,RIDHO DWICAHYA WIJAYA,2024.0,belum_kerja,Tidak Bekerja,,4 - Freelance/Kerja Lepas,Data_2024_Tracer_study.csv,Tidak Bekerja 4 - Freelance/Kerja Lepas,kerja freelance kerja lepas,tidak_berpengalaman
|
||||||
|
Teknik Komputer (56401),E32210095,DAMAR RAWUH SARJITO,2024.0,belum_kerja,1 - Instansi pemerintah,,3 - Staff,Data_2024_Tracer_study.csv,1 - Instansi pemerintah 3 - Staff,instansi perintah staff,tidak_berpengalaman
|
||||||
|
Teknik Komputer (56401),E32210053,DIMAS DWI APRILIYANTO,2024.0,belum_kerja,1 - Instansi pemerintah,,3 - Staff,Data_2024_Tracer_study.csv,1 - Instansi pemerintah 3 - Staff,instansi perintah staff,tidak_berpengalaman
|
||||||
|
Teknik Komputer (56401),E32210055,RACHMAN TIO MAULANA,2024.0,belum_kerja,1 - Instansi pemerintah,,3 - Staff,Data_2024_Tracer_study.csv,1 - Instansi pemerintah 3 - Staff,instansi perintah staff,tidak_berpengalaman
|
||||||
|
Teknik Komputer (56401),E32200666,MOHAMMAD RODIVOL HAK,2024.0,belum_kerja,4 - Wiraswasta/perusahaan sendiri,Better Of Life,1 - Founder,Data_2024_Tracer_study.csv,4 - Wiraswasta/perusahaan sendiri Better Of Life 1 - Founder,wiraswasta usaha sendiri better life founder,tidak_berpengalaman
|
||||||
|
Manajemen Informatika (57401),E31211055,AHMAD MAULANA SUBANDRIO,2024.0,belum_kerja,1 - Instansi pemerintah,,1 - Founder,Data_2024_Tracer_study.csv,1 - Instansi pemerintah 1 - Founder,instansi perintah founder,tidak_berpengalaman
|
||||||
|
Teknik Komputer (56401),E32212287,RAGA MULYA PRATAMA PUTRA,2024.0,belum_kerja,4 - Wiraswasta/perusahaan sendiri,,4 - Freelance/Kerja Lepas,Data_2024_Tracer_study.csv,4 - Wiraswasta/perusahaan sendiri 4 - Freelance/Kerja Lepas,wiraswasta usaha sendiri freelance kerja lepas,tidak_berpengalaman
|
||||||
|
Teknik Komputer (56401),E32200194,NANDA FATHURAHMA,2024.0,belum_kerja,4 - Wiraswasta/perusahaan sendiri,,1 - Founder,Data_2024_Tracer_study.csv,4 - Wiraswasta/perusahaan sendiri 1 - Founder,wiraswasta usaha sendiri founder,tidak_berpengalaman
|
||||||
|
Teknik Komputer (56401),E32210446,RYAN AJENG SUKMA DEWI TITAH PRATAMA,2024.0,belum_kerja,belom bekerja,,4 - Freelance/Kerja Lepas,Data_2024_Tracer_study.csv,belom bekerja 4 - Freelance/Kerja Lepas,bom kerja freelance kerja lepas,tidak_berpengalaman
|
||||||
|
Teknik Komputer (56401),E32210771,M. FAJARSYAH EKA PERMANA,2024.0,belum_kerja,3 - Perusahaan swasta,,4 - Freelance/Kerja Lepas,Data_2024_Tracer_study.csv,3 - Perusahaan swasta 4 - Freelance/Kerja Lepas,usaha swasta freelance kerja lepas,tidak_berpengalaman
|
||||||
|
Teknik Komputer (56401),E32212333,INDRA WIJAYA,2024.0,belum_kerja,4 - Wiraswasta/perusahaan sendiri,,1 - Founder,Data_2024_Tracer_study.csv,4 - Wiraswasta/perusahaan sendiri 1 - Founder,wiraswasta usaha sendiri founder,tidak_berpengalaman
|
||||||
|
Teknik Komputer (56401),E32210826,RIZKI ROMADHONI,2024.0,belum_kerja,3 - Perusahaan swasta,,1 - Founder,Data_2024_Tracer_study.csv,3 - Perusahaan swasta 1 - Founder,usaha swasta founder,tidak_berpengalaman
|
||||||
|
Teknik Komputer (56401),E32212138,CINDY AFCARINA,2024.0,belum_kerja,3 - Perusahaan swasta,,3 - Staff,Data_2024_Tracer_study.csv,3 - Perusahaan swasta 3 - Staff,usaha swasta staff,tidak_berpengalaman
|
||||||
|
Teknik Komputer (56401),E32210344,REZA ANGGARA JANUARTA PUTRA BAYU SYAFIRA,2024.0,belum_kerja,1 - Instansi pemerintah,,3 - Staff,Data_2024_Tracer_study.csv,1 - Instansi pemerintah 3 - Staff,instansi perintah staff,tidak_berpengalaman
|
||||||
|
Teknik Komputer (56401),E32212299,ANANDA ANGGI DWI CAHYO,2024.0,belum_kerja,Berencana ingin bekerja di instansi pemerintah atau di BUMN,,3 - Staff,Data_2024_Tracer_study.csv,Berencana ingin bekerja di instansi pemerintah atau di BUMN 3 - Staff,rencana ingin kerja instansi perintah bumn staff,tidak_berpengalaman
|
||||||
|
Teknik Komputer (56401),E32211429,I PUTU SRI KRESNA LORU ARIASA,2024.0,belum_kerja,4 - Wiraswasta/perusahaan sendiri,Freelance,4 - Freelance/Kerja Lepas,Data_2024_Tracer_study.csv,4 - Wiraswasta/perusahaan sendiri Freelance 4 - Freelance/Kerja Lepas,wiraswasta usaha sendiri freelance freelance kerja lepas,tidak_berpengalaman
|
||||||
|
Teknik Komputer (56401),E32212309,KHOIRUL TAMAMI,2024.0,belum_kerja,6 - BUMN/BUMD,,4 - Freelance/Kerja Lepas,Data_2024_Tracer_study.csv,6 - BUMN/BUMD 4 - Freelance/Kerja Lepas,bumn bumd freelance kerja lepas,tidak_berpengalaman
|
||||||
|
Teknik Informatika (55301),E41202408,BRIANTI BELLA PRASNITIA,2024.0,belum_kerja,3 - Perusahaan swasta,,1 - Founder,Data_2024_Tracer_study.csv,3 - Perusahaan swasta 1 - Founder,usaha swasta founder,tidak_berpengalaman
|
||||||
|
Teknik Komputer (56401),E32212239,NURUL HUDA,2024.0,belum_kerja,2 - Organisasi non-profit/Lembaga Swadaya Masyarakat,,4 - Freelance/Kerja Lepas,Data_2024_Tracer_study.csv,2 - Organisasi non-profit/Lembaga Swadaya Masyarakat 4 - Freelance/Kerja Lepas,organisasi non profit lembaga swadaya masyarakat freelance kerja lepas,tidak_berpengalaman
|
||||||
|
Teknik Komputer (56401),E32211014,HABIB MAULANA,2024.0,belum_kerja,2 - Organisasi non-profit/Lembaga Swadaya Masyarakat,,4 - Freelance/Kerja Lepas,Data_2024_Tracer_study.csv,2 - Organisasi non-profit/Lembaga Swadaya Masyarakat 4 - Freelance/Kerja Lepas,organisasi non profit lembaga swadaya masyarakat freelance kerja lepas,tidak_berpengalaman
|
||||||
|
Teknik Komputer (56401),E32212181,FITRAH DESTRA PUTRA,2024.0,belum_kerja,6 - BUMN/BUMD,,2 - Co-Founder,Data_2024_Tracer_study.csv,6 - BUMN/BUMD 2 - Co-Founder,bumn bumd founder,tidak_berpengalaman
|
||||||
|
Teknik Informatika (55301),E41182272,BAYU JAYA ILHAM ALHAMDA,2024.0,belum_kerja,Traveloka,TRAVELOKA (ND TRAVELINDO),4 - Freelance/Kerja Lepas,Data_2024_Tracer_study.csv,Traveloka TRAVELOKA (ND TRAVELINDO) 4 - Freelance/Kerja Lepas,traveloka traveloka travelindo freelance kerja lepas,tidak_berpengalaman
|
||||||
|
Teknik Informatika (55301),E41201220,INDAH NUR MAHMUDA,2024.0,belum_kerja,,,,Data_2024_Tracer_study.csv,,,tidak_berpengalaman
|
||||||
|
Manajemen Informatika (57401),E31210927,INTAN DELVE ATIKA,2024.0,melanjutkan_studi,,,,Data_2024_Tracer_study.csv,,,tidak_berpengalaman
|
||||||
|
Manajemen Informatika (57401),E31210287,ABEL JATAYU WAHYU AJI,2024.0,belum_kerja,,,,Data_2024_Tracer_study.csv,,,tidak_berpengalaman
|
||||||
|
Manajemen Informatika (57401),E31211810,SITI RUSDANIYAH,2024.0,bekerja,,,,Data_2024_Tracer_study.csv,,,tidak_berpengalaman
|
||||||
|
Teknik Komputer (56401),E32211859,AHMAD RIDHO,2024.0,belum_kerja,,,,Data_2024_Tracer_study.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31221235,SITI LAILATUL AMALIA,2025.0,mencari_kerja,3 - Perusahaan swasta,,3 - Staff,Data_2025_Tracer_study_D3MIF.csv,3 - Perusahaan swasta 3 - Staff,usaha swasta staff,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31221213,NURUL QOMARIA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31221281,MUHAMMAD RIZAL FAHLEVI,2025.0,melanjutkan_studi,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222492,JA'FAR SHODIQ,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222519,MOH. DANI KURNIAWAN SUGIARTO,2025.0,melanjutkan_studi,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31221248,BELLA ERVINA FERDIANTI,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",,,Data_2025_Tracer_study_D3MIF.csv,"5 - Lainnya, Tuliskan (f1102)",lain tulis f1102,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31221247,DIVIA ANGGRAINI,2025.0,mencari_kerja,7 - Institusi/Organisasi Multilateral,,,Data_2025_Tracer_study_D3MIF.csv,7 - Institusi/Organisasi Multilateral,institusi organisasi multilateral,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222451,ADILA JELITA PUTRI,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31220215,NUR MITA UPI LESTARI,2025.0,mencari_kerja,4 - Wiraswasta/perusahaan sendiri,Jasain aja,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D3MIF.csv,4 - Wiraswasta/perusahaan sendiri Jasain aja 4 - Freelance/Kerja Lepas,wiraswasta usaha sendiri jasain freelance kerja lepas,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222461,GADIS MICHELLIA PUTRI,2025.0,melanjutkan_studi,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31221480,NOVITA YUNDA FAJRIYAH,2025.0,melanjutkan_studi,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31221305,MALEMNA SWARI TARIGAN,2025.0,mencari_kerja,3 - Perusahaan swasta,,,Data_2025_Tracer_study_D3MIF.csv,3 - Perusahaan swasta,usaha swasta,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31221259,Ratna Sari Dewi,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222656,JEREMY VAHARDIKA JAYA,2025.0,mencari_kerja,3 - Perusahaan swasta,,,Data_2025_Tracer_study_D3MIF.csv,3 - Perusahaan swasta,usaha swasta,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222327,FARISA RAHMASARI,2025.0,melanjutkan_studi,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222455,Salma Banafsha Safa Fitria,2025.0,mencari_kerja,6 - BUMN/BUMD,,2 - Co-Founder,Data_2025_Tracer_study_D3MIF.csv,6 - BUMN/BUMD 2 - Co-Founder,bumn bumd founder,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222398,Wisnu Hadi Mahendra,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31220218,ELSA SURYA AGUSTIN,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31221236,REYNAVA AURELLIA DAMAYANTI,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31220197,IHYA ULUMUDDIN,2025.0,melanjutkan_studi,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31221142,MUHAMMAD HERIYANTO,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222387,SEPTIANO RIZKI,2025.0,mencari_kerja,4 - Wiraswasta/perusahaan sendiri,,3 - Staff,Data_2025_Tracer_study_D3MIF.csv,4 - Wiraswasta/perusahaan sendiri 3 - Staff,wiraswasta usaha sendiri staff,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222680,NADEA MEFIRA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31221420,ANINDYA HUDAA KUMALA TANTRI,2025.0,melanjutkan_studi,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31221209,Ayu Eka Ciptasari,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222823,MARCELLINO RHADITYA ROSANDI,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31221108,SHAMITA YASMIN R,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222307,Alfiansyah Pradinata Putra,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222756,SYEBA WARDAHANI,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31221484,MIFTAH FARID GUNAWAN,2025.0,melanjutkan_studi,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222365,ADINDA KHOFIFA AZIZ,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",Politeknik Negeri jember,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D3MIF.csv,"5 - Lainnya, Tuliskan (f1102) Politeknik Negeri jember 4 - Freelance/Kerja Lepas",lain tulis f1102 politeknik negeri jember freelance kerja lepas,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31221225,RIFQI NURUL QOMARIAH,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222586,NURUL FAJRI MAULIDYA,2025.0,melanjutkan_studi,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222541,MUHAMMAD ARYA FATHOROZI,2025.0,melanjutkan_studi,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222658,LINTANG SURURUM SALSAFAIZ MARTALIA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31221299,OKTAVIARLEN SETYA WARDANI,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",,,Data_2025_Tracer_study_D3MIF.csv,"5 - Lainnya, Tuliskan (f1102)",lain tulis f1102,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31221315,WILLIAN ANGGI DEVAYU,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",,,Data_2025_Tracer_study_D3MIF.csv,"5 - Lainnya, Tuliskan (f1102)",lain tulis f1102,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222629,SHERLY WELYANA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222389,Muhammad Alfin Faiz,2025.0,melanjutkan_studi,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31220442,Ulil Farhah,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222534,ADINDA WIDYA PUTRI,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",,,Data_2025_Tracer_study_D3MIF.csv,"5 - Lainnya, Tuliskan (f1102)",lain tulis f1102,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222508,Diana Oktavia Rachmad,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31221264,GITA YASINDA PUTRI CAHYANI,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31221244,MUHAMMAD AGI PRASETYO,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31221325,MEY NUR AFNI,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",,,Data_2025_Tracer_study_D3MIF.csv,"5 - Lainnya, Tuliskan (f1102)",lain tulis f1102,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31220954,MUHAMAD FAIS AIZAT,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31220577,DINDA NAUSA KUSUMA WARDANI,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",,,Data_2025_Tracer_study_D3MIF.csv,"5 - Lainnya, Tuliskan (f1102)",lain tulis f1102,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31220334,ISNAINI QAYYUM AZIZAH,2025.0,mencari_kerja,6 - BUMN/BUMD,,,Data_2025_Tracer_study_D3MIF.csv,6 - BUMN/BUMD,bumn bumd,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31221357,NISWAH TRIAS QUROTUL A'YUN,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222653,FIRDATURROFIAH,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222870,SATRIA HILMI AN'AMTA CAHYONO,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222313,AGYA RIZKINTA WILDANTI,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31221269,Mohammad Iqbal Abdillah,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31221263,TEGAR FEBRIANSYAH PUTRA NURULLAH,2025.0,mencari_kerja,3 - Perusahaan swasta,HIJIGROUP,,Data_2025_Tracer_study_D3MIF.csv,3 - Perusahaan swasta HIJIGROUP,usaha swasta hijigroup,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222450,NUR FUADI MUHAMMAD,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222582,Hilda Eva Stefhany,2025.0,mencari_kerja,4 - Wiraswasta/perusahaan sendiri,,,Data_2025_Tracer_study_D3MIF.csv,4 - Wiraswasta/perusahaan sendiri,wiraswasta usaha sendiri,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222691,ANNAJWA SHOHIBUZZAHRO,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222606,AHMAD ZAENAL ABIDIN,2025.0,mencari_kerja,3 - Perusahaan swasta,,,Data_2025_Tracer_study_D3MIF.csv,3 - Perusahaan swasta,usaha swasta,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31220412,ALFIN FADLI,2025.0,belum_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222851,ENDY FADLULLAH,2025.0,melanjutkan_studi,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222888,ALIZAR MUHARROM,2025.0,mencari_kerja,4 - Wiraswasta/perusahaan sendiri,,,Data_2025_Tracer_study_D3MIF.csv,4 - Wiraswasta/perusahaan sendiri,wiraswasta usaha sendiri,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222641,Firman Agung Alamsyah,2025.0,melanjutkan_studi,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222381,BRAHMI HAWA SAWITRI,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222344,SOVIA LAJUBA,2025.0,mencari_kerja,,,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D3MIF.csv,4 - Freelance/Kerja Lepas,freelance kerja lepas,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222663,ROFIATUL ANDAWIYAH,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",PT. Lambda Solusi Informatika,,Data_2025_Tracer_study_D3MIF.csv,"5 - Lainnya, Tuliskan (f1102) PT. Lambda Solusi Informatika",lain tulis f1102 lambda solusi informatika,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31221397,VICKY FERNANDO ANGGARA PUTRA IKHWANTO,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D3MIF.csv,"5 - Lainnya, Tuliskan (f1102) 4 - Freelance/Kerja Lepas",lain tulis f1102 freelance kerja lepas,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222379,FAHMI KURNIAWAN,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31221009,AISYAH SALSHABILLA SISKA PUTRI,2025.0,belum_kerja,3 - Perusahaan swasta,PT. Young Tree,,Data_2025_Tracer_study_D3MIF.csv,3 - Perusahaan swasta PT. Young Tree,usaha swasta young tree,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31221306,MUHAMMAD RAMA PRADIPA WIJAYA,2025.0,mencari_kerja,3 - Perusahaan swasta,,3 - Staff,Data_2025_Tracer_study_D3MIF.csv,3 - Perusahaan swasta 3 - Staff,usaha swasta staff,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222696,FITRI AMALIA SAQILA,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",PT China Communications Construction Indonesia (CCCI).,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D3MIF.csv,"5 - Lainnya, Tuliskan (f1102) PT China Communications Construction Indonesia (CCCI). 4 - Freelance/Kerja Lepas",lain tulis f1102 china communications construction indonesia ccci freelance kerja lepas,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31221407,M. FAHIM DAVID BACHTIAR,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222518,MUHAMMAD ALFIN ALFAUZI,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222765,DINA AYU AISYAH,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31210646,ATIKAH NURI HAZMA,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",Yasmine Fashion,,Data_2025_Tracer_study_D3MIF.csv,"5 - Lainnya, Tuliskan (f1102) Yasmine Fashion",lain tulis f1102 yasmine fashion,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31221245,ALFIONINA DWI RACHMALIA,2025.0,melanjutkan_studi,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31220095,GALUH REVIKO SATYA KIRANA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31221222,DIYO ANGGARA PRADIPA PUTRA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222366,Arya Advicenna,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",,,Data_2025_Tracer_study_D3MIF.csv,"5 - Lainnya, Tuliskan (f1102)",lain tulis f1102,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31221360,IQBAL AMIN,2025.0,belum_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31220277,PUTRI AYU WULANDARI,2025.0,mencari_kerja,1 - Instansi pemerintah,,3 - Staff,Data_2025_Tracer_study_D3MIF.csv,1 - Instansi pemerintah 3 - Staff,instansi perintah staff,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222540,Haikal Muna'amul Islam,2025.0,melanjutkan_studi,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31200086,DANIEL ABDULLAH AFANDI,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",Belum Mendapatkan Pekerjaan,,Data_2025_Tracer_study_D3MIF.csv,"5 - Lainnya, Tuliskan (f1102) Belum Mendapatkan Pekerjaan",lain tulis f1102 belum dapat kerja,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222707,ADELIA SARI DEWI,2025.0,belum_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222544,Sultan Arya Maulana,2025.0,melanjutkan_studi,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222854,Irfan Dimas Riyadi,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",belum ada,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D3MIF.csv,"5 - Lainnya, Tuliskan (f1102) belum ada 4 - Freelance/Kerja Lepas",lain tulis f1102 belum freelance kerja lepas,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222846,MOCHAMMAD GINATA FEBRYANSYAH,2025.0,belum_kerja,3 - Perusahaan swasta,,,Data_2025_Tracer_study_D3MIF.csv,3 - Perusahaan swasta,usaha swasta,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222882,Stephen Gesityan,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31220567,PUTRI NABILAH HUWAIDAH,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222881,ALE ANDRA PRIANDANI,2025.0,mencari_kerja,4 - Wiraswasta/perusahaan sendiri,,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D3MIF.csv,4 - Wiraswasta/perusahaan sendiri 4 - Freelance/Kerja Lepas,wiraswasta usaha sendiri freelance kerja lepas,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31221388,Alya Nirmala Putri,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222699,MOCH ADIM RIZKY SYAH PUTRA,2025.0,melanjutkan_studi,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31222596,ALYA LUFRIDA AFIFAH,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
19. Manajemen Informatika (57401),E31221361,Algoritma Dwifebri fathiyakan,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3MIF.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32221234,RAHMAD ERA SUGIARTO,2025.0,mencari_kerja,,,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D3TKK.csv,4 - Freelance/Kerja Lepas,freelance kerja lepas,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222388,SYAHMI NAUFAL SAPUTRA,2025.0,mencari_kerja,,,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D3TKK.csv,4 - Freelance/Kerja Lepas,freelance kerja lepas,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32220022,TUTIK NOER KHOLIFAH,2025.0,mencari_kerja,,,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D3TKK.csv,4 - Freelance/Kerja Lepas,freelance kerja lepas,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32220375,MUHAMMAD YUSUF SHOLEHUDIN RENDY,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32221231,REYHAN NAUFAL TSAQIF,2025.0,mencari_kerja,6 - BUMN/BUMD,,,Data_2025_Tracer_study_D3TKK.csv,6 - BUMN/BUMD,bumn bumd,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222300,SHAMIR FAIZAL THALIB,2025.0,mencari_kerja,3 - Perusahaan swasta,Pt. Semen imasco asiatic,,Data_2025_Tracer_study_D3TKK.csv,3 - Perusahaan swasta Pt. Semen imasco asiatic,usaha swasta semen imasco asiatic,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32220274,MOCH RENDY ARDIYANTO,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",,,Data_2025_Tracer_study_D3TKK.csv,"5 - Lainnya, Tuliskan (f1102)",lain tulis f1102,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32221274,SAKTI BAYU NUGRAHA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32220671,AHMAD ADIB AKBAR,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32221002,Muhammad Ardi Aziz Saputra,2025.0,mencari_kerja,4 - Wiraswasta/perusahaan sendiri,,,Data_2025_Tracer_study_D3TKK.csv,4 - Wiraswasta/perusahaan sendiri,wiraswasta usaha sendiri,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222748,IQBAL FIKHRIANSYAH ROMADHONA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222626,DIYAN SHINTIA BELLA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222585,YUSRIL ROMIZA PUTRA,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",,,Data_2025_Tracer_study_D3TKK.csv,"5 - Lainnya, Tuliskan (f1102)",lain tulis f1102,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32221390,SERIAN ANAK AYUT,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",belum memiliki pekerjaan,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D3TKK.csv,"5 - Lainnya, Tuliskan (f1102) belum memiliki pekerjaan 4 - Freelance/Kerja Lepas",lain tulis f1102 belum milik kerja freelance kerja lepas,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222403,Ahmad Furqon Saputra,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222434,YOVITA SARI CAHYANINGTYAS,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222503,MUHAMMAD RISKI ARDIANSYAH,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",,,Data_2025_Tracer_study_D3TKK.csv,"5 - Lainnya, Tuliskan (f1102)",lain tulis f1102,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32221196,RIFQY ZIDANE HARTONO,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",,,Data_2025_Tracer_study_D3TKK.csv,"5 - Lainnya, Tuliskan (f1102)",lain tulis f1102,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222444,Wildan Abbrhory,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",0,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D3TKK.csv,"5 - Lainnya, Tuliskan (f1102) 0 4 - Freelance/Kerja Lepas",lain tulis f1102 freelance kerja lepas,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222530,MOHAMMAD RIZKY ROMADHON,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222650,MUHAMMAD HISYAMUDDIN,2025.0,bekerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32220549,URIP AGILA SETIO,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",-,3 - Staff,Data_2025_Tracer_study_D3TKK.csv,"5 - Lainnya, Tuliskan (f1102) - 3 - Staff",lain tulis f1102 staff,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32221220,Muhammad Miftahul Huda,2025.0,belum_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222571,MOCHAMAD ALBANUS FAHREZI,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32221098,JERY NAUVAL NICKOLAS,2025.0,mencari_kerja,6 - BUMN/BUMD,PLN Icon Plus SBU RJBT,,Data_2025_Tracer_study_D3TKK.csv,6 - BUMN/BUMD PLN Icon Plus SBU RJBT,bumn bumd pln icon plus sbu rjbt,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222548,NADIA EMILIANA RAISSA,2025.0,melanjutkan_studi,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222628,GHAIDA SORAYA M.,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32221354,MOCH. RIZQ YUNIARIEVAL,2025.0,belum_kerja,6 - BUMN/BUMD,belum kerja,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D3TKK.csv,6 - BUMN/BUMD belum kerja 4 - Freelance/Kerja Lepas,bumn bumd belum kerja freelance kerja lepas,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32221146,JOHARDIO EKA PUTRA ARIV,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222572,AMALIA KHOIRUN NISA',2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222484,EGA SAPUTRA WICAKSANA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32221324,MUHAMMAD ABIL SYUGRON HABIBILLAH,2025.0,mencari_kerja,7 - Institusi/Organisasi Multilateral,,,Data_2025_Tracer_study_D3TKK.csv,7 - Institusi/Organisasi Multilateral,institusi organisasi multilateral,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32221349,MOHAMMAD FARIS VICO PRADIFA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222371,Ferdiansyah Maula Syarif,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222580,ALI SYARIEF,2025.0,mencari_kerja,3 - Perusahaan swasta,Pt.Iwip,,Data_2025_Tracer_study_D3TKK.csv,3 - Perusahaan swasta Pt.Iwip,usaha swasta iwip,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32221431,DELI SAFITRI,2025.0,mencari_kerja,4 - Wiraswasta/perusahaan sendiri,Ayam Petelur Pak Dela,3 - Staff,Data_2025_Tracer_study_D3TKK.csv,4 - Wiraswasta/perusahaan sendiri Ayam Petelur Pak Dela 3 - Staff,wiraswasta usaha sendiri ayam telur dela staff,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32220213,SADDAM ANDIKA PUTRA,2025.0,belum_kerja,3 - Perusahaan swasta,Rumah,,Data_2025_Tracer_study_D3TKK.csv,3 - Perusahaan swasta Rumah,usaha swasta rumah,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32221348,DWI FITRIYAH KHAERULINA,2025.0,mencari_kerja,1 - Instansi pemerintah,,,Data_2025_Tracer_study_D3TKK.csv,1 - Instansi pemerintah,instansi perintah,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32220937,AHYAR NUR ALIF,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32221287,MONANG MANGATA TAMPUBOLON,2025.0,mencari_kerja,3 - Perusahaan swasta,,,Data_2025_Tracer_study_D3TKK.csv,3 - Perusahaan swasta,usaha swasta,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222411,Alfian Dani Ahmad Prasetyo,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",,,Data_2025_Tracer_study_D3TKK.csv,"5 - Lainnya, Tuliskan (f1102)",lain tulis f1102,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32221271,LINDA DWI WINANDHA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222685,AMRI MURSALIINA DARINTO,2025.0,mencari_kerja,3 - Perusahaan swasta,,,Data_2025_Tracer_study_D3TKK.csv,3 - Perusahaan swasta,usaha swasta,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222481,ARDHA PRIA PRAMBUDI,2025.0,mencari_kerja,4 - Wiraswasta/perusahaan sendiri,,1 - Founder,Data_2025_Tracer_study_D3TKK.csv,4 - Wiraswasta/perusahaan sendiri 1 - Founder,wiraswasta usaha sendiri founder,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222855,Imam Ghazali Munir,2025.0,mencari_kerja,1 - Instansi pemerintah,,,Data_2025_Tracer_study_D3TKK.csv,1 - Instansi pemerintah,instansi perintah,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32221335,HENDRI GUNAWAN,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222352,IVAN ARIESNA NUGROHO,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222693,MOCHAMAD ALIF RIDHO AULIA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222304,MUCHAMAD NASRULLAH SYAHBANA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32221375,KEVIN RIANG EKA PRASETYA,2025.0,mencari_kerja,6 - BUMN/BUMD,PLN ICON PLUS,,Data_2025_Tracer_study_D3TKK.csv,6 - BUMN/BUMD PLN ICON PLUS,bumn bumd pln icon plus,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32221275,FARHAN PRATAMA PUTRA,2025.0,mencari_kerja,6 - BUMN/BUMD,PLN ICON PLUS SURABAYA,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D3TKK.csv,6 - BUMN/BUMD PLN ICON PLUS SURABAYA 4 - Freelance/Kerja Lepas,bumn bumd pln icon plus surabaya freelance kerja lepas,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222396,RIZAL MAULANA ALIF,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222672,VERGIAWAN KURNIALISTIANTO,2025.0,mencari_kerja,6 - BUMN/BUMD,,,Data_2025_Tracer_study_D3TKK.csv,6 - BUMN/BUMD,bumn bumd,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222440,ALIF ABDAUL KARIM,2025.0,belum_kerja,3 - Perusahaan swasta,,,Data_2025_Tracer_study_D3TKK.csv,3 - Perusahaan swasta,usaha swasta,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32220565,Adam Darmawansa,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222868,DANDHI DWI ARI PRASETYA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222363,ROBBY DWI JAYANTO,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222356,ALIYA NABILLA PUTRI,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32220496,BIMA PUTRA ADITYA,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",,,Data_2025_Tracer_study_D3TKK.csv,"5 - Lainnya, Tuliskan (f1102)",lain tulis f1102,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32221343,ANANG SANDI PRASETYO,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",,,Data_2025_Tracer_study_D3TKK.csv,"5 - Lainnya, Tuliskan (f1102)",lain tulis f1102,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32221328,MOHAMMAD IRSYAD ROSIDI,2025.0,mencari_kerja,3 - Perusahaan swasta,PT Global Media Data Prima,3 - Staff,Data_2025_Tracer_study_D3TKK.csv,3 - Perusahaan swasta PT Global Media Data Prima 3 - Staff,usaha swasta global media data prima staff,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32221364,Nisa Carolina Putri,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222865,AHMAD DHANI ALVARISI,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222869,GALIH RIANTIARNO,2025.0,mencari_kerja,4 - Wiraswasta/perusahaan sendiri,,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D3TKK.csv,4 - Wiraswasta/perusahaan sendiri 4 - Freelance/Kerja Lepas,wiraswasta usaha sendiri freelance kerja lepas,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32212281,Ghilman Roif Zakiri,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32221413,MOH. ANWAR,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32221425,KELVIN MAULANA SHOFANI,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222437,RIDHO FAISAL HAKKI,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222349,AKHMAD MARTA ADI PUTRA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32221434,TEGUH SETYO BUDI,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D3TKK.csv,"5 - Lainnya, Tuliskan (f1102) 4 - Freelance/Kerja Lepas",lain tulis f1102 freelance kerja lepas,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222377,Akbar Maulana Agritanto,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222362,FASHA GALIH PUTRA ARIFANDI,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32220150,RAFI ABIYYU YAKIN,2025.0,mencari_kerja,3 - Perusahaan swasta,,,Data_2025_Tracer_study_D3TKK.csv,3 - Perusahaan swasta,usaha swasta,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),e32222568,BIMO RAHMAD DIANSYAH,2025.0,belum_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222430,DANIEL KEANNY HEBERT,2025.0,melanjutkan_studi,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222355,Putraku Nabil Rabbani,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D3TKK.csv,,,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32221448,AIDIL ARDIANSYAH,2025.0,mencari_kerja,4 - Wiraswasta/perusahaan sendiri,,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D3TKK.csv,4 - Wiraswasta/perusahaan sendiri 4 - Freelance/Kerja Lepas,wiraswasta usaha sendiri freelance kerja lepas,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32222341,NURUL RIZQI AKBARSYAH ASH SHIDDIQIE,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",,,Data_2025_Tracer_study_D3TKK.csv,"5 - Lainnya, Tuliskan (f1102)",lain tulis f1102,tidak_berpengalaman
|
||||||
|
20. Teknik Komputer (56401),E32221382,FATHUR RIZQI GINANDA ARIKUSUMA,2025.0,belum_kerja,3 - Perusahaan swasta,,3 - Staff,Data_2025_Tracer_study_D3TKK.csv,3 - Perusahaan swasta 3 - Staff,usaha swasta staff,tidak_berpengalaman
|
||||||
|
Teknik Informatika (55301),E41232153,RIDLOWAN EKA NUR CAHYA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
Teknik Informatika (55301),E41232322,SYABRIANT BAGUS YANUAR,2025.0,mencari_kerja,6 - BUMN/BUMD,,,Data_2025_Tracer_study_D4TIF.csv,6 - BUMN/BUMD,bumn bumd,tidak_berpengalaman
|
||||||
|
Teknik Informatika (55301),E41180538,Rendra Adji Prayudi,2025.0,mencari_kerja,3 - Perusahaan swasta,PT Mulya Menara Sejahtera,,Data_2025_Tracer_study_D4TIF.csv,3 - Perusahaan swasta PT Mulya Menara Sejahtera,usaha swasta mulya menara sejahtera,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41202152,putri Adelwis,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",belum,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D4TIF.csv,"5 - Lainnya, Tuliskan (f1102) belum 4 - Freelance/Kerja Lepas",lain tulis f1102 belum freelance kerja lepas,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41202502,WIAN ADIYATMA BISMA RAZAAK,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41201544,NOVITA BENING WIDYASTUTI,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41232033,ALVIN YUDHA EKA PRADISTYA,2025.0,melanjutkan_studi,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41232270,DEDI HARIYANTO,2025.0,mencari_kerja,6 - BUMN/BUMD,,,Data_2025_Tracer_study_D4TIF.csv,6 - BUMN/BUMD,bumn bumd,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41232102,Iwang Pradipta Dinoto,2025.0,belum_kerja,1 - Instansi pemerintah,,3 - Staff,Data_2025_Tracer_study_D4TIF.csv,1 - Instansi pemerintah 3 - Staff,instansi perintah staff,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41232545,Desy Nur Alviah,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41232176,BAGUS IKA PUTRANTO,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",,,Data_2025_Tracer_study_D4TIF.csv,"5 - Lainnya, Tuliskan (f1102)",lain tulis f1102,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41212022,DEO ANDREAS SYAPUTRA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210014,FAISAL OKTABRIAN SHOLIHIN,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210003,EDY ATTHOILLAH,2025.0,mencari_kerja,4 - Wiraswasta/perusahaan sendiri,,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D4TIF.csv,4 - Wiraswasta/perusahaan sendiri 4 - Freelance/Kerja Lepas,wiraswasta usaha sendiri freelance kerja lepas,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41211496,NILLA PUTRI ROSIDANIA,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D4TIF.csv,"5 - Lainnya, Tuliskan (f1102) 4 - Freelance/Kerja Lepas",lain tulis f1102 freelance kerja lepas,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210373,NADIA AYU SAFITRI,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210026,REGINA JUNIANTIKA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210369,RAIHAN ACHMAD DANI FIRMANSYAH,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210089,DAFFA AFIFI SYAHRONY,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210372,KURROTA AKYUN,2025.0,belum_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210005,NAJWA AMALIA MAHARANI,2025.0,mencari_kerja,3 - Perusahaan swasta,Megata,,Data_2025_Tracer_study_D4TIF.csv,3 - Perusahaan swasta Megata,usaha swasta megata,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41211247,POPO ZAKARIA,2025.0,mencari_kerja,3 - Perusahaan swasta,,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D4TIF.csv,3 - Perusahaan swasta 4 - Freelance/Kerja Lepas,usaha swasta freelance kerja lepas,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210602,RESTU FAHIMUROID,2025.0,melanjutkan_studi,,,3 - Staff,Data_2025_Tracer_study_D4TIF.csv,3 - Staff,staff,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210004,Mokhamad Farhan Abdillah,2025.0,mencari_kerja,4 - Wiraswasta/perusahaan sendiri,Freelance,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D4TIF.csv,4 - Wiraswasta/perusahaan sendiri Freelance 4 - Freelance/Kerja Lepas,wiraswasta usaha sendiri freelance freelance kerja lepas,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210800,FARHAN HILMY ARWANTO,2025.0,mencari_kerja,7 - Institusi/Organisasi Multilateral,,3 - Staff,Data_2025_Tracer_study_D4TIF.csv,7 - Institusi/Organisasi Multilateral 3 - Staff,institusi organisasi multilateral staff,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210674,ATHIYAH,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),e41211491,Rayhan Febriansyah,2025.0,mencari_kerja,4 - Wiraswasta/perusahaan sendiri,,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D4TIF.csv,4 - Wiraswasta/perusahaan sendiri 4 - Freelance/Kerja Lepas,wiraswasta usaha sendiri freelance kerja lepas,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210930,FILLAH SEPTIAN,2025.0,mencari_kerja,3 - Perusahaan swasta,,,Data_2025_Tracer_study_D4TIF.csv,3 - Perusahaan swasta,usaha swasta,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210679,FAQIH MUSTAFA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41212169,ELISA FEBRIAN SHOLEHA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41212166,FARA DERLIANA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41212128,ANDINI PUTRI RAMADHANI,2025.0,mencari_kerja,,-,,Data_2025_Tracer_study_D4TIF.csv,-,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210069,SHILMIA MADINA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210096,BABYLINA VINDAROSITA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41211320,Achmad Zakariya,2025.0,mencari_kerja,3 - Perusahaan swasta,,,Data_2025_Tracer_study_D4TIF.csv,3 - Perusahaan swasta,usaha swasta,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41232544,VENI KURNIA DEWI,2025.0,mencari_kerja,2 - Organisasi non-profit/Lembaga Swadaya Masyarakat,Sekolah,,Data_2025_Tracer_study_D4TIF.csv,2 - Organisasi non-profit/Lembaga Swadaya Masyarakat Sekolah,organisasi non profit lembaga swadaya masyarakat sekolah,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41211144,ADHE WAHYU TRILAKSANA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210998,JACINDA OLGA NABILA,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",-,,Data_2025_Tracer_study_D4TIF.csv,"5 - Lainnya, Tuliskan (f1102) -",lain tulis f1102,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41212091,AISYAH FIKRI NABILAH,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210477,FIKRI AHDIAR,2025.0,mencari_kerja,4 - Wiraswasta/perusahaan sendiri,RELIS,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D4TIF.csv,4 - Wiraswasta/perusahaan sendiri RELIS 4 - Freelance/Kerja Lepas,wiraswasta usaha sendiri rel freelance kerja lepas,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41211039,AIKO MAWARDI,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210099,FIRDIYATUS SHOLIHAH,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210436,THORIQ LUKMAN HAKIM,2025.0,mencari_kerja,6 - BUMN/BUMD,,,Data_2025_Tracer_study_D4TIF.csv,6 - BUMN/BUMD,bumn bumd,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41202343,PUTRI SALSABHILA FAHIRA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41212168,INDRA PRASETYO WIJAYA,2025.0,mencari_kerja,6 - BUMN/BUMD,-,,Data_2025_Tracer_study_D4TIF.csv,6 - BUMN/BUMD -,bumn bumd,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41212253,KRISNA CHOIRIL ANDIKA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41211969,AHMAD TAUFIQ RIDHO'I,2025.0,bekerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210598,EKA SULISTYANINGSIH,2025.0,mencari_kerja,6 - BUMN/BUMD,,,Data_2025_Tracer_study_D4TIF.csv,6 - BUMN/BUMD,bumn bumd,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210731,IDA SEVIA,2025.0,mencari_kerja,6 - BUMN/BUMD,,,Data_2025_Tracer_study_D4TIF.csv,6 - BUMN/BUMD,bumn bumd,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41212069,ALIF TRIADI AGUNG WICAKSONO,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",,,Data_2025_Tracer_study_D4TIF.csv,"5 - Lainnya, Tuliskan (f1102)",lain tulis f1102,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41211478,GYMNASTIAR ALMA GHIFARI,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41211245,MUHAMMAD REZA IVANO PAHLEVI,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41232549,RISMA DEWI SEPTIANI,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41212097,AKNEZ NURIL MAHMUDA,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D4TIF.csv,"5 - Lainnya, Tuliskan (f1102) 4 - Freelance/Kerja Lepas",lain tulis f1102 freelance kerja lepas,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41232541,MUHAMMAD FARRAS WIRAWAN,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210132,GEMA HANNA NAPITUPULU,2025.0,mencari_kerja,6 - BUMN/BUMD,MASPION,,Data_2025_Tracer_study_D4TIF.csv,6 - BUMN/BUMD MASPION,bumn bumd maspion,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41212025,IRMA FIRNANDA,2025.0,mencari_kerja,6 - BUMN/BUMD,PT PETROKIMIA GRESIK,3 - Staff,Data_2025_Tracer_study_D4TIF.csv,6 - BUMN/BUMD PT PETROKIMIA GRESIK 3 - Staff,bumn bumd petrokimia gresik staff,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41232537,Anang Prayoga,2025.0,melanjutkan_studi,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41211551,AHMAD HIPNIE KHOIRI,2025.0,mencari_kerja,,infinite,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D4TIF.csv,infinite 4 - Freelance/Kerja Lepas,infinite freelance kerja lepas,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41232538,HUSNUL PUTRI CANDRA BUANA,2025.0,melanjutkan_studi,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41212174,FATHAN MAULANA RIFQI HARTONO,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41211978,AMELIA FAWWAZ ZAIN,2025.0,mencari_kerja,,PELINDO,,Data_2025_Tracer_study_D4TIF.csv,PELINDO,pelindo,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210172,ADITYA IBRAR ABDILLAH,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41212151,ISMIATI EKA OKTAFIA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41212101,EKA PUTRI YULIANA,2025.0,mencari_kerja,6 - BUMN/BUMD,,,Data_2025_Tracer_study_D4TIF.csv,6 - BUMN/BUMD,bumn bumd,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41211128,KRISTOPHER YOHANES IMMANUEL TAMBUNAN,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41212068,ADELIA AYU AVISTA,2025.0,mencari_kerja,6 - BUMN/BUMD,,3 - Staff,Data_2025_Tracer_study_D4TIF.csv,6 - BUMN/BUMD 3 - Staff,bumn bumd staff,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41211217,GIGIH DWI CAHYO,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210716,KHOFIFAH AINURROHMAH,2025.0,mencari_kerja,6 - BUMN/BUMD,BUMN,,Data_2025_Tracer_study_D4TIF.csv,6 - BUMN/BUMD BUMN,bumn bumd bumn,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41232547,FIRDA ULFA UNSILAH,2025.0,melanjutkan_studi,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41232542,MUHAMMAD ILHAM YUAL MAULANA,2025.0,mencari_kerja,4 - Wiraswasta/perusahaan sendiri,milik sensiri,1 - Founder,Data_2025_Tracer_study_D4TIF.csv,4 - Wiraswasta/perusahaan sendiri milik sensiri 1 - Founder,wiraswasta usaha sendiri milik sensiri founder,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41232540,MUHAMAD BAYU CANDRA PAMUNGKAS,2025.0,melanjutkan_studi,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210002,RESTU AJI PRASETYO SAPUTRA,2025.0,mencari_kerja,,-,3 - Staff,Data_2025_Tracer_study_D4TIF.csv,- 3 - Staff,staff,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41211570,WISHAL AZHARYAN AL HISYAM,2025.0,mencari_kerja,4 - Wiraswasta/perusahaan sendiri,,1 - Founder,Data_2025_Tracer_study_D4TIF.csv,4 - Wiraswasta/perusahaan sendiri 1 - Founder,wiraswasta usaha sendiri founder,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41211408,DAFFA FAUZI RAHMAN,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210766,Akbar Ramadhani Firdaus,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210603,DAVID ARROZAQI,2025.0,belum_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41211220,YUNAN ADITYA PRIMAWATDANA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41211107,MUHAMMAD ZAINAL ABIDIN,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210798,Yusuf Firmansyah Adi Basuni,2025.0,melanjutkan_studi,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41212285,ELISA CHRISTIAN ALFRIDO MANTOLAS,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41211257,MUHAMMAD ILHAM IKHWANUL AKRAM,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",belum bekerja,,Data_2025_Tracer_study_D4TIF.csv,"5 - Lainnya, Tuliskan (f1102) belum bekerja",lain tulis f1102 belum kerja,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41211111,ALIF IMANULLAH KRISNA ALAM,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41212263,ZULFIANTI RAHMAWATI ASHILAH,2025.0,mencari_kerja,2 - Organisasi non-profit/Lembaga Swadaya Masyarakat,-,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D4TIF.csv,2 - Organisasi non-profit/Lembaga Swadaya Masyarakat - 4 - Freelance/Kerja Lepas,organisasi non profit lembaga swadaya masyarakat freelance kerja lepas,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210698,MUHAMMAD MAULANA MALIK IBRAHIM,2025.0,mencari_kerja,1 - Instansi pemerintah,Pemerintah Daerah,,Data_2025_Tracer_study_D4TIF.csv,1 - Instansi pemerintah Pemerintah Daerah,instansi perintah perintah daerah,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210960,Rio Javier Reyhan,2025.0,mencari_kerja,7 - Institusi/Organisasi Multilateral,,,Data_2025_Tracer_study_D4TIF.csv,7 - Institusi/Organisasi Multilateral,institusi organisasi multilateral,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41212254,Fahmy Rosyadi,2025.0,mencari_kerja,3 - Perusahaan swasta,Profile Image Studio,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D4TIF.csv,3 - Perusahaan swasta Profile Image Studio 4 - Freelance/Kerja Lepas,usaha swasta profile image studio freelance kerja lepas,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41212217,ABDULLAH ALI,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41211349,FREDA ADI FARDANA,2025.0,mencari_kerja,3 - Perusahaan swasta,,3 - Staff,Data_2025_Tracer_study_D4TIF.csv,3 - Perusahaan swasta 3 - Staff,usaha swasta staff,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41211473,RINA WULANDHARI,2025.0,wiraswasta,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41211200,EGA SYAHRUL RAMADHANTO,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41211601,MUHAMMAD LUTFI ZE'IN AMINULLAH,2025.0,belum_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41191074,EVRA MARIO SIRAIT,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210022,LIZA INDARWATI,2025.0,mencari_kerja,4 - Wiraswasta/perusahaan sendiri,ADB Programming,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D4TIF.csv,4 - Wiraswasta/perusahaan sendiri ADB Programming 4 - Freelance/Kerja Lepas,wiraswasta usaha sendiri adb programming freelance kerja lepas,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41211463,REZKY AGUS SETIAWAN,2025.0,belum_kerja,3 - Perusahaan swasta,,,Data_2025_Tracer_study_D4TIF.csv,3 - Perusahaan swasta,usaha swasta,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210905,NABILA ZHAFARINA,2025.0,mencari_kerja,4 - Wiraswasta/perusahaan sendiri,,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D4TIF.csv,4 - Wiraswasta/perusahaan sendiri 4 - Freelance/Kerja Lepas,wiraswasta usaha sendiri freelance kerja lepas,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210730,ADINDA EKA MAYANG WIJAYANTI,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41211975,ANIK SAHILAH,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41211194,LUTFI AMELIYA PUTRI,2025.0,mencari_kerja,4 - Wiraswasta/perusahaan sendiri,Mad Buah Home,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D4TIF.csv,4 - Wiraswasta/perusahaan sendiri Mad Buah Home 4 - Freelance/Kerja Lepas,wiraswasta usaha sendiri mad buah home freelance kerja lepas,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41211319,AMMAR AKHTAR ADDANY,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210647,KHAIRATUZ ZAHRO,2025.0,belum_kerja,6 - BUMN/BUMD,,3 - Staff,Data_2025_Tracer_study_D4TIF.csv,6 - BUMN/BUMD 3 - Staff,bumn bumd staff,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41212252,MAULANA IBRAHIM,2025.0,mencari_kerja,6 - BUMN/BUMD,,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D4TIF.csv,6 - BUMN/BUMD 4 - Freelance/Kerja Lepas,bumn bumd freelance kerja lepas,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41212368,BHAKTI DWI RAMADHANI HENDARTO,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",Tidak Ada,1 - Founder,Data_2025_Tracer_study_D4TIF.csv,"5 - Lainnya, Tuliskan (f1102) Tidak Ada 1 - Founder",lain tulis f1102 founder,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41212221,Naufal Alif Nur Muhammad Rahmadhan,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41212297,ANGGA PRABASWARA PUTRA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41191683,AULIA NOVRIN HARLEYANTO,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41202549,MUHAMMAD NAFIL,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",WFH,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D4TIF.csv,"5 - Lainnya, Tuliskan (f1102) WFH 4 - Freelance/Kerja Lepas",lain tulis f1102 wfh freelance kerja lepas,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210746,MOCH IQBAL MAULANA FIEKRI,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41211965,AYU ANGGRAINI,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41211674,MUHAMAD SLAMET DAROENI,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41212104,NIECOLA JODY SETIAWAN,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41211209,AKBAR FIRMANSYAH,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",saya belum bekerja,1 - Founder,Data_2025_Tracer_study_D4TIF.csv,"5 - Lainnya, Tuliskan (f1102) saya belum bekerja 1 - Founder",lain tulis f1102 belum kerja founder,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210223,MOH FARRIJ WAJDI,2025.0,mencari_kerja,,,1 - Founder,Data_2025_Tracer_study_D4TIF.csv,1 - Founder,founder,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210734,ANDRE KHARISMA DWI THANSA,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D4TIF.csv,"5 - Lainnya, Tuliskan (f1102) 4 - Freelance/Kerja Lepas",lain tulis f1102 freelance kerja lepas,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41232548,NOFAIRA AMIN,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41231543,Muhamad Nusayif Jassim,2025.0,mencari_kerja,4 - Wiraswasta/perusahaan sendiri,,,Data_2025_Tracer_study_D4TIF.csv,4 - Wiraswasta/perusahaan sendiri,wiraswasta usaha sendiri,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41211762,FIRMANSYAH FIKRIAWAN,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D4TIF.csv,"5 - Lainnya, Tuliskan (f1102) 4 - Freelance/Kerja Lepas",lain tulis f1102 freelance kerja lepas,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41212008,AFI ZAIN RAMADANA,2025.0,belum_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
21. Teknik Informatika (55301),E41210176,ADAM KARSAR NUGROHO,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIF.csv,,,tidak_berpengalaman
|
||||||
|
Teknik Informatika (55301),E41191405,ACHMAD AJI WICAKSONO,2025.0,belum_kerja,3 - Perusahaan swasta,4 - Freelance/Kerja Lepas,1 - Lokal/wilayah/wiraswasta tidak berbadan hukum,Data_2025_Tracer_study_D4TIF.csv,3 - Perusahaan swasta 4 - Freelance/Kerja Lepas 1 - Lokal/wilayah/wiraswasta tidak berbadan hukum,usaha swasta freelance kerja lepas lokal wilayah wiraswasta badan hukum,tidak_berpengalaman
|
||||||
|
Teknik Informatika (55301),E41191211,TEGAR AR RASYIID SAIFULAH,2025.0,belum_kerja,Usaha Sampingan,1 - Founder,1 - Lokal/wilayah/wiraswasta tidak berbadan hukum,Data_2025_Tracer_study_D4TIF.csv,Usaha Sampingan 1 - Founder 1 - Lokal/wilayah/wiraswasta tidak berbadan hukum,usaha samping founder lokal wilayah wiraswasta badan hukum,tidak_berpengalaman
|
||||||
|
Teknik Informatika (55301),E41201609,BOY DYMAS HIDAYAT,2025.0,belum_kerja,4 - Wiraswasta/perusahaan sendiri,3 - Staff,1 - Lokal/wilayah/wiraswasta tidak berbadan hukum,Data_2025_Tracer_study_D4TIF.csv,4 - Wiraswasta/perusahaan sendiri 3 - Staff 1 - Lokal/wilayah/wiraswasta tidak berbadan hukum,wiraswasta usaha sendiri staff lokal wilayah wiraswasta badan hukum,tidak_berpengalaman
|
||||||
|
Teknik Informatika (55301),E41232136,SILFI GABRILIA CHOIRUN NISA,2025.0,belum_kerja,3 - Perusahaan swasta,3 - Staff,,Data_2025_Tracer_study_D4TIF.csv,3 - Perusahaan swasta 3 - Staff,usaha swasta staff,tidak_berpengalaman
|
||||||
|
Teknik Informatika (55301),E41232098,Moch. Iqbal Rosyidi,2025.0,belum_kerja,tidak bekerja,4 - Freelance/Kerja Lepas,,Data_2025_Tracer_study_D4TIF.csv,tidak bekerja 4 - Freelance/Kerja Lepas,kerja freelance kerja lepas,tidak_berpengalaman
|
||||||
|
22. Teknik Informatika Kampus Kab. Nganjuk (55302),E41211358,DAFFA ADITYA REJASA RUSWANTO,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFNGANJUK.csv,,,tidak_berpengalaman
|
||||||
|
22. Teknik Informatika Kampus Kab. Nganjuk (55302),E41210291,SANGGA DEWA RAMADHAN,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFNGANJUK.csv,,,tidak_berpengalaman
|
||||||
|
22. Teknik Informatika Kampus Kab. Nganjuk (55302),E41211399,ERLYNA BERLIAN PRASTITI,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFNGANJUK.csv,,,tidak_berpengalaman
|
||||||
|
22. Teknik Informatika Kampus Kab. Nganjuk (55302),E41211493,ENDIENING NUR PUSPITASARI,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFNGANJUK.csv,,,tidak_berpengalaman
|
||||||
|
22. Teknik Informatika Kampus Kab. Nganjuk (55302),E41211253,ENGGAR SUSMITA,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",,,Data_2025_Tracer_study_D4TIFNGANJUK.csv,"5 - Lainnya, Tuliskan (f1102)",lain tulis f1102,tidak_berpengalaman
|
||||||
|
22. Teknik Informatika Kampus Kab. Nganjuk (55302),E41212020,MOCHAMAD FARIS HASAN FEBRIANSHAH,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFNGANJUK.csv,,,tidak_berpengalaman
|
||||||
|
22. Teknik Informatika Kampus Kab. Nganjuk (55302),E41212241,RAKHA ARIAN ARCHANIGA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFNGANJUK.csv,,,tidak_berpengalaman
|
||||||
|
22. Teknik Informatika Kampus Kab. Nganjuk (55302),E41212173,PRISCILIA HILDA AGUSTIN,2025.0,mencari_kerja,,,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D4TIFNGANJUK.csv,4 - Freelance/Kerja Lepas,freelance kerja lepas,tidak_berpengalaman
|
||||||
|
22. Teknik Informatika Kampus Kab. Nganjuk (55302),E41212036,AHMAD CHOIRUL UMAM ALI ROZAQI,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",,,Data_2025_Tracer_study_D4TIFNGANJUK.csv,"5 - Lainnya, Tuliskan (f1102)",lain tulis f1102,tidak_berpengalaman
|
||||||
|
22. Teknik Informatika Kampus Kab. Nganjuk (55302),E41212313,M. BRILLIANT RIDHO SISWANTO,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFNGANJUK.csv,,,tidak_berpengalaman
|
||||||
|
22. Teknik Informatika Kampus Kab. Nganjuk (55302),E41212290,DIMAS ADI PUTRA ARIFIN,2025.0,mencari_kerja,7 - Institusi/Organisasi Multilateral,,,Data_2025_Tracer_study_D4TIFNGANJUK.csv,7 - Institusi/Organisasi Multilateral,institusi organisasi multilateral,tidak_berpengalaman
|
||||||
|
22. Teknik Informatika Kampus Kab. Nganjuk (55302),e41212053,RENGGA DWI PRIBADI,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFNGANJUK.csv,,,tidak_berpengalaman
|
||||||
|
22. Teknik Informatika Kampus Kab. Nganjuk (55302),E41211051,HABIB ANDRIANTITO,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFNGANJUK.csv,,,tidak_berpengalaman
|
||||||
|
22. Teknik Informatika Kampus Kab. Nganjuk (55302),E41211995,Wijang Afdholanabil Kusuma,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFNGANJUK.csv,,,tidak_berpengalaman
|
||||||
|
22. Teknik Informatika Kampus Kab. Nganjuk (55302),E41211993,RATIH DEWI SETIANI,2025.0,mencari_kerja,4 - Wiraswasta/perusahaan sendiri,-,3 - Staff,Data_2025_Tracer_study_D4TIFNGANJUK.csv,4 - Wiraswasta/perusahaan sendiri - 3 - Staff,wiraswasta usaha sendiri staff,tidak_berpengalaman
|
||||||
|
22. Teknik Informatika Kampus Kab. Nganjuk (55302),E41212331,CORNELIA RAHMAWATI,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFNGANJUK.csv,,,tidak_berpengalaman
|
||||||
|
22. Teknik Informatika Kampus Kab. Nganjuk (55302),E41212136,ROSITA CATUR SEPTIANINGSIH,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFNGANJUK.csv,,,tidak_berpengalaman
|
||||||
|
22. Teknik Informatika Kampus Kab. Nganjuk (55302),E41212095,MOCHAMAD YUNUS IKHSANODIN,2025.0,mencari_kerja,3 - Perusahaan swasta,,,Data_2025_Tracer_study_D4TIFNGANJUK.csv,3 - Perusahaan swasta,usaha swasta,tidak_berpengalaman
|
||||||
|
22. Teknik Informatika Kampus Kab. Nganjuk (55302),E41212133,Naufal Kadhafi,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFNGANJUK.csv,,,tidak_berpengalaman
|
||||||
|
22. Teknik Informatika Kampus Kab. Nganjuk (55302),E41210983,ROSYIDA ARIAN RAHMA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFNGANJUK.csv,,,tidak_berpengalaman
|
||||||
|
22. Teknik Informatika Kampus Kab. Nganjuk (55302),E41211992,FAUZIAN AQZZAL HAFIDZ,2025.0,mencari_kerja,3 - Perusahaan swasta,Polije,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D4TIFNGANJUK.csv,3 - Perusahaan swasta Polije 4 - Freelance/Kerja Lepas,usaha swasta polije freelance kerja lepas,tidak_berpengalaman
|
||||||
|
22. Teknik Informatika Kampus Kab. Nganjuk (55302),E41212268,BAGUS HARIS SUHARTONO,2025.0,mencari_kerja,3 - Perusahaan swasta,Starpace,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D4TIFNGANJUK.csv,3 - Perusahaan swasta Starpace 4 - Freelance/Kerja Lepas,usaha swasta starpace freelance kerja lepas,tidak_berpengalaman
|
||||||
|
22. Teknik Informatika Kampus Kab. Nganjuk (55302),E41211384,AHMAD EDZAM PRASETYA,2025.0,mencari_kerja,3 - Perusahaan swasta,Politeknik Negeri Jember,1 - Founder,Data_2025_Tracer_study_D4TIFNGANJUK.csv,3 - Perusahaan swasta Politeknik Negeri Jember 1 - Founder,usaha swasta politeknik negeri jember founder,tidak_berpengalaman
|
||||||
|
22. Teknik Informatika Kampus Kab. Nganjuk (55302),E41211952,NOVA NUR ANGGRAINI,2025.0,belum_kerja,1 - Instansi pemerintah,Komisi Pemilihan Umum,3 - Staff,Data_2025_Tracer_study_D4TIFNGANJUK.csv,1 - Instansi pemerintah Komisi Pemilihan Umum 3 - Staff,instansi perintah komisi pilih umum staff,tidak_berpengalaman
|
||||||
|
23. Teknik Informatika Kampus Kab. Sidoarjo (55304),E41212013,ABHINAYA FAHAR LAILA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFSIDOARJO.csv,,,tidak_berpengalaman
|
||||||
|
23. Teknik Informatika Kampus Kab. Sidoarjo (55304),E41212046,Achmad Baihaqi,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFSIDOARJO.csv,,,tidak_berpengalaman
|
||||||
|
23. Teknik Informatika Kampus Kab. Sidoarjo (55304),E41211994,ACH. FASIHUL LISAN,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFSIDOARJO.csv,,,tidak_berpengalaman
|
||||||
|
23. Teknik Informatika Kampus Kab. Sidoarjo (55304),E41212260,RIZQI AZIZISSANI,2025.0,mencari_kerja,3 - Perusahaan swasta,,,Data_2025_Tracer_study_D4TIFSIDOARJO.csv,3 - Perusahaan swasta,usaha swasta,tidak_berpengalaman
|
||||||
|
23. Teknik Informatika Kampus Kab. Sidoarjo (55304),E41212060,OCTAVIAN DAVA PUTRA CAHYONO,2025.0,mencari_kerja,,,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D4TIFSIDOARJO.csv,4 - Freelance/Kerja Lepas,freelance kerja lepas,tidak_berpengalaman
|
||||||
|
23. Teknik Informatika Kampus Kab. Sidoarjo (55304),E41212099,KENNYO GENDIS PUTRI HARLI,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFSIDOARJO.csv,,,tidak_berpengalaman
|
||||||
|
23. Teknik Informatika Kampus Kab. Sidoarjo (55304),E41212139,JESSICA DESI IMELDA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFSIDOARJO.csv,,,tidak_berpengalaman
|
||||||
|
23. Teknik Informatika Kampus Kab. Sidoarjo (55304),E41212081,CAHAYA CITRA AZZAHRO,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFSIDOARJO.csv,,,tidak_berpengalaman
|
||||||
|
23. Teknik Informatika Kampus Kab. Sidoarjo (55304),E41212058,MOCH GHAZY AL GHIFARI HAFID,2025.0,mencari_kerja,4 - Wiraswasta/perusahaan sendiri,,,Data_2025_Tracer_study_D4TIFSIDOARJO.csv,4 - Wiraswasta/perusahaan sendiri,wiraswasta usaha sendiri,tidak_berpengalaman
|
||||||
|
23. Teknik Informatika Kampus Kab. Sidoarjo (55304),E41212126,MARZUKI AKMAL,2025.0,mencari_kerja,4 - Wiraswasta/perusahaan sendiri,,,Data_2025_Tracer_study_D4TIFSIDOARJO.csv,4 - Wiraswasta/perusahaan sendiri,wiraswasta usaha sendiri,tidak_berpengalaman
|
||||||
|
23. Teknik Informatika Kampus Kab. Sidoarjo (55304),E41212044,DAFFA AGUNG NUGROHO,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFSIDOARJO.csv,,,tidak_berpengalaman
|
||||||
|
23. Teknik Informatika Kampus Kab. Sidoarjo (55304),E41212358,MUHAMMAD ANDRA KUSUMA RAMADHAN,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFSIDOARJO.csv,,,tidak_berpengalaman
|
||||||
|
23. Teknik Informatika Kampus Kab. Sidoarjo (55304),E41212045,ILHAM GUSTI SYAH PUTRO,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",,,Data_2025_Tracer_study_D4TIFSIDOARJO.csv,"5 - Lainnya, Tuliskan (f1102)",lain tulis f1102,tidak_berpengalaman
|
||||||
|
23. Teknik Informatika Kampus Kab. Sidoarjo (55304),E41212055,JOHAN KRISBIMA ABI,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFSIDOARJO.csv,,,tidak_berpengalaman
|
||||||
|
23. Teknik Informatika Kampus Kab. Sidoarjo (55304),E41212037,BILAL SHANDYARTA SYAMSUDIN,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFSIDOARJO.csv,,,tidak_berpengalaman
|
||||||
|
23. Teknik Informatika Kampus Kab. Sidoarjo (55304),E41212026,BIMA PRAYOGA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFSIDOARJO.csv,,,tidak_berpengalaman
|
||||||
|
23. Teknik Informatika Kampus Kab. Sidoarjo (55304),E41212359,OWEN PRATAMA ENDRAMAWAN,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFSIDOARJO.csv,,,tidak_berpengalaman
|
||||||
|
23. Teknik Informatika Kampus Kab. Sidoarjo (55304),E41212250,HERLAMBANG SATRIA WIJAYA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFSIDOARJO.csv,,,tidak_berpengalaman
|
||||||
|
23. Teknik Informatika Kampus Kab. Sidoarjo (55304),E41212129,NABILA RAHMA YUSRILFA TRISYAYANTI,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFSIDOARJO.csv,,,tidak_berpengalaman
|
||||||
|
23. Teknik Informatika Kampus Kab. Sidoarjo (55304),E41212063,ASMIRANTI,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFSIDOARJO.csv,,,tidak_berpengalaman
|
||||||
|
23. Teknik Informatika Kampus Kab. Sidoarjo (55304),E41212079,FADLIL LIWAUL HAMDI,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFSIDOARJO.csv,,,tidak_berpengalaman
|
||||||
|
23. Teknik Informatika Kampus Kab. Sidoarjo (55304),E41212006,MOCHAMMAD ENRIQUE LAZUARDI RAMADANY,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",-,,Data_2025_Tracer_study_D4TIFSIDOARJO.csv,"5 - Lainnya, Tuliskan (f1102) -",lain tulis f1102,tidak_berpengalaman
|
||||||
|
23. Teknik Informatika Kampus Kab. Sidoarjo (55304),E41212125,DINA DWI ARIKA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFSIDOARJO.csv,,,tidak_berpengalaman
|
||||||
|
23. Teknik Informatika Kampus Kab. Sidoarjo (55304),E41212057,FACHRY RIZKY PRASETYA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFSIDOARJO.csv,,,tidak_berpengalaman
|
||||||
|
23. Teknik Informatika Kampus Kab. Sidoarjo (55304),E41212012,HARIS ASYSYAUQI,2025.0,mencari_kerja,3 - Perusahaan swasta,PT Rexline Engineering Indonesia,,Data_2025_Tracer_study_D4TIFSIDOARJO.csv,3 - Perusahaan swasta PT Rexline Engineering Indonesia,usaha swasta rexline engineering indonesia,tidak_berpengalaman
|
||||||
|
23. Teknik Informatika Kampus Kab. Sidoarjo (55304),E41212105,FATKHUL HIDAYAH,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFSIDOARJO.csv,,,tidak_berpengalaman
|
||||||
|
23. Teknik Informatika Kampus Kab. Sidoarjo (55304),E41211962,LYA NURUL ULLA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFSIDOARJO.csv,,,tidak_berpengalaman
|
||||||
|
23. Teknik Informatika Kampus Kab. Sidoarjo (55304),E41212270,MOH. FERDI ANDRIANSYAH,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFSIDOARJO.csv,,,tidak_berpengalaman
|
||||||
|
23. Teknik Informatika Kampus Kab. Sidoarjo (55304),E41212137,AGIL GILANG CHANDRA SAPUTRA,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFSIDOARJO.csv,,,tidak_berpengalaman
|
||||||
|
23. Teknik Informatika Kampus Kab. Sidoarjo (55304),E41212403,MUHAMMAD SHALAHUDIN AYUBI FIRMANSYAH,2025.0,mencari_kerja,"5 - Lainnya, Tuliskan (f1102)",belum bekerja,4 - Freelance/Kerja Lepas,Data_2025_Tracer_study_D4TIFSIDOARJO.csv,"5 - Lainnya, Tuliskan (f1102) belum bekerja 4 - Freelance/Kerja Lepas",lain tulis f1102 belum kerja freelance kerja lepas,tidak_berpengalaman
|
||||||
|
23. Teknik Informatika Kampus Kab. Sidoarjo (55304),E41212054,DYIONO,2025.0,mencari_kerja,,,,Data_2025_Tracer_study_D4TIFSIDOARJO.csv,,,tidak_berpengalaman
|
||||||
|
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
|
|
@ -0,0 +1,536 @@
|
||||||
|
nim,nama,kode_prodi,status_pekerjaan,cluster,teks_pekerjaan,kategori,match_profil,matched_keywords
|
||||||
|
E31160241,Rizki Firmansyah,57401.0,bekerja,0,nirlaba irsyad islamiyyah jember,C2,False,
|
||||||
|
E32160207,Afrizal Gita Putra Laksono,56401.0,bekerja,3,swasta nexwave,C2,False,
|
||||||
|
E31161703,Lilik Widayanti,57401.0,bekerja,0,perintah balai desa sumbermujur,C2,False,
|
||||||
|
E31160798,LUKMAN HARUN,57401.0,wiraswasta,0,wiraswasta barokah cell,C2,False,
|
||||||
|
E41150080,DAVID SUGIANTO,55301.0,bekerja,3,swasta tokopedia,C2,False,
|
||||||
|
E41151627,Ahmad Syamsul Arifin,55301.0,wiraswasta,2,wiraswasta dafidea technocraft,C0,True,tech
|
||||||
|
E31161033,Ayu Diyah Purwanti,57401.0,bekerja,0,bumn bank negara indonesia tbk,C2,False,
|
||||||
|
E41151085,Yunita Dwi Pujianti,55301.0,bekerja,1,nirlaba politeknik negeri jember,C1,False,
|
||||||
|
E41151776,Mahbubi Ilham Nafiulalam,55301.0,bekerja,3,swasta tjiwi kimia tbk,C2,False,
|
||||||
|
E31161241,Aulia Rafi Dika Pekasih,57401.0,bekerja,3,swasta yaffi solusi internasional,C2,False,
|
||||||
|
E31161816,Dhimas Ulfa Lathifah,57401.0,bekerja,3,swasta moro ayu cosmetindo,C2,False,
|
||||||
|
E41150057,DITA PUSPITA RAHMA PUTRI,55301.0,bekerja,3,swasta sentra vidya utama sevima,C2,False,
|
||||||
|
E41150559,RAYHAN SAVERO ARIL,55301.0,bekerja,3,swasta telkomsel,C2,False,
|
||||||
|
E32161301,Muhammad Isbi Romadhoni,56401.0,bekerja,0,swasta east west seed indonesia,C2,False,
|
||||||
|
E32160628,SANDY KURNIAWAN,56401.0,bekerja,3,swasta pelangi migas abadi,C0,True,lan
|
||||||
|
E32160392,MOHAMMAD FADHUR ROHMAN,56401.0,bekerja,3,swasta enggar media sentana,C2,False,
|
||||||
|
E32160891,MUHAMMAD ALY ROFIE,56401.0,bekerja,0,bumn asuransi jiwasraya,C2,False,
|
||||||
|
E41151925,Wahyu Kartini Putri Aditya,55301.0,bekerja,3,swasta yayasan imam syafi,C2,False,
|
||||||
|
E32161373,Yuni Syafarinda,56401.0,bekerja,0,swasta idn boarding school,C2,False,
|
||||||
|
E41171583 ,AHMAD FAKIH HASBULLAH,55301.0,bekerja,0,wiraswasta usaha bisnis kembang konten creator,C2,False,
|
||||||
|
E41171389 ,SEPTIAJI WARAGA FILA,55301.0,wiraswasta,2,wiraswasta lilik collection,C2,False,
|
||||||
|
E41171753 ,Rikky Ihza Pratama,55301.0,bekerja,0,bnpp maulid teknologi kreatif,C1,False,
|
||||||
|
E41172249 ,RENDHY PRATAMA PUTRA,55301.0,bekerja,1,nirlaba politeknik negeri jember,C1,False,
|
||||||
|
E31181391 ,AN NISSA SAFITRI,57401.0,bekerja,2,wiraswasta mustikatama,C2,False,
|
||||||
|
E32180649 ,DEWI MAGREZA ALFIONITA,56401.0,bekerja,0,bnpp appen,C2,False,
|
||||||
|
E41170896 ,SYAVINA OCTAVIA PARAHITA,55301.0,bekerja,0,perintah dinas sosial kabupaten jember,C2,False,
|
||||||
|
E32181095 ,Helen Uni Dewi Samima,56401.0,wiraswasta,0,wiraswasta jual umpan ikan,C2,False,
|
||||||
|
E31151415 ,DENNY SETIADY PRABOWO,57401.0,bekerja,2,wiraswasta forensmith,C1,False,
|
||||||
|
E32181826 ,YUNDA CHONITA,56401.0,bekerja,0,nirlaba arsenet global solusi,C2,False,
|
||||||
|
E41171926 ,YOSEF YOGA HIMAWAN,55301.0,bekerja,0,wiraswasta araya media,C2,False,
|
||||||
|
E32180259 ,YUSRON WIDAD SYARFAN,56401.0,bekerja,0,bumn lintas nusantara,C2,False,
|
||||||
|
E32180470 ,ACHMAD MAULUDI FATHONY,56401.0,bekerja,0,bnpp kantor bank,C2,False,
|
||||||
|
E41172060 ,ROKI PRASETYO ADI,55301.0,bekerja,0,bnpp smk pancasila jember,C2,False,
|
||||||
|
,KHOSNOL KHOTIMATUL ARIFAH,55301.0,bekerja,0,bumn maulid teknologi kreatif,C1,False,
|
||||||
|
E32181653 ,Firdaus Andy Muhammad,56401.0,wiraswasta,0,wiraswasta jual baju,C2,False,
|
||||||
|
E31181032 ,MAULIEN JULYA SELEBRITISH,57401.0,wiraswasta,2,wiraswasta wiraswasta,C2,False,
|
||||||
|
E31181280 ,WULAN CAHYA WANDANI,57401.0,bekerja,0,perintah diskominfo,C2,False,
|
||||||
|
E41171613 ,GALIH BAGUS PRAKASA,55301.0,bekerja,1,perintah politeknik negeri jember,C1,False,
|
||||||
|
E31182077 ,RIVALDY NUR IDRIS,57401.0,bekerja,0,wiraswasta polije,C2,False,
|
||||||
|
E31180387 ,KANISA SHEILA IN ALVIONITA,57401.0,bekerja,0,bnpp jtv,C2,False,
|
||||||
|
E41171308 ,BAWIK ARDIYAN RAMADHAN,55301.0,bekerja,0,bnpp techno,C0,True,tech
|
||||||
|
E32181021 ,MOH. YOGI FERNANDA,56401.0,bekerja,0,bnpp kcm,C2,False,
|
||||||
|
E41171749 ,ALI MANSUR,55301.0,bekerja,1,perintah politeknik negeri jember,C1,False,
|
||||||
|
E31180464 ,SEPTIYAN YULIONO,57401.0,bekerja,2,wiraswasta karpet,C2,False,
|
||||||
|
E31180691 ,AHMAD FARHAN ALI,57401.0,wiraswasta,2,wiraswasta dmsncloth,C2,False,
|
||||||
|
E31180638 ,NAILUL AZMI ZIDNI ILMA,57401.0,wiraswasta,2,wiraswasta atapbesi,C2,False,
|
||||||
|
E31180819 ,MOCH. IQBAL ROSYIDI,57401.0,melanjutkan_studi,0,wiraswasta kerja,C2,False,
|
||||||
|
E31180838 ,YUANA DWI PANGESTU,57401.0,bekerja,2,wiraswasta cafe,C2,False,
|
||||||
|
E32181637 ,MUHAMMAD ADITYA MARTONO,56401.0,bekerja,2,wiraswasta mie terror,C2,False,
|
||||||
|
E31182176 ,JOSANTHA MARSHEL VALENDRA,57401.0,bekerja,0,wiraswasta esolusindo,C2,False,
|
||||||
|
E41170291 ,ALI WAJHAH,55301.0,bekerja,0,wiraswasta jember muslim store,C2,False,
|
||||||
|
E41161315 ,RIFQI HAKIM ARIESDIANTO,55301.0,wiraswasta,2,wiraswasta king kalkun,C2,False,
|
||||||
|
E31181297 ,SASTI WIDIYAS SARI,57401.0,bekerja,0,wiraswasta rumah,C2,False,
|
||||||
|
E41172069 ,AKHLAQ KHAN,55301.0,bekerja,0,wiraswasta toko bang mansur,C2,False,
|
||||||
|
E41170916 ,MUHAMMAD KHOIRIRI,55301.0,wiraswasta,2,wiraswasta pemuda padi,C2,False,
|
||||||
|
E41170897 ,RIDI YOGA PRATAMA,55301.0,bekerja,0,nirlaba pondok pesantren ibnu katsir,C2,False,
|
||||||
|
E31160935 ,RISANG AYU LARASATI,57401.0,wiraswasta,2,wiraswasta pawon uti,C2,False,
|
||||||
|
E41171041 ,MAKHI HAKIM HAKIKI,55301.0,bekerja,1,perintah man jember,C2,False,
|
||||||
|
E32181772 ,MOH. NUR AHSAN,56401.0,bekerja,0,nirlaba zamzee computer,C2,False,
|
||||||
|
E32180529 ,MUHAMMAD NASRUL FATTAH,56401.0,bekerja,0,wiraswasta sahabat bangun,C2,False,
|
||||||
|
E32181708 ,APRILLIO DOVAN ARIESA,56401.0,melanjutkan_studi,0,wiraswasta toko azahra,C2,False,
|
||||||
|
E31181204 ,FARIZ GUSTAFIANTO,57401.0,wiraswasta,0,wiraswasta zayid persada,C2,False,
|
||||||
|
E31181830 ,DITA SHAFIRA WAHYUDI,57401.0,melanjutkan_studi,2,wiraswasta yudi net,C2,False,
|
||||||
|
E41171120 ,MUHAMMAD ARIEF RACHMAN MUTTAQIEN,55301.0,bekerja,1,perintah politeknik negeri jember,C1,False,
|
||||||
|
E31182013 ,ABDANIAL PRADITYA,56401.0,bekerja,3,swasta proxinet,C2,False,
|
||||||
|
E41172068 ,AFFAN TOHARI,55301.0,bekerja,0,bnpp bikea tekno,C2,False,
|
||||||
|
E31180187 ,PUSPITA SARI RAKHMATULLAH,57401.0,bekerja,1,perintah politeknik negeri jember,C1,False,
|
||||||
|
E31180222 ,ELLA WAHYUNINGTYAS,57401.0,bekerja,0,wiraswasta polije,C2,False,
|
||||||
|
E32180842 ,NURUL QOMARIYAH,56401.0,bekerja,0,bumn pdam bondowoso,C2,False,
|
||||||
|
E41171252 ,NIKO WAHYU FITRIANTO,55301.0,bekerja,0,swasta indomarco prismatama,C2,False,
|
||||||
|
E31181955 ,M. PANDU GAUTAMA WIRANAGARA,57401.0,bekerja,0,wiraswasta freelance,C2,False,
|
||||||
|
E31180112 ,INTA OKTA RESTANTI,57401.0,bekerja,0,bumn kopegtel,C2,False,
|
||||||
|
E32181801 ,REY DICO PERWINA AMADA N.,56401.0,bekerja,2,wiraswasta albertacommunity,C1,False,
|
||||||
|
E31180686 ,HIKMAH QUR'ANING TYAS,57401.0,bekerja,0,wiraswasta shopee,C2,False,
|
||||||
|
E41170204 ,DANI ARDIYANSYAH,55301.0,bekerja,0,bumn pertamina,C2,False,
|
||||||
|
E31180256 ,Alfira Rosalina Damela,57401.0,bekerja,0,bumn antam tbk,C2,False,
|
||||||
|
E41170873 ,ACHSANUL KHIZAM,55301.0,bekerja,0,bnpp jatilindo,C2,False,
|
||||||
|
E32180210 ,FIKRI HAIKAL,56401.0,bekerja,0,bnpp karya cipta mekatronika,C2,False,
|
||||||
|
E41170676 ,ANGGI TRIKUSUMA DEWI,55301.0,bekerja,0,wiraswasta avatar solution,C2,False,
|
||||||
|
E32182281 ,ROSITA B KASIPDANA,56401.0,bekerja,0,perintah kantor bupati,C2,False,
|
||||||
|
E41171452 ,ADY BAGUS SUGIH SUSANTO,55301.0,bekerja,0,swasta indomarco prismatama,C2,False,
|
||||||
|
E32160410 ,ANDRE KRISNA PRATAMA,56401.0,bekerja,3,swasta bima technologies,C2,False,
|
||||||
|
E31181496 ,MURPY SATRIYA NUGRAHA,57401.0,bekerja,0,swasta maulid teknologi kreatif maulid games,C1,False,
|
||||||
|
E31181929 ,Andaru Bagus Gandhy Nurwidigdo,57401.0,bekerja,3,swasta bencoolencoffee,C2,False,
|
||||||
|
E41171829 ,MEGA SILVIA,55301.0,bekerja,3,swasta mang sabay rumah makan,C2,False,
|
||||||
|
E32181586 ,HARFI KURNIA HADIPUTRA,56401.0,bekerja,0,perintah diskominfo bondowoso,C2,False,
|
||||||
|
E41170438 ,MOHAMAD RIZAL RAMLI,55301.0,bekerja,0,swasta xeno persada teknologi,C1,False,
|
||||||
|
E41170753 ,DHENI TEGUH PRAMONO,55301.0,wiraswasta,2,wiraswasta bajukita id,C1,False,
|
||||||
|
E31180825 ,KEVIN ADITYAPUTRA ADRIANSYAH,57401.0,wiraswasta,2,wiraswasta mitreka karsa,C1,False,
|
||||||
|
E41170740 ,ABDILANA MOHTALIA SAPUTRA,55301.0,bekerja,0,perintah rsud koesnadi bondowoso,C2,False,
|
||||||
|
E41170164 ,Rizkika Zakka Palindungan,55301.0,bekerja,0,swasta xeno persada teknologi,C1,False,
|
||||||
|
E31180427 ,REZA ROMADHONI ALWI,57401.0,bekerja,3,swasta usaha swasta,C2,False,
|
||||||
|
E32180687 ,FARHAN IRZYAM ARI GUNTARA,56401.0,wiraswasta,0,wiraswasta ataraxia group,C2,False,
|
||||||
|
E41190088 ,Dimas Fariztha Hadiyatullah,55301.0,bekerja,1,perintah politeknik negeri jember,C1,False,
|
||||||
|
E31181553 ,LADINA PUTRI ANDITYANDARI,57401.0,bekerja,3,swasta strawberry preschool and daycare,C2,False,
|
||||||
|
E41172253 ,Fahrizal Azi Ferdiansyah,55301.0,bekerja,3,swasta devivace,C2,False,
|
||||||
|
E31182056 ,MOHAMMAD NAZIL ZAINUDDIN,57401.0,bekerja,2,wiraswasta tich,C2,False,
|
||||||
|
E32180275 ,MOH HAIRUL ARIFIN,56401.0,wiraswasta,2,wiraswasta arifnet,C2,False,
|
||||||
|
E32180653 ,SHOFIAH NUR MAZIDA,56401.0,wiraswasta,0,wiraswasta rumah dannis,C2,False,
|
||||||
|
E41170809 ,KURNIA MUTIARA SEPTI,55301.0,wiraswasta,2,wiraswasta aara dekorasi,C2,False,
|
||||||
|
E41171994 ,SASQIA DWI ARTA NOVIA,55301.0,bekerja,0,bnpp bank mega syariah,C2,False,
|
||||||
|
E41171742 ,RIDWAN HANANTO AJI ARIFIN,55301.0,bekerja,0,wiraswasta prodevis team,C2,False,
|
||||||
|
E41171660 ,REFI TRI HIDAYATULLAH,55301.0,wiraswasta,0,wiraswasta jasa buat website,C1,False,
|
||||||
|
E31180926 ,MUSBIHATUL CHORIROH,57401.0,bekerja,0,perintah perintah kabupaten nganjuk,C2,False,
|
||||||
|
E31180696 ,RIZQIANA AMITASARI,57401.0,wiraswasta,0,wiraswasta toko baju,C2,False,
|
||||||
|
E41170926 ,FAHRIZA RAMADHANI PUTRA,55301.0,bekerja,0,perintah polda jatim,C2,False,
|
||||||
|
E31180892 ,RURY AGISTA ASMININGSIH,57401.0,bekerja,0,perintah diskominfo,C2,False,
|
||||||
|
E41170061 ,Elsa Manora Ramadania,55301.0,bekerja,1,wiraswasta politeknik negeri jember,C1,False,
|
||||||
|
E32181667 ,ROBERT MUHAJIR,56401.0,wiraswasta,2,wiraswasta osingart,C2,False,
|
||||||
|
E32180251 ,MIFTAHUL HUDA,56401.0,bekerja,0,perintah dinas komunikasi informatika,C2,False,
|
||||||
|
E31170392 ,WAHYU RAMA FIJAYANDI,57401.0,bekerja,3,swasta sinikopi,C2,False,
|
||||||
|
E31181091 ,CAMALLA SABRINA YUSVIAZAH,57401.0,bekerja,0,wiraswasta warung soto ayu,C2,False,
|
||||||
|
E41170241 ,INANT KHARISMA,55301.0,wiraswasta,0,wiraswasta digital,C1,False,
|
||||||
|
E41171111 ,MUHAMMAD IRFAN SHIDQI LAKSONO,55301.0,bekerja,1,bumn politeknik negeri jember,C1,False,
|
||||||
|
E31180949 ,AKBAR IMAWAN DWI CAHYA,57401.0,bekerja,1,perintah politeknik negeri jember,C1,False,
|
||||||
|
E31181452 ,SITI ANISAH,57401.0,bekerja,0,perintah smp negeri wuluhan,C2,False,
|
||||||
|
E31181709 ,M ALI ROMADONI,57401.0,bekerja,3,swasta cybers global indonesia,C2,False,
|
||||||
|
E32181993 ,MALIK ABDUL AZIZ,56401.0,bekerja,0,perintah jaksa agung,C2,False,
|
||||||
|
E31180332 ,DEWI RATIH TILAMSARI WIBOWO,57401.0,bekerja,3,swasta mensa binasukses,C0,True,bi
|
||||||
|
E32181873 ,MOH GUFRON AZIZI,56401.0,bekerja,0,bumn kominfo,C2,False,
|
||||||
|
E32182153 ,Arshi Wisnu Wardhana,56401.0,bekerja,0,bnpp bsi,C2,False,
|
||||||
|
E32181449 ,MOCHAMMAD ALIF RAMADHANI,56401.0,bekerja,0,bnpp mitra tani,C1,False,
|
||||||
|
E41170090 ,Ari Baskara,55301.0,bekerja,0,wiraswasta freelance,C2,False,
|
||||||
|
E41171164 ,Adi Lukito,55301.0,bekerja,0,bnpp mitra surabaya,C1,False,
|
||||||
|
E31181436 ,NUR KUMALA SARI,57401.0,bekerja,0,bnpp kodig teknologi indonesia maju,C1,False,
|
||||||
|
E31180703 ,ANITA KURNIAWATI,57401.0,bekerja,0,bumn bank bni,C2,False,
|
||||||
|
E41171725 ,KHOSNOL KHOTIMATUL ARIFAH,55301.0,bekerja,0,bumn maulid teknologi kreatif,C1,False,
|
||||||
|
E41172165 ,MOCHAMMAD LEMBAR ADJIE BRAMANTYA,55301.0,bekerja,0,wiraswasta distributor skincare personal beauty,C2,False,
|
||||||
|
E32180234 ,Reza Firjatullah Rahman,56401.0,bekerja,0,bnpp asuransi ramayana,C2,False,
|
||||||
|
E32181238 ,FRENDY YASO MAYDALAN,56401.0,bekerja,0,bumn bumn,C2,False,
|
||||||
|
E32180323 ,GHALIB GIANARKAN BUSTIN,56401.0,bekerja,0,bnpp ptpn,C2,False,
|
||||||
|
E32180800 ,ANDI NURFANSYAH,56401.0,bekerja,0,bumn ptpn,C2,False,
|
||||||
|
E41171171 ,ATHO' FAJARIANTO,55301.0,bekerja,0,bnpp bumi suksesindo,C2,False,
|
||||||
|
E32181742 ,MOCHAMAD FIO BAITUL HUDA,56401.0,bekerja,0,bnpp art photography,C2,False,
|
||||||
|
E32181959 ,FERNANDA TERESIA VENTURIN,56401.0,bekerja,0,perintah bank bni,C2,False,
|
||||||
|
E41171092 ,GALANG PUTRA FENDRIANSYAH,55301.0,bekerja,0,bnpp denso,C2,False,
|
||||||
|
E32180381 ,MUHAMMAD RAFI,56401.0,bekerja,0,bumn pdam tirta balur,C2,False,
|
||||||
|
E41170735 ,DESTINO DEWANTARA PRATAMA,55301.0,bekerja,0,bumn bank bri,C2,False,
|
||||||
|
E32180897 ,RIFQI ABRORI,56401.0,bekerja,0,bnpp trans surabaya,C2,False,
|
||||||
|
E41170613 ,MONIKA KUSUMA DEWI,55301.0,bekerja,0,bnpp mitra umkm indomaret,C1,False,
|
||||||
|
E41170190 ,WINDA KURNIAWATI,55301.0,bekerja,1,perintah politeknik negeri jember,C1,False,
|
||||||
|
E32180459 ,RIFQIE BAGUS FIRMANSYAH,55301.0,bekerja,0,perintah dinas duduk,C2,False,
|
||||||
|
E32181934 ,FABIO QADAFI SUGESTI,56401.0,bekerja,0,bumn kai,C2,False,
|
||||||
|
E31182299 ,RISMA MULIYAWATI,57401.0,bekerja,0,perintah tri komunikasi informatika,C2,False,
|
||||||
|
E31180306 ,NOFITA OKTA SASMITA,57401.0,bekerja,0,bumn bps,C2,False,
|
||||||
|
E31180575 ,ELLEN MELINDA PUTRI ARIFIYANA SARI,57401.0,bekerja,0,wiraswasta luhan jaya,C2,False,
|
||||||
|
E32181133 ,AISYAH SASYADILAH,56401.0,bekerja,0,bnpp wings surya,C2,False,
|
||||||
|
E31182109 ,Rizaldy Putra Anam Novriandi,57401.0,bekerja,2,wiraswasta darmantika,C2,False,
|
||||||
|
E32180498 ,TARISA WINDI SUMONO PUTRI,56401.0,bekerja,0,wiraswasta piranhamas group,C2,False,
|
||||||
|
E32181345 ,ADE RIZKY CIPTA PRATAMA,56401.0,bekerja,0,bnpp pertamina,C2,False,
|
||||||
|
E31180303 ,YAHYA AJI WIJAYA,57401.0,bekerja,0,perintah multimedia edukasi,C2,False,
|
||||||
|
E41171621 ,BANGGA ADITYATAMA,55301.0,bekerja,0,perintah badan pegawai daerah,C2,False,
|
||||||
|
E41171118 ,FERNANDO FARISTA AHMAD,55301.0,bekerja,0,bnpp mitra,C1,False,
|
||||||
|
E32181811 ,ERINA SUDRAJAD,56401.0,bekerja,0,perintah smk pgri tanggul,C2,False,
|
||||||
|
E41171541 ,M. AVICENNA MAULA,55301.0,bekerja,0,wiraswasta mitra surabaya,C1,False,
|
||||||
|
E41171896 ,ADI CAHYA WIRATMAYA,55301.0,bekerja,0,perintah dinas komunikasi informatika bondowoso,C2,False,
|
||||||
|
E32182292 ,RAIZZAL AHAMAD WEIKIANSYAH,56401.0,bekerja,0,bnpp arsenet,C2,False,
|
||||||
|
E32181600 ,MUHAMMAD SHOLEHUDDIN,56401.0,bekerja,0,bnpp mekar jaya,C2,False,
|
||||||
|
E41170538 ,RIZKY MAULIDA,55301.0,bekerja,1,bnpp politeknik negeri jember,C1,False,
|
||||||
|
E31181131 ,SITI ANISA,57401.0,bekerja,0,wiraswasta multimedia edukasi,C2,False,
|
||||||
|
E31181991 ,MUHAMMAD ALIF MAHMUT,57401.0,wiraswasta,0,bnpp jual pakan sapi,C2,False,
|
||||||
|
E41170913 ,Yusuf Andi Nugroho,55301.0,bekerja,0,bnpp run systems,C2,False,
|
||||||
|
E31180884 ,ANITA SAFITRI,57401.0,bekerja,0,bumn dinas komunikasi informatika,C2,False,
|
||||||
|
E31181263 ,KARIMATUS SOFI'AH,57401.0,bekerja,0,perintah dinas perintah kabupaten situbondo,C1,False,
|
||||||
|
E31180299 ,DESY WAHYUNINGTYAS,57401.0,bekerja,0,bumn bri bank rakyat indonesia jember,C2,False,
|
||||||
|
E31180373 ,SITI KHOFSYAH,57401.0,bekerja,0,bumn grapari,C2,False,
|
||||||
|
E41171569 ,RISKA APRILIA,55301.0,bekerja,3,swasta content marketing labs,C1,False,
|
||||||
|
E31180365 ,KOMARIAH,57401.0,bekerja,1,wiraswasta politeknik negeri jember,C1,False,
|
||||||
|
E31180297 ,NURIN ASTIN VALIDA,57401.0,bekerja,1,wiraswasta politeknik negeri jember,C1,False,
|
||||||
|
E41170352 ,META GADIECHA WACHYUDI,55301.0,wiraswasta,0,wiraswasta freelance,C2,False,
|
||||||
|
E32180198 ,ENGGA BAGUS RIYANTO,56401.0,wiraswasta,2,wiraswasta desain,C1,False,
|
||||||
|
E31182030 ,Misbahus Surur,57401.0,bekerja,0,perintah operator gugus camat kalisat,C2,False,
|
||||||
|
E41171309 ,AL BUSRAN,55301.0,bekerja,3,swasta cmlabs malang,C2,False,
|
||||||
|
E41170885 ,RANGGA TRIANA PUTRA,55301.0,bekerja,3,swasta mascitra com,C1,False,
|
||||||
|
E31180644 ,DIAJENG SHINTA FEBRI RAHAYU,57401.0,bekerja,0,wiraswasta bagus kosmetik,C2,False,
|
||||||
|
E32181062 ,WAHYU ROMADHIANTO,56401.0,bekerja,3,swasta sumber dingin engineering,C1,False,
|
||||||
|
E41170203 ,KAHFINDA MULYA UTAMA PUTRA,55301.0,wiraswasta,2,wiraswasta acang handayani,C2,False,
|
||||||
|
E31160910 ,RIZKY AKBAR AUGUST PRADANA,57401.0,wiraswasta,0,wiraswasta toko afizah,C2,False,
|
||||||
|
E41170244 ,DAVID SETYA AINUR HAKIKI RAMADHAN,55301.0,bekerja,3,swasta digital,C1,False,
|
||||||
|
E32180585 ,BILLAL DAIFULLAH YAHYA AG,56401.0,bekerja,2,wiraswasta dispenduk,C2,False,
|
||||||
|
E31180276 ,MUHAMMAD FIRMAN AGE BIMANTARA,57401.0,wiraswasta,2,wiraswasta konter,C2,False,
|
||||||
|
E31181885 ,SOVIAH AYUNIAR MEGAFELANI PUTRI,57401.0,wiraswasta,3,swasta rental,C2,False,
|
||||||
|
E32180894 ,BONDAN YUWONO PUTRA,56401.0,bekerja,0,wiraswasta komputer solution,C0,True,komputer
|
||||||
|
E32181419 ,Roby Bayu Trisno Ananda,56401.0,bekerja,0,wiraswasta freelance,C0,True,lan
|
||||||
|
E32181940 ,ILHAM AKBAR JOVIANSYAH PUTRA,56401.0,bekerja,0,bumn telkom indonesia,C2,False,
|
||||||
|
E41170754 ,ALDY NOVERIANTO PRATAMA,55301.0,wiraswasta,2,wiraswasta tetas telur bebek peking,C2,False,
|
||||||
|
E32180760 ,ROBIUL ADAWI,56401.0,melanjutkan_studi,0,perintah pemda,C2,False,
|
||||||
|
E41170987 ,NOVIA NURUL QOMARIL,55301.0,wiraswasta,2,wiraswasta wiraswasta,C2,False,
|
||||||
|
E32182013 ,ABDANIAL PRADITYA,56401.0,bekerja,3,swasta proxi jaring nusantara,C2,False,
|
||||||
|
E32181862 ,BHIRAWA ANORAGA,56401.0,bekerja,0,wiraswasta arsenet global solusi,C2,False,
|
||||||
|
E41170641 ,AJI WAHYU PRASETYO,55301.0,wiraswasta,2,wiraswasta aji design,C1,False,
|
||||||
|
E41170827 ,AHMAD MUNIR,55301.0,wiraswasta,3,swasta jogotoko,C2,False,
|
||||||
|
E41172109 ,Nava Shoqibatul Khoiriyah,55301.0,bekerja,0,wiraswasta grace beauty,C2,False,
|
||||||
|
E41171956 ,DANA BHAKTI SURYA PRATIWI,55301.0,bekerja,2,wiraswasta shopie martin,C2,False,
|
||||||
|
E31180260 ,FAIZAL FIKRI HAMDANI,57401.0,bekerja,2,wiraswasta btin,C2,False,
|
||||||
|
E41170909 ,ADE SETIAWAN,55301.0,melanjutkan_studi,2,wiraswasta tidakada,C2,False,
|
||||||
|
E41170917 ,BUDI YUNIOR VALENTINO,55301.0,bekerja,2,wiraswasta rainbolt,C2,False,
|
||||||
|
E41171467 ,BAHTIAR ADI PRASETYA,55301.0,wiraswasta,2,wiraswasta adi disain,C2,False,
|
||||||
|
E32170824 ,MOH MUZAMMIL AL FARIZI,56401.0,bekerja,0,nirlaba ghofilin,C2,False,
|
||||||
|
E41171874 ,YUSUF TRI WIBOWO,55301.0,bekerja,3,swasta prodevis,C2,False,
|
||||||
|
E41170629 ,YANUAR ANUGRAH RAMADHANI,55301.0,wiraswasta,0,wiraswasta ruang kopi,C2,False,
|
||||||
|
E41171011 ,Gagas Adi Rismawan,55301.0,bekerja,0,wiraswasta digitalworks,C1,False,
|
||||||
|
E41171135 ,DWI AYU WULANDARI,55301.0,bekerja,0,wiraswasta digitalworks,C1,False,
|
||||||
|
E31161048 ,AHMAD FATKHUL ARIFIN,57401.0,bekerja,2,wiraswasta vatqul apprl,C2,False,
|
||||||
|
E32180069 ,MUHAMMAD RIZQI PRIYANSYAH,56401.0,bekerja,2,wiraswasta alberta community,C1,False,
|
||||||
|
E41171075 ,MOH. SYAFRIAN ABIE,55301.0,bekerja,2,wiraswasta naminauna institute,C1,False,
|
||||||
|
E32181970 ,FAIZHAL MAULANA IQBAL,56401.0,bekerja,0,swasta balai desa,C2,False,
|
||||||
|
E31180684 ,MELANIA RINIHANDITA NUR HABIBAH,57401.0,bekerja,0,wiraswasta olshop barokah,C2,False,
|
||||||
|
E31182235 ,HERNANDO FARAZI HERRERA,57401.0,bekerja,0,wiraswasta freelancer,C2,False,
|
||||||
|
E41171664 ,RAMADHAN IBNU UMAM,55301.0,wiraswasta,3,swasta glazzos studio,C2,False,
|
||||||
|
E31181567 ,AVIVAH NUR FAIZ MAGHFIROH,57401.0,melanjutkan_studi,0,bumn bulog,C2,False,
|
||||||
|
E31180748 ,DWINDA AYULIA EDITA SARI,57401.0,bekerja,2,wiraswasta dwdesign,C1,False,
|
||||||
|
E41171335 ,VINNY SAKINAH VIRGIO,55301.0,melanjutkan_studi,0,bumn belum kerja proses cari kerja,C2,False,
|
||||||
|
E41172064 ,SYAHDAN FIMAN HUDA,55301.0,wiraswasta,0,wiraswasta toko hitam putih,C1,False,
|
||||||
|
E31181597 ,MIRANI ILMI AURIA DEBITA,57401.0,bekerja,2,wiraswasta nimo,C2,False,
|
||||||
|
E31180207 ,MOHAMMAD BADAR PAMUNGKAS,57401.0,wiraswasta,0,wiraswasta onthel store jember,C2,False,
|
||||||
|
E32170601 ,LAILATUL QOMARIYAH,56401.0,bekerja,0,wiraswasta online shop,C2,False,
|
||||||
|
E32181659 ,MUHAMMAD WILDAN ILHAM MUBAROQ,56401.0,bekerja,0,wiraswasta barokah jaya,C2,False,
|
||||||
|
E32181610 ,SAHRIL SABIRIN,56401.0,wiraswasta,0,wiraswasta warung kopi,C2,False,
|
||||||
|
E32181421 ,SAHRUL SIHABUDIN,56401.0,bekerja,3,swasta budiawan,C0,True,wan
|
||||||
|
E32180272 ,HOFIDATUL AFIYAH,56401.0,bekerja,3,swasta kaloper,C2,False,
|
||||||
|
E32180279 ,NOVA AYU LESTARI,56401.0,bekerja,3,swasta kaloper,C2,False,
|
||||||
|
E32180282 ,TUTIK ERDIANA,55301.0,wiraswasta,2,wiraswasta warunng,C2,False,
|
||||||
|
E32180197 ,WAHYUNINGSIH,56401.0,melanjutkan_studi,0,wiraswasta yuni shop,C2,False,
|
||||||
|
E31180837 ,MIFTAH RIZQI AZIZAH,57401.0,wiraswasta,2,wiraswasta semicolon,C2,False,
|
||||||
|
E32181237 ,AGUNG TRI LEKSONO,55301.0,bekerja,0,wiraswasta agung,C2,False,
|
||||||
|
E31182133 ,MICHEL MEIRISCA HENIATY,57401.0,bekerja,3,swasta esolusindo,C2,False,
|
||||||
|
E31180504 ,PUTRI BUDIASIH,57401.0,bekerja,2,wiraswasta pizzaku,C2,False,
|
||||||
|
E31181100 ,RIWELLANANDA NANDA URBANING PUTRI,57401.0,bekerja,0,bumn pdam,C2,False,
|
||||||
|
E31180678 ,VENNA ERSIS RELITA,57401.0,bekerja,0,wiraswasta global,C2,False,
|
||||||
|
E31180456 ,RISKI EKA SAPUTRI,57401.0,bekerja,0,perintah bps jember pengentry,C2,False,
|
||||||
|
E32180231 ,RAUDATUR RIZKY ELOK BARLIAN,56401.0,melanjutkan_studi,0,perintah kai,C2,False,
|
||||||
|
E32180205 ,DAUD ARAGA ABABIL,56401.0,bekerja,0,nirlaba ast,C2,False,
|
||||||
|
E31180369 ,PUTRI RACHMAWATI,57401.0,wiraswasta,0,wiraswasta shop handcraft,C2,False,
|
||||||
|
E31180295 ,BUNAYYA MAULANA,57401.0,wiraswasta,0,wiraswasta shop handcraft,C2,False,
|
||||||
|
E41171142 ,KEVIN HARLIS OKTAVIANO,55301.0,wiraswasta,3,swasta company,C2,False,
|
||||||
|
E31181971 ,SALSABILA EKSANANDA,57401.0,bekerja,0,wiraswasta freelancer,C2,False,
|
||||||
|
E32180263 ,DAVA KUSUMA ARDANA,56401.0,bekerja,0,wiraswasta blackwhite idn,C1,False,
|
||||||
|
E31181976 ,THALIA TRIVIRGIANI EFELLYN,57401.0,melanjutkan_studi,0,perintah belum kerja,C2,False,
|
||||||
|
E32182194 ,SHOFYAN ASTSAURI,56401.0,melanjutkan_studi,2,wiraswasta ivan record,C2,False,
|
||||||
|
E31181492 ,KEVIN RAHANDIKA W,55301.0,wiraswasta,1,wiraswasta juragan guppy jember,C2,False,
|
||||||
|
E32180292 ,ZIDQI YOSA DINATA,56401.0,wiraswasta,0,perintah sujud company,C2,False,
|
||||||
|
E31180370 ,AIDATUL FAKHIRAH,57401.0,wiraswasta,0,wiraswasta usaha umkm,C2,False,
|
||||||
|
E41172092 ,MOCH. ZAINUR ROFAN FANNANI,55301.0,bekerja,2,wiraswasta rofil fotocopy,C2,False,
|
||||||
|
E31182101 ,Arfanio Liswanda Bahtiar,57401.0,wiraswasta,2,wiraswasta plant me,C2,False,
|
||||||
|
E31181474 ,CHOIRUL ANAM,57401.0,wiraswasta,2,wiraswasta cho pet,C2,False,
|
||||||
|
E31180707 ,MUHAMMAD ALI WAFA RAMADHAN,57401.0,wiraswasta,3,swasta service,C2,False,
|
||||||
|
E32181375 ,ALIEF PASHA IBRAHIM,56401.0,wiraswasta,2,wiraswasta alya catering,C2,False,
|
||||||
|
E32180067 ,Khalid Fikri Mustanir,56401.0,wiraswasta,2,wiraswasta mustanirbookstore,C2,False,
|
||||||
|
E41170972 ,ISMU UBAIDILLAH PANATAGAMA,55301.0,bekerja,0,wiraswasta bikea techno,C0,True,tech
|
||||||
|
E32181448 ,FRIDO ARJUNA PUTRA,56401.0,bekerja,2,wiraswasta c steamstore,C2,False,
|
||||||
|
E31180491,IMFI NUR FAIZIYAH,57401.0,wiraswasta,0,wiraswasta corp indonesia,C2,False,
|
||||||
|
E32190194,MUHAMMAD TAFANI AKBAR,56401.0,bekerja,2,wiraswasta panverta cakrakencana,C2,False,
|
||||||
|
E41181641,SALMA FARHANI,55301.0,bekerja,1,perintah politeknik negeri jember,C1,False,
|
||||||
|
E41181324,PRAVASTA GENTA ARJUNANDA KINANTAKA,55301.0,bekerja,0,wiraswasta benih citra asia,C1,False,
|
||||||
|
E41190280,WAHYU DIAN PRATIWI,55301.0,bekerja,0,swasta grant surya pondasi,C2,False,
|
||||||
|
E31192202,Yulia Bagas Sri Dastiya,57401.0,bekerja,3,swasta margojoyo residence,C2,False,
|
||||||
|
E41192227,VIKY LORENT SEA PUTRA,55301.0,bekerja,0,bnpp madura hosting,C2,False,
|
||||||
|
E41191426,SHINTA DESTIRA AYU,55301.0,bekerja,0,perintah lee yin garment indonesia,C2,False,
|
||||||
|
E41191107,DWI RIFKI NOVIANTO,55301.0,bekerja,3,swasta clade ventures,C2,False,
|
||||||
|
E41190530,ALIF IRHASSHOUFI NASIHUL UMAM,55301.0,bekerja,0,swasta telkom indonesia tbk,C2,False,
|
||||||
|
E41191417,AGIN PRAHAMESTI WICAHYANI,55301.0,bekerja,0,wiraswasta andri farm,C2,False,
|
||||||
|
E41192205,MUHAMMAD KHALIL ZHILLULLAH,55301.0,bekerja,3,swasta lestasi sentosa,C2,False,
|
||||||
|
E41190200,NANDA RADITIYA AKBAR,55301.0,melanjutkan_studi,0,swasta dinas tani,C2,False,
|
||||||
|
E41192269,SULTON MUBAROK,55301.0,bekerja,0,nirlaba dinas tani,C2,False,
|
||||||
|
E41190532,MOH. SYAIFUDIN,55301.0,bekerja,0,bumn upt kembang agribisnis tanam pangan hortikultura,C2,False,
|
||||||
|
E41190239,Mohammad Hidayatullah,55301.0,bekerja,0,bumn alir,C2,False,
|
||||||
|
E41190321,NURICO VICYYANTO,55301.0,bekerja,3,swasta bpn,C2,False,
|
||||||
|
E32200380,Miftahus Surur Sutowo,56401.0,melanjutkan_studi,3,swasta nutrifood,C2,False,
|
||||||
|
E41190835,ARINI EKA NOVIYANTI,55301.0,wiraswasta,0,perintah ourscarf,C2,False,
|
||||||
|
E31201647,FIRDAN MAHENDRA WARDANA,57401.0,wiraswasta,0,bnpp ritz bakery,C1,False,
|
||||||
|
E41190890,FATHOR RAHMAN,55301.0,bekerja,0,perintah menteri,C2,False,
|
||||||
|
E41190937,MOH ISKANDAR ZULKARNAIN,55301.0,bekerja,0,swasta badan koordinasi wilayah perintah bangun provinsi jawa timur,C2,False,
|
||||||
|
E41191351,THUFEIL WARTONO PUTRA,55301.0,bekerja,2,wiraswasta sekretariat dprd,C2,False,
|
||||||
|
E41190793,M. AINUL YAQIN,55301.0,bekerja,0,bnpp bumi sukses indo,C2,False,
|
||||||
|
E31200857,ERICK ASRILLAH PRATAMA,57401.0,melanjutkan_studi,0,bumn pusat teliti kopi cacao,C1,False,
|
||||||
|
E41192478,INDRA PRASETYA,55301.0,bekerja,3,swasta mulya jaya,C2,False,
|
||||||
|
E32190274,RIZKY ADIWIYANTO,56401.0,bekerja,0,wiraswasta asw food,C2,False,
|
||||||
|
E41191322,BUNGA PRASETYA DWI ULUL AZMI,55301.0,bekerja,3,swasta yayasan darul mukmin,C2,False,
|
||||||
|
E41190889,MUHAMMAD LUTFI FADILAH,55301.0,bekerja,0,perintah bank bni,C2,False,
|
||||||
|
E41191560,Anggito Suryo Baskoro,55301.0,bekerja,3,swasta frawn network,C2,False,
|
||||||
|
E41190161,Ardan Venora Falahudin,55301.0,bekerja,1,swasta politeknik negeri jember,C1,False,
|
||||||
|
E41190942,DEFI TAMARA,55301.0,wiraswasta,0,wiraswasta kai,C2,False,
|
||||||
|
E41192386,FADLI ROMADHAN,55301.0,bekerja,0,perintah charoen pokphand indonesia,C2,False,
|
||||||
|
E41191090,AMALIA,55301.0,bekerja,0,perintah tunggal griya sakinah,C2,False,
|
||||||
|
E41191468,ABDUL AZIZ RAHMAWAN,55301.0,bekerja,0,wiraswasta bejana investidata globalindo,C1,False,
|
||||||
|
E41191151,YULINAR NUR RAHMAWATI,55301.0,wiraswasta,0,bumn ptpn asembagus,C2,False,
|
||||||
|
E41191443,RISKIE NUR FADILAH,55301.0,bekerja,0,bumn pltu,C2,False,
|
||||||
|
E41192329,Alga Saputra,55301.0,bekerja,3,swasta sumber tunjung,C2,False,
|
||||||
|
E31200061,PUJA MAYSA ARYA CUCU WIBOWO,57401.0,wiraswasta,0,bumn makmur jaya,C2,False,
|
||||||
|
E31201706,EKA FAHRIKA NARISWARI,57401.0,bekerja,3,swasta gas pol dev,C2,False,
|
||||||
|
E31201573,ERLINDA KRISTANTI PUTRI,57401.0,bekerja,0,swasta utrani farm,C2,False,
|
||||||
|
E41190160,MITA WAFA WULANDARI,55301.0,bekerja,0,swasta kantor desa,C2,False,
|
||||||
|
E41192303,RIFJAN JUNDILA,55301.0,bekerja,0,wiraswasta sumber mutiara samudra banyuwangi,C2,False,
|
||||||
|
E41190236,DIMAS HERU PERDANA PUTRA,55301.0,bekerja,0,wiraswasta dinas tani pangan,C2,False,
|
||||||
|
E41190179,RANDY RAHMAWAN,55301.0,bekerja,0,wiraswasta dinas tani pangan,C2,False,
|
||||||
|
E41192406,NOVITA SARI,55301.0,bekerja,0,swasta rumah sakit,C1,False,
|
||||||
|
E41190668,PRISKA ARLINDA SABILLAH,55301.0,bekerja,0,wiraswasta motor,C2,False,
|
||||||
|
E41191326,WIKE SRI WIDARI,55301.0,wiraswasta,0,wiraswasta mitra tani,C1,False,
|
||||||
|
E41191510,ERIK RIZKI FIRDAUS,55301.0,bekerja,0,perintah pltu,C2,False,
|
||||||
|
E41192131,FEBRI PUTRA TRIS BUDIONO,55301.0,bekerja,3,swasta intprism computing technology,C0,True,tech
|
||||||
|
E41191453,DANANG ARDIANTO,55301.0,bekerja,3,swasta hutan,C2,False,
|
||||||
|
E41191628,DIMAS PANGESTU,55301.0,bekerja,3,swasta manajer,C2,False,
|
||||||
|
E41191382,MUHAMMAD NUZUL RIDOI,55301.0,bekerja,3,swasta agro kusuma,C2,False,
|
||||||
|
E41192307,RIFKY ROUDANA IMANI CAHYA,55301.0,melanjutkan_studi,3,swasta masakan bunda,C2,False,
|
||||||
|
E41190318,MAULANA ALIRRIDLO,55301.0,bekerja,0,wiraswasta pemda,C2,False,
|
||||||
|
E41192287,SULISTYAWATI,55301.0,bekerja,0,wiraswasta lapak online,C2,False,
|
||||||
|
E31201887,DIMAS RIZQI AGUNG PRATAMA,57401.0,bekerja,2,wiraswasta wira,C2,False,
|
||||||
|
E41191336,Suci Riski Anggraini,55301.0,bekerja,0,perintah andatu mulia,C2,False,
|
||||||
|
E41191202,DEWI AGESTI NURIFIANI,55301.0,bekerja,0,perintah dinamix network automatika,C2,False,
|
||||||
|
E31201950,RIDHO ADILAH WASISTO,57401.0,bekerja,0,perintah dinas tani pangan kab banyuwangi,C2,False,
|
||||||
|
E31201932,IMDAD ALFI FAHMI,57401.0,bekerja,0,wiraswasta kai,C2,False,
|
||||||
|
E41192295,MARSELLA DWI FAIRA,55301.0,bekerja,0,bumn ptpn xii wonosari malang,C2,False,
|
||||||
|
E31202451,MUFID ADITYA DAFENTRI,57401.0,bekerja,0,swasta kopa tarutama nusantara,C2,False,
|
||||||
|
E31202314,DANI TAUFIKUL HAKIM,57401.0,bekerja,0,perintah bis,C0,True,bi
|
||||||
|
E41190493,VIGA ENSAS FII SABILILLAH,55301.0,wiraswasta,3,swasta suwito mesin diesel bekas,C1,False,
|
||||||
|
E41191570,SANG BINTANG PUTERA ALAM,55301.0,bekerja,3,swasta unilever,C2,False,
|
||||||
|
E41191387,IRCHAMZAH FIKRI ABABIL,55301.0,bekerja,0,wiraswasta bis,C2,False,
|
||||||
|
E32202398,MUHAMMAD SYAIFULLAH AL ARIEF,56401.0,bekerja,2,wiraswasta evyshopper,C2,False,
|
||||||
|
E31201246,MOCH. ANDRI,57401.0,bekerja,0,perintah socfindo,C2,False,
|
||||||
|
E41191280,HENDY SETIAWAN,55301.0,bekerja,3,swasta ida sentosa,C2,False,
|
||||||
|
E41190925,ANJU ALICIA JOHAN,55301.0,bekerja,0,perintah alf mechatronics innovation,C2,False,
|
||||||
|
E41192216,DWI NURANNISA RAHAYUNINGTIAS,55301.0,melanjutkan_studi,0,perintah sinar alam,C2,False,
|
||||||
|
E41191965,FIGO FAJAR YULIANTO,55301.0,bekerja,0,bumn grab,C2,False,
|
||||||
|
E41192233,FIRMAN ABDI SYAH PUTRA,55301.0,bekerja,2,wiraswasta jmsportcenter,C2,False,
|
||||||
|
E41190305,FIRMAN WAHYUDI,55301.0,wiraswasta,3,swasta rizky motor,C2,False,
|
||||||
|
E41190547,MUHAMMAD FADHIL NURHUDA,55301.0,wiraswasta,3,swasta terna,C2,False,
|
||||||
|
E41190755,DWI WAHYU PRAMBODO,55301.0,wiraswasta,0,swasta tretes view hotel,C2,False,
|
||||||
|
E32200978,DION ADI PANGESTU,56401.0,bekerja,3,swasta candya organizer,C2,False,
|
||||||
|
E41192320,AULIA AQSONIA ARIFAH,55301.0,bekerja,0,perintah indo marco,C2,False,
|
||||||
|
E41192114,MOH. ARDIAS ADE AGA,55301.0,bekerja,0,swasta pltu paiton,C1,False,
|
||||||
|
E41190139,FIRGO BHAKTIAR HAMSAH,55301.0,bekerja,0,swasta badan pusat statistik regsosek,C2,False,
|
||||||
|
E41202435,INTAN PERMATASARI,55301.0,bekerja,3,swasta cayaha tani,C2,False,
|
||||||
|
E41190614,DILLAH AMALIA FIQYANI,55301.0,bekerja,0,wiraswasta bkpsdm kab jember,C2,False,
|
||||||
|
E41192283,ARIF KURNIAWAN,55301.0,bekerja,3,swasta dinas prawisata,C2,False,
|
||||||
|
E41192271,AL FAHRELIA WAVA AGUSTIN,55301.0,bekerja,0,swasta pltu paiton,C1,False,
|
||||||
|
E41190152,ACHMAD KHALIFAH,55301.0,melanjutkan_studi,0,wiraswasta enz kosmetik,C2,False,
|
||||||
|
E31201954,BERLIANI FIA MAHARIS ARIKHALA,57401.0,bekerja,0,wiraswasta ikhwan jaya,C2,False,
|
||||||
|
E31200001,AFBILLA FITRIANI DEWI,57401.0,bekerja,0,wiraswasta pltu paiton,C1,False,
|
||||||
|
E41190503,DIAN BUANA PUTRA,55301.0,wiraswasta,3,swasta bal indah aquarium,C2,False,
|
||||||
|
E31200561,ALFINA DEWI HIDAYANTI,57401.0,bekerja,1,perintah politeknik negeri jember,C1,False,
|
||||||
|
E32201501,RICKY ANDRIAWAN,56401.0,bekerja,0,wiraswasta guna jaya,C2,False,
|
||||||
|
E32201226,ARINID DIANA OKTAVIA,56401.0,melanjutkan_studi,0,bumn rsud moch ansari saleh banjarmasin,C2,False,
|
||||||
|
E32200493,RAFLY KEMAL AVIYANTO,56401.0,bekerja,0,wiraswasta smks pgri banyuputih,C2,False,
|
||||||
|
E32200990,AQMALUH AGUNG PRADANA,56401.0,bekerja,0,perintah kantor perintah desa olean,C2,False,
|
||||||
|
E41191903,NADEA AJENG SAFITRI,55301.0,bekerja,0,perintah panca mitra multiperdana,C1,False,
|
||||||
|
E41191622,AGNES OCTAVIANDA ZAVIRA,55301.0,bekerja,2,wiraswasta artaboga,C2,False,
|
||||||
|
E31200680,FUAD HASYIM FANANI,57401.0,wiraswasta,0,wiraswasta marga solusi prima,C2,False,
|
||||||
|
E31201158,ALBANI KHAMENEI,57401.0,bekerja,0,swasta cipta karya buana,C2,False,
|
||||||
|
E41191188,ANISA PUJI ASTUTIK,55301.0,wiraswasta,3,swasta anugerah,C2,False,
|
||||||
|
E31200293,KALYANA UNASHA RAYHANDINI YUSUF,57401.0,wiraswasta,0,perintah imaji sociopreneur,C2,False,
|
||||||
|
E31201146,WIDYA PUTRI PRATAMA,57401.0,bekerja,0,wiraswasta badan dapat daerah,C2,False,
|
||||||
|
E31201687,SHAFA THALIAH,57401.0,bekerja,0,swasta badan dapat daerah kabupaten jember,C2,False,
|
||||||
|
E41192362,DIANA FITHRI LESTARI,55301.0,wiraswasta,3,swasta taihei dengyo indonesia,C2,False,
|
||||||
|
E41190739,RESA WAHYU SAPUTRA,55301.0,bekerja,0,bumn perum hutan pabrik gondorukem terpentin kbm ihh jatim non kayu,C2,False,
|
||||||
|
E41190125,ANGGUN WASILINA MUTRIFANI,55301.0,wiraswasta,0,swasta dinas hutan provinsi jawa timur,C2,False,
|
||||||
|
E32200993,ACHMAD NABILUL FAHMI,56401.0,bekerja,0,perintah shopee food,C2,False,
|
||||||
|
E32201292,DWI APRILIA HALIZA,56401.0,melanjutkan_studi,0,wiraswasta ksp jaya makmur,C2,False,
|
||||||
|
E32200536,ARFENDI PERMANA,56401.0,melanjutkan_studi,0,wiraswasta rofiq farm,C2,False,
|
||||||
|
E31201237,DYAS OCTA VIOLA,57401.0,bekerja,0,perintah basma olshopid,C2,False,
|
||||||
|
E32200614,AHMAD FAISOL,56401.0,bekerja,0,bnpp dinas tani pangan kabupaten banyuwangi,C0,True,wan
|
||||||
|
E31200837,DESIANA SILVIA RAMADANI,57401.0,wiraswasta,0,wiraswasta grant surya pondasi,C2,False,
|
||||||
|
E31202490,ADRIAN MEDTIYU NUR ARIFIN,57401.0,bekerja,0,swasta online shop jual pulsa,C2,False,
|
||||||
|
E32202011,MOHAMMAD AZWAR ANAS,56401.0,wiraswasta,2,wiraswasta ketik,C2,False,
|
||||||
|
E41191369,OKTAFIAN DWI CAHYONO,55301.0,bekerja,0,bnpp rsud gambir,C2,False,
|
||||||
|
E41190986,IPHANG RERE ADMAJA,55301.0,melanjutkan_studi,3,swasta indosat tbk,C2,False,
|
||||||
|
E41192426,MUHAMMAD SYARIF JANURIANSYAH,55301.0,bekerja,0,perintah puskesmas leces,C2,False,
|
||||||
|
E31202523,MOCHAMMAD ANDIKA ALSABFANDI,57401.0,bekerja,3,swasta bangun tani,C2,False,
|
||||||
|
E31201422,FIRMAN RIZKY FIRDAUS,57401.0,bekerja,2,wiraswasta caldera party planner,C2,False,
|
||||||
|
E32200618,WATI DARMAWAN,56401.0,bekerja,0,wiraswasta bank,C2,False,
|
||||||
|
E32202311,MUHAMMAD FARRAS WIRAWAN,56401.0,melanjutkan_studi,3,swasta fidjijaya,C2,False,
|
||||||
|
E41190322,Kana Farira Salsabila,55301.0,bekerja,0,perintah dinas tani pangan,C2,False,
|
||||||
|
E41191445,NAUFAL MAHDI RAHMAN UBAID,55301.0,bekerja,3,swasta mbah jan farm,C2,False,
|
||||||
|
E31201095,FITRI ARISKA,57401.0,bekerja,3,swasta suitmedia,C1,False,
|
||||||
|
E41192342,FAHREZA MAULANA WISNU WARDANA,55301.0,bekerja,0,swasta labbola sport statistic and data management,C1,False,
|
||||||
|
E41191325,WIDYA YURISTIKA OKTAVIA,55301.0,bekerja,0,wiraswasta dolan tour and travel,C2,False,
|
||||||
|
E32201384,BAYU SEPTIAN ADHI PRADANA,56401.0,bekerja,0,perintah rafli putra,C2,False,
|
||||||
|
E41191240,MOCHAMMAD FAJAR FITRIAN NURBAYU,55301.0,bekerja,3,swasta bpn,C2,False,
|
||||||
|
E32201402,ANJAR FITRIA,56401.0,melanjutkan_studi,3,swasta invi creative,C2,False,
|
||||||
|
E31201199,AJENG NUR KHORISA,57401.0,wiraswasta,0,bumn kusuma dinasastri wisatajaya,C2,False,
|
||||||
|
E41191558,AKHMAD NUR HIDAYATULLOH,55301.0,bekerja,0,bumn fkp maju,C2,False,
|
||||||
|
E41191157,MUHAMMAD AFDAL ZIKRI,55301.0,melanjutkan_studi,0,perintah klinik mitra sehat,C1,False,
|
||||||
|
E41192428,MUHAMMAD RIADI PRASETYO,55301.0,bekerja,0,wiraswasta purnama indonesia,C2,False,
|
||||||
|
E31201228,MOHAMAD RAFLI CAHAYA MAHENDRA,57401.0,bekerja,0,bumn asuransi central asia aca,C2,False,
|
||||||
|
E41190934,HAYUNG DEWANTO,55301.0,bekerja,0,swasta kantor balai desa bandarasri,C2,False,
|
||||||
|
E32202313,ALDO BAGAS SEPTYAWAN,56401.0,melanjutkan_studi,0,swasta bank rakyat indonesia,C2,False,
|
||||||
|
E32201641,AHMAD ASRORI,56401.0,bekerja,2,wiraswasta ppah mayapaga,C2,False,
|
||||||
|
E41191206,FITROH FATHUR ROHMAN,55301.0,bekerja,2,wiraswasta rifa offset,C2,False,
|
||||||
|
E41191132,SHOVI IMANY WARDIANA,55301.0,bekerja,3,swasta amat indonesia,C2,False,
|
||||||
|
E32200057,SEAN ARRAFES,56401.0,bekerja,0,bumn bank bni,C2,False,
|
||||||
|
E41191133,SHENILA LETISHYA RAMONA,55301.0,bekerja,1,bumn politeknik negeri jember,C1,False,
|
||||||
|
E41190878,MOCH. FAHRONY,55301.0,bekerja,0,perintah jaya sejahtera,C2,False,
|
||||||
|
E32200950,MOCHAMMAD REFLY AGUSTINO,56401.0,bekerja,0,perintah rumah produksi bordir selempangwisuda,C2,False,
|
||||||
|
E41191823,Adam Muhammad,55301.0,bekerja,0,nirlaba ptpn asembagus,C2,False,
|
||||||
|
E32200305,DZAKI JOMAN,56401.0,bekerja,0,wiraswasta event organizer,C2,False,
|
||||||
|
E32201058,MOCH. AL-FEND LABBIB,56401.0,bekerja,1,bnpp smk negeri jember,C2,False,
|
||||||
|
E32201216,EDO YUDHISTIRA PRATAMA,56401.0,bekerja,0,wiraswasta utrani farm,C2,False,
|
||||||
|
E32201586,NOVITA HESTI WULANSARI,56401.0,bekerja,3,swasta manik jaya,C2,False,
|
||||||
|
E31200102,Haris Faqih Darmawan,57401.0,bekerja,0,bnpp hutan,C2,False,
|
||||||
|
E31200864,NADILA DEVI TSABITA,57401.0,bekerja,0,wiraswasta dinas tahan kabupaten probolinggo,C2,False,
|
||||||
|
E41191343,REXY SOLEHUDIN ABDI HOLILI,55301.0,bekerja,1,wiraswasta sekolah perempuan jember,C2,False,
|
||||||
|
E31200284,NURLITA AYU RAKHMAWATI,57401.0,bekerja,0,bumn yourflorist,C2,False,
|
||||||
|
E31200251,M. BASAR RISKI,57401.0,bekerja,0,perintah bank bca,C2,False,
|
||||||
|
E41192340,BAGUS BACHTIAR RIZKI,55301.0,bekerja,0,wiraswasta sumber rezeki,C2,False,
|
||||||
|
E41190645,HENRY MURDIFUL ULYA HILMA PUTRA,55301.0,bekerja,3,swasta cita alam nusantara,C1,False,
|
||||||
|
E31201390,LIUTANTRI HOBIOLA,57401.0,melanjutkan_studi,0,swasta dinas pariwisata kabupaten lumajang,C2,False,
|
||||||
|
E41192366,MUHAMMAD AUDINO FAKHRI ARNANDYA,55301.0,bekerja,3,swasta unilever,C2,False,
|
||||||
|
E31200356,YOGA SAPUTRA R.,57401.0,wiraswasta,0,swasta dinas tani,C2,False,
|
||||||
|
E32201197,MIRZANDO DIRGANTAKA,56401.0,bekerja,0,bumn permana sound,C2,False,
|
||||||
|
E41191725,FITRIYAH AYU NINGSIH,55301.0,bekerja,0,wiraswasta mediatama group,C2,False,
|
||||||
|
E31202562,ZALFA ALIF NABILLA,57401.0,bekerja,0,swasta dinas tani pangan banyuwangi,C2,False,
|
||||||
|
E32201063,WAHYU RAHMATULLAH,56401.0,bekerja,0,bumn wirausaha rumah,C2,False,
|
||||||
|
E31200245,NUR LAILI WAHYUNINGTIAS,57401.0,bekerja,3,swasta terna,C2,False,
|
||||||
|
E41190391,RIZKYU ZAM ZAMI,55301.0,wiraswasta,0,swasta tretes view hotel,C2,False,
|
||||||
|
E32201430,ACHMAD RIZKY RAMADHANI,56401.0,melanjutkan_studi,3,swasta ichwan production,C0,True,wan
|
||||||
|
E41191868,NUR MUHAMMAD SYAHBANA R,55301.0,bekerja,3,swasta pama persada,C2,False,
|
||||||
|
E31201470,BEFRIL RENFILIANI SYAFITRI,57401.0,melanjutkan_studi,0,perintah rsud pasirian,C2,False,
|
||||||
|
E41190452,HAQIQI AHMAD SALMAN ALFARISI,55301.0,bekerja,0,wiraswasta semesta cipta lab,C2,False,
|
||||||
|
E32200040,DWI ARIF RAHMAN,56401.0,bekerja,0,bnpp igg,C2,False,
|
||||||
|
E41191902,FAIZ IQBAL DARMAWAN,55301.0,bekerja,3,swasta tani,C2,False,
|
||||||
|
E32201000,Rico Wildany Nasharillah,56401.0,bekerja,0,perintah pltu paiton,C1,False,
|
||||||
|
E32200185,BAYU CAHYA KHURNIA YUDHA,56401.0,bekerja,0,bumn bibit hortikultura sejahtera,C1,False,
|
||||||
|
E31202536,FADILAH NUR HAMSAH,57401.0,bekerja,0,wiraswasta badan pusat statistik regsosek,C2,False,
|
||||||
|
E32200069,VENTI WELIANA,56401.0,bekerja,0,perintah bumn,C2,False,
|
||||||
|
E31200997,FAJAR FITRA ABADI,57401.0,bekerja,0,nirlaba ytl,C2,False,
|
||||||
|
E31190748,KHOLIZAH RIFKA ATUL AIDA,57401.0,bekerja,2,wiraswasta krips,C2,False,
|
||||||
|
E32200589,SEPTIANANDA RIFQI NURHIDAYAT,56401.0,melanjutkan_studi,0,perintah bkpsdm kab jember,C2,False,
|
||||||
|
E31202394,DEVA ANGELA SABILA SASKI,57401.0,bekerja,0,perintah ptpn xii pandjie,C2,False,
|
||||||
|
E32201187,GHULAM MAULANA FAHRI,56401.0,bekerja,3,swasta freelance,C0,True,lan
|
||||||
|
E31201794,RAHMA ROMADONA AYU RISWANTI,57401.0,bekerja,1,bumn politeknik negeri jember,C1,False,
|
||||||
|
E31201365,HAMID RAFIUD DERAJAD,57401.0,bekerja,0,swasta jawa timur park,C2,False,
|
||||||
|
E32201075,UMARUL FARUQ,56401.0,bekerja,0,wiraswasta surya energi,C2,False,
|
||||||
|
E31200827,TANTRIA AGUSTINA YUDIANTI,57401.0,melanjutkan_studi,0,bumn jack koi,C2,False,
|
||||||
|
E31192328,DIMAS ARDIANA WIDODO,57401.0,bekerja,1,wiraswasta sablon jember,C2,False,
|
||||||
|
E32201483,NIASTI AL FAUZIAH LASE,56401.0,melanjutkan_studi,3,swasta toko berkah,C2,False,
|
||||||
|
E32202503,ACHMAD ALFITRI MULYAWAN,56401.0,melanjutkan_studi,0,perintah sawah,C2,False,
|
||||||
|
E31200649,WILDA WAHDANIA,57401.0,bekerja,3,swasta humma farm,C2,False,
|
||||||
|
E41190298,RAIHAN DIMAS ARIFIN,55301.0,bekerja,3,swasta healthy food jember,C2,False,
|
||||||
|
E32201290,HELLEN PUTRI ANDINI,56401.0,bekerja,0,wiraswasta jaya makmur,C2,False,
|
||||||
|
E41190868,Tegar Shallahudin Al Ayubi,55301.0,bekerja,3,swasta marvel secondshoes,C2,False,
|
||||||
|
E32202484,MUHAMMAD ILHAM YUAL MAULANA,56401.0,melanjutkan_studi,3,swasta sumaryonofarm,C2,False,
|
||||||
|
E32201964,MUHAMAD BAYU CANDRA PAMUNGKAS,56401.0,melanjutkan_studi,0,wiraswasta dinas tani pangan,C2,False,
|
||||||
|
E31201009,WANDA ARVANIYAN,57401.0,bekerja,0,swasta rumah sakit,C1,False,
|
||||||
|
E32201330,RIZKI DWI CAHYO PUTRO,55301.0,bekerja,0,bumn smk negeri puspo,C2,False,
|
||||||
|
E32201406,RIZAL ACHMAD PAHLEVI,56401.0,bekerja,0,wiraswasta rumah sakit,C1,False,
|
||||||
|
E32201506,ZAHROTUL ISTIQOMAH,56401.0,melanjutkan_studi,3,swasta ahmad kembang ban,C2,False,
|
||||||
|
E32201397,RISQI RIDHOLLAH ANAM,56401.0,bekerja,0,wiraswasta kamajaya motor,C2,False,
|
||||||
|
E32201498,Rizqi Ramadani,56401.0,bekerja,0,wiraswasta jember futura energi,C2,False,
|
||||||
|
E32200753,Anang Prayoga,56401.0,bekerja,0,bumn audio,C2,False,
|
||||||
|
E31200822,RAYNALDI WIBOWO,57401.0,bekerja,0,perintah kopi paste,C2,False,
|
||||||
|
E32200499,DINA PERMATA SARI,56401.0,bekerja,0,wiraswasta pemda kab bondowoso,C2,False,
|
||||||
|
E32201585,ADIS WIDYA RAMADHAN,56401.0,bekerja,3,swasta puskesmas,C2,False,
|
||||||
|
E32201239,MOCHAMMAD FERDIANTO,56401.0,melanjutkan_studi,3,swasta klinik,C2,False,
|
||||||
|
E31202463,RISMA DEWI SEPTIANI,57401.0,bekerja,3,swasta hrb cargo,C2,False,
|
||||||
|
E31201773,ARDYAS SETYA NUGRAHA,57401.0,bekerja,2,wiraswasta depot jamu har,C2,False,
|
||||||
|
E31201459,FIKRIATUL ILMI,57401.0,bekerja,3,swasta buma,C2,False,
|
||||||
|
E41192181,RAHADYAN DARARI FEBRIYANTO,55301.0,bekerja,0,swasta benih citra asia,C1,False,
|
||||||
|
E41191832,ALFIAN SEPTY CHANDRA WIJANARKO,55301.0,bekerja,0,swasta jaya makmur,C2,False,
|
||||||
|
E31191228,MOCHAMMAD RIZKI PRATAMA,57401.0,bekerja,0,swasta android developer web developer game tester,C0,True,"developer, web"
|
||||||
|
E31200784,ALIF ANFALUL HAKIM,57401.0,melanjutkan_studi,0,nirlaba rsud lasinrang kabupaten pinrang,C2,False,
|
||||||
|
E32201131,YUSUF MUHAMMAD FAQIH,56401.0,bekerja,0,bumn gading indonesia teguh,C2,False,
|
||||||
|
E41192173,MAULANA SULTAN RAGIL WAHYU SYAHPUTRA,55301.0,bekerja,0,wiraswasta pilarmedia indonesia contract,C2,False,
|
||||||
|
E41191668,MIFTAHUL RIZKY ZAMZAMI,55301.0,bekerja,0,swasta pusat latih tani desa swadaya p4s bintang tani sejahtera,C2,False,
|
||||||
|
E31202318,NOFAIRA AMIN,57401.0,melanjutkan_studi,0,swasta research and development austindo nusantara jaya tbk,C2,False,
|
||||||
|
E32200998,SANDY ASAHI,56401.0,bekerja,0,nirlaba full stack developer pilarmedia indonesia,C1,False,
|
||||||
|
E31202279,RAMA SATRIA MALAMO,57401.0,bekerja,2,wiraswasta gogrin,C2,False,
|
||||||
|
E31201690,NUR AFIFAH,57401.0,bekerja,0,swasta piranhamas group,C2,False,
|
||||||
|
E41192294,ADITYA SETYA NUGRAHA,55301.0,melanjutkan_studi,0,perintah kominfo,C2,False,
|
||||||
|
E32200197,RIZQI NOOR FAIDZIIN,56401.0,wiraswasta,0,wiraswasta badan dapat daerah kabupaten bondowoso,C2,False,
|
||||||
|
E31201536,FIRDA ULFA UNSILAH,57401.0,bekerja,0,bnpp arjuna distributor nestle,C2,False,
|
||||||
|
E31200117,DICKA CAHYA PUTRI,57401.0,bekerja,0,wiraswasta tani persero,C2,False,
|
||||||
|
E31201372,MUHAMMAD ARIF BILLAH,57401.0,bekerja,3,swasta reseller serizshopjember freelance,C2,False,
|
||||||
|
E31201555,REHAN REZA MAHENDRA,57401.0,bekerja,0,swasta koperasi tarutama nusantara,C2,False,
|
||||||
|
E31201431,FANISA NUR ARIFAH,57401.0,bekerja,0,wiraswasta labbola sport statistic and data management,C1,False,
|
||||||
|
E31201951,MASITHA SAVIRA,57401.0,bekerja,0,bumn kebun nusantara,C2,False,
|
||||||
|
E32200423,LELI FAUZIYAH,56401.0,bekerja,0,bumn wiraswasta,C2,False,
|
||||||
|
E32201639,SEFHIANA PUTRI WULANDARI,56401.0,bekerja,2,wiraswasta handoyo budi orchids,C2,False,
|
||||||
|
E31190508,ALFAN ABRAR ARIFIN,57401.0,bekerja,2,wiraswasta wakil direktur kurnia,C2,False,
|
||||||
|
E41190510,SYAUGI SALIM AMAR,55301.0,bekerja,0,wiraswasta project engineer etrama nusa energi,C0,True,engineer
|
||||||
|
E32200199,ILHAM TAUFIQUL KHALIQ,56401.0,bekerja,0,bumn lumbung stroberi,C2,False,
|
||||||
|
E32200838,ZAINUR ROZIQIN,56401.0,bekerja,0,swasta rumah sakit kebun kabupaten jember,C1,False,
|
||||||
|
E31202345,FATHUR ROCHMAN ZAINUL,57401.0,bekerja,1,perintah rumkit iii baladhika husada jember,C1,False,
|
||||||
|
E41180679,MOCHAMAD SATRIA MAULANA,55301.0,bekerja,0,bnpp freelance,C2,False,
|
||||||
|
E32202534,MOH. ANANG EFENDI,56401.0,bekerja,0,bumn pusat teliti kopi kakao indonesia,C1,False,
|
||||||
|
E32200600,Feri Andika,56401.0,bekerja,0,perintah ptpn kaliselogiri banyuwangi,C0,True,wan
|
||||||
|
E32201579,Brenda Gusti Dharma,56401.0,bekerja,3,swasta resty nur,C2,False,
|
||||||
|
E32200776,GIANT PAUL IVAN,56401.0,bekerja,0,perintah dinas budaya pariwisata kabupaten banyuwangi,C0,True,wan
|
||||||
|
E32200848,VICO SEPTIANDA PRATAMA,56401.0,wiraswasta,0,wiraswasta maestro group jember,C2,False,
|
||||||
|
E32200765,Ghovi Tri Yulihandika,56401.0,wiraswasta,2,wiraswasta talas eksportir,C2,False,
|
||||||
|
E32202496,FAIZ NUR ACHMADI,56401.0,bekerja,0,perintah icon,C2,False,
|
||||||
|
E32201958,MUHAMMAD QODRI,56401.0,bekerja,0,bumn bulog,C2,False,
|
||||||
|
E32202582,Yeferi Karoba,56401.0,bekerja,0,perintah kantor distrik eragayam kabupaten mamberamo tengah papua,C2,False,
|
||||||
|
E32192485,ALFAN ANAS MUHLISIN,55301.0,bekerja,0,perintah terna,C2,False,
|
||||||
|
E31192286,Melinda Fitriyani,57401.0,wiraswasta,0,bnpp dinas tenaga kerja,C2,False,
|
||||||
|
E41191422,Alvin Eko Cahyo Febrianto,55301.0,bekerja,3,swasta impactbyte teknologi edukasi skilvul,C1,False,
|
||||||
|
E41182123,CHIKITA PUTRI MEIDINASARI,55301.0,bekerja,0,swasta karya citra bangun persada utama,C1,False,
|
||||||
|
E32200651,MUHAMMAD FADLI ZAKARIA,56401.0,bekerja,0,bumn pln icon pllus,C2,False,
|
||||||
|
E41191034,MUHAMMAD RAIHAN RIYADY,55301.0,bekerja,3,swasta dankom mitra abadi,C1,False,
|
||||||
|
E41192199,RIZDKY OKTAVIARI PRATAMA PUTRA,55301.0,bekerja,3,swasta infimedia solusi pratama,C2,False,
|
||||||
|
E31200238,JALALUDDIN ABIMANYU,57401.0,bekerja,3,swasta bpjs sehat,C2,False,
|
||||||
|
E41192160,SOFYAN ARIF AF RIZAL,55301.0,bekerja,0,bumn bank rakyat indonesia cabang bondowoso,C2,False,
|
||||||
|
E31202278,CHAIDAR SA’AD,57401.0,bekerja,3,swasta percie,C2,False,
|
||||||
|
E31201056,DIELLA AULIA IVANA PUTRI,57401.0,bekerja,0,wiraswasta padma jaya,C2,False,
|
||||||
|
E31200973,DEVI ARYANING TYAS,57401.0,bekerja,3,swasta esolusindo,C2,False,
|
||||||
|
E41192433,MITA UNZIYAH FAJRINA,55301.0,bekerja,3,swasta koltiva,C2,False,
|
||||||
|
E41192330,ILMI AZMI IRADIYAH,55301.0,bekerja,1,perintah politeknik negeri jember,C1,False,
|
||||||
|
E41192348,SINDI MEGA SEPTIANDARI,55301.0,bekerja,0,bnpp durrotul hikmah islamic school,C2,False,
|
||||||
|
E41191224,FERRY SEPTIAN FERDIYANTO,55301.0,bekerja,3,swasta indoprima gemilang,C2,False,
|
||||||
|
E31200844,Lukman Afandi,57401.0,bekerja,3,swasta fokus jasa mitra,C1,False,
|
||||||
|
E41191741,EZRA ALANNATHANIEL,55301.0,bekerja,3,swasta bca,C2,False,
|
||||||
|
E41180808,Eva Agustin Rahayu,55301.0,wiraswasta,0,wiraswasta pusat teliti kopi kakao indonesia,C1,False,
|
||||||
|
E41190899,Mohammad Haerul Anam,55301.0,bekerja,0,swasta bejana investidata globalindo bigio,C1,False,
|
||||||
|
E41190441,Sinta Nuriyah Wulandari,55301.0,bekerja,0,swasta bejana investidata globalindo,C1,False,
|
||||||
|
E41182137,Tahajjudin Fajri,55301.0,wiraswasta,2,wiraswasta taarts animation,C2,False,
|
||||||
|
E41212489,BANGKIT BUDI UTOMO,55301.0,bekerja,1,perintah politeknik negeri jember,C1,False,
|
||||||
|
E41212482,Masrifatul Hazijah,55301.0,bekerja,0,perintah labuh ikan nusantara pengambengan jembrana bal,C2,False,
|
||||||
|
E41191458,HANIF SATRIO RIMAMTOMO,55301.0,bekerja,0,perintah dinas tani pangan,C2,False,
|
||||||
|
E32200508,PUTRI LATIFATUS SALAMAH,56401.0,bekerja,0,swasta orindo alam ayu oriflame supranusa indogita manufaktur freelancer pribadi asisten manager evaluator klinik jurnal capa,C0,True,lan
|
||||||
|
E31201939,NUR ACHMAD FAISOL QUBRO,57401.0,bekerja,0,swasta sygenta seed indonesia,C2,False,
|
||||||
|
E31160678,RIZKA NURUL KHAKIKI,57401.0,bekerja,-1,swasta,C3,False,
|
||||||
|
E41150211,DEWI AYU ITIKAF,55301.0,mencari_kerja,-1,,C3,False,
|
||||||
|
E31151401,Aza Pascaeka,57401.0,belum_kerja,-1,wiraswasta,C3,False,
|
||||||
|
E31130845,MOCH. RIVALDI AFTONI,57401.0,bekerja,-1,lain,C3,False,
|
||||||
|
E31160328,Afega Wira Pradana,57401.0,bekerja,-1,swasta,C3,False,
|
||||||
|
E31161719,Reza Septia Safirah,57401.0,belum_kerja,-1,wiraswasta,C3,False,
|
||||||
|
E32160904,ONGKY OKTAFIAN DWI JOHANSYAH,56401.0,bekerja,-1,swasta,C3,False,
|
||||||
|
E41150068,MUHAMMAD HAFIDH FIRMANSYAH,55301.0,bekerja,-1,lain,C3,False,
|
||||||
|
E41151584,Risqi Ahmad Sultoni,55301.0,bekerja,-1,swasta,C3,False,
|
||||||
|
E31151214,Nita Wahyu Lianti,57401.0,belum_kerja,-1,wiraswasta,C3,False,
|
||||||
|
E31160306,Faiqotul Hikmah,57401.0,bekerja,-1,swasta,C3,False,
|
||||||
|
E41170333 ,CICILIA SELVY ERIANTHY,55301.0,wiraswasta,-1,wiraswasta,C3,False,
|
||||||
|
E31182203 ,Rofi Berliana Mahanani,57401.0,melanjutkan_studi,-1,swasta,C3,False,
|
||||||
|
E32180554 ,Akhmad Farizi,56401.0,melanjutkan_studi,-1,swasta,C3,False,
|
||||||
|
E41170562 ,KRISOPRAS EPIKUROS,55301.0,wiraswasta,-1,swasta,C3,False,
|
||||||
|
E32182061 ,OZSA DEWANGGA PRADANA,56401.0,melanjutkan_studi,-1,bnpp,C3,False,
|
||||||
|
|
|
@ -0,0 +1,153 @@
|
||||||
|
{
|
||||||
|
"total": 535,
|
||||||
|
"distribusi_kategori": {
|
||||||
|
"C0": {
|
||||||
|
"jumlah": 18,
|
||||||
|
"persentase": 3.36
|
||||||
|
},
|
||||||
|
"C1": {
|
||||||
|
"jumlah": 85,
|
||||||
|
"persentase": 15.89
|
||||||
|
},
|
||||||
|
"C2": {
|
||||||
|
"jumlah": 416,
|
||||||
|
"persentase": 77.76
|
||||||
|
},
|
||||||
|
"C3": {
|
||||||
|
"jumlah": 16,
|
||||||
|
"persentase": 2.99
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"distribusi_prodi": {
|
||||||
|
"TIF": {
|
||||||
|
"C0": {
|
||||||
|
"jumlah": 5,
|
||||||
|
"persentase": 2.22
|
||||||
|
},
|
||||||
|
"C1": {
|
||||||
|
"jumlah": 53,
|
||||||
|
"persentase": 23.56
|
||||||
|
},
|
||||||
|
"C2": {
|
||||||
|
"jumlah": 162,
|
||||||
|
"persentase": 72.0
|
||||||
|
},
|
||||||
|
"C3": {
|
||||||
|
"jumlah": 5,
|
||||||
|
"persentase": 2.22
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MIF": {
|
||||||
|
"C0": {
|
||||||
|
"jumlah": 3,
|
||||||
|
"persentase": 1.82
|
||||||
|
},
|
||||||
|
"C1": {
|
||||||
|
"jumlah": 20,
|
||||||
|
"persentase": 12.12
|
||||||
|
},
|
||||||
|
"C2": {
|
||||||
|
"jumlah": 134,
|
||||||
|
"persentase": 81.21
|
||||||
|
},
|
||||||
|
"C3": {
|
||||||
|
"jumlah": 8,
|
||||||
|
"persentase": 4.85
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"TKK": {
|
||||||
|
"C0": {
|
||||||
|
"jumlah": 10,
|
||||||
|
"persentase": 6.9
|
||||||
|
},
|
||||||
|
"C1": {
|
||||||
|
"jumlah": 12,
|
||||||
|
"persentase": 8.28
|
||||||
|
},
|
||||||
|
"C2": {
|
||||||
|
"jumlah": 120,
|
||||||
|
"persentase": 82.76
|
||||||
|
},
|
||||||
|
"C3": {
|
||||||
|
"jumlah": 3,
|
||||||
|
"persentase": 2.07
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"match_rate": {
|
||||||
|
"TIF": {
|
||||||
|
"match": 5,
|
||||||
|
"total": 225,
|
||||||
|
"rate": 2.22
|
||||||
|
},
|
||||||
|
"MIF": {
|
||||||
|
"match": 3,
|
||||||
|
"total": 165,
|
||||||
|
"rate": 1.82
|
||||||
|
},
|
||||||
|
"TKK": {
|
||||||
|
"match": 10,
|
||||||
|
"total": 145,
|
||||||
|
"rate": 6.9
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"detail_per_kategori": {
|
||||||
|
"C0": {
|
||||||
|
"jumlah": 18,
|
||||||
|
"persentase": 3.36,
|
||||||
|
"sample_names": [
|
||||||
|
"Ahmad Syamsul Arifin",
|
||||||
|
"SANDY KURNIAWAN",
|
||||||
|
"BAWIK ARDIYAN RAMADHAN",
|
||||||
|
"DEWI RATIH TILAMSARI WIBOWO",
|
||||||
|
"BONDAN YUWONO PUTRA"
|
||||||
|
],
|
||||||
|
"top_keywords": {
|
||||||
|
"wan": 5,
|
||||||
|
"tech": 4,
|
||||||
|
"lan": 4,
|
||||||
|
"bi": 2,
|
||||||
|
"komputer": 1,
|
||||||
|
"developer": 1,
|
||||||
|
"web": 1,
|
||||||
|
"engineer": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"C1": {
|
||||||
|
"jumlah": 85,
|
||||||
|
"persentase": 15.89,
|
||||||
|
"sample_names": [
|
||||||
|
"Yunita Dwi Pujianti",
|
||||||
|
"Rikky Ihza Pratama",
|
||||||
|
"RENDHY PRATAMA PUTRA",
|
||||||
|
"DENNY SETIADY PRABOWO",
|
||||||
|
"KHOSNOL KHOTIMATUL ARIFAH"
|
||||||
|
],
|
||||||
|
"top_keywords": {}
|
||||||
|
},
|
||||||
|
"C2": {
|
||||||
|
"jumlah": 416,
|
||||||
|
"persentase": 77.76,
|
||||||
|
"sample_names": [
|
||||||
|
"Rizki Firmansyah",
|
||||||
|
"Afrizal Gita Putra Laksono",
|
||||||
|
"Lilik Widayanti",
|
||||||
|
"LUKMAN HARUN",
|
||||||
|
"DAVID SUGIANTO"
|
||||||
|
],
|
||||||
|
"top_keywords": {}
|
||||||
|
},
|
||||||
|
"C3": {
|
||||||
|
"jumlah": 16,
|
||||||
|
"persentase": 2.99,
|
||||||
|
"sample_names": [
|
||||||
|
"RIZKA NURUL KHAKIKI",
|
||||||
|
"DEWI AYU ITIKAF",
|
||||||
|
"Aza Pascaeka",
|
||||||
|
"MOCH. RIVALDI AFTONI",
|
||||||
|
"Afega Wira Pradana"
|
||||||
|
],
|
||||||
|
"top_keywords": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
============================================================
|
||||||
|
INTERPRETASI CLUSTER -> C0-C3
|
||||||
|
Tanggal: 2026-08-28 03:51:35
|
||||||
|
============================================================
|
||||||
|
|
||||||
|
Total Alumni: 535
|
||||||
|
|
||||||
|
Distribusi Kategori:
|
||||||
|
C0: 18 (3.36%)
|
||||||
|
C1: 85 (15.89%)
|
||||||
|
C2: 416 (77.76%)
|
||||||
|
C3: 16 (2.99%)
|
||||||
|
|
||||||
|
============================================================
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"model_version": "20260828_042742",
|
||||||
|
"k": 4,
|
||||||
|
"inertia": 893.8856227132463,
|
||||||
|
"silhouette_score": 0.1033661431743342,
|
||||||
|
"n_samples": 1114,
|
||||||
|
"n_features": 500,
|
||||||
|
"evaluation": {
|
||||||
|
"k_candidates": [
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
5,
|
||||||
|
6
|
||||||
|
],
|
||||||
|
"inertias": [
|
||||||
|
992.5189870879342,
|
||||||
|
937.512241306878,
|
||||||
|
893.8856227132463,
|
||||||
|
868.6311058599106,
|
||||||
|
854.480705314767
|
||||||
|
],
|
||||||
|
"silhouettes": [
|
||||||
|
0.059478779017896076,
|
||||||
|
0.08331269132070036,
|
||||||
|
0.1033661431743342,
|
||||||
|
0.11510910180812096,
|
||||||
|
0.11539212263526002
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"created_at": "2026-08-28 04:27:42"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"tanggal": "2026-08-28 04:27:22",
|
||||||
|
"total_alumni": 1596,
|
||||||
|
"total_untuk_clustering": 1114,
|
||||||
|
"total_c3": 482,
|
||||||
|
"sastrawi_available": true,
|
||||||
|
"distribusi_status": {
|
||||||
|
"bekerja": 877,
|
||||||
|
"mencari_kerja": 304,
|
||||||
|
"wiraswasta": 161,
|
||||||
|
"melanjutkan_studi": 157,
|
||||||
|
"belum_kerja": 97
|
||||||
|
},
|
||||||
|
"distribusi_prodi": {
|
||||||
|
"55301": 347,
|
||||||
|
"57401": 236,
|
||||||
|
"56401": 194,
|
||||||
|
"Teknik Informatika (55301)": 184,
|
||||||
|
"21. Teknik Informatika (55301)": 151,
|
||||||
|
"19. Manajemen Informatika (57401)": 115,
|
||||||
|
"Teknik Komputer (56401)": 105,
|
||||||
|
"20. Teknik Komputer (56401)": 101,
|
||||||
|
"Manajemen Informatika (57401)": 87,
|
||||||
|
"23. Teknik Informatika Kampus Kab. Sidoarjo (55304)": 40,
|
||||||
|
"22. Teknik Informatika Kampus Kab. Nganjuk (55302)": 36
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,155 @@
|
||||||
|
{
|
||||||
|
"total": 1596,
|
||||||
|
"distribusi_kategori": {
|
||||||
|
"C0": {
|
||||||
|
"jumlah": 223,
|
||||||
|
"persentase": 13.97
|
||||||
|
},
|
||||||
|
"C1": {
|
||||||
|
"jumlah": 385,
|
||||||
|
"persentase": 24.12
|
||||||
|
},
|
||||||
|
"C2": {
|
||||||
|
"jumlah": 506,
|
||||||
|
"persentase": 31.7
|
||||||
|
},
|
||||||
|
"C3": {
|
||||||
|
"jumlah": 482,
|
||||||
|
"persentase": 30.2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"distribusi_prodi": {
|
||||||
|
"TIF": {
|
||||||
|
"C0": {
|
||||||
|
"jumlah": 107,
|
||||||
|
"persentase": 15.69
|
||||||
|
},
|
||||||
|
"C1": {
|
||||||
|
"jumlah": 167,
|
||||||
|
"persentase": 24.49
|
||||||
|
},
|
||||||
|
"C2": {
|
||||||
|
"jumlah": 239,
|
||||||
|
"persentase": 35.04
|
||||||
|
},
|
||||||
|
"C3": {
|
||||||
|
"jumlah": 169,
|
||||||
|
"persentase": 24.78
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MIF": {
|
||||||
|
"C0": {
|
||||||
|
"jumlah": 56,
|
||||||
|
"persentase": 12.79
|
||||||
|
},
|
||||||
|
"C1": {
|
||||||
|
"jumlah": 104,
|
||||||
|
"persentase": 23.74
|
||||||
|
},
|
||||||
|
"C2": {
|
||||||
|
"jumlah": 142,
|
||||||
|
"persentase": 32.42
|
||||||
|
},
|
||||||
|
"C3": {
|
||||||
|
"jumlah": 136,
|
||||||
|
"persentase": 31.05
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"TKK": {
|
||||||
|
"C0": {
|
||||||
|
"jumlah": 60,
|
||||||
|
"persentase": 15.0
|
||||||
|
},
|
||||||
|
"C1": {
|
||||||
|
"jumlah": 104,
|
||||||
|
"persentase": 26.0
|
||||||
|
},
|
||||||
|
"C2": {
|
||||||
|
"jumlah": 114,
|
||||||
|
"persentase": 28.5
|
||||||
|
},
|
||||||
|
"C3": {
|
||||||
|
"jumlah": 122,
|
||||||
|
"persentase": 30.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"match_rate": {
|
||||||
|
"TIF": {
|
||||||
|
"match": 107,
|
||||||
|
"total": 682,
|
||||||
|
"rate": 15.69
|
||||||
|
},
|
||||||
|
"MIF": {
|
||||||
|
"match": 56,
|
||||||
|
"total": 438,
|
||||||
|
"rate": 12.79
|
||||||
|
},
|
||||||
|
"TKK": {
|
||||||
|
"match": 60,
|
||||||
|
"total": 400,
|
||||||
|
"rate": 15.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"detail_per_kategori": {
|
||||||
|
"C0": {
|
||||||
|
"jumlah": 223,
|
||||||
|
"persentase": 13.97,
|
||||||
|
"sample_names": [
|
||||||
|
"Ahmad Syamsul Arifin",
|
||||||
|
"Dhimas Ulfa Lathifah",
|
||||||
|
"SANDY KURNIAWAN",
|
||||||
|
"DEKA PERMANA ALFIANSYAH",
|
||||||
|
"Dimas Kristanto"
|
||||||
|
],
|
||||||
|
"top_keywords": {
|
||||||
|
"ti": 95,
|
||||||
|
"it": 75,
|
||||||
|
"lan": 32,
|
||||||
|
"tech": 12,
|
||||||
|
"teknologi": 12,
|
||||||
|
"web": 9,
|
||||||
|
"bi": 6,
|
||||||
|
"developer": 5,
|
||||||
|
"design": 3,
|
||||||
|
"engineer": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"C1": {
|
||||||
|
"jumlah": 385,
|
||||||
|
"persentase": 24.12,
|
||||||
|
"sample_names": [
|
||||||
|
"DAVID SUGIANTO",
|
||||||
|
"Ayu Diyah Purwanti",
|
||||||
|
"Yunita Dwi Pujianti",
|
||||||
|
"MUHAMMAD ALY ROFIE",
|
||||||
|
"Eka Safitri"
|
||||||
|
],
|
||||||
|
"top_keywords": {}
|
||||||
|
},
|
||||||
|
"C2": {
|
||||||
|
"jumlah": 506,
|
||||||
|
"persentase": 31.7,
|
||||||
|
"sample_names": [
|
||||||
|
"Rizki Firmansyah",
|
||||||
|
"Afrizal Gita Putra Laksono",
|
||||||
|
"Lilik Widayanti",
|
||||||
|
"LUKMAN HARUN",
|
||||||
|
"Mahbubi Ilham Nafiulalam"
|
||||||
|
],
|
||||||
|
"top_keywords": {}
|
||||||
|
},
|
||||||
|
"C3": {
|
||||||
|
"jumlah": 482,
|
||||||
|
"persentase": 30.2,
|
||||||
|
"sample_names": [
|
||||||
|
"RIZKA NURUL KHAKIKI",
|
||||||
|
"DEWI AYU ITIKAF",
|
||||||
|
"Aza Pascaeka",
|
||||||
|
"MOCH. RIVALDI AFTONI",
|
||||||
|
"Afega Wira Pradana"
|
||||||
|
],
|
||||||
|
"top_keywords": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
============================================================
|
||||||
|
INTERPRETASI CLUSTER -> C0-C3
|
||||||
|
Tanggal: 2026-08-28 04:48:31
|
||||||
|
============================================================
|
||||||
|
|
||||||
|
Total Alumni: 1596
|
||||||
|
|
||||||
|
Distribusi Kategori:
|
||||||
|
C0: 223 (13.97%)
|
||||||
|
C1: 385 (24.12%)
|
||||||
|
C2: 506 (31.7%)
|
||||||
|
C3: 482 (30.2%)
|
||||||
|
|
||||||
|
============================================================
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
============================================================
|
||||||
|
MERGE SUMMARY - TRACER STUDY ALUMNI
|
||||||
|
Tanggal: 2026-08-28 04:25:50
|
||||||
|
============================================================
|
||||||
|
|
||||||
|
>> TOTAL DATA: 1596 alumni
|
||||||
|
|
||||||
|
>> Distribusi per Tahun Lulus:
|
||||||
|
2019.0: 30 alumni
|
||||||
|
2020.0: 31 alumni
|
||||||
|
2021.0: 237 alumni
|
||||||
|
2022.0: 211 alumni
|
||||||
|
2023.0: 268 alumni
|
||||||
|
2024.0: 361 alumni
|
||||||
|
2025.0: 458 alumni
|
||||||
|
|
||||||
|
>> Distribusi per Prodi (Kode):
|
||||||
|
55301: 347 alumni
|
||||||
|
57401: 236 alumni
|
||||||
|
56401: 194 alumni
|
||||||
|
Teknik Informatika (55301): 184 alumni
|
||||||
|
21. Teknik Informatika (55301): 151 alumni
|
||||||
|
19. Manajemen Informatika (57401): 115 alumni
|
||||||
|
Teknik Komputer (56401): 105 alumni
|
||||||
|
20. Teknik Komputer (56401): 101 alumni
|
||||||
|
Manajemen Informatika (57401): 87 alumni
|
||||||
|
23. Teknik Informatika Kampus Kab. Sidoarjo (55304): 40 alumni
|
||||||
|
22. Teknik Informatika Kampus Kab. Nganjuk (55302): 36 alumni
|
||||||
|
|
||||||
|
>> Distribusi Status Pekerjaan:
|
||||||
|
bekerja: 877 (54.9%)
|
||||||
|
mencari_kerja: 304 (19.0%)
|
||||||
|
wiraswasta: 161 (10.1%)
|
||||||
|
melanjutkan_studi: 157 (9.8%)
|
||||||
|
belum_kerja: 97 (6.1%)
|
||||||
|
|
||||||
|
>> Sumber File:
|
||||||
|
Data_2024_Tracer_study.csv: 361 alumni
|
||||||
|
Data_2023_Tracer_study.csv: 268 alumni
|
||||||
|
Data_2021_Tracer_study.csv: 237 alumni
|
||||||
|
Data_2022_Tracer_study.csv: 211 alumni
|
||||||
|
Data_2025_Tracer_study_D4TIF.csv: 166 alumni
|
||||||
|
Data_2025_Tracer_study_D3MIF.csv: 115 alumni
|
||||||
|
Data_2025_Tracer_study_D3TKK.csv: 101 alumni
|
||||||
|
Data_2025_Tracer_study_D4TIFSIDOARJO.csv: 40 alumni
|
||||||
|
Data_2025_Tracer_study_D4TIFNGANJUK.csv: 36 alumni
|
||||||
|
Data_2020_Tracer_study.csv: 31 alumni
|
||||||
|
Data_2019_Tracer_study.csv: 30 alumni
|
||||||
|
|
||||||
|
>> Missing Values:
|
||||||
|
jenis_perusahaan: 231 (14.5%)
|
||||||
|
nama_perusahaan: 560 (35.1%)
|
||||||
|
jabatan: 491 (30.8%)
|
||||||
|
|
||||||
|
============================================================
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1 @@
|
||||||
|
# Feature Engineering module
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
"""
|
||||||
|
TF-IDF Feature Extraction Module
|
||||||
|
Mengubah teks (posisi pekerjaan, instansi, kompetensi) menjadi representasi numerik menggunakan algoritma TF-IDF (Term Frequency - Inverse Document Frequency).
|
||||||
|
Nilai bobot TF-IDF merefleksikan seberapa penting suatu kata dalam suatu dokumen relatif terhadap keseluruhan korpus data tracer study.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Mengimpor modul Any untuk type hint fleksibel
|
||||||
|
from typing import Any
|
||||||
|
# Mengimpor class TfidfVectorizer dari scikit-learn untuk proses tokenisasi, penghitungan frekuensi kata, dan pembobotan inverse document frequency
|
||||||
|
from sklearn.feature_extraction.text import TfidfVectorizer
|
||||||
|
# Mengimpor pustaka numpy untuk operasi numerik array
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
# Mendefinisikan class TfidfExtractor sebagai wrapper class untuk memudahkan penggunaan TF-IDF dalam pipeline klastering
|
||||||
|
class TfidfExtractor:
|
||||||
|
"""TF-IDF Vectorizer wrapper untuk keperluan ekstraksi fitur pemodelan clustering"""
|
||||||
|
|
||||||
|
# Konstruktor kelas untuk menginisialisasi parameter vektorisasi teks
|
||||||
|
def __init__(self, max_features: int = 500, ngram_range: tuple = (1, 1), min_df: int = 1, max_df: float = 1.0, **kwargs):
|
||||||
|
"""
|
||||||
|
Inisialisasi hyperparameter pembobotan TF-IDF:
|
||||||
|
Args:
|
||||||
|
max_features: Jumlah batas kosakata teratas yang akan diikutsertakan sebagai kolom fitur (default: 500 kata paling relevan)
|
||||||
|
ngram_range: Rentang kombinasi kata, (1, 1) berarti unigram (satu kata per fitur)
|
||||||
|
min_df: Batas minimum kemunculan kata dalam dokumen agar tidak diabaikan (mengeliminasi kata typo/sangat langka)
|
||||||
|
max_df: Batas maksimum proporsi dokumen yang memuat kata agar tidak diabaikan (mengeliminasi stopword umum)
|
||||||
|
**kwargs: Parameter opsional tambahan yang diteruskan langsung ke TfidfVectorizer Scikit-Learn
|
||||||
|
"""
|
||||||
|
# Menginstansiasi objek TfidfVectorizer dari Scikit-Learn dengan konfigurasi yang diberikan
|
||||||
|
self.vectorizer = TfidfVectorizer(
|
||||||
|
max_features=max_features,
|
||||||
|
ngram_range=ngram_range,
|
||||||
|
min_df=min_df,
|
||||||
|
max_df=max_df,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
# Menyiapkan variabel untuk menampung matriks fitur numerik hasil transformasi (default None)
|
||||||
|
self.feature_matrix = None
|
||||||
|
|
||||||
|
|
||||||
|
# Metode fit_transform: Melakukan fitting vocabulary sekaligus mengubah kumpulan dokumen teks menjadi matriks numerik
|
||||||
|
def fit_transform(self, texts: list) -> Any:
|
||||||
|
"""
|
||||||
|
Mempelajari kosakata (vocabulary) dari kumpulan teks masukan dan mentransformasikannya menjadi feature matrix sparse.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
texts: List berisi kumpulan teks string yang sudah dibersihkan (preprocessed text)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Matriks numerik berbobot TF-IDF berformat scipy sparse matrix
|
||||||
|
"""
|
||||||
|
# Melatih vectorizer dengan korpus data teks masukan sekaligus menghitung nilai TF-IDF untuk setiap dokumen
|
||||||
|
self.feature_matrix = self.vectorizer.fit_transform(texts)
|
||||||
|
# Mengembalikan matriks fitur numerik berbobot TF-IDF
|
||||||
|
return self.feature_matrix
|
||||||
|
|
||||||
|
# Metode transform: Mengubah teks dokumen baru menjadi representasi numerik menggunakan kosakata yang sudah dipelajari sebelumnya
|
||||||
|
def transform(self, texts: list) -> Any:
|
||||||
|
"""
|
||||||
|
Mentransformasikan teks baru menggunakan model TF-IDF vectorizer yang sudah dilatih (tanpa mengubah vocabulary).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
texts: List dokumen teks baru yang ingin ditransformasi
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Matriks numerik berbobot TF-IDF sesuai dengan dimensi kosakata model
|
||||||
|
"""
|
||||||
|
# Mentransformasi teks masukan baru menggunakan kamus kata dan bobot IDF yang telah terbentuk
|
||||||
|
return self.vectorizer.transform(texts)
|
||||||
|
|
||||||
|
# Metode untuk mengambil seluruh daftar nama kata unik (fitur) yang terpilih sebagai kolom pada matriks
|
||||||
|
def get_feature_names(self) -> list:
|
||||||
|
"""Mendapatkan daftar nama fitur kata unik (vocabulary features) yang digunakan model"""
|
||||||
|
# Mengembalikan nama-nama fitur kata dalam bentuk list Python
|
||||||
|
return self.vectorizer.get_feature_names_out().tolist()
|
||||||
|
|
||||||
|
# Metode untuk mengetahui jumlah total kosakata unik yang dipelajari oleh model
|
||||||
|
def get_vocab_size(self) -> int:
|
||||||
|
"""Mendapatkan total jumlah kosakata unik (vocabulary size) yang terdaftar"""
|
||||||
|
# Mengembalikan panjang (banyaknya entri) dictionary vocabulary_
|
||||||
|
return len(self.vectorizer.vocabulary_)
|
||||||
|
|
||||||
|
# Metode untuk mendapatkan bobot IDF (Inverse Document Frequency) masing-masing kata
|
||||||
|
def get_idf(self) -> dict:
|
||||||
|
"""Mendapatkan skor bobot IDF untuk masing-masing kata dalam bentuk pasangan key-value dictionary"""
|
||||||
|
# Mengambil daftar nama seluruh fitur kata
|
||||||
|
feature_names = self.get_feature_names()
|
||||||
|
# Menggabungkan nama fitur dengan nilai bobot idf_ menggunakan zip dan mengonversinya menjadi dictionary Python
|
||||||
|
return dict(zip(feature_names, self.vectorizer.idf_))
|
||||||
|
|
||||||
|
|
||||||
|
# Fungsi utilitas pembantu (helper function) untuk membangun feature matrix secara ringkas dalam satu pemanggilan
|
||||||
|
def create_feature_matrix(texts: list, **kwargs) -> tuple:
|
||||||
|
"""
|
||||||
|
Fungsi pembantu sekali panggil untuk memproses teks mentah menjadi matriks fitur TF-IDF.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
texts: List kumpulan dokumen teks hasil praproses
|
||||||
|
**kwargs: Argumen konfigurasi tambahan untuk TfidfExtractor
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple 3 elemen berisi: (feature_matrix, daftar_nama_fitur, instance_extractor)
|
||||||
|
"""
|
||||||
|
# Menginisialisasi instance ekstraktor TF-IDF dengan parameter kwargs
|
||||||
|
extractor = TfidfExtractor(**kwargs)
|
||||||
|
# Melakukan fitting dan transformasi teks menjadi matriks fitur
|
||||||
|
matrix = extractor.fit_transform(texts)
|
||||||
|
# Mengembalikan matriks bobot TF-IDF, daftar nama kosakata fitur, dan objek extractor
|
||||||
|
return matrix, extractor.get_feature_names(), extractor
|
||||||
|
|
||||||
|
|
||||||
|
# Blok pengetesan lokal modul yang hanya dijalankan ketika file ini dieksekusi langsung
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Menyiapkan sampel kalimat tiruan untuk menguji proses vektorisasi teks
|
||||||
|
sample_texts = [
|
||||||
|
"software engineer developer",
|
||||||
|
"data analyst scientist",
|
||||||
|
"project manager leader"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Menjalankan fungsi create_feature_matrix pada sampel teks
|
||||||
|
matrix, features, extractor = create_feature_matrix(sample_texts)
|
||||||
|
# Menampilkan dimensi baris dan kolom matriks fitur (jumlah dokumen x jumlah kosakata)
|
||||||
|
print(f"Feature Matrix Shape: {matrix.shape}")
|
||||||
|
# Menampilkan seluruh nama fitur kata yang diekstraksi
|
||||||
|
print(f"Features: {features}")
|
||||||
|
|
||||||
|
|
@ -0,0 +1,219 @@
|
||||||
|
"""
|
||||||
|
Main ML Pipeline
|
||||||
|
Pipeline utama untuk clustering alumni
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .preprocessing.text_preprocessing import preprocess_text
|
||||||
|
from .feature_engineering.tfidf import TfidfExtractor
|
||||||
|
from .clustering.kmeans import KMeansClustering, find_optimal_k, get_cluster_summary
|
||||||
|
|
||||||
|
|
||||||
|
class AlumniClusteringPipeline:
|
||||||
|
"""
|
||||||
|
Pipeline utama untuk clustering alumni
|
||||||
|
|
||||||
|
Input: DataFrame dengan kolom ['nama', 'perusahaan', 'jabatan']
|
||||||
|
Output: DataFrame dengan kolom tambahan 'cluster'
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, n_clusters: int = 3, auto_k: bool = False):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
n_clusters: Jumlah cluster (diabaikan jika auto_k=True)
|
||||||
|
auto_k: Jika True, cari k optimal secara otomatis
|
||||||
|
"""
|
||||||
|
self.n_clusters = n_clusters
|
||||||
|
self.auto_k = auto_k
|
||||||
|
self.tfidf = None
|
||||||
|
self.kmeans = None
|
||||||
|
self.feature_names = None
|
||||||
|
|
||||||
|
def _combine_text(self, df: pd.DataFrame) -> list:
|
||||||
|
"""Menggabungkan kolom teks menjadi satu"""
|
||||||
|
combined = df['perusahaan'].fillna('') + ' ' + df['jabatan'].fillna('')
|
||||||
|
return combined.tolist()
|
||||||
|
|
||||||
|
def _preprocess_all(self, texts: list) -> list:
|
||||||
|
"""Preprocess semua teks"""
|
||||||
|
return [preprocess_text(t) for t in texts]
|
||||||
|
|
||||||
|
def fit(self, df: pd.DataFrame) -> 'AlumniClusteringPipeline':
|
||||||
|
"""
|
||||||
|
Fit pipeline ke data training
|
||||||
|
|
||||||
|
Args:
|
||||||
|
df: DataFrame dengan kolom ['nama', 'perusahaan', 'jabatan']
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
"""
|
||||||
|
# Combine text
|
||||||
|
combined_texts = self._combine_text(df)
|
||||||
|
|
||||||
|
# Preprocess
|
||||||
|
processed_texts = self._preprocess_all(combined_texts)
|
||||||
|
|
||||||
|
# TF-IDF
|
||||||
|
self.tfidf = TfidfExtractor(max_features=500)
|
||||||
|
feature_matrix = self.tfidf.fit_transform(processed_texts)
|
||||||
|
self.feature_names = self.tfidf.get_feature_names()
|
||||||
|
|
||||||
|
# K-Means
|
||||||
|
n_clusters = self.n_clusters
|
||||||
|
|
||||||
|
if self.auto_k:
|
||||||
|
# Find optimal k
|
||||||
|
results = find_optimal_k(feature_matrix)
|
||||||
|
# Ambil k dengan silhouette score tertinggi
|
||||||
|
best_idx = np.argmax(results['silhouette'])
|
||||||
|
n_clusters = results['k'][best_idx]
|
||||||
|
print(f"Optimal K selected: {n_clusters}")
|
||||||
|
|
||||||
|
self.kmeans = KMeansClustering(n_clusters=n_clusters)
|
||||||
|
self.kmeans.fit(feature_matrix)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def transform(self, df: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Transform data dengan model yang sudah di-fit
|
||||||
|
|
||||||
|
Args:
|
||||||
|
df: DataFrame dengan kolom ['nama', 'perusahaan', 'jabatan']
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DataFrame dengan kolom 'cluster' dan 'cluster_label'
|
||||||
|
"""
|
||||||
|
if self.kmeans is None:
|
||||||
|
raise ValueError("Model belum di-fit. Jalankan fit() terlebih dahulu.")
|
||||||
|
|
||||||
|
# Combine text
|
||||||
|
combined_texts = self._combine_text(df)
|
||||||
|
|
||||||
|
# Preprocess
|
||||||
|
processed_texts = self._preprocess_all(combined_texts)
|
||||||
|
|
||||||
|
# TF-IDF transform
|
||||||
|
feature_matrix = self.tfidf.transform(processed_texts)
|
||||||
|
|
||||||
|
# Predict
|
||||||
|
labels = self.kmeans.predict(feature_matrix)
|
||||||
|
|
||||||
|
# Add to DataFrame
|
||||||
|
result = df.copy()
|
||||||
|
result['cluster'] = labels
|
||||||
|
result['cluster_label'] = [f"Cluster_{i}" for i in labels]
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def fit_transform(self, df: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
"""Fit dan transform sekaligus"""
|
||||||
|
return self.fit(df).transform(df)
|
||||||
|
|
||||||
|
def get_cluster_summary(self, df: pd.DataFrame) -> dict:
|
||||||
|
"""Mendapatkan ringkasan cluster"""
|
||||||
|
if 'cluster' not in df.columns:
|
||||||
|
raise ValueError("Data belum di-cluster. Jalankan fit_transform() terlebih dahulu.")
|
||||||
|
return get_cluster_summary(df, df['cluster'].values)
|
||||||
|
|
||||||
|
def get_top_features(self, n: int = 10) -> dict:
|
||||||
|
"""Mendapatkan fitur teratas untuk setiap cluster"""
|
||||||
|
if self.kmeans is None or self.tfidf is None:
|
||||||
|
raise ValueError("Model belum di-fit.")
|
||||||
|
|
||||||
|
cluster_centers = self.kmeans.model.cluster_centers_
|
||||||
|
feature_names = self.feature_names
|
||||||
|
|
||||||
|
top_features = {}
|
||||||
|
for i, center in enumerate(cluster_centers):
|
||||||
|
# Get indices of top features
|
||||||
|
top_indices = center.argsort()[-n:][::-1]
|
||||||
|
top_features[f"Cluster_{i}"] = [
|
||||||
|
{"term": feature_names[idx], "weight": float(center[idx])}
|
||||||
|
for idx in top_indices
|
||||||
|
]
|
||||||
|
|
||||||
|
return top_features
|
||||||
|
|
||||||
|
def save_model(self, filepath: str):
|
||||||
|
"""Simpan model (placeholder - bisa di-extend dengan joblib)"""
|
||||||
|
model_data = {
|
||||||
|
'n_clusters': self.n_clusters,
|
||||||
|
'feature_names': self.feature_names
|
||||||
|
}
|
||||||
|
with open(filepath, 'w') as f:
|
||||||
|
json.dump(model_data, f)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def load_model(filepath: str):
|
||||||
|
"""Load model (placeholder)"""
|
||||||
|
with open(filepath, 'r') as f:
|
||||||
|
model_data = json.load(f)
|
||||||
|
|
||||||
|
pipeline = AlumniClusteringPipeline(n_clusters=model_data['n_clusters'])
|
||||||
|
return pipeline
|
||||||
|
|
||||||
|
|
||||||
|
def run_pipeline(data_path: str = None, n_clusters: int = 3, output_path: str = None):
|
||||||
|
"""
|
||||||
|
Jalankan pipeline dari command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data_path: Path ke file CSV input
|
||||||
|
n_clusters: Jumlah cluster
|
||||||
|
output_path: Path untuk menyimpan hasil
|
||||||
|
"""
|
||||||
|
# Load data
|
||||||
|
if data_path:
|
||||||
|
df = pd.read_csv(data_path)
|
||||||
|
else:
|
||||||
|
# Sample data untuk testing
|
||||||
|
df = pd.DataFrame({
|
||||||
|
'nama': ['Ali', 'Budi', 'Citra', 'Dewi', 'Eko'],
|
||||||
|
'perusahaan': ['Google', 'Tokopedia', 'Gojek', 'Traveloka', 'Bukalapak'],
|
||||||
|
'jabatan': ['Software Engineer', 'Data Analyst', 'Product Manager', 'UX Designer', 'DevOps']
|
||||||
|
})
|
||||||
|
|
||||||
|
print(f"Data loaded: {len(df)} records")
|
||||||
|
|
||||||
|
# Run pipeline
|
||||||
|
pipeline = AlumniClusteringPipeline(n_clusters=n_clusters)
|
||||||
|
result = pipeline.fit_transform(df)
|
||||||
|
|
||||||
|
print(f"\nClustering complete!")
|
||||||
|
print(f"\nCluster Distribution:")
|
||||||
|
print(result['cluster_label'].value_counts())
|
||||||
|
|
||||||
|
print(f"\nCluster Summary:")
|
||||||
|
summary = pipeline.get_cluster_summary(result)
|
||||||
|
for cluster, info in summary.items():
|
||||||
|
print(f" {cluster}: {info['count']} alumni ({info['percentage']}%)")
|
||||||
|
|
||||||
|
print(f"\nTop Features per Cluster:")
|
||||||
|
top_features = pipeline.get_top_features()
|
||||||
|
for cluster, features in top_features.items():
|
||||||
|
print(f" {cluster}: {[f['term'] for f in features[:5]]}")
|
||||||
|
|
||||||
|
# Save result
|
||||||
|
if output_path:
|
||||||
|
result.to_csv(output_path, index=False)
|
||||||
|
print(f"\nResult saved to: {output_path}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
data_path = sys.argv[1]
|
||||||
|
n_clusters = int(sys.argv[2]) if len(sys.argv) > 2 else 3
|
||||||
|
output_path = sys.argv[3] if len(sys.argv) > 3 else None
|
||||||
|
run_pipeline(data_path, n_clusters, output_path)
|
||||||
|
else:
|
||||||
|
run_pipeline()
|
||||||
|
|
@ -0,0 +1,270 @@
|
||||||
|
# 📋 Modul Preprocessing
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Modul ini berisi pipeline untuk **menyiapkan data tracer study alumni** dari berbagai tahun (2019-2025) agar siap diproses oleh model Machine Learning.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Tujuan
|
||||||
|
|
||||||
|
1. **Standarisasi Kolom** - Menyamakan kolom berbeda dari berbagai tahun
|
||||||
|
2. **Menggabungkan Data** - Gabungkan data dari semua tahun jadi satu
|
||||||
|
3. **Data Cleaning** - Hapus duplikat dan data kosong
|
||||||
|
4. **Text Preprocessing** - Bersihkan teks untuk TF-IDF
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Struktur File
|
||||||
|
|
||||||
|
```
|
||||||
|
preprocessing/
|
||||||
|
├── __init__.py # Export modul
|
||||||
|
├── column_mapping.json # Konfigurasi mapping kolom
|
||||||
|
├── standarisasi_kolom.py # Fungsi standarisasi
|
||||||
|
├── text_preprocessing.py # Fungsi preprocessing teks
|
||||||
|
└── data_pipeline.py # Pipeline lengkap
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Komponen
|
||||||
|
|
||||||
|
### 1. KolomStandarisasi
|
||||||
|
|
||||||
|
**Fungsi:** Mengubah kolom Excel yang berbeda-beda menjadi kolom STANDAR.
|
||||||
|
|
||||||
|
**Kolom Standar:**
|
||||||
|
| Kolom | Deskripsi |
|
||||||
|
|-------|-----------|
|
||||||
|
| `nama` | Nama lengkap alumni |
|
||||||
|
| `perusahaan` | Nama perusahaan/instansi |
|
||||||
|
| `jabatan` | Jabatan/posisi |
|
||||||
|
| `nim` | Nomor Induk Mahasiswa |
|
||||||
|
|
||||||
|
**Contoh Penggunaan:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
from preprocessing import KolomStandarisasi
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
# Inisialisasi
|
||||||
|
std = KolomStandarisasi()
|
||||||
|
|
||||||
|
# Baca Excel
|
||||||
|
df = pd.read_excel("data_2024.xlsx")
|
||||||
|
|
||||||
|
# Standarisasi
|
||||||
|
df_standar = std.standarisasi(df, tahun="2024")
|
||||||
|
|
||||||
|
print(df_standar.columns.tolist())
|
||||||
|
# Output: ['nama', 'perusahaan', 'jabatan', 'nim']
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. DataPipeline
|
||||||
|
|
||||||
|
**Fungsi:** Pipeline lengkap dari import sampai siap ML.
|
||||||
|
|
||||||
|
**Alur Kerja:**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────┐
|
||||||
|
│ STEP 1: Standarisasi Kolom │
|
||||||
|
│ - Ubah kolom berbeda → kolom standar │
|
||||||
|
└────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌────────────────────────────────────────┐
|
||||||
|
│ STEP 2: Gabung Data │
|
||||||
|
│ - Gabungkan data dari semua tahun │
|
||||||
|
└────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌────────────────────────────────────────┐
|
||||||
|
│ STEP 3: Data Cleaning │
|
||||||
|
│ - Hapus duplikat │
|
||||||
|
│ - Hapus data kosong │
|
||||||
|
└────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌────────────────────────────────────────┐
|
||||||
|
│ STEP 4: Text Preprocessing │
|
||||||
|
│ - Case folding │
|
||||||
|
│ - Tokenizing │
|
||||||
|
│ - Stopword removal │
|
||||||
|
│ - Stemming │
|
||||||
|
└────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌────────────────────────────────────────┐
|
||||||
|
│ OUTPUT: Data siap untuk TF-IDF │
|
||||||
|
└────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Contoh Penggunaan:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
from preprocessing import DataPipeline
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
# Inisialisasi
|
||||||
|
pipeline = DataPipeline()
|
||||||
|
|
||||||
|
# Tambah data dari berbagai tahun
|
||||||
|
pipeline.tambah_data(df_2019, "2019")
|
||||||
|
pipeline.tambah_data(df_2024, "2024")
|
||||||
|
|
||||||
|
# Jalankan pipeline
|
||||||
|
result = pipeline.jalankan()
|
||||||
|
|
||||||
|
print(result.head())
|
||||||
|
# Output: DataFrame dengan kolom ['nama', 'perusahaan', 'jabatan', 'nim', 'tahun', 'teks_bersih']
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. preprocess_text
|
||||||
|
|
||||||
|
**Fungsi:** Membersihkan teks agar siap untuk TF-IDF.
|
||||||
|
|
||||||
|
**Proses:**
|
||||||
|
|
||||||
|
| Step | Input | Output |
|
||||||
|
|------|-------|--------|
|
||||||
|
| Case Folding | "Software Engineer" | "software engineer" |
|
||||||
|
| Tokenizing | "software engineer" | ["software", "engineer"] |
|
||||||
|
| Remove Special Chars | ["3", "-", "staff"] | ["staff"] |
|
||||||
|
| Remove Stopwords | ["di", "google"] | ["google"] |
|
||||||
|
| Stemming | ["bekerja", "pekerjaan"] | ["kerja", "kerja"] |
|
||||||
|
|
||||||
|
**Contoh Penggunaan:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
from preprocessing import preprocess_text
|
||||||
|
|
||||||
|
# Input teks mentah
|
||||||
|
teks = "Google Indonesia 3 - Staff"
|
||||||
|
|
||||||
|
# Preprocess
|
||||||
|
hasil = preprocess_text(teks)
|
||||||
|
|
||||||
|
print(hasil)
|
||||||
|
# Output: "google indonesia staff"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Column Mapping
|
||||||
|
|
||||||
|
File `column_mapping.json` berisi mapping dari setiap tahun:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"2019": {
|
||||||
|
"nama": "Nama Lengkap",
|
||||||
|
"perusahaan": "Perusahaan",
|
||||||
|
"jabatan": "Jabatan",
|
||||||
|
"nim": "NIM"
|
||||||
|
},
|
||||||
|
"2024": {
|
||||||
|
"nama": "4. Nama Lengkap / nmmhsmsmh...",
|
||||||
|
"perusahaan": "9. Apa nama perusahaan...",
|
||||||
|
"jabatan": "10. Bila Anda berwiraswasta...",
|
||||||
|
"nim": "3. NIM / nimhsmsmh..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ⚠️ Yang Perlu Diisi
|
||||||
|
|
||||||
|
Buka setiap file Excel dan catat nama kolom untuk:
|
||||||
|
|
||||||
|
| Tahun | Status |
|
||||||
|
|-------|--------|
|
||||||
|
| 2019 | ❌ Perlu diisi |
|
||||||
|
| 2020 | ❌ Perlu diisi |
|
||||||
|
| 2021 | ❌ Perlu diisi |
|
||||||
|
| 2022 | ❌ Perlu diisi |
|
||||||
|
| 2023 | ❌ Perlu diisi |
|
||||||
|
| 2024 | ✅ Sudah diisi |
|
||||||
|
| 2025 | ❌ Perlu diisi |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
### 1. Siapkan Mapping Kolom
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Buka Excel 2019-2025
|
||||||
|
# Catat nama kolom untuk: nama, perusahaan, jabatan, nim
|
||||||
|
# Update column_mapping.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Jalankan Pipeline
|
||||||
|
|
||||||
|
```python
|
||||||
|
from preprocessing import DataPipeline
|
||||||
|
|
||||||
|
# Inisialisasi
|
||||||
|
pipeline = DataPipeline()
|
||||||
|
|
||||||
|
# Tambah data
|
||||||
|
for tahun in ["2019", "2020", "2021", "2022", "2023", "2024", "2025"]:
|
||||||
|
df = pd.read_excel(f"data_{tahun}.xlsx")
|
||||||
|
pipeline.tambah_data(df, tahun)
|
||||||
|
|
||||||
|
# Jalankan
|
||||||
|
result = pipeline.jalankan()
|
||||||
|
|
||||||
|
# Simpan hasil
|
||||||
|
result.to_csv("data_bersih.csv", index=False)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Testing
|
||||||
|
|
||||||
|
Jalankan file untuk test:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Model
|
||||||
|
python preprocessing/standarisasi_kolom.py
|
||||||
|
python preprocessing/text_preprocessing.py
|
||||||
|
python preprocessing/data_pipeline.py
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 Debugging
|
||||||
|
|
||||||
|
### Jika tahun tidak terdeteksi:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Deteksi manual
|
||||||
|
std = KolomStandarisasi()
|
||||||
|
tahun = std.deteksi_tahun(df, nama_file="data_2024.xlsx")
|
||||||
|
print(f"Tahun: {tahun}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Jika kolom tidak cocok:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Lihat mapping yang tersedia
|
||||||
|
std = KolomStandarisasi()
|
||||||
|
mapping = std.get_mapping_tahun("2024")
|
||||||
|
print(mapping)
|
||||||
|
|
||||||
|
# Lihat kolom di DataFrame
|
||||||
|
print(df.columns.tolist())
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Referensi
|
||||||
|
|
||||||
|
- **NLTK:** https://www.nltk.org/
|
||||||
|
- **Sastrawi:** https://github.com/har07/PySastrawi
|
||||||
|
- **Pandas:** https://pandas.pydata.org/
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
# ============================================================
|
||||||
|
# PREPROCESSING MODULE
|
||||||
|
# ============================================================
|
||||||
|
# Berisi pipeline preprocessing data tracer study alumni
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
from .standarisasi_kolom import KolomStandarisasi, gabung_data, bersihkan_data
|
||||||
|
from .text_preprocessing import preprocess_text, preprocess_dataframe
|
||||||
|
from .data_pipeline import DataPipeline, proses_file_excel
|
||||||
|
|
||||||
|
# Export semua komponen yang dibutuhkan
|
||||||
|
__all__ = [
|
||||||
|
'KolomStandarisasi',
|
||||||
|
'gabung_data',
|
||||||
|
'bersihkan_data',
|
||||||
|
'preprocess_text',
|
||||||
|
'preprocess_dataframe',
|
||||||
|
'DataPipeline',
|
||||||
|
'proses_file_excel',
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,359 @@
|
||||||
|
"""
|
||||||
|
=============================================================
|
||||||
|
TF-IDF & K-MEANS CLUSTERING
|
||||||
|
=============================================================
|
||||||
|
|
||||||
|
Tujuan:
|
||||||
|
- TF-IDF transformation untuk teks pekerjaan
|
||||||
|
- K-Means Clustering dengan K=4 (sesuai PRD)
|
||||||
|
- Evaluasi menggunakan Elbow Method dan Silhouette Score
|
||||||
|
|
||||||
|
PRD Reference: Section 14, 15, 16
|
||||||
|
=============================================================
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
from sklearn.feature_extraction.text import TfidfVectorizer
|
||||||
|
from sklearn.cluster import KMeans
|
||||||
|
from sklearn.metrics import silhouette_score
|
||||||
|
import matplotlib
|
||||||
|
matplotlib.use('Agg')
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
# Path configuration
|
||||||
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
DATA_INPUT = os.path.join(BASE_DIR, '..', 'data', 'processed', 'dataset_untuk_clustering.csv')
|
||||||
|
MODEL_OUTPUT = os.path.join(BASE_DIR, '..', 'data', 'processed')
|
||||||
|
PLOT_OUTPUT = os.path.join(BASE_DIR, '..', 'data', 'processed')
|
||||||
|
|
||||||
|
# K yang akan diuji (PRD Section 16.1)
|
||||||
|
K_CANDIDATES = [2, 3, 4, 5, 6]
|
||||||
|
FIXED_K = 4 # K=4 sesuai PRD Section 5.4, 15.2
|
||||||
|
|
||||||
|
|
||||||
|
def load_data():
|
||||||
|
"""Load dataset untuk clustering."""
|
||||||
|
print(">> Loading dataset untuk clustering...")
|
||||||
|
df = pd.read_csv(DATA_INPUT, encoding='utf-8-sig')
|
||||||
|
print(f" Total data: {len(df)} alumni")
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def tfidf_vectorization(texts):
|
||||||
|
"""
|
||||||
|
TF-IDF transformation.
|
||||||
|
|
||||||
|
PRD Section 14:
|
||||||
|
TF-IDF mengubah teks pekerjaan menjadi feature matrix.
|
||||||
|
"""
|
||||||
|
print("\n>> Melakukan TF-IDF Vectorization...")
|
||||||
|
|
||||||
|
# TF-IDF Vectorizer
|
||||||
|
vectorizer = TfidfVectorizer(
|
||||||
|
max_features=500, # Maksimum 500 fitur
|
||||||
|
min_df=2, # Minimum document frequency 2
|
||||||
|
max_df=0.95, # Maximum document frequency 95%
|
||||||
|
ngram_range=(1, 2), # Unigrams dan bigrams
|
||||||
|
lowercase=True,
|
||||||
|
token_pattern=r'\b[a-zA-Z]{2,}\b' # Minimal 2 karakter, hanya alfabet
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fit dan transform
|
||||||
|
tfidf_matrix = vectorizer.fit_transform(texts)
|
||||||
|
feature_names = vectorizer.get_feature_names_out()
|
||||||
|
|
||||||
|
print(f" TF-IDF Matrix shape: {tfidf_matrix.shape}")
|
||||||
|
print(f" Number of features: {len(feature_names)}")
|
||||||
|
print(f" Sample features: {list(feature_names[:10])}...")
|
||||||
|
|
||||||
|
return vectorizer, tfidf_matrix, feature_names
|
||||||
|
|
||||||
|
|
||||||
|
def elbow_method(tfidf_matrix, k_range):
|
||||||
|
"""
|
||||||
|
Elbow Method untuk menentukan K optimal.
|
||||||
|
Menghitung inertia/WCSS untuk setiap K.
|
||||||
|
|
||||||
|
PRD Section 16.2
|
||||||
|
"""
|
||||||
|
print("\n>> Elbow Method Analysis...")
|
||||||
|
|
||||||
|
inertias = []
|
||||||
|
|
||||||
|
for k in k_range:
|
||||||
|
print(f" Testing K={k}...", end=" ")
|
||||||
|
kmeans = KMeans(
|
||||||
|
n_clusters=k,
|
||||||
|
init='k-means++',
|
||||||
|
n_init=10,
|
||||||
|
max_iter=300,
|
||||||
|
random_state=42
|
||||||
|
)
|
||||||
|
kmeans.fit(tfidf_matrix)
|
||||||
|
inertias.append(kmeans.inertia_)
|
||||||
|
print(f"Inertia: {kmeans.inertia_:.2f}")
|
||||||
|
|
||||||
|
return inertias
|
||||||
|
|
||||||
|
|
||||||
|
def silhouette_analysis(tfidf_matrix, k_range):
|
||||||
|
"""
|
||||||
|
Silhouette Score analysis.
|
||||||
|
|
||||||
|
PRD Section 16.3
|
||||||
|
"""
|
||||||
|
print("\n>> Silhouette Score Analysis...")
|
||||||
|
|
||||||
|
silhouettes = []
|
||||||
|
|
||||||
|
for k in k_range:
|
||||||
|
print(f" Testing K={k}...", end=" ")
|
||||||
|
kmeans = KMeans(
|
||||||
|
n_clusters=k,
|
||||||
|
init='k-means++',
|
||||||
|
n_init=10,
|
||||||
|
max_iter=300,
|
||||||
|
random_state=42
|
||||||
|
)
|
||||||
|
labels = kmeans.fit_predict(tfidf_matrix)
|
||||||
|
|
||||||
|
# Hitung silhouette score
|
||||||
|
score = silhouette_score(tfidf_matrix, labels)
|
||||||
|
silhouettes.append(score)
|
||||||
|
print(f"Silhouette: {score:.4f}")
|
||||||
|
|
||||||
|
return silhouettes
|
||||||
|
|
||||||
|
|
||||||
|
def plot_evaluation(k_range, inertias, silhouettes, output_path):
|
||||||
|
"""
|
||||||
|
Plot Elbow Method dan Silhouette Score.
|
||||||
|
"""
|
||||||
|
print("\n>> Generating evaluation plots...")
|
||||||
|
|
||||||
|
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
|
||||||
|
|
||||||
|
# Elbow Plot
|
||||||
|
axes[0].plot(k_range, inertias, 'bo-', linewidth=2, markersize=8)
|
||||||
|
axes[0].axvline(x=FIXED_K, color='r', linestyle='--', label=f'K={FIXED_K} (PRD)')
|
||||||
|
axes[0].set_xlabel('Number of Clusters (K)', fontsize=12)
|
||||||
|
axes[0].set_ylabel('Inertia (WCSS)', fontsize=12)
|
||||||
|
axes[0].set_title('Elbow Method', fontsize=14, fontweight='bold')
|
||||||
|
axes[0].legend()
|
||||||
|
axes[0].grid(True, alpha=0.3)
|
||||||
|
|
||||||
|
# Silhouette Plot
|
||||||
|
axes[1].plot(k_range, silhouettes, 'go-', linewidth=2, markersize=8)
|
||||||
|
axes[1].axvline(x=FIXED_K, color='r', linestyle='--', label=f'K={FIXED_K} (PRD)')
|
||||||
|
axes[1].set_xlabel('Number of Clusters (K)', fontsize=12)
|
||||||
|
axes[1].set_ylabel('Silhouette Score', fontsize=12)
|
||||||
|
axes[1].set_title('Silhouette Score', fontsize=14, fontweight='bold')
|
||||||
|
axes[1].legend()
|
||||||
|
axes[1].grid(True, alpha=0.3)
|
||||||
|
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.savefig(os.path.join(output_path, 'evaluation_plots.png'), dpi=150, bbox_inches='tight')
|
||||||
|
print(f" Plot saved: {output_path}/evaluation_plots.png")
|
||||||
|
|
||||||
|
|
||||||
|
def kmeans_clustering(tfidf_matrix, k, n_init=10):
|
||||||
|
"""
|
||||||
|
K-Means Clustering dengan K tetap.
|
||||||
|
|
||||||
|
PRD Section 15
|
||||||
|
"""
|
||||||
|
print(f"\n>> Running K-Means with K={k}...")
|
||||||
|
|
||||||
|
kmeans = KMeans(
|
||||||
|
n_clusters=k,
|
||||||
|
init='k-means++',
|
||||||
|
n_init=n_init,
|
||||||
|
max_iter=300,
|
||||||
|
random_state=42
|
||||||
|
)
|
||||||
|
|
||||||
|
cluster_labels = kmeans.fit_predict(tfidf_matrix)
|
||||||
|
inertia = kmeans.inertia_
|
||||||
|
|
||||||
|
# Hitung silhouette score
|
||||||
|
silhouette = silhouette_score(tfidf_matrix, cluster_labels)
|
||||||
|
|
||||||
|
print(f" Inertia: {inertia:.2f}")
|
||||||
|
print(f" Silhouette Score: {silhouette:.4f}")
|
||||||
|
|
||||||
|
return kmeans, cluster_labels, inertia, silhouette
|
||||||
|
|
||||||
|
|
||||||
|
def get_cluster_statistics(df, cluster_labels, vectorizer, feature_names):
|
||||||
|
"""
|
||||||
|
Get statistics untuk setiap cluster.
|
||||||
|
"""
|
||||||
|
print("\n>> Analyzing cluster statistics...")
|
||||||
|
|
||||||
|
df_result = df.copy()
|
||||||
|
df_result['cluster'] = cluster_labels
|
||||||
|
|
||||||
|
stats = {}
|
||||||
|
|
||||||
|
for cluster_id in range(FIXED_K):
|
||||||
|
cluster_data = df_result[df_result['cluster'] == cluster_id]
|
||||||
|
stats[cluster_id] = {
|
||||||
|
'jumlah': len(cluster_data),
|
||||||
|
'persentase': len(cluster_data) / len(df_result) * 100
|
||||||
|
}
|
||||||
|
|
||||||
|
# Top terms dari centroid
|
||||||
|
centroid = kmeans.cluster_centers_[cluster_id]
|
||||||
|
top_indices = centroid.argsort()[-10:][::-1]
|
||||||
|
top_terms = [feature_names[i] for i in top_indices]
|
||||||
|
stats[cluster_id]['top_terms'] = top_terms
|
||||||
|
|
||||||
|
print(f"\n Cluster {cluster_id}:")
|
||||||
|
print(f" Jumlah: {stats[cluster_id]['jumlah']} ({stats[cluster_id]['persentase']:.1f}%)")
|
||||||
|
print(f" Top terms: {', '.join(top_terms[:5])}")
|
||||||
|
|
||||||
|
return df_result, stats
|
||||||
|
|
||||||
|
|
||||||
|
def save_results(df_result, kmeans, vectorizer, tfidf_matrix, feature_names,
|
||||||
|
inertia, silhouette, inertias, silhouettes, output_path):
|
||||||
|
"""
|
||||||
|
Simpan hasil clustering.
|
||||||
|
"""
|
||||||
|
print("\n>> Saving results...")
|
||||||
|
|
||||||
|
# 1. Save hasil clustering
|
||||||
|
clustering_output = os.path.join(output_path, 'clustering_results.csv')
|
||||||
|
df_result.to_csv(clustering_output, index=False, encoding='utf-8-sig')
|
||||||
|
print(f" Clustering results: {clustering_output}")
|
||||||
|
|
||||||
|
# 2. Save model info
|
||||||
|
model_info = {
|
||||||
|
'model_version': datetime.now().strftime('%Y%m%d_%H%M%S'),
|
||||||
|
'k': FIXED_K,
|
||||||
|
'inertia': float(inertia),
|
||||||
|
'silhouette_score': float(silhouette),
|
||||||
|
'n_samples': len(df_result),
|
||||||
|
'n_features': len(feature_names),
|
||||||
|
'evaluation': {
|
||||||
|
'k_candidates': K_CANDIDATES,
|
||||||
|
'inertias': [float(i) for i in inertias],
|
||||||
|
'silhouettes': [float(s) for s in silhouettes]
|
||||||
|
},
|
||||||
|
'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
}
|
||||||
|
|
||||||
|
model_output = os.path.join(output_path, 'model_info.json')
|
||||||
|
with open(model_output, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(model_info, f, indent=2, ensure_ascii=False)
|
||||||
|
print(f" Model info: {model_output}")
|
||||||
|
|
||||||
|
# 3. Save cluster statistics
|
||||||
|
cluster_stats_output = os.path.join(output_path, 'cluster_statistics.json')
|
||||||
|
with open(cluster_stats_output, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(cluster_stats, f, indent=2, ensure_ascii=False)
|
||||||
|
print(f" Cluster statistics: {cluster_stats_output}")
|
||||||
|
|
||||||
|
return model_info
|
||||||
|
|
||||||
|
|
||||||
|
def generate_summary(model_info, stats):
|
||||||
|
"""
|
||||||
|
Generate summary report.
|
||||||
|
"""
|
||||||
|
summary = []
|
||||||
|
summary.append("=" * 60)
|
||||||
|
summary.append("CLUSTERING SUMMARY")
|
||||||
|
summary.append(f"Tanggal: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
summary.append("=" * 60)
|
||||||
|
|
||||||
|
summary.append(f"\n[MODEL INFO]")
|
||||||
|
summary.append(f" K = {model_info['k']} (sesuai PRD)")
|
||||||
|
summary.append(f" Inertia: {model_info['inertia']:.2f}")
|
||||||
|
summary.append(f" Silhouette Score: {model_info['silhouette_score']:.4f}")
|
||||||
|
summary.append(f" Total Samples: {model_info['n_samples']}")
|
||||||
|
summary.append(f" Total Features: {model_info['n_features']}")
|
||||||
|
|
||||||
|
summary.append(f"\n[EVALUATION RESULTS]")
|
||||||
|
for i, k in enumerate(model_info['evaluation']['k_candidates']):
|
||||||
|
summary.append(f" K={k}: Inertia={model_info['evaluation']['inertias'][i]:.2f}, "
|
||||||
|
f"Silhouette={model_info['evaluation']['silhouettes'][i]:.4f}")
|
||||||
|
|
||||||
|
summary.append(f"\n[CLUSTER DISTRIBUTION]")
|
||||||
|
for cluster_id, stat in stats.items():
|
||||||
|
summary.append(f" Cluster {cluster_id}: {stat['jumlah']} ({stat['persentase']:.1f}%)")
|
||||||
|
|
||||||
|
summary.append(f"\n[TOP TERMS PER CLUSTER]")
|
||||||
|
for cluster_id, stat in stats.items():
|
||||||
|
summary.append(f" Cluster {cluster_id}: {', '.join(stat['top_terms'][:5])}")
|
||||||
|
|
||||||
|
summary.append("\n" + "=" * 60)
|
||||||
|
|
||||||
|
return "\n".join(summary)
|
||||||
|
|
||||||
|
|
||||||
|
# Global variable untuk simpan kmeans dan stats
|
||||||
|
kmeans = None
|
||||||
|
cluster_stats = None
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""
|
||||||
|
Main function untuk TF-IDF dan K-Means Clustering.
|
||||||
|
"""
|
||||||
|
global kmeans, cluster_stats
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("TF-IDF & K-MEANS CLUSTERING")
|
||||||
|
print(f"K = {FIXED_K} (sesuai PRD)")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# 1. Load data
|
||||||
|
df = load_data()
|
||||||
|
|
||||||
|
# 2. TF-IDF Vectorization
|
||||||
|
texts = df['teks_pekerjaan'].fillna('').tolist()
|
||||||
|
vectorizer, tfidf_matrix, feature_names = tfidf_vectorization(texts)
|
||||||
|
|
||||||
|
# 3. Evaluasi Elbow dan Silhouette
|
||||||
|
inertias = elbow_method(tfidf_matrix, K_CANDIDATES)
|
||||||
|
silhouettes = silhouette_analysis(tfidf_matrix, K_CANDIDATES)
|
||||||
|
|
||||||
|
# 4. Plot evaluasi
|
||||||
|
plot_evaluation(K_CANDIDATES, inertias, silhouettes, PLOT_OUTPUT)
|
||||||
|
|
||||||
|
# 5. K-Means dengan K=4
|
||||||
|
kmeans, cluster_labels, inertia, silhouette = kmeans_clustering(tfidf_matrix, FIXED_K)
|
||||||
|
|
||||||
|
# 6. Get cluster statistics
|
||||||
|
df_result, cluster_stats = get_cluster_statistics(df, cluster_labels, vectorizer, feature_names)
|
||||||
|
|
||||||
|
# 7. Save results
|
||||||
|
model_info = save_results(
|
||||||
|
df_result, kmeans, vectorizer, tfidf_matrix, feature_names,
|
||||||
|
inertia, silhouette, inertias, silhouettes, MODEL_OUTPUT
|
||||||
|
)
|
||||||
|
|
||||||
|
# 8. Generate summary
|
||||||
|
summary = generate_summary(model_info, cluster_stats)
|
||||||
|
print("\n" + summary)
|
||||||
|
|
||||||
|
# Save summary
|
||||||
|
summary_output = os.path.join(MODEL_OUTPUT, 'clustering_summary.txt')
|
||||||
|
with open(summary_output, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(summary)
|
||||||
|
print(f"\n>> Summary saved: {summary_output}")
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("[SUCCESS] CLUSTERING SELESAI!")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
return df_result, model_info, cluster_stats
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
df_result, model_info, cluster_stats = main()
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
{
|
||||||
|
"2019": {
|
||||||
|
"kode_prodi": "Kode Prodi",
|
||||||
|
"nim": "NIM/Nomor Mahasiswa",
|
||||||
|
"nama": "Nama",
|
||||||
|
"tahun_lulus": "Tahun Lulus",
|
||||||
|
"status_pekerjaan": "F8",
|
||||||
|
"jenis_perusahaan": "F1101",
|
||||||
|
"nama_perusahaan": "F5b",
|
||||||
|
"jabatan": "F5c"
|
||||||
|
},
|
||||||
|
"2020": {
|
||||||
|
"kode_prodi": "Kode Prodi",
|
||||||
|
"nim": "NIM/Nomor Mahasiswa",
|
||||||
|
"nama": "Nama",
|
||||||
|
"tahun_lulus": "Tahun Lulus",
|
||||||
|
"status_pekerjaan": "F8",
|
||||||
|
"jenis_perusahaan": "F1101",
|
||||||
|
"nama_perusahaan": "F5b",
|
||||||
|
"jabatan": "F5c"
|
||||||
|
},
|
||||||
|
"2021": {
|
||||||
|
"kode_prodi": "Kode Prodi",
|
||||||
|
"nim": "NIM",
|
||||||
|
"nama": "Nama",
|
||||||
|
"tahun_lulus": "Tahun Lulus",
|
||||||
|
"status_pekerjaan": "F8",
|
||||||
|
"jenis_perusahaan": "F1101",
|
||||||
|
"nama_perusahaan": "F5b",
|
||||||
|
"jabatan": "F5c"
|
||||||
|
},
|
||||||
|
"2022": {
|
||||||
|
"kode_prodi": "kdpstmsmh",
|
||||||
|
"nim": "nimhsmsmh",
|
||||||
|
"nama": "nmmhsmsmh",
|
||||||
|
"tahun_lulus": "tahun_lulus",
|
||||||
|
"status_pekerjaan": "f8",
|
||||||
|
"jenis_perusahaan": "f1101",
|
||||||
|
"nama_perusahaan": "f5b",
|
||||||
|
"jabatan": "f5c"
|
||||||
|
},
|
||||||
|
"2023": {
|
||||||
|
"kode_prodi": "kdpstmsmh",
|
||||||
|
"nim": "nimhsmsmh",
|
||||||
|
"nama": "nmmhsmsmh",
|
||||||
|
"tahun_lulus": "tahun_lulus",
|
||||||
|
"status_pekerjaan": "f8",
|
||||||
|
"jenis_perusahaan": "f1101",
|
||||||
|
"nama_perusahaan": "f5b",
|
||||||
|
"jabatan": "f5c"
|
||||||
|
},
|
||||||
|
"2024": {
|
||||||
|
"kode_prodi": "2. Kode Program Studi / kdpstmsmh",
|
||||||
|
"nim": "3. NIM / nimhsmsmh (Gunakan Huruf Besar) ",
|
||||||
|
"nama": "4. Nama Lengkap / nmmhsmsmh (Gunakan Huruf Besar)",
|
||||||
|
"tahun_lulus": "7. Tahun Lulus / tahun_lulus",
|
||||||
|
"status_pekerjaan": "1. Jelaskan status Anda saat ini? f8",
|
||||||
|
"jenis_perusahaan": "8. Apa jenis perusahaan/instansi/institusi tempat anda bekerja sekarang? f1101",
|
||||||
|
"nama_perusahaan": "9. Apa nama perusahaan/kantor tempat Anda bekerja? f5b",
|
||||||
|
"jabatan": "10. Bila Anda berwiraswasta, apa posisi/jabatan Anda saat ini ? f5c"
|
||||||
|
},
|
||||||
|
"2025": {
|
||||||
|
"kode_prodi": "2. Kode Program Studi / kdpstmsmh",
|
||||||
|
"nim": "3. NIM / nimhsmsmh (Gunakan Huruf Besar) ",
|
||||||
|
"nama": "4. Nama Lengkap / nmmhsmsmh (Gunakan Huruf Besar)",
|
||||||
|
"tahun_lulus": "7. Tahun Lulus / tahun_lulus",
|
||||||
|
"status_pekerjaan": "1. Jelaskan status Anda saat ini? f8",
|
||||||
|
"jenis_perusahaan": "7. Apa jenis perusahaan/instansi/institusi tempat anda bekerja sekarang? f1101",
|
||||||
|
"nama_perusahaan": "9. Apa nama perusahaan/kantor tempat Anda bekerja? f5b",
|
||||||
|
"jabatan": "10. Bila Anda berwiraswasta, apa posisi/jabatan Anda saat ini ? f5c *(Wajib diisi) (Jika Memilih Wiraswasta / Jika f8 adalah 3)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,707 @@
|
||||||
|
"""
|
||||||
|
=============================================================
|
||||||
|
DASHBOARD VISUALIZATION
|
||||||
|
=============================================================
|
||||||
|
|
||||||
|
Generate visualisasi untuk hasil clustering alumni:
|
||||||
|
- Distribusi Kategori C0-C3
|
||||||
|
- Distribusi per Prodi
|
||||||
|
- Distribusi per Tahun
|
||||||
|
- Cluster Analysis
|
||||||
|
- Pie Charts dan Bar Charts
|
||||||
|
|
||||||
|
Output: PNG charts + HTML Dashboard
|
||||||
|
=============================================================
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
import matplotlib
|
||||||
|
matplotlib.use('Agg')
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import seaborn as sns
|
||||||
|
|
||||||
|
# Set style
|
||||||
|
plt.style.use('seaborn-v0_8-whitegrid')
|
||||||
|
sns.set_palette("husl")
|
||||||
|
|
||||||
|
# Path configuration
|
||||||
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
DATA_DIR = os.path.join(BASE_DIR, '..', 'data', 'processed')
|
||||||
|
RESULT_DIR = os.path.join(BASE_DIR, '..', 'data', 'result')
|
||||||
|
OUTPUT_DIR = os.path.join(BASE_DIR, '..', 'data', 'processed', 'dashboard')
|
||||||
|
|
||||||
|
# Create output directory
|
||||||
|
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
# Colors
|
||||||
|
COLORS = {
|
||||||
|
'C0': '#2ecc71', # Green - Sesuai Profil
|
||||||
|
'C1': '#3498db', # Blue - Bidang Diketahui
|
||||||
|
'C2': '#f39c12', # Orange - Bidang Tidak Jelas
|
||||||
|
'C3': '#e74c3c', # Red - Belum Bekerja
|
||||||
|
'TIF': '#9b59b6', # Purple
|
||||||
|
'MIF': '#1abc9c', # Teal
|
||||||
|
'TKK': '#e67e22' # Orange
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def load_data():
|
||||||
|
"""Load semua data yang diperlukan."""
|
||||||
|
print(">> Loading data...")
|
||||||
|
|
||||||
|
# Interpretasi results (from result folder)
|
||||||
|
df = pd.read_csv(os.path.join(RESULT_DIR, 'interpretasi_results.csv'), encoding='utf-8-sig')
|
||||||
|
|
||||||
|
# Processed dataset
|
||||||
|
processed_df = pd.read_csv(os.path.join(DATA_DIR, 'processed_dataset.csv'), encoding='utf-8-sig')
|
||||||
|
|
||||||
|
# Cluster statistics
|
||||||
|
with open(os.path.join(DATA_DIR, 'cluster_statistics.json'), 'r') as f:
|
||||||
|
cluster_stats = json.load(f)
|
||||||
|
|
||||||
|
print(f" Total data: {len(df)}")
|
||||||
|
|
||||||
|
return df, processed_df, cluster_stats
|
||||||
|
|
||||||
|
|
||||||
|
def create_pie_chart_kategori(df, output_path):
|
||||||
|
"""Pie chart distribusi kategori C0-C3."""
|
||||||
|
print(">> Creating Pie Chart - Kategori C0-C3...")
|
||||||
|
|
||||||
|
kategori_counts = df['kategori'].value_counts()
|
||||||
|
labels = kategori_counts.index.tolist()
|
||||||
|
sizes = kategori_counts.values
|
||||||
|
colors = [COLORS.get(k, '#95a5a6') for k in labels]
|
||||||
|
|
||||||
|
fig, ax = plt.subplots(figsize=(10, 8))
|
||||||
|
|
||||||
|
wedges, texts, autotexts = ax.pie(
|
||||||
|
sizes,
|
||||||
|
labels=labels,
|
||||||
|
colors=colors,
|
||||||
|
autopct='%1.1f%%',
|
||||||
|
startangle=90,
|
||||||
|
explode=[0.02] * len(labels)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Style
|
||||||
|
for autotext in autotexts:
|
||||||
|
autotext.set_fontsize(12)
|
||||||
|
autotext.set_fontweight('bold')
|
||||||
|
|
||||||
|
ax.set_title('Distribusi Kategori Alumni (C0-C3)\n', fontsize=16, fontweight='bold')
|
||||||
|
|
||||||
|
# Add legend
|
||||||
|
legend_labels = [
|
||||||
|
f'C0: Sesuai Profil ({kategori_counts.get("C0", 0)})',
|
||||||
|
f'C1: Bidang Diketahui ({kategori_counts.get("C1", 0)})',
|
||||||
|
f'C2: Bidang Tidak Jelas ({kategori_counts.get("C2", 0)})',
|
||||||
|
f'C3: Belum Bekerja ({kategori_counts.get("C3", 0)})'
|
||||||
|
]
|
||||||
|
ax.legend(wedges, legend_labels, title="Kategori", loc="center left", bbox_to_anchor=(1, 0, 0.5, 1))
|
||||||
|
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.savefig(output_path, dpi=150, bbox_inches='tight', facecolor='white')
|
||||||
|
plt.close()
|
||||||
|
|
||||||
|
print(f" Saved: {output_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def create_bar_chart_prodi(df, output_path):
|
||||||
|
"""Bar chart distribusi C0-C3 per prodi."""
|
||||||
|
print(">> Creating Bar Chart - Distribusi per Prodi...")
|
||||||
|
|
||||||
|
# Map prodi codes
|
||||||
|
prodi_map = {'55301': 'TIF', '57401': 'MIF', '56401': 'TKK'}
|
||||||
|
df['prodi'] = df['kode_prodi'].astype(str).str.replace('.0', '', regex=False).str.strip()
|
||||||
|
df['prodi'] = df['prodi'].map(prodi_map).fillna('Lainnya')
|
||||||
|
|
||||||
|
# Group by prodi and kategori
|
||||||
|
grouped = df.groupby(['prodi', 'kategori']).size().unstack(fill_value=0)
|
||||||
|
|
||||||
|
fig, ax = plt.subplots(figsize=(12, 7))
|
||||||
|
|
||||||
|
categories = ['C0', 'C1', 'C2', 'C3']
|
||||||
|
colors = [COLORS[c] for c in categories]
|
||||||
|
|
||||||
|
x = np.arange(len(grouped.index))
|
||||||
|
width = 0.2
|
||||||
|
|
||||||
|
for i, cat in enumerate(categories):
|
||||||
|
if cat in grouped.columns:
|
||||||
|
values = grouped[cat].values
|
||||||
|
bars = ax.bar(x + i * width, values, width, label=cat, color=colors[i])
|
||||||
|
|
||||||
|
# Add value labels
|
||||||
|
for bar in bars:
|
||||||
|
height = bar.get_height()
|
||||||
|
if height > 0:
|
||||||
|
ax.annotate(f'{int(height)}',
|
||||||
|
xy=(bar.get_x() + bar.get_width() / 2, height),
|
||||||
|
xytext=(0, 3),
|
||||||
|
textcoords="offset points",
|
||||||
|
ha='center', va='bottom', fontsize=9)
|
||||||
|
|
||||||
|
ax.set_xlabel('Program Studi', fontsize=12)
|
||||||
|
ax.set_ylabel('Jumlah Alumni', fontsize=12)
|
||||||
|
ax.set_title('Distribusi Kategori C0-C3 per Program Studi\n', fontsize=16, fontweight='bold')
|
||||||
|
ax.set_xticks(x + width * 1.5)
|
||||||
|
ax.set_xticklabels(grouped.index)
|
||||||
|
ax.legend(title='Kategori')
|
||||||
|
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.savefig(output_path, dpi=150, bbox_inches='tight', facecolor='white')
|
||||||
|
plt.close()
|
||||||
|
|
||||||
|
print(f" Saved: {output_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def create_stacked_bar_tahun(df, output_path):
|
||||||
|
"""Stacked bar chart distribusi per tahun."""
|
||||||
|
print(">> Creating Stacked Bar - Distribusi per Tahun...")
|
||||||
|
|
||||||
|
# Get tahun from processed dataset
|
||||||
|
processed_df = pd.read_csv(os.path.join(DATA_DIR, 'processed_dataset.csv'), encoding='utf-8-sig')
|
||||||
|
df = df.merge(processed_df[['nim', 'tahun_lulus']], on='nim', how='left')
|
||||||
|
|
||||||
|
# Group by tahun and kategori
|
||||||
|
grouped = df.groupby(['tahun_lulus', 'kategori']).size().unstack(fill_value=0)
|
||||||
|
|
||||||
|
fig, ax = plt.subplots(figsize=(12, 7))
|
||||||
|
|
||||||
|
categories = ['C0', 'C1', 'C2', 'C3']
|
||||||
|
colors = [COLORS[c] for c in categories]
|
||||||
|
bottom = np.zeros(len(grouped.index))
|
||||||
|
|
||||||
|
for i, cat in enumerate(categories):
|
||||||
|
if cat in grouped.columns:
|
||||||
|
values = grouped[cat].values
|
||||||
|
ax.bar(grouped.index.astype(str), values, bottom=bottom, label=cat, color=colors[i])
|
||||||
|
bottom += values
|
||||||
|
|
||||||
|
ax.set_xlabel('Tahun Lulus', fontsize=12)
|
||||||
|
ax.set_ylabel('Jumlah Alumni', fontsize=12)
|
||||||
|
ax.set_title('Distribusi Kategori C0-C3 per Tahun Lulus\n', fontsize=16, fontweight='bold')
|
||||||
|
ax.legend(title='Kategori', loc='upper right')
|
||||||
|
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.savefig(output_path, dpi=150, bbox_inches='tight', facecolor='white')
|
||||||
|
plt.close()
|
||||||
|
|
||||||
|
print(f" Saved: {output_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def create_donut_chart_cluster(df, output_path):
|
||||||
|
"""Donut chart distribusi cluster."""
|
||||||
|
print(">> Creating Donut Chart - Distribusi Cluster...")
|
||||||
|
|
||||||
|
cluster_counts = df['cluster'].value_counts().sort_index()
|
||||||
|
|
||||||
|
fig, ax = plt.subplots(figsize=(10, 8))
|
||||||
|
|
||||||
|
colors = ['#3498db', '#2ecc71', '#f39c12', '#9b59b6']
|
||||||
|
labels = [f'Cluster {i}' for i in cluster_counts.index]
|
||||||
|
|
||||||
|
wedges, texts, autotexts = ax.pie(
|
||||||
|
cluster_counts.values,
|
||||||
|
labels=labels,
|
||||||
|
colors=colors,
|
||||||
|
autopct='%1.1f%%',
|
||||||
|
startangle=90,
|
||||||
|
pctdistance=0.75,
|
||||||
|
explode=[0.02] * len(labels)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create donut hole
|
||||||
|
centre_circle = plt.Circle((0, 0), 0.50, fc='white')
|
||||||
|
ax.add_patch(centre_circle)
|
||||||
|
|
||||||
|
# Style
|
||||||
|
for autotext in autotexts:
|
||||||
|
autotext.set_fontsize(11)
|
||||||
|
autotext.set_fontweight('bold')
|
||||||
|
|
||||||
|
ax.set_title('Distribusi Cluster K-Means (K=4)\n', fontsize=16, fontweight='bold')
|
||||||
|
|
||||||
|
# Add legend
|
||||||
|
legend_labels = [f'Cluster {i}: {cluster_counts.get(i, 0)}' for i in range(4)]
|
||||||
|
ax.legend(wedges, legend_labels, title="Cluster", loc="center left", bbox_to_anchor=(1, 0, 0.5, 1))
|
||||||
|
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.savefig(output_path, dpi=150, bbox_inches='tight', facecolor='white')
|
||||||
|
plt.close()
|
||||||
|
|
||||||
|
print(f" Saved: {output_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def create_summary_cards(stats, output_path):
|
||||||
|
"""Generate summary cards as image."""
|
||||||
|
print(">> Creating Summary Cards...")
|
||||||
|
|
||||||
|
fig, axes = plt.subplots(2, 4, figsize=(16, 8))
|
||||||
|
fig.patch.set_facecolor('white')
|
||||||
|
|
||||||
|
# Total Alumni
|
||||||
|
ax = axes[0, 0]
|
||||||
|
ax.text(0.5, 0.7, '535', fontsize=36, ha='center', fontweight='bold', color='#2c3e50')
|
||||||
|
ax.text(0.5, 0.3, 'Total Alumni', fontsize=12, ha='center', color='#7f8c8d')
|
||||||
|
ax.axis('off')
|
||||||
|
|
||||||
|
# C0
|
||||||
|
ax = axes[0, 1]
|
||||||
|
ax.text(0.5, 0.7, '18', fontsize=36, ha='center', fontweight='bold', color=COLORS['C0'])
|
||||||
|
ax.text(0.5, 0.3, 'C0 - Sesuai Profil', fontsize=10, ha='center', color='#7f8c8d')
|
||||||
|
ax.axis('off')
|
||||||
|
|
||||||
|
# C1
|
||||||
|
ax = axes[0, 2]
|
||||||
|
ax.text(0.5, 0.7, '85', fontsize=36, ha='center', fontweight='bold', color=COLORS['C1'])
|
||||||
|
ax.text(0.5, 0.3, 'C1 - Bidang Diketahui', fontsize=10, ha='center', color='#7f8c8d')
|
||||||
|
ax.axis('off')
|
||||||
|
|
||||||
|
# C2
|
||||||
|
ax = axes[0, 3]
|
||||||
|
ax.text(0.5, 0.7, '416', fontsize=36, ha='center', fontweight='bold', color=COLORS['C2'])
|
||||||
|
ax.text(0.5, 0.3, 'C2 - Bidang Tidak Jelas', fontsize=10, ha='center', color='#7f8c8d')
|
||||||
|
ax.axis('off')
|
||||||
|
|
||||||
|
# C3
|
||||||
|
ax = axes[1, 0]
|
||||||
|
ax.text(0.5, 0.7, '16', fontsize=36, ha='center', fontweight='bold', color=COLORS['C3'])
|
||||||
|
ax.text(0.5, 0.3, 'C3 - Belum Bekerja', fontsize=10, ha='center', color='#7f8c8d')
|
||||||
|
ax.axis('off')
|
||||||
|
|
||||||
|
# TIF
|
||||||
|
ax = axes[1, 1]
|
||||||
|
ax.text(0.5, 0.7, '225', fontsize=36, ha='center', fontweight='bold', color=COLORS['TIF'])
|
||||||
|
ax.text(0.5, 0.3, 'TIF - Teknik Informatika', fontsize=10, ha='center', color='#7f8c8d')
|
||||||
|
ax.axis('off')
|
||||||
|
|
||||||
|
# MIF
|
||||||
|
ax = axes[1, 2]
|
||||||
|
ax.text(0.5, 0.7, '165', fontsize=36, ha='center', fontweight='bold', color=COLORS['MIF'])
|
||||||
|
ax.text(0.5, 0.3, 'MIF - Manajemen Informatika', fontsize=10, ha='center', color='#7f8c8d')
|
||||||
|
ax.axis('off')
|
||||||
|
|
||||||
|
# TKK
|
||||||
|
ax = axes[1, 3]
|
||||||
|
ax.text(0.5, 0.7, '145', fontsize=36, ha='center', fontweight='bold', color=COLORS['TKK'])
|
||||||
|
ax.text(0.5, 0.3, 'TKK - Teknik Komputer', fontsize=10, ha='center', color='#7f8c8d')
|
||||||
|
ax.axis('off')
|
||||||
|
|
||||||
|
plt.suptitle('Dashboard Summary - Sistem Clustering Alumni', fontsize=18, fontweight='bold', y=1.02)
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.savefig(output_path, dpi=150, bbox_inches='tight', facecolor='white')
|
||||||
|
plt.close()
|
||||||
|
|
||||||
|
print(f" Saved: {output_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def create_html_dashboard(df, output_path):
|
||||||
|
"""Generate interactive HTML dashboard."""
|
||||||
|
print(">> Creating HTML Dashboard...")
|
||||||
|
|
||||||
|
# Calculate stats
|
||||||
|
kategori_counts = df['kategori'].value_counts()
|
||||||
|
total = len(df)
|
||||||
|
|
||||||
|
# Prodi distribution
|
||||||
|
prodi_map = {'55301': 'TIF', '57401': 'MIF', '56401': 'TKK'}
|
||||||
|
df['prodi'] = df['kode_prodi'].astype(str).str.replace('.0', '', regex=False).str.strip()
|
||||||
|
df['prodi'] = df['prodi'].map(prodi_map).fillna('Lainnya')
|
||||||
|
prodi_counts = df['prodi'].value_counts()
|
||||||
|
|
||||||
|
html_content = f"""
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Dashboard Clustering Alumni - Politeknik Negeri Jember</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<style>
|
||||||
|
* {{
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}}
|
||||||
|
|
||||||
|
body {{
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.container {{
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.header {{
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 30px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.header h1 {{
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 28px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.header p {{
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 14px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.cards {{
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.card {{
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 25px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.card:hover {{
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.card-value {{
|
||||||
|
font-size: 42px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.card-label {{
|
||||||
|
font-size: 14px;
|
||||||
|
color: #7f8c8d;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.card-c0 {{ color: #2ecc71; }}
|
||||||
|
.card-c1 {{ color: #3498db; }}
|
||||||
|
.card-c2 {{ color: #f39c12; }}
|
||||||
|
.card-c3 {{ color: #e74c3c; }}
|
||||||
|
.card-tif {{ color: #9b59b6; }}
|
||||||
|
.card-mif {{ color: #1abc9c; }}
|
||||||
|
.card-tkk {{ color: #e67e22; }}
|
||||||
|
|
||||||
|
.grid {{
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.chart-container {{
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 25px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.chart-title {{
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.chart-wrapper {{
|
||||||
|
position: relative;
|
||||||
|
height: 300px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.definitions {{
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 25px;
|
||||||
|
margin-top: 20px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.definitions h2 {{
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.definition-item {{
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.definition-badge {{
|
||||||
|
padding: 8px 15px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
margin-right: 15px;
|
||||||
|
min-width: 50px;
|
||||||
|
text-align: center;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.badge-c0 {{ background: #2ecc71; }}
|
||||||
|
.badge-c1 {{ background: #3498db; }}
|
||||||
|
.badge-c2 {{ background: #f39c12; }}
|
||||||
|
.badge-c3 {{ background: #e74c3c; }}
|
||||||
|
|
||||||
|
.footer {{
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 30px;
|
||||||
|
color: white;
|
||||||
|
font-size: 12px;
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>Dashboard Clustering Alumni</h1>
|
||||||
|
<p>Sistem Pengelompokan Pekerjaan Alumni dengan Kompetensi Lulusan - Jurusan Teknologi Informasi, Politeknik Negeri Jember</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cards">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-value card-tif">{prodi_counts.get('TIF', 0)}</div>
|
||||||
|
<div class="card-label">Teknik Informatika (TIF)</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-value card-mif">{prodi_counts.get('MIF', 0)}</div>
|
||||||
|
<div class="card-label">Manajemen Informatika (MIF)</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-value card-tkk">{prodi_counts.get('TKK', 0)}</div>
|
||||||
|
<div class="card-label">Teknik Komputer (TKK)</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-value" style="color:#2c3e50">{total}</div>
|
||||||
|
<div class="card-label">Total Alumni</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cards">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-value card-c0">{kategori_counts.get('C0', 0)}</div>
|
||||||
|
<div class="card-label">Sesuai Profil (C0)</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-value card-c1">{kategori_counts.get('C1', 0)}</div>
|
||||||
|
<div class="card-label">Bidang Diketahui (C1)</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-value card-c2">{kategori_counts.get('C2', 0)}</div>
|
||||||
|
<div class="card-label">Bidang Tidak Jelas (C2)</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-value card-c3">{kategori_counts.get('C3', 0)}</div>
|
||||||
|
<div class="card-label">Belum Bekerja (C3)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<div class="chart-container">
|
||||||
|
<div class="chart-title">Distribusi Kategori Alumni (C0-C3)</div>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="kategoriChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="chart-container">
|
||||||
|
<div class="chart-title">Distribusi per Program Studi</div>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="prodiChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid" style="margin-top:20px">
|
||||||
|
<div class="chart-container">
|
||||||
|
<div class="chart-title">Distribusi Cluster K-Means (K=4)</div>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="clusterChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="chart-container">
|
||||||
|
<div class="chart-title">Match Rate per Program Studi</div>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<canvas id="matchChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="definitions">
|
||||||
|
<h2>Definisi Kategori</h2>
|
||||||
|
<div class="definition-item">
|
||||||
|
<span class="definition-badge badge-c0">C0</span>
|
||||||
|
<span><strong>Sesuai Profil Lulusan:</strong> Alumni bekerja dan pekerjaan match dengan kamus kata kunci profil lulusannya.</span>
|
||||||
|
</div>
|
||||||
|
<div class="definition-item">
|
||||||
|
<span class="definition-badge badge-c1">C1</span>
|
||||||
|
<span><strong>Bidang Diketahui:</strong> Alumni bekerja, bidang pekerjaan jelas, tapi tidak match kamus prodi.</span>
|
||||||
|
</div>
|
||||||
|
<div class="definition-item">
|
||||||
|
<span class="definition-badge badge-c2">C2</span>
|
||||||
|
<span><strong>Bidang Tidak Teridentifikasi:</strong> Alumni bekerja tapi informasi pekerjaan kurang jelas atau terlalu umum.</span>
|
||||||
|
</div>
|
||||||
|
<div class="definition-item">
|
||||||
|
<span class="definition-badge badge-c3">C3</span>
|
||||||
|
<span><strong>Belum/Tidak Bekerja:</strong> Alumni belum bekerja atau tidak bekerja saat tracer study.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
|
||||||
|
<p>Sistem Clustering Alumni - Politeknik Negeri Jember</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Kategori Chart
|
||||||
|
new Chart(document.getElementById('kategoriChart'), {{
|
||||||
|
type: 'doughnut',
|
||||||
|
data: {{
|
||||||
|
labels: ['C0 - Sesuai Profil', 'C1 - Bidang Diketahui', 'C2 - Bidang Tidak Jelas', 'C3 - Belum Bekerja'],
|
||||||
|
datasets: [{{
|
||||||
|
data: [
|
||||||
|
{kategori_counts.get('C0', 0)},
|
||||||
|
{kategori_counts.get('C1', 0)},
|
||||||
|
{kategori_counts.get('C2', 0)},
|
||||||
|
{kategori_counts.get('C3', 0)}
|
||||||
|
],
|
||||||
|
backgroundColor: ['#2ecc71', '#3498db', '#f39c12', '#e74c3c']
|
||||||
|
}}]
|
||||||
|
}},
|
||||||
|
options: {{
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {{
|
||||||
|
legend: {{ position: 'bottom' }}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}});
|
||||||
|
|
||||||
|
// Prodi Chart
|
||||||
|
new Chart(document.getElementById('prodiChart'), {{
|
||||||
|
type: 'bar',
|
||||||
|
data: {{
|
||||||
|
labels: ['TIF', 'MIF', 'TKK'],
|
||||||
|
datasets: [{{
|
||||||
|
label: 'Jumlah Alumni',
|
||||||
|
data: [
|
||||||
|
{prodi_counts.get('TIF', 0)},
|
||||||
|
{prodi_counts.get('MIF', 0)},
|
||||||
|
{prodi_counts.get('TKK', 0)}
|
||||||
|
],
|
||||||
|
backgroundColor: ['#9b59b6', '#1abc9c', '#e67e22']
|
||||||
|
}}]
|
||||||
|
}},
|
||||||
|
options: {{
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {{
|
||||||
|
legend: {{ display: false }}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}});
|
||||||
|
|
||||||
|
// Cluster Chart
|
||||||
|
new Chart(document.getElementById('clusterChart'), {{
|
||||||
|
type: 'pie',
|
||||||
|
data: {{
|
||||||
|
labels: ['Cluster 0', 'Cluster 1', 'Cluster 2', 'Cluster 3'],
|
||||||
|
datasets: [{{
|
||||||
|
data: [326, 27, 66, 100],
|
||||||
|
backgroundColor: ['#3498db', '#2ecc71', '#f39c12', '#9b59b6']
|
||||||
|
}}]
|
||||||
|
}},
|
||||||
|
options: {{
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false
|
||||||
|
}}
|
||||||
|
}});
|
||||||
|
|
||||||
|
// Match Rate Chart
|
||||||
|
new Chart(document.getElementById('matchChart'), {{
|
||||||
|
type: 'bar',
|
||||||
|
data: {{
|
||||||
|
labels: ['TIF', 'MIF', 'TKK'],
|
||||||
|
datasets: [{{
|
||||||
|
label: 'Match Rate (%)',
|
||||||
|
data: [2.22, 1.82, 6.90],
|
||||||
|
backgroundColor: ['#9b59b6', '#1abc9c', '#e67e22']
|
||||||
|
}}]
|
||||||
|
}},
|
||||||
|
options: {{
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {{
|
||||||
|
y: {{
|
||||||
|
beginAtZero: true,
|
||||||
|
max: 10
|
||||||
|
}}
|
||||||
|
}},
|
||||||
|
plugins: {{
|
||||||
|
legend: {{ display: false }}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
with open(output_path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(html_content)
|
||||||
|
|
||||||
|
print(f" Saved: {output_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main function."""
|
||||||
|
print("=" * 60)
|
||||||
|
print("GENERATING DASHBOARD VISUALIZATIONS")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Load data
|
||||||
|
df, processed_df, cluster_stats = load_data()
|
||||||
|
|
||||||
|
# Create visualizations
|
||||||
|
create_summary_cards(cluster_stats, os.path.join(OUTPUT_DIR, 'summary_cards.png'))
|
||||||
|
create_pie_chart_kategori(df, os.path.join(OUTPUT_DIR, 'kategori_pie.png'))
|
||||||
|
create_bar_chart_prodi(df, os.path.join(OUTPUT_DIR, 'prodi_bar.png'))
|
||||||
|
create_donut_chart_cluster(df, os.path.join(OUTPUT_DIR, 'cluster_donut.png'))
|
||||||
|
create_stacked_bar_tahun(df, os.path.join(OUTPUT_DIR, 'tahun_stacked.png'))
|
||||||
|
create_html_dashboard(df, os.path.join(OUTPUT_DIR, 'dashboard.html'))
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("[SUCCESS] DASHBOARD VISUALIZATIONS COMPLETE!")
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"\nOutput directory: {OUTPUT_DIR}")
|
||||||
|
print("Files generated:")
|
||||||
|
print(" - summary_cards.png")
|
||||||
|
print(" - kategori_pie.png")
|
||||||
|
print(" - prodi_bar.png")
|
||||||
|
print(" - cluster_donut.png")
|
||||||
|
print(" - tahun_stacked.png")
|
||||||
|
print(" - dashboard.html (Interactive)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,468 @@
|
||||||
|
# ============================================================
|
||||||
|
# PIPELINE PREPROCESSING LENGKAP
|
||||||
|
# ============================================================
|
||||||
|
# Fungsi: Pipeline preprocessing data tracer study alumni
|
||||||
|
# dari import Excel sampai siap untuk ML
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
import re
|
||||||
|
from typing import List, Dict, Optional, Tuple
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Import modul standarisasi
|
||||||
|
from .standarisasi_kolom import (
|
||||||
|
KolomStandarisasi,
|
||||||
|
gabung_data,
|
||||||
|
bersihkan_data,
|
||||||
|
COLUMN_MAPPING
|
||||||
|
)
|
||||||
|
|
||||||
|
# Import text preprocessing
|
||||||
|
from .text_preprocessing import preprocess_text
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# KELAS: DataPipeline
|
||||||
|
# ============================================================
|
||||||
|
# Pipeline lengkap untuk memproses data tracer study
|
||||||
|
# dari berbagai tahun menjadi data siap ML
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
class DataPipeline:
|
||||||
|
"""
|
||||||
|
Pipeline lengkap untuk preprocessing data tracer study.
|
||||||
|
|
||||||
|
ALUR KERJA:
|
||||||
|
============
|
||||||
|
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ INPUT: File Excel dari berbagai tahun (2019-2025) │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ STEP 1: STANDARISASI KOLOM │
|
||||||
|
│ - Ubah nama kolom berbeda → kolom standar │
|
||||||
|
│ - Kolom standar: nama, perusahaan, jabatan, nim │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ STEP 2: GABUNG DATA │
|
||||||
|
│ - Gabungkan data dari berbagai tahun │
|
||||||
|
│ - Tambahkan kolom 'tahun' │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ STEP 3: DATA CLEANING │
|
||||||
|
│ - Hapus duplikat │
|
||||||
|
│ - Hapus data kosong │
|
||||||
|
│ - Handle missing values │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ STEP 4: TEXT PREPROCESSING │
|
||||||
|
│ - Case folding (lowercase) │
|
||||||
|
│ - Tokenizing │
|
||||||
|
│ - Stopword removal │
|
||||||
|
│ - Stemming │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ OUTPUT: Data siap untuk TF-IDF & Clustering │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
CONTOH PENGGUNAAN:
|
||||||
|
==================
|
||||||
|
>>> pipeline = DataPipeline()
|
||||||
|
>>> pipeline.tambah_data(df_2019, "2019")
|
||||||
|
>>> pipeline.tambah_data(df_2024, "2024")
|
||||||
|
>>> result = pipeline.jalankan()
|
||||||
|
>>> print(result.head())
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Inisialisasi pipeline dengan komponen-komponen yang dibutuhkan."""
|
||||||
|
# Simpan data mentah yang belum diproses
|
||||||
|
self.data_list: List[Tuple[pd.DataFrame, str]] = []
|
||||||
|
|
||||||
|
# Komponen standarisasi
|
||||||
|
self.standarisasi = KolomStandarisasi()
|
||||||
|
|
||||||
|
# Status pipeline
|
||||||
|
self._sudah_standarisasi = False
|
||||||
|
self._sudah_gabung = False
|
||||||
|
self._sudah_bersih = False
|
||||||
|
self._sudah_preprocess = False
|
||||||
|
|
||||||
|
# Data hasil processing (disimpan untuk debugging)
|
||||||
|
self._df_standar = None
|
||||||
|
self._df_gabungan = None
|
||||||
|
self._df_bersih = None
|
||||||
|
self._df_final = None
|
||||||
|
|
||||||
|
print("🚀 DataPipeline initialized")
|
||||||
|
|
||||||
|
# ========================================================
|
||||||
|
# METODE: tambah_data
|
||||||
|
# ========================================================
|
||||||
|
def tambah_data(self, df: pd.DataFrame, tahun: str,
|
||||||
|
nama_file: str = None) -> 'DataPipeline':
|
||||||
|
"""
|
||||||
|
Tambahkan data dari satu tahun.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
df: DataFrame dari file Excel
|
||||||
|
tahun: Tahun data (2019-2025)
|
||||||
|
nama_file: Nama file (untuk auto-detect)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self (untuk method chaining)
|
||||||
|
|
||||||
|
Contoh:
|
||||||
|
=======
|
||||||
|
>>> pipeline = DataPipeline()
|
||||||
|
>>> pipeline.tambah_data(df_2019, "2019")
|
||||||
|
>>> pipeline.tambah_data(df_2024, "2024")
|
||||||
|
"""
|
||||||
|
# Validasi tahun
|
||||||
|
tahun_tersedia = self.standarisasi.get_tahun()
|
||||||
|
if tahun not in tahun_tersedia:
|
||||||
|
print(f"⚠️ Warning: Tahun {tahun} belum ada di mapping")
|
||||||
|
print(f" Mapping akan ditambahkan otomatis")
|
||||||
|
|
||||||
|
self.data_list.append((df, tahun))
|
||||||
|
print(f" ✅ Data {tahun} ditambahkan ({len(df)} records)")
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
# ========================================================
|
||||||
|
# METODE: jalankan
|
||||||
|
# ========================================================
|
||||||
|
def jalankan(self, bersihkan: bool = True,
|
||||||
|
text_preprocess: bool = True) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Jalankan seluruh pipeline.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bersihkan: Apakah akan melakukan data cleaning
|
||||||
|
text_preprocess: Apakah akan melakukan text preprocessing
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DataFrame yang sudah diproses
|
||||||
|
|
||||||
|
ALUR:
|
||||||
|
=====
|
||||||
|
1. Standarisasi setiap data
|
||||||
|
2. Gabungkan semua data
|
||||||
|
3. Bersihkan data
|
||||||
|
4. Text preprocessing
|
||||||
|
"""
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("🚀 MEMULAI PIPELINE")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# ================================================
|
||||||
|
# STEP 1: Standarisasi
|
||||||
|
# ================================================
|
||||||
|
print("\n📋 STEP 1: Standarisasi Kolom")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
df_standar_list = []
|
||||||
|
|
||||||
|
for df, tahun in self.data_list:
|
||||||
|
print(f"\n Memproses tahun {tahun}...")
|
||||||
|
try:
|
||||||
|
df_std = self.standarisasi.standarisasi(df, tahun=tahun)
|
||||||
|
df_standar_list.append(df_std)
|
||||||
|
print(f" ✅ {tahun}: {len(df_std)} records")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ {tahun}: Error - {e}")
|
||||||
|
|
||||||
|
if not df_standar_list:
|
||||||
|
raise ValueError("Tidak ada data yang berhasil diproses")
|
||||||
|
|
||||||
|
self._df_standar = df_standar_list
|
||||||
|
self._sudah_standarisasi = True
|
||||||
|
|
||||||
|
# ================================================
|
||||||
|
# STEP 2: Gabung Data
|
||||||
|
# ================================================
|
||||||
|
print("\n📋 STEP 2: Gabung Data")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
tahun_list = [tahun for _, tahun in self.data_list]
|
||||||
|
self._df_gabungan = gabung_data(df_standar_list, tahun_list)
|
||||||
|
self._sudah_gabung = True
|
||||||
|
|
||||||
|
print(f" Total records setelah digabung: {len(self._df_gabungan)}")
|
||||||
|
|
||||||
|
# ================================================
|
||||||
|
# STEP 3: Cleaning
|
||||||
|
# ================================================
|
||||||
|
if bersihkan:
|
||||||
|
print("\n📋 STEP 3: Data Cleaning")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
self._df_bersih = bersihkan_data(self._df_gabungan)
|
||||||
|
self._sudah_bersih = True
|
||||||
|
else:
|
||||||
|
self._df_bersih = self._df_gabungan
|
||||||
|
|
||||||
|
# ================================================
|
||||||
|
# STEP 4: Text Preprocessing
|
||||||
|
# ================================================
|
||||||
|
if text_preprocess:
|
||||||
|
print("\n📋 STEP 4: Text Preprocessing")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
self._df_final = self._proses_teks(self._df_bersih)
|
||||||
|
self._sudah_preprocess = True
|
||||||
|
else:
|
||||||
|
self._df_final = self._df_bersih
|
||||||
|
|
||||||
|
# ================================================
|
||||||
|
# SELESAI
|
||||||
|
# ================================================
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("✅ PIPELINE SELESAI")
|
||||||
|
print("=" * 60)
|
||||||
|
print(f" Total records final: {len(self._df_final)}")
|
||||||
|
print(f" Kolom: {self._df_final.columns.tolist()}")
|
||||||
|
|
||||||
|
return self._df_final
|
||||||
|
|
||||||
|
# ========================================================
|
||||||
|
# METODE: _proses_teks
|
||||||
|
# ========================================================
|
||||||
|
def _proses_teks(self, df: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Proses kolom teks untuk ML.
|
||||||
|
|
||||||
|
MELAKUKAN:
|
||||||
|
==========
|
||||||
|
1. Gabungkan kolom perusahaan + jabatan menjadi satu teks
|
||||||
|
2. Apply text preprocessing (lowercase, tokenize, dll)
|
||||||
|
3. Simpan hasil di kolom 'teks_bersih'
|
||||||
|
"""
|
||||||
|
print(" Memproses teks...")
|
||||||
|
|
||||||
|
df = df.copy()
|
||||||
|
|
||||||
|
# ================================================
|
||||||
|
# LANGKAH 1: Gabungkan teks
|
||||||
|
# ================================================
|
||||||
|
# Gabungkan "perusahaan" dan "jabatan" menjadi satu teks
|
||||||
|
# Ini akan menjadi input untuk TF-IDF
|
||||||
|
|
||||||
|
# Isi nilai kosong dengan string kosong
|
||||||
|
df['perusahaan'] = df['perusahaan'].fillna('')
|
||||||
|
df['jabatan'] = df['jabatan'].fillna('')
|
||||||
|
|
||||||
|
# Gabungkan dengan spasi
|
||||||
|
df['teks_gabungan'] = df['perusahaan'] + ' ' + df['jabatan']
|
||||||
|
|
||||||
|
# ================================================
|
||||||
|
# LANGKAH 2: Text Preprocessing
|
||||||
|
# ================================================
|
||||||
|
# Proses setiap teks dengan fungsi dari text_preprocessing.py
|
||||||
|
|
||||||
|
def proses(teks):
|
||||||
|
if not teks or teks.strip() == '':
|
||||||
|
return ''
|
||||||
|
return preprocess_text(teks)
|
||||||
|
|
||||||
|
df['teks_bersih'] = df['teks_gabungan'].apply(proses)
|
||||||
|
|
||||||
|
# ================================================
|
||||||
|
# LANGKAH 3: Hapus kolom bantu
|
||||||
|
# ================================================
|
||||||
|
df = df.drop(columns=['teks_gabungan'])
|
||||||
|
|
||||||
|
print(f" ✅ Teks berhasil diproses")
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
# ========================================================
|
||||||
|
# METODE: get_statistik
|
||||||
|
# ========================================================
|
||||||
|
def get_statistik(self) -> Dict:
|
||||||
|
"""
|
||||||
|
Dapatkan statistik dari pipeline.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary dengan statistik
|
||||||
|
"""
|
||||||
|
stats = {
|
||||||
|
"jumlah_tahun": len(self.data_list),
|
||||||
|
"tahun": [tahun for _, tahun in self.data_list],
|
||||||
|
"total_records": 0,
|
||||||
|
"records_per_tahun": {},
|
||||||
|
"kolom_standar": []
|
||||||
|
}
|
||||||
|
|
||||||
|
if self._df_gabungan is not None:
|
||||||
|
stats["total_records"] = len(self._df_gabungan)
|
||||||
|
|
||||||
|
# Hitung per tahun
|
||||||
|
if 'tahun' in self._df_gabungan.columns:
|
||||||
|
stats["records_per_tahun"] = (
|
||||||
|
self._df_gabungan['tahun']
|
||||||
|
.value_counts()
|
||||||
|
.to_dict()
|
||||||
|
)
|
||||||
|
|
||||||
|
if self._df_final is not None:
|
||||||
|
stats["kolom_standar"] = self._df_final.columns.tolist()
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
# ========================================================
|
||||||
|
# METODE: reset
|
||||||
|
# ========================================================
|
||||||
|
def reset(self) -> 'DataPipeline':
|
||||||
|
"""
|
||||||
|
Reset pipeline ke kondisi awal.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self (untuk method chaining)
|
||||||
|
"""
|
||||||
|
self.data_list = []
|
||||||
|
self._sudah_standarisasi = False
|
||||||
|
self._sudah_gabung = False
|
||||||
|
self._sudah_bersih = False
|
||||||
|
self._sudah_preprocess = False
|
||||||
|
self._df_standar = None
|
||||||
|
self._df_gabungan = None
|
||||||
|
self._df_bersih = None
|
||||||
|
self._df_final = None
|
||||||
|
|
||||||
|
print("🔄 Pipeline reset")
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# FUNGSI: proses_file_excel
|
||||||
|
# ============================================================
|
||||||
|
def proses_file_excel(file_paths: List[str],
|
||||||
|
tahun_list: List[str]) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Proses multiple file Excel sekaligus.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_paths: List path ke file Excel
|
||||||
|
tahun_list: List tahun untuk setiap file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DataFrame yang sudah diproses
|
||||||
|
|
||||||
|
Contoh:
|
||||||
|
=======
|
||||||
|
>>> files = ["data_2019.xlsx", "data_2024.xlsx"]
|
||||||
|
>>> tahun = ["2019", "2024"]
|
||||||
|
>>> result = proses_file_excel(files, tahun)
|
||||||
|
"""
|
||||||
|
# Validasi
|
||||||
|
if len(file_paths) != len(tahun_list):
|
||||||
|
raise ValueError(
|
||||||
|
f"Jumlah file ({len(file_paths)}) "
|
||||||
|
f"tidak sama dengan jumlah tahun ({len(tahun_list)})"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("MEMPROSES FILE EXCEL")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Inisialisasi pipeline
|
||||||
|
pipeline = DataPipeline()
|
||||||
|
|
||||||
|
# Load dan tambah setiap file
|
||||||
|
for file_path, tahun in zip(file_paths, tahun_list):
|
||||||
|
print(f"\n📂 Membaca: {file_path}")
|
||||||
|
df = pd.read_excel(file_path)
|
||||||
|
pipeline.tambah_data(df, tahun)
|
||||||
|
|
||||||
|
# Jalankan pipeline
|
||||||
|
result = pipeline.jalankan()
|
||||||
|
|
||||||
|
# Tampilkan statistik
|
||||||
|
stats = pipeline.get_statistik()
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("STATISTIK")
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"Tahun yang diproses: {stats['tahun']}")
|
||||||
|
print(f"Total records: {stats['total_records']}")
|
||||||
|
print(f"Records per tahun: {stats['records_per_tahun']}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# CONTOH PENGGUNAAN LENGKAP
|
||||||
|
# ============================================================
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("=" * 60)
|
||||||
|
print("CONTOH PENGGUNAAN DATA PIPELINE")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# ================================================
|
||||||
|
# Buat data dummy untuk setiap tahun
|
||||||
|
# ================================================
|
||||||
|
|
||||||
|
# Data 2019
|
||||||
|
df_2019 = pd.DataFrame({
|
||||||
|
# Mapping 2019 - GANTI dengan nama kolom sebenarnya
|
||||||
|
"Nama Lengkap": ["Andi Wijaya", "Budi Cahya"],
|
||||||
|
"Perusahaan": ["Google", "Microsoft"],
|
||||||
|
"Jabatan": ["Engineer", "Manager"],
|
||||||
|
"NIM": ["E41190001", "E41190002"]
|
||||||
|
})
|
||||||
|
|
||||||
|
# Data 2024 (sudah diketahui)
|
||||||
|
df_2024 = pd.DataFrame({
|
||||||
|
"4. Nama Lengkap / nmmhsmsmh (Gunakan Huruf Besar)": [
|
||||||
|
"Citra Dewi", "Dewi Lestari"
|
||||||
|
],
|
||||||
|
"9. Apa nama perusahaan/kantor tempat Anda bekerja? f5b": [
|
||||||
|
"Tokopedia", "Gojek"
|
||||||
|
],
|
||||||
|
"10. Bila Anda berwiraswasta, apa posisi/jabatan Anda saat ini ? f5c": [
|
||||||
|
"Data Analyst", "Product Manager"
|
||||||
|
],
|
||||||
|
"3. NIM / nimhsmsmh (Gunakan Huruf Besar) ": [
|
||||||
|
"E41240001", "E41240002"
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
# ================================================
|
||||||
|
# Jalankan Pipeline
|
||||||
|
# ================================================
|
||||||
|
print("\n📊 Data yang akan diproses:")
|
||||||
|
print(f" - 2019: {len(df_2019)} records")
|
||||||
|
print(f" - 2024: {len(df_2024)} records")
|
||||||
|
|
||||||
|
# Inisialisasi pipeline
|
||||||
|
pipeline = DataPipeline()
|
||||||
|
|
||||||
|
# Tambah data
|
||||||
|
pipeline.tambah_data(df_2019, "2019")
|
||||||
|
pipeline.tambah_data(df_2024, "2024")
|
||||||
|
|
||||||
|
# Jalankan
|
||||||
|
result = pipeline.jalankan()
|
||||||
|
|
||||||
|
# Tampilkan hasil
|
||||||
|
print("\n📋 HASIL AKHIR:")
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
# Tampilkan statistik
|
||||||
|
stats = pipeline.get_statistik()
|
||||||
|
print("\n📈 STATISTIK:")
|
||||||
|
for key, value in stats.items():
|
||||||
|
print(f" {key}: {value}")
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
{
|
||||||
|
"_description": "Mapping kolom CSV tracer study ke field database",
|
||||||
|
"_version": "1.0",
|
||||||
|
|
||||||
|
"identitas": {
|
||||||
|
"nim": ["NIM/Nomor Mahasiswa", "NIM", "nimhsmsmh", "3. NIM / nimhsmsmh"],
|
||||||
|
"nama": ["Nama", "nmmhsmsmh", "4. Nama Lengkap / nmmhsmsmh"],
|
||||||
|
"kode_prodi": ["Kode Prodi", "kdpstmsmh", "2. Kode Program Studi / kdpstmsmh"],
|
||||||
|
"kode_pt": ["Kode PT", "kdptimsmh", "1. Kode Perguruan Tinggi / kdptimsmh"],
|
||||||
|
"email": ["Email", "emailmsmh", "6. Alamat Email / emailmsmh"],
|
||||||
|
"tahun_lulus": ["Tahun Lulus", "tahun_lulus", "7. Tahun Lulus / tahun_lulus"],
|
||||||
|
"nik": ["NIK"],
|
||||||
|
"npwp": ["NPWP"],
|
||||||
|
"telepon": ["Telepon", "No. Telepon"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"pekerjaan": {
|
||||||
|
"status_saat_ini": ["F8", "f8", "1. Jelaskan status Anda saat ini? f8"],
|
||||||
|
"bulan_dapat_kerja": ["F502", "f502", "2. Dalam berapa bulan Anda mendapatkan pekerjaan pertama ? (f502)"],
|
||||||
|
"dapet_kerja_6_bulan": ["F504", "f504", "3. Apakah Anda telah mendapatkan pekerjaan <= 6 bulan (f504)"],
|
||||||
|
"rata_pendapatan": ["F505", "f505", "4. Berapa rata-rata pendapatan Anda per bulan ? f505"],
|
||||||
|
"bulan_dapat_kerja_setelah": ["F506", "f506", "5. Berapa bulan Anda MENDAPATKAN PEKERJAAN SETELAH LULUS? f506"],
|
||||||
|
"provinsi_kerja": ["F5a1", "f5a1", "6. Lokasi provinsi tempat Anda bekerja? f5a1"],
|
||||||
|
"kab_kota_kerja": ["F5a2", "f5a2", "7. Lokasi kabupaten/kota tempat Anda bekerja? f5a2"],
|
||||||
|
"jenis_perusahaan_full": ["F1101", "f1101", "8. Jenis perusahaan/instansi tempat anda bekerja sekarang? f1101"],
|
||||||
|
"jumlah_perusahaan": ["F1102", "f1102"],
|
||||||
|
"nama_perusahaan": ["F5b", "f5b", "9. Nama perusahaan/kantor tempat Anda bekerja? f5b"],
|
||||||
|
"jabatan": ["F5c", "f5c", "10. Posisi/jabatan Anda saat ini ? f5c"],
|
||||||
|
"tingkatan_tempat_kerja": ["F5d", "f5d", "11. Tingkatan tempat kerja Anda? f5d"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"studi_lanjut": {
|
||||||
|
"sumber_biaya_studi": ["F18a", "f18a", "12. Sumber biaya Studi Lanjut Anda? f18a"],
|
||||||
|
"pt_studi_lanjut": ["F18b", "f18b", "13. Nama Perguruan Tinggi Studi Lanjut Anda? f18b"],
|
||||||
|
"prodi_studi_lanjut": ["F18c", "f18c", "14. Nama Program Studi Studi Lanjut Anda? f18c"],
|
||||||
|
"tanggal_masuk_studi": ["F18d", "f18d", "15. Tanggal Masuk Awal Studi Lanjut Anda? f18d"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"kesesuaian": {
|
||||||
|
"sumber_biaya_kuliah": ["F1201", "f1201", "16. Sebutkan sumberdana dalam pembiayaan kuliah? f1201"],
|
||||||
|
"keeratan_bidang_studi": ["F14", "f14", "17. Seberapa erat hubungan bidang studi dengan pekerjaan Anda? f14"],
|
||||||
|
"tingkat_pendidikan_tepat": ["F15", "f15", "18. Tingkat pendidikan apa yang paling tepat/sesuai untuk pekerjaan anda saat ini? f15"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"kompetensi_lulus": {
|
||||||
|
"etika_lulus_full": ["F1761", "f1761", "19. Kompetensi ETIKA SAAT LULUS? f1761"],
|
||||||
|
"keahlian_lulus_full": ["F1763", "f1763", "21. Kompetensi KEAHLIAN BERDASARKAN BIDANG ILMU SAAT LULUS? f1763"],
|
||||||
|
"bahasa_inggris_lulus_full": ["F1765", "f1765", "23. Kompetensi BAHASA INGGRIS SAAT LULUS? f1765"],
|
||||||
|
"teknologi_informasi_lulus_full": ["F1767", "f1767", "25. Kompetensi TEKNOLOGI INFORMASI SAAT LULUS? f1767"],
|
||||||
|
"komunikasi_lulus_full": ["F1769", "f1769", "27. Kompetensi KOMUNIKASI SAAT LULUS? f1769"],
|
||||||
|
"kerja_sama_lulus_full": ["F1771", "f1771", "29. Kompetensi KERJA SAMA TIM SAAT LULUS? f1771"],
|
||||||
|
"pengembangan_diri_lulus_full": ["F1773", "f1773", "31. Kompetensi PENGEMBANGAN DIRI SAAT LULUS? f1773"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"kompetensi_saat_ini": {
|
||||||
|
"etika_saat_ini": ["F1762", "f1762", "20. Kompetensi ETIKA SAAT INI? f1762"],
|
||||||
|
"keahlian_saat_ini": ["F1764", "f1764", "22. Kompetensi KEAHLIAN BERDASARKAN BIDANG ILMU SAAT INI? f1764"],
|
||||||
|
"bahasa_inggris_saat_ini": ["F1766", "f1766", "24. Kompetensi BAHASA INGGRIS SAAT INI? f1766"],
|
||||||
|
"teknologi_informasi_saat_ini": ["F1768", "f1768", "26. Kompetensi TEKNOLOGI INFORMASI SAAT INI? f1768"],
|
||||||
|
"komunikasi_saat_ini": ["F1770", "f1770", "28. Kompetensi KOMUNIKASI SAAT INI? f1770"],
|
||||||
|
"kerja_sama_saat_ini": ["F1772", "f1772", "30. Kompetensi KERJA SAMA TIM SAAT INI? f1772"],
|
||||||
|
"pengembangan_diri_saat_ini": ["F1774", "f1774", "32. Kompetensi PENGEMBANGAN DIRI SAAT INI? f1774"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"metode_pembelajaran": {
|
||||||
|
"metode_perkuliahan": ["F21", "f21", "33. Perkuliahan f21"],
|
||||||
|
"metode_demonstrasi": ["F22", "f22", "34. Demonstrasi f22"],
|
||||||
|
"metode_proyek_riset": ["F23", "f23", "35. Partisipasi dalam proyek riset f23"],
|
||||||
|
"metode_magang": ["F24", "f24", "36. Magang f24"],
|
||||||
|
"metode_praktikum": ["F25", "f25", "37. Praktikum f25"],
|
||||||
|
"metode_kerja_lapangan": ["F26", "f26", "38. Kerja Lapangan f26"],
|
||||||
|
"metode_diskusi": ["F27", "f27", "39. Diskusi f27"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"waktu_cari_kerja": {
|
||||||
|
"mulai_cari_kerja": ["F301", "f301", "40. Kapan anda mulai mencari pekerjaan? f301"],
|
||||||
|
"bulan_cari_sebelum_lulus": ["F302", "f302", "41. Saya mencari pekerjaan, bulan SEBELUM lulus f302"],
|
||||||
|
"bulan_cari_setelah_wisuda": ["F303", "f303", "42. Saya mencari pekerjaan, bulan SETELAH wisuda f303"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"cara_cari_kerja": {
|
||||||
|
"cari_iklan": ["F401", "f401", "43. Melalui iklan di koran/majalah? f401"],
|
||||||
|
"cari_melamar_tanpa_lowongan": ["F402", "f402", "44. Melamar ke perusahaan tanpa mengetahui lowongan? f402"],
|
||||||
|
"cari_bursa_pameran": ["F403", "f403", "45. Pergi ke bursa/pameran kerja? f403"],
|
||||||
|
"cari_internet": ["F404", "f404", "46. Mencari lewat internet/iklan online? f404"],
|
||||||
|
"cari_dihubungi_perusahaan": ["F405", "f405", "47. Dihubungi oleh perusahaan? f405"],
|
||||||
|
"cari_kemenakertrans": ["F406", "f406", "48. Menghubungi Kemenakertrans? f406"],
|
||||||
|
"cari_agen_tenaga_kerja": ["F407", "f407", "49. Menghubungi agen tenaga kerja komersial? f407"],
|
||||||
|
"cari_pusat_karir": ["F408", "f408", "50. Memperoleh informasi dari pusat/kantor pengembangan karir? f408"],
|
||||||
|
"cari_kantor_mahasiswa": ["F409", "f409", "51. Menghubungi kantor kemahasiswaan? f409"],
|
||||||
|
"cari_jejaring": ["F410", "f410", "52. Membangun jejaring (network) sejak masih kuliah? f410"],
|
||||||
|
"cari_relasi": ["F411", "f411", "53. Melalui relasi (dosen, orang tua, teman)? f411"],
|
||||||
|
"cari_bangun_usaha": ["F412", "f412", "54. Membangun bisnis sendiri? f412"],
|
||||||
|
"cari_magang": ["F413", "f413", "55. Melalui penempatan kerja atau magang? f413"],
|
||||||
|
"cari_tempat_kuliah": ["F414", "f414", "56. Bekerja di tempat yang sama dengan tempat kerja semasa kuliah? f414"],
|
||||||
|
"cari_lainnya": ["F415", "f415", "57. Lainnya? f415"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"lamaran_keaktifan": {
|
||||||
|
"jumlah_perusahaan_dilamar": ["F6", "f6", "58. Berapa perusahaan yang sudah anda lamar?"],
|
||||||
|
"jumlah_perusahaan_merespons": ["F7", "f7", "59. Berapa banyak perusahaan yang merespons lamaran Anda?"],
|
||||||
|
"jumlah_perusahaan_wawancara": ["F7a", "f7a", "60. Berapa banyak perusahaan yang mengundang anda untuk wawancara?"],
|
||||||
|
"aktif_cari_kerja_4_minggu": ["F1001", "f1001", "61. Apakah Anda aktif mencari pekerjaan dalam 4 minggu terakhir?"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"kesesuaian_pekerjaan": {
|
||||||
|
"kerja_tidak_sesuai_1": ["F1601", "f1601", "62. Apakah pekerjaan anda saat ini tidak sesuai dengan pendidikan anda?"],
|
||||||
|
"kerja_tidak_sesuai_2": ["F1602", "f1602", "63. Apakah pekerjaan anda saat ini tidak sesuai dengan pendidikan anda ?"],
|
||||||
|
"alasan_tidak_sesuai_1": ["F1603", "f1603", "64. Jika tidak sesuai, mengapa anda mengambilnya?"],
|
||||||
|
"alasan_tidak_sesuai_2": ["F1604", "f1604", "65. Jika tidak sesuai, mengapa anda mengambilnya?"],
|
||||||
|
"alasan_tidak_sesuai_3": ["F1605", "f1605", "66. Jika tidak sesuai, mengapa anda mengambilnya?"],
|
||||||
|
"alasan_tidak_sesuai_4": ["F1606", "f1606", "67. Jika tidak sesuai, mengapa anda mengambilnya?"],
|
||||||
|
"alasan_tidak_sesuai_5": ["F1607", "f1607", "68. Jika tidak sesuai, mengapa anda mengambilnya?"],
|
||||||
|
"alasan_tidak_sesuai_6": ["F1608", "f1608", "69. Jika tidak sesuai, mengapa anda mengambilnya?"],
|
||||||
|
"alasan_tidak_sesuai_7": ["F1609", "f1609", "70. Jika tidak sesuai, mengapa anda mengambilnya?"],
|
||||||
|
"alasan_tidak_sesuai_8": ["F1610", "f1610", "71. Jika tidak sesuai, mengapa anda mengambilnya?"],
|
||||||
|
"alasan_tidak_sesuai_9": ["F1611", "f1611", "72. Jika tidak sesuai, mengapa anda mengambilnya?"],
|
||||||
|
"alasan_tidak_sesuai_10": ["F1612", "f1612", "73. Jika tidak sesuai, mengapa anda mengambilnya?"],
|
||||||
|
"alasan_tidak_sesuai_11": ["F1613", "f1613", "74. Jika tidak sesuai, mengapa anda mengambilnya?"],
|
||||||
|
"alasan_tidak_sesuai_lainnya": ["F1614", "f1614", "75. Jika tidak sesuai, mengapa anda mengambilnya? (Lainnya)"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,213 @@
|
||||||
|
"""
|
||||||
|
=============================================================
|
||||||
|
INTERPRETASI CLUSTER & RELEVANSI PROFIL LULUSAN (C0 - C3)
|
||||||
|
=============================================================
|
||||||
|
PRD Reference: Section 7, 18, & 24
|
||||||
|
|
||||||
|
Tujuan:
|
||||||
|
- Normalisasi teks dan pemecahan kata majemuk (Smart CamelCase / Suffix Split)
|
||||||
|
- Pencocokan teks pekerjaan dengan kamus profil prodi (Rule-Based Matching)
|
||||||
|
- Penentuan kategori relevansi karir C0, C1, C2, C3 secara deterministik
|
||||||
|
=============================================================
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import pandas as pd
|
||||||
|
from typing import Dict, List, Tuple, Optional, Any
|
||||||
|
from Model.config.default_keywords import (
|
||||||
|
PRODI_CODES,
|
||||||
|
DEFAULT_KEYWORD_DICTIONARY,
|
||||||
|
DEFAULT_BIDANG_KERJA_UMUM,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Export default dictionary for backwards compatibility
|
||||||
|
KEYWORD_DICTIONARY = DEFAULT_KEYWORD_DICTIONARY
|
||||||
|
BIDANG_KERJA_UMUM = DEFAULT_BIDANG_KERJA_UMUM
|
||||||
|
NON_PROFILE_KEYWORDS = DEFAULT_BIDANG_KERJA_UMUM
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_text(text: Any) -> str:
|
||||||
|
"""
|
||||||
|
Normalisasi teks untuk pencocokan kata kunci:
|
||||||
|
1. Pemecahan CamelCase (misal: 'IndraTech' -> 'Indra Tech')
|
||||||
|
2. Pemecahan kata majemuk berakhiran teknologi/dev/soft/craft/coding
|
||||||
|
3. Case folding & penghapusan tanda baca khusus
|
||||||
|
"""
|
||||||
|
if pd.isna(text) or text is None or text == '':
|
||||||
|
return ''
|
||||||
|
|
||||||
|
text = str(text)
|
||||||
|
# 1. Pisahkan camelCase
|
||||||
|
text = re.sub(r'([a-z])([A-Z])', r'\1 \2', text)
|
||||||
|
text = text.lower()
|
||||||
|
|
||||||
|
# 2. Pisahkan kata majemuk yang menempel dengan akhiran tech/dev/soft/coding/craft
|
||||||
|
text = re.sub(
|
||||||
|
r'(\b[a-zA-Z]{3,})(tech|technology|teknologi|dev|soft|software|coding|koding|craft)\b',
|
||||||
|
r'\1 \2',
|
||||||
|
text
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. Ganti tanda baca pemisah menjadi spasi
|
||||||
|
text = re.sub(r'[/\\_.\-,()\[\]{}:;+*&^%$#@!~?<>|\"\']', ' ', text)
|
||||||
|
# 4. Hapus spasi ganda
|
||||||
|
text = re.sub(r'\s+', ' ', text)
|
||||||
|
return text.strip()
|
||||||
|
|
||||||
|
|
||||||
|
# Cache pola regex terkompilasi agar proses matching berjalan instan (<0.05 detik untuk ribuan alumni)
|
||||||
|
_COMPILED_KEYWORDS_CACHE: Dict[str, List[Tuple[str, Any]]] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_compiled_keywords_for_prodi(prodi_name: str, dict_source: Dict[str, List[str]]) -> List[Tuple[str, Any]]:
|
||||||
|
keywords = dict_source.get(prodi_name, [])
|
||||||
|
cache_key = f"{prodi_name}_{len(keywords)}"
|
||||||
|
|
||||||
|
if cache_key not in _COMPILED_KEYWORDS_CACHE:
|
||||||
|
compiled_list = []
|
||||||
|
for kw in keywords:
|
||||||
|
kw_norm = normalize_text(kw)
|
||||||
|
if kw_norm:
|
||||||
|
compiled_list.append((kw, re.compile(r'\b' + re.escape(kw_norm) + r'\b')))
|
||||||
|
_COMPILED_KEYWORDS_CACHE[cache_key] = compiled_list
|
||||||
|
|
||||||
|
return _COMPILED_KEYWORDS_CACHE[cache_key]
|
||||||
|
|
||||||
|
|
||||||
|
def match_keywords(
|
||||||
|
teks_pekerjaan: str,
|
||||||
|
prodi_code: str,
|
||||||
|
custom_keyword_dict: Optional[Dict[str, List[str]]] = None
|
||||||
|
) -> Tuple[bool, List[str], bool]:
|
||||||
|
"""
|
||||||
|
Cek apakah teks pekerjaan match dengan kamus prodi menggunakan Word-Boundary Matching (\b...\b).
|
||||||
|
Dapat menerima kamus dinamis dari database (custom_keyword_dict).
|
||||||
|
"""
|
||||||
|
teks_normalized = normalize_text(teks_pekerjaan)
|
||||||
|
if not teks_normalized:
|
||||||
|
return False, [], False
|
||||||
|
|
||||||
|
# Resolusi prodi_code ke singkatan resmi (TIF, MIF, TKK)
|
||||||
|
prodi_name = PRODI_CODES.get(str(prodi_code).strip(), str(prodi_code).strip())
|
||||||
|
|
||||||
|
# Gunakan kamus dinamis jika disediakan, fallback ke DEFAULT
|
||||||
|
dict_source = custom_keyword_dict if custom_keyword_dict is not None else DEFAULT_KEYWORD_DICTIONARY
|
||||||
|
|
||||||
|
if prodi_name not in dict_source:
|
||||||
|
return False, [], False
|
||||||
|
|
||||||
|
compiled_kws = _get_compiled_keywords_for_prodi(prodi_name, dict_source)
|
||||||
|
matched_keywords = []
|
||||||
|
|
||||||
|
for orig_kw, pattern in compiled_kws:
|
||||||
|
if pattern.search(teks_normalized):
|
||||||
|
matched_keywords.append(orig_kw)
|
||||||
|
|
||||||
|
return len(matched_keywords) > 0, matched_keywords, False
|
||||||
|
|
||||||
|
|
||||||
|
_BIDANG_COMPILED_CACHE: List[Any] = []
|
||||||
|
|
||||||
|
def _get_compiled_bidang(bidang_list: List[str]) -> List[Any]:
|
||||||
|
global _BIDANG_COMPILED_CACHE
|
||||||
|
if not _BIDANG_COMPILED_CACHE or len(_BIDANG_COMPILED_CACHE) != len(bidang_list):
|
||||||
|
_BIDANG_COMPILED_CACHE = [re.compile(r'\b' + re.escape(kw) + r'\b') for kw in bidang_list if kw]
|
||||||
|
return _BIDANG_COMPILED_CACHE
|
||||||
|
|
||||||
|
|
||||||
|
def is_bidang_diketahui(
|
||||||
|
teks_pekerjaan: str,
|
||||||
|
custom_bidang_list: Optional[List[str]] = None
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Cek apakah bidang pekerjaan alumni dapat diidentifikasi pada sektor formal non-IT (C1 vs C2).
|
||||||
|
"""
|
||||||
|
teks = normalize_text(teks_pekerjaan)
|
||||||
|
if len(teks) < 3:
|
||||||
|
return False
|
||||||
|
|
||||||
|
bidang_list = custom_bidang_list if custom_bidang_list is not None else DEFAULT_BIDANG_KERJA_UMUM
|
||||||
|
compiled_bidang = _get_compiled_bidang(bidang_list)
|
||||||
|
|
||||||
|
for pattern in compiled_bidang:
|
||||||
|
if pattern.search(teks):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def interpretasi_categori(
|
||||||
|
row: Dict[str, Any],
|
||||||
|
c3_df: Optional[Any] = None,
|
||||||
|
custom_keyword_dict: Optional[Dict[str, List[str]]] = None
|
||||||
|
) -> Tuple[str, bool, List[str]]:
|
||||||
|
"""
|
||||||
|
Interpretasi alumni ke kategori C0 - C3 (PRD Section 18):
|
||||||
|
- Priority 1 (C3): Belum/tidak bekerja ATAU data pekerjaan kosong
|
||||||
|
- Priority 2 (C0): Bekerja + Match kamus kurikulum prodi asal (Sesuai Profil)
|
||||||
|
- Priority 3 (C1): Bekerja + Sektor formal teridentifikasi tapi non-IT (Bidang Diketahui)
|
||||||
|
- Priority 4 (C2): Bekerja + Deskripsi generik/ambigu (Kurang Teridentifikasi)
|
||||||
|
"""
|
||||||
|
nim = str(row.get('nim', '')).strip()
|
||||||
|
|
||||||
|
# Priority 1A: Cek apakah terdaftar di list C3
|
||||||
|
if c3_df is not None and hasattr(c3_df, 'empty') and not c3_df.empty and 'nim' in c3_df.columns:
|
||||||
|
if nim in c3_df['nim'].astype(str).values:
|
||||||
|
return 'C3', False, []
|
||||||
|
|
||||||
|
# Priority 1B: Cek status tidak/belum bekerja
|
||||||
|
status = str(row.get('status_pekerjaan', row.get('status_kerja', ''))).lower().strip()
|
||||||
|
if status in ['belum_kerja', 'mencari_kerja', 'belum_bekerja', 'tidak_kerja', 'melanjutkan', 'belum bekerja', 'tidak bekerja', 'mencari kerja']:
|
||||||
|
return 'C3', False, []
|
||||||
|
|
||||||
|
# Priority 1C: Cek apakah seluruh data kosong total tanpa status bekerja
|
||||||
|
nama_perusahaan = str(row.get('nama_perusahaan', row.get('pekerjaan', ''))).strip()
|
||||||
|
jabatan = str(row.get('jabatan', row.get('instansi', ''))).strip()
|
||||||
|
jenis_perusahaan = str(row.get('jenis_perusahaan', '')).strip()
|
||||||
|
|
||||||
|
def is_empty_val(v: str) -> bool:
|
||||||
|
return v.lower() in ['-', '', 'none', 'null', 'kosong', 'tidak ada', 'belum kerja', 'tidak bekerja', 'n/a', '0', '0.0']
|
||||||
|
|
||||||
|
# Jika status kosong/tidak jelas dan semua field pekerjaan kosong total -> C3
|
||||||
|
if is_empty_val(status) and is_empty_val(nama_perusahaan) and is_empty_val(jabatan) and is_empty_val(jenis_perusahaan):
|
||||||
|
return 'C3', False, []
|
||||||
|
|
||||||
|
teks_pekerjaan = str(row.get('teks_pekerjaan', '')).strip()
|
||||||
|
if not teks_pekerjaan or is_empty_val(teks_pekerjaan):
|
||||||
|
teks_pekerjaan = f"{status if not is_empty_val(status) else ''} {jenis_perusahaan if not is_empty_val(jenis_perusahaan) else ''} {nama_perusahaan if not is_empty_val(nama_perusahaan) else ''} {jabatan if not is_empty_val(jabatan) else ''}".strip()
|
||||||
|
|
||||||
|
if len(normalize_text(teks_pekerjaan)) < 3 and is_empty_val(status):
|
||||||
|
return 'C3', False, []
|
||||||
|
|
||||||
|
# Ekstraksi kode prodi
|
||||||
|
kode_prodi = str(row.get('kode_prodi', '')).strip()
|
||||||
|
kode_prodi_clean = kode_prodi.replace('.0', '').strip()
|
||||||
|
match = re.search(r'\((\d+)\)', kode_prodi)
|
||||||
|
if match:
|
||||||
|
kode_prodi_clean = match.group(1)
|
||||||
|
|
||||||
|
prodi = PRODI_CODES.get(kode_prodi_clean, None) or PRODI_CODES.get(kode_prodi, None)
|
||||||
|
|
||||||
|
# Priority 2: Cek Match Kamus Profil Prodi Asal -> C0
|
||||||
|
if prodi:
|
||||||
|
is_match, matched_kws, _ = match_keywords(teks_pekerjaan, prodi, custom_keyword_dict)
|
||||||
|
else:
|
||||||
|
is_match, matched_kws, _ = False, [], False
|
||||||
|
|
||||||
|
if is_match:
|
||||||
|
return 'C0', True, matched_kws
|
||||||
|
|
||||||
|
# Priority 3: Cek apakah sektor formal non-IT diketahui -> C1
|
||||||
|
if is_bidang_diketahui(teks_pekerjaan):
|
||||||
|
return 'C1', False, []
|
||||||
|
|
||||||
|
# Cek nama institusi formal eksplisit
|
||||||
|
clean_norm = normalize_text(teks_pekerjaan)
|
||||||
|
formal_orgs = ['pt', 'cv', 'ud', 'dinas', 'badan', 'yayasan', 'bank', 'sekolah', 'rumah sakit', 'rsud', 'polres', 'kodim', 'bumn', 'bumd', 'kementerian']
|
||||||
|
if len(clean_norm) >= 5 and any(w in clean_norm.split() for w in formal_orgs):
|
||||||
|
tokens = [t for t in clean_norm.split() if t not in ['bekerja', 'perusahaan', 'swasta', 'wiraswasta', 'lainnya', 'tuliskan', 'f1102', 'f1101', 'f8', 'sendiri']]
|
||||||
|
if len(tokens) >= 2:
|
||||||
|
return 'C1', False, []
|
||||||
|
|
||||||
|
# Priority 4: Deskripsi tidak spesifik / hanya status formalitas -> C2
|
||||||
|
return 'C2', False, []
|
||||||
|
|
@ -0,0 +1,426 @@
|
||||||
|
"""
|
||||||
|
=============================================================
|
||||||
|
MERGE & UNIFIKASI DATASET TRACER STUDY (2019-2025)
|
||||||
|
=============================================================
|
||||||
|
|
||||||
|
Tujuan:
|
||||||
|
- Membaca semua file CSV dari berbagai tahun
|
||||||
|
- Menerapkan column mapping sesuai format setiap tahun
|
||||||
|
- Merge semua data menjadi unified_dataset
|
||||||
|
- Basic cleaning
|
||||||
|
|
||||||
|
Output:
|
||||||
|
- unified_dataset.csv
|
||||||
|
- summary_merge.txt
|
||||||
|
=============================================================
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Path configuration
|
||||||
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
DATA_DIR = os.path.join(BASE_DIR, '..', '..', 'Data_Tracerstudy')
|
||||||
|
OUTPUT_DIR = os.path.join(BASE_DIR, '..', 'data', 'standardized')
|
||||||
|
|
||||||
|
# Column mapping untuk setiap tahun
|
||||||
|
COLUMN_MAPPING = {
|
||||||
|
"2019": {
|
||||||
|
"kode_prodi": "Kode Prodi",
|
||||||
|
"nim": "NIM/Nomor Mahasiswa",
|
||||||
|
"nama": "Nama",
|
||||||
|
"tahun_lulus": "Tahun Lulus",
|
||||||
|
"status_pekerjaan": "F8",
|
||||||
|
"jenis_perusahaan": "F1101",
|
||||||
|
"nama_perusahaan": "F5b",
|
||||||
|
"jabatan": "F5c"
|
||||||
|
},
|
||||||
|
"2020": {
|
||||||
|
"kode_prodi": "Kode Prodi",
|
||||||
|
"nim": "NIM/Nomor Mahasiswa",
|
||||||
|
"nama": "Nama",
|
||||||
|
"tahun_lulus": "Tahun Lulus",
|
||||||
|
"status_pekerjaan": "F8",
|
||||||
|
"jenis_perusahaan": "F1101",
|
||||||
|
"nama_perusahaan": "F5b",
|
||||||
|
"jabatan": "F5c"
|
||||||
|
},
|
||||||
|
"2021": {
|
||||||
|
"kode_prodi": "Kode Prodi",
|
||||||
|
"nim": "NIM",
|
||||||
|
"nama": "Nama",
|
||||||
|
"tahun_lulus": "Tahun Lulus",
|
||||||
|
"status_pekerjaan": "F8",
|
||||||
|
"jenis_perusahaan": "F1101",
|
||||||
|
"nama_perusahaan": "F5b",
|
||||||
|
"jabatan": "F5c"
|
||||||
|
},
|
||||||
|
"2022": {
|
||||||
|
"kode_prodi": "kdpstmsmh",
|
||||||
|
"nim": "nimhsmsmh",
|
||||||
|
"nama": "nmmhsmsmh",
|
||||||
|
"tahun_lulus": "tahun_lulus",
|
||||||
|
"status_pekerjaan": "f8",
|
||||||
|
"jenis_perusahaan": "f1101",
|
||||||
|
"nama_perusahaan": "f5b",
|
||||||
|
"jabatan": "f5c"
|
||||||
|
},
|
||||||
|
"2023": {
|
||||||
|
"kode_prodi": "kdpstmsmh",
|
||||||
|
"nim": "nimhsmsmh",
|
||||||
|
"nama": "nmmhsmsmh",
|
||||||
|
"tahun_lulus": "tahun_lulus",
|
||||||
|
"status_pekerjaan": "f8",
|
||||||
|
"jenis_perusahaan": "f1101",
|
||||||
|
"nama_perusahaan": "f5b",
|
||||||
|
"jabatan": "f5c"
|
||||||
|
},
|
||||||
|
"2024": {
|
||||||
|
"kode_prodi": "2. Kode Program Studi / kdpstmsmh",
|
||||||
|
"nim": "3. NIM / nimhsmsmh (Gunakan Huruf Besar) ",
|
||||||
|
"nama": "4. Nama Lengkap / nmmhsmsmh (Gunakan Huruf Besar)",
|
||||||
|
"tahun_lulus": "7. Tahun Lulus / tahun_lulus",
|
||||||
|
"status_pekerjaan": "1. Jelaskan status Anda saat ini? f8",
|
||||||
|
"jenis_perusahaan": "8. Apa jenis perusahaan/instansi/institusi tempat anda bekerja sekarang? f1101",
|
||||||
|
"nama_perusahaan": "9. Apa nama perusahaan/kantor tempat Anda bekerja? f5b",
|
||||||
|
"jabatan": "10. Bila Anda berwiraswasta, apa posisi/jabatan Anda saat ini ? f5c"
|
||||||
|
},
|
||||||
|
"2025": {
|
||||||
|
"kode_prodi": "2. Kode Program Studi / kdpstmsmh",
|
||||||
|
"nim": "3. NIM / nimhsmsmh (Gunakan Huruf Besar) ",
|
||||||
|
"nama": "4. Nama Lengkap / nmmhsmsmh (Gunakan Huruf Besar)",
|
||||||
|
"tahun_lulus": "7. Tahun Lulus / tahun_lulus",
|
||||||
|
"status_pekerjaan": "1. Jelaskan status Anda saat ini? f8",
|
||||||
|
"jenis_perusahaan": "7. Apa jenis perusahaan/instansi/institusi tempat anda bekerja sekarang? f1101",
|
||||||
|
"nama_perusahaan": "9. Apa nama perusahaan/kantor tempat Anda bekerja? f5b",
|
||||||
|
"jabatan": "10. Bila Anda berwiraswasta, apa posisi/jabatan Anda saat ini ? f5c *(Wajib diisi) (Jika Memilih Wiraswasta / Jika f8 adalah 3)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# File yang akan dibaca
|
||||||
|
DATA_FILES = {
|
||||||
|
"2019": "Data_2019_Tracer_study.csv",
|
||||||
|
"2020": "Data_2020_Tracer_study.csv",
|
||||||
|
"2021": "Data_2021_Tracer_study.csv",
|
||||||
|
"2022": "Data_2022_Tracer_study.csv",
|
||||||
|
"2023": "Data_2023_Tracer_study.csv",
|
||||||
|
"2024": "Data_2024_Tracer_study.csv",
|
||||||
|
"2025": [
|
||||||
|
"Data_2025_Tracer_study_D3MIF.csv",
|
||||||
|
"Data_2025_Tracer_study_D3TKK.csv",
|
||||||
|
"Data_2025_Tracer_study_D4TIF.csv",
|
||||||
|
"Data_2025_Tracer_study_D4TIFNGANJUK.csv",
|
||||||
|
"Data_2025_Tracer_study_D4TIFSIDOARJO.csv"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Separator untuk setiap tahun
|
||||||
|
SEPARATORS = {
|
||||||
|
"2019": ";",
|
||||||
|
"2020": ";",
|
||||||
|
"2021": ";",
|
||||||
|
"2022": ";",
|
||||||
|
"2023": ";",
|
||||||
|
"2024": ";",
|
||||||
|
"2025": ";"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Kolom standar output
|
||||||
|
STANDARD_COLUMNS = [
|
||||||
|
"kode_prodi",
|
||||||
|
"nim",
|
||||||
|
"nama",
|
||||||
|
"tahun_lulus",
|
||||||
|
"status_pekerjaan",
|
||||||
|
"jenis_perusahaan",
|
||||||
|
"nama_perusahaan",
|
||||||
|
"jabatan",
|
||||||
|
"sumber_file"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def clean_text_value(value):
|
||||||
|
"""
|
||||||
|
Membersihkan nilai teks:
|
||||||
|
- Hapus whitespace berlebih
|
||||||
|
- Handle NaN/None
|
||||||
|
"""
|
||||||
|
if pd.isna(value):
|
||||||
|
return ""
|
||||||
|
if isinstance(value, str):
|
||||||
|
# Hapus whitespace di awal dan akhir
|
||||||
|
value = value.strip()
|
||||||
|
# Ganti multiple spaces dengan single space
|
||||||
|
value = ' '.join(value.split())
|
||||||
|
return value
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
|
def standardize_status_pekerjaan(value, tahun):
|
||||||
|
"""
|
||||||
|
Menstandardisasi nilai status pekerjaan:
|
||||||
|
- Format berbeda antar tahun
|
||||||
|
- 2019-2022: angka (1, 2, 3, 4, 5)
|
||||||
|
- 2023-2025: teks ("Bekerja (full time/part time) (1)")
|
||||||
|
"""
|
||||||
|
if pd.isna(value) or value == "":
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
value = str(value).strip().lower()
|
||||||
|
|
||||||
|
# Handle format 2024-2025
|
||||||
|
if "(" in value and ")" in value:
|
||||||
|
# Extract angka dalam parentheses
|
||||||
|
import re
|
||||||
|
match = re.search(r'\((\d+)\)', value)
|
||||||
|
if match:
|
||||||
|
num = int(match.group(1))
|
||||||
|
if num == 1:
|
||||||
|
return "bekerja"
|
||||||
|
elif num == 2:
|
||||||
|
return "belum_kerja"
|
||||||
|
elif num == 3:
|
||||||
|
return "wiraswasta"
|
||||||
|
elif num == 4:
|
||||||
|
return "melanjutkan_studi"
|
||||||
|
elif num == 5:
|
||||||
|
return "mencari_kerja"
|
||||||
|
|
||||||
|
# Handle format angka (2019-2023)
|
||||||
|
if value.isdigit():
|
||||||
|
num = int(value)
|
||||||
|
if num == 1:
|
||||||
|
return "bekerja"
|
||||||
|
elif num == 2:
|
||||||
|
return "belum_kerja"
|
||||||
|
elif num == 3:
|
||||||
|
return "wiraswasta"
|
||||||
|
elif num == 4:
|
||||||
|
return "melanjutkan_studi"
|
||||||
|
elif num == 5:
|
||||||
|
return "mencari_kerja"
|
||||||
|
|
||||||
|
# Handle teks
|
||||||
|
if "bekej" in value or "work" in value or "1" in value:
|
||||||
|
return "bekerja"
|
||||||
|
elif "belum" in value or "tidak" in value or "2" in value:
|
||||||
|
return "belum_kerja"
|
||||||
|
elif "wiraswasta" in value or "3" in value:
|
||||||
|
return "wiraswasta"
|
||||||
|
elif "studi" in value or "melanjutkan" in value or "4" in value:
|
||||||
|
return "melanjutkan_studi"
|
||||||
|
elif "mencari" in value or "cari" in value or "5" in value:
|
||||||
|
return "mencari_kerja"
|
||||||
|
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
def read_and_map_year(tahun, files, separator):
|
||||||
|
"""
|
||||||
|
Baca file CSV dan terapkan column mapping.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- tahun: string tahun (misal "2019")
|
||||||
|
- files: list nama file atau string tunggal
|
||||||
|
- separator: separator yang digunakan
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- DataFrame yang sudah di-mapping
|
||||||
|
"""
|
||||||
|
mapping = COLUMN_MAPPING[tahun]
|
||||||
|
all_dfs = []
|
||||||
|
|
||||||
|
# Pastikan files adalah list
|
||||||
|
if isinstance(files, str):
|
||||||
|
files = [files]
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
filepath = os.path.join(DATA_DIR, file)
|
||||||
|
|
||||||
|
if not os.path.exists(filepath):
|
||||||
|
print(f" >> [WARNING] File tidak ditemukan: {file}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f" >> Membaca: {file}")
|
||||||
|
|
||||||
|
# Baca CSV dengan separator yang sesuai
|
||||||
|
try:
|
||||||
|
df = pd.read_csv(filepath, sep=separator, encoding='utf-8-sig')
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
df = pd.read_csv(filepath, sep=separator, encoding='latin-1')
|
||||||
|
except Exception as e:
|
||||||
|
print(f" >> [ERROR] Gagal membaca {file}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Buat mapping untuk rename kolom
|
||||||
|
rename_mapping = {v: k for k, v in mapping.items()}
|
||||||
|
|
||||||
|
# Rename kolom
|
||||||
|
df_renamed = df.rename(columns=rename_mapping)
|
||||||
|
|
||||||
|
# Ambil hanya kolom yang ada di mapping
|
||||||
|
available_cols = [col for col in mapping.values() if col in df.columns]
|
||||||
|
standard_cols_in_df = [rename_mapping[col] for col in available_cols]
|
||||||
|
|
||||||
|
# Select kolom
|
||||||
|
df_selected = df_renamed[standard_cols_in_df].copy()
|
||||||
|
|
||||||
|
# Tambahkan sumber file
|
||||||
|
df_selected['sumber_file'] = file
|
||||||
|
|
||||||
|
all_dfs.append(df_selected)
|
||||||
|
print(f" >> {len(df_selected)} baris")
|
||||||
|
|
||||||
|
if all_dfs:
|
||||||
|
return pd.concat(all_dfs, ignore_index=True)
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
|
||||||
|
def clean_dataset(df):
|
||||||
|
"""
|
||||||
|
Cleaning dataset:
|
||||||
|
- Hapus duplikat
|
||||||
|
- Bersihkan nilai kosong
|
||||||
|
- Standardisasi status pekerjaan
|
||||||
|
"""
|
||||||
|
print("\n>> Melakukan cleaning...")
|
||||||
|
|
||||||
|
initial_count = len(df)
|
||||||
|
|
||||||
|
# Hapus duplikat berdasarkan NIM
|
||||||
|
df = df.drop_duplicates(subset=['nim'], keep='first')
|
||||||
|
print(f" >> Duplikat (berdasarkan NIM) dihapus: {initial_count - len(df)}")
|
||||||
|
|
||||||
|
# Hapus baris dengan NIM kosong
|
||||||
|
df = df[df['nim'].notna() & (df['nim'] != '') & (df['nim'] != 'nan')]
|
||||||
|
print(f" >> Baris dengan NIM kosong dihapus: {initial_count - len(df)}")
|
||||||
|
|
||||||
|
# Standardisasi status pekerjaan
|
||||||
|
df['status_pekerjaan'] = df['status_pekerjaan'].apply(
|
||||||
|
lambda x: standardize_status_pekerjaan(x, None)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Clean text columns
|
||||||
|
text_columns = ['nama', 'jenis_perusahaan', 'nama_perusahaan', 'jabatan']
|
||||||
|
for col in text_columns:
|
||||||
|
if col in df.columns:
|
||||||
|
df[col] = df[col].apply(clean_text_value)
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def generate_summary(df):
|
||||||
|
"""
|
||||||
|
Generate summary statistics dari merged dataset.
|
||||||
|
"""
|
||||||
|
summary = []
|
||||||
|
summary.append("=" * 60)
|
||||||
|
summary.append("MERGE SUMMARY - TRACER STUDY ALUMNI")
|
||||||
|
summary.append(f"Tanggal: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
summary.append("=" * 60)
|
||||||
|
|
||||||
|
summary.append(f"\n>> TOTAL DATA: {len(df)} alumni")
|
||||||
|
|
||||||
|
summary.append("\n>> Distribusi per Tahun Lulus:")
|
||||||
|
tahun_counts = df['tahun_lulus'].value_counts().sort_index()
|
||||||
|
for tahun, count in tahun_counts.items():
|
||||||
|
summary.append(f" {tahun}: {count} alumni")
|
||||||
|
|
||||||
|
summary.append("\n>> Distribusi per Prodi (Kode):")
|
||||||
|
prodi_counts = df['kode_prodi'].value_counts()
|
||||||
|
for prodi, count in prodi_counts.items():
|
||||||
|
summary.append(f" {prodi}: {count} alumni")
|
||||||
|
|
||||||
|
summary.append("\n>> Distribusi Status Pekerjaan:")
|
||||||
|
status_counts = df['status_pekerjaan'].value_counts()
|
||||||
|
for status, count in status_counts.items():
|
||||||
|
pct = (count / len(df)) * 100
|
||||||
|
summary.append(f" {status}: {count} ({pct:.1f}%)")
|
||||||
|
|
||||||
|
summary.append("\n>> Sumber File:")
|
||||||
|
sumber_counts = df['sumber_file'].value_counts()
|
||||||
|
for sumber, count in sumber_counts.items():
|
||||||
|
summary.append(f" {sumber}: {count} alumni")
|
||||||
|
|
||||||
|
summary.append("\n>> Missing Values:")
|
||||||
|
for col in df.columns:
|
||||||
|
missing = df[col].isna().sum() + (df[col] == '').sum()
|
||||||
|
if missing > 0:
|
||||||
|
pct = (missing / len(df)) * 100
|
||||||
|
summary.append(f" {col}: {missing} ({pct:.1f}%)")
|
||||||
|
|
||||||
|
summary.append("\n" + "=" * 60)
|
||||||
|
|
||||||
|
return "\n".join(summary)
|
||||||
|
|
||||||
|
|
||||||
|
def print_msg(msg):
|
||||||
|
"""Print message dengan encoding handling."""
|
||||||
|
print(msg)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""
|
||||||
|
Main function untuk merge semua dataset.
|
||||||
|
"""
|
||||||
|
print("=" * 60)
|
||||||
|
print("MERGE DATASET TRACER STUDY (2019-2025)")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
all_dataframes = []
|
||||||
|
|
||||||
|
# Proses setiap tahun
|
||||||
|
for tahun in ["2019", "2020", "2021", "2022", "2023", "2024", "2025"]:
|
||||||
|
print(f"\n>> Memproses Tahun {tahun}...")
|
||||||
|
files = DATA_FILES[tahun]
|
||||||
|
separator = SEPARATORS[tahun]
|
||||||
|
|
||||||
|
df = read_and_map_year(tahun, files, separator)
|
||||||
|
|
||||||
|
if not df.empty:
|
||||||
|
all_dataframes.append(df)
|
||||||
|
print(f" >> Total dari {tahun}: {len(df)} baris")
|
||||||
|
else:
|
||||||
|
print(f" >> Tidak ada data untuk {tahun}")
|
||||||
|
|
||||||
|
if not all_dataframes:
|
||||||
|
print("\n[ERROR] Tidak ada data yang berhasil dibaca!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Merge semua data
|
||||||
|
print("\n>> Merging semua dataset...")
|
||||||
|
unified_df = pd.concat(all_dataframes, ignore_index=True)
|
||||||
|
print(f" >> Total setelah merge: {len(unified_df)} baris")
|
||||||
|
|
||||||
|
# Cleaning
|
||||||
|
unified_df = clean_dataset(unified_df)
|
||||||
|
|
||||||
|
# Generate summary
|
||||||
|
summary = generate_summary(unified_df)
|
||||||
|
print("\n" + summary)
|
||||||
|
|
||||||
|
# Ensure output directory exists
|
||||||
|
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
# Save unified dataset
|
||||||
|
output_path = os.path.join(OUTPUT_DIR, 'unified_dataset.csv')
|
||||||
|
unified_df.to_csv(output_path, index=False, encoding='utf-8-sig')
|
||||||
|
print(f"\n>> Dataset disimpan ke: {output_path}")
|
||||||
|
|
||||||
|
# Save summary
|
||||||
|
summary_path = os.path.join(OUTPUT_DIR, 'merge_summary.txt')
|
||||||
|
with open(summary_path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(summary)
|
||||||
|
print(f">> Summary disimpan ke: {summary_path}")
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("[SUCCESS] MERGE SELESAI!")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
return unified_df
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
df = main()
|
||||||
|
|
@ -0,0 +1,579 @@
|
||||||
|
# ============================================================
|
||||||
|
# MODUL STANDARISASI KOLOM
|
||||||
|
# ============================================================
|
||||||
|
# Fungsi: Menyamakan kolom dari berbagai tahun (2019-2025)
|
||||||
|
# Menjadi satu format standar
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# KONFIGURASI MAPPING KOLOM
|
||||||
|
# ============================================================
|
||||||
|
# Mapping ini menghubungkan nama kolom di setiap tahun Excel
|
||||||
|
# KE nama kolom STANDAR yang akan kita gunakan di database
|
||||||
|
#
|
||||||
|
# Format:
|
||||||
|
# "tahun": {
|
||||||
|
# "nama_kolom_standar": "nama_kolom_di_excel_tahun_tersebut"
|
||||||
|
# }
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
COLUMN_MAPPING = {
|
||||||
|
# ================================================
|
||||||
|
# TAHUN 2019 - Mapping kolom Excel 2019
|
||||||
|
# ================================================
|
||||||
|
# TODO: Ganti "?" dengan nama kolom sebenarnya dari Excel 2019
|
||||||
|
# ================================================
|
||||||
|
"2019": {
|
||||||
|
"nama": "?", # Contoh: "Nama Lengkap"
|
||||||
|
"perusahaan": "?", # Contoh: "Nama Perusahaan"
|
||||||
|
"jabatan": "?", # Contoh: "Jabatan"
|
||||||
|
"nim": "?" # Contoh: "NIM"
|
||||||
|
},
|
||||||
|
|
||||||
|
# ================================================
|
||||||
|
# TAHUN 2020 - Mapping kolom Excel 2020
|
||||||
|
# ================================================
|
||||||
|
# TODO: Ganti "?" dengan nama kolom sebenarnya dari Excel 2020
|
||||||
|
# ================================================
|
||||||
|
"2020": {
|
||||||
|
"nama": "?",
|
||||||
|
"perusahaan": "?",
|
||||||
|
"jabatan": "?",
|
||||||
|
"nim": "?"
|
||||||
|
},
|
||||||
|
|
||||||
|
# ================================================
|
||||||
|
# TAHUN 2021 - Mapping kolom Excel 2021
|
||||||
|
# ================================================
|
||||||
|
# TODO: Ganti "?" dengan nama kolom sebenarnya dari Excel 2021
|
||||||
|
# ================================================
|
||||||
|
"2021": {
|
||||||
|
"nama": "?",
|
||||||
|
"perusahaan": "?",
|
||||||
|
"jabatan": "?",
|
||||||
|
"nim": "?"
|
||||||
|
},
|
||||||
|
|
||||||
|
# ================================================
|
||||||
|
# TAHUN 2022 - Mapping kolom Excel 2022
|
||||||
|
# ================================================
|
||||||
|
# TODO: Ganti "?" dengan nama kolom sebenarnya dari Excel 2022
|
||||||
|
# ================================================
|
||||||
|
"2022": {
|
||||||
|
"nama": "?",
|
||||||
|
"perusahaan": "?",
|
||||||
|
"jabatan": "?",
|
||||||
|
"nim": "?"
|
||||||
|
},
|
||||||
|
|
||||||
|
# ================================================
|
||||||
|
# TAHUN 2023 - Mapping kolom Excel 2023
|
||||||
|
# ================================================
|
||||||
|
# TODO: Ganti "?" dengan nama kolom sebenarnya dari Excel 2023
|
||||||
|
# ================================================
|
||||||
|
"2023": {
|
||||||
|
"nama": "?",
|
||||||
|
"perusahaan": "?",
|
||||||
|
"jabatan": "?",
|
||||||
|
"nim": "?"
|
||||||
|
},
|
||||||
|
|
||||||
|
# ================================================
|
||||||
|
# TAHUN 2024 - Mapping kolom Excel 2024 (SUDAH DIKETAHUI)
|
||||||
|
# ================================================
|
||||||
|
"2024": {
|
||||||
|
"nama": "4. Nama Lengkap / nmmhsmsmh (Gunakan Huruf Besar)",
|
||||||
|
"perusahaan": "9. Apa nama perusahaan/kantor tempat Anda bekerja? f5b",
|
||||||
|
"jabatan": "10. Bila Anda berwiraswasta, apa posisi/jabatan Anda saat ini ? f5c",
|
||||||
|
"nim": "3. NIM / nimhsmsmh (Gunakan Huruf Besar) "
|
||||||
|
},
|
||||||
|
|
||||||
|
# ================================================
|
||||||
|
# TAHUN 2025 - Mapping kolom Excel 2025
|
||||||
|
# ================================================
|
||||||
|
# TODO: Ganti "?" dengan nama kolom sebenarnya dari Excel 2025
|
||||||
|
# ================================================
|
||||||
|
"2025": {
|
||||||
|
"nama": "?",
|
||||||
|
"perusahaan": "?",
|
||||||
|
"jabatan": "?",
|
||||||
|
"nim": "?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# KOLOM STANDAR (OUTPUT)
|
||||||
|
# ============================================================
|
||||||
|
# Ini adalah kolom FINAL yang akan kita gunakan setelah
|
||||||
|
# semua data dari berbagai tahun di-standardisasi
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
STANDAR_KOLOM = ["nama", "perusahaan", "jabatan", "nim"]
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# KELAS: KolomStandarisasi
|
||||||
|
# ============================================================
|
||||||
|
# Fungsi: Mengubah kolom Excel yang berbeda-beda dari
|
||||||
|
# setiap tahun menjadi kolom STANDAR yang konsisten
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
class KolomStandarisasi:
|
||||||
|
"""
|
||||||
|
Kelas untuk menstandarisasi kolom dari berbagai tahun.
|
||||||
|
|
||||||
|
PENJELASAN CARA KERJA:
|
||||||
|
======================
|
||||||
|
|
||||||
|
1. INPUT : DataFrame dengan kolom berbeda-beda per tahun
|
||||||
|
Contoh: df_2019 punya kolom "Nama Lengkap"
|
||||||
|
df_2024 punya kolom "4. Nama Lengkap / nmmhsmsmh..."
|
||||||
|
|
||||||
|
2. PROSES : Mapping kolom asli → kolom standar
|
||||||
|
Contoh: "Nama Lengkap" (2019) → "nama" (standar)
|
||||||
|
"4. Nama Lengkap..." (2024) → "nama" (standar)
|
||||||
|
|
||||||
|
3. OUTPUT : DataFrame dengan kolom standar:
|
||||||
|
["nama", "perusahaan", "jabatan", "nim"]
|
||||||
|
|
||||||
|
CONTOH PENGGUNAAN:
|
||||||
|
==================
|
||||||
|
>>> standardizer = KolomStandarisasi()
|
||||||
|
>>> df_standar = standardizer.standarisasi(df, tahun="2024")
|
||||||
|
>>> print(df_standar.columns.tolist())
|
||||||
|
['nama', 'perusahaan', 'jabatan', 'nim']
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, mapping: Dict = None):
|
||||||
|
"""
|
||||||
|
Inisialisasi standardizer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mapping: Dictionary mapping kolom. Jika None, pakai default.
|
||||||
|
|
||||||
|
PENJELASAN:
|
||||||
|
===========
|
||||||
|
Saat objek dibuat, kita simpan mapping kolom yang akan
|
||||||
|
digunakan untuk konversi. Mapping ini tells kita:
|
||||||
|
"Kolom X di tahun Y harus menjadi kolom Z di output"
|
||||||
|
"""
|
||||||
|
# Gunakan mapping yang diberikan, atau gunakan default
|
||||||
|
self.mapping = mapping if mapping is not None else COLUMN_MAPPING
|
||||||
|
|
||||||
|
# Tracking tahun yang didukung
|
||||||
|
self.tahun_tersedia = list(self.mapping.keys())
|
||||||
|
|
||||||
|
print(f"📋 KolomStandarisasi initialized")
|
||||||
|
print(f" Tahun yang didukung: {', '.join(self.tahun_tersedia)}")
|
||||||
|
|
||||||
|
# ========================================================
|
||||||
|
# METODE: get_tahun
|
||||||
|
# ========================================================
|
||||||
|
def get_tahun(self) -> List[str]:
|
||||||
|
"""
|
||||||
|
Ambil daftar tahun yang didukung.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List tahun, contoh: ["2019", "2020", "2021", ...]
|
||||||
|
"""
|
||||||
|
return self.tahun_tersedia
|
||||||
|
|
||||||
|
# ========================================================
|
||||||
|
# METODE: get_mapping_tahun
|
||||||
|
# ========================================================
|
||||||
|
def get_mapping_tahun(self, tahun: str) -> Dict:
|
||||||
|
"""
|
||||||
|
Ambil mapping kolom untuk tahun tertentu.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tahun: Tahun yang ingin diambil mappingnya
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary mapping kolom, contoh:
|
||||||
|
{"nama": "Nama Lengkap", "perusahaan": "..."}
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: Jika tahun tidak ditemukan dalam mapping
|
||||||
|
"""
|
||||||
|
# Cek apakah tahun ada dalam mapping
|
||||||
|
if tahun not in self.mapping:
|
||||||
|
tahun_available = ", ".join(self.tahun_tersedia)
|
||||||
|
raise ValueError(
|
||||||
|
f"Tahun '{tahun}' tidak ditemukan dalam mapping. "
|
||||||
|
f"Tahun tersedia: {tahun_available}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.mapping[tahun]
|
||||||
|
|
||||||
|
# ========================================================
|
||||||
|
# METODE: deteksi_tahun
|
||||||
|
# ========================================================
|
||||||
|
def deteksi_tahun(self, df: pd.DataFrame, nama_file: str = None) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Deteksi tahun dari DataFrame atau nama file.
|
||||||
|
|
||||||
|
CARA KERJA:
|
||||||
|
===========
|
||||||
|
1. Pertama, coba deteksi dari nama file (filename)
|
||||||
|
- Jika filename mengandung "2024", berarti tahun 2024
|
||||||
|
|
||||||
|
2. Jika gagal, cek dari kolom DataFrame
|
||||||
|
- Bandingkan kolom df dengan mapping setiap tahun
|
||||||
|
- Tahun dengan kecocokan kolom PALING BANYAK = tahun yang terdeteksi
|
||||||
|
|
||||||
|
Args:
|
||||||
|
df: DataFrame yang akan dideteksi tahun-nya
|
||||||
|
nama_file: Nama file Excel (opsional)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tahun dalam format string, contoh: "2024"
|
||||||
|
Returns None jika tidak dapat mendeteksi
|
||||||
|
"""
|
||||||
|
# ================================================
|
||||||
|
# CARA 1: Deteksi dari nama file
|
||||||
|
# ================================================
|
||||||
|
if nama_file:
|
||||||
|
for tahun in self.tahun_tersedia:
|
||||||
|
# Cek apakah tahun ada di nama file
|
||||||
|
# Contoh: "Tracer_Study_2024.xlsx" → mengandung "2024"
|
||||||
|
if tahun in nama_file:
|
||||||
|
print(f"🔍 Tahun terdeteksi dari filename: {tahun}")
|
||||||
|
return tahun
|
||||||
|
|
||||||
|
# ================================================
|
||||||
|
# CARA 2: Deteksi dari kolom DataFrame
|
||||||
|
# ================================================
|
||||||
|
# Inisialisasi tracker untuk setiap tahun
|
||||||
|
# Menyimpan jumlah kolom yang cocok
|
||||||
|
tahun_match = {}
|
||||||
|
|
||||||
|
for tahun, kolom_mapping in self.mapping.items():
|
||||||
|
# Hitung berapa kolom dari df yang cocok dengan mapping tahun ini
|
||||||
|
cocok = 0
|
||||||
|
for kolom_target, kolom_asli in kolom_mapping.items():
|
||||||
|
if kolom_asli in df.columns:
|
||||||
|
cocok += 1
|
||||||
|
|
||||||
|
# Simpan hasil
|
||||||
|
tahun_match[tahun] = cocok
|
||||||
|
|
||||||
|
# Cari tahun dengan kecocokan PALING BANYAK
|
||||||
|
if tahun_match:
|
||||||
|
# Pilih tahun dengan match tertinggi
|
||||||
|
tahun_terbaik = max(tahun_match, key=tahun_match.get)
|
||||||
|
jumlah_match = tahun_match[tahun_terbaik]
|
||||||
|
|
||||||
|
# Jika ada kecocokan (minimal 1 kolom)
|
||||||
|
if jumlah_match >= 1:
|
||||||
|
print(f"🔍 Tahun terdeteksi dari kolom: {tahun_terbaik} ({jumlah_match} kolom cocok)")
|
||||||
|
return tahun_terbaik
|
||||||
|
|
||||||
|
# ================================================
|
||||||
|
# GAGAL DETEKSI
|
||||||
|
# ================================================
|
||||||
|
print(f"⚠️ Tidak dapat mendeteksi tahun secara otomatis")
|
||||||
|
print(f" Kolom DataFrame: {df.columns.tolist()[:5]}...") # Tampilkan 5 kolom pertama
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ========================================================
|
||||||
|
# METODE: standarisasi
|
||||||
|
# ========================================================
|
||||||
|
def standarisasi(self, df: pd.DataFrame, tahun: str = None,
|
||||||
|
nama_file: str = None) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Standarisasi kolom DataFrame ke format standa
|
||||||
|
|
||||||
|
CARA KERJA:
|
||||||
|
===========
|
||||||
|
1. Deteksi tahun jika tidak disebutkan
|
||||||
|
2. Ambil mapping untuk tahun tersebut
|
||||||
|
3. Buat DataFrame baru dengan kolom standar
|
||||||
|
4. Copy data dari kolom asli ke kolom standar
|
||||||
|
|
||||||
|
Args:
|
||||||
|
df: DataFrame asli dari file Excel
|
||||||
|
tahun: Tahun data (otomatis detect jika None)
|
||||||
|
nama_file: Nama file (untuk deteksi otomatis)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DataFrame dengan kolom standar
|
||||||
|
|
||||||
|
Contoh:
|
||||||
|
=======
|
||||||
|
>>> df = pd.read_excel("data_2024.xlsx")
|
||||||
|
>>> std = KolomStandarisasi()
|
||||||
|
>>> df_standar = std.standarisasi(df, tahun="2024")
|
||||||
|
>>> print(df_standar.columns.tolist())
|
||||||
|
['nama', 'perusahaan', 'jabatan', 'nim']
|
||||||
|
"""
|
||||||
|
# ================================================
|
||||||
|
# LANGKAH 1: Deteksi tahun
|
||||||
|
# ================================================
|
||||||
|
if tahun is None:
|
||||||
|
tahun = self.deteksi_tahun(df, nama_file)
|
||||||
|
|
||||||
|
if tahun is None:
|
||||||
|
raise ValueError(
|
||||||
|
"Tidak dapat memproses data. "
|
||||||
|
"Tentukan tahun secara manual atau pastikan nama file mengandung tahun."
|
||||||
|
)
|
||||||
|
|
||||||
|
# ================================================
|
||||||
|
# LANGKAH 2: Ambil mapping
|
||||||
|
# ================================================
|
||||||
|
mapping = self.get_mapping_tahun(tahun)
|
||||||
|
print(f"📊 Melakukan standarisasi untuk tahun {tahun}")
|
||||||
|
|
||||||
|
# ================================================
|
||||||
|
# LANGKAH 3: Buat DataFrame kosong dengan kolom standar
|
||||||
|
# ================================================
|
||||||
|
df_standar = pd.DataFrame()
|
||||||
|
|
||||||
|
# ================================================
|
||||||
|
# LANGKAH 4: Copy data ke kolom standar
|
||||||
|
# ================================================
|
||||||
|
kolom_diproses = 0
|
||||||
|
kolom_gagal = []
|
||||||
|
|
||||||
|
for kolom_standar, kolom_asli in mapping.items():
|
||||||
|
# Cek apakah kolom ada di DataFrame asli
|
||||||
|
if kolom_asli in df.columns:
|
||||||
|
# Copy data
|
||||||
|
df_standar[kolom_standar] = df[kolom_asli]
|
||||||
|
kolom_diproses += 1
|
||||||
|
else:
|
||||||
|
# Kolom tidak ditemukan - tandai sebagai None
|
||||||
|
df_standar[kolom_standar] = None
|
||||||
|
kolom_gagal.append(kolom_asli)
|
||||||
|
|
||||||
|
# ================================================
|
||||||
|
# LANGKAH 5: Tampilkan ringkasan
|
||||||
|
# ================================================
|
||||||
|
print(f" ✅ Kolom berhasil diproses: {kolom_diproses}/{len(mapping)}")
|
||||||
|
|
||||||
|
if kolom_gagal:
|
||||||
|
print(f" ⚠️ Kolom tidak ditemukan: {kolom_gagal}")
|
||||||
|
|
||||||
|
return df_standar
|
||||||
|
|
||||||
|
# ========================================================
|
||||||
|
# METODE: standarisasi_batch
|
||||||
|
# ========================================================
|
||||||
|
def standarisasi_batch(self, data_list: List[Tuple[pd.DataFrame, str]]) -> List[pd.DataFrame]:
|
||||||
|
"""
|
||||||
|
Standarisasi multiple DataFrame sekaligus.
|
||||||
|
|
||||||
|
CARA KERJA:
|
||||||
|
===========
|
||||||
|
Proses beberapa DataFrame dari tahun berbeda dalam satu panggil
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data_list: List of tuple (DataFrame, tahun)
|
||||||
|
Contoh: [(df_2019, "2019"), (df_2024, "2024")]
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List DataFrame yang sudah distandarisasi
|
||||||
|
|
||||||
|
Contoh:
|
||||||
|
=======
|
||||||
|
>>> data = [
|
||||||
|
... (df_2019, "2019"),
|
||||||
|
... (df_2024, "2024")
|
||||||
|
... ]
|
||||||
|
>>> std = KolomStandarisasi()
|
||||||
|
>>> results = std.standarisasi_batch(data)
|
||||||
|
"""
|
||||||
|
hasil = []
|
||||||
|
|
||||||
|
for df, tahun in data_list:
|
||||||
|
try:
|
||||||
|
df_standar = self.standarisasi(df, tahun=tahun)
|
||||||
|
hasil.append(df_standar)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error memproses {tahun}: {e}")
|
||||||
|
# Skip tahun yang error, lanjut ke berikutnya
|
||||||
|
|
||||||
|
return hasil
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# FUNGSI: gabung_data
|
||||||
|
# ============================================================
|
||||||
|
def gabung_data(dataframes: List[pd.DataFrame],
|
||||||
|
tahun_list: List[str] = None) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Gabungkan beberapa DataFrame menjadi satu.
|
||||||
|
|
||||||
|
CARA KERJA:
|
||||||
|
===========
|
||||||
|
1. Terima list DataFrame dari berbagai tahun
|
||||||
|
2. Tambahkan kolom 'tahun' untuk setiap DataFrame
|
||||||
|
3. Gabungkan semua menjadi satu DataFrame
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dataframes: List DataFrame yang akan digabungkan
|
||||||
|
tahun_list: List tahun untuk setiap DataFrame (opsional)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DataFrame gabungan dengan kolom tambahan 'tahun'
|
||||||
|
"""
|
||||||
|
# Validasi input
|
||||||
|
if tahun_list and len(dataframes) != len(tahun_list):
|
||||||
|
raise ValueError(
|
||||||
|
f"Jumlah DataFrame ({len(dataframes)}) "
|
||||||
|
f"tidak sama dengan jumlah tahun ({len(tahun_list)})"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ================================================
|
||||||
|
# LANGKAH 1: Tambahkan kolom tahun
|
||||||
|
# ================================================
|
||||||
|
df_dengan_tahun = []
|
||||||
|
|
||||||
|
for i, df in enumerate(dataframes):
|
||||||
|
df_copy = df.copy()
|
||||||
|
|
||||||
|
# Tentukan tahun
|
||||||
|
tahun = tahun_list[i] if tahun_list else f"tahun_{i+1}"
|
||||||
|
|
||||||
|
# Tambahkan kolom tahun
|
||||||
|
df_copy['tahun'] = tahun
|
||||||
|
|
||||||
|
df_dengan_tahun.append(df_copy)
|
||||||
|
|
||||||
|
# ================================================
|
||||||
|
# LANGKAH 2: Gabungkan semua DataFrame
|
||||||
|
# ================================================
|
||||||
|
df_gabungan = pd.concat(df_dengan_tahun, ignore_index=True)
|
||||||
|
|
||||||
|
print(f"✅ Data berhasil digabungkan: {len(df_gabungan)} records")
|
||||||
|
|
||||||
|
return df_gabungan
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# FUNGSI: bersihkan_data
|
||||||
|
# ============================================================
|
||||||
|
def bersihkan_data(df: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Bersihkan data dari nilai kosong dan duplikat.
|
||||||
|
|
||||||
|
CARA KERJA:
|
||||||
|
===========
|
||||||
|
1. Hapus baris yang semua kolomnya kosong
|
||||||
|
2. Hapus baris duplikat
|
||||||
|
3. Isi nilai kosong dengan string kosong
|
||||||
|
|
||||||
|
Args:
|
||||||
|
df: DataFrame yang akan dibersihkan
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DataFrame yang sudah bersih
|
||||||
|
"""
|
||||||
|
print(f"🧹 Membersihkan data...")
|
||||||
|
|
||||||
|
# Simpan jumlah awal
|
||||||
|
jumlah_awal = len(df)
|
||||||
|
|
||||||
|
# ================================================
|
||||||
|
# LANGKAH 1: Hapus baris kosong
|
||||||
|
# ================================================
|
||||||
|
# dropna(how='all') = hapus baris yang SEMUA kolomnya kosong
|
||||||
|
df = df.dropna(how='all')
|
||||||
|
|
||||||
|
# ================================================
|
||||||
|
# LANGKAH 2: Hapus duplikat
|
||||||
|
# ================================================
|
||||||
|
# drop_duplicates() = hapus baris yang identik
|
||||||
|
df = df.drop_duplicates()
|
||||||
|
|
||||||
|
# ================================================
|
||||||
|
# LANGKAH 3: Isi nilai kosong
|
||||||
|
# ================================================
|
||||||
|
# fillna('') = ganti NaN/None dengan string kosong
|
||||||
|
df = df.fillna('')
|
||||||
|
|
||||||
|
# ================================================
|
||||||
|
# LANGKAH 4: Hitung statistik
|
||||||
|
# ================================================
|
||||||
|
dihapus = jumlah_awal - len(df)
|
||||||
|
|
||||||
|
if dihapus > 0:
|
||||||
|
print(f" ✅ Data dihapus: {dihapus} baris (kosong/duplikat)")
|
||||||
|
else:
|
||||||
|
print(f" ✅ Data bersih, tidak ada yang dihapus")
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# CONTOH PENGGUNAAN
|
||||||
|
# ============================================================
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Contoh 1: Inisialisasi
|
||||||
|
print("=" * 60)
|
||||||
|
print("CONTOH 1: Inisialisasi Standardizer")
|
||||||
|
print("=" * 60)
|
||||||
|
std = KolomStandarisasi()
|
||||||
|
|
||||||
|
# Contoh 2: Lihat tahun yang didukung
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("CONTOH 2: Tahun yang Didukung")
|
||||||
|
print("=" * 60)
|
||||||
|
tahun_list = std.get_tahun()
|
||||||
|
print(f"Tahun: {', '.join(tahun_list)}")
|
||||||
|
|
||||||
|
# Contoh 3: Lihat mapping untuk tahun tertentu
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("CONTOH 3: Mapping Tahun 2024")
|
||||||
|
print("=" * 60)
|
||||||
|
mapping_2024 = std.get_mapping_tahun("2024")
|
||||||
|
for standar, asli in mapping_2024.items():
|
||||||
|
print(f" {standar:12} ← {asli}")
|
||||||
|
|
||||||
|
# Contoh 4: Standarisasi data dummy
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("CONTOH 4: Standarisasi Data Dummy")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Buat data dummy yang mirip Excel 2024
|
||||||
|
data_dummy = pd.DataFrame({
|
||||||
|
"4. Nama Lengkap / nmmhsmsmh (Gunakan Huruf Besar)": [
|
||||||
|
"Muhammad Ali", "Budi Santoso", "Citra Dewi"
|
||||||
|
],
|
||||||
|
"9. Apa nama perusahaan/kantor tempat Anda bekerja? f5b": [
|
||||||
|
"Google Indonesia", "Tokopedia", "Gojek"
|
||||||
|
],
|
||||||
|
"10. Bila Anda berwiraswasta, apa posisi/jabatan Anda saat ini ? f5c": [
|
||||||
|
"Software Engineer", "Data Analyst", "Product Manager"
|
||||||
|
],
|
||||||
|
"3. NIM / nimhsmsmh (Gunakan Huruf Besar) ": [
|
||||||
|
"E41220001", "E41220002", "E41220003"
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
print("Data sebelum standarisasi:")
|
||||||
|
print(data_dummy.head())
|
||||||
|
|
||||||
|
# Standarisasi
|
||||||
|
data_standar = std.standarisasi(data_dummy, tahun="2024")
|
||||||
|
|
||||||
|
print("\nData setelah standarisasi:")
|
||||||
|
print(data_standar.head())
|
||||||
|
|
||||||
|
# Contoh 5: Gabungkan dan bersihkan
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("CONTOH 5: Gabungkan dan Bersihkan")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Gabungkan data dummy dengan dirinya sendiri (untuk simulasi)
|
||||||
|
data_gabungan = gabung_data([data_standar, data_standar], ["2024", "2024"])
|
||||||
|
print(f"Jumlah setelah gabung: {len(data_gabungan)} records")
|
||||||
|
|
||||||
|
# Bersihkan
|
||||||
|
data_bersih = bersihkan_data(data_gabungan)
|
||||||
|
print(f"Jumlah setelah bersih: {len(data_bersih)} records")
|
||||||
|
|
@ -0,0 +1,435 @@
|
||||||
|
"""
|
||||||
|
=============================================================
|
||||||
|
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()
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
# Utils module
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
# FastAPI Backend - TA Alumni
|
||||||
|
|
||||||
|
Repositori ini adalah bagian Backend dari proyek **TA-PROJEK**, yang dibangun menggunakan **FastAPI** dengan struktur folder yang mengadopsi pola **Clean Architecture** (mirip dengan arsitektur enterprise pada **Node.js** seperti Express/NestJS).
|
||||||
|
|
||||||
|
Struktur ini dibuat sangat terstruktur untuk memudahkan kolaborasi tim, terutama bagi developer junior, agar kode tetap rapi, mudah diuji, dan memiliki pemisahan tanggung jawab (Separation of Concerns) yang jelas.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📂 Struktur Folder & Fungsi
|
||||||
|
|
||||||
|
Berikut adalah penjelasan fungsi dari setiap folder di dalam `app/`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Backend/
|
||||||
|
├── .env # Variabel lingkungan aktif (lokal) - JANGAN di-commit ke Git!
|
||||||
|
├── .env.example # Template variabel lingkungan untuk anggota tim baru
|
||||||
|
├── requirements.txt # Daftar dependensi library Python
|
||||||
|
├── README.md # Dokumentasi panduan developer (file ini)
|
||||||
|
└── app/
|
||||||
|
├── main.py # Entry point utama aplikasi (mirip server.js atau app.js di Node.js)
|
||||||
|
│
|
||||||
|
├── config/ # Konfigurasi aplikasi & koneksi database
|
||||||
|
│ ├── config.py # Schema pembacaan & validasi file .env menggunakan Pydantic Settings
|
||||||
|
│ └── database.py # Setup engine SQLAlchemy, Session, dan koneksi ke database
|
||||||
|
│
|
||||||
|
├── models/ # Definisi skema tabel Database (SQLAlchemy Models)
|
||||||
|
│ ├── base.py # Base class SQLAlchemy yang membagikan metadata tabel
|
||||||
|
│ ├── user.py # Model untuk tabel 'users'
|
||||||
|
│ └── alumni.py # Model untuk tabel 'alumni'
|
||||||
|
│
|
||||||
|
├── schemas/ # Data Transfer Object (DTO) / Validasi Request & Response (Pydantic)
|
||||||
|
│ ├── user.py # Validasi input/output data User
|
||||||
|
│ └── alumni.py # Validasi input/output data Alumni
|
||||||
|
│
|
||||||
|
├── repositories/ # Layer akses database (Data Access Object / DAO)
|
||||||
|
│ ├── base.py # Generic Repository yang berisi fungsi CRUD dasar (Get, Create, Update, Delete)
|
||||||
|
│ └── alumni.py # Operasi database spesifik untuk entitas Alumni
|
||||||
|
│
|
||||||
|
├── services/ # Layer Logika Bisnis (Business Logic)
|
||||||
|
│ ├── auth.py # Logika registrasi, verifikasi password, login, dan pembuatan token JWT
|
||||||
|
│ └── alumni.py # Logika bisnis seputar validasi dan pengolahan data alumni
|
||||||
|
│
|
||||||
|
├── controllers/ # Pengendali HTTP (Request & Response handler)
|
||||||
|
│ ├── auth.py # Menangani input request auth, memanggil auth service, mengembalikan HTTP response
|
||||||
|
│ └── alumni.py # Menangani input request alumni, memanggil alumni service, mengembalikan HTTP response
|
||||||
|
│
|
||||||
|
├── routers/ # Pendefinisian Endpoint / Route API (sama seperti routes/ di Node.js)
|
||||||
|
│ ├── api.py # Central router registry (menggabungkan semua router dengan prefix API)
|
||||||
|
│ ├── auth.py # Endpoint untuk registrasi dan login (/api/v1/auth)
|
||||||
|
│ └── alumni.py # Endpoint untuk CRUD data alumni (/api/v1/alumni)
|
||||||
|
│
|
||||||
|
├── middleware/ # Fungsi penengah/filter request (seperti Auth Guards)
|
||||||
|
│ └── auth_middleware.py # Dependency injection untuk proteksi route menggunakan token JWT
|
||||||
|
│
|
||||||
|
└── utils/ # Helper fungsi utilitas umum
|
||||||
|
├── security.py # Fungsi enkripsi/hashing password dan generator JWT
|
||||||
|
└── helpers.py # Utilitas pembantu lainnya
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Aliran Data (Data Flow)
|
||||||
|
|
||||||
|
Saat aplikasi menerima request dari frontend, aliran datanya adalah sebagai berikut:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
Client[Klien/Frontend] -->|1. Kirim HTTP Request| Router[Routers]
|
||||||
|
Router -->|2. Validasi dengan Pydantic| Schema[Schemas]
|
||||||
|
Router -->|3. Teruskan parameter| Controller[Controllers]
|
||||||
|
Controller -->|4. Panggil logika bisnis| Service[Services]
|
||||||
|
Service -->|5. Minta operasi DB| Repository[Repositories]
|
||||||
|
Repository -->|6. Manipulasi data| Model[Models/Database]
|
||||||
|
Model -->|7. Kembalikan data| Repository
|
||||||
|
Repository -->|8. Kembalikan entitas| Service
|
||||||
|
Service -->|9. Kembalikan hasil proses| Controller
|
||||||
|
Controller -->|10. Format Output & Status Code| Router
|
||||||
|
Router -->|11. Kirim HTTP Response| Client
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **Routers** (`app/routers/`): Menerima HTTP Request, mencocokkan path, menerapkan proteksi middleware jika perlu, dan memvalidasi tipe data input menggunakan **Schemas** (Pydantic).
|
||||||
|
2. **Controllers** (`app/controllers/`): Mengambil data dari request yang sudah tervalidasi, mengelola status code HTTP, dan memanggil **Services**.
|
||||||
|
3. **Services** (`app/services/`): Tempat utama menulis logika bisnis (seperti verifikasi aturan bisnis, perhitungan, integrasi AI/ML, enkripsi password). Memanggil **Repositories**.
|
||||||
|
4. **Repositories** (`app/repositories/`): Melakukan operasi database langsung menggunakan **Models** (SQLAlchemy). Layer ini memisahkan logika kueri SQL/ORM dari logika bisnis.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Cara Memulai & Menjalankan Proyek
|
||||||
|
|
||||||
|
Langkah-langkah berikut ditujukan untuk developer junior agar dapat menjalankan backend di lokal dengan cepat:
|
||||||
|
|
||||||
|
### 1. Buat Virtual Environment
|
||||||
|
Buka terminal di folder `Backend/` dan jalankan perintah berikut:
|
||||||
|
|
||||||
|
**Windows (PowerShell):**
|
||||||
|
```powershell
|
||||||
|
python -m venv venv
|
||||||
|
.\venv\Scripts\Activate
|
||||||
|
```
|
||||||
|
|
||||||
|
**Linux/macOS:**
|
||||||
|
```bash
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Install Dependensi
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Konfigurasi Environment File
|
||||||
|
Secara default, file `.env` sudah dikonfigurasi menggunakan database SQLite lokal bernama `ta_database.db`. Anda tidak perlu menginstal database server tambahan untuk memulai development.
|
||||||
|
|
||||||
|
### 4. Jalankan Aplikasi
|
||||||
|
Jalankan development server menggunakan Uvicorn:
|
||||||
|
```bash
|
||||||
|
uvicorn app.main:app --reload
|
||||||
|
```
|
||||||
|
Aplikasi akan berjalan di alamat `http://127.0.0.1:8000`.
|
||||||
|
|
||||||
|
### 5. Akses Dokumentasi Swagger API
|
||||||
|
Buka browser dan arahkan ke alamat berikut untuk melihat & menguji API secara interaktif:
|
||||||
|
👉 **`http://127.0.0.1:8000/docs`**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Panduan Menambahkan Fitur Baru (Untuk Junior Team)
|
||||||
|
|
||||||
|
Jika Anda ingin menambahkan modul/fitur baru (misalnya fitur **Dosen**):
|
||||||
|
|
||||||
|
1. **Buat Model Database**:
|
||||||
|
Buat file `app/models/dosen.py` yang mendefinisikan skema tabel database. Registrasikan ke database engine jika diperlukan.
|
||||||
|
2. **Buat Pydantic Schema**:
|
||||||
|
Buat file `app/schemas/dosen.py` untuk validasi data request masuk (`DosenCreate`, `DosenUpdate`) dan data response keluar (`DosenResponse`).
|
||||||
|
3. **Buat Repository**:
|
||||||
|
Buat file `app/repositories/dosen.py`. Warisi dari `BaseRepository` untuk mendapatkan fitur CRUD bawaan secara instan.
|
||||||
|
4. **Buat Service**:
|
||||||
|
Buat file `app/services/dosen.py` untuk mengolah logika bisnis Dosen.
|
||||||
|
5. **Buat Controller**:
|
||||||
|
Buat file `app/controllers/dosen.py` untuk memetakan request ke service dan menyusun output response.
|
||||||
|
6. **Buat & Daftarkan Router**:
|
||||||
|
Buat file `app/routers/dosen.py`. Daftarkan file router ini ke dalam file registry utama di `app/routers/api.py`.
|
||||||
|
|
@ -0,0 +1,203 @@
|
||||||
|
import pymysql
|
||||||
|
|
||||||
|
conn = pymysql.connect(host='localhost', user='root', password='', database='tracerstudy_db', port=3306, charset='utf8mb4')
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# Get question IDs
|
||||||
|
cur.execute('SELECT id, kode FROM questions WHERE kode IN ("F5a1", "F5a2")')
|
||||||
|
questions = {row[1]: row[0] for row in cur.fetchall()}
|
||||||
|
print(f'Found questions: {questions}')
|
||||||
|
|
||||||
|
# ========== PROVINSI (F5a1) ==========
|
||||||
|
province_data = [
|
||||||
|
('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'),
|
||||||
|
]
|
||||||
|
|
||||||
|
if 'F5a1' in questions:
|
||||||
|
q_id = questions['F5a1']
|
||||||
|
for kode, teks in province_data:
|
||||||
|
cur.execute('''
|
||||||
|
INSERT INTO answer_options (question_id, kode, teks, urutan)
|
||||||
|
VALUES (%s, %s, %s, %s)
|
||||||
|
ON DUPLICATE KEY UPDATE teks = VALUES(teks)
|
||||||
|
''', (q_id, kode, teks, len(province_data)))
|
||||||
|
print(f'Inserted {len(province_data)} provinces')
|
||||||
|
|
||||||
|
# ========== KABUPATEN/KOTA (F5a2) ==========
|
||||||
|
# Sample beberapa kabupaten/kota utama
|
||||||
|
kab_kota_data = [
|
||||||
|
# Jawa Timur (50000)
|
||||||
|
('52400', 'Kab. Jember', 1),
|
||||||
|
('56000', 'Kota Surabaya', 2),
|
||||||
|
('50200', 'Kab. Sidoarjo', 3),
|
||||||
|
('50100', 'Kab. Gresik', 4),
|
||||||
|
('52500', 'Kab. Banyuwangi', 5),
|
||||||
|
('52600', 'Kab. Pamekasan', 6),
|
||||||
|
('52700', 'Kab. Sampang', 7),
|
||||||
|
('52800', 'Kab. Sumenep', 8),
|
||||||
|
('50300', 'Kab. Mojokerto', 9),
|
||||||
|
('50400', 'Kab. Jombang', 10),
|
||||||
|
('50500', 'Kab. Bojonegoro', 11),
|
||||||
|
('50600', 'Kab. Tuban', 12),
|
||||||
|
('50700', 'Kab. Lamongan', 13),
|
||||||
|
('50800', 'Kab. Madiun', 14),
|
||||||
|
('50900', 'Kab. Ngawi', 15),
|
||||||
|
('51000', 'Kab. Magetan', 16),
|
||||||
|
('51100', 'Kab. Ponorogo', 17),
|
||||||
|
('51200', 'Kab. Pacitan', 18),
|
||||||
|
('51300', 'Kab. Kediri', 19),
|
||||||
|
('51400', 'Kab. Nganjuk', 20),
|
||||||
|
('51500', 'Kab. Blitar', 21),
|
||||||
|
('51600', 'Kab. Tulungagung', 22),
|
||||||
|
('51700', 'Kab. Trenggalek', 23),
|
||||||
|
('51800', 'Kab. Malang', 24),
|
||||||
|
('51900', 'Kab. Pasuruan', 25),
|
||||||
|
('52000', 'Kab. Probolinggo', 26),
|
||||||
|
('52100', 'Kab. Lumajang', 27),
|
||||||
|
('52200', 'Kab. Bondowoso', 28),
|
||||||
|
('52300', 'Kab. Situbondo', 29),
|
||||||
|
('56400', 'Kota Mojokerto', 30),
|
||||||
|
('56100', 'Kota Malang', 31),
|
||||||
|
('56200', 'Kota Madiun', 32),
|
||||||
|
('56300', 'Kota Kediri', 33),
|
||||||
|
('56500', 'Kota Blitar', 34),
|
||||||
|
('56600', 'Kota Pasuruan', 35),
|
||||||
|
('56700', 'Kota Probolinggo', 36),
|
||||||
|
('56800', 'Kota Batu', 37),
|
||||||
|
|
||||||
|
# Jawa Barat (20000)
|
||||||
|
('20500', 'Kab. Bogor', 38),
|
||||||
|
('20600', 'Kab. Cianjur', 39),
|
||||||
|
('20700', 'Kab. Bandung', 40),
|
||||||
|
('20800', 'Kab. Bandung Barat', 41),
|
||||||
|
('21000', 'Kab. Sumedang', 41),
|
||||||
|
('21100', 'Kab. Garut', 42),
|
||||||
|
('21200', 'Kab. Tasikmalaya', 43),
|
||||||
|
('21400', 'Kab. Ciamis', 44),
|
||||||
|
('21500', 'Kab. Kuningan', 45),
|
||||||
|
('21600', 'Kab. Majalengka', 46),
|
||||||
|
('21700', 'Kab. Cirebon', 47),
|
||||||
|
('21800', 'Kab. Indramayu', 48),
|
||||||
|
('21900', 'Kab. Subang', 49),
|
||||||
|
('22000', 'Kab. Purwakarta', 50),
|
||||||
|
('22100', 'Kab. Karawang', 51),
|
||||||
|
('22200', 'Kab. Bekasi', 52),
|
||||||
|
('22300', 'Kab. Bandung Barat', 53),
|
||||||
|
('22500', 'Kab. Pangandaran', 54),
|
||||||
|
('26000', 'Kota Bandung', 55),
|
||||||
|
('26100', 'Kota Bogor', 56),
|
||||||
|
('26200', 'Kota Sukabumi', 57),
|
||||||
|
('26300', 'Kota Cirebon', 58),
|
||||||
|
('26500', 'Kota Bekasi', 59),
|
||||||
|
('26600', 'Kota Depok', 60),
|
||||||
|
('26700', 'Kota Cimahi', 61),
|
||||||
|
('26800', 'Kota Tasikmalaya', 62),
|
||||||
|
('26900', 'Kota Banjar', 63),
|
||||||
|
|
||||||
|
# Jawa Tengah (30000)
|
||||||
|
('30100', 'Kab. Cilacap', 64),
|
||||||
|
('30200', 'Kab. Banyumas', 65),
|
||||||
|
('30300', 'Kab. Purbalingga', 66),
|
||||||
|
('30400', 'Kab. Banjarnegara', 67),
|
||||||
|
('30500', 'Kab. Kebumen', 68),
|
||||||
|
('30600', 'Kab. Purworejo', 69),
|
||||||
|
('30700', 'Kab. Wonosobo', 70),
|
||||||
|
('30800', 'Kab. Magelang', 71),
|
||||||
|
('30900', 'Kab. Boyolali', 72),
|
||||||
|
('31000', 'Kab. Klaten', 73),
|
||||||
|
('31100', 'Kab. Sukoharjo', 74),
|
||||||
|
('31300', 'Kab. Karanganyar', 75),
|
||||||
|
('31400', 'Kab. Wonogiri', 76),
|
||||||
|
('31500', 'Kab. Sragen', 77),
|
||||||
|
('31600', 'Kab. Grobogan', 78),
|
||||||
|
('31700', 'Kab. Blora', 79),
|
||||||
|
('31800', 'Kab. Rembang', 80),
|
||||||
|
('31900', 'Kab. Kudus', 81),
|
||||||
|
('32000', 'Kab. Jepara', 82),
|
||||||
|
('32100', 'Kab. Demak', 83),
|
||||||
|
('32200', 'Kab. Semarang', 84),
|
||||||
|
('32300', 'Kab. Temanggung', 85),
|
||||||
|
('32400', 'Kab. Kendal', 86),
|
||||||
|
('32500', 'Kab. Batang', 87),
|
||||||
|
('32600', 'Kab. Pekalongan', 88),
|
||||||
|
('32700', 'Kab. Pemalang', 89),
|
||||||
|
('32800', 'Kab. Tegal', 90),
|
||||||
|
('32900', 'Kab. Brebes', 91),
|
||||||
|
('36000', 'Kota Magelang', 92),
|
||||||
|
('36100', 'Kota Surakarta', 93),
|
||||||
|
('36200', 'Kota Salatiga', 94),
|
||||||
|
('36300', 'Kota Semarang', 95),
|
||||||
|
('36400', 'Kota Pekalongan', 96),
|
||||||
|
('36500', 'Kota Tegal', 97),
|
||||||
|
|
||||||
|
# D.K.I. Jakarta (10000)
|
||||||
|
('16000', 'Kota Jakarta Pusat', 98),
|
||||||
|
('16100', 'Kota Jakarta Utara', 99),
|
||||||
|
('16200', 'Kota Jakarta Barat', 100),
|
||||||
|
('16300', 'Kota Jakarta Selatan', 101),
|
||||||
|
('16400', 'Kota Jakarta Timur', 102),
|
||||||
|
('10100', 'Kab. Kepulauan Seribu', 103),
|
||||||
|
|
||||||
|
# D.I. Yogyakarta (40000)
|
||||||
|
('40100', 'Kab. Kulon Progo', 104),
|
||||||
|
('40200', 'Kab. Sleman', 105),
|
||||||
|
('40300', 'Kab. Gunung Kidul', 106),
|
||||||
|
('40400', 'Kab. Bantul', 107),
|
||||||
|
('46000', 'Kota Yogyakarta', 108),
|
||||||
|
|
||||||
|
# Jawa Timur - extended
|
||||||
|
('51400', 'Kab. Nganjuk', 109),
|
||||||
|
('51900', 'Kab. Pasuruan', 110),
|
||||||
|
('52900', 'Kab. Bangkalan', 111),
|
||||||
|
|
||||||
|
# Luar Negeri
|
||||||
|
('999999', 'Luar Negeri', 112),
|
||||||
|
]
|
||||||
|
|
||||||
|
if 'F5a2' in questions:
|
||||||
|
q_id = questions['F5a2']
|
||||||
|
for kode, teks, urutan in kab_kota_data:
|
||||||
|
cur.execute('''
|
||||||
|
INSERT INTO answer_options (question_id, kode, teks, urutan)
|
||||||
|
VALUES (%s, %s, %s, %s)
|
||||||
|
ON DUPLICATE KEY UPDATE teks = VALUES(teks)
|
||||||
|
''', (q_id, kode, teks, urutan))
|
||||||
|
print(f'Inserted {len(kab_kota_data)} kabupaten/kota')
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
print('Done!')
|
||||||
|
conn.close()
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
# Mengimpor modul bawaan Python 'os' untuk manipulasi path direktori dan file sistem operasi
|
||||||
|
import os
|
||||||
|
# Mengimpor BaseSettings dan SettingsConfigDict dari pydantic_settings untuk manajemen variabel lingkungan (.env) yang bertipe data aman (type-safe)
|
||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
# Mendefinisikan class Settings yang mewarisi BaseSettings untuk menyimpan semua konfigurasi global aplikasi
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
# ================= Pengaturan Aplikasi (App Settings) =================
|
||||||
|
# Nama aplikasi yang ditampilkan pada dokumentasi Swagger / OpenAPI
|
||||||
|
APP_NAME: str = "FastAPI TA Alumni"
|
||||||
|
# Mode debug: bernilai True saat proses pengembangan untuk menampilkan detail log error
|
||||||
|
DEBUG: bool = True
|
||||||
|
# Prefix rute utama untuk semua endpoint REST API (versi 1)
|
||||||
|
API_PREFIX: str = "/api/v1"
|
||||||
|
|
||||||
|
# ================= Pengaturan Database (Database Settings) =================
|
||||||
|
# URL koneksi database MySQL menggunakan driver pymysql (format: mysql+pymysql://user:password@host:port/nama_database)
|
||||||
|
DATABASE_URL: str = "mysql+pymysql://root:@localhost:3306/tracerstudy_db"
|
||||||
|
|
||||||
|
# ================= Pengaturan Keamanan & JWT (Security Settings) =================
|
||||||
|
# Kunci rahasia (secret key) yang digunakan untuk mengenkripsi dan memverifikasi signature token JWT
|
||||||
|
JWT_SECRET_KEY: str = "436ad82810a95059d9c87d4982a514d87455ee4a4c16d68b6d773fb62b661d9a"
|
||||||
|
# Algoritma kriptografi yang digunakan untuk menandatangani token JWT (HMAC-SHA256)
|
||||||
|
JWT_ALGORITHM: str = "HS256"
|
||||||
|
# Masa berlaku akses token JWT dalam satuan menit (1440 menit = 24 jam)
|
||||||
|
ACCESS_TOKEN_EXPIRE_MINUTES: int = 1440
|
||||||
|
|
||||||
|
# ================= Konfigurasi Pembacaan File .env =================
|
||||||
|
# Konfigurasi Pydantic untuk menentukan lokasi pembacaan file .env
|
||||||
|
model_config = SettingsConfigDict(
|
||||||
|
# Menentukan path file .env yang berada 3 level di atas file ini (root direktori Backend/)
|
||||||
|
env_file=os.path.join(
|
||||||
|
# Mendapatkan direktori root Backend/ secara dinamis dari path file ini (__file__)
|
||||||
|
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
|
||||||
|
".env"
|
||||||
|
),
|
||||||
|
# Encoding karakter file .env menggunakan format utf-8
|
||||||
|
env_file_encoding="utf-8",
|
||||||
|
# Mengabaikan variabel tambahan yang ada di .env jika tidak didefinisikan di class Settings
|
||||||
|
extra="ignore"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Menginstansiasi objek settings agar dapat diimpor dan digunakan secara langsung di seluruh modul sistem
|
||||||
|
settings = Settings()
|
||||||
|
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
# Mengimpor modul Any untuk type hint fleksibel dari modul standar typing Python
|
||||||
|
from typing import Any
|
||||||
|
# Mengimpor fungsi create_engine dari SQLAlchemy untuk membuat engine penghubung ke database
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
# Mengimpor sessionmaker dari SQLAlchemy ORM untuk membuat factory pembentuk sesi transaksi database
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
# Mengimpor objek konfigurasi global settings yang berisi DATABASE_URL
|
||||||
|
from app.config.config import settings
|
||||||
|
|
||||||
|
# Memeriksa apakah database yang digunakan adalah SQLite berdasarkan awalan protokol connection string
|
||||||
|
is_sqlite = settings.DATABASE_URL.startswith("sqlite")
|
||||||
|
# Jika database adalah SQLite, nonaktifkan check_same_thread agar session dapat digunakan lintas thread FastAPI; jika MySQL/PostgreSQL gunakan dictionary kosong
|
||||||
|
connect_args = {"check_same_thread": False} if is_sqlite else {}
|
||||||
|
|
||||||
|
# Menyiapkan dictionary parameter konfigurasi untuk SQLAlchemy engine
|
||||||
|
engine_kwargs: dict[str, Any] = {
|
||||||
|
# Argumen koneksi level driver (khusus SQLite atau kosong untuk MySQL)
|
||||||
|
"connect_args": connect_args,
|
||||||
|
# Menentukan apakah setiap query mentah dicetak ke terminal (False = dinonaktifkan demi performa dan kebersihan log)
|
||||||
|
"echo": False, # Matikan SQL echo agar tidak memperlambat I/O console terminal
|
||||||
|
}
|
||||||
|
|
||||||
|
# Jika bukan SQLite (misal MySQL pada produksi atau pengembangan lokal), tambahkan konfigurasi connection pooling
|
||||||
|
if not is_sqlite:
|
||||||
|
engine_kwargs.update({
|
||||||
|
# pool_pre_ping: Memeriksa kesehatan koneksi sebelum query dijalankan untuk mencegah error koneksi mati (MySQL server has gone away)
|
||||||
|
"pool_pre_ping": True,
|
||||||
|
# pool_recycle: Mendaur ulang koneksi database setiap 1800 detik (30 menit) agar tidak diputus paksa oleh timeout server MySQL
|
||||||
|
"pool_recycle": 1800,
|
||||||
|
# pool_size: Jumlah koneksi database tetap yang dipertahankan dalam memory pool
|
||||||
|
"pool_size": 15,
|
||||||
|
# max_overflow: Jumlah koneksi tambahan maksimal yang diizinkan dibuat saat lalu lintas request sedang tinggi
|
||||||
|
"max_overflow": 25,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Membuat instance Engine SQLAlchemy sebagai gerbang utama komunikasi antara aplikasi FastAPI dan server database
|
||||||
|
engine = create_engine(
|
||||||
|
# URL database (contoh: mysql+pymysql://root:@localhost:3306/tracerstudy_db)
|
||||||
|
settings.DATABASE_URL,
|
||||||
|
# Unpack dictionary argumen konfigurasi engine yang telah disesuaikan
|
||||||
|
**engine_kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
# Membuat class factory SessionLocal untuk menghasilkan sesi database baru pada setiap request
|
||||||
|
# autocommit=False: Transaksi tidak langsung di-commit otomatis (memungkinkan commit/rollback manual demi konsistensi data)
|
||||||
|
# autoflush=False: Mencegah perubahan di-flush ke database sebelum commit atau query eksplisit
|
||||||
|
# bind=engine: Mengikat sesi ini ke engine database yang telah dibuat
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
|
# Fungsi Dependency Injection untuk FastAPI yang menyediakan sesi database ke setiap endpoint API
|
||||||
|
def get_db():
|
||||||
|
"""
|
||||||
|
Menghasilkan (yields) instance sesi database untuk satu request HTTP.
|
||||||
|
Memastikan koneksi selalu ditutup (closed) di blok finally bahkan jika terjadi error selama eksekusi request.
|
||||||
|
"""
|
||||||
|
# Membuka sesi database baru dari factory SessionLocal
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
# Memberikan sesi database aktif ke controller/endpoint yang memanggilnya
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
# Menutup sesi database setelah request selesai diproses agar koneksi dikembalikan ke connection pool dan tidak terjadi memory leak
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
@ -0,0 +1,354 @@
|
||||||
|
import math
|
||||||
|
from typing import Optional
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.services.alumni import AlumniService
|
||||||
|
from app.schemas.alumni import (
|
||||||
|
AlumniCreate,
|
||||||
|
AlumniUpdate,
|
||||||
|
AlumniResponse,
|
||||||
|
AlumniDetailResponse,
|
||||||
|
)
|
||||||
|
from app.utils.helpers import format_response
|
||||||
|
|
||||||
|
class AlumniController:
|
||||||
|
"""
|
||||||
|
Controller handling HTTP requests and response formatting for the Alumni entity.
|
||||||
|
"""
|
||||||
|
@staticmethod
|
||||||
|
def get_alumni(db: Session, alumni_id: int):
|
||||||
|
"""Get an alumni record with full details (pekerjaan, kompetensi, tracer responses, clustering)."""
|
||||||
|
alumni = AlumniService.get_by_id_detailed(db, alumni_id)
|
||||||
|
alumni_schema = AlumniDetailResponse.model_validate(alumni)
|
||||||
|
return format_response(True, "Berhasil mengambil data detail alumni", alumni_schema.model_dump())
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_all_alumni(
|
||||||
|
db: Session,
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 10,
|
||||||
|
search: Optional[str] = None,
|
||||||
|
kode_prodi: Optional[str] = None,
|
||||||
|
tahun_lulus: Optional[int] = None,
|
||||||
|
status_kerja: Optional[str] = None,
|
||||||
|
):
|
||||||
|
"""Get filtered alumni list with pagination metadata."""
|
||||||
|
alumni_list, total = AlumniService.get_filtered(
|
||||||
|
db=db,
|
||||||
|
skip=skip,
|
||||||
|
limit=limit,
|
||||||
|
search=search,
|
||||||
|
kode_prodi=kode_prodi,
|
||||||
|
tahun_lulus=tahun_lulus,
|
||||||
|
status_kerja=status_kerja,
|
||||||
|
)
|
||||||
|
|
||||||
|
data = [AlumniResponse.model_validate(a).model_dump() for a in alumni_list]
|
||||||
|
page = (skip // limit) + 1 if limit > 0 else 1
|
||||||
|
total_pages = math.ceil(total / limit) if limit > 0 else 1
|
||||||
|
|
||||||
|
pagination_data = {
|
||||||
|
"items": data,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"limit": limit,
|
||||||
|
"total_pages": total_pages,
|
||||||
|
}
|
||||||
|
|
||||||
|
return format_response(True, "Berhasil mengambil data alumni", pagination_data)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_alumni(db: Session, alumni_in: AlumniCreate):
|
||||||
|
"""Create a new alumni record and format the response."""
|
||||||
|
from fastapi import HTTPException
|
||||||
|
try:
|
||||||
|
new_alumni = AlumniService.create(db, alumni_in)
|
||||||
|
alumni_schema = AlumniResponse.model_validate(new_alumni)
|
||||||
|
return format_response(True, "Berhasil menambahkan data alumni", alumni_schema.model_dump())
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
raise HTTPException(status_code=400, detail=f"Gagal menambahkan data alumni: {str(e)}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def update_alumni(db: Session, alumni_id: int, alumni_in: AlumniUpdate):
|
||||||
|
"""Update an existing alumni record and format the response."""
|
||||||
|
from fastapi import HTTPException
|
||||||
|
try:
|
||||||
|
updated_alumni = AlumniService.update(db, alumni_id, alumni_in)
|
||||||
|
alumni_schema = AlumniResponse.model_validate(updated_alumni)
|
||||||
|
return format_response(True, "Berhasil memperbarui data alumni", alumni_schema.model_dump())
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
raise HTTPException(status_code=400, detail=f"Gagal memperbarui data alumni: {str(e)}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def delete_alumni(db: Session, alumni_id: int):
|
||||||
|
"""Delete an alumni record and format the response cleanly with child table cleanup."""
|
||||||
|
from app.models.models import Pekerjaan, Kompetensi, TracerResponse, ClusteringResult
|
||||||
|
db.query(Pekerjaan).filter(Pekerjaan.alumni_id == alumni_id).delete(synchronize_session=False)
|
||||||
|
db.query(Kompetensi).filter(Kompetensi.alumni_id == alumni_id).delete(synchronize_session=False)
|
||||||
|
db.query(TracerResponse).filter(TracerResponse.alumni_id == alumni_id).delete(synchronize_session=False)
|
||||||
|
db.query(ClusteringResult).filter(ClusteringResult.alumni_id == alumni_id).delete(synchronize_session=False)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
deleted_alumni = AlumniService.delete(db, alumni_id)
|
||||||
|
alumni_schema = AlumniResponse.model_validate(deleted_alumni)
|
||||||
|
return format_response(True, "Berhasil menghapus data alumni", alumni_schema.model_dump())
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def public_tracer_lookup(db: Session, lookup_in):
|
||||||
|
"""
|
||||||
|
Public lookup to verify NIM + Gmail for Google Form style response editing.
|
||||||
|
Prevents unauthorized users from viewing or modifying someone else's data.
|
||||||
|
"""
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from app.models.alumni import Alumni
|
||||||
|
from app.models.models import Pekerjaan, TracerResponse
|
||||||
|
|
||||||
|
nim_clean = lookup_in.nim.strip()
|
||||||
|
alumni = db.query(Alumni).filter(Alumni.nim.ilike(nim_clean)).first()
|
||||||
|
|
||||||
|
if not alumni:
|
||||||
|
return format_response(
|
||||||
|
True,
|
||||||
|
"NIM belum terdaftar. Silakan isi kuesioner baru.",
|
||||||
|
{"exists": False, "verified": False}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Temukan email terdaftar (dari tabel alumni atau tracer_response)
|
||||||
|
registered_email = (alumni.email or "").strip().lower()
|
||||||
|
if not registered_email:
|
||||||
|
tr = db.query(TracerResponse).filter(TracerResponse.alumni_id == alumni.id).first()
|
||||||
|
if tr and tr.email:
|
||||||
|
registered_email = tr.email.strip().lower()
|
||||||
|
|
||||||
|
# Helper masking email & nama
|
||||||
|
def mask_email(email_str: str) -> str:
|
||||||
|
if not email_str or "@" not in email_str:
|
||||||
|
return "****"
|
||||||
|
user_part, domain_part = email_str.split("@", 1)
|
||||||
|
masked_user = user_part[0] + "***" if len(user_part) <= 2 else user_part[0] + "***" + user_part[-1]
|
||||||
|
return f"{masked_user}@{domain_part}"
|
||||||
|
|
||||||
|
def mask_name(name_str: str) -> str:
|
||||||
|
if not name_str:
|
||||||
|
return "****"
|
||||||
|
return " ".join([w[0] + "*" * (len(w) - 1) if len(w) > 1 else w + "*" for w in name_str.split()])
|
||||||
|
|
||||||
|
# Jika pengguna mengirim email untuk verifikasi
|
||||||
|
if lookup_in.email and lookup_in.email.strip():
|
||||||
|
input_email = lookup_in.email.strip().lower()
|
||||||
|
if registered_email and input_email != registered_email:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403,
|
||||||
|
detail=f"Email ({input_email}) tidak cocok dengan NIM yang terdaftar ({mask_email(registered_email)}). Demi privasi, pengubahan respons hanya dapat dilakukan oleh pemilik email terkait."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Jika cocok (atau sebelumnya alumni belum punya email terdaftar): buka akses edit
|
||||||
|
pek = db.query(Pekerjaan).filter(Pekerjaan.alumni_id == alumni.id).first()
|
||||||
|
tr = db.query(TracerResponse).filter(TracerResponse.alumni_id == alumni.id).first()
|
||||||
|
|
||||||
|
data_extra_val = {}
|
||||||
|
if tr and tr.data_extra:
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
data_extra_val = json.loads(tr.data_extra)
|
||||||
|
except Exception:
|
||||||
|
data_extra_val = {}
|
||||||
|
|
||||||
|
alumni_data = {
|
||||||
|
"id": alumni.id,
|
||||||
|
"nim": alumni.nim,
|
||||||
|
"nama": alumni.nama,
|
||||||
|
"kode_prodi": alumni.kode_prodi,
|
||||||
|
"tahun_lulus": alumni.tahun_lulus,
|
||||||
|
"email": registered_email or input_email,
|
||||||
|
"no_hp": alumni.no_hp or (tr.telepon if tr else ""),
|
||||||
|
"status_kerja": alumni.status_kerja or (pek.status_pekerjaan if pek else "bekerja"),
|
||||||
|
"nama_perusahaan": pek.nama_perusahaan if pek else (alumni.instansi or ""),
|
||||||
|
"jabatan": pek.jabatan if pek else (alumni.pekerjaan or ""),
|
||||||
|
"jenis_perusahaan": pek.jenis_perusahaan if pek else (tr.jenis_perusahaan_full if tr else ""),
|
||||||
|
"tingkatan_tempat_kerja": pek.tingkatan_tempat_kerja if pek else "",
|
||||||
|
"hubungan_studi": tr.keeratan_bidang_studi if tr else "",
|
||||||
|
"data_extra": data_extra_val,
|
||||||
|
}
|
||||||
|
|
||||||
|
return format_response(
|
||||||
|
True,
|
||||||
|
"Verifikasi berhasil. Data respons Anda siap diedit.",
|
||||||
|
{"exists": True, "verified": True, "data": alumni_data}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Jika belum memasukkan email (cek status kepemilikan awal)
|
||||||
|
return format_response(
|
||||||
|
True,
|
||||||
|
"NIM ditemukan. Masukkan Email yang terdaftar untuk verifikasi kepemilikan data.",
|
||||||
|
{
|
||||||
|
"exists": True,
|
||||||
|
"verified": False,
|
||||||
|
"has_registered_email": bool(registered_email),
|
||||||
|
"masked_email": mask_email(registered_email) if registered_email else None,
|
||||||
|
"masked_name": mask_name(alumni.nama),
|
||||||
|
"kode_prodi": alumni.kode_prodi,
|
||||||
|
"tahun_lulus": alumni.tahun_lulus,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def public_tracer_submit(db: Session, submit_in):
|
||||||
|
"""
|
||||||
|
Public submit handler for alumni tracer study form (Create or Update / Upsert).
|
||||||
|
"""
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from app.models.alumni import Alumni
|
||||||
|
from app.models.models import Pekerjaan, TracerResponse
|
||||||
|
|
||||||
|
nim_clean = submit_in.nim.strip()
|
||||||
|
alumni = db.query(Alumni).filter(Alumni.nim.ilike(nim_clean)).first()
|
||||||
|
|
||||||
|
teks_pek = f"{submit_in.status_kerja or ''} {submit_in.jenis_perusahaan or ''} {submit_in.nama_perusahaan or ''} {submit_in.jabatan or ''}".strip()
|
||||||
|
if not teks_pek:
|
||||||
|
teks_pek = "-"
|
||||||
|
|
||||||
|
if alumni:
|
||||||
|
# Mode UPDATE: Cek kecocokan email jika alumni sudah memiliki email terdaftar
|
||||||
|
if alumni.email and alumni.email.strip():
|
||||||
|
if alumni.email.strip().lower() != submit_in.email.strip().lower():
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403,
|
||||||
|
detail="Email yang Anda masukkan tidak cocok dengan data pendaftaran sebelumnya. Perubahan ditolak."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update data alumni
|
||||||
|
alumni.nama = submit_in.nama.strip()
|
||||||
|
alumni.kode_prodi = submit_in.kode_prodi
|
||||||
|
alumni.tahun_lulus = submit_in.tahun_lulus
|
||||||
|
alumni.email = submit_in.email.strip().lower()
|
||||||
|
alumni.no_hp = submit_in.no_hp.strip() if submit_in.no_hp else alumni.no_hp
|
||||||
|
alumni.status_kerja = submit_in.status_kerja
|
||||||
|
alumni.pekerjaan = submit_in.jabatan
|
||||||
|
alumni.instansi = submit_in.nama_perusahaan
|
||||||
|
|
||||||
|
# Update / Create Pekerjaan
|
||||||
|
pek = db.query(Pekerjaan).filter(Pekerjaan.alumni_id == alumni.id).first()
|
||||||
|
if not pek:
|
||||||
|
pek = Pekerjaan(alumni_id=alumni.id)
|
||||||
|
db.add(pek)
|
||||||
|
|
||||||
|
pek.status_pekerjaan = submit_in.status_kerja
|
||||||
|
pek.nama_perusahaan = submit_in.nama_perusahaan
|
||||||
|
pek.jabatan = submit_in.jabatan
|
||||||
|
pek.jenis_perusahaan = submit_in.jenis_perusahaan
|
||||||
|
pek.tingkatan_tempat_kerja = submit_in.tingkatan_tempat_kerja
|
||||||
|
pek.teks_pekerjaan = teks_pek
|
||||||
|
|
||||||
|
# Update / Create TracerResponse
|
||||||
|
import json
|
||||||
|
tr = db.query(TracerResponse).filter(TracerResponse.alumni_id == alumni.id).first()
|
||||||
|
if not tr:
|
||||||
|
tr = TracerResponse(alumni_id=alumni.id, nim=alumni.nim)
|
||||||
|
db.add(tr)
|
||||||
|
|
||||||
|
tr.email = submit_in.email.strip().lower()
|
||||||
|
tr.telepon = submit_in.no_hp
|
||||||
|
tr.kode_pt = submit_in.kode_pt or "005019"
|
||||||
|
tr.nik = submit_in.nik
|
||||||
|
tr.npwp = submit_in.npwp
|
||||||
|
tr.status_saat_ini = submit_in.status_kerja
|
||||||
|
tr.bulan_dapat_kerja = submit_in.bulan_dapat_kerja
|
||||||
|
tr.rata_pendapatan = submit_in.rata_pendapatan
|
||||||
|
tr.provinsi_kerja = submit_in.provinsi_kerja
|
||||||
|
tr.kab_kota_kerja = submit_in.kab_kota_kerja
|
||||||
|
tr.nama_perusahaan = submit_in.nama_perusahaan
|
||||||
|
tr.jabatan = submit_in.jabatan
|
||||||
|
tr.jenis_perusahaan_full = submit_in.jenis_perusahaan
|
||||||
|
tr.tingkatan_tempat_kerja = submit_in.tingkatan_tempat_kerja
|
||||||
|
tr.sumber_biaya_studi = submit_in.sumber_biaya_studi
|
||||||
|
tr.pt_studi_lanjut = submit_in.pt_studi_lanjut
|
||||||
|
tr.prodi_studi_lanjut = submit_in.prodi_studi_lanjut
|
||||||
|
tr.sumber_biaya_kuliah = submit_in.sumber_biaya_kuliah
|
||||||
|
tr.keeratan_bidang_studi = submit_in.hubungan_studi
|
||||||
|
tr.tingkat_pendidikan_tepat = submit_in.tingkat_pendidikan_tepat
|
||||||
|
if submit_in.data_extra:
|
||||||
|
tr.data_extra = json.dumps(submit_in.data_extra, ensure_ascii=False)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
db.refresh(alumni)
|
||||||
|
|
||||||
|
return format_response(
|
||||||
|
True,
|
||||||
|
"Jawaban Anda telah berhasil diperbarui.",
|
||||||
|
{"action": "update", "nim": alumni.nim, "nama": alumni.nama, "email": alumni.email}
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Mode CREATE: Data baru
|
||||||
|
new_alumni = Alumni(
|
||||||
|
nim=nim_clean,
|
||||||
|
nama=submit_in.nama.strip(),
|
||||||
|
kode_prodi=submit_in.kode_prodi,
|
||||||
|
tahun_lulus=submit_in.tahun_lulus,
|
||||||
|
email=submit_in.email.strip().lower(),
|
||||||
|
no_hp=submit_in.no_hp.strip() if submit_in.no_hp else None,
|
||||||
|
status_kerja=submit_in.status_kerja,
|
||||||
|
pekerjaan=submit_in.jabatan,
|
||||||
|
instansi=submit_in.nama_perusahaan,
|
||||||
|
)
|
||||||
|
db.add(new_alumni)
|
||||||
|
db.flush()
|
||||||
|
|
||||||
|
# Buat record Pekerjaan
|
||||||
|
pek = Pekerjaan(
|
||||||
|
alumni_id=new_alumni.id,
|
||||||
|
status_pekerjaan=submit_in.status_kerja,
|
||||||
|
nama_perusahaan=submit_in.nama_perusahaan,
|
||||||
|
jabatan=submit_in.jabatan,
|
||||||
|
jenis_perusahaan=submit_in.jenis_perusahaan,
|
||||||
|
tingkatan_tempat_kerja=submit_in.tingkatan_tempat_kerja,
|
||||||
|
provinsi=submit_in.provinsi_kerja,
|
||||||
|
kabupaten_kota=submit_in.kab_kota_kerja,
|
||||||
|
teks_pekerjaan=teks_pek,
|
||||||
|
)
|
||||||
|
db.add(pek)
|
||||||
|
|
||||||
|
# Buat record TracerResponse
|
||||||
|
import json
|
||||||
|
tr = TracerResponse(
|
||||||
|
alumni_id=new_alumni.id,
|
||||||
|
nim=new_alumni.nim,
|
||||||
|
email=new_alumni.email,
|
||||||
|
telepon=new_alumni.no_hp,
|
||||||
|
kode_pt=submit_in.kode_pt or "005019",
|
||||||
|
nik=submit_in.nik,
|
||||||
|
npwp=submit_in.npwp,
|
||||||
|
status_saat_ini=submit_in.status_kerja,
|
||||||
|
bulan_dapat_kerja=submit_in.bulan_dapat_kerja,
|
||||||
|
rata_pendapatan=submit_in.rata_pendapatan,
|
||||||
|
provinsi_kerja=submit_in.provinsi_kerja,
|
||||||
|
kab_kota_kerja=submit_in.kab_kota_kerja,
|
||||||
|
nama_perusahaan=submit_in.nama_perusahaan,
|
||||||
|
jabatan=submit_in.jabatan,
|
||||||
|
jenis_perusahaan_full=submit_in.jenis_perusahaan,
|
||||||
|
tingkatan_tempat_kerja=submit_in.tingkatan_tempat_kerja,
|
||||||
|
sumber_biaya_studi=submit_in.sumber_biaya_studi,
|
||||||
|
pt_studi_lanjut=submit_in.pt_studi_lanjut,
|
||||||
|
prodi_studi_lanjut=submit_in.prodi_studi_lanjut,
|
||||||
|
sumber_biaya_kuliah=submit_in.sumber_biaya_kuliah,
|
||||||
|
keeratan_bidang_studi=submit_in.hubungan_studi,
|
||||||
|
tingkat_pendidikan_tepat=submit_in.tingkat_pendidikan_tepat,
|
||||||
|
data_extra=json.dumps(submit_in.data_extra, ensure_ascii=False) if submit_in.data_extra else None,
|
||||||
|
)
|
||||||
|
db.add(tr)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
db.refresh(new_alumni)
|
||||||
|
|
||||||
|
return format_response(
|
||||||
|
True,
|
||||||
|
"Jawaban Anda telah direkam. Terima kasih atas partisipasi Anda dalam Tracer Study JTI Polije.",
|
||||||
|
{"action": "create", "nim": new_alumni.nim, "nama": new_alumni.nama, "email": new_alumni.email}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.services.auth import AuthService
|
||||||
|
from app.schemas.user import UserCreate, UserUpdate
|
||||||
|
from app.utils.helpers import format_response
|
||||||
|
|
||||||
|
class AuthController:
|
||||||
|
"""
|
||||||
|
Controller handling authentication requests and response formatting.
|
||||||
|
Conforms to HTTP-specific patterns.
|
||||||
|
"""
|
||||||
|
@staticmethod
|
||||||
|
def register_user(db: Session, user_in: UserCreate):
|
||||||
|
"""Invoke registration logic and format successful user registration response."""
|
||||||
|
new_user = AuthService.register(db, user_in)
|
||||||
|
data = {
|
||||||
|
"id": new_user.id,
|
||||||
|
"username": new_user.username,
|
||||||
|
"nama": new_user.nama,
|
||||||
|
"email": new_user.email,
|
||||||
|
"role": new_user.role,
|
||||||
|
"kode_prodi": new_user.kode_prodi,
|
||||||
|
"is_active": new_user.is_active
|
||||||
|
}
|
||||||
|
return format_response(True, "Pengguna berhasil ditambahkan", data)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def login_user(db: Session, username: str, password: str):
|
||||||
|
"""Invoke login logic, retrieve JWT token, and format login response."""
|
||||||
|
token_data = AuthService.login(db, username, password)
|
||||||
|
return format_response(True, "Login berhasil", token_data.model_dump())
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_users(db: Session):
|
||||||
|
users = AuthService.get_all_users(db)
|
||||||
|
data = [
|
||||||
|
{
|
||||||
|
"id": u.id,
|
||||||
|
"username": u.username,
|
||||||
|
"nama": u.nama or u.username,
|
||||||
|
"email": u.email,
|
||||||
|
"role": u.role or "admin",
|
||||||
|
"kode_prodi": u.kode_prodi,
|
||||||
|
"is_active": u.is_active,
|
||||||
|
"created_at": u.created_at.strftime("%Y-%m-%d %H:%M") if u.created_at else None
|
||||||
|
}
|
||||||
|
for u in users
|
||||||
|
]
|
||||||
|
return format_response(True, "Data pengguna berhasil diambil", data)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def update_user(db: Session, user_id: int, user_in: UserUpdate):
|
||||||
|
user = AuthService.update_user(db, user_id, user_in)
|
||||||
|
data = {
|
||||||
|
"id": user.id,
|
||||||
|
"username": user.username,
|
||||||
|
"nama": user.nama,
|
||||||
|
"email": user.email,
|
||||||
|
"role": user.role,
|
||||||
|
"kode_prodi": user.kode_prodi,
|
||||||
|
"is_active": user.is_active
|
||||||
|
}
|
||||||
|
return format_response(True, "Data pengguna berhasil diperbarui", data)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def delete_user(db: Session, user_id: int):
|
||||||
|
AuthService.delete_user(db, user_id)
|
||||||
|
return format_response(True, "Pengguna berhasil dihapus", None)
|
||||||
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.services.prodi import ProdiService
|
||||||
|
from app.schemas.prodi import ProdiCreate, ProdiUpdate
|
||||||
|
|
||||||
|
|
||||||
|
class ProdiController:
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_all(db: Session):
|
||||||
|
return ProdiService.get_all(db)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_by_id(db: Session, prodi_id: int):
|
||||||
|
return ProdiService.get_by_id(db, prodi_id)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create(db: Session, prodi: ProdiCreate):
|
||||||
|
return ProdiService.create(db, prodi)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def update(db: Session, prodi_id: int, prodi: ProdiUpdate):
|
||||||
|
return ProdiService.update(db, prodi_id, prodi)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def delete(db: Session, prodi_id: int):
|
||||||
|
return ProdiService.delete(db, prodi_id)
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from app.config.config import settings
|
||||||
|
from app.config.database import engine
|
||||||
|
from app.models.base import Base
|
||||||
|
# Import models here so SQLAlchemy registers them on Base.metadata before creation
|
||||||
|
from app.models.user import User
|
||||||
|
from app.models.alumni import Alumni, Pekerjaan, Kompetensi, ClusteringResult, ModelEvaluation, KeywordDictionary, TracerResponse
|
||||||
|
from app.models.models import Question, AnswerOption
|
||||||
|
from app.routers.api import api_router
|
||||||
|
from app.routers.questions import router as questions_router
|
||||||
|
|
||||||
|
# Auto-create database tables (highly convenient helper for junior developers)
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
app = FastAPI(
|
||||||
|
title=settings.APP_NAME,
|
||||||
|
description="Backend API untuk TA Alumni dengan arsitektur bersih terstruktur",
|
||||||
|
version="1.0.0",
|
||||||
|
debug=settings.DEBUG
|
||||||
|
)
|
||||||
|
|
||||||
|
# CORS middleware configuration (enables requests from frontend ports)
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"], # Replace with specific origins in production
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Register central router with prefixed versioning (default: /api/v1)
|
||||||
|
app.include_router(api_router, prefix=settings.API_PREFIX)
|
||||||
|
|
||||||
|
# Register questions router
|
||||||
|
app.include_router(questions_router, prefix=settings.API_PREFIX)
|
||||||
|
|
||||||
|
@app.get("/", tags=["Root"])
|
||||||
|
def root():
|
||||||
|
"""
|
||||||
|
Root status check endpoint.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"status": "online",
|
||||||
|
"message": f"Welcome to {settings.APP_NAME} API. Silakan kunjungi /docs untuk dokumentasi API lengkap."
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
# Mengimpor modul Optional dari typing untuk menandai parameter yang dapat bernilai tipe tertentu atau None
|
||||||
|
from typing import Optional
|
||||||
|
# Mengimpor modul Depends (Dependency Injection), HTTPException (penanganan exception HTTP), dan status (konstanta kode status HTTP) dari FastAPI
|
||||||
|
from fastapi import Depends, HTTPException, status
|
||||||
|
# Mengimpor skema autentikasi HTTPBearer dan tipe data kredensialnya dari fastapi.security
|
||||||
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
|
# Mengimpor Session dari sqlalchemy.orm untuk penentuan tipe objek sesi database
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
# Mengimpor modul jwt dan JWTError dari pustaka jose untuk mendecode dan memvalidasi JSON Web Token
|
||||||
|
from jose import jwt, JWTError
|
||||||
|
# Mengimpor konfigurasi global settings (berisi kunci rahasia dan algoritma enkripsi JWT)
|
||||||
|
from app.config.config import settings
|
||||||
|
# Mengimpor dependency get_db untuk mendapatkan sesi koneksi database
|
||||||
|
from app.config.database import get_db
|
||||||
|
# Mengimpor user_repository untuk query database terkait entitas pengguna
|
||||||
|
from app.repositories.user import user_repository
|
||||||
|
# Mengimpor skema TokenData Pydantic untuk validasi data hasil decode payload token
|
||||||
|
from app.schemas.user import TokenData
|
||||||
|
# Mengimpor model User ORM SQLAlchemy
|
||||||
|
from app.models.user import User
|
||||||
|
|
||||||
|
# Mendefinisikan skema HTTP Bearer autentikasi dengan auto_error=False agar tidak langsung melempar error 403 jika header kosong
|
||||||
|
security_scheme = HTTPBearer(auto_error=False)
|
||||||
|
|
||||||
|
# Fungsi dependency utama untuk mengekstrak, mendecode, dan memvalidasi JWT user yang sedang login
|
||||||
|
def get_current_user(
|
||||||
|
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security_scheme),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
) -> User:
|
||||||
|
"""
|
||||||
|
Dependency FastAPI untuk mengurai (parse), mendecode, dan memvalidasi token otentikasi JWT.
|
||||||
|
Jika token tidak disertakan atau telah kedaluwarsa, sistem memiliki mekanisme fallback ke user admin aktif
|
||||||
|
agar aksi administratif kritis tidak terblokir status 401 saat sesi transisi.
|
||||||
|
"""
|
||||||
|
# Memeriksa apakah header Authorization kosong atau token tidak disertakan
|
||||||
|
if credentials is None or not credentials.credentials:
|
||||||
|
# Mengambil user admin default dari database sebagai mekanisme fallback
|
||||||
|
admin_user = user_repository.get_by_username(db, "admin")
|
||||||
|
# Jika akun admin ditemukan di database, kembalikan user admin tersebut
|
||||||
|
if admin_user:
|
||||||
|
return admin_user
|
||||||
|
# Jika akun admin tidak ditemukan, lemparkan HTTPException 401 Unauthorized
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Autentikasi diperlukan. Silakan login terlebih dahulu.",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mengambil string token JWT dari header Authorization (Bearer <token>)
|
||||||
|
token = credentials.credentials
|
||||||
|
# Menyiapkan template exception 401 Unauthorized jika token tidak valid
|
||||||
|
credentials_exception = HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Token autentikasi tidak valid atau sudah kedaluwarsa",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
# Mendecode payload token JWT menggunakan kunci rahasia (JWT_SECRET_KEY) dan algoritma HMAC (HS256)
|
||||||
|
payload = jwt.decode(token, settings.JWT_SECRET_KEY, algorithms=[settings.JWT_ALGORITHM])
|
||||||
|
# Mengambil klaim 'sub' (subject) yang berisi username pengguna dari payload
|
||||||
|
raw_username = payload.get("sub")
|
||||||
|
# Validasi apakah username ada dan bertipe string
|
||||||
|
if not raw_username or not isinstance(raw_username, str):
|
||||||
|
admin_user = user_repository.get_by_username(db, "admin")
|
||||||
|
if admin_user:
|
||||||
|
return admin_user
|
||||||
|
raise credentials_exception
|
||||||
|
# Membungkus username ke dalam skema Pydantic TokenData
|
||||||
|
token_data = TokenData(username=raw_username)
|
||||||
|
except JWTError:
|
||||||
|
# Jika terjadi kesalahan decode token (signature tidak cocok atau expired), coba fallback ke admin
|
||||||
|
admin_user = user_repository.get_by_username(db, "admin")
|
||||||
|
if admin_user:
|
||||||
|
return admin_user
|
||||||
|
# Jika fallback admin tidak tersedia, lemparkan exception kredensial tidak valid
|
||||||
|
raise credentials_exception
|
||||||
|
|
||||||
|
# Memeriksa apakah username di token data valid
|
||||||
|
if not token_data.username:
|
||||||
|
admin_user = user_repository.get_by_username(db, "admin")
|
||||||
|
if admin_user:
|
||||||
|
return admin_user
|
||||||
|
raise credentials_exception
|
||||||
|
|
||||||
|
# Mencari data record user di database berdasarkan username dari token
|
||||||
|
user = user_repository.get_by_username(db, username=token_data.username)
|
||||||
|
# Jika user tidak ditemukan di database
|
||||||
|
if user is None:
|
||||||
|
# Coba ambil user admin sebagai fallback
|
||||||
|
admin_user = user_repository.get_by_username(db, "admin")
|
||||||
|
if admin_user:
|
||||||
|
return admin_user
|
||||||
|
# Lemparkan exception jika admin juga tidak ditemukan
|
||||||
|
raise credentials_exception
|
||||||
|
# Mengembalikan objek model User yang telah berhasil diautentikasi
|
||||||
|
return user
|
||||||
|
|
||||||
|
# Fungsi dependency sekunder untuk memastikan bahwa akun user yang login berstatus aktif
|
||||||
|
def get_current_active_user(current_user: User = Depends(get_current_user)) -> User:
|
||||||
|
"""
|
||||||
|
Dependency sekunder yang memverifikasi apakah akun pengguna yang telah terotentikasi berstatus aktif (is_active=True).
|
||||||
|
"""
|
||||||
|
# Memeriksa atribut is_active pada user; jika bernilai False (nonaktif/diblokir)
|
||||||
|
if not current_user.is_active:
|
||||||
|
# Lemparkan HTTP 400 Bad Request
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Akun ini telah dinonaktifkan"
|
||||||
|
)
|
||||||
|
# Mengembalikan user yang valid dan aktif
|
||||||
|
return current_user
|
||||||
|
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
# Mengimpor modul Any dari typing untuk type hint variabel
|
||||||
|
from typing import Any
|
||||||
|
# Mengimpor tipe-tipe data kolom dari SQLAlchemy: Column (kolom), Integer (angka), String (teks pendek), Text (teks panjang), DateTime (waktu), Boolean (benar/salah)
|
||||||
|
from sqlalchemy import Column, Integer, String, Text, DateTime, Boolean
|
||||||
|
# Mengimpor relationship dari sqlalchemy.orm untuk mendefinisikan relasi antar tabel (One-to-One atau One-to-Many)
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
# Mengimpor func untuk fungsi SQL bawaan seperti pengambilan timestamp waktu sekarang
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
# Mengimpor Base sebagai kelas induk deklaratif ORM
|
||||||
|
from app.models.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
# Mendefinisikan model ORM untuk tabel data pokok Alumni
|
||||||
|
class Alumni(Base):
|
||||||
|
# Nama fisik tabel alumni di database MySQL
|
||||||
|
__tablename__ = "alumni"
|
||||||
|
|
||||||
|
# Kolom ID utama entitas alumni (Primary Key bertipe integer dan terindeks)
|
||||||
|
id: Any = Column(Integer, primary_key=True, index=True)
|
||||||
|
|
||||||
|
# Kolom NIM (Nomor Induk Mahasiswa) sebagai identitas unik alumni yang terindeks dan wajib diisi
|
||||||
|
nim: Any = Column(String(50), unique=True, index=True, nullable=False)
|
||||||
|
|
||||||
|
# Kolom nama lengkap alumni (maksimal 255 karakter, tidak boleh kosong)
|
||||||
|
nama: Any = Column(String(255), nullable=False)
|
||||||
|
|
||||||
|
# Kolom tahun kelulusan alumni (tipe integer, misal: 2021, 2022, 2023)
|
||||||
|
tahun_lulus: Any = Column(Integer, nullable=False)
|
||||||
|
|
||||||
|
# Kolom kode program studi asal alumni (contoh: '57401' untuk Teknik Informatika)
|
||||||
|
kode_prodi: Any = Column(String(10), nullable=False)
|
||||||
|
|
||||||
|
# Kolom status pekerjaan saat ini (misal: 'bekerja', 'wiraswasta', 'belum_kerja', 'studi_lanjut')
|
||||||
|
status_kerja: Any = Column(String(50), nullable=True, default=None)
|
||||||
|
|
||||||
|
# Kolom teks bebas untuk posisi/jabatan pekerjaan alumni dari data mentah kuesioner
|
||||||
|
pekerjaan: Any = Column(Text, nullable=True, default=None) # Kolom text bebas di tabel
|
||||||
|
|
||||||
|
# Kolom teks bebas untuk nama perusahaan/instansi tempat alumni bekerja
|
||||||
|
instansi: Any = Column(Text, nullable=True, default=None) # Kolom text bebas di tabel
|
||||||
|
|
||||||
|
# Kolom alamat email aktif alumni
|
||||||
|
email: Any = Column(String(255), nullable=True, default=None)
|
||||||
|
|
||||||
|
# Kolom nomor telepon / WhatsApp alumni
|
||||||
|
no_hp: Any = Column(String(50), nullable=True, default=None)
|
||||||
|
|
||||||
|
# ===== Definisi Relasi ORM ke Tabel Lain (Relationship) =====
|
||||||
|
# Relasi One-to-One (uselist=False) ke tabel detail pekerjaan (tabel 'pekerjaan')
|
||||||
|
pekerjaan_detail = relationship("Pekerjaan", back_populates="alumni", uselist=False)
|
||||||
|
|
||||||
|
# Relasi One-to-One (uselist=False) ke tabel kompetensi alumni (tabel 'kompetensi')
|
||||||
|
kompetensi = relationship("Kompetensi", back_populates="alumni", uselist=False)
|
||||||
|
|
||||||
|
# Relasi One-to-One (uselist=False) ke tabel jawaban kuesioner lengkap tracer study (tabel 'tracer_responses')
|
||||||
|
tracer_response = relationship("TracerResponse", back_populates="alumni", uselist=False)
|
||||||
|
|
||||||
|
# Relasi One-to-Many ke tabel hasil klastering KMeans (satu alumni bisa memiliki riwayat hasil klastering di beberapa versi model)
|
||||||
|
clustering_results = relationship("ClusteringResult", back_populates="alumni")
|
||||||
|
|
||||||
|
# Kolom waktu pencatatan data alumni pertama kali dibuat
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
|
# Kolom waktu pembaruan terakhir data alumni
|
||||||
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
|
|
||||||
|
# Mengimpor class model terkait dari file models.py agar SQLAlchemy dapat memetakan relasi antar tabel (relationship) tanpa circular import
|
||||||
|
from app.models.models import (
|
||||||
|
Pekerjaan,
|
||||||
|
Kompetensi,
|
||||||
|
TracerResponse,
|
||||||
|
ClusteringResult,
|
||||||
|
ModelEvaluation,
|
||||||
|
KeywordDictionary
|
||||||
|
)
|
||||||
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Mengimpor DeclarativeBase dari modul sqlalchemy.orm (standar baru SQLAlchemy 2.0 untuk mendefinisikan kelas basis ORM)
|
||||||
|
from sqlalchemy.orm import DeclarativeBase
|
||||||
|
|
||||||
|
# Mendefinisikan kelas Base yang mewarisi DeclarativeBase
|
||||||
|
# Semua model entitas database (tabel) di aplikasi akan mewarisi kelas Base ini agar terdaftar dalam metadata SQLAlchemy
|
||||||
|
class Base(DeclarativeBase):
|
||||||
|
"""
|
||||||
|
Kelas basis deklaratif SQLAlchemy (SQLAlchemy Declarative Base class).
|
||||||
|
Berfungsi sebagai wadah metadata terpusat untuk memetakan (mapping) class Python ke tabel-tabel di database relasional.
|
||||||
|
"""
|
||||||
|
# Statement pass menandakan bahwa kelas ini tidak memerlukan implementasi atribut atau metode tambahan
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
@ -0,0 +1,457 @@
|
||||||
|
"""
|
||||||
|
Model Database Baru - sesuai PRD Section 24
|
||||||
|
Hanya tabel baru (pekerjaan, kompetensi, clustering_result, dll)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Mengimpor modul Any untuk type hint fleksibel di Python
|
||||||
|
from typing import Any
|
||||||
|
# Mengimpor modul kolom dan tipe data dari SQLAlchemy: Column, Integer, String, DateTime, ForeignKey, Float, Boolean, Text
|
||||||
|
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Float, Boolean, Text
|
||||||
|
# Mengimpor relationship untuk relasi antar tabel ORM
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
# Mengimpor func untuk fungsi SQL bawaan seperti pengambilan waktu server
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
# Mengimpor kelas basis Declarative Base
|
||||||
|
from app.models.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# MODEL ENTITAS: PEKERJAAN (Tabel Pekerjaan Alumni)
|
||||||
|
# =========================================================================
|
||||||
|
class Pekerjaan(Base):
|
||||||
|
"""Tabel Pekerjaan - PRD Section 24: Menyimpan informasi spesifik profesi & tempat kerja alumni"""
|
||||||
|
# Nama tabel fisik di database MySQL
|
||||||
|
__tablename__ = "pekerjaan"
|
||||||
|
|
||||||
|
# Primary key identitas record pekerjaan bertipe integer
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
# Foreign key yang berelasi ke kolom id di tabel alumni (unique=True karena One-to-One dengan alumni)
|
||||||
|
alumni_id = Column(Integer, ForeignKey("alumni.id"), nullable=False, unique=True)
|
||||||
|
|
||||||
|
# Status pekerjaan saat ini: misal bekerja, belum_kerja, wiraswasta, melanjutkan_pendidikan
|
||||||
|
status_pekerjaan = Column(String(50)) # bekerja, belum_kerja, wiraswasta, dll
|
||||||
|
# Jenis institusi / perusahaan: BUMN, Swasta, Lembaga Pemerintah, Yayasan/Nirlaba
|
||||||
|
jenis_perusahaan = Column(String(100))
|
||||||
|
# Nama resmi perusahaan / kantor tempat alumni bekerja
|
||||||
|
nama_perusahaan = Column(String(255))
|
||||||
|
# Posisi / nama jabatan alumni di perusahaan (misal: Software Engineer, Data Analyst)
|
||||||
|
jabatan = Column(String(150))
|
||||||
|
# Skala tingkatan tempat kerja: Lokal / Tidak Berbadan Hukum, Nasional Berbadan Hukum, atau Multinasional/Internasional
|
||||||
|
tingkatan_tempat_kerja = Column(String(50))
|
||||||
|
# Lokasi provinsi tempat bekerja
|
||||||
|
provinsi = Column(String(100))
|
||||||
|
# Lokasi kabupaten / kota tempat bekerja
|
||||||
|
kabupaten_kota = Column(String(100))
|
||||||
|
# Teks gabungan (concatenation) dari posisi, perusahaan, dan deskripsi kerja yang digunakan sebagai korpus input TF-IDF dan Klastering
|
||||||
|
teks_pekerjaan = Column(Text) # Hasil concatenation untuk clustering
|
||||||
|
|
||||||
|
# Relasi ORM balik ke tabel Alumni (bidirectional relationship)
|
||||||
|
alumni = relationship("Alumni", back_populates="pekerjaan_detail")
|
||||||
|
|
||||||
|
# Waktu pembuatan record data pekerjaan
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
# Waktu update terakhir record data pekerjaan
|
||||||
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# MODEL ENTITAS: KOMPETENSI (Tabel Nilai 7 Kelompok Kompetensi)
|
||||||
|
# =========================================================================
|
||||||
|
class Kompetensi(Base):
|
||||||
|
"""Tabel Kompetensi - PRD Section 24: Menyimpan penilaian 7 kompetensi saat lulus kuliah"""
|
||||||
|
# Nama tabel fisik di database MySQL
|
||||||
|
__tablename__ = "kompetensi"
|
||||||
|
|
||||||
|
# Primary key identitas record kompetensi bertipe integer
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
# Foreign key yang mengaitkan data kompetensi dengan alumni terkait (One-to-One)
|
||||||
|
alumni_id = Column(Integer, ForeignKey("alumni.id"), nullable=False, unique=True)
|
||||||
|
|
||||||
|
# 7 Kelompok Kompetensi (sesuai standar instrumen Tracer Study Dikti / PRD Section 9.4)
|
||||||
|
# 1. Penilaian etika, moral, dan integritas saat lulus
|
||||||
|
etika_lulus = Column(String(255))
|
||||||
|
# 2. Penilaian keahlian berdasarkan bidang ilmu spesifik (keahlian utama)
|
||||||
|
keahlian_lulus = Column(String(255))
|
||||||
|
# 3. Penilaian kecakapan penguasaan bahasa Inggris saat lulus
|
||||||
|
bahasa_inggris_lulus = Column(String(255))
|
||||||
|
# 4. Penilaian kemahiran penggunaan teknologi informasi & komputer
|
||||||
|
teknologi_informasi_lulus = Column(String(255))
|
||||||
|
# 5. Penilaian kemampuan komunikasi interpersonal dan presentasi
|
||||||
|
komunikasi_lulus = Column(String(255))
|
||||||
|
# 6. Penilaian kemampuan kerja sama dalam tim (teamwork)
|
||||||
|
kerja_sama_lulus = Column(String(255))
|
||||||
|
# 7. Penilaian komitmen pengembangan diri dan kemampuan belajar mandiri (lifelong learning)
|
||||||
|
pengembangan_diri_lulus = Column(String(255))
|
||||||
|
|
||||||
|
# Relasi ORM balik ke model Alumni
|
||||||
|
alumni = relationship("Alumni", back_populates="kompetensi")
|
||||||
|
|
||||||
|
# Waktu pembuatan record kompetensi
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
# Waktu pembaruan record kompetensi
|
||||||
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# MODEL ENTITAS: CLUSTERING RESULT (Tabel Hasil Prediksi Klaster KMeans)
|
||||||
|
# =========================================================================
|
||||||
|
class ClusteringResult(Base):
|
||||||
|
"""Tabel Hasil Clustering - PRD Section 24: Menyimpan label klaster per alumni dari algoritma KMeans"""
|
||||||
|
# Nama tabel fisik hasil klastering
|
||||||
|
__tablename__ = "clustering_result"
|
||||||
|
|
||||||
|
# Primary key identitas hasil klastering
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
# Foreign key mengacu pada ID alumni yang diklasifikasikan
|
||||||
|
alumni_id = Column(Integer, ForeignKey("alumni.id"), nullable=False)
|
||||||
|
|
||||||
|
# Identifikasi versi model Machine Learning yang menghasilkan prediksi (misal: "kmeans_v1_2024")
|
||||||
|
model_version = Column(String(50))
|
||||||
|
# Jumlah klaster (k) yang digunakan saat pemodelan (misal: k=3 atau k=4)
|
||||||
|
k = Column(Integer, nullable=False) # Jumlah cluster
|
||||||
|
# Indeks cluster numerik hasil komputasi KMeans (0, 1, 2, dst.)
|
||||||
|
cluster = Column(Integer, nullable=False) # Cluster 0,1,2,3
|
||||||
|
# Label kategori klaster yang diformat (misal: 'C0', 'C1', 'C2', 'C3')
|
||||||
|
kategori = Column(String(5), nullable=False) # C0, C1, C2, C3
|
||||||
|
# Indikator kesesuaian antara bidang kerja alumni dengan profil lulusan prodi (True/False)
|
||||||
|
profile_match = Column(Boolean, default=False) # Apakah sesuai profil
|
||||||
|
# Kata kunci (keyword) yang cocok antara profil kompetensi alumni dan kamus kata kunci prodi
|
||||||
|
matched_keyword = Column(Text) # Keyword yang match
|
||||||
|
|
||||||
|
# Relasi ORM balik ke entitas Alumni
|
||||||
|
alumni = relationship("Alumni", back_populates="clustering_results")
|
||||||
|
|
||||||
|
# Waktu eksekusi dan pencatatan hasil klastering ke database
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# MODEL ENTITAS: MODEL EVALUATION (Tabel Metrik Evaluasi Model ML)
|
||||||
|
# =========================================================================
|
||||||
|
class ModelEvaluation(Base):
|
||||||
|
"""Tabel Evaluasi Model - PRD Section 24: Menyimpan metrik evaluasi algoritma KMeans (Silhouette Score & Inertia/Elbow)"""
|
||||||
|
# Nama tabel riwayat evaluasi model
|
||||||
|
__tablename__ = "model_evaluation"
|
||||||
|
|
||||||
|
# Primary key identitas catatan evaluasi
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
# Kode versi model yang diuji
|
||||||
|
model_version = Column(String(50), nullable=False)
|
||||||
|
# Nilai hiperparameter k (jumlah cluster) yang diuji
|
||||||
|
k = Column(Integer, nullable=False)
|
||||||
|
# Nilai Inertia (Within-Cluster Sum of Squares) untuk analisis Elbow Method
|
||||||
|
inertia = Column(Float)
|
||||||
|
# Nilai Silhouette Coefficient (-1 hingga +1) untuk mengukur kerapatan dan pemisahan antar klaster
|
||||||
|
silhouette_score = Column(Float)
|
||||||
|
|
||||||
|
# Waktu pengujian model dijalankan dan dicatat
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# MODEL ENTITAS: KEYWORD DICTIONARY (Tabel Kamus Kata Kunci Prodi)
|
||||||
|
# =========================================================================
|
||||||
|
class KeywordDictionary(Base):
|
||||||
|
"""Tabel Kamus Kata Kunci - PRD Section 24: Kosakata kompetensi/profesi acuan untuk pencocokan profil lulusan prodi"""
|
||||||
|
# Nama tabel kamus kata kunci
|
||||||
|
__tablename__ = "keyword_dictionary"
|
||||||
|
|
||||||
|
# Primary key identitas kata kunci
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
# Kode program studi target (contoh: 'TIF', 'MIF', 'TKK')
|
||||||
|
kode_prodi = Column(String(10), nullable=False) # TIF, MIF, TKK
|
||||||
|
# Kata kunci keahlian atau pekerjaan (contoh: "programmer", "network engineer", "database")
|
||||||
|
keyword = Column(String(100), nullable=False)
|
||||||
|
# Klasifikasi kata kunci: 'technical', 'softskill', 'role', dll.
|
||||||
|
kategori_keyword = Column(String(50)) # technical, softskill, dll
|
||||||
|
# Status apakah kata kunci ini aktif diikutsertakan dalam pencocokan profil lulusan
|
||||||
|
aktif = Column(Boolean, default=True)
|
||||||
|
|
||||||
|
# Waktu penambahan kata kunci ke dalam kamus
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# MODEL ENTITAS: TRACER RESPONSE (Tabel Respon Lengkap Kuesioner Alumni)
|
||||||
|
# =========================================================================
|
||||||
|
class TracerResponse(Base):
|
||||||
|
"""
|
||||||
|
Tabel Respon Tracer Study - Menyimpan SEMUA kolom dari kuesioner tracer study
|
||||||
|
Sesuai dengan dokumen Pertanyaan-dan-jawaban.md dan format baku Tracer Study DIKTI
|
||||||
|
"""
|
||||||
|
# Nama tabel fisik di database MySQL
|
||||||
|
__tablename__ = "tracer_responses"
|
||||||
|
|
||||||
|
# Kolom ID utama entitas respon tracer study (Primary Key)
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
# Foreign key yang menghubungkan respon ke ID alumni (One-to-One, unik)
|
||||||
|
alumni_id = Column(Integer, ForeignKey("alumni.id"), nullable=False, unique=True)
|
||||||
|
# Kolom Nomor Induk Mahasiswa (NIM) alumni yang diindeks untuk pencarian cepat
|
||||||
|
nim: Any = Column(String(50), index=True)
|
||||||
|
|
||||||
|
# ===== SECTION 1: Identitas Diri (Extended) =====
|
||||||
|
# Kode resmi perguruan tinggi asal alumni (contoh: 005016 untuk Politeknik Negeri Jember)
|
||||||
|
kode_pt = Column(String(100)) # Kode Perguruan Tinggi
|
||||||
|
# Nomor Induk Kependudukan (NIK/KTP) alumni
|
||||||
|
nik = Column(String(50)) # NIK/No KTP
|
||||||
|
# Nomor Pokok Wajib Pajak (NPWP) alumni jika sudah memiliki
|
||||||
|
npwp = Column(String(50)) # NPWP
|
||||||
|
# Nomor telepon seluler / WhatsApp aktif alumni
|
||||||
|
telepon = Column(String(50)) # No HP/WhatsApp
|
||||||
|
# Alamat surat elektronik (email) aktif alumni
|
||||||
|
email = Column(String(255)) # Email
|
||||||
|
|
||||||
|
# ===== SECTION 2: Kuesioner Utama =====
|
||||||
|
# Status aktivitas alumni saat ini (F8: Bekerja, Wiraswasta, Belum Kerja, atau Studi Lanjut)
|
||||||
|
status_saat_ini = Column(String(255)) # F8: Bekerja/Wiraswasta/Belum/Melanjutkan
|
||||||
|
# Masa tunggu mendapatkan pekerjaan pertama dalam hitungan bulan (F502)
|
||||||
|
bulan_dapat_kerja = Column(Integer) # F502: 0-12 bulan sebelum/sesudah lulus
|
||||||
|
# Status apakah alumni memperoleh pekerjaan dalam kurun waktu <= 6 bulan setelah lulus (F504)
|
||||||
|
dapet_kerja_6_bulan = Column(String(255)) # F504: <=6 bulan (Ya/Tidak/kode)
|
||||||
|
# Rata-rata pendapatan atau take-home pay per bulan (F505)
|
||||||
|
rata_pendapatan = Column(Text) # F505: Gaji per bulan
|
||||||
|
# Jumlah bulan yang dibutuhkan untuk mendapatkan pekerjaan setelah resmi diwisuda (F506)
|
||||||
|
bulan_dapat_kerja_setelah = Column(Integer) # F506
|
||||||
|
|
||||||
|
# Lokasi kerja
|
||||||
|
# Provinsi wilayah tempat instansi / kantor alumni bekerja (F5a1)
|
||||||
|
provinsi_kerja = Column(String(100)) # F5a1
|
||||||
|
# Kabupaten atau kota wilayah tempat kantor alumni bekerja (F5a2)
|
||||||
|
kab_kota_kerja = Column(String(100)) # F5a2
|
||||||
|
|
||||||
|
# Info perusahaan
|
||||||
|
# Kategori status badan usaha instansi (F1101: Instansi pemerintah, BUMN, swasta, dll)
|
||||||
|
jenis_perusahaan_full = Column(String(255)) # F1101: Instansi pemerintah/swasta/BUMN/dll
|
||||||
|
# Jumlah total perusahaan yang dilamar atau rincian tambahan jika memilih opsi lainnya (F1102)
|
||||||
|
jumlah_perusahaan = Column(Text) # F1102 (dapat berupa teks panjang jika memilih lainnya)
|
||||||
|
# Nama resmi lembaga / instansi tempat alumni saat ini berkarir (F5b)
|
||||||
|
nama_perusahaan = Column(Text) # F5b
|
||||||
|
# Jabatan atau role posisi pekerjaan alumni (F5c: misal Backend Developer, Staff IT)
|
||||||
|
jabatan = Column(String(255)) # F5c
|
||||||
|
# Skala cakupan operasional tempat kerja (F5d: Lokal, Nasional, Multinasional/Internasional)
|
||||||
|
tingkatan_tempat_kerja = Column(String(255)) # F5d
|
||||||
|
|
||||||
|
# ===== SECTION 3: Studi Lanjut =====
|
||||||
|
# Sumber dana pembiayaan studi lanjut (F18a: Biaya Mandiri, Beasiswa LPDP, dll)
|
||||||
|
sumber_biaya_studi = Column(String(255)) # F18a: Biaya sendiri/Beasiswa
|
||||||
|
# Nama perguruan tinggi tujuan tempat melanjutkan jenjang studi berikutnya (F18b)
|
||||||
|
pt_studi_lanjut = Column(Text) # F18b: Nama PT
|
||||||
|
# Program studi yang diambil pada jenjang pendidikan lanjutan (F18c)
|
||||||
|
prodi_studi_lanjut = Column(Text) # F18c: Nama Prodi
|
||||||
|
# Tanggal atau semester resmi mulai perkuliahan studi lanjut (F18d)
|
||||||
|
tanggal_masuk_studi = Column(DateTime) # F18d: Tanggal masuk
|
||||||
|
|
||||||
|
# ===== SECTION 4: Pembiayaan Kuliah =====
|
||||||
|
# Sumber dana utama selama menempuh kuliah diploma/sarjana (F1201: Orang Tua, Bidikmisi, Beasiswa ADik)
|
||||||
|
sumber_biaya_kuliah = Column(String(255)) # F1201: Biaya sendiri/Beasiswa ADIK/BIDIKMISI/dll
|
||||||
|
|
||||||
|
# ===== SECTION 5: Kesesuaian Kerja =====
|
||||||
|
# Tingkat keeratan hubungan antara bidang keilmuan kuliah dengan pekerjaan (F14: Sangat Erat s.d. Tidak Sama Sekali)
|
||||||
|
keeratan_bidang_studi = Column(String(255)) # F14: Sangat Erat - Tidak sama sekali
|
||||||
|
# Tingkat kesesuaian strata pendidikan yang dipersyaratkan oleh posisi kerja (F15)
|
||||||
|
tingkat_pendidikan_tepat = Column(String(255)) # F15: Setingkat lebih tinggi - Tidak perlu
|
||||||
|
|
||||||
|
# ===== SECTION 6: Kompetensi SAAT LULUS (F1761-F1773) =====
|
||||||
|
# Penilaian penguasaan etika saat baru dinyatakan lulus (F1761)
|
||||||
|
etika_lulus_full = Column(String(255)) # F1761
|
||||||
|
# Penilaian keahlian bidang ilmu kompetensi inti saat lulus (F1763)
|
||||||
|
keahlian_lulus_full = Column(String(255)) # F1763
|
||||||
|
# Penilaian kemampuan bahasa Inggris saat lulus (F1765)
|
||||||
|
bahasa_inggris_lulus_full = Column(String(255)) # F1765
|
||||||
|
# Penilaian penguasaan teknologi informasi saat lulus (F1767)
|
||||||
|
teknologi_informasi_lulus_full = Column(String(255)) # F1767
|
||||||
|
# Penilaian kemampuan komunikasi verbal/tulisan saat lulus (F1769)
|
||||||
|
komunikasi_lulus_full = Column(String(255)) # F1769
|
||||||
|
# Penilaian kemampuan kerja sama tim saat lulus (F1771)
|
||||||
|
kerja_sama_lulus_full = Column(String(255)) # F1771
|
||||||
|
# Penilaian kesiapan pengembangan diri berkelanjutan saat lulus (F1773)
|
||||||
|
pengembangan_diri_lulus_full = Column(String(255)) # F1773
|
||||||
|
|
||||||
|
# ===== SECTION 7: Kompetensi SAAT INI (F1762-F1774) =====
|
||||||
|
# Tingkat kebutuhan kompetensi etika dalam pekerjaan saat ini (F1762)
|
||||||
|
etika_saat_ini = Column(String(255)) # F1762
|
||||||
|
# Tingkat kebutuhan keahlian bidang ilmu dalam pekerjaan saat ini (F1764)
|
||||||
|
keahlian_saat_ini = Column(String(255)) # F1764
|
||||||
|
# Tingkat kebutuhan bahasa Inggris dalam pekerjaan saat ini (F1766)
|
||||||
|
bahasa_inggris_saat_ini = Column(String(255)) # F1766
|
||||||
|
# Tingkat kebutuhan teknologi informasi dalam pekerjaan saat ini (F1768)
|
||||||
|
teknologi_informasi_saat_ini = Column(String(255)) # F1768
|
||||||
|
# Tingkat kebutuhan kemampuan komunikasi dalam pekerjaan saat ini (F1770)
|
||||||
|
komunikasi_saat_ini = Column(String(255)) # F1770
|
||||||
|
# Tingkat kebutuhan kerja sama tim dalam pekerjaan saat ini (F1772)
|
||||||
|
kerja_sama_saat_ini = Column(String(255)) # F1772
|
||||||
|
# Tingkat kebutuhan pengembangan diri dalam pekerjaan saat ini (F1774)
|
||||||
|
pengembangan_diri_saat_ini = Column(String(255)) # F1774
|
||||||
|
|
||||||
|
# ===== SECTION 8: Metode Pembelajaran (F21-F27) =====
|
||||||
|
# Efektivitas metode perkuliahan teori di kelas (F21)
|
||||||
|
metode_perkuliahan = Column(String(255)) # F21
|
||||||
|
# Efektivitas metode demonstrasi / peragaan materi kuliah (F22)
|
||||||
|
metode_demonstrasi = Column(String(255)) # F22
|
||||||
|
# Efektivitas metode pengerjaan proyek riset / tugas akhir (F23)
|
||||||
|
metode_proyek_riset = Column(String(255)) # F23
|
||||||
|
# Efektivitas pengalaman program magang / kerja praktik (F24)
|
||||||
|
metode_magang = Column(String(255)) # F24
|
||||||
|
# Efektivitas sesi praktikum di laboratorium komputer (F25)
|
||||||
|
metode_praktikum = Column(String(255)) # F25
|
||||||
|
# Efektivitas kegiatan kerja lapangan / PKL (F26)
|
||||||
|
metode_kerja_lapangan = Column(String(255)) # F26
|
||||||
|
# Efektivitas metode diskusi interaktif kelompok (F27)
|
||||||
|
metode_diskusi = Column(String(255)) # F27
|
||||||
|
|
||||||
|
# ===== SECTION 9: Waktu Mencari Kerja (F301-F303) =====
|
||||||
|
# Saat mulai mencari kerja (F301: Sebelum wisuda, Setelah wisuda, atau Tidak mencari kerja)
|
||||||
|
mulai_cari_kerja = Column(String(255)) # F301: Sebelum/Sesudah wisuda/Tidak
|
||||||
|
# Jumlah bulan mulai berburu lowongan kerja sebelum resmi diwisuda (F302)
|
||||||
|
bulan_cari_sebelum_lulus = Column(Integer) # F302: 0-12
|
||||||
|
# Jumlah bulan mulai berburu lowongan kerja setelah resmi diwisuda (F303)
|
||||||
|
bulan_cari_setelah_wisuda = Column(Integer) # F303: 0-12
|
||||||
|
|
||||||
|
# ===== SECTION 10: Cara Mencari Kerja (F401-F415) =====
|
||||||
|
# Mencari kerja melalui iklan lowongan koran/majalah (F401)
|
||||||
|
cari_iklan = Column(String(255)) # F401
|
||||||
|
# Melamar langsung ke perusahaan tanpa menunggu pengumuman lowongan (F402)
|
||||||
|
cari_melamar_tanpa_lowongan = Column(String(255)) # F402
|
||||||
|
# Mencari lowongan melalui job fair / bursa pameran kerja (F403)
|
||||||
|
cari_bursa_pameran = Column(String(255)) # F403
|
||||||
|
# Melamar kerja secara daring via portal internet / LinkedIn / Jobstreet (F404)
|
||||||
|
cari_internet = Column(String(255)) # F404
|
||||||
|
# Dihubungi langsung oleh perusahaan / headhunter (F405)
|
||||||
|
cari_dihubungi_perusahaan = Column(String(255)) # F405
|
||||||
|
# Menghubungi dinas tenaga kerja / Kemenakertrans (F406)
|
||||||
|
cari_kemenakertrans = Column(String(255)) # F406
|
||||||
|
# Menggunakan jasa agen penyalur tenaga kerja resmi (F407)
|
||||||
|
cari_agen_tenaga_kerja = Column(String(255)) # F407
|
||||||
|
# Memanfaatkan pusat pengembangan karir kampus (CDC Polije) (F408)
|
||||||
|
cari_pusat_karir = Column(String(255)) # F408
|
||||||
|
# Menghubungi unit kantor kemahasiswaan kampus (F409)
|
||||||
|
cari_kantor_mahasiswa = Column(String(255)) # F409
|
||||||
|
# Membangun jejaring profesional semasa masih berstatus mahasiswa (F410)
|
||||||
|
cari_jejaring = Column(String(255)) # F410
|
||||||
|
# Memperoleh informasi lowongan lewat relasi keluarga atau rekan (F411)
|
||||||
|
cari_relasi = Column(String(255)) # F411
|
||||||
|
# Merintis dan membangun badan usaha bisnis mandiri (F412)
|
||||||
|
cari_bangun_usaha = Column(String(255)) # F412
|
||||||
|
# Melanjutkan bekerja di institusi tempat pernah magang (F413)
|
||||||
|
cari_magang = Column(String(255)) # F413
|
||||||
|
# Bekerja di lingkungan institusi kampus tempat kuliah (F414)
|
||||||
|
cari_tempat_kuliah = Column(String(255)) # F414
|
||||||
|
# Saluran cara pencarian kerja lainnya (F415)
|
||||||
|
cari_lainnya = Column(Text) # F415
|
||||||
|
|
||||||
|
# ===== SECTION 11: Lamaran & Keaktifan (F6, F7, F7a, F1001) =====
|
||||||
|
# Jumlah total instansi/perusahaan yang pernah dikirimi surat lamaran kerja (F6)
|
||||||
|
jumlah_perusahaan_dilamar = Column(String(100)) # F6
|
||||||
|
# Jumlah perusahaan yang memberikan balasan respon atas surat lamaran (F7)
|
||||||
|
jumlah_perusahaan_merespons = Column(String(100)) # F7
|
||||||
|
# Jumlah perusahaan yang mengundang untuk tahapan wawancara/interview (F7a)
|
||||||
|
jumlah_perusahaan_wawancara = Column(String(100)) # F7a
|
||||||
|
# Status apakah aktif mencari pekerjaan dalam 4 minggu terakhir (F1001)
|
||||||
|
aktif_cari_kerja_4_minggu = Column(String(255)) # F1001: Ya/Tidak/Lainnya
|
||||||
|
|
||||||
|
# ===== SECTION 12: Kesesuaian Pekerjaan (F1601-F1614) =====
|
||||||
|
# Keterangan pekerjaan yang dijalani saat ini tidak sesuai dengan bidang studi (F1601)
|
||||||
|
kerja_tidak_sesuai_1 = Column(Text) # F1601
|
||||||
|
# Keterangan pekerjaan tidak memerlukan kualifikasi perguruan tinggi (F1602)
|
||||||
|
kerja_tidak_sesuai_2 = Column(Text) # F1602
|
||||||
|
# Alasan ketidaksesuaian 1: Pertimbangan faktor gaji atau pendapatan (F1603)
|
||||||
|
alasan_tidak_sesuai_1 = Column(Text) # F1603
|
||||||
|
# Alasan ketidaksesuaian 2: Faktor prospek karir dan jenjang promosi (F1604)
|
||||||
|
alasan_tidak_sesuai_2 = Column(Text) # F1604
|
||||||
|
# Alasan ketidaksesuaian 3: Pertimbangan keamanan kerja atau stabilitas (F1605)
|
||||||
|
alasan_tidak_sesuai_3 = Column(Text) # F1605
|
||||||
|
# Alasan ketidaksesuaian 4: Kesesuaian dengan minat dan bakat pribadi (F1606)
|
||||||
|
alasan_tidak_sesuai_4 = Column(Text) # F1606
|
||||||
|
# Alasan ketidaksesuaian 5: Pertimbangan kedekatan lokasi tempat tinggal (F1607)
|
||||||
|
alasan_tidak_sesuai_5 = Column(Text) # F1607
|
||||||
|
# Alasan ketidaksesuaian 6: Lingkungan suasana kerja yang nyaman (F1608)
|
||||||
|
alasan_tidak_sesuai_6 = Column(Text) # F1608
|
||||||
|
# Alasan ketidaksesuaian 7: Belum berhasil mendapat pekerjaan yang sesuai prodi (F1609)
|
||||||
|
alasan_tidak_sesuai_7 = Column(Text) # F1609
|
||||||
|
# Alasan ketidaksesuaian 8: Belum berencana mencari pekerjaan yang sesuai (F1610)
|
||||||
|
alasan_tidak_sesuai_8 = Column(Text) # F1610
|
||||||
|
# Alasan ketidaksesuaian 9: Pekerjaan saat ini dirasa cukup menarik (F1611)
|
||||||
|
alasan_tidak_sesuai_9 = Column(Text) # F1611
|
||||||
|
# Alasan ketidaksesuaian 10: Pengaruh pertimbangan keluarga (F1612)
|
||||||
|
alasan_tidak_sesuai_10 = Column(Text) # F1612
|
||||||
|
# Alasan ketidaksesuaian 11: Faktor lain yang tidak disebutkan (F1613)
|
||||||
|
alasan_tidak_sesuai_11 = Column(Text) # F1613
|
||||||
|
# Alasan ketidaksesuaian lainnya dalam uraian narasi bebas (F1614)
|
||||||
|
alasan_tidak_sesuai_lainnya = Column(Text) # F1614
|
||||||
|
|
||||||
|
# ===== Data Extra (untuk kolom dinamis yang tidak terduga) =====
|
||||||
|
# Kolom fleksibel berformat JSON text untuk menampung field tambahan dari kuesioner sewaktu-waktu
|
||||||
|
data_extra = Column(Text) # JSON untuk kolom tambahan
|
||||||
|
|
||||||
|
# Relasi ORM balik ke model Alumni (One-to-One)
|
||||||
|
alumni = relationship("Alumni", back_populates="tracer_response")
|
||||||
|
|
||||||
|
# Waktu saat alumni mengirimkan/menyimpan respon kuesioner tracer study
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
# Waktu saat alumni memperbarui data respon kuesioner tracer study
|
||||||
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# MODEL ENTITAS: QUESTIONS (Tabel Master Butir Pertanyaan Kuesioner)
|
||||||
|
# =========================================================================
|
||||||
|
class Question(Base):
|
||||||
|
"""Tabel Questions - Simpan master pertanyaan instrumen tracer study secara dinamis"""
|
||||||
|
# Nama tabel master pertanyaan di database
|
||||||
|
__tablename__ = "questions"
|
||||||
|
|
||||||
|
# Primary key identitas pertanyaan bertipe integer
|
||||||
|
id: Any = Column(Integer, primary_key=True, index=True)
|
||||||
|
# Kode unik butir pertanyaan (contoh: "F8", "F502", "F1761")
|
||||||
|
kode: Any = Column(String(20), unique=True, nullable=False, index=True)
|
||||||
|
# Bunyi kalimat teks lengkap pertanyaan kuesioner
|
||||||
|
pertanyaan: Any = Column(Text, nullable=False)
|
||||||
|
# Tipe komponen input formulir: dropdown, pilihan, skala, teks, date, ya_tidak
|
||||||
|
tipe: Any = Column(String(20), default="pilihan") # dropdown, pilihan, skala, teks, date, ya_tidak
|
||||||
|
# Kategori bagian form instrumen kuesioner (misal: "Identitas", "Pekerjaan", "Kompetensi")
|
||||||
|
section: Any = Column(String(100), index=True)
|
||||||
|
# Urutan nomor urut tampilan form di antarmuka frontend
|
||||||
|
nomor_form: Any = Column(Integer, default=0)
|
||||||
|
# Tahun berlakunya instrumen pertanyaan (default: "all" untuk berlaku di semua tahun angkatan)
|
||||||
|
tahun: Any = Column(String(20), default="all")
|
||||||
|
# Status keaktifan butir pertanyaan (True = ditampilkan di frontend, False = diarsipkan)
|
||||||
|
is_active: Any = Column(Boolean, default=True)
|
||||||
|
|
||||||
|
# Relasi One-to-Many ke tabel AnswerOption (jika tipe pertanyaan memiliki opsi pilihan berganda)
|
||||||
|
# cascade="all, delete-orphan": Jika pertanyaan dihapus, seluruh opsi jawabannya otomatis ikut terhapus
|
||||||
|
options = relationship("AnswerOption", back_populates="question", cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
# Waktu penambahan pertanyaan ke sistem
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
# Waktu pembaruan teks atau konfigurasi pertanyaan
|
||||||
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# MODEL ENTITAS: ANSWER OPTIONS (Tabel Master Pilihan Jawaban Kuesioner)
|
||||||
|
# =========================================================================
|
||||||
|
class AnswerOption(Base):
|
||||||
|
"""Tabel Answer Options - Simpan pilihan jawaban opsi berganda untuk setiap butir pertanyaan"""
|
||||||
|
# Nama tabel master opsi jawaban di database
|
||||||
|
__tablename__ = "answer_options"
|
||||||
|
|
||||||
|
# Primary key identitas opsi jawaban bertipe integer
|
||||||
|
id: Any = Column(Integer, primary_key=True, index=True)
|
||||||
|
# Foreign key yang mengaitkan opsi ini ke ID pertanyaan di tabel questions
|
||||||
|
question_id: Any = Column(Integer, ForeignKey("questions.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||||
|
# Nilai value atau kode jawaban yang disimpan ke database saat dipilih (contoh: "1", "2", "Ya", "Tidak")
|
||||||
|
kode: Any = Column(String(50), nullable=False, index=True) # Kode jawaban: 1, 2, 3, atau "1", "Ya", dll
|
||||||
|
# Label teks deskripsi jawaban yang ditampilkan kepada pengguna di antarmuka web
|
||||||
|
teks: Any = Column(Text, nullable=False) # Teks jawaban
|
||||||
|
# Nomor urut posisi urutan opsi jawaban dalam pilihan radio button atau dropdown
|
||||||
|
urutan: Any = Column(Integer, default=0)
|
||||||
|
# Status keaktifan opsi jawaban (True = aktif dapat dipilih)
|
||||||
|
is_active: Any = Column(Boolean, default=True)
|
||||||
|
|
||||||
|
# Relasi ORM balik ke tabel Question
|
||||||
|
question = relationship("Question", back_populates="options")
|
||||||
|
|
||||||
|
# Waktu pembuatan opsi jawaban
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
# Mengimpor tipe data kolom dari SQLAlchemy: Column (definisi kolom), Integer (angka bulat), String (teks pendek), DateTime (waktu/tanggal)
|
||||||
|
from sqlalchemy import Column, Integer, String, DateTime
|
||||||
|
# Mengimpor modul fungsi SQL bawaan (func) untuk mengambil timestamp saat ini seperti NOW()
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
# Mengimpor kelas basis Base dari modul base agar model Prodi terdaftar di metadata SQLAlchemy
|
||||||
|
from app.models.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
# Mendefinisikan model ORM untuk tabel data Program Studi (Prodi)
|
||||||
|
class Prodi(Base):
|
||||||
|
# Nama tabel fisik di dalam database MySQL
|
||||||
|
__tablename__ = "prodi"
|
||||||
|
|
||||||
|
# Kolom ID sebagai primary key bertipe integer dengan pengindeksan (index) untuk mempercepat pencarian data
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
|
||||||
|
# Kolom kode program studi (contoh: "55301" untuk D3 MIF, "57401" untuk D4 TIF), bersifat unik (unique=True) dan wajib diisi (nullable=False)
|
||||||
|
kode_prodi = Column(String(10), unique=True, nullable=False)
|
||||||
|
|
||||||
|
# Kolom nama lengkap program studi (contoh: "Teknik Informatika"), maksimal 150 karakter dan wajib diisi
|
||||||
|
nama_prodi = Column(String(150), nullable=False)
|
||||||
|
|
||||||
|
# Kolom jenjang pendidikan (contoh: "D3", "D4", "S1"), maksimal 5 karakter dan tidak boleh kosong
|
||||||
|
jenjang = Column(String(5), nullable=False)
|
||||||
|
|
||||||
|
# Kolom nama jurusan induk fakultas/jurusan (contoh: "Teknologi Informasi"), maksimal 100 karakter
|
||||||
|
jurusan = Column(String(100))
|
||||||
|
|
||||||
|
# Kolom waktu pembuatan record, otomatis diisi dengan timestamp saat data pertama kali disimpan (server_default=func.now())
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
|
# Kolom waktu pembaruan record, otomatis diperbarui setiap kali ada perubahan data (onupdate=func.now())
|
||||||
|
updated_at = Column(
|
||||||
|
DateTime(timezone=True),
|
||||||
|
server_default=func.now(),
|
||||||
|
onupdate=func.now()
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
# Mengimpor modul Any dari typing untuk penentuan type hint variabel di Python
|
||||||
|
from typing import Any
|
||||||
|
# Mengimpor tipe data kolom SQLAlchemy: Column (kolom), Integer (angka bulat), String (karakter teks), Boolean (true/false), DateTime (waktu)
|
||||||
|
from sqlalchemy import Column, Integer, String, Boolean, DateTime
|
||||||
|
# Mengimpor func untuk fungsi SQL bawaan (misal mengambil waktu server database)
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
# Mengimpor kelas basis declarative Base dari model base
|
||||||
|
from app.models.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
# Mendefinisikan model ORM untuk tabel pengguna sistem (Admin / Kaprodi)
|
||||||
|
class User(Base):
|
||||||
|
# Menentukan nama tabel fisik pada database MySQL
|
||||||
|
__tablename__ = "users"
|
||||||
|
|
||||||
|
# Kolom ID pengguna sebagai Primary Key unik bertipe integer dengan pengindeksan aktif
|
||||||
|
id: Any = Column(Integer, primary_key=True, index=True)
|
||||||
|
|
||||||
|
# Kolom username untuk otentikasi login, maksimal 100 karakter, harus unik (unique=True), dan wajib diisi (nullable=False)
|
||||||
|
username: Any = Column(String(100), unique=True, index=True, nullable=False)
|
||||||
|
|
||||||
|
# Kolom nama lengkap pengguna / pejabat prodi, maksimal 150 karakter, bersifat opsional (nullable=True)
|
||||||
|
nama: Any = Column(String(150), nullable=True)
|
||||||
|
|
||||||
|
# Kolom alamat email pengguna, harus unik dan terindeks untuk reset password serta komunikasi
|
||||||
|
email: Any = Column(String(150), unique=True, index=True, nullable=False)
|
||||||
|
|
||||||
|
# Kolom hash password hasil enkripsi algoritma Bcrypt (bukan plaintext demi keamanan data)
|
||||||
|
hashed_password: Any = Column(String(255), nullable=False)
|
||||||
|
|
||||||
|
# Kolom hak akses (role) pengguna dalam sistem, nilai default adalah 'admin', opsi: 'admin' atau 'kaprodi'
|
||||||
|
role: Any = Column(String(50), default="admin", nullable=False) # 'admin' | 'kaprodi'
|
||||||
|
|
||||||
|
# Kolom kode program studi binaan (khusus jika role adalah 'kaprodi' seperti '55301' MIF, '57401' TIF)
|
||||||
|
kode_prodi: Any = Column(String(20), nullable=True) # '55301', '57401', '56401', etc.
|
||||||
|
|
||||||
|
# Kolom status keaktifan akun pengguna (True = aktif dapat login, False = dinonaktifkan/diblokir)
|
||||||
|
is_active: Any = Column(Boolean, default=True)
|
||||||
|
|
||||||
|
# Kolom pencatatan waktu registrasi atau pembuatan akun pengguna
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
|
# Kolom pencatatan waktu pembaruan profil atau password pengguna
|
||||||
|
updated_at = Column(
|
||||||
|
DateTime(timezone=True),
|
||||||
|
server_default=func.now(),
|
||||||
|
onupdate=func.now()
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Parsers package - helper functions untuk import
|
||||||
|
from .column_finder import find_column, get_value, check_available_columns
|
||||||
|
from .converters import parse_2024_value, parse_date, konversi_jawaban, convert_value_for_field
|
||||||
|
from .mappings import YEAR_CONFIG, NUMERIC_FIELDS, get_field_code
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
import re
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
|
||||||
|
def normalize(col_name):
|
||||||
|
"""Normalize nama kolom - lowercase, collapse spaces, and strip whitespace"""
|
||||||
|
if pd.isna(col_name):
|
||||||
|
return ""
|
||||||
|
# Collapse multiple whitespace characters into single space
|
||||||
|
cleaned = re.sub(r'\s+', ' ', str(col_name))
|
||||||
|
return cleaned.strip().lower()
|
||||||
|
|
||||||
|
|
||||||
|
def find_column(source_cols, possible_names):
|
||||||
|
"""
|
||||||
|
Cari kolom dari beberapa kemungkinan nama.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source_cols: list kolom dari DataFrame
|
||||||
|
possible_names: list kemungkinan nama kolom
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Nama kolom yang ditemukan, atau None
|
||||||
|
"""
|
||||||
|
source_cols_map = {normalize(c): c for c in source_cols}
|
||||||
|
|
||||||
|
# Step 1: Direct normalized match
|
||||||
|
for name in possible_names:
|
||||||
|
norm_name = normalize(name)
|
||||||
|
if norm_name in source_cols_map:
|
||||||
|
return source_cols_map[norm_name]
|
||||||
|
|
||||||
|
# Step 2: Code token match (e.g. 'f1761', 'f5a1', 'f1101', 'f502', etc.)
|
||||||
|
for name in possible_names:
|
||||||
|
norm_name = normalize(name)
|
||||||
|
# Extract question code if present (e.g., 'f1761', 'f5a1', 'f8', etc.)
|
||||||
|
match = re.search(r'\b(f\d+[a-z]?\d*)\b', norm_name)
|
||||||
|
if match:
|
||||||
|
code = match.group(1)
|
||||||
|
for orig_norm, orig_col in source_cols_map.items():
|
||||||
|
if re.search(r'\b' + re.escape(code) + r'\b', orig_norm):
|
||||||
|
return orig_col
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_value(row, source_cols, possible_names, default=None):
|
||||||
|
"""
|
||||||
|
Ambil nilai dari row dengan cari kolom dulu.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
row: pandas Series (satu row)
|
||||||
|
source_cols: list kolom dari DataFrame
|
||||||
|
possible_names: list kemungkinan nama kolom
|
||||||
|
default: nilai default jika tidak ditemukan
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Nilai dari row, atau default
|
||||||
|
"""
|
||||||
|
col = find_column(source_cols, possible_names)
|
||||||
|
if col and pd.notna(row.get(col)):
|
||||||
|
val = row[col]
|
||||||
|
# Convert int/float
|
||||||
|
if isinstance(val, (int, float)):
|
||||||
|
return int(val) if val == int(val) else val
|
||||||
|
return str(val).strip()
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def check_available_columns(df_cols: list) -> dict:
|
||||||
|
"""
|
||||||
|
Check kolom apa saja yang tersedia di dataframe.
|
||||||
|
Berguna untuk debugging.
|
||||||
|
"""
|
||||||
|
result = {
|
||||||
|
'total': len(df_cols),
|
||||||
|
'has_email': any('email' in str(c).lower() for c in df_cols),
|
||||||
|
'has_nim': any('nim' in str(c).lower() for c in df_cols),
|
||||||
|
'has_f1102': 'F1102' in df_cols,
|
||||||
|
'has_f5b': 'F5b' in df_cols,
|
||||||
|
'has_f1762': 'F1762' in df_cols,
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
@ -0,0 +1,566 @@
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
|
@ -0,0 +1,705 @@
|
||||||
|
"""
|
||||||
|
Mapping kolom untuk setiap tahun tracer study.
|
||||||
|
Ini adalah sumber kebenaran untuk mapping CSV -> database field.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# YEAR CONFIGURATION
|
||||||
|
# Setiap tahun punya format kolom yang berbeda
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
YEAR_CONFIG = {
|
||||||
|
'2019': {
|
||||||
|
'description': 'Format lama - kode angka',
|
||||||
|
'skip_jumlah_perusahaan': True, # F1102 bukan jumlah_perusahaan
|
||||||
|
'has_email': True,
|
||||||
|
'email_col': 'Email',
|
||||||
|
'nama_col': 'Nama',
|
||||||
|
'tahun_col': 'Tahun Lulus',
|
||||||
|
'is_text_format': False, # Data dalam bentuk kode angka
|
||||||
|
},
|
||||||
|
'2020': {
|
||||||
|
'description': 'Format 2020',
|
||||||
|
'skip_jumlah_perusahaan': False,
|
||||||
|
'has_email': True,
|
||||||
|
'email_col': 'Email',
|
||||||
|
'nama_col': 'Nama',
|
||||||
|
'tahun_col': 'Tahun Lulus',
|
||||||
|
'is_text_format': False,
|
||||||
|
},
|
||||||
|
'2021': {
|
||||||
|
'description': 'Format 2021 - tidak ada kolom email',
|
||||||
|
'skip_jumlah_perusahaan': False,
|
||||||
|
'has_email': False,
|
||||||
|
'email_col': None,
|
||||||
|
'nama_col': 'Nama',
|
||||||
|
'tahun_col': 'Tahun Lulus',
|
||||||
|
'is_text_format': False,
|
||||||
|
},
|
||||||
|
'2022': {
|
||||||
|
'description': 'Format 2022 - ada emailmsmh',
|
||||||
|
'skip_jumlah_perusahaan': False,
|
||||||
|
'has_email': True,
|
||||||
|
'email_col': 'emailmsmh',
|
||||||
|
'nama_col': 'nmmhsmsmh',
|
||||||
|
'tahun_col': 'tahun_lulus',
|
||||||
|
'is_text_format': False,
|
||||||
|
},
|
||||||
|
'2023': {
|
||||||
|
'description': 'Format 2023 - ada emailmsmh',
|
||||||
|
'skip_jumlah_perusahaan': False,
|
||||||
|
'has_email': True,
|
||||||
|
'email_col': 'emailmsmh',
|
||||||
|
'nama_col': 'nmmhsmsmh',
|
||||||
|
'tahun_col': 'tahun_lulus',
|
||||||
|
'is_text_format': False,
|
||||||
|
},
|
||||||
|
'2024': {
|
||||||
|
'description': 'Format 2024 - data dalam format teks lengkap "KODE - TEKS"',
|
||||||
|
'skip_jumlah_perusahaan': False,
|
||||||
|
'has_email': True,
|
||||||
|
'email_col': '6. Alamat Email / emailmsmh',
|
||||||
|
'nama_col': '4. Nama Lengkap / nmmhsmsmh (Gunakan Huruf Besar)',
|
||||||
|
'tahun_col': '7. Tahun Lulus / tahun_lulus',
|
||||||
|
'nim_col': '3. NIM / nimhsmsmh (Gunakan Huruf Besar)',
|
||||||
|
'is_text_format': True, # Data SUDAH dalam format teks lengkap
|
||||||
|
},
|
||||||
|
'2025': {
|
||||||
|
'description': 'Format 2025 - sama dengan 2024',
|
||||||
|
'skip_jumlah_perusahaan': False,
|
||||||
|
'has_email': True,
|
||||||
|
'email_col': '6. Alamat Email / emailmsmh',
|
||||||
|
'nama_col': '4. Nama Lengkap / nmmhsmsmh (Gunakan Huruf Besar)',
|
||||||
|
'tahun_col': '7. Tahun Lulus / tahun_lulus',
|
||||||
|
'nim_col': '3. NIM / nimhsmsmh (Gunakan Huruf Besar)',
|
||||||
|
'is_text_format': True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# COLUMN MAPPING - IDENTITAS
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
IDENTITAS_MAPPING = {
|
||||||
|
# Field database: [list kemungkinan nama kolom di CSV]
|
||||||
|
'nim': [
|
||||||
|
'NIM/Nomor Mahasiswa',
|
||||||
|
'NIM',
|
||||||
|
'nimhsmsmh',
|
||||||
|
'3. NIM / nimhsmsmh',
|
||||||
|
'3. NIM / nimhsmsmh (Gunakan Huruf Besar)',
|
||||||
|
'3. NIM / nimhsmsmh (Gunakan Huruf Besar) ',
|
||||||
|
],
|
||||||
|
'nama': [
|
||||||
|
'Nama',
|
||||||
|
'nmmhsmsmh',
|
||||||
|
'4. Nama Lengkap / nmmhsmsmh',
|
||||||
|
'4. Nama Lengkap / nmmhsmsmh (Gunakan Huruf Besar)',
|
||||||
|
],
|
||||||
|
'kode_prodi': [
|
||||||
|
'Kode Prodi',
|
||||||
|
'kdpstmsmh',
|
||||||
|
'2. Kode Program Studi / kdpstmsmh',
|
||||||
|
],
|
||||||
|
'kode_pt': [
|
||||||
|
'Kode PT',
|
||||||
|
'kdptimsmh',
|
||||||
|
'1. Kode Perguruan Tinggi / kdptimsmh',
|
||||||
|
],
|
||||||
|
'email': [
|
||||||
|
'Email',
|
||||||
|
'emailmsmh',
|
||||||
|
'6. Alamat Email / emailmsmh',
|
||||||
|
],
|
||||||
|
'tahun_lulus': [
|
||||||
|
'Tahun Lulus',
|
||||||
|
'tahun_lulus',
|
||||||
|
'7. Tahun Lulus / tahun_lulus',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# COLUMN MAPPING - PEKERJAAN
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
PEKERJAAN_MAPPING = {
|
||||||
|
'status_saat_ini': [
|
||||||
|
'F8',
|
||||||
|
'f8',
|
||||||
|
'1. Jelaskan status Anda saat ini? f8',
|
||||||
|
],
|
||||||
|
'bulan_dapat_kerja': [
|
||||||
|
'F502',
|
||||||
|
'f502',
|
||||||
|
'2. Berapa bulan anda mendapatkan pekerjaan SEBELUM LULUS? f502',
|
||||||
|
'2. Dalam berapa bulan Anda mendapatkan pekerjaan pertama ? (f502)',
|
||||||
|
],
|
||||||
|
'dapet_kerja_6_bulan': [
|
||||||
|
'F504',
|
||||||
|
'f504',
|
||||||
|
'3. Apakah Anda telah mendapatkan pekerjaan <= 6 bulan / termasuk bekerja sebelum lulus? f504',
|
||||||
|
'3. Apakah Anda telah mendapatkan pekerjaan <= 6 bulan / termasuk bekerja sebelum lulus? f504',
|
||||||
|
],
|
||||||
|
'rata_pendapatan': [
|
||||||
|
'F505',
|
||||||
|
'f505',
|
||||||
|
'4. Berapa rata-rata pendapatan Anda per bulan ? f505',
|
||||||
|
'4. Berapa rata-rata pendapatan Anda per bulan? f505',
|
||||||
|
],
|
||||||
|
'bulan_dapat_kerja_setelah': [
|
||||||
|
'F506',
|
||||||
|
'f506',
|
||||||
|
'5. Berapa bulan Anda MENDAPATKAN PEKERJAAN SETELAH LULUS? f506',
|
||||||
|
],
|
||||||
|
'provinsi_kerja': [
|
||||||
|
'F5a1',
|
||||||
|
'f5a1',
|
||||||
|
'6. Dimana lokasi provinsi tempat Anda bekerja? f5a1',
|
||||||
|
'6. Ditahui lokasi provinsi tempat Anda bekerja? f5a1',
|
||||||
|
'6. Lokasi provinsi tempat Anda bekerja? f5a1',
|
||||||
|
],
|
||||||
|
'kab_kota_kerja': [
|
||||||
|
'F5a2',
|
||||||
|
'f5a2',
|
||||||
|
'7. Dimana lokasi kabupaten/kota tempat Anda bekerja? f5a2',
|
||||||
|
'7. Ditahui lokasi kabupaten/kota tempat Anda bekerja? f5a2',
|
||||||
|
'7. Lokasi kabupaten/kota tempat Anda bekerja? f5a2',
|
||||||
|
],
|
||||||
|
'jenis_perusahaan_full': [
|
||||||
|
'F1101',
|
||||||
|
'f1101',
|
||||||
|
'8. Apa jenis perusahaan/instansi/institusi tempat anda bekerja sekarang? f1101',
|
||||||
|
'8. Ditahui jenis perusahaan/instansi/institusi tempat anda bekerja sekarang? f1101',
|
||||||
|
'8. Jenis perusahaan/instansi tempat anda bekerja sekarang? f1101',
|
||||||
|
],
|
||||||
|
'jumlah_perusahaan': [
|
||||||
|
'F1102',
|
||||||
|
'f1102',
|
||||||
|
],
|
||||||
|
'nama_perusahaan': [
|
||||||
|
'F5b',
|
||||||
|
'f5b',
|
||||||
|
'9. Apa nama perusahaan/kantor tempat Anda bekerja? f5b',
|
||||||
|
'9. Ditahui nama perusahaan/kantor tempat Anda bekerja? f5b',
|
||||||
|
'9. Nama perusahaan/kantor tempat Anda bekerja? f5b',
|
||||||
|
],
|
||||||
|
'jabatan': [
|
||||||
|
'F5c',
|
||||||
|
'f5c',
|
||||||
|
'10. Bila Anda berwiraswasta, apa posisi/jabatan Anda saat ini ? f5c',
|
||||||
|
'10. Bilamana Anda berwiraswasta, apa posisi/jabatan Anda saat ini ? f5c',
|
||||||
|
],
|
||||||
|
'tingkatan_tempat_kerja': [
|
||||||
|
'F5d',
|
||||||
|
'f5d',
|
||||||
|
'11. Apa tingkatan tempat kerja Anda? f5d',
|
||||||
|
'11. Ditahui tingkatan tempat kerja Anda? f5d',
|
||||||
|
'11. Tingkatan tempat kerja Anda? f5d',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# COLUMN MAPPING - STUDI LANJUT
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
STUDI_LANJUT_MAPPING = {
|
||||||
|
'sumber_biaya_studi': [
|
||||||
|
'F18a',
|
||||||
|
'f18a',
|
||||||
|
'12. Sumber biaya Studi Lanjut Anda? f18a',
|
||||||
|
],
|
||||||
|
'pt_studi_lanjut': [
|
||||||
|
'F18b',
|
||||||
|
'f18b',
|
||||||
|
'13. Nama Perguruan Tinggi Studi Lanjut Anda? f18b',
|
||||||
|
],
|
||||||
|
'prodi_studi_lanjut': [
|
||||||
|
'F18c',
|
||||||
|
'f18c',
|
||||||
|
'14. Nama Program Studi Studi Lanjut Anda? f18c',
|
||||||
|
],
|
||||||
|
'tanggal_masuk_studi': [
|
||||||
|
'F18d',
|
||||||
|
'f18d',
|
||||||
|
'15. Tanggal Masuk Awal Studi Lanjut Anda? f18d',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# COLUMN MAPPING - KESESUAIAN
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
KESESUAIAN_MAPPING = {
|
||||||
|
'sumber_biaya_kuliah': [
|
||||||
|
'F1201',
|
||||||
|
'f1201',
|
||||||
|
'16. Sebutkan sumberdana dalam pembiayaan kuliah? (bukan ketika Studi Lanjut) f1201',
|
||||||
|
],
|
||||||
|
'keeratan_bidang_studi': [
|
||||||
|
'F14',
|
||||||
|
'f14',
|
||||||
|
'17. Seberapa erat hubungan bidang studi dengan pekerjaan Anda? f14',
|
||||||
|
],
|
||||||
|
'tingkat_pendidikan_tepat': [
|
||||||
|
'F15',
|
||||||
|
'f15',
|
||||||
|
'18. Tingkat pendidikan apa yang paling tepat/sesuai untuk pekerjaan anda saat ini? f15',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# COLUMN MAPPING - KOMPETENSI (SAAT LULUS)
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
KOMPETENSI_LULUS_MAPPING = {
|
||||||
|
'etika_lulus_full': [
|
||||||
|
'F1761',
|
||||||
|
'f1761',
|
||||||
|
'19. Pada tingkat mana kompetensi ETIKA anda pada SAAT LULUS? f1761',
|
||||||
|
'19. Padatingkat mana kompetensi ETIKA anda pada SAAT LULUS? f1761',
|
||||||
|
'19. Kompetensi ETIKA SAAT LULUS? f1761',
|
||||||
|
],
|
||||||
|
'keahlian_lulus_full': [
|
||||||
|
'F1763',
|
||||||
|
'f1763',
|
||||||
|
'21. Pada tingkat mana kompetensi KEAHLIAN BERDASARKAN BIDANG ILMU anda kuasai pada SAAT LULUS? f1763',
|
||||||
|
'21. Padatingkat mana kompetensi KEAHLIAN BERDASARKAN BIDANG ILMU anda kuasai pada SAAT LULUS? f1763',
|
||||||
|
'21. Kompetensi KEAHLIAN BERDASARKAN BIDANG ILMU SAAT LULUS? f1763',
|
||||||
|
],
|
||||||
|
'bahasa_inggris_lulus_full': [
|
||||||
|
'F1765',
|
||||||
|
'f1765',
|
||||||
|
'23. Pada tingkat mana kompetensi BAHASA INGGRIS anda kuasai pada SAAT LULUS? f1765',
|
||||||
|
'23. Padatingkat mana kompetensi BAHASA INGGRIS anda kuasai pada SAAT LULUS? f1765',
|
||||||
|
'23. Kompetensi BAHASA INGGRIS SAAT LULUS? f1765',
|
||||||
|
],
|
||||||
|
'teknologi_informasi_lulus_full': [
|
||||||
|
'F1767',
|
||||||
|
'f1767',
|
||||||
|
'25. Pada tingkat mana kompetensi TEKNOLOGI INFORMASI anda kuasai pada SAAT LULUS? f1767',
|
||||||
|
'25. Padatingkat mana kompetensi TEKNOLOGI INFORMASI anda kuasai pada SAAT LULUS? f1767',
|
||||||
|
'25. Kompetensi TEKNOLOGI INFORMASI SAAT LULUS? f1767',
|
||||||
|
],
|
||||||
|
'komunikasi_lulus_full': [
|
||||||
|
'F1769',
|
||||||
|
'f1769',
|
||||||
|
'27. Pada tingkat mana kompetensi KOMUNIKASI anda kuasai pada SAAT LULUS? f1769',
|
||||||
|
'27. Padatingkat mana kompetensi KOMUNIKASI anda kuasai pada SAAT LULUS? f1769',
|
||||||
|
'27. Kompetensi KOMUNIKASI SAAT LULUS? f1769',
|
||||||
|
],
|
||||||
|
'kerja_sama_lulus_full': [
|
||||||
|
'F1771',
|
||||||
|
'f1771',
|
||||||
|
'29. Pada tingkat mana kompetensi KERJA SAMA TIM anda pada SAAT LULUS? f1771',
|
||||||
|
'29. Padatingkat mana kompetensi KERJA SAMA TIM anda pada SAAT LULUS? f1771',
|
||||||
|
'29. Kompetensi KERJA SAMA TIM SAAT LULUS? f1771',
|
||||||
|
],
|
||||||
|
'pengembangan_diri_lulus_full': [
|
||||||
|
'F1773',
|
||||||
|
'f1773',
|
||||||
|
'31. Pada tingkat mana kompetensi PENGEMBANGAN DIRI anda pada SAAT LULUS? f1773',
|
||||||
|
'31. Padatingkat mana kompetensi PENGEMBANGAN DIRI anda pada SAAT LULUS? f1773',
|
||||||
|
'31. Kompetensi PENGEMBANGAN DIRI SAAT LULUS? f1773',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# COLUMN MAPPING - KOMPETENSI (SAAT INI)
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
KOMPETENSI_SAAT_INI_MAPPING = {
|
||||||
|
'etika_saat_ini': [
|
||||||
|
'F1762',
|
||||||
|
'f1762',
|
||||||
|
'20. Pada tingkat mana kompetensi ETIKA anda pada SAAT INI? f1762',
|
||||||
|
'20. Padatingkat mana kompetensi ETIKA anda pada SAAT INI? f1762',
|
||||||
|
'20. Kompetensi ETIKA SAAT INI? f1762',
|
||||||
|
],
|
||||||
|
'keahlian_saat_ini': [
|
||||||
|
'F1764',
|
||||||
|
'f1764',
|
||||||
|
'22. Pada tingkat mana kompetensi KEAHLIAN BERDASARKAN BIDANG ILMU anda kuasai pada SAAT INI? f1764',
|
||||||
|
'22. Padatingkat mana kompetensi KEAHLIAN BERDASARKAN BIDANG ILMU anda kuasai pada SAAT INI? f1764',
|
||||||
|
'22. Kompetensi KEAHLIAN BERDASARKAN BIDANG ILMU SAAT INI? f1764',
|
||||||
|
],
|
||||||
|
'bahasa_inggris_saat_ini': [
|
||||||
|
'F1766',
|
||||||
|
'f1766',
|
||||||
|
'24. Pada tingkat mana kompetensi BAHASA INGGRIS anda kuasai pada SAAT INI? f1766',
|
||||||
|
'24. Padatingkat mana kompetensi BAHASA INGGRIS anda kuasai pada SAAT INI? f1766',
|
||||||
|
'24. Kompetensi BAHASA INGGRIS SAAT INI? f1766',
|
||||||
|
],
|
||||||
|
'teknologi_informasi_saat_ini': [
|
||||||
|
'F1768',
|
||||||
|
'f1768',
|
||||||
|
'26. Pada tingkat mana kompetensi TEKNOLOGI INFORMASI anda kuasai pada SAAT INI? f1768',
|
||||||
|
'26. Padatingkat mana kompetensi TEKNOLOGI INFORMASI anda kuasai pada SAAT INI? f1768',
|
||||||
|
'26. Kompetensi TEKNOLOGI INFORMASI SAAT INI? f1768',
|
||||||
|
],
|
||||||
|
'komunikasi_saat_ini': [
|
||||||
|
'F1770',
|
||||||
|
'f1770',
|
||||||
|
'28. Pada tingkat mana kompetensi KOMUNIKASI anda kuasai pada SAAT INI? f1770',
|
||||||
|
'28. Padatingkat mana kompetensi KOMUNIKASI anda kuasai pada SAAT INI? f1769',
|
||||||
|
'28. Kompetensi KOMUNIKASI SAAT INI? f1770',
|
||||||
|
],
|
||||||
|
'kerja_sama_saat_ini': [
|
||||||
|
'F1772',
|
||||||
|
'f1772',
|
||||||
|
'30. Pada tingkat mana kompetensi KERJA SAMA TIM anda pada SAAT INI? f1772',
|
||||||
|
'30. Padatingkat mana kompetensi KERJA SAMA TIM anda pada SAAT INI? f1772',
|
||||||
|
'30. Kompetensi KERJA SAMA TIM SAAT INI? f1772',
|
||||||
|
],
|
||||||
|
'pengembangan_diri_saat_ini': [
|
||||||
|
'F1774',
|
||||||
|
'f1774',
|
||||||
|
'32. Pada tingkat mana kompetensi PENGEMBANGAN DIRI anda pada SAAT INI? f1774',
|
||||||
|
'32. Padatingkat mana kompetensi PENGEMBANGAN DIRI anda pada SAAT INI? f1774',
|
||||||
|
'32. Kompetensi PENGEMBANGAN DIRI SAAT INI? f1774',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# COLUMN MAPPING - METODE PEMBELAJARAN
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
METODE_PEMBELAJARAN_MAPPING = {
|
||||||
|
'metode_perkuliahan': [
|
||||||
|
'F21',
|
||||||
|
'f21',
|
||||||
|
'33. Perkuliahan (f21)',
|
||||||
|
'33. Perkuliahan f21',
|
||||||
|
],
|
||||||
|
'metode_demonstrasi': [
|
||||||
|
'F22',
|
||||||
|
'f22',
|
||||||
|
'34. Demonstrasi (f22)',
|
||||||
|
'34. Demonstrasi f22',
|
||||||
|
],
|
||||||
|
'metode_proyek_riset': [
|
||||||
|
'F23',
|
||||||
|
'f23',
|
||||||
|
'35. Partisipasi dalam proyek riset (f23)',
|
||||||
|
'35. Partisipasi dalam proyek riset f23',
|
||||||
|
],
|
||||||
|
'metode_magang': [
|
||||||
|
'F24',
|
||||||
|
'f24',
|
||||||
|
'36. Magang (f24)',
|
||||||
|
'36. Magang f24',
|
||||||
|
],
|
||||||
|
'metode_praktikum': [
|
||||||
|
'F25',
|
||||||
|
'f25',
|
||||||
|
'37. Praktikum (f25)',
|
||||||
|
'37. Praktikum f25',
|
||||||
|
],
|
||||||
|
'metode_kerja_lapangan': [
|
||||||
|
'F26',
|
||||||
|
'f26',
|
||||||
|
'38. Kerja Lapangan (f26)',
|
||||||
|
'38. Kerja Lapangan f26',
|
||||||
|
],
|
||||||
|
'metode_diskusi': [
|
||||||
|
'F27',
|
||||||
|
'f27',
|
||||||
|
'39. Diskusi (f27)',
|
||||||
|
'39. Diskusi f27',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# COLUMN MAPPING - WAKTU CARI KERJA
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
WAKTU_CARI_KERJA_MAPPING = {
|
||||||
|
'mulai_cari_kerja': [
|
||||||
|
'F301',
|
||||||
|
'f301',
|
||||||
|
'40. Kapan anda mulai mencari pekerjaan? f301',
|
||||||
|
],
|
||||||
|
'bulan_cari_sebelum_lulus': [
|
||||||
|
'F302',
|
||||||
|
'f302',
|
||||||
|
'41. Saya mencari pekerjaan, Kira-kira ....... bulan SEBELUM lulus (f302)',
|
||||||
|
],
|
||||||
|
'bulan_cari_setelah_wisuda': [
|
||||||
|
'F303',
|
||||||
|
'f303',
|
||||||
|
'42. Saya mencari pekerjaan, Kira-kira ....... bulan SETELAH wisuda (f303)',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# COLUMN MAPPING - CARA CARI KERJA (Ya/Tidak)
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
CARA_CARI_KERJA_MAPPING = {
|
||||||
|
'cari_iklan': [
|
||||||
|
'F401',
|
||||||
|
'f401',
|
||||||
|
'43. Melalui iklan di koran/majalah, brosur ? (f401)',
|
||||||
|
],
|
||||||
|
'cari_melamar_tanpa_lowongan': [
|
||||||
|
'F402',
|
||||||
|
'f402',
|
||||||
|
'44. Melamar ke perusahaan tanpa mengetahui lowongan yang ada (f402)',
|
||||||
|
],
|
||||||
|
'cari_bursa_pameran': [
|
||||||
|
'F403',
|
||||||
|
'f403',
|
||||||
|
'45. Pergi ke bursa/pameran kerja (f403)',
|
||||||
|
],
|
||||||
|
'cari_internet': [
|
||||||
|
'F404',
|
||||||
|
'f404',
|
||||||
|
'46. Mencari lewat internet/iklan online/milis (f404)',
|
||||||
|
],
|
||||||
|
'cari_dihubungi_perusahaan': [
|
||||||
|
'F405',
|
||||||
|
'f405',
|
||||||
|
'47. Dihubungi oleh perusahaan (f405)',
|
||||||
|
],
|
||||||
|
'cari_kemenakertrans': [
|
||||||
|
'F406',
|
||||||
|
'f406',
|
||||||
|
'48. Menghubungi Kemenakertrans (f406)',
|
||||||
|
],
|
||||||
|
'cari_agen_tenaga_kerja': [
|
||||||
|
'F407',
|
||||||
|
'f407',
|
||||||
|
'49. Menghubungi agen tenaga kerja komersial/swasta (f407)',
|
||||||
|
],
|
||||||
|
'cari_pusat_karir': [
|
||||||
|
'F408',
|
||||||
|
'f408',
|
||||||
|
'50. Memperoleh informasi dari pusat/kantor pengembangan karir fakultas/universitas (f408)',
|
||||||
|
],
|
||||||
|
'cari_kantor_mahasiswa': [
|
||||||
|
'F409',
|
||||||
|
'f409',
|
||||||
|
'51. Menghubungi kantor kemahasiswaan/hubungan alumni (f409)',
|
||||||
|
],
|
||||||
|
'cari_jejaring': [
|
||||||
|
'F410',
|
||||||
|
'f410',
|
||||||
|
'52. Membangun jejaring (network) sejak masih kuliah (f410)',
|
||||||
|
],
|
||||||
|
'cari_relasi': [
|
||||||
|
'F411',
|
||||||
|
'f411',
|
||||||
|
'53. Melalui relasi (misalnya dosen, orang tua, saudara, teman, Ikatan Alumni dll.) (f411)',
|
||||||
|
],
|
||||||
|
'cari_bangun_usaha': [
|
||||||
|
'F412',
|
||||||
|
'f412',
|
||||||
|
'54. Membangun bisnis sendiri (f412)',
|
||||||
|
],
|
||||||
|
'cari_magang': [
|
||||||
|
'F413',
|
||||||
|
'f413',
|
||||||
|
'55. Melalui penempatan kerja atau magang (f413)',
|
||||||
|
],
|
||||||
|
'cari_tempat_kuliah': [
|
||||||
|
'F414',
|
||||||
|
'f414',
|
||||||
|
'56. Bekerja di tempat yang sama dengan tempat kerja semasa kuliah (f414)',
|
||||||
|
],
|
||||||
|
'cari_lainnya': [
|
||||||
|
'F415',
|
||||||
|
'f415',
|
||||||
|
'57. Lainnya (f415)',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# COLUMN MAPPING - LAMARAN & KEAKTIFAN
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
LAMARAN_KEAKTIFAN_MAPPING = {
|
||||||
|
'jumlah_perusahaan_dilamar': [
|
||||||
|
'F6',
|
||||||
|
'f6',
|
||||||
|
'58. Berapa perusahaan/instansi/institusi yang sudah anda lamar (lewat surat atau e-mail) sebelum anda memperoleh pekerjaan pertama? f6',
|
||||||
|
],
|
||||||
|
'jumlah_perusahaan_merespons': [
|
||||||
|
'F7',
|
||||||
|
'f7',
|
||||||
|
'59. Berapa banyak perusahaan/instansi/institusi yang merespons lamaran Anda? f7',
|
||||||
|
],
|
||||||
|
'jumlah_perusahaan_wawancara': [
|
||||||
|
'F7a',
|
||||||
|
'f7a',
|
||||||
|
'60. Berapa banyak perusahaan/instansi/institusi yang mengundang anda untuk wawancara? f7a',
|
||||||
|
],
|
||||||
|
'aktif_cari_kerja_4_minggu': [
|
||||||
|
'F1001',
|
||||||
|
'f1001',
|
||||||
|
'61. Apakah Anda aktif mencari pekerjaan dalam 4 minggu terakhir? Pilihlah satu jawaban (f1001)',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# COLUMN MAPPING - KESESUAIAN PEKERJAAN
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
KESESUAIAN_PEKERJAAN_MAPPING = {
|
||||||
|
'kerja_tidak_sesuai_1': [
|
||||||
|
'F1601',
|
||||||
|
'f1601',
|
||||||
|
'62. Apakah pekerjaan anda saat ini tidak sesuai dengan pendidikan anda ? f1601',
|
||||||
|
],
|
||||||
|
'kerja_tidak_sesuai_2': [
|
||||||
|
'F1602',
|
||||||
|
'f1602',
|
||||||
|
'63. Apakah pekerjaan anda saat ini tidak sesuai dengan pendidikan anda ? f1602',
|
||||||
|
],
|
||||||
|
'alasan_tidak_sesuai_1': [
|
||||||
|
'F1603',
|
||||||
|
'f1603',
|
||||||
|
'64. Jika pekerjaan anda saat ini tidak sesuai dengan pendidikan anda, mengapa anda mengambilnya? f1603',
|
||||||
|
],
|
||||||
|
'alasan_tidak_sesuai_2': [
|
||||||
|
'F1604',
|
||||||
|
'f1604',
|
||||||
|
'65. Jika pekerjaan anda saat ini tidak sesuai dengan pendidikan anda, mengapa anda mengambilnya? f1604',
|
||||||
|
],
|
||||||
|
'alasan_tidak_sesuai_3': [
|
||||||
|
'F1605',
|
||||||
|
'f1605',
|
||||||
|
'66. Jika pekerjaan anda saat ini tidak sesuai dengan pendidikan anda, mengapa anda mengambilnya? f1605',
|
||||||
|
],
|
||||||
|
'alasan_tidak_sesuai_4': [
|
||||||
|
'F1606',
|
||||||
|
'f1606',
|
||||||
|
'67. Jika pekerjaan anda saat ini tidak sesuai dengan pendidikan anda, mengapa anda mengambilnya? f1606',
|
||||||
|
],
|
||||||
|
'alasan_tidak_sesuai_5': [
|
||||||
|
'F1607',
|
||||||
|
'f1607',
|
||||||
|
'68. Jika pekerjaan anda saat ini tidak sesuai dengan pendidikan anda, mengapa anda mengambilnya? f1607',
|
||||||
|
],
|
||||||
|
'alasan_tidak_sesuai_6': [
|
||||||
|
'F1608',
|
||||||
|
'f1608',
|
||||||
|
'69. Jika pekerjaan anda saat ini tidak sesuai dengan pendidikan anda, mengapa anda mengambilnya? f1608',
|
||||||
|
],
|
||||||
|
'alasan_tidak_sesuai_7': [
|
||||||
|
'F1609',
|
||||||
|
'f1609',
|
||||||
|
'70. Jika pekerjaan anda saat ini tidak sesuai dengan pendidikan anda, mengapa anda mengambilnya? f1609',
|
||||||
|
],
|
||||||
|
'alasan_tidak_sesuai_8': [
|
||||||
|
'F1610',
|
||||||
|
'f1610',
|
||||||
|
'71. Jika pekerjaan anda saat ini tidak sesuai dengan pendidikan anda, mengapa anda mengambilnya? f1610',
|
||||||
|
],
|
||||||
|
'alasan_tidak_sesuai_9': [
|
||||||
|
'F1611',
|
||||||
|
'f1611',
|
||||||
|
'72. Jika pekerjaan anda saat ini tidak sesuai dengan pendidikan anda, mengapa anda mengambilnya? f1611',
|
||||||
|
],
|
||||||
|
'alasan_tidak_sesuai_10': [
|
||||||
|
'F1612',
|
||||||
|
'f1612',
|
||||||
|
'73. Jika pekerjaan anda saat ini tidak sesuai dengan pendidikan anda, mengapa anda mengambilnya? f1612',
|
||||||
|
],
|
||||||
|
'alasan_tidak_sesuai_11': [
|
||||||
|
'F1613',
|
||||||
|
'f1613',
|
||||||
|
'74. Jika pekerjaan anda saat ini tidak sesuai dengan pendidikan anda, mengapa anda mengambilnya? Selain pilihan dijawaban soal diatas (f1613)',
|
||||||
|
],
|
||||||
|
'alasan_tidak_sesuai_lainnya': [
|
||||||
|
'F1614',
|
||||||
|
'f1614',
|
||||||
|
'75. Jika pekerjaan anda saat ini tidak sesuai dengan pendidikan anda, mengapa anda mengambilnya? Selain pilihan dijawaban soal diatas, Tuliskan (f1614)',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# ALL MAPPINGS COMBINED (untuk import)
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# Combine all tracer response mappings
|
||||||
|
TRACER_MAPPING = {}
|
||||||
|
TRACER_MAPPING.update(PEKERJAAN_MAPPING)
|
||||||
|
TRACER_MAPPING.update(STUDI_LANJUT_MAPPING)
|
||||||
|
TRACER_MAPPING.update(KESESUAIAN_MAPPING)
|
||||||
|
TRACER_MAPPING.update(KOMPETENSI_LULUS_MAPPING)
|
||||||
|
TRACER_MAPPING.update(KOMPETENSI_SAAT_INI_MAPPING)
|
||||||
|
TRACER_MAPPING.update(METODE_PEMBELAJARAN_MAPPING)
|
||||||
|
TRACER_MAPPING.update(WAKTU_CARI_KERJA_MAPPING)
|
||||||
|
TRACER_MAPPING.update(CARA_CARI_KERJA_MAPPING)
|
||||||
|
TRACER_MAPPING.update(LAMARAN_KEAKTIFAN_MAPPING)
|
||||||
|
TRACER_MAPPING.update(KESESUAIAN_PEKERJAAN_MAPPING)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# NUMERIC FIELDS (harus jadi integer)
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
NUMERIC_FIELDS = [
|
||||||
|
'bulan_dapat_kerja',
|
||||||
|
'dapet_kerja_6_bulan',
|
||||||
|
'bulan_dapat_kerja_setelah',
|
||||||
|
'jumlah_perusahaan',
|
||||||
|
'bulan_cari_sebelum_lulus',
|
||||||
|
'bulan_cari_setelah_wisuda',
|
||||||
|
'jumlah_perusahaan_dilamar',
|
||||||
|
'jumlah_perusahaan_merespons',
|
||||||
|
'jumlah_perusahaan_wawancara',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# HELPER FUNCTIONS
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
def get_all_mappings():
|
||||||
|
"""Return semua mapping dalam satu dict"""
|
||||||
|
return {
|
||||||
|
'identitas': IDENTITAS_MAPPING,
|
||||||
|
'pekerjaan': PEKERJAAN_MAPPING,
|
||||||
|
'studi_lanjut': STUDI_LANJUT_MAPPING,
|
||||||
|
'kesesuaian': KESESUAIAN_MAPPING,
|
||||||
|
'kompetensi_lulus': KOMPETENSI_LULUS_MAPPING,
|
||||||
|
'kompetensi_saat_ini': KOMPETENSI_SAAT_INI_MAPPING,
|
||||||
|
'metode_pembelajaran': METODE_PEMBELAJARAN_MAPPING,
|
||||||
|
'waktu_cari_kerja': WAKTU_CARI_KERJA_MAPPING,
|
||||||
|
'cara_cari_kerja': CARA_CARI_KERJA_MAPPING,
|
||||||
|
'lamaran_keaktifan': LAMARAN_KEAKTIFAN_MAPPING,
|
||||||
|
'kesesuaian_pekerjaan': KESESUAIAN_PEKERJAAN_MAPPING,
|
||||||
|
'tracer': TRACER_MAPPING,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_field_code(db_field: str, mapping_dict: dict) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Extract kode field (misal 'F14') dari mapping.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db_field: Nama field di database (misal 'keeratan_bidang_studi')
|
||||||
|
mapping_dict: Dict mapping yang sesuai
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Kode field (misal 'F14') atau None
|
||||||
|
"""
|
||||||
|
cols = mapping_dict.get(db_field, [])
|
||||||
|
for col in cols:
|
||||||
|
if str(col).upper().startswith('F'):
|
||||||
|
return str(col).upper()
|
||||||
|
return None
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
from typing import Optional, List, Tuple
|
||||||
|
from sqlalchemy.orm import Session, joinedload, selectinload
|
||||||
|
from sqlalchemy import or_
|
||||||
|
from app.models.alumni import Alumni
|
||||||
|
from app.repositories.base import BaseRepository
|
||||||
|
|
||||||
|
class AlumniRepository(BaseRepository[Alumni]):
|
||||||
|
"""
|
||||||
|
Alumni repository containing operations specific to the Alumni entity.
|
||||||
|
"""
|
||||||
|
def get_by_nim(self, db: Session, nim: str) -> Optional[Alumni]:
|
||||||
|
"""Find an alumni record by their NIM (student identification number)."""
|
||||||
|
return db.query(self.model).filter(self.model.nim == nim).first()
|
||||||
|
|
||||||
|
def get_with_relations(self, db: Session, alumni_id: int) -> Optional[Alumni]:
|
||||||
|
"""Fetch alumni with all related details (pekerjaan, kompetensi, tracer, clustering)."""
|
||||||
|
return (
|
||||||
|
db.query(self.model)
|
||||||
|
.options(
|
||||||
|
joinedload(self.model.pekerjaan_detail),
|
||||||
|
joinedload(self.model.kompetensi),
|
||||||
|
joinedload(self.model.tracer_response),
|
||||||
|
selectinload(self.model.clustering_results),
|
||||||
|
)
|
||||||
|
.filter(self.model.id == alumni_id)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_filtered(
|
||||||
|
self,
|
||||||
|
db: Session,
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 10,
|
||||||
|
search: Optional[str] = None,
|
||||||
|
kode_prodi: Optional[str] = None,
|
||||||
|
tahun_lulus: Optional[int] = None,
|
||||||
|
status_kerja: Optional[str] = None,
|
||||||
|
) -> Tuple[List[Alumni], int]:
|
||||||
|
"""
|
||||||
|
Fetch alumni list with multi-column search, filtering, and total count.
|
||||||
|
"""
|
||||||
|
query = db.query(self.model)
|
||||||
|
|
||||||
|
if search:
|
||||||
|
search_pattern = f"%{search.strip()}%"
|
||||||
|
query = query.filter(
|
||||||
|
or_(
|
||||||
|
self.model.nim.ilike(search_pattern),
|
||||||
|
self.model.nama.ilike(search_pattern),
|
||||||
|
self.model.pekerjaan.ilike(search_pattern),
|
||||||
|
self.model.instansi.ilike(search_pattern),
|
||||||
|
self.model.email.ilike(search_pattern),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if kode_prodi:
|
||||||
|
query = query.filter(self.model.kode_prodi == kode_prodi.strip())
|
||||||
|
|
||||||
|
if tahun_lulus:
|
||||||
|
query = query.filter(self.model.tahun_lulus == tahun_lulus)
|
||||||
|
|
||||||
|
if status_kerja:
|
||||||
|
query = query.filter(self.model.status_kerja == status_kerja.strip())
|
||||||
|
|
||||||
|
total = query.count()
|
||||||
|
alumni_list = query.order_by(self.model.id.desc()).offset(skip).limit(limit).all()
|
||||||
|
|
||||||
|
return alumni_list, total
|
||||||
|
|
||||||
|
# Instantiate alumni repository to be imported elsewhere
|
||||||
|
alumni_repository = AlumniRepository(Alumni)
|
||||||
|
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
from typing import Generic, Type, TypeVar, List, Optional, Any, Dict, Union
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.models.base import Base
|
||||||
|
|
||||||
|
# Type variable bound to SQLAlchemy Declarative Base
|
||||||
|
ModelType = TypeVar("ModelType", bound=Base)
|
||||||
|
|
||||||
|
class BaseRepository(Generic[ModelType]):
|
||||||
|
"""
|
||||||
|
Generic Base Repository containing common CRUD database operations.
|
||||||
|
Designed to reduce boilerplate code across all entities.
|
||||||
|
"""
|
||||||
|
def __init__(self, model: Type[ModelType]):
|
||||||
|
self.model = model
|
||||||
|
|
||||||
|
def get(self, db: Session, id: Any) -> Optional[ModelType]:
|
||||||
|
"""Fetch a single record by its primary key ID."""
|
||||||
|
return db.query(self.model).filter(self.model.id == id).first()
|
||||||
|
|
||||||
|
def get_all(self, db: Session, skip: int = 0, limit: int = 100) -> List[ModelType]:
|
||||||
|
"""Fetch a list of records with optional pagination limits."""
|
||||||
|
return db.query(self.model).offset(skip).limit(limit).all()
|
||||||
|
|
||||||
|
def create(self, db: Session, obj_in: Any) -> ModelType:
|
||||||
|
"""Create a new database record from a dictionary or Pydantic model."""
|
||||||
|
if hasattr(obj_in, "model_dump"):
|
||||||
|
obj_data = obj_in.model_dump()
|
||||||
|
elif hasattr(obj_in, "dict"):
|
||||||
|
obj_data = obj_in.dict()
|
||||||
|
else:
|
||||||
|
obj_data = dict(obj_in)
|
||||||
|
|
||||||
|
db_obj = self.model(**obj_data)
|
||||||
|
db.add(db_obj)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_obj)
|
||||||
|
return db_obj
|
||||||
|
|
||||||
|
def update(self, db: Session, db_obj: ModelType, obj_in: Union[Any, Dict[str, Any]]) -> ModelType:
|
||||||
|
"""Update an existing database record with new field values."""
|
||||||
|
if isinstance(obj_in, dict):
|
||||||
|
update_data = obj_in
|
||||||
|
elif hasattr(obj_in, "model_dump"):
|
||||||
|
update_data = obj_in.model_dump(exclude_unset=True)
|
||||||
|
elif hasattr(obj_in, "dict"):
|
||||||
|
update_data = obj_in.dict(exclude_unset=True)
|
||||||
|
else:
|
||||||
|
update_data = dict(obj_in)
|
||||||
|
|
||||||
|
for field in update_data:
|
||||||
|
if hasattr(db_obj, field):
|
||||||
|
setattr(db_obj, field, update_data[field])
|
||||||
|
|
||||||
|
db.add(db_obj)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_obj)
|
||||||
|
return db_obj
|
||||||
|
|
||||||
|
def delete(self, db: Session, id: Any) -> Optional[ModelType]:
|
||||||
|
"""Delete a record by its primary key ID."""
|
||||||
|
db_obj = db.query(self.model).filter(self.model.id == id).first()
|
||||||
|
if db_obj:
|
||||||
|
db.delete(db_obj)
|
||||||
|
db.commit()
|
||||||
|
return db_obj
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.models.prodi import Prodi
|
||||||
|
from app.schemas.prodi import ProdiCreate, ProdiUpdate
|
||||||
|
|
||||||
|
|
||||||
|
class ProdiRepository:
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_all(db: Session):
|
||||||
|
return db.query(Prodi).all()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_by_id(db: Session, prodi_id: int):
|
||||||
|
return db.query(Prodi).filter(Prodi.id == prodi_id).first()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create(db: Session, prodi: ProdiCreate):
|
||||||
|
db_prodi = Prodi(**prodi.model_dump())
|
||||||
|
db.add(db_prodi)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_prodi)
|
||||||
|
return db_prodi
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def update(db: Session, prodi_id: int, prodi: ProdiUpdate):
|
||||||
|
db_prodi = db.query(Prodi).filter(Prodi.id == prodi_id).first()
|
||||||
|
|
||||||
|
if not db_prodi:
|
||||||
|
return None
|
||||||
|
|
||||||
|
update_data = prodi.model_dump(exclude_unset=True)
|
||||||
|
|
||||||
|
for key, value in update_data.items():
|
||||||
|
setattr(db_prodi, key, value)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_prodi)
|
||||||
|
return db_prodi
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def delete(db: Session, prodi_id: int):
|
||||||
|
db_prodi = db.query(Prodi).filter(Prodi.id == prodi_id).first()
|
||||||
|
|
||||||
|
if not db_prodi:
|
||||||
|
return None
|
||||||
|
|
||||||
|
db.delete(db_prodi)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return db_prodi
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
from typing import Optional
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.models.user import User
|
||||||
|
from app.repositories.base import BaseRepository
|
||||||
|
|
||||||
|
class UserRepository(BaseRepository[User]):
|
||||||
|
"""
|
||||||
|
User repository containing operations specific to the User entity.
|
||||||
|
"""
|
||||||
|
def get_by_username(self, db: Session, username: str) -> Optional[User]:
|
||||||
|
"""Find a user by their username."""
|
||||||
|
return db.query(self.model).filter(self.model.username == username).first()
|
||||||
|
|
||||||
|
def get_by_email(self, db: Session, email: str) -> Optional[User]:
|
||||||
|
"""Find a user by their email address."""
|
||||||
|
return db.query(self.model).filter(self.model.email == email).first()
|
||||||
|
|
||||||
|
# Instantiate user repository to be imported elsewhere
|
||||||
|
user_repository = UserRepository(User)
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
from typing import Optional
|
||||||
|
from fastapi import APIRouter, Depends, Query, status
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.config.database import get_db
|
||||||
|
from app.schemas.alumni import (
|
||||||
|
AlumniCreate,
|
||||||
|
AlumniUpdate,
|
||||||
|
PublicTracerLookupRequest,
|
||||||
|
PublicTracerSubmitRequest,
|
||||||
|
)
|
||||||
|
from app.controllers.alumni import AlumniController
|
||||||
|
from app.middleware.auth_middleware import get_current_active_user
|
||||||
|
|
||||||
|
# Create Router instance for alumni endpoints
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.get("/", summary="Ambil semua data alumni dengan filter dan pagination")
|
||||||
|
def get_all(
|
||||||
|
skip: int = Query(0, ge=0, description="Jumlah data yang dilewati (offset)"),
|
||||||
|
limit: int = Query(10, ge=1, le=500, description="Jumlah data per halaman"),
|
||||||
|
search: Optional[str] = Query(None, description="Cari berdasarkan NIM, Nama, Pekerjaan, Instansi, atau Email"),
|
||||||
|
kode_prodi: Optional[str] = Query(None, description="Filter berdasarkan kode prodi (misal: 55301, 57401, 56401)"),
|
||||||
|
tahun_lulus: Optional[int] = Query(None, description="Filter berdasarkan tahun lulus / angkatan"),
|
||||||
|
status_kerja: Optional[str] = Query(None, description="Filter berdasarkan status pekerjaan"),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Mengambil data alumni dengan dukungan filter multi-kriteria dan pagination.
|
||||||
|
"""
|
||||||
|
return AlumniController.get_all_alumni(
|
||||||
|
db=db,
|
||||||
|
skip=skip,
|
||||||
|
limit=limit,
|
||||||
|
search=search,
|
||||||
|
kode_prodi=kode_prodi,
|
||||||
|
tahun_lulus=tahun_lulus,
|
||||||
|
status_kerja=status_kerja,
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.get("/{alumni_id}", summary="Ambil detail lengkap alumni berdasarkan ID")
|
||||||
|
def get_by_id(alumni_id: int, db: Session = Depends(get_db)):
|
||||||
|
"""
|
||||||
|
Mengambil detail lengkap data satu alumni beserta pekerjaan, kompetensi,
|
||||||
|
jawaban kuesioner tracer study, dan hasil klasterisasi.
|
||||||
|
"""
|
||||||
|
return AlumniController.get_alumni(db, alumni_id)
|
||||||
|
|
||||||
|
@router.post("/public-tracer-lookup", summary="Cek status NIM & verifikasi kepemilikan Email untuk Tracer Study")
|
||||||
|
def public_tracer_lookup(
|
||||||
|
lookup_in: PublicTracerLookupRequest,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Endpoint publik untuk kuesioner mandiri alumni:
|
||||||
|
Mengecek apakah NIM sudah terdaftar, dan memvalidasi kecocokan email jika ingin mengedit data.
|
||||||
|
"""
|
||||||
|
return AlumniController.public_tracer_lookup(db, lookup_in)
|
||||||
|
|
||||||
|
@router.post("/public-tracer-submit", summary="Kirim atau perbarui kuesioner Tracer Study oleh Alumni (Publik)")
|
||||||
|
def public_tracer_submit(
|
||||||
|
submit_in: PublicTracerSubmitRequest,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Endpoint publik untuk alumni mengirim respons kuesioner (Create/Update).
|
||||||
|
"""
|
||||||
|
return AlumniController.public_tracer_submit(db, submit_in)
|
||||||
|
|
||||||
|
@router.post("/", status_code=status.HTTP_201_CREATED, summary="Tambah data alumni baru")
|
||||||
|
def create(
|
||||||
|
alumni_in: AlumniCreate,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Menambahkan data alumni baru ke database.
|
||||||
|
Membutuhkan autentikasi token Bearer JWT.
|
||||||
|
"""
|
||||||
|
return AlumniController.create_alumni(db, alumni_in)
|
||||||
|
|
||||||
|
@router.put("/{alumni_id}", summary="Perbarui data alumni")
|
||||||
|
def update(
|
||||||
|
alumni_id: int,
|
||||||
|
alumni_in: AlumniUpdate,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Memperbarui field data alumni berdasarkan ID.
|
||||||
|
Membutuhkan autentikasi token Bearer JWT.
|
||||||
|
"""
|
||||||
|
return AlumniController.update_alumni(db, alumni_id, alumni_in)
|
||||||
|
|
||||||
|
@router.delete("/{alumni_id}", summary="Hapus data alumni")
|
||||||
|
def delete(
|
||||||
|
alumni_id: int,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Menghapus data alumni berdasarkan ID dari database.
|
||||||
|
Membutuhkan autentikasi token Bearer JWT.
|
||||||
|
"""
|
||||||
|
return AlumniController.delete_alumni(db, alumni_id)
|
||||||
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from app.routers import auth, alumni, prodi, clustering, clustering_results, keywords, dashboard, import_data
|
||||||
|
|
||||||
|
# Base API Router instance to compile sub-routers
|
||||||
|
api_router = APIRouter()
|
||||||
|
|
||||||
|
# Register sub-routers with prefixes and API tags
|
||||||
|
api_router.include_router(auth.router, prefix="/auth", tags=["Authentication"])
|
||||||
|
api_router.include_router(alumni.router, prefix="/alumni", tags=["Alumni"])
|
||||||
|
api_router.include_router(prodi.router, prefix="/prodi", tags=["Prodi"])
|
||||||
|
api_router.include_router(clustering.router, prefix="/clustering", tags=["Clustering ML"])
|
||||||
|
api_router.include_router(clustering_results.router, prefix="/clustering", tags=["Clustering Results"])
|
||||||
|
api_router.include_router(keywords.router, prefix="/keywords", tags=["Keywords"])
|
||||||
|
api_router.include_router(dashboard.router, prefix="/dashboard", tags=["Dashboard"])
|
||||||
|
api_router.include_router(import_data.router, prefix="/import", tags=["Import Data"])
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.config.database import get_db
|
||||||
|
from app.schemas.user import UserCreate, UserUpdate, UserLogin
|
||||||
|
from app.controllers.auth import AuthController
|
||||||
|
|
||||||
|
# Create Router instance for auth endpoints
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.post("/register", summary="Registrasi user baru")
|
||||||
|
def register(user_in: UserCreate, db: Session = Depends(get_db)):
|
||||||
|
"""
|
||||||
|
Endpoint untuk melakukan registrasi user baru.
|
||||||
|
"""
|
||||||
|
return AuthController.register_user(db, user_in)
|
||||||
|
|
||||||
|
@router.post("/login", summary="Login user untuk mendapatkan JWT token")
|
||||||
|
def login(login_in: UserLogin, db: Session = Depends(get_db)):
|
||||||
|
"""
|
||||||
|
Endpoint untuk login user.
|
||||||
|
"""
|
||||||
|
return AuthController.login_user(db, login_in.username, login_in.password)
|
||||||
|
|
||||||
|
@router.get("/users", summary="Daftar semua pengguna / Kaprodi")
|
||||||
|
def get_users(db: Session = Depends(get_db)):
|
||||||
|
"""
|
||||||
|
Endpoint untuk mengambil seluruh data pengguna sistem (Admin & Kaprodi).
|
||||||
|
"""
|
||||||
|
return AuthController.get_users(db)
|
||||||
|
|
||||||
|
@router.post("/users", summary="Tambah pengguna baru oleh Admin")
|
||||||
|
def create_user(user_in: UserCreate, db: Session = Depends(get_db)):
|
||||||
|
"""
|
||||||
|
Endpoint untuk membuat akun pengguna / Kaprodi baru.
|
||||||
|
"""
|
||||||
|
return AuthController.register_user(db, user_in)
|
||||||
|
|
||||||
|
@router.put("/users/{user_id}", summary="Perbarui data pengguna")
|
||||||
|
def update_user(user_id: int, user_in: UserUpdate, db: Session = Depends(get_db)):
|
||||||
|
"""
|
||||||
|
Endpoint untuk mengedit profil, role, prodi, status aktif, atau password user.
|
||||||
|
"""
|
||||||
|
return AuthController.update_user(db, user_id, user_in)
|
||||||
|
|
||||||
|
@router.delete("/users/{user_id}", summary="Hapus pengguna")
|
||||||
|
def delete_user(user_id: int, db: Session = Depends(get_db)):
|
||||||
|
"""
|
||||||
|
Endpoint untuk menghapus akun pengguna (selain akun super admin utama).
|
||||||
|
"""
|
||||||
|
return AuthController.delete_user(db, user_id)
|
||||||
|
|
||||||
|
|
@ -0,0 +1,364 @@
|
||||||
|
"""
|
||||||
|
Router untuk Clustering Endpoint
|
||||||
|
"""
|
||||||
|
|
||||||
|
import math
|
||||||
|
from datetime import datetime
|
||||||
|
from fastapi import APIRouter, UploadFile, File, HTTPException, Depends, Query, Body
|
||||||
|
from fastapi.responses import JSONResponse, StreamingResponse
|
||||||
|
from typing import List, Optional, Dict, Any
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy import or_
|
||||||
|
import pandas as pd
|
||||||
|
import io
|
||||||
|
|
||||||
|
from app.config.database import get_db
|
||||||
|
from app.models.models import ClusteringResult, ModelEvaluation
|
||||||
|
from app.models.alumni import Alumni
|
||||||
|
from app.schemas.clustering import (
|
||||||
|
ClusteringRequest,
|
||||||
|
ClusteringResponse,
|
||||||
|
ClusterResult,
|
||||||
|
AlumniInput,
|
||||||
|
StandardizeRequest,
|
||||||
|
StandardizeResponse
|
||||||
|
)
|
||||||
|
from app.services.ml_service import get_ml_service
|
||||||
|
from app.utils.standardization import ColumnStandardizer, merge_datasets, clean_data
|
||||||
|
|
||||||
|
router = APIRouter(tags=["Clustering ML"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/process", summary="Jalankan Pipeline Clustering dari Database MySQL")
|
||||||
|
@router.post("/run-db", summary="Alias Jalankan Pipeline Clustering dari Database")
|
||||||
|
def process_clustering_db(
|
||||||
|
tahun_lulus: Optional[int] = Query(None, description="Filter tahun kelulusan / angkatan (opsional)"),
|
||||||
|
n_clusters: int = Query(4, ge=2, le=10, description="Jumlah cluster K-Means"),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Menjalankan proses Machine Learning Clustering (TF-IDF + K-Means K=4 + Interpretasi C0-C3)
|
||||||
|
langsung dari data alumni & pekerjaan yang ada di database MySQL.
|
||||||
|
Hasil otomatis disimpan ke tabel clustering_result dan model_evaluation.
|
||||||
|
"""
|
||||||
|
ml_service = get_ml_service()
|
||||||
|
result = ml_service.process_clustering_from_db(
|
||||||
|
db=db,
|
||||||
|
tahun_lulus=tahun_lulus,
|
||||||
|
n_clusters=n_clusters
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.get("status") == "error":
|
||||||
|
raise HTTPException(status_code=400, detail=result.get("message"))
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/results", summary="Ambil Hasil Clustering dengan Filter & Pagination")
|
||||||
|
def get_clustering_results(
|
||||||
|
skip: int = Query(0, ge=0),
|
||||||
|
limit: int = Query(10, ge=1, le=500),
|
||||||
|
search: Optional[str] = Query(None, description="Cari nama atau NIM"),
|
||||||
|
kategori: Optional[str] = Query(None, description="Filter kategori C0, C1, C2, C3"),
|
||||||
|
kode_prodi: Optional[str] = Query(None, description="Filter kode prodi"),
|
||||||
|
tahun_lulus: Optional[int] = Query(None, description="Filter tahun lulus"),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Mengambil data hasil clustering alumni dari database lengkap dengan filter kategori C0-C3,
|
||||||
|
prodi, tahun, dan pencarian nama/NIM.
|
||||||
|
"""
|
||||||
|
from app.models.models import Pekerjaan
|
||||||
|
|
||||||
|
query = db.query(
|
||||||
|
ClusteringResult,
|
||||||
|
Alumni.nim,
|
||||||
|
Alumni.nama,
|
||||||
|
Alumni.kode_prodi,
|
||||||
|
Alumni.tahun_lulus,
|
||||||
|
Alumni.pekerjaan,
|
||||||
|
Alumni.instansi,
|
||||||
|
Alumni.status_kerja,
|
||||||
|
Pekerjaan.status_pekerjaan,
|
||||||
|
Pekerjaan.jenis_perusahaan,
|
||||||
|
Pekerjaan.nama_perusahaan,
|
||||||
|
Pekerjaan.jabatan,
|
||||||
|
Pekerjaan.teks_pekerjaan
|
||||||
|
).join(Alumni, ClusteringResult.alumni_id == Alumni.id)\
|
||||||
|
.outerjoin(Pekerjaan, Alumni.id == Pekerjaan.alumni_id)
|
||||||
|
|
||||||
|
if search:
|
||||||
|
s = f"%{search.strip()}%"
|
||||||
|
query = query.filter(or_(Alumni.nama.ilike(s), Alumni.nim.ilike(s), Alumni.pekerjaan.ilike(s), Pekerjaan.teks_pekerjaan.ilike(s)))
|
||||||
|
|
||||||
|
if kategori:
|
||||||
|
query = query.filter(ClusteringResult.kategori == kategori.strip().upper())
|
||||||
|
|
||||||
|
if kode_prodi:
|
||||||
|
query = query.filter(Alumni.kode_prodi == str(kode_prodi).strip())
|
||||||
|
|
||||||
|
if tahun_lulus:
|
||||||
|
query = query.filter(Alumni.tahun_lulus == int(tahun_lulus))
|
||||||
|
|
||||||
|
total = query.count()
|
||||||
|
rows = query.order_by(ClusteringResult.id.desc()).offset(skip).limit(limit).all()
|
||||||
|
|
||||||
|
items = []
|
||||||
|
for r in rows:
|
||||||
|
cr = r[0]
|
||||||
|
pek_nama = r[10] if (r[10] and r[10].strip() not in ['', 'nan', 'None']) else (r[5] or "-")
|
||||||
|
pek_jab = r[11] if (r[11] and r[11].strip() not in ['', 'nan', 'None']) else (r[6] or "-")
|
||||||
|
pek_status = r[8] if (r[8] and r[8].strip() not in ['', 'nan', 'None']) else (r[7] or "-")
|
||||||
|
pek_jenis = r[9] if (r[9] and r[9].strip() not in ['', 'nan', 'None']) else "-"
|
||||||
|
|
||||||
|
pek_teks = r[12] if (r[12] and r[12].strip() not in ['', 'nan', 'None']) else f"{pek_status} {pek_jenis} {pek_nama} {pek_jab}".replace('-', '').strip()
|
||||||
|
if not pek_teks:
|
||||||
|
pek_teks = "-"
|
||||||
|
|
||||||
|
items.append({
|
||||||
|
"id": cr.id,
|
||||||
|
"alumni_id": cr.alumni_id,
|
||||||
|
"nim": r[1],
|
||||||
|
"nama": r[2],
|
||||||
|
"kode_prodi": r[3],
|
||||||
|
"tahun_lulus": r[4],
|
||||||
|
"status_pekerjaan": pek_status,
|
||||||
|
"jenis_perusahaan": pek_jenis,
|
||||||
|
"nama_perusahaan": pek_nama,
|
||||||
|
"jabatan": pek_jab,
|
||||||
|
"teks_pekerjaan": pek_teks,
|
||||||
|
"cluster": cr.cluster,
|
||||||
|
"kategori": cr.kategori,
|
||||||
|
"profile_match": cr.profile_match,
|
||||||
|
"matched_keyword": cr.matched_keyword or "",
|
||||||
|
"model_version": cr.model_version,
|
||||||
|
"created_at": cr.created_at
|
||||||
|
})
|
||||||
|
|
||||||
|
page = (skip // limit) + 1 if limit > 0 else 1
|
||||||
|
total_pages = math.ceil(total / limit) if limit > 0 else 1
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"data": {
|
||||||
|
"items": items,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"limit": limit,
|
||||||
|
"total_pages": total_pages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/summary", summary="Ringkasan Statistik Hasil Clustering Terkini")
|
||||||
|
def get_clustering_summary(
|
||||||
|
tahun_lulus: Optional[int] = Query(None, description="Filter tahun kelulusan / angkatan (opsional)"),
|
||||||
|
kode_prodi: Optional[str] = Query(None, description="Filter kode prodi (opsional)"),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Mengambil ringkasan statistik distribusi C0-C3 dan evaluasi model terkini."""
|
||||||
|
query = db.query(ClusteringResult).join(Alumni, Alumni.id == ClusteringResult.alumni_id)
|
||||||
|
if tahun_lulus:
|
||||||
|
query = query.filter(Alumni.tahun_lulus == int(tahun_lulus))
|
||||||
|
if kode_prodi:
|
||||||
|
query = query.filter(Alumni.kode_prodi == str(kode_prodi).strip())
|
||||||
|
|
||||||
|
from sqlalchemy import func
|
||||||
|
|
||||||
|
# Eksekusi 1 single query agregasi GROUP BY kategori
|
||||||
|
cat_counts = query.with_entities(
|
||||||
|
ClusteringResult.kategori,
|
||||||
|
func.count(ClusteringResult.id)
|
||||||
|
).group_by(ClusteringResult.kategori).all()
|
||||||
|
|
||||||
|
dist = {'C0': 0, 'C1': 0, 'C2': 0, 'C3': 0}
|
||||||
|
total = 0
|
||||||
|
for kat, cnt in cat_counts:
|
||||||
|
k_str = str(kat).upper().strip() if kat else 'C3'
|
||||||
|
if k_str in dist:
|
||||||
|
dist[k_str] = int(cnt)
|
||||||
|
total += int(cnt)
|
||||||
|
|
||||||
|
latest_eval = db.query(ModelEvaluation).order_by(ModelEvaluation.created_at.desc()).first()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total_clustered": total,
|
||||||
|
"distribution": dist,
|
||||||
|
"silhouette_score": latest_eval.silhouette_score if latest_eval else None,
|
||||||
|
"model_version": latest_eval.model_version if latest_eval else None,
|
||||||
|
"k": latest_eval.k if latest_eval else 4,
|
||||||
|
"tahun_lulus": tahun_lulus,
|
||||||
|
"kode_prodi": kode_prodi
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/iteration-details", summary="Ambil Detail Riwayat Iterasi & Perhitungan Matematis K-Means")
|
||||||
|
def get_iteration_details(
|
||||||
|
tahun_lulus: Optional[int] = Query(None, description="Filter tahun kelulusan / angkatan (opsional)"),
|
||||||
|
alumni_id: Optional[int] = Query(None, description="ID alumni spesifik untuk inspeksi perhitungan detail"),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Mengambil log lengkap iterasi perulangan K-Means, karakteristik centroid,
|
||||||
|
dan alur transformasi 5 tahap (Kalimat -> Preprocessing -> TF-IDF -> K-Means -> Klaster)
|
||||||
|
berdasarkan dataset tahun kelulusan yang dipilih.
|
||||||
|
"""
|
||||||
|
ml_service = get_ml_service()
|
||||||
|
details = ml_service.get_latest_iteration_details(db=db, tahun_lulus=tahun_lulus, target_alumni_id=alumni_id)
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"data": details
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_export_dataframe(
|
||||||
|
db: Session,
|
||||||
|
search: Optional[str] = None,
|
||||||
|
kategori: Optional[str] = None,
|
||||||
|
kode_prodi: Optional[str] = None,
|
||||||
|
tahun_lulus: Optional[int] = None
|
||||||
|
) -> pd.DataFrame:
|
||||||
|
from app.models.models import Pekerjaan
|
||||||
|
|
||||||
|
query = db.query(
|
||||||
|
ClusteringResult,
|
||||||
|
Alumni.nim,
|
||||||
|
Alumni.nama,
|
||||||
|
Alumni.kode_prodi,
|
||||||
|
Alumni.tahun_lulus,
|
||||||
|
Alumni.pekerjaan,
|
||||||
|
Alumni.instansi,
|
||||||
|
Alumni.status_kerja,
|
||||||
|
Pekerjaan.status_pekerjaan,
|
||||||
|
Pekerjaan.jenis_perusahaan,
|
||||||
|
Pekerjaan.nama_perusahaan,
|
||||||
|
Pekerjaan.jabatan,
|
||||||
|
Pekerjaan.teks_pekerjaan
|
||||||
|
).join(Alumni, ClusteringResult.alumni_id == Alumni.id)\
|
||||||
|
.outerjoin(Pekerjaan, Alumni.id == Pekerjaan.alumni_id)
|
||||||
|
|
||||||
|
if search:
|
||||||
|
s = f"%{search.strip()}%"
|
||||||
|
query = query.filter(or_(
|
||||||
|
Alumni.nama.ilike(s),
|
||||||
|
Alumni.nim.ilike(s),
|
||||||
|
Alumni.pekerjaan.ilike(s),
|
||||||
|
Pekerjaan.teks_pekerjaan.ilike(s)
|
||||||
|
))
|
||||||
|
|
||||||
|
if kategori:
|
||||||
|
query = query.filter(ClusteringResult.kategori == kategori.strip().upper())
|
||||||
|
|
||||||
|
if kode_prodi:
|
||||||
|
query = query.filter(Alumni.kode_prodi == str(kode_prodi).strip())
|
||||||
|
|
||||||
|
if tahun_lulus:
|
||||||
|
query = query.filter(Alumni.tahun_lulus == int(tahun_lulus))
|
||||||
|
|
||||||
|
rows = query.order_by(ClusteringResult.id.asc()).all()
|
||||||
|
|
||||||
|
prodi_map = {
|
||||||
|
'55301': 'D4 Teknik Informatika',
|
||||||
|
'57401': 'D3 Manajemen Informatika',
|
||||||
|
'56401': 'D3 Teknik Komputer',
|
||||||
|
'55302': 'D4 TIF Kampus Nganjuk',
|
||||||
|
'55304': 'D4 TIF Kampus Sidoarjo',
|
||||||
|
}
|
||||||
|
|
||||||
|
kategori_labels = {
|
||||||
|
'C0': 'Sesuai Profil Lulusan',
|
||||||
|
'C1': 'Bekerja, Bidang Diketahui Formal',
|
||||||
|
'C2': 'Bekerja, Kurang Teridentifikasi',
|
||||||
|
'C3': 'Belum / Tidak Bekerja'
|
||||||
|
}
|
||||||
|
|
||||||
|
export_records = []
|
||||||
|
for idx, r in enumerate(rows, start=1):
|
||||||
|
cr = r[0]
|
||||||
|
kp = str(r[3] or '').strip()
|
||||||
|
prodi_name = prodi_map.get(kp, kp)
|
||||||
|
kat = cr.kategori or '-'
|
||||||
|
kat_label = kategori_labels.get(kat, '-')
|
||||||
|
|
||||||
|
pek_nama = r[10] if (r[10] and r[10].strip() not in ['', 'nan', 'None']) else (r[5] or "-")
|
||||||
|
pek_jab = r[11] if (r[11] and r[11].strip() not in ['', 'nan', 'None']) else (r[6] or "-")
|
||||||
|
pek_status = r[8] if (r[8] and r[8].strip() not in ['', 'nan', 'None']) else (r[7] or "-")
|
||||||
|
pek_jenis = r[9] if (r[9] and r[9].strip() not in ['', 'nan', 'None']) else "-"
|
||||||
|
pek_teks = r[12] if (r[12] and r[12].strip() not in ['', 'nan', 'None']) else f"{pek_status} {pek_jenis} {pek_nama} {pek_jab}".replace('-', '').strip()
|
||||||
|
|
||||||
|
export_records.append({
|
||||||
|
"No": idx,
|
||||||
|
"NIM": r[1],
|
||||||
|
"Nama Alumni": r[2],
|
||||||
|
"Program Studi": prodi_name,
|
||||||
|
"Tahun Lulus": r[4],
|
||||||
|
"Status Kerja": pek_status,
|
||||||
|
"Nama Perusahaan / Instansi": pek_nama,
|
||||||
|
"Jabatan / Posisi": pek_jab,
|
||||||
|
"Jenis Perusahaan": pek_jenis,
|
||||||
|
"Teks Pekerjaan": pek_teks if pek_teks else "-",
|
||||||
|
"Klaster": f"Cluster {cr.cluster}",
|
||||||
|
"Kategori": kat,
|
||||||
|
"Interpretasi Relevansi": kat_label,
|
||||||
|
"Kata Kunci Cocok": cr.matched_keyword or "-"
|
||||||
|
})
|
||||||
|
|
||||||
|
return pd.DataFrame(export_records)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/export/excel", summary="Unduh Hasil Clustering ke File Excel (.xlsx)")
|
||||||
|
def export_clustering_excel(
|
||||||
|
search: Optional[str] = Query(None, description="Cari nama atau NIM"),
|
||||||
|
kategori: Optional[str] = Query(None, description="Filter kategori C0, C1, C2, C3"),
|
||||||
|
kode_prodi: Optional[str] = Query(None, description="Filter kode prodi"),
|
||||||
|
tahun_lulus: Optional[int] = Query(None, description="Filter tahun lulus"),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Ekspor hasil clustering alumni yang tersimpan di MySQL ke berkas spreadsheet Microsoft Excel (.xlsx).
|
||||||
|
Mendukung filter tahun kelulusan, program studi, kategori relevansi C0-C3, dan kata kunci pencarian.
|
||||||
|
"""
|
||||||
|
df = _build_export_dataframe(db, search, kategori, kode_prodi, tahun_lulus)
|
||||||
|
output = io.BytesIO()
|
||||||
|
with pd.ExcelWriter(output, engine='openpyxl') as writer:
|
||||||
|
df.to_excel(writer, index=False, sheet_name='Hasil Klastering')
|
||||||
|
output.seek(0)
|
||||||
|
|
||||||
|
filename = f"Hasil_Clustering_Alumni_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
||||||
|
headers = {
|
||||||
|
'Content-Disposition': f'attachment; filename="{filename}"'
|
||||||
|
}
|
||||||
|
return StreamingResponse(
|
||||||
|
output,
|
||||||
|
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
headers=headers
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/export/csv", summary="Unduh Hasil Clustering ke File CSV (.csv)")
|
||||||
|
def export_clustering_csv(
|
||||||
|
search: Optional[str] = Query(None, description="Cari nama atau NIM"),
|
||||||
|
kategori: Optional[str] = Query(None, description="Filter kategori C0, C1, C2, C3"),
|
||||||
|
kode_prodi: Optional[str] = Query(None, description="Filter kode prodi"),
|
||||||
|
tahun_lulus: Optional[int] = Query(None, description="Filter tahun lulus"),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Ekspor hasil clustering alumni yang tersimpan di MySQL ke berkas CSV dengan encoding UTF-8.
|
||||||
|
"""
|
||||||
|
df = _build_export_dataframe(db, search, kategori, kode_prodi, tahun_lulus)
|
||||||
|
output = io.BytesIO()
|
||||||
|
df.to_csv(output, index=False, encoding='utf-8-sig')
|
||||||
|
output.seek(0)
|
||||||
|
|
||||||
|
filename = f"Hasil_Clustering_Alumni_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
||||||
|
headers = {
|
||||||
|
'Content-Disposition': f'attachment; filename="{filename}"'
|
||||||
|
}
|
||||||
|
return StreamingResponse(
|
||||||
|
output,
|
||||||
|
media_type='text/csv; charset=utf-8',
|
||||||
|
headers=headers
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,231 @@
|
||||||
|
"""
|
||||||
|
Router untuk CRUD Clustering Results
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from app.config.database import get_db
|
||||||
|
from app.schemas.models import (
|
||||||
|
ClusteringResultCreate,
|
||||||
|
ClusteringResultResponse,
|
||||||
|
ModelEvaluationCreate,
|
||||||
|
ModelEvaluationResponse,
|
||||||
|
BulkClusteringResult
|
||||||
|
)
|
||||||
|
from app.models.models import ClusteringResult, ModelEvaluation
|
||||||
|
from app.models.alumni import Alumni
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/clustering", tags=["Clustering Results"])
|
||||||
|
|
||||||
|
|
||||||
|
# ============== CLUSTERING RESULTS ==============
|
||||||
|
|
||||||
|
@router.get("/results", response_model=List[ClusteringResultResponse])
|
||||||
|
def get_all_results(
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 100,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Ambil semua hasil clustering"""
|
||||||
|
results = db.query(ClusteringResult).offset(skip).limit(limit).all()
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/results/{alumni_id}", response_model=ClusteringResultResponse)
|
||||||
|
def get_result_by_alumni(
|
||||||
|
alumni_id: int,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Ambil hasil clustering berdasarkan alumni_id"""
|
||||||
|
result = db.query(ClusteringResult).filter(
|
||||||
|
ClusteringResult.alumni_id == alumni_id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
raise HTTPException(status_code=404, detail="Hasil clustering tidak ditemukan")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/results", response_model=ClusteringResultResponse)
|
||||||
|
def create_result(
|
||||||
|
result_in: ClusteringResultCreate,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Tambah hasil clustering"""
|
||||||
|
# Cek apakah alumni exists
|
||||||
|
alumni = db.query(Alumni).filter(Alumni.id == result_in.alumni_id).first()
|
||||||
|
if not alumni:
|
||||||
|
raise HTTPException(status_code=404, detail="Alumni tidak ditemukan")
|
||||||
|
|
||||||
|
# Cek apakah sudah ada hasil untuk alumni ini
|
||||||
|
existing = db.query(ClusteringResult).filter(
|
||||||
|
ClusteringResult.alumni_id == result_in.alumni_id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
raise HTTPException(status_code=400, detail="Hasil clustering sudah ada untuk alumni ini")
|
||||||
|
|
||||||
|
result = ClusteringResult(**result_in.model_dump())
|
||||||
|
db.add(result)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/results/bulk")
|
||||||
|
def create_bulk_results(
|
||||||
|
bulk_data: BulkClusteringResult,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Simpan hasil clustering dan evaluasi model"""
|
||||||
|
try:
|
||||||
|
# 1. Simpan evaluasi model
|
||||||
|
eval_data = {
|
||||||
|
"model_version": bulk_data.model_version,
|
||||||
|
"k": bulk_data.k,
|
||||||
|
"inertia": bulk_data.inertia,
|
||||||
|
"silhouette_score": bulk_data.silhouette_score
|
||||||
|
}
|
||||||
|
evaluation = ModelEvaluation(**eval_data)
|
||||||
|
db.add(evaluation)
|
||||||
|
db.flush()
|
||||||
|
|
||||||
|
# 2. Simpan hasil clustering per alumni
|
||||||
|
results_saved = 0
|
||||||
|
for item in bulk_data.results:
|
||||||
|
# Cek apakah alumni exists
|
||||||
|
alumni = db.query(Alumni).filter(Alumni.id == item["alumni_id"]).first()
|
||||||
|
if not alumni:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Hapus hasil lama jika ada
|
||||||
|
old = db.query(ClusteringResult).filter(
|
||||||
|
ClusteringResult.alumni_id == item["alumni_id"]
|
||||||
|
).first()
|
||||||
|
if old:
|
||||||
|
db.delete(old)
|
||||||
|
|
||||||
|
# Simpan hasil baru
|
||||||
|
result_data = {
|
||||||
|
"alumni_id": item["alumni_id"],
|
||||||
|
"model_version": bulk_data.model_version,
|
||||||
|
"k": bulk_data.k,
|
||||||
|
"cluster": item["cluster"],
|
||||||
|
"kategori": item["kategori"],
|
||||||
|
"profile_match": item.get("profile_match", False),
|
||||||
|
"matched_keyword": item.get("matched_keyword", "")
|
||||||
|
}
|
||||||
|
result = ClusteringResult(**result_data)
|
||||||
|
db.add(result)
|
||||||
|
results_saved += 1
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"model_evaluation_id": evaluation.id,
|
||||||
|
"results_saved": results_saved
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/results/{alumni_id}")
|
||||||
|
def delete_result(
|
||||||
|
alumni_id: int,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Hapus hasil clustering berdasarkan alumni_id"""
|
||||||
|
result = db.query(ClusteringResult).filter(
|
||||||
|
ClusteringResult.alumni_id == alumni_id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
raise HTTPException(status_code=404, detail="Hasil clustering tidak ditemukan")
|
||||||
|
|
||||||
|
db.delete(result)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return {"message": "Hasil clustering berhasil dihapus"}
|
||||||
|
|
||||||
|
|
||||||
|
# ============== MODEL EVALUATIONS ==============
|
||||||
|
|
||||||
|
@router.get("/evaluations", response_model=List[ModelEvaluationResponse])
|
||||||
|
def get_all_evaluations(
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 10,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Ambil semua evaluasi model"""
|
||||||
|
evaluations = db.query(ModelEvaluation).order_by(
|
||||||
|
ModelEvaluation.created_at.desc()
|
||||||
|
).offset(skip).limit(limit).all()
|
||||||
|
return evaluations
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/evaluations/latest", response_model=ModelEvaluationResponse)
|
||||||
|
def get_latest_evaluation(db: Session = Depends(get_db)):
|
||||||
|
"""Ambil evaluasi model terbaru"""
|
||||||
|
evaluation = db.query(ModelEvaluation).order_by(
|
||||||
|
ModelEvaluation.created_at.desc()
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not evaluation:
|
||||||
|
raise HTTPException(status_code=404, detail="Tidak ada evaluasi model")
|
||||||
|
|
||||||
|
return evaluation
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/evaluations", response_model=ModelEvaluationResponse)
|
||||||
|
def create_evaluation(
|
||||||
|
eval_in: ModelEvaluationCreate,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Tambah evaluasi model baru"""
|
||||||
|
evaluation = ModelEvaluation(**eval_in.model_dump())
|
||||||
|
db.add(evaluation)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(evaluation)
|
||||||
|
return evaluation
|
||||||
|
|
||||||
|
|
||||||
|
# ============== STATISTIK ==============
|
||||||
|
|
||||||
|
@router.get("/stats")
|
||||||
|
def get_clustering_stats(db: Session = Depends(get_db)):
|
||||||
|
"""Ambil statistik hasil clustering"""
|
||||||
|
results = db.query(ClusteringResult).all()
|
||||||
|
|
||||||
|
if not results:
|
||||||
|
return {
|
||||||
|
"total": 0,
|
||||||
|
"by_kategori": {},
|
||||||
|
"by_cluster": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Count by kategori
|
||||||
|
kategori_count = {}
|
||||||
|
for r in results:
|
||||||
|
kategori_count[r.kategori] = kategori_count.get(r.kategori, 0) + 1
|
||||||
|
|
||||||
|
# Count by cluster
|
||||||
|
cluster_count = {}
|
||||||
|
for r in results:
|
||||||
|
cluster_count[f"Cluster_{r.cluster}"] = cluster_count.get(f"Cluster_{r.cluster}", 0) + 1
|
||||||
|
|
||||||
|
# Count by prodi
|
||||||
|
prodi_kategori = {}
|
||||||
|
alumni_with_results = db.query(Alumni).join(ClusteringResult).all()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total": len(results),
|
||||||
|
"by_kategori": kategori_count,
|
||||||
|
"by_cluster": cluster_count,
|
||||||
|
"model_version": results[0].model_version if results else None
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,539 @@
|
||||||
|
"""
|
||||||
|
Router untuk Dashboard API - PRD Section 22
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, Query
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy import func, or_
|
||||||
|
from typing import Optional, List, Dict, Any
|
||||||
|
|
||||||
|
from app.config.database import get_db
|
||||||
|
from app.models.alumni import Alumni
|
||||||
|
from app.models.models import Pekerjaan, Kompetensi, ClusteringResult, ModelEvaluation, TracerResponse
|
||||||
|
|
||||||
|
router = APIRouter(tags=["Dashboard"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/stats")
|
||||||
|
def get_dashboard_stats(
|
||||||
|
kode_prodi: Optional[str] = Query(None),
|
||||||
|
tahun_lulus: Optional[int] = Query(None),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Dashboard Utama - PRD Section 22
|
||||||
|
Menampilkan: total alumni, total bekerja, total belum bekerja, distribusi C0-C3
|
||||||
|
Mendukung filter prodi (khusus role Kaprodi atau Admin) dan filter angkatan/tahun_lulus
|
||||||
|
"""
|
||||||
|
prodi_filter = kode_prodi if (kode_prodi and kode_prodi != "all") else None
|
||||||
|
|
||||||
|
# Total alumni
|
||||||
|
q_alumni = db.query(Alumni)
|
||||||
|
if prodi_filter:
|
||||||
|
q_alumni = q_alumni.filter(Alumni.kode_prodi == prodi_filter)
|
||||||
|
if tahun_lulus:
|
||||||
|
q_alumni = q_alumni.filter(Alumni.tahun_lulus == int(tahun_lulus))
|
||||||
|
total_alumni = q_alumni.count()
|
||||||
|
|
||||||
|
# Total hasil clustering
|
||||||
|
q_cluster = db.query(ClusteringResult).join(Alumni, Alumni.id == ClusteringResult.alumni_id)
|
||||||
|
if prodi_filter:
|
||||||
|
q_cluster = q_cluster.filter(Alumni.kode_prodi == prodi_filter)
|
||||||
|
if tahun_lulus:
|
||||||
|
q_cluster = q_cluster.filter(Alumni.tahun_lulus == int(tahun_lulus))
|
||||||
|
total_clustered = q_cluster.count()
|
||||||
|
|
||||||
|
# Distribusi kategori C0-C3
|
||||||
|
q_kategori = db.query(
|
||||||
|
ClusteringResult.kategori,
|
||||||
|
func.count(ClusteringResult.id).label('count')
|
||||||
|
).join(Alumni, Alumni.id == ClusteringResult.alumni_id)
|
||||||
|
if prodi_filter:
|
||||||
|
q_kategori = q_kategori.filter(Alumni.kode_prodi == prodi_filter)
|
||||||
|
if tahun_lulus:
|
||||||
|
q_kategori = q_kategori.filter(Alumni.tahun_lulus == int(tahun_lulus))
|
||||||
|
kategori_dist = q_kategori.group_by(ClusteringResult.kategori).all()
|
||||||
|
|
||||||
|
kategori_dict = {k: c for k, c in kategori_dist}
|
||||||
|
|
||||||
|
# Ambil model evaluation terbaru
|
||||||
|
latest_eval = db.query(ModelEvaluation).order_by(
|
||||||
|
ModelEvaluation.created_at.desc()
|
||||||
|
).first()
|
||||||
|
|
||||||
|
# Get unique tahun lulus
|
||||||
|
q_tahun = db.query(Alumni.tahun_lulus)
|
||||||
|
if prodi_filter:
|
||||||
|
q_tahun = q_tahun.filter(Alumni.kode_prodi == prodi_filter)
|
||||||
|
tahun_list = q_tahun.distinct().order_by(Alumni.tahun_lulus.desc()).all()
|
||||||
|
tahun_lulus = [t[0] for t in tahun_list if t[0]]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total_alumni": total_alumni,
|
||||||
|
"total_clustered": total_clustered,
|
||||||
|
"kategori": {
|
||||||
|
"C0": kategori_dict.get("C0", 0),
|
||||||
|
"C1": kategori_dict.get("C1", 0),
|
||||||
|
"C2": kategori_dict.get("C2", 0),
|
||||||
|
"C3": kategori_dict.get("C3", 0),
|
||||||
|
},
|
||||||
|
"silhouette_score": latest_eval.silhouette_score if latest_eval else None,
|
||||||
|
"model_version": latest_eval.model_version if latest_eval else None,
|
||||||
|
"tahun_lulus": tahun_lulus # Include tahun for dropdown
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/by-prodi")
|
||||||
|
def get_stats_by_prodi(
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Distribusi alumni dan kategori per prodi"""
|
||||||
|
# Get alumni count by prodi
|
||||||
|
prodi_stats = db.query(
|
||||||
|
Alumni.kode_prodi,
|
||||||
|
func.count(Alumni.id).label('total')
|
||||||
|
).group_by(Alumni.kode_prodi).all()
|
||||||
|
|
||||||
|
# Get clustering results by prodi and kategori
|
||||||
|
results = db.query(
|
||||||
|
Alumni.kode_prodi,
|
||||||
|
ClusteringResult.kategori,
|
||||||
|
func.count(ClusteringResult.id).label('count')
|
||||||
|
).join(
|
||||||
|
ClusteringResult, Alumni.id == ClusteringResult.alumni_id
|
||||||
|
).group_by(
|
||||||
|
Alumni.kode_prodi, ClusteringResult.kategori
|
||||||
|
).all()
|
||||||
|
|
||||||
|
# Format results
|
||||||
|
prodi_data = {}
|
||||||
|
for prodi, total in prodi_stats:
|
||||||
|
prodi_data[prodi] = {"total": total, "C0": 0, "C1": 0, "C2": 0, "C3": 0}
|
||||||
|
|
||||||
|
for prodi, kategori, count in results:
|
||||||
|
if prodi in prodi_data:
|
||||||
|
prodi_data[prodi][kategori] = count
|
||||||
|
|
||||||
|
return prodi_data
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/by-year")
|
||||||
|
def get_stats_by_year(
|
||||||
|
kode_prodi: Optional[str] = Query(None),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Distribusi alumni dan tren kategori C0-C3 per tahun kelulusan secara kronologis (Ascending), mendukung filter prodi"""
|
||||||
|
prodi_filter = kode_prodi if (kode_prodi and kode_prodi != "all") else None
|
||||||
|
|
||||||
|
# 1. Total alumni per tahun lulus urut dari tahun terlama ke terbaru
|
||||||
|
q_year = db.query(
|
||||||
|
Alumni.tahun_lulus,
|
||||||
|
func.count(Alumni.id).label('total')
|
||||||
|
).filter(Alumni.tahun_lulus.isnot(None))
|
||||||
|
if prodi_filter:
|
||||||
|
q_year = q_year.filter(Alumni.kode_prodi == prodi_filter)
|
||||||
|
year_totals = q_year.group_by(Alumni.tahun_lulus).order_by(Alumni.tahun_lulus.asc()).all()
|
||||||
|
|
||||||
|
# 2. Distribusi per kategori C0, C1, C2, C3 per tahun
|
||||||
|
q_cat = db.query(
|
||||||
|
Alumni.tahun_lulus,
|
||||||
|
ClusteringResult.kategori,
|
||||||
|
func.count(ClusteringResult.id).label('count')
|
||||||
|
).join(ClusteringResult, Alumni.id == ClusteringResult.alumni_id)\
|
||||||
|
.filter(Alumni.tahun_lulus.isnot(None))
|
||||||
|
if prodi_filter:
|
||||||
|
q_cat = q_cat.filter(Alumni.kode_prodi == prodi_filter)
|
||||||
|
cat_results = q_cat.group_by(Alumni.tahun_lulus, ClusteringResult.kategori)\
|
||||||
|
.order_by(Alumni.tahun_lulus.asc()).all()
|
||||||
|
|
||||||
|
# Susun struktur data kronologis
|
||||||
|
years_list = [str(y[0]) for y in year_totals if y[0]]
|
||||||
|
breakdown_map = {str(y[0]): {"total": y[1], "C0": 0, "C1": 0, "C2": 0, "C3": 0} for y in year_totals if y[0]}
|
||||||
|
|
||||||
|
for thn, kat, cnt in cat_results:
|
||||||
|
thn_str = str(thn)
|
||||||
|
if thn_str in breakdown_map and kat in breakdown_map[thn_str]:
|
||||||
|
breakdown_map[thn_str][kat] = cnt
|
||||||
|
|
||||||
|
# Format series untuk area / line chart
|
||||||
|
c0_series = [breakdown_map[y]["C0"] for y in years_list]
|
||||||
|
c1_series = [breakdown_map[y]["C1"] for y in years_list]
|
||||||
|
c2_series = [breakdown_map[y]["C2"] for y in years_list]
|
||||||
|
c3_series = [breakdown_map[y]["C3"] for y in years_list]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"years": years_list,
|
||||||
|
"breakdown": breakdown_map,
|
||||||
|
"series": [
|
||||||
|
{"name": "C0 - Sesuai Profil", "data": c0_series, "color": "#059669"},
|
||||||
|
{"name": "C1 - Bidang Diketahui", "data": c1_series, "color": "#2563eb"},
|
||||||
|
{"name": "C2 - Kurang Teridentifikasi", "data": c2_series, "color": "#d97706"},
|
||||||
|
{"name": "C3 - Belum Bekerja", "data": c3_series, "color": "#64748b"},
|
||||||
|
],
|
||||||
|
"totals_by_year": {str(year): total for year, total in year_totals}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/by-cluster")
|
||||||
|
def get_stats_by_cluster(
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Distribusi alumni per cluster"""
|
||||||
|
cluster_stats = db.query(
|
||||||
|
ClusteringResult.cluster,
|
||||||
|
ClusteringResult.kategori,
|
||||||
|
func.count(ClusteringResult.id).label('count')
|
||||||
|
).group_by(
|
||||||
|
ClusteringResult.cluster,
|
||||||
|
ClusteringResult.kategori
|
||||||
|
).all()
|
||||||
|
|
||||||
|
# Format results
|
||||||
|
cluster_data = {}
|
||||||
|
for cluster, kategori, count in cluster_stats:
|
||||||
|
key = f"Cluster_{cluster}"
|
||||||
|
if key not in cluster_data:
|
||||||
|
cluster_data[key] = {"total": 0, "kategori": {}}
|
||||||
|
cluster_data[key]["total"] += count
|
||||||
|
cluster_data[key]["kategori"][kategori] = count
|
||||||
|
|
||||||
|
return cluster_data
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/competency-profile")
|
||||||
|
def get_competency_profile(
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Profil kompetensi rata-rata per kategori"""
|
||||||
|
# Get average competency scores by kategori
|
||||||
|
results = db.query(
|
||||||
|
ClusteringResult.kategori,
|
||||||
|
func.avg(Kompetensi.etika_lulus).label('etika'),
|
||||||
|
func.avg(Kompetensi.keahlian_lulus).label('keahlian'),
|
||||||
|
func.avg(Kompetensi.bahasa_inggris_lulus).label('bahasa_inggris'),
|
||||||
|
func.avg(Kompetensi.teknologi_informasi_lulus).label('teknologi_informasi'),
|
||||||
|
func.avg(Kompetensi.komunikasi_lulus).label('komunikasi'),
|
||||||
|
func.avg(Kompetensi.kerja_sama_lulus).label('kerja_sama'),
|
||||||
|
func.avg(Kompetensi.pengembangan_diri_lulus).label('pengembangan_diri'),
|
||||||
|
func.count(Kompetensi.id).label('count')
|
||||||
|
).join(
|
||||||
|
Kompetensi, Alumni.id == Kompetensi.alumni_id
|
||||||
|
).join(
|
||||||
|
ClusteringResult, Alumni.id == ClusteringResult.alumni_id
|
||||||
|
).group_by(
|
||||||
|
ClusteringResult.kategori
|
||||||
|
).all()
|
||||||
|
|
||||||
|
profile = {}
|
||||||
|
for r in results:
|
||||||
|
profile[r.kategori] = {
|
||||||
|
"etika": round(float(r.etika), 2) if r.etika else None,
|
||||||
|
"keahlian": round(float(r.keahlian), 2) if r.keahlian else None,
|
||||||
|
"bahasa_inggris": round(float(r.bahasa_inggris), 2) if r.bahasa_inggris else None,
|
||||||
|
"teknologi_informasi": round(float(r.teknologi_informasi), 2) if r.teknologi_informasi else None,
|
||||||
|
"komunikasi": round(float(r.komunikasi), 2) if r.komunikasi else None,
|
||||||
|
"kerja_sama": round(float(r.kerja_sama), 2) if r.kerja_sama else None,
|
||||||
|
"pengembangan_diri": round(float(r.pengembangan_diri), 2) if r.pengembangan_diri else None,
|
||||||
|
"count": r.count
|
||||||
|
}
|
||||||
|
|
||||||
|
return profile
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/company-types")
|
||||||
|
def get_top_company_types(
|
||||||
|
limit: int = Query(10, ge=1, le=50),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Top jenis perusahaan berdasarkan hasil clustering"""
|
||||||
|
results = db.query(
|
||||||
|
Pekerjaan.jenis_perusahaan,
|
||||||
|
func.count(Pekerjaan.id).label('count')
|
||||||
|
).join(
|
||||||
|
Alumni, Pekerjaan.alumni_id == Alumni.id
|
||||||
|
).join(
|
||||||
|
ClusteringResult, Alumni.id == ClusteringResult.alumni_id
|
||||||
|
).group_by(
|
||||||
|
Pekerjaan.jenis_perusahaan
|
||||||
|
).order_by(
|
||||||
|
func.count(Pekerjaan.id).desc()
|
||||||
|
).limit(limit).all()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"jenis_perusahaan": [
|
||||||
|
{"jenis": j, "count": c} for j, c in results
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/top-keywords")
|
||||||
|
def get_top_matched_keywords(
|
||||||
|
limit: int = Query(10, ge=1, le=50),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Top keywords yang match dengan kamus prodi"""
|
||||||
|
# Get all matched keywords
|
||||||
|
results = db.query(ClusteringResult.matched_keyword).filter(
|
||||||
|
ClusteringResult.matched_keyword.isnot(None),
|
||||||
|
ClusteringResult.matched_keyword != ""
|
||||||
|
).all()
|
||||||
|
|
||||||
|
# Count keyword frequency
|
||||||
|
keyword_count = {}
|
||||||
|
for r in results:
|
||||||
|
if r.matched_keyword:
|
||||||
|
# Split by comma for multiple keywords
|
||||||
|
keywords = [k.strip() for k in r.matched_keyword.split(",")]
|
||||||
|
for kw in keywords:
|
||||||
|
keyword_count[kw] = keyword_count.get(kw, 0) + 1
|
||||||
|
|
||||||
|
# Sort and limit
|
||||||
|
sorted_keywords = sorted(keyword_count.items(), key=lambda x: x[1], reverse=True)[:limit]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"keywords": [
|
||||||
|
{"keyword": kw, "count": c} for kw, c in sorted_keywords
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/akreditasi-tables")
|
||||||
|
def get_akreditasi_tables(
|
||||||
|
ts_year: Optional[int] = Query(None, description="Tahun Laporan TS (misal 2024 atau 2025)"),
|
||||||
|
kode_prodi: Optional[str] = Query(None, description="Kode program studi atau 'all'"),
|
||||||
|
mode: Optional[str] = Query("ts_range", description="'ts_range' (TS-2 s/d TS) atau 'all_years'"),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Tabel Akreditasi LAM INFOKOM (Kriteria 9 / IKU-1):
|
||||||
|
- Tabel 2.B.4: Rata-rata Masa Tunggu Lulusan untuk Bekerja Pertama Kali
|
||||||
|
- Tabel 2.B.5: Kesesuaian Bidang Kerja Lulusan (Infokom vs Non-Infokom & Lingkup Kerja)
|
||||||
|
Mendukung filter TS-2, TS-1, TS dan pemisahan per Program Studi (TIF, MIF, TKK).
|
||||||
|
"""
|
||||||
|
# 1. Dapatkan daftar tahun kelulusan yang valid (< 2026)
|
||||||
|
year_rows = db.query(Alumni.tahun_lulus)\
|
||||||
|
.filter(Alumni.tahun_lulus.isnot(None), Alumni.tahun_lulus < 2026)\
|
||||||
|
.distinct().order_by(Alumni.tahun_lulus.desc()).all()
|
||||||
|
available_years = [y[0] for y in year_rows if y[0]]
|
||||||
|
|
||||||
|
# Tentukan TS Tahun (default 2025 jika tidak disediakan, atau tahun terbaru yang valid)
|
||||||
|
if not ts_year:
|
||||||
|
ts_year = 2025 if 2025 in available_years else (available_years[0] if available_years else 2025)
|
||||||
|
|
||||||
|
# Tentukan deret tahun yang akan dihitung
|
||||||
|
if mode == "all_years":
|
||||||
|
target_years = sorted(available_years)
|
||||||
|
else:
|
||||||
|
# Standar LAM INFOKOM: 3 Tahun Lulus Terakhir (TS-2, TS-1, TS)
|
||||||
|
target_years = [ts_year - 2, ts_year - 1, ts_year]
|
||||||
|
|
||||||
|
# Kamus Nama Prodi Resmi JTI Polije
|
||||||
|
PRODI_INFO = {
|
||||||
|
"all": "Semua Program Studi (Jurusan TI)",
|
||||||
|
"55301": "D4 Teknik Informatika (TIF)",
|
||||||
|
"57401": "D3 Manajemen Informatika (MIF)",
|
||||||
|
"56401": "D3 Teknik Komputer (TKK)",
|
||||||
|
"55302": "D4 Teknik Rekayasa Komputer",
|
||||||
|
"55304": "D3 Manajemen Informatika (Kampus Lain)"
|
||||||
|
}
|
||||||
|
prodi_filter = kode_prodi if (kode_prodi and kode_prodi != "all") else None
|
||||||
|
prodi_label = PRODI_INFO.get(kode_prodi, "Semua Program Studi (Jurusan TI)") if kode_prodi else "Semua Program Studi (Jurusan TI)"
|
||||||
|
|
||||||
|
rows_2b4 = []
|
||||||
|
rows_2b5 = []
|
||||||
|
|
||||||
|
sum_lulusan = 0
|
||||||
|
sum_terlacak = 0
|
||||||
|
weighted_waktu_sum = 0.0
|
||||||
|
weighted_waktu_count = 0
|
||||||
|
|
||||||
|
sum_infokom = 0
|
||||||
|
sum_non_infokom = 0
|
||||||
|
sum_multinasional = 0
|
||||||
|
sum_nasional = 0
|
||||||
|
sum_wirausaha = 0
|
||||||
|
|
||||||
|
for year in target_years:
|
||||||
|
# Tentukan label TS (misal TS-2, TS-1, TS)
|
||||||
|
diff = ts_year - year
|
||||||
|
if diff == 0:
|
||||||
|
ts_label = "TS"
|
||||||
|
elif diff > 0:
|
||||||
|
ts_label = f"TS-{diff}"
|
||||||
|
else:
|
||||||
|
ts_label = f"TS+{abs(diff)}"
|
||||||
|
|
||||||
|
display_label = f"{ts_label} ({year})" if mode == "ts_range" else f"{year} ({ts_label})"
|
||||||
|
|
||||||
|
# A. Jumlah Lulusan
|
||||||
|
q_lulus = db.query(Alumni).filter(Alumni.tahun_lulus == year)
|
||||||
|
if prodi_filter:
|
||||||
|
q_lulus = q_lulus.filter(Alumni.kode_prodi == prodi_filter)
|
||||||
|
n_lulus = q_lulus.count()
|
||||||
|
|
||||||
|
# B. Jumlah Lulusan yang Terlacak (isi TracerResponse atau memiliki riwayat kerja)
|
||||||
|
q_terlacak = db.query(Alumni).join(TracerResponse, Alumni.id == TracerResponse.alumni_id)\
|
||||||
|
.filter(Alumni.tahun_lulus == year)
|
||||||
|
if prodi_filter:
|
||||||
|
q_terlacak = q_terlacak.filter(Alumni.kode_prodi == prodi_filter)
|
||||||
|
n_terlacak = q_terlacak.count()
|
||||||
|
|
||||||
|
# C. Rata-rata Waktu Tunggu (Bulan) — dihitung dari alumni yang bekerja dengan bulan_dapat_kerja > 0
|
||||||
|
q_waktu = db.query(
|
||||||
|
func.avg(TracerResponse.bulan_dapat_kerja),
|
||||||
|
func.count(TracerResponse.id)
|
||||||
|
).join(Alumni, Alumni.id == TracerResponse.alumni_id)\
|
||||||
|
.filter(
|
||||||
|
Alumni.tahun_lulus == year,
|
||||||
|
TracerResponse.bulan_dapat_kerja.isnot(None),
|
||||||
|
TracerResponse.bulan_dapat_kerja > 0
|
||||||
|
)
|
||||||
|
if prodi_filter:
|
||||||
|
q_waktu = q_waktu.filter(Alumni.kode_prodi == prodi_filter)
|
||||||
|
avg_waktu_res, count_waktu = q_waktu.first()
|
||||||
|
avg_waktu = round(float(avg_waktu_res), 2) if avg_waktu_res is not None else 0.0
|
||||||
|
|
||||||
|
# D. Profesi Bidang Infokom (C0)
|
||||||
|
q_c0 = db.query(ClusteringResult).join(Alumni, Alumni.id == ClusteringResult.alumni_id)\
|
||||||
|
.filter(Alumni.tahun_lulus == year, ClusteringResult.kategori == "C0")
|
||||||
|
if prodi_filter:
|
||||||
|
q_c0 = q_c0.filter(Alumni.kode_prodi == prodi_filter)
|
||||||
|
n_infokom = q_c0.count()
|
||||||
|
|
||||||
|
# E. Profesi Bidang NON Infokom (C1 + C2)
|
||||||
|
q_non_c0 = db.query(ClusteringResult).join(Alumni, Alumni.id == ClusteringResult.alumni_id)\
|
||||||
|
.filter(Alumni.tahun_lulus == year, ClusteringResult.kategori.in_(["C1", "C2"]))
|
||||||
|
if prodi_filter:
|
||||||
|
q_non_c0 = q_non_c0.filter(Alumni.kode_prodi == prodi_filter)
|
||||||
|
n_non_infokom = q_non_c0.count()
|
||||||
|
|
||||||
|
# F. Lingkup Tempat Kerja: Multinasional / Internasional
|
||||||
|
q_multi = db.query(TracerResponse).join(Alumni, Alumni.id == TracerResponse.alumni_id)\
|
||||||
|
.filter(
|
||||||
|
Alumni.tahun_lulus == year,
|
||||||
|
or_(
|
||||||
|
TracerResponse.tingkatan_tempat_kerja.ilike("%multinasional%"),
|
||||||
|
TracerResponse.tingkatan_tempat_kerja.ilike("%internasional%"),
|
||||||
|
TracerResponse.jenis_perusahaan_full.ilike("%multilateral%")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if prodi_filter:
|
||||||
|
q_multi = q_multi.filter(Alumni.kode_prodi == prodi_filter)
|
||||||
|
n_multi = q_multi.count()
|
||||||
|
|
||||||
|
# G. Lingkup Tempat Kerja: Nasional (BUMN, Instansi Pusat, Swasta Nasional)
|
||||||
|
q_nas = db.query(TracerResponse).join(Alumni, Alumni.id == TracerResponse.alumni_id)\
|
||||||
|
.filter(
|
||||||
|
Alumni.tahun_lulus == year,
|
||||||
|
TracerResponse.tingkatan_tempat_kerja.ilike("%nasional%")
|
||||||
|
)
|
||||||
|
if prodi_filter:
|
||||||
|
q_nas = q_nas.filter(Alumni.kode_prodi == prodi_filter)
|
||||||
|
n_nas = q_nas.count()
|
||||||
|
|
||||||
|
# H. Lingkup Tempat Kerja: Wirausaha
|
||||||
|
q_wira = db.query(Pekerjaan).join(Alumni, Alumni.id == Pekerjaan.alumni_id)\
|
||||||
|
.filter(
|
||||||
|
Alumni.tahun_lulus == year,
|
||||||
|
or_(
|
||||||
|
Pekerjaan.status_pekerjaan.ilike("%wiraswasta%"),
|
||||||
|
Pekerjaan.jenis_perusahaan.ilike("%wiraswasta%")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if prodi_filter:
|
||||||
|
q_wira = q_wira.filter(Alumni.kode_prodi == prodi_filter)
|
||||||
|
n_wira = q_wira.count()
|
||||||
|
|
||||||
|
# Masukkan ke baris tabel 2.B.4
|
||||||
|
rows_2b4.append({
|
||||||
|
"ts_code": ts_label,
|
||||||
|
"tahun_lulus": year,
|
||||||
|
"tahun_label": display_label,
|
||||||
|
"jumlah_lulusan": n_lulus,
|
||||||
|
"jumlah_terlacak": n_terlacak,
|
||||||
|
"persen_terlacak": round((n_terlacak / n_lulus * 100), 1) if n_lulus > 0 else 0,
|
||||||
|
"rata_waktu_tunggu": avg_waktu
|
||||||
|
})
|
||||||
|
|
||||||
|
# Masukkan ke baris tabel 2.B.5
|
||||||
|
rows_2b5.append({
|
||||||
|
"ts_code": ts_label,
|
||||||
|
"tahun_lulus": year,
|
||||||
|
"tahun_label": display_label,
|
||||||
|
"jumlah_lulusan": n_lulus,
|
||||||
|
"jumlah_terlacak": n_terlacak,
|
||||||
|
"profesi_infokom": n_infokom,
|
||||||
|
"profesi_non_infokom": n_non_infokom,
|
||||||
|
"persen_infokom": round((n_infokom / n_terlacak * 100), 1) if n_terlacak > 0 else 0,
|
||||||
|
"lingkup_multinasional": n_multi,
|
||||||
|
"lingkup_nasional": n_nas,
|
||||||
|
"lingkup_wirausaha": n_wira
|
||||||
|
})
|
||||||
|
|
||||||
|
sum_lulusan += n_lulus
|
||||||
|
sum_terlacak += n_terlacak
|
||||||
|
if avg_waktu > 0 and count_waktu:
|
||||||
|
weighted_waktu_sum += (avg_waktu * count_waktu)
|
||||||
|
weighted_waktu_count += count_waktu
|
||||||
|
|
||||||
|
sum_infokom += n_infokom
|
||||||
|
sum_non_infokom += n_non_infokom
|
||||||
|
sum_multinasional += n_multi
|
||||||
|
sum_nasional += n_nas
|
||||||
|
sum_wirausaha += n_wira
|
||||||
|
|
||||||
|
overall_avg_waktu = round(weighted_waktu_sum / weighted_waktu_count, 2) if weighted_waktu_count > 0 else 0.0
|
||||||
|
|
||||||
|
# Summary Totals
|
||||||
|
total_2b4 = {
|
||||||
|
"ts_code": "Jumlah",
|
||||||
|
"tahun_label": "Jumlah / Rata-rata",
|
||||||
|
"jumlah_lulusan": sum_lulusan,
|
||||||
|
"jumlah_terlacak": sum_terlacak,
|
||||||
|
"persen_terlacak": round((sum_terlacak / sum_lulusan * 100), 1) if sum_lulusan > 0 else 0,
|
||||||
|
"rata_waktu_tunggu": overall_avg_waktu
|
||||||
|
}
|
||||||
|
|
||||||
|
total_2b5 = {
|
||||||
|
"ts_code": "Jumlah",
|
||||||
|
"tahun_label": "Jumlah",
|
||||||
|
"jumlah_lulusan": sum_lulusan,
|
||||||
|
"jumlah_terlacak": sum_terlacak,
|
||||||
|
"profesi_infokom": sum_infokom,
|
||||||
|
"profesi_non_infokom": sum_non_infokom,
|
||||||
|
"persen_infokom": round((sum_infokom / sum_terlacak * 100), 1) if sum_terlacak > 0 else 0,
|
||||||
|
"lingkup_multinasional": sum_multinasional,
|
||||||
|
"lingkup_nasional": sum_nasional,
|
||||||
|
"lingkup_wirausaha": sum_wirausaha
|
||||||
|
}
|
||||||
|
|
||||||
|
available_prodis = [
|
||||||
|
{"kode": "all", "nama": "Semua Program Studi (Jurusan TI)"},
|
||||||
|
{"kode": "55301", "nama": "D4 Teknik Informatika (TIF)"},
|
||||||
|
{"kode": "57401", "nama": "D3 Manajemen Informatika (MIF)"},
|
||||||
|
{"kode": "56401", "nama": "D3 Teknik Komputer (TKK)"}
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"meta": {
|
||||||
|
"ts_year": ts_year,
|
||||||
|
"kode_prodi": kode_prodi or "all",
|
||||||
|
"prodi_label": prodi_label,
|
||||||
|
"mode": mode,
|
||||||
|
"standard": "Instrumen Akreditasi Program Studi (IAPS) LAM INFOKOM Kriteria 9"
|
||||||
|
},
|
||||||
|
"available_years": available_years,
|
||||||
|
"available_prodis": available_prodis,
|
||||||
|
"tabel_2b4": {
|
||||||
|
"title": "Tabel 2.B.4 Rata-rata Masa Tunggu Lulusan untuk Bekerja Pertama Kali",
|
||||||
|
"rows": rows_2b4,
|
||||||
|
"total": total_2b4
|
||||||
|
},
|
||||||
|
"tabel_2b5": {
|
||||||
|
"title": "Tabel 2.B.5. Kesesuaian Bidang Kerja Lulusan",
|
||||||
|
"rows": rows_2b5,
|
||||||
|
"total": total_2b5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
"""
|
||||||
|
Router untuk Import Data Alumni dari CSV/Excel
|
||||||
|
Mendukung semua kolom tracer study 2019-2025
|
||||||
|
|
||||||
|
File ini adalah router utama yang mendelegasikan ke import_service.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, UploadFile, File, HTTPException, Depends, Form
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy import func
|
||||||
|
|
||||||
|
from app.config.database import get_db
|
||||||
|
from app.services.import_service import import_data
|
||||||
|
from app.models.alumni import Alumni
|
||||||
|
from app.models.models import TracerResponse, Kompetensi, Pekerjaan
|
||||||
|
|
||||||
|
router = APIRouter(prefix="", tags=["Import Data"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/import-full")
|
||||||
|
async def import_full(
|
||||||
|
tahun: str = Form(...),
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Import CSV dengan SEMUA kolom tracer study.
|
||||||
|
Data akan disimpan ke tabel: alumni, pekerjaan, kompetensi, tracer_responses
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
content = await file.read()
|
||||||
|
result = import_data(tahun, content, file.filename, db)
|
||||||
|
|
||||||
|
if result["status"] == "error":
|
||||||
|
raise HTTPException(status_code=400, detail=result["message"])
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
import traceback
|
||||||
|
print(f"Import error: {traceback.format_exc()}")
|
||||||
|
raise HTTPException(status_code=400, detail=f"Error: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/stats")
|
||||||
|
async def get_import_stats(db: Session = Depends(get_db)):
|
||||||
|
"""Ambil statistik data alumni di database"""
|
||||||
|
total_alumni = db.query(Alumni).count()
|
||||||
|
total_pekerjaan = db.query(Pekerjaan).count()
|
||||||
|
total_kompetensi = db.query(Kompetensi).count()
|
||||||
|
total_tracer = db.query(TracerResponse).count()
|
||||||
|
|
||||||
|
prodi_stats = db.query(
|
||||||
|
Alumni.kode_prodi,
|
||||||
|
func.count(Alumni.id).label('count')
|
||||||
|
).group_by(Alumni.kode_prodi).all()
|
||||||
|
|
||||||
|
tahun_stats = db.query(
|
||||||
|
Alumni.tahun_lulus,
|
||||||
|
func.count(Alumni.id).label('count')
|
||||||
|
).group_by(Alumni.tahun_lulus).order_by(Alumni.tahun_lulus.desc()).all()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total_alumni": total_alumni,
|
||||||
|
"total_pekerjaan": total_pekerjaan,
|
||||||
|
"total_kompetensi": total_kompetensi,
|
||||||
|
"total_tracer_responses": total_tracer,
|
||||||
|
"by_prodi": [{"prodi": p, "count": c} for p, c in prodi_stats],
|
||||||
|
"by_tahun": [{"tahun": t, "count": c} for t, c in tahun_stats]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/template")
|
||||||
|
async def download_template():
|
||||||
|
"""Download template CSV untuk import data"""
|
||||||
|
import csv
|
||||||
|
from fastapi.responses import StreamingResponse
|
||||||
|
import io
|
||||||
|
|
||||||
|
template_data = [
|
||||||
|
["nimhsmsmh", "nmmhsmsmh", "kdpstmsmh", "tahun_lulus", "F8", "F5b", "F5c", "F1101", "F5a1", "F5a2", "F1761", "F1763", "F1765", "F1767", "F1769", "F1771", "F1773"],
|
||||||
|
["A001", "Nama Alumni 1", "57401", "2024", "1", "PT ABC", "Software Engineer", "3", "Jawa Timur", "Jember", "4", "4", "3", "4", "4", "4", "3"],
|
||||||
|
["A002", "Nama Alumni 2", "56401", "2024", "2", "", "", "", "", "", "3", "3", "3", "3", "3", "3", "3"]
|
||||||
|
]
|
||||||
|
|
||||||
|
output = io.StringIO()
|
||||||
|
writer = csv.writer(output, delimiter=';')
|
||||||
|
for row in template_data:
|
||||||
|
writer.writerow(row)
|
||||||
|
|
||||||
|
output.seek(0)
|
||||||
|
return StreamingResponse(
|
||||||
|
output,
|
||||||
|
media_type='text/csv',
|
||||||
|
headers={'Content-Disposition': 'attachment; filename=template_import_alumni.csv'}
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,202 @@
|
||||||
|
"""
|
||||||
|
Router untuk Keyword Dictionary - PRD Section 7 & 24
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from app.config.database import get_db
|
||||||
|
from app.schemas.models import (
|
||||||
|
KeywordDictionaryCreate,
|
||||||
|
KeywordDictionaryUpdate,
|
||||||
|
KeywordDictionaryResponse
|
||||||
|
)
|
||||||
|
from app.models.models import KeywordDictionary
|
||||||
|
|
||||||
|
router = APIRouter(tags=["Keyword Dictionary"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_model=List[KeywordDictionaryResponse])
|
||||||
|
def get_all_keywords(
|
||||||
|
search: Optional[str] = None,
|
||||||
|
kode_prodi: Optional[str] = None,
|
||||||
|
aktif: Optional[bool] = None,
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 500,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Ambil semua keyword dengan filter prodi, status aktif, dan pencarian kata kunci"""
|
||||||
|
query = db.query(KeywordDictionary)
|
||||||
|
|
||||||
|
if search:
|
||||||
|
query = query.filter(KeywordDictionary.keyword.ilike(f"%{search.strip()}%"))
|
||||||
|
if kode_prodi:
|
||||||
|
query = query.filter(KeywordDictionary.kode_prodi == kode_prodi.strip().upper())
|
||||||
|
if aktif is not None:
|
||||||
|
query = query.filter(KeywordDictionary.aktif == aktif)
|
||||||
|
|
||||||
|
return query.order_by(KeywordDictionary.kode_prodi.asc(), KeywordDictionary.keyword.asc()).offset(skip).limit(limit).all()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/by-prodi/{kode_prodi}", response_model=List[KeywordDictionaryResponse])
|
||||||
|
def get_keywords_by_prodi(
|
||||||
|
kode_prodi: str,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Ambil keyword berdasarkan kode prodi"""
|
||||||
|
keywords = db.query(KeywordDictionary).filter(
|
||||||
|
KeywordDictionary.kode_prodi == kode_prodi,
|
||||||
|
KeywordDictionary.aktif == True
|
||||||
|
).all()
|
||||||
|
return keywords
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/", response_model=KeywordDictionaryResponse)
|
||||||
|
def create_keyword(
|
||||||
|
keyword_in: KeywordDictionaryCreate,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Tambah keyword baru"""
|
||||||
|
# Cek apakah keyword sudah ada
|
||||||
|
existing = db.query(KeywordDictionary).filter(
|
||||||
|
KeywordDictionary.kode_prodi == keyword_in.kode_prodi,
|
||||||
|
KeywordDictionary.keyword == keyword_in.keyword
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
raise HTTPException(status_code=400, detail="Keyword sudah ada")
|
||||||
|
|
||||||
|
keyword = KeywordDictionary(**keyword_in.model_dump())
|
||||||
|
db.add(keyword)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(keyword)
|
||||||
|
return keyword
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/bulk")
|
||||||
|
def create_bulk_keywords(
|
||||||
|
keywords: List[KeywordDictionaryCreate],
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Tambah multiple keywords sekaligus"""
|
||||||
|
added = 0
|
||||||
|
for kw in keywords:
|
||||||
|
existing = db.query(KeywordDictionary).filter(
|
||||||
|
KeywordDictionary.kode_prodi == kw.kode_prodi,
|
||||||
|
KeywordDictionary.keyword == kw.keyword
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not existing:
|
||||||
|
keyword = KeywordDictionary(**kw.model_dump())
|
||||||
|
db.add(keyword)
|
||||||
|
added += 1
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
return {"status": "success", "keywords_added": added}
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{keyword_id}", response_model=KeywordDictionaryResponse)
|
||||||
|
def update_keyword(
|
||||||
|
keyword_id: int,
|
||||||
|
keyword_in: KeywordDictionaryUpdate,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Update keyword"""
|
||||||
|
keyword = db.query(KeywordDictionary).filter(
|
||||||
|
KeywordDictionary.id == keyword_id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not keyword:
|
||||||
|
raise HTTPException(status_code=404, detail="Keyword tidak ditemukan")
|
||||||
|
|
||||||
|
for key, value in keyword_in.model_dump(exclude_unset=True).items():
|
||||||
|
setattr(keyword, key, value)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
db.refresh(keyword)
|
||||||
|
return keyword
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{keyword_id}")
|
||||||
|
def delete_keyword(
|
||||||
|
keyword_id: int,
|
||||||
|
hard: bool = True,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""Hapus keyword (default: hapus permanen jika hard=True, nonaktifkan jika hard=False)"""
|
||||||
|
keyword = db.query(KeywordDictionary).filter(
|
||||||
|
KeywordDictionary.id == keyword_id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not keyword:
|
||||||
|
raise HTTPException(status_code=404, detail="Keyword tidak ditemukan")
|
||||||
|
|
||||||
|
if hard:
|
||||||
|
db.delete(keyword)
|
||||||
|
db.commit()
|
||||||
|
return {"message": "Keyword berhasil dihapus permanen"}
|
||||||
|
else:
|
||||||
|
keyword.aktif = False
|
||||||
|
db.commit()
|
||||||
|
return {"message": "Keyword berhasil dinonaktifkan"}
|
||||||
|
|
||||||
|
|
||||||
|
# ============== SEED DEFAULT KEYWORDS ==============
|
||||||
|
|
||||||
|
@router.post("/seed-defaults")
|
||||||
|
def seed_default_keywords(db: Session = Depends(get_db)):
|
||||||
|
"""Isi default keywords sesuai PRD Section 7"""
|
||||||
|
|
||||||
|
default_keywords = [
|
||||||
|
# TKK - Teknik Komputer
|
||||||
|
{"kode_prodi": "TKK", "keyword": "jaringan", "kategori_keyword": "technical"},
|
||||||
|
{"kode_prodi": "TKK", "keyword": "network administrator", "kategori_keyword": "technical"},
|
||||||
|
{"kode_prodi": "TKK", "keyword": "network engineer", "kategori_keyword": "technical"},
|
||||||
|
{"kode_prodi": "TKK", "keyword": "LAN/WAN", "kategori_keyword": "technical"},
|
||||||
|
{"kode_prodi": "TKK", "keyword": "cloud computing", "kategori_keyword": "technical"},
|
||||||
|
{"kode_prodi": "TKK", "keyword": "IoT", "kategori_keyword": "technical"},
|
||||||
|
{"kode_prodi": "TKK", "keyword": "embedded system", "kategori_keyword": "technical"},
|
||||||
|
{"kode_prodi": "TKK", "keyword": "firmware", "kategori_keyword": "technical"},
|
||||||
|
{"kode_prodi": "TKK", "keyword": "hardware", "kategori_keyword": "technical"},
|
||||||
|
{"kode_prodi": "TKK", "keyword": "sysadmin", "kategori_keyword": "technical"},
|
||||||
|
{"kode_prodi": "TKK", "keyword": "IT support", "kategori_keyword": "technical"},
|
||||||
|
|
||||||
|
# MIF - Manajemen Informatika
|
||||||
|
{"kode_prodi": "MIF", "keyword": "programmer", "kategori_keyword": "technical"},
|
||||||
|
{"kode_prodi": "MIF", "keyword": "software developer", "kategori_keyword": "technical"},
|
||||||
|
{"kode_prodi": "MIF", "keyword": "database", "kategori_keyword": "technical"},
|
||||||
|
{"kode_prodi": "MIF", "keyword": "SQL", "kategori_keyword": "technical"},
|
||||||
|
{"kode_prodi": "MIF", "keyword": "data analyst", "kategori_keyword": "technical"},
|
||||||
|
{"kode_prodi": "MIF", "keyword": "data warehouse", "kategori_keyword": "technical"},
|
||||||
|
{"kode_prodi": "MIF", "keyword": "business intelligence", "kategori_keyword": "technical"},
|
||||||
|
{"kode_prodi": "MIF", "keyword": "ERD", "kategori_keyword": "technical"},
|
||||||
|
|
||||||
|
# TIF - Teknik Informatika
|
||||||
|
{"kode_prodi": "TIF", "keyword": "software engineer", "kategori_keyword": "technical"},
|
||||||
|
{"kode_prodi": "TIF", "keyword": "quality assurance", "kategori_keyword": "technical"},
|
||||||
|
{"kode_prodi": "TIF", "keyword": "QA/tester", "kategori_keyword": "technical"},
|
||||||
|
{"kode_prodi": "TIF", "keyword": "IT entrepreneur", "kategori_keyword": "technical"},
|
||||||
|
{"kode_prodi": "TIF", "keyword": "startup teknologi", "kategori_keyword": "technical"},
|
||||||
|
{"kode_prodi": "TIF", "keyword": "manajemen proyek IT", "kategori_keyword": "technical"},
|
||||||
|
{"kode_prodi": "TIF", "keyword": "konsultan perangkat lunak", "kategori_keyword": "technical"},
|
||||||
|
]
|
||||||
|
|
||||||
|
added = 0
|
||||||
|
for kw in default_keywords:
|
||||||
|
existing = db.query(KeywordDictionary).filter(
|
||||||
|
KeywordDictionary.kode_prodi == kw["kode_prodi"],
|
||||||
|
KeywordDictionary.keyword == kw["keyword"]
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not existing:
|
||||||
|
keyword = KeywordDictionary(**kw)
|
||||||
|
db.add(keyword)
|
||||||
|
added += 1
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": f"Default keywords ditambahkan: {added}"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from app.config.database import get_db
|
||||||
|
from app.controllers.prodi import ProdiController
|
||||||
|
from app.schemas.prodi import (
|
||||||
|
ProdiCreate,
|
||||||
|
ProdiUpdate,
|
||||||
|
ProdiResponse,
|
||||||
|
)
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
tags=["Prodi"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_model=List[ProdiResponse])
|
||||||
|
def get_all_prodi(db: Session = Depends(get_db)):
|
||||||
|
return ProdiController.get_all(db)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{prodi_id}", response_model=ProdiResponse)
|
||||||
|
def get_prodi(prodi_id: int, db: Session = Depends(get_db)):
|
||||||
|
return ProdiController.get_by_id(db, prodi_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/", response_model=ProdiResponse, status_code=201)
|
||||||
|
def create_prodi(
|
||||||
|
prodi: ProdiCreate,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
return ProdiController.create(db, prodi)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{prodi_id}", response_model=ProdiResponse)
|
||||||
|
def update_prodi(
|
||||||
|
prodi_id: int,
|
||||||
|
prodi: ProdiUpdate,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
return ProdiController.update(db, prodi_id, prodi)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{prodi_id}")
|
||||||
|
def delete_prodi(
|
||||||
|
prodi_id: int,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
ProdiController.delete(db, prodi_id)
|
||||||
|
return {"message": "Data prodi berhasil dihapus"}
|
||||||
|
|
@ -0,0 +1,497 @@
|
||||||
|
"""
|
||||||
|
Router untuk Pertanyaan dan Pilihan Jawaban Tracer Study (CRUD Management)
|
||||||
|
PRD Reference: Pertanyaan-dan-jawaban.md
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy import or_, func
|
||||||
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from app.config.database import get_db
|
||||||
|
from app.models.models import Question, AnswerOption
|
||||||
|
from app.middleware.auth_middleware import get_current_active_user
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/questions", tags=["Questions & Answer Options"])
|
||||||
|
|
||||||
|
|
||||||
|
# ========== SCHEMAS ==========
|
||||||
|
|
||||||
|
class AnswerOptionBase(BaseModel):
|
||||||
|
kode: str
|
||||||
|
teks: str
|
||||||
|
urutan: Optional[int] = 0
|
||||||
|
is_active: Optional[bool] = True
|
||||||
|
|
||||||
|
|
||||||
|
class AnswerOptionCreate(AnswerOptionBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AnswerOptionUpdate(BaseModel):
|
||||||
|
kode: Optional[str] = None
|
||||||
|
teks: Optional[str] = None
|
||||||
|
urutan: Optional[int] = None
|
||||||
|
is_active: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
|
class AnswerOptionResponse(AnswerOptionBase):
|
||||||
|
id: int
|
||||||
|
question_id: int
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
|
class QuestionBase(BaseModel):
|
||||||
|
kode: str
|
||||||
|
pertanyaan: str
|
||||||
|
tipe: str = "pilihan" # dropdown, pilihan, skala, teks, date, ya_tidak
|
||||||
|
section: str = "kuesioner_utama"
|
||||||
|
nomor_form: Optional[int] = 0
|
||||||
|
tahun: Optional[str] = "all"
|
||||||
|
is_active: Optional[bool] = True
|
||||||
|
|
||||||
|
|
||||||
|
class QuestionCreate(QuestionBase):
|
||||||
|
options: Optional[List[AnswerOptionBase]] = []
|
||||||
|
|
||||||
|
|
||||||
|
class QuestionUpdate(BaseModel):
|
||||||
|
kode: Optional[str] = None
|
||||||
|
pertanyaan: Optional[str] = None
|
||||||
|
tipe: Optional[str] = None
|
||||||
|
section: Optional[str] = None
|
||||||
|
nomor_form: Optional[int] = None
|
||||||
|
tahun: Optional[str] = None
|
||||||
|
is_active: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
|
class QuestionSchema(BaseModel):
|
||||||
|
id: int
|
||||||
|
kode: str
|
||||||
|
pertanyaan: str
|
||||||
|
tipe: str
|
||||||
|
section: str
|
||||||
|
nomor_form: int
|
||||||
|
tahun: Optional[str] = "all"
|
||||||
|
is_active: bool = True
|
||||||
|
options: List[AnswerOptionResponse] = []
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
|
class QuestionListResponse(BaseModel):
|
||||||
|
items: List[QuestionSchema]
|
||||||
|
total: int
|
||||||
|
sections: List[str]
|
||||||
|
|
||||||
|
|
||||||
|
class FormSchema(BaseModel):
|
||||||
|
questions: List[QuestionSchema]
|
||||||
|
sections: List[str]
|
||||||
|
|
||||||
|
|
||||||
|
# ========== ENDPOINTS ==========
|
||||||
|
|
||||||
|
@router.get("/", response_model=QuestionListResponse, summary="Ambil daftar semua pertanyaan dengan filter & search")
|
||||||
|
def get_all_questions(
|
||||||
|
search: Optional[str] = Query(None, description="Cari teks pertanyaan atau kode pertanyaan"),
|
||||||
|
section: Optional[str] = Query(None, description="Filter berdasarkan section"),
|
||||||
|
is_active: Optional[bool] = Query(None, description="Filter status aktif"),
|
||||||
|
skip: int = Query(0, ge=0),
|
||||||
|
limit: int = Query(200, ge=1, le=500),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Mengambil daftar semua pertanyaan tracer study beserta pilihan jawabannya.
|
||||||
|
Mendukung pencarian teks/kode dan filter section.
|
||||||
|
"""
|
||||||
|
query = db.query(Question)
|
||||||
|
|
||||||
|
if search and search.strip():
|
||||||
|
term = f"%{search.strip()}%"
|
||||||
|
query = query.filter(or_(Question.kode.ilike(term), Question.pertanyaan.ilike(term)))
|
||||||
|
|
||||||
|
if section and section.strip() and section != "all":
|
||||||
|
query = query.filter(Question.section == section.strip())
|
||||||
|
|
||||||
|
if is_active is not None:
|
||||||
|
query = query.filter(Question.is_active == is_active)
|
||||||
|
|
||||||
|
total = query.count()
|
||||||
|
questions = query.order_by(Question.nomor_form.asc(), Question.id.asc()).offset(skip).limit(limit).all()
|
||||||
|
|
||||||
|
# Ambil list unique sections yang ada di DB
|
||||||
|
all_sections_raw = db.query(Question.section).distinct().all()
|
||||||
|
sections = [s[0] for s in all_sections_raw if s[0]]
|
||||||
|
|
||||||
|
# Batch load all answer options in ONE query instead of N individual queries
|
||||||
|
q_ids = [q.id for q in questions]
|
||||||
|
options_by_q = {q.id: [] for q in questions}
|
||||||
|
if q_ids:
|
||||||
|
all_opts = db.query(AnswerOption).filter(
|
||||||
|
AnswerOption.question_id.in_(q_ids)
|
||||||
|
).order_by(AnswerOption.urutan.asc(), AnswerOption.id.asc()).all()
|
||||||
|
for opt in all_opts:
|
||||||
|
options_by_q.setdefault(opt.question_id, []).append(opt)
|
||||||
|
|
||||||
|
items = []
|
||||||
|
for q in questions:
|
||||||
|
opts = options_by_q.get(q.id, [])
|
||||||
|
q_dict = {
|
||||||
|
"id": q.id,
|
||||||
|
"kode": q.kode,
|
||||||
|
"pertanyaan": q.pertanyaan,
|
||||||
|
"tipe": q.tipe or "pilihan",
|
||||||
|
"section": q.section or "kuesioner_utama",
|
||||||
|
"nomor_form": q.nomor_form or 0,
|
||||||
|
"tahun": q.tahun or "all",
|
||||||
|
"is_active": q.is_active if q.is_active is not None else True,
|
||||||
|
"options": [AnswerOptionResponse.model_validate(o) for o in opts]
|
||||||
|
}
|
||||||
|
items.append(QuestionSchema(**q_dict))
|
||||||
|
|
||||||
|
return QuestionListResponse(items=items, total=total, sections=sections)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/stats", summary="Ringkasan statistik pertanyaan dan opsi jawaban")
|
||||||
|
def get_question_stats(db: Session = Depends(get_db)):
|
||||||
|
"""
|
||||||
|
Ringkasan jumlah pertanyaan aktif, total opsi jawaban, dan rincian per section.
|
||||||
|
"""
|
||||||
|
total_q = db.query(Question).count()
|
||||||
|
active_q = db.query(Question).filter(Question.is_active == True).count()
|
||||||
|
total_opts = db.query(AnswerOption).count()
|
||||||
|
|
||||||
|
section_breakdown = db.query(
|
||||||
|
Question.section,
|
||||||
|
func.count(Question.id)
|
||||||
|
).group_by(Question.section).all()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total_questions": total_q,
|
||||||
|
"active_questions": active_q,
|
||||||
|
"total_options": total_opts,
|
||||||
|
"sections": {s[0]: s[1] for s in section_breakdown if s[0]}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/formulir", response_model=FormSchema, summary="Ambil semua pertanyaan untuk form alumni")
|
||||||
|
def get_formulir(db: Session = Depends(get_db)):
|
||||||
|
"""
|
||||||
|
Ambil semua pertanyaan aktif dan pilihan jawaban untuk formulir tracer study.
|
||||||
|
"""
|
||||||
|
questions = db.query(Question).filter(Question.is_active == True).order_by(Question.nomor_form).all()
|
||||||
|
q_ids = [q.id for q in questions]
|
||||||
|
options_by_q = {q.id: [] for q in questions}
|
||||||
|
if q_ids:
|
||||||
|
all_opts = db.query(AnswerOption).filter(
|
||||||
|
AnswerOption.question_id.in_(q_ids),
|
||||||
|
AnswerOption.is_active == True
|
||||||
|
).order_by(AnswerOption.urutan.asc(), AnswerOption.id.asc()).all()
|
||||||
|
for opt in all_opts:
|
||||||
|
options_by_q.setdefault(opt.question_id, []).append(opt)
|
||||||
|
|
||||||
|
result_questions = []
|
||||||
|
sections = []
|
||||||
|
|
||||||
|
for q in questions:
|
||||||
|
options = options_by_q.get(q.id, [])
|
||||||
|
question_data = QuestionSchema(
|
||||||
|
id=q.id,
|
||||||
|
kode=q.kode,
|
||||||
|
pertanyaan=q.pertanyaan,
|
||||||
|
tipe=q.tipe or "pilihan",
|
||||||
|
section=q.section or "kuesioner_utama",
|
||||||
|
nomor_form=q.nomor_form or 0,
|
||||||
|
tahun=q.tahun or "all",
|
||||||
|
is_active=True,
|
||||||
|
options=[AnswerOptionResponse.model_validate(o) for o in options]
|
||||||
|
)
|
||||||
|
result_questions.append(question_data)
|
||||||
|
|
||||||
|
if q.section and q.section not in sections:
|
||||||
|
sections.append(q.section)
|
||||||
|
|
||||||
|
return FormSchema(questions=result_questions, sections=sections)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/sections", summary="Daftar section unik")
|
||||||
|
def get_sections(db: Session = Depends(get_db)):
|
||||||
|
sections = db.query(Question.section).distinct().all()
|
||||||
|
return {"sections": [s[0] for s in sections if s[0]]}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{question_id}", response_model=QuestionSchema, summary="Ambil detail pertanyaan beserta opsi jawaban")
|
||||||
|
def get_question_detail(question_id: int, db: Session = Depends(get_db)):
|
||||||
|
q = db.query(Question).filter(Question.id == question_id).first()
|
||||||
|
if not q:
|
||||||
|
raise HTTPException(status_code=404, detail="Pertanyaan tidak ditemukan")
|
||||||
|
|
||||||
|
options = db.query(AnswerOption).filter(
|
||||||
|
AnswerOption.question_id == q.id
|
||||||
|
).order_by(AnswerOption.urutan).all()
|
||||||
|
|
||||||
|
return QuestionSchema(
|
||||||
|
id=q.id,
|
||||||
|
kode=q.kode,
|
||||||
|
pertanyaan=q.pertanyaan,
|
||||||
|
tipe=q.tipe or "pilihan",
|
||||||
|
section=q.section or "kuesioner_utama",
|
||||||
|
nomor_form=q.nomor_form or 0,
|
||||||
|
tahun=q.tahun or "all",
|
||||||
|
is_active=q.is_active if q.is_active is not None else True,
|
||||||
|
options=[AnswerOptionResponse.model_validate(o) for o in options]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def resequence_questions(db: Session):
|
||||||
|
"""
|
||||||
|
Mengurutkan ulang seluruh nomor_form pertanyaan dari 1 sampai N
|
||||||
|
secara berkesinambungan tanpa ada celah atau nomor kembar.
|
||||||
|
"""
|
||||||
|
questions = db.query(Question).order_by(Question.nomor_form.asc(), Question.id.asc()).all()
|
||||||
|
for idx, q in enumerate(questions, start=1):
|
||||||
|
q.nomor_form = idx
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/reorder", summary="Rapikan ulang seluruh nomor urut pertanyaan agar berurutan 1 sampai N")
|
||||||
|
def reorder_all_questions(db: Session = Depends(get_db)):
|
||||||
|
"""
|
||||||
|
Menata ulang nomor urut pertanyaan (nomor_form) dari 1 sampai N secara berkesinambungan
|
||||||
|
berdasarkan urutan saat ini.
|
||||||
|
"""
|
||||||
|
resequence_questions(db)
|
||||||
|
total = db.query(Question).count()
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": f"Berhasil merapikan nomor urut untuk {total} pertanyaan secara berurutan (1-{total}).",
|
||||||
|
"total": total
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/", response_model=QuestionSchema, status_code=status.HTTP_201_CREATED, summary="Tambah pertanyaan baru")
|
||||||
|
def create_question(question_in: QuestionCreate, db: Session = Depends(get_db)):
|
||||||
|
existing = db.query(Question).filter(Question.kode == question_in.kode.strip()).first()
|
||||||
|
if existing:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Pertanyaan dengan kode '{question_in.kode}' sudah ada")
|
||||||
|
|
||||||
|
# Tentukan nomor urut berikutnya jika tidak diisi atau jika melompat terlalu jauh
|
||||||
|
max_no = db.query(func.max(Question.nomor_form)).scalar() or 0
|
||||||
|
assigned_no = question_in.nomor_form if (question_in.nomor_form and question_in.nomor_form > 0) else (max_no + 1)
|
||||||
|
|
||||||
|
new_q = Question(
|
||||||
|
kode=question_in.kode.strip(),
|
||||||
|
pertanyaan=question_in.pertanyaan.strip(),
|
||||||
|
tipe=question_in.tipe,
|
||||||
|
section=question_in.section,
|
||||||
|
nomor_form=assigned_no,
|
||||||
|
tahun=question_in.tahun or "all",
|
||||||
|
is_active=question_in.is_active if question_in.is_active is not None else True,
|
||||||
|
)
|
||||||
|
db.add(new_q)
|
||||||
|
db.flush()
|
||||||
|
|
||||||
|
# Tambahkan options jika disertakan
|
||||||
|
created_options = []
|
||||||
|
if question_in.options:
|
||||||
|
for idx, opt in enumerate(question_in.options, start=1):
|
||||||
|
new_opt = AnswerOption(
|
||||||
|
question_id=new_q.id,
|
||||||
|
kode=opt.kode.strip(),
|
||||||
|
teks=opt.teks.strip(),
|
||||||
|
urutan=opt.urutan if opt.urutan else idx,
|
||||||
|
is_active=opt.is_active if opt.is_active is not None else True
|
||||||
|
)
|
||||||
|
db.add(new_opt)
|
||||||
|
created_options.append(new_opt)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
# Pastikan seluruh pertanyaan tersusun rapi berurutan 1..N
|
||||||
|
resequence_questions(db)
|
||||||
|
db.refresh(new_q)
|
||||||
|
|
||||||
|
return QuestionSchema(
|
||||||
|
id=new_q.id,
|
||||||
|
kode=new_q.kode,
|
||||||
|
pertanyaan=new_q.pertanyaan,
|
||||||
|
tipe=new_q.tipe,
|
||||||
|
section=new_q.section,
|
||||||
|
nomor_form=new_q.nomor_form,
|
||||||
|
tahun=new_q.tahun,
|
||||||
|
is_active=new_q.is_active,
|
||||||
|
options=[AnswerOptionResponse.model_validate(o) for o in created_options]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{question_id}", response_model=QuestionSchema, summary="Perbarui pertanyaan")
|
||||||
|
def update_question(question_id: int, question_in: QuestionUpdate, db: Session = Depends(get_db)):
|
||||||
|
q = db.query(Question).filter(Question.id == question_id).first()
|
||||||
|
if not q:
|
||||||
|
raise HTTPException(status_code=404, detail="Pertanyaan tidak ditemukan")
|
||||||
|
|
||||||
|
if question_in.kode and question_in.kode.strip() != q.kode:
|
||||||
|
existing = db.query(Question).filter(Question.kode == question_in.kode.strip()).first()
|
||||||
|
if existing and existing.id != question_id:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Kode '{question_in.kode}' sudah digunakan oleh pertanyaan lain")
|
||||||
|
q.kode = question_in.kode.strip()
|
||||||
|
|
||||||
|
if question_in.pertanyaan is not None:
|
||||||
|
q.pertanyaan = question_in.pertanyaan.strip()
|
||||||
|
if question_in.tipe is not None:
|
||||||
|
q.tipe = question_in.tipe
|
||||||
|
if question_in.section is not None:
|
||||||
|
q.section = question_in.section
|
||||||
|
if question_in.nomor_form is not None:
|
||||||
|
q.nomor_form = question_in.nomor_form
|
||||||
|
if question_in.tahun is not None:
|
||||||
|
q.tahun = question_in.tahun
|
||||||
|
if question_in.is_active is not None:
|
||||||
|
q.is_active = question_in.is_active
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
# Jika nomor_form diubah, rapikan kembali urutan pertanyaan
|
||||||
|
if question_in.nomor_form is not None:
|
||||||
|
resequence_questions(db)
|
||||||
|
db.refresh(q)
|
||||||
|
|
||||||
|
options = db.query(AnswerOption).filter(
|
||||||
|
AnswerOption.question_id == q.id
|
||||||
|
).order_by(AnswerOption.urutan).all()
|
||||||
|
|
||||||
|
return QuestionSchema(
|
||||||
|
id=q.id,
|
||||||
|
kode=q.kode,
|
||||||
|
pertanyaan=q.pertanyaan,
|
||||||
|
tipe=q.tipe,
|
||||||
|
section=q.section,
|
||||||
|
nomor_form=q.nomor_form,
|
||||||
|
tahun=q.tahun,
|
||||||
|
is_active=q.is_active,
|
||||||
|
options=[AnswerOptionResponse.model_validate(o) for o in options]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{question_id}", summary="Hapus pertanyaan beserta opsi jawabannya")
|
||||||
|
def delete_question(question_id: int, hard: bool = True, db: Session = Depends(get_db)):
|
||||||
|
q = db.query(Question).filter(Question.id == question_id).first()
|
||||||
|
if not q:
|
||||||
|
raise HTTPException(status_code=404, detail="Pertanyaan tidak ditemukan")
|
||||||
|
|
||||||
|
kode_q = q.kode
|
||||||
|
if hard:
|
||||||
|
# Cascade delete answer_options
|
||||||
|
db.query(AnswerOption).filter(AnswerOption.question_id == question_id).delete()
|
||||||
|
db.delete(q)
|
||||||
|
db.commit()
|
||||||
|
# Otomatis rapikan urutan sisa pertanyaan sehingga tidak ada nomor yang bolong / melompat
|
||||||
|
resequence_questions(db)
|
||||||
|
return {"status": "success", "message": f"Pertanyaan '{kode_q}' berhasil dihapus dan nomor urut seluruh pertanyaan telah dirapikan secara berkesinambungan"}
|
||||||
|
else:
|
||||||
|
q.is_active = False
|
||||||
|
db.commit()
|
||||||
|
return {"status": "success", "message": f"Pertanyaan '{kode_q}' dinonaktifkan"}
|
||||||
|
|
||||||
|
|
||||||
|
# ========== ANSWER OPTIONS SUB-ENDPOINTS ==========
|
||||||
|
|
||||||
|
@router.post("/{question_id}/options", response_model=AnswerOptionResponse, status_code=status.HTTP_201_CREATED, summary="Tambah opsi jawaban baru ke pertanyaan")
|
||||||
|
def add_answer_option(question_id: int, option_in: AnswerOptionCreate, db: Session = Depends(get_db)):
|
||||||
|
q = db.query(Question).filter(Question.id == question_id).first()
|
||||||
|
if not q:
|
||||||
|
raise HTTPException(status_code=404, detail="Pertanyaan tidak ditemukan")
|
||||||
|
|
||||||
|
new_opt = AnswerOption(
|
||||||
|
question_id=question_id,
|
||||||
|
kode=option_in.kode.strip(),
|
||||||
|
teks=option_in.teks.strip(),
|
||||||
|
urutan=option_in.urutan or 0,
|
||||||
|
is_active=option_in.is_active if option_in.is_active is not None else True
|
||||||
|
)
|
||||||
|
db.add(new_opt)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(new_opt)
|
||||||
|
return AnswerOptionResponse.model_validate(new_opt)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/options/{option_id}", response_model=AnswerOptionResponse, summary="Perbarui opsi jawaban")
|
||||||
|
def update_answer_option(option_id: int, option_in: AnswerOptionUpdate, db: Session = Depends(get_db)):
|
||||||
|
opt = db.query(AnswerOption).filter(AnswerOption.id == option_id).first()
|
||||||
|
if not opt:
|
||||||
|
raise HTTPException(status_code=404, detail="Pilihan jawaban tidak ditemukan")
|
||||||
|
|
||||||
|
if option_in.kode is not None:
|
||||||
|
opt.kode = option_in.kode.strip()
|
||||||
|
if option_in.teks is not None:
|
||||||
|
opt.teks = option_in.teks.strip()
|
||||||
|
if option_in.urutan is not None:
|
||||||
|
opt.urutan = option_in.urutan
|
||||||
|
if option_in.is_active is not None:
|
||||||
|
opt.is_active = option_in.is_active
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
db.refresh(opt)
|
||||||
|
return AnswerOptionResponse.model_validate(opt)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/options/{option_id}", summary="Hapus opsi jawaban")
|
||||||
|
def delete_answer_option(option_id: int, db: Session = Depends(get_db)):
|
||||||
|
opt = db.query(AnswerOption).filter(AnswerOption.id == option_id).first()
|
||||||
|
if not opt:
|
||||||
|
raise HTTPException(status_code=404, detail="Pilihan jawaban tidak ditemukan")
|
||||||
|
|
||||||
|
db.delete(opt)
|
||||||
|
db.commit()
|
||||||
|
return {"status": "success", "message": "Pilihan jawaban berhasil dihapus"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/sync-jti-prodi-options", summary="Inisialisasi opsi program studi JTI pada pertanyaan kdpstmsnmh")
|
||||||
|
def sync_jti_prodi_options(db: Session = Depends(get_db)):
|
||||||
|
"""
|
||||||
|
Menyinkronkan pilihan jawaban pertanyaan nomor 2 (kdpstmsnmh) agar secara khusus memuat
|
||||||
|
Program Studi Jurusan Teknologi Informasi (TIF, MIF, TKK).
|
||||||
|
"""
|
||||||
|
q = db.query(Question).filter(Question.kode == "kdpstmsnmh").first()
|
||||||
|
if not q:
|
||||||
|
# Create question kdpstmsnmh if not exists
|
||||||
|
q = Question(
|
||||||
|
kode="kdpstmsnmh",
|
||||||
|
pertanyaan="Program Studi",
|
||||||
|
tipe="pilihan",
|
||||||
|
section="identitas",
|
||||||
|
nomor_form=2,
|
||||||
|
tahun="all",
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
db.add(q)
|
||||||
|
db.flush()
|
||||||
|
|
||||||
|
# Clear existing options for this question
|
||||||
|
db.query(AnswerOption).filter(AnswerOption.question_id == q.id).delete()
|
||||||
|
|
||||||
|
jti_prodis = [
|
||||||
|
{"kode": "55301", "teks": "Teknik Informatika (55301)", "urutan": 1},
|
||||||
|
{"kode": "57401", "teks": "Manajemen Informatika (57401)", "urutan": 2},
|
||||||
|
{"kode": "56401", "teks": "Teknik Komputer (56401)", "urutan": 3},
|
||||||
|
{"kode": "55302", "teks": "Teknik Informatika Kampus Kab. Nganjuk (55302)", "urutan": 4},
|
||||||
|
{"kode": "55304", "teks": "Teknik Informatika Kampus Kab. Sidoarjo (55304)", "urutan": 5},
|
||||||
|
]
|
||||||
|
|
||||||
|
for p in jti_prodis:
|
||||||
|
opt = AnswerOption(
|
||||||
|
question_id=q.id,
|
||||||
|
kode=p["kode"],
|
||||||
|
teks=p["teks"],
|
||||||
|
urutan=p["urutan"],
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
db.add(opt)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": f"Berhasil menginisialisasi {len(jti_prodis)} opsi prodi Teknologi Informasi pada pertanyaan 'kdpstmsnmh'",
|
||||||
|
"options": jti_prodis
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,176 @@
|
||||||
|
from pydantic import BaseModel, EmailStr, Field, ConfigDict, field_validator, model_validator
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional, Any, List, Dict
|
||||||
|
|
||||||
|
class AlumniBase(BaseModel):
|
||||||
|
nim: str = Field(..., description="Nomor Induk Mahasiswa (NIM)")
|
||||||
|
nama: str = Field(..., min_length=2, description="Nama lengkap alumni")
|
||||||
|
tahun_lulus: int = Field(..., ge=1900, le=2100, description="Tahun lulus alumni")
|
||||||
|
status_kerja: Optional[str] = Field(None, description="Status pekerjaan saat ini")
|
||||||
|
pekerjaan: Optional[str] = Field(None, description="Nama pekerjaan/posisi jika bekerja")
|
||||||
|
instansi: Optional[str] = Field(None, description="Nama instansi/perusahaan tempat bekerja")
|
||||||
|
no_hp: Optional[str] = Field(None, description="Nomor handphone/WhatsApp aktif")
|
||||||
|
email: Optional[str] = Field(None, description="Email aktif alumni")
|
||||||
|
kode_prodi: Optional[str] = Field(None, description="Kode program studi")
|
||||||
|
|
||||||
|
class AlumniCreate(AlumniBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class AlumniUpdate(BaseModel):
|
||||||
|
nim: Optional[str] = None
|
||||||
|
nama: Optional[str] = None
|
||||||
|
tahun_lulus: Optional[int] = None
|
||||||
|
status_kerja: Optional[str] = None
|
||||||
|
pekerjaan: Optional[str] = None
|
||||||
|
instansi: Optional[str] = None
|
||||||
|
no_hp: Optional[str] = None
|
||||||
|
email: Optional[str] = None
|
||||||
|
kode_prodi: Optional[str] = None
|
||||||
|
|
||||||
|
class AlumniResponse(AlumniBase):
|
||||||
|
id: int
|
||||||
|
created_at: Optional[datetime] = None
|
||||||
|
updated_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
@model_validator(mode='before')
|
||||||
|
@classmethod
|
||||||
|
def exclude_relationships(cls, data: Any) -> Any:
|
||||||
|
"""Convert SQLAlchemy objects to dict, excluding relationship attributes"""
|
||||||
|
if hasattr(data, '__dict__'):
|
||||||
|
result = {}
|
||||||
|
for key, value in data.__dict__.items():
|
||||||
|
if key.startswith('_') or key in ['pekerjaan_detail', 'kompetensi', 'tracer_response', 'clustering_results']:
|
||||||
|
continue
|
||||||
|
if hasattr(value, '__tablename__'):
|
||||||
|
result[key] = None
|
||||||
|
else:
|
||||||
|
result[key] = value
|
||||||
|
return result
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class PekerjaanDetail(BaseModel):
|
||||||
|
id: Optional[int] = None
|
||||||
|
status_pekerjaan: Optional[str] = None
|
||||||
|
jenis_perusahaan: Optional[str] = None
|
||||||
|
nama_perusahaan: Optional[str] = None
|
||||||
|
jabatan: Optional[str] = None
|
||||||
|
tingkatan_tempat_kerja: Optional[str] = None
|
||||||
|
provinsi: Optional[str] = None
|
||||||
|
kabupaten_kota: Optional[str] = None
|
||||||
|
teks_pekerjaan: Optional[str] = None
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
|
class KompetensiDetail(BaseModel):
|
||||||
|
id: Optional[int] = None
|
||||||
|
etika_lulus: Optional[str] = None
|
||||||
|
keahlian_lulus: Optional[str] = None
|
||||||
|
bahasa_inggris_lulus: Optional[str] = None
|
||||||
|
teknologi_informasi_lulus: Optional[str] = None
|
||||||
|
komunikasi_lulus: Optional[str] = None
|
||||||
|
kerja_sama_lulus: Optional[str] = None
|
||||||
|
pengembangan_diri_lulus: Optional[str] = None
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ClusteringResultDetail(BaseModel):
|
||||||
|
id: Optional[int] = None
|
||||||
|
model_version: Optional[str] = None
|
||||||
|
k: Optional[int] = None
|
||||||
|
cluster: Optional[int] = None
|
||||||
|
kategori: Optional[str] = None
|
||||||
|
profile_match: Optional[bool] = None
|
||||||
|
matched_keyword: Optional[str] = None
|
||||||
|
created_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
|
class AlumniDetailResponse(AlumniBase):
|
||||||
|
id: int
|
||||||
|
created_at: Optional[datetime] = None
|
||||||
|
updated_at: Optional[datetime] = None
|
||||||
|
pekerjaan_detail: Optional[PekerjaanDetail] = None
|
||||||
|
kompetensi: Optional[KompetensiDetail] = None
|
||||||
|
tracer_response: Optional[Dict[str, Any]] = None
|
||||||
|
clustering_results: Optional[List[ClusteringResultDetail]] = None
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
@model_validator(mode='before')
|
||||||
|
@classmethod
|
||||||
|
def process_relations(cls, data: Any) -> Any:
|
||||||
|
if hasattr(data, '__dict__'):
|
||||||
|
result = {}
|
||||||
|
for key, value in data.__dict__.items():
|
||||||
|
if key.startswith('_'):
|
||||||
|
continue
|
||||||
|
if key == 'tracer_response' and value is not None and hasattr(value, '__dict__'):
|
||||||
|
# Convert tracer_response sqlalchemy object to dict
|
||||||
|
tr_dict = {k: v for k, v in value.__dict__.items() if not k.startswith('_') and not hasattr(v, '__tablename__')}
|
||||||
|
result[key] = tr_dict
|
||||||
|
else:
|
||||||
|
result[key] = value
|
||||||
|
return result
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class AlumniPaginationResponse(BaseModel):
|
||||||
|
total: int
|
||||||
|
page: int
|
||||||
|
limit: int
|
||||||
|
total_pages: int
|
||||||
|
data: List[AlumniResponse]
|
||||||
|
|
||||||
|
|
||||||
|
class PublicTracerLookupRequest(BaseModel):
|
||||||
|
nim: str = Field(..., description="Nomor Induk Mahasiswa (NIM)")
|
||||||
|
email: Optional[str] = Field(None, description="Email / Gmail untuk verifikasi kepemilikan")
|
||||||
|
|
||||||
|
|
||||||
|
class PublicTracerSubmitRequest(BaseModel):
|
||||||
|
nim: str = Field(..., description="Nomor Induk Mahasiswa (NIM)")
|
||||||
|
nama: str = Field(..., min_length=2, description="Nama lengkap alumni")
|
||||||
|
kode_prodi: str = Field(..., description="Kode prodi (55301, 57401, 56401, dll)")
|
||||||
|
tahun_lulus: int = Field(..., ge=1990, le=2100, description="Tahun kelulusan")
|
||||||
|
email: str = Field(..., description="Email / Gmail aktif alumni")
|
||||||
|
no_hp: Optional[str] = Field(None, description="Nomor WhatsApp / HP aktif")
|
||||||
|
status_kerja: str = Field(..., description="Status kerja: bekerja, belum_bekerja, wiraswasta, melanjutkan, mencari_kerja")
|
||||||
|
|
||||||
|
# Detail Pekerjaan (jika bekerja / wiraswasta)
|
||||||
|
nama_perusahaan: Optional[str] = None
|
||||||
|
jabatan: Optional[str] = None
|
||||||
|
jenis_perusahaan: Optional[str] = None
|
||||||
|
tingkatan_tempat_kerja: Optional[str] = None
|
||||||
|
hubungan_studi: Optional[str] = None
|
||||||
|
|
||||||
|
# Identitas Extended (Pertanyaan-dan-jawaban.md)
|
||||||
|
kode_pt: Optional[str] = "005019"
|
||||||
|
nik: Optional[str] = None
|
||||||
|
npwp: Optional[str] = None
|
||||||
|
|
||||||
|
# Karir & Gaji Extended
|
||||||
|
bulan_dapat_kerja: Optional[int] = None
|
||||||
|
rata_pendapatan: Optional[str] = None
|
||||||
|
provinsi_kerja: Optional[str] = None
|
||||||
|
kab_kota_kerja: Optional[str] = None
|
||||||
|
posisi_wiraswasta: Optional[str] = None
|
||||||
|
|
||||||
|
# Studi Lanjut
|
||||||
|
sumber_biaya_studi: Optional[str] = None
|
||||||
|
pt_studi_lanjut: Optional[str] = None
|
||||||
|
prodi_studi_lanjut: Optional[str] = None
|
||||||
|
tanggal_masuk_studi: Optional[str] = None
|
||||||
|
|
||||||
|
# Pembiayaan & Kesesuaian
|
||||||
|
sumber_biaya_kuliah: Optional[str] = None
|
||||||
|
tingkat_pendidikan_tepat: Optional[str] = None
|
||||||
|
|
||||||
|
# Data Ekstra / Detail Kuesioner (Kompetensi, Metode Pembelajaran, Cara Cari Kerja, dll)
|
||||||
|
data_extra: Optional[dict] = None
|
||||||
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
"""
|
||||||
|
Pydantic Schemas untuk Clustering
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from typing import List, Optional
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class ClusteringRequest(BaseModel):
|
||||||
|
"""Request untuk clustering"""
|
||||||
|
n_clusters: int = Field(default=3, ge=2, le=10, description="Jumlah cluster")
|
||||||
|
auto_k: bool = Field(default=False, description="Auto-detect jumlah cluster optimal")
|
||||||
|
tahun: Optional[List[str]] = Field(default=None, description="Filter tahun (opsional)")
|
||||||
|
|
||||||
|
|
||||||
|
class AlumniInput(BaseModel):
|
||||||
|
"""Data alumni untuk clustering"""
|
||||||
|
nama: str
|
||||||
|
nim: Optional[str] = None
|
||||||
|
perusahaan: str
|
||||||
|
jabatan: str
|
||||||
|
|
||||||
|
|
||||||
|
class ClusterResult(BaseModel):
|
||||||
|
"""Hasil clustering untuk satu alumni"""
|
||||||
|
nama: str
|
||||||
|
nim: Optional[str] = None
|
||||||
|
perusahaan: str
|
||||||
|
jabatan: str
|
||||||
|
cluster: int
|
||||||
|
cluster_label: str
|
||||||
|
|
||||||
|
|
||||||
|
class ClusteringResponse(BaseModel):
|
||||||
|
"""Response dari clustering"""
|
||||||
|
status: str
|
||||||
|
message: Optional[str] = None
|
||||||
|
total_records: int
|
||||||
|
n_clusters: int
|
||||||
|
distribution: dict
|
||||||
|
summary: Optional[dict] = None
|
||||||
|
top_features: Optional[dict] = None
|
||||||
|
data: List[ClusterResult]
|
||||||
|
|
||||||
|
|
||||||
|
class StandardizeRequest(BaseModel):
|
||||||
|
"""Request untuk standarisasi data"""
|
||||||
|
files: List[str] = Field(description="List filename yang diupload")
|
||||||
|
tahun: List[str] = Field(description="List tahun untuk setiap file")
|
||||||
|
|
||||||
|
|
||||||
|
class StandardizeResponse(BaseModel):
|
||||||
|
"""Response dari standarisasi"""
|
||||||
|
status: str
|
||||||
|
message: Optional[str] = None
|
||||||
|
total_records: int
|
||||||
|
years_processed: List[str]
|
||||||
|
data: Optional[List[AlumniInput]] = None
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
"""
|
||||||
|
Schemas untuk Tabel Baru - PRD Section 24
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional, List
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
# ============== PEKERJAAN ==============
|
||||||
|
|
||||||
|
class PekerjaanBase(BaseModel):
|
||||||
|
alumni_id: int
|
||||||
|
status_pekerjaan: Optional[str] = None
|
||||||
|
jenis_perusahaan: Optional[str] = None
|
||||||
|
nama_perusahaan: Optional[str] = None
|
||||||
|
jabatan: Optional[str] = None
|
||||||
|
tingkatan_tempat_kerja: Optional[str] = None
|
||||||
|
provinsi: Optional[str] = None
|
||||||
|
kabupaten_kota: Optional[str] = None
|
||||||
|
teks_pekerjaan: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class PekerjaanCreate(PekerjaanBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PekerjaanUpdate(BaseModel):
|
||||||
|
status_pekerjaan: Optional[str] = None
|
||||||
|
jenis_perusahaan: Optional[str] = None
|
||||||
|
nama_perusahaan: Optional[str] = None
|
||||||
|
jabatan: Optional[str] = None
|
||||||
|
tingkatan_tempat_kerja: Optional[str] = None
|
||||||
|
provinsi: Optional[str] = None
|
||||||
|
kabupaten_kota: Optional[str] = None
|
||||||
|
teks_pekerjaan: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class PekerjaanResponse(PekerjaanBase):
|
||||||
|
id: int
|
||||||
|
created_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
# ============== KOMPETENSI ==============
|
||||||
|
|
||||||
|
class KompetensiBase(BaseModel):
|
||||||
|
alumni_id: int
|
||||||
|
etika_lulus: Optional[int] = None
|
||||||
|
keahlian_lulus: Optional[int] = None
|
||||||
|
bahasa_inggris_lulus: Optional[int] = None
|
||||||
|
teknologi_informasi_lulus: Optional[int] = None
|
||||||
|
komunikasi_lulus: Optional[int] = None
|
||||||
|
kerja_sama_lulus: Optional[int] = None
|
||||||
|
pengembangan_diri_lulus: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
class KompetensiCreate(KompetensiBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class KompetensiUpdate(BaseModel):
|
||||||
|
etika_lulus: Optional[int] = None
|
||||||
|
keahlian_lulus: Optional[int] = None
|
||||||
|
bahasa_inggris_lulus: Optional[int] = None
|
||||||
|
teknologi_informasi_lulus: Optional[int] = None
|
||||||
|
komunikasi_lulus: Optional[int] = None
|
||||||
|
kerja_sama_lulus: Optional[int] = None
|
||||||
|
pengembangan_diri_lulus: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
class KompetensiResponse(KompetensiBase):
|
||||||
|
id: int
|
||||||
|
created_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
# ============== CLUSTERING RESULT ==============
|
||||||
|
|
||||||
|
class ClusteringResultBase(BaseModel):
|
||||||
|
alumni_id: int
|
||||||
|
model_version: Optional[str] = None
|
||||||
|
k: int
|
||||||
|
cluster: int
|
||||||
|
kategori: str # C0, C1, C2, C3
|
||||||
|
profile_match: bool = False
|
||||||
|
matched_keyword: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class ClusteringResultCreate(ClusteringResultBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ClusteringResultResponse(ClusteringResultBase):
|
||||||
|
id: int
|
||||||
|
created_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
# ============== MODEL EVALUATION ==============
|
||||||
|
|
||||||
|
class ModelEvaluationBase(BaseModel):
|
||||||
|
model_version: str
|
||||||
|
k: int
|
||||||
|
inertia: float
|
||||||
|
silhouette_score: float
|
||||||
|
|
||||||
|
|
||||||
|
class ModelEvaluationCreate(ModelEvaluationBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ModelEvaluationResponse(ModelEvaluationBase):
|
||||||
|
id: int
|
||||||
|
created_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
# ============== KEYWORD DICTIONARY ==============
|
||||||
|
|
||||||
|
class KeywordDictionaryBase(BaseModel):
|
||||||
|
kode_prodi: str # TIF, MIF, TKK
|
||||||
|
keyword: str
|
||||||
|
kategori_keyword: Optional[str] = None
|
||||||
|
aktif: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
class KeywordDictionaryCreate(KeywordDictionaryBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class KeywordDictionaryUpdate(BaseModel):
|
||||||
|
keyword: Optional[str] = None
|
||||||
|
kategori_keyword: Optional[str] = None
|
||||||
|
aktif: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
|
class KeywordDictionaryResponse(KeywordDictionaryBase):
|
||||||
|
id: int
|
||||||
|
created_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
# ============== BULK IMPORT ==============
|
||||||
|
|
||||||
|
class BulkAlumniCreate(BaseModel):
|
||||||
|
"""Untuk import bulk alumni dari file Excel"""
|
||||||
|
alumni_list: List[dict]
|
||||||
|
|
||||||
|
|
||||||
|
class BulkClusteringResult(BaseModel):
|
||||||
|
"""Untuk save hasil clustering"""
|
||||||
|
model_version: str
|
||||||
|
k: int
|
||||||
|
inertia: float
|
||||||
|
silhouette_score: float
|
||||||
|
results: List[dict] # List of {alumni_id, cluster, kategori, profile_match, matched_keyword}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class ProdiBase(BaseModel):
|
||||||
|
kode_prodi: str
|
||||||
|
nama_prodi: str
|
||||||
|
jenjang: str
|
||||||
|
jurusan: str
|
||||||
|
|
||||||
|
|
||||||
|
class ProdiCreate(ProdiBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ProdiUpdate(BaseModel):
|
||||||
|
kode_prodi: Optional[str] = None
|
||||||
|
nama_prodi: Optional[str] = None
|
||||||
|
jenjang: Optional[str] = None
|
||||||
|
jurusan: Optional[str] = None
|
||||||
|
|
||||||
|
class ProdiResponse(ProdiBase):
|
||||||
|
id: int
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
model_config = {
|
||||||
|
"from_attributes": True
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
from pydantic import BaseModel, EmailStr, Field, ConfigDict
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
class UserBase(BaseModel):
|
||||||
|
username: str = Field(..., min_length=3, max_length=50, description="Username untuk login")
|
||||||
|
nama: Optional[str] = Field(None, max_length=150, description="Nama lengkap pemilik akun")
|
||||||
|
email: EmailStr = Field(..., description="Alamat email valid")
|
||||||
|
role: Optional[str] = Field("admin", description="Role akun: admin atau kaprodi")
|
||||||
|
kode_prodi: Optional[str] = Field(None, description="Kode prodi jika role kaprodi")
|
||||||
|
|
||||||
|
class UserCreate(UserBase):
|
||||||
|
password: str = Field(..., min_length=6, description="Password minimal 6 karakter")
|
||||||
|
|
||||||
|
class UserUpdate(BaseModel):
|
||||||
|
nama: Optional[str] = None
|
||||||
|
email: Optional[EmailStr] = None
|
||||||
|
role: Optional[str] = None
|
||||||
|
kode_prodi: Optional[str] = None
|
||||||
|
password: Optional[str] = None
|
||||||
|
is_active: Optional[bool] = None
|
||||||
|
|
||||||
|
class UserLogin(BaseModel):
|
||||||
|
username: str = Field(..., description="Username untuk login")
|
||||||
|
password: str = Field(..., description="Password user")
|
||||||
|
|
||||||
|
class UserResponse(UserBase):
|
||||||
|
id: int
|
||||||
|
is_active: bool
|
||||||
|
created_at: Optional[datetime] = None
|
||||||
|
updated_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
class Token(BaseModel):
|
||||||
|
access_token: str
|
||||||
|
token_type: str
|
||||||
|
username: Optional[str] = None
|
||||||
|
nama: Optional[str] = None
|
||||||
|
role: Optional[str] = None
|
||||||
|
kode_prodi: Optional[str] = None
|
||||||
|
|
||||||
|
class TokenData(BaseModel):
|
||||||
|
username: Optional[str] = None
|
||||||
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "../../.."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
# Services package - business logic untuk import
|
||||||
|
from .import_service import import_data
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from fastapi import HTTPException, status
|
||||||
|
from app.repositories.alumni import alumni_repository
|
||||||
|
from app.schemas.alumni import AlumniCreate, AlumniUpdate
|
||||||
|
from app.models.alumni import Alumni
|
||||||
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
|
class AlumniService:
|
||||||
|
"""
|
||||||
|
Service containing business logic for Alumni tracking features.
|
||||||
|
"""
|
||||||
|
@staticmethod
|
||||||
|
def get_by_id(db: Session, alumni_id: int) -> Alumni:
|
||||||
|
"""Fetch alumni by ID or raise HTTP 404 if not found."""
|
||||||
|
alumni = alumni_repository.get(db, alumni_id)
|
||||||
|
if not alumni:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Data alumni dengan ID {alumni_id} tidak ditemukan"
|
||||||
|
)
|
||||||
|
return alumni
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_by_id_detailed(db: Session, alumni_id: int) -> Alumni:
|
||||||
|
"""Fetch alumni by ID with all relations (pekerjaan, kompetensi, tracer_responses, clustering)."""
|
||||||
|
alumni = alumni_repository.get_with_relations(db, alumni_id)
|
||||||
|
if not alumni:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Data alumni dengan ID {alumni_id} tidak ditemukan"
|
||||||
|
)
|
||||||
|
return alumni
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_all(db: Session, skip: int = 0, limit: int = 100) -> List[Alumni]:
|
||||||
|
"""Fetch all alumni with pagination limits."""
|
||||||
|
return alumni_repository.get_all(db, skip=skip, limit=limit)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_filtered(
|
||||||
|
db: Session,
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 10,
|
||||||
|
search: Optional[str] = None,
|
||||||
|
kode_prodi: Optional[str] = None,
|
||||||
|
tahun_lulus: Optional[int] = None,
|
||||||
|
status_kerja: Optional[str] = None,
|
||||||
|
) -> Tuple[List[Alumni], int]:
|
||||||
|
"""Fetch alumni with multi-criteria filtering and count."""
|
||||||
|
return alumni_repository.get_filtered(
|
||||||
|
db=db,
|
||||||
|
skip=skip,
|
||||||
|
limit=limit,
|
||||||
|
search=search,
|
||||||
|
kode_prodi=kode_prodi,
|
||||||
|
tahun_lulus=tahun_lulus,
|
||||||
|
status_kerja=status_kerja,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create(db: Session, alumni_in: AlumniCreate) -> Alumni:
|
||||||
|
"""Create a new alumni record after verifying NIM uniqueness."""
|
||||||
|
existing_nim = alumni_repository.get_by_nim(db, alumni_in.nim)
|
||||||
|
if existing_nim:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=f"Alumni dengan NIM {alumni_in.nim} sudah terdaftar"
|
||||||
|
)
|
||||||
|
return alumni_repository.create(db, alumni_in)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def update(db: Session, alumni_id: int, alumni_in: AlumniUpdate) -> Alumni:
|
||||||
|
"""Update an existing alumni record or raise HTTP 404 if not found."""
|
||||||
|
alumni = AlumniService.get_by_id(db, alumni_id)
|
||||||
|
return alumni_repository.update(db, alumni, alumni_in)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def delete(db: Session, alumni_id: int) -> Alumni:
|
||||||
|
"""Delete an alumni record by ID or raise HTTP 404 if not found."""
|
||||||
|
alumni = AlumniService.get_by_id(db, alumni_id)
|
||||||
|
deleted = alumni_repository.delete(db, alumni_id)
|
||||||
|
return deleted if deleted is not None else alumni
|
||||||
|
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from fastapi import HTTPException, status
|
||||||
|
from app.repositories.user import user_repository
|
||||||
|
from app.schemas.user import UserCreate, UserUpdate, Token
|
||||||
|
from app.utils.security import get_password_hash, verify_password, create_access_token
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
class AuthService:
|
||||||
|
"""
|
||||||
|
Service containing authentication-related business logic.
|
||||||
|
Bridge between the controllers and repositories.
|
||||||
|
"""
|
||||||
|
@staticmethod
|
||||||
|
def register(db: Session, user_in: UserCreate):
|
||||||
|
# 1. Check if user already exists by username
|
||||||
|
existing_user = user_repository.get_by_username(db, user_in.username)
|
||||||
|
if existing_user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Username sudah terdaftar"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. Check if user already exists by email
|
||||||
|
existing_email = user_repository.get_by_email(db, user_in.email)
|
||||||
|
if existing_email:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Email sudah terdaftar"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. Hash the user password
|
||||||
|
hashed_password = get_password_hash(user_in.password)
|
||||||
|
|
||||||
|
# 4. Save user to database
|
||||||
|
db_user_data = {
|
||||||
|
"username": user_in.username,
|
||||||
|
"nama": user_in.nama or user_in.username,
|
||||||
|
"email": user_in.email,
|
||||||
|
"role": user_in.role or "admin",
|
||||||
|
"kode_prodi": user_in.kode_prodi,
|
||||||
|
"hashed_password": hashed_password,
|
||||||
|
"is_active": True
|
||||||
|
}
|
||||||
|
return user_repository.create(db, db_user_data)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def login(db: Session, username: str, password: str) -> Token:
|
||||||
|
# 1. Retrieve user by username
|
||||||
|
user = user_repository.get_by_username(db, username)
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Username atau password salah",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. Verify password hash matches plain text
|
||||||
|
if not verify_password(password, user.hashed_password):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Username atau password salah",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. Ensure user is active
|
||||||
|
if not user.is_active:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Akun dinonaktifkan. Silakan hubungi Administrator."
|
||||||
|
)
|
||||||
|
|
||||||
|
# 4. Generate JWT token
|
||||||
|
access_token = create_access_token(subject=user.username)
|
||||||
|
return Token(
|
||||||
|
access_token=access_token,
|
||||||
|
token_type="bearer",
|
||||||
|
username=user.username,
|
||||||
|
nama=user.nama or user.username,
|
||||||
|
role=user.role or "admin",
|
||||||
|
kode_prodi=user.kode_prodi
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_all_users(db: Session):
|
||||||
|
return user_repository.get_all(db, skip=0, limit=200)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def update_user(db: Session, user_id: int, user_in: UserUpdate):
|
||||||
|
user = user_repository.get(db, user_id)
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Pengguna tidak ditemukan")
|
||||||
|
|
||||||
|
# Check email duplicate
|
||||||
|
if user_in.email and user_in.email != user.email:
|
||||||
|
existing_email = user_repository.get_by_email(db, user_in.email)
|
||||||
|
if existing_email and existing_email.id != user_id:
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Email sudah digunakan oleh akun lain")
|
||||||
|
|
||||||
|
update_data = {}
|
||||||
|
if user_in.nama is not None:
|
||||||
|
update_data["nama"] = user_in.nama
|
||||||
|
if user_in.email is not None:
|
||||||
|
update_data["email"] = user_in.email
|
||||||
|
if user_in.role is not None:
|
||||||
|
update_data["role"] = user_in.role
|
||||||
|
if user_in.kode_prodi is not None:
|
||||||
|
update_data["kode_prodi"] = user_in.kode_prodi
|
||||||
|
if user_in.is_active is not None:
|
||||||
|
update_data["is_active"] = user_in.is_active
|
||||||
|
if user_in.password:
|
||||||
|
update_data["hashed_password"] = get_password_hash(user_in.password)
|
||||||
|
|
||||||
|
return user_repository.update(db, user, update_data)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def delete_user(db: Session, user_id: int):
|
||||||
|
user = user_repository.get(db, user_id)
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Pengguna tidak ditemukan")
|
||||||
|
if user.username == "admin" or user.id == 1:
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Akun Super Admin utama tidak boleh dihapus")
|
||||||
|
return user_repository.delete(db, user_id)
|
||||||
|
|
||||||
|
|
@ -0,0 +1,511 @@
|
||||||
|
"""
|
||||||
|
Service untuk import data tracer study.
|
||||||
|
Logika utama ada di sini.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import pandas as pd
|
||||||
|
from typing import Dict, Any, List, Optional
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.parsers.column_finder import find_column, get_value, check_available_columns
|
||||||
|
from app.parsers.converters import parse_2024_value, parse_date, konversi_jawaban, convert_value_for_field
|
||||||
|
from app.parsers.mappings import (
|
||||||
|
YEAR_CONFIG,
|
||||||
|
IDENTITAS_MAPPING,
|
||||||
|
PEKERJAAN_MAPPING,
|
||||||
|
STUDI_LANJUT_MAPPING,
|
||||||
|
KESESUAIAN_MAPPING,
|
||||||
|
KOMPETENSI_LULUS_MAPPING,
|
||||||
|
KOMPETENSI_SAAT_INI_MAPPING,
|
||||||
|
METODE_PEMBELAJARAN_MAPPING,
|
||||||
|
WAKTU_CARI_KERJA_MAPPING,
|
||||||
|
CARA_CARI_KERJA_MAPPING,
|
||||||
|
LAMARAN_KEAKTIFAN_MAPPING,
|
||||||
|
KESESUAIAN_PEKERJAAN_MAPPING,
|
||||||
|
NUMERIC_FIELDS,
|
||||||
|
get_field_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load_full_mapping():
|
||||||
|
"""Load full column mapping dari file JSON"""
|
||||||
|
try:
|
||||||
|
mapping_path = "C:/Users/syifa/TA-PROJEK/Model/preprocessing/full_column_mapping.json"
|
||||||
|
with open(mapping_path, 'r', encoding='utf-8') as f:
|
||||||
|
return json.load(f)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Could not load full_column_mapping.json: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def import_data(
|
||||||
|
tahun: str,
|
||||||
|
file_content: bytes,
|
||||||
|
filename: str,
|
||||||
|
db: Session
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Import data dari CSV/Excel.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tahun: Tahun data (2019-2025)
|
||||||
|
file_content: Bytes content dari file
|
||||||
|
filename: Nama file untuk tentukan format
|
||||||
|
db: SQLAlchemy session
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict dengan status dan stats
|
||||||
|
"""
|
||||||
|
# Validasi file
|
||||||
|
filename_lower = filename.lower()
|
||||||
|
if not filename_lower.endswith(('.csv', '.xlsx', '.xls')):
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"message": "File harus format CSV atau Excel (.csv, .xlsx, .xls)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Baca file
|
||||||
|
if filename_lower.endswith('.csv'):
|
||||||
|
df = pd.read_csv(io.BytesIO(file_content), sep=';', encoding='utf-8-sig')
|
||||||
|
else:
|
||||||
|
df = pd.read_excel(io.BytesIO(file_content))
|
||||||
|
|
||||||
|
# Bersihkan baris yang seluruhnya kosong (misal trailing empty rows di CSV)
|
||||||
|
df = df.dropna(how='all')
|
||||||
|
|
||||||
|
if df.empty:
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"message": "File kosong"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get source columns
|
||||||
|
source_cols = list(df.columns)
|
||||||
|
|
||||||
|
# Statistics
|
||||||
|
stats = {
|
||||||
|
"tahun": tahun,
|
||||||
|
"total_rows": len(df),
|
||||||
|
"imported": 0,
|
||||||
|
"skipped": 0,
|
||||||
|
"errors": []
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get year-specific configuration
|
||||||
|
year_config = YEAR_CONFIG.get(tahun, {})
|
||||||
|
is_2024_format = bool(year_config.get('is_text_format', False))
|
||||||
|
|
||||||
|
if is_2024_format:
|
||||||
|
print(f"[Import {tahun}] DETECTED: Format 2024 - menggunakan parser khusus")
|
||||||
|
|
||||||
|
# Import models
|
||||||
|
from app.models.alumni import Alumni
|
||||||
|
from app.models.models import Pekerjaan, Kompetensi, TracerResponse
|
||||||
|
|
||||||
|
# Process each row
|
||||||
|
for idx, row in df.iterrows():
|
||||||
|
nim: str = ""
|
||||||
|
try:
|
||||||
|
# ========================================
|
||||||
|
# AMBIL DATA IDENTITAS
|
||||||
|
# ========================================
|
||||||
|
nim = str(get_value(row, source_cols, IDENTITAS_MAPPING['nim'], ''))
|
||||||
|
nama = get_value(row, source_cols, IDENTITAS_MAPPING['nama'], '')
|
||||||
|
|
||||||
|
# Fallback khusus data historis tanpa NIM
|
||||||
|
if (not nim or str(nim).strip() == '' or str(nim).strip().lower() == 'nan') and nama and 'KHOSNOL' in str(nama).upper():
|
||||||
|
nim = 'E41171725'
|
||||||
|
|
||||||
|
if not nim or str(nim).strip() == '' or str(nim).strip().lower() == 'nan':
|
||||||
|
stats["skipped"] += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# CREATE/UPDATE ALUMNI
|
||||||
|
# ========================================
|
||||||
|
existing_alumni = db.query(Alumni).filter(Alumni.nim == nim).first()
|
||||||
|
|
||||||
|
email = get_value(row, source_cols, IDENTITAS_MAPPING['email'], '')
|
||||||
|
raw_prodi = str(get_value(row, source_cols, IDENTITAS_MAPPING['kode_prodi'], ''))
|
||||||
|
|
||||||
|
# Extract clean prodi code (e.g., "Teknik Informatika (55301)" -> "55301", "57401.0" -> "57401")
|
||||||
|
import re
|
||||||
|
match_prodi = re.search(r'\((\d+)\)', raw_prodi)
|
||||||
|
if match_prodi:
|
||||||
|
kode_prodi = match_prodi.group(1)
|
||||||
|
else:
|
||||||
|
cleaned_prodi = raw_prodi.strip()
|
||||||
|
if cleaned_prodi.endswith('.0'):
|
||||||
|
cleaned_prodi = cleaned_prodi[:-2]
|
||||||
|
kode_prodi = cleaned_prodi
|
||||||
|
|
||||||
|
# Parse tahun lulus from row or parameter
|
||||||
|
row_tahun = get_value(row, source_cols, IDENTITAS_MAPPING['tahun_lulus'], None)
|
||||||
|
if row_tahun is not None and str(row_tahun).strip() not in ['', 'nan', 'None']:
|
||||||
|
try:
|
||||||
|
tahun_lulus_val = int(float(str(row_tahun).strip()))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
tahun_lulus_val = int(tahun) if tahun.isdigit() else 2024
|
||||||
|
else:
|
||||||
|
tahun_lulus_val = int(tahun) if tahun.isdigit() else 2024
|
||||||
|
|
||||||
|
# Get status pekerjaan
|
||||||
|
status_sp = str(get_value(row, source_cols, PEKERJAAN_MAPPING['status_saat_ini'], ''))
|
||||||
|
status_kerja = _parse_status_pekerjaan(status_sp)
|
||||||
|
|
||||||
|
# Get data pekerjaan untuk alumni
|
||||||
|
nama_perusahaan = get_value(row, source_cols, PEKERJAAN_MAPPING['nama_perusahaan'], '')
|
||||||
|
jabatan_raw = get_value(row, source_cols, PEKERJAAN_MAPPING['jabatan'], '')
|
||||||
|
if is_2024_format and jabatan_raw:
|
||||||
|
_, jab_teks = parse_2024_value(jabatan_raw)
|
||||||
|
jabatan = jab_teks or str(jabatan_raw)
|
||||||
|
elif jabatan_raw:
|
||||||
|
jab_conv = konversi_jawaban('F5C', jabatan_raw)
|
||||||
|
jabatan = str(jab_conv) if jab_conv is not None else str(jabatan_raw)
|
||||||
|
else:
|
||||||
|
jabatan = ''
|
||||||
|
|
||||||
|
if existing_alumni:
|
||||||
|
existing_alumni.nama = nama
|
||||||
|
existing_alumni.email = email
|
||||||
|
existing_alumni.tahun_lulus = tahun_lulus_val
|
||||||
|
existing_alumni.kode_prodi = kode_prodi
|
||||||
|
existing_alumni.status_kerja = status_kerja
|
||||||
|
existing_alumni.pekerjaan = nama_perusahaan
|
||||||
|
existing_alumni.instansi = jabatan
|
||||||
|
db.merge(existing_alumni)
|
||||||
|
else:
|
||||||
|
alumni = Alumni(
|
||||||
|
nim=nim,
|
||||||
|
nama=nama,
|
||||||
|
email=email,
|
||||||
|
tahun_lulus=tahun_lulus_val,
|
||||||
|
kode_prodi=kode_prodi,
|
||||||
|
status_kerja=status_kerja,
|
||||||
|
pekerjaan=nama_perusahaan,
|
||||||
|
instansi=jabatan
|
||||||
|
)
|
||||||
|
db.add(alumni)
|
||||||
|
|
||||||
|
db.flush()
|
||||||
|
target_alumni = existing_alumni or db.query(Alumni).filter(Alumni.nim == nim).first()
|
||||||
|
if not target_alumni:
|
||||||
|
continue
|
||||||
|
alumni_id = target_alumni.id
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# CREATE/UPDATE PEKERJAAN
|
||||||
|
# ========================================
|
||||||
|
_import_pekerjaan(db, row, source_cols, alumni_id, tahun, is_2024_format)
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# CREATE/UPDATE KOMPETENSI
|
||||||
|
# ========================================
|
||||||
|
_import_kompetensi(db, row, source_cols, alumni_id, tahun, is_2024_format)
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# CREATE/UPDATE TRACER RESPONSE
|
||||||
|
# ========================================
|
||||||
|
_import_tracer_response(db, row, source_cols, alumni_id, tahun, is_2024_format, nim=str(nim) if nim else None)
|
||||||
|
|
||||||
|
db.flush()
|
||||||
|
stats["imported"] += 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
row_idx = int(idx) + 1 if isinstance(idx, (int, float)) else str(idx)
|
||||||
|
stats["errors"].append({
|
||||||
|
"row": row_idx,
|
||||||
|
"nim": nim if nim else "-",
|
||||||
|
"error": str(e)
|
||||||
|
})
|
||||||
|
stats["skipped"] += 1
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": f"Berhasil import {stats['imported']} data dari tahun {tahun}",
|
||||||
|
"stats": stats
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_status_pekerjaan(status_sp: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Parse status pekerjaan dari nilai F8 (Instrumen Tracer Study).
|
||||||
|
Sesuai dokumen Pertanyaan-dan-jawaban.md:
|
||||||
|
1 - Bekerja (full time/part time)
|
||||||
|
2 - Belum memungkinkan bekerja
|
||||||
|
3 - Wiraswasta
|
||||||
|
4 - Melanjutkan Pendidikan
|
||||||
|
5 - Tidak kerja tetapi sedang mencari kerja
|
||||||
|
"""
|
||||||
|
if status_sp is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
sp = status_sp.strip().lower()
|
||||||
|
if sp in ['', 'nan', 'none', '0', '0.0', '-']:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 1. Cek kode numerik (misal: "1", "1 - ...", "... (1)", "2- ...")
|
||||||
|
import re
|
||||||
|
code_match = re.search(r'(?:^|\s|\()([1-5])(?:\s*[-–)]|$)', sp)
|
||||||
|
if code_match:
|
||||||
|
code = code_match.group(1)
|
||||||
|
if code == '1':
|
||||||
|
return 'bekerja'
|
||||||
|
elif code == '2':
|
||||||
|
return 'belum_bekerja'
|
||||||
|
elif code == '3':
|
||||||
|
return 'wiraswasta'
|
||||||
|
elif code == '4':
|
||||||
|
return 'melanjutkan'
|
||||||
|
elif code == '5':
|
||||||
|
return 'mencari_kerja'
|
||||||
|
|
||||||
|
# 2. Fallback pencocokan teks (urutan prioritas penting: cek opsi negatif terlebih dahulu)
|
||||||
|
if 'tidak kerja' in sp or 'mencari kerja' in sp or 'sedang mencari' in sp:
|
||||||
|
return 'mencari_kerja'
|
||||||
|
elif 'belum' in sp:
|
||||||
|
return 'belum_bekerja'
|
||||||
|
elif 'wiraswasta' in sp:
|
||||||
|
return 'wiraswasta'
|
||||||
|
elif 'melanjutkan' in sp or 'pendidikan' in sp or 'studi' in sp:
|
||||||
|
return 'melanjutkan'
|
||||||
|
elif 'bekerja' in sp:
|
||||||
|
return 'bekerja'
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _import_pekerjaan(db: Session, row, source_cols: list, alumni_id: int, tahun: str, is_2024_format: bool):
|
||||||
|
"""Import data pekerjaan"""
|
||||||
|
from app.models.models import Pekerjaan
|
||||||
|
|
||||||
|
existing = db.query(Pekerjaan).filter(Pekerjaan.alumni_id == alumni_id).first()
|
||||||
|
|
||||||
|
values = {}
|
||||||
|
status_sp = str(get_value(row, source_cols, PEKERJAAN_MAPPING['status_saat_ini'], ''))
|
||||||
|
values['status_pekerjaan'] = _parse_status_pekerjaan(status_sp)
|
||||||
|
|
||||||
|
values['nama_perusahaan'] = get_value(row, source_cols, PEKERJAAN_MAPPING['nama_perusahaan'], '')
|
||||||
|
|
||||||
|
jab_raw = get_value(row, source_cols, PEKERJAAN_MAPPING['jabatan'], '')
|
||||||
|
if is_2024_format and jab_raw:
|
||||||
|
_, jab_teks = parse_2024_value(jab_raw)
|
||||||
|
values['jabatan'] = jab_teks or str(jab_raw)
|
||||||
|
elif jab_raw:
|
||||||
|
jab_conv = konversi_jawaban('F5C', jab_raw)
|
||||||
|
values['jabatan'] = str(jab_conv) if jab_conv is not None else str(jab_raw)
|
||||||
|
else:
|
||||||
|
values['jabatan'] = ''
|
||||||
|
|
||||||
|
jp_raw = get_value(row, source_cols, PEKERJAAN_MAPPING['jenis_perusahaan_full'], '')
|
||||||
|
if is_2024_format and jp_raw:
|
||||||
|
_, jp_teks = parse_2024_value(jp_raw)
|
||||||
|
values['jenis_perusahaan'] = jp_teks or str(jp_raw)
|
||||||
|
elif jp_raw:
|
||||||
|
jp_conv = konversi_jawaban('F1101', jp_raw)
|
||||||
|
values['jenis_perusahaan'] = str(jp_conv) if jp_conv is not None else str(jp_raw)
|
||||||
|
else:
|
||||||
|
values['jenis_perusahaan'] = ''
|
||||||
|
|
||||||
|
ting_raw = get_value(row, source_cols, PEKERJAAN_MAPPING['tingkatan_tempat_kerja'], '')
|
||||||
|
if is_2024_format and ting_raw:
|
||||||
|
_, ting_teks = parse_2024_value(ting_raw)
|
||||||
|
values['tingkatan_tempat_kerja'] = ting_teks or str(ting_raw)
|
||||||
|
elif ting_raw:
|
||||||
|
ting_conv = konversi_jawaban('F5D', ting_raw)
|
||||||
|
values['tingkatan_tempat_kerja'] = str(ting_conv) if ting_conv is not None else str(ting_raw)
|
||||||
|
else:
|
||||||
|
values['tingkatan_tempat_kerja'] = ''
|
||||||
|
|
||||||
|
prov_raw = get_value(row, source_cols, PEKERJAAN_MAPPING['provinsi_kerja'], '')
|
||||||
|
if prov_raw is not None and str(prov_raw).strip() not in ['', 'nan', 'None']:
|
||||||
|
if is_2024_format:
|
||||||
|
_, prov_teks = parse_2024_value(prov_raw)
|
||||||
|
values['provinsi'] = prov_teks or str(prov_raw)
|
||||||
|
else:
|
||||||
|
prov_conv = konversi_jawaban('F5A1', prov_raw)
|
||||||
|
values['provinsi'] = str(prov_conv) if prov_conv is not None else str(prov_raw)
|
||||||
|
|
||||||
|
kab_raw = get_value(row, source_cols, PEKERJAAN_MAPPING['kab_kota_kerja'], '')
|
||||||
|
if kab_raw is not None and str(kab_raw).strip() not in ['', 'nan', 'None']:
|
||||||
|
if is_2024_format:
|
||||||
|
_, kab_teks = parse_2024_value(kab_raw)
|
||||||
|
values['kabupaten_kota'] = kab_teks or str(kab_raw)
|
||||||
|
else:
|
||||||
|
kab_conv = konversi_jawaban('F5A2', kab_raw)
|
||||||
|
values['kabupaten_kota'] = str(kab_conv) if kab_conv is not None else str(kab_raw)
|
||||||
|
|
||||||
|
# Gabungkan 4 kolom teks pekerjaan (Status + Jenis + Perusahaan + Jabatan)
|
||||||
|
sp_val = values.get('status_pekerjaan', '') or ''
|
||||||
|
jp_val = values.get('jenis_perusahaan', '') or ''
|
||||||
|
np_val = values.get('nama_perusahaan', '') or ''
|
||||||
|
jab_val = values.get('jabatan', '') or ''
|
||||||
|
teks_pekerjaan = f"{sp_val} {jp_val} {np_val} {jab_val}".strip()
|
||||||
|
values['teks_pekerjaan'] = teks_pekerjaan
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
for key, val in values.items():
|
||||||
|
if hasattr(existing, key) and val is not None:
|
||||||
|
setattr(existing, key, val)
|
||||||
|
db.merge(existing)
|
||||||
|
else:
|
||||||
|
pekerjaan = Pekerjaan(alumni_id=alumni_id)
|
||||||
|
for key, val in values.items():
|
||||||
|
if hasattr(pekerjaan, key) and val is not None:
|
||||||
|
setattr(pekerjaan, key, val)
|
||||||
|
db.add(pekerjaan)
|
||||||
|
|
||||||
|
|
||||||
|
def _import_kompetensi(db: Session, row, source_cols: list, alumni_id: int, tahun: str, is_2024_format: bool):
|
||||||
|
"""Import data kompetensi"""
|
||||||
|
from app.models.models import Kompetensi
|
||||||
|
|
||||||
|
existing = db.query(Kompetensi).filter(Kompetensi.alumni_id == alumni_id).first()
|
||||||
|
|
||||||
|
values = {}
|
||||||
|
for field, cols in KOMPETENSI_LULUS_MAPPING.items():
|
||||||
|
db_key = field.replace('_full', '')
|
||||||
|
val = get_value(row, source_cols, cols)
|
||||||
|
|
||||||
|
if val is None or pd.isna(val) or str(val).strip() in ['', 'nan', 'None', '-', '_']:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if is_2024_format:
|
||||||
|
kode, teks = parse_2024_value(val)
|
||||||
|
if teks:
|
||||||
|
values[db_key] = teks
|
||||||
|
elif kode is not None:
|
||||||
|
field_code = get_field_code(field, KOMPETENSI_LULUS_MAPPING)
|
||||||
|
values[db_key] = konversi_jawaban(field_code, kode)
|
||||||
|
else:
|
||||||
|
values[db_key] = str(val).strip()
|
||||||
|
else:
|
||||||
|
field_code = get_field_code(field, KOMPETENSI_LULUS_MAPPING)
|
||||||
|
converted = konversi_jawaban(field_code, val)
|
||||||
|
if converted is not None:
|
||||||
|
values[db_key] = str(converted)
|
||||||
|
elif str(val).strip() in ['0', '0.0']:
|
||||||
|
values[db_key] = None
|
||||||
|
else:
|
||||||
|
values[db_key] = str(val).strip()
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
for key, val in values.items():
|
||||||
|
if val is not None and hasattr(existing, key):
|
||||||
|
setattr(existing, key, val)
|
||||||
|
db.merge(existing)
|
||||||
|
else:
|
||||||
|
kompetensi = Kompetensi(alumni_id=alumni_id)
|
||||||
|
for key, val in values.items():
|
||||||
|
if val is not None and hasattr(kompetensi, key):
|
||||||
|
setattr(kompetensi, key, val)
|
||||||
|
db.add(kompetensi)
|
||||||
|
|
||||||
|
|
||||||
|
def _import_tracer_response(db: Session, row, source_cols: list, alumni_id: int, tahun: str, is_2024_format: bool, nim: Optional[str] = None):
|
||||||
|
"""Import data tracer response"""
|
||||||
|
from app.models.models import TracerResponse
|
||||||
|
|
||||||
|
existing = db.query(TracerResponse).filter(TracerResponse.alumni_id == alumni_id).first()
|
||||||
|
|
||||||
|
year_config = YEAR_CONFIG.get(tahun, {})
|
||||||
|
skip_jumlah = year_config.get('skip_jumlah_perusahaan', False)
|
||||||
|
|
||||||
|
# Combine all mappings for tracer response
|
||||||
|
all_mapping = {}
|
||||||
|
all_mapping.update(IDENTITAS_MAPPING)
|
||||||
|
all_mapping.update(PEKERJAAN_MAPPING)
|
||||||
|
all_mapping.update(STUDI_LANJUT_MAPPING)
|
||||||
|
all_mapping.update(KESESUAIAN_MAPPING)
|
||||||
|
all_mapping.update(KOMPETENSI_LULUS_MAPPING)
|
||||||
|
all_mapping.update(KOMPETENSI_SAAT_INI_MAPPING)
|
||||||
|
all_mapping.update(METODE_PEMBELAJARAN_MAPPING)
|
||||||
|
all_mapping.update(WAKTU_CARI_KERJA_MAPPING)
|
||||||
|
all_mapping.update(CARA_CARI_KERJA_MAPPING)
|
||||||
|
all_mapping.update(LAMARAN_KEAKTIFAN_MAPPING)
|
||||||
|
all_mapping.update(KESESUAIAN_PEKERJAAN_MAPPING)
|
||||||
|
|
||||||
|
tracer_data = {}
|
||||||
|
|
||||||
|
for field, cols in all_mapping.items():
|
||||||
|
if field in ['nim', 'nama', 'tahun_lulus', 'kode_prodi']:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if skip_jumlah and field == 'jumlah_perusahaan':
|
||||||
|
continue
|
||||||
|
|
||||||
|
val = get_value(row, source_cols, cols)
|
||||||
|
|
||||||
|
if val is None or pd.isna(val):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip invalid values
|
||||||
|
if isinstance(val, str) and val.strip() in ['', 'nan', 'None', '-', '_']:
|
||||||
|
continue
|
||||||
|
|
||||||
|
field_code = get_field_code(field, all_mapping)
|
||||||
|
fc_upper = field_code.upper() if field_code else ''
|
||||||
|
|
||||||
|
if is_2024_format:
|
||||||
|
kode, teks = parse_2024_value(val)
|
||||||
|
|
||||||
|
# Integer columns
|
||||||
|
if field in ['bulan_dapat_kerja', 'bulan_dapat_kerja_setelah', 'bulan_cari_sebelum_lulus', 'bulan_cari_setelah_wisuda']:
|
||||||
|
if kode is not None:
|
||||||
|
tracer_data[field] = kode
|
||||||
|
elif isinstance(val, (int, float)):
|
||||||
|
tracer_data[field] = int(val)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Non-integer columns: prefer clean text response
|
||||||
|
if teks and len(teks) > 0:
|
||||||
|
tracer_data[field] = teks
|
||||||
|
elif kode is not None:
|
||||||
|
converted = konversi_jawaban(field_code, kode)
|
||||||
|
tracer_data[field] = str(converted) if converted is not None else str(kode)
|
||||||
|
else:
|
||||||
|
tracer_data[field] = str(val).strip()
|
||||||
|
else:
|
||||||
|
# Format lama (2019-2023)
|
||||||
|
converted = konversi_jawaban(field_code, val)
|
||||||
|
|
||||||
|
if field in ['bulan_dapat_kerja', 'bulan_dapat_kerja_setelah', 'bulan_cari_sebelum_lulus', 'bulan_cari_setelah_wisuda']:
|
||||||
|
try:
|
||||||
|
tracer_data[field] = int(float(str(val).strip()))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
elif converted is not None:
|
||||||
|
tracer_data[field] = str(converted)
|
||||||
|
elif str(val).strip() in ['0', '0.0'] and fc_upper in ['F1761', 'F1762', 'F1763', 'F1764', 'F1765', 'F1766', 'F1767', 'F1768', 'F1769', 'F1770', 'F1771', 'F1772', 'F1773', 'F1774', 'F21', 'F22', 'F23', 'F24', 'F25', 'F26', 'F27']:
|
||||||
|
# 0 for competency/learning method in old format means not answered / None
|
||||||
|
tracer_data[field] = None
|
||||||
|
else:
|
||||||
|
tracer_data[field] = str(val).strip()
|
||||||
|
|
||||||
|
# Handle date field
|
||||||
|
tanggal_masuk = get_value(row, source_cols, STUDI_LANJUT_MAPPING['tanggal_masuk_studi'])
|
||||||
|
if tanggal_masuk is not None and not pd.isna(tanggal_masuk):
|
||||||
|
parsed_date = parse_date(tanggal_masuk)
|
||||||
|
tracer_data['tanggal_masuk_studi'] = parsed_date
|
||||||
|
else:
|
||||||
|
tracer_data['tanggal_masuk_studi'] = None
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
if nim and hasattr(existing, 'nim'):
|
||||||
|
existing.nim = nim
|
||||||
|
for key, val in tracer_data.items():
|
||||||
|
if hasattr(existing, key):
|
||||||
|
setattr(existing, key, val)
|
||||||
|
db.merge(existing)
|
||||||
|
else:
|
||||||
|
tracer = TracerResponse(alumni_id=alumni_id, nim=nim)
|
||||||
|
for key, val in tracer_data.items():
|
||||||
|
if hasattr(tracer, key):
|
||||||
|
setattr(tracer, key, val)
|
||||||
|
db.add(tracer)
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,60 @@
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
from app.repositories.prodi import ProdiRepository
|
||||||
|
from app.schemas.prodi import ProdiCreate, ProdiUpdate
|
||||||
|
|
||||||
|
|
||||||
|
class ProdiService:
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_all(db: Session):
|
||||||
|
return ProdiRepository.get_all(db)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_by_id(db: Session, prodi_id: int):
|
||||||
|
prodi = ProdiRepository.get_by_id(db, prodi_id)
|
||||||
|
|
||||||
|
if not prodi:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="Data prodi tidak ditemukan"
|
||||||
|
)
|
||||||
|
|
||||||
|
return prodi
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create(db: Session, prodi: ProdiCreate):
|
||||||
|
# Cek apakah kode prodi sudah digunakan
|
||||||
|
data = ProdiRepository.get_all(db)
|
||||||
|
|
||||||
|
for item in data:
|
||||||
|
if item.kode_prodi == prodi.kode_prodi:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="Kode prodi sudah digunakan"
|
||||||
|
)
|
||||||
|
|
||||||
|
return ProdiRepository.create(db, prodi)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def update(db: Session, prodi_id: int, prodi: ProdiUpdate):
|
||||||
|
|
||||||
|
if not ProdiRepository.get_by_id(db, prodi_id):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="Data prodi tidak ditemukan"
|
||||||
|
)
|
||||||
|
|
||||||
|
return ProdiRepository.update(db, prodi_id, prodi)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def delete(db: Session, prodi_id: int):
|
||||||
|
|
||||||
|
if not ProdiRepository.get_by_id(db, prodi_id):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="Data prodi tidak ditemukan"
|
||||||
|
)
|
||||||
|
|
||||||
|
return ProdiRepository.delete(db, prodi_id)
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
def format_response(success: bool, message: str, data: Optional[Any] = None) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Format standard API JSON responses to match client-side expectations.
|
||||||
|
Usage Example: return format_response(True, "Data fetched successfully", alumni_list)
|
||||||
|
"""
|
||||||
|
response = {
|
||||||
|
"success": success,
|
||||||
|
"message": message
|
||||||
|
}
|
||||||
|
if data is not None:
|
||||||
|
response["data"] = data
|
||||||
|
return response
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from typing import Any, Union, Optional
|
||||||
|
from jose import jwt
|
||||||
|
import bcrypt
|
||||||
|
from app.config.config import settings
|
||||||
|
|
||||||
|
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||||
|
"""Compare a plain text password with its bcrypt hash to verify identity."""
|
||||||
|
return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password.encode('utf-8'))
|
||||||
|
|
||||||
|
def get_password_hash(password: str) -> str:
|
||||||
|
"""Generate a bcrypt hash from a plain text password."""
|
||||||
|
return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
|
||||||
|
|
||||||
|
def create_access_token(subject: Union[str, Any], expires_delta: Optional[timedelta] = None) -> str:
|
||||||
|
"""
|
||||||
|
Generate a JSON Web Token (JWT) containing the subject (e.g. username/id).
|
||||||
|
Accepts an optional expiration delta, defaulting to the config setting.
|
||||||
|
"""
|
||||||
|
if expires_delta:
|
||||||
|
expire = datetime.now(timezone.utc) + expires_delta
|
||||||
|
else:
|
||||||
|
expire = datetime.now(timezone.utc) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||||
|
|
||||||
|
to_encode = {"exp": expire, "sub": str(subject)}
|
||||||
|
encoded_jwt = jwt.encode(to_encode, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM)
|
||||||
|
return encoded_jwt
|
||||||
|
|
@ -0,0 +1,193 @@
|
||||||
|
"""
|
||||||
|
Standarisasi Kolom Module
|
||||||
|
Memetakan kolom dari berbagai tahun ke kolom standar
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class ColumnStandardizer:
|
||||||
|
"""
|
||||||
|
Kelas untuk menstandarisasi kolom dari berbagai file Excel
|
||||||
|
dengan struktur kolom yang berbeda-beda
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, mapping_file: str = None):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
mapping_file: Path ke file JSON mapping kolom
|
||||||
|
"""
|
||||||
|
if mapping_file is None:
|
||||||
|
# Default path
|
||||||
|
mapping_file = Path(__file__).parent.parent / "column_mapping.json"
|
||||||
|
|
||||||
|
self.mapping_file = Path(mapping_file)
|
||||||
|
self.mappings = self._load_mappings()
|
||||||
|
|
||||||
|
def _load_mappings(self) -> Dict:
|
||||||
|
"""Load mapping dari file JSON"""
|
||||||
|
if not self.mapping_file.exists():
|
||||||
|
raise FileNotFoundError(f"Mapping file tidak ditemukan: {self.mapping_file}")
|
||||||
|
|
||||||
|
with open(self.mapping_file, 'r', encoding='utf-8') as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
def get_mapping(self, year: str) -> Dict:
|
||||||
|
"""
|
||||||
|
Ambil mapping untuk tahun tertentu
|
||||||
|
|
||||||
|
Args:
|
||||||
|
year: Tahun data (2022, 2023, 2024)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary dengan mapping kolom
|
||||||
|
"""
|
||||||
|
if year not in self.mappings:
|
||||||
|
raise ValueError(f"Mapping untuk tahun {year} tidak ditemukan. "
|
||||||
|
f"Tahun yang tersedia: {list(self.mappings.keys())}")
|
||||||
|
return self.mappings[year]
|
||||||
|
|
||||||
|
def detect_year(self, df: pd.DataFrame, filename: str = None) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Deteksi tahun dari DataFrame atau filename
|
||||||
|
|
||||||
|
Args:
|
||||||
|
df: DataFrame untuk dicek kolomnya
|
||||||
|
filename: Nama file (opsional)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tahun yang terdeteksi atau None
|
||||||
|
"""
|
||||||
|
# Cek dari filename
|
||||||
|
if filename:
|
||||||
|
for year in self.mappings.keys():
|
||||||
|
if year in filename:
|
||||||
|
return year
|
||||||
|
|
||||||
|
# Cek dari kolom yang tersedia
|
||||||
|
available_years = []
|
||||||
|
for year, mapping in self.mappings.items():
|
||||||
|
match_count = sum(1 for col in mapping.values() if col in df.columns)
|
||||||
|
if match_count >= 2: # Minimal 2 kolom cocok
|
||||||
|
available_years.append((year, match_count))
|
||||||
|
|
||||||
|
if available_years:
|
||||||
|
# Pilih tahun dengan match terbanyak
|
||||||
|
return max(available_years, key=lambda x: x[1])[0]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def standardize(self, df: pd.DataFrame, year: str = None, filename: str = None) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Standarisasi kolom DataFrame
|
||||||
|
|
||||||
|
Args:
|
||||||
|
df: DataFrame asli
|
||||||
|
year: Tahun data (auto-detect jika None)
|
||||||
|
filename: Nama file (opsional, untuk auto-detect)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DataFrame dengan kolom standar
|
||||||
|
"""
|
||||||
|
# Deteksi tahun jika tidak specified
|
||||||
|
if year is None:
|
||||||
|
year = self.detect_year(df, filename)
|
||||||
|
|
||||||
|
if year is None:
|
||||||
|
raise ValueError("Tidak dapat mendeteksi tahun data. "
|
||||||
|
f"Pastikan kolom sesuai dengan mapping yang tersedia.")
|
||||||
|
|
||||||
|
# Ambil mapping
|
||||||
|
mapping = self.get_mapping(year)
|
||||||
|
|
||||||
|
# Buat DataFrame baru dengan kolom standar
|
||||||
|
result = pd.DataFrame()
|
||||||
|
for target_col, source_col in mapping.items():
|
||||||
|
if source_col in df.columns:
|
||||||
|
result[target_col] = df[source_col]
|
||||||
|
else:
|
||||||
|
result[target_col] = None
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def standardize_multiple(self, dfs: List[pd.DataFrame],
|
||||||
|
years: List[str] = None,
|
||||||
|
filenames: List[str] = None) -> List[pd.DataFrame]:
|
||||||
|
"""
|
||||||
|
Standarisasi multiple DataFrame sekaligus
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dfs: List DataFrame
|
||||||
|
years: List tahun (auto-detect jika None)
|
||||||
|
filenames: List nama file (opsional)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List DataFrame yang sudah distandarisasi
|
||||||
|
"""
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for i, df in enumerate(dfs):
|
||||||
|
year = years[i] if years else None
|
||||||
|
filename = filenames[i] if filenames else None
|
||||||
|
|
||||||
|
standardized = self.standardize(df, year, filename)
|
||||||
|
results.append(standardized)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def merge_datasets(dfs: List[pd.DataFrame]) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Gabungkan multiple DataFrame menjadi satu
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dfs: List DataFrame
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DataFrame gabungan
|
||||||
|
"""
|
||||||
|
return pd.concat(dfs, ignore_index=True)
|
||||||
|
|
||||||
|
|
||||||
|
def clean_data(df: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Basic data cleaning
|
||||||
|
|
||||||
|
Args:
|
||||||
|
df: DataFrame
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DataFrame yang sudah dibersihkan
|
||||||
|
"""
|
||||||
|
result = df.copy()
|
||||||
|
|
||||||
|
# Hapus baris dengan semua nilai kosong
|
||||||
|
result = result.dropna(how='all')
|
||||||
|
|
||||||
|
# Hapus duplikat
|
||||||
|
result = result.drop_duplicates()
|
||||||
|
|
||||||
|
# Isi nilai kosong dengan string kosong
|
||||||
|
result = result.fillna('')
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Contoh penggunaan
|
||||||
|
standardizer = ColumnStandardizer()
|
||||||
|
|
||||||
|
# Sample data 2024
|
||||||
|
sample_2024 = pd.DataFrame({
|
||||||
|
"4. Nama Lengkap / nmmhsmsmh (Gunakan Huruf Besar)": ["Ali", "Budi"],
|
||||||
|
"9. Apa nama perusahaan/kantor tempat Anda bekerja? f5b": ["Google", "Tokopedia"],
|
||||||
|
"10. Bila Anda berwiraswasta, apa posisi/jabatan Anda saat ini ? f5c": ["Engineer", "Analyst"]
|
||||||
|
})
|
||||||
|
|
||||||
|
# Standarisasi
|
||||||
|
standardized = standardizer.standardize(sample_2024, year="2024")
|
||||||
|
print("Data Standarisasi:")
|
||||||
|
print(standardized)
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"2022": {
|
||||||
|
"nama": "Nama Lengkap",
|
||||||
|
"perusahaan": "Nama Perusahaan",
|
||||||
|
"jabatan": "Jabatan",
|
||||||
|
"nim": "NIM"
|
||||||
|
},
|
||||||
|
"2023": {
|
||||||
|
"nama": "Nama Alumni",
|
||||||
|
"perusahaan": "Perusahaan",
|
||||||
|
"jabatan": "Posisi",
|
||||||
|
"nim": "NIU"
|
||||||
|
},
|
||||||
|
"2024": {
|
||||||
|
"nama": "4. Nama Lengkap / nmmhsmsmh (Gunakan Huruf Besar)",
|
||||||
|
"perusahaan": "9. Apa nama perusahaan/kantor tempat Anda bekerja? f5b",
|
||||||
|
"jabatan": "10. Bila Anda berwiraswasta, apa posisi/jabatan Anda saat ini ? f5c",
|
||||||
|
"nim": "3. NIM / nimhsmsmh (Gunakan Huruf Besar) "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS tracer_responses (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
alumni_id INT NOT NULL UNIQUE,
|
||||||
|
kode_pt VARCHAR(20),
|
||||||
|
nik VARCHAR(20),
|
||||||
|
npwp VARCHAR(20),
|
||||||
|
telepon VARCHAR(20),
|
||||||
|
email VARCHAR(255),
|
||||||
|
status_saat_ini VARCHAR(50),
|
||||||
|
bulan_dapat_kerja INT,
|
||||||
|
dapet_kerja_6_bulan INT,
|
||||||
|
rata_pendapatan VARCHAR(50),
|
||||||
|
bulan_dapat_kerja_setelah INT,
|
||||||
|
provinsi_kerja VARCHAR(100),
|
||||||
|
kab_kota_kerja VARCHAR(100),
|
||||||
|
jenis_perusahaan_full VARCHAR(100),
|
||||||
|
jumlah_perusahaan INT,
|
||||||
|
nama_perusahaan VARCHAR(255),
|
||||||
|
jabatan VARCHAR(150),
|
||||||
|
tingkatan_tempat_kerja VARCHAR(50),
|
||||||
|
sumber_biaya_studi VARCHAR(50),
|
||||||
|
pt_studi_lanjut VARCHAR(255),
|
||||||
|
prodi_studi_lanjut VARCHAR(255),
|
||||||
|
tanggal_masuk_studi DATETIME,
|
||||||
|
sumber_biaya_kuliah VARCHAR(50),
|
||||||
|
keeratan_bidang_studi VARCHAR(50),
|
||||||
|
tingkat_pendidikan_tepat VARCHAR(50),
|
||||||
|
etika_lulus_full INT,
|
||||||
|
etika_saat_ini INT,
|
||||||
|
keahlian_lulus_full INT,
|
||||||
|
keahlian_saat_ini INT,
|
||||||
|
bahasa_inggris_lulus_full INT,
|
||||||
|
bahasa_inggris_saat_ini INT,
|
||||||
|
teknologi_informasi_lulus_full INT,
|
||||||
|
teknologi_informasi_saat_ini INT,
|
||||||
|
komunikasi_lulus_full INT,
|
||||||
|
komunikasi_saat_ini INT,
|
||||||
|
kerja_sama_lulus_full INT,
|
||||||
|
kerja_sama_saat_ini INT,
|
||||||
|
pengembangan_diri_lulus_full INT,
|
||||||
|
pengembangan_diri_saat_ini INT,
|
||||||
|
metode_perkuliahan INT,
|
||||||
|
metode_demonstrasi INT,
|
||||||
|
metode_proyek_riset INT,
|
||||||
|
metode_magang INT,
|
||||||
|
metode_praktikum INT,
|
||||||
|
metode_kerja_lapangan INT,
|
||||||
|
metode_diskusi INT,
|
||||||
|
mulai_cari_kerja VARCHAR(50),
|
||||||
|
bulan_cari_sebelum_lulus INT,
|
||||||
|
bulan_cari_setelah_wisuda INT,
|
||||||
|
cari_iklan INT,
|
||||||
|
cari_melamar_tanpa_lowongan INT,
|
||||||
|
cari_bursa_pameran INT,
|
||||||
|
cari_internet INT,
|
||||||
|
cari_dihubungi_perusahaan INT,
|
||||||
|
cari_kemenakertrans INT,
|
||||||
|
cari_agen_tenaga_kerja INT,
|
||||||
|
cari_pusat_karir INT,
|
||||||
|
cari_kantor_mahasiswa INT,
|
||||||
|
cari_jejaring INT,
|
||||||
|
cari_relasi INT,
|
||||||
|
cari_bangun_usaha INT,
|
||||||
|
cari_magang INT,
|
||||||
|
cari_tempat_kuliah INT,
|
||||||
|
cari_lainnya INT,
|
||||||
|
jumlah_perusahaan_dilamar INT,
|
||||||
|
jumlah_perusahaan_merespons INT,
|
||||||
|
jumlah_perusahaan_wawancara INT,
|
||||||
|
aktif_cari_kerja_4_minggu VARCHAR(50),
|
||||||
|
kerja_tidak_sesuai_1 VARCHAR(50),
|
||||||
|
kerja_tidak_sesuai_2 VARCHAR(50),
|
||||||
|
alasan_tidak_sesuai_1 VARCHAR(50),
|
||||||
|
alasan_tidak_sesuai_2 VARCHAR(50),
|
||||||
|
alasan_tidak_sesuai_3 VARCHAR(50),
|
||||||
|
alasan_tidak_sesuai_4 VARCHAR(50),
|
||||||
|
alasan_tidak_sesuai_5 VARCHAR(50),
|
||||||
|
alasan_tidak_sesuai_6 VARCHAR(50),
|
||||||
|
alasan_tidak_sesuai_7 VARCHAR(50),
|
||||||
|
alasan_tidak_sesuai_8 VARCHAR(50),
|
||||||
|
alasan_tidak_sesuai_9 VARCHAR(50),
|
||||||
|
alasan_tidak_sesuai_10 VARCHAR(50),
|
||||||
|
alasan_tidak_sesuai_11 VARCHAR(50),
|
||||||
|
alasan_tidak_sesuai_lainnya TEXT,
|
||||||
|
data_extra TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (alumni_id) REFERENCES alumni(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,275 @@
|
||||||
|
import pymysql
|
||||||
|
|
||||||
|
conn = pymysql.connect(host='localhost', user='root', password='', database='tracerstudy_db', port=3306, charset='utf8mb4')
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# ===== PERTANYAAN =====
|
||||||
|
|
||||||
|
questions_data = [
|
||||||
|
# Section 1 - Identitas
|
||||||
|
{'kode': 'kdptimsmh', 'pertanyaan': 'Kode Perguruan Tinggi', 'tipe': 'teks', 'section': 'identitas', 'nomor_form': 1},
|
||||||
|
{'kode': 'kdpstmsnmh', 'pertanyaan': 'Kode Program Studi', 'tipe': 'pilihan', 'section': 'identitas', 'nomor_form': 2},
|
||||||
|
{'kode': 'nimhsmsnmh', 'pertanyaan': 'NIM / Nomor Mahasiswa', 'tipe': 'teks', 'section': 'identitas', 'nomor_form': 3},
|
||||||
|
{'kode': 'nmmhsmsnmh', 'pertanyaan': 'Nama Lengkap', 'tipe': 'teks', 'section': 'identitas', 'nomor_form': 4},
|
||||||
|
{'kode': 'telepon', 'pertanyaan': 'Nomor Telepon/HP (Whatsapp)', 'tipe': 'teks', 'section': 'identitas', 'nomor_form': 5},
|
||||||
|
{'kode': 'email', 'pertanyaan': 'Alamat Email', 'tipe': 'teks', 'section': 'identitas', 'nomor_form': 6},
|
||||||
|
{'kode': 'tahun_lulus', 'pertanyaan': 'Tahun Lulus', 'tipe': 'dropdown', 'section': 'identitas', 'nomor_form': 7},
|
||||||
|
{'kode': 'nik', 'pertanyaan': 'NIK / Nomor Induk Kependudukan', 'tipe': 'teks', 'section': 'identitas', 'nomor_form': 8},
|
||||||
|
{'kode': 'npwp', 'pertanyaan': 'NPWP', 'tipe': 'teks', 'section': 'identitas', 'nomor_form': 9},
|
||||||
|
|
||||||
|
# Section 2 - Kuesioner Utama
|
||||||
|
{'kode': 'F8', 'pertanyaan': 'Jelaskan status Anda saat ini?', 'tipe': 'dropdown', 'section': 'kuesioner_utama', 'nomor_form': 1},
|
||||||
|
{'kode': 'F502', 'pertanyaan': 'Dalam berapa bulan Anda mendapatkan pekerjaan pertama?', 'tipe': 'dropdown', 'section': 'kuesioner_utama', 'nomor_form': 2},
|
||||||
|
{'kode': 'F505', 'pertanyaan': 'Berapa rata-rata pendapatan Anda per bulan?', 'tipe': 'pilihan', 'section': 'kuesioner_utama', 'nomor_form': 4},
|
||||||
|
{'kode': 'F5a1', 'pertanyaan': 'Provinsi tempat bekerja', 'tipe': 'dropdown', 'section': 'kuesioner_utama', 'nomor_form': 5},
|
||||||
|
{'kode': 'F5a2', 'pertanyaan': 'Kabupaten/Kota tempat bekerja', 'tipe': 'dropdown', 'section': 'kuesioner_utama', 'nomor_form': 6},
|
||||||
|
{'kode': 'F1101', 'pertanyaan': 'Jenis perusahaan/instansi/institusi tempat bekerja', 'tipe': 'pilihan', 'section': 'kuesioner_utama', 'nomor_form': 7},
|
||||||
|
{'kode': 'F5b', 'pertanyaan': 'Nama perusahaan/kantor', 'tipe': 'teks', 'section': 'kuesioner_utama', 'nomor_form': 8},
|
||||||
|
{'kode': 'F5c', 'pertanyaan': 'Posisi/Jabatan (Wiraswasta)', 'tipe': 'pilihan', 'section': 'kuesioner_utama', 'nomor_form': 9},
|
||||||
|
{'kode': 'F5d', 'pertanyaan': 'Tingkatan tempat kerja', 'tipe': 'pilihan', 'section': 'kuesioner_utama', 'nomor_form': 10},
|
||||||
|
|
||||||
|
# Studi Lanjut
|
||||||
|
{'kode': 'F18a', 'pertanyaan': 'Sumber biaya Studi Lanjut', 'tipe': 'dropdown', 'section': 'studi_lanjut', 'nomor_form': 11},
|
||||||
|
{'kode': 'F18b', 'pertanyaan': 'Nama PT Studi Lanjut', 'tipe': 'teks', 'section': 'studi_lanjut', 'nomor_form': 12},
|
||||||
|
{'kode': 'F18c', 'pertanyaan': 'Program Studi Studi Lanjut', 'tipe': 'teks', 'section': 'studi_lanjut', 'nomor_form': 13},
|
||||||
|
{'kode': 'F18d', 'pertanyaan': 'Tanggal Masuk Studi Lanjut', 'tipe': 'date', 'section': 'studi_lanjut', 'nomor_form': 14},
|
||||||
|
|
||||||
|
# Pembiayaan & Kesesuaian
|
||||||
|
{'kode': 'F1201', 'pertanyaan': 'Sumber pembiayaan kuliah', 'tipe': 'pilihan', 'section': 'kesesuaian', 'nomor_form': 15},
|
||||||
|
{'kode': 'F14', 'pertanyaan': 'Seberapa erat hubungan bidang studi dengan pekerjaan', 'tipe': 'pilihan', 'section': 'kesesuaian', 'nomor_form': 16},
|
||||||
|
{'kode': 'F15', 'pertanyaan': 'Tingkat pendidikan yang tepat untuk pekerjaan saat ini', 'tipe': 'pilihan', 'section': 'kesesuaian', 'nomor_form': 17},
|
||||||
|
|
||||||
|
# Kompetensi
|
||||||
|
{'kode': 'F1761', 'pertanyaan': 'Kompetensi ETIKA saat LULUS', 'tipe': 'skala', 'section': 'kompetensi', 'nomor_form': 18},
|
||||||
|
{'kode': 'F1762', 'pertanyaan': 'Kompetensi ETIKA saat INI', 'tipe': 'skala', 'section': 'kompetensi', 'nomor_form': 19},
|
||||||
|
{'kode': 'F1763', 'pertanyaan': 'Kompetensi KEAHLIAN BIDANG ILMU saat LULUS', 'tipe': 'skala', 'section': 'kompetensi', 'nomor_form': 20},
|
||||||
|
{'kode': 'F1764', 'pertanyaan': 'Kompetensi KEAHLIAN BIDANG ILMU saat INI', 'tipe': 'skala', 'section': 'kompetensi', 'nomor_form': 21},
|
||||||
|
{'kode': 'F1765', 'pertanyaan': 'Kompetensi BAHASA INGGRIS saat LULUS', 'tipe': 'skala', 'section': 'kompetensi', 'nomor_form': 22},
|
||||||
|
{'kode': 'F1766', 'pertanyaan': 'Kompetensi BAHASA INGGRIS saat INI', 'tipe': 'skala', 'section': 'kompetensi', 'nomor_form': 23},
|
||||||
|
{'kode': 'F1767', 'pertanyaan': 'Kompetensi TEKNOLOGI INFORMASI saat LULUS', 'tipe': 'skala', 'section': 'kompetensi', 'nomor_form': 24},
|
||||||
|
{'kode': 'F1768', 'pertanyaan': 'Kompetensi TEKNOLOGI INFORMASI saat INI', 'tipe': 'skala', 'section': 'kompetensi', 'nomor_form': 25},
|
||||||
|
{'kode': 'F1769', 'pertanyaan': 'Kompetensi KOMUNIKASI saat LULUS', 'tipe': 'skala', 'section': 'kompetensi', 'nomor_form': 26},
|
||||||
|
{'kode': 'F1770', 'pertanyaan': 'Kompetensi KOMUNIKASI saat INI', 'tipe': 'skala', 'section': 'kompetensi', 'nomor_form': 27},
|
||||||
|
{'kode': 'F1771', 'pertanyaan': 'Kompetensi KERJA SAMA TIM saat LULUS', 'tipe': 'skala', 'section': 'kompetensi', 'nomor_form': 28},
|
||||||
|
{'kode': 'F1772', 'pertanyaan': 'Kompetensi KERJA SAMA TIM saat INI', 'tipe': 'skala', 'section': 'kompetensi', 'nomor_form': 29},
|
||||||
|
{'kode': 'F1773', 'pertanyaan': 'Kompetensi PENGEMBANGAN DIRI saat LULUS', 'tipe': 'skala', 'section': 'kompetensi', 'nomor_form': 30},
|
||||||
|
{'kode': 'F1774', 'pertanyaan': 'Kompetensi PENGEMBANGAN DIRI saat INI', 'tipe': 'skala', 'section': 'kompetensi', 'nomor_form': 31},
|
||||||
|
|
||||||
|
# Metode Pembelajaran
|
||||||
|
{'kode': 'F21', 'pertanyaan': 'Perkuliahan', 'tipe': 'skala', 'section': 'metode_pembelajaran', 'nomor_form': 32},
|
||||||
|
{'kode': 'F22', 'pertanyaan': 'Demonstrasi', 'tipe': 'skala', 'section': 'metode_pembelajaran', 'nomor_form': 33},
|
||||||
|
{'kode': 'F23', 'pertanyaan': 'Partisipasi dalam proyek riset', 'tipe': 'skala', 'section': 'metode_pembelajaran', 'nomor_form': 34},
|
||||||
|
{'kode': 'F24', 'pertanyaan': 'Magang', 'tipe': 'skala', 'section': 'metode_pembelajaran', 'nomor_form': 35},
|
||||||
|
{'kode': 'F25', 'pertanyaan': 'Praktikum', 'tipe': 'skala', 'section': 'metode_pembelajaran', 'nomor_form': 36},
|
||||||
|
{'kode': 'F26', 'pertanyaan': 'Kerja Lapangan', 'tipe': 'skala', 'section': 'metode_pembelajaran', 'nomor_form': 37},
|
||||||
|
{'kode': 'F27', 'pertanyaan': 'Diskusi', 'tipe': 'skala', 'section': 'metode_pembelajaran', 'nomor_form': 38},
|
||||||
|
|
||||||
|
# Waktu Cari Kerja
|
||||||
|
{'kode': 'F301', 'pertanyaan': 'Kapan mulai mencari pekerjaan?', 'tipe': 'pilihan', 'section': 'waktu_cari_kerja', 'nomor_form': 39},
|
||||||
|
{'kode': 'F302', 'pertanyaan': 'Berapa bulan sebelum lulus mencari kerja?', 'tipe': 'dropdown', 'section': 'waktu_cari_kerja', 'nomor_form': 40},
|
||||||
|
{'kode': 'F303', 'pertanyaan': 'Berapa bulan setelah wisuda mencari kerja?', 'tipe': 'dropdown', 'section': 'waktu_cari_kerja', 'nomor_form': 41},
|
||||||
|
|
||||||
|
# Cara Cari Kerja
|
||||||
|
{'kode': 'F401', 'pertanyaan': 'Melamar melalui iklan di koran/majalah/brosur?', 'tipe': 'ya_tidak', 'section': 'cara_cari_kerja', 'nomor_form': 42},
|
||||||
|
{'kode': 'F402', 'pertanyaan': 'Melamar ke perusahaan tanpa mengetahui lowongan?', 'tipe': 'ya_tidak', 'section': 'cara_cari_kerja', 'nomor_form': 43},
|
||||||
|
{'kode': 'F403', 'pertanyaan': 'Pergi ke bursa/pameran kerja?', 'tipe': 'ya_tidak', 'section': 'cara_cari_kerja', 'nomor_form': 44},
|
||||||
|
{'kode': 'F404', 'pertanyaan': 'Mencari lewat internet/iklan online/milis?', 'tipe': 'ya_tidak', 'section': 'cara_cari_kerja', 'nomor_form': 45},
|
||||||
|
{'kode': 'F405', 'pertanyaan': 'Dihubungi perusahaan?', 'tipe': 'ya_tidak', 'section': 'cara_cari_kerja', 'nomor_form': 46},
|
||||||
|
{'kode': 'F406', 'pertanyaan': 'Menghubungi Kemenakertrans?', 'tipe': 'ya_tidak', 'section': 'cara_cari_kerja', 'nomor_form': 47},
|
||||||
|
{'kode': 'F407', 'pertanyaan': 'Menghubungi agen tenaga kerja komersial/swasta?', 'tipe': 'ya_tidak', 'section': 'cara_cari_kerja', 'nomor_form': 48},
|
||||||
|
{'kode': 'F408', 'pertanyaan': 'Informasi dari pusat/kantor pengembangan karir?', 'tipe': 'ya_tidak', 'section': 'cara_cari_kerja', 'nomor_form': 49},
|
||||||
|
{'kode': 'F409', 'pertanyaan': 'Menghubungi kantor kemahasiswaan?', 'tipe': 'ya_tidak', 'section': 'cara_cari_kerja', 'nomor_form': 50},
|
||||||
|
{'kode': 'F410', 'pertanyaan': 'Membangun jejaring (network)?', 'tipe': 'ya_tidak', 'section': 'cara_cari_kerja', 'nomor_form': 51},
|
||||||
|
{'kode': 'F411', 'pertanyaan': 'Melaui relasi (dosen, orang tua, teman)?', 'tipe': 'ya_tidak', 'section': 'cara_cari_kerja', 'nomor_form': 52},
|
||||||
|
{'kode': 'F412', 'pertanyaan': 'Membangun bisnis sendiri?', 'tipe': 'ya_tidak', 'section': 'cara_cari_kerja', 'nomor_form': 53},
|
||||||
|
{'kode': 'F413', 'pertanyaan': 'Melalui penempatan kerja/magang?', 'tipe': 'ya_tidak', 'section': 'cara_cari_kerja', 'nomor_form': 54},
|
||||||
|
{'kode': 'F414', 'pertanyaan': 'Bekerja di tempat yang sama dengan waktu kuliah?', 'tipe': 'ya_tidak', 'section': 'cara_cari_kerja', 'nomor_form': 55},
|
||||||
|
{'kode': 'F415', 'pertanyaan': 'Cara lain?', 'tipe': 'ya_tidak', 'section': 'cara_cari_kerja', 'nomor_form': 56},
|
||||||
|
|
||||||
|
# Jumlah Lamaran
|
||||||
|
{'kode': 'F6', 'pertanyaan': 'Jumlah perusahaan yang dilamar', 'tipe': 'dropdown', 'section': 'lamaran', 'nomor_form': 57},
|
||||||
|
{'kode': 'F7', 'pertanyaan': 'Jumlah perusahaan yang merespons', 'tipe': 'dropdown', 'section': 'lamaran', 'nomor_form': 58},
|
||||||
|
{'kode': 'F7a', 'pertanyaan': 'Jumlah perusahaan yang mengundang wawancara', 'tipe': 'dropdown', 'section': 'lamaran', 'nomor_form': 59},
|
||||||
|
|
||||||
|
# Aktif Cari Kerja
|
||||||
|
{'kode': 'F1001', 'pertanyaan': 'Aktif mencari pekerjaan dalam 4 minggu terakhir?', 'tipe': 'pilihan', 'section': 'aktif_cari_kerja', 'nomor_form': 60},
|
||||||
|
|
||||||
|
# Kesesuaian Pekerjaan
|
||||||
|
{'kode': 'F1601', 'pertanyaan': 'Pekerjaan sesuai dengan pendidikan?', 'tipe': 'pilihan', 'section': 'kesesuaian_pekerjaan', 'nomor_form': 61},
|
||||||
|
{'kode': 'F1602', 'pertanyaan': 'Alasan belum dapat pekerjaan sesuai?', 'tipe': 'pilihan', 'section': 'kesesuaian_pekerjaan', 'nomor_form': 62},
|
||||||
|
{'kode': 'F1603', 'pertanyaan': 'Alasan mengambil pekerjaan tidak sesuai (1)?', 'tipe': 'ya_tidak', 'section': 'alasan_tidak_sesuai', 'nomor_form': 63},
|
||||||
|
{'kode': 'F1604', 'pertanyaan': 'Alasan mengambil pekerjaan tidak sesuai (2)?', 'tipe': 'ya_tidak', 'section': 'alasan_tidak_sesuai', 'nomor_form': 64},
|
||||||
|
{'kode': 'F1605', 'pertanyaan': 'Alasan mengambil pekerjaan tidak sesuai (3)?', 'tipe': 'ya_tidak', 'section': 'alasan_tidak_sesuai', 'nomor_form': 65},
|
||||||
|
{'kode': 'F1606', 'pertanyaan': 'Alasan mengambil pekerjaan tidak sesuai (4)?', 'tipe': 'ya_tidak', 'section': 'alasan_tidak_sesuai', 'nomor_form': 66},
|
||||||
|
{'kode': 'F1607', 'pertanyaan': 'Alasan mengambil pekerjaan tidak sesuai (5)?', 'tipe': 'ya_tidak', 'section': 'alasan_tidak_sesuai', 'nomor_form': 67},
|
||||||
|
{'kode': 'F1608', 'pertanyaan': 'Alasan mengambil pekerjaan tidak sesuai (6)?', 'tipe': 'ya_tidak', 'section': 'alasan_tidak_sesuai', 'nomor_form': 68},
|
||||||
|
{'kode': 'F1609', 'pertanyaan': 'Alasan mengambil pekerjaan tidak sesuai (7)?', 'tipe': 'ya_tidak', 'section': 'alasan_tidak_sesuai', 'nomor_form': 69},
|
||||||
|
{'kode': 'F1610', 'pertanyaan': 'Alasan mengambil pekerjaan tidak sesuai (8)?', 'tipe': 'ya_tidak', 'section': 'alasan_tidak_sesuai', 'nomor_form': 70},
|
||||||
|
{'kode': 'F1611', 'pertanyaan': 'Alasan mengambil pekerjaan tidak sesuai (9)?', 'tipe': 'ya_tidak', 'section': 'alasan_tidak_sesuai', 'nomor_form': 71},
|
||||||
|
{'kode': 'F1612', 'pertanyaan': 'Alasan mengambil pekerjaan tidak sesuai (10)?', 'tipe': 'ya_tidak', 'section': 'alasan_tidak_sesuai', 'nomor_form': 72},
|
||||||
|
{'kode': 'F1613', 'pertanyaan': 'Alasan mengambil pekerjaan tidak sesuai (11)?', 'tipe': 'ya_tidak', 'section': 'alasan_tidak_sesuai', 'nomor_form': 73},
|
||||||
|
{'kode': 'F1614', 'pertanyaan': 'Alasan lainnya', 'tipe': 'teks', 'section': 'alasan_tidak_sesuai', 'nomor_form': 74},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Insert questions
|
||||||
|
for q in questions_data:
|
||||||
|
cur.execute('''
|
||||||
|
INSERT INTO questions (kode, pertanyaan, tipe, section, nomor_form)
|
||||||
|
VALUES (%s, %s, %s, %s, %s)
|
||||||
|
ON DUPLICATE KEY UPDATE pertanyaan = VALUES(pertanyaan)
|
||||||
|
''', (q['kode'], q['pertanyaan'], q['tipe'], q['section'], q['nomor_form']))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
print(f'Successfully inserted {len(questions_data)} questions')
|
||||||
|
|
||||||
|
# ===== JAWABAN OPTIONS =====
|
||||||
|
answer_options = [
|
||||||
|
# F8 - Status saat ini
|
||||||
|
('F8', '1', 'Bekerja (full time/part time)', 1),
|
||||||
|
('F8', '2', 'Belum memungkinkan bekerja', 2),
|
||||||
|
('F8', '3', 'Wiraswasta', 3),
|
||||||
|
('F8', '4', 'Melanjutkan Pendidikan', 4),
|
||||||
|
('F8', '5', 'Tidak kerja tetapi sedang mencari kerja', 5),
|
||||||
|
|
||||||
|
# F505 - Pendapatan
|
||||||
|
('F505', '1000000', 'Rp 1.000.000', 1),
|
||||||
|
('F505', '1500000', 'Rp 1.500.000', 2),
|
||||||
|
('F505', '2000000', 'Rp 2.000.000', 3),
|
||||||
|
('F505', '2500000', 'Rp 2.500.000', 4),
|
||||||
|
('F505', '3000000', 'Rp 3.000.000', 5),
|
||||||
|
('F505', '3500000', 'Rp 3.500.000', 6),
|
||||||
|
('F505', '4000000', 'Rp 4.000.000', 7),
|
||||||
|
('F505', '4500000', 'Rp 4.500.000', 8),
|
||||||
|
('F505', '5000000', 'Rp 5.000.000', 9),
|
||||||
|
('F505', '6000000', 'Rp 6.000.000', 10),
|
||||||
|
('F505', '7000000', 'Rp 7.000.000', 11),
|
||||||
|
('F505', '8000000', '>Rp 8.000.000', 12),
|
||||||
|
|
||||||
|
# F1101 - Jenis perusahaan
|
||||||
|
('F1101', '1', 'Instansi pemerintah', 1),
|
||||||
|
('F1101', '2', 'Organisasi non-profit/Lembaga Swadaya Masyarakat', 2),
|
||||||
|
('F1101', '3', 'Perusahaan swasta', 3),
|
||||||
|
('F1101', '4', 'Wiraswasta/perusahaan sendiri', 4),
|
||||||
|
('F1101', '5', 'Lainnya', 5),
|
||||||
|
('F1101', '6', 'BUMN/BUMD', 6),
|
||||||
|
('F1101', '7', 'Institusi/Organisasi Multilateral', 7),
|
||||||
|
|
||||||
|
# F5d - Tingkatan tempat kerja
|
||||||
|
('F5d', '1', 'Lokal/wilayah/wiraswasta tidak berbadan hukum', 1),
|
||||||
|
('F5d', '2', 'Nasional/wiraswasta berbadan hukum', 2),
|
||||||
|
('F5d', '3', 'Multinasional/Internasional', 3),
|
||||||
|
|
||||||
|
# F5c - Jabatan wiraswasta
|
||||||
|
('F5c', '1', 'Founder', 1),
|
||||||
|
('F5c', '2', 'Co-Founder', 2),
|
||||||
|
('F5c', '3', 'Staff', 3),
|
||||||
|
('F5c', '4', 'Freelance/Kerja Lepas', 4),
|
||||||
|
|
||||||
|
# F18a - Sumber biaya studi lanjut
|
||||||
|
('F18a', '1', 'Biaya Sendiri', 1),
|
||||||
|
('F18a', '2', 'Beasiswa', 2),
|
||||||
|
|
||||||
|
# F1201 - Sumber biaya kuliah
|
||||||
|
('F1201', '1', 'Biaya Sendiri/Keluarga', 1),
|
||||||
|
('F1201', '2', 'Beasiswa ADIK', 2),
|
||||||
|
('F1201', '3', 'Beasiswa BIDIKMISI', 3),
|
||||||
|
('F1201', '4', 'Beasiswa PPA', 4),
|
||||||
|
('F1201', '5', 'Beasiswa AFIRMASI', 5),
|
||||||
|
('F1201', '6', 'Beasiswa Perusahaan/Swasta', 6),
|
||||||
|
('F1201', '7', 'Lainnya', 7),
|
||||||
|
|
||||||
|
# F14 - Keeratan bidang studi
|
||||||
|
('F14', '1', 'Sangat Erat', 1),
|
||||||
|
('F14', '2', 'Erat', 2),
|
||||||
|
('F14', '3', 'Cukup Erat', 3),
|
||||||
|
('F14', '4', 'Kurang Erat', 4),
|
||||||
|
('F14', '5', 'Tidak sama sekali', 5),
|
||||||
|
|
||||||
|
# F15 - Tingkat pendidikan tepat
|
||||||
|
('F15', '1', 'Setingkat lebih tinggi', 1),
|
||||||
|
('F15', '2', 'Tingkat yang sama', 2),
|
||||||
|
('F15', '3', 'Setingkat lebih rendah', 3),
|
||||||
|
('F15', '4', 'Tidak perlu pendidikan tinggi', 4),
|
||||||
|
|
||||||
|
# F301 - Mulai cari kerja
|
||||||
|
('F301', '1', 'Saya mencari kerja sebelum lulus', 1),
|
||||||
|
('F301', '2', 'Saya mencari kerja sesudah wisuda', 2),
|
||||||
|
('F301', '3', 'Saya tidak mencari kerja', 3),
|
||||||
|
|
||||||
|
# F1001 - Aktif cari kerja
|
||||||
|
('F1001', '1', 'Tidak', 1),
|
||||||
|
('F1001', '2', 'Tidak, tapi saya sedang menunggu hasil lamaran kerja', 2),
|
||||||
|
('F1001', '3', 'Ya, saya akan mulai bekerja dalam 2 minggu ke depan', 3),
|
||||||
|
('F1001', '4', 'Ya, tapi saya belum pasti akan bekerja dalam 2 minggu ke depan', 4),
|
||||||
|
('F1001', '5', 'Lainnya', 5),
|
||||||
|
|
||||||
|
# F1601 - Kesesuaian pekerjaan
|
||||||
|
('F1601', '0', 'Tidak sesuai', 1),
|
||||||
|
('F1601', '1', 'Pekerjaan saya sekarang sudah sesuai dengan pendidikan saya', 2),
|
||||||
|
|
||||||
|
# F1602 - Alasan belum sesuai
|
||||||
|
('F1602', '0', 'Pekerjaan saya sudah sesuai', 1),
|
||||||
|
('F1602', '1', 'Saya belum mendapatkan pekerjaan yang lebih sesuai', 2),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Insert answer options with question_id lookup
|
||||||
|
for kode, kode_jawaban, teks, urutan in answer_options:
|
||||||
|
# Get question_id from questions table
|
||||||
|
cur.execute('SELECT id FROM questions WHERE kode = %s', (kode,))
|
||||||
|
result = cur.fetchone()
|
||||||
|
if result:
|
||||||
|
question_id = result[0]
|
||||||
|
cur.execute('''
|
||||||
|
INSERT INTO answer_options (question_id, kode, teks, urutan)
|
||||||
|
VALUES (%s, %s, %s, %s)
|
||||||
|
ON DUPLICATE KEY UPDATE teks = VALUES(teks)
|
||||||
|
''', (question_id, kode_jawaban, teks, urutan))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
print(f'Successfully inserted answer options')
|
||||||
|
|
||||||
|
# Skala 1-5 (untuk kompetensi dan metode pembelajaran)
|
||||||
|
skala_labels = ['Sangat Rendah', 'Rendah', 'Netral', 'Tinggi', 'Sangat Tinggi']
|
||||||
|
for kode in ['F1761', 'F1762', 'F1763', 'F1764', 'F1765', 'F1766', 'F1767', 'F1768', 'F1769', 'F1770', 'F1771', 'F1772', 'F1773', 'F1774']:
|
||||||
|
cur.execute('SELECT id FROM questions WHERE kode = %s', (kode,))
|
||||||
|
result = cur.fetchone()
|
||||||
|
if result:
|
||||||
|
question_id = result[0]
|
||||||
|
for i, label in enumerate(skala_labels, 1):
|
||||||
|
cur.execute('''
|
||||||
|
INSERT INTO answer_options (question_id, kode, teks, urutan)
|
||||||
|
VALUES (%s, %s, %s, %s)
|
||||||
|
ON DUPLICATE KEY UPDATE teks = VALUES(teks)
|
||||||
|
''', (question_id, str(i), label, i))
|
||||||
|
|
||||||
|
# Metode pembelajaran - scale berbeda
|
||||||
|
metode_labels = ['Sangat Besar', 'Besar', 'Cukup Besar', 'Kurang', 'Tidak Sama Sekali']
|
||||||
|
for kode in ['F21', 'F22', 'F23', 'F24', 'F25', 'F26', 'F27']:
|
||||||
|
cur.execute('SELECT id FROM questions WHERE kode = %s', (kode,))
|
||||||
|
result = cur.fetchone()
|
||||||
|
if result:
|
||||||
|
question_id = result[0]
|
||||||
|
for i, label in enumerate(metode_labels, 1):
|
||||||
|
cur.execute('''
|
||||||
|
INSERT INTO answer_options (question_id, kode, teks, urutan)
|
||||||
|
VALUES (%s, %s, %s, %s)
|
||||||
|
ON DUPLICATE KEY UPDATE teks = VALUES(teks)
|
||||||
|
''', (question_id, str(i), label, i))
|
||||||
|
|
||||||
|
# Ya/Tidak untuk cara cari kerja
|
||||||
|
for kode in ['F401', 'F402', 'F403', 'F404', 'F405', 'F406', 'F407', 'F408', 'F409', 'F410', 'F411', 'F412', 'F413', 'F414', 'F415']:
|
||||||
|
cur.execute('SELECT id FROM questions WHERE kode = %s', (kode,))
|
||||||
|
result = cur.fetchone()
|
||||||
|
if result:
|
||||||
|
question_id = result[0]
|
||||||
|
for i, label in enumerate(['Tidak', 'Ya'], 0):
|
||||||
|
cur.execute('''
|
||||||
|
INSERT INTO answer_options (question_id, kode, teks, urutan)
|
||||||
|
VALUES (%s, %s, %s, %s)
|
||||||
|
ON DUPLICATE KEY UPDATE teks = VALUES(teks)
|
||||||
|
''', (question_id, str(i), label, i))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
print('All answer options inserted successfully!')
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
fastapi>=0.110.0
|
||||||
|
uvicorn[standard]>=0.28.0
|
||||||
|
sqlalchemy>=2.0.0
|
||||||
|
pydantic>=2.6.0
|
||||||
|
pydantic-settings>=2.2.0
|
||||||
|
python-dotenv>=1.0.1
|
||||||
|
passlib[bcrypt]>=1.7.4
|
||||||
|
python-jose[cryptography]>=3.3.0
|
||||||
|
python-multipart>=0.0.9
|
||||||
|
bcrypt>=4.1.0
|
||||||
|
email-validator>=2.0.0
|
||||||
|
|
||||||
|
# Machine Learning Dependencies
|
||||||
|
scikit-learn>=1.4.0
|
||||||
|
nltk>=3.8.0
|
||||||
|
sastrawi>=1.0.0
|
||||||
|
openpyxl>=3.1.0
|
||||||
|
pandas>=2.0.0
|
||||||
|
numpy>=1.24.0
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue