Nächstes Kapitel: Boolsche Maskierung von Arrays
Es wird kaum einen Ingenieur oder Wissenschaftler geben, der nicht die Notwendigkeit kennt, synthetische Daten zu erzeugen. Aber einige fragen sich sicherlich, was denn mit "synthetischen Testdaten" gemeint ist. Es gibt viele Situationen, in denen ein Wissenschaftler oder ein Ingenieur Testdaten braucht, es aber nahezu unmöglich ist, an "richtige" Daten heranzukommen, z.B. an eine Stichprobe aus einer Population, die aus Messungen gewonnen wurde. Die Aufgabe bei der Erstellung von synthetischen Daten besteht darin, Daten zu produzieren, die den "richtigen Daten" sehr nahe kommen. Python ist eine ideale Sprache, um auf einfache Weise solche Daten zu produzieren, weil sie starke numerische und linguistische Funktionen bereitstellt.
Synthetische Daten sind ebenfalls notwendig, um spezielle Bedürfnisse oder bestimmte Bedingungen zu erfüllen, die nicht oder nicht häufig genug in "echten Daten" vorkommen.
Im vorigen Kapitel "Python, NumPy und Wahrscheinlichkeiten" haben wir einige Funktionen beschrieben, die wir im folgenden benötigen:
- find_interval
- weighted_choice
- cartesian_choice
- weighted_cartesian_choice
- weighted_sample
Sie sollten mit der Funktionsweise dieser Funktionen vertraut sein.
Wir haben die Funktionen im Modul mit dem Namen bk_random abgelegt.
Definition des Umfangs für die synthetische Datenerzeugung
Wir möchten Lösungen vorstellen für die folgende Aufgabe:
Wir haben n endliche Mengen die Daten verschiedener Typen enthalten:
D1, D2, ... Dn
Die Mengen Di sind die Daten-Mengen aus denen wir unsere synthetischen Daten herleiten wollen.
In der aktuellen Implementierung sind die Mengen Tupels oder Listen, um es praktisch zu halten.
Der Prozess der synthetischen Datenerzeugung kann in den zwei Funktionen "synthesizer" und "synthesize" definiert werden. Den Begriff "Synthesizer" wird normalerweise für ein computergestützes Gerät verwendet, welches Sound produziert. Unser "Synthesizer" prouziert Strings oder alternativ Tupels mit Daten, wie wir später sehen werden.
Die Funktion "synthesizer" erstellt die Funktion "synthesize":
synthesize = synthesizer( (D1, D2, ... Dn) )
Die Funktion "synthesize", - die in unserer Implementierung auch ein Generator sein kann, - erwartet kein Argument und das Ergebnis des Funktions-Aufrufs von "synthesize()" ist
- eine Liste oder ein Tupel t = (d1, d2, ... dn), wobei di zufällig aus Di gezogen wurde
- oder ein String der die Elemente str(d1), str(d2), ... str(dn) enthält, bei denen di ebenfalls zufällig aus Di gezogen wurde.
Lassen Sie uns mit einem einfachen Beispiel starten. Wir haben eine Liste mit Vornamen und eine Liste mit Nachnamen. Wir möchten weitere Mitarbeiter anheuern für ein Firma. Natürlich ist es einfacher einen Spezialisten in unserer synthetischen Umgebung anzuheuern als jemanden im richtigen Leben. Alles was benötigt ist die Funktion "cartesian_choice" aus dem bk_random-Modul und die Verkettung der zufällig gezogenen Vornamen und Nachnamen.
import bk_random
firstnames = ["John", "Eve", "Jane", "Paul",
"Frank", "Laura", "Robert",
"Kathrin", "Roger", "Simone",
"Bernard", "Sarah", "Yvonne"]
surnames = ["Singer", "Miles", "Moore",
"Looper", "Rampman", "Chopman",
"Smiley", "Bychan", "Smith",
"Baker", "Miller", "Cook"]
number_of_specialists = 15
employees = set()
while len(employees) < number_of_specialists:
employee = bk_random.cartesian_choice(firstnames, surnames)
employees.add(" ".join(employee))
print(employees)
Das war einfach genug, aber wir möchten dies nun mehr strukturieren, indem wir den bereits erwähnten Synthesizer verwenden. Im folgenden Code fehlt noch die Implementierung für den Fall, dass der Parameter "weights" nicht None ist:
import bk_random
firstnames = ["John", "Eve", "Jane", "Paul",
"Frank", "Laura", "Robert",
"Kathrin", "Roger", "Simone",
"Bernard", "Sarah", "Yvonne"]
surnames = ["Singer", "Miles", "Moore",
"Looper", "Rampman", "Chopman",
"Smiley", "Bychan", "Smith",
"Baker", "Miller", "Cook"]
def synthesizer( data, weights=None, format_func=None, repeats=True):
"""
data is a tuple or list of lists or tuples containing the
data
weights is a list or tuple of lists or tuples with the
corresponding weights of the data lists or tuples
format_func is a reference to a function which defines
how a random result of the creator function will be formated.
If None, "creator" will return the list "res".
If repeats is set to True, the results of helper will not be unique
"""
if not repeats:
memory = set()
def synthesize():
while True:
res = bk_random.cartesian_choice(*data)
if not repeats:
sres = str(res)
while sres in memory:
res = bk_random.cartesian_choice(*data)
sres = str(res)
memory.add(sres)
if format_func:
yield format_func(res)
else:
yield res
return synthesize
recruit_employee = synthesizer( (firstnames, surnames),
format_func=lambda x: " ".join(x),
repeats=False)
employee = recruit_employee()
for _ in range(15):
print(next(employee))
Im vorigen Beispiel hat jeder Name, d.h. Vor- und Nachname, die gleiche Wahrscheinlichkeit gezogen zu werden. Das ist nicht wirklich realisitsch, weil wir in Ländern wie US oder England Namen wie "Smith" oder "Miller" öfter erwarten als Namen wie "Rampman" oder "Bychan". Wir erweitern unsere synthesizer-Funktion um zusätzlichen Code für den "gewichteten" Fall, d.h. "weights" ist nicht None. Wenn Gewichtungen (weights) gegeben sind, müssen wir die Funktion weighted_cartesian_choice aus dem Modul bk_random verwenden. Wenn "weights" auf None gesetzt ist, müssen wir die Funktion cartesian_choice verwenden. Für lagern diese Entscheidung in eine weitere Unterfunktion des Synthesizers aus diese sauberer zu halten.
Wir wollen an dieser Stelle nicht mit Wahrscheinlichkeiten zwischen 0 und 1 umher werfen um die Gewichtungen zu definieren. Also nehmen wir den Umweg über Integer-Werte, die wir nachher normalisieren.
from bk_random import cartesian_choice, weighted_cartesian_choice
weighted_firstnames = [ ("John", 80), ("Eve", 70), ("Jane", 2),
("Paul", 8), ("Frank", 20), ("Laura", 6),
("Robert", 17), ("Zoe", 3), ("Roger", 8),
("Simone", 9), ("Bernard", 8), ("Sarah", 7),
("Yvonne", 11), ("Bill", 12), ("Bernd", 10)]
weighted_surnames = [('Singer', 2), ('Miles', 2), ('Moore', 5),
('Looper', 1), ('Rampman', 1), ('Chopman', 1),
('Smiley', 1), ('Bychan', 1), ('Smith', 150),
('Baker', 144), ('Miller', 87), ('Cook', 5),
('Joyce', 1), ('Bush', 5), ('Shorter', 6),
('Klein', 1)]
firstnames, weights = zip(*weighted_firstnames)
wsum = sum(weights)
weights_firstnames = [ x / wsum for x in weights]
surnames, weights = zip(*weighted_surnames)
wsum = sum(weights)
weights_surnames = [ x / wsum for x in weights]
weights = (weights_firstnames, weights_surnames)
def synthesizer( data, weights=None, format_func=None, repeats=True):
"""
"data" is a tuple or list of lists or tuples containing the
data.
"weights" is a list or tuple of lists or tuples with the
corresponding weights of the data lists or tuples.
"format_func" is a reference to a function which defines
how a random result of the creator function will be formated.
If None,the generator "synthesize" will yield the list "res".
If "repeats" is set to True, the output values yielded by
"synthesize" will not be unique.
"""
if not repeats:
memory = set()
def choice(data, weights):
if weights:
return weighted_cartesian_choice(*zip(data, weights))
else:
return cartesian_choice(*data)
def synthesize():
while True:
res = choice(data, weights)
if not repeats:
sres = str(res)
while sres in memory:
res = choice(data, weights)
sres = str(res)
memory.add(sres)
if format_func:
yield format_func(res)
else:
yield res
return synthesize
recruit_employee = synthesizer( (firstnames, surnames),
weights = weights,
format_func=lambda x: " ".join(x),
repeats=False)
employee = recruit_employee()
for _ in range(8):
print(next(employee))
Wein Beispiel
Stellen Sie sich vor, dass Sie ein Dutzend Weine beschreiben müssen. Sehr wahrscheinlich für die meisten eine schöne Vorstellung, allerdings nicht für mich. Der Hauptgrund: "Ich bein kein Weintrinker!"
Wir können eine kleines Python-Programm schreiben, die unsere synthesize-Funktion benutzt um automatisch "anspruchsvolle Kritiken" zu schreiben, wie z.B.:
This wine is light-bodied with a conveniently juicy bouquet leading to a lingering flamboyant finish!
Finden Sie ein paar Adverben, wie "nahtlos", "bestimmend", und ein paar Adjektive wie "fruchtig" und "veredelt" um das Aroma zu beschreiben.
Wenn Sie ihre Liste definiert haben, können Sie die synthesize-Funktion verwenden.
Sollten Sie es nicht selbst machen wollen, so ist hier unsere Lösung:
import bk_random
body = ['light-bodied', 'medium-bodied', 'full-bodied']
adverbs = ['appropriately', 'assertively', 'authoritatively',
'compellingly', 'completely', 'continually',
'conveniently', 'credibly', 'distinctively',
'dramatically', 'dynamically', 'efficiently',
'energistically', 'enthusiastically', 'fungibly',
'globally', 'holisticly', 'interactively',
'intrinsically', 'monotonectally', 'objectively',
'phosfluorescently', 'proactively', 'professionally',
'progressively', 'quickly', 'rapidiously',
'seamlessly', 'synergistically', 'uniquely']
noun = ['aroma', 'bouquet', 'flavour']
aromas = ['angular', 'bright', 'lingering', 'butterscotch',
'buttery', 'chocolate', 'complex', 'earth', 'flabby',
'flamboyant', 'fleshy', 'flowers', 'food friendly',
'fruits', 'grass', 'herbs', 'jammy', 'juicy', 'mocha',
'oaked', 'refined', 'structured', 'tight', 'toast',
'toasty', 'tobacco', 'unctuous', 'unoaked', 'vanilla',
'velvetly']
example = """This wine is light-bodied with a completely buttery
bouquet leading to a lingering fruity finish!"""
def describe(data):
body, adv, adj, noun, adj2 = data
format_str = "This wine is %s with a %s %s %s\nleading to"
format_str += " a lingering %s finish!"
return format_str % (body, adv, adj, noun, adj2)
t = bk_random.cartesian_choice(body, adverbs, aromas, noun, aromas)
data = (body, adverbs, aromas, noun, aromas)
synthesize = synthesizer( data, weights=None, format_func=describe, repeats=True)
criticism = synthesize()
for i in range(1, 13):
print("{0:d}. wine:".format(i))
print(next(criticism))
print()
Übung: Internationale Katastrophen-Operation
Es wäre großartig, wenn die in dieser Übung beschriebenen Probleme wirklich künstlich bleiben. Komplett unrealistisch, aber ein schöner Tagtraum. Jedoch, die Aufgabe dieser Übung ist es, synthetische Test-Daten bereitszustellen für eine internationale Katastrophen-Operation. Die Länder, die an dieser Operation beteiligt sind, sind z.B. Frankreich, Schweiz, Deutschland, Kanada, die Niederlande, die vereinigten Staaten, Österreich, Belgien und Luxemburg.
Wir möchten eine Datei erstellen mit zufälligen Einträgen verschiedener Berater. Jede Zeile sollte folgende Informationen beinhalten:
eindeutige Identifikation, Vorname, Nachname, Land, Fachbereich
Beispiel:
001, Jean-Paul, Rennier, France, Medical Aid 002, Nathan, Bloomfield, Canada, Security Aid 003, Michael, Mayer, Germany, Social Worker
Aus praktischen Gründen reduzieren wir in der folgenden Beispiel-Implementierung die Länder auf Frankreich, Schweiz und Deutschland:
from bk_random import cartesian_choice, weighted_cartesian_choice
countries = ["France", "Switzerland", "Germany"]
w_firstnames = { "France" : [ ("Marie", 10), ("Thomas", 10),
("Camille", 10), ("Nicolas", 9),
("Léa", 10), ("Julien", 9),
("Manon", 9), ("Quentin", 9),
("Chloé", 8), ("Maxime", 9),
("Laura", 7), ("Alexandre", 6),
("Clementine", 2), ("Grégory", 2),
("Sandra", 1), ("Philippe", 1)],
"Switzerland": [ ("Sarah", 10), ("Hans", 10),
("Laura", 9), ("Peter", 8),
("Mélissa", 9), ("Walter", 7),
("Océane", 7), ("Daniel", 7),
("Noémie", 6), ("Reto", 7),
("Laura", 7), ("Bruno", 6),
("Eva", 2), ("Urli", 4),
("Sandra", 1), ("Marcel", 1)],
"Germany": [ ("Ursula", 10), ("Peter", 10),
("Monika", 9), ("Michael", 8),
("Brigitte", 9), ("Thomas", 7),
("Stefanie", 7), ("Andreas", 7),
("Maria", 6), ("Wolfgang", 7),
("Gabriele", 7), ("Manfred", 6),
("Nicole", 2), ("Matthias", 4),
("Christine", 1), ("Dirk", 1)] }
w_surnames = { "France" : [ ("Matin", 10), ("Bernard", 10),
("Camille", 10), ("Nicolas", 9),
("Dubois", 10), ("Petit", 9),
("Durand", 8), ("Leroy", 8),
("Fournier", 7), ("Lambert", 6),
("Mercier", 5), ("Rousseau", 4),
("Mathieu", 2), ("Fontaine", 2),
("Muller", 1), ("Robin", 1)],
"Switzerland": [ ("Müller", 10), ("Meier", 10),
("Schmid", 9), ("Keller", 8),
("Weber", 9), ("Huber", 7),
("Schneider", 7), ("Meyer", 7),
("Steiner", 6), ("Fischer", 7),
("Gerber", 7), ("Brunner", 6),
("Baumann", 2), ("Frei", 4),
("Zimmermann", 1), ("Moser", 1)],
"Germany": [ ("Müller", 10), ("Schmidt", 10),
("Schneider", 9), ("Fischer", 8),
("Weber", 9), ("Meyer", 7),
("Wagner", 7), ("Becker", 7),
("Schulz", 6), ("Hoffmann", 7),
("Schäfer", 7), ("Koch", 6),
("Bauer", 2), ("Richter", 4),
("Klein", 2), ("Schröder", 1)] }
# separate names and weights
synthesize = {}
identifier = 1
for country in w_firstnames:
firstnames, weights = zip(*w_firstnames[country])
wsum = sum(weights)
weights_firstnames = [ x / wsum for x in weights]
w_firstnames[country] = [firstnames, weights_firstnames]
surnames, weights = zip(*w_surnames[country])
wsum = sum(weights)
weights_surnames = [ x / wsum for x in weights]
w_surnames[country] = [surnames, weights_firstnames]
synthesize[country] = synthesizer( (firstnames, surnames),
(weights_firstnames,
weights_surnames),
format_func=lambda x: " ".join(x),
repeats=False)
nation_prob = [("Germany", 0.3),
("France", 0.3),
("Switzerland", 0.4)]
profession_prob = [("Medical Aid", 0.3),
("Social Worker", 0.5),
("Security Aid", 0.2)]
helpers = []
for _ in range(50):
country = weighted_cartesian_choice(zip(*nation_prob))
profession = weighted_cartesian_choice(zip(*profession_prob))
country, profession = country[0], profession[0]
s = synthesize[country]()
uid = "{id:05d}".format(id=identifier)
helpers.append((uid, country, next(s), profession ))
identifier += 1
print(helpers)
Nächstes Kapitel: Boolsche Maskierung von Arrays