Pandas: создайте два новых столбца в фрейме данных со значениями, вычисленными из уже существующего столбца

100

Я работаю с библиотекой pandas и хочу добавить два новых столбца в фрейм данных dfс n столбцами (n> 0).
Эти новые столбцы являются результатом применения функции к одному из столбцов во фрейме данных.

Применяемая функция выглядит так:

def calculate(x):
    ...operate...
    return z, y

Один из способов создания нового столбца для функции, возвращающей только значение:

df['new_col']) = df['column_A'].map(a_function)

Итак, что я хочу и безуспешно пробовал (*), это что-то вроде:

(df['new_col_zetas'], df['new_col_ys']) = df['column_A'].map(calculate)

Каким может быть лучший способ добиться этого? Я просмотрел документацию, ничего не понимая.

** df['column_A'].map(calculate)возвращает серию панд для каждого элемента, состоящего из кортежа z, y. И попытка назначить это двум столбцам фрейма данных приводит к ошибке ValueError. *

Хоакин
источник

Ответы:

119

Я бы просто использовал zip:

In [1]: from pandas import *

In [2]: def calculate(x):
   ...:     return x*2, x*3
   ...: 

In [3]: df = DataFrame({'a': [1,2,3], 'b': [2,3,4]})

In [4]: df
Out[4]: 
   a  b
0  1  2
1  2  3
2  3  4

In [5]: df["A1"], df["A2"] = zip(*df["a"].map(calculate))

In [6]: df
Out[6]: 
   a  b  A1  A2
0  1  2   2   3
1  2  3   4   6
2  3  4   6   9
DSM
источник
Спасибо, отлично, работает. Я не нашел ничего подобного в документации по 0.8.1 ... Полагаю, я всегда должен думать о Series как о списках кортежей ...
Хоакин
Есть ли какая-то разница в производительности при этом? zip (* map (calculate, df ["a"])) вместо zip (* df ["a"]. map (calculate)), который также дает (как указано выше) [(2, 4, 6), ( 3, 6, 9)]?
ekta
1
При создании нового столбца я получаю следующее предупреждение: «SettingWithCopyWarning: значение пытается быть установлено на копии фрагмента из DataFrame. Попробуйте вместо этого использовать .loc [row_indexer, col_indexer] = value». Стоит ли мне об этом беспокоиться? pandas v.0.15
taras
47

На мой взгляд, главный ответ ошибочен. Надеюсь, никто не будет массово импортировать все панды в свое пространство имен с помощью from pandas import *. Кроме того, mapметод следует зарезервировать для тех случаев, когда ему передается словарь или серия. Он может выполнять функцию, но это то, applyдля чего используется.

Итак, если вы должны использовать вышеуказанный подход, я бы написал его так

df["A1"], df["A2"] = zip(*df["a"].apply(calculate))

На самом деле нет причин использовать здесь zip. Вы можете просто сделать это:

df["A1"], df["A2"] = calculate(df['a'])

Этот второй метод также намного быстрее на больших DataFrames.

df = pd.DataFrame({'a': [1,2,3] * 100000, 'b': [2,3,4] * 100000})

DataFrame создан из 300 000 строк

%timeit df["A1"], df["A2"] = calculate(df['a'])
2.65 ms ± 92.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

%timeit df["A1"], df["A2"] = zip(*df["a"].apply(calculate))
159 ms ± 5.24 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

В 60 раз быстрее, чем zip


В общем, избегайте использования apply

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

%%timeit
A1, A2 = [], []
for val in df['a']:
    A1.append(val**2)
    A2.append(val**3)

df['A1'] = A1
df['A2'] = A2

298 ms ± 7.14 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Таким образом, это вдвое медленнее, что не является ужасным падением производительности, но если мы цитонизируем вышесказанное, мы получим гораздо лучшую производительность. Предполагая, что вы используете ipython:

%load_ext cython

%%cython
cpdef power(vals):
    A1, A2 = [], []
    cdef double val
    for val in vals:
        A1.append(val**2)
        A2.append(val**3)

    return A1, A2

%timeit df['A1'], df['A2'] = power(df['a'])
72.7 ms ± 2.16 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

Непосредственное назначение без применения

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

%timeit df['A1'], df['A2'] = df['a'] ** 2, df['a'] ** 3
5.13 ms ± 320 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Это использует преимущества чрезвычайно быстрых векторизованных операций NumPy вместо наших циклов. Теперь у нас есть 30-кратное ускорение по сравнению с оригиналом.


Самый простой тест скорости с apply

Приведенный выше пример должен ясно показать, насколько медленным applyможет быть, но для большей ясности давайте рассмотрим самый простой пример. Возведем в квадрат серию из 10 миллионов чисел с применением и без

s = pd.Series(np.random.rand(10000000))

%timeit s.apply(calc)
3.3 s ± 57.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Без применения в 50 раз быстрее

%timeit s ** 2
66 ms ± 2 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Тед Петру
источник
1
Это действительно отличный ответ. Я хотел спросить: что вы думаете о applymapслучае, когда вам нужно реализовать определенную функцию для каждого элемента фрейма данных?
Дэвид
3
Хотя в этом ответе есть несколько хороших советов, я считаю, что основной совет, который следует использовать func(series)вместо, series.apply(func)применим только тогда, когда функция полностью определена с использованием операций, которые ведут себя одинаково как для отдельного значения, так и для серии. Так обстоит дело в примере в первом ответе, но не в вопросе OP, который в более общем плане спрашивает о применении функций к столбцам. 1/2
Грэм Ли
1
Например, если df is: DataFrame({'a': ['Aaron', 'Bert', 'Christopher'], 'b': ['Bold', 'Courageous', 'Distrusted']})and calcis: def calc(x): return x[0], len(x)then tdf.a.apply(calc))и calc(tdf.a)возвращает очень разные вещи.
Грэм Ли