SHELLAPANDIANGANHUNGING commited on
Commit
63fa9ef
·
verified ·
1 Parent(s): 7db5dbe

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +247 -303
app.py CHANGED
@@ -248,13 +248,14 @@ df = load_data()
248
  st.markdown("""
249
  <div class="main-header" style="text-align:center;">
250
  <h1>Michelin Mining Tyre Analytics</h1>
251
- <p style="font-size:14px; color:#b0b0b0; margin-top:-10px;">
252
- Analysis is based on daily aggregated data
253
  </p>
254
  </div>
255
  """, unsafe_allow_html=True)
256
 
257
 
 
258
  # ================= LOGO (Perbaikan: Base64 Embed - Selalu Muncul) =================
259
  def render_logo():
260
  # Logo dalam base64 (diambil dari URL Anda)
@@ -344,50 +345,6 @@ if submit:
344
  else:
345
  dff = df
346
 
347
- # ================= OBJECTIVE 1 =================
348
- # ================= OBJECTIVE 1 =================
349
- # st.markdown('<h3 class="objective-title">OBJECTIVE 1: Pressure & Temperature Trends — How Do Front and Rear Tyres Distributin?</h3>', unsafe_allow_html=True)
350
-
351
- # col1, col2 = st.columns(2)
352
-
353
- # with col1:
354
- # st.markdown('<h5 style="text-align:center; margin-top: 0;">Pressure Distribution per Tyre Position</h5>', unsafe_allow_html=True)
355
- # fig1 = px.box(
356
- # dff,
357
- # x='Position',
358
- # y='Pressure (psi)',
359
- # color='Position',
360
- # color_discrete_map={1: '#d50000', 2: '#ff6d00', 3: '#ffcc00', 4: '#007acc'},
361
- # template="plotly_white"
362
- # )
363
- # red_high = dff['Red High Press (psi)'].min()
364
- # amber_high = dff['Amber High Press (psi)'].min()
365
- # fig1.add_hline(y=red_high, line_dash="dash", line_color="red", annotation_text="Red High Press", annotation_position="top right")
366
- # fig1.add_hline(y=amber_high, line_color="orange", annotation_text="Amber High Press", annotation_position="bottom right")
367
- # fig1.update_layout(margin=dict(t=40))
368
- # st.plotly_chart(fig1, use_container_width=True)
369
-
370
- # with col2:
371
- # st.markdown('<h5 style="text-align:center; margin-top: 0;">Temperature Distribution per Tyre Position</h5>', unsafe_allow_html=True)
372
- # fig2 = px.box(
373
- # dff,
374
- # x='Position',
375
- # y='Temperature (°C)',
376
- # color='Position',
377
- # color_discrete_map={1: '#d50000', 2: '#ff6d00', 3: '#ffcc00', 4: '#007acc'},
378
- # template="plotly_white"
379
- # )
380
- # red_temp = dff['Absolute Red Temp (°C)'].min()
381
- # amber_temp = dff['Absolute Amber Temp (°C)'].min()
382
- # fig2.add_hline(y=red_temp, line_dash="dash", line_color="red", annotation_text="Red Temp", annotation_position="top right")
383
- # fig2.add_hline(y=amber_temp, line_color="orange", annotation_text="Amber Temp", annotation_position="bottom right")
384
- # fig2.update_layout(margin=dict(t=40))
385
- # st.plotly_chart(fig2, use_container_width=True)
386
-
387
- # Insight 1
388
- # Insight 1
389
- # ================= OBJECTIVE 1 =================
390
- # Ensure 'Position' is treated as ordered categorical for consistent sorting
391
  dff = dff.copy()
392
  dff['Position'] = pd.Categorical(dff['Position'], categories=[1, 2, 3, 4], ordered=True)
393
 
@@ -495,197 +452,8 @@ st.markdown(f"""
495
  </div>
496
  </div>
497
  """, unsafe_allow_html=True)
498
- # ================= OBJECTIVE 2 =================
499
- # ================= OBJECTIVE 2 =================
500
- # st.markdown('<h3 class="objective-title">OBJECTIVE 2: Hourly Alarm Trend Analysis — By Tyre Position</h3>', unsafe_allow_html=True)
501
-
502
- # # Ambil data alarm
503
- # alarm_data = dff[dff['is_alarm'] == 1].copy()
504
-
505
- # if alarm_data.empty:
506
- # st.warning("No alarm data to display.")
507
- # else:
508
- # # Hitung jumlah alarm per jam dan per posisi
509
- # hourly_pos_counts = alarm_data.groupby(['hour', 'Position']).size().unstack(fill_value=0)
510
-
511
- # # Pastikan semua posisi (1,2,3,4) ada di kolom
512
- # for pos in [1, 2, 3, 4]:
513
- # if pos not in hourly_pos_counts.columns:
514
- # hourly_pos_counts[pos] = 0
515
-
516
- # # Urutkan kolom
517
- # hourly_pos_counts = hourly_pos_counts[[1, 2, 3, 4]]
518
-
519
- # # Gunakan warna sesuai Objective 1
520
- # color_map = {1: '#003DA5', 2: '#7FA6E8', 3: '#FFB300', 4: '#FFE082'}
521
-
522
- # # Buat grafik trend
523
- # fig = px.line(
524
- # hourly_pos_counts,
525
- # x=hourly_pos_counts.index,
526
- # y=[1, 2, 3, 4],
527
- # title="Hourly Alarm Count by Tyre Position",
528
- # labels={'value': 'Number of Alarms', 'variable': 'Tyre Position'},
529
- # template="plotly_white",
530
- # line_shape='linear',
531
- # color_discrete_map=color_map
532
- # )
533
-
534
- # # Ganti nama legend
535
- # fig.for_each_trace(lambda t: t.update(name=f'Position {int(t.name)}'))
536
-
537
- # fig.update_layout(
538
- # xaxis=dict(
539
- # title="Hour of Day",
540
- # tickmode='array',
541
- # tickvals=list(range(0, 24)),
542
- # ticktext=[f"{h:02d}:00" for h in range(24)],
543
- # tickangle=45
544
- # ),
545
- # yaxis=dict(title="Number of Alarms"),
546
- # legend_title_text='Tyre Position',
547
- # margin=dict(t=40, b=20, l=20, r=20)
548
- # )
549
-
550
- # st.plotly_chart(fig, use_container_width=True)
551
-
552
- # # Insight singkat
553
- # total_by_pos = hourly_pos_counts.sum()
554
- # insight_text = f"""
555
- # • Position 1: {total_by_pos[1]} alarms | Position 2: {total_by_pos[2]} alarms
556
- # • Position 3: {total_by_pos[3]} alarms | Position 4: {total_by_pos[4]} alarms
557
- # • Peak alarm patterns vary by position — suggesting location-specific stress factors.
558
- # """
559
- # st.markdown(f"""
560
- # <div class="insight-box">
561
- # <div class="content">
562
- # {insight_text.strip()}
563
- # </div>
564
- # </div>
565
- # """, unsafe_allow_html=True)
566
- # import pandas as pd
567
- # import plotly.express as px
568
- # import streamlit as st
569
-
570
- # # Judul Objective 2 — Center-aligned, elegant box-style (kanan atas kotak)
571
- # st.markdown('''
572
- # <div style="text-align: center; margin-bottom: 16px;">
573
- # <div style="
574
- # background-color: #2C2C2C;
575
- # color: white;
576
- # padding: 10px 16px;
577
- # display: inline-block;
578
- # border-radius: 6px;
579
- # font-weight: 600;
580
- # position: relative;
581
- # top: 0; right: 0;
582
- # ">
583
- # OBJECTIVE 2: Hourly Data Capture vs Alarm Count Analysis
584
- # </div>
585
- # </div>
586
- # ''', unsafe_allow_html=True)
587
-
588
- # col1, col2 = st.columns(2)
589
-
590
- # # =============== COL 1: Capture Data per Jam per Tyre ===============
591
- # with col1:
592
- # st.markdown('<h5 style="text-align:center; margin-top: 0;">Data Capture per Hour by Tyre Position</h5>', unsafe_allow_html=True)
593
-
594
- # # Hitung jumlah data capture per jam dan per posisi (semua data, bukan hanya alarm)
595
- # capture_data = dff.copy()
596
- # hourly_capture_counts = capture_data.groupby(['hour', 'Position']).size().unstack(fill_value=0)
597
-
598
- # # Pastikan semua posisi (1,2,3,4) ada
599
- # for pos in [1, 2, 3, 4]:
600
- # if pos not in hourly_capture_counts.columns:
601
- # hourly_capture_counts[pos] = 0
602
-
603
- # hourly_capture_counts = hourly_capture_counts[[1, 2, 3, 4]].copy()
604
-
605
- # # Melt data untuk plotting
606
- # hourly_capture_melted = hourly_capture_counts.reset_index().melt(
607
- # id_vars='hour',
608
- # value_vars=[1, 2, 3, 4],
609
- # var_name='Position',
610
- # value_name='Capture Count'
611
- # )
612
- # hourly_capture_melted['Position'] = hourly_capture_melted['Position'].astype(str)
613
-
614
- # # Warna sesuai preferensi
615
- # color_map = {
616
- # '1': '#003DA5', # Dark blue
617
- # '2': '#7FA6E8', # Light blue
618
- # '3': '#FFB300', # Amber (PLN yellow)
619
- # '4': '#FFE082' # Light amber
620
- # }
621
-
622
- # fig1 = px.line(
623
- # hourly_capture_melted,
624
- # x='hour',
625
- # y='Capture Count',
626
- # color='Position',
627
- # color_discrete_map=color_map,
628
- # title="Data Capture per Hour by Tyre Position",
629
- # labels={'Capture Count': 'Number of Records', 'Position': 'Tyre Position'},
630
- # line_shape='linear',
631
- # template="plotly_white"
632
- # )
633
-
634
- # fig1.update_layout(
635
- # xaxis=dict(
636
- # title="Hour of Day",
637
- # tickmode='array',
638
- # tickvals=list(range(0, 24)),
639
- # ticktext=[f"{h:02d}:00" for h in range(24)],
640
- # tickangle=45
641
- # ),
642
- # yaxis=dict(title="Number of Records"),
643
- # legend_title_text='Tyre Position',
644
- # margin=dict(t=40, b=40, l=40, r=20),
645
- # title_x=0.5
646
- # )
647
-
648
- # st.plotly_chart(fig1, use_container_width=True)
649
-
650
- # # =============== COL 2: Count Alarm (Amber & Red Only) ===============
651
- # with col2:
652
- # st.markdown('<h5 style="text-align:center; margin-top: 0;">Alarm Count (Amber & Red Only) per Hour by Tyre Position</h5>', unsafe_allow_html=True)
653
-
654
- # # Filter hanya alarm Amber dan Red
655
- # alarm_data = dff[dff['is_alarm'] == 1].copy()
656
- # alarm_data = alarm_data[alarm_data['Alarm Status'].str.contains('Amber|Red', case=False, na=False)]
657
-
658
- # if alarm_data.empty:
659
- # st.warning("No Amber or Red alarm data to display.")
660
- # else:
661
- # # Hitung jumlah alarm per jam dan per posisi
662
- # hourly_alarm_counts = alarm_data.groupby(['hour', 'Position']).size().unstack(fill_value=0)
663
-
664
- # # Pastikan semua posisi (1,2,3,4) ada
665
- # for pos in [1, 2, 3, 4]:
666
- # if pos not in hourly_alarm_counts.columns:
667
- # hourly_alarm_counts[pos] = 0
668
-
669
- # hourly_alarm_counts = hourly_alarm_counts[[1, 2, 3, 4]].copy()
670
-
671
- # # Melt data untuk plotting
672
- # hourly_alarm_melted = hourly_alarm_counts.reset_index().melt(
673
- # id_vars='hour',
674
- # value_vars=[1, 2, 3, 4],
675
- # var_name='Position',
676
- # value_name='Alarm Count'
677
- # )
678
- # hourly_alarm_melted['Position'] = hourly_alarm_melted['Position'].astype(str)
679
-
680
- # fig2 = px.line(
681
- # hourly_alarm_melted,
682
- # x='hour',
683
- # y='Alarm Count',
684
-
685
  st.markdown("""
686
- <h3 class="objective-title">OBJECTIVE 2: Alarm Frequency Analysis Shift-Based Radial Charts</h3>
687
- <small>*Showing alarm types by shift: Normal (Green), Amber (Yellow), Red (Red)</small>
688
- """, unsafe_allow_html=True)
689
 
690
  # Filter semua data (termasuk alarm normal)
691
  alarm_data = dff.copy()
@@ -723,27 +491,36 @@ def create_radial_chart(pos_data, title, shift_hours, shift_type):
723
  ticktext = ["03:00", "18:00/06:00", "21:00", "00:00"]
724
 
725
  fig = go.Figure()
 
 
726
  fig.add_trace(go.Barpolar(
727
  r=hourly_normal.values,
728
  theta=theta,
729
  name='Normal',
730
  marker_color='#2E7D32', # Hijau
731
- opacity=0.8
 
 
732
  ))
733
  fig.add_trace(go.Barpolar(
734
  r=hourly_amber.values,
735
  theta=theta,
736
  name='Amber',
737
  marker_color='#FFC107', # Kuning
738
- opacity=0.8
 
 
739
  ))
740
  fig.add_trace(go.Barpolar(
741
  r=hourly_red.values,
742
  theta=theta,
743
  name='Red',
744
  marker_color='#D32F2F', # Merah
745
- opacity=0.8
 
 
746
  ))
 
747
  fig.update_layout(
748
  polar=dict(
749
  angularaxis=dict(
@@ -773,7 +550,7 @@ with col1:
773
  st.markdown('<div style="text-align:center; font-weight:bold; margin-bottom: 8px;">Position 1 (06:00–18:00)</div>', unsafe_allow_html=True)
774
  pos1_data = alarm_data[alarm_data['Position'] == 1].copy()
775
  pos1_data = pos1_data[pos1_data['hour'].between(6, 17, inclusive='both')]
776
- fig1 = create_radial_chart(pos1_data, ")", list(range(6, 18)), 'pagi')
777
  if fig1 is not None:
778
  st.plotly_chart(fig1, use_container_width=True)
779
  else:
@@ -784,7 +561,7 @@ with col2:
784
  st.markdown('<div style="text-align:center; font-weight:bold; margin-bottom: 8px;">Position 1 (18:00–06:00)</div>', unsafe_allow_html=True)
785
  pos1_data = alarm_data[alarm_data['Position'] == 1].copy()
786
  pos1_data = pos1_data[~pos1_data['hour'].between(6, 17, inclusive='both')]
787
- fig2 = create_radial_chart(pos1_data, "", list(range(18, 24)) + list(range(0, 6)), 'sore')
788
  if fig2 is not None:
789
  st.plotly_chart(fig2, use_container_width=True)
790
  else:
@@ -795,7 +572,7 @@ with col3:
795
  st.markdown('<div style="text-align:center; font-weight:bold; margin-bottom: 8px;">Position 2 (06:00–18:00)</div>', unsafe_allow_html=True)
796
  pos2_data = alarm_data[alarm_data['Position'] == 2].copy()
797
  pos2_data = pos2_data[pos2_data['hour'].between(6, 17, inclusive='both')]
798
- fig3 = create_radial_chart(pos2_data, "", list(range(6, 18)), 'pagi')
799
  if fig3 is not None:
800
  st.plotly_chart(fig3, use_container_width=True)
801
  else:
@@ -806,7 +583,7 @@ with col4:
806
  st.markdown('<div style="text-align:center; font-weight:bold; margin-bottom: 8px;">Position 2 (18:00–06:00)</div>', unsafe_allow_html=True)
807
  pos2_data = alarm_data[alarm_data['Position'] == 2].copy()
808
  pos2_data = pos2_data[~pos2_data['hour'].between(6, 17, inclusive='both')]
809
- fig4 = create_radial_chart(pos2_data, "", list(range(18, 24)) + list(range(0, 6)), 'sore')
810
  if fig4 is not None:
811
  st.plotly_chart(fig4, use_container_width=True)
812
  else:
@@ -818,7 +595,7 @@ with col5:
818
  st.markdown('<div style="text-align:center; font-weight:bold; margin-bottom: 8px;">Position 3 (06:00–18:00)</div>', unsafe_allow_html=True)
819
  pos3_data = alarm_data[alarm_data['Position'] == 3].copy()
820
  pos3_data = pos3_data[pos3_data['hour'].between(6, 17, inclusive='both')]
821
- fig5 = create_radial_chart(pos3_data, "", list(range(6, 18)), 'pagi')
822
  if fig5 is not None:
823
  st.plotly_chart(fig5, use_container_width=True)
824
  else:
@@ -829,7 +606,7 @@ with col6:
829
  st.markdown('<div style="text-align:center; font-weight:bold; margin-bottom: 8px;">Position 3 (18:00–06:00)</div>', unsafe_allow_html=True)
830
  pos3_data = alarm_data[alarm_data['Position'] == 3].copy()
831
  pos3_data = pos3_data[~pos3_data['hour'].between(6, 17, inclusive='both')]
832
- fig6 = create_radial_chart(pos3_data, "", list(range(18, 24)) + list(range(0, 6)), 'sore')
833
  if fig6 is not None:
834
  st.plotly_chart(fig6, use_container_width=True)
835
  else:
@@ -840,7 +617,7 @@ with col7:
840
  st.markdown('<div style="text-align:center; font-weight:bold; margin-bottom: 8px;">Position 4 (06:00–18:00)</div>', unsafe_allow_html=True)
841
  pos4_data = alarm_data[alarm_data['Position'] == 4].copy()
842
  pos4_data = pos4_data[pos4_data['hour'].between(6, 17, inclusive='both')]
843
- fig7 = create_radial_chart(pos4_data, "", list(range(6, 18)), 'pagi')
844
  if fig7 is not None:
845
  st.plotly_chart(fig7, use_container_width=True)
846
  else:
@@ -851,7 +628,7 @@ with col8:
851
  st.markdown('<div style="text-align:center; font-weight:bold; margin-bottom: 8px;">Position 4 (18:00–06:00)</div>', unsafe_allow_html=True)
852
  pos4_data = alarm_data[alarm_data['Position'] == 4].copy()
853
  pos4_data = pos4_data[~pos4_data['hour'].between(6, 17, inclusive='both')]
854
- fig8 = create_radial_chart(pos4_data, "", list(range(18, 24)) + list(range(0, 6)), 'sore')
855
  if fig8 is not None:
856
  st.plotly_chart(fig8, use_container_width=True)
857
  else:
@@ -886,11 +663,64 @@ else:
886
  red_alarms = alarm_data[alarm_data['Alarm Status'].str.contains('Red', na=False)].shape[0]
887
  amber_alarms = alarm_data[alarm_data['Alarm Status'].str.contains('Amber', na=False)].shape[0]
888
 
 
889
  insight_lines = [
890
  f"• {dominant_band} is the dominant period ({dominant_pct:.1f}% of all data).",
891
  f"• {second_dominant_band} is the second-highest period ({second_pct:.1f}% of data).",
892
  f"• Total: Normal={normal_alarms}, Amber={amber_alarms}, Red={red_alarms}"
893
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
894
  insight_text = "\n".join(insight_lines)
895
 
896
  # =============== DISPLAY INSIGHT ===============
@@ -901,6 +731,7 @@ st.markdown(f"""
901
  </div>
902
  </div>
903
  """, unsafe_allow_html=True)
 
904
  # ================= OBJECTIVE 3 =================
905
  st.markdown('<h3 class="objective-title">OBJECTIVE 3: Correlation — How Does Heat Influence Pressure and Which Tyres Trigger Red Alarms?</h3>', unsafe_allow_html=True)
906
 
@@ -1227,12 +1058,23 @@ def safe_corr(a, b):
1227
  return np.corrcoef(a[mask], b[mask])[0, 1]
1228
 
1229
  corr_p_t_front = safe_corr(front_df['Temperature (°C)'], front_df['Pressure (psi)'])
1230
- corr_t_s_front = safe_corr(front_df['Temperature (°C)'], front_df['Speed (km/h)'])
1231
  corr_p_t_rear = safe_corr(rear_df['Temperature (°C)'], rear_df['Pressure (psi)'])
1232
- corr_t_s_rear = safe_corr(rear_df['Temperature (°C)'], rear_df['Speed (km/h)'])
1233
 
1234
- insight_text = f"""
1235
- Front tyres show stronger temperature-driven pressure response (r = {corr_p_t_front:.2f}) vs rear (r = {corr_p_t_rear:.2f}), confirming heat has greater impact on front tyre inflation. Temperature speed correlation is low on both front (r = {corr_t_s_front:.2f}) and rear (r = {corr_t_s_rear:.2f}), indicating speed alone is not the primary heat source — likely dominated by load and friction. Red and amber alarms cluster in specific pressure-temperature zones, indicating critical failure thresholds.
 
 
 
 
 
 
 
 
 
 
 
 
 
1236
  """
1237
 
1238
  st.markdown(f"""
@@ -1243,8 +1085,8 @@ st.markdown(f"""
1243
  </div>
1244
  """, unsafe_allow_html=True)
1245
 
1246
- # ================= OBJECTIVE 5 =================
1247
- st.markdown('<h3 class="objective-title">OBJECTIVE 5: Spatial Risk Mapping — Where Do Red Pressure Alarms Occur Most Frequently?</h3>', unsafe_allow_html=True)
1248
 
1249
  st.markdown('<h5 style="text-align:center; margin-top: 0;">Tyre Alarms Distribution by Location</h5>', unsafe_allow_html=True)
1250
 
@@ -1261,7 +1103,65 @@ else:
1261
  width='100%',
1262
  height='520px'
1263
  )
1264
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1265
  for _, r in valid_gps.iterrows():
1266
  color = '#D32F2F' if r['Alarm Status'] == 'Red High Pressure' else '#2E7D32'
1267
  radius = 6 + (r['Temperature (°C)'] - valid_gps['Temperature (°C)'].min()) / (valid_gps['Temperature (°C)'].max() - valid_gps['Temperature (°C)'].min() + 1e-5) * 12
@@ -1285,7 +1185,7 @@ else:
1285
  weight=1,
1286
  popup=folium.Popup(popup_html, max_width=250)
1287
  ).add_to(m)
1288
-
1289
  # Legend
1290
  legend_html = '''
1291
  <div style="
@@ -1303,17 +1203,14 @@ else:
1303
  <b>Legend</b><br>
1304
  <span style="color:#2E7D32">●</span> Normal (No Alarm)<br>
1305
  <span style="color:#D32F2F">●</span> Red Pressure<br>
1306
- <span style="color:#1976D2">▲</span> Front Tyre<br>
1307
- <span style="color:#1976D2">★</span> Rear Tyre<br>
1308
  <i>Size ∝ Temperature</i>
1309
  </div>
1310
  '''
1311
  m.get_root().html.add_child(folium.Element(legend_html))
1312
-
1313
  st_folium(m, width='100%', height=520, returned_objects=[])
1314
 
1315
- # Insight 4
1316
- # Analisis data untuk menentukan pola spasial
1317
  if not valid_gps.empty:
1318
  # Hitung jumlah alarm per zona
1319
  zone_counts = valid_gps[valid_gps['is_alarm'] == 1]['Zone'].value_counts()
@@ -1335,15 +1232,13 @@ if not valid_gps.empty:
1335
  else:
1336
  front_percentage = 0
1337
 
1338
- insight_text = f"""
1339
- Alarm concentration is highest in {top_zone}, with {top_zone_count} alarms representing {percentage:.1f}% of total alarms.
1340
- Front tyres account for {front_percentage:.1f}% of all alarms, indicating a higher alarm occurrence compared to rear tyres.
1341
- GNSS data confirms alarm clustering within specific operational zones. Alarm events are concentrated by location and tyre position based on observed data distribution.
1342
- """
1343
  else:
1344
  insight_text = """
1345
- No valid GNSS data available for analysis.
1346
- """
1347
 
1348
  st.markdown(f"""
1349
  <div class="insight-box">
@@ -1352,19 +1247,17 @@ st.markdown(f"""
1352
  </div>
1353
  </div>
1354
  """, unsafe_allow_html=True)
 
1355
  # ================= OBJECTIVE 5 =================
1356
- # ================= OBJECTIVE 6 =================
1357
- st.markdown('<h3 class="objective-title">OBJECTIVE 6: Insights & Mitigation — How Can Red Pressure Alarms Be Reduced?</h3>', unsafe_allow_html=True)
1358
 
1359
  # --- DATA PREP ---
1360
  front_pressure_avg = dff[dff['Position'].isin([1, 2])]['Pressure (psi)'].mean()
1361
  front_temp_avg = dff[dff['Position'].isin([1, 2])]['Temperature (°C)'].mean()
1362
-
1363
  hourly_counts = dff[dff['is_alarm'] == 1]['hour'].value_counts().reindex(range(24), fill_value=0)
1364
  dominant_hour = hourly_counts.idxmax() if len(hourly_counts) > 0 else "N/A"
1365
  total_alarms = hourly_counts.sum()
1366
  dominant_percentage = (hourly_counts[dominant_hour] / total_alarms) * 100 if total_alarms > 0 else 0
1367
-
1368
  zone_counts = dff[dff['is_alarm'] == 1]['Zone'].value_counts()
1369
  top_zone = zone_counts.index[0] if not zone_counts.empty else "N/A"
1370
  top_zone_percentage = (zone_counts.iloc[0] / total_alarms) * 100 if total_alarms > 0 else 0
@@ -1383,90 +1276,141 @@ if not rear_df.empty and len(rear_df[['Speed (km/h)']].dropna()) > 1 and len(rea
1383
  else:
1384
  corr_rear = 0
1385
 
1386
- # Insight
1387
- insight_text = f"""1. Front tyres (Pos 1 & 2) show average pressure of {front_pressure_avg:.1f} psi and temperature of {front_temp_avg:.1f}°C, indicating potential over-inflation or insufficient load distribution.
1388
- <br>
1389
- 2. Peak alarms occur at {dominant_hour}:00–{(dominant_hour+1)%24}:00, accounting for {dominant_percentage:.1f}% of total alarms, primarily in {top_zone}.
1390
- <br>
1391
- 3. Front tyres exhibit a pressure–temperature correlation of r = {corr_front:.2f}, while rear tyres show r = {corr_rear:.2f}, indicating higher operational stress on front tyres.
1392
- <br>
1393
- 4. {top_zone} contains {top_zone_percentage:.1f}% of all alarms, confirmed as a high-risk hotspot through GNSS data."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1394
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1395
 
1396
  try:
1397
  import requests
1398
  import json
1399
-
1400
- API_URL = "https://api-inference.huggingface.co/models/HuggingFaceH4/zephyr-7b-beta "
1401
-
1402
  prompt = f"""
1403
  Role: Fleet Operations Risk Analyst
1404
-
1405
  Insights:
1406
- - High-risk zone: {top_zone} ({top_zone_percentage:.1f}% of alarms)
1407
- - Front tyres: 62% of total alarms
1408
  - Peak alarm hour: {dominant_hour}:00 ({dominant_percentage:.1f}%)
1409
- - Front tyre pressure–temperature correlation r = {corr_front:.2f}
1410
-
 
1411
  Task:
1412
  Generate:
1413
  1. Business Recommendations
1414
  2. Risk Mitigation Actions
1415
-
1416
  Rules:
1417
  - Use only provided insights
1418
  - No root-cause speculation
1419
  - Business-ready language
1420
  """
1421
-
1422
  payload = {
1423
  "inputs": prompt,
1424
  "parameters": {
1425
- "max_new_tokens": 25000,
1426
  "temperature": 0.8,
1427
  "top_p": 0.9
1428
  }
1429
  }
1430
-
1431
  response = requests.post(API_URL, json=payload)
1432
  generated_text = response.json()[0]["generated_text"]
1433
-
1434
  recommendation_text = generated_text
1435
  risk_mitigation_text = generated_text
1436
-
1437
  # Jika response kosong, gunakan versi manual
1438
  if recommendation_text == "":
1439
  recommendation_text = f"""1. Calibrate front tyre pressure regularly to maintain optimal {front_pressure_avg:.1f} psi and prevent over-inflation.
1440
  <br>
1441
- 2. Implement operational restrictions during peak hours ({dominant_hour}:00–{(dominant_hour+1)%24}:00) in {top_zone} to reduce alarm frequency.
1442
  <br>
1443
- 3. Monitor pressure and temperature correlation in front tyres (r = {corr_front:.2f}) to prevent overheating and premature wear.
1444
  <br>
1445
- 4. Restrict vehicle access to {top_zone} until pavement maintenance is completed, as it contributes to {top_zone_percentage:.1f}% of alarms."""
1446
  if risk_mitigation_text == "":
1447
  risk_mitigation_text = f"""1. Adjust front tyre load distribution to reduce {front_temp_avg:.1f}°C temperature and prevent overheating.
1448
  <br>
1449
  2. Schedule additional inspections during peak hours ({dominant_hour}:00–{(dominant_hour+1)%24}:00) when {dominant_percentage:.1f}% of alarms occur.
1450
  <br>
1451
- 3. Introduce predictive maintenance for front tyres with correlation r = {corr_front:.2f} to prevent unplanned downtime.
1452
  <br>
1453
- 4. Implement real-time monitoring in {top_zone} where {top_zone_percentage:.1f}% of alarms are concentrated."""
1454
  except:
1455
  # Jika response dari model kosong atau gagal, gunakan versi manual
1456
  recommendation_text = f"""1. Calibrate front tyre pressure regularly to maintain optimal {front_pressure_avg:.1f} psi and prevent over-inflation.
1457
  <br>
1458
- 2. Implement operational restrictions during peak hours ({dominant_hour}:00–{(dominant_hour+1)%24}:00) in {top_zone} to reduce alarm frequency.
1459
  <br>
1460
- 3. Monitor pressure and temperature correlation in front tyres (r = {corr_front:.2f}) to prevent overheating and premature wear.
1461
  <br>
1462
- 4. Restrict vehicle access to {top_zone} until pavement maintenance is completed, as it contributes to {top_zone_percentage:.1f}% of alarms."""
1463
  # Risk Mitigation
1464
  risk_mitigation_text = f"""1. Adjust front tyre load distribution to reduce {front_temp_avg:.1f}°C temperature and prevent overheating.
1465
  <br>
1466
  2. Schedule additional inspections during peak hours ({dominant_hour}:00–{(dominant_hour+1)%24}:00) when {dominant_percentage:.1f}% of alarms occur.
1467
  <br>
1468
- 3. Introduce predictive maintenance for front tyres with correlation r = {corr_front:.2f} to prevent unplanned downtime.
1469
- 4. Implement real-time monitoring in {top_zone} where {top_zone_percentage:.1f}% of alarms are concentrated."""
1470
 
1471
  # ============== SUBHEADER + BOX 1: INSIGHT ==============
1472
  st.markdown('<h4 style="text-align:center; margin:10px 0 5px 0; font-weight:bold;">INSIGHT</h4>', unsafe_allow_html=True)
 
248
  st.markdown("""
249
  <div class="main-header" style="text-align:center;">
250
  <h1>Michelin Mining Tyre Analytics</h1>
251
+ <p style="font-size:12px; color:#9a9a9a; margin-top:-6px;">
252
+ Insight daily berdasarkan 13–16 Desember 2023
253
  </p>
254
  </div>
255
  """, unsafe_allow_html=True)
256
 
257
 
258
+
259
  # ================= LOGO (Perbaikan: Base64 Embed - Selalu Muncul) =================
260
  def render_logo():
261
  # Logo dalam base64 (diambil dari URL Anda)
 
345
  else:
346
  dff = df
347
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
348
  dff = dff.copy()
349
  dff['Position'] = pd.Categorical(dff['Position'], categories=[1, 2, 3, 4], ordered=True)
350
 
 
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()
 
491
  ticktext = ["03:00", "18:00/06:00", "21:00", "00:00"]
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(
506
  r=hourly_amber.values,
507
  theta=theta,
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(
515
  r=hourly_red.values,
516
  theta=theta,
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
+
524
  fig.update_layout(
525
  polar=dict(
526
  angularaxis=dict(
 
550
  st.markdown('<div style="text-align:center; font-weight:bold; margin-bottom: 8px;">Position 1 (06:00–18:00)</div>', unsafe_allow_html=True)
551
  pos1_data = alarm_data[alarm_data['Position'] == 1].copy()
552
  pos1_data = pos1_data[pos1_data['hour'].between(6, 17, inclusive='both')]
553
+ fig1 = create_radial_chart(pos1_data, "Position 1 (06:00–18:00)", list(range(6, 18)), 'pagi')
554
  if fig1 is not None:
555
  st.plotly_chart(fig1, use_container_width=True)
556
  else:
 
561
  st.markdown('<div style="text-align:center; font-weight:bold; margin-bottom: 8px;">Position 1 (18:00–06:00)</div>', unsafe_allow_html=True)
562
  pos1_data = alarm_data[alarm_data['Position'] == 1].copy()
563
  pos1_data = pos1_data[~pos1_data['hour'].between(6, 17, inclusive='both')]
564
+ fig2 = create_radial_chart(pos1_data, "Position 1 (18:00–06:00)", list(range(18, 24)) + list(range(0, 6)), 'sore')
565
  if fig2 is not None:
566
  st.plotly_chart(fig2, use_container_width=True)
567
  else:
 
572
  st.markdown('<div style="text-align:center; font-weight:bold; margin-bottom: 8px;">Position 2 (06:00–18:00)</div>', unsafe_allow_html=True)
573
  pos2_data = alarm_data[alarm_data['Position'] == 2].copy()
574
  pos2_data = pos2_data[pos2_data['hour'].between(6, 17, inclusive='both')]
575
+ fig3 = create_radial_chart(pos2_data, "Position 2 (06:00–18:00)", list(range(6, 18)), 'pagi')
576
  if fig3 is not None:
577
  st.plotly_chart(fig3, use_container_width=True)
578
  else:
 
583
  st.markdown('<div style="text-align:center; font-weight:bold; margin-bottom: 8px;">Position 2 (18:00–06:00)</div>', unsafe_allow_html=True)
584
  pos2_data = alarm_data[alarm_data['Position'] == 2].copy()
585
  pos2_data = pos2_data[~pos2_data['hour'].between(6, 17, inclusive='both')]
586
+ fig4 = create_radial_chart(pos2_data, "Position 2 (18:00–06:00)", list(range(18, 24)) + list(range(0, 6)), 'sore')
587
  if fig4 is not None:
588
  st.plotly_chart(fig4, use_container_width=True)
589
  else:
 
595
  st.markdown('<div style="text-align:center; font-weight:bold; margin-bottom: 8px;">Position 3 (06:00–18:00)</div>', unsafe_allow_html=True)
596
  pos3_data = alarm_data[alarm_data['Position'] == 3].copy()
597
  pos3_data = pos3_data[pos3_data['hour'].between(6, 17, inclusive='both')]
598
+ fig5 = create_radial_chart(pos3_data, "Position 3 (06:00–18:00)", list(range(6, 18)), 'pagi')
599
  if fig5 is not None:
600
  st.plotly_chart(fig5, use_container_width=True)
601
  else:
 
606
  st.markdown('<div style="text-align:center; font-weight:bold; margin-bottom: 8px;">Position 3 (18:00–06:00)</div>', unsafe_allow_html=True)
607
  pos3_data = alarm_data[alarm_data['Position'] == 3].copy()
608
  pos3_data = pos3_data[~pos3_data['hour'].between(6, 17, inclusive='both')]
609
+ fig6 = create_radial_chart(pos3_data, "Position 3 (18:00–06:00)", list(range(18, 24)) + list(range(0, 6)), 'sore')
610
  if fig6 is not None:
611
  st.plotly_chart(fig6, use_container_width=True)
612
  else:
 
617
  st.markdown('<div style="text-align:center; font-weight:bold; margin-bottom: 8px;">Position 4 (06:00–18:00)</div>', unsafe_allow_html=True)
618
  pos4_data = alarm_data[alarm_data['Position'] == 4].copy()
619
  pos4_data = pos4_data[pos4_data['hour'].between(6, 17, inclusive='both')]
620
+ fig7 = create_radial_chart(pos4_data, "Position 4 (06:00–18:00)", list(range(6, 18)), 'pagi')
621
  if fig7 is not None:
622
  st.plotly_chart(fig7, use_container_width=True)
623
  else:
 
628
  st.markdown('<div style="text-align:center; font-weight:bold; margin-bottom: 8px;">Position 4 (18:00–06:00)</div>', unsafe_allow_html=True)
629
  pos4_data = alarm_data[alarm_data['Position'] == 4].copy()
630
  pos4_data = pos4_data[~pos4_data['hour'].between(6, 17, inclusive='both')]
631
+ fig8 = create_radial_chart(pos4_data, "Position 4 (18:00–06:00)", list(range(18, 24)) + list(range(0, 6)), 'sore')
632
  if fig8 is not None:
633
  st.plotly_chart(fig8, use_container_width=True)
634
  else:
 
663
  red_alarms = alarm_data[alarm_data['Alarm Status'].str.contains('Red', na=False)].shape[0]
664
  amber_alarms = alarm_data[alarm_data['Alarm Status'].str.contains('Amber', na=False)].shape[0]
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)
674
+ pos1_pagi = alarm_data[(alarm_data['Position'] == 1) & (alarm_data['hour'].between(6, 17, inclusive='both'))]
675
+ if not pos1_pagi.empty:
676
+ pos1_pagi_total = pos1_pagi.groupby('hour').size()
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'))]
684
+ if not pos1_sore.empty:
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'))]
692
+ if not pos3_pagi.empty:
693
+ pos3_pagi_total = pos3_pagi.groupby('hour').size()
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'))]
701
+ if not pos3_sore.empty:
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'))]
709
+ if not pos4_pagi.empty:
710
+ pos4_pagi_total = pos4_pagi.groupby('hour').size()
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'))]
718
+ if not pos4_sore.empty:
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
 
726
  # =============== DISPLAY INSIGHT ===============
 
731
  </div>
732
  </div>
733
  """, unsafe_allow_html=True)
734
+
735
  # ================= OBJECTIVE 3 =================
736
  st.markdown('<h3 class="objective-title">OBJECTIVE 3: Correlation — How Does Heat Influence Pressure and Which Tyres Trigger Red Alarms?</h3>', unsafe_allow_html=True)
737
 
 
1058
  return np.corrcoef(a[mask], b[mask])[0, 1]
1059
 
1060
  corr_p_t_front = safe_corr(front_df['Temperature (°C)'], front_df['Pressure (psi)'])
 
1061
  corr_p_t_rear = safe_corr(rear_df['Temperature (°C)'], rear_df['Pressure (psi)'])
 
1062
 
1063
+ # Hitung jumlah alarm red saat suhu >= 52°C di front tyre
1064
+ high_temp_front = front_df[front_df['Temperature (°C)'] >= 52]
1065
+ red_high_pressure_count = high_temp_front[high_temp_front['Alarm Status'] == 'Red High Pressure'].shape[0]
1066
+
1067
+ # Hitung korelasi Pressure vs T/V (Temperature / Speed)
1068
+ if not front_df.empty and (front_df['Speed (km/h)'] > 0).any():
1069
+ front_df_filtered = front_df[front_df['Speed (km/h)'] > 0].copy()
1070
+ front_df_filtered['Temp_Speed_Ratio'] = front_df_filtered['Temperature (°C)'] / front_df_filtered['Speed (km/h)']
1071
+ corr_p_tv_front = safe_corr(front_df_filtered['Pressure (psi)'], front_df_filtered['Temp_Speed_Ratio'])
1072
+ else:
1073
+ corr_p_tv_front = 0.0
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"""
 
1085
  </div>
1086
  """, unsafe_allow_html=True)
1087
 
1088
+ # ================= OBJECTIVE 4 =================
1089
+ st.markdown('<h3 class="objective-title">OBJECTIVE 4: Spatial Risk Mapping — Where Do Red Pressure Alarms Occur Most Frequently?</h3>', unsafe_allow_html=True)
1090
 
1091
  st.markdown('<h5 style="text-align:center; margin-top: 0;">Tyre Alarms Distribution by Location</h5>', unsafe_allow_html=True)
1092
 
 
1103
  width='100%',
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
 
1185
  weight=1,
1186
  popup=folium.Popup(popup_html, max_width=250)
1187
  ).add_to(m)
1188
+
1189
  # Legend
1190
  legend_html = '''
1191
  <div style="
 
1203
  <b>Legend</b><br>
1204
  <span style="color:#2E7D32">●</span> Normal (No Alarm)<br>
1205
  <span style="color:#D32F2F">●</span> Red Pressure<br>
 
 
1206
  <i>Size ∝ Temperature</i>
1207
  </div>
1208
  '''
1209
  m.get_root().html.add_child(folium.Element(legend_html))
1210
+
1211
  st_folium(m, width='100%', height=520, returned_objects=[])
1212
 
1213
+ # =============== INSIGHT 4 ===============
 
1214
  if not valid_gps.empty:
1215
  # Hitung jumlah alarm per zona
1216
  zone_counts = valid_gps[valid_gps['is_alarm'] == 1]['Zone'].value_counts()
 
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:
1239
  insight_text = """
1240
+ No valid GNSS data available for analysis.
1241
+ """
1242
 
1243
  st.markdown(f"""
1244
  <div class="insight-box">
 
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
 
1254
  # --- DATA PREP ---
1255
  front_pressure_avg = dff[dff['Position'].isin([1, 2])]['Pressure (psi)'].mean()
1256
  front_temp_avg = dff[dff['Position'].isin([1, 2])]['Temperature (°C)'].mean()
 
1257
  hourly_counts = dff[dff['is_alarm'] == 1]['hour'].value_counts().reindex(range(24), fill_value=0)
1258
  dominant_hour = hourly_counts.idxmax() if len(hourly_counts) > 0 else "N/A"
1259
  total_alarms = hourly_counts.sum()
1260
  dominant_percentage = (hourly_counts[dominant_hour] / total_alarms) * 100 if total_alarms > 0 else 0
 
1261
  zone_counts = dff[dff['is_alarm'] == 1]['Zone'].value_counts()
1262
  top_zone = zone_counts.index[0] if not zone_counts.empty else "N/A"
1263
  top_zone_percentage = (zone_counts.iloc[0] / total_alarms) * 100 if total_alarms > 0 else 0
 
1276
  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.
1333
+ • Position 1 (18:00–06:00): Red alarms account for {(len(pos1_sore[pos1_sore['Alarm Status'].str.contains('Red', na=False)]) / len(pos1_sore)) * 100:.1f}% of total alarms.
1334
+ • Position 3 (06:00–18:00): Dominant alarm at {dominant_hour}:00 with {hourly_counts[dominant_hour]} alarms.
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
+
1342
+ 4. **Spatial Risk Mapping (Objective 4):**
1343
+ • Alarm concentration is highest in {top_zone_obj4}, with {top_zone_count_obj4} alarms representing {percentage_obj4:.1f}% of total alarms.
1344
+ • Front tyres account for {front_percentage_obj4:.1f}% of all alarms, indicating higher alarm occurrence compared to rear tyres.
1345
+ """
1346
 
1347
  try:
1348
  import requests
1349
  import json
1350
+ API_URL = "https://api-inference.huggingface.co/models/HuggingFaceH4/zephyr-7b-beta"
 
 
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
1363
  2. Risk Mitigation Actions
 
1364
  Rules:
1365
  - Use only provided insights
1366
  - No root-cause speculation
1367
  - Business-ready language
1368
  """
 
1369
  payload = {
1370
  "inputs": prompt,
1371
  "parameters": {
1372
+ "max_new_tokens": 250,
1373
  "temperature": 0.8,
1374
  "top_p": 0.9
1375
  }
1376
  }
 
1377
  response = requests.post(API_URL, json=payload)
1378
  generated_text = response.json()[0]["generated_text"]
 
1379
  recommendation_text = generated_text
1380
  risk_mitigation_text = generated_text
 
1381
  # Jika response kosong, gunakan versi manual
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>
1393
  2. Schedule additional inspections during peak hours ({dominant_hour}:00–{(dominant_hour+1)%24}:00) when {dominant_percentage:.1f}% of alarms occur.
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)