# 需求文档：SPI 消费力指数（Spending Power Index）

## 简介

SPI（Spending Power Index，消费力指数）是 NeoZQYY 指数体系的新增客户级指数，用于评估会员在门店场景内的综合消费力层级。SPI 基于消费水平（Level）、消费速度（Speed）、消费稳定性（Stability）三个子分加权合成，与现有 NCI/WBI/RS/OS/MS/ML 指数协同使用，为运营人员提供客户分层和资源分配依据。

## 术语表

- **SPI_Task**：`SpendingPowerIndexTask`，负责计算 SPI 指数的 ETL 任务
- **BaseIndexTask**：指数算法任务基类，提供展示分映射（Winsorize → 压缩 → MinMax 0-10 → EWMA 平滑）
- **cfg_index_parameters**：`dws.cfg_index_parameters` 表，按 `index_type` + `param_name` 存储指数算法参数
- **dws_member_spending_power_index**：SPI 指数结果表，存储会员级消费力评分
- **Raw_Score**：原始评分，由算法直接计算得出的未归一化分数
- **Display_Score**：展示分，Raw_Score 经 P5/P95 Winsorize → 可选压缩 → MinMax 映射到 [0, 10] 的归一化分数
- **Level_Sub_Score**：消费水平子分，衡量客户消费金额层级与客单水平
- **Speed_Sub_Score**：消费速度子分，衡量近期消费推进速度与节奏变化
- **Stability_Sub_Score**：消费稳定性子分，衡量消费行为的时间覆盖稳定性
- **Winsorize**：分位截断，将值限制在 [P5, P95] 范围内以消除极端值影响
- **EWMA**：指数加权移动平均（Exponential Weighted Moving Average），用于平滑分位点避免批次间波动
- **log1p**：`ln(1 + x)` 压缩变换，用于处理长尾分布
- **settle_type**：结算类型，1=台桌结账，3=商城订单，5=充值订单
- **task_registry**：`orchestration/task_registry.py`，ETL 任务注册表
- **delete-before-insert**：按门店全量刷新策略，先删除该门店所有旧记录再插入新记录

## 需求

### 需求 1：SPI 结果表创建

**用户故事：** 作为 ETL 开发者，我需要创建 SPI 指数结果表，以便存储会员级消费力评分及中间特征。

#### 验收标准

1. THE SPI_Task SHALL 将结果写入 `dws.dws_member_spending_power_index` 表，主键为 `(site_id, member_id)`
2. THE dws_member_spending_power_index 表 SHALL 包含基础特征字段：`spend_30`、`spend_90`、`recharge_90`、`orders_30`、`orders_90`、`visit_days_30`、`visit_days_90`、`avg_ticket_90`、`active_weeks_90`、`daily_spend_ewma_90`
3. THE dws_member_spending_power_index 表 SHALL 包含子分字段：`score_level_raw`、`score_level_display`、`score_speed_raw`、`score_speed_display`、`score_stability_raw`、`score_stability_display`
4. THE dws_member_spending_power_index 表 SHALL 包含总分字段：`raw_score`（SPI 原始分）和 `display_score`（SPI 展示分，numeric(5,2)）
5. THE dws_member_spending_power_index 表 SHALL 包含元数据字段：`calc_time`、`created_at`、`updated_at`
6. THE 开发者 SHALL 编写迁移脚本 `db/etl_feiqiu/migrations/<日期>_create_dws_member_spending_power_index.sql`，在测试库 test_etl_feiqiu 中执行建表
7. WHEN DDL 在测试库执行成功后，THE 开发者 SHALL 运行 `python scripts/ops/gen_consolidated_ddl.py` 从测试库导出最新 DDL，自动合并到 `docs/database/ddl/etl_feiqiu__dws.sql`

### 需求 2：SPI 基础特征提取

**用户故事：** 作为 ETL 开发者，我需要从 DWD 层提取会员消费和充值数据，以便计算 SPI 所需的基础特征。

#### 验收标准

1. THE SPI_Task SHALL 从 `dwd.dwd_settlement_head` 提取近 90 天消费订单（settle_type IN (1, 3)），聚合为客户级特征
2. THE SPI_Task SHALL 从 `dwd.dwd_recharge_order` 提取近 90 天充值订单（settle_type = 5），聚合为客户级充值特征
3. THE SPI_Task SHALL 计算以下基础特征：`spend_30`（近30天消费总额）、`spend_90`（近90天消费总额）、`recharge_90`（近90天充值总额）、`orders_30`（近30天消费笔数）、`orders_90`（近90天消费笔数）、`visit_days_30`（近30天消费日数，按天去重）、`visit_days_90`（近90天消费日数，按天去重）
4. THE SPI_Task SHALL 计算 `avg_ticket_90 = spend_90 / max(orders_90, 1)`
5. THE SPI_Task SHALL 计算 `active_weeks_90`（近90天有消费的自然周数，最多13周）
6. THE SPI_Task SHALL 对近90天日消费序列计算 EWMA 得到 `daily_spend_ewma_90`，平滑系数从 cfg_index_parameters 读取

### 需求 3：Level 子分计算

**用户故事：** 作为 ETL 开发者，我需要实现消费水平（Level）子分算法，以便衡量客户的消费金额层级。

#### 验收标准

1. THE SPI_Task SHALL 按以下公式计算 Level 子分：`L = w_s30 × ln(1 + spend_30/M30) + w_s90 × ln(1 + spend_90/M90) + w_ticket × ln(1 + avg_ticket_90/T0) + w_r90 × ln(1 + recharge_90/R90)`
2. THE SPI_Task SHALL 从 cfg_index_parameters 读取 Level 子分的权重参数（`w_level_spend_30`、`w_level_spend_90`、`w_level_ticket_90`、`w_level_recharge_90`）和金额压缩基数（`amount_base_spend_30`、`amount_base_spend_90`、`amount_base_ticket_90`、`amount_base_recharge_90`）
3. WHEN 所有消费和充值金额均为 0 时，THE SPI_Task SHALL 将 Level 子分 Raw 设为 0.0

### 需求 4：Speed 子分计算

**用户故事：** 作为 ETL 开发者，我需要实现消费速度（Speed）子分算法，以便衡量客户近期消费推进速度。

#### 验收标准

1. THE SPI_Task SHALL 计算绝对速度：`V_abs = ln(1 + spend_30 / (max(visit_days_30, 1) × V0))`
2. THE SPI_Task SHALL 计算相对速度：`V_rel = ln((v_30 + ε) / (v_90 + ε))`，其中 `v_30 = spend_30 / 30`，`v_90 = spend_90 / 90`，`ε` 为防除零小量
3. THE SPI_Task SHALL 计算 EWMA 速度：`V_ewma = ln(1 + daily_spend_ewma_90 / E0)`
4. THE SPI_Task SHALL 按以下公式合成 Speed 子分：`S = w_abs × V_abs + w_rel × max(0, V_rel) + w_ewma × V_ewma`
5. THE SPI_Task SHALL 仅对加速（`V_rel > 0`）加分，不对减速直接扣分（通过 `max(0, V_rel)` 实现）

### 需求 5：Stability 子分计算

**用户故事：** 作为 ETL 开发者，我需要实现消费稳定性（Stability）子分算法，以便识别稳定高消费与偶发冲高。

#### 验收标准

1. THE SPI_Task SHALL 使用近 90 天数据计算稳定性，窗口固定为 90 天
2. THE SPI_Task SHALL 按周覆盖率计算稳定性：`P = active_weeks_90 / 13`（近90天共约13个自然周）
3. WHEN cfg_index_parameters 中 `use_stability = 0` 时，THE SPI_Task SHALL 将 Stability 子分权重视为 0，跳过稳定性计算
4. THE Stability_Sub_Score SHALL 的取值范围为 [0, 1]

### 需求 6：SPI 总分合成与展示分映射

**用户故事：** 作为 ETL 开发者，我需要将三个子分加权合成 SPI 总分并映射为展示分，以便业务人员直观理解客户消费力层级。

#### 验收标准

1. THE SPI_Task SHALL 按以下公式计算 SPI 总分：`SPI_raw = w_L × L + w_S × S + w_P × P`，默认权重 `w_L=0.60`、`w_S=0.30`、`w_P=0.10`
2. THE SPI_Task SHALL 复用 BaseIndexTask 的 `batch_normalize_to_display` 方法将 Raw_Score 映射为 Display_Score（0-10 分）
3. THE SPI_Task SHALL 对 Level、Speed、Stability 三个子分分别独立映射为展示分（0-10 分）
4. THE SPI_Task SHALL 支持通过 cfg_index_parameters 配置压缩模式（`compression_mode`：0=无压缩，1=log1p，2=asinh）
5. THE SPI_Task SHALL 支持通过 cfg_index_parameters 配置 EWMA 分位平滑（`use_smoothing`、`ewma_alpha`）
6. THE Display_Score SHALL 保留 2 位小数，取值范围为 [0.00, 10.00]

### 需求 7：SPI 配置参数管理

**用户故事：** 作为 ETL 开发者，我需要在 cfg_index_parameters 中注册 SPI 的全部默认参数，以便算法参数可配置、可追溯。

#### 验收标准

1. THE 种子数据脚本 SHALL 在 cfg_index_parameters 中插入 `index_type='SPI'` 的全部参数，包括窗口参数、金额压缩基数、子分权重、总分权重、映射与平滑参数
2. THE SPI_Task SHALL 通过 BaseIndexTask 的 `load_index_parameters(index_type='SPI')` 加载参数
3. IF cfg_index_parameters 中缺少某个 SPI 参数，THEN THE SPI_Task SHALL 使用 DEFAULT_PARAMS 字典中定义的默认值
4. THE 种子数据脚本 SHALL 追加到 `db/etl_feiqiu/seeds/seed_index_parameters.sql`

### 需求 8：金额压缩基数校准

**用户故事：** 作为 ETL 开发者，我需要提供金额压缩基数的校准机制，以便各门店的 SPI 评分能适配不同的消费水平分布。

#### 验收标准

1. THE SPI_Task SHALL 在首次执行或参数缺失时，支持从门店历史数据自动计算金额压缩基数的建议值
2. THE SPI_Task SHALL 以近 90 天消费数据的中位数作为各金额压缩基数的默认校准值：`amount_base_spend_30` 取近30天消费中位数、`amount_base_spend_90` 取近90天消费中位数、`amount_base_ticket_90` 取90天客单中位数、`amount_base_recharge_90` 取90天充值中位数、`amount_base_speed_abs` 取每消费日平均消费中位数、`amount_base_ewma_90` 取日消费 EWMA 中位数
3. IF cfg_index_parameters 中已存在对应的金额压缩基数参数，THEN THE SPI_Task SHALL 优先使用配置表中的值而非自动校准值
4. THE SPI_Task SHALL 在日志中输出实际使用的金额压缩基数值，便于运营人员审查和手动调优
5. THE 种子数据脚本 SHALL 为金额压缩基数提供合理的初始默认值（基于典型台球门店消费水平）

### 需求 9：SPI 任务注册与执行

**用户故事：** 作为 ETL 开发者，我需要将 SPI 任务注册到 task_registry 并实现完整的执行流程，以便通过调度器触发计算。

#### 验收标准

1. THE SPI_Task SHALL 以任务代码 `DWS_SPENDING_POWER_INDEX` 注册到 task_registry，`layer="INDEX"`，`requires_db_config=False`
2. THE SPI_Task SHALL 声明依赖 `depends_on=["DWS_MEMBER_CONSUMPTION"]`
3. THE SPI_Task SHALL 采用 delete-before-insert 策略：先按 `site_id` 删除旧记录，再批量插入新记录
4. WHEN 门店无任何消费或充值数据时，THE SPI_Task SHALL 返回 `{'status': 'skipped', 'reason': 'no_data'}` 并跳过计算
5. THE SPI_Task SHALL 在执行完成后保存分位点历史到 `dws_index_percentile_history` 表（index_type='SPI'）

### 需求 10：SPI 算法正确性测试

**用户故事：** 作为 ETL 开发者，我需要通过属性测试（hypothesis）验证 SPI 算法的正确性，以便确保计算逻辑符合 PRD 定义。

#### 验收标准

1. THE 属性测试 SHALL 验证：对于任意非负消费/充值金额，SPI_raw 为非负值
2. THE 属性测试 SHALL 验证：在其他条件不变时，增加 spend_30 或 spend_90 不会导致 Level 子分下降（单调性）
3. THE 属性测试 SHALL 验证：在其他条件不变时，增加 spend_30 不会导致 Speed 子分下降（单调性）
4. THE 属性测试 SHALL 验证：Stability 子分取值范围为 [0, 1]
5. THE 属性测试 SHALL 验证：Display_Score 取值范围为 [0.00, 10.00]
6. THE 属性测试 SHALL 验证：SPI 总分权重 `w_L + w_S + w_P` 之和为 1.0（权重归一化）

### 需求 11：文档更新

**用户故事：** 作为 ETL 开发者，我需要更新相关文档，以便团队成员了解 SPI 的表结构、算法逻辑和使用方式。

#### 验收标准

1. THE 开发者 SHALL 编写数据库手册文档 `docs/database/BD_Manual_dws_member_spending_power_index.md`，包含表结构、字段说明、索引、验证 SQL
2. THE 开发者 SHALL 更新 ETL 任务文档 `apps/etl/connectors/feiqiu/docs/etl_tasks/index_tasks.md`，新增 SPI 任务章节
3. THE 文档 SHALL 包含 SPI 算法公式、参数清单、数据来源、计算流程说明
