Actualización de un valor en un marco de datos de pandas en un bucle iterrows [duplicate]

8 minutos de lectura

avatar de usuario de lokheart
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?

  • 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

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 mappero 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)

¿Ha sido útil esta solución?