SHELLAPANDIANGANHUNGING commited on
Commit
d136d76
·
verified ·
1 Parent(s): 485ad54

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +318 -285
app.py CHANGED
@@ -664,202 +664,266 @@ else:
664
  </div>
665
  """, unsafe_allow_html=True)
666
  # ================= OBJECTIVE 3 =================
667
- st.markdown("""
668
- <h3 class="objective-title">OBJECTIVE 3: Alarm Frequency Analysis — When, Where, and Which Tyres Matter Most?</h3>
669
- <small>*Showing only Red High Pressure Alarms</small>
670
- """, unsafe_allow_html=True)
671
-
672
 
673
- # Filter hanya Position 1 & 2 untuk Objective 2
674
- dff_obj2 = dff[dff['Position'].isin([1, 2])].copy()
 
675
 
676
- col_a, col_b = st.columns(2)
677
 
678
- # --- COL A: Alarm Distribution by Hour (Hanya Position 1 & 2) ---
679
- with col_a:
680
- st.markdown('<h5 style="text-align:center; margin-top: 0;">Alarm Distribution by Hour</h5>', unsafe_allow_html=True)
681
-
682
- alarm_hour_pos = dff_obj2[dff_obj2['is_alarm'] == 1][['hour', 'Position']].copy()
683
 
684
- if alarm_hour_pos.empty:
685
- st.warning("No alarm data to display.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
686
  else:
687
- # Hitung alarm per jam & posisi
688
- hourly_pos_counts = alarm_hour_pos.groupby(['hour', 'Position']).size().unstack(fill_value=0)
689
- positions = sorted([p for p in [1, 2] if p in hourly_pos_counts.columns]) # hanya 1 & 2
690
- color_map = {1: '#d50000', 2: '#ff6d00'}
691
-
692
- fig_polar = go.Figure()
693
- max_r = max(hourly_pos_counts.sum(axis=1)) * 1.05 if not hourly_pos_counts.empty else 10
694
-
695
- for pos in positions:
696
- if pos in hourly_pos_counts.columns:
697
- counts = hourly_pos_counts[pos].reindex(range(24), fill_value=0).values
698
- theta = [h * 15 for h in range(24)] # 24 jam → 360° / 24 = 15° per jam
699
- fig_polar.add_trace(go.Barpolar(
700
- r=counts,
701
- theta=theta,
702
- width=15,
703
- name=f'Position {pos}',
704
- marker_color=color_map[pos],
705
- opacity=0.85,
706
- hovertemplate='<b>Hour:</b> %{theta:0f}:00<br><b>Alarms:</b> %{r}<extra></extra>'
707
- ))
708
-
709
- fig_polar.update_layout(
710
- polar=dict(
711
- radialaxis=dict(visible=True, range=[0, max_r], tickfont=dict(size=10)),
712
- angularaxis=dict(
713
- direction="clockwise",
714
- tickvals=[0, 90, 180, 270],
715
- ticktext=["00:00", "06:00", "12:00", "18:00"],
716
- tickfont=dict(size=11)
717
- )
718
- ),
719
- legend=dict(
720
- title_text='Tyre Position',
721
- yanchor="top",
722
- y=0.98,
723
- xanchor="left",
724
- x=1.02,
725
- bgcolor="rgba(255,255,255,0.7)",
726
- borderwidth=0.5,
727
- itemclick=False,
728
- itemdoubleclick=False
729
- ),
730
- margin=dict(t=40, b=20, l=20, r=120),
731
- hovermode="closest"
732
- )
733
- st.plotly_chart(fig_polar, use_container_width=True)
734
 
735
- # --- COL B: Alarm Hotspots (Front Tyres Only: Pos 1 & 2) ---
736
- with col_b:
737
- st.markdown('<h5 style="text-align:center; margin-top: 0;">Alarm Hotspots by Tyre, Position & Zone</h5>', unsafe_allow_html=True)
738
-
739
- # Filter hanya alarm di front tyres (Pos 1 & 2)
740
- front_alarm_data = dff_obj2[(dff_obj2['is_alarm'] == 1) & (dff_obj2['Position'].isin([1, 2]))].copy()
741
 
742
- if front_alarm_data.empty:
743
- st.warning("No alarm data for front tyres to display.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
744
  else:
745
- agg_data = (
746
- front_alarm_data
747
- .groupby(['TyreSN', 'Position', 'Zone'])
748
- .size()
749
- .reset_index(name='Count')
750
- )
751
- agg_data['Percentage'] = (agg_data['Count'] / agg_data['Count'].sum()) * 100
752
-
753
- # Warna eksplisit untuk 1 & 2
754
- color_map_front = {1: '#d50000', 2: '#ff6d00'}
755
-
756
- fig_bubble = px.scatter(
757
- agg_data,
758
- x='Position',
759
- y='Zone',
760
- size='Count',
761
- color='Position',
762
- color_discrete_map=color_map_front,
763
- hover_name='TyreSN',
764
- hover_data={'Position': True, 'Zone': True, 'Count': True, 'Percentage': ':.1f%'},
765
- size_max=55,
766
- template='plotly_white',
767
- category_orders={'Position': [1, 2]}
768
- )
769
-
770
- # Tambahkan label singkat di tengah bubble (4 digit akhir SN)
771
- fig_bubble.update_traces(
772
- text=agg_data['TyreSN'].str[-4:],
773
- textposition='middle center',
774
- textfont=dict(color='white', size=9)
775
- )
776
-
777
- fig_bubble.update_layout(
778
- xaxis=dict(
779
- title='Position',
780
- tickmode='array',
781
- tickvals=[1, 2],
782
- ticktext=['1', '2'],
783
- tickfont=dict(size=12)
784
- ),
785
- yaxis=dict(title='Zone', tickfont=dict(size=12)),
786
- legend=dict(
787
- title_text='Tyre Position',
788
- yanchor="top",
789
- y=0.98,
790
- xanchor="left",
791
- x=1.02,
792
- bgcolor="rgba(255,255,255,0.7)",
793
- borderwidth=0.5
794
- ),
795
- margin=dict(t=40, b=20, l=20, r=120),
796
- showlegend=True
797
- )
798
-
799
- # Rename legend entries
800
- fig_bubble.for_each_trace(lambda t: t.update(
801
- name=f'Position {int(t.name)}'
802
- ))
803
-
804
- st.plotly_chart(fig_bubble, use_container_width=True)
805
-
806
- # --- INSIGHT 2: Actionable, Numeric, Time-Group Based ---
807
- alarm_hours = dff_obj2[dff_obj2['is_alarm'] == 1]['hour']
808
-
809
- if alarm_hours.empty:
810
- insight_text = "• No alarm data available for analysis."
811
- else:
812
- # Group hours into time bands
813
- def hour_to_band(h):
814
- if 0 <= h < 6: return "00:00–06:00 (Night)"
815
- if 6 <= h < 12: return "06:00–12:00 (Morning)"
816
- if 12 <= h < 18: return "12:00–18:00 (Afternoon)"
817
- return "18:00–00:00 (Evening)"
818
-
819
- alarm_hours_df = pd.DataFrame({'hour': alarm_hours})
820
- alarm_hours_df['band'] = alarm_hours_df['hour'].apply(hour_to_band)
821
- band_counts = alarm_hours_df['band'].value_counts().sort_index() # sort by natural order
822
-
823
- # Identify dominant & second-dominant bands
824
- top_bands = band_counts.nlargest(2)
825
- dominant_band = top_bands.index[0] if len(top_bands) > 0 else "N/A"
826
- second_dominant_band = top_bands.index[1] if len(top_bands) > 1 else "N/A"
827
 
828
- dominant_pct = (top_bands.iloc[0] / band_counts.sum() * 100) if len(top_bands) > 0 else 0
829
- second_pct = (top_bands.iloc[1] / band_counts.sum() * 100) if len(top_bands) > 1 else 0
 
 
 
 
 
 
 
830
 
831
- # Front vs Rear alarm share (hanya pos 1-2 vs 3-4)
832
- front_alarms = dff_obj2[(dff_obj2['is_alarm'] == 1) & (dff_obj2['Position'].isin([1, 2]))].shape[0]
833
- rear_alarms = dff_obj2[(dff_obj2['is_alarm'] == 1) & (dff_obj2['Position'].isin([3, 4]))].shape[0]
834
- total_alarms = front_alarms + rear_alarms
835
- front_pct = front_alarms / total_alarms * 100 if total_alarms > 0 else 0
 
 
 
836
 
837
- # Top zone
838
- top_zone = dff_obj2[dff_obj2['is_alarm'] == 1]['Zone'].value_counts().index[0] if not dff_obj2[dff_obj2['is_alarm'] == 1].empty else "N/A"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
839
 
840
- # Build insight bullets
841
- insight_lines = [
842
- f"• {dominant_band} is the dominant alarm period ({dominant_pct:.1f}% of all alarms).",
843
- f"• {second_dominant_band} is the second-highest period ({second_pct:.1f}% of alarms)."
844
- ]
845
- if front_alarms > 0:
846
- insight_lines.append(f"• Front tyres (Pos 1 & 2) account for {front_pct:.1f}% of all alarms, indicating higher stress or usage intensity upfront.")
847
- if top_zone != "N/A":
848
- insight_lines.append(f"• Zone {top_zone} records the highest alarm frequency across all positions.")
849
- insight_lines.append("• Alarm clustering in specific hours and front positions suggests opportunity for targeted inspection scheduling.")
850
-
851
- insight_text = "\n".join(insight_lines)
852
-
853
- # --- Display Insight Box ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
854
  st.markdown(f"""
855
  <div class="insight-box">
856
  <div class="content">
857
- {insight_text}
858
  </div>
859
  </div>
860
  """, unsafe_allow_html=True)
861
  # ================= OBJECTIVE 4 =================
862
-
863
  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)
864
 
865
  # Prepare data
@@ -868,20 +932,11 @@ rear_df = dff[dff['Position'].isin([3, 4])].copy()
868
 
869
  col1, col2 = st.columns(2)
870
 
871
- # =============== COL 1: Front — Temperature → Pressure ===============
872
  with col1:
873
  st.markdown('<h5 style="text-align:center; margin-top: 0;">Front Tyres: Temperature → Pressure</h5>', unsafe_allow_html=True)
874
 
875
  if not front_df.empty:
876
- # Tambahkan kategori alarm status
877
- front_df['Category'] = front_df.apply(
878
- lambda row: f"Normal Front Tyre" if row['Alarm Status'] == 'No Alarm'
879
- else f"Amber Pressure Front Tyre" if 'Amber' in row['Alarm Status']
880
- else f"Red Pressure Front Tyre", axis=1
881
- )
882
- categories = ["Normal Front Tyre", "Amber Pressure Front Tyre", "Red Pressure Front Tyre"]
883
- front_df['Category'] = pd.Categorical(front_df['Category'], categories=categories, ordered=True)
884
-
885
  # Filter valid data
886
  valid_data = front_df.dropna(subset=['Temperature (°C)', 'Pressure (psi)'])
887
  if len(valid_data) > 1:
@@ -892,30 +947,42 @@ with col1:
892
  y_line = model.predict(x_line)
893
  corr = np.corrcoef(valid_data['Temperature (°C)'], valid_data['Pressure (psi)'])[0, 1]
894
 
 
895
  fig1 = px.scatter(
896
  valid_data,
897
  x='Temperature (°C)',
898
  y='Pressure (psi)',
899
- color='Category',
900
- color_discrete_map={
901
- "Normal Front Tyre": "#2E7D32", # Hijau
902
- "Amber Pressure Front Tyre": "#FFC107", # Kuning
903
- "Red Pressure Front Tyre": "#D32F2F" # Merah
904
- },
905
- category_orders={'Category': categories},
906
  template="plotly_white",
907
  labels={'Temperature (°C)': 'Temperature (°C)', 'Pressure (psi)': 'Pressure (psi)'}
908
  )
909
 
910
- fig1.update_traces(
911
- hovertemplate="<b>%{marker.color}</b><br>Temp: %{x:.1f}°C<br>Pressure: %{y:.1f} psi<extra></extra>",
912
- marker=dict(size=6)
913
- )
914
-
915
  fig1.add_trace(go.Scatter(
916
  x=x_line.flatten(), y=y_line,
917
- mode='lines', name='Trend Line',
918
- line=dict(color='#1976D2', dash='dot', width=2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
919
  ))
920
 
921
  fig1.update_layout(
@@ -933,13 +1000,14 @@ with col1:
933
  )
934
  ],
935
  legend=dict(
936
- title_text='Tyre Status',
937
  bgcolor="white",
938
  bordercolor="lightgray",
939
  borderwidth=1,
940
  itemclick=False,
941
  itemdoubleclick=False
942
- )
 
943
  )
944
  st.plotly_chart(fig1, use_container_width=True)
945
  else:
@@ -952,45 +1020,27 @@ with col2:
952
  st.markdown('<h5 style="text-align:center; margin-top: 0;">Front Tyres: Temperature / Speed (Boxplot)</h5>', unsafe_allow_html=True)
953
 
954
  if not front_df.empty:
955
- # Tambahkan kategori alarm status
956
- front_df['Category'] = front_df.apply(
957
- lambda row: f"Normal Front Tyre" if row['Alarm Status'] == 'No Alarm'
958
- else f"Amber Pressure Front Tyre" if 'Amber' in row['Alarm Status']
959
- else f"Red Pressure Front Tyre", axis=1
960
- )
961
- categories = ["Normal Front Tyre", "Amber Pressure Front Tyre", "Red Pressure Front Tyre"]
962
- front_df['Category'] = pd.Categorical(front_df['Category'], categories=categories, ordered=True)
963
-
964
- # Hitung rasio suhu/kecepatan (hindari pembagian dengan nol)
965
  front_df['Temp_Speed_Ratio'] = front_df['Temperature (°C)'] / (front_df['Speed (km/h)'] + 1e-6)
966
 
967
- valid_data = front_df.dropna(subset=['Temp_Speed_Ratio', 'Category'])
968
  if not valid_data.empty:
969
  fig2 = px.box(
970
  valid_data,
971
- x='Category',
972
  y='Temp_Speed_Ratio',
973
- color='Category',
974
- color_discrete_map={
975
- "Normal Front Tyre": "#2E7D32",
976
- "Amber Pressure Front Tyre": "#FFC107",
977
- "Red Pressure Front Tyre": "#D32F2F"
978
- },
979
- category_orders={'Category': categories},
980
  template="plotly_white",
981
- labels={'Temp_Speed_Ratio': 'Temperature / Speed', 'Category': 'Tyre Status'}
 
 
 
 
 
982
  )
983
 
984
  fig2.update_layout(
985
  margin=dict(t=40),
986
- legend=dict(
987
- title_text='Tyre Status',
988
- bgcolor="white",
989
- bordercolor="lightgray",
990
- borderwidth=1,
991
- itemclick=False,
992
- itemdoubleclick=False
993
- )
994
  )
995
  st.plotly_chart(fig2, use_container_width=True)
996
  else:
@@ -998,21 +1048,13 @@ with col2:
998
  else:
999
  st.warning("No front tyre data.")
1000
 
1001
- # =============== COL 3: Rear — Temperature → Pressure ===============
1002
  col3, col4 = st.columns(2)
1003
 
1004
  with col3:
1005
  st.markdown('<h5 style="text-align:center; margin-top: 0;">Rear Tyres: Temperature → Pressure</h5>', unsafe_allow_html=True)
1006
 
1007
  if not rear_df.empty:
1008
- rear_df['Category'] = rear_df.apply(
1009
- lambda row: f"Normal Rear Tyre" if row['Alarm Status'] == 'No Alarm'
1010
- else f"Amber Pressure Rear Tyre" if 'Amber' in row['Alarm Status']
1011
- else f"Red Pressure Rear Tyre", axis=1
1012
- )
1013
- categories = ["Normal Rear Tyre", "Amber Pressure Rear Tyre", "Red Pressure Rear Tyre"]
1014
- rear_df['Category'] = pd.Categorical(rear_df['Category'], categories=categories, ordered=True)
1015
-
1016
  valid_data = rear_df.dropna(subset=['Temperature (°C)', 'Pressure (psi)'])
1017
  if len(valid_data) > 1:
1018
  X = valid_data[['Temperature (°C)']]
@@ -1026,25 +1068,34 @@ with col3:
1026
  valid_data,
1027
  x='Temperature (°C)',
1028
  y='Pressure (psi)',
1029
- color='Category',
1030
- color_discrete_map={
1031
- "Normal Rear Tyre": "#2E7D32",
1032
- "Amber Pressure Rear Tyre": "#FFC107",
1033
- "Red Pressure Rear Tyre": "#D32F2F"
1034
- },
1035
- category_orders={'Category': categories},
1036
- template="plotly_white"
1037
- )
1038
-
1039
- fig3.update_traces(
1040
- hovertemplate="<b>%{marker.color}</b><br>Temp: %{x:.1f}°C<br>Pressure: %{y:.1f} psi<extra></extra>",
1041
- marker=dict(size=6)
1042
  )
1043
 
1044
  fig3.add_trace(go.Scatter(
1045
  x=x_line.flatten(), y=y_line,
1046
- mode='lines', name='Trend Line',
1047
- line=dict(color='#1976D2', dash='dot', width=2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1048
  ))
1049
 
1050
  fig3.update_layout(
@@ -1062,13 +1113,14 @@ with col3:
1062
  )
1063
  ],
1064
  legend=dict(
1065
- title_text='Tyre Status',
1066
  bgcolor="white",
1067
  bordercolor="lightgray",
1068
  borderwidth=1,
1069
  itemclick=False,
1070
  itemdoubleclick=False
1071
- )
 
1072
  )
1073
  st.plotly_chart(fig3, use_container_width=True)
1074
  else:
@@ -1081,44 +1133,26 @@ with col4:
1081
  st.markdown('<h5 style="text-align:center; margin-top: 0;">Rear Tyres: Temperature / Speed (Boxplot)</h5>', unsafe_allow_html=True)
1082
 
1083
  if not rear_df.empty:
1084
- rear_df['Category'] = rear_df.apply(
1085
- lambda row: f"Normal Rear Tyre" if row['Alarm Status'] == 'No Alarm'
1086
- else f"Amber Pressure Rear Tyre" if 'Amber' in row['Alarm Status']
1087
- else f"Red Pressure Rear Tyre", axis=1
1088
- )
1089
- categories = ["Normal Rear Tyre", "Amber Pressure Rear Tyre", "Red Pressure Rear Tyre"]
1090
- rear_df['Category'] = pd.Categorical(rear_df['Category'], categories=categories, ordered=True)
1091
-
1092
- # Hitung rasio suhu/kecepatan (hindari pembagian dengan nol)
1093
  rear_df['Temp_Speed_Ratio'] = rear_df['Temperature (°C)'] / (rear_df['Speed (km/h)'] + 1e-6)
1094
 
1095
- valid_data = rear_df.dropna(subset=['Temp_Speed_Ratio', 'Category'])
1096
  if not valid_data.empty:
1097
  fig4 = px.box(
1098
  valid_data,
1099
- x='Category',
1100
  y='Temp_Speed_Ratio',
1101
- color='Category',
1102
- color_discrete_map={
1103
- "Normal Rear Tyre": "#2E7D32",
1104
- "Amber Pressure Rear Tyre": "#FFC107",
1105
- "Red Pressure Rear Tyre": "#D32F2F"
1106
- },
1107
- category_orders={'Category': categories},
1108
  template="plotly_white",
1109
- labels={'Temp_Speed_Ratio': 'Temperature / Speed', 'Category': 'Tyre Status'}
 
 
 
 
 
1110
  )
1111
 
1112
  fig4.update_layout(
1113
  margin=dict(t=40),
1114
- legend=dict(
1115
- title_text='Tyre Status',
1116
- bgcolor="white",
1117
- bordercolor="lightgray",
1118
- borderwidth=1,
1119
- itemclick=False,
1120
- itemdoubleclick=False
1121
- )
1122
  )
1123
  st.plotly_chart(fig4, use_container_width=True)
1124
  else:
@@ -1127,7 +1161,6 @@ with col4:
1127
  st.warning("No rear tyre data.")
1128
 
1129
  # =============== INSIGHT 4 ===============
1130
- # Insight tetap sama, karena hanya menggabungkan data dari semua chart
1131
  def safe_corr(a, b):
1132
  mask = ~(np.isnan(a) | np.isnan(b))
1133
  if mask.sum() < 2:
 
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
 
932
 
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
  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
  )
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:
 
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:
1028
  fig2 = px.box(
1029
  valid_data,
 
1030
  y='Temp_Speed_Ratio',
 
 
 
 
 
 
 
1031
  template="plotly_white",
1032
+ labels={'Temp_Speed_Ratio': 'Temperature / Speed'}
1033
+ )
1034
+
1035
+ fig2.update_traces(
1036
+ marker_color='#003DA5',
1037
+ name='Front Tyres'
1038
  )
1039
 
1040
  fig2.update_layout(
1041
  margin=dict(t=40),
1042
+ yaxis_title='Temperature / Speed',
1043
+ showlegend=False
 
 
 
 
 
 
1044
  )
1045
  st.plotly_chart(fig2, use_container_width=True)
1046
  else:
 
1048
  else:
1049
  st.warning("No front tyre data.")
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
  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
  )
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
  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:
1140
  fig4 = px.box(
1141
  valid_data,
 
1142
  y='Temp_Speed_Ratio',
 
 
 
 
 
 
 
1143
  template="plotly_white",
1144
+ labels={'Temp_Speed_Ratio': 'Temperature / Speed'}
1145
+ )
1146
+
1147
+ fig4.update_traces(
1148
+ marker_color='#FFB300',
1149
+ name='Rear Tyres'
1150
  )
1151
 
1152
  fig4.update_layout(
1153
  margin=dict(t=40),
1154
+ yaxis_title='Temperature / Speed',
1155
+ showlegend=False
 
 
 
 
 
 
1156
  )
1157
  st.plotly_chart(fig4, use_container_width=True)
1158
  else:
 
1161
  st.warning("No rear tyre data.")
1162
 
1163
  # =============== INSIGHT 4 ===============
 
1164
  def safe_corr(a, b):
1165
  mask = ~(np.isnan(a) | np.isnan(b))
1166
  if mask.sum() < 2: