Time-to-Event Data, Censoring, and Hazard Functions › Visualizing Survival, Censoring, and Hazards
Machine Learning from ScratchAll chapters Time-to-Event Data, Censoring, and Hazard Functions › Visualizing Survival, Censoring, and HazardsSection view · Sections in this chapter5 / 10 ↓
Reading tools Definitions, links & notes
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Before fitting anything, it is worth looking at the shapes involved. The three quantities , , and move together, and the way they move is easiest to grasp from a figure. We will use a Weibull family because it conveniently covers the three qualitative hazard shapes: constant (exponential as a special case), increasing, and decreasing.
The Weibull survival function with shape and scale is , and the corresponding hazard is . Here controls the shape of the hazard and is the scale parameter measured in the same time units as . Note that this scale plays a different role from the exponential rate used in the worked example below, where is the constant hazard. When the hazard is constant at . When the hazard increases with time (wear-out). When the hazard decreases with time (infant mortality). Increasing, decreasing, and approximately constant hazards over particular time ranges have been observed in component reliability data. Understanding these survival curves is a first diagnostic tool.
PythonIn [3]Expand
def weibull_survival(t, shape, scale):
return np.exp(-((t / scale) ** shape))
def weibull_hazard(t, shape, scale):
return (shape / scale) * (t / scale) ** (shape - 1.0)
t_grid = np.linspace(0.01, 20.0, 400)
t_grid_survival = np.linspace(0.0, 20.0, 400)
scenarios = [
dict(name="Exponential (shape = 1)", shape=1.0, scale=5.0),
dict(name="Weibull increasing (shape = 2)", shape=2.0, scale=5.0),
dict(name="Weibull decreasing (shape = 0.5)", shape=0.5, scale=5.0),
]
for s in scenarios:
s["S"] = weibull_survival(t_grid_survival, s["shape"], s["scale"])
s["h"] = weibull_hazard(t_grid, s["shape"], s["scale"])The next two figure cells are split purely for presentation. Both read the precomputed grids and the scenario dictionaries, and neither recomputes the data. The first shows the survival functions. The second shows the hazards.
PythonIn [4]Expand
from book_plot_style import PALETTE, polish_axes, use_book_style
def pal(i: int):
keys = ("primary", "amber", "teal", "rose")
return PALETTE[keys[i % len(keys)]]
use_book_style()
fig, ax = plt.subplots(figsize=(6.0, 4.0))
for i, s in enumerate(scenarios):
ax.plot(
t_grid_survival,
s["S"],
color=pal(i),
linewidth=1.1,
linestyle=("-", "--", "-.")[i],
label=s["name"],
)
ax.set_xlabel("Time t")
ax.set_ylabel("Survival probability S(t)")
ax.set_title("Weibull survival at three shapes")
ax.set_ylim(0, 1.02)
ax.legend(loc="upper right")
polish_axes(ax)
plt.tight_layout()
plt.show()VisualizationOut [4]Expand

The survival plot makes two things visible. First, all three start at and eventually decay to zero, as required. Second, the shape parameter changes the shape of the decay. The decreasing-hazard case (infant mortality) drops steeply early and then flattens, so its survival curve has a long tail. The increasing-hazard case (wear-out) stays near one for a while and then falls rapidly. These are two ends of the stated monotone-hazard spectrum. Real data require their own hazard diagnostics.
PythonIn [5]Expand
import matplotlib.pyplot as plt
from book_plot_style import PALETTE, polish_axes, use_book_style
use_book_style()
fig, ax = plt.subplots(figsize=(6.0, 4.0))
for i, s in enumerate(scenarios):
ax.plot(
t_grid,
s["h"],
color=pal(i),
linewidth=1.1,
linestyle=("-", "--", "-.")[i],
label=s["name"],
)
ax.set_xlabel("Time t")
ax.set_ylabel("Hazard h(t)")
ax.set_title("Weibull hazards at three shapes")
ax.set_ylim(0, 2.4)
ax.legend(loc="upper right")
polish_axes(ax)
plt.tight_layout()
plt.show()VisualizationOut [5]Expand

A constant hazard means the same instantaneous failure rate at each age; for an exponential lifetime it also gives a memoryless residual lifetime. A rising hazard can describe wear-out, while a falling hazard can describe early failures followed by lower risk among survivors. A decreasing population hazard alone does not prove that individual devices become stronger. The hazard grid starts at because the shape- hazard diverges as ; the survival grid includes zero.
Cumulative hazard is dimensionless and increases at rate . At a given time, a larger corresponds to a smaller survival probability through . The absolute rate of decline of survival is , so a larger hazard alone does not imply a steeper survival curve when the survival levels differ.
PythonIn [6]Expand
import numpy as np
cumulative_hazards = []
for scenario in scenarios:
cumulative_hazards.append(
dict(name=scenario["name"], H=-np.log(scenario["S"]))
)VisualizationOut [7]Expand

For the censoring plot, imagine devices installed at independent, uniformly distributed calendar dates during a fifteen-unit recruitment period, with follow-up ending at a common administrative cutoff. Each device is followed from installation, so its potential follow-up duration is uniform from zero to fifteen and independent of its simulated lifetime . This is staggered recruitment without left truncation. Horizontal segments end at ; diamonds mark failures and squares mark censorings. A censoring marker locates the end of observation, not an unobserved failure.
PythonIn [8]Expand
rng = np.random.default_rng(0)
n_swim = 40
T_swim = rng.exponential(scale=5.0, size=n_swim)
C_swim = rng.uniform(0, 15.0, size=n_swim)
Y_swim = np.minimum(T_swim, C_swim)
delta_swim = (T_swim <= C_swim).astype(int)
order = np.argsort(Y_swim)
Y_swim = Y_swim[order]
delta_swim = delta_swim[order]PythonIn [9]Expand
import matplotlib.pyplot as plt
from book_plot_style import PALETTE, polish_axes, use_book_style
use_book_style()
fig, ax = plt.subplots(figsize=(6.0, 4.5))
for i, (y, d) in enumerate(zip(Y_swim, delta_swim)):
color = pal(0) if d == 1 else pal(1)
ax.plot([0.0, y], [i, i], color=color, linewidth=1.1)
marker = "D" if d == 1 else "s"
ax.plot(y, i, marker=marker, color=color, markersize=5)
ax.set_xlabel("Observed time Y (simulated units)")
ax.set_ylabel("Subject index (sorted by Y)")
ax.set_title("Observed failures and censoring")
ax.legend(
handles=[
plt.Line2D(
[],
[],
color=pal(0),
marker="D",
linestyle="-",
label="Observed event",
),
plt.Line2D(
[],
[],
color=pal(1),
marker="s",
linestyle="-",
label="Right-censored",
),
],
loc="lower right",
)
polish_axes(ax)
plt.tight_layout()
plt.show()VisualizationOut [9]Expand

This is the visual form of the representation. Read each line as "device was observed from 0 to ; the marker tells us whether the device failed or was still running." The censored lines are not wrong or missing. They carry important information, namely that the true failure time is greater than the endpoint we see. A correct survival likelihood gives those records weight for the region to the right of the marker, which is exactly the probability that the event had not yet occurred at the moment we stopped observing.
Comments
Add to the conversation
A question, a correction, or an example from your own work.
No comments yet. Be the first to share your thoughts!