
本文介绍如何利用 pandas 和 jinja2,根据每个家庭内部成员在各场会议中的出席模式(即行间相似性),自动构建结构清晰、语法自然的英文邀请语句,并支持按家庭分组聚合与事件归并。
本文介绍如何利用 pandas 和 jinja2,根据每个家庭内部成员在各场会议中的出席模式(即行间相似性),自动构建结构清晰、语法自然的英文邀请语句,并支持按家庭分组聚合与事件归并。
在数据分析与自动化邮件/通知生成场景中,常需基于结构化数据(如家庭成员参会记录)生成符合自然语言习惯的个性化文本。本教程以“为每个家庭负责人生成定制化邀请文案”为任务目标,核心挑战在于:识别同一家庭内不同成员在各场会议中的出席一致性(即行间相似性),并据此分组归纳事件与参与者。
我们采用以下技术路径实现:
- 使用 groupby("family") 按家庭切分数据;
- 对每组提取会议列(meeting1, meeting2, ...),通过布尔矩阵运算识别“出席组合相同”的事件;
- 利用 @ 矩阵乘法将参会模式映射为成员列表字符串,再按模式聚类;
- 借助 Jinja2 模板引擎渲染语法严谨的英文句子,自动处理逗号分隔与 “and” 连接(如 "abba, ben and berry");
- 最终可选择性地将结果注入原 DataFrame 的指定行(仅家庭负责人)或广播至全家族行。
✅ 示例代码(完整可运行)
import pandas as pd
from jinja2 import Template
# 构造示例数据
df = pd.DataFrame({
"family": [1, 1, 1, 2, 2, 3, 3],
"fam_head": [True, None, None, None, True, None, True],
"name": ["abba", "ben", "berry", "jack", "joe", "razia", "riri"],
"meeting1": [1, 1, None, 1, 1, 1, None],
"meeting2": [1, 1, None, None, None, None, 1],
"meeting3": [1, 1, None, None, None, None, None],
"meeting4": [1, 1, 1, 1, 1, 1, None],
}).convert_dtypes()
# 定义 Jinja2 模板(支持自然语言逻辑)
TPL = Template("""
Hi {{ head }}, delighted for {{ others }} to come.
{% for meetings, attendees in events.items() -%}
{% if not same_meeting -%}
{{ attendees }}: {{ meetings }}
{% endif -%}
{% endfor -%}
""".strip())
# 辅助函数:将列表转为 "A, B and C" 格式
import re
def comma_nd(inp):
if isinstance(inp, str):
return inp
s = ", ".join(list(inp))
return re.sub(r",(?!.*,)", " and", s)
# 主处理函数
MCOLS = df.filter(like="meeting").columns
def greet(g):
# 提取家庭负责人姓名
head_idx = g["fam_head"].idxmax()
head = g.at[head_idx, "name"]
# 构建 "others":除负责人外的成员,"you" 替换负责人名
others_list = g.sort_values("fam_head")["name"].replace(head, "you").tolist()
# 判断是否所有成员参会完全一致
same_meeting = g.duplicated(MCOLS, keep=False).all()
# 关键步骤:按参会模式分组事件
tmp = g.set_index("name")[MCOLS].T.dropna(how="all")
if tmp.empty:
events = {}
else:
# 转布尔 + 矩阵乘法 → 每行(会议)对应出席者字符串
bool_mat = tmp.fillna(0).astype(bool)
attendee_strs = (bool_mat @ (tmp.columns + ", ")).str.strip(", ")
# 按出席者组合(字符串)分组,合并会议名
events = (attendee_strs.reset_index(name="attendees")
.groupby("attendees")["index"]
.agg(comma_nd)
.to_dict())
return TPL.render(
head=head,
others=comma_nd(others_list),
same_meeting=same_meeting,
events=events
).strip()
# 应用分组处理
greetings = df.groupby("family", group_keys=False).apply(greet)
# 注入原 DataFrame(方式一:仅填入家庭负责人行)
df["greet_card"] = None
df.loc[df["fam_head"], "greet_card"] = greetings.to_numpy()
# 查看结果
print(df[["family", "fam_head", "name", "greet_card"]])⚠️ 注意事项与最佳实践
- 会议列识别:使用 df.filter(like="meeting") 是健壮做法;若列名不规则,建议显式传入 MCOLS = ["meeting1", "meeting2", ...]。
- 空值处理:dropna(how="all") 排除全空会议行;fillna(0).astype(bool) 将非空值统一视为 True(出席),确保逻辑一致。
- 自然语言边界:正则 r",(?!.*,)" 精准替换最后一个逗号为 " and",避免 "A, B, C and D" 错误为 "A and B and C and D"。
- 性能提示:对超大家庭(>100人)或大量会议(>50列),矩阵乘法可能变慢,可改用 apply + frozenset 哈希替代。
- 扩展性:模板支持添加日期、签名、链接等字段;只需在 TPL.render() 中传入额外变量即可。
该方案不仅解决了原始问题中“按相似性分组事件”的核心需求,更提供了一套可复用的文本生成范式——将结构化数据的语义关系,精准映射为人类可读的自然语言输出。










