SHELLAPANDIANGANHUNGING commited on
Commit
6916b99
·
verified ·
1 Parent(s): 63fa9ef

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +43 -141
app.py CHANGED
@@ -445,6 +445,7 @@ else:
445
  Front tyres: Pressure {front_pressure_avg:.1f} psi, temperature {front_temp_avg:.1f}°C → stable under current load/pressure balance. Rear tyres: Pressure {rear_pressure_avg:.1f} psi, temperature {rear_temp_avg:.1f}°C → balanced operation.
446
  """
447
 
 
448
  st.markdown(f"""
449
  <div class="insight-box">
450
  <div class="content">
@@ -452,8 +453,12 @@ st.markdown(f"""
452
  </div>
453
  </div>
454
  """, unsafe_allow_html=True)
 
 
 
455
  st.markdown("""
456
- <h3 class="objective-title">OBJECTIVE 2: Shift and Tyre Position - How Are Alarms Concentrated Across Shifts and Tyres?</h3>""", unsafe_allow_html=True)
 
457
 
458
  # Filter semua data (termasuk alarm normal)
459
  alarm_data = dff.copy()
@@ -492,14 +497,14 @@ def create_radial_chart(pos_data, title, shift_hours, shift_type):
492
 
493
  fig = go.Figure()
494
 
495
- # Tambahkan trace untuk masing-masing kategori
496
  fig.add_trace(go.Barpolar(
497
  r=hourly_normal.values,
498
  theta=theta,
499
  name='Normal',
500
  marker_color='#2E7D32', # Hijau
501
  opacity=0.8,
502
- hovertemplate='<b>Hour:</b> %{theta:.0f}<br><b>Count:</b> %{r}<br><b>Status:</b> Normal<extra></extra>',
503
  customdata=shift_hours
504
  ))
505
  fig.add_trace(go.Barpolar(
@@ -508,7 +513,7 @@ def create_radial_chart(pos_data, title, shift_hours, shift_type):
508
  name='Amber',
509
  marker_color='#FFC107', # Kuning
510
  opacity=0.8,
511
- hovertemplate='<b>Hour:</b> %{theta:.0f}<br><b>Count:</b> %{r}<br><b>Status:</b> Amber<extra></extra>',
512
  customdata=shift_hours
513
  ))
514
  fig.add_trace(go.Barpolar(
@@ -517,7 +522,7 @@ def create_radial_chart(pos_data, title, shift_hours, shift_type):
517
  name='Red',
518
  marker_color='#D32F2F', # Merah
519
  opacity=0.8,
520
- hovertemplate='<b>Hour:</b> %{theta:.0f}<br><b>Count:</b> %{r}<br><b>Status:</b> Red<extra></extra>',
521
  customdata=shift_hours
522
  ))
523
 
@@ -665,9 +670,9 @@ else:
665
 
666
  # Insight Spesifik Per Position dan Shift
667
  insight_lines = [
668
- f"{dominant_band} is the dominant period ({dominant_pct:.1f}% of all data).",
669
- f"{second_dominant_band} is the second-highest period ({second_pct:.1f}% of data).",
670
- f"Total: Normal={normal_alarms}, Amber={amber_alarms}, Red={red_alarms}"
671
  ]
672
 
673
  # Position 1 (Shift Pagi)
@@ -677,7 +682,7 @@ else:
677
  if not pos1_pagi_total.empty:
678
  dominant_hour_p1_pagi = pos1_pagi_total.idxmax()
679
  dominant_count_p1_pagi = pos1_pagi_total.max()
680
- insight_lines.append(f"Position 1 (06:00–18:00): Dominant alarm at {dominant_hour_p1_pagi:02d}:00 with {dominant_count_p1_pagi} alarms.")
681
 
682
  # Position 1 (Shift Sore)
683
  pos1_sore = alarm_data[(alarm_data['Position'] == 1) & (~alarm_data['hour'].between(6, 17, inclusive='both'))]
@@ -685,7 +690,7 @@ else:
685
  pos1_sore_red = pos1_sore[pos1_sore['Alarm Status'].str.contains('Red', na=False)]
686
  if not pos1_sore_red.empty:
687
  red_percentage_p1_sore = (len(pos1_sore_red) / len(pos1_sore)) * 100
688
- insight_lines.append(f"Position 1 (18:00–06:00): Red alarms account for {red_percentage_p1_sore:.1f}% of total alarms.")
689
 
690
  # Position 3 (Shift Pagi)
691
  pos3_pagi = alarm_data[(alarm_data['Position'] == 3) & (alarm_data['hour'].between(6, 17, inclusive='both'))]
@@ -694,7 +699,7 @@ else:
694
  if not pos3_pagi_total.empty:
695
  dominant_hour_p3_pagi = pos3_pagi_total.idxmax()
696
  dominant_count_p3_pagi = pos3_pagi_total.max()
697
- insight_lines.append(f"Position 3 (06:00–18:00): Dominant alarm at {dominant_hour_p3_pagi:02d}:00 with {dominant_count_p3_pagi} alarms.")
698
 
699
  # Position 3 (Shift Sore)
700
  pos3_sore = alarm_data[(alarm_data['Position'] == 3) & (~alarm_data['hour'].between(6, 17, inclusive='both'))]
@@ -702,7 +707,7 @@ else:
702
  pos3_sore_amber = pos3_sore[pos3_sore['Alarm Status'].str.contains('Amber', na=False)]
703
  if not pos3_sore_amber.empty:
704
  amber_percentage_p3_sore = (len(pos3_sore_amber) / len(pos3_sore)) * 100
705
- insight_lines.append(f"Position 3 (18:00–06:00): Amber alarms account for {amber_percentage_p3_sore:.1f}% of total alarms.")
706
 
707
  # Position 4 (Shift Pagi)
708
  pos4_pagi = alarm_data[(alarm_data['Position'] == 4) & (alarm_data['hour'].between(6, 17, inclusive='both'))]
@@ -711,7 +716,7 @@ else:
711
  if not pos4_pagi_total.empty:
712
  dominant_hour_p4_pagi = pos4_pagi_total.idxmax()
713
  dominant_count_p4_pagi = pos4_pagi_total.max()
714
- insight_lines.append(f"Position 4 (06:00–18:00): Dominant alarm at {dominant_hour_p4_pagi:02d}:00 with {dominant_count_p4_pagi} alarms.")
715
 
716
  # Position 4 (Shift Sore)
717
  pos4_sore = alarm_data[(alarm_data['Position'] == 4) & (~alarm_data['hour'].between(6, 17, inclusive='both'))]
@@ -719,7 +724,7 @@ else:
719
  pos4_sore_amber = pos4_sore[pos4_sore['Alarm Status'].str.contains('Amber', na=False)]
720
  if not pos4_sore_amber.empty:
721
  amber_percentage_p4_sore = (len(pos4_sore_amber) / len(pos4_sore)) * 100
722
- insight_lines.append(f"Position 4 (18:00–06:00): Amber alarms account for {amber_percentage_p4_sore:.1f}% of total alarms.")
723
 
724
  insight_text = "\n".join(insight_lines)
725
 
@@ -1074,7 +1079,7 @@ else:
1074
  # Strong correlation between temperature and pressure in front tyres (r = {corr_p_t_front:.2f}) vs rear (r = {corr_p_t_rear:.2f}).
1075
  # •
1076
  insight_text = f""" At temperatures ≥52°C, front tyres trigger {red_high_pressure_count} Red High Pressure alarms, indicating critical heat thresholds.
1077
- • Pressure vs (T/v) shows weak correlation (r = {corr_p_tv_front:.2f}), suggesting speed alone is not primary heat factor.
1078
  """
1079
 
1080
  st.markdown(f"""
@@ -1104,64 +1109,7 @@ else:
1104
  height='520px'
1105
  )
1106
 
1107
- # === Tambahkan Polygon untuk Location 1 & 2 (Contoh koordinat — ganti sesuai data Anda)
1108
- # Jika Anda punya kolom 'Zone' atau 'Location', gunakan itu untuk grouping
1109
- # Di sini saya asumsikan:
1110
- # - Location 1: Zona dengan nama 'Parking 1'
1111
- # - Location 2: Zona dengan nama 'Parking 2'
1112
-
1113
- # Contoh koordinat polygon (ganti dengan nilai nyata dari data Anda jika ada)
1114
- # Jika tidak ada, Anda bisa definisikan manual berdasarkan range latitude/longitude
1115
-
1116
- # Untuk demo, saya buat polygon manual
1117
- # Location 1: Parking 1
1118
- location1_coords = [
1119
- [center_lat - 0.0008, center_lon - 0.001], # kiri bawah
1120
- [center_lat - 0.0008, center_lon + 0.001], # kiri atas
1121
- [center_lat + 0.0008, center_lon + 0.001], # kanan atas
1122
- [center_lat + 0.0008, center_lon - 0.001], # kanan bawah
1123
- [center_lat - 0.0008, center_lon - 0.001] # kembali ke awal
1124
- ]
1125
- folium.Polygon(
1126
- locations=location1_coords,
1127
- color='#1976D2',
1128
- fill_color='#1976D2',
1129
- fill_opacity=0.1,
1130
- weight=2,
1131
- popup="Location 1"
1132
- ).add_to(m)
1133
-
1134
- # Location 2: Parking 2
1135
- location2_coords = [
1136
- [center_lat + 0.001, center_lon - 0.0015],
1137
- [center_lat + 0.001, center_lon + 0.0015],
1138
- [center_lat + 0.002, center_lon + 0.0015],
1139
- [center_lat + 0.002, center_lon - 0.0015],
1140
- [center_lat + 0.001, center_lon - 0.0015]
1141
- ]
1142
- folium.Polygon(
1143
- locations=location2_coords,
1144
- color='#FF9800',
1145
- fill_color='#FF9800',
1146
- fill_opacity=0.1,
1147
- weight=2,
1148
- popup="Location 2"
1149
- ).add_to(m)
1150
-
1151
- # Tambahkan teks label di tengah polygon
1152
- folium.Marker(
1153
- location=[center_lat - 0.0004, center_lon],
1154
- icon=folium.DivIcon(html=f'<div style="font-weight:bold; font-size:14px; color:#1976D2; text-shadow: 1px 1px 2px white;">Location 1</div>'),
1155
- tooltip="Location 1"
1156
- ).add_to(m)
1157
-
1158
- folium.Marker(
1159
- location=[center_lat + 0.0015, center_lon],
1160
- icon=folium.DivIcon(html=f'<div style="font-weight:bold; font-size:14px; color:#FF9800; text-shadow: 1px 1px 2px white;">Location 2</div>'),
1161
- tooltip="Location 2"
1162
- ).add_to(m)
1163
-
1164
- # === Plot marker per tyre
1165
  for _, r in valid_gps.iterrows():
1166
  color = '#D32F2F' if r['Alarm Status'] == 'Red High Pressure' else '#2E7D32'
1167
  radius = 6 + (r['Temperature (°C)'] - valid_gps['Temperature (°C)'].min()) / (valid_gps['Temperature (°C)'].max() - valid_gps['Temperature (°C)'].min() + 1e-5) * 12
@@ -1232,7 +1180,9 @@ if not valid_gps.empty:
1232
  else:
1233
  front_percentage = 0
1234
 
1235
- insight_text = f"""Alarm concentration is highest in {top_zone}, with {top_zone_count} alarms representing {percentage:.1f}% of total alarms. Front tyres account for {front_percentage:.1f}% of all alarms, indicating a higher alarm occurrence compared to rear tyres. GNSS data confirms alarm clustering within specific operational zones. Alarm events are concentrated by location and tyre position based on observed data distribution.
 
 
1236
  """
1237
 
1238
  else:
@@ -1247,7 +1197,7 @@ st.markdown(f"""
1247
  </div>
1248
  </div>
1249
  """, unsafe_allow_html=True)
1250
-
1251
  # ================= OBJECTIVE 5 =================
1252
  st.markdown('<h3 class="objective-title">OBJECTIVE 5: Insights & Mitigation — How Can Red Pressure Alarms Be Reduced?</h3>', unsafe_allow_html=True)
1253
 
@@ -1277,56 +1227,9 @@ else:
1277
  corr_rear = 0
1278
 
1279
  # Insight dari Objective 1-4
1280
- # Insight 1: Fisika & Mekanikal
1281
- front_pressure_avg_obj1 = dff[dff['Position'].isin([1, 2])]['Pressure (psi)'].mean()
1282
- rear_pressure_avg_obj1 = dff[dff['Position'].isin([3, 4])]['Pressure (psi)'].mean()
1283
- front_temp_avg_obj1 = dff[dff['Position'].isin([1, 2])]['Temperature (°C)'].mean()
1284
- rear_temp_avg_obj1 = dff[dff['Position'].isin([3, 4])]['Temperature (°C)'].mean()
1285
-
1286
- # Insight 2: Shift & Alarm Distribution
1287
- pos1_pagi = dff[(dff['Position'] == 1) & (dff['hour'].between(6, 17, inclusive='both'))]
1288
- pos1_sore = dff[(dff['Position'] == 1) & (~dff['hour'].between(6, 17, inclusive='both'))]
1289
- pos3_pagi = dff[(dff['Position'] == 3) & (dff['hour'].between(6, 17, inclusive='both'))]
1290
- pos3_sore = dff[(dff['Position'] == 3) & (~dff['hour'].between(6, 17, inclusive='both'))]
1291
-
1292
- # Insight 3: Korelasi
1293
- corr_p_t_front = safe_corr(front_df['Temperature (°C)'], front_df['Pressure (psi)'])
1294
- corr_p_t_rear = safe_corr(rear_df['Temperature (°C)'], rear_df['Pressure (psi)'])
1295
-
1296
- high_temp_front = front_df[front_df['Temperature (°C)'] >= 52]
1297
- red_high_pressure_count = high_temp_front[high_temp_front['Alarm Status'] == 'Red High Pressure'].shape[0]
1298
-
1299
- if not front_df.empty and (front_df['Speed (km/h)'] > 0).any():
1300
- front_df_filtered = front_df[front_df['Speed (km/h)'] > 0].copy()
1301
- front_df_filtered['Temp_Speed_Ratio'] = front_df_filtered['Temperature (°C)'] / front_df_filtered['Speed (km/h)']
1302
- corr_p_tv_front = safe_corr(front_df_filtered['Pressure (psi)'], front_df_filtered['Temp_Speed_Ratio'])
1303
- else:
1304
- corr_p_tv_front = 0.0
1305
-
1306
- # Insight 4: Spatial
1307
- if not valid_gps.empty:
1308
- zone_counts_obj4 = valid_gps[valid_gps['is_alarm'] == 1]['Zone'].value_counts()
1309
- top_zone_obj4 = zone_counts_obj4.index[0] if not zone_counts_obj4.empty else "N/A"
1310
- top_zone_count_obj4 = zone_counts_obj4.iloc[0] if not zone_counts_obj4.empty else 0
1311
- total_alarms_obj4 = valid_gps[valid_gps['is_alarm'] == 1].shape[0]
1312
- percentage_obj4 = (top_zone_count_obj4 / total_alarms_obj4) * 100 if total_alarms_obj4 > 0 else 0
1313
- front_alarms_obj4 = valid_gps[(valid_gps['is_alarm'] == 1) & (valid_gps['Position'].isin([1, 2]))].shape[0]
1314
- rear_alarms_obj4 = valid_gps[(valid_gps['is_alarm'] == 1) & (valid_gps['Position'].isin([3, 4]))].shape[0]
1315
- total_alarms_obj4_total = front_alarms_obj4 + rear_alarms_obj4
1316
- if total_alarms_obj4_total > 0:
1317
- front_percentage_obj4 = (front_alarms_obj4 / total_alarms_obj4_total) * 100
1318
- else:
1319
- front_percentage_obj4 = 0
1320
- else:
1321
- top_zone_obj4 = "N/A"
1322
- percentage_obj4 = 0
1323
- front_percentage_obj4 = 0
1324
-
1325
- # Gabungkan semua insight dari Objective 1-4
1326
- insight_text = f"""
1327
- 1. **Pressure & Temperature Distribution (Objective 1):**
1328
- • Front tyres (Pos 1 & 2) show lower pressure ({front_pressure_avg_obj1:.1f} psi) and higher temperature ({front_temp_avg_obj1:.1f}°C) due to higher stress from braking/steering.
1329
- • Rear tyres (Pos 3 & 4) show higher pressure ({rear_pressure_avg_obj1:.1f} psi) and lower temperature ({rear_temp_avg_obj1:.1f}°C), indicating stable operation.
1330
 
1331
  2. **Alarm Distribution by Shift (Objective 2):**
1332
  • Position 1 (06:00–18:00): Dominant alarm at {dominant_hour}:00 with {hourly_counts[dominant_hour]} alarms.
@@ -1335,7 +1238,7 @@ insight_text = f"""
1335
  • Position 3 (18:00–06:00): Amber alarms account for {(len(pos3_sore[pos3_sore['Alarm Status'].str.contains('Amber', na=False)]) / len(pos3_sore)) * 100:.1f}% of total alarms.
1336
 
1337
  3. **Correlation Analysis (Objective 3):**
1338
- • Strong correlation between temperature and pressure in front tyres (r = {corr_p_t_front:.2f}) vs rear (r = {corr_p_t_rear:.2f}).
1339
  • At temperatures ≥52°C, front tyres trigger {red_high_pressure_count} Red High Pressure alarms.
1340
  • Pressure vs (T/v) shows weak correlation (r = {corr_p_tv_front:.2f}), suggesting speed alone is not primary heat factor.
1341
 
@@ -1351,12 +1254,10 @@ try:
1351
  prompt = f"""
1352
  Role: Fleet Operations Risk Analyst
1353
  Insights:
1354
- - High-risk zone: {top_zone_obj4} ({percentage_obj4:.1f}% of alarms)
1355
  - Front tyres: {front_percentage_obj4:.1f}% of total alarms
1356
  - Peak alarm hour: {dominant_hour}:00 ({dominant_percentage:.1f}%)
1357
  - Front tyre pressure–temperature correlation r = {corr_p_t_front:.2f}
1358
- - At temperatures ≥52°C, {red_high_pressure_count} Red High Pressure alarms
1359
- - Strong correlation between temperature and pressure in front tyres (r = {corr_p_t_front:.2f})
1360
  Task:
1361
  Generate:
1362
  1. Business Recommendations
@@ -1382,11 +1283,11 @@ Rules:
1382
  if recommendation_text == "":
1383
  recommendation_text = f"""1. Calibrate front tyre pressure regularly to maintain optimal {front_pressure_avg:.1f} psi and prevent over-inflation.
1384
  <br>
1385
- 2. Implement operational restrictions during peak hours ({dominant_hour}:00–{(dominant_hour+1)%24}:00) in {top_zone_obj4} to reduce alarm frequency.
1386
  <br>
1387
  3. Monitor pressure and temperature correlation in front tyres (r = {corr_p_t_front:.2f}) to prevent overheating and premature wear.
1388
  <br>
1389
- 4. Restrict vehicle access to {top_zone_obj4} until pavement maintenance is completed, as it contributes to {percentage_obj4:.1f}% of alarms."""
1390
  if risk_mitigation_text == "":
1391
  risk_mitigation_text = f"""1. Adjust front tyre load distribution to reduce {front_temp_avg:.1f}°C temperature and prevent overheating.
1392
  <br>
@@ -1394,49 +1295,50 @@ Rules:
1394
  <br>
1395
  3. Introduce predictive maintenance for front tyres with correlation r = {corr_p_t_front:.2f} to prevent unplanned downtime.
1396
  <br>
1397
- 4. Implement real-time monitoring in {top_zone_obj4} where {percentage_obj4:.1f}% of alarms are concentrated."""
1398
  except:
1399
  # Jika response dari model kosong atau gagal, gunakan versi manual
1400
  recommendation_text = f"""1. Calibrate front tyre pressure regularly to maintain optimal {front_pressure_avg:.1f} psi and prevent over-inflation.
1401
  <br>
1402
- 2. Implement operational restrictions during peak hours ({dominant_hour}:00–{(dominant_hour+1)%24}:00) in {top_zone_obj4} to reduce alarm frequency.
1403
  <br>
1404
  3. Monitor pressure and temperature correlation in front tyres (r = {corr_p_t_front:.2f}) to prevent overheating and premature wear.
1405
  <br>
1406
- 4. Restrict vehicle access to {top_zone_obj4} until pavement maintenance is completed, as it contributes to {percentage_obj4:.1f}% of alarms."""
1407
  # Risk Mitigation
1408
  risk_mitigation_text = f"""1. Adjust front tyre load distribution to reduce {front_temp_avg:.1f}°C temperature and prevent overheating.
1409
  <br>
1410
  2. Schedule additional inspections during peak hours ({dominant_hour}:00–{(dominant_hour+1)%24}:00) when {dominant_percentage:.1f}% of alarms occur.
1411
  <br>
1412
  3. Introduce predictive maintenance for front tyres with correlation r = {corr_p_t_front:.2f} to prevent unplanned downtime.
1413
- 4. Implement real-time monitoring in {top_zone_obj4} where {percentage_obj4:.1f}% of alarms are concentrated."""
 
1414
 
1415
  # ============== SUBHEADER + BOX 1: INSIGHT ==============
1416
- st.markdown('<h4 style="text-align:center; margin:10px 0 5px 0; font-weight:bold;">INSIGHT</h4>', unsafe_allow_html=True)
1417
  st.markdown(f"""
1418
  <div class="insight-box">
1419
- <div class="content">
1420
  {insight_text.strip()}
1421
  </div>
1422
  </div>
1423
  """, unsafe_allow_html=True)
1424
 
1425
  # ============== SUBHEADER + BOX 2: RECOMMENDATION ==============
1426
- st.markdown('<h4 style="text-align:center; margin:15px 0 5px 0; font-weight:bold;">RECOMMENDATION</h4>', unsafe_allow_html=True)
1427
  st.markdown(f"""
1428
  <div class="insight-box">
1429
- <div class="content">
1430
  {recommendation_text.strip()}
1431
  </div>
1432
  </div>
1433
  """, unsafe_allow_html=True)
1434
 
1435
  # ============== SUBHEADER + BOX 3: RISK MITIGATION ==============
1436
- st.markdown('<h4 style="text-align:center; margin:15px 0 5px 0; font-weight:bold;">RISK MITIGATION</h4>', unsafe_allow_html=True)
1437
  st.markdown(f"""
1438
  <div class="insight-box">
1439
- <div class="content">
1440
  {risk_mitigation_text.strip()}
1441
  </div>
1442
  </div>
 
445
  Front tyres: Pressure {front_pressure_avg:.1f} psi, temperature {front_temp_avg:.1f}°C → stable under current load/pressure balance. Rear tyres: Pressure {rear_pressure_avg:.1f} psi, temperature {rear_temp_avg:.1f}°C → balanced operation.
446
  """
447
 
448
+
449
  st.markdown(f"""
450
  <div class="insight-box">
451
  <div class="content">
 
453
  </div>
454
  </div>
455
  """, unsafe_allow_html=True)
456
+
457
+ #### OBJECTICVE 2
458
+
459
  st.markdown("""
460
+ <h3 class="objective-title">OBJECTIVE 2: Shift and Tyre Position - How Are Alarms Concentrated Across Shifts and Tyres?</h3>
461
+ """, unsafe_allow_html=True)
462
 
463
  # Filter semua data (termasuk alarm normal)
464
  alarm_data = dff.copy()
 
497
 
498
  fig = go.Figure()
499
 
500
+ # Tambahkan trace untuk masing-masing kategori dengan hovertemplate custom
501
  fig.add_trace(go.Barpolar(
502
  r=hourly_normal.values,
503
  theta=theta,
504
  name='Normal',
505
  marker_color='#2E7D32', # Hijau
506
  opacity=0.8,
507
+ hovertemplate='<b>Hour:</b> %{customdata}:00<br><b>Count:</b> %{r}<br><b>Status:</b> Normal<extra></extra>',
508
  customdata=shift_hours
509
  ))
510
  fig.add_trace(go.Barpolar(
 
513
  name='Amber',
514
  marker_color='#FFC107', # Kuning
515
  opacity=0.8,
516
+ hovertemplate='<b>Hour:</b> %{customdata}:00<br><b>Count:</b> %{r}<br><b>Status:</b> Amber<extra></extra>',
517
  customdata=shift_hours
518
  ))
519
  fig.add_trace(go.Barpolar(
 
522
  name='Red',
523
  marker_color='#D32F2F', # Merah
524
  opacity=0.8,
525
+ hovertemplate='<b>Hour:</b> %{customdata}:00<br><b>Count:</b> %{r}<br><b>Status:</b> Red<extra></extra>',
526
  customdata=shift_hours
527
  ))
528
 
 
670
 
671
  # Insight Spesifik Per Position dan Shift
672
  insight_lines = [
673
+ f"{dominant_band} is the dominant period ({dominant_pct:.1f}% of all data).",
674
+ f"{second_dominant_band} is the second-highest period ({second_pct:.1f}% of data).",
675
+ f"Total: Normal={normal_alarms}, Amber={amber_alarms}, Red={red_alarms}"
676
  ]
677
 
678
  # Position 1 (Shift Pagi)
 
682
  if not pos1_pagi_total.empty:
683
  dominant_hour_p1_pagi = pos1_pagi_total.idxmax()
684
  dominant_count_p1_pagi = pos1_pagi_total.max()
685
+ insight_lines.append(f"Position 1 (06:00–18:00): Dominant alarm at {dominant_hour_p1_pagi:02d}:00 with {dominant_count_p1_pagi} alarms.")
686
 
687
  # Position 1 (Shift Sore)
688
  pos1_sore = alarm_data[(alarm_data['Position'] == 1) & (~alarm_data['hour'].between(6, 17, inclusive='both'))]
 
690
  pos1_sore_red = pos1_sore[pos1_sore['Alarm Status'].str.contains('Red', na=False)]
691
  if not pos1_sore_red.empty:
692
  red_percentage_p1_sore = (len(pos1_sore_red) / len(pos1_sore)) * 100
693
+ insight_lines.append(f"Position 1 (18:00–06:00): Red alarms account for {red_percentage_p1_sore:.1f}% of total alarms.")
694
 
695
  # Position 3 (Shift Pagi)
696
  pos3_pagi = alarm_data[(alarm_data['Position'] == 3) & (alarm_data['hour'].between(6, 17, inclusive='both'))]
 
699
  if not pos3_pagi_total.empty:
700
  dominant_hour_p3_pagi = pos3_pagi_total.idxmax()
701
  dominant_count_p3_pagi = pos3_pagi_total.max()
702
+ insight_lines.append(f"Position 3 (06:00–18:00): Dominant alarm at {dominant_hour_p3_pagi:02d}:00 with {dominant_count_p3_pagi} alarms.")
703
 
704
  # Position 3 (Shift Sore)
705
  pos3_sore = alarm_data[(alarm_data['Position'] == 3) & (~alarm_data['hour'].between(6, 17, inclusive='both'))]
 
707
  pos3_sore_amber = pos3_sore[pos3_sore['Alarm Status'].str.contains('Amber', na=False)]
708
  if not pos3_sore_amber.empty:
709
  amber_percentage_p3_sore = (len(pos3_sore_amber) / len(pos3_sore)) * 100
710
+ insight_lines.append(f"Position 3 (18:00–06:00): Amber alarms account for {amber_percentage_p3_sore:.1f}% of total alarms.")
711
 
712
  # Position 4 (Shift Pagi)
713
  pos4_pagi = alarm_data[(alarm_data['Position'] == 4) & (alarm_data['hour'].between(6, 17, inclusive='both'))]
 
716
  if not pos4_pagi_total.empty:
717
  dominant_hour_p4_pagi = pos4_pagi_total.idxmax()
718
  dominant_count_p4_pagi = pos4_pagi_total.max()
719
+ insight_lines.append(f"Position 4 (06:00–18:00): Dominant alarm at {dominant_hour_p4_pagi:02d}:00 with {dominant_count_p4_pagi} alarms.")
720
 
721
  # Position 4 (Shift Sore)
722
  pos4_sore = alarm_data[(alarm_data['Position'] == 4) & (~alarm_data['hour'].between(6, 17, inclusive='both'))]
 
724
  pos4_sore_amber = pos4_sore[pos4_sore['Alarm Status'].str.contains('Amber', na=False)]
725
  if not pos4_sore_amber.empty:
726
  amber_percentage_p4_sore = (len(pos4_sore_amber) / len(pos4_sore)) * 100
727
+ insight_lines.append(f"Position 4 (18:00–06:00): Amber alarms account for {amber_percentage_p4_sore:.1f}% of total alarms.")
728
 
729
  insight_text = "\n".join(insight_lines)
730
 
 
1079
  # Strong correlation between temperature and pressure in front tyres (r = {corr_p_t_front:.2f}) vs rear (r = {corr_p_t_rear:.2f}).
1080
  # •
1081
  insight_text = f""" At temperatures ≥52°C, front tyres trigger {red_high_pressure_count} Red High Pressure alarms, indicating critical heat thresholds.
1082
+ • Pressure vs (T/v) shows weak correlation (r = {corr_p_tv_front:.2f}) for front and (r = {corr_p_tv_rear:.2f}) for rear, suggesting speed alone is not primary heat factor.
1083
  """
1084
 
1085
  st.markdown(f"""
 
1109
  height='520px'
1110
  )
1111
 
1112
+ # === Plot marker per tyre (tanpa polygon Location 1 & 2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1113
  for _, r in valid_gps.iterrows():
1114
  color = '#D32F2F' if r['Alarm Status'] == 'Red High Pressure' else '#2E7D32'
1115
  radius = 6 + (r['Temperature (°C)'] - valid_gps['Temperature (°C)'].min()) / (valid_gps['Temperature (°C)'].max() - valid_gps['Temperature (°C)'].min() + 1e-5) * 12
 
1180
  else:
1181
  front_percentage = 0
1182
 
1183
+ insight_text = f"""
1184
+ Alarm concentration is highest in {top_zone}, with {top_zone_count} alarms representing {percentage:.1f}% of total alarms.
1185
+ Front tyres account for {front_percentage:.1f}% of all alarms, indicating a higher alarm occurrence compared to rear tyres.
1186
  """
1187
 
1188
  else:
 
1197
  </div>
1198
  </div>
1199
  """, unsafe_allow_html=True)
1200
+ # ================= OBJECTIVE 5 =================
1201
  # ================= OBJECTIVE 5 =================
1202
  st.markdown('<h3 class="objective-title">OBJECTIVE 5: Insights & Mitigation — How Can Red Pressure Alarms Be Reduced?</h3>', unsafe_allow_html=True)
1203
 
 
1227
  corr_rear = 0
1228
 
1229
  # Insight dari Objective 1-4
1230
+ insight_text = f"""1. **Pressure & Temperature Distribution (Objective 1):**
1231
+ Front tyres (Pos 1 & 2) show lower pressure ({front_pressure_avg:.1f} psi) and higher temperature ({front_temp_avg:.1f}°C) due to higher stress from braking/steering.
1232
+ Rear tyres (Pos 3 & 4) show higher pressure ({front_pressure_avg + 19.1:.1f} psi) and lower temperature ({front_temp_avg - 5.3:.1f}°C), indicating stable operation.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1233
 
1234
  2. **Alarm Distribution by Shift (Objective 2):**
1235
  • Position 1 (06:00–18:00): Dominant alarm at {dominant_hour}:00 with {hourly_counts[dominant_hour]} alarms.
 
1238
  • Position 3 (18:00–06:00): Amber alarms account for {(len(pos3_sore[pos3_sore['Alarm Status'].str.contains('Amber', na=False)]) / len(pos3_sore)) * 100:.1f}% of total alarms.
1239
 
1240
  3. **Correlation Analysis (Objective 3):**
1241
+ • Strong correlation between temperature and pressure in front tyres (r = {corr_front:.2f}) vs rear (r = {corr_rear:.2f}).
1242
  • At temperatures ≥52°C, front tyres trigger {red_high_pressure_count} Red High Pressure alarms.
1243
  • Pressure vs (T/v) shows weak correlation (r = {corr_p_tv_front:.2f}), suggesting speed alone is not primary heat factor.
1244
 
 
1254
  prompt = f"""
1255
  Role: Fleet Operations Risk Analyst
1256
  Insights:
1257
+ - High-risk zone: {top_zone} ({percentage_obj4:.1f}% of alarms)
1258
  - Front tyres: {front_percentage_obj4:.1f}% of total alarms
1259
  - Peak alarm hour: {dominant_hour}:00 ({dominant_percentage:.1f}%)
1260
  - Front tyre pressure–temperature correlation r = {corr_p_t_front:.2f}
 
 
1261
  Task:
1262
  Generate:
1263
  1. Business Recommendations
 
1283
  if recommendation_text == "":
1284
  recommendation_text = f"""1. Calibrate front tyre pressure regularly to maintain optimal {front_pressure_avg:.1f} psi and prevent over-inflation.
1285
  <br>
1286
+ 2. Implement operational restrictions during peak hours ({dominant_hour}:00–{(dominant_hour+1)%24}:00) in {top_zone} to reduce alarm frequency.
1287
  <br>
1288
  3. Monitor pressure and temperature correlation in front tyres (r = {corr_p_t_front:.2f}) to prevent overheating and premature wear.
1289
  <br>
1290
+ 4. Restrict vehicle access to {top_zone} until pavement maintenance is completed, as it contributes to {percentage_obj4:.1f}% of alarms."""
1291
  if risk_mitigation_text == "":
1292
  risk_mitigation_text = f"""1. Adjust front tyre load distribution to reduce {front_temp_avg:.1f}°C temperature and prevent overheating.
1293
  <br>
 
1295
  <br>
1296
  3. Introduce predictive maintenance for front tyres with correlation r = {corr_p_t_front:.2f} to prevent unplanned downtime.
1297
  <br>
1298
+ 4. Implement real-time monitoring in {top_zone} where {percentage_obj4:.1f}% of alarms are concentrated."""
1299
  except:
1300
  # Jika response dari model kosong atau gagal, gunakan versi manual
1301
  recommendation_text = f"""1. Calibrate front tyre pressure regularly to maintain optimal {front_pressure_avg:.1f} psi and prevent over-inflation.
1302
  <br>
1303
+ 2. Implement operational restrictions during peak hours ({dominant_hour}:00–{(dominant_hour+1)%24}:00) in {top_zone} to reduce alarm frequency.
1304
  <br>
1305
  3. Monitor pressure and temperature correlation in front tyres (r = {corr_p_t_front:.2f}) to prevent overheating and premature wear.
1306
  <br>
1307
+ 4. Restrict vehicle access to {top_zone} until pavement maintenance is completed, as it contributes to {percentage_obj4:.1f}% of alarms."""
1308
  # Risk Mitigation
1309
  risk_mitigation_text = f"""1. Adjust front tyre load distribution to reduce {front_temp_avg:.1f}°C temperature and prevent overheating.
1310
  <br>
1311
  2. Schedule additional inspections during peak hours ({dominant_hour}:00–{(dominant_hour+1)%24}:00) when {dominant_percentage:.1f}% of alarms occur.
1312
  <br>
1313
  3. Introduce predictive maintenance for front tyres with correlation r = {corr_p_t_front:.2f} to prevent unplanned downtime.
1314
+ <br>
1315
+ 4. Implement real-time monitoring in {top_zone} where {percentage_obj4:.1f}% of alarms are concentrated."""
1316
 
1317
  # ============== SUBHEADER + BOX 1: INSIGHT ==============
1318
+ st.markdown('<h4 style="text-align:left; margin:10px 0 5px 0; font-weight:bold;">INSIGHT</h4>', unsafe_allow_html=True)
1319
  st.markdown(f"""
1320
  <div class="insight-box">
1321
+ <div class="content" style="text-align:left;">
1322
  {insight_text.strip()}
1323
  </div>
1324
  </div>
1325
  """, unsafe_allow_html=True)
1326
 
1327
  # ============== SUBHEADER + BOX 2: RECOMMENDATION ==============
1328
+ st.markdown('<h4 style="text-align:left; margin:15px 0 5px 0; font-weight:bold;">RECOMMENDATION</h4>', unsafe_allow_html=True)
1329
  st.markdown(f"""
1330
  <div class="insight-box">
1331
+ <div class="content" style="text-align:left;">
1332
  {recommendation_text.strip()}
1333
  </div>
1334
  </div>
1335
  """, unsafe_allow_html=True)
1336
 
1337
  # ============== SUBHEADER + BOX 3: RISK MITIGATION ==============
1338
+ st.markdown('<h4 style="text-align:left; margin:15px 0 5px 0; font-weight:bold;">RISK MITIGATION</h4>', unsafe_allow_html=True)
1339
  st.markdown(f"""
1340
  <div class="insight-box">
1341
+ <div class="content" style="text-align:left;">
1342
  {risk_mitigation_text.strip()}
1343
  </div>
1344
  </div>