muhalwan commited on
Commit
cf29b06
·
1 Parent(s): dbac8dd
Files changed (1) hide show
  1. app.py +343 -92
app.py CHANGED
@@ -62,7 +62,7 @@ def generate_predictions(
62
  semester: Target semester (1 = Ganjil/Odd, 2 = Genap/Even)
63
 
64
  Returns:
65
- Tuple of (summary_text, recommendations_df, all_predictions_df)
66
  """
67
  global \
68
  _processor, \
@@ -74,7 +74,11 @@ def generate_predictions(
74
 
75
  try:
76
  if semester not in [1, 2]:
77
- return "❌ Error: Semester must be 1 (Ganjil) or 2 (Genap)", None, None
 
 
 
 
78
 
79
  if year < 2020 or year > 2030:
80
  return "❌ Error: Year must be between 2020 and 2030", None, None
@@ -97,6 +101,21 @@ def generate_predictions(
97
  _config.prediction.PREDICT_YEAR = year
98
  _config.prediction.PREDICT_SEMESTER = semester
99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  if _backtest_metrics is None:
101
  logger.info("Running backtest for the first time...")
102
  evaluator = Evaluator(_config)
@@ -123,65 +142,119 @@ def generate_predictions(
123
  semester,
124
  )
125
 
126
- recommended = predictions[predictions["recommendation"] == "BUKA"].copy()
127
-
128
  semester_name = "Ganjil (Odd)" if semester == 1 else "Genap (Even)"
129
- summary = f"""
130
- ## 📊 Prediction Summary for {year} Semester {semester_name}
 
 
 
 
 
 
 
 
131
 
132
- ### Model Performance (Backtest)
133
- - **Mean Absolute Error (MAE)**: {metrics["mae"]:.2f} students
134
- - **Root Mean Squared Error (RMSE)**: {metrics["rmse"]:.2f} students
 
 
 
 
135
 
136
- ### Recommendations
137
- - **Courses to Open**: {len(recommended)}
138
- - **Total Seats Needed**: {int(recommended["recommended_quota"].sum()) if not recommended.empty else 0}
139
- - **Estimated Students**: {int(recommended["predicted_enrollment"].sum()) if not recommended.empty else 0}
140
 
141
- ### Top Course
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  """
 
 
143
 
144
- if not recommended.empty:
145
- top_course = recommended.iloc[0]
146
- summary += f"- **{top_course['nama_mk']}** ({top_course['kode_mk']})\n"
147
- summary += (
148
- f" - Predicted: {top_course['predicted_enrollment']:.0f} students\n"
149
- )
150
- summary += (
151
- f" - Recommended Quota: {top_course['recommended_quota']:.0f} seats"
152
- )
153
- else:
154
- summary += "- No courses recommended to open"
155
-
156
- if not recommended.empty:
157
- recommended_display = recommended[
158
- [
159
- "kode_mk",
160
- "nama_mk",
161
- "predicted_enrollment",
162
- "recommended_quota",
163
- "strategy",
164
- ]
165
- ].copy()
166
- recommended_display.columns = [
167
- "Course Code",
168
- "Course Name",
169
- "Predicted Students",
170
- "Recommended Quota",
171
- "Prediction Strategy",
172
- ]
173
- recommended_display["Predicted Students"] = recommended_display[
174
- "Predicted Students"
175
- ].round(1)
176
- recommended_display["Recommended Quota"] = recommended_display[
177
- "Recommended Quota"
178
- ].astype(int)
179
- recommended_display = recommended_display.sort_values(
180
- "Predicted Students", ascending=False
181
- )
182
  else:
183
- recommended_display = pd.DataFrame()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
 
 
185
  # All predictions
186
  all_predictions_display = predictions[
187
  [
@@ -190,29 +263,125 @@ def generate_predictions(
190
  "predicted_enrollment",
191
  "recommended_quota",
192
  "recommendation",
 
193
  "strategy",
194
  ]
195
  ].copy()
196
  all_predictions_display.columns = [
197
  "Course Code",
198
  "Course Name",
199
- "Predicted Students",
200
- "Recommended Quota",
201
- "Recommendation",
 
202
  "Strategy",
203
  ]
204
- all_predictions_display["Predicted Students"] = all_predictions_display[
205
- "Predicted Students"
206
  ].round(1)
207
- all_predictions_display["Recommended Quota"] = all_predictions_display[
208
- "Recommended Quota"
209
- ].astype(int)
 
 
 
 
210
  all_predictions_display = all_predictions_display.sort_values(
211
- "Predicted Students", ascending=False
212
  )
213
 
214
- logger.info("✓ Predictions generated successfully")
215
- return summary, recommended_display, all_predictions_display
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
 
217
  except Exception as e:
218
  error_msg = f"❌ Error generating predictions: {str(e)}"
@@ -291,79 +460,161 @@ with gr.Blocks(title="SKS Enrollment Predictor") as demo:
291
  sanitize_html=False,
292
  )
293
 
 
 
 
 
 
 
 
 
294
  with gr.Tabs():
295
- with gr.Tab("Generate Predictions"):
296
  with gr.Row():
297
- with gr.Column(scale=1):
 
 
298
  year_input = gr.Number(
299
- label="Target Year",
300
  value=2025,
301
  precision=0,
302
  minimum=2020,
303
  maximum=2030,
304
- info="Masukkan tahun yang ingin diprediksi",
305
  )
306
 
307
  semester_input = gr.Radio(
308
  choices=[1, 2],
309
  label="Semester",
310
  value=2,
311
- info="1 = Ganjil, 2 = Genap",
312
  )
313
 
314
  predict_btn = gr.Button(
315
- "Generate Predictions", variant="primary", size="lg"
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  )
317
 
318
  with gr.Column(scale=2):
319
  summary_output = gr.Markdown(
320
- label="Summary", value="Click 'Generate Predictions' to start"
 
 
 
 
 
321
  )
322
 
323
- gr.Markdown("### Recommended Courses to Open")
324
- recommended_output = gr.Dataframe(
325
- label="Courses Recommended to Open", wrap=True, interactive=False
326
- )
327
 
328
- with gr.Accordion("View All Predictions", open=False):
329
- all_predictions_output = gr.Dataframe(
330
- label="All Elective Courses", wrap=True, interactive=False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
  )
332
 
333
- with gr.Tab("Data Information"):
334
- gr.Markdown()
335
 
336
- data_info_btn = gr.Button("Refresh Data Info", variant="secondary")
337
  data_info_output = gr.Markdown()
338
 
339
  data_info_btn.click(fn=get_data_info, inputs=[], outputs=data_info_output)
340
-
341
  demo.load(fn=get_data_info, inputs=[], outputs=data_info_output)
342
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
  predict_btn.click(
344
- fn=generate_predictions,
345
  inputs=[year_input, semester_input],
346
- outputs=[summary_output, recommended_output, all_predictions_output],
 
 
 
 
 
 
347
  )
348
 
349
  # Footer
 
350
  if os.getenv("DEMO_MODE", "false").lower() == "true":
351
  gr.Markdown(
352
  """
353
- ---
354
- <div style='text-align: center; color: #666; font-size: 0.9em;'>
355
- 📊 Demo Version with Anonymized Data | For Educational Purposes
356
  </div>
357
- """
 
358
  )
359
  else:
360
  gr.Markdown(
361
  """
362
- ---
363
- <div style='text-align: center; color: #666; font-size: 0.9em;'>
364
- 🔒 Private & Confidential | For Authorized Use Only
365
  </div>
366
- """
 
367
  )
368
 
369
  # Launch the app
 
62
  semester: Target semester (1 = Ganjil/Odd, 2 = Genap/Even)
63
 
64
  Returns:
65
+ Tuple of (summary_text, all_predictions_df, comparison_df)
66
  """
67
  global \
68
  _processor, \
 
74
 
75
  try:
76
  if semester not in [1, 2]:
77
+ return (
78
+ "❌ Error: Semester must be 1 (Ganjil) or 2 (Genap)",
79
+ None,
80
+ None,
81
+ )
82
 
83
  if year < 2020 or year > 2030:
84
  return "❌ Error: Year must be between 2020 and 2030", None, None
 
101
  _config.prediction.PREDICT_YEAR = year
102
  _config.prediction.PREDICT_SEMESTER = semester
103
 
104
+ # Check if actual data exists for this year/semester
105
+ actual_data = _df_enrollment[
106
+ (_df_enrollment["thn"] == year) & (_df_enrollment["smt"] == semester)
107
+ ]
108
+ has_actual_data = len(actual_data) > 0
109
+
110
+ if has_actual_data:
111
+ logger.info(
112
+ f"✓ Found actual enrollment data for {year} Semester {semester} - will compare predictions vs actual"
113
+ )
114
+ else:
115
+ logger.info(
116
+ f"ℹ No actual data for {year} Semester {semester} - generating future predictions"
117
+ )
118
+
119
  if _backtest_metrics is None:
120
  logger.info("Running backtest for the first time...")
121
  evaluator = Evaluator(_config)
 
142
  semester,
143
  )
144
 
 
 
145
  semester_name = "Ganjil (Odd)" if semester == 1 else "Genap (Even)"
146
+ total_to_open = len(predictions[predictions["recommendation"] == "BUKA"])
147
+ total_seats = (
148
+ int(
149
+ predictions[predictions["recommendation"] == "BUKA"][
150
+ "recommended_quota"
151
+ ].sum()
152
+ )
153
+ if total_to_open > 0
154
+ else 0
155
+ )
156
 
157
+ # Build summary with actual vs prediction comparison if data exists
158
+ if has_actual_data:
159
+ # Merge predictions with actual data
160
+ comparison = predictions.merge(
161
+ actual_data[["kode_mk", "enrollment"]], on="kode_mk", how="left"
162
+ )
163
+ comparison = comparison.rename(columns={"enrollment": "actual_enrollment"})
164
 
165
+ # Calculate comparison metrics only for courses with actual data
166
+ courses_with_actual = comparison[
167
+ comparison["actual_enrollment"].notna()
168
+ ].copy()
169
 
170
+ if len(courses_with_actual) > 0:
171
+ comparison_mae = abs(
172
+ courses_with_actual["predicted_enrollment"]
173
+ - courses_with_actual["actual_enrollment"]
174
+ ).mean()
175
+ comparison_rmse = (
176
+ (
177
+ courses_with_actual["predicted_enrollment"]
178
+ - courses_with_actual["actual_enrollment"]
179
+ )
180
+ ** 2
181
+ ).mean() ** 0.5
182
+ total_actual = courses_with_actual["actual_enrollment"].sum()
183
+ total_predicted = courses_with_actual["predicted_enrollment"].sum()
184
+
185
+ summary = f"""## 📊 {year} Semester {semester_name} - Validation Against Actual Data
186
+
187
+ <div style='background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 20px; border-radius: 10px; color: white; margin-bottom: 20px;'>
188
+ <h3 style='margin-top: 0; color: white;'>✅ Historical Validation</h3>
189
+ <p style='margin-bottom: 5px;'><strong>Status:</strong> Comparing predictions against actual enrollment data</p>
190
+ <p style='margin-bottom: 0;'><strong>Courses Validated:</strong> {len(courses_with_actual)} courses</p>
191
+ </div>
192
+
193
+ ### 📈 Prediction Accuracy
194
+ | Metric | Value |
195
+ |--------|-------|
196
+ | **Prediction MAE** | {comparison_mae:.2f} students |
197
+ | **Prediction RMSE** | {comparison_rmse:.2f} students |
198
+ | **Overall Accuracy** | {(1 - abs(total_predicted - total_actual) / total_actual) * 100:.1f}% |
199
+
200
+ ### 📊 Enrollment Summary
201
+ | Category | Actual | Predicted | Difference |
202
+ |----------|--------|-----------|------------|
203
+ | **Total Students** | {int(total_actual)} | {int(total_predicted)} | {int(total_predicted - total_actual):+d} |
204
+ | **Courses Analyzed** | {len(courses_with_actual)} | {len(courses_with_actual)} | - |
205
+
206
+ ### 🎯 Model Baseline (Cross-Validation)
207
+ - **Backtest MAE**: {metrics["mae"]:.2f} students
208
+ - **Backtest RMSE**: {metrics["rmse"]:.2f} students
209
+
210
+ ### 💡 Recommendation Summary
211
+ - **Courses Recommended to Open**: {total_to_open}
212
+ - **Total Quota Needed**: {total_seats} seats
213
+
214
+ ---
215
+ 💡 **Tip:** Scroll down to see the detailed comparison table showing prediction accuracy for each course.
216
  """
217
+ else:
218
+ summary = f"""## 📊 {year} Semester {semester_name}
219
 
220
+ <div style='background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); padding: 20px; border-radius: 10px; color: white; margin-bottom: 20px;'>
221
+ <h3 style='margin-top: 0; color: white;'>⚠️ Limited Validation Data</h3>
222
+ <p style='margin-bottom: 0;'>Actual semester data exists, but no matching elective courses found for comparison</p>
223
+ </div>
224
+
225
+ ### 🎯 Model Performance (Backtest)
226
+ - **MAE**: {metrics["mae"]:.2f} students
227
+ - **RMSE**: {metrics["rmse"]:.2f} students
228
+
229
+ ### 💡 Recommendations
230
+ - **Courses to Open**: {total_to_open}
231
+ | **Total Seats** | {total_seats} |
232
+ | **Estimated Total Students** | {int(predictions["predicted_enrollment"].sum())} |
233
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  else:
235
+ summary = f"""## 📊 {year} Semester {semester_name} - Future Prediction
236
+
237
+ <div style='background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); padding: 20px; border-radius: 10px; color: white; margin-bottom: 20px;'>
238
+ <h3 style='margin-top: 0; color: white;'>🔮 Forward-Looking Forecast</h3>
239
+ <p style='margin-bottom: 5px;'><strong>Status:</strong> No actual enrollment data available</p>
240
+ <p style='margin-bottom: 0;'><strong>Type:</strong> Predictive forecast based on historical trends</p>
241
+ </div>
242
+
243
+ ### 🎯 Model Performance (Historical Backtest)
244
+ | Metric | Value |
245
+ |--------|-------|
246
+ | **MAE** | {metrics["mae"]:.2f} students |
247
+ | **RMSE** | {metrics["rmse"]:.2f} students |
248
+
249
+ ### 📋 Forecast Summary
250
+ | Category | Value |
251
+ |----------|-------|
252
+ | **Courses to Open** | {total_to_open} |
253
+ | **Total Seats** | {total_seats} |
254
+ | **Estimated Total Students** | {int(predictions["predicted_enrollment"].sum())} |
255
+ """
256
 
257
+ # Prepare all predictions display
258
  # All predictions
259
  all_predictions_display = predictions[
260
  [
 
263
  "predicted_enrollment",
264
  "recommended_quota",
265
  "recommendation",
266
+ "confidence",
267
  "strategy",
268
  ]
269
  ].copy()
270
  all_predictions_display.columns = [
271
  "Course Code",
272
  "Course Name",
273
+ "Predicted",
274
+ "Quota",
275
+ "Status",
276
+ "Confidence",
277
  "Strategy",
278
  ]
279
+ all_predictions_display["Predicted"] = all_predictions_display[
280
+ "Predicted"
281
  ].round(1)
282
+ all_predictions_display["Quota"] = all_predictions_display["Quota"].astype(int)
283
+
284
+ # Add status emoji
285
+ all_predictions_display["Status"] = all_predictions_display["Status"].map(
286
+ {"BUKA": "✅ OPEN", "TUTUP": "❌ CLOSE"}
287
+ )
288
+
289
  all_predictions_display = all_predictions_display.sort_values(
290
+ "Predicted", ascending=False
291
  )
292
 
293
+ # Prepare comparison table if actual data exists
294
+ comparison_display = None
295
+ if has_actual_data:
296
+ logger.info(
297
+ f"Building comparison table - Actual data has {len(actual_data)} courses"
298
+ )
299
+ logger.info(f"Predictions has {len(predictions)} courses")
300
+
301
+ comparison = predictions.merge(
302
+ actual_data[["kode_mk", "enrollment"]], on="kode_mk", how="left"
303
+ )
304
+ comparison = comparison.rename(columns={"enrollment": "actual_enrollment"})
305
+
306
+ # Filter to courses with actual data and calculate error
307
+ courses_with_actual = comparison[
308
+ comparison["actual_enrollment"].notna()
309
+ ].copy()
310
+
311
+ logger.info(
312
+ f"Courses with matching actual data: {len(courses_with_actual)}"
313
+ )
314
+ if len(courses_with_actual) > 0:
315
+ logger.info(
316
+ f"Matching courses: {courses_with_actual['kode_mk'].tolist()}"
317
+ )
318
+
319
+ if len(courses_with_actual) > 0:
320
+ courses_with_actual["error"] = (
321
+ courses_with_actual["predicted_enrollment"]
322
+ - courses_with_actual["actual_enrollment"]
323
+ )
324
+ courses_with_actual["abs_error"] = abs(courses_with_actual["error"])
325
+ courses_with_actual["accuracy_%"] = 100 * (
326
+ 1
327
+ - courses_with_actual["abs_error"]
328
+ / courses_with_actual["actual_enrollment"].replace(0, 1)
329
+ )
330
+
331
+ comparison_display = courses_with_actual[
332
+ [
333
+ "kode_mk",
334
+ "nama_mk",
335
+ "actual_enrollment",
336
+ "predicted_enrollment",
337
+ "error",
338
+ "abs_error",
339
+ "accuracy_%",
340
+ "strategy",
341
+ ]
342
+ ].copy()
343
+
344
+ comparison_display.columns = [
345
+ "Course Code",
346
+ "Course Name",
347
+ "Actual",
348
+ "Predicted",
349
+ "Error",
350
+ "Abs Error",
351
+ "Accuracy %",
352
+ "Strategy",
353
+ ]
354
+
355
+ comparison_display["Actual"] = comparison_display["Actual"].astype(int)
356
+ comparison_display["Predicted"] = comparison_display["Predicted"].round(
357
+ 1
358
+ )
359
+ comparison_display["Error"] = comparison_display["Error"].round(1)
360
+ comparison_display["Abs Error"] = comparison_display["Abs Error"].round(
361
+ 1
362
+ )
363
+ comparison_display["Accuracy %"] = comparison_display[
364
+ "Accuracy %"
365
+ ].round(1)
366
+
367
+ comparison_display = comparison_display.sort_values(
368
+ "Abs Error", ascending=False
369
+ )
370
+
371
+ logger.info(
372
+ f"✓ Comparison table created with {len(comparison_display)} courses"
373
+ )
374
+ else:
375
+ logger.warning(
376
+ "⚠️ Actual data exists but no matching courses found for comparison"
377
+ )
378
+ logger.warning(f"Predicted courses: {predictions['kode_mk'].tolist()}")
379
+ logger.warning(f"Actual courses: {actual_data['kode_mk'].tolist()}")
380
+
381
+ logger.info(
382
+ f"✓ Predictions generated successfully (comparison_display: {comparison_display is not None})"
383
+ )
384
+ return summary, all_predictions_display, comparison_display
385
 
386
  except Exception as e:
387
  error_msg = f"❌ Error generating predictions: {str(e)}"
 
460
  sanitize_html=False,
461
  )
462
 
463
+ # Header
464
+ gr.Markdown(
465
+ """
466
+ # 🎓 SKS Course Enrollment Prediction System
467
+ ### Intelligent forecasting for elective course planning
468
+ """
469
+ )
470
+
471
  with gr.Tabs():
472
+ with gr.Tab("📊 Predictions", id="predictions"):
473
  with gr.Row():
474
+ with gr.Column(scale=1, min_width=300):
475
+ gr.Markdown("### 🎯 Select Target Semester")
476
+
477
  year_input = gr.Number(
478
+ label="Year",
479
  value=2025,
480
  precision=0,
481
  minimum=2020,
482
  maximum=2030,
 
483
  )
484
 
485
  semester_input = gr.Radio(
486
  choices=[1, 2],
487
  label="Semester",
488
  value=2,
489
+ info="1 = Ganjil (Odd) | 2 = Genap (Even)",
490
  )
491
 
492
  predict_btn = gr.Button(
493
+ "🚀 Generate Predictions",
494
+ variant="primary",
495
+ size="lg",
496
+ scale=1,
497
+ )
498
+
499
+ gr.Markdown(
500
+ """
501
+ ---
502
+ **💡 Tips:**
503
+ - Historical semesters show validation results
504
+ - Future semesters show forecasts
505
+ - Use filters in tables to find specific courses
506
+ """
507
  )
508
 
509
  with gr.Column(scale=2):
510
  summary_output = gr.Markdown(
511
+ value="""
512
+ <div style='text-align: center; padding: 40px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 10px; color: white;'>
513
+ <h2 style='color: white; margin-top: 0;'>👈 Select Parameters</h2>
514
+ <p style='margin-bottom: 0;'>Choose a year and semester, then click "Generate Predictions"</p>
515
+ </div>
516
+ """
517
  )
518
 
519
+ gr.Markdown("---")
 
 
 
520
 
521
+ with gr.Row():
522
+ with gr.Column():
523
+ gr.Markdown("### 📋 All Course Predictions")
524
+ gr.Markdown(
525
+ "*Complete list of all elective courses with recommendations*"
526
+ )
527
+ all_predictions_output = gr.Dataframe(
528
+ label="Predictions",
529
+ wrap=True,
530
+ interactive=False,
531
+ )
532
+
533
+ gr.Markdown("---")
534
+
535
+ with gr.Accordion(
536
+ "📈 Prediction vs Actual Comparison", open=False
537
+ ) as comparison_accordion:
538
+ comparison_info = gr.Markdown(
539
+ value="*This section will show validation data when actual enrollment is available*",
540
+ )
541
+ comparison_output = gr.Dataframe(
542
+ label="Detailed Comparison",
543
+ wrap=True,
544
+ interactive=False,
545
  )
546
 
547
+ with gr.Tab("ℹ️ Data Info", id="info"):
548
+ gr.Markdown("### 📁 Dataset Information")
549
 
550
+ data_info_btn = gr.Button("🔄 Refresh Data Info", variant="secondary")
551
  data_info_output = gr.Markdown()
552
 
553
  data_info_btn.click(fn=get_data_info, inputs=[], outputs=data_info_output)
 
554
  demo.load(fn=get_data_info, inputs=[], outputs=data_info_output)
555
 
556
+ def update_ui_with_predictions(year, semester):
557
+ """Wrapper to handle UI updates based on whether comparison data exists."""
558
+ summary, all_predictions, comparison = generate_predictions(year, semester)
559
+
560
+ logger.info(
561
+ f"UI Update: comparison is None: {comparison is None}, empty: {comparison.empty if comparison is not None else 'N/A'}"
562
+ )
563
+
564
+ if comparison is not None and not comparison.empty:
565
+ logger.info(f"Showing comparison table with {len(comparison)} rows")
566
+ return (
567
+ summary,
568
+ all_predictions,
569
+ gr.update(open=True), # Open accordion
570
+ gr.update(
571
+ value=f"**✅ Validated against {len(comparison)} courses with actual enrollment data**\n\nThe table below shows prediction accuracy for each course:",
572
+ ),
573
+ gr.update(value=comparison),
574
+ )
575
+ else:
576
+ logger.info("Hiding comparison table - no data available")
577
+ return (
578
+ summary,
579
+ all_predictions,
580
+ gr.update(open=False), # Keep accordion closed
581
+ gr.update(
582
+ value="*No actual enrollment data available for this semester (future prediction)*",
583
+ ),
584
+ gr.update(value=None),
585
+ )
586
+
587
  predict_btn.click(
588
+ fn=update_ui_with_predictions,
589
  inputs=[year_input, semester_input],
590
+ outputs=[
591
+ summary_output,
592
+ all_predictions_output,
593
+ comparison_accordion,
594
+ comparison_info,
595
+ comparison_output,
596
+ ],
597
  )
598
 
599
  # Footer
600
+ gr.Markdown("---")
601
  if os.getenv("DEMO_MODE", "false").lower() == "true":
602
  gr.Markdown(
603
  """
604
+ <div style='text-align: center; padding: 20px; background: #f8f9fa; border-radius: 10px;'>
605
+ <p style='margin: 0; color: #666;'>📊 <strong>Demo Version</strong> with Anonymized Data | For Educational Purposes</p>
 
606
  </div>
607
+ """,
608
+ sanitize_html=False,
609
  )
610
  else:
611
  gr.Markdown(
612
  """
613
+ <div style='text-align: center; padding: 20px; background: #f8f9fa; border-radius: 10px;'>
614
+ <p style='margin: 0; color: #666;'>🔒 <strong>Private & Confidential</strong> | For Authorized Use Only</p>
 
615
  </div>
616
+ """,
617
+ sanitize_html=False,
618
  )
619
 
620
  # Launch the app