SettingWithCopyWarning no Pandas: Como Corrigir Este Aviso

SettingWithCopyWarning é um aviso que o Pandas pode emitir quando fazemos uma atribuição a um DataFrame. Isso pode acontecer quando usamos atribuições encadeadas ou quando usamos um DataFrame criado a partir de uma fatia. É uma fonte comum de bugs no código do Pandas com a qual todos já nos deparamos antes. Pode ser difícil de depurar porque o aviso pode aparecer em um código que aparentemente deveria funcionar bem.

Compreender o SettingWithCopyWarning é importante porque sinaliza possíveis problemas com a manipulação de dados. Este aviso sugere que o seu código pode não estar alterando os dados conforme pretendido, o que pode resultar em consequências não intencionais e bugs obscuros difíceis de rastrear.

Neste artigo, exploraremos o SettingWithCopyWarning no pandas e como evitá-lo. Também discutiremos o futuro do Pandas e como a opção copy_on_write mudará a forma como trabalhamos com DataFrames.

Visualizações e Cópias de DataFrame

Ao selecionarmos uma fatia de um DataFrame e atribuí-la a uma variável, podemos obter uma visualização ou uma cópia fresca do DataFrame.

Com uma visualização, a memória entre ambos os DataFrames é compartilhada. Isso significa que modificar um valor de uma célula presente em ambos os DataFrames modificará ambos.

Com uma cópia, uma nova memória é alocada e um DataFrame independente com os mesmos valores do original é criado. Neste caso, ambos os DataFrames são entidades distintas, portanto, modificar um valor em um deles não afeta o outro.

O Pandas tenta evitar criar uma cópia sempre que possível para otimizar o desempenho. No entanto, é impossível prever antecipadamente se obteremos uma visualização ou uma cópia. O SettingWithCopyWarning é acionado sempre que atribuímos um valor a um DataFrame para o qual não está claro se é uma cópia ou uma visualização de outro DataFrame.

Compreender o SettingWithCopyWarning com Dados Reais

Vamos usar o conjunto de dados Kaggle Real Estate Data London 2024 para aprender como o SettingWithCopyWarning ocorre e como corrigi-lo.

Este conjunto de dados contém dados imobiliários recentes de Londres. Aqui está uma visão geral das colunas presentes no conjunto de dados:

  • addedOn: A data em que o anúncio foi adicionado.
  • title: O título do anúncio.
  • descriptionHtml: Uma descrição em HTML do anúncio.
  • propertyType: O tipo de propriedade. O valor será "Não Especificado" se o tipo não foi especificado.
  • sizeSqFeetMax: O tamanho máximo em pés quadrados.
  • bedrooms: O número de quartos.
  • listingUpdatedReason: Motivo da atualização do anúncio (por exemplo, novo anúncio, redução de preço).
  • price: O preço do anúncio em libras.

Exemplo com uma variável temporária explícita

Diga-nos que as propriedades com um tipo de propriedade não especificado são casas. Assim, queremos atualizar todas as linhas com propertyType igual a "Not Specified" para "House". Uma maneira de fazer isso é filtrar as linhas com um tipo de propriedade não especificado em uma variável temporária DataFrame e atualizar os valores da coluna propertyType assim:

import pandas as pd dataset_name = "realestate_data_london_2024_nov.csv" df = pd.read_csv(dataset_name) # Obter todas as linhas com tipo de propriedade não especificado no_property_type = df[df["propertyType"] == "Not Specified"] # Atualizar o tipo de propriedade para “Casa” nessas linhas no_property_type["propertyType"] = "House"

A execução deste código fará com que o pandas produza o 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"

O motivo para isso é que o pandas não pode saber se o DataFrame no_property_type é uma visualização ou uma cópia de df.

Isso é um problema porque o comportamento do código a seguir pode ser muito diferente dependendo se é uma visualização ou uma cópia.

Neste exemplo, nosso objetivo é modificar o DataFrame original. Isso só acontecerá se no_property_type for uma visualização. Se o restante de nosso código pressupõe que df foi modificado, pode estar errado porque não há como garantir que este seja o caso. Devido a esse comportamento incerto, o Pandas emite o aviso para nos informar sobre esse fato.

Mesmo que nosso código seja executado corretamente porque obtivemos uma visualização, poderíamos obter uma cópia em execuções subsequentes, e o código não funcionará conforme pretendido. Portanto, é importante não ignorar este aviso e garantir que nosso código sempre faça o que queremos que ele faça.

Exemplo com uma variável temporária oculta

No exemplo anterior, fica claro que uma variável temporária está sendo usada porque estamos atribuindo explicitamente parte do DataFrame a uma variável chamada no_property_type.

No entanto, em alguns casos, isso não é tão explícito. O exemplo mais comum onde ocorre o SettingWithCopyWarning é com a indexação encadeada. Suponha que substituímos as duas últimas linhas por uma única linha:

df[df["propertyType"] == "Not Specified"]["propertyType"] = "House"

À primeira vista, não parece que uma variável temporária está sendo criada. No entanto, executá-la resulta em um SettingWithCopyWarning também. 

A forma como este código é executado é:

  1. df[df["propertyType"] == "Not Specified"] é avaliado e armazenado temporariamente na memória. 
  2. O índice ["propertyType"] desse local temporário na memória é acessado. 

Os acessos aos índices são avaliados um por um e, portanto, a indexação encadeada resulta no mesmo aviso porque não sabemos se os resultados intermediários são visualizações ou cópias. O código acima é essencialmente o mesmo que fazer:

tmp = df[df["propertyType"] == "Not Specified"] tmp["propertyType"] = "House"

Este exemplo é frequentemente referido como indexação encadeada porque encadeamos acessos indexados usando []. Primeiro, acessamos [df["propertyType"] == "Not Specified"] e depois ["propertyType"].

Como Resolver o SettingWithCopyWarning

Vamos aprender como escrever nosso código de forma que não haja ambiguidade e o SettingWithCopyWarning não seja acionado. Aprendemos que o aviso surge de uma ambiguidade sobre se um DataFrame é uma visão ou uma cópia de outro DataFrame.

O modo de corrigir isso é garantir que cada DataFrame que criamos seja uma cópia se quisermos que seja uma cópia ou uma visão se quisermos que seja uma visão.

Modificar com segurança o DataFrame original com loc 

Vamos corrigir o código do exemplo acima onde queremos modificar o DataFrame original. Para evitar o uso de uma variável temporária, use a propriedade indexadora loc.

df.loc[df["propertyType"] == "Not Specified", "propertyType"] = "House"

Com este código, estamos agindo diretamente no DataFrame original df via a propriedade indexadora loc, então não há necessidade de variáveis intermediárias. Isso é o que precisamos fazer quando queremos modificar diretamente o DataFrame original.

Isso pode parecer indexação encadeada à primeira vista porque ainda há parâmetros, mas não é. O que define cada indexação são os colchetes angulares [].

Observe que usar loc é seguro apenas se atribuirmos um valor diretamente, como fizemos acima. Se, em vez disso, usarmos uma variável temporária, caímos novamente no mesmo problema. Aqui estão dois exemplos de código que não resolvem o problema:

  1. Usando loc com uma variável temporária:
# Usar loc com uma variável temporária não resolve o problema no_property_type = df.loc[df["propertyType"] == "Not Specified"] no_property_type["propertyType"] = "House"
  1. Usando loc juntamente com um índice (mesmo que indexação encadeada):
# Usar loc com indexação é o mesmo que indexação encadeada df.loc[df["propertyType"] == "Not Specified"]["propertyType"] = "House"

Ambos esses exemplos tendem a confundir as pessoas porque é uma concepção errada comum de que enquanto houver um loc, estamos modificando os dados originais. Isso é incorreto. A única maneira de garantir que o valor esteja sendo atribuído ao DataFrame original é atribuí-lo diretamente usando um único loc sem nenhuma indexação separada.

Trabalhando com segurança com uma cópia do DataFrame original com copy()

Quando queremos garantir que estamos operando em uma cópia do DataFrame, devemos usar o método .copy().

Digamos que nos pediram para analisar o preço por metro quadrado dos imóveis. Não queremos modificar os dados originais. O objetivo é criar um novo DataFrame com os resultados da análise para enviar a outra equipe.

O primeiro passo é filtrar algumas linhas e limpar os dados. Especificamente, precisamos:

  • Remova as linhas em que sizeSqFeetMax não está definido.
  • Remova as linhas em que o preço é "POA" (preço sob consulta).
  • Converta os preços para valores numéricos (no conjunto de dados original, os preços são strings no seguinte formato: "£25,000,000")

Podemos realizar os passos acima usando o seguinte código:

# 1. Filtrar todas as propriedades sem tamanho ou preço properties_with_size_and_price = df[df["sizeSqFeetMax"].notna() & (df["price"] != "POA")] # 2. Remover os caracteres £ e , das colunas de preço properties_with_size_and_price["price"] = properties_with_size_and_price["price"].str.replace("£", "", regex=False).str.replace(",", "", regex=False) # 3. Converter a coluna de preço para valores numéricos properties_with_size_and_price["price"] = pd.to_numeric(properties_with_size_and_price["price"])

Para calcular o preço por metro quadrado, criamos uma nova coluna cujos valores são o resultado da divisão da coluna price pela coluna sizeSqFeetMax:

properties_with_size_and_price["pricePerSqFt"] = properties_with_size_and_price["price"] / properties_with_size_and_price["sizeSqFeetMax"]

Se executarmos este código, obtemos o SettingWithCopyWarning novamente. Isso não deve ser uma surpresa, pois criamos e modificamos explicitamente uma variável temporária DataFrame properties_with_size_and_price.

Já que queremos trabalhar com uma cópia dos dados em vez do DataFrame original, podemos corrigir o problema garantindo que properties_with_size_and_price seja uma cópia nova do DataFrame e não uma visualização, utilizando o método .copy() na primeira linha:

properties_with_size_and_price = df[df["sizeSqFeetMax"].notna() & (df["price"] != "POA")].copy()

Adicionando novas colunas com segurança

A criação de novas colunas se comporta da mesma maneira que a atribuição de valores. Sempre que for ambíguo se estamos lidando com uma cópia ou uma visualização, o pandas emitirá um aviso SettingWithCopyWarning.

Se quisermos trabalhar com uma cópia dos dados, devemos copiá-los explicitamente usando o método .copy(). Em seguida, somos livres para atribuir uma nova coluna da maneira que desejarmos. Fizemos isso quando criamos a coluna pricePerSqFt no exemplo anterior.

Por outro lado, se quisermos modificar o DataFrame original, existem dois casos a considerar.

  1. Se a nova coluna abranger todas as linhas, podemos modificar diretamente o DataFrame original. Isso não causará um aviso porque não estaremos selecionando um subconjunto das linhas. Por exemplo, poderíamos adicionar uma coluna note para cada linha em que o tipo de casa está ausente:
df["notes"] = df["propertyType"].apply(lambda house_type: "Missing house type" if house_type == "Not Specified" else "")
  1. Se a nova coluna definir valores apenas para um subconjunto das linhas, então podemos usar a propriedade do indexador loc. Por exemplo:
df.loc[df["propertyType"] == "Not Specified", "notes"] = "Missing house type"

Observe que, neste caso, o valor nas colunas que não foram selecionadas será indefinido, portanto, a primeira abordagem é preferida, pois nos permite especificar um valor para cada linha.

SettingWithCopyWarning Erro no Pandas 3.0

Neste momento, SettingWithCopyWarning é apenas um aviso, não um erro. Nosso código ainda é executado e o Pandas simplesmente nos informa para termos cuidado.

De acordo com a documentação oficial do Pandas, SettingWithCopyWarning não será mais utilizado a partir da versão 3.0 e será substituído por um erro por padrão, aplicando padrões de código mais rigorosos.

Para garantir que nosso código permaneça compatível com futuras versões do pandas, é recomendável atualizá-lo agora para gerar um erro em vez de um aviso.

Isto é feito configurando a seguinte opção após importar o pandas:

import pandas as pd pd.options.mode.copy_on_write = True

Adicionar isso ao código existente garantirá que lidemos com cada atribuição ambígua em nosso código e garantiremos que o código ainda funcione quando atualizarmos para o pandas 3.0.

Conclusão

O SettingWithCopyWarning ocorre sempre que nosso código torna ambíguo se um valor que estamos modificando é uma visualização ou uma cópia. Podemos corrigir isso sendo sempre explícitos sobre o que queremos:

  • Se quisermos trabalhar com uma cópia, devemos explicitamente copiá-la usando o método copy().
  • Se quisermos modificar o DataFrame original, devemos usar a propriedade indexadora loc e atribuir o valor diretamente ao acessar os dados sem usar variáveis intermediárias.

Apesar de não ser um erro, não devemos ignorar este aviso, pois pode levar a resultados inesperados. Além disso, a partir do Pandas 3.0, ele se tornará um erro por padrão, portanto, devemos proteger nosso código ativando o Copy-on-Write em nosso código atual usando pd.options.mode.copy_on_write = True. Isso garantirá que o código permaneça funcional para futuras versões do Pandas.

Source:
https://www.datacamp.com/tutorial/settingwithcopywarning-pandas