P3-5.7 折叠多个后续事件的规则
Section ID: P3-5.7 Version: v2026.09.19
副标题: 同一个样本之后的多个事件应该按什么规则折叠进一个表结构?
即使样本(sample)单位和输入窗口都已经定好了,表结构里仍然常常会再卡住一个地方:同一个样本之后挂着多个后续事件。比如,一次动作之后,可能依次留下 review、warning、failure、revisit。如果不先决定要怎样把它们折叠成一个结果列,同一个样本在不同表里就很容易变成不同含义。
如果后续事件有多个,就应该先写清:它们是按什么折叠规则(folding rule)被折叠进同一个表结构里的。
常见的折叠规则有下面这些。
| 折叠规则 | 含义 |
any | 只要发生过一次,就记为 1 |
first | 把最早出现的后续事件作为代表 |
worst | 把最严重的状态作为代表 |
count | 直接保留发生次数 |
例如,假设同一个样本之后,留下了下面这样的后续事件。
| event_id | follow_up_events |
| A | review, failure |
| B | review |
| C | none |
把它折叠成什么样的表,会直接改变结果列的含义。
| event_id | any_failure | first_event | event_count |
| A | 1 | review | 2 |
| B | 0 | review | 1 |
| C | 0 | none | 0 |
也就是说,即使面对的是同一个源事件(source event),只要 代表结果到底选什么 的规则不同,表结构就会不同。这个问题本质上是一个数据建模问题:要先决定用什么规则把代表结果折叠进表里。
先留下下面这些备注,后面的混乱会少很多。
| 先写下来的备注 | 为什么需要 |
| 哪些后续事件被看成同一组 | 为了固定这张表所处理的结果范围 |
使用了 any、first、worst、count 里的哪一种 | 为了重新解释结果列到底是什么意思 |
| 折叠出来的结果是用于报告,还是预测候选 | 为了避免把比较报告和目标标签候选(target candidate)混在一起 |
最终表中也要让折叠规则可追踪。例如留下 folding_rule、severity_cutoff、follow_up_window_days、source_event_count、target_candidate_name 作为 备注,就能再次说明 any_selected_event=1 是在哪个事件范围和阈值下得到的。即使来自同一个后续事件日志,first_event 和 worst_event 也是不同列,不能只看一个列名就把它固定成实际目标标签。
汇总后续事件之前,还要定义观察期间和去重规则。如果目标是7 天内是否失败,第 9 天的失败不应计入。如果传输重试使同一事件存储了两次,就要核对事件标识,避免 count 重复计数。first 应按发生时间排序确定,并预先规定同时发生或严重程度相同的事件如何排序。
下面的示例假定样本清单中的所有样本都已完成追踪,后续事件日志已限定在分析期间内,而且没有重复记录。只有在这些假设下,才能给没有事件的 S30 标记 0。仍在观察中的样本,即使尚未发生事件,也必须保留为 pending。
小例子:
问题情境:确认当同一个样本之后存在多个后续事件时,first、worst、count、any 这些不同规则会生成不同的结果列。
输入(input):样本名册 p3_5_7_sample_roster.csv、后续事件日志 p3_5_7_follow_up_events.csv、事件严重程度表 p3_5_7_event_severity.csv,以及要比较的严重程度阈值候选 severity_cutoffs
第一个 CSV 中的一行,是最终结果表里必须保留的一个样本。第二个 CSV 中的一行,是样本之后实际发生过的一个后续事件。第三个 CSV 把事件名称转换成严重程度数字,用来计算 worst 和 any_selected_event 规则。
期望输出(output):即使是同一个源事件,first_event、worst_event、event_count、event_sequence、any_failure、any_selected_event 也会被生成成不同结果。改变 severity_cutoffs 时,所选样本数和样本列表也会改变。
要确认的概念:当多个后续事件被折叠成一个结果列时,必须先写明折叠规则和阈值(threshold),否则表结构的含义会漂移
手动汇总 S01、S02 和 S30
以下 CSV 是为本例设计的虚构数据。观测期为样本之后第 1~7 天,包含两端,并假设名单中的 36 个样本都已完成追踪。days_after_sample 是天数位置,不是精确发生时刻。CSV 没有追踪完成标记,也没有单个后续事件的 ID,因此仅凭这些文件无法验证追踪完成或没有重复记录。
| sample_id | 观测期内记录 | first_event | worst_event | event_count | any_failure |
| S01 | 第 1 天 review → 第 3 天 warning → 第 5 天 failure | review | failure | 3 | 1 |
| S02 | 第 2 天 review → 第 4 天 warning | review | warning | 2 | 0 |
| S30 | 无,假设追踪完成 | none | none | 0 | 0 |
any_failure 表示是否存在事件类型为 failure 或 critical_failure 的记录。这是本例的类型映射,不随严重程度阈值改变。count 统计观测期内所有后续事件,而非仅统计失败。first 会隐藏后来的失败,worst 会隐藏之前警告与复查的顺序;需要顺序时应保留 event_sequence。
严重程度是本例设定的顺序等级。review=2、warning=3、failure=4 只规定排序,并不表示失败比复查严重两倍。any_selected_event 回答的是是否存在达到所选严重程度阈值的事件,与 any_failure 不同。S02 在阈值 4 时为 0,阈值 3 时为 1,但仍没有失败记录。
例子仅纳入第 1~7 天,first 按天数排序。同一天的记录按 CSV 行顺序处理,不能据此断定实际发生先后。worst 依次按严重程度降序、天数升序、原始行顺序选择。实际运行中应取得更精确的发生时刻与事件 ID,再制定并列与去重规则。
| # 这个例子把同一样本之后的多个后续事件折叠进表结构,并确定代表标签。
import csv
from collections import defaultdict
from pathlib import Path
sample_roster_path = Path("docs/assets/part-03/chapter-05/p3_5_7_sample_roster.csv")
follow_up_events_path = Path("docs/assets/part-03/chapter-05/p3_5_7_follow_up_events.csv")
event_severity_path = Path("docs/assets/part-03/chapter-05/p3_5_7_event_severity.csv")
selected_severity_cutoff = 4
severity_cutoffs = [4, 3, 2]
follow_up_window_days = 7
failure_types = {"failure", "critical_failure"}
preview_row_count = 12
def read_csv(path):
with path.open(newline="", encoding="utf-8") as file:
return list(csv.DictReader(file))
sample_roster = read_csv(sample_roster_path)
follow_ups = read_csv(follow_up_events_path)
severity_table = read_csv(event_severity_path)
severity_by_type = {row["event_type"]: int(row["severity"]) for row in severity_table}
for row in follow_ups:
row["days_after_sample"] = int(row["days_after_sample"])
row["severity"] = severity_by_type[row["event_type"]]
period_events = [row for row in follow_ups if 1 <= row["days_after_sample"] <= follow_up_window_days]
ordered_events = sorted(period_events, key=lambda row: (row["sample_id"], row["days_after_sample"]))
events_by_sample = defaultdict(list)
for row in ordered_events:
events_by_sample[row["sample_id"]].append(row)
folded = []
for sample in sample_roster:
sample_id = sample["sample_id"]
events = events_by_sample.get(sample_id, [])
if events:
first_event = events[0]["event_type"]
worst = sorted(events, key=lambda row: (-row["severity"], row["days_after_sample"]))[0]
worst_event = worst["event_type"]
worst_severity = worst["severity"]
event_sequence = " > ".join(row["event_type"] for row in events)
else:
first_event = "none"
worst_event = "none"
worst_severity = 0
event_sequence = "none"
folded.append(
{
"sample_id": sample_id,
"first_event": first_event,
"worst_event": worst_event,
"worst_severity": worst_severity,
"event_count": len(events),
"event_sequence": event_sequence,
"any_failure": int(any(row["event_type"] in failure_types for row in events)),
"any_selected_event": int(any(row["severity"] >= selected_severity_cutoff for row in events)),
}
)
cutoff_results = []
for cutoff in severity_cutoffs:
selected = [row for row in folded if row["event_count"] > 0 and row["worst_severity"] >= cutoff]
cutoff_results.append(
{
"severity_cutoff": cutoff,
"selected_sample_count": len(selected),
"selected_samples": ",".join(row["sample_id"] for row in selected) or "none",
}
)
print("1) raw follow-up events")
print("sample_id days_after_sample event_type source_system")
for row in follow_ups[:preview_row_count]:
print(
f"{row['sample_id']:>9} {row['days_after_sample']:>18} "
f"{row['event_type']:>16} {row['source_system']:>13}"
)
print(f"... {len(follow_ups) - preview_row_count} more follow-up events")
print()
print("2) severity rule table")
print(" event_type severity")
for row in severity_table[:preview_row_count]:
print(f"{row['event_type']:>16} {int(row['severity']):>9}")
print(f"... {len(severity_table) - preview_row_count} more severity rules")
print()
print(f"3) folded result when severity_cutoff = {selected_severity_cutoff}")
print(
"sample_id first_event worst_event worst_severity event_count"
" event_sequence any_failure any_selected_event"
)
for row in folded[:preview_row_count]:
print(
f"{row['sample_id']:>9} {row['first_event']:>16} {row['worst_event']:>16} "
f"{row['worst_severity']:>15} {row['event_count']:>12} "
f"{row['event_sequence']:>26} {row['any_failure']:>12} {row['any_selected_event']:>19}"
)
print(f"... {len(folded) - preview_row_count} more folded samples")
print()
print("4) sensitivity by severity_cutoff")
print(
" severity_cutoff selected_sample_count"
" selected_samples"
)
for row in cutoff_results:
print(
f"{row['severity_cutoff']:>24} {row['selected_sample_count']:>21} "
f"{row['selected_samples']:>83}"
)
|
期望输出:
| 1) raw follow-up events
sample_id days_after_sample event_type source_system
S01 1 review human_queue
S01 3 warning monitor
S01 5 failure maintenance
S02 2 review human_queue
S02 4 warning monitor
S03 1 revisit service
S04 1 warning monitor
S05 1 revisit service
S05 2 review human_queue
S06 3 minor_adjustment operator
S07 1 warning monitor
S07 6 failure maintenance
... 24 more follow-up events
2) severity rule table
event_type severity
none 0
revisit 1
minor_adjustment 1
inspection 2
review 2
warning 3
failure 4
critical_failure 5
sensor_noise 0
operator_note 1
calibration 1
slow_recovery 2
... 24 more severity rules
3) folded result when severity_cutoff = 4
sample_id first_event worst_event worst_severity event_count event_sequence any_failure any_selected_event
S01 review failure 4 3 review > warning > failure 1 1
S02 review warning 3 2 review > warning 0 0
S03 revisit revisit 1 1 revisit 0 0
S04 warning warning 3 1 warning 0 0
S05 revisit review 2 2 revisit > review 0 0
S06 minor_adjustment minor_adjustment 1 1 minor_adjustment 0 0
S07 warning failure 4 2 warning > failure 1 1
S08 review review 2 1 review 0 0
S09 revisit revisit 1 1 revisit 0 0
S10 warning warning 3 1 warning 0 0
S11 inspection inspection 2 1 inspection 0 0
S12 review warning 3 2 review > warning 0 0
... 24 more folded samples
4) sensitivity by severity_cutoff
severity_cutoff selected_sample_count selected_samples
4 5 S01,S07,S13,S19,S25
3 12 S01,S02,S04,S07,S10,S12,S13,S16,S19,S22,S25,S28
2 21 S01,S02,S04,S05,S07,S08,S10,S11,S12,S13,S16,S17,S18,S19,S21,S22,S24,S25,S26,S28,S29
|
所选样本数在阈值 4 时为 5,阈值 3 时为 12,阈值 2 时为 21。把 4 降为 3,新纳入的是 S02、S04、S10、S12、S16、S22、S28,共 7 个最严重记录为 warning 的样本。这不是产生了新的失败,而是扩大了选择范围。有失败类型记录的样本数仍为 5。
运行前先预测把 selected_severity_cutoff 改为 3 时,S02 的两个标记会怎样变化。答案是 any_selected_event=1、any_failure=0。再假设 S01 的 failure 发生在第 9 天而非第 5 天,七天内结果就变为 first=review、worst=warning、count=2、any_failure=0。该事件落在观测期外,不代表它从原始历史中被删除。
如果 S30 尚未完成追踪,就不能把结果确定为 none 和 0。这段代码假设虚构名单已完成追踪;真实的未完成资料需要增加完成状态,并单独处理为 pending。也不能把这些未来结果混入样本时点的预测输入。结果列是否作为监督学习标签,应在确定预测时点与目标之后判断。
将多个后续事件汇总为样本级结果
这一节压缩的是一点:多个后续事件 并不会自动变成同一个结果列。同一组事件,按 any、first、worst、count 里的不同规则折叠后,会得到不同的代表结果列。
flowchart TD
F[多个后续事件]
R{按什么折叠规则?}
A[Any]
B[First]
C[Worst]
D[Count]
O[代表结果列]
F --> R
R --> A
R --> B
R --> C
R --> D
A --> O
B --> O
C --> O
D --> O
检查清单
来源与参考资料
- Google for Developers, Machine Learning Glossary. 参考 label 与 labeled example 的定义。any/first/worst/count、严重程度等级与失败类型映射均为本例自行设计。 / 2026-09-19
- W3C, PROV-Overview. 提供追踪数据生成与派生过程的一般依据,并不直接规定本节的汇总规则。 / 2026-09-19