perch: Threshold Sensitivity Analysis for Linkage Score Cutoff...

View source: R/perch.R

perchR Documentation

Threshold Sensitivity Analysis for Linkage Score Cutoff Selection

Description

**Bird note**: A starling tests a perch before committing its weight to it — sitting briefly, sensing stability, probing whether this particular spot will hold before deciding to stay or move on. perch() does exactly that to a threshold: it tries every candidate value across your scored pairs and shows you precisely what the linkage looks like at each one — match count, link rate, clerical burden — so you can commit to the perch that holds, informed by the same benchmarks the AIHW, WA Data Linkage Unit, and PHRN use in practice.

Sweeps a range of candidate Fellegi-Sunter threshold values across a scored pairs object and returns a structured sensitivity table with one row per candidate cutoff. Each row reports the number and percentage of pairs that would be accepted or rejected at that threshold, the number falling in a configurable clerical review zone around it, and (if n_records_df1 is supplied) the estimated link rate for the primary dataset. Four canonical thresholds are highlighted in the printed table and the plot with annotations from the three key Australian and international linkage authorities — AIHW, WA Data Linkage Unit, and the Population Health Research Network (PHRN).

Call perch() after murmuration_plot confirms your weight distribution is bimodal, then pass the chosen value to murmuration(threshold_value = ...).

Usage

perch(
  pairs_pred,
  weight_col = "weights",
  n_records_df1 = NULL,
  thresholds = seq(5, 30, by = 1),
  clerical_window = 6,
  highlight_thresholds = c(10, 15, 17, 20),
  report = TRUE,
  plot = TRUE,
  palette = "sch",
  interactive = FALSE
)

Arguments

pairs_pred

A pairs object (data frame) as returned by reclin2::predict.problink_em(m, pairs = pairs, add = TRUE), containing a numeric linkage weight column.

weight_col

Character. Name of the numeric linkage weight column. Default "weights" (the column name murmuration uses internally).

n_records_df1

Integer or NULL. Number of records in the primary (df1) dataset passed to murmuration. When supplied, a link_rate column is added showing the proportion of df1 records that would receive at least one accepted match at each threshold. Default NULL (link rate column omitted).

thresholds

Numeric vector of candidate threshold values to evaluate. Default seq(5, 30, by = 1), covering the full practical range for Australian population health linkage with a complete variable set. Narrow this for targeted comparisons (e.g. seq(14, 22, by = 0.5)).

clerical_window

Numeric. Width of the clerical review zone centred on each threshold value: the band [threshold - clerical_window/2, threshold + clerical_window/2]. Pairs in this band are those that would change classification if the threshold shifted by half the window width — i.e. the marginal cases a human reviewer would be asked to adjudicate in a two-threshold design. Default 6 (matching the AIHW/WADLU convention of a roughly 10-unit clerical zone centred at each candidate threshold).

highlight_thresholds

Numeric vector. Threshold values to mark with [*] in the printed table and with diamond symbols in the plot. Default c(10, 15, 17, 20) — the four canonical reference points from Australian and international linkage guidance (see Details).

report

Logical. If TRUE (default), prints a formatted sensitivity table with reference annotations to the console.

plot

Logical. If TRUE (default), generates and returns a ggplot2 figure (or patchwork composite if n_records_df1 is supplied) showing match count and link rate against threshold with AIHW zone shading and reference markers. If FALSE, returns only the data frame invisibly.

palette

Character. Colour palette: "sch" (SCH Evergreen, default), "default" (teal/coral), "grey" (greyscale, publication-safe).

interactive

Logical. If TRUE, returns a plotly htmlwidget with hover tooltips. Requires plotly. Default FALSE.

Details

## Interpreting the output

Use perch() together with murmuration_plot:

  1. murmuration_plot() confirms the weight distribution is bimodal and shows you where the valley between the two clusters lies.

  2. perch() quantifies the match count, link rate, and clerical burden at each candidate cutoff in that valley.

  3. Choose the threshold that best balances precision and recall for your study design (see benchmarks below), then pass it to murmuration(threshold_value = ...).

The n_clerical column is the key diagnostic for threshold placement. A large n_clerical relative to n_above means the threshold sits in a high-density region of the score distribution — a small shift would reclassify many pairs. You want to sit in the valley (low density), not on a slope (high density). If every threshold in your range has a large clerical burden, the two score modes overlap substantially — this is a signal that the variable set or blocking strategy needs revision before threshold selection will be meaningful.

## Australian and international linkage benchmarks

The Fellegi-Sunter weights are log-likelihood ratios and are **dataset-specific** — no universal cutoff exists. These benchmarks from recognised Australian and international linkage authorities inform where to look:

| Threshold | Authority and basis | |—|—| | **10–20 (clerical zone)** | **AIHW and WA Data Linkage Unit (WADLU)**: the established practice is a two-threshold approach with a clerical review zone of roughly 10–20. Pairs above ~20 are auto-accepted as matches; pairs below ~10 are auto-rejected as non-matches; pairs in between are sent for human adjudication. When operating without a dedicated clerical reviewer (as in most routine surveillance settings), the single threshold should sit somewhere in this range, with the direction of error dictated by the study design. | | **~15–20** | **Population Health Research Network (PHRN)**: no universal cutoff is mandated, but the operational target is a false-match rate below 0.5 | **12–17** | **starling balanced default** — typical starting point for SCPHU routine surveillance linkage with a complete variable set. | | **17–22** | **High specificity** — appropriate when false matches carry high analytical cost, e.g. vaccine effectiveness studies where falsely attributing vaccination status to an unvaccinated person biases the VE estimate downward. | | **8–12** | **High sensitivity** — suitable for small datasets or rare-disease surveillance where missing a true match is the dominant concern. |

## Note on link rate

The link_rate column approximates the proportion of primary-dataset records (df1) that would receive at least one accepted match at a given threshold. It counts accepted pairs above threshold rather than unique linked records (one df1 record can appear in multiple pairs), so it is an upper bound on the true link rate. Nonetheless it is useful for spotting the point at which raising the threshold begins to meaningfully reduce coverage — a sharp inflection in the link rate curve often marks the valley between the two score modes.

Value

When plot = FALSE, returns the sensitivity data frame invisibly. When plot = TRUE (default), prints the data frame to the console (if report = TRUE) and returns the plot object. The data frame has one row per threshold value and the following columns:

threshold

Numeric. The candidate cutoff.

n_above

Integer. Pairs with weight \geq threshold (accepted as matches at this cutoff).

n_below

Integer. Pairs with weight < threshold (rejected as non-matches).

pct_above

Numeric. Percentage of all pairs above threshold.

n_clerical

Integer. Pairs in the clerical review zone [threshold - clerical_window/2, threshold + clerical_window/2].

pct_clerical

Numeric. Percentage in the clerical zone.

link_rate

Numeric or NA. Proportion of n_records_df1 records that would receive a match. Only populated when n_records_df1 is supplied.

reference

Character. Annotation for the four highlighted threshold values; NA for all others.

References

Australian Institute of Health and Welfare (2021). Data linkage: cohort studies. AIHW, Canberra. https://www.aihw.gov.au/reports/methods-data-development/data-linkage

Holman, C.D.J. et al. (1999). A population-based linkage study of 3.4 million records in Western Australia. Australian and New Zealand Journal of Public Health, 23(5): 453–459. doi:10.1111/j.1467-842X.1999.tb01297.x

Population Health Research Network (2023). PHRN Linkage Guidelines. PHRN, Perth. https://www.phrn.org.au/

Fellegi, I. and Sunter, A. (1969). A theory for record linkage. Journal of the American Statistical Association, 64(328): 1183–1210. doi:10.2307/2286061

See Also

murmuration for the linkage step. murmuration_plot for visual weight distribution inspection. preflight for pre-linkage data quality auditing. flock for blocking variable construction.

Examples

## Not run: 
# Standard workflow: score pairs then perch to find the best threshold
library(reclin2)

pairs <- pair_blocking(df1, df2, "block2")
compare_pairs(pairs,
  on = c("lettername1", "lettername2", "dob", "medicare10"),
  default_comparator = jaro_winkler(0.9), inplace = TRUE)
m          <- problink_em(
  ~ lettername1 + lettername2 + dob + medicare10, data = pairs)
pairs_pred <- predict(m, pairs = pairs, add = TRUE)

# Step 1: inspect the distribution
murmuration_plot(pairs_pred, threshold = 17)

# Step 2: quantify the trade-off across the plausible range
results <- perch(pairs_pred, n_records_df1 = nrow(df1))

# Step 3: narrow to the AIHW clerical zone for fine-grained inspection
results <- perch(pairs_pred,
  thresholds    = seq(10, 20, by = 0.5),
  n_records_df1 = nrow(df1))

# Get only the data frame (no plot)
tbl <- perch(pairs_pred, plot = FALSE)

# Interactive dual-panel plot for exploratory use
perch(pairs_pred, n_records_df1 = nrow(df1), interactive = TRUE)

# Greyscale for publication figures
perch(pairs_pred, palette = "grey")

# Step 4: apply the chosen threshold
linked <- murmuration(df1, df2,
  linkage_type  = "v2c",
  event_date    = "onset_date",
  blocking_var  = "block2",
  compare_vars  = c("lettername1", "lettername2", "dob", "medicare10"),
  threshold_value = 18)

## End(Not run)


starling documentation built on July 10, 2026, 9:07 a.m.