Introduction to tooth

The tooth package provides tools for dental public health research: caries index calculation, odontogram heatmap visualisations, and tooth numbering conversion.

Sample Data

The package includes sim_exam, a simulated dataset with 20 patients, 7 teeth per quadrant, and 7 surfaces per tooth (5 coronal + 2 root).

library(tooth)
data(sim_exam)
head(sim_exam)
#>   record_id tooth_num tooth_surface code lesion_code act filling_code
#> 1       P01       ll1           buc    2           0   0            0
#> 2       P01       ll1           dis    2           0   0            4
#> 3       P01       ll1           lin    2           0   0            0
#> 4       P01       ll1           mes    2           4   2            0
#> 5       P01       ll1           occ    2           0   0            0
#> 6       P01       ll1         rootb    2           5   1            0

Calculating DMFT

calc_dmft() takes long-format surface data and returns one row per person with Decayed (DT), Filled (FT), Missing (MT), and combined index counts.

dmft <- calc_dmft(sim_exam)
head(dmft)
#> # A tibble: 6 × 9
#>   record_id    DT    FT    MT   DFT  DMFT num_teeth DT_yn DFT_yn
#>   <chr>     <int> <int> <int> <int> <int>     <int> <int>  <int>
#> 1 P01          18     2     2    20    22        26     1      1
#> 2 P02          17     3     3    20    23        25     1      1
#> 3 P03          16     6     1    22    23        27     1      1
#> 4 P04          18     5     1    23    24        27     1      1
#> 5 P05          10     8     1    18    19        27     1      1
#> 6 P06          18     5     0    23    23        28     1      1
summary(dmft[, c("DT", "FT", "MT", "DMFT", "num_teeth")])
#>        DT              FT             MT            DMFT         num_teeth    
#>  Min.   :10.00   Min.   :2.00   Min.   :0.00   Min.   :19.00   Min.   :25.00  
#>  1st Qu.:17.00   1st Qu.:3.00   1st Qu.:1.00   1st Qu.:23.00   1st Qu.:26.00  
#>  Median :18.00   Median :4.00   Median :1.00   Median :24.00   Median :27.00  
#>  Mean   :18.05   Mean   :4.45   Mean   :1.25   Mean   :23.75   Mean   :26.75  
#>  3rd Qu.:20.00   3rd Qu.:6.00   3rd Qu.:2.00   3rd Qu.:25.25   3rd Qu.:27.00  
#>  Max.   :22.00   Max.   :8.00   Max.   :3.00   Max.   :26.00   Max.   :28.00

Stratification

Add a grouping variable and pass it via strata:

sim_exam$treatment <- ifelse(
  as.numeric(gsub("P", "", sim_exam$record_id)) <= 10, "SDF", "ART"
)

dmft_by_arm <- calc_dmft(sim_exam, strata = "treatment")
head(dmft_by_arm)
#> # A tibble: 6 × 10
#>   record_id treatment    DT    FT    MT   DFT  DMFT num_teeth DT_yn DFT_yn
#>   <chr>     <chr>     <int> <int> <int> <int> <int>     <int> <int>  <int>
#> 1 P01       SDF          18     2     2    20    22        26     1      1
#> 2 P02       SDF          17     3     3    20    23        25     1      1
#> 3 P03       SDF          16     6     1    22    23        27     1      1
#> 4 P04       SDF          18     5     1    23    24        27     1      1
#> 5 P05       SDF          10     8     1    18    19        27     1      1
#> 6 P06       SDF          18     5     0    23    23        28     1      1

Separate Root Caries

dmft_root <- calc_dmft(sim_exam, root_lesion_col = "lesion_code")
head(dmft_root[, c("record_id", "DT", "MT", "DMFT", "RDT")])
#> # A tibble: 6 × 5
#>   record_id    DT    MT  DMFT   RDT
#>   <chr>     <int> <int> <int> <int>
#> 1 P01          18     2    22     9
#> 2 P02          17     3    23     6
#> 3 P03          16     1    23    11
#> 4 P04          18     1    24     8
#> 5 P05          10     1    19     8
#> 6 P06          18     0    23     8

RDT (root-decayed teeth) is counted independently from coronal DT.

Calculating DMFS

dmfs <- calc_dmfs(sim_exam)
head(dmfs)
#> # A tibble: 6 × 8
#>   record_id    DS    FS    MS   DFS  DMFS DS_yn DFS_yn
#>   <chr>     <dbl> <int> <dbl> <int> <int> <int>  <int>
#> 1 P01          21     8    10    29    39     1      1
#> 2 P02          27    11    15    38    53     1      1
#> 3 P03          25    13     5    38    43     1      1
#> 4 P04          31    10     5    41    46     1      1
#> 5 P05          12    14     5    26    31     1      1
#> 6 P06          23    16     0    39    39     1      1

Drawing a Single Tooth

draw_tooth() returns polygon geometry for one tooth. Use it to understand the surface layout or build custom visualisations.

library(ggplot2)

sv <- data.frame(
  tooth_surface = c("buc", "lin", "mes", "dis", "occ", "rootb", "rootl"),
  prop = c(0.12, 0.05, 0.30, 0.25, 0.45, 0.08, 0.03)
)

parts <- draw_tooth("ur1", "ur", 1, TRUE, sv,
                    surfaces = c("buc","lin","mes","dis","occ","rootb","rootl"),
                    display_label = "FDI 11")

ggplot() +
  geom_polygon(data = parts$crown,
               aes(x = x, y = y, group = group_id, fill = value),
               color = "grey50") +
  geom_polygon(data = parts$roots,
               aes(x = x, y = y, group = group_id, fill = value),
               color = "grey50") +
  geom_segment(data = parts$diags,
               aes(x = x, y = y, xend = xend, yend = yend),
               color = "grey50") +
  geom_path(data = parts$outline, aes(x = x, y = y),
            color = "grey40") +
  geom_text(data = parts$labels,
            aes(x = x, y = y, label = label),
            size = 5, fontface = "bold", color = "grey40") +
  geom_text(data = parts$num_label,
            aes(x = x, y = y, label = label),
            size = 7, fontface = "bold") +
  scale_fill_gradient(low = "white", high = "#C62828",
                      limits = c(0, 0.5), name = "Proportion") +
  coord_fixed() +
  theme_void() +
  theme(plot.background = element_rect(fill = "white", color = NA))

Surface abbreviations: B = Buccal, L = Lingual, M = Mesial, D = Distal, O = Occlusal, RB = Root-Buccal, RL = Root-Lingual.

Odontogram Heatmaps

Preparing data

Aggregate surface data to proportions per tooth-surface:

library(dplyr)
#> 
#> Attaching package: 'dplyr'
#> The following objects are masked from 'package:stats':
#> 
#>     filter, lag
#> The following objects are masked from 'package:base':
#> 
#>     intersect, setdiff, setequal, union

decay_prop <- sim_exam %>%
  filter(tooth_surface %in% c("buc", "lin", "mes", "dis", "occ")) %>%
  group_by(tooth_num, tooth_surface) %>%
  summarise(
    prop = mean(lesion_code %in% 3:6 & act == 2, na.rm = TRUE),
    .groups = "drop"
  )

Basic odontogram

build_odontogram(
  data = decay_prop,
  teeth_per_quadrant = 7,
  title = "Active Caries Proportion by Surface",
  legend_title = "Caries\nProportion",
  surfaces = c("buc", "lin", "mes", "dis", "occ")
)

With root surfaces (RB and RL)

decay_all <- sim_exam %>%
  group_by(tooth_num, tooth_surface) %>%
  summarise(
    prop = mean(lesion_code %in% 3:6 & act == 2, na.rm = TRUE),
    .groups = "drop"
  )

build_odontogram(
  data = decay_all,
  teeth_per_quadrant = 7,
  title = "Active Caries \u2014 Coronal and Root Surfaces",
  legend_title = "Caries\nProportion",
  surfaces = c("buc", "lin", "mes", "dis", "occ", "rootb", "rootl")
)

No surface labels

Set show_labels = FALSE for a cleaner publication-ready look:

build_odontogram(
  data = decay_prop,
  teeth_per_quadrant = 7,
  title = "Active Caries \u2014 Clean Export",
  surfaces = c("buc", "lin", "mes", "dis", "occ"),
  show_labels = FALSE,
  tooth_label_size = 3.5
)

FDI tooth numbering

Display tooth numbers in FDI (ISO 3950) notation instead of quadrant format:

build_odontogram(
  data = decay_prop,
  teeth_per_quadrant = 7,
  title = "Active Caries \u2014 FDI Numbering",
  surfaces = c("buc", "lin", "mes", "dis", "occ"),
  numbering = "fdi"
)

Fixed colour scale

Use min_val and max_val to ensure consistent colour scales across multiple plots:

build_odontogram(
  data = decay_prop,
  teeth_per_quadrant = 7,
  title = "Fixed Scale (0 to 0.5)",
  surfaces = c("buc", "lin", "mes", "dis", "occ"),
  min_val = 0, max_val = 0.5
)

Stratified by treatment arm

decay_by_arm <- sim_exam %>%
  filter(tooth_surface %in% c("buc", "lin", "mes", "dis", "occ")) %>%
  group_by(treatment, tooth_num, tooth_surface) %>%
  summarise(
    prop = mean(lesion_code %in% 3:6 & act == 2, na.rm = TRUE),
    .groups = "drop"
  )

build_odontogram(
  data = decay_by_arm,
  teeth_per_quadrant = 7,
  title = "Active Caries",
  legend_title = "Caries\nProportion",
  surfaces = c("buc", "lin", "mes", "dis", "occ"),
  strata = "treatment",
  ncol = 1
)

Custom strata labels

Rename strata panels with strata_labels:

build_odontogram(
  data = decay_by_arm,
  teeth_per_quadrant = 7,
  title = "Active Caries",
  surfaces = c("buc", "lin", "mes", "dis", "occ"),
  strata = "treatment",
  strata_labels = c("SDF" = "Silver Diamine Fluoride",
                    "ART" = "Atraumatic Restorative Treatment"),
  ncol = 1
)

Summary statistics per stratum

Pass a stats data frame to display sample size and clinical measures below each panel title:

# Compute stats from DMFT results
dmft_summary <- dmft_by_arm %>%
  group_by(treatment) %>%
  summarise(
    n = dplyr::n(),
    mean_DT = round(mean(DT, na.rm = TRUE), 1),
    mean_DMFT = round(mean(DMFT, na.rm = TRUE), 1),
    .groups = "drop"
  )
dmft_summary
#> # A tibble: 2 × 4
#>   treatment     n mean_DT mean_DMFT
#>   <chr>     <int>   <dbl>     <dbl>
#> 1 ART          10    18.5      24  
#> 2 SDF          10    17.6      23.5

build_odontogram(
  data = decay_by_arm,
  teeth_per_quadrant = 7,
  title = "Active Caries",
  surfaces = c("buc", "lin", "mes", "dis", "occ"),
  strata = "treatment",
  stats = dmft_summary,
  ncol = 1
)

Significance testing with p-value stars

Add stats_test, stats_var, and stats_raw to compute and display significance stars comparing strata:

build_odontogram(
  data = decay_by_arm,
  teeth_per_quadrant = 7,
  title = "Active Caries",
  surfaces = c("buc", "lin", "mes", "dis", "occ"),
  strata = "treatment",
  stats = dmft_summary,
  stats_test = "wilcox",
  stats_var = "DMFT",
  stats_raw = dmft_by_arm,
  ncol = 1
)

Stars follow: *** p < 0.001, ** p < 0.01, * p < 0.05, ns otherwise. Use stats_test = "t" for a t-test or stats_test = "wilcox" for a Wilcoxon rank-sum (non-parametric).

Footnote

Add an abbreviation key and p-value definitions below the plot:

build_odontogram(
  data = decay_by_arm,
  teeth_per_quadrant = 7,
  title = "Active Caries",
  surfaces = c("buc", "lin", "mes", "dis", "occ"),
  strata = "treatment",
  stats = dmft_summary,
  stats_test = "wilcox",
  stats_var = "DMFT",
  stats_raw = dmft_by_arm,
  footnote = paste0(
    "B = Buccal, L = Lingual, M = Mesial, D = Distal, O = Occlusal.\n",
    "DT = Decayed Teeth, DMFT = Decayed/Missing/Filled Teeth.\n",
    "* p < 0.05, ** p < 0.01, *** p < 0.001 (Wilcoxon rank-sum test)."
  ),
  ncol = 1
)

Primary Dentition

For primary teeth, use dentition = "primary" (5 teeth per quadrant):

primary_teeth <- paste0(
  rep(c("ur", "ul", "lr", "ll"), each = 5), rep(1:5, 4)
)
d_primary <- expand.grid(
  tooth_num = primary_teeth,
  tooth_surface = c("buc", "lin", "mes", "dis", "occ"),
  stringsAsFactors = FALSE
)
d_primary$prop <- runif(nrow(d_primary), 0, 0.5)

build_odontogram(
  data = d_primary,
  dentition = "primary",
  title = "Primary Dentition \u2014 Simulated Caries",
  color_high = "#E65100",
  surfaces = c("buc", "lin", "mes", "dis", "occ"),
  show_roots = FALSE
)

Tooth Numbering Conversion

Convert between FDI (ISO 3950), Universal (ADA), and quadrant notation:

tooth_convert("11", from = "fdi", to = "quadrant")
#> [1] "ur1"
tooth_convert("ur1", from = "quadrant", to = "universal")
#> [1] "8"
tooth_convert(c("11", "21", "36", "46"), from = "fdi", to = "quadrant")
#> [1] "ur1" "ul1" "ll6" "lr6"

Tooth Configuration

Generate a layout table for any arch configuration:

tooth_config("permanent", teeth_per_quadrant = 7)
#> # A tibble: 28 × 4
#>    quadrant   num tooth_id is_upper
#>    <chr>    <int> <chr>    <lgl>   
#>  1 ur           1 ur1      TRUE    
#>  2 ur           2 ur2      TRUE    
#>  3 ur           3 ur3      TRUE    
#>  4 ur           4 ur4      TRUE    
#>  5 ur           5 ur5      TRUE    
#>  6 ur           6 ur6      TRUE    
#>  7 ur           7 ur7      TRUE    
#>  8 ul           1 ul1      TRUE    
#>  9 ul           2 ul2      TRUE    
#> 10 ul           3 ul3      TRUE    
#> # ℹ 18 more rows
tooth_config("primary")
#> # A tibble: 20 × 4
#>    quadrant   num tooth_id is_upper
#>    <chr>    <int> <chr>    <lgl>   
#>  1 ur           1 ur1      TRUE    
#>  2 ur           2 ur2      TRUE    
#>  3 ur           3 ur3      TRUE    
#>  4 ur           4 ur4      TRUE    
#>  5 ur           5 ur5      TRUE    
#>  6 ul           1 ul1      TRUE    
#>  7 ul           2 ul2      TRUE    
#>  8 ul           3 ul3      TRUE    
#>  9 ul           4 ul4      TRUE    
#> 10 ul           5 ul5      TRUE    
#> 11 lr           1 lr1      FALSE   
#> 12 lr           2 lr2      FALSE   
#> 13 lr           3 lr3      FALSE   
#> 14 lr           4 lr4      FALSE   
#> 15 lr           5 lr5      FALSE   
#> 16 ll           1 ll1      FALSE   
#> 17 ll           2 ll2      FALSE   
#> 18 ll           3 ll3      FALSE   
#> 19 ll           4 ll4      FALSE   
#> 20 ll           5 ll5      FALSE