Bivariate Analysis
The first 'does one thing move with another?'
Two variables, one question: does X move with Y? Cross-tabs for categories, scatter/grouped-box for numerics, correlation and segment-mixed means point to the hypotheses worth testing.
What you will learn
- Build cross-tabs for category pairs
- Read scatter, grouped box and correlation together
- Spot segment-mixed relationships and confounding cues
What is this?
Bivariate analysis pairs variables: crosstab for two categoricals, grouped box/bar for a numeric-by-category, scatter for two numerics, and Pearson correlation as a scalar summary. The recurring trap is the confound — a third variable that actually drives both, which bivariate pairs can only hint at.
Why do you need it?
- The sharpest analysis questions are relational: does plan change spend?
- A confound seen early ('older users churn more' hides 'legacy plan') redirects the analysis.
- Crosstabs and grouped summaries are the raw material of every dashboard segment.
- Correlation, honestly read, is the pre-causation checkpoint.
Mental model
Marriage records vs states studied together
Bivariate analysis
Looking at marriage rate by state and temperature by the same states suggests two maps that 'appear' related — until you notice population density moves both. Bivariate charts show the apparent link; the confound lurks one dimension deeper.
How it works
- pd.crosstab(df.a, df.b) tabulates category co-occurrences.
- df.groupby(cat)[num].agg(['mean','median']) summarizes a numeric per segment.
- Scatter (df.plot.scatter x, y) shows direction, shape and outliers together.
- Grouped box plots compare segment distributions tail-to-tail.
- df[num_cols].corr() gives the numeric correlation matrix in one call.
- Stratify: re-slice by a likely confounder and watch the relationship change.
Question → view → readout.
Examples
Numeric-by-category, then stratify
import numpy as np, pandas as pddf = pd.DataFrame({ "plan": ["basic", "pro"] * 50, "tenure": np.random.default_rng(6).integers(1, 60, 100),})df["spend"] = df["plan"].map({"basic": 20, "pro": 60}) + df["tenure"] * 0.5 + np.random.default_rng(6).normal(0, 5, 100)print(df.groupby("plan")["spend"].agg(["mean", "median"]).round(1))print(df[["tenure", "spend"]].corr().round(3))Grouped summaries show plan differences; the correlation with tenure hints a confound worth stratifying.
In production
- A support team crosstabs ticket priority by region and finds the SLA rule drives both.
- A marketing BI slice shows spend-by-channel, then by user's plan to expose channel-plan confounds.
- A retention analysis scatters days-since-last-login vs churn probability segmented by plan tier.
Best practices
- Always pair the crosstab with row/column percentages (normalize=True) — counts alone deceive on unbalanced groups.
- Add the confounder stratify step before claiming any relationship.
- Correlation is a readout, not a conclusion — anchor it to the scatter.
- Keep segment sample sizes visible; tiny cells look like big effects.
Common mistakes
- Confusing correlation with causation straight out of the scatter.
- Crosstabs with raw counts hiding wildly different group sizes.
- Ignoring the confound that reverses the relationship when stratified.
- Reporting high correlation from a handful of outliers.
Debugging tips
- A correlation flips sign when segmented → it was confounded; report both layers.
- Scatter with a tight vertical line → a factor or rounding dominates; fix scale first.
- Grouped means close but spreads huge → report medians and IQRs too.
- When a cross-tab cell is disproportionate, break it by the third variable.
Interview checkpoint
Hands-on lab
- Build grouped means and a crosstab
- Read correlation with scatter intuition
- Stratify by a confounder and compare
Profile Plan vs Spend with Stratification
30 min · beginner
Quick quiz
A high correlation between X and Y most often means:
Stratifying by a third variable can change the direction of an apparent relationship.