如何有效地向pandas数据框添加多个列,其值依赖于其他列

时间:2018-01-19 15:57:55

标签: python pandas dataframe

我有什么:

  • 包含许多行和几个现有列(python,pandas)的数据框。
  • Python 3.6,所以依赖于特定版本的解决方案对我来说很好(但显然也适用于早期版本的解决方案也很好)。

我想做什么:

  • 向数据框添加多个其他列,其中这些新列中的值都取决于同一行中现有列中值的某些方式。
  • 必须保留数据帧的原始顺序。如果解决方案改变了排序,我可以通过基于其中一个现有列手动排序来恢复它,但显然这会带来额外的开销。

我已经有以下代码,它可以正常工作。但是,分析表明此代码是我的代码中的重要瓶颈之一,所以我想尽可能优化它,我也有理由相信应该是可能的:

df["NewColumn1"] = df.apply(lambda row: compute_new_column1_value(row), axis=1)
df["NewColumn2"] = df.apply(lambda row: compute_new_column2_value(row), axis=1)

# a few more lines of code like the above

我将此解决方案基于this one之类的问题的答案(这是一个类似于我的问题,但特别是关于添加一个新列,而我的问题是添加许多新列)。我想这些df.apply()调用中的每一个都是通过所有行的循环在内部实现的,我怀疑应该可以使用只循环所有循环一次的解决方案来优化它(而不是每次一次)我要添加的专栏。)

在其他答案中,我看到了对assign()函数的引用,它确实支持一次添加多个列。我尝试以下列方式使用它:

# WARNING: this does NOT work
df = df.assign(
    NewColumn1=lambda row: compute_new_column1_value(row),
    NewColumn2=lambda row: compute_new_column2_value(row),
    # more lines like the two above
)

这不起作用的原因是因为lambda实际上根本没有接收到数据帧的行,所以它们似乎只是立即得到整个数据帧。然后期望每个lambda一次返回一个完整的列/系列/数组值。所以,我的问题在于我必须最终在lambda中的所有循环中实现手动循环,这显然会对性能更糟。

我可以从概念上考虑两个解决方案,但到目前为止还无法找到如何实际实现它们:

  1. 类似于df.assign()(支持一次添加多个列),但能够将行传递到lambda而不是完整的数据帧

  2. 一种向compute_new_columnX_value()函数进行向量化的方法,以便它们可以像df.assign()期望的那样用作lambda。

  3. 到目前为止我的第二个解决方案的问题是基于行的版本我的一些函数看起来如下,我很难找到如何正确地向量化它们:

    def compute_new_column1_value(row):
        if row["SomeExistingColumn"] in some_dictionary:
            return some_dictionary[row["SomeExistingColumn"]]
        else:
            return some_default_value
    

7 个答案:

答案 0 :(得分:2)

您是否尝试将列初始化为nan,逐行迭代数据,并使用loc分配值?

import numpy as np
import pandas as pd

df = pd.DataFrame(np.random.randint(0, 20, (10, 5)))

df[5] = np.nan
df[6] = np.nan

for i, row in df.iterrows():
    df.loc[i, 5] = row[1] + row[4]
    df.loc[i, 6] = row[3] * 2

print(df)

产量

    0   1   2   3   4
0  17   4   3  11  10
1  16   1  14  11  16
2   4  18  12  19   7
3  11   3   7  10   5
4  11   0  10   1  17
5   5  17  10   3   8
6   0   0   7   3   6
7   7  18  18  13   8
8  16   4  12  11  16
9  13   9  15   8  19

    0   1   2   3   4     5     6
0  17   4   3  11  10  14.0  22.0
1  16   1  14  11  16  17.0  22.0
2   4  18  12  19   7  25.0  38.0
3  11   3   7  10   5   8.0  20.0
4  11   0  10   1  17  17.0   2.0
5   5  17  10   3   8  25.0   6.0
6   0   0   7   3   6   6.0   6.0
7   7  18  18  13   8  26.0  26.0
8  16   4  12  11  16  20.0  22.0
9  13   9  15   8  19  28.0  16.0

答案 1 :(得分:1)

不是试图将行标签放入.assign(),而是可以 在将.assign()链接到它之前,将一个布尔掩码应用于数据框。下面的示例可以很容易地扩展到多个布尔条件和多个lambdas,有或没有额外的for循环或if语句。

import pandas as pd

# Create data frame
idx = np.arange(0, 10)
rnd = pd.Series(np.random.randint(10, 20, 10))
alpha_idx = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']

df = pd.DataFrame({'idx': idx, 'A': rnd, 'B': 100})
df.index = alpha_idx

# First assign() dependent on a boolean mask
df_tmp = df[df['A'] < 15].assign(AmulB = lambda x: (x.A.mul(x.B)),
               A_B = lambda x: x.B - x.A)

# Second assign() dependent on a boolean mask
df_tmp2 = df[df['A'] >= 15].assign(AmulB = lambda x: (x.A.div(x.B)),
               A_B = lambda x: x.B + x.A)


# Create a new df with different lambdas combined
df_lambdas = df_tmp.append(df_tmp2)

# Sort values
df_lambdas.sort_values('idx', axis=0, inplace=True)
print(df_lambdas)

    A    B  idx
a  19  100    0
b  17  100    1
c  16  100    2
d  13  100    3
e  15  100    4
f  10  100    5
g  16  100    6
h  15  100    7
i  13  100    8
j  10  100    9 

    A    B  idx  A_B    AmulB
a  19  100    0  119     0.19
b  17  100    1  117     0.17
c  16  100    2  116     0.16
d  13  100    3   87  1300.00
e  15  100    4  115     0.15
f  10  100    5   90  1000.00
g  16  100    6  116     0.16
h  15  100    7  115     0.15
i  13  100    8   87  1300.00
j  10  100    9   90  1000.00

答案 2 :(得分:1)

如果您只有50个条件要检查,那么迭代条件并填充块中的单元格而不是逐行遍历整个帧可能更好。顺便说一句.assign()不仅接受lambda函数,而且代码也可以比我之前的建议更具可读性。下面是一个修改版本,也填充了额外的列。如果这个数据框有10,000,000行,我只想在A列中对10组数字范围应用不同的操作,这将是填充额外列的一种非常巧妙的方法。

import pandas as pd
import numpy as np

# Create data frame
rnd = np.random.randint(1, 10, 10)
rnd2 = np.random.randint(100, 1000, 10)
df = pd.DataFrame(
        {'A': rnd, 'B': rnd2, 'C': np.nan, 'D': np.nan, 'E': np.nan })

# Define different ways of filling the extra cells
def f1():
    return df['A'].mul(df['B'])

def f2():
    return np.log10(df['A'])

def f3():
    return df['B'] - df['A']

def f4():
    return df['A'].div(df['B'])

def f5():
    return np.sqrt(df['B'])

def f6():
    return df['A'] + df['B']

# First assign() dependent on a boolean mask
df[df['A'] < 50] = df[df['A'] < 15].assign(C = f1(), D = f2(), E = f3())

# Second assign() dependent on a boolean mask
df[df['A'] >= 50] = df[df['A'] >= 50].assign(C = f4(), D = f5(), E = f6())

print(df)

     A      B       C         D    E
0  4.0  845.0  3380.0  0.602060  841
1  3.0  967.0  2901.0  0.477121  964
2  3.0  468.0  1404.0  0.477121  465
3  2.0  548.0  1096.0  0.301030  546
4  3.0  393.0  1179.0  0.477121  390
5  7.0  741.0  5187.0  0.845098  734
6  1.0  269.0   269.0  0.000000  268
7  4.0  731.0  2924.0  0.602060  727
8  4.0  193.0   772.0  0.602060  189
9  3.0  306.0   918.0  0.477121  303

答案 3 :(得分:1)

我真的被这个问题所吸引,所以这是涉及外部词典的另一个例子:

import pandas as pd
import numpy as np

# Create data frame and external dictionaries
rnd = pd.Series(np.random.randint(10, 100, 10))

names = 'Rafael Roger Grigor Alexander Dominic Marin David Jack Stan Pablo'
name = names.split(' ')

surnames = 'Nadal Federer Dimitrov Zverev Thiem Cilic Goffin Sock Wawrinka Busta'
surname = surnames.split()

countries_str = ('Spain Switzerland Bulgaria Germany Austria Croatia Belgium USA Switzerland Spain')
country = countries_str.split(' ')

player = dict(zip(name, surname))
player_country = dict(zip(name, country))

df = pd.DataFrame(
        {'A': rnd, 'B': 100, 'Name': name, 'Points': np.nan, 'Surname': np.nan, 'Country': np.nan})

df = df[['A', 'B', 'Name', 'Surname', 'Country', 'Points']]
df.loc[9, 'Name'] = 'Dennis'

print(df)

# Functions to fill the empty columns
def f1():
    return df['A'].mul(df['B'])

def f2():
    return np.random.randint(1, 10)

def f3():
    return player[key]

def f4():
    return player_country[key]

def f5():
    return 'Unknown'

def f6():
    return 0

# .assign() dependent on a boolean mask
for key, value in player.items():
    df[df['Name'] == key] = df[df['Name'] == key].assign(
            Surname = f3(), Country = f4(), Points = f1())

df[df['Name']=='Dennis'] = df[df['Name'] == 'Dennis'].assign(
        Surname = f5(), Country = f5(), Points = f6())
df = df.sort_values('Points', ascending=False)
print(df)

     A      B       Name   Surname      Country  Points
1  97.0  100.0      Roger   Federer  Switzerland  9700.0
4  93.0  100.0    Dominic     Thiem      Austria  9300.0
8  92.0  100.0       Stan  Wawrinka  Switzerland  9200.0
5  86.0  100.0      Marin     Cilic      Croatia  8600.0
6  67.0  100.0      David    Goffin      Belgium  6700.0
7  61.0  100.0       Jack      Sock          USA  6100.0
0  35.0  100.0     Rafael     Nadal        Spain  3500.0
2  34.0  100.0     Grigor  Dimitrov     Bulgaria  3400.0
3  25.0  100.0  Alexander    Zverev      Germany  2500.0
9  48.0  100.0     Dennis   Unknown      Unknown     0.0

答案 4 :(得分:0)

由于我在评论中提供的原因,到目前为止提供的答案并没有为我的具体案例提供加速。到目前为止,我能找到的最佳解决方案主要基于this answer to another question。它没有给我一个大的加速(大约10%),但它是迄今为止我能做到的最好的。如果它们存在,我仍然对更快的解决方案非常感兴趣!

事实证明,就像assign函数一样,apply实际上也可以提供lambda,它一次返回多列的一系列值,而不是只返回一个lambda的值标量。所以,到目前为止我实施的最快的实现如下:

# first initialize all the new columns with standard values for entire df at once
# this turns out to be very important. Skipping this comes at a high computational cost
for new_column in ["NewColumn1", "NewColumn2", "etc."]:
    df[new_column] = np.nan

df = df.apply(compute_all_new_columns, axis=1)

然后,不是将所有那些单独的lambda用于所有不同的新列,而是将它们全部实现在同一个函数中:

def compute_all_new_columns(row):
    if row["SomeExistingColumn"] in some_dictionary:
        row["NewColumn1"] = some_dictionary[row["SomeExistingColumn"]]
    else:
        row["NewColumn1"] = some_default_value

    if some_other_condition:
        row["NewColumn2"] = whatever
    else:
        row["NewColumn2"] = row["SomeExistingColumn"] * whatever

    # assign values to other new columns here

结果数据框包含它先前执行的所有列,以及compute_all_new_columns函数逐行插入的所有新列的值。保留原始顺序。这个解决方案不包含任何基于python的循环(速度很慢),并且只有一个循环通过“幕后”的行''由pandas apply函数提供给我们

答案 5 :(得分:0)

此代码解决了我的答案的缺点,该答案基于使用外部字典填充数据框中的其他列。由于字典可以很容易地转换为数据帧,因此该示例基于从另一个数据帧的列中绘制数据。在这里,我回答的问题是,2017年十大网球运动员的名字在美国是多么受欢迎。这是通过从2016年美国社会保障管理局登记册中搜索32,868个婴儿名字并将性别和婴儿数量列添加到ATP数据框来实现的。我没有计时,但结果很快就会回来,我希望这比使用词典更适合你。由于后者中的密钥不能重复,因此您不会获得下面给出的两个性别的婴儿名称的行重复。

# Show first few lines and info
print('Baby Name Data Head:')
print(df_names.head())

print('Baby Name Data Info:')
print(df_names.info())

print('ATP Top Ten: \n', df_ATP)

#           Add the baby inormation to the ATP data frame

df_ATP = pd.merge(df_ATP, df_names, on=['Name', 'Name'], how='left')
print('Modified ATP Top Ten: \n', df_ATP)

Baby Name Data Head:
       Name Sex  Number_of_Births
0      Emma   F             19414
1    Olivia   F             19246
2       Ava   F             16237
3    Sophia   F             16070
4  Isabella   F             14722

Baby Name Data Info:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 32868 entries, 0 to 32867
Data columns (total 3 columns):
Name                32868 non-null category
Sex                 32868 non-null category
Number_of_Births    32868 non-null int64
dtypes: category(2), int64(1)
memory usage: 1.8 MB

ATP Top Ten: 
     A    B       Name   Surname      Country  Points
0  96  100      Roger   Federer  Switzerland  9600.0
1  80  100     Grigor  Dimitrov     Bulgaria  8000.0
2  72  100    Dominic     Thiem      Austria  7200.0
3  65  100      Pablo     Busta        Spain  6500.0
4  58  100       Stan  Wawrinka  Switzerland  5800.0
5  56  100       Jack      Sock          USA  5600.0
6  44  100      Marin     Cilic      Croatia  4400.0
7  43  100      David    Goffin      Belgium  4300.0
8  25  100  Alexander    Zverev      Germany  2500.0
9  14  100     Rafael     Nadal        Spain  1400.0

Modified ATP Top Ten: 
      A    B       Name   Surname      Country  Points Sex  Number_of_Births
0   96  100      Roger   Federer  Switzerland  9600.0   M               407
1   80  100     Grigor  Dimitrov     Bulgaria  8000.0   M                 7
2   72  100    Dominic     Thiem      Austria  7200.0   F                11
3   72  100    Dominic     Thiem      Austria  7200.0   M              5394
4   65  100      Pablo     Busta        Spain  6500.0   M               787
5   58  100       Stan  Wawrinka  Switzerland  5800.0   M                15
6   56  100       Jack      Sock          USA  5600.0   F                11
7   56  100       Jack      Sock          USA  5600.0   M              8367
8   44  100      Marin     Cilic      Croatia  4400.0   F               165
9   44  100      Marin     Cilic      Croatia  4400.0   M                10
10  43  100      David    Goffin      Belgium  4300.0   F                14
11  43  100      David    Goffin      Belgium  4300.0   M             11028
12  25  100  Alexander    Zverev      Germany  2500.0   F                18
13  25  100  Alexander    Zverev      Germany  2500.0   M             13321
14  14  100     Rafael     Nadal        Spain  1400.0   M              1232

答案 6 :(得分:0)

由于您检查了代码很慢,因为您正在检查非常大的字典的键,我想我会发布下面的代码。这里的技巧是使用set intersection,它产生一个集合,该集合仅包含在感兴趣的数据框架和参考字典中找到的密钥。下面,我正在快速推导出一个包含2017年前10名ATP网球运动员名字的数据框,使用30294长的婴儿名字词典。由此产生的集合交集只有10个元素,因为它已经找到了字典中所有前10名玩家的名字。因此,允许填充额外列的for循环非常短。

import pandas as pd

# Prepare a large dictionary from the file containing US baby names from 2016
# The number of keys is 30294
df_bn = pd.read_csv('C:\yob2016.txt', header=None)   
names = list(df_bn[0])
sex = list(df_bn[1])
bn_dict = dict(zip(names, sex))

# Create a tennis player data frame
names = 'Rafael Roger Grigor Alexander Dominic Marin David Jack Stan Pablo'
name = names.split(' ')
surnames = 'Nadal Federer Dimitrov Zverev Thiem Cilic Goffin Sock Wawrinka Busta'
surname = surnames.split()
df = pd.DataFrame({'Name': name, 'Surname': surname})

# Set intersection and filling the new data frame column
bn_set = set(bn_dict.keys())
tennis_player_set = set(df['Name'])

set_intersection = bn_set.intersection(tennis_player_set)

df['Sex'] = ''

for key in set_intersection:
    df.loc[df['Name'] == key, 'Sex'] = bn_dict[key]

print(df)

        Name   Surname Sex
0     Rafael     Nadal   M
1      Roger   Federer   M
2     Grigor  Dimitrov   M
3  Alexander    Zverev   M
4    Dominic     Thiem   M
5      Marin     Cilic   M
6      David    Goffin   M
7       Jack      Sock   M
8       Stan  Wawrinka   M
9      Pablo     Busta   M