Zum Hauptinhalt springen

Realistische Zufallsdatensätze mit Trumania generieren

Entdecke Python Trumania, eine szenariobasierte Bibliothek zur Generierung zufälliger Datensätze. Lerne in diesem Schritt-für-Schritt-Tutorial, wie du synthetische Datensätze erstellst.
· 15 Min. lesen

Entdecke KI

Hinweis der Redaktion: Dieser Beitrag ist in Zusammenarbeit mit Milan van der Meer entstanden. Beide Autor:innen dieses Beitrags sind Teil des Teams von Real Impact Analytics, einem innovativen belgischen Big-Data-Startup, das den Wert von Telekomdaten hebt, indem es „Big Data appifiziert“.

Dieses Tutorial gibt dir einen ersten Eindruck, warum du zufällige Datensätze generieren solltest und was du davon erwarten kannst. Außerdem zeigt es dir anhand erster Beispiele, wie du Trumania, eine Python-Bibliothek zur Datengenerierung, einsetzt.

Mehr Infos findest du auf Trumanias GitHub!

Warum zufällige Datensätze generieren?

Das Erzeugen zufälliger Datensätze ist für Data Engineers und Data Scientists gleichermaßen relevant. Warum arbeiten Data Scientists und Data Engineers mit synthetischen Daten – und wie kommen sie daran?

Als Data Engineer möchtest du nach dem Schreiben deiner neuen, großartigen Datenverarbeitungsanwendung End-to-End testen – dafür brauchst du Eingabedaten.

Als Data Scientist profitierst du von Datengenerierung, weil du damit verschiedene Ansätze zur Exploration von Datensätzen, Algorithmen, Visualisierungstechniken ausprobieren oder Annahmen über das Verhalten einer Methode gegen viele unterschiedliche Datensätze deiner Wahl validieren kannst.

In beiden Fällen liegt es nahe, einfach echte Daten zu verwenden. Ein kleines Problem: Produktivdaten sind in der Regel schwer zu bekommen – und mit neuen europäischen Vorgaben zu Datenschutz und Sicherheit wird es nicht einfacher.

Vielleicht gehörst du zu den Glücklichen und hast einen schönen, vollständigen Produktivdatensatz auf deinem Laptop. Selbst dann möchtest du oft mehr Varianz im Datensatz, um verschiedene Situationen zu prüfen: unterschiedliche Populationsgrößen, verschiedene Verteilungen, typische Datenqualitätsprobleme wie fehlende oder ungültige Werte ...

Eine ausführlichere Begründung, warum das Generieren zufälliger Datensätze sinnvoll ist, findest du hier: „Why synthetic data is about to become a major competitive advantage“.

Schema-basierte Zufallsdatengenerierung: Beziehungen zählen!

Dieser Abschnitt veranschaulicht die schema-basierte Datengenerierung und ihre Schwächen.

Es gibt bereits viele Tools, um Zufallsdatensätze zu erzeugen. Ein gängiger Ansatz ist die schema-basierte Generierung: Du definierst einen Bauplan und erzeugst damit Entitäten. Khermes und LogSynth sind zwei Beispiele.

Ein Beispiel für eine solche Konfiguration könnte ein Personenschema beinhalten:

{
  {
    "field": "Name",
    "class": NameGenerator
  },
  {
    "field": "Age",
    "class": RandomInt(18, 65)
  },
  {
    "field": "Gender",
    "class": RandomPicker(["Male", "Female", "Other"])
  } 
}

Dieses Schema definiert Daten zu einer Person: Name, Alter und Geschlecht. Schnell und simpel – aber mit Einschränkungen. Ein Hauptproblem: Beziehungen zwischen Entitäten oder zeitliche Abhängigkeiten realistisch abzubilden.

Wie machst du etwa bestimmte Namen wahrscheinlicher abhängig vom Geschlecht? Oder wie erzeugst du ein Log von Nutzeraktionen, in dem Aktionen am Wochenende häufiger auftreten?

Die schema-basierte Vorgehensweise erschwert es, deine Anwendung mit realistischen Besonderheiten zu konfrontieren, z. B.:

  • Unausgewogene Datensätze (schwierig für Group-by-Operationen);
  • Geclusterte Muster in Entitätsnetzwerken, z. B. Erkennen sozialer Gruppen über Interaktionen (etwa Telefonate);
  • Unterschiedliche Aktivitätsprofile über die Zeit. Aktionen passieren zu bestimmten Tages- oder Wochenzeiten häufiger;
  • Kausale Abhängigkeiten zwischen Aktionen, z. B. viele Käufe in einem Laden, die letztlich ein „Out-of-stock“-Ereignis auslösen.

Trumania setzt auf Szenarien, um diese Lücken zu schließen und realistischere Datensätze zu erzeugen. Während ein Szenario abläuft, interagieren verschiedene Populationen miteinander, aktualisieren ihre Eigenschaften und erzeugen Logs.

Ein Beispiel: Menschen rufen ihre Freund:innen an. Für Anrufe werden Minuten verbraucht, sind die Minuten aufgebraucht, geht es in den Shop ... Der resultierende Datensatz könnte ein Log der Anrufe, der Shop-Besuche und der Lagerbestände pro Stunde sein. Aktionen können zufällig oder deterministisch sein.

In Trumania sind die erzeugten Datensätze typischerweise Zeitreihen, weil sie aus dem Ablauf eines Szenarios über die Zeit entstehen.

Abhängige Daten mit Trumania generieren (aka Tutorial-Zeit!)

Im folgenden Beispiel baust du ein einfaches Szenario, in dem Personen einander Nachrichten schicken. Das Ergebnis ist eine Zeitreihe der zwischen Personen ausgetauschten Nachrichten.

Die Schritte sind:

  • Erstellen eines Trumania-Circus – der Welt, in der das Szenario läuft
  • Hinzufügen einer „Hello World“-Story, die einfache, fest codierte Logs erzeugt
  • Hinzufügen einer Relationship, um jeder Person Lieblingszitate zuzuordnen, die als Nachrichteninhalt dienen
  • Parametrisieren der Zeitkomponenten, um eine realistischere Verteilung der Nachrichten über den Tag zu erzielen
  • Hinzufügen einer weiteren Relationship, die ein soziales Netzwerk definiert, damit der aus dem Nachrichtenlog entstehende Graph realistischer wird

Einen Trumania Circus erstellen

Zweck

Als Erstes erstellst du einen Circus – die Welt, in der alle Elemente des Szenarios leben. Außerdem legen wir eine Grundpopulation von persons an. Da vieles in Trumania zufällig ist, lernst du hier auch das Konzept der generators kennen, die zufälliges Verhalten steuern.

Wir fügen noch keine Story hinzu, alles bleibt zunächst statisch.

Los geht’s!

So geht’s

Ein Trumania-Circus wird so erstellt:

from trumania.core import circus

example_circus = circus.Circus(name="example1", 
                               master_seed=12345,
                               start=pd.Timestamp("1 Jan 2017 00:00"),
                               step_duration=pd.Timedelta("1h"))

In Trumania steuert eine zentrale Uhr alle zeitbezogenen Elemente. Wichtig im obigen Snippet ist step_duration=pd.Timedelta("1h") – die Uhr tickt also in 1-Stunden-Schritten.

Als Nächstes fügst du die Population person hinzu. Eine Population ist im Kern eine Menge von Agenten mit einer id und Attributen.

Du kannst Attributwerte manuell festlegen, meist willst du sie aber zufällig generieren. Definiere dafür einige generators:

from trumania.core.random_generators import SequencialGenerator, FakerGenerator, NumpyRandomGenerator

id_gen = SequencialGenerator(prefix="PERSON_")
age_gen = NumpyRandomGenerator(method="normal", loc=3, scale=5,
                               seed=next(example_circus.seeder))
name_gen = FakerGenerator(method="name", seed=next(example_circus.seeder))

Ein Trumania-Generator liefert bei generate() Daten. id_gen erzeugt Strings wie PERSON_0001, PERSON_0002 ... age_gen sampelt wiederholt aus einer Normalverteilung, name_gen liefert zufällige Namen.

Hinweis: Jede statistische Verteilung aus numpy sowie jeder Faker-Provider ist in Trumania verfügbar und kann leicht erweitert werden.

Jetzt fügst du 1000 Personen mit fortlaufenden IDs und zwei zufälligen Attributen hinzu:

person = example_circus.create_population(name="person", size=1000, ids_gen=id_gen)
person.create_attribute("NAME", init_gen=name_gen)
person.create_attribute("AGE", init_gen=age_gen)

Ergebnis

Per person.to_dataframe() kannst du dir die generierten Attribute aller person-Mitglieder ansehen.

+-------------------+-----------------+---------+
|                   | NAME            |     age |
|-------------------+-----------------+---------|
| PERSON_0000000000 | Amy Berger      | 28.588  |
| PERSON_0000000001 | Michael Curry   | 28.7499 |
| PERSON_0000000002 | Robert Ramirez  | 35.9242 |
| PERSON_0000000003 | Derek Gonzalez  | 34.7091 |
| PERSON_0000000004 | Gregory Fischer | 25.6009 |
| PERSON_0000000005 | Erica Walker    | 33.9758 |
| PERSON_0000000006 | Bradley Collins | 24.4428 |
| PERSON_0000000007 | James Rodriguez | 34.7835 |
| PERSON_0000000008 | Brandy Padilla  | 34.6296 |
| PERSON_0000000009 | Mark Taylor     | 38.2286 |
+-------------------+-----------------+---------+

Tipp: Den kompletten Code für diese Grundpopulation findest du in diesem Snippet auf Github!

Hello-World-Statements: Stories erstellen

Zweck

Machen wir es spannender und fügen eine story hinzu. Eine Trumania-Story kapselt dynamisches Verhalten. Sie wird jedes Mal ausgeführt, wenn die simulierte Uhr einen Schritt weiterschaltet.

Wir haben oben step_duration=pd.Timedelta("1h") gesetzt – jede logische Stunde kann also jede Story ausgeführt werden.

Wir bauen eine simple Hello-World-Story, in der alle Mitglieder der Population person regelmäßig die Nachricht „hello world“ senden.

So geht’s

Beim Erstellen einer story legst du zuerst die initiierende Population fest – hier person. Zweitens definierst du den timer, der festlegt, wann jedes Mitglied die Aktion triggert. Zunächst setzen wir ihn hart auf 1 – damit führt jedes Mitglied die Story bei jedem Zeitschritt aus (später bauen wir ausgefeiltere Timer):

hello_world = example_circus.create_story(
    name="hello_world",
    initiating_population=example_circus.populations["person"],
    member_id_field="PERSON_ID",
    timer_gen=ConstantDependentGenerator(value=1)
)

Noch macht die Story nichts – wir müssen Operationen hinzufügen. Diese können zufällig oder deterministisch sein, Attribute lesen/schreiben oder Seiteneffekte haben.

Wir starten mit zwei deterministischen Operationen plus Logger:

hello_world.set_operations(
    example_circus.clock.ops.timestamp(named_as="TIME"),
    ConstantGenerator(value="hello world").ops.generate(named_as="MESSAGE"),
    FieldLogger(log_id="hello")
)

example_circus.clock.ops.timestamp erzeugt einen zufälligen Zeitstempel innerhalb des aktuellen 1-Stunden-Intervalls, ConstantGenerator liefert konstante Werte.

Ergebnis

Lassen wir den Circus 48 Stunden simuliert laufen:

example_circus.run(
    duration=pd.Timedelta("48h"),
    log_output_folder="example_scenario",
    delete_existing_logs=True
)

Das ergibt einen Datensatz, in dem jede der 1000 Personen über 2 Tage hinweg 48-mal „hello world“ gesagt hat.

Das erste Feld PERSON_ID entspricht member_id_field der story. TIME und MESSAGE stammen aus den beiden Operationen.

+-------+-------------------+---------------------+-------------+
|       | PERSON_ID         | TIME                | MESSAGE     |
|-------+-------------------+---------------------+-------------|
|  0    | PERSON_0000000000 | 2017-01-01 01:14:12 | hello world |
|  1    | PERSON_0000000001 | 2017-01-01 01:23:51 | hello world |
|  2    | PERSON_0000000002 | 2017-01-01 01:37:11 | hello world |
|  3    | PERSON_0000000003 | 2017-01-01 01:33:12 | hello world |
|  4    | PERSON_0000000004 | 2017-01-01 01:08:02 | hello world |
|  5    | PERSON_0000000005 | 2017-01-01 01:13:36 | hello world |
|  6    | PERSON_0000000006 | 2017-01-01 01:35:08 | hello world |
|  7    | PERSON_0000000007 | 2017-01-01 01:24:30 | hello world |
|  8    | PERSON_0000000008 | 2017-01-01 01:01:51 | hello world |
|  9    | PERSON_0000000009 | 2017-01-01 01:41:41 | hello world |
| ...                                                           |
| 23990 | PERSON_0000000990 | 2017-01-02 23:33:28 | hello world |
| 23991 | PERSON_0000000991 | 2017-01-02 23:15:26 | hello world |
| 23992 | PERSON_0000000992 | 2017-01-02 23:45:56 | hello world |
| 23993 | PERSON_0000000993 | 2017-01-02 23:25:16 | hello world |
| 23994 | PERSON_0000000994 | 2017-01-02 23:33:55 | hello world |
| 23995 | PERSON_0000000995 | 2017-01-02 23:41:45 | hello world |
| 23996 | PERSON_0000000996 | 2017-01-02 23:39:23 | hello world |
| 23997 | PERSON_0000000997 | 2017-01-02 23:45:53 | hello world |
| 23998 | PERSON_0000000998 | 2017-01-02 23:29:25 | hello world |
| 23999 | PERSON_0000000999 | 2017-01-02 23:12:24 | hello world |
+-------+-------------------+---------------------+-------------+

Tipp: Den kompletten Code findest du auf Github

Jemand, dem man Hello World sagt

Zweck

Wir erweitern das Beispiel und fügen eine zufällige weitere Person als Gesprächspartner:in hinzu. Vorerst kann jede Person mit gleicher Wahrscheinlichkeit mit jeder anderen sprechen – nicht sehr realistisch (ein soziales Netzwerk folgt später). Außerdem reichern wir den Datensatz um Namen basierend auf der ID an.

So geht’s

Die andere Person speichern wir im neuen Feld OTHER_PERSON. In dieser einfachen Variante wählen wir ungewichtet zufällig aus der Population person. Dafür nutzen wir die Operation select_one dieser Population:

example_circus.set_operations(
    example_circus.clock.ops.timestamp(named_as="TIME"),
    ConstantGenerator(value="hello world").ops.generate(named_as="MESSAGE"),

    example_circus.populations["person"].ops.select_one(named_as="OTHER_PERSON"),

    example_circus.populations["person"]
        .ops.lookup(id_field="PERSON_ID", select={"NAME": "EMITTER_NAME"}),

    example_circus.populations["person"]
        .ops.lookup(id_field="OTHER_PERSON", select={"NAME": "RECEIVER_NAME"}),

    FieldLogger(log_id="hello")
)

Ergebnis

Nach erneutem Lauf enthält der Datensatz das neue Feld OTHER_PERSON:

+-------+-------------------+---------------------+-------------+-------------------+---------------------+--------------------+
|       | PERSON_ID         | TIME                | MESSAGE     | OTHER_PERSON      | EMITTER_NAME        | RECEIVER_NAME      |
|-------+-------------------+---------------------+-------------+-------------------+---------------------+--------------------|
|  0    | PERSON_0000000000 | 2017-01-01 01:14:12 | hello world | PERSON_0000000852 | Ann Cruz            | Sophia Black       |
|  1    | PERSON_0000000001 | 2017-01-01 01:23:51 | hello world | PERSON_0000000429 | Kimberly Sanchez    | Jeffrey Ryan       |
|  2    | PERSON_0000000002 | 2017-01-01 01:37:11 | hello world | PERSON_0000000925 | Bethany Smith       | Regina Brown       |
|  3    | PERSON_0000000003 | 2017-01-01 01:33:12 | hello world | PERSON_0000000347 | Frank Middleton     | Jacob Ross         |
|  4    | PERSON_0000000004 | 2017-01-01 01:08:02 | hello world | PERSON_0000000211 | Cheryl Decker       | Joshua Miller      |
|  5    | PERSON_0000000005 | 2017-01-01 01:13:36 | hello world | PERSON_0000000779 | Thomas Rodriguez    | Nicole Tanner      |
|  6    | PERSON_0000000006 | 2017-01-01 01:35:08 | hello world | PERSON_0000000331 | James Peters        | Melissa Rogers     |
|  7    | PERSON_0000000007 | 2017-01-01 01:24:30 | hello world | PERSON_0000000234 | Allison Hansen      | Taylor Smith       |
|  8    | PERSON_0000000008 | 2017-01-01 01:01:51 | hello world | PERSON_0000000678 | Candice Sellers     | James Smith        |
|  9    | PERSON_0000000009 | 2017-01-01 01:41:41 | hello world | PERSON_0000000108 | Maria White         | James Nguyen       |
| ...                                                                                                                          | 
| 23990 | PERSON_0000000990 | 2017-01-02 23:33:28 | hello world | PERSON_0000000689 | Natasha Brown       | Bailey Ramirez DDS |
| 23991 | PERSON_0000000991 | 2017-01-02 23:15:26 | hello world | PERSON_0000000250 | Shelly Ponce        | Jordan Johnson     |
| 23992 | PERSON_0000000992 | 2017-01-02 23:45:56 | hello world | PERSON_0000000413 | Steven Mendez       | Crystal Duffy      |
| 23993 | PERSON_0000000993 | 2017-01-02 23:25:16 | hello world | PERSON_0000000670 | Morgan Rice         | Jonathan Obrien    |
| 23994 | PERSON_0000000994 | 2017-01-02 23:33:55 | hello world | PERSON_0000000411 | Crystal Vincent     | George Mathis      |
| 23995 | PERSON_0000000995 | 2017-01-02 23:41:45 | hello world | PERSON_0000000563 | Dawn Kim            | Jennifer Martinez  |
| 23996 | PERSON_0000000996 | 2017-01-02 23:39:23 | hello world | PERSON_0000000668 | Jimmy Franco        | Dr. Mark Ruiz MD   |
| 23997 | PERSON_0000000997 | 2017-01-02 23:45:53 | hello world | PERSON_0000000268 | Christian Christian | Victoria Donovan   |
| 23998 | PERSON_0000000998 | 2017-01-02 23:29:25 | hello world | PERSON_0000000829 | David Hernandez     | Margaret Anderson  |
| 23999 | PERSON_0000000999 | 2017-01-02 23:12:24 | hello world | PERSON_0000000944 | Tamara Ramirez      | Andrew Curtis      |
+-------+-------------------+---------------------+-------------+-------------------+---------------------+--------------------+

Tipp: Den gesamten Code siehst du hier

Du sagst das immer

Zweck

Wir machen weiter und erzeugen statt des langweiligen hello world eine zufällige Satz-Nachricht im Feld MESSAGE. Damit zeigen wir eine erste einfache Nutzung von Trumanias Relationship.

Wir ordnen jeder Person 4 Zitate zu. Jedes Mal, wenn eine Person eine Nachricht sendet, wird der Inhalt aus ihren 4 Zitaten ausgewählt. Außerdem vergeben wir je Zitat ein weight, das die Wahrscheinlichkeit steuert: Ein hohes Gewicht führt dazu, dass dieses Zitat häufiger gewählt wird.

Wie legst du los?

So geht’s

Zuerst erstellen wir einen Generator für Zitate, ähnlich den oben genutzten Faker-Generatoren:

quote_generator = FakerGenerator(method="sentence", nb_words=6, variable_nb_words=True,
                                 seed=next(example_circus.seeder))

Den Generator könntest du direkt in der story verwenden, spannender wird es, wenn jede Person stets eines ihrer 4 Lieblingszitate nutzt. Dafür erstellen wir eine Relationship zwischen Mitgliedern der Population person und Satzwerten.

Erst erzeugen wir eine leere Relationship:

quotes_rel = example_circus.populations["person"].create_relationship("quotes")

Dann befüllen wir diese Beziehung mit je 4 Zitaten pro Person. person.ids ist eine pandas-Series der Größe 1000 mit allen IDs, quote_generator.generate(size=person.size) liefert eine weitere Series mit zufälligen Zitaten.

Jeder Durchlauf der folgenden For-Schleife fügt 1000 Relationen hinzu – ein Zitat pro Nutzer:in. Die ersten 1000 Zitate erhalten weight=1, die nächsten weight=2 usw.

for w in range(4):
    quotes_rel.add_relations(
        from_ids=person.ids,
        to_ids=quote_generator.generate(size=person.size),
        weights=w
    )

Jetzt ersetzen wir den ConstantGenerator in der Story durch eine select_one-Operation auf dieser Relationship. Das bedeutet: Für jeden Wert in PERSON_ID suche die Beziehungen in quotes, die von dieser ID ausgehen, und wähle zufällig einen verknüpften Wert. Ohne Override gelten die oben gesetzten Gewichte aus add_relations:

example_circus.set_operations(
    example_circus.clock.ops.timestamp(named_as="TIME"),

    example_circus.populations["person"].get_relationship("quotes")
        .ops.select_one(from_field="PERSON_ID", named_as="MESSAGE"),

    example_circus.populations["person"].ops.select_one(named_as="OTHER_PERSON"),

    example_circus.populations["person"]
        .ops.lookup(id_field="PERSON_ID", select={"NAME": "EMITTER_NAME"}),

    example_circus.populations["person"]
        .ops.lookup(id_field="OTHER_PERSON", select={"NAME": "RECEIVER_NAME"}),

    FieldLogger(log_id="hello")
)

Ergebnis

Beim nächsten Lauf enthält MESSAGE nun das zufällige Zitat:

+-------+-------------------+---------------------+-----------------------------------------------------+-------------------+---------------------+---------------------+
|       | PERSON_ID         | TIME                | MESSAGE                                             | OTHER_PERSON      | EMITTER_NAME        | RECEIVER_NAME       |
|-------+-------------------+---------------------+-----------------------------------------------------+-------------------+---------------------+---------------------|
|  0    | PERSON_0000000000 | 2017-01-01 01:14:12 | Become period risk wait now toward less.            | PERSON_0000000158 | Ann Cruz            | Victoria Washington |
|  1    | PERSON_0000000001 | 2017-01-01 01:23:51 | Month push down.                                    | PERSON_0000000366 | Kimberly Sanchez    | Steven Williams     |
|  2    | PERSON_0000000002 | 2017-01-01 01:37:11 | Blue general carry else deep problem area.          | PERSON_0000000613 | Bethany Smith       | Frances Davis       |
|  3    | PERSON_0000000003 | 2017-01-01 01:33:12 | Phone sister pretty suddenly allow conference.      | PERSON_0000000086 | Frank Middleton     | Ashley Fernandez    |
|  4    | PERSON_0000000004 | 2017-01-01 01:08:02 | Each discussion several send wide process.          | PERSON_0000000972 | Cheryl Decker       | Latoya Flynn        |
|  5    | PERSON_0000000005 | 2017-01-01 01:13:36 | Guess can issue writer.                             | PERSON_0000000030 | Thomas Rodriguez    | Lindsay Bailey      |
|  6    | PERSON_0000000006 | 2017-01-01 01:35:08 | Boy step claim camera common but our.               | PERSON_0000000548 | James Peters        | Thomas Ward         |

| ...                                                                                                                                                                   | 23993 | PERSON_0000000993 | 2017-01-02 23:25:16 | May course and kill from news.                      | PERSON_0000000202 | Morgan Rice         | Patricia Williams   |
| 23994 | PERSON_0000000994 | 2017-01-02 23:33:55 | Water door live hospital together safe.             | PERSON_0000000268 | Crystal Vincent     | Victoria Donovan    |
| 23995 | PERSON_0000000995 | 2017-01-02 23:41:45 | Stage control management read although thousand.    | PERSON_0000000466 | Dawn Kim            | Ashley Baxter       |
| 23996 | PERSON_0000000996 | 2017-01-02 23:39:23 | Their that region majority break article.           | PERSON_0000000620 | Jimmy Franco        | Alex Dominguez      |
| 23997 | PERSON_0000000997 | 2017-01-02 23:45:53 | Lead simple audience response eye.                  | PERSON_0000000319 | Christian Christian | David Smith         |
| 23998 | PERSON_0000000998 | 2017-01-02 23:29:25 | Vote man son yeah child.                            | PERSON_0000000487 | David Hernandez     | Joshua Park         |
| 23999 | PERSON_0000000999 | 2017-01-02 23:12:24 | Answer region wind condition someone bed cover.     | PERSON_0000000237 | Tamara Ramirez      | Beverly Rodriguez   |
+-------+-------------------+---------------------+-----------------------------------------------------+-------------------+---------------------+---------------------+

Dank der Relationship sind Felder im Datensatz nun verknüpft: Bestimmte Sätze werden von bestimmten Nutzer:innen häufiger verwendet.

Tipp: Den gesamten Code findest du auf Github

Nicht was du tust, sondern wann du es tust

Zweck

Bislang produziert jedes Mitglied der person-Population genau jede Stunde eine Nachricht. Machen wir das realistischer.

Trumania steuert die zeitlichen Aspekte einer Story über zwei Konzepte: Aktivitätslevel und Timer-Profile. Zusammen bestimmen sie die Wahrscheinlichkeit, mit der die story zu einer bestimmten Tageszeit für eine bestimmte Person getriggert wird.

So geht’s

Wir aktualisieren die story und parametrisieren beide Aspekte.

Zuerst definieren wir das Timer-Profil der Story – es steuert, wann mehr oder weniger Aktionen ausgelöst werden. Dieses Profil gilt für alle Mitglieder der Population, die die Story triggern.

Trumania bringt einige vordefinierte Profile mit, z. B. DefaultDailyTimerGenerator, das der täglichen Telefonnutzung aus einem realen Produktivdatensatz entspricht:

Trumania Default Daily Timer Generator Visualization

Instanziieren wir es im Szenario:

from trumania.components.time_patterns.profilers import DefaultDailyTimerGenerator

story_timer_gen = DefaultDailyTimerGenerator(
    clock=example_circus.clock, 
    seed=next(example_circus.seeder))

Zweitens vergeben wir für die Story Aktivitäts-Level pro Mitglied – also wie aktiv jede Person in dieser Story ist. Du kannst jedem Mitglied einen anderen Level zuweisen, sodass manche häufiger, andere seltener triggern.

Statt manuell nutzen wir hier einen Zufallsgenerator, der jedem Mitglied einen Aktivitätslevel zuweist.

Zuerst definieren wir drei Level mit durchschnittlich 3, 10 und 20 Triggern pro Tag:

low_activity = story_timer_gen.activity(n=3, per=pd.Timedelta("1 day"))
med_activity = story_timer_gen.activity(n=10, per=pd.Timedelta("1 day"))
high_activity = story_timer_gen.activity(n=20, per=pd.Timedelta("1 day"))

Dann erstellen wir den eigentlichen Aktivitätsgenerator, erneut ein Numpy-Generator basierend auf choice. Unten: 20% erhalten „low“, 70% „medium“, 10% „high“:

activity_gen = NumpyRandomGenerator(
    method="choice", 
    a=[low_activity, med_activity, high_activity],
    p=[.2, .7, .1],
    seed=next(example_circus.seeder))

Jetzt nutzen wir Timer-Profil und Aktivitätsgenerator in der Story:

hello_world = example_circus.create_story(
    name="hello_world",
    initiating_population=example_circus.populations["person"],
    member_id_field="PERSON_ID",

    timer_gen=story_timer_gen,
    activity_gen=activity_gen
)

Ergebnis

Das Schema des Datensatzes bleibt wie zuvor:

+-------+-------------------+---------------------+---------------------------------------------------------+-------------------+---------------------+----------------------+
|       | PERSON_ID         | TIME                | MESSAGE                                                 | OTHER_PERSON      | EMITTER_NAME        | RECEIVER_NAME        |
|-------+-------------------+---------------------+---------------------------------------------------------+-------------------+---------------------+----------------------|
|  0    | PERSON_0000000016 | 2017-01-01 00:14:12 | Still try sex sure.                                     | PERSON_0000000739 | Johnny Moore        | Adam Barrett         |
|  1    | PERSON_0000000063 | 2017-01-01 00:23:51 | Resource magazine wide.                                 | PERSON_0000000511 | Cody Pham           | Tina Simmons         |
|  2    | PERSON_0000000064 | 2017-01-01 00:37:11 | Heat sure simple letter better forget.                  | PERSON_0000000044 | Katherine Fleming   | Angela Gray          |
|  3    | PERSON_0000000076 | 2017-01-01 00:33:12 | Star our conference always place ball.                  | PERSON_0000000959 | Benjamin Reese II   | Barbara Alexander    |
|  4    | PERSON_0000000088 | 2017-01-01 00:08:02 | Hot event five left become time commercial.             | PERSON_0000000000 | Zachary Cook        | Ann Cruz             |
| ...                                                                                                                                                                        |
| 28556 | PERSON_0000000962 | 2017-01-04 00:07:45 | Then include rock rule scientist condition.             | PERSON_0000000448 | Johnny Washington   | Melissa Adams        |
| 28557 | PERSON_0000000980 | 2017-01-04 00:04:44 | Collection full argue interview property pattern never. | PERSON_0000000731 | James Ramirez       | David Morrow         |
| 28558 | PERSON_0000000983 | 2017-01-04 00:23:21 | High day prepare see.                                   | PERSON_0000000509 | Evan Horne          | Robert Sims          |
| 28559 | PERSON_0000000988 | 2017-01-04 00:43:56 | Likely south school result case federal seat.           | PERSON_0000000819 | Benjamin Campbell   | Michelle Jackson DDS |
+-------+-------------------+---------------------+---------------------------------------------------------+-------------------+---------------------+----------------------+

Tipp: Sieh dir das Snippet auf Github an

Analyse der Ergebnisse

Die von dir konfigurierten Zeitmuster sollten sich in der Struktur des Datensatzes widerspiegeln. Das zeigst du mit einer einfachen Analyse des resulting_dataset:

  • Die Anzahl der Nachrichten pro Stunde sollte der gewählten Verteilung folgen (ähnlich der Kurve des DefaultDailyTimerGenerator oben):
time_profile = (
    resulting_dataset[["MESSAGE", "TIME"]]
    .groupby(by=example_5_df.TIME.dt.hour)["MESSAGE"]
    .count()
)
time_profile.plot()

Trumania Default Daily Timer Generator Visualization

  • Auch das Histogramm der Nachrichtenanzahl pro Nutzer:in sollte deiner Konfiguration entsprechen:
    • low_activity mit durchschnittlich 3 Nachrichten pro Tag, med_activity mit 10 und high_activity mit 20, über 5 Tage simuliert. Das ergibt drei Gruppen mit ungefähr 15, 50 und 100 Nachrichten insgesamt – die drei Zentren im Histogramm unten.
    • Die Wahrscheinlichkeiten .2, .7 und .1 spiegeln sich in der jeweiligen Fläche der „Buckel“ wider:
usage_per_user = resulting_dataset[["MESSAGE", "PERSON_ID"]].groupby("PERSON_ID")["MESSAGE"].count()
usage_per_user.plot(kind="hist")

Histogram visualization & graph of message frequency per user

The Social Network

Zweck

Wir können das Szenario weiter verbessern, indem Personen ihren Freund:innen schreiben statt zufälligen Empfänger:innen. Dadurch entsteht mehr Struktur im Datensatz, denn der soziale Graph lässt sich aus dem Nachrichtenlog rekonstruieren.

So geht’s

Wir modellieren das soziale Netzwerk erneut als Relationship, wie schon bei den Zitaten. Erinnerst du dich: Die Relationship quotes hatte Gewichte, die die Wahrscheinlichkeit steuerten. Genauso verwenden wir Gewichte, um festzulegen, welche Freund:innen eher kontaktiert werden.

Hier definieren wir eine Relationship von person zu person. Grundsätzlich kannst du in Trumania Beziehungen zwischen beliebigen Populationen anlegen.

Es gibt viele Wege, Kanten in einem sozialen Graphen zu erzeugen. Ein klassischer Ansatz ist das Erdős–Rényi-Modell. In Trumania ist es bereits integriert – du musst es nur ins Szenario einbinden.

Wir definieren einen Graphen, in dem jede Person 20 Freund:innen hat. Die Relationship erhält automatisch Gewichte mit einer Pareto-Verteilung.

from trumania.components.social_networks.erdos_renyi import WithErdosRenyi

# self is the circus here, see example code on github for details
self.add_er_social_network_relationship(
    self.populations["person"],
    relationship_name="friends",
    average_degree=20)

Mit dieser Relationship passen wir die Story erneut an. OTHER_PERSON wird nicht mehr zufällig aus der gesamten Population gewählt, sondern aus den Freund:innen jeder person – mit Wahrscheinlichkeit proportional zum Gewicht:

hello_world.set_operations(
    self.clock.ops.timestamp(named_as="TIME"),

    self.populations["person"].get_relationship("quotes")
        .ops.select_one(from_field="PERSON_ID",named_as="MESSAGE"),

    self.populations["person"]
        .get_relationship("friends")
        .ops.select_one(from_field="PERSON_ID", named_as="OTHER_PERSON"),

    self.populations["person"]
        .ops.lookup(id_field="PERSON_ID", select={"NAME": "EMITTER_NAME"}),

    self.populations["person"]
        .ops.lookup(id_field="OTHER_PERSON", select={"NAME": "RECEIVER_NAME"}),

    operations.FieldLogger(log_id="hello_6")
)

Ergebnis

Der resultierende Datensatz hat wieder dasselbe Schema – durch das soziale Netzwerk nun mit noch mehr Struktur:

+-------+-------------------+---------------------+----------------------------------------------------+-------------------+-------------------+------------------+
|       | PERSON_ID         | TIME                | MESSAGE                                            | OTHER_PERSON      | EMITTER_NAME      | RECEIVER_NAME    |
|-------+-------------------+---------------------+----------------------------------------------------+-------------------+-------------------+------------------|
|  0    | PERSON_0000000004 | 2017-01-01 00:14:12 | Clearly see sure.                                  | PERSON_0000000321 | Cheryl Decker     | Ian White        |
|  1    | PERSON_0000000012 | 2017-01-01 00:23:51 | Offer or interview clear structure watch capital.  | PERSON_0000000697 | Kelly Green       | Ashley Turner    |
|  2    | PERSON_0000000018 | 2017-01-01 00:37:11 | Recently race draw thousand around ahead.          | PERSON_0000000989 | Laura Stephenson  | Hannah James     |
|  3    | PERSON_0000000040 | 2017-01-01 00:33:12 | To protect man image power beyond.                 | PERSON_0000000418 | Jeffrey Miller    | Scott Collins    |
...                                                                               |
| 28966 | PERSON_0000000985 | 2017-01-04 00:09:23 | However agreement fear door land hotel.            | PERSON_0000000211 | Laura Mason       | Joshua Miller    |
| 28967 | PERSON_0000000995 | 2017-01-04 00:48:08 | Top heat window quite forward friend somebody.     | PERSON_0000000171 | Dawn Kim          | Sandra Phillips  |
| 28968 | PERSON_0000000999 | 2017-01-04 00:58:46 | Answer region wind condition someone bed cover.    | PERSON_0000000252 | Tamara Ramirez    | Jordan Collins   |
+-------+-------------------+---------------------+----------------------------------------------------+-------------------+-------------------+------------------+

Tipp: Schau dir das Snippet auf Github an

Analyse der Ergebnisse

Ein grober Blick auf das soziale Netzwerk: Zähle die Anzahl einzigartiger Freund:innen, die jede Person kontaktiert. Läuft die Simulation lang genug, erhältst du eine Untergrenze für die tatsächliche Größe jedes sozialen Umfelds (aufgrund der Pareto-Gewichte sind manche Freund:innen sehr unwahrscheinlich; du brauchst lange Läufe, um alle zu beobachten).

Wir haben 15 simulierte Tage ausgeführt und Folgendes erhalten – zentriert nahe und etwas unter dem durchschnittlichen Grad 20 des Graphen:

social_group_size = (
    resulting_dataset[["PERSON_ID", "OTHER_PERSON"]]
        .groupby("PERSON_ID")
        .agg(lambda friends_ids: len(friends_ids.unique()))
    )
social_group_size.plot(kind="hist")

Histogram of observed social group size

Da geht noch mehr!

Weitere Trumania-Features, die wir hier nicht behandelt haben:

  • Mehrere Populationen in einem Szenario
  • Mehrere Stories in einem Szenario
  • Probabilistische Abhängigkeiten zwischen Stories (z. B. Rückruf nach eingehendem Anruf)
  • Status-Updates (z. B. Guthabenabbuchung basierend auf Anrufdauer)
  • Wiederverwendbare Bausteine wie Geografien, zusätzliche Timer-Profile ...
  • Persistenz von Szenarioteilen

Fazit

Wir haben Trumania als szenariobasierte Bibliothek zur Datengenerierung in Python vorgestellt. Die erzeugten Datensätze lassen sich vielseitig einsetzen – für Tests, Lernen und Benchmarking.

Um Anwendungen oder Algorithmen sinnvoll zu testen, brauchen wir Datensätze mit erwartbaren statistischen Eigenschaften. Wir haben gezeigt, dass Trumania das kann – mit einem einfachen „Message-Log“-Datensatz, der eine Verteilung der Nachrichten pro Nutzer:in, eine Tageszeitverteilung und eine Verteilung über einen sozialen Graphen respektiert.

Außerdem haben wir gezeigt, wie flexibel Trumanias Komponenten sind und wie damit unterschiedlichste Szenarien entstehen.

Ein Trumania-Szenario kann Beziehungen zwischen Entitäten enthalten und mit theoretischen Zufallsverteilungen, empirischen Verteilungen aus echten Datensätzen oder sogar mit Produktivdaten (z. B. Standort-IDs) konfiguriert werden. Wir haben etwa eine empirische Verteilung von Telefonzeitstempeln genutzt, um den Random-Timer unserer Story zu konfigurieren.

In diesem Sinne: Leg los! Schau auf Trumanias Github vorbei und schreib dein eigenes Szenario ;)

Du kannst dich auch gern an Svend Vanderveken oder Co-Autor Milan van der Meer wenden:

  • Svend Vanderveken ist freiberuflicher Software Engineer mit Fokus auf Datenverarbeitung – vor allem Kafka, Scala, SQL, Python ... Folge ihm auf seinem Blog svend.kelesia.com und auf Twitter @sv3ndk.
  • Milan van der Meer arbeitet als Software Engineer bei Real Impact Analytics. Vernetze dich mit ihm auf LinkedIn.

Hinweis der Redaktion: Dieser Beitrag ist in Zusammenarbeit mit Milan van der Meer entstanden. Beide Autor:innen dieses Beitrags sind Teil des Teams von Real Impact Analytics, einem innovativen belgischen Big-Data-Startup, das den Wert von Telekomdaten hebt, indem es „Big Data appifiziert“.

Dieses Tutorial gibt dir einen ersten Eindruck, warum du zufällige Datensätze generieren solltest und was du davon erwarten kannst. Außerdem zeigt es dir anhand erster Beispiele, wie du Trumania, eine Python-Bibliothek zur Datengenerierung, einsetzt.

Mehr Infos findest du auf Trumanias GitHub!

Warum zufällige Datensätze generieren?

Das Erzeugen zufälliger Datensätze ist für Data Engineers und Data Scientists gleichermaßen relevant. Warum arbeiten Data Scientists und Data Engineers mit synthetischen Daten – und wie kommen sie daran?

Als Data Engineer möchtest du nach dem Schreiben deiner neuen, großartigen Datenverarbeitungsanwendung End-to-End testen – dafür brauchst du Eingabedaten.

Als Data Scientist profitierst du von Datengenerierung, weil du damit verschiedene Ansätze zur Exploration von Datensätzen, Algorithmen, Visualisierungstechniken ausprobieren oder Annahmen über das Verhalten einer Methode gegen viele unterschiedliche Datensätze deiner Wahl validieren kannst.

In beiden Fällen liegt es nahe, einfach echte Daten zu verwenden. Ein kleines Problem: Produktivdaten sind in der Regel schwer zu bekommen – und mit neuen europäischen Vorgaben zu Datenschutz und Sicherheit wird es nicht einfacher.

Vielleicht gehörst du zu den Glücklichen und hast einen schönen, vollständigen Produktivdatensatz auf deinem Laptop. Selbst dann möchtest du oft mehr Varianz im Datensatz, um verschiedene Situationen zu prüfen: unterschiedliche Populationsgrößen, verschiedene Verteilungen, typische Datenqualitätsprobleme wie fehlende oder ungültige Werte ...

Eine ausführlichere Begründung, warum das Generieren zufälliger Datensätze sinnvoll ist, findest du hier: „Why synthetic data is about to become a major competitive advantage“.

Schema-basierte Zufallsdatengenerierung: Beziehungen zählen!

Dieser Abschnitt veranschaulicht die schema-basierte Datengenerierung und ihre Schwächen.

Es gibt bereits viele Tools, um Zufallsdatensätze zu erzeugen. Ein gängiger Ansatz ist die schema-basierte Generierung: Du definierst einen Bauplan und erzeugst damit Entitäten. Khermes und LogSynth sind zwei Beispiele.

Ein Beispiel für eine solche Konfiguration könnte ein Personenschema beinhalten:

{
  {
    "field": "Name",
    "class": NameGenerator
  },
  {
    "field": "Age",
    "class": RandomInt(18, 65)
  },
  {
    "field": "Gender",
    "class": RandomPicker(["Male", "Female", "Other"])
  } 
}

Dieses Schema definiert Daten zu einer Person: Name, Alter und Geschlecht. Schnell und simpel – aber mit Einschränkungen. Ein Hauptproblem: Beziehungen zwischen Entitäten oder zeitliche Abhängigkeiten realistisch abzubilden.

Wie machst du etwa bestimmte Namen wahrscheinlicher abhängig vom Geschlecht? Oder wie erzeugst du ein Log von Nutzeraktionen, in dem Aktionen am Wochenende häufiger auftreten?

Die schema-basierte Vorgehensweise erschwert es, deine Anwendung mit realistischen Besonderheiten zu konfrontieren, z. B.:

  • Unausgewogene Datensätze (schwierig für Group-by-Operationen);
  • Geclusterte Muster in Entitätsnetzwerken, z. B. Erkennen sozialer Gruppen über Interaktionen (etwa Telefonate);
  • Unterschiedliche Aktivitätsprofile über die Zeit. Aktionen passieren zu bestimmten Tages- oder Wochenzeiten häufiger;
  • Kausale Abhängigkeiten zwischen Aktionen, z. B. viele Käufe in einem Laden, die letztlich ein „Out-of-stock“-Ereignis auslösen.

Trumania setzt auf Szenarien, um diese Lücken zu schließen und realistischere Datensätze zu erzeugen. Während ein Szenario abläuft, interagieren verschiedene Populationen miteinander, aktualisieren ihre Eigenschaften und erzeugen Logs.

Ein Beispiel: Menschen rufen ihre Freund:innen an. Für Anrufe werden Minuten verbraucht, sind die Minuten aufgebraucht, geht es in den Shop ... Der resultierende Datensatz könnte ein Log der Anrufe, der Shop-Besuche und der Lagerbestände pro Stunde sein. Aktionen können zufällig oder deterministisch sein.

In Trumania sind die erzeugten Datensätze typischerweise Zeitreihen, weil sie aus dem Ablauf eines Szenarios über die Zeit entstehen.

Im folgenden Beispiel baust du ein einfaches Szenario, in dem Personen einander Nachrichten schicken. Das Ergebnis ist eine Zeitreihe der zwischen Personen ausgetauschten Nachrichten.

Die Schritte sind:

  • Erstellen eines Trumania-Circus – der Welt, in der das Szenario läuft
  • Hinzufügen einer „Hello World“-Story, die einfache, fest codierte Logs erzeugt
  • Hinzufügen einer Relationship, um jeder Person Lieblingszitate zuzuordnen, die als Nachrichteninhalt dienen
  • Parametrisieren der Zeitkomponenten, um eine realistischere Verteilung der Nachrichten über den Tag zu erzielen
  • Hinzufügen einer weiteren Relationship, die ein soziales Netzwerk definiert, damit der aus dem Nachrichtenlog entstehende Graph realistischer wird

Einen Trumania Circus erstellen

Zweck

Als Erstes erstellst du einen Circus – die Welt, in der alle Elemente des Szenarios leben. Außerdem legen wir eine Grundpopulation von persons an. Da vieles in Trumania zufällig ist, lernst du hier auch das Konzept der generators kennen, die zufälliges Verhalten steuern.

Wir fügen noch keine Story hinzu, alles bleibt zunächst statisch.

Los geht’s!

So geht’s

Ein Trumania-Circus wird so erstellt:

from trumania.core import circus

example_circus = circus.Circus(name="example1", 
                               master_seed=12345,
                               start=pd.Timestamp("1 Jan 2017 00:00"),
                               step_duration=pd.Timedelta("1h"))

In Trumania steuert eine zentrale Uhr alle zeitbezogenen Elemente. Wichtig im obigen Snippet ist step_duration=pd.Timedelta("1h") – die Uhr tickt also in 1-Stunden-Schritten.

Als Nächstes fügst du die Population person hinzu. Eine Population ist im Kern eine Menge von Agenten mit einer id und Attributen.

Du kannst Attributwerte manuell festlegen, meist willst du sie aber zufällig generieren. Definiere dafür einige generators:

from trumania.core.random_generators import SequencialGenerator, FakerGenerator, NumpyRandomGenerator

id_gen = SequencialGenerator(prefix="PERSON_")
age_gen = NumpyRandomGenerator(method="normal", loc=3, scale=5,
                               seed=next(example_circus.seeder))
name_gen = FakerGenerator(method="name", seed=next(example_circus.seeder))

Ein Trumania-Generator liefert bei generate() Daten. id_gen erzeugt Strings wie PERSON_0001, PERSON_0002 ... age_gen sampelt wiederholt aus einer Normalverteilung, name_gen liefert zufällige Namen.

Hinweis: Jede statistische Verteilung aus numpy sowie jeder Faker-Provider ist in Trumania verfügbar und kann leicht erweitert werden.

Jetzt fügst du 1000 Personen mit fortlaufenden IDs und zwei zufälligen Attributen hinzu:

person = example_circus.create_population(name="person", size=1000, ids_gen=id_gen)
person.create_attribute("NAME", init_gen=name_gen)
person.create_attribute("AGE", init_gen=age_gen)

Ergebnis

Per person.to_dataframe() kannst du dir die generierten Attribute aller person-Mitglieder ansehen.

+-------------------+-----------------+---------+
|                   | NAME            |     age |
|-------------------+-----------------+---------|
| PERSON_0000000000 | Amy Berger      | 28.588  |
| PERSON_0000000001 | Michael Curry   | 28.7499 |
| PERSON_0000000002 | Robert Ramirez  | 35.9242 |
| PERSON_0000000003 | Derek Gonzalez  | 34.7091 |
| PERSON_0000000004 | Gregory Fischer | 25.6009 |
| PERSON_0000000005 | Erica Walker    | 33.9758 |
| PERSON_0000000006 | Bradley Collins | 24.4428 |
| PERSON_0000000007 | James Rodriguez | 34.7835 |
| PERSON_0000000008 | Brandy Padilla  | 34.6296 |
| PERSON_0000000009 | Mark Taylor     | 38.2286 |
+-------------------+-----------------+---------+

Tipp: Den kompletten Code für diese Grundpopulation findest du in diesem Snippet auf Github!

Hello-World-Statements: Stories erstellen

Zweck

Machen wir es spannender und fügen eine story hinzu. Eine Trumania-Story kapselt dynamisches Verhalten. Sie wird jedes Mal ausgeführt, wenn die simulierte Uhr einen Schritt weiterschaltet.

Wir haben oben step_duration=pd.Timedelta("1h") gesetzt – jede logische Stunde kann also jede Story ausgeführt werden.

Wir bauen eine simple Hello-World-Story, in der alle Mitglieder der Population person regelmäßig die Nachricht „hello world“ senden.

So geht’s

Beim Erstellen einer story legst du zuerst die initiierende Population fest – hier person. Zweitens definierst du den timer, der festlegt, wann jedes Mitglied die Aktion triggert. Zunächst setzen wir ihn hart auf 1 – damit führt jedes Mitglied die Story bei jedem Zeitschritt aus (später bauen wir ausgefeiltere Timer):

hello_world = example_circus.create_story(
    name="hello_world",
    initiating_population=example_circus.populations["person"],
    member_id_field="PERSON_ID",
    timer_gen=ConstantDependentGenerator(value=1)
)

Noch macht die Story nichts – wir müssen Operationen hinzufügen. Diese können zufällig oder deterministisch sein, Attribute lesen/schreiben oder Seiteneffekte haben.

Wir starten mit zwei deterministischen Operationen plus Logger:

hello_world.set_operations(
    example_circus.clock.ops.timestamp(named_as="TIME"),
    ConstantGenerator(value="hello world").ops.generate(named_as="MESSAGE"),
    FieldLogger(log_id="hello")
)

example_circus.clock.ops.timestamp erzeugt einen zufälligen Zeitstempel innerhalb des aktuellen 1-Stunden-Intervalls, ConstantGenerator liefert konstante Werte.

Ergebnis

Lassen wir den Circus 48 Stunden simuliert laufen:

example_circus.run(
    duration=pd.Timedelta("48h"),
    log_output_folder="example_scenario",
    delete_existing_logs=True
)

Das ergibt einen Datensatz, in dem jede der 1000 Personen über 2 Tage hinweg 48-mal „hello world“ gesagt hat.

Das erste Feld PERSON_ID entspricht member_id_field der story. TIME und MESSAGE stammen aus den beiden Operationen.

+-------+-------------------+---------------------+-------------+
|       | PERSON_ID         | TIME                | MESSAGE     |
|-------+-------------------+---------------------+-------------|
|  0    | PERSON_0000000000 | 2017-01-01 01:14:12 | hello world |
|  1    | PERSON_0000000001 | 2017-01-01 01:23:51 | hello world |
|  2    | PERSON_0000000002 | 2017-01-01 01:37:11 | hello world |
|  3    | PERSON_0000000003 | 2017-01-01 01:33:12 | hello world |
|  4    | PERSON_0000000004 | 2017-01-01 01:08:02 | hello world |
|  5    | PERSON_0000000005 | 2017-01-01 01:13:36 | hello world |
|  6    | PERSON_0000000006 | 2017-01-01 01:35:08 | hello world |
|  7    | PERSON_0000000007 | 2017-01-01 01:24:30 | hello world |
|  8    | PERSON_0000000008 | 2017-01-01 01:01:51 | hello world |
|  9    | PERSON_0000000009 | 2017-01-01 01:41:41 | hello world |
| ...                                                           |
| 23990 | PERSON_0000000990 | 2017-01-02 23:33:28 | hello world |
| 23991 | PERSON_0000000991 | 2017-01-02 23:15:26 | hello world |
| 23992 | PERSON_0000000992 | 2017-01-02 23:45:56 | hello world |
| 23993 | PERSON_0000000993 | 2017-01-02 23:25:16 | hello world |
| 23994 | PERSON_0000000994 | 2017-01-02 23:33:55 | hello world |
| 23995 | PERSON_0000000995 | 2017-01-02 23:41:45 | hello world |
| 23996 | PERSON_0000000996 | 2017-01-02 23:39:23 | hello world |
| 23997 | PERSON_0000000997 | 2017-01-02 23:45:53 | hello world |
| 23998 | PERSON_0000000998 | 2017-01-02 23:29:25 | hello world |
| 23999 | PERSON_0000000999 | 2017-01-02 23:12:24 | hello world |
+-------+-------------------+---------------------+-------------+

Tipp: Den kompletten Code findest du auf Github

Jemand, dem man Hello World sagt

Zweck

Wir erweitern das Beispiel und fügen eine zufällige weitere Person als Gesprächspartner:in hinzu. Vorerst kann jede Person mit gleicher Wahrscheinlichkeit mit jeder anderen sprechen – nicht sehr realistisch (ein soziales Netzwerk folgt später). Außerdem reichern wir den Datensatz um Namen basierend auf der ID an.

So geht’s

Die andere Person speichern wir im neuen Feld OTHER_PERSON. In dieser einfachen Variante wählen wir ungewichtet zufällig aus der Population person. Dafür nutzen wir die Operation select_one dieser Population:

example_circus.set_operations(
    example_circus.clock.ops.timestamp(named_as="TIME"),
    ConstantGenerator(value="hello world").ops.generate(named_as="MESSAGE"),

    example_circus.populations["person"].ops.select_one(named_as="OTHER_PERSON"),

    example_circus.populations["person"]
        .ops.lookup(id_field="PERSON_ID", select={"NAME": "EMITTER_NAME"}),

    example_circus.populations["person"]
        .ops.lookup(id_field="OTHER_PERSON", select={"NAME": "RECEIVER_NAME"}),

    FieldLogger(log_id="hello")
)

Ergebnis

Nach erneutem Lauf enthält der Datensatz das neue Feld OTHER_PERSON:

+-------+-------------------+---------------------+-------------+-------------------+---------------------+--------------------+
|       | PERSON_ID         | TIME                | MESSAGE     | OTHER_PERSON      | EMITTER_NAME        | RECEIVER_NAME      |
|-------+-------------------+---------------------+-------------+-------------------+---------------------+--------------------|
|  0    | PERSON_0000000000 | 2017-01-01 01:14:12 | hello world | PERSON_0000000852 | Ann Cruz            | Sophia Black       |
|  1    | PERSON_0000000001 | 2017-01-01 01:23:51 | hello world | PERSON_0000000429 | Kimberly Sanchez    | Jeffrey Ryan       |
|  2    | PERSON_0000000002 | 2017-01-01 01:37:11 | hello world | PERSON_0000000925 | Bethany Smith       | Regina Brown       |
|  3    | PERSON_0000000003 | 2017-01-01 01:33:12 | hello world | PERSON_0000000347 | Frank Middleton     | Jacob Ross         |
|  4    | PERSON_0000000004 | 2017-01-01 01:08:02 | hello world | PERSON_0000000211 | Cheryl Decker       | Joshua Miller      |
|  5    | PERSON_0000000005 | 2017-01-01 01:13:36 | hello world | PERSON_0000000779 | Thomas Rodriguez    | Nicole Tanner      |
|  6    | PERSON_0000000006 | 2017-01-01 01:35:08 | hello world | PERSON_0000000331 | James Peters        | Melissa Rogers     |
|  7    | PERSON_0000000007 | 2017-01-01 01:24:30 | hello world | PERSON_0000000234 | Allison Hansen      | Taylor Smith       |
|  8    | PERSON_0000000008 | 2017-01-01 01:01:51 | hello world | PERSON_0000000678 | Candice Sellers     | James Smith        |
|  9    | PERSON_0000000009 | 2017-01-01 01:41:41 | hello world | PERSON_0000000108 | Maria White         | James Nguyen       |
| ...                                                                                                                          | 
| 23990 | PERSON_0000000990 | 2017-01-02 23:33:28 | hello world | PERSON_0000000689 | Natasha Brown       | Bailey Ramirez DDS |
| 23991 | PERSON_0000000991 | 2017-01-02 23:15:26 | hello world | PERSON_0000000250 | Shelly Ponce        | Jordan Johnson     |
| 23992 | PERSON_0000000992 | 2017-01-02 23:45:56 | hello world | PERSON_0000000413 | Steven Mendez       | Crystal Duffy      |
| 23993 | PERSON_0000000993 | 2017-01-02 23:25:16 | hello world | PERSON_0000000670 | Morgan Rice         | Jonathan Obrien    |
| 23994 | PERSON_0000000994 | 2017-01-02 23:33:55 | hello world | PERSON_0000000411 | Crystal Vincent     | George Mathis      |
| 23995 | PERSON_0000000995 | 2017-01-02 23:41:45 | hello world | PERSON_0000000563 | Dawn Kim            | Jennifer Martinez  |
| 23996 | PERSON_0000000996 | 2017-01-02 23:39:23 | hello world | PERSON_0000000668 | Jimmy Franco        | Dr. Mark Ruiz MD   |
| 23997 | PERSON_0000000997 | 2017-01-02 23:45:53 | hello world | PERSON_0000000268 | Christian Christian | Victoria Donovan   |
| 23998 | PERSON_0000000998 | 2017-01-02 23:29:25 | hello world | PERSON_0000000829 | David Hernandez     | Margaret Anderson  |
| 23999 | PERSON_0000000999 | 2017-01-02 23:12:24 | hello world | PERSON_0000000944 | Tamara Ramirez      | Andrew Curtis      |
+-------+-------------------+---------------------+-------------+-------------------+---------------------+--------------------+

Tipp: Den gesamten Code siehst du hier

Du sagst das immer

Zweck

Wir machen weiter und erzeugen statt des langweiligen hello world eine zufällige Satz-Nachricht im Feld MESSAGE. Damit zeigen wir eine erste einfache Nutzung von Trumanias Relationship.

Wir ordnen jeder Person 4 Zitate zu. Jedes Mal, wenn eine Person eine Nachricht sendet, wird der Inhalt aus ihren 4 Zitaten ausgewählt. Außerdem vergeben wir je Zitat ein weight, das die Wahrscheinlichkeit steuert: Ein hohes Gewicht führt dazu, dass dieses Zitat häufiger gewählt wird.

Wie legst du los?

So geht’s

Zuerst erstellen wir einen Generator für Zitate, ähnlich den oben genutzten Faker-Generatoren:

quote_generator = FakerGenerator(method="sentence", nb_words=6, variable_nb_words=True,
                                 seed=next(example_circus.seeder))

Den Generator könntest du direkt in der story verwenden, spannender wird es, wenn jede Person stets eines ihrer 4 Lieblingszitate nutzt. Dafür erstellen wir eine Relationship zwischen Mitgliedern der Population person und Satzwerten.

Erst erzeugen wir eine leere Relationship:

quotes_rel = example_circus.populations["person"].create_relationship("quotes")

Dann befüllen wir diese Beziehung mit je 4 Zitaten pro Person. person.ids ist eine pandas-Series der Größe 1000 mit allen IDs, quote_generator.generate(size=person.size) liefert eine weitere Series mit zufälligen Zitaten.

Jeder Durchlauf der folgenden For-Schleife fügt 1000 Relationen hinzu – ein Zitat pro Nutzer:in. Die ersten 1000 Zitate erhalten weight=1, die nächsten weight=2 usw.

for w in range(4):
    quotes_rel.add_relations(
        from_ids=person.ids,
        to_ids=quote_generator.generate(size=person.size),
        weights=w
    )

Jetzt ersetzen wir den ConstantGenerator in der Story durch eine select_one-Operation auf dieser Relationship. Das bedeutet: Für jeden Wert in PERSON_ID suche die Beziehungen in quotes, die von dieser ID ausgehen, und wähle zufällig einen verknüpften Wert. Ohne Override gelten die oben gesetzten Gewichte aus add_relations:

example_circus.set_operations(
    example_circus.clock.ops.timestamp(named_as="TIME"),

    example_circus.populations["person"].get_relationship("quotes")
        .ops.select_one(from_field="PERSON_ID", named_as="MESSAGE"),

    example_circus.populations["person"].ops.select_one(named_as="OTHER_PERSON"),

    example_circus.populations["person"]
        .ops.lookup(id_field="PERSON_ID", select={"NAME": "EMITTER_NAME"}),

    example_circus.populations["person"]
        .ops.lookup(id_field="OTHER_PERSON", select={"NAME": "RECEIVER_NAME"}),

    FieldLogger(log_id="hello")
)

Ergebnis

Beim nächsten Lauf enthält MESSAGE nun das zufällige Zitat:

+-------+-------------------+---------------------+-----------------------------------------------------+-------------------+---------------------+---------------------+
|       | PERSON_ID         | TIME                | MESSAGE                                             | OTHER_PERSON      | EMITTER_NAME        | RECEIVER_NAME       |
|-------+-------------------+---------------------+-----------------------------------------------------+-------------------+---------------------+---------------------|
|  0    | PERSON_0000000000 | 2017-01-01 01:14:12 | Become period risk wait now toward less.            | PERSON_0000000158 | Ann Cruz            | Victoria Washington |
|  1    | PERSON_0000000001 | 2017-01-01 01:23:51 | Month push down.                                    | PERSON_0000000366 | Kimberly Sanchez    | Steven Williams     |
|  2    | PERSON_0000000002 | 2017-01-01 01:37:11 | Blue general carry else deep problem area.          | PERSON_0000000613 | Bethany Smith       | Frances Davis       |
|  3    | PERSON_0000000003 | 2017-01-01 01:33:12 | Phone sister pretty suddenly allow conference.      | PERSON_0000000086 | Frank Middleton     | Ashley Fernandez    |
|  4    | PERSON_0000000004 | 2017-01-01 01:08:02 | Each discussion several send wide process.          | PERSON_0000000972 | Cheryl Decker       | Latoya Flynn        |
|  5    | PERSON_0000000005 | 2017-01-01 01:13:36 | Guess can issue writer.                             | PERSON_0000000030 | Thomas Rodriguez    | Lindsay Bailey      |
|  6    | PERSON_0000000006 | 2017-01-01 01:35:08 | Boy step claim camera common but our.               | PERSON_0000000548 | James Peters        | Thomas Ward         |

| ...                                                                                                                                                                   | 23993 | PERSON_0000000993 | 2017-01-02 23:25:16 | May course and kill from news.                      | PERSON_0000000202 | Morgan Rice         | Patricia Williams   |
| 23994 | PERSON_0000000994 | 2017-01-02 23:33:55 | Water door live hospital together safe.             | PERSON_0000000268 | Crystal Vincent     | Victoria Donovan    |
| 23995 | PERSON_0000000995 | 2017-01-02 23:41:45 | Stage control management read although thousand.    | PERSON_0000000466 | Dawn Kim            | Ashley Baxter       |
| 23996 | PERSON_0000000996 | 2017-01-02 23:39:23 | Their that region majority break article.           | PERSON_0000000620 | Jimmy Franco        | Alex Dominguez      |
| 23997 | PERSON_0000000997 | 2017-01-02 23:45:53 | Lead simple audience response eye.                  | PERSON_0000000319 | Christian Christian | David Smith         |
| 23998 | PERSON_0000000998 | 2017-01-02 23:29:25 | Vote man son yeah child.                            | PERSON_0000000487 | David Hernandez     | Joshua Park         |
| 23999 | PERSON_0000000999 | 2017-01-02 23:12:24 | Answer region wind condition someone bed cover.     | PERSON_0000000237 | Tamara Ramirez      | Beverly Rodriguez   |
+-------+-------------------+---------------------+-----------------------------------------------------+-------------------+---------------------+---------------------+

Dank der Relationship sind Felder im Datensatz nun verknüpft: Bestimmte Sätze werden von bestimmten Nutzer:innen häufiger verwendet.

Tipp: Den gesamten Code findest du auf Github

Nicht was du tust, sondern wann du es tust

Zweck

Bislang produziert jedes Mitglied der person-Population genau jede Stunde eine Nachricht. Machen wir das realistischer.

Trumania steuert die zeitlichen Aspekte einer Story über zwei Konzepte: Aktivitätslevel und Timer-Profile. Zusammen bestimmen sie die Wahrscheinlichkeit, mit der die story zu einer bestimmten Tageszeit für eine bestimmte Person getriggert wird.

So geht’s

Wir aktualisieren die story und parametrisieren beide Aspekte.

Zuerst definieren wir das Timer-Profil der Story – es steuert, wann mehr oder weniger Aktionen ausgelöst werden. Dieses Profil gilt für alle Mitglieder der Population, die die Story triggern.

Trumania bringt einige vordefinierte Profile mit, z. B. DefaultDailyTimerGenerator, das der täglichen Telefonnutzung aus einem realen Produktivdatensatz entspricht:

Trumania Default Daily Timer Generator Visualization

Instanziieren wir es im Szenario:

from trumania.components.time_patterns.profilers import DefaultDailyTimerGenerator

story_timer_gen = DefaultDailyTimerGenerator(
    clock=example_circus.clock, 
    seed=next(example_circus.seeder))

Zweitens vergeben wir für die Story Aktivitäts-Level pro Mitglied – also wie aktiv jede Person in dieser Story ist. Du kannst jedem Mitglied einen anderen Level zuweisen, sodass manche häufiger, andere seltener triggern.

Statt manuell nutzen wir hier einen Zufallsgenerator, der jedem Mitglied einen Aktivitätslevel zuweist.

Zuerst definieren wir drei Level mit durchschnittlich 3, 10 und 20 Triggern pro Tag:

low_activity = story_timer_gen.activity(n=3, per=pd.Timedelta("1 day"))
med_activity = story_timer_gen.activity(n=10, per=pd.Timedelta("1 day"))
high_activity = story_timer_gen.activity(n=20, per=pd.Timedelta("1 day"))

Dann erstellen wir den eigentlichen Aktivitätsgenerator, erneut ein Numpy-Generator basierend auf choice. Unten: 20% erhalten „low“, 70% „medium“, 10% „high“:

activity_gen = NumpyRandomGenerator(
    method="choice", 
    a=[low_activity, med_activity, high_activity],
    p=[.2, .7, .1],
    seed=next(example_circus.seeder))

Jetzt nutzen wir Timer-Profil und Aktivitätsgenerator in der Story:

hello_world = example_circus.create_story(
    name="hello_world",
    initiating_population=example_circus.populations["person"],
    member_id_field="PERSON_ID",

    timer_gen=story_timer_gen,
    activity_gen=activity_gen
)

Ergebnis

Das Schema des Datensatzes bleibt wie zuvor:

+-------+-------------------+---------------------+---------------------------------------------------------+-------------------+---------------------+----------------------+
|       | PERSON_ID         | TIME                | MESSAGE                                                 | OTHER_PERSON      | EMITTER_NAME        | RECEIVER_NAME        |
|-------+-------------------+---------------------+---------------------------------------------------------+-------------------+---------------------+----------------------|
|  0    | PERSON_0000000016 | 2017-01-01 00:14:12 | Still try sex sure.                                     | PERSON_0000000739 | Johnny Moore        | Adam Barrett         |
|  1    | PERSON_0000000063 | 2017-01-01 00:23:51 | Resource magazine wide.                                 | PERSON_0000000511 | Cody Pham           | Tina Simmons         |
|  2    | PERSON_0000000064 | 2017-01-01 00:37:11 | Heat sure simple letter better forget.                  | PERSON_0000000044 | Katherine Fleming   | Angela Gray          |
|  3    | PERSON_0000000076 | 2017-01-01 00:33:12 | Star our conference always place ball.                  | PERSON_0000000959 | Benjamin Reese II   | Barbara Alexander    |
|  4    | PERSON_0000000088 | 2017-01-01 00:08:02 | Hot event five left become time commercial.             | PERSON_0000000000 | Zachary Cook        | Ann Cruz             |
| ...                                                                                                                                                                        |
| 28556 | PERSON_0000000962 | 2017-01-04 00:07:45 | Then include rock rule scientist condition.             | PERSON_0000000448 | Johnny Washington   | Melissa Adams        |
| 28557 | PERSON_0000000980 | 2017-01-04 00:04:44 | Collection full argue interview property pattern never. | PERSON_0000000731 | James Ramirez       | David Morrow         |
| 28558 | PERSON_0000000983 | 2017-01-04 00:23:21 | High day prepare see.                                   | PERSON_0000000509 | Evan Horne          | Robert Sims          |
| 28559 | PERSON_0000000988 | 2017-01-04 00:43:56 | Likely south school result case federal seat.           | PERSON_0000000819 | Benjamin Campbell   | Michelle Jackson DDS |
+-------+-------------------+---------------------+---------------------------------------------------------+-------------------+---------------------+----------------------+

Tipp: Sieh dir das Snippet auf Github an

Analyse der Ergebnisse

Die von dir konfigurierten Zeitmuster sollten sich in der Struktur des Datensatzes widerspiegeln. Das zeigst du mit einer einfachen Analyse des resulting_dataset:

  • Die Anzahl der Nachrichten pro Stunde sollte der gewählten Verteilung folgen (ähnlich der Kurve des DefaultDailyTimerGenerator oben):
time_profile = (
    resulting_dataset[["MESSAGE", "TIME"]]
    .groupby(by=example_5_df.TIME.dt.hour)["MESSAGE"]
    .count()
)
time_profile.plot()

Trumania Default Daily Timer Generator Visualization

  • Auch das Histogramm der Nachrichtenanzahl pro Nutzer:in sollte deiner Konfiguration entsprechen:
    • low_activity mit durchschnittlich 3 Nachrichten pro Tag, med_activity mit 10 und high_activity mit 20, über 5 Tage simuliert. Das ergibt drei Gruppen mit ungefähr 15, 50 und 100 Nachrichten insgesamt – die drei Zentren im Histogramm unten.
    • Die Wahrscheinlichkeiten .2, .7 und .1 spiegeln sich in der jeweiligen Fläche der „Buckel“ wider:
usage_per_user = resulting_dataset[["MESSAGE", "PERSON_ID"]].groupby("PERSON_ID")["MESSAGE"].count()
usage_per_user.plot(kind="hist")

Histogram visualization & graph of message frequency per user

The Social Network

Zweck

Wir können das Szenario weiter verbessern, indem Personen ihren Freund:innen schreiben statt zufälligen Empfänger:innen. Dadurch entsteht mehr Struktur im Datensatz, denn der soziale Graph lässt sich aus dem Nachrichtenlog rekonstruieren.

So geht’s

Wir modellieren das soziale Netzwerk erneut als Relationship, wie schon bei den Zitaten. Erinnerst du dich: Die Relationship quotes hatte Gewichte, die die Wahrscheinlichkeit steuerten. Genauso verwenden wir Gewichte, um festzulegen, welche Freund:innen eher kontaktiert werden.

Hier definieren wir eine Relationship von person zu person. Grundsätzlich kannst du in Trumania Beziehungen zwischen beliebigen Populationen anlegen.

Es gibt viele Wege, Kanten in einem sozialen Graphen zu erzeugen. Ein klassischer Ansatz ist das Erdős–Rényi-Modell. In Trumania ist es bereits integriert – du musst es nur ins Szenario einbinden.

Wir definieren einen Graphen, in dem jede Person 20 Freund:innen hat. Die Relationship erhält automatisch Gewichte mit einer Pareto-Verteilung.

from trumania.components.social_networks.erdos_renyi import WithErdosRenyi

# self is the circus here, see example code on github for details
self.add_er_social_network_relationship(
    self.populations["person"],
    relationship_name="friends",
    average_degree=20)

Mit dieser Relationship passen wir die Story erneut an. OTHER_PERSON wird nicht mehr zufällig aus der gesamten Population gewählt, sondern aus den Freund:innen jeder person – mit Wahrscheinlichkeit proportional zum Gewicht:

hello_world.set_operations(
    self.clock.ops.timestamp(named_as="TIME"),

    self.populations["person"].get_relationship("quotes")
        .ops.select_one(from_field="PERSON_ID",named_as="MESSAGE"),

    self.populations["person"]
        .get_relationship("friends")
        .ops.select_one(from_field="PERSON_ID", named_as="OTHER_PERSON"),

    self.populations["person"]
        .ops.lookup(id_field="PERSON_ID", select={"NAME": "EMITTER_NAME"}),

    self.populations["person"]
        .ops.lookup(id_field="OTHER_PERSON", select={"NAME": "RECEIVER_NAME"}),

    operations.FieldLogger(log_id="hello_6")
)

Ergebnis

Der resultierende Datensatz hat wieder dasselbe Schema – durch das soziale Netzwerk nun mit noch mehr Struktur:

+-------+-------------------+---------------------+----------------------------------------------------+-------------------+-------------------+------------------+
|       | PERSON_ID         | TIME                | MESSAGE                                            | OTHER_PERSON      | EMITTER_NAME      | RECEIVER_NAME    |
|-------+-------------------+---------------------+----------------------------------------------------+-------------------+-------------------+------------------|
|  0    | PERSON_0000000004 | 2017-01-01 00:14:12 | Clearly see sure.                                  | PERSON_0000000321 | Cheryl Decker     | Ian White        |
|  1    | PERSON_0000000012 | 2017-01-01 00:23:51 | Offer or interview clear structure watch capital.  | PERSON_0000000697 | Kelly Green       | Ashley Turner    |
|  2    | PERSON_0000000018 | 2017-01-01 00:37:11 | Recently race draw thousand around ahead.          | PERSON_0000000989 | Laura Stephenson  | Hannah James     |
|  3    | PERSON_0000000040 | 2017-01-01 00:33:12 | To protect man image power beyond.                 | PERSON_0000000418 | Jeffrey Miller    | Scott Collins    |
...                                                                               |
| 28966 | PERSON_0000000985 | 2017-01-04 00:09:23 | However agreement fear door land hotel.            | PERSON_0000000211 | Laura Mason       | Joshua Miller    |
| 28967 | PERSON_0000000995 | 2017-01-04 00:48:08 | Top heat window quite forward friend somebody.     | PERSON_0000000171 | Dawn Kim          | Sandra Phillips  |
| 28968 | PERSON_0000000999 | 2017-01-04 00:58:46 | Answer region wind condition someone bed cover.    | PERSON_0000000252 | Tamara Ramirez    | Jordan Collins   |
+-------+-------------------+---------------------+----------------------------------------------------+-------------------+-------------------+------------------+

Tipp: Schau dir das Snippet auf Github an

Analyse der Ergebnisse

Ein grober Blick auf das soziale Netzwerk: Zähle die Anzahl einzigartiger Freund:innen, die jede Person kontaktiert. Läuft die Simulation lang genug, erhältst du eine Untergrenze für die tatsächliche Größe jedes sozialen Umfelds (aufgrund der Pareto-Gewichte sind manche Freund:innen sehr unwahrscheinlich; du brauchst lange Läufe, um alle zu beobachten).

Wir haben 15 simulierte Tage ausgeführt und Folgendes erhalten – zentriert nahe und etwas unter dem durchschnittlichen Grad 20 des Graphen:

social_group_size = (
    resulting_dataset[["PERSON_ID", "OTHER_PERSON"]]
        .groupby("PERSON_ID")
        .agg(lambda friends_ids: len(friends_ids.unique()))
    )
social_group_size.plot(kind="hist")

Histogram of observed social group size

Da geht noch mehr!

Weitere Trumania-Features, die wir hier nicht behandelt haben:

  • Mehrere Populationen in einem Szenario
  • Mehrere Stories in einem Szenario
  • Probabilistische Abhängigkeiten zwischen Stories (z. B. Rückruf nach eingehendem Anruf)
  • Status-Updates (z. B. Guthabenabbuchung basierend auf Anrufdauer)
  • Wiederverwendbare Bausteine wie Geografien, zusätzliche Timer-Profile ...
  • Persistenz von Szenarioteilen

Fazit

Wir haben Trumania als szenariobasierte Bibliothek zur Datengenerierung in Python vorgestellt. Die erzeugten Datensätze lassen sich vielseitig einsetzen – für Tests, Lernen und Benchmarking.

Um Anwendungen oder Algorithmen sinnvoll zu testen, brauchen wir Datensätze mit erwartbaren statistischen Eigenschaften. Wir haben gezeigt, dass Trumania das kann – mit einem einfachen „Message-Log“-Datensatz, der eine Verteilung der Nachrichten pro Nutzer:in, eine Tageszeitverteilung und eine Verteilung über einen sozialen Graphen respektiert.

Außerdem haben wir gezeigt, wie flexibel Trumanias Komponenten sind und wie damit unterschiedlichste Szenarien entstehen.

Ein Trumania-Szenario kann Beziehungen zwischen Entitäten enthalten und mit theoretischen Zufallsverteilungen, empirischen Verteilungen aus echten Datensätzen oder sogar mit Produktivdaten (z. B. Standort-IDs) konfiguriert werden. Wir haben etwa eine empirische Verteilung von Telefonzeitstempeln genutzt, um den Random-Timer unserer Story zu konfigurieren.

In diesem Sinne: Leg los! Schau auf Trumanias Github vorbei und schreib dein eigenes Szenario ;)

Du kannst dich auch gern an Svend Vanderveken oder Co-Autor Milan van der Meer wenden:

  • Svend Vanderveken ist freiberuflicher Software Engineer mit Fokus auf Datenverarbeitung – vor allem Kafka, Scala, SQL, Python ... Folge ihm auf seinem Blog svend.kelesia.com und auf Twitter @sv3ndk.
  • Milan van der Meer arbeitet als Software Engineer bei Real Impact Analytics. Vernetze dich mit ihm auf LinkedIn.

Themen

Ähnlich

Tutorial

Python Datenstrukturen Tutorial