Конкатенация датафреймов вдоль оси

Поскольку мы работаем с таблицами, то выбор осей для их соединения невелик: либо таблицы соединяются вертикально, либо горизонтально. Но одно дело, когда ты работаешь с массивами индексы которых вдоль каждой из осей являются строго упорядоченными целыми числами (например массивы NumPy). И совсем другое дело когда помимо целочисленных индексов еще имеются метки элементов, как в датафреймах Pandas. Например, мы можем очень легко "склеить" два массива NumPy (при условии что их размеры вдоль осей являются согласованными):

arr1 = np.zeros((3, 2))
arr1
array([[0., 0.],
       [0., 0.],
       [0., 0.]])
arr2 = np.ones((3, 2))
arr2
array([[1., 1.],
       [1., 1.],
       [1., 1.]])
# Соединение массивов по вертикали:
np.concatenate((arr1, arr2), axis=0)
array([[0., 0.],
       [0., 0.],
       [0., 0.],
       [1., 1.],
       [1., 1.],
       [1., 1.]])
# Соединение массивов по горизонтали:
np.concatenate((arr1, arr2), axis=1)
array([[0., 0., 1., 1.],
       [0., 0., 1., 1.],
       [0., 0., 1., 1.]])

А теперь давайте превратим массивы arr1 и arr2 в следующие датафреймы:

df1 = pd.DataFrame(data=arr1,
                   index=list('ABC'),
                   columns=['val_1', 'val_2'])
df1
val_1 val_2
A 0.0 0.0
B 0.0 0.0
C 0.0 0.0
df2 = pd.DataFrame(data=arr2,
                   index=list('ABX'),
                   columns=['val_1', 'val_5'])
df2
val_1 val_5
A 1.0 1.0
B 1.0 1.0
X 1.0 1.0

После того как появились названия столбцов и метки строк, конкатенация уже не кажется такой однозначной, верно? Допустим мы склеиваем датафреймы по вертикали, но как быть со столбцами 'val_2' и 'val_5' ? Вдруг в этих столбцах находятся значения разных признаков? А если конкатенация выполняется по горизонтали, то что делать со строками 'C' и 'X' ? Ведь вполне возможно, что метки строк обозначают разные наблюдения.

Скорее всего, в голове проницательного читателя уже промелькнула мысль - "У нас же есть NaN-ы, благодаря им мы сможем избавиться от неоднозначности." и это действительно так:

pd.concat((df1, df2), axis=0)
val_1 val_2 val_5
A 0.0 0.0 NaN
B 0.0 0.0 NaN
C 0.0 0.0 NaN
A 1.0 NaN 1.0
B 1.0 NaN 1.0
X 1.0 NaN 1.0
pd.concat((df1, df2), axis=1)
val_1 val_2 val_1 val_5
A 0.0 0.0 1.0 1.0
B 0.0 0.0 1.0 1.0
C 0.0 0.0 NaN NaN
X NaN NaN 1.0 1.0

Функция concat() принимает список (кортеж) датафреймов (серий) и по умолчанию соединяет их вдоль первой (нулевой оси). Например, у нас есть следующие три серии:

s1 = pd.Series([0, 0], index=['A', 'B'])
s1
A    0
B    0
dtype: int64
s2 = pd.Series([9, 9, 9], index=['A', 'B', 'C'])
s2
A    9
B    9
C    9
dtype: int64
s3 = pd.Series(['x', 'y', 'z'], index=['B', 'C', 'D'])
s3
B    x
C    y
D    z
dtype: object

Мы можем "склеить" их по длине, т.е. воспринимать каждую из них как столбец и надставить одну серию на другую, в результате чего получим новую серию:

pd.concat([s1, s2, s3])    # axis=0 по умолчанию
A    0
B    0
A    9
B    9
C    9
B    x
C    y
D    z
dtype: object

А можем выстроить эти серии по горизонтали (по прежнему воспринимая каждую из них как столбец), в результате чего получим новый датафрейм:

pd.concat([s1, s2, s3], axis=1)
0 1 2
A 0.0 9.0 NaN
B 0.0 9.0 x
C NaN 9.0 y
D NaN NaN z

Обратите внимание, что по умолчанию выполняется внешнее соединение, т.е. в результат попадают все метки серий. Что бы сделать внутреннее соединение достаточно указать параметр join со значением 'inner':

pd.concat([s1, s2, s3], axis=1, join='inner')
0 1 2
B 0 9 x

Если мы хотим сохранить информацию о сериях из которых получен результирующий объект, то можем воспользоваться параметром keys в котором нужно перечислить желаемые метки в виде строк. Если серии соединяются вдоль нулевой оси, то указанные метки становятся мультииндексом:

pd.concat([s1, s2, s3], keys=['s1', 's2', 's3'])
s1  A    0
    B    0
s2  A    9
    B    9
    C    9
s3  B    x
    C    y
    D    z
dtype: object

А если соединение выполняется по оси столбцов (axis = 1), то указанные метки становятся названиями столбцов:

pd.concat([s1, s2, s3], axis=1, keys=['s1', 's2', 's3'])
s1 s2 s3
A 0.0 9.0 NaN
B 0.0 9.0 x
C NaN 9.0 y
D NaN NaN z

Добиться того же самого результата можно, если передать функции concat() словарь:

pd.concat({'s1': s1, 's2': s2, 's3': s3}, axis=1)
s1 s2 s3
A 0.0 9.0 NaN
B 0.0 9.0 x
C NaN 9.0 y
D NaN NaN z

Все вышесказанное так же применимо и к датафреймам:

df1 = pd.DataFrame(np.arange(4).reshape(2, 2),
                   index=['A', 'B'],
                   columns=['col_1', 'col_2'])

df2 = pd.DataFrame(np.arange(10, 41, 10).reshape(2, 2),
                   index=['B', 'C'],
                   columns=['col_2', 'col_3'])

df3 = pd.DataFrame(np.arange(111, 445, 111).reshape(2, 2),
                   index=['B', 'C'],
                   columns=['col_2', 'col_3'])
pd.concat([df1, df2, df3], keys=['data_1', 'data_2', 'data_3'])
col_1 col_2 col_3
data_1 A 0.0 1 NaN
B 2.0 3 NaN
data_2 B NaN 10 20.0
C NaN 30 40.0
data_3 B NaN 111 222.0
C NaN 333 444.0
pd.concat([df1, df2, df3], keys=['data_1', 'data_2', 'data_3'], axis=1)
data_1 data_2 data_3
col_1 col_2 col_2 col_3 col_2 col_3
A 0.0 1.0 NaN NaN NaN NaN
B 2.0 3.0 10.0 20.0 111.0 222.0
C NaN NaN 30.0 40.0 333.0 444.0

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

pd.concat([df1, df2, df3], ignore_index=True)
col_1 col_2 col_3
0 0.0 1 NaN
1 2.0 3 NaN
2 NaN 10 20.0
3 NaN 30 40.0
4 NaN 111 222.0
5 NaN 333 444.0
pd.concat([df1, df2, df3], axis=1, ignore_index=True)
0 1 2 3 4 5
A 0.0 1.0 NaN NaN NaN NaN
B 2.0 3.0 10.0 20.0 111.0 222.0
C NaN NaN 30.0 40.0 333.0 444.0