Table of Contents

    Load a CSV; inspect types; clean missing values; build a train-only preprocessing function

    GUIDED IMPLEMENTATION • 60 MIN

    Load a CSV, Inspect Types, Clean Missing Values and Build a Train-Only Preprocessing Function

    Build a practical, leakage-safe data preparation workflow with pandas and scikit-learn, including CSV loading, type inspection, missing-value analysis, train-test splitting and reusable preprocessing code.

    Learning objective: By the end of this guided implementation, you will have runnable, documented Python code that loads a CSV file, inspects column types, cleans common data-quality problems, splits data before learning preprocessing values, fits preprocessing only on the training set and applies the same fitted transformations to validation, test or future data.

    1. Why Train-Only Preprocessing Matters

    Data preparation is part of the machine-learning model. Any preprocessing step that learns information from data, such as a median, mean, most-frequent category, scaling parameter or category vocabulary, must be fitted using the training set only.

    If preprocessing statistics are calculated from the complete dataset before the train-test split, information from the test set influences the training process. This is called data leakage. Leakage can make evaluation results appear better than the model's true performance on unseen data.

    CORRECT WORKFLOW
    Load → Inspect → Split → Fit on train → Transform train and test
    Leaky approach Calculate medians, means, standard deviations or category mappings from the full dataset and then split it.
    Leakage-safe approach Split the data first. Fit all learned preprocessing rules on X_train only, then reuse those fitted rules to transform X_train, X_test and future records.

    2. Prerequisites

    You should understand Python variables, functions, dictionaries and basic pandas DataFrame operations. This implementation uses pandas for data inspection and scikit-learn for train-test splitting and reusable preprocessing.

    Required Setup

    • Python 3.9 or later is recommended.
    • Install pandas, NumPy and scikit-learn.
    • Use Jupyter Notebook, VS Code, PyCharm or another Python environment.
    • Keep the source CSV unchanged and write cleaned outputs to separate files.
    • Know the target column that the model will predict.
    python -m pip install pandas numpy scikit-learn
    import numpy as np
    import pandas as pd
    import sklearn
    
    print("NumPy:", np.__version__)
    print("pandas:", pd.__version__)
    print("scikit-learn:", sklearn.__version__)

    3. Create a Practice CSV

    If you already have a CSV file, you can skip this section and update the file path in the next step. The following code creates a small dataset containing numeric values, categories, missing values, an invalid numeric value and one duplicate row.

    from pathlib import Path
    import pandas as pd
    
    sample_data = pd.DataFrame({
        "customer_id": [101, 102, 103, 104, 105, 106, 107, 107],
        "age": [25, 41, None, 36, 29, 52, 44, 44],
        "monthly_income": [35000, 62000, 48000, None, 41000, 79000, "unknown", "unknown"],
        "city": ["Kolkata", "Delhi", None, "Mumbai", " kolkata ", "Delhi", "Pune", "Pune"],
        "signup_date": [
            "2026-01-05", "2026-01-10", "invalid", "2026-02-01",
            "2026-02-08", None, "2026-03-15", "2026-03-15"
        ],
        "purchased": [1, 0, 1, 0, 1, 1, 0, 0]
    })
    
    csv_path = Path("customer_training_data.csv")
    sample_data.to_csv(csv_path, index=False)
    print(f"Created: {csv_path.resolve()}")

    4. Load the CSV Safely

    Use a function that checks the file path, loads the file and confirms that data is present. Fail early with a clear error instead of allowing an empty or incorrect dataset to move further into the pipeline.

    from pathlib import Path
    import pandas as pd
    
    
    def load_csv(file_path: str) -> pd.DataFrame:
        """Load a CSV file and perform basic structural validation."""
        path = Path(file_path)
    
        if not path.exists():
            raise FileNotFoundError(f"CSV file not found: {path.resolve()}")
    
        if path.suffix.lower() != ".csv":
            raise ValueError(f"Expected a .csv file, received: {path.suffix}")
    
        df = pd.read_csv(path)
    
        if df.empty:
            raise ValueError("The CSV file contains no data rows.")
    
        if not df.columns.is_unique:
            raise ValueError("The CSV contains duplicate column names.")
    
        return df
    
    
    df = load_csv("customer_training_data.csv")
    print(df.head())
    Real-world note: A production file may require options such as encoding="utf-8", sep=";", na_values=["NA", "N/A", "?"] or explicit dtype settings. Configure these according to the actual data contract instead of guessing silently.

    5. Inspect Shape, Types and Sample Values

    Inspection should happen before cleaning. Record the number of rows and columns, inferred dtypes, missing-value counts, duplicate counts and representative values.

    def inspect_dataframe(df: pd.DataFrame) -> None:
        """Print a compact diagnostic report for a DataFrame."""
        print("\n--- SHAPE ---")
        print(f"Rows: {df.shape[0]}, Columns: {df.shape[1]}")
    
        print("\n--- FIRST FIVE ROWS ---")
        print(df.head())
    
        print("\n--- DATA TYPES ---")
        print(df.dtypes)
    
        print("\n--- DATAFRAME INFO ---")
        df.info()
    
        print("\n--- MISSING VALUES ---")
        missing = pd.DataFrame({
            "missing_count": df.isna().sum(),
            "missing_percent": df.isna().mean().mul(100).round(2)
        })
        print(missing.sort_values("missing_percent", ascending=False))
    
        print("\n--- DUPLICATE ROWS ---")
        print(df.duplicated().sum())
    
        print("\n--- DESCRIPTIVE SUMMARY ---")
        print(df.describe(include="all").transpose())
    
    
    inspect_dataframe(df)

    6. Why Inferred Types May Be Wrong

    CSV files do not store rich database-style schemas. pandas infers each column's dtype from its values. A numeric column containing a value such as "unknown" may be loaded as object instead of a numerical dtype.

    Observed dtypePossible meaningRequired check
    int64Whole numbersDecide whether it is a quantity, label or identifier.
    float64Decimal values or numbers containing missing dataCheck valid range and unit.
    objectUsually text, but possibly mixed dataInspect representative and unique values.
    boolTrue or false valuesConfirm the encoding is consistent.
    datetime64[ns]Parsed date and timeConfirm timezone and date semantics.
    categoryRepeated labelsConfirm allowed levels and unseen-category behavior.

    7. Perform Deterministic Structural Cleaning

    Some transformations do not learn statistical information from the dataset. Examples include normalizing column names, converting known invalid tokens to missing values, trimming strings and parsing dates. These deterministic rules may be encapsulated separately from learned preprocessing.

    import numpy as np
    import pandas as pd
    
    
    def clean_structure(df: pd.DataFrame) -> pd.DataFrame:
        """
        Apply deterministic cleaning rules.
    
        This function does not calculate means, medians, modes or scaling
        parameters. It returns a new DataFrame and leaves the input unchanged.
        """
        cleaned = df.copy()
    
        # Normalize column names.
        cleaned.columns = (
            cleaned.columns
            .str.strip()
            .str.lower()
            .str.replace(r"[^a-z0-9]+", "_", regex=True)
            .str.strip("_")
        )
    
        # Remove exact duplicate records.
        cleaned = cleaned.drop_duplicates().reset_index(drop=True)
    
        # Convert known invalid tokens to missing values.
        missing_tokens = ["", " ", "na", "n/a", "none", "null", "unknown", "?"]
        cleaned = cleaned.replace(missing_tokens, np.nan)
    
        # Convert fields according to the known schema.
        if "monthly_income" in cleaned.columns:
            cleaned["monthly_income"] = pd.to_numeric(
                cleaned["monthly_income"], errors="coerce"
            )
    
        if "age" in cleaned.columns:
            cleaned["age"] = pd.to_numeric(cleaned["age"], errors="coerce")
    
        if "signup_date" in cleaned.columns:
            cleaned["signup_date"] = pd.to_datetime(
                cleaned["signup_date"], errors="coerce"
            )
    
        # Standardize known text fields.
        if "city" in cleaned.columns:
            cleaned["city"] = cleaned["city"].astype("string").str.strip().str.title()
    
        return cleaned
    
    
    clean_df = clean_structure(df)
    inspect_dataframe(clean_df)
    Be careful with duplicate removal: Only remove rows when you understand what defines a duplicate. Two identical transactions may both be valid. When a business key exists, use it deliberately, such as drop_duplicates(subset=["order_id"]).

    8. Validate the Expected Schema

    A preprocessing pipeline should verify required columns and basic assumptions. This prevents a renamed or missing source field from producing a confusing error later.

    def validate_schema(df: pd.DataFrame, target_column: str) -> None:
        """Validate required columns and simple business assumptions."""
        required_columns = {
            "customer_id",
            "age",
            "monthly_income",
            "city",
            "signup_date",
            target_column
        }
    
        missing_columns = required_columns.difference(df.columns)
        if missing_columns:
            raise ValueError(
                "Required columns are missing: "
                + ", ".join(sorted(missing_columns))
            )
    
        if df[target_column].isna().any():
            raise ValueError("The target column contains missing values.")
    
        valid_target_values = {0, 1}
        actual_target_values = set(df[target_column].dropna().unique())
        if not actual_target_values.issubset(valid_target_values):
            raise ValueError(
                f"Unexpected target values: {sorted(actual_target_values)}"
            )
    
        if "age" in df.columns:
            invalid_age = df["age"].dropna().lt(0) | df["age"].dropna().gt(120)
            if invalid_age.any():
                raise ValueError("Age contains values outside the allowed range 0-120.")
    
    
    TARGET_COLUMN = "purchased"
    validate_schema(clean_df, TARGET_COLUMN)

    9. Separate Features and Target

    The target must not be included in the feature-preprocessing inputs. Otherwise the transformation can accidentally expose the answer to the model.

    TARGET_COLUMN = "purchased"
    
    X = clean_df.drop(columns=[TARGET_COLUMN])
    y = clean_df[TARGET_COLUMN].astype("int64")
    
    print("Feature shape:", X.shape)
    print("Target shape:", y.shape)
    print("Target distribution:")
    print(y.value_counts(normalize=True).sort_index())

    10. Split Before Learning Imputation Values

    Perform the train-test split before fitting imputers, encoders or scalers. For classification, stratify=y attempts to preserve the target distribution in both subsets.

    from sklearn.model_selection import train_test_split
    
    X_train, X_test, y_train, y_test = train_test_split(
        X,
        y,
        test_size=0.25,
        random_state=42,
        stratify=y
    )
    
    print("X_train:", X_train.shape)
    print("X_test:", X_test.shape)
    print("y_train:", y_train.shape)
    print("y_test:", y_test.shape)
    REPRODUCIBILITY
    Use a fixed random_state during development so the same split can be reproduced.
    Small-dataset note: The demonstration dataset is intentionally tiny. Stratification can fail if a target class has too few observations for the requested split. For real model evaluation, use enough representative records and choose the split or cross-validation strategy based on the problem.

    11. Decide Which Columns Enter the Model

    An identifier such as customer_id identifies a record but usually should not be treated as a numerical predictive feature. Dates can be transformed into meaningful components such as account age, year, month or day of week.

    def build_model_features(df: pd.DataFrame) -> pd.DataFrame:
        """Create deterministic model features from the structurally cleaned data."""
        features = df.copy()
    
        # Drop a record identifier that should not be learned as a numeric quantity.
        features = features.drop(columns=["customer_id"], errors="ignore")
    
        # Convert date into model-friendly components.
        if "signup_date" in features.columns:
            features["signup_year"] = features["signup_date"].dt.year
            features["signup_month"] = features["signup_date"].dt.month
            features["signup_day_of_week"] = features["signup_date"].dt.dayofweek
            features = features.drop(columns=["signup_date"])
    
        return features
    
    
    X_train_features = build_model_features(X_train)
    X_test_features = build_model_features(X_test)
    
    print(X_train_features.dtypes)

    12. Inspect Missing Values in Training Data

    Review missingness after the split and feature creation. The training subset determines the imputation strategy and learned replacement values.

    def missing_value_report(df: pd.DataFrame) -> pd.DataFrame:
        """Return count, percentage and dtype for each column."""
        report = pd.DataFrame({
            "dtype": df.dtypes.astype(str),
            "missing_count": df.isna().sum(),
            "missing_percent": df.isna().mean().mul(100).round(2)
        })
    
        return report.sort_values(
            ["missing_percent", "missing_count"],
            ascending=False
        )
    
    
    print(missing_value_report(X_train_features))

    13. Select Numeric and Categorical Columns

    For this implementation, numeric features receive median imputation and standard scaling. Categorical features receive most-frequent imputation and one-hot encoding.

    numeric_features = X_train_features.select_dtypes(
        include=["number"]
    ).columns.tolist()
    
    categorical_features = X_train_features.select_dtypes(
        include=["object", "string", "category", "bool"]
    ).columns.tolist()
    
    print("Numeric features:", numeric_features)
    print("Categorical features:", categorical_features)
    Feature typeMissing-value strategyAdditional transformation
    NumericMedian of the training columnStandard scaling fitted on training data
    CategoricalMost frequent training categoryOne-hot encoding with unknown categories ignored
    IdentifierNormally not imputed for modelingDrop or treat according to business meaning
    DateDepends on the derived featureExtract meaningful components before preprocessing

    14. Build a Reusable Train-Only Preprocessor

    A scikit-learn ColumnTransformer applies different pipelines to different column groups. The object learns values only when fit() or fit_transform() is called.

    from sklearn.compose import ColumnTransformer
    from sklearn.impute import SimpleImputer
    from sklearn.pipeline import Pipeline
    from sklearn.preprocessing import OneHotEncoder, StandardScaler
    
    
    def build_preprocessor(
        numeric_features: list[str],
        categorical_features: list[str]
    ) -> ColumnTransformer:
        """Build an unfitted preprocessing graph for numeric and categorical data."""
    
        numeric_pipeline = Pipeline(steps=[
            ("imputer", SimpleImputer(strategy="median", add_indicator=True)),
            ("scaler", StandardScaler())
        ])
    
        categorical_pipeline = Pipeline(steps=[
            ("imputer", SimpleImputer(strategy="most_frequent")),
            (
                "encoder",
                OneHotEncoder(handle_unknown="ignore", sparse_output=False)
            )
        ])
    
        return ColumnTransformer(
            transformers=[
                ("numeric", numeric_pipeline, numeric_features),
                ("categorical", categorical_pipeline, categorical_features)
            ],
            remainder="drop",
            verbose_feature_names_out=False
        )
    
    
    preprocessor = build_preprocessor(
        numeric_features=numeric_features,
        categorical_features=categorical_features
    )
    Why handle_unknown="ignore"? A category may appear in the test set or future production data that was absent from training. Ignoring unknown categories prevents transformation from failing, while the new category receives zeros across the learned one-hot columns.

    15. Fit Only on Training Data

    This is the most important implementation step. Call fit_transform() on the training features. Call only transform() on the test features.

    X_train_processed = preprocessor.fit_transform(X_train_features)
    X_test_processed = preprocessor.transform(X_test_features)
    
    feature_names = preprocessor.get_feature_names_out()
    
    X_train_processed = pd.DataFrame(
        X_train_processed,
        columns=feature_names,
        index=X_train_features.index
    )
    
    X_test_processed = pd.DataFrame(
        X_test_processed,
        columns=feature_names,
        index=X_test_features.index
    )
    
    print("Processed training shape:", X_train_processed.shape)
    print("Processed test shape:", X_test_processed.shape)
    print(X_train_processed.head())

    Do not fit again on test data

    • preprocessor.fit_transform(X_test)
    • Learns medians from test records
    • Learns test category vocabulary
    • Makes evaluation inconsistent and leaky

    Reuse the training-fitted object

    • fit_transform(X_train)
    • transform(X_test)
    • Same column order
    • Same imputation and scaling parameters

    16. Inspect Learned Training Statistics

    The fitted pipelines retain the values learned from the training subset. Inspecting them helps verify the implementation and document the preprocessing behavior.

    numeric_imputer = (
        preprocessor
        .named_transformers_["numeric"]
        .named_steps["imputer"]
    )
    
    categorical_imputer = (
        preprocessor
        .named_transformers_["categorical"]
        .named_steps["imputer"]
    )
    
    print("Numeric columns:", numeric_features)
    print("Training medians:", numeric_imputer.statistics_)
    
    print("Categorical columns:", categorical_features)
    print("Training category replacements:", categorical_imputer.statistics_)

    17. Validate the Processed Output

    A preprocessing function should verify that training and test outputs have compatible schemas and contain no unexpected missing values.

    def validate_processed_data(
        X_train_processed: pd.DataFrame,
        X_test_processed: pd.DataFrame,
        y_train: pd.Series,
        y_test: pd.Series
    ) -> None:
        """Run post-transformation quality checks."""
    
        if list(X_train_processed.columns) != list(X_test_processed.columns):
            raise ValueError("Training and test feature columns do not match.")
    
        if X_train_processed.isna().any().any():
            raise ValueError("Processed training data still contains missing values.")
    
        if X_test_processed.isna().any().any():
            raise ValueError("Processed test data still contains missing values.")
    
        if len(X_train_processed) != len(y_train):
            raise ValueError("Training feature and target row counts do not match.")
    
        if len(X_test_processed) != len(y_test):
            raise ValueError("Test feature and target row counts do not match.")
    
        print("Processed data validation passed.")
    
    
    validate_processed_data(
        X_train_processed,
        X_test_processed,
        y_train,
        y_test
    )

    18. Complete Runnable Implementation

    The following script combines the full workflow into reusable functions. Save it as train_only_preprocessing.py, place the CSV file in the expected location and run it from the terminal or your Python environment.

    from __future__ import annotations
    
    from dataclasses import dataclass
    from pathlib import Path
    
    import joblib
    import numpy as np
    import pandas as pd
    from sklearn.compose import ColumnTransformer
    from sklearn.impute import SimpleImputer
    from sklearn.model_selection import train_test_split
    from sklearn.pipeline import Pipeline
    from sklearn.preprocessing import OneHotEncoder, StandardScaler
    
    
    @dataclass
    class PreparedData:
        """Container returned by the train-only preprocessing workflow."""
    
        X_train: pd.DataFrame
        X_test: pd.DataFrame
        y_train: pd.Series
        y_test: pd.Series
        preprocessor: ColumnTransformer
        numeric_features: list[str]
        categorical_features: list[str]
    
    
    def load_csv(file_path: str | Path) -> pd.DataFrame:
        """Load a non-empty CSV with unique column names."""
        path = Path(file_path)
    
        if not path.exists():
            raise FileNotFoundError(f"CSV file not found: {path.resolve()}")
    
        if path.suffix.lower() != ".csv":
            raise ValueError(f"Expected a CSV file, received: {path.suffix}")
    
        df = pd.read_csv(path)
    
        if df.empty:
            raise ValueError("The CSV file contains no data rows.")
    
        if not df.columns.is_unique:
            raise ValueError("Duplicate column names were detected.")
    
        return df
    
    
    def clean_structure(df: pd.DataFrame) -> pd.DataFrame:
        """Apply deterministic, non-statistical cleaning rules."""
        cleaned = df.copy()
    
        cleaned.columns = (
            cleaned.columns
            .str.strip()
            .str.lower()
            .str.replace(r"[^a-z0-9]+", "_", regex=True)
            .str.strip("_")
        )
    
        cleaned = cleaned.drop_duplicates().reset_index(drop=True)
    
        missing_tokens = ["", " ", "na", "n/a", "none", "null", "unknown", "?"]
        cleaned = cleaned.replace(missing_tokens, np.nan)
    
        if "age" in cleaned.columns:
            cleaned["age"] = pd.to_numeric(cleaned["age"], errors="coerce")
    
        if "monthly_income" in cleaned.columns:
            cleaned["monthly_income"] = pd.to_numeric(
                cleaned["monthly_income"], errors="coerce"
            )
    
        if "city" in cleaned.columns:
            cleaned["city"] = cleaned["city"].astype("string").str.strip().str.title()
    
        if "signup_date" in cleaned.columns:
            cleaned["signup_date"] = pd.to_datetime(
                cleaned["signup_date"], errors="coerce"
            )
    
        return cleaned
    
    
    def build_model_features(df: pd.DataFrame) -> pd.DataFrame:
        """Create deterministic model inputs without learning dataset statistics."""
        features = df.copy()
        features = features.drop(columns=["customer_id"], errors="ignore")
    
        if "signup_date" in features.columns:
            features["signup_year"] = features["signup_date"].dt.year
            features["signup_month"] = features["signup_date"].dt.month
            features["signup_day_of_week"] = features["signup_date"].dt.dayofweek
            features = features.drop(columns=["signup_date"])
    
        return features
    
    
    def build_preprocessor(
        numeric_features: list[str],
        categorical_features: list[str]
    ) -> ColumnTransformer:
        """Build an unfitted preprocessing object."""
    
        numeric_pipeline = Pipeline(steps=[
            ("imputer", SimpleImputer(strategy="median", add_indicator=True)),
            ("scaler", StandardScaler())
        ])
    
        categorical_pipeline = Pipeline(steps=[
            ("imputer", SimpleImputer(strategy="most_frequent")),
            (
                "encoder",
                OneHotEncoder(handle_unknown="ignore", sparse_output=False)
            )
        ])
    
        return ColumnTransformer(
            transformers=[
                ("numeric", numeric_pipeline, numeric_features),
                ("categorical", categorical_pipeline, categorical_features)
            ],
            remainder="drop",
            verbose_feature_names_out=False
        )
    
    
    def prepare_train_test_data(
        file_path: str | Path,
        target_column: str,
        test_size: float = 0.25,
        random_state: int = 42
    ) -> PreparedData:
        """
        Load a CSV and build leakage-safe train and test matrices.
    
        Learned preprocessing values are fitted exclusively on X_train.
        The fitted object is reused to transform X_test.
        """
        raw_df = load_csv(file_path)
        clean_df = clean_structure(raw_df)
    
        if target_column not in clean_df.columns:
            raise ValueError(f"Target column not found: {target_column}")
    
        if clean_df[target_column].isna().any():
            raise ValueError("Target column contains missing values.")
    
        X = clean_df.drop(columns=[target_column])
        y = clean_df[target_column]
    
        X_train, X_test, y_train, y_test = train_test_split(
            X,
            y,
            test_size=test_size,
            random_state=random_state,
            stratify=y
        )
    
        X_train_features = build_model_features(X_train)
        X_test_features = build_model_features(X_test)
    
        numeric_features = X_train_features.select_dtypes(
            include=["number"]
        ).columns.tolist()
    
        categorical_features = X_train_features.select_dtypes(
            include=["object", "string", "category", "bool"]
        ).columns.tolist()
    
        if not numeric_features and not categorical_features:
            raise ValueError("No supported model features were found.")
    
        preprocessor = build_preprocessor(
            numeric_features=numeric_features,
            categorical_features=categorical_features
        )
    
        train_array = preprocessor.fit_transform(X_train_features)
        test_array = preprocessor.transform(X_test_features)
    
        feature_names = preprocessor.get_feature_names_out()
    
        X_train_processed = pd.DataFrame(
            train_array,
            columns=feature_names,
            index=X_train_features.index
        )
    
        X_test_processed = pd.DataFrame(
            test_array,
            columns=feature_names,
            index=X_test_features.index
        )
    
        if X_train_processed.isna().any().any():
            raise ValueError("Processed training data contains missing values.")
    
        if X_test_processed.isna().any().any():
            raise ValueError("Processed test data contains missing values.")
    
        if list(X_train_processed.columns) != list(X_test_processed.columns):
            raise ValueError("Processed training and test schemas differ.")
    
        return PreparedData(
            X_train=X_train_processed,
            X_test=X_test_processed,
            y_train=y_train.copy(),
            y_test=y_test.copy(),
            preprocessor=preprocessor,
            numeric_features=numeric_features,
            categorical_features=categorical_features
        )
    
    
    def main() -> None:
        prepared = prepare_train_test_data(
            file_path="customer_training_data.csv",
            target_column="purchased",
            test_size=0.25,
            random_state=42
        )
    
        print("Training shape:", prepared.X_train.shape)
        print("Test shape:", prepared.X_test.shape)
        print("Numeric features:", prepared.numeric_features)
        print("Categorical features:", prepared.categorical_features)
        print("\nProcessed training preview:")
        print(prepared.X_train.head())
    
        # Persist the fitted preprocessor for later inference.
        joblib.dump(prepared.preprocessor, "customer_preprocessor.joblib")
        print("\nSaved fitted preprocessor: customer_preprocessor.joblib")
    
    
    if __name__ == "__main__":
        main()

    19. Install the Persistence Dependency

    The complete implementation uses joblib to save the fitted preprocessing object. It is normally installed as a dependency of scikit-learn, but it can also be installed explicitly.

    python -m pip install joblib

    20. Run the Script

    python train_only_preprocessing.py

    The script should print training and test shapes, detected feature groups and a preview of the processed training data. It also saves the fitted preprocessor as customer_preprocessor.joblib.

    21. Transform New Unseen Data

    Production or future records must pass through the same deterministic feature-building logic and the same fitted preprocessor. Do not fit the preprocessor again.

    import joblib
    import pandas as pd
    
    # Load the fitted training-only preprocessor.
    preprocessor = joblib.load("customer_preprocessor.joblib")
    
    new_customers = pd.DataFrame({
        "customer_id": [201, 202],
        "age": [33, None],
        "monthly_income": [55000, 47000],
        "city": ["Kolkata", "Bengaluru"],
        "signup_date": ["2026-04-01", "2026-04-03"]
    })
    
    new_customers = clean_structure(new_customers)
    new_features = build_model_features(new_customers)
    new_array = preprocessor.transform(new_features)
    
    new_processed = pd.DataFrame(
        new_array,
        columns=preprocessor.get_feature_names_out(),
        index=new_features.index
    )
    
    print(new_processed)
    Deployment requirement: Save the feature-building code, fitted preprocessor and trained model as one versioned pipeline or release unit. Changing a preprocessing rule without retraining the corresponding model can invalidate predictions.

    22. Prefer a Full Model Pipeline

    When a model is added, combine preprocessing and the estimator in one scikit-learn Pipeline. Calling fit() on the pipeline fits preprocessing and the model using training data. Calling predict() automatically reuses the fitted transformations.

    from sklearn.linear_model import LogisticRegression
    from sklearn.pipeline import Pipeline
    
    model_pipeline = Pipeline(steps=[
        ("preprocessor", build_preprocessor(
            numeric_features=numeric_features,
            categorical_features=categorical_features
        )),
        ("model", LogisticRegression(max_iter=1000))
    ])
    
    model_pipeline.fit(X_train_features, y_train)
    predictions = model_pipeline.predict(X_test_features)
    
    print(predictions)

    23. Cross-Validation Without Leakage

    For cross-validation, pass the complete pipeline to the validation function. scikit-learn then fits a separate preprocessor within each training fold and transforms that fold's validation subset with the corresponding fold-fitted object.

    from sklearn.model_selection import StratifiedKFold, cross_val_score
    
    cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
    
    scores = cross_val_score(
        model_pipeline,
        X_train_features,
        y_train,
        cv=cv,
        scoring="accuracy"
    )
    
    print("Fold scores:", scores)
    print("Mean accuracy:", scores.mean())
    Incorrect cross-validation design Preprocess the complete training dataset once, then cross-validate a model on the already-transformed result. This allows each validation fold to influence preprocessing statistics.
    Correct cross-validation design Put preprocessing and the estimator inside one pipeline and cross-validate the pipeline.

    24. Train-Only Statistical Derivation

    Suppose a numerical feature contains training values \(x_1, x_2, \ldots, x_m\). A training mean is:

    TRAINING MEAN
    \[ \mu_{train}=\frac{1}{m}\sum_{i=1}^{m}x_i \]

    Standardization of a training or test value uses parameters learned from training data:

    TRAIN-ONLY STANDARDIZATION
    \[ z=\frac{x-\mu_{train}}{\sigma_{train}} \]

    The test subset must not contribute to \(\mu_{train}\), \(\sigma_{train}\), a training median or a category vocabulary.

    25. Missing-Value Strategy Is a Modeling Decision

    The median and most-frequent strategies used in this implementation are reasonable baselines, not universal answers. Choose a strategy based on the source and meaning of missingness.

    SituationPossible strategyRisk or consideration
    Numeric feature with moderate skewTraining medianReduces sensitivity to extreme values but can compress variation.
    Numeric absence may carry meaningImputation plus missing indicatorThe indicator can preserve the fact that a value was absent.
    Categorical value missingMost frequent or explicit "Missing" categoryMost frequent can hide a meaningful absence.
    Column mostly missingInvestigate, redesign or dropA hard percentage threshold should be based on domain value, not habit.
    Time-dependent dataPast-only or causal imputationFuture observations must not influence earlier records.

    26. Time-Based Data Requires a Different Split

    Random train-test splitting is not appropriate when the objective is to predict the future from the past. For time-dependent records, sort by time and reserve the latest period for validation or testing.

    sorted_df = clean_df.sort_values("signup_date").reset_index(drop=True)
    
    split_index = int(len(sorted_df) * 0.80)
    train_df = sorted_df.iloc[:split_index].copy()
    test_df = sorted_df.iloc[split_index:].copy()
    
    X_train = train_df.drop(columns=[TARGET_COLUMN])
    y_train = train_df[TARGET_COLUMN]
    
    X_test = test_df.drop(columns=[TARGET_COLUMN])
    y_test = test_df[TARGET_COLUMN]
    Design assumption: Choose random, grouped or time-based splitting according to how the model will be used. A customer, patient, device or account appearing in both training and test sets can also cause leakage when multiple rows belong to the same entity.

    27. Common Implementation Errors

    ProblemCorrection
    Fill missing values before splittingSplit first and fit the imputer on training data.
    Run fit_transform() on both train and testUse fit_transform() for train and transform() for test.
    Treat IDs as ordinary numeric featuresDrop them unless their inclusion has a defensible meaning.
    Ignore malformed numeric stringsUse pd.to_numeric(..., errors="coerce") and report the resulting missing values.
    Parse dates without handling failuresUse pd.to_datetime(..., errors="coerce") and inspect NaT counts.
    Allow unseen categories to crash inferenceUse OneHotEncoder(handle_unknown="ignore").
    Save only the trained modelPersist the matching fitted preprocessor or the complete model pipeline.
    Change preprocessing after trainingVersion preprocessing and model artifacts together.

    28. Production Checklist

    Before Using the Pipeline

    • Confirm the CSV delimiter, encoding, decimal convention and missing-value tokens.
    • Validate required columns and target values.
    • Separate identifiers from predictive variables.
    • Choose random, grouped or time-based splitting according to deployment.
    • Fit imputers, encoders, scalers and selectors only on training data.
    • Use the same fitted transformations for evaluation and production inference.
    • Verify that processed training and test columns match in name and order.
    • Check that no missing or infinite values remain unless the model supports them.
    • Persist and version the fitted pipeline with the trained model.
    • Monitor production data for schema changes, unseen categories and distribution drift.

    29. Guided Practice Tasks

    Extend the Implementation

    • Add a column containing values such as "₹45,000" and write a deterministic currency-cleaning rule.
    • Add a categorical value that appears only in test data and verify that transformation still succeeds.
    • Inspect the exact medians learned from training data and compare them with full-dataset medians.
    • Replace random splitting with a date-based split and document why it fits the use case.
    • Add a LogisticRegression estimator and evaluate accuracy, precision, recall and F1 score.
    • Save the complete fitted model pipeline with joblib and reload it for prediction.
    • Add automated checks for allowed ranges, duplicate business keys and schema drift.

    30. Final Workflow Summary

    1

    Load

    Validate the path, file type, rows and column names.

    2

    Inspect

    Review shape, types, sample values, missingness and duplicates.

    3

    Clean Deterministically

    Normalize names, parse known types and standardize text without learning statistics.

    4

    Split

    Create training and test subsets before fitting statistical preprocessing.

    5

    Fit on Training Data

    Learn medians, category levels and scaling parameters from X_train only.

    6

    Transform and Validate

    Reuse the fitted object on train, test and future data, then verify schema and missingness.

    Key Takeaway

    A reliable preprocessing workflow does more than clean a CSV. It protects evaluation integrity. Perform deterministic structural cleaning, split the data, fit learned preprocessing rules on the training subset only, reuse the fitted transformer everywhere else, validate the resulting schema and persist the preprocessor with the model.

    Get Neural Networks and Deep Learning updates by email

    Short, practical lessons and course updates from Rumman Ansari. No spam.