Intermediate

Statistical Analysis with Matplotlib

Statistical distributions, histograms, boxplots, violin plots and subplot mosaics with Matplotlib.

Prerequisites: Python basics, Matplotlib ≥ 3.6, NumPy ≥ 1.23
Environment: Python ≥ 3.8, Jupyter Notebook


Table of Contents

  1. Course Overview
  2. Core Concepts: Statistical Distributions
  3. Data Generation with NumPy RNG
  4. Histograms with Matplotlib
  5. Boxplots
  6. Violin Plots
  7. Subplots and Subplot Mosaics
  8. Style Sheets and Visual Customization
  9. Mermaid Diagrams
  10. Mathematical Reference Formulas
  11. Reference Tables
  12. 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

ShapeDescriptionExample
Normal (Bell curve)Symmetric, ~68% of data around the meanHuman population height
Left-skewedTail to the left, majority of data on the rightBatteries failing before expected time
Right-skewed (Exponential)Tail to the rightWait times, income
Bimodal / MultimodalTwo or more peaksMix 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

ParameterTypeDescription
binsint, list, strNumber of bins, manual edges, or method ('auto', 'sqrt', 'fd', etc.)
densityboolIf True, displays probability density (y-axis = probability)
cumulativeboolIf True, displays cumulative sum
rangetuple(min, max) — limits the value range
colorstrBar color
rwidthfloatRelative bar width (0 to 1)
histtypestr'bar', 'step', 'stepfilled'
alignstrBar alignment: 'left', 'mid', 'right'
orientationstr'vertical' (default) or 'horizontal'

5. Boxplots

Anatomy of a Boxplot

The boxplot is the visual representation of descriptive statistics:

ElementMeaning
Center markerMean (if showmeans=True)
Center band in the boxMedian (Q2 — 50th percentile)
Lower edge of the boxQ1 (25th percentile)
Upper edge of the boxQ3 (75th percentile)
IQRInterquartile Range = Q3 − Q1
Lower whiskerQ1 − 1.5 × IQR
Upper whiskerQ3 + 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=True is required for the box to accept a facecolor (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:

  • bodiesPolyCollection (iterable)
  • cbars, cmeans, cmedians, cquantiles, cmins, cmaxesLineCollection (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

StyleCharacteristics
'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

CriterionHistogramBoxplotViolin Plot
Primary goalFrequencies and distribution shapeSummary statisticsKDE + summary statistics
Required data1D numerical array1D or 2D (columns)1D or list
Detects outliersPartially (visually)Yes (fliers)Partially (KDE)
Detects bimodalityYesNoYes
Shows densityWith density=TrueNoYes (KDE)
Multivariate comparisonWith histtype='step'NativelyNatively
Customization complexityLowMedium (dicts)High (element dict)
Matplotlib functionplt.hist()plt.boxplot()plt.violinplot()

Common Matplotlib Parameters

ParameterApplicable ToDescription
colorhist, boxplot (props)Main color
figsizeplt.figure()Figure size (width, height)
showmeansboxplot, violinplotShow the mean
showmediansviolinplotShow the median
notchboxplotNotch (CI around median)
vertboxplotTrue=vertical, False=horizontal
patch_artistboxplotFilled box (accepts facecolor)
quantilesviolinplotList of quantiles to display
layoutplt.figure()'constrained' prevents overlaps

NumPy RNG Methods

MethodKey ParametersDescription
rng.integers()low, high, size, endpointRandom integers
rng.normal()loc (mean), scale (std), sizeNormal distribution
rng.exponential()scale (1/λ), sizeExponential distribution
np.concatenate()[arr1, arr2]Combine arrays
np.quantile()arr, qCompute a quantile

Binning Strategies for plt.hist()

bins ValueMethodUse Cases
intManualExact control
'auto'Max of Sturges/FDGeneral use
'fd'Freedman-DiaconisData with outliers
'sqrt'Square rootModerately-sized datasets
'sturges'SturgesSmall normal datasets
'rice'RiceLarge datasets
'scott'ScottNormal 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
  1. Generate / load data with numpy.random.default_rng(seed=...) for reproducibility
  2. Initial exploration with pd.Series(x).describe() — descriptive statistics
  3. Histogram for overall shape and distribution type choice
  4. Boxplot for quartiles, outliers, and multi-series comparisons
  5. Violin plot for density and bimodality detection
  6. Subplot mosaic for comparative dashboards
  7. Style sheet for consistent and accessible presentation

Best Practices

  • Always use a seed for reproducibility of generated data
  • Test multiple binning strategies (histogram) before choosing
  • Prefer patch_artist=True for filled boxplots
  • Use layout='constrained' in plt.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

Interested in this course?

Contact us to book it or get a custom training plan for your team.