Synthetische Testdaten mit Python



Definition synthetischer Daten

Chernoff Faces

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)
{'Jane Smiley', 'Simone Rampman', 'Sarah Moore', 'Laura Miles', 'Roger Rampman', 'Eve Cook', 'Roger Looper', 'Eve Moore', 'Paul Rampman', 'Robert Smith', 'Simone Chopman', 'Sarah Smiley', 'Bernard Chopman', 'Simone Looper', 'John Chopman'}

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))
    
Yvonne Rampman
Yvonne Singer
Frank Bychan
Yvonne Chopman
Frank Cook
Robert Bychan
Eve Bychan
Jane Baker
Jane Miller
Kathrin Singer
Roger Baker
Roger Miles
Laura Miles
Frank Baker
Paul Smiley

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



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 continually structured bouquet
leading to a lingering toast finish!
2. wine:
This wine is light-bodied with a interactively mocha flavour
leading to a lingering angular finish!
3. wine:
This wine is full-bodied with a monotonectally lingering bouquet
leading to a lingering tight finish!
4. wine:
This wine is medium-bodied with a interactively angular flavour
leading to a lingering tobacco finish!
5. wine:
This wine is light-bodied with a dramatically flowers aroma
leading to a lingering tight finish!
6. wine:
This wine is light-bodied with a progressively juicy flavour
leading to a lingering toasty finish!
7. wine:
This wine is light-bodied with a credibly bright bouquet
leading to a lingering unoaked finish!
8. wine:
This wine is light-bodied with a dramatically flabby bouquet
leading to a lingering tobacco finish!
9. wine:
This wine is light-bodied with a progressively bright bouquet
leading to a lingering jammy finish!
10. wine:
This wine is medium-bodied with a rapidiously butterscotch flavour
leading to a lingering tobacco finish!
11. wine:
This wine is light-bodied with a professionally velvetly flavour
leading to a lingering jammy finish!
12. wine:
This wine is light-bodied with a proactively bright bouquet
leading to a lingering unctuous 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', 'Switzerland', 'Laura Fischer', 'Security Aid'), ('00002', 'Germany', 'Michael Meyer', 'Social Worker'), ('00003', 'France', 'Alexandre Petit', 'Social Worker'), ('00004', 'Switzerland', 'Hans Schmid', 'Medical Aid'), ('00005', 'Germany', 'Maria Schmidt', 'Social Worker'), ('00006', 'Germany', 'Peter Müller', 'Social Worker'), ('00007', 'France', 'Alexandre Nicolas', 'Social Worker'), ('00008', 'Switzerland', 'Walter Schmid', 'Social Worker'), ('00009', 'Switzerland', 'Laura Weber', 'Security Aid'), ('00010', 'Germany', 'Gabriele Bauer', 'Social Worker'), ('00011', 'Switzerland', 'Bruno Fischer', 'Medical Aid'), ('00012', 'Germany', 'Stefanie Meyer', 'Social Worker'), ('00013', 'Switzerland', 'Hans Gerber', 'Social Worker'), ('00014', 'Germany', 'Brigitte Weber', 'Medical Aid'), ('00015', 'Germany', 'Wolfgang Müller', 'Social Worker'), ('00016', 'Switzerland', 'Noémie Keller', 'Social Worker'), ('00017', 'France', 'Alexandre Mercier', 'Social Worker'), ('00018', 'Switzerland', 'Walter Frei', 'Social Worker'), ('00019', 'Germany', 'Brigitte Richter', 'Social Worker'), ('00020', 'Switzerland', 'Marcel Keller', 'Medical Aid'), ('00021', 'Switzerland', 'Hans Fischer', 'Security Aid'), ('00022', 'Switzerland', 'Urli Schneider', 'Medical Aid'), ('00023', 'Switzerland', 'Daniel Meier', 'Social Worker'), ('00024', 'Germany', 'Wolfgang Schneider', 'Social Worker'), ('00025', 'Switzerland', 'Bruno Steiner', 'Medical Aid'), ('00026', 'France', 'Marie Matin', 'Social Worker'), ('00027', 'France', 'Léa Mercier', 'Medical Aid'), ('00028', 'Switzerland', 'Mélissa Brunner', 'Medical Aid'), ('00029', 'Germany', 'Monika Schulz', 'Security Aid'), ('00030', 'France', 'Maxime Durand', 'Security Aid'), ('00031', 'France', 'Manon Durand', 'Medical Aid'), ('00032', 'France', 'Alexandre Dubois', 'Social Worker'), ('00033', 'Germany', 'Ursula Koch', 'Security Aid'), ('00034', 'Germany', 'Ursula Schulz', 'Security Aid'), ('00035', 'Germany', 'Peter Koch', 'Social Worker'), ('00036', 'Switzerland', 'Hans Meyer', 'Social Worker'), ('00037', 'Switzerland', 'Hans Huber', 'Medical Aid'), ('00038', 'France', 'Léa Dubois', 'Security Aid'), ('00039', 'France', 'Nicolas Leroy', 'Social Worker'), ('00040', 'Switzerland', 'Reto Müller', 'Security Aid'), ('00041', 'Switzerland', 'Noémie Weber', 'Social Worker'), ('00042', 'Germany', 'Stefanie Richter', 'Social Worker'), ('00043', 'Germany', 'Michael Schneider', 'Security Aid'), ('00044', 'France', 'Laura Nicolas', 'Security Aid'), ('00045', 'France', 'Chloé Mathieu', 'Social Worker'), ('00046', 'Switzerland', 'Daniel Huber', 'Social Worker'), ('00047', 'Switzerland', 'Sarah Fischer', 'Medical Aid'), ('00048', 'Germany', 'Matthias Richter', 'Medical Aid'), ('00049', 'Switzerland', 'Walter Schneider', 'Social Worker'), ('00050', 'Switzerland', 'Laura Huber', 'Security Aid')]