| import streamlit as st |
| import pandas as pd |
| import plotly.express as px |
| import plotly.graph_objects as go |
| from datetime import datetime |
|
|
| if 'submitted' not in st.session_state: |
| st.session_state.submitted = False |
|
|
| st.set_page_config(page_title="Blowby Pressure & Fuel Impact Dashboard", layout="wide") |
|
|
|
|
| st.markdown(""" |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600;700&display=swap'); |
| |
| .main { |
| background: linear-gradient(135deg, #1a2a44 0%, #2c3e50 100%); |
| font-family: 'Poppins', sans-serif; |
| color: #ecf0f1; |
| padding: 20px; |
| } |
| .stButton>button { |
| background: linear-gradient(90deg, #f1c40f, #e67e22); |
| color: #1a2a44; |
| border: none; |
| border-radius: 25px; |
| padding: 12px 30px; |
| font-weight: 600; |
| transition: all 0.3s ease; |
| box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); |
| } |
| .stButton>button:hover { |
| background: linear-gradient(90deg, #e67e22, #f1c40f); |
| transform: translateY(-2px); |
| box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3); |
| } |
| .stRadio>div>label { |
| font-size: 16px; |
| color: #ecf0f1; |
| background-color: rgba(255, 255, 255, 0.1); |
| padding: 8px 15px; |
| border-radius: 15px; |
| transition: all 0.3s ease; |
| } |
| .stRadio>div>label:hover { |
| background-color: rgba(255, 255, 255, 0.2); |
| } |
| .metric-card { |
| background: rgba(255, 255, 255, 0.95); |
| padding: 20px; |
| border-radius: 15px; |
| box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15); |
| margin-bottom: 20px; |
| transition: transform 0.3s ease; |
| } |
| .metric-card:hover { |
| transform: translateY(-5px); |
| } |
| h1 { |
| color: #f1c40f; |
| font-weight: 700; |
| text-align: center; |
| font-size: 2.5em; |
| text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); |
| } |
| h2 { |
| color: #ecf0f1; |
| font-weight: 600; |
| font-size: 1.8em; |
| margin-bottom: 15px; |
| } |
| .stSelectbox { |
| background-color: rgba(255, 255, 255, 0.1); |
| border-radius: 10px; |
| padding: 5px; |
| } |
| .stSlider>div>div>div { |
| background-color: #f1c40f !important; |
| } |
| .stNumberInput input { |
| background-color: rgba(255, 255, 255, 0.1); |
| color: #ecf0f1; |
| border-radius: 10px; |
| border: 1px solid #f1c40f; |
| } |
| .thank-you-message { |
| display: flex; |
| justify-content: center; |
| align-items: center; |
| height: 80vh; |
| font-size: 2.5em; |
| font-weight: 600; |
| color: #f1c40f; |
| text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); |
| } |
| </style> |
| """, unsafe_allow_html=True) |
|
|
|
|
| def show_thank_you_message(): |
| st.markdown(""" |
| <div class="thank-you-message"> |
| Terimakasih sudah simulasi |
| </div> |
| """, unsafe_allow_html=True) |
|
|
|
|
| if st.session_state.submitted: |
| show_thank_you_message() |
| else: |
|
|
| with st.sidebar: |
| st.markdown("<h2 style='color: #f1c40f;'>โ๏ธ Dashboard Controls</h2>", unsafe_allow_html=True) |
| st.markdown("**Developed by: Bukit Technology (RnD)**") |
| st.markdown(f"**Last Updated:** {datetime.now().strftime('%Y-%m-%d')}") |
| st.markdown("---") |
| st.markdown("Use this calculator to analyze blowby pressure and fuel impact for strategic decisions.") |
|
|
|
|
| col_logo, col_title = st.columns([1, 4]) |
| with col_logo: |
| try: |
| st.image("buma ina.png", width=150) |
| except FileNotFoundError: |
| st.warning("โ ๏ธ File logo 'buma_ina.PNG' tidak ditemukan. Silakan pastikan file ada di direktori yang sama dengan script.") |
| with col_title: |
| st.title("Calculator CHA for Fuel Rate Impact") |
| |
|
|
|
|
| try: |
| detail = pd.read_csv('1_DEV_SUMMARY_detail_data_HD785_7_ENGINE_last3month.csv') |
| except FileNotFoundError: |
| st.error("โ File '1_DEV_SUMMARY_detail_data_HD785_7_ENGINE_last3month.csv' tidak ditemukan.") |
| st.stop() |
|
|
|
|
| st.subheader("๐ Pilih Unit untuk Analisis") |
| unit_list = sorted(detail['equipment_no'].dropna().unique()) |
| selected_unit = st.selectbox("Pilih unit number yang akan disimulasikan:", unit_list, help="Pilih nomor peralatan untuk analisis") |
|
|
| unit_df = detail[detail['equipment_no'] == selected_unit].sort_values("comp_life") |
| if unit_df.empty: |
| st.warning("โ ๏ธ Data tidak tersedia untuk unit ini.") |
| st.stop() |
|
|
|
|
| current_life_df = ( |
| detail[detail['solving_algorithm'].notna()] |
| .sort_values(['equipment_no', 'comp_life']) |
| .groupby('equipment_no') |
| .tail(1) |
| ) |
| if selected_unit not in current_life_df['equipment_no'].values: |
| st.error("โ Data current component life tidak tersedia untuk unit ini.") |
| st.stop() |
| current_life = current_life_df.loc[current_life_df['equipment_no'] == selected_unit, 'comp_life'].iloc[0] |
|
|
| col1, col2 = st.columns([2, 1]) |
|
|
| with col1: |
|
|
| st.subheader("๐ Grafik Blowby Pressure terhadap Umur Komponen") |
| max_life = unit_df['comp_life'].max() |
| fig = px.line(unit_df, x='comp_life', y='blowby_press_max_act', |
| title=f"Unit: {selected_unit}", |
| labels={'comp_life': 'Component Life (Hours)', 'blowby_press_max_act': 'Pressure (mmH2O)'}, |
| line_shape='linear', render_mode='svg') |
| fig.add_vline(x=current_life, line_dash="dash", line_color="#7f8c8d", annotation_text="Current Life", annotation_position="top left") |
| fig.update_traces(line_color='#1abc9c', line_width=3, marker=dict(size=8)) |
| fig.update_layout( |
| plot_bgcolor='rgba(0,0,0,0)', |
| paper_bgcolor='rgba(0,0,0,0)', |
| font=dict(family="Poppins", size=12, color="#ecf0f1"), |
| title_font=dict(size=18, color="#f1c40f"), |
| showlegend=True, |
| xaxis=dict(showgrid=True, gridcolor='rgba(255,255,255,0.2)'), |
| yaxis=dict(showgrid=True, gridcolor='rgba(255,255,255,0.2)') |
| ) |
| st.plotly_chart(fig, use_container_width=True) |
|
|
| with col2: |
|
|
| st.subheader("โณ Current Component Life") |
| st.markdown(f""" |
| <div class="metric-card"> |
| <h3 style="color: #f1c40f;">Current Life</h3> |
| <p style="font-size: 24px; color: #1abc9c; font-weight: bold;">{current_life:,.0f} jam</p> |
| </div> |
| """, unsafe_allow_html=True) |
|
|
|
|
| st.subheader("โ๏ธ Simulasi Perpanjangan Umur Komponen") |
| input_method = st.radio("Pilih metode input:", ["Slider", "Input Numerik"], horizontal=True) |
|
|
|
|
| max_deviation = int(max_life - current_life) |
|
|
| if input_method == "Slider": |
| extended_life = st.slider( |
| "Simulasikan Perpanjangan Umur Komponen Hingga (jam):", |
| min_value=int(current_life + 100), |
| max_value=int(max_life), |
| value=int(min(current_life + 1000, max_life)), |
| step=100, |
| help="Geser untuk memilih umur komponen yang diinginkan" |
| ) |
| deviation = extended_life - current_life |
| else: |
| deviation = st.number_input( |
| "Masukkan Perpanjangan Umur Komponen Sebesar (jam):", |
| min_value=100, |
| max_value=max_deviation, |
| value=min(1000, max_deviation), |
| step=100, |
| help="Masukkan jumlah jam perpanjangan dari current life" |
| ) |
| extended_life = current_life + deviation |
|
|
|
|
| st.markdown(f"**Umur Komponen Diperpanjang Hingga:** {extended_life:,.0f} jam (Perpanjangan: {deviation:,.0f} jam)", unsafe_allow_html=True) |
|
|
|
|
| sim_df = unit_df[(unit_df['comp_life'] > current_life) & (unit_df['comp_life'] <= extended_life)] |
|
|
| def hitung_avg_blowby(df, batas): |
| tmp = df[['comp_life', 'blowby_press_max_act']].dropna().sort_values('comp_life') |
| tmp = tmp[tmp['comp_life'] <= batas] |
| if tmp.empty: |
| return 0 |
| tail = tmp.tail(10) |
| if len(tail) < 10: |
| st.warning(f"Hanya {len(tail)} titik data (<= {batas} jam).") |
| return tail['blowby_press_max_act'].mean() |
|
|
| def hitung_avg_blowbyawal(df, batas): |
| tmp = df[['comp_life', 'blowby_press_max_act']].dropna().sort_values('comp_life') |
| tmp = tmp[tmp['comp_life'] <= batas] |
| if tmp.empty: |
| return 0 |
| tail = tmp.tail(30) |
| if len(tail) < 30: |
| st.warning(f"Hanya {len(tail)} titik data (<= {batas} jam).") |
| return tail['blowby_press_max_act'].mean() |
|
|
| def fuel_awal_data(df, batas): |
| tmp = df[['comp_life', 'fuel_rate_act']].dropna().sort_values('comp_life') |
| tmp = tmp[tmp['comp_life'] <= batas] |
| if tmp.empty: |
| return 0 |
| tail = tmp.tail(5) |
| if len(tail) < 5: |
| st.warning(f"Hanya {len(tail)} titik data (<= {batas} jam).") |
| return tail['fuel_rate_act'].mean() |
| |
| def fuel_awal_setelah_replacement(df, batas): |
| tmp = df[['comp_life', 'fuel_rate_act']].dropna().sort_values('comp_life') |
| tmp = tmp[tmp['comp_life'] <= batas] |
| if tmp.empty: |
| return 0 |
| head = tmp.head(5) |
| if len(head) < 5: |
| st.warning(f"Hanya {len(head)} titik data (<= {batas} jam).") |
| return head['fuel_rate_act'].mean() |
|
|
| st.subheader("โฝ Prediksi Fuel Rate") |
| avg_blowby_awal = fuel_awal_data(unit_df, current_life) |
| avg_blowby_akhir = hitung_avg_blowby(unit_df, extended_life) |
| fuel_setelah_replacement =fuel_awal_setelah_replacement(unit_df, extended_life) |
| delta_life = extended_life - current_life |
| fuel_rate_awal = (avg_blowby_awal * 0.004) + 71.86 |
| fuel_rate_akhir = (avg_blowby_akhir * 0.004) + 71.86 |
|
|
| hours_ext = extended_life - current_life |
| fuel_usage = fuel_rate_akhir * hours_ext |
|
|
| col3, col4, col5 = st.columns(3) |
| with col3: |
| st.markdown(f""" |
| <div class="metric-card"> |
| <h4 style="color: #34495e;">Fuel Rate Terakhir</h4> |
| <p style="font-size: 20px; color: #3498db;">{fuel_rate_awal:.2f} L/h</p> |
| </div> |
| """, unsafe_allow_html=True) |
| with col4: |
| st.markdown(f""" |
| <div class="metric-card"> |
| <h4 style="color: #34495e;">Fuel Akumulasi</h4> |
| <p style="font-size: 20px; color: #3498db;">{(fuel_rate_akhir * delta_life):,.2f} L</p> |
| </div> |
| """, unsafe_allow_html=True) |
| with col5: |
| st.markdown(f""" |
| <div class="metric-card"> |
| <h4 style="color: #34495e;">Total Extend</h4> |
| <p style="font-size: 20px; color: #3498db;">{delta_life:,.2f} Hm</p> |
| </div> |
| """, unsafe_allow_html=True) |
|
|
| if not sim_df.empty and sim_df['trendline_blow'].notna().any(): |
| last_f = sim_df['trendline_blow'].dropna().iloc[-1] |
| fr_f = last_f * 0.004 + 71.86 |
| rpm_f = last_f * 0.034 + 1303.75 |
| col6, col7 = st.columns(2) |
| with col6: |
| st.markdown(f""" |
| <div class="metric-card"> |
| <h4 style="color: #34495e;">Fuel Rate Forecast</h4> |
| <p style="font-size: 20px; color: #e67e22;">{fr_f:.2f} L/h</p> |
| </div> |
| """, unsafe_allow_html=True) |
| with col7: |
| st.markdown(f""" |
| <div class="metric-card"> |
| <h4 style="color: #34495e;">Engine Speed Forecast</h4> |
| <p style="font-size: 20px; color: #e67e22;">{rpm_f:.2f} RPM</p> |
| </div> |
| """, unsafe_allow_html=True) |
|
|
|
|
| st.subheader("๐ฐ Analisis Keuntungan dan Kerugian") |
| harga_engine = 110_000 |
| harga_fuel = 1 |
|
|
| cost_per_h = harga_engine / current_life |
| saving = ((cost_per_h * delta_life) + (fuel_setelah_replacement* delta_life)) |
| fuel_cost = fuel_usage * harga_fuel |
| net_savings = fuel_cost - saving |
|
|
| col8, col9, col10 = st.columns(3) |
| with col8: |
| st.markdown(f""" |
| <div class="metric-card"> |
| <h4 style="color: #34495e;">Cost Replacement</h4> |
| <p style="font-size: 20px; color: #2ecc71;">USD {saving:,.2f}</p> |
| </div> |
| """, unsafe_allow_html=True) |
| with col9: |
| st.markdown(f""" |
| <div class="metric-card"> |
| <h4 style="color: #34495e;">Cost Fuel (Extend)</h4> |
| <p style="font-size: 20px; color: #e74c3c;">USD {fuel_cost:,.2f}</p> |
| </div> |
| """, unsafe_allow_html=True) |
| with col10: |
| st.markdown(f""" |
| <div class="metric-card"> |
| <h4 style="color: #34495e;">Net Savings</h4> |
| <p style="font-size: 20px; color: #9b59b6;">USD {net_savings:,.2f}</p> |
| </div> |
| """, unsafe_allow_html=True) |
|
|
| st.subheader("Perbandingan Penghematan vs Biaya Fuel") |
| fig2 = go.Figure() |
| fig2.add_trace(go.Bar( |
| x=[saving, fuel_cost], |
| y=["Replacement Cost", "Fuel Cost if Extend"], |
| orientation='h', |
| marker=dict(color=['#2ecc71', '#e74c3c']), |
| text=[f"USD {saving:,.2f}", f"USD {fuel_cost:,.2f}"], |
| textposition='auto' |
| )) |
| fig2.update_layout( |
| title="Perbandingan Biaya", |
| title_font=dict(size=18, color="#f1c40f"), |
| plot_bgcolor='rgba(0,0,0,0)', |
| paper_bgcolor='rgba(0,0,0,0)', |
| font=dict(family="Poppins", size=12, color="#ecf0f1"), |
| showlegend=False, |
| xaxis=dict(showgrid=True, gridcolor='rgba(255,255,255,0.2)'), |
| yaxis=dict(showgrid=False) |
| ) |
| st.plotly_chart(fig2, use_container_width=True) |
|
|
|
|
| st.subheader("๐ Opsi Tindakan") |
| choice = st.radio("Pilih tindakan:", ["replacement/service", "extend component"], horizontal=True) |
| if choice == "replacement/service": |
| st.success("โ
Anda memilih untuk replacement component.") |
| else: |
| st.error("โ Anda memilih untuk extend component.") |
|
|
|
|
| if st.button("Submit"): |
| st.session_state.submitted = True |
| st.rerun() |
|
|
| st.markdown("---") |
| st.markdown("<p style='text-align: center; color: #ecf0f1;'>ยฉ 2025 Your Company. All Rights Reserved.</p>", unsafe_allow_html=True) |