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

210
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,73 +100,48 @@ 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
if os.path.exists(DATA_PATH):
try: try:
df = pd.read_csv(DATA_PATH)
df = yf.download( # --- PEMBERSIHAN DATA AGAR SESUAI FORMAT MODEL ---
ticker, # 1. Hapus kolom index lama jika ada
start="2024-01-01", if "Unnamed: 0" in df.columns:
end=date.today() + timedelta(days=1), df = df.drop(columns=["Unnamed: 0"])
progress=False,
auto_adjust=True,
multi_level_index=False
)
if df is not None and not df.empty: # 2. Standarisasi nama kolom Date
# Bersihkan Index & Kolom if "Date" not in df.columns:
df = df.reset_index() # Coba cari kolom yang mirip 'date'
found = False
new_cols = []
for col in df.columns: for col in df.columns:
col_name = col[0] if isinstance(col, tuple) else str(col) if col.lower() == "date":
new_cols.append(col_name) df = df.rename(columns={col: "Date"})
df.columns = new_cols found = True
break
# Pastikan kolom pertama adalah Date if not found:
if 'Date' not in df.columns: # Jika tidak ada header Date, asumsikan kolom pertama adalah Date
df = df.rename(columns={df.columns[0]: 'Date'}) df = df.rename(columns={df.columns[0]: 'Date'})
# Hapus Timezone # 3. Konversi ke datetime
df['Date'] = pd.to_datetime(df['Date']).dt.tz_localize(None) 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)
# Simpan Backup (Timpa file lama agar fresh) return df, "admin_file"
try:
df.to_csv("eth_backup.csv", index=False)
except:
pass
return df, "online"
except Exception as e: except Exception as e:
print(f"Gagal Online: {e}") return None, f"error_read: {str(e)}"
else:
# 2. COBA BACKUP (JIKA ONLINE GAGAL) return None, "no_file"
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."""
@ -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,30 +280,35 @@ 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-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-subtitle'>Data historis & prediksi ETH-USD berbasis GRU</div>", unsafe_allow_html=True)
df, data_source = load_eth_data() df, data_source = load_eth_data()
model, scaler = load_gru_assets() model, scaler = load_gru_assets()
# LOGIC UI (TOAST/WARNING) DITARUH DI SINI (DILUAR CACHE) # LOGIC CHECK DATA
if data_source == "backup": if data_source == "no_file":
st.toast("Koneksi Yahoo lambat. Menggunakan data backup lokal.", icon="⚠️") st.warning("⚠️ Data historis belum tersedia. Silakan hubungi Admin untuk mengupload 'databackup_eth'.")
elif data_source == "error": return # Stop eksekusi jika data tidak ada
st.error("❌ Gagal memuat data (Online gagal & Backup 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: if df is not None and model is not None and scaler is not None:
# Menampilkan tabel data historis # Menampilkan tabel data historis
st.markdown("<div class='card'>", unsafe_allow_html=True) 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.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) # --- 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 # 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)
@ -381,8 +358,7 @@ if df is not None and model is not None and scaler is not None:
if start_index - time_step < 0: if start_index - time_step < 0:
st.error( st.error(
f"Tanggal terlalu awal untuk diprediksi. " f"Tanggal terlalu awal untuk diprediksi. "
f"Butuh {time_step} hari data sebelumnya. Pilih tanggal setelah " f"Butuh {time_step} hari data sebelumnya."
f"{(df['Date'].min() + timedelta(days=time_step)).strftime('%Y-%m-%d')}."
) )
st.stop() st.stop()
@ -435,5 +411,53 @@ elif model is None:
st.error("Gagal memuat model. Pastikan file 'gru_model.h5' ada di folder yang sama dengan aplikasi.") 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.") 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.") # --- 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()

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