# Dino English · 用户去留(设备生命周期)规则

| 项 | 值 |
| --- | --- |
| 板目录 | `boards/user-lifecycle/` |
| 负责人 | @lee.winqi |
| 主问题 | **装了 App 的人留下了没有;没留下的,走之前发生了什么** |
| 实测依据 | BigQuery,窗口 2026-07-19 → 08-17(30 天)+ 08-04 → 08-17(14 天) |
| 建立日期 | 2026-08-19 |
| 状态 | **规则已定,未开发** |

**这份文件是「用户去留」这块板的唯一标准。**口径有分歧以本文件为准。

本文件只管这一块板。其余看板规则在 [`DASHBOARDS.md`](./DASHBOARDS.md)、[`USER-ROUTE.md`](./USER-ROUTE.md)、
[`PERFORMANCE.md`](./PERFORMANCE.md)、[`NETWORK-THIRDPARTY.md`](./NETWORK-THIRDPARTY.md)、
[`docs/USER-BLOCKER.md`](./docs/USER-BLOCKER.md);页面做法在 [`PAGE-STANDARD.md`](./PAGE-STANDARD.md);
代码写法在 [`DEVELOPMENT.md`](./DEVELOPMENT.md)。本文件不重复、不覆盖。

---

## 0. 为什么要单独做这块板

现有八块板全部在回答**「这一段成不成」**:首屏渲染达成率 95.83%、进教室 WebView 成功率、支付三段成功率……
这些数字都是对的。但把它们全部加起来,也回答不了一个问题:

> **这些人后来还在吗?**

实测(30 天,Android):**一半的新装设备只活跃 1 天**;14 天窗口里 12,273 台设备有 5,290 台卸载。
现有任何一块板都看不见这件事 —— 因为它们的统计单位是**事件**,而这个问题的单位是**设备**。

## 1. 边界

### 1.1 管

| 范围 | 说明 |
| --- | --- |
| 设备的**一生** | 从 `first_open` 到 `app_remove`(或到最后一次活动) |
| **结局** | 卸载了 / 沉默了 / 还在用 |
| 结局的**前情** | 走之前最后做了什么(仅相关性,见 §4.3) |

### 1.2 不管(交给谁)

| 不管 | 归属 |
| --- | --- |
| 任何一段链路的技术成功率 | 对应的技术健康板 |
| 某一段里谁被卡住了 | [`docs/USER-BLOCKER.md`](./docs/USER-BLOCKER.md) |
| 打开 App 之后走到哪一步 | [`USER-ROUTE.md`](./USER-ROUTE.md) |
| 收入、成交、履约 | 支付板与服务端 |

### 1.3 与 user-blocker 的分工(最容易混的一处)

两块板都在讲「用户」,但问的不是一件事:

| | user-blocker | 本板 |
| --- | --- | --- |
| 问题 | **某一段里谁被卡住了** | **这个人最后怎么样了** |
| 单位 | 事件 / 订单 / 阶段事件 | **设备** |
| 时间轴 | 窗口内聚合 | 设备的一生 |
| 范围 | 注册 / 支付 / 课堂三条路径 | 全事件流 |

`USER-BLOCKER.md` §2 明确把「用户级跨阶段漏斗」列为 V1 **不做**的事。本板做的正是它主动留下的空白,
**不是重复建设**。两块板的数字**不得互相印证或相减** —— 单位不同。

---

## 2. 统计单位:设备

### 2.1 唯一口径

```
统计单位 = user_pseudo_id(GA4 导出标准列)
```

实测 events_20260817:三端覆盖率 **100%**(`user_pseudo_id IS NULL` 为 0 条)。
这是它区别于 `ga_session_id` 的关键 —— 后者在 Android 上有四个事件缺 48~68%(见 [`USER-ROUTE.md`](./USER-ROUTE.md) §4.2)。

**为什么不用等 `session_id` / `trace_id`:**这两个 ID 至今「待建」,但它们解决的是别的问题。

| ID | 作用域 | 状态 |
| --- | --- | --- |
| `trace_id` | 一次业务链路 | 待建(仅语音链路有) |
| `session_id` | 一次打开 App | 待建 |
| **`user_pseudo_id`** | **一台设备的全部历史** | **现成,100% 覆盖** |

本板要的是设备级全历史,**不依赖任何新增埋点**。

### 2.2 一台设备 ≠ 一个人(硬约束)

`user_pseudo_id` 是**安装实例 ID**,卸载重装即换新。因此:

- **`first_open` 不等于「新用户」** —— 可能是同一个人重装。本板一律称「新装设备」,**禁止写成「新用户」**。
- **不得用本板的设备数冒充 DAU / MAU**,也不得与任何按人计的口径比较。
- 登录后可用 `user_id` 把多台设备缝成一个账号。实测带 `user_id` 的事件占比:
  Android 76% / iOS 87% / **Web 仅 17%**。Web 基本缝不上,本板 **V1 不纳入 Web**。

### 2.3 不相加

本板的数字**不得与任何其他板相加或相除**。`USER-BLOCKER.md` §4.1 已经写过
「任何页面摘要都不能把这三种单位相加」,本板引入的是**第四种单位**,同样适用。

**两端也各算各的**(项目一贯规则):Android 与 iOS 的结局定义本就不同(§3.3),更不能合并。

---

## 3. 口径

### 3.1 主指标:首日流失率(固定观察期口径)

```
首日流失率 = 安装后第 2 天(install_date + 1)没有任何事件的设备 ÷ 该日全部新装设备
           按安装日(install cohort)分组,两端各算各的
```

实测(Android):**稳定在 60 ~ 72%**(07-20 → 08-14)。

#### ⚠️ 为什么不用「窗口内只活跃 1 天」(2026-08-19 修订,初版口径已废)

本文件初版把主指标定义成「在查询窗口内只活跃 1 天」。**那是错的** ——
它的数值取决于观察了多久,不是业务事实。实测同一批队列,两种口径的差随观察时长单调放大:

| 安装日 | 已观察 | 固定口径 | 窗口口径 | 差 |
| --- | --- | --- | --- | --- |
| 07-20 | 28 天 | 65.3% | 34.4% | **30.9pp** |
| 08-04 | 13 天 | 67.3% | 49.8% | 17.5pp |
| 08-14 | 3 天 | 59.9% | 54.9% | 5.0pp |
| 08-16 | 1 天 | 66.8% | 66.8% | **0** |

窗口越长,同一台设备越有机会被观察到第二次活跃,流失率就越低。**若三个窗口(7 天 / 30 天 / 上个月)
并存,窗口口径会让它们互相打架 —— 而差异不是业务变化,是口径伪影。**

固定口径只看「安装后第 2 天有没有回来」,与窗口长度无关,因此三个窗口可比。这是它作主指标的唯一理由。

标准 D3 / D7 留存同属固定观察期口径,可作下钻,但需要更长的观察期(见 §3.4),V1 不做。

### 3.2 ⚠️ 观察期规则(本板最硬的一条)

**安装日晚于「窗口末尾 − 1 天」的队列,必须整队排除出主指标分母。**

理由是机械的,不是经验值:固定口径要看「安装后第 2 天」,**那一天的日表必须存在**。
实测 08-17 的 `pct_d1 = 100%`,纯粹因为 08-18 还没有日表 —— 不是那天装的人全跑了。

| 安装日 | install + 1 有日表? | 固定口径首日流失率 |
| --- | --- | --- |
| 08-16 | ✅ 有(08-17) | 66.8% ← 真实 |
| 08-17 | ❌ 无(08-18 未结算) | **100.0%** ← 假的 |

**规则:观察期 = 1 天。**页面上**必须显示被排除的队列与设备数**,不许闷掉。

> 初版把观察期定为 3 天,那是配合已废的窗口口径拍的。改用固定口径后,
> 只需保证 D1 那天的日表存在即可,**观察期缩短到 1 天,数据更新鲜、损失的队列更少**。

**窗口起点一侧另有两个坑,见 §3.4。**

### 3.3 结局分类(两端定义不同)

| 结局 | 判定 | Android | iOS |
| --- | --- | --- | --- |
| **卸载** | 有 `app_remove` 事件 | ✅ 可用 | ❌ **数据不存在** |
| **沉默** | 无 `app_remove`,且最后活动距窗口末尾 > 观察期 | ✅ | ✅ |
| **仍活跃** | 最后活动在观察期内 | ✅ | ✅ |

**iOS 没有卸载数据,这是平台限制不是埋点缺陷。**实测 08-04 → 08-17 十四天,
iOS `app_remove` **每天都是 0**,Android 同期每天 330~450。Apple 不提供卸载回调。

**三条硬规则:**

1. **iOS 的卸载相关指标一律显示「平台不支持」,不得留空、不得显示 0、不得用「沉默」冒充「卸载」。**
   显示 0 会被读成「iOS 没人卸载」,那是彻头彻尾的谎。
2. **卸载率不得跨端比较。**分母分子的定义在两端根本不同。
3. **「沉默」不等于「流失」** —— 用户可能只是没打开。措辞上一律用「沉默」,不得写成「流失」「卸载」。

---

### 3.4 窗口设置:三个窗口都能出,但队列要筛

改用固定口径(§3.1)后,**最近 7 天 / 最近 30 天 / 上个月三个窗口互相可比**,与其他板保持一致。
但本板的窗口语义和别的板不同 —— 别的板是「这段时间发生的事」,本板是
**「这段时间安装的设备,观察它们后续」**,因此队列要先筛两道。

#### 筛选一:末尾队列(§3.2)

安装日晚于「窗口末尾 − 1 天」的队列排除。三个窗口一视同仁。

#### 筛选二:单日新装量 < 50 的队列排除

实测 7 月初的 `first_open`(Android):

| 日期 | 07-04 | 07-05 | 07-06 | 07-07 | 07-08 | 07-09 | **07-10** |
| --- | --- | --- | --- | --- | --- | --- | --- |
| 新装 | 3 | 1 | 12 | 7 | 15 | 43 | **1,959** |

**一天装 1 台不是业务现实,是数据不全。**07-10 之前的日表虽然存在,但 `first_open` 几乎没有。
按 50 台为界一刀切掉,干净且有据。被排除的队列**必须在页面上显示**。

#### 三个窗口的实际可用区间

| 窗口 | 标称 | 实际可用队列 | 可用吗 |
| --- | --- | --- | --- |
| 最近 7 天 | 末尾 7 天 | 6 天(排末尾 1 天),约 3,000 台 | ✅ |
| 最近 30 天 | 末尾 30 天 | 29 天 | ✅ |
| 上个月(7 月) | 07-01 → 07-31 | **07-10 → 07-31,共 22 天** | ⚠️ 见下 |

**窗口丢弃规则:**可用队列的新装设备合计 < 500 台,该窗口整个不出(同
[`USER-ROUTE.md`](./USER-ROUTE.md) 的 `MIN_CORE` 做法)。脚本必须把实际可用区间和被排除的队列打到 stderr。

#### ⚠️ 「上个月」窗口混着两批质量完全不同的流量

7 月可用区间内部并不均质,实测(Android):

| 区间 | 日均新装 | 首日流失率 |
| --- | --- | --- |
| 07-10 → 07-19 | 1,300 ~ 2,700 台 | **76 ~ 89%** |
| 07-20 → 07-31 | 240 ~ 1,000 台 | 61 ~ 72% |

前半段是一波投放,量大且**第二天就走掉近九成**;后半段回落到自然量水平。
**整月平均会把这个结构抹平**,得出一个既不代表投放也不代表自然量的数字。

**规则:「上个月」窗口必须同时显示队列级明细(按安装日的折线),不得只给一个整月汇总数。**
这条对 7 天 / 30 天窗口同样适用 —— 本板**任何窗口都必须能下钻到安装日**,
因为流失率对流量结构极其敏感,汇总数单独看会骗人。

## 4. 卸载归因(Android 专属下钻)

### 4.1 样本

实测 08-04 → 08-17(14 天,Android):

| | 台数 |
| --- | --- |
| 全部设备 | 12,273 |
| 其中卸载 | **5,290(43.1%)** |
| 其中**安装也在窗口内**(完整一生可见) | **3,454(占卸载的 65.3%)** |
| 卸载但安装早于窗口 | 1,836 |

卸载设备的活跃天数四分位:**1 / 1 / 1 / 2 / 11**(中位数 **1 天**);
事件数四分位:1 / 21 / 54 / 111 / 2,509(中位数 **54 条**)。

**只有「完整一生可见」的 3,454 台可用于归因**,另 1,836 台的前情在窗口外,**必须排除并标注**。

### 4.2 「走之前最后做了什么」

**先剔掉三个事件再看**:`app_remove`(终点本身)、`user_engagement` / `screen_view`
(GA4 自动事件,不表示任何用户意图)。

**剔除不会丢样本。**某台设备的最后一个事件若是自动事件,剔除后会回退到它更早的那个真动作 ——
实测 3,454 台完整生命周期设备**全部**都有可归因的真动作。

实测(08-04 → 08-17,Android,剔除后 TOP):

| 最后动作 | 台数 | 性质 |
| --- | --- | --- |
| `appsflyer_conversion_result` success | 504 | 背景 |
| `sdk_init_result` success | 417 | 背景 |
| `api_result` **tech_fail** | **280** | 背景 |
| `api_result` success | 263 | 背景 |
| `api_result` user_abort | 169 | 背景 |
| **`click` login_phone** | **104** | **用户动作** |
| `page_ready_result` success | 94 | 背景 |
| **`page_view` subscription** | **87** | **用户动作** |
| `mem_snapshot` | 83 | 背景 |
| **`click` welcome_get_started** | **77** | **用户动作** |
| **`trigger` class_stage_start / end** | **74 / 70** | **用户动作** |
| `voice_tts_result` user_abort | 72 | 半用户动作 |
| `signature_refresh_result` success / tech_fail | 68 / 61 | 背景 |

共 120 种,TOP 15 覆盖 2,423 / 3,454 = 70.2%。

#### ⚠️ 判读纪律:必须区分「背景事件」与「用户动作」

`sdk_init_result` / `appsflyer_conversion_result` / `api_result` / `mem_snapshot` 这类是
**背景事件** —— App 自己在跑,和用户当时在做什么无关。它们排在前面只说明它们频率高,
**不说明用户是因为它们才走的**。

真正可解读的是 `click` / `page_view` / `trigger` 这类**用户动作**:
在登录页点了手机登录(104)、看了订阅页(87)、点了 welcome 引导(77)、上课上到一半(74 / 70)。

**页面必须把两类分开展示,不得混在一张排行榜里。**

#### 一个被推翻的初版结论

本文件初版写过「接口非成功对接口成功约 5:1」。**那是错的**,来自一个方法论错误:
当时没剔自动事件,只统计了「最后一个事件恰好不是自动事件」的那部分设备,样本有偏。

剔除自动事件、全部 3,454 台都参与之后,实测是
**`api_result` tech_fail 280 对 success 263,约 1.06 : 1** —— 接口失败之后卸载的人,
和接口成功之后卸载的人,**几乎一样多**。

**结论:这个指标目前看不出「接口失败导致卸载」。**保留它是为了看用户动作类的信号,
不是为了给接口定罪。

### 4.3 ⚠️ 相关性不是因果

**「最后一个动作是接口失败」不等于「因为接口失败才卸载」。**页面措辞一律用
「走之前最后发生的是……」,**禁止**出现「导致」「因此卸载」「造成流失」这类因果表述。

要证因果需要对照组(同样遇到接口失败但没卸载的设备),**V1 不做**,列入 §8 后续。

---

## 5. 取数条件

### 5.1 成本闸门

**完全沿用 [`DASHBOARDS.md`](./DASHBOARDS.md) §4.4**:必带日期条件、单次窗口 ≤365 天、
禁 `SELECT *`、不确定先 dry-run(>10 GiB 收窄)。

本板的特殊之处:**要按设备聚合全事件流**,不能只查 `app_diagnostic`。
因此单次扫描量比其他板大,**取数脚本必须一次查询拿全所有指标,禁止分指标多次扫表**。
参考实测:30 天全事件流一次聚合约 2.4 GB。

### 5.2 数据边界

| 项 | 值 |
| --- | --- |
| 最早日表 | `events_20260701` |
| 最新已结算 | `events_20260817`(取数时实测,以 `__TABLES__` 为准) |
| **可回溯天数** | **47 天** |

**47 天是硬顶。**任何需要更长观察期的指标(如 D30 留存、长周期回访)**V1 一律不做**,
不得用不足的窗口硬算。

### 5.3 窗口与队列

- 主表窗口 30 天,与其他板一致
- **队列(cohort)= 安装日**,不是活动日
- 按 §3.2 排除窗口首尾队列
- 窗口终点为最新日表时标「末日暂定」(同 [`DASHBOARDS.md`](./DASHBOARDS.md) §4.2.1)

---

## 6. 已知缺口

上线首版必须在页面显式列出:

| # | 缺口 | 影响 | 处理 |
| --- | --- | --- | --- |
| 1 | **iOS 无 `app_remove`** | iOS 看不到卸载,只能看沉默 | §3.3 三条硬规则;显示「平台不支持」 |
| 2 | **重装换 `user_pseudo_id`** | `first_open` 不等于新用户;同一人重装算两台 | §2.2;一律称「新装设备」 |
| 3 | **只有 47 天** | 长周期留存做不了 | §5.2;V1 不做 D30 |
| 4 | **观察期不足会让流失率虚高** | 末尾队列实测高达 100% | §3.2 排除首尾队列并显示排除量 |
| 5 | **卸载归因只有 65.3% 完整样本** | 另 1,836 台前情在窗口外 | 排除并标注,不得混入 |
| 6 | **背景事件排在最后动作前列** | `sdk_init` / `appsflyer` / `api_result` 这类和用户当时在做什么无关,排前面只因频率高 | §4.2 必须剔自动事件,且背景事件与用户动作分开展示 |
| 7 | **相关性不是因果** | 「最后动作」易被读成原因 | §4.3 措辞禁令 |
| 8 | **Web 只有 17% 带 `user_id`** | 缝不上账号 | V1 不纳入 Web |
| 9 | **窗口起点当天队列异常** | 07-19 新装 1,360、卸载率 71.3%,显著偏高 | §3.2 一并排除 |
| 10 | **「沉默」不等于「流失」** | 用户可能只是没打开 | §3.3 措辞纪律 |
| 11 | **07-10 之前 `first_open` 数据不全** | 日均个位数,不是业务现实 | §3.4 筛选二:单日 < 50 台的队列排除并显示 |
| 12 | **「上个月」混着投放高峰与自然量** | 07-10~07-19 首日流失 76~89%,07-20 后 61~72%,整月平均会抹平 | §3.4:任何窗口都必须能下钻到安装日,不得只给汇总数 |

---

## 7. 判读与着色

### 7.1 ⚠️ 本板的数字**不上健康分档色**

[`DASHBOARDS.md`](./DASHBOARDS.md) §5.4 规则 3 写死:**只有技术成功率能上分档色**。

流失率、卸载率、留存率**都不是技术成功率**,一律**不得使用红 / 琥珀 / 青三档**。
理由不只是遵守规则 —— 「首日流失 50%」到底算好算坏,取决于投放质量、渠道、品类,
**没有一个 95% / 99% 那样的普适阈值**。涂上红色就是在替业务下一个数据支撑不了的判断。

**允许的视觉手段:**中性色阶(表达大小,不表达好坏)、与自身历史对比的趋势箭头、
被排除队列的灰显。

### 7.2 沿用的判据

- 样本不足 → 中性灰不判档(同 [`PAGE-STANDARD.md`](./PAGE-STANDARD.md) §3.2)
- 分母为 0 → 显示 `—`,不算 0.00%
- 两端不合并、不相加

---

## 8. 完成条件(DoD)

### V1(本期)

1. 按安装队列出首日流失率,两端各算各的,**已排除首尾队列并显示排除量**
2. 结局分布(卸载 / 沉默 / 仍活跃),**iOS 卸载显示「平台不支持」**
3. Android 卸载归因:完整生命周期样本的「走之前最后做了什么」,**已剔自动事件**
4. 页面显式标注 §6 全部**十二项**缺口
5. 取数脚本 `tools/gen-user-lifecycle-data.py`,窗口参数复用 `win.py`,一次查询取全
6. 自检页 `boards/user-lifecycle/health-user-lifecycle_debug.html` 全 PASS

### 后续(不在本期)

- 因果对照组(§4.3)
- 标准 D1/D3/D7 留存曲线
- 按渠道 / 归因来源下钻
- 账号级(`user_id`)缝合分析

### 8.1 验收清单

沿用 [`DASHBOARDS.md`](./DASHBOARDS.md) §7 通用清单,**另加本板六条**:

- [ ] 页面上**没有任何一处**把设备数与其他板的事件数相加或相除
- [ ] iOS 的卸载相关格子显示「平台不支持」,**不是 0、不是空**
- [ ] 流失率 / 卸载率**没有**使用红 / 琥珀 / 青三档色
- [ ] 被观察期排除的队列数与设备数**在页面上可见**
- [ ] 「最后动作」已剔除 `user_engagement` / `screen_view`
- [ ] 全文**没有**「导致」「因此卸载」「造成流失」这类因果表述;「沉默」没有被写成「流失」
- [ ] 首日流失用的是**固定观察期口径**(安装后第 2 天有没有回来),**不是**「窗口内只活跃 1 天」
- [ ] 每个窗口都能**下钻到安装日**(折线),没有只给整月/整窗汇总数
- [ ] 被排除的队列(末尾 1 天 + 单日 < 50 台)**在页面上可见**

---

## 9. 待拍板

| # | 事项 | 卡不卡开发 |
| --- | --- | --- |
| ① | ~~观察期定 3 天~~ **改定 1 天**(§3.2)。理由是机械的:固定口径只需 D1 那天日表存在 | 已定 |
| ② | ~~主指标用「只活跃 1 天」~~ **改用固定观察期口径**(§3.1)。初版口径会让三个窗口互相打架,实测差达 30.9pp | 已定 |
| ⑤ | 三个窗口(7 天 / 30 天 / 上个月)全出,上个月实际只有 07-10 → 07-31 可用(§3.4)—— 认可否? | 卡 |
| ③ | V1 不纳入 Web(仅 17% 带 `user_id`)—— 认可否? | 不卡 |
| ④ | 板子在导航里叫「用户去留」还是别的名字 | 不卡 |

**需要对外推动:**

| 事项 | 影响 | 找谁 |
| --- | --- | --- |
| iOS 卸载数据无解,是否接受「只看沉默」 | iOS 侧永远没有卸载口径 | PM |
| GA4 导出仅 47 天,是否需要长期归档 | 长周期留存永远做不了 | 数据侧 |

---

## 附:本文件的实测证据

| 证据 | 窗口 | 用在哪 |
| --- | --- | --- |
| `user_pseudo_id` 三端覆盖率 | events_20260817 | §2.1 |
| 日表边界 47 天 | `__TABLES__` | §5.2 |
| 每日设备 / 新装 / 卸载 | 08-04 → 08-17 | §3.3、§4.1 |
| 设备生命周期与卸载样本量 | 08-04 → 08-17 | §4.1 |
| 卸载前最后动作分布 | 08-04 → 08-17 | §4.2 |
| 按安装队列的首日流失率(两种口径对照) | 07-01 → 08-17 | §3.1、§3.2、§3.4 |

全部经 BigQuery 实跑,非文档推断。重跑时注意日表回填([`DASHBOARDS.md`](./DASHBOARDS.md) §4.2.1)。
