¿Cómo lidiar con los SettingWithCopyWarning
pandas?
Esta publicación está destinada a lectores que,
- Me gustaría entender qué significa esta advertencia
- Me gustaría entender diferentes formas de suprimir esta advertencia.
- 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, 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
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 loc
para la asignación basada en etiquetas y 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
y iat
.
Más se puede encontrar en la documentación .
Nota
Todas las operaciones de indexación booleana realizadas con loc
también se pueden realizar 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
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:
Hacer una deepcopy
df2 = df[['A']].copy(deep=True)
df2['A'] /= 2
Cambiopd.options.mode.chained_assignment
Puede ajustarse a None
, "warn"
o "raise"
. "warn"
es el 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 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 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
una 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 df2
debe 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.