From 15eda61833000f359929055dcdc6129d50b4e9ca Mon Sep 17 00:00:00 2001 From: Azmikun1 Date: Wed, 17 Dec 2025 15:27:30 +0700 Subject: [PATCH] new --- app.py | 554 ++++++++++++++++++--------------------------------------- 1 file changed, 177 insertions(+), 377 deletions(-) diff --git a/app.py b/app.py index 22b1816..c3545b2 100644 --- a/app.py +++ b/app.py @@ -1,4 +1,3 @@ -import requests import streamlit as st import joblib import pandas as pd @@ -12,461 +11,262 @@ import matplotlib.pyplot as plt import matplotlib.dates as mdates from matplotlib import rcParams -# Set font untuk mendukung karakter Indonesia +# Set font rcParams['font.family'] = 'DejaVu Sans' # --- Konfigurasi Halaman --- -st.set_page_config( - page_title="Prediksi Harga Ethereum (GRU)", - page_icon="๐Ÿช™" -) +st.set_page_config(page_title="Prediksi Harga Ethereum (GRU)", page_icon="๐Ÿช™") -# --- GLOBAL STYLES (UI only) --- +# --- GLOBAL STYLES --- st.markdown(""" """, unsafe_allow_html=True) +# --- TOMBOL RESET CACHE (PENTING!) --- +with st.sidebar: + st.header("โš™๏ธ Kontrol Data") + if st.button("๐Ÿ”„ Paksa Update Data (Clear Cache)"): + st.cache_data.clear() + st.success("Cache dihapus! Silakan tekan 'R' untuk reload.") + st.stop() # Hentikan app sebentar biar user reload -# --- Fungsi-fungsi Bantuan --- - +# --- FUNGSI LOAD DATA (VERSI YFINANCE ONLY) --- @st.cache_data(ttl="1h") def load_eth_data(): """ - Metode Hybrid (Tumpuk Data) dengan Debug Info. + Fokus: Download via Library yfinance. + Fitur: + 1. Mengatasi data bolong (Resample Daily). + 2. Auto Adjust harga (OHLC bersih). """ ticker = "ETH-USD" - df_final = None - # --- BAGIAN 1: BACA DATA LAMA (BASE) --- + # Kita set start date agak jauh biar grafiknya bagus + start_date = "2024-01-01" + end_date = date.today() + timedelta(days=1) + + st.toast("Sedang menghubungi server Yahoo Finance...", icon="โณ") + try: - df_base = pd.read_csv("eth_backup.csv") + # DOWNLOAD ONLINE + df = yf.download( + ticker, + start=start_date, + end=end_date, + progress=False, + auto_adjust=True, # Biar harga bersih + multi_level_index=False + ) - # Bersihkan kolom sampah - if "Unnamed: 0" in df_base.columns: - df_base = df_base.drop(columns=["Unnamed: 0"]) - - # Standarisasi kolom Date - if "Date" not in df_base.columns: - df_base = df_base.rename(columns={df_base.columns[0]: "Date"}) - - df_base["Date"] = pd.to_datetime(df_base["Date"]).dt.tz_localize(None) - - except FileNotFoundError: - df_base = pd.DataFrame(columns=["Date", "Open", "High", "Low", "Close", "Volume"]) - - # --- BAGIAN 2: HITUNG TANGGAL DOWNLOAD --- - today = date.today() - - if not df_base.empty: - last_date_csv = df_base["Date"].max() - start_download = last_date_csv + timedelta(days=1) - else: - start_download = pd.to_datetime("2020-01-01") - last_date_csv = "Kosong" - - # DEBUG INFO DI SIDEBAR (Supaya kelihatan prosesnya) - with st.sidebar: - st.write(f"๐Ÿ“‚ **Status Data:**") - st.write(f"- Terakhir di CSV: `{str(last_date_csv).split()[0]}`") - st.write(f"- Target Download: `{str(start_download.date())}` s.d `{today}`") - - # --- BAGIAN 3: DOWNLOAD & GABUNG --- - # Hanya download jika tanggal download <= hari ini - if start_download.date() <= today: - try: - df_new = yf.download( - ticker, - start=start_download, - end=today + timedelta(days=1), - progress=False, - auto_adjust=True, - multi_level_index=False - ) + if df is not None and not df.empty: + # 1. Bersihkan Index + df = df.reset_index() - if df_new is not None and not df_new.empty: - df_new = df_new.reset_index() - - # Rapikan kolom - new_cols = [] - for col in df_new.columns: - col_name = col[0] if isinstance(col, tuple) else str(col) - new_cols.append(col_name) - df_new.columns = new_cols - - if 'Date' not in df_new.columns: - df_new = df_new.rename(columns={df_new.columns[0]: 'Date'}) - - df_new['Date'] = pd.to_datetime(df_new['Date']).dt.tz_localize(None) - - # INFO DEBUG - st.sidebar.success(f"โœ… Berhasil download {len(df_new)} hari baru!") - - # GABUNGKAN - df_final = pd.concat([df_base, df_new], ignore_index=True) - - else: - st.sidebar.warning("โš ๏ธ Download dijalankan tapi data kosong (Mungkin libur/belum close).") - df_final = df_base - - except Exception as e: - st.sidebar.error(f"โŒ Gagal update online: {e}") - df_final = df_base - else: - st.sidebar.info("โœ… Data CSV sudah paling update.") - df_final = df_base + # 2. Rapikan Kolom (Cegah MultiIndex/Tuple) + new_cols = [] + for col in df.columns: + col_name = col[0] if isinstance(col, tuple) else str(col) + new_cols.append(col_name) + df.columns = new_cols + + # 3. Pastikan kolom Date ada + if 'Date' not in df.columns: + df = df.rename(columns={df.columns[0]: 'Date'}) + + # 4. Hapus Timezone + df['Date'] = pd.to_datetime(df['Date']).dt.tz_localize(None) + + # --- BAGIAN PENTING: TAMBAL DATA BOLONG (RESAMPLING) --- + # Ini mengatasi masalah "Loncat" dari tgl 15 ke 17. + # Kita paksa buat tanggal harian (Daily) lengkap. + df = df.sort_values('Date').set_index('Date') + + # Buat range tanggal penuh dari awal sampai akhir data + full_idx = pd.date_range(start=df.index.min(), end=df.index.max(), freq='D') + + # Reindex & Forward Fill (Isi kekosongan dengan data hari sebelumnya) + df = df.reindex(full_idx).ffill().reset_index() + df = df.rename(columns={'index': 'Date'}) + # ------------------------------------------------------- - # --- FINALISASI --- - if df_final is not None and not df_final.empty: - df_final = df_final.drop_duplicates(subset="Date", keep="last") - df_final = df_final.sort_values("Date").reset_index(drop=True) + # 5. Simpan Backup Otomatis + try: + df.to_csv("eth_backup.csv", index=False) + except: + pass + + return df, "online" + + except Exception as e: + print(f"Error yfinance: {e}") + + # FALLBACK: JIKA DOWNLOAD GAGAL, BACA BACKUP LAMA + try: + df_backup = pd.read_csv("eth_backup.csv") + if "Unnamed: 0" in df_backup.columns: df_backup = df_backup.drop(columns=["Unnamed: 0"]) + if "Date" not in df_backup.columns: df_backup = df_backup.rename(columns={df_backup.columns[0]: "Date"}) + df_backup["Date"] = pd.to_datetime(df_backup["Date"]).dt.tz_localize(None) - target_cols = ['Date', 'Open', 'High', 'Low', 'Close', 'Volume'] - available = [c for c in target_cols if c in df_final.columns] - df_final = df_final[available] + return df_backup, "backup" + except: + return None, "error" - return df_final, "mixed" - return None, "error" - +# --- Fungsi Helper Lainnya (Model & Scaler) --- def validate_scaler(scaler): - """Validasi scaler agar konsisten dengan training.""" - issues = [] - warnings = [] - - if not isinstance(scaler, MinMaxScaler): - warnings.append(f"Scaler bukan MinMaxScaler (terdeteksi: {type(scaler).__name__}). Pastikan ini scaler training.") - - required_attrs = ["n_features_in_", "data_min_", "data_max_", "scale_", "min_"] - for a in required_attrs: - if not hasattr(scaler, a): - issues.append(f"Scaler belum ter-fit atau tidak valid (atribut '{a}' tidak ada).") - - if hasattr(scaler, "n_features_in_"): - if int(scaler.n_features_in_) != 1: - issues.append(f"Scaler mengharapkan {scaler.n_features_in_} fitur, tapi aplikasi hanya pakai 1 fitur ('Close').") - - ok = (len(issues) == 0) - return ok, issues, warnings - + issues, warnings = [], [] + if not isinstance(scaler, MinMaxScaler): warnings.append(f"Scaler bukan MinMaxScaler.") + return (len(issues)==0), issues, warnings def validate_model_input(model): - """Pastikan input model bentuknya (None, time_step, 1).""" issues = [] try: - shape = model.input_shape # biasanya (None, time_step, 1) - if not (isinstance(shape, (list, tuple)) and len(shape) == 3): - issues.append(f"input_shape tidak sesuai harapan: {shape} (harus 3 dimensi).") - else: - if shape[2] != 1: - issues.append(f"Jumlah fitur input model = {shape[2]}, tapi aplikasi membentuk fitur=1.") - if shape[1] is None or int(shape[1]) <= 1: - issues.append(f"time_step tidak valid: {shape[1]}.") - except Exception as e: - issues.append(f"Gagal membaca input_shape model: {e}") - - ok = (len(issues) == 0) - return ok, issues - + shape = model.input_shape + if shape[2] != 1: issues.append(f"Fitur model = {shape[2]}, input = 1.") + except: pass + return (len(issues)==0), issues @st.cache_resource def load_gru_assets(): - """Load model & scaler hasil training (wajib pakai scaler yang sama).""" model_path = Path("gru_model.h5") scaler_path = Path("scaler_gru.pkl") - - if not model_path.exists() or not scaler_path.exists(): - st.warning( - f"File tidak ditemukan.\n" - f"Model: {model_path.resolve().name} ada? {model_path.exists()}\n" - f"Scaler: {scaler_path.resolve().name} ada? {scaler_path.exists()}" - ) - return None, None - + if not model_path.exists() or not scaler_path.exists(): return None, None try: model = load_model(model_path, compile=False) scaler = joblib.load(scaler_path) - - ok_scaler, scaler_issues, scaler_warnings = validate_scaler(scaler) - for w in scaler_warnings: - st.warning("โš ๏ธ " + w) - if not ok_scaler: - st.error("โŒ Scaler tidak kompatibel:\n- " + "\n- ".join(scaler_issues)) - return None, None - - ok_model, model_issues = validate_model_input(model) - if not ok_model: - st.error("โŒ Model input tidak kompatibel:\n- " + "\n- ".join(model_issues)) - return None, None - return model, scaler - - except Exception as e: - st.error(f"Gagal load aset: {e}") - return None, None - + except: return None, None def predict_from_sequence_pure(model, initial_sequence_scaled, n_days): - - - seq = np.array(initial_sequence_scaled, dtype="float32").reshape(-1) # (time_step,) + seq = np.array(initial_sequence_scaled, dtype="float32").reshape(-1) preds = [] - for _ in range(n_days): - x = seq.reshape(1, -1, 1) # (1, time_step, 1) - y = float(model.predict(x, verbose=0)[0, 0]) # output model (scaled) + x = seq.reshape(1, -1, 1) + y = float(model.predict(x, verbose=0)[0, 0]) preds.append(y) - seq = np.append(seq[1:], y) # autoregressive (murni) - + seq = np.append(seq[1:], y) return np.array(preds, dtype="float32").reshape(-1, 1) - def create_combined_chart(df, start_date, future_dates, future_predictions): - """Membuat grafik gabungan dengan gaya yang sama seperti referensi.""" - context_start_date = start_date - timedelta(days=60) - recent_df = df[(df["Date"] >= context_start_date) & (df["Date"] <= start_date)].copy() - - window_size = 7 - recent_df["Trend_Aktual"] = recent_df["Close"].rolling(window=window_size).mean() - - predictions_flat = future_predictions.flatten() - + context_start = start_date - timedelta(days=60) + recent_df = df[(df["Date"] >= context_start) & (df["Date"] <= start_date)].copy() + recent_df["Trend"] = recent_df["Close"].rolling(window=7).mean() + fig, ax = plt.subplots(figsize=(14, 8)) + ax.plot(recent_df["Date"], recent_df["Close"], color="gray", alpha=0.5, label="Harga Aktual") + ax.plot(recent_df["Date"], recent_df["Trend"], color="#1f77b4", linewidth=2, label="Tren Aktual") + ax.plot(future_dates, future_predictions.flatten(), color="#d62728", linewidth=2, marker="o", markersize=4, label="Prediksi GRU") + + if not recent_df.empty: + ax.plot([recent_df["Date"].iloc[-1], future_dates[0]], + [recent_df["Trend"].dropna().iloc[-1], future_predictions.flatten()[0]], + color="#d62728", linestyle="--", alpha=0.7) - ax.plot( - recent_df["Date"], recent_df["Close"], - color="gray", linewidth=1.0, label="Harga Aktual (Harian)", alpha=0.5 - ) - ax.plot( - recent_df["Date"], recent_df["Trend_Aktual"], - color="#1f77b4", linewidth=2.5, label="Tren Harga Aktual", alpha=0.9 - ) - ax.plot( - future_dates, predictions_flat, - color="#d62728", linewidth=2.5, label="Harga Prediksi (GRU)", - alpha=0.9, marker="o", markersize=4 - ) - ax.scatter( - recent_df["Date"], recent_df["Close"], - color="black", s=15, alpha=0.6, zorder=5, label="Data Harian Aktual" - ) - - # garis penghubung trend terakhir ke prediksi pertama - try: - last_trend_date = recent_df["Date"].iloc[-1] - last_trend_price = recent_df["Trend_Aktual"].dropna().iloc[-1] - first_prediction_date = future_dates[0] - first_prediction_price = predictions_flat[0] - - ax.plot( - [last_trend_date, first_prediction_date], - [last_trend_price, first_prediction_price], - color="#d62728", linestyle="--", linewidth=2.0, alpha=0.7 - ) - except Exception: - pass - - ax.set_xlabel("Waktu", fontsize=12, fontweight="bold") - ax.set_ylabel("Harga (USD)", fontsize=12, fontweight="bold") - ax.set_title("Analisis Tren Historis dan Prediksi Harga Ethereum", fontsize=16, fontweight="bold", pad=20) - + ax.set_title("Analisis Tren & Prediksi Ethereum") ax.xaxis.set_major_formatter(mdates.DateFormatter("%b %d, %Y")) plt.xticks(rotation=45) - ax.grid(True, alpha=0.3, linestyle="--") - - handles, labels = ax.get_legend_handles_labels() - order = [2, 0, 1, 3] - if len(handles) == 4: - ax.legend([handles[idx] for idx in order], [labels[idx] for idx in order], loc="upper left", fontsize=11) - else: - ax.legend(loc="upper left", fontsize=11) - - plt.tight_layout() - ax.set_facecolor("#f8f9fa") - fig.patch.set_facecolor("white") - + ax.legend() + ax.grid(True, alpha=0.3) return fig -# --- Tampilan Antarmuka Aplikasi --- +# --- MAIN UI --- st.markdown("
Prediksi Harga Ethereum (GRU)
", unsafe_allow_html=True) st.markdown("
Data historis & prediksi ETH-USD berbasis GRU
", unsafe_allow_html=True) -df, data_source = load_eth_data() +# LOAD DATA +df, status = load_eth_data() + +# Notifikasi Status +if status == "backup": + st.toast("Koneksi Yahoo Gagal. Pakai Backup.", icon="โš ๏ธ") +elif status == "error": + st.error("โŒ Gagal memuat data (Library gagal & Backup tidak ada).") + st.stop() + +# LOAD MODEL model, scaler = load_gru_assets() -# LOGIC UI (TOAST/WARNING) DITARUH DI SINI (DILUAR CACHE) -if data_source == "backup": - st.toast("Koneksi Yahoo lambat. Menggunakan data backup lokal.", icon="โš ๏ธ") -elif data_source == "error": - st.error("โŒ Gagal memuat data (Online gagal & Backup tidak ada).") - if df is not None and model is not None and scaler is not None: - # Menampilkan tabel data historis + + # INFO UPDATE DATA + last_date = df["Date"].max() + st.info(f"๐Ÿ“… Data Terupdate sampai: **{last_date.strftime('%d %B %Y')}**") + + # 1. Tampilkan Data st.markdown("
", unsafe_allow_html=True) st.markdown("
๐Ÿ“š Data Historis Harga Ethereum
", unsafe_allow_html=True) - st.dataframe(df, height=300, use_container_width=True) + st.dataframe(df.tail(10).sort_values("Date", ascending=False), height=300, use_container_width=True) # Tampilkan 10 data terakhir biar kelihatan update st.markdown("
", unsafe_allow_html=True) - # Menampilkan visualisasi line chart + # 2. Chart Historis st.markdown("
", unsafe_allow_html=True) st.markdown("
๐Ÿ“ˆ Visualisasi Harga Historis
", unsafe_allow_html=True) - st.line_chart(df.rename(columns={"Date": "index"}).set_index("index")["Close"]) + st.line_chart(df.set_index("Date")["Close"]) st.markdown("
", unsafe_allow_html=True) + # 3. Prediksi st.markdown("
", unsafe_allow_html=True) - st.markdown("
๐ŸŽฏ Mulai Prediksi Berdasarkan Tanggal
", unsafe_allow_html=True) - + st.markdown("
๐ŸŽฏ Mulai Prediksi
", unsafe_allow_html=True) + time_step = int(model.input_shape[1]) - min_selectable_date = (df["Date"].min() + timedelta(days=time_step)).date() - max_selectable_date = df["Date"].max().date() + min_date = (df["Date"].min() + timedelta(days=time_step)).date() + max_date = df["Date"].max().date() - col1, col2 = st.columns(2) - with col1: - selected_date = st.date_input( - "Pilih tanggal mulai prediksi:", - value=max_selectable_date, - min_value=min_selectable_date, - max_value=max_selectable_date, - help=f"Pilih tanggal antara {min_selectable_date} dan {max_selectable_date}" - ) - with col2: - prediction_days = st.slider( - "Pilih jumlah hari untuk prediksi (1-30 hari):", - min_value=1, max_value=30, value=15, step=1 - ) + c1, c2 = st.columns(2) + with c1: + # Default value ke hari ini (max_date) + start_date = st.date_input("Mulai tanggal:", value=max_date, min_value=min_date, max_value=max_date) + with c2: + days = st.slider("Jumlah hari:", 1, 30, 15) - if st.button("Buat Prediksi", key="predict_button"): - if prediction_days < 1: - st.warning("Jumlah hari prediksi minimal **1**. Silakan geser slidernya dulu. ๐Ÿ™‚") - st.stop() - - with st.spinner("Memproses dan menjalankan prediksi..."): - # scaling (mengikuti scaler training) - close_values = df[["Close"]].values.astype(float) - scaled_data = scaler.transform(close_values) - - # Cocokkan berdasarkan tanggal (robust) - selected_date_dt = pd.to_datetime(selected_date) - mask = df["Date"].dt.date == selected_date_dt.date() - idxs = np.where(mask.to_numpy())[0] - - if len(idxs) == 0: - st.error(f"Tanggal {selected_date_dt.strftime('%Y-%m-%d')} tidak ditemukan dalam dataset.") - st.stop() - - start_index = int(idxs[0]) - - if start_index - time_step < 0: - st.error( - f"Tanggal terlalu awal untuk diprediksi. " - f"Butuh {time_step} hari data sebelumnya. Pilih tanggal setelah " - f"{(df['Date'].min() + timedelta(days=time_step)).strftime('%Y-%m-%d')}." - ) - st.stop() - - initial_sequence = scaled_data[start_index - time_step: start_index] # (time_step,1) - - # โœ… Prediksi MURNI dari model - future_predictions_scaled = predict_from_sequence_pure(model, initial_sequence, prediction_days) - - # โœ… Inverse transform MURNI dari scaler - future_predictions = scaler.inverse_transform(future_predictions_scaled) - - # sanity-check - if not np.isfinite(future_predictions).all(): - st.error("โŒ Hasil prediksi mengandung NaN/Inf. Periksa kompatibilitas model/scaler.") - st.stop() - - future_dates = [selected_date_dt + timedelta(days=i) for i in range(1, prediction_days + 1)] - - st.markdown("
", unsafe_allow_html=True) - st.subheader("๐Ÿ“Š Grafik Prediksi (Historis + Prediksi)") - fig = create_combined_chart(df, selected_date_dt, future_dates, future_predictions) - st.pyplot(fig) - st.markdown("
", unsafe_allow_html=True) - - st.markdown("
", unsafe_allow_html=True) - st.subheader("๐Ÿงพ Tabel Detail Prediksi") - prediction_table_df = pd.DataFrame({ - "Tanggal": [d.strftime("%Y-%m-%d") for d in future_dates], - "Harga Prediksi (USD)": [f"USD {price[0]:,.0f}" for price in future_predictions] - }) - prediction_table_df.index = prediction_table_df.index + 1 - st.dataframe(prediction_table_df, use_container_width=True) - st.markdown("
", unsafe_allow_html=True) - - last_price = float(df.loc[start_index, "Close"]) - max_prediction = float(np.max(future_predictions)) - min_prediction = float(np.min(future_predictions)) - - st.info(f""" -๐Ÿ“Š **Informasi Prediksi:** -- Harga terakhir pada tanggal {selected_date.strftime('%Y-%m-%d')}: USD {last_price:,.0f} -- Harga prediksi tertinggi: USD {max_prediction:,.0f} -- Harga prediksi terendah: USD {min_prediction:,.0f} -""") + if st.button("Buat Prediksi"): + with st.spinner("Memproses..."): + vals = df[["Close"]].values.astype(float) + scaled = scaler.transform(vals) + + check_date = pd.to_datetime(start_date) + # Cari index tanggal + idx = df[df["Date"].dt.date == check_date.date()].index + + if len(idx) > 0: + idx = idx[0] + if idx < time_step: + st.error(f"Data tidak cukup (butuh {time_step} hari sebelumnya).") + else: + seq = scaled[idx-time_step : idx] + pred_scaled = predict_from_sequence_pure(model, seq, days) + pred_real = scaler.inverse_transform(pred_scaled) + + f_dates = [check_date + timedelta(days=i) for i in range(1, days+1)] + + st.markdown("
", unsafe_allow_html=True) + fig = create_combined_chart(df, check_date, f_dates, pred_real) + st.pyplot(fig) + + res_df = pd.DataFrame({"Tanggal": [d.strftime("%Y-%m-%d") for d in f_dates], "Harga (USD)": pred_real.flatten()}) + st.dataframe(res_df, use_container_width=True) + st.markdown("
", unsafe_allow_html=True) + else: + st.error("Tanggal tidak ditemukan dalam dataset.") st.markdown("
", unsafe_allow_html=True) st.markdown("", unsafe_allow_html=True) elif model is None: - st.error("Gagal memuat model. Pastikan file 'gru_model.h5' ada di folder yang sama dengan aplikasi.") -elif scaler is None: - st.error("Gagal memuat scaler. Pastikan file 'scaler_gru.pkl' ada di folder yang sama dengan aplikasi.") -else: - st.error("Gagal memuat data. Coba cek koneksi internet atau sumber data yfinance.") + st.error("Gagal memuat Model/Scaler. Pastikan file .h5 dan .pkl ada.") \ No newline at end of file