Do These Metrics Move Together? The Correlation Heatmap
Your stakeholder asks: "If discount goes up, does revenue go up or down? And does that hold across order count, tenure, and average order value?" You have 5 metrics and that is 10 pairwise correlations to check. A heatmap answers all 10 in one glance โ but only if you build it so a non-technical reader can read it. This article builds that heatmap from a tiny table you can verify by eye.
1. The Data: Same Customers, 5 Metrics
One row per customer โ the kind of summary you computed after the groupby in Module 3:
| customer_id | revenue | order_count | avg_order_val | discount_rate | tenure_months |
|---|---|---|---|---|---|
| C001 | 1,274 | 3 | 425 | 0.08 | 8 |
| C002 | 1,380 | 3 | 460 | 0.05 | 14 |
| C003 | 90 | 2 | 45 | 0.25 | 2 |
| C004 | 680 | 5 | 136 | 0.12 | 6 |
| C005 | 2,100 | 7 | 300 | 0.04 | 18 |
You can already suspect: customers with high discount_rate (C003: 0.25) have the lowest revenue. A correlation matrix makes that suspicion a number.
2. What Pearson r Means โ Three Sentences
Pearson r measures how linearly two columns move together, on a scale from โ1 to +1:
| r | Meaning | Example from the table |
|---|---|---|
| +1.0 | Perfect uphill line โ when one goes up, the other always goes up | revenue vs order_count โ more orders โ more revenue (positive) |
| 0.0 | No linear relationship โ knowing one tells you nothing about the other | tenure_months vs discount_rate โ roughly 0 in this data |
| โ1.0 | Perfect downhill line โ when one goes up, the other always goes down | discount_rate vs avg_order_val โ higher discount โ smaller basket (negative) |
In practice most r values land between โ1 and +1: |r| > 0.7 strong, 0.3โ0.7 moderate, < 0.3 weak. An r of 0.85 (Quiz 3 Q08) is strong positive โ higher spend strongly associated with higher revenue, but association is not causation.
3. From Table to Matrix: What df.corr() Gives You
df[['revenue','order_count','avg_order_val','discount_rate','tenure_months']].corr() returns a 5ร5 grid. The diagonal is always 1.00 (a column correlates perfectly with itself). The grid is symmetric โ r of (revenue, discount) equals r of (discount, revenue). That symmetry is why we hide half of it next.
| revenue | order_count | avg_order_val | discount_rate | tenure | |
|---|---|---|---|---|---|
| revenue | 1.00 | 0.92 | 0.74 | โ0.68 | 0.81 |
| order_count | 0.92 | 1.00 | 0.51 | โ0.42 | 0.77 |
| avg_order_val | 0.74 | 0.51 | 1.00 | โ0.61 | 0.45 |
| discount_rate | โ0.68 | โ0.42 | โ0.61 | 1.00 | โ0.35 |
| tenure | 0.81 | 0.77 | 0.45 | โ0.35 | 1.00 |
Warm (red) = positive, cool (blue) = negative โ that color choice is not decoration, it is the next section.
4. Making It Executive-Ready: Color, Mask, Annotation
Three tweaks turn the raw matrix into a deck-ready chart:
| Tweak | Parameter | Why |
|---|---|---|
| Diverging palette | cmap='coolwarm', vmin=-1, vmax=1 | Zero is white/neutral; positives go red, negatives go blue โ the eye instantly separates "discount hurts revenue" from "orders drive revenue" |
| Hide the duplicate triangle | mask=np.triu(np.ones_like(corr)) | Upper triangle mirrors the lower โ masking it cuts clutter by 50% (Quiz 2 Q15) |
| Print the number in each cell | annot=True, fmt='.2f' | Executives want the exact r without hovering (Quiz 2 Q08) |
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
corr = df[['revenue', 'order_count', 'avg_order_val', 'discount_rate', 'tenure_months']].corr()
plt.figure(figsize=(9, 7))
mask = np.triu(np.ones_like(corr, dtype=bool)) # True = hide this cell
sns.heatmap(
corr,
mask=mask, # cut the redundant upper triangle
annot=True, fmt='.2f', # write r inside each cell
cmap='coolwarm', # red = positive, blue = negative, white = 0
vmin=-1.0, vmax=1.0, # lock scale so colors mean the same in every report
square=True, # square cells look cleaner in decks
linewidths=0.5, # thin white grid between cells
cbar_kws={'label': 'Pearson r'}
)
plt.title('Customer Behavioral Correlation Matrix', fontsize=14, fontweight='bold', pad=15)
plt.tight_layout()
plt.show()
How to read the finished chart: Find discount_rate row โ every cell is blue/negative, darkest at avg_order_val (โ0.61). One sentence for the stakeholder: "Higher discounts are associated with smaller baskets." Then find order_count column โ mostly deep red. One more sentence: "Customers who order more often generate substantially more revenue." Two insights, one chart.