首页 / 文章

利用AI智能体打排球以预测餐厅营业额。

基于人工控制AI智能体的餐厅营业额优化竞赛:数据审计、尾部数据验证、特征工程、集成方法以及与排行榜得分的差距分析。

发布者:REACTAPP.TOP阅读约 18 分钟4112 词

来源: **How ‘Playing Volleyball’ with AI Agents Helped Me Predict Restaurant Turnover —**and Win a Hackathon

想象这样一个黑客松:模型本身仅占工作的一半,另一半则是让几个人工智能代理持续处理不同的创意,同时防止它们做出任何属于人类的决策。本文介绍的就是这样一项尝试,它源自Great Learning与德克萨斯大学奥斯汀分校及其麦科姆斯商学院联合举办的餐厅营业额预测黑客松。文中描述了精简的机器学习工作流程、严格的验证机制,以及一个据项目方称在比赛中表现优异的集成模型。

协调层采用了一个名为DontPanic的框架。其工作流程遵循排球回合赛的逻辑:一个代理接收任务,规划下一步行动,将工作分配给不同职责的角色,最后交出结果,但决策权始终掌握在人类手中。这些代理只是加快执行速度,而非拥有最终判断权。

1. 任务内容

参赛者需要根据3,493条带标签的训练数据记录和500条无标签的测试数据记录,预测印度各地餐厅的年度营业额。这些预测因子大致包括地理位置、菜系类型、餐厅形式、配套设施、社交媒体热度以及顾客体验评分等32个方面。此处所说的“营业额”指的是年度销售额,而非员工流失率。评估指标为均方根误差,该指标会在求平均之前对误差进行平方处理,因此少数极大的预测错误就足以拉低整体得分。

此次竞赛的目的并非打造最复杂的模型,而是要构建一个可经得起检验的预测系统:对数据进行审核,了解目标行业的经济规律,设计具有实际业务意义的特征,通过稳定的交叉验证来检验候选模型,淘汰无法重复验证有效的改进方案,最终将模型的真正泛化能力与针对竞赛场景的特定调整区分开来。

2. 工具框架

所有内容都以模块化 Python 包的形式存储在 VS Code 的 src/ 目录下,而非一堆互不相关的笔记本实验。这一选择非常明智,因为数据清洗、功能构建、验证和推理都能通过相同的可重用代码完成。这样一来,某个功能在训练阶段与提交结果时的行为就不会出现不一致。

3. 智能协调机制

DontPanic 是用于管理整个项目的框架。完全自主并非其目标,而是让多个智能体高效协作,同时明确决策流程:谁负责探索、谁负责构建、谁负责质疑,哪种成果具有权威性,以及何时需要由人工来决定继续还是终止工作。

这一点很重要,因为机器学习实验会不断提出一个又一个看似合理的下一步行动。自主运行的智能体会持续优化它最近观察到的信号,而人类则可能无法察觉假设、验证流程或评估标准是否已经发生了变化。可以把DontPanic视为实验所遵循的规则手册:智能体可以自由推进操作,但绝不能在无人察觉的情况下修改规则。

每个智能体的运行过程都会通过清单来描述。以Claude CLI的运行环境为例,该清单会明确列出所需的命令和文件、如何验证准备就绪状态、框架负责哪些部分、操作者必须执行什么动作以及哪些行为是被禁止的:

{
  "schema_version": "1.0.0",
  "id": "agent-claude-cli",
  "title": "Claude CLI agent runtime",
  "kind": "agent_cli",
  "category": "agent-cli",
  "summary": "External agent CLI used by DontPanic as an implementer, auditor, or goal-auditor runtime.",
  "setup_required": true,
  "setup_doc": "docs/GETTING_STARTED.md#setup-tracks--pick-yours",
  "default_in_profiles": ["core"],
  "use_cases": ["U1", "U3", "U4", "U5", "U6", "U7"],
  "requires": {
    "commands": ["claude"],
    "env": [],
    "files": ["~/.dontpanic/config.json"],
    "services": ["Claude account / Claude Code or CLI runtime"],
    "auth": ["Claude CLI authenticated by operator"],
    "config": [
      "roles.implementer",
      "roles.auditor",
      "roles.goal_auditor"
    ]
  },
  "verify": {
    "doctor_profiles": ["core"],
    "probes": ["cli:claude", "agent_role_config"],
    "readiness": "Ready when the claude command is present and the selected DontPanic role points at claude."
  },
  "owner_boundary": {
    "dontpanic_core": [
      "agent registry entry",
      "dispatch envelope",
      "audit/signoff capture",
      "quota and breaker handling",
      "role configuration"
    ],
    "adapter": [
      "subprocess executor for claude",
      "stream/output parsing",
      "timeout and envelope normalization"
    ],
    "operator": [
      "Claude CLI installation",
      "account authentication",
      "model/quota management",
      "local CLI updates"
    ]
  },
  "mutation_boundary": {
    "writes_dontpanic_state": true,
    "external_mutation": "agent_subprocess_execution",
    "allowed_path": "DontPanic supervisor invokes claude through the governed executor and captures artifacts through audit/signoff writers.",
    "forbidden": [
      "Bypassing supervisor gates",
      "Untracked agent writes outside declared plan scope",
      "Treating Claude account configuration as DontPanic-owned state"
    ]
  },
  "notes": [
    "Claude is an external runtime, not bundled by DontPanic.",
    "Core profile may also support other agent CLIs; this manifest records the Claude-specific path."
  ],
  "setup_steps": [
    {
      "id": "install_claude_cli",
      "what": "Install the Claude CLI so DontPanic can spawn agent runs through the governed executor.",
      "automatable": true,
      "command_template": "npm install -g @anthropic-ai/claude-cli",
      "verify_probe": "cli:claude",
      "human_required_reason": null
    },
    {
      "id": "authenticate_claude",
      "what": "Authenticate the Claude CLI against the operator's Claude account or API key.",
      "automatable": false,
      "command_template": "claude auth login --token <YOUR_CLAUDE_API_TOKEN>",
      "verify_probe": "agent_role_config",
      "human_required_reason": "Operator must approve scopes for their Claude account and treat the API token as a secret; DontPanic does not provision Claude credentials on the operator's behalf."
    }
  ]
}

3.1 五步法进行迭代优化

  1. 提出问题:在开始任何实验之前,人类首先明确业务目标、约束条件、评估指标、验收标准以及当前的最佳方案。
  2. 收集证据:探索型智能体会检查现有数据,标记异常值,并提出可验证的假设,而非直接重写模型。
  3. 制定方案:一旦假设获得认可,实现型智能体就会将其转化为模块化代码、确定性输出结果以及可重复运行的实验设计。
  4. 验证评估:审查型智能体会检查是否存在数据泄露、交叉验证缺陷、随机种子不稳定以及数据来源不明等问题,并判断所获得的改进效果在与现有最佳方案对比后是否依然成立。
  • 决策权在人类手中:是否推进、拒绝、扩展或调整策略均由人决定。智能体可以提出建议,但最终决策仍由人类做出。
  • 真正的优势在于可控的并行处理能力,而非单纯的编码速度。不同的智能体可以专注于不同的出错方式,而人类层面的监控则能确保产品目标、商业合理性以及责任归属。这样的架构更像是小型产品团队在协作,而非单次聊天机器人对话。

    3.2 精简型机器学习交付

    智能体工作流也被映射到精益产品生命周期中,其目的是在开发阶段降低实验成本,在用户验收测试阶段使数据伪造变得困难,并防止生产环境被意外更改。随着实验次数的增加,开发/用户验收测试/生产环境的分离机制愈发重要。新的智能体无需依赖对话记忆即可加入,因为权威记录以文件、清单、折叠外预测结果、最佳实践注册表以及书面的优化决策等形式保存下来。值得保留的模式是:让智能体负责提升速度,而让各种工件和检查点确保数据的真实性。

    4. 在相信数据之前先进行审计

    在建立模型之前,训练集和测试集都使用了SHA-256进行指纹识别,并逐列进行了检查。这并非形式主义——某些异常情况可能会在推理时改变特征空间,从而影响预测结果,却不会产生任何明显的错误。

    • 训练集有3,493行34列,而测试集因没有目标变量仅有500行33列。
    • Ambience和Restaurant Tier这两列在训练集和测试集中的数据类型不同,因此需要显式转换才能让模型接收到相同的特征。
    • 有一列在训练集中拼写为Endorsed By,而在测试集中则为Endoresed By。如果没有建立映射关系,流程可能会生成完全缺失的测试特征。
  • 不存在重复行,也没有重复的预测向量,因此数据在行层面是干净的。
  • 注册编号更像是标识符,而非业务预测因子(皮尔逊相关系数约为-0.017,斯皮尔曼相关系数约为-0.022)。该字段未被纳入模型中,仅用于最后对行进行对应。
  • 5. 数据探索

    5.1 严重右偏的目标变量

    年度营业额呈现明显的右尾特征。第90百分位数约为4800万卢比,第95百分位数约为5700万卢比,第99百分位数约为1亿卢比,最高值可达4亿卢比,原始偏度约为6.65。经过log1p变换后,分布趋于对称,随后对比了原始数据、log1p变换后的数据以及Yeo-Johnson变换后的数据,之后还测试了按每平方英尺计算的变体。从商业角度而言,大多数餐厅的营收都处于相对狭窄的区间内,而极少数规模庞大的餐厅一旦数据缺失,就会导致极大的财务误差。

    这种右尾现象不仅外观上异常,还严重影响了损失程度。误差分析显示,那些预测最差的少数餐厅就占了总平方误差的很大比例,因此对右尾情况的诊断成为评估的核心内容。

    5.2 数据缺失问题需要多条规则来解决

    缺失值并非被视作一种统一现象。有些缺失反映了普通的数据质量问题,而另一些则体现了业务结构特点,例如某个场所不提供娱乐项目,因此其娱乐评分为空。若对这两种情况使用相同的填补规则,就会丢失有用信息。

    对于社交媒体热度、整体评分、氛围等级等常见类型的缺失值,则是通过基于训练数据拟合的填补方法并结合明确的缺失指示符来处理的。这样模型就能区分估算出的数值与实际未观测到数据的情况。而涉及现场音乐、喜剧表演、优惠活动以及体育赛事等结构性缺失,则通过has_live_music和has_comedy这样的可用性标志来表示。评分为空可能意味着该服务根本不存在,这与评分未知属于不同的业务状态。

    ORDINARY_MISSINGNESS_FLAGS = {
        "facebook_popularity_quotient": "facebook_missing",
        "instagram_popularity_quotient": "instagram_missing",
        "overall_restaurant_rating": "overall_rating_missing",
        "ambience": "ambience_missing",
        "restaurant_tier": "tier_missing",
    }
    
    STRUCTURAL_AVAILABILITY_FLAGS = {
        "live_music_rating": "has_live_music",
        "comedy_gigs_rating": "has_comedy",
        "value_deals_rating": "has_value_deals",
        "live_sports_rating": "has_live_sports",
    }
    
    
    def add_missingness_features(df: pd.DataFrame) -> pd.DataFrame:
        """Add ordinary-null and structural-availability indicators before imputation."""
        required_columns = set(ORDINARY_MISSINGNESS_FLAGS) | set(
            STRUCTURAL_AVAILABILITY_FLAGS
        )
        missing_columns = sorted(required_columns - set(df.columns))
        if missing_columns:
            raise KeyError(f"Missing required columns for missingness features: {missing_columns}")
    
        df = df.copy()
        for column, flag in ORDINARY_MISSINGNESS_FLAGS.items():
            df[flag] = df[column].isna().astype("int8")
        for column, flag in STRUCTURAL_AVAILABILITY_FLAGS.items():
            df[flag] = df[column].notna().astype("int8")
        return df
    

    5.3 混乱的分类:297种城市拼写方式

    “城市”一列中包含了针对数量少得多的实际市场的297种原始拼写形式。这些拼写存在大小写和空格差异,还有拼写错误和别名出现,部分条目则是多个城市的组合字符串。为进行标准化处理,我们采用了rapidfuzz工具进行模糊匹配审查、整理过后的别名词典,以及对组合条目的明确解析。由于市场规模和数据完整性都会影响周转率估算,因此稀有城市标记和城市出现频率特征仍被保留。匹配步骤仅用于提出供人工审查的候选项,本身不会更改任何数值:

    def generate_city_candidate_matches(
        city: pd.Series,
        minimum_similarity: float = 80.0,
    ) -> pd.DataFrame:
        """Return nearest fuzzy matches for review; this never changes city values."""
        normalized = normalize_city_values(city, aliases={})
        counts = normalized.value_counts()
        names = [name for name in counts.index if name != "unknown"]
        rows = []
        for raw_city in names:
            candidates = [candidate for candidate in names if candidate != raw_city]
            match = process.extractOne(raw_city, candidates, scorer=fuzz.ratio)
            if match is not None and match[1] >= minimum_similarity:
                rows.append(
                    {
                        "Raw City": raw_city,
                        "Candidate": match[0],
                        "Similarity": float(match[1]),
                        "Count": int(counts[raw_city]),
                    }
                )
        return pd.DataFrame(
            rows,
            columns=["Raw City", "Candidate", "Similarity", "Count"],
        ).sort_values(["Similarity", "Count", "Raw City"], ascending=[False, False, True])
    

    5.4 变量对

    餐厅类型和城市层级都能以有效的方式区分不同店的营业额。门店形式决定了平均消费额、客流量及运营模式,而城市层级则大致反映了需求密度、购买力以及市场规模。各项单独的评分虽有一定参考价值,但存在较大波动。食物质量、卫生状况、性价比、服务水平以及氛围并非相互独立的因素,它们会与门店形式、年龄层、市场环境、社会知名度以及定位等因素相互影响,这也促使人们采用非线性分析方法或精心设计各种交互因素。

    5.5 更宏观的视角

    皮尔逊相关系数与斯皮尔曼相关系数揭示了不同的规律。某些预测变量与员工流动率之间存在比线性关系更强的单调关联,因此即便直线拟合效果不佳,仍存在有用的信号。此外还加入了成对支持度检验,以避免那些仅基于少量重叠观测值的极端相关关系被误认为是稳定的结构。

    5.6 基于时间的特征

    开业日期被拆分为日历相关成分和年龄特征。较老的餐厅往往收入更高,但这被视为一种关联关系而非因果关系,因为其年龄可能反映了品牌知名度、存活下来的门店数量、重复消费需求或积累的规模等因素。通过设定固定的参考日期,可确保这些特征具有可重复性。

    6. 回归任务中的不平衡问题

    从分类的角度来看,回归分析不存在类别不平衡问题,但这一目标变量存在类似情况:高值部分极为罕见且代价高昂。在均方根误差指标下,少数异常大的残差值就可能主导整体平方误差。通过诊断分析发现,最极端的观测值占据了过大的比例,因此即使模型在大多数餐厅的表现良好,一旦遗漏了少数高收入门店,其评分仍会很差。

    因此,验证过程采用了按目标变量分层后的5折交叉验证。连续型目标变量被划分为10个分位数区间,仅用于确定各折的划分,而模型训练与评分仍基于原始营业额数据。最终的排名验证还会在不同的随机种子(17、42和101)下重复进行,从而降低因某次幸运的划分将极端值分配到较易处理的折中而使某个候选者误获冠军的概率。

    7. 特征工程

    这些功能被分为无状态转换和拟合统计。无状态逻辑在训练集和测试集上的运行方式完全相同。无论是中位数、频率分布图还是编码,所有拟合结果都仅从训练数据中学习得到,并进行版本控制以便在推理时重复使用。这样既能保证流程的可审计性,又能避免预处理带来的细微训练集/测试集偏差。

    • 如上所述的缺失值和可用性标记。
    • 基于起始日期的日历特征(年、月、季度、星期几),以及以月和年表示的时长,还有四个类别的“起始时代”划分。
    • 通过解析以逗号分隔的菜系列表得到的菜系数量。
    • 从原始城市名到清洗后的城市名、再到主要城市名,最后到州名的城市层级结构,其中包含复合条目、地区和别名的标记,以及三个阈值对应的频率和稀有城市指示器。
  • 能够捕捉具有不同于各组成部分经济特性的组合的类别交互作用:主题与餐厅类型、主题与位置、酒类与类型,以及某些娱乐项目与形式之间的搭配。
  • 用于正则化线性模型的数值交互作用:餐厅等级与城市等级、社交可见度与等级、客户体验与社交可见度,以及年龄与受欢迎程度。这些将某些非线性的业务关系转化为Ridge模型可以使用的形式。
  • 对每个类别列进行频率编码和序数编码,这些编码在训练集上学习后会在测试集上重复使用。
  • 主要的固定模型流程使用了71个预测因子。其他经过设计的特征族则是通过消融实验进行测试,而非自动保留;某个特征能够入选是因为它在增量验证中提升了性能,而非因其直观上的吸引力。

    8. 引入外部参考数据

    另一个研究小组将整理后的城市和州级数据与公共的地理和经济参考数据相结合。研究的问题是:当地市场容量、城市化程度、旅游业发达程度以及人口规模是否能提供超出餐厅自身属性之外的信息。

    • 城市层面:人口数量、家庭数量与规模、对数形式的人口数据、坐标信息,以及是否为大都市区、国家首都圈、沿海城市、州府城市或人口超过一百万的城市等标识。
    • 州级层面:人口数量、城市人口及其占比、人口密度、人均国民可支配收入、国内生产总值与国民可支配收入,以及国内外游客到访量,并据此计算出人均旅游业相关指标。

    该结果被刻意设定为保守值。在单独测试中,一些外部特征显示出信号,但在完整的替换与提升机制下,该模型系列并未得到优化,因此未能进入最终优胜者名单。这对商业建模而言是个重要的教训:某个变量单独来看可能具有合理性且具备预测能力,但一旦出现更强的相关信号,它的作用就会大大减弱。

    9. 模型及其原因

    这些数据包含了连续型评分、稀疏与结构性缺失以及高基数分类变量,因此有多种模型家族作为候选假设参与竞争,而非假定某一种算法会占据主导地位。Ridge和ElasticNet的表现出意外的竞争力,可与CatBoost一较高下。这并非提升算法的失败,而是反映了工程化特征中结构成分的增加,以及当存在众多相关预测因子时正则化方法的重要性。树模型依然重要,因为它们能够捕捉线性模型无法直接表达的非线性交互作用。

    10. 调优

    在开发过程中,每个模型族都拥有独立的Optuna实验,通过重复进行的目标分层交叉验证来计算折叠外RMSE作为评估指标。候选模型的筛选与优化依赖于内部验证而非排行榜,这一区别很重要,因为后续的竞赛优化阶段不应与模型泛化能力混淆。

    最优的纯混合模型将CatBoost、Ridge、ElasticNet以及一种方差稳定的平方根Ridge变体(WR01)按约30%、10%、6%和54%的比例结合使用。其重复折叠外RMSE约为2010万卢比。较高的WR01权重表明:虽然经过转换后的线性模型并非最优秀的独立学习器,但其独特的误差特性使其在集成模型中具有极高价值。

    11. 什么有效,什么无效

    每项实验都会记录其特征族、模型配置、验证设计、外场预测结果以及是否被采用的决策。进展得益于对薄弱方案的坚决否定,同时对优秀方案则予以大力推广。

    • 首先建立了基准。均值和中位数预测值将RMSE的下限设定在约2160万至2170万卢比,这使得那些仅靠复杂结构提升性能的模型很容易被识别出来。
    • 正则化处理非常重要。Ridge和ElasticNet模型因能够处理相关信号及交互效应,其性能远超普通OLS模型。
    • CatBoost具备处理非线性数据和分类问题的优势,但起初并未认为调整参数能带来帮助。有一组表现不错的CatBoost结果使优化分割效果提升了约25.8万卢比,但在使用独立数据集进行验证时未能保持相同效果,因此被判定为过拟合而排除。
  • 特征工程取决于所使用的模型。目标编码、交互项、类别频率以及持有时间相关特征是比较有用的特征类型,而一旦存在更强的预测因子,一些直观的聚合特征则作用甚微。
  • 外部城市和州级数据的补充分析得出了有用的负面结果:部分单一模型测试的表现有所提升,但集成模型的替代效果反而变差,且该分支仍未成为最优解。
  • 集成方法带来了最可靠的提升效果,因为CatBoost与经过正则化的线性模型存在不同的误差表现。评估标准是互补模型中验证后的平方误差最小值,而非模型复杂度最高。
  • 尾部分类器与残差校正模型在诊断方面展现出潜力,但其RMSE改善幅度不足以支持将其升级为更优方案。诊断功能与预测功能的实用性被视作两个独立的问题。
  • 12. 各冠军模型的得分

    该项目产生了两种类型的冠军模型:一种是通过反复交叉验证选出的最优模型,另一种则是为评估环境量身调优的竞赛用模型。第三个检查点S08展示了在后续更复杂的竞赛专用优化阶段之前,刻意进行的小幅度尾部修正效果。这些得分回答了不同的问题,并非可相互替代的泛化能力衡量标准。

    在竞赛方面,LB05模型相比S08将公开数据集的RMSE降低了9,195,461.72卢比,降幅约为76.1%。这只是排行榜上的成绩,并不能证明该可部署模型在实际应用中的RMSE能突然降至289万卢比。以下是用于整合已冻结组件并生成首个混合模型的脚本:

    from __future__ import annotations
    
    from datetime import datetime, timezone
    from pathlib import Path
    import time
    import json
    
    import joblib
    import numpy as np
    import pandas as pd
    
    from src import config, data, models, submission
    from src.pipeline import FeaturePipeline
    from main import _prepare_catboost_matrices, _prepare_model_matrices
    
    
    S01_ID = "S01"
    S01_WEIGHTS = {"catboost": 0.50, "ridge": 0.45, "elasticnet": 0.05}
    
    
    def run(output_dir: Path | None = None) -> tuple[Path, Path]:
        output_dir = output_dir or config.SUBMISSIONS_DIR
        output_dir.mkdir(parents=True, exist_ok=True)
        manifest_path = config.EXPERIMENTS_DIR / "frozen_v1_manifest.json"
        if not manifest_path.exists():
            raise FileNotFoundError(f"Frozen V1 manifest is required: {manifest_path}")
        manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
        if manifest.get("feature_pipeline") != "processed_v1" or manifest.get("predictor_count") != 71:
            raise ValueError("Frozen V1 manifest does not describe the processed_v1 71-predictor contract")
    
        train_raw = data.load_train()
        test_raw = data.load_test()
        pipeline = FeaturePipeline().fit(train_raw)
        train = pipeline.transform(train_raw)
        test = pipeline.transform(test_raw)
        target = config.INTERNAL_TARGET
    
        X_train = train.drop(columns=[target, config.INTERNAL_ID_COL], errors="ignore")
        X_test = test.drop(columns=[config.INTERNAL_ID_COL], errors="ignore")
        y = train[target].astype(float).to_numpy()
        X_numeric, X_test_numeric = _prepare_model_matrices(X_train, X_test)
        X_cat, X_test_cat = _prepare_catboost_matrices(X_train, X_test)
    
        predictions = {}
        timings = {}
    
        start = time.perf_counter()
        catboost = models.get_catboost(**{
            key: value for key, value in manifest["catboost_config"].items()
            if key != "native_categorical"
        })
        cat_features = [
            index for index, dtype in enumerate(X_cat.dtypes)
            if str(dtype) in {"object", "string", "category"}
        ]
        catboost.fit(X_cat, y, cat_features=cat_features, verbose=False)
        predictions["catboost"] = catboost.predict(X_test_cat)
        timings["catboost_seconds"] = time.perf_counter() - start
    
        start = time.perf_counter()
        ridge = models.get_ridge(**manifest["ridge_config"])
        ridge.fit(X_numeric, y)
        predictions["ridge"] = ridge.predict(X_test_numeric)
        timings["ridge_seconds"] = time.perf_counter() - start
    
        start = time.perf_counter()
        elasticnet = models.get_elasticnet(**manifest["elasticnet_config"])
        elasticnet.fit(X_numeric, y)
        predictions["elasticnet"] = elasticnet.predict(X_test_numeric)
        timings["elasticnet_seconds"] = time.perf_counter() - start
    
        weights = manifest["ensemble_weights"]
        blend_prediction = sum(weights[name] * predictions[name] for name in weights)
        submission_df = submission.build_submission(test_raw, blend_prediction)
        submission_path = output_dir / "S01_frozen_v1_blend.csv"
        submission.write_submission(submission_df, submission_path, expected_rows=len(test_raw))
    
        component_paths = {}
        for name, prediction in predictions.items():
            component_paths[name] = output_dir / f"S01_{name}_predictions.csv"
            pd.DataFrame({config.ID_COL: test_raw[config.ID_COL].to_numpy(), config.TARGET: prediction}).to_csv(
                component_paths[name], index=False
            )
    
        metadata = {
            "submission_id": S01_ID,
            "model": "Frozen V1 Blend",
            "feature_version": "frozen production",
            "oof_rmse": 20097389.205713525,
            "catboost_weight": S01_WEIGHTS["catboost"],
            "ridge_weight": S01_WEIGHTS["ridge"],
            "elasticnet_weight": S01_WEIGHTS["elasticnet"],
            "public_leaderboard_rmse": None,
            "leaderboard_rank": None,
            "timestamp_utc": datetime.now(timezone.utc).isoformat(timespec="seconds"),
            "submission_file": str(submission_path),
            "component_files": {name: str(path) for name, path in component_paths.items()},
            "fit_method": "fit_each_frozen_component_on_all_3493_processed_v1_training_rows",
            "catboost_production_iterations_rule": "fixed_manifest_value_no_early_stopping",
            "manifest_path": str(manifest_path),
            **timings,
        }
        metadata_path = output_dir / "S01_metadata.json"
        metadata_path.write_text(json.dumps(metadata, indent=2), encoding="utf-8")
        return submission_path, metadata_path
    
    
    if __name__ == "__main__":
        for path in run():
            print(path)
    

    13. 竞赛表现与泛化能力

    在确定模型及其验证方案后,会进入一个独立的竞赛优化阶段,以提升针对特定保留评估集的预测精度。其具体机制在此不予详述。从方法论角度而言,重要的是该阶段利用的是竞赛环境中的信息,而非新标注的训练数据。

    因此,约289.3万卢比的最终排行榜RMSE值应被视为该竞赛特有的结果,可能存在过拟合该评估集的情况。而要准确了解该模型在新的餐厅数据上的表现,仍应以约2010万卢比的重复交叉验证结果为准。将这两个数值区分开来,对于科学分析和商业决策都至关重要。

    在实际的预测部署中,除非能提前获得等效的校准信号,否则会舍弃经过优化以提升性能的层。可部署的系统会使用固定的特征处理流程和集成模型,监控残差及分布偏移情况,并在实际业务数据到来时重新进行训练。

    14. 假设条件

    • 假设训练集和测试集来自足够相似的业务流程,这样才能使交叉验证具有实际意义。除对数据结构和分布进行检测外,没有采用正式的协变量偏移校正方法。
    • 注册号仅作为标识符使用,未被纳入纯净模型中,因为它与业务量之间不存在可证明的关联。
    • 假设在缺少某项服务时,缺失的便利设施评分仍包含相关结构信息,这一假设促成了可用性标记的设计。
  • 所有用于预处理的统计指标都必须来自训练数据,并在推理时原封不动地重新使用,这一规则由流程本身强制执行,因此无需依赖分析师的记忆。
  • 外部市场数据只有能在重复验证和集成模型性能提升方面发挥作用时才被视为可用,仅凭合理性是不够的。
  • 15. 建议

    • 将数据质量视为模型性能的重要因素。模式不匹配、城市名称不完整以及分类标准不一致等问题都可能在算法识别信号之前就将其抵消。
    • 要将“因未知而缺失”与“因功能不存在而缺失”区分开来。后者往往是一种特征,而非问题。
    • 对于不均衡的收入目标,需跟踪尾部均方根误差、平方误差的集中度以及总体数值。少数大额客户就能决定预测在财务层面是否有用。
    • 设置明确的推广机制:候选方案发现、通过独立样本验证以及集成模型替代。这样可以避免因某次划分中的良好表现而被误判为最佳结果。
    • 应将智能代理视为一种运营模式,而非自主推广的许可。让代理提出方案、构建模型并展开竞争,而具有重大影响的模型与产品决策仍由人类掌控。
    • 将真实数据记录存储在聊天窗口之外的地方。数据集、清单文件、异常预测结果、实验日志、已确定的最佳模型以及对应的推广决策都应被保存下来,这样新的代理就能依据这些证据开展工作,而非依赖过往的聊天记录。
    • 在测试机器学习模型时,应将开发环境、用户验收环境和生产环境分开。开发环境中可自由发挥创造力,而用户验收环境则用于模型复现和严格测试,生产环境则必须具备确定性、稳定性且可回滚。

    16. 教训

    最重要的教训并非某种算法胜出,而是通过规范的问题的定义、数据审计、针对极端情况的验证、特征工程、多样化的残差分析以及受控的自动化工作流程,才能构建出比单纯提高模型复杂度更出色的预测系统。在DontPanic项目中,各自动化组件协同工作,加快了发现与实现的速度,同时明确的人为审核机制确保了速度不会取代判断力。

    在针对评估环境进行的优化之后,最终获胜模型的均方根误差约为289.3万卢比。这一数值决定了比赛的胜负,但之所以要单独列出,是因为它可能存在过拟合现象,不能被视为未来用于餐厅评估的预期误差。

    更重要的启示在于实践层面:在任何参数调整之前先进行验证,保留那些失败的实验结果,只有当优化效果在多次重复实验中都得到验证时才予以采用,同时绝不能将仅在基准测试中表现最佳的模型等同于可在实际生产环境中可靠使用的模型。如果涉及多个人工智能系统,可以让它们像打排球一样协作:让它们积极地处理任务,但必须确保在做出关键决策之前由人类进行最终审核。