Every metric you ever analyze has an underlying distribution. Most analysts never bother to look - and then they're confused when their "average" doesn't match what users actually experience. This lesson is about training your eye.
The four shapes you'll see most
Normal (bell-shaped) - symmetric, single hump. Most data clustered around the mean, tapering off in both directions. Mean = median = mode. Examples: adult height, IQ scores, measurement errors.
Log-normal - right-skewed with a long tail of high values. Mean > median. Mean is dragged up by the tail. Examples: income, house prices, time-to-complete a task, customer lifetime value.
Long-tailed / power-law - extreme right-skew. A tiny fraction of observations account for most of the mass. Examples: city populations, website traffic per page, revenue per customer in B2B.
Bimodal - two distinct humps. Usually a sign you're mixing two populations. Examples: order values from a freemium product (free users near zero, paid users much higher), session durations (bouncers vs engaged users).
Spotting shapes & log transforms in Python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
# Generate right-skewed Customer Lifetime Value (LTV) data
np.random.seed(42)
raw_ltv = np.random.lognormal(mean=4.5, sigma=1.2, size=5000)
df = pd.DataFrame({"ltv": raw_ltv})
# Log-transformation to normalize skewed distributions
df["log_ltv"] = np.log1p(df["ltv"])
print("--- Raw LTV vs Log-Transformed LTV ---")
print(f"Raw LTV: Mean = ${df['ltv'].mean():.2f}, Median = ${df['ltv'].median():.2f}")
print(f"Log LTV: Mean = {df['log_ltv'].mean():.2f}, Std = {df['log_ltv'].std():.2f}")
# Plotting distribution comparison
fig, axes = plt.subplots(1, 2, figsize=(10, 4))
sns.histplot(df["ltv"], kde=True, ax=axes[0], color="crimson")
axes[0].set_title("Raw LTV (Right-Skewed / Log-Normal)")
axes[0].set_xlabel("LTV ($)")
sns.histplot(df["log_ltv"], kde=True, ax=axes[1], color="teal")
axes[1].set_title("log1p(LTV) (Transformed to Bell Shape)")
axes[1].set_xlabel("Log(LTV + 1)")
plt.tight_layout()
# plt.show()
Why the shape changes which stats you trust
| Shape | Use | Avoid |
|---|---|---|
| Normal | Mean + std | Nothing - both work |
| Log-normal | Median + percentiles, or log-transform then use mean | Raw mean (it lies) |
| Long-tailed | Percentiles only (p50, p90, p99) | Mean, std (both useless) |
| Bimodal | Split groups, analyze separately | Any single summary statistic |
Takeaway
Before you summarize any metric:
- Plot its histogram (
sns.histplot(df['col'], kde=True)). - Identify the shape.
- Pick the summary statistic that fits the shape.
Two lines of Python save you from a meeting where someone says "but the average is fine?!" and you have to explain that the average is meaningless because the distribution is bimodal.