Данные категорийного типа
Это введение в тип данных категорий pandas, включая краткое сравнение с factor.
Categoricals — это тип данных pandas, соответствующий категориальным переменным в статистике. Категориальная переменная принимает ограниченное, и обычно фиксированное, количество возможных значений (categories; levels в R). Примерами являются пол, социальный класс, группа крови, страна проживания, время наблюдения или оценка по шкале Ликерта.
В отличие от статистических категориальных переменных, категориальные данные могут иметь порядок (например, «полностью согласен» против «согласен» или «первое наблюдение» против «второе наблюдение»), но числовые операции (сложение, деление и т.д.) невозможны.
Все значения категориальных данных находятся либо в 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:
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', использовалось стандартное поведение:
Категории вычисляются из данных.
Категории не упорядочены.
Для управления этим поведением вместо передачи '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() для сохранения шага factorize в стандартном режиме конструктора:
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)
Примечание
В отличие от функции factor в R, категориальные данные не преобразуют входные значения в строки; категории сохранят тот же тип данных, что и исходные значения.
Примечание
В отличие от функции factor в R, в настоящее время нет возможности назначать/изменять метки во время создания. Используйте categories для изменения категорий после создания.
CategoricalDtype
Тип категориальных данных полностью описывается:
categories: последовательность уникальных значений без пропущенных значений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)
In [47]: CategoricalDtype(["a", "b", "c"], ordered=True)
Out[47]: CategoricalDtype(categories=['a', 'b', 'c'], ordered=True)
In [48]: CategoricalDtype()
Out[48]: CategoricalDtype(categories=None, ordered=False)
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
Предупреждение
Поскольку dtype='category' по существу CategoricalDtype(None, False), и поскольку все экземпляры CategoricalDtype сравниваются как равные 'category', все экземпляры CategoricalDtype сравниваются как равные CategoricalDtype(None, False), независимо от categories или ordered.
Описание
Использование 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():
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’s factor, категориальные данные могут содержать категории других типов, помимо строковых.
Примечание
Обратите внимание, что присвоение новых категорий — это операция на месте, в то время как большинство других операций в Series.cat по умолчанию возвращают новую Series типа category.
Категории должны быть уникальными, иначе будет поднято исключение 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():
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.sort_values(inplace=True)
In [90]: s = pd.Series(["a", "b", "c", "a"]).astype(CategoricalDtype(ordered=True))
In [91]: s.sort_values(inplace=True)
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.sort_values(inplace=True)
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.sort_values(inplace=True)
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.
Если нужно выполнить сравнение на «неравенство» категориального Series со спископодобным объектом, который не является категориальными данными, необходимо явно преобразовать категориальные данные обратно в исходные значения:
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 <class 'numpy.ndarray'>.
If you want to compare values, use 'np.asarray(cat) <op> 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
dtype: int64
DataFrame методы, такие как DataFrame.sum(), также показывают «неиспользуемые» категории.
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]),
.....: )
.....:
In [134]: df.groupby(axis=1, level=1).sum()
Out[134]:
One Two Three
0 3 3 0
1 9 6 0
Группировка также покажет «неиспользуемые» категории:
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").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"]).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"])
Out[143]:
values
A B
a c 1
d 2
b c 3
d 4
Обработка данных
Оптимизированные методы доступа к данным pandas .loc, .iloc, .at, и .iat, работают как обычно. Единственное различие — тип возвращаемого значения (для получения) и то, что можно назначать только значения, уже присутствующие в categories.
Получение
Если операция среза возвращает либо DataFrame, либо столбец типа Series, тип category сохраняется.
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: int64
Примечание
Возвращаемое Series (или DataFrame) имеет тот же тип, что и если вы использовали .str.<method> / .dt.<method> на 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 типа строка, где много элементов повторяется (то есть количество уникальных элементов в Series намного меньше длины Series). В этом случае быстрее преобразовать исходное Series в один из типов category и использовать .str.<method> или .dt.<property> на нем.
Установка
Установка значений в категориальном столбце (или 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 совпадают:
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, в противном случае результаты зависят от типа базовых категорий. Слияния, приводящие к типам данных, не являющимся категориальными, вероятно, потребуют больше памяти. Используйте .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 | identical | результат |
|---|---|---|---|
category | category | True | category |
category (object) | category (object) | False | object (тип определяется по умолчанию) |
category (int) | category (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 [1]: a = pd.Categorical(["a", "b"], ordered=True)
In [2]: b = pd.Categorical(["a", "b", "c"], ordered=True)
In [3]: union_categoricals([a, b])
Out[3]:
TypeError: to union ordered Categoricals, all categories must be the same
Упорядоченные категориальные данные с разными категориями или порядками можно объединить, используя аргумент ignore_ordered=True.
In [203]: a = pd.Categorical(["a", "b", "c"], ordered=True)
In [204]: b = pd.Categorical(["c", "b", "a"], ordered=True)
In [205]: union_categoricals([a, b], ignore_order=True)
Out[205]:
['a', 'b', 'c', 'c', 'b', 'a']
Categories (3, object): ['a', 'b', 'c']
union_categoricals() также работает со списком или массивом, содержащим категориальные данные, но обратите внимание, что результирующий массив всегда будет простым Categorical:
In [206]: a = pd.Series(["b", "c"], dtype="category")
In [207]: b = pd.Series(["a", "b"], dtype="category")
In [208]: union_categoricals([a, b])
Out[208]:
['b', 'c', 'a', 'b']
Categories (3, object): ['b', 'c', 'a']
Примечание
union_categoricals может перекодировать целочисленные коды категорий при объединении категориальных данных. Вероятно, это то, что вам нужно, но если вы полагаетесь на точное нумерование категорий, имейте в виду это.
In [209]: c1 = pd.Categorical(["b", "c"])
In [210]: c2 = pd.Categorical(["a", "b"])
In [211]: c1
Out[211]:
['b', 'c']
Categories (2, object): ['b', 'c']
# "b" is coded to 0
In [212]: c1.codes
Out[212]: array([0, 1], dtype=int8)
In [213]: c2
Out[213]:
['a', 'b']
Categories (2, object): ['a', 'b']
# "b" is coded to 1
In [214]: c2.codes
Out[214]: array([0, 1], dtype=int8)
In [215]: c = union_categoricals([c1, c2])
In [216]: c
Out[216]:
['b', 'c', 'a', 'b']
Categories (3, object): ['b', 'c', 'a']
# "b" is coded to 0 throughout, same as c1, different from c2
In [217]: c.codes
Out[217]: array([0, 1, 2, 0], dtype=int8)
Получение данных в/из
Вы можете записать данные, содержащие типы category в HDFStore. См. здесь для примера и замечаний.
Также можно записать данные в файлы формата Stata и считать данные из них. См. здесь для примера и замечаний.
Запись в CSV-файл преобразует данные, фактически удаляя любую информацию о категориях (категории и порядок). Поэтому, если вы считываете CSV-файл, вам необходимо преобразовать соответствующие столбцы обратно в category и назначить правильные категории и порядок категорий.
In [218]: import io
In [219]: s = pd.Series(pd.Categorical(["a", "b", "b", "a", "a", "d"]))
# rename the categories
In [220]: s = s.cat.rename_categories(["very good", "good", "bad"])
# reorder the categories and add missing categories
In [221]: s = s.cat.set_categories(["very bad", "bad", "medium", "good", "very good"])
In [222]: df = pd.DataFrame({"cats": s, "vals": [1, 2, 3, 4, 5, 6]})
In [223]: csv = io.StringIO()
In [224]: df.to_csv(csv)
In [225]: df2 = pd.read_csv(io.StringIO(csv.getvalue()))
In [226]: df2.dtypes
Out[226]:
Unnamed: 0 int64
cats object
vals int64
dtype: object
In [227]: df2["cats"]
Out[227]:
0 very good
1 good
2 good
3 very good
4 very good
5 bad
Name: cats, dtype: object
# Redo the category
In [228]: df2["cats"] = df2["cats"].astype("category")
In [229]: df2["cats"].cat.set_categories(
.....: ["very bad", "bad", "medium", "good", "very good"], inplace=True
.....: )
.....:
In [230]: df2.dtypes
Out[230]:
Unnamed: 0 int64
cats category
vals int64
dtype: object
In [231]: df2["cats"]
Out[231]:
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 для представления отсутствующих данных. Оно по умолчанию не включается в вычисления. См. раздел Отсутствующие данные.
Пропущенные значения не должны включаться в categories категорий, только в values. Вместо этого предполагается, что NaN отличается и всегда возможен. При работе с codes категорий, пропущенные значения всегда будут иметь код -1.
In [232]: s = pd.Series(["a", "b", np.nan, "a"], dtype="category")
# only two categories
In [233]: s
Out[233]:
0 a
1 b
2 NaN
3 a
dtype: category
Categories (2, object): ['a', 'b']
In [234]: s.cat.codes
Out[234]:
0 0
1 1
2 -1
3 0
dtype: int8
Методы работы с отсутствующими данными, например, isna(), fillna(), dropna(), работают нормально:
In [235]: s = pd.Series(["a", "b", np.nan], dtype="category")
In [236]: s
Out[236]:
0 a
1 b
2 NaN
dtype: category
Categories (2, object): ['a', 'b']
In [237]: pd.isna(s)
Out[237]:
0 False
1 False
2 True
dtype: bool
In [238]: s.fillna("a")
Out[238]:
0 a
1 b
2 a
dtype: category
Categories (2, object): ['a', 'b']
Отличия от R-функций factor
Ниже перечислены отличия от функций R’s factor:
Функции R’s
levelsназываютсяcategories.Значения R’s
levelsвсегда строкового типа, тогда какcategoriesв pandas могут иметь любой тип данных.Невозможно указать метки при создании. Используйте
s.cat.rename_categories(new_labels)позже.В отличие от функции R’s
factor, использование категориальных данных в качестве единственного входного параметра для создания нового категориального ряда не удалит неиспользуемые категории, а создаст новый категориальный ряд, равный переданному.R допускает включение пропущенных значений в свои
levels(categoriespandas). pandas не допускаетNaNкатегорий, но пропущенные значения всё ещё могут присутствовать вvalues.
Особенности
Использование памяти
Использование памяти Categorical пропорционально количеству категорий плюс длине данных. В отличие от этого, тип данных object пропорционален длине данных с постоянным коэффициентом.
In [239]: s = pd.Series(["foo", "bar"] * 1000)
# object dtype
In [240]: s.nbytes
Out[240]: 16000
# category dtype
In [241]: s.astype("category").nbytes
Out[241]: 2016
Примечание
Если количество категорий приближается к длине данных, Categorical будет использовать почти такую же или большую память, чем эквивалентное представление типа данных object.
In [242]: s = pd.Series(["foo%04d" % i for i in range(2000)])
# object dtype
In [243]: s.nbytes
Out[243]: 16000
# category dtype
In [244]: s.astype("category").nbytes
Out[244]: 20000
Categorical — не массив numpy
В настоящее время категориальные данные и лежащий в основе тип Categorical реализованы как объект Python, а не как низкоуровневый тип данных массива NumPy. Это приводит к некоторым проблемам.
Сам NumPy не знает о новом типе dtype:
In [245]: try:
.....: np.dtype("category")
.....: except TypeError as e:
.....: print("TypeError:", str(e))
.....:
TypeError: data type 'category' not understood
In [246]: dtype = pd.Categorical(["a"]).dtype
In [247]: try:
.....: np.dtype(dtype)
.....: except TypeError as e:
.....: print("TypeError:", str(e))
.....:
TypeError: Cannot interpret 'CategoricalDtype(categories=['a'], ordered=False)' as a data type
Сравнения типов данных работают:
In [248]: dtype == np.str_
Out[248]: False
In [249]: np.str_ == dtype
Out[249]: False
Чтобы проверить, содержит ли Series категориальные данные, используйте hasattr(s, 'cat'):
In [250]: hasattr(pd.Series(["a"], dtype="category"), "cat")
Out[250]: True
In [251]: hasattr(pd.Series(["a"]), "cat")
Out[251]: False
Использование функций NumPy над Series типа category не должно работать, так как Categoricals — не числовые данные (даже если .categories числовые).
In [252]: s = pd.Series(pd.Categorical([1, 2, 3, 4]))
In [253]: try:
.....: np.sum(s)
.....: except TypeError as e:
.....: print("TypeError:", str(e))
.....:
TypeError: 'Categorical' with dtype category does not support reduction 'sum'
Примечание
Если такая функция работает, пожалуйста, откройте баг-репорт на https://github.com/pandas-dev/pandas!
dtype в apply
pandas в настоящее время не сохраняет dtype в функциях apply: если вы применяете функцию по строкам, вы получаете Series типа object dtype (то же самое, что получить строку —> получение одного элемента вернёт базовый тип), а применение по столбцам также приведёт к преобразованию в тип object. NaN значения не затрагиваются. Вы можете использовать fillna для обработки пропущенных значений перед применением функции.
In [254]: df = pd.DataFrame(
.....: {
.....: "a": [1, 2, 3, 4],
.....: "b": ["a", "b", "c", "d"],
.....: "cats": pd.Categorical([1, 2, 3, 2]),
.....: }
.....: )
.....:
In [255]: df.apply(lambda row: type(row["cats"]), axis=1)
Out[255]:
0 <class 'int'>
1 <class 'int'>
2 <class 'int'>
3 <class 'int'>
dtype: object
In [256]: df.apply(lambda col: col.dtype, axis=0)
Out[256]:
a int64
b object
cats category
dtype: object
Индекс Categorical
CategoricalIndex — это тип индекса, полезный для поддержки индексирования с дубликатами. Это контейнер вокруг Categorical, который позволяет эффективно индексировать и хранить индекс с большим количеством дублированных элементов. Подробное объяснение см. в документации по расширенному индексированию по адресу advanced indexing docs.
Установление индекса создаст CategoricalIndex:
In [257]: cats = pd.Categorical([1, 2, 3, 4], categories=[4, 2, 3, 1])
In [258]: strings = ["a", "b", "c", "d"]
In [259]: values = [4, 2, 3, 1]
In [260]: df = pd.DataFrame({"strings": strings, "values": values}, index=cats)
In [261]: df.index
Out[261]: CategoricalIndex([1, 2, 3, 4], categories=[4, 2, 3, 1], ordered=False, dtype='category')
# This now sorts by the categories order
In [262]: df.sort_index()
Out[262]:
strings values
4 d 1
2 b 2
3 c 3
1 a 4
Побочные эффекты
Создание Series из Categorical не скопирует входной Categorical. Это означает, что изменения в Series в большинстве случаев изменят исходный Categorical:
In [263]: cat = pd.Categorical([1, 2, 3, 10], categories=[1, 2, 3, 4, 10])
In [264]: s = pd.Series(cat, name="cat")
In [265]: cat
Out[265]:
[1, 2, 3, 10]
Categories (5, int64): [1, 2, 3, 4, 10]
In [266]: s.iloc[0:2] = 10
In [267]: cat
Out[267]:
[10, 10, 3, 10]
Categories (5, int64): [1, 2, 3, 4, 10]
In [268]: df = pd.DataFrame(s)
In [269]: df["cat"].cat.categories = [1, 2, 3, 4, 5]
In [270]: cat
Out[270]:
[5, 5, 3, 5]
Categories (5, int64): [1, 2, 3, 4, 5]
Используйте copy=True для предотвращения такого поведения или просто не используйте 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. Использование целочисленного массива (например, np.array([1,2,3,4])) продемонстрирует то же поведение, в то время как использование строкового массива (например, np.array(["a","b","c","a"])) не будет.
© 2008–2022, AQR Capital Management, LLC, Lambda Foundry, Inc. and PyData Development Team
Licensed under the 3-clause BSD License.
https://pandas.pydata.org/pandas-docs/version/1.5.0/user_guide/categorical.html