Bivariate Analysis¶
Bivariate analysis examines the relationship between two variables. Does spend increase with tenure? Do orders from certain regions have higher cancellation rates? Are weekends busier than weekdays? These are bivariate questions — and the answers drive business decisions.
Learning Objectives¶
- Analyse relationships between two numeric variables (scatter plots, correlation)
- Analyse relationships between a numeric and categorical variable (grouped statistics, box plots)
- Analyse relationships between two categorical variables (crosstab, heatmap)
- Avoid the correlation/causation confusion
Numeric vs Numeric — Scatter Plot¶
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
df = pd.read_csv("orders_clean.csv")
df["total"] = df["quantity"] * df["unit_price"]
# Basic scatter plot
fig, ax = plt.subplots(figsize=(8, 5))
ax.scatter(df["unit_price"], df["total"], alpha=0.4, color="#0D9488", s=20)
ax.set_xlabel("Unit Price (£)")
ax.set_ylabel("Order Total (£)")
ax.set_title("Unit Price vs Order Total")
plt.tight_layout()
plt.show()
# Seaborn with regression line
sns.regplot(data=df, x="unit_price", y="total", scatter_kws={"alpha": 0.3})
plt.title("Unit Price vs Order Total with Trend Line")
plt.show()
Numeric vs Categorical — Grouped Statistics¶
# Summary stats by category
category_stats = (
df.groupby("category")["total"]
.agg(["count", "mean", "median", "std"])
.round(2)
.sort_values("median", ascending=False)
)
print(category_stats)
# Box plots — compare distribution across groups
fig, ax = plt.subplots(figsize=(10, 5))
df.boxplot(column="total", by="category", ax=ax)
ax.set_title("Order Total by Category")
ax.set_xlabel("Category")
ax.set_ylabel("Total (£)")
plt.suptitle("") # remove pandas auto-title
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
# Seaborn violin plot — shows full distribution shape
plt.figure(figsize=(12, 5))
sns.violinplot(data=df, x="category", y="total", palette="teal")
plt.title("Order Total Distribution by Category")
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
Statistical test — is the difference significant?¶
from scipy import stats
# Are Electronics orders significantly higher value than Books orders?
electronics = df[df["category"] == "Electronics"]["total"]
books = df[df["category"] == "Books"]["total"]
t_stat, p_value = stats.ttest_ind(electronics, books)
print(f"t-statistic: {t_stat:.3f}")
print(f"p-value: {p_value:.4f}")
print(f"Significant at 5% level: {p_value < 0.05}")
Categorical vs Categorical — Crosstab¶
# Cross-tabulation: status × category
crosstab = pd.crosstab(df["category"], df["status"])
print(crosstab)
# Normalized version: row percentages
crosstab_pct = pd.crosstab(df["category"], df["status"], normalize="index").round(3) * 100
print(crosstab_pct)
# Visualise as heatmap
plt.figure(figsize=(10, 6))
sns.heatmap(crosstab_pct, annot=True, fmt=".1f", cmap="YlOrRd",
linewidths=0.5, cbar_kws={"label": "% of Row"})
plt.title("Status Distribution by Category (%)")
plt.tight_layout()
plt.show()
Sample output:
| category | cancelled | completed | pending |
|---|---|---|---|
| Books | 8.2 | 67.3 | 24.5 |
| Electronics | 15.1 | 58.3 | 26.6 |
| Furniture | 5.0 | 72.5 | 22.5 |
Insight: Electronics has a higher cancellation rate than Furniture — worth investigating.
Time vs Numeric — Trend Analysis Preview¶
# Daily revenue trend
daily_rev = (
df[df["status"] == "completed"]
.groupby("order_date")["total"]
.sum()
)
plt.figure(figsize=(14, 5))
plt.plot(daily_rev.index, daily_rev.values, color="#0D9488", linewidth=1.5, alpha=0.8)
plt.fill_between(daily_rev.index, daily_rev.values, alpha=0.15, color="#0D9488")
plt.title("Daily Revenue Trend")
plt.xlabel("Date")
plt.ylabel("Revenue (£)")
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Seaborn Pair Plot — All Numeric Pairs at Once¶
numeric_cols = ["quantity", "unit_price", "total"]
pair_df = df[numeric_cols + ["category"]].dropna()
g = sns.pairplot(
pair_df,
hue="category",
vars=numeric_cols,
plot_kws={"alpha": 0.4},
diag_kind="kde",
)
g.fig.suptitle("Pairwise Relationships", y=1.02)
plt.show()
Correlation vs Causation¶
Correlation does not imply causation
Two variables moving together doesn't mean one causes the other. Ice cream sales and drowning rates are correlated — because both increase in hot weather. Always ask: is there a third variable driving both?
Common types of spurious correlations in business data: - Seasonality: revenue and marketing spend both rise in November — but December spend caused December revenue, not September spend - Confounders: VIP customers have higher order totals AND higher engagement — but the high engagement might be because of the high spend, or both might be caused by their product type - Reverse causation: "High spend correlates with longer customer tenure" — did high spend cause them to stay, or did long tenure lead to high spend?
Practice Exercises¶
Warm-up
1. Create a scatter plot of quantity vs total.
2. Create a box plot of total grouped by status.
3. Create a crosstab of category vs status and show the row percentages.
Main
4. For each category, calculate mean, median, and std of total. Which category has the highest variability (std)?
5. Create a heatmap showing cancellation rate by category and by month.
6. Use sns.regplot to show the relationship between unit_price and total. Does a higher unit price always mean higher order total?
Stretch
7. Run a t-test to determine if the mean order value for weekends is significantly different from weekdays. Interpret the p-value.
8. Build a pair plot for all numeric columns, coloured by category. Identify one surprising relationship and write a 2-sentence interpretation.
Interview Questions¶
[Beginner] What is a scatter plot and what does it show?
[Mid-level] You find a strong positive correlation (r = 0.82) between marketing spend and revenue. A colleague says "increasing marketing spend will increase revenue." What's missing from this reasoning?
[Senior] How would you determine whether the relationship between two variables is truly causal rather than merely correlated?