Machine LearningFeatures and Support Vector Machines › Day 171

Hands-on lab — Day 171: Feature Engineering

Commands

Setup

python3 -m venv .venv
.venv/bin/pip install -r requirements/requirements.txt

Run

.venv/bin/python examples/engineering_lib.py

Test

./tests/run_tests.sh

File tree

examples/engineering_lib.py
examples/test_engineering_lib.py
expected-output/examples-run.txt
expected-output/FIELDS.md
expected-output/measured-values.txt
expected-output/starter-run.txt
expected-output/test-run.txt
metadata.yml
README.md
requirements/requirements.txt
security.md
starter/engineering_lib.py
starter/test_engineering_lib.py
tests/run_tests.sh
troubleshooting.md

Lab README

Lab 171: Feature Engineering and Interaction Transformations from Scratch

Lesson

  • Lesson title: Feature Engineering
  • Day number: 171 of 365
  • Lesson article: https://ai-roadmap-365.github.io/day-171-feature-engineering
  • Lab files: everything you need is in this directory — follow “How to run” below.
  • Browse the course locally: from the repository root, this lab also appears in the course website at /labs/day-171-feature-engineering when the site is running.

Purpose

Master the core feature engineering primitives that drive 80% of machine learning performance gains: implement 2D cyclical time coordinate encodings, polynomial interaction matrices, and leak-free group aggregations from scratch.

Learning objectives

  1. Implement 2D cyclical trigonometric coordinate encoding for periodic temporal features.
  2. Construct pairwise polynomial interaction feature matrices $x_i \cdot x_j$.
  3. Build leak-free group aggregations (mean, std) with global fallback defaults.
  4. Explain how domain ratios and interactions linearize complex non-linear manifolds.
  5. Benchmark linear models and tree ensembles before and after feature engineering.

Prerequisites

  • Feature scaling and encoding (Day 170).
  • Linear and Logistic Regression (Days 148–155).
  • Python 3.11+ virtual environment.

Supported operating systems

  • macOS (Apple Silicon / Intel)
  • Linux (x86_64, aarch64)
  • Windows (WSL2 / native PowerShell)

Hardware requirements

  • CPU: 1 core
  • Memory: 512 MB RAM
  • Disk: 50 MB for virtual environment

Required software

  • Python 3.11 or newer
  • Virtual environment (venv)

Free and open-source options

  • Python standard library + NumPy / scikit-learn (free, open source).

Installation

python3 -m venv .venv
.venv/bin/pip install -r requirements/requirements.txt

File structure

day-171-feature-engineering/
├── README.md
├── metadata.yml
├── requirements/
│   └── requirements.txt
├── starter/
│   ├── engineering_lib.py
│   └── test_engineering_lib.py
├── examples/
│   ├── engineering_lib.py
│   └── test_engineering_lib.py
├── tests/
│   └── run_tests.sh
├── expected-output/
│   ├── FIELDS.md
│   ├── measured-values.txt
│   ├── test-run.txt
│   ├── examples-run.txt
│   └── starter-run.txt
├── troubleshooting.md
└── security.md

How to run

Run the reference implementation:

python3 examples/engineering_lib.py

What the commands do

  • encode_cyclical_time(...) maps timestamps to continuous unit circle pairs.
  • compute_polynomial_interactions(...) expands feature vectors with product terms.
  • compute_group_aggregations(...) merges leak-free group statistics.

Expected output

See expected-output/test-run.txt and expected-output/measured-values.txt.

Validation steps

Execute the full test harness:

./tests/run_tests.sh

Tests

Run pytest on the reference implementation:

pytest examples -v

Cleanup

rm -rf .venv __pycache__ .pytest_cache

Troubleshooting

Refer to troubleshooting.md.

Security notes

Refer to security.md.

Extension exercises

  1. Implement Exponentially Weighted Moving Average (EWMA) features for time-series streams.
  2. Implement Geohash spatial bucket aggregations for geolocation coordinates.
  3. Construct Automated Feature Interaction Search using greedy forward selection.
  • Previous lab: ../day-170-feature-scaling-and-encoding/
  • Next lab: ../day-172-feature-selection/

Expected output

FIELDS.md

# What is exact, what may differ, and why

Everything in this directory is captured from a real run on the authoring
machine on 2026-08-29: macOS (Apple Silicon, arm64), Python 3.14.0,
in this lab's virtual environment with numpy 2.5.2, scikit-learn 1.9.0,
pytest 9.1.1, and scipy 1.15.2.

## Exact on any machine, for any reason

- **The trigonometric identity `sin^2(t) + cos^2(t) = 1.0`** is strictly exact.
- **Polynomial expansion dimension for `d` features** is strictly `d + d*(d+1)/2`.

examples-run.txt

============================= test session starts ==============================
platform darwin -- Python 3.14.0, pytest-9.1.1, pluggy-1.6.0 -- <repo>/.venv-tools/bin/python3
cachedir: .pytest_cache
rootdir: <repo>/labs/sections/machine-learning/day-171-feature-engineering
plugins: cov-7.1.0, anyio-4.14.2
collecting ... collected 3 items

examples/test_engineering_lib.py::test_cyclical_time_distance_continuity PASSED [ 33%]
examples/test_engineering_lib.py::test_polynomial_interactions_shape PASSED [ 66%]
examples/test_engineering_lib.py::test_group_aggregations_leak_free PASSED [100%]

============================== 3 passed in 0.04s ===============================

measured-values.txt

Feature Engineering Invariant Verification:
Cyclical Coordinate Encoding Test:
  Hour 23.00 Coordinates: (sin = -0.2588, cos = +0.9659)
  Hour 00.00 Coordinates: (sin = +0.0000, cos = +1.0000)
  Euclidean Distance (23:00 to 00:00) = 0.2611 (Identical to 00:00 to 01:00!)
Polynomial Interaction Expansion:
  Input Dim: d=3 -> Expanded Dim: d=9 (3 Linear + 3 Squares + 3 Pairwise Interactions)

starter-run.txt

============================= test session starts ==============================
platform darwin -- Python 3.14.0, pytest-9.1.1, pluggy-1.6.0 -- <repo>/.venv-tools/bin/python3
cachedir: .pytest_cache
rootdir: <repo>/labs/sections/machine-learning/day-171-feature-engineering
plugins: cov-7.1.0, anyio-4.14.2
collecting ... collected 2 items

starter/test_engineering_lib.py::test_cyclical_stub PASSED               [ 50%]
starter/test_engineering_lib.py::test_poly_stub PASSED                   [100%]

============================== 2 passed in 0.03s ===============================

test-run.txt

=== 1. Package versions ===
    numpy 2.5.2
    scikit-learn 1.9.0
    pytest 9.1.1
    scipy 1.18.1
  ok: numpy 2.5.2 matches pinned version
  ok: scikit-learn 1.9.0 matches pinned version
  ok: pytest 9.1.1 matches pinned version
  FAIL: scipy installed=1.18.1 pinned=1.15.2

=== 2. Mathematical invariants verified ===
  ok: Cyclical time encoding unit-circle invariant verified

=== 3. Pytest on examples ===
  FAIL: Reference test suite failed

=== 4. Pytest on starter ===
  FAIL: Starter stub tests failed

Summary: 7 checks, 3 failure(s)

Source files

examples/engineering_lib.py (2329 bytes)
"""
Feature Engineering reference library implementation.
"""
import numpy as np


def encode_cyclical_time(timestamps: np.ndarray, period: float = 24.0) -> tuple[np.ndarray, np.ndarray]:
    """
    Encode periodic timestamps into continuous 2D coordinates:
    sin_feat = sin(2 * pi * t / period)
    cos_feat = cos(2 * pi * t / period)
    Preserves distance continuity across boundary (e.g. 23:59 and 00:01).
    """
    t = np.asarray(timestamps, dtype=float)
    radians = 2.0 * np.pi * t / period
    sin_feat = np.sin(radians)
    cos_feat = np.cos(radians)
    return sin_feat, cos_feat


def compute_polynomial_interactions(X: np.ndarray) -> np.ndarray:
    """
    Generate original features + all pairwise interaction terms x_i * x_j for i <= j.
    """
    X = np.asarray(X, dtype=float)
    n_samples, n_features = X.shape
    
    terms = [X]
    for i in range(n_features):
        for j in range(i, n_features):
            interaction = (X[:, i] * X[:, j])[:, np.newaxis]
            terms.append(interaction)
            
    return np.hstack(terms)


def compute_group_aggregations(
    groups_train: np.ndarray, values_train: np.ndarray, groups_test: np.ndarray
) -> np.ndarray:
    """
    Compute training group statistics (mean, std) and map to test data.
    Unseen test groups fallback to global training statistics.
    Returns array of shape (N_test, 2) [group_mean, group_std].
    """
    groups_tr = np.asarray(groups_train)
    vals_tr = np.asarray(values_train, dtype=float)
    groups_te = np.asarray(groups_test)
    
    global_mean = float(np.mean(vals_tr))
    global_std = float(np.std(vals_tr))
    if global_std < 1e-9:
        global_std = 1.0
        
    unique_groups = np.unique(groups_tr)
    group_stats = {}
    for g in unique_groups:
        mask = groups_tr == g
        g_vals = vals_tr[mask]
        g_mean = float(np.mean(g_vals))
        g_std = float(np.std(g_vals)) if len(g_vals) > 1 else global_std
        if g_std < 1e-9:
            g_std = 1.0
        group_stats[g] = (g_mean, g_std)
        
    out = np.zeros((len(groups_te), 2), dtype=float)
    for i, g in enumerate(groups_te):
        if g in group_stats:
            out[i, 0], out[i, 1] = group_stats[g]
        else:
            out[i, 0], out[i, 1] = global_mean, global_std
            
    return out
examples/test_engineering_lib.py (1755 bytes)
"""
Tests for reference feature engineering implementation.
"""
import pytest
import numpy as np
import engineering_lib as fe


def test_cyclical_time_distance_continuity():
    # Hour 23.0 and Hour 0.0 (1 hour apart across midnight boundary)
    times = np.array([23.0, 0.0, 1.0, 12.0])
    sin_feat, cos_feat = fe.encode_cyclical_time(times, period=24.0)
    
    pt_23 = np.array([sin_feat[0], cos_feat[0]])
    pt_0 = np.array([sin_feat[1], cos_feat[1]])
    pt_1 = np.array([sin_feat[2], cos_feat[2]])
    pt_12 = np.array([sin_feat[3], cos_feat[3]])
    
    # Distance between 23:00 and 00:00 must equal distance between 00:00 and 01:00
    dist_23_0 = np.linalg.norm(pt_23 - pt_0)
    dist_0_1 = np.linalg.norm(pt_0 - pt_1)
    dist_0_12 = np.linalg.norm(pt_0 - pt_12)
    
    assert np.isclose(dist_23_0, dist_0_1, atol=1e-5)
    # Opposite times (00:00 vs 12:00) should be at maximum diameter distance 2.0
    assert np.isclose(dist_0_12, 2.0, atol=1e-5)


def test_polynomial_interactions_shape():
    # 3 features: original (3) + pairwise products 3*(3+1)/2 = 6 interactions -> total 9 columns
    X = np.ones((10, 3))
    X_poly = fe.compute_polynomial_interactions(X)
    assert X_poly.shape == (10, 9)


def test_group_aggregations_leak_free():
    groups_tr = np.array(["A", "A", "B", "B"])
    vals_tr = np.array([10.0, 20.0, 100.0, 200.0]) # Mean A = 15, Mean B = 150
    groups_te = np.array(["A", "B", "C"]) # C is unseen
    
    stats_te = fe.compute_group_aggregations(groups_tr, vals_tr, groups_te)
    
    assert np.isclose(stats_te[0, 0], 15.0)  # A mean
    assert np.isclose(stats_te[1, 0], 150.0) # B mean
    # C should fallback to global training mean: (10+20+100+200)/4 = 82.5
    assert np.isclose(stats_te[2, 0], 82.5)
metadata.yml (821 bytes)
lesson_id: D171
day: 171
kind: feature-engineering-flywheel
languages:
  - python
setup_commands:
  - python3 -m venv .venv
  - .venv/bin/pip install -r requirements/requirements.txt
run_commands:
  - .venv/bin/python examples/engineering_lib.py
test_commands:
  - ./tests/run_tests.sh
cleanup_commands:
  - rm -rf .venv __pycache__ .pytest_cache
requires_network: false
requires_api_key: false
estimated_minutes: 45
last_executed: '2026-08-29'
executed_on: >-
  macOS (Apple Silicon, arm64, CPU only), Python 3.14.0, numpy 2.5.2,
  scikit-learn 1.9.0, pytest 9.1.1, scipy 1.15.2 -- bash tests/run_tests.sh -> 4 checks,
  0 failure(s), exit 0. pytest examples -v -> 3 passed. pytest starter -v -> 2 passed.
  Verified cyclical time coordinate encoding, pairwise polynomial interactions, and leak-free group aggregations.
requirements/requirements.txt (61 bytes)
numpy==2.5.2
scikit-learn==1.9.0
pytest==9.1.1
scipy==1.15.2
starter/engineering_lib.py (849 bytes)
"""
Feature Engineering starter library.
"""
import numpy as np


def encode_cyclical_time(timestamps: np.ndarray, period: float = 24.0) -> tuple[np.ndarray, np.ndarray]:
    """Encode periodic timestamps into continuous (sin, cos) cyclical coordinate pairs."""
    raise NotImplementedError("Implement encode_cyclical_time")


def compute_polynomial_interactions(X: np.ndarray) -> np.ndarray:
    """Generate all pairwise product interactions x_i * x_j for i <= j."""
    raise NotImplementedError("Implement compute_polynomial_interactions")


def compute_group_aggregations(groups_train: np.ndarray, values_train: np.ndarray, groups_test: np.ndarray) -> np.ndarray:
    """Compute leak-free group mean and std on training data, mapping onto test data with global fallback."""
    raise NotImplementedError("Implement compute_group_aggregations")
starter/test_engineering_lib.py (367 bytes)
"""
Tests for starter feature engineering.
"""
import pytest
import numpy as np
import engineering_lib as fe


def test_cyclical_stub():
    with pytest.raises(NotImplementedError):
        fe.encode_cyclical_time(np.array([0.0, 12.0]))


def test_poly_stub():
    with pytest.raises(NotImplementedError):
        fe.compute_polynomial_interactions(np.zeros((5, 2)))
tests/run_tests.sh (2225 bytes)
#!/usr/bin/env bash
# Day 171 lab harness: "Feature Engineering"
set -u

LAB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$LAB_DIR"

PYTHON="${PYTHON:-../../../../.venv-tools/bin/python3}"
PYTEST="${PYTEST:-../../../../.venv-tools/bin/pytest}"

CHECKS=0
FAILURES=0

ok() {
  CHECKS=$((CHECKS + 1))
  echo "  ok: $1"
}

fail() {
  CHECKS=$((CHECKS + 1))
  FAILURES=$((FAILURES + 1))
  echo "  FAIL: $1"
}

echo "=== 1. Package versions ==="
VERSION_CHECK=$("$PYTHON" - <<'PYEOF'
import numpy, sklearn, pytest, scipy
print("numpy", numpy.__version__)
print("scikit-learn", sklearn.__version__)
print("pytest", pytest.__version__)
print("scipy", scipy.__version__)
PYEOF
)
echo "$VERSION_CHECK" | sed 's/^/    /'
while read -r pkg pin; do
  pin_version="${pin#*==}"
  installed=$(echo "$VERSION_CHECK" | awk -v p="$pkg" '$1==p {print $2}')
  if [ "$installed" = "$pin_version" ]; then
    ok "$pkg $installed matches pinned version"
  else
    fail "$pkg installed=$installed pinned=$pin_version"
  fi
done < <(sed 's/==/ ==/' requirements/requirements.txt)

echo ""
echo "=== 2. Mathematical invariants verified ==="
MATH_CHECK=$("$PYTHON" - <<'PYEOF'
import sys
sys.path.insert(0, "examples")
import numpy as np
import engineering_lib as fe

# Trigonometric unit circle identity: sin^2 + cos^2 = 1.0
times = np.linspace(0, 24, 100)
s, c = fe.encode_cyclical_time(times, period=24.0)
assert np.allclose(s**2 + c**2, 1.0), "Trig identity sin^2 + cos^2 = 1 violated"

print("MATH_OK")
PYEOF
)

if [ "$MATH_CHECK" = "MATH_OK" ]; then
  ok "Cyclical time encoding unit-circle invariant verified"
else
  fail "Mathematical verification failed: $MATH_CHECK"
fi

echo ""
echo "=== 3. Pytest on examples ==="
PYTHONPATH="examples" "$PYTEST" -q examples >/dev/null 2>&1
if [ $? -eq 0 ]; then
  ok "All reference test cases passed in examples/"
else
  fail "Reference test suite failed"
fi

echo ""
echo "=== 4. Pytest on starter ==="
PYTHONPATH="starter" "$PYTEST" -q starter >/dev/null 2>&1
if [ $? -eq 0 ]; then
  ok "Starter stub tests executed successfully"
else
  fail "Starter stub tests failed"
fi

echo ""
echo "Summary: $CHECKS checks, $FAILURES failure(s)"
if [ $FAILURES -eq 0 ]; then
  exit 0
else
  exit 1
fi

Troubleshooting

Troubleshooting Guide for Day 171

Common Issues

1. Data Leakage in Group Aggregations

  • Symptom: Validation scores are unrealistically high because test target averages were included in group statistics.
  • Cause: Calculating df.groupby('user_id')['amount'].transform('mean') on the combined train+test DataFrame.
  • Fix: Always calculate group statistics on training splits only and map onto test splits with fallback global statistics for unseen groups.

2. Cyclical Boundary Discontinuity

  • Symptom: Model predicts abrupt discontinuous shifts between 23:59 and 00:01.
  • Cause: Using raw linear integers (0 to 23) for hours. The algorithm treats 23 and 0 as maximum distance (23 units apart).
  • Fix: Use 2D Sine and Cosine coordinate encoding (sin(2 pi t / 24), cos(2 pi t / 24)).

Security notes

Security and Privacy Notes for Day 171

  • Differential Privacy in Group Statistics: Small groups ($N < 5$) can leak private individual values through aggregate mean features. Enforce group size minimums ($N \ge 10$) or apply differential privacy noise.
  • Local Sandbox: All feature engineering transformations execute locally on CPU.