在iterrows循环中更新pandas dataframe中的值

124 投票
3 回答
167741 浏览
提问于 2025-04-18 18:24

我正在做一些地理编码的工作,使用 selenium 来抓取我需要的地址的 x-y 坐标。我把一个 xls 文件导入到了 pandas 数据框中,想用一个明确的循环来更新那些没有 x-y 坐标的行,像下面这样:

for index, row in rche_df.iterrows():
    if isinstance(row.wgs1984_latitude, float):
        row = row.copy()
        target = row.address_chi
        dict_temp = geocoding(target)
        row.wgs1984_latitude = dict_temp['lat']
        row.wgs1984_longitude = dict_temp['long']

我看过了 为什么在对 pandas 数据框使用 iterrows 时这个函数不生效?,我知道 iterrows 只是给我们一个视图,而不是可以编辑的副本。但是,如果我真的想逐行更新值呢?使用 lambda 可行吗?

3 个回答

2

1. 使用 itertuples() 更好

Pandas 的 DataFrame 实际上是由一系列列/序列对象组成的(比如说 for x in df 是在遍历列的标签),所以即使要用循环,最好还是在列之间循环。使用 iterrows() 这种方式其实是不太推荐的,因为它会为每一行创建一个序列,这样会让代码变得很慢。更好的选择是使用 itertuples()。这个方法会为每一行创建一个命名元组,你可以通过索引或者列标签来访问它。对原代码几乎没有什么修改就能使用这个方法。

另外(正如 @Alireza Mazochi 提到的),如果你想给某个单元格赋值,使用 atloc 更快。

for row in rche_df.itertuples():
#                  ^^^^^^^^^^   <------ `itertuples` instead of `iterrows`
    if isinstance(row.wgs1984_latitude, float):
        target = row.address_chi
        dict_temp = geocoding(target)
        rche_df.at[row.Index, 'wgs1984_latitude'] = dict_temp['lat']
        rche_df.at[row.Index, 'wgs1984_longitude'] = dict_temp['long']
        #       ^^ ^^^^^^^^^  <---- `at` instead of `loc` for faster assignment
        #                           `row.Index` is the row's index, can also use `row[0]`

如你所见,使用 itertuples() 的语法几乎和 iterrows() 一样,但速度快了超过 6 倍(你可以用简单的 timeit 测试来验证这一点)。

2. to_dict() 也是一个选择

使用 itertuples() 的一个缺点是,如果列标签中有空格(例如 'Col A'),在转换为命名元组时会出现问题。所以如果 'address_chi''address chi',你就不能通过 row.address chi 来访问它。解决这个问题的一种方法是把 DataFrame 转换成字典,然后遍历这个字典。

同样,语法几乎和 iterrows() 一样。

for index, row in rche_df.to_dict('index').items():
#                         ^^^^^^^^^^^^^^^^^^^^^^^^  <---- convert to a dict
    if isinstance(row['wgs1984_latitude'], float):
        target = row['address_chi']
        dict_temp = geocoding(target)
        rche_df.at[index, 'wgs1984_latitude'] = dict_temp['lat']
        rche_df.at[index, 'wgs1984_longitude'] = dict_temp['long']

这种方法的速度也比 iterrows() 快大约 6 倍,但比 itertuples() 稍慢(而且它比 itertuples() 更占内存,因为它创建了一个明确的字典,而 itertuples() 创建的是一个生成器)。

3. 只遍历必要的列/行

在原代码中,主要的瓶颈(以及为什么有时候在 pandas DataFrame 中需要循环)是因为 geocoding() 这个函数没有向量化。因此,让代码变得更快的一种方法是只在相关的列('address_chi')和相关的行上调用它(使用布尔掩码进行过滤)。

注意,创建布尔掩码只是因为原代码中有一个 if 条件。如果不需要条件检查,就不需要布尔掩码,那么必要的循环就只需要在特定的列(address_chi)上进行一次循环。

# boolean mask to filter only the relevant rows
# this is analogous to if-clause in the loop in the OP
msk = [isinstance(row, float) for row in rche_df['wgs1984_latitude'].tolist()]

# call geocoding on the relevant values 
# (filtered using the boolean mask built above) 
# in the address_chi column
# and create a nested list
out = []
for target in rche_df.loc[msk, 'address_chi'].tolist():
    dict_temp = geocoding(target)
    out.append([dict_temp['lat'], dict_temp['long']])

# assign the nested list to the relevant rows of the original frame
rche_df.loc[msk, ['wgs1984_latitude', 'wgs1984_longitude']] = out

这种方法比 iterrows() 快大约 40 倍。


一个有效的示例和性能测试

def geocoding(x):
    return {'lat': x*2, 'long': x*2}


def iterrows_(df):
    
    for index, row in df.iterrows():
        if isinstance(row.wgs1984_latitude, float):
            target = row.address_chi        
            dict_temp = geocoding(target)
            df.at[index, 'wgs1984_latitude'] = dict_temp['lat']
            df.at[index, 'wgs1984_longitude'] = dict_temp['long']
    
    return df


def itertuples_(df):
    
    for row in df.itertuples():
        if isinstance(row.wgs1984_latitude, float):
            target = row.address_chi
            dict_temp = geocoding(target)
            df.at[row.Index, 'wgs1984_latitude'] = dict_temp['lat']
            df.at[row.Index, 'wgs1984_longitude'] = dict_temp['long']
        
    return df


def to_dict_(df):
    
    for index, row in df.to_dict('index').items():
        if isinstance(row['wgs1984_latitude'], float):
            target = row['address_chi']
            dict_temp = geocoding(target)
            df.at[index, 'wgs1984_latitude'] = dict_temp['lat']
            df.at[index, 'wgs1984_longitude'] = dict_temp['long']
            
    return df


def boolean_mask_loop(df):

    msk = [isinstance(row, float) for row in df['wgs1984_latitude'].tolist()]

    out = []
    for target in df.loc[msk, 'address_chi'].tolist():
        dict_temp = geocoding(target)
        out.append([dict_temp['lat'], dict_temp['long']])

    df.loc[msk, ['wgs1984_latitude', 'wgs1984_longitude']] = out
    
    return df


df = pd.DataFrame({'address_chi': range(20000)})
df['wgs1984_latitude'] = pd.Series([x if x%2 else float('nan') for x in df['address_chi'].tolist()], dtype=object)


%timeit itertuples_(df.copy())
# 248 ms ± 12.6 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

%timeit boolean_mask_loop(df.copy())
# 38.7 ms ± 1.19 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

%timeit to_dict_(df.copy())
# 289 ms ± 10.1 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

%timeit iterrows_(df.copy())
# 1.57 s ± 27.8 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
11

还有一种方法,基于这个问题

for index, row in rche_df.iterrows():
    if isinstance(row.wgs1984_latitude, float):
        row = row.copy()
        target = row.address_chi        
        dict_temp = geocoding(target)
        
        rche_df.at[index, 'wgs1984_latitude'] = dict_temp['lat']
        rche_df.at[index, 'wgs1984_longitude'] = dict_temp['long']

这个链接讲解了.loc.at之间的区别。简单来说,.at.loc快。

223

iterrows 得到的每一行数据其实是原始数据框的副本,所以对这些副本的修改不会影响到原始的数据框。幸运的是,因为你从 iterrows 得到的每一项都包含了当前的索引,你可以用这个索引来访问和修改数据框中对应的那一行:

for index, row in rche_df.iterrows():
    if isinstance(row.wgs1984_latitude, float):
        row = row.copy()
        target = row.address_chi        
        dict_temp = geocoding(target)
        rche_df.loc[index, 'wgs1984_latitude'] = dict_temp['lat']
        rche_df.loc[index, 'wgs1984_longitude'] = dict_temp['long']

根据我的经验,这种方法的速度似乎比使用 applymap 这样的方式要慢一些,但最终还是要看你自己怎么权衡性能和编码的简便性。

撰写回答