The Knowledge Discovery in Database process is detailed in the following phases:
In this sense, it may be useful to define a key operation that is antecedent to Knowledge Discovery that statisticians often find themselves having to face: Record Linkage.
Record Linkage is an operation that allows joining multiple datasets in order to have more information. The objective is to identify records referring to the same individual, but located in different files, through common keys that do not perfectly correspond.
Input: two data sets that observe overlapping groups of units.
Problem: lack of a unique and error-free identification code
Solution: use of a set of variables capable (jointly) of identifying records
Attention: variables can have "problems" (there is no unique one)
Objective: greater number of correct matches, fewer number of incorrect matches
Input file preparation (pre-processing):
Selection of common identifying attributes (blocking and matching variables):
Deterministic: Fixed rules; manual check (clerical review) for errors.
Probabilistic: Statistical model with "optimal" decision rules and error probabilities.
When data cannot discriminate a match, use:
Evaluate the accuracy of the result and determine if standard estimators can be applied directly.
| MANUAL REVIEW RESULTS | Matched | Not Matched |
|---|---|---|
| Linkage Matched | TP | FP |
| Linkage Not Matched | TN | FN |
Below we demonstrate record linkage techniques using string similarity measures on fake data.
from difflib import SequenceMatcher
def string_similarity(s1, s2):
return SequenceMatcher(None, s1.lower(), s2.lower()).ratio()
# Example pairs
pairs = [
("John Smith", "Jon Smith"), # typo
("Maria Garcia", "Maria G."), # abbreviation
("Robert Johnson", "Bob Johnson"), # nickname
("Jennifer Williams", "Jenny Williams"),
("Michael Brown", "David Wilson"), # no match
]
for a, b in pairs:
sim = string_similarity(a, b)
print(f" '{a}' vs '{b}' -> {sim:.3f}")
# Output:
# 'John Smith' vs 'Jon Smith' -> 0.947
# 'Maria Garcia' vs 'Maria G.' -> 0.667
# 'Robert Johnson' vs 'Bob Johnson' -> 0.769
# 'Jennifer Williams' vs 'Jenny Williams' -> 0.875
# 'Michael Brown' vs 'David Wilson' -> 0.308
def record_similarity(rec_a, rec_b, weights={'name': 0.4, 'dob': 0.4, 'city': 0.2}):
name_sim = string_similarity(rec_a['name'], rec_b['name'])
dob_sim = 1.0 if rec_a['dob'] == rec_b['dob'] else 0.0
city_sim = string_similarity(rec_a['city'], rec_b['city'])
return weights['name']*name_sim + weights['dob']*dob_sim + weights['city']*city_sim
# Compare all pairs
dataset_a = [{'name': 'John Smith', 'dob': '1985-03-15', 'city': 'New York'}]
dataset_b = [{'name': 'Jon Smith', 'dob': '1985-03-15', 'city': 'New York'},
{'name': 'David Wilson', 'dob': '1995-04-18', 'city': 'Dallas'}]
for b in dataset_b:
sim = record_similarity(dataset_a[0], b)
match = "MATCH" if sim > 0.7 else "NO MATCH"
print(f" {dataset_a[0]['name']} vs {b['name']}: {sim:.3f} [{match}]")
# Output:
# John Smith vs Jon Smith: 0.979 [MATCH]
# John Smith vs David Wilson: 0.149 [NO MATCH]
# Blocking: reduce comparison space by grouping on DOB year
def block_by_year(records):
blocks = {}
for i, rec in enumerate(records):
year = rec['dob'][:4]
blocks.setdefault(year, []).append(i)
return blocks
dataset = [
{'name': 'John', 'dob': '1985-03-15'},
{'name': 'Maria', 'dob': '1990-07-22'},
{'name': 'Jon', 'dob': '1985-04-20'},
{'name': 'Bob', 'dob': '1978-11-30'},
]
blocks = block_by_year(dataset)
total_pairs = len(dataset) * (len(dataset)-1) // 2
blocked_pairs = sum(len(v)*(len(v)-1)//2 for v in blocks.values())
print(f"Total possible pairs: {total_pairs}")
print(f"Pairs after blocking: {blocked_pairs}")
print(f"Reduction: {(1 - blocked_pairs/total_pairs)*100:.0f}%")
# Output:
# Total possible pairs: 6
# Pairs after blocking: 1
# Reduction: 83%