Pandas 2.2 中文官方教程和指南(十七)

发布于:2024-04-25 ⋅ 阅读:(38) ⋅ 点赞:(0)

原文:pandas.pydata.org/docs/

重复标签

原文:pandas.pydata.org/docs/user_guide/duplicates.html

Index对象不需要是唯一的;你可以有重复的行或列标签。这一点可能一开始会有点困惑。如果你熟悉 SQL,你会知道行标签类似于表上的主键,你绝不希望在 SQL 表中有重复项。但 pandas 的一个作用是在数据传输到某个下游系统之前清理混乱的真实世界数据。而真实世界的数据中有重复项,即使在应该是唯一的字段中也是如此。

本节描述了重复标签如何改变某些操作的行为,以及如何在操作过程中防止重复项的出现,或者在出现重复项时如何检测它们。

In [1]: import pandas as pd

In [2]: import numpy as np 

重复标签的后果

一些 pandas 方法(例如Series.reindex())在存在重复项时根本无法工作。输出无法确定,因此 pandas 会引发异常。

In [3]: s1 = pd.Series([0, 1, 2], index=["a", "b", "b"])

In [4]: s1.reindex(["a", "b", "c"])
---------------------------------------------------------------------------
ValueError  Traceback (most recent call last)
Cell In[4], line 1
----> 1 s1.reindex(["a", "b", "c"])

File ~/work/pandas/pandas/pandas/core/series.py:5153, in Series.reindex(self, index, axis, method, copy, level, fill_value, limit, tolerance)
  5136 @doc(
  5137     NDFrame.reindex,  # type: ignore[has-type]
  5138     klass=_shared_doc_kwargs["klass"],
   (...)
  5151     tolerance=None,
  5152 ) -> Series:
-> 5153     return super().reindex(
  5154         index=index,
  5155         method=method,
  5156         copy=copy,
  5157         level=level,
  5158         fill_value=fill_value,
  5159         limit=limit,
  5160         tolerance=tolerance,
  5161     )

File ~/work/pandas/pandas/pandas/core/generic.py:5610, in NDFrame.reindex(self, labels, index, columns, axis, method, copy, level, fill_value, limit, tolerance)
  5607     return self._reindex_multi(axes, copy, fill_value)
  5609 # perform the reindex on the axes
-> 5610 return self._reindex_axes(
  5611     axes, level, limit, tolerance, method, fill_value, copy
  5612 ).__finalize__(self, method="reindex")

File ~/work/pandas/pandas/pandas/core/generic.py:5633, in NDFrame._reindex_axes(self, axes, level, limit, tolerance, method, fill_value, copy)
  5630     continue
  5632 ax = self._get_axis(a)
-> 5633 new_index, indexer = ax.reindex(
  5634     labels, level=level, limit=limit, tolerance=tolerance, method=method
  5635 )
  5637 axis = self._get_axis_number(a)
  5638 obj = obj._reindex_with_indexers(
  5639     {axis: [new_index, indexer]},
  5640     fill_value=fill_value,
  5641     copy=copy,
  5642     allow_dups=False,
  5643 )

File ~/work/pandas/pandas/pandas/core/indexes/base.py:4429, in Index.reindex(self, target, method, level, limit, tolerance)
  4426     raise ValueError("cannot handle a non-unique multi-index!")
  4427 elif not self.is_unique:
  4428     # GH#42568
-> 4429     raise ValueError("cannot reindex on an axis with duplicate labels")
  4430 else:
  4431     indexer, _ = self.get_indexer_non_unique(target)

ValueError: cannot reindex on an axis with duplicate labels 

其他方法,如索引,可能会产生非常令人惊讶的结果。通常使用标量进行索引会降低维度。使用标量切片DataFrame将返回一个Series。使用标量切片Series将返回一个标量。但是对于重复项,情况并非如此。

In [5]: df1 = pd.DataFrame([[0, 1, 2], [3, 4, 5]], columns=["A", "A", "B"])

In [6]: df1
Out[6]: 
 A  A  B
0  0  1  2
1  3  4  5 

我们的列中有重复项。如果我们切片'B',我们会得到一个Series

In [7]: df1["B"]  # a series
Out[7]: 
0    2
1    5
Name: B, dtype: int64 

但是切片'A'返回一个DataFrame

In [8]: df1["A"]  # a DataFrame
Out[8]: 
 A  A
0  0  1
1  3  4 

这也适用于行标签

In [9]: df2 = pd.DataFrame({"A": [0, 1, 2]}, index=["a", "a", "b"])

In [10]: df2
Out[10]: 
 A
a  0
a  1
b  2

In [11]: df2.loc["b", "A"]  # a scalar
Out[11]: 2

In [12]: df2.loc["a", "A"]  # a Series
Out[12]: 
a    0
a    1
Name: A, dtype: int64 

重复标签检测

您可以使用Index.is_unique检查Index(存储行或列标签)是否唯一:

In [13]: df2
Out[13]: 
 A
a  0
a  1
b  2

In [14]: df2.index.is_unique
Out[14]: False

In [15]: df2.columns.is_unique
Out[15]: True 

注意

检查索引是否唯一对于大型数据集来说有点昂贵。pandas 会缓存此结果,因此在相同的索引上重新检查非常快。

Index.duplicated()将返回一个布尔数组,指示标签是否重复。

In [16]: df2.index.duplicated()
Out[16]: array([False,  True, False]) 

可以用作布尔过滤器来删除重复行。

In [17]: df2.loc[~df2.index.duplicated(), :]
Out[17]: 
 A
a  0
b  2 

如果您需要额外的逻辑来处理重复标签,而不仅仅是删除重复项,则在索引上使用groupby()是一个常见的技巧。例如,我们将通过取具有相同标签的所有行的平均值来解决重复项。

In [18]: df2.groupby(level=0).mean()
Out[18]: 
 A
a  0.5
b  2.0 

禁止重复标签

版本 1.2.0 中的新功能。

如上所述,在读取原始数据时处理重复项是一个重要的功能。也就是说,您可能希望避免在数据处理管道中引入重复项(从方法如pandas.concat()rename()等)。SeriesDataFrame通过调用.set_flags(allows_duplicate_labels=False)禁止重复标签(默认情况下允许)。如果存在重复标签,将引发异常。

In [19]: pd.Series([0, 1, 2], index=["a", "b", "b"]).set_flags(allows_duplicate_labels=False)
---------------------------------------------------------------------------
DuplicateLabelError  Traceback (most recent call last)
Cell In[19], line 1
----> 1 pd.Series([0, 1, 2], index=["a", "b", "b"]).set_flags(allows_duplicate_labels=False)

File ~/work/pandas/pandas/pandas/core/generic.py:508, in NDFrame.set_flags(self, copy, allows_duplicate_labels)
  506 df = self.copy(deep=copy and not using_copy_on_write())
  507 if allows_duplicate_labels is not None:
--> 508     df.flags["allows_duplicate_labels"] = allows_duplicate_labels
  509 return df

File ~/work/pandas/pandas/pandas/core/flags.py:109, in Flags.__setitem__(self, key, value)
  107 if key not in self._keys:
  108     raise ValueError(f"Unknown flag {key}. Must be one of {self._keys}")
--> 109 setattr(self, key, value)

File ~/work/pandas/pandas/pandas/core/flags.py:96, in Flags.allows_duplicate_labels(self, value)
  94 if not value:
  95     for ax in obj.axes:
---> 96         ax._maybe_check_unique()
  98 self._allows_duplicate_labels = value

File ~/work/pandas/pandas/pandas/core/indexes/base.py:715, in Index._maybe_check_unique(self)
  712 duplicates = self._format_duplicate_message()
  713 msg += f"\n{duplicates}"
--> 715 raise DuplicateLabelError(msg)

DuplicateLabelError: Index has duplicates.
      positions
label          
b        [1, 2] 

这适用于DataFrame的行和列标签

In [20]: pd.DataFrame([[0, 1, 2], [3, 4, 5]], columns=["A", "B", "C"],).set_flags(
 ....:    allows_duplicate_labels=False
 ....: )
 ....: 
Out[20]: 
 A  B  C
0  0  1  2
1  3  4  5 

可以使用allows_duplicate_labels来检查或设置此属性,该属性指示该对象是否可以具有重复标签。

In [21]: df = pd.DataFrame({"A": [0, 1, 2, 3]}, index=["x", "y", "X", "Y"]).set_flags(
 ....:    allows_duplicate_labels=False
 ....: )
 ....: 

In [22]: df
Out[22]: 
 A
x  0
y  1
X  2
Y  3

In [23]: df.flags.allows_duplicate_labels
Out[23]: False 

DataFrame.set_flags()可用于返回一个新的DataFrame,其中包含allows_duplicate_labels等属性设置为某个值

In [24]: df2 = df.set_flags(allows_duplicate_labels=True)

In [25]: df2.flags.allows_duplicate_labels
Out[25]: True 

返回的新DataFrame是对旧DataFrame上相同数据的视图。或者该属性可以直接设置在同一对象上。

In [26]: df2.flags.allows_duplicate_labels = False

In [27]: df2.flags.allows_duplicate_labels
Out[27]: False 

在处理原始杂乱数据时,您可能首先会读取杂乱数据(其中可能存在重复标签),然后去重,并且在之后禁止重复,以确保您的数据流水线不会引入重复。

>>> raw = pd.read_csv("...")
>>> deduplicated = raw.groupby(level=0).first()  # remove duplicates
>>> deduplicated.flags.allows_duplicate_labels = False  # disallow going forward 

在具有重复标签的SeriesDataFrame上设置allows_duplicate_labels=False,或执行引入重复标签的操作,会导致引发errors.DuplicateLabelError

In [28]: df.rename(str.upper)
---------------------------------------------------------------------------
DuplicateLabelError  Traceback (most recent call last)
Cell In[28], line 1
----> 1 df.rename(str.upper)

File ~/work/pandas/pandas/pandas/core/frame.py:5767, in DataFrame.rename(self, mapper, index, columns, axis, copy, inplace, level, errors)
  5636 def rename(
  5637     self,
  5638     mapper: Renamer | None = None,
   (...)
  5646     errors: IgnoreRaise = "ignore",
  5647 ) -> DataFrame | None:
  5648  """
  5649 Rename columns or index labels.
  5650  
 (...)
  5765 4  3  6
  5766 """
-> 5767     return super()._rename(
  5768         mapper=mapper,
  5769         index=index,
  5770         columns=columns,
  5771         axis=axis,
  5772         copy=copy,
  5773         inplace=inplace,
  5774         level=level,
  5775         errors=errors,
  5776     )

File ~/work/pandas/pandas/pandas/core/generic.py:1140, in NDFrame._rename(self, mapper, index, columns, axis, copy, inplace, level, errors)
  1138     return None
  1139 else:
-> 1140     return result.__finalize__(self, method="rename")

File ~/work/pandas/pandas/pandas/core/generic.py:6262, in NDFrame.__finalize__(self, other, method, **kwargs)
  6255 if other.attrs:
  6256     # We want attrs propagation to have minimal performance
  6257     # impact if attrs are not used; i.e. attrs is an empty dict.
  6258     # One could make the deepcopy unconditionally, but a deepcopy
  6259     # of an empty dict is 50x more expensive than the empty check.
  6260     self.attrs = deepcopy(other.attrs)
-> 6262 self.flags.allows_duplicate_labels = other.flags.allows_duplicate_labels
  6263 # For subclasses using _metadata.
  6264 for name in set(self._metadata) & set(other._metadata):

File ~/work/pandas/pandas/pandas/core/flags.py:96, in Flags.allows_duplicate_labels(self, value)
  94 if not value:
  95     for ax in obj.axes:
---> 96         ax._maybe_check_unique()
  98 self._allows_duplicate_labels = value

File ~/work/pandas/pandas/pandas/core/indexes/base.py:715, in Index._maybe_check_unique(self)
  712 duplicates = self._format_duplicate_message()
  713 msg += f"\n{duplicates}"
--> 715 raise DuplicateLabelError(msg)

DuplicateLabelError: Index has duplicates.
      positions
label          
X        [0, 2]
Y        [1, 3] 

此错误消息包含重复的标签,以及SeriesDataFrame中所有重复项(包括“原始”)的数字位置

重复标签传播

一般来说,不允许重复是“粘性的”。它会通过操作保留下来。

In [29]: s1 = pd.Series(0, index=["a", "b"]).set_flags(allows_duplicate_labels=False)

In [30]: s1
Out[30]: 
a    0
b    0
dtype: int64

In [31]: s1.head().rename({"a": "b"})
---------------------------------------------------------------------------
DuplicateLabelError  Traceback (most recent call last)
Cell In[31], line 1
----> 1 s1.head().rename({"a": "b"})

File ~/work/pandas/pandas/pandas/core/series.py:5090, in Series.rename(self, index, axis, copy, inplace, level, errors)
  5083     axis = self._get_axis_number(axis)
  5085 if callable(index) or is_dict_like(index):
  5086     # error: Argument 1 to "_rename" of "NDFrame" has incompatible
  5087     # type "Union[Union[Mapping[Any, Hashable], Callable[[Any],
  5088     # Hashable]], Hashable, None]"; expected "Union[Mapping[Any,
  5089     # Hashable], Callable[[Any], Hashable], None]"
-> 5090     return super()._rename(
  5091         index,  # type: ignore[arg-type]
  5092         copy=copy,
  5093         inplace=inplace,
  5094         level=level,
  5095         errors=errors,
  5096     )
  5097 else:
  5098     return self._set_name(index, inplace=inplace, deep=copy)

File ~/work/pandas/pandas/pandas/core/generic.py:1140, in NDFrame._rename(self, mapper, index, columns, axis, copy, inplace, level, errors)
  1138     return None
  1139 else:
-> 1140     return result.__finalize__(self, method="rename")

File ~/work/pandas/pandas/pandas/core/generic.py:6262, in NDFrame.__finalize__(self, other, method, **kwargs)
  6255 if other.attrs:
  6256     # We want attrs propagation to have minimal performance
  6257     # impact if attrs are not used; i.e. attrs is an empty dict.
  6258     # One could make the deepcopy unconditionally, but a deepcopy
  6259     # of an empty dict is 50x more expensive than the empty check.
  6260     self.attrs = deepcopy(other.attrs)
-> 6262 self.flags.allows_duplicate_labels = other.flags.allows_duplicate_labels
  6263 # For subclasses using _metadata.
  6264 for name in set(self._metadata) & set(other._metadata):

File ~/work/pandas/pandas/pandas/core/flags.py:96, in Flags.allows_duplicate_labels(self, value)
  94 if not value:
  95     for ax in obj.axes:
---> 96         ax._maybe_check_unique()
  98 self._allows_duplicate_labels = value

File ~/work/pandas/pandas/pandas/core/indexes/base.py:715, in Index._maybe_check_unique(self)
  712 duplicates = self._format_duplicate_message()
  713 msg += f"\n{duplicates}"
--> 715 raise DuplicateLabelError(msg)

DuplicateLabelError: Index has duplicates.
      positions
label          
b        [0, 1] 

警告

这是一个实验性功能。目前,许多方法未能传播allows_duplicate_labels的值。未来版本预计每个接受或返回一个或多个 DataFrame 或 Series 对象的方法都将传播allows_duplicate_labels

重复标签的后果

一些 pandas 方法(例如Series.reindex())在存在重复时无法正常工作。输出结果无法确定,因此 pandas 会报错。

In [3]: s1 = pd.Series([0, 1, 2], index=["a", "b", "b"])

In [4]: s1.reindex(["a", "b", "c"])
---------------------------------------------------------------------------
ValueError  Traceback (most recent call last)
Cell In[4], line 1
----> 1 s1.reindex(["a", "b", "c"])

File ~/work/pandas/pandas/pandas/core/series.py:5153, in Series.reindex(self, index, axis, method, copy, level, fill_value, limit, tolerance)
  5136 @doc(
  5137     NDFrame.reindex,  # type: ignore[has-type]
  5138     klass=_shared_doc_kwargs["klass"],
   (...)
  5151     tolerance=None,
  5152 ) -> Series:
-> 5153     return super().reindex(
  5154         index=index,
  5155         method=method,
  5156         copy=copy,
  5157         level=level,
  5158         fill_value=fill_value,
  5159         limit=limit,
  5160         tolerance=tolerance,
  5161     )

File ~/work/pandas/pandas/pandas/core/generic.py:5610, in NDFrame.reindex(self, labels, index, columns, axis, method, copy, level, fill_value, limit, tolerance)
  5607     return self._reindex_multi(axes, copy, fill_value)
  5609 # perform the reindex on the axes
-> 5610 return self._reindex_axes(
  5611     axes, level, limit, tolerance, method, fill_value, copy
  5612 ).__finalize__(self, method="reindex")

File ~/work/pandas/pandas/pandas/core/generic.py:5633, in NDFrame._reindex_axes(self, axes, level, limit, tolerance, method, fill_value, copy)
  5630     continue
  5632 ax = self._get_axis(a)
-> 5633 new_index, indexer = ax.reindex(
  5634     labels, level=level, limit=limit, tolerance=tolerance, method=method
  5635 )
  5637 axis = self._get_axis_number(a)
  5638 obj = obj._reindex_with_indexers(
  5639     {axis: [new_index, indexer]},
  5640     fill_value=fill_value,
  5641     copy=copy,
  5642     allow_dups=False,
  5643 )

File ~/work/pandas/pandas/pandas/core/indexes/base.py:4429, in Index.reindex(self, target, method, level, limit, tolerance)
  4426     raise ValueError("cannot handle a non-unique multi-index!")
  4427 elif not self.is_unique:
  4428     # GH#42568
-> 4429     raise ValueError("cannot reindex on an axis with duplicate labels")
  4430 else:
  4431     indexer, _ = self.get_indexer_non_unique(target)

ValueError: cannot reindex on an axis with duplicate labels 

其他方法,如索引,可能会产生非常奇怪的结果。通常使用标量进行索引将减少维度。使用标量对DataFrame进行切片将返回一个Series。使用标量对Series进行切片将返回一个标量。但是对于重复项,情况并非如此。

In [5]: df1 = pd.DataFrame([[0, 1, 2], [3, 4, 5]], columns=["A", "A", "B"])

In [6]: df1
Out[6]: 
 A  A  B
0  0  1  2
1  3  4  5 

我们在列中有重复。如果我们切片'B',我们会得到一个Series

In [7]: df1["B"]  # a series
Out[7]: 
0    2
1    5
Name: B, dtype: int64 

但是切片'A'会返回一个DataFrame

In [8]: df1["A"]  # a DataFrame
Out[8]: 
 A  A
0  0  1
1  3  4 

这也适用于行标签

In [9]: df2 = pd.DataFrame({"A": [0, 1, 2]}, index=["a", "a", "b"])

In [10]: df2
Out[10]: 
 A
a  0
a  1
b  2

In [11]: df2.loc["b", "A"]  # a scalar
Out[11]: 2

In [12]: df2.loc["a", "A"]  # a Series
Out[12]: 
a    0
a    1
Name: A, dtype: int64 

重复标签检测

您可以使用Index.is_unique检查Index(存储行或列标签)是否唯一:

In [13]: df2
Out[13]: 
 A
a  0
a  1
b  2

In [14]: df2.index.is_unique
Out[14]: False

In [15]: df2.columns.is_unique
Out[15]: True 

注意

检查索引是否唯一对于大型数据集来说是比较昂贵的。pandas 会缓存此结果,因此在相同的索引上重新检查非常快。

Index.duplicated()会返回一个布尔型 ndarray,指示标签是否重复。

In [16]: df2.index.duplicated()
Out[16]: array([False,  True, False]) 

可以将其用作布尔过滤器以删除重复行。

In [17]: df2.loc[~df2.index.duplicated(), :]
Out[17]: 
 A
a  0
b  2 

如果您需要额外的逻辑来处理重复标签,而不仅仅是删除重复项,则在索引上使用groupby()是一种常见的技巧。例如,我们将通过取具有相同标签的所有行的平均值来解决重复项。

In [18]: df2.groupby(level=0).mean()
Out[18]: 
 A
a  0.5
b  2.0 

不允许重复标签

新版本 1.2.0 中新增。

如上所述,在读取原始数据时处理重复是一个重要功能。也就是说,您可能希望避免在数据处理流水线中引入重复(从方法如pandas.concat()rename()等)。通过调用.set_flags(allows_duplicate_labels=False)SeriesDataFrame不允许重复标签(默认允许)。如果存在重复标签,将引发异常。

In [19]: pd.Series([0, 1, 2], index=["a", "b", "b"]).set_flags(allows_duplicate_labels=False)
---------------------------------------------------------------------------
DuplicateLabelError  Traceback (most recent call last)
Cell In[19], line 1
----> 1 pd.Series([0, 1, 2], index=["a", "b", "b"]).set_flags(allows_duplicate_labels=False)

File ~/work/pandas/pandas/pandas/core/generic.py:508, in NDFrame.set_flags(self, copy, allows_duplicate_labels)
  506 df = self.copy(deep=copy and not using_copy_on_write())
  507 if allows_duplicate_labels is not None:
--> 508     df.flags["allows_duplicate_labels"] = allows_duplicate_labels
  509 return df

File ~/work/pandas/pandas/pandas/core/flags.py:109, in Flags.__setitem__(self, key, value)
  107 if key not in self._keys:
  108     raise ValueError(f"Unknown flag {key}. Must be one of {self._keys}")
--> 109 setattr(self, key, value)

File ~/work/pandas/pandas/pandas/core/flags.py:96, in Flags.allows_duplicate_labels(self, value)
  94 if not value:
  95     for ax in obj.axes:
---> 96         ax._maybe_check_unique()
  98 self._allows_duplicate_labels = value

File ~/work/pandas/pandas/pandas/core/indexes/base.py:715, in Index._maybe_check_unique(self)
  712 duplicates = self._format_duplicate_message()
  713 msg += f"\n{duplicates}"
--> 715 raise DuplicateLabelError(msg)

DuplicateLabelError: Index has duplicates.
      positions
label          
b        [1, 2] 

这适用于DataFrame的行标签和列标签。

In [20]: pd.DataFrame([[0, 1, 2], [3, 4, 5]], columns=["A", "B", "C"],).set_flags(
 ....:    allows_duplicate_labels=False
 ....: )
 ....: 
Out[20]: 
 A  B  C
0  0  1  2
1  3  4  5 

可以使用allows_duplicate_labels来检查或设置此属性,该属性指示该对象是否可以具有重复标签。

In [21]: df = pd.DataFrame({"A": [0, 1, 2, 3]}, index=["x", "y", "X", "Y"]).set_flags(
 ....:    allows_duplicate_labels=False
 ....: )
 ....: 

In [22]: df
Out[22]: 
 A
x  0
y  1
X  2
Y  3

In [23]: df.flags.allows_duplicate_labels
Out[23]: False 

DataFrame.set_flags()可用于返回一个新的DataFrame,其中属性如allows_duplicate_labels设置为某个值。

In [24]: df2 = df.set_flags(allows_duplicate_labels=True)

In [25]: df2.flags.allows_duplicate_labels
Out[25]: True 

返回的新DataFrame是与旧DataFrame相同数据的视图。或者该属性可以直接设置在同一对象上。

In [26]: df2.flags.allows_duplicate_labels = False

In [27]: df2.flags.allows_duplicate_labels
Out[27]: False 

在处理原始混乱数据时,您可能首先读取混乱数据(可能具有重复标签),去重,然后禁止未来出现重复,以确保您的数据流水线不会引入重复。

>>> raw = pd.read_csv("...")
>>> deduplicated = raw.groupby(level=0).first()  # remove duplicates
>>> deduplicated.flags.allows_duplicate_labels = False  # disallow going forward 

设置allows_duplicate_labels=False在具有重复标签的SeriesDataFrame上,或者在SeriesDataFrame上执行引入重复标签的操作,而该SeriesDataFrame不允许重复标签时,将引发errors.DuplicateLabelError

In [28]: df.rename(str.upper)
---------------------------------------------------------------------------
DuplicateLabelError  Traceback (most recent call last)
Cell In[28], line 1
----> 1 df.rename(str.upper)

File ~/work/pandas/pandas/pandas/core/frame.py:5767, in DataFrame.rename(self, mapper, index, columns, axis, copy, inplace, level, errors)
  5636 def rename(
  5637     self,
  5638     mapper: Renamer | None = None,
   (...)
  5646     errors: IgnoreRaise = "ignore",
  5647 ) -> DataFrame | None:
  5648  """
  5649 Rename columns or index labels.
  5650  
 (...)
  5765 4  3  6
  5766 """
-> 5767     return super()._rename(
  5768         mapper=mapper,
  5769         index=index,
  5770         columns=columns,
  5771         axis=axis,
  5772         copy=copy,
  5773         inplace=inplace,
  5774         level=level,
  5775         errors=errors,
  5776     )

File ~/work/pandas/pandas/pandas/core/generic.py:1140, in NDFrame._rename(self, mapper, index, columns, axis, copy, inplace, level, errors)
  1138     return None
  1139 else:
-> 1140     return result.__finalize__(self, method="rename")

File ~/work/pandas/pandas/pandas/core/generic.py:6262, in NDFrame.__finalize__(self, other, method, **kwargs)
  6255 if other.attrs:
  6256     # We want attrs propagation to have minimal performance
  6257     # impact if attrs are not used; i.e. attrs is an empty dict.
  6258     # One could make the deepcopy unconditionally, but a deepcopy
  6259     # of an empty dict is 50x more expensive than the empty check.
  6260     self.attrs = deepcopy(other.attrs)
-> 6262 self.flags.allows_duplicate_labels = other.flags.allows_duplicate_labels
  6263 # For subclasses using _metadata.
  6264 for name in set(self._metadata) & set(other._metadata):

File ~/work/pandas/pandas/pandas/core/flags.py:96, in Flags.allows_duplicate_labels(self, value)
  94 if not value:
  95     for ax in obj.axes:
---> 96         ax._maybe_check_unique()
  98 self._allows_duplicate_labels = value

File ~/work/pandas/pandas/pandas/core/indexes/base.py:715, in Index._maybe_check_unique(self)
  712 duplicates = self._format_duplicate_message()
  713 msg += f"\n{duplicates}"
--> 715 raise DuplicateLabelError(msg)

DuplicateLabelError: Index has duplicates.
      positions
label          
X        [0, 2]
Y        [1, 3] 

此错误消息包含重复的标签以及所有重复项(包括“原始”)在SeriesDataFrame中的数值位置。

重复标签传播

一般来说,禁止重复是“粘性”的。它会通过操作保留下来。

In [29]: s1 = pd.Series(0, index=["a", "b"]).set_flags(allows_duplicate_labels=False)

In [30]: s1
Out[30]: 
a    0
b    0
dtype: int64

In [31]: s1.head().rename({"a": "b"})
---------------------------------------------------------------------------
DuplicateLabelError  Traceback (most recent call last)
Cell In[31], line 1
----> 1 s1.head().rename({"a": "b"})

File ~/work/pandas/pandas/pandas/core/series.py:5090, in Series.rename(self, index, axis, copy, inplace, level, errors)
  5083     axis = self._get_axis_number(axis)
  5085 if callable(index) or is_dict_like(index):
  5086     # error: Argument 1 to "_rename" of "NDFrame" has incompatible
  5087     # type "Union[Union[Mapping[Any, Hashable], Callable[[Any],
  5088     # Hashable]], Hashable, None]"; expected "Union[Mapping[Any,
  5089     # Hashable], Callable[[Any], Hashable], None]"
-> 5090     return super()._rename(
  5091         index,  # type: ignore[arg-type]
  5092         copy=copy,
  5093         inplace=inplace,
  5094         level=level,
  5095         errors=errors,
  5096     )
  5097 else:
  5098     return self._set_name(index, inplace=inplace, deep=copy)

File ~/work/pandas/pandas/pandas/core/generic.py:1140, in NDFrame._rename(self, mapper, index, columns, axis, copy, inplace, level, errors)
  1138     return None
  1139 else:
-> 1140     return result.__finalize__(self, method="rename")

File ~/work/pandas/pandas/pandas/core/generic.py:6262, in NDFrame.__finalize__(self, other, method, **kwargs)
  6255 if other.attrs:
  6256     # We want attrs propagation to have minimal performance
  6257     # impact if attrs are not used; i.e. attrs is an empty dict.
  6258     # One could make the deepcopy unconditionally, but a deepcopy
  6259     # of an empty dict is 50x more expensive than the empty check.
  6260     self.attrs = deepcopy(other.attrs)
-> 6262 self.flags.allows_duplicate_labels = other.flags.allows_duplicate_labels
  6263 # For subclasses using _metadata.
  6264 for name in set(self._metadata) & set(other._metadata):

File ~/work/pandas/pandas/pandas/core/flags.py:96, in Flags.allows_duplicate_labels(self, value)
  94 if not value:
  95     for ax in obj.axes:
---> 96         ax._maybe_check_unique()
  98 self._allows_duplicate_labels = value

File ~/work/pandas/pandas/pandas/core/indexes/base.py:715, in Index._maybe_check_unique(self)
  712 duplicates = self._format_duplicate_message()
  713 msg += f"\n{duplicates}"
--> 715 raise DuplicateLabelError(msg)

DuplicateLabelError: Index has duplicates.
      positions
label          
b        [0, 1] 

警告

这是一个实验性功能。目前,许多方法未能传播allows_duplicate_labels值。在未来版本中,预计每个接受或返回一个或多个 DataFrame 或 Series 对象的方法将传播allows_duplicate_labels

重复标签传播

一般来说,禁止重复是“粘性”的。它会通过操作保留下来。

In [29]: s1 = pd.Series(0, index=["a", "b"]).set_flags(allows_duplicate_labels=False)

In [30]: s1
Out[30]: 
a    0
b    0
dtype: int64

In [31]: s1.head().rename({"a": "b"})
---------------------------------------------------------------------------
DuplicateLabelError  Traceback (most recent call last)
Cell In[31], line 1
----> 1 s1.head().rename({"a": "b"})

File ~/work/pandas/pandas/pandas/core/series.py:5090, in Series.rename(self, index, axis, copy, inplace, level, errors)
  5083     axis = self._get_axis_number(axis)
  5085 if callable(index) or is_dict_like(index):
  5086     # error: Argument 1 to "_rename" of "NDFrame" has incompatible
  5087     # type "Union[Union[Mapping[Any, Hashable], Callable[[Any],
  5088     # Hashable]], Hashable, None]"; expected "Union[Mapping[Any,
  5089     # Hashable], Callable[[Any], Hashable], None]"
-> 5090     return super()._rename(
  5091         index,  # type: ignore[arg-type]
  5092         copy=copy,
  5093         inplace=inplace,
  5094         level=level,
  5095         errors=errors,
  5096     )
  5097 else:
  5098     return self._set_name(index, inplace=inplace, deep=copy)

File ~/work/pandas/pandas/pandas/core/generic.py:1140, in NDFrame._rename(self, mapper, index, columns, axis, copy, inplace, level, errors)
  1138     return None
  1139 else:
-> 1140     return result.__finalize__(self, method="rename")

File ~/work/pandas/pandas/pandas/core/generic.py:6262, in NDFrame.__finalize__(self, other, method, **kwargs)
  6255 if other.attrs:
  6256     # We want attrs propagation to have minimal performance
  6257     # impact if attrs are not used; i.e. attrs is an empty dict.
  6258     # One could make the deepcopy unconditionally, but a deepcopy
  6259     # of an empty dict is 50x more expensive than the empty check.
  6260     self.attrs = deepcopy(other.attrs)
-> 6262 self.flags.allows_duplicate_labels = other.flags.allows_duplicate_labels
  6263 # For subclasses using _metadata.
  6264 for name in set(self._metadata) & set(other._metadata):

File ~/work/pandas/pandas/pandas/core/flags.py:96, in Flags.allows_duplicate_labels(self, value)
  94 if not value:
  95     for ax in obj.axes:
---> 96         ax._maybe_check_unique()
  98 self._allows_duplicate_labels = value

File ~/work/pandas/pandas/pandas/core/indexes/base.py:715, in Index._maybe_check_unique(self)
  712 duplicates = self._format_duplicate_message()
  713 msg += f"\n{duplicates}"
--> 715 raise DuplicateLabelError(msg)

DuplicateLabelError: Index has duplicates.
      positions
label          
b        [0, 1] 

警告

这是一个实验性功能。目前,许多方法未能传播allows_duplicate_labels值。在未来版本中,预计每个接受或返回一个或多个 DataFrame 或 Series 对象的方法将传播allows_duplicate_labels

分类数据

原文:pandas.pydata.org/docs/user_guide/categorical.html

这是关于 pandas 分类数据类型的介绍,包括与 R 的factor的简短比较。

Categoricals是一种与统计学中的分类变量对应的 pandas 数据类型。分类变量只能取有限且通常固定的可能值(categories;在 R 中称为levels)。例如性别、社会阶层、血型、国家隶属、观察时间或通过 Likert 量表进行评分等。

与统计学中的分类变量相反,分类数据可能具有顺序(例如‘强烈同意’与‘同意’或‘第一次观察’与‘第二次观察’),但不支持数值运算(加法、除法等)。

分类数据的所有值都在categoriesnp.nan中。顺序由categories的顺序而不是值的词法顺序定义。在内部,数据结构由一个categories数组和一个指向categories数组中实际值的整数数组codes组成。

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

  • 由仅包含几个不同值的字符串变量组成。将这样的字符串变量转换为分类变量将节省一些内存,参见这里。

  • 变量的词法顺序与逻辑顺序(“one”、“two”、“three”)不同。通过转换为分类变量并在类别上指定顺序,排序和最小/最大值将使用逻辑顺序而不是词法顺序,参见这里。

  • 作为向其他 Python 库发出信号的方式,表明该列应被视为分类变量(例如使用适当的统计方法或绘图类型)。

另请参阅 categoricals 的 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, object): ['a', 'b', 'c'] 

通过将现有的Series或列转换为category数据类型:

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

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

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

通过使用特殊函数,例如cut(),将数据分组为离散的箱。请参阅文档中有关切片的示例。

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

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

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

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

通过将pandas.Categorical对象传递给Series或将其分配给DataFrame

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

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

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

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

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

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

分类数据具有特定的category dtype:

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

DataFrame 创建

类似于前一节中将单个列转换为分类变量的情况,DataFrame中的所有列都可以在构建期间或构建后批量转换为分类变量。

可以在构建期间通过在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',我们使用了默认行为:

  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 = pd.unique(df.to_numpy().ravel())来以编程方式确定categories参数。

如果你已经有了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: 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可以在任何需要dtype的地方使用。例如pandas.read_csv()pandas.DataFrame.astype(),或者在Series构造函数中。

注意

作为一种便利,当你希望类别的默认行为是无序的,并且等于数组中存在的集合值时,可以在CategoricalDtype的位置使用字符串'category'。换句话说,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()将产生类似于string类型的SeriesDataFrame的输出。

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 

使用类别

分类数据具有categoriesordered属性,列出了它们可能的值以及排序是否重要。这些属性被公开为s.cat.categoriess.cat.ordered。如果您不手动指定类别和排序,它们将从传递的参数中推断出来。

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

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

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

也可以按特定顺序传递类别:

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

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

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

注意

新的分类数据不会自动排序。您必须显式传递ordered=True来指示有序的Categorical

注意

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

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

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

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

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

重命名类别

通过使用rename_categories()方法来重命名类别:

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

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

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

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

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

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

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

注意

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

类别必须是唯一的,否则会引发ValueError

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

类别也不能是NaN,否则会引���ValueError

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

追加新类别

可以通过使用add_categories()方法来追加类别:

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

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

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

删除类别

通过使用remove_categories()方法可以删除类别。被删除的值将被np.nan替换。

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

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

删除未使用的类别

也可以删除未使用的类别:

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

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

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

设置类别

如果您想要一次性执行删除和添加新类别的操作(这样做有一定的速度优势),或者简单地将类别设置为预定义的规模,请使用set_categories()

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

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

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

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

注意

请注意,Categorical.set_categories()无法知道某个类别是有意省略的还是因为拼写错误或(在 Python3 下)由于类型差异(例如,NumPy S1 dtype 和 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

多列排序

分类dtyped列将以与其他列类似的方式参与多列排序。分类的排序由该列的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、数组等)进行相等比较(==!=),长度与分类数据相同。

  • 所有与另一个分类系列的比较(==!=>>=<<=),当ordered==Truecategories相同时。

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

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

注意

对分类数据与Seriesnp.arraylist或具有不同类别或排序的分类数据的任何“非相等”比较都会引发TypeError,因为自定义类别排序可能会被解释为两种方式:一种考虑排序,一种不考虑排序。

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

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

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

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

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

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

与具有相同类别和顺序的分类或标量进行比较有效:

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

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

相等比较适用于任何长度相同的类列表对象和标量:

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

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

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

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

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

如果要对非分类数据进行“非相等”比较,需要明确地将分类数据转换回原始值:

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

In [125]: try:
 .....:    cat > base
 .....: except TypeError as e:
 .....:    print("TypeError:", str(e))
 .....: 
TypeError: Cannot compare a Categorical for op __gt__ with type <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 

Groupby 在 observed=False 时也会显示“未使用”的类别:

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

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

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

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

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

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

透视表:

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

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

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

数据整理

优化的 pandas 数据访问方法 .loc, .iloc, .at, 和 .iat 的工作方式与正常情况下相同。唯一的区别在于返回类型(用于获取)以及只有已在 categories 中的值才能被赋值。

获取

如果切片操作返回 DataFrameSeries 类型的列,则 category dtype 会被保留。

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

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

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

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

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

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

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

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

类别类型未保留的一个例子是如果您取一行:结果的 Series 的 dtype 是 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

要获得类型为 category 的单个值 Series,您需要传入一个包含单个值的列表:

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

字符串和日期时间访问器

如果 s.cat.categories 是适当类型,则访问器 .dt.str 将起作用:

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)与在该类型的 Series 上使用 .str.<method> / .dt.<method> 时的类型相同(而不是 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 

合并/连接

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

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

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

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

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

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

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

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

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

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

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

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

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

arg1 arg2 相同 结果
类别 类别 True 类别
类别(object) 类别(object) False object(推断出的 dtype)

| 类别(int) | 类别(float) | False | float(推断出的 dtype) | ### 合并

如果要合并不一定具有相同类别的分类,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 dtypes 的数据写入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 数据库时也适用于相同规则。

缺失数据

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 的 factor 函数的差异可以观察到:

  • R 的 levels 被命名为 categories

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

  • 不可能在创建时指定标签。之后使用 s.cat.rename_categories(new_labels)。在创建时指定标签。之后使用 s.cat.rename_categories(new_labels)

  • 与 R 的 factor 函数相反,将分类数据作为唯一输入来创建新的分类系列 不会 删除未使用的类别,而是创建一个与传入的相等的新分类系列!

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

注意事项

内存使用

Categorical 的内存使用量与类别数量加上数据长度成正比。相比之下,object dtype 是数据长度的常数倍。

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

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

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

注意

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

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 数组 dtype。这会导致一些问题。

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 

Dtype 比较有效:

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 

在类型为 categorySeries 上使��� NumPy 函数应该不起作用,因为 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 提交 bug!

在 apply 中的 dtype

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

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

In [258]: df.apply(lambda row: type(row["cats"]), axis=1)
Out[258]: 
0    <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 

副作用

Categorical构造Series不会复制输入的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"]))则不会。

对象创建

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

通过将现有的Series或列转换为category dtype:

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

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

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

通过使用特殊函数,例如cut(),将数据分组为离散的箱。请参阅文档中有关切片的示例。

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

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

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

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

通过将pandas.Categorical对象传递给Series或将其分配给DataFrame

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

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

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

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

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

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

分类数据具有特定的category dtype:

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

DataFrame 创建

类似于前一节中将单个列转换为分类的情况,可以在构建过程中或之后将DataFrame中的所有列批量转换为分类。

这可以在构造过程中通过在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'时,使用了默认行为:

  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'] 

同样,可以使用CategoricalDtypeDataFrame一起使用,以确保所有列中的类别保持一致。

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参数。

如果您已经有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: 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来更改类别。

系列创建

可以通过几种方式创建DataFrame中的分类Series或列:

在构造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 dtype:

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',我们使用了默认行为:

  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'] 

同样,可以在DataFrame中使用CategoricalDtype来确保所有列之间的类别保持一致。

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参数。

如果你已经有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: 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 

相等语义

两个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()将产生类似于string类型的SeriesDataFrame的输出。

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

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

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

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

使用类别

分类数据有一个categories和一个ordered属性,列出了它们可能的值以及排序是否重要。这些属性暴露为s.cat.categoriess.cat.ordered。如果您不手动指定类别和排序,它们将从传递的参数中推断出来。

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

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

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

可以按特定顺序传递类别:

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

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

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

注意

新的分类数据不会自动排序。您必须显式传递ordered=True来指示有序的Categorical

注意

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

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

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

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

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

重命名类别

通过使用rename_categories()方法来重命名类别:

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

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

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

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

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

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

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

注意

与 R 的factor相反,分类数据可以具有其他类型的类别而不仅仅是字符串。

类别必须是唯一的,否则会引发ValueError

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

类别也不能是NaN,否则会引发ValueError

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

添加新类别

可以使用add_categories()方法添加类别:

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

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

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

删除类别

可以使用remove_categories()方法删除类别。被删除的值将被np.nan替换。

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

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

删除未使用的类别

也可以删除未使用的类别:

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

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

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

设置类别

如果您想要一次性删除并添加新类别(这样有一定的速度优势),或者简单地将类别设置为预定义的范围,请使用set_categories()

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

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

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

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

注意

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

重命名类别

通过使用rename_categories()方法来重命名类别:

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

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

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

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

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

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

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

注意

与 R 的factor相反,分类数据可以具有其他类型的类别而不仅仅是字符串。

类别必须是唯一的,否则会引发ValueError

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

类别也不能是NaN,否则会引发ValueError

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

添加新类别

可以使用add_categories()方法添加类别:

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

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

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

删除类别

可以使用remove_categories()方法删除类别。被删除的值将被np.nan替换。

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

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

删除未使用的类别

也可以删除未使用的类别:

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

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

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

设置类别

如果您想要一次性删除并添加新类别(这样有一定的速度优势),或者简单地将类别设置为预定义的范围,请使用set_categories()

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

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

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

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

注意

请注意,Categorical.set_categories()无法知道某个类别是有意省略还是因为拼写错误或(在 Python3 下)由于类型差异(例如,NumPy S1 dtype 和 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'] 

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

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 

重新排序

通过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、数组等)进行比较。

  • 所有对另一个分类系列进行比较(==!=>>=<<=),当ordered==Truecategories相同时。

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

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

注意

任何对分类数据与Seriesnp.arraylist或具有不同类别或排序的分类数据进行“非相等”比较都会引发TypeError,因为自定义类别排序可能会被解释为两种方式:一种考虑排序,一种不考虑。

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

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

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

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

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

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

与具有相同类别和排序或标量的分类进行比较有效:

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

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

相等比较适用于任何长度相同的类似列表对象和标量:

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

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

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

这不起作用,因为类别不同:

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

如果要对分类系列与非分类数据的类似列表对象进行“非相等”比较,需要明确并将分类数据转换回原始值:

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

In [125]: try:
 .....:    cat > base
 .....: except TypeError as e:
 .....:    print("TypeError:", str(e))
 .....: 
TypeError: Cannot compare a Categorical for op __gt__ with type <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时,Groupby 也会显示“未使用”的类别:

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

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

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

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

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

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

透视表:

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

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

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

数据整理

优化过的 pandas 数据访问方法.loc.iloc.at.iat的工作方式与正常情况下相同。唯一的区别是返回类型(用于获取)和只有已在categories中的值才能被赋值。

获取

如果切片操作返回DataFrame或类型为Series的列,则category dtype 将被保留。

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

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

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

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

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

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

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

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

如果您只取一行,则类别类型不会被保留的示例:结果的Series的 dtype 为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

要获取类型为category的单个值Series,您需要传入一个包含单个值的列表:

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

字符串和日期时间访问器

如果s.cat.categories的访问器.dt.str是适当类型,则会起作用:

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)与如果您在该类型的Series上使用.str.<method> / .dt.<method>时的类型相同(而不是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 

合并/连接

默认情况下,合并包含相同类别的SeriesDataFrames将导致category类型,否则结果将取决于底层类别的类型。导致非分类类型的合并可能会导致更高的内存使用量。使用.astypeunion_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 相同 结果
category category True category
category (object) category (object) False object (dtype is inferred)

| category (int) | category (float) | False | float (dtype is inferred) | ### Unioning

如果要合并不一定具有相同类别的分类变量,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) 

获取

如果切片操作返回DataFrameSeries类型的列,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

要获得类型为category的单一值Series,您可以传入一个只有一个值的列表:

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

字符串和日期时间访问器

如果s.cat.categories的类型适当,访问器.dt.str将起作用:

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)与在该类型的Series上使用.str.<method> / .dt.<method>时的类型相同(而不是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 

合并/连接

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

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

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

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

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

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

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

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

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

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

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

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

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

arg1 arg2 相同 结果
类别 类别 True 类别
类别(object) 类别(object) False object(数据类型被推断)
类别(int) 类别(float) False float(数据类型被推断)

联合

如果要组合不一定具有相同类别的分类变量,则union_categoricals() 函数将组合一个类别列表。新的类别将是被组合的类别的并集。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

注意

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

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

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

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

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

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

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

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

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

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

数据的读取/写入

您可以将包含category dtypes 的数据写入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'] 

使用to_sql将数据写入 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 的levels被命名为categories

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

  • 不可能在创建时指定标签。之后使用s.cat.rename_categories(new_labels)

  • 与 R 的factor函数相反,将分类数据作为创建新分类系列的唯一输入将不会删除未使用的类别,而是创建一个等于传入的新分类系列!

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

注意事项

内存使用

Categorical的内存使用量与类别数和数据长度成正比。相比之下,object dtype 是数据长度的常数倍。

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

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

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

注意

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

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 数组 dtype。这会导致一些问题。

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 

Dtype 比较有效:

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 

在类型为 categorySeries 上使用 NumPy 函数应该不起作用,因为 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 提交 bug!

apply 中的 dtype

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

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

In [258]: df.apply(lambda row: type(row["cats"]), axis=1)
Out[258]: 
0    <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 

副作用

Categorical 构建 Series 不会复制输入的 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"]))则不会。

内存使用

Categorical 的内存使用量与类别数量加上数据长度成正比。相比之下,object dtype 是数据长度的常数倍。

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

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

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

注意

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

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 数组 dtype。这会导致一些问题。

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 

Dtype 比较有效:

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 

在类型为 categorySeries 上使用 NumPy 函数应该不起作用,因为 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 提交 bug!

apply 中的 dtype

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

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

In [258]: df.apply(lambda row: type(row["cats"]), axis=1)
Out[258]: 
0    <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 

副作用

Categorical构建Series不会复制输入的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"]))则不会。