Synthetische Test-Daten mit Python



Definition Synthetischer Daten

Chernoff Faces

Es wird kaum einen Ingeneur oder Wissenschaftler geben, der nicht die Notwendigkeit Synthetischer Daten versteht. Aber einige fragen sich sicherlich, was denn mit "synthetischen Test-Daten" gemeint ist. Es gibt viele Situationen, in denen ein Wissenschaftler oder ein Ingeneur Test-Daten braucht, es aber nahezu unmöglich ist an "richtige" Daten heranzukommen, z.B. an eine Sample 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 es starke numerische und linguistische Funktionen bereitstellt.

Synthetische Daten sind ebenfalls notwendig um spezielle Bedürfnisse oder bestimmte Bedingungen zu erfüllen die nicht in "echten Daten" vorkommen.

Im vorigen Kapitel "Python, Numpy und Wahrscheinlichkeiten" haben wir einige Funktionen beschrieben, die wir im folgenden benötigen:

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

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)
{'Simone Looper', 'Laura Miles', 'Kathrin Miles', 'Paul Miles', 'Paul Rampman', 'Laura Singer', 'Laura Chopman', 'John Baker', 'Bernard Chopman', 'Yvonne Bychan', 'Paul Smiley', 'Frank Smith', 'Sarah Smith', 'Frank Chopman', 'Laura Smiley'}

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))
    
Frank Looper
Roger Cook
John Smith
Bernard Chopman
Roger Looper
Eve Singer
Frank Smiley
Jane Cook
Laura Singer
John Smiley
Laura Miller
Laura Rampman
Jane Singer
Yvonne Rampman
Sarah Rampman

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))
Frank Smith
Robert Smith
John Baker
Eve Smith
John Miller
Frank Miller
Eve Baker
Yvonne Smith



Wein Beispiel

grapes

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()
1. wine:
This wine is full-bodied with a intrinsically velvetly aroma
leading to a lingering vanilla finish!
2. wine:
This wine is light-bodied with a credibly butterscotch bouquet
leading to a lingering velvetly finish!
3. wine:
This wine is medium-bodied with a dynamically flabby flavour
leading to a lingering food friendly finish!
4. wine:
This wine is light-bodied with a interactively jammy aroma
leading to a lingering flabby finish!
5. wine:
This wine is light-bodied with a quickly unctuous flavour
leading to a lingering unoaked finish!
6. wine:
This wine is light-bodied with a dynamically refined bouquet
leading to a lingering structured finish!
7. wine:
This wine is medium-bodied with a quickly unoaked flavour
leading to a lingering flowers finish!
8. wine:
This wine is full-bodied with a credibly flowers bouquet
leading to a lingering herbs finish!
9. wine:
This wine is light-bodied with a compellingly juicy aroma
leading to a lingering unctuous finish!
10. wine:
This wine is medium-bodied with a appropriately herbs bouquet
leading to a lingering lingering finish!
11. wine:
This wine is light-bodied with a holisticly buttery bouquet
leading to a lingering lingering finish!
12. wine:
This wine is light-bodied with a enthusiastically juicy bouquet
leading to a lingering tight finish!



Übung: Internationale Katastrophen-Operation

World of Flags

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)
[('00001', 'France', 'Manon Nicolas', 'Social Worker'), ('00002', 'France', 'Thomas Petit', 'Medical Aid'), ('00003', 'Switzerland', 'Noémie Steiner', 'Social Worker'), ('00004', 'Switzerland', 'Daniel Müller', 'Social Worker'), ('00005', 'Germany', 'Dirk Schneider', 'Social Worker'), ('00006', 'France', 'Laura Nicolas', 'Social Worker'), ('00007', 'Germany', 'Stefanie Schmidt', 'Social Worker'), ('00008', 'France', 'Camille Nicolas', 'Social Worker'), ('00009', 'Switzerland', 'Eva Weber', 'Social Worker'), ('00010', 'Switzerland', 'Sarah Steiner', 'Security Aid'), ('00011', 'Switzerland', 'Sarah Müller', 'Social Worker'), ('00012', 'Germany', 'Ursula Klein', 'Social Worker'), ('00013', 'Switzerland', 'Walter Brunner', 'Medical Aid'), ('00014', 'Switzerland', 'Peter Zimmermann', 'Social Worker'), ('00015', 'France', 'Laura Camille', 'Security Aid'), ('00016', 'Switzerland', 'Marcel Keller', 'Medical Aid'), ('00017', 'Germany', 'Wolfgang Schneider', 'Medical Aid'), ('00018', 'Germany', 'Maria Schmidt', 'Medical Aid'), ('00019', 'Germany', 'Andreas Weber', 'Social Worker'), ('00020', 'France', 'Chloé Fournier', 'Social Worker'), ('00021', 'Switzerland', 'Hans Keller', 'Medical Aid'), ('00022', 'Germany', 'Ursula Schneider', 'Medical Aid'), ('00023', 'Switzerland', 'Urli Moser', 'Medical Aid'), ('00024', 'Germany', 'Andreas Müller', 'Medical Aid'), ('00025', 'Switzerland', 'Reto Meier', 'Medical Aid'), ('00026', 'France', 'Quentin Dubois', 'Security Aid'), ('00027', 'Germany', 'Brigitte Koch', 'Medical Aid'), ('00028', 'Germany', 'Stefanie Weber', 'Social Worker'), ('00029', 'Germany', 'Stefanie Schneider', 'Social Worker'), ('00030', 'Germany', 'Maria Müller', 'Security Aid'), ('00031', 'France', 'Alexandre Nicolas', 'Social Worker'), ('00032', 'Switzerland', 'Walter Schneider', 'Medical Aid'), ('00033', 'Germany', 'Brigitte Wagner', 'Social Worker'), ('00034', 'France', 'Laura Lambert', 'Medical Aid'), ('00035', 'Switzerland', 'Mélissa Huber', 'Social Worker'), ('00036', 'France', 'Léa Camille', 'Social Worker'), ('00037', 'Germany', 'Thomas Fischer', 'Social Worker'), ('00038', 'Germany', 'Manfred Meyer', 'Security Aid'), ('00039', 'France', 'Camille Dubois', 'Social Worker'), ('00040', 'France', 'Julien Fournier', 'Social Worker'), ('00041', 'France', 'Laura Matin', 'Security Aid'), ('00042', 'France', 'Maxime Dubois', 'Medical Aid'), ('00043', 'Switzerland', 'Daniel Frei', 'Social Worker'), ('00044', 'Germany', 'Monika Schmidt', 'Social Worker'), ('00045', 'Germany', 'Andreas Koch', 'Security Aid'), ('00046', 'France', 'Chloé Robin', 'Medical Aid'), ('00047', 'France', 'Maxime Muller', 'Social Worker'), ('00048', 'France', 'Quentin Bernard', 'Social Worker'), ('00049', 'Switzerland', 'Hans Müller', 'Medical Aid'), ('00050', 'Switzerland', 'Reto Fischer', 'Medical Aid')]