在iterrows循环中更新pandas dataframe中的值
我正在做一些地理编码的工作,使用 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 个回答
1. 使用 itertuples()
更好
Pandas 的 DataFrame 实际上是由一系列列/序列对象组成的(比如说 for x in df
是在遍历列的标签),所以即使要用循环,最好还是在列之间循环。使用 iterrows()
这种方式其实是不太推荐的,因为它会为每一行创建一个序列,这样会让代码变得很慢。更好的选择是使用 itertuples()
。这个方法会为每一行创建一个命名元组,你可以通过索引或者列标签来访问它。对原代码几乎没有什么修改就能使用这个方法。
另外(正如 @Alireza Mazochi 提到的),如果你想给某个单元格赋值,使用 at
比 loc
更快。
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)
还有一种方法,基于这个问题:
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
快。
从 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']
根据我的经验,这种方法的速度似乎比使用 apply
或 map
这样的方式要慢一些,但最终还是要看你自己怎么权衡性能和编码的简便性。