Accéder au contenu principal

Générer des jeux de données aléatoires réalistes avec Trumania

Découvrez Trumania en Python, une bibliothèque de génération de jeux de données aléatoires basée sur des scénarios. Apprenez à créer un jeu de données synthétique et aléatoire dans ce tutoriel pas à pas.
Actualisé 19 sept. 2026  · 15 min lire

Explorer avec l’IA

ChatGPTClaudePerplexity

Note de la rédaction : cet article a été rédigé en collaboration avec Milan van der Meer. Les deux auteurs de ce billet font partie de l'équipe Real Impact Analytics, une startup belge innovante dans le big data qui valorise les données télécom en « appifiant le big data ».

Ce tutoriel vous donne un aperçu des raisons pour lesquelles vous pourriez vouloir générer des jeux de données aléatoires et de ce que vous pouvez en attendre. Il vous guidera aussi à travers vos premiers exemples d'utilisation de Trumania, une bibliothèque Python de génération de données.

Pour en savoir plus, rendez-vous sur le GitHub de Trumania !

Pourquoi générer des jeux de données aléatoires ?

La génération de données aléatoires est pertinente à la fois pour les data engineers et les data scientists. Pourquoi travaillent-ils avec des données synthétiques et comment s'en procurent-ils ?

En tant que data engineer, une fois votre nouvelle application de traitement de données prête, vient le moment de tester de bout en bout : il vous faut donc des données d'entrée.

Côté data science, la génération de données permet d'expérimenter différentes façons d'explorer des jeux de données, de tester des algorithmes, des techniques de visualisation, ou encore de valider des hypothèses sur le comportement d'une méthode face à de multiples jeux de données de votre choix.

Dans les deux cas, la tentation est d'utiliser des données réelles. Un petit problème cependant : les données de production sont généralement difficiles à obtenir, même partiellement, et cela ne s'arrange pas avec les nouvelles réglementations européennes sur la confidentialité et la sécurité.

Peut-être faites-vous partie des chanceux qui ont un bel extrait complet de données de production sur leur ordinateur. Même dans ce cas, vous souhaiterez davantage de variabilité pour valider divers scénarios : tailles de population différentes, distributions de données variées, problèmes de qualité (valeurs manquantes ou invalides), etc.

Pour un tour d'horizon plus complet des bénéfices de la génération de données aléatoires, lisez « Why synthetic data is about to become a major competitive advantage ».

Génération aléatoire basée sur un schéma : de bonnes relations, c'est essentiel !

Cette section illustre la génération de données aléatoires basée sur un schéma et ses limites.

De nombreux outils existent déjà pour générer des jeux de données aléatoires. Une approche courante est la génération basée sur un schéma, qui permet de définir un plan et de l'utiliser pour générer des entités. Khermes et LogSynth en sont deux exemples.

Un exemple de configuration basée sur un schéma pourrait inclure ce schéma de personne :

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

Ce schéma définit la génération de quelques données sur une personne : nom, âge et genre. C'est simple, rapide, mais limité. Une limite majeure est la difficulté à simuler des relations entre entités ou dépendantes du temps.

Par exemple, comment rendre certains prénoms plus probables selon le genre ? Ou comment créer un journal d'actions utilisateur où les actions surviennent plus souvent le week-end ?

L'approche par schéma n'expose pas facilement votre application aux spécificités et difficultés du monde réel, comme :

  • des jeux de données déséquilibrés (opérations de type group-by difficiles) ;
  • des motifs de regroupement dans les réseaux d'entités, comme la découverte de communautés via les interactions (ex. : appels téléphoniques) ;
  • des profils d'activité temporels variés (actions plus probables à certains moments de la journée ou de la semaine) ;
  • des relations causales entre actions (ex. : de nombreux achats en boutique qui déclenchent un événement « rupture de stock »).

Trumania est fondé sur la notion de scénario pour pallier ces limites et générer des jeux de données plus réalistes. Au fil du scénario, différentes populations interagissent, mettent à jour leurs propriétés et émettent des logs.

Un exemple de scénario : des personnes qui appellent leurs amis. Lors d'un appel, elles consomment des minutes, et lorsqu'elles n'en ont plus, elles doivent aller en boutique... Le jeu de données résultant pourrait être le journal des appels, le journal des achats et le niveau de stock à chaque heure. Les actions d'un scénario peuvent être aléatoires ou déterministes.

Avec Trumania, les jeux de données générés sont généralement des séries temporelles, car ils résultent de l'exécution d'un scénario qui se déroule dans le temps.

Générer des données interreliées avec Trumania (c'est l'heure du tutoriel !)

Dans l'exemple ci-dessous, vous allez construire un scénario de base où plusieurs personnes s'envoient des messages. Le jeu de données obtenu sera une série temporelle de messages échangés.

Voici les étapes que nous allons suivre :

  • créer un circus Trumania, le monde où s'exécutera le scénario ;
  • ajouter une histoire « hello world » qui produit des logs simples en dur ;
  • ajouter une relationship pour associer des citations favorites à chaque personne, utilisées comme contenu des messages ;
  • paramétrer le temps de l'histoire pour une distribution plus réaliste des messages au fil de la journée ;
  • ajouter une autre relationship pour définir un réseau social, afin que le graphe social déduit du journal des messages soit plus réaliste.

Créer un circus Trumania

Objectif

La première étape consiste à créer un circus, le monde dans lequel vivent tous les éléments du scénario. Nous allons aussi créer une population de base de persons. Comme de nombreux aspects de Trumania sont aléatoires, cette étape introduit les generators, qui contrôlent divers comportements aléatoires.

Nous n'ajoutons pas encore de scénario : tout reste statique.

Allons-y !

Mode d'emploi

Un circus Trumania se crée simplement ainsi :

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"))

Dans Trumania, tous les éléments liés au temps sont pilotés par une horloge centrale. L'élément clé dans l'extrait ci-dessus est step_duration=pd.Timedelta("1h"), qui définit une avancée de l'horloge par pas d'une heure.

Ajoutez ensuite la population person au circus. Une population est un ensemble d'agents avec un id et des attributs.

Vous pouvez renseigner manuellement chaque attribut, mais le plus souvent vous souhaiterez les générer aléatoirement. Définissons donc quelques 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))

Un générateur Trumania fournit des données quand on appelle sa méthode generate(). Dans l'exemple, id_gen génère des chaînes comme PERSON_0001, PERSON_0002, ... age_gen échantillonne selon une loi normale, et name_gen renvoie des noms aléatoires.

Toute distribution statistique de numpy ainsi que tout provider Faker sont disponibles dans Trumania, et il est facile d'en ajouter de nouveaux.

Avec cela, vous pouvez créer une population de 1000 personnes avec des identifiants séquentiels et deux attributs aléatoires de base :

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)

Résultat

Vous pouvez déjà visualiser les attributs générés de tous les membres de la population person via person.to_dataframe().

+-------------------+-----------------+---------+
|                   | 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 |
+-------------------+-----------------+---------+

Astuce : pour voir le script complet de cette population de base, consultez cet extrait sur Github !

Déclarations « Hello World » : créer des stories

Objectif

Rendons cela plus intéressant en ajoutant une story. Une story Trumania encapsule un aspect dynamique du scénario. Elle s'exécute à chaque pas de l'horloge simulée.

Nous avons défini step_duration=pd.Timedelta("1h") dans le circus : chaque heure logique, chaque story peut s'exécuter.

Créons une simple story « hello-world » où tous les membres de la population person émettent régulièrement le message « hello world ».

Mode d'emploi

Premier point clé à spécifier : la population qui initie la story. Ici, person. Deuxième point : le timer, qui définit quand chaque membre déclenche l'action. Pour l'instant, on force la valeur à 1, ce qui implique que tous exécutent la story à chaque pas d'horloge (vous raffinerez les timers ensuite) :

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)
)

À ce stade, la story est vide : il faut y ajouter des operations. Elles peuvent être aléatoires ou déterministes, lire et mettre à jour les attributs de n'importe quelle population ou autre état du circus, et produire des effets de bord.

Ajoutons deux opérations déterministes et un logger pour obtenir des logs :

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 génère un horodatage aléatoire dans l'intervalle horaire courant, et ConstantGenerator produit une valeur constante en dur.

Résultat

Lançons le circus pendant 48 h de temps simulé :

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

Vous obtenez un jeu de données où chacune des 1000 personnes a dit « hello world » 48 fois sur 2 jours.

Le premier champ, PERSON_ID, correspond au member_id_field de la story. Les deux autres, TIME et MESSAGE, correspondent aux opérations ajoutées.

+-------+-------------------+---------------------+-------------+
|       | 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 |
+-------+-------------------+---------------------+-------------+

Astuce : retrouvez l'extrait complet sur Github

Quelqu'un à qui dire « Hello World »

Objectif

Améliorons en ajoutant une autre personne aléatoire à la conversation. Pour l'instant, nous dirons que toute personne peut parler à n'importe quelle autre avec une probabilité égale, ce qui n'est pas très réaliste (vous ajouterez un réseau social plus loin). Nous allons aussi enrichir le jeu de données avec le nom des personnes à partir de leur ID.

Mode d'emploi

Disons que l'autre personne sera stockée dans un nouveau champ OTHER_PERSON. Dans ce cas simple, on veut y placer le résultat d'une sélection aléatoire non pondérée dans la population person. Utilisez l'opération aléatoire select_one de cette 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")
)

Résultat

En relançant le circus, vous obtenez un jeu de données avec le nouveau champ 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      |
+-------+-------------------+---------------------+-------------+-------------------+---------------------+--------------------+

Astuce : consultez le code complet ici

Vous dites toujours ça

Objectif

Allons plus loin en générant une phrase aléatoire dans le champ MESSAGE à la place du banal hello world en dur. Cela illustre un premier usage de base des Relationship de Trumania.

Vous allez associer à chaque personne 4 citations. À chaque émission de message, le contenu sera choisi parmi les 4 citations de cette personne. En outre, chaque citation sera associée à un weight différent définissant sa probabilité : une citation avec un poids élevé sera émise plus souvent que celle avec un poids faible.

Comment procéder ?

Mode d'emploi

Commencez par créer un générateur de citations, similaire aux autres générateurs Faker vus plus haut :

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

Vous pourriez utiliser ce générateur tel quel dans la story, mais rendons l'exercice plus intéressant en contraignant chaque personne à utiliser l'une de ses 4 citations favorites. Pour cela, créez une Relationship entre les membres de la population person et des valeurs de phrases.

Créez d'abord une relationship vide :

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

Puis, alimentez-la avec 4 citations par personne. Dans le code ci-dessous, person.ids est une Series pandas de taille 1000 avec tous les identifiants de person, et quote_generator.generate(size=person.size) fournit une autre Series pandas de taille 1000, avec des citations aléatoires.

Chaque passage de la boucle ci-dessous ajoute 1000 relations : une citation pour chacun des 1000 utilisateurs. Les 1000 premières auront un poids = 1, les suivantes un poids = 2, etc.

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

Vous pouvez maintenant remplacer le ConstantGenerator par une opération select_one sur cette relationship. En clair : pour chaque valeur dans PERSON_ID, cherchez les relations quotes qui partent de cet ID et sélectionnez aléatoirement une valeur liée. Sans surcharge des poids dans l'opération, ce sont les poids par défaut définis lors des add_relations qui s'appliquent :

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")
)

Résultat

Au lancement du circus, le champ MESSAGE contient désormais une citation aléatoire :

+-------+-------------------+---------------------+-----------------------------------------------------+-------------------+---------------------+---------------------+
|       | 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   |
+-------+-------------------+---------------------+-----------------------------------------------------+-------------------+---------------------+---------------------+

L'intérêt de la relationship utilisée pour les citations est que les champs du jeu de données deviennent corrélés : certaines phrases sont plus souvent utilisées par certains utilisateurs.

Astuce : consultez l'extrait complet sur Github

Ce n'est pas ce que vous faites, c'est quand vous le faites

Objectif

Jusqu'ici, chaque membre de la population person produit exactement un message par heure. Rendons cela plus réaliste.

Trumania combine deux concepts pour piloter le temps d'exécution d'une story : les niveaux d'activité et les profils de timer. Ensemble, ils déterminent la probabilité de déclencher la story pour un utilisateur donné et une heure donnée.

Mode d'emploi

Vous allez mettre à jour la story et paramétrer ces deux aspects.

Commencez par créer le profil de timer de la story, qui contrôle les moments où plus ou moins d'actions sont déclenchées. Ce profil est identique pour tous les membres de la population.

Trumania inclut des profils par défaut intéressants, comme DefaultDailyTimerGenerator, qui correspond à l'usage téléphonique quotidien mesuré sur un jeu de données de production :

Trumania Default Daily Timer Generator Visualization

Instancions-le dans votre scénario :

from trumania.components.time_patterns.profilers import DefaultDailyTimerGenerator

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

Ensuite, définissez les niveaux d'activité des membres pour cette story : à quel point chaque utilisateur est actif. Vous pouvez fixer un niveau propre à chaque membre pour que certains déclenchent l'action plus souvent que d'autres.

Vous pourriez tout définir à la main, mais nous allons utiliser un générateur aléatoire qui attribue un niveau d'activité à chaque membre.

Définissons d'abord trois niveaux à 3, 10 et 20 déclenchements par jour en moyenne :

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"))

Créez ensuite le générateur d'activité basé sur numpy.choice. Celui-ci attribuera une faible activité à 20 % de la population, moyenne à 70 % et élevée à 10 % :

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

Vous pouvez maintenant utiliser ce profil de timer et ce générateur d'activité dans la 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
)

Résultat

Le jeu de données résultant a le même schéma qu'auparavant :

+-------+-------------------+---------------------+---------------------------------------------------------+-------------------+---------------------+----------------------+
|       | 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 |
+-------+-------------------+---------------------+---------------------------------------------------------+-------------------+---------------------+----------------------+

Astuce : consultez l'extrait sur Github

Analyse des résultats

Les patterns temporels inclus dans la configuration de votre scénario doivent maintenant apparaître dans la structure interne du jeu de données. Vous pouvez le montrer avec une analyse simple sur le resulting_dataset :

  • le nombre de messages à chaque heure de la journée doit suivre la distribution spécifiée (remarquez la similarité avec la courbe du DefaultDailyTimerGenerator ci-dessus) :
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

  • L'histogramme du nombre de messages par utilisateur doit aussi refléter votre configuration :
    • vous avez configuré low_activity à 3 messages par jour en moyenne, med_activity à 10 et high_activity à 20, puis exécuté la simulation sur 5 jours. On doit donc observer 3 groupes de person autour de ~15, ~50 et ~100 messages au total, correspondant aux 3 centres de l'histogramme ci-dessous ;
    • vous avez fixé les probabilités de ces niveaux à .2, .7 et .1, ce qui correspond grossièrement à l'aire de chaque « bosse » de l'histogramme :
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

Le réseau social

Objectif

Vous pouvez encore améliorer le scénario en connectant les personnes pour qu'elles envoient des messages à leurs amis plutôt qu'à une personne aléatoire. Cela introduit beaucoup de structure dans le jeu de données, puisque le graphe social doit désormais être déductible du journal des messages.

Mode d'emploi

Modélisez le réseau social via une Relationship, comme pour les citations. Vous vous souvenez que la relationship quotes avait un poids définissant la probabilité d'une citation selon la personne. Ici, vous pouvez utiliser des poids pour indiquer quels amis ont plus ou moins de chances d'être contactés.

Notez que vous définissez ici une relationship de person à person, bien que Trumania permette des relationships entre n'importe quelles populations.

Il existe de nombreuses manières de générer les arêtes d'un graphe social. Une méthode classique consiste à utiliser l'algorithme d'Erdős–Rényi. C'est particulièrement simple dans Trumania, car il est intégré au framework. Il suffit de l'injecter dans votre scénario pour créer une relationship.

Ici, vous allez définir un graphe social entre les membres de person où chaque personne a 20 amis. Un poids sera automatiquement ajouté à cette relationship avec une distribution de Pareto.

from trumania.components.social_networks.erdos_renyi import WithErdosRenyi

# self est le circus ici, voir l'exemple sur github pour les détails
self.add_er_social_network_relationship(
    self.populations["person"],
    relationship_name="friends",
    average_degree=20)

Avec cette relationship en place, vous pouvez revoir notre story. Cette fois, OTHER_PERSON ne sera plus choisi au hasard dans la population, mais sélectionné parmi les amis de chaque membre person, avec une probabilité proportionnelle au poids défini dans le réseau social :

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")
)

Résultat

Encore une fois, le jeu de données a le même schéma que précédemment, mais il présente davantage de structure grâce à ce réseau social de base :

+-------+-------------------+---------------------+----------------------------------------------------+-------------------+-------------------+------------------+
|       | 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   |
+-------+-------------------+---------------------+----------------------------------------------------+-------------------+-------------------+------------------+

Astuce : jetez un œil à l'extrait sur Github

Analyse des résultats

Une façon simple d'explorer ce réseau social est de compter le nombre d'amis uniques contactés par chaque personne. Si vous exécutez la simulation suffisamment longtemps, cela vous donne une borne inférieure du nombre réel de personnes dans le réseau social de chacun (avec des poids de type Pareto, de nombreux amis ont une probabilité très faible d'être appelés, il faut donc une longue exécution pour les observer tous).

Vous exécutez le scénario sur 15 jours simulés et obtenez cette distribution, centrée légèrement en dessous du degré moyen de 20 défini dans le graphe social :

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

Et ce n'est pas tout !

Plusieurs fonctionnalités de Trumania ne sont pas décrites ici :

  • plusieurs populations dans un scénario
  • plusieurs stories dans un scénario
  • dépendances probabilistes entre stories (ex. : une personne rappelle après avoir été appelée)
  • mise à jour d'état (ex. : diminuer un solde en fonction de la durée d'appel)
  • briques de scénario réutilisables : géographies, profils de timer supplémentaires, ...
  • persistance de composants de scénario

Conclusion

Nous avons présenté Trumania comme une bibliothèque de génération de données basée sur des scénarios en Python. Les jeux de données générés peuvent servir à de nombreux usages : tests, apprentissage, benchmarking.

Nous avons expliqué que, pour bien tester une application ou un algorithme, il faut des jeux de données qui respectent certaines propriétés statistiques attendues. Nous avons montré que Trumania sait le faire avec un exemple de « journal de messages » respectant une distribution du nombre de messages par utilisateur, une distribution sur les heures de la journée, ainsi qu'une distribution des échanges au sein d'un graphe social.

Nous espérons avoir montré que la flexibilité des composants Trumania permet de créer une grande variété de scénarios.

Un scénario Trumania peut inclure des relations entre entités et être configuré avec des distributions aléatoires théoriques, des distributions empiriques observées dans un jeu de données réel ou même des données de production (par ex. : un jeu d'IDs de lieux). Nous l'avons illustré en réutilisant une distribution statistique d'horodatages d'usage téléphonique observée sur des données réelles pour configurer le timer aléatoire de notre story.

Avec cela en tête, lancez-vous ! Rendez-vous sur le Github de Trumania et écrivez votre propre scénario ;)

Sinon, n'hésitez pas à contacter Svend Vanderveken ou le co-auteur Milan van der Meer :

  • Svend Vanderveken est ingénieur logiciel freelance, expert des solutions de traitement de données (Kafka, Scala, SQL, Python, ...). Suivez-le sur son blog svend.kelesia.com et sur Twitter @sv3ndk.
  • Milan van der Meer est actuellement ingénieur logiciel chez Real Impact Analytics. Vous pouvez le contacter sur LinkedIn.

Note de la rédaction : cet article a été rédigé en collaboration avec Milan van der Meer. Les deux auteurs de ce billet font partie de l'équipe Real Impact Analytics, une startup belge innovante dans le big data qui valorise les données télécom en « appifiant le big data ».

Ce tutoriel vous donne un aperçu des raisons pour lesquelles vous pourriez vouloir générer des jeux de données aléatoires et de ce que vous pouvez en attendre. Il vous guidera aussi à travers vos premiers exemples d'utilisation de Trumania, une bibliothèque Python de génération de données.

Pour en savoir plus, rendez-vous sur le GitHub de Trumania !

Pourquoi générer des jeux de données aléatoires ?

La génération de données aléatoires est pertinente à la fois pour les data engineers et les data scientists. Pourquoi travaillent-ils avec des données synthétiques et comment s'en procurent-ils ?

En tant que data engineer, une fois votre nouvelle application de traitement de données prête, vient le moment de tester de bout en bout : il vous faut donc des données d'entrée.

Côté data science, la génération de données permet d'expérimenter différentes façons d'explorer des jeux de données, de tester des algorithmes, des techniques de visualisation, ou encore de valider des hypothèses sur le comportement d'une méthode face à de multiples jeux de données de votre choix.

Dans les deux cas, la tentation est d'utiliser des données réelles. Un petit problème cependant : les données de production sont généralement difficiles à obtenir, même partiellement, et cela ne s'arrange pas avec les nouvelles réglementations européennes sur la confidentialité et la sécurité.

Peut-être faites-vous partie des chanceux qui ont un bel extrait complet de données de production sur leur ordinateur. Même dans ce cas, vous souhaiterez davantage de variabilité pour valider divers scénarios : tailles de population différentes, distributions de données variées, problèmes de qualité (valeurs manquantes ou invalides), etc.

Pour un tour d'horizon plus complet des bénéfices de la génération de données aléatoires, lisez « Why synthetic data is about to become a major competitive advantage ».

Génération aléatoire basée sur un schéma : de bonnes relations, c'est essentiel !

Cette section illustre la génération de données aléatoires basée sur un schéma et ses limites.

De nombreux outils existent déjà pour générer des jeux de données aléatoires. Une approche courante est la génération basée sur un schéma, qui permet de définir un plan et de l'utiliser pour générer des entités. Khermes et LogSynth en sont deux exemples.

Un exemple de configuration basée sur un schéma pourrait inclure ce schéma de personne :

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

Ce schéma définit la génération de quelques données sur une personne : nom, âge et genre. C'est simple, rapide, mais limité. Une limite majeure est la difficulté à simuler des relations entre entités ou dépendantes du temps.

Par exemple, comment rendre certains prénoms plus probables selon le genre ? Ou comment créer un journal d'actions utilisateur où les actions surviennent plus souvent le week-end ?

L'approche par schéma n'expose pas facilement votre application aux spécificités et difficultés du monde réel, comme :

  • des jeux de données déséquilibrés (opérations de type group-by difficiles) ;
  • des motifs de regroupement dans les réseaux d'entités, comme la découverte de communautés via les interactions (ex. : appels téléphoniques) ;
  • des profils d'activité temporels variés (actions plus probables à certains moments de la journée ou de la semaine) ;
  • des relations causales entre actions (ex. : de nombreux achats en boutique qui déclenchent un événement « rupture de stock »).

Trumania est fondé sur la notion de scénario pour pallier ces limites et générer des jeux de données plus réalistes. Au fil du scénario, différentes populations interagissent, mettent à jour leurs propriétés et émettent des logs.

Un exemple de scénario : des personnes qui appellent leurs amis. Lors d'un appel, elles consomment des minutes, et lorsqu'elles n'en ont plus, elles doivent aller en boutique... Le jeu de données résultant pourrait être le journal des appels, le journal des achats et le niveau de stock à chaque heure. Les actions d'un scénario peuvent être aléatoires ou déterministes.

Avec Trumania, les jeux de données générés sont généralement des séries temporelles, car ils résultent de l'exécution d'un scénario qui se déroule dans le temps.

Générer des données interreliées avec Trumania (c'est l'heure du tutoriel !)

Dans l'exemple ci-dessous, vous allez construire un scénario de base où plusieurs personnes s'envoient des messages. Le jeu de données obtenu sera une série temporelle de messages échangés.

Voici les étapes que nous allons suivre :

  • créer un circus Trumania, le monde où s'exécutera le scénario ;
  • ajouter une histoire « hello world » qui produit des logs simples en dur ;
  • ajouter une relationship pour associer des citations favorites à chaque personne, utilisées comme contenu des messages ;
  • paramétrer le temps de l'histoire pour une distribution plus réaliste des messages au fil de la journée ;
  • ajouter une autre relationship pour définir un réseau social, afin que le graphe social déduit du journal des messages soit plus réaliste.

Créer un circus Trumania

Objectif

La première étape consiste à créer un circus, le monde dans lequel vivent tous les éléments du scénario. Nous allons aussi créer une population de base de persons. Comme de nombreux aspects de Trumania sont aléatoires, cette étape introduit les generators, qui contrôlent divers comportements aléatoires.

Nous n'ajoutons pas encore de scénario : tout reste statique.

Allons-y !

Mode d'emploi

Un circus Trumania se crée simplement ainsi :

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"))

Dans Trumania, tous les éléments liés au temps sont pilotés par une horloge centrale. L'élément clé dans l'extrait ci-dessus est step_duration=pd.Timedelta("1h"), qui définit une avancée de l'horloge par pas d'une heure.

Ajoutez ensuite la population person au circus. Une population est un ensemble d'agents avec un id et des attributs.

Vous pouvez renseigner manuellement chaque attribut, mais le plus souvent vous souhaiterez les générer aléatoirement. Définissons donc quelques 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))

Un générateur Trumania fournit des données quand on appelle sa méthode generate(). Dans l'exemple, id_gen génère des chaînes comme PERSON_0001, PERSON_0002, ... age_gen échantillonne selon une loi normale, et name_gen renvoie des noms aléatoires.

Toute distribution statistique de numpy ainsi que tout provider Faker sont disponibles dans Trumania, et il est facile d'en ajouter de nouveaux.

Avec cela, vous pouvez créer une population de 1000 personnes avec des identifiants séquentiels et deux attributs aléatoires de base :

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)

Résultat

Vous pouvez déjà visualiser les attributs générés de tous les membres de la population person via person.to_dataframe().

+-------------------+-----------------+---------+
|                   | 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 |
+-------------------+-----------------+---------+

Astuce : pour voir le script complet de cette population de base, consultez cet extrait sur Github !

Déclarations « Hello World » : créer des stories

Objectif

Rendons cela plus intéressant en ajoutant une story. Une story Trumania encapsule un aspect dynamique du scénario. Elle s'exécute à chaque pas de l'horloge simulée.

Nous avons défini step_duration=pd.Timedelta("1h") dans le circus : chaque heure logique, chaque story peut s'exécuter.

Créons une simple story « hello-world » où tous les membres de la population person émettent régulièrement le message « hello world ».

Mode d'emploi

Premier point clé à spécifier : la population qui initie la story. Ici, person. Deuxième point : le timer, qui définit quand chaque membre déclenche l'action. Pour l'instant, on force la valeur à 1, ce qui implique que tous exécutent la story à chaque pas d'horloge (vous raffinerez les timers ensuite) :

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)
)

À ce stade, la story est vide : il faut y ajouter des operations. Elles peuvent être aléatoires ou déterministes, lire et mettre à jour les attributs de n'importe quelle population ou autre état du circus, et produire des effets de bord.

Ajoutons deux opérations déterministes et un logger pour obtenir des logs :

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 génère un horodatage aléatoire dans l'intervalle horaire courant, et ConstantGenerator produit une valeur constante en dur.

Résultat

Lançons le circus pendant 48 h de temps simulé :

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

Vous obtenez un jeu de données où chacune des 1000 personnes a dit « hello world » 48 fois sur 2 jours.

Le premier champ, PERSON_ID, correspond au member_id_field de la story. Les deux autres, TIME et MESSAGE, correspondent aux opérations ajoutées.

+-------+-------------------+---------------------+-------------+
|       | 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 |
+-------+-------------------+---------------------+-------------+

Astuce : retrouvez l'extrait complet sur Github

Quelqu'un à qui dire « Hello World »

Objectif

Améliorons en ajoutant une autre personne aléatoire à la conversation. Pour l'instant, nous dirons que toute personne peut parler à n'importe quelle autre avec une probabilité égale, ce qui n'est pas très réaliste (vous ajouterez un réseau social plus loin). Nous allons aussi enrichir le jeu de données avec le nom des personnes à partir de leur ID.

Mode d'emploi

Disons que l'autre personne sera stockée dans un nouveau champ OTHER_PERSON. Dans ce cas simple, on veut y placer le résultat d'une sélection aléatoire non pondérée dans la population person. Utilisez l'opération aléatoire select_one de cette 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")
)

Résultat

En relançant le circus, vous obtenez un jeu de données avec le nouveau champ 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      |
+-------+-------------------+---------------------+-------------+-------------------+---------------------+--------------------+

Astuce : consultez le code complet ici

Vous dites toujours ça

Objectif

Allons plus loin en générant une phrase aléatoire dans le champ MESSAGE à la place du banal hello world en dur. Cela illustre un premier usage de base des Relationship de Trumania.

Vous allez associer à chaque personne 4 citations. À chaque émission de message, le contenu sera choisi parmi les 4 citations de cette personne. En outre, chaque citation sera associée à un weight différent définissant sa probabilité : une citation avec un poids élevé sera émise plus souvent que celle avec un poids faible.

Comment procéder ?

Mode d'emploi

Commencez par créer un générateur de citations, similaire aux autres générateurs Faker vus plus haut :

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

Vous pourriez utiliser ce générateur tel quel dans la story, mais rendons l'exercice plus intéressant en contraignant chaque personne à utiliser l'une de ses 4 citations favorites. Pour cela, créez une Relationship entre les membres de la population person et des valeurs de phrases.

Créez d'abord une relationship vide :

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

Puis, alimentez-la avec 4 citations par personne. Dans le code ci-dessous, person.ids est une Series pandas de taille 1000 avec tous les identifiants de person, et quote_generator.generate(size=person.size) fournit une autre Series pandas de taille 1000, avec des citations aléatoires.

Chaque passage de la boucle ci-dessous ajoute 1000 relations : une citation pour chacun des 1000 utilisateurs. Les 1000 premières auront un poids = 1, les suivantes un poids = 2, etc.

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

Vous pouvez maintenant remplacer le ConstantGenerator par une opération select_one sur cette relationship. En clair : pour chaque valeur dans PERSON_ID, cherchez les relations quotes qui partent de cet ID et sélectionnez aléatoirement une valeur liée. Sans surcharge des poids dans l'opération, ce sont les poids par défaut définis lors des add_relations qui s'appliquent :

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")
)

Résultat

Au lancement du circus, le champ MESSAGE contient désormais une citation aléatoire :

+-------+-------------------+---------------------+-----------------------------------------------------+-------------------+---------------------+---------------------+
|       | 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   |
+-------+-------------------+---------------------+-----------------------------------------------------+-------------------+---------------------+---------------------+

L'intérêt de la relationship utilisée pour les citations est que les champs du jeu de données deviennent corrélés : certaines phrases sont plus souvent utilisées par certains utilisateurs.

Astuce : consultez l'extrait complet sur Github

Ce n'est pas ce que vous faites, c'est quand vous le faites

Objectif

Jusqu'ici, chaque membre de la population person produit exactement un message par heure. Rendons cela plus réaliste.

Trumania combine deux concepts pour piloter le temps d'exécution d'une story : les niveaux d'activité et les profils de timer. Ensemble, ils déterminent la probabilité de déclencher la story pour un utilisateur donné et une heure donnée.

Mode d'emploi

Vous allez mettre à jour la story et paramétrer ces deux aspects.

Commencez par créer le profil de timer de la story, qui contrôle les moments où plus ou moins d'actions sont déclenchées. Ce profil est identique pour tous les membres de la population.

Trumania inclut des profils par défaut intéressants, comme DefaultDailyTimerGenerator, qui correspond à l'usage téléphonique quotidien mesuré sur un jeu de données de production :

Trumania Default Daily Timer Generator Visualization

Instancions-le dans votre scénario :

from trumania.components.time_patterns.profilers import DefaultDailyTimerGenerator

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

Ensuite, définissez les niveaux d'activité des membres pour cette story : à quel point chaque utilisateur est actif. Vous pouvez fixer un niveau propre à chaque membre pour que certains déclenchent l'action plus souvent que d'autres.

Vous pourriez tout définir à la main, mais nous allons utiliser un générateur aléatoire qui attribue un niveau d'activité à chaque membre.

Définissons d'abord trois niveaux à 3, 10 et 20 déclenchements par jour en moyenne :

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"))

Créez ensuite le générateur d'activité basé sur numpy.choice. Celui-ci attribuera une faible activité à 20 % de la population, moyenne à 70 % et élevée à 10 % :

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

Vous pouvez maintenant utiliser ce profil de timer et ce générateur d'activité dans la 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
)

Résultat

Le jeu de données résultant a le même schéma qu'auparavant :

+-------+-------------------+---------------------+---------------------------------------------------------+-------------------+---------------------+----------------------+
|       | 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 |
+-------+-------------------+---------------------+---------------------------------------------------------+-------------------+---------------------+----------------------+

Astuce : consultez l'extrait sur Github

Analyse des résultats

Les patterns temporels inclus dans la configuration de votre scénario doivent maintenant apparaître dans la structure interne du jeu de données. Vous pouvez le montrer avec une analyse simple sur le resulting_dataset :

  • le nombre de messages à chaque heure de la journée doit suivre la distribution spécifiée (remarquez la similarité avec la courbe du DefaultDailyTimerGenerator ci-dessus) :
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

  • L'histogramme du nombre de messages par utilisateur doit aussi refléter votre configuration :
    • vous avez configuré low_activity à 3 messages par jour en moyenne, med_activity à 10 et high_activity à 20, puis exécuté la simulation sur 5 jours. On doit donc observer 3 groupes de person autour de ~15, ~50 et ~100 messages au total, correspondant aux 3 centres de l'histogramme ci-dessous ;
    • vous avez fixé les probabilités de ces niveaux à .2, .7 et .1, ce qui correspond grossièrement à l'aire de chaque « bosse » de l'histogramme :
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

Le réseau social

Objectif

Vous pouvez encore améliorer le scénario en connectant les personnes pour qu'elles envoient des messages à leurs amis plutôt qu'à une personne aléatoire. Cela introduit beaucoup de structure dans le jeu de données, puisque le graphe social doit désormais être déductible du journal des messages.

Mode d'emploi

Modélisez le réseau social via une Relationship, comme pour les citations. Vous vous souvenez que la relationship quotes avait un poids définissant la probabilité d'une citation selon la personne. Ici, vous pouvez utiliser des poids pour indiquer quels amis ont plus ou moins de chances d'être contactés.

Notez que vous définissez ici une relationship de person à person, bien que Trumania permette des relationships entre n'importe quelles populations.

Il existe de nombreuses manières de générer les arêtes d'un graphe social. Une méthode classique consiste à utiliser l'algorithme d'Erdős–Rényi. C'est particulièrement simple dans Trumania, car il est intégré au framework. Il suffit de l'injecter dans votre scénario pour créer une relationship.

Ici, vous allez définir un graphe social entre les membres de person où chaque personne a 20 amis. Un poids sera automatiquement ajouté à cette relationship avec une distribution de Pareto.

from trumania.components.social_networks.erdos_renyi import WithErdosRenyi

# self est le circus ici, voir l'exemple sur github pour les détails
self.add_er_social_network_relationship(
    self.populations["person"],
    relationship_name="friends",
    average_degree=20)

Avec cette relationship en place, vous pouvez revoir notre story. Cette fois, OTHER_PERSON ne sera plus choisi au hasard dans la population, mais sélectionné parmi les amis de chaque membre person, avec une probabilité proportionnelle au poids défini dans le réseau social :

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")
)

Résultat

Encore une fois, le jeu de données a le même schéma que précédemment, mais il présente davantage de structure grâce à ce réseau social de base :

+-------+-------------------+---------------------+----------------------------------------------------+-------------------+-------------------+------------------+
|       | 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   |
+-------+-------------------+---------------------+----------------------------------------------------+-------------------+-------------------+------------------+

Astuce : jetez un œil à l'extrait sur Github

Analyse des résultats

Une façon simple d'explorer ce réseau social est de compter le nombre d'amis uniques contactés par chaque personne. Si vous exécutez la simulation suffisamment longtemps, cela vous donne une borne inférieure du nombre réel de personnes dans le réseau social de chacun (avec des poids de type Pareto, de nombreux amis ont une probabilité très faible d'être appelés, il faut donc une longue exécution pour les observer tous).

Vous exécutez le scénario sur 15 jours simulés et obtenez cette distribution, centrée légèrement en dessous du degré moyen de 20 défini dans le graphe social :

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

Et ce n'est pas tout !

Plusieurs fonctionnalités de Trumania ne sont pas décrites ici :

  • plusieurs populations dans un scénario
  • plusieurs stories dans un scénario
  • dépendances probabilistes entre stories (ex. : une personne rappelle après avoir été appelée)
  • mise à jour d'état (ex. : diminuer un solde en fonction de la durée d'appel)
  • briques de scénario réutilisables : géographies, profils de timer supplémentaires, ...
  • persistance de composants de scénario

Conclusion

Nous avons présenté Trumania comme une bibliothèque de génération de données basée sur des scénarios en Python. Les jeux de données générés peuvent servir à de nombreux usages : tests, apprentissage, benchmarking.

Nous avons expliqué que, pour bien tester une application ou un algorithme, il faut des jeux de données qui respectent certaines propriétés statistiques attendues. Nous avons montré que Trumania sait le faire avec un exemple de « journal de messages » respectant une distribution du nombre de messages par utilisateur, une distribution sur les heures de la journée, ainsi qu'une distribution des échanges au sein d'un graphe social.

Nous espérons avoir montré que la flexibilité des composants Trumania permet de créer une grande variété de scénarios.

Un scénario Trumania peut inclure des relations entre entités et être configuré avec des distributions aléatoires théoriques, des distributions empiriques observées dans un jeu de données réel ou même des données de production (par ex. : un jeu d'IDs de lieux). Nous l'avons illustré en réutilisant une distribution statistique d'horodatages d'usage téléphonique observée sur des données réelles pour configurer le timer aléatoire de notre story.

Avec cela en tête, lancez-vous ! Rendez-vous sur le Github de Trumania et écrivez votre propre scénario ;)

Sinon, n'hésitez pas à contacter Svend Vanderveken ou le co-auteur Milan van der Meer :

  • Svend Vanderveken est ingénieur logiciel freelance, expert des solutions de traitement de données (Kafka, Scala, SQL, Python, ...). Suivez-le sur son blog svend.kelesia.com et sur Twitter @sv3ndk.
  • Milan van der Meer est actuellement ingénieur logiciel chez Real Impact Analytics. Vous pouvez le contacter sur LinkedIn.
Sujets
Python