The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.
The Fellegi-Sunter EM algorithm scores pairs by comparing field values. Poor data quality in any of those fields degrades the score distribution in ways that no threshold choice can fully recover:
starling provides three functions specifically for
catching these issues before murmuration() is called.
preflight() — structured pre-linkage auditpreflight() runs a battery of checks across both
datasets and returns a structured report. It is the recommended first
step before any linkage call.
library(starling)
data(cases_notifiable)
data(vax_air)
audit <- preflight(
data1 = cases_notifiable,
data2 = vax_air,
linkage_vars = c("lettername1", "lettername2", "dob", "medicare10"),
id_col1 = "id_var",
id_col2 = "id_var",
date_cols = c("dob", "onset_date"),
medicare_col = "medicare10",
verbose = TRUE
)
#>
#> ================================================================
#> starling::preflight() - Pre-Linkage Data Quality Report
#> ================================================================
#> Dataset 1: 300 records x 10 columns
#> Dataset 2: 400 records x 15 columns
#>
#> == 1. Completeness of linkage variables ==
#> Variable Dataset Present Missing % Missing
#> ------------------------------------------------------------
#> lettername1 data1 300 0 0%
#> lettername2 data1 300 0 0%
#> dob data1 NA NA NA%
#> medicare10 data1 300 0 0%
#> lettername1 data2 400 0 0%
#> lettername2 data2 400 0 0%
#> dob data2 NA NA NA%
#> medicare10 data2 400 0 0%
#>
#> == 2. Duplicate records (by ID column) ==
#> Dataset Records Unique IDs Duplicates % Duplicate
#> ------------------------------------------------------------
#> data1 300 300 0 0%
#> data2 400 400 0 0%
#>
#> == 4. Date plausibility ==
#> Column: dob
#> data1 - Missing: 0 | Future: 0 | Pre-1900: 0 | Range: 1905-04-01 to 1989-03-10
#> data2 - Missing: 0 | Future: 0 | Pre-1900: 0 | Range: 1905-02-09 to 1990-01-01
#> Column: onset_date
#> data1 - Missing: 0 | Future: 0 | Pre-1900: 0 | Range: 2024-01-05 to 2024-12-30
#> data2: column not found
#>
#> == 5. Medicare number validation ==
#> data1 - Valid: 0 (0%) | Invalid checksum: 0 (0%) | Missing: 300
#> data2 - Valid: 0 (0%) | Invalid checksum: 0 (0%) | Missing: 400
#>
#> == 6. Name field quality ==
#> Column Dataset Too short Numeric Unusual chars
#> -----------------------------------------------------------------
#> lettername1 data1 0 0 0
#> lettername1 data2 0 0 0
#> lettername2 data1 0 0 0
#> lettername2 data2 0 0 0
#>
#> == 7. Categorical variable consistency (shared columns) ==
#> 'gender': data1 levels = {Female, Male} | data2 levels = {Female, Male}
#>
#> == Summary of flags ==
#> 2 issue(s) flagged:
#> [!] [HIGH MISSINGNESS] 'NA' in NA: NA missing
#> [!] [HIGH MISSINGNESS] 'NA' in NA: NA missing
#> ================================================================
The audit list contains structured results for
programmatic access:
# Completeness table
head(audit$completeness)
#> variable dataset n_records n_present n_missing pct_missing
#> 1 lettername1 data1 300 300 0 0%
#> 2 lettername2 data1 300 300 0 0%
#> 3 dob data1 300 NA NA NA%
#> 4 medicare10 data1 300 300 0 0%
#> 5 lettername1 data2 400 400 0 0%
#> 6 lettername2 data2 400 400 0 0%
# Duplicate ID counts
audit$duplicates
#> dataset n_records n_unique_ids n_duplicate_ids pct_duplicate
#> 1 data1 300 300 0 0%
#> 2 data2 400 400 0 0%
# Flags raised (empty = all clear)
audit$flags
#> [1] "[HIGH MISSINGNESS] 'NA' in NA: NA missing"
#> [2] "[HIGH MISSINGNESS] 'NA' in NA: NA missing"
check_medicare() — Modulus 10 checksum validationAustralian Medicare numbers contain a check digit at position 9
calculated from positions 1–8 using the weights 1, 3, 7, 9, 1, 3, 7, 9.
check_medicare() verifies this and returns a three-state
flag:
| Value | Meaning |
|---|---|
1L |
Checksum passes — number is internally consistent |
0L |
Checksum fails — at least one digit is wrong |
NA |
Missing, blank, or non-numeric — not verifiable |
cases_checked <- check_medicare(
cases_notifiable,
medicare_col = "medicare10",
output_col = "medicare_valid",
verbose = TRUE
)
#>
#> -- starling::check_medicare() ----------------------------------
#> Total records : 300
#> Missing / blank : 300
#> Present (n) : 0
#> Valid checksum : 0 (N/A%)
#> Invalid checksum : 0 (N/A%)
#> Output column : 'medicare_valid' (1=valid, 0=invalid, NA=missing)
#> ----------------------------------------------------------------
# Distribution of flags
table(cases_checked$medicare_valid, useNA = "always")
#>
#> <NA>
#> 300
Do not discard the record — it may still link correctly on name and
DOB. Instead, replace the invalid number with NA before
linkage so it does not contribute negatively to the score:
cases_checked$medicare10 <- ifelse(
cases_checked$medicare_valid == 1L,
cases_checked$medicare10,
NA_character_
)
flock() — blocking variable constructionflock() creates one to three blocking keys from
demographic fields. Blocking restricts murmuration() to
comparing only pairs that share a blocking key, making the search
tractable for large datasets without sacrificing recall on the key
variables.
# Single-field block (gender only — broadest, lowest specificity)
cases_blocked <- flock(
cases_checked,
block1_vars = "gender", # block1: gender only
block2_vars = "gender", # block2: same here; use composite in production
block3_vars = "postcode", # block3: postcode (finer)
birth_year_col = "dob" # derives birth_year column for composite use
)
vax_blocked <- flock(
vax_air,
block1_vars = "gender",
block3_vars = "postcode",
birth_year_col = "dob"
)
# Inspect blocking key distributions
table(cases_blocked$block1)
#>
#> Female Male
#> 141 159
head(sort(table(cases_blocked$block3), decreasing = TRUE))
#>
#> 4562 4555 4552 4556 4557 4560
#> 30 29 28 28 28 25
| Dataset size | Recommended block | Rationale |
|---|---|---|
| < 10 000 records | gender |
Broadest; halves candidate space immediately |
| 10 000–500 000 | gender + birth_year composite |
Balances sensitivity and specificity |
| > 500 000 | postcode + birth_year |
Fine-grained; requires high postcode completeness |
For maximum recall, run murmuration() twice — once with
a broad block, once with a fine block — and union the results:
linked_broad <- murmuration(cases_blocked, vax_blocked,
blocking_var = "block1", ...)
linked_fine <- murmuration(cases_blocked, vax_blocked,
blocking_var = "block3", ...)
# Union, keeping the highest-scoring link per case
linked_all <- dplyr::bind_rows(linked_broad, linked_fine) |>
dplyr::group_by(id_var.x) |>
dplyr::slice_max(weights, n = 1, with_ties = FALSE) |>
dplyr::ungroup()
library(starling)
data(cases_notifiable); data(vax_air)
# 1. Audit
preflight(cases_notifiable, vax_air,
linkage_vars = c("lettername1", "lettername2", "dob", "medicare10"),
medicare_col = "medicare10")
# 2. Fix Medicare
cases <- check_medicare(cases_notifiable)
cases$medicare10 <- ifelse(cases$medicare_valid == 1L,
cases$medicare10, NA_character_)
# 3. Block
cases <- flock(cases, block1_vars = "gender", birth_year_col = "dob")
vax <- flock(vax_air, block1_vars = "gender", birth_year_col = "dob")
# 4. Link
linked <- murmuration(cases, vax,
linkage_type = "v2c",
event_date = "onset_date",
id_var = "id_var",
blocking_var = "block1",
compare_vars = c("lettername1", "lettername2", "dob", "medicare10"),
threshold_value = 17)
sessionInfo()
#> R version 4.5.2 (2025-10-31 ucrt)
#> Platform: x86_64-w64-mingw32/x64
#> Running under: Windows 11 x64 (build 26100)
#>
#> Matrix products: default
#> LAPACK version 3.12.1
#>
#> locale:
#> [1] LC_COLLATE=C LC_CTYPE=English_Australia.utf8
#> [3] LC_MONETARY=English_Australia.utf8 LC_NUMERIC=C
#> [5] LC_TIME=English_Australia.utf8
#>
#> time zone: Australia/Brisbane
#> tzcode source: internal
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] starling_1.1.0
#>
#> loaded via a namespace (and not attached):
#> [1] gtable_0.3.6 jsonlite_2.0.0 dplyr_1.2.0
#> [4] compiler_4.5.2 Rcpp_1.1.1 tidyselect_1.2.1
#> [7] stringr_1.6.0 parallel_4.5.2 jquerylib_0.1.4
#> [10] scales_1.4.0 yaml_2.3.12 fastmap_1.2.0
#> [13] ggplot2_4.0.3 R6_2.6.1 generics_0.1.4
#> [16] knitr_1.51 tibble_3.3.1 lubridate_1.9.5
#> [19] bslib_0.10.0 pillar_1.11.1 RColorBrewer_1.1-3
#> [22] rlang_1.2.0 stringi_1.8.7 cachem_1.1.0
#> [25] reclin2_0.6.0 xfun_0.56 sass_0.4.10
#> [28] S7_0.2.1 otel_0.2.0 timechange_0.4.0
#> [31] cli_3.6.5 magrittr_2.0.4 stringdist_0.9.17
#> [34] digest_0.6.39 grid_4.5.2 rstudioapi_0.18.0
#> [37] lifecycle_1.0.5 vctrs_0.7.1 lpSolve_5.6.23
#> [40] data.table_1.18.2.1 evaluate_1.0.5 glue_1.8.0
#> [43] farver_2.1.2 rmarkdown_2.31 tools_4.5.2
#> [46] pkgconfig_2.0.3 htmltools_0.5.9
These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.