Adicionar nova coluna ao quadro de dados com base no dicionário

23

Eu tenho um quadro de dados e um dicionário. Preciso adicionar uma nova coluna ao quadro de dados e calcular seus valores com base no dicionário.

Aprendizado de máquina, adicionando novo recurso com base em alguma tabela:

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 a seguinte saída:

   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
fonte

Respostas:

13

Como scoreé um dicionário (portanto, as teclas são únicas), podemos usar o MultiIndexalinhamento

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
fonte
11
Nice um dos MultiIIndex. Alternativa: df['score'] =df.set_index(['gender', 'age', 'cholesterol', 'smoke']).index.map(score).fillna(0).to_numpy().
Quang Hoang
4
@ALollz, perdoe-me, eu amo suas respostas, mas tenho que falar quando vejo tantos votos positivos em uma resposta como esta. Esta resposta é boa e inteligente. Mas isso não é ótimo. Existem muitas partes móveis sem grande ganho. No processo, você criou uma nova dfvia set_index, um novo Seriesconstrutor de via. Embora você tenha o benefício do alinhamento de índice ao atribuí-lo df['score']. Por fim, fillna(0, downcast='infer')faz o trabalho, mas ninguém deve preferir esta solução demorada com a criação de muitos objetos pandas desnecessariamente.
piRSquared
Mais uma vez, desculpas, você também recebeu meu voto positivo, só quero orientar as pessoas para respostas mais simples.
piRSquared
@piRSquared Fui almoçar e fiquei surpreso com a atenção que recebi quando voltei. Concordo que é um pouco complicado fazer algo que um simples mergepoderia realizar. Achei que a resposta seria publicada rapidamente, então optei por uma alternativa e, por algum motivo, tinha MultiIndices em minha mente. Concordo que esta provavelmente não deve ser a resposta aceita, por isso espero que isso não aconteça.
ALollz 29/10/19
11
Oh, eu estou com você. Já respondi o mesmo muitas vezes. Estou apenas fazendo o possível para servir a comunidade (-: confio em que você entenda minha intenção.
piRSquared
7

Usando assigncom uma compreensão de lista, obtendo uma tupla de valores (cada linha) do scoredicionário, padronizando para zero se não for encontrado.

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

Horários

Dada a variedade de abordagens, pensei que seria interessante comparar alguns dos horários.

# 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)
Alexander
fonte
Meu favorito um pouco. No entanto, apenas para garantir que tudo permaneça o tipo pretendido ao processar através do score.getuso itertuplesou zip(*map(df.get, df))... Para reiterar, essa é minha abordagem preferida.
piRSquared
11
df.assign(score=[score.get(t, 0) for t in zip(*map(df.get, df))])
piRSquared
11
Por fim, a maior parte do que estou escrevendo é arrogante, pois o hash de 1.0é o mesmo que o hash, 1portanto, as pesquisas de tuplas devem resultar na mesma resposta, independentemente. Desculpas @Alexander por tantos comentários sobre isso, mas eu só quero que as pessoas votem mais nisso porque ... elas deveriam (-:
piRSquared
11
Enquanto você estiver na hora certa, olhe para a minha sugestão. Há ocasiões em que .valuesé caro
piRSquared
11
@AndyL. você pode até controlar quais colunas e em qual ordem: zip(*map(df.get, ['col2', 'col1', 'col5']))ou obter tuplas de uma modificação de df:zip(*map(df.eq(1).get, df))
piRSquared em 29/10/19
4

Você pode usar o mapa , já que score é um dicionário:

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

Resultado

   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, você pode usar uma compreensão de lista:

df['score'] = [score.get(t, 0) for t in zip(df.gender, df.age, df.cholesterol, df.smoke)]
print(df)
Dani Mesejo
fonte
Eu gostaria de estender minha pergunta. Realmente preciso adicionar a base da coluna no intervalo do valor da coluna. Por exemplo, se 40 <idade <50, então pontuação = 4 etc ... Agora o dicionário mapeia exatamente algum valor. Mesmos verdadeiros e para outras teclas ....
Mikola
11
Adicionar um exemplo do que você realmente quer
Dani Mesejo
Exemplo simples: # Aqui 40 e 50, 10 e 20 são faixas etárias para as quais devo usar score = 4 (ou 5) score = {(1, 40, 50, 1, 1): 4, (0, 10, 20 , 1, 3): 5}
Mikola
@Mikola Então, se gender = 1 e 40 <idade <50 e assim por diante ...
Dani Mesejo
11
@ Mikola Você deve informar todos os corpos, embora neste momento eu acredite que é melhor se você fizer outra pergunta.
Dani Mesejo 29/10/19
4

Lista de compreensão e mapa:

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

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
9       0   15            1      2    0.0
Quang Hoang
fonte
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

Ou 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
fonte
2

Pode ser outra maneira seria 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
fonte
2

Solução simples de uma linha, Use gete em tuplelinha,

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

A solução acima está assumindo que não há outras colunas além das desejadas em ordem. Caso contrário, use apenas colunas

cols = ['gender','age','cholesterol','smoke']
df['score'] = df[cols].apply(lambda x: score.get(tuple(x), 0), axis=1)
Vishnudev
fonte
O uso de score.geté bom. No entanto, você deve preferir uma compreensão, na minha opinião. Veja os horários do @ Alexander .
piRSquared
Ok @piSquared. Manteremos isso em mente.
Vishnudev 29/10/19