数据类型
所有数据处理相关的库类型系统都是重中之重。Pandas 最初的类型系统继承自 numpy,实际上 Series 中的值就是使用 ndarray 来存储的。但是由于 Pandas 中处理的数据要比 numpy 庞杂,因此直接继承 numpy 会有很多问题,其中比较麻烦的就是对缺失值的处理。于是引入了 numpy_nullable 类型,就是在 numpy 的基础上为 Boolean、String 和 Integer 引入了 NA 作为缺失值。尽管则稍微改善了一些性能,但是毕竟还是会有些问题。于是在 Panda 2.0 中引入了 pyarrow 类型系统。他真正的解决了 pandas 中类型系统的一系列问题,最大的问题就是兼容性可能不太好。
数据类型和数据结构
这两个概念非常容易混淆:
- 数据结构: 指数据元素之间的逻辑或物理关系集合,强调数据如何组织和存储。例如,数组、链表等结构通过特定规则管理元素间的关联
- 数据类型: 包含数据值的集合及在该集合上定义的操作(如整数类型的加减运算),他是数据结构和操作的封装
他两个是相辅相成的,数据结构是数据类型的底层支撑,而数据类型是更高层次的抽象。他们的区别主要在于关注点的不同:
- 数据结构: 侧重数据元素的关系(如线性、树形、网状结构)及存储实现(顺序存储、链式存储等),因此更接近于底层数据结构更加重要
- 数据类型: 关注数据的取值范围(如布尔类型只能为 True/False)和合法操作(如整数可进行算术运算),因此在用户层数据类型更加重要
两个概念是有内在联系的,并且不同的数据类型和数据结构的联系也不太一样:
- 基本数据类型: 整数、浮点数、布尔值、字符等,他的数据结构更多的是解决如何存储实现(非常底层)
- 复合数据类型: 将数据结构与操作封装,如队列的入队/出队操作需依赖数组或链表的实现
数据结构是数据的组织骨架,而数据类型是骨架+操作的完整封装。理解两者差异有助于选择合适的数据模型(如选择数组还是哈希表),而联系则体现在实际编程中数据类型的实现离不开底层结构的设计。
在 Pandas 中数据结构和数据类型分装的并没有那么紧密,他在不同的时候使用不同的类型系统:
- 对于标量: 他使用 Python 的类型系统,例如 str、int、float、datetime 等
- 对于向量: 他具有自己的数据结构就是 Series,而Series 是一大堆数据结构的统称,对应到实际就是 IntegerArray、DatetimeArray、StringArray 等数据结构,而 Series.dtype 决定了他的数据类型。因此 Series 可以看作是数据结构和数据类型的结合
- 对于 DataFrame: 这个更特殊,他算是一个容器类数据结构,其中的元素是 Series,他同样有自己的类型这取决于 Series 的类型集合
Pandas 类型系统
Pandas 目前支持三种类型系统,他们的对应关系如下:
pyarrow | pandas扩展(numpy_nullable) | numpy |
---|---|---|
pa.bool_() |
BooleanDtype |
np.bool_ |
pa.int8() |
Int8Dtype |
np.int8 |
pa.int16() |
Int16Dtype |
np.int16 |
pa.int32() |
Int32Dtype |
np.int32 |
pa.int64() |
Int64Dtype |
np.int64 |
pa.uint8() |
UInt8Dtype |
np.uint8 |
pa.uint16() |
UInt16Dtype |
np.uint16 |
pa.uint32() |
UInt32Dtype |
np.uint32 |
pa.uint64() |
UInt64Dtype |
np.uint64 |
pa.float32() |
Float32Dtype |
np.float32 |
pa.float64() |
Float64Dtype |
np.float64 |
pa.time32() |
- | - |
pa.time64() |
- | - |
pa.timestamp() |
DatetimeTZDtype |
datetime64[ns] (不支持时区) |
pa.date32() |
- | - |
pa.date64() |
- | - |
pa.duration() |
- | np.timedelta64 |
pa.binary() |
- | - |
pa.string() |
StringDtype |
np.str_ |
pa.decimal128() |
- | - |
pa.list_() |
- | - |
pa.map_() |
- | - |
pa.dictionary() |
CategoricalDtype |
- |
可以看到 pyarrow 的表现力更好,而之前都是在 numpy 类型系统上的扩展。
类型转换
类型转换分为两种情况:
-
兼容类型的类型转换,这通常并不损失精度,他又分为两种情况,一种是自动转换,一种是手动转换
- Series.astype(): 手动进行转换
Series|Datetime.convert_dtypes(dtype_backend="numpy_nullable"|"pyarrow")
根据所选自动将对应的数据类型(DataFrame 自动构造的是 numpy 所以 convert_dtypes 不需要这种类型)
-
不兼容类型转换,这种通常是将字符串转换为其他类型,或者是丢失精度的转换,目前主要有两个方法实现
- pd.to_numeric: 转换为数值
- pd.to_datetime: 转换为时间类型
astype
Series.astype
用于转换兼容类型,这里比较特殊的是 Pyarrow,他需要使用 pd.ArrowDtype(pa.bool_())
这样的包装方法来包装,当然也可以使用更加简便的字符串表示法 "bool[pyarrow]"
。对于 numpy_nullable 则是 pd.BooleanDtype
或者是取消了 Dtype
后缀且全小写的 boolean
。
to_numeric
pd.to_numeric()将 data 转换为数值类型:
def to_numeric(
arg, # 要转换的数据,可以是标量、列表、元组、Series、ndarray
errors:{"ignore", "raise", "coerce"} = "raise",
# "raise": 无效的解析将引发异常
# "coerce": 无效的解析将设置为 NaN
# "ignore": 无效的解析直接返回
dtype_backend: {"numpy_nullable", "pyarrow"}="numpy_nullable" # 类型系统
):
pass
它用于将其他值转换为数值,主要针对的是字符串,注意他是能够对混用字符串或者数值的序列执行转换的:
to_datetime
to_datetime()将 data 转换为时间类型:
def to_datetime(
arg, # 可以是 int float str datetime 标量,也可以是上面四种类型标量组成的列表、元组、ndarray、Series
errors:{"ignore", "raise", "coerce"} = "raise",
# "raise": 无效的解析将引发异常
# "coerce": 无效的解析将设置为 NaN
# "ignore": 无效的解析直接返回
utc:bool=False,
# 如果为 True 该函数始终返回时区感知的 UTC 时间(无),无时区的会当作本地时间转为 UTC 时间
# 如果为 False,无时区的保持默认
format:str=None,
# 解释时间的 strftime
# 也可以能是 "ISO8601"
# 也可以是 "mixed" 混合多种形式,注意他只针对于字符串形式的
unit:str='ns', # 时间戳(整数、浮点数)的精度
origin='unix', # 整数、浮点数表示的原点,默认是 unix 即以 1970-1-1 为原点,也可以是 datetime 来自定义原点
):
pass
大多数情况下它会自动检测来执行转换,其中:
- 对于整数和浮点数: 会自动被认为是时间戳,此时可以使用 unit 来决定时间戳的符号
- 对于字符串: 会自动认为是时间格式字符串,其中允许出现
["2020/1/1", "2020-10-11", "20201111"]
这样的不同形式,当然不要太过依赖这类自动解析,有可能出问题,如果想要确保一定不出问题就通过 format 来强制指定需要解析的字符串类型
可以混合字符串和数值类型,但是此时就无法指定 unix
、format
这类参数,也就是说他会根据默认的参数来自动检测匹配:
# 时间戳明显是 s 级别的,但是只能使用默认的 ns
pd.to_datetime(pd.Series([1740925298, "2020/1/1", "2020-10-11", "20201111"]))
0 1970-01-01 00:00:01.740925298
1 2020-01-01 00:00:00.000000000
2 2020-10-11 00:00:00.000000000
3 2020-11-11 00:00:00.000000000
dtype: datetime64[ns]