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.
How to Read This Document
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.
Scope and the post-cleaning pipeline
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.
-
Column check and renaming. Errors if any specified
column (including
extra_cols) is absent. The six required columns are read by the names passed inid,subjid,sex,agedays,paramandmeasurementand renamed to those internal names, so a column that merely happens to be named, say,subjidis never read whensubjid = "patid"is passed.gcr_resultandextra_colsmay not reuse a namelongwide()needs (a required column, or an output column such asageyorht); that is an error. -
Inclusion filter. If
include_all = TRUE, all rows are kept andinclusion_typesis ignored. Otherwise only rows whosegcr_resultis ininclusion_types(defaultc("Include")) are kept. Aninclusion_typesvalue that is not a version 3 exclusion code (Summary or Detailed, or abin_exclude/tri_excludevalue), such as the 2.x code"Exclude-Carried-Forward", matches nothing, solongwide()warns and points to the code mapping invignette("experienced-start-here"). -
Derived ages. Adds
agey(agedays / 365.25, rounded to 4 places) andagem(agey * 12). -
Sex recode. Parses
sexwith the same parsercleangrowth()uses — the default recognized codes (0/1, m/M, f/F, b/g, male/female, boy/girl; case- and whitespace-insensitive) or the user’ssex_codinglist — and overwritessexwithMorF. A retained row whose sex is missing or unrecognized getsNAsex, andlongwide()warns with the count and up to five unrecognized values (ext_bmiz()cannot compute BMIz for those rows). Pass the samesex_codinggiven tocleangrowth()if you used one, becausecleangrowth()returns sex as entered, with no record of the coding. With the default coding,longwide()makes the same two checks ascleangrowth(), on the wholesexcolumn 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. Passingsex_codingremoves both. -
Param and measurement. Reads
paramthe waycleangrowth()does (its output returnsparamandmeasurementverbatim):param_codinglabels resolve to a canonical parameter, imperial values are converted to metric (HEIGHTIN/LENGTHIN× 2.54,WEIGHTLBS÷ 2.2046226), andLENGTHCMis treated asHEIGHTCM(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 sameparam_codinggiven tocleangrowth()if you used one. Sohtandwtin the output are always cm and kg. -
Unique ids. The function requires unique
ids among the kept height and weight rows and errors on duplicates. -
Height/weight separation and re-merge. Height rows
and weight rows with a non-missing measurement are split out, tagged
with
ht_id/wt_id, then merged onsubjid, agey, agem, agedays, sex.keep_unmatched_datacontrols 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 ashtandwt. -
extra_colshandling. 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 asht_<col>andwt_<col>plus a logicalmatch_<col>column flagging where they agree. - Drops the intermediate
HEIGHTCM/WEIGHTKGcolumns (their values live inht/wt) and gives the subject and age columns back the names passed assubjidandagedays.
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.
- The default inner join silently drops measurements without a
same-day partner of the other parameter. Set
keep_unmatched_data = TRUEto retain them. - Duplicate
ids are a hard error — the wide pivot depends on a unique per-row id. - No optional packages are needed;
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.
simple_bmi(wide_df, wtcol = "wt", htcol = "ht")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.
-
datahas at least age, sex, weight, height, and BMI columns. The default column names (agem,wt,ht,bmi) matchlongwide()+simple_bmi()output; remap via the arguments if different. -
agemust 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). -
sexshould be text:M/F, which is whatlongwide()returns, or another text code that works the same as incleangrowth()(for exampleb/gormale/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 fromcleangrowth()’s numerical 0/1, for historical reasons. The 1/2 coding matches the CDC reference data, CDC software, and thecdcanthropackage. Any other numeric value stops the run. The column must be namedsex. -
wtin kg,htin cm. -
adjust.integer.age— if allagevalues are integers, add 0.5 (placing integer ages at mid-month, as the CDC macro does). Disable by settingFALSE. -
ref.data.path— directory holding the reference file. Empty default loads the packagedinst/extdata/CDCref_d.csv.gzviasystem.file(); override only when developing against a local copy.
Mechanics (high level).
- Checks that the named input columns (and
sex) exist, and warns ifdataalready 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. Copiesdata(dorig) and reads the four named columns plussexinto a separate working table with aseq_row index;dataitself is never modified and its column names are kept. - Optionally bumps integer ages by 0.5 (in the working table only; the returned age column is as entered).
- Stops if
sexhas any numeric value unless the present values are exactly 1 and 2 (see Notes / pitfalls), then normalizessextosexn∈ {1, 2} via first-character matching (logical values are not recognized). - Restricts to ages 24–240 months with at least one of wt/ht present.
- Loads the CDC LMS reference (
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 referencemref/sref). - For ages not present in the reference grid, linearly interpolates
the L/M/S values per sex (
approx()). - Computes LMS z-scores and modified z-scores for weight, height, and
BMI via
z_score()(see below). - Derives percentiles (
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. -
Extended BMIz/percentile. Computes the half-normal
scale parameter
sigma(a sex-specific quadratic in age in years). For rows with LMS BMI percentile ≥ 95, replaces the percentile with90 + 10 * pnorm((bmi - p95) / sigma)and the z-score withqnorm(ebp/100). Where the extended percentile is so close to 100 thatqnormreturns 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. - Renames internal columns to the public output names, selects the
output set, and adds the new metrics to the copy
dorig, matched byseq_so every original row and column is returned in the original order (rows outside the age range getNAmetrics). The input columns keep the user’s names. Variable labels (descriptions such as “Age (months)”) are attached as each column’slabelattribute (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.
- Numerical sex coding (1/2) differs from
cleangrowth()(0/1), soext_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 codedMorF. If 0 or another number means unknown, set it toNA. A warning gives the count of rows aged 24–240 months with a missing or unrecognized sex. - Age units are months, not days or years.
- Rows outside 24–240 months are excluded from the metrics but still
returned (with
NAmetrics) because the result is merged back onto the original rows. - Age 730 days converts to 23.98 months (730 / 30.4375), which is
below 24 months, so a row at agedays 730 gets
NAmetrics even though a child at that age may be exactly 2 years old; metrics begin at day 731. This matches thecdcanthropackage. A warning gives the number of rows at about 729–730 days. - The
qnorm-infinity cap at 8.21 is intentional and matches the CDC macro’s handling. - Dependencies:
data.table,R.utils(exceptextract), andstats::approx/pnorm/qnorm, all from Imports; no optional packages are needed.
Internal helpers (cdc.R, not exported)
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.)
