Prerequisites: Python basics, Matplotlib ≥ 3.6, NumPy ≥ 1.23
Environment: Python ≥ 3.8, Jupyter Notebook
Table of Contents
- Course Overview
- Core Concepts: Statistical Distributions
- Data Generation with NumPy RNG
- Histograms with Matplotlib
- Boxplots
- Violin Plots
- Subplots and Subplot Mosaics
- Style Sheets and Visual Customization
- Mermaid Diagrams
- Mathematical Reference Formulas
- Reference Tables
- Summary and Best Practices
1. Course Overview
This course uses Matplotlib to explore statistical distributions in numerical datasets. The three core chart types are:
- Histogram — value frequencies within intervals (bins)
- Boxplot — visual representation of descriptive statistics (summary statistics)
- Violin plot — boxplot enriched with a Gaussian kernel density estimate (KDE)
Training data is generated with NumPy’s random number generator (RNG), ensuring reproducible samples via the seed parameter.
2. Core Concepts: Statistical Distributions
What Is a Distribution?
A distribution shows the frequencies of measurements or counts in a variable. Data may come from:
- a controlled experiment (student heights, battery lifetimes)
- natural events (number of cars in a parking lot)
Typical Distribution Shapes
| Shape | Description | Example |
|---|---|---|
| Normal (Bell curve) | Symmetric, ~68% of data around the mean | Human population height |
| Left-skewed | Tail to the left, majority of data on the right | Batteries failing before expected time |
| Right-skewed (Exponential) | Tail to the right | Wait times, income |
| Bimodal / Multimodal | Two or more peaks | Mix of two populations |
Why Use Visualizations?
- Initial overview of data structure
- Indication of the general shape of the distribution
- Effective communication in an analysis project
- Starting point before formal statistical tests
3. Data Generation with NumPy RNG
RNG (Random Number Generator) Principle
NumPy’s default_rng class returns a generator object. Passing a seed makes results reproducible.
import numpy as np
# Create generator object with reproducible seed
rng = np.random.default_rng(seed=112)
print(rng) # Generator(PCG64)
Main Generator Methods
rng = np.random.default_rng(seed=112)
# Single random integer
x = rng.integers(low=1, high=10)
print(x, x.dtype, type(x), sep='\n')
# Integer with inclusive high
x_inclusive = rng.integers(low=1, high=10, endpoint=True)
# Array of 15 random integers
arr = rng.integers(low=1, high=10, size=15)
# 3 arrays of 15 integers each (wide format)
matrix = rng.integers(low=1, high=10, size=(3, 15))
print(matrix[1]) # Access second array
Normal Distribution
rng = np.random.default_rng(112)
# loc = mean, scale = std, size = number of observations
x = rng.normal(loc=150, scale=10, size=500)
Exponential Distribution
rng = np.random.default_rng(seed=112)
z = rng.exponential(scale=1, size=500)
Bimodal Distribution (combination)
rng = np.random.default_rng(seed=112)
m1 = rng.normal(loc=150, scale=10, size=500)
m2 = rng.normal(loc=230, scale=5, size=500)
mX = np.concatenate([m1, m2]) # Bimodal dataset
4. Histograms with Matplotlib
Concept
The histogram differs from a bar plot:
- Built on a single numerical array
- Divides the range into equal, non-overlapping intervals (bins)
- Counts the number of instances in each bin (frequency)
- The overall shape reveals the distribution
Key rule: too few bins = hidden pattern; too many bins = granular plot.
Basic Histogram
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(112)
x = rng.normal(loc=150, scale=10, size=500)
# Minimal histogram
plt.hist(x)
plt.show()
Display and Closing
# Explicit display
plt.figure()
plt.hist(x)
plt.show()
# Close without displaying (avoids inline display)
plt.hist(x)
plt.close()
Accessing Return Values
frequency_counts = plt.hist(x)[0] # Counts per bin
bin_edges = plt.hist(x)[1] # Bin edges
plt.close()
print('Frequencies:', frequency_counts)
print('Bin edges: ', bin_edges)
Key plt.hist() Parameters
# Fixed number of bins
plt.hist(x, bins=15)
plt.show()
# Built-in bin estimation methods
plt.hist(x, bins='sqrt') # Square root method
plt.hist(x, bins='auto') # Automatic method
# Probability density histogram
plt.hist(x, density=True)
# Cumulative histogram
plt.hist(x, cumulative=True)
# Cumulative density
plt.hist(x, density=True, cumulative=True)
# Subset by range
plt.hist(x, bins=5, range=(x.mean() - x.std(), x.mean() + x.std()))
plt.xlim(x.min(), x.max())
plt.show()
# Visual customization
plt.hist(x, color='steelblue', rwidth=0.8)
plt.show()
Multivariate Histogram
rng = np.random.default_rng(seed=112)
y = rng.normal(loc=150, scale=10, size=(2, 500))
plt.hist(y[0], histtype='step', color='navy')
plt.hist(y[1], histtype='step', color='orange')
plt.show()
plt.hist() Parameter Table
| Parameter | Type | Description |
|---|---|---|
bins | int, list, str | Number of bins, manual edges, or method ('auto', 'sqrt', 'fd', etc.) |
density | bool | If True, displays probability density (y-axis = probability) |
cumulative | bool | If True, displays cumulative sum |
range | tuple | (min, max) — limits the value range |
color | str | Bar color |
rwidth | float | Relative bar width (0 to 1) |
histtype | str | 'bar', 'step', 'stepfilled' |
align | str | Bar alignment: 'left', 'mid', 'right' |
orientation | str | 'vertical' (default) or 'horizontal' |
5. Boxplots
Anatomy of a Boxplot
The boxplot is the visual representation of descriptive statistics:
| Element | Meaning |
|---|---|
| Center marker | Mean (if showmeans=True) |
| Center band in the box | Median (Q2 — 50th percentile) |
| Lower edge of the box | Q1 (25th percentile) |
| Upper edge of the box | Q3 (75th percentile) |
| IQR | Interquartile Range = Q3 − Q1 |
| Lower whisker | Q1 − 1.5 × IQR |
| Upper whisker | Q3 + 1.5 × IQR |
| Isolated points (fliers) | Outliers (values outside the whiskers) |
Descriptive Statistics with Pandas and NumPy
import pandas as pd
import numpy as np
rng = np.random.default_rng(112)
x = rng.normal(loc=150, scale=10, size=500)
# With Pandas
pd.Series(x).describe()
# With NumPy only
count = len(x)
mean = x.mean()
std = x.std()
q1 = np.quantile(x, 0.25)
q2 = np.quantile(x, 0.5) # median
q3 = np.quantile(x, 0.75)
print(f'mean={mean:.3f}, std={std:.3f}, Q1={q1:.3f}, Q2={q2:.3f}, Q3={q3:.3f}')
Basic Boxplot
plt.boxplot(x, showmeans=True)
plt.show()
# Adjust whisker length (IQR multiplier)
plt.boxplot(x, showmeans=True, whis=2)
plt.show()
Notched Boxplot (Confidence Intervals)
# Single notched boxplot
plt.boxplot(z, notch=True, showmeans=True)
plt.show()
# Three series compared (wide format: columns = variables)
rng = np.random.default_rng(seed=112)
x3 = rng.integers(low=1, high=10, size=(50, 3))
labs = list('abc')
plt.figure(figsize=(10, 6))
plt.boxplot(x3, labels=labs, notch=True)
plt.show()
Rule: if the notches (confidence intervals) don’t overlap, the medians are considered significantly different.
Boxplot with Pandas DataFrame
x3DF = pd.DataFrame(x3, columns=labs)
x3DF.boxplot(notch=True)
Mean Line Instead of a Marker
plt.boxplot(z, meanline=True, showmeans=True)
plt.show()
Advanced Visual Customization
flierprops = dict(marker='^', markerfacecolor='silver', markeredgecolor='black')
medianprops = dict(linestyle='--', color='teal', linewidth=1.5)
meanprops = dict(linestyle='-.', color='darkorange', linewidth=1.5)
plt.figure(figsize=(6, 5))
plt.boxplot(z, meanline=True, showmeans=True,
flierprops=flierprops,
medianprops=medianprops,
meanprops=meanprops)
plt.show()
Hiding Outliers
# Option A: replaces marker with a space (outliers present but invisible)
plt.boxplot(z, sym='')
plt.show()
# Option B: completely excludes outliers (shortened Y-axis)
plt.boxplot(z, showfliers=False)
plt.show()
Horizontal Boxplot with Full Customization
boxprops = dict(facecolor='lightcyan', edgecolor='lightcyan')
lineprops = dict(color='slategrey', linestyle='dotted')
meanprops = dict(markeredgecolor='black', marker='x')
medianprops = dict(color='magenta', linestyle='dashed')
plt.boxplot(x3, labels=labs, patch_artist=True,
boxprops=boxprops,
whiskerprops=lineprops,
capprops=lineprops,
meanprops=meanprops,
medianprops=medianprops,
vert=False, # horizontal orientation
showmeans=True)
plt.show()
Note:
patch_artist=Trueis required for the box to accept afacecolor(otherwise it’s a line element).
6. Violin Plots
Concept
The violin plot combines the advantages of the boxplot with the Gaussian kernel density estimate (KDE):
- The further the curve extends from the central axis, the higher the probability of a random value at that point
- Shows the distribution shape (symmetric, asymmetric, bimodal)
- Excels where the boxplot fails: bimodal and multimodal distributions
Basic Violin Plot
# Normal distribution
plt.violinplot(x, showmeans=True, showmedians=True)
plt.show()
# Exponential distribution (asymmetric)
plt.violinplot(z, showmeans=True, showmedians=True)
plt.show()
Where the Violin Plot Outperforms the Boxplot: Bimodal Distribution
rng = np.random.default_rng(seed=112)
m1 = rng.normal(loc=150, scale=10, size=500)
m2 = rng.normal(loc=230, scale=5, size=500)
mX = np.concatenate([m1, m2])
# The boxplot doesn't reveal bimodality
plt.boxplot(mX, showmeans=True)
plt.show()
# The violin plot clearly reveals the two modes
plt.violinplot(mX, showmeans=True, showmedians=True)
plt.show()
# Confirmed by histogram
plt.hist(mX, bins='sqrt')
plt.show()
Violin Plot with Quantiles
plt.violinplot(mX, showmeans=True, showmedians=True, quantiles=[0.25, 0.75])
plt.show()
Customizing the Violin Plot
The violin plot returns a dictionary of graphical elements:
bodies→PolyCollection(iterable)cbars,cmeans,cmedians,cquantiles,cmins,cmaxes→LineCollection(non-iterable)
violin = plt.violinplot(mX, showmeans=True, showmedians=True, quantiles=[0.25, 0.75])
# Modify bodies (PolyCollection — iterable)
for pc in violin['bodies']:
pc.set_facecolor('yellowgreen')
pc.set_edgecolor('darkolivegreen')
pc.set_alpha(0.8)
# Modify lines (LineCollection — non-iterable)
violin['cbars'].set_linestyle('dotted')
violin['cmeans'].set_linestyle('dashed')
violin['cmeans'].set_color('firebrick')
violin['cquantiles'].set_color('firebrick')
violin['cmins'].set_linewidth(0) # Hide min
violin['cmaxes'].set_linewidth(0) # Hide max
plt.show()
7. Subplots and Subplot Mosaics
Why Subplots?
A single chart is rarely sufficient to communicate a dataset in depth. The three chart types (histogram, boxplot, violin plot) complement each other and are often displayed side by side.
Subplot Mosaic — Named-Axis System
# Step 1: Create the figure
fig = plt.figure(layout='constrained')
# Step 2: Define the layout with descriptive keys
mosaic = fig.subplot_mosaic([['boxplot', 'violinplot']])
# Step 3: Create charts and assign to named axes
mosaic['boxplot'].boxplot(x)
mosaic['violinplot'].violinplot(x, showmeans=True, showmedians=True)
plt.show()
Vertical Layout (Stacked)
fig = plt.figure(layout='constrained')
# Two nested lists = two rows
mosaic = fig.subplot_mosaic([['boxplot'],
['violinplot']])
mosaic['boxplot'].boxplot(x)
mosaic['violinplot'].violinplot(x, showmeans=True, showmedians=True)
plt.show()
Dashboard with 3 Charts
fig = plt.figure(layout='constrained')
mosaic = fig.subplot_mosaic([['histogram', 'boxplot'],
['histogram', 'violinplot']])
mosaic['histogram'].hist(mX, bins='sqrt')
mosaic['boxplot'].boxplot(mX, showmeans=True)
mosaic['violinplot'].violinplot(mX, showmeans=True, showmedians=True)
plt.show()
8. Style Sheets and Visual Customization
Listing Available Style Sheets
print(plt.style.available)
Applying a Style Sheet
plt.style.use('Solarize_Light2')
# Style applies to all subsequent charts
plt.hist(x, bins=15)
plt.show()
plt.boxplot(x3)
plt.show()
Resetting Styles
# IMPORTANT: reset before changing style
import matplotlib as mpl
mpl.rcdefaults()
Tip: blue/orange/gray palettes are the most accessible for people with color vision deficiencies.
Useful Style Sheets
| Style | Characteristics |
|---|---|
'Solarize_Light2' | Light background, soft colors, visible grid |
'ggplot' | Inspired by R ggplot2, gray background, white grid |
'seaborn-v0_8' | Seaborn palette, clean background |
'dark_background' | Black background, vivid colors |
'fivethirtyeight' | Journalistic style, distinctive colors |
'bmh' | Bayesian Methods for Hackers |
9. Mermaid Diagrams
Statistical Analysis Pipeline with Matplotlib
flowchart TD
A[Raw data / Dataset] --> B[NumPy RNG generation\nor file loading]
B --> C{Analysis type}
C --> D[Univariate\ndistribution]
C --> E[Multivariate\ncomparison]
D --> F[Histogram\nplt.hist]
D --> G[Boxplot\nplt.boxplot]
D --> H[Violin plot\nplt.violinplot]
E --> I[Side-by-side\nhistograms]
E --> J[Multiple\nboxplots]
E --> K[Subplot Mosaic\nDashboard]
F --> L[Distribution\ninterpretation]
G --> L
H --> L
I --> L
J --> L
K --> L
L --> M{Hypotheses\nconfirmed?}
M -- No --> N[Formal statistical\ntests]
M -- Yes --> O[Report / Communication]
Statistical Distribution Types
flowchart LR
A[Distribution] --> B[Normal\nbell curve]
A --> C[Skewed]
A --> D[Bimodal /\nMultimodal]
B --> B1["mean ≈ median\nsymmetry ~68% ±1σ"]
C --> C1[Left-skewed\ntail to the left]
C --> C2[Right-skewed\ntail to the right]
D --> D1["two peaks\n(two populations)"]
B1 --> E[Boxplot\nadequate]
C1 --> E
C2 --> E
D1 --> F[Violin plot\nnecessary]
10. Mathematical Reference Formulas
Mean
$$\bar{x} = \frac{1}{n} \sum_{i=1}^{n} x_i$$
Variance
$$\sigma^2 = \frac{1}{n} \sum_{i=1}^{n} (x_i - \bar{x})^2$$
Standard Deviation
$$\sigma = \sqrt{\frac{1}{n} \sum_{i=1}^{n} (x_i - \bar{x})^2}$$
Quartiles and IQR (Interquartile Range)
$$IQR = Q_3 - Q_1$$
Boxplot whiskers:
$$\text{Whisker}{lower} = Q_1 - 1.5 \times IQR \qquad \text{Whisker}{upper} = Q_3 + 1.5 \times IQR$$
Normal Distribution
The probability density function:
$$f(x) = \frac{1}{\sigma\sqrt{2\pi}} \exp\left(-\frac{(x-\mu)^2}{2\sigma^2}\right)$$
Property: approximately $68%$ of data lies within $[\mu - \sigma,\ \mu + \sigma]$.
The 68-95-99.7 Rule (Empirical Rule)
$$P(\mu - \sigma \leq X \leq \mu + \sigma) \approx 68.27%$$ $$P(\mu - 2\sigma \leq X \leq \mu + 2\sigma) \approx 95.45%$$ $$P(\mu - 3\sigma \leq X \leq \mu + 3\sigma) \approx 99.73%$$
Exponential Distribution
$$f(x; \lambda) = \lambda e^{-\lambda x}, \quad x \geq 0$$
With scale = 1/λ in NumPy: rng.exponential(scale=1/λ, size=n)
Pearson Correlation
$$r = \frac{\sum_{i=1}^{n}(x_i - \bar{x})(y_i - \bar{y})}{\sqrt{\sum_{i=1}^{n}(x_i - \bar{x})^2 \cdot \sum_{i=1}^{n}(y_i - \bar{y})^2}}$$
Values: $r \in [-1, 1]$ — close to $\pm 1$ = strong correlation, close to $0$ = weak correlation.
11. Reference Tables
Comparison of the Three Statistical Chart Types
| Criterion | Histogram | Boxplot | Violin Plot |
|---|---|---|---|
| Primary goal | Frequencies and distribution shape | Summary statistics | KDE + summary statistics |
| Required data | 1D numerical array | 1D or 2D (columns) | 1D or list |
| Detects outliers | Partially (visually) | Yes (fliers) | Partially (KDE) |
| Detects bimodality | Yes | No | Yes |
| Shows density | With density=True | No | Yes (KDE) |
| Multivariate comparison | With histtype='step' | Natively | Natively |
| Customization complexity | Low | Medium (dicts) | High (element dict) |
| Matplotlib function | plt.hist() | plt.boxplot() | plt.violinplot() |
Common Matplotlib Parameters
| Parameter | Applicable To | Description |
|---|---|---|
color | hist, boxplot (props) | Main color |
figsize | plt.figure() | Figure size (width, height) |
showmeans | boxplot, violinplot | Show the mean |
showmedians | violinplot | Show the median |
notch | boxplot | Notch (CI around median) |
vert | boxplot | True=vertical, False=horizontal |
patch_artist | boxplot | Filled box (accepts facecolor) |
quantiles | violinplot | List of quantiles to display |
layout | plt.figure() | 'constrained' prevents overlaps |
NumPy RNG Methods
| Method | Key Parameters | Description |
|---|---|---|
rng.integers() | low, high, size, endpoint | Random integers |
rng.normal() | loc (mean), scale (std), size | Normal distribution |
rng.exponential() | scale (1/λ), size | Exponential distribution |
np.concatenate() | [arr1, arr2] | Combine arrays |
np.quantile() | arr, q | Compute a quantile |
Binning Strategies for plt.hist()
bins Value | Method | Use Cases |
|---|---|---|
int | Manual | Exact control |
'auto' | Max of Sturges/FD | General use |
'fd' | Freedman-Diaconis | Data with outliers |
'sqrt' | Square root | Moderately-sized datasets |
'sturges' | Sturges | Small normal datasets |
'rice' | Rice | Large datasets |
'scott' | Scott | Normal distributions |
12. Summary and Best Practices
What Each Chart Reveals
Histogram:
- Distribution shape (symmetric, asymmetric, plateaus)
- Presence of peaks and gaps in the data
- Relative frequencies with
density=True
Boxplot:
- Box symmetry or asymmetry
- Relative whisker length
- Outlier trend (unilateral or bilateral)
- Approximation between mean and median
Violin Plot:
- Complete variability with KDE
- Reliable detection of bimodal / multimodal distributions
- Density comparison between groups
Recommended Workflow
- Generate / load data with
numpy.random.default_rng(seed=...)for reproducibility - Initial exploration with
pd.Series(x).describe()— descriptive statistics - Histogram for overall shape and distribution type choice
- Boxplot for quartiles, outliers, and multi-series comparisons
- Violin plot for density and bimodality detection
- Subplot mosaic for comparative dashboards
- Style sheet for consistent and accessible presentation
Best Practices
- Always use a
seedfor reproducibility of generated data - Test multiple binning strategies (histogram) before choosing
- Prefer
patch_artist=Truefor filled boxplots - Use
layout='constrained'inplt.figure()to avoid overlaps - Blue/orange/gray palettes are the most accessible (colorblindness)
- Reset with
mpl.rcdefaults()before changing style sheets - For bimodal data, always complement the boxplot with a violin plot or histogram
Typical Initial Setup
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
import pandas as pd
# Version check
import sys
print('Python:', sys.version)
print('Matplotlib:', mpl.__version__)
print('NumPy:', np.__version__)
# Reproducible RNG setup
rng = np.random.default_rng(seed=112)
# Global style
plt.style.use('Solarize_Light2')
Complete Example: Statistical Analysis Dashboard
import numpy as np
import matplotlib.pyplot as plt
rng = np.random.default_rng(seed=112)
# Bimodal data
m1 = rng.normal(loc=150, scale=10, size=500)
m2 = rng.normal(loc=230, scale=5, size=500)
bimodal_data = np.concatenate([m1, m2])
# Normal data for comparison
normal_data = rng.normal(loc=190, scale=15, size=500)
# 2x2 dashboard
fig = plt.figure(figsize=(12, 8), layout='constrained')
mosaic = fig.subplot_mosaic([['hist_normal', 'hist_bimodal'],
['box_compare', 'violin_compare']])
mosaic['hist_normal'].hist(normal_data, bins='auto', color='steelblue')
mosaic['hist_normal'].set_title('Normal Distribution - Histogram')
mosaic['hist_bimodal'].hist(bimodal_data, bins='sqrt', color='darkorange')
mosaic['hist_bimodal'].set_title('Bimodal Distribution - Histogram')
mosaic['box_compare'].boxplot([normal_data, bimodal_data],
labels=['Normal', 'Bimodal'], showmeans=True)
mosaic['box_compare'].set_title('Boxplot Comparison')
mosaic['violin_compare'].violinplot([normal_data, bimodal_data],
showmeans=True, showmedians=True)
mosaic['violin_compare'].set_title('Violin Plot Comparison')
mosaic['violin_compare'].set_xticks([1, 2])
mosaic['violin_compare'].set_xticklabels(['Normal', 'Bimodal'])
plt.suptitle('Statistical Analysis Dashboard', fontsize=14)
plt.show()
Search Terms
statistical · analysis · matplotlib · python · foundations · data · engineering · analytics · distribution · boxplot · violin · plot · style · customization · numpy · plt.hist · rng · sheets · bimodal · chart · concept · dashboard · exponential · generator