数据类型详解 Categorical Date Time 的区别与应用
在数据处理领域,类型系统常被视为一个“能用即可”的次要角色。许多开发者认为,只要程序能输出结果,选择何种数据类型似乎无关紧要。然而,事实真的如此吗?恰恰相反,正确理解并高效运用类型系统,往往是区分普通脚本与高性能、高健壮性应用程序的核心所在。它能带来的优势是立竿见影的:内存占用可能降低数十倍,计算性能提升数倍,代码逻辑也因此变得更加清晰可维护,潜在的缺陷自然大幅减少。
本文将深入探讨Polars这一高性能数据处理库的类型系统,揭示它如何成为您数据分析工作中的强大武器。

1. Polars支持哪些数据类型?
Polars提供了一套丰富且精细的数据类型体系,足以应对各类复杂的数据分析场景。您可以通过一个简单的命令来查看其支持的所有数据类型:
import polars as pl
# 查看所有数据类型
print(pl.datatypes)
从基础的整数、浮点数,到字符串、日期时间,再到更高级的分类、列表和结构体类型,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()
# 字符串 → 日期时间
pl.col("datetime_str").str.to_datetime()
4. Categorical:节省内存的利器
什么是Categorical类型?
简而言之,当某一列存在大量重复的字符串值时,使用Categorical(分类)类型可以带来惊人的内存节省和性能提升。其内部机制是使用整数编码来代表不同的类别,而非存储完整的原始字符串。
来看一个直观的对比示例:
# 生成100万行数据,城市只有‘北京’、‘上海’、‘深圳’3个类别
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时直接指定
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(
# 计算List长度
pl.col("scores").list.lengths().alias("考试次数"),
# 求List中的最大值
pl.col("scores").list.max().alias("最高分"),
# 求List中的平均值
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. 删除order_id为Null的行
.drop_nulls()
# 2. 类型转换与清洗
.with_columns(
# 尝试转换金额,无效值会变为Null
pl.col("amount").cast(pl.Int32, strict=False).alias("金额"),
pl.col("date").str.to_date().alias("日期"),
)
# 3. 删除转换后金额为Null的行(如‘abc’)
.drop_nulls(subset=["金额"])
# 4. 分类列使用Categorical节省内存
.with_columns(
pl.col("category").cast(pl.Categorical)
)
# 5. 新增计算列(例如计算含税金额)
.with_columns(
(pl.col("金额") * 1.1).alias("含税金额"),
)
# 6. 选择并重排最终需要的列
.select(["order_id", "customer", "金额", "含税金额", "日期", "category"])
)
print(clean_df)
print(clean_df.dtypes)
10. 避坑指南
在实际使用中,有几个常见的“坑”需要注意避开。
坑1:字符串转日期时格式不匹配
# ❌ 错误:默认格式可能不匹配,导致解析失败或错误
pl.col("date_str").str.to_date() # 未指定格式
# ✅ 正确:明确指定日期字符串的格式
pl.col("date_str").str.to_date("%Y/%m/%d")
坑2:误将高基数列转换为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等复杂类型的应用。熟练掌握这些知识,意味着您不仅能编写出可运行的代码,更能构建出高效、清晰、内存友好的高质量数据处理程序。数据处理的效率与优雅,往往就体现在对这些数据类型的精妙运用之中。
热门专题
热门推荐
为庆祝品牌投身赛车运动整整125年,斯柯达正式推出了晶锐Fabia Motorsport Edition特别版。这款车基于Fabia 130打造,设计灵感直接来源于征战赛场的Fabia RS Rally2拉力赛车,整体风格充满了对赛事历史的致敬意味。不过,得先说明白,它的升级重点主要落在了外观和底盘
Grayscale 通过其以太坊质押 ETF 质押了 102,400 个 ETH,价值 2 37 亿美元 先来看一组数据:资产管理巨头 Grayscale 最近通过其以太坊质押 ETF,一口气质押了超过10万个 ETH,价值约2 37亿美元。这个动作本身不小,但更有意思的是市场的后续反应——或者说,
劳斯莱斯库里南自问世以来,始终是超豪华全尺寸SUV领域的标杆。对于追求极致安全又不愿牺牲低调气质的高净值人士而言,如何实现“隐形”的顶级防护,一直是核心诉求。如今,加拿大专业防弹车制造商Inkas,以一款近乎“零痕迹”改装的库里南,给出了完美解决方案——一座移动的“隐形堡垒”。 区别于常见的外露装甲
新加坡维塔士工作室正考虑将《侠盗猎车手V》与《荒野大镖客:救赎2》移植至任天堂Switch平台。该团队拥有丰富的移植经验,曾成功负责多款游戏的跨平台适配。这两款作品全球销量巨大,若能登陆Switch,其便携特性可能成为新的市场增长点。
当高尔夫GTI迎来五十周年里程碑,传奇的纽博格林北环赛道成为其致敬历史与展望未来的最佳舞台。这里不仅铭刻了燃油性能图腾的巅峰时刻,也正式开启了电动GTI的新纪元。近日,大众汽车正式宣布,高尔夫GTI 50周年版在纽北创下全新纪录,荣膺最快前驱量产车称号;与此同时,品牌首款纯电动GTI车型——ID





