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