Зачем нужен pandas и Series изнутри
Почему pandas победил в анализе данных и что такое Series на самом деле, а не «список с подписями».
Series — одномерный массив значений, у каждого из которых есть метка (индекс). Индекс — не украшение, а механизм, по которому pandas выравнивает данные при любой операции.
Место pandas в стеке данных
Python сам по себе для анализа табличных данных неудобен: списки и словари не знают про столбцы, типы и пропуски, а циклы по строкам медленные. numpy даёт быстрые числовые массивы, но у них нет меток, разнородных типов в столбцах и удобной работы с пропусками. pandas надстраивается над numpy и добавляет два главных понятия: метки (индекс) и выравнивание по этим меткам. Внизу почти всё считается numpy-операциями на C, поэтому pandas быстрый, а сверху вы работаете в терминах «столбец Цена», а не «третий массив».
Типичное место pandas в работе с данными: загрузить сырой CSV или выгрузку из базы, почистить (пропуски, типы, дубликаты), преобразовать (новые признаки, агрегации), объединить с другими таблицами и отдать результат дальше — в график, отчёт, модель машинного обучения или обратно в базу. Это «рабочая лошадь» между источником данных и конечным потребителем.
Series: значения плюс индекс
Series создаётся из списка, и pandas сам присваивает целочисленный индекс 0, 1, 2…, либо вы задаёте метки явно:
import pandas as pd
s = pd.Series([10, 20, 30], index=["a", "b", "c"])
print(s)
# a 10
# b 20
# c 30
# dtype: int64
print(s["b"]) # 20 — доступ по метке
print(s.values) # [10 20 30] — "голый" numpy-массив
print(s.index) # Index(['a', 'b', 'c'], dtype='object')
Series — это пара «массив значений + объект-индекс». Значения хранятся как один типизированный numpy-массив (отсюда единый dtype на всю Series), а индекс — это отдельная структура с метками. Понимание этой пары объясняет почти всё поведение pandas.
Часто Series сравнивают со словарём: и там, и там значение достаётся по ключу. Но различия принципиальны. Во-первых, метки индекса могут повторяться — в словаре ключи уникальны, а в Series вполне может быть две строки с меткой "москва". Во-вторых, у Series есть порядок и позиционный доступ, чего у словаря по смыслу нет. В-третьих, значения Series лежат в едином типизированном массиве, поэтому над ними работают быстрые векторные операции, недоступные обычному словарю. Создать Series из словаря, кстати, можно напрямую — ключи станут индексом: pd.Series({"a": 1, "b": 2}).
Главная идея: выравнивание по индексу
Когда вы складываете две Series, pandas не складывает их «позиция к позиции», как numpy. Он сначала сопоставляет элементы по меткам индекса, и только потом считает. Метки, которых нет в одной из Series, дают пропуск NaN.
prices = pd.Series([100, 200, 300], index=["хлеб", "молоко", "сыр"])
discount = pd.Series([10, 50], index=["сыр", "хлеб"])
print(prices - discount)
# молоко NaN ← метки 'молоко' нет в discount
# сыр 290.0
# хлеб 90.0
# dtype: float64
Обратите внимание: порядок в результате выровнен по объединению меток, а не по тому, как вы их перечислили. И тип стал float64, потому что NaN — это число с плавающей точкой, и pandas «повысил» весь столбец. Это и есть выравнивание по индексу — фундамент, на котором стоят merge, join, операции между столбцами и присваивание.
Зачем это сделано именно так? Представьте, что numpy складывал бы по позициям: тогда, перемешав строки в одной из таблиц, вы получили бы тихо неправильный результат — цена хлеба сложилась бы со скидкой на сыр. Выравнивание по меткам делает операции корректными независимо от порядка строк: pandas сам сопоставит «хлеб» с «хлебом». Это снимает с вас целый класс ошибок, но требует следить за тем, чтобы метки были осмысленными и сопоставимыми. Если же выравнивание не нужно (вы точно знаете, что порядки совпадают, и хотите скорости numpy), можно работать с .values или привести индексы к одинаковым заранее.
Чтобы прочувствовать механику, реализуем выравнивание вручную на чистом Python — pandas внутри делает примерно то же самое, только на C и для целых массивов сразу:
prices = {"хлеб": 100, "молоко": 200, "сыр": 300}
discount = {"сыр": 10, "хлеб": 50}
# объединяем метки обеих "Series"
labels = sorted(set(prices) | set(discount))
result = {}
for key in labels:
if key in prices and key in discount:
result[key] = prices[key] - discount[key]
else:
result[key] = None # аналог NaN: метки нет в одной из сторон
for key in labels:
print(key, result[key])
Вывод:
молоко None сыр 290 хлеб 50
Мы вручную собрали объединение меток и для отсутствующих поставили None — ровно так pandas получает NaN. Понимание этого избавляет от половины будущих сюрпризов.
Векторизация: операции над всем массивом сразу
Series поддерживает векторизованные операции: вы пишете действие над всей Series, а не цикл по элементам. Это и короче, и в десятки раз быстрее, потому что цикл уходит в C.
s = pd.Series([1, 2, 3, 4])
print(s * 10) # умножение всех элементов
print(s[s > 2]) # булева маска: только элементы > 2
print(s.mean()) # 2.5 — агрегаты тоже встроены
Покажем выигрыш векторизации без pandas — сравним «поэлементный цикл» с операцией над всем массивом по смыслу. В stdlib векторизации нет, но идея «одна операция вместо явного цикла» видна и так:
data = [1, 2, 3, 4, 5]
# поэлементно, "как новичок"
result_loop = []
for x in data:
result_loop.append(x * x)
# одной операцией над всей коллекцией (идея векторизации)
result_vec = [x * x for x in data]
print("loop:", result_loop)
print("vec: ", result_vec)
print("совпадают:", result_loop == result_vec)
Вывод:
loop: [1, 4, 9, 16, 25] vec: [1, 4, 9, 16, 25] совпадают: True
В pandas s ** 2 сделает то же самое, но вычисление уйдёт в numpy и пройдёт по всему массиву за один проход на C, без интерпретатора Python на каждом шаге.
У векторизации есть и второе важное свойство, помимо скорости: она работает в связке с выравниванием и пропусками. Когда вы пишете s1 + s2, pandas не только складывает на C, но и выравнивает по индексу и аккуратно протаскивает NaN через вычисление (любая арифметика с NaN даёт NaN). Поэтому векторный код в pandas — это не просто «быстрый цикл», а «быстрый цикл, который сам знает про метки и пропуски». Большинство встроенных агрегатов при этом по умолчанию игнорируют пропуски: s.mean() посчитает среднее по непустым значениям, а не вернёт NaN из-за одной дыры (управляется параметром skipna).
Доступ к элементам: ловушка целочисленного индекса
У Series есть две системы координат: по метке и по позиции. Пока индекс — это строки (a, b, c), всё однозначно. Но если индекс сам целочисленный, обычные квадратные скобки трактуют число как метку, а не позицию, и это путает.
s = pd.Series(["x", "y", "z"], index=[10, 20, 30])
s[10] # 'x' — это МЕТКА 10, а не "десятый элемент"
s.iloc[0] # 'x' — а вот это позиция 0, всегда однозначно
s.loc[10] # 'x' — явный доступ по метке
Правило на всю жизнь: для предсказуемости используйте явные .loc (по меткам) и .iloc (по позициям). Подробно мы разберём их в следующем разделе, но привычку стоит закладывать сразу.
Полезные операции над Series
Помимо арифметики, у Series есть богатый набор методов, которые пригодятся постоянно. Стоит знать их с самого начала:
| Метод | Что делает |
s.value_counts() | частоты уникальных значений (мини-гистограмма по столбцу) |
s.unique() / s.nunique() | уникальные значения / их число |
s.sort_values() | сортировка по значениям |
s.isna() / s.fillna(x) | найти / заполнить пропуски |
s.map(func) | применить функцию или словарь к каждому элементу |
s.astype(тип) | сменить dtype |
value_counts() заслуживает отдельного упоминания: это, пожалуй, самый используемый разведочный метод. «Сколько у меня записей каждого статуса», «какие города встречаются и насколько часто» — один вызов, и вы видите структуру столбца. По умолчанию он сортирует по убыванию частоты и не считает пропуски (включаются через dropna=False).
Подводные камни
- Появился NaN — проверьте индексы. Если после арифметики между Series вылезли пропуски, почти всегда метки не совпали. Это не «баг», а выравнивание.
- Тип «уплыл» в float. Один
NaNпревращает целочисленную Series вfloat64. Чтобы хранить целые с пропусками, нужен nullable-типInt64(о нём — в уроке про dtypes). - Не путайте
s.valuesи Series.s.valuesтеряет индекс и возвращает numpy-массив; почти всегда вам нужна сама Series с метками.
Лучшие практики
- Думайте о Series как о «массиве с метками», а не о словаре: метки могут повторяться, а порядок важен.
- Опирайтесь на выравнивание: оно делает код корректным «по построению», если индексы осмысленные (id клиента, дата, код товара).
- Предпочитайте векторные операции явным циклам — это быстрее и читается как формула.
Итог
- pandas = numpy + метки + выравнивание по меткам.
- Series — пара «типизированный массив значений + индекс».
- Операции между Series выравниваются по индексу; несовпадение меток даёт NaN.
- Векторизация заменяет циклы и работает на C.