2024年权威测评:Categorical、Date、Time三大类型系统深度对比与精选推荐

2026-05-18阅读 0热度 0
类型系统

在数据处理实践中,类型系统远非一个可被忽视的配角。许多开发者认为只要代码能执行,类型选择无关紧要。然而,精确的类型控制恰恰是构建高效、健壮数据处理管道与编写临时脚本的本质区别。它能带来可量化的性能优势:内存占用可能降低数十倍,计算速度提升数倍,同时增强代码的可读性与可维护性,从而显著减少潜在错误。

本文将深入剖析Polars这一高性能数据处理库的类型系统,展示其如何成为提升数据处理效率的核心工具。

1. Polars支持哪些数据类型?

Polars提供了一套精细且完备的数据类型体系,覆盖从基础到复杂的各类数据场景。你可以通过以下命令快速浏览所有支持的类型:

import polars as pl
# 查看所有数据类型
print(pl.datatypes)

其类型体系涵盖基础数值类型(整数、浮点数)、文本类型、日期时间类型,以及更高级的分类(Categorical)、列表(List)和结构体(Struct)类型。深入理解这些类型是发挥Polars性能潜力的第一步。

2. 创建不同类型的列

在创建DataFrame时,显式指定列的数据类型是标准做法。

整数和浮点数:Polars区分不同位宽的数值类型(如Int8, Int16, Int32, Int64, Float32, Float64),这对于处理大规模数据时的内存优化至关重要。

df = pl.DataFrame({
    "int32_col": [1, 2, 3],
    "int64_col": [1, 2, 3],
    "float32_col": [1.5, 2.5, 3.5],
    "float64_col": [1.5, 2.5, 3.5],
})
# 查看数据类型
print(df.dtypes)
# [Int32, Int64, Float32, Float64]

字符串:字符串默认使用Utf8类型存储。对于重复值较多的列,可考虑使用Categorical类型以优化内存,下文将详细阐述。

df = pl.DataFrame({
    "name": ["张三", "李四", "王五"],  # 默认 Utf8
    "code": pl.Series("code", ["A", "B", "C"], dtype=pl.Categorical)  # 分类
})

日期与时间:Polars对Python原生的日期时间对象有良好的支持,创建时可自动推断类型。

from datetime import date, datetime, time
df = pl.DataFrame({
    "pu re_date": [date(2024, 1, 1), date(2024, 1, 2)],
    "pu re_time": [time(10, 30), time(14, 45)],
    "pu re_datetime": [datetime(2024, 1, 1, 10, 30), datetime(2024, 1, 2, 14, 45)]
})
print(df)

输出结果清晰地展示了不同的日期时间类型:

┌───────────┬──────────┬─────────────────────┐
│ pu re_date ┆ pu re_time ┆ pu re_datetime       │
│ date      ┆ time      ┆ datetime[μs]       │
╞═══════════╪═══════════╪═════════════════════╡
│ 2024-01-01┆ 10:30:00  ┆ 2024-01-01 10:30:00│
│ 2024-01-02┆ 14:45:00  ┆ 2024-01-02 14:45:00│
└───────────┴───────────┴─────────────────────┘

3. 类型转换:cast

数据清洗过程中,类型转换是常规操作。Polars使用cast()方法完成此任务。

df = pl.DataFrame({
    "num_str": ["1", "2", "3"],
    "price": [10.5, 20.3, 30.1]
})
# 字符串转整数
result = df.with_columns(
    pl.col("num_str").cast(pl.Int32).alias("num_int")
)
print(result)

转换后,新列的数据类型变为Int32:

┌─────────┬──────┬──────────┐
│ num_str ┆ price ┆ num_int  │
│ str     ┆ f64   ┆ i32      │
╞═════════╪══════╪══════════╡
│ 1       ┆ 10.5  ┆ 1        │
│ 2       ┆ 20.3  ┆ 2        │
│ 3       ┆ 30.1  ┆ 3        │
└─────────┴───────┴──────────┘

以下是一些典型的数据类型转换场景:

# 字符串 → 整数
pl.col("num_str").cast(pl.Int32)
# 整数 → 浮点
pl.col("num_int").cast(pl.Float64)
# 浮点 → 整数(直接截断)
pl.col("price").cast(pl.Int32)  # 10.5 → 10
# 四舍五入后转整数
pl.col("price").round(0).cast(pl.Int32)  # 10.5 → 11
# 日期 → 字符串
pl.col("date").cast(pl.Utf8)
# 字符串 → 日期
pl.col("date_str").str.to_date()
# 字符串 → Datetime
pl.col("datetime_str").str.to_datetime()

4. Categorical:节省内存的神器

什么是Categorical?

简而言之,当某一列包含大量重复的字符串值时,使用Categorical类型可以带来显著的内存节省。其内部机制是使用整数编码来映射不同的类别,而非存储原始字符串的多个副本。

通过一个直观的示例对比:

# 生成100万行数据,仅包含“北京”、“上海”、“深圳”三个城市
cities = ["北京", "上海", "深圳"] * 1000000

# 使用Utf8类型
df_utf8 = pl.DataFrame({"城市": cities})
print(f"Utf8内存: {df_utf8.get_column('城市').estimated_size() / 1024 / 1024:.2f} MB")

# 使用Categorical类型
df_cat = pl.DataFrame({"城市": pl.Series("城市", cities, dtype=pl.Categorical)})
print(f"Cat内存: {df_cat.get_column('城市').estimated_size() / 1024 / 1024:.2f} MB")

内存占用对比结果悬殊:

Utf8内存: 57.00 MB
Cat内存: 0.50 MB

内存节省超过99%!这对于处理海量数据集具有重大意义。

创建Categorical列

主要有两种方式:

# 方法1:创建Series时指定dtype
df = pl.DataFrame({
    "城市": pl.Series(["北京", "上海", "深圳"], dtype=pl.Categorical)
})

# 方法2:对现有列进行类型转换
df = pl.DataFrame({"城市": ["北京", "上海", "深圳"]})
df = df.with_columns(
    pl.col("城市").cast(pl.Categorical)
)

何时使用Categorical?

遵循一个核心原则:低基数,高重复。典型场景包括性别、省份、产品分类、状态码等。对于像“用户ID”、“订单号”这类几乎每个值都唯一的高基数列,使用Categorical反而会增加额外的映射开销。

5. 日期时间处理:dt模块

Polars为日期时间列提供了功能强大的.dt访问器,使相关操作变得简洁高效。

字符串转日期

df = pl.DataFrame({
    "date_str": ["2024-01-15", "2024-02-20", "2024-03-25"]
})
result = df.with_columns(
    pl.col("date_str").str.to_date().alias("date")
)
print(result)

提取日期时间组件

从日期时间列中提取特定部分是常见需求:

df = pl.DataFrame({
    "dt": [datetime(2024, 1, 15, 10, 30, 45)]
})
result = df.with_columns(
    pl.col("dt").dt.year().alias("年"),
    pl.col("dt").dt.month().alias("月"),
    pl.col("dt").dt.day().alias("日"),
    pl.col("dt").dt.hour().alias("时"),
    pl.col("dt").dt.minute().alias("分"),
    pl.col("dt").dt.second().alias("秒"),
    pl.col("dt").dt.weekday().alias("星期几"),  # 1=周一, 7=周日
    pl.col("dt").dt.day_of_year().alias("一年第几天"),
)
print(result)

日期时间格式化

将日期时间对象转换为特定格式的字符串:

# 日期 → 字符串
result = df.with_columns(
    pl.col("dt").dt.strftime("%Y年%m月%d日").alias("中文格式"),
    pl.col("dt").dt.strftime("%Y-%m-%d").alias("ISO格式"),
    pl.col("dt").dt.strftime("%H:%M:%S").alias("时间格式"),
)
print(result)

日期计算与调整

执行日期的加减、截断等运算:

from datetime import timedelta
df = pl.DataFrame({
    "date": [date(2024, 1, 1), date(2024, 1, 15), date(2024, 2, 1)]
})
result = df.with_columns(
    # 加7天
    (pl.col("date") + timedelta(days=7)).alias("加7天"),
    # 减3天
    (pl.col("date") - timedelta(days=3)).alias("减3天"),
    # 截断到月初
    pl.col("date").dt.truncate("1mo").alias("月初"),
    # 获取当月最后一天
    pl.col("date").dt.month_end().alias("月末"),
    # 计算所属季度
    pl.col("date").dt.quarter().alias("季度"),
)
print(result)

6. 字符串处理:str模块

字符串操作通过.str访问器进行,其功能全面且高效。

常用字符串操作

df = pl.DataFrame({
    "name": ["Zhang San", "LI SI", "Wang Wu"],
    "email": ["zhang@qq.com", "li@163.com", "wang@gmail.com"]
})
result = df.with_columns(
    # 转大写
    pl.col("name").str.to_uppercase().alias("大写"),
    # 转小写
    pl.col("name").str.to_lowercase().alias("小写"),
    # 首字母大写
    pl.col("name").str.to_titlecase().alias("首大写"),
    # 计算字符串长度
    pl.col("name").str.lengths().alias("长度"),
    # 提取@符号前的部分
    pl.col("email").str.strip_prefix("@").alias("邮箱前缀"),
)
print(result)

字符串匹配与替换

result = df.with_columns(
    # 判断是否包含子串
    pl.col("email").str.contains("qq").alias("是QQ邮箱"),
    # 判断是否以某字符串开头
    pl.col("email").str.starts_with("zhang").alias("是zhang开头"),
    # 替换子串
    pl.col("email").str.replace("gmail", "outlook").alias("替换后"),
    # 移除字符串首尾空格
    pl.col("name").str.strip().alias("去空格"),
)
print(result)

使用正则表达式提取

利用正则表达式从字符串中提取目标子串:

df = pl.DataFrame({
    “text”: [“订单号: A12345”, “订单号: B67890”, “订单号: C11111”]
})
result = df.with_columns(
    # 提取数字部分
    pl.col(“text”).str.extract(r”(\d+)”, 0).alias(“订单号”),
)
print(result)

7. Null值处理

现实数据中普遍存在Null值。Polars提供了灵活的方法来处理缺失数据。

检测Null值

df = pl.DataFrame({
    "name": ["张三", None, "王五"],
    "age": [25, 30, None],
    "salary": [8000, None, 12000]
})
# 检查是否为Null
result = df.select(
    pl.col("name").is_null().alias("name是Null"),
    pl.col("age").is_not_null().alias("age非Null"),
)
print(result)

填充Null值

# 使用固定值填充
result = df.with_columns(
    pl.col("age").fill_null(0).alias("age填0"),
    pl.col("salary").fill_null(pl.col("salary").mean()).alias("salary填均值"),
)
# 使用前向填充策略
result = df.with_columns(
    pl.col("name").fill_null(strategy="forward").alias("用前值填充"),
)

删除包含Null的行

# 删除任何列包含Null的行
result = df.drop_nulls()
# 仅删除指定列包含Null的行
result = df.drop_nulls(subset=["salary"])

8. List和Struct类型

List类型:存储值序列

List类型允许在一列中存储数组,适用于存储如多次考试成绩、用户浏览历史等序列化数据。

df = pl.DataFrame({
    "name": ["张三", "李四"],
    "scores": [[90, 85, 92], [78, 88, 95]]
})
result = df.with_columns(
    # 计算列表长度
    pl.col("scores").list.lengths().alias("考试次数"),
    # 求列表最大值
    pl.col("scores").list.max().alias("最高分"),
    # 计算列表平均值
    pl.col("scores").list.mean().alias("平均分"),
)
print(result)

Struct类型:组合字段

Struct类型可以将多个相关的字段组合成一个列,类似于字典或JSON对象,便于组织复杂数据。

df = pl.DataFrame({
    "name": ["张三", "李四"],
    "info": [
        {"age": 25, "city": "北京"},
        {"age": 30, "city": "上海"}
    ]
})
result = df.with_columns(
    pl.col("info").struct.field("age").alias("年龄"),
    pl.col("info").struct.field("city").alias("城市"),
)
print(result)

9. 实战:数据清洗

将上述知识点整合,完成一个完整的数据清洗流程:

raw_df = pl.DataFrame({
    "order_id": ["A-001", "B-002", "C-003", None],
    "customer": ["Zhang", "LI", "Wang", "Zhao"],
    "amount": ["100", "200", "abc", "400"],
    "date": ["2024-01-01", "2024-01-02", "2024-01-03", "2024-01-04"],
    "category": ["电子产品", "电子产品", "服装", "服装"]
})

# 完整清洗流程
clean_df = (
    raw_df
    # 1. 删除包含Null的行
    .drop_nulls()
    # 2. 执行类型转换
    .with_columns(
        pl.col("amount").cast(pl.Int32).alias("金额"),
        pl.col("date").str.to_date().alias("日期"),
    )
    # 3. 将分类列转换为Categorical以节省内存
    .with_columns(
        pl.col("category").cast(pl.Categorical)
    )
    # 4. 新增计算列(例如计算含税金额)
    .with_columns(
        (pl.col("金额") * 1.1).alias("含税金额"),
    )
    # 5. 选择最终需要的列
    .select(["order_id", "customer", "金额", "含税金额", "日期", "category"])
)
print(clean_df)
print(clean_df.dtypes)

10. 避坑指南

在实际应用中,注意避开以下几个常见陷阱。

陷阱1:字符串转日期时格式不匹配

# ❌ 错误:未指定格式可能导致解析失败
pl.col("date_str").str.to_date()  # 默认期望ISO格式
# ✅ 正确:明确指定字符串格式
pl.col("date_str").str.to_date("%Y/%m/%d")

陷阱2:对高基数列误用Categorical

# ❌ 错误:姓名这类高基数列使用Categorical会适得其反
pl.col("姓名").cast(pl.Categorical)  # 内存开销反而增大
# ✅ 正确:仅对低基数、高重复的列使用Categorical
pl.col("城市").cast(pl.Categorical)

陷阱3:浮点数转整数导致精度丢失

# ❌ 错误:直接转换会截断小数部分,price=10.9会变成10
pl.col("price").cast(pl.Int32)
# ✅ 正确:先进行四舍五入再转换
pl.col("price").round(0).cast(pl.Int32)

11. 总结

本文系统性地梳理了Polars的类型系统。从基础数据类型的创建与转换,到能极大优化内存的Categorical类型,再到功能强大的日期时间(.dt)和字符串(.str)处理模块,最后涵盖了Null值处理以及List、Struct等复杂类型。熟练掌握这些内容,意味着你不仅能实现功能,更能编写出高效、清晰、内存友好的高质量代码。数据处理的性能优势与代码优雅性,往往就体现在对这些数据类型的精准运用之中。

免责声明

本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。

相关阅读

更多
欢迎回来 登录或注册后,可保存提示词和历史记录
登录后可同步收藏、历史记录和常用模板
注册即表示同意服务条款与隐私政策