|
|
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) |