Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import pandas as pd | |
| import numpy as np | |
| import plotly.express as px | |
| import plotly.graph_objects as go | |
| from plotly.subplots import make_subplots | |
| import folium | |
| from streamlit_folium import st_folium | |
| from sklearn.linear_model import LinearRegression | |
| # ================= CONFIG | |
| st.set_page_config( | |
| page_title="Michelin Mining Tyre Analytics", | |
| page_icon="", | |
| layout="wide", | |
| initial_sidebar_state="expanded" | |
| ) | |
| # ================= CUSTOM CSS | |
| st.markdown(""" | |
| <style> | |
| /* ================= ROOT & COLORS ================= */ | |
| :root { | |
| --michelin-blue: #003A8F; | |
| --surface: #FFFFFF; | |
| --surface-alt: #F9FAFB; | |
| --text-dark: #1F2D3D; | |
| --text-muted: #6C757D; | |
| --border: #E9ECEF; | |
| --shadow-sm: 0 2px 6px rgba(0,0,0,0.04); | |
| --shadow: 0 6px 16px rgba(0,0,0,0.06); | |
| --accent-yellow: #FFD100; | |
| --filter-bg: #F5F7FA; | |
| } | |
| /* ================= GLOBAL TYPOGRAPHY & ALIGNMENT ================= */ | |
| .stApp { | |
| background-color: var(--surface); | |
| font-family: 'Segoe UI', system-ui, sans-serif; | |
| color: var(--text-dark); | |
| } | |
| /* Force center alignment for ALL headings */ | |
| h1, h2, h3, h4, h5, h6, | |
| .stMarkdown, .stText, p, div[data-testid="stMarkdownContainer"], | |
| label, .stSelectbox label, .stMultiselect label, .stCheckbox label { | |
| text-align: center !important; | |
| } | |
| /* Fix bullet/number list centering */ | |
| .stMarkdown ul, .stMarkdown ol { | |
| text-align: left !important; | |
| margin-left: auto; | |
| margin-right: auto; | |
| max-width: 800px; | |
| } | |
| /* ================= SIDEBAR (Power BI Style) ================= */ | |
| [data-testid="stSidebar"] { | |
| background: var(--filter-bg) !important; | |
| color: var(--text-dark); | |
| padding: 20px 12px; | |
| } | |
| [data-testid="stSidebar"] h3 { | |
| color: var(--michelin-blue); | |
| font-weight: 700; | |
| margin-bottom: 12px; | |
| } | |
| /* Power BI-style dropdowns */ | |
| [data-testid="stSelectbox"] div[data-baseweb="select"], | |
| [data-testid="stMultiselect"] div[data-baseweb="select"] { | |
| background-color: white !important; | |
| border-radius: 8px !important; | |
| border: 1px solid var(--border) !important; | |
| box-shadow: var(--shadow-sm); | |
| padding: 6px 10px !important; | |
| min-height: 40px !important; | |
| } | |
| [data-testid="stSelectbox"] div[data-baseweb="select"] > div, | |
| [data-testid="stMultiselect"] div[data-baseweb="select"] > div { | |
| color: var(--text-dark) !important; | |
| font-weight: 500; | |
| } | |
| /* Remove red tags from multiselect */ | |
| [data-testid="stMultiselect"] div[data-baseweb="select"] .stMultiSelectTag { | |
| display: none !important; | |
| } | |
| /* Submit button */ | |
| [data-testid="stSidebar"] .stButton > button { | |
| width: 100%; | |
| background: var(--accent-yellow); | |
| color: var(--michelin-blue); | |
| font-weight: 700; | |
| border-radius: 10px; | |
| padding: 12px 0; | |
| margin-top: 16px; | |
| box-shadow: var(--shadow); | |
| border: none; | |
| font-size: 1.05rem; | |
| } | |
| [data-testid="stSidebar"] .stButton > button:hover { | |
| background: #FFC107; | |
| transform: translateY(-1px); | |
| box-shadow: 0 8px 16px rgba(0,0,0,0.12); | |
| } | |
| /* ================= HEADER ================= */ | |
| .main-header h1 { | |
| font-size: 2.4rem; | |
| margin-bottom: 6px; | |
| font-weight: 800; | |
| color: var(--michelin-blue); | |
| } | |
| .main-header p { | |
| font-size: 1.15rem; | |
| color: var(--text-muted); | |
| margin-top: 0; | |
| } | |
| /* ================= OBJECTIVE TITLE (NO BACKGROUND BOX) ================= */ | |
| .objective-title { | |
| text-align: center !important; | |
| font-size: 1.6rem; | |
| font-weight: 800; | |
| color: var(--michelin-blue); | |
| margin: 40px 0 24px 0; | |
| } | |
| /* ================= INSIGHT LLM-STYLE (Like Screenshot) ================= */ | |
| .insight-box { | |
| background: var(--surface-alt); | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| padding: 20px; | |
| box-shadow: var(--shadow-sm); | |
| margin: 20px 0 30px 0; | |
| position: relative; | |
| display: flex; | |
| align-items: flex-start; | |
| gap: 12px; | |
| } | |
| .insight-box .content { | |
| flex: 1; | |
| font-size: 1.05rem; | |
| line-height: 1.65; | |
| color: var(--text-dark); | |
| text-align: left; | |
| } | |
| .insight-box .tag { | |
| position: absolute; | |
| top: 12px; | |
| right: 16px; | |
| background: var(--michelin-blue); | |
| color: white; | |
| font-size: 0.85rem; | |
| font-weight: 700; | |
| padding: 6px 12px; | |
| border-radius: 8px; | |
| letter-spacing: 0.5px; | |
| } | |
| /* ================= PLOTLY ================= */ | |
| .plotly-graph-div { | |
| border-radius: 12px; | |
| overflow: hidden; | |
| box-shadow: var(--shadow-sm); | |
| border: 1px solid var(--border); | |
| } | |
| /* ================= LOGO (Perbaikan: PLN Style - Header Right) ================= */ | |
| .logo-pln { | |
| display: flex; | |
| align-items: center; | |
| justify-content: flex-end; | |
| width: 150px; | |
| height: auto; | |
| margin-left: auto; | |
| } | |
| /* ================= FOOTER ================= */ | |
| .footer { | |
| text-align: center; | |
| font-size: 0.9rem; | |
| color: var(--text-muted); | |
| margin-top: 50px; | |
| padding: 20px 0; | |
| border-top: 1px solid var(--border); | |
| } | |
| /* ================= STREAMLIT TWEAKS ================= */ | |
| div.block-container { | |
| padding-top: 2rem; | |
| } | |
| section[data-testid="stSidebar"] { | |
| width: 280px !important; | |
| min-width: 280px !important; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # ================= LOAD | |
| def load_data(): | |
| try: | |
| df = pd.read_excel("df_final.xlsx", sheet_name="Sheet1") | |
| except FileNotFoundError: | |
| st.error("❌ File `df_final.xlsx` not found. Please ensure it's in the same directory.") | |
| st.stop() | |
| # Fix encoding (e.g., '°C' → '°C') | |
| df.columns = df.columns.str.replace("Â", "") | |
| for col in df.select_dtypes(include='object').columns: | |
| df[col] = df[col].astype(str).str.replace("Â", "") | |
| # Parse datetime | |
| df['Time'] = pd.to_datetime(df['Time'], errors='coerce') | |
| df = df.dropna(subset=['Time']) | |
| df['hour'] = df['Time'].dt.hour | |
| # Alarm flag | |
| df['is_alarm'] = (~df['Alarm Status'].str.contains('No Alarm', na=False)).astype(int) | |
| # Dynamic risk score | |
| p = df['Pressure (psi)'] | |
| p_red_high = df['Red High Press (psi)'] | |
| p_amber_high = df['Amber High Press (psi)'] | |
| t = df['Temperature (°C)'] | |
| t_red = df['Absolute Red Temp (°C)'] | |
| t_amber = df['Absolute Amber Temp (°C)'] | |
| p_norm = np.clip((p - p_amber_high) / (p_red_high - p_amber_high), 0, 1) | |
| t_norm = np.clip((t - t_amber) / (t_red - t_amber), 0, 1) | |
| df['risk_score'] = 0.6 * p_norm + 0.4 * t_norm | |
| def get_risk_label(score): | |
| if score >= 0.8: return 'Very High Risk' | |
| elif score >= 0.6: return 'High Risk' | |
| elif score >= 0.3: return 'Moderate Risk' | |
| else: return 'Slight Risk' | |
| df['Risk Level'] = df['risk_score'].apply(get_risk_label) | |
| # Add Position Group | |
| df['Position Group'] = df['Position'].apply(lambda x: 'Front' if x in [1, 2] else 'Rear') | |
| return df | |
| df = load_data() | |
| # ================= LOGO FUNCTION (Perbaikan: Tanpa Base64, Gunakan st.image) ================= | |
| def render_pln_logo(): | |
| try: | |
| st.image("logo.png", width=120) | |
| except Exception: | |
| # Fallback: logo placeholder (putih, border halus) | |
| st.markdown('<div style="width:120px; height:40px; background:white; border:1px solid #e0e0e0; border-radius:6px; margin-left:auto;"></div>', unsafe_allow_html=True) | |
| # ================= HEADER (Perbaikan: Dengan Logo di Kanan) ================= | |
| st.markdown('<div style="display:flex; align-items:center; justify-content:space-between; margin:20px 0; padding:0 20px;">', unsafe_allow_html=True) | |
| col1, col2, col3 = st.columns([1, 5, 1]) | |
| with col1: | |
| st.write("") # left spacer | |
| with col2: | |
| st.markdown('<h1 style="color:#003A8F; font-weight:800; margin:0; text-align:center;">Michelin Mining Tyre Analytics</h1>', unsafe_allow_html=True) | |
| st.markdown('<p style="font-size:14px; color:#b0b0b0; margin-top:-10px; text-align:center;">Analysis is based on daily aggregated data</p>', unsafe_allow_html=True) | |
| with col3: | |
| render_pln_logo() # ← logo di kanan, seperti PLN | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| # Lanjutkan dengan sidebar dan objek lainnya di bawah sini... | |
| # ================= SIDEBAR FILTERS ================= | |
| with st.sidebar: | |
| st.markdown("### Filter") | |
| # Tyre Type: with 'All' option, behaves like before | |
| tyre_types = st.selectbox( | |
| "Tyre Type", options=['All'] + sorted(df['Tyre Type'].dropna().unique()), index=0 | |
| ) | |
| if tyre_types != 'All': | |
| tyre_types = [tyre_types] | |
| else: | |
| tyre_types = sorted(df['Tyre Type'].dropna().unique()) | |
| # Date: behaves like Tyre Type — show 'All' by default | |
| date_options = sorted(df['date'].astype(str).unique()) | |
| dates_selected = st.selectbox( | |
| "Date", options=['All'] + date_options, index=0 | |
| ) | |
| if dates_selected != 'All': | |
| dates = [dates_selected] | |
| else: | |
| dates = date_options | |
| # Zone: behaves like Tyre Type — show 'All' by default | |
| zone_options = sorted(df['Zone'].dropna().unique()) | |
| zones_selected = st.selectbox( | |
| "Zone", options=['All'] + zone_options, index=0 | |
| ) | |
| if zones_selected != 'All': | |
| zones = [zones_selected] | |
| else: | |
| zones = zone_options | |
| # Position: behaves like Tyre Type — show 'All' by default | |
| pos_options = sorted(df['Position'].astype(int).dropna().unique()) | |
| positions_selected = st.selectbox( | |
| "Position", options=['All'] + pos_options, index=0 | |
| ) | |
| if positions_selected != 'All': | |
| positions = [positions_selected] | |
| else: | |
| positions = pos_options | |
| # Alarm Status: behaves like Tyre Type — show 'All' by default | |
| alarm_options = ["No Alarm", "Red High Pressure"] | |
| alarms_selected = st.selectbox( | |
| "Alarm Status", options=['All'] + alarm_options, index=0 | |
| ) | |
| if alarms_selected != 'All': | |
| alarms = [alarms_selected] | |
| else: | |
| alarms = alarm_options | |
| submit = st.button("Submit") | |
| # Apply filters | |
| if submit: | |
| dff = df.copy() | |
| if dates: dff = dff[dff['date'].astype(str).isin(dates)] | |
| if zones: dff = dff[dff['Zone'].isin(zones)] | |
| if positions: dff = dff[dff['Position'].astype(int).isin(positions)] | |
| if tyre_types: dff = dff[dff['Tyre Type'].isin(tyre_types)] | |
| if alarms: dff = dff[dff['Alarm Status'].isin(alarms)] | |
| else: | |
| dff = df | |
| # ================= OBJECTIVE 1 ================= | |
| dff = dff.copy() | |
| dff['Position'] = pd.Categorical(dff['Position'], categories=[1, 2, 3, 4], ordered=True) | |
| # Optional: Use descriptive labels (if desired) | |
| position_labels = { | |
| 1: '1: Front Left', | |
| 2: '2: Front Right', | |
| 3: '3: Rear Left', | |
| 4: '4: Rear Right' | |
| } | |
| # Or keep as just '1', '2'... if minimal | |
| # position_labels = {1: '1', 2: '2', 3: '3', 4: '4'} | |
| dff['Position_Label'] = dff['Position'].map(position_labels) | |
| st.markdown('<h3 class="objective-title">OBJECTIVE 1: Pressure & Temperature Trends — How Do Front and Rear Tyres Distribute?</h3>', unsafe_allow_html=True) | |
| col1, col2 = st.columns(2) | |
| # Define consistent color mapping | |
| color_map = {1: '#d50000', 2: '#ff6d00', 3: '#ffcc00', 4: '#007acc'} | |
| category_order = [1, 2, 3, 4] | |
| with col1: | |
| st.markdown('<h5 style="text-align:center; margin-top: 0;">Pressure Distribution per Tyre Position</h5>', unsafe_allow_html=True) | |
| fig1 = px.box( | |
| dff, | |
| x='Position_Label', | |
| y='Pressure (psi)', | |
| color='Position', | |
| color_discrete_map=color_map, | |
| category_orders={'Position': category_order}, | |
| template="plotly_white", | |
| labels={'Position_Label': 'Position'} | |
| ) | |
| red_high = dff['Red High Press (psi)'].min() | |
| amber_high = dff['Amber High Press (psi)'].min() | |
| fig1.add_hline(y=red_high, line_dash="dash", line_color="red", annotation_text="Red High Press", annotation_position="top right") | |
| fig1.add_hline(y=amber_high, line_color="orange", annotation_text="Amber High Press", annotation_position="bottom right") | |
| fig1.update_layout( | |
| margin=dict(t=40), | |
| legend_title_text='Position', | |
| legend=dict( | |
| yanchor="top", | |
| y=0.99, | |
| xanchor="left", | |
| x=1.02 # Place legend outside plot to avoid overlap | |
| ) | |
| ) | |
| st.plotly_chart(fig1, use_container_width=True) | |
| with col2: | |
| st.markdown('<h5 style="text-align:center; margin-top: 0;">Temperature Distribution per Tyre Position</h5>', unsafe_allow_html=True) | |
| fig2 = px.box( | |
| dff, | |
| x='Position_Label', | |
| y='Temperature (°C)', | |
| color='Position', | |
| color_discrete_map=color_map, | |
| category_orders={'Position': category_order}, | |
| template="plotly_white", | |
| labels={'Position_Label': 'Position'} | |
| ) | |
| red_temp = dff['Absolute Red Temp (°C)'].min() | |
| amber_temp = dff['Absolute Amber Temp (°C)'].min() | |
| fig2.add_hline(y=red_temp, line_dash="dash", line_color="red", annotation_text="Red Temp", annotation_position="top right") | |
| fig2.add_hline(y=amber_temp, line_color="orange", annotation_text="Amber Temp", annotation_position="bottom right") | |
| fig2.update_layout( | |
| margin=dict(t=40), | |
| legend_title_text='Position', | |
| legend=dict( | |
| yanchor="top", | |
| y=0.99, | |
| xanchor="left", | |
| x=1.02 | |
| ) | |
| ) | |
| st.plotly_chart(fig2, use_container_width=True) | |
| # Insight 1 | |
| # Analisis data untuk menentukan pola | |
| front_pressure_avg = dff[dff['Position'].isin([1, 2])]['Pressure (psi)'].mean() | |
| rear_pressure_avg = dff[dff['Position'].isin([3, 4])]['Pressure (psi)'].mean() | |
| front_temp_avg = dff[dff['Position'].isin([1, 2])]['Temperature (°C)'].mean() | |
| rear_temp_avg = dff[dff['Position'].isin([3, 4])]['Temperature (°C)'].mean() | |
| if front_pressure_avg < rear_pressure_avg and front_temp_avg > rear_temp_avg: | |
| insight_text = f""" | |
| Front tyres (Pos 1 & 2): Higher average pressure and temperature ({front_pressure_avg:.1f} psi, {front_temp_avg:.1f}°C) indicate higher loading and heat generation. Rear tyres (Pos 3 & 4): Lower average temperature ({rear_temp_avg:.1f}°C) suggests lighter effective loading. | |
| """ | |
| elif front_pressure_avg > rear_pressure_avg and front_temp_avg < rear_temp_avg: | |
| insight_text = f""" | |
| Front tyres (Pos 1 & 2): Average pressure {front_pressure_avg:.1f} psi and average temperature {front_temp_avg:.1f}°C show lower heat levels compared to the rear tyres. Rear tyres (Pos 3 & 4): Average pressure {rear_pressure_avg:.1f} psi and average temperature {rear_temp_avg:.1f}°C show higher heat levels. | |
| """ | |
| else: | |
| insight_text = f""" | |
| Front tyres: Pressure {front_pressure_avg:.1f} psi, temperature {front_temp_avg:.1f}°C. Rear tyres: Pressure {rear_pressure_avg:.1f} psi, temperature {rear_temp_avg:.1f}°C. | |
| """ | |
| st.markdown(f""" | |
| <div class="insight-box"> | |
| <div class="content"> | |
| {insight_text.strip()} | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # ================= OBJECTIVE 2 ================= | |
| st.markdown('<h3 class="objective-title">OBJECTIVE 2: Alarm Frequency Analysis — When, Where, and Which Tyres Matter Most?</h3>', unsafe_allow_html=True) | |
| col_a, col_b = st.columns(2) | |
| # --- COL A: Alarm Distribution by Hour (Polar Chart) --- | |
| with col_a: | |
| st.markdown('<h5 style="text-align:center; margin-top: 0;">Alarm Distribution by Hour</h5>', unsafe_allow_html=True) | |
| alarm_hour_pos = dff[dff['is_alarm'] == 1][['hour', 'Position']].copy() | |
| if alarm_hour_pos.empty: | |
| st.warning("No alarm data to display.") | |
| else: | |
| # Hitung alarm per jam & posisi | |
| hourly_pos_counts = alarm_hour_pos.groupby(['hour', 'Position']).size().unstack(fill_value=0) | |
| positions = sorted([p for p in [1, 2, 3, 4] if p in hourly_pos_counts.columns]) # enforce 1-4 order | |
| color_map = {1: '#d50000', 2: '#ff6d00', 3: '#ffcc00', 4: '#007acc'} | |
| fig_polar = go.Figure() | |
| max_r = max(hourly_pos_counts.sum(axis=1)) * 1.05 if not hourly_pos_counts.empty else 10 | |
| for pos in positions: | |
| if pos in hourly_pos_counts.columns: | |
| counts = hourly_pos_counts[pos].reindex(range(24), fill_value=0).values | |
| theta = [h * 15 for h in range(24)] # 24 jam → 360° / 24 = 15° per jam | |
| fig_polar.add_trace(go.Barpolar( | |
| r=counts, | |
| theta=theta, | |
| width=15, | |
| name=f'Position {pos}', | |
| marker_color=color_map[pos], | |
| opacity=0.85, | |
| hovertemplate='<b>Hour:</b> %{theta:0f}:00<br><b>Alarms:</b> %{r}<extra></extra>' | |
| )) | |
| fig_polar.update_layout( | |
| polar=dict( | |
| radialaxis=dict(visible=True, range=[0, max_r], tickfont=dict(size=10)), | |
| angularaxis=dict( | |
| direction="clockwise", | |
| tickvals=[0, 90, 180, 270], | |
| ticktext=["00:00", "06:00", "12:00", "18:00"], | |
| tickfont=dict(size=11) | |
| ) | |
| ), | |
| legend=dict( | |
| title_text='Tyre Position', | |
| yanchor="top", | |
| y=0.98, | |
| xanchor="left", | |
| x=1.02, | |
| bgcolor="rgba(255,255,255,0.7)", | |
| borderwidth=0.5, | |
| itemclick=False, # prevent accidental legend toggle | |
| itemdoubleclick=False | |
| ), | |
| margin=dict(t=40, b=20, l=20, r=120), | |
| hovermode="closest" | |
| ) | |
| st.plotly_chart(fig_polar, use_container_width=True) | |
| # --- COL B: Alarm Hotspots (Front Tyres Only: Pos 1 & 2) --- | |
| with col_b: | |
| st.markdown('<h5 style="text-align:center; margin-top: 0;">Alarm Hotspots by Tyre, Position & Zone</h5>', unsafe_allow_html=True) | |
| # Filter hanya alarm di front tyres (Pos 1 & 2) | |
| front_alarm_data = dff[(dff['is_alarm'] == 1) & (dff['Position'].isin([1, 2]))].copy() | |
| if front_alarm_data.empty: | |
| st.warning("No alarm data for front tyres to display.") | |
| else: | |
| agg_data = ( | |
| front_alarm_data | |
| .groupby(['TyreSN', 'Position', 'Zone']) | |
| .size() | |
| .reset_index(name='Count') | |
| ) | |
| agg_data['Percentage'] = (agg_data['Count'] / agg_data['Count'].sum()) * 100 | |
| # Warna eksplisit untuk 1 & 2 | |
| color_map_front = {1: '#d50000', 2: '#ff6d00'} | |
| fig_bubble = px.scatter( | |
| agg_data, | |
| x='Position', | |
| y='Zone', | |
| size='Count', | |
| color='Position', | |
| color_discrete_map=color_map_front, | |
| hover_name='TyreSN', | |
| hover_data={'Position': True, 'Zone': True, 'Count': True, 'Percentage': ':.1f%'}, | |
| size_max=55, | |
| template='plotly_white', | |
| category_orders={'Position': [1, 2]} | |
| ) | |
| # Tambahkan label singkat di tengah bubble (4 digit akhir SN) | |
| fig_bubble.update_traces( | |
| text=agg_data['TyreSN'].str[-4:], | |
| textposition='middle center', | |
| textfont=dict(color='white', size=9) | |
| ) | |
| fig_bubble.update_layout( | |
| xaxis=dict( | |
| title='Position', | |
| tickmode='array', | |
| tickvals=[1, 2], | |
| ticktext=['1', '2'], | |
| tickfont=dict(size=12) | |
| ), | |
| yaxis=dict(title='Zone', tickfont=dict(size=12)), | |
| legend=dict( | |
| title_text='Tyre Position', | |
| yanchor="top", | |
| y=0.98, | |
| xanchor="left", | |
| x=1.02, | |
| bgcolor="rgba(255,255,255,0.7)", | |
| borderwidth=0.5 | |
| ), | |
| margin=dict(t=40, b=20, l=20, r=120), | |
| showlegend=True | |
| ) | |
| # Rename legend entries | |
| fig_bubble.for_each_trace(lambda t: t.update( | |
| name=f'Position {int(t.name)}' | |
| )) | |
| st.plotly_chart(fig_bubble, use_container_width=True) | |
| # --- INSIGHT 2: Actionable, Numeric, Time-Group Based --- | |
| alarm_hours = dff[dff['is_alarm'] == 1]['hour'] | |
| if alarm_hours.empty: | |
| insight_text = "• No alarm data available for analysis." | |
| else: | |
| # Group hours into time bands | |
| def hour_to_band(h): | |
| if 0 <= h < 6: return "00:00–06:00 (Night)" | |
| if 6 <= h < 12: return "06:00–12:00 (Morning)" | |
| if 12 <= h < 18: return "12:00–18:00 (Afternoon)" | |
| return "18:00–00:00 (Evening)" | |
| alarm_hours_df = pd.DataFrame({'hour': alarm_hours}) | |
| alarm_hours_df['band'] = alarm_hours_df['hour'].apply(hour_to_band) | |
| band_counts = alarm_hours_df['band'].value_counts().sort_index() # sort by natural order | |
| # Identify dominant & second-dominant bands | |
| top_bands = band_counts.nlargest(2) | |
| dominant_band = top_bands.index[0] if len(top_bands) > 0 else "N/A" | |
| second_dominant_band = top_bands.index[1] if len(top_bands) > 1 else "N/A" | |
| dominant_pct = (top_bands.iloc[0] / band_counts.sum() * 100) if len(top_bands) > 0 else 0 | |
| second_pct = (top_bands.iloc[1] / band_counts.sum() * 100) if len(top_bands) > 1 else 0 | |
| # Front vs Rear alarm share | |
| front_alarms = dff[(dff['is_alarm'] == 1) & (dff['Position'].isin([1, 2]))].shape[0] | |
| rear_alarms = dff[(dff['is_alarm'] == 1) & (dff['Position'].isin([3, 4]))].shape[0] | |
| total_alarms = front_alarms + rear_alarms | |
| front_pct = front_alarms / total_alarms * 100 if total_alarms > 0 else 0 | |
| # Top zone | |
| top_zone = dff[dff['is_alarm'] == 1]['Zone'].value_counts().index[0] if not dff[dff['is_alarm'] == 1].empty else "N/A" | |
| # Build insight bullets | |
| insight_lines = [ | |
| f"{dominant_band} is the dominant alarm period ({dominant_pct:.1f}% of all alarms).", | |
| f"{second_dominant_band} is the second-highest period ({second_pct:.1f}% of alarms)." | |
| ] | |
| if front_alarms > 0: | |
| insight_lines.append(f"Front tyres (Pos 1 & 2) account for {front_pct:.1f}% of all alarms, indicating higher stress or usage intensity upfront.") | |
| if top_zone != "N/A": | |
| insight_lines.append(f"Zone {top_zone} records the highest alarm frequency across all positions.") | |
| insight_lines.append("• Alarm clustering in specific hours and front positions suggests opportunity for targeted inspection scheduling.") | |
| insight_text = "\n".join(insight_lines) | |
| # --- Display Insight Box --- | |
| st.markdown(f""" | |
| <div class="insight-box"> | |
| <div class="content"> | |
| {insight_text} | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # ================= OBJECTIVE 3 ================= | |
| 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) | |
| # Prepare data | |
| front_df = dff[dff['Position'].isin([1, 2])].copy() | |
| rear_df = dff[dff['Position'].isin([3, 4])].copy() | |
| col1, col2 = st.columns(2) | |
| # =============== COL 1: Front — Temperature → Pressure =============== | |
| with col1: | |
| st.markdown('<h5 style="text-align:center; margin-top: 0;">Front Tyres: Temperature → Pressure</h5>', unsafe_allow_html=True) | |
| if not front_df.empty: | |
| front_df['Category'] = front_df.apply( | |
| lambda row: f"{'Normal' if row['Alarm Status'] == 'No Alarm' else 'Red Pressure'} Front Tyre", axis=1 | |
| ) | |
| categories = ["Normal Front Tyre", "Red Pressure Front Tyre"] | |
| front_df['Category'] = pd.Categorical(front_df['Category'], categories=categories, ordered=True) | |
| # Filter valid data | |
| valid_data = front_df.dropna(subset=['Temperature (°C)', 'Pressure (psi)']) | |
| if len(valid_data) > 1: | |
| X = valid_data[['Temperature (°C)']] | |
| y = valid_data['Pressure (psi)'] | |
| model = LinearRegression().fit(X, y) | |
| x_line = np.linspace(X.min(), X.max(), 100).reshape(-1, 1) | |
| y_line = model.predict(x_line) | |
| corr = np.corrcoef(valid_data['Temperature (°C)'], valid_data['Pressure (psi)'])[0, 1] | |
| fig1 = px.scatter( | |
| valid_data, | |
| x='Temperature (°C)', | |
| y='Pressure (psi)', | |
| color='Category', | |
| color_discrete_map={ | |
| "Normal Front Tyre": "#2E7D32", | |
| "Red Pressure Front Tyre": "#D32F2F" | |
| }, | |
| category_orders={'Category': categories}, | |
| template="plotly_white", | |
| labels={'Temperature (°C)': 'Temperature (°C)', 'Pressure (psi)': 'Pressure (psi)'} | |
| ) | |
| fig1.update_traces( | |
| hovertemplate="<b>%{marker.color}</b><br>Temp: %{x:.1f}°C<br>Pressure: %{y:.1f} psi<extra></extra>", | |
| marker=dict(size=6) | |
| ) | |
| fig1.add_trace(go.Scatter( | |
| x=x_line.flatten(), y=y_line, | |
| mode='lines', name='Trend Line', | |
| line=dict(color='#1976D2', dash='dot', width=2) | |
| )) | |
| fig1.update_layout( | |
| margin=dict(t=40), | |
| annotations=[ | |
| dict( | |
| x=0.95, y=0.95, | |
| xref="paper", yref="paper", | |
| text=f"r = {corr:.2f}", | |
| showarrow=False, | |
| bgcolor="white", | |
| bordercolor="black", | |
| borderwidth=1, | |
| font=dict(color="black") | |
| ) | |
| ], | |
| legend=dict( | |
| title_text='Tyre Status', | |
| bgcolor="white", | |
| bordercolor="lightgray", | |
| borderwidth=1, | |
| itemclick=False, | |
| itemdoubleclick=False | |
| ) | |
| ) | |
| st.plotly_chart(fig1, use_container_width=True) | |
| else: | |
| st.warning("Insufficient data for front tyres.") | |
| else: | |
| st.warning("No front tyre data.") | |
| # =============== COL 2: Front — Temperature vs Speed =============== | |
| with col2: | |
| st.markdown('<h5 style="text-align:center; margin-top: 0;">Front Tyres: Temperature → Speed</h5>', unsafe_allow_html=True) | |
| if not front_df.empty: | |
| front_df['Category'] = front_df.apply( | |
| lambda row: f"{'Normal' if row['Alarm Status'] == 'No Alarm' else 'Red Pressure'} Front Tyre", axis=1 | |
| ) | |
| categories = ["Normal Front Tyre", "Red Pressure Front Tyre"] | |
| front_df['Category'] = pd.Categorical(front_df['Category'], categories=categories, ordered=True) | |
| valid_data = front_df.dropna(subset=['Temperature (°C)', 'Speed (km/h)']) | |
| if len(valid_data) > 1: | |
| X = valid_data[['Temperature (°C)']] | |
| y = valid_data['Speed (km/h)'] | |
| model = LinearRegression().fit(X, y) | |
| x_line = np.linspace(X.min(), X.max(), 100).reshape(-1, 1) | |
| y_line = model.predict(x_line) | |
| corr = np.corrcoef(valid_data['Temperature (°C)'], valid_data['Speed (km/h)'])[0, 1] | |
| fig2 = px.scatter( | |
| valid_data, | |
| x='Temperature (°C)', | |
| y='Speed (km/h)', | |
| color='Category', | |
| color_discrete_map={ | |
| "Normal Front Tyre": "#2E7D32", | |
| "Red Pressure Front Tyre": "#D32F2F" | |
| }, | |
| category_orders={'Category': categories}, | |
| template="plotly_white" | |
| ) | |
| fig2.update_traces( | |
| hovertemplate="<b>%{marker.color}</b><br>Temp: %{x:.1f}°C<br>Speed: %{y:.1f} km/h<extra></extra>", | |
| marker=dict(size=6) | |
| ) | |
| fig2.add_trace(go.Scatter( | |
| x=x_line.flatten(), y=y_line, | |
| mode='lines', name='Trend Line', | |
| line=dict(color='#1976D2', dash='dot', width=2) | |
| )) | |
| fig2.update_layout( | |
| margin=dict(t=40), | |
| annotations=[ | |
| dict( | |
| x=0.95, y=0.95, | |
| xref="paper", yref="paper", | |
| text=f"r = {corr:.2f}", | |
| showarrow=False, | |
| bgcolor="white", | |
| bordercolor="black", | |
| borderwidth=1, | |
| font=dict(color="black") | |
| ) | |
| ], | |
| legend=dict( | |
| title_text='Tyre Status', | |
| bgcolor="white", | |
| bordercolor="lightgray", | |
| borderwidth=1, | |
| itemclick=False, | |
| itemdoubleclick=False | |
| ) | |
| ) | |
| st.plotly_chart(fig2, use_container_width=True) | |
| else: | |
| st.warning("Insufficient data for front tyres.") | |
| else: | |
| st.warning("No front tyre data.") | |
| # =============== COL 3: Rear — Temperature → Pressure =============== | |
| col3, col4 = st.columns(2) | |
| with col3: | |
| st.markdown('<h5 style="text-align:center; margin-top: 0;">Rear Tyres: Temperature → Pressure</h5>', unsafe_allow_html=True) | |
| if not rear_df.empty: | |
| rear_df['Category'] = rear_df.apply( | |
| lambda row: f"{'Normal' if row['Alarm Status'] == 'No Alarm' else 'Red Pressure'} Rear Tyre", axis=1 | |
| ) | |
| categories = ["Normal Rear Tyre", "Red Pressure Rear Tyre"] | |
| rear_df['Category'] = pd.Categorical(rear_df['Category'], categories=categories, ordered=True) | |
| valid_data = rear_df.dropna(subset=['Temperature (°C)', 'Pressure (psi)']) | |
| if len(valid_data) > 1: | |
| X = valid_data[['Temperature (°C)']] | |
| y = valid_data['Pressure (psi)'] | |
| model = LinearRegression().fit(X, y) | |
| x_line = np.linspace(X.min(), X.max(), 100).reshape(-1, 1) | |
| y_line = model.predict(x_line) | |
| corr = np.corrcoef(valid_data['Temperature (°C)'], valid_data['Pressure (psi)'])[0, 1] | |
| fig3 = px.scatter( | |
| valid_data, | |
| x='Temperature (°C)', | |
| y='Pressure (psi)', | |
| color='Category', | |
| color_discrete_map={ | |
| "Normal Rear Tyre": "#2E7D32", | |
| "Red Pressure Rear Tyre": "#D32F2F" | |
| }, | |
| category_orders={'Category': categories}, | |
| template="plotly_white" | |
| ) | |
| fig3.update_traces( | |
| hovertemplate="<b>%{marker.color}</b><br>Temp: %{x:.1f}°C<br>Pressure: %{y:.1f} psi<extra></extra>", | |
| marker=dict(size=6) | |
| ) | |
| fig3.add_trace(go.Scatter( | |
| x=x_line.flatten(), y=y_line, | |
| mode='lines', name='Trend Line', | |
| line=dict(color='#1976D2', dash='dot', width=2) | |
| )) | |
| fig3.update_layout( | |
| margin=dict(t=40), | |
| annotations=[ | |
| dict( | |
| x=0.95, y=0.95, | |
| xref="paper", yref="paper", | |
| text=f"r = {corr:.2f}", | |
| showarrow=False, | |
| bgcolor="white", | |
| bordercolor="black", | |
| borderwidth=1, | |
| font=dict(color="black") | |
| ) | |
| ], | |
| legend=dict( | |
| title_text='Tyre Status', | |
| bgcolor="white", | |
| bordercolor="lightgray", | |
| borderwidth=1, | |
| itemclick=False, | |
| itemdoubleclick=False | |
| ) | |
| ) | |
| st.plotly_chart(fig3, use_container_width=True) | |
| else: | |
| st.warning("Insufficient data for rear tyres.") | |
| else: | |
| st.warning("No rear tyre data.") | |
| # =============== COL 4: Rear — Temperature vs Speed =============== | |
| with col4: | |
| st.markdown('<h5 style="text-align:center; margin-top: 0;">Rear Tyres: Temperature → Speed</h5>', unsafe_allow_html=True) | |
| if not rear_df.empty: | |
| rear_df['Category'] = rear_df.apply( | |
| lambda row: f"{'Normal' if row['Alarm Status'] == 'No Alarm' else 'Red Pressure'} Rear Tyre", axis=1 | |
| ) | |
| categories = ["Normal Rear Tyre", "Red Pressure Rear Tyre"] | |
| rear_df['Category'] = pd.Categorical(rear_df['Category'], categories=categories, ordered=True) | |
| valid_data = rear_df.dropna(subset=['Temperature (°C)', 'Speed (km/h)']) | |
| if len(valid_data) > 1: | |
| X = valid_data[['Temperature (°C)']] | |
| y = valid_data['Speed (km/h)'] | |
| model = LinearRegression().fit(X, y) | |
| x_line = np.linspace(X.min(), X.max(), 100).reshape(-1, 1) | |
| y_line = model.predict(x_line) | |
| corr = np.corrcoef(valid_data['Temperature (°C)'], valid_data['Speed (km/h)'])[0, 1] | |
| fig4 = px.scatter( | |
| valid_data, | |
| x='Temperature (°C)', | |
| y='Speed (km/h)', | |
| color='Category', | |
| color_discrete_map={ | |
| "Normal Rear Tyre": "#2E7D32", | |
| "Red Pressure Rear Tyre": "#D32F2F" | |
| }, | |
| category_orders={'Category': categories}, | |
| template="plotly_white" | |
| ) | |
| fig4.update_traces( | |
| hovertemplate="<b>%{marker.color}</b><br>Temp: %{x:.1f}°C<br>Speed: %{y:.1f} km/h<extra></extra>", | |
| marker=dict(size=6) | |
| ) | |
| fig4.add_trace(go.Scatter( | |
| x=x_line.flatten(), y=y_line, | |
| mode='lines', name='Trend Line', | |
| line=dict(color='#1976D2', dash='dot', width=2) | |
| )) | |
| fig4.update_layout( | |
| margin=dict(t=40), | |
| annotations=[ | |
| dict( | |
| x=0.95, y=0.95, | |
| xref="paper", yref="paper", | |
| text=f"r = {corr:.2f}", | |
| showarrow=False, | |
| bgcolor="white", | |
| bordercolor="black", | |
| borderwidth=1, | |
| font=dict(color="black") | |
| ) | |
| ], | |
| legend=dict( | |
| title_text='Tyre Status', | |
| bgcolor="white", | |
| bordercolor="lightgray", | |
| borderwidth=1, | |
| itemclick=False, | |
| itemdoubleclick=False | |
| ) | |
| ) | |
| st.plotly_chart(fig4, use_container_width=True) | |
| else: | |
| st.warning("Insufficient data for rear tyres.") | |
| else: | |
| st.warning("No rear tyre data.") | |
| # =============== INSIGHT 3 =============== | |
| # Compute correlations safely | |
| def safe_corr(a, b): | |
| mask = ~(np.isnan(a) | np.isnan(b)) | |
| if mask.sum() < 2: | |
| return 0.0 | |
| return np.corrcoef(a[mask], b[mask])[0, 1] | |
| corr_p_t_front = safe_corr(front_df['Temperature (°C)'], front_df['Pressure (psi)']) | |
| corr_t_s_front = safe_corr(front_df['Temperature (°C)'], front_df['Speed (km/h)']) | |
| corr_p_t_rear = safe_corr(rear_df['Temperature (°C)'], rear_df['Pressure (psi)']) | |
| corr_t_s_rear = safe_corr(rear_df['Temperature (°C)'], rear_df['Speed (km/h)']) | |
| insight_text = f""" | |
| 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. | |
| """ | |
| st.markdown(f""" | |
| <div class="insight-box"> | |
| <div class="content"> | |
| {insight_text.strip()} | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # ================= OBJECTIVE 4 ================= | |
| st.markdown('<h3 class="objective-title">OBJECTIVE 4: Spatial Risk Mapping — Where Do Red Pressure Alarms Occur Most Frequently?</h3>', unsafe_allow_html=True) | |
| st.markdown('<h5 style="text-align:center; margin-top: 0;">Tyre Alarms Distribution by Location</h5>', unsafe_allow_html=True) | |
| valid_gps = dff.dropna(subset=['Latitude_y', 'Longitude_y']) | |
| if valid_gps.empty: | |
| st.warning("No valid GNSS coordinates for selected filters.") | |
| else: | |
| center_lat = valid_gps['Latitude_y'].mean() | |
| center_lon = valid_gps['Longitude_y'].mean() | |
| m = folium.Map( | |
| location=[center_lat, center_lon], | |
| zoom_start=16, | |
| tiles='CartoDB positron', | |
| width='100%', | |
| height='520px' | |
| ) | |
| for _, r in valid_gps.iterrows(): | |
| color = '#D32F2F' if r['Alarm Status'] == 'Red High Pressure' else '#2E7D32' | |
| radius = 6 + (r['Temperature (°C)'] - valid_gps['Temperature (°C)'].min()) / (valid_gps['Temperature (°C)'].max() - valid_gps['Temperature (°C)'].min() + 1e-5) * 12 | |
| popup_html = f""" | |
| <div style="font-family:Segoe UI; font-size:13px; line-height:1.4"> | |
| <b>SN:</b> {r['TyreSN']} | Pos: {int(r['Position'])}<br> | |
| <b>Zone:</b> {r['Zone']}<br> | |
| <b>Press:</b> {r['Pressure (psi)']:.1f} psi<br> | |
| <b>Temp:</b> {r['Temperature (°C)']:.1f} °C<br> | |
| <b>Speed:</b> {r['Speed (km/h)']:.1f} km/h<br> | |
| <b>Alarm:</b> {r['Alarm Status']} | |
| </div> | |
| """ | |
| folium.CircleMarker( | |
| location=[r['Latitude_y'], r['Longitude_y']], | |
| radius=radius, | |
| color=color, | |
| fill=True, | |
| fill_color=color, | |
| fill_opacity=0.75, | |
| weight=1, | |
| popup=folium.Popup(popup_html, max_width=250) | |
| ).add_to(m) | |
| # Legend | |
| legend_html = ''' | |
| <div style=" | |
| position: fixed; | |
| bottom: 60px; right: 20px; | |
| background: white; | |
| border: 1px solid #E9ECEF; | |
| border-radius: 10px; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.08); | |
| padding: 12px; | |
| font-family: Segoe UI; | |
| font-size: 13px; | |
| z-index: 9999; | |
| "> | |
| <b>Legend</b><br> | |
| <span style="color:#2E7D32">●</span> Normal (No Alarm)<br> | |
| <span style="color:#D32F2F">●</span> Red Pressure<br> | |
| <span style="color:#1976D2">▲</span> Front Tyre<br> | |
| <span style="color:#1976D2">★</span> Rear Tyre<br> | |
| <i>Size ∝ Temperature</i> | |
| </div> | |
| ''' | |
| m.get_root().html.add_child(folium.Element(legend_html)) | |
| st_folium(m, width='100%', height=520, returned_objects=[]) | |
| # Insight 4 | |
| # Analisis data untuk menentukan pola spasial | |
| if not valid_gps.empty: | |
| # Hitung jumlah alarm per zona | |
| zone_counts = valid_gps[valid_gps['is_alarm'] == 1]['Zone'].value_counts() | |
| if not zone_counts.empty: | |
| top_zone = zone_counts.index[0] | |
| top_zone_count = zone_counts.iloc[0] | |
| total_alarms = valid_gps[valid_gps['is_alarm'] == 1].shape[0] | |
| percentage = (top_zone_count / total_alarms) * 100 | |
| else: | |
| top_zone = "N/A" | |
| percentage = 0 | |
| # Hitung jumlah alarm per posisi (front vs rear) | |
| front_alarms = valid_gps[(valid_gps['is_alarm'] == 1) & (valid_gps['Position'].isin([1, 2]))].shape[0] | |
| rear_alarms = valid_gps[(valid_gps['is_alarm'] == 1) & (valid_gps['Position'].isin([3, 4]))].shape[0] | |
| total_alarms = front_alarms + rear_alarms | |
| if total_alarms > 0: | |
| front_percentage = (front_alarms / total_alarms) * 100 | |
| else: | |
| front_percentage = 0 | |
| 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. | |
| """ | |
| else: | |
| insight_text = """ | |
| No valid GNSS data available for analysis. | |
| """ | |
| st.markdown(f""" | |
| <div class="insight-box"> | |
| <div class="content"> | |
| {insight_text.strip()} | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # ================= OBJECTIVE 5 ================= | |
| st.markdown('<h3 class="objective-title">OBJECTIVE 5: Insights & Mitigation — How Can Red Pressure Alarms Be Reduced?</h3>', unsafe_allow_html=True) | |
| # --- DATA PREP --- | |
| front_pressure_avg = dff[dff['Position'].isin([1, 2])]['Pressure (psi)'].mean() | |
| front_temp_avg = dff[dff['Position'].isin([1, 2])]['Temperature (°C)'].mean() | |
| hourly_counts = dff[dff['is_alarm'] == 1]['hour'].value_counts().reindex(range(24), fill_value=0) | |
| dominant_hour = hourly_counts.idxmax() if len(hourly_counts) > 0 else "N/A" | |
| total_alarms = hourly_counts.sum() | |
| dominant_percentage = (hourly_counts[dominant_hour] / total_alarms) * 100 if total_alarms > 0 else 0 | |
| zone_counts = dff[dff['is_alarm'] == 1]['Zone'].value_counts() | |
| top_zone = zone_counts.index[0] if not zone_counts.empty else "N/A" | |
| top_zone_percentage = (zone_counts.iloc[0] / total_alarms) * 100 if total_alarms > 0 else 0 | |
| # Correlation analysis | |
| front_df = dff[dff['Position'].isin([1, 2])] | |
| rear_df = dff[dff['Position'].isin([3, 4])] | |
| if not front_df.empty and len(front_df[['Pressure (psi)']].dropna()) > 1 and len(front_df[['Temperature (°C)']].dropna()) > 1: | |
| corr_front = np.corrcoef(front_df['Pressure (psi)'], front_df['Temperature (°C)'])[0,1] | |
| else: | |
| corr_front = 0 | |
| if not rear_df.empty and len(rear_df[['Speed (km/h)']].dropna()) > 1 and len(rear_df[['Temperature (°C)']].dropna()) > 1: | |
| corr_rear = np.corrcoef(rear_df['Speed (km/h)'], rear_df['Temperature (°C)'])[0,1] | |
| else: | |
| corr_rear = 0 | |
| # Insight | |
| 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 (Objective 1). | |
| <br> | |
| 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} (Objective 2). | |
| <br> | |
| 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 (Objective 3). | |
| <br> | |
| 4. {top_zone} contains {top_zone_percentage:.1f}% of all alarms, confirmed as a high-risk hotspot through GNSS data (Objective 4).""" | |
| try: | |
| import requests | |
| import json | |
| API_URL = "https://api-inference.huggingface.co/models/HuggingFaceH4/zephyr-7b-beta " | |
| prompt = f""" | |
| Role: Fleet Operations Risk Analyst | |
| Insights: | |
| - High-risk zone: {top_zone} ({top_zone_percentage:.1f}% of alarms) | |
| - Front tyres: 62% of total alarms | |
| - Peak alarm hour: {dominant_hour}:00 ({dominant_percentage:.1f}%) | |
| - Front tyre pressure–temperature correlation r = {corr_front:.2f} | |
| Task: | |
| Generate: | |
| 1. Business Recommendations | |
| 2. Risk Mitigation Actions | |
| Rules: | |
| - Use only provided insights | |
| - No root-cause speculation | |
| - Business-ready language | |
| """ | |
| payload = { | |
| "inputs": prompt, | |
| "parameters": { | |
| "max_new_tokens": 25000, | |
| "temperature": 0.8, | |
| "top_p": 0.9 | |
| } | |
| } | |
| response = requests.post(API_URL, json=payload) | |
| generated_text = response.json()[0]["generated_text"] | |
| recommendation_text = generated_text | |
| risk_mitigation_text = generated_text | |
| # Jika response kosong, gunakan versi manual | |
| if recommendation_text == "": | |
| recommendation_text = f"""1. Calibrate front tyre pressure regularly to maintain optimal {front_pressure_avg:.1f} psi and prevent over-inflation (Objective 1). | |
| <br> | |
| 2. Implement operational restrictions during peak hours ({dominant_hour}:00–{(dominant_hour+1)%24}:00) in {top_zone} to reduce alarm frequency (Objective 2). | |
| <br> | |
| 3. Monitor pressure and temperature correlation in front tyres (r = {corr_front:.2f}) to prevent overheating and premature wear (Objective 3). | |
| <br> | |
| 4. Restrict vehicle access to {top_zone} until pavement maintenance is completed, as it contributes to {top_zone_percentage:.1f}% of alarms (Objective 4).""" | |
| if risk_mitigation_text == "": | |
| risk_mitigation_text = f"""1. Adjust front tyre load distribution to reduce {front_temp_avg:.1f}°C temperature and prevent overheating (Objective 1). | |
| <br> | |
| 2. Schedule additional inspections during peak hours ({dominant_hour}:00–{(dominant_hour+1)%24}:00) when {dominant_percentage:.1f}% of alarms occur (Objective 2). | |
| <br> | |
| 3. Introduce predictive maintenance for front tyres with correlation r = {corr_front:.2f} to prevent unplanned downtime (Objective 3). | |
| <br> | |
| 4. Implement real-time monitoring in {top_zone} where {top_zone_percentage:.1f}% of alarms are concentrated (Objective 4).""" | |
| except: | |
| # Jika response dari model kosong atau gagal, gunakan versi manual | |
| recommendation_text = f"""1. Calibrate front tyre pressure regularly to maintain optimal {front_pressure_avg:.1f} psi and prevent over-inflation (Objective 1). | |
| <br> | |
| 2. Implement operational restrictions during peak hours ({dominant_hour}:00–{(dominant_hour+1)%24}:00) in {top_zone} to reduce alarm frequency (Objective 2). | |
| <br> | |
| 3. Monitor pressure and temperature correlation in front tyres (r = {corr_front:.2f}) to prevent overheating and premature wear (Objective 3). | |
| <br> | |
| 4. Restrict vehicle access to {top_zone} until pavement maintenance is completed, as it contributes to {top_zone_percentage:.1f}% of alarms (Objective 4).""" | |
| # Risk Mitigation | |
| risk_mitigation_text = f"""1. Adjust front tyre load distribution to reduce {front_temp_avg:.1f}°C temperature and prevent overheating (Objective 1). | |
| <br> | |
| 2. Schedule additional inspections during peak hours ({dominant_hour}:00–{(dominant_hour+1)%24}:00) when {dominant_percentage:.1f}% of alarms occur (Objective 2). | |
| <br> | |
| 3. Introduce predictive maintenance for front tyres with correlation r = {corr_front:.2f} to prevent unplanned downtime (Objective 3). | |
| <br> | |
| 4. Implement real-time monitoring in {top_zone} where {top_zone_percentage:.1f}% of alarms are concentrated (Objective 4).""" | |
| # ============== SUBHEADER + BOX 1: INSIGHT ============== | |
| st.markdown('<h4 style="text-align:center; margin:10px 0 5px 0; font-weight:bold;">INSIGHT</h4>', unsafe_allow_html=True) | |
| st.markdown(f""" | |
| <div class="insight-box"> | |
| <div class="content"> | |
| {insight_text.strip()} | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # ============== SUBHEADER + BOX 2: RECOMMENDATION ============== | |
| st.markdown('<h4 style="text-align:center; margin:15px 0 5px 0; font-weight:bold;">RECOMMENDATION</h4>', unsafe_allow_html=True) | |
| st.markdown(f""" | |
| <div class="insight-box"> | |
| <div class="content"> | |
| {recommendation_text.strip()} | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # ============== SUBHEADER + BOX 3: RISK MITIGATION ============== | |
| st.markdown('<h4 style="text-align:center; margin:15px 0 5px 0; font-weight:bold;">RISK MITIGATION</h4>', unsafe_allow_html=True) | |
| st.markdown(f""" | |
| <div class="insight-box"> | |
| <div class="content"> | |
| {risk_mitigation_text.strip()} | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # ================= FOOTER ================= | |
| st.markdown(""" | |
| <div class="footer"> | |
| Michelin Mining Tyre Analytics | |
| </div> | |
| """, unsafe_allow_html=True) |