Добавить новый столбец в фрейм данных на основе словаря

23

У меня есть датафрейм и словарь. Мне нужно добавить новый столбец в фрейм данных и рассчитать его значения на основе словаря.

Машинное обучение, добавление новой функции на основе некоторой таблицы:

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)

Я ожидаю следующий вывод:

   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
Микола
источник

Ответы:

13

Поскольку scoreэто словарь (поэтому ключи уникальны), мы можем использовать MultiIndexвыравнивание

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
источник
1
Хороший из MultiIIndex. Альтернатива: df['score'] =df.set_index(['gender', 'age', 'cholesterol', 'smoke']).index.map(score).fillna(0).to_numpy().
Куанг Хоанг
4
@ ALollz, прости меня, мне нравятся твои ответы, но я должен говорить, когда вижу так много откликов на такой ответ. Этот ответ хорошо и умно. Но это не здорово. Слишком много движущихся частей для большого выигрыша. В процессе вы создали новый dfvia set_index, новый Seriesконструктор via. Хотя вы получаете преимущество выравнивания индекса, когда назначаете его df['score']. Наконец, fillna(0, downcast='infer')работа выполнена, но никто не должен предпочитать это длинное решение с ненужным созданием множества объектов панд.
piRSquared
Опять же, извинения, у вас есть и мое возражение, я просто хочу помочь людям найти более простые ответы.
piRSquared
@piRSquared Я пошла на ланч и была удивлена, что привлекла к себе внимание, когда вернулась. Я согласен, что все немного запутанно делать что-то, что mergeможет сделать простое . Я подумал, что этот ответ будет опубликован быстро, поэтому я выбрал альтернативу, и по некоторым причинам у меня на уме были MultiIndices. Я согласен, это, вероятно, не должен быть принятый ответ, так что, надеюсь, этого не произойдет.
ALollz
1
О, я с тобой. Я отвечал так же много раз. Я просто делаю все возможное, чтобы служить сообществу (-: я верю, что вы поймете мое намерение.
piRSquared
7

Использование assignс пониманием списка, получение набора значений (каждой строки) из scoreсловаря, по умолчанию равным нулю, если не найден.

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

Задержки

Учитывая разнообразие подходов, мне было бы интересно сравнить некоторые моменты.

# 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)
Александр
источник
Мой любимый немного. Тем не менее, просто чтобы убедиться, что все остается намеченным типом при обработке через score.getя бы использовал itertuplesили zip(*map(df.get, df))... Повторюсь, это мой предпочтительный подход.
piRSquared
1
df.assign(score=[score.get(t, 0) for t in zip(*map(df.get, df))])
piRSquared
1
Наконец, большая часть того, что я пишу, является бластерной, потому что хэш 1.0такой же, как и хэш, 1поэтому поиск кортежей должен приводить к одному и тому же ответу независимо от этого. Извинения @Alexander за столько комментариев по этому поводу, но я просто хочу, чтобы люди высказывались об этом больше, потому что ... они должны (-:
piRSquared
1
Пока вы рассчитываете время, посмотрите на мое предложение. Есть случаи, когда .valuesэто дорого
piRSquared
1
@AndyL. Вы можете даже контролировать, какие столбцы и в каком порядке: zip(*map(df.get, ['col2', 'col1', 'col5']))или получить кортежи модификации df:zip(*map(df.eq(1).get, df))
piRSquared
4

Вы можете использовать карту , так как счет является словарем:

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

Вывод

   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

В качестве альтернативы вы можете использовать понимание списка:

df['score'] = [score.get(t, 0) for t in zip(df.gender, df.age, df.cholesterol, df.smoke)]
print(df)
Дани Месехо
источник
Я хотел бы расширить свой вопрос. На самом деле мне нужно добавить базу столбцов на диапазон значений столбца. Например, если 40 <возраст <50, тогда оценка = 4 и т. Д. Теперь словарь отображает точное какое-то значение. То же самое верно и для других ключей ....
Микола
1
Добавьте пример того, что вы действительно хотите
Дани Месехо
Простой пример: # Здесь 40 и 50, 10 и 20 - возрастной диапазон, для которого я должен использовать счет = 4 (или 5) счет = {(1, 40, 50, 1, 1): 4, (0, 10, 20 , 1, 3): 5}
Микола
@Mikola Так, если пол = 1 и 40 <возраст <50 и так далее ...
Дани Месехо
1
@Mikola Вы должны дать знать каждому телу, хотя на данный момент я считаю, что лучше, если вы зададите другой вопрос.
Дани Месехо
4

Понимание списка и карта:

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

Вывод:

   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
Куанг Хоанг
источник
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

Или 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
источник
2

Может быть, другой способ будет использовать .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
Анки
источник
2

Простое однострочное решение, Использование getи построчно tuple,

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

Вышеупомянутое решение предполагает, что нет никаких столбцов, кроме желаемых в порядке. Если нет, просто используйте столбцы

cols = ['gender','age','cholesterol','smoke']
df['score'] = df[cols].apply(lambda x: score.get(tuple(x), 0), axis=1)
Vishnudev
источник
Использование score.getэто хорошо. Тем не менее, вы должны предпочесть понимание, на мой взгляд. Смотрите @ Александр времени.
piRSquared
Хорошо @piSquared. Будет иметь это в виду.
Вишнудев