Как мне работать с DataFrame с серией для каждого столбца

Цель и мотивация

Я видел такой вопрос несколько раз и видел много других вопросов, которые включают в себя некоторый элемент этого. Совсем недавно мне пришлось потратить немного времени на объяснение этой концепции в комментариях в поисках подходящих канонических вопросов и ответов. Я не нашел один, и поэтому я думал, что напишу один.

Этот вопрос обычно возникает в отношении конкретной операции, но в равной степени относится к большинству арифметических операций.

  • Как вычесть Series из каждого столбца в DataFrame?
  • Как добавить Series из каждого столбца в DataFrame?
  • Как мне умножить Series из каждого столбца в DataFrame?
  • Как разделить Series из каждого столбца в DataFrame?

Вопрос

Дан Series s и DataFrame df. Как мне работать с каждым столбцом df с помощью s?

df = pd.DataFrame(
    [[1, 2, 3], [4, 5, 6]],
    index=[0, 1],
    columns=['a', 'b', 'c']
)

s = pd.Series([3, 14], index=[0, 1])

Когда я пытаюсь добавить их, я получаю все np.nan

df + s

    a   b   c   0   1
0 NaN NaN NaN NaN NaN
1 NaN NaN NaN NaN NaN

То, что я думал, я должен получить,

    a   b   c
0   4   5   6
1  18  19  20

Ответ 1

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


Полезно создать мысленную модель объектов Series и DataFrame.

Анатомия Series

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

index

Это ключи, на которые мы можем ссылаться, чтобы получить соответствующие значения. Когда элементы индекса уникальны, сравнение со словарем становится очень близким.

values

Это соответствующие значения, которые указываются индексом.

Анатомия DataFrame

DataFrame следует рассматривать как словарь Series или Series Series. В этом случае ключи - это имена столбцов, а значения - сами столбцы как объекты Series. Каждая Series соглашается использовать один и тот же index который является индексом DataFrame.

columns

Это ключи, на которые мы можем ссылаться, чтобы получить в соответствующей Series.

index

Это индекс, который все значения Series согласны разделить.

Примечание: RE: columns и index объекты

Это такие же вещи. DataFrame index может быть использован в качестве еще одного DataFrame columns. На самом деле, это происходит, когда вы делаете df.T чтобы получить транспонирование.

values

Это двумерный массив, содержащий данные в DataFrame. Реальность такова, что values НЕ являются тем, что хранится внутри объекта DataFrame. (Ну, иногда это так, но я не собираюсь пытаться описать менеджер блоков). Дело в том, что лучше думать об этом как о доступе к двумерному массиву данных.


Определить пример данных

Это примеры объектов pandas.Index которые можно использовать в качестве index Series или DataFrame или в качестве columns DataFrame

idx_lower = pd.Index([*'abcde'], name='lower')
idx_range = pd.RangeIndex(5, name='range')

Это примеры объектов pandas.Series которые используют объекты pandas.Index выше.

s0 = pd.Series(range(10, 15), idx_lower)
s1 = pd.Series(range(30, 40, 2), idx_lower)
s2 = pd.Series(range(50, 10, -8), idx_range)

Это примеры объектов pandas.DataFrame которые используют объекты pandas.Index выше.

df0 = pd.DataFrame(100, index=idx_range, columns=idx_lower)
df1 = pd.DataFrame(
    np.arange(np.product(df0.shape)).reshape(df0.shape),
    index=idx_range, columns=idx_lower
)

Series по Series

При работе на двух Series выравнивание очевидно. Вы выравниваете index одной Series с index другой.

s1 + s0

lower
a    40
b    43
c    46
d    49
e    52
dtype: int64

Это то же самое, что когда я произвольно перетасовываю одну перед тем, как работать. Индексы все равно будут выравниваться.

s1 + s0.sample(frac=1)

lower
a    40
b    43
c    46
d    49
e    52
dtype: int64

И это НЕ тот случай, когда вместо этого я оперирую значениями перемешанной Series. В этом случае у Pandas нет index для выравнивания, и поэтому он работает с позиций.

s1 + s0.sample(frac=1).values

lower
a    42
b    42
c    47
d    50
e    49
dtype: int64

Добавьте скаляр

s1 + 1

lower
a    31
b    33
c    35
d    37
e    39
dtype: int64

DataFrame на DataFrame

Подобное верно при работе между двумя DataFrame
Выравнивание очевидно и делает то, что мы должны сделать

df0 + df1

lower    a    b    c    d    e
range                         
0      100  101  102  103  104
1      105  106  107  108  109
2      110  111  112  113  114
3      115  116  117  118  119
4      120  121  122  123  124

Перемешать второй DataFrame по обеим осям. index и columns все равно выровняются и дадут нам то же самое.

df0 + df1.sample(frac=1).sample(frac=1, axis=1)

lower    a    b    c    d    e
range                         
0      100  101  102  103  104
1      105  106  107  108  109
2      110  111  112  113  114
3      115  116  117  118  119
4      120  121  122  123  124

Та же перестановка, но добавьте массив, а не DataFrame. Больше не выравнивается и получит разные результаты.

df0 + df1.sample(frac=1).sample(frac=1, axis=1).values

lower    a    b    c    d    e
range                         
0      123  124  121  122  120
1      118  119  116  117  115
2      108  109  106  107  105
3      103  104  101  102  100
4      113  114  111  112  110

Добавьте одномерный массив. Выровняется по столбцам и транслируется по строкам.

df0 + [*range(2, df0.shape[1] + 2)]

lower    a    b    c    d    e
range                         
0      102  103  104  105  106
1      102  103  104  105  106
2      102  103  104  105  106
3      102  103  104  105  106
4      102  103  104  105  106

Добавьте скаляр. Нечего согласовывать с таким вещанием на все

df0 + 1

lower    a    b    c    d    e
range                         
0      101  101  101  101  101
1      101  101  101  101  101
2      101  101  101  101  101
3      101  101  101  101  101
4      101  101  101  101  101

DataFrame on Series

Если DataFrame следует рассматривать как словари Series и Series как словари значений, то естественно, что при работе между DataFrame и Series они должны быть выровнены по своим "ключам".

s0:
lower    a    b    c    d    e
        10   11   12   13   14

df0:
lower    a    b    c    d    e
range                         
0      100  100  100  100  100
1      100  100  100  100  100
2      100  100  100  100  100
3      100  100  100  100  100
4      100  100  100  100  100

И когда мы работаем, 10 в s0['a'] добавляется ко всему столбцу df0['a']

df0 + s0

lower    a    b    c    d    e
range                         
0      110  111  112  113  114
1      110  111  112  113  114
2      110  111  112  113  114
3      110  111  112  113  114
4      110  111  112  113  114

Суть вопроса и смысл поста

Что если я захочу s2 и df0?

s2:               df0:

             |    lower    a    b    c    d    e
range        |    range                         
0      50    |    0      100  100  100  100  100
1      42    |    1      100  100  100  100  100
2      34    |    2      100  100  100  100  100
3      26    |    3      100  100  100  100  100
4      18    |    4      100  100  100  100  100

Когда я работаю, я получаю все np.nan как указано в вопросе

df0 + s2

        a   b   c   d   e   0   1   2   3   4
range                                        
0     NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
1     NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
2     NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
3     NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
4     NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN

Это не производит то, что мы хотели. Потому что Панд выравнивает index s2 по columns df0. columns результата включают в себя объединение index s2 и columns df0.

Мы могли бы подделать это хитрым транспонированием

(df0.T + s2).T

lower    a    b    c    d    e
range                         
0      150  150  150  150  150
1      142  142  142  142  142
2      134  134  134  134  134
3      126  126  126  126  126
4      118  118  118  118  118

Но оказывается, что у Панд есть лучшее решение. Существуют методы работы, которые позволяют нам передавать аргумент axis чтобы указать ось для выравнивания.

- sub
+ add
* mul
/ div
** pow

И поэтому ответ просто

df0.add(s2, axis='index')

lower    a    b    c    d    e
range                         
0      150  150  150  150  150
1      142  142  142  142  142
2      134  134  134  134  134
3      126  126  126  126  126
4      118  118  118  118  118

Оказывается, axis='index' является синонимом axis=0.
Как axis='columns' синонимичны с axis=1

df0.add(s2, axis=0)

lower    a    b    c    d    e
range                         
0      150  150  150  150  150
1      142  142  142  142  142
2      134  134  134  134  134
3      126  126  126  126  126
4      118  118  118  118  118

Остальные операции

df0.sub(s2, axis=0)

lower   a   b   c   d   e
range                    
0      50  50  50  50  50
1      58  58  58  58  58
2      66  66  66  66  66
3      74  74  74  74  74
4      82  82  82  82  82

df0.mul(s2, axis=0)

lower     a     b     c     d     e
range                              
0      5000  5000  5000  5000  5000
1      4200  4200  4200  4200  4200
2      3400  3400  3400  3400  3400
3      2600  2600  2600  2600  2600
4      1800  1800  1800  1800  1800

df0.div(s2, axis=0)

lower         a         b         c         d         e
range                                                  
0      2.000000  2.000000  2.000000  2.000000  2.000000
1      2.380952  2.380952  2.380952  2.380952  2.380952
2      2.941176  2.941176  2.941176  2.941176  2.941176
3      3.846154  3.846154  3.846154  3.846154  3.846154
4      5.555556  5.555556  5.555556  5.555556  5.555556

df0.pow(1 / s2, axis=0)

lower         a         b         c         d         e
range                                                  
0      1.096478  1.096478  1.096478  1.096478  1.096478
1      1.115884  1.115884  1.115884  1.115884  1.115884
2      1.145048  1.145048  1.145048  1.145048  1.145048
3      1.193777  1.193777  1.193777  1.193777  1.193777
4      1.291550  1.291550  1.291550  1.291550  1.291550

Ответ 2

Я предпочитаю метод, упомянутый @piSquared (то есть df.add(s, axis = 0)), но другой метод использует apply вместе с lambda для выполнения действия над каждым столбцом в кадре данных:

>>>> df.apply(lambda col: col + s)
    a   b   c
0   4   5   6
1  18  19  20

Чтобы применить лямбда-функцию к строкам, используйте axis=1:

>>> df.T.apply(lambda row: row + s, axis=1)
   0   1
a  4  18
b  5  19
c  6  20

Этот метод может быть полезен, когда преобразование является более сложным, например:

df.apply(lambda col: 0.5 * col ** 2 + 2 * s - 3)