Структуры данных
Основными рабочими структурами данных в 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()
.