Trumania:基于行为建模的合成数据仿真引擎
1. 项目概述为什么你需要 Trumania 这样的数据生成器在真实的数据工程和机器学习工作流中我见过太多团队卡在同一个地方没有合适的数据。不是数据太少就是数据太“脏”又或者——最常见也最棘手的情况——根本拿不到生产数据。你可能刚写完一个漂亮的实时用户行为分析 pipeline兴冲冲想跑通 end-to-end 测试结果发现连一条像样的输入记录都没有你也可能正为一个新提出的推荐算法做消融实验需要对比它在不同用户规模、不同活跃度分布、不同冷启动比例下的表现但手头只有一份静态的、脱敏后严重失真的样本集。这时候随便从网上扒点 CSV 或者用random.randint()拼凑几列数字不仅帮不上忙反而会埋下巨大的隐患你验证的不是模型本身而是你对“随机”的朴素想象。Trumania 就是为解决这类问题而生的。它不是一个简单的“造数工具”而是一个基于行为建模的合成数据仿真引擎。它的核心思想非常朴素真实世界的数据不是凭空出现的而是由一群有属性、有关系、有动机、有时间规律的“人”或泛指任何实体在特定场景下互动产生的。一个通话日志背后是用户在不同时段的通信意愿、社交圈层的强弱、资费套餐的约束一个电商订单流背后是用户的购买力、浏览路径、促销敏感度和库存状态的动态博弈。Trumania 把这种因果逻辑显式地编码进你的数据生成过程里。它不让你定义“我要生成 1000 行其中 age 字段服从正态分布”而是让你定义“创建一个 1000 人的社区其中 20% 是低活跃度用户平均每天发 3 条消息70% 是中等活跃度每天 10 条10% 是高活跃度每天 20 条并且他们的消息高峰集中在晚上 8 点到 11 点周末发送频率比工作日高 40%”。最终生成的是一份自带时间戳、自带上下文、自带统计偏差的、真正“活”的数据集。这直接解决了传统方案的三大硬伤。第一是关系缺失。Schema-based 工具比如 Khermes 或 LogSynth能轻松定义“用户表有 name、age、gender 三列”但它无法表达“男性用户更倾向于使用‘兄弟’、‘哥们’这类称呼而女性用户更常使用‘亲爱的’、‘宝贝’等昵称”——这种字段间的语义耦合在 Trumania 里通过Relationship关系机制天然实现。第二是时序失真。真实行为具有强烈的周期性、突发性和依赖性。一个用户不会在凌晨三点准时发第 17 条朋友圈他的行为受生物钟、工作日程、突发事件影响。Trumania 的timer profile计时器配置文件和activity level活跃度等级组合能精准复刻这种复杂的时间模式。第三是场景不可控。你想测试你的风控模型在“大量新注册用户集中涌入并进行小额试探性交易”这种特定压力场景下的表现吗Schema 工具只能给你一堆孤立的“新用户”和“交易”而 Trumania 可以让你编写一个“新用户注册 → 领取首单红包 → 发起 3 笔 5 元以下测试订单 → 根据订单反馈决定是否继续”的完整故事链Story让数据从这个链条中自然涌现。关键词“Trumania”、“合成数据”、“行为建模”、“时序数据生成”、“关系型数据生成”这些不是空洞的标签而是你解决上述所有痛点时最该掌握的核心能力。2. 核心设计哲学从“造表”到“造世界”2.1 为什么放弃 Schema-Based拥抱 Scenario-Based理解 Trumania 的第一步是彻底告别“数据库建模”的思维惯性。当你看到一个 JSON Schema 定义了{field: Name, class: NameGenerator}你的大脑会立刻联想到一张二维表格每一行是一个独立的、静止的记录。这种范式在生成单张、无关联的参考数据比如一份用于 UI 填充的假用户列表时足够高效。但一旦涉及多表关联、业务流程或时间演化它就暴露出结构性缺陷。让我用一个具体例子说明。假设你要生成一份模拟的“用户-好友-消息”三元组数据。用 Schema 方式你可能会这样写{ users: [ {id: U001, name: Alice, gender: F}, {id: U002, name: Bob, gender: M}, ... ], friends: [ {user_id: U001, friend_id: U002}, {user_id: U002, friend_id: U001}, ... ], messages: [ {sender_id: U001, receiver_id: U002, content: Hi Bob!, timestamp: 2023-01-01T09:00:00Z}, ... ] }问题来了如何保证messages表里的sender_id和receiver_id一定存在于friends关系中如何让 Alice 给 Bob 发的消息内容比给一个完全陌生的用户发的内容更口语化、更频繁如何让所有消息的timestamp分布符合“上班族白天工作、晚上社交”的真实规律而不是均匀地洒在 24 小时内Schema 工具对此无能为力它只能靠你在生成后用脚本做繁琐的后处理或者干脆接受数据的“虚假合理性”。Trumania 的解法是釜底抽薪它不生成“表”它运行一个“世界”。这个世界里核心元素是Circus马戏团、Population种群、Story故事和Relationship关系。Circus是整个仿真的容器和时间中枢它定义了仿真从何时开始、以多快的速度推进step_duration。Population不是数据表而是一群有身份id、有属性attributes、能行动的智能体。Story是驱动世界运转的剧本它定义了“谁在什么时候因为什么理由做了什么事”。Relationship则是连接不同种群或同一一种群内部个体的纽带它承载着概率、权重和上下文信息。这种设计带来的根本性优势在于因果可追溯。你看到一条消息记录不仅能知道它的sender_id和receiver_id还能回溯到这条消息是由person种群中的某个成员触发的触发的时机由DefaultDailyTimerGenerator计算出的概率决定消息的内容是从该成员专属的quotes关系中根据预设的权重随机选取的而接收者则是从该成员的social_network关系中挑选的。每一个数据点都是一个微型决策链的终点。这使得你生成的数据天然具备了用于测试复杂业务逻辑、验证算法鲁棒性、甚至进行反事实推理What-if Analysis所需的深度结构。2.2 Trumania 的四大支柱Circus, Population, Story, Relationship要真正驾驭 Trumania必须吃透这四个概念它们构成了整个框架的骨架。Circus马戏团世界的舞台与心跳Circus是一切的起点和中心。它不是一个抽象的类而是一个具体的、可运行的仿真环境实例。你可以把它想象成一个精密的钟表工厂。Circus的核心职责有二一是提供一个全局、统一、可预测的时间源clock所有事件都严格按这个时钟的节拍发生二是作为所有其他组件种群、故事、关系的注册中心和管理容器。创建一个Circus时最关键的参数是step_duration。这个值决定了仿真的粒度。如果你设置step_durationpd.Timedelta(1h)那么整个世界每“跳”一格就代表现实时间过去了一小时所有被激活的Story都会在这一格内执行一次。这个设定看似简单却深刻影响着后续所有行为的建模精度。选择 1 小时适合模拟宏观的用户活跃度趋势选择 1 分钟就能捕捉到用户在 App 内的点击流选择 1 秒则可以逼近网络请求级别的仿真。Circus还负责管理随机种子master_seed确保在相同配置下每次运行都能得到完全一致的、可复现的结果——这对于调试和 A/B 测试至关重要。Population种群世界的居民Population是Circus中的居民是行为的主体。它不是一个被动的数据集合而是一个拥有生命特征的实体。每个Population必须有一个唯一的name如person、shop和一个确定的size如1000。它的核心是id这是每个居民的身份标识通常由一个ids_genID 生成器来分配比如SequencialGenerator(prefixPERSON_)会生成PERSON_0001,PERSON_0002... 这种格式。更重要的是Population可以拥有attributes属性这些属性不是静态的而是由Generator生成器在初始化时动态赋予的。NumpyRandomGenerator可以从任意 numpy 支持的分布正态、泊松、指数中采样FakerGenerator则能生成逼真的姓名、地址、公司名等。关键在于这些属性是绑定在每个id上的它们构成了居民的“人格画像”为后续的Story提供了决策依据。Story故事世界的剧情如果说Population是演员那么Story就是剧本。它是 Trumania 中最核心的动态单元定义了“谁在何时做了什么”。一个Story必须指定initiating_population发起种群即由哪个种群的成员来触发这个故事。它还必须有一个timer_gen计时器生成器它决定了每个成员在每个时间步长step_duration内触发该故事的概率。这个概率可以是恒定的ConstantDependentGenerator(value1)即每次都触发也可以是高度复杂的DefaultDailyTimerGenerator其内部封装了基于真实电信数据拟合出的每日活跃度曲线。Story的灵魂在于它的operations操作序列。这些操作是原子性的指令可以读取种群属性、查询关系、生成时间戳、产生随机值最终将结果记录到日志中。一个Story的执行就是一次完整的、带有上下文的“行为事件”的发生。Relationship关系世界的纽带Relationship是 Trumania 实现“真实性”的秘密武器。它打破了传统数据生成中字段间孤立的状态建立了实体间的动态联系。一个Relationship总是依附于一个Population并定义了该种群的每个成员from_id可以关联到哪些其他值to_id。最经典的例子就是“用户-喜好语录”关系。你为每个用户from_id关联 4 条语录to_id并为每条语录赋予一个weight权重。当用户“说话”时Story中的select_one操作会根据这些权重从该用户的专属语录池中进行加权随机抽取。这就天然地实现了“张三爱说‘今天天气不错’李四总爱讲‘这个需求我看看’”这种个性化行为。Relationship还可以是“用户-好友”、“用户-常去店铺”、“店铺-库存商品”等它让生成的数据不再是扁平的表格而是一个立体的、相互关联的网络。正是这种关系网络使得 Trumania 生成的数据能够涌现出真实的集群效应、传播路径和长尾分布。3. 实操详解从零构建一个“社交消息”仿真系统3.1 第一步搭建世界舞台Creating a Circus我们从最基础的Circus创建开始。这一步看似只是几行代码但它奠定了整个仿真的时空基础。请务必理解每一个参数的意义因为它们是后续所有行为的“物理定律”。import pandas as pd from trumania.core import circus # 创建一个名为 social_sim 的 Circus example_circus circus.Circus( namesocial_sim, master_seed12345, # 全局随机种子确保结果可复现 startpd.Timestamp(1 Jan 2023 00:00), # 仿真开始时间 step_durationpd.Timedelta(1h) # 时间步长1小时 )这段代码创建了一个名为social_sim的仿真世界。master_seed12345是关键它意味着无论你何时、在何台机器上运行这段代码只要种子不变生成的所有随机数序列都将完全一致。这对于团队协作和问题排查是无价的。start参数定义了这个世界的“创世时刻”所有生成的时间戳都将以此为基准。而step_durationpd.Timedelta(1h)则是这个世界的“心跳频率”。它告诉 Trumania“每过一个小时我就要检查一遍看有哪些故事该上演了。” 这个设定直接决定了你后续Story的触发节奏。如果你的目标是模拟用户一天内的活跃度变化1 小时是合适的粒度如果你想观察用户在会议期间例如 14:00-15:00的即时通讯爆发那么你可能需要将step_duration调整为pd.Timedelta(5min)以获得更精细的控制。接下来我们需要为这个世界填充第一批居民——一个 1000 人的person种群。这里的关键是理解Generator的作用。我们不会手动为每个人写一个名字和年龄而是定义一套“造人规则”。from trumania.core.random_generators import SequencialGenerator, FakerGenerator, NumpyRandomGenerator # 为每个人生成唯一的 ID id_gen SequencialGenerator(prefixPERSON_) # 为每个人生成一个随机年龄服从均值为 35、标准差为 12 的正态分布 # 注意我们使用 circus 自带的 seeder 来获取一个子种子保证随机性独立 age_gen NumpyRandomGenerator( methodnormal, loc35, scale12, seednext(example_circus.seeder) ) # 为每个人生成一个随机姓名使用 Faker 库的 name 方法 name_gen FakerGenerator( methodname, seednext(example_circus.seeder) )SequencialGenerator确保了 ID 的唯一性和可读性这是后续所有关联操作的基础。NumpyRandomGenerator的强大之处在于它无缝集成了 numpy 的全部统计分布。我们选择了正态分布来模拟年龄因为它最符合人口统计的现实大多数人在 20-50 岁之间两端人数递减。loc35是均值scale12是标准差这意味着大约 68% 的用户年龄会在 23-47 岁之间。FakerGenerator则提供了开箱即用的、文化上合理的姓名生成能力避免了random.choice([John, Mary])这种过于简陋的方案。最后我们将这些规则应用到Circus中创建种群# 在 Circus 中创建一个名为 person 的种群大小为 1000 person example_circus.create_population( nameperson, size1000, ids_genid_gen ) # 为这个种群添加两个属性NAME 和 AGE person.create_attribute(NAME, init_genname_gen) person.create_attribute(AGE, init_genage_gen)执行完毕后你可以通过person.to_dataframe()查看前几行数据。你会看到一个包含id、NAME、AGE三列的 DataFrame。这里的id就是PERSON_0001这样的字符串NAME是 Faker 生成的真实姓名AGE是一个浮点数正态分布的采样结果。这已经是一个合格的、有血有肉的初始种群了。它不再是一堆冰冷的数字而是一个由 1000 个具有独特身份和基本特征的“人”组成的社区。3.2 第二步编写第一个剧本Hello World Story有了居民世界还是一片寂静。现在我们要为他们编写第一个剧本让他们“开口说话”。这个Story的目标很简单让每个用户每小时都发送一条“hello world”消息。这虽然简单但却是理解Story工作原理的完美入口。# 创建一个名为 hello_world 的 Story hello_world example_circus.create_story( namehello_world, initiating_populationexample_circus.populations[person], # 由 person 种群发起 member_id_fieldPERSON_ID, # 日志中记录发起者的 ID 字段名 timer_genConstantDependentGenerator(value1) # 每次都触发 )initiating_population指定了谁是主角。member_id_fieldPERSON_ID是一个约定俗成的命名它告诉 Trumania在最终生成的日志里我要用PERSON_ID这个字段来记录是哪位用户发起了这次行为。timer_genConstantDependentGenerator(value1)是最简单的计时器value1意味着概率为 100%即在每一个时间步长每小时内person种群中的每一位成员都会触发一次这个Story。然而此时的Story还只是一个空壳它还没有任何动作。我们需要为其添加operations也就是一系列具体的指令。from trumania.core.loggers import FieldLogger # 为 Story 添加操作序列 hello_world.set_operations( # 操作1生成一个当前时间步长内的随机时间戳并命名为 TIME example_circus.clock.ops.timestamp(named_asTIME), # 操作2生成一个恒定的字符串 hello world并命名为 MESSAGE ConstantGenerator(valuehello world).ops.generate(named_asMESSAGE), # 操作3将以上所有操作的结果记录到一个名为 hello 的日志文件中 FieldLogger(log_idhello) )example_circus.clock.ops.timestamp(named_asTIME)是一个精妙的设计。它并没有简单地返回Circus.start的时间而是返回一个在[当前步长开始时间, 当前步长结束时间)区间内均匀分布的随机时间戳。例如如果当前是2023-01-01 01:00:00这个时间步那么生成的时间戳可能是2023-01-01 01:23:45。这使得日志看起来更加真实避免了所有事件都精确地发生在整点的“机械感”。ConstantGenerator(valuehello world).ops.generate(named_asMESSAGE)则展示了 Trumania 的操作链式调用风格。ConstantGenerator是一个最基础的生成器它永远返回你设定的值。.ops.generate()是它的“执行”方法named_asMESSAGE则指定了这个操作的输出将被存储在日志的MESSAGE字段下。最后FieldLogger(log_idhello)是日志的“落点”。它会将本次Story执行所产生的所有named_as字段汇总成一行日志写入到一个以log_id命名的文件中例如hello.csv。现在让我们运行这个简单的世界# 运行仿真 48 小时2 天 example_circus.run( durationpd.Timedelta(48h), log_output_folderoutput, # 日志输出到 output/ 文件夹 delete_existing_logsTrue # 覆盖已存在的日志 )运行结束后打开output/hello.csv你会看到一个包含 48000 行1000 人 × 48 小时的 CSV 文件。它的结构如下PERSON_IDTIMEMESSAGEPERSON_00012023-01-01 00:12:34hello worldPERSON_00022023-01-01 00:25:17hello world.........这就是 Trumania 的第一次心跳。它证明了整个框架是联通的Circus提供了时间Population提供了主体Story提供了行为Operations提供了细节Logger提供了输出。虽然内容还很初级但这个骨架已经完整后续的所有增强都是在这个骨架上添砖加瓦。3.3 第三步注入关系与个性Adding Relationships让每个人都说“hello world”显然不够真实。真实世界中每个人的表达方式千差万别。Trumania 通过Relationship机制优雅地解决了这个问题。我们的目标是为每个用户关联 4 条他/她最喜欢的语录并且每条语录都有不同的“使用频率”从而让数据自然地呈现出个性化。首先我们需要一个能生成语录的生成器# 创建一个语录生成器生成 6 个单词的随机句子 quote_generator FakerGenerator( methodsentence, nb_words6, variable_nb_wordsTrue, # 句子长度可以在 6 附近浮动 seednext(example_circus.seeder) )FakerGenerator的sentence方法非常强大它能生成语法正确、语义通顺的英文句子远胜于简单的单词拼接。nb_words6设定了句子的平均长度variable_nb_wordsTrue则增加了变化让句子长度在 4-8 个单词之间波动更符合人类语言习惯。接下来我们创建一个名为quotes的Relationship并将其绑定到person种群上# 为 person 种群创建一个名为 quotes 的关系 quotes_rel person.create_relationship(quotes)这行代码创建了一个空的关系容器。现在我们需要往里面“装”数据。我们的策略是为每个用户添加 4 条语录并且让第 1 条语录的权重为 1最不常用第 2 条为 2第 3 条为 3第 4 条为 4最常用。这样用户在“说话”时就会更倾向于重复使用他/她最喜欢的那条语录。# 为每个用户添加 4 条语录权重依次递增 for weight in [1, 2, 3, 4]: quotes_rel.add_relations( from_idsperson.ids, # 所有用户的 ID 列表 to_idsquote_generator.generate(sizeperson.size), # 生成 1000 条随机语录 weightsweight # 为这 1000 条语录都赋予相同的权重 )person.ids是一个 pandas Series包含了所有 1000 个用户的 ID。quote_generator.generate(sizeperson.size)则生成了 1000 条随机语录。add_relations方法将这两者一一对应起来并打上weight标签。循环四次就为每个用户都关联了 4 条语录且权重各不相同。现在quotes关系已经准备就绪。我们需要修改hello_worldStory让它不再使用恒定的字符串而是从这个关系中“挑选”语录# 修改 Story 的操作序列用关系查询替代恒定生成 hello_world.set_operations( example_circus.clock.ops.timestamp(named_asTIME), # 关键修改从 quotes 关系中根据 PERSON_ID 查找并随机选择一条语录 person.get_relationship(quotes).ops.select_one( from_fieldPERSON_ID, # 从 PERSON_ID 字段的值出发 named_asMESSAGE # 将选中的语录存入 MESSAGE 字段 ), # 保持原有的日志记录 FieldLogger(log_idhello) )person.get_relationship(quotes).ops.select_one(...)是整个关系机制的精华所在。from_fieldPERSON_ID告诉 Trumania“请查看当前正在执行Story的这个用户的PERSON_ID是什么”。然后它会去quotes关系中查找所有from_id等于这个PERSON_ID的记录并根据它们的weights进行加权随机抽取。这意味着对于PERSON_0001他/她的 4 条语录中权重为 4 的那条被选中的概率是权重为 1 的那条的 4 倍。再次运行example_circus.run(...)打开新的hello.csv你会发现MESSAGE字段已经变成了五花八门的随机句子PERSON_IDTIMEMESSAGEPERSON_00012023-01-01 00:12:34The green car quickly drove down the street.PERSON_00012023-01-01 01:05:22The green car quickly drove down the street.PERSON_00012023-01-01 02:33:11She opened the door and walked into the bright sunlight.PERSON_00022023-01-01 00:44:55His old laptop made a strange whirring noise.PERSON_00022023-01-01 01:18:09His old laptop made a strange whirring noise.注意看PERSON_0001的前三条记录第一条和第二条是完全一样的句子而第三条则不同。这正是权重机制在起作用——他/她最喜欢的那条语录加粗显示被重复使用了。数据开始有了“性格”这正是 Trumania 的魔力所在。3.4 第四步赋予时间灵魂Parameterizing Time目前我们的用户每小时都在“说话”这在统计上是均匀的。但真实世界并非如此。一个典型的上班族工作日的上午 9 点到下午 6 点是工作时间即时通讯相对沉寂而晚上 8 点到 11 点则是社交高峰周末的活跃度又会整体上移。Trumania 通过timer profile和activity level的组合完美地模拟了这种复杂的时间模式。首先我们需要一个内置的、经过真实数据校准的timer profilefrom trumania.components.time_patterns.profilers import DefaultDailyTimerGenerator # 创建一个默认的每日活跃度计时器 story_timer_gen DefaultDailyTimerGenerator( clockexample_circus.clock, seednext(example_circus.seeder) )DefaultDailyTimerGenerator是 Trumania 团队基于海量电信数据提炼出的模型。它的内部曲线并非一个简单的正弦波而是包含了多个峰值如早高峰、午休、晚高峰和谷值如深夜并且工作日和周末的曲线形状也不同。你不需要理解它的数学细节只需要知道它代表了“一个典型用户在一天中每个小时触发某项行为的相对概率”。仅仅有timer profile还不够因为profile是群体的共性而每个用户还有自己的个性——有的用户天生话痨有的则沉默寡言。这就是activity level的用武之地。我们需要为每个用户分配一个“基础活跃度”它会与timer profile相乘得到该用户在特定时刻的绝对触发概率。# 定义三种活跃度等级低、中、高分别对应每天平均触发 3、10、20 次 low_activity story_timer_gen.activity(n3, perpd.Timedelta(1 day)) med_activity story_timer_gen.activity(n10, perpd.Timedelta(1 day)) high_activity story_timer_gen.activity(n20, perpd.Timedelta(1 day)) # 创建一个随机生成器按照 20%:70%:10% 的比例为用户分配活跃度等级 activity_gen NumpyRandomGenerator( methodchoice, a[low_activity, med_activity, high_activity], p[0.2, 0.7, 0.1], # 概率权重 seednext(example_circus.seeder) )story_timer_gen.activity(n10, perpd.Timedelta(1 day))这个调用非常巧妙。它并不是直接返回一个数字 10而是返回一个“活动对象”这个对象内部封装了如何将timer profile的相对概率缩放为一个绝对的、符合“每天平均 10 次”这一目标的触发率。NumpyRandomGenerator的choice方法则负责将这三种活动对象按照预设的概率分布随机分配给 1000 个用户。最后我们将这些新的时间参数注入到Story的创建过程中# 重新创建 Story传入新的 timer_gen 和 activity_gen hello_world example_circus.create_story( namehello_world, initiating_populationexample_circus.populations[person], member_id_fieldPERSON_ID, timer_genstory_timer_gen, # 新的计时器配置文件 activity_genactivity_gen # 新的活跃度生成器 )注意我们不再使用ConstantDependentGenerator而是将story_timer_gen和activity_gen作为参数直接传入create_story。这意味着Story的触发逻辑现在由这两个组件共同决定timer_gen提供了“一天中哪个时段更容易触发”的时间模式activity_gen提供了“这个用户总体上有多爱触发”的个体差异。再次运行仿真建议运行 5 天即durationpd.Timedelta(5d)然后对生成的日志进行简单的数据分析你就能直观地看到效果import pandas as pd import matplotlib.pyplot as plt # 读取日志 df pd.read_csv(output/hello.csv) # 分析每小时的消息总数 hourly_count df.groupby(df[TIME].dt.hour)[MESSAGE].count() hourly_count.plot(kindbar, titleMessages per Hour (24h)) plt.show() # 分析每个用户的消息总数 user_count df.groupby(PERSON_ID)[MESSAGE].count() user_count.hist(bins50, titleMessages per User (5 days)) plt.show()第一张图每小时消息数应该会呈现出一个清晰的双峰曲线一个主峰在晚上 8-11 点一个次峰在下午 2-4 点这正是DefaultDailyTimerGenerator的典型特征。第二张图每个用户的消息总数则应该呈现出三个明显的“峰群”左侧一小簇约 200 人的总数在 15 左右3 次/天 × 5 天中间一大簇约 700 人在 50 左右10 次/天 × 5 天右侧一小簇约 100 人在 100 左右20 次/天 × 5 天。这完美地验证了我们对activity level的配置。数据不再是均匀的噪音而是拥有了真实世界的时间脉搏和个体差异。4. 进阶实战构建一个“用户-好友-消息”社交网络4.1 构建社交关系网Social Network Relationship到目前为止我们的消息都是“广播式”的每个用户随机选择一个接收者。这离真实的社交网络还很远。真实世界中人们的消息主要流向自己的好友、家人和同事。Trumania 的Relationship机制同样可以用来构建这种复杂的、有向的、带权重的社交图谱。我们的目标是为每个用户from_id关联 5-15 个他/她的“好友”to_id并且这些好友不是随机的而是遵循“朋友的朋友也是朋友”的小世界特性。Trumania 本身不提供图生成算法但我们可以利用外部库如networkx来生成一个符合要求的图然后将其导入。import networkx as nx import numpy as np # 步骤1创建一个空的无向图 G nx.Graph() # 步骤2添加 1000 个节点对应 1000 个用户 G.add_nodes_from(person.ids.tolist()) # 步骤3使用 Watts-Strogatz 小世界模型生成边 # n1000, k10 (每个节点先连接最近的10个邻居), p0.1 (随机重连概率) # 这会产生一个既有局部聚类朋友圈又有全局短路径六度空间的网络 G nx.watts_strogatz_graph(n1000, k10, p0.1, seednext(example_circus.seeder)) # 步骤4将 networkx 图转换为 Trumania 可用的格式 # 我们需要两个列表from_ids 和 to_ids from_ids [] to_ids [] # 遍历图中的每一条边 for edge in G.edges(): # 将边转换为有向边u - v 和 v - u from_ids.append(edge[0]) to_ids.append(edge[1]) from_ids.append(edge[1]) to_ids.append(edge[0]) # 步骤5创建一个名为 social_network 的 Relationship social_rel person.create_relationship(social_network) # 步骤6将生成的边批量添加到关系中 social_rel.add_relations( from_idsfrom_ids, to_ids