Altere um valor com base em outro valor em pandas

107

Estou tentando reprogramar meu código Stata em Python para melhorias de velocidade, e fui apontado na direção de PANDAS. Estou, no entanto, tendo dificuldade em entender como processar os dados.

Digamos que eu queira iterar todos os valores no cabeçalho da coluna 'ID'. Se esse ID corresponder a um número específico, quero alterar dois valores correspondentes FirstName e LastName.

No Stata, é assim:

replace FirstName = "Matt" if ID==103
replace LastName =  "Jones" if ID==103

Portanto, isso substitui todos os valores em FirstName que correspondem aos valores de ID == 103 para Matt.

No PANDAS, estou tentando algo assim

df = read_csv("test.csv")
for i in df['ID']:
    if i ==103:
          ...

Não tenho certeza para onde ir a partir daqui. Alguma ideia?

Parseltongue
fonte

Respostas:

180

Uma opção é usar os recursos de segmentação e indexação do Python para avaliar logicamente os locais onde sua condição se mantém e substituir os dados lá.

Supondo que você possa carregar seus dados diretamente no pandascom pandas.read_csv, o código a seguir pode ser útil para você.

import pandas
df = pandas.read_csv("test.csv")
df.loc[df.ID == 103, 'FirstName'] = "Matt"
df.loc[df.ID == 103, 'LastName'] = "Jones"

Conforme mencionado nos comentários, você também pode fazer a atribuição às duas colunas de uma vez:

df.loc[df.ID == 103, ['FirstName', 'LastName']] = 'Matt', 'Jones'

Observe que você precisará da pandasversão 0.11 ou mais recente para usar locnas operações de atribuição de substituição.


Outra maneira de fazer isso é usar o que é chamado de atribuição em cadeia. O comportamento disso é menos estável e, portanto, não é considerada a melhor solução (é explicitamente desencorajado nos documentos), mas é útil saber sobre:

import pandas
df = pandas.read_csv("test.csv")
df['FirstName'][df.ID == 103] = "Matt"
df['LastName'][df.ID == 103] = "Jones"
ely
fonte
16
que tal adicionar também este sabor:df.loc[df.ID == 103, ['FirstName', 'LastName']] = 'Matt', 'Jones'
Boud,
2
-1 "Outra maneira de fazer isso é usar o que é chamado de atribuição em cadeia." Não. Enfaticamente, não. É única útil saber que a atribuição acorrentado não é confiável. Não é que seja uma solução confiável e não ideal, a situação é muito pior . Você até reconheceu isso em outro lugar no Stack Overflow . Tente evitar dar a ilusão de que a atribuição em cadeia é uma opção viável. Os primeiros dois métodos fornecidos foram suficientes e são a forma preferida de fazer isso.
Phillip Cloud
9
Discordo. Não entendo por que você insiste em tentar pedantemente afirmar que a atribuição em cadeia não é uma forma viável. Reconheci que não é considerada a forma preferida. O que mais você quer. É absurdo agir como se isso não fosse uma maneira de fazer isso. Na verdade, no meu sistema agora (versão 0.8), é a maneira certa de fazer isso. Não estou interessado em seus votos positivos se você vai assumir esta posição. Sinta-se à vontade para sinalizar o seu ponto com um voto negativo, mas já refleti sobre o seu ponto e discordo dele.
ely
11
A internet é um negócio sério. De qualquer forma, EMS, gostei de saber que a opção existe.
Parseltongue,
Um problema que você pode enfrentar é que o csv tem pontos / pontos nos nomes das colunas e as atribuições ficam confusas. Você pode corrigir as colunas usando algo assim: cols = df.columns cols = cols.map (lambda x: x.replace ('.', '_') If isinstance (x, str) else x) df.columns = cols
ski_squaw
37

Você pode usar map, ele pode mapear valores de um dicionário ou até mesmo uma função personalizada.

Suponha que este seja o seu df:

    ID First_Name Last_Name
0  103          a         b
1  104          c         d

Crie os dictos:

fnames = {103: "Matt", 104: "Mr"}
lnames = {103: "Jones", 104: "X"}

E mapa:

df['First_Name'] = df['ID'].map(fnames)
df['Last_Name'] = df['ID'].map(lnames)

O resultado será:

    ID First_Name Last_Name
0  103       Matt     Jones
1  104         Mr         X

Ou use uma função personalizada:

names = {103: ("Matt", "Jones"), 104: ("Mr", "X")}
df['First_Name'] = df['ID'].map(lambda x: names[x][0])
Rutger Kassies
fonte
2
Isso não gerará um KeyError se os valores não existirem em seu dicionário?
EdChum
1
A função personalizada funcionará, as outras funcionarão de qualquer maneira. Mas presumi que o dicté criado para o mapeamento. Caso contrário, alguma verificação / limpeza pode ser feita com base em algo como:df.ID.isin(names.keys())
Rutger Kassies
A função personalizada pode ser expandida para qualquer função (não anônima).
user989762
14

A pergunta original trata de um caso de uso específico e restrito. Para aqueles que precisam de respostas mais genéricas, aqui estão alguns exemplos:

Criação de uma nova coluna usando dados de outras colunas

Dado o quadro de dados abaixo:

import pandas as pd
import numpy as np

df = pd.DataFrame([['dog', 'hound', 5],
                   ['cat', 'ragdoll', 1]],
                  columns=['animal', 'type', 'age'])

In[1]:
Out[1]:
  animal     type  age
----------------------
0    dog    hound    5
1    cat  ragdoll    1

Abaixo, estamos adicionando uma nova descriptioncoluna como uma concatenação de outras colunas usando a +operação que é substituída por séries. Formatação de string extravagante, strings f etc. não funcionarão aqui, pois +se aplica a valores escalares e não a valores 'primitivos':

df['description'] = 'A ' + df.age.astype(str) + ' years old ' \
                    + df.type + ' ' + df.animal

In [2]: df
Out[2]:
  animal     type  age                description
-------------------------------------------------
0    dog    hound    5    A 5 years old hound dog
1    cat  ragdoll    1  A 1 years old ragdoll cat

Obtemos 1 yearso gato (em vez de 1 year), que corrigiremos a seguir usando condicionais.

Modificando uma coluna existente com condicionais

Aqui, estamos substituindo a animalcoluna original por valores de outras colunas e usando np.wherepara definir uma substring condicional com base no valor de age:

# append 's' to 'age' if it's greater than 1
df.animal = df.animal + ", " + df.type + ", " + \
    df.age.astype(str) + " year" + np.where(df.age > 1, 's', '')

In [3]: df
Out[3]:
                 animal     type  age
-------------------------------------
0   dog, hound, 5 years    hound    5
1  cat, ragdoll, 1 year  ragdoll    1

Modificando várias colunas com condicionais

Uma abordagem mais flexível é chamar .apply()um dataframe inteiro em vez de uma única coluna:

def transform_row(r):
    r.animal = 'wild ' + r.type
    r.type = r.animal + ' creature'
    r.age = "{} year{}".format(r.age, r.age > 1 and 's' or '')
    return r

df.apply(transform_row, axis=1)

In[4]:
Out[4]:
         animal            type      age
----------------------------------------
0    wild hound    dog creature  5 years
1  wild ragdoll    cat creature   1 year

No código acima, a transform_row(r)função pega um Seriesobjeto que representa uma determinada linha (indicado por axis=1, o valor padrão de axis=0fornecerá um Seriesobjeto para cada coluna). Isso simplifica o processamento, pois podemos acessar os valores 'primitivos' reais na linha usando os nomes das colunas e ter visibilidade de outras células na linha / coluna fornecida.

ccpizza
fonte
1
Obrigado por dedicar seu tempo para escrever uma resposta tão abrangente. Muito apreciado.
Parseltongue
Obrigado por esta resposta extremamente útil. Um acompanhamento - e se quisermos modificar uma coluna fazendo matemática na coluna, em vez de modificar uma string? Por exemplo, usando o exemplo acima, e se quisermos multiplicar a coluna df.age por 7 se df.animal == 'cachorro'? Obrigado!
GbG
1
@GbG: np.whereé provavelmente o que você está procurando, consulte, por exemplo, stackoverflow.com/a/42540310/191246, mas também é possível que você não consiga ajustar a lógica em uma operação escalar, então você precisa transformar explicitamente a célula numericamente semelhante a como é feita emtransform_row
ccpizza
Obrigado @ccpizza! É mesmo o que eu procurava.
GbG
13

Essa pergunta ainda pode ser consultada com frequência suficiente para que valha a pena oferecer um adendo à resposta do Sr. Kassies. A dictclasse interna pode ser subclassificada de forma que um padrão seja retornado para as chaves 'ausentes'. Este mecanismo funciona bem para pandas. Mas veja abaixo.

Desta forma, é possível evitar erros de chave.

>>> import pandas as pd
>>> data = { 'ID': [ 101, 201, 301, 401 ] }
>>> df = pd.DataFrame(data)
>>> class SurnameMap(dict):
...     def __missing__(self, key):
...         return ''
...     
>>> surnamemap = SurnameMap()
>>> surnamemap[101] = 'Mohanty'
>>> surnamemap[301] = 'Drake'
>>> df['Surname'] = df['ID'].apply(lambda x: surnamemap[x])
>>> df
    ID  Surname
0  101  Mohanty
1  201         
2  301    Drake
3  401         

A mesma coisa pode ser feita de maneira mais simples da seguinte maneira. O uso do argumento 'default' para o getmétodo de um objeto dict torna desnecessário criar uma subclasse de um dict.

>>> import pandas as pd
>>> data = { 'ID': [ 101, 201, 301, 401 ] }
>>> df = pd.DataFrame(data)
>>> surnamemap = {}
>>> surnamemap[101] = 'Mohanty'
>>> surnamemap[301] = 'Drake'
>>> df['Surname'] = df['ID'].apply(lambda x: surnamemap.get(x, ''))
>>> df
    ID  Surname
0  101  Mohanty
1  201         
2  301    Drake
3  401         
Bill Bell
fonte
1
esta é de longe a melhor e mais fácil resposta que já vi, com excelente tratamento padrão. Obrigado.
Brendan
@Brendan: Oh! Muito obrigado.
Bill Bell