Data & preparation
Data preparation
Models need repeatable preparation, quality checks and feature engineering before training.
raw dataset + schema/label rules
cleaned/versioned train, validation and test datasets
I separate data preparation from model training so transformations are reproducible and independently operationalized.
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.
S3 raw -> Processing/ETL -> train/validation/test -> training
Implementation anatomy
The sequence you would actually build or configure.
- 1
Land/version raw data in S3.
- 2
Profile schema, nulls, duplicates, class balance and leakage.
- 3
Run deterministic cleaning/feature engineering.
- 4
Split data reproducibly.
- 5
Persist outputs and transformation lineage.
Concrete example
A real model-development or production flow.
Raw complaint_id, text, product, timestamp, resolution_code
Remove invalid labels and normalize text
Keep resolution_code as target only
Stratify train/validation split
Write versioned S3 prefixes
Implementation lab
Concrete code, data manipulation and configuration for this lifecycle step.
1. Load and profile raw complaint data
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))
2. Clean text and remove unusable rows
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"]
3. Reproducible train / validation / test split
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))
4. Write versioned split files
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
)
5. Run the cleaning script as a SageMaker Processing job
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.