How to deal with SettingWithCopyWarning
in 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 saber cómo mejorar su código y seguir buenas prácticas para evitar esta advertencia en el futuro.
Configuración
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 entender qué 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 , según el 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 réplica de los datos del original y la modificación de la copia no tiene ningún efecto sobre el original.
Como se mencionó en otras respuestas, SettingWithCopyWarning
se creó para marcar las operaciones de "asignación encadenada". Considere df
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, por lo general, es difícil predecir si se devuelve una vista o una copia, por lo que esto se convierte en gran medida en un problema cuando se intenta volver a asignar valores. 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 bien, según se __getitem__
devuelva una vista o una copia, es posible que la __setitem__
operación no funcione .
En general, debe utilizar loc
para la asignación basada en etiquetas y iloc
para la asignación basada en enteros/posicionales, ya que la especificación garantiza que siempre funcionan en el original. Además, para configurar una sola celda, debe usar at
y iat
.
Se puede encontrar más en la documentación .
Note
All boolean indexing operations done with loc
can also be done with iloc
. The only difference is that iloc
expects either
integers/positions for index or a numpy array of boolean values, and
integer/position indexes for the columns.
For example,
df.loc[df.A > 5, 'B'] = 4
Can be written nas
df.iloc[(df.A > 5).values, 1] = 4
And,
df.loc[1, 'A'] = 100
Can be written as
df.iloc[1, 0] = 100
And so on.
¡Solo dime cómo suprimir la advertencia!
Considere una operación simple en la columna "A" de df
. Seleccionar "A" y dividir por 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:
(recomendado) Use loc
para dividir subconjuntos :
df2 = df.loc[:, ['A']]
df2['A'] /= 2 # Does not raise
Cambiarpd.options.mode.chained_assignment
Puede establecerse en None
, "warn"
o "raise"
. "warn"
es el predeterminado. None
suprimirá la advertencia por completo y "raise"
lanzará un SettingWithCopyError
, evitando que la operación se lleve a cabo.
pd.options.mode.chained_assignment = None
df2['A'] /= 2
Hacer unadeepcopy
df2 = df[['A']].copy(deep=True)
df2['A'] /= 2
@Peter Cotton en los comentarios, se le ocurrió una buena manera de cambiar el modo de manera no intrusiva (modificado a partir de esta esencia ) usando un administrador de contexto, para configurar el modo solo mientras sea necesario, y luego restablecerlo a la Estado original al terminar.
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?
Muchas veces, los usuarios intentan buscar formas de suprimir esta excepción sin comprender completamente por qué se generó 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 "X" con raíces más profundas. Se plantearán preguntas basadas en problemas comunes que se encuentran con esta advertencia, y luego se presentarán soluciones.
Question 1
I have a 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
I want to assign values in col "A" > 5 to 1000. My expected output is
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
Question 21
I am trying to set the value in cell (1, 'D') to 12345. My expected output is
A B C D E
0 5 0 3 3 7
1 9 3 5 12345 4
2 7 6 8 8 1
I have tried different ways of accessing this cell, such as
df['D'][1]
. What is the best way to do this?
1. This question isn't specifically related to the warning, but
it is good to understand how to do this particular operation correctly
so as to avoid situations where the warning could potentially arise in
future.
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
Question 3
I am trying to subset values based on some condition. I have a
DataFrame
A B C D E
1 9 3 5 2 4
2 7 6 8 8 1
I would like to assign values in "D" to 123 such that "C" == 5. I
tried
df2.loc[df2.C == 5, 'D'] = 123
Which seems fine but I am still getting the
SettingWithCopyWarning
! How do I fix this?
En realidad, esto probablemente se deba a un código que se encuentra más arriba en su canalización. ¿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 tendría que hacer es asignar df2
a una copia :
df2 = df[df.A > 5].copy()
# Or,
# df2 = df.loc[df.A > 5, :]
Question 4
I'm trying to drop column "C" in-place from
A B C D E
1 9 3 5 2 4
2 7 6 8 8 1
But using
df2.drop('C', axis=1, inplace=True)
Throws SettingWithCopyWarning
. Why is this happening?
Esto se debe a df2
que debe haberse creado como una vista de alguna otra operación de corte, como
df2 = df[df.A > 5]
La solución aquí es hacer o usar copy()
, como antes.df
loc