Pandas Series: что это и как с ней работать, связь с DataFrame

Pandas Series: что это и как с ней работать, связь с DataFrame Полезное

Series в pandas — это одномерный массив значений, у каждого из которых есть метка (индекс), причем у всей Series один тип данных (dtype). Исключение по смыслу — тип object: формально он один, но внутри могут лежать разнородные объекты Python. Столбец таблицы DataFrame — это тоже Series: отсюда и связь двух главных структур pandas. Ниже — как создать Series, как обращаться к элементам по меткам и по позициям, почему сложение двух Series иногда дает NaN и какие методы нужны чаще всего.

Примеры проверены 24.09.2026 на pandas 3.0.6 и Python 3.14.7. В pandas 2.x часть поведения отличается — такие места отмечены.

Четыре части любой Series

Чтобы дальше не путаться, разведем термины:

Часть Что это Как получить
значения (values) сами данные, внутри обычно массив NumPy s.to_numpy()
индекс (index) метки элементов: числа, строки, даты s.index
тип (dtype) один тип на всю Series: int64, float64, str, Int64 s.dtype
имя (name) подпись; у столбца DataFrame совпадает с его названием s.name

Ключевое отличие от списка Python: индекс — это метки, а не номера позиций. По умолчанию метки совпадают с позициями 0, 1, 2, но после фильтрации, сортировки или создания из словаря они расходятся.

Минимальный пример

Установка: pip install pandas (NumPy подтянется как зависимость). Pandas 3 требует Python 3.11 или новее: на Python 3.10 и старше та же команда молча поставит pandas 2.x, и часть примеров ниже поведет себя иначе. Проверить версию можно так: print(pd.__version__).

import pandas as pd

sales = pd.Series([120, 95, 140], index=["пн", "вт", "ср"], name="продажи")
print(sales)
print(sales["вт"])
print(sales.mean())
пн    120
вт     95
ср    140
Name: продажи, dtype: int64
95
118.33333333333333

Слева печатается индекс, справа значения, внизу имя и тип. Обращение sales["вт"] идет по метке, а mean() считает среднее сразу по всем значениям, без цикла.

Как создать Series

import pandas as pd

a = pd.Series([10, 20, 30])                          # из списка
b = pd.Series({"Москва": 13.1, "Казань": 1.3})       # из словаря: ключи -> индекс
c = pd.Series(0, index=["x", "y", "z"])              # скаляр размножается по индексу
d = pd.Series(["Python", "SQL", None])               # строки
e = pd.Series([1, 2, None])                          # целые с пропуском
f = pd.Series([1, 2, None], dtype="Int64")           # целые с пропуском, nullable-тип
for s in (a, b, c, d, e, f):
    print(s.dtype, list(s.index))
int64 [0, 1, 2]
float64 ['Москва', 'Казань']
int64 ['x', 'y', 'z']
str [0, 1, 2]
float64 [0, 1, 2]
Int64 [0, 1, 2]

На что смотреть:

  • Строки в pandas 3 по умолчанию получают тип str (отдельный строковый dtype). В pandas 2.x та же Series была бы object.
  • Один пропуск превращает целые в float64: обычный int64 не умеет хранить NaN. Если нужны именно целые с пропусками, укажите dtype="Int64" (с большой буквы) — пропуск тогда печатается как <NA>.

Индекс: loc, iloc и квадратные скобки

У Series два способа адресации, и их важно не смешивать:

Способ Ищет по Срез a:b Пример
s.loc[...] метке индекса включает правую границу s.loc[10:30]
s.iloc[...] позиции (0, 1, 2…) не включает правую границу, как в списке s.iloc[0:3]
s[...] в pandas 3 — по метке для одиночного ключа зависит от типа индекса лучше явно loc/iloc
import pandas as pd

s = pd.Series([100, 200, 300, 400], index=[10, 20, 30, 40])
print(s.loc[20])               # 200 - метка 20
print(s.iloc[1])               # 200 - вторая позиция
print(s.loc[10:30].tolist())   # [100, 200, 300] - метка 30 включена
print(s.iloc[0:3].tolist())    # [100, 200, 300] - позиция 3 не включена
print(s[0])                    # KeyError: 0 - метки 0 нет

Последняя строка падает с KeyError: 0: квадратные скобки с одним числом ищут метку, а не позицию. При целочисленном индексе так было и в pandas 2.x. А вот для Series со строковым индексом (t[0] при индексе a, b, c) поведение поменялось: в pandas 2.x такой вызов еще срабатывал как позиционный с предупреждением FutureWarning, в 3.0 этот откат убран, и тоже получается KeyError: 0. Исправление — писать s.iloc[0], если нужен первый элемент.

Фильтрация условием тоже идет через индексацию: условие дает булеву Series той же длины.

print(s[s > 150].tolist())                      # [200, 300, 400]
print(s.loc[(s > 150) & (s < 400)].tolist())    # [200, 300]

Условия соединяются через & и |, каждое в скобках. Слова and/or здесь не работают — об этом ниже, в разделе ошибок.

Векторные операции и выравнивание по индексу

Арифметика над Series выполняется сразу для всех элементов: s * 2, s + 10, s / s.sum(). Но при операции с двумя Series pandas сопоставляет элементы по меткам, а не по позициям. Это главная особенность, которая удивляет новичков.

import pandas as pd

jan = pd.Series({"Анна": 5, "Борис": 3, "Вера": 7})
feb = pd.Series({"Борис": 4, "Вера": 2, "Глеб": 6})
print(jan + feb)
print(jan.add(feb, fill_value=0))
Анна     NaN
Борис    7.0
Вера     9.0
Глеб     NaN
dtype: float64
Анна     5.0
Борис    7.0
Вера     9.0
Глеб     6.0
dtype: float64

Индекс результата — объединение меток обеих Series. Где метка есть только с одной стороны, получается NaN, а тип из-за этого становится float64. Метод add(..., fill_value=0) считает отсутствующее значение нулем. Аналогично работают sub, mul, div.

Обратная ситуация: данные идут в одном порядке, но индексы перепутаны, и нужно сложить именно по позициям.

x = pd.Series([1, 2, 3], index=["a", "b", "c"])
y = pd.Series([10, 20, 30], index=["c", "b", "a"])
print((x + y).tolist())                   # [31, 22, 13] - сложено по меткам
print(x.to_numpy() + y.to_numpy())        # [11 22 33] - сложено по позициям

Какой вариант верный, зависит от смысла данных. Если метки — это ключи (ФИО, id товара), выравнивание защищает от ошибки сдвига. Если метки случайные (после фильтрации), сначала выровняйте индекс через reset_index(drop=True) или работайте с массивом to_numpy().

Пропуски: NaN, isna и fillna

import pandas as pd

e = pd.Series([1.5, None, 3.0])
print(len(e), e.count(), e.isna().sum())   # 3 2 1
print(e.fillna(0).tolist())                 # [1.5, 0.0, 3.0]

len считает все элементы, count() — только непустые. Статистика вроде mean() и sum() по умолчанию пропуски пропускает. Сравнение s == None для поиска пропусков не подходит — используйте isna().

value_counts, map и apply

import pandas as pd

langs = pd.Series(["python", "sql", "python", "java", "python", "sql"])
print(langs.value_counts())
print(langs.map({"python": "Python", "sql": "SQL"}))
print(langs.str.len().tolist())
print(langs.apply(lambda w: w.upper() if len(w) > 3 else w).tolist())
python    3
sql       2
java      1
Name: count, dtype: int64
0    Python
1       SQL
2    Python
3       NaN
4    Python
5       SQL
dtype: str
[6, 3, 6, 4, 6, 3]
['PYTHON', 'sql', 'PYTHON', 'JAVA', 'PYTHON', 'sql']
  • value_counts() — частоты значений по убыванию; с normalize=True — доли. Результат сам является Series: значения стали индексом.
  • map(словарь) — замена по справочнику. Значение, которого нет в словаре, становится NaN (здесь java), и это легко пропустить.
  • .str — векторные строковые методы (len, lower, contains), они быстрее и короче apply.
  • apply(функция) — произвольная функция для каждого элемента. Она удобна, но выполняется циклом на Python, поэтому для арифметики и строк сначала ищите встроенный векторный метод.

Series и DataFrame: столбец и строка

DataFrame можно представить как набор Series с общим индексом строк. Выбор одного столбца возвращает Series, выбор строки через loc — тоже Series, но уже с названиями столбцов в индексе.

import pandas as pd

df = pd.DataFrame({"курс": ["Python", "SQL", "Java"], "цена": [100, 80, 120]})
price = df["цена"]
print(type(price).__name__, price.name)    # Series цена
print(df.loc[0])
cols = ["курс", "цена"]
print(type(df.loc[:, cols]).__name__)      # DataFrame
курс    Python
цена       100
Name: 0, dtype: object

Строка смешивает текст и число, поэтому ее тип — object. Выбор списка столбцов возвращает уже DataFrame. Подробно про таблицы — фильтрацию, группировку, объединение — в статье о работе с DataFrame.

Copy-on-Write: изменения через Series в pandas 3

С pandas 3.0 по умолчанию включен режим Copy-on-Write: любой результат индексации ведет себя как отдельная копия. Для Series это значит следующее.

price = df["цена"]
price.iloc[0] = 999
print(df["цена"].tolist(), price.tolist())   # [100, 80, 120] [999, 80, 120]

df["цена"][0] = 999                          # цепочка присваиваний
print(df["цена"].tolist())                   # [100, 80, 120] + ChainedAssignmentError

df.loc[0, "цена"] = 999                      # одно действие
print(df["цена"].tolist())                   # [999, 80, 120]

Изменение извлеченной Series не трогает DataFrame, а цепочка df["цена"][0] = ... не работает и выдает предупреждение ChainedAssignmentError. В pandas 2.x без включенного Copy-on-Write первые два варианта могли изменить исходную таблицу, отсюда много старых примеров в интернете, которые в 3.0 молча ничего не меняют. Правильный способ — присваивание одним шагом через df.loc[строка, столбец].

Типичные ошибки

Симптом Причина Что делать
KeyError: 0 при s[0] скобки ищут метку, а не позицию s.iloc[0]
после сложения появились NaN индексы двух Series не совпали add(..., fill_value=0) или reset_index(drop=True)
целые стали 1.0, 2.0 в данных есть пропуск dtype="Int64" или fillna до приведения
ValueError: The truth value of a Series is ambiguous if s: или and/or с Series s.empty, s.any(), s.all(); в условиях — & и побитовое ИЛИ
изменение не попало в таблицу Copy-on-Write в pandas 3 df.loc[...] = ... одним шагом

Про ошибку truth value is ambiguous: в pandas 3.0.6 ее текст по-прежнему предлагает a.bool(), но метод Series.bool() в 3.0 удален. Опирайтесь на empty, any(), all() или item().

Выводы

  • Series — одномерные данные с индексом-метками и одним dtype; столбец DataFrame — это Series.
  • loc работает по меткам и включает правую границу среза, iloc — по позициям; в pandas 3 s[0] ищет метку.
  • Операции над двумя Series выравниваются по индексу: несовпавшие метки дают NaN, fill_value это исправляет.
  • Для частот, замен и преобразований — value_counts, map, .str; apply оставлять для нестандартной логики.
  • В pandas 3 действует Copy-on-Write: менять таблицу через df.loc, а не через извлеченную Series.

Где применяется / связь с практикой

Series встречается в любой задаче анализа данных: столбец выручки, частоты категорий, результат groupby по одному показателю, временной ряд с датами в индексе. Понимание выравнивания по индексу и Copy-on-Write избавляет от тихих ошибок в отчетах, когда код отрабатывает без исключений, но считает не то.

Освойте тему на практике

Системно пройти pandas, NumPy, визуализацию и подготовку данных для аналитики можно на курсе «Python для аналитики». Разобрать отдельные темы с преподавателями вживую можно на открытых уроках Otus.

FAQ

Чем Series отличается от списка Python и от массива NumPy?
У списка нет меток и может быть смесь типов; у массива NumPy есть единый тип, но нет индекса-меток. Series добавляет к массиву индекс, имя и выравнивание по меткам при операциях.

Как превратить Series в DataFrame и обратно?
s.to_frame() дает таблицу из одного столбца с именем Series (или 0, если имени нет), а df["столбец"] возвращает Series обратно. Несколько Series с общим индексом объединяет pd.concat(..., axis=1).

Как узнать, есть ли значение среди элементов Series?
Оператор in проверяет индекс, а не значения: "вт" in sales вернет True по метке. Для значений нужно s.isin([95]).any() или 95 in s.to_numpy().

OTUS Журнал
Скидка 5% 12-19 октября на курсы (popup)
Бесплатные открытые уроки (поп-ап)