类别数据#

这是对 pandas 类别数据类型的介绍,包括与 R 的 factor 的简要比较。

Categoricals 是 pandas 中对应统计学中类别变量的数据类型。类别变量取值有限且通常固定(categories;R 中称为 levels)。例如性别、社会阶层、血型、国家归属、观察时间或李克特量表评分。

与统计学中的类别变量不同,类别数据可能具有顺序(例如,“非常同意” vs “同意” 或 “第一次观察” vs “第二次观察”),但不能进行数值运算(加法、除法等)。

类别数据的 所有值 必须在 categories 中,或为 np.nan。顺序由 categories 的顺序决定,而不是值的词典顺序。内部数据结构由一个 categories 数组和一个整数数组 codes 组成,后者指向 categories 数组中的实际值。

在以下情况下,类别数据类型很有用:

  • 一个只包含少量不同值的字符串变量。将此类字符串变量转换为类别变量可以节省一些内存,请参阅 此处

  • 变量的词典顺序与逻辑顺序(“一”、“二”、“三”)不同。通过转换为类别变量并指定类别顺序,排序和 min/max 操作将使用逻辑顺序而不是词典顺序,请参阅 此处

  • 作为信号,告知其他 Python 库此列应被视为类别变量(例如,使用合适的统计方法或绘图类型)。

另请参阅 类别数据的 API 文档

对象创建#

Series 创建#

类别 SeriesDataFrame 中的列可以通过多种方式创建:

在构造 Series 时指定 dtype="category"

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, str): ['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(
   ....:     [None, "b", "c", None], 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, str): ['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         str
B    category
dtype: object

DataFrame 创建#

与上一节将单个列转换为类别的示例类似,DataFrame 中的所有列都可以在构造期间或之后批量转换为类别。

这可以通过在 DataFrame 构造函数中指定 dtype="category" 来完成。

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, str): ['a', 'b', 'c']

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

同样,可以使用 DataFrame.astype() 批量转换现有 DataFrame 中的所有列。

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, str): ['a', 'b', 'c']

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

控制行为#

在上面传递 dtype='category' 的示例中,我们使用了默认行为:

  1. 类别是从数据中推断出来的。

  2. 类别是无序的。

要控制这些行为,请传递 'category' 的实例,而不是传递 'category' 字符串,而是使用 CategoricalDtype 的实例。

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

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

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, str): ['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, str): ['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, str): ['a' < 'b' < 'c' < 'd']

注意

要执行表级转换,将整个 DataFrame 中的所有标签用作每列的类别,则 categories 参数可以通过 categories = pd.unique(df.to_numpy().ravel()) 以编程方式确定。

如果您已经有了 codescategories,则可以使用 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: str

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

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

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

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=str)

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

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

请注意,应考虑 categories_dtype,尤其是在与两个空的 CategoricalDtype 实例进行比较时。

In [53]: c2 = pd.Categorical(np.array([], dtype=object))

In [54]: c3 = pd.Categorical(np.array([], dtype=float))

In [55]: c2.dtype == c3.dtype
Out[55]: False

描述#

对类别数据使用 describe() 将产生与 string 类型 SeriesDataFrame 类似的输出。

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

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

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

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

处理类别#

类别数据具有 categoriesordered 属性,它们列出了其可能的值以及顺序是否重要。这些属性通过 s.cat.categoriess.cat.ordered 暴露。如果您不手动指定类别和顺序,它们将从传入的参数推断出来。

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

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

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

也可以按特定顺序传入类别。

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

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

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

注意

新创建的类别数据 **不** 是自动排序的。您必须显式传递 ordered=True 来指示一个有序的 Categorical

注意

unique() 的结果不总是与 Series.cat.categories 相同,因为 Series.unique() 有一些保证,即它按出现顺序返回类别,并且只包含实际存在的值。

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

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

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

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

重命名类别#

通过使用 rename_categories() 方法进行类别重命名。

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

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

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

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

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

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

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

注意

与 R 的 factor 不同,类别数据可以具有除字符串以外的其他数据类型的类别。

类别必须唯一,否则将引发 ValueError

In [77]: 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 [78]: 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 [79]: s = s.cat.add_categories([4])

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

In [81]: s
Out[81]: 
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 [82]: s = s.cat.remove_categories([4])

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

移除未使用的类别#

也可以通过以下方式移除未使用的类别:

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

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

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

设置类别#

如果您想一步完成移除和添加新类别(这有一些速度优势),或者仅仅是将类别设置为预定义的尺度,请使用 set_categories()

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

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

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

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

注意

请注意,Categorical.set_categories() 无法知道某个类别是故意省略的,还是因为拼写错误,或者(在 Python3 中)由于类型差异(例如,NumPy S1 数据类型和 Python 字符串)而被省略。这可能会导致意外的行为!

排序和顺序#

如果类别数据是有序的(s.cat.ordered == True),那么类别的顺序就有意义,并且可以执行某些操作。如果类别是无序的,.min()/.max() 将引发 TypeError

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

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

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

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

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

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

您可以使用 as_ordered() 将类别数据设置为有序,或使用 as_unordered() 将其设置为无序。默认情况下,这些将返回一个 *新* 对象。

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

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

排序将使用类别定义的顺序,而不是数据类型中存在的任何词典顺序。即使对于字符串和数字数据也是如此。

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

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

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

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

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

In [104]: s.min(), s.max()
Out[104]: (np.int64(2), np.int64(1))

重新排序#

可以通过 Categorical.reorder_categories()Categorical.set_categories() 方法重新排序类别。对于 Categorical.reorder_categories(),所有旧类别都必须包含在新类别中,并且不允许新类别。这将必然使排序顺序与类别顺序相同。

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

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

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

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

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

In [110]: s.min(), s.max()
Out[110]: (np.int64(2), np.int64(1))

注意

请注意分配新类别和重新排序类别之间的区别:前者重命名类别,因此 Series 中的单个值会被重命名,但如果第一个位置被排序到最后,则重命名的值仍将被排序到最后。重新排序意味着之后值的排序方式不同,但 Series 中的单个值不会改变。

注意

如果 Categorical 不是有序的,Series.min()Series.max() 将引发 TypeError。数值运算如 +-*/ 以及基于它们的运算(例如,如果数组的长度是偶数,Series.median(),它需要计算两个值之间的平均值)将不起作用并引发 TypeError

多列排序#

类别数据类型的列将以与其他列类似的方式参与多列排序。类别的顺序由该列的 categories 决定。

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

In [112]: dfs.sort_values(by=["A", "B"])
Out[112]: 
   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 [113]: dfs["A"] = dfs["A"].cat.reorder_categories(["a", "b", "e"])

In [114]: dfs.sort_values(by=["A", "B"])
Out[114]: 
   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、数组等)进行相等性比较(==!=)。

  • ordered==Truecategories 相同时,类别数据与另一个类别 Series 的所有比较(==!=>>=<<=)。

  • 类别数据与标量的所有比较。

所有其他比较,特别是具有不同类别的两个类别之间的“非相等”比较,或类别与任何列表类对象之间的比较,将引发 TypeError

注意

类别数据与具有不同类别或顺序的 Series、np.array、列表或类别数据的任何“非相等”比较都将引发 TypeError,因为自定义类别顺序可以有两种解释:一种考虑顺序,一种不考虑顺序。

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

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

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

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

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

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

与具有相同类别和顺序的类别或标量进行比较可以正常工作。

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

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

相等性比较可以与任何相同长度的列表类对象和标量一起工作。

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

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

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

这不起作用,因为类别不相同。

In [126]: 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 [127]: base = np.array([1, 2, 3])

In [128]: 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 [129]: np.asarray(cat) > base
Out[129]: array([False, False, False])

当比较两个无序类别且具有相同类别时,不考虑顺序。

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

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

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

操作#

除了 Series.min()Series.max()Series.mode() 之外,类别数据还可以进行以下操作:

Series 方法,例如 Series.value_counts() 将使用所有类别,即使某些类别未在数据中出现。

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

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

DataFrame 方法,例如 DataFrame.sum(),在 observed=False 时也会显示“未使用的”类别。

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

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

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

observed=False 时,GroupBy 也会显示“未使用的”类别。

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

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

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

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

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

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

透视表

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

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

In [146]: pd.pivot_table(df, values="values", index=["A", "B"], observed=False)
Out[146]: 
     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 [147]: idx = pd.Index(["h", "i", "j", "k", "l", "m", "n"])

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

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

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

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

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

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

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

一个类别类型未被保留的例子是,如果您获取一行:结果的 Seriesobject 数据类型。

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

从类别数据中返回单个项目也将返回该值,而不是长度为“1”的类别。

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

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

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

注意

这与 R 的 factor 函数形成对比,其中 factor(c(1,2,3))[1] 返回单个 factor 值。

要获得一个长度为 1 的 category 类型 Series,请传入一个包含单个值的列表。

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

字符串和日期时间访问器#

s.cat.categories 的类型合适时,访问器 .dt.str 将工作。

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

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

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

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

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

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

In [166]: date_cat
Out[166]: 
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[us]): [2015-01-01, 2015-01-02, 2015-01-03, 2015-01-04, 2015-01-05]

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

注意

返回的 Series(或 DataFrame)的类型与您在类型为该类型的 Series 上使用 .str.<method>.dt.<method> 时的类型相同(而不是 category 类型!)。

这意味着,从 Series 的访问器方法和属性返回的值,以及从此 Series 转换为 category 类型后的访问器方法和属性返回的值将是相等的。

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

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

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

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

注意

工作在 categories 上进行,然后构造一个新的 Series。这会对性能产生影响,如果您有一个字符串类型的 Series,其中很多元素重复(即 Series 中唯一元素的数量远小于 Series 的长度)。在这种情况下,将原始 Series 转换为 category 类型,并在该类型上使用 .str.<method>.dt.<property> 可能会更快。

设置#

只要值包含在 categories 中,就可以在类别列(或 Series)中设置值。

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

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

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

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

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

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

In [178]: 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 [179]: df.loc["j":"k", "cats"] = pd.Categorical(["a", "a"], categories=["a", "b"])

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

In [181]: 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 [182]: df = pd.DataFrame({"a": [1, 1, 1, 1, 1], "b": ["a", "a", "a", "a", "a"]})

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

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

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

In [186]: df.dtypes
Out[186]: 
a    int64
b      str
dtype: object

合并/连接#

默认情况下,组合包含相同类别的 SeriesDataFrames 会得到 category 数据类型,否则结果将取决于底层类别的 dtype。合并产生的非类别数据类型可能会导致更高的内存使用量。使用 .astypeunion_categoricals 来确保 category 结果。

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

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

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

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

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

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

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

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

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

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

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

下表总结了合并 Categoricals 的结果:

arg1

arg2

identical

result

category

category

True

category

category (object)

category (object)

False

object (dtype is inferred)

category (int)

category (float)

False

float (dtype is inferred)

联合#

如果您想组合不一定具有相同类别的类别,union_categoricals() 函数将组合一个列表类的类别。新类别将是正在组合的类别的并集。

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

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

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

In [201]: union_categoricals([a, b])
Out[201]: 
['b', 'c', 'a', 'b']
Categories (3, str): ['b', 'c', 'a']

默认情况下,结果类别将按照它们在数据中出现的顺序排序。如果您希望类别按字母顺序排序,请使用 sort_categories=True 参数。

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

union_categoricals 也适用于“简单”情况,即组合两个具有相同类别和顺序信息的类别(例如,您也可以 append 的)。

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

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

In [205]: union_categoricals([a, b])
Out[205]: 
['a', 'b', 'a', 'b', 'a']
Categories (2, str): ['a' < 'b']

以下代码会引发 TypeError,因为类别是有序的且不相同。

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

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

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

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

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

可以通过使用 ignore_ordered=True 参数来合并具有不同类别或顺序的有序类别。

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

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

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

union_categoricals 也适用于 CategoricalIndex 或包含类别数据的 Series,但请注意,结果数组将始终是普通的 Categorical

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

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

In [214]: union_categoricals([a, b])
Out[214]: 
['b', 'c', 'a', 'b']
Categories (3, str): ['b', 'c', 'a']

注意

union_categoricals 在组合类别时可能会重新编码类别的整数代码。这很可能是您想要的,但如果您依赖于类别的确切编号,请注意。

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

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

In [217]: c1
Out[217]: 
['b', 'c']
Categories (2, str): ['b', 'c']

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

In [219]: c2
Out[219]: 
['a', 'b']
Categories (2, str): ['a', 'b']

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

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

In [222]: c
Out[222]: 
['b', 'c', 'a', 'b']
Categories (3, str): ['b', 'c', 'a']

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

数据导入/导出#

您可以将包含 category 数据类型的写入 HDFStore。有关示例和注意事项,请参阅 此处

也可以将数据写入和读取 *Stata* 格式文件。有关示例和注意事项,请参阅 此处

写入 CSV 文件会将数据进行转换,有效地移除关于类别(类别和顺序)的任何信息。因此,如果您重新读取 CSV 文件,您必须将相关列转换回 category 并分配正确的类别和类别顺序。

In [224]: import io

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

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

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

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

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

In [230]: df.to_csv(csv)

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

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

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

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

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

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

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

使用 to_sql 将数据写入 SQL 数据库也同样适用。

缺失数据#

pandas 主要使用 np.nan 值来表示缺失数据。默认情况下,它不包含在计算中。请参阅 缺失数据部分

缺失值 **不** 应该包含在类别的 categories 中,而应该只包含在 values 中。相反,我们理解 NaN 是不同的,并且始终是一种可能性。当使用类别的 codes 时,缺失值将始终具有代码 -1

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

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

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

处理缺失数据的方法,例如 isna()fillna()dropna(),都可以正常工作。

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

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

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

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

与 R 的 factor 的区别#

与 R 的 factor 函数相比,可以观察到以下区别:

  • R 的 levels 被命名为 categories

  • R 的 levels 始终是字符串类型,而 pandas 中的 categories 可以是任何数据类型。

  • 无法在创建时指定标签。事后使用 s.cat.rename_categories(new_labels)

  • 与 R 的 factor 函数不同,使用类别数据作为唯一输入来创建新的类别 Series **不会** 移除未使用的类别,而是创建一个与传入的 Series 相等的新的类别 Series!

  • R 允许将缺失值包含在其 levels(pandas 的 categories)中。pandas 不允许 NaN 类别,但 values 中仍然可以包含缺失值。

陷阱#

内存使用#

Categorical 的内存使用量与类别的数量加上数据的长度成正比。相比之下,object 数据类型是数据的长度乘以一个常数。

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

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

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

注意

如果类别的数量接近数据的长度,Categorical 将使用与等效的 object 数据类型表示几乎相同或更多的内存。

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

# object dtype
In [249]: s.nbytes
Out[249]: 30000

# category dtype
In [250]: s.astype("category").nbytes
Out[250]: 34250

Categorical 不是 numpy 数组#

当前,类别数据和底层的 Categorical 实现为 Python 对象,而不是低级 NumPy 数组数据类型。这会导致一些问题。

NumPy 本身不了解新的 dtype

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

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

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

Dtype 比较工作正常

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

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

要检查 Series 是否包含类别数据,请使用 hasattr(s, 'cat')

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

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

在类型为 category 的 Series 上使用 NumPy 函数不应该起作用,因为 Categoricals 不是数值数据(即使 .categories 是数值类型)。

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

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

注意

如果此类函数有效,请在 pandas-dev/pandas 提交 bug!

apply 中的 dtype#

pandas 目前在 apply 函数中不保留 dtype:如果您沿行应用,您将得到一个 object dtype 的 Series(与获取一行相同 -> 获取一个元素将返回一个基本类型),沿列应用也会转换为 object。NaN 值不受影响。您可以使用 fillna 在应用函数之前处理缺失值。

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

In [261]: df.apply(lambda row: type(row["cats"]), axis=1)
Out[261]: 
0    <class 'int'>
1    <class 'int'>
2    <class 'int'>
3    <class 'int'>
dtype: object

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

Categorical 索引#

CategoricalIndex 是一种索引类型,适用于支持带有重复项的索引。这是一个围绕 Categorical 的容器,可以高效地索引和存储带有大量重复元素的索引。有关更详细的解释,请参阅 高级索引文档

设置索引将创建一个 CategoricalIndex

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

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

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

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

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

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

副作用#

Categorical 构建 Series 不会复制输入的 Categorical。这意味着在大多数情况下,对 Series 的更改将更改原始 Categorical

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

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

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

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

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

使用 copy=True 来防止此类行为,或者干脆不要重用 Categoricals

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

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

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

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

In [278]: cat
Out[278]: 
[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"]))则不会。