September 28, 2026
Streamlit: Membuat Dashboard Data.
Membangun dashboard data dengan Streamlit: model eksekusi, widget st.*, layout, caching st.cache_data, session state, multipage app dengan st.navigation, dan deploy.
Streamlit mengubah skrip Python menjadi web app interaktif tanpa menulis HTML, CSS, atau JavaScript. Dashboard skripsi saya — Price Prediction System — dibangun dengan Streamlit: halaman fetch data, analitik harian, agregasi bulanan, forecast, dan perbandingan antar kota. Contoh di bawah mengikuti API Streamlit terbaru (seri 1.6x).
pip install streamlit plotly pandas
streamlit run app.py # buka http://localhost:8501
1. Model Eksekusi: Skrip Dijalankan Ulang
Hal terpenting yang harus dipahami: setiap interaksi (klik, ketik, geser slider) menjalankan ulang seluruh skrip dari atas ke bawah. Nilai widget dikembalikan sebagai variabel biasa.
import streamlit as st
import pandas as pd
import numpy as np
st.title("Harga Beras Harian")
hari = st.slider("Jumlah hari", 7, 90, 30) # rerun saat digeser
idx = pd.date_range(end=pd.Timestamp.today().normalize(), periods=hari, freq="D")
harga = 13000 + np.random.default_rng(1).normal(0, 60, hari).cumsum()
df = pd.DataFrame({"harga": harga.round()}, index=idx)
st.line_chart(df)
st.metric("Harga terakhir", f"Rp {df['harga'].iloc[-1]:,.0f}".replace(",", "."),
delta=f"{df['harga'].iloc[-1] - df['harga'].iloc[-2]:+.0f}")
Konsekuensinya: kerja mahal (query DB, fetch API, training model) harus di-cache, dan state yang perlu bertahan antar-rerun disimpan di st.session_state.
2. Widget yang Sering Dipakai
| Widget | Mengembalikan | Contoh pakai |
|---|---|---|
st.selectbox | satu nilai | pilih kota |
st.multiselect | list | bandingkan beberapa kota |
st.date_input | date atau tuple rentang | periode analisis |
st.slider | angka/rentang | proporsi data training 60–90% |
st.radio / st.segmented_control | satu nilai | metode Auto / SES / DES |
st.number_input | angka | nilai α |
st.button | True di rerun saat diklik | jalankan fetch |
st.toggle / st.checkbox | bool | tampilkan tabel mentah |
st.file_uploader | file-like / None | upload CSV |
Output yang paling sering: st.dataframe, st.metric, st.plotly_chart, st.line_chart, dan status st.success/st.warning/st.error.
metode = st.radio("Metode", ["Auto", "SES", "DES"], horizontal=True)
train = st.slider("Data training (%)", 60, 90, 80, step=5)
alpha = st.number_input("Alpha (α)", 0.05, 0.95, 0.5, step=0.05,
disabled=metode == "Auto")
if st.button("Jalankan forecast", type="primary"):
st.write(f"Menjalankan {metode} dengan {train}% data training, α={alpha}")
Untuk banyak input yang baru diproses bersamaan, bungkus dalam st.form agar tidak rerun di setiap ketikan:
with st.form("filter"):
kota = st.multiselect("Kota", ["Kab. Malang", "Kota Surabaya", "Kab. Jember"])
rentang = st.date_input("Rentang", value=(pd.Timestamp("2025-01-01"), pd.Timestamp("2025-06-30")))
submitted = st.form_submit_button("Terapkan")
3. Layout
st.set_page_config(page_title="Analitik Harian", page_icon="📊", layout="wide") # taruh di awal
with st.sidebar:
st.header("Filter")
kota = st.selectbox("Kota", ["Kab. Malang", "Kota Surabaya"])
c1, c2, c3, c4 = st.columns(4)
c1.metric("Rata-rata", "Rp 13.240")
c2.metric("Tertinggi", "Rp 13.900")
c3.metric("Terendah", "Rp 12.980")
c4.metric("Median", "Rp 13.210")
tab1, tab2 = st.tabs(["🚨 Lonjakan Harga", "📉 Perubahan Harian"])
with tab1:
st.write("Tabel lonjakan ≥ 5%")
with tab2:
st.write("Tabel perubahan harian")
with st.expander("Lihat data mentah"):
st.dataframe(df, width="stretch")
with st.container(border=True):
st.caption("Sumber: data harga pangan resmi Pemprov Jawa Timur")
use_container_width=Truesudah deprecated dist.dataframe,st.plotly_chart, dan kawan-kawan. Gantinyawidth="stretch"(default) atauwidth="content". Kode lama di skripsi saya masih memakai bentuk lama — jalan, tetapi memunculkan peringatan.
4. Caching
Dua decorator, dua kegunaan:
| Decorator | Untuk | Nilai yang dikembalikan |
|---|---|---|
@st.cache_data | Hasil komputasi/data: DataFrame, list, dict, hasil query | salinan baru tiap pemanggilan (aman dimodifikasi) |
@st.cache_resource | Objek global yang di-share: engine DB, model ML, client API | objek yang sama untuk semua user & sesi |
import os
from sqlalchemy import create_engine, text
@st.cache_resource
def get_engine():
return create_engine(os.environ["DATABASE_URL"], pool_pre_ping=True)
@st.cache_data(ttl="10m", show_spinner="Memuat data harga...")
def load_harian(kode_kota: str, komoditas_id: int, start, end) -> pd.DataFrame:
q = text("""SELECT tanggal, harga FROM history_data_komoditas
WHERE kode_kota = :k AND komoditas_id = :c
AND tanggal BETWEEN :s AND :e ORDER BY tanggal""")
with get_engine().connect() as conn:
df = pd.read_sql(q, conn, params={"k": kode_kota, "c": komoditas_id, "s": start, "e": end})
return df.assign(tanggal=pd.to_datetime(df["tanggal"])).set_index("tanggal")
@st.cache_data(ttl=120)
def daftar_kota() -> list[str]:
with get_engine().connect() as conn:
return pd.read_sql(text("SELECT DISTINCT kode_kota FROM history_data_komoditas"), conn)["kode_kota"].tolist()
Aturan cache key: argumen fungsi di-hash. Argumen yang tidak bisa di-hash (mis. koneksi) beri awalan underscore — def load(_conn, kota) — agar dilewati. Setelah fetch data baru, buang cache dengan load_harian.clear() atau st.cache_data.clear().
5. Session State
st.session_state adalah dict per tab browser yang bertahan antar-rerun.
if "hasil_forecast" not in st.session_state:
st.session_state.hasil_forecast = None
if st.button("Hitung"):
st.session_state.hasil_forecast = {"metode": "DES", "mape": 1.86}
if st.session_state.hasil_forecast:
st.json(st.session_state.hasil_forecast)
# widget dengan key otomatis tersimpan di session_state
st.selectbox("Kota", ["Kab. Malang", "Kota Surabaya"], key="kota_aktif")
st.write(st.session_state.kota_aktif)
6. Multipage App
Cara lama (masih didukung): folder pages/ di samping entrypoint. Nama file menentukan urutan dan label — begitulah skripsi saya disusun:
app/
├── Home.py
└── pages/
├── 0_Fetch_Data.py
├── 1_Analitik_Harian.py
├── 2_Agregasi_Bulanan.py
├── 3_Forecast.py
└── 4_Compare_Kota.py
Cara yang disarankan sekarang: st.Page + st.navigation, yang memberi kontrol penuh atas judul, ikon, grup, dan posisi menu.
# streamlit_app.py (entrypoint)
import streamlit as st
st.set_page_config(page_title="Dashboard Harga", layout="wide")
pages = {
"Data": [
st.Page("views/home.py", title="Beranda", icon="🏠", default=True),
st.Page("views/fetch.py", title="Fetch Data", icon="🔄"),
],
"Analisis": [
st.Page("views/harian.py", title="Analitik Harian", icon="📊"),
st.Page("views/bulanan.py", title="Agregasi Bulanan", icon="📅"),
st.Page("views/forecast.py", title="Forecast", icon="📈"),
],
}
pg = st.navigation(pages) # position="sidebar" | "top" | "hidden"
pg.run()
Begitu st.navigation dipanggil, folder pages/ diabaikan, jadi pilih salah satu. Pindah halaman dari kode: st.switch_page("views/forecast.py"); tautan: st.page_link(...).
7. Konfigurasi, Secrets & Deploy
# .streamlit/config.toml
[theme]
primaryColor = "#0f766e"
base = "light"
[server]
headless = true
# .streamlit/secrets.toml — JANGAN di-commit
[connections.sql]
dialect = "mysql"
driver = "pymysql"
host = "db.example.internal"
port = 3306
database = "harga_komoditas"
username = "app_readonly"
password = "isi-di-server"
Dengan secrets di atas, conn = st.connection("sql") lalu conn.query("SELECT ...", ttl=600) sudah sekaligus melakukan caching.
Opsi deploy:
| Opsi | Cocok untuk | Catatan |
|---|---|---|
| Streamlit Community Cloud | demo publik dari repo GitHub | secrets diisi lewat dashboard; DB harus bisa diakses dari internet |
| VPS + systemd/Docker | DB lokal/internal | streamlit run app.py --server.port 8501 --server.address 0.0.0.0 di belakang reverse proxy |
| Container | reproducible | lihat Docker Compose untuk Dev Environment |
Streamlit memakai WebSocket, jadi reverse proxy (Nginx, Cloudflare Tunnel) harus meneruskan header Upgrade. Untuk chart yang lebih kustom di luar Python, bandingkan dengan pendekatan di Visualisasi Data dengan ECharts di React
.
Kesalahan Umum
| Kesalahan | Akibat | Perbaikan |
|---|---|---|
| Query DB tanpa cache | Setiap klik = query ulang, dashboard lambat | @st.cache_data(ttl=...) |
Engine/model di st.cache_data | Objek di-pickle/disalin, bisa error | Pakai @st.cache_resource |
Memodifikasi objek dari cache_resource | Perubahan bocor ke semua user | Salin dulu atau pakai cache_data |
Mengandalkan st.button sebagai state | Nilai True hanya satu rerun, hasil hilang | Simpan hasil ke st.session_state |
Dua widget identik tanpa key | DuplicateElementId error | Beri key unik |
Layout wide diset di tiap halaman secara berbeda | Tampilan melompat saat pindah halaman | Set st.set_page_config di entrypoint; panggilan berikutnya hanya menimpa parameter yang disebut |
Mencampur pages/ dan st.navigation | Halaman di pages/ hilang | Pilih satu pendekatan |
Commit secrets.toml | Kredensial bocor | .gitignore; isi lewat secrets di server |

Hey! I’m Fanny, the software engineer tending to this digital garden. You can read more about me, or subscribe by email.