分类数据#
这是对 pandas 分类数据类型的介绍,包括与 R 的 factor
的简短比较。
Categoricals
是 pandas 的一种数据类型,对应于统计学中的分类变量。分类变量取值有限且通常固定,可能的取值称为 categories``(在 R 中称为 ``levels
)。例如性别、社会阶层、血型、国家归属、观察时间或通过李克特量表的评分。
与统计分类变量相比,分类数据可能有顺序(例如‘非常同意’与‘同意’或‘第一次观察’与‘第二次观察’),但数值操作(加法、除法等)是不可能的。
分类数据的所有的值要么在 categories
中,要么是 np.nan
。顺序由 categories
的顺序定义,而不是值的词法顺序。内部,数据结构由一个 categories
数组和一个整数数组 codes
组成,后者指向 categories
数组中的实际值。
类别数据类型在以下情况下非常有用:
一个仅由几个不同值组成的字符串变量。将这样的字符串变量转换为分类变量将节省一些内存,请参见 这里。
变量的词法顺序与逻辑顺序(“one”,“two”,“three”)不同。通过转换为分类变量并指定类别顺序,排序和最小/最大值将使用逻辑顺序而不是词法顺序,请参见 这里。
作为向其他 Python 库发出的信号,表明此列应被视为分类变量(例如,使用合适的统计方法或绘图类型)。
另请参阅 分类的API文档。
对象创建#
系列创建#
分类 Series
或 DataFrame
中的列可以通过几种方式创建:
通过在构造 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, object): ['a', 'b', 'c']
通过将现有的 Series
或列转换为 category
dtype:
In [3]: df = pd.DataFrame({"A": ["a", "b", "c", "a"]})
In [4]: df["B"] = df["A"].astype("category")
In [5]: df
Out[5]:
A B
0 a a
1 b b
2 c c
3 a a
通过使用特殊函数,例如 cut()
,它将数据分组到离散的箱中。请参阅文档中的 分箱示例。
In [6]: df = pd.DataFrame({"value": np.random.randint(0, 100, 20)})
In [7]: labels = ["{0} - {1}".format(i, i + 9) for i in range(0, 100, 10)]
In [8]: df["group"] = pd.cut(df.value, range(0, 105, 10), right=False, labels=labels)
In [9]: df.head(10)
Out[9]:
value group
0 65 60 - 69
1 49 40 - 49
2 56 50 - 59
3 43 40 - 49
4 43 40 - 49
5 91 90 - 99
6 32 30 - 39
7 87 80 - 89
8 36 30 - 39
9 8 0 - 9
通过传递一个 pandas.Categorical
对象给 Series
或将其分配给 DataFrame
。
In [10]: raw_cat = pd.Categorical(
....: ["a", "b", "c", "a"], categories=["b", "c", "d"], ordered=False
....: )
....:
In [11]: s = pd.Series(raw_cat)
In [12]: s
Out[12]:
0 NaN
1 b
2 c
3 NaN
dtype: category
Categories (3, object): ['b', 'c', 'd']
In [13]: df = pd.DataFrame({"A": ["a", "b", "c", "a"]})
In [14]: df["B"] = raw_cat
In [15]: df
Out[15]:
A B
0 a NaN
1 b b
2 c c
3 a NaN
分类数据具有特定的 category
数据类型:
In [16]: df.dtypes
Out[16]:
A object
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, 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.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, 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 = pd.unique(df.to_numpy().ravel())
以编程方式确定 categories
参数。
如果你已经有 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#
一个分类的类型完全由其描述
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, 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
的顺序不被考虑。注意,具有不同 dtypes 的类别是不相同的。
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()
将产生类似于 Series
或 DataFrame
类型为 string
的输出。
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
使用分类#
分类数据有一个 categories
和一个 ordered
属性,这些属性列出了它们的可能值以及顺序是否重要。这些属性通过 s.cat.categories
和 s.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='object')
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='object')
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, object): ['a', 'b', 'c', 'd']
# categories
In [68]: s.cat.categories
Out[68]: Index(['a', 'b', 'c', 'd'], dtype='object')
# uniques
In [69]: s.unique()
Out[69]:
['b', 'a', 'c']
Categories (4, object): ['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, object): ['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, object): ['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, object): ['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, object): ['a', 'b', 'c', 'd']
In [86]: s.cat.remove_unused_categories()
Out[86]:
0 a
1 b
2 a
dtype: category
Categories (2, object): ['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, object): ['-', '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, object): ['one', 'two', 'three', 'four']
备注
请注意,Categorical.set_categories()
无法知道某些类别是被故意省略的,还是因为拼写错误,或者(在Python3中)由于类型差异(例如,NumPy S1 dtype 和 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, object): ['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, object): ['a' < 'b' < 'c']
In [98]: s.cat.as_unordered()
Out[98]:
0 a
3 a
1 b
2 c
dtype: category
Categories (3, object): ['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]: (2, 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]: (2, 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
比较#
在以下三种情况下,可以比较分类数据与其他对象:
比较相等性(
==
和!=
)与一个类似列表的对象(列表、系列、数组等),该对象的长度与分类数据相同。当
ordered==True
且categories
相同时,所有对分类数据的比较(==
、!=
、>
、>=
、<
和<=
)与另一个分类系列进行的比较。所有对分类数据的标量比较。
所有其他比较,特别是具有不同类别或类别与任何类似列表对象之间的“非等式”比较,将引发 TypeError
。
备注
任何与 Series
、 np.array
、 list
或具有不同类别或排序的分类数据的“非等式”比较都会引发 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.
如果你想对一个分类系列与一个非分类数据的类似列表对象进行“非等式”比较,你需要明确地将分类数据转换回原始值:
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
Groupby 在 observed=False
时也会显示“未使用”的类别:
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, object): ['a', 'b', 'c']
In [154]: df[df["cats"] == "b"]
Out[154]:
cats values
i b 2
j b 2
k b 2
一个类别类型没有被保留的例子是如果你取一行:结果的 Series
是 dtype object
:
# 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
。
要获取一个类型为 category
的单值 Series
,你可以传入一个包含单个值的列表:
In [159]: df.loc[["h"], "cats"]
Out[159]:
h x
Name: cats, dtype: category
Categories (3, object): ['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, object): ['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[ns]): [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
的访问器的方法和属性返回的值,以及从转换为 category
类型的 Series
的访问器的方法和属性返回的值将是相等的:
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>
可能会更快。
设置#
在分类列(或 Series
)中设置值,只要该值包含在 categories
中即可:
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 object
dtype: object
合并 / 连接#
默认情况下,合并包含相同类别的 Series
或 DataFrames
会生成 category
数据类型,否则结果将取决于底层类别的数据类型。合并结果为非分类数据类型可能会占用更多内存。使用 .astype
或 union_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, object): ['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: object
# 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, object): ['a', 'b', 'c']
In [197]: union_categoricals([s1.array, s3.array])
Out[197]:
['a', 'b', 'b', 'c']
Categories (3, object): ['a', 'b', 'c']
下表总结了合并 Categoricals
的结果:
arg1 |
arg2 |
identical |
result |
---|---|---|---|
类别 |
类别 |
真 |
类别 |
类别 (对象) |
类别 (对象) |
假 |
对象(dtype 是推断的) |
类别 (int) |
类别 (浮点数) |
假 |
浮点数(数据类型是推断的) |
联合#
如果你想合并不一定具有相同类别的分类,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, object): ['b', 'c', 'a']
默认情况下,生成的类别将按它们在数据中出现的顺序排列。如果你想让类别按字母顺序排序,请使用 sort_categories=True
参数。
In [202]: union_categoricals([a, b], sort_categories=True)
Out[202]:
['b', 'c', 'a', 'b']
Categories (3, object): ['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, object): ['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 /home/pandas/pandas/core/dtypes/concat.py:328, in union_categoricals(to_union, sort_categories, ignore_order)
326 if all(c.ordered for c in to_union):
327 msg = "to union ordered Categoricals, all categories must be the same"
--> 328 raise TypeError(msg)
329 raise TypeError("Categorical.ordered must be the same")
331 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, object): ['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, object): ['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, object): ['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, object): ['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, object): ['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
dtypes 的数据写入 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 object
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: object
# 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, object): ['very bad', 'bad', 'medium', 'good', 'very good']
使用 to_sql
写入 SQL 数据库也是同样的道理。
缺失数据#
pandas 主要使用值 np.nan
来表示缺失数据。默认情况下,它不包含在计算中。请参见 缺失数据部分。
缺失值不应包含在 Categorical 的 categories
中,而应包含在 values
中。相反,NaN 被理解为不同的,并且始终是一个可能性。在使用 Categorical 的 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, object): ['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, object): ['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, object): ['a', 'b']
与 R 的 factor
的区别#
可以观察到以下与 R 的因子函数的差异:
R 的
levels
被命名为categories
。R 的
levels
总是字符串类型,而 pandas 中的categories
可以是任何数据类型。在创建时无法指定标签。之后使用
s.cat.rename_categories(new_labels)
。与 R 的
factor
函数相比,使用分类数据作为唯一输入来创建新的分类系列将 不会 移除未使用的类别,而是创建一个与传入数据相等的新分类系列!R 允许在其
levels``(pandas 的 ``categories
)中包含缺失值。pandas 不允许NaN
类别,但缺失值仍然可以存在于values
中。
陷阱#
内存使用#
Categorical
的内存使用量与类别数量加上数据长度成正比。相比之下,object
dtype 是数据长度的常数倍。
In [245]: s = pd.Series(["foo", "bar"] * 1000)
# object dtype
In [246]: s.nbytes
Out[246]: 16000
# category dtype
In [247]: s.astype("category").nbytes
Out[247]: 2016
备注
如果类别的数量接近数据的长度,Categorical
将使用几乎相同或更多的内存比等效的 object
dtype 表示。
In [248]: s = pd.Series(["foo%04d" % i for i in range(2000)])
# object dtype
In [249]: s.nbytes
Out[249]: 16000
# category dtype
In [250]: s.astype("category").nbytes
Out[250]: 20000
Categorical
不是一个 numpy
数组#
目前,分类数据和底层 Categorical
是作为 Python 对象实现的,而不是作为低级 NumPy 数组 dtype 实现的。这导致了一些问题。
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=object)' as a data type
数据类型比较有效:
In [254]: dtype == np.str_
Out[254]: False
In [255]: np.str_ == dtype
Out[255]: False
要检查一个 Series 是否包含 Categorical 数据,使用 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 提交一个错误报告!
dtype 在 apply 中#
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 object
cats category
dtype: object
分类索引#
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"])
)则不会。