Pandas数据帧上的分位数归一化

19

简单来说,在Python中如何对一个大的Pandas数据框(可能有2,000,000行)应用分位数归一化?

PS. 我知道有一个名为rpy2的软件包可以在子进程中运行R,使用R中的分位数归一化。 但事实是,当我使用以下数据集时,R无法计算出正确的结果:

5.690386092696389541e-05,2.051450375415418849e-05,1.963190184049079707e-05,1.258362869906251862e-04,1.503352476021528139e-04,6.881341586355676286e-06
8.535579139044583634e-05,5.128625938538547123e-06,1.635991820040899643e-05,6.291814349531259308e-05,3.006704952043056075e-05,6.881341586355676286e-06
5.690386092696389541e-05,2.051450375415418849e-05,1.963190184049079707e-05,1.258362869906251862e-04,1.503352476021528139e-04,6.881341586355676286e-06
2.845193046348194770e-05,1.538587781561563968e-05,2.944785276073619561e-05,4.194542899687506431e-05,6.013409904086112150e-05,1.032201237953351358e-05

编辑:

我的要求是:

在给定的数据的基础上,如何按照https://en.wikipedia.org/wiki/Quantile_normalization中所述的步骤应用分位数标准化。

我发现Python的一段代码可以计算分位数标准化:

import rpy2.robjects as robjects
import numpy as np
from rpy2.robjects.packages import importr
preprocessCore = importr('preprocessCore')


matrix = [ [1,2,3,4,5], [1,3,5,7,9], [2,4,6,8,10] ]
v = robjects.FloatVector([ element for col in matrix for element in col ])
m = robjects.r['matrix'](v, ncol = len(matrix), byrow=False)
Rnormalized_matrix = preprocessCore.normalize_quantiles(m)
normalized_matrix = np.array( Rnormalized_matrix)

这段代码在使用本身的样例数据时能够良好运行,但是当我使用上述数据进行测试时,结果却出现了错误。

由于ryp2提供了一个在python子进程中运行R的接口,我直接在R中再次进行了测试,结果仍然错误。因此,我认为问题出在R的方法上。


我删除了“R”标签,因为(1)你没有使用R,(2)你不想在答案中出现R。但是如果你说“R无法计算正确的结果”,这听起来像是你要么贬低R(为了什么?),要么想让别人纠正你未发布的代码。无论哪种方式,也许我没有理解你想要什么:分位数归一化需要源和目标分布,我不确定你在这里提供的是哪一个。你能澄清一下吗? - r2evans
@r2evans 感谢您的评论,我已经编辑了问题。顺便说一下,我在Google上找到的代码是将R作为Python的子进程运行。直接运行R后,我发现结果是错误的。此外,我不确定您所说的“目标分布”是什么意思。根据维基百科,分位数归一化的计算不涉及该术语。希望我已经清楚地表达了问题,就是对我提供的数据应用分位数归一化。 - Shawn. L
你说得对,我的“目标”这个术语并不是很好。维基引用了“使两个分布相同”的概念,所以我想知道你的两个分布是什么。现在你提供了额外的代码(和数据,定义为“矩阵”),我对哪个是你实际需要进行量化归一化的数据感到困惑。(也许这是一个愚蠢的问题,但是矩阵是否与你实际需要的矩阵相比转置了?) - r2evans
@r2evans 对我造成的困惑我感到抱歉。FYI,实际数据是一个(2119055,124)矩阵。我上面提供的数据只是用于测试的小子集。是的,我考虑了转置的问题。正如你所看到的,在示例代码中,矩阵是(3,5),但归一化结果是(5,3),因此我总结出使用此代码需要先转置矩阵。更明确地说,我的数据是(4,6),为了使用该代码,我将分配转置后的数据,即(6,4)给变量matrix,然后继续进行。 - Shawn. L
9个回答

22
使用来自维基百科文章的示例数据集:
df = pd.DataFrame({'C1': {'A': 5, 'B': 2, 'C': 3, 'D': 4},
                   'C2': {'A': 4, 'B': 1, 'C': 4, 'D': 2},
                   'C3': {'A': 3, 'B': 4, 'C': 6, 'D': 8}})

df
Out: 
   C1  C2  C3
A   5   4   3
B   2   1   4
C   3   4   6
D   4   2   8

对于每个等级,可以通过以下方式计算平均值:
rank_mean = df.stack().groupby(df.rank(method='first').stack().astype(int)).mean()

rank_mean
Out: 
1    2.000000
2    3.000000
3    4.666667
4    5.666667
dtype: float64

然后,生成的序列rank_mean可以用作映射,以获得规范化的结果:

df.rank(method='min').stack().astype(int).map(rank_mean).unstack()
Out: 
         C1        C2        C3
A  5.666667  4.666667  2.000000
B  2.000000  2.000000  3.000000
C  3.000000  4.666667  4.666667
D  4.666667  3.000000  5.666667

2
优雅地使用 groupbymap 和堆叠/展开。你是一名 pandas 开发者吗? - O.rka
3
谢谢。不,我只是一个普通的用户。 - ayhan
@ayhan 为什么在第一和第二个处理行中使用了不同的排名方法,即 firstmin - Mischa Lisovyi
1
只是指出(并自我推广)这不会根据维基百科生成“正确”的结果。我实现了一种快速方法,可以产生正确的结果,并且可以使用conda或pip进行安装:https://dev59.com/6VoU5IYBdhLWcg3wAjYs#62792272 - Maarten-vd-Sande
1
这看起来很不错!唯一需要修复的是具有相同排名的值,可以使用平均值,并在其之间插值rank_mean。我在我的帖子中添加了调整。https://dev59.com/6VoU5IYBdhLWcg3wAjYs#67597273 - chase

9

好的,我已经实现了这种相对高效的方法。

完成后,这个逻辑似乎很简单,但无论如何,我决定在这里发布它,供任何感到困惑的人参考,就像我在搜索不到可用代码时那样。

代码在 github 上:Quantile Normalize


3
值得注意的一点是,Ayhan和Shawn的代码都使用较小的秩平均数来处理并列情况,但如果您使用R包processcore的normalize.quantiles()函数,则会使用秩平均数的平均值来处理并列情况。
使用上述示例:
> df

   C1  C2  C3
A   5   4   3
B   2   1   4
C   3   4   6
D   4   2   8

> normalize.quantiles(as.matrix(df))

         C1        C2        C3
A  5.666667  5.166667  2.000000
B  2.000000  2.000000  3.000000
C  3.000000  5.166667  4.666667
D  4.666667  3.000000  5.666667

只是想说,我为Python制作了一个名为qnorm的包/答案,它可以处理并列情况:https://dev59.com/6VoU5IYBdhLWcg3wAjYs#62792272 - Maarten-vd-Sande

2
这只是一个小调整,但我想许多人已经注意到了@ayhan的答案中微妙的“缺陷”。
我对它进行了小小的调整,得到了“正确”的答案,而不必为如此简单的功能使用任何外部库。
唯一需要调整的是[添加插值值]部分。
import pandas as pd

df = pd.DataFrame({'C1': {'A': 5, 'B': 2, 'C': 3, 'D': 4},
                   'C2': {'A': 4, 'B': 1, 'C': 4, 'D': 2},
                   'C3': {'A': 3, 'B': 4, 'C': 6, 'D': 8}})

def quant_norm(df):
    ranks = (df.rank(method="first")
              .stack())
    rank_mean = (df.stack()
                   .groupby(ranks)
                   .mean())
    # Add interpolated values in between ranks
    finer_ranks = ((rank_mean.index+0.5).to_list() +
                    rank_mean.index.to_list())
    rank_mean = rank_mean.reindex(finer_ranks).sort_index().interpolate()
    return (df.rank(method='average')
              .stack()
              .map(rank_mean)
              .unstack())
quant_norm(df)

Out[122]: 
         C1        C2        C3
A  5.666667  5.166667  2.000000
B  2.000000  2.000000  3.000000
C  3.000000  5.166667  4.666667
D  4.666667  3.000000  5.666667

1
以下代码与preprocessCore::normalize.quantiles.use.target的结果相同,我发现它比上面的解决方案更简单清晰。另外,性能应该好到巨大的数组长度。
import numpy as np

def quantile_normalize_using_target(x, target):
    """
    Both `x` and `target` are numpy arrays of equal lengths.
    """

    target_sorted = np.sort(target)

    return target_sorted[x.argsort().argsort()]

一旦你有一个 pandas.DataFrame,就很容易实现:
quantile_normalize_using_target(df[0].as_matrix(),
                                df[1].as_matrix())

(在上面的示例中,将第一列归一化为第二列作为参考分布。)

0

我是pandas的新手,对于这个问题来说有点晚了,但我认为答案可能也会有用。它基于@ayhan的伟大answer构建:

def quantile_normalize(dataframe, cols, pandas=pd):

    # copy dataframe and only use the columns with numerical values
    df = dataframe.copy().filter(items=cols)

    # columns from the original dataframe not specified in cols
    non_numeric = dataframe.filter(items=list(filter(lambda col: col not in cols, list(dataframe))))


    rank_mean = df.stack().groupby(df.rank(method='first').stack().astype(int)).mean()  

    norm = df.rank(method='min').stack().astype(int).map(rank_mean).unstack()


    result = pandas.concat([norm, non_numeric], axis=1)
    return result

这里的主要区别更接近于一些真实世界的应用。通常情况下,您只有数值数据的矩阵,此时原始答案就足够了。

有时候,您还会有基于文本的数据。这使您可以指定数值数据的列cols,并对这些列运行分位数归一化。最后,它将从原始数据框中合并回非数字(或不需要归一化)的列。

例如,如果您在维基示例中添加了一些“元数据”(char):

df = pd.DataFrame({
    'rep1': [5, 2, 3, 4],
    'rep2': [4, 1, 4, 2],
    'rep3': [3, 4, 6, 8],
    'char': ['gene_a', 'gene_b', 'gene_c', 'gene_d']
}, index = ['a', 'b', 'c', 'd'])

然后你可以调用

quantile_normalize(t, ['rep1', 'rep2', 'rep3'])

获取

    rep1        rep2        rep3        char
a   5.666667    4.666667    2.000000    gene_a
b   2.000000    2.000000    3.000000    gene_b
c   3.000000    4.666667    4.666667    gene_c
d   4.666667    3.000000    5.666667    gene_d

0
请注意,scikit-learn 提供了一个分位数归一化模块
import pandas as pd
import sklearn.preprocessing

df = pd.DataFrame({'C1': {'A': 5, 'B': 2, 'C': 3, 'D': 4},
                   'C2': {'A': 4, 'B': 1, 'C': 4, 'D': 2},
                   'C3': {'A': 3, 'B': 4, 'C': 6, 'D': 8}})
sklearn.preprocessing.quantile_transform(df)

array([[1.  , 1.  , 0.  ],
       [0.  , 0.  , 0.33],
       [0.33, 1.  , 0.67],
       [0.67, 0.33, 1.  ]])

还可以将数据调整为正态分布,而不是均匀分布:

sklearn.preprocessing.quantile_transform(df, output_distribution="normal")

array([[ 5.2 ,  5.2 , -5.2 ],
       [-5.2 , -5.2 , -0.43],
       [-0.43,  5.2 ,  0.43],
       [ 0.43, -0.43,  5.2 ]])

0

正如@msg所指出的那样,这里的解决方案都没有考虑到并列情况。我编写了一个名为qnorm的Python包,它处理并列情况,并正确地重新创建了Wikipedia分位数归一化示例

import pandas as pd
import qnorm

df = pd.DataFrame({'C1': {'A': 5, 'B': 2, 'C': 3, 'D': 4},
                   'C2': {'A': 4, 'B': 1, 'C': 4, 'D': 2},
                   'C3': {'A': 3, 'B': 4, 'C': 6, 'D': 8}})

print(qnorm.quantile_normalize(df))
         C1        C2        C3
A  5.666667  5.166667  2.000000
B  2.000000  2.000000  3.000000
C  3.000000  5.166667  4.666667
D  4.666667  3.000000  5.666667

安装可以使用pip或conda完成

pip install qnorm

或者

conda config --add channels conda-forge
conda install qnorm

这与在df.rank()中指定method='average'有何不同? - Sos
1
@Sos 我无法清晰地在评论中放置数据框,但你为什么不试试呢?我使用method ='average'qnorm得到了不同的结果。它只是以不同的方式解决连接问题。 - Maarten-vd-Sande
我尝试使用你的包,但它抛出了一个非精确类型数组错误。我的输入也是一个数据框,有什么解决方法吗? - Xiaoxixi
@Xiaoxixi 谢谢你让我知道,我以前从来没有遇到过这个问题。每一列的类型是什么?你可以用 df.dtypes 进行检查。如果你有一个 Github 账户,你可以在 Github 页面上创建一个问题,并提供一个能够重现错误的小代码片段吗?然后我应该能够快速修复它:https://github.com/Maarten-vd-Sande/qnorm/issues/new - Maarten-vd-Sande
@Xiaoxixi 我查了一些资料,发现当你使用“非标准”数据类型(例如float16)时会出现这种情况。程序仍然会崩溃,但会输出一条消息告诉你要转换为例如float32。 - Maarten-vd-Sande
显示剩余2条评论

0

可能更加健壮的方法是在每行上使用中位数而不是平均数(基于Shawn.L的代码):

def quantileNormalize(df_input):
    df = df_input.copy()
    #compute rank
    dic = {}
    for col in df:
        dic[col] = df[col].sort_values(na_position='first').values
    sorted_df = pd.DataFrame(dic)
    #rank = sorted_df.mean(axis = 1).tolist()
    rank = sorted_df.median(axis = 1).tolist()
    #sort
    for col in df:
        # compute percentile rank [0,1] for each score in column 
        t = df[col].rank( pct=True, method='max' ).values
        # replace percentile values in column with quantile normalized score
        # retrieve q_norm score using calling rank with percentile value
        df[col] = [ np.nanpercentile( rank, i*100 ) if ~np.isnan(i) else np.nan for i in t ]
    return df

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接