Spaces:
Sleeping
Sleeping
| from typing import Dict | |
| def get_color(value: float, thresholds: tuple = (50, 25)) -> str: | |
| high, low = thresholds | |
| if value >= high: | |
| return "#4ade80" | |
| elif value >= low: | |
| return "#fb923c" | |
| else: | |
| return "#f87171" | |
| def get_diff_color(value: float) -> str: | |
| return "#4ade80" if value >= 0 else "#f87171" | |
| # Card Components | |
| def metric_card(title: str, value: str, color: str, subtitle: str = "") -> str: | |
| subtitle_html = ( | |
| f'<div style="font-size: 11px; color: #9ca3af; margin-top: 4px;">{subtitle}</div>' | |
| if subtitle | |
| else "" | |
| ) | |
| return f""" | |
| <div style="background: #1e293b; padding: 20px; border-radius: 12px; border-left: 4px solid {color};"> | |
| <div style="font-size: 12px; color: #9ca3af; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px;">{title}</div> | |
| <div style="font-size: 28px; font-weight: 700; color: {color};">{value}</div> | |
| {subtitle_html} | |
| </div> | |
| """ | |
| def info_row(label: str, value: str, color: str = "#fff", border: bool = True) -> str: | |
| border_style = "border-bottom: 1px solid #334155;" if border else "" | |
| return f""" | |
| <div style="display: flex; justify-content: space-between; padding: 12px 0; {border_style}"> | |
| <span style="color: #9ca3af;">{label}</span> | |
| <span style="font-weight: 600; color: {color};">{value}</span> | |
| </div> | |
| """ | |
| def info_card(title: str, rows: list) -> str: | |
| rows_html = "".join(rows) | |
| return f""" | |
| <div style="background: #1e293b; padding: 20px; border-radius: 12px;"> | |
| <h4 style="margin: 0 0 16px 0; color: #fff; font-size: 14px; font-weight: 600;">{title}</h4> | |
| {rows_html} | |
| </div> | |
| """ | |
| # Summary Templates | |
| def build_validation_summary(data: Dict) -> str: | |
| """Build summary HTML for validation mode (when actual data exists).""" | |
| year = data["year"] | |
| semester_name = data["semester_name"] | |
| class_capacity = data["class_capacity"] | |
| data_source = data.get("data_source", "kalkulasi") | |
| # Metrics | |
| class_accuracy_pct = data.get("class_accuracy_pct", 0) | |
| class_within_one_pct = data.get("class_within_one_pct", 0) | |
| total_classes = data.get("total_classes", 0) | |
| comparison_mae = data.get("comparison_mae", 0) | |
| comparison_rmse = data.get("comparison_rmse", 0) | |
| total_for_class_accuracy = data.get("total_for_class_accuracy", 0) | |
| # Enrollment metrics | |
| total_actual = data.get("total_actual", 0) | |
| total_predicted = data.get("total_predicted", 0) | |
| accuracy_pct = data.get("accuracy_pct", 0) | |
| class_matches = data.get("class_matches", 0) | |
| class_within_one = data.get("class_within_one", 0) | |
| # Colors | |
| class_accuracy_color = get_color(class_accuracy_pct) | |
| diff_color = get_diff_color(total_predicted - total_actual) | |
| return f""" | |
| <div style="padding: 24px;"> | |
| <div style="margin-bottom: 24px;"> | |
| <h2 style="margin: 0 0 8px 0; color: #fff; font-size: 24px; font-weight: 600;">{ | |
| year | |
| } Semester {semester_name}</h2> | |
| <p style="color: #9ca3af; margin: 0; font-size: 14px;">Validasi prediksi terhadap data aktual | Kapasitas per kelas: { | |
| class_capacity | |
| } mahasiswa | Sumber kelas aktual: {data_source}</p> | |
| </div> | |
| <div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 24px;"> | |
| { | |
| metric_card( | |
| "Akurasi Kelas", | |
| f"{class_accuracy_pct:.1f}%", | |
| class_accuracy_color, | |
| f"±1 kelas: {class_within_one_pct:.1f}%", | |
| ) | |
| } | |
| {metric_card("Total Kelas Prediksi", str(total_classes), "#60a5fa")} | |
| { | |
| metric_card( | |
| "MAE / RMSE", f"{comparison_mae:.1f} / {comparison_rmse:.1f}", "#a78bfa" | |
| ) | |
| } | |
| {metric_card("MK Divalidasi", str(total_for_class_accuracy), "#fb923c")} | |
| </div> | |
| <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px;"> | |
| { | |
| info_card( | |
| "Ringkasan Enrollment", | |
| [ | |
| info_row("Total Aktual", str(int(total_actual))), | |
| info_row("Total Prediksi", str(int(total_predicted))), | |
| info_row( | |
| "Selisih", | |
| f"{int(total_predicted - total_actual):+d}", | |
| diff_color, | |
| border=False, | |
| ), | |
| ], | |
| ) | |
| } | |
| { | |
| info_card( | |
| f"Akurasi Prediksi Kelas (dari {data_source})", | |
| [ | |
| info_row( | |
| "Kelas Tepat", | |
| f"{class_matches}/{total_for_class_accuracy}", | |
| "#4ade80", | |
| ), | |
| info_row( | |
| "Selisih ±1 Kelas", | |
| f"{class_within_one}/{total_for_class_accuracy}", | |
| "#60a5fa", | |
| ), | |
| info_row("Akurasi Enrollment", f"{accuracy_pct:.1f}%", border=False), | |
| ], | |
| ) | |
| } | |
| </div> | |
| </div> | |
| """ | |
| def build_no_match_summary(data: Dict) -> str: | |
| year = data["year"] | |
| semester_name = data["semester_name"] | |
| metrics = data.get("metrics", {"mae": 0, "rmse": 0}) | |
| total_to_open = data.get("total_to_open", 0) | |
| total_classes = data.get("total_classes", 0) | |
| return f""" | |
| <div style="padding: 24px;"> | |
| <div style="margin-bottom: 24px;"> | |
| <h2 style="margin: 0 0 8px 0; color: #fff; font-size: 24px; font-weight: 600;">{year} Semester {semester_name}</h2> | |
| <p style="color: #9ca3af; margin: 0; font-size: 14px;">Data semester ada, tetapi tidak ditemukan MK pilihan yang cocok</p> | |
| </div> | |
| <div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px;"> | |
| {metric_card("MAE (Backtest)", f"{metrics['mae']:.2f}", "#60a5fa")} | |
| {metric_card("RMSE (Backtest)", f"{metrics['rmse']:.2f}", "#a78bfa")} | |
| {metric_card("MK Dibuka", str(total_to_open), "#4ade80")} | |
| {metric_card("Total Kelas", str(total_classes), "#fb923c")} | |
| </div> | |
| </div> | |
| """ | |
| def build_future_prediction_summary(data: Dict) -> str: | |
| year = data["year"] | |
| semester_name = data["semester_name"] | |
| class_capacity = data["class_capacity"] | |
| metrics = data.get("metrics", {"mae": 0, "rmse": 0}) | |
| total_to_open = data.get("total_to_open", 0) | |
| total_classes = data.get("total_classes", 0) | |
| total_predicted_students = data.get("total_predicted_students", 0) | |
| total_capacity = data.get("total_capacity", 0) | |
| avg_utilization = ( | |
| (total_predicted_students / total_capacity * 100) if total_capacity > 0 else 0 | |
| ) | |
| return f""" | |
| <div style="padding: 24px;"> | |
| <div style="margin-bottom: 24px;"> | |
| <h2 style="margin: 0 0 8px 0; color: #fff; font-size: 24px; font-weight: 600;">{ | |
| year | |
| } Semester {semester_name}</h2> | |
| <p style="color: #9ca3af; margin: 0; font-size: 14px;">Prediksi masa depan berdasarkan tren historis | Kapasitas per kelas: { | |
| class_capacity | |
| } mahasiswa</p> | |
| </div> | |
| <div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 24px;"> | |
| {metric_card("MK Dibuka", str(total_to_open), "#4ade80")} | |
| {metric_card("Total Kelas Dibuka", str(total_classes), "#60a5fa")} | |
| {metric_card("Prediksi Mahasiswa", str(total_predicted_students), "#a78bfa")} | |
| {metric_card("Total Kuota", str(total_capacity), "#fb923c")} | |
| </div> | |
| <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px;"> | |
| { | |
| info_card( | |
| "Backtest Metrics", | |
| [ | |
| info_row("MAE", f"{metrics['mae']:.2f}"), | |
| info_row("RMSE", f"{metrics['rmse']:.2f}", border=False), | |
| ], | |
| ) | |
| } | |
| { | |
| info_card( | |
| "Kapasitas Info", | |
| [ | |
| info_row("Kapasitas/Kelas", f"{class_capacity} mhs"), | |
| info_row("Avg Utilization", f"{avg_utilization:.1f}%", border=False), | |
| ], | |
| ) | |
| } | |
| </div> | |
| </div> | |
| """ | |
| def build_prediction_summary(data: Dict) -> str: | |
| has_actual_data = data.get("has_actual_data", False) | |
| if has_actual_data: | |
| if "comparison_mae" in data: | |
| return build_validation_summary(data) | |
| else: | |
| return build_no_match_summary(data) | |
| else: | |
| return build_future_prediction_summary(data) | |
| def build_multi_year_summary(data: Dict) -> str: | |
| year = data["year"] | |
| years_ahead = data["years_ahead"] | |
| semester_name = data["semester_name"] | |
| class_capacity = data["class_capacity"] | |
| first_year_classes = data["first_year_classes"] | |
| last_year_classes = data["last_year_classes"] | |
| growth_classes = data["growth_classes"] | |
| growth_students = data["growth_students"] | |
| growth_class_color = get_diff_color(growth_classes) | |
| growth_student_color = get_diff_color(growth_students) | |
| return f""" | |
| <div style="padding: 24px;"> | |
| <div style="margin-bottom: 24px;"> | |
| <h2 style="margin: 0 0 8px 0; color: #fff; font-size: 24px; font-weight: 600;">Proyeksi {years_ahead} Tahun ke Depan - Semester {semester_name}</h2> | |
| <p style="color: #9ca3af; margin: 0; font-size: 14px;">Forecasting kebutuhan kelas {year} - {year + years_ahead} | Kapasitas per kelas: {class_capacity} mahasiswa</p> | |
| </div> | |
| <div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 24px;"> | |
| {metric_card(f"Kelas ({year})", str(first_year_classes), "#4ade80")} | |
| {metric_card(f"Kelas ({year + years_ahead})", str(last_year_classes), "#60a5fa")} | |
| {metric_card("Pertumbuhan Kelas", f"{growth_classes:+d}", growth_class_color)} | |
| {metric_card("Pertumbuhan Mhs", f"{growth_students:+d}", growth_student_color)} | |
| </div> | |
| </div> | |
| """ | |
| # Placeholder Templates | |
| def placeholder_card(title: str, subtitle: str) -> str: | |
| return f""" | |
| <div style="padding: 60px 40px; text-align: center; background: #1e293b; border-radius: 12px;"> | |
| <h3 style="color: #fff; margin: 0 0 8px 0; font-size: 18px; font-weight: 600;">{title}</h3> | |
| <p style="color: #9ca3af; margin: 0; font-size: 14px;">{subtitle}</p> | |
| </div> | |
| """ | |
| def get_prediction_placeholder() -> str: | |
| return placeholder_card( | |
| "Pilih tahun dan semester", | |
| "Klik Generate Predictions untuk melihat rekomendasi jumlah kelas", | |
| ) | |
| def get_forecast_placeholder() -> str: | |
| return placeholder_card( | |
| "Proyeksi Multi-Tahun", "Lihat tren kebutuhan kelas beberapa tahun ke depan" | |
| ) | |
| # Data Info Component | |
| def build_data_info(data: Dict) -> str: | |
| if "error" in data: | |
| return f"<p style='color: #f87171;'>{data['error']}</p>" | |
| return f""" | |
| <div style="padding: 8px 0;"> | |
| <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px;"> | |
| <div style="background: #1e293b; padding: 16px; border-radius: 8px; text-align: center;"> | |
| <div style="font-size: 11px; color: #9ca3af; margin-bottom: 6px;">Total MK</div> | |
| <div style="font-size: 20px; font-weight: 700; color: #fff;">{data["total_courses"]}</div> | |
| </div> | |
| <div style="background: #1e293b; padding: 16px; border-radius: 8px; text-align: center;"> | |
| <div style="font-size: 11px; color: #9ca3af; margin-bottom: 6px;">MK Pilihan</div> | |
| <div style="font-size: 20px; font-weight: 700; color: #4ade80;">{data["elective_courses"]}</div> | |
| </div> | |
| <div style="background: #1e293b; padding: 16px; border-radius: 8px; text-align: center;"> | |
| <div style="font-size: 11px; color: #9ca3af; margin-bottom: 6px;">Kapasitas/Kelas</div> | |
| <div style="font-size: 20px; font-weight: 700; color: #60a5fa;">{data["class_capacity"]}</div> | |
| </div> | |
| <div style="background: #1e293b; padding: 16px; border-radius: 8px; text-align: center;"> | |
| <div style="font-size: 11px; color: #9ca3af; margin-bottom: 6px;">Tahun Data</div> | |
| <div style="font-size: 20px; font-weight: 700; color: #fb923c;">{data["year_min"]}-{data["year_max"]}</div> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |