Anonymous Data That Is Not
Three ordinary fields, and how many people they identify
A model is built from data about people, and that fact carries obligations that are legal rather than optional. This week is the practical version of them.
Removing the names is not anonymisation
import pandas as pd
rng = np.random.RandomState(0)
n = 2000
df = pd.DataFrame({
'postcode': rng.choice(['M1', 'M2', 'M3', 'M4', 'M5'], n),
'birth_year': rng.randint(1950, 2006, n),
'sex': rng.choice(['F', 'M'], n),
'condition': rng.choice(['none', 'asthma', 'diabetes', 'cardiac'],
n, p=[0.7, 0.13, 0.12, 0.05]),
})
# the 'anonymous' release: no names, no identifiers
released = df[['postcode', 'birth_year', 'sex', 'condition']]
print(released.head(3).to_string(index=False))
print()
combo = released.groupby(['postcode', 'birth_year', 'sex']).size()
unique = int((combo == 1).sum())
print('%d people in the release' % len(released))
print('%d of them are the only person with their combination of'
% unique)
print('postcode, birth year and sex')
print()
print('that is %.1f%% of the dataset, individually identifiable to'
% (100 * unique / len(released)))
print('anybody who knows those three ordinary facts about them')
M5 1966 F none
M1 1978 F none
M4 1959 F none
2000 people in the release
53 of them are the only person with their combination of
postcode, birth year and sex
that is 2.6% of the dataset, individually identifiable to
anybody who knows those three ordinary facts about them
This is a well documented failure, not a hypothetical
Public health, transport and search datasets have all been released as anonymous and then re-identified by researchers using exactly this technique. The lesson is that anonymity is a property of a dataset in the context of every other dataset in the world, not a property you can establish by inspecting your own columns.
import pandas as pd
rng = np.random.RandomState(0)
n = 2000
df = pd.DataFrame({
'postcode': rng.choice(['M1', 'M2', 'M3', 'M4', 'M5'], n),
'birth_year': rng.randint(1950, 2006, n),
'sex': rng.choice(['F', 'M'], n),
'condition': rng.choice(['none', 'asthma', 'diabetes', 'cardiac'],
n, p=[0.7, 0.13, 0.12, 0.05]),
})
def unique_share(cols):
sizes = df.groupby(cols).size()
counts = df.groupby(cols).size().reset_index(name='k')
merged = df.merge(counts, on=cols)
return float((merged['k'] == 1).mean())
print('%-44s %10s' % ('quasi-identifiers released', 'unique'))
for cols in [['postcode'], ['postcode', 'sex'],
['postcode', 'birth_year'],
['postcode', 'birth_year', 'sex']]:
print('%-44s %10.3f' % (', '.join(cols), unique_share(cols)))
postcode 0.000
postcode, sex 0.000
postcode, birth_year 0.000
postcode, birth_year, sex 0.026
Each additional column multiplies the number of distinct combinations, so uniqueness rises very quickly. This is why we only released three harmless fields is not a defence.