Rendhaputra commited on
Commit
89065dc
·
1 Parent(s): cad0179

Integrasi model Prophet dan tombol selektor interaktif di dashboard

Browse files
Files changed (4) hide show
  1. bta_prophet.py +276 -0
  2. bta_xgboost.py +542 -0
  3. push_to_hf.py +45 -0
  4. 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