Files
yellow-bank-soal/app/models/tryout_stats.py

169 lines
4.8 KiB
Python

"""
TryoutStats model for tracking tryout-level statistics.
Maintains running statistics for dynamic normalization and reporting.
"""
from datetime import datetime
from typing import Union
from sqlalchemy import (
CheckConstraint,
DateTime,
Float,
ForeignKey,
ForeignKeyConstraint,
Index,
Integer,
String,
func,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
class TryoutStats(Base):
"""
TryoutStats model for maintaining tryout-level statistics.
Tracks participant counts, score distributions, and calculated
normalization parameters (rataan, sb) for dynamic normalization.
Attributes:
id: Primary key
website_id: Website identifier
tryout_id: Tryout identifier
participant_count: Number of completed sessions
total_nm_sum: Running sum of NM scores
total_nm_sq_sum: Running sum of squared NM scores (for variance calc)
rataan: Calculated mean of NM scores
sb: Calculated standard deviation of NM scores
min_nm: Minimum NM score observed
max_nm: Maximum NM score observed
last_calculated: Timestamp of last statistics update
created_at: Record creation timestamp
updated_at: Record update timestamp
tryout: Tryout relationship
"""
__tablename__ = "tryout_stats"
# Primary key
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
# Foreign keys
website_id: Mapped[int] = mapped_column(
ForeignKey("websites.id", ondelete="CASCADE", onupdate="CASCADE"),
nullable=False,
index=True,
comment="Website identifier",
)
tryout_id: Mapped[str] = mapped_column(
String(255),
nullable=False,
index=True,
comment="Tryout identifier",
)
# Running statistics
participant_count: Mapped[int] = mapped_column(
Integer,
nullable=False,
default=0,
comment="Number of completed sessions",
)
total_nm_sum: Mapped[float] = mapped_column(
Float,
nullable=False,
default=0.0,
comment="Running sum of NM scores",
)
total_nm_sq_sum: Mapped[float] = mapped_column(
Float,
nullable=False,
default=0.0,
comment="Running sum of squared NM scores",
)
# Calculated statistics
rataan: Mapped[Union[float, None]] = mapped_column(
Float,
nullable=True,
comment="Calculated mean of NM scores",
)
sb: Mapped[Union[float, None]] = mapped_column(
Float,
nullable=True,
comment="Calculated standard deviation of NM scores",
)
# Score range
min_nm: Mapped[Union[int, None]] = mapped_column(
Integer,
nullable=True,
comment="Minimum NM score observed",
)
max_nm: Mapped[Union[int, None]] = mapped_column(
Integer,
nullable=True,
comment="Maximum NM score observed",
)
# Timestamps
last_calculated: Mapped[Union[datetime, None]] = mapped_column(
DateTime(timezone=True),
nullable=True,
comment="Timestamp of last statistics update",
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
server_default=func.now(),
onupdate=func.now(),
)
# Relationships
tryout: Mapped["Tryout"] = relationship(
"Tryout", back_populates="stats", lazy="selectin"
)
# Constraints and indexes
__table_args__ = (
ForeignKeyConstraint(
["website_id", "tryout_id"],
["tryouts.website_id", "tryouts.tryout_id"],
name="fk_tryout_stats_tryout",
ondelete="CASCADE",
onupdate="CASCADE",
),
Index(
"ix_tryout_stats_website_id_tryout_id",
"website_id",
"tryout_id",
unique=True,
),
# Participant count must be non-negative
CheckConstraint("participant_count >= 0", "ck_participant_count_non_negative"),
# Min and max NM must be within valid range [0, 1000]
CheckConstraint(
"min_nm IS NULL OR (min_nm >= 0 AND min_nm <= 1000)",
"ck_min_nm_range",
),
CheckConstraint(
"max_nm IS NULL OR (max_nm >= 0 AND max_nm <= 1000)",
"ck_max_nm_range",
),
# Min must be less than or equal to max
CheckConstraint(
"min_nm IS NULL OR max_nm IS NULL OR min_nm <= max_nm",
"ck_min_max_nm_order",
),
)
def __repr__(self) -> str:
return f"<TryoutStats(tryout_id={self.tryout_id}, participant_count={self.participant_count})>"