SHELLAPANDIANGANHUNGING commited on
Commit
f1b9ca7
Β·
verified Β·
1 Parent(s): 116b3c3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +266 -317
app.py CHANGED
@@ -490,345 +490,293 @@ st.markdown(f"""
490
  </div>
491
  """, unsafe_allow_html=True)
492
 
493
- #### OBJECTICVE 2
494
- 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)
495
-
496
- # Prepare data
497
- front_df = dff[dff['Position'].isin([1, 2])].copy()
498
- rear_df = dff[dff['Position'].isin([3, 4])].copy()
499
-
500
- col1, col2 = st.columns(2)
501
-
502
- # =============== COL 1: Front β€” Temperature β†’ Pressure (Scatter + Regression Area) ===============
503
- with col1:
504
- st.markdown('<h5 style="text-align:center; margin-top: 0;">Front Tyres: Temperature β†’ Pressure</h5>', unsafe_allow_html=True)
505
-
506
- if not front_df.empty:
507
- # Tambahkan kategori alarm status
508
- front_df['Category'] = front_df.apply(
509
- lambda row: f"Normal Front Tyre" if row['Alarm Status'] == 'No Alarm'
510
- else f"Amber Pressure Front Tyre" if 'Amber' in row['Alarm Status']
511
- else f"Red Pressure Front Tyre", axis=1
512
- )
513
- categories = ["Normal Front Tyre", "Amber Pressure Front Tyre", "Red Pressure Front Tyre"]
514
- front_df['Category'] = pd.Categorical(front_df['Category'], categories=categories, ordered=True)
515
 
516
- # Filter valid data
517
- valid_data = front_df.dropna(subset=['Temperature (Β°C)', 'Pressure (psi)'])
518
- if len(valid_data) > 1:
519
- X = valid_data[['Temperature (Β°C)']]
520
- y = valid_data['Pressure (psi)']
521
- model = LinearRegression().fit(X, y)
522
- x_line = np.linspace(X.min(), X.max(), 100).reshape(-1, 1)
523
- y_line = model.predict(x_line)
524
- corr = np.corrcoef(valid_data['Temperature (Β°C)'], valid_data['Pressure (psi)'])[0, 1]
525
-
526
- fig1 = px.scatter(
527
- valid_data,
528
- x='Temperature (Β°C)',
529
- y='Pressure (psi)',
530
- color='Category',
531
- color_discrete_map={
532
- "Normal Front Tyre": "#2E7D32", # Hijau
533
- "Amber Pressure Front Tyre": "#FFC107", # Kuning
534
- "Red Pressure Front Tyre": "#D32F2F" # Merah
535
- },
536
- category_orders={'Category': categories},
537
- template="plotly_white",
538
- labels={'Temperature (Β°C)': 'Temperature (Β°C)', 'Pressure (psi)': 'Pressure (psi)'}
539
- )
540
 
541
- fig1.update_traces(
542
- hovertemplate="<b>%{marker.color}</b><br>Temp: %{x:.1f}Β°C<br>Pressure: %{y:.1f} psi<extra></extra>",
543
- marker=dict(size=6)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
544
  )
 
 
 
 
 
 
 
 
545
 
546
- fig1.add_trace(go.Scatter(
547
- x=x_line.flatten(), y=y_line,
548
- mode='lines', name='Trend Line',
549
- line=dict(color='#1976D2', dash='dot', width=2)
550
- ))
551
-
552
- # Tambahkan area confidence interval (soft background)
553
- y_pred = model.predict(X)
554
- residuals = y - y_pred
555
- mse = np.mean(residuals**2)
556
- std_error = np.sqrt(mse)
557
- y_upper = y_line + 1.96 * std_error
558
- y_lower = y_line - 1.96 * std_error
559
-
560
- fig1.add_trace(go.Scatter(
561
- x=np.concatenate([x_line.flatten(), x_line.flatten()[::-1]]),
562
- y=np.concatenate([y_upper, y_lower[::-1]]),
563
- fill='toself',
564
- fillcolor='rgba(211, 47, 47, 0.1)', # Merah transparan
565
- line=dict(color='rgba(255,255,255,0)'),
566
- showlegend=False,
567
- name='Confidence Interval'
568
- ))
569
-
570
- fig1.update_layout(
571
- margin=dict(t=40),
572
- annotations=[
573
- dict(
574
- x=0.95, y=0.95,
575
- xref="paper", yref="paper",
576
- text=f"r = {corr:.2f}",
577
- showarrow=False,
578
- bgcolor="white",
579
- bordercolor="black",
580
- borderwidth=1,
581
- font=dict(color="black")
582
- )
583
- ],
584
- legend=dict(
585
- title_text='Tyre Status',
586
- bgcolor="white",
587
- bordercolor="lightgray",
588
- borderwidth=1,
589
- itemclick=False,
590
- itemdoubleclick=False
591
- )
592
- )
593
- st.plotly_chart(fig1, use_container_width=True)
594
- else:
595
- st.warning("Insufficient data for front tyres.")
596
  else:
597
- st.warning("No front tyre data.")
598
 
599
- # =============== COL 2: Front β€” Pressure vs (Temperature / Speed) ===============
600
  with col2:
601
- st.markdown('<h5 style="text-align:center; margin-top: 0;">Front Tyres: Pressure vs (Temperature / Speed)</h5>', unsafe_allow_html=True)
602
-
603
- if not front_df.empty:
604
- # Filter kecepatan > 0 untuk hindari pembagian dengan nol
605
- front_df = front_df[front_df['Speed (km/h)'] > 0]
606
- front_df['Temp_Speed_Ratio'] = front_df['Temperature (Β°C)'] / front_df['Speed (km/h)']
607
-
608
- # Tambahkan kategori alarm status
609
- front_df['Category'] = front_df.apply(
610
- lambda row: f"Normal Front Tyre" if row['Alarm Status'] == 'No Alarm'
611
- else f"Amber Pressure Front Tyre" if 'Amber' in row['Alarm Status']
612
- else f"Red Pressure Front Tyre", axis=1
613
- )
614
- categories = ["Normal Front Tyre", "Amber Pressure Front Tyre", "Red Pressure Front Tyre"]
615
- front_df['Category'] = pd.Categorical(front_df['Category'], categories=categories, ordered=True)
616
-
617
- valid_data = front_df.dropna(subset=['Temp_Speed_Ratio', 'Pressure (psi)'])
618
- if not valid_data.empty:
619
- fig2 = px.scatter(
620
- valid_data,
621
- x='Temp_Speed_Ratio',
622
- y='Pressure (psi)',
623
- color='Category',
624
- color_discrete_map={
625
- "Normal Front Tyre": "#2E7D32", # Hijau
626
- "Amber Pressure Front Tyre": "#FFC107", # Kuning
627
- "Red Pressure Front Tyre": "#D32F2F" # Merah
628
- },
629
- category_orders={'Category': categories},
630
- template="plotly_white",
631
- labels={'Temp_Speed_Ratio': 'Temperature / Speed', 'Pressure (psi)': 'Pressure (psi)'}
632
- )
633
-
634
- fig2.update_traces(
635
- hovertemplate="<b>%{marker.color}</b><br>T/S: %{x:.2f}<br>Pressure: %{y:.1f} psi<extra></extra>",
636
- marker=dict(size=6)
637
- )
638
-
639
- fig2.update_layout(
640
- margin=dict(t=40),
641
- legend=dict(
642
- title_text='Tyre Status',
643
- bgcolor="white",
644
- bordercolor="lightgray",
645
- borderwidth=1,
646
- itemclick=False,
647
- itemdoubleclick=False
648
- )
649
- )
650
- st.plotly_chart(fig2, use_container_width=True)
651
- else:
652
- st.warning("Insufficient data for front tyres.")
653
  else:
654
- st.warning("No front tyre data.")
655
-
656
- # =============== COL 3: Rear β€” Temperature β†’ Pressure (Scatter + Regression Area) ===============
657
- col3, col4 = st.columns(2)
658
 
659
  with col3:
660
- st.markdown('<h5 style="text-align:center; margin-top: 0;">Rear Tyres: Temperature β†’ Pressure</h5>', unsafe_allow_html=True)
661
-
662
- if not rear_df.empty:
663
- rear_df['Category'] = rear_df.apply(
664
- lambda row: f"Normal Rear Tyre" if row['Alarm Status'] == 'No Alarm'
665
- else f"Amber Pressure Rear Tyre" if 'Amber' in row['Alarm Status']
666
- else f"Red Pressure Rear Tyre", axis=1
667
- )
668
- categories = ["Normal Rear Tyre", "Amber Pressure Rear Tyre", "Red Pressure Rear Tyre"]
669
- rear_df['Category'] = pd.Categorical(rear_df['Category'], categories=categories, ordered=True)
670
-
671
- valid_data = rear_df.dropna(subset=['Temperature (Β°C)', 'Pressure (psi)'])
672
- if len(valid_data) > 1:
673
- X = valid_data[['Temperature (Β°C)']]
674
- y = valid_data['Pressure (psi)']
675
- model = LinearRegression().fit(X, y)
676
- x_line = np.linspace(X.min(), X.max(), 100).reshape(-1, 1)
677
- y_line = model.predict(x_line)
678
- corr = np.corrcoef(valid_data['Temperature (Β°C)'], valid_data['Pressure (psi)'])[0, 1]
679
-
680
- fig3 = px.scatter(
681
- valid_data,
682
- x='Temperature (Β°C)',
683
- y='Pressure (psi)',
684
- color='Category',
685
- color_discrete_map={
686
- "Normal Rear Tyre": "#2E7D32",
687
- "Amber Pressure Rear Tyre": "#FFC107",
688
- "Red Pressure Rear Tyre": "#D32F2F"
689
- },
690
- category_orders={'Category': categories},
691
- template="plotly_white"
692
- )
693
-
694
- fig3.update_traces(
695
- hovertemplate="<b>%{marker.color}</b><br>Temp: %{x:.1f}Β°C<br>Pressure: %{y:.1f} psi<extra></extra>",
696
- marker=dict(size=6)
697
- )
698
-
699
- fig3.add_trace(go.Scatter(
700
- x=x_line.flatten(), y=y_line,
701
- mode='lines', name='Trend Line',
702
- line=dict(color='#1976D2', dash='dot', width=2)
703
- ))
704
-
705
- # Tambahkan area confidence interval (soft background)
706
- y_pred = model.predict(X)
707
- residuals = y - y_pred
708
- mse = np.mean(residuals**2)
709
- std_error = np.sqrt(mse)
710
- y_upper = y_line + 1.96 * std_error
711
- y_lower = y_line - 1.96 * std_error
712
-
713
- fig3.add_trace(go.Scatter(
714
- x=np.concatenate([x_line.flatten(), x_line.flatten()[::-1]]),
715
- y=np.concatenate([y_upper, y_lower[::-1]]),
716
- fill='toself',
717
- fillcolor='rgba(211, 47, 47, 0.1)', # Merah transparan
718
- line=dict(color='rgba(255,255,255,0)'),
719
- showlegend=False,
720
- name='Confidence Interval'
721
- ))
722
-
723
- fig3.update_layout(
724
- margin=dict(t=40),
725
- annotations=[
726
- dict(
727
- x=0.95, y=0.95,
728
- xref="paper", yref="paper",
729
- text=f"r = {corr:.2f}",
730
- showarrow=False,
731
- bgcolor="white",
732
- bordercolor="black",
733
- borderwidth=1,
734
- font=dict(color="black")
735
- )
736
- ],
737
- legend=dict(
738
- title_text='Tyre Status',
739
- bgcolor="white",
740
- bordercolor="lightgray",
741
- borderwidth=1,
742
- itemclick=False,
743
- itemdoubleclick=False
744
- )
745
- )
746
- st.plotly_chart(fig3, use_container_width=True)
747
- else:
748
- st.warning("Insufficient data for rear tyres.")
749
  else:
750
- st.warning("No rear tyre data.")
751
 
752
- # =============== COL 4: Rear β€” Pressure vs (Temperature / Speed) ===============
753
  with col4:
754
- st.markdown('<h5 style="text-align:center; margin-top: 0;">Rear Tyres: Pressure vs (Temperature / Speed)</h5>', unsafe_allow_html=True)
755
-
756
- if not rear_df.empty:
757
- # Filter kecepatan > 0 untuk hindari pembagian dengan nol
758
- rear_df = rear_df[rear_df['Speed (km/h)'] > 0]
759
- rear_df['Temp_Speed_Ratio'] = rear_df['Temperature (Β°C)'] / rear_df['Speed (km/h)']
760
-
761
- # Tambahkan kategori alarm status
762
- rear_df['Category'] = rear_df.apply(
763
- lambda row: f"Normal Rear Tyre" if row['Alarm Status'] == 'No Alarm'
764
- else f"Amber Pressure Rear Tyre" if 'Amber' in row['Alarm Status']
765
- else f"Red Pressure Rear Tyre", axis=1
766
- )
767
- categories = ["Normal Rear Tyre", "Amber Pressure Rear Tyre", "Red Pressure Rear Tyre"]
768
- rear_df['Category'] = pd.Categorical(rear_df['Category'], categories=categories, ordered=True)
769
-
770
- valid_data = rear_df.dropna(subset=['Temp_Speed_Ratio', 'Pressure (psi)'])
771
- if not valid_data.empty:
772
- fig4 = px.scatter(
773
- valid_data,
774
- x='Temp_Speed_Ratio',
775
- y='Pressure (psi)',
776
- color='Category',
777
- color_discrete_map={
778
- "Normal Rear Tyre": "#2E7D32",
779
- "Amber Pressure Rear Tyre": "#FFC107",
780
- "Red Pressure Rear Tyre": "#D32F2F"
781
- },
782
- category_orders={'Category': categories},
783
- template="plotly_white"
784
- )
785
-
786
- fig4.update_traces(
787
- hovertemplate="<b>%{marker.color}</b><br>T/S: %{x:.2f}<br>Pressure: %{y:.1f} psi<extra></extra>",
788
- marker=dict(size=6)
789
- )
790
-
791
- fig4.update_layout(
792
- margin=dict(t=40),
793
- legend=dict(
794
- title_text='Tyre Status',
795
- bgcolor="white",
796
- bordercolor="lightgray",
797
- borderwidth=1,
798
- itemclick=False,
799
- itemdoubleclick=False
800
- )
801
- )
802
- st.plotly_chart(fig4, use_container_width=True)
803
- else:
804
- st.warning("Insufficient data for rear tyres.")
805
  else:
806
- st.warning("No rear tyre data.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
807
 
808
  # =============== INSIGHT 3 ===============
809
- def safe_corr(a, b):
810
- mask = ~(np.isnan(a) | np.isnan(b))
811
- if mask.sum() < 2:
812
- return 0.0
813
- return np.corrcoef(a[mask], b[mask])[0, 1]
814
-
815
- corr_p_t_front = safe_corr(front_df['Temperature (Β°C)'], front_df['Pressure (psi)'])
816
- corr_t_s_front = safe_corr(front_df['Temperature (Β°C)'], front_df['Speed (km/h)'])
817
- corr_p_t_rear = safe_corr(rear_df['Temperature (Β°C)'], rear_df['Pressure (psi)'])
818
- corr_t_s_rear = safe_corr(rear_df['Temperature (Β°C)'], rear_df['Speed (km/h)'])
819
-
820
- insight_text = f"""
821
- 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.
822
- """
823
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
824
  st.markdown(f"""
825
  <div class="insight-box">
826
  <div class="content">
827
- {insight_text.strip()}
828
  </div>
829
  </div>
830
  """, unsafe_allow_html=True)
831
- ####OBJECTIVE 3
832
  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)
833
 
834
  # Prepare data
@@ -1167,6 +1115,7 @@ st.markdown(f"""
1167
  </div>
1168
  """, unsafe_allow_html=True)
1169
 
 
1170
  # ================= OBJECTIVE 4 =================
1171
  st.markdown('<h3 class="objective-title">OBJECTIVE 4: Spatial Risk Mapping β€” Where Do Red Pressure Alarms Occur Most Frequently?</h3>', unsafe_allow_html=True)
1172
 
 
490
  </div>
491
  """, unsafe_allow_html=True)
492
 
493
+ ####obejctic=ve 2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
494
 
495
+ st.markdown("""
496
+ <h3 class="objective-title">OBJECTIVE 2: Shift and Tyre Position - How Are Alarms Concentrated Across Shifts and Tyres?</h3>
497
+ """, unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
498
 
499
+ # Filter semua data (termasuk alarm normal)
500
+ alarm_data = dff.copy()
501
+
502
+ # Buat 2 baris Γ— 4 kolom
503
+ col1, col2, col3, col4 = st.columns(4)
504
+ col5, col6, col7, col8 = st.columns(4)
505
+
506
+ # Fungsi helper untuk membuat radial chart per posisi dan shift
507
+ def create_radial_chart(pos_data, title, shift_hours, shift_type):
508
+ if pos_data.empty:
509
+ return None
510
+
511
+ # Kelompokkan jam dan status
512
+ hourly_status_counts = pos_data.groupby(['hour', 'Alarm Status']).size().unstack(fill_value=0)
513
+
514
+ # Klasifikasi berdasarkan kata kunci
515
+ hourly_normal = hourly_status_counts.get('No Alarm', pd.Series(0, index=shift_hours)).reindex(shift_hours, fill_value=0)
516
+ hourly_amber = hourly_status_counts.filter(regex='Amber').sum(axis=1).reindex(shift_hours, fill_value=0) # Semua yang mengandung "Amber"
517
+ hourly_red = hourly_status_counts.filter(regex='Red').sum(axis=1).reindex(shift_hours, fill_value=0) # Semua yang mengandung "Red"
518
+
519
+ # Total per jam
520
+ total_per_hour = hourly_normal + hourly_amber + hourly_red
521
+
522
+ # Sudut: sesuaikan agar jam 12 di bawah (180Β°), jam 6 di kanan (90Β°), jam 3 di atas (0Β°), jam 9 di kiri (270Β°)
523
+ if shift_type == 'pagi':
524
+ # Shift Pagi (06:00–18:00) β†’ 0Β° = 03:00, 90Β° = 06:00, 180Β° = 12:00, 270Β° = 18:00
525
+ theta = [(h - 3) * 30 for h in shift_hours] # 12 jam * 30Β° = 360Β°
526
+ tickvals = [0, 90, 180, 270]
527
+ ticktext = ["03:00", "06:00", "12:00", "18:00"]
528
+ else: # Shift Sore (18:00–06:00) β€” TAPI ikuti aturan jam seperti pagi
529
+ # Shift Sore (18:00–06:00) β†’ 0Β° = 21:00, 90Β° = 00:00, 180Β° = 06:00, 270Β° = 12:00
530
+ # Tapi karena ingin ikut aturan pagi, kita mapping jam agar 0Β° = 03:00, 90Β° = 06:00, 180Β° = 12:00, 270Β° = 18:00
531
+ # Maka kita gunakan jam pagi (06:00–18:00) sebagai referensi jam, tapi data diambil dari sore (18:00–06:00)
532
+ # Kita mapping: 18->06, 19->07, ..., 23->11, 00->12, 01->13, ..., 05->17
533
+ # Maka: theta = [(mapped_h - 3) * 30] where mapped_h = h + 12 if h < 6 else h - 12
534
+ theta = [(h + 12 if h < 6 else h - 12 - 3) * 30 for h in shift_hours] # 12 jam * 30Β° = 360Β°
535
+ tickvals = [0, 90, 180, 270]
536
+ ticktext = ["21:00", "00:00", "06:00", "12:00"]
537
+
538
+ fig = go.Figure()
539
+
540
+ # Tambahkan trace untuk masing-masing kategori dengan hovertemplate custom
541
+ fig.add_trace(go.Barpolar(
542
+ r=hourly_normal.values,
543
+ theta=theta,
544
+ name='Normal',
545
+ marker_color='#2E7D32', # Hijau
546
+ opacity=0.8,
547
+ hovertemplate='<b>Hour:</b> %{customdata}:00<br><b>Count:</b> %{r}<br><b>Status:</b> Normal<extra></extra>',
548
+ customdata=shift_hours
549
+ ))
550
+ fig.add_trace(go.Barpolar(
551
+ r=hourly_amber.values,
552
+ theta=theta,
553
+ name='Amber',
554
+ marker_color='#FFC107', # Kuning
555
+ opacity=0.8,
556
+ hovertemplate='<b>Hour:</b> %{customdata}:00<br><b>Count:</b> %{r}<br><b>Status:</b> Amber<extra></extra>',
557
+ customdata=shift_hours
558
+ ))
559
+ fig.add_trace(go.Barpolar(
560
+ r=hourly_red.values,
561
+ theta=theta,
562
+ name='Red',
563
+ marker_color='#D32F2F', # Merah
564
+ opacity=0.8,
565
+ hovertemplate='<b>Hour:</b> %{customdata}:00<br><b>Count:</b> %{r}<br><b>Status:</b> Red<extra></extra>',
566
+ customdata=shift_hours
567
+ ))
568
+
569
+ fig.update_layout(
570
+ polar=dict(
571
+ angularaxis=dict(
572
+ direction="clockwise",
573
+ period=len(shift_hours),
574
+ rotation=0,
575
+ tickvals=tickvals,
576
+ ticktext=ticktext,
577
+ tickfont=dict(size=12)
578
+ ),
579
+ radialaxis=dict(
580
+ visible=True,
581
+ range=[0, max(total_per_hour.max() * 1.1, 1)]
582
  )
583
+ ),
584
+ showlegend=False,
585
+ margin=dict(t=30, b=20, l=20, r=20),
586
+ height=250,
587
+ title_text=title,
588
+ title_x=0.5
589
+ )
590
+ return fig
591
 
592
+ # =============== ROW 1: Position 1 & 2 (Pagi & Sore) ===============
593
+ with col1:
594
+ # Position 1 Pagi (06:00–18:00)
595
+ st.markdown('<div style="text-align:center; font-weight:bold; margin-bottom: 8px;">Position 1 (06:00–18:00)</div>', unsafe_allow_html=True)
596
+ pos1_data = alarm_data[alarm_data['Position'] == 1].copy()
597
+ pos1_data = pos1_data[pos1_data['hour'].between(6, 17, inclusive='both')]
598
+ fig1 = create_radial_chart(pos1_data, "Position 1 (06:00–18:00)", list(range(6, 18)), 'pagi')
599
+ if fig1 is not None:
600
+ st.plotly_chart(fig1, use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
601
  else:
602
+ st.warning("No data for Position 1 (06:00–18:00)")
603
 
 
604
  with col2:
605
+ # Position 1 Sore (18:00–06:00) β€” TAPI ikuti aturan jam seperti pagi
606
+ st.markdown('<div style="text-align:center; font-weight:bold; margin-bottom: 8px;">Position 1 (18:00–06:00)</div>', unsafe_allow_html=True)
607
+ pos1_data = alarm_data[alarm_data['Position'] == 1].copy()
608
+ pos1_data = pos1_data[~pos1_data['hour'].between(6, 17, inclusive='both')]
609
+ fig2 = create_radial_chart(pos1_data, "Position 1 (18:00–06:00)", list(range(18, 24)) + list(range(0, 6)), 'sore')
610
+ if fig2 is not None:
611
+ st.plotly_chart(fig2, use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
612
  else:
613
+ st.warning("No data for Position 1 (18:00–06:00)")
 
 
 
614
 
615
  with col3:
616
+ # Position 2 Pagi (06:00–18:00)
617
+ st.markdown('<div style="text-align:center; font-weight:bold; margin-bottom: 8px;">Position 2 (06:00–18:00)</div>', unsafe_allow_html=True)
618
+ pos2_data = alarm_data[alarm_data['Position'] == 2].copy()
619
+ pos2_data = pos2_data[pos2_data['hour'].between(6, 17, inclusive='both')]
620
+ fig3 = create_radial_chart(pos2_data, "Position 2 (06:00–18:00)", list(range(6, 18)), 'pagi')
621
+ if fig3 is not None:
622
+ st.plotly_chart(fig3, use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
623
  else:
624
+ st.warning("No data for Position 2 (06:00–18:00)")
625
 
 
626
  with col4:
627
+ # Position 2 Sore (18:00–06:00) β€” TAPI ikuti aturan jam seperti pagi
628
+ st.markdown('<div style="text-align:center; font-weight:bold; margin-bottom: 8px;">Position 2 (18:00–06:00)</div>', unsafe_allow_html=True)
629
+ pos2_data = alarm_data[alarm_data['Position'] == 2].copy()
630
+ pos2_data = pos2_data[~pos2_data['hour'].between(6, 17, inclusive='both')]
631
+ fig4 = create_radial_chart(pos2_data, "Position 2 (18:00–06:00)", list(range(18, 24)) + list(range(0, 6)), 'sore')
632
+ if fig4 is not None:
633
+ st.plotly_chart(fig4, use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
634
  else:
635
+ st.warning("No data for Position 2 (18:00–06:00)")
636
+
637
+ # =============== ROW 2: Position 3 & 4 (Pagi & Sore) ===============
638
+ with col5:
639
+ # Position 3 Pagi (06:00–18:00)
640
+ st.markdown('<div style="text-align:center; font-weight:bold; margin-bottom: 8px;">Position 3 (06:00–18:00)</div>', unsafe_allow_html=True)
641
+ pos3_data = alarm_data[alarm_data['Position'] == 3].copy()
642
+ pos3_data = pos3_data[pos3_data['hour'].between(6, 17, inclusive='both')]
643
+ fig5 = create_radial_chart(pos3_data, "Position 3 (06:00–18:00)", list(range(6, 18)), 'pagi')
644
+ if fig5 is not None:
645
+ st.plotly_chart(fig5, use_container_width=True)
646
+ else:
647
+ st.warning("No data for Position 3 (06:00–18:00)")
648
+
649
+ with col6:
650
+ # Position 3 Sore (18:00–06:00) β€” TAPI ikuti aturan jam seperti pagi
651
+ st.markdown('<div style="text-align:center; font-weight:bold; margin-bottom: 8px;">Position 3 (18:00–06:00)</div>', unsafe_allow_html=True)
652
+ pos3_data = alarm_data[alarm_data['Position'] == 3].copy()
653
+ pos3_data = pos3_data[~pos3_data['hour'].between(6, 17, inclusive='both')]
654
+ fig6 = create_radial_chart(pos3_data, "Position 3 (18:00–06:00)", list(range(18, 24)) + list(range(0, 6)), 'sore')
655
+ if fig6 is not None:
656
+ st.plotly_chart(fig6, use_container_width=True)
657
+ else:
658
+ st.warning("No data for Position 3 (18:00–06:00)")
659
+
660
+ with col7:
661
+ # Position 4 Pagi (06:00–18:00)
662
+ st.markdown('<div style="text-align:center; font-weight:bold; margin-bottom: 8px;">Position 4 (06:00–18:00)</div>', unsafe_allow_html=True)
663
+ pos4_data = alarm_data[alarm_data['Position'] == 4].copy()
664
+ pos4_data = pos4_data[pos4_data['hour'].between(6, 17, inclusive='both')]
665
+ fig7 = create_radial_chart(pos4_data, "Position 4 (06:00–18:00)", list(range(6, 18)), 'pagi')
666
+ if fig7 is not None:
667
+ st.plotly_chart(fig7, use_container_width=True)
668
+ else:
669
+ st.warning("No data for Position 4 (06:00–18:00)")
670
+
671
+ with col8:
672
+ # Position 4 Sore (18:00–06:00) β€” TAPI ikuti aturan jam seperti pagi
673
+ st.markdown('<div style="text-align:center; font-weight:bold; margin-bottom: 8px;">Position 4 (18:00–06:00)</div>', unsafe_allow_html=True)
674
+ pos4_data = alarm_data[alarm_data['Position'] == 4].copy()
675
+ pos4_data = pos4_data[~pos4_data['hour'].between(6, 17, inclusive='both')]
676
+ fig8 = create_radial_chart(pos4_data, "Position 4 (18:00–06:00)", list(range(18, 24)) + list(range(0, 6)), 'sore')
677
+ if fig8 is not None:
678
+ st.plotly_chart(fig8, use_container_width=True)
679
+ else:
680
+ st.warning("No data for Position 4 (18:00–06:00)")
681
 
682
  # =============== INSIGHT 3 ===============
683
+ if alarm_data.empty:
684
+ insight_text = "β€’ No data available for analysis."
685
+ else:
686
+ # Insight tetap sama
687
+ alarm_hours = alarm_data['hour']
688
+
689
+ def hour_to_band(h):
690
+ if 0 <= h < 6: return "00:00–06:00 (Night)"
691
+ if 6 <= h < 12: return "06:00–12:00 (Morning)"
692
+ if 12 <= h < 18: return "12:00–18:00 (Afternoon)"
693
+ return "18:00–00:00 (Evening)"
694
+
695
+ alarm_hours_df = pd.DataFrame({'hour': alarm_hours})
696
+ alarm_hours_df['band'] = alarm_hours_df['hour'].apply(hour_to_band)
697
+ band_counts = alarm_hours_df['band'].value_counts().sort_index()
698
+
699
+ top_bands = band_counts.nlargest(2)
700
+ dominant_band = top_bands.index[0] if len(top_bands) > 0 else "N/A"
701
+ second_dominant_band = top_bands.index[1] if len(top_bands) > 1 else "N/A"
702
+
703
+ dominant_pct = (top_bands.iloc[0] / band_counts.sum() * 100) if len(top_bands) > 0 else 0
704
+ second_pct = (top_bands.iloc[1] / band_counts.sum() * 100) if len(top_bands) > 1 else 0
705
+
706
+ # Hitung jumlah masing-masing jenis alarm
707
+ normal_alarms = alarm_data[alarm_data['Alarm Status'] == 'No Alarm'].shape[0]
708
+ red_alarms = alarm_data[alarm_data['Alarm Status'].str.contains('Red', na=False)].shape[0]
709
+ amber_alarms = alarm_data[alarm_data['Alarm Status'].str.contains('Amber', na=False)].shape[0]
710
+
711
+ # Insight Spesifik Per Position dan Shift
712
+ insight_lines = [
713
+ f"{dominant_band} is the dominant period ({dominant_pct:.1f}% of all data).",
714
+ f"{second_dominant_band} is the second-highest period ({second_pct:.1f}% of data).",
715
+ f"Total: Normal={normal_alarms}, Amber={amber_alarms}, Red={red_alarms}"
716
+ ]
717
+
718
+ # Position 1 (Shift Pagi)
719
+ pos1_pagi = alarm_data[(alarm_data['Position'] == 1) & (alarm_data['hour'].between(6, 17, inclusive='both'))]
720
+ if not pos1_pagi.empty:
721
+ pos1_pagi_total = pos1_pagi.groupby('hour').size()
722
+ if not pos1_pagi_total.empty:
723
+ dominant_hour_p1_pagi = pos1_pagi_total.idxmax()
724
+ dominant_count_p1_pagi = pos1_pagi_total.max()
725
+ 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.")
726
+
727
+ # Position 1 (Shift Sore)
728
+ pos1_sore = alarm_data[(alarm_data['Position'] == 1) & (~alarm_data['hour'].between(6, 17, inclusive='both'))]
729
+ if not pos1_sore.empty:
730
+ pos1_sore_red = pos1_sore[pos1_sore['Alarm Status'].str.contains('Red', na=False)]
731
+ if not pos1_sore_red.empty:
732
+ red_percentage_p1_sore = (len(pos1_sore_red) / len(pos1_sore)) * 100
733
+ insight_lines.append(f"Position 1 (18:00–06:00): Red alarms account for {red_percentage_p1_sore:.1f}% of total alarms.")
734
+
735
+ # Position 3 (Shift Pagi)
736
+ pos3_pagi = alarm_data[(alarm_data['Position'] == 3) & (alarm_data['hour'].between(6, 17, inclusive='both'))]
737
+ if not pos3_pagi.empty:
738
+ pos3_pagi_total = pos3_pagi.groupby('hour').size()
739
+ if not pos3_pagi_total.empty:
740
+ dominant_hour_p3_pagi = pos3_pagi_total.idxmax()
741
+ dominant_count_p3_pagi = pos3_pagi_total.max()
742
+ 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.")
743
+
744
+ # Position 3 (Shift Sore)
745
+ pos3_sore = alarm_data[(alarm_data['Position'] == 3) & (~alarm_data['hour'].between(6, 17, inclusive='both'))]
746
+ if not pos3_sore.empty:
747
+ pos3_sore_amber = pos3_sore[pos3_sore['Alarm Status'].str.contains('Amber', na=False)]
748
+ if not pos3_sore_amber.empty:
749
+ amber_percentage_p3_sore = (len(pos3_sore_amber) / len(pos3_sore)) * 100
750
+ insight_lines.append(f"Position 3 (18:00–06:00): Amber alarms account for {amber_percentage_p3_sore:.1f}% of total alarms.")
751
+
752
+ # Position 4 (Shift Pagi)
753
+ pos4_pagi = alarm_data[(alarm_data['Position'] == 4) & (alarm_data['hour'].between(6, 17, inclusive='both'))]
754
+ if not pos4_pagi.empty:
755
+ pos4_pagi_total = pos4_pagi.groupby('hour').size()
756
+ if not pos4_pagi_total.empty:
757
+ dominant_hour_p4_pagi = pos4_pagi_total.idxmax()
758
+ dominant_count_p4_pagi = pos4_pagi_total.max()
759
+ 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.")
760
+
761
+ # Position 4 (Shift Sore)
762
+ pos4_sore = alarm_data[(alarm_data['Position'] == 4) & (~alarm_data['hour'].between(6, 17, inclusive='both'))]
763
+ if not pos4_sore.empty:
764
+ pos4_sore_amber = pos4_sore[pos4_sore['Alarm Status'].str.contains('Amber', na=False)]
765
+ if not pos4_sore_amber.empty:
766
+ amber_percentage_p4_sore = (len(pos4_sore_amber) / len(pos4_sore)) * 100
767
+ insight_lines.append(f"Position 4 (18:00–06:00): Amber alarms account for {amber_percentage_p4_sore:.1f}% of total alarms.")
768
+
769
+ insight_text = "\n".join(insight_lines)
770
+
771
+ # =============== DISPLAY INSIGHT ===============
772
  st.markdown(f"""
773
  <div class="insight-box">
774
  <div class="content">
775
+ {insight_text}
776
  </div>
777
  </div>
778
  """, unsafe_allow_html=True)
779
+ #### OBJECTICVE 3
780
  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)
781
 
782
  # Prepare data
 
1115
  </div>
1116
  """, unsafe_allow_html=True)
1117
 
1118
+
1119
  # ================= OBJECTIVE 4 =================
1120
  st.markdown('<h3 class="objective-title">OBJECTIVE 4: Spatial Risk Mapping β€” Where Do Red Pressure Alarms Occur Most Frequently?</h3>', unsafe_allow_html=True)
1121