Como lidar com os SettingWithCopyWarning
pandas?
Este post é destinado a leitores que,
- Gostaria de entender o que esse aviso significa
- Gostaria de entender diferentes maneiras de suprimir esse aviso
- Gostaria de entender como melhorar seu código e seguir boas práticas para evitar esse aviso no futuro.
Configuração
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
Qual é o SettingWithCopyWarning
?
Para saber como lidar com esse aviso, é importante entender o que significa e por que ele é gerado em primeiro lugar.
Ao filtrar DataFrames, é possível dividir / indexar um quadro para retornar uma visualização ou uma cópia , dependendo do layout interno e de vários detalhes de implementação. Uma "visão" é, como o termo sugere, uma visão dos dados originais; portanto, modificar a visão pode modificar o objeto original. Por outro lado, uma "cópia" é uma replicação de dados do original e a modificação da cópia não afeta o original.
Conforme mencionado por outras respostas, o SettingWithCopyWarning
foi criado para sinalizar operações de "atribuição encadeada". Considere df
na configuração acima. Suponha que você queira selecionar todos os valores na coluna "B", onde os valores na coluna "A" são> 5. O Pandas permite fazer isso de maneiras diferentes, algumas mais corretas que outras. Por exemplo,
df[df.A > 5]['B']
1 3
2 6
Name: B, dtype: int64
E,
df.loc[df.A > 5, 'B']
1 3
2 6
Name: B, dtype: int64
Eles retornam o mesmo resultado, portanto, se você estiver apenas lendo esses valores, não fará diferença. Então qual é o problema? O problema com a atribuição encadeada é que geralmente é difícil prever se uma exibição ou uma cópia é retornada; portanto , isso se torna um problema quando você está tentando atribuir valores novamente. Para desenvolver o exemplo anterior, considere como esse código é executado pelo intérprete:
df.loc[df.A > 5, 'B'] = 4
# becomes
df.__setitem__((df.A > 5, 'B'), 4)
Com uma única __setitem__
chamada para df
. OTOH, considere este código:
df[df.A > 5]['B'] = 4
# becomes
df.__getitem__(df.A > 5).__setitem__('B", 4)
Agora, dependendo do __getitem__
retorno de uma exibição ou cópia, a __setitem__
operação pode não funcionar .
Em geral, você deve usar loc
para atribuição baseada em etiqueta e atribuição baseada iloc
em número inteiro / posicional, pois as especificações garantem que elas sempre operem no original. Além disso, para definir uma única célula, você deve usar at
e iat
.
Mais pode ser encontrado na documentação .
Nota
Todas as operações de indexação booleana feitas com loc
também podem ser feitas iloc
. A única diferença é que iloc
espera números inteiros / posições para o índice ou uma matriz numpy de valores booleanos e índices inteiros / posições para as colunas.
Por exemplo,
df.loc[df.A > 5, 'B'] = 4
Pode ser escrito nas
df.iloc[(df.A > 5).values, 1] = 4
E,
df.loc[1, 'A'] = 100
Pode ser escrito como
df.iloc[1, 0] = 100
E assim por diante.
Apenas me diga como suprimir o aviso!
Considere uma operação simples na coluna "A" de df
. Selecionar "A" e dividir por 2 aumentará o aviso, mas a operação 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
Existem algumas maneiras de silenciar diretamente esse aviso:
Faça um deepcopy
df2 = df[['A']].copy(deep=True)
df2['A'] /= 2
Mudançapd.options.mode.chained_assignment
pode ser definida como None
, "warn"
ou "raise"
. "warn"
é o padrão. None
suprimirá o aviso por completo e "raise"
emitirá um SettingWithCopyError
, impedindo a operação.
pd.options.mode.chained_assignment = None
df2['A'] /= 2
@ Peter Cotton nos comentários, apresentou uma boa maneira de alterar o modo de maneira não invasiva (modificada a partir desta essência ) usando um gerenciador de contexto, para definir o modo apenas pelo tempo necessário e redefini-lo para o estado original quando 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
O uso é o seguinte:
# some code here
with ChainedAssignent():
df2['A'] /= 2
# more code follows
Ou, para aumentar a exceção
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
O "Problema XY": O que estou fazendo de errado?
Na maioria das vezes, os usuários tentam procurar maneiras de suprimir essa exceção sem entender completamente por que ela foi criada. Este é um bom exemplo de um problema XY , em que os usuários tentam resolver um problema "Y" que é na verdade um sintoma de um problema profundamente enraizado "X". As perguntas serão levantadas com base em problemas comuns que encontram esse aviso e as soluções serão apresentadas.
Pergunta 1
Eu tenho um 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
Quero atribuir valores na col "A"> 5 a 1000. Minha saída esperada é
A B C D E
0 5 0 3 3 7
1 1000 3 5 2 4
2 1000 6 8 8 1
Maneira errada de fazer isso:
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
Caminho certo usando loc
:
df.loc[df.A > 5, 'A'] = 1000
Pergunta 2 1
Estou tentando definir o valor na célula (1, 'D') para 12345. Minha saída esperada é
A B C D E
0 5 0 3 3 7
1 9 3 5 12345 4
2 7 6 8 8 1
Eu tentei maneiras diferentes de acessar esta célula, como
df['D'][1]
. Qual é a melhor maneira de fazer isso?
1. Essa pergunta não está especificamente relacionada ao aviso, mas é bom entender como executar essa operação específica corretamente, para evitar situações em que o aviso possa surgir no futuro.
Você pode usar qualquer um dos seguintes métodos para fazer isso.
df.loc[1, 'D'] = 12345
df.iloc[1, 3] = 12345
df.at[1, 'D'] = 12345
df.iat[1, 3] = 12345
Pergunta 3
Estou tentando subconjunto de valores com base em alguma condição. Eu tenho um DataFrame
A B C D E
1 9 3 5 2 4
2 7 6 8 8 1
Eu gostaria de atribuir valores em "D" a 123, de modo que "C" == 5. Tentei
df2.loc[df2.C == 5, 'D'] = 123
O que parece bom, mas ainda estou conseguindo o
SettingWithCopyWarning
! Como faço para corrigir isso?
Na verdade, isso provavelmente ocorre por causa do código mais alto no seu pipeline. Você criou a df2
partir de algo maior, como
df2 = df[df.A > 5]
? Nesse caso, a indexação booleana retornará uma exibição, e df2
fará referência ao original. O que você precisa fazer é atribuir df2
a uma cópia :
df2 = df[df.A > 5].copy()
# Or,
# df2 = df.loc[df.A > 5, :]
Pergunta 4
Estou tentando soltar a coluna "C" no local de
A B C D E
1 9 3 5 2 4
2 7 6 8 8 1
Mas usando
df2.drop('C', axis=1, inplace=True)
Joga SettingWithCopyWarning
. Por que isso está acontecendo?
Isso ocorre porque df2
deve ter sido criado como uma visualização de alguma outra operação de fatiamento, como
df2 = df[df.A > 5]
A solução aqui é que quer fazer um copy()
dos df
, ou o uso loc
, como antes.