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.