熊猫 - 扩展反分位数函数

时间:2016-03-15 23:42:19

标签: python pandas scipy percentile

我有一个值数据框:

df = pd.DataFrame(np.random.uniform(0,1,(500,2)), columns = ['a', 'b'])
>>> print df
            a         b
1    0.277438  0.042671
..        ...       ...
499  0.570952  0.865869

[500 rows x 2 columns]

我想通过用它们的百分位替换值来转换它,其中百分位数取代先前行中所有值的分布。也就是说,如果你做df.T.unstack(),它将是一个纯粹的扩展样本。如果您将索引视为DatetimeIndex,这可能会更直观,并且我要求在整个横截面历史记录中采用扩展百分位数。

所以目标就是这个人:

      a   b
0    99  99
..   ..  ..
499  58  84

理想情况我想在包含该行的之前的所有行中的所有值的集合上分配值,因此不完全是扩展百分位;但如果我们不能得到那个,那很好。)

我有一个真的丑陋的方式,我转换并取消堆叠数据帧,生成百分位掩码,并使用for循环覆盖数据帧上的掩码以获得百分位数:< / p>

percentile_boundaries_over_time = pd.DataFrame({integer: 
                                     pd.expanding_quantile(df.T.unstack(), integer/100.0) 
                                     for integer in range(0,101,1)})

percentile_mask = pd.Series(index = df.unstack().unstack().unstack().index)

for integer in range(0,100,1):
    percentile_mask[(df.unstack().unstack().unstack() >= percentile_boundaries_over_time[integer]) &
                    (df.unstack().unstack().unstack() <= percentile_boundaries_over_time[integer+1])] = integer

我一直在尝试使用scipy.stats.percentileofscore()和pd.expanding_apply()来更快地工作,但是它没有给出正确的输出,而是我在疯狂地试图找出原因。这就是我一直在玩的:

perc = pd.expanding_apply(df, lambda x: stats.percentileofscore(x, x[-1], kind='weak'))

有没有人想过为什么这会产生不正确的输出?或者更快的方式来完成这整个练习?任何和所有的帮助非常感谢!

3 个答案:

答案 0 :(得分:1)

正如其他几位评论者指出的那样,计算每行的百分位数可能涉及每次对数据进行排序。对于任何当前的预打包解决方案,可能都是这种情况,包括pd.DataFrame.rankscipy.stats.percentileofscore。反复排序是浪费和计算密集型的,因此我们需要一种最小化的解决方案。

退后一步,找到相对于现有数据集的值的逆分位数与找到我们将该值插入数据集的位置类似(如果已排序)。问题是我们还有一组不断扩展的数据。值得庆幸的是,一些排序算法处理大多数排序数据(并插入少量未排序的元素)非常快。因此,我们的策略是维护我们自己的排序数据数组,并在每次迭代时将其添加到现有列表中,并在新扩展的排序集中查询它们的位置。鉴于数据已经分类,后一种操作也很快。

我认为insertion sort是最快的排序,但它在Python中的性能可能比任何原生的NumPy排序慢。合并排序似乎是NumPy中可用选项中最好的。理想的解决方案是编写一些Cython,但使用我们上面的NumPy策略可以让我们大部分时间。

这是一个手卷解决方案:

def quantiles_by_row(df):
    """ Reconstruct a DataFrame of expanding quantiles by row """

    # Construct skeleton of DataFrame what we'll fill with quantile values
    quantile_df = pd.DataFrame(np.NaN, index=df.index, columns=df.columns)

    # Pre-allocate numpy array. We only want to keep the non-NaN values from our DataFrame
    num_valid = np.sum(~np.isnan(df.values))
    sorted_array = np.empty(num_valid)

    # We want to maintain that sorted_array[:length] has data and is sorted
    length = 0

    # Iterates over ndarray rows
    for i, row_array in enumerate(df.values):

        # Extract non-NaN numpy array from row
        row_is_nan = np.isnan(row_array)
        add_array = row_array[~row_is_nan]

        # Add new data to our sorted_array and sort.
        new_length = length + len(add_array)
        sorted_array[length:new_length] = add_array
        length = new_length
        sorted_array[:length].sort(kind="mergesort")

        # Query the relative positions, divide by length to get quantiles
        quantile_row = np.searchsorted(sorted_array[:length], add_array, side="left").astype(np.float) / length

        # Insert values into quantile_df
        quantile_df.iloc[i][~row_is_nan] = quantile_row

    return quantile_df

根据bhalperin提供的数据(离线),此解决方案的速度提高了10倍。

最后一条评论:np.searchsorted包含'left''right'的选项,用于确定您是否希望预期的插入位置成为可能的第一个或最后一个合适的位置。如果您的数据中有很多重复项,这很重要。上述解决方案的更准确版本将采用'left''right'的平均值:

# Query the relative positions, divide to get quantiles
left_rank_row = np.searchsorted(sorted_array[:length], add_array, side="left")
right_rank_row = np.searchsorted(sorted_array[:length], add_array, side="right")
quantile_row = (left_rank_row + right_rank_row).astype(np.float) / (length * 2)

答案 1 :(得分:0)

这是尝试在包含该行要求之前的所有行中的所有值的集合上实现“百分位数”。在给定2D数据时stats.percentileofscore似乎起作用,因此squeeze似乎有助于获得正确的结果:

a_percentile = pd.Series(np.nan, index=df.index)
b_percentile = pd.Series(np.nan, index=df.index)

for current_index in df.index:
    preceding_rows = df.loc[:current_index, :]
    # Combine values from all columns into a single 1D array
    #   * 2 should be * N if you have N columns
    combined = preceding_rows.values.reshape((1, len(preceding_rows) *2)).squeeze()
    a_percentile[current_index] = stats.percentileofscore(
        combined, 
        df.loc[current_index, 'a'], 
        kind='weak'
    )
    b_percentile[current_index] = stats.percentileofscore(
        combined, 
        df.loc[current_index, 'b'], 
        kind='weak'
    )

答案 2 :(得分:0)

但还不是很清楚,但是你想要累计金额除以总数吗?

norm = 100.0/df.a.sum()
df['cum_a'] = df.a.cumsum()
df['cum_a'] = df.cum_a * norm

同上b