Skip to content

Scoring Strategies

After comparison rules produce individual scores, a scoring strategy combines them into a single composite score for each record pair. This score drives the match/review/reject decision.

Comparison Table

StrategyComplexityTraining DataInterpretabilityBest For
weighted_sumLowNoneHighMost use cases — start here
fellegi_sunterMediumOptional (EM)HighCalibrated probabilities, regulatory
ml_ensembleHighRequiredMediumLarge labeled datasets
customVariesNoneDependsDomain-specific formulas

Weighted Sum

The default scoring strategy. Simple, interpretable, and effective for most use cases.

Formula

composite_score = sum(score_i × weight_i) / sum(weight_i)

Each rule contributes its score multiplied by its weight. The result is normalized by the total weight, producing a score between 0.0 and 1.0.

Worked Example

Consider matching CRM and Stripe records for "John Smith":

Rules and weights:

RuleFieldsAlgorithmWeight
email_exactemailexact1.0
name_fuzzynamejaro_winkler0.6
phone_exactphoneexact0.85

Pair: CRM record vs Stripe record

RuleCRM ValueStripe ValueScore
email_exact[email protected][email protected]1.0
name_fuzzyJohn SmithJon Smith0.96
phone_exact555-0100555-01010.0

Calculation:

composite = (1.0 × 1.0 + 0.96 × 0.6 + 0.0 × 0.85) / (1.0 + 0.6 + 0.85)
          = (1.0 + 0.576 + 0.0) / 2.45
          = 1.576 / 2.45
          = 0.643

With thresholds match: 0.9, review: 0.6, this pair scores 0.643 — a review candidate. The matching email and name are strong signals, but the mismatched phone pulled the score down.

YAML Config

yaml
rules:
  - name: email_exact
    type: exact
    field: email
    weight: 1.0
  - name: name_fuzzy
    type: similarity
    field: name
    algorithm: jaro_winkler
    threshold: 0.88
    weight: 0.6
  - name: phone_exact
    type: exact
    field: phone
    weight: 0.85

decision:
  scoring: weighted_sum
  thresholds:
    match: 0.9
    review: 0.6
    reject: 0.3

Tips

  • High weight = high importance. Give weight 1.0 to your strongest identifier.
  • Don't over-weight fuzzy rules. A name match alone shouldn't push a pair over the match threshold.
  • Weights are relative. Only the ratios matter — weights of [2.0, 1.2, 1.7] produce the same result as [1.0, 0.6, 0.85].

Fellegi-Sunter

The gold standard for probabilistic record linkage, developed by Fellegi and Sunter in 1969. Instead of ad-hoc weights, it uses statistical probabilities to compute a log-likelihood ratio that a pair is a match.

Core Concepts

For each comparison field, two probabilities are estimated:

  • m-probability: P(field agrees | records are a true match). "How often does this field match when two records really are the same person?"
  • u-probability: P(field agrees | records are not a match). "How often does this field match by coincidence?"

A field is discriminating when m is high and u is low. Email with m=0.95, u=0.01 is highly discriminating — matches almost always agree, and coincidental agreement is rare. First name with m=0.90, u=0.15 is less discriminating — many non-matches share a first name.

Typical m/u Values

Field Typem-probabilityu-probabilityDiscriminating Power
Email0.950.01Very high
Phone0.900.02Very high
SSN/Tax ID0.980.001Extremely high
Last name0.850.05High
First name0.900.15Medium
City0.800.10Medium
State/Province0.850.30Low
Gender0.980.50Very low
Country0.950.60Very low

Log-Likelihood Ratios

For each field, Fellegi-Sunter computes a weight based on agreement or disagreement:

w_agree    = ln(m / u)           # positive: evidence FOR match
w_disagree = ln((1 - m) / (1 - u))  # negative: evidence AGAINST match

The composite score is the sum of per-field weights.

Worked Example

Setup: 4-field comparison — email, name, phone, city.

Fieldmuw_agree = ln(m/u)w_disagree = ln((1-m)/(1-u))
email0.950.01ln(95) = 4.55ln(0.05/0.99) = -2.98
name0.900.10ln(9) = 2.20ln(0.10/0.90) = -2.20
phone0.900.02ln(45) = 3.81ln(0.10/0.98) = -2.28
city0.800.10ln(8) = 2.08ln(0.20/0.90) = -1.50

Pair: Jon Smith (CRM) vs John Smith (Stripe)

FieldCRMStripeAgrees?Weight
email[email protected][email protected]Yes+4.55
nameJohn SmithJon SmithYes (JW=0.96)+2.20
phone555-0100555-0101No-2.28
citySan FranciscoSan FranciscoYes+2.08
Composite+6.55

Comparison with weighted sum:

The same pair scored 0.643 with weighted sum (a "review"). With Fellegi-Sunter, the composite of +6.55 reflects that three strong agreements far outweigh one phone mismatch. The calibrated probabilities give more credit to the email match (extremely unlikely to agree by chance) than a simple weight of 1.0 would.

Posterior Probability

The raw composite score can be converted to a posterior probability — the probability that this pair is a true match given the observed agreements:

P(match | data) = sigmoid(composite) = 1 / (1 + e^(-composite))

For our example: P = 1 / (1 + e^(-6.55)) = 0.9986 — a 99.86% probability of being a true match.

This is useful for:

  • Setting thresholds in probability terms ("match if P > 0.95")
  • Regulatory environments where you need to justify match decisions
  • Comparing match quality across different specs

EM Training

If you don't know the m/u probabilities for your data, Expectation-Maximization (EM) can estimate them automatically from unlabeled data.

How EM works:

  1. Initialize: Start with reasonable guesses for m and u (e.g., m=0.9, u=0.1 for all fields)
  2. E-step: For each pair, compute the probability of being a match given current m/u values
  3. M-step: Re-estimate m/u values based on the match probabilities
  4. Repeat: Until m/u values converge (typically 5-20 iterations)

EM finds the m/u values that best explain the observed agreement patterns in your data — without needing labeled training data.

Unbiased u-Probability Estimation

Standard EM estimates u-probabilities from blocked pairs — but blocked pairs always agree on the blocking field, which inflates u for that field. For example, if you block on email, every candidate pair shares an email, so EM thinks email agreement is common among non-matches.

The fix is a two-phase approach:

  1. Phase 1: Estimate u-probabilities from random (unblocked) pairs via estimate_u: random_sampling. Random pairs represent the true non-match population, so the agreement rate gives an unbiased u estimate.
  2. Phase 2: Run EM with u frozen (fix_u_probabilities=true) to only learn m-probabilities from blocked candidate pairs.

This is the same approach used by Splink and recommended in the record linkage literature.

YAML Config with EM + random u estimation (recommended):

yaml
decision:
  scoring:
    strategy: fellegi_sunter
    training:
      method: em
      estimate_u: random_sampling  # Phase 1: unbiased u from random pairs
      max_random_pairs: 1000000    # Sample up to 1M random pairs for u
      max_iterations: 25           # Phase 2: EM iterations for m
      convergence_threshold: 0.001
    fields:
      - name: email
        comparator: exact
        normalizer: email          # Gmail dots, plus-addressing
        m_probability: 0.95        # initial estimate (EM will refine)
        u_probability: 0.01
      - name: first_name
        comparator: jaro_winkler
        normalizer: nickname       # Bob → robert before compare
        m_probability: 0.90
        u_probability: 0.10
      - name: phone
        comparator: exact
        normalizer: phone          # E.164 normalization
        m_probability: 0.90
        u_probability: 0.02
      - name: city
        comparator: exact
        m_probability: 0.80
        u_probability: 0.10
    thresholds:
      match: 8.0          # log-likelihood composite
      possible: 4.0       # needs human review
      non_match: -5.0
  thresholds:
    match: 0.95
    review: 0.70

YAML Config without EM (fixed probabilities):

yaml
decision:
  scoring:
    strategy: fellegi_sunter
    fields:
      - name: email
        comparator: exact
        normalizer: email
        m_probability: 0.95
        u_probability: 0.01
      - name: name
        comparator: jaro_winkler
        normalizer: name
        m_probability: 0.90
        u_probability: 0.10
      - name: phone
        comparator: exact
        normalizer: phone
        m_probability: 0.90
        u_probability: 0.02
    thresholds:
      match: 6.0        # log-likelihood composite
      possible: 2.0
      non_match: -3.0
  thresholds:
    match: 0.9
    review: 0.7

Each field supports an optional normalizer that runs before comparison — both during EM training and scoring. See Matching Rules > Pre-Match Normalization for the full list.

Sensitivity Analysis

Understanding which fields drive your match decisions:

Field elasticity — How much does removing a field change the composite score?

Field RemovedAvg. Composite ChangeImpact
email-4.55 (agree) / +2.98 (disagree)Critical — removing email drops most matches below threshold
phone-3.81 / +2.28High — phone is the second strongest signal
name-2.20 / +2.20Medium — contributes but doesn't dominate
city-2.08 / +1.50Low — confirmatory but not decisive

Threshold sensitivity — Small changes in the match threshold can significantly change match counts:

Match ThresholdPairs MatchedPrecisionRecall
8.02,34099.8%82%
6.03,18099.2%91%
4.04,52097.1%96%
2.06,89091.3%99%

The right threshold depends on your tolerance for false positives vs. false negatives. Start with a high threshold and lower it until recall is acceptable.

ML Ensemble

Use a logistic regression model with trained coefficients to score pairs. This requires labeled training data but can capture non-linear relationships between fields.

How It Works

score = sigmoid(bias + sum(coefficient_i × rule_score_i))
      = 1 / (1 + e^(-(bias + sum(coefficient_i × rule_score_i))))

The coefficients are trained on labeled match/non-match pairs. The output is a probability between 0 and 1.

YAML Config

yaml
decision:
  scoring:
    strategy: ml_ensemble
    bias: -2.5
    coefficients:
      email_exact: 4.2
      name_fuzzy: 1.8
      phone_exact: 3.1
      address_fuzzy: 0.9
  thresholds:
    match: 0.90
    review: 0.60

When to Use

  • You have 1,000+ labeled match/non-match pairs
  • Simple weighted sum isn't capturing the patterns in your data
  • You've tried Fellegi-Sunter and want to compare performance

When Not to Use

  • You don't have labeled training data (use weighted sum or Fellegi-Sunter with EM instead)
  • You need to explain individual match decisions to regulators (coefficients are less interpretable than m/u probabilities)

Custom Expression

Define your own scoring formula using arithmetic operators and rule scores as variables.

How It Works

Write an expression using rule names as variables. Available operators: +, -, *, /, min(), max().

YAML Config

yaml
rules:
  - name: email_exact
    type: exact
    field: email
    weight: 1.0
  - name: name_fuzzy
    type: similarity
    field: name
    algorithm: jaro_winkler
    threshold: 0.85
    weight: 0.6
  - name: phone_exact
    type: exact
    field: phone
    weight: 0.85

decision:
  scoring:
    strategy: custom
    expression: "max(email_exact, (name_fuzzy * 0.7 + phone_exact * 0.3))"
  thresholds:
    match: 0.9
    review: 0.6

This scores a pair as the maximum of:

  1. The email score alone (an exact email match = automatic 1.0)
  2. A weighted combination of name and phone

When to Use

  • You need domain-specific logic that weighted sum can't express
  • You want to implement "if email matches, always match regardless of other fields"
  • You're migrating from a legacy system with a known scoring formula

Choosing a Strategy

                    Do you have labeled training data?
                           /              \
                         Yes               No
                         /                   \
                   ML Ensemble          Do you need calibrated
                                        probabilities?
                                         /          \
                                       Yes           No
                                       /               \
                              Fellegi-Sunter       Weighted Sum
                              (with or without EM)   (default)

Start with weighted_sum. It's the simplest, requires no training data, and works well for 80% of use cases. Switch to Fellegi-Sunter when you need calibrated probabilities or regulatory justification. Use ML Ensemble only when you have labeled data and weighted sum isn't performing well enough.

Next Steps

The identity and delegation layer for AI agents.