Ir al contenido principal

Generar conjuntos de datos aleatorios realistas con Trumania

Descubre Trumania para Python, una biblioteca de generación de conjuntos de datos aleatorios basada en escenarios. Aprende paso a paso a crear datos sintéticos y aleatorios.
Actualizado 17 sept 2026  · 15 min leer

Explorar con IA

ChatGPTClaudePerplexity

Nota del editor: esta publicación se escribió en colaboración con Milan van der Meer. Ambos autores forman parte del equipo de Real Impact Analytics, una startup belga de big data que captura el valor de los datos de telecomunicaciones al "appificar el big data".

Este tutorial te da una pequeña muestra de por qué puede interesarte generar conjuntos de datos aleatorios y qué puedes esperar de ellos. También te guiará por unos primeros ejemplos de cómo usar Trumania, una biblioteca de Python para generación de datos.

Para más información, puedes visitar el GitHub de Trumania.

¿Por qué generar conjuntos de datos aleatorios?

Generar datos aleatorios es útil tanto para data engineers como para data scientists. ¿Por qué trabajan con datos sintéticos y cómo los obtienen?

Como data engineer, después de desarrollar tu nueva y estupenda aplicación de procesamiento de datos, llega el momento de probar de extremo a extremo y, por tanto, necesitas datos de entrada.

Como data scientist, la generación de datos te permite experimentar con distintas formas de explorar conjuntos de datos, algoritmos, técnicas de visualización o validar supuestos sobre el comportamiento de un método frente a muchos conjuntos de datos diferentes a tu elección.

En ambos casos, la opción tentadora es usar datos reales. El pequeño problema es que los datos de producción suelen ser difíciles de conseguir, incluso parcialmente, y con las nuevas leyes europeas sobre privacidad y seguridad no está siendo más fácil.

Puede que seas de los afortunados y tengas un conjunto de datos de producción completo en tu portátil. Aun así, te interesará tener más variabilidad para validar distintas situaciones: tamaños de población diferentes, distribuciones de datos diversas, problemas de calidad como datos ausentes o no válidos...

Si quieres profundizar en por qué generar datos aleatorios es útil, echa un vistazo a "Why synthetic data is about to become a major competitive advantage".

Generación de datos aleatorios basada en esquemas: ¡necesitamos buenas relaciones!

Esta sección ilustra la generación de datos aleatorios basada en esquemas y muestra sus limitaciones.

Existen muchas herramientas para generar conjuntos de datos aleatorios. Un enfoque común es la generación basada en esquemas, que te permite definir un plano y usarlo para crear entidades. Khermes y LogSynth son dos ejemplos.

Un ejemplo de configuración basada en esquema podría incluir este esquema de persona:

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

Este esquema define la generación de algunos datos sobre una persona: nombre, edad y género. Es simple y rápido, pero tiene limitaciones. Una de las principales es simular relaciones entre distintas entidades o basadas en el tiempo.

Por ejemplo, ¿cómo hacer que ciertos nombres sean más probables según el género? ¿O cómo crear un registro de acciones de usuario en el que las acciones sean más frecuentes durante los fines de semana?

El enfoque basado en esquemas no facilita exponer tu nueva aplicación a particularidades del mundo real, como:

  • Conjuntos de datos desbalanceados (dificultan operaciones tipo group-by);
  • Patrones agrupados en la red de entidades. Es decir, descubrir grupos sociales a través de interacciones de usuario (por ejemplo, llamadas telefónicas);
  • Perfiles de actividad temporales variados. Las acciones son más probables en ciertos momentos del día o de la semana;
  • Relaciones causales entre acciones. Por ejemplo, muchas compras en una tienda que acaban provocando un evento de "sin stock".

Trumania se basa en escenarios para solventar estas carencias y generar conjuntos de datos más realistas. A medida que se despliega el escenario, varias poblaciones interactúan entre sí, actualizan sus propiedades y emiten logs.

Un ejemplo de escenario serían personas llamando a sus amistades. Cuando alguien llama, paga en minutos; cuando se agotan, tiene que ir a una tienda... El conjunto de datos resultante podría ser el registro de llamadas de usuario, el registro de compras y el nivel de stock por hora. Las acciones en un escenario pueden ser aleatorias o deterministas.

En Trumania, los conjuntos de datos generados suelen ser series temporales, ya que resultan de la ejecución de un escenario que se desarrolla en el tiempo.

Generar datos interrelacionados con Trumania (¡hora del tutorial!)

En el ejemplo siguiente verás cómo elaborar un escenario básico en el que varias personas se envían mensajes entre sí. El conjunto de datos resultante será la serie temporal de mensajes intercambiados.

Seguiremos estos pasos:

  • crear un circus de Trumania, el mundo en el que se ejecutará el escenario
  • añadir una historia "hello world" que produzca logs básicos codificados a mano
  • añadir una relación para asignar citas favoritas a cada persona, que usarás como contenido de los mensajes
  • parametrizar el tiempo de la historia para obtener una distribución más realista de mensajes a lo largo del día
  • añadir otra relación para definir una red social, de modo que el grafo social que emerge del log de mensajes sea más realista

Crear un circus de Trumania

Propósito

El primer paso es crear un circus, el mundo en el que existirán todos los elementos del escenario. También crearemos una población básica de persons. Como muchos aspectos de Trumania son aleatorios, este paso introduce el concepto de generators, que controlan comportamientos aleatorios.

No añadiremos ninguna historia por ahora, así que todo será estático.

¡Vamos allá!

Cómo hacerlo

Un circus de Trumania se crea así de simple:

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

En Trumania, todos los elementos relacionados con el tiempo están controlados por un reloj central. La parte más importante en el fragmento anterior es step_duration=pd.Timedelta("1h"), que define que el reloj avanzará en pasos de 1 hora.

Después, añade la población person al circus. Una población es básicamente un conjunto de agentes con un id y algunos atributos.

Es posible especificar los valores de cada atributo a mano, pero en la mayoría de los casos querrás generarlos aleatoriamente, así que primero definamos algunos generators para ello:

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 generador de Trumania proporciona datos cuando se llama a su método generate(). En el ejemplo anterior, id_gen generará cadenas del tipo PERSON_0001, PERSON_0002, ... age_gen muestreará repetidamente de una distribución normal y name_gen devolverá nombres de personas aleatorios.

Ten en cuenta que en Trumania están disponibles cualquier distribución estadística de numpy y cualquier proveedor de Faker, y es fácil ampliar Trumania con otros nuevos.

Con esto listo, ya puedes añadir una población de 1000 personas con IDs secuenciales y dos atributos aleatorios básicos:

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)

Resultado

Ya puedes echar un vistazo a los atributos generados de todos los miembros de la población person llamando a 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 |
+-------------------+-----------------+---------+

Consejo: si quieres ver el script completo de esta población básica de usuarios, revisa este snippet en Github.

Mensajes hello world: crear historias

Propósito

Hagamos esto un poco más interesante añadiendo una story. Una historia en Trumania es una pieza del escenario que encapsula algún aspecto dinámico. Se ejecuta cada vez que el reloj simulado avanza un paso.

Recuerda que estableciste step_duration=pd.Timedelta("1h") en el circus, así que cada hora lógica, cada historia podrá ejecutarse.

Vas a crear una sencilla historia hello-world en la que todos los miembros de la población person emitirán regularmente el mensaje "hello world".

Cómo hacerlo

Al crear una story, lo primero es indicar la población que la inicia. En este caso será person. Lo segundo es definir el timer, que marca cuándo cada miembro dispara la acción. Por ahora, lo fijamos a 1, lo que implica que todos ejecutarán la historia en cada paso del reloj (más adelante crearás temporizadores más elaborados):

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

De momento la historia está vacía y no hace nada: necesitas añadirle algunas operaciones. Las operaciones pueden ser aleatorias o deterministas, pueden leer y actualizar atributos de cualquier población u otro estado del circus, e incluso tener efectos secundarios.

Por ahora, añadamos dos operaciones deterministas y un registrador para obtener 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 genera una marca temporal aleatoria dentro del intervalo actual de 1 hora y ConstantGenerator produce valores constantes codificados.

Resultado

Ejecutemos el circus durante 48 h de tiempo simulado:

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

Esto debería producir el siguiente conjunto de datos básico, en el que cada una de las 1000 personas dijo "hello world" 48 veces en 2 días.

El primer campo del conjunto de datos, PERSON_ID, corresponde a member_id_field de la story. Los otros dos, TIME y MESSAGE, corresponden a las dos operaciones que añadiste.

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

Consejo: consulta el snippet completo en Github

Alguien a quien decir Hello World

Propósito

Mejoremos esto añadiendo otra persona aleatoria a la conversación. Por ahora, diremos que cualquiera puede hablar con cualquiera con igual probabilidad, lo cual no es muy realista (añadirás una red social más adelante). Además, enriquecerás el conjunto de datos añadiendo el nombre de las personas a partir de su ID.

Cómo hacerlo

Guardaremos a la otra persona en un nuevo campo OTHER_PERSON. En este caso simplificado, quieres poner ahí el resultado de una selección aleatoria no ponderada de la población person. Puedes usar la operación aleatoria select_one de esa población:

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

Resultado

Al ejecutar de nuevo el circus, obtendrás un conjunto con el nuevo campo 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      |
+-------+-------------------+---------------------+-------------+-------------------+---------------------+--------------------+

Consejo: puedes ver el código completo aquí

Siempre dices lo mismo

Propósito

Mejorémoslo generando una frase aleatoria en el campo MESSAGE en lugar del aburrido hello world codificado. Así ilustramos un primer uso básico de las Relationship de Trumania.

Vas a asociar a cada persona 4 citas. Cada vez que alguien envíe un mensaje, el contenido se elegirá entre sus 4 citas. Además, asociarás a cada cita un weight distinto que define su probabilidad: una cita con peso alto aparecerá más a menudo que una con peso bajo.

¿Cómo empezamos?

Cómo hacerlo

Primero crea un generador de citas, similar a los generadores de Faker anteriores:

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

Podrías usarlo directamente en la story como antes, pero hagámoslo más interesante restringiendo a cada persona a usar siempre una de sus 4 citas favoritas. Para ello, creas una Relationship entre los miembros de la población person y algunos valores de frases.

Primero, creas una relación vacía:

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

Luego, la pueblas con 4 citas por persona. En el código, person.ids es una Series de pandas de tamaño 1000 con todos los ids de la población person, y quote_generator.generate(size=person.size) devuelve otra Series también de tamaño 1000 con citas aleatorias.

Esto significa que cada pasada del bucle añade 1000 relaciones: una cita para cada uno de los 1000 usuarios. Las primeras 1000 tendrán weight=1, las siguientes serán más frecuentes con weight=2, ...

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

Ahora puedes sustituir el ConstantGenerator en tu historia por una operación select_one sobre esa relación. Se lee así: para cada valor actual en el campo PERSON_ID, busca en quotes las relaciones que parten de ese ID y selecciona aleatoriamente un valor relacionado. Como no sobrescribes pesos, se usan los definidos arriba en 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")
)

Resultado

Al ejecutar el circus, el campo MESSAGE contendrá ahora la cita aleatoria:

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

La gracia de la relación de citas es que ahora los campos del conjunto de datos generado están relacionados: algunas frases son más usadas por ciertos usuarios.

Consejo: revisa el snippet completo en Github

No es lo que haces, es cuándo lo haces

Propósito

Hasta ahora, cada miembro de la población person produce exactamente un mensaje cada hora. Hagámoslo más realista.

Trumania usa dos conceptos para controlar el tiempo de ejecución de una historia: niveles de actividad y perfiles de temporizador. Juntos determinan la probabilidad de disparar la story dado un usuario y una hora del día.

Cómo hacerlo

Actualizarás la story y parametrizarás estos dos aspectos.

Primero, creemos el perfil de temporizador de la historia, que controla cuándo se disparan más o menos acciones. Este perfil es común a todos los miembros que ejecutan la historia.

Trumania incluye algunos por defecto interesantes, como DefaultDailyTimerGenerator, que corresponde al uso telefónico diario medido en un conjunto de datos de producción real:

Trumania Default Daily Timer Generator Visualization

Instanciémoslo en tu escenario:

from trumania.components.time_patterns.profilers import DefaultDailyTimerGenerator

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

En segundo lugar, están los niveles de actividad de cada miembro para esa historia, que definen lo activo que es cada usuario. Puedes fijar niveles distintos para que unos disparen la acción más a menudo que otros.

Podrías especificarlos manualmente, aunque aquí usaremos un generador aleatorio que asignará un nivel de actividad a cada miembro.

Primero, definimos tres niveles de 3, 10 y 20 disparos por día de media:

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

Luego, creamos el generador de actividad, basado en choice de Numpy. El de abajo asignará actividad baja al 20% de la población, media al 70% y alta al 10%:

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

Y ahora puedes usar este perfil y el generador de actividad en tu historia:

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
)

Resultado

El conjunto de datos resultante tiene el mismo esquema que antes:

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

Consejo: revisa el snippet en Github

Análisis del resultado

Los patrones temporales que configuraste deberían ser visibles en la estructura del conjunto resultante. Puedes demostrarlo con un análisis básico sobre resulting_dataset:

  • el número de mensajes en cada hora del día debería seguir la distribución especificada (fíjate en lo similar que es a la curva de DefaultDailyTimerGenerator de arriba):
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

  • el histograma de número de mensajes por usuario también debería ajustarse a tu configuración:
    • configuraste low_activity a 3 mensajes de media al día, med_activity a 10 y high_activity a 20, y ejecutaste 5 días. Esto debería dar 3 grupos de person con aproximadamente 15, 50 y 100 mensajes totales, que corresponden a los 3 centros del histograma
    • configuraste las probabilidades de estos niveles en .2, .7 y .1, lo que se corresponde de forma aproximada con el área de cada "joroba" del histograma:
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

La red social

Propósito

Puedes mejorar aún más el escenario conectando a las personas para que envíen mensajes a sus amistades durante la historia, en lugar de a alguien aleatorio. Esto introducirá mucha estructura, ya que ahora debería poder descubrirse el grafo social a partir del log de mensajes.

Cómo hacerlo

Puedes modelar la red social con otra Relationship, igual que hiciste con las citas. Recordarás que la relación de quotes tenía un peso, que definía qué cita era más o menos probable. Aquí puedes hacer lo mismo para que ciertas amistades sean más probables que otras.

Ten en cuenta que aquí defines una relación de person a person, aunque en general Trumania permite definir relaciones entre cualquier par de poblaciones.

Hay muchas formas de generar las aristas de un grafo social. Una básica y clásica es el algoritmo de Erdős–Rényi. En Trumania es especialmente sencillo porque ya está integrado. Solo necesitas inyectarlo para crear una relación.

Aquí definirás un grafo social entre los miembros de person en el que cada persona tenga 20 amistades. Observa que automáticamente se añadirá un peso con una distribución de Pareto.

from trumania.components.social_networks.erdos_renyi import WithErdosRenyi

# self es el circus aquí; consulta el código de ejemplo en github para más detalles
self.add_er_social_network_relationship(
    self.populations["person"],
    relationship_name="friends",
    average_degree=20)

Con esta relación, puedes revisar la historia otra vez. Ahora OTHER_PERSON ya no se elegirá al azar en la población, sino entre las amistades de cada miembro de person, con probabilidad proporcional al peso definido en la red 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")
)

Resultado

De nuevo, el conjunto resultante tiene el mismo esquema que antes, aunque esta vez con aún más estructura gracias a la red social básica:

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

Consejo: puedes echar un vistazo al snippet en Github

Análisis del resultado

Una forma muy simple de explorar esa red social es contar el número de amistades únicas a las que llama cada persona. Si ejecutas la simulación suficiente tiempo, esto te dará una cota inferior del tamaño real de la red social de cada uno (como usas pesos con distribución de Pareto, muchas amistades son realmente poco probables, así que necesitas ejecutar mucho para observar todas las posibles).

Ejecutamos el escenario durante 15 días simulados y obtuvimos esto, centrado muy cerca y un poco por debajo del grado medio 20 configurado en tu grafo 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

¡Hay más!

Varias funcionalidades de Trumania no se han descrito en esta publicación:

  • tener varias poblaciones en un escenario
  • tener varias historias en un escenario
  • añadir dependencias probabilísticas entre historias (p. ej., que alguien llame a una amistad después de haber sido llamado)
  • actualización de estado (p. ej., disminuir saldos según la duración de la llamada)
  • piezas de escenario reutilizables, como geografías, más perfiles de temporizador, ...
  • persistencia de piezas de escenario

Conclusión

Hemos presentado Trumania como una biblioteca de generación de datos basada en escenarios en Python. Los conjuntos de datos generados pueden usarse para probar, aprender y hacer benchmarking, entre otras aplicaciones.

Explicamos que, para probar correctamente una aplicación o un algoritmo, necesitamos conjuntos de datos que respeten ciertas propiedades estadísticas esperadas. Mostramos que Trumania puede hacerlo con un ejemplo en el que generamos un "registro de mensajes" básico que respeta una distribución de número de mensajes por usuario, una distribución por hora del día y una distribución de llamadas dentro de un grafo social.

Esperamos haber demostrado también que la flexibilidad de los componentes de Trumania permite crear una gran variedad de escenarios.

Un escenario en Trumania puede incluir relaciones entre entidades y configurarse con distribuciones aleatorias teóricas, distribuciones empíricas observadas en un conjunto real o incluso datos de producción (p. ej., un conjunto de ids de ubicaciones). Lo demostramos reutilizando una distribución estadística de marcas temporales de uso del teléfono observada en un conjunto real para configurar el temporizador aleatorio de nuestra historia.

Con esto en mente, ¡lánzate! Entra en el Github de Trumania y escribe tu propio escenario ;)

Si no, contacta con Svend Vanderveken o el coautor Milan van der Meer:

  • Svend Vanderveken es software engineer freelance especializado en soluciones de procesamiento de datos, sobre todo con Kafka, Scala, SQL, Python... Síguele en su blog svend.kelesia.com y en Twitter @sv3ndk.
  • Milan van der Meer trabaja actualmente como software engineer en Real Impact Analytics. Puedes conectar con él en LinkedIn

Nota del editor: esta publicación se escribió en colaboración con Milan van der Meer. Ambos autores forman parte del equipo de Real Impact Analytics, una startup belga de big data que captura el valor de los datos de telecomunicaciones al "appificar el big data".

Este tutorial te da una pequeña muestra de por qué puede interesarte generar conjuntos de datos aleatorios y qué puedes esperar de ellos. También te guiará por unos primeros ejemplos de cómo usar Trumania, una biblioteca de Python para generación de datos.

Para más información, puedes visitar el GitHub de Trumania.

¿Por qué generar conjuntos de datos aleatorios?

Generar datos aleatorios es útil tanto para data engineers como para data scientists. ¿Por qué trabajan con datos sintéticos y cómo los obtienen?

Como data engineer, después de desarrollar tu nueva y estupenda aplicación de procesamiento de datos, llega el momento de probar de extremo a extremo y, por tanto, necesitas datos de entrada.

Como data scientist, la generación de datos te permite experimentar con distintas formas de explorar conjuntos de datos, algoritmos, técnicas de visualización o validar supuestos sobre el comportamiento de un método frente a muchos conjuntos de datos diferentes a tu elección.

En ambos casos, la opción tentadora es usar datos reales. El pequeño problema es que los datos de producción suelen ser difíciles de conseguir, incluso parcialmente, y con las nuevas leyes europeas sobre privacidad y seguridad no está siendo más fácil.

Puede que seas de los afortunados y tengas un conjunto de datos de producción completo en tu portátil. Aun así, te interesará tener más variabilidad para validar distintas situaciones: tamaños de población diferentes, distribuciones de datos diversas, problemas de calidad como datos ausentes o no válidos...

Si quieres profundizar en por qué generar datos aleatorios es útil, echa un vistazo a "Why synthetic data is about to become a major competitive advantage".

Generación de datos aleatorios basada en esquemas: ¡necesitamos buenas relaciones!

Esta sección ilustra la generación de datos aleatorios basada en esquemas y muestra sus limitaciones.

Existen muchas herramientas para generar conjuntos de datos aleatorios. Un enfoque común es la generación basada en esquemas, que te permite definir un plano y usarlo para crear entidades. Khermes y LogSynth son dos ejemplos.

Un ejemplo de configuración basada en esquema podría incluir este esquema de persona:

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

Este esquema define la generación de algunos datos sobre una persona: nombre, edad y género. Es simple y rápido, pero tiene limitaciones. Una de las principales es simular relaciones entre distintas entidades o basadas en el tiempo.

Por ejemplo, ¿cómo hacer que ciertos nombres sean más probables según el género? ¿O cómo crear un registro de acciones de usuario en el que las acciones sean más frecuentes durante los fines de semana?

El enfoque basado en esquemas no facilita exponer tu nueva aplicación a particularidades del mundo real, como:

  • Conjuntos de datos desbalanceados (dificultan operaciones tipo group-by);
  • Patrones agrupados en la red de entidades. Es decir, descubrir grupos sociales a través de interacciones de usuario (por ejemplo, llamadas telefónicas);
  • Perfiles de actividad temporales variados. Las acciones son más probables en ciertos momentos del día o de la semana;
  • Relaciones causales entre acciones. Por ejemplo, muchas compras en una tienda que acaban provocando un evento de "sin stock".

Trumania se basa en escenarios para solventar estas carencias y generar conjuntos de datos más realistas. A medida que se despliega el escenario, varias poblaciones interactúan entre sí, actualizan sus propiedades y emiten logs.

Un ejemplo de escenario serían personas llamando a sus amistades. Cuando alguien llama, paga en minutos; cuando se agotan, tiene que ir a una tienda... El conjunto de datos resultante podría ser el registro de llamadas de usuario, el registro de compras y el nivel de stock por hora. Las acciones en un escenario pueden ser aleatorias o deterministas.

En Trumania, los conjuntos de datos generados suelen ser series temporales, ya que resultan de la ejecución de un escenario que se desarrolla en el tiempo.

Generar datos interrelacionados con Trumania (¡hora del tutorial!)

En el ejemplo siguiente verás cómo elaborar un escenario básico en el que varias personas se envían mensajes entre sí. El conjunto de datos resultante será la serie temporal de mensajes intercambiados.

Seguiremos estos pasos:

  • crear un circus de Trumania, el mundo en el que se ejecutará el escenario
  • añadir una historia "hello world" que produzca logs básicos codificados a mano
  • añadir una relación para asignar citas favoritas a cada persona, que usarás como contenido de los mensajes
  • parametrizar el tiempo de la historia para obtener una distribución más realista de mensajes a lo largo del día
  • añadir otra relación para definir una red social, de modo que el grafo social que emerge del log de mensajes sea más realista

Crear un circus de Trumania

Propósito

El primer paso es crear un circus, el mundo en el que existirán todos los elementos del escenario. También crearemos una población básica de persons. Como muchos aspectos de Trumania son aleatorios, este paso introduce el concepto de generators, que controlan comportamientos aleatorios.

No añadiremos ninguna historia por ahora, así que todo será estático.

¡Vamos allá!

Cómo hacerlo

Un circus de Trumania se crea así de simple:

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

En Trumania, todos los elementos relacionados con el tiempo están controlados por un reloj central. La parte más importante en el fragmento anterior es step_duration=pd.Timedelta("1h"), que define que el reloj avanzará en pasos de 1 hora.

Después, añade la población person al circus. Una población es básicamente un conjunto de agentes con un id y algunos atributos.

Es posible especificar los valores de cada atributo a mano, pero en la mayoría de los casos querrás generarlos aleatoriamente, así que primero definamos algunos generators para ello:

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 generador de Trumania proporciona datos cuando se llama a su método generate(). En el ejemplo anterior, id_gen generará cadenas del tipo PERSON_0001, PERSON_0002, ... age_gen muestreará repetidamente de una distribución normal y name_gen devolverá nombres de personas aleatorios.

Ten en cuenta que en Trumania están disponibles cualquier distribución estadística de numpy y cualquier proveedor de Faker, y es fácil ampliar Trumania con otros nuevos.

Con esto listo, ya puedes añadir una población de 1000 personas con IDs secuenciales y dos atributos aleatorios básicos:

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)

Resultado

Ya puedes echar un vistazo a los atributos generados de todos los miembros de la población person llamando a 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 |
+-------------------+-----------------+---------+

Consejo: si quieres ver el script completo de esta población básica de usuarios, revisa este snippet en Github.

Mensajes hello world: crear historias

Propósito

Hagamos esto un poco más interesante añadiendo una story. Una historia en Trumania es una pieza del escenario que encapsula algún aspecto dinámico. Se ejecuta cada vez que el reloj simulado avanza un paso.

Recuerda que estableciste step_duration=pd.Timedelta("1h") en el circus, así que cada hora lógica, cada historia podrá ejecutarse.

Vas a crear una sencilla historia hello-world en la que todos los miembros de la población person emitirán regularmente el mensaje "hello world".

Cómo hacerlo

Al crear una story, lo primero es indicar la población que la inicia. En este caso será person. Lo segundo es definir el timer, que marca cuándo cada miembro dispara la acción. Por ahora, lo fijamos a 1, lo que implica que todos ejecutarán la historia en cada paso del reloj (más adelante crearás temporizadores más elaborados):

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

De momento la historia está vacía y no hace nada: necesitas añadirle algunas operaciones. Las operaciones pueden ser aleatorias o deterministas, pueden leer y actualizar atributos de cualquier población u otro estado del circus, e incluso tener efectos secundarios.

Por ahora, añadamos dos operaciones deterministas y un registrador para obtener 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 genera una marca temporal aleatoria dentro del intervalo actual de 1 hora y ConstantGenerator produce valores constantes codificados.

Resultado

Ejecutemos el circus durante 48 h de tiempo simulado:

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

Esto debería producir el siguiente conjunto de datos básico, en el que cada una de las 1000 personas dijo "hello world" 48 veces en 2 días.

El primer campo del conjunto de datos, PERSON_ID, corresponde a member_id_field de la story. Los otros dos, TIME y MESSAGE, corresponden a las dos operaciones que añadiste.

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

Consejo: consulta el snippet completo en Github

Alguien a quien decir Hello World

Propósito

Mejoremos esto añadiendo otra persona aleatoria a la conversación. Por ahora, diremos que cualquiera puede hablar con cualquiera con igual probabilidad, lo cual no es muy realista (añadirás una red social más adelante). Además, enriquecerás el conjunto de datos añadiendo el nombre de las personas a partir de su ID.

Cómo hacerlo

Guardaremos a la otra persona en un nuevo campo OTHER_PERSON. En este caso simplificado, quieres poner ahí el resultado de una selección aleatoria no ponderada de la población person. Puedes usar la operación aleatoria select_one de esa población:

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

Resultado

Al ejecutar de nuevo el circus, obtendrás un conjunto con el nuevo campo 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      |
+-------+-------------------+---------------------+-------------+-------------------+---------------------+--------------------+

Consejo: puedes ver el código completo aquí

Siempre dices lo mismo

Propósito

Mejorémoslo generando una frase aleatoria en el campo MESSAGE en lugar del aburrido hello world codificado. Así ilustramos un primer uso básico de las Relationship de Trumania.

Vas a asociar a cada persona 4 citas. Cada vez que alguien envíe un mensaje, el contenido se elegirá entre sus 4 citas. Además, asociarás a cada cita un weight distinto que define su probabilidad: una cita con peso alto aparecerá más a menudo que una con peso bajo.

¿Cómo empezamos?

Cómo hacerlo

Primero crea un generador de citas, similar a los generadores de Faker anteriores:

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

Podrías usarlo directamente en la story como antes, pero hagámoslo más interesante restringiendo a cada persona a usar siempre una de sus 4 citas favoritas. Para ello, creas una Relationship entre los miembros de la población person y algunos valores de frases.

Primero, creas una relación vacía:

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

Luego, la pueblas con 4 citas por persona. En el código, person.ids es una Series de pandas de tamaño 1000 con todos los ids de la población person, y quote_generator.generate(size=person.size) devuelve otra Series también de tamaño 1000 con citas aleatorias.

Esto significa que cada pasada del bucle añade 1000 relaciones: una cita para cada uno de los 1000 usuarios. Las primeras 1000 tendrán weight=1, las siguientes serán más frecuentes con weight=2, ...

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

Ahora puedes sustituir el ConstantGenerator en tu historia por una operación select_one sobre esa relación. Se lee así: para cada valor actual en el campo PERSON_ID, busca en quotes las relaciones que parten de ese ID y selecciona aleatoriamente un valor relacionado. Como no sobrescribes pesos, se usan los definidos arriba en 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")
)

Resultado

Al ejecutar el circus, el campo MESSAGE contendrá ahora la cita aleatoria:

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

La gracia de la relación de citas es que ahora los campos del conjunto de datos generado están relacionados: algunas frases son más usadas por ciertos usuarios.

Consejo: revisa el snippet completo en Github

No es lo que haces, es cuándo lo haces

Propósito

Hasta ahora, cada miembro de la población person produce exactamente un mensaje cada hora. Hagámoslo más realista.

Trumania usa dos conceptos para controlar el tiempo de ejecución de una historia: niveles de actividad y perfiles de temporizador. Juntos determinan la probabilidad de disparar la story dado un usuario y una hora del día.

Cómo hacerlo

Actualizarás la story y parametrizarás estos dos aspectos.

Primero, creemos el perfil de temporizador de la historia, que controla cuándo se disparan más o menos acciones. Este perfil es común a todos los miembros que ejecutan la historia.

Trumania incluye algunos por defecto interesantes, como DefaultDailyTimerGenerator, que corresponde al uso telefónico diario medido en un conjunto de datos de producción real:

Trumania Default Daily Timer Generator Visualization

Instanciémoslo en tu escenario:

from trumania.components.time_patterns.profilers import DefaultDailyTimerGenerator

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

En segundo lugar, están los niveles de actividad de cada miembro para esa historia, que definen lo activo que es cada usuario. Puedes fijar niveles distintos para que unos disparen la acción más a menudo que otros.

Podrías especificarlos manualmente, aunque aquí usaremos un generador aleatorio que asignará un nivel de actividad a cada miembro.

Primero, definimos tres niveles de 3, 10 y 20 disparos por día de media:

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

Luego, creamos el generador de actividad, basado en choice de Numpy. El de abajo asignará actividad baja al 20% de la población, media al 70% y alta al 10%:

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

Y ahora puedes usar este perfil y el generador de actividad en tu historia:

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
)

Resultado

El conjunto de datos resultante tiene el mismo esquema que antes:

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

Consejo: revisa el snippet en Github

Análisis del resultado

Los patrones temporales que configuraste deberían ser visibles en la estructura del conjunto resultante. Puedes demostrarlo con un análisis básico sobre resulting_dataset:

  • el número de mensajes en cada hora del día debería seguir la distribución especificada (fíjate en lo similar que es a la curva de DefaultDailyTimerGenerator de arriba):
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

  • el histograma de número de mensajes por usuario también debería ajustarse a tu configuración:
    • configuraste low_activity a 3 mensajes de media al día, med_activity a 10 y high_activity a 20, y ejecutaste 5 días. Esto debería dar 3 grupos de person con aproximadamente 15, 50 y 100 mensajes totales, que corresponden a los 3 centros del histograma
    • configuraste las probabilidades de estos niveles en .2, .7 y .1, lo que se corresponde de forma aproximada con el área de cada "joroba" del histograma:
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

La red social

Propósito

Puedes mejorar aún más el escenario conectando a las personas para que envíen mensajes a sus amistades durante la historia, en lugar de a alguien aleatorio. Esto introducirá mucha estructura, ya que ahora debería poder descubrirse el grafo social a partir del log de mensajes.

Cómo hacerlo

Puedes modelar la red social con otra Relationship, igual que hiciste con las citas. Recordarás que la relación de quotes tenía un peso, que definía qué cita era más o menos probable. Aquí puedes hacer lo mismo para que ciertas amistades sean más probables que otras.

Ten en cuenta que aquí defines una relación de person a person, aunque en general Trumania permite definir relaciones entre cualquier par de poblaciones.

Hay muchas formas de generar las aristas de un grafo social. Una básica y clásica es el algoritmo de Erdős–Rényi. En Trumania es especialmente sencillo porque ya está integrado. Solo necesitas inyectarlo para crear una relación.

Aquí definirás un grafo social entre los miembros de person en el que cada persona tenga 20 amistades. Observa que automáticamente se añadirá un peso con una distribución de Pareto.

from trumania.components.social_networks.erdos_renyi import WithErdosRenyi

# self es el circus aquí; consulta el código de ejemplo en github para más detalles
self.add_er_social_network_relationship(
    self.populations["person"],
    relationship_name="friends",
    average_degree=20)

Con esta relación, puedes revisar la historia otra vez. Ahora OTHER_PERSON ya no se elegirá al azar en la población, sino entre las amistades de cada miembro de person, con probabilidad proporcional al peso definido en la red 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")
)

Resultado

De nuevo, el conjunto resultante tiene el mismo esquema que antes, aunque esta vez con aún más estructura gracias a la red social básica:

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

Consejo: puedes echar un vistazo al snippet en Github

Análisis del resultado

Una forma muy simple de explorar esa red social es contar el número de amistades únicas a las que llama cada persona. Si ejecutas la simulación suficiente tiempo, esto te dará una cota inferior del tamaño real de la red social de cada uno (como usas pesos con distribución de Pareto, muchas amistades son realmente poco probables, así que necesitas ejecutar mucho para observar todas las posibles).

Ejecutamos el escenario durante 15 días simulados y obtuvimos esto, centrado muy cerca y un poco por debajo del grado medio 20 configurado en tu grafo 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

¡Hay más!

Varias funcionalidades de Trumania no se han descrito en esta publicación:

  • tener varias poblaciones en un escenario
  • tener varias historias en un escenario
  • añadir dependencias probabilísticas entre historias (p. ej., que alguien llame a una amistad después de haber sido llamado)
  • actualización de estado (p. ej., disminuir saldos según la duración de la llamada)
  • piezas de escenario reutilizables, como geografías, más perfiles de temporizador, ...
  • persistencia de piezas de escenario

Conclusión

Hemos presentado Trumania como una biblioteca de generación de datos basada en escenarios en Python. Los conjuntos de datos generados pueden usarse para probar, aprender y hacer benchmarking, entre otras aplicaciones.

Explicamos que, para probar correctamente una aplicación o un algoritmo, necesitamos conjuntos de datos que respeten ciertas propiedades estadísticas esperadas. Mostramos que Trumania puede hacerlo con un ejemplo en el que generamos un "registro de mensajes" básico que respeta una distribución de número de mensajes por usuario, una distribución por hora del día y una distribución de llamadas dentro de un grafo social.

Esperamos haber demostrado también que la flexibilidad de los componentes de Trumania permite crear una gran variedad de escenarios.

Un escenario en Trumania puede incluir relaciones entre entidades y configurarse con distribuciones aleatorias teóricas, distribuciones empíricas observadas en un conjunto real o incluso datos de producción (p. ej., un conjunto de ids de ubicaciones). Lo demostramos reutilizando una distribución estadística de marcas temporales de uso del teléfono observada en un conjunto real para configurar el temporizador aleatorio de nuestra historia.

Con esto en mente, ¡lánzate! Entra en el Github de Trumania y escribe tu propio escenario ;)

Si no, contacta con Svend Vanderveken o el coautor Milan van der Meer:

  • Svend Vanderveken es software engineer freelance especializado en soluciones de procesamiento de datos, sobre todo con Kafka, Scala, SQL, Python... Síguele en su blog svend.kelesia.com y en Twitter @sv3ndk.
  • Milan van der Meer trabaja actualmente como software engineer en Real Impact Analytics. Puedes conectar con él en LinkedIn
Temas
Python
Relacionado

Tutorial

Tutorial de Generación de nubes de palabras en Python

Aprende a realizar Análisis exploratorios de datos para el Procesamiento del lenguaje natural utilizando WordCloud en Python.
Duong Vu's photo

Duong Vu

11 min

Tutorial

Tutorial de Python sobre conjuntos y teoría de conjuntos

Aprende sobre los conjuntos en Python: qué son, cómo crearlos, cuándo usarlos, funciones incorporadas y su relación con las operaciones de la teoría de conjuntos.
DataCamp Team's photo

DataCamp Team

13 min

Tutorial

Aprendizaje automático de datos categóricos con el tutorial de Python

Aprenda los trucos más comunes para manejar datos categóricos y preprocesarlos para construir modelos de aprendizaje automático.
Moez Ali's photo

Moez Ali

14 min

Tutorial

Introducción al t-SNE

Aprende a visualizar datos de alta dimensión en un espacio de baja dimensión utilizando una técnica de reducción no lineal de la dimensionalidad.
Abid Ali Awan's photo

Abid Ali Awan

14 min

Tutorial

Introducción al trazado con Matplotlib en Python

Este tutorial muestra cómo utilizar Matplotlib, una potente biblioteca de visualización de datos en Python, para crear gráficos de líneas, barras y dispersión con datos bursátiles.

Kevin Babitz

25 min

Tutorial

Ajuste fino de GPT-3 mediante la API OpenAI y Python

Libere todo el potencial de GPT-3 mediante el ajuste fino. Aprenda a utilizar la API de OpenAI y Python para mejorar este modelo de red neuronal avanzado para su caso de uso específico.
Zoumana Keita 's photo

Zoumana Keita

12 min

Ver MásVer Más