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