Skip to contents

Each input is a score table over the *same* set of (RAW, ANON) candidate pairs. The result holds the weighted sum of their SCORE columns.

Usage

combine_scores(scores, weights = NULL, scale_check = c("warn", "none"))

Arguments

scores

a list of score tables (each with columns RAW_ROW_NUMBER, ANON_ROW_NUMBER and SCORE), all covering the same candidate pairs and all of the same `score_type`

weights

numeric vector of weights, one per element of `scores` (default: 1 for each, so combining a single score table returns it unchanged). Must be non-negative and not all zero – a negative weight would flip that component's orientation, turning "far apart" into "good match".

scale_check

`"warn"` (default) to warn when the weighted standard deviations of two components differ by more than 10x, `"none"` to skip the check. Components with zero weight or constant SCORE are excluded: they add a constant and cannot change any ranking.

Value

a "reid_scores" table over the same candidate pairs, ordered as the first element of `scores`, whose SCORE is the weighted sum and whose `score_type` is that of the inputs.

Details

The pair sets are required to match exactly, and a pair missing from one input is an error rather than a silently dropped row: dropping candidate pairs would shrink the attacker's search space and therefore report a *lower* reidentification rate than the truth, which is the failure direction a safety-checking tool must never take quietly (docs/lessons-learned.md section 2).

SCALING IS THE CALLER'S JOB. The metrics in this package live on very different scales – an edit distance is a small integer, a squared L2 quantile distance is unbounded – so an unweighted sum lets whichever metric happens to have the largest spread dominate. `combine_scores()` deliberately does not normalise: normalisation is a separate, explicit step ([normalize_scores()], or [score_multi()] which does it for you). Use `weights` to compensate otherwise, and read a combined score as "this particular weighted sum", not as a scale-free distance.

WHAT DOMINATION ACTUALLY COSTS. The component with the widest weighted spread decides the ranking; the others only break its ties. Whether that hurts depends on the *dominant* component, and the effect runs in both directions – so a large scale gap is a condition to check, not a defect by itself:

  • dominant component also the most informative: the sum is as good as, or better than, the normalised sum. On the fixture in `docs/default-changes.md` an under-weighted informative axis still reached 0.8417, identical to the normalised combination.

  • dominant component the *less* informative one: the sum tracks that component and adding attributes lowers the measured rate. On the same fixture, ZIP alone scores 0.2000 and normalised ZIP + SPEND_DIST scores 0.4450, but the unnormalised sum – where SPEND_DIST's spread is roughly 16000x ZIP's – scores 0.0300. A safety tool reporting 0.03 where 0.45 is achievable understates the risk 15-fold, and nothing errors.

Because the harmful case cannot be told from the harmless one without running the attack, `combine_scores()` warns when the weighted spreads of two components differ by more than 10x. 10 is where the loss first becomes visible in the calibration recorded in `docs/default-changes.md` (ratio 3: under 1 and is also the largest gap that costs nothing when the dominant component is the informative one, so it does not fire on the harmless direction. Silence the check with `scale_check = "none"` when the scale gap is deliberate, as in an IDF block where the magnitude *is* the evidence.

See also

[normalize_scores()] to put the components on a common scale first, and [score_multi()] which normalises and combines in one call.

Examples

raw <- data.frame(ROW_NUMBER = 1:5, V = c(10, 20, 30, 40, 50), W = c(1, 1, 2, 2, 3))
d <- join_raw_anon_data(raw, raw)
parts <- normalize_scores(list(score_num(d, "V"), score_num(d, "W")), "range")
combined <- combine_scores(parts, weights = c(1, 2))
match_greedy(combined)
#>   ANON_ROW_NUMBER RAW_ROW_NUMBER CONFIDENCE RESULT
#> 1               1              1  0.2054120   TRUE
#> 2               2              2  0.2276257   TRUE
#> 3               3              3  0.3471051   TRUE
#> 4               4              4  0.3210806   TRUE
#> 5               5              5  1.0270600   TRUE