Technical reference: algorithm functions

2026-11-08

This document is the reference for growthcleanr’s core algorithm functions — the shared computations the cleaning steps rely on: the z-scores, EWMA, recentering, gestational-age correction, and the unit-error-range / CSD-z helper family. (The step-by-step algorithm logic itself lives in vignette("child-algorithm") and vignette("adult-algorithm"); the non-algorithm helper functions live in vignette("utilities").) It covers every z-score computed by growthcleanr version 3, how each is calculated, which reference tables are used, and what is returned by cleangrowth().

growthcleanr uses two fundamentally different z-score methods for different purposes. Understanding the distinction is essential to avoid misinterpreting detailed output and is necessary to replicate calculations.


1. Two Z-Score Methods

Feature CSD Method LMS Method
Used for Main cleaning algorithm (sd.orig, tbc.sd) Extended BMI only (ext_bmiz); not computed by the cleaning algorithm
Formula (X - M) / csd ((X/M)^L - 1) / (L*S)
Effect at extremes Separate SDs below and above the median (csd_neg/csd_pos), linear within each side, so the upper tail is not compressed Accounts for skewness via L; compressed z-scores at upper tail for right-skewed params (e.g., weight)
Why used for cleaning Large weight in a very heavy child produces a larger z-change, making implausible values easier to detect
Intermediate rounding None (removed in v3 — see §8) None (removed in v3 — see §8)

2. CSD Method — sd.orig

The cleaning algorithm uses the Conditional Standard Deviation (CSD) method. This divides the difference from the median by a half-SD derived from the reference distribution, rather than the full LMS-adjusted SD. This is the same approach the CDC uses for its modified z-scores, described in:

> Kuczmarski RJ, Ogden CL, Guo SS, et al. 2000 CDC growth charts for the United States: Methods and development. National Center for Health Statistics. Vital Health Stat 11(246). 2002. PDF

Formula

If measurement < M:   sd.orig = (measurement - M) / csd_neg
If measurement >= M:  sd.orig = (measurement - M) / csd_pos

For linear measurements, measurement here includes any z-score-only recumbent/standing offset (length.adjust, or the gestational-age window under the default); see vignette("wrapper"), Phase 8.

Where:

The csd_pos and csd_neg values are pre-computed and stored in the reference tables as columns csdpos and csdneg.

Reference tables

Table Source Used for
growthfile_who.csv.gz WHO 2006 HEIGHTCM/WEIGHTKG age < 5y, all HEADCM
growthfile_cdc_ext_infants.csv.gz CDC 2000 (weight, height, head circumference); CDC 2022 extended BMI charts (Wei et al., 2020) (BMI) HEIGHTCM/WEIGHTKG age >= 2y

Age blending — child algorithm (default, version 3)

The child algorithm computes sd.orig_who and sd.orig_cdc separately, then blends them based on age and parameter:

Age range Parameter Formula
< 2 years any sd.orig = sd.orig_who
2–5 years HEIGHTCM, WEIGHTKG sd.orig = (sd.orig_cdc * (age_yr - 2) + sd.orig_who * (5 - age_yr)) / 3
> 5 years HEIGHTCM, WEIGHTKG sd.orig = sd.orig_cdc
any age HEADCM sd.orig = sd.orig_who

At the boundaries (exactly 2y or 5y), the formula yields 100% WHO and 100% CDC respectively (both arms equal at the boundary).

The gradual transition from WHO to CDC values is described in:

> Daymont C, Hwang W, Paul IM, Shur N, Freedman DS. Creation and Evaluation of New Growth Charts With a Gradual Transition From WHO to CDC Values. Pediatrics. 2025 Sep 1;156(3):e2025070697. doi: 10.1542/peds.2025-070697. PMID: 40819831.

sd.orig_who, sd.orig_cdc, and the blended sd.orig are not rounded at intermediate steps in version 3 — see §8.

Age blending — calc_and_recenter_z_scores() (Step 15/16 plus/minus perturbation)

This internal helper recomputes recentered z-scores for the p_plus / p_minus perturbed measurement values used by the Step 15/16 (moderate-EWMA / trajectory) steps. It uses the same blending formula, WHO/CDC tables, and age windows as the main z-score path — WHO-only < 2y, blend 2–5y ÷ 3, CDC-only > 5y — so the perturbed z-scores are on the same footing as tbc.sd:

Age range Parameter Formula
< 2 years any WHO only
2–5 years HEIGHTCM, WEIGHTKG (cdc*(age-2) + who*(5-age))/3
> 5 years HEIGHTCM, WEIGHTKG CDC only
any age HEADCM WHO only

3. LMS Method (not used by the cleaning algorithm)

The cleaning algorithm computes no LMS z-score. No CDC-only Box-Cox z-score (z.orig) is calculated or returned by cleangrowth(). The standard LMS formula is used only in the standalone ext_bmiz() extended-BMI tool (§7):

If L != 0:  z = ((X / M)^L - 1) / (L * S)
If L == 0:  z = log(X / M) / S

If you need an age-blended LMS z-score for analysis (the conventional growth z-score, distinct from the CSD scores the algorithm uses), compute it separately — see §10.


4. Recentered Scores — tbc.sd and ctbc.sd

The cleaning algorithm works on recentered z-scores so that outlier detection is age-stable: the population-median CSD z-score drifts with age, so subtracting a per-agedays median (computed separately for each param and sex) puts every measurement on a flatter footing before trajectory checks. By default the median comes from a fixed precomputed file (see below); it can instead be turned off (sd.recenter = "none") or supplied directly by the caller.

tbc.sd (recentered blended CSD)

tbc.sd = sd.orig - rcmedian.sd

Where rcmedian.sd is the population-median offset looked up by (param, sex, agedays). Its source is set by the sd.recenter argument to cleangrowth():

Recentering exists because the population-median CSD z-score drifts with age; subtracting the per-param/sex reference median puts the cleaning thresholds on a flatter, age-stable footing. (See vignette("wrapper"), Phase 11, for how the join is done; see vignette("recentering") for how the packaged reference medians were derived.)

ctbc.sd (corrected recentered CSD)

Same formula, but uses sd.corr (gestational-age-corrected z-score for preterm infants) instead of sd.orig:

ctbc.sd = sd.corr - rcmedian.sd

Equal to tbc.sd unless the subject’s gestational-age correction is kept.


5. Gestational-Age-Corrected Scores — sd.corr

For preterm infants, growthcleanr applies a gestational-age correction using gestational-age reference values (see vignette("wrapper") → “Gestational-age reference values”).

Column Description
sd.corr CSD z-score after gestational-age correction

The gestational-age correction writes a separate column (sd.corr); it does not overwrite sd.orig with a corrected score, so sd.orig remains the uncorrected reference CSD z-score. One exception: under the default, Child Step 2b rescores sd.orig 0.7 cm lower for height rows in the gestational-age window (vignette("wrapper"), Phase 8). PIV (Child Step 7) and CF rescue (Child Step 6) therefore read sd.orig directly as the uncorrected score. When no correction applies (term infants, older children), sd.corr = sd.orig.


6. EWMA (Exponentially Weighted Moving Average)

The EWMA is the central mechanism for detecting implausible values. It is computed from recentered CSD z-scores (tbc.sd).

Weight formula

weight_i = (5 + |agedays_current - agedays_i|) ^ exponent

EWMA variants (internal — not in output)

Variable Excludes
ewma.all Only the current observation
ewma.before Current + immediately prior observation
ewma.after Current + immediately subsequent observation

The deviation from EWMA is dewma = tbc.sd - ewma.

EWMA values are not included in cleangrowth() output. They are computed internally during each algorithm iteration and discarded.


7. Extended BMI Z-Scores — ext_bmiz()

The standalone ext_bmiz() function computes extended BMI z-scores for children 2–19.9 years, following the CDC’s extended BMI method.

For children with BMI at or below the 95th percentile, the extended z-score equals the standard LMS z-score. For children above the 95th percentile, a half-normal distribution with a fitted sigma parameter is used:

Sigma formula (scale parameter)

Males:   sigma = 0.3728 + 0.5196 * age_yr - 0.0091 * age_yr^2
Females: sigma = 0.8334 + 0.3712 * age_yr - 0.0011 * age_yr^2

Where age_yr = age in years (= age in months / 12).

Extended BMI percentile

For BMI <= 95th percentile:  ebp = 100 * pnorm(bz)        [standard LMS]
For BMI > 95th percentile:   ebp = 90 + 10 * pnorm((BMI - p95) / sigma)

The extended BMI z-score is then qnorm(ebp / 100). If ebp > 99.99999999999999, extended BMIz is capped at 8.21.

Reference: CDC SAS macro (updated December 2022); NCHS report doi: 10.15620/cdc:121711. 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.


8. Rounding

Intermediate z-score rounding has been removed in version 3. sd.orig_who, sd.orig_cdc, the blended sd.orig, tbc.sd, and ctbc.sd are all carried at full numeric precision throughout the cleaning pipeline. The previous Stata-style rounding to 0.001 (half away from zero) existed only for cross-platform Stata-vs-R validation and has been removed along with the round_stata() helper; it was never needed by the algorithm itself. Downstream consumers can round the returned columns to whatever precision they like.


9. Columns Returned by cleangrowth()

The full return-value contract — which columns are always returned, which are gated behind options (debug, cf_detail, corr_detail, etc.), and what each can contain — is documented in vignette("output") (the single source of truth).

The z-scores described in this document (sd.orig, tbc.sd, sd.corr, ctbc.sd, and the plus/minus perturbation variants) are not in the default output; they are returned only under debug = TRUE (or full_detail = TRUE), and on child rows only. See vignette("output") §7.


10. Key Distinctions for Downstream Use

sd.orig vs a standard LMS z-score

sd.orig uses CSD, which is not the conventional z-score for growth research. It is intentionally linear in the measurement (no compression of the upper tail), which makes the algorithm more sensitive to implausibly high values. Do not use sd.orig or tbc.sd as population-referenced z-scores in analyses; compute LMS z-scores separately.

Computing a blended LMS z-score for analysis

growthcleanr does not produce an LMS z-score. If you need the conventional age-blended LMS z-score for analysis, apply the LMS formula yourself using the same WHO/CDC blend the cleaning algorithm uses for its CSD scores (WHO-only < 2y, weighted blend 2–5y ÷ 3, CDC-only > 5y). Standard research practice blends WHO (younger children) and CDC (older children) this way rather than using a CDC-only reference.

EWMA output

EWMA/DEWMA values are computed internally and not returned. If you need DEWMA for quality measures, compute it independently using your preferred z-score input (typically blended LMS z-scores).


11. Reference Table Summary

File Location Content
growthfile_who.csv.gz inst/extdata/ WHO 2006 L/M/S + csdpos/csdneg
growthfile_cdc_ext_infants.csv.gz inst/extdata/ CDC 2000 (weight, height, head circumference) and CDC 2022 extended BMI L/M/S + csd columns
CDCref_d.csv.gz inst/extdata/ CDC reference for ext_bmiz()
who_hc_vel_3sd_infants.csv.gz inst/extdata/ WHO HC velocity thresholds (infant table; only ≤ 24 mo is used — see the velocity step in vignette("child-algorithm"))
who_hc_maxvel_3sd_infants.csv.gz inst/extdata/ WHO HC max velocity thresholds (infant table; only ≤ 24 mo is used)
Fenton tables inst/extdata/ Approximated Fenton CSD values and approximated Fenton median weights by gestational age, for gestational-age correction of preterm infants (see vignette("wrapper") → “Gestational-age reference values”)

Unit-Error-Range and CSD-z Helper Reference (child + adult)

Single shared reference for the unit-error-range helper family and the small adult CSD-z reference it depends on. These helpers span both algorithms: the child algorithm and the adult algorithm each compute an informational unit_error_range output column through the same band-logic helper (.unit_error_range_flag()), and the adult algorithm additionally uses its CSD-z helpers inside Evil Twins (Adult Step 9Wa) for the UER3 unit-error split and the median-band |z| tiebreak. Because the family is cross-algorithm, it is documented here rather than duplicated in the child and adult narratives; both narratives’ Support Functions sections point here.

This document describes what the current R code does. It is a debugging aid and a long-term technical reference. References to code use function names rather than line numbers, because line numbers go stale with each edit.


What unit_error_range is

unit_error_range is a default-ON, informational logical output column (parameter unit_error_range = TRUE; see vignette("configuration") and vignette("output")). It never affects exclude and is not a member of exclude.levels. For each row it answers a single question:

Is this measurement’s magnitude consistent with a metric/imperial unit-conversion error (the value was recorded in the wrong unit system), AND is it outside the plausible z = −3 to +3 band?

TRUE flags values that look like they could be a unit error and are not already within the plausible range. It is purely a magnitude/range test — it does not check the rest of the trajectory, and it makes no exclusion decision. It is set to FALSE (not evaluated) for rows with the internal codes Exclude-Missing, Exclude-Not-Cleaned, and Exclude-Not-GC-Param (internal names; for the reported codes, see vignette("exclusion-codes")) and for rows with missing age (see the .compute_unit_error_range() dispatch below).

The same flag is also computed internally during cleaning — in child Evil Twins (Child Step 9) and adult Evil Twins (Adult Step 9Wa) — as one input to the UER3 unit-error-split selection logic. The output column and the internal use share one helper.

cleangrowth() also uses this flag for a systematic-unit-error warning: when more than 20% of a parameter group’s cleaned (evaluated) values are flagged TRUE, it emits a soft warning() that the whole column may be in the wrong unit (e.g. weight recorded in pounds but labeled WEIGHTKG). The warning also notes that a high share may instead reflect people with atypical growth (for example, extremely premature infants, severe obesity, some medical conditions), whose real values can fall in the unit-error range. The share is computed per parameter group (weight / height / head circumference) over the same evaluated rows the flag applies to. It is advisory only — no minimum sample size and no effect on exclude.

File locations (why the family is split across two files)

Helper File Why there
.unit_error_range_flag() (shared band logic) adult_support.R Co-located with adult Evil Twins, which consumes it; the child side reaches it via the package namespace.
.adult_csd_ref(), .adult_csd_lookup(), .adult_z3(), .adult_csd_z() adult_support.R Adult-only CSD reference; also needed by the standalone adult test harness, which sources only the adult files.
.child_z3() (child zn3/zp3 lookup) child_clean.R Child reference-table lookup.
.compute_unit_error_range() (child + adult output assembler) child_clean.R Runs at cleangrowth() output assembly over the full (child + adult) result.

The header comments at adult_support.R (above .adult_csd_ref()) and child_clean.R (above .child_z3()) both state this split explicitly.


The shared band logic: .unit_error_range_flag(param, value, zn3, zp3)

Purpose. Direction-agnostic unit-error-range test. Given each row’s param, its metric measurement value, and the plausible-band endpoints zn3/zp3 (the measurements at recentered z = −3 and z = +3 for that row’s reference), it builds the two unit-error bands and the plausible carve-out and returns a logical vector.

The two unit-error bands. A value is “unit-error-shaped” if, had it actually been recorded in the other unit system, it would have landed in the plausible band. Two directions:

The band endpoints are the plausible endpoints scaled by the conversion factor for the param:

Both unit-error bands are exclusive (value > pmin(...) & value < pmax(...), using pmin/pmax so the band orients correctly regardless of endpoint order).

The plausible carve-out range_3. The inclusive interval value >= zn3 & value <= zp3. A value already inside the plausible band is never flagged, even if it also happens to fall in a unit-error band.

Return. TRUE iff (in range_im OR in range_mi) AND NOT in range_3. NA endpoints (no reference lookup matched, e.g. HEADCM out of cleaning range) → FALSE (res[is.na(res)] <- FALSE).

Callers. Child Step 9 Evil Twins (builds the internal uer column), the child+adult output assembler .compute_unit_error_range(), and adult Evil Twins (evil_twins() in adult_support.R, as sp_uer).


Child side

.child_z3(param, sex, agedays, ref.data.path = "")

Purpose. The child interface that the .unit_error_range_flag() band plugs into: returns, per row, the recentered (uncorrected) z = −3 / z = +3 measurements (zn3, zp3) for child rows. These are looked up — not computed at runtime — from the reference table inst/extdata/child_z3_reference.csv.gz, keyed by (param, sex, agedays).

Inputs/return. Equal-length param / sex / agedays vectors; output preserves input row order (a .ord sequence is merged through and re-sorted). Returns list(zn3, zp3). ref.data.path = "" reads the installed package copy via system.file(); a non-empty path reads child_z3_reference.csv.gz from there (for testing / non-installed use).

Returns NA where no lookup row matches — notably HEADCM beyond the 5-year HC cleaning range (the table has no rows there), which then yields FALSE from the flag.

Code location. child_clean.R, function .child_z3().

.compute_unit_error_range(dt, adult_cutpoint, ref.data.path = "")

Purpose. Assembles the public unit_error_range column over the full result (child + adult rows together), at cleangrowth() output assembly. Dispatches the zn3/zp3 lookup by row type, then calls the shared .unit_error_range_flag().

Dispatch.

Inputs. dt must carry param, sex, agedays, v (the metric measurement), and exclude. Returns the logical vector for the whole table.

Caller. cleangrowth() output assembly, all_results[, unit_error_range := .compute_unit_error_range(...)], gated on the unit_error_range parameter.

Code location. child_clean.R, function .compute_unit_error_range().


Adult side

The adult algorithm computes no recentered z-scores. For both the unit-error band endpoints and the Evil Twins CSD work it uses a small fixed sex-specific NHANES reference with two-sided conditional SDs.

.adult_csd_ref()

Purpose. The single named source of the adult CSD constants — sex-specific median M and below-/above-median SDs (SD_neg, SD_pos) for adult HEIGHTCM and WEIGHTKG. Kept as one object so the constants are never re-entered inline in a z-score function.

Provenance. Constants are from NHANES 2017–2020 + 2021–2023 (unweighted, ages 18+). The hard-coded values are:

param sex M SD_neg SD_pos
HEIGHTCM 1 (F) 160.3 7.1 7.1
HEIGHTCM 0 (M) 174.1 7.8657 7.7
WEIGHTKG 1 (F) 74.0 13.75 29.9
WEIGHTKG 0 (M) 85.2 14.65 29.6

No HC (adults have none). sex: 0 = male, 1 = female.

sex = NA fallback. A sex = NA row is appended per param as the simple mean of the male and female constants, so adults whose sex is missing/unrecognized (coerced to NA at input) get a sensible reference instead of NA z-scores. This is a derived fallback computed at runtime, not a re-derived pooled-sex NHANES distribution — the M/F constants in the table above are the values of record. Computed in code (not hard-coded) so it stays in sync if the empirical M/F constants ever change.

Code location. adult_support.R, function .adult_csd_ref().

.adult_csd_lookup(param, sex)

Purpose. Look up (M, SD_neg, SD_pos) per (param, sex). NA-safe: sex = NA resolves to the averaged fallback row. Uses a -1 integer sentinel for NA sex in both the query and the reference (.sexkey) so the merge does not depend on NA-matching join semantics. Returns NA for params not in the adult reference (e.g. HEADCM). Equal-length vector inputs; output preserves input order (via a .ord re-sort).

Code location. adult_support.R, function .adult_csd_lookup().

.adult_z3(param, sex)

Purpose. Adult band endpoints: zn3 = M − 3·SD_neg, zp3 = M + 3·SD_pos. NA-safe via .adult_csd_lookup(); NA for params not in the reference. This is the adult analogue of .child_z3() — it is what .compute_unit_error_range() calls for adult rows. Returns list(zn3, zp3).

Code location. adult_support.R, function .adult_z3().

.adult_csd_z(param, sex, value)

Purpose. Per-value two-sided conditional-SD z: z = (value − M) / SD_neg if value < M, else (value − M) / SD_pos. NA-safe via .adult_csd_lookup(); NA for params not in the reference. Used only inside adult Evil Twins — for the UER3 plausible-group test (which group has |z| < 3) and the median-band |z| tiebreak. It does not feed the unit_error_range output column (that path uses .adult_z3() endpoints + .unit_error_range_flag(), not a per-value z).

Callers. evil_twins() in adult_support.R.

Code location. adult_support.R, function .adult_csd_z().


How the pieces fit

unit_error_range OUTPUT column (default ON):
  .compute_unit_error_range(dt, adult_cutpoint)         [child_clean.R]
      ├── child rows → .child_z3()  → zn3/zp3 from child_z3_reference.csv.gz
      ├── adult rows → .adult_z3()  → zn3/zp3 = M ± 3·SD   [adult_support.R]
      └── .unit_error_range_flag(param, v, zn3, zp3)       [adult_support.R]

INTERNAL use during cleaning (one input to the unit-error split):
  child Step 9 Evil Twins:  .child_z3() + .unit_error_range_flag()  → uer
  adult Step 9Wa Evil Twins: .adult_z3() + .unit_error_range_flag() → sp_uer
                             .adult_csd_z() → UER3 group split + |z| tiebreak

Two distinct uses of the adult CSD reference. The band endpoints (.adult_z3()) feed both the output column and the adult ET unit-error band; the per-value z (.adult_csd_z()) is ET-internal only. Both draw their constants from the single .adult_csd_ref() object.


Notes for maintainers