Категориальные данные#

Это введение в категориальный тип данных pandas, включая краткое сравнение с factor.

Categoricals являются типом данных pandas, соответствующим категориальным переменным в статистике. Категориальная переменная принимает ограниченное и обычно фиксированное количество возможных значений (categories; levels в R). Примеры: пол, социальный класс, группа крови, страна, время наблюдения или оценка по шкале Лайкерта.

В отличие от статистических категориальных переменных, категориальные данные могут иметь порядок (например, ‘strongly agree’ против ‘agree’ или ‘first observation’ против ‘second observation’), но числовые операции (сложение, деление, …) невозможны.

Все значения категориальных данных находятся либо в categories или np.nan. Порядок определяется порядком categories, не лексический порядок значений. Внутренне структура данных состоит из categories массив и целочисленный массив из codes которые указывают на реальное значение в categories массив.

Категориальный тип данных полезен в следующих случаях:

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

  • Лексический порядок переменной не совпадает с логическим порядком («один», «два», «три»). Преобразовав в категориальный и указав порядок категорий, сортировка и min/max будут использовать логический порядок вместо лексического, см. здесь.

  • Как сигнал другим библиотекам Python, что этот столбец следует рассматривать как категориальную переменную (например, для использования подходящих статистических методов или типов графиков).

См. также Документация API по категориальным данным.

Создание объектов#

Создание Series#

Категориальный Series или столбцы в DataFrame может быть создан несколькими способами:

Указав dtype="category" при создании Series:

In [1]: s = pd.Series(["a", "b", "c", "a"], dtype="category")

In [2]: s
Out[2]: 
0    a
1    b
2    c
3    a
dtype: category
Categories (3, object): ['a', 'b', 'c']

Series или столбец в category dtype:

In [3]: df = pd.DataFrame({"A": ["a", "b", "c", "a"]})

In [4]: df["B"] = df["A"].astype("category")

In [5]: df
Out[5]: 
   A  B
0  a  a
1  b  b
2  c  c
3  a  a

Используя специальные функции, такие как cut(), который группирует данные в дискретные интервалы. См. пример на тайлинге в документации.

In [6]: df = pd.DataFrame({"value": np.random.randint(0, 100, 20)})

In [7]: labels = ["{0} - {1}".format(i, i + 9) for i in range(0, 100, 10)]

In [8]: df["group"] = pd.cut(df.value, range(0, 105, 10), right=False, labels=labels)

In [9]: df.head(10)
Out[9]: 
   value    group
0     65  60 - 69
1     49  40 - 49
2     56  50 - 59
3     43  40 - 49
4     43  40 - 49
5     91  90 - 99
6     32  30 - 39
7     87  80 - 89
8     36  30 - 39
9      8    0 - 9

Передавая pandas.Categorical объект в Series или присвоение его DataFrame.

In [10]: raw_cat = pd.Categorical(
   ....:     ["a", "b", "c", "a"], categories=["b", "c", "d"], ordered=False
   ....: )
   ....: 

In [11]: s = pd.Series(raw_cat)

In [12]: s
Out[12]: 
0    NaN
1      b
2      c
3    NaN
dtype: category
Categories (3, object): ['b', 'c', 'd']

In [13]: df = pd.DataFrame({"A": ["a", "b", "c", "a"]})

In [14]: df["B"] = raw_cat

In [15]: df
Out[15]: 
   A    B
0  a  NaN
1  b    b
2  c    c
3  a  NaN

Категориальные данные имеют специфический category dtype:

In [16]: df.dtypes
Out[16]: 
A      object
B    category
dtype: object

Создание DataFrame#

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

Это можно сделать во время создания, указав dtype="category" в DataFrame конструктор:

In [17]: df = pd.DataFrame({"A": list("abca"), "B": list("bccd")}, dtype="category")

In [18]: df.dtypes
Out[18]: 
A    category
B    category
dtype: object

Обратите внимание, что категории в каждом столбце различаются; преобразование выполняется по столбцам, поэтому только метки, присутствующие в данном столбце, являются категориями:

In [19]: df["A"]
Out[19]: 
0    a
1    b
2    c
3    a
Name: A, dtype: category
Categories (3, object): ['a', 'b', 'c']

In [20]: df["B"]
Out[20]: 
0    b
1    c
2    c
3    d
Name: B, dtype: category
Categories (3, object): ['b', 'c', 'd']

Аналогично, все столбцы в существующем DataFrame может быть пакетно преобразован с помощью DataFrame.astype():

In [21]: df = pd.DataFrame({"A": list("abca"), "B": list("bccd")})

In [22]: df_cat = df.astype("category")

In [23]: df_cat.dtypes
Out[23]: 
A    category
B    category
dtype: object

Это преобразование также выполняется столбец за столбцом:

In [24]: df_cat["A"]
Out[24]: 
0    a
1    b
2    c
3    a
Name: A, dtype: category
Categories (3, object): ['a', 'b', 'c']

In [25]: df_cat["B"]
Out[25]: 
0    b
1    c
2    c
3    d
Name: B, dtype: category
Categories (3, object): ['b', 'c', 'd']

Управление поведением#

В приведенных выше примерах, где мы передавали dtype='category', мы использовали поведение по умолчанию:

  1. Категории выводятся из данных.

  2. Категории не упорядочены.

Чтобы управлять этим поведением, вместо передачи 'category', используйте экземпляр из CategoricalDtype.

In [26]: from pandas.api.types import CategoricalDtype

In [27]: s = pd.Series(["a", "b", "c", "a"])

In [28]: cat_type = CategoricalDtype(categories=["b", "c", "d"], ordered=True)

In [29]: s_cat = s.astype(cat_type)

In [30]: s_cat
Out[30]: 
0    NaN
1      b
2      c
3    NaN
dtype: category
Categories (3, object): ['b' < 'c' < 'd']

Аналогично, CategoricalDtype может использоваться с DataFrame чтобы гарантировать, что категории согласованы между всеми столбцами.

In [31]: from pandas.api.types import CategoricalDtype

In [32]: df = pd.DataFrame({"A": list("abca"), "B": list("bccd")})

In [33]: cat_type = CategoricalDtype(categories=list("abcd"), ordered=True)

In [34]: df_cat = df.astype(cat_type)

In [35]: df_cat["A"]
Out[35]: 
0    a
1    b
2    c
3    a
Name: A, dtype: category
Categories (4, object): ['a' < 'b' < 'c' < 'd']

In [36]: df_cat["B"]
Out[36]: 
0    b
1    c
2    c
3    d
Name: B, dtype: category
Categories (4, object): ['a' < 'b' < 'c' < 'd']

Примечание

Для выполнения преобразования на уровне всей таблицы, где все метки во всем DataFrame используются как категории для каждого столбца, categories параметр может быть определен программно с помощью categories = pd.unique(df.to_numpy().ravel()).

Если у вас уже есть codes и categories, вы можете использовать from_codes() конструктор для сохранения шага факторизации во время обычного режима конструктора:

In [37]: splitter = np.random.choice([0, 1], 5, p=[0.5, 0.5])

In [38]: s = pd.Series(pd.Categorical.from_codes(splitter, categories=["train", "test"]))

Восстановление исходных данных#

Чтобы вернуться к исходному Series или массива NumPy, используйте Series.astype(original_dtype) или np.asarray(categorical):

In [39]: s = pd.Series(["a", "b", "c", "a"])

In [40]: s
Out[40]: 
0    a
1    b
2    c
3    a
dtype: object

In [41]: s2 = s.astype("category")

In [42]: s2
Out[42]: 
0    a
1    b
2    c
3    a
dtype: category
Categories (3, object): ['a', 'b', 'c']

In [43]: s2.astype(str)
Out[43]: 
0    a
1    b
2    c
3    a
dtype: object

In [44]: np.asarray(s2)
Out[44]: array(['a', 'b', 'c', 'a'], dtype=object)

Примечание

В отличие от R, factor функция, категориальные данные не преобразуют входные значения в строки; категории будут иметь тот же тип данных, что и исходные значения.

Примечание

В отличие от R, factor функция, в настоящее время нет возможности назначать/изменять метки во время создания. Используйте categories чтобы изменить категории после создания.

CategoricalDtype#

Тип категориального полностью описывается

  1. categories: последовательность уникальных значений без пропущенных значений

  2. ordered: логическое значение

Эта информация может храниться в CategoricalDtype. categories аргумент является необязательным, что подразумевает, что фактические категории должны быть выведены из того, что присутствует в данных, когда pandas.Categorical создан. Категории по умолчанию считаются неупорядоченными.

In [45]: from pandas.api.types import CategoricalDtype

In [46]: CategoricalDtype(["a", "b", "c"])
Out[46]: CategoricalDtype(categories=['a', 'b', 'c'], ordered=False, categories_dtype=object)

In [47]: CategoricalDtype(["a", "b", "c"], ordered=True)
Out[47]: CategoricalDtype(categories=['a', 'b', 'c'], ordered=True, categories_dtype=object)

In [48]: CategoricalDtype()
Out[48]: CategoricalDtype(categories=None, ordered=False, categories_dtype=None)

A CategoricalDtype может использоваться в любом месте, где pandas ожидает dtype. Например pandas.read_csv(), pandas.DataFrame.astype(), или в Series конструктор.

Примечание

Для удобства вы можете использовать строку 'category' вместо CategoricalDtype когда требуется поведение по умолчанию категорий как неупорядоченных и равных набору значений, присутствующих в массиве. Другими словами, dtype='category' эквивалентно dtype=CategoricalDtype().

Семантика равенства#

Два экземпляра CategoricalDtype считаются равными, если они имеют одинаковые категории и порядок. При сравнении двух неупорядоченных категориальных данных порядок categories не учитывается.

In [49]: c1 = CategoricalDtype(["a", "b", "c"], ordered=False)

# Equal, since order is not considered when ordered=False
In [50]: c1 == CategoricalDtype(["b", "c", "a"], ordered=False)
Out[50]: True

# Unequal, since the second CategoricalDtype is ordered
In [51]: c1 == CategoricalDtype(["a", "b", "c"], ordered=True)
Out[51]: False

Все экземпляры CategoricalDtype сравниваются равными строке 'category'.

In [52]: c1 == "category"
Out[52]: True

Описание#

Используя describe() на категориальных данных даст аналогичный результат, как Series или DataFrame типа string.

In [53]: cat = pd.Categorical(["a", "c", "c", np.nan], categories=["b", "a", "c"])

In [54]: df = pd.DataFrame({"cat": cat, "s": ["a", "c", "c", np.nan]})

In [55]: df.describe()
Out[55]: 
       cat  s
count    3  3
unique   2  2
top      c  c
freq     2  2

In [56]: df["cat"].describe()
Out[56]: 
count     3
unique    2
top       c
freq      2
Name: cat, dtype: object

Работа с категориями#

Категориальные данные имеют categories и ordered свойство, которое перечисляет их возможные значения и имеет ли значение порядок или нет. Эти свойства представлены как s.cat.categories и s.cat.ordered. Если вы не укажете категории и порядок вручную, они выводятся из переданных аргументов.

In [57]: s = pd.Series(["a", "b", "c", "a"], dtype="category")

In [58]: s.cat.categories
Out[58]: Index(['a', 'b', 'c'], dtype='object')

In [59]: s.cat.ordered
Out[59]: False

Также можно передать категории в определенном порядке:

In [60]: s = pd.Series(pd.Categorical(["a", "b", "c", "a"], categories=["c", "b", "a"]))

In [61]: s.cat.categories
Out[61]: Index(['c', 'b', 'a'], dtype='object')

In [62]: s.cat.ordered
Out[62]: False

Примечание

Новые категориальные данные не автоматически упорядочены. Необходимо явно передать ordered=True для указания упорядоченного Categorical.

Примечание

Результат unique() не всегда совпадает с Series.cat.categories, потому что Series.unique() имеет несколько гарантий, а именно: возвращает категории в порядке появления и включает только значения, которые фактически присутствуют.

In [63]: s = pd.Series(list("babc")).astype(CategoricalDtype(list("abcd")))

In [64]: s
Out[64]: 
0    b
1    a
2    b
3    c
dtype: category
Categories (4, object): ['a', 'b', 'c', 'd']

# categories
In [65]: s.cat.categories
Out[65]: Index(['a', 'b', 'c', 'd'], dtype='object')

# uniques
In [66]: s.unique()
Out[66]: 
['b', 'a', 'c']
Categories (4, object): ['a', 'b', 'c', 'd']

Переименование категорий#

Переименование категорий выполняется с использованием rename_categories() method:

In [67]: s = pd.Series(["a", "b", "c", "a"], dtype="category")

In [68]: s
Out[68]: 
0    a
1    b
2    c
3    a
dtype: category
Categories (3, object): ['a', 'b', 'c']

In [69]: new_categories = ["Group %s" % g for g in s.cat.categories]

In [70]: s = s.cat.rename_categories(new_categories)

In [71]: s
Out[71]: 
0    Group a
1    Group b
2    Group c
3    Group a
dtype: category
Categories (3, object): ['Group a', 'Group b', 'Group c']

# You can also pass a dict-like object to map the renaming
In [72]: s = s.cat.rename_categories({1: "x", 2: "y", 3: "z"})

In [73]: s
Out[73]: 
0    Group a
1    Group b
2    Group c
3    Group a
dtype: category
Categories (3, object): ['Group a', 'Group b', 'Group c']

Примечание

В отличие от R, factor, категориальные данные могут иметь категории других типов, кроме строк.

Категории должны быть уникальными или ValueError вызывается исключение:

In [74]: try:
   ....:     s = s.cat.rename_categories([1, 1, 1])
   ....: except ValueError as e:
   ....:     print("ValueError:", str(e))
   ....: 
ValueError: Categorical categories must be unique

Категории также не должны быть NaN или ValueError вызывается исключение:

In [75]: try:
   ....:     s = s.cat.rename_categories([1, 2, np.nan])
   ....: except ValueError as e:
   ....:     print("ValueError:", str(e))
   ....: 
ValueError: Categorical categories cannot be null

Добавление новых категорий#

Добавление категорий можно выполнить с помощью add_categories() method:

In [76]: s = s.cat.add_categories([4])

In [77]: s.cat.categories
Out[77]: Index(['Group a', 'Group b', 'Group c', 4], dtype='object')

In [78]: s
Out[78]: 
0    Group a
1    Group b
2    Group c
3    Group a
dtype: category
Categories (4, object): ['Group a', 'Group b', 'Group c', 4]

Удаление категорий#

Удаление категорий можно выполнить с помощью remove_categories() метод. Удаляемые значения заменяются на np.nan.:

In [79]: s = s.cat.remove_categories([4])

In [80]: s
Out[80]: 
0    Group a
1    Group b
2    Group c
3    Group a
dtype: category
Categories (3, object): ['Group a', 'Group b', 'Group c']

Удаление неиспользуемых категорий#

Удаление неиспользуемых категорий также можно выполнить:

In [81]: s = pd.Series(pd.Categorical(["a", "b", "a"], categories=["a", "b", "c", "d"]))

In [82]: s
Out[82]: 
0    a
1    b
2    a
dtype: category
Categories (4, object): ['a', 'b', 'c', 'd']

In [83]: s.cat.remove_unused_categories()
Out[83]: 
0    a
1    b
2    a
dtype: category
Categories (2, object): ['a', 'b']

Установка категорий#

Если вы хотите удалить и добавить новые категории за один шаг (что дает некоторое преимущество в скорости) или просто установить категории в предопределенный масштаб, используйте set_categories().

In [84]: s = pd.Series(["one", "two", "four", "-"], dtype="category")

In [85]: s
Out[85]: 
0     one
1     two
2    four
3       -
dtype: category
Categories (4, object): ['-', 'four', 'one', 'two']

In [86]: s = s.cat.set_categories(["one", "two", "three", "four"])

In [87]: s
Out[87]: 
0     one
1     two
2    four
3     NaN
dtype: category
Categories (4, object): ['one', 'two', 'three', 'four']

Примечание

Имейте в виду, что Categorical.set_categories() нельзя определить, пропущена ли категория намеренно или из-за опечатки или (в Python3) из-за разницы типов (например, тип NumPy S1 и строки Python). Это может привести к неожиданному поведению!

Сортировка и порядок#

Если категориальные данные упорядочены (s.cat.ordered == True), тогда порядок категорий имеет значение, и возможны определенные операции. Если категориальный неупорядочен, .min()/.max() вызовет TypeError.

In [88]: s = pd.Series(pd.Categorical(["a", "b", "c", "a"], ordered=False))

In [89]: s = s.sort_values()

In [90]: s = pd.Series(["a", "b", "c", "a"]).astype(CategoricalDtype(ordered=True))

In [91]: s = s.sort_values()

In [92]: s
Out[92]: 
0    a
3    a
1    b
2    c
dtype: category
Categories (3, object): ['a' < 'b' < 'c']

In [93]: s.min(), s.max()
Out[93]: ('a', 'c')

Вы можете установить категориальные данные как упорядоченные, используя as_ordered() или неупорядоченный с использованием as_unordered(). По умолчанию они будут возвращать новый объект.

In [94]: s.cat.as_ordered()
Out[94]: 
0    a
3    a
1    b
2    c
dtype: category
Categories (3, object): ['a' < 'b' < 'c']

In [95]: s.cat.as_unordered()
Out[95]: 
0    a
3    a
1    b
2    c
dtype: category
Categories (3, object): ['a', 'b', 'c']

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

In [96]: s = pd.Series([1, 2, 3, 1], dtype="category")

In [97]: s = s.cat.set_categories([2, 3, 1], ordered=True)

In [98]: s
Out[98]: 
0    1
1    2
2    3
3    1
dtype: category
Categories (3, int64): [2 < 3 < 1]

In [99]: s = s.sort_values()

In [100]: s
Out[100]: 
1    2
2    3
0    1
3    1
dtype: category
Categories (3, int64): [2 < 3 < 1]

In [101]: s.min(), s.max()
Out[101]: (2, 1)

Переупорядочивание#

Переупорядочивание категорий возможно через Categorical.reorder_categories() и Categorical.set_categories() методы. Для Categorical.reorder_categories(), все старые категории должны быть включены в новые категории, и новые категории не допускаются. Это обязательно сделает порядок сортировки таким же, как порядок категорий.

In [102]: s = pd.Series([1, 2, 3, 1], dtype="category")

In [103]: s = s.cat.reorder_categories([2, 3, 1], ordered=True)

In [104]: s
Out[104]: 
0    1
1    2
2    3
3    1
dtype: category
Categories (3, int64): [2 < 3 < 1]

In [105]: s = s.sort_values()

In [106]: s
Out[106]: 
1    2
2    3
0    1
3    1
dtype: category
Categories (3, int64): [2 < 3 < 1]

In [107]: s.min(), s.max()
Out[107]: (2, 1)

Примечание

Обратите внимание на разницу между назначением новых категорий и изменением порядка категорий: первое переименовывает категории и, следовательно, отдельные значения в Series, но если первая позиция была отсортирована последней, переименованное значение всё равно будет отсортировано последним. Переупорядочивание означает, что способ сортировки значений отличается после этого, но не то, что отдельные значения в Series изменены.

Примечание

Если Categorical не упорядочен, Series.min() и Series.max() вызовет исключение TypeError. Числовые операции, такие как +, -, *, / и операции на их основе (например, Series.median(), который должен вычислять среднее между двумя значениями, если длина массива четная) не работают и вызывают TypeError.

Сортировка по нескольким столбцам#

Столбец с категориальным типом данных будет участвовать в сортировке по нескольким столбцам аналогично другим столбцам. Порядок категориального столбца определяется categories этого столбца.

In [108]: dfs = pd.DataFrame(
   .....:     {
   .....:         "A": pd.Categorical(
   .....:             list("bbeebbaa"),
   .....:             categories=["e", "a", "b"],
   .....:             ordered=True,
   .....:         ),
   .....:         "B": [1, 2, 1, 2, 2, 1, 2, 1],
   .....:     }
   .....: )
   .....: 

In [109]: dfs.sort_values(by=["A", "B"])
Out[109]: 
   A  B
2  e  1
3  e  2
7  a  1
6  a  2
0  b  1
5  b  1
1  b  2
4  b  2

Переупорядочивание categories изменяет будущую сортировку.

In [110]: dfs["A"] = dfs["A"].cat.reorder_categories(["a", "b", "e"])

In [111]: dfs.sort_values(by=["A", "B"])
Out[111]: 
   A  B
7  a  1
6  a  2
0  b  1
5  b  1
1  b  2
4  b  2
2  e  1
3  e  2

Сравнения#

Сравнение категориальных данных с другими объектами возможно в трёх случаях:

  • Сравнение на равенство (== и !=) в объект, подобный списку (список, Series, массив, …) той же длины, что и категориальные данные.

  • Все сравнения (==, !=, >, >=, <, и <=) категориальных данных в другую категориальную Series, когда ordered==True и categories одинаковы.

  • Все сравнения категориальных данных со скаляром.

Все другие сравнения, особенно сравнения «неравенства» двух категориальных объектов с разными категориями или категориального объекта с любым спискообразным объектом, вызовут TypeError.

Примечание

Любые сравнения «неравенства» категориальных данных с Series, np.array, list или категориальные данные с разными категориями или порядком вызовут TypeError поскольку пользовательское упорядочивание категорий можно интерпретировать двумя способами: один с учётом упорядочивания, а другой без.

In [112]: cat = pd.Series([1, 2, 3]).astype(CategoricalDtype([3, 2, 1], ordered=True))

In [113]: cat_base = pd.Series([2, 2, 2]).astype(CategoricalDtype([3, 2, 1], ordered=True))

In [114]: cat_base2 = pd.Series([2, 2, 2]).astype(CategoricalDtype(ordered=True))

In [115]: cat
Out[115]: 
0    1
1    2
2    3
dtype: category
Categories (3, int64): [3 < 2 < 1]

In [116]: cat_base
Out[116]: 
0    2
1    2
2    2
dtype: category
Categories (3, int64): [3 < 2 < 1]

In [117]: cat_base2
Out[117]: 
0    2
1    2
2    2
dtype: category
Categories (1, int64): [2]

Сравнение с категориальным с теми же категориями и порядком или со скаляром работает:

In [118]: cat > cat_base
Out[118]: 
0     True
1    False
2    False
dtype: bool

In [119]: cat > 2
Out[119]: 
0     True
1    False
2    False
dtype: bool

Сравнения на равенство работают с любыми спискообразными объектами той же длины и скалярами:

In [120]: cat == cat_base
Out[120]: 
0    False
1     True
2    False
dtype: bool

In [121]: cat == np.array([1, 2, 3])
Out[121]: 
0    True
1    True
2    True
dtype: bool

In [122]: cat == 2
Out[122]: 
0    False
1     True
2    False
dtype: bool

Это не работает, потому что категории не совпадают:

In [123]: try:
   .....:     cat > cat_base2
   .....: except TypeError as e:
   .....:     print("TypeError:", str(e))
   .....: 
TypeError: Categoricals can only be compared if 'categories' are the same.

Если вы хотите выполнить сравнение «неравенства» категориального ряда с объектом, похожим на список, который не является категориальными данными, вам нужно быть явным и преобразовать категориальные данные обратно в исходные значения:

In [124]: base = np.array([1, 2, 3])

In [125]: try:
   .....:     cat > base
   .....: except TypeError as e:
   .....:     print("TypeError:", str(e))
   .....: 
TypeError: Cannot compare a Categorical for op __gt__ with type .
If you want to compare values, use 'np.asarray(cat)  other'.

In [126]: np.asarray(cat) > base
Out[126]: array([False, False, False])

При сравнении двух неупорядоченных категориальных переменных с одинаковыми категориями порядок не учитывается:

In [127]: c1 = pd.Categorical(["a", "b"], categories=["a", "b"], ordered=False)

In [128]: c2 = pd.Categorical(["a", "b"], categories=["b", "a"], ordered=False)

In [129]: c1 == c2
Out[129]: array([ True,  True])

Операции#

Помимо Series.min(), Series.max() и Series.mode(), следующие операции возможны с категориальными данными:

Series методы, такие как Series.value_counts() будет использовать все категории, даже если некоторые категории отсутствуют в данных:

In [130]: s = pd.Series(pd.Categorical(["a", "b", "c", "c"], categories=["c", "a", "b", "d"]))

In [131]: s.value_counts()
Out[131]: 
c    2
a    1
b    1
d    0
Name: count, dtype: int64

DataFrame методы, такие как DataFrame.sum() также показывать "неиспользуемые" категории, когда observed=False.

In [132]: columns = pd.Categorical(
   .....:     ["One", "One", "Two"], categories=["One", "Two", "Three"], ordered=True
   .....: )
   .....: 

In [133]: df = pd.DataFrame(
   .....:     data=[[1, 2, 3], [4, 5, 6]],
   .....:     columns=pd.MultiIndex.from_arrays([["A", "B", "B"], columns]),
   .....: ).T
   .....: 

In [134]: df.groupby(level=1, observed=False).sum()
Out[134]: 
       0  1
One    3  9
Two    3  6
Three  0  0

Groupby также будет показывать "неиспользуемые" категории, когда observed=False:

In [135]: cats = pd.Categorical(
   .....:     ["a", "b", "b", "b", "c", "c", "c"], categories=["a", "b", "c", "d"]
   .....: )
   .....: 

In [136]: df = pd.DataFrame({"cats": cats, "values": [1, 2, 2, 2, 3, 4, 5]})

In [137]: df.groupby("cats", observed=False).mean()
Out[137]: 
      values
cats        
a        1.0
b        2.0
c        4.0
d        NaN

In [138]: cats2 = pd.Categorical(["a", "a", "b", "b"], categories=["a", "b", "c"])

In [139]: df2 = pd.DataFrame(
   .....:     {
   .....:         "cats": cats2,
   .....:         "B": ["c", "d", "c", "d"],
   .....:         "values": [1, 2, 3, 4],
   .....:     }
   .....: )
   .....: 

In [140]: df2.groupby(["cats", "B"], observed=False).mean()
Out[140]: 
        values
cats B        
a    c     1.0
     d     2.0
b    c     3.0
     d     4.0
c    c     NaN
     d     NaN

Сводные таблицы:

In [141]: raw_cat = pd.Categorical(["a", "a", "b", "b"], categories=["a", "b", "c"])

In [142]: df = pd.DataFrame({"A": raw_cat, "B": ["c", "d", "c", "d"], "values": [1, 2, 3, 4]})

In [143]: pd.pivot_table(df, values="values", index=["A", "B"], observed=False)
Out[143]: 
     values
A B        
a c     1.0
  d     2.0
b c     3.0
  d     4.0

Обработка данных#

Оптимизированные методы доступа к данным pandas .loc, .iloc, .at, и .iat, работают как обычно. Единственное отличие — тип возвращаемого значения (для получения) и то, что только значения, уже находящиеся в categories может быть присвоено.

Получение#

Если операция среза возвращает либо DataFrame или столбец типа Series, category dtype сохраняется.

In [144]: idx = pd.Index(["h", "i", "j", "k", "l", "m", "n"])

In [145]: cats = pd.Series(["a", "b", "b", "b", "c", "c", "c"], dtype="category", index=idx)

In [146]: values = [1, 2, 2, 2, 3, 4, 5]

In [147]: df = pd.DataFrame({"cats": cats, "values": values}, index=idx)

In [148]: df.iloc[2:4, :]
Out[148]: 
  cats  values
j    b       2
k    b       2

In [149]: df.iloc[2:4, :].dtypes
Out[149]: 
cats      category
values       int64
dtype: object

In [150]: df.loc["h":"j", "cats"]
Out[150]: 
h    a
i    b
j    b
Name: cats, dtype: category
Categories (3, object): ['a', 'b', 'c']

In [151]: df[df["cats"] == "b"]
Out[151]: 
  cats  values
i    b       2
j    b       2
k    b       2

Пример, где тип категории не сохраняется, если взять одну строку: результирующий Series имеет тип данных object:

# get the complete "h" row as a Series
In [152]: df.loc["h", :]
Out[152]: 
cats      a
values    1
Name: h, dtype: object

Возврат одного элемента из категориальных данных также вернёт значение, а не категориальный объект длины «1».

In [153]: df.iat[0, 0]
Out[153]: 'a'

In [154]: df["cats"] = df["cats"].cat.rename_categories(["x", "y", "z"])

In [155]: df.at["h", "cats"]  # returns a string
Out[155]: 'x'

Примечание

Это контрастирует с R’s factor функция, где factor(c(1,2,3))[1] возвращает одно значение factor.

Чтобы получить одно значение Series типа category, вы передаете список с одним значением:

In [156]: df.loc[["h"], "cats"]
Out[156]: 
h    x
Name: cats, dtype: category
Categories (3, object): ['x', 'y', 'z']

Аксессоры для строк и дат/времени#

Аксессоры .dt и .str будет работать, если s.cat.categories имеют подходящий тип:

In [157]: str_s = pd.Series(list("aabb"))

In [158]: str_cat = str_s.astype("category")

In [159]: str_cat
Out[159]: 
0    a
1    a
2    b
3    b
dtype: category
Categories (2, object): ['a', 'b']

In [160]: str_cat.str.contains("a")
Out[160]: 
0     True
1     True
2    False
3    False
dtype: bool

In [161]: date_s = pd.Series(pd.date_range("1/1/2015", periods=5))

In [162]: date_cat = date_s.astype("category")

In [163]: date_cat
Out[163]: 
0   2015-01-01
1   2015-01-02
2   2015-01-03
3   2015-01-04
4   2015-01-05
dtype: category
Categories (5, datetime64[ns]): [2015-01-01, 2015-01-02, 2015-01-03, 2015-01-04, 2015-01-05]

In [164]: date_cat.dt.day
Out[164]: 
0    1
1    2
2    3
3    4
4    5
dtype: int32

Примечание

Возвращаемый Series (или DataFrame) имеет тот же тип, как если бы вы использовали .str. / .dt. на Series этого типа (а не типа category!).

Это означает, что возвращаемые значения методов и свойств аксессоров Series и возвращаемые значения методов и свойств на аксессорах этого Series преобразован в один из типов category будет равно:

In [165]: ret_s = str_s.str.contains("a")

In [166]: ret_cat = str_cat.str.contains("a")

In [167]: ret_s.dtype == ret_cat.dtype
Out[167]: True

In [168]: ret_s == ret_cat
Out[168]: 
0    True
1    True
2    True
3    True
dtype: bool

Примечание

Работа выполняется на categories и затем новый Series создаётся. Это имеет некоторые последствия для производительности, если у вас есть Series типа string, где много элементов повторяются (т.е. количество уникальных элементов в Series значительно меньше длины Series). В этом случае может быть быстрее преобразовать исходный Series к одному из типов category и использовать .str. или .dt. на этом.

Установка#

Установка значений в категориальном столбце (или Series) работает, пока значение включено в categories:

In [169]: idx = pd.Index(["h", "i", "j", "k", "l", "m", "n"])

In [170]: cats = pd.Categorical(["a", "a", "a", "a", "a", "a", "a"], categories=["a", "b"])

In [171]: values = [1, 1, 1, 1, 1, 1, 1]

In [172]: df = pd.DataFrame({"cats": cats, "values": values}, index=idx)

In [173]: df.iloc[2:4, :] = [["b", 2], ["b", 2]]

In [174]: df
Out[174]: 
  cats  values
h    a       1
i    a       1
j    b       2
k    b       2
l    a       1
m    a       1
n    a       1

In [175]: try:
   .....:     df.iloc[2:4, :] = [["c", 3], ["c", 3]]
   .....: except TypeError as e:
   .....:     print("TypeError:", str(e))
   .....: 
TypeError: Cannot setitem on a Categorical with a new category, set the categories first

Установка значений путем присваивания категориальных данных также проверит, что categories match:

In [176]: df.loc["j":"k", "cats"] = pd.Categorical(["a", "a"], categories=["a", "b"])

In [177]: df
Out[177]: 
  cats  values
h    a       1
i    a       1
j    a       2
k    a       2
l    a       1
m    a       1
n    a       1

In [178]: try:
   .....:     df.loc["j":"k", "cats"] = pd.Categorical(["b", "b"], categories=["a", "b", "c"])
   .....: except TypeError as e:
   .....:     print("TypeError:", str(e))
   .....: 
TypeError: Cannot set a Categorical with another, without identical categories

Присвоение Categorical к частям столбца других типов будет использовать значения:

In [179]: df = pd.DataFrame({"a": [1, 1, 1, 1, 1], "b": ["a", "a", "a", "a", "a"]})

In [180]: df.loc[1:2, "a"] = pd.Categorical(["b", "b"], categories=["a", "b"])

In [181]: df.loc[2:3, "b"] = pd.Categorical(["b", "b"], categories=["a", "b"])

In [182]: df
Out[182]: 
   a  b
0  1  a
1  b  a
2  b  b
3  1  b
4  1  a

In [183]: df.dtypes
Out[183]: 
a    object
b    object
dtype: object

Слияние / конкатенация#

По умолчанию, объединение Series или DataFrames которые содержат одинаковые категории, приводит к category dtype, иначе результаты будут зависеть от dtype базовых категорий. Слияния, приводящие к некатегориальным dtypes, вероятно, будут иметь большее использование памяти. Используйте .astype или union_categoricals чтобы гарантировать category результаты.

In [184]: from pandas.api.types import union_categoricals

# same categories
In [185]: s1 = pd.Series(["a", "b"], dtype="category")

In [186]: s2 = pd.Series(["a", "b", "a"], dtype="category")

In [187]: pd.concat([s1, s2])
Out[187]: 
0    a
1    b
0    a
1    b
2    a
dtype: category
Categories (2, object): ['a', 'b']

# different categories
In [188]: s3 = pd.Series(["b", "c"], dtype="category")

In [189]: pd.concat([s1, s3])
Out[189]: 
0    a
1    b
0    b
1    c
dtype: object

# Output dtype is inferred based on categories values
In [190]: int_cats = pd.Series([1, 2], dtype="category")

In [191]: float_cats = pd.Series([3.0, 4.0], dtype="category")

In [192]: pd.concat([int_cats, float_cats])
Out[192]: 
0    1.0
1    2.0
0    3.0
1    4.0
dtype: float64

In [193]: pd.concat([s1, s3]).astype("category")
Out[193]: 
0    a
1    b
0    b
1    c
dtype: category
Categories (3, object): ['a', 'b', 'c']

In [194]: union_categoricals([s1.array, s3.array])
Out[194]: 
['a', 'b', 'b', 'c']
Categories (3, object): ['a', 'b', 'c']

Следующая таблица суммирует результаты слияния Categoricals:

arg1

arg2

идентичный

результат

категория

категория

True

категория

категория (object)

категория (object)

False

объект (тип данных выводится)

категория (целое число)

категория (float)

False

float (тип данных выводится)

Объединение#

Если вы хотите объединить категориальные данные, которые не обязательно имеют одинаковые категории, то union_categoricals() функция объединит список категориальных данных. Новые категории будут объединением категорий, которые комбинируются.

In [195]: from pandas.api.types import union_categoricals

In [196]: a = pd.Categorical(["b", "c"])

In [197]: b = pd.Categorical(["a", "b"])

In [198]: union_categoricals([a, b])
Out[198]: 
['b', 'c', 'a', 'b']
Categories (3, object): ['b', 'c', 'a']

По умолчанию результирующие категории будут упорядочены так, как они появляются в данных. Если вы хотите, чтобы категории были лексикографически отсортированы, используйте sort_categories=True аргумент.

In [199]: union_categoricals([a, b], sort_categories=True)
Out[199]: 
['b', 'c', 'a', 'b']
Categories (3, object): ['a', 'b', 'c']

union_categoricals также работает с "простыми" случаями объединения двух категориальных переменных с одинаковыми категориями и информацией о порядке (например, что вы также могли бы append для).

In [200]: a = pd.Categorical(["a", "b"], ordered=True)

In [201]: b = pd.Categorical(["a", "b", "a"], ordered=True)

In [202]: union_categoricals([a, b])
Out[202]: 
['a', 'b', 'a', 'b', 'a']
Categories (2, object): ['a' < 'b']

Приведенный ниже код вызывает TypeError поскольку категории упорядочены и не идентичны.

In [203]: a = pd.Categorical(["a", "b"], ordered=True)

In [204]: b = pd.Categorical(["a", "b", "c"], ordered=True)

In [205]: union_categoricals([a, b])
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[205], line 1
----> 1 union_categoricals([a, b])

File ~/work/pandas/pandas/pandas/core/dtypes/concat.py:341, in union_categoricals(to_union, sort_categories, ignore_order)
    339     if all(c.ordered for c in to_union):
    340         msg = "to union ordered Categoricals, all categories must be the same"
--> 341         raise TypeError(msg)
    342     raise TypeError("Categorical.ordered must be the same")
    344 if ignore_order:

TypeError: to union ordered Categoricals, all categories must be the same

Упорядоченные категориальные данные с разными категориями или порядком можно объединить, используя ignore_ordered=True аргумент.

In [206]: a = pd.Categorical(["a", "b", "c"], ordered=True)

In [207]: b = pd.Categorical(["c", "b", "a"], ordered=True)

In [208]: union_categoricals([a, b], ignore_order=True)
Out[208]: 
['a', 'b', 'c', 'c', 'b', 'a']
Categories (3, object): ['a', 'b', 'c']

union_categoricals() также работает с CategoricalIndex, или Series содержащие категориальные данные, но обратите внимание, что результирующий массив всегда будет простым Categorical:

In [209]: a = pd.Series(["b", "c"], dtype="category")

In [210]: b = pd.Series(["a", "b"], dtype="category")

In [211]: union_categoricals([a, b])
Out[211]: 
['b', 'c', 'a', 'b']
Categories (3, object): ['b', 'c', 'a']

Примечание

union_categoricals может перекодировать целочисленные коды для категорий при объединении категориальных данных. Это, скорее всего, то, что вам нужно, но если вы полагаетесь на точную нумерацию категорий, будьте внимательны.

In [212]: c1 = pd.Categorical(["b", "c"])

In [213]: c2 = pd.Categorical(["a", "b"])

In [214]: c1
Out[214]: 
['b', 'c']
Categories (2, object): ['b', 'c']

# "b" is coded to 0
In [215]: c1.codes
Out[215]: array([0, 1], dtype=int8)

In [216]: c2
Out[216]: 
['a', 'b']
Categories (2, object): ['a', 'b']

# "b" is coded to 1
In [217]: c2.codes
Out[217]: array([0, 1], dtype=int8)

In [218]: c = union_categoricals([c1, c2])

In [219]: c
Out[219]: 
['b', 'c', 'a', 'b']
Categories (3, object): ['b', 'c', 'a']

# "b" is coded to 0 throughout, same as c1, different from c2
In [220]: c.codes
Out[220]: array([0, 1, 2, 0], dtype=int8)

Получение данных в/из#

Вы можете записывать данные, содержащие category типы данных в HDFStore. См. здесь для примера и предостережений.

Также возможно записывать данные в и читать данные из Stata форматные файлы. См. здесь для примера и предостережений.

Запись в CSV-файл преобразует данные, фактически удаляя любую информацию о категориальных данных (категории и порядок). Поэтому если вы прочитаете CSV-файл обратно, вам придется преобразовать соответствующие столбцы обратно в category и назначить правильные категории и порядок категорий.

In [221]: import io

In [222]: s = pd.Series(pd.Categorical(["a", "b", "b", "a", "a", "d"]))

# rename the categories
In [223]: s = s.cat.rename_categories(["very good", "good", "bad"])

# reorder the categories and add missing categories
In [224]: s = s.cat.set_categories(["very bad", "bad", "medium", "good", "very good"])

In [225]: df = pd.DataFrame({"cats": s, "vals": [1, 2, 3, 4, 5, 6]})

In [226]: csv = io.StringIO()

In [227]: df.to_csv(csv)

In [228]: df2 = pd.read_csv(io.StringIO(csv.getvalue()))

In [229]: df2.dtypes
Out[229]: 
Unnamed: 0     int64
cats          object
vals           int64
dtype: object

In [230]: df2["cats"]
Out[230]: 
0    very good
1         good
2         good
3    very good
4    very good
5          bad
Name: cats, dtype: object

# Redo the category
In [231]: df2["cats"] = df2["cats"].astype("category")

In [232]: df2["cats"] = df2["cats"].cat.set_categories(
   .....:     ["very bad", "bad", "medium", "good", "very good"]
   .....: )
   .....: 

In [233]: df2.dtypes
Out[233]: 
Unnamed: 0       int64
cats          category
vals             int64
dtype: object

In [234]: df2["cats"]
Out[234]: 
0    very good
1         good
2         good
3    very good
4    very good
5          bad
Name: cats, dtype: category
Categories (5, object): ['very bad', 'bad', 'medium', 'good', 'very good']

То же самое относится к записи в базу данных SQL с to_sql.

Отсутствующие данные#

pandas в основном использует значение np.nan для представления отсутствующих данных. По умолчанию он не включается в вычисления. См. Раздел Пропущенные данные.

Пропущенные значения должны не будет включён в Categorical's categories, только в values. Вместо этого считается, что NaN отличается и всегда возможен. При работе с Categorical codes, пропущенные значения всегда будут иметь код -1.

In [235]: s = pd.Series(["a", "b", np.nan, "a"], dtype="category")

# only two categories
In [236]: s
Out[236]: 
0      a
1      b
2    NaN
3      a
dtype: category
Categories (2, object): ['a', 'b']

In [237]: s.cat.codes
Out[237]: 
0    0
1    1
2   -1
3    0
dtype: int8

Методы для работы с пропущенными данными, например, isna(), fillna(), dropna(), все работают нормально:

In [238]: s = pd.Series(["a", "b", np.nan], dtype="category")

In [239]: s
Out[239]: 
0      a
1      b
2    NaN
dtype: category
Categories (2, object): ['a', 'b']

In [240]: pd.isna(s)
Out[240]: 
0    False
1    False
2     True
dtype: bool

In [241]: s.fillna("a")
Out[241]: 
0    a
1    b
2    a
dtype: category
Categories (2, object): ['a', 'b']

Отличия от R factor#

Можно отметить следующие отличия от функций factor в R:

  • R levels именуются categories.

  • R levels всегда имеют тип string, в то время как categories в pandas может быть любого типа данных.

  • Невозможно указать метки во время создания. Используйте s.cat.rename_categories(new_labels) впоследствии.

  • В отличие от R, factor функция, использующая категориальные данные как единственный вход для создания новой категориальной серии, будет не удалить неиспользуемые категории, но создать новую категориальную серию, которая равна переданной!

  • R позволяет включать пропущенные значения в свои levels (pandas' categories). pandas не позволяет NaN категории, но пропущенные значения всё ещё могут быть в values.

Подводные камни#

Использование памяти#

Использование памяти Categorical пропорционально количеству категорий плюс длине данных. В отличие от этого, object dtype является константой, умноженной на длину данных.

In [242]: s = pd.Series(["foo", "bar"] * 1000)

# object dtype
In [243]: s.nbytes
Out[243]: 16000

# category dtype
In [244]: s.astype("category").nbytes
Out[244]: 2016

Примечание

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

In [245]: s = pd.Series(["foo%04d" % i for i in range(2000)])

# object dtype
In [246]: s.nbytes
Out[246]: 16000

# category dtype
In [247]: s.astype("category").nbytes
Out[247]: 20000

Categorical не является numpy массив#

В настоящее время категориальные данные и базовые Categorical реализован как объект Python, а не как низкоуровневый тип данных массива NumPy. Это приводит к некоторым проблемам.

NumPy сам не знает о новом dtype:

In [248]: try:
   .....:     np.dtype("category")
   .....: except TypeError as e:
   .....:     print("TypeError:", str(e))
   .....: 
TypeError: data type 'category' not understood

In [249]: dtype = pd.Categorical(["a"]).dtype

In [250]: try:
   .....:     np.dtype(dtype)
   .....: except TypeError as e:
   .....:     print("TypeError:", str(e))
   .....: 
TypeError: Cannot interpret 'CategoricalDtype(categories=['a'], ordered=False, categories_dtype=object)' as a data type

Сравнения типов данных работают:

In [251]: dtype == np.str_
Out[251]: False

In [252]: np.str_ == dtype
Out[252]: False

Чтобы проверить, содержит ли Series категориальные данные, используйте hasattr(s, 'cat'):

In [253]: hasattr(pd.Series(["a"], dtype="category"), "cat")
Out[253]: True

In [254]: hasattr(pd.Series(["a"]), "cat")
Out[254]: False

Использование функций NumPy на Series типа category не должно работать как Categoricals не являются числовыми данными (даже в случае, если .categories является числовым).

In [255]: s = pd.Series(pd.Categorical([1, 2, 3, 4]))

In [256]: try:
   .....:     np.sum(s)
   .....: except TypeError as e:
   .....:     print("TypeError:", str(e))
   .....: 
TypeError: 'Categorical' with dtype category does not support reduction 'sum'

Примечание

Если такая функция работает, пожалуйста, сообщите об ошибке на pandas-dev/pandas!

dtype в apply#

pandas в настоящее время не сохраняет dtype в функциях apply: Если применяете по строкам, получаете Series of object dtype (так же, как получение строки -> получение одного элемента вернет базовый тип) и применение по столбцам также преобразует в object. NaN значения не затрагиваются. Вы можете использовать fillna для обработки пропущенных значений перед применением функции.

In [257]: df = pd.DataFrame(
   .....:     {
   .....:         "a": [1, 2, 3, 4],
   .....:         "b": ["a", "b", "c", "d"],
   .....:         "cats": pd.Categorical([1, 2, 3, 2]),
   .....:     }
   .....: )
   .....: 

In [258]: df.apply(lambda row: type(row["cats"]), axis=1)
Out[258]: 
0    
1    
2    
3    
dtype: object

In [259]: df.apply(lambda col: col.dtype, axis=0)
Out[259]: 
a          int64
b         object
cats    category
dtype: object

Категориальный индекс#

CategoricalIndex является типом индекса, полезным для поддержки индексирования с дубликатами. Это контейнер вокруг Categorical и позволяет эффективное индексирование и хранение индекса с большим количеством повторяющихся элементов. См. документация по расширенной индексации для более подробного объяснения.

Установка индекса создаст CategoricalIndex:

In [260]: cats = pd.Categorical([1, 2, 3, 4], categories=[4, 2, 3, 1])

In [261]: strings = ["a", "b", "c", "d"]

In [262]: values = [4, 2, 3, 1]

In [263]: df = pd.DataFrame({"strings": strings, "values": values}, index=cats)

In [264]: df.index
Out[264]: CategoricalIndex([1, 2, 3, 4], categories=[4, 2, 3, 1], ordered=False, dtype='category')

# This now sorts by the categories order
In [265]: df.sort_index()
Out[265]: 
  strings  values
4       d       1
2       b       2
3       c       3
1       a       4

Побочные эффекты#

Создание Series из Categorical не будет копировать входные данные Categorical. Это означает, что изменения в Series в большинстве случаев изменит исходный Categorical:

In [266]: cat = pd.Categorical([1, 2, 3, 10], categories=[1, 2, 3, 4, 10])

In [267]: s = pd.Series(cat, name="cat")

In [268]: cat
Out[268]: 
[1, 2, 3, 10]
Categories (5, int64): [1, 2, 3, 4, 10]

In [269]: s.iloc[0:2] = 10

In [270]: cat
Out[270]: 
[10, 10, 3, 10]
Categories (5, int64): [1, 2, 3, 4, 10]

Используйте copy=True Страница устранения неполадок Numba Categoricals:

In [271]: cat = pd.Categorical([1, 2, 3, 10], categories=[1, 2, 3, 4, 10])

In [272]: s = pd.Series(cat, name="cat", copy=True)

In [273]: cat
Out[273]: 
[1, 2, 3, 10]
Categories (5, int64): [1, 2, 3, 4, 10]

In [274]: s.iloc[0:2] = 10

In [275]: cat
Out[275]: 
[1, 2, 3, 10]
Categories (5, int64): [1, 2, 3, 4, 10]

Примечание

Это также происходит в некоторых случаях, когда вы предоставляете массив NumPy вместо Categorical: используя массив int (например, np.array([1,2,3,4])) будет демонстрировать такое же поведение, в то время как использование строкового массива (например, np.array(["a","b","c","a"])) не будет.