从值计数中计算分位数

7 投票
2 回答
3647 浏览
提问于 2025-04-18 15:34

我想在Python中计算多个大向量的百分位数。我不想把这些向量拼在一起,然后再用numpy.percentile处理这个巨大的向量,有没有更有效的方法呢?

我的想法是,首先统计不同值的出现频率(比如可以用scipy.stats.itemfreq),然后把不同向量的这些频率结合起来,最后根据这些计数来计算百分位数。

可惜的是,我找不到可以合并频率表的函数(这并不简单,因为不同的表可能包含不同的项目),也找不到可以从项目频率表计算百分位数的函数。我需要自己实现这些功能吗,还是可以使用现有的Python函数?那些函数是什么呢?

2 个回答

4

这个问题困扰了我很久,所以我决定花点时间来解决它。我的想法是重用一些来自 scipy.stats 的东西,这样我们就能直接使用 cdfppf 了。

有一个类叫 rv_discrete,它是为了被其他类继承而设计的。在查看它的子类时,我发现了一个叫 rv_sample 的类,描述很有意思:一个由支持和数值定义的“样本”离散分布。 这个类并没有在API中公开,但当你直接将数值传递给 rv_discrete 时,它会被使用。

所以,这里有一个可能的解决方案:

import numpy as np
import scipy.stats

# some mapping from numeric values to the frequencies
freqs = np.array([
    [1, 3],
    [2, 10],
    [3, 13],
    [4, 12],
    [5, 9],
    [6, 4],
])

def distrib_from_freqs(arr: np.ndarray) -> scipy.stats.rv_discrete:
    pmf = arr[:, 1] / arr[:, 1].sum()
    distrib = scipy.stats.rv_discrete(values=(arr[:, 0], pmf))
    return distrib

distrib = distrib_from_freqs(freqs)

print(distrib.pmf(freqs[:, 0]))
print(distrib.cdf(freqs[:, 0]))
print(distrib.ppf(distrib.cdf(freqs[:, 0])))  # percentiles

# [0.05882353 0.19607843 0.25490196 0.23529412 0.17647059 0.07843137]
# [0.05882353 0.25490196 0.50980392 0.74509804 0.92156863 1.        ]
# [1. 2. 3. 4. 5. 6.]

# max, median, 1st quartile, 3rd quartile
print(distrib.ppf([1.0, 0.5, 0.25, 0.75]))
# [6. 3. 2. 5.]

# the distribution describes values from (0, 1] 
#   and 0 results with a value right before the minimum:
print(distrib.ppf(0))
# 0.0
4

根据Julien Palard的建议,我使用了collections.Counter来解决第一个问题(计算和合并频率表),而我自己实现了第二个问题(从频率表中计算百分位数):

from collections import Counter

def calc_percentiles(cnts_dict, percentiles_to_calc=range(101)):
    """Returns [(percentile, value)] with nearest rank percentiles.
    Percentile 0: <min_value>, 100: <max_value>.
    cnts_dict: { <value>: <count> }
    percentiles_to_calc: iterable for percentiles to calculate; 0 <= ~ <= 100
    """
    assert all(0 <= p <= 100 for p in percentiles_to_calc)
    percentiles = []
    num = sum(cnts_dict.values())
    cnts = sorted(cnts_dict.items())
    curr_cnts_pos = 0  # current position in cnts
    curr_pos = cnts[0][1]  # sum of freqs up to current_cnts_pos
    for p in sorted(percentiles_to_calc):
        if p < 100:
            percentile_pos = p / 100.0 * num
            while curr_pos <= percentile_pos and curr_cnts_pos < len(cnts):
                curr_cnts_pos += 1
                curr_pos += cnts[curr_cnts_pos][1]
            percentiles.append((p, cnts[curr_cnts_pos][0]))
        else:
            percentiles.append((p, cnts[-1][0]))  # we could add a small value
    return percentiles

cnts_dict = Counter()
for segment in segment_iterator:
    cnts_dict += Counter(segment)

percentiles = calc_percentiles(cnts_dict)

撰写回答