πŸŽ‰ 75% of content is free forever β€” Unlock Premium from $10/mo β†’
CW
Search courses…
πŸ’Ό Servicesℹ️ Aboutβœ‰οΈ ContactView Pricing Plansfrom $10

Record Linkage and Data Matching

Advanced Statistical MethodsData Methods🟒 Free Lesson

Advertisement

Record Linkage and Data Matching

Advanced Statistical Methods

Connecting Records Across Imperfect Databases

Record linkage identifies records that refer to the same entity across different data sources using probabilistic models and string distance metrics. The Felleg-Sunter model provides the theoretical foundation.

  • Public health β€” Link hospital records to disease registries for longitudinal studies
  • Bureau of statistics β€” Combine administrative datasets while preserving privacy
  • Finance β€” Match customer records across mergers and acquisitions for risk assessment

Record linkage bridges the gap between siloed datasets to unlock richer analytical insights.


Record linkage (also called entity resolution, deduplication, or data matching) identifies records across or within databases that refer to the same real-world entity. When unique identifiers are absent, statistical and probabilistic methods are needed to determine whether two records represent the same entity. This lesson develops the mathematical foundations of record linkage from deterministic rules through the Felleg-Sunter probabilistic framework.


Deterministic vs Probabilistic Linkage


Felleg-Sunter Model


String Distance Metrics


Blocking


EM Algorithm for Linkage


Privacy-Preserving Record Linkage


Evaluation Metrics


Python Implementation

import numpy as np
import pandas as pd
from collections import Counter

np.random.seed(42)

# --- String Distance Metrics ---
def levenshtein_distance(s, t):
    """Compute Levenshtein edit distance via dynamic programming."""
    m, n = len(s), len(t)
    dp = np.zeros((m + 1, n + 1), dtype=int)
    dp[:, 0] = np.arange(m + 1)
    dp[0, :] = np.arange(n + 1)
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            cost = 0 if s[i-1] == t[j-1] else 1
            dp[i, j] = min(dp[i-1, j] + 1, dp[i, j-1] + 1, dp[i-1, j-1] + cost)
    return dp[m, n]

def jaro_winkler(s, t, p=0.1):
    """Compute Jaro-Winkler similarity."""
    if s == t: return 1.0
    len_s, len_t = len(s), len(t)
    match_distance = max(len_s, len_t) // 2 - 1
    s_matches = [False] * len_s
    t_matches = [False] * len_t
    matches = transpositions = 0

    for i in range(len_s):
        start = max(0, i - match_distance)
        end = min(i + match_distance + 1, len_t)
        for j in range(start, end):
            if t_matches[j] or s[i] != t[j]: continue
            s_matches[i] = t_matches[j] = True
            matches += 1
            break

    if matches == 0: return 0.0

    k = 0
    for i in range(len_s):
        if not s_matches[i]: continue
        while not t_matches[k]: k += 1
        if s[i] != t[k]: transpositions += 1
        k += 1

    jaro = (matches/len_s + matches/len_t + (matches - transpositions/2)/matches) / 3

    # Common prefix (max 4 chars)
    prefix = 0
    for i in range(min(4, len_s, len_t)):
        if s[i] == t[i]: prefix += 1
        else: break

    return jaro + prefix * p * (1 - jaro)

# --- Test string distances ---
pairs = [
    ("Smith", "Smyth"), ("Johnson", "Johnsen"),
    ("Washington", "Washngton"), ("Michael", "Micheal"),
    ("New York", "New Yrok"), ("Robert", "Robbert"),
]
print("=== String Distance Metrics ===")
print(f"{'Pair':<30} {'Levenshtein':<14} {'Jaro-Winkler':<14}")
print("-" * 58)
for s, t in pairs:
    lev = levenshtein_distance(s, t)
    jw = jaro_winkler(s, t)
    print(f"({s}, {t}){'':>{26-len(s)-len(t)}} {lev:<14} {jw:<14.4f}")

# --- Felleg-Sunter Model ---
def felleg_sunter_weights(match_probs, non_match_probs, agreement_vector):
    """Compute Felleg-Sunter match weight for a pair."""
    weight = 0.0
    for gamma, m_k, u_k in zip(agreement_vector, match_probs, non_match_probs):
        if gamma == 1:
            weight += np.log(m_k / u_k)
        else:
            weight += np.log((1 - m_k) / (1 - u_k))
    return weight

# Simulated parameters (estimated from field distributions)
# Fields: [first_name, last_name, DOB, city, ZIP]
m_k = np.array([0.95, 0.98, 0.90, 0.85, 0.92])  # match probs
u_k = np.array([0.01, 0.005, 0.02, 0.03, 0.05])  # non-match probs

print("\n=== Felleg-Sunter Match Weights ===")
test_pairs = [
    ("John Smith", "John Smith", [1,1,1,1,1]),
    ("John Smith", "Jon Smith", [0,1,1,1,1]),
    ("John Smith", "John Smyth", [1,0,1,1,1]),
    ("John Smith", "Jane Doe", [0,0,0,0,0]),
]
for name1, name2, gamma in test_pairs:
    w = felleg_sunter_weights(m_k, u_k, gamma)
    agreement = sum(gamma)
    print(f"({name1}, {name2}) agree on {agreement}/5 fields: W = {w:.2f} bits")

# --- EM Algorithm for Linkage ---
np.random.seed(42)
n_pairs = 5000
n_true_matches = 200

# Generate comparison vectors
gamma_true = np.random.binomial(1, m_k, size=(n_true_matches, 5))
gamma_false = np.random.binomial(1, u_k, size=(n_pairs - n_true_matches, 5))
gamma_all = np.vstack([gamma_true, gamma_false])
labels = np.concatenate([np.ones(n_true_matches), np.zeros(n_pairs - n_true_matches)])

# EM algorithm
n_pairs_total = len(gamma_all)
n_fields = gamma_all.shape[1]
pi = n_true_matches / n_pairs_total
m_em = np.random.uniform(0.7, 0.95, n_fields)
u_em = np.random.uniform(0.01, 0.1, n_fields)

for iteration in range(50):
    # E-step
    p_match = pi * np.prod(m_em ** gamma_all * (1 - m_em) ** (1 - gamma_all), axis=1)
    p_nonmatch = (1 - pi) * np.prod(u_em ** gamma_all * (1 - u_em) ** (1 - gamma_all), axis=1)
    w_post = p_match / (p_match + p_nonmatch + 1e-300)

    # M-step
    pi_new = np.mean(w_post)
    m_em_new = np.average(gamma_all, weights=w_post, axis=0)
    u_em_new = np.average(gamma_all, weights=1 - w_post, axis=0)

    if np.max(np.abs(m_em - m_em_new)) < 1e-6:
        print(f"EM converged after {iteration + 1} iterations")
        break
    pi, m_em, u_em = pi_new, m_em_new, u_em_new

print(f"\n=== EM Estimates ===")
print(f"Ο€ (match prior): {pi:.4f} (true: {n_true_matches/n_pairs_total:.4f})")
fields = ['First Name', 'Last Name', 'DOB', 'City', 'ZIP']
print(f"{'Field':<15} {'m_k (est)':<12} {'m_k (true)':<12} {'u_k (est)':<12} {'u_k (true)':<12}")
print("-" * 63)
for i, field in enumerate(fields):
    print(f"{field:<15} {m_em[i]:<12.4f} {m_k[i]:<12.4f} {u_em[i]:<12.4f} {u_k[i]:<12.4f}")

# Classification
threshold = 0.5
predicted = (w_post >= threshold).astype(int)
tp = np.sum((predicted == 1) & (labels == 1))
fp = np.sum((predicted == 1) & (labels == 0))
fn = np.sum((predicted == 0) & (labels == 1))
precision = tp / (tp + fp) if (tp + fp) > 0 else 0
recall = tp / (tp + fn) if (tp + fn) > 0 else 0
f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0

print(f"\n=== Linkage Evaluation ===")
print(f"Precision: {precision:.4f}")
print(f"Recall: {recall:.4f}")
print(f"F1: {f1:.4f}")

Summary

Need Expert Statistics Help?

Get personalized tutoring, project support, or professional consulting.

Advertisement