The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.

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

These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.