SettingWithCopyWarning
es una advertencia que Pandas puede emitir cuando hacemos una asignación a un DataFrame. Esto puede ocurrir cuando usamos asignaciones encadenadas o cuando usamos un DataFrame creado a partir de un corte. Es una fuente común de errores en el código de Pandas que todos hemos enfrentado antes. Puede ser difícil de depurar porque la advertencia puede aparecer en un código que parece que debería funcionar bien.
Comprender el SettingWithCopyWarning
es importante porque señala posibles problemas con la manipulación de datos. Esta advertencia sugiere que su código puede no estar alterando los datos según lo previsto, lo que puede resultar en consecuencias no deseadas y errores oscuros difíciles de rastrear.
En este artículo, exploraremos el SettingWithCopyWarning
en pandas y cómo evitarlo. También discutiremos el futuro de Pandas y cómo la opción copy_on_write
cambiará la forma en que trabajamos con DataFrames.
Vistas y Copias de DataFrame
Cuando seleccionamos un corte de un DataFrame y lo asignamos a una variable, podemos obtener una vista o una copia fresca del DataFrame.
Con una vista, la memoria entre ambos DataFrames se comparte. Esto significa que modificar un valor de una celda que está presente en ambos DataFrames modificará a ambos.
Con una copia, se asigna nueva memoria y se crea un DataFrame independiente con los mismos valores que el original. En este caso, ambos DataFrames son entidades distintas, por lo que modificar un valor en uno de ellos no afecta al otro.
Pandas intenta evitar crear una copia cuando puede para optimizar el rendimiento. Sin embargo, es imposible predecir de antemano si obtendremos una vista o una copia. La SettingWithCopyWarning
se genera siempre que asignamos un valor a un DataFrame para el cual no está claro si es una copia o una vista de otro DataFrame.
Comprendiendo la SettingWithCopyWarning
con Datos Reales
Utilizaremos el conjunto de datos de Kaggle Este Real Estate Data London 2024 para aprender cómo ocurre la SettingWithCopyWarning
y cómo solucionarlo.
Este conjunto de datos contiene información reciente sobre bienes raíces en Londres. Aquí tienes un resumen de las columnas presentes en el conjunto de datos:
addedOn
: La fecha en la que se agregó la lista.title
: El título de la lista.descriptionHtml
: Una descripción HTML de la lista.propertyType
: El tipo de propiedad. El valor será"No especificado"
si el tipo no fue especificado.sizeSqFeetMax
: El tamaño máximo en pies cuadrados.bedrooms
: El número de dormitorios.listingUpdatedReason
: Razón para actualizar la lista (por ejemplo, nueva lista, reducción de precio).price
: El precio de la lista en libras.
Ejemplo con una variable temporal explícita
Digamos que se nos dice que las propiedades con un tipo de propiedad no especificado son casas. Por lo tanto, queremos actualizar todas las filas con propertyType
igual a "Not Specified"
a "House"
. Una forma de hacer esto es filtrar las filas con un tipo de propiedad no especificado en una variable DataFrame temporal y actualizar los valores de la columna propertyType
de esta manera:
import pandas as pd dataset_name = "realestate_data_london_2024_nov.csv" df = pd.read_csv(dataset_name) # Obtener todas las filas con un tipo de propiedad no especificado no_property_type = df[df["propertyType"] == "Not Specified"] # Actualizar el tipo de propiedad a "Casa" en esas filas no_property_type["propertyType"] = "House"
Al ejecutar este código, pandas producirá la SettingWithCopyWarning
:
SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame. Try using .loc[row_indexer,col_indexer] = value instead See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy no_property_type["propertyType"] = "House"
La razón de esto es que pandas no puede saber si el DataFrame no_property_type
es una vista o una copia de df
.
Esto es un problema porque el comportamiento del código siguiente puede ser muy diferente dependiendo de si es una vista o una copia.
En este ejemplo, nuestro objetivo es modificar el DataFrame original. Esto solo sucederá si no_property_type
es una vista. Si el resto de nuestro código asume que df
fue modificado, puede estar equivocado porque no hay forma de garantizar que este sea el caso. Debido a este comportamiento incierto, Pandas emite la advertencia para informarnos sobre ese hecho.
Incluso si nuestro código se ejecuta correctamente porque obtuvimos una vista, podríamos obtener una copia en ejecuciones posteriores y el código no funcionará como se pretendía. Por lo tanto, es importante no ignorar esta advertencia y asegurarnos de que nuestro código siempre haga lo que queremos que haga.
Ejemplo con una variable temporal oculta
En el ejemplo anterior, es claro que se está utilizando una variable temporal porque estamos asignando explícitamente parte del DataFrame a una variable llamada no_property_type
.
Sin embargo, en algunos casos, esto no es tan explícito. El ejemplo más común donde se produce la advertencia SettingWithCopyWarning
es con indexación encadenada. Supongamos que reemplazamos las dos últimas líneas por una sola línea:
df[df["propertyType"] == "Not Specified"]["propertyType"] = "House"
A simple vista, no parece que se esté creando una variable temporal. Sin embargo, al ejecutarlo también resulta en una advertencia SettingWithCopyWarning
.
La forma en que se ejecuta este código es:
df[df["propertyType"] == "Not Specified"]
se evalúa y se almacena temporalmente en la memoria.- Se accede al índice
["propertyType"]
de esa ubicación temporal en la memoria.
Los accesos a los índices se evalúan uno por uno y, por lo tanto, la indexación encadenada da como resultado la misma advertencia porque no sabemos si los resultados intermedios son vistas o copias. El código anterior es esencialmente lo mismo que hacer:
tmp = df[df["propertyType"] == "Not Specified"] tmp["propertyType"] = "House"
Este ejemplo se conoce comúnmente como indexación encadenada porque encadenamos accesos indexados usando []
. Primero, accedemos a [df["propertyType"] == "Not Specified"]
y luego a ["propertyType"]
.
Cómo resolver la advertencia SettingWithCopyWarning
Aprendamos cómo escribir nuestro código para que no haya ambigüedad y no se active el SettingWithCopyWarning
. Aprendimos que la advertencia surge de una ambigüedad sobre si un DataFrame es una vista o una copia de otro DataFrame.
La manera de solucionarlo es asegurarnos de que cada DataFrame que creemos sea una copia si queremos que sea una copia o una vista si queremos que sea una vista.
Modificar de forma segura el DataFrame original con loc
Corrijamos el código del ejemplo anterior donde queremos modificar el DataFrame original. Para evitar usar una variable temporal, use la propiedad del indexador loc
.
df.loc[df["propertyType"] == "Not Specified", "propertyType"] = "House"
Con este código, estamos actuando directamente sobre el DataFrame original df
a través de la propiedad del indexador loc
, por lo que no hay necesidad de variables intermedias. Esto es lo que necesitamos hacer cuando queremos modificar directamente el DataFrame original.
Esto puede parecer indexación encadenada a primera vista porque todavía hay parámetros, pero no lo es. Lo que define cada indexación son los corchetes cuadrados []
.
Tenga en cuenta que usar loc
es seguro solo si asignamos un valor directamente, como hicimos anteriormente. Si en cambio usamos una variable temporal, caemos nuevamente en el mismo problema. Aquí hay dos ejemplos de código que no solucionan el problema:
- Usando
loc
con una variable temporal:
# Usar loc junto con una variable temporal no soluciona el problema no_property_type = df.loc[df["propertyType"] == "Not Specified"] no_property_type["propertyType"] = "House"
- Usando
loc
junto con un índice (igual que la indexación encadenada):
# Usar loc más la indexación es lo mismo que la indexación encadenada df.loc[df["propertyType"] == "Not Specified"]["propertyType"] = "House"
Ambos ejemplos tienden a confundir a las personas porque es una idea errónea común que mientras haya un loc
, estamos modificando los datos originales. Esto es incorrecto. La única forma de asegurar que el valor se está asignando al DataFrame original es asignándolo directamente usando un único loc
sin ninguna indexación separada.
Trabajando de forma segura con una copia del DataFrame original con copy()
Cuando queremos asegurarnos de que estamos operando en una copia del DataFrame, debemos usar el método .copy()
.
Supongamos que se nos pide analizar el precio por pie cuadrado de las propiedades. No queremos modificar los datos originales. El objetivo es crear un nuevo DataFrame con los resultados del análisis para enviarlo a otro equipo.
El primer paso es filtrar algunas filas y limpiar los datos. Específicamente, necesitamos:
- Eliminar las filas donde
sizeSqFeetMax
no esté definido. - Eliminar las filas donde el
price
es"POA"
(precio bajo consulta). - Convertir los precios a valores numéricos (en el dataset original, los precios son cadenas de texto con el siguiente formato:
"£25,000,000"
)
Podemos realizar los pasos anteriores utilizando el siguiente código:
# 1. Filtrar todas las propiedades sin tamaño o precio properties_with_size_and_price = df[df["sizeSqFeetMax"].notna() & (df["price"] != "POA")] # 2. Eliminar los caracteres £ y , de las columnas de precio properties_with_size_and_price["price"] = properties_with_size_and_price["price"].str.replace("£", "", regex=False).str.replace(",", "", regex=False) # 3. Convertir la columna de precio a valores numéricos properties_with_size_and_price["price"] = pd.to_numeric(properties_with_size_and_price["price"])
Para calcular el precio por pie cuadrado, creamos una nueva columna cuyos valores son el resultado de dividir la columna price
por la columna sizeSqFeetMax
:
properties_with_size_and_price["pricePerSqFt"] = properties_with_size_and_price["price"] / properties_with_size_and_price["sizeSqFeetMax"]
Si ejecutamos este código, obtenemos nuevamente el SettingWithCopyWarning
. Esto no debería ser una sorpresa porque creamos y modificamos explícitamente una variable temporal DataFrame properties_with_size_and_price
.
Dado que queremos trabajar en una copia de los datos en lugar del DataFrame original, podemos solucionar el problema asegurándonos de que properties_with_size_and_price
sea una copia fresca del DataFrame y no una vista utilizando el método .copy()
en la primera línea:
properties_with_size_and_price = df[df["sizeSqFeetMax"].notna() & (df["price"] != "POA")].copy()
Agregando nuevas columnas de forma segura
Crear nuevas columnas se comporta de la misma manera que asignar valores. Siempre que sea ambiguo si estamos trabajando con una copia o una vista, pandas generará una SettingWithCopyWarning
.
Si queremos trabajar con una copia de los datos, debemos copiarlos explícitamente utilizando el método .copy()
. Entonces, somos libres de asignar una nueva columna de la forma que deseemos. Hicimos esto cuando creamos la columna pricePerSqFt
en el ejemplo anterior.
Por otro lado, si queremos modificar el DataFrame original hay dos casos a considerar.
- Si la nueva columna abarca todas las filas, podemos modificar directamente el DataFrame original. Esto no causará una advertencia porque no estaremos seleccionando un subconjunto de filas. Por ejemplo, podríamos añadir una columna
note
para cada fila donde el tipo de casa falte:
df["notes"] = df["propertyType"].apply(lambda house_type: "Missing house type" if house_type == "Not Specified" else "")
- Si la nueva columna solo define valores para un subconjunto de filas, entonces podemos utilizar la propiedad del indexador
loc
. Por ejemplo:
df.loc[df["propertyType"] == "Not Specified", "notes"] = "Missing house type"
Ten en cuenta que en este caso, el valor en las columnas que no fueron seleccionadas será indefinido, por lo que se prefiere el primer enfoque, ya que nos permite especificar un valor para cada fila.
Error de SettingWithCopyWarning
en Pandas 3.0
Actualmente, SettingWithCopyWarning
es solo una advertencia, no un error. Nuestro código sigue ejecutándose y Pandas simplemente nos informa que debemos tener cuidado.
Según la documentación oficial de Pandas, SettingWithCopyWarning
ya no se utilizará a partir de la versión 3.0 y será reemplazado por un error por defecto, aplicando estándares de código más estrictos.
Para asegurarnos de que nuestro código siga siendo compatible con futuras versiones de pandas, se recomienda actualizarlo para que genere un error en lugar de una advertencia.
Esto se hace configurando la siguiente opción después de importar pandas:
import pandas as pd pd.options.mode.copy_on_write = True
Agregar esto al código existente se asegurará de que tratemos cada asignación ambigua en nuestro código y de que el código siga funcionando cuando actualicemos a pandas 3.0.
Conclusión
La SettingWithCopyWarning
ocurre siempre que nuestro código hace ambigua la modificación de si un valor que estamos modificando es una vista o una copia. Podemos solucionarlo siendo siempre explícitos sobre lo que queremos:
- Si queremos trabajar con una copia, debemos copiarla explícitamente usando el método
copy()
. - Si queremos modificar el DataFrame original, debemos usar la propiedad del indexador
loc
y asignar el valor directamente al acceder a los datos sin usar variables intermedias.
A pesar de no ser un error, no debemos ignorar esta advertencia porque puede provocar resultados inesperados. Además, a partir de Pandas 3.0, se convertirá en un error de forma predeterminada, por lo que debemos asegurarnos de que nuestro código esté preparado para el futuro activando la opción de Copia en Escritura en nuestro código actual mediante pd.options.mode.copy_on_write = True
. Esto garantizará que el código siga siendo funcional para las futuras versiones de Pandas.
Source:
https://www.datacamp.com/tutorial/settingwithcopywarning-pandas