Drag the degrees-of-freedom slider below. The vermillion dashed curve is the fixed standard Normal N(0,1) you met in this chapter. The blue solid curve (with circular markers) is a Student’s t-distribution, the close cousin of the Normal you will use for inference in later chapters. At df=1 the t-curve sits lower in the middle and has noticeably heavier tails -- it expects extreme values more often than the Normal does. As you raise the degrees of freedom through 2, 5, 10, 30 and finally ∞, the tails thin, the peak rises, and the t-curve converges onto the Normal until the two are indistinguishable.
import numpy as np
import plotly.graph_objects as go
from IPython.display import HTML
# Okabe-Ito colorblind-safe palette. We also distinguish the two curves by LINE
# STYLE and MARKERS, never by color alone: the Normal is a dashed line, the t is
# a solid line carrying circular markers -- so the picture reads correctly in
# grayscale and for colorblind viewers.
OI_BLUE = "#0072B2" # the Student's t curve (slider-controlled)
OI_VERMILION = "#D55E00" # the fixed standard-Normal reference curve
# Fully deterministic figure: the curves below are analytic pdfs, not data.
# Seed set only for parity with the CLT generator template.
rng = np.random.default_rng(2200)
# --- pdf backends: SciPy if present, else a self-contained numpy fallback -----
try:
from scipy import stats
def normal_pdf(x):
return stats.norm.pdf(x) # standard Normal N(0, 1)
def t_pdf(x, df):
return stats.t.pdf(x, df) # Student's t with df dof
BACKEND = "scipy.stats"
except ModuleNotFoundError:
from math import lgamma, log, pi
def normal_pdf(x):
# f(x) = (1/sqrt(2*pi)) * exp(-x^2 / 2)
return np.exp(-0.5 * x**2) / np.sqrt(2.0 * pi)
def t_pdf(x, df):
# f(x) = Gamma((df+1)/2) / [sqrt(df*pi) * Gamma(df/2)]
# * (1 + x^2/df)^(-(df+1)/2)
# Compute the normalizing constant in log-space (lgamma) so it stays
# stable even for very large df, where the raw Gammas would overflow.
log_c = lgamma((df + 1.0) / 2.0) - lgamma(df / 2.0) - 0.5 * log(df * pi)
return np.exp(log_c) * (1.0 + x**2 / df) ** (-(df + 1.0) / 2.0)
BACKEND = "numpy-fallback"
# The "infinity" stop is the limiting case t -> Normal, so we draw the Normal pdf
# there; every finite df uses the Student's t pdf.
def curve_for_df(x, df):
return normal_pdf(x) if np.isinf(df) else t_pdf(x, df)
# Slider stops: df in {1, 2, 5, 10, 30, infinity(=Normal)}.
df_values = [1.0, 2.0, 5.0, 10.0, 30.0, np.inf]
df_labels = ["1", "2", "5", "10", "30", "infinity (= Normal)"]
# Fixed x-grid. A dense grid for the smooth dashed reference; a coarser grid for
# the t-curve so its circular markers are individually visible (not a solid band).
x_ref = np.linspace(-4.5, 4.5, 401)
x_t = np.linspace(-4.5, 4.5, 49)
fig = go.Figure()
# Trace 0: the FIXED standard-Normal reference -- always visible, dashed.
fig.add_trace(go.Scatter(
x=x_ref, y=normal_pdf(x_ref), mode="lines",
name="Standard Normal N(0, 1)",
line=dict(color=OI_VERMILION, width=2.6, dash="dash"),
hovertemplate="z = %{x:.2f}<br>Normal density = %{y:.3f}<extra></extra>",
))
# Traces 1..6: one Student's t pdf per df stop; only df = 1 visible at load.
for k, (df, lab) in enumerate(zip(df_values, df_labels)):
fig.add_trace(go.Scatter(
x=x_t, y=curve_for_df(x_t, df), mode="lines+markers",
name=f"Student's t (df = {lab})", visible=(k == 0),
line=dict(color=OI_BLUE, width=2.4),
marker=dict(symbol="circle", size=5, color=OI_BLUE),
hovertemplate="t = %{x:.2f}<br>t density = %{y:.3f}<extra></extra>",
))
# Slider: keep the reference (trace 0) always on; show exactly one t-curve.
steps = []
for k, lab in enumerate(df_labels):
visible = [True] + [j == k for j in range(len(df_values))]
steps.append(dict(
method="update", label=lab,
args=[{"visible": visible},
{"title.text": "Student's t vs. the standard Normal "
f"(computed pdf curves) - df = {lab}"}]))
fig.update_layout(
template="simple_white",
xaxis_title="Value of the standardized variable (z or t)",
yaxis_title="Probability density",
xaxis_range=[-4.5, 4.5],
yaxis_range=[0, 0.43], # FIXED so the curve does not rescale as df changes
height=470, margin=dict(t=70, r=20, b=60, l=70),
legend=dict(yanchor="top", y=0.99, xanchor="left", x=0.01),
sliders=[dict(active=0, pad={"t": 40},
currentvalue={"prefix": "Degrees of freedom df = "},
steps=steps)],
title="Student's t vs. the standard Normal (computed pdf curves) - df = 1",
)
# THE GOTCHA FIX: emit text/html ourselves (embeddable div + CDN plotly.js).
HTML(fig.to_html(full_html=False, include_plotlyjs="cdn"))
Loading...