Cómo tratar con SettingWithCopyWarning en Pandas

python pandas dataframe chained-assignment


Background

Acabo de actualizar mis Pandas de 0.11 a 0.13.0rc1.Ahora,la aplicación está sacando muchas nuevas advertencias.Una de ellas como esta:

E:\FinReporter\FM_EXT.py:449: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_index,col_indexer] = value instead
  quote_df['TVol']   = quote_df['TVol']/TVOL_SCALE

Quiero saber qué significa exactamente.¿Necesito cambiar algo?

¿Cómo debo suspender la advertencia si insisto en usar quote_df['TVol'] = quote_df['TVol']/TVOL_SCALE ?

La función que da los errores

def _decode_stock_quote(list_of_150_stk_str):
    """decode the webpage and return dataframe"""

    from cStringIO import StringIO

    str_of_all = "".join(list_of_150_stk_str)

    quote_df = pd.read_csv(StringIO(str_of_all), sep=',', names=list('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefg')) #dtype={'A': object, 'B': object, 'C': np.float64}
    quote_df.rename(columns={'A':'STK', 'B':'TOpen', 'C':'TPCLOSE', 'D':'TPrice', 'E':'THigh', 'F':'TLow', 'I':'TVol', 'J':'TAmt', 'e':'TDate', 'f':'TTime'}, inplace=True)
    quote_df = quote_df.ix[:,[0,3,2,1,4,5,8,9,30,31]]
    quote_df['TClose'] = quote_df['TPrice']
    quote_df['RT']     = 100 * (quote_df['TPrice']/quote_df['TPCLOSE'] - 1)
    quote_df['TVol']   = quote_df['TVol']/TVOL_SCALE
    quote_df['TAmt']   = quote_df['TAmt']/TAMT_SCALE
    quote_df['STK_ID'] = quote_df['STK'].str.slice(13,19)
    quote_df['STK_Name'] = quote_df['STK'].str.slice(21,30)#.decode('gb2312')
    quote_df['TDate']  = quote_df.TDate.map(lambda x: x[0:4]+x[5:7]+x[8:10])

    return quote_df

Más mensajes de error

E:\FinReporter\FM_EXT.py:449: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_index,col_indexer] = value instead
  quote_df['TVol']   = quote_df['TVol']/TVOL_SCALE
E:\FinReporter\FM_EXT.py:450: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_index,col_indexer] = value instead
  quote_df['TAmt']   = quote_df['TAmt']/TAMT_SCALE
E:\FinReporter\FM_EXT.py:453: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_index,col_indexer] = value instead
  quote_df['TDate']  = quote_df.TDate.map(lambda x: x[0:4]+x[5:7]+x[8:10])



Answer 1 Garrett


El SettingWithCopyWarning fue creado para marcar potencialmente confuso asignaciones "encadenados", tales como los siguientes, que no siempre funciona como se esperaba, sobre todo cuando la primera selección devuelve una copia . [Ver GH5390 y GH5597 para una discusión de antecedentes.]

df[df['A'] > 2]['B'] = new_val  # new_val not set in df

La advertencia ofrece una sugerencia para reescribir de la siguiente manera:

df.loc[df['A'] > 2, 'B'] = new_val

Sin embargo,esto no se ajusta a su uso,que es equivalente a:

df = df[df['A'] > 2]
df['B'] = new_val

Si bien está claro que no le importa que las escrituras vuelvan al marco original (ya que está sobrescribiendo la referencia), desafortunadamente este patrón no puede diferenciarse del primer ejemplo de asignación encadenada. De ahí la advertencia (falso positivo). El potencial de falsos positivos se aborda en los documentos sobre indexación , si desea leer más. Puede deshabilitar esta nueva advertencia de forma segura con la siguiente asignación.

import pandas as pd
pd.options.mode.chained_assignment = None  # default='warn'



Answer 2 cs95


¿Cómo lidiar con SettingWithCopyWarning en Pandas?

Este post está dirigido a los lectores que,

  1. Me gustaría entender lo que significa esta advertencia
  2. Le gustaría entender las diferentes formas de suprimir esta advertencia
  3. Le gustaría entender cómo mejorar su código y seguir las buenas prácticas para evitar esta advertencia en el futuro.

Setup

np.random.seed(0)
df = pd.DataFrame(np.random.choice(10, (3, 5)), columns=list('ABCDE'))
df
   A  B  C  D  E
0  5  0  3  3  7
1  9  3  5  2  4
2  7  6  8  8  1

¿Qué es el SettingWithCopyWarning ?

Para saber cómo tratar esta advertencia,es importante entender lo que significa y por qué se plantea en primer lugar.

Al filtrar DataFrames, es posible dividir / indexar un marco para devolver una vista o una copia , dependiendo del diseño interno y varios detalles de implementación. Una "vista" es, como sugiere el término, una vista de los datos originales, por lo que modificar la vista puede modificar el objeto original. Por otro lado, una "copia" es una replicación de datos del original, y la modificación de la copia no tiene ningún efecto sobre el original.

Como se menciona en otras respuestas, el SettingWithCopyWarning se creó para marcar las operaciones de "asignación encadenada". Considere df en la configuración anterior. Suponga que desea seleccionar todos los valores en la columna "B" donde los valores en la columna "A" son> 5. Pandas le permite hacer esto de diferentes maneras, algunas más correctas que otras. Por ejemplo,

df[df.A > 5]['B']

1    3
2    6
Name: B, dtype: int64

And,

df.loc[df.A > 5, 'B']

1    3
2    6
Name: B, dtype: int64

Estos devuelven el mismo resultado, por lo que si solo está leyendo estos valores, no hay diferencia. Entonces, ¿cuál es el problema? El problema con la asignación encadenada es que, en general, es difícil predecir si se devuelve una vista o una copia, por lo que esto se convierte en un problema en gran medida cuando intenta asignar valores de nuevo. Para construir sobre el ejemplo anterior, considere cómo el intérprete ejecuta este código:

df.loc[df.A > 5, 'B'] = 4
# becomes
df.__setitem__((df.A > 5, 'B'), 4)

Con una sola llamada __setitem__ a df . OTOH, considere este código:

df[df.A > 5]['B'] = 4
# becomes
df.__getitem__(df.A > 5).__setitem__('B", 4)

Ahora, dependiendo de si __getitem__ devolvió una vista o una copia, la operación __setitem__ puede no funcionar .

En general, debe usar loc para la asignación basada en etiquetas e iloc para la asignación basada en enteros / posiciones, ya que la especificación garantiza que siempre operan en el original. Además, para configurar una sola celda, debe usar at e iat .

Más se puede encontrar en la documentación .

Note
Todas las operaciones de indexación booleana hechas con loc también se pueden hacer con iloc . La única diferencia es que iloc espera números enteros / posiciones para el índice o una matriz numpy de valores booleanos, e índices enteros / posiciones para las columnas.

Por ejemplo,

df.loc[df.A > 5, 'B'] = 4

Puede escribirse nas

df.iloc[(df.A > 5).values, 1] = 4

And,

df.loc[1, 'A'] = 100

Puede escribirse como

df.iloc[1, 0] = 100

Y así sucesivamente.


¡Sólo dime cómo suprimir la advertencia!

Considere una operación simple en la columna "A" de df . Al seleccionar "A" y dividir por 2, aparecerá la advertencia, pero la operación funcionará.

df2 = df[['A']]
df2['A'] /= 2
/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/IPython/__main__.py:1: 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

df2
     A
0  2.5
1  4.5
2  3.5

Hay un par de formas de silenciar directamente esta advertencia:

  1. Hacer una deepcopy

    df2 = df[['A']].copy(deep=True)
    df2['A'] /= 2
  2. Cambiar pd.options.mode.chained_assignment
    Se puede establecer en None , "warn" o "raise" . "warn" es el valor predeterminado. None suprimirá la advertencia por completo, y "raise" arrojará un SettingWithCopyError , evitando que la operación se realice .

    pd.options.mode.chained_assignment = None
    df2['A'] /= 2

@Peter Cotton en los comentarios, ideó una buena manera de cambiar de manera no intrusiva el modo (modificado desde esta esencia ) usando un administrador de contexto, para configurar el modo solo el tiempo que sea necesario, y restablecerlo de nuevo al estado original cuando termine.

class ChainedAssignent:
    def __init__(self, chained=None):
        acceptable = [None, 'warn', 'raise']
        assert chained in acceptable, "chained must be in " + str(acceptable)
        self.swcw = chained

    def __enter__(self):
        self.saved_swcw = pd.options.mode.chained_assignment
        pd.options.mode.chained_assignment = self.swcw
        return self

    def __exit__(self, *args):
        pd.options.mode.chained_assignment = self.saved_swcw

El uso es el siguiente:

# some code here
with ChainedAssignent():
    df2['A'] /= 2
# more code follows

O,para plantear la excepción

with ChainedAssignent(chained='raise'):
    df2['A'] /= 2

SettingWithCopyError: 
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

El "Problema XY":¿Qué estoy haciendo mal?

La mayoría de las veces, los usuarios intentan buscar formas de suprimir esta excepción sin comprender completamente por qué se planteó en primer lugar. Este es un buen ejemplo de un problema XY , donde los usuarios intentan resolver un problema "Y" que en realidad es un síntoma de un problema más profundo "X". Se formularán preguntas basadas en problemas comunes que se encuentran con esta advertencia, y luego se presentarán soluciones.

Pregunta 1
Tengo un DataFrame

df
       A  B  C  D  E
    0  5  0  3  3  7
    1  9  3  5  2  4
    2  7  6  8  8  1

Quiero asignar valores en la columna "A"> 5 a 1000. Mi salida esperada es

      A  B  C  D  E
0     5  0  3  3  7
1  1000  3  5  2  4
2  1000  6  8  8  1

La manera incorrecta de hacer esto:

df.A[df.A > 5] = 1000         # works, because df.A returns a view
df[df.A > 5]['A'] = 1000      # does not work
df.loc[df.A  5]['A'] = 1000   # does not work

Manera correcta usando loc :

df.loc[df.A > 5, 'A'] = 1000


Pregunta 2 1
Estoy tratando de fijar el valor en la celda (1,'D')a 12345.Mi resultado esperado es

   A  B  C      D  E
0  5  0  3      3  7
1  9  3  5  12345  4
2  7  6  8      8  1

He intentado diferentes formas de acceder a esta celda, como df['D'][1] . ¿Cuál es la mejor manera de hacer esto?

1. Esta pregunta no está específicamente relacionada con la advertencia, pero es bueno entender cómo hacer esta operación en particular correctamente para evitar situaciones en las que la advertencia podría surgir en el futuro.

Para ello,puede utilizar cualquiera de los siguientes métodos.

df.loc[1, 'D'] = 12345
df.iloc[1, 3] = 12345
df.at[1, 'D'] = 12345
df.iat[1, 3] = 12345


Pregunta 3
Estoy tratando de subconjuntar valores basados en alguna condición.Tengo un DataFrame

   A  B  C  D  E
1  9  3  5  2  4
2  7  6  8  8  1

Me gustaría asignar los valores de "D" a 123 de tal manera que "C" ==5.Intenté

df2.loc[df2.C == 5, 'D'] = 123

Lo que parece estar bien, ¡pero sigo obteniendo SettingWithCopyWarning ! ¿Cómo puedo solucionar esto?

En realidad, esto probablemente se deba a un código más arriba en su tubería. ¿ df2 partir de algo más grande, como

df2 = df[df.A > 5]

? En este caso, la indexación booleana devolverá una vista, por lo que df2 hará referencia al original. Lo que debe hacer es asignar df2 a una copia :

df2 = df[df.A > 5].copy()
# Or,
# df2 = df.loc[df.A > 5, :]


Pregunta 4
Estoy tratando de dejar caer la columna "C" en el lugar de

   A  B  C  D  E
1  9  3  5  2  4
2  7  6  8  8  1

Pero usando

df2.drop('C', axis=1, inplace=True)

Lanza SettingWithCopyWarning . ¿Por qué está pasando esto?

Esto se debe a que df2 debe haberse creado como una vista desde alguna otra operación de corte, como

df2 = df[df.A > 5]

La solución aquí es hacer una copy() de df o usar loc , como antes.




Answer 3 Jeff


En general, el objetivo de SettingWithCopyWarning es mostrar a los usuarios (y especialmente a los nuevos usuarios) que pueden estar operando en una copia y no en el original como piensan. No son falsos positivos (OIA si usted sabe lo que está haciendo podría ser aceptable ). Una posibilidad es simplemente apagar la (por defecto advertir advertencia) como sugieren @Garrett.

Aquí hay otra opción:

In [1]: df = DataFrame(np.random.randn(5, 2), columns=list('AB'))

In [2]: dfa = df.ix[:, [1, 0]]

In [3]: dfa.is_copy
Out[3]: True

In [4]: dfa['A'] /= 2
/usr/local/bin/ipython:1: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_index,col_indexer] = value instead
  #!/usr/local/bin/python

Puede establecer el indicador is_copy en False , que desactivará efectivamente la comprobación para ese objeto :

In [5]: dfa.is_copy = False

In [6]: dfa['A'] /= 2

Si usted explícitamente copia,entonces no habrá más advertencias:

In [7]: dfa = df.ix[:, [1, 0]].copy()

In [8]: dfa['A'] /= 2

El código que el OP muestra arriba, aunque es legítimo, y probablemente algo que yo también hago, es técnicamente un caso para esta advertencia, y no un falso positivo. Otra forma de no tener la advertencia sería realizar la operación de selección a través de reindex ar , p. Ej.

quote_df = quote_df.reindex(columns=['STK', ...])

Or,

quote_df = quote_df.reindex(['STK', ...], axis=1)  # v.0.21



Answer 4 firelynx


Advertencia de copia del marco de datos de Pandas

Cuando vas y haces algo como esto:

quote_df = quote_df.ix[:,[0,3,2,1,4,5,8,9,30,31]]

pandas.ix en este caso devuelve un nuevo marco de datos independiente.

Cualquier valor que decida cambiar en este marco de datos,no cambiará el marco de datos original.

Esto es lo que los pandas tratan de advertirte.


¿Por qué .ix es una mala idea?

El objeto .ix intenta hacer más de una cosa, y para cualquiera que haya leído algo sobre código limpio, este es un olor fuerte.

Dado este marco de datos:

df = pd.DataFrame({"a": [1,2,3,4], "b": [1,1,2,2]})

Dos comportamientos:

dfcopy = df.ix[:,["a"]]
dfcopy.a.ix[0] = 2

Comportamiento uno: dfcopy ahora es un marco de datos independiente. Cambiarlo no cambiará df

df.ix[0, "a"] = 3

Comportamiento dos:Esto cambia el marco de datos original.


Use .loc en su lugar

Los desarrolladores de pandas reconocieron que el objeto .ix era bastante maloliente [especulativamente] y, por lo tanto, crearon dos nuevos objetos que ayudan en el acceso y la asignación de datos. (El otro ser .iloc )

.loc es más rápido porque no intenta crear una copia de los datos.

.loc está destinado a modificar su marco de datos existente en el lugar, que es más eficiente en memoria.

.loc es predecible, tiene un comportamiento.


La solución

Lo que estás haciendo en tu ejemplo de código es cargar un archivo grande con muchas columnas,y luego modificarlo para que sea más pequeño.

La función pd.read_csv puede ayudarlo con mucho de esto y también hace que la carga del archivo sea mucho más rápida.

Así que en lugar de hacer esto

quote_df = pd.read_csv(StringIO(str_of_all), sep=',', names=list('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefg')) #dtype={'A': object, 'B': object, 'C': np.float64}
quote_df.rename(columns={'A':'STK', 'B':'TOpen', 'C':'TPCLOSE', 'D':'TPrice', 'E':'THigh', 'F':'TLow', 'I':'TVol', 'J':'TAmt', 'e':'TDate', 'f':'TTime'}, inplace=True)
quote_df = quote_df.ix[:,[0,3,2,1,4,5,8,9,30,31]]

Hazlo.

columns = ['STK', 'TPrice', 'TPCLOSE', 'TOpen', 'THigh', 'TLow', 'TVol', 'TAmt', 'TDate', 'TTime']
df = pd.read_csv(StringIO(str_of_all), sep=',', usecols=[0,3,2,1,4,5,8,9,30,31])
df.columns = columns

Esto solo leerá las columnas que le interesen y las nombrará correctamente. No es necesario usar el malvado objeto .ix para hacer cosas mágicas.




Answer 5 user443854


Aquí respondo a la pregunta directamente.¿Cómo se trata?

Haga un .copy(deep=False) después de cortar. Ver pandas.DataFrame.copy .

Espera,¿una rebanada no devuelve una copia? Después de todo,¿esto es lo que el mensaje de advertencia intenta decir? Lee la respuesta larga:

import pandas as pd
df = pd.DataFrame({'x':[1,2,3]})

Esto da una advertencia:

df0 = df[df.x>2]
df0['foo'] = 'bar'

Esto no:

df1 = df[df.x>2].copy(deep=False)
df1['foo'] = 'bar'

Tanto df0 como df1 son objetos de DataFrame , pero algo en ellos es diferente y permite a los pandas imprimir la advertencia. Averigüemos qué es.

import inspect
slice= df[df.x>2]
slice_copy = df[df.x>2].copy(deep=False)
inspect.getmembers(slice)
inspect.getmembers(slice_copy)

Usando la herramienta de diferenciación de su elección,verá que más allá de un par de direcciones,la única diferencia material es esta:

|          | slice   | slice_copy |
| _is_copy | weakref | None       |

El método que decide si se debe advertir es DataFrame._check_setitem_copy que verifica _is_copy . Así que aquí tienes. Haga una copy para que su DataFrame no sea _is_copy .

La advertencia sugiere usar .loc , pero si usa .loc en un marco que _is_copy , seguirá recibiendo la misma advertencia. ¿Engañoso? Si. ¿Molesto? Usted apuesta. ¿Servicial? Potencialmente, cuando se utiliza la asignación encadenada. Pero no puede detectar correctamente la asignación de la cadena e imprime la advertencia indiscriminadamente.