MODELCATALOG

Data & preparation

Data & preparation

Data preparation

Models need repeatable preparation, quality checks and feature engineering before training.

Inputs

raw dataset + schema/label rules

Outputs

cleaned/versioned train, validation and test datasets

Core explanation

I separate data preparation from model training so transformations are reproducible and independently operationalized.

01

Mental model

What this means before worrying about syntax.

Raw data commonly lands in S3, is processed and validated, and the resulting training-ready data becomes input to a training job.

  • A reproducible training dataset is a lifecycle asset, not just a notebook dataframe.
  • Data quality and leakage checks precede model optimization.
  • Train/validation/test splits must preserve the intended production decision context.
Lifecycle position
S3 raw -> Processing/ETL -> train/validation/test -> training
02

Implementation anatomy

The sequence you would actually build or configure.

  1. 1

    Land/version raw data in S3.

  2. 2

    Profile schema, nulls, duplicates, class balance and leakage.

  3. 3

    Run deterministic cleaning/feature engineering.

  4. 4

    Split data reproducibly.

  5. 5

    Persist outputs and transformation lineage.

Objects / terms worth remembering
S3Processing JobData WranglerGlue (optional)
03

Concrete example

A real model-development or production flow.

1

Raw complaint_id, text, product, timestamp, resolution_code

↓
2

Remove invalid labels and normalize text

↓
3

Keep resolution_code as target only

↓
4

Stratify train/validation split

↓
5

Write versioned S3 prefixes

04

Implementation lab

Concrete code, data manipulation and configuration for this lifecycle step.

01

1. Load and profile raw complaint data

python

Always inspect schema, nulls, duplicate keys and target balance before fitting anything.

import pandas as pd

df = pd.read_csv("bank_customer_complaints_raw.csv")

print(df.shape)
print(df.dtypes)
print(df.isna().sum().sort_values(ascending=False).head(15))
print("duplicate complaint_id:", df["complaint_id"].duplicated().sum())
print(df["category"].value_counts(dropna=False))
02

2. Clean text and remove unusable rows

python

The exact cleaning policy is domain-specific. The important implementation point is that target fields such as resolution/category never leak into the feature matrix.

import re
import pandas as pd

def clean_text(value: str) -> str:
    value = "" if pd.isna(value) else str(value)
    value = value.lower()
    value = re.sub(r"https?://\S+|www\.\S+", " ", value)
    value = re.sub(r"\S+@\S+", " ", value)
    value = re.sub(r"[^a-z0-9\s']", " ", value)
    value = re.sub(r"\s+", " ", value).strip()
    return value

clean = (
    df.drop_duplicates(subset=["complaint_id"])
      .dropna(subset=["complaint_text", "category"])
      .copy()
)

clean["clean_text"] = clean["complaint_text"].map(clean_text)
clean = clean[clean["clean_text"].str.len() >= 10]

# Target-only fields must not enter X.
X = clean["clean_text"]
y = clean["category"]
03

3. Reproducible train / validation / test split

python

For time-sensitive models, replace random splitting with a time-based split to prevent future-to-past leakage.

from sklearn.model_selection import train_test_split

# First reserve an untouched 15% test set.
X_dev, X_test, y_dev, y_test = train_test_split(
    X, y,
    test_size=0.15,
    random_state=42,
    stratify=y,
)

# Split remaining 85% into train and validation.
X_train, X_val, y_train, y_val = train_test_split(
    X_dev, y_dev,
    test_size=0.1765,   # ~15% of original total
    random_state=42,
    stratify=y_dev,
)

print(len(X_train), len(X_val), len(X_test))
print(y_train.value_counts(normalize=True).round(3))
04

4. Write versioned split files

python

Persist the split. Do not regenerate a different random split every time you retrain the same candidate.

from pathlib import Path
import pandas as pd

out = Path("prepared/v2026_08_17")
for part in ("train", "validation", "test"):
    (out / part).mkdir(parents=True, exist_ok=True)

pd.DataFrame({"text": X_train, "label": y_train}).to_csv(
    out / "train/data.csv", index=False
)
pd.DataFrame({"text": X_val, "label": y_val}).to_csv(
    out / "validation/data.csv", index=False
)
pd.DataFrame({"text": X_test, "label": y_test}).to_csv(
    out / "test/data.csv", index=False
)
05

5. Run the cleaning script as a SageMaker Processing job

python

SageMaker V3 keeps Processing under sagemaker.core.processing. The cleaning/splitting logic remains ordinary Python running in the processing container.

from sagemaker.core.processing import ScriptProcessor
from sagemaker.core.shapes import ProcessingInput, ProcessingOutput

processor = ScriptProcessor(
    role=SAGEMAKER_ROLE_ARN,
    image_uri=PROCESSING_IMAGE_URI,
    command=["python3"],
    instance_count=1,
    instance_type="ml.m5.xlarge",
    base_job_name="complaint-prep",
)

processor.run(
    code="src/preprocess.py",
    inputs=[
        ProcessingInput(
            input_name="raw",
            source=RAW_S3_URI,
            destination="/opt/ml/processing/input"
        )
    ],
    outputs=[
        ProcessingOutput(
            output_name="prepared",
            source="/opt/ml/processing/output",
            destination=PREPARED_S3_URI,
        )
    ],
    arguments=["--seed", "42"],
)

Design decisions that matter

  • Processing job vs upstream data-engineering pipeline.
  • Random vs time-based split.
  • Which transformations must also exist at inference.

Common failure modes

  • Fitting TF-IDF on the full dataset leaks validation/test information.
  • Random split can leak future behavior in time-dependent use cases.
  • Schema drift can break code before statistical drift is visible.

Production readiness check

Items to close before treating this step as production-ready.

✓ schema✓ nulls✓ duplicates✓ class balance✓ leakage✓ PII handling✓ snapshot/version