Skip to content

第2章 项目搭建与配置设计 ​

2.1 概述 ​

在动手写模型之前,先把项目骨架搭好。一个好的项目结构和配置设计,可以让后续几千行代码有条不紊地组织在一起。

本章会完成两件事:

  1. 搭建 Python 项目结构,配置依赖和开发工具
  2. 设计系统的核心配置——用 dataclass 把所有"魔数"收归一处

2.2 项目结构 ​

coupon-rec/
├── pyproject.toml              # 项目元数据与依赖
├── src/
│   ├── config.py               # 全局配置
│   ├── features/               # 特征工程
│   │   ├── browse_features.py
│   │   ├── user_features.py
│   │   └── feature_pipeline.py
│   ├── models/                 # 四个核心模型
│   │   ├── intent_model.py
│   │   ├── uplift_model.py
│   │   ├── sensitivity_model.py
│   │   └── gmv_model.py
│   ├── optimizer/              # 预算优化
│   │   ├── budget_optimizer.py
│   │   └── coupon_allocator.py
│   ├── experiment/             # AB 实验
│   │   ├── ab_test.py
│   │   └── metrics.py
│   ├── risk/                   # 风控
│   │   └── anti_fraud.py
│   └── serving/                # 在线服务
│       └── api.py
├── scripts/
│   ├── generate_sample_data.py # 模拟数据生成
│   ├── train_models.py         # 模型训练
│   └── run_allocation.py       # 离线分配
├── tests/                      # 38 个单元测试
└── data/                       # 数据目录(不入库)

项目按功能模块划分目录,每个目录对应一个独立的技术主题,也对应本书的一个或多个章节。

2.3 依赖管理 ​

使用 pyproject.toml 管理项目元数据和依赖,这是 Python 社区推荐的现代方式:

toml
<!-- pyproject.toml -->
[project]
name = "coupon-rec"
version = "0.1.0"
description = "智能优惠券发放算法系统 - 电商新用户场景"
requires-python = ">=3.10"
dependencies = [
    "lightgbm>=4.0",
    "scikit-learn>=1.5",
    "pandas>=2.0",
    "numpy>=1.26",
    "scipy>=1.11",
    "fastapi>=0.100",
    "uvicorn>=0.23",
    "pydantic>=2.0",
    "joblib>=1.3",
]

[project.optional-dependencies]
dev = [
    "pytest==8.3.4",
    "pytest-cov==6.0.0",
    "ruff==0.8.4",
]

[build-system]
requires = ["setuptools>=68.0"]
build-backend = "setuptools.build_meta"

关键依赖说明:

依赖用途选型理由
lightgbm所有模型的训练框架工业界结构化数据建模首选,原生支持类别特征
scikit-learn数据拆分、评估指标ML 生态标准库
pandas特征工程、数据处理表格数据处理的事实标准
scipyAB 实验的统计检验z-test、正态分布
fastapi在线预测 API异步高性能,自带 Swagger 文档
pydantic请求/响应数据校验FastAPI 原生支持,类型安全
joblib模型序列化比 pickle 更高效,支持大数组压缩

为什么不用 causalml?

业界常用的 Uber causalml 库依赖 Cython 编译,在部分 Python 版本(如 3.13)上难以安装。本项目直接用 LightGBM 手写 T-Learner,既避开依赖地狱,又能深入理解算法原理。

2.4 核心配置设计 ​

好的配置设计要做到两点:① 所有超参数集中管理 ② 不可变(避免运行时被意外修改)。

Python 的 frozen dataclass 完美满足这两个要求:

python
<!-- src/config.py -->
from dataclasses import dataclass, field

@dataclass(frozen=True)
class CouponConfig:
    """优惠券系统全局配置."""

    # 候选面额集合 (元)
    coupon_amounts: tuple[int, ...] = (5, 10, 15, 20, 30, 50)

    # 预算
    daily_budget: float = 1_000_000.0  # 日预算 100万元

    # 毛利率
    margin_rate: float = 0.20

    # AB 实验流量分配
    control_ratio: float = 0.10       # 对照组 10%
    model_ratio: float = 0.45         # 模型组 45%
    rule_ratio: float = 0.30          # 规则组 30%
    explore_ratio: float = 0.15       # 探索组 15%

    # 风控阈值
    max_coupons_per_day: int = 3      # 单用户日领券上限
    max_accounts_per_device: int = 3  # 同设备最大关联账号数
    refund_rate_threshold: float = 0.5  # 退款率阈值

    # 模型参数
    uplift_threshold: float = 0.0     # Uplift 分阈值,>0 才发券
    observation_window_days: int = 7  # 转化观察窗口

设计要点 ​

frozen=True:配置实例创建后不可修改。如果有人写 config.daily_budget = 0,Python 会直接抛出 FrozenInstanceError。这在多人协作中非常重要——防止某个角落里的代码偷偷改了全局配置。

tuple 而非 list:面额集合用 tuple 而非 list,同样是为了不可变。面额集合在系统生命周期内不应改变。

默认值即文档:每个字段的默认值就是生产环境的推荐值,注释说明了业务含义。新人看代码就能理解每个参数的作用。

2.5 LightGBM 训练参数 ​

四个模型(意图、Uplift、敏感度、GMV)都用 LightGBM,训练参数统一管理:

python
<!-- src/config.py -->
@dataclass
class LGBMParams:
    """LightGBM 通用训练参数."""

    objective: str = "binary"
    n_estimators: int = 500
    learning_rate: float = 0.05
    num_leaves: int = 63
    feature_fraction: float = 0.8
    bagging_fraction: float = 0.8
    bagging_freq: int = 5
    min_child_samples: int = 50
    reg_alpha: float = 0.1
    reg_lambda: float = 0.1
    verbose: int = -1
    random_state: int = 42

    def to_dict(self) -> dict:
        return {k: v for k, v in self.__dict__.items()}

参数说明:

参数值作用
num_leaves=632^6-1控制树的复杂度,过大容易过拟合
feature_fraction=0.880%每棵树随机选 80% 特征,增加多样性
bagging_fraction=0.880%每棵树随机选 80% 样本
min_child_samples=50最少 50叶子节点最少样本数,防过拟合
reg_alpha=0.1L1 正则稀疏化特征选择
reg_lambda=0.1L2 正则平滑系数

LGBMParams 不设 frozen=True——不同模型可能需要覆盖 objective(分类用 "binary",回归用 "regression")。

2.6 特征工程配置 ​

python
<!-- src/config.py -->
@dataclass
class FeatureConfig:
    """特征工程配置."""

    # 时间窗口 (天)
    time_windows: tuple[int, ...] = (1, 3, 7)

    # 类别特征列
    categorical_features: tuple[str, ...] = (
        "device_os",
        "device_brand",
        "device_price_tier",
        "channel_source",
        "city_tier",
        "top1_category_id",
    )

    # 数值特征缺失值填充
    numeric_fill_value: float = -999.0

time_windows = (1, 3, 7):浏览特征会按近 1 天、近 3 天、近 7 天三个时间窗口聚合。窗口太短捕捉不到趋势,太长会引入噪声,(1, 3, 7) 是常用的经验值。

numeric_fill_value = -999.0:LightGBM 原生支持缺失值处理,填充为 -999 而非 0,是为了让模型能区分"真正的 0 值"和"缺失"。

2.7 生命周期阶段枚举 ​

新用户的六个生命周期阶段,用类常量表示,配合默认面额映射:

python
<!-- src/config.py -->
class UserStage:
    REGISTERED_NO_BROWSE = "registered_no_browse"
    BROWSED_NO_CART = "browsed_no_cart"
    CARTED_NO_ORDER = "carted_no_order"
    FIRST_ORDER_DONE = "first_order_done"
    SECOND_ORDER_DONE = "second_order_done"
    SILENT = "silent"

# 默认面额策略 (规则兜底)
STAGE_DEFAULT_AMOUNTS: dict[str, int] = {
    UserStage.REGISTERED_NO_BROWSE: 20,
    UserStage.BROWSED_NO_CART: 15,
    UserStage.CARTED_NO_ORDER: 10,
    UserStage.FIRST_ORDER_DONE: 10,
    UserStage.SECOND_ORDER_DONE: 5,
    UserStage.SILENT: 30,
}

STAGE_DEFAULT_AMOUNTS 是规则策略的兜底方案:当模型不可用(冷启动期、模型故障)时,根据用户所处阶段发放固定面额。注意沉默新客面额最高(¥30),因为召回成本本身就高。

2.8 配置的使用方式 ​

整个系统通过单例配置实例来共享配置:

python
<!-- src/config.py -->
default_config = CouponConfig()
default_lgbm_params = LGBMParams()
default_feature_config = FeatureConfig()

各模块通过构造函数注入配置,默认使用单例:

python
class BudgetOptimizer:
    def __init__(self, config: CouponConfig | None = None):
        self.config = config or default_config

这种模式的好处:

  • 生产环境:直接用默认配置
  • 测试:传入自定义配置(比如设小预算方便测试)
  • 实验:不同实验组可以用不同配置

本章小结 ​

  • 项目采用模块化目录结构,按功能域(features / models / optimizer / experiment / risk / serving)组织
  • pyproject.toml 管理依赖,避免 requirements.txt 的版本锁定问题
  • 核心配置用 frozen dataclass,集中管理、不可变、自文档化
  • LGBMParams 提供统一的模型训练参数,不同模型可覆盖特定字段
  • 六个用户生命周期阶段配合默认面额映射,提供规则策略兜底

思考题 ​

  1. 为什么 CouponConfig 用 frozen=True 而 LGBMParams 不用?在什么场景下你会希望运行时修改配置?
  2. numeric_fill_value = -999.0 和 fillna(0) 在模型训练中有什么区别?哪些特征填 0 更合理?
  3. 如果要支持多个业务线(如服装、3C、生鲜),配置设计需要做哪些改动?

基于 Uplift Modeling 的电商优惠券智能发放系统