Nota do editor: este post foi escrito em colaboração com Milan van der Meer. Ambos os autores fazem parte da equipe da Real Impact Analytics, uma startup belga inovadora de big data que captura valor em dados de telecom ao "transformar big data em apps".
Este tutorial dá um gostinho do porquê você pode querer gerar conjuntos de dados aleatórios e o que esperar deles. Também vai guiá-lo por alguns primeiros exemplos de como usar o Trumania, uma biblioteca Python para geração de dados.
Para mais informações, visite o GitHub do Trumania!
Por que gerar conjuntos de dados aleatórios?
Gerar conjuntos de dados aleatórios é relevante tanto para data engineers quanto para data scientists. Por que esses profissionais trabalham com dados sintéticos e como eles os obtêm?
Como data engineer, depois de escrever sua nova e incrível aplicação de processamento de dados, chega a hora de testar ponta a ponta — e, para isso, você precisa de dados de entrada.
Como data scientist, você se beneficia da geração de dados porque ela permite experimentar diferentes formas de explorar datasets, algoritmos, técnicas de visualização de dados ou validar suposições sobre o comportamento de um método em muitos conjuntos de dados diferentes à sua escolha.
Nos dois casos, é tentador usar dados reais. O problema é que dados de produção costumam ser difíceis de obter, mesmo parcialmente — e isso não está ficando mais fácil com as novas leis europeias de privacidade e segurança.
Talvez você seja um dos sortudos e tenha um conjunto completo de dados de produção no seu laptop. Ainda assim, você provavelmente vai querer mais variabilidade para validar diferentes situações: tamanhos de população distintos, distribuições de dados variadas, problemas de qualidade como dados ausentes ou inválidos...
Para uma leitura mais completa sobre por que gerar dados aleatórios é útil, confira "Why synthetic data is about to become a major competitive advantage".
Geração de dados aleatórios baseada em esquema: precisamos de bons relacionamentos!
Esta seção tenta ilustrar a geração de dados aleatórios baseada em esquema e mostrar suas limitações.
Já existem muitas ferramentas para gerar datasets aleatórios. Uma abordagem comum é a geração baseada em esquema, que permite definir um blueprint e usá-lo para gerar entidades. Khermes e LogSynth são dois exemplos.
Um exemplo de configuração baseada em esquema poderia incluir este schema de pessoa:
{
{
"field": "Name",
"class": NameGenerator
},
{
"field": "Age",
"class": RandomInt(18, 65)
},
{
"field": "Gender",
"class": RandomPicker(["Male", "Female", "Other"])
}
}
Esse schema define a geração de alguns dados sobre uma pessoa: nome, idade e gênero. É simples, rápido — e tem limitações. Uma limitação importante é a dificuldade de simular relacionamentos entre diferentes entidades ou dependências temporais.
Por exemplo, como tornar alguns nomes mais prováveis dado o gênero? Ou como criar um log de ações de usuários em que certas ações são mais frequentes nos fins de semana?
A abordagem baseada em schema não facilita expor sua aplicação a especificidades do mundo real, como:
- Datasets desbalanceados (dificuldades em operações do tipo group by);
- Padrões de clusters na rede de entidades. Ou seja, descobrir grupos sociais por meio de interações (por exemplo, ligações telefônicas);
- Diferentes perfis de atividade no tempo. Ações são mais prováveis em certos horários do dia ou da semana;
- Relações causais entre ações. Por exemplo, muitas compras em uma loja que acabam gerando um evento de "estoque esgotado".
O Trumania é baseado em cenários para lidar com essas limitações e gerar datasets mais realistas. À medida que o cenário se desenrola, várias populações interagem entre si, atualizam propriedades e emitem logs.
Um exemplo de cenário seria pessoas ligando para amigos. Quando alguém liga, paga em minutos; quando os minutos acabam, precisa ir até uma loja... O dataset resultante pode ser o log de chamadas, o log de compras e o nível de estoque a cada hora. As ações no cenário podem ser aleatórias ou determinísticas.
No Trumania, os datasets gerados normalmente são séries temporais, pois resultam da execução de um cenário que se desenrola ao longo do tempo.
Gerando dados inter-relacionados com o Trumania (hora do tutorial!)
No exemplo abaixo, você verá como elaborar um cenário básico no qual várias pessoas enviam mensagens entre si. O dataset resultante será uma série temporal de mensagens trocadas entre pessoas.
A sequência de passos será:
- criar um circus do Trumania, que é o mundo onde o cenário é executado
- adicionar uma história "hello world" que produz logs básicos, hard-coded
- adicionar um relationship para atribuir citações preferidas a cada pessoa, usadas como conteúdo das mensagens.
- parametrizar o tempo da história para obter uma distribuição de mensagens mais realista ao longo do dia
- adicionar outro relacionamento para definir uma rede social, deixando o grafo social que emerge do log mais realista
Criando um circus do Trumania
Objetivo
O primeiro passo é criar um circus, o mundo em que todos os elementos do cenário existirão. Também vamos criar uma população básica de persons. Como muitos aspectos do Trumania são aleatórios, este passo também introduz o conceito de generators, usados para controlar vários comportamentos randômicos.
Por enquanto, não vamos adicionar nenhuma história; tudo ficará estático.
Vamos lá!
Como fazer
Um circus do Trumania é criado assim:
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"))
No Trumania, todos os elementos relacionados ao tempo são controlados por um relógio central. A parte mais importante no trecho acima é step_duration=pd.Timedelta("1h"), que define que o relógio avança em passos de 1 hora.
Em seguida, adicione a população person ao circus. Uma população é essencialmente um conjunto de agentes com um id e alguns atributos.
É possível especificar manualmente os valores de cada atributo, mas, na maioria dos casos, você vai querer gerá-los aleatoriamente. Então, primeiro defina alguns generators para isso:
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))
Um generator do Trumania fornece dados quando seu método generate() é chamado. No exemplo, id_gen gera strings como PERSON_0001, PERSON_0002... age_gen amostra repetidamente de uma distribuição normal e name_gen fornece nomes de pessoas aleatórios.
Note que qualquer distribuição estatística do numpy e qualquer provider do Faker estão disponíveis no Trumania, e é fácil estender com novos.
Com isso, você pode adicionar uma população de 1000 pessoas com IDs sequenciais e dois atributos aleatórios 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
Você já pode ver os atributos gerados de todos os membros da população person chamando 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 |
+-------------------+-----------------+---------+
Dica: se quiser ver o script completo para essa população básica, confira este snippet no Github!
Declarações Hello World: criando histórias
Objetivo
Vamos deixar mais interessante adicionando uma story. Uma história no Trumania encapsula um aspecto dinâmico do cenário. Ela é executada sempre que o relógio simulado avança um passo.
Lembre que você definiu step_duration=pd.Timedelta("1h") no circus acima — isso implica que, a cada hora lógica, cada história pode ser executada.
Vamos criar uma história simples de hello world em que todos os membros da população person emitem regularmente a mensagem "hello world".
Como fazer
O primeiro ponto ao criar uma story é a população que a inicia; aqui, será person. O segundo é o timer, que define quando cada membro dispara a ação. Por ora, vamos fixá-lo em 1, o que implica que todos executarão a história a cada passo do relógio (criaremos timers mais elaborados depois):
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)
)
Até aqui, a história está vazia; precisamos adicionar algumas operações. Elas podem ser aleatórias ou determinísticas, ler/atualizar atributos de qualquer população ou outro estado do circus — e ainda ter efeitos colaterais.
Por enquanto, adicione duas operações determinísticas simples e um logger para realmente obter 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 gera um timestamp aleatório dentro do intervalo atual de 1 hora e ConstantGenerator produz valores fixos.
Resultado
Vamos rodar o circus por 48h de tempo simulado:
example_circus.run(
duration=pd.Timedelta("48h"),
log_output_folder="example_scenario",
delete_existing_logs=True
)
Isso deve produzir o dataset básico a seguir, no qual cada uma das 1000 pessoas disse "hello world" 48 vezes ao longo de 2 dias.
O primeiro campo, PERSON_ID, corresponde ao member_id_field da story. Os outros dois, TIME e MESSAGE, correspondem às duas operações principais que você colocou na história.
+-------+-------------------+---------------------+-------------+
| | 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 |
+-------+-------------------+---------------------+-------------+
Dica: veja o snippet completo no Github
Alguém para dizer Hello World
Objetivo
Vamos melhorar adicionando outra pessoa aleatória à conversa. Por enquanto, qualquer pessoa pode falar com qualquer outra com probabilidade igual — o que não é muito realista (adicionaremos uma rede social depois). Também vamos enriquecer o dataset adicionando o nome das pessoas com base no ID.
Como fazer
Vamos armazenar a outra pessoa em um novo campo OTHER_PERSON. Neste caso simples, queremos apenas preencher esse campo com uma seleção aleatória não ponderada da população person. Basta usar a operação aleatória select_one dessa população:
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
Ao rodar novamente, você terá um dataset com o novo 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 |
+-------+-------------------+---------------------+-------------+-------------------+---------------------+--------------------+
Dica: veja o código completo aqui
Você sempre diz isso
Objetivo
Vamos melhorar gerando uma frase aleatória no campo MESSAGE, em vez do hello world fixo. Isso permite ilustrar um uso básico de Relationship do Trumania.
Vamos associar a cada pessoa 4 citações. Sempre que ela emitir uma mensagem, o conteúdo será escolhido entre as 4 citações dela. Além disso, vamos associar a cada citação um weight diferente, que define sua probabilidade: uma citação com peso alto aparecerá com mais frequência que uma com peso baixo.
Como começar?
Como fazer
Primeiro, crie um gerador de citações, semelhante aos outros Faker que vimos:
quote_generator = FakerGenerator(method="sentence", nb_words=6, variable_nb_words=True,
seed=next(example_circus.seeder))
Você poderia usar esse gerador diretamente na story, mas vamos deixar mais interessante restringindo cada pessoa a usar sempre uma de suas 4 citações favoritas. Para isso, crie um Relationship entre os membros da população person e valores de frases.
Primeiro, crie um relacionamento vazio:
quotes_rel = example_circus.populations["person"].create_relationship("quotes")
Depois, povoe esse relacionamento com 4 citações para cada pessoa. No código abaixo, person.ids é uma Series do pandas com 1000 ids da população person, e quote_generator.generate(size=person.size) fornece outra Series também de tamanho 1000, com citações aleatórias.
Isso significa que cada passada do for abaixo adiciona 1000 relações: uma citação por usuário. As primeiras 1000 terão weight=1; as próximas, weight=2; e assim por diante...
for w in range(4):
quotes_rel.add_relations(
from_ids=person.ids,
to_ids=quote_generator.generate(size=person.size),
weights=w
)
Agora você pode substituir o ConstantGenerator na história por uma operação select_one nesse relacionamento. Em outras palavras: para cada valor em PERSON_ID, procure as relações em quotes que partem desse ID e selecione aleatoriamente um valor relacionado. Como não sobrescrevemos pesos abaixo, valem os definidos em 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
Ao rodar o circus, o campo MESSAGE agora deve conter uma citação aleatória:
+-------+-------------------+---------------------+-----------------------------------------------------+-------------------+---------------------+---------------------+
| | 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 |
+-------+-------------------+---------------------+-----------------------------------------------------+-------------------+---------------------+---------------------+
O ponto do relacionamento usado para as citações é que agora os campos do dataset gerado estão relacionados: algumas frases são usadas com maior frequência por certos usuários.
Dica: veja o snippet completo no Github
Não é o que você faz, é quando você faz
Objetivo
Até agora, cada membro da população person produz exatamente uma mensagem por hora. Vamos deixar isso mais realista.
O Trumania usa a combinação de 2 conceitos para controlar o tempo de execução de uma história: níveis de atividade e perfis de timer. Juntos, eles determinam a probabilidade de disparar a story dado um usuário específico e o horário do dia.
Como fazer
Vamos atualizar a story e parametrizar esses dois aspectos.
Primeiro, crie o perfil de timer da história, que controla os momentos em que mais ou menos ações são disparadas. Esse perfil é o mesmo para todos os membros da população que disparam a história.
O Trumania já traz alguns padrões interessantes, como o DefaultDailyTimerGenerator, que corresponde ao uso diário de telefone medido em um dataset real de produção:

Vamos instanciá-lo no seu cenário:
from trumania.components.time_patterns.profilers import DefaultDailyTimerGenerator
story_timer_gen = DefaultDailyTimerGenerator(
clock=example_circus.clock,
seed=next(example_circus.seeder))
Depois, temos os níveis de atividade dos membros da população para essa história, que definem quão ativo é cada usuário. Você pode definir níveis diferentes para cada membro, fazendo com que alguns disparem a ação mais ou menos vezes que outros.
Dá para especificar manualmente cada nível, mas aqui vamos usar um gerador aleatório que atribuirá um nível de atividade a cada membro.
Primeiro, definimos três níveis de atividade de, em média, 3, 10 e 20 disparos por dia:
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"))
Depois, criamos o gerador de atividade, usando Numpy com o método choice. Abaixo, 20% terão baixa atividade, 70% média e 10% alta:
activity_gen = NumpyRandomGenerator(
method="choice",
a=[low_activity, med_activity, high_activity],
p=[.2, .7, .1],
seed=next(example_circus.seeder))
Agora, use esse perfil de timer e o gerador de atividade na história:
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
O dataset resultante tem o mesmo schema de 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 |
+-------+-------------------+---------------------+---------------------------------------------------------+-------------------+---------------------+----------------------+
Dica: confira o snippet no Github
Análise do resultado
Os padrões de tempo incluídos na configuração do cenário devem estar visíveis na estrutura do dataset. Dá para demonstrar com uma análise simples sobre o resulting_dataset:
- o número de mensagens em cada hora do dia deve seguir a distribuição especificada (note a semelhança com a curva do
DefaultDailyTimerGeneratoracima):
time_profile = (
resulting_dataset[["MESSAGE", "TIME"]]
.groupby(by=example_5_df.TIME.dt.hour)["MESSAGE"]
.count()
)
time_profile.plot()

- o histograma de mensagens por usuário também deve respeitar sua configuração:
- você definiu
low_activitycomo 3 mensagens em média por dia,med_activitycomo 10 ehigh_activitycomo 20 e rodou a simulação por 5 dias. Isso deve gerar 3 grupos depersoncom cerca de 15, 50 e 100 mensagens totais — correspondendo aos 3 centros do histograma abaixo - você definiu as probabilidades desses níveis como .2, .7 e .1, o que também corresponde aproximadamente à área de cada "barriga" do histograma:
- você definiu
usage_per_user = resulting_dataset[["MESSAGE", "PERSON_ID"]].groupby("PERSON_ID")["MESSAGE"].count()
usage_per_user.plot(kind="hist")

A rede social
Objetivo
É possível melhorar ainda mais conectando as pessoas para que enviem mensagens aos amigos durante a execução da história, em vez de para alguém aleatório. Isso introduz muita estrutura ao dataset, pois o grafo social passa a ser descobrível a partir do log.
Como fazer
Você pode modelar a rede social com um Relationship novamente, o mesmo conceito usado para as citações. Lembre que o relacionamento quotes tinha um peso — que definia a probabilidade de cada citação. Aqui, use pesos para definir quais amigos têm maior ou menor chance de serem contatados.
Note que aqui estamos definindo um relacionamento de person para person, embora, em geral, o Trumania permita relacionamentos entre quaisquer populações.
Existem várias formas de gerar as arestas de um grafo social. Uma clássica é usar o algoritmo de Erdos-Renyi. No Trumania é especialmente fácil, pois já está embutido no framework. Basta injetá-lo no seu cenário para criar um relacionamento.
Aqui, vamos definir um grafo social entre os membros de person no qual cada pessoa tem 20 amigos. Note que um peso será adicionado automaticamente a esse relacionamento com uma distribuição de Pareto.
from trumania.components.social_networks.erdos_renyi import WithErdosRenyi
# self é o circus aqui; veja o código de exemplo no github para detalhes
self.add_er_social_network_relationship(
self.populations["person"],
relationship_name="friends",
average_degree=20)
Com esse relacionamento, vamos revisar a história. Agora, OTHER_PERSON não será mais escolhido aleatoriamente entre toda a população, mas sim entre os amigos de cada membro person, com probabilidade proporcional ao peso definido na rede 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
Mais uma vez, o dataset resultante tem o mesmo schema de antes — mas agora com ainda mais estrutura graças à sua rede 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 |
+-------+-------------------+---------------------+----------------------------------------------------+-------------------+-------------------+------------------+
Dica: dê uma olhada no snippet no Github
Análise do resultado
Uma forma simples de inspecionar essa rede social é contar o número de amigos únicos contatados por cada pessoa. Se você rodar a simulação por tempo suficiente, isso fornece um limite inferior do número real de pessoas na rede social de cada um (como os pesos seguem uma distribuição de Pareto, muitas amizades têm probabilidade muito baixa de serem acionadas — então é preciso uma simulação longa para observar todos os possíveis contatos).
Executamos o cenário por 15 dias simulados e obtivemos isto, centrado bem próximo e um pouco abaixo do grau médio de 20 definido no 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")

Tem mais!
Vários recursos do Trumania não foram descritos neste post:
- ter várias populações em um cenário
- ter várias histórias em um cenário
- adicionar dependências probabilísticas entre histórias (ex.: alguém liga para um amigo após ter recebido uma ligação)
- atualização de estado (ex.: reduzir saldo com base na duração da chamada)
- peças reutilizáveis de cenário, como geografias, mais perfis de timer, ...
- persistência de partes do cenário
Conclusão
Apresentamos o Trumania como uma biblioteca de geração de dados baseada em cenários em Python. Os datasets gerados podem ser usados em uma ampla variedade de aplicações, como testes, aprendizado e benchmarking.
Explicamos que, para testar corretamente uma aplicação ou algoritmo, precisamos de datasets que respeitem certas propriedades estatísticas esperadas. Ilustramos que o Trumania é capaz disso num exemplo em que geramos um dataset básico de "message log" que respeita a distribuição de mensagens por usuário, a distribuição por horário do dia e a distribuição de contatos em um grafo social.
Esperamos também ter mostrado que a flexibilidade dos componentes do Trumania permite criar uma grande variedade de cenários.
Um cenário do Trumania pode incluir relacionamentos entre entidades e ser configurado com distribuições aleatórias teóricas, distribuições empíricas observadas em um dataset real ou até dados de produção (por exemplo, um dataset de IDs de localização). Demonstramos isso reutilizando uma distribuição estatística de timestamps de uso de telefone observada em um dataset real para configurar o timer aleatório de tempo da nossa história.
Com isso em mente, mãos à obra! Acesse o Github do Trumania e escreva seu próprio cenário ;)
Se preferir, entre em contato com Svend Vanderveken ou com o coautor Milan van der Meer:
- Svend Vanderveken é engenheiro de software freelance com expertise em soluções de processamento de dados, principalmente em Kafka, Scala, SQL, Python... Acompanhe pelo blog svend.kelesia.com e no Twitter @sv3ndk.
- Milan van der Meer atualmente trabalha como engenheiro de software na Real Impact Analytics. Conecte-se com ele no LinkedIn
Nota do editor: este post foi escrito em colaboração com Milan van der Meer. Ambos os autores fazem parte da equipe da Real Impact Analytics, uma startup belga inovadora de big data que captura valor em dados de telecom ao "transformar big data em apps".
Este tutorial dá um gostinho do porquê você pode querer gerar conjuntos de dados aleatórios e o que esperar deles. Também vai guiá-lo por alguns primeiros exemplos de como usar o Trumania, uma biblioteca Python para geração de dados.
Para mais informações, visite o GitHub do Trumania!
Por que gerar conjuntos de dados aleatórios?
Gerar conjuntos de dados aleatórios é relevante tanto para data engineers quanto para data scientists. Por que esses profissionais trabalham com dados sintéticos e como eles os obtêm?
Como data engineer, depois de escrever sua nova e incrível aplicação de processamento de dados, chega a hora de testar ponta a ponta — e, para isso, você precisa de dados de entrada.
Como data scientist, você se beneficia da geração de dados porque ela permite experimentar diferentes formas de explorar datasets, algoritmos, técnicas de visualização de dados ou validar suposições sobre o comportamento de um método em muitos conjuntos de dados diferentes à sua escolha.
Nos dois casos, é tentador usar dados reais. O problema é que dados de produção costumam ser difíceis de obter, mesmo parcialmente — e isso não está ficando mais fácil com as novas leis europeias de privacidade e segurança.
Talvez você seja um dos sortudos e tenha um conjunto completo de dados de produção no seu laptop. Ainda assim, você provavelmente vai querer mais variabilidade para validar diferentes situações: tamanhos de população distintos, distribuições de dados variadas, problemas de qualidade como dados ausentes ou inválidos...
Para uma leitura mais completa sobre por que gerar dados aleatórios é útil, confira "Why synthetic data is about to become a major competitive advantage".
Geração de dados aleatórios baseada em esquema: precisamos de bons relacionamentos!
Esta seção tenta ilustrar a geração de dados aleatórios baseada em esquema e mostrar suas limitações.
Já existem muitas ferramentas para gerar datasets aleatórios. Uma abordagem comum é a geração baseada em esquema, que permite definir um blueprint e usá-lo para gerar entidades. Khermes e LogSynth são dois exemplos.
Um exemplo de configuração baseada em esquema poderia incluir este schema de pessoa:
{
{
"field": "Name",
"class": NameGenerator
},
{
"field": "Age",
"class": RandomInt(18, 65)
},
{
"field": "Gender",
"class": RandomPicker(["Male", "Female", "Other"])
}
}
Esse schema define a geração de alguns dados sobre uma pessoa: nome, idade e gênero. É simples, rápido — e tem limitações. Uma limitação importante é a dificuldade de simular relacionamentos entre diferentes entidades ou dependências temporais.
Por exemplo, como tornar alguns nomes mais prováveis dado o gênero? Ou como criar um log de ações de usuários em que certas ações são mais frequentes nos fins de semana?
A abordagem baseada em schema não facilita expor sua aplicação a especificidades do mundo real, como:
- Datasets desbalanceados (dificuldades em operações do tipo group by);
- Padrões de clusters na rede de entidades — por exemplo, descobrir grupos sociais via interações (como ligações telefônicas);
- Diferentes perfis de atividade no tempo — ações mais prováveis em certos horários do dia ou dias da semana;
- Relações causais entre ações — por exemplo, muitas compras em uma loja culminando em um evento de "estoque esgotado".
O Trumania é baseado em cenários para lidar com essas limitações e gerar datasets mais realistas. À medida que o cenário se desenrola, várias populações interagem entre si, atualizam propriedades e emitem logs.
Um exemplo de cenário seria pessoas ligando para amigos. Quando alguém liga, paga em minutos; quando os minutos acabam, precisa ir até uma loja... O dataset resultante pode ser o log de chamadas, o log de compras e o nível de estoque a cada hora. As ações no cenário podem ser aleatórias ou determinísticas.
No Trumania, os datasets gerados normalmente são séries temporais, pois resultam da execução de um cenário que se desenrola ao longo do tempo.
Gerando dados inter-relacionados com o Trumania (hora do tutorial!)
No exemplo abaixo, você verá como elaborar um cenário básico no qual várias pessoas enviam mensagens entre si. O dataset resultante será uma série temporal de mensagens trocadas entre pessoas.
A sequência de passos será:
- criar um circus do Trumania, que é o mundo onde o cenário é executado
- adicionar uma história "hello world" que produz logs básicos, hard-coded
- adicionar um relationship para atribuir citações preferidas a cada pessoa, usadas como conteúdo das mensagens
- parametrizar o tempo da história para obter uma distribuição de mensagens mais realista ao longo do dia
- adicionar outro relacionamento para definir uma rede social, deixando o grafo social que emerge do log mais realista
Criando um circus do Trumania
Objetivo
O primeiro passo é criar um circus, o mundo em que todos os elementos do cenário existirão. Também vamos criar uma população básica de persons. Como muitos aspectos do Trumania são aleatórios, este passo também introduz o conceito de generators, usados para controlar vários comportamentos randômicos.
Por enquanto, não vamos adicionar nenhuma história; tudo ficará estático.
Vamos lá!
Como fazer
Um circus do Trumania é criado assim:
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"))
No Trumania, todos os elementos relacionados ao tempo são controlados por um relógio central. A parte mais importante no trecho acima é step_duration=pd.Timedelta("1h"), que define que o relógio avança em passos de 1 hora.
Em seguida, adicione a população person ao circus. Uma população é essencialmente um conjunto de agentes com um id e alguns atributos.
É possível especificar manualmente os valores de cada atributo, mas, na maioria dos casos, você vai querer gerá-los aleatoriamente. Então, primeiro defina alguns generators para isso:
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))
Um generator do Trumania fornece dados quando seu método generate() é chamado. No exemplo, id_gen gera strings como PERSON_0001, PERSON_0002... age_gen amostra repetidamente de uma distribuição normal e name_gen fornece nomes de pessoas aleatórios.
Note que qualquer distribuição estatística do numpy e qualquer provider do Faker estão disponíveis no Trumania, e é fácil estender com novos.
Com isso, você pode adicionar uma população de 1000 pessoas com IDs sequenciais e dois atributos aleatórios 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
Você já pode ver os atributos gerados de todos os membros da população person chamando 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 |
+-------------------+-----------------+---------+
Dica: se quiser ver o script completo para essa população básica, confira este snippet no Github!
Declarações Hello World: criando histórias
Objetivo
Vamos deixar mais interessante adicionando uma story. Uma história no Trumania encapsula um aspecto dinâmico do cenário. Ela é executada sempre que o relógio simulado avança um passo.
Lembre que você definiu step_duration=pd.Timedelta("1h") no circus acima — isso implica que, a cada hora lógica, cada história pode ser executada.
Vamos criar uma história simples de hello world em que todos os membros da população person emitem regularmente a mensagem "hello world".
Como fazer
O primeiro ponto ao criar uma story é a população que a inicia; aqui, será person. O segundo é o timer, que define quando cada membro dispara a ação. Por ora, vamos fixá-lo em 1, o que implica que todos executarão a história a cada passo do relógio (criaremos timers mais elaborados depois):
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)
)
Até aqui, a história está vazia; precisamos adicionar algumas operações. Elas podem ser aleatórias ou determinísticas, ler/atualizar atributos de qualquer população ou outro estado do circus — e ainda ter efeitos colaterais.
Por enquanto, adicione duas operações determinísticas simples e um logger para realmente obter 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 gera um timestamp aleatório dentro do intervalo atual de 1 hora e ConstantGenerator produz valores fixos.
Resultado
Vamos rodar o circus por 48h de tempo simulado:
example_circus.run(
duration=pd.Timedelta("48h"),
log_output_folder="example_scenario",
delete_existing_logs=True
)
Isso deve produzir o dataset básico a seguir, no qual cada uma das 1000 pessoas disse "hello world" 48 vezes ao longo de 2 dias.
O primeiro campo, PERSON_ID, corresponde ao member_id_field da story. Os outros dois, TIME e MESSAGE, correspondem às duas operações principais que você colocou na história.
+-------+-------------------+---------------------+-------------+
| | 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 |
+-------+-------------------+---------------------+-------------+
Dica: veja o snippet completo no Github
Alguém para dizer Hello World
Objetivo
Vamos melhorar adicionando outra pessoa aleatória à conversa. Por enquanto, qualquer pessoa pode falar com qualquer outra com probabilidade igual — o que não é muito realista (adicionaremos uma rede social depois). Também vamos enriquecer o dataset adicionando o nome das pessoas com base no ID.
Como fazer
Vamos armazenar a outra pessoa em um novo campo OTHER_PERSON. Neste caso simples, queremos apenas preencher esse campo com uma seleção aleatória não ponderada da população person. Basta usar a operação aleatória select_one dessa população:
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
Ao rodar novamente, você terá um dataset com o novo 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 |
+-------+-------------------+---------------------+-------------+-------------------+---------------------+--------------------+
Dica: veja o código completo aqui
Você sempre diz isso
Objetivo
Vamos melhorar gerando uma frase aleatória no campo MESSAGE, em vez do hello world fixo. Isso permite ilustrar um uso básico de Relationship do Trumania.
Vamos associar a cada pessoa 4 citações. Sempre que ela emitir uma mensagem, o conteúdo será escolhido entre as 4 citações dela. Além disso, vamos associar a cada citação um weight diferente, que define sua probabilidade: uma citação com peso alto aparecerá com mais frequência que uma com peso baixo.
Como começar?
Como fazer
Primeiro, crie um gerador de citações, semelhante aos outros Faker que vimos:
quote_generator = FakerGenerator(method="sentence", nb_words=6, variable_nb_words=True,
seed=next(example_circus.seeder))
Você poderia usar esse gerador diretamente na story, mas vamos deixar mais interessante restringindo cada pessoa a usar sempre uma de suas 4 citações favoritas. Para isso, crie um Relationship entre os membros da população person e valores de frases.
Primeiro, crie um relacionamento vazio:
quotes_rel = example_circus.populations["person"].create_relationship("quotes")
Depois, povoe esse relacionamento com 4 citações para cada pessoa. No código abaixo, person.ids é uma Series do pandas com 1000 ids da população person, e quote_generator.generate(size=person.size) fornece outra Series também de tamanho 1000, com citações aleatórias.
Isso significa que cada passada do for abaixo adiciona 1000 relações: uma citação por usuário. As primeiras 1000 terão weight=1; as próximas, weight=2; e assim por diante...
for w in range(4):
quotes_rel.add_relations(
from_ids=person.ids,
to_ids=quote_generator.generate(size=person.size),
weights=w
)
Agora você pode substituir o ConstantGenerator na história por uma operação select_one nesse relacionamento. Em outras palavras: para cada valor em PERSON_ID, procure as relações em quotes que partem desse ID e selecione aleatoriamente um valor relacionado. Como não sobrescrevemos pesos abaixo, valem os definidos em 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
Ao rodar o circus, o campo MESSAGE agora deve conter uma citação aleatória:
+-------+-------------------+---------------------+-----------------------------------------------------+-------------------+---------------------+---------------------+
| | 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 |
+-------+-------------------+---------------------+-----------------------------------------------------+-------------------+---------------------+---------------------+
O ponto do relacionamento usado para as citações é que agora os campos do dataset gerado estão relacionados: algumas frases são usadas com maior frequência por certos usuários.
Dica: veja o snippet completo no Github
Não é o que você faz, é quando você faz
Objetivo
Até agora, cada membro da população person produz exatamente uma mensagem por hora. Vamos deixar isso mais realista.
O Trumania usa a combinação de 2 conceitos para controlar o tempo de execução de uma história: níveis de atividade e perfis de timer. Juntos, eles determinam a probabilidade de disparar a story dado um usuário específico e o horário do dia.
Como fazer
Vamos atualizar a story e parametrizar esses dois aspectos.
Primeiro, crie o perfil de timer da história, que controla os momentos em que mais ou menos ações são disparadas. Esse perfil é o mesmo para todos os membros da população que disparam a história.
O Trumania já traz alguns padrões interessantes, como o DefaultDailyTimerGenerator, que corresponde ao uso diário de telefone medido em um dataset real de produção:

Vamos instanciá-lo no seu cenário:
from trumania.components.time_patterns.profilers import DefaultDailyTimerGenerator
story_timer_gen = DefaultDailyTimerGenerator(
clock=example_circus.clock,
seed=next(example_circus.seeder))
Depois, temos os níveis de atividade dos membros da população para essa história, que definem quão ativo é cada usuário. Você pode definir níveis diferentes para cada membro, fazendo com que alguns disparem a ação mais ou menos vezes que outros.
Dá para especificar manualmente cada nível, mas aqui vamos usar um gerador aleatório que atribuirá um nível de atividade a cada membro.
Primeiro, definimos três níveis de atividade de, em média, 3, 10 e 20 disparos por dia:
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"))
Depois, criamos o gerador de atividade, usando Numpy com o método choice. Abaixo, 20% terão baixa atividade, 70% média e 10% alta:
activity_gen = NumpyRandomGenerator(
method="choice",
a=[low_activity, med_activity, high_activity],
p=[.2, .7, .1],
seed=next(example_circus.seeder))
Agora, use esse perfil de timer e o gerador de atividade na história:
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
O dataset resultante tem o mesmo schema de 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 |
+-------+-------------------+---------------------+---------------------------------------------------------+-------------------+---------------------+----------------------+
Dica: confira o snippet no Github
Análise do resultado
Os padrões de tempo incluídos na configuração do cenário devem estar visíveis na estrutura do dataset. Dá para demonstrar com uma análise simples sobre o resulting_dataset:
- o número de mensagens em cada hora do dia deve seguir a distribuição especificada (note a semelhança com a curva do
DefaultDailyTimerGeneratoracima):
time_profile = (
resulting_dataset[["MESSAGE", "TIME"]]
.groupby(by=example_5_df.TIME.dt.hour)["MESSAGE"]
.count()
)
time_profile.plot()

- o histograma de mensagens por usuário também deve respeitar sua configuração:
- você definiu
low_activitycomo 3 mensagens em média por dia,med_activitycomo 10 ehigh_activitycomo 20 e rodou a simulação por 5 dias. Isso deve gerar 3 grupos depersoncom cerca de 15, 50 e 100 mensagens totais — correspondendo aos 3 centros do histograma abaixo - você definiu as probabilidades desses níveis como .2, .7 e .1, o que também corresponde aproximadamente à área de cada "barriga" do histograma:
- você definiu
usage_per_user = resulting_dataset[["MESSAGE", "PERSON_ID"]].groupby("PERSON_ID")["MESSAGE"].count()
usage_per_user.plot(kind="hist")

A rede social
Objetivo
É possível melhorar ainda mais conectando as pessoas para que enviem mensagens aos amigos durante a execução da história, em vez de para alguém aleatório. Isso introduz muita estrutura ao dataset, pois o grafo social passa a ser descobrível a partir do log.
Como fazer
Você pode modelar a rede social com um Relationship novamente, o mesmo conceito usado para as citações. Lembre que o relacionamento quotes tinha um peso — que definia a probabilidade de cada citação. Aqui, use pesos para definir quais amigos têm maior ou menor chance de serem contatados.
Note que aqui estamos definindo um relacionamento de person para person, embora, em geral, o Trumania permita relacionamentos entre quaisquer populações.
Existem várias formas de gerar as arestas de um grafo social. Uma clássica é usar o algoritmo de Erdos-Renyi. No Trumania é especialmente fácil, pois já está embutido no framework. Basta injetá-lo no seu cenário para criar um relacionamento.
Aqui, vamos definir um grafo social entre os membros de person no qual cada pessoa tem 20 amigos. Note que um peso será adicionado automaticamente a esse relacionamento com uma distribuição de Pareto.
from trumania.components.social_networks.erdos_renyi import WithErdosRenyi
# self é o circus aqui; veja o código de exemplo no github para detalhes
self.add_er_social_network_relationship(
self.populations["person"],
relationship_name="friends",
average_degree=20)
Com esse relacionamento, vamos revisar a história. Agora, OTHER_PERSON não será mais escolhido aleatoriamente entre toda a população, mas sim entre os amigos de cada membro person, com probabilidade proporcional ao peso definido na rede 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
Mais uma vez, o dataset resultante tem o mesmo schema de antes — mas agora com ainda mais estrutura graças à sua rede 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 |
+-------+-------------------+---------------------+----------------------------------------------------+-------------------+-------------------+------------------+
Dica: dê uma olhada no snippet no Github
Análise do resultado
Uma forma simples de inspecionar essa rede social é contar o número de amigos únicos contatados por cada pessoa. Se você rodar a simulação por tempo suficiente, isso fornece um limite inferior do número real de pessoas na rede social de cada um (como os pesos seguem uma distribuição de Pareto, muitas amizades têm probabilidade muito baixa de serem acionadas — então é preciso uma simulação longa para observar todos os possíveis contatos).
Executamos o cenário por 15 dias simulados e obtivemos isto, centrado bem próximo e um pouco abaixo do grau médio de 20 definido no 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")

Tem mais!
Vários recursos do Trumania não foram descritos neste post:
- ter várias populações em um cenário
- ter várias histórias em um cenário
- adicionar dependências probabilísticas entre histórias (ex.: alguém liga para um amigo após ter recebido uma ligação)
- atualização de estado (ex.: reduzir saldo com base na duração da chamada)
- peças reutilizáveis de cenário, como geografias, mais perfis de timer, ...
- persistência de partes do cenário
Conclusão
Apresentamos o Trumania como uma biblioteca de geração de dados baseada em cenários em Python. Os datasets gerados podem ser usados em uma ampla variedade de aplicações, como testes, aprendizado e benchmarking.
Explicamos que, para testar corretamente uma aplicação ou algoritmo, precisamos de datasets que respeitem certas propriedades estatísticas esperadas. Ilustramos que o Trumania é capaz disso num exemplo em que geramos um dataset básico de "message log" que respeita a distribuição de mensagens por usuário, a distribuição por horário do dia e a distribuição de contatos em um grafo social.
Esperamos também ter mostrado que a flexibilidade dos componentes do Trumania permite criar uma grande variedade de cenários.
Um cenário do Trumania pode incluir relacionamentos entre entidades e ser configurado com distribuições aleatórias teóricas, distribuições empíricas observadas em um dataset real ou até dados de produção (por exemplo, um dataset de IDs de localização). Demonstramos isso reutilizando uma distribuição estatística de timestamps de uso de telefone observada em um dataset real para configurar o timer aleatório de tempo da nossa história.
Com isso em mente, mãos à obra! Acesse o Github do Trumania e escreva seu próprio cenário ;)
Se preferir, entre em contato com Svend Vanderveken ou com o coautor Milan van der Meer:
- Svend Vanderveken é engenheiro de software freelance com expertise em soluções de processamento de dados, principalmente em Kafka, Scala, SQL, Python... Acompanhe pelo blog svend.kelesia.com e no Twitter @sv3ndk.
- Milan van der Meer atualmente trabalha como engenheiro de software na Real Impact Analytics. Conecte-se com ele no LinkedIn