Cambiar un valor basado en otro valor en pandas

107

Estoy tratando de reprogramar mi código Stata en Python para mejorar la velocidad, y me señalaron en la dirección de PANDAS. Sin embargo, estoy teniendo dificultades para entender cómo procesar los datos.

Digamos que quiero iterar sobre todos los valores en el encabezado de columna 'ID'. Si ese ID coincide con un número específico, entonces quiero cambiar dos valores correspondientes FirstName y LastName.

En Stata se ve así:

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

Entonces, esto reemplaza todos los valores en FirstName que corresponden con los valores de ID == 103 a Matt.

En PANDAS, estoy intentando algo como esto

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

No estoy seguro de a dónde ir desde aquí. ¿Algunas ideas?

Pársel
fuente

Respuestas:

180

Una opción es utilizar las funciones de segmentación e indexación de Python para evaluar lógicamente los lugares donde se mantiene su condición y sobrescribir los datos allí.

Suponiendo que usted puede cargar sus datos directamente en pandasla pandas.read_csvcontinuación, el siguiente código podría ser útil para usted.

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

Como se menciona en los comentarios, también puede hacer la asignación a ambas columnas de una sola vez:

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

Tenga en cuenta que necesitará la pandasversión 0.11 o más reciente para utilizarla locen las operaciones de asignación de sobrescritura.


Otra forma de hacerlo es usar lo que se llama asignación encadenada. El comportamiento de esto es menos estable y, por lo tanto, no se considera la mejor solución (se desaconseja explícitamente en los documentos), pero es útil conocer:

import pandas
df = pandas.read_csv("test.csv")
df['FirstName'][df.ID == 103] = "Matt"
df['LastName'][df.ID == 103] = "Jones"
ely
fuente
16
¿ df.loc[df.ID == 103, ['FirstName', 'LastName']] = 'Matt', 'Jones'
Qué tal si agregas
2
-1 "Otra forma de hacerlo es usar lo que se llama asignación encadenada". No. Enfáticamente, no. Es solamente útil saber que la asignación encadenada no es fiable. No es que sea una solución confiable y no óptima, la situación es mucho peor . Incluso has reconocido esto en otra parte de Stack Overflow . Intente evitar dar la ilusión de que la asignación encadenada es una opción viable. Los dos primeros métodos que proporcionó fueron suficientes y son la forma preferida de hacerlo.
Phillip Cloud
9
Estoy en desacuerdo. No entiendo por qué persiste en tratar pedanteramente de afirmar que la asignación encadenada no es una forma viable. Reconocí que no se considera la forma preferida. Qué más quieres. Es absurdo actuar como si esto no fuera una forma de hacerlo. De hecho, en mi sistema ahora mismo (versión 0.8), es la forma correcta de hacerlo. No me interesan sus votos a favor si va a ocupar este puesto. Siéntase libre de señalar su punto con un voto negativo, pero ya he reflexionado sobre su punto y no estoy de acuerdo con él.
ely
11
El Internet es un negocio serio. En cualquier caso, EMS, aprecié saber que existe la opción.
Pársel
Un problema con el que puede encontrarse es que el csv tiene puntos / puntos en los nombres de las columnas y las asignaciones se arruinan. Puede arreglar las columnas usando algo como esto: cols = df.columns cols = cols.map (lambda x: x.replace ('.', '_') If isinstance (x, str) else x) df.columns = cols
ski_squaw
37

Puede usar map, puede mapear valores de un diccionario o incluso una función personalizada.

Suponga que este es su df:

    ID First_Name Last_Name
0  103          a         b
1  104          c         d

Crea los dictados:

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

Y mapa:

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

El resultado será:

    ID First_Name Last_Name
0  103       Matt     Jones
1  104         Mr         X

O use una función personalizada:

names = {103: ("Matt", "Jones"), 104: ("Mr", "X")}
df['First_Name'] = df['ID'].map(lambda x: names[x][0])
Rutger Kassies
fuente
2
¿No generará esto un KeyError si los valores no existen en su dictado?
EdChum
1
La función personalizada funcionará, las otras funcionarán de todos modos. Pero asumí que dictse creó para el mapeo. De lo contrario, se puede realizar una verificación / limpieza en función de algo como:df.ID.isin(names.keys())
Rutger Kassies
La función personalizada se puede expandir a cualquier función (no anónima).
user989762
14

La pregunta original aborda un caso de uso limitado específico. Para aquellos que necesitan respuestas más genéricas, aquí hay algunos ejemplos:

Creando una nueva columna usando datos de otras columnas

Dado el marco de datos a continuación:

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

A continuación, agregamos una nueva descriptioncolumna como una concatenación de otras columnas mediante la +operación que se reemplaza para la serie. El formato de cadena de fantasía, cadenas f, etc.no funcionarán aquí, ya que se +aplica a escalares y no 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

Obtenemos 1 yearspara el gato (en lugar de 1 year) que arreglaremos a continuación usando condicionales.

Modificar una columna existente con condicionales

Aquí estamos reemplazando la animalcolumna original con valores de otras columnas y usamos np.wherepara establecer una subcadena condicional basada en el 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

Modificar varias columnas con condicionales

Un enfoque más flexible es llamar .apply()a un marco de datos completo en lugar de a una sola columna:

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

En el código anterior, la transform_row(r)función toma un Seriesobjeto que representa una fila determinada (indicado por axis=1, el valor predeterminado de axis=0proporcionará un Seriesobjeto para cada columna). Esto simplifica el procesamiento ya que podemos acceder a los valores 'primitivos' reales en la fila usando los nombres de las columnas y tener visibilidad de otras celdas en la fila / columna dada.

ccpizza
fuente
1
Gracias por tomarse el tiempo para escribir una respuesta tan completa. Muy apreciado.
Parseltongue
Gracias por esta respuesta extremadamente útil. Un seguimiento: ¿qué pasa si queremos modificar una columna haciendo cálculos en la columna, en lugar de modificar una cadena? Por ejemplo, usando el ejemplo anterior, ¿qué pasa si queremos multiplicar la columna df.age por 7 si df.animal == 'dog'? ¡Gracias!
GbG
1
@GbG: np.wherees probablemente lo que está buscando, consulte, por ejemplo, stackoverflow.com/a/42540310/191246 pero también es posible que no pueda ajustar la lógica en una operación escalar, entonces necesitaría transformar explícitamente la celda numéricamente similar a cómo se hacetransform_row
ccpizza
¡Gracias @ccpizza! Justo lo que estaba buscando.
GbG
13

Esta pregunta aún podría ser visitada con suficiente frecuencia como para que valga la pena ofrecer un apéndice a la respuesta del Sr. Kassies. La dictclase incorporada se puede subclasificar para que se devuelva un valor predeterminado para las claves "faltantes". Este mecanismo funciona bien para los pandas. Pero mira a continuación.

De esta forma es posible evitar errores de claves.

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

Lo mismo se puede hacer de forma más sencilla de la siguiente manera. El uso del argumento 'predeterminado' para el getmétodo de un objeto dict hace que sea innecesario subclasificar un 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
fuente
1
esta es, con mucho, la mejor y más fácil respuesta que he visto, con un excelente manejo predeterminado. Gracias.
Brendan
@Brendan: ¡Oh! Muchas gracias.
Bill Bell