_pkgdown home block, CITATION, Zenodo
metadata, paper, and top-level vignette openings no longer describe
pft as an "ERS/ATS 2022" implementation; they now present it as
reference-value and interpretation tooling composing GLI, ATS/ERS,
and GOLD standards, with the 2022 ATS/ERS interpretive statement
called out as the most recent standard at the time of the current
release. Function names, output column suffixes, argument defaults,
and provenance citations are unchanged._pkgdown reference-index section "Predecessor 2005
standard" to "Legacy interpretive primitives".R/gold.R and R/constants.R.First CRAN release. CRAN pretest fixes; no user-facing behaviour changes.
pft_dlco_hb_correct() docstring with ASCII (^-1), so the PDF
version of the reference manual builds under LaTeX without special
Unicode support.R (>= 4.1) in the Depends field to match the fact that
pft_volume_subpattern()'s example uses the base-R pipe (|>),
introduced in R 4.1.'ATS', 'ERS', 'GLI') in single
quotes in the DESCRIPTION Description field per CRAN convention.Initial public release. Baseline feature set implementing routine pulmonary function test interpretation under the ERS/ATS 2022 technical standard, with GLI-family reference equations (GLI-2012 and GLI Global 2022 spirometry, GLI-2021 static lung volumes, GLI-2017 diffusion capacity with the 2020 author correction), the ERS/ATS 2022 pattern classifier, severity grading, bronchodilator response, PRISm identification, hemoglobin correction, the FEV1Q survival index, GOLD 1-4 airflow-limitation grading, and Graham 2019 A-F spirometry acceptability grading. Pellegrino et al. 2005 legacy primitives are retained for cross-standard reclassification analyses. Detailed change log for the development history follows.
The default year argument for pft_spirometry(), pft_interpret(),
pft_classify(), pft_prism(), and pft_volume_subpattern() is
now 2022 (the GLI Global / race-neutral equations) — previously
2012. This aligns the package with the current ERS/ATS 2022
technical-standard recommendation and removes the need for a
race column when callers omit year. The combined effect with
the always-suffixed output convention is that
pft_spirometry(d) now emits fev1_pred_2022 (was
fev1_pred), and pft_classify(d) looks up fev1_lln_2022
by default (was fev1_lln). To reproduce the previous behaviour
pass year = 2012 explicitly and include a race column.
Spirometry reference outputs from pft_spirometry() / pft_interpret()
now always carry a four-digit GLI-year suffix: fev1_pred_2012,
fev1_lln_2012, fev1_zscore_2012, ... for GLI 2012, and the
existing _2022 columns for GLI 2022. Previously the default
(2012) emitted unsuffixed columns and only 2022 carried a suffix;
the asymmetry meant flipping a default in the future would
silently change column names of code written against the
unsuffixed convention. The always-suffix scheme makes the source
equation explicit at every column and removes the implicit
"unsuffixed == default" rule.
Lung-volume (Hall 2021) and diffusion (GLI 2017) outputs stay unsuffixed for now — each module currently ships a single standard. The same suffixing convention will be adopted there when competing standards emerge.
Knock-on changes:
pft_classify(), pft_prism(), and pft_volume_subpattern()
gain a year argument (default 2012) that suffixes the
spirometry-derived _lln defaults. Standalone callers should
pass the year matching their upstream reference-function call.pft_interpret() threads year through the internal pipeline
so the helpers above receive it automatically.pft_long()'s year column is now populated for every
spirometry row; volume / diffusion rows remain year = NA until
those modules adopt the convention.print.pft_result annotates the per-measure rows with the
chosen GLI year (e.g. "FEV1 (2012)"). When both _2012 and
_2022 columns are present, the highest year is rendered.pft_plot() deduplicates per measure when both years are
present, picking the highest year so a single lollipop renders
one z-score per measure.Tests, vignettes, and rendered docs have been updated to the new
convention. If you wrote code against fev1_pred /
fev1_zscore / etc., add the _2012 suffix to those names (or
pass column-name overrides to the helpers).
pft_compare(), print.pft_compare(), summary.pft_compare(), and
the pft_plot(type = "compare") mode have been removed. The
GLI 2012 vs GLI Global 2022 reclassification analysis can still be
reproduced by calling pft_interpret(data, year = 2012) and
pft_interpret(data, year = 2022) and computing deltas on the
resulting columns directly. Removed for being outside the package's
core ATS/ERS reference-value + interpretation mission.
pft_plot() has been simplified to the single-patient lollipop
figure; the histogram, trajectory, and bdr modes have been
removed. The signature is now pft_plot(data) with no type
argument. Cohort and longitudinal figures are easier to build
directly from pft_long() output piped into ggplot2.
pft_cohort_summary() has been removed. Its outputs (per-measure
z-score quantiles, ATS pattern frequencies, PRISm prevalence,
diffusion-category frequencies) are easier expressed as plain
dplyr::group_by() |> summarise() calls on a pft_interpret()
result, optionally piped through pft_long() first.
pft_dlco_hb_correct() no longer inspects its hemoglobin
argument for likely-g/dL inputs (the previous behaviour was to
warn and multiply by 10 when any value was below 30). Unit
detection is out of scope for the package; pass g/L directly.
Callers that were relying on the auto-conversion will now get
numerically wrong corrections instead of a warning, so audit any
upstream code that produced this argument.
pft_required_columns() has been removed. It returned a hardcoded
list of column names per function — documentation written as code,
which had to be hand-synced with the actual function signatures. The
same content is now in vignette("input-format").
pft_validate() has been removed. Most of its checks (sex coding,
age and height range, race level membership) duplicated warnings
already emitted by the reference functions' input normalisation,
and the rest (positive-value, FEV1 ≤ FVC) would surface naturally
as NaN z-scores from the LMS power transform. Callers can write
these checks inline against their cohort if they still want them.
pft_glance() and the broom::glance() S3 method on pft_result
have been removed. The function returned four passthrough columns
plus three trivial row-stats (worst_zscore, n_below_lln,
n_above_uln), all of which are easier computed inline from a
pft_long() result. pft_long() and the broom::tidy() S3
method are kept.
pft_interpret() no longer auto-derives fev1fvc_measured from
fev1_measured / fvc_measured (and the analogous
frc_tlc_measured). Trust-the-caller: supply the ratio column
explicitly if you want pattern classification, PRISm, or the
volume sub-pattern stages to run. Note that in ATS/ERS "best test"
workflows the reported ratio comes from a single maneuver and may
not equal fev1 / fvc constructed from best-of-N picks, so the
caller's explicit ratio is the safer source.
pft_classify(), pft_prism(), pft_volume_subpattern(), and
pft_diffusion_interpret() now accept tidy-eval column-name
overrides (bare name, string, or !!var) for every input column,
matching the pattern already used by pft_spirometry(),
pft_volumes(), pft_diffusion(), and pft_interpret(). Defaults
are the canonical pft column names, so callers using the convention
pass no extra arguments. The error message when a required column
is absent now reads "required column(s) missing from input: ..."
(was function-specific wording).pft_diffusion_interpret() gains an SI.units argument matching
pft_diffusion() and pft_interpret(). The previous silent
traditional-vs-SI auto-detect is dropped; SI-units callers must now
pass SI.units = TRUE (or override dlco / kco directly).
Threaded through pft_interpret() so the package-level SI.units
flag now reaches every stage that needs it.pft_classify() now tolerates absence of the tlc / tlc_lln
columns: when either is missing from data, the existing
spirometry-only fallback (Stanojevic 2022 Table 5 / Pellegrino 2005
Fig 2) triggers without raising. Previously a caller working from
spirometry-only data had to add explicit NA TLC columns first.pft_long() pivots a pft_result to long form (one row per
(patient, measure)). The tidy.pft_result() S3 method
dispatches to it when broom is installed.pft_diffusion_interpret(data) assigns a Hughes & Pride 2012
clinical category (Normal / Parenchymal / Volume loss / Mixed /
Vascular / Elevated KCO / Other) from DLCO, VA, KCO z-scores. Run
automatically by pft_interpret() when the diffusion z-scores are
present.pft_volume_subpattern(data) differentiates the six Stanojevic
2022 Figure 10 lung-volume sub-patterns (Normal lung volumes /
Large lungs / Hyperinflation / Simple restriction / Complex
restriction / Mixed disorder). Auto-run by pft_interpret() when
the requisite ratio columns are present.pft_fev1q(fev1, sex, age) implements the FEV1Q adult mortality
index from Stanojevic 2022 Box 3.pft_dlco_hb_correct(dlco, hemoglobin, sex, age) applies the
Cotes 1972 / Stanojevic 2017 hemoglobin correction. Reference Hb
is 146 g/L (males ≥ 15) or 134 g/L (females, males < 15). Hb input
must be in g/L; the function does not detect or convert g/dL (see
the breaking change above).pft_quality() — child age cutoff corrected from age < 6 to
age <= 6 per Graham 2019 Table 10; a 6-year-old is now graded
as a child.pft_quality() — child 10%-of-highest repeatability rule
(Graham 2019 Table 10 footnote) was not applied; now
max(absolute, 0.10 * max(values)) for age <= 6.pft_quality() — sessions with n >= 2 acceptable maneuvers and
best-two diff above all A/C/D thresholds were graded F; now
correctly graded E ("usable but with poor repeatability"). Grade
U ("0 acceptable AND ≥ 1 usable") is not implemented because the
function takes only acceptable maneuvers.pft_gold() — added optional fev1fvc argument enforcing the
GOLD "FEV1/FVC < 0.7" prerequisite (Figure 2.10 header). Default
preserves prior behaviour for existing callers; passing
fev1fvc_measured returns NA for non-obstructed rows instead
of a spurious GOLD grade.rlang, tibble).Pellegrino 2005 interpretive primitives are now available so the
package can serve a cross-standard reclassification analysis
(comparing Stanojevic 2022 against the predecessor algorithm on the
same cohort). All constants and decision logic are verified line-by-
line against the source PDF (papers/pellegrino_2005/); the
extraction is documented in
papers/pellegrino_2005/verification.md.
pft_classify() gains a standard = c("2022", "2005") argument.
The 2022 path is the default and is unchanged. The 2005 path
implements the Pellegrino et al. ERJ 2005 Figure 2 algorithm: it
has four labels (Normal, Obstructed, Restricted, Mixed) -- no
Non-specific category, which was introduced after 2005. Cells
that 2022 labels "Non-specific" are labeled "Restricted" under
2005.pft_severity_2005(pctpred) grades severity from FEV1 percent
predicted into the five Pellegrino bands
(mild / moderate / moderately severe / severe / very severe).pft_bdr_2005(pre, post) applies the dual >=12% AND >=200 mL
criterion from the 2005 standard, without needing the patient's
predicted value.pft_interpret() gains a matching standard = c("2022", "2005")
argument that dispatches all three primitives to the 2005 forms in
one call. year (GLI equation year) and standard (interpretive
rules) are independent -- you can pair GLI 2022 race-neutral
equations with the 2005 interpretive logic, or any other
combination, for nuanced reclassification analyses.pft_spirometry(), pft_volumes(), pft_diffusion(), and
pft_interpret() now accept tidyverse-style column references for
the demographics inputs. Bare names (sex = Sex), strings
(sex = "Sex"), and rlang injection (sex = !!my_var) are all
supported. Defaults match the canonical column names, so existing
code keeps working unchanged. The user's original column names are
preserved in the output.sex value other than "M"
was previously treated as "F" without warning, so a cohort
with "Male" / "Female" / "male" etc. silently produced female
predictions. pft_spirometry(), pft_volumes(), and
pft_diffusion() now soft-correct common variants
("male" -> "M", "Female" -> "F", etc.) with a warning;
truly unrecognised values (e.g. "X", "Unknown") are set to NA
rather than mis-coded.race values are similarly soft-corrected case-insensitively with
whitespace and synonym tolerance ("caucasian" -> "Caucasian",
"white" -> "Caucasian", "black" -> "AfrAm", etc.). All
normalisation findings roll up into a single consolidated warning
per call.sex, age, height columns (or race for GLI 2012) now
error with a clear message listing the expected names, rather than
silently producing all-NA output.data-raw/build_gli_2012.R,
build_gli_2022.R, build_gli_2021_volumes.R, and
build_gli_2017_diffusion.R each read the published lookup-table
workbook (or, where unavailable, the equation table from the article
PDF) and regenerate the corresponding .RData and CSV artifacts.R/spirometry.R, R/lung_volumes.R, R/diffusion_capacity.R, and
R/ats_classification.R now carry @references to the source papers
(and the 2020 author correction for the diffusion equations).data-raw/coeffs_spline_spiro.RData: an orphan blob holding
GLI 2012 polynomial coefficients for ages 25-95, which the package
never used (the lookup-table approach in R/spirometry.R interpolates
directly from spline values instead).pft_quality(values, age) grades a set of acceptable spirometry
maneuvers A-F per the Graham et al. ATS/ERS 2019 spirometry
standardization update (doi:10.1164/rccm.201908-1590ST). Tighter
repeatability thresholds applied for children under 6.pft_gold(fev1_pctpred) returns the GOLD COPD severity grade (1-4)
from FEV1 % predicted.pft_severity(zscore) returns one of "normal", "mild",
"moderate", "severe" per the Stanojevic et al. ERJ 2022 z-score
cut points (>= -1.645, > -2.5, > -4, <= -4).pft_bdr(pre, post, predicted) classifies BDR per
the 2022 criterion (>10% change relative to predicted, replacing the
earlier 12%/200 mL rule).pft_prism(data) adds a prism logical column flagging
Preserved Ratio Impaired Spirometry (FEV1 below LLN, FEV1/FVC at or
above LLN). Requires only spirometry; does not need TLC.pft_change(z1, z2, r) computes the conditional change
z-score recommended by Stanojevic 2022 for interpreting serial PFT
measurements over time. Configurable autocorrelation r.pft_interpret(data) is a single-call workflow that auto-detects
which inputs are present and emits a complete Stanojevic
2022-compliant interpretation: reference values, z-scores, percent
predicted, severity grading, ATS pattern, PRISm flag, and
bronchodilator response. This is the recommended entry point for
clinical-style reporting.pft_plot(result) generates a clinical-style z-score lollipop plot
with severity-band shading. Requires ggplot2 (Suggests).pft_spirometry(), pft_volumes(), and pft_diffusion()
now optionally compute z-scores and percent predicted. Supply a
<measure>_measured column in the input data frame (e.g.
fev1_measured, frc_measured, dlco_measured) and the function
appends <measure>_zscore and <measure>_pctpred columns alongside
the existing <measure>_pred / <measure>_lln / <measure>_uln.
Backwards compatible: callers who only supply demographics continue
to get the three existing reference-value columns and nothing else.((measured/M)^L - 1) / (L*S); percent
predicted is (measured / M) * 100. Both propagate NA from the
measured value, the LMS parameters, or the LLN as expected.tests/testthat/gli_2022_oracle.csv covers z-score and
percent predicted as well as predicted and LLN, validated at
tolerance 1e-8.pft_spirometry(), pft_volumes(), and
pft_diffusion() previously crashed with a "missing value where
TRUE/FALSE needed" error when any row had a missing value (NA) in
sex, age, or height. They now skip such rows and emit NA for
the reference values on that row, matching the behaviour already
provided for missing race (spirometry) and for pft_classify().
Real clinical PFT data routinely contains missing demographics; the
prior behaviour required callers to filter NAs themselves.ANNN and NANN pattern
labels were inverted relative to Stanojevic et al. ERJ 2022 Figure 8.
The classifier now correctly returns:
ANNN (isolated low FEV1) → "Normal"NANN (isolated low FVC) → "Non-specific"
Previously these two were swapped. The change re-labels patients
whose spirometry profile is "isolated low FEV1 + everything else
normal" (previously "Non-specific", now "Normal") and vice versa for
isolated low FVC. See notes/ats_classification_label_fix.md for the
clinical-review memo.fev1_lln instead of fvc_lln. Combined with the label-swap above,
patients with FVC slightly below their FVC LLN but above their FEV1
LLN are now correctly routed to "Non-specific" rather than being
silently labelled "Normal".ats_pattern_combination
output column. (Initial expansion: previous internal release; bug
fixes: this release.)NA-propagation tests, out-of-range
tests, structural / column-contract tests, and a clinical-scenario
suite for ats_classification grounded in Stanojevic 2022 Figure 8 /
Table 5 / Table 8.tests/testthat/gli_2022_oracle.csv covering predicted,
LLN, z-score, and percent predicted for FEV1, FVC, and FEV1/FVC.
Regenerated via data-raw/build_gli_2022_oracle.R (see that script
for provenance). Only the static CSV ships; no test-time dependency
on any external package.inst/CITATION so citation("pft") returns the package and the
underlying reference papers as bibentry objects.DESCRIPTION and
shipped via LICENSE / LICENSE.md.notes/ats_classification_label_fix.md records the rationale and
clinical-review questions for the ATS pattern-label changes above.renv for dependency management.papers/
but are excluded from git and from the R CMD build tarball (they
are copyrighted publisher content).notes/ (clinical-review memos and similar) is also Rbuildignored.
docs/ is reserved for the pkgdown-built site (gitignored; built and
deployed to the gh-pages branch by .github/workflows/pkgdown.yaml).