Agregue una nueva columna al marco de datos según el diccionario

23

Tengo un marco de datos y un diccionario. Necesito agregar una nueva columna al marco de datos y calcular sus valores en función del diccionario.

Aprendizaje automático, agregando nuevas características basadas en alguna tabla:

score = {(1, 45, 1, 1) : 4, (0, 1, 2, 1) : 5}
df = pd.DataFrame(data = {
    'gender' :      [1,  1,  0, 1,  1,  0,  0,  0,  1,  0],
    'age' :         [13, 45, 1, 45, 15, 16, 16, 16, 15, 15],
    'cholesterol' : [1,  2,  2, 1, 1, 1, 1, 1, 1, 1],
    'smoke' :       [0,  0,  1, 1, 7, 8, 3, 4, 4, 2]},
     dtype = np.int64)

print(df, '\n')
df['score'] = 0
df.score = score[(df.gender, df.age, df.cholesterol, df.smoke)]
print(df)

Espero el siguiente resultado:

   gender  age  cholesterol  smoke    score
0       1   13            1      0      0 
1       1   45            2      0      0
2       0    1            2      1      5
3       1   45            1      1      4
4       1   15            1      7      0
5       0   16            1      8      0
6       0   16            1      3      0
7       0   16            1      4      0
8       1   15            1      4      0
9       0   15            1      2      0
Mikola
fuente

Respuestas:

13

Dado que scorees un diccionario (por lo que las teclas son únicas) podemos usar la MultiIndexalineación

df = df.set_index(['gender', 'age', 'cholesterol', 'smoke'])
df['score'] = pd.Series(score)  # Assign values based on the tuple
df = df.fillna(0, downcast='infer').reset_index()  # Back to columns

   gender  age  cholesterol  smoke  score
0       1   13            1      0      0
1       1   45            2      0      0
2       0    1            2      1      5
3       1   45            1      1      4
4       1   15            1      7      0
5       0   16            1      8      0
6       0   16            1      3      0
7       0   16            1      4      0
8       1   15            1      4      0
9       0   15            1      2      0
ALollz
fuente
1
Buena de MultiIIndex. Alternativa: df['score'] =df.set_index(['gender', 'age', 'cholesterol', 'smoke']).index.map(score).fillna(0).to_numpy().
Quang Hoang
44
@ALollz, perdóname, me encantan tus respuestas, pero tengo que hablar cuando veo tantos votos a favor en una respuesta como esta. Esta respuesta es bien Y inteligente. Pero no es genial. Hay demasiadas partes móviles sin gran ganancia. En el proceso, ha creado una nueva dfvía set_index, un nuevo Seriesconstructor de vías. Aunque obtiene un beneficio de la alineación del índice cuando se lo asigna df['score']. Por último, fillna(0, downcast='infer')hace el trabajo, pero nadie debería preferir esta larga solución con la creación de muchos objetos pandas innecesariamente.
piRSquared
Nuevamente, disculpas, ustedes también tienen mi voto positivo, solo quiero guiar a la gente a respuestas más simples.
piRSquared
@piRSquared Fui a almorzar y me sorprendió que me llamara la atención cuando regresé. Estoy de acuerdo en que es un poco complicado hacer todo lo que un simple mergepodría lograr. Pensé que esa respuesta se publicaría rápidamente, así que opté por una alternativa y, por alguna razón, tenía MultiIndices en mi mente. Estoy de acuerdo, esta probablemente no debería ser la respuesta aceptada, así que espero que eso no suceda.
ALollz
1
Oh estoy contigo He respondido lo mismo muchas veces. Estoy haciendo todo lo posible para servir a la comunidad (-: Confío en que
entiendas
7

Utilizando assignuna comprensión de lista, obteniendo una tupla de valores (cada fila) del scorediccionario, por defecto a cero si no se encuentra.

>>> df.assign(score=[score.get(tuple(row), 0) for row in df.values])
   gender  age  cholesterol  smoke  score
0       1   13            1      0      0
1       1   45            2      0      0
2       0    1            2      1      5
3       1   45            1      1      4
4       1   15            1      7      0
5       0   16            1      8      0
6       0   16            1      3      0
7       0   16            1      4      0
8       1   15            1      4      0
9       0   15            1      2      0

Tiempos

Dada la variedad de enfoques, pensé que sería interesante comparar algunos de los tiempos.

# Initial dataframe 100k rows (10 rows of identical data replicated 10k times).
df = pd.DataFrame(data = {
    'gender' :      [1,  1,  0, 1,  1,  0,  0,  0,  1,  0] * 10000,
    'age' :         [13, 45, 1, 45, 15, 16, 16, 16, 15, 15] * 10000,
    'cholesterol' : [1,  2,  2, 1, 1, 1, 1, 1, 1, 1] * 10000,
    'smoke' :       [0,  0,  1, 1, 7, 8, 3, 4, 4, 2] * 10000},
     dtype = np.int64)

%timeit -n 10 df.assign(score=[score.get(tuple(v), 0) for v in df.values])
# 223 ms ± 9.28 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

%%timeit -n 10 
df.assign(score=[score.get(t, 0) for t in zip(*map(df.get, df))])
# 76.8 ms ± 2.8 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

%%timeit -n 10
df.assign(score=[score.get(v, 0) for v in df.itertuples(index=False)])
# 113 ms ± 2.58 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

%timeit -n 10 df.assign(score=df.apply(lambda x: score.get(tuple(x), 0), axis=1))
# 1.84 s ± 77.3 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

%%timeit -n 10
(df
 .set_index(['gender', 'age', 'cholesterol', 'smoke'])
 .assign(score=pd.Series(score))
 .fillna(0, downcast='infer')
 .reset_index()
)
# 138 ms ± 11.5 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

%%timeit -n 10
s=pd.Series(score)
s.index.names=['gender','age','cholesterol','smoke']
df.merge(s.to_frame('score').reset_index(),how='left').fillna(0).astype(int)
# 24 ms ± 2.27 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

%%timeit -n 10
df.assign(score=pd.Series(zip(df.gender, df.age, df.cholesterol, df.smoke))
                .map(score)
                .fillna(0)
                .astype(int))
# 191 ms ± 7.54 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

%%timeit -n 10
df.assign(score=df[['gender', 'age', 'cholesterol', 'smoke']]
                .apply(tuple, axis=1)
                .map(score)
                .fillna(0))
# 1.95 s ± 134 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Alejandro
fuente
Mi favorito por un momento. Sin embargo, solo para asegurarme de que todo sigue siendo el tipo deseado cuando proceso score.get, usaría itertupleso zip(*map(df.get, df))... Para reiterar, este es mi enfoque preferido.
piRSquared
1
df.assign(score=[score.get(t, 0) for t in zip(*map(df.get, df))])
piRSquared
1
Por último, la mayor parte de lo que estoy escribiendo es fanfarrón porque el hash de 1.0es el mismo que el hash, por 1lo que las búsquedas de tuplas deberían dar como resultado la misma respuesta. Disculpas @Alexander por tantos comentarios sobre esto, pero solo quiero que las personas voten más por esto porque ... deberían (-:
piRSquared
1
Mientras estés cronometrando, mira mi sugerencia. Hay ocasiones en que .valueses caro
piRSquared el
1
@AndyL. incluso puede controlar qué columnas y en qué orden: zip(*map(df.get, ['col2', 'col1', 'col5']))u obtener tuplas de una modificación de df:zip(*map(df.eq(1).get, df))
piRSquared
4

Podrías usar map , ya que score es un diccionario:

df['score'] = df[['gender', 'age', 'cholesterol', 'smoke']].apply(tuple, axis=1).map(score).fillna(0)
print(df)

Salida

   gender  age  cholesterol  smoke  score
0       1   13            1      0    0.0
1       1   45            2      0    0.0
2       0    1            2      1    5.0
3       1   45            1      1    4.0
4       1   15            1      7    0.0
5       0   16            1      8    0.0
6       0   16            1      3    0.0
7       0   16            1      4    0.0
8       1   15            1      4    0.0
9       0   15            1      2    0.0

Como alternativa, podría utilizar una lista de comprensión:

df['score'] = [score.get(t, 0) for t in zip(df.gender, df.age, df.cholesterol, df.smoke)]
print(df)
Dani Mesejo
fuente
Me gustaría extender mi pregunta. Realmente necesito agregar una base de columna en el rango del valor de la columna. Por ejemplo, si 40 <edad <50, entonces puntuación = 4, etc ... Ahora el diccionario asigna un valor exacto. Lo mismo y para otras claves ...
Mikola
1
Agregue un ejemplo de lo que realmente quiere
Dani Mesejo el
Ejemplo simple: # Aquí 40 y 50, 10 y 20 son rangos de edad para los que debería usar puntaje = 4 (o 5) puntaje = {(1, 40, 50, 1, 1): 4, (0, 10, 20 , 1, 3): 5}
Mikola
@Mikola Entonces, si género = 1 y 40 <edad <50 y así sucesivamente ...
Dani Mesejo
1
@Mikola Deberías informar a todos, aunque en este punto creo que es mejor si haces otra pregunta.
Dani Mesejo
4

Lista de comprensión y mapa:

df['score'] = (pd.Series(zip(df.gender, df.age, df.cholesterol, df.smoke))
               .map(score)
               .fillna(0)
               .astype(int)
              )

Salida:

   gender  age  cholesterol  smoke  score
0       1   13            1      0      0
1       1   45            2      0      0
2       0    1            2      1      5
3       1   45            1      1      4
4       1   15            1      7      0
5       0   16            1      8      0
6       0   16            1      3      0
7       0   16            1      4      0
8       1   15            1      4      0
9       0   15            1      2      0
9       0   15            1      2    0.0
Quang Hoang
fuente
4

reindex

df['socre']=pd.Series(score).reindex(pd.MultiIndex.from_frame(df),fill_value=0).values
df
Out[173]: 
   gender  age  cholesterol  smoke  socre
0       1   13            1      0      0
1       1   45            2      0      0
2       0    1            2      1      5
3       1   45            1      1      4
4       1   15            1      7      0
5       0   16            1      8      0
6       0   16            1      3      0
7       0   16            1      4      0
8       1   15            1      4      0
9       0   15            1      2      0

O merge

s=pd.Series(score)
s.index.names=['gender','age','cholesterol','smoke']
df=df.merge(s.to_frame('score').reset_index(),how='left').fillna(0)
Out[166]: 
   gender  age  cholesterol  smoke  score
0       1   13            1      0    0.0
1       1   45            2      0    0.0
2       0    1            2      1    5.0
3       1   45            1      1    4.0
4       1   15            1      7    0.0
5       0   16            1      8    0.0
6       0   16            1      3    0.0
7       0   16            1      4    0.0
8       1   15            1      4    0.0
9       0   15            1      2    0.0
YOBEN_S
fuente
2

Puede ser otra forma estaría usando .loc[]:

m=df.set_index(df.columns.tolist())
m.loc[list(score.keys())].assign(
           score=score.values()).reindex(m.index,fill_value=0).reset_index()

   gender  age  cholesterol  smoke  score
0       1   13            1      0      0
1       1   45            2      0      0
2       0    1            2      1      5
3       1   45            1      1      4
4       1   15            1      7      0
5       0   16            1      8      0
6       0   16            1      3      0
7       0   16            1      4      0
8       1   15            1      4      0
9       0   15            1      2      0
anky
fuente
2

Solución simple de una línea, uso gety tuplefila-sabio,

df['score'] = df.apply(lambda x: score.get(tuple(x), 0), axis=1)

La solución anterior supone que no hay columnas distintas de las deseadas en orden. Si no, solo use columnas

cols = ['gender','age','cholesterol','smoke']
df['score'] = df[cols].apply(lambda x: score.get(tuple(x), 0), axis=1)
Vishnudev
fuente
El uso de score.getes bueno. Sin embargo, debería preferir una comprensión, en mi opinión. Ver los horarios de @ Alexander .
piRSquared
Ok @piSquared. Tendré eso en mente.
Vishnudev