Структуры данных

Основными рабочими структурами данных в Pandas являются объекты Series и DataFrame, но прежде чем переходить к их изучению, давайте сделаем необходимые импорты:

import numpy as np
import pandas as pd

Series

Series - это структура данных, которая сочетает свойства одномерного массивы NumPy и словаря Python, т.е. доступ к каждому элементу может быть получен, либо с помощью индекса, либо с помощью идентификатора (ключа). Создать объект Series можно с помощью следующей команды:

s = pd.Series(data, index=index_data)

Аргументом data может быть массив NumPy, словарь Python или скалярное значение. Аргумент index не является обязательным и может быть последовательностью, содержащей идентификаторы каждого элемента в data. Если index не указан, то он будет создан автоматически, а его значения будут целыми числами из интервала [0, 1, 2, ..., len(data) - 1], т.е. будут совпадать с индексами одномерного массива.

Очень часто объекты Series называют, просто - сериями, аргумент data - данными, а index - индексом.

Давайте создадим серию из массива NumPy:

s = pd.Series(np.random.randint(-5, 6, 10),
              index=list('abcdefghij'))
s
a    5
b    0
c    5
d    5
e    1
f   -2
g    1
h    2
i    4
j   -3
dtype: int32

Атрибут .index позволяет взглянуть на идентификаторы элементов (их иногда еще называют метками элементов):

s.index
Index(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'], dtype='object')

Если индекс не указан, как здесь:

s = pd.Series(np.random.randint(-5, 6, 10))
s
0    1
1    3
2   -5
3    2
4    3
5    3
6    0
7   -1
8    4
9   -1
dtype: int32

то он будет создан автоматически, а его значения будут совпадать с значениями индексов массива data

s.index
RangeIndex(start=0, stop=10, step=1)

Важно отметить, что элементы индекса могут иметь одинаковые значения:

s = pd.Series(np.random.randint(-5, 6, 10),
              index=['a']*5 + ['b']*5)
s
a    5
a    0
a   -3
a   -4
a   -4
b    1
b    3
b   -5
b   -2
b   -3
dtype: int32

Серии также могут быть созданы из словарей:

data = {i: np.random.randint(10) for i in 'abcdefghij'}
data
{'a': 5,
 'b': 5,
 'c': 6,
 'd': 5,
 'e': 8,
 'f': 5,
 'g': 2,
 'h': 2,
 'i': 1,
 'j': 4}
s = pd.Series(data)
s
a    5
b    5
c    6
d    5
e    8
f    5
g    2
h    2
i    1
j    4
dtype: int64

Если вместе со словарем указан еще и индекс, то из данных будут извлечены только те элементы, ключи которых присутствуют в index:

s = pd.Series(data, index=list('facdxyz'))
s
f    5.0
a    5.0
c    6.0
d    5.0
x    NaN
y    NaN
z    NaN
dtype: float64

Для ключей, которых нет в data, но есть в index создаются значения NaN, которые обозначают отсутствующие элементы.

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

s = pd.Series(777, index=list('abcdefghij'))
s
a    777
b    777
c    777
d    777
e    777
f    777
g    777
h    777
i    777
j    777
dtype: int64

Серии являются массиво-подобными объектами, поэтому они могут быть переданы в качестве аргументов, практически во все функции NumPy и в этом ракурсе ведут себя практически точно так же как и array объекты:

s = pd.Series(np.random.randint(-5, 6, 10),
              index=list('abcdefghij'))
s
a    3
b    2
c    2
d    0
e    1
f    2
g    1
h   -1
i   -5
j   -2
dtype: int32

Например, вот обычное извлечение разреженного среза (среза с заданным шагом):

s[::2]
a    3
c    2
e    1
g    1
i   -5
dtype: int32

Результат булевой индексации (применения логической маски):

s[s < s.mean()]
d    0
h   -1
i   -5
j   -2
dtype: int32

Извлечение элементов с помощью массива индексов:

s[[0, -1, 1, -2]]
a    3
j   -2
b    2
i   -5
dtype: int32

Применение функции NumPy к серии и выполнение векторизованной арифметической операции:

np.diff(s)**2
array([ 1,  0,  4,  1,  1,  1,  4, 16,  9], dtype=int32)

При этом серии подобны словарю в том смысле, что доступ к элементам может быть выполнен по идентификаторам (меткам) в index:

s['d']
0
s['d'] = 7777
s
a       3
b       2
c       2
d    7777
e       1
f       2
g       1
h      -1
i      -5
j      -2
dtype: int32
'd' in s
True
s.keys(), s.values
(Index(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'], dtype='object'),
 array([   3,    2,    2, 7777,    1,    2,    1,   -1,   -5,   -2]))

Как и в NumPy, все операции над сериями векторизованы, но все они выполняются с выравниванием индекса:

s1 = pd.Series(np.arange(5),
               index=list('abcde'))
s2 = pd.Series(np.arange(5, 10),
               index=list('acexy'))
s1, s2
(a    0
 b    1
 c    2
 d    3
 e    4
 dtype: int32,
 a    5
 c    6
 e    7
 x    8
 y    9
 dtype: int32)
s1 + s2
a     5.0
b     NaN
c     8.0
d     NaN
e    11.0
x     NaN
y     NaN
dtype: float64

Выравнивание означает, что операции выполняются только над элементами с одинаковым индексом, остальные индексы попадают в результат со значениями NaN.

Зачастую, данные в сериях являются значениями некоторой переменной из конкретной прикладной области, поэтому полезно давать сериям осмысленные названия:

s = pd.Series(np.arange(5),
               index=list('abcde'),
               name='x_values')
s
a    0
b    1
c    2
d    3
e    4
Name: x_values, dtype: int32

Сменить название можно с помощью метода rename():

s.rename('y_values')
a    0
b    1
c    2
d    3
e    4
Name: y_values, dtype: int32

DataFrame

DataFrame - это объект, который сочетает свойства структурированных массивов NumPy и словарей Python. Самая простая аналогия для DataFrame - это таблица, столбцами которой являются объекты Series. Обычно, объекты DataFrame называют, просто - датафреймами.

Создать датафрейм можно несколькими способами, например из словаря, значениями элементов которого являются объекты Series, а ключами - строки с названиями будующих столбцов:

# создаем словарь где ключ - это имя столбца,
# а значение - это объект Series:
data = {'first_column': pd.Series(range(5), index=list('edcba')),
        'second_column': pd.Series(range(5, 10), index=list('gfedc')),
        'third_column': pd.Series(range(2, 7), index=list('acdfg'))}

# создаем датафрейм:
df = pd.DataFrame(data)
df
first_column second_column third_column
a 4.0 NaN 2.0
b 3.0 NaN NaN
c 2.0 9.0 3.0
d 1.0 8.0 4.0
e 0.0 7.0 NaN
f NaN 6.0 5.0
g NaN 5.0 6.0

Можно указать необходимые индексы и столбцы:

pd.DataFrame(data,
             # указываем нужные индексы:
             index=['c', 'd']) 
first_column second_column third_column
c 2 5 3
d 3 6 4
pd.DataFrame(data,
             # указываем нужные индексы:
             index=['c', 'd', 'f', 'g', 'z'],
             # указываем необходимые столбцы:
             columns=['second_column',
                      'third_column',
                      'fourth_column'])
second_column third_column fourth_column
c 5.0 3.0 NaN
d 6.0 4.0 NaN
f 8.0 5.0 NaN
g 9.0 6.0 NaN
z NaN NaN NaN

Датафрейм может быть создан из структурированного массива NumPy:

data = np.array([('a', 11, .9),
                 ('b', 22, .8),
                 ('c', 33, 0.7)],
                dtype=[('col_1', 'U1'),
                       ('col_2', 'i2'),
                       ('col_3', 'f2')])
pd.DataFrame(data)
col_1 col_2 col_3
0 a 11 0.899902
1 b 22 0.799805
2 c 33 0.700195
pd.DataFrame(data, index=['one', 'two', 'three'])
col_1 col_2 col_3
one a 11 0.899902
two b 22 0.799805
three c 33 0.700195
pd.DataFrame(data,
             index=['one', 'two', 'three'],
             columns=['col_2', 'col_1', 'col_4'])
col_2 col_1 col_4
one 11 a NaN
two 22 b NaN
three 33 c NaN

Датафрейм может быть создан из списка словарей:

data = [{'a': 11, 'b': 22},
        {'b': 33, 'c': 44},
        {'a': 55, 'c': 66}]

pd.DataFrame(data,
             index=['one', 'two', 'three'],
             columns=['c', 'a', 'b'])
c a b
one NaN 11.0 22.0
two 44.0 NaN 33.0
three 66.0 55.0 NaN

Датафрейм может быть создан из словаря словарей:

data = {'X': {'a': 1, 'b': 2},
        'Y': {'a': 11, 'c': 22}}

pd.DataFrame(data)
X Y
a 1.0 11.0
b 2.0 NaN
c NaN 22.0

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

data = {('group_1', 'x'):{('i', 'm'): 11, ('i', 'n'): 22,
                          ('j', 'm'): 11, ('j', 'n'): 22},
        ('group_1', 'y'):{('i', 'm'): 33, ('i', 'n'): 44,
                          ('j', 'm'): 33, ('j', 'n'): 44},
        ('group_1', 'z'):{('i', 'm'): 55, ('i', 'n'): 66,
                          ('j', 'm'): 55, ('j', 'n'): 66},
        ('group_2', 'x'):{('i', 'm'): 11, ('i', 'n'): 22,
                          ('j', 'm'): 11, ('j', 'n'): 22},
        ('group_2', 'y'):{('i', 'm'): 33, ('i', 'n'): 44,
                          ('j', 'm'): 33, ('j', 'n'): 44},
        ('group_2', 'z'):{('i', 'm'): 55, ('i', 'n'): 66,
                          ('j', 'm'): 55, ('j', 'n'): 66}}

pd.DataFrame(data)
group_1 group_2
x y z x y z
i m 11 33 55 11 33 55
n 22 44 66 22 44 66
j m 11 33 55 11 33 55
n 22 44 66 22 44 66

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

df = pd.DataFrame(np.random.randint(0, 10, size=(5, 2)),
                  columns=['col_1', 'col_2'])
df
col_1 col_2
0 9 3
1 9 5
2 5 1
3 6 5
4 3 8

Например, добавить новый столбец в датафрейм можно точно так же как и новый элемент в словарь, т.е. просто указать ключ и присвоить ему новую серию:

df['col_3'] = df['col_1']*100
df
col_1 col_2 col_3
0 9 3 900
1 9 5 900
2 5 1 500
3 6 5 600
4 3 8 300

Наверное вы обратили внимание на то, что новыми столбцами датафрейма могут быть результаты вычислений над имеющимися столбцами. В примере выше мы взяли столбец col_1, умножили его на 100, а результат присвоили новому столбцу, который стал частью исходного датафрейма. Это очень удобно и мы можем этим успешно и часто пользоваться. А в самих вычислениях может учавствовать несколько столбцов, например, мы можем выполнить сравнение значений нескольких столбцов, а результат добавить в тот же самый датафрейм:

df['col_4'] = df['col_1'] > df['col_2']
df
col_1 col_2 col_3 col_4
0 9 3 900 True
1 9 5 900 True
2 5 1 500 True
3 6 5 600 True
4 3 8 300 False

Удалить столбец можно с помощью, уже знакомого по словарям, метода pop():

df.pop('col_1')
0    0
1    1
2    8
3    8
4    1
Name: col_1, dtype: int32

Данный метод удаляет указанный элемент, но как вы обратили внимание, возвращает его значение в виде серии. Тем не менее, столбец действительно удалился:

df
col_2 col_3 col_4
0 3 900 True
1 5 900 True
2 1 500 True
3 5 600 True
4 8 300 False

Вставить столбец в нужное место можно с помощью метода insert:

df.insert(0,         # необходимое положение нового столбца
          'col_1',   # название нового столбца
          
          # содержимое нового столбца:
          df['col_2']/df['col_3'])
df
col_1 col_2 col_3 col_4
0 0.003333 3 900 True
1 0.005556 5 900 True
2 0.002000 1 500 True
3 0.008333 5 600 True
4 0.026667 8 300 False

Обратите внимание, как я вставляю комментарии в код. Так лучше не делать. Но делаю я это для того что бы было проще понять код. По хорошему, если вы выполняете примеры, то просто удалите все, что после #. Комментарии - это классно, но лучше не вносить их, так как это делаю я, а выделять им отдельное место, рядом с конструкциями языка.

Если новому столбцу присваивается скалярное значение, то им будет заполнен весь столбец:

df['col_5'] = 10
print(df)
col_1 col_2 col_3 col_4 col_5
0 0.003333 3 900 True 10
1 0.005556 5 900 True 10
2 0.002000 1 500 True 10
3 0.008333 5 600 True 10
4 0.026667 8 300 False 10

Если вставить серию у которой значения индекса не совпадают с индексами датафрейма, то произойдет выравнивание индексов:

df['col_6'] = df['col_5'][::2]
df
col_1 col_2 col_3 col_4 col_5 col_6
0 0.003333 3 900 True 10 10.0
1 0.005556 5 900 True 10 NaN
2 0.002000 1 500 True 10 10.0
3 0.008333 5 600 True 10 NaN
4 0.026667 8 300 False 10 10.0

Метод assign() позволяет создавать столбцы, которые могут быть получены в результате вычислений:

df = pd.DataFrame(np.random.randint(0, 10, (5, 3)),
                  columns=list('abc'))

df
a b c
0 8 4 6
1 3 1 4
2 7 9 0
3 0 4 2
4 2 4 5

При этом возвращается новый датафрейм, а исходный остается без изменений:

df
a b c
0 8 4 6
1 3 1 4
2 7 9 0
3 0 4 2
4 2 4 5

Метод assign() так же позволяет использовать лямбда-функции:

df.assign(d=lambda x: x['a']*x['b'] + x['c'])
a b c d
0 8 4 6 38
1 3 1 4 7
2 7 9 0 63
3 0 4 2 2
4 2 4 5 13

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

Допустим у нас есть следующий датафрейм:

df = pd.DataFrame(np.random.randint(0, 10, (5, 3)),
                  index=list('abcde'),
                  columns=list('XYZ'))
df
X Y Z
a 2 8 5
b 1 7 6
c 4 6 6
d 3 4 5
e 6 7 3

Что бы обратиться к его строкам по их индексу (меткам) в index перед оператором [] нужно указать индексатор loc:

df.loc['c']
X    4
Y    6
Z    6
Name: c, dtype: int32

А что бы обратиться к той же строке, но по целочисленному индексу, нужен индексатор iloc:

df.iloc[2]
X    4
Y    6
Z    6
Name: c, dtype: int32

Возвращаемые индексаторами, объекты являются сериями:

type(df.iloc[2])
pandas.core.series.Series

Причем индексы этих серий, совпадают с названиями столбцов датафрейма, что в общем-то более чем логично:

df.iloc[2].index
Index(['X', 'Y', 'Z'], dtype='object')

Что бы получить доступ к элементу извлеченной строки, нужно принять во внимание, что этот элемент находится на пересечении строки и столбца, поскольку строка уже извлечена, то остается указать просто название нужного столбца в качестве индекса во втором операторе []:

df.iloc[2]['Y']
6

Индексаторами loc и iloc можно воспользоваться для выделения срезов (фрагментов таблиц):

df.loc['b':'d']
X Y Z
b 1 7 6
c 4 6 6
d 3 4 5
df.iloc[1:4]
X Y Z
b 1 7 6
c 4 6 6
d 3 4 5

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

df[1:4]     # эквивалентно df['b':'d']
X Y Z
b 1 7 6
c 4 6 6
d 3 4 5

Так же как и в NumPy получить датафрейм из нужных строк можно с помощью логических массивов:

# строки, сумма элементов которых больше 14:
df[df.sum(axis=1) > 14]
X Y Z
a 2 8 5
c 4 6 6
e 6 7 3

Выравнивание данных

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

df_1 = pd.DataFrame(np.arange(4*7).reshape(7, 4),
                    index=['row_'+str(i) for i in range(7)],
                    columns=['col_'+str(i) for i in range(4)])
df_1
col_0 col_1 col_2 col_3
row_0 0 1 2 3
row_1 4 5 6 7
row_2 8 9 10 11
row_3 12 13 14 15
row_4 16 17 18 19
row_5 20 21 22 23
row_6 24 25 26 27
df_2 = pd.DataFrame(np.arange(4*7). reshape(4, 7),
                    index=['row_'+str(i) for i in range(4)],
                    columns=['col_'+str(i) for i in range(7)])
df_2
col_0 col_1 col_2 col_3 col_4 col_5 col_6
row_0 0 1 2 3 4 5 6
row_1 7 8 9 10 11 12 13
row_2 14 15 16 17 18 19 20
row_3 21 22 23 24 25 26 27

А теперь выполним их произведение:

df_1 * df_2
col_0 col_1 col_2 col_3 col_4 col_5 col_6
row_0 0.0 1.0 4.0 9.0 NaN NaN NaN
row_1 28.0 40.0 54.0 70.0 NaN NaN NaN
row_2 112.0 135.0 160.0 187.0 NaN NaN NaN
row_3 252.0 286.0 322.0 360.0 NaN NaN NaN
row_4 NaN NaN NaN NaN NaN NaN NaN
row_5 NaN NaN NaN NaN NaN NaN NaN
row_6 NaN NaN NaN NaN NaN NaN NaN

Если выполняется операция между датафреймом и серией, то индексы серии выравниваются по столбцам датафрейма, а затем происходит транслирование серии по всему датафрейму, например

s = df_2.iloc[0]
s
col_0    0
col_1    1
col_2    2
col_3    3
col_4    4
col_5    5
col_6    6
Name: row_0, dtype: int32
df_2 * s
col_0 col_1 col_2 col_3 col_4 col_5 col_6
row_0 0 1 4 9 16 25 36
row_1 0 8 18 30 44 60 78
row_2 0 15 32 51 72 95 120
row_3 0 22 46 72 100 130 162
s = df_2.iloc[0][::2]
s
col_0    0
col_2    2
col_4    4
col_6    6
Name: row_0, dtype: int32
df_2 + s
col_0 col_1 col_2 col_3 col_4 col_5 col_6
row_0 0.0 NaN 4.0 NaN 8.0 NaN 12.0
row_1 7.0 NaN 11.0 NaN 15.0 NaN 19.0
row_2 14.0 NaN 18.0 NaN 22.0 NaN 26.0
row_3 21.0 NaN 25.0 NaN 29.0 NaN 33.0

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

s = df_2['col_4']
s
row_0     4
row_1    11
row_2    18
row_3    25
Name: col_4, dtype: int32

Но если попытаться выполнить это, то мы получим далеко не то что нужно:

df_2 - s
col_0 col_1 col_2 col_3 col_4 col_5 col_6 row_0 row_1 row_2 row_3
row_0 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
row_1 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
row_2 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
row_3 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN

Что бы выполнить подобную операцию нужно воспользоваться методом sub():

df_2.sub(s, axis=0)
col_0 col_1 col_2 col_3 col_4 col_5 col_6
row_0 -4 -3 -2 -1 0 1 2
row_1 -4 -3 -2 -1 0 1 2
row_2 -4 -3 -2 -1 0 1 2
row_3 -4 -3 -2 -1 0 1 2

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

2**df_2
col_0 col_1 col_2 col_3 col_4 col_5 col_6
row_0 1 2 4 8 16 32 64
row_1 128 256 512 1024 2048 4096 8192
row_2 16384 32768 65536 131072 262144 524288 1048576
row_3 2097152 4194304 8388608 16777216 33554432 67108864 134217728
2*df_2 + 1
col_0 col_1 col_2 col_3 col_4 col_5 col_6
row_0 1 3 5 7 9 11 13
row_1 15 17 19 21 23 25 27
row_2 29 31 33 35 37 39 41
row_3 43 45 47 49 51 53 55

Логические операции, так же выполняются с выполнением транслирования:

df_1 = pd.DataFrame(np.random.randint(0, 3, (7, 4)),
                    index=['row_'+str(i) for i in range(7)],
                    columns=['col_'+str(i) for i in range(4)])
df_1
col_0 col_1 col_2 col_3
row_0 0 0 0 1
row_1 2 0 0 0
row_2 0 1 1 1
row_3 2 1 0 0
row_4 2 1 1 0
row_5 2 2 2 1
row_6 1 2 2 1
df_1 == df_1.loc['row_2']
col_0 col_1 col_2 col_3
row_0 True False False True
row_1 False False False False
row_2 True True True True
row_3 False True False False
row_4 False True True False
row_5 False False False True
row_6 False False False True
df_2 = pd.DataFrame(np.random.randint(0, 3, (7, 4)),
                    index=['row_'+str(i) for i in range(7)],
                    columns=['col_'+str(i) for i in range(4)])
df_2
col_0 col_1 col_2 col_3
row_0 2 1 2 0
row_1 2 2 2 0
row_2 1 1 1 2
row_3 0 1 1 2
row_4 1 1 1 0
row_5 2 2 2 2
row_6 2 0 1 0
df_1 == df_2
col_0 col_1 col_2 col_3
row_0 False False False False
row_1 True False False True
row_2 False True True False
row_3 False True False False
row_4 False True True True
row_5 True True True False
row_6 False False False False

Тоже самое касается и побитовых логических операций:

df_1 = pd.DataFrame(np.random.randint(0, 2, (7, 4)),
                    index=['row_'+str(i) for i in range(7)],
                    columns=['col_'+str(i) for i in range(4)],
                    # задаем логический тип данных:
                    dtype=bool)

df_2 = pd.DataFrame(np.random.randint(0, 2, (7, 4)),
                    index=['row_'+str(i) for i in range(7)],
                    columns=['col_'+str(i) for i in range(4)],
                    dtype=bool)
df_1 & df_2
col_0 col_1 col_2 col_3
row_0 False False False True
row_1 False True False False
row_2 False False False True
row_3 False False False False
row_4 True True False False
row_5 True False False True
row_6 False False True True

Что бы выполнить транспонирование датафрейма, т.е. сделать так что бы строки стали столбцами, а столбцы строками, достаточно воспользоваться атрибутом .T:

df_1 = pd.DataFrame(np.arange(4*7).reshape(7, 4),
                    index=['row_'+str(i) for i in range(7)],
                    columns=['col_'+str(i) for i in range(4)])
df_1
col_0 col_1 col_2 col_3
row_0 0 1 2 3
row_1 4 5 6 7
row_2 8 9 10 11
row_3 12 13 14 15
row_4 16 17 18 19
row_5 20 21 22 23
row_6 24 25 26 27
df_1.T
row_0 row_1 row_2 row_3 row_4 row_5 row_6
col_0 0 4 8 12 16 20 24
col_1 1 5 9 13 17 21 25
col_2 2 6 10 14 18 22 26
col_3 3 7 11 15 19 23 27

Все поэлементные функции NumPy могут выполняться над сериями и датафреймами, при условии, что они состоят из числовых значений:

np.exp2(df_1)
col_0 col_1 col_2 col_3
row_0 1.0 2.0 4.0 8.0
row_1 16.0 32.0 64.0 128.0
row_2 256.0 512.0 1024.0 2048.0
row_3 4096.0 8192.0 16384.0 32768.0
row_4 65536.0 131072.0 262144.0 524288.0
row_5 1048576.0 2097152.0 4194304.0 8388608.0
row_6 16777216.0 33554432.0 67108864.0 134217728.0

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

np.mean(df_1)
col_0    12.0
col_1    13.0
col_2    14.0
col_3    15.0
dtype: float64
np.mean(np.arange(4*7).reshape(7, 4))
13.5

Тем не менее, серии полностью поддерживают универсальные функции NumPy, причем выравнивание индексов происходит перед применением данных функций:

s1 = df_1['col_0'][3:]
s1
row_3    12
row_4    16
row_5    20
row_6    24
Name: col_0, dtype: int32
np.sqrt(s1)
row_3    3.464102
row_4    4.000000
row_5    4.472136
row_6    4.898979
Name: col_0, dtype: float64
s2 = df_1['col_2'][:-2]
s2
row_0     2
row_1     6
row_2    10
row_3    14
row_4    18
Name: col_2, dtype: int32
# индексы выравниваются:
np.mod(s2, s1)
row_0    NaN
row_1    NaN
row_2    NaN
row_3    2.0
row_4    2.0
row_5    NaN
row_6    NaN
dtype: float64

Вывод на экран

Чаще всего датафреймы содержат очень много строк и столбцов, которые просто не могут поместиться на экран. В качестве примера, мы можем воспользоваться наборами данных из пакета Seaborn. Скорее всего вы работаете в дистрибутиве Anaconda или в Гугловском колабе, так что Seaborn наверняка у вас уже установлен. А если нет, то быстрее пересаживайтесь на анаконду и колаб!

# импортируем seaborn
import seaborn as sns
# загружаем данные:
data = sns.load_dataset('tips')
# попытаемся вывести их на экран:
data
total_bill tip sex smoker day time size
0 16.99 1.01 Female No Sun Dinner 2
1 10.34 1.66 Male No Sun Dinner 3
2 21.01 3.50 Male No Sun Dinner 3
3 23.68 3.31 Male No Sun Dinner 2
4 24.59 3.61 Female No Sun Dinner 4
... ... ... ... ... ... ... ...
239 29.03 5.92 Male No Sat Dinner 3
240 27.18 2.00 Female Yes Sat Dinner 2
241 22.67 2.00 Male Yes Sat Dinner 2
242 17.82 1.75 Male No Sat Dinner 2
243 18.78 3.00 Female No Thur Dinner 2

Это данные о чаевых в ресторане, которые состоят из семи столбцов и 244-х строк, понятно, что выводить все эти строки на экран не имеет смысла, т.к. получится очень длинная таблица. Что бы получить какое-то представление о таблице можно воспользоваться методом info():

data.info()
RangeIndex: 244 entries, 0 to 243
Data columns (total 7 columns):
 #   Column      Non-Null Count  Dtype   
---  ------      --------------  -----   
 0   total_bill  244 non-null    float64 
 1   tip         244 non-null    float64 
 2   sex         244 non-null    category
 3   smoker      244 non-null    category
 4   day         244 non-null    category
 5   time        244 non-null    category
 6   size        244 non-null    int64   
dtypes: category(4), float64(2), int64(1)
memory usage: 7.3 KB

Да, чаще всего серии и датафреймы являются очень большими и их вывод просто не помещается на экран, поэтому, они выводятся в "урезанной" форме:

s = pd.Series(np.random.rand(3000))
s
0       0.756637
1       0.885349
2       0.525605
3       0.753712
4       0.388052
          ...   
2995    0.484288
2996    0.860380
2997    0.714192
2998    0.315336
2999    0.292255
Length: 3000, dtype: float64

Что бы просмотреть их небольшой фрагмент, можно воспользоваться методами head() или tail():

s.head()
0    0.756637
1    0.885349
2    0.525605
3    0.753712
4    0.388052
dtype: float64
s.tail()
2995    0.484288
2996    0.860380
2997    0.714192
2998    0.315336
2999    0.292255
dtype: float64

По умолчанию выводится всего 5 строк, но мы можем указать столько строк сколько нам нужно:

s.head(11)
0     0.756637
1     0.885349
2     0.525605
3     0.753712
4     0.388052
5     0.521513
6     0.728470
7     0.872524
8     0.451322
9     0.343387
10    0.441652
dtype: float64

Атрибуты объектов Pandas

Допустим у нас есть следующий датафрейм:

df = pd.DataFrame(np.random.rand(5, 4),
                  index=pd.date_range('1/1/2020', periods=5),
                  columns=list('ABCD'))
df
A B C D
2020-01-01 0.291797 0.898429 0.471657 0.170232
2020-01-02 0.781077 0.088060 0.056392 0.110569
2020-01-03 0.949604 0.781641 0.281778 0.488396
2020-01-04 0.730843 0.055508 0.513162 0.109554
2020-01-05 0.703591 0.060644 0.542054 0.995164

Что бы получить доступ к его метаданным, таким как форма, названия столбцов и индексам строк, можно воспользоваться его атрибутами:

# форма датафрейма:
df.shape
(5, 4)
# имена столбцов:
df.columns
Index(['A', 'B', 'C', 'D'], dtype='object')
# индексы строк:
df.index
DatetimeIndex(['2020-01-01', '2020-01-02', '2020-01-03', '2020-01-04',
               '2020-01-05'],
              dtype='datetime64[ns]', freq='D')

Объекты с именами столбцов и индексами строк являются неизменяемыми (немутирующими), так что если попробовать переименовать имя одного столбца или изменить индекс одной из строк, то мы увидим ошибку:

# эта команда вызовет ошибку:
df.columns[0] = 'col_1'

# df.index[0] = 1     # и эта тоже
TypeError: Index does not support mutable operations

Но ничто нам не запрещает спокойно назначать датафреймам новые объекты columns и index:

# Назначаем датафрейму список с новыми
# названиями столбцов:
df.columns = ['col_' + str(i) for i in range(len(df.columns))]

df
col_0 col_1 col_2 col_3
2020-01-01 0.291797 0.898429 0.471657 0.170232
2020-01-02 0.781077 0.088060 0.056392 0.110569
2020-01-03 0.949604 0.781641 0.281778 0.488396
2020-01-04 0.730843 0.055508 0.513162 0.109554
2020-01-05 0.703591 0.060644 0.542054 0.995164
# Назначаем датафрейму список с новыми 
# индексами строк:
df.index = ['id_' + str(i) for i in range(len(df.index))]
df
col_0 col_1 col_2 col_3
id_0 0.291797 0.898429 0.471657 0.170232
id_1 0.781077 0.088060 0.056392 0.110569
id_2 0.949604 0.781641 0.281778 0.488396
id_3 0.730843 0.055508 0.513162 0.109554
id_4 0.703591 0.060644 0.542054 0.995164

В тоже время объекты Pandas (Series, DataFrame, Index, Columns) можно рассматривать как контейнеры для обычных массивов, которые содержат фактические данные и над которыми можно выполнять действия и вычисления. Основным типом используемых массивов являются массивы NumPy, но Pandas идет несколько дальше этих массивов и расширяет множество типов данных, которые могут в них содержаться.

Что бы преобразовать серии и их индексы в массивы NumPy можно воспользоваться их атрибутом array:

s.array
<PandasArray>
[ 0.7566373097067226,  0.8853494774968603,  0.5256051606102947,
  0.7537124219053462, 0.38805158317338295,  0.5215129391083598,
  0.7284701113086132,  0.8725242706319586,  0.4513219917590101,
  0.3433872195192209,
 ...
  0.7496924032706176,  0.7649128596816056,  0.1465641402387845,
 0.34692762365568786,  0.8616631882386389,  0.4842883439511987,
  0.8603801362720178,  0.7141924387167984,  0.3153358791276174,
 0.29225468355252293]
Length: 3000, dtype: float64
s.index.array
<PandasArray>
[   0,    1,    2,    3,    4,    5,    6,    7,    8,    9,
 ...
 2990, 2991, 2992, 2993, 2994, 2995, 2996, 2997, 2998, 2999]
Length: 3000, dtype: int64

Вы можете заметить, что возвращаемые объекты не очень-то и похожи на массивы NumPy, так и есть - это одномерные объекты, базового класса ExtensionArray, которые очень похожи на обычные массивы NumPy, но для Pandas они позволяют добиться большей функциональности за счет расширения типов. Эта информация, конечно вряд ли является очень полезной, но она дает хорошее представление о том, что серии и датафреймы Pandas это не просто смесь свойств словарей и массивов в буквальном смысле этого слова, а нечто более сложное.

# можем изменить элемент серии:
s.array[0] = 0.7777
s
0       0.777700
1       0.885349
2       0.525605
3       0.753712
4       0.388052
          ...   
2995    0.484288
2996    0.860380
2997    0.714192
2998    0.315336
2999    0.292255
Length: 3000, dtype: float64
# казалось бы, что теперь мы можем изменить
# один из индексов:
s.index.array[0] = 777
# И мы вроде бы его действительно изменили:
s.index.array
<PandasArray>
[ 777,    1,    2,    3,    4,    5,    6,    7,    8,    9,
 ...
 2990, 2991, 2992, 2993, 2994, 2995, 2996, 2997, 2998, 2999]
Length: 3000, dtype: int64
# но... это не так:
s.index
RangeIndex(start=0, stop=3000, step=1)

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

Если нам нужен именно массив NumPy то можно воспользоваться методом Pandas to_numpy() или методом NumPy asarray():

s.to_numpy()
array([0.7777    , 0.88534948, 0.52560516, ..., 0.71419244, 0.31533588,
       0.29225468])
np.asarray(s)
array([0.7777    , 0.88534948, 0.52560516, ..., 0.71419244, 0.31533588,
       0.29225468])

Когда серии и объекты индексов основаны на классе ExtensionArray, то иногда бывает полезно включать копирование данных и преобразование их типа с помощью аргументов copy и dtype. Например, в NumPy нет типа данных для хранения даты и времени с учетом часовых поясов, а в Pandas есть. Допустим, у нас есть вот такая серия, которая хранит дату с московским временем:

s_dt = pd.Series(pd.date_range('01/01/2020',
                               periods=5,
                               tz="Europe/Moscow"))
s_dt
0   2020-01-01 00:00:00+03:00
1   2020-01-02 00:00:00+03:00
2   2020-01-03 00:00:00+03:00
3   2020-01-04 00:00:00+03:00
4   2020-01-05 00:00:00+03:00
dtype: datetime64[ns, Europe/Moscow]

Если мы перобразуем эту серию в массив NumPy, то увидим что его данные будут типа object:

s_dt.to_numpy()
array([Timestamp('2020-01-01 00:00:00+0300', tz='Europe/Moscow', freq='D'),
       Timestamp('2020-01-02 00:00:00+0300', tz='Europe/Moscow', freq='D'),
       Timestamp('2020-01-03 00:00:00+0300', tz='Europe/Moscow', freq='D'),
       Timestamp('2020-01-04 00:00:00+0300', tz='Europe/Moscow', freq='D'),
       Timestamp('2020-01-05 00:00:00+0300', tz='Europe/Moscow', freq='D')],
      dtype=object)

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

s_dt.to_numpy(dtype="datetime64[ns]")
array(['2019-12-31T21:00:00.000000000', '2020-01-01T21:00:00.000000000',
       '2020-01-02T21:00:00.000000000', '2020-01-03T21:00:00.000000000',
       '2020-01-04T21:00:00.000000000'], dtype='datetime64[ns]')

С преобразованием датафреймов в массивы все немного сложнее. Если все столбцы датафрейма имеют единый тип данных, то метод to_numpy вернет массив с тем же самым типом данных:

df.to_numpy()
array([[0.29179735, 0.89842931, 0.47165696, 0.17023219],
       [0.78107744, 0.0880599 , 0.05639239, 0.1105689 ],
       [0.94960403, 0.78164148, 0.28177801, 0.48839612],
       [0.73084347, 0.05550788, 0.51316206, 0.10955422],
       [0.70359109, 0.06064443, 0.54205405, 0.99516418]])

Но если данные в столбцах разного типа, то тип данных результирующего масива будет преобразован к наиболее общему типу NumPy:

df['col_4'] = list('abcde')
df
col_0 col_1 col_2 col_3 col_4
id_0 0.291797 0.898429 0.471657 0.170232 a
id_1 0.781077 0.088060 0.056392 0.110569 b
id_2 0.949604 0.781641 0.281778 0.488396 c
id_3 0.730843 0.055508 0.513162 0.109554 d
id_4 0.703591 0.060644 0.542054 0.995164 e
df.to_numpy()
array([[0.2917973548795997, 0.898429306889781, 0.47165696129825396,
        0.17023219026443925, 'a'],
       [0.7810774352278931, 0.08805989587285967, 0.056392392541371184,
        0.110568898025453, 'b'],
       [0.949604030529145, 0.7816414838487163, 0.28177801318706375,
        0.48839612177912317, 'c'],
       [0.7308434748046037, 0.055507880608684834, 0.5131620639128139,
        0.10955421692713718, 'd'],
       [0.7035910884042201, 0.06064442885754162, 0.5420540476511414,
        0.9951641797984419, 'e']], dtype=object)

Раньше для этих целей рекомендовалось использовать метод values(), но сейчас данный метод считается устаревшим. Так что если вам нужно преобразовать объекты Pandas в массивы NumPy, то пользуйтесь атрибутом array или методом to_numpy().