AdvancedIndependently reviewed, not yet spot-checked by a human

Nomograms

The nomogram is the signature figure of the clinical prediction-model literature, and the thing most often misread as a model in its own right. It is not one. It takes an already-fitted model's linear predictor, rescales it into points, and draws it as a ruler — adding no information at all. This page proves that with one patient computed two ways and an exact affine identity across a whole development cohort, then hands the real questions back to internal validation, external validation and calibration.

A nomogram is not a model

In the oncology and surgical literature the nomogram is close to a formatting requirement: fit the model, report the C-index, and finish with that horizontal ruler covered in tick marks. It appears so often that papers write sentences like “we developed a nomogram to predict five-year recurrence” — as though the nomogram were a kind of model.

It is not a model. It takes the linear predictor of a model that has already been fitted and rescales it onto a 0 to 100 point scale, then draws it. No new information enters, and nothing is estimated at the moment the picture is drawn. It is a hand-calculation device: it lets someone with a pen and a straight edge get the same number the software would give.

That makes “is this a good nomogram?” the wrong question. The question is whether the model behind it has been through internal validation, external validation and calibration. The second half of this page hands those three questions back with the answers this site has already measured on the same model.

What the ruler is drawn from

This page deliberately uses the same model as the calibration page: a Cox model with four predictors fitted on survival::rotterdam (2982 patients, 1713 events) at a 5-year horizon, externally validated on survival::gbsg (686 patients, 299 events).

Using the same model is what the argument needs. If a nomogram really is only a change of drawing, then it must not move a single number the calibration page measured.

A nomogram. The top horizontal axis is Points, running from 0 to 100. Below it are four predictor axes in turn: age at diagnosis, tumour size in millimetres, positive node count, and grade 3 histology coded 0 or 1. Each tick on a predictor axis lines up vertically with a position on the points axis; the positive-node axis spans the full width of the figure, while the other three are much shorter; the shortest is the grade 3 axis at 13.14 points, with age only slightly longer at 14.03. Below the predictor axes is a Total Points axis, and at the bottom an axis of five-year risk of recurrence or death whose labels increase from left to right but are unevenly spaced, compressed towards the high-risk end.
Nothing on this figure is new. The tick positions on the four predictor axes, and the mapping from total points down to risk, are a re-typesetting of a Cox model that was already fitted.Plotting script figures/scripts/B5-09-nomogram.R

Reading it takes three steps:

  1. On each predictor axis find the patient’s value and read upwards to the points axis.
  2. Add the four point scores and locate the sum on the total points axis.
  3. Read downwards from there to the risk axis at the bottom.

Walking down the ruler once

Take a hypothetical patient: age 55, a 35 mm tumour, 4 positive nodes, grade 3. This patient sits deliberately near the middle of the development cohort, for the reason just given; the script records the same sentence as its selection_rule.

Now read the points off, one axis at a time. Not recomputed from the coefficients — interpolated on the axes of the picture itself, which is the same thing a reader with a ruler does:

PredictorThis patientPoints read off the axis
Age at diagnosis (years)556.59
Tumour size (mm)3511.64
Positive nodes (count)411.43
Grade 3 histology (1 = yes)113.14
Total points42.80

Converting 42.80 points back gives a linear predictor of 0.280401, which through the baseline survival function gives a five-year risk of 52.2%.

The other route never touches the figure: multiply the four covariate values by their coefficients and add, giving a linear predictor of 0.280401 and, through the same baseline survival function, 52.2%.

Two panels side by side. The left panel lists four rows of value and points — age, tumour size, positive nodes, grade 3 — each with its score on the right, then a rule and the total, 42.80 points, followed by one line of arithmetic dividing the total by the points per unit of linear predictor and adding a constant to recover the linear predictor. The right panel shows two bars whose heights are the five-year risk: one labelled as read off the nomogram, one labelled as computed from the linear predictor. The two bars are the same height, each captioned with its value to six decimal places, and a line of red text between them gives their difference as a number close to the limit of floating-point precision.
Left: adding the points up and converting back, which is what a reader with a ruler does. Right: the five-year risk each route arrives at. The two bars matching is not a coincidence; it is a definition.Plotting script figures/scripts/B5-09-nomogram.R

Not just this patient: the whole cohort

One patient agreeing could be a patient chosen to agree. So the script does it for every patient in the development cohort: read each one’s points off the four axes, add them up, and compare against that patient’s own linear predictor.

A scatter plot. The horizontal axis is the linear predictor from the Cox model and the vertical axis is the total points read off the nomogram; each point is one patient from the development cohort, some two thousand of them. Every point falls on one perfectly straight line with no visible scatter, and a red dashed line lies on top of them; the legend states the line's slope and intercept along with the maximum residual and the R squared.
Total points against the linear predictor for every patient in the development cohort. This is not a well-fitting model; it is an identity, and the straight line is a consequence of how the points scale was built.Plotting script figures/scripts/B5-09-nomogram.R

Across 2982 patients the largest residual is 4.3e-14 and R2R^2 is 1.000000. Total points is an affine transformation of the linear predictor — a shift and a multiplication — and nothing else:

Total Points=(ηη0)×k\text{Total Points} = (\eta - \eta_0) \times k

Here η\eta is the linear predictor, η0\eta_0 is the linear predictor at zero total points, and kk is how many points one unit of linear predictor is worth. For this model kk is 39.57 points and η0\eta_0 is -0.801. It is negative because rms reports a linear predictor centred on the mean covariate values, and the patient scoring zero total points sits at the low end of every axis, well below that mean.

Where the points scale comes from

A change of units needs an exchange rate. The rule rms::nomogram uses is to work out how far each predictor can move the linear predictor over the tick range of its own axis, give the predictor that moves it furthest a full 100-point axis, and scale every other axis by the same factor. The tick range is not the observed range: here the node axis is ticked out to 35 while the largest count in the cohort is 34, and the exchange rate is computed from the former.

In this model the widest axis is the positive node count: its coefficient multiplied by the span of its axis is the largest of the four, so it takes the full scale and the other three axes are shorter: grade 3 is the shortest at 13.14 points, with age just behind at 14.03 — and age is short enough that the script has to hand it an explicit set of ticks, or the defaults crowd into an unreadable band. That fixes the exchange rate at 39.57 points per unit of linear predictor.

There is also an implementation trap that has nothing to do with interpretation and everything to do with a page telling the truth: the package that draws the picture and the package that produced the reported coefficients must be the same fit. nomogram() only understands an rms::cph object, while most papers report coefficients from survival::coxph. The convergence tolerance cph uses by default is the looser of the two, and the coefficients then agree only to the fourth or fifth decimal — enough to make the claim that these are the same model not quite true.

The script behind this page tightens the tolerance and asserts the agreement with stopifnot():

Predictorrms::cphsurvival::coxphDifference
Age at diagnosis (years)0.0053710.0053712.3e-17
Tumour size (mm)0.0147080.0147081.2e-17
Positive nodes (count)0.0722020.0722026.9e-17
Grade 3 histology (1 = yes)0.3320170.3320174.6e-15

The largest of the four differences is 4.6e-15, well inside the threshold the script enforces. If some later change ever pulls the two apart, the script stops instead of quietly shipping a figure of a model that is not the one in the text.

Not the same thing as a Fagan nomogram

The diagnostic accuracy page has a figure that is also called a nomogram. The two share the old idea of replacing arithmetic with geometry, and differ in everything else:

Fagan nomogramThe nomogram on this page
Class of problemUpdating a probability with a test resultTurning a fitted model into a risk
InputPre-test probability and a likelihood ratioOne patient’s full set of predictor values
Operation performedMultiply the odds by the likelihood ratioRescale the linear predictor into points, then into risk
What must exist firstA test with a known sensitivity and specificityA fitted regression model
How it is usedJoin two points with a straight line and extend it to the third axisRead vertically from each axis for points, sum, read down for risk
OutputPost-test probabilityPredicted risk

The most practical difference is physical: the Fagan figure asks you to draw a slanted line across three axes, while this one only ever asks for vertical alignment followed by addition. Anyone laying a ruler diagonally across a prediction-model nomogram has confused the two.

So what should be asked of the figure

Since the nomogram adds no information, judging it is the same as judging the model behind it. For this model, the site has measured three things:

QuestionThis modelPage
Ranking, development cohortC-index 0.666Internal validation
Ranking, external cohortC-index 0.642External validation
Whether the numbers themselves are rightCalibration slope 0.691 (95% CI 0.539–0.842)Calibration

Discrimination drops a little from development to external validation, which is expected. The row worth stopping on is the last one: the calibration slope is well below 1, and the whole of its 95% confidence interval lies below 1.

A slope below 1 means the predictions are too spread out: high-risk patients are pushed too high and low-risk patients pressed too low. Translated back onto this figure, that is the same sentence in pictorial form — both ends of the ruler are too long. A patient landing at the high end reads a risk above what patients like them actually experienced in survival::gbsg, and a patient at the low end reads one below.

Running it yourself

library(survival); library(rms)
data(cancer, package = "survival")

dev <- rotterdam
dev$rfs_time  <- pmin(dev$rtime, dev$dtime)
dev$rfs_event <- as.integer(dev$recur == 1 | dev$death == 1)
dev$size_mm   <- c("<=20" = 15, "20-50" = 35, ">50" = 60)[as.character(dev$size)]
dev$grade3    <- as.integer(dev$grade >= 3)

# Labels must be attached BEFORE cph(), or the picture keeps the bare
# variable names -- with no error anywhere.
label(dev$age)     <- "Age at diagnosis (years)"
label(dev$size_mm) <- "Tumour size (mm)"
label(dev$nodes)   <- "Positive nodes (count)"
label(dev$grade3)  <- "Grade 3 histology (1 = yes)"

dd <- datadist(dev); options(datadist = "dd")

# eps defaults to 1e-4, which leaves the coefficients agreeing with coxph
# only to about 1e-5.
fit <- cph(Surv(rfs_time, rfs_event) ~ age + size_mm + nodes + grade3,
           data = dev, surv = TRUE, x = TRUE, y = TRUE, eps = 1e-9, tol = 1e-12)
max(abs(coef(fit) - coef(coxph(Surv(rfs_time, rfs_event) ~
       age + size_mm + nodes + grade3, data = dev))))    # should be ~1e-15

H     <- 5 * 365.25
surv5 <- Survival(fit)
risk5 <- function(lp) 1 - surv5(H, lp)

nom <- nomogram(fit, fun = risk5, funlabel = "5-year risk")
plot(nom, xfrac = 0.34)

# The exchange rate lives in the object's own attributes
info <- attr(nom, "info")
info$sc          # points per unit of linear predictor
info$Intercept   # linear predictor at zero total points

# One patient, two routes
patient <- data.frame(age = 55, size_mm = 35, nodes = 4, grade3 = 1)
lp_direct <- as.numeric(predict(fit, newdata = patient, type = "lp"))

# Once labels are attached, nom's elements are named by LABEL; a lookup by
# variable name returns NULL, and NULL[["points"]] is NULL, not an error.
ax     <- nom[["Positive nodes (count)"]]
p_node <- approx(ax$nodes, ax$points, xout = patient$nodes)$y

# With all four axes read and summed:
# lp_from_points <- total_points / info$sc + info$Intercept
# risk5(lp_from_points) matches risk5(lp_direct) to floating-point precision

Verified against R 4.6.0 with survival 3.8.6 and rms 8.1.1. Two places that fail silently: label() has to be set before cph() runs, because plot.nomogram reads the labels stored in the fit and setting them afterwards quietly leaves the bare variable names on the picture; and once labels exist, nomogram() names its list elements by label, so looking an element up by variable name returns NULL, and NULL[["points"]] is NULL rather than an error.

Four questions for a paper with a nomogram

  1. Is there an external validation? A nomogram drawn only on the development cohort is an unvalidated model, nicely typeset.
  2. Is there a calibration curve? The figure hands the reader an absolute risk, and whether an absolute risk is right is a calibration question, not a C-index question.
  3. What are the points used for? If the paper cuts them into high, intermediate and low risk groups, where did the cut-points come from and has anyone reproduced them elsewhere? Points carry no meaning across studies.
  4. What does it buy over the staging system already in use? That is a question for the decision curve or net reclassification, not for how the figure looks.

Common misuses

MisuseWhy it is wrong
Treating the nomogram as a model or a methodIt is a re-typesetting of an existing model; its information content is one number, the linear predictor
Writing that the nomogram improved predictive performanceA change of units moves nobody’s ranking and nobody’s predicted risk
Publishing the figure without the coefficients and baseline survivalWithout those two, nobody can reproduce the model on their own data
Comparing points against the points in another paperEach exchange rate is set by the widest predictor in its own dataset; the two scales are different rulers
Claiming the predictor worth the most points is the most important oneThat reflects the range of that variable in that dataset, not its effect size or its clinical weight
Declaring clinical usability on the strength of a development-cohort figureWhat is missing is external validation and calibration, and no amount of drawing supplies it
Measuring distances along the risk axis as if it were linearTotal points to risk is a non-linear transformation; the high-risk end is compressed
Laying a ruler diagonally across the axesThat is how a Fagan nomogram is read; here the axes are read vertically and summed
Drawing with rms::cph while reporting survival::coxph, without checkingTheir default convergence tolerances differ, so the figure and the text drift onto slightly different models

Reproducing every number on this page

/opt/homebrew/bin/Rscript figures/scripts/B5-09-nomogram.R

Read the figure

The answer comes from the same statistical output that produced this page's figures, not from a number typed in beside them.

Plot every development-cohort patient's total points against that patient's own linear predictor and the result is a perfectly straight line. What does that mean?

Show the answer and why

Correct answer: 1.000000 - total points are an affine transform of the linear predictor, so this figure adds no information about any patient

An R-squared of 1.000000 would be a warning sign anywhere else, usually meaning the outcome has found its way in among the predictors; here it is the entire argument of the page - total points equal the linear predictor shifted and then multiplied by a constant, the way Celsius becomes Fahrenheit. So unusually precise asks the wrong question: the largest residual of 0.000000 is not accuracy, it is the floating-point rounding an identity leaves behind. 0.666129 is the same model's C-index in the development cohort, which is the model's ranking ability and has nothing to do with this scatter plot. A nomogram changes the units, not the amount of information, and any claim that a model became more clinically useful because a nomogram was drawn cannot survive this figure.

On this nomogram the positive-nodes axis takes the full score on its own, and the other three axes are shared out in the same proportion. What does that tell you?

Show the answer and why

Correct answer: 100.0 points means this variable moves the linear predictor further than any other across its own axis range, and the conversion rate was set from it

The conversion rate is set like this: work out how far each variable can move the linear predictor across its own axis range, give the largest mover the full score, and share the rest out in the same proportion. So 100.0 describes which coefficient times axis span is largest in this dataset, not clinical importance - change the dataset, change the set of covariates, or simply draw one axis a little wider, and the same patient's points change. 26.2 and 13.1 are the same story: they are traces of the ranges of variation in this dataset rather than weights belonging to the variables, which is why setting them beside another paper's points means nothing. What does travel between studies is the risk itself.

This page deliberately demonstrates the two routes on a patient whose five-year risk sits mid-scale in the development cohort, rather than on a patient at very high risk. Why does that choice affect what the demonstration proves?

Show the answer and why

Correct answer: It does, because the risk axis at the bottom has a flat stretch: this patient's five-year risk of 0.522 does not sit on it

Going from total points to risk passes through a non-linear transform, so the same ten points buy different amounts of risk depending on where on the axis you are standing, and where the risk is already close to its ceiling further points buy almost nothing - that is the flat stretch of the risk axis. That is what limits the demonstration: on a patient out there, the two routes can travel two clearly different linear predictors and still land on risks that look identical, which makes agreement cheap. So this patient's five-year risk of 0.522 was chosen mid-scale on purpose, and the script records the same reason. The conversion really is a fixed 39.572 points per unit of linear predictor, but that is the relationship between total points and the linear predictor; the risk axis is a further layer on top of it, and a fixed conversion does not make that layer evenly spaced. 42.795 is the total: the points axis itself is evenly spaced from end to end and measures just as accurately at either extreme, and the non-linear one is the axis at the bottom.

When the points conversion is set, the package uses each axis's tick range rather than the range actually observed in the cohort. Where do the two differ in this model?

Show the answer and why

Correct answer: The nodes axis is ticked to 35, one above the largest value actually seen, and the conversion rate is computed from the tick side

The conversion rate is computed from the tick range, not the observed range. Three of the four axes have the two coinciding here; only the nodes axis differs, ticked to 35 where the largest value actually seen is 34. One node looks negligible, but it is part of the conversion rate - which is to say how many points an axis is worth depends on where whoever drew the figure put the ticks. Tick the 90 axis ten years further and the points for age change too. That is the mechanical reason points do not travel between papers.

This page deliberately uses the very same model as the calibration page. If a nomogram really is only a redrawing, what has to hold?

Show the answer and why

Correct answer: The calibration slope in the external cohort is still 0.691, identical to what the calibration page reports - a redrawing changes no measured number

A nomogram is a re-typesetting of a Cox model that was already fitted, so it cannot move any number computed from that model: the external calibration slope is still 0.691, the same value the calibration page reports, and that is the testable version of the claim that it is the same model. 0.666 and 0.642 are the C-indices in the development and external cohorts, and neither moves because a figure was redrawn - ease of use is an interface property, while ranking and scale were both settled before anything was plotted. What does break this in practice is drawing and reporting from two different fits: the function that draws a nomogram only accepts one package's model object, most papers report coefficients from another, and the two convergence tolerances leave the coefficients agreeing to only a few decimal places.

Sources and licences

This page is original writing

Report a content problem

The statistics on this site are written by AI and reviewed by AI; a human only spot-checks. What you can see may be what we cannot.

The more specific, the more fixable — e.g. which sentence disagrees with which textbook or paper.

Needed only if you want a reply; reports without it are still read.

Sent along with your report

These are attached automatically. You can drop any of them.