SettingWithCopyWarning in Pandas: Come risolvere questo avvertimento

SettingWithCopyWarning è un avvertimento che Pandas può generare quando facciamo un’assegnazione a un DataFrame. Ciò può accadere quando utilizziamo assegnazioni concatenate o quando utilizziamo un DataFrame creato da una fetta. È una fonte comune di bug nel codice Pandas che tutti abbiamo affrontato in passato. Può essere difficile da individuare perché l’avvertimento può comparire in codice che sembra dovrebbe funzionare correttamente.

Comprendere il SettingWithCopyWarning è importante perché segnala potenziali problemi con la manipolazione dei dati. Questo avvertimento suggerisce che il tuo codice potrebbe non modificare i dati come previsto, il che può comportare conseguenze indesiderate e bug oscuri difficili da rintracciare.

In questo articolo, esploreremo il SettingWithCopyWarning in pandas e come evitarlo. Discuteremo anche il futuro di Pandas e come l’opzione copy_on_write cambierà il modo in cui lavoriamo con i DataFrames.

Visualizzazioni e Copie dei DataFrame

Quando selezioniamo una fetta di un DataFrame e la assegniamo a una variabile, possiamo ottenere una visualizzazione o una copia fresca del DataFrame.

Con una visualizzazione, la memoria tra entrambi i DataFrames è condivisa. Ciò significa che modificare un valore da una cella presente in entrambi i DataFrames modificherà entrambi.

Con una copia, viene allocata nuova memoria e viene creato un DataFrame indipendente con gli stessi valori del DataFrame originale. In questo caso, entrambi i DataFrames sono entità distinte, quindi modificare un valore in uno di essi non influirà sull’altro.

Pandas cerca di evitare di creare una copia quando può per ottimizzare le prestazioni. Tuttavia, è impossibile prevedere in anticipo se otterremo una vista o una copia. Viene generato il SettingWithCopyWarning ogni volta che assegniamo un valore a un DataFrame per il quale non è chiaro se si tratta di una copia o di una vista da un altro DataFrame.

Comprensione del SettingWithCopyWarning con dati reali

Utilizzeremo il dataset Kaggle Questi Dati Immobiliari Londra 2024 per imparare come si verifica il SettingWithCopyWarning e come risolverlo.

Questo dataset contiene dati immobiliari recenti di Londra. Ecco una panoramica delle colonne presenti nel dataset:

  • addedOn: La data in cui è stata aggiunta la quotazione.
  • title: Il titolo della quotazione.
  • descriptionHtml: Una descrizione HTML della quotazione.
  • propertyType: Il tipo di proprietà. Il valore sarà "Non specificato" se il tipo non è stato specificato.
  • sizeSqFeetMax: La dimensione massima in piedi quadrati.
  • bedrooms: Il numero di camere da letto.
  • listingUpdatedReason: Motivo dell’aggiornamento della quotazione (ad esempio, nuova quotazione, riduzione del prezzo).
  • price: Il prezzo della quotazione in sterline.

Esempio con una variabile temporanea esplicita

Diciamo che ci viene detto che le proprietà con un tipo di proprietà non specificato sono case. Vogliamo quindi aggiornare tutte le righe con propertyType uguale a "Non specificato" a "Casa". Un modo per farlo è filtrare le righe con un tipo di proprietà non specificato in una variabile DataFrame temporanea e aggiornare i valori della colonna propertyType</code in questo modo:

import pandas as pd dataset_name = "realestate_data_london_2024_nov.csv" df = pd.read_csv(dataset_name) # Ottenere tutte le righe con tipo di proprietà non specificato no_property_type = df[df["propertyType"] == "Not Specified"] # Aggiornare il tipo di proprietà a "Casa" su quelle righe no_property_type["propertyType"] = "House"

Eseguendo questo codice farà sì che pandas produca il 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"

Il motivo di ciò è che pandas non può sapere se il DataFrame no_property_type è una vista o una copia di df

Questo è un problema perché il comportamento del codice seguente può essere molto diverso a seconda che si tratti di una vista o di una copia. 

In questo esempio, il nostro obiettivo è modificare il DataFrame originale. Ciò accadrà solo se no_property_type è una vista. Se il resto del nostro codice assume che df sia stato modificato, potrebbe essere sbagliato perché non c’è modo di garantire che ciò sia vero. A causa di questo comportamento incerto, Pandas lancia l’avviso per informarci di questo fatto.

Anche se il nostro codice viene eseguito correttamente perché abbiamo ottenuto una vista, potremmo ottenere una copia nelle esecuzioni successive e il codice non funzionerà come previsto. Pertanto, è importante non ignorare questo avviso e assicurarsi che il nostro codice faccia sempre ciò che vogliamo che faccia.

Esempio con una variabile temporanea nascosta

Nell’esempio precedente, è chiaro che viene utilizzata una variabile temporanea perché stiamo assegnando esplicitamente una parte del DataFrame a una variabile chiamata no_property_type.

Tuttavia, in alcuni casi, questo non è così esplicito. L’esempio più comune in cui si verifica il SettingWithCopyWarning è con l’indicizzazione concatenata. Supponiamo che sostituiamo le ultime due righe con una singola riga:

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

A prima vista, non sembra che venga creata una variabile temporanea. Tuttavia, eseguendola si ottiene un SettingWithCopyWarning anche in questo caso. 

Il modo in cui viene eseguito questo codice è:

  1. df[df["propertyType"] == "Not Specified"] viene valutato e temporaneamente memorizzato in memoria. 
  2. Viene quindi accesso l’indice ["propertyType"] di quella posizione di memoria temporanea. 

Gli accessi agli indici vengono valutati uno per uno e, quindi, l’indicizzazione concatenata porta allo stesso avviso perché non sappiamo se i risultati intermedi sono visualizzazioni o copie. Il codice sopra è essenzialmente lo stesso di fare:

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

Questo esempio è spesso chiamato indicizzazione concatenata perché concateniamo gli accessi indicizzati usando []. Prima accediamo a [df["propertyType"] == "Not Specified"] e poi a ["propertyType"].

Come risolvere il SettingWithCopyWarning

Impariamo come scrivere il nostro codice in modo che non ci sia ambiguità e il SettingWithCopyWarning non venga attivato. Abbiamo appreso che l’avvertimento nasce da un’ambiguità su se un DataFrame è una vista o una copia di un altro DataFrame.

Il modo per risolvere è assicurarsi che ogni DataFrame che creiamo sia una copia se vogliamo che lo sia o una vista se vogliamo che lo sia.

Modifica in modo sicuro il DataFrame originale con loc

Correggiamo il codice dell’esempio precedente in cui vogliamo modificare il DataFrame originale. Per evitare di utilizzare una variabile temporanea, utilizzare la proprietà indicizzatore loc.

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

Con questo codice, stiamo agendo direttamente sul DataFrame originale df tramite la proprietà indicizzatore loc, quindi non c’è bisogno di variabili intermedie. Questo è ciò che dobbiamo fare quando vogliamo modificare direttamente il DataFrame originale.

Questo potrebbe sembrare un’indicizzazione concatenata a prima vista perché ci sono ancora parametri, ma non lo è. Ciò che definisce ciascuna indicizzazione sono le parentesi quadre [].

Si noti che utilizzare loc è sicuro solo se assegniamo direttamente un valore, come abbiamo fatto sopra. Se invece usiamo una variabile temporanea, cadremo nuovamente nello stesso problema. Ecco due esempi di codice che non risolvono il problema:

  1. Utilizzo di loc con una variabile temporanea:
# Utilizzare loc insieme a una variabile temporanea non risolve il problema no_property_type = df.loc[df["propertyType"] == "Not Specified"] no_property_type["propertyType"] = "House"
  1. Utilizzo di loc insieme a un indice (come l’indicizzazione concatenata):
# Utilizzare loc insieme all'indicizzazione è equivalente all'indicizzazione concatenata df.loc[df["propertyType"] == "Not Specified"]["propertyType"] = "House"

Entrambi questi esempi tendono a confondere le persone perché è diffusa la convinzione erronea che, con loc, si stia modificando i dati originali. Questo è sbagliato. L’unico modo per garantire che il valore venga assegnato al DataFrame originale è assegnarlo direttamente utilizzando un singolo loc senza alcuna indicizzazione separata.

Lavorare in modo sicuro con una copia del DataFrame originale con copy()

Quando vogliamo assicurarci di operare su una copia del DataFrame, dovremmo utilizzare il metodo .copy().

Supponiamo ci venga chiesto di analizzare il prezzo al metro quadrato delle proprietà. Non vogliamo modificare i dati originali. L’obiettivo è creare un nuovo DataFrame con i risultati dell’analisi da inviare a un’altra squadra.

Il primo passo è filtrare alcune righe e pulire i dati. In particolare, dobbiamo:

  • Rimuovere le righe in cui sizeSqFeetMax non è definito.
  • Rimuovere le righe in cui il prezzo è "POA" (prezzo su richiesta).
  • Convertire i prezzi in valori numerici (nel dataset originale, i prezzi sono stringhe con il seguente formato: "£25,000,000")

Possiamo eseguire i passaggi sopra usando il seguente codice:

# 1. Filtrare tutte le proprietà prive di dimensioni o prezzo properties_with_size_and_price = df[df["sizeSqFeetMax"].notna() & (df["price"] != "POA")] # 2. Rimuovere i caratteri £ e , dalle colonne dei prezzi properties_with_size_and_price["price"] = properties_with_size_and_price["price"].str.replace("£", "", regex=False).str.replace(",", "", regex=False) # 3. Convertire la colonna dei prezzi in valori numerici properties_with_size_and_price["price"] = pd.to_numeric(properties_with_size_and_price["price"])

Per calcolare il prezzo per piede quadrato, creiamo una nuova colonna i cui valori sono il risultato della divisione della colonna prezzo per la colonna sizeSqFeetMax:

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

Se eseguiamo questo codice, otteniamo di nuovo il SettingWithCopyWarning. Questo non dovrebbe sorprenderci perché abbiamo creato esplicitamente e modificato una variabile DataFrame temporanea properties_with_size_and_price.

Dato che vogliamo lavorare su una copia dei dati piuttosto che sul DataFrame originale, possiamo risolvere il problema assicurandoci che properties_with_size_and_price sia una copia fresca del DataFrame e non una vista utilizzando il metodo .copy() sulla prima riga:

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

Aggiunta sicura di nuove colonne

Creare nuove colonne comporta allo stesso modo dell’assegnazione di valori. Quando non è chiaro se stiamo lavorando su una copia o una vista, pandas solleverà un SettingWithCopyWarning.

Se vogliamo lavorare con una copia dei dati, dovremmo copiarli esplicitamente usando il metodo .copy(). Quindi, siamo liberi di assegnare una nuova colonna nel modo che desideriamo. Abbiamo fatto questo quando abbiamo creato la colonna pricePerSqFt nell’esempio precedente.

D’altra parte, se vogliamo modificare il DataFrame originale ci sono due casi da considerare.

  1. Se la nuova colonna copre ogni riga, possiamo modificare direttamente il DataFrame originale. Questo non causerà un avviso perché non selezioneremo un sottoinsieme delle righe. Ad esempio, potremmo aggiungere una colonna note per ogni riga in cui il tipo di casa manca:
df["notes"] = df["propertyType"].apply(lambda house_type: "Missing house type" if house_type == "Not Specified" else "")
  1. Se la nuova colonna definisce solo valori per un sottoinsieme delle righe, allora possiamo usare la proprietà indicizzatore loc. Ad esempio:
df.loc[df["propertyType"] == "Not Specified", "notes"] = "Missing house type"

Si noti che in questo caso, il valore delle colonne che non sono state selezionate sarà non definito, quindi il primo approccio è preferito poiché ci consente di specificare un valore per ogni riga.

SettingWithCopyWarning Errore in Pandas 3.0

Attualmente, SettingWithCopyWarning è solo un avviso, non un errore. Il nostro codice viene comunque eseguito e Pandas ci informa semplicemente di fare attenzione.

Secondo la documentazione ufficiale di Pandas, SettingWithCopyWarning non sarà più utilizzato a partire dalla versione 3.0 e sarà sostituito da un errore per impostazione predefinita, imponendo standard di codice più rigorosi.

Per assicurarsi che il nostro codice rimanga compatibile con le future versioni di pandas, è consigliabile aggiornarlo già ora per generare un errore invece di un avviso.

Questo viene fatto impostando l’opzione seguente dopo aver importato pandas:

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

Aggiungendo questo al codice esistente ci assicuriamo di gestire ogni assegnazione ambigua nel nostro codice e garantiamo che il codice funzioni ancora quando passiamo a pandas 3.0.

Conclusione

Il SettingWithCopyWarning si verifica ogni volta che nel nostro codice diventa ambiguo se il valore che stiamo modificando è una vista o una copia. Possiamo risolverlo essendo sempre espliciti su ciò che vogliamo:

  • Se vogliamo lavorare con una copia, dovremmo copiarla esplicitamente utilizzando il metodo copy().
  • Se vogliamo modificare il DataFrame originale, dovremmo utilizzare la proprietà indicizzatore loc e assegnare il valore direttamente quando accediamo ai dati senza utilizzare variabili intermedie.

Nonostante non sia un errore, non dovremmo ignorare questo avvertimento poiché potrebbe portare a risultati inaspettati. Inoltre, a partire da Pandas 3.0, diventerà un errore predefinito, quindi dovremmo rendere il nostro codice pronto per il futuro attivando la modalità Copy-on-Write nel nostro codice attuale utilizzando pd.options.mode.copy_on_write = True. Questo garantirà che il codice rimanga funzionale per le future versioni di Pandas.

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