NumPy и pandas
Физик обращается к языку программирования ради данных, поэтому начать целесообразно с инструментов, приспособленных для быстрых вычислений.
NumPy
Python поначалу разочаровывает: список из миллиона чисел занимает десятки мегабайт, а цикл по нему выполняется очень долго. Причины, рассмотренные в главе про объекты и память, сводятся к одному: каждое число в списке является отдельным объектом со своей обёрткой, размещённым где-то в куче.
Решением является NumPy. Идея заключается в том, чтобы хранить числа так, как их хранит C, то есть подряд, одного типа, без обёрток, а операции над ними передавать скомпилированному коду, работающему сразу над всем массивом. Выигрыш достигает сотен раз. На NumPy построен научный стек: SciPy, pandas, scikit-learn и библиотеки машинного обучения используют массивы NumPy.
Полная документация: numpy.org/doc.
NumPy решает две задачи:
- хранить многомерные массивы (в том числе матрицы);
- быстро считать математические функции сразу от всего массива.
Основой библиотеки является один объект, ndarray.
Отличия массива от списка:
- длина массива, заданная в момент создания, остаётся неизменной, тогда как список растёт динамически;
- все элементы массива одного типа;
- операции пишутся сразу над массивом целиком, без цикла.
Отсюда следуют две сильные стороны NumPy: векторизация и broadcasting.
import numpy as np
Способы создания массивов
Массив может быть получен тремя способами: преобразованием из обычной структуры Python, генерацией встроенной функцией или чтением с диска. Все три способа встречаются примерно одинаково часто.
Конвертация из структур Python
np.array([1, 2, 3, 4, 5])
array([1, 2, 3, 4, 5])
При конвертации можно задавать тип данных с помощью аргумента dtype:
np.array([1, 2, 3, 4, 5], dtype=np.float32)
array([1., 2., 3., 4., 5.], dtype=float32)
Аналогичное преобразование:
np.float32([1, 2, 3, 4, 5])
array([1., 2., 3., 4., 5.], dtype=float32)
Генерация массивов
- arange работает как аналог range из Python, но принимает и нецелочисленный шаг
- linspace равномерно разбивает отрезок на n-1 интервал
- logspace разбивает отрезок по логарифмической шкале
- zeros создаёт массив заданной размерности, заполненный нулями
- ones создаёт массив заданной размерности, заполненный единицами
- empty создаёт массив заданной размерности, не инициализированный никаким значением, то есть заполненный мусором из памяти
np.arange(0, 5, 0.5)
array([0. , 0.5, 1. , 1.5, 2. , 2.5, 3. , 3.5, 4. , 4.5])
np.linspace(0, 5, 11)
array([0. , 0.5, 1. , 1.5, 2. , 2.5, 3. , 3.5, 4. , 4.5, 5. ])
np.logspace(0, 9, 10, base=2)
array([ 1., 2., 4., 8., 16., 32., 64., 128., 256., 512.])
np.zeros((2, 2))
array([[0., 0.],
[0., 0.]])
np.ones((2, 2))
array([[1., 1.],
[1., 1.]])
np.empty((2, 2))
array([[1., 1.],
[1., 1.]])
np.diag([1,2,3])
array([[1, 0, 0],
[0, 2, 0],
[0, 0, 3]])
Размеры массива хранятся в поле shape, а число измерений — в поле ndim.
array = np.ones((2, 3,))
print('Размерность массива - %s, количество размерностей - %d'%(array.shape, array.ndim))
array
Размерность массива - (2, 3), количество размерностей - 2
array([[1., 1., 1.],
[1., 1., 1.]])
## Чему равны ndim и shape в следующих случаях
print(np.diag([1,2,3]).shape, np.diag([1,2,3]).ndim)
print(np.zeros((5, 5, 5)).shape, np.zeros((5, 5, 5)).ndim)
(3, 3) 2
(5, 5, 5) 3
Метод reshape изменяет форму массива, не изменяя самих данных
array = np.arange(0, 6, 0.5)
array = array.reshape((2, 6))
array
array([[0. , 0.5, 1. , 1.5, 2. , 2.5],
[3. , 3.5, 4. , 4.5, 5. , 5.5]])
Многомерный массив разворачивается в вектор функцией ravel
array = np.ravel(array)
array
array([0. , 0.5, 1. , 1.5, 2. , 2.5, 3. , 3.5, 4. , 4.5, 5. , 5.5])
# Какие будут массивы?
print(np.ravel(np.diag([1,2])))
print(np.reshape(np.diag([1,2]), [1, 4]))
[1 0 0 2]
[[1 0 0 2]]
Индексация
В NumPy применяется привычная индексация Python, включая отрицательные индексы и срезы, записываемые так же, как для списка
print(array[0])
print(array[-1])
print(array[1:-1])
print(array[1:-1:2])
print(array[::-1])
0.0
5.5
[0.5 1. 1.5 2. 2.5 3. 3.5 4. 4.5 5. ]
[0.5 1.5 2.5 3.5 4.5]
[5.5 5. 4.5 4. 3.5 3. 2.5 2. 1.5 1. 0.5 0. ]
print(array.shape)
(12,)
print(array[None,0:, None].ndim, array[None,0:, None].shape)
array[None,0:, None]
3 (1, 12, 1)
array([[[0. ],
[0.5],
[1. ],
[1.5],
[2. ],
[2.5],
[3. ],
[3.5],
[4. ],
[4.5],
[5. ],
[5.5]]])
Замечание: индексы и срезы, перечисляемые для многомерного массива, разделять квадратными скобками не следует,
т.е. вместо matrix[i][j] нужно использовать matrix[i, j]
Массив, подставленный вместо индекса, может быть и списком номеров, и булевой маской:
array[[0, 2, 4, 6, 8, 10]]
array([0., 1., 2., 3., 4., 5.])
array[[True, False, True, False, True, False, True, False, True, False, True, False]]
array([0., 1., 2., 3., 4., 5.])
# Что будет выведено?
x = np.array([[1, 2, 3]])
y = np.array([1, 2, 3])
print (x.shape, y.shape)
print(np.array_equal(x, y))
print(np.array_equal(x, y[None, :]))
(1, 3) (3,)
False
True
x = np.arange(10)
x
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
x[(x % 2 == 0) & (x > 5)]
array([6, 8])
print(x)
y = x[x>5]
y *= 2
print(y)
print(x)
[0 1 2 3 4 5 6 7 8 9]
[12 14 16 18]
[0 1 2 3 4 5 6 7 8 9]
Срез массива в NumPy является представлением тех же данных, а не их копией: изменение среза наподобие x[2:5] приводит к изменению исходного массива. Отбор по маске или по списку индексов всегда возвращает копию, и исходный массив остаётся нетронутым. Когда требуются собственные данные, применяется метод copy.
x.copy()
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
Сохранение и чтение массивов в бинарном формате
with open('out.npy', 'wb') as f:
np.save(f, x)
with open('out.npy', 'rb') as f:
print(f.read())
with open('out.npy', 'rb') as f:
y = np.load(f)
print(y)
b"\x93NUMPY\x01\x00v\x00{'descr': '<i8', 'fortran_order': False, 'shape': (10,), } \n\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x07\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\t\x00\x00\x00\x00\x00\x00\x00"
[0 1 2 3 4 5 6 7 8 9]
Чтение данных с помощью функции genfromtxt
Для примера потребуется файл iris_subset.txt. Он не приложен, и настоящий не нужен:
числа в нём случайные (чашелистик длиной 1134 см встретится ниже в выводе).
Важна только структура: текстовая таблица с заголовком, четыре числовых столбца
и один строковый. Такой файл необходимо создать самостоятельно, поместив в первую строку имена
столбцов, разделённые , :
sepal_length_in_cm, sepal_width_in_cm, petal_length_in_cm, petal_width_in_cm, class
1.0, 1.0, 10.0, 121.0, setosa
1.0, 314.0, 13.0, 121.0, versicolor
Если требуется работать с настоящими ирисами, их предоставляет scikit-learn одной строкой:
from sklearn.datasets import load_iris.
iris = np.genfromtxt('iris_subset.txt', delimiter=', ', names=True, dtype=[('sepal_length_in_cm', 'f8'),
('sepal_width_in_cm', 'f8'),
('petal_length_in_cm', 'f8'),
('petal_width_in_cm', 'f8'),
('class', 'U10')])
iris
array([(1.000e+00, 1. , 10., 121. , 'setosa'),
(1.000e+00, 314. , 13., 121. , 'versicolor'),
(1.134e+03, 1. , 103., 1421. , 'setosa'),
(1.000e+00, 141. , 10., 121. , 'versicolor'),
(1.440e+02, 1. , 4582., 13481. , 'versicolor'),
(1.000e+00, 13.3, 10., 121. , 'versicolor'),
(1.141e+03, 1. , 1341., 1231.1, 'setosa'),
(7.320e+02, 131. , 139., 92.1, 'setosa')],
dtype=[('sepal_length_in_cm', '<f8'), ('sepal_width_in_cm', '<f8'), ('petal_length_in_cm', '<f8'), ('petal_width_in_cm', '<f8'), ('class', '<U10')])
genfromtxt с names=True возвращает структурированный массив, хранящий имена столбцов вместе с данными. Строка из него запрашивается по номеру, а столбец — по названию.
print('Описание первого элемента: %s'%iris[0])
print('Значения столбца sepal_length_in_cm: %s'%iris['sepal_length_in_cm'])
Описание первого элемента: (1., 1., 10., 121., 'setosa')
Значения столбца sepal_length_in_cm: [1.000e+00 1.000e+00 1.134e+03 1.000e+00 1.440e+02 1.000e+00 1.141e+03
7.320e+02]
sepal_length_setosa = iris['sepal_length_in_cm'][iris['class'] == 'setosa']
sepal_length_versicolor = iris['sepal_length_in_cm'][iris['class'] == 'versicolor']
print('Значения столбца sepal_length_in_cm\n\tclass setosa: %s\n\tclass versicolor: %s'%(sepal_length_setosa,
sepal_length_versicolor))
Значения столбца sepal_length_in_cm
class setosa: [1.000e+00 1.134e+03 1.141e+03 7.320e+02]
class versicolor: [ 1. 1. 144. 1.]
Строки в начале и в конце файла пропускаются аргументами skip_header и skip_footer, а столбцы отбираются через usecols.
iris_class = np.genfromtxt('iris_subset.txt', delimiter=', ', skip_header=1, usecols=4, dtype='U10')
iris_class
array(['setosa', 'versicolor', 'setosa', 'versicolor', 'versicolor',
'versicolor', 'setosa', 'setosa'], dtype='<U10')
iris_features = np.genfromtxt('iris_subset.txt', delimiter=', ', skip_header=1, usecols=range(4))
iris_features
array([[1.0000e+00, 1.0000e+00, 1.0000e+01, 1.2100e+02],
[1.0000e+00, 3.1400e+02, 1.3000e+01, 1.2100e+02],
[1.1340e+03, 1.0000e+00, 1.0300e+02, 1.4210e+03],
[1.0000e+00, 1.4100e+02, 1.0000e+01, 1.2100e+02],
[1.4400e+02, 1.0000e+00, 4.5820e+03, 1.3481e+04],
[1.0000e+00, 1.3300e+01, 1.0000e+01, 1.2100e+02],
[1.1410e+03, 1.0000e+00, 1.3410e+03, 1.2311e+03],
[7.3200e+02, 1.3100e+02, 1.3900e+02, 9.2100e+01]])
features_setosa = iris_features[iris_class == 'setosa']
features_versicolor = iris_features[iris_class == 'versicolor']
Операции в NumPy производятся над векторами одинаковой размерности целиком, без цикла.
Поэлементная разность двух векторов:
sepal_length_versicolor - sepal_length_setosa
array([ 0., -1133., -997., -731.])
Аналогично для многомерных массивов.
features_versicolor - features_setosa
array([[ 0.00000e+00, 3.13000e+02, 3.00000e+00, 0.00000e+00],
[-1.13300e+03, 1.40000e+02, -9.30000e+01, -1.30000e+03],
[-9.97000e+02, 0.00000e+00, 3.24100e+03, 1.22499e+04],
[-7.31000e+02, -1.17700e+02, -1.29000e+02, 2.89000e+01]])
Broadcasting
Broadcasting снимает требование одинаковой формы и разрешает арифметику над массивами разных, но согласованных между собой размерностей. Простейшим случаем является умножение вектора на число.

2*np.arange(1, 4)
array([2, 4, 6])
Правило согласования размерностей:
In order to broadcast, the size of the trailing axes for both arrays in an operation must either be the same size or one of them must be one.
Таким образом, для выполнения broadcasting длины осей, отсчитываемых с конца, должны либо совпадать, либо одна из них должна быть равна единице.
Если количество размерностей не совпадает, к массиву меньшей размерности слева дописываются фиктивные оси, не занимающие памяти, например:
a = np.ones((2, 3, 4))
b = np.ones(4)
c = a * b # здесь a.shape = (2, 3, 4), а b.shape считается равным (1, 1, 4)
Прибавим к каждой строке матрицы один и тот же вектор:

np.array([[0, 0, 0], [10, 10, 10], [20, 20, 20], [30, 30, 30]]) + np.arange(3)
array([[ 0, 1, 2],
[10, 11, 12],
[20, 21, 22],
[30, 31, 32]])
Со столбцами такой приём не работает: вектор из четырёх элементов не согласуется с матрицей по последней оси.

Сначала вектор, дополненный новой осью, приводится к виду:
np.arange(4)[:, np.newaxis]
array([[0],
[1],
[2],
[3]])
Затем к нему прибавляется матрица:
np.arange(4)[:, np.newaxis]+np.array([[0, 0, 0], [10, 10, 10], [20, 20, 20], [30, 30, 30]])
array([[ 0, 0, 0],
[11, 11, 11],
[22, 22, 22],
[33, 33, 33]])
Кроме того, в NumPy имеются сводные операции над массивами: np.min, np.max, np.sum, np.mean и т.д.
print('Среднее значение всех значений класса versicolor: %s'%np.mean(features_versicolor))
print('Среднее значение каждого признака класса versicolor: %s'%np.mean(features_versicolor, axis=0))
Среднее значение всех значений класса versicolor: 1192.20625
Среднее значение каждого признака класса versicolor: [ 36.75 117.325 1153.75 3461. ]
Вычислим без цикла \(\frac{1}{n} \sum\limits_{i=1}^n |x_i-y_i|\) для каждой пары \((x, y)\), где \(x\) обозначает вектор признаков объекта из класса setosa, а \(y\) вектор признаков объекта из класса versicolor.
np.mean(np.abs(features_setosa - features_versicolor[:, np.newaxis]), axis=2)
array([[7.900000e+01, 7.090000e+02, 9.727750e+02, 2.672250e+02],
[3.500000e+01, 6.665000e+02, 9.302750e+02, 2.247250e+02],
[4.518750e+03, 4.382250e+03, 4.121975e+03, 4.637475e+03],
[3.075000e+00, 6.345750e+02, 8.983500e+02, 2.516500e+02]])
Операции
Почти каждая сводная операция имеет аргумент axis, задающий номер оси, вдоль которой она вычисляется. Без него операция обрабатывает весь массив целиком и возвращает одно число, а с ним сворачивает только указанную ось, оставляя остальные нетронутыми.
x = np.arange(40).reshape(5, 2, 4)
print(x)
[[[ 0 1 2 3]
[ 4 5 6 7]]
[[ 8 9 10 11]
[12 13 14 15]]
[[16 17 18 19]
[20 21 22 23]]
[[24 25 26 27]
[28 29 30 31]]
[[32 33 34 35]
[36 37 38 39]]]
print(x.mean())
print(np.mean(x))
19.5
19.5
x.mean(axis=0)
array([[16., 17., 18., 19.],
[20., 21., 22., 23.]])
x.mean(axis=1)
array([[ 2., 3., 4., 5.],
[10., 11., 12., 13.],
[18., 19., 20., 21.],
[26., 27., 28., 29.],
[34., 35., 36., 37.]])
x.mean(axis=2)
array([[ 1.5, 5.5],
[ 9.5, 13.5],
[17.5, 21.5],
[25.5, 29.5],
[33.5, 37.5]])
x.mean(axis=(0,2))
array([17.5, 21.5])
x.mean(axis=(0,1,2))
19.5
Конкатенация многомерных массивов
Объединение массивов выполняют функции np.concatenate, np.hstack, np.vstack, np.dstack, различающиеся только осью объединения.
x = np.arange(10).reshape(5, 2)
y = np.arange(100, 120).reshape(5, 4)
x
array([[0, 1],
[2, 3],
[4, 5],
[6, 7],
[8, 9]])
y
array([[100, 101, 102, 103],
[104, 105, 106, 107],
[108, 109, 110, 111],
[112, 113, 114, 115],
[116, 117, 118, 119]])
np.hstack((x, y))
array([[ 0, 1, 100, 101, 102, 103],
[ 2, 3, 104, 105, 106, 107],
[ 4, 5, 108, 109, 110, 111],
[ 6, 7, 112, 113, 114, 115],
[ 8, 9, 116, 117, 118, 119]])
x = np.ones([2, 3])
y = np.zeros([2, 2])
# Какой будет результат
print(np.hstack((x,y)).shape)
print(np.vstack((x,y)).shape)
(2, 5)
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[54], line 3
1 # Какой будет результат
2 print(np.hstack((x,y)).shape)
----> 3 print(np.vstack((x,y)).shape)
File /opt/anaconda3/lib/python3.12/site-packages/numpy/core/shape_base.py:289, in vstack(tup, dtype, casting)
287 if not isinstance(arrs, list):
288 arrs = [arrs]
--> 289 return _nx.concatenate(arrs, 0, dtype=dtype, casting=casting)
ValueError: all the input array dimensions except for the concatenation axis must match exactly, but along dimension 1, the array at index 0 has size 3 and the array at index 1 has size 2
p = np.arange(1).reshape([1, 1, 1, 1])
p
array([[[[0]]]])
print("vstack: ", np.vstack((p, p)).shape)
print("hstack: ", np.hstack((p, p)).shape)
print("dstack: ", np.dstack((p, p)).shape)
print("concatenate: ", np.concatenate((p, p), axis=3).shape)
vstack: (2, 1, 1, 1)
hstack: (1, 2, 1, 1)
dstack: (1, 1, 2, 1)
concatenate: (1, 1, 1, 2)
Типы
От типа массива зависят и занимаемая память, и диапазон представимых значений. Рассмотрим, что происходит с числом 70000 в uint16. С версии NumPy 2.0 конструктор массива на таком значении завершается ошибкой OverflowError, а без предупреждения оно усекается только при явном приведении через astype, превращаясь в 4464; такое переполнение, не сопровождаемое ошибкой, является классическим источником неверных результатов.
x = [1, 2, 70000]
np.array(x, dtype=np.float32)
array([1.e+00, 2.e+00, 7.e+04], dtype=float32)
np.array(x, dtype=np.uint16)
OverflowError: Python integer 70000 out of bounds for uint16
Ранее NumPy усекал значение, не помещавшееся в тип, и лишь выдавал предупреждение; с версии 2.0 это ошибка. Незаметное переполнение осталось только при явном приведении:
np.array(70000).astype(np.uint16)
4464
uint16 хранит остаток по модулю \( 2^{16} \), а
\( 70000 - 65536 = 4464 \). Такая ошибка не приводит к исключению и ничего не выводит.
np.array(x, dtype=np.str_)
array(['1', '2', '70000'], dtype='<U5')
Функциональное программирование
NumPy предоставляет несколько способов поэлементного применения обычной функции Python к массиву, и разница в скорости между ними поучительна.
def f(value):
return np.sqrt(value)
print(np.apply_along_axis(f, 0, np.arange(10)))
[0. 1. 1.41421356 1.73205081 2. 2.23606798
2.44948974 2.64575131 2.82842712 3. ]
vf = np.vectorize(f)
%%timeit
vf(np.arange(100000))
146 ms ± 2.4 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
%%timeit
np.apply_along_axis(f, 0, np.arange(100000))
1.89 ms ± 31.9 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
%%timeit
np.array([f(v) for v in np.arange(100000)])
129 ms ± 861 μs per loop (mean ± std. dev. of 7 runs, 10 loops each)
Разница в семьдесят раз выглядит убедительно, однако выводы из неё были бы поспешными. np.vectorize и списковое включение вызывают f сто тысяч раз, и 130–150 миллисекунд составляют стоимость ста тысяч вызовов функции Python. В то же время apply_along_axis с axis=0 на одномерном массиве вызывает f один раз, передавая в неё весь массив целиком, так что измерен здесь один векторный np.sqrt, а не поэлементный обход.
Прежде чем доверять отношению времён, необходимо разобраться, что делает каждая из сравниваемых версий. np.vectorize не векторизует, а лишь оборачивает цикл, и ускорения от него ожидать не следует.
Pandas
Pandas читает данные, приводит их в порядок, вычисляет по ним сводки и строит графики. Если NumPy предоставляет массив чисел, то pandas предоставляет таблицу с именованными столбцами и индексом, размечающим строки.
import pandas as pd
df = pd.read_csv("titanic.csv", sep='\t')
Набор данных о «Титанике» находится в открытом доступе (например,
в OpenML под именем titanic).
Файл использует табуляцию, а не запятую в качестве разделителя, поэтому sep='\t' обязателен.
Файл, загруженный из другого источника, может оказаться обычным CSV.
df.head(3)
| PassengerId | Survived | Pclass | Name | Sex | Age | SibSp | Parch | Ticket | Fare | Cabin | Embarked | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 0 | 3 | Braund, Mr. Owen Harris | male | 22.0 | 1 | 0 | A/5 21171 | 7.2500 | NaN | S |
| 1 | 2 | 1 | 1 | Cumings, Mrs. John Bradley (Florence Briggs Th... | female | 38.0 | 1 | 0 | PC 17599 | 71.2833 | C85 | C |
| 2 | 3 | 1 | 3 | Heikkinen, Miss. Laina | female | 26.0 | 0 | 0 | STON/O2. 3101282 | 7.9250 | NaN | S |
view = df[df['Sex'] == 'female']
list(((df['Sex'] == 'female') & (df['Age'] > 30)).index)
[0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
46,
47,
48,
49,
50,
51,
52,
53,
54,
55,
56,
57,
58,
59,
60,
61,
62,
63,
64,
65,
66,
67,
68,
69,
70,
71,
72,
73,
74,
75,
76,
77,
78,
79,
80,
81,
82,
83,
84,
85,
86,
87,
88,
89,
90,
91,
92,
93,
94,
95,
96,
97,
98,
99,
100,
101,
102,
103,
104,
105,
106,
107,
108,
109,
110,
111,
112,
113,
114,
115,
116,
117,
118,
119,
120,
121,
122,
123,
124,
125,
126,
127,
128,
129,
130,
131,
132,
133,
134,
135,
136,
137,
138,
139,
140,
141,
142,
143,
144,
145,
146,
147,
148,
149,
150,
151,
152,
153,
154,
155]
df[(df['Sex'] == 'female') & (df['Age'] > 30)].index
Index([1, 3, 11, 15, 18, 25, 40, 52, 61, 85, 98, 123, 132], dtype='int64')
df.drop(index=df[(df['Sex'] == 'female') & (df['Age'] > 30)].index, inplace=True)
df.loc[78]
PassengerId 79
Survived 1
Pclass 2
Name Caldwell, Master. Alden Gates
Sex male
Age 0.83
SibSp 0
Parch 2
Ticket 248738
Fare 29.0
Cabin NaN
Embarked S
Name: 78, dtype: object
df.iloc[0]
PassengerId 1
Survived 0
Pclass 3
Name Braund, Mr. Owen Harris
Sex male
Age 22.0
SibSp 1
Parch 0
Ticket A/5 21171
Fare 7.25
Cabin NaN
Embarked S
Name: 0, dtype: object
df.describe()
| PassengerId | Survived | Pclass | Age | SibSp | Parch | Fare | |
|---|---|---|---|---|---|---|---|
| count | 143.000000 | 143.000000 | 143.000000 | 113.000000 | 143.000000 | 143.000000 | 143.000000 |
| mean | 80.902098 | 0.307692 | 2.461538 | 26.702035 | 0.601399 | 0.391608 | 27.526018 |
| std | 44.536473 | 0.463161 | 0.776134 | 14.483237 | 1.075593 | 0.813919 | 40.406013 |
| min | 1.000000 | 0.000000 | 1.000000 | 0.830000 | 0.000000 | 0.000000 | 6.750000 |
| 25% | 43.500000 | 0.000000 | 2.000000 | 19.000000 | 0.000000 | 0.000000 | 7.925000 |
| 50% | 81.000000 | 0.000000 | 3.000000 | 24.000000 | 0.000000 | 0.000000 | 13.000000 |
| 75% | 118.500000 | 1.000000 | 3.000000 | 33.000000 | 1.000000 | 0.000000 | 29.597900 |
| max | 156.000000 | 1.000000 | 3.000000 | 71.000000 | 5.000000 | 5.000000 | 263.000000 |
df[["Sex", "Cabin"]].describe()
| Sex | Cabin | |
|---|---|---|
| count | 143 | 25 |
| unique | 2 | 23 |
| top | male | C23 C25 C27 |
| freq | 100 | 2 |
Срезы в DataFrame
Наиболее частой ошибкой начинающего пользователя pandas является невнимание к тому, что срез иногда оказывается представлением исходной таблицы, а иногда её копией.
Индексация
После сортировки индекс, приписанный строке, сохраняется и перестаёт совпадать с её номером по порядку.
df.sort_values("Age", inplace=True)
df.head(3)
| PassengerId | Survived | Pclass | Name | Sex | Age | SibSp | Parch | Ticket | Fare | Cabin | Embarked | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 78 | 79 | 1 | 2 | Caldwell, Master. Alden Gates | male | 0.83 | 0 | 2 | 248738 | 29.000 | NaN | S |
| 7 | 8 | 0 | 3 | Palsson, Master. Gosta Leonard | male | 2.00 | 3 | 1 | 349909 | 21.075 | NaN | S |
| 119 | 120 | 0 | 3 | Andersson, Miss. Ellis Anna Maria | female | 2.00 | 4 | 2 | 347082 | 31.275 | NaN | S |
df.iloc[78]
PassengerId 67
Survived 1
Pclass 2
Name Nye, Mrs. (Elizabeth Ramell)
Sex female
Age 29.0
SibSp 0
Parch 0
Ticket C.A. 29395
Fare 10.5
Cabin F33
Embarked S
Name: 66, dtype: object
df.loc[78]
PassengerId 79
Survived 1
Pclass 2
Name Caldwell, Master. Alden Gates
Sex male
Age 0.83
SibSp 0
Parch 2
Ticket 248738
Fare 29.0
Cabin NaN
Embarked S
Name: 78, dtype: object
df.loc[[78, 79, 100], ["Age", "Cabin"]]
| Age | Cabin | |
|---|---|---|
| 78 | 0.83 | NaN |
| 79 | 30.00 | NaN |
| 100 | 28.00 | NaN |
Чтобы изменять данные среза, не затрагивая основную таблицу, достаточно работать с копией.
df_slice_copy = df.loc[[78, 79, 100], ["Age", "Cabin"]].copy()
df_slice_copy[:] = 3
df_slice_copy
| Age | Cabin | |
|---|---|---|
| 78 | 3.0 | 3 |
| 79 | 3.0 | 3 |
| 100 | 3.0 | 3 |
Если же изменять требуется саму основную таблицу, то используется loc.
df.head(3)
| PassengerId | Survived | Pclass | Name | Sex | Age | SibSp | Parch | Ticket | Fare | Cabin | Embarked | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 78 | 79 | 1 | 2 | Caldwell, Master. Alden Gates | male | 0.83 | 0 | 2 | 248738 | 29.000 | NaN | S |
| 7 | 8 | 0 | 3 | Palsson, Master. Gosta Leonard | male | 2.00 | 3 | 1 | 349909 | 21.075 | NaN | S |
| 119 | 120 | 0 | 3 | Andersson, Miss. Ellis Anna Maria | female | 2.00 | 4 | 2 | 347082 | 31.275 | NaN | S |
some_slice = df["Age"].isin([20, 25,30])
df.loc[some_slice, "Fare"] = df.loc[some_slice, "Fare"] * 10
Следующий способ применять не рекомендуется:
slice_df = df[some_slice]
slice_df["Fare"] = slice_df["Fare"] * 10
С pandas 3.0 включён механизм Copy-on-Write, и SettingWithCopyWarning удалён. Запись в slice_df теперь гарантированно попадает в копию, исходная таблица не изменяется, и предупреждения об этом не выдаётся. Правило прежнее: исходная таблица изменяется только через df.loc[маска, столбец] = ....
Столбцы отбираются названием или списком названий в [].
Замечание: если передаётся название одного столбца, то возвращается объект класса pandas.Series, а если список названий столбцов, то возвращается pandas.DataFrame; для получения numpy.array достаточно обратиться к полю values.
Series и DataFrame имеют много общих методов, работающих одинаково в обоих случаях.
df["Age"].head(5)
78 0.83
7 2.00
119 2.00
16 2.00
43 3.00
Name: Age, dtype: float64
df[["Age"]].head(5)
| Age | |
|---|---|
| 78 | 0.83 |
| 7 | 2.00 |
| 119 | 2.00 |
| 16 | 2.00 |
| 43 | 3.00 |
pd.Series
Одномерный срез датафрейма является pd.Series, столбцом значений, снабжённым индексом.
Извлечь из pd.Series обычный np.array можно, однако обычно это не требуется: вместе с массивом теряется индекс.
df["Age"].head(5).values
array([0.83, 2. , 2. , 2. , 3. ])
Индекс извлекается отдельно:
df["Age"].head(5).index
Index([78, 7, 119, 16, 43], dtype='int64')
Создаётся Series так же, как np.array, только индекс задаётся явно.
pd.Series([1, 2, 3], index=["Red", "Green", "Blue"])
Red 1
Green 2
Blue 3
dtype: int64
pd.Series(1, index=["Red", "Green", "Blue"])
Red 1
Green 1
Blue 1
dtype: int64
pd.Series([1, 2, 3], index=["Red", "Green", "Blue"])
Red 1
Green 2
Blue 3
dtype: int64
Series разворачивается обратно в DataFrame:
s = pd.Series([1, 2, 3], index=["Red", "Green", "Blue"])
s.to_frame("Values")
| Values | |
|---|---|
| Red | 1 |
| Green | 2 |
| Blue | 3 |
s.loc["Red"]
1
s.iloc[0]
1
Объединение таблиц
Две таблицы сводятся в одну методом join, сопоставляющим строки по индексу.
df1 = df[["Age", "Parch"]].copy()
df2 = df[["Ticket", "Fare"]].copy()
df1.join(df2).head(5)
| Age | Parch | Ticket | Fare | |
|---|---|---|---|---|
| 78 | 0.83 | 2 | 248738 | 29.0000 |
| 7 | 2.00 | 1 | 349909 | 21.0750 |
| 119 | 2.00 | 2 | 347082 | 31.2750 |
| 16 | 2.00 | 1 | 382652 | 29.1250 |
| 43 | 3.00 | 2 | SC/Paris 2123 | 41.5792 |
df1 = df[["Age", "Parch", "PassengerId"]].copy()
df2 = df[["Ticket", "Fare", "PassengerId"]].copy()
pd.merge(df1, df2, on=["PassengerId"]).head(5)
| Age | Parch | PassengerId | Ticket | Fare | |
|---|---|---|---|---|---|
| 0 | 0.83 | 2 | 79 | 248738 | 29.0000 |
| 1 | 2.00 | 1 | 8 | 349909 | 21.0750 |
| 2 | 2.00 | 2 | 120 | 347082 | 31.2750 |
| 3 | 2.00 | 1 | 17 | 382652 | 29.1250 |
| 4 | 3.00 | 2 | 44 | SC/Paris 2123 | 41.5792 |
pd.merge(df1, df2, on=["PassengerId"], how="inner").head(5)
| Age | Parch | PassengerId | Ticket | Fare | |
|---|---|---|---|---|---|
| 0 | 0.83 | 2 | 79 | 248738 | 29.0000 |
| 1 | 2.00 | 1 | 8 | 349909 | 21.0750 |
| 2 | 2.00 | 2 | 120 | 347082 | 31.2750 |
| 3 | 2.00 | 1 | 17 | 382652 | 29.1250 |
| 4 | 3.00 | 2 | 44 | SC/Paris 2123 | 41.5792 |
Группировка
Средний возраст пассажира по классам каюты может быть вычислен напрямую тремя почти одинаковыми строками. Такой подход не рекомендуется: классов может оказаться тридцать. Для этой задачи предназначен groupby, разбивающий таблицу на группы.
print("Pclass 1: ", df[df["Pclass"] == 1]["Age"].mean())
print("Pclass 2: ", df[df["Pclass"] == 2]["Age"].mean())
print("Pclass 3: ", df[df["Pclass"] == 3]["Age"].mean())
Pclass 1: 36.86363636363637
Pclass 2: 26.68576923076923
Pclass 3: 23.26923076923077
df.groupby(["Pclass"])[["Age"]].mean()
| Age | |
|---|---|
| Pclass | |
| 1 | 36.863636 |
| 2 | 26.685769 |
| 3 | 23.269231 |
Сам по себе groupby ничего не вычисляет, а возвращает объект, хранящий разбиение и ожидающий сводной операции.
df.groupby(["Survived", "Pclass"])
<pandas.core.groupby.generic.DataFrameGroupBy object at 0x73b08903f7d0>
df.groupby(["Survived", "Pclass"])["PassengerId"].count()
Survived Pclass
0 1 18
2 16
3 65
1 1 7
2 11
3 26
Name: PassengerId, dtype: int64
df.groupby(["Survived", "Pclass"])[["PassengerId", "Cabin"]].count()
| PassengerId | Cabin | ||
|---|---|---|---|
| Survived | Pclass | ||
| 0 | 1 | 18 | 12 |
| 2 | 16 | 1 | |
| 3 | 65 | 1 | |
| 1 | 1 | 7 | 7 |
| 2 | 11 | 2 | |
| 3 | 26 | 2 |
df.groupby(["Survived", "Pclass"])[["PassengerId", "Fare"]].describe()
| PassengerId | Fare | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| count | mean | std | min | 25% | 50% | 75% | max | count | mean | std | min | 25% | 50% | 75% | max | ||
| Survived | Pclass | ||||||||||||||||
| 0 | 1 | 18.0 | 82.555556 | 44.501450 | 7.0 | 40.75 | 88.5 | 117.00 | 156.0 | 18.0 | 80.035183 | 66.109719 | 27.7208 | 51.896875 | 61.2771 | 78.721875 | 263.0000 |
| 2 | 16.0 | 107.187500 | 44.842270 | 21.0 | 72.50 | 122.0 | 145.25 | 151.0 | 16.0 | 33.555725 | 32.412477 | 10.5000 | 12.881250 | 23.5000 | 31.740600 | 130.0000 | |
| 3 | 65.0 | 80.892308 | 42.930585 | 1.0 | 49.00 | 87.0 | 114.00 | 155.0 | 65.0 | 19.123272 | 20.710190 | 6.7500 | 7.895800 | 8.0500 | 21.075000 | 98.2500 | |
| 1 | 1 | 7.0 | 84.000000 | 49.568135 | 24.0 | 44.00 | 89.0 | 117.50 | 152.0 | 7.0 | 90.966057 | 85.998766 | 26.2833 | 35.500000 | 63.3583 | 106.560400 | 263.0000 |
| 2 | 11.0 | 57.181818 | 35.261362 | 10.0 | 33.00 | 57.0 | 73.00 | 134.0 | 11.0 | 21.627273 | 10.581905 | 10.5000 | 11.750000 | 26.0000 | 28.375000 | 41.5792 | |
| 3 | 26.0 | 72.807692 | 46.318048 | 3.0 | 34.00 | 72.0 | 109.50 | 147.0 | 26.0 | 16.698400 | 24.254889 | 7.1417 | 7.756250 | 7.9250 | 14.244775 | 124.7500 | |
Работа с временными метками
Показания приборов почти всегда поступают с меткой времени, проставленной системой сбора. Добавим в таблицу столбец с временем в формате Unix и рассмотрим, какие операции pandas предоставляет для него после приведения к своему типу.
tdf = df.copy()
tdf["ts"] = range(1560000000, 1560000000 + tdf.shape[0])
tdf.head(2)
| PassengerId | Survived | Pclass | Name | Sex | Age | SibSp | Parch | Ticket | Fare | Cabin | Embarked | ts | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 78 | 79 | 1 | 2 | Caldwell, Master. Alden Gates | male | 0.83 | 0 | 2 | 248738 | 29.000 | NaN | S | 1560000000 |
| 7 | 8 | 0 | 3 | Palsson, Master. Gosta Leonard | male | 2.00 | 3 | 1 | 349909 | 21.075 | NaN | S | 1560000001 |
Столбец, приведённый к datetime, получает операции, недоступные целому числу.
tdf["ts"] = pd.to_datetime(tdf["ts"], unit="s")
tdf.head(2)
| PassengerId | Survived | Pclass | Name | Sex | Age | SibSp | Parch | Ticket | Fare | Cabin | Embarked | ts | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 78 | 79 | 1 | 2 | Caldwell, Master. Alden Gates | male | 0.83 | 0 | 2 | 248738 | 29.000 | NaN | S | 2019-06-08 13:20:00 |
| 7 | 8 | 0 | 3 | Palsson, Master. Gosta Leonard | male | 2.00 | 3 | 1 | 349909 | 21.075 | NaN | S | 2019-06-08 13:20:01 |
Время, установленное в качестве индекса, делает доступным resample, пересчитывающий ряд на равномерную сетку заданного шага.
tdf.set_index("ts", inplace=True)
tdf.resample("15s").sum()[["PassengerId", "Survived", "Pclass", "Sex"]]
| PassengerId | Survived | Pclass | Sex | |
|---|---|---|---|---|
| ts | ||||
| 2019-06-08 13:20:00 | 862 | 7 | 41 | malemalefemalemalefemalemalefemalefemalemalefe... |
| 2019-06-08 13:20:15 | 1302 | 4 | 40 | femalefemalefemalefemalemalemalefemalefemalefe... |
| 2019-06-08 13:20:30 | 1235 | 3 | 38 | malemalefemalefemalemalemalemalemalemalemalema... |
| 2019-06-08 13:20:45 | 1628 | 6 | 33 | femalefemalemalemalemalemalemalemalefemalemale... |
| 2019-06-08 13:21:00 | 1057 | 5 | 37 | malemalemalemalefemalefemalemalefemalemalemale... |
| 2019-06-08 13:21:15 | 1144 | 6 | 37 | malemalefemalefemalemalefemalemalemalemalemale... |
| 2019-06-08 13:21:30 | 1590 | 0 | 28 | malemalemalemalemalemalemalemalemalemalemalema... |
| 2019-06-08 13:21:45 | 845 | 4 | 33 | malemalemalemalemalemalemalemalemalemalefemale... |
| 2019-06-08 13:22:00 | 912 | 6 | 41 | femalemalemalemalemalefemalemalemalemalemalema... |
| 2019-06-08 13:22:15 | 994 | 3 | 24 | malemalefemalemalemalefemalefemalemale |
Скользящие окна
Скользящее окно (rolling) сглаживает зашумлённый ряд: для каждой точки вычисляется среднее (либо сумма, либо максимум) нескольких соседних. Первые значения, попавшие в неполное окно, остаются пустыми.

tdf.sort_index(inplace=True)
tdf[["Fare"]].rolling(window=5).mean().head(10)
| Fare | |
|---|---|
| ts | |
| 2019-06-08 13:20:00 | NaN |
| 2019-06-08 13:20:01 | NaN |
| 2019-06-08 13:20:02 | NaN |
| 2019-06-08 13:20:03 | NaN |
| 2019-06-08 13:20:04 | 30.41084 |
| 2019-06-08 13:20:05 | 30.19084 |
| 2019-06-08 13:20:06 | 29.31584 |
| 2019-06-08 13:20:07 | 28.61084 |
| 2019-06-08 13:20:08 | 30.72334 |
| 2019-06-08 13:20:09 | 26.62250 |
Скользящее окно сочетается с группировкой: окно внутри каждой группы вычисляется независимо.
rol = tdf[["Sex", "Fare"]].groupby(["Sex"]).rolling(window=5).mean()
rol.head(100)
| Fare | ||
|---|---|---|
| Sex | ts | |
| female | 2019-06-08 13:20:02 | NaN |
| 2019-06-08 13:20:04 | NaN | |
| 2019-06-08 13:20:06 | NaN | |
| 2019-06-08 13:20:07 | NaN | |
| 2019-06-08 13:20:09 | 27.67584 | |
| ... | ... | ... |
| male | 2019-06-08 13:21:26 | 14.02416 |
| 2019-06-08 13:21:27 | 17.12416 | |
| 2019-06-08 13:21:28 | 12.72000 | |
| 2019-06-08 13:21:29 | 16.34084 | |
| 2019-06-08 13:21:30 | 19.81000 |
100 rows × 1 columns
rol.loc['male'].head(10)
| Fare | |
|---|---|
| ts | |
| 2019-06-08 13:20:00 | NaN |
| 2019-06-08 13:20:01 | NaN |
| 2019-06-08 13:20:03 | NaN |
| 2019-06-08 13:20:05 | NaN |
| 2019-06-08 13:20:08 | 29.35750 |
| 2019-06-08 13:20:11 | 32.93750 |
| 2019-06-08 13:20:12 | 30.97084 |
| 2019-06-08 13:20:19 | 26.98918 |
| 2019-06-08 13:20:20 | 28.28418 |
| 2019-06-08 13:20:26 | 22.64668 |
Работа со строками
Строковые столбцы имеют аксессор .str, через который к каждому элементу применяются обычные строковые методы, выстраиваемые в цепочку. Ниже из полного имени пассажира извлекается личное имя: строка приводится к нижнему регистру, запятая заменяется, строка разрезается по точке и берётся вторая часть.
df["Name"].str.lower()\
.str.replace(",", " ")\
.str.split(".").str[1]\
.head(10)
78 alden gates
7 gosta leonard
119 ellis anna maria
16 eugene
43 simonne marie anne andree
63 harald
10 marguerite rut
58 constance mirium
50 juha niilo
24 torborg danira
Name: Name, dtype: object
Пропущенные значения
В реальных данных пропуски присутствуют всегда: прибор не сработал, поле анкеты осталось пустым. Pandas обозначает их как NaN и предоставляет несколько способов их обработки: строки с пропуском удаляются целиком, пропуски заполняются значением или находятся маской.
df["Cabin"].head(15)
78 NaN
7 NaN
119 NaN
16 NaN
43 NaN
63 NaN
10 G6
58 NaN
50 NaN
24 NaN
147 NaN
59 NaN
125 NaN
39 NaN
9 NaN
Name: Cabin, dtype: object
df["Cabin"].dropna().head(15)
10 G6
136 D47
27 C23 C25 C27
102 D26
151 C2
97 D10 D12
88 C23 C25 C27
118 B58 B60
139 B86
75 F G73
23 A6
66 F33
21 D56
148 F2
137 C123
Name: Cabin, dtype: object
df["Cabin"].fillna(3).head(5)
78 3
7 3
119 3
16 3
43 3
Name: Cabin, dtype: object
df["Cabin"].bfill().head(15)
78 G6
7 G6
119 G6
16 G6
43 G6
63 G6
10 G6
58 D47
50 D47
24 D47
147 D47
59 D47
125 D47
39 D47
9 D47
Name: Cabin, dtype: object
pd.isna(df["Cabin"]).head(10)
78 True
7 True
119 True
16 True
43 True
63 True
10 False
58 True
50 True
24 True
Name: Cabin, dtype: bool
Функция apply
Когда готовой векторной операции не нашлось, остаётся apply, применяющий заданную функцию к каждой строке (axis=1) или к каждому столбцу. Внутри работает обычный цикл Python со всеми издержками, рассмотренными в главе про производительность, поэтому перед написанием apply следует ещё раз поискать векторное решение.
def dummpy_example(row):
return row['Sex'] * row['Pclass']
df['dummy_example'] = df.apply(dummpy_example, axis=1)
df.tail(3)
| PassengerId | Survived | Pclass | Name | Sex | Age | SibSp | Parch | Ticket | Fare | Cabin | Embarked | dummy_example | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 128 | 129 | 1 | 3 | Peter, Miss. Anna | female | NaN | 1 | 1 | 2668 | 22.3583 | F E69 | C | femalefemalefemale |
| 140 | 141 | 0 | 3 | Boulos, Mrs. Joseph (Sultana) | female | NaN | 0 | 2 | 2678 | 15.2458 | NaN | C | femalefemalefemale |
| 154 | 155 | 0 | 3 | Olsen, Mr. Ole Martin | male | NaN | 0 | 0 | Fa 265302 | 7.3125 | NaN | S | malemalemale |
Визуализация
Метод plot() строит ряд как есть, resample("10s").mean().plot() сначала усредняет его по десятисекундным интервалам и даёт сглаженную кривую, а hist() строит гистограмму распределения.
tdf["Fare"].plot()
<Axes: xlabel='ts'>

tdf["Fare"].resample("10s").mean().plot()
<Axes: xlabel='ts'>

tdf["Sex"].hist()
<Axes: >
