¿Cómo tratar con SettingWithCopyWarning en Pandas?

Background

Acabo de actualizar mi Pandas de 0.11 a 0.13.0rc1. Ahora, la aplicación está sacando muchas advertencias nuevas. Uno de ellos como este:

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? ¿Tengo que cambiar algo?

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

La función que da 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])
Solución

El SettingWithCopyWarning fue creado para señalar asignaciones "encadenadas" potencialmente confusas, como las siguientes, que no siempre funcionan como se espera, particularmente cuando la primera selección devuelve una copia. [ver GH5390 y GH5597 para una discusión de fondo].

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

La advertencia ofrece una sugerencia para reescribir como sigue:

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

Mientras que está claro que no te importa que las escrituras vuelvan al marco original (ya que sobrescribiste la referencia a él), desafortunadamente este patrón no puede diferenciarse del primer ejemplo de asignación encadenada, de ahí la advertencia (falso positivo). El potencial de los falsos positivos se trata en los docs on indexing, si quieres leer más. Puede desactivar con seguridad esta nueva advertencia con la siguiente asignación.

pd.options.mode.chained_assignment = None  # default='warn'
Comentarios (10)

En general, el objetivo del 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 creen. Hay falsos positivos (es decir, si sabes lo que estás haciendo podría estar bien). Una posibilidad es simplemente desactivar la advertencia (por defecto advertencia) como sugiere @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, lo que desactivará efectivamente la comprobación, para ese objeto:

In [5]: dfa.is_copy = False

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

Si se copia explícitamente, no se producirá ninguna otra advertencia:

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

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

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

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

O,

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

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 pandas trata de advertirle.


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, esto es un fuerte olor.

Dado este dataframe:

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 es ahora un marco de datos independiente. Si se cambia, no cambiará df.

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

Comportamiento dos: Esto cambia el marco de datos original.


Utilizar .loc en su lugar

Los desarrolladores de pandas reconocieron que el objeto .ix era bastante apestoso [especulativamente] y por ello crearon dos nuevos objetos que ayudan en la adhesión y asignación de datos. (El otro es .iloc)

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

.loc está pensado para modificar el marco de datos existente in situ, lo que es más eficiente en cuanto a la 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 ayudarte con mucho de esto y también hacer la carga del archivo 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]]

Haz esto

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 sólo leerá las columnas que le interesan, y las nombrará correctamente. No es necesario usar el malvado objeto .ix para hacer cosas mágicas.

Comentarios (5)