编者按:本文与 Milan van der Meer 合作撰写。两位作者均来自 Real Impact Analytics 团队,这是一家创新性的比利时大数据初创公司,通过“将大数据应用化”来挖掘电信数据的价值。
本教程将简要介绍为何需要生成随机数据集,以及可以从中获得什么。同时,我们还将通过一些入门示例带您上手 Trumania——一个用于数据生成的 Python 库。
欲了解更多信息,欢迎访问 Trumania 的 GitHub!
为什么要生成随机数据集?
生成随机数据集对数据工程师和数据科学家都很有意义。为什么他们要使用合成数据,又是如何获取这些数据的?
作为数据工程师,当您完成了一款出色的数据处理应用后,就该开始进行端到端测试了,因此需要一些输入数据。
作为数据科学家,数据生成可以让您在探索数据集、算法、数据可视化技术的不同方法上进行实验,或针对您选择的多种不同数据集来验证对某种方法行为的假设。
两种情况下,一个很诱人的选项是直接使用真实数据。但问题在于,生产数据通常很难获取,即使是部分数据也不容易。随着欧洲隐私与安全法规的更新,获取难度还在加大。
也许您很幸运,笔记本上就有一份完整的生产数据集。即便如此,您仍可能希望数据集具备更多可变性,以便验证不同场景,例如不同的人群规模、不同的数据分布、各种数据质量问题(如缺失或无效数据)等。
想更系统地了解生成随机数据集的价值,请阅读 “为什么合成数据即将成为重大竞争优势”。
基于模式的随机数据生成:关系要“过硬”!
本节将展示一种基于模式(schema-based)的随机数据生成方式,并指出其局限。
目前已有许多工具可用于生成随机数据集。其中一类常见方法是基于模式的生成,允许您先定义“蓝图”,再据此生成实体。Khermes 和 LogSynth 就是这种工具的两个示例。
例如,一个基于模式的配置可能包含如下的人物(person)模式:
{
{
"field": "Name",
"class": NameGenerator
},
{
"field": "Age",
"class": RandomInt(18, 65)
},
{
"field": "Gender",
"class": RandomPicker(["Male", "Female", "Other"])
}
}
该模式定义了一个人的一些数据:姓名、年龄和性别。它简单、快速,但也有局限。一个主要局限是很难模拟不同实体之间或基于时间的真实关系。
例如,如何让某些名字更可能出现在特定性别中?又或者,如何创建用户行为日志,使得某些操作在周末更容易发生?
基于模式的方法并不容易让您的应用暴露在真实世界的特性或难点之下,例如:
- 不平衡数据集(对分组聚合类操作会有挑战);
- 实体网络中的聚类模式。即通过用户交互(例如电话呼叫)发现社交群体;
- 多样的时间活动画像。某些行为在一天或一周中的特定时间更可能发生;
- 行为之间的因果关系。比如,门店内频繁购买行为最终触发“缺货”事件。
Trumania 以场景(scenario)为基础,旨在解决这些问题并生成更逼真的数据集。随着场景推进,不同人群会彼此互动、更新属性并产生日志。
一个示例场景是人们打电话给朋友。打电话会消耗分钟数,分钟数用完就得去商店充值……生成的数据集可以是用户通话日志、购物行为日志,以及每小时的库存水平。场景中的动作既可以是随机的,也可以是确定的。
在 Trumania 中,生成的数据集通常是时间序列,因为它们来自于随时间展开的场景执行。
使用 Trumania 生成相互关联的数据(教程时间!)
下面的示例中,您将看到如何构建一个基础场景,让人们彼此发送消息。生成的数据集将是人与人之间消息往来的时间序列。
我们将按以下步骤进行:
- 创建一个 Trumania 的 circus(马戏团),这是场景运行的“世界”
- 添加一个“hello world”故事(story),产出基本的硬编码日志
- 添加一个关系(relationship),给每个人分配一些最爱引用语,用于生成消息内容
- 为故事配置时间参数,使消息在一天内的分布更加真实
- 再添加一个关系来定义社交网络,使由消息日志所呈现的社交图更逼真
创建 Trumania Circus
目的
第一步是创建一个 circus,即所有场景元素存在的世界。我们还将创建一个基础的 persons 人群。由于 Trumania 的许多方面都是随机的,这一步还将介绍 generators(生成器)的概念,用于控制多种随机行为。
此时我们不添加任何场景逻辑,一切都是静态的。
开始吧!
如何操作
Trumania 的 circus 可按如下方式创建:
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"))
在 Trumania 中,所有与时间相关的元素都由一个中心时钟控制。上面代码片段中最重要的是 step_duration=pd.Timedelta("1h"),它定义了时钟每次以 1 小时为步长递增。
接下来,将 person 人群添加到 circus 中。人群本质上是一组具有 id 和若干属性的智能体。
每个属性的值可以手工指定,但多数情况下您会希望随机生成,因此先定义几个用于此目的的 generators:
from trumania.core.random_generators import SequencialGenerator, FakerGenerator, NumpyRandomGenerator
id_gen = SequencialGenerator(prefix="PERSON_")
age_gen = NumpyRandomGenerator(method="normal", loc=3, scale=5,
seed=next(example_circus.seeder))
name_gen = FakerGenerator(method="name", seed=next(example_circus.seeder))
Trumania 生成器在其 generate() 方法被调用时负责提供数据。上例中,id_gen 会生成类似 PERSON_0001、PERSON_0002 的字符串;age_gen 会反复从正态分布中采样;name_gen 则提供随机人名。
请注意,来自 numpy 的任意统计分布以及任意 Faker 提供器在 Trumania 中都可用,且可轻松扩展。
准备就绪后,您可以添加一个规模为 1000 的 person 人群,使用顺序 ID,并包含两个基础的随机属性:
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)
结果
您可以通过调用 person.to_dataframe() 来查看 person 人群所有成员生成的属性。
+-------------------+-----------------+---------+
| | 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 |
+-------------------+-----------------+---------+
提示:想看该基础用户人群的完整脚本,请查看这段 Github 代码片段!
Hello World 语句:创建故事
目的
接下来,通过添加一个 story 让内容更有趣。Trumania 的 story 本质上是封装某些动态行为的场景片段。每当模拟时钟前进一个步长,就会执行一次 story。
回想一下,您在上面的 circus 中设置了 step_duration=pd.Timedelta("1h"),这意味着每个逻辑小时,每个 story 都有机会被执行。
我们将创建一个简单的 hello-world 故事,其中 person 人群的所有成员会定期发出“hello world”消息。
如何操作
创建 story 时首先要指定的,是触发该 story 的人群。在这里就是 person。其次是定义 timer,确定人群中每个成员何时触发该动作。当前我们将其硬编码为 1,表示每个时钟步长所有成员都会执行该 story(稍后我们会构建更复杂的计时器):
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)
)
目前 story 是空的,什么也不做:您需要向其添加一些操作。操作可以是随机或确定的,它们可以读取和更新任何人群的属性或 circus 中的其他状态,甚至产生副作用。
现在,我们添加两个简单的确定性操作和一个日志记录器来生成日志:
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 会在当前 1 小时时间区间内生成一个随机时间戳,而 ConstantGenerator 则产出固定的硬编码值。
结果
让 circus 运行 48 小时的模拟时间:
example_circus.run(
duration=pd.Timedelta("48h"),
log_output_folder="example_scenario",
delete_existing_logs=True
)
这将生成如下基础数据集:每位 1000 名用户在 2 天内各说了 48 次“hello world”。
结果数据集的首列 PERSON_ID 对应 story 的 member_id_field。另外两列 TIME 与 MESSAGE 分别对应您在故事中设置的两项主要操作。
+-------+-------------------+---------------------+-------------+
| | 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 |
+-------+-------------------+---------------------+-------------+
提示:完整代码见 Github
把 Hello World 说给谁听
目的
我们再改进一下,引入另一位随机人物参与对话。当前先假设任何人都以相同概率与任意他人对话(这并不太真实,稍后我们会加入社交网络)。此外,我们将根据 ID 补充姓名信息,丰富数据集。
如何操作
假设另一位人物存放在新字段 OTHER_PERSON 中。在这个简化情形中,您只需从 person 人群中进行一次无权重的随机选择,并将结果放入该字段。可以直接使用该人群的 select_one 随机操作:
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")
)
结果
再次运行 circus,您将得到包含新字段 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 |
+-------+-------------------+---------------------+-------------+-------------------+---------------------+--------------------+
提示:完整代码见 这里
“你总这么说”
目的
进一步改进:在 MESSAGE 字段中生成随机句子,而不是无聊的硬编码 hello world。这将展示 Trumania Relationship 的一个基础用法。
我们将为每个人关联 4 条引用语。每当某人发送消息时,内容将从其 4 条引用语中选取。此外,我们会为每条引用语关联不同的 weight(权重),以定义其被选中的可能性:权重高的引用语会比权重低的更常被其所有者使用。
如何开始?
如何操作
第一步是创建引用语生成器,与上文所见的其他 Faker 生成器类似:
quote_generator = FakerGenerator(method="sentence", nb_words=6, variable_nb_words=True,
seed=next(example_circus.seeder))
您当然可以像之前那样将该生成器直接用于 story,但我们不妨更有趣一点:限制每个人只能使用其 4 条最爱引用语之一。为此,创建 person 人群成员与句子值之间的 Relationship。
首先,创建一个空关系:
quotes_rel = example_circus.populations["person"].create_relationship("quotes")
然后,为每个人填充 4 条引用语。下面的代码中,person.ids 是大小为 1000 的 pandas Series,包含 person 人群的所有 id;quote_generator.generate(size=person.size) 会再生成一个同为 1000 大小的 pandas Series,其中是随机引用语。
这意味着下方 for 循环的每一次迭代都会在关系中填充 1000 条关联:为 1000 名用户各添加 1 条引用语。第一批 1000 条引用语的权重设为 1,下一批的权重设为 2,从而让它们更易被选中,依此类推……
for w in range(4):
quotes_rel.add_relations(
from_ids=person.ids,
to_ids=quote_generator.generate(size=person.size),
weights=w
)
现在,您可以将 story 中的 ConstantGenerator 替换为对该关系的 select_one 操作。含义如下:对字段 PERSON_ID 中的每个值,查找 quotes 中从该 ID 出发的关系,并随机选择一个关联值。由于下面的操作没有覆盖权重,将使用上面 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")
)
结果
运行 circus 后,MESSAGE 字段现在应包含随机引用语:
+-------+-------------------+---------------------+-----------------------------------------------------+-------------------+---------------------+---------------------+
| | 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 |
+-------+-------------------+---------------------+-----------------------------------------------------+-------------------+---------------------+---------------------+
使用该引用语关系的意义在于:现在生成数据集中的字段彼此有关联——某些句子更常被特定用户使用。
提示:查看 Github 上的完整代码片段
做什么不如什么时候做
目的
目前,person 人群的每位成员每天每小时都会发送恰好一条消息。我们来让它更贴近现实。
Trumania 使用“活动水平(activity levels)”与“计时器画像(timer profiles)”两种概念的组合来控制故事执行的时间维度。两者共同决定在特定用户与一天中的特定时间触发 story 的概率。
如何操作
我们将更新 story 并对这两个方面进行参数化。
首先创建故事的计时器画像,它控制在何时更可能或更不可能触发动作。该画像对触发该 story 的所有人群成员相同。
Trumania 内置了一些有趣的默认画像,例如 DefaultDailyTimerGenerator,它对应我们在真实生产数据集中测得的日常手机使用情况:

在您的场景中实例化一个:
from trumania.components.time_patterns.profilers import DefaultDailyTimerGenerator
story_timer_gen = DefaultDailyTimerGenerator(
clock=example_circus.clock,
seed=next(example_circus.seeder))
其次,为该 story 配置人群成员的活动水平,定义每位用户在该 story 中的活跃程度。您可以为每个成员设定不同水平,从而让某些成员更频繁或更少地触发该动作。
当然也可以手动指定每个水平,但这里我们使用一个随机生成器,为每位成员分配一个活动水平。
首先,定义三个活动水平,平均每天触发 3、10、20 次:
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"))
然后创建实际的活动生成器,它仍然是一个基于 choice 方法的 Numpy 生成器。下面配置表示:为 20% 的人群分配低活动、70% 分配中活动、10% 分配高活动:
activity_gen = NumpyRandomGenerator(
method="choice",
a=[low_activity, med_activity, high_activity],
p=[.2, .7, .1],
seed=next(example_circus.seeder))
现在即可将该计时器画像与活动生成器用于您的 story:
hello_world = example_circus.create_story(
name="hello_world",
initiating_population=example_circus.populations["person"],
member_id_field="PERSON_ID",
timer_gen=story_timer_gen,
activity_gen=activity_gen
)
结果
结果数据集的模式与之前相同:
+-------+-------------------+---------------------+---------------------------------------------------------+-------------------+---------------------+----------------------+
| | 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 |
+-------+-------------------+---------------------+---------------------------------------------------------+-------------------+---------------------+----------------------+
提示:查看 Github 代码片段
结果分析
您在场景配置中引入的时间模式现在应能在结果数据集的内部结构中体现。可以通过对 resulting_dataset 进行基础分析来验证:
- 一天中各小时的消息数量应符合指定分布(注意它与上文
DefaultDailyTimerGenerator曲线的相似性):
time_profile = (
resulting_dataset[["MESSAGE", "TIME"]]
.groupby(by=example_5_df.TIME.dt.hour)["MESSAGE"]
.count()
)
time_profile.plot()

- 每位用户消息数量的直方图同样应符合配置:
- 您为
low_activity配置了日均 3 条消息、med_activity为 10 条、high_activity为 20 条,并运行了 5 天的模拟。应产生大致 3 组person,总消息数约为 15、50 与 100,分别对应下图直方图的 3 个中心; - 您将这些活动水平的概率配置为 .2、.7、.1,这也大致对应了直方图中各“隆起”的相对面积:
- 您为
usage_per_user = resulting_dataset[["MESSAGE", "PERSON_ID"]].groupby("PERSON_ID")["MESSAGE"].count()
usage_per_user.plot(kind="hist")

社交网络
目的
您可以进一步改进场景:让人们在故事执行过程中给朋友发消息,而不是随机找人。这样会为结果数据集引入大量结构化信息,因为现在可以从消息日志中挖掘出社交图。
如何操作
我们仍然使用 Relationship 来建模社交网络,与上文引用语的建模方式相同。回忆一下,quotes 关系设有权重,用于定义在给定人物下哪条引用语更可能出现。在这里也同理,用权重定义哪些朋友更可能被呼叫。
需要注意,这里我们定义的是从 person 到 person 的关系,但一般而言,Trumania 允许在任意两个人群之间定义关系。
生成社交图边的方式很多。一种基础而经典的方法是使用 Erdos-Renyi 算法。在 Trumania 中这尤其简单,因为它已经内置于框架中。您只需将其引入场景以创建关系。
在此,我们将为 person 成员之间定义一个社交图,使每个人大约有 20 个朋友。注意,关系会自动附加一个符合 帕累托分布的权重。
from trumania.components.social_networks.erdos_renyi import WithErdosRenyi
# self is the circus here, see example code on github for details
self.add_er_social_network_relationship(
self.populations["person"],
relationship_name="friends",
average_degree=20)
关系就位后,我们再次回到 story。这一次,OTHER_PERSON 将不再从整个人群中随机选择,而是从每个 person 成员的朋友中选择,且选择概率与社交网络中定义的权重成正比:
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")
)
结果
结果数据集的模式仍与之前相同,不过由于引入了基础的社交网络,现在的结构性更强:
+-------+-------------------+---------------------+----------------------------------------------------+-------------------+-------------------+------------------+
| | 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 |
+-------+-------------------+---------------------+----------------------------------------------------+-------------------+-------------------+------------------+
提示:可查看 Github 代码片段
结果分析
一种非常粗略的观察社交网络的方法,是统计每人拨打过电话的唯一朋友数量。如果模拟运行足够久,这个数值应能提供每人社交网络真实规模的下界(由于我们使用了帕累托分布的权重,许多人有极不可能被呼叫到的朋友,因此需要运行很久才能真正观察到所有可能被呼叫的人)。
我们将场景运行了 15 天的模拟时间,得到如下结果:其中心位置非常接近且略低于您在社交图中设定的平均度 20:
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")

远不止这些!
本文未涵盖的 Trumania 特性还包括:
- 在同一场景中使用多个人人群
- 在同一场景中包含多个故事
- 为故事之间添加概率依赖(例如:某人被呼叫后再回拨朋友)
- 状态更新(例如:根据通话时长减少余额)
- 可复用的场景片段,如地理、更多计时器画像等
- 场景片段的持久化
结语
我们介绍了 Trumania 作为一个基于场景的 Python 数据生成库。生成的数据集可用于测试、学习与基准评测等多种场景。
我们说明了要恰当地测试应用或算法,就需要具备符合预期统计性质的数据集。我们以一个基础的“消息日志”数据集为例,展示了 Trumania 能生成符合每用户消息数量分布、一天内时间分布,以及社交图中呼叫分布的数据。
我们也希望展示出,Trumania 组件的灵活性可以支持构建各种各样的场景。
Trumania 场景可以包含实体之间的关系,并能使用理论随机分布、真实数据集中观测到的经验分布,甚至真实产线数据(如位置 ID 数据集等)进行配置。我们通过复用真实数据集中观测到的手机使用时间戳分布来配置故事的时间随机计时器,演示了这一点。
心动不如行动!访问 Trumania 的 Github,编写您自己的场景吧;)
另外,欢迎联系作者 Svend Vanderveken 或合著者 Milan van der Meer:
- Svend Vanderveken 是一名自由职业软件工程师,专长于数据处理解决方案,主要涉及 Kafka、Scala、SQL、Python 等。通过其博客 svend.kelesia.com 与推特 @sv3ndk 关注他。
- Milan van der Meer 目前在 Real Impact Analytics 担任软件工程师。欢迎在 LinkedIn 上与他建立联系。
编者按:本文与 Milan van der Meer 合作撰写。两位作者均来自 Real Impact Analytics 团队,这是一家创新性的比利时大数据初创公司,通过“将大数据应用化”来挖掘电信数据的价值。
本教程将简要介绍为何需要生成随机数据集,以及可以从中获得什么。同时,我们还将通过一些入门示例带您上手 Trumania——一个用于数据生成的 Python 库。
欲了解更多信息,欢迎访问 Trumania 的 GitHub!
为什么要生成随机数据集?
生成随机数据集对数据工程师和数据科学家都很有意义。为什么他们要使用合成数据,又是如何获取这些数据的?
作为数据工程师,当您完成了一款出色的数据处理应用后,就该开始进行端到端测试了,因此需要一些输入数据。
作为数据科学家,数据生成可以让您在探索数据集、算法、数据可视化技术的不同方法上进行实验,或针对您选择的多种不同数据集来验证对某种方法行为的假设。
两种情况下,一个很诱人的选项是直接使用真实数据。但问题在于,生产数据通常很难获取,即使是部分数据也不容易。随着欧洲隐私与安全法规的更新,获取难度还在加大。
也许您很幸运,笔记本上就有一份完整的生产数据集。即便如此,您仍可能希望数据集具备更多可变性,以便验证不同场景,例如不同的人群规模、不同的数据分布、各种数据质量问题(如缺失或无效数据)等。
想更系统地了解生成随机数据集的价值,请阅读 “为什么合成数据即将成为重大竞争优势”。
基于模式的随机数据生成:关系要“过硬”!
本节将展示一种基于模式(schema-based)的随机数据生成方式,并指出其局限。
目前已有许多工具可用于生成随机数据集。其中一类常见方法是基于模式的生成,允许您先定义“蓝图”,再据此生成实体。Khermes 和 LogSynth 就是这种工具的两个示例。
例如,一个基于模式的配置可能包含如下的人物(person)模式:
{
{
"field": "Name",
"class": NameGenerator
},
{
"field": "Age",
"class": RandomInt(18, 65)
},
{
"field": "Gender",
"class": RandomPicker(["Male", "Female", "Other"])
}
}
该模式定义了一个人的一些数据:姓名、年龄和性别。它简单、快速,但也有局限。一个主要局限是很难模拟不同实体之间或基于时间的真实关系。
例如,如何让某些名字更可能出现在特定性别中?又或者,如何创建用户行为日志,使得某些操作在周末更容易发生?
基于模式的方法并不容易让您的应用暴露在真实世界的特性或难点之下,例如:
- 不平衡数据集(对分组聚合类操作会有挑战);
- 实体网络中的聚类模式。即通过用户交互(例如电话呼叫)发现社交群体;
- 多样的时间活动画像。某些行为在一天或一周中的特定时间更可能发生;
- 行为之间的因果关系。比如,门店内频繁购买行为最终触发“缺货”事件。
Trumania 以场景(scenario)为基础,旨在解决这些问题并生成更逼真的数据集。随着场景推进,不同人群会彼此互动、更新属性并产生日志。
一个示例场景是人们打电话给朋友。打电话会消耗分钟数,分钟数用完就得去商店充值……生成的数据集可以是用户通话日志、购物行为日志,以及每小时的库存水平。场景中的动作既可以是随机的,也可以是确定的。
在 Trumania 中,生成的数据集通常是时间序列,因为它们来自于随时间展开的场景执行。
使用 Trumania 生成相互关联的数据(教程时间!)
下面的示例中,您将看到如何构建一个基础场景,让人们彼此发送消息。生成的数据集将是人与人之间消息往来的时间序列。
我们将按以下步骤进行:
- 创建一个 Trumania 的 circus(马戏团),这是场景运行的“世界”
- 添加一个“hello world”故事(story),产出基本的硬编码日志
- 添加一个关系(relationship),给每个人分配一些最爱引用语,用于生成消息内容
- 为故事配置时间参数,使消息在一天内的分布更加真实
- 再添加一个关系来定义社交网络,使由消息日志所呈现的社交图更逼真
创建 Trumania Circus
目的
第一步是创建一个 circus,即所有场景元素存在的世界。我们还将创建一个基础的 persons 人群。由于 Trumania 的许多方面都是随机的,这一步还将介绍 generators(生成器)的概念,用于控制多种随机行为。
此时我们不添加任何场景逻辑,一切都是静态的。
开始吧!
如何操作
Trumania 的 circus 可按如下方式创建:
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"))
在 Trumania 中,所有与时间相关的元素都由一个中心时钟控制。上面代码片段中最重要的是 step_duration=pd.Timedelta("1h"),它定义了时钟每次以 1 小时为步长递增。
接下来,将 person 人群添加到 circus 中。人群本质上是一组具有 id 和若干属性的智能体。
每个属性的值可以手工指定,但多数情况下您会希望随机生成,因此先定义几个用于此目的的 generators:
from trumania.core.random_generators import SequencialGenerator, FakerGenerator, NumpyRandomGenerator
id_gen = SequencialGenerator(prefix="PERSON_")
age_gen = NumpyRandomGenerator(method="normal", loc=3, scale=5,
seed=next(example_circus.seeder))
name_gen = FakerGenerator(method="name", seed=next(example_circus.seeder))
Trumania 生成器在其 generate() 方法被调用时负责提供数据。上例中,id_gen 会生成类似 PERSON_0001、PERSON_0002 的字符串;age_gen 会反复从正态分布中采样;name_gen 则提供随机人名。
请注意,来自 numpy 的任意统计分布以及任意 Faker 提供器在 Trumania 中都可用,且可轻松扩展。
准备就绪后,您可以添加一个规模为 1000 的 person 人群,使用顺序 ID,并包含两个基础的随机属性:
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)
结果
您可以通过调用 person.to_dataframe() 来查看 person 人群所有成员生成的属性。
+-------------------+-----------------+---------+
| | 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 |
+-------------------+-----------------+---------+
提示:想看该基础用户人群的完整脚本,请查看这段 Github 代码片段!
Hello World 语句:创建故事
目的
接下来,通过添加一个 story 让内容更有趣。Trumania 的 story 本质上是封装某些动态行为的场景片段。每当模拟时钟前进一个步长,就会执行一次 story。
回想一下,您在上面的 circus 中设置了 step_duration=pd.Timedelta("1h"),这意味着每个逻辑小时,每个 story 都有机会被执行。
我们将创建一个简单的 hello-world 故事,其中 person 人群的所有成员会定期发出“hello world”消息。
如何操作
创建 story 时首先要指定的,是触发该 story 的人群。在这里就是 person。其次是定义 timer,确定人群中每个成员何时触发该动作。当前我们将其硬编码为 1,表示每个时钟步长所有成员都会执行该 story(稍后我们会构建更复杂的计时器):
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)
)
目前 story 是空的,什么也不做:您需要向其添加一些操作。操作可以是随机或确定的,它们可以读取和更新任何人群的属性或 circus 中的其他状态,甚至产生副作用。
现在,我们添加两个简单的确定性操作和一个日志记录器来生成日志:
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 会在当前 1 小时时间区间内生成一个随机时间戳,而 ConstantGenerator 则产出固定的硬编码值。
结果
让 circus 运行 48 小时的模拟时间:
example_circus.run(
duration=pd.Timedelta("48h"),
log_output_folder="example_scenario",
delete_existing_logs=True
)
这将生成如下基础数据集:每位 1000 名用户在 2 天内各说了 48 次“hello world”。
结果数据集的首列 PERSON_ID 对应 story 的 member_id_field。另外两列 TIME 与 MESSAGE 分别对应您在故事中设置的两项主要操作。
+-------+-------------------+---------------------+-------------+
| | 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 |
+-------+-------------------+---------------------+-------------+
提示:完整代码见 Github
把 Hello World 说给谁听
目的
我们再改进一下,引入另一位随机人物参与对话。当前先假设任何人都以相同概率与任意他人对话(这并不太真实,稍后我们会加入社交网络)。此外,我们将根据 ID 补充姓名信息,丰富数据集。
如何操作
假设另一位人物存放在新字段 OTHER_PERSON 中。在这个简化情形中,您只需从 person 人群中进行一次无权重的随机选择,并将结果放入该字段。可以直接使用该人群的 select_one 随机操作:
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")
)
结果
再次运行 circus,您将得到包含新字段 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 |
+-------+-------------------+---------------------+-------------+-------------------+---------------------+--------------------+
提示:完整代码见 这里
“你总这么说”
目的
进一步改进:在 MESSAGE 字段中生成随机句子,而不是无聊的硬编码 hello world。这将展示 Trumania Relationship 的一个基础用法。
我们将为每个人关联 4 条引用语。每当某人发送消息时,内容将从其 4 条引用语中选取。此外,我们会为每条引用语关联不同的 weight(权重),以定义其被选中的可能性:权重高的引用语会比权重低的更常被其所有者使用。
如何开始?
如何操作
第一步是创建引用语生成器,与上文所见的其他 Faker 生成器类似:
quote_generator = FakerGenerator(method="sentence", nb_words=6, variable_nb_words=True,
seed=next(example_circus.seeder))
您当然可以像之前那样将该生成器直接用于 story,但我们不妨更有趣一点:限制每个人只能使用其 4 条最爱引用语之一。为此,创建 person 人群成员与句子值之间的 Relationship。
首先,创建一个空关系:
quotes_rel = example_circus.populations["person"].create_relationship("quotes")
然后,为每个人填充 4 条引用语。下面的代码中,person.ids 是大小为 1000 的 pandas Series,包含 person 人群的所有 id;quote_generator.generate(size=person.size) 会再生成一个同为 1000 大小的 pandas Series,其中是随机引用语。
这意味着下方 for 循环的每一次迭代都会在关系中填充 1000 条关联:为 1000 名用户各添加 1 条引用语。第一批 1000 条引用语的权重设为 1,下一批的权重设为 2,从而让它们更易被选中,依此类推……
for w in range(4):
quotes_rel.add_relations(
from_ids=person.ids,
to_ids=quote_generator.generate(size=person.size),
weights=w
)
现在,您可以将 story 中的 ConstantGenerator 替换为对该关系的 select_one 操作。含义如下:对字段 PERSON_ID 中的每个值,查找 quotes 中从该 ID 出发的关系,并随机选择一个关联值。由于下面的操作没有覆盖权重,将使用上面 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")
)
结果
运行 circus 后,MESSAGE 字段现在应包含随机引用语:
+-------+-------------------+---------------------+-----------------------------------------------------+-------------------+---------------------+---------------------+
| | 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 |
+-------+-------------------+---------------------+-----------------------------------------------------+-------------------+---------------------+---------------------+
使用该引用语关系的意义在于:现在生成数据集中的字段彼此有关联——某些句子更常被特定用户使用。
提示:查看 Github 上的完整代码片段
做什么不如什么时候做
目的
目前,person 人群的每位成员每天每小时都会发送恰好一条消息。我们来让它更贴近现实。
Trumania 使用“活动水平(activity levels)”与“计时器画像(timer profiles)”两种概念的组合来控制故事执行的时间维度。两者共同决定在特定用户与一天中的特定时间触发 story 的概率。
如何操作
我们将更新 story 并对这两个方面进行参数化。
首先创建故事的计时器画像,它控制在何时更可能或更不可能触发动作。该画像对触发该 story 的所有人群成员相同。
Trumania 内置了一些有趣的默认画像,例如 DefaultDailyTimerGenerator,它对应我们在真实生产数据集中测得的日常手机使用情况:

在您的场景中实例化一个:
from trumania.components.time_patterns.profilers import DefaultDailyTimerGenerator
story_timer_gen = DefaultDailyTimerGenerator(
clock=example_circus.clock,
seed=next(example_circus.seeder))
其次,为该 story 配置人群成员的活动水平,定义每位用户在该 story 中的活跃程度。您可以为每个成员设定不同水平,从而让某些成员更频繁或更少地触发该动作。
当然也可以手动指定每个水平,但这里我们使用一个随机生成器,为每位成员分配一个活动水平。
首先,定义三个活动水平,平均每天触发 3、10、20 次:
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"))
然后创建实际的活动生成器,它仍然是一个基于 choice 方法的 Numpy 生成器。下面配置表示:为 20% 的人群分配低活动、70% 分配中活动、10% 分配高活动:
activity_gen = NumpyRandomGenerator(
method="choice",
a=[low_activity, med_activity, high_activity],
p=[.2, .7, .1],
seed=next(example_circus.seeder))
现在即可将该计时器画像与活动生成器用于您的 story:
hello_world = example_circus.create_story(
name="hello_world",
initiating_population=example_circus.populations["person"],
member_id_field="PERSON_ID",
timer_gen=story_timer_gen,
activity_gen=activity_gen
)
结果
结果数据集的模式与之前相同:
+-------+-------------------+---------------------+---------------------------------------------------------+-------------------+---------------------+----------------------+
| | 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 |
+-------+-------------------+---------------------+---------------------------------------------------------+-------------------+---------------------+----------------------+
提示:查看 Github 代码片段
结果分析
您在场景配置中引入的时间模式现在应能在结果数据集的内部结构中体现。可以通过对 resulting_dataset 进行基础分析来验证:
- 一天中各小时的消息数量应符合指定分布(注意它与上文
DefaultDailyTimerGenerator曲线的相似性):
time_profile = (
resulting_dataset[["MESSAGE", "TIME"]]
.groupby(by=example_5_df.TIME.dt.hour)["MESSAGE"]
.count()
)
time_profile.plot()

- 每位用户消息数量的直方图同样应符合配置:
- 您为
low_activity配置了日均 3 条消息、med_activity为 10 条、high_activity为 20 条,并运行了 5 天的模拟。应产生大致 3 组person,总消息数约为 15、50 与 100,分别对应下图直方图的 3 个中心; - 您将这些活动水平的概率配置为 .2、.7、.1,这也大致对应了直方图中各“隆起”的相对面积:
- 您为
usage_per_user = resulting_dataset[["MESSAGE", "PERSON_ID"]].groupby("PERSON_ID")["MESSAGE"].count()
usage_per_user.plot(kind="hist")

社交网络
目的
您可以进一步改进场景:让人们在故事执行过程中给朋友发消息,而不是随机找人。这样会为结果数据集引入大量结构化信息,因为现在可以从消息日志中挖掘出社交图。
如何操作
我们仍然使用 Relationship 来建模社交网络,与上文引用语的建模方式相同。回忆一下,quotes 关系设有权重,用于定义在给定人物下哪条引用语更可能出现。在这里也同理,用权重定义哪些朋友更可能被呼叫。
需要注意,这里我们定义的是从 person 到 person 的关系,但一般而言,Trumania 允许在任意两个人群之间定义关系。
生成社交图边的方式很多。一种基础而经典的方法是使用 Erdos-Renyi 算法。在 Trumania 中这尤其简单,因为它已经内置于框架中。您只需将其引入场景以创建关系。
在此,我们将为 person 成员之间定义一个社交图,使每个人大约有 20 个朋友。注意,关系会自动附加一个符合 帕累托分布的权重。
from trumania.components.social_networks.erdos_renyi import WithErdosRenyi
# self is the circus here, see example code on github for details
self.add_er_social_network_relationship(
self.populations["person"],
relationship_name="friends",
average_degree=20)
关系就位后,我们再次回到 story。这一次,OTHER_PERSON 将不再从整个人群中随机选择,而是从每个 person 成员的朋友中选择,且选择概率与社交网络中定义的权重成正比:
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")
)
结果
结果数据集的模式仍与之前相同,不过由于引入了基础的社交网络,现在的结构性更强:
+-------+-------------------+---------------------+----------------------------------------------------+-------------------+-------------------+------------------+
| | 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 |
+-------+-------------------+---------------------+----------------------------------------------------+-------------------+-------------------+------------------+
提示:可查看 Github 代码片段
结果分析
一种非常粗略的观察社交网络的方法,是统计每人拨打过电话的唯一朋友数量。如果模拟运行足够久,这个数值应能提供每人社交网络真实规模的下界(由于我们使用了帕累托分布的权重,许多人有极不可能被呼叫到的朋友,因此需要运行很久才能真正观察到所有可能被呼叫的人)。
我们将场景运行了 15 天的模拟时间,得到如下结果:其中心位置非常接近且略低于您在社交图中设定的平均度 20:
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")

远不止这些!
本文未涵盖的 Trumania 特性还包括:
- 在同一场景中使用多个人人群
- 在同一场景中包含多个故事
- 为故事之间添加概率依赖(例如:某人被呼叫后再回拨朋友)
- 状态更新(例如:根据通话时长减少余额)
- 可复用的场景片段,如地理、更多计时器画像等
- 场景片段的持久化
结语
我们介绍了 Trumania 作为一个基于场景的 Python 数据生成库。生成的数据集可用于测试、学习与基准评测等多种场景。
我们说明了要恰当地测试应用或算法,就需要具备符合预期统计性质的数据集。我们以一个基础的“消息日志”数据集为例,展示了 Trumania 能生成符合每用户消息数量分布、一天内时间分布,以及社交图中呼叫分布的数据。
我们也希望展示出,Trumania 组件的灵活性可以支持构建各种各样的场景。
Trumania 场景可以包含实体之间的关系,并能使用理论随机分布、真实数据集中观测到的经验分布,甚至真实产线数据(如位置 ID 数据集等)进行配置。我们通过复用真实数据集中观测到的手机使用时间戳分布来配置故事的时间随机计时器,演示了这一点。
心动不如行动!访问 Trumania 的 Github,编写您自己的场景吧;)
另外,欢迎联系作者 Svend Vanderveken 或合著者 Milan van der Meer:
- Svend Vanderveken 是一名自由职业软件工程师,专长于数据处理解决方案,主要涉及 Kafka、Scala、SQL、Python 等。通过其博客 svend.kelesia.com 与推特 @sv3ndk 关注他。
- Milan van der Meer 目前在 Real Impact Analytics 担任软件工程师。欢迎在 LinkedIn 上与他建立联系。