前言:一个看起来应该很简单的概率

告警阈值、等待时间、实验数据,这些场景经常要算一个概率。写到标准正态分布的右尾时,最顺手的写法是 1−CDF(x)。大多数输入看起来都正常,直到把 x 调到 9:结果成了 0。

这个 0 值得停下来看看。理论上右侧还有面积,只是已经很小。于是这次给 statrs4cj 做鸿蒙接入,我把“小概率会不会消失”做成了页面上的一组对照:同一阈值,同时显示 PDF、CDF、直接相减和 SF。拖动一下,比埋在日志里看一串小数直观。

欢迎加入 CJMP 社区,交流公共逻辑库与鸿蒙接入

统计计算放在 CJMP 的 common 公共包,鸿蒙页面通过 HAR 门面调用。本文按这个实验展开,后面会交代文件改动、数值检查,以及模拟器和 MatePad Edge 的真实运行结果。

先看 x=9,再看代码

点击“极小右尾”,阈值切到 9,曲线的横轴随之扩展到 12,橙色竖线仍然落在实际阈值的位置。左侧累计概率显示为 1,直接相减显示为 0;SF 显示 1.128588406e−19。这里保留了两种写法的结果,方便读者自己复现差别。

CDF 接近 1 时,Float64 的有限精度已经无法保留那一小段差值。后续再拿 1 去减,丢掉的位数就回不来了。正态分布的 SF 走互补误差函数 erfc,把右尾直接算出来。这个选择关系到统计结果,不只是页面的小数位设置。

public func cdf(x: Float64): Float64 { return 0.5*erfc((mean-x)/(stdDev*1.4142135623730951)) }
    public func sf(x: Float64): Float64 { return 0.5*erfc((x-mean)/(stdDev*1.4142135623730951)) }

上面是 Normal 中的两个实际方法。CDF 与 SF 共用均值和标准差参数,分别把方向交给 erfc;SF 没有再绕回 CDF 做相减。PDF 描述的是密度,单点密度值也不能当作这一段的概率面积使用。

erf.cj 的分段系数来自固定的 statrs 源码。仓颉版本保留 Horner 多项式求值、各分段的边界以及尾部平方计算的舍入补偿。移植这种函数时,只抄一个短近似公式容易把误差悄悄带进分布接口;我保留了上游这部分实现,再用独立的高精度值检查转换结果。

开发配套

statrs 固定源码与原始接口

基础工具安装直接参考 CPF-RN 组织的环境文件。那份资料包含 RN 专用的 Node/pnpm 要求,本文的 CJMP 工程按自己的官方配套继续配置,不把 RN 的依赖清单一起搬过来。安装过程就不再重复铺一长段。

CPF-RN:通用工具安装指南

CJMP:开发准备与工具配套

项目

本次实际使用

框架及模式

CJMP Tools 0.2.2,logic-module / native

DevEco Studio

6.1.1.300

HarmonyOS SDK

6.1.1(24)

仓颉编译器 / cjpm

1.1.3

模拟器

x86_64,6.1.1.350

真机

HUAWEI MatePad Edge,arm64-v8a,6.1.1.120

本地工程

MyApplication44,保留已配置的包名及签名

版本这里单独说清楚:活动资料优先要求 HarmonyOS 7.0/API 26,本次可用环境实际为 API 24,已经完成的是这一套环境的双端实测。CJMP 0.2.2 文档推荐 6.1 配套,并列出仓颉 1.1.0-beta.22;本机用 1.1.3,因此构建目录和桥接方式做了明确调整。API 26 需要换配套以后重新验证,本文没有把它提前写成已通过。

CJMP 0.2.2 发行说明

适配文件放在哪里,分别改了什么

CJMP 是这里的框架,仓颉负责实现公共逻辑。项目按官方 native 逻辑模块模板建立,project.conf 记录 logic-module/native,cjpm.toml 把 common 编译成动态公共包。平台目录保留鸿蒙桥接代码,示例应用通过 statrs4cj 这个 HAR 包名依赖它。

CJMP 项目类型与 native 逻辑模块

harmony/cjmp/statrs4cj/
  project.conf
  logic-module/cjpm.toml
  logic-module/common/distributions.cj
  logic-module/common/erf.cj
  logic-module/common/special.cj
  logic-module/hos/demo.cj
  logic-module/hos/test_suite.cj
  logic-module/hos/native/napi_bridge.c
  hos/statrs4cj/index.ets
  scripts/build-logic.ps1
example/harmony/MyApplication44/
tests/reference-vectors.json
tests/test_suite.cj
tools/generate-reference.py

harmony 里有实际源码,example/harmony 里有能调用它的鸿蒙应用。下面按模块列出改动理由,复现时能直接找到文件,不需要在文章与工程之间猜路径。表内前三行的前缀是 logic-module/common,桥接文件位于 logic-module/hos。

文件或配置

改动及原因

erf.cj

将上游误差函数及逆函数转为仓颉数组和循环,保留近似区间与系数。

special.cj

迁移 Lanczos 与 Gamma 递推;Beta 连分数重组为有迭代上限的求值;补小量 expm1/log1p。

distributions.cj

把常用分布改成仓颉类;Rust Result 改为异常,明确构造规则和边界。

project.conf / cjpm.toml

定义 CJMP 公共逻辑模块及两个鸿蒙目标架构,数学实现由 cjpm 编译。

demo.cj / napi_bridge.c

公共包完成计算;C ABI/NAPI 复制结果并管理字符串释放。

hos/statrs4cj

提供 HAR 门面、原生类型声明和构建钩子,页面按库依赖接入。

example/harmony/MyApplication44

加入曲线、阈值切换和设备自检;曲线采样值由公共包返回。

tests / tools

保存独立参考数据、容差与生成脚本,结果可以重新核对。

当前 SDK 的构建工具在 build-tools 下;脚本先构建 common,再构建设备测试和平台桥。运行依赖按 ELF 的 NEEDED 信息收集。两种 ABI 分别处理,避免只在模拟器架构上编出了库,却把真机所需的 arm64 文件漏掉。

Gamma 和 Beta:累计概率也有分支

正态之外,页面还提供 Gamma、Beta 和 Student t 的对照。Gamma 的第二个参数按 statrs 的约定使用 rate。拿 Gamma(2,1) 举例,x=3 时 CDF 约为 0.800851727;要是把 rate 看成 scale,换一组参数后就很容易得到含义完全不同的结果。

正则化 Gamma 的累计概率用级数和连分数处理不同区间。靠近下端时计算 P,尾部计算 Q,再按需要返回另一侧。这里对循环加了 10000 次上限,失败会抛错,不用一次不收敛的结果继续更新页面。

public func gammaP(a: Float64, x: Float64): Float64 {
    positive(a,"shape")
    if (x.isNaN()) { return x }; if (x < 0.0) { throw IllegalArgumentException("x < 0") }
    if (x == 0.0) { return 0.0 }; if (x.isInf()) { return 1.0 }
    return if (x <= 1.0 || x <= a) { gammaSeries(a,x) } else { 1.0 - gammaFraction(a,x) }
}

Beta 用参数对称性选择求值方向,连分数采用有保护的小分母求值。Student t 再利用正则化 Beta 计算尾概率。它们的数值依赖有共同底层,所以测试会分别覆盖特殊函数与最终分布,方便分清出错的是哪一层。

另外补了一个容易被忽略的小量问题:指数分布 CDF 用 exp(x)−1、分位数用 log(1−p) 时,极小输入也可能先被舍掉。special.cj 的 expMinusOne 和 logOnePlus 在小量区间用级数保留结果;参考数据里包含 p=1e−100 的指数分位数检查。

把结果送到页面,顺便检查一次刷新

ArkTS 页面导入 statrs4cj。初始化和切换分布时,调用 HAR 的 StatrsBridge;返回的曲线采样点与 PDF/CDF/SF 都来自仓颉。Canvas 只把这些点绘制出来,页面没有再实现另一份概率公式。

import native, { StatrsResult } from 'libstatrs_napi.so';
export { StatrsResult } from 'libstatrs_napi.so';
/** All numerical results are calculated by the CJMP common Cangjie package. */
export class StatrsBridge {
  static calculate(action: string, x: number): StatrsResult {
    return native.perform(action, x);
  }
}

桥接处有一项必须落实:仓颉分配的字符串由 NAPI 复制到返回对象,随后释放。数学包中不保存 ArkTS 对象,页面也不持有原生指针。这版门面是同步演示接口,调用串行进行;大量计算或多线程业务需要另外设计任务调度,不能把演示调用方式直接当作完整并发方案。

实测时还发现了一个页面问题:分布切换后,说明文字和阈值变了,数值卡片却停在初始值。原因在 Builder 按值传递的数字参数没有跟着预期刷新。最终把数值卡片改成带 @Prop 的组件,再逐个按钮检查屏幕读数。这个检查能避免“库算对了,截图却没更新”的情况。

@Component
struct MetricCard {
  @Prop label: string = '';
  @Prop value: number = 0;
  @Prop testId: string = '';
}

上面只列出卡片的输入声明,完整构建函数在示例 Index.ets。这里的刷新问题属于页面接入,和误差函数的移植分开记录,后续读者排障时也更容易找对文件。

再用一组大基数样本检查方差

分布结果之外,我保留了一个样本统计按钮。它装入 1e12+[1,2,3,4,5],均值应该是 1000000000003,样本方差应该是 2.5。大数的平方再相减容易放大抵消误差,所以 RunningStatistics 使用逐点更新的 Welford 算法。

public class RunningStatistics {
    private var n: Int64=0
    private var average: Float64=0.0
    public prop count: Int64 { get() { return n } }
    public prop mean: Float64 { get() { return average } }
    var m2: Float64=0.0
    public func push(x: Float64): Unit { finite(x,"sample"); n++; let d=x-average; average+=d/Float64(n); m2+=d*(x-average) }
    public prop sampleVariance: Float64 { get() { return if (count<2) { Float64.NaN } else { m2/Float64(count-1) } } }
    public prop populationVariance: Float64 { get() { return if (count==0) { Float64.NaN } else { m2/Float64(count) } } }
}

样本方差除以 n−1,总体方差除以 n。只有一个数据点时,样本方差返回 NaN;空序列没有可用的总体方差。页面把这些语义写在标签中,库端则拒绝 NaN/Inf 样本,避免把无效数据悄悄计入统计。

运行效果与设备测试

先通过官方 CJMP Tools 构建 HAR,再编译 MyApplication44 的 HAP。真机使用这个工程已修复的签名;模拟器使用同一次构建的对应包。安装之后从应用按钮执行测试,下面两段 GIF 分别来自模拟器与 MatePad Edge 的连续操作,依次切换分布、检查方差、触发非法参数和运行自检。

python "<CJMP SDK 根目录>/cjmp-tools/tools/main.py" build har --platform ohos-arm64

数值参考使用 mpmath 1.3.0 生成,通常取 100 位十进制精度,极端逆正态参考取 200 位。338 条参考数据逐项保存期望值与容差,覆盖分段边界、不同分布参数和小尾概率。容差分函数设置,不把一组样例的结果扩张成所有参数下的统一精度保证。

另外执行 398 次 CDF+SF 互补检查,以及分位数端点、NaN/Inf、非法参数、大基数均值和方差检查。参考数据参与比较的计算发生在设备内的仓颉库,Python 负责生成参考值和记录操作,不替设备算出一个 PASS。

PASS | 338 high-precision references | 398 complement checks | boundary/validation/statistics checks

两端都得到上面的报告。测试脚本还读取页面的实际文字,检查 Gamma、Beta、Student t 的 CDF、x=9 的非零 SF,以及样本方差 2.5。因此这里既有库计算验证,也有显示层读数验证。原始截图、操作顺序、包哈希和源码一致性记录保存在本地交付资料中。

这版覆盖到哪一步

内容

当前覆盖

常用连续分布

Normal、Exponential、Uniform、LogNormal、Gamma、Beta、ChiSquared、StudentsT

常用离散分布

Binomial、Poisson

特殊函数

erf/erfc 及逆函数,Gamma/Beta 的所列函数

样本统计

均值、样本方差、总体方差

本次运行平台

鸿蒙 x86_64 模拟器、arm64 MatePad Edge

这是首批常用接口移植,上游的随机采样、多元分布、KDE、假设检验和其他未列出的接口尚未实现。Gamma、Beta、Student t 的分位数接口也不在这一版。跨平台公共目录已经组织好,但 Android/iOS 没有做运行验证,不能仅凭目录名把支持平台扩展出去。

参数规则同样写进 README:构造参数要求有限值,lnGamma/gamma 目前只接受正数;Binomial 的 trials 限制为 0 到 1000000。Rust 的错误返回改为仓颉异常,部分检查更严格。这些都是适配后的具体约定,使用时应查看覆盖表和边界说明。

源码

项目开源地址:statrs4cj(AtomGit)

写在最后

这一轮最有用的检查,就是让一个本来“看起来没问题”的计算露出差别:CDF 舍入为 1 时,右尾仍然有值。沿着它往下查,可以把上游算法、仓颉类型、鸿蒙桥接、页面刷新和设备测试串起来。适配文章也就有了可以复现的问题与结果。

目前 MyApplication44 已经能在两端切换分布、查看曲线、检查大基数方差和运行数值自检。后续扩展统计接口时,继续用固定上游、独立参考值和真实设备结果对照,比单纯增加页面按钮更容易发现问题。

欢迎加入 CJMP 社区,一起补充统计接口和复现用例

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐