Distal Radius Malunion Repair

Outcomes Based on Osteotomy and Graft Type

Author

Kingery MT

Published

November 12, 2023

1 Data Manipulation

1.1 Packages

Code
library(tidyverse)      # for everything
library(tidymodels)     # for modeling
library(rms)            # for more modeling
library(Hmisc)          # for stats
library(magrittr)       # for numbers
library(here)           # for path management
library(readr)
library(openxlsx)
library(readr)          # for importing data
library(broom.mixed)    # for converting Bayesian models to tidy tibbles
library(dotwhisker)     # for visualizing regression results
library(skimr)          # for variable summaries
library(tableone)       # for tables
library(gt)             # for tables
library(gtsummary)      # for prettier tables
library(patchwork)      # for multi-planel figures
library(glue)           # for working with strings
library(RColorBrewer)   # for color palettes


# Load custom functions
# source("C:/Users/kinge/Dropbox/Research/Resources/mtk-custom-functions.R") # Windows
source("/Users/mtk/Library/CloudStorage/Dropbox/Research/Resources/mtk-custom-functions.R") # Mac

1.2 Data

Code
data_path <- here('Distal-Radius-Malunion_Data_Clean_20231105.xlsx')

df.raw <- read.xlsx(data_path,
                    detectDates = TRUE)

# df.raw %>% colnames()

1.3 Tidy data

Code
# Fix variable types
df.raw <- df.raw %>%
  mutate(across(c(
    mrn,
    sex,
    smoker,
    complication,
    reoperation
  ),
  factor),
  across(c(dob,
           doi,
           dos,
           reop.date),
         ymd))


# Encode qualitative columns as factors
df.raw <- df.raw %>% mutate_if(is.character, as.factor)

# Reorder and rename factors/levels
# df.raw <- df.raw %>%
#   mutate(var1 =
#            factor(var1,
#                   levels = c('level1',
#                              'level2',
#                              'level3')))


# Drop unused levels
df.raw <- df.raw %>% droplevels()


# Create new vars
df.raw <- df.raw %>%
  # Age
  mutate(age = (((doi - dob) / 365.25) %>% as.numeric())) %>%
  # Time from injury to surgery
  mutate(time_inj_to_surg = (((dos - doi) / 7) %>% as.numeric())) %>%
  # Time from surgery to revision
  mutate(time_surg_to_rev = (((reop.date - dos) / 7) %>% as.numeric())) %>%
  # Follow up duration
  mutate(followup = (((date.finalfu - doi) / 365.25) %>% as.numeric())) %>% 
  # Osteotomy gap
  mutate(gap_avg = rowMeans(select(., starts_with('gap_')), na.rm = TRUE)) %>% 
  # Volar tilt preop
  mutate(volartilt_preop_avg = rowMeans(select(., starts_with('volartilt_preop_')), na.rm = TRUE)) %>% 
  # Ulnar variance preop
  mutate(ulnarvar_preop_avg = rowMeans(select(., starts_with('ulnarvar_preop_')), na.rm = TRUE)) %>% 
  # Radial inclination preop
  mutate(radialinc_preop_avg = rowMeans(select(., starts_with('radialinc_preop_')), na.rm = TRUE)) %>% 
  # Volar tilt postop
  mutate(volartilt_postop_avg = rowMeans(select(., starts_with('volartilt_postop_')), na.rm = TRUE)) %>% 
  # Ulnar variance postop
  mutate(ulnarvar_postop_avg = rowMeans(select(., starts_with('ulnarvar_postop_')), na.rm = TRUE)) %>% 
  # Radial inclination postop
  mutate(radialinc_postop_avg = rowMeans(select(., starts_with('radialinc_postop_')), na.rm = TRUE)) %>% 
  # Volar tilt final follow up
  mutate(volartilt_finalfu_avg = rowMeans(select(., starts_with('volartilt_finalfu_')), na.rm = TRUE)) %>% 
  # Ulnar variance final follow up
  mutate(ulnarvar_finalfu_avg = rowMeans(select(., starts_with('ulnarvar_finalfu_')), na.rm = TRUE)) %>% 
  # Radial inclination final follow up
  mutate(radialinc_finalfu_avg = rowMeans(select(., starts_with('radialinc_finalfu_')), na.rm = TRUE)) %>% 
  # Volar tilt subsidence
  mutate(delta_volartilt = volartilt_finalfu_avg - volartilt_postop_avg) %>% 
  # Ulnar variance subsidence
  mutate(delta_ulnarvar = ulnarvar_finalfu_avg - ulnarvar_postop_avg) %>% 
  # Radial inclination subsidence
  mutate(delta_radialinc = radialinc_finalfu_avg - radialinc_postop_avg) %>% 
  # Volar tilt correction
  mutate(correction_volartilt = volartilt_postop_avg - volartilt_preop_avg) %>% 
  # Ulnar variance correction
  mutate(correction_ulnarvar = ulnarvar_postop_avg - ulnarvar_preop_avg) %>% 
  # Radial inclination correction
  mutate(correction_radialinc = radialinc_postop_avg - radialinc_preop_avg) %>% 
  # Preoperative dorsal tilt
  mutate(dorsaltilt_preop = case_when(
    volartilt_preop_avg < 0 ~ TRUE,
    TRUE ~ FALSE
  ))


df <- df.raw %>% 
  filter(!is.na(osteotomy))

rm(df.raw)

2 Methods

2.1 Patient Selection

This was a retrospective cohort study consisting of patients who underwent operative correction of distal radius fracture malunion at a large, urban, academic institution between 2011 and 2022. Inclusion criteria consisted of any patient who sustained a distal radius fracture complicated by malunion who then underwent operative repair of the malunion with an osteotomy and subsequent fixation. Exclusion criteria consisted of inadequate postoperative follow up, which was defined as less than 1 year or lack of follow up to the point of radiographic evidence of healed osteotomy. Both the electronic medical record system and individual surgeon case logs were searched. The following CPT codes were used to identify potentially eligible patients:

  • 25350 - Osteotomy, radius; distal third)

  • 25365 - Osteotomy; radius and ulna

  • 25391 - Osteoplasty, radius OR ulna; lengthening with autograft

  • 25400 - Repair of nonunion or malunion, radius OR ulna; without graft

  • 25405 - Repair of nonunion or malunion, radius OR ulna; with iliac or other autograft

  • 25415 - Repair of nonunion or malunion, radius AND ulna; without graft

  • 25420 - Repair of nonunion or malunion, radius AND ulna; with iliac or other autograft

For each identified patient, the operative report was used to assess whether the indication and procedure were relevant for this study. Patients undergoing any of the above procedures for an indication other than distal radius malunion were excluded.

2.2 Data Collection

The medical records of all eligible patients were reviewed and relevant data was recorded. This included demographic information (age, sex, BMI, smoking history), details related to the injury and surgery (duration between initial injury and malunion repair, type of osteotomy, type of bone graft), radiographic characteristics (volar tilt, ulnar varianace, radial inclination), and clinical outcomes (wrist range of motion at final follow up, reoperations or revision surgeries). Patients were grouped based on the type of graft used (structural graft consisting of cortical bone versus non-structural graft consisting of cancellous bone). The groups were further stratified based on the type of osteotomy performed (distraction osteotomy versus wedge osteotomy).

Radiographic measurements were performed by two independent orthopedic surgeons. Volar tilt, ulnar variance, and radial inclination were each measured on radiographs obtained preoperatively, immediately postoperative, and at final follow up with evidence of bony consolidation. Intra-class correlation coefficients were used to ensure appropriate agreement and reliability of the radiographic measurements between the two raters.

2.3 Statistic Analysis

Statistical analysis was performed using R (R Foundation for Statistical Computing, Vienna, Austria). The primary outcome of this study was volar tilt subsidence following osteotomy, defined as the change in volar tilt between the immediate postoperative radiographs and the final follow up radiographs after the osteotomy had healed. Secondary outcomes included ulnar variance subsidence and radial inclination subsidence. Comparisons between groups were

A priori sample size estimation was performed based on the primary outcome of volar tilt subsidence. To detect a 3 degree difference in volar tilt angle between the structural graft group and the non-structural graft group with 80% power and alpha = 0.05, assuming a 3 degree standard deviation and roughly 3:1 ratio of patients in non-structural group to patients in the structural group, a total sample size of 44 patients was needed.

3 Results

Code
n_patients <- df %>% distinct(mrn) %>% nrow() %>% fnum0()

w_timetosurgery <- wilcox.test(time_inj_to_surg ~ graft,
                               data = df)

3.1 Baseline Characteristics

A total of 51 patients met inclusion criteria and were included in the analysis. 70.6% of patients were female and the mean age was 51.2 +/- 18.8 years. 38 patients (74.5%) had non-structural bone graft during malunion repair, while 13 patients (25.5%) had structural bone graft during malunion repair. With respect to osteotomy type, 32 patients (62.7%) underwent distraction osteotomy and 19 patients (37.3%) underwent wedge osteotomy. The overall mean duration from injury to operative malunion repair was 18.7 +/- 16.9 weeks. The non-structural graft group had a significantly shorter duration from initial injury to malunion repair (16.6 +/- 17.0 weeks versus 24.7 +/- 15.5 weeks, p = 0.024). Mean follow-up duration was 1.4 +/- 2.2 years from the time of surgery. There was no difference between the non-structural graft group and the structural graft group with respect to baseline demographic characteristics (Table 1).

Code
# All patients
t1 <- df %>%
  select(
    age,
    sex,
    bmi,
    smoker,
    time_inj_to_surg,
    osteotomy,
    graft,
    followup
  ) %>%
  mutate(graft = recode_factor(graft,
                               'nonstructural' = 'Non-structural',
                               'structural' = 'Structural')) %>%
  tbl_summary(
    by = graft,
    missing = 'no',
    type = list(),
    statistic = list(all_continuous() ~ '{mean} +/- {sd}'),
    digits = list(all_categorical() ~ c(0, 1),
                  all_continuous() ~ c(1, 1)),
    label = list(
      osteotomy ~ 'Osteotomy type',
      age ~ 'Age',
      sex ~ 'Sex',
      bmi ~ 'BMI',
      smoker ~ 'Smoking history',
      time_inj_to_surg ~ 'Time from injury to surgery (weeks)',
      followup ~ 'Follow up duration (years)'
    )
  ) %>%
  add_p(
    test = list(
      c('age') ~ 't.test',
      c('bmi', 'time_inj_to_surg', 'followup') ~ 'wilcox.test'
    ),
    pvalue_fun = function(x)
      style_pvalue(x, digits = 3),
    test.args = all_tests('fisher.test') ~ list(simulate.p.value = TRUE
    )
  ) %>%
  bold_p(t = 0.05) %>%
  add_overall() %>%
  modify_spanning_header(all_stat_cols(stat_0 = F) ~ "**Graft Type**") %>%
  modify_caption("<div style='text-align: left; font-weight: bold'>
                 Baseline Demographics") %>%
  bold_labels() %>%
  as_gt() %>%
  gt::tab_options(table.font.size = px(12),
                  data_row.padding = gt::px(3))

t1
Table 1:

Baseline Demographics

Characteristic Overall, N = 511 Graft Type p-value2
Non-structural, N = 381 Structural, N = 131
Age 51.2 +/- 18.8 52.2 +/- 19.2 48.3 +/- 18.1 0.525
Sex


0.487
    Female 36 (70.6%) 28 (73.7%) 8 (61.5%)
    Male 15 (29.4%) 10 (26.3%) 5 (38.5%)
BMI 25.8 +/- 5.5 26.0 +/- 5.9 25.0 +/- 4.0 0.871
Smoking history


0.286
    Current 10 (19.6%) 6 (15.8%) 4 (30.8%)
    Former 13 (25.5%) 9 (23.7%) 4 (30.8%)
    Never 28 (54.9%) 23 (60.5%) 5 (38.5%)
Time from injury to surgery (weeks) 18.7 +/- 16.9 16.6 +/- 17.0 24.7 +/- 15.5 0.024
Osteotomy type


0.515
    Distraction 32 (62.7%) 25 (65.8%) 7 (53.8%)
    Wedge 19 (37.3%) 13 (34.2%) 6 (46.2%)
Follow up duration (years) 1.4 +/- 2.2 1.3 +/- 1.9 1.7 +/- 2.9 0.287
1 Mean +/- SD; n (%)
2 Welch Two Sample t-test; Fisher’s exact test; Wilcoxon rank sum test; Fisher’s Exact Test for Count Data with simulated p-value (based on 2000 replicates)
Code
library(irr)

# Volar tilt ICC
icc_volartilt <- df %>%
  select(mrn, starts_with("volartilt_"), -contains("avg")) %>%
  pivot_longer(cols = -mrn, 
               names_to = c("time", "rater"),
               names_pattern = "(.+)_(\\d)$") %>% 
  pivot_wider(names_from = "rater",
              values_from = value,
              names_prefix = "rater_") %>%
  select(starts_with("rater_")) %>%
  icc(.,
      model = 'twoway',
      type = 'agreement',
      unit = 'average')

# Ulnar variance ICC
icc_ulnarvar <- df %>%
  select(mrn, starts_with("ulnarvar_"), -contains("avg")) %>%
  pivot_longer(cols = -mrn, 
               names_to = c("time", "rater"),
               names_pattern = "(.+)_(\\d)$") %>% 
  pivot_wider(names_from = "rater",
              values_from = value,
              names_prefix = "rater_") %>%
  select(starts_with("rater_")) %>%
  icc(.,
      model = 'twoway',
      type = 'agreement',
      unit = 'average')

# Radial inclincation ICC
icc_radialinc <- df %>%
  select(mrn, starts_with("radialinc_"), -contains("avg")) %>%
  pivot_longer(cols = -mrn, 
               names_to = c("time", "rater"),
               names_pattern = "(.+)_(\\d)$") %>% 
  pivot_wider(names_from = "rater",
              values_from = value,
              names_prefix = "rater_") %>%
  select(starts_with("rater_")) %>%
  icc(.,
      model = 'twoway',
      type = 'agreement',
      unit = 'average')

3.2 Inter-rater reliability

Intra-class correlation coefficients (ICC) were used to assess the inter-rater reliability for radiographic measurements. ICCs were calculated using a two-way mixed effects model to evaluate the absolute agreement between raters. Good reliability was observed for radiographic measurement of volar tilt (ICC = 0.901, 95% CI [0.86, 0.93], p = < 0.001), ulnar variance (ICC = 0.805, 95% CI [0.66, 0.88], p = < 0.001), and radial inclination (ICC = 0.853, 95% CI [0.8, 0.89], p = < 0.001).

3.3 Radiographic Outcomes

Code
# Sub dfs
df_structural <- df %>% 
  filter(graft == 'structural') %>% 
  mutate(volartilt_severe = case_when(
    volartilt_preop_avg < 0 ~ TRUE,
    TRUE ~ FALSE
  ))

df_nonstructural <- df %>% 
  filter(graft == 'nonstructural') %>% 
  mutate(volartilt_severe = case_when(
    volartilt_preop_avg < 0 ~ TRUE,
    TRUE ~ FALSE
  ))

df_wedge <- df %>% 
  filter(osteotomy == 'Wedge')

df_distraction <- df %>% 
  filter(osteotomy == 'Distraction')


# Comparisons
t_volartilt_pre <-
  t.test(df_nonstructural$volartilt_preop_avg,
              df_structural$volartilt_preop_avg)

t_volartilt_delta <-
  wilcox.test(df_nonstructural$delta_volartilt,
         df_structural$delta_volartilt)


# Specific for distraction osteotomies
df_distraction_structural <- df %>% 
  filter(osteotomy == 'Distraction') %>% 
  filter(graft == 'structural')

df_distraction_nonstructural <- df %>%
  filter(osteotomy == 'Distraction') %>%
  filter(graft == 'nonstructural')


t_distraction_ulnarvar <-
  t.test(
    df_distraction_nonstructural$delta_ulnarvar,
    df_distraction_structural$delta_ulnarvar
  )

There was a wide range of preoperative sagittal plane deformity with respect to volar tilt (-41.55 degrees to 28.4 degrees), and patients in the structural graft group were associated with more severe sagittal plane deformity than the non-structural graft group (-20.5 +/- 16.0 degrees versus -7.0 +/- 21.5 degrees, 95% CI [1.5, 25.4], p = 0.028). Despite the more severe dorsal angulation in the structural graft group, there was no difference in the osteotomy gap size or degree of intraoperative volar tilt correction between groups (Table). Importantly, there was no difference in volar tilt subsidence (i.e., change from immediate postoperative to final follow-up) between graft type groups (-0.5 +/- 2.4 degrees versus -0.7 +/- 2.8 degrees, p = 0.658). Given that the structural graft group had more severe preoperative sagittal plane deformity but similar intraoperative correction and subsidence compared to the non-structural graft group, the structural graft group was associated with less optimal volar tilt at the postoperative and final timepoints (Table).

There was no difference in preoperative ulnar variance or radial inclination between the structural graft group and the non-structural graft group. Furthermore, there was no difference in the degree of intraoperative correction achieved or the amount of subsidence between graft type groups when patients who underwent wedge osteotomies and distraction osteotomies were considered together (Table).

Code
# All patients
t2_all <- df %>%
  select(
    graft,
    gap_avg,
    volartilt_preop_avg,
    correction_volartilt,
    volartilt_postop_avg,
    delta_volartilt,
    volartilt_finalfu_avg,
    ulnarvar_preop_avg,
    correction_ulnarvar,
    ulnarvar_postop_avg,
    delta_ulnarvar,
    ulnarvar_finalfu_avg,
    radialinc_preop_avg,
    correction_radialinc,
    radialinc_postop_avg,
    delta_radialinc,
    radialinc_finalfu_avg
  ) %>%
  mutate(graft = recode_factor(graft,
                               'nonstructural' = 'Non-structural',
                               'structural' = 'Structural')) %>%
  tbl_summary(
    by = graft,
    missing = 'no',
    type = list(
      ulnarvar_preop_avg ~ 'continuous',
      ulnarvar_postop_avg ~ 'continuous',
      correction_ulnarvar ~ 'continuous',
      ulnarvar_finalfu_avg ~ 'continuous',
      delta_ulnarvar ~ 'continuous'
    ),
    statistic = list(all_continuous() ~ '{mean} +/- {sd}'),
    digits = list(all_categorical() ~ c(0, 1),
                  all_continuous() ~ c(1, 1)),
    label = list(
      gap_avg ~ 'Osteotomy gap (mm)',
      volartilt_preop_avg ~ 'Preoperative',
      correction_volartilt ~ 'Intraoperative correction',
      volartilt_postop_avg ~ 'Postoperative',
      delta_volartilt ~ 'Subsidence',
      volartilt_finalfu_avg ~ 'Final',
      ulnarvar_preop_avg ~ 'Preoperative',
      correction_ulnarvar ~ 'Intraoperative correction',
      ulnarvar_postop_avg ~ 'Postoperative',
      delta_ulnarvar ~ 'Subsidence',
      ulnarvar_finalfu_avg ~ 'Final',
      radialinc_preop_avg ~ 'Preoperative',
      correction_radialinc ~ 'Intraoperative correction',
      radialinc_postop_avg ~ 'Postoperative',
      delta_radialinc ~ 'Subsidence',
      radialinc_finalfu_avg ~ 'Final'
    )
  ) %>%
  add_p(
    test = list(
      c('gap_avg',
        'volartilt_preop_avg',
        'correction_volartilt',
        'delta_volartilt',
        'radialinc_preop_avg',
        'correction_radialinc',
        'radialinc_postop_avg',
        'radialinc_finalfu_avg') ~ 't.test',
      c('volartilt_postop_avg',
        'volartilt_finalfu_avg',
        'ulnarvar_preop_avg',
        'correction_ulnarvar',
        'ulnarvar_postop_avg',
        'delta_ulnarvar',
        'ulnarvar_finalfu_avg',
        'delta_radialinc') ~ 'wilcox.test'
    ),
    pvalue_fun = function(x)
      style_pvalue(x, digits = 3),
    test.args = all_tests('fisher.test') ~ list(simulate.p.value = TRUE)
  ) %>%
  bold_p(t = 0.05) %>%
  # add_overall() %>%
  modify_spanning_header(all_stat_cols(stat_0 = F) ~ "**Graft Type**") %>%
  modify_caption("<div style='text-align: left; font-weight: bold'>
                 Radiographic Outcomes") %>%
  italicize_levels() %>% 
  modify_column_indent(
    columns = label,
    rows = variable %in% c(
      "correction_volartilt",
      "correction_ulnarvar",
      "correction_radialinc",
      "delta_volartilt",
      "delta_ulnarvar",
      "delta_radialinc"
      )
  ) %>% 
  as_gt() %>%
  tab_row_group(label = md('**Volar Tilt (Degrees)**'),
                rows = variable %in% c('volartilt_preop_avg',
                                       'correction_volartilt',
                                       'volartilt_postop_avg',
                                       'delta_volartilt',
                                       'volartilt_finalfu_avg'
                                       )) %>%
  tab_row_group(label = md('**Ulnar Variance (mm)**'),
                rows = variable %in% c('ulnarvar_preop_avg',
                                       'correction_ulnarvar',
                                       'ulnarvar_postop_avg',
                                       'delta_ulnarvar',
                                       'ulnarvar_finalfu_avg'
                                       )) %>%
  tab_row_group(label = md('**Radial Inclination (Degrees)**'),
                rows = variable %in% c('radialinc_preop_avg',
                                       'correction_radialinc',
                                       'radialinc_postop_avg',
                                       'delta_radialinc',
                                       'radialinc_finalfu_avg'
                                       )) %>%
  tab_row_group(label = md('**Osteotomy**'),
                rows = variable %in% c('gap_avg')) %>%
  row_group_order(groups = c(
    '**Osteotomy**',
    '**Volar Tilt (Degrees)**',
    '**Ulnar Variance (mm)**',
    '**Radial Inclination (Degrees)**'
  )) %>%
  gt::tab_options(table.font.size = px(12),
                  data_row.padding = gt::px(3)) %>% 
  tab_options(row_group.padding = px(8))

t2_all
Radiographic Outcomes
Characteristic Graft Type p-value2
Non-structural, N = 381 Structural, N = 131
Osteotomy
Osteotomy gap (mm) 4.8 +/- 2.8 5.6 +/- 3.9 0.515
Volar Tilt (Degrees)
Preoperative -7.0 +/- 21.5 -20.5 +/- 16.0 0.028
    Intraoperative correction 15.3 +/- 20.3 21.8 +/- 9.3 0.135
Postoperative 8.3 +/- 5.3 1.2 +/- 9.4 0.005
    Subsidence -0.7 +/- 2.8 -0.5 +/- 2.4 0.805
Final 7.6 +/- 6.1 0.8 +/- 10.1 0.020
Ulnar Variance (mm)
Preoperative 0.9 +/- 1.1 1.4 +/- 0.7 0.200
    Intraoperative correction -1.0 +/- 0.8 -1.2 +/- 1.4 0.303
Postoperative -0.2 +/- 0.7 0.2 +/- 0.9 0.075
    Subsidence 0.2 +/- 0.5 0.1 +/- 0.6 0.344
Final 0.0 +/- 0.8 0.3 +/- 0.7 0.314
Radial Inclination (Degrees)
Preoperative 14.9 +/- 6.1 16.0 +/- 7.5 0.641
    Intraoperative correction 5.3 +/- 4.6 6.1 +/- 6.1 0.657
Postoperative 20.1 +/- 5.7 21.4 +/- 4.0 0.398
    Subsidence -0.5 +/- 3.1 -0.8 +/- 2.6 0.905
Final 19.7 +/- 4.9 20.6 +/- 4.5 0.535
1 Mean +/- SD
2 Welch Two Sample t-test; Wilcoxon rank sum exact test; Wilcoxon rank sum test
Code
t2_wedge <- df %>%
  filter(osteotomy == 'Wedge') %>% 
  select(
    graft,
    gap_avg,
    volartilt_preop_avg,
    correction_volartilt,
    volartilt_postop_avg,
    delta_volartilt,
    volartilt_finalfu_avg,
    ulnarvar_preop_avg,
    correction_ulnarvar,
    ulnarvar_postop_avg,
    delta_ulnarvar,
    ulnarvar_finalfu_avg,
    radialinc_preop_avg,
    correction_radialinc,
    radialinc_postop_avg,
    delta_radialinc,
    radialinc_finalfu_avg
  ) %>%
  mutate(graft = recode_factor(graft,
                               'nonstructural' = 'Non-structural',
                               'structural' = 'Structural')) %>%
  tbl_summary(
    by = graft,
    missing = 'no',
    type = list(
      ulnarvar_preop_avg ~ 'continuous',
      ulnarvar_postop_avg ~ 'continuous',
      correction_ulnarvar ~ 'continuous',
      ulnarvar_finalfu_avg ~ 'continuous',
      delta_ulnarvar ~ 'continuous'
    ),
    statistic = list(all_continuous() ~ '{mean} +/- {sd}'),
    digits = list(all_categorical() ~ c(0, 1),
                  all_continuous() ~ c(1, 1)),
    label = list(
      gap_avg ~ 'Osteotomy gap (mm)',
      volartilt_preop_avg ~ 'Preoperative',
      correction_volartilt ~ 'Intraoperative correction',
      volartilt_postop_avg ~ 'Postoperative',
      delta_volartilt ~ 'Subsidence',
      volartilt_finalfu_avg ~ 'Final',
      ulnarvar_preop_avg ~ 'Preoperative',
      correction_ulnarvar ~ 'Intraoperative correction',
      ulnarvar_postop_avg ~ 'Postoperative',
      delta_ulnarvar ~ 'Subsidence',
      ulnarvar_finalfu_avg ~ 'Final',
      radialinc_preop_avg ~ 'Preoperative',
      correction_radialinc ~ 'Intraoperative correction',
      radialinc_postop_avg ~ 'Postoperative',
      delta_radialinc ~ 'Subsidence',
      radialinc_finalfu_avg ~ 'Final'
    )
  ) %>%
  add_p(
    test = list(
      c('volartilt_preop_avg',
        'correction_volartilt',
        'delta_volartilt',
        'correction_ulnarvar',
        'radialinc_postop_avg',
        'radialinc_finalfu_avg',
        'delta_radialinc') ~ 't.test',
      c('gap_avg',
        'volartilt_postop_avg',
        'volartilt_finalfu_avg',
        'ulnarvar_preop_avg',
        'ulnarvar_postop_avg',
        'delta_ulnarvar',
        'ulnarvar_finalfu_avg',
        'radialinc_preop_avg',
        'correction_radialinc') ~ 'wilcox.test'
    ),
    pvalue_fun = function(x)
      style_pvalue(x, digits = 3),
    test.args = all_tests('fisher.test') ~ list(simulate.p.value = TRUE)
  ) %>%
  bold_p(t = 0.05) %>%
  # add_overall() %>%
  modify_spanning_header(all_stat_cols(stat_0 = F) ~ "**Graft Type**") %>%
  modify_caption("<div style='text-align: left; font-weight: bold'>
                 Radiographic Outcomes for Wedge and Distraction Osteotomies") %>%
  italicize_levels() %>% 
  modify_column_indent(
    columns = label,
    rows = variable %in% c(
      "correction_volartilt",
      "correction_ulnarvar",
      "correction_radialinc",
      "delta_volartilt",
      "delta_ulnarvar",
      "delta_radialinc"
      )
  )

# t2_wedge
Code
t2_distraction <- df %>%
  filter(osteotomy == 'Distraction') %>% 
  select(
    graft,
    gap_avg,
    volartilt_preop_avg,
    correction_volartilt,
    volartilt_postop_avg,
    delta_volartilt,
    volartilt_finalfu_avg,
    ulnarvar_preop_avg,
    correction_ulnarvar,
    ulnarvar_postop_avg,
    delta_ulnarvar,
    ulnarvar_finalfu_avg,
    radialinc_preop_avg,
    correction_radialinc,
    radialinc_postop_avg,
    delta_radialinc,
    radialinc_finalfu_avg
  ) %>%
  mutate(graft = recode_factor(graft,
                               'nonstructural' = 'Non-structural',
                               'structural' = 'Structural')) %>%
  tbl_summary(
    by = graft,
    missing = 'no',
    type = list(
      ulnarvar_preop_avg ~ 'continuous',
      ulnarvar_postop_avg ~ 'continuous',
      correction_ulnarvar ~ 'continuous',
      ulnarvar_finalfu_avg ~ 'continuous',
      delta_ulnarvar ~ 'continuous'
    ),
    statistic = list(all_continuous() ~ '{mean} +/- {sd}'),
    digits = list(all_categorical() ~ c(0, 1),
                  all_continuous() ~ c(1, 1)),
    label = list(
      gap_avg ~ 'Osteotomy gap (mm)',
      volartilt_preop_avg ~ 'Preoperative',
      correction_volartilt ~ 'Intraoperative correction',
      volartilt_postop_avg ~ 'Postoperative',
      delta_volartilt ~ 'Subsidence',
      volartilt_finalfu_avg ~ 'Final',
      ulnarvar_preop_avg ~ 'Preoperative',
      correction_ulnarvar ~ 'Intraoperative correction',
      ulnarvar_postop_avg ~ 'Postoperative',
      delta_ulnarvar ~ 'Subsidence',
      ulnarvar_finalfu_avg ~ 'Final',
      radialinc_preop_avg ~ 'Preoperative',
      correction_radialinc ~ 'Intraoperative correction',
      radialinc_postop_avg ~ 'Postoperative',
      delta_radialinc ~ 'Subsidence',
      radialinc_finalfu_avg ~ 'Final'
    )
  ) %>%
  add_p(
    test = list(
      c('gap_avg',
        'volartilt_postop_avg',
        'volartilt_preop_avg',
        'volartilt_finalfu_avg',
        'correction_volartilt',
        'delta_volartilt',
        'radialinc_preop_avg',
        'correction_radialinc',
        'radialinc_postop_avg',
        'radialinc_finalfu_avg') ~ 't.test',
      c('ulnarvar_preop_avg',
        'correction_ulnarvar',
        'ulnarvar_postop_avg',
        'delta_ulnarvar',
        'ulnarvar_finalfu_avg',
        'delta_radialinc') ~ 'wilcox.test'
    ),
    pvalue_fun = function(x)
      style_pvalue(x, digits = 3),
    test.args = all_tests('fisher.test') ~ list(simulate.p.value = TRUE)
  ) %>%
  bold_p(t = 0.05) %>%
  # add_overall() %>%
  modify_spanning_header(all_stat_cols(stat_0 = F) ~ "**Graft Type**") %>%
  modify_caption("<div style='text-align: left; font-weight: bold'>
                 Radiographic Outcomes for Wedge and Distraction Osteotomies") %>%
  italicize_levels() %>% 
  modify_column_indent(
    columns = label,
    rows = variable %in% c(
      "correction_volartilt",
      "correction_ulnarvar",
      "correction_radialinc",
      "delta_volartilt",
      "delta_ulnarvar",
      "delta_radialinc"
      )
  )

# t2_distraction

Patients were then stratified based on the specific type of osteotomy performed and the radiographic outcomes were again compared between structural grafts and non-structural grafts (Figure 1). For patients who underwent wedge osteotomies, there was no significant difference in volar tilt subsidence, ulnar variance subsidence, or radial inclination subsidence between structural grafts and non-structural grafts (Table). Similarly, for patients who underwent distraction osteotomies, there was no significant difference in volar tilt subsidence, ulnar variance subsidence, or radial inclination subsidence between structural grafts and non-structural grafts.

Code
t2_combined <- tbl_merge(
  tbls = list(t2_wedge,
              t2_distraction),
          tab_spanner = c(
            "**Wedge Osteotomies**", 
            "**Distraction Osteotomies**"
          )) %>% 
  as_gt() %>%
  tab_row_group(label = md('**Volar Tilt (Degrees)**'),
                rows = variable %in% c('volartilt_preop_avg',
                                       'correction_volartilt',
                                       'volartilt_postop_avg',
                                       'delta_volartilt',
                                       'volartilt_finalfu_avg'
                                       )) %>%
  tab_row_group(label = md('**Ulnar Variance (mm)**'),
                rows = variable %in% c('ulnarvar_preop_avg',
                                       'correction_ulnarvar',
                                       'ulnarvar_postop_avg',
                                       'delta_ulnarvar',
                                       'ulnarvar_finalfu_avg'
                                       )) %>%
  tab_row_group(label = md('**Radial Inclination (Degrees)**'),
                rows = variable %in% c('radialinc_preop_avg',
                                       'correction_radialinc',
                                       'radialinc_postop_avg',
                                       'delta_radialinc',
                                       'radialinc_finalfu_avg'
                                       )) %>%
  tab_row_group(label = md('**Osteotomy**'),
                rows = variable %in% c('gap_avg')) %>%
  row_group_order(groups = c(
    '**Osteotomy**',
    '**Volar Tilt (Degrees)**',
    '**Ulnar Variance (mm)**',
    '**Radial Inclination (Degrees)**'
  )) %>%
  gt::tab_options(table.font.size = px(12),
                  data_row.padding = gt::px(3)) %>% 
  tab_options(row_group.padding = px(8))

t2_combined
Table 2:

Radiographic Outcomes for Wedge and Distraction Osteotomies

Characteristic Wedge Osteotomies Distraction Osteotomies
Non-structural, N = 131 Structural, N = 61 p-value2 Non-structural, N = 251 Structural, N = 71 p-value3
Osteotomy
Osteotomy gap (mm) 3.7 +/- 2.0 7.0 +/- 4.9 0.160 5.3 +/- 3.0 4.3 +/- 2.7 0.431
Volar Tilt (Degrees)
Preoperative -1.5 +/- 22.6 -16.3 +/- 23.0 0.258 -9.9 +/- 20.8 -23.5 +/- 9.7 0.022
    Intraoperative correction 10.1 +/- 21.9 18.7 +/- 13.6 0.338 18.0 +/- 19.4 24.0 +/- 4.7 0.171
Postoperative 8.6 +/- 3.7 2.1 +/- 12.6 0.179 8.1 +/- 6.1 0.5 +/- 6.6 0.023
    Subsidence -0.1 +/- 2.9 -0.7 +/- 2.9 0.698 -1.0 +/- 2.8 -0.3 +/- 2.0 0.483
Final 8.5 +/- 5.6 1.4 +/- 14.0 0.323 7.2 +/- 6.4 0.2 +/- 6.1 0.025
Ulnar Variance (mm)
Preoperative 0.5 +/- 1.2 1.6 +/- 0.4 0.070 1.1 +/- 0.9 1.3 +/- 0.9 0.869
    Intraoperative correction -0.8 +/- 0.8 -1.7 +/- 1.2 0.152 -1.2 +/- 0.7 -0.9 +/- 1.5 0.981
Postoperative -0.3 +/- 0.8 -0.1 +/- 1.0 0.170 -0.1 +/- 0.6 0.4 +/- 0.8 0.226
    Subsidence 0.0 +/- 0.2 0.3 +/- 0.9 0.910 0.3 +/- 0.6 0.0 +/- 0.0 0.268
Final -0.3 +/- 0.9 0.2 +/- 0.4 0.074 0.2 +/- 0.7 0.4 +/- 0.8 0.823
Radial Inclination (Degrees)
Preoperative 19.4 +/- 5.6 15.9 +/- 6.2 0.443 12.5 +/- 5.0 16.1 +/- 8.8 0.339
    Intraoperative correction 2.4 +/- 3.7 6.5 +/- 6.3 0.208 6.8 +/- 4.4 5.9 +/- 6.4 0.736
Postoperative 21.8 +/- 5.1 20.7 +/- 4.9 0.674 19.3 +/- 6.0 21.9 +/- 3.3 0.141
    Subsidence -1.7 +/- 2.1 -1.8 +/- 3.7 0.965 0.2 +/- 3.4 0.1 +/- 0.9 0.909
Final 20.1 +/- 5.5 19.0 +/- 5.3 0.673 19.4 +/- 4.6 22.0 +/- 3.5 0.139
1 Mean +/- SD
2 Wilcoxon rank sum test; Welch Two Sample t-test; Wilcoxon rank sum exact test
3 Welch Two Sample t-test; Wilcoxon rank sum test
Code
# Volar tilt
p1_volartilt <- df %>%
  select(
    mrn,
    graft,
    osteotomy,
    volartilt_preop_avg,
    volartilt_postop_avg,
    volartilt_finalfu_avg
  ) %>%
  mutate(graft = recode_factor(graft,
                               'nonstructural' = 'Non-structural',
                               'structural' = 'Structural')) %>%
  pivot_longer(
    cols = contains('volartilt_'),
    names_to = 'timepoint',
    names_prefix = 'volartilt_',
    values_to = 'value'
  ) %>%
  ggplot(., aes(x = timepoint, y = value, fill = osteotomy)) +
  geom_boxplot(aes()) +
  geom_point(aes(x = timepoint,
                 y = value)) +
  scale_x_discrete(
    limits = c('preop_avg', 'postop_avg', 'finalfu_avg'),
    labels = c('Preoperative', 'Postoperative', 'Final')
  ) +
  facet_grid(vars(osteotomy), vars(graft)) + 
  ylab('Volar Tilt (Degrees)') + 
  xlab('') + 
  theme_gray(base_size = 14) + 
  theme(legend.position = "") 
  

# Ulnar variance
p1_ulnarvar <- df %>%
  select(
    mrn,
    graft,
    osteotomy,
    ulnarvar_preop_avg,
    ulnarvar_postop_avg,
    ulnarvar_finalfu_avg
  ) %>%
  mutate(graft = recode_factor(graft,
                               'nonstructural' = 'Non-structural',
                               'structural' = 'Structural')) %>%
  pivot_longer(
    cols = contains('ulnarvar_'),
    names_to = 'timepoint',
    names_prefix = 'ulnarvar_',
    values_to = 'value'
  ) %>%
  ggplot(., aes(x = timepoint, y = value, fill = osteotomy)) +
  geom_boxplot(aes()) +
  geom_dotplot(binaxis='y', stackdir='center', dotsize=1.5, fill = 'black') +
  scale_x_discrete(
    limits = c('preop_avg', 'postop_avg', 'finalfu_avg'),
    labels = c('Preoperative', 'Postoperative', 'Final')
  ) +
  facet_grid(vars(osteotomy), vars(graft)) + 
  ylab('Ulnar Variance (mm)') + 
  xlab('') + 
  theme_gray(base_size = 14) + 
  theme(legend.position = "") 


# Radial inclination
p1_radialinc <- df %>%
  select(
    mrn,
    graft,
    osteotomy,
    radialinc_preop_avg,
    radialinc_postop_avg,
    radialinc_finalfu_avg
  ) %>%
  mutate(graft = recode_factor(graft,
                               'nonstructural' = 'Non-structural',
                               'structural' = 'Structural')) %>%
  pivot_longer(
    cols = contains('radialinc_'),
    names_to = 'timepoint',
    names_prefix = 'radialinc_',
    values_to = 'value'
  ) %>%
  ggplot(., aes(x = timepoint, y = value, fill = osteotomy)) +
  geom_boxplot(aes()) +
  geom_point(aes(x = timepoint,
                 y = value)) +
  scale_x_discrete(
    limits = c('preop_avg', 'postop_avg', 'finalfu_avg'),
    labels = c('Preoperative', 'Postoperative', 'Final')
  ) +
  facet_grid(vars(osteotomy), vars(graft)) + 
  ylab('Radial Inclination (Degrees)') + 
  xlab('') + 
  theme_gray(base_size = 14) + 
  theme(legend.position = "") 
Code
p1 <- p1_volartilt / p1_ulnarvar / p1_radialinc

p1

Figure 1: Radiographic outcomes

3.4 Clinical Outcomes

Code
t3_wedge <- df %>%
  filter(osteotomy == 'Wedge') %>% 
  select(
    graft,
    flex.finalfu,
    ext.finalfu,
    prono.finalfu,
    sup.finalfu,
    reoperation
  ) %>%
  mutate(graft = recode_factor(graft,
                               'nonstructural' = 'Non-structural',
                               'structural' = 'Structural')) %>%
  tbl_summary(
    by = graft,
    missing = 'no',
    type = list(
      prono.finalfu ~ 'continuous',
      sup.finalfu ~ 'continuous'
    ),
    statistic = list(all_continuous() ~ '{mean} +/- {sd}'),
    digits = list(all_categorical() ~ c(0, 1),
                  all_continuous() ~ c(1, 1)),
    label = list(
      flex.finalfu ~ 'Flexion',
      ext.finalfu ~ 'Extension',
      prono.finalfu ~ 'Pronation',
      sup.finalfu ~ 'Supination',
      reoperation ~ 'Reoperation'
    )
  ) %>%
  add_p(
    test = list(
      c('ext.finalfu',
        'flex.finalfu') ~ 't.test',
      c('prono.finalfu',
        'sup.finalfu') ~ 'wilcox.test'
    ),
    pvalue_fun = function(x)
      style_pvalue(x, digits = 3),
    test.args = all_tests('fisher.test') ~ list(simulate.p.value = TRUE)
  ) %>%
  bold_p(t = 0.05) %>%
  modify_spanning_header(all_stat_cols(stat_0 = F) ~ "**Graft Type**") %>%
  modify_caption("<div style='text-align: left; font-weight: bold'>
                 Clinical Outcomes for Wedge and Distraction Osteotomies") %>%
  italicize_levels()
Code
t3_distraction <- df %>%
  filter(osteotomy == 'Distraction') %>% 
  select(
    graft,
    flex.finalfu,
    ext.finalfu,
    prono.finalfu,
    sup.finalfu,
    reoperation
  ) %>%
  mutate(graft = recode_factor(graft,
                               'nonstructural' = 'Non-structural',
                               'structural' = 'Structural')) %>%
  tbl_summary(
    by = graft,
    missing = 'no',
    type = list(
      prono.finalfu ~ 'continuous',
      sup.finalfu ~ 'continuous',
      flex.finalfu ~ 'continuous'
    ),
    statistic = list(all_continuous() ~ '{mean} +/- {sd}'),
    digits = list(all_categorical() ~ c(0, 1),
                  all_continuous() ~ c(1, 1)),
    label = list(
      flex.finalfu ~ 'Flexion',
      ext.finalfu ~ 'Extension',
      prono.finalfu ~ 'Pronation',
      sup.finalfu ~ 'Supination',
      reoperation ~ 'Reoperation'
    )
  ) %>%
  add_p(
    test = list(
      c('ext.finalfu',
        'flex.finalfu') ~ 't.test',
      c('prono.finalfu',
        'sup.finalfu') ~ 'wilcox.test'
    ),
    pvalue_fun = function(x)
      style_pvalue(x, digits = 3),
    test.args = all_tests('fisher.test') ~ list(simulate.p.value = TRUE)
  ) %>%
  bold_p(t = 0.05) %>%
  modify_spanning_header(all_stat_cols(stat_0 = F) ~ "**Graft Type**") %>%
  modify_caption("<div style='text-align: left; font-weight: bold'>
                 Clinical Outcomes for Wedge and Distraction Osteotomies") %>%
  italicize_levels()


# Reoperation rate comparison
c_reop <- chisq.test(table(df$graft, df$reoperation))

There was no difference in wrist range of motion between structural and non-structural grafts (Table). Likewise, there was no difference in the reoperation rate between graft type groups. 6 patients in the non-structural graft group (15.8%) underwent reoperation and 3 patients in the structural graft group (23.1%) underwent reoperation (χ2 = 0.018, p = 0.893). There was no difference in reoperation rate when stratifying by osteotomy type (Table). Of the 9 total patients who underwent reoperation following malunion repair, 8 patients underwent removal of symptomatic hardware (2 patients with a concomitant ulnar shortening osteotomy and 1 patient with concomitant EIP to EPL transfer and Darrach procedure).

Code
t3_combined <- tbl_merge(
  tbls = list(t3_wedge,
              t3_distraction),
          tab_spanner = c(
            "**Wedge Osteotomies**", 
            "**Distraction Osteotomies**"
          )) %>% 
  as_gt() %>%
  tab_row_group(label = md('**Range of Motion (Degrees)**'),
                rows = variable %in% c(
                  'flex.finalfu',
                  'ext.finalfu',
                  'prono.finalfu',
                  'sup.finalfu'
                  )) %>%
  row_group_order(groups = c(
    NA,
    '**Range of Motion (Degrees)**'
  )) %>%
  gt::tab_options(table.font.size = px(12),
                  data_row.padding = gt::px(3)) %>% 
  tab_options(row_group.padding = px(8))

t3_combined
Table 3:

Clinical Outcomes for Wedge and Distraction Osteotomies

Characteristic Wedge Osteotomies Distraction Osteotomies
Non-structural, N = 131 Structural, N = 61 p-value2 Non-structural, N = 251 Structural, N = 71 p-value2
Reoperation 0 (0.0%) 2 (33.3%) 0.088 6 (25.0%) 1 (14.3%) >0.999
Range of Motion (Degrees)
Flexion 55.0 +/- 17.9 51.0 +/- 12.4 0.616 50.5 +/- 16.7 56.0 +/- 10.8 0.397
Extension 51.7 +/- 19.0 55.0 +/- 20.6 0.772 45.8 +/- 20.3 45.0 +/- 22.4 0.945
Pronation 75.5 +/- 9.8 83.8 +/- 4.8 0.187 74.5 +/- 19.3 77.0 +/- 9.1 >0.999
Supination 78.0 +/- 7.9 76.3 +/- 11.1 0.941 70.5 +/- 17.4 72.0 +/- 10.4 0.861
1 Mean +/- SD; n (%)
2 Welch Two Sample t-test; Wilcoxon rank sum test; Fisher’s exact test

3.5 PROs

Code
# Load PRO data
data_path_pros <- here('Distal-Radius-Malunion_Data_PROs_20231105.xlsx')

df_pros <- read.xlsx(data_path_pros,
                     detectDates = TRUE) %>%
  mutate(across(c(mrn),
                factor),
         across(c(date_pro),
                ymd))

# Merge with other df
df_temp <- df %>% select(mrn,
                         dos,
                         graft,
                         osteotomy)

df_pros <- df_pros %>% left_join(df_temp,
                                 by = 'mrn')

# Remove excluded patients
df_pros <- df_pros %>% 
  filter(!is.na(dos))

# Time from surgery to PRO var
df_pros <- df_pros %>% 
  mutate(timepoint = (date_pro - dos) %>% as.numeric()) %>% 
  mutate(timepoint_bin = case_when(
    timepoint <= 0 ~ 0,
    timepoint > 0 ~ timepoint
  ))


# Final follow up PROs
df_pros_final <- df_pros %>%
  group_by(mrn) %>%
  # Only get last timepoint
  filter(timepoint == max(timepoint)) %>%
  filter(timepoint > 0) %>%
  # Remove anyone without any PROs completed
  filter(!(
    is.na(promis_intensity) &
      is.na(promis_interference) & is.na(promis_ue_function)
  )) %>% 
  ungroup()

A small proportion of patients completed PROMIS questionnaires at the 1 year postoperative timepoint (Table). There was no difference in PROMIS Intensity score, PROMIS Interference score, or PROMIS Upper Extremity Function score between patients with non-structural grafts and those with structural grafts.

Code
t_pros <- df_pros_final %>%
  select(
    graft,
    osteotomy,
    contains('promis')
  ) %>%
  mutate(graft = recode_factor(graft,
                               'nonstructural' = 'Non-structural',
                               'structural' = 'Structural')) %>%
  tbl_summary(
    by = graft,
    missing = 'no',
    type = list(
      promis_ue_function ~ 'continuous'
    ),
    statistic = list(all_continuous() ~ '{mean} +/- {sd}'),
    digits = list(all_categorical() ~ c(0, 1),
                  all_continuous() ~ c(1, 1)),
    label = list(
      osteotomy ~ 'Osteotomy',
      promis_intensity ~ 'PROMIS Intensity',
      promis_interference ~ 'PROMIS Interference',
      promis_ue_function ~ 'PROMIS UE Function'
    )
  ) %>%
  add_p(
    test = list(
      c('promis_intensity',
        'promis_interference') ~ 't.test',
      c('promis_ue_function') ~ 'wilcox.test'
    ),
    pvalue_fun = function(x)
      style_pvalue(x, digits = 3),
    test.args = all_tests('fisher.test') ~ list(simulate.p.value = TRUE)
  ) %>%
  bold_p(t = 0.05) %>%
  # add_overall() %>%
  modify_spanning_header(all_stat_cols(stat_0 = F) ~ "**Graft Type**") %>%
  modify_caption("<div style='text-align: left; font-weight: bold'>
                 Functional Outcomes") %>%
  italicize_levels() %>% 
  as_gt() %>% 
  gt::tab_options(table.font.size = px(12),
                  data_row.padding = gt::px(3))

t_pros
Table 4:

Functional outcomes at a minimum of 1 year following surgery.

Characteristic Graft Type p-value2
Non-structural, N = 101 Structural, N = 41
Osteotomy

>0.999
    Distraction 6 (60.0%) 2 (50.0%)
    Wedge 4 (40.0%) 2 (50.0%)
PROMIS Intensity 47.8 +/- 8.6 48.4 +/- 13.0 0.934
PROMIS Interference 59.5 +/- 5.4 60.4 +/- 10.2 0.874
PROMIS UE Function 37.6 +/- 11.4 31.0 +/- 2.0 0.473
1 n (%); Mean +/- SD
2 Fisher’s exact test; Welch Two Sample t-test; Wilcoxon rank sum test