--- title: "Translating from Splink" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Translating from Splink} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = '#>' ) ``` `irelink` translates the Python [splink](https://github.com/moj-analytical-services/splink) library into idiomatic R. This vignette maps common Splink 5 patterns to `irelink` so you can get started quickly. ## Design differences Splink uses an object-oriented design centered on a `Linker` class. `irelink` uses a functional pipeline that fits naturally in R. The `Linker` object's namespaced methods such as `linker.training.*` and `linker.inference.*` become standalone functions that accept and return an `il_model` object. Splink bundles comparison levels into high-level comparison classes such as `JaroWinklerAtThresholds`. In `irelink`, the `cl_*()` functions fill the same role and can be passed directly to `il_compare()`. ## Core workflow | Step | splink (Python) | irelink (R) | |------|----------------|-------------| | Load data | `splink_datasets.fake_1000` | `fake_1000` | | Choose backend | `DuckDBAPI()` | `DBI::dbConnect(duckdb::duckdb())` | | Register data | `db_api.register(df, ...)` | *handled by* `il_model()` | | Define settings | `SettingsCreator(...)` | `il_spec() |>`
`il_compare(...) |>`
`il_block_on(...)` | | Create model | `Linker(df_sdf, settings)` | `il_model(df, spec = spec, con = con)` | | Estimate prior | `linker.training.`
`estimate_probability_two_random_records_match(...)` | `il_estimate_prior(model, ...)` | | Estimate u | `linker.training.`
`estimate_u_using_random_sampling(...)` | `il_estimate_u(model)` | | Estimate m (EM) | `linker.training.`
`estimate_parameters_using_expectation_maximisation(...)` | `il_estimate_em(model, ...)` | | Estimate m (labels) | `linker.training.`
`estimate_m_from_pairwise_labels(...)` | `il_estimate_m_from_labels(model, ...)` | | Predict | `linker.inference.predict(...)` | `predict(model, ...)` | | Cluster | `linker.clustering.`
`cluster_pairwise_predictions_at_threshold(...)` | `il_cluster(pairs)` | | Deterministic link | `linker.deterministic_link()` | `il_deterministic_link(df, ...)` | | Match new records | `linker.inference.`
`predict_between(df_sdf, new_sdf, ...)` | `il_find_matches(model, new_records, ...)` | | Pairs within new records | `linker.inference.`
`predict_within(new_sdf, ...)` | `il_attach(model, new_records) |>`
`predict(...)` | | Score chosen pairs | `linker.inference.`
`score_pair(...)`, `score_pairs(...)` | `il_score_pairs(model, records_l, records_r)` | Splink 5 requires registering each input with `db_api.register()` before building a `Linker`, and the `Linker` no longer takes `db_api`. In `irelink`, `il_model()` registers data frames, lazy tables, or table names on the supplied connection itself. `irelink` also supports `link_type = "link_and_dedupe"` for two-table jobs where duplicates may exist within each input table and across the two tables. `irelink` scores in-memory inputs and DBI-backed tables, including lazy DuckDB results. Splink 5's chunked prediction, DuckDB source pruning, and Parquet-backed intermediate tables are not available, so very large workflows should rely on explicit blocking and `predict(collect = FALSE)`. ## Comparison levels Comparison levels are the building blocks used to score how similar two records are on a field. Each `cl_*()` function corresponds to a Splink comparison level class. | splink (Python) | irelink (R) | |-----------------|-------------| | `ExactMatchLevel` | `cl_exact()` | | `LevenshteinLevel` | `cl_levenshtein()` | | `DamerauLevenshteinLevel` | `cl_damerau_levenshtein()` | | `JaroLevel` | `cl_jaro()` | | `JaroWinklerLevel` | `cl_jaro_winkler()` | | `JaccardLevel` | `cl_jaccard()` | | `CosineSimilarityLevel` | `cl_cosine()` | | `AbsoluteDifferenceLevel` | `cl_numeric_diff()` | | `PercentageDifferenceLevel` | `cl_pct_diff()` | | `AbsoluteTimeDifferenceAtThresholds` | `cl_date_diff()` | | `DistanceInKMLevel` | `cl_geo_distance()` | | `ArrayIntersectLevel` | `cl_array_intersect()` | | `CustomLevel` | `cl_custom()` | | `NullLevel` | `cl_null()` | | `ElseLevel` | `cl_else()` | | `And` | `cl_and()` | | `Or` | `cl_or()` | | `Not` | `cl_not()` | ## Domain-specific comparisons Splink provides high-level comparison classes for common field types. In `irelink`, these are helper functions that return preconfigured sets of levels. | splink (Python) | irelink (R) | |-----------------|-------------| | `NameComparison` | `cl_name()` | | `ForenameSurnameComparison` | `cl_forename_surname()` | | `DateOfBirthComparison` | `cl_dob()` (Levenshtein for one-character typos) | | `EmailComparison` | `cl_email()` | | `PostcodeComparison` | `cl_postcode()` | ## Model inspection | splink (Python) | irelink (R) | |-----------------|-------------| | `linker.visualisations.match_weights_chart()` | `il_weights(model)` | | `linker.visualisations.`
`parameter_estimate_comparisons_chart()` | `il_parameters(model)` | | `linker.visualisations.waterfall_chart(...)` | `il_waterfall(pairs, ...)` | | *comparison levels for a pair, without a model* | `il_compare_records(record_a, record_b, spec)` | | `linker.evaluation.`
`prediction_errors_from_labels_column(...)` | `il_errors(model, ...)` | | `linker.evaluation.unlinkables_chart()` | `il_unlinkables(model)` | ## Evaluation Splink 5 combines these analyses in `linker.evaluation.accuracy_analysis_from_labels_column()`, selected with `output_type`. | splink (Python) | irelink (R) | |-----------------|-------------| | `accuracy_analysis_from_labels_column(`
`..., output_type="accuracy")` | `il_accuracy(model, ...)` | | `accuracy_analysis_from_labels_column(`
`..., output_type="precision_recall")` | `il_precision_recall(model, ...)` | | `accuracy_analysis_from_labels_column(`
`..., output_type="roc")` | `il_roc(model, ...)` | ## Data profiling | splink (Python) | irelink (R) | |-----------------|-------------| | `splink.exploratory.profile_columns(...)` | `il_profile(df, ...)` | | `splink.exploratory.completeness_chart(...)` | `il_completeness(df, ...)` | | `splink.blocking_analysis.`
`count_comparisons_from_blocking_rules(...)` | `il_count_pairs(df, ...)` | | `splink.blocking_analysis.n_largest_blocks(...)` | `il_largest_blocks(df, ...)` | Splink 5 estimates blocking comparison counts from a 5% record sample by default. `il_count_pairs()` computes exact counts unless you set `record_sample_proportion` below 1. ## Persistence | splink (Python) | irelink (R) | |-----------------|-------------| | `linker.misc.save_model_to_json(...)` | `il_save(model, path)` | | `Linker(df_sdf, "model.json")` | `il_load(path)` | | `linker.table_management.`
`delete_tables_created_by_splink_from_db()` | `il_cleanup_all(con)` | | *model-scoped cleanup* | `il_cleanup(model)` | ## Blocking rules In Splink, you create blocking rules with `block_on()`, and `irelink` uses the same function name. The main difference is where the rules are used: Splink passes them into `SettingsCreator`, while `irelink` adds them to a spec with `il_block_on()` or passes them directly to training functions. ```r # blocking in the spec spec <- il_spec() |> il_compare(first_name, cl_jaro_winkler(0.9, 0.7)) |> il_block_on(surname) # blocking in EM training model <- il_estimate_em(model, block_on(surname)) ``` ## Example: side-by-side deduplication Below is a minimal deduplication example in both Splink and `irelink`. **splink (Python):** ```python from splink import Linker, SettingsCreator, DuckDBAPI, block_on, splink_datasets import splink.comparison_library as cl db_api = DuckDBAPI() df_sdf = db_api.register(splink_datasets.fake_1000, dataset_display_name="fake_1000") settings = SettingsCreator( link_type="dedupe_only", comparisons=[ cl.JaroWinklerAtThresholds("first_name", [0.9, 0.7]), cl.JaroWinklerAtThresholds("surname", [0.9, 0.7]), cl.ExactMatch("dob"), ], blocking_rules_to_generate_predictions=[ block_on("first_name"), block_on("surname"), ], ) linker = Linker(df_sdf, settings) linker.training.estimate_u_using_random_sampling(max_pairs=1e6) linker.training.estimate_parameters_using_expectation_maximisation( block_on("surname") ) pairwise = linker.inference.predict(threshold_match_probability=0.5) clusters = linker.clustering.cluster_pairwise_predictions_at_threshold( pairwise, 0.95 ) ``` **irelink (R):** ```r library(irelink) df <- fake_1000 con <- DBI::dbConnect(duckdb::duckdb()) spec <- il_spec() |> il_compare(first_name, cl_jaro_winkler(0.9, 0.7)) |> il_compare(surname, cl_jaro_winkler(0.9, 0.7)) |> il_compare(dob, cl_exact()) |> il_block_on(first_name) |> il_block_on(surname) model <- il_model(df, spec = spec, con = con) model <- il_estimate_u(model) model <- il_estimate_em(model, block_on(surname)) pairs <- predict(model, threshold = 0.5) clusters <- il_cluster(pairs) il_cleanup(model) DBI::dbDisconnect(con, shutdown = TRUE) ``` The examples above use probability thresholds because those transfer cleanly between Splink and `irelink`. In Splink, prediction `match_weight` includes the prior odds. In `irelink`, `match_weight` is evidence only, and `total_match_weight` is the prior-inclusive log2 odds. Keep that difference in mind if you translate match-weight thresholds between the two packages. ## Example: finding matches against new records **splink (Python):** ```python new_sdf = db_api.register( [{"unique_id": 1001, "first_name": "Jhon", "surname": "Smith", "dob": "1990-01-15"}], dataset_display_name="new_records", ) results = linker.inference.predict_between( df_sdf, new_sdf, threshold_match_probability=0.5 ) ``` **irelink (R):** ```r new_df <- data.frame( first_name = "Jhon", surname = "Smith", dob = "1990-01-15" ) results <- il_find_matches(model, new_df, threshold = 0.5) ``` `il_find_matches()` corresponds to `predict_between()`: it scores new records against the model's existing data, but not new records against each other. For Splink 5's `predict_within()`, attach the new records to the trained model with `il_attach()` and call `predict()`. Note that `il_attach()` computes term frequencies from the attached records. When those are too few to be representative, replace them with term frequencies from the full data using `il_register_tf(..., overwrite = TRUE)`.