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
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
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).