# 全局优选搜索算法说明

## 1. 设计目标与应用场景

全局优选搜索算法用于在“可比公司+多指标”的组合空间中，自动寻找一组 **指标集合** 与 **可比公司集合**，使得：

- 在满足一系列约束（样本数量、调整系数区间、估值倍数合理区间等）的前提下；
- 调整后的估值倍数均值尽量靠近给定目标值（或目标区间中心）；
- 有效可比公司数量尽可能多；
- 调整后估值倍数分布更集中（标准差更小、集中度更高）。

典型使用场景：

- 估值日期已确定，标的公司及候选可比公司已筛选；
- 已完成指标归一化、熵权计算所需的准备步骤；
- 需要在“估值倍数别名（PE/PB/PS 等）+ 指标集合 + 可比公司集合”之间自动寻找合理的全局优选解。

## 2. 数据准备与输入结构

算法依赖以下几类核心数据：

- **标准化指标数据**：`FinancialIndicatorNormalization`
  - `indicators`: 指标列表（含 `itemid` 等元信息）；
  - `normalized_data`: 每家公司在各指标上的标准化值（0-1 或其他归一化区间）。
- **估值倍数原始结果**：`comparable_company_calc_batches.batch_result`
  - 对每个 `stock_code`（公司代码）存储一次批量计算后的估值倍数 `value`，记为 `y_raw`；
- **标的公司识别**：
  - 以 `LOCAL_COMPANY*` 开头的公司代码视为标的公司；
- **调用参数**（以 `greedy_scan_max_peer_avg` 为例）：
  - `project_id`, `valuation_date_id`, `alias`：定位项目、估值日、估值倍数别名；
  - `min_company_count`, `min_indicator_count`：最小公司数、最小指标数约束；
  - `step_ratio`, `span_ratio`, `max_steps`：扫描模式的步长、范围与最大步数；
  - `lower`, `upper`：估值倍数比例区间（例如 0.8–1.2）；
  - `r_lower`, `r_upper`：调整系数区间，用于约束 `r_c = X_T / X_c`；
  - `selected_company_codes`：前端进一步筛选后的可比公司白名单（可选）。

### 2.1 标的公司与可比公司集合

- 从归一化数据中提取所有公司代码：`companies_all`；
- 通过前缀 `LOCAL_COMPANY` 识别标的公司，记为 `target_code`；
- 可比公司集合：
  - 需同时满足：
    - 非标的公司；
    - 在 `batch_result` 中存在非空且有限的 `y_raw` 值；
    - 如前端传入 `selected_company_codes`，则必须在该集合中；
  - 经过排序后与标的公司一起组成初始公司列表 `company_codes`。

### 2.2 指标集合

- 从归一化数据中的 `indicators` 提取所有指标 ID：`all_indicators`；
- 经过排序后，形成算法的初始指标列表 `indicator_ids`；
- 要求 `indicator_ids.len() >= min_indicator_count`，否则直接报错返回。

## 3. 熵权得分与调整系数

算法的核心思想之一，是利用熵权法计算“公司综合得分”，然后用标的公司与可比公司得分之比构造调整系数 `r_c`，对估值倍数进行调整。

### 3.1 熵权得分计算

给定当前指标集合 $I$ 与公司集合 $C$，先对每个公司 $c \in C$，根据标准化值与熵权计算综合得分：

1. 对每个指标 $i \in I$：
   - 使用 `EntropyWeightService::calculate_entropy_weights` 计算指标的熵权 $w_i$；
2. 对每家公司 $c$：
   - 记其在指标 $i$ 的标准化值为 $x_{c,i}$；
   - 综合得分定义为：

$$
X_c = \sum_{i \in I} w_i \cdot x_{c,i}
$$

其中，标的公司的得分记为 $X_T$，可比公司 $c$ 的得分记为 $X_c$。

### 3.2 调整系数与调整后估值倍数

对于每个可比公司 $c$：

- 调整系数：

$$
r_c = \frac{X_T}{X_c}
$$

- 原始估值倍数：$y_c^{\text{raw}}$（来自 `batch_result`）；
- 调整后估值倍数：

$$
y_c^{\text{adj}} = r_c \cdot y_c^{\text{raw}}
$$

算法会对 $r_c$ 与 $y_c^{\text{adj}}$ 施加一系列约束（见第 5 节），以确保调整后的结果既方向正确，又围绕目标估值倍数收敛。

## 4. MAD 过滤：鲁棒异常值剔除

在熵权加权之前，算法引入一层鲁棒性极强的异常值过滤 —— **中位绝对偏差（MAD）过滤**，主要用于过滤原始估值倍数 $y_c^{\text{raw}}$ 的极端异常值。

### 4.1 MAD 计算步骤

设可比公司原始估值倍数集合为 $\{y_c\}$（不含标的公司，且要求 $y_c \ge 0$）：

1. 计算中位数 $m$：

$$
m = \text{median}(y_c)
$$

2. 计算绝对偏差：$d_c = |y_c - m|$；
3. 计算绝对偏差的中位数 $\text{MAD}$：

$$
\text{MAD} = \text{median}(d_c)
$$

4. 使用经验系数 1.4826 将 MAD 转换为正态分布下的标准差近似：

$$
\hat{\sigma} = 1.4826 \cdot \text{MAD}
$$

### 4.2 基于 MAD 的初筛规则

- 设过滤阈值 $k = 3$；
- 若 $\hat{\sigma} > 0$，则保留满足

$$
|y_c - m| \le k \cdot \hat{\sigma}
$$

的公司，记为 `raw_selected`；
- 若 $\hat{\sigma} \approx 0$（所有值几乎相同），则至少保留 $(\text{min\_company\_count}-1)$ 家可比公司，按偏差由小到大补齐；
- 最终形成一本“原始筛选公司集” $S_0$，用于后续所有步骤中 `raw_selected` 的参考。

## 5. 多重约束体系

全局优选搜索在每一步评估公司与指标组合时，需要同时满足多层约束：

### 5.1 样本数量约束

- 公司数量约束：
  - 总公司数 $ C| \ge \text{min\_company\_count} $（含标的）；
  - 有效可比公司数 $\text{valid\_peer\_count} \ge \text{min\_valid\_peer\_required}$；
- 指标数量约束：
  - 当前指标数量 $|I| \ge \text{min\_indicator\_count}$；
  - 优选阶段中还会考虑目标指标数量 $\text{preferred\_indicator\_count}$。

### 5.2 调整系数区间约束

对于每个可比公司 $c$：

- 要求调整系数 $r_c$ 落在用户可配置区间：

$$
r_c \in [r_{\text{lower}}, r_{\text{upper}}]
$$

默认值示例：$r_{\text{lower}} = 0.67$，$r_{\text{upper}} = 1.5$，并支持在扫描模式中自适应放宽。

### 5.3 估值倍数比例区间约束

设目标估值倍数为 $y_{\text{target}}$，则对每个公司调整后估值倍数的比例：

$$
\rho_c = \frac{y_c^{\text{adj}}}{y_{\text{target}}}
$$

要求：

$$
\rho_c \in [\text{lower}, \text{upper}]
$$

即调整后的估值倍数相对于目标估值倍数不能过度偏离；若传入 `min_abs_value`，则区间下界/上界还会与绝对偏差约束比较，取更宽的范围。

### 5.4 调整效果约束

为了避免“调整后反而更糟”的情况，算法对每家公司额外施加 **改善约束**：

- 记原始偏差为

$$
\Delta_c^{\text{raw}} = |y_c^{\text{raw}} - y_{\text{target}}|
$$

- 调整后偏差为

$$
\Delta_c^{\text{adj}} = |y_c^{\text{adj}} - y_{\text{target}}|
$$

- 要求：

$$
\Delta_c^{\text{adj}} < 0.8 \cdot \Delta_c^{\text{raw}}
$$

即调整后偏差需至少减少 20%（可视为“改善阈值”），否则该公司不会被视为有效可比公司。

### 5.5 有效可比公司集合与统计量

对满足上述所有约束的公司，记其集合为 $P$（有效可比公司集合），则：

- 有效可比公司数：$|P| = \text{valid\_peer\_count}$；
- 调整后估值倍数均值：

$$
\bar{y}^{\text{adj}} = \frac{1}{|P|} \sum_{c \in P} y_c^{\text{adj}}
$$

- 均值与目标之比：

$$
\rho^{\text{avg}} = \frac{\bar{y}^{\text{adj}}}{y_{\text{target}}}
$$

- 调整后标准差：

$$
\sigma^{\text{adj}} = \sqrt{\frac{1}{|P|} \sum_{c \in P} (y_c^{\text{adj}} - \bar{y}^{\text{adj}})^2}
$$

- 原始 `raw_selected` 公司集上的标准差：$\sigma^{\text{raw}}$；
- 集中度改善指标：

$$
\text{improvement} = \frac{\sigma^{\text{raw}}}{\sigma^{\text{adj}}}
$$

该值越大，说明通过调整和优选后，估值倍数分布越集中。

## 6. 全局优选搜索的阶段结构

当前代码中主要存在两类“全局优选搜索”命令：

1. **扫描模式**：`greedy_scan_max_peer_avg`
   - 目标：在不同 $y_{\text{target}}$ 候选值上，寻找能产生最多有效可比公司的配置；
   - 适合用于自动寻找“合理的目标估值倍数水平”。
2. **区间搜索模式**：`greedy_calculate_range_search`
   - 目标：给定固定 $y_{\text{target}}$ 与比例区间 $[lower, upper]$，在公司集合和指标集合中做贪心搜索与局部搜索优化；
   - 适合用于在用户提供估值倍数区间的前提下做自动优选。

### 6.1 扫描模式：`greedy_scan_max_peer_avg`

#### 6.1.1 初始化 y_target 候选列表

1. 收集所有可比公司的非负 `y_raw`；
2. 使用 `GreedyCalculationService::calculate_kde_mode` 对 $\{y_c^{\text{raw}}\}$ 做核密度估计，得到 **模式值** $m_{\text{KDE}}$；
3. 若 $m_{\text{KDE}}$ 无效，则退化为：
   - 使用正值的均值；或
   - 使用最小正值；
4. 以 $m_{\text{KDE}}$ 为基准生成候选目标倍数：

$$
y_{\text{target}}^{(k)} = m_{\text{KDE}} \cdot (1 + k \cdot \text{step\_ratio}), \quad k \in [-K, K]
$$

其中 $K$ 由 `span_ratio` 与 `max_steps` 限制：

$$
K = \min\left( \left\lceil \frac{\text{span\_ratio}}{\text{step\_ratio}} \right\rceil, \text{max\_steps} \right)
$$

候选集合会去重并按数值排序。

#### 6.1.2 对每个候选 y_target 运行优选算法

对每个候选 $y_{\text{target}}^{(k)}$，执行以下步骤：

1. 基于当前 `company_codes` 与 `indicator_ids` 运行一次 MAD 过滤与熵权计算；
2. 进入 **指标移除阶段**：
   - 在不低于最小指标数与目标指标数的前提下，尝试逐个删除指标；
   - 每次删除尝试后重新计算：有效可比公司数、调整后标准差、与区间中心的距离等；
   - 选择带来“最优改善”的删除方案：
     - 优先提高 `valid_peer_count`；
     - 其次降低 `adjusted_std`；
     - 再次靠近目标区间中心；
   - 重复直到没有删除带来改善，或步数超过上限。
3. 进入 **公司移除阶段**：
   - 在不低于最小公司数的前提下，尝试逐个删除非标的公司；
   - 评价标准与指标阶段类似：优先提高有效可比公司数，其次降低标准差；
   - 重复直到收敛或达到步数上限。
4. 基于最终的公司与指标集合，计算：
   - 有效可比公司数；
   - 调整后均值、标准差、集中度改善；
   - 最终指标数与公司数；
   - 公司层面的详细统计（仅对全局最优点生成，以节约开销）。

上述过程得到一个 `ScanPoint` 结构，加入结果列表。

#### 6.1.3 选择全局最优 ScanPoint

在所有扫描点中，按照以下多层排序逻辑选择全局最优解：

1. 首先最大化 `valid_peer_count`（有效可比公司数）；
2. 在有效公司数相同的前提下，最小化 `adjusted_std`（调整后标准差）；
3. 若标准差也相同，则选择 `y_adj_avg` 较大的点；
4. 若仍相同，则按候选顺序与索引稳定性保证可重复性。

最终结果类型为 `ScanResultPayload`：

- `median_init`: 用于初始化的 KDE 模式值；
- `points`: 所有扫描点列表；
- `best`: 按上述规则选出的全局最优点；
- `company_count`, `indicator_count`: 初始公司数与指标数；
- `lower`, `upper`: 可选的比例区间约束。

### 6.2 区间搜索模式：`greedy_calculate_range_search`

区间搜索模式在给定固定 $y_{\text{target}}$ 和区间 $[lower, upper]$ 的情况下，执行更细致的 **三阶段优选 + 交换优化搜索**，并通过 Tauri 事件 `greedy_step` 实时向前端推送步骤信息。

#### 6.2.1 阶段一：指标移除阶段（indicator phase）

1. 初始化：
   - 以全部指标和所有公司（满足数据完备性者）作为起点；
   - 统计初始的 `valid_peer_count`、`y_adj_avg_ratio`、`adjusted_std` 等量；
   - 向前端发送 `phase="indicator"`, `action="start"` 的步骤事件。
2. 迭代删除指标：
   - 对每个候选要删除的指标 $i$：
     - 构造试验集合 $I' = I \setminus \{i\}$；
     - 若 $|I'| < \text{min_indicator_count}` 或违背目标指标数，则跳过；
     - 重新调用 `compute` 函数，得出试验结果的各项指标；
   - 按以下规则选择最佳删除候选：
     - 优先满足/保持 `valid_peer_count \ge \text{preferred_valid_peers}`；
     - 在此基础上，优先提高 `valid_peer_count`；
     - 若有效公司数相同，则优先减小 `adjusted_std`；
     - 若标准差也近似相同，则优先减小与区间中心的距离；
     - 完全相同时，按照指标列表索引从小到大保证确定性；
   - 应用最佳删除，更新当前集合并发出 `greedy_step` 事件，记录被删除的指标 ID；
   - 直到无法再通过删除指标改善目标函数或达到步数上限。

#### 6.2.2 阶段二：公司移除阶段（company phase）

在指标集合固定的前提下，对可比公司集合执行类似的贪心移除：

1. 保证总公司数不低于 `min_company_count`，且标的公司永远保留；
2. 针对每个非标的公司，构建试验集合并重新计算：
   - 若删掉该公司能够提高 `valid_peer_count` 或降低 `adjusted_std`，则视为候选；
3. 在满足目标公司数与有效公司数的前提下，应用最佳删除操作；
4. 每次删除后发出 `phase="company"`, `action="remove"` 的步骤事件；
5. 重复直到无法改进或达步数上限。

#### 6.2.3 阶段三：交换与局部搜索优化（swap phase）

当简单的删除操作不再带来改进时，算法还会尝试进行“交换式”局部搜索：

- 在已删除的指标/公司集合与保留集合之间，尝试“移除一个 + 增加一个”的操作；
- 交换的目的是在保证约束条件的前提下，微调样本结构，使整体指标进一步改善；
- 每一次可接受的交换都会发出对应的 `greedy_step` 事件，便于前端展示搜索轨迹。

（具体交换逻辑在 `greedy_calculate_range_search` 后半部分实现，这里给出了设计层面的说明。）

## 7. 事件机制与前端集成

区间搜索模式中，算法通过 Tauri 的事件机制，将每一步搜索状态推送给前端：

- 事件名称：`"greedy_step"`；
- 载荷结构：`GreedyStepPayload`，主要字段包括：
  - `phase`: 当前阶段（`"indicator"` / `"company"` / `"swap"`）；
  - `action`: 当前动作（`"start"`, `"remove"`, `"swap"` 等）；
  - `companies`, `indicators`: 当前保留的公司和指标列表；
  - `x_target`, `y_target`: 标的公司得分与目标倍数；
  - `y_adj_avg`, `y_adj_avg_ratio`: 调整后均值及其相对比例；
  - `lower`, `upper`, `lower_abs`, `upper_abs`, `center_ratio`: 区间相关参数；
  - `in_range`, `distance_to_center`: 是否在区间内及与中心的距离；
  - `valid_peer_count`, `raw_selected_count`: 有效可比公司数与 MAD 筛选后公司数；
  - `raw_std`, `adjusted_std`, `concentration_improvement`: 标准差与集中度改善；
  - `min_valid_peer_required`, `preferred_indicator_count`: 数量目标；
  - `step`, `message`: 当前步骤序号及可选说明。

前端可以基于这些事件：

- 绘制搜索过程动画或时间线；
- 实时展示当前保留的公司和指标；
- 显示“有效公司数/集中度改善/区间偏离”等关键指标随步骤的变化。

扫描模式 `greedy_scan_max_peer_avg` 则直接返回 `ScanResultPayload` JSON 字符串，由前端自行解析和展示。

## 8. 决策规则与稳定性设计

为了保证算法在多次运行中的 **确定性** 与 **可复现性**，实现中引入了以下设计：

- 对初始指标列表、公司列表进行排序；
- 在需要按偏差排序的步骤中，使用稳定排序算法，并在偏差相同的情况下按公司代码排序；
- 使用 `IndexMap` 存储中间得分映射，确保插入顺序稳定；
- 在多候选同优的情况下，总是选择索引更小（即更靠前）的候选；
- 对浮点数比较加入 `EPSILON` 容差，避免极小数值抖动导致的排序不稳定。

这些设计保证同一数据集在同一版本算法下多次运行，能够产生相同的结果，为审计与回溯提供基础。

## 9. 使用建议与注意事项

- **指标质量优先**：建议优先确保指标含义合理、方向正确、归一化配置无误，再使用全局优选搜索算法；
- **约束参数调优**：
  - 当有效可比公司数偏少时，可适度放宽 `r_lower/r_upper` 区间或比例区间 `[lower, upper]`；
  - 当结果过于分散时，可以收紧约束，或提高 `preferred_valid_peers` 的目标；
- **样本数量下限**：为了统计稳定性，一般建议有效可比公司数不少于 5；
- **结果解释**：可结合 `company_stats` 中每家公司 `exclusion_reason` 字段，追踪每家公司的纳入/剔除原因，提升模型透明度；
- **性能考虑**：
  - 扫描模式利用 Rayon 并行计算，对大量候选点仍可保持较好性能；
  - 但在公司/指标数量极大时，仍需注意前端交互节奏和后端资源占用。

## 10. 小结

全局优选搜索算法在 GSDJGX App 中承担着“在复杂样本与指标空间内自动寻找稳健估值倍数”的核心职责。它在传统可比公司法基础上，引入了：

- 熵权法的多指标综合评分；
- MAD 过滤与多重约束的鲁棒异常处理；
- 贪心优选 + 扫描与局部交换的搜索策略；
- 候选结果的集中度度量与可视化过程输出。

通过上述机制，系统能够在保证可解释性的前提下，自动为用户提供一组在统计意义上更稳定、更集中、且接近目标估值水平的可比公司与指标组合。