在前后端开发联调前 的提交20260223
This commit is contained in:
1
.kiro/specs/spi-spending-power-index/.config.kiro
Normal file
1
.kiro/specs/spi-spending-power-index/.config.kiro
Normal file
@@ -0,0 +1 @@
|
||||
{"generationMode": "requirements-first"}
|
||||
395
.kiro/specs/spi-spending-power-index/design.md
Normal file
395
.kiro/specs/spi-spending-power-index/design.md
Normal file
@@ -0,0 +1,395 @@
|
||||
# 设计文档:SPI 消费力指数(Spending Power Index)
|
||||
|
||||
## 概述
|
||||
|
||||
SPI 是 NeoZQYY 指数体系的第 7 个指数(继 WBI/NCI/RS/OS/MS/ML 之后),粒度为 `(site_id, member_id)`,用于衡量会员在门店内的综合消费力层级。
|
||||
|
||||
SPI 采用"主分 + 子分"结构:
|
||||
- Level(消费水平):基于消费金额和客单价的 log1p 压缩加权
|
||||
- Speed(消费速度):基于绝对速度、相对速度、EWMA 速度的加权
|
||||
- Stability(消费稳定性):基于近 90 天周覆盖率
|
||||
|
||||
SPI 不继承 `MemberIndexBaseTask`(该基类为 WBI/NCI 共享的会员分群逻辑,SPI 不需要 NEW/OLD/STOP 分群),而是直接继承 `BaseIndexTask`,自行实现数据提取和评分逻辑。
|
||||
|
||||
### 设计决策
|
||||
|
||||
1. **继承 BaseIndexTask 而非 MemberIndexBaseTask**:SPI 不需要会员分群(NEW/OLD/STOP),所有有消费记录的会员均参与计算。MemberIndexBaseTask 的 `_build_member_activity` 提取的特征(intervals、t_v/t_r/t_a 等)与 SPI 需求不匹配,复用反而增加耦合。
|
||||
2. **独立数据提取**:SPI 需要按周聚合、日消费序列等 MemberIndexBaseTask 不提供的特征,因此自行编写 SQL 提取逻辑。
|
||||
3. **金额压缩基数自动校准**:首次执行时从门店数据计算中位数作为基数,后续可通过 cfg_index_parameters 手动覆盖。
|
||||
4. **子分独立映射**:Level/Speed/Stability 各自独立做 batch_normalize_to_display,使用不同的 index_type 后缀(SPI_LEVEL/SPI_SPEED/SPI_STABILITY)隔离分位历史。
|
||||
|
||||
## 架构
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph 数据来源
|
||||
A[dwd.dwd_settlement_head<br/>消费订单]
|
||||
B[dwd.dwd_recharge_order<br/>充值订单]
|
||||
end
|
||||
|
||||
subgraph SpendingPowerIndexTask
|
||||
C[extract_spending_features<br/>提取基础特征]
|
||||
D[calculate_level<br/>Level 子分]
|
||||
E[calculate_speed<br/>Speed 子分]
|
||||
F[calculate_stability<br/>Stability 子分]
|
||||
G[calculate_spi_raw<br/>SPI 总分合成]
|
||||
H[batch_normalize_to_display<br/>展示分映射]
|
||||
end
|
||||
|
||||
subgraph 配置
|
||||
I[cfg_index_parameters<br/>index_type='SPI']
|
||||
end
|
||||
|
||||
subgraph 输出
|
||||
J[dws.dws_member_spending_power_index]
|
||||
K[dws.dws_index_percentile_history]
|
||||
end
|
||||
|
||||
A --> C
|
||||
B --> C
|
||||
I --> C
|
||||
I --> D
|
||||
I --> E
|
||||
I --> F
|
||||
I --> G
|
||||
C --> D
|
||||
C --> E
|
||||
C --> F
|
||||
D --> G
|
||||
E --> G
|
||||
F --> G
|
||||
G --> H
|
||||
H --> J
|
||||
H --> K
|
||||
```
|
||||
|
||||
### 继承体系
|
||||
|
||||
```
|
||||
BaseTask
|
||||
└── BaseDwsTask
|
||||
└── BaseIndexTask
|
||||
├── MemberIndexBaseTask ← WBI / NCI(不使用)
|
||||
├── RelationIndexTask ← RS/OS/MS/ML
|
||||
├── MlManualImportTask ← ML 台账导入
|
||||
└── SpendingPowerIndexTask ← SPI(新增)
|
||||
```
|
||||
|
||||
## 组件与接口
|
||||
|
||||
### SpendingPowerIndexTask
|
||||
|
||||
继承 `BaseIndexTask`,实现以下接口:
|
||||
|
||||
```python
|
||||
class SpendingPowerIndexTask(BaseIndexTask):
|
||||
INDEX_TYPE = "SPI"
|
||||
|
||||
DEFAULT_PARAMS = {
|
||||
# 窗口参数
|
||||
'spend_window_short_days': 30,
|
||||
'spend_window_long_days': 90,
|
||||
'ewma_alpha_daily_spend': 0.3,
|
||||
# 金额压缩基数(初始默认值,可被自动校准或配置表覆盖)
|
||||
'amount_base_spend_30': 500.0,
|
||||
'amount_base_spend_90': 1500.0,
|
||||
'amount_base_ticket_90': 200.0,
|
||||
'amount_base_recharge_90': 1000.0,
|
||||
'amount_base_speed_abs': 100.0,
|
||||
'amount_base_ewma_90': 50.0,
|
||||
# Level 子分权重
|
||||
'w_level_spend_30': 0.30,
|
||||
'w_level_spend_90': 0.35,
|
||||
'w_level_ticket_90': 0.20,
|
||||
'w_level_recharge_90': 0.15,
|
||||
# Speed 子分权重
|
||||
'w_speed_abs': 0.50,
|
||||
'w_speed_rel': 0.30,
|
||||
'w_speed_ewma': 0.20,
|
||||
# 总分权重
|
||||
'weight_level': 0.60,
|
||||
'weight_speed': 0.30,
|
||||
'weight_stability': 0.10,
|
||||
# 稳定性参数
|
||||
'stability_window_days': 90,
|
||||
'use_stability': 1,
|
||||
# 映射与平滑
|
||||
'percentile_lower': 5,
|
||||
'percentile_upper': 95,
|
||||
'compression_mode': 1, # log1p
|
||||
'use_smoothing': 1,
|
||||
'ewma_alpha': 0.2,
|
||||
# 速度计算
|
||||
'speed_epsilon': 1e-6,
|
||||
}
|
||||
|
||||
# --- 必须实现的抽象方法 ---
|
||||
def get_task_code(self) -> str: ...
|
||||
def get_target_table(self) -> str: ...
|
||||
def get_primary_keys(self) -> List[str]: ...
|
||||
def get_index_type(self) -> str: ...
|
||||
|
||||
# --- 核心执行流程 ---
|
||||
def execute(self, context=None) -> Dict[str, Any]: ...
|
||||
|
||||
# --- 数据提取 ---
|
||||
def _extract_spending_features(self, site_id, params) -> Dict[int, SPIMemberFeatures]: ...
|
||||
def _extract_recharge_features(self, site_id, params) -> Dict[int, RechargeFeatures]: ...
|
||||
def _calibrate_amount_bases(self, features, params) -> Dict[str, float]: ...
|
||||
|
||||
# --- 子分计算(纯函数,可独立测试) ---
|
||||
@staticmethod
|
||||
def compute_level(features, params) -> float: ...
|
||||
@staticmethod
|
||||
def compute_speed(features, params) -> float: ...
|
||||
@staticmethod
|
||||
def compute_stability(features, params) -> float: ...
|
||||
@staticmethod
|
||||
def compute_spi_raw(level, speed, stability, params) -> float: ...
|
||||
|
||||
# --- 持久化 ---
|
||||
def _save_spi_data(self, data_list, site_id) -> int: ...
|
||||
```
|
||||
|
||||
### 关键设计:子分计算为静态方法
|
||||
|
||||
`compute_level`、`compute_speed`、`compute_stability`、`compute_spi_raw` 设计为 `@staticmethod`,不依赖数据库或任务实例状态。这使得属性测试可以直接调用这些纯函数,无需 mock 数据库连接。
|
||||
|
||||
|
||||
### SPIMemberFeatures 数据类
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class SPIMemberFeatures:
|
||||
"""SPI 计算所需的会员级特征"""
|
||||
member_id: int
|
||||
site_id: int
|
||||
|
||||
# 基础特征
|
||||
spend_30: float = 0.0 # 近30天消费总额
|
||||
spend_90: float = 0.0 # 近90天消费总额
|
||||
recharge_90: float = 0.0 # 近90天充值总额
|
||||
orders_30: int = 0 # 近30天消费笔数
|
||||
orders_90: int = 0 # 近90天消费笔数
|
||||
visit_days_30: int = 0 # 近30天消费日数(按天去重)
|
||||
visit_days_90: int = 0 # 近90天消费日数(按天去重)
|
||||
avg_ticket_90: float = 0.0 # 90天客单价
|
||||
active_weeks_90: int = 0 # 近90天有消费的自然周数
|
||||
daily_spend_ewma_90: float = 0.0 # 日消费 EWMA
|
||||
|
||||
# 子分
|
||||
score_level_raw: float = 0.0
|
||||
score_speed_raw: float = 0.0
|
||||
score_stability_raw: float = 0.0
|
||||
|
||||
# 展示分(归一化后填充)
|
||||
score_level_display: float = 0.0
|
||||
score_speed_display: float = 0.0
|
||||
score_stability_display: float = 0.0
|
||||
|
||||
# 总分
|
||||
raw_score: float = 0.0
|
||||
display_score: float = 0.0
|
||||
```
|
||||
|
||||
## 数据模型
|
||||
|
||||
### dws.dws_member_spending_power_index 表结构
|
||||
|
||||
```sql
|
||||
CREATE TABLE dws.dws_member_spending_power_index (
|
||||
spi_id BIGSERIAL PRIMARY KEY,
|
||||
site_id INTEGER NOT NULL,
|
||||
member_id BIGINT NOT NULL,
|
||||
|
||||
-- 基础特征
|
||||
spend_30 NUMERIC(14,2) DEFAULT 0,
|
||||
spend_90 NUMERIC(14,2) DEFAULT 0,
|
||||
recharge_90 NUMERIC(14,2) DEFAULT 0,
|
||||
orders_30 INTEGER DEFAULT 0,
|
||||
orders_90 INTEGER DEFAULT 0,
|
||||
visit_days_30 INTEGER DEFAULT 0,
|
||||
visit_days_90 INTEGER DEFAULT 0,
|
||||
avg_ticket_90 NUMERIC(14,2) DEFAULT 0,
|
||||
active_weeks_90 INTEGER DEFAULT 0,
|
||||
daily_spend_ewma_90 NUMERIC(14,2) DEFAULT 0,
|
||||
|
||||
-- 子分(Raw)
|
||||
score_level_raw NUMERIC(10,4) DEFAULT 0,
|
||||
score_speed_raw NUMERIC(10,4) DEFAULT 0,
|
||||
score_stability_raw NUMERIC(10,4) DEFAULT 0,
|
||||
|
||||
-- 子分(Display 0-10)
|
||||
score_level_display NUMERIC(5,2) DEFAULT 0,
|
||||
score_speed_display NUMERIC(5,2) DEFAULT 0,
|
||||
score_stability_display NUMERIC(5,2) DEFAULT 0,
|
||||
|
||||
-- 总分
|
||||
raw_score NUMERIC(10,4) DEFAULT 0,
|
||||
display_score NUMERIC(5,2) DEFAULT 0,
|
||||
|
||||
-- 元数据
|
||||
calc_time TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 唯一约束(业务主键)
|
||||
CREATE UNIQUE INDEX idx_spi_site_member
|
||||
ON dws.dws_member_spending_power_index (site_id, member_id);
|
||||
|
||||
-- 查询索引
|
||||
CREATE INDEX idx_spi_display_score
|
||||
ON dws.dws_member_spending_power_index (site_id, display_score DESC);
|
||||
```
|
||||
|
||||
### cfg_index_parameters 新增种子数据
|
||||
|
||||
在 `db/etl_feiqiu/seeds/seed_index_parameters.sql` 中追加 `index_type='SPI'` 的参数行,格式与现有 WBI/NCI 参数一致。
|
||||
|
||||
### 执行流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Scheduler as 调度器
|
||||
participant Task as SpendingPowerIndexTask
|
||||
participant DB as PostgreSQL
|
||||
participant Base as BaseIndexTask
|
||||
|
||||
Scheduler->>Task: execute(context)
|
||||
Task->>DB: 获取 site_id
|
||||
Task->>Base: load_index_parameters('SPI')
|
||||
Base->>DB: SELECT FROM cfg_index_parameters
|
||||
Base-->>Task: params dict
|
||||
|
||||
Task->>DB: 提取消费订单(近90天)
|
||||
Task->>DB: 提取充值订单(近90天)
|
||||
Task->>Task: 聚合会员级特征
|
||||
Task->>Task: 校准金额压缩基数(如需)
|
||||
|
||||
loop 每个会员
|
||||
Task->>Task: compute_level(features, params)
|
||||
Task->>Task: compute_speed(features, params)
|
||||
Task->>Task: compute_stability(features, params)
|
||||
Task->>Task: compute_spi_raw(L, S, P, params)
|
||||
end
|
||||
|
||||
Task->>Base: batch_normalize_to_display(SPI raw scores)
|
||||
Task->>Base: batch_normalize_to_display(Level raw scores)
|
||||
Task->>Base: batch_normalize_to_display(Speed raw scores)
|
||||
Task->>Base: batch_normalize_to_display(Stability raw scores)
|
||||
|
||||
Task->>DB: DELETE FROM dws_member_spending_power_index WHERE site_id = %s
|
||||
Task->>DB: INSERT INTO dws_member_spending_power_index (batch)
|
||||
Task->>Base: save_percentile_history('SPI')
|
||||
Task-->>Scheduler: {status, member_count, records_inserted}
|
||||
```
|
||||
|
||||
|
||||
## 正确性属性
|
||||
|
||||
*正确性属性(Correctness Property)是系统在所有合法执行路径上都应成立的行为特征——本质上是对"系统应该做什么"的形式化陈述。属性是人类可读规格与机器可验证正确性保证之间的桥梁。*
|
||||
|
||||
以下属性基于需求文档中的验收标准推导,每个属性都是可通过 hypothesis 属性测试验证的全称量化命题。子分计算函数(`compute_level`、`compute_speed`、`compute_stability`、`compute_spi_raw`)设计为纯静态方法,不依赖数据库,可直接用于属性测试。
|
||||
|
||||
### Property 1: SPI 总分非负性
|
||||
|
||||
*For any* 非负的 Level、Speed、Stability 子分和非负的权重参数,`compute_spi_raw(L, S, P, params)` 的返回值应为非负。
|
||||
|
||||
推导:`SPI_raw = w_L × L + w_S × S + w_P × P`,当所有子分 ≥ 0 且所有权重 ≥ 0 时,加权和必然 ≥ 0。
|
||||
|
||||
**Validates: Requirements 6.1, 10.1**
|
||||
|
||||
### Property 2: Level 子分关于消费金额单调非递减
|
||||
|
||||
*For any* 非负的特征值和参数,若仅增加 `spend_30` 或 `spend_90`(其他特征不变),`compute_level` 的返回值不应减少。
|
||||
|
||||
推导:`L` 中每一项形如 `w × ln(1 + x/M)`,`ln(1 + x/M)` 关于 `x` 单调递增(`x ≥ 0, M > 0`),权重 `w ≥ 0`,因此增加任一消费金额项只会使 `L` 增加或不变。
|
||||
|
||||
**Validates: Requirements 3.1, 10.2**
|
||||
|
||||
### Property 3: Speed 子分关于 spend_30 单调非递减
|
||||
|
||||
*For any* 非负的特征值和参数,若仅增加 `spend_30`(其他特征不变),`compute_speed` 的返回值不应减少。
|
||||
|
||||
推导:
|
||||
- `V_abs = ln(1 + spend_30 / (max(visit_days_30, 1) × V0))`:关于 spend_30 单调递增
|
||||
- `V_rel = ln((spend_30/30 + ε) / (spend_90/90 + ε))`:spend_30 增加使分子增大,`max(0, V_rel)` 不减
|
||||
- `V_ewma`:不依赖 spend_30,不变
|
||||
- 三项加权和中前两项不减,第三项不变,总和不减
|
||||
|
||||
**Validates: Requirements 4.1, 4.4, 10.3**
|
||||
|
||||
### Property 4: Stability 子分取值范围 [0, 1]
|
||||
|
||||
*For any* `active_weeks_90` 在 [0, 13] 范围内,`compute_stability` 的返回值应在 [0, 1] 范围内。
|
||||
|
||||
推导:`P = active_weeks_90 / 13`,当 `active_weeks_90 ∈ {0, 1, ..., 13}` 时,`P ∈ [0, 1]`。
|
||||
|
||||
**Validates: Requirements 5.2, 5.4, 10.4**
|
||||
|
||||
### Property 5: Display Score 取值范围 [0, 10]
|
||||
|
||||
*For any* 非空的 raw_score 列表(所有值非负),经 `batch_normalize_to_display` 映射后,所有 display_score 应在 [0.00, 10.00] 范围内。
|
||||
|
||||
推导:`batch_normalize_to_display` 内部先 Winsorize 到 [P5, P95],再 MinMax 映射到 [0, 10],最后 `max(0, min(10, score))` 截断。
|
||||
|
||||
**Validates: Requirements 6.6, 10.5**
|
||||
|
||||
## 错误处理
|
||||
|
||||
| 场景 | 处理方式 |
|
||||
|------|----------|
|
||||
| 门店无消费/充值数据 | 返回 `{'status': 'skipped', 'reason': 'no_data'}`,不写入任何记录 |
|
||||
| cfg_index_parameters 中缺少 SPI 参数 | 使用 `DEFAULT_PARAMS` 字典中的默认值,日志 WARNING |
|
||||
| 金额压缩基数为 0 或负数 | 使用 DEFAULT_PARAMS 中的默认基数,日志 WARNING |
|
||||
| orders_90 = 0 导致除零 | `avg_ticket_90 = spend_90 / max(orders_90, 1)`,分母至少为 1 |
|
||||
| visit_days_30 = 0 导致除零 | `V_abs` 公式中 `max(visit_days_30, 1)`,分母至少为 1 |
|
||||
| v_30 和 v_90 均为 0 导致 V_rel 除零 | 使用 `ε`(speed_epsilon,默认 1e-6)防除零 |
|
||||
| 所有会员 raw_score 相同 | `batch_normalize_to_display` 在 `max - min < ε` 时返回 5.0 |
|
||||
| 数据库写入失败 | 事务回滚,抛出异常由调度器处理 |
|
||||
| EWMA 分位历史不存在(首次执行) | 不平滑,直接使用当前分位点 |
|
||||
|
||||
## 测试策略
|
||||
|
||||
### 属性测试(hypothesis)
|
||||
|
||||
属性测试位于 `tests/` 目录(Monorepo 级),使用 `hypothesis` 库。
|
||||
|
||||
每个属性测试对应设计文档中的一个 Property,最少运行 100 次迭代。
|
||||
|
||||
测试文件:`tests/test_spi_properties.py`
|
||||
|
||||
```python
|
||||
# Feature: spi-spending-power-index, Property 1: SPI 总分非负性
|
||||
@given(
|
||||
level=st.floats(min_value=0, max_value=100),
|
||||
speed=st.floats(min_value=0, max_value=100),
|
||||
stability=st.floats(min_value=0, max_value=1),
|
||||
)
|
||||
@settings(max_examples=200)
|
||||
def test_spi_raw_non_negative(level, speed, stability):
|
||||
params = SpendingPowerIndexTask.DEFAULT_PARAMS
|
||||
result = SpendingPowerIndexTask.compute_spi_raw(level, speed, stability, params)
|
||||
assert result >= 0
|
||||
```
|
||||
|
||||
属性测试库:`hypothesis`(已在项目依赖中)
|
||||
|
||||
### 单元测试
|
||||
|
||||
单元测试位于 `apps/etl/connectors/feiqiu/tests/unit/`,使用 FakeDB/FakeAPI 工具。
|
||||
|
||||
重点覆盖:
|
||||
- 边界情况:全零输入、单一极大值输入
|
||||
- 配置回退:参数缺失时使用默认值
|
||||
- 任务注册:验证 task_registry 中 SPI 任务的注册信息
|
||||
- use_stability=0 时稳定性子分不参与计算
|
||||
|
||||
### 测试配置
|
||||
|
||||
- 属性测试:`cd C:\NeoZQYY && pytest tests/test_spi_properties.py -v`
|
||||
- 单元测试:`cd apps/etl/connectors/feiqiu && pytest tests/unit/test_spi_task.py -v`
|
||||
- 每个属性测试标注 `@settings(max_examples=200)`
|
||||
- 每个属性测试注释引用设计文档 Property 编号
|
||||
|
||||
156
.kiro/specs/spi-spending-power-index/requirements.md
Normal file
156
.kiro/specs/spi-spending-power-index/requirements.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# 需求文档: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 算法公式、参数清单、数据来源、计算流程说明
|
||||
131
.kiro/specs/spi-spending-power-index/tasks.md
Normal file
131
.kiro/specs/spi-spending-power-index/tasks.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# 实现计划:SPI 消费力指数(Spending Power Index)
|
||||
|
||||
## 概述
|
||||
|
||||
基于设计文档,将 SPI 指数建设拆分为 DDL 建表 → 核心算法实现 → 任务注册与执行流程 → 配置种子数据 → 属性测试 → 文档更新 六个阶段,每个阶段增量构建并可验证。
|
||||
|
||||
## 任务
|
||||
|
||||
- [x] 1. 创建 DDL 与数据库表
|
||||
- [x] 1.1 编写迁移脚本 `db/etl_feiqiu/migrations/<日期>_create_dws_member_spending_power_index.sql`
|
||||
- 创建 `dws.dws_member_spending_power_index` 表(含序列、唯一索引、查询索引)
|
||||
- 字段定义参照设计文档数据模型章节
|
||||
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5_
|
||||
- [x] 1.2 在测试库 test_etl_feiqiu 执行迁移脚本建表
|
||||
- 通过 TEST_DB_DSN 连接测试库执行 SQL
|
||||
- _Requirements: 1.6_
|
||||
- [x] 1.3 运行 `gen_consolidated_ddl.py` 从测试库导出 DDL 合并到主 DDL
|
||||
- 执行 `python scripts/ops/gen_consolidated_ddl.py`,验证 `docs/database/ddl/etl_feiqiu__dws.sql` 已包含新表
|
||||
- _Requirements: 1.7_
|
||||
|
||||
- [x] 2. 实现 SPI 核心算法(纯函数)
|
||||
- [x] 2.1 创建 `SPIMemberFeatures` 数据类和 `SpendingPowerIndexTask` 骨架
|
||||
- 新建 `apps/etl/connectors/feiqiu/tasks/dws/index/spending_power_index_task.py`
|
||||
- 定义 `SPIMemberFeatures` dataclass
|
||||
- 定义 `SpendingPowerIndexTask` 类继承 `BaseIndexTask`,包含 `INDEX_TYPE`、`DEFAULT_PARAMS`、抽象方法实现
|
||||
- _Requirements: 7.2, 7.3, 9.1_
|
||||
- [x] 2.2 实现 `compute_level` 静态方法
|
||||
- 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)`
|
||||
- 全零输入返回 0.0
|
||||
- _Requirements: 3.1, 3.2, 3.3_
|
||||
- [x] 2.3 实现 `compute_speed` 静态方法
|
||||
- 绝对速度 V_abs、相对速度 V_rel(仅加速加分)、EWMA 速度 V_ewma
|
||||
- Speed 子分公式:`S = w_abs × V_abs + w_rel × max(0, V_rel) + w_ewma × V_ewma`
|
||||
- _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5_
|
||||
- [x] 2.4 实现 `compute_stability` 静态方法
|
||||
- 周覆盖率:`P = active_weeks_90 / 13`
|
||||
- 支持 `use_stability=0` 时返回 0.0
|
||||
- _Requirements: 5.1, 5.2, 5.3, 5.4_
|
||||
- [x] 2.5 实现 `compute_spi_raw` 静态方法
|
||||
- 总分公式:`SPI_raw = w_L × L + w_S × S + w_P × P`
|
||||
- _Requirements: 6.1_
|
||||
- [x] 2.6 编写属性测试:SPI 总分非负性
|
||||
- **Property 1: SPI 总分非负性**
|
||||
- **Validates: Requirements 6.1, 10.1**
|
||||
- [x] 2.7 编写属性测试:Level 子分单调性
|
||||
- **Property 2: Level 子分关于消费金额单调非递减**
|
||||
- **Validates: Requirements 3.1, 10.2**
|
||||
- [x] 2.8 编写属性测试:Speed 子分单调性
|
||||
- **Property 3: Speed 子分关于 spend_30 单调非递减**
|
||||
- **Validates: Requirements 4.1, 4.4, 10.3**
|
||||
- [x] 2.9 编写属性测试:Stability 子分范围
|
||||
- **Property 4: Stability 子分取值范围 [0, 1]**
|
||||
- **Validates: Requirements 5.2, 5.4, 10.4**
|
||||
- [x] 2.10 编写属性测试:Display Score 范围
|
||||
- **Property 5: Display Score 取值范围 [0, 10]**
|
||||
- **Validates: Requirements 6.6, 10.5**
|
||||
|
||||
- [x] 3. 检查点 - 确保核心算法测试通过
|
||||
- 运行 `cd C:\NeoZQYY && pytest tests/test_spi_properties.py -v`
|
||||
- 确保所有属性测试通过,如有问题请询问用户。
|
||||
|
||||
- [x] 4. 实现数据提取与执行流程
|
||||
- [x] 4.1 实现 `_extract_spending_features` 方法
|
||||
- 从 `dwd.dwd_settlement_head` 提取近 90 天消费订单,聚合为会员级特征
|
||||
- 计算 spend_30/90、orders_30/90、visit_days_30/90、avg_ticket_90、active_weeks_90
|
||||
- _Requirements: 2.1, 2.3, 2.4, 2.5_
|
||||
- [x] 4.2 实现 `_extract_recharge_features` 方法
|
||||
- 从 `dwd.dwd_recharge_order` 提取近 90 天充值订单
|
||||
- _Requirements: 2.2_
|
||||
- [x] 4.3 实现 `_compute_daily_spend_ewma` 方法
|
||||
- 对近 90 天日消费序列计算 EWMA
|
||||
- _Requirements: 2.6_
|
||||
- [x] 4.4 实现 `_calibrate_amount_bases` 方法
|
||||
- 从门店数据计算中位数作为金额压缩基数校准值
|
||||
- 配置表优先级高于自动校准值
|
||||
- 日志输出实际使用的基数值
|
||||
- _Requirements: 8.1, 8.2, 8.3, 8.4_
|
||||
- [x] 4.5 实现 `execute` 方法(完整执行流程)
|
||||
- 获取 site_id → 加载参数 → 提取特征 → 校准基数 → 计算子分 → 合成总分 → 归一化展示分 → 持久化
|
||||
- delete-before-insert 策略
|
||||
- 无数据时返回 skipped
|
||||
- 保存分位点历史
|
||||
- _Requirements: 6.2, 6.3, 6.4, 6.5, 6.6, 9.3, 9.4, 9.5_
|
||||
- [x] 4.6 实现 `_save_spi_data` 方法
|
||||
- 批量 INSERT 到 dws_member_spending_power_index
|
||||
- _Requirements: 1.1_
|
||||
|
||||
- [x] 5. 任务注册与模块导出
|
||||
- [x] 5.1 在 `tasks/dws/index/__init__.py` 中导出 `SpendingPowerIndexTask`
|
||||
- 添加 import 和 __all__ 条目
|
||||
- _Requirements: 9.1_
|
||||
- [x] 5.2 在 `tasks/dws/__init__.py` 中导出 `SpendingPowerIndexTask`
|
||||
- 添加 import 和 __all__ 条目
|
||||
- _Requirements: 9.1_
|
||||
- [x] 5.3 在 `orchestration/task_registry.py` 中注册 SPI 任务
|
||||
- `default_registry.register("DWS_SPENDING_POWER_INDEX", SpendingPowerIndexTask, requires_db_config=False, layer="INDEX", depends_on=["DWS_MEMBER_CONSUMPTION"])`
|
||||
- _Requirements: 9.1, 9.2_
|
||||
|
||||
- [-] 6. 配置种子数据
|
||||
- [x] 6.1 在 `db/etl_feiqiu/seeds/seed_index_parameters.sql` 中追加 SPI 参数
|
||||
- 插入 `index_type='SPI'` 的全部参数行(窗口、基数、权重、映射、稳定性)
|
||||
- 金额压缩基数使用合理初始默认值
|
||||
- _Requirements: 7.1, 7.4, 8.5_
|
||||
- [~] 6.2 在测试库执行种子数据脚本
|
||||
- 通过 TEST_DB_DSN 连接测试库执行 INSERT
|
||||
- _Requirements: 7.1_
|
||||
|
||||
- [~] 7. 检查点 - 确保单元测试和集成验证通过
|
||||
- 运行 `cd apps/etl/connectors/feiqiu && pytest tests/unit/test_spi_task.py -v`
|
||||
- 确保所有测试通过,如有问题请询问用户。
|
||||
|
||||
- [ ] 8. 文档更新
|
||||
- [~] 8.1 编写数据库手册 `docs/database/BD_Manual_dws_member_spending_power_index.md`
|
||||
- 包含表结构、字段说明、索引、验证 SQL(至少 3 条)、兼容性说明、回滚策略
|
||||
- _Requirements: 11.1_
|
||||
- [~] 8.2 更新 ETL 任务文档 `apps/etl/connectors/feiqiu/docs/etl_tasks/index_tasks.md`
|
||||
- 新增 DWS_SPENDING_POWER_INDEX 章节,包含算法公式、参数清单、数据来源、计算流程
|
||||
- 更新概述表格和继承体系图
|
||||
- _Requirements: 11.2, 11.3_
|
||||
|
||||
- [~] 9. 最终检查点 - 确保所有测试通过
|
||||
- 运行属性测试:`cd C:\NeoZQYY && pytest tests/test_spi_properties.py -v`
|
||||
- 运行单元测试:`cd apps/etl/connectors/feiqiu && pytest tests/unit/test_spi_task.py -v`
|
||||
- 确保所有测试通过,如有问题请询问用户。
|
||||
|
||||
## 备注
|
||||
|
||||
- 标记 `*` 的子任务为可选(属性测试),可跳过以加速 MVP
|
||||
- 每个任务引用具体需求编号以确保可追溯
|
||||
- 检查点确保增量验证
|
||||
- 属性测试验证全称正确性属性,单元测试验证具体示例和边界情况
|
||||
Reference in New Issue
Block a user