SHELLAPANDIANGANHUNGING commited on
Commit
1e29781
·
verified ·
1 Parent(s): 9e19112

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +150 -358
app.py CHANGED
@@ -570,34 +570,38 @@ st.markdown('''
570
  position: relative;
571
  top: 0; right: 0;
572
  ">
573
- OBJECTIVE 2: Hourly Alarm Trend Analysis By Tyre Position
574
  </div>
575
  </div>
576
  ''', unsafe_allow_html=True)
577
 
578
- # Ambil data alarm
579
- alarm_data = dff[dff['is_alarm'] == 1].copy()
580
 
581
- if alarm_data.empty:
582
- st.warning("No alarm data to display.")
583
- else:
584
- # Hitung jumlah alarm per jam dan per posisi (pastikan posisi 1–4)
585
- hourly_pos_counts = alarm_data.groupby(['hour', 'Position']).size().unstack(fill_value=0)
 
 
 
 
586
  for pos in [1, 2, 3, 4]:
587
- if pos not in hourly_pos_counts.columns:
588
- hourly_pos_counts[pos] = 0
589
- hourly_pos_counts = hourly_pos_counts[[1, 2, 3, 4]].copy()
 
590
 
591
- # === PERBAIKAN UTAMA: gunakan melt + string column names ===
592
- hourly_melted = hourly_pos_counts.reset_index().melt(
593
  id_vars='hour',
594
  value_vars=[1, 2, 3, 4],
595
  var_name='Position',
596
- value_name='Alarm Count'
597
  )
598
- hourly_melted['Position'] = hourly_melted['Position'].astype(str) # pastikan string
599
 
600
- # Warna sesuai preferensi (exact match)
601
  color_map = {
602
  '1': '#003DA5', # Dark blue
603
  '2': '#7FA6E8', # Light blue
@@ -605,20 +609,19 @@ else:
605
  '4': '#FFE082' # Light amber
606
  }
607
 
608
- # Plot trend
609
- fig = px.line(
610
- hourly_melted,
611
  x='hour',
612
- y='Alarm Count',
613
  color='Position',
614
  color_discrete_map=color_map,
615
- title="Hourly Alarm Count by Tyre Position",
616
- labels={'Alarm Count': 'Number of Alarms', 'Position': 'Tyre Position'},
617
  line_shape='linear',
618
  template="plotly_white"
619
  )
620
 
621
- fig.update_layout(
622
  xaxis=dict(
623
  title="Hour of Day",
624
  tickmode='array',
@@ -626,304 +629,91 @@ else:
626
  ticktext=[f"{h:02d}:00" for h in range(24)],
627
  tickangle=45
628
  ),
629
- yaxis=dict(title="Number of Alarms"),
630
  legend_title_text='Tyre Position',
631
  margin=dict(t=40, b=40, l=40, r=20),
632
- title_x=0.5 # center title
633
  )
634
 
635
- st.plotly_chart(fig, use_container_width=True)
636
-
637
-
638
- # === ANALISIS WAKTU DOMINAN (00–18 vs 18–24) ===
639
- alarm_data['period'] = alarm_data['hour'].apply(
640
- lambda h: '00:00–18:00' if 0 <= h < 18 else '18:00–24:00'
641
- )
642
- dom_per_pos = alarm_data.groupby(['Position', 'period']).size().unstack(fill_value=0)
643
- # Pastikan kolom ada
644
- for per in ['00:00–18:00', '18:00–24:00']:
645
- if per not in dom_per_pos.columns:
646
- dom_per_pos[per] = 0
647
- dom_per_pos['Dominant Period'] = dom_per_pos.idxmax(axis=1)
648
- dom_per_pos['Dominant %'] = (
649
- dom_per_pos[['00:00–18:00', '18:00–24:00']].max(axis=1) /
650
- dom_per_pos[['00:00–18:00', '18:00–24:00']].sum(axis=1) * 100
651
- ).round(2)
652
-
653
- # Total per posisi
654
- total_by_pos = alarm_data['Position'].value_counts().reindex([1,2,3,4], fill_value=0)
655
-
656
-
657
- insight_text = f"""
658
- Positions 1 & 2 (front) show stronger daytime concentration; Positions 3 & 4 (rear) have more balanced distribution."""
659
- st.markdown(f"""
660
- <div class="insight-box">
661
- <div class="content">
662
- {insight_text.strip()}
663
- </div>
664
- </div>
665
- """, unsafe_allow_html=True)
666
- # ================= OBJECTIVE 3 =================
667
- st.markdown('<h3 class="objective-title">OBJECTIVE 4: Correlation — How Does Heat Influence Pressure and Which Tyres Trigger Red Alarms?</h3>', unsafe_allow_html=True)
668
-
669
- # Prepare data
670
- front_df = dff[dff['Position'].isin([1, 2])].copy()
671
- rear_df = dff[dff['Position'].isin([3, 4])].copy()
672
-
673
- col1, col2 = st.columns(2)
674
-
675
- # =============== COL 1: Front — Temperature → Pressure (Scatter + Regression Area) ===============
676
- with col1:
677
- st.markdown('<h5 style="text-align:center; margin-top: 0;">Front Tyres: Temperature → Pressure</h5>', unsafe_allow_html=True)
678
-
679
- if not front_df.empty:
680
- # Filter valid data
681
- valid_data = front_df.dropna(subset=['Temperature (°C)', 'Pressure (psi)'])
682
- if len(valid_data) > 1:
683
- X = valid_data[['Temperature (°C)']]
684
- y = valid_data['Pressure (psi)']
685
- model = LinearRegression().fit(X, y)
686
- x_line = np.linspace(X.min(), X.max(), 100).reshape(-1, 1)
687
- y_line = model.predict(x_line)
688
- corr = np.corrcoef(valid_data['Temperature (°C)'], valid_data['Pressure (psi)'])[0, 1]
689
-
690
- # Buat scatter plot
691
- fig1 = px.scatter(
692
- valid_data,
693
- x='Temperature (°C)',
694
- y='Pressure (psi)',
695
- color_discrete_sequence=['#003DA5'],
696
- template="plotly_white",
697
- labels={'Temperature (°C)': 'Temperature (°C)', 'Pressure (psi)': 'Pressure (psi)'}
698
- )
699
-
700
- # Tambahkan garis regresi
701
- fig1.add_trace(go.Scatter(
702
- x=x_line.flatten(), y=y_line,
703
- mode='lines',
704
- name='Linear Regression',
705
- line=dict(color='#D32F2F', width=2)
706
- ))
707
-
708
- # Tambahkan area confidence interval (soft background)
709
- # Hitung standard error
710
- y_pred = model.predict(X)
711
- residuals = y - y_pred
712
- mse = np.mean(residuals**2)
713
- std_error = np.sqrt(mse)
714
- y_upper = y_line + 1.96 * std_error
715
- y_lower = y_line - 1.96 * std_error
716
-
717
- # Tambahkan area
718
- fig1.add_trace(go.Scatter(
719
- x=np.concatenate([x_line.flatten(), x_line.flatten()[::-1]]),
720
- y=np.concatenate([y_upper, y_lower[::-1]]),
721
- fill='toself',
722
- fillcolor='rgba(211, 47, 47, 0.1)', # Merah transparan
723
- line=dict(color='rgba(255,255,255,0)'),
724
- showlegend=False,
725
- name='Confidence Interval'
726
- ))
727
-
728
- fig1.update_layout(
729
- margin=dict(t=40),
730
- annotations=[
731
- dict(
732
- x=0.95, y=0.95,
733
- xref="paper", yref="paper",
734
- text=f"r = {corr:.2f}",
735
- showarrow=False,
736
- bgcolor="white",
737
- bordercolor="black",
738
- borderwidth=1,
739
- font=dict(color="black")
740
- )
741
- ],
742
- legend=dict(
743
- title_text='Data & Regression',
744
- bgcolor="white",
745
- bordercolor="lightgray",
746
- borderwidth=1,
747
- itemclick=False,
748
- itemdoubleclick=False
749
- ),
750
- showlegend=True
751
- )
752
- st.plotly_chart(fig1, use_container_width=True)
753
- else:
754
- st.warning("Insufficient data for front tyres.")
755
- else:
756
- st.warning("No front tyre data.")
757
 
758
- # =============== COL 2: Front Temperature / Speed (Boxplot) ===============
759
  with col2:
760
- st.markdown('<h5 style="text-align:center; margin-top: 0;">Front Tyres: Temperature / Speed (Boxplot)</h5>', unsafe_allow_html=True)
761
-
762
- if not front_df.empty:
763
- # Hitung rasio suhu/kecepatan
764
- front_df['Temp_Speed_Ratio'] = front_df['Temperature (°C)'] / (front_df['Speed (km/h)'] + 1e-6)
765
-
766
- valid_data = front_df.dropna(subset=['Temp_Speed_Ratio'])
767
- if not valid_data.empty:
768
- fig2 = px.box(
769
- valid_data,
770
- y='Temp_Speed_Ratio',
771
- template="plotly_white",
772
- labels={'Temp_Speed_Ratio': 'Temperature / Speed'}
773
- )
774
-
775
- fig2.update_traces(
776
- marker_color='#003DA5',
777
- name='Front Tyres'
778
- )
779
-
780
- fig2.update_layout(
781
- margin=dict(t=40),
782
- yaxis_title='Temperature / Speed',
783
- showlegend=False
784
- )
785
- st.plotly_chart(fig2, use_container_width=True)
786
- else:
787
- st.warning("Insufficient data for front tyres.")
788
- else:
789
- st.warning("No front tyre data.")
790
-
791
- # =============== COL 3: Rear — Temperature → Pressure (Scatter + Regression Area) ===============
792
- col3, col4 = st.columns(2)
793
-
794
- with col3:
795
- st.markdown('<h5 style="text-align:center; margin-top: 0;">Rear Tyres: Temperature → Pressure</h5>', unsafe_allow_html=True)
796
-
797
- if not rear_df.empty:
798
- valid_data = rear_df.dropna(subset=['Temperature (°C)', 'Pressure (psi)'])
799
- if len(valid_data) > 1:
800
- X = valid_data[['Temperature (°C)']]
801
- y = valid_data['Pressure (psi)']
802
- model = LinearRegression().fit(X, y)
803
- x_line = np.linspace(X.min(), X.max(), 100).reshape(-1, 1)
804
- y_line = model.predict(x_line)
805
- corr = np.corrcoef(valid_data['Temperature (°C)'], valid_data['Pressure (psi)'])[0, 1]
806
-
807
- fig3 = px.scatter(
808
- valid_data,
809
- x='Temperature (°C)',
810
- y='Pressure (psi)',
811
- color_discrete_sequence=['#FFB300'],
812
- template="plotly_white",
813
- labels={'Temperature (°C)': 'Temperature (°C)', 'Pressure (psi)': 'Pressure (psi)'}
814
- )
815
-
816
- fig3.add_trace(go.Scatter(
817
- x=x_line.flatten(), y=y_line,
818
- mode='lines',
819
- name='Linear Regression',
820
- line=dict(color='#D32F2F', width=2)
821
- ))
822
 
823
- # Confidence interval area
824
- y_pred = model.predict(X)
825
- residuals = y - y_pred
826
- mse = np.mean(residuals**2)
827
- std_error = np.sqrt(mse)
828
- y_upper = y_line + 1.96 * std_error
829
- y_lower = y_line - 1.96 * std_error
830
 
831
- fig3.add_trace(go.Scatter(
832
- x=np.concatenate([x_line.flatten(), x_line.flatten()[::-1]]),
833
- y=np.concatenate([y_upper, y_lower[::-1]]),
834
- fill='toself',
835
- fillcolor='rgba(211, 47, 47, 0.1)',
836
- line=dict(color='rgba(255,255,255,0)'),
837
- showlegend=False,
838
- name='Confidence Interval'
839
- ))
840
-
841
- fig3.update_layout(
842
- margin=dict(t=40),
843
- annotations=[
844
- dict(
845
- x=0.95, y=0.95,
846
- xref="paper", yref="paper",
847
- text=f"r = {corr:.2f}",
848
- showarrow=False,
849
- bgcolor="white",
850
- bordercolor="black",
851
- borderwidth=1,
852
- font=dict(color="black")
853
- )
854
- ],
855
- legend=dict(
856
- title_text='Data & Regression',
857
- bgcolor="white",
858
- bordercolor="lightgray",
859
- borderwidth=1,
860
- itemclick=False,
861
- itemdoubleclick=False
862
- ),
863
- showlegend=True
864
- )
865
- st.plotly_chart(fig3, use_container_width=True)
866
- else:
867
- st.warning("Insufficient data for rear tyres.")
868
  else:
869
- st.warning("No rear tyre data.")
870
-
871
- # =============== COL 4: Rear — Temperature / Speed (Boxplot) ===============
872
- with col4:
873
- st.markdown('<h5 style="text-align:center; margin-top: 0;">Rear Tyres: Temperature / Speed (Boxplot)</h5>', unsafe_allow_html=True)
874
-
875
- if not rear_df.empty:
876
- rear_df['Temp_Speed_Ratio'] = rear_df['Temperature (°C)'] / (rear_df['Speed (km/h)'] + 1e-6)
877
-
878
- valid_data = rear_df.dropna(subset=['Temp_Speed_Ratio'])
879
- if not valid_data.empty:
880
- fig4 = px.box(
881
- valid_data,
882
- y='Temp_Speed_Ratio',
883
- template="plotly_white",
884
- labels={'Temp_Speed_Ratio': 'Temperature / Speed'}
885
- )
886
-
887
- fig4.update_traces(
888
- marker_color='#FFB300',
889
- name='Rear Tyres'
890
- )
 
 
 
 
 
 
 
 
891
 
892
- fig4.update_layout(
893
- margin=dict(t=40),
894
- yaxis_title='Temperature / Speed',
895
- showlegend=False
896
- )
897
- st.plotly_chart(fig4, use_container_width=True)
898
- else:
899
- st.warning("Insufficient data for rear tyres.")
900
- else:
901
- st.warning("No rear tyre data.")
 
 
 
902
 
903
- # =============== INSIGHT 4 ===============
904
- def safe_corr(a, b):
905
- mask = ~(np.isnan(a) | np.isnan(b))
906
- if mask.sum() < 2:
907
- return 0.0
908
- return np.corrcoef(a[mask], b[mask])[0, 1]
909
 
910
- corr_p_t_front = safe_corr(front_df['Temperature (°C)'], front_df['Pressure (psi)'])
911
- corr_t_s_front = safe_corr(front_df['Temperature (°C)'], front_df['Speed (km/h)'])
912
- corr_p_t_rear = safe_corr(rear_df['Temperature (°C)'], rear_df['Pressure (psi)'])
913
- corr_t_s_rear = safe_corr(rear_df['Temperature (°C)'], rear_df['Speed (km/h)'])
914
 
915
  insight_text = f"""
916
- 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.
 
 
917
  """
918
 
919
  st.markdown(f"""
920
  <div class="insight-box">
921
- <div class="content">
922
  {insight_text.strip()}
923
- </div>
924
  </div>
925
  """, unsafe_allow_html=True)
926
- # ================= OBJECTIVE 4 =================
927
  st.markdown('<h3 class="objective-title">OBJECTIVE 4: Correlation — How Does Heat Influence Pressure and Which Tyres Trigger Red Alarms?</h3>', unsafe_allow_html=True)
928
 
929
  # Prepare data
@@ -933,10 +723,20 @@ rear_df = dff[dff['Position'].isin([3, 4])].copy()
933
  col1, col2 = st.columns(2)
934
 
935
  # =============== COL 1: Front — Temperature → Pressure (Scatter + Regression Area) ===============
 
936
  with col1:
937
  st.markdown('<h5 style="text-align:center; margin-top: 0;">Front Tyres: Temperature → Pressure</h5>', unsafe_allow_html=True)
938
 
939
  if not front_df.empty:
 
 
 
 
 
 
 
 
 
940
  # Filter valid data
941
  valid_data = front_df.dropna(subset=['Temperature (°C)', 'Pressure (psi)'])
942
  if len(valid_data) > 1:
@@ -947,42 +747,30 @@ with col1:
947
  y_line = model.predict(x_line)
948
  corr = np.corrcoef(valid_data['Temperature (°C)'], valid_data['Pressure (psi)'])[0, 1]
949
 
950
- # Buat scatter plot
951
  fig1 = px.scatter(
952
  valid_data,
953
  x='Temperature (°C)',
954
  y='Pressure (psi)',
955
- color_discrete_sequence=['#003DA5'],
 
 
 
 
 
 
956
  template="plotly_white",
957
  labels={'Temperature (°C)': 'Temperature (°C)', 'Pressure (psi)': 'Pressure (psi)'}
958
  )
959
 
960
- # Tambahkan garis regresi
961
- fig1.add_trace(go.Scatter(
962
- x=x_line.flatten(), y=y_line,
963
- mode='lines',
964
- name='Linear Regression',
965
- line=dict(color='#D32F2F', width=2)
966
- ))
967
-
968
- # Tambahkan area confidence interval (soft background)
969
- # Hitung standard error
970
- y_pred = model.predict(X)
971
- residuals = y - y_pred
972
- mse = np.mean(residuals**2)
973
- std_error = np.sqrt(mse)
974
- y_upper = y_line + 1.96 * std_error
975
- y_lower = y_line - 1.96 * std_error
976
 
977
- # Tambahkan area
978
  fig1.add_trace(go.Scatter(
979
- x=np.concatenate([x_line.flatten(), x_line.flatten()[::-1]]),
980
- y=np.concatenate([y_upper, y_lower[::-1]]),
981
- fill='toself',
982
- fillcolor='rgba(211, 47, 47, 0.1)', # Merah transparan
983
- line=dict(color='rgba(255,255,255,0)'),
984
- showlegend=False,
985
- name='Confidence Interval'
986
  ))
987
 
988
  fig1.update_layout(
@@ -1000,14 +788,13 @@ with col1:
1000
  )
1001
  ],
1002
  legend=dict(
1003
- title_text='Data & Regression',
1004
  bgcolor="white",
1005
  bordercolor="lightgray",
1006
  borderwidth=1,
1007
  itemclick=False,
1008
  itemdoubleclick=False
1009
- ),
1010
- showlegend=True
1011
  )
1012
  st.plotly_chart(fig1, use_container_width=True)
1013
  else:
@@ -1015,13 +802,16 @@ with col1:
1015
  else:
1016
  st.warning("No front tyre data.")
1017
 
 
1018
  # =============== COL 2: Front — Temperature / Speed (Boxplot) ===============
1019
  with col2:
1020
  st.markdown('<h5 style="text-align:center; margin-top: 0;">Front Tyres: Temperature / Speed (Boxplot)</h5>', unsafe_allow_html=True)
1021
 
1022
  if not front_df.empty:
1023
  # Hitung rasio suhu/kecepatan
1024
- front_df['Temp_Speed_Ratio'] = front_df['Temperature (°C)'] / (front_df['Speed (km/h)'] + 1e-6)
 
 
1025
 
1026
  valid_data = front_df.dropna(subset=['Temp_Speed_Ratio'])
1027
  if not valid_data.empty:
@@ -1050,11 +840,21 @@ with col2:
1050
 
1051
  # =============== COL 3: Rear — Temperature → Pressure (Scatter + Regression Area) ===============
1052
  col3, col4 = st.columns(2)
 
 
1053
 
1054
  with col3:
1055
  st.markdown('<h5 style="text-align:center; margin-top: 0;">Rear Tyres: Temperature → Pressure</h5>', unsafe_allow_html=True)
1056
 
1057
  if not rear_df.empty:
 
 
 
 
 
 
 
 
1058
  valid_data = rear_df.dropna(subset=['Temperature (°C)', 'Pressure (psi)'])
1059
  if len(valid_data) > 1:
1060
  X = valid_data[['Temperature (°C)']]
@@ -1068,34 +868,25 @@ with col3:
1068
  valid_data,
1069
  x='Temperature (°C)',
1070
  y='Pressure (psi)',
1071
- color_discrete_sequence=['#FFB300'],
1072
- template="plotly_white",
1073
- labels={'Temperature (°C)': 'Temperature (°C)', 'Pressure (psi)': 'Pressure (psi)'}
 
 
 
 
 
1074
  )
1075
 
1076
- fig3.add_trace(go.Scatter(
1077
- x=x_line.flatten(), y=y_line,
1078
- mode='lines',
1079
- name='Linear Regression',
1080
- line=dict(color='#D32F2F', width=2)
1081
- ))
1082
-
1083
- # Confidence interval area
1084
- y_pred = model.predict(X)
1085
- residuals = y - y_pred
1086
- mse = np.mean(residuals**2)
1087
- std_error = np.sqrt(mse)
1088
- y_upper = y_line + 1.96 * std_error
1089
- y_lower = y_line - 1.96 * std_error
1090
 
1091
  fig3.add_trace(go.Scatter(
1092
- x=np.concatenate([x_line.flatten(), x_line.flatten()[::-1]]),
1093
- y=np.concatenate([y_upper, y_lower[::-1]]),
1094
- fill='toself',
1095
- fillcolor='rgba(211, 47, 47, 0.1)',
1096
- line=dict(color='rgba(255,255,255,0)'),
1097
- showlegend=False,
1098
- name='Confidence Interval'
1099
  ))
1100
 
1101
  fig3.update_layout(
@@ -1113,14 +904,13 @@ with col3:
1113
  )
1114
  ],
1115
  legend=dict(
1116
- title_text='Data & Regression',
1117
  bgcolor="white",
1118
  bordercolor="lightgray",
1119
  borderwidth=1,
1120
  itemclick=False,
1121
  itemdoubleclick=False
1122
- ),
1123
- showlegend=True
1124
  )
1125
  st.plotly_chart(fig3, use_container_width=True)
1126
  else:
@@ -1133,7 +923,9 @@ with col4:
1133
  st.markdown('<h5 style="text-align:center; margin-top: 0;">Rear Tyres: Temperature / Speed (Boxplot)</h5>', unsafe_allow_html=True)
1134
 
1135
  if not rear_df.empty:
1136
- rear_df['Temp_Speed_Ratio'] = rear_df['Temperature (°C)'] / (rear_df['Speed (km/h)'] + 1e-6)
 
 
1137
 
1138
  valid_data = rear_df.dropna(subset=['Temp_Speed_Ratio'])
1139
  if not valid_data.empty:
 
570
  position: relative;
571
  top: 0; right: 0;
572
  ">
573
+ OBJECTIVE 2: Hourly Data Capture vs Alarm Count Analysis
574
  </div>
575
  </div>
576
  ''', unsafe_allow_html=True)
577
 
578
+ col1, col2 = st.columns(2)
 
579
 
580
+ # =============== COL 1: Capture Data per Jam per Tyre ===============
581
+ with col1:
582
+ st.markdown('<h5 style="text-align:center; margin-top: 0;">Data Capture per Hour by Tyre Position</h5>', unsafe_allow_html=True)
583
+
584
+ # Hitung jumlah data capture per jam dan per posisi (semua data, bukan hanya alarm)
585
+ capture_data = dff.copy()
586
+ hourly_capture_counts = capture_data.groupby(['hour', 'Position']).size().unstack(fill_value=0)
587
+
588
+ # Pastikan semua posisi (1,2,3,4) ada
589
  for pos in [1, 2, 3, 4]:
590
+ if pos not in hourly_capture_counts.columns:
591
+ hourly_capture_counts[pos] = 0
592
+
593
+ hourly_capture_counts = hourly_capture_counts[[1, 2, 3, 4]].copy()
594
 
595
+ # Melt data untuk plotting
596
+ hourly_capture_melted = hourly_capture_counts.reset_index().melt(
597
  id_vars='hour',
598
  value_vars=[1, 2, 3, 4],
599
  var_name='Position',
600
+ value_name='Capture Count'
601
  )
602
+ hourly_capture_melted['Position'] = hourly_capture_melted['Position'].astype(str)
603
 
604
+ # Warna sesuai preferensi
605
  color_map = {
606
  '1': '#003DA5', # Dark blue
607
  '2': '#7FA6E8', # Light blue
 
609
  '4': '#FFE082' # Light amber
610
  }
611
 
612
+ fig1 = px.line(
613
+ hourly_capture_melted,
 
614
  x='hour',
615
+ y='Capture Count',
616
  color='Position',
617
  color_discrete_map=color_map,
618
+ title="Data Capture per Hour by Tyre Position",
619
+ labels={'Capture Count': 'Number of Records', 'Position': 'Tyre Position'},
620
  line_shape='linear',
621
  template="plotly_white"
622
  )
623
 
624
+ fig1.update_layout(
625
  xaxis=dict(
626
  title="Hour of Day",
627
  tickmode='array',
 
629
  ticktext=[f"{h:02d}:00" for h in range(24)],
630
  tickangle=45
631
  ),
632
+ yaxis=dict(title="Number of Records"),
633
  legend_title_text='Tyre Position',
634
  margin=dict(t=40, b=40, l=40, r=20),
635
+ title_x=0.5
636
  )
637
 
638
+ st.plotly_chart(fig1, use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
639
 
640
+ # =============== COL 2: Count Alarm (Amber & Red Only) ===============
641
  with col2:
642
+ 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)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
643
 
644
+ # Filter hanya alarm Amber dan Red
645
+ alarm_data = dff[dff['is_alarm'] == 1].copy()
646
+ alarm_data = alarm_data[alarm_data['Alarm Status'].str.contains('Amber|Red', case=False, na=False)]
 
 
 
 
647
 
648
+ if alarm_data.empty:
649
+ st.warning("No Amber or Red alarm data to display.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
650
  else:
651
+ # Hitung jumlah alarm per jam dan per posisi
652
+ hourly_alarm_counts = alarm_data.groupby(['hour', 'Position']).size().unstack(fill_value=0)
653
+
654
+ # Pastikan semua posisi (1,2,3,4) ada
655
+ for pos in [1, 2, 3, 4]:
656
+ if pos not in hourly_alarm_counts.columns:
657
+ hourly_alarm_counts[pos] = 0
658
+
659
+ hourly_alarm_counts = hourly_alarm_counts[[1, 2, 3, 4]].copy()
660
+
661
+ # Melt data untuk plotting
662
+ hourly_alarm_melted = hourly_alarm_counts.reset_index().melt(
663
+ id_vars='hour',
664
+ value_vars=[1, 2, 3, 4],
665
+ var_name='Position',
666
+ value_name='Alarm Count'
667
+ )
668
+ hourly_alarm_melted['Position'] = hourly_alarm_melted['Position'].astype(str)
669
+
670
+ fig2 = px.line(
671
+ hourly_alarm_melted,
672
+ x='hour',
673
+ y='Alarm Count',
674
+ color='Position',
675
+ color_discrete_map=color_map,
676
+ title="Alarm Count (Amber & Red Only) per Hour by Tyre Position",
677
+ labels={'Alarm Count': 'Number of Alarms', 'Position': 'Tyre Position'},
678
+ line_shape='linear',
679
+ template="plotly_white"
680
+ )
681
 
682
+ fig2.update_layout(
683
+ xaxis=dict(
684
+ title="Hour of Day",
685
+ tickmode='array',
686
+ tickvals=list(range(0, 24)),
687
+ ticktext=[f"{h:02d}:00" for h in range(24)],
688
+ tickangle=45
689
+ ),
690
+ yaxis=dict(title="Number of Alarms"),
691
+ legend_title_text='Tyre Position',
692
+ margin=dict(t=40, b=40, l=40, r=20),
693
+ title_x=0.5
694
+ )
695
 
696
+ st.plotly_chart(fig2, use_container_width=True)
 
 
 
 
 
697
 
698
+ # =============== INSIGHT ===============
699
+ # Insight tetap bisa menampilkan perbandingan
700
+ capture_by_pos = dff['Position'].value_counts().reindex([1,2,3,4], fill_value=0)
701
+ alarm_by_pos = alarm_data['Position'].value_counts().reindex([1,2,3,4], fill_value=0)
702
 
703
  insight_text = f"""
704
+ Total data capture: Pos 1={capture_by_pos[1]}, 2={capture_by_pos[2]}, 3={capture_by_pos[3]}, 4={capture_by_pos[4]}
705
+ • Total alarms (Amber/Red): Pos 1={alarm_by_pos[1]}, 2={alarm_by_pos[2]}, 3={alarm_by_pos[3]}, 4={alarm_by_pos[4]}
706
+ • Alarm density varies significantly between positions — suggesting different operational stress levels.
707
  """
708
 
709
  st.markdown(f"""
710
  <div class="insight-box">
711
+ <div class="content">
712
  {insight_text.strip()}
713
+ </div>
714
  </div>
715
  """, unsafe_allow_html=True)
716
+ # ================= OBJECTIVE 3 =================
717
  st.markdown('<h3 class="objective-title">OBJECTIVE 4: Correlation — How Does Heat Influence Pressure and Which Tyres Trigger Red Alarms?</h3>', unsafe_allow_html=True)
718
 
719
  # Prepare data
 
723
  col1, col2 = st.columns(2)
724
 
725
  # =============== COL 1: Front — Temperature → Pressure (Scatter + Regression Area) ===============
726
+ # =============== COL 1: Front — Temperature → Pressure ===============
727
  with col1:
728
  st.markdown('<h5 style="text-align:center; margin-top: 0;">Front Tyres: Temperature → Pressure</h5>', unsafe_allow_html=True)
729
 
730
  if not front_df.empty:
731
+ # Tambahkan kategori alarm status
732
+ front_df['Category'] = front_df.apply(
733
+ lambda row: f"Normal Front Tyre" if row['Alarm Status'] == 'No Alarm'
734
+ else f"Amber Pressure Front Tyre" if 'Amber' in row['Alarm Status']
735
+ else f"Red Pressure Front Tyre", axis=1
736
+ )
737
+ categories = ["Normal Front Tyre", "Amber Pressure Front Tyre", "Red Pressure Front Tyre"]
738
+ front_df['Category'] = pd.Categorical(front_df['Category'], categories=categories, ordered=True)
739
+
740
  # Filter valid data
741
  valid_data = front_df.dropna(subset=['Temperature (°C)', 'Pressure (psi)'])
742
  if len(valid_data) > 1:
 
747
  y_line = model.predict(x_line)
748
  corr = np.corrcoef(valid_data['Temperature (°C)'], valid_data['Pressure (psi)'])[0, 1]
749
 
 
750
  fig1 = px.scatter(
751
  valid_data,
752
  x='Temperature (°C)',
753
  y='Pressure (psi)',
754
+ color='Category',
755
+ color_discrete_map={
756
+ "Normal Front Tyre": "#2E7D32", # Hijau
757
+ "Amber Pressure Front Tyre": "#FFC107", # Kuning
758
+ "Red Pressure Front Tyre": "#D32F2F" # Merah
759
+ },
760
+ category_orders={'Category': categories},
761
  template="plotly_white",
762
  labels={'Temperature (°C)': 'Temperature (°C)', 'Pressure (psi)': 'Pressure (psi)'}
763
  )
764
 
765
+ fig1.update_traces(
766
+ hovertemplate="<b>%{marker.color}</b><br>Temp: %{x:.1f}°C<br>Pressure: %{y:.1f} psi<extra></extra>",
767
+ marker=dict(size=6)
768
+ )
 
 
 
 
 
 
 
 
 
 
 
 
769
 
 
770
  fig1.add_trace(go.Scatter(
771
+ x=x_line.flatten(), y=y_line,
772
+ mode='lines', name='Trend Line',
773
+ line=dict(color='#1976D2', dash='dot', width=2)
 
 
 
 
774
  ))
775
 
776
  fig1.update_layout(
 
788
  )
789
  ],
790
  legend=dict(
791
+ title_text='Tyre Status',
792
  bgcolor="white",
793
  bordercolor="lightgray",
794
  borderwidth=1,
795
  itemclick=False,
796
  itemdoubleclick=False
797
+ )
 
798
  )
799
  st.plotly_chart(fig1, use_container_width=True)
800
  else:
 
802
  else:
803
  st.warning("No front tyre data.")
804
 
805
+
806
  # =============== COL 2: Front — Temperature / Speed (Boxplot) ===============
807
  with col2:
808
  st.markdown('<h5 style="text-align:center; margin-top: 0;">Front Tyres: Temperature / Speed (Boxplot)</h5>', unsafe_allow_html=True)
809
 
810
  if not front_df.empty:
811
  # Hitung rasio suhu/kecepatan
812
+ front_df = front_df[front_df['Speed (km/h)'] > 0]
813
+
814
+ front_df['Temp_Speed_Ratio'] = front_df['Temperature (°C)'] / (front_df['Speed (km/h)'])
815
 
816
  valid_data = front_df.dropna(subset=['Temp_Speed_Ratio'])
817
  if not valid_data.empty:
 
840
 
841
  # =============== COL 3: Rear — Temperature → Pressure (Scatter + Regression Area) ===============
842
  col3, col4 = st.columns(2)
843
+ # =============== COL 3: Rear — Temperature → Pressure ===============
844
+ col3, col4 = st.columns(2)
845
 
846
  with col3:
847
  st.markdown('<h5 style="text-align:center; margin-top: 0;">Rear Tyres: Temperature → Pressure</h5>', unsafe_allow_html=True)
848
 
849
  if not rear_df.empty:
850
+ rear_df['Category'] = rear_df.apply(
851
+ lambda row: f"Normal Rear Tyre" if row['Alarm Status'] == 'No Alarm'
852
+ else f"Amber Pressure Rear Tyre" if 'Amber' in row['Alarm Status']
853
+ else f"Red Pressure Rear Tyre", axis=1
854
+ )
855
+ categories = ["Normal Rear Tyre", "Amber Pressure Rear Tyre", "Red Pressure Rear Tyre"]
856
+ rear_df['Category'] = pd.Categorical(rear_df['Category'], categories=categories, ordered=True)
857
+
858
  valid_data = rear_df.dropna(subset=['Temperature (°C)', 'Pressure (psi)'])
859
  if len(valid_data) > 1:
860
  X = valid_data[['Temperature (°C)']]
 
868
  valid_data,
869
  x='Temperature (°C)',
870
  y='Pressure (psi)',
871
+ color='Category',
872
+ color_discrete_map={
873
+ "Normal Rear Tyre": "#2E7D32",
874
+ "Amber Pressure Rear Tyre": "#FFC107",
875
+ "Red Pressure Rear Tyre": "#D32F2F"
876
+ },
877
+ category_orders={'Category': categories},
878
+ template="plotly_white"
879
  )
880
 
881
+ fig3.update_traces(
882
+ hovertemplate="<b>%{marker.color}</b><br>Temp: %{x:.1f}°C<br>Pressure: %{y:.1f} psi<extra></extra>",
883
+ marker=dict(size=6)
884
+ )
 
 
 
 
 
 
 
 
 
 
885
 
886
  fig3.add_trace(go.Scatter(
887
+ x=x_line.flatten(), y=y_line,
888
+ mode='lines', name='Trend Line',
889
+ line=dict(color='#1976D2', dash='dot', width=2)
 
 
 
 
890
  ))
891
 
892
  fig3.update_layout(
 
904
  )
905
  ],
906
  legend=dict(
907
+ title_text='Tyre Status',
908
  bgcolor="white",
909
  bordercolor="lightgray",
910
  borderwidth=1,
911
  itemclick=False,
912
  itemdoubleclick=False
913
+ )
 
914
  )
915
  st.plotly_chart(fig3, use_container_width=True)
916
  else:
 
923
  st.markdown('<h5 style="text-align:center; margin-top: 0;">Rear Tyres: Temperature / Speed (Boxplot)</h5>', unsafe_allow_html=True)
924
 
925
  if not rear_df.empty:
926
+ rear_df = rear_df[rear_df['Speed (km/h)'] > 0]
927
+
928
+ rear_df['Temp_Speed_Ratio'] = rear_df['Temperature (°C)'] / (rear_df['Speed (km/h)'] )
929
 
930
  valid_data = rear_df.dropna(subset=['Temp_Speed_Ratio'])
931
  if not valid_data.empty: