35.3 Streamlit mint ellenőrizhető bemutató

A labor Northstar-modellje elkészült: a pipeline.joblib, a model_meta.json és a control_predictions.npz a Modellartefaktum és betöltési határ alfejezetben leírt kapun átment, a FastAPI-végpont a FastAPI végpont és ellenőrzött bemenet alfejezetben ugyanezt a szerződést szolgálja ki. Most egy kutatói demót építünk, amely ugyanazt a betanított artefaktumot használja, de emberi bevitellel: a felhasználó egy rendelés jellemzőit adja meg, a felület pedig megmutatja a becsült késési valószínűséget, a döntési küszöböt, a modellverziót és az érvényességi korlátokat. Az előzetes becslés: ha a teszt-AUC körülbelül 0,70–0,75, a demóban egy átlagos kosárra a késési valószínűség jellemzően 0,2–0,6 közé esik; egy nagy távolságú, nem prioritásos rendelés ennél magasabbat mutathat. A pontos érték azonban a betanított modelltől függ, ezért a bemutató előtt a kontrollpredikciók egyezését ellenőrizni kell.

35.3.1 A Streamlit mint kutatói demó, nem dashboard

A Streamlit egy Python-szkriptet futtat: minden widget-interakció a teljes szkriptet újrarendezi felülről lefelé. Ez három gyakorlati következménnyel jár. Egyrészt a drága objektumokat, például a betanított pipeline-t, nem szabad minden újrafutásban betölteni; erre való a st.cache_resource. Másrészt a felhasználó bemeneteit és az előzményeket a st.session_state tárolja, mert a lokális változók az újrafutáskor elvesznek. Harmadrészt a felület logikája azt a szerződést jeleníti meg, amelyet a Pydantic-séma már kikényszerített az API-ban; a UI tehát nem lehet tágabb, mint a modell érvényes bemeneti tartománya.

A félreértések elkerüléséhez érdemes elkülöníteni a cache-et a state-től. A cache a drága erőforrások, például a modellbetöltés eredményét őrzi meg a szerverfolyamat élettartamára; a betöltő függvény argumentumai alkotják a cache kulcsát. A state ezzel szemben az adott böngészős munkamenet állapota: a beküldött űrlap értékei, a predikciós előzmények, egy kiválasztott összehasonlító eset. A kettőt nem cseréljük fel: a modellt nem rakjuk session_state-be arra az esetre, ha több felhasználó osztozik rajta, a felhasználói előzményt pedig nem tekintjük processz-szintű erőforrásnak.

35.3.2 A valószínűség mint modell output, nem mérési eredmény

A késési célváltozó bináris: y=1y=1, ha a kézbesítés meghaladja a 7 napot. A logisztikus modell egy lineáris pontszámot számít a feature-vektorból:

z(x)=β0+∑j=1dβjxj, z(x)=\beta_0+\sum_{j=1}^{d}\beta_j x_j,

majd a szigmoid függvénnyel képez valószínűséget:

p̂(x)=P(késés=1∣x)=11+e−z(x). \hat{p}(x)=P(\text{késés}=1\mid x)=\frac{1}{1+e^{-z(x)}}.

Itt xx a rendelés pillanatában ismert feature-vektor, dd a feature-ek száma, βj\beta_j a betanított együttható, p̂\hat{p} pedig a modell által becsült valószínűség. A predict_proba kimenete egy (1,2)(1, 2) alakú tömb: az első oszlop a P(y=0)P(y=0), a második a P(y=1)P(y=1) becslése. A demóban a második oszlopot jelenítjük meg.

A tisztességes megjelenítés a Kalibrált valószínűség és predikciós bizonytalanság alfejezetben tanultakra épül: a 0,73-as érték nem azt jelenti, hogy a rendelés 73%-os „valódi” eséllyel késik, hanem hogy a kalibráltnak szánt modell ezt a feltételes valószínűséget becsüli. A kalibrációt a validációs halmazon mértük, nem a UI-ban. A modellkártyán ezért mindig fel kell tüntetni a validációs Brier-pontszámot is:

B=1n∑i=1n(yi−p̂i)2, B=\frac{1}{n}\sum_{i=1}^{n}\left(y_i-\hat{p}_i\right)^2,

ahol nn a validációs sorok száma, yi∈{0,1}y_i\in\{0,1\} a tényleges kimenet, p̂i\hat{p}_i a becsült valószínűség. Ha például B=0,18B=0,18, az átlagos négyzetes eltérés 0,18, ami azt jelzi, hogy az egyedi pontbecslés körül jelentős bizonytalanság van; a UI ezt szóban és vizuálisan is jelöli, nem kelt hamis precizitást.

Szemléltető szigmoid átvitel - Számított függvénypélda, nem mérés.

A döntéshez a betanítás során rögzített küszöböt használjuk: ha p̂≥0.5\hat{p}\ge 0.5, a predikció „késik”, különben „nem késik”. Ez a küszöb a modellkártya része, nem a felhasználó szabadon tologatható paramétere, mert a küszöb eltolása a validációs hiba ismerete nélkül önkényes lenne.

35.3.3 Teljes minimális munkapélda

A labor könyvtárában a következő fájlokra támaszkodunk: artifacts/pipeline.joblib, artifacts/model_meta.json, artifacts/control_predictions.npz, valamint a korábban elkészült schemas.py és model_loader.py. A schemas.py a Pydantic szerződést tartalmazza, a model_loader.py a hash-ellenőrzött betöltést. A demó alkalmazásban egyetlen új modul, a streamlit_demo.py készül.

Először telepítjük a Streamlit csomagot a projektbe, majd elindítjuk az alkalmazást:

cd lab
uv add "streamlit>=1.40"
uv sync
uv run streamlit run streamlit_demo.py --server.headless true

Az utolsó sor platformfüggetlen; Windows PowerShellben ugyanígy futtatható a lab munkakönyvtárból. A pontos Streamlit-verzió a saját uv.lock-ban rögzítendő; a könyv keretjegyzete nem állítja, hogy egy adott verziókombinációt előzetesen ellenőriztünk.

Az alábbi kód a leírt API-t használja; a könyv írásakor nem futtattuk, ezért a hozzá tartozó felületi kijelzés szemléltető jellegű.

# streamlit_demo.py — kutatói demó, új tanítás nélkül
from pathlib import Path
import json
import numpy as np
import pandas as pd
import streamlit as st
from pydantic import ValidationError
from schemas import NorthstarRequest, LatePrediction
from model_loader import load_verified_model, ArtifactVerificationError

ARTIFACT_DIR = Path("artifacts")
THRESHOLD = 0.5  # a meta is tartalmazza; itt biztonsági alapértelmezés

@st.cache_resource(show_spinner="Modell ellenőrzése és betöltése...")
def get_verified_model(artifact_dir: str):
    return load_verified_model(Path(artifact_dir))

st.set_page_config(page_title="Northstar késés demó", layout="wide")

# Oldalsáv: modellkártya a meta-fájlból
meta_path = ARTIFACT_DIR / "model_meta.json"
meta = json.loads(meta_path.read_text(encoding="utf-8"))

with st.sidebar:
    st.header("Modellkártya")
    st.markdown(f"**Modellverzió:** {meta['model_version']}")
    st.markdown(f"**Betanítási cutoff:** {meta['training_cutoff']}")
    st.markdown(f"**Feature-lista:** {', '.join(meta['features'])}")
    st.markdown(f"**Validációs AUC:** {meta['validation_auc']:.3f}")
    st.markdown(f"**Validációs Brier:** {meta['validation_brier']:.3f}")
    st.markdown(f"**Döntési küszöb:** {meta.get('threshold', 0.5)}")
    st.warning(
        "Kutatói demó. A valószínűség pontbecslés; a kalibrációt a "
        "validációs halmazon mértük. Nem termelési döntési felület."
    )

try:
    package = get_verified_model(str(ARTIFACT_DIR))
    model = package["model"]
except ArtifactVerificationError as exc:
    st.error(f"A modellartefaktum ellenőrzése sikertelen: {exc}")
    st.stop()

st.title("Rendeléskésés becslése rendeléskor")
st.markdown(
    "Csak a rendelés pillanatában ismert mezőket fogadja el; minden "
    "predikció a betanított modellt használja további tanítás nélkül."
)

with st.form("prediction_form"):
    c1, c2, c3 = st.columns(3)
    distance_km = c1.number_input("Távolság (km)", min_value=0.0, value=25.0, step=1.0)
    priority_flag = c2.checkbox("Prioritás szállítás", value=False)
    basket_minor = c3.number_input("Kosár értéke (EUR-cent)", min_value=0, value=5000, step=500)
    quantity_sum = c1.number_input("Darabszám", min_value=1, value=3, step=1)
    account_age_days = c2.number_input("Fiók kora (nap)", min_value=0, value=120, step=1)
    segment = c3.selectbox("Szegmens", ["consumer", "business"])
    region = c1.selectbox("Régió", ["north", "central", "south"])
    acquisition_channel = c2.selectbox("Akvizíciós csatorna", ["search", "referral", "direct"])
    submitted = st.form_submit_button("Predikció futtatása")

if submitted:
    raw = {
        "distance_km": distance_km,
        "priority_flag": int(priority_flag),
        "basket_minor": basket_minor,
        "quantity_sum": quantity_sum,
        "account_age_days": account_age_days,
        "segment": segment,
        "region": region,
        "acquisition_channel": acquisition_channel,
    }
    try:
        request = NorthstarRequest(**raw)
        # A pipeline a meta features sorrendjében várja az oszlopokat.
        frame = pd.DataFrame([request.model_dump()], columns=meta["features"])
        proba = model.predict_proba(frame)[0, 1]
        predicted_late = int(proba >= THRESHOLD)
        result = LatePrediction(
            late_probability=float(proba),
            predicted_late=predicted_late,
            model_version=meta["model_version"],
        )
        history = st.session_state.setdefault("history", [])
        history.append({"input": raw, "result": result.model_dump()})
        st.session_state["history"] = history[-5:]
        st.success(
            f"Késési valószínűség: **{result.late_probability:.3f}** "
            f"(döntés: {'késhet' if result.predicted_late else 'időben érkezhet'})"
        )
        st.caption(
            "Ez a becslés a modellkártyán jelzett validációs bizonytalansággal "
            "együtt értelmezendő; egyetlen rendelés esetén nem mérési tény."
        )
    except ValidationError as exc:
        st.error(f"Érvénytelen bemenet: {exc}")

if st.session_state.get("history"):
    st.subheader("Utolsó predikciók")
    st.dataframe(pd.DataFrame(st.session_state["history"]))

A kód szándékosan nem hív tanítást: a model objektum a get_verified_model cache-elt erőforrása, a predikció pedig csak predict_proba. A widgetek minden értéket a Pydantic-tartománynak megfelelő típusban adnak: a checkbox lebegőpontos True helyett egész 0/1 értékké alakul, a kategóriák selectbox-ból jönnek, így a séma nem kap érvénytelen literált.

35.3.4 Eredmény értelmezése és kézi kontroll

A szemléltető futás eredménye: a felhasználó 25 km távolságot, 50 eurós kosarat, 3 darabot, 120 napos fiókot, consumer/north/search mezőket ad meg. A bemutató a model_meta.json szerinti 0.1.0-s verziót írja ki; a kontrollpredikciók egyezése már a betöltéskor megtörtént. A megjelenő valószínűség a modell konkrét válasza; ha például 0,21, akkor a lineáris pontszám z≈−1,32z \approx -1{,}32, mert σ(−1,32)≈0,21\sigma(-1{,}32)\approx 0{,}21. A felület nem mutat konfidenciaintervallumot, mert a betanított modellből egyetlen pontbecslés származik; a bizonytalanságot a validációs Brier-pontszám és a szöveges figyelmeztetés jeleníti meg.

Kézi kontrollként ellenőrizhetjük, hogy a predict_proba kimenet oszlopai összegeznek-e 1-re. A frame egy (1,d)(1, d) alakú DataFrame, a proba egy (1,2)(1, 2) alakú NumPy-tömb; az eredmény p̂1+p̂0=1\hat{p}_1+\hat{p}_0=1 10−910^{-9} abszolút tűrésen belül. A döntési szabály pedig csak a meta szerinti küszöböt használja, ezért a kijelzés determinisztikus ugyanarra a bemenetre.

A Streamlit állapot- és cache-kezelésében nem tárolható ellenőrizetlen felhasználói bemenetből betöltött pickle/joblib objektum.

35.3.5 Feladatok

  1. Értelmezés. A demóban a felhasználó azt látja, hogy p̂=0,73\hat{p}=0{,}73. Írja le, mit jelent ez a modellkártya szerint, és miért helytelen a felületen azt állítani, hogy „a csomag 73% valószínűséggel késik” minden további feltétel nélkül. Hivatkozzon a validációs Brier-pontszám szerepére.

  2. Implementálás. Bővítse a streamlit_demo.py-t egy gombbal, amely a session_state-ben tárolt előzményt törli, és egy másik gombbal, amely a predikciós űrlapot visszaállítja az alapértelmezett értékekre. A modellbetöltőhöz ne nyúljon.

  3. Hibakeresés. Egy fejlesztő a következő sort írta a get_verified_model helyére:

    model = load_verified_model(ARTIFACT_DIR)

    A felhasználó azt tapasztalja, hogy minden widget-kattintás után hosszú „Modell ellenőrzése” üzenet jelenik meg. Magyarázza el a Streamlit végrehajtási modellje alapján, miért, és adja meg a javítást.

  4. Megváltozott feltétel. A modellt újratanítják, a model_meta.json model_version értéke 1.1.0-ra változik, a fájlok azonos könyvtárban maradnak. A demo újraindítás nélkül továbbra is a régi verziót mutatja. Miért nem elég a st.cache_resource argumentumában csak a könyvtárnevet szerepeltetni? Adjon olyan cache-kulcsot, amely verzióváltáskor automatikusan újratölti a modellt.

  5. Visszautalás és önellenőrzés. Hogyan kapcsolódik a bemutató a Modellartefaktum és betöltési határ alfejezetben bevezetett betöltési határhoz? Soroljon fel legalább két ellenőrzést, amelyet a load_verified_model a UI előtt már elvégzett, és indokolja, miért nem helyettesítheti ezeket a tetszetős felület.

35.3.6 Megoldások és indoklás

  1. A p̂=0,73\hat{p}=0{,}73 a modell becsült feltételes valószínűsége annak, hogy a rendelés késik a megadott feature-ek mellett. A kalibráltnak szánt modell esetén ez azt jelenti, hogy sok hasonló, 0,73 körüli pontszámú esetből hosszú távon körülbelül 73% késhet; de egyetlen rendelésnél nincs „részleges” késés, és a becslés hibája a Brier-pontszámban jelenik meg. A 0,73 tehát modell output, nem a valóság közvetlen mérése. A validációs Brier-pontszám, például 0,18, azt mutatja, hogy az egyedi becslések átlagos négyzetes eltérése jelentős; ezért a felületnek szövegesen is jeleznie kell a bizonytalanságot.

  2. A kiegészítő kód a form után helyezhető:

    col_clear, col_reset = st.columns(2)
    if col_clear.button("Előzmény törlése"):
        st.session_state.pop("history", None)
        st.rerun()
    
    if col_reset.button("Alapértelmezett űrlap"):
        for key in list(st.session_state.keys()):
            if key.startswith("form_"):
                del st.session_state[key]
        st.rerun()

    A valódi visszaállításhoz a widgeteket key paraméterrel kell ellátni, például key="form_distance_km", mert a st.session_state így őrzi az értékeket. A st.rerun újrarendezi a szkriptet, de a modell a cache_resource miatt nem töltődik újra. A törlés után az előzmények nem jelennek meg, mert a feltétel st.session_state.get("history") hamis lesz.

  3. A Streamlit minden interakcióra újrafuttatja a szkriptet felülről lefelé; a load_verified_model(ARTIFACT_DIR) tehát minden widget-változásnál újra deszerializálja a pipeline-t és futtatja a hash-, séma- és kontrollpredikció-ellenőrzést. Ez a drága betöltés a lassulás oka. A javítás a @st.cache_resource dekorátor, amely a függvény argumentumát használja kulcsként, és a betöltött modellt a folyamat szintjén tárolja. A kulcs itt a könyvtárnév szövege; ha a fájlok nem változnak, a betöltés egyszer történik.

  4. A st.cache_resource a függvény argumentumainak hash-ét használja; ha az argumentum továbbra is "artifacts", a cache nem tudja, hogy a fájlok megváltoztak. A load_verified_model belül hash-ellenőrzést végez, de a Streamlit cache előbb tér vissza a régi objektummal, ezért a meta régi verziót mutat. Megoldás: a cache kulcsba bele kell foglalni a verziót vagy a meta hash-ét. Például:

    @st.cache_resource(show_spinner="Modell ellenőrzése és betöltése...")
    def get_verified_model(artifact_dir: str, model_version: str):
        return load_verified_model(Path(artifact_dir))
    
    meta = json.loads((ARTIFACT_DIR / "model_meta.json").read_text())
    package = get_verified_model(str(ARTIFACT_DIR), meta["model_version"])

    A meta beolvasása olcsó, és nem szükséges cache-elni. A verzióváltáskor megváltozik a kulcs, a modell újratöltődik, a régi cache bejegyzés elavul. A model_version mellett a meta artifact_hashes összegét is lehet kulcsként használni, ha a verziószám nem változna.

  5. A load_verified_model először a fájlhasheket ellenőrzi a model_meta.json-ban rögzített értékekkel, csak ezután deszerializál; ellenőrzi a feature-listát a séma ellen, és a kontrollpredikciókat a 10−910^{-9} abszolút tűrésen belül összehasonlítja. A demó emiatt tudja megjeleníteni a modellverziót anélkül, hogy ismeretlen fájlt futtatna. A tetszetős UI ezeket az ellenőrzéseket nem pótolja: a szép felület ugyanazt a hibás artefaktumot is szépen mutatná, a betöltési kapu viszont megakadályozza, hogy sérült vagy nem várt objektum kerüljön a felületre.

35.3.7 Gyakori tévedés ellenpéldával

A leggyakoribb hiba a vizuális megjelenítés összekeverése a statisztikai érvényességgel. Ha a UI egy 0,9-es valószínűséget piros, nagy betűkkel jelenít meg, a felhasználó hajlamos azt gondolni, hogy a modell nagyon magabiztos. Ez azonban csak megjelenítés. Tegyük fel, hogy egy buta baseline minden rendelésre 0,75-öt ad; a felület ugyanolyan figyelmeztető színnel mutatja, de a validációs Brier-pontszám magas, a megbízhatóság pedig gyenge. A kalibrációt és az általánosítást a validációs halmazon kell mérni (Kalibrált valószínűség és predikciós bizonytalanság, Hiperparaméter és nested értékelés), nem a UI színpalettájával. Hasonlóképpen hibás a widgeteket az érvényességi tartományon túlra engedni: ha a UI nagyobb távolságot vagy negatív árat fogad el, mint a betanítási adatok, a modell extrapolál, és ezt a felület nem teheti láthatatlanná.

35.3.8 Rövid összefoglaló

A Streamlit-demó ugyanazt a betanított pipeline-t és meta-fájlt használja, amelyet a FastAPI-végpont is kiszolgál, új tanítás nélkül. A st.cache_resource a drága modellbetöltést őrzi, a st.session_state a felhasználói előzményt és űrlapállapotot tárolja, a Pydantic-séma pedig ugyanazt a bemeneti tartományt ellenőrzi, mint az API. A modellkártya a verziót, a feature-listát, a validációs AUC-t, Brier-pontszámot és küszöböt mutatja; a prediktív valószínűség pontbecslés, amelyet a UI szövegesen is annak nevez. A betöltő hash- és kontrollpredikció-ellenőrzése biztosítja, hogy a látványos felület ne takarjon el sérült artefaktumot. A következő alkalmazás Kiadás, visszaállítás és terhelési próba: kiadás, visszaállítás és terhelési próba, ahol a demó azt kérdezi meg, hogyan válik ellenőrzött szolgáltatássá.