| """Pandera data-validation contracts for the Telco Customer Churn dataset. |
| |
| Both the training contract (cleaned frame + target) and the serving contract |
| (19 feature columns, no target) derive from a single source of truth: the |
| ``ALLOWED`` mapping of categorical feature → permitted value set. |
| |
| Usage |
| ----- |
| Training pipeline : ``validate_clean(df)`` — call after ``clean_telco()`` |
| Serving / batch : ``validate_serving(df)`` — call before the scoring pipeline |
| """ |
| from __future__ import annotations |
|
|
| import pandas as pd |
| import pandera.pandas as pa |
|
|
| |
| |
| |
|
|
| _YES_NO: frozenset[str] = frozenset({"No", "Yes"}) |
| _YES_NO_NS: frozenset[str] = frozenset({"No", "No internet service", "Yes"}) |
| _YES_NO_NPS: frozenset[str] = frozenset({"No", "No phone service", "Yes"}) |
|
|
| |
| |
| |
| |
| |
| ALLOWED: dict[str, frozenset[str]] = { |
| "gender": frozenset({"Female", "Male"}), |
| "SeniorCitizen": frozenset({"0", "1"}), |
| "Partner": _YES_NO, |
| "Dependents": _YES_NO, |
| "PhoneService": _YES_NO, |
| "MultipleLines": _YES_NO_NPS, |
| "InternetService": frozenset({"DSL", "Fiber optic", "No"}), |
| "OnlineSecurity": _YES_NO_NS, |
| "OnlineBackup": _YES_NO_NS, |
| "DeviceProtection": _YES_NO_NS, |
| "TechSupport": _YES_NO_NS, |
| "StreamingTV": _YES_NO_NS, |
| "StreamingMovies": _YES_NO_NS, |
| "Contract": frozenset({"Month-to-month", "One year", "Two year"}), |
| "PaperlessBilling": _YES_NO, |
| "PaymentMethod": frozenset({ |
| "Bank transfer (automatic)", |
| "Credit card (automatic)", |
| "Electronic check", |
| "Mailed check", |
| }), |
| } |
|
|
| |
| |
| |
|
|
|
|
| def _cat_col(allowed: frozenset[str]) -> pa.Column: |
| return pa.Column(dtype=str, checks=pa.Check.isin(list(allowed)), nullable=False) |
|
|
|
|
| def _feature_columns() -> dict[str, pa.Column]: |
| """Build the 19 feature columns that appear in both training and serving schemas.""" |
| cols: dict[str, pa.Column] = { |
| "tenure": pa.Column( |
| dtype=float, |
| checks=[pa.Check.ge(0), pa.Check.le(72)], |
| nullable=False, |
| ), |
| "MonthlyCharges": pa.Column( |
| dtype=float, |
| checks=pa.Check.gt(0), |
| nullable=False, |
| ), |
| "TotalCharges": pa.Column( |
| dtype=float, |
| checks=pa.Check.ge(0), |
| nullable=False, |
| ), |
| } |
| for feature, allowed in ALLOWED.items(): |
| cols[feature] = _cat_col(allowed) |
| return cols |
|
|
|
|
| |
| |
| |
|
|
| _TRAIN_COLS: dict[str, pa.Column] = _feature_columns() |
| _TRAIN_COLS["Churn"] = pa.Column(dtype=int, checks=pa.Check.isin([0, 1]), nullable=False) |
|
|
| TRAIN_SCHEMA: pa.DataFrameSchema = pa.DataFrameSchema( |
| columns=_TRAIN_COLS, |
| coerce=False, |
| strict=False, |
| ) |
|
|
| SERVE_SCHEMA: pa.DataFrameSchema = pa.DataFrameSchema( |
| columns=_feature_columns(), |
| coerce=False, |
| strict=False, |
| ) |
|
|
| |
| |
| |
|
|
|
|
| def _format_errors(exc: pa.errors.SchemaErrors) -> str: |
| try: |
| cols = exc.failure_cases["column"].dropna().unique().tolist() |
| detail = exc.failure_cases[["column", "check", "failure_case"]].to_string(index=False) |
| return f"column(s) {cols}:\n{detail}" |
| except Exception: |
| return str(exc) |
|
|
|
|
| |
| |
| |
|
|
|
|
| def validate_clean(df: pd.DataFrame) -> pd.DataFrame: |
| """Validate the post-``clean_telco`` DataFrame against the training contract. |
| |
| Runs a lazy (collect-all-errors) Pandera check. On violation, raises |
| ``ValueError`` naming every offending column and failure case. Returns |
| ``df`` unchanged on success. |
| """ |
| try: |
| return TRAIN_SCHEMA.validate(df, lazy=True) |
| except pa.errors.SchemaErrors as exc: |
| raise ValueError( |
| f"validate_clean: {len(exc.failure_cases)} violation(s) in {_format_errors(exc)}" |
| ) from exc |
| except pa.errors.SchemaError as exc: |
| raise ValueError(f"validate_clean: {exc}") from exc |
|
|
|
|
| def validate_serving(df: pd.DataFrame) -> pd.DataFrame: |
| """Validate a serving-input DataFrame (19 features, no Churn target). |
| |
| Reuses the same ``ALLOWED`` sets as the training contract. Suitable for |
| batch scoring and drift-monitoring pipelines. |
| """ |
| try: |
| return SERVE_SCHEMA.validate(df, lazy=True) |
| except pa.errors.SchemaErrors as exc: |
| raise ValueError( |
| f"validate_serving: {len(exc.failure_cases)} violation(s) in {_format_errors(exc)}" |
| ) from exc |
| except pa.errors.SchemaError as exc: |
| raise ValueError(f"validate_serving: {exc}") from exc |
|
|