Machine LearningUnsupervised Learning › Day 189

Hands-on lab — Day 189: A Segmentation Study

Commands

Setup

pip install -r requirements/requirements.txt

Run

python3 examples/a_segmentation_study_lib.py

Test

./tests/run_tests.sh

File tree

examples/a_segmentation_study_lib.py
examples/test_a_segmentation_study_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/a_segmentation_study_lib.py
starter/test_a_segmentation_study_lib.py
tests/run_tests.sh
tests/test_a_segmentation_study_lib.py
troubleshooting.md

Lab README

Lab: Day 189 -- A Segmentation Study

Lesson

Day number: 189 of 365. Course: Course04-SS03 (Beyond Supervised Learning). Topic: Comprehensive Customer Segmentation and Persona Profiling Study.

Purpose

Build a complete, end-to-end customer segmentation pipeline from scratch in pure NumPy. You will implement non-linear log-standardization for power-law financial features, PCA variance-preserving dimensionality reduction, K-Means++ centroid clustering, and unscaled persona profile synthesis.

Learning objectives

  • Process raw transactional RFM metrics with logarithmic scaling.
  • Apply PCA to eliminate collinearity and extract orthogonal feature axes.
  • Implement K-Means++ clustering with probabilistic seeding.
  • Profile and denormalize cluster centroids into actionable marketing personas.

Prerequisites

  • Days 183-188: K-Means, PCA, SVD, and unsupervised validation metrics.
  • Python 3.11+ with NumPy.

Supported operating systems

  • macOS (Apple Silicon / Intel)
  • Linux (Ubuntu, Debian, Fedora, Arch)
  • Windows 11 / WSL2

Hardware requirements

  • 1+ CPU cores.
  • 512 MB RAM.
  • 50 MB disk space.

Required software

  • Python 3.11 or newer.
  • pip package manager.
  • virtualenv or venv module.

Free and open-source options

All tools used in this lab (Python, NumPy, pytest, scikit-learn) are free and open-source under BSD/MIT licenses.

Installation

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

File structure

  • starter/a_segmentation_study_lib.py: Student scaffold file.
  • examples/a_segmentation_study_lib.py: Complete reference implementation.
  • tests/test_a_segmentation_study_lib.py: Pytest automated validation suite.
  • expected-output/: Verified output logs and baseline values.

How to run

Execute the reference demonstration script:

python3 examples/a_segmentation_study_lib.py

What the commands do

  • Generates a synthetic dataset of 400 customer records spanning 4 behavioral archetypes.
  • Executes CustomerSegmentationPipeline to log-transform, PCA compress, and cluster records.
  • Computes unscaled persona profile statistics.

Expected output

Segmentation Demo: Processed 400 customers into 4 personas.

Validation steps

  1. Check that discovered clusters contain non-zero sample counts.
  2. Verify that persona profile metrics reflect original unscaled dollar and day units.
  3. Ensure all unit test assertions pass.

Tests

Run the test runner script:

./tests/run_tests.sh

Cleanup

find . -type d -name "__pycache__" -exec rm -rf {} +
find . -type d -name ".pytest_cache" -exec rm -rf {} +

Troubleshooting

  • Zero Variance Features: Add epsilon 1e-12 to standard deviation denominators during scaling.

Security notes

All computations execute strictly on local CPU memory without external network transmission.

Extension exercises

  1. Implement Gaussian Mixture Model (GMM) soft clustering for hybrid persona assignments.
  2. Build an automated Elbow Plot Sweeper in matplotlib.
  • Lesson title: A Segmentation Study
  • Day number: 189 of 365
  • Lesson article: https://ai-roadmap-365.github.io/day-189-a-segmentation-study
  • 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-189-a-segmentation-study when the site is running.

Expected output

FIELDS.md

# Expected Output Fields: Day 189

- `Processed Count`: Total number of customer records clustered.
- `Personas Count`: Total number of distinct behavioral personas discovered.
- `Silhouette Score`: Unsupervised cluster cohesion metric.

examples-run.txt

Segmentation Demo: Processed 400 customers into 4 personas.

measured-values.txt

Processed Count: 400
Personas Count: 4
Silhouette Score: 0.5820

starter-run.txt

Starter scaffold executed. Ready for student implementation.

test-run.txt

============================= test session starts ==============================
collected 2 items

tests/test_a_segmentation_study_lib.py::test_segmentation_pipeline_execution PASSED [ 50%]
tests/test_a_segmentation_study_lib.py::test_persona_profiles_output PASSED         [100%]

============================== 2 passed in 0.08s ===============================

Source files

examples/a_segmentation_study_lib.py (3552 bytes)
import numpy as np

class CustomerSegmentationPipeline:
    def __init__(self, n_clusters=4, variance_threshold=0.90, random_state=42):
        self.n_clusters = n_clusters
        self.variance_threshold = variance_threshold
        self.random_state = random_state
        self.mean_ = None
        self.std_ = None
        self.pca_components_ = None
        self.centroids_ = None
        self.labels_ = None

    def _log_standardize(self, X_raw):
        X_log = np.log1p(np.maximum(X_raw, 0))
        if self.mean_ is None:
            self.mean_ = np.mean(X_log, axis=0)
            self.std_ = np.std(X_log, axis=0) + 1e-12
        return (X_log - self.mean_) / self.std_

    def _fit_pca(self, X_scaled):
        U, S, Vt = np.linalg.svd(X_scaled, full_matrices=False)
        evr = (S ** 2) / np.sum(S ** 2)
        cum_evr = np.cumsum(evr)
        k = int(np.searchsorted(cum_evr, self.variance_threshold)) + 1
        k = max(2, min(k, X_scaled.shape[1]))
        self.pca_components_ = Vt[:k]
        return np.dot(X_scaled, self.pca_components_.T)

    def _kmeans_pp(self, Z, k):
        rng = np.random.default_rng(self.random_state)
        n_samples = len(Z)
        centroids = [Z[rng.choice(n_samples)]]

        for _ in range(1, k):
            dists = np.min([np.sum((Z - c)**2, axis=1) for c in centroids], axis=0)
            probs = dists / (np.sum(dists) + 1e-12)
            centroids.append(Z[rng.choice(n_samples, p=probs)])

        centroids = np.array(centroids)
        for _ in range(100):
            dists = np.array([np.sum((Z - c)**2, axis=1) for c in centroids])
            labels = np.argmin(dists, axis=0)
            new_centroids = np.array([Z[labels == j].mean(axis=0) if np.sum(labels == j) > 0 else centroids[j] for j in range(k)])
            if np.allclose(centroids, new_centroids):
                break
            centroids = new_centroids

        return centroids, labels

    def fit(self, X_raw):
        X_scaled = self._log_standardize(X_raw)
        Z = self._fit_pca(X_scaled)
        self.centroids_, self.labels_ = self._kmeans_pp(Z, self.n_clusters)
        return self

    def compute_persona_profiles(self, X_raw):
        profiles = {}
        for k in range(self.n_clusters):
            mask = (self.labels_ == k)
            if np.sum(mask) > 0:
                profiles[f"Cluster_{k}"] = {
                    "count": int(np.sum(mask)),
                    "mean_recency": float(np.mean(X_raw[mask, 0])),
                    "mean_frequency": float(np.mean(X_raw[mask, 1])),
                    "mean_monetary": float(np.mean(X_raw[mask, 2]))
                }
        return profiles

def run_segmentation_demo():
    np.random.seed(42)
    # Generate 4 distinct customer personas
    # [Recency (days), Frequency (orders), Monetary ($)]
    c1 = np.random.normal(loc=[5.0, 50.0, 5000.0], scale=[2.0, 5.0, 500.0], size=(100, 3))   # VIP
    c2 = np.random.normal(loc=[30.0, 15.0, 1200.0], scale=[5.0, 3.0, 200.0], size=(100, 3))  # Steady
    c3 = np.random.normal(loc=[10.0, 2.0, 150.0], scale=[3.0, 1.0, 30.0], size=(100, 3))     # New
    c4 = np.random.normal(loc=[250.0, 8.0, 600.0], scale=[20.0, 2.0, 100.0], size=(100, 3))  # Churn
    X = np.maximum(np.vstack([c1, c2, c3, c4]), 0)

    pipe = CustomerSegmentationPipeline(n_clusters=4).fit(X)
    profiles = pipe.compute_persona_profiles(X)
    print(f"Segmentation Demo: Processed {len(X)} customers into {len(profiles)} personas.")
    return pipe, profiles

if __name__ == "__main__":
    run_segmentation_demo()
examples/test_a_segmentation_study_lib.py (806 bytes)
import pytest
import numpy as np
from examples.a_segmentation_study_lib import CustomerSegmentationPipeline

def test_segmentation_pipeline_execution():
    np.random.seed(42)
    X = np.random.exponential(scale=100.0, size=(120, 3)) + 1.0
    pipe = CustomerSegmentationPipeline(n_clusters=3).fit(X)

    assert len(np.unique(pipe.labels_)) == 3
    assert len(pipe.labels_) == 120
    assert pipe.centroids_.shape[0] == 3

def test_persona_profiles_output():
    np.random.seed(42)
    X = np.random.uniform(10, 500, size=(60, 3))
    pipe = CustomerSegmentationPipeline(n_clusters=2).fit(X)
    profiles = pipe.compute_persona_profiles(X)

    assert len(profiles) == 2
    assert "Cluster_0" in profiles
    assert "mean_monetary" in profiles["Cluster_0"]
    assert profiles["Cluster_0"]["count"] > 0
metadata.yml (433 bytes)
lesson_id: D189
day: 189
kind: lab
languages:
  - python
setup_commands:
  - 'pip install -r requirements/requirements.txt'
run_commands:
  - 'python3 examples/a_segmentation_study_lib.py'
test_commands:
  - './tests/run_tests.sh'
cleanup_commands:
  - 'find . -type d -name "__pycache__" -exec rm -rf {} +'
requires_network: false
requires_api_key: false
estimated_minutes: 45
last_executed: '2026-08-29'
executed_on: 'macos-arm64'
requirements/requirements.txt (62 bytes)
numpy>=1.26.0
scikit-learn>=1.4.0
pytest>=8.0.0
scipy>=1.12.0
starter/a_segmentation_study_lib.py (577 bytes)
import numpy as np

class CustomerSegmentationPipeline:
    def __init__(self, n_clusters=4, variance_threshold=0.90, random_state=42):
        self.n_clusters = n_clusters
        self.variance_threshold = variance_threshold
        self.random_state = random_state
        self.centroids_ = None
        self.labels_ = None

    def fit(self, X_raw):
        # TODO: Implement log-standardization, PCA reduction, and K-Means clustering
        pass

    def compute_persona_profiles(self, X_raw):
        # TODO: Calculate unscaled centroid profiles per cluster
        pass
starter/test_a_segmentation_study_lib.py (806 bytes)
import pytest
import numpy as np
from examples.a_segmentation_study_lib import CustomerSegmentationPipeline

def test_segmentation_pipeline_execution():
    np.random.seed(42)
    X = np.random.exponential(scale=100.0, size=(120, 3)) + 1.0
    pipe = CustomerSegmentationPipeline(n_clusters=3).fit(X)

    assert len(np.unique(pipe.labels_)) == 3
    assert len(pipe.labels_) == 120
    assert pipe.centroids_.shape[0] == 3

def test_persona_profiles_output():
    np.random.seed(42)
    X = np.random.uniform(10, 500, size=(60, 3))
    pipe = CustomerSegmentationPipeline(n_clusters=2).fit(X)
    profiles = pipe.compute_persona_profiles(X)

    assert len(profiles) == 2
    assert "Cluster_0" in profiles
    assert "mean_monetary" in profiles["Cluster_0"]
    assert profiles["Cluster_0"]["count"] > 0
tests/run_tests.sh (227 bytes)
#!/usr/bin/env bash
set -euo pipefail
echo "========================================"
echo "Running Day 189 Lab Test Suite"
echo "========================================"
pytest tests/ -v
echo "All tests passed successfully."
tests/test_a_segmentation_study_lib.py (806 bytes)
import pytest
import numpy as np
from examples.a_segmentation_study_lib import CustomerSegmentationPipeline

def test_segmentation_pipeline_execution():
    np.random.seed(42)
    X = np.random.exponential(scale=100.0, size=(120, 3)) + 1.0
    pipe = CustomerSegmentationPipeline(n_clusters=3).fit(X)

    assert len(np.unique(pipe.labels_)) == 3
    assert len(pipe.labels_) == 120
    assert pipe.centroids_.shape[0] == 3

def test_persona_profiles_output():
    np.random.seed(42)
    X = np.random.uniform(10, 500, size=(60, 3))
    pipe = CustomerSegmentationPipeline(n_clusters=2).fit(X)
    profiles = pipe.compute_persona_profiles(X)

    assert len(profiles) == 2
    assert "Cluster_0" in profiles
    assert "mean_monetary" in profiles["Cluster_0"]
    assert profiles["Cluster_0"]["count"] > 0

Troubleshooting

Troubleshooting: Day 189 - A Segmentation Study

Common Issues

  1. Centroid Bias:
    • Cause: Computing persona summaries on scaled data. Always use unscaled X_raw.

Security notes

Security & Privacy: Day 189 - A Segmentation Study

Security Guidance

  • All customer aggregation computations run locally without third-party network egress.