This commit is contained in:
Azmikun1 2026-06-28 21:20:28 +07:00
parent 51a4b77e36
commit 38b54f89b9
3 changed files with 737 additions and 219 deletions

452
app1.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,19 +18,9 @@ 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>
@ -42,15 +32,6 @@ 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;
@ -100,49 +81,74 @@ html, body, [class*="css"] { font-family: "Inter", "DejaVu Sans", sans-serif; }
""", unsafe_allow_html=True) """, unsafe_allow_html=True)
# --- Fungsi-fungsi Bantuan (JANGAN UBAH LOGIKA UTAMA) --- # --- Fungsi-fungsi Bantuan ---
@st.cache_data(ttl="1h") @st.cache_data(ttl="1h")
def load_eth_data(): def load_eth_data():
""" ticker = "ETH-USD"
df = None
"""
# Cek apakah file admin ada # 1. COBA ONLINE
if os.path.exists(DATA_PATH): try:
try:
df = pd.read_csv(DATA_PATH) df = yf.download(
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()
# --- PEMBERSIHAN DATA AGAR SESUAI FORMAT MODEL --- new_cols = []
# 1. Hapus kolom index lama jika ada for col in df.columns:
if "Unnamed: 0" in df.columns: col_name = col[0] if isinstance(col, tuple) else str(col)
df = df.drop(columns=["Unnamed: 0"]) new_cols.append(col_name)
df.columns = new_cols
# 2. Standarisasi nama kolom Date # Kolom pertama adalah Date
if "Date" not in df.columns: if 'Date' not in df.columns:
# Coba cari kolom yang mirip 'date' df = df.rename(columns={df.columns[0]: 'Date'})
found = False
for col in df.columns:
if col.lower() == "date":
df = df.rename(columns={col: "Date"})
found = True
break
if not found:
# Jika tidak ada header Date, asumsikan kolom pertama adalah Date
df = df.rename(columns={df.columns[0]: 'Date'})
# 3. Konversi ke datetime
df["Date"] = pd.to_datetime(df["Date"])
# Hapus timezone jika ada agar kompatibel dengan matplotlib/numpy
if df["Date"].dt.tz is not None:
df["Date"] = df["Date"].dt.tz_localize(None)
return df, "admin_file"
except Exception as e:
return None, f"error_read: {str(e)}"
else:
return None, "no_file"
# Hapus Timezone
df['Date'] = pd.to_datetime(df['Date']).dt.tz_localize(None)
# Simpan Backup (Timpa file lama agar fresh)
try:
df.to_csv("eth_backup.csv", index=False)
except:
pass
return df, "online"
except Exception as e:
print(f"Gagal Online: {e}")
# 2. COBA BACKUP (JIKA ONLINE GAGAL)
try:
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
if "Date" in df_backup.columns:
df_backup["Date"] = pd.to_datetime(df_backup["Date"])
elif df_backup.columns[0].lower() == "date":
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 = []
@ -185,23 +191,44 @@ def validate_model_input(model):
@st.cache_resource @st.cache_resource
def load_gru_assets(): def load_gru_assets():
"""Load model & scaler hasil training.""" """Load model & scaler hasil training (wajib pakai scaler yang sama)."""
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 = []
@ -215,8 +242,8 @@ 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.""" """Membuat grafik gabungan dengan gaya yang sama seperti referensi."""
context_start_date = start_date - timedelta(days=60) context_start_date = start_date - timedelta(days=609)
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()
window_size = 7 window_size = 7
@ -226,24 +253,32 @@ def create_combined_chart(df, start_date, future_dates, future_predictions):
fig, ax = plt.subplots(figsize=(14, 8)) fig, ax = plt.subplots(figsize=(14, 8))
ax.plot(
recent_df["Date"], recent_df["Close"],
color="gray", linewidth=1.0, label="Harga Aktual (Harian)", alpha=0.5
)
ax.plot( ax.plot(
recent_df["Date"], recent_df["Trend_Aktual"], recent_df["Date"], recent_df["Trend_Aktual"],
color="#1f77b4", linewidth=2.5, label="Tren Harga Aktual", alpha=0.9 color="#1f77b4", linewidth=2.5, label="Tren Harga Aktual", alpha=0.9
) )
# Membuat tren prediksi menggunakan moving average 7 hari
pred_trend = (
pd.Series(predictions_flat)
.rolling(window=7, min_periods=1)
.mean()
)
ax.plot( ax.plot(
future_dates, predictions_flat, future_dates,
color="#d62728", linewidth=2.5, label="Harga Prediksi (GRU)", pred_trend,
alpha=0.9, marker="o", markersize=4 color="#d62728",
linewidth=2.5,
label="Tren Harga Prediksi (GRU)",
alpha=0.9
) )
ax.scatter( ax.scatter(
recent_df["Date"], recent_df["Close"], recent_df["Date"], recent_df["Close"],
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]
@ -280,184 +315,165 @@ def create_combined_chart(df, start_date, future_dates, future_predictions):
return fig return fig
# --- HALAMAN: DASHBOARD PUBLIK (Landing Page) --- # --- Tampilan Antarmuka Aplikasi ---
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)
df, data_source = load_eth_data() st.markdown("<div class='app-title'>Prediksi Harga Ethereum (GRU)</div>", unsafe_allow_html=True)
model, scaler = load_gru_assets() st.markdown("<div class='app-subtitle'>Data historis & prediksi ETH-USD berbasis GRU</div>", unsafe_allow_html=True)
# LOGIC CHECK DATA df, data_source = load_eth_data()
if data_source == "no_file": model, scaler = load_gru_assets()
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
if df is not None and model is not None and scaler is not None: # LOGIC UI (TOAST/WARNING) DITARUH DI SINI (DILUAR CACHE)
# Menampilkan tabel data historis if data_source == "backup":
st.markdown("<div class='card'>", unsafe_allow_html=True) st.toast("Koneksi Yahoo lambat. Menggunakan data backup lokal.", icon="⚠️")
st.markdown("<div class='h-section'>📚 Data Historis Harga Ethereum </div>", unsafe_allow_html=True) elif data_source == "error":
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 if df is not None and model is not None and scaler is not None:
st.dataframe(df, height=400, use_container_width=True) # Menampilkan tabel data historis
st.markdown("<div class='card'>", unsafe_allow_html=True)
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.markdown("</div>", unsafe_allow_html=True)
# Menampilkan visualisasi line chart # Menampilkan visualisasi line chart
st.markdown("<div class='card'>", unsafe_allow_html=True) st.markdown("<div class='card'>", unsafe_allow_html=True)
st.markdown("<div class='h-section'>📈 Visualisasi Harga Historis</div>", 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.line_chart(df.rename(columns={"Date": "index"}).set_index("index")["Close"]) st.markdown("</div>", unsafe_allow_html=True)
st.markdown("</div>", unsafe_allow_html=True)
st.markdown("<div class='card'>", 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) st.markdown("<div class='h-section'>🎯 Mulai Prediksi Berdasarkan Tanggal</div>", unsafe_allow_html=True)
time_step = int(model.input_shape[1]) time_step = int(model.input_shape[1])
min_selectable_date = (df["Date"].min() + timedelta(days=time_step)).date() min_selectable_date = (df["Date"].min() + timedelta(days=time_step)).date()
max_selectable_date = df["Date"].max().date() max_selectable_date = df["Date"].max().date()
col1, col2 = st.columns(2) col1, col2 = st.columns(2)
with col1: with col1:
selected_date = st.date_input( selected_date = st.date_input(
"Pilih tanggal mulai prediksi:", "Pilih tanggal mulai prediksi:",
value=max_selectable_date, value=max_selectable_date,
min_value=min_selectable_date, min_value=min_selectable_date,
max_value=max_selectable_date, max_value=max_selectable_date,
help=f"Pilih tanggal antara {min_selectable_date} dan {max_selectable_date}" help=f"Pilih tanggal antara {min_selectable_date} dan {max_selectable_date}"
) )
with col2: with col2:
prediction_days = st.slider( prediction_days = st.slider(
"Pilih jumlah hari untuk prediksi (1-30 hari):", "Pilih jumlah hari untuk prediksi (1-30 hari):",
min_value=1, max_value=30, value=15, step=1 min_value=1, max_value=30, value=15, step=1
) )
if st.button("Buat Prediksi", key="predict_button"): if st.button("Buat Prediksi", key="predict_button"):
if prediction_days < 1: if prediction_days < 1:
st.warning("Jumlah hari prediksi minimal **1**. Silakan geser slidernya dulu. 🙂") 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() st.stop()
with st.spinner("Memproses dan menjalankan prediksi..."): start_index = int(idxs[0])
# scaling (mengikuti scaler training)
close_values = df[["Close"]].values.astype(float)
scaled_data = scaler.transform(close_values)
# Cocokkan berdasarkan tanggal (robust) if start_index - time_step < 0:
selected_date_dt = pd.to_datetime(selected_date) st.error(
mask = df["Date"].dt.date == selected_date_dt.date() f"Tanggal terlalu awal untuk diprediksi. "
idxs = np.where(mask.to_numpy())[0] f"Butuh {time_step} hari data sebelumnya. Pilih tanggal setelah "
f"{(df['Date'].min() + timedelta(days=time_step)).strftime('%Y-%m-%d')}."
)
st.stop()
if len(idxs) == 0: initial_sequence = scaled_data[start_index - time_step: start_index] # (time_step,1)
st.error(f"Tanggal {selected_date_dt.strftime('%Y-%m-%d')} tidak ditemukan dalam dataset.")
st.stop()
start_index = int(idxs[0]) # ✅ Prediksi MURNI dari model
future_predictions_scaled = predict_from_sequence_pure(model, initial_sequence, prediction_days)
if start_index - time_step < 0: # ✅ Inverse transform MURNI dari scaler
st.error( future_predictions = scaler.inverse_transform(future_predictions_scaled)
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) # sanity-check
if not np.isfinite(future_predictions).all():
st.error("❌ Hasil prediksi mengandung NaN/Inf. Periksa kompatibilitas model/scaler.")
st.stop()
# ✅ Prediksi MURNI dari model future_dates = [selected_date_dt + timedelta(days=i) for i in range(1, prediction_days + 1)]
future_predictions_scaled = predict_from_sequence_pure(model, initial_sequence, prediction_days)
# ✅ Inverse transform MURNI dari scaler st.markdown("<div class='card'>", unsafe_allow_html=True)
future_predictions = scaler.inverse_transform(future_predictions_scaled) 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)
# sanity-check st.markdown("<div class='card'>", unsafe_allow_html=True)
if not np.isfinite(future_predictions).all():
st.error("❌ Hasil prediksi mengandung NaN/Inf. Periksa kompatibilitas model/scaler.") #Test
st.stop() st.subheader("📈 Grafik Detail Prediksi")
future_dates = [selected_date_dt + timedelta(days=i) for i in range(1, prediction_days + 1)] fig2, ax2 = plt.subplots(figsize=(12, 5))
st.markdown("<div class='card'>", unsafe_allow_html=True) ax2.plot(
st.subheader("📊 Grafik Prediksi (Historis + Prediksi)") future_dates,
fig = create_combined_chart(df, selected_date_dt, future_dates, future_predictions) future_predictions.flatten(),
st.pyplot(fig) marker='o',
st.markdown("</div>", unsafe_allow_html=True) linewidth=2.5
)
st.markdown("<div class='card'>", unsafe_allow_html=True) ax2.set_title(
st.subheader("🧾 Tabel Detail Prediksi") "Grafik Detail Hasil Prediksi Harga Ethereum",
prediction_table_df = pd.DataFrame({ fontsize=14,
"Tanggal": [d.strftime("%Y-%m-%d") for d in future_dates], fontweight="bold"
"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"]) ax2.set_xlabel("Tanggal")
max_prediction = float(np.max(future_predictions)) ax2.set_ylabel("Harga Prediksi (USD)")
min_prediction = float(np.min(future_predictions))
st.info(f""" ax2.grid(True, alpha=0.3)
📊 **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) plt.xticks(rotation=45)
st.markdown("<div class='footer'>Dibuat oleh Muhammad Gilman Nadhif Azmi</div>", unsafe_allow_html=True) plt.tight_layout()
elif model is None: st.pyplot(fig2)
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 --- st.markdown("</div>", unsafe_allow_html=True)
def show_admin_page():
st.title("🔐 Admin Panel")
st.write("Area khusus admin untuk memperbarui dataset harga Ethereum.")
# Simple Password check st.markdown("<div class='card'>", unsafe_allow_html=True)
password = st.text_input("Masukkan Password Admin:", type="password")
if password == "admin123": # Ganti password sesuai keinginan
st.success("Akses Diterima.")
st.divider()
st.subheader("📂 Upload Dataset Baru")
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.")
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)
# --- MAIN NAVIGATION (SIDEBAR) --- last_price = float(df.loc[start_index, "Close"])
# Ini adalah logika utama yang memisahkan tampilan Admin dan User Biasa max_prediction = float(np.max(future_predictions))
min_prediction = float(np.min(future_predictions))
st.sidebar.title("Navigasi") st.info(f"""
menu = st.sidebar.radio("Pilih Halaman:", ["Dashboard (Public)", "Admin Panel"]) 📊 **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 menu == "Dashboard (Public)": st.markdown("</div>", unsafe_allow_html=True)
show_dashboard() st.markdown("<div class='footer'>Dibuat oleh Muhammad Gilman Nadhif Azmi</div>", unsafe_allow_html=True)
elif menu == "Admin Panel":
show_admin_page() 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.")

479
apptest.py Normal file
View File

@ -0,0 +1,479 @@
import requests
import streamlit as st
import joblib
import pandas as pd
import numpy as np
from sklearn.preprocessing import MinMaxScaler
from tensorflow.keras.models import load_model
from datetime import date, timedelta
import yfinance as yf
from pathlib import Path
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from matplotlib import rcParams
# Set font untuk mendukung karakter Indonesia
rcParams['font.family'] = 'DejaVu Sans'
# --- Konfigurasi Halaman ---
st.set_page_config(
page_title="Prediksi Harga Ethereum (GRU)",
page_icon="🪙"
)
# --- GLOBAL STYLES (UI only) ---
st.markdown("""
<style>
/* font & warna dasar */
html, body, [class*="css"] { font-family: "Inter", "DejaVu Sans", sans-serif; }
:root {
--card-bg: #ffffff;
--muted: #6b7280;
--ring: #e5e7eb;
}
/* gradient title */
.app-title {
text-align:center;
font-weight:800;
font-size: 32px;
background: linear-gradient(90deg, #6EE7F9 0%, #7C3AED 50%, #F59E0B 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin: 0.25rem 0 0.5rem 0;
}
/* subtitle */
.app-subtitle {
text-align:center;
color: var(--muted);
margin-bottom: 1.25rem;
}
/* card container */
.card {
background: var(--card-bg);
border: 1px solid var(--ring);
border-radius: 16px;
padding: 1rem 1.25rem;
box-shadow: 0 6px 20px rgba(0,0,0,0.05);
margin-bottom: 1rem;
}
/* section heading */
.h-section {
font-weight:700;
font-size: 20px;
margin: 0 0 .5rem 0;
}
/* table tweaks */
.dataframe tbody tr:hover { background-color: #fafafa; }
/* footer */
.footer {
text-align:center;
color: var(--muted);
font-size: 13px;
margin-top: 2rem;
}
</style>
""", unsafe_allow_html=True)
# --- Fungsi-fungsi Bantuan ---
@st.cache_data(ttl="1h")
def load_eth_data():
ticker = "ETH-USD"
df = None
# 1. COBA ONLINE
try:
df = yf.download(
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 = []
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
# Kolom pertama adalah Date
if 'Date' not in df.columns:
df = df.rename(columns={df.columns[0]: 'Date'})
# Hapus Timezone
df['Date'] = pd.to_datetime(df['Date']).dt.tz_localize(None)
# Simpan Backup (Timpa file lama agar fresh)
try:
df.to_csv("eth_backup.csv", index=False)
except:
pass
return df, "online"
except Exception as e:
print(f"Gagal Online: {e}")
# 2. COBA BACKUP (JIKA ONLINE GAGAL)
try:
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
if "Date" in df_backup.columns:
df_backup["Date"] = pd.to_datetime(df_backup["Date"])
elif df_backup.columns[0].lower() == "date":
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):
"""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
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
@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
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
def predict_from_sequence_pure(model, initial_sequence_scaled, n_days):
seq = np.array(initial_sequence_scaled, dtype="float32").reshape(-1) # (time_step,)
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)
preds.append(y)
seq = np.append(seq[1:], y) # autoregressive (murni)
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=609)
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()
fig, ax = plt.subplots(figsize=(14, 8))
ax.plot(
recent_df["Date"], recent_df["Trend_Aktual"],
color="#1f77b4", linewidth=2.5, label="Tren Harga Aktual", alpha=0.9
)
# Membuat tren prediksi menggunakan moving average 7 hari
pred_trend = (
pd.Series(predictions_flat)
.rolling(window=7, min_periods=1)
.mean()
)
ax.plot(
future_dates,
pred_trend,
color="#d62728",
linewidth=2.5,
label="Tren Harga Prediksi (GRU)",
alpha=0.9
)
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.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")
return fig
# --- Tampilan Antarmuka Aplikasi ---
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)
df, data_source = load_eth_data()
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
st.markdown("<div class='card'>", unsafe_allow_html=True)
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.markdown("</div>", unsafe_allow_html=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)
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. 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)
#Test
st.subheader("📈 Grafik Detail Prediksi")
fig2, ax2 = plt.subplots(figsize=(12, 5))
ax2.plot(
future_dates,
future_predictions.flatten(),
marker='o',
linewidth=2.5
)
ax2.set_title(
"Grafik Detail Hasil Prediksi Harga Ethereum",
fontsize=14,
fontweight="bold"
)
ax2.set_xlabel("Tanggal")
ax2.set_ylabel("Harga Prediksi (USD)")
ax2.grid(True, alpha=0.3)
plt.xticks(rotation=45)
plt.tight_layout()
st.pyplot(fig2)
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

@ -882,4 +882,27 @@ Date,Close,High,Low,Open,Volume
2026-05-30,2019.458251953125,2028.4417724609375,2000.165771484375,2011.909423828125,7478416074 2026-05-30,2019.458251953125,2028.4417724609375,2000.165771484375,2011.909423828125,7478416074
2026-05-31,2004.341552734375,2034.1192626953125,1991.90673828125,2019.242431640625,9253505209 2026-05-31,2004.341552734375,2034.1192626953125,1991.90673828125,2019.242431640625,9253505209
2026-06-01,2003.22119140625,2017.6077880859375,1956.1522216796875,2004.2425537109375,19458810001 2026-06-01,2003.22119140625,2017.6077880859375,1956.1522216796875,2004.2425537109375,19458810001
2026-06-03,1848.1400146484375,1870.7220458984375,1848.860107421875,1857.32568359375,25659957248 2026-06-02,1857.71630859375,2003.970703125,1838.05859375,2003.26025390625,25205401403
2026-06-03,1811.7322998046875,1889.058837890625,1771.3145751953125,1857.6417236328125,24083350214
2026-06-04,1769.58056640625,1817.3465576171875,1717.802490234375,1811.7425537109375,27112623897
2026-06-05,1580.86083984375,1772.1077880859375,1540.1619873046875,1769.986083984375,39924177669
2026-06-06,1568.7677001953125,1599.9534912109375,1506.5057373046875,1580.88037109375,18426248218
2026-06-07,1686.396240234375,1714.970703125,1563.77587890625,1568.74951171875,15172606860
2026-06-08,1690.1485595703125,1713.4710693359375,1644.79150390625,1686.251220703125,17427269148
2026-06-09,1637.7066650390625,1695.52294921875,1613.834228515625,1690.21337890625,15196855528
2026-06-10,1620.1376953125,1665.7607421875,1603.8258056640625,1637.6348876953125,12655731412
2026-06-11,1672.2806396484375,1690.4488525390625,1620.08447265625,1620.1400146484375,12379882531
2026-06-12,1665.1278076171875,1689.4443359375,1650.628662109375,1671.8189697265625,9989137815
2026-06-13,1680.214599609375,1693.603271484375,1661.609130859375,1665.0487060546875,6101094488
2026-06-14,1724.61328125,1729.350830078125,1654.2105712890625,1680.146484375,7804982667
2026-06-15,1794.961181640625,1847.769775390625,1709.253662109375,1724.5716552734375,17921331594
2026-06-16,1790.398193359375,1837.2041015625,1758.130126953125,1794.93798828125,14698492893
2026-06-17,1747.885498046875,1807.2828369140625,1724.7178955078125,1790.396240234375,14185457929
2026-06-18,1709.533447265625,1760.880859375,1670.1038818359375,1747.8778076171875,12942260783
2026-06-19,1710.982421875,1717.1932373046875,1678.1962890625,1709.4986572265625,7855725190
2026-06-20,1739.3013916015625,1747.259521484375,1702.8017578125,1710.982177734375,7773536600
2026-06-21,1704.58056640625,1739.48583984375,1701.9364013671875,1739.341064453125,8498711726
2026-06-22,1726.5108642578125,1776.8321533203125,1704.5166015625,1704.6304931640625,13584368622
2026-06-23,1665.436767578125,1733.6123046875,1638.253173828125,1726.519775390625,10431438278
2026-06-24,1619.9249267578125,1687.4923095703125,1551.4842529296875,1665.309814453125,14059894232
2026-06-26,1556.2900390625,1568.429443359375,1556.8143310546875,1564.8594970703125,15403906048

1 Date Close High Low Open Volume
882 2026-05-30 2019.458251953125 2028.4417724609375 2000.165771484375 2011.909423828125 7478416074
883 2026-05-31 2004.341552734375 2034.1192626953125 1991.90673828125 2019.242431640625 9253505209
884 2026-06-01 2003.22119140625 2017.6077880859375 1956.1522216796875 2004.2425537109375 19458810001
885 2026-06-03 2026-06-02 1848.1400146484375 1857.71630859375 1870.7220458984375 2003.970703125 1848.860107421875 1838.05859375 1857.32568359375 2003.26025390625 25659957248 25205401403
886 2026-06-03 1811.7322998046875 1889.058837890625 1771.3145751953125 1857.6417236328125 24083350214
887 2026-06-04 1769.58056640625 1817.3465576171875 1717.802490234375 1811.7425537109375 27112623897
888 2026-06-05 1580.86083984375 1772.1077880859375 1540.1619873046875 1769.986083984375 39924177669
889 2026-06-06 1568.7677001953125 1599.9534912109375 1506.5057373046875 1580.88037109375 18426248218
890 2026-06-07 1686.396240234375 1714.970703125 1563.77587890625 1568.74951171875 15172606860
891 2026-06-08 1690.1485595703125 1713.4710693359375 1644.79150390625 1686.251220703125 17427269148
892 2026-06-09 1637.7066650390625 1695.52294921875 1613.834228515625 1690.21337890625 15196855528
893 2026-06-10 1620.1376953125 1665.7607421875 1603.8258056640625 1637.6348876953125 12655731412
894 2026-06-11 1672.2806396484375 1690.4488525390625 1620.08447265625 1620.1400146484375 12379882531
895 2026-06-12 1665.1278076171875 1689.4443359375 1650.628662109375 1671.8189697265625 9989137815
896 2026-06-13 1680.214599609375 1693.603271484375 1661.609130859375 1665.0487060546875 6101094488
897 2026-06-14 1724.61328125 1729.350830078125 1654.2105712890625 1680.146484375 7804982667
898 2026-06-15 1794.961181640625 1847.769775390625 1709.253662109375 1724.5716552734375 17921331594
899 2026-06-16 1790.398193359375 1837.2041015625 1758.130126953125 1794.93798828125 14698492893
900 2026-06-17 1747.885498046875 1807.2828369140625 1724.7178955078125 1790.396240234375 14185457929
901 2026-06-18 1709.533447265625 1760.880859375 1670.1038818359375 1747.8778076171875 12942260783
902 2026-06-19 1710.982421875 1717.1932373046875 1678.1962890625 1709.4986572265625 7855725190
903 2026-06-20 1739.3013916015625 1747.259521484375 1702.8017578125 1710.982177734375 7773536600
904 2026-06-21 1704.58056640625 1739.48583984375 1701.9364013671875 1739.341064453125 8498711726
905 2026-06-22 1726.5108642578125 1776.8321533203125 1704.5166015625 1704.6304931640625 13584368622
906 2026-06-23 1665.436767578125 1733.6123046875 1638.253173828125 1726.519775390625 10431438278
907 2026-06-24 1619.9249267578125 1687.4923095703125 1551.4842529296875 1665.309814453125 14059894232
908 2026-06-26 1556.2900390625 1568.429443359375 1556.8143310546875 1564.8594970703125 15403906048