Зачем нужен 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.
Проверьте себя
1. Что произойдёт при сложении двух Series с частично разными метками индекса?
AСложение идёт по позициям, как в numpy
Bpandas выровняет по меткам; несовпавшие метки дадут NaN
CБудет ошибка из-за разной длины
DОстанутся только метки первой Series
2. Series s имеет целочисленный индекс [10, 20, 30]. Что вернёт s[10]?
AЭлемент на позиции 10 (будет ошибка)
BЭлемент с меткой 10 — первый по порядку
CСрез до позиции 10
DСписок из 10 элементов
3. Почему целочисленная Series становится float64 после появления одного NaN?
AЭто случайная ошибка округления
BNaN — значение типа float, и pandas повышает тип всего массива
Cpandas всегда хранит данные как float
DИз-за выравнивания индекса теряется точность
Поддержать проект