#statistics #data-analysis #incremental #kurtosis #skewness #rust

incr_stats

使用 Rust 的快速、可扩展、增量描述性统计

3 个稳定版本

1.0.2 2024 年 2 月 21 日
1.0.1 2023 年 2 月 16 日
1.0.0 2023 年 2 月 15 日

#68 in 数学

Download history 125/week @ 2024-04-22 98/week @ 2024-04-29 23/week @ 2024-05-06 27/week @ 2024-05-13 28/week @ 2024-05-20 4/week @ 2024-05-27 55/week @ 2024-06-03 93/week @ 2024-06-10 13/week @ 2024-06-17 43/week @ 2024-06-24 32/week @ 2024-07-01 47/week @ 2024-07-08 50/week @ 2024-07-22 37/week @ 2024-07-29 11/week @ 2024-08-05

每月 98 次下载
2 crates 中使用

MIT/Apache 许可

90KB
2K SLoC

incr_stats

使用 Rust 的快速、可扩展、增量描述性统计

incr_stats 提供以下特性:时间优化和内存优化的可扩展描述性统计

  • 增量更新(也称为 streaming)意味着不存储源数据
  • 在请求时计算统计数据几乎是瞬时的
  • 比非增量等效项更快,同时需要远少于存储空间
  • 允许快速更新已计算的统计数据
  • 特别适合内存占用低的程序或数据集过大无法存储的应用程序,例如连续数据生成
  • 提供包括偏度和峰度的描述性统计
  • 提供总体和样本统计
  • 防止因 NaN 和 Inf 造成的错误
  • 进行了广泛的准确性测试
  • 与 R 和 Octave 验证
  • 为每个统计量提供了额外的优化批量函数
  • 为每个统计量提供了等效的教科书批量函数
  • 速度基准比较了增量计算和批量计算

包含的统计数据

incr_stats 统计包提供了以下描述性统计

  • count()
  • min()
  • max()
  • sum()
  • mean()
  • population_variance()
  • sample_variance()
  • population_standarddeviation()
  • sample_standarddeviation()
  • population_skewness()
  • sample_skewness()
  • population_kurtosis()
  • sample_kurtosis()

示例

incr_stats 统计包在 f64 数据上运行,易于使用。

增量/流式处理

无需存储整个数据流即可计算其描述性统计。

use incr_stats::incr::Stats;

let mut s = Stats::new();

// Update the stats as data becomes available, without storing it.
s.update(1.2)?;
s.update(0.2)?;
// ...

// Calculate the descriptive statistics as needed.
println!("The skewness is {:.4}", s.sample_skewness()?);
println!("The kurtosis is {:.4}", s.sample_kurtosis()?);

一些计算是通过每个 update() 来进行的,因此它们比简单地存储值要慢。然而,生成所需的统计信息只需要进行很少的计算,所以例如 sample_kurtosis(),与传统在整个数组上操作的传统算法相比几乎是瞬时的。

还提供了一个 array_update() 函数,以便可以对一组值执行增量更新。

incr_stats 包含了其他两个相同计算的版本,用于比较。通常,增量版本是最快的,但它们都产生相同的结果。

此示例在 examples/incr_example.ps 中,可以使用以下命令运行:$ cargo run --example incr_example

记忆化

此版本需要存储的数据,但进行了优化并提供相同的精度。描述性统计之间相互依赖,例如偏度依赖于方差,方差依赖于均值。此版本确保在请求任何统计信息时仅执行所需的最小计算。此外,后续请求不会重复已完成的计算。

use incr_stats::vec::Stats;

let a = vec![1.2, -1.0, 2.3, 10.0, -3.0, 3.2, 0.33, 0.23, 0.23, 1.0];
let mut s = Stats::new(&a)?;

println!("The skewness is {:.4}", d.sample_skewness()?);
println!("The kurtosis is {:.4}", d.sample_kurtosis()?);

此示例在 examples/vec_example.ps 中,可以使用以下命令运行:$ cargo run --example vec_example

批处理

最后,第三个版本使用传统的、教科书中的计算方法。这些计算没有其他开销。它们主要包含在比较和测试中,但在只需要一个或两个统计信息时,对于存储的数据可能是最快的。

use incr_stats::batch;

let a = vec![1.2, -1.0, 2.3, 10.0, -3.0, 3.2, 0.33, 0.23, 0.23, 1.0];

println!("The skewness is {:.4}", batch::sample_skewness(&a)?);
println!("The kurtosis is {:.4}", batch::sample_kurtosis(&a)?);

此示例在 examples/batch_example.ps 中,可以使用以下命令运行:$ cargo run --example batch_example

如何选择?

首先选择 incr 统计,除非你的用途符合下面描述的优化。

incr,增量统计

incr 统计不存储数据,而是只存储在请求完整统计信息时所需的一些中间值。它一次或几次更新一个或几个值。

增量统计在计算上最终与批量计算等价,只是在单个数据更新上分摊了计算。这是计算大型数据集描述性统计的最快方式。

如果以下情况也适用:

  • 内存有限
  • 数据无限,例如在流式或连续数据应用程序中
  • 你不想存储和管理所有数据
  • 当前的统计信息需要更新
  • 数据一次只出现一个或几个点,但需要整体统计信息
  • 请求统计信息时必须快速计算,即没有时间在整个数据集上计算。

vec,优化向量统计

消除数据存储并使最终统计计算快速进行的权衡是,对每个update()执行中间计算比简单地存储数据要慢。

vec统计结构在执行存储数据上的计算时缓存中间结果。这种优化确保了当其他统计需要时,依赖计算(如均值和方差)被重复使用。重复调用变为查找而不是重新计算。

当以下情况发生时,使用vec统计:

  • 应用程序不能为增量update()提供足够的时间,但只有时间存储数据
  • 你只对一两个统计感兴趣,因此update()计算的开销浪费在不会请求的统计上
  • 如果最终统计的计算速度慢,因为是在整个数据集上操作,这也是可以接受的。

batch,未优化的教科书方程

batch函数主要包括用于准确度比较和测试,但它们可能比具有检查先前计算值的开销的vec版本略快。如果只需要一个统计量,则不会在统计量之间进行重复使用,这会使vec版本更快。

当以下情况发生时,使用batch统计:

  • 需要绝对最快的单个统计量(例如,sample_kurtosis)的计算

总体和样本统计量

incr_stats提供总体和样本统计量。

代码记录了RGNU Octave统计包中的相应函数,明确了它们的命名和参数差异。

准确性:R和Octave验证

incr_stats单元测试包括与RGNU Octave统计包结果匹配至13位小数的示例。

请参阅相应RGNU Octave函数的代码。

速度

数据处理分为两部分:获取和处理。对于基于数组的系统,获取意味着只是存储数据。然后,当请求统计时,处理所有数据。相比之下,增量统计在获取时进行一些处理,使得最终统计计算非常快。

因此,关于速度,实际上是一个问题,即你想在哪里花费计算时间

  • 随着数据的收集逐渐一点一点地?那么更新慢,但最终统计快。
  • 在请求统计时一次性完成?那么更新快,只是存储数据,但最终统计慢。

增量统计在每次数据点更新时都会进行统计矩的计算,这带来了额外的开销。这种开销使得在任何时候都可以快速计算出完整的描述性统计,而不必存储整个数据集。对于单个数据点来说,这看起来是相当大的计算量。而且,与存储数组的情况相比,对所有点的计算量似乎要大得多。但实际上,对于存储数组版本,也需要进行几乎相同的计算,因此在处理时间方面,两者几乎是相同的。存储数组版本会多次遍历数据;增量版本则会展开这些循环,将每个循环计算分解成在值获取时执行的单独的 update() 操作。

这就是为什么人们更喜欢存储数组统计的唯一原因:

  1. 数据必须在采集时尽可能快地处理;只有存储它的功夫,或者
  2. 只需要一个或两个统计量,例如,当不需要计算偏度时,就没有必要在每次 update() 时计算第四阶矩的中间值。

基准测试

Criterion 基准测试包括增量、记忆化和批量统计计算的比较。实际的实验时间会因机器和操作系统而异,所以我们这里考虑这些结果具有代表性,并做出相对比较。

对于具有 10 和 1,000,000 个随机值的数据集,以下是计算总时间的比较 [越低越好]

Kurtosis 表示仅计算了 sample_kurtosis,显示了如果只需要一个统计量时的时间。《code>All stats 表示计算了所有统计量(countmin/maxsummean{samp/pop}_variance{samp/pop}_standard_deviation{samp/pop}_skewness{samp/pop}_kurtosis)。

方法 10, Kurtosis 10, All stats 1M, Kurtosis 1M, All stats
incr 81.736 ns 101.35 ns 7.004 ms 7.105 ms
batch 40.124 ns 228.20 ns 3.830 ms 29.128 ms
vec 68.094 ns 106.80 ns 4.124 ms 8.411 ms

分析

当需要多个统计量时,无论数据集大小如何,增量(incr)统计是最快的。

如上所述,增量版本和优化的存储数组(vec)版本的计算量相似,但增量版本比优化存储数组版本快 5.1%(10 个数据点)到 15.5%(1M 个数据点)。

正如预期的那样,对于只计算一个统计量,未经优化的存储数组版本(batch)代码最快,不受数据集大小的影响。这是因为它只为一个统计量执行了最少的计算。

请注意,对于大型(1M)数据集的增量版本,计算所有统计数据(7.105 毫秒)所需的时间与仅计算样本峰度的计算时间(7.004 毫秒)非常接近(1.4%),这是因为在这个基准测试运行中,增量版本的多数工作量都花在单个点更新计算上,这包括所有统计数据所需的中间值。因此,计算最终 all stats 与任何单个统计量所需的时间趋于一致。

incr 更新和最终时间

增量统计包在每次 update() 上都会进行一些计算。这些计算有多昂贵?incr 的最终计算时间有多快?

方法 10 个值 1M 个值
更新 11.722 纳秒 11.843 纳秒
最终 2.591 纳秒 2.584 纳秒

此表显示了每次单个 update()sample_kurtosis() 最终计算的 10 个和 1,000,000 个值的时间。

正如预期的那样,由于数据集的大小,时间上没有显著的差异。

我们看到每个更新都需要 <12 纳秒来计算中间时刻。这项工作分散在所有更新中,使得最终计算 sample_kurtosis() 只需要 2.6 纳秒,大约是更新时间的五分之一。

错误处理

incr_stats crate 以简单和一致的方式处理错误。只有三种类型的错误

  1. NotEnoughData:这个错误仅仅意味着需要更多的数据来允许计算统计量。例如,样本偏度计算包括除以 n-1,因此至少需要 2 个数据点来避免除以 0.0。
  2. Undefined:即使有足够的有效数据,一些统计量也会产生未定义的结果。例如,如果所有数据都是相同的值,则方差为 0.0。偏度和峰度是衡量围绕中心趋势的分布的度量,它们不存在。这一事实通过除以方差(即除以 0.0)反映在计算中。因此,这些是未定义的。
  3. InvalidData:浮点数据被检查是否存在来自 IEEE 754 标准的 NaNs 和 Infs。

不需要进行这些区分的调用者只需对任何错误做出反应。

许可

根据您的选择,在 Apache License, Version 2.0MIT 许可证 下许可。
除非您明确声明,否则根据 Apache-2.0 许可证定义的,您有意提交给此 crate 的任何贡献都应按上述方式双许可,而无需任何额外条款或条件。

依赖关系

~0.7–1.5MB
~30K SLoC