【问题标题】:How to efficiently vectorise hypergeomentric CDF calculation?如何有效地矢量化超几何 CDF 计算?
【发布时间】:2017-03-16 10:30:52
【问题描述】:

我有一组包含 4 列和 1,000,000 行的数据框。 对于每一行,我想运行一个超几何测试,将这些列的 4 个值作为输入并返回一个 p 值(使用超几何分布的累积概率密度函数)。

我已经尝试了两种基于 SciPy(如下)的实现,但都很难扩展。 有没有其他方法可以以更高的效率实现我在下面所做的事情? 我有一个用 R 编写的工作解决方案(在底部),但不幸的是代码必须用 Python 编写, 因为它将用于从 Postgres DB 加载数据的 Airflow 任务中,而目前 R 没有 Postgres 钩子。

慢速 SciPy 实现

这样创建的示例数据(使用 10,000 行而不是完整的 52 * 1,000,000 行):

import numpy as np
import pandas as pd
from scipy.stats import hypergeom
from timeit import default_timer as timer

n_rows = 10000
n_total = 1000
max_good = 400
max_sample = 200
s = 100

df = pd.DataFrame({
  'ngood': np.random.hypergeometric(ngood=max_good, nbad=n_total - max_good,
                                    nsample=s, size=n_rows),
  'nsamp': np.random.hypergeometric(ngood=max_sample, nbad=n_total - max_sample,
                                    nsample=s, size=n_rows)
})

df = df.assign(kgood=np.array([
    np.random.hypergeometric(ngood=ngood, nbad=n_total - ngood,
                             nsample=nsamp, size=1)
    for ngood, nsamp
    in zip(df.ngood, df.nsamp)
]))

基于 for 理解的缓慢实现:

start = timer()
res = [
    hypergeom.cdf(k=ngood_found, M=n_total, n=ngood, N=nsamp)
    for ngood_found, ngood, nsamp
    in zip(df.kgood, df.ngood, df.nsamp)
]
end = timer()
print(res[0:10])
print("Elapsed time: %fs" % (end - start))
[0.44247900002512713, 0.71587318053768023, 0.97215178135616498]
Elapsed time: 2.405838s

基于 numpy 向量化的慢实现:

vectorized_test = np.vectorize(hypergeom.cdf, otypes=[np.float], excluded='M')
start = timer()
res = vectorized_test(k=df.kgood.values, M=n_total,
                      n=df.ngood.values, N=df.nsamp.values)
end = timer()
print(res[0:10])
print("Elapsed time: %fs" % (end - start))
[ 0.442479    0.71587318  0.97215178]
Elapsed time: 2.518952s

快速 R 实现

这说明上面的计算可以在几毫秒内完成。 诀窍是 phyper 在 C 级别矢量化, 与本质上是 python 循环 AFAIK 的 numpy 矢量化相反。

library(microbenchmark)

n_rows <- 10000
n_total <- 1000
max_good <- 400
max_sample <- 200
s <- 100

df <- data.frame(
  ngood = rhyper(nn=n_rows, m=max_good, n=n_total - max_good, k=s),
  nsamp = rhyper(nn=n_rows, m=max_sample, n=n_total - max_sample, k=s)
)

df$kgood <- rhyper(nn=n_rows, m=df$ngood, n=n_total - df$ngood, k=df$nsamp)

microbenchmark(
  res <- phyper(q = df$k, m = df$ngood, n = n_total - df$ngood, k=df$nsamp)
)
Unit: milliseconds
                                                                 expr      
 phyper(q = df$k, m = df$ngood, n = n_total - df$ngood, k = df$nsamp) 

     min      lq     mean   median       uq      max neval
2.984852 3.00838 3.350509 3.134745 3.439138 5.462694   100

【问题讨论】:

    标签: python numpy scipy airflow


    【解决方案1】:

    将hypergeom.cdf的结果缓存为:

    from functools import lru_cache
    
    #@lru_cache(maxsize = 16*1024)
    #def fn(k, n, N):
    #    return hypergeom.cdf(k = k, M=n_total, n = n, N = N)
    
    data = {}
    def fn(k, n, N):
        key = (k, n, N)
        if not key in data:
            val = hypergeom.cdf(k = k, M=n_total, n = n, N = N)
            data[key] = val
        else:
            val = data[key]
        return val
    
    start = timer()
    res = [
        fn(ngood_found, ngood, nsamp)
        for ngood_found, ngood, nsamp
        in zip(df.kgood, df.ngood, df.nsamp)
    ]
    
    end = timer()
    print(res[0:10])
    print("Elapsed time: %fs" % (end - start))
    

    这在我的机器上给出:Elapsed time: 0.279891s(0.315840s 和 lru_cache)

    编辑:

    事实上,瓶颈似乎在于超几何 CDF 本身的计算(而不是 for 循环的开销)。 为了测试这一点,我为 GSL 包中的函数 gsl_cdf_hypergeometric_P 创建了一个 SWIG 文件 _cdf.i。

    %module cdf
    %{
    #include "gsl/gsl_cdf.h"
    %}
    double gsl_cdf_hypergeometric_P(int, int, int, int);
    

    然后这个文件被“转换”成一个包:

    swig -c++ -python _cdf.i
    g++ -fPIC -c _cdf_wrap.c -I${HOME}/venvs/p3/include/python3.5m
    g++ -shared _cdf_wrap.o -o _cdf.so -lgsl
    

    然后可以在原始示例中直接使用它:

    import numpy as np
    import pandas as pd
    from scipy.stats import hypergeom
    from timeit import default_timer as timer
    from cdf import gsl_cdf_hypergeometric_P
    
    n_rows = 10000
    n_total = 1000
    max_good = 400
    max_sample = 200
    s = 100
    
    df = pd.DataFrame({
      'ngood': np.random.hypergeometric(ngood=max_good, nbad=n_total - max_good,
                                        nsample=s, size=n_rows),
      'nsamp': np.random.hypergeometric(ngood=max_sample, nbad=n_total - max_sample,
                                        nsample=s, size=n_rows)
    })
    
    df = df.assign(kgood=np.array([
        np.random.hypergeometric(ngood=ngood, nbad=n_total - ngood,
                                 nsample=nsamp, size=1)
        for ngood, nsamp
        in zip(df.ngood, df.nsamp)
    ]))
    
    start = timer()
    res = [
        hypergeom.cdf(k=ngood_found, M=n_total, n=ngood, N=nsamp)
        for ngood_found, ngood, nsamp
        in zip(df.kgood, df.ngood, df.nsamp)
    ]
    end = timer()
    print(res[0:10])
    print("Elapsed time: %fs" % (end - start))
    
    def cdf(k, M, n, N):
        return gsl_cdf_hypergeometric_P(int(k), int(n), int(M-n), int(N))
    
    start = timer()
    res = [
        cdf(k=ngood_found, M=n_total, n=ngood, N=nsamp)
        for ngood_found, ngood, nsamp
        in zip(df.kgood, df.ngood, df.nsamp)
    ]
    end = timer()
    print(res[0:10])
    print("Elapsed time: %fs" % (end - start))
    

    这会产生:

    [0.58605423287644209, 0.38055520197355552, 0.70597920363472055, 0.99728041338849138, 0.79797439957395955, 0.42245057292366844, 0.58627644982763727, 0.74819471224742817, 0.75121042470714849, 0.48561471798885397]
    Elapsed time: 2.069916s
    [0.5860542328771666, 0.38055520197384757, 0.7059792036350717, 0.997280413389543, 0.7979743995750694, 0.4224505729249291, 0.5862764498272103, 0.7481947122472634, 0.7512104247082603, 0.4856147179890127]
    Elapsed time: 0.018253s
    

    因此,即使使用普通的for 循环,速度提升也非常显着。

    【讨论】:

    • 真是太聪明了!我在我的一个数据帧上对其进行了测试,结果在总共 1,420,290 行中只有 109,096 行。
    • @Backlin 也许可以通过利用超几何分布的对称性来进一步减少它......
    • @Backlin 似乎循环并不是使 Python 版本变慢的原因 - 我已经更新了答案...
    • 哇,这就是我要找的东西!以前没有听说过 SWIG,但它似乎非常有用。
    • 我知道这个答案已经很老了(但也很神奇!)——这仍然是实现这一目标的唯一方法吗?我不确定如何正确运行您上面列出的这些 swig/g++ 命令。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-07-31
    • 1970-01-01
    • 1970-01-01
    • 2019-11-24
    • 1970-01-01
    相关资源
    最近更新 更多