
Domänenverteilung
Um die Verteilung der Domänen aufrecht zu erhalten, müssen im ersten Durchgang die Daten eine Zuordnung von einer echten Domäne zu einer gefälschten Domäne herstellen. Domains wie gmail.com können zudem auf die weiße Liste gesetzt werden und z.B. direkt zugeordnet werden. Für diesen Fall brauchen wir beispielsweise nur einen gefälschten Benutzernamen.
Um eine Domänenzuordnung oder Whitelist zu erstellen, muss ein Objekt mittels CSV erstellt oder importiert werden, dass eine Whitelist von einer Festplatte oder aus dem ursprünglichen Datensatz generieren kann. Dafür kann folgendes Skript verwendet werden:
import csv
import json
from faker import Factory
from collections import Counter
from collections import MutableMapping
class DomainMapping(MutableMapping):
@classmethod
def load(cls, fobj):
"""
Load the mapping from a JSON file on disk.
"""
data = json.load(fobj)
return cls(**data)
@classmethod
def generate(cls, emails):
"""
Pass through a list of emails and count domains to whitelist.
"""
# Count all the domains in each email address
counts = Counter([
email.split("@")[-1] for email in emails
])
# Create a domain mapping
domains = cls()
# Ask the user what domains to whitelist based on frequency
for idx, (domain, count) in enumerate(counts.most_common())):
prompt = "{}/{}: Whitelist {} ({} addresses)?".format(
idx+1, len(counts), domain, count
)
print prompt
ans = raw_input("[y/n/q] > ").lower()
if ans.startswith('y'):
# Whitelist the domain
domains[domain] = domain
elif ans.startswith('n'):
# Create a fake domain
domains[domain]
elif ans.startswith('q'):
break
else:
continue
return domains
def __init__(self, whitelist=[], mapping={}):
# Create the domain mapping properties
self.fake = Factory.create()
self.domains = mapping
# Add the whitelist as a mapping to itself.
for domain in whitelist:
self.domains[domain] = domain
def dump(self, fobj):
"""
Dump the domain mapping whitelist/mapping to JSON.
"""
whitelist = []
mapping = self.domains.copy()
for key in mapping.keys():
if key == mapping[key]:
whitelist.append(mapping.pop(key))
json.dump({
'whitelist': whitelist,
'mapping': mapping
}, fobj, indent=2)
def __getitem__(self, key):
"""
Get a fake domain for a real domain.
"""
if key not in self.domains:
self.domains[key] = self.fake.domain_name()
return self.domains[key]
def __setitem__(self, key, val):
self.domains[key] = val
def __delitem__(self, key):
del self.domains[key]
def __iter__(self):
for key in self.domains:
yield key
Wir wollen nun den Code zur Einfachheit etwas aufschlüsseln.
- Durch
MutableMapping
wird die Klasse erweitert, die eine abstrakte Basisklasse in einem collections Modul ist. - Die Basisklasse gibt uns die Möglichkeit, sich wie ein
dict
Objekt zu verhalten. Dafür müssen wir lediglich entsprechende Methoden bereitstellen, wie z.B.getitem
oder Wörterbuchmethoden wiesetitem
,delitem
,iter
,pop values domains
. - Die
getitem
Methode verhält sich sehr ähnlich zurdefaultdict
Methode, die mittels eines Schlüssels versuchen wird, Daten abzurufen, die keine Zuordnung besitzen. Somit werden gefälschte Daten in ihrem Namen generiert und alle Domains, die nicht auf der Whitelist sitzen, anonymisiert.
Anschließend laden wir mit load
die Dumpdaten in eine JSON Datei auf die Festplatte. Damit erhalten wir die Zuordnung der Anonymisierungsläufe. Diese Methode ist recht simpel, da sie sich ein offenes, dateiähnliches Objekt heraussucht, mit dem JSON Modul kombiniert und die Domänenzuordnung instanziert und zurückgibt. Dafür ist die dump-Methode etwas komplexer. Sie teilt die Whitelist und das Mapping in separate Objekte auf, damit die Daten auf der Festplatte geändert werden können. Beide Methoden ermöglichen uns jedoch, das Mapping in eine JSON Datei zu laden und zu speichern:
{
„whitelist“ : [
„ gmail.com “,
„ yahoo.com “
],
„mapping“ : {
„ districtdatalabs.com “ : „fadel.org“,
„umd.edu“ : „ferrystanton.org“
}
}
Mit der generate-Methode
wird es ermöglicht, einen ersten Durchgang durch die Email Liste zu starten, die Anzahl der Domains zu zählen und diese dann dem User in der Reihenfolge der Häufigkeit vorzuschlagen. Der User hat dann die Option diese zur Whitelist hinzuzufügen. Dies sieht dann wie folgt aus:
1/245: Whitelist "gmail.com" (817 addresses)?
[y/n/q] >
Beachten Sie, dass die Eingabeaufforderung eine Fortschrittsanzeige enthält. Diese ist wichtig für große Datensätze mit eindeutigen Domains. Beenden Sie den Vorgang, werden weiterhin Domains gefälscht und der Benutzer sieht allerdings nur die häufigsten Beispiele der Whitelist. Die Intention dahinter ist, dass dieser Mechanismus die CSV Datei liest, daraus die Whitelist erstellt und diese auf der Festplatte speichert, damit sie als Standard für die Anonymisierung verwendet werden kann.
Realistische Profile
Um realistische Profile erstellen zu können ist en entsprechender Anbieter notwendig, der die Domainkarte verwendet und gefälschte Daten für jede Kombination generiert. Dieser Anbieter bietet auch die Möglichkeit, mehrere Namen und Email-Adressen einem einzelnen Profil zuzuordnen. Um diesen Anbieter zu erstellen, nutzen wir folgenden Code:
class Profile(object):
def __init__(self, domains):
self.domains = domains
self.generator = Factory.create()
def fuzzy_profile(self, name=None, email=None):
"""
Return a profile that allows for fuzzy names and emails.
"""
parts = self.fuzzy_name_parts()
return {
"names": {name: self.fuzzy_name(parts, name)},
"emails": {email: self.fuzzy_email(parts, email)},
}
def fuzzy_name_parts(self):
"""
Returns first, middle, and last name parts
"""
return (
self.generator.first_name(),
self.generator.first_name(),
self.generator.last_name()
)
def fuzzy_name(self, parts, name=None):
"""
Creates a name that has a similar case to the passed in name.
"""
# Extract the first, initial, and last name from the parts.
first, middle, last = parts
# Create the name, with chance of middle or initial included.
chance = self.generator.random_digit()
if chance < 2:
fname = u"{} {}. {}".format(first, middle[0], last)
elif chance < 4:
fname = u"{} {} {}".format(first, middle, last)
else:
fname = u"{} {}".format(first, last)
if name is not None:
# Match the capitalization of the name
if name.isupper(): return fname.upper()
if name.islower(): return fname.lower()
return fname
def fuzzy_email(self, parts, email=None):
"""
Creates an email similar to the name and original email.
"""
# Extract the first, initial, and last name from the parts.
first, middle, last = parts
# Use the domain mapping to identify the fake domain.
if email is not None:
domain = self.domains[email.split("@")[-1]]
else:
domain = self.generator.domain_name()
# Create the username based on the name parts
chance = self.generator.random_digit()
if chance < 2:
username = u"{}.{}".format(first, last)
elif chance < 3:
username = u"{}.{}.{}".format(first, middle[0], last)
elif chance < 6:
username = u"{}{}".format(first[0], last)
elif chance < 8:
username = last
else:
username = u"{}{}".format(
first, self.generator.random_number()
)
# Match the case of the email
if email is not None:
if email.islower(): username = username.lower()
if email.isupper(): username = username.upper()
else:
username = username.lower()
return u"{}@{}".format(username, domain)
Auch hier sind wir wieder mit einer Menge Code konfrontiert, den wir kurz aufschlüsseln werden:
- Ein Profil ist zunächst eine Kombination der Zuordnung von original Datensatz zu gefälschtem Datensatz.
- Die Zuordnung erfolgt über die Domain zur ursprünglichen Email Domain
So wird zum Beispiel jede Email-Domain mit der Endung @madafa.de derselben gefälschten Domain zugeordnet.
Um auf die Beziehung von Namen zu Emails zurückgreifen zu können, müssen wir direkter auf die Namen zugreifen können. Dafür nutzen wir den “Namensteile-Generator”, der gefälschte Vor. & Nachnamen generiert. Darauf basierend kann die Email-Adresse sich entsprechend anpassen und verschiedene Formen annehmen. Damit erhalten wir realistischere Profile:
>>> fake.fuzzy_profile()
{'names': {None: u'Zaire Ebert'}, 'emails': {None: u'ebert@von.com'}}
>>> fake.fuzzy_profile(
... name='Daniel Webster', email='dictionaryguy@gmail.com')
{'names': {'Daniel Webster': u'Georgia McDermott'},
'emails': {'dictionaryguy@gmail.com': u'georgia9@gmail.com'}}
Hierbei ist es wichtig, dieses Profilobjekt so einzurichten, dass es mehrere Namen und Email-Adressen demselben Objekt zuordnen kann. Diesen Vorgang nennen wir Fuzzy-Matching.
Fuzzy Matching gefälschter Daten
In unserem ursprünglichen Datensatz haben wir Entitäten dupliziert, mit derselben Email-Adresse aber einem anderem Namen. Hierbei ist es ganz egal, ob Sie den Zweitnamen verwenden oder einen Spitznamen. Auch können geschäftliche Email-Adressen dafür verwendet werden. Damit erstellen wir unscharfe (fuzzy) Profilobjekte. Diese ermöglichen uns die Zuordnung aller vorhandenen Namensteile zu falschen Namen. Wichtig ist es, dass wir diese Duplikate erkennen können. Dafür nutzen wir das fuzzywuzzy Modul:
$ pip install fuzzywuzzy python-Levenshtein
Analog zu unserem Domain-Mapping gehen wir auch hier vor und suchen nach ähnlichen Namens- & Email Paaren, um sie dem User vorzuschlagen. Denkt jetzt der User, dass es sich um ein Duplikat handelt, führen wir das Profilobjekt zu einem einzigen Profil zusammen und verwenden die Zuordnung der Anonymisierung.
Zunächst müssen wir exakte Duplikate entfernen. Dazu erstellen wir eine hashfähige Datenstruktur für unsere Profile mit einer namedtuple
:
from collections import namedtuple
from itertools import combinations
Person = namedtuple('Person', 'name, email')
def pairs_from_rows(rows):
"""
Expects rows of dictionaries with name and email keys.
"""
# Create a set of person tuples (no exact duplicates)
people = set([
Person(row['name'], row['email']) for row in rows
])
# Yield ordered pairs of people objects without replacement
for pair in combinations(people, 2):
yield pair
Das namedtuple
ist eine kompakte und unveränderliche Datenstruktur, die auf Namen und Eigenschaften zugreifen kann. Da sie unveränderlich ist, ist sie im Gegensatz zu Dictionaries auch hashbar. Mit der pairs_from_rows
Funktion eliminieren wir dann die exakten Duplikate. In diesem Vorgang werden einige Tupel erstellt, die dann die combinations
Funktion verwenden, um in intertools
Paare zu erzeugen.
Im nächsten Schritt möchten wir herausfinden, wie hoch die Ähnlichkeit der Paare tatsächlich ist. Dafür greifen wir auf die fuzzywuzzy Bibliothek zurück, um einen partiellen Mittelwert zu ermitteln.
from fuzzywuzzy import fuzz
from functools import partial
def normalize(value, email=False):
"""
Make everything lowercase and remove spaces.
If email, only take the username portion to compare.
"""
if email:
value = value.split("@")[0]
return value.lower().replace(" ", "")
def person_similarity(pair):
"""
Returns the mean of the normalized partial ratio scores.
"""
# Normalize the names and the emails
names = map(normalize, [p.name for p in pair])
email = map(
partial(normalize, email=True), [p.email for p in pair]
)
# Compute the partial ratio scores for both names and emails
scores = [
fuzz.partial_ratio(a, b) for a, b in [names, emails]
]
# Return the mean score of the pair
return float(sum(scores)) / len(scores)
Die Ergebniswerte liegen dann entweder zwischen 0 (keine Übereinstimmung) bis 100 (exakte Übereinstimmung). Ein Wert von 100 ist jedoch sehr unwahrscheinlich, da wir bereits in den oberen Schritten die exakten Übereinstimmungen eliminiert haben sollten. Hier ein Beispiel:
>>> person_similarity([
... Person('John Lennon', 'john.lennon@gmail.com'),
... Person('J. Lennon', 'jlennon@example.org')
... ] )
80.5
Mit dem Fuzzing-Prozess durchlaufen wir unseren kompletten Datensatz, erstellen Personenpaare und berechnen die Ähnlichkeitswerte. Damit können wir zusätzlich nach Prozentpunkten filtern und die Ergebnisse unseren Anforderungen gemäß bewerten. Wurde ein Duplikat gefunden, kann dieses mit einem Profilobjekt zusammengeführt werden und eine Namens- & Email-Adressen Zuordnung stattfinden.
Fazit
Wie bereits zu Beginn erwähnt, erfinden wir mit dieser Methode nicht das Rad neu. Sie hilft uns lediglich dabei, Datensätze entsprechend zu verschleiern und damit vor Missbrauch zu schützen. Trotz allem können auch bei den recht simplen Vorgängen diverse Probleme aufkommen, sodass wir immer dazu raten, sich Experten zur Seite zu holen. Als zertifizierte Experten sind wir Ihre passenden Ansprechpartner und treiben mit Ihnen gemeinsam den Bereich Datenwissenschaft weiter voran. Kontaktieren Sie uns gerne bei Bedarf oder Rückfragen hier.