Столбцы в pandas#

Резюмируя содержание предыдущей страницы, столбцы таблиц в pandas представляются объектами pandas.Series, у которых есть сходства и различия с одномерными массивами NumPy: оба являются последовательностями данных (т.е. элементы столбца пронумерованы и по номеру можно получить доступ к соответствующему значению в столбце) одного типа, но в отличие от массивов NumPy столбцы pandas поддерживают продвинутую индексацию.

Кроме этого важно помнить, что содержимое столбцов pandas изменяемо, а длина — нет.

horizontal_line = "_" * 80  

Создание#

Создать столбец pandas можно многими разными способами. Сам по себе pandas столбец обычно создают конструктором pandas.Series. С помощью него создать столбец можно из

  1. скаляра (число, str);

  2. последовательности (list, np.array и т.д.);

  3. словаря.

Можно создать и пустой pd.Series, но большого смысла в этом нет из-за невозможности наращивать длину столбца на месте.

Скаляр#

Если передать в качестве параметра скаляр, то создаётся столбец из одного элемента. При этом строка тоже считается скаляром, несмотря на то, что она формально является последовательностью.

import pandas as pd
import numpy as np

s = pd.Series("abc")
s
0    abc
dtype: object

Вывод сообщает, что s — столбец из одного элемента "abc" типа object, индекс которого равен 0.

  • object — самый общий тип данных в столбце. В pd.Series типа object можно хранить данные любого типа, но этого следует по возможности избегать из соображений производительности, читабельности и удобства применений методов библиотеки pandas. Указать тип столбца можно дополнительным параметром dtype (“string” для строк).

  • при создании pd.Series скаляра, индекс единственного элемента будет равен 0. Можно явно указать индекс с помощью параметра index.

s = pd.Series("abc", dtype="string", index=[42])
s
42    abc
dtype: string

Последовательность.#

Более содержательный пример — создание pd.Series из списка или массива NumPy.

data = [7, 12, 42]
s = pd.Series(data)
s
0     7
1    12
2    42
dtype: int64

По умолчанию генерируется RangeIndex, который соответствует смещению. При необходимости можно в явном виде указать необходимый индекс в виде последовательности той же длинны: за это отвечает параметр index.

index = ["a", "b", "c"]
s = pd.Series(data, index=index)
s
a     7
b    12
c    42
dtype: int64

На самом деле данные столбца хранятся в массиве NumPy, к которому можно получить доступ по атрибуту values столбца.

values = s.values
print(f"{values=}")
print(f"{type(values)=}")
values=array([ 7, 12, 42], dtype=int64)
type(values)=<class 'numpy.ndarray'>

Код в ячейке ниже демонстрирует, что изменение в возвращенном массиве отражаются на исходном столбце.

print(s)
print(horizontal_line)

values[0] = 0
print(s)
a     7
b    12
c    42
dtype: int64
________________________________________________________________________________
a     0
b    12
c    42
dtype: int64

Тип столбца выводится согласно правилам NumPy: наиболее общий тип (вплоть до object), если не указывается явно.

Словарь#

При создании pd.Series из словаря, ключи попадают в индекс столбца, а значения попадают в данные столбца.

d = {"a": 7, "b": 12, "c": 42}
s = pd.Series(d)
s
a     7
b    12
c    42
dtype: int64

Индексация#

Подробнее разберем особенности индексации объектов pandas. Индексация строк таблиц очень похожа на индексацию элементов столбца, и многие утверждения в этом разделе работают аналогично и в случае таблиц pandas.

Индексация по метке vs индексация по смещению#

Индексация с помощью .loc[] vs .iloc[]#

Получать значение из столбца можно двумя способами:

  • по метке (используя индекс столбца) — “.loc[]”;

  • по целочисленному смещению — “.iloc[]”.

Продемонстрируем разницу между “.loc[]” и “.iloc[]” на примере столбца из предыдущего раздела.

print(s)
a     7
b    12
c    42
dtype: int64

Индексирование с помощью “.iloc[]” эквивалентно индексированию лежащему под капотом массиву NumPy, т.е. это индексирование по смещению.

for i in range(3):
    print(f"{i=}: {s.iloc[i]=}")
i=0: s.iloc[i]=7
i=1: s.iloc[i]=12
i=2: s.iloc[i]=42

Индексирование с помощью “.loc[]” подразумевает использование индекса столбца, т.е. доступ осуществляется по метке.

for label in "abc":
    print(f"{label=}: {s.loc[label]=}")
label='a': s.loc[label]=7
label='b': s.loc[label]=12
label='c': s.loc[label]=42

Индексация с помощью [] и почему рекомендуется её избегать#

Вообще говоря, можно использовать для индексации и просто “[]”. Тогда способ индексации определятся по переданному значению. В нашем примере метки представляются строками, а значит если указать между пары квадратных скобок строку, то индексация будет произведена по метке.

for i in range(3):
    print(f"{i=}: {s[i]=}")

print(horizontal_line)

for label in "abc":
    print(f"{label=}: {s[label]=}")
i=0: s[i]=7
i=1: s[i]=12
i=2: s[i]=42
________________________________________________________________________________
label='a': s[label]=7
label='b': s[label]=12
label='c': s[label]=42

Если индекс столбца сам по себе является целочисленным (например, создаваемый по умолчанию RangeIndex), то при использовании “[]pandas попытается индексировать именно по метке.

s = pd.Series(data=[7, 13, 42], index=[7, 13, 42])
print(s)

print(horizontal_line)

for label in (7, 13, 42):
    print(f"{label=}: {s[label]=}")

print(horizontal_line)

try:
    s[0]
except KeyError:
    print("Ошибка при обращении по метке '0'.") # Только в случае ошибки
7      7
13    13
42    42
dtype: int64
________________________________________________________________________________
label=7: s[label]=7
label=13: s[label]=13
label=42: s[label]=42
________________________________________________________________________________
Ошибка при обращении по метке '0'.

Нередко осмысленного индекса придумать не удаётся и вы довольствуетесь сгенерированным по умолчанию RangeIndex. В таком случае следует проявлять особую осторожность. Хотя изначальный индекс и совпадает с индексацией по смещению, а значит “[]”, “.loc[]” и “.iloc[]” произведут одинаковый эффект. Однако в процессе работы могут получиться столбцы, у которых такое свойство нарушится, что может привести к логическим ошибкам.

Для демонстрации создадим новый столбец s.

s = pd.Series([1, 2, 3])
print(s)

print(horizontal_line)

for i in range(3):
    print(f"{i=}: {s[i]=}, {s.iloc[i]=}, {s.loc[i]=}")
0    1
1    2
2    3
dtype: int64
________________________________________________________________________________
i=0: s[i]=1, s.iloc[i]=1, s.loc[i]=1
i=1: s[i]=2, s.iloc[i]=2, s.loc[i]=2
i=2: s[i]=3, s.iloc[i]=3, s.loc[i]=3

Видим, что индексация с помощью “[]”, “.loc[]” и “.iloc[]” приводит к одинаковому результату.

Теперь извлечем последние два элемента этого столбца простым срезом.

tail = s.iloc[-2:]
print(tail)
1    2
2    3
dtype: int64

Обратите внимание, что в итоговом столбце метки начинаются с 1, а значит теперь индексация “.loc[]” и “.iloc[]” приведет к разным результатам.

for i in (0, 1):
    print(f"{i=}: {tail.iloc[i]=}")

print(horizontal_line)

for i in (1, 2):
    print(f"{i=}: {tail.loc[i]=}")
i=0: tail.iloc[i]=2
i=1: tail.iloc[i]=3
________________________________________________________________________________
i=1: tail.loc[i]=2
i=2: tail.loc[i]=3

Индексация с помощью “[]” в таком случае эквивалента индексации с помощью “.”, т.е. обращение по нулевому индексу s[0] — ошибка.

Warning

В связи с тем, что индексация с помощью “[]” явным образом не указывает, будет ли использоваться индексация по меткам или по смещению, а также из-за того, что использование “[]” предрасполагает к логическим ошибкам, разработчики и сообщество программистов рекомендуют не использовать[]” для индексации столбцов pandas никогда и всегда использовать или “.loc[]” или “.iloc[]”, чтобы явно сообщить свои намерения.

Индексация по метке#

Индексация по смещению работает как в NumPy, а индексация по метке больше похожа на получения значений по ключу в словарях (dict). Однако пара отличий все же есть.

Во-первых, в метках могут содержаться дубликаты. Если индексировать по метке, которая встречается в индексе не единожды, то в качестве результата вы получите новый столбец.

duplicates = pd.Series([1, 2, 3], index=["a", "b", "b"])
print(duplicates)

print(horizontal_line)

duplicates.loc["b"]
a    1
b    2
b    3
dtype: int64
________________________________________________________________________________
b    2
b    3
dtype: int64

Во-вторых, можно за один раз извлекать сразу несколько элементов. Чтобы продемонстрировать, создадим новый столбец.

values = range(6)
index = list("aecdbf")
s = pd.Series(values, index)
print(s)
a    0
e    1
c    2
d    3
b    4
f    5
dtype: int64

Можно передать в “.loc[]” список меток и получить столбец с теми же значениями.

s.loc[["b", "c", "f"]]
b    4
c    2
f    5
dtype: int64

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

s.loc["e":"b"]
e    1
c    2
d    3
b    4
dtype: int64

Индексация булевыми масками#

Как и массивы NumPy столбцы pandas можно индексировать булевыми масками. в случае булевой маски mask, выражения “.loc[mask]” и “.iloc[mask]” делают одно и тоже, поэтому тут оправданно применение простых квадратных скобок “[]”.

mask = [False, False, True, False, True, True]
s[mask]
c    2
b    4
f    5
dtype: int64

По аналогии это позволяет применять логические операции к столбцам pandas и сразу же элегантно фильтровать значений.

s[(s >= 5) | (s <= 2)]
a    42
e     1
c     2
f     5
dtype: int64

Изменяемость pandas.Series#

Аналогично с массивами NumPy, можно изменять содержимое ячейки/среза, но нельзя изменять размер (длину) столбца.

Например, изменить содержимое ячейки по метке "a" можно изменить следующим образом.

print(s)

print(horizontal_line)

s.loc['a'] = 42
print(s)
a    0
e    1
c    2
d    3
b    4
f    5
dtype: int64
________________________________________________________________________________
a    42
e     1
c     2
d     3
b     4
f     5
dtype: int64

С изменением размера следует быть аккуратным. Следующая операция как бы увеличит размер столбца s.

print(s)

print(horizontal_line)

s.loc['z'] = 42
print(s)
a     0
e     1
c     2
d     3
b     4
f     5
z    42
dtype: int64
________________________________________________________________________________
a     0
e     1
c     2
d     3
b     4
f     5
z    42
dtype: int64

Но pandas перевыделяет память, копирует данные старого столбца и дозаписывает новое значение при каждом добавлении нового значения в столбец или новой строки в таблицу. Т.е. использовать объекты pandas для накопления строк по одной крайне неэффективно. Если есть такая необходимость, то обычно данные накапливают в контейнерах python (список, словарь) и трансформируют в pandas объект в самом конце или по накоплении некого блока информации. Альтернативой служит создание большой таблицы/столбца и работа только с первыми n строками, увеличивая n при необходимости.

Операции над столбцами#

При выполнении операций над столбцами метки играют важную роль. Например, при сложении двух столбцов складываются значения с одинаковыми метками. Индекс результирующего столбца — объединение индексов столбцов слагаемых, а напротив тех меток, которые присутствуют в индексе только одного столбца записывается значение np.nan.

s1 = pd.Series([1, 2, 3], index=["a", "b", "c"], dtype="int64")
print(s1)
print(horizontal_line)

s2 = pd.Series([10, 20, 30], index=["c", "b", "d"], dtype="int64")
print(s2)
print(horizontal_line)

s = s1 + s2
print(s)
a    1
b    2
c    3
dtype: int64
________________________________________________________________________________
c    10
b    20
d    30
dtype: int64
________________________________________________________________________________
a     NaN
b    22.0
c    13.0
d     NaN
dtype: float64

Рисунок ниже иллюстрирует, что произошло.

../../_images/addition.png

Также можно заметить, что данные результирующего столбца имеют тип float64, несмотря на то, что исходные столбцы целочисленного типа. Это объясняется тем, что появившееся значения NaN не могут быть представлены стандартным числовым типом, что вынуждает приведение к float. Избежать такого поведения можно используя расширенные целочисленные типы, для использования которых указывается тип с заглавной буквы “I”.

s1 = pd.Series([1, 2, 3], index=["a", "b", "c"], dtype="Int64")
print(s1)
print(horizontal_line)

s2 = pd.Series([10, 20, 30], index=["c", "b", "d"], dtype="Int64")
print(s2)
print(horizontal_line)

print(s1 + s2)
a    1
b    2
c    3
dtype: Int64
________________________________________________________________________________
c    10
b    20
d    30
dtype: Int64
________________________________________________________________________________
a    <NA>
b      22
c      13
d    <NA>
dtype: Int64

Большинство методов массивов NumPy переопределены в pandas для pd.Series таким образом, чтобы обрабатывать пропущенные значения.

pandas_mean = s.mean()
numpy_mean = np.mean(s.values)

print(f"{pandas_mean=}, {numpy_mean=}")
pandas_mean=17.5, numpy_mean=nan