capnographyssa

View the Project on GitHub awconway/capnographyssa

Sequence analysis of capnography waveform abnormalities during nurse-administered procedural sedation and analgesia in the cardiac catheterization laboratory

Reproducibility

Launch Rstudio Binder

This repository contains the data and code used for statistical analysis.

library(tidyverse)
library(TraMineR)
library(cluster)
library(WeightedCluster)
library(gt)
library(qwraps2)

Setting up dataframe

Load in capnography data

myCapnoData <- read_csv("data.csv")

Code to produce Table 1

options(qwraps2_markup = "markdown")
our_summary1 <-
  list("Age" =
         list("mean (sd)" = ~ qwraps2::mean_sd(Age)),
       "Sex" =
         list("Female" = ~ qwraps2::n_perc0(Sex==1)),
       "Procedure" =
         list("Permanent pacemaker implant or generator change" = ~ qwraps2::n_perc0(procedure == "PPM"),
              "Implantable cardioverter defibrillator implant 
              or generator change"  = ~ qwraps2::n_perc0(procedure == "ICD"),
              "Cardiac resynchronisation therapy"  = ~ qwraps2::n_perc0(procedure == "CRT"),
              "Atrial flutter ablation"  = ~ qwraps2::n_perc0(procedure == "flutter"),
              "Other arrhythmia ablation"  = ~ qwraps2::n_perc0(procedure == "RFA"),
              "Diagnostic electrophysiology study"  = ~ qwraps2::n_perc0(procedure == "EPS"),
              "Loop recorder implant"  = ~ qwraps2::n_perc0(procedure == "Loop recorder implant")),
       "BMI" =
         list("mean (sd)" = ~ qwraps2::mean_sd(BMI, na_rm = TRUE, show_n = "never")),
       "ASA classification status" =
         list("One" = ~ qwraps2::n_perc0(ASA == 1),
              "Two"  = ~ qwraps2::n_perc0(ASA == 2),
              "Three"  = ~ qwraps2::n_perc0(ASA == 3),
              "Four"  = ~ qwraps2::n_perc0(ASA == 4)),
       "Obstructive Sleep Apnoea" =
         list("Yes" = ~ qwraps2::n_perc0(OSA == 1),
              "No"  = ~ qwraps2::n_perc0(OSA == 0)),
        "STOP-BANG Obstructive Sleep Apnoea Risk Classification" =
         list("Low" = ~ qwraps2::n_perc0(STOPBANG == "low", na_rm = TRUE),
              "Moderate"  = ~ qwraps2::n_perc0(STOPBANG == "intermediate", na_rm = TRUE),
              "High"  = ~ qwraps2::n_perc0(STOPBANG == "high", na_rm = TRUE)),
       "Chronic Obstructive Pulmonary Disease" =
         list("Yes" = ~ qwraps2::n_perc0(COPD == 1),
              "No"  = ~ qwraps2::n_perc0(COPD == 0)),
       "Past or present smoker" =
         list("Yes" = ~ qwraps2::n_perc0(Smoker == 1, na_rm = TRUE),
              "No"  = ~ qwraps2::n_perc0(Smoker == 0, na_rm = TRUE)),
       "Charlson Comoridity Index" =
         list("mean (sd)" = ~ qwraps2::mean_sd(CCI, na_rm = TRUE, show_n = "never")),
       "Midazolam total dose (mg)" =
         list("max" = ~ max(Midazolam),
           "mean (sd)" = ~ qwraps2::mean_sd(Midazolam)),
       "Fentanyl total dose (mg)" =
         list("max" = ~ max(Fentanyl),
           "mean (sd)" = ~ qwraps2::mean_sd(Fentanyl)),
       "TcCO2 at baseline (mm Hg)" =
         list("mean (sd)" = ~ qwraps2::mean_sd(PCO2.baseline)),
       "TcCO2 peak (mm Hg)" =
         list("max" = ~ max(PCO2.peak),
              "mean (sd)" = ~ qwraps2::mean_sd(PCO2.peak))
       )

whole <- summary_table(myCapnoData, our_summary1)

grouped <- summary_table(dplyr::group_by(myCapnoData, cluster.CHI), our_summary1)
both <- print(cbind(whole, grouped),
      rtitle = "Summary Statistics",
      cnames = c("Total Sample", "Normal breathing","Hypopnea", "Apnea", "Bradypnea"))
Summary Statistics Total Sample Normal breathing Hypopnea Apnea Bradypnea
Age
mean (sd) 72.95 ± 11.28 73.83 ± 11.63 73.21 ± 11.93 70.20 ± 10.46 72.14 ± 7.73
Sex
Female 35 (34) 12 (29) 17 (45) 4 (27) 2 (29)
Procedure
Permanent pacemaker implant or generator change 62 (61) 27 (64) 23 (61) 7 (47) 5 (71)
Implantable cardioverter defibrillator implant
or generator change 10 (10) 4 (10) 4 (11) 1 (7) 1 (14)
Cardiac resynchronisation therapy 5 (5) 1 (2) 2 (5) 2 (13) 0 (0)
Atrial flutter ablation 8 (8) 3 (7) 3 (8) 2 (13) 0 (0)
Other arrhythmia ablation 13 (13) 5 (12) 4 (11) 3 (20) 1 (14)
Diagnostic electrophysiology study 3 (3) 2 (5) 1 (3) 0 (0) 0 (0)
Loop recorder implant 1 (1) 0 (0) 1 (3) 0 (0) 0 (0)
BMI
mean (sd) 28.74 ± 5.36 29.92 ± 5.31 28.54 ± 5.09 25.77 ± 3.90 29.32 ± 8.29
ASA classification status
One 11 (11) 4 (10) 5 (13) 2 (13) 0 (0)
Two 52 (51) 25 (60) 13 (34) 9 (60) 5 (71)
Three 32 (31) 9 (21) 18 (47) 3 (20) 2 (29)
Four 7 (7) 4 (10) 2 (5) 1 (7) 0 (0)
Obstructive Sleep Apnoea
Yes 25 (25) 10 (24) 7 (18) 4 (27) 4 (57)
No 77 (75) 32 (76) 31 (82) 11 (73) 3 (43)
STOP-BANG Obstructive Sleep Apnoea Risk Classification
Low 39 (41) 13 (32) 19 (53) 5 (38) 2 (29)
Moderate 26 (27) 12 (30) 9 (25) 3 (23) 2 (29)
High 31 (32) 15 (38) 8 (22) 5 (38) 3 (43)
Chronic Obstructive Pulmonary Disease
Yes 16 (16) 9 (21) 4 (11) 1 (7) 2 (29)
No 86 (84) 33 (79) 34 (89) 14 (93) 5 (71)
Past or present smoker
Yes 41 (41) 14 (33) 15 (41) 6 (40) 6 (86)
No 60 (59) 28 (67) 22 (59) 9 (60) 1 (14)
Charlson Comoridity Index
mean (sd) 5.61 ± 2.40 5.52 ± 2.24 5.76 ± 2.09 4.60 ± 2.06 7.43 ± 4.39
Midazolam total dose (mg)
max 6 6 6 5 3
mean (sd) 2.05 ± 1.10 1.88 ± 1.14 2.16 ± 1.10 2.20 ± 1.15 2.14 ± 0.69
Fentanyl total dose (mg)
max 125 100 125 125 100
mean (sd) 55.40 ± 24.26 49.40 ± 22.42 58.58 ± 23.38 58.33 ± 29.38 67.86 ± 23.78
TcCO2 at baseline (mm Hg)
mean (sd) 37.48 ± 4.61 37.09 ± 4.12 38.43 ± 4.64 36.27 ± 5.43 37.26 ± 5.38
TcCO2 peak (mm Hg)
max 70.5 55.8 70.5 62.9 57.8
mean (sd) 47.23 ± 6.68 44.58 ± 4.95 49.54 ± 7.06 47.73 ± 7.55 49.56 ± 7.27

Respiratory state sequence analysis

Setting up state sequence object using TraMineR

rsa <- read_csv("rsa.csv")
rsa.alphabet1 <- c("0", "1","3", "5")
rsa.labels1 <- c("normal breathing", "hypoventilation","bradypnoea", "apnoea")
rsa.scodes1 <- c("NB", "Hypo", "Brady", "Apn")
rsa.seq1 <- seqdef(rsa, alphabet = rsa.alphabet1, states = rsa.scodes1, 
                   labels = rsa.labels1, left = "DEL", right = "DEL", gaps = "DEL", fill = TRUE)
##  [>] found missing values ('NA') in sequence data

##  [>] preparing 102 sequences

##  [>] coding void elements with '%' and missing values with '*'

##  [>] state coding:

##        [alphabet]  [label]  [long label]

##      1  0           NB       normal breathing

##      2  1           Hypo     hypoventilation

##      3  3           Brady    bradypnoea

##      4  5           Apn      apnoea

##  [>] 102 sequences in the data set

##  [>] min/max sequence length: 391/6831

Chi-squared distance over the full observed timeframe

This distance measure produced the most logical clustering solution with clusters of patients whose post-sedation respiratory state sequence was dominiated by “normal breathing”, “hypopnoeic hypoventilation”, “periods of apnoea” and “bradypnoeic hypoventilation”.

First we make a function to test different cluster sizes

clusterfunction <- function(x){

CHI <- seqdist(rsa.seq1, method = "CHI2", with.missing = TRUE, norm = "auto",  step = max(seqlength(rsa.seq1)))
clusterward.CHI <- agnes(CHI, diss = TRUE, method = "ward")
cl1.3.CHI <- cutree(clusterward.CHI, k = x)
cl1.3fac.CHI <- factor(cl1.3.CHI, labels = paste("Type", 1:x))

#Plot all the sequences within each cluster.

seqIplot(rsa.seq1, group = cl1.3fac.CHI, sortv = "from.start")
}

CHI distance matrix with 3 clusters

clusterfunction(3)

CHI distance matrix with 5 clusters

clusterfunction(5)

CHI distance matrix with 6 clusters

clusterfunction(6)

CHI distance matrix with 4 clusters

Code used to produce Figure 1

CHI <- seqdist(rsa.seq1, method = "CHI2", with.missing = TRUE, norm = "auto",  step = max(seqlength(rsa.seq1)))
clusterward.CHI <- agnes(CHI, diss = TRUE, method = "ward")
cl1.3.CHI <- cutree(clusterward.CHI, k = 4)
cl1.3fac.CHI <- factor(cl1.3.CHI, labels = paste("Type", 1:4))
seqIplot(rsa.seq1, group = cl1.3fac.CHI, sortv = "from.start",yaxis = FALSE, with.legend=TRUE, xtstep=1000)

Cluster quality

Measures of the quality of the cluster solution were calculated. These measures of cluster quality were not used to aid selection of the number of clusters or the optmial algorithm because on visual inspection, none of the other cluster solutions made clinical sense.

wcClusterQuality(CHI, clustering = cl1.3fac.CHI)
## $stats
##         PBC          HG        HGSD         ASW        ASWw          CH 
##  0.52499382  0.80161249  0.80161249  0.47990380  0.50174989 34.13297724 
##          R2        CHsq        R2sq          HC 
##  0.51097544 93.83231497  0.74176340  0.07241897 
## 
## $ASW
##              ASW      ASWw
## Type 1 0.6374376 0.6460700
## Type 2 0.3612490 0.3774208
## Type 3 0.4066809 0.4458104
## Type 4 0.3357337 0.4306289

In general, the results of the cluster quality measures indicate that a reasonable structure was identified.

PCA

Principal Components score for the total doses of midazolam and fentanyl was calculated because these two variables were highly correlated. The scores were added to the dataframe for use in the multivariable distance matrix regression analysis

p.comps <- myCapnoData %>% 
  select(Fentanyl, Midazolam) %>%
  princomp(cor=TRUE)

myCapnoData <- myCapnoData %>%
  mutate(PCA1 = p.comps$scores[,1] , PCA2 = p.comps$scores[,2])

Multivariable distance matrix regression analysis

Code to produce Table 2.

set.seed(1)
diss.multi <- dissmfacw(CHI ~ PCO2.peak + PCO2.baseline + OSA  + Age+ BMI + Sex + DOSA  + PCA1 + PCA2 + Smoker + COPD + CCI + intervention + STOPBANG + Emergency + SPO, data = myCapnoData, R = 1000)
diss.multi.table <- data.frame(diss.multi$mfac)
colnames(diss.multi.table) <- c("Variable", "Pseudo F", "Pseudo R2", "p value")
diss.multi.table <- diss.multi.table %>% 
  mutate(Total  = `Pseudo R2`/diss.multi.table[nrow(diss.multi.table),3])
colnames(diss.multi.table) <- c("Variable", "Pseudo F", "Pseudo R2", "p value", "Proportion of variance explained")
col.order <- c("Variable", "Pseudo F", "Pseudo R2",  "Proportion of variance explained", "p value")
diss.multi.table$Variable <- c("Peak TcCO2", "Baseline TcCO2", "OSA", "Age", "Body mass index", "Sex", "Day surgery admission", "PCA factor 1 of total sedation doses", "PCA factor 2 of total sedation doses", "Past or present smoker", "COPD", "Charlson comorbidity index score", "Patients who received intervention to support respiration", "STOPBANG (low, intermediate or high risk)", "Emergency admission", "Time above SpO2 97%", "Total")
diss.multi.table <- diss.multi.table[,col.order]
round_df <- function(df, digits) {
  nums <- vapply(df, is.numeric, FUN.VALUE = logical(1))

  df[,nums] <- round(df[,nums], digits = digits)

  (df)
}

diss.multi.table <- round_df(diss.multi.table, 3)
kableExtra::kable(diss.multi.table, caption = "Multivariate distance matrix regression analysis")
Multivariate distance matrix regression analysis
Variable Pseudo F Pseudo R2 Proportion of variance explained p value
Peak TcCO2 4.358 0.043 0.152 0.002
Baseline TcCO2 1.525 0.015 0.053 0.140
OSA 2.337 0.023 0.082 0.023
Age 0.689 0.007 0.024 0.674
Body mass index 0.936 0.009 0.033 0.460
Sex 0.599 0.006 0.021 0.800
Day surgery admission 1.270 0.012 0.044 0.243
PCA factor 1 of total sedation doses 1.404 0.014 0.049 0.185
PCA factor 2 of total sedation doses 1.608 0.016 0.056 0.134
Past or present smoker 2.793 0.027 0.097 0.010
COPD 1.136 0.011 0.040 0.284
Charlson comorbidity index score 2.626 0.026 0.092 0.021
Patients who received intervention to support respiration 5.700 0.056 0.199 0.001
STOPBANG (low, intermediate or high risk) 0.896 0.018 0.063 0.541
Emergency admission 0.800 0.008 0.028 0.576
Time above SpO2 97% 0.477 0.005 0.017 0.898
Total 1.686 0.282 1.000 0.001

Sequence index plot of patients who received interventions to support breathing ===============================================================================

Code to produce images in Figure 2

selection <- c(2,10,12,16,18,20,26,34,46,48,57,76,81)
rsa.seq.interventions <- rsa.seq1[selection,]
seqIplot(rsa.seq.interventions,yaxis=FALSE, sortv = "from.start",axes=F, with.legend=F, ylab = "")
axis(1, at=c(1,1000, 2000, 3000)-.5, labels=c("1", "1000", "2000", "3000"))

seqlegend(rsa.seq.interventions, ncol=2)