This commit is contained in:
Azmikun1 2025-12-18 08:30:49 +07:00
parent bb8fa42585
commit 4c6f66a4da
2 changed files with 233 additions and 209 deletions

440
app.py
View File

@ -3,10 +3,10 @@ import streamlit as st
import joblib import joblib
import pandas as pd import pandas as pd
import numpy as np import numpy as np
import os # Tambahan untuk manajemen file
from sklearn.preprocessing import MinMaxScaler from sklearn.preprocessing import MinMaxScaler
from tensorflow.keras.models import load_model from tensorflow.keras.models import load_model
from datetime import date, timedelta from datetime import date, timedelta
import yfinance as yf
from pathlib import Path from pathlib import Path
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import matplotlib.dates as mdates import matplotlib.dates as mdates
@ -18,9 +18,19 @@ rcParams['font.family'] = 'DejaVu Sans'
# --- Konfigurasi Halaman --- # --- Konfigurasi Halaman ---
st.set_page_config( st.set_page_config(
page_title="Prediksi Harga Ethereum (GRU)", page_title="Prediksi Harga Ethereum (GRU)",
page_icon="🪙" page_icon="🪙",
layout="wide" # Opsional: agar tampilan lebih luas
) )
# --- KONFIGURASI FILE ADMIN ---
DATA_FOLDER = 'dataset'
DATA_FILE = 'databackup_eth.csv'
DATA_PATH = os.path.join(DATA_FOLDER, DATA_FILE)
# Pastikan folder dataset ada
if not os.path.exists(DATA_FOLDER):
os.makedirs(DATA_FOLDER)
# --- GLOBAL STYLES (UI only) --- # --- GLOBAL STYLES (UI only) ---
st.markdown(""" st.markdown("""
<style> <style>
@ -32,6 +42,15 @@ html, body, [class*="css"] { font-family: "Inter", "DejaVu Sans", sans-serif; }
--ring: #e5e7eb; --ring: #e5e7eb;
} }
/* --- PERBAIKAN DISINI: MEMBATASI LEBAR KONTEN --- */
/* Ini memaksa konten tetap di tengah dengan lebar maksimal tertentu */
.block-container {
max-width: 1000px; /* Atur angka ini sesuai selera (misal 900px - 1200px) */
padding-top: 2rem;
padding-bottom: 2rem;
margin: auto; /* Posisi otomatis di tengah */
}
/* gradient title */ /* gradient title */
.app-title { .app-title {
text-align:center; text-align:center;
@ -81,74 +100,49 @@ html, body, [class*="css"] { font-family: "Inter", "DejaVu Sans", sans-serif; }
""", unsafe_allow_html=True) """, unsafe_allow_html=True)
# --- Fungsi-fungsi Bantuan --- # --- Fungsi-fungsi Bantuan (JANGAN UBAH LOGIKA UTAMA) ---
@st.cache_data(ttl="1h") @st.cache_data(ttl="1h")
def load_eth_data(): def load_eth_data():
ticker = "ETH-USD" """
df = None
"""
# 1. COBA ONLINE # Cek apakah file admin ada
try: if os.path.exists(DATA_PATH):
try:
df = yf.download( df = pd.read_csv(DATA_PATH)
ticker,
start="2024-01-01",
end=date.today() + timedelta(days=1),
progress=False,
auto_adjust=True,
multi_level_index=False
)
if df is not None and not df.empty:
# Bersihkan Index & Kolom
df = df.reset_index()
new_cols = [] # --- PEMBERSIHAN DATA AGAR SESUAI FORMAT MODEL ---
for col in df.columns: # 1. Hapus kolom index lama jika ada
col_name = col[0] if isinstance(col, tuple) else str(col) if "Unnamed: 0" in df.columns:
new_cols.append(col_name) df = df.drop(columns=["Unnamed: 0"])
df.columns = new_cols
# Pastikan kolom pertama adalah Date # 2. Standarisasi nama kolom Date
if 'Date' not in df.columns: if "Date" not in df.columns:
df = df.rename(columns={df.columns[0]: 'Date'}) # Coba cari kolom yang mirip 'date'
found = False
# Hapus Timezone for col in df.columns:
df['Date'] = pd.to_datetime(df['Date']).dt.tz_localize(None) if col.lower() == "date":
df = df.rename(columns={col: "Date"})
# Simpan Backup (Timpa file lama agar fresh) found = True
try: break
df.to_csv("eth_backup.csv", index=False) if not found:
except: # Jika tidak ada header Date, asumsikan kolom pertama adalah Date
pass df = df.rename(columns={df.columns[0]: 'Date'})
return df, "online" # 3. Konversi ke datetime
df["Date"] = pd.to_datetime(df["Date"])
except Exception as e: # Hapus timezone jika ada agar kompatibel dengan matplotlib/numpy
print(f"Gagal Online: {e}") if df["Date"].dt.tz is not None:
df["Date"] = df["Date"].dt.tz_localize(None)
# 2. COBA BACKUP (JIKA ONLINE GAGAL)
try: return df, "admin_file"
df_backup = pd.read_csv("eth_backup.csv")
# Bersihkan kolom sampah jika ada
if "Unnamed: 0" in df_backup.columns:
df_backup = df_backup.drop(columns=["Unnamed: 0"])
# Pastikan kolom Date dikenali except Exception as e:
if "Date" in df_backup.columns: return None, f"error_read: {str(e)}"
df_backup["Date"] = pd.to_datetime(df_backup["Date"]) else:
elif df_backup.columns[0].lower() == "date": return None, "no_file"
df_backup = df_backup.rename(columns={df_backup.columns[0]: "Date"})
df_backup["Date"] = pd.to_datetime(df_backup["Date"])
return df_backup, "backup"
except FileNotFoundError:
# 3. GAGAL TOTAL
return None, "error"
def validate_scaler(scaler): def validate_scaler(scaler):
"""Validasi scaler agar konsisten dengan training.""" """Validasi scaler agar konsisten dengan training."""
issues = [] issues = []
@ -191,44 +185,23 @@ def validate_model_input(model):
@st.cache_resource @st.cache_resource
def load_gru_assets(): def load_gru_assets():
"""Load model & scaler hasil training (wajib pakai scaler yang sama).""" """Load model & scaler hasil training."""
model_path = Path("gru_model.h5") model_path = Path("gru_model.h5")
scaler_path = Path("scaler_gru.pkl") scaler_path = Path("scaler_gru.pkl")
if not model_path.exists() or not scaler_path.exists(): 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 return None, None
try: try:
model = load_model(model_path, compile=False) model = load_model(model_path, compile=False)
scaler = joblib.load(scaler_path) 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 return model, scaler
except Exception as e: except Exception as e:
st.error(f"Gagal load aset: {e}") st.error(f"Gagal load aset: {e}")
return None, None return None, None
def predict_from_sequence_pure(model, initial_sequence_scaled, n_days): 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) # (time_step,)
preds = [] preds = []
@ -242,7 +215,7 @@ def predict_from_sequence_pure(model, initial_sequence_scaled, n_days):
def create_combined_chart(df, start_date, future_dates, future_predictions): def create_combined_chart(df, start_date, future_dates, future_predictions):
"""Membuat grafik gabungan dengan gaya yang sama seperti referensi.""" """Membuat grafik gabungan."""
context_start_date = start_date - timedelta(days=60) context_start_date = start_date - timedelta(days=60)
recent_df = df[(df["Date"] >= context_start_date) & (df["Date"] <= start_date)].copy() recent_df = df[(df["Date"] >= context_start_date) & (df["Date"] <= start_date)].copy()
@ -271,7 +244,6 @@ def create_combined_chart(df, start_date, future_dates, future_predictions):
color="black", s=15, alpha=0.6, zorder=5, label="Data Harian Aktual" color="black", s=15, alpha=0.6, zorder=5, label="Data Harian Aktual"
) )
# garis penghubung trend terakhir ke prediksi pertama
try: try:
last_trend_date = recent_df["Date"].iloc[-1] last_trend_date = recent_df["Date"].iloc[-1]
last_trend_price = recent_df["Trend_Aktual"].dropna().iloc[-1] last_trend_price = recent_df["Trend_Aktual"].dropna().iloc[-1]
@ -308,132 +280,184 @@ def create_combined_chart(df, start_date, future_dates, future_predictions):
return fig return fig
# --- Tampilan Antarmuka Aplikasi --- # --- HALAMAN: DASHBOARD PUBLIK (Landing Page) ---
def show_dashboard():
st.markdown("<div class='app-title'>Prediksi Harga Ethereum (GRU)</div>", unsafe_allow_html=True)
st.markdown("<div class='app-subtitle'>Data historis & prediksi ETH-USD berbasis GRU</div>", unsafe_allow_html=True)
st.markdown("<div class='app-title'>Prediksi Harga Ethereum (GRU)</div>", unsafe_allow_html=True) df, data_source = load_eth_data()
st.markdown("<div class='app-subtitle'>Data historis & prediksi ETH-USD berbasis GRU</div>", unsafe_allow_html=True) model, scaler = load_gru_assets()
df, data_source = load_eth_data() # LOGIC CHECK DATA
model, scaler = load_gru_assets() if data_source == "no_file":
st.warning("⚠️ Data historis belum tersedia. Silakan hubungi Admin untuk mengupload 'databackup_eth'.")
return # Stop eksekusi jika data tidak ada
elif "error" in data_source:
st.error(f"❌ Terjadi kesalahan membaca data: {data_source}")
return
# LOGIC UI (TOAST/WARNING) DITARUH DI SINI (DILUAR CACHE) if df is not None and model is not None and scaler is not None:
if data_source == "backup": # Menampilkan tabel data historis
st.toast("Koneksi Yahoo lambat. Menggunakan data backup lokal.", icon="⚠️") st.markdown("<div class='card'>", unsafe_allow_html=True)
elif data_source == "error": st.markdown("<div class='h-section'>📚 Data Historis Harga Ethereum </div>", unsafe_allow_html=True)
st.error("❌ Gagal memuat data (Online gagal & Backup tidak ada).")
# --- PERUBAHAN DISINI: HAPUS .tail(100) ---
# Menggunakan height=400 agar tabel lebih tinggi dan bisa di-scroll
st.dataframe(df, height=400, use_container_width=True)
# Menampilkan visualisasi line chart
st.markdown("<div class='card'>", unsafe_allow_html=True)
st.markdown("<div class='h-section'>📈 Visualisasi Harga Historis</div>", unsafe_allow_html=True)
# Grafik ini sudah otomatis mengambil full data df
st.line_chart(df.rename(columns={"Date": "index"}).set_index("index")["Close"])
st.markdown("</div>", unsafe_allow_html=True)
st.markdown("<div class='card'>", unsafe_allow_html=True)
st.markdown("<div class='h-section'>🎯 Mulai Prediksi Berdasarkan Tanggal</div>", 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()
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
)
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."
)
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("<div class='card'>", 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("</div>", unsafe_allow_html=True)
st.markdown("<div class='card'>", 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("</div>", 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}
""")
st.markdown("</div>", unsafe_allow_html=True)
st.markdown("<div class='footer'>Dibuat oleh Muhammad Gilman Nadhif Azmi</div>", 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.")
# --- HALAMAN: ADMIN LOGIN & UPLOAD ---
def show_admin_page():
st.title("🔐 Admin Panel")
st.write("Area khusus admin untuk memperbarui dataset harga Ethereum.")
# Simple Password check
password = st.text_input("Masukkan Password Admin:", type="password")
if df is not None and model is not None and scaler is not None: if password == "admin123": # Ganti password sesuai keinginan
# Menampilkan tabel data historis st.success("Akses Diterima.")
st.markdown("<div class='card'>", unsafe_allow_html=True) st.divider()
st.markdown("<div class='h-section'>📚 Data Historis Harga Ethereum</div>", unsafe_allow_html=True)
st.dataframe(df, height=300, use_container_width=True) st.subheader("📂 Upload Dataset Baru")
st.markdown("</div>", unsafe_allow_html=True) st.info(f"File akan disimpan sebagai: `{DATA_FILE}` di dalam folder sistem.")
uploaded_file = st.file_uploader("Upload file CSV (Format: Date, Close, dll)", type=['csv'])
if uploaded_file is not None:
# Baca preview
try:
df_preview = pd.read_csv(uploaded_file)
st.write("Preview Data:", df_preview.head())
if st.button("💾 Simpan & Update Sistem"):
# Simpan file ke path yang ditentukan
df_preview.to_csv(DATA_PATH, index=False)
# Clear cache agar halaman public langsung berubah
st.cache_data.clear()
st.success(f"Berhasil! Data telah diperbarui. Pengguna publik sekarang melihat data baru.")
except Exception as e:
st.error(f"File error: {e}")
elif password:
st.error("Password Salah.")
# Menampilkan visualisasi line chart
st.markdown("<div class='card'>", unsafe_allow_html=True)
st.markdown("<div class='h-section'>📈 Visualisasi Harga Historis</div>", unsafe_allow_html=True)
st.line_chart(df.rename(columns={"Date": "index"}).set_index("index")["Close"])
st.markdown("</div>", unsafe_allow_html=True)
st.markdown("<div class='card'>", unsafe_allow_html=True) # --- MAIN NAVIGATION (SIDEBAR) ---
st.markdown("<div class='h-section'>🎯 Mulai Prediksi Berdasarkan Tanggal</div>", unsafe_allow_html=True) # Ini adalah logika utama yang memisahkan tampilan Admin dan User Biasa
time_step = int(model.input_shape[1]) st.sidebar.title("Navigasi")
min_selectable_date = (df["Date"].min() + timedelta(days=time_step)).date() menu = st.sidebar.radio("Pilih Halaman:", ["Dashboard (Public)", "Admin Panel"])
max_selectable_date = df["Date"].max().date()
col1, col2 = st.columns(2) if menu == "Dashboard (Public)":
with col1: show_dashboard()
selected_date = st.date_input( elif menu == "Admin Panel":
"Pilih tanggal mulai prediksi:", show_admin_page()
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
)
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("<div class='card'>", 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("</div>", unsafe_allow_html=True)
st.markdown("<div class='card'>", 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("</div>", 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}
""")
st.markdown("</div>", unsafe_allow_html=True)
st.markdown("<div class='footer'>Dibuat oleh Muhammad Gilman Nadhif Azmi</div>", 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.")

View File

@ -715,4 +715,4 @@ Date,Close,High,Low,Open,Volume
2025-12-14,3060.5947265625,3128.622802734375,3034.692626953125,3116.743896484375,15619543350 2025-12-14,3060.5947265625,3128.622802734375,3034.692626953125,3116.743896484375,15619543350
2025-12-15,2964.18310546875,3175.1181640625,2899.685791015625,3060.4814453125,28765976892 2025-12-15,2964.18310546875,3175.1181640625,2899.685791015625,3060.4814453125,28765976892
2025-12-16,2964.180908203125,2978.92138671875,2890.01171875,2964.379638671875,22709189818 2025-12-16,2964.180908203125,2978.92138671875,2890.01171875,2964.379638671875,22709189818
2025-12-17,2933.59375,2969.88525390625,2920.556884765625,2962.631103515625,19631171584 2025-12-18,2829.30078125,2836.117431640625,2822.574951171875,2832.8203125,26052483072

1 Date Close High Low Open Volume
715 2025-12-14 3060.5947265625 3128.622802734375 3034.692626953125 3116.743896484375 15619543350
716 2025-12-15 2964.18310546875 3175.1181640625 2899.685791015625 3060.4814453125 28765976892
717 2025-12-16 2964.180908203125 2978.92138671875 2890.01171875 2964.379638671875 22709189818
718 2025-12-17 2025-12-18 2933.59375 2829.30078125 2969.88525390625 2836.117431640625 2920.556884765625 2822.574951171875 2962.631103515625 2832.8203125 19631171584 26052483072