Assuming access to the available source data files, the Notebook contains the complete statistical code for replicating sections:
- “Physicians’ communication strategies and patient-reported understanding”
- “Effects of combining dyadic-level SM concordance and physicians’ communication strategy”
- “Classifying physician-level communication strategies”
- Figure 1
See README in repository for instructions on how to access the .csv file.
Please note: Kaiser Permanante (owners of data) will not allow any individual-level data (even a small subset) to be shown in a public repository. A version of this HTML exists with the restricted .csv files that includes a subset of individual-level data to aid in tutorial interpretation.
library(tidyverse)
library(pander)
df.models = read.csv("Duran_Schillinger_KNPC_restricted_models.csv")
pander(names(df.models))
PCP_ID1, DV_CAREEXPL, predictions_LP, predicted_new_CP, Age, sex, white, ed_HS and CHARLSON_INDEX
The following code is used to generate all reported results in the corresponding section of the manuscript.
Output would show, for example, Physician PCPXXXX using low complexity with a patient classified as having low health literacy (cat_match = “lowCP-lowLP”) but also high complexity with a patient classified as low health literacy (cat_match = “lowCP-highLP”). Please note: Kaiser Permanante (owners of data) will not allow any individual-level data (even a small subset) to be shown in a public repository. A version of this HTML exists with the restricted .csv files that includes a subset of individual-level data to aid in tutorial interpretation.
df.models = df.models %>% mutate(
cat_match = case_when(
predicted_new_CP=="low" & predictions_LP=="Low" ~ "lowCP-lowLP",
predicted_new_CP=="low" & predictions_LP=="High" ~ "lowCP-highLP",
predicted_new_CP=="high" & predictions_LP=="Low" ~ "highCP-lowLP",
predicted_new_CP=="high" & predictions_LP=="High" ~ "highCP-highLP",
TRUE ~ NA_character_))
df.models$cat_match = as.factor(df.models$cat_match)
# pander(names(df.models[c(1,3,4,10)]))
Output would show, for example, that Physician PCPXXX had 6 unique patients. Please note: Kaiser Permanante (owners of data) will not allow any individual-level data (even a small subset) to be shown in a public repository. A version of this HTML exists with the restricted .csv files that includes a subset of individual-level data to aid in tutorial interpretation.
prepProvider = df.models %>%
group_by(PCP_ID1) %>%
dplyr::summarize(uniquepat = n()) %>%
ungroup()
prepProvider2 = left_join(prepProvider,df.models, by="PCP_ID1")
multProvider = prepProvider2 %>% filter(uniquepat >= 4)
multProvider = multProvider[c(1:2, 4, 11)] ## showing only the variables necessary to run the analyses
# pander(multProvider[33:40,])
Output would show that, for example, Physician PCPXXX’s 6 unique patients, there was 1 highCP-highLP interaction, 2 highCP-lowLP interactions, 2 lowCP-highLP interactions, and 1 lowCP-lowLP interaction. Please note: Kaiser Permanante (owners of data) will not allow any individual-level data (even a small subset) to be shown in a public repository. A version of this HTML exists with the restricted .csv files that includes a subset of individual-level data to aid in tutorial interpretation.
prepProvider3 = multProvider %>%
group_by(PCP_ID1, cat_match) %>%
dplyr::summarize(uniquematch = n()) %>%
ungroup()
multProvider2 = left_join(prepProvider3, multProvider, by=c("PCP_ID1", "cat_match"))
# pander(multProvider2[33:40,])
multProvider3 = multProvider2 %>%
group_by(PCP_ID1, predictions_LP) %>%
dplyr::summarize(numHighLow = n()) %>%
ungroup()
multProvider4 = left_join(multProvider3, multProvider2, by=c("PCP_ID1", "predictions_LP"))
keepProvider = multProvider4 %>% mutate(
atleast2 = case_when(
numHighLow==uniquepat ~ "same", ## when patients are all high or all low
predictions_LP=="Low" & numHighLow >= 2 ~ "keep",
predictions_LP=="High" & numHighLow >= 2 ~ "keep",
TRUE ~ "remove"))
## Final filtering
keepProvider = keepProvider %>%
group_by(PCP_ID1) %>%
filter(!any(atleast2 == "remove") & !any(atleast2 == "same")) %>%
ungroup()
Output would show that, for example, the distribution of PCPXXX’s six patients into relevant “cat_match” categories. Please note: Kaiser Permanante (owners of data) will not allow any individual-level data (even a small subset) to be shown in a public repository. A version of this HTML exists with the restricted .csv files that includes a subset of individual-level data to aid in tutorial interpretation.
## simple aggregation
keepProvider = keepProvider %>%
group_by(PCP_ID1, cat_match) %>%
dplyr::summarize(uniquematch = mean(uniquematch)) %>%
ungroup()
# pander(keepProvider[10:20,])
highLP = filter(keepProvider, cat_match == "lowCP-highLP" | cat_match == "highCP-highLP") %>%
group_by(PCP_ID1) %>%
mutate(countLP = sum(uniquematch)) %>%
ungroup()
lowLP = filter(keepProvider, cat_match == "lowCP-lowLP" | cat_match == "highCP-lowLP") %>%
group_by(PCP_ID1) %>%
mutate(countLP = sum(uniquematch)) %>%
ungroup()
keepProvider.prop = rbind(highLP, lowLP) %>% arrange(PCP_ID1, cat_match) %>% mutate(propscore = uniquematch/countLP)
## Put in similar format as shown in Table 4a.
keepProvider.prop2 = spread(keepProvider.prop, cat_match, propscore)
keepProvider.prop2 = keepProvider.prop2 %>% group_by(PCP_ID1) %>%
summarise_if(is.numeric, mean, na.rm = TRUE) %>%
select(-uniquematch, -countLP)
keepProvider.prop2[is.na(keepProvider.prop2)] <- 0 ## if category does not exist, produces NA, converted to 0 occurrences
# pander(head(keepProvider.prop2))
rcols = c("lowCP-lowLP", "highCP-highLP", "lowCP-highLP", "highCP-lowLP")
PCP_ID1 = c()
abs_tailor = c()
univ_prec = c()
always_high = c()
anti_tailor = c()
ambig = c()
targ_lowLP = c()
targ_highLP = c()
euc.dist <- function(x1, x2) sqrt(sum((x1 - x2) ^ 2))
for (i in 1:nrow(keepProvider.prop2)) {
prov_profile = as.matrix(keepProvider.prop2[i,rcols])
## "gold standard vector benchmarks"
abs_tailor1 = t(as.matrix(c(1, 1, 0, 0)))
univ_prec2 = t(as.matrix(c(1, 0, 1, 0)))
always_high3 = t(as.matrix(c(0, 1, 0, 1)))
anti_tailor4 = t(as.matrix(c(0, 0, 1, 1)))
ambig5 = t(as.matrix(c(.5, .5, .5, .5)))
targ_lowLP6 = t(as.matrix(c(1, .5, .5, 0)))
targ_highLP7 = t(as.matrix(c(.5, 1, 0, .5)))
## generate the "resemblance scores"
dist1 = euc.dist(prov_profile, abs_tailor1)
dist2 = euc.dist(prov_profile, univ_prec2)
dist3 = euc.dist(prov_profile, always_high3)
dist4 = euc.dist(prov_profile, anti_tailor4)
dist5 = euc.dist(prov_profile, ambig5)
dist6 = euc.dist(prov_profile, targ_lowLP6)
dist7 = euc.dist(prov_profile, targ_highLP7)
abs_tailor = c(abs_tailor, dist1)
univ_prec = c(univ_prec, dist2)
always_high = c(always_high, dist3)
anti_tailor = c(anti_tailor, dist4)
ambig = c(ambig, dist5)
targ_lowLP = c(targ_lowLP, dist6)
targ_highLP = c(targ_highLP, dist7)
PCP_ID1 = c(PCP_ID1, as.character(keepProvider.prop2$PCP_ID1[i]))
}
tailoringScores = data.frame(PCP_ID1, abs_tailor, univ_prec, always_high, anti_tailor, targ_lowLP, targ_highLP, ambig)
# pander(head(tailoringScores[1:7]))
masterBig = merge(tailoringScores[c(1,2,3)], df.models, by="PCP_ID1")
masterBig = masterBig[!(is.na(masterBig$DV_CAREEXPL)),]
Corresponds to results reported in "Table 3. Results of physicians’ communication strategies and patient-reported understanding.
df.models2.var = masterBig %>% mutate(
CHARLSON_INDEX_z = scale(CHARLSON_INDEX, center=FALSE),
Age_z = scale(Age, center=FALSE),
abs_tailor_zc = scale(abs_tailor),
univ_prec_zc = scale(univ_prec)
)
library(lme4)
library(strengejacke)
## # Attaching packages (red = needs update)
## ⚠ ggeffects 1.1.0 ✔ sjlabelled 1.1.8
## ✔ sjmisc 2.8.7 ✔ sjstats 0.18.1
## ✔ sjPlot 2.8.9 ✔ esc 0.5.1
##
## Update packages in red with 'sj_update()'.
Step 1: Build relevant model with and without interaction term and test interaction.
ut.interact <- glmer(DV_CAREEXPL ~ abs_tailor_zc*predictions_LP + Age_z + sex + white + CHARLSON_INDEX_z + ed_HS + (1|PCP_ID1), df.models2.var, family = binomial)
ut.interact <- update(ut.interact, control = glmerControl(optimizer="bobyqa"))
ut.main <- glmer(DV_CAREEXPL ~ abs_tailor_zc + predictions_LP + Age_z + sex + white + CHARLSON_INDEX_z + ed_HS + (1|PCP_ID1), df.models2.var, family = binomial)
ut.main <- update(ut.main, control = glmerControl(optimizer="bobyqa"))
pander(anova(ut.main, ut.interact))
| npar | AIC | BIC | logLik | deviance | Chisq | Df | Pr(>Chisq) | |
|---|---|---|---|---|---|---|---|---|
| ut.main | 9 | 1799 | 1852 | -890.3 | 1781 | NA | NA | NA |
| ut.interact | 10 | 1800 | 1859 | -890 | 1780 | 0.5002 | 1 | 0.4794 |
Step 2: Show results for main effects.
tab_model(ut.main, digits = 3, digits.re = 3)
| DV_CAREEXPL | |||
|---|---|---|---|
| Predictors | Odds Ratios | CI | p |
| (Intercept) | 0.111 | 0.049 – 0.249 | <0.001 |
| abs_tailor_zc | 1.191 | 1.040 – 1.365 | 0.012 |
| predictions_LP [Low] | 1.426 | 1.110 – 1.832 | 0.006 |
| Age_z | 1.177 | 0.537 – 2.578 | 0.684 |
| sex | 0.908 | 0.705 – 1.170 | 0.455 |
| white | 0.579 | 0.423 – 0.793 | 0.001 |
| CHARLSON_INDEX_z | 1.152 | 0.930 – 1.428 | 0.195 |
| ed_HS | 0.655 | 0.509 – 0.844 | 0.001 |
| Random Effects | |||
| σ2 | 3.290 | ||
| τ00 PCP_ID1 | 0.182 | ||
| ICC | 0.052 | ||
| N PCP_ID1 | 420 | ||
| Observations | 2660 | ||
| Marginal R2 / Conditional R2 | 0.054 / 0.103 | ||
Step 1: Build relevant model with and without interaction term and test interaction.
up.interact <- glmer(DV_CAREEXPL ~ univ_prec_zc*predictions_LP + Age_z + sex + white + CHARLSON_INDEX_z + ed_HS + (1|PCP_ID1), df.models2.var, family = binomial)
up.interact <- update(up.interact, control = glmerControl(optimizer="bobyqa"))
up.main <- glmer(DV_CAREEXPL ~ univ_prec_zc + predictions_LP + Age_z + sex + white + CHARLSON_INDEX_z + ed_HS + (1|PCP_ID1), df.models2.var, family = binomial)
up.main <- update(up.main, control = glmerControl(optimizer="bobyqa"))
pander(anova(up.main, up.interact))
| npar | AIC | BIC | logLik | deviance | Chisq | Df | Pr(>Chisq) | |
|---|---|---|---|---|---|---|---|---|
| up.main | 9 | 1804 | 1857 | -892.8 | 1786 | NA | NA | NA |
| up.interact | 10 | 1805 | 1864 | -892.5 | 1785 | 0.7446 | 1 | 0.3882 |
Step 2: Show results for main effects.
tab_model(up.main, digits = 3, digits.re = 3)
| DV_CAREEXPL | |||
|---|---|---|---|
| Predictors | Odds Ratios | CI | p |
| (Intercept) | 0.107 | 0.047 – 0.241 | <0.001 |
| univ_prec_zc | 0.927 | 0.812 – 1.058 | 0.262 |
| predictions_LP [Low] | 1.427 | 1.109 – 1.835 | 0.006 |
| Age_z | 1.227 | 0.558 – 2.696 | 0.611 |
| sex | 0.908 | 0.704 – 1.171 | 0.457 |
| white | 0.579 | 0.422 – 0.794 | 0.001 |
| CHARLSON_INDEX_z | 1.141 | 0.921 – 1.415 | 0.227 |
| ed_HS | 0.654 | 0.507 – 0.844 | 0.001 |
| Random Effects | |||
| σ2 | 3.290 | ||
| τ00 PCP_ID1 | 0.217 | ||
| ICC | 0.062 | ||
| N PCP_ID1 | 420 | ||
| Observations | 2660 | ||
| Marginal R2 / Conditional R2 | 0.047 / 0.106 | ||
df_h1 = df.models2.var %>% filter(cat_match == "lowCP-lowLP" | cat_match == "highCP-lowLP")
df_h1$cat_match <- relevel(df_h1$cat_match, "lowCP-lowLP")
Step 1: Build relevant model with and without interaction term and test interaction.
interact_low <- glmer(DV_CAREEXPL ~ abs_tailor_zc*cat_match + Age_z + sex + white + CHARLSON_INDEX_z + ed_HS + (1|PCP_ID1), df_h1, family = binomial)
interact_low <- update(interact_low, control = glmerControl(optimizer="bobyqa"))
main_low <- glmer(DV_CAREEXPL ~ abs_tailor_zc + cat_match + Age_z + sex + white + CHARLSON_INDEX_z + ed_HS + (1|PCP_ID1), df_h1, family = binomial)
main_low <- update(main_low, control = glmerControl(optimizer="bobyqa"))
pander(anova(interact_low, main_low))
| npar | AIC | BIC | logLik | deviance | Chisq | Df | Pr(>Chisq) | |
|---|---|---|---|---|---|---|---|---|
| main_low | 9 | 837.1 | 882 | -409.6 | 819.1 | NA | NA | NA |
| interact_low | 10 | 833.2 | 883.1 | -406.6 | 813.2 | 5.945 | 1 | 0.01476 |
library(interactions)
pander(sim_slopes(interact_low, pred = abs_tailor_zc, modx = cat_match, jnplot = FALSE)$slopes)
| Value of cat_match | Est. | S.E. | 2.5% | 97.5% | z val. | p |
|---|---|---|---|---|---|---|
| highCP-lowLP | -0.1389 | 0.143 | -0.4192 | 0.1414 | -0.9711 | 0.3315 |
| lowCP-lowLP | 0.3686 | 0.1574 | 0.06021 | 0.6771 | 2.343 | 0.01915 |
df_h2 = df.models2.var %>% filter(cat_match == "highCP-highLP" | cat_match == "lowCP-highLP")
df_h2$cat_match <- relevel(df_h2$cat_match, "highCP-highLP")
Step 1: Build relevant model with and without interaction term and test interaction.
interact_high <- glmer(DV_CAREEXPL ~ abs_tailor_zc*cat_match + Age_z + sex + white + CHARLSON_INDEX_z + ed_HS + (1|PCP_ID1), df_h2, family = binomial)
interact_high <- update(interact_high, control = glmerControl(optimizer="bobyqa"))
main_high <- glmer(DV_CAREEXPL ~ abs_tailor_zc + cat_match + Age_z + sex + white + CHARLSON_INDEX_z + ed_HS + (1|PCP_ID1), df_h2, family = binomial)
main_high <- update(main_high, control = glmerControl(optimizer="bobyqa"))
pander(anova(interact_high, main_high))
| npar | AIC | BIC | logLik | deviance | Chisq | Df | |
|---|---|---|---|---|---|---|---|
| main_high | 9 | 955.2 | 1003 | -468.6 | 937.2 | NA | NA |
| interact_high | 10 | 957.2 | 1011 | -468.6 | 937.2 | 0.04205 | 1 |
| Pr(>Chisq) | |
|---|---|
| main_high | NA |
| interact_high | 0.8375 |
Step 1: Build relevant model with and without interaction term and test interaction.
interact_low <- glmer(DV_CAREEXPL ~ univ_prec_zc*cat_match + Age_z + sex + white + CHARLSON_INDEX_z + ed_HS + (1|PCP_ID1), df_h1, family = binomial)
interact_low <- update(interact_low, control = glmerControl(optimizer="bobyqa"))
main_low <- glmer(DV_CAREEXPL ~ univ_prec_zc + cat_match + Age_z + sex + white + CHARLSON_INDEX_z + ed_HS + (1|PCP_ID1), df_h1, family = binomial)
main_low <- update(main_low, control = glmerControl(optimizer="bobyqa"))
pander(anova(interact_low, main_low))
| npar | AIC | BIC | logLik | deviance | Chisq | Df | |
|---|---|---|---|---|---|---|---|
| main_low | 9 | 835.9 | 880.7 | -408.9 | 817.9 | NA | NA |
| interact_low | 10 | 837.3 | 887.1 | -408.6 | 817.3 | 0.5937 | 1 |
| Pr(>Chisq) | |
|---|---|
| main_low | NA |
| interact_low | 0.441 |
Step 1: Build relevant model with and without interaction term and test interaction.
interact_high <- glmer(DV_CAREEXPL ~ univ_prec_zc*cat_match + Age_z + sex + white + CHARLSON_INDEX_z + ed_HS + (1|PCP_ID1), df_h2, family = binomial)
interact_high <- update(interact_high, control = glmerControl(optimizer="bobyqa"))
main_high <- glmer(DV_CAREEXPL ~ univ_prec_zc + cat_match + Age_z + sex + white + CHARLSON_INDEX_z + ed_HS + (1|PCP_ID1), df_h2, family = binomial)
main_high <- update(main_high, control = glmerControl(optimizer="bobyqa"))
pander(anova(interact_high, main_high))
| npar | AIC | BIC | logLik | deviance | Chisq | Df | |
|---|---|---|---|---|---|---|---|
| main_high | 9 | 958.8 | 1007 | -470.4 | 940.8 | NA | NA |
| interact_high | 10 | 960.6 | 1014 | -470.3 | 940.6 | 0.1779 | 1 |
| Pr(>Chisq) | |
|---|---|
| main_high | NA |
| interact_high | 0.6732 |
library(kableExtra)
# Table A
matA = matrix(c("5/6 = 0.83", "7/10 = 0.70", "1/6 = 0.17", "3/10 = 0.30"),
ncol=4,byrow=TRUE)
colnames(matA) <- c("Low HL","High HL","Low HL","High HL")
kable(matA,
# caption = "Table Xa. Doctor w/ 16 patients: 6 Low LP and 10 High LP",
caption = "A.",
align = "cccc") %>%
add_header_above(c("Physician Low Complexity" = 2, "Physician High Complexity" = 2)) %>%
kable_styling(bootstrap_options = "striped")
| Low HL | High HL | Low HL | High HL |
|---|---|---|---|
| 5/6 = 0.83 | 7/10 = 0.70 | 1/6 = 0.17 | 3/10 = 0.30 |
## Table B
matB = matrix(c("Universal precautions", 1, 1, 0, 0, 0.488,
"Universal tailoring", 1, 0, 0, 1, 1.019,
"Tailor only low", 1, 0.5, 0, 0.5, 0.371,
"Tailor only high", 0.5, 0, 0.5, 1, 1.094,
"No Precautions", 0, 0, 1, 1, 1.307,
"Anti-Tailor", 0, 1, 0, 1, 1.452),
ncol=6,byrow=TRUE)
colnames(matB) <- c("Strategy", "Low HL","High HL","Low HL","High HL", "Resemblance scores")
kable(matB,
caption = "B.",
# caption = "Table Xb. 'Gold standard' vector representation for each doctor tailoring type. The last column shows the example doctor's euclidean distance score from each type with the closest score in bold.",
align = "lccccc") %>%
add_header_above(c("", "Physician Low Complexity" = 2, "Physician High Complexity" = 2, "")) %>%
# row_spec(2, bold = TRUE) %>%
kable_styling(bootstrap_options = "striped")
| Strategy | Low HL | High HL | Low HL | High HL | Resemblance scores |
|---|---|---|---|---|---|
| Universal precautions | 1 | 1 | 0 | 0 | 0.488 |
| Universal tailoring | 1 | 0 | 0 | 1 | 1.019 |
| Tailor only low | 1 | 0.5 | 0 | 0.5 | 0.371 |
| Tailor only high | 0.5 | 0 | 0.5 | 1 | 1.094 |
| No Precautions | 0 | 0 | 1 | 1 | 1.307 |
| Anti-Tailor | 0 | 1 | 0 | 1 | 1.452 |
g1 = gather(tailoringScores, "abs_tailor", "univ_prec", "targ_lowLP", "targ_highLP", "always_high", "anti_tailor", key="matching", value="Distance_from_Gold_Standard")
g1$matching <- factor(g1$matching, levels=c("univ_prec", "abs_tailor", "targ_lowLP", "targ_highLP", "always_high", "anti_tailor"))
g2 = g1 %>% mutate(matching = recode_factor(matching, "abs_tailor" = "Univ \nTailor", "univ_prec" = "Univ \nPrecaut", "targ_lowLP" = "Tailor \nOnly \nLow", "targ_highLP" = "Tailor \nOnly \nHigh", "always_high" = "No\n Precaut", "anti_tailor" = "Anti- \nTailor"))
theme_USGS_box <- function(base_family = "serif", ...){
theme_bw(base_family = base_family, ...) +
theme(
panel.grid = element_blank(),
plot.title = element_text(size = 8),
# axis.title = element_text(size = 8),
axis.ticks.length = unit(-0.05, "in"),
axis.text.y = element_text(margin=unit(c(0.3,0.3,0.3,0.3), "cm")),
axis.text.x = element_text(margin=unit(c(0.3,0.3,0.3,0.3), "cm")),
axis.ticks.x = element_blank(),
aspect.ratio = 1,
legend.background = element_rect(color = "black", fill = "white")
)
}
boxplotGold = ggplot(data = g2,
aes(matching, Distance_from_Gold_Standard)) +
stat_boxplot(geom ='errorbar', width = 0.4) +
geom_boxplot(width = 0.4, fill = "lightgrey") +
# stat_summary(fun.data = n_fun, geom = "text", hjust = 0.5) +
# expand_limits(y = 0) +
theme_USGS_box() +
xlab(label = "Tailoring Style") +
ylab(label = "Distance from Gold Standard")
boxplotGold