Como lidar com os SettingWithCopyWarningpandas?
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 SettingWithCopyWarningfoi criado para sinalizar operações de "atribuição encadeada". Considere dfna 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 locpara atribuição baseada em etiqueta e atribuição baseada ilocem 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 ate iat.
Mais pode ser encontrado na documentação .
Nota
Todas as operações de indexação booleana feitas com loctambém podem ser feitas iloc. A única diferença é que ilocespera 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. Nonesuprimirá 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 df2partir de algo maior, como
df2 = df[df.A > 5]
? Nesse caso, a indexação booleana retornará uma exibição, e df2fará referência ao original. O que você precisa fazer é atribuir df2a 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 df2deve 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.