score generalised (interval / categorical) attributes by containment
Source:R/generalize.R
score_containment.RdThe score for anonymised data that publishes *regions* instead of values: "30代" rather than 37, "東京都" rather than 千代田区, "135****" rather than a full postcode. No distance can be computed on such a column – and [score_num()] on it either errors or coerces to NA and reports that nobody was reidentified, which reads as "safe". The question containment asks instead is which RAW records *could* have produced this ANON record.
Usage
score_containment(
dat_raw_anon,
targets,
row_number = "ROW_NUMBER",
hierarchy = NULL,
rules = NULL,
units = generalization_units(),
.fn_name = "score_containment"
)Arguments
- dat_raw_anon
dataframe of raw_anon form
- targets
character vector of column names (before RAW_/ANON_ prefixing) to intersect
- row_number
name of the row-number column *before* the RAW_/ANON_ prefixing done by [join_raw_anon_data()] (default: "ROW_NUMBER")
- hierarchy
a "reid_hierarchy" object from [read_generalization_hierarchy()] or [generalization_hierarchy()], or NULL (the default) when every generalisation is an interval that speaks for itself
- rules
optional named character vector forcing a rule per column, one of `"auto"` (default: interval if the value parses as one, else exact string equality, in both cases widened by the hierarchy), `"interval"`, `"exact"` or `"prefix"` (for masked codes such as `"135****"`)
- units
unit strings that may follow a number, see [generalization_units()]
Warns when any ANON record ends up with an **empty** candidate set. That is the failure Issue #101 is about: nothing is contained, every candidate ties at 1, and the record contributes the random baseline to the reported rate – while [reid_evaluate()]'s `blocked` / `n_true_missing` / `truth_coverage` all still report a healthy join, because the candidate table keeps its full shape. The warning names the published values that excluded everybody.
- .fn_name
name used in error messages; a function that wraps this one passes its own name so the message points at the function the user actually called
Value
a "reid_scores" table (a distance in \[0, 1\]: smaller is a better match), carrying a `candidate_count` attribute – the `k` per ANON record. [reid_evaluate()] reads it and prints how many records have `k == 0`.
Details
Several `targets` are intersected, not added: a record must fall inside the published region of **every** attribute to stay a candidate. That is what makes weak attackers measurable – each attribute the attacker holds cuts the candidate set again, and the cuts multiply.
The score is `1 - 1/k` for a contained candidate, where `k` is the number of RAW records that survive for that ANON record, and `1` for an excluded one. All survivors therefore tie, [match_greedy()] draws among them uniformly and reports `CONFIDENCE = 1/k`, and `1/k` is exactly the information the release gave away. Use [containment_counts()] to see the `k` values directly.
A value of `"*"`, `"**"`, `""` or `NA` on the ANON side means "suppressed" and matches every RAW value, so a fully suppressed column contributes nothing rather than excluding everybody.
See also
[containment_counts()] for the per-record narrowing, and [generalize_value()] for building generalised columns.
Examples
raw <- data.frame(ROW_NUMBER = 1:4, AGE = c(31, 37, 46, 52),
SEX = c("M", "F", "F", "M"), stringsAsFactors = FALSE)
anon <- data.frame(ROW_NUMBER = 1:4, AGE = c("30s", "30s", "40s", "50s"),
SEX = c("M", "F", "F", "M"), stringsAsFactors = FALSE)
d <- join_raw_anon_data(raw, anon)
match_greedy(score_containment(d, c("AGE", "SEX")))
#> ANON_ROW_NUMBER RAW_ROW_NUMBER CONFIDENCE RESULT
#> 1 1 1 2 TRUE
#> 2 2 2 2 TRUE
#> 3 3 3 2 TRUE
#> 4 4 4 2 TRUE