lokheart
Estoy haciendo un trabajo de geocodificación que usé selenium
para raspar en pantalla la coordenada xy que necesito para la dirección de una ubicación, importé un archivo xls al marco de datos de pandas y quiero usar un bucle explícito para actualizar las filas que no tienen la coordenada xy, como a continuación:
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']
He leído ¿Por qué esta función no “toma” después de iterar sobre un DataFrame de pandas? y soy plenamente consciente de que iterrows
solo nos da una vista en lugar de una copia para editar, pero ¿y si realmente actualizo el valor fila por fila? Es lambda
¿factible?
Las filas de las que regresas iterrows
son copias que ya no están conectadas al marco de datos original, por lo que las ediciones no cambian su marco de datos. Afortunadamente, porque cada artículo que recibes de iterrows
contiene el índice actual, puede usarlo para acceder y editar la fila relevante del marco de datos:
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']
En mi experiencia, este enfoque parece más lento que usar un enfoque como apply
o map
pero como siempre, depende de usted decidir cómo equilibrar el rendimiento y la facilidad de codificación.
-
Esto no es estrictamente cierto, es posible que no sean copias. Específicamente si el dtype es el mismo para todas las columnas
–Andy Hayden
25 de agosto de 2014 a las 7:51
-
Esto me dio una advertencia de copia. Terminé usando: stackoverflow.com/questions/33518124/…
-Peter Ehrlich
26 de diciembre de 2017 a las 18:53
-
¿No recuperas el índice de todos modos? Consulte la respuesta de @ jpp a Pandas para el bucle sobre el marco de datos que proporciona demasiados valores para desempaquetar. El error que obtengo del código en esta respuesta es
ValueError: too many values to unpack (expected 2)
– papa tonto
12 de febrero de 2019 a las 14:24
-
Descubrí que tenía que correr
df = df.reset_index()
para que esto funcione sin un error de índice porque había cortado y cortado mi marco de datos.– Gama032
24 de diciembre de 2021 a las 6:54
Otra forma basada en esta pregunta:
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']
Este enlace describe la diferencia entre .loc
y .at
. Dentro de poco, .at
más rápido que .loc
.
1. Uso itertuples()
en cambio
Pandas DataFrames son realmente una colección de objetos de columnas/series (por ejemplo, for x in df
itera sobre las etiquetas de las columnas), por lo que incluso si se implementa un bucle, es mejor si el bucle se repite entre las columnas. iterrows()
es anti-patrón para ese comportamiento de pandas “nativo” porque crea una serie para cada fila, lo que ralentiza mucho el código. Una opción mejor/más rápida es usar itertuples()
. Crea tuplas con nombre de cada fila a las que puede acceder por índice o por etiqueta de columna. Casi no hay modificación en el código en el OP para aplicarlo.
Además (como mencionó @Alireza Mazochi), para asignar un valor a una sola celda, at
es más rápido que 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]`
Como puedes ver, usando itertuples()
es casi la misma sintaxis que usar iterrows()
sin embargo, es más de 6 veces más rápido (puede verificarlo con un simple timeit
prueba).
2. to_dict()
tambien es una opcion
Un inconveniente de itertuples()
es que cada vez que hay un espacio en la etiqueta de una columna (p. ej. 'Col A'
etc.), se destruirá cuando se convierta en una tupla con nombre, por lo que, por ejemplo, si 'address_chi'
era 'address chi'
no será posible acceder a él a través de row.address chi
. Una forma de resolver este problema es convertir el DataFrame en un diccionario e iterarlo.
Una vez más, la sintaxis es casi la misma que la utilizada para 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']
Este método también es unas 6 veces más rápido que iterrows()
pero un poco más lento que itertuples()
(también es más intensivo en memoria que itertuples()
porque crea un diccionario explícito mientras que itertuples()
crea un generador).
3. Iterar solo sobre las columnas/filas necesarias
El principal cuello de botella en el código particular en el OP (y en general, por qué a veces es necesario un bucle en un marco de datos de pandas) es que la función geocoding()
no está vectorizado. Entonces, una forma de hacer que el código sea mucho más rápido es llamarlo solo en la columna relevante ('address_chi'
) y en las filas correspondientes (filtradas con una máscara booleana).
Tenga en cuenta que la creación de la máscara booleana fue necesaria solo porque había una cláusula if en el código original. Si no se necesitaba una verificación condicional, no se necesita la máscara booleana, por lo que el ciclo necesario se reduce a un solo ciclo sobre una columna en particular (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
Este método es unas 40 veces más rápido que iterrows()
.
Un ejemplo práctico y una prueba de rendimiento
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)
I pensar tu puedes hacer
rche_df.loc[index, 'wgs1984_latitude'] = dict_temp['lat']
, es decir, use el índice para llegar a la sección correcta del marco de datos original. Avíseme si eso no funciona e intentaré encontrar una respuesta adecuada.– Mario
25 de agosto de 2014 a las 3:39
Parece que @Marius está funcionando, gracias, otra alternativa es convertir el marco de datos en dict y usar el bucle for normal para realizar la modificación.
– lokheart
25 de agosto de 2014 a las 3:43
Esta respuesta no funcionó para mí (por qué diablos no…), pero esto sí: stackoverflow.com/questions/23330654/…
– pablo
25 de mayo de 2018 a las 14:57