如何使用pandas groupby()的split-apply-combine模式同时规范化多列

vol*_*erH 4 python normalization pandas split-apply-combine

我正在尝试对包含多个带有数值可观察量(特征)的列、带有日期和实验条件的列以及其他非数值条件(例如文件名)的 Pandas 数据表中的实验数据进行标准化。

我想要

  • 使用拆分-应用-组合范例
  • 使用子组的聚合统计数据在组内标准化
  • 使用不同的归一化(例如除以控制均值、Z 分数)
  • 将此应用于所有数字列(可观察值)
  • 最后,生成与原始结构相同的扩充数据表,但增加了列,例如,对于列 Observable1,应添加列 normalized_Observable1

可以使用以下代码片段生成具有此结构的简化数据表:

import numpy as np
import pandas as pd
df = pd.DataFrame({
   'condition': ['ctrl', 'abc', 'ctrl', 'abc', 'def', 'ctlr', 'ctlr', 'asdasd', 'afff', 'afff', 'gr1','gr2', 'gr2', 'ctrl', 'ctrl', 'kjkj','asht','ctrl'],
   'date':  ['20170131', '20170131', '20170131', '20170131','20170131', '20170606', '20170606', '20170606', '20170606', '20170606', '20170404', '20170404', '20170404', '20170404', '20170404', '20161212', '20161212', '20161212'],
   'observation1':  [1.2, 2.2, 1.3, 1.1, 2.3 , 2.3, 4.2, 3.3, 5.1, 3.3, 3.4, 5.5, 9.9, 3.2, 1.1, 3.3, 1.2, 5.4],
   'observation2':  [3.1, 2.2, 2.1, 1.2,  2.4, 1.2, 1.5, 1.33, 1.5, 1.6, 1.4, 1.3, 0.9, 0.78, 1.2, 4.0, 5.0, 6.0],
   'observation3':  [2.0, 1.2, 1.2, 2.01, 2.55, 2.05, 1.66, 3.2, 3.21, 3.04, 8.01, 9.1, 7.06, 8.1, 7.9, 5.12, 5.23, 5.15],
   'rawsource': ["1.tif", "2.tif", "3.tif",  "4.tif", "5.tif","6.tif", "7.tif", "8.tif", "9.tif", "10.tif", "11.tif", "12.tif", "13.tif", "14.tif", "15.tif", "16.tif", "17.tif", "18.tif"]
})
print(df)
Run Code Online (Sandbox Code Playgroud)

看起来像这样

   condition      date  observation1  observation2  observation3 rawsource
0       ctrl  20170131           1.2          3.10          2.00     1.tif
1        abc  20170131           2.2          2.20          1.20     2.tif
2       ctrl  20170131           1.3          2.10          1.20     3.tif
3        abc  20170131           1.1          1.20          2.01     4.tif
4        def  20170131           2.3          2.40          2.55     5.tif
5       ctlr  20170606           2.3          1.20          2.05     6.tif
6       ctlr  20170606           4.2          1.50          1.66     7.tif
7     asdasd  20170606           3.3          1.33          3.20     8.tif
8       afff  20170606           5.1          1.50          3.21     9.tif
9       afff  20170606           3.3          1.60          3.04    10.tif
10       gr1  20170404           3.4          1.40          8.01    11.tif
11       gr2  20170404           5.5          1.30          9.10    12.tif
12       gr2  20170404           9.9          0.90          7.06    13.tif
13      ctrl  20170404           3.2          0.78          8.10    14.tif
14      ctrl  20170404           1.1          1.20          7.90    15.tif
15      kjkj  20161212           3.3          4.00          5.12    16.tif
16      asht  20161212           1.2          5.00          5.23    17.tif
17      ctrl  20161212           5.4          6.00          5.15    18.tif
Run Code Online (Sandbox Code Playgroud)

现在,对于每个实验日期,我都有不同的实验条件,但我总是有名为ctrl. 我想要执行的规范化之一是计算(对于每个数字列)该日期的控制实验的平均值,然后将该日期的所有可观察值除以相应的平均值。

我可以使用以下方法快速计算一些每个日期、每个条件的摘要统计信息:

grsummary = df.groupby(["date","condition"]).agg((min, max, np.nanmean, np.nanstd))
Run Code Online (Sandbox Code Playgroud)

然后我想在每个实验日期的标准化中应用这些汇总统计数据:

grdate = df.groupby("date")
Run Code Online (Sandbox Code Playgroud)

并以这样的方式应用规范化:

def normalize_by_ctrlmean(grp_frame, summarystats):
    #  the following is only pseudo-code as I don't know how to do this
    grp_frame/ summarystats(nanmean)

grdate.apply(normalize_by_cntrlmean, summarystats= grsummary)
Run Code Online (Sandbox Code Playgroud)

最后一步只是伪代码。这就是我正在努力解决的问题。我可以使用嵌套 for 循环对数字列的日期、条件和列名进行规范化,但我是拆分应用组合范例的新手,我认为必须有一个简单的解决方案?非常感谢任何帮助。

Who*_*ack 7

以下是您可以使用df.apply以下方法执行此操作的方法:


分裂

由于您要“按日期”执行操作,因此只需按日期拆分:

grdate = df.groupby("date")
Run Code Online (Sandbox Code Playgroud)


应用与组合

接下来,定义一个可以应用于每个组的转换函数,将组本身作为参数。

在您的情况下,该函数应计算组ctrl值的均值,然后将组的所有观察值除以该均值:

def norm_apply(group):

    # Select the 'ctrl' condition
    ctrl_selected = group[group['condition']=='ctrl']

    # Extract its numerical values
    ctrl_numeric = ctrl_selected.select_dtypes(include=[np.number])

    # Compute the means (column-wise)
    ctrl_means = np.nanmean(ctrl_numeric,axis=0) 

    # Extract numerical values for all conditions
    group_numeric = group.select_dtypes(include=[np.number])

    # Divide by the ctrl means
    divided = group_numeric / ctrl_means

    # Return result
    return divided
Run Code Online (Sandbox Code Playgroud)

(如果您愿意,您可以将其作为一个愚蠢的单线来执行...)

norm_apply = lambda x : x.select_dtypes(include=[np.number]) / np.nanmean(x[x['condition']=='ctrl'].select_dtypes(include=[np.number]),axis=0)
Run Code Online (Sandbox Code Playgroud)


现在您可以简单地apply将此函数添加到您的分组数据框中:

normed = grdate.apply(norm_apply)
Run Code Online (Sandbox Code Playgroud)

这应该给你你需要的值,组合成与原始 df 相同的形状/顺序:

normed.head()

>>   observation1  observation2  observation3
0          0.96      1.192308       1.25000
1          1.76      0.846154       0.75000
2          1.04      0.807692       0.75000
3          0.88      0.461538       1.25625
4          1.84      0.923077       1.59375
Run Code Online (Sandbox Code Playgroud)


合并到原始数据帧

将这些结果添加回原始 df 的一种方法是这样的:

# Add prefix to column names
normed = normed.add_prefix('normed_')

# Concatenate with initial data frame
final = pd.concat([df,normed],axis=1)
display(final.head())
Run Code Online (Sandbox Code Playgroud)


最后,您可以按日期和条件分组并查看方法:

final.groupby(['date','condition']).mean()
Run Code Online (Sandbox Code Playgroud)

如果一切正常,则ctlr条件均应为1.0。



(旁注:虽然 Ian Thompson 的回答也有效,但我相信这种方法更符合拆分-应用-组合的意识形态。)