Tools to plot Tanner pubertal stage measurements against the Dutch 1997 references and express them as standard deviation scores (SDS).
This package implements the stage line diagram method described in:
van Buuren, S. and Ooms, J.C.L. (2009). Stage line diagram: An age-conditional reference diagram for tracking development. Statistics in Medicine, 28(11), 1569–1579. https://doi.org/10.1002/sim.3567
@article{vanbuuren-2009-1,
title = {Stage line diagram: An age-conditional reference diagram for tracking development},
author = {{van Buuren}, S. and Ooms, J.C.L.},
year = {2009},
journal = {Statistics in Medicine},
volume = {28},
number = {11},
pages = {1569--1579},
doi = {10.1002/sim.3567}
}tanner has two identities. plot_stadia() (and its helpers
plot_stadia_general(), plot_stadia_lines(), plot_stadia_data()),
interpolate_trajectory(), find_stage_segments(), calculate_sds()
and tnologo() are the supported general-purpose library API: pure
functions (or plotting functions taking explicit arguments) that anyone
can library(tanner) and call interactively, covered by unit tests and
this README’s walkthrough.
plotter(), plotterpro() and upload_tryCatch_pro() are rApache
request handlers for the existing webapp deployment (see Production
deployment (rApache)): they read
GET/POST/FILES globals injected by rApache at request time and
aren’t meant to be called interactively from R. They’re held to a lower
bar — no unit tests, not part of this walkthrough — and kept exported
only so the existing deployment keeps working.
This README covers three things:
- Calculating SDS in plain R — no web app needed.
- Running the local mock web app — try the browser frontends without setting up Apache/rApache.
- Producing bulk diagrams with Puberty Pro — upload a whole dataset and get one PDF with everyone’s plot, plus a CSV/TXT of SDS values.
For the original production deployment (Apache + rApache), see Production deployment (rApache) at the end.
# from GitHub
install.packages("devtools")
devtools::install_github("growthcharts/tanner")Or from a local clone:
install.packages("devtools")
devtools::install(".")
# or load without installing, from the repo root
devtools::load_all(".")plot_stadia() draws a patient’s pubertal stage trajectory against the
Dutch 1997 reference percentiles. Here’s a boy’s genital stage followed
from age 8 to 16.5 (patient 7 from the original pubertal demo data.txt
demo dataset used to produce genital.pdf/boy.pdf in this package’s
source history):
library(tanner)
mydata <- data.frame(
id = 1,
age = c(
8.10,
8.37,
8.58,
8.87,
9.12,
9.34,
9.58,
9.86,
10.11,
10.36,
10.59,
10.86,
11.15,
11.34,
11.59,
11.88,
12.13,
12.36,
12.61,
13.13,
13.63,
14.16,
14.66,
16.13,
16.53
),
sex = "M",
gen = c(
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
2,
2,
3,
3,
3,
3,
4,
4
),
phb = NA,
tv = NA,
bre = NA,
phg = NA,
men = NA
)
plot_stadia(
data = mydata,
persons = 1,
type = c(TRUE, FALSE, FALSE),
plotline = c(TRUE, FALSE, FALSE),
title = "Puberty Plot (patient 7, pubertal demo data.txt)",
padid = FALSE
)The shaded bands mark the 1%, 5% and 10% earliest/latest percentiles for the genital reference curve; the thick line is this patient’s own trajectory.
calculate_sds(age, stage, type) converts an age + observed pubertal
stage into a standard deviation score, by interpolating the Dutch 1997
reference percentiles (nl1997). type is one of "gen", "phb",
"tv", "bre", "phg" or "men".
library(tanner)
# a single measurement: a 12-year-old boy at genital stage 3
calculate_sds(age = 12, stage = 3, type = "gen")[1] 1.11
bre and phg stages are coded 1–5, men is coded 1 (not yet) or 2
(menarche reached) — use them as-is:
girls <- read.csv("inst/webapps/pubertypro/demo.csv")
names(girls) <- tolower(names(girls))
girls$age <- as.numeric(gsub(",", ".", girls$age))
girls <- girls[girls$sex == "F", ]
girls$SDS_bre <- round(calculate_sds(girls$age, girls$bre, "bre"), 2)
girls$SDS_phg <- round(calculate_sds(girls$age, girls$phg, "phg"), 2)
girls$SDS_men <- round(calculate_sds(girls$age, girls$men, "men"), 2)gen and phb are coded 1–5 and can be used as-is. tv (testicular
volume) is recorded in ml, typically read off a 12-point Prader
orchidometer (1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20 or 25), but
calculate_sds() and plot_stadia() accept any ml value directly —
including readings between Prader beads (e.g. 11 or 16 ml) — by
interpolating between the surrounding reference percentiles. No recoding
needed:
boys <- read.csv("inst/webapps/pubertypro/demo.csv")
names(boys) <- tolower(names(boys))
boys$age <- as.numeric(gsub(",", ".", boys$age))
boys <- boys[boys$sex == "M", ]
boys$SDS_gen <- round(calculate_sds(boys$age, boys$gen, "gen"), 2)
boys$SDS_phb <- round(calculate_sds(boys$age, boys$phb, "phb"), 2)
boys$SDS_tv <- round(calculate_sds(boys$age, boys$tv, "tv"), 2)Note: an age/stage combination can fall outside the plottable range (the SDS is effectively ±Inf for an extreme outlier, e.g. genital stage 1 at age 19).
calculate_sds()still returns a value, butplot_stadia()will silently skip drawing a point for it, since it falls off the chart.
calculate_sds() is vectorized, so a whole data.frame with both sexes
can be processed in one go. Calling it on a stage column that doesn’t
apply to a row (e.g. gen for a girl) just returns NA for that row,
so there’s no need to split the data by sex first:
mydata <- read.csv("inst/webapps/pubertypro/demo.csv")
names(mydata) <- tolower(names(mydata))
mydata$age <- as.numeric(gsub(",", ".", mydata$age))
mydata$SDS_gen <- round(calculate_sds(mydata$age, mydata$gen, "gen"), 2)
mydata$SDS_phb <- round(calculate_sds(mydata$age, mydata$phb, "phb"), 2)
mydata$SDS_tv <- round(calculate_sds(mydata$age, mydata$tv, "tv"), 2)
mydata$SDS_bre <- round(calculate_sds(mydata$age, mydata$bre, "bre"), 2)
mydata$SDS_phg <- round(calculate_sds(mydata$age, mydata$phg, "phg"), 2)
mydata$SDS_men <- round(calculate_sds(mydata$age, mydata$men, "men"), 2)This is the same calculation Puberty Pro runs for every uploaded dataset.
inst/webapps/ ships two browser frontends (puberty
for a single patient, pubertypro for batch upload). They were
originally built to run behind Apache + rApache, which is now hard to
set up on modern systems.
tools/run_mock_webapp.R serves both frontends
locally with httpuv,
without needing Apache or rApache at all.
install.packages("httpuv")
install.packages("devtools") # only needed if you run from a source checkoutFrom the repo root, in a terminal:
Rscript tools/run_mock_webapp.Ror, from an R/RStudio/Positron console, with the repo as the working directory:
source("tools/run_mock_webapp.R")Either way it prints the URLs and opens the single-patient app in your default browser automatically:
Mock webapps running at:
http://127.0.0.1:7654/puberty/
http://127.0.0.1:7654/pubertypro/
Press Ctrl+C to stop.
Open http://127.0.0.1:7654/pubertypro/ yourself for the batch upload
app — only the first URL is opened automatically.
- Terminal (
Rscript): pressCtrl+C, or close the terminal. - R console (
source()): pressCtrl+Cin the console (or the stop/interrupt button in RStudio/Positron) to break the server loop, or restart the R session.
If you try to start a second instance while one is still running, you’ll
get Failed to create server / address already in use. Find and stop
the process holding port 7654:
# macOS / Linux
lsof -nP -iTCP:7654 -sTCP:LISTEN
kill <PID># Windows (PowerShell)
Get-NetTCPConnection -LocalPort 7654 | Select-Object OwningProcess
Stop-Process -Id <PID>The pubertypro app processes an entire dataset (many patients) in one
go: it draws one puberty plot per patient into a single multi-page PDF,
computes SDS values for every measurement, and offers the result as a
downloadable CSV/TXT alongside the PDF.
- Start the mock server and open
http://127.0.0.1:7654/pubertypro/. - Upload a data file. Sample files are linked on the page itself, and
also available directly in the repo:
inst/webapps/pubertypro/demo.csv,
demo.sav,
demo.txt. Required columns:
id,age,sex("M"/"F"), plusgen,phb,tvfor boys and/orbre,phg,menfor girls (NA-able per row). - Review the parsed per-patient summary table, then click GO!.
- Download the generated files from the icon links:
- PDF — one page per patient, with the puberty stadia plot
(
plot_stadia()under the hood). - CSV / TXT — the original data plus
SDS_gen,SDS_phb,SDS_tv,SDS_bre,SDS_phg,SDS_mencolumns.
- PDF — one page per patient, with the puberty stadia plot
(
Doing the same thing from plain R, without the web app, looks like:
library(tanner)
mydata <- read.csv("inst/webapps/pubertypro/demo.csv")
names(mydata) <- tolower(names(mydata))
mydata$age <- as.numeric(gsub(",", ".", mydata$age))
pdf("all_patients.pdf", paper = "a4r", width = 11.67, height = 8.27)
plot_stadia(
data = mydata,
persons = unique(mydata$id),
type = c(TRUE, TRUE, TRUE),
plotline = c(FALSE, FALSE, FALSE),
padid = TRUE
)
dev.off()The original deployment runs the webapps behind Apache with rApache:
# install R and the tanner package
R CMD INSTALL tanner_1.5.0.tar.gz
# install rapache, e.g. using the package
sudo apt-get install libapache2-mod-r-base
# copy the site file from the package and activate
sudo cp -Rf /usr/local/lib/R/site-library/tanner/sites-available/puberty /etc/apache2/sites-available
sudo a2ensite puberty
# restart apache
sudo service apache2 restart
# if anything isn't working, check the error log:
tail /var/log/apache2/error.log