Como lidar com SettingWithCopyWarning no Pandas?

629

fundo

Acabei de atualizar meus Pandas de 0.11 para 0.13.0rc1. Agora, o aplicativo está lançando muitos novos avisos. Um deles assim:

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

Eu quero saber o que exatamente isso significa? Preciso mudar alguma coisa?

Como devo suspender o aviso se eu insistir em usar quote_df['TVol'] = quote_df['TVol']/TVOL_SCALE?

A função que gera erros

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

Mais mensagens de erro

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])
Grande erro
fonte
2
Aqui está um gerente de contexto para definir temporariamente o nível de aviso gist.github.com/notbanker/2be3ed34539c86e22ffdd88fd95ad8bc
Peter Cotton
2
você pode usar df.set_value, docs aqui - pandas.pydata.org/pandas-docs/stable/generated/...
leonprou
1
pandas.pydata.org/pandas-docs/stable/… documento oficial explicado em detalhes
wyx
3
@leonprou df.set_valuefoi preterido. Pandas agora recomenda o uso .at[]ou .iat[]em seu lugar. docs aqui pandas.pydata.org/pandas-docs/stable/generated/…
Kyle C
Estou surpreso que ninguém tenha mencionado pandas option_contextaqui: pandas.pydata.org/pandas-docs/stable/user_guide/options.html , use aswith pd.option_context("mode.chained_assignment", None): [...]
m-dz

Respostas:

795

O SettingWithCopyWarningfoi criado para sinalizar atribuições "encadeadas" potencialmente confusas, como as seguintes, que nem sempre funcionam como o esperado, principalmente quando a primeira seleção retorna uma cópia . [consulte GH5390 e GH5597 para discussão em segundo plano.]

df[df['A'] > 2]['B'] = new_val  # new_val not set in df

O aviso oferece uma sugestão para reescrever da seguinte maneira:

df.loc[df['A'] > 2, 'B'] = new_val

No entanto, isso não se adequa ao seu uso, o que equivale a:

df = df[df['A'] > 2]
df['B'] = new_val

Embora esteja claro que você não se importa com as gravações voltando ao quadro original (já que você está substituindo a referência a ele), infelizmente esse padrão não pode ser diferenciado do primeiro exemplo de atribuição encadeada. Daí o aviso (falso positivo). O potencial de falsos positivos é abordado nos documentos sobre indexação , se você quiser ler mais. Você pode desativar com segurança esse novo aviso com a seguinte atribuição.

import pandas as pd
pd.options.mode.chained_assignment = None  # default='warn'
Garrett
fonte
34
Eu acho que eu seria a favor de não avisar sobre isso. Se você trabalha com a sintaxe da atribuição em cadeia, pode definitivamente descobrir a ordem da indexação que precisa acontecer para que ela funcione conforme o esperado em qualquer situação. Eu acho que é excessivamente paranóico o fato de existirem essas precauções exaustivas. No mesmo espírito de "deixar todo mundo crescer" sobre métodos ou atributos de classe 'privada', acho melhor para os pandas deixar os usuários crescerem sobre tarefas encadeadas. Use-os apenas se souber o que está fazendo.
Ely
48
É um pouco impiedoso tentar avisar as pessoas quando elas buscam alternativas. Os métodos de acesso mais recentes do Pandas (aprimorados .ix, aprimorados .ilocetc.) podem definitivamente ser vistos como "o caminho principal" sem avisar a todos incessantemente sobre outros caminhos. Em vez disso, deixe-os serem adultos e, se quiserem fazer tarefas encadeadas, que assim seja. Meus dois centavos de qualquer maneira. Vemos comentários desapontados dos desenvolvedores do Pandas aqui frequentemente, quando tarefas encadeadas funcionarão para resolver um problema, mas não seriam consideradas a maneira "principal" de fazer isso.
ely
8
@EMS O problema é que nem sempre é claro a partir do código em que uma cópia versus uma visualização está sendo feita e vários bugs / confusões surgem desse problema. Estávamos pensando em colocar um arquivo / opções rc para fazer a configuração automaticamente, o que pode ser mais útil, considerando o funcionamento da configuração com aviso de cópia.
Jeff Tratner
3
O motivo para avisar é que as pessoas estão atualizando o código antigo, é claro. E eu definitivamente preciso de um aviso, porque estou lidando com um código antigo muito feio.
Thomas Andrews
16
Em uma nota lateral, descobri que desabilitar o aviso chained_assignment: pd.options.mode.chained_assignment = Noneresultou em meu código sendo executado aproximadamente 6 vezes mais rápido. Alguém mais teve resultados semelhantes?
Muon
209

Como lidar com os SettingWithCopyWarningpandas?

Este post é destinado a leitores que,

  1. Gostaria de entender o que esse aviso significa
  2. Gostaria de entender diferentes maneiras de suprimir esse aviso
  3. 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:

  1. Faça um deepcopy

    df2 = df[['A']].copy(deep=True)
    df2['A'] /= 2
    
  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.

cs95
fonte
7
PS: Deixe-me saber se sua situação não é abordada na lista de perguntas da seção 3. Vou alterar meu post.
cs95
150

Em geral, o objetivo SettingWithCopyWarningé mostrar aos usuários (e especialmente aos novos) que eles podem estar operando em uma cópia e não no original como pensam. Não são falsos positivos (IOW, se você sabe o que está fazendo que poderia ser ok ). Uma possibilidade é simplesmente desativar o aviso (por padrão avisar ) como sugerido pelo @Garrett.

Aqui está outra opção:

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

Você pode definir o is_copysinalizador como False, que efetivamente desativará a verificação, para esse objeto :

In [5]: dfa.is_copy = False

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

Se você copiar explicitamente, nenhum aviso adicional ocorrerá:

In [7]: dfa = df.ix[:, [1, 0]].copy()

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

O código que o OP está mostrando acima, apesar de legítimo, e provavelmente algo que eu também faço, é tecnicamente um caso para esse aviso, e não um falso positivo. Outra maneira de não receber o aviso seria fazer a operação de seleção via reindex, por exemplo,

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

Ou,

quote_df = quote_df.reindex(['STK', ...], axis=1)  # v.0.21
Jeff
fonte
Obrigado pelas informações e discussão, basta desligar o aviso para deixar o console em silêncio. Parece a visualização e a tabela no banco de dados SQL. Eu preciso saber mais sobre o benefício da introdução do conceito de 'copiar', mas IMHO é um pouco um fardo para cuidar do sutil semântica, sintaxe diferença ..
bigbug
19
Eu concordo com a cópia (); está claro e corrigiu meu problema (que era um falso positivo).
Rdchambers
5
após a atualização para 0.16eu ver muitos mais falsos positivos, o problema com falsos positivos é que se aprende a ignorá-lo, mesmo que às vezes seja legítimo.
dashesy
3
@ dashesy você está perdendo o ponto. às vezes talvez até na maioria das vezes isso funcione. Mas isso pode acontecer, por exemplo, se o quadro for maior / menor ou se você adicionar uma coluna com um tipo diferente de que não funciona. Essa é a questão. Você está fazendo algo que pode funcionar, mas não é garantido. Isso é muito diferente dos avisos de descontinuação. Se você quiser continuar usando e funciona, ótimo. Mas esteja avisado.
71115 Jeff
3
@ Jeff faz sentido agora, por isso é um undefinedcomportamento. Se alguma coisa deve gerar um erro então (para evitar armadilhas vistas em C), uma vez que apié congelado, o comportamento atual do aviso faz sentido para compatibilidade com versões anteriores. E vou fazê-los jogar para pegá-los como erros no meu código de produção ( warnings.filterwarnings('error', r'SettingWithCopyWarning). Além disso, a sugestão de uso .locàs vezes também não ajuda (se estiver em um grupo).
Dashesy 07/07
41

Aviso de cópia do dataframe do Pandas

Quando você vai fazer algo assim:

quote_df = quote_df.ix[:,[0,3,2,1,4,5,8,9,30,31]]

pandas.ix nesse caso, retorna um novo quadro de dados independente.

Quaisquer valores que você decidir alterar nesse quadro de dados não mudarão o quadro de dados original.

É sobre isso que os pandas tentam avisá-lo.


Por que .ixé uma má ideia

O .ixobjeto tenta fazer mais de uma coisa e, para quem leu alguma coisa sobre código limpo, esse é um cheiro forte.

Dado esse quadro de dados:

df = pd.DataFrame({"a": [1,2,3,4], "b": [1,1,2,2]})

Dois comportamentos:

dfcopy = df.ix[:,["a"]]
dfcopy.a.ix[0] = 2

Comportamento um: dfcopyagora é um quadro de dados independente. Mudando isso não vai mudardf

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

Comportamento dois: isso altera o quadro de dados original.


Use em .locvez disso

Os desenvolvedores do pandas reconheceram que o .ixobjeto era bastante fedorento [especulativamente] e, assim, criaram dois novos objetos que ajudam na acessão e atribuição de dados. (O outro ser .iloc)

.loc é mais rápido, porque não tenta criar uma cópia dos dados.

.loc destina-se a modificar o quadro de dados existente no local, o que é mais eficiente em memória.

.loc é previsível, tem um comportamento.


A solução

O que você está fazendo no seu exemplo de código é carregar um arquivo grande com muitas colunas e modificá-lo para que seja menor.

A pd.read_csvfunção pode ajudá-lo com muito disso e também agilizar o carregamento do arquivo.

Então, ao invés de fazer isso

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

Faça isso

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

Isso lerá apenas as colunas das quais você está interessado e as nomeie adequadamente. Não há necessidade de usar o .ixobjeto maligno para fazer coisas mágicas.

firelynx
fonte
"Os desenvolvedores do pandas reconheceram que o objeto .ix era bastante fedorento [especulativamente] e, portanto, criaram dois novos objetos" - qual é o outro?
Jf328
3
@ jf328 .iloc acho
Brian Bien
1
É sim .iloc. Estes são os dois métodos principais para indexar estruturas de dados de pandas. Leia mais na documentação.
Ninjakannon
Como substituir uma coluna DataFrame por registros de data e hora na coluna com objeto ou string de data e hora?
Boldnik
@boldnik Verifique esta resposta stackoverflow.com/a/37453925/3730397
firelynx 27/17/17
20

Aqui eu respondo a pergunta diretamente. Como lidar com isso?

Faça um .copy(deep=False)depois de cortar. Consulte pandas.DataFrame.copy .

Espere, uma fatia não retorna uma cópia? Afinal, é isso que a mensagem de aviso está tentando dizer? Leia a resposta longa:

import pandas as pd
df = pd.DataFrame({'x':[1,2,3]})

Isso dá um aviso:

df0 = df[df.x>2]
df0['foo'] = 'bar'

Isto não:

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

Ambos df0e df1são DataFrameobjetos, mas alguma coisa sobre eles é diferente que permite pandas para imprimir o aviso. Vamos descobrir o que é isso.

import inspect
slice= df[df.x>2]
slice_copy = df[df.x>2].copy(deep=False)
inspect.getmembers(slice)
inspect.getmembers(slice_copy)

Usando sua ferramenta diff de escolha, você verá que, além de alguns endereços, a única diferença material é esta:

|          | slice   | slice_copy |
| _is_copy | weakref | None       |

O método que decide se deve avisar é DataFrame._check_setitem_copyquais verificações _is_copy. Então aqui está você. Faça um copypara que seu DataFrame não seja _is_copy.

O aviso está sugerindo o uso .loc, mas se você usar .locem um quadro que _is_copy, você ainda receberá o mesmo aviso. Enganador? Sim. Irritante? Pode apostar. Útil? Potencialmente, quando a atribuição encadeada é usada. Mas ele não pode detectar corretamente a atribuição da cadeia e imprime o aviso indiscriminadamente.

user443854
fonte
11

Este tópico é realmente confuso com os pandas. Felizmente, ele tem uma solução relativamente simples.

O problema é que nem sempre é claro se as operações de filtragem de dados (por exemplo, loc) retornam uma cópia ou uma exibição do DataFrame. O uso posterior desse DataFrame filtrado pode, portanto, ser confuso.

A solução simples é (a menos que você precise trabalhar com conjuntos muito grandes de dados):

Sempre que você precisar atualizar algum valor, sempre copie implicitamente o DataFrame antes da atribuição.

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
fonte
Existe um erro de digitação: implicitamente deve ser explicitamente
s9527
7

Para remover qualquer dúvida, minha solução foi fazer uma cópia profunda da fatia em vez de uma cópia regular. Isso pode não ser aplicável, dependendo do seu contexto (restrições de memória / tamanho da fatia, potencial de degradação do desempenho - especialmente se a cópia ocorrer em um loop como ocorreu comigo, etc ...)

Para ficar claro, aqui está o aviso que recebi:

/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

Ilustração

Eu tinha dúvidas de que o aviso foi lançado por causa de uma coluna que eu estava soltando em uma cópia da fatia. Embora não esteja tecnicamente tentando definir um valor na cópia da fatia, isso ainda era uma modificação da cópia da fatia. Abaixo estão os passos (simplificados) que tomei para confirmar a suspeita, espero que ajude aqueles de nós que estão tentando entender o aviso.

Exemplo 1: soltar uma coluna no original afeta a cópia

Já sabíamos disso, mas este é um lembrete saudável. NÃO é disso que trata o aviso.

>> 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

É possível evitar alterações feitas no df1 para afetar o 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

Exemplo 2: soltar uma coluna na cópia pode afetar o original

Na verdade, isso ilustra o aviso.

>> 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

É possível evitar alterações feitas no df2 para afetar o 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

Felicidades!

Raphvanns
fonte
4

Isso deve funcionar:

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

Alguns podem querer simplesmente suprimir o aviso:

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
delica
fonte
3

Se você atribuiu a fatia a uma variável e deseja definir usando a variável como a seguir:

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

E você não deseja usar a solução Jeffs porque sua condição de computação df2é longa ou por algum outro motivo, você pode usar o seguinte:

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

df2.index.tolist() retorna os índices de todas as entradas no df2, que serão usadas para definir a coluna B no quadro de dados original.

Steohan
fonte
este é 9 tempo mais caro do df, em seguida, [ "B"] = valor
Claudiu Creanga
Você pode explicar isso mais profundamente @ClaudiuCreanga?
precisa saber é o seguinte
2

Para mim, esse problema ocorreu em um exemplo a seguir> simplificado. E eu também fui capaz de resolvê-lo (espero que com uma solução correta):

código antigo com aviso:

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

Isso imprimiu o aviso para a linha old_row[field] = new_row[field]

Como as linhas no método update_row são realmente do tipo Series, substituí a linha por:

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

ou seja, método para acessar / pesquisas para a Series. Mesmo que ambos funcionem bem e o resultado seja o mesmo, dessa forma não preciso desativar os avisos (= mantenha-os para outros problemas de indexação de cadeia em outro lugar).

Espero que isso ajude alguém.

Petr Szturc
fonte
2

Você poderia evitar todo o problema como este, acredito:

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 Atribuir. Na documentação : atribua novas colunas a um DataFrame, retornando um novo objeto (uma cópia) com todas as colunas originais, além das novas.

Veja o artigo de Tom Augspurger sobre encadeamento de métodos em pandas: https://tomaugspurger.github.io/method-chaining

hughdbrown
fonte
2

Pergunta / observação para iniciantes de acompanhamento

Talvez um esclarecimento para outros iniciantes como eu (eu sou do R, que parece funcionar de maneira um pouco diferente sob o capô). O seguinte código funcional e de aparência inofensiva continuava produzindo o aviso SettingWithCopy, e eu não conseguia entender o porquê. Eu tinha lido e entendido o emitido com "indexação em cadeia", mas meu código não contém nenhum:

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

Mas depois, tarde demais, observei onde a função plot () é chamada:

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

Portanto, "df" não é um quadro de dados, mas um objeto que lembra de alguma forma que ele foi criado pela indexação de um quadro de dados (o que é uma visão?) Que faria a linha no plot ()

 df['target'] = ...

equivalente a

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

que é uma indexação encadeada. Eu entendi direito?

De qualquer forma,

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

consertou.

musbur
fonte
1

Como essa pergunta já está totalmente explicada e discutida nas respostas existentes, fornecerei uma pandasabordagem clara ao gerenciador de contexto usando pandas.option_context(links para documentos e exemplo ) - não há absolutamente nenhuma necessidade de criar uma classe personalizada com todos os métodos de dunder e outros sinos e assobios.

Primeiro, o próprio código de gerenciador de contexto:

from contextlib import contextmanager

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

Então um exemplo:

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 a pena notar que as duas abordagens não são modificadas a, o que é um pouco surpreendente para mim, e mesmo uma cópia rasa de df .copy(deep=False)impediria que esse aviso fosse acionado (tanto quanto eu entendo, a cópia rasa deve pelo menos modificar atambém, mas não é pandasmágica.).

m-dz
fonte
hmmm, eu entendo se aviso levantado algo está errado, obviamente, então é melhor evitar aviso como suprimir, o que você acha?
jezrael 03/02
Não, aviso é apenas um aviso. Como aqui, está avisando que algo pode estar errado, o que é ótimo saber, mas se você sabe o que e por que está fazendo, é perfeitamente bom suprimir alguns deles. Consulte a explicação em stackoverflow.com/a/20627316/4272484 sobre como reatribuir referências.
m-dz
1

Eu estava recebendo esse problema .apply()ao atribuir um novo quadro de dados a partir de um quadro de dados preexistente no qual usei o .query()método. Por exemplo:

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

Retornaria esse erro. A correção que parece resolver o erro nesse caso é alterando isso para:

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

No entanto, isso NÃO é eficiente, especialmente ao usar quadros de dados grandes, devido à necessidade de fazer uma nova cópia.

Se você estiver usando o .apply()método para gerar uma nova coluna e seus valores, uma correção que resolve o erro e é mais eficiente é adicionar .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
fonte