Spaces:
Sleeping
Sleeping
Commit ·
89065dc
1
Parent(s): cad0179
Integrasi model Prophet dan tombol selektor interaktif di dashboard
Browse files- bta_prophet.py +276 -0
- bta_xgboost.py +542 -0
- push_to_hf.py +45 -0
- requirements.txt +2 -1
bta_prophet.py
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
BTA Thickness Prediction — Prophet Time-Series Version
|
| 4 |
+
======================================================
|
| 5 |
+
Strategy: Pure Time-Series Forecasting using Facebook Prophet.
|
| 6 |
+
Predicts BTA thickness over time without using temperature data.
|
| 7 |
+
Adheres to Clean Code principles.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
import sys
|
| 12 |
+
import glob
|
| 13 |
+
import json
|
| 14 |
+
import pandas as pd
|
| 15 |
+
import matplotlib.pyplot as plt
|
| 16 |
+
import seaborn as sns
|
| 17 |
+
from prophet import Prophet
|
| 18 |
+
from prophet.serialize import model_to_json
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
CRITICAL_THRESHOLD_MM = 115.0
|
| 22 |
+
WARNING_THRESHOLD_MM = 130.0
|
| 23 |
+
FORECAST_DAYS = 90
|
| 24 |
+
DEFAULT_CSV_FILE = 'data-temp-clean.csv'
|
| 25 |
+
|
| 26 |
+
def main():
|
| 27 |
+
"""High-level orchestrator following the stepdown rule."""
|
| 28 |
+
print("BTA Prophet Forecasting Model Initialization...")
|
| 29 |
+
|
| 30 |
+
try:
|
| 31 |
+
csv_path = get_target_csv_path()
|
| 32 |
+
df_cleaned = load_and_clean_data(csv_path)
|
| 33 |
+
|
| 34 |
+
print(f"Loaded {len(df_cleaned)} actual manual measurement points.")
|
| 35 |
+
print(f" Date range: {df_cleaned['tanggal_parsed'].min().date()} to {df_cleaned['tanggal_parsed'].max().date()}")
|
| 36 |
+
print(f" Current thickness: {df_cleaned['ketebalan_parsed'].iloc[-1]} mm")
|
| 37 |
+
|
| 38 |
+
prophet_df = prepare_prophet_dataframe(df_cleaned)
|
| 39 |
+
|
| 40 |
+
model = train_prophet_model(prophet_df)
|
| 41 |
+
forecast = forecast_thickness(model, days=FORECAST_DAYS)
|
| 42 |
+
|
| 43 |
+
# Calculate remaining days from the last known actual measurement date
|
| 44 |
+
last_measurement_date = prophet_df['ds'].max()
|
| 45 |
+
days_remaining = estimate_days_to_threshold(
|
| 46 |
+
forecast_df=forecast,
|
| 47 |
+
current_date=last_measurement_date,
|
| 48 |
+
threshold=CRITICAL_THRESHOLD_MM
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
print_forecast_summary(forecast, days_remaining)
|
| 52 |
+
print_forecast_table(historical_df=prophet_df, forecast_df=forecast)
|
| 53 |
+
|
| 54 |
+
output_image_path = 'bta_prophet_predictions.png'
|
| 55 |
+
plot_and_save_forecast(prophet_df, forecast, output_image_path)
|
| 56 |
+
print(f"Prediction plot saved to '{output_image_path}'.")
|
| 57 |
+
|
| 58 |
+
output_model_path = 'model_prophet_bta.json'
|
| 59 |
+
save_model_json(model, output_model_path)
|
| 60 |
+
print(f"Prophet model serialized and saved to '{output_model_path}'.")
|
| 61 |
+
|
| 62 |
+
except Exception as error:
|
| 63 |
+
print(f"Error during model execution: {error}", file=sys.stderr)
|
| 64 |
+
sys.exit(1)
|
| 65 |
+
|
| 66 |
+
def get_target_csv_path() -> str:
|
| 67 |
+
"""Finds target CSV file dynamically or from command line arguments."""
|
| 68 |
+
if len(sys.argv) > 1:
|
| 69 |
+
provided_path = sys.argv[1]
|
| 70 |
+
if not os.path.exists(provided_path):
|
| 71 |
+
raise FileNotFoundError(f"Provided CSV file '{provided_path}' does not exist.")
|
| 72 |
+
return provided_path
|
| 73 |
+
|
| 74 |
+
csv_files = glob.glob('*.csv') + glob.glob('*.csv.csv')
|
| 75 |
+
if not csv_files:
|
| 76 |
+
raise FileNotFoundError("No CSV files found in the current directory.")
|
| 77 |
+
|
| 78 |
+
# Standardize names and prioritize default file
|
| 79 |
+
unique_files = list(set([os.path.basename(f) for f in csv_files]))
|
| 80 |
+
|
| 81 |
+
# Try finding standard name variations
|
| 82 |
+
for name in [DEFAULT_CSV_FILE, DEFAULT_CSV_FILE + '.csv', 'data-temp-clean.csv.csv']:
|
| 83 |
+
if name in unique_files:
|
| 84 |
+
return name
|
| 85 |
+
|
| 86 |
+
return unique_files[0]
|
| 87 |
+
|
| 88 |
+
def load_and_clean_data(file_path: str) -> pd.DataFrame:
|
| 89 |
+
"""Reads data, cleans spaces, and filters for valid manual measurements."""
|
| 90 |
+
df = pd.read_csv(file_path)
|
| 91 |
+
df.columns = [str(col).strip() for col in df.columns]
|
| 92 |
+
|
| 93 |
+
# Rename key columns for ease of access
|
| 94 |
+
df = df.rename(columns={
|
| 95 |
+
'Tanggal': 'tanggal_raw',
|
| 96 |
+
'Ketebalan BTA (mm)': 'ketebalan_raw'
|
| 97 |
+
})
|
| 98 |
+
|
| 99 |
+
# Clean and parse types
|
| 100 |
+
df['ketebalan_parsed'] = pd.to_numeric(df['ketebalan_raw'], errors='coerce')
|
| 101 |
+
df['tanggal_parsed'] = pd.to_datetime(df['tanggal_raw'], errors='coerce')
|
| 102 |
+
|
| 103 |
+
# Drop rows without valid actual measurements (only keep actual measurement dates)
|
| 104 |
+
cleaned_df = df.dropna(subset=['tanggal_parsed', 'ketebalan_parsed'])
|
| 105 |
+
|
| 106 |
+
# Sort chronologically
|
| 107 |
+
return cleaned_df.sort_values('tanggal_parsed').reset_index(drop=True)
|
| 108 |
+
|
| 109 |
+
def prepare_prophet_dataframe(df: pd.DataFrame) -> pd.DataFrame:
|
| 110 |
+
"""Formats DataFrame columns to Prophet expected names (ds and y)."""
|
| 111 |
+
return df[['tanggal_parsed', 'ketebalan_parsed']].rename(
|
| 112 |
+
columns={'tanggal_parsed': 'ds', 'ketebalan_parsed': 'y'}
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
def train_prophet_model(df: pd.DataFrame) -> Prophet:
|
| 116 |
+
"""Trains a Prophet model with settings optimized for BTA wear dynamics."""
|
| 117 |
+
# Since BTA thickness wear is monotonic and non-seasonal, disable seasonalities
|
| 118 |
+
model = Prophet(
|
| 119 |
+
growth='linear',
|
| 120 |
+
yearly_seasonality=False,
|
| 121 |
+
weekly_seasonality=False,
|
| 122 |
+
daily_seasonality=False
|
| 123 |
+
)
|
| 124 |
+
model.fit(df)
|
| 125 |
+
return model
|
| 126 |
+
|
| 127 |
+
def forecast_thickness(model: Prophet, days: int) -> pd.DataFrame:
|
| 128 |
+
"""Forecasts BTA thickness into the future."""
|
| 129 |
+
future = model.make_future_dataframe(periods=days)
|
| 130 |
+
return model.predict(future)
|
| 131 |
+
|
| 132 |
+
def estimate_days_to_threshold(forecast_df: pd.DataFrame, current_date: pd.Timestamp, threshold: float) -> int:
|
| 133 |
+
"""Finds the number of days until the forecasted thickness crosses a threshold."""
|
| 134 |
+
critical_predictions = forecast_df[forecast_df['yhat'] <= threshold]
|
| 135 |
+
if critical_predictions.empty:
|
| 136 |
+
return FORECAST_DAYS
|
| 137 |
+
|
| 138 |
+
earliest_critical_date = critical_predictions['ds'].min()
|
| 139 |
+
days_remaining = (earliest_critical_date - current_date).days
|
| 140 |
+
return max(0, days_remaining)
|
| 141 |
+
|
| 142 |
+
def print_forecast_summary(forecast_df: pd.DataFrame, days_remaining: int):
|
| 143 |
+
"""Outputs text summary of the forecast details."""
|
| 144 |
+
last_prediction = forecast_df.iloc[-1]
|
| 145 |
+
last_date = last_prediction['ds'].date()
|
| 146 |
+
predicted_thickness = last_prediction['yhat']
|
| 147 |
+
|
| 148 |
+
print("\n" + "="*50)
|
| 149 |
+
print(f"PROPHET FORECAST RESULTS (Next {FORECAST_DAYS} days)")
|
| 150 |
+
print("="*50)
|
| 151 |
+
print(f" Target Date : {last_date}")
|
| 152 |
+
print(f" Predicted Thickness : {predicted_thickness:.2f} mm")
|
| 153 |
+
print(f" Confidence Interval : [{last_prediction['yhat_lower']:.2f} - {last_prediction['yhat_upper']:.2f}] mm")
|
| 154 |
+
print(f" Estimated Days to {CRITICAL_THRESHOLD_MM}mm: about {days_remaining} days")
|
| 155 |
+
print("="*50 + "\n")
|
| 156 |
+
|
| 157 |
+
def print_forecast_table(historical_df: pd.DataFrame, forecast_df: pd.DataFrame):
|
| 158 |
+
"""Prints a sequential (runtut) table containing both historical actual measurements and future predictions, matching the timeline of the line chart."""
|
| 159 |
+
# Merge historical actual 'y' onto forecast_df
|
| 160 |
+
merged_df = pd.merge(
|
| 161 |
+
forecast_df[['ds', 'yhat', 'yhat_lower', 'yhat_upper']],
|
| 162 |
+
historical_df[['ds', 'y']],
|
| 163 |
+
on='ds',
|
| 164 |
+
how='left'
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
# Rename columns for presentation
|
| 168 |
+
display_df = merged_df.rename(columns={
|
| 169 |
+
'ds': 'Date',
|
| 170 |
+
'y': 'Actual (mm)',
|
| 171 |
+
'yhat': 'Predicted (mm)',
|
| 172 |
+
'yhat_lower': 'Lower Bound (mm)',
|
| 173 |
+
'yhat_upper': 'Upper Bound (mm)'
|
| 174 |
+
})
|
| 175 |
+
|
| 176 |
+
# Format Date
|
| 177 |
+
display_df['Date'] = display_df['Date'].dt.date
|
| 178 |
+
|
| 179 |
+
# Format numbers
|
| 180 |
+
for column in ['Predicted (mm)', 'Lower Bound (mm)', 'Upper Bound (mm)']:
|
| 181 |
+
display_df[column] = display_df[column].round(2)
|
| 182 |
+
|
| 183 |
+
# Format actual values (replace NaN with '-' for clean output)
|
| 184 |
+
display_df['Actual (mm)'] = display_df['Actual (mm)'].apply(
|
| 185 |
+
lambda val: f"{val:.1f}" if pd.notna(val) else "-"
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
# Reorder columns to put Actual next to Date
|
| 189 |
+
cols = ['Date', 'Actual (mm)', 'Predicted (mm)', 'Lower Bound (mm)', 'Upper Bound (mm)']
|
| 190 |
+
display_df = display_df[cols]
|
| 191 |
+
|
| 192 |
+
# Configure pandas to print the full dataframe without truncation
|
| 193 |
+
pd.set_option('display.max_rows', 150)
|
| 194 |
+
|
| 195 |
+
print("CHRONOLOGICAL BTA THICKNESS DATA & FORECAST (Runtut):")
|
| 196 |
+
print(display_df.to_string(index=False))
|
| 197 |
+
print("="*50 + "\n")
|
| 198 |
+
|
| 199 |
+
def plot_and_save_forecast(historical_df: pd.DataFrame, forecast_df: pd.DataFrame, output_path: str):
|
| 200 |
+
"""Generates and saves visual report comparing historical data and future predictions."""
|
| 201 |
+
sns.set_theme(style='whitegrid')
|
| 202 |
+
fig, ax = plt.subplots(figsize=(14, 7))
|
| 203 |
+
|
| 204 |
+
# Plot historical actual measurements
|
| 205 |
+
ax.scatter(
|
| 206 |
+
historical_df['ds'],
|
| 207 |
+
historical_df['y'],
|
| 208 |
+
color='royalblue',
|
| 209 |
+
s=70,
|
| 210 |
+
label='Actual Measurement (Manual)',
|
| 211 |
+
zorder=5
|
| 212 |
+
)
|
| 213 |
+
|
| 214 |
+
# Plot predicted values
|
| 215 |
+
ax.plot(
|
| 216 |
+
forecast_df['ds'],
|
| 217 |
+
forecast_df['yhat'],
|
| 218 |
+
color='darkorange',
|
| 219 |
+
linewidth=2,
|
| 220 |
+
label='Predicted Trend (Prophet)',
|
| 221 |
+
zorder=4
|
| 222 |
+
)
|
| 223 |
+
|
| 224 |
+
# Plot uncertainty interval
|
| 225 |
+
ax.fill_between(
|
| 226 |
+
forecast_df['ds'],
|
| 227 |
+
forecast_df['yhat_lower'],
|
| 228 |
+
forecast_df['yhat_upper'],
|
| 229 |
+
color='darkorange',
|
| 230 |
+
alpha=0.15,
|
| 231 |
+
label='Uncertainty Interval (Confidence Interval)'
|
| 232 |
+
)
|
| 233 |
+
|
| 234 |
+
# Draw operational thresholds
|
| 235 |
+
ax.axhline(
|
| 236 |
+
y=CRITICAL_THRESHOLD_MM,
|
| 237 |
+
color='red',
|
| 238 |
+
linestyle='--',
|
| 239 |
+
linewidth=1.5,
|
| 240 |
+
label=f'Critical Threshold ({CRITICAL_THRESHOLD_MM} mm)'
|
| 241 |
+
)
|
| 242 |
+
ax.axhline(
|
| 243 |
+
y=WARNING_THRESHOLD_MM,
|
| 244 |
+
color='orange',
|
| 245 |
+
linestyle=':',
|
| 246 |
+
linewidth=1.5,
|
| 247 |
+
label=f'Warning Threshold ({WARNING_THRESHOLD_MM} mm)'
|
| 248 |
+
)
|
| 249 |
+
|
| 250 |
+
# Highlight final data anchor
|
| 251 |
+
last_actual_date = historical_df['ds'].max()
|
| 252 |
+
ax.axvline(
|
| 253 |
+
x=last_actual_date,
|
| 254 |
+
color='gray',
|
| 255 |
+
linestyle=':',
|
| 256 |
+
alpha=0.8,
|
| 257 |
+
label='Last Known Measurement'
|
| 258 |
+
)
|
| 259 |
+
|
| 260 |
+
ax.set_title('BTA Thickness Forecasting — Prophet Time-Series Model', fontsize=14, fontweight='bold')
|
| 261 |
+
ax.set_ylabel('Thickness (mm)')
|
| 262 |
+
ax.set_xlabel('Date')
|
| 263 |
+
ax.legend(loc='upper right', frameon=True)
|
| 264 |
+
ax.set_ylim(90, 245)
|
| 265 |
+
|
| 266 |
+
plt.tight_layout()
|
| 267 |
+
plt.savefig(output_path, dpi=150)
|
| 268 |
+
plt.close()
|
| 269 |
+
|
| 270 |
+
def save_model_json(model: Prophet, filepath: str):
|
| 271 |
+
"""Serializes Prophet model to a portable JSON format."""
|
| 272 |
+
with open(filepath, 'w') as out_file:
|
| 273 |
+
json.dump(model_to_json(model), out_file)
|
| 274 |
+
|
| 275 |
+
if __name__ == '__main__':
|
| 276 |
+
main()
|
bta_xgboost.py
ADDED
|
@@ -0,0 +1,542 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""XGBOOST.ipynb
|
| 3 |
+
|
| 4 |
+
Automatically generated by Colab.
|
| 5 |
+
|
| 6 |
+
Original file is located at
|
| 7 |
+
https://colab.research.google.com/drive/1tDtwG0rHNfmKvSU-iQ32jGa1xB4HAqCn
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
# -*- coding: utf-8 -*-
|
| 11 |
+
"""
|
| 12 |
+
BTA_Predict_FINAL.ipynb
|
| 13 |
+
================================================
|
| 14 |
+
BTA Thickness Prediction — Production-Ready Version
|
| 15 |
+
Strategy: Anchor-Based Wear Rate Model
|
| 16 |
+
(didasarkan insight bahwa:
|
| 17 |
+
- Ketebalan diukur manual → diskrit, bukan harian
|
| 18 |
+
- Korelasi waktu vs ketebalan = -0.995
|
| 19 |
+
- Korelasi suhu vs ketebalan = -0.16 (lemah)
|
| 20 |
+
→ Prediksi = Anchor terakhir + laju aus + suhu modifier)
|
| 21 |
+
================================================
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
# ============================================================
|
| 25 |
+
# CELL 1: Install & Import
|
| 26 |
+
# ============================================================
|
| 27 |
+
# !pip install -q xgboost scikit-learn pandas numpy matplotlib seaborn
|
| 28 |
+
|
| 29 |
+
import pandas as pd
|
| 30 |
+
import numpy as np
|
| 31 |
+
from xgboost import XGBRegressor
|
| 32 |
+
from sklearn.linear_model import LinearRegression, Ridge
|
| 33 |
+
from sklearn.preprocessing import PolynomialFeatures
|
| 34 |
+
from sklearn.pipeline import Pipeline
|
| 35 |
+
from sklearn.metrics import mean_absolute_error, mean_squared_error
|
| 36 |
+
from datetime import datetime, timedelta
|
| 37 |
+
import matplotlib.pyplot as plt
|
| 38 |
+
import matplotlib.dates as mdates
|
| 39 |
+
import seaborn as sns
|
| 40 |
+
import warnings, io
|
| 41 |
+
|
| 42 |
+
warnings.filterwarnings('ignore')
|
| 43 |
+
print("✅ Libraries loaded.")
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
# ============================================================
|
| 47 |
+
# CELL 2: Upload & Load Data
|
| 48 |
+
# ============================================================
|
| 49 |
+
import os
|
| 50 |
+
import sys
|
| 51 |
+
import glob
|
| 52 |
+
|
| 53 |
+
# Cari semua berkas CSV di folder saat ini
|
| 54 |
+
csv_files = glob.glob('*.csv') + glob.glob('*.csv.csv')
|
| 55 |
+
# Hilangkan duplikat dan rapikan path
|
| 56 |
+
csv_files = list(set([os.path.basename(f) for f in csv_files]))
|
| 57 |
+
|
| 58 |
+
# Prioritaskan argumen baris perintah
|
| 59 |
+
if len(sys.argv) > 1:
|
| 60 |
+
filename = sys.argv[1]
|
| 61 |
+
if not os.path.exists(filename):
|
| 62 |
+
print(f"❌ File '{filename}' yang diberikan melalui argumen tidak ditemukan!")
|
| 63 |
+
sys.exit(1)
|
| 64 |
+
else:
|
| 65 |
+
# Jika tidak ada argumen, cari CSV secara otomatis
|
| 66 |
+
if not csv_files:
|
| 67 |
+
raise FileNotFoundError("Tidak ditemukan file CSV di direktori saat ini.")
|
| 68 |
+
elif len(csv_files) == 1:
|
| 69 |
+
filename = csv_files[0]
|
| 70 |
+
else:
|
| 71 |
+
print("\n📂 Ditemukan beberapa file CSV di direktori saat ini:")
|
| 72 |
+
for idx, f in enumerate(csv_files, 1):
|
| 73 |
+
print(f" [{idx}] {f}")
|
| 74 |
+
|
| 75 |
+
# Cari default file jika ada
|
| 76 |
+
default_file = 'data-temp-clean.csv'
|
| 77 |
+
if default_file not in csv_files and 'data-temp-clean.csv.csv' in csv_files:
|
| 78 |
+
default_file = 'data-temp-clean.csv.csv'
|
| 79 |
+
|
| 80 |
+
default_idx = csv_files.index(default_file) + 1 if default_file in csv_files else 1
|
| 81 |
+
|
| 82 |
+
try:
|
| 83 |
+
choice = input(f"Pilih file untuk diproses (Tekan Enter untuk default [{csv_files[default_idx-1]}]): ").strip()
|
| 84 |
+
if choice == "":
|
| 85 |
+
filename = csv_files[default_idx-1]
|
| 86 |
+
else:
|
| 87 |
+
choice_idx = int(choice) - 1
|
| 88 |
+
if 0 <= choice_idx < len(csv_files):
|
| 89 |
+
filename = csv_files[choice_idx]
|
| 90 |
+
else:
|
| 91 |
+
print("❌ Pilihan tidak valid. Menggunakan file default.")
|
| 92 |
+
filename = csv_files[default_idx-1]
|
| 93 |
+
except Exception:
|
| 94 |
+
filename = csv_files[default_idx-1]
|
| 95 |
+
|
| 96 |
+
print(f"📂 Membaca file data: '{filename}'")
|
| 97 |
+
df = pd.read_csv(filename)
|
| 98 |
+
|
| 99 |
+
# Menentukan nama dasar berkas untuk keluaran dinamis
|
| 100 |
+
base_name = os.path.splitext(filename)[0]
|
| 101 |
+
if base_name.endswith('.csv'):
|
| 102 |
+
base_name = os.path.splitext(base_name)[0]
|
| 103 |
+
|
| 104 |
+
# bersihin nama kolom dari spasi
|
| 105 |
+
df.columns = [str(c).strip() for c in df.columns]
|
| 106 |
+
|
| 107 |
+
# rename kolomnya biar gampang dipanggil
|
| 108 |
+
df = df.rename(columns={
|
| 109 |
+
'Tanggal' : 'tanggal',
|
| 110 |
+
'Cone Depan (°C)' : 'cone_depan',
|
| 111 |
+
'Bodi Tengah (°C)' : 'body_tengah',
|
| 112 |
+
'Cone Belakang (°C)': 'cone_belakang',
|
| 113 |
+
'Ketebalan BTA (mm)': 'ketebalan'
|
| 114 |
+
})
|
| 115 |
+
|
| 116 |
+
# pastiin tipe data udah angka
|
| 117 |
+
for col in ['cone_depan', 'body_tengah', 'cone_belakang', 'ketebalan']:
|
| 118 |
+
df[col] = pd.to_numeric(df[col], errors='coerce')
|
| 119 |
+
|
| 120 |
+
df['tanggal'] = pd.to_datetime(df['tanggal'], dayfirst=False, errors='coerce')
|
| 121 |
+
df = df.dropna(subset=['cone_depan', 'ketebalan', 'tanggal']).sort_values('tanggal').reset_index(drop=True)
|
| 122 |
+
|
| 123 |
+
# referensi waktu dari hari pertama operasi
|
| 124 |
+
T0 = df['tanggal'].min()
|
| 125 |
+
df['hari_ke'] = (df['tanggal'] - T0).dt.days
|
| 126 |
+
|
| 127 |
+
print(f"✅ Data: {len(df)} baris | {df['tanggal'].min().date()} s/d {df['tanggal'].max().date()}")
|
| 128 |
+
print(f" Ketebalan: min={df['ketebalan'].min():.0f}mm | max={df['ketebalan'].max():.0f}mm")
|
| 129 |
+
print(f" Pengukuran terakhir: {df['ketebalan'].iloc[-1]:.0f}mm pada {df['tanggal'].iloc[-1].date()}")
|
| 130 |
+
|
| 131 |
+
# ============================================================
|
| 132 |
+
# CELL 3: Hitung Titik Pengukuran & Laju Aus
|
| 133 |
+
# ============================================================
|
| 134 |
+
#
|
| 135 |
+
# INSIGHT: Ketebalan BTA diukur manual secara berkala (tidak harian).
|
| 136 |
+
# Data harian hanya mengulang nilai pengukuran terakhir sampai ada
|
| 137 |
+
# pengukuran baru. Oleh karena itu, model yang benar adalah:
|
| 138 |
+
# Ketebalan_hari_ini = Ketebalan_terakhir_diukur + (laju_aus × hari_berlalu)
|
| 139 |
+
#
|
| 140 |
+
|
| 141 |
+
# Identifikasi titik pengukuran aktual (ketika nilai berubah)
|
| 142 |
+
mask_change = df['ketebalan'] != df['ketebalan'].shift(1)
|
| 143 |
+
df_ukur = df[mask_change].copy().reset_index(drop=True)
|
| 144 |
+
|
| 145 |
+
# Hitung laju aus antar pengukuran
|
| 146 |
+
df_ukur['delta_tebal'] = df_ukur['ketebalan'].diff() # negatif = menipis
|
| 147 |
+
df_ukur['delta_hari'] = df_ukur['hari_ke'].diff()
|
| 148 |
+
df_ukur['laju_aus'] = df_ukur['delta_tebal'] / df_ukur['delta_hari'] # mm/hari
|
| 149 |
+
|
| 150 |
+
# Suhu rata-rata selama periode antar pengukuran
|
| 151 |
+
for i in range(1, len(df_ukur)):
|
| 152 |
+
h_start = df_ukur.loc[i-1, 'hari_ke']
|
| 153 |
+
h_end = df_ukur.loc[i, 'hari_ke']
|
| 154 |
+
mask = (df['hari_ke'] >= h_start) & (df['hari_ke'] < h_end)
|
| 155 |
+
df_ukur.loc[i, 'suhu_avg_periode'] = df.loc[mask, 'cone_depan'].add(
|
| 156 |
+
df.loc[mask, 'body_tengah']).add(df.loc[mask, 'cone_belakang']).div(3).mean()
|
| 157 |
+
|
| 158 |
+
df_ukur = df_ukur.dropna(subset=['laju_aus'])
|
| 159 |
+
|
| 160 |
+
# Laju aus statistik
|
| 161 |
+
laju_mean = df_ukur['laju_aus'].mean() # mm/hari (negatif)
|
| 162 |
+
laju_recent = df_ukur['laju_aus'].tail(5).mean() # laju 5 periode terakhir
|
| 163 |
+
|
| 164 |
+
print(f"\n📊 Analisis Laju Aus:")
|
| 165 |
+
print(f" Rata-rata historis : {laju_mean:.4f} mm/hari")
|
| 166 |
+
print(f" Rata-rata 5 terbaru : {laju_recent:.4f} mm/hari")
|
| 167 |
+
print(f" Pengukuran terakhir : {df_ukur['ketebalan'].iloc[-1]:.0f}mm "
|
| 168 |
+
f"pada hari ke-{df_ukur['hari_ke'].iloc[-1]}")
|
| 169 |
+
print(f"\n{df_ukur[['tanggal','ketebalan','delta_hari','laju_aus','suhu_avg_periode']].to_string(index=False)}")
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
# ============================================================
|
| 173 |
+
# CELL 4: Model XGBoost — Prediksi Laju Aus dari Suhu
|
| 174 |
+
# ============================================================
|
| 175 |
+
#
|
| 176 |
+
# Karena laju aus antar periode bervariasi, kita gunakan XGBoost
|
| 177 |
+
# untuk mempelajari: "Seberapa cepat BTA menipis berdasarkan suhu?"
|
| 178 |
+
# Lalu gunakan laju ini untuk proyeksi ke depan.
|
| 179 |
+
#
|
| 180 |
+
|
| 181 |
+
# Fitur: suhu rata-rata & karakteristik suhu selama satu periode
|
| 182 |
+
X_rate = df_ukur[['suhu_avg_periode']].fillna(df_ukur['suhu_avg_periode'].mean())
|
| 183 |
+
y_rate = df_ukur['laju_aus'] # target: laju aus (mm/hari)
|
| 184 |
+
|
| 185 |
+
model_rate = XGBRegressor(
|
| 186 |
+
n_estimators = 200,
|
| 187 |
+
learning_rate = 0.05,
|
| 188 |
+
max_depth = 3,
|
| 189 |
+
subsample = 0.8,
|
| 190 |
+
random_state = 42
|
| 191 |
+
)
|
| 192 |
+
model_rate.fit(X_rate, y_rate)
|
| 193 |
+
|
| 194 |
+
print(f"\n✅ Model laju aus dilatih pada {len(df_ukur)} titik pengukuran.")
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
# ============================================================
|
| 198 |
+
# CELL 5: Fungsi Prediksi Harian (PRODUCTION READY)
|
| 199 |
+
# ============================================================
|
| 200 |
+
|
| 201 |
+
# State terakhir yang diketahui
|
| 202 |
+
TEBAL_TERAKHIR = float(df['ketebalan'].iloc[-1]) # mm
|
| 203 |
+
TANGGAL_UKUR = df['tanggal'].iloc[-1] # tanggal pengukuran aktual terakhir
|
| 204 |
+
HARI_UKUR = int(df['hari_ke'].iloc[-1]) # hari ke- dari pengukuran tsb
|
| 205 |
+
|
| 206 |
+
# Batas operasi
|
| 207 |
+
BATAS_KRITIS = 115.0 # mm
|
| 208 |
+
BATAS_WARNING = 130.0 # mm
|
| 209 |
+
BATAS_SUHU = 400.0 # °C
|
| 210 |
+
WARN_SUHU = 375.0 # °C
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def predict_bta_daily(t_depan: float, t_tengah: float, t_belakang: float,
|
| 214 |
+
tanggal_cek: str = None,
|
| 215 |
+
tebal_aktual: float = None):
|
| 216 |
+
"""
|
| 217 |
+
Prediksi ketebalan BTA untuk monitoring harian produksi.
|
| 218 |
+
|
| 219 |
+
Parameters
|
| 220 |
+
----------
|
| 221 |
+
t_depan : Suhu Cone Depan (°C) hari ini
|
| 222 |
+
t_tengah : Suhu Bodi Tengah (°C) hari ini
|
| 223 |
+
t_belakang : Suhu Cone Belakang (°C) hari ini
|
| 224 |
+
tanggal_cek : Tanggal pengecekan 'DD/MM/YYYY' (default: hari ini)
|
| 225 |
+
tebal_aktual : (Opsional) Jika ada hasil pengukuran BTA hari ini,
|
| 226 |
+
masukkan di sini untuk update anchor secara otomatis
|
| 227 |
+
"""
|
| 228 |
+
global TEBAL_TERAKHIR, TANGGAL_UKUR, HARI_UKUR
|
| 229 |
+
|
| 230 |
+
# Update anchor jika ada pengukuran aktual baru
|
| 231 |
+
if tebal_aktual is not None:
|
| 232 |
+
TEBAL_TERAKHIR = float(tebal_aktual)
|
| 233 |
+
TANGGAL_UKUR = datetime.now() if tanggal_cek is None else datetime.strptime(tanggal_cek, '%d/%m/%Y')
|
| 234 |
+
HARI_UKUR = int((TANGGAL_UKUR - T0).days)
|
| 235 |
+
print(f"🔄 Anchor diperbarui: {TEBAL_TERAKHIR}mm pada {TANGGAL_UKUR.date()}")
|
| 236 |
+
|
| 237 |
+
# Tanggal hari ini
|
| 238 |
+
tgl_cek = datetime.now() if tanggal_cek is None else datetime.strptime(tanggal_cek, '%d/%m/%Y')
|
| 239 |
+
hari_sejak_ukur = (tgl_cek - (TANGGAL_UKUR if isinstance(TANGGAL_UKUR, datetime)
|
| 240 |
+
else pd.Timestamp(TANGGAL_UKUR).to_pydatetime())).days
|
| 241 |
+
|
| 242 |
+
# Suhu hari ini
|
| 243 |
+
suhu_avg = (t_depan + t_tengah + t_belakang) / 3
|
| 244 |
+
|
| 245 |
+
# Prediksi laju aus berdasarkan suhu hari ini
|
| 246 |
+
laju_pred = model_rate.predict(pd.DataFrame([[suhu_avg]], columns=['suhu_avg_periode']))[0]
|
| 247 |
+
|
| 248 |
+
# Koreksi: gunakan weighted average antara laju historis dan prediksi model
|
| 249 |
+
# (karena data terbatas, beri bobot lebih ke historis)
|
| 250 |
+
laju_efektif = 0.4 * laju_pred + 0.6 * laju_recent
|
| 251 |
+
|
| 252 |
+
# Proyeksi ketebalan hari ini
|
| 253 |
+
tebal_pred = TEBAL_TERAKHIR + (laju_efektif * hari_sejak_ukur)
|
| 254 |
+
tebal_pred = max(tebal_pred, 100.0) # floor fisik
|
| 255 |
+
|
| 256 |
+
# Estimasi sisa hari ke batas kritis
|
| 257 |
+
if tebal_pred > BATAS_KRITIS and laju_efektif < 0:
|
| 258 |
+
sisa_tebal = tebal_pred - BATAS_KRITIS
|
| 259 |
+
sisa_hari = int(sisa_tebal / abs(laju_efektif))
|
| 260 |
+
tgl_kritis = tgl_cek + timedelta(days=sisa_hari)
|
| 261 |
+
else:
|
| 262 |
+
sisa_hari = 0
|
| 263 |
+
tgl_kritis = tgl_cek
|
| 264 |
+
|
| 265 |
+
# Status & alert
|
| 266 |
+
if suhu_avg > BATAS_SUHU or tebal_pred < BATAS_KRITIS:
|
| 267 |
+
status = "🔴 CRITICAL"
|
| 268 |
+
aksi = "SEGERA PERBAIKAN / SLAGGING — Koordinasi maintenance sekarang!"
|
| 269 |
+
border = "!"*52
|
| 270 |
+
elif suhu_avg > WARN_SUHU or tebal_pred < BATAS_WARNING:
|
| 271 |
+
status = "🟡 WARNING"
|
| 272 |
+
aksi = "Siapkan jadwal maintenance. Monitor lebih sering."
|
| 273 |
+
border = "="*52
|
| 274 |
+
else:
|
| 275 |
+
status = "🟢 AMAN"
|
| 276 |
+
aksi = "Operasi normal. Lanjutkan monitoring rutin."
|
| 277 |
+
border = "-"*52
|
| 278 |
+
|
| 279 |
+
print(f"\n{border}")
|
| 280 |
+
print(f" 🏭 BTA DAILY MONITORING — {tgl_cek.strftime('%d %B %Y')}")
|
| 281 |
+
print(f"{border}")
|
| 282 |
+
print(f" 📡 Input Suhu : Depan={t_depan}°C | Tengah={t_tengah}°C | Belakang={t_belakang}°C")
|
| 283 |
+
print(f" 🌡️ Suhu Rata-rata : {suhu_avg:.1f}°C")
|
| 284 |
+
print(f" 📅 Anchor Terakhir : {TEBAL_TERAKHIR:.0f}mm ({(TANGGAL_UKUR if isinstance(TANGGAL_UKUR, datetime) else pd.Timestamp(TANGGAL_UKUR).to_pydatetime()).strftime('%d %b %Y')})")
|
| 285 |
+
print(f" ⏱️ Hari sejak ukur : {hari_sejak_ukur} hari")
|
| 286 |
+
print(f" 📉 Laju Aus Est. : {laju_efektif:.4f} mm/hari")
|
| 287 |
+
print(f"{'='*52}")
|
| 288 |
+
print(f" 🧱 PREDIKSI TEBAL : {tebal_pred:.1f} mm")
|
| 289 |
+
print(f" 📊 Status : {status}")
|
| 290 |
+
print(f" ⚡ Aksi : {aksi}")
|
| 291 |
+
print(f"{'-'*52}")
|
| 292 |
+
if tebal_pred > BATAS_KRITIS:
|
| 293 |
+
print(f" ⏳ Estimasi Sisa : ± {sisa_hari} hari lagi")
|
| 294 |
+
print(f" 🔧 Est. Kritis : {tgl_kritis.strftime('%d %B %Y')}")
|
| 295 |
+
else:
|
| 296 |
+
print(f" ⏳ Status : Ketebalan sudah di/bawah batas kritis!")
|
| 297 |
+
print(f"{border}\n")
|
| 298 |
+
|
| 299 |
+
return {
|
| 300 |
+
'tebal_prediksi' : round(tebal_pred, 1),
|
| 301 |
+
'laju_aus' : round(laju_efektif, 4),
|
| 302 |
+
'sisa_hari' : sisa_hari,
|
| 303 |
+
'est_kritis' : tgl_kritis.strftime('%d %B %Y'),
|
| 304 |
+
'status' : status
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
# ============================================================
|
| 309 |
+
# CELL 6: Uji Coba Prediksi
|
| 310 |
+
# ============================================================
|
| 311 |
+
print("=" * 60)
|
| 312 |
+
print("🧪 UJI COBA PREDIKSI HARIAN")
|
| 313 |
+
print("=" * 60)
|
| 314 |
+
|
| 315 |
+
# Contoh 1: Suhu normal
|
| 316 |
+
_ = predict_bta_daily(t_depan=378, t_tengah=310, t_belakang=355)
|
| 317 |
+
|
| 318 |
+
# Contoh 2: Suhu tinggi (mendekati kritis)
|
| 319 |
+
_ = predict_bta_daily(t_depan=435, t_tengah=410, t_belakang=372)
|
| 320 |
+
|
| 321 |
+
# Contoh 3: Suhu rendah (kondisi baik)
|
| 322 |
+
_ = predict_bta_daily(t_depan=320, t_tengah=295, t_belakang=340)
|
| 323 |
+
|
| 324 |
+
# Contoh 4: Ada pengukuran BTA baru hari ini (misal diukur = 118mm)
|
| 325 |
+
# Ini akan update anchor sehingga prediksi ke depan lebih akurat
|
| 326 |
+
# _ = predict_bta_daily(t_depan=350, t_tengah=320, t_belakang=360, tebal_aktual=118)
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
# ============================================================
|
| 330 |
+
# CELL 7: Visualisasi Historis + Proyeksi
|
| 331 |
+
# ============================================================
|
| 332 |
+
sns.set_theme(style='whitegrid')
|
| 333 |
+
fig, axes = plt.subplots(2, 1, figsize=(16, 12))
|
| 334 |
+
fig.suptitle('BTA Thickness Monitoring — Rotary Furnace', fontsize=15, fontweight='bold')
|
| 335 |
+
|
| 336 |
+
# --- Panel 1: History + Proyeksi ---
|
| 337 |
+
ax1 = axes[0]
|
| 338 |
+
|
| 339 |
+
# Data aktual (stepwise — karena nilai hanya berubah saat diukur)
|
| 340 |
+
ax1.step(df['tanggal'], df['ketebalan'], where='post',
|
| 341 |
+
color='royalblue', linewidth=2.5, label='Ketebalan Aktual (Pengukuran Manual)', zorder=3)
|
| 342 |
+
|
| 343 |
+
# Titik pengukuran aktual
|
| 344 |
+
ax1.scatter(df_ukur['tanggal'], df_ukur['ketebalan'],
|
| 345 |
+
color='royalblue', s=80, zorder=5, label='Titik Pengukuran Aktual')
|
| 346 |
+
|
| 347 |
+
# Proyeksi 90 hari ke depan
|
| 348 |
+
tgl_terakhir = df['tanggal'].max()
|
| 349 |
+
proj_hari = 90
|
| 350 |
+
proj_dates = [tgl_terakhir + timedelta(days=i) for i in range(0, proj_hari+1)]
|
| 351 |
+
|
| 352 |
+
# Gunakan laju recent untuk proyeksi
|
| 353 |
+
suhu_asumsi = df['cone_depan'].add(df['body_tengah']).add(df['cone_belakang']).div(3).tail(14).mean()
|
| 354 |
+
laju_proj = model_rate.predict(pd.DataFrame([[suhu_asumsi]], columns=['suhu_avg_periode']))[0]
|
| 355 |
+
laju_proj = 0.4 * laju_proj + 0.6 * laju_recent # weighted
|
| 356 |
+
|
| 357 |
+
proj_tebal = [max(TEBAL_TERAKHIR + laju_proj * i, 95) for i in range(0, proj_hari+1)]
|
| 358 |
+
|
| 359 |
+
ax1.plot(proj_dates, proj_tebal,
|
| 360 |
+
color='darkorange', linewidth=2, linestyle='--',
|
| 361 |
+
label=f'Proyeksi 90 Hari (laju={laju_proj:.4f} mm/hari)', zorder=4)
|
| 362 |
+
|
| 363 |
+
# Confidence band proyeksi
|
| 364 |
+
laju_hi = laju_proj * 0.7 # lebih lambat (optimis)
|
| 365 |
+
laju_lo = laju_proj * 1.3 # lebih cepat (pesimis)
|
| 366 |
+
proj_hi = [max(TEBAL_TERAKHIR + laju_hi * i, 95) for i in range(0, proj_hari+1)]
|
| 367 |
+
proj_lo = [max(TEBAL_TERAKHIR + laju_lo * i, 95) for i in range(0, proj_hari+1)]
|
| 368 |
+
ax1.fill_between(proj_dates, proj_lo, proj_hi, alpha=0.15, color='darkorange', label='Range Proyeksi (±30%)')
|
| 369 |
+
|
| 370 |
+
# Garis batas
|
| 371 |
+
ax1.axhline(y=BATAS_KRITIS, color='red', linestyle=':', linewidth=2, label=f'Batas Kritis ({BATAS_KRITIS}mm)')
|
| 372 |
+
ax1.axhline(y=BATAS_WARNING, color='orange', linestyle='--', linewidth=1.5, label=f'Batas Warning ({BATAS_WARNING}mm)')
|
| 373 |
+
ax1.axvline(x=tgl_terakhir, color='gray', linestyle=':', linewidth=1, label='Data Terakhir')
|
| 374 |
+
|
| 375 |
+
# Annotasi kapan kritis
|
| 376 |
+
for i, (tgl, tp) in enumerate(zip(proj_dates, proj_tebal)):
|
| 377 |
+
if tp <= BATAS_KRITIS:
|
| 378 |
+
ax1.annotate(f'Est. Kritis:\n{tgl.strftime("%d %b %Y")}',
|
| 379 |
+
xy=(tgl, BATAS_KRITIS), xytext=(-100, 30),
|
| 380 |
+
textcoords='offset points', color='red', fontsize=9, fontweight='bold',
|
| 381 |
+
arrowprops=dict(arrowstyle='->', color='red', lw=1.5))
|
| 382 |
+
break
|
| 383 |
+
|
| 384 |
+
ax1.set_title('Historis Ketebalan BTA & Proyeksi ke Depan', fontsize=12)
|
| 385 |
+
ax1.set_ylabel('Ketebalan (mm)')
|
| 386 |
+
ax1.legend(loc='upper right', fontsize=8.5)
|
| 387 |
+
ax1.xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))
|
| 388 |
+
ax1.xaxis.set_major_locator(mdates.MonthLocator(interval=2))
|
| 389 |
+
plt.setp(ax1.xaxis.get_majorticklabels(), rotation=30)
|
| 390 |
+
ax1.set_ylim(90, 245)
|
| 391 |
+
|
| 392 |
+
# --- Panel 2: Suhu Monitoring ---
|
| 393 |
+
ax2 = axes[1]
|
| 394 |
+
suhu_avg_series = (df['cone_depan'] + df['body_tengah'] + df['cone_belakang']) / 3
|
| 395 |
+
ma7 = suhu_avg_series.rolling(7, min_periods=1).mean()
|
| 396 |
+
|
| 397 |
+
ax2.plot(df['tanggal'], suhu_avg_series, color='lightcoral', alpha=0.35, linewidth=1, label='Suhu Avg (Raw)')
|
| 398 |
+
ax2.plot(df['tanggal'], ma7, color='crimson', linewidth=2, label='Suhu Avg MA-7')
|
| 399 |
+
ax2.axhline(y=BATAS_SUHU, color='darkred', linestyle='--', linewidth=1.5, label=f'Batas Suhu ({BATAS_SUHU}°C)')
|
| 400 |
+
ax2.axhline(y=WARN_SUHU, color='orange', linestyle=':', linewidth=1.2, label=f'Warning Suhu ({WARN_SUHU}°C)')
|
| 401 |
+
ax2.axvline(x=tgl_terakhir, color='gray', linestyle=':', linewidth=1)
|
| 402 |
+
|
| 403 |
+
ax2.set_title('Monitoring Suhu Harian', fontsize=12)
|
| 404 |
+
ax2.set_ylabel('Suhu (°C)')
|
| 405 |
+
ax2.set_xlabel('Tanggal')
|
| 406 |
+
ax2.legend(fontsize=8.5)
|
| 407 |
+
ax2.xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))
|
| 408 |
+
ax2.xaxis.set_major_locator(mdates.MonthLocator(interval=2))
|
| 409 |
+
plt.setp(ax2.xaxis.get_majorticklabels(), rotation=30)
|
| 410 |
+
|
| 411 |
+
plt.tight_layout()
|
| 412 |
+
output_monitoring = f'bta_monitoring_{base_name}.png'
|
| 413 |
+
plt.savefig(output_monitoring, dpi=150, bbox_inches='tight')
|
| 414 |
+
plt.show()
|
| 415 |
+
print(f"✅ Grafik disimpan: '{output_monitoring}'")
|
| 416 |
+
|
| 417 |
+
|
| 418 |
+
# ============================================================
|
| 419 |
+
# CELL 8: Simpan Model & State
|
| 420 |
+
# ============================================================
|
| 421 |
+
import pickle
|
| 422 |
+
|
| 423 |
+
output_model = f'model_rate_{base_name}.pkl'
|
| 424 |
+
with open(output_model, 'wb') as f:
|
| 425 |
+
pickle.dump(model_rate, f)
|
| 426 |
+
|
| 427 |
+
# Simpan juga dalam format native JSON untuk kompatibilitas Hugging Face
|
| 428 |
+
output_model_json = f'model_rate_{base_name}.json'
|
| 429 |
+
model_rate.save_model(output_model_json)
|
| 430 |
+
model_rate.save_model('xgboost_bta.json')
|
| 431 |
+
|
| 432 |
+
state = {
|
| 433 |
+
'T0' : T0,
|
| 434 |
+
'tebal_terakhir' : TEBAL_TERAKHIR,
|
| 435 |
+
'tanggal_ukur' : TANGGAL_UKUR,
|
| 436 |
+
'hari_ukur' : HARI_UKUR,
|
| 437 |
+
'laju_mean' : laju_mean,
|
| 438 |
+
'laju_recent' : laju_recent,
|
| 439 |
+
'batas_kritis' : BATAS_KRITIS,
|
| 440 |
+
'batas_warning' : BATAS_WARNING,
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
output_state = f'bta_state_{base_name}.pkl'
|
| 444 |
+
with open(output_state, 'wb') as f:
|
| 445 |
+
pickle.dump(state, f)
|
| 446 |
+
|
| 447 |
+
print("\n✅ Model & State disimpan:")
|
| 448 |
+
print(f" 📦 {output_model} — Model laju aus XGBoost (Pickle)")
|
| 449 |
+
print(f" 📦 xgboost_bta.json — Model laju aus XGBoost (JSON - HF compatible)")
|
| 450 |
+
print(f" 📦 {output_state} — State pengukuran terakhir")
|
| 451 |
+
print("\n💡 TIP PENGGUNAAN HARIAN:")
|
| 452 |
+
print(" Setiap ada pengukuran BTA baru, gunakan parameter tebal_aktual=XXX")
|
| 453 |
+
print(" agar anchor diperbarui dan prediksi makin akurat.")
|
| 454 |
+
print(" Contoh: predict_bta_daily(350, 320, 360, tebal_aktual=118)")
|
| 455 |
+
|
| 456 |
+
"""##ACTUAL VS PREDICT"""
|
| 457 |
+
|
| 458 |
+
import pandas as pd
|
| 459 |
+
|
| 460 |
+
pred_values = []
|
| 461 |
+
pred_dates = []
|
| 462 |
+
|
| 463 |
+
# set patokan awal dari data pertama
|
| 464 |
+
current_anchor = df['ketebalan'].iloc[0]
|
| 465 |
+
anchor_date = df['tanggal'].iloc[0]
|
| 466 |
+
|
| 467 |
+
for index, row in df.iterrows():
|
| 468 |
+
# update patokan kalau ada data ukur manual di hari itu
|
| 469 |
+
if row['tanggal'] in df_ukur['tanggal'].values:
|
| 470 |
+
current_anchor = row['ketebalan']
|
| 471 |
+
anchor_date = row['tanggal']
|
| 472 |
+
|
| 473 |
+
suhu_avg = (row['cone_depan'] + row['body_tengah'] + row['cone_belakang']) / 3
|
| 474 |
+
|
| 475 |
+
# jalanin modelnya buat nebak laju aus
|
| 476 |
+
laju_pred = model_rate.predict(pd.DataFrame([[suhu_avg]], columns=['suhu_avg_periode']))[0]
|
| 477 |
+
|
| 478 |
+
# gabungin tebakan model sama rata-rata laju terbaru biar stabil
|
| 479 |
+
laju_efektif = 0.4 * laju_pred + 0.6 * laju_recent
|
| 480 |
+
|
| 481 |
+
hari_sejak = (row['tanggal'] - anchor_date).days
|
| 482 |
+
tebal_pred = current_anchor + (laju_efektif * hari_sejak)
|
| 483 |
+
|
| 484 |
+
pred_values.append(tebal_pred)
|
| 485 |
+
pred_dates.append(row['tanggal'])
|
| 486 |
+
|
| 487 |
+
# jadiin format series sesuai yang diminta matplotlib kamu
|
| 488 |
+
historical_predictions = pd.Series(pred_values, index=pred_dates)
|
| 489 |
+
|
| 490 |
+
import matplotlib.pyplot as plt
|
| 491 |
+
import matplotlib.dates as mdates
|
| 492 |
+
import pandas as pd
|
| 493 |
+
|
| 494 |
+
# Ensure necessary variables are available from previous cells.
|
| 495 |
+
# df, df_ukur, historical_predictions, BATAS_KRITIS, BATAS_WARNING
|
| 496 |
+
|
| 497 |
+
# Plotting the comparison
|
| 498 |
+
fig, ax = plt.subplots(figsize=(16, 8))
|
| 499 |
+
|
| 500 |
+
# Actual BTA thickness (stepwise)
|
| 501 |
+
ax.step(df['tanggal'], df['ketebalan'], where='post',
|
| 502 |
+
color='royalblue', linewidth=2.5, label='Ketebalan Aktual (Pengukuran Manual)', zorder=3)
|
| 503 |
+
ax.scatter(df_ukur['tanggal'], df_ukur['ketebalan'],
|
| 504 |
+
color='royalblue', s=80, zorder=5, label='Titik Pengukuran Aktual')
|
| 505 |
+
|
| 506 |
+
# Predicted BTA thickness (model-based) - using the pre-calculated historical_predictions
|
| 507 |
+
ax.plot(historical_predictions.index, historical_predictions.values,
|
| 508 |
+
color='darkgreen', linestyle='-', linewidth=1.5, label='Prediksi Model Harian', zorder=2)
|
| 509 |
+
|
| 510 |
+
# Add thresholds
|
| 511 |
+
ax.axhline(y=BATAS_KRITIS, color='red', linestyle=':', linewidth=2, label=f'Batas Kritis ({BATAS_KRITIS}mm)')
|
| 512 |
+
ax.axhline(y=BATAS_WARNING, color='orange', linestyle='--', linewidth=1.5, label=f'Batas Warning ({BATAS_WARNING}mm)')
|
| 513 |
+
|
| 514 |
+
ax.set_title('Perbandingan Ketebalan BTA Aktual vs. Prediksi Model Historis', fontsize=15, fontweight='bold')
|
| 515 |
+
ax.set_ylabel('Ketebalan (mm)')
|
| 516 |
+
ax.set_xlabel('Tanggal')
|
| 517 |
+
ax.legend(loc='upper right', fontsize=10)
|
| 518 |
+
ax.xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))
|
| 519 |
+
ax.xaxis.set_major_locator(mdates.MonthLocator(interval=2))
|
| 520 |
+
plt.setp(ax.xaxis.get_majorticklabels(), rotation=30)
|
| 521 |
+
ax.set_ylim(90, 245) # Consistent y-axis with previous plots
|
| 522 |
+
plt.grid(True)
|
| 523 |
+
plt.tight_layout()
|
| 524 |
+
output_comparison = f'bta_comparison_{base_name}.png'
|
| 525 |
+
plt.savefig(output_comparison, dpi=150, bbox_inches='tight')
|
| 526 |
+
plt.show()
|
| 527 |
+
|
| 528 |
+
print(f"✅ Grafik perbandingan aktual vs. prediksi historis ditampilkan dan disimpan ke '{output_comparison}'.")
|
| 529 |
+
|
| 530 |
+
# Input suhu dari pengguna
|
| 531 |
+
t_depan_input = float(input("Masukkan suhu Cone Depan (°C): "))
|
| 532 |
+
t_tengah_input = float(input("Masukkan suhu Bodi Tengah (°C): "))
|
| 533 |
+
t_belakang_input = float(input("Masukkan suhu Cone Belakang (°C): "))
|
| 534 |
+
|
| 535 |
+
# Panggil fungsi prediksi dengan input suhu
|
| 536 |
+
result = predict_bta_daily(t_depan=t_depan_input,
|
| 537 |
+
t_tengah=t_tengah_input,
|
| 538 |
+
t_belakang=t_belakang_input)
|
| 539 |
+
|
| 540 |
+
print("\nRingkasan Prediksi:")
|
| 541 |
+
for key, value in result.items():
|
| 542 |
+
print(f"- {key.replace('_', ' ').title()}: {value}")
|
push_to_hf.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
# Fix httpx NO_PROXY parsing bug on Windows when it contains IPv6 loopback '::1'
|
| 4 |
+
for env_var in ["NO_PROXY", "no_proxy"]:
|
| 5 |
+
if env_var in os.environ:
|
| 6 |
+
os.environ[env_var] = os.environ[env_var].replace(",::1", "").replace("::1", "")
|
| 7 |
+
|
| 8 |
+
from huggingface_hub import HfApi
|
| 9 |
+
|
| 10 |
+
# Target repository on Hugging Face
|
| 11 |
+
repo_id = "Rendhaputra/BTA_predictive"
|
| 12 |
+
|
| 13 |
+
# Cek token dari environment variable
|
| 14 |
+
token = os.environ.get("HF_TOKEN")
|
| 15 |
+
if not token:
|
| 16 |
+
print("🔑 Hugging Face Token tidak ditemukan di environment variables.")
|
| 17 |
+
token = input("Masukkan Hugging Face Token Anda (dengan akses WRITE): ").strip()
|
| 18 |
+
|
| 19 |
+
if not token:
|
| 20 |
+
print("❌ Token diperlukan untuk mengunggah berkas ke Hugging Face!")
|
| 21 |
+
exit(1)
|
| 22 |
+
|
| 23 |
+
api = HfApi()
|
| 24 |
+
|
| 25 |
+
# Upload both XGBoost and Prophet models
|
| 26 |
+
files_to_upload = ["xgboost_bta.json", "model_prophet_bta.json"]
|
| 27 |
+
|
| 28 |
+
for file_name in files_to_upload:
|
| 29 |
+
if os.path.exists(file_name):
|
| 30 |
+
try:
|
| 31 |
+
print(f"📤 Sedang mengunggah '{file_name}' ke repo '{repo_id}'...")
|
| 32 |
+
api.upload_file(
|
| 33 |
+
path_or_fileobj=file_name,
|
| 34 |
+
path_in_repo=file_name,
|
| 35 |
+
repo_id=repo_id,
|
| 36 |
+
token=token,
|
| 37 |
+
repo_type="model"
|
| 38 |
+
)
|
| 39 |
+
print(f"✅ Berkas '{file_name}' berhasil diunggah!")
|
| 40 |
+
except Exception as e:
|
| 41 |
+
print(f"❌ Terjadi kesalahan saat mengunggah '{file_name}': {e}")
|
| 42 |
+
else:
|
| 43 |
+
print(f"⚠️ Berkas '{file_name}' tidak ditemukan, melewati...")
|
| 44 |
+
|
| 45 |
+
print(f"\n🔗 Tautan repositori: https://huggingface.co/{repo_id}/tree/main")
|
requirements.txt
CHANGED
|
@@ -5,4 +5,5 @@ pandas
|
|
| 5 |
scikit-learn
|
| 6 |
matplotlib
|
| 7 |
seaborn
|
| 8 |
-
plotly
|
|
|
|
|
|
| 5 |
scikit-learn
|
| 6 |
matplotlib
|
| 7 |
seaborn
|
| 8 |
+
plotly
|
| 9 |
+
prophet
|