PAYATHON 2026

如何用 Python 将模糊文本描述解析为结构化时间序列数据?

技术老齐

建议让 LLM 提取原始短语,再用 Python 按规则处理数字、单位和修饰词,将区间策略设为可配置项,最后用 pandas 整理结果。题目中的这类表达用正则表达式就能解析,也方便核查每个结果的处理过程。

just under 200 million 没有给出下限,仅凭字面无法推导出 195。要把它转换成 195~200,需要先约定业务规则,例如“比目标值低不超过 2.5%”。

先定义模糊表达的处理规则

要得到题目中的结果,可以采用以下约定,所有数值统一以“百万订阅用户”为单位。

表达类型示例处理规则
近似值roughly 30 million保留中心值,输出 30, 30
近似值hovered around 220 million保留中心值,输出 220, 220
略低于just under 200 million下限取 200 × (1 - 0.025),上限取 200
无修饰词200 million保留数值,输出 200, 200

使用这些规则时,需要区分中心值和区间边界的含义:

  • roughly 30 million 的上下限都输出为 30,只是暂时保留近似中心值,不能据此认为实际人数恰好为 3000 万。
  • just under 200 million 严格来说不包含 200。目标表没有表示开闭区间的字段,建议在底层数据中额外保存 upper_inclusive=False。

如果分析需要近似区间,也可以把 roughly x 约定为 [x × 0.95, x × 1.05]。这个比例同样是人为设定的,应根据数据来源和分析用途确定。

Python 实现

下面的解析器支持题目中的英文表达,以及 thousand、million 和 billion 三种单位。它使用完整匹配,遇到规则之外的文本会报错,避免仅提取到其中一个数字就判定解析成功。

代码依赖 pandas,正则表达式和 Decimal 都来自 Python 标准库。

import re
from decimal import Decimal

import pandas as pd


# 示例策略:just under 的下限比目标值低 2.5%
JUST_UNDER_MARGIN = Decimal("0.025")

# 统一转换为“百万订阅用户”
UNIT_TO_MILLIONS = {
    "thousand": Decimal("0.001"),
    "million": Decimal("1"),
    "billion": Decimal("1000"),
}

PATTERN = re.compile(
    r"(?:(?P<qualifier>"
    r"roughly|about|approximately|around|hovered around|just under"
    r")\s+)?"
    r"(?P<value>[0-9]+(?:\.[0-9]+)?)\s+"
    r"(?P<unit>thousand|million|billion)"
    r"(?:\s+subscribers)?"
    r"(?:\s+worldwide)?"
)


def parse_subscribers(text):
    if not isinstance(text, str):
        raise ValueError("订阅人数描述必须是字符串")

    normalized = " ".join(text.casefold().split())
    match = PATTERN.fullmatch(normalized)

    if match is None:
        raise ValueError(f"不支持的订阅人数描述:{text!r}")

    qualifier = match.group("qualifier") or "exact"
    value = (
        Decimal(match.group("value"))
        * UNIT_TO_MILLIONS[match.group("unit")]
    )

    lower = upper = value
    upper_inclusive = True

    if qualifier == "just under":
        lower = value * (Decimal("1") - JUST_UNDER_MARGIN)
        upper_inclusive = False

    return {
        "subscribers_min": float(lower),
        "subscribers_max": float(upper),
        "confidence": "high" if qualifier == "exact" else "medium",
        "qualifier": qualifier,
        "upper_inclusive": upper_inclusive,
    }


data = {
    "raw_extractions": [
        {
            "platform_mention": "Netflix",
            "year_mention": "2012",
            "subscriber_mention":
                "roughly 30 million subscribers worldwide",
        },
        {
            "platform_mention": "Netflix",
            "year_mention": "2020",
            "subscriber_mention": "just under 200 million",
        },
        {
            "platform_mention": "Netflix",
            "year_mention": "2022",
            "subscriber_mention":
                "hovered around 220 million subscribers",
        },
    ]
}

records = []
errors = []

for index, item in enumerate(data["raw_extractions"]):
    try:
        year_text = str(item["year_mention"]).strip()
        if re.fullmatch(r"[0-9]{4}", year_text) is None:
            raise ValueError("年份必须是四位数字")

        platform = item["platform_mention"]
        if not isinstance(platform, str) or not platform.strip():
            raise ValueError("平台名称必须是非空字符串")
        platform = platform.strip()

        # 按需要扩充平台别名表
        platform = {"netflix": "Netflix"}.get(
            platform.casefold(), platform
        )

        parsed = parse_subscribers(item["subscriber_mention"])
        records.append({
            "year": int(year_text),
            "platform": platform,
            **parsed,
            "raw_subscriber_mention": item["subscriber_mention"],
        })

    except (KeyError, ValueError) as exc:
        errors.append({
            "index": index,
            "error": str(exc),
            "raw": item,
        })

columns = [
    "year",
    "platform",
    "subscribers_min",
    "subscribers_max",
    "confidence",
]

# audit_df 保留修饰词、原文和区间边界信息
audit_df = pd.DataFrame(records)

df = (
    audit_df.reindex(columns=columns)
    .sort_values(["platform", "year"])
    .reset_index(drop=True)
)

print(df.to_string(index=False))

if errors:
    print("待复核记录:", errors)

按上述规则,结果为:

yearplatformsubscribers_minsubscribers_maxconfidence
2012Netflix30.030.0medium
2020Netflix195.0200.0medium
2022Netflix220.0220.0medium

保留小数便于处理 30.5 million 等表达,不建议仅为显示整齐就把整列转换为整数。

如何理解 confidence

示例中的 confidence 是自定义的解析标签:

  • high:短语没有近似修饰词,并且完整命中规则。
  • medium:包含模糊修饰词,需要保留近似含义或应用区间策略。

这些标签未经概率校准,也不能保证原始数据正确。即使成功解析了 200 million,LLM 仍可能提取错平台、年份或统计口径。

如果需要评估抽取质量,可以建立人工标注样本,分别检查数字、单位、时间和平台是否提取正确,再确定置信度的计算方式。LLM 自报的置信度不能直接当作可靠概率。

扩展规则时要注意什么

正则表达式适合处理格式相对固定的短语,以下表达需要增加专门规则:

表达需要处理的信息
between 195 and 200 million两个端点共享 million 单位
more than 200 million只有下限,上限未知
at most 200 million只有上限,且包含端点
nearly doubled to 200 million同时包含变化关系和最终值

对于单侧界限,未知端点应保存为缺失值。例如,more than 200 million 无法推出有限的上限。数值解析库可以帮助识别数字和单位,模糊词对应的区间宽度仍需由业务规则确定。

句式增多后,可以让 LLM 先提取中间结构,例如:

{
  "value": 200,
  "unit": "million",
  "relation": "just_under",
  "evidence": "just under 200 million"
}

再由 Python 校验字段、统一单位并计算区间。以后调整 just_under 的处理比例,就可以重新计算已有记录,同时保留原始证据供复核。

时间序列也需要统一统计口径。年末订阅数、全年平均订阅数和某季度订阅数,不能仅凭年份相同就合并;付费订阅账户与用户人数也应分别记录。题目中的数据只有年份,还不足以判断这些差异。

备注:内容仅供参考。