new
This commit is contained in:
parent
51a4b77e36
commit
38b54f89b9
292
app1.py
292
app1.py
|
|
@ -3,10 +3,10 @@ import streamlit as st
|
|||
import joblib
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import os # Tambahan untuk manajemen file
|
||||
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
|
||||
|
|
@ -18,19 +18,9 @@ rcParams['font.family'] = 'DejaVu Sans'
|
|||
# --- Konfigurasi Halaman ---
|
||||
st.set_page_config(
|
||||
page_title="Prediksi Harga Ethereum (GRU)",
|
||||
page_icon="🪙",
|
||||
layout="wide" # Opsional: agar tampilan lebih luas
|
||||
page_icon="🪙"
|
||||
)
|
||||
|
||||
# --- 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) ---
|
||||
st.markdown("""
|
||||
<style>
|
||||
|
|
@ -42,15 +32,6 @@ html, body, [class*="css"] { font-family: "Inter", "DejaVu Sans", sans-serif; }
|
|||
--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 */
|
||||
.app-title {
|
||||
text-align:center;
|
||||
|
|
@ -100,48 +81,73 @@ html, body, [class*="css"] { font-family: "Inter", "DejaVu Sans", sans-serif; }
|
|||
""", unsafe_allow_html=True)
|
||||
|
||||
|
||||
# --- Fungsi-fungsi Bantuan (JANGAN UBAH LOGIKA UTAMA) ---
|
||||
# --- Fungsi-fungsi Bantuan ---
|
||||
|
||||
@st.cache_data(ttl="1h")
|
||||
def load_eth_data():
|
||||
"""
|
||||
ticker = "ETH-USD"
|
||||
df = None
|
||||
|
||||
"""
|
||||
# Cek apakah file admin ada
|
||||
if os.path.exists(DATA_PATH):
|
||||
# 1. COBA ONLINE
|
||||
try:
|
||||
df = pd.read_csv(DATA_PATH)
|
||||
|
||||
# --- PEMBERSIHAN DATA AGAR SESUAI FORMAT MODEL ---
|
||||
# 1. Hapus kolom index lama jika ada
|
||||
if "Unnamed: 0" in df.columns:
|
||||
df = df.drop(columns=["Unnamed: 0"])
|
||||
df = yf.download(
|
||||
ticker,
|
||||
start="2024-01-01",
|
||||
end=date.today() + timedelta(days=1),
|
||||
progress=False,
|
||||
auto_adjust=True,
|
||||
multi_level_index=False
|
||||
)
|
||||
|
||||
# 2. Standarisasi nama kolom Date
|
||||
if "Date" not in df.columns:
|
||||
# Coba cari kolom yang mirip 'date'
|
||||
found = False
|
||||
if df is not None and not df.empty:
|
||||
# Bersihkan Index & Kolom
|
||||
df = df.reset_index()
|
||||
|
||||
new_cols = []
|
||||
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
|
||||
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'})
|
||||
|
||||
# 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)
|
||||
# Hapus Timezone
|
||||
df['Date'] = pd.to_datetime(df['Date']).dt.tz_localize(None)
|
||||
|
||||
return df, "admin_file"
|
||||
# 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:
|
||||
return None, f"error_read: {str(e)}"
|
||||
else:
|
||||
return None, "no_file"
|
||||
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."""
|
||||
|
|
@ -185,23 +191,44 @@ def validate_model_input(model):
|
|||
|
||||
@st.cache_resource
|
||||
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")
|
||||
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 = []
|
||||
|
||||
|
|
@ -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):
|
||||
"""Membuat grafik gabungan."""
|
||||
context_start_date = start_date - timedelta(days=60)
|
||||
"""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
|
||||
|
|
@ -226,24 +253,32 @@ def create_combined_chart(df, start_date, future_dates, future_predictions):
|
|||
|
||||
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(
|
||||
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, predictions_flat,
|
||||
color="#d62728", linewidth=2.5, label="Harga Prediksi (GRU)",
|
||||
alpha=0.9, marker="o", markersize=4
|
||||
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]
|
||||
|
|
@ -280,35 +315,30 @@ def create_combined_chart(df, start_date, future_dates, future_predictions):
|
|||
return fig
|
||||
|
||||
|
||||
# --- 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)
|
||||
# --- Tampilan Antarmuka Aplikasi ---
|
||||
|
||||
df, data_source = load_eth_data()
|
||||
model, scaler = load_gru_assets()
|
||||
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)
|
||||
|
||||
# LOGIC CHECK DATA
|
||||
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
|
||||
df, data_source = load_eth_data()
|
||||
model, scaler = load_gru_assets()
|
||||
|
||||
if df is not None and model is not None and scaler is not None:
|
||||
# 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)
|
||||
|
||||
# --- 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)
|
||||
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)
|
||||
# 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)
|
||||
|
||||
|
|
@ -358,7 +388,8 @@ def show_dashboard():
|
|||
if start_index - time_step < 0:
|
||||
st.error(
|
||||
f"Tanggal terlalu awal untuk diprediksi. "
|
||||
f"Butuh {time_step} hari data sebelumnya."
|
||||
f"Butuh {time_step} hari data sebelumnya. Pilih tanggal setelah "
|
||||
f"{(df['Date'].min() + timedelta(days=time_step)).strftime('%Y-%m-%d')}."
|
||||
)
|
||||
st.stop()
|
||||
|
||||
|
|
@ -384,6 +415,39 @@ def show_dashboard():
|
|||
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],
|
||||
|
|
@ -398,66 +462,18 @@ def show_dashboard():
|
|||
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}
|
||||
""")
|
||||
📊 **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:
|
||||
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:
|
||||
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 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.")
|
||||
|
||||
|
||||
# --- MAIN NAVIGATION (SIDEBAR) ---
|
||||
# Ini adalah logika utama yang memisahkan tampilan Admin dan User Biasa
|
||||
|
||||
st.sidebar.title("Navigasi")
|
||||
menu = st.sidebar.radio("Pilih Halaman:", ["Dashboard (Public)", "Admin Panel"])
|
||||
|
||||
if menu == "Dashboard (Public)":
|
||||
show_dashboard()
|
||||
elif menu == "Admin Panel":
|
||||
show_admin_page()
|
||||
else:
|
||||
st.error("Gagal memuat data. Coba cek koneksi internet atau sumber data yfinance.")
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
|
|
@ -882,4 +882,27 @@ Date,Close,High,Low,Open,Volume
|
|||
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-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
|
||||
|
|
|
|||
|
Loading…
Reference in New Issue