Technical reference for the public utility functions in
utils.R (recode_sex(),
longwide(), simple_bmi()) and the extended
BMIz functions in cdc.R (ext_bmiz() and its
internal helpers z_score() and
set_cols_first()).
This document describes what the current R code does in the utility and extended-BMIz functions. It is intended as a debugging aid and a long-term technical reference. References to code use function names rather than line numbers, because line numbers become stale with each code edit.
These functions sit outside the cleaning algorithm.
They do not assign exclusion codes and are never called by
cleangrowth(), cleanchild(), or
cleanadult(). They are user-facing helpers for the steps
that come after cleaning: reshaping cleangrowth() output,
and computing extended CDC BMI metrics on the cleaned data.
The algorithm vignettes cover the cleaning pipeline itself. This vignette covers the surrounding toolchain.
The three utility functions plus ext_bmiz() form a
natural workflow that takes cleaned long-format data through to extended
BMI z-scores:
cleangrowth() output → longwide() → wide ht/wt rows, filtered by exclusion code
simple_bmi() → adds bmi column
ext_bmiz() → extended + LMS BMIz, BMIp, sigma, etc.
cleangrowth() returns sex exactly as submitted
(e.g. M/F or 0/1).
longwide() recodes it to M/F,
reading the submitted codes the same way cleangrowth() does
(the default recognized codes, or the sex_coding you pass),
and ext_bmiz() reads M/F
directly, so callers using the standard pipeline do not need to recode
sex. recode_sex() is a standalone helper for recoding one
explicit pair of codes to another.
The functions accept either a data.frame or
data.table. Several coerce to data.table
internally for speed.
recode_sex() (utils.R)Purpose. Recode a binary sex variable from one
explicit pair of codes to another (e.g. 0/1 to
M/F for ext_bmiz(), using
targetm = "M" and targetf = "F";
ext_bmiz() reads only the column named sex, so
use targetcol = "sex" for that). longwide() no
longer uses it: it parses sex the way cleangrowth()
does.
Signature.
recode_sex(input_data,
sourcecol = "sex",
sourcem = "0",
sourcef = "1",
targetcol = "sex_recoded",
targetm = 1L,
targetf = 2L)Parameters. sourcecol is the existing
sex column; sourcem/sourcef are the values it
uses for male/female. targetcol is the column to write,
which may be sourcecol itself;
targetm/targetf are the values written for
male/female. Defaults map sex 0/1 →
sex_recoded 1/2.
Mechanics. Coerces to data.table,
matches sourcecol against sourcem and
sourcef, then writes targetcol from those
matches: targetm, targetf, or NA
for rows matching neither value.
Output. Returns a data.table with the
added/updated targetcol. The input is not modified, and
unless targetcol is sourcecol the source
column is preserved, so the recode can be chained (e.g., recode back
from text labels to numbers).
longwide() (utils.R)Purpose. Transform long-format growthcleanr data (one row per measurement) into wide format (one row per subject-age with paired height and weight), filtered to the exclusion codes the caller wants to keep. This is the reshape step that prepares cleaned data for BMI computation.
Signature.
longwide(long_df,
id = "id",
subjid = "subjid",
sex = "sex",
agedays = "agedays",
param = "param",
measurement = "measurement",
gcr_result = "exclude",
include_all = FALSE,
inclusion_types = c("Include"),
extra_cols = NULL,
keep_unmatched_data = FALSE,
sex_coding = <cleangrowth() default codes>,
param_coding = NULL)Input expectations. A long data frame with the named
id/subjid/sex/agedays/param/measurement columns plus an exclusion
column. The gcr_result default is "exclude",
which matches the cleangrowth() output column from version
3 onward — so a cleangrowth() result
data.table can be passed (or merged onto the input)
directly. For data with a differently named exclusion column, pass
gcr_result explicitly. If the column is missing and the
default "exclude" was used, the error suggests
gcr_result = "gcr_result" (the column name 2.x scripts
used).
Mechanics.
extra_cols) is absent. The six required
columns are read by the names passed in id,
subjid, sex, agedays,
param and measurement and renamed to those
internal names, so a column that merely happens to be named, say,
subjid is never read when subjid = "patid" is
passed. gcr_result and extra_cols may not
reuse a name longwide() needs (a required column, or an
output column such as agey or ht); that is an
error.include_all = TRUE, all rows are kept and
inclusion_types is ignored. Otherwise only rows whose
gcr_result is in inclusion_types (default
c("Include")) are kept. An inclusion_types
value that is not a version 3 exclusion code (Summary or Detailed, or a
bin_exclude/tri_exclude value), such as the
2.x code "Exclude-Carried-Forward", matches nothing, so
longwide() warns and points to the code mapping in
vignette("experienced-start-here").agey
(agedays / 365.25, rounded to 4 places) and
agem (agey * 12).sex with the same
parser cleangrowth() uses — the default recognized codes
(0/1, m/M, f/F, b/g, male/female, boy/girl; case- and
whitespace-insensitive) or the user’s sex_coding list — and
overwrites sex with M or F. A
retained row whose sex is missing or unrecognized gets NA
sex, and longwide() warns with the count and up to five
unrecognized values (ext_bmiz() cannot compute BMIz for
those rows). Pass the same sex_coding given to
cleangrowth() if you used one, because
cleangrowth() returns sex as entered, with no record of the
coding. With the default coding, longwide() makes the same
two checks as cleangrowth(), on the whole sex
column before rows are filtered and at every age: it stops if the values
are only 1 and 2 with at least one 2, and gives a prominent warning if
every value is 1 across more than one subject. Passing
sex_coding removes both.param the
way cleangrowth() does (its output returns
param and measurement verbatim):
param_coding labels resolve to a canonical parameter,
imperial values are converted to metric
(HEIGHTIN/LENGTHIN × 2.54,
WEIGHTLBS ÷ 2.2046226), and LENGTHCM is
treated as HEIGHTCM (label only; no length adjustment).
Only height and weight rows are kept: head circumference is dropped
silently, and a row with an unrecognized parameter is dropped with a
warning naming up to five values. Pass the same
param_coding given to cleangrowth() if you
used one. So ht and wt in the output are
always cm and kg.ids among the kept height and weight rows and
errors on duplicates.ht_id/wt_id, then merged on
subjid, agey, agem, agedays, sex.
keep_unmatched_data controls whether the merge is an inner
join (default, all = FALSE) or a full outer join that
retains heights without a same-day weight and vice versa. The merged
measurements are exposed as ht and wt.extra_cols handling. For each extra
column, the height-side (.x) and weight-side
(.y) values are compared. If they match on every row
(treating NA/NA as a match), a single merged column is kept. Otherwise
the function keeps both as ht_<col> and
wt_<col> plus a logical
match_<col> column flagging where they agree.HEIGHTCM/WEIGHTKG
columns (their values live in ht/wt) and gives
the subject and age columns back the names passed as subjid
and agedays.Output. A wide data frame with one row per matched
subject-age, carrying the subject column (under the name passed as
subjid), the two ids,
agey/agem/agedays, recoded
sex (M or F), ht,
wt, and any requested extra columns. Suitable for
simple_bmi() then ext_bmiz().
Notes / pitfalls.
keep_unmatched_data = TRUE to retain them.ids are a hard error — the wide pivot depends
on a unique per-row id.longwide() uses base R
only. If no rows are kept (or there are heights but no weights), it
returns a data frame with the usual columns and no rows.simple_bmi() (utils.R)Purpose. Add a BMI column to wide-format data using
the standard formula. Intended for output from
longwide().
Signature.
Mechanics. Verifies wtcol and
htcol are present (errors otherwise), coerces to
data.table if needed, and computes
bmi := wt / (ht * 0.01)^2 — i.e., weight in kg over height
in meters squared (height is supplied in cm, hence the 0.01
factor).
Output. The input as a data.table with
an added bmi column.
ext_bmiz() (cdc.R)Purpose. Compute the extended BMI metrics defined by the CDC, alongside the standard LMS z-scores and percentiles, for children aged 2–19.9 years. The extended BMIz was introduced to better characterize BMI above the 95th percentile, where the original LMS distribution is compressed and poorly discriminates severe obesity. For BMI at or below the 95th percentile the extended values equal the LMS values; they diverge only above it.
This function uses the R code published on the CDC website and the cdcanthro R package. It is directly relevant to research comparing extended vs. original CDC BMIz. The extended BMI method is described in:
Wei R, Ogden CL, Parsons VL, Freedman DS, Hales CM. A method for calculating BMI z-scores and percentiles above the 95th percentile of the CDC growth charts. Ann Hum Biol. 2020 Sep;47(6):514-521. doi: 10.1080/03014460.2020.1808065. PMCID: PMC11232929.
Signature.
ext_bmiz(data,
age = "agem",
wt = "wt",
ht = "ht",
bmi = "bmi",
adjust.integer.age = TRUE,
ref.data.path = "")Input expectations.
data has at least age, sex, weight, height, and BMI
columns. The default column names (agem, wt,
ht, bmi) match longwide() +
simple_bmi() output; remap via the arguments if
different.age must be in months, using the most
precise value available (years × 12, or days ÷ 30.4375). Only ages
24–240 months are evaluated; others are dropped from the computation
(but see merge-back below).sex should be text:
M/F, which is what longwide()
returns, or another text code that works the same as in
cleangrowth() (for example b/g or
male/female). Numeric sex is accepted only if
the column has both 1s and 2s and no other values, read
as 1 = male, 2 = female — different from
cleangrowth()’s numerical 0/1, for historical reasons.
The 1/2 coding matches the CDC reference data, CDC software, and the
cdcanthro package. Any other numeric value stops the run.
The column must be named sex.wt in kg, ht in cm.adjust.integer.age — if all age values are
integers, add 0.5 (placing integer ages at mid-month, as the CDC macro
does). Disable by setting FALSE.ref.data.path — directory holding the reference file.
Empty default loads the packaged
inst/extdata/CDCref_d.csv.gz via
system.file(); override only when developing against a
local copy.Mechanics (high level).
sex) exist,
and warns if data already has a column named like one of
the output columns (for example, from an earlier run): the returned data
has the newly calculated values in that column. Copies data
(dorig) and reads the four named columns plus
sex into a separate working table with a seq_
row index; data itself is never modified and its column
names are kept.sex has any numeric value unless the present
values are exactly 1 and 2 (see Notes / pitfalls), then normalizes
sex to sexn ∈ {1, 2} via first-character
matching (logical values are not recognized).CDCref_d.csv.gz,
denom == "age"), assembling per-month L/M/S parameters for
weight, height, and BMI (including the age-240 row used for the
adjusted-distance reference mref/sref).approx()).z_score() (see below).pnorm), the 95th and 97th BMI
percentiles (p95, p97), bmip95
(BMI as a percent of the 95th percentile), the obese / severe-obese
flags, and the alternative distance/percent-distance metrics from PMID
31439056.sigma (a sex-specific quadratic in age in
years). For rows with LMS BMI percentile ≥ 95, replaces the percentile
with 90 + 10 * pnorm((bmi - p95) / sigma) and the z-score
with qnorm(ebp/100). Where the extended percentile is so
close to 100 that qnorm returns infinity (only above
99.99999999999999, e.g. an implausibly high BMI at a young age — likely
data error), the extended z is capped at 8.21.dorig,
matched by seq_ so every original row and column is
returned in the original order (rows outside the age range get
NA metrics). The input columns keep the user’s names.
Variable labels (descriptions such as “Age (months)”) are attached as
each column’s label attribute (on the copy).Key output columns.
bmiz/bmip (LMS or extended, identical
below the 95th percentile),
original_bmiz/original_bmip (always LMS),
mod_bmiz/mod_waz/mod_haz
(modified z-scores),
waz/wp/haz/hp
(weight- and height-for-age LMS z/percentile),
p95/p97/bmip95,
sigma, and the obese/sev_obese
flags (sev_obese = BMI ≥ 120% of the 95th percentile). See
?ext_bmiz for the full column set.
Notes / pitfalls.
cleangrowth()
(0/1), so ext_bmiz() stops on numeric sex unless the column
has both 1s and 2s and no other values. This includes a column of only
1s, which could be all-male 1/2 data or all-female 0/1 data, and a
column of only 2s. Numeric text and factor values are treated the same
way. Single-sex data should be coded M or F.
If 0 or another number means unknown, set it to NA. A
warning gives the count of rows aged 24–240 months with a missing or
unrecognized sex.NA metrics) because the result is merged
back onto the original rows.NA metrics
even though a child at that age may be exactly 2 years old; metrics
begin at day 731. This matches the cdcanthro package. A
warning gives the number of rows at about 729–730 days.qnorm-infinity cap at 8.21 is intentional and
matches the CDC macro’s handling.data.table, R.utils (except
extract), and stats::approx/pnorm/qnorm, all
from Imports; no optional packages are needed.z_score(var, l, m, s) — Given a
measurement and its LMS parameters, returns a two-element list: the
standard LMS z-score (((var/m)^l) - 1) / (l*s) and a
modified z-score. The modified z-score expresses the
value as a multiple of half the distance from the median to the ±2 SD
points (sdm2 below the median, sdp2 above),
which keeps extreme values on a more interpretable scale than the raw
LMS z. Used for all three parameters (weight, height, BMI).
set_cols_first(DT, cols, intersection = TRUE)
— Reorders a data.table so cols come first
(via setcolorder). With intersection = TRUE
(default) only columns actually present are moved, so it is safe to pass
a superset. Used to restore the original column order on the merged-back
result. (Adapted from the hutils package.)