new
This commit is contained in:
parent
bb8fa42585
commit
4c6f66a4da
210
app.py
210
app.py
|
|
@ -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()
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
Loading…
Reference in New Issue