OC_P8 / database /models.py
KLEB38's picture
Upload folder using huggingface_hub
1091d74 verified
Raw
History Blame Contribute Delete
3.87 kB
"""SQLAlchemy models for the prediction monitoring database.
A single Table definition is parametrised by name so the production table
(``predictions_log``) and the CI test table (``predictions_log_test``) share
exactly the same schema without duplication.
The 768 engineered features are stored in a JSONB column rather than as
individual columns — three feature names exceed PostgreSQL's 63-character
identifier limit, and a flat JSONB also keeps the schema stable when the
feature engineering pipeline evolves.
"""
from __future__ import annotations
from sqlalchemy import (
Boolean,
Column,
DateTime,
Float,
Integer,
MetaData,
String,
Table,
Text,
func,
)
from sqlalchemy.dialects.postgresql import JSONB, UUID
PROD_TABLE_NAME = "predictions_log"
TEST_TABLE_NAME = "predictions_log_test"
def build_predictions_log_table(name: str, metadata: MetaData) -> Table:
"""Return a Table object with the prediction log schema, bound to ``metadata``.
Args:
name: physical table name in PostgreSQL.
metadata: SQLAlchemy MetaData to attach the Table to. Pass a fresh
MetaData per table to avoid duplicate-key collisions in tests.
Returns:
Table ready to be created via ``metadata.create_all(engine)``.
"""
return Table(
name,
metadata,
# Identity
Column(
"id",
UUID(as_uuid=True),
primary_key=True,
server_default=func.gen_random_uuid(),
),
Column(
"timestamp",
DateTime(timezone=True),
nullable=False,
server_default=func.now(),
index=True,
),
Column("sk_id_curr", Integer, nullable=False, index=True),
Column("client_known", Boolean, nullable=False),
# Operational metrics. All timings are stored as INTEGER milliseconds —
# we always round at the Python layer (``round()``, not ``int()``)
# before insert, which gives unbiased values. The handler ``latency_ms``
# only covers the request handler body (assembly + inference + return
# construction). The DB INSERT itself is measured separately as
# ``db_log_ms`` so the full server-side budget can be reconstructed as
# ``latency_ms + db_log_ms``.
Column("latency_ms", Integer, nullable=False),
# Fine-grained timings added in étape 4. Nullable so legacy rows
# (pre-instrumentation) remain valid. Populated only on the success
# path — error rows leave them NULL.
Column("feature_assembly_ms", Integer, nullable=True),
Column("inference_ms", Integer, nullable=True),
Column("inference_cpu_ms", Integer, nullable=True),
# Plumbing = latency_ms - feature_assembly_ms - inference_ms, computed
# once in the API at log time so the dashboard (and any other reader)
# reads a self-describing column instead of re-deriving the formula.
Column("plumbing_ms", Integer, nullable=True),
Column("db_log_ms", Integer, nullable=True),
Column("status_code", Integer, nullable=False, server_default="200"),
Column("error_message", Text, nullable=True),
# Payloads
Column("raw_input", JSONB, nullable=False),
Column("features", JSONB, nullable=False),
# Model output. Nullable so error rows (status != 200) can carry NULL.
Column("probability_default", Float, nullable=True),
Column("decision", String(16), nullable=False, index=True),
Column("threshold", Float, nullable=False),
Column("model_version", String(64), nullable=False),
Column("top_shap", JSONB, nullable=True),
# Ground truth — filled post-hoc when business feedback arrives
Column("ground_truth", Integer, nullable=True),
)