# 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 x 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 x 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 x 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 x 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 x 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
    )
    #> Warning in wilcox.test.default(g1, g2): cannot compute exact p-value with ties

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
    )
    #> Warning in wilcox.test.default(g1, g2): cannot compute exact p-value with ties

## 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 x 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
    #> # i 18 more rows
    tooth_config("primary")
    #> # A tibble: 20 x 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