Python for Actuarial Students · Course 3
Lesson 5

Unsupervised Learning & Risk Group Discovery

No target and no labels: turning 180,000 health encounters into member profiles, then letting K-means find the patient archetypes.

Lesson 5 · OverviewBy the end of this lesson you can…

  • Contrast clustering with regression: what “right” means without a target
  • Aggregate multi-encounter data into one behavioral profile per member
  • Explain the K-means algorithm, its distance metric, and the curse of dimensionality
  • Transform and scale features so distances mean something
  • Choose k with the elbow method and silhouette scores
  • Translate clusters into business language with z-score heatmaps and radar charts
75 min
Concepts & worked examples
75 min
Lab: patient archetypes

Companion notebook: lesson-05-clustering.ipynb — worked examples and self-checking exercises.

Part 1

Learning Without a Target

What clustering is for, and what it can’t do.

TheoryClustering vs regression

Regression (Lessons 1–4)Clustering (this lesson)
InputFeatures and a targetFeatures only
GoalPredict the targetGroup similar rows
“Correct” meansLow error on new dataGroups that are compact, separated, and useful
ValidationHoldout scoreInternal indices + business judgment
OutputA number per rowA label per row + group profiles

Why cluster members at all?

  • Care-management programs need target populations
  • Underwriters need a small number of recognizable risk groups
  • Pricing can use cluster membership as a rating variable

K-means will always return k clusters, even from pure noise. Finding groups is easy; finding groups that are real and meaningful is the work.

Part 2

Feature Engineering for Clustering

From encounters to one row per member.

AggregationMany encounters → one static profile

enc = pd.read_parquet("data/health/encounters.parquet")
mem = pd.read_csv("data/health/members.csv")
e = enc[enc["service_date"].dt.year == 2024]

counts = e.pivot_table(index="member_id",
                       columns="service_category",
                       values="encounter_id",
                       aggfunc="count", fill_value=0)
counts.columns = [c.lower().replace(" ", "_")
                  for c in counts.columns]
prof = counts.reindex(mem["member_id"], fill_value=0)

prof["total_allowed"] = (e.groupby("member_id")["allowed"]
                          .sum().reindex(prof.index, fill_value=0))
prof["active_months"] = (e.assign(m=e["service_date"].dt.month)
                          .groupby("member_id")["m"].nunique()
                          .reindex(prof.index, fill_value=0))

180,472 encounters in 2024, 22.6 per member, become an 8,000 × 10 table:

FeatureMeanMedianMax
office_visit3.30233
pharmacy12.734133
emergency0.39015
inpatient0.1007
total_allowed9,9521,714421,889
active_months7.07612

reindex keeps members with no encounters: they’re a real (healthy) group, not missing data.

Transform & scaleMake distances mean something

from sklearn.preprocessing import StandardScaler
X = StandardScaler().fit_transform(np.log1p(prof))
  1. log1p tames the skew: one member with 133 fills shouldn’t define the pharmacy axis
  2. Standardize so each feature has mean 0, sd 1. Otherwise total_allowed (sd $27,718) outweighs everything measured in counts
PreparationCluster sizesAgreement with truth (ARI)
Raw values7,501 · 420 · 790.178
Standardized5,004 · 2,438 · 5580.918
log1p + standardized4,868 · 2,501 · 6310.926

ARI (adjusted Rand index) compares two labelings: 1 = identical, 0 = chance. We can compute it only because this book was simulated from known archetypes; real data never gives you this.

Part 3

K-Means

Centroids, distances, and dimensions.

AlgorithmLloyd’s algorithm

$$\min_{C_1,\dots,C_k}\;\sum_{j=1}^{k}\sum_{i\in C_j}\lVert \mathbf{x}_i - \boldsymbol{\mu}_j\rVert^2$$

  1. Place k centroids (k-means++: spread out at random)
  2. Assign each member to the nearest centroid (Euclidean distance)
  3. Update each centroid to the mean of its members
  4. Repeat 2–3 until nothing moves
from sklearn.cluster import KMeans
km = KMeans(n_clusters=3, n_init=10, random_state=0).fit(X)
km.labels_          # cluster of each member
km.cluster_centers_ # 3 × 10, in standardized units
km.inertia_         # within-cluster sum of squares: 37,097

Each run can land in a different local minimum. n_init=10 runs ten starts and keeps the best; random_state makes it reproducible. Cluster numbers are arbitrary, so name clusters by their profile, never by their label.

DimensionalityThe curse of dimensionality

The ratio of farthest to nearest neighbor distance, minus one, falls from over 1000 in one dimension to 0.16 in 500 dimensions

500 random points; how much farther is the farthest than the nearest?

2 features22×
10 features1.6×
100 features0.40×
500 features0.16×

When every point is about equally far from every other, “nearest centroid” loses meaning. Keep clustering features few and purposeful, or reduce dimension first (e.g. principal components).

Choosing kElbow and silhouette

Within-cluster sum of squares falls with k with an elbow at 3; mean silhouette is 0.41 at k=2, 0.39 at k=3, then drops to about 0.2

Elbow: inertia 47,844 → 37,097 → 33,628 for k = 2, 3, 4. The big gain stops after 3.

Silhouette (−1 to 1): how much closer each member is to its own cluster than to the next. 0.408 at k = 2, 0.393 at k = 3, then ≈ 0.21.

The silhouette slightly prefers 2; the elbow and the business prefer 3. Splitting off the 8% of members who drive 60% of cost is worth it. Metrics guide k; they don’t choose it.

Part 4

Translating Clusters

From centroids to archetypes a care manager recognizes.

HeatmapCentroids as z-scores

Heatmap of cluster centroid z-scores: Healthy is below average on every feature, Chronic-Maintenance is high on pharmacy, lab and active months, Heavy-Use is very high on emergency, inpatient, imaging and outpatient surgery

Healthy: below average on everything; active only 4 months a year.

Chronic-Maintenance: steady pharmacy and lab, 11 active months, but no more ER or inpatient use than average.

Heavy-Use: +2 sd on emergency, inpatient, imaging, and surgery.

ProfilesRadar chart and the numbers behind it

Radar chart of the three cluster profiles across ten features
Mean per memberHealthyChronicHeavy
Members4,8682,501631
Pharmacy fills2.426.238.6
Specialist visits0.32.89.1
Emergency visits0.090.332.98
Inpatient stays0.010.051.04
Active months4.311.311.9
Allowed, 20241,4199,89276,017
Share of members61%31%8%
Share of cost9%31%60%

ValidationAre the clusters real and useful?

Against the hidden archetypes (answer key)

True ↓ / cluster →HealthyChronicHeavy
Healthy4,833730
Chronic-Maintenance352,31928
Heavy-Use0109603

96.9% of members sit on the diagonal; ARI 0.926. The errors sit at the borders: heavy users having a quiet year look chronic.

Against next year’s cost (real-world test)

Cluster (2024)Mean 2024Mean 2025Median 2025
Healthy1,4193,812875
Chronic9,89215,8947,787
Heavy-Use76,01747,06225,333

The groups still separate cost a year later, but they drift toward the middle: regression to the mean. Some heavy users recover; some healthy members get sick. Never price next year off this year’s extremes.

Actuarial Lab · 75 minDiscovering patient archetypes

  1. Build the 2024 member profile: eight category counts, total allowed, active months
  2. log1p-transform and standardize
  3. Run K-means for k = 2…10; plot inertia and silhouette
  4. Fit k = 3; name the clusters from their profiles
  5. Draw the z-score heatmap and radar chart
  6. Tabulate members, cost, and cost share by cluster
  7. Check against archetypes.csv (ARI) and against 2025 cost
  8. Repeat without the log, and without scaling: what changes?

Answer key

Inertia k = 337,097
Silhouette k = 2 · 30.408 · 0.393
Cluster sizes4,868 · 2,501 · 631
Heavy-Use share of cost60%
ARI vs archetypes0.926

Save the labels: Lesson 6 turns them into rules.

RecapLesson 5 checklist

  • I know what clustering can and can’t tell me
  • I aggregate encounters into one profile per member, keeping the zeros
  • I log-transform skewed counts and standardize before K-means
  • I understand why distances fail in high dimensions
  • I choose k with the elbow, silhouette, and business sense
  • I name clusters by their profiles
  • I explain clusters with heatmaps, radar charts, and cost shares
  • I validate clusters against future outcomes
Up next
Lesson 6

Rule Extraction & Interpretability of Unsupervised Models

Centroids don’t fit in an underwriting manual. Surrogate decision trees turn clusters into a handful of readable rules.