# Getting started with pft

`pft` implements the Stanojevic et al. ERJ 2022 ERS/ATS interpretive strategy for pulmonary function tests: reference values, z-scores, percent predicted, ATS pattern classification, severity grading, bronchodilator response, PRISm screening, and conditional change scores, all from a data-frame-pipeline API.

The sections below run the pipeline on a single patient and then on a small cohort.

    library(pft)

# 1. Reference values from demographics alone

The simplest call: pass age, sex, height (and race for GLI 2012) and get predicted values, lower limits of normal (LLN), and upper limits of normal (ULN) for every measure.

    patient <- data.frame(
      sex = "M", age = 45, height = 178
    )

    ref <- pft_spirometry(patient)
    ref[, c("fev1_pred_2022", "fev1_lln_2022", "fev1_uln_2022",
            "fvc_pred_2022",  "fvc_lln_2022",  "fvc_uln_2022")]
    #> # A tibble: 1 × 6
    #>   fev1_pred_2022 fev1_lln_2022 fev1_uln_2022 fvc_pred_2022 fvc_lln_2022
    #>            <dbl>         <dbl>         <dbl>         <dbl>        <dbl>
    #> 1           3.87          2.94          4.75          4.81         3.68
    #> # ℹ 1 more variable: fvc_uln_2022 <dbl>

The default is GLI 2022 (“GLI Global”), the race-neutral equation set recommended by the ERS/ATS 2022 standard. To use the predecessor GLI 2012 multi-ethnic equations, pass `year = 2012` and include a `race` column.

The same pattern works for lung volumes and diffusion:

    pft_volumes(patient)[, c("frc_pred", "tlc_pred", "rv_pred", "vc_pred")]
    #> # A tibble: 1 × 4
    #>   frc_pred tlc_pred rv_pred vc_pred
    #>      <dbl>    <dbl>   <dbl>   <dbl>
    #> 1     3.39     7.21    1.72    5.50
    pft_diffusion(patient)[, c("dlco_pred", "kco_tr_pred", "va_pred")]
    #> # A tibble: 1 × 3
    #>   dlco_pred kco_tr_pred va_pred
    #>       <dbl>       <dbl>   <dbl>
    #> 1      30.3        4.58    6.67

# 2. Z-scores and percent predicted from measured values

Add `<measure>_measured` columns and z-scores and percent-predicted appear automatically next to the reference values.

    patient_with_measurements <- data.frame(
      sex = "M", age = 45, height = 178, race = "Caucasian",
      fev1_measured = 2.5,
      fvc_measured  = 3.8
    )

    out <- pft_spirometry(patient_with_measurements)
    out[, c("fev1_pred_2022", "fev1_zscore_2022", "fev1_pctpred_2022",
            "fvc_pred_2022",  "fvc_zscore_2022",  "fvc_pctpred_2022")]
    #> # A tibble: 1 × 6
    #>   fev1_pred_2022 fev1_zscore_2022 fev1_pctpred_2022 fvc_pred_2022
    #>            <dbl>            <dbl>             <dbl>         <dbl>
    #> 1           3.87            -2.39              64.6          4.81
    #> # ℹ 2 more variables: fvc_zscore_2022 <dbl>, fvc_pctpred_2022 <dbl>

The z-score uses the standard LMS formula `((measured / M)^L - 1) / (L * S)`. Percent predicted is `(measured / M) * 100`.

# 3. Severity grading

`pft_severity()` maps a z-score to one of four categories per the Stanojevic 2022 cut points:

    pft_severity(c(0, -1.7, -3, -5))
    #> [1] "normal"   "mild"     "moderate" "severe"

You can grade any z-score column directly:

    out$fev1_severity_2022 <- pft_severity(out$fev1_zscore_2022)
    out$fvc_severity_2022  <- pft_severity(out$fvc_zscore_2022)
    out[, c("fev1_zscore_2022", "fev1_severity_2022", "fvc_zscore_2022", "fvc_severity_2022")]
    #> # A tibble: 1 × 4
    #>   fev1_zscore_2022 fev1_severity_2022 fvc_zscore_2022 fvc_severity_2022
    #>              <dbl> <chr>                        <dbl> <chr>
    #> 1            -2.39 mild                         -1.47 normal

# 4. ATS pattern classification

Given measured spirometry plus TLC and their LLNs, `pft_classify()` labels the pattern per Stanojevic 2022 Figure 8:

    classification_input <- data.frame(
      fev1 = 2.5,  fev1_lln_2022 = 3.0,
      fvc  = 3.8,  fvc_lln_2022  = 3.5,
      fev1fvc = 0.66, fev1fvc_lln_2022 = 0.70,
      tlc  = 6.0,  tlc_lln = 5.0
    )
    pft_classify(classification_input)[
      , c("ats_classification", "ats_pattern_combination")
    ]
    #> # A tibble: 1 × 2
    #>   ats_classification ats_pattern_combination
    #>   <chr>              <chr>
    #> 1 Obstructed         ANAN

The 4-character `ats_pattern_combination` records which inputs drove the label (A = abnormal / below LLN, N = at or above LLN), in the order FEV1, FVC, FEV1/FVC, TLC. `ANAN` above means FEV1 and FEV1/FVC are low; FVC and TLC are normal.

# 5. Bronchodilator response

The Stanojevic 2022 BDR criterion is a >10% change relative to predicted in FEV1 or FVC (replacing the 2005 12% / 200 mL rule):

    pft_bdr(pre = 2.5, post = 3.0, predicted = 4.0)
    #> # A tibble: 1 × 2
    #>   pct_pred_change is_significant
    #>             <dbl> <lgl>
    #> 1            12.5 TRUE

# 6. PRISm screening

Preserved Ratio Impaired Spirometry: low FEV1 with normal FEV1/FVC. Spirometry-only; no TLC needed.

    pft_prism(data.frame(
      fev1    = 2.0,  fev1_lln_2022    = 2.5,
      fvc     = 2.6,  fvc_lln_2022     = 3.0,
      fev1fvc = 0.80, fev1fvc_lln_2022 = 0.70
    ))
    #> # A tibble: 1 × 7
    #>    fev1 fev1_lln_2022   fvc fvc_lln_2022 fev1fvc fev1fvc_lln_2022 prism
    #>   <dbl>         <dbl> <dbl>        <dbl>   <dbl>            <dbl> <lgl>
    #> 1     2           2.5   2.6            3     0.8              0.7 TRUE

# 7. Serial change

For longitudinal monitoring, the conditional change score (CCS) adjusts for regression to the mean using a within-subject z-score autocorrelation `r`. `|CCS| > 1.96` (the Stanojevic 2022 two-sided 95% threshold) indicates a change outside the normal-limits range.

    # z dropped from -0.5 to -2.5 over 1 year; r ≈ 0.7 for adult FEV1
    pft_change(z1 = -0.5, z2 = -2.5, r = 0.7)
    #> # A tibble: 1 × 3
    #>     ccs r_used is_significant
    #>   <dbl>  <dbl> <lgl>
    #> 1 -3.01    0.7 TRUE

# 8. The one-call workflow

`pft_interpret()` auto-detects every available input and produces the full Stanojevic 2022-compliant interpretation in one call:

    patient <- data.frame(
      sex = "M", age = 45, height = 178, race = "Caucasian",
      fev1_measured     = 2.5,
      fvc_measured      = 3.8,
      fev1fvc_measured  = 2.5 / 3.8,
      tlc_measured      = 6.0,
      fev1_pre          = 2.5,
      fev1_post         = 2.9
    )
    result <- pft_interpret(patient)

    # A high-level subset of the ~60 columns generated:
    result[, c("fev1_pred_2022", "fev1_zscore_2022", "fev1_severity_2022",
               "fvc_zscore_2022", "fvc_severity_2022",
               "ats_classification", "prism",
               "fev1_bdr_pct", "fev1_bdr_significant")]
    #> <pft_result>
    #>  Measure     Pred  Measured Z     Severity
    #>  FEV1 (2022)  3.87 -        -2.39 mild
    #>
    #> Pattern: Obstructed
    #> PRISm: FALSE
    #> BDR FEV1: TRUE ( 10.3% of predicted)
    #>
    #> Use `as_tibble(x)` or `as.data.frame(x)` for the full output (9 columns).

# 9. Visualisation

`pft_plot()` produces a clinical-style z-score lollipop figure with severity bands. Requires `ggplot2` (Suggests).

    pft_plot(result)

# 10. Cohort analyses

Everything composes naturally in a pipeline. Apply `pft_interpret()` to a multi-row data frame and the output is the same data frame with ~60 interpretation columns appended:

    cohort <- data.frame(
      sex    = c("M", "F", "M"),
      age    = c(45, 60, 30),
      height = c(178, 165, 175),
      race   = c("Caucasian", "AfrAm", "Caucasian"),
      fev1_measured = c(2.5, 1.8, 4.0),
      fvc_measured  = c(3.8, 2.4, 5.2),
      fev1fvc_measured = c(2.5/3.8, 1.8/2.4, 4.0/5.2),
      tlc_measured  = c(6.0, 4.5, 6.8)
    )

    interpreted <- pft_interpret(cohort)
    interpreted[, c("sex", "age",
                    "fev1_zscore_2022", "fev1_severity_2022",
                    "ats_classification", "prism")]
    #> # A tibble: 3 × 6
    #>   sex     age fev1_zscore_2022 fev1_severity_2022 ats_classification prism
    #>   <chr> <dbl>            <dbl> <chr>              <chr>              <lgl>
    #> 1 M        45           -2.39  mild               Obstructed         FALSE
    #> 2 F        60           -1.58  normal             Normal             FALSE
    #> 3 M        30           -0.122 normal             Normal             FALSE

# 11. Long-form tidier for downstream analysis

`pft_long()` pivots a wide `pft_result` into one row per `(patient, measure)`, the natural shape for `dplyr` / `ggplot2` workflows.

    pft_long(interpreted)[1:6, ]
    #> # A tibble: 6 × 10
    #>   .patient measure year   pred   lln   uln measured zscore pctpred severity
    #>      <int> <chr>   <chr> <dbl> <dbl> <dbl>    <dbl>  <dbl>   <dbl> <chr>
    #> 1        1 fev1    2022   3.87  2.94  4.75      2.5 -2.39     64.6 mild
    #> 2        2 fev1    2022   2.47  1.77  3.12      1.8 -1.58     73.0 normal
    #> 3        3 fev1    2022   4.07  3.16  4.93      4   -0.122    98.4 normal
    #> 4        1 fvc     2022   4.81  3.68  5.95      3.8 -1.47     79.1 normal
    #> 5        2 fvc     2022   3.10  2.25  3.97      2.4 -1.36     77.4 normal
    #> 6        3 fvc     2022   4.87  3.79  5.96      5.2  0.497   107.  normal

The S3 method `tidy.pft_result()` dispatches to it when `broom` is installed, so `broom::tidy(interpreted)` is identical to `pft_long(interpreted)`.

# 12. Diffusion clinical category

When `pft_diffusion()` outputs are available (the default in `pft_interpret()` when demographics are supplied), the Hughes & Pride 2012 categorical interpretation falls out of `dlco_zscore`, `va_zscore`, `kco_*_zscore`:

    patient_dlco <- data.frame(
      sex = "M", age = 50, height = 178, race = "Caucasian",
      dlco_measured   = 6,      # low
      va_measured     = 6,
      kco_tr_measured = 1.0     # also low -> Parenchymal pattern
    )
    pft_interpret(patient_dlco)$diffusion_category
    #> [1] "Parenchymal"

# Citations

See `citation("pft")` for the package and underlying reference standards as `bibentry` objects, suitable for direct inclusion in publications.

    citation("pft")
    #>
    #> Please cite the underlying reference standard for whichever function(s)
    #> you use, in addition to (or instead of) the pft package itself.
    #>
    #>   Johnson P, Helgeson S (2026). _pft: Pulmonary Function Test
    #>   Interpretation per ERS/ATS 2022_. R package version 1.0.1,
    #>   <https://github.com/overdodactyl/pft>.
    #>
    #> GLI 2012 spirometry equations (year = 2012):
    #>
    #>   Quanjer P, Stanojevic S, Cole T, et al. (2012). "Multi-ethnic
    #>   reference values for spirometry for the 3-95-yr age range: the global
    #>   lung function 2012 equations." _European Respiratory Journal_,
    #>   *40*(6), 1324-1343. doi:10.1183/09031936.00080312
    #>   <https://doi.org/10.1183/09031936.00080312>.
    #>
    #> GLI Global 2022 spirometry equations (year = 2022):
    #>
    #>   Bowerman C, Bhakta N, Brazzale D, et al. (2023). "A race-neutral
    #>   approach to the interpretation of lung function measurements."
    #>   _American Journal of Respiratory and Critical Care Medicine_,
    #>   *207*(6), 768-774. doi:10.1164/rccm.202205-0963OC
    #>   <https://doi.org/10.1164/rccm.202205-0963OC>.
    #>
    #> GLI 2021 static lung volumes (volume_normals):
    #>
    #>   Hall G, Filipow N, Ruppel G, et al. (2021). "Official ERS technical
    #>   standard: Global Lung Function Initiative reference values for static
    #>   lung volumes in individuals of European ancestry." _European
    #>   Respiratory Journal_, *57*(3), 2000289.
    #>   doi:10.1183/13993003.00289-2020
    #>   <https://doi.org/10.1183/13993003.00289-2020>.
    #>
    #> GLI 2017 TLCO / DLCO (diffusion_normals). Author correction (2020),
    #> doi:10.1183/13993003.50010-2017, is the version implemented here:
    #>
    #>   Stanojevic S, Graham B, Cooper B, et al. (2017). "Official ERS
    #>   technical standards: Global Lung Function Initiative reference values
    #>   for the carbon monoxide transfer factor for Caucasians." _European
    #>   Respiratory Journal_, *50*(3), 1700010.
    #>   doi:10.1183/13993003.00010-2017
    #>   <https://doi.org/10.1183/13993003.00010-2017>.
    #>
    #> Pattern interpretation algorithm (ats_classification):
    #>
    #>   Stanojevic S, Kaminsky D, Miller M, et al. (2022). "ERS/ATS technical
    #>   standard on interpretive strategies for routine lung function tests."
    #>   _European Respiratory Journal_, *60*(1), 2101499.
    #>   doi:10.1183/13993003.01499-2021
    #>   <https://doi.org/10.1183/13993003.01499-2021>.
    #>
    #> To see these entries in BibTeX format, use 'print(<citation>,
    #> bibtex=TRUE)', 'toBibtex(.)', or set
    #> 'options(citation.bibtex.max=999)'.