Spec-Zone.ru › pandas 2

Данные категорий

Это введение в тип данных 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 тип:

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)

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
END_OF_DOCUMENT_MARKER

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

Категориальные данные имеют свойство 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']

Примечание

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

Категории должны быть уникальными, иначе будет поднято исключение 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() не может определить, была ли категория опущена намеренно или из-за опечатки или (под Python 3) из-за различия типов (например, тип 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.

Если вам нужно выполнить сравнение «неравенства» категориальной 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
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

Группа также покажет «неиспользуемые» категории, когда 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 сохраняется.

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 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.<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

одинаковые

результат

категория

категория

да

категория

категория (объект)

категория (объект)

нет

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

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

категория (вещественное)

нет

вещественное (тип выводится)

Объединение

Если вы хотите объединить категориальные данные, которые не обязательно имеют одинаковые категории, функция 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 для представления пропущенных данных. По умолчанию оно не включается в вычисления. См. раздел Пропущенные данные.

Пропущенные значения не должны включаться в categories категориальной переменной, только в values. Вместо этого предполагается, что NaN отличается и всегда является возможностью. При работе с 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

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

  • Факторы R называются categories.

  • Факторы R всегда строкового типа, а categories в pandas могут иметь любой тип данных.

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

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

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

END_OF_DOCUMENT_MARKER

Особенности

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

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

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!

Тип данных в apply

В настоящее время pandas не сохраняет тип данных в функциях apply: если вы применяете функцию по строкам, вы получаете Series типа object dtype (так же, как получить строку — получение одного элемента вернет базовый тип), а применение по столбцам также приведет к преобразованию в объект. 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    <class 'int'>
1    <class 'int'>
2    <class 'int'>
3    <class 'int'>
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 для предотвращения такого поведения или просто не используйте повторно 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/2.2.2/user_guide/categorical.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API