¿Cómo lidiar con SettingWithCopyWarning en Pandas?

630

Antecedentes

Acabo de actualizar mis Pandas de 0.11 a 0.13.0rc1. Ahora, la aplicación está apareciendo muchas advertencias nuevas. Uno de ellos así:

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 usarla 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])
bicho grande
fuente
2
Aquí hay un administrador de contexto para establecer temporalmente el nivel de advertencia gist.github.com/notbanker/2be3ed34539c86e22ffdd88fd95ad8bc
Peter Cotton
2
puedes usar df.set_valuedocumentos aquí - pandas.pydata.org/pandas-docs/stable/generated/…
leonprou
1
pandas.pydata.org/pandas-docs/stable/… documento oficial explica en detalle
wyx
3
@leonprou df.set_valueha quedado en desuso. Pandas ahora recomienda usar .at[]o en su .iat[]lugar. documentos aquí pandas.pydata.org/pandas-docs/stable/generated/…
Kyle C
Me sorprende que nadie haya mencionado los pandas option_contextaquí: pandas.pydata.org/pandas-docs/stable/user_guide/options.html , use comowith pd.option_context("mode.chained_assignment", None): [...]
m-dz

Respuestas:

795

Se SettingWithCopyWarningcreó para marcar asignaciones "encadenadas" potencialmente confusas, como las siguientes, que no siempre funcionan como se esperaba, particularmente 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, lo que equivale 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'
Garrett
fuente
34
Creo que en su mayoría estaría a favor de no advertir sobre esto en absoluto. Si trabaja con la sintaxis de asignación encadenada, definitivamente puede determinar el orden de indexación que debe suceder para que funcione como se espera en cualquier situación dada. Creo que es demasiado paranoico que existan estas precauciones exhaustivas al respecto. En el mismo espíritu que "dejar que todos sean adultos" sobre los métodos o atributos de clase "privados", creo que es mejor que los pandas permitan que los usuarios sean adultos sobre tareas encadenadas. Úselos solo si sabe lo que está haciendo.
ely
48
Es un poco atípico tratar de advertir a la gente cuando buscan alternativas. Los métodos de acceso Pandas de estilo más nuevo (mejorados .ix, mejorados .iloc, etc.) definitivamente pueden ser vistos como "la forma principal" sin advertir a todos incesantemente sobre otras formas. En cambio, déjenlos ser adultos y si quieren hacer una tarea encadenada, que así sea. Mis dos centavos de todos modos. Uno ve comentarios descontentos de los desarrolladores de Pandas aquí a menudo cuando las asignaciones encadenadas funcionarán para resolver un problema, pero no se considerarían la forma "principal" de hacerlo.
ely
8
@EMS el problema es que no siempre está claro en el código dónde se realiza una copia frente a una vista y surgen varios errores / confusión a causa de este problema. Estábamos considerando poner un archivo / opciones rc para hacer la configuración automáticamente, lo que podría ser más útil dado cómo funciona la configuración con advertencia de copia.
Jeff Tratner
3
La razón para advertir es, por supuesto, que las personas actualicen el código antiguo. Y definitivamente necesito una advertencia, porque estoy lidiando con un código antiguo muy feo.
Thomas Andrews el
16
En una nota al margen, descubrí que deshabilitar la advertencia chained_assignment: pd.options.mode.chained_assignment = Noneha resultado en que mi código se ejecute aproximadamente 6 veces más rápido. ¿Alguien más experimentó resultados similares?
Muon
209

¿Cómo lidiar con los SettingWithCopyWarningpandas?

Esta publicación está destinada a lectores que,

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

Preparar

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

¿Cuál es el SettingWithCopyWarning?

Para saber cómo lidiar con esta advertencia, es importante comprender 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, SettingWithCopyWarningse creó para marcar las operaciones de "asignación encadenada". Considere dfen 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

Y,

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 __setitem__llamada 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 __setitem__operación puede no funcionar .

En general, debe usarlo locpara la asignación basada en etiquetas y ilocpara 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 aty iat.

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

Nota
Todas las operaciones de indexación booleana realizadas con loctambién se pueden realizar con iloc. La única diferencia es que ilocespera 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

Se puede escribir nas

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

Y,

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

Se puede escribir como

df.iloc[1, 0] = 100

Y así.


¡Solo dime cómo suprimir la advertencia!

Considere una operación simple en la columna "A" de df. Seleccionar "A" y dividir entre 2 generará 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. Cambiopd.options.mode.chained_assignment
    Puede ajustarse a None, "warn"o "raise". "warn"es el predeterminado Nonesuprimirá 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 forma agradable de cambiar el modo de manera no intrusiva (modificado desde esta esencia ) usando un administrador de contexto, para configurar el modo solo el tiempo que sea necesario y restablecerlo de nuevo a 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

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 establecer el valor en la celda (1, 'D') en 12345. Mi salida esperada 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.

Puede usar cualquiera de los siguientes métodos para hacer esto.

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 subdividir valores en función de 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 valores en "D" a 123 de modo que "C" == 5. Intenté

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

Lo que parece estar bien, ¡pero todavía estoy recibiendo el SettingWithCopyWarning! ¿Cómo puedo solucionar esto?

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

df2 = df[df.A > 5]

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

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


Pregunta 4
Estoy tratando de colocar 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 df2debe a que 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 un uso copy()de df, o usar loc, como antes.

cs95
fuente
77
PD: Avíseme si su situación no está cubierta por la lista de preguntas de la sección 3. Enmendaré mi publicación.
cs95
150

En general, el objetivo SettingWithCopyWarninges 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 is_copyindicador en False, que desactivará efectivamente la comprobación, para ese objeto :

In [5]: dfa.is_copy = False

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

Si copia explícitamente, no se producirán 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, p. Ej.

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

O,

quote_df = quote_df.reindex(['STK', ...], axis=1)  # v.0.21
Jeff
fuente
Gracias por la información y la discusión, solo apago la advertencia para dejar que la consola silencie esto. Suena como la vista y la tabla en la base de datos SQL. Necesito saber más sobre el beneficio de la introducción del concepto de 'copia', pero en mi humilde opinión es un tanto una carga de cuidar de lo sutil semántica, la sintaxis diferencia ..
bigbug
19
Estoy de acuerdo con la copia (); está claro y solucionó mi problema (que era un falso positivo).
rdchambers
55
Después de la actualización 0.16, veo muchos más falsos positivos, el problema con los falsos positivos es que uno aprende a ignorarlo, aunque a veces es legítimo.
guiones
3
@dashesy te estás perdiendo el punto. a veces tal vez incluso la mayoría de las veces podría funcionar. Pero puede suceder, por ejemplo, si el marco es más grande / más pequeño o si agrega una columna, por ejemplo, de un tipo diferente que no funciona. Ese es el punto. Está haciendo algo que puede funcionar pero que no está garantizado. Esto es muy diferente de las advertencias de desaprobación. Si quieres seguir usándolo y funciona, genial. Pero se prevenido.
Jeff
3
@Jeff tiene sentido ahora, por lo que es un undefinedcomportamiento. En todo caso, debería arrojar un error (para evitar errores vistos C), ya que apiestá congelado, el comportamiento actual de advertencia tiene sentido para la compatibilidad con versiones anteriores. Y los haré lanzar para atraparlos como errores en mi código de producción ( warnings.filterwarnings('error', r'SettingWithCopyWarning). Además, la sugerencia de uso a .locveces tampoco ayuda (si está en un grupo).
guiones
41

Advertencia de copia de 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 de lo que los pandas intentan advertirte.


Por qué .ix es una mala idea?

El .ixobjeto 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: dfcopyahora es un marco de datos independiente. Cambiarlo no cambiarádf

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

Comportamiento dos: esto cambia el marco de datos original.


Usar en su .loclugar

Los desarrolladores de pandas reconocieron que el .ixobjeto era bastante maloliente [especulativamente] y, por lo tanto, crearon dos nuevos objetos que ayudan a acceder y asignar 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á haciendo en su ejemplo de código es cargar un archivo grande con muchas columnas y luego modificarlo para que sea más pequeño.

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

Entonces, 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]]

Hacer 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 solo leerá las columnas que le interesen y las nombrará correctamente. No es necesario usar el .ixobjeto maligno para hacer cosas mágicas.

firelynx
fuente
"Los desarrolladores de pandas reconocieron que el objeto .ix olía bastante [especulativamente] y, por lo tanto, crearon dos nuevos objetos", ¿cuál es el otro?
jf328
3
@ jf328 .iloc creo
Brian Bien
1
Sí, es .iloc. Estos son los dos métodos principales para indexar estructuras de datos de pandas. Lea más en la documentación.
Ninjakannon
¿Cómo se debe reemplazar una columna DataFrame con marcas de tiempo en una columna con un objeto de fecha o cadena?
boldnik
@boldnik Comprueba esta respuesta stackoverflow.com/a/37453925/3730397
firelynx
20

Aquí respondo la pregunta directamente. ¿Como lidiar con?

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 lo hace:

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

Ambos df0y df1son DataFrameobjetos, pero algo en ellos es diferente que 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 su herramienta de 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 advertir es DataFrame._check_setitem_copyqué controles _is_copy. Así que aquí tienes. Haga un copypara que su DataFrame no sea_is_copy .

La advertencia sugiere su uso .loc, pero si la usa .locen un marco _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.

usuario443854
fuente
11

Este tema es realmente confuso con los pandas. Afortunadamente, tiene una solución relativamente simple.

El problema es que no siempre está claro si las operaciones de filtrado de datos (por ejemplo, loc) devuelven una copia o una vista del DataFrame. El uso adicional de dicho DataFrame filtrado podría ser confuso.

La solución simple es (a menos que necesite trabajar con conjuntos de datos muy grandes):

Siempre que necesite actualizar cualquier valor, asegúrese siempre de copiar implícitamente el DataFrame antes de la asignación.

df  # Some DataFrame
df = df.loc[:, 0:2]  # Some filtering (unsure whether a view or copy is returned)
df = df.copy()  # Ensuring a copy is made
df[df["Name"] == "John"] = "Johny"  # Assignment can be done now (no warning)
Mikulas
fuente
Hay un error tipográfico: implícitamente debería ser explícitamente
s9527
7

Para eliminar cualquier duda, mi solución fue hacer una copia profunda del segmento en lugar de una copia normal. Esto puede no ser aplicable dependiendo de su contexto (restricciones de memoria / tamaño del segmento, potencial de degradación del rendimiento, especialmente si la copia se produce en un bucle como lo hizo para mí, etc.)

Para ser claros, aquí está la advertencia que recibí:

/opt/anaconda3/lib/python3.6/site-packages/ipykernel/__main__.py:54:
SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame
See the caveats in the documentation:
http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy

Ilustración

Tenía dudas de que la advertencia fuera lanzada debido a una columna que estaba cayendo en una copia de la rebanada. Aunque técnicamente no estaba tratando de establecer un valor en la copia del segmento, todavía era una modificación de la copia del segmento. A continuación se detallan los pasos (simplificados) que he tomado para confirmar la sospecha, espero que ayude a aquellos de nosotros que estamos tratando de entender la advertencia.

Ejemplo 1: colocar una columna en el original afecta la copia

Ya lo sabíamos, pero este es un recordatorio saludable. De esto NO se trata la advertencia.

>> data1 = {'A': [111, 112, 113], 'B':[121, 122, 123]}
>> df1 = pd.DataFrame(data1)
>> df1

    A   B
0   111 121
1   112 122
2   113 123


>> df2 = df1
>> df2

A   B
0   111 121
1   112 122
2   113 123

# Dropping a column on df1 affects df2
>> df1.drop('A', axis=1, inplace=True)
>> df2
    B
0   121
1   122
2   123

Es posible evitar que los cambios realizados en df1 afecten a df2

>> data1 = {'A': [111, 112, 113], 'B':[121, 122, 123]}
>> df1 = pd.DataFrame(data1)
>> df1

A   B
0   111 121
1   112 122
2   113 123

>> import copy
>> df2 = copy.deepcopy(df1)
>> df2
A   B
0   111 121
1   112 122
2   113 123

# Dropping a column on df1 does not affect df2
>> df1.drop('A', axis=1, inplace=True)
>> df2
    A   B
0   111 121
1   112 122
2   113 123

Ejemplo 2: colocar una columna en la copia puede afectar el original

Esto realmente ilustra la advertencia.

>> data1 = {'A': [111, 112, 113], 'B':[121, 122, 123]}
>> df1 = pd.DataFrame(data1)
>> df1

    A   B
0   111 121
1   112 122
2   113 123

>> df2 = df1
>> df2

    A   B
0   111 121
1   112 122
2   113 123

# Dropping a column on df2 can affect df1
# No slice involved here, but I believe the principle remains the same?
# Let me know if not
>> df2.drop('A', axis=1, inplace=True)
>> df1

B
0   121
1   122
2   123

Es posible evitar que los cambios realizados en df2 afecten a df1

>> data1 = {'A': [111, 112, 113], 'B':[121, 122, 123]}
>> df1 = pd.DataFrame(data1)
>> df1

    A   B
0   111 121
1   112 122
2   113 123

>> import copy
>> df2 = copy.deepcopy(df1)
>> df2

A   B
0   111 121
1   112 122
2   113 123

>> df2.drop('A', axis=1, inplace=True)
>> df1

A   B
0   111 121
1   112 122
2   113 123

¡Salud!

Raphvanns
fuente
4

Esto debería funcionar:

quote_df.loc[:,'TVol'] = quote_df['TVol']/TVOL_SCALE
jrouquie
fuente
4

Algunos pueden querer simplemente suprimir la advertencia:

class SupressSettingWithCopyWarning:
    def __enter__(self):
        pd.options.mode.chained_assignment = None

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

with SupressSettingWithCopyWarning():
    #code that produces warning
delicada
fuente
3

Si ha asignado el segmento a una variable y desea establecerlo usando la variable como se muestra a continuación:

df2 = df[df['A'] > 2]
df2['B'] = value

Y no desea utilizar la solución de Jeff porque su computación de condición df2es demasiado larga o por alguna otra razón, entonces puede usar lo siguiente:

df.loc[df2.index.tolist(), 'B'] = value

df2.index.tolist() devuelve los índices de todas las entradas en df2, que luego se utilizarán para establecer la columna B en el marco de datos original.

Steohan
fuente
esto es 9 veces más caro que df ["B"] = valor
Claudiu Creanga
¿Puedes explicar esto más profundamente @ClaudiuCreanga?
gies0r
2

Para mí, este problema ocurrió en un siguiente> ejemplo <simplificado. Y también pude resolverlo (con suerte con una solución correcta):

código antiguo con advertencia:

def update_old_dataframe(old_dataframe, new_dataframe):
    for new_index, new_row in new_dataframe.iterrorws():
        old_dataframe.loc[new_index] = update_row(old_dataframe.loc[new_index], new_row)

def update_row(old_row, new_row):
    for field in [list_of_columns]:
        # line with warning because of chain indexing old_dataframe[new_index][field]
        old_row[field] = new_row[field]  
    return old_row

Esto imprimió la advertencia para la línea old_row[field] = new_row[field]

Dado que las filas en el método update_row son realmente de tipo Series, reemplacé la línea con:

old_row.at[field] = new_row.at[field]

es decir, método para acceder / búsquedas para a Series. Si bien ambos funcionan bien y el resultado es el mismo, de esta manera no tengo que desactivar las advertencias (= guardarlas para otros problemas de indexación de la cadena en otro lugar).

Espero que esto pueda ayudar a alguien.

Petr Szturc
fuente
2

Podría evitar todo el problema como este, creo:

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

Usando Asignar. De la documentación : Asigne nuevas columnas a un DataFrame, devolviendo un nuevo objeto (una copia) con todas las columnas originales además de las nuevas.

Consulte el artículo de Tom Augspurger sobre el método de encadenamiento en pandas: https://tomaugspurger.github.io/method-chaining

hughdbrown
fuente
2

Seguimiento principiante pregunta / comentario

Tal vez una aclaración para otros principiantes como yo (vengo de R, que parece funcionar de manera un poco diferente bajo el capó). El siguiente código funcional y de aspecto inofensivo siguió produciendo la advertencia SettingWithCopy, y no pude entender por qué. Había leído y entendido el emitido con "indexación encadenada", pero mi código no contiene ninguno:

def plot(pdb, df, title, **kw):
    df['target'] = (df['ogg'] + df['ugg']) / 2
    # ...

Pero luego, más tarde, demasiado tarde, miré dónde se llama la función plot ():

    df = data[data['anz_emw'] > 0]
    pixbuf = plot(pdb, df, title)

Entonces, "df" no es un marco de datos, sino un objeto que de alguna manera recuerda que fue creado al indexar un marco de datos (entonces, ¿es eso una vista?) Que haría la línea en plot ()

 df['target'] = ...

equivalente a

 data[data['anz_emw'] > 0]['target'] = ...

que es una indexación encadenada ¿Lo entendí bien?

De todas formas,

def plot(pdb, df, title, **kw):
    df.loc[:,'target'] = (df['ogg'] + df['ugg']) / 2

arreglado.

musbur
fuente
1

Como esta pregunta ya está completamente explicada y discutida en las respuestas existentes, solo proporcionaré un pandasenfoque ordenado para el administrador de contexto usando pandas.option_context(enlaces a documentos y ejemplos ): no hay absolutamente ninguna necesidad de crear una clase personalizada con todos los métodos dunder y otras campanas y silbidos.

Primero el código del administrador de contexto en sí:

from contextlib import contextmanager

@contextmanager
def SuppressPandasWarning():
    with pd.option_context("mode.chained_assignment", None):
        yield

Entonces un ejemplo:

import pandas as pd
from string import ascii_letters

a = pd.DataFrame({"A": list(ascii_letters[0:4]), "B": range(0,4)})

mask = a["A"].isin(["c", "d"])
# Even shallow copy below is enough to not raise the warning, but why is a mystery to me.
b = a.loc[mask]  # .copy(deep=False)

# Raises the `SettingWithCopyWarning`
b["B"] = b["B"] * 2

# Does not!
with SuppressPandasWarning():
    b["B"] = b["B"] * 2

Vale la pena notar que ambos enfoques no se modifican a, lo cual es un poco sorprendente para mí, e incluso una copia de df superficial .copy(deep=False)evitaría que se genere esta advertencia (por lo que entiendo, la copia superficial al menos también debería modificarse a, pero no lo hace). 't. pandasmagia.).

m-dz
fuente
hmmm, entiendo que si la advertencia planteó que algo está mal obviamente, así que mejor es evitar la advertencia como suprimirla, ¿qué crees?
Israel
No, la advertencia es solo una advertencia. Al igual que aquí, te advierte que algo podría estar mal, lo cual es bueno saber, pero si sabes qué y por qué lo estás haciendo, está perfectamente bien suprimir algunos de ellos. Consulte la explicación en stackoverflow.com/a/20627316/4272484 sobre la reasignación de referencias.
m-dz
1

Había tenido este problema .apply()al asignar un nuevo marco de datos desde un marco de datos preexistente en el que he usado el .query()método. Por ejemplo:

prop_df = df.query('column == "value"')
prop_df['new_column'] = prop_df.apply(function, axis=1)

Volvería este error. La solución que parece resolver el error en este caso es cambiar esto a:

prop_df = df.copy(deep=True)
prop_df = prop_df.query('column == "value"')
prop_df['new_column'] = prop_df.apply(function, axis=1)

Sin embargo, esto NO es eficiente, especialmente cuando se utilizan grandes marcos de datos, debido a que tiene que hacer una nueva copia.

Si está utilizando el .apply()método para generar una nueva columna y sus valores, una solución que resuelve el error y es más eficiente es agregar .reset_index(drop=True):

prop_df = df.query('column == "value"').reset_index(drop=True)
prop_df['new_column'] = prop_df.apply(function, axis=1)
ZG1997
fuente