字符串、日期与分类数据
前置知识:缺失、重复与类型清洗
本章目录
本章学习目标
- 使用字符串和日期访问器
- 避免时区和日期边界误差
- 理解类别编码及未观测类别
文本是数据而非随意字符
字符串列可以包含缺失,应使用 string 类型配合 str 访问器。str.strip 清理两端空白,str.lower 统一大小写,str.contains 按模式筛选,str.extract 提取结构信息。清洗渠道 ' Email ' 为 'email' 可能合理,但把产品型号强制小写或把账户前导零删掉可能改变标识。先明确字段的业务规则,再应用清洗。
字符串方法通常传播缺失;contains 返回的可空布尔结果,作为筛选条件时需要明确 na=False 或单独处理缺失。模式可能是正则表达式,查找字面的点号或括号时使用 regex=False 或正确转义。不要给所有文本列执行 astype(str) 后把缺失变成文字 'nan',否则后续 isna 不再识别真正缺失。
日期、时间和时间范围
时间戳表达某个时刻,业务日期表达某个时区里的日历日。to_datetime 解析以后,dt.year、dt.month、dt.day 等访问器提取组成信息,to_period('M') 可以表达月份分组。只比较月份数字会把不同年份的一月合并,应使用年月或完整日期范围。
筛选一天的数据通常使用左闭右开的时间区间:大于等于当天零点且小于下一天零点。使用当天二十三点五十九分五十九秒作为上界会漏掉更精细的时间。日期排序依赖日期 dtype,非规范字符串的字典序不一定等于时间顺序。先解析,再按明确时间单位计算差值,不能直接把整数时间戳单位猜成秒或毫秒。
小数据:时区会改变日期
UTC 的一月一日十八点,在上海是下一天凌晨两点。先以 utc=True 解析 UTC 时刻,再 tz_convert 到上海,最后提取 date,才能获得当地业务日期一月二日。若先截取原字符串前十位,就会把同一事件归到错误的日子。tz_localize 给无时区时间声明原来所属时区,tz_convert 才把已有时区的时刻换成另一个显示时区;两者不能互换。
夏令时地区有重复或不存在的本地时间,解析策略需要明确。教学 CSV 日期统一不带时区,示例另外给出 UTC 到上海的边界测试。做 SQL 对照时还要核对数据库会话时区与字段类型,不能以为 DATE(timestamp) 在所有环境中都得到同一个日期。
分类类型和统计口径
category 适合重复很多、取值集合有限的列,内部保存类别词典与代码,可以降低内存并限制取值。国家、渠道等列可以采用它,用户编号通常不适合简单转成分类来掩盖唯一值数量。ordered=True 声明类别顺序,使等级比较和排序符合业务含义;普通类别词典顺序不自动意味着数学大小。
groupby 时显式指定 observed=True,只输出实际出现的类别组合;若需要完整类别列表,应明确 observed=False 并解释未出现组合。pandas 版本对默认值可能有变化,教学代码主动声明避免统计结果随升级改变。新类别写入前需要扩充类别词典,否则赋值可能失败。
把转换接入分析
脚本先解析注册日期,规范渠道,再添加年月字段并将国家转成分类,输出示例帮助核对。转换后的表仍要保留 user_id 和原始粒度;任何字符串拆分导致一条记录变多行,都必须重新说明粒度。练习检查清洗后类别分布以及清洗前后的缺失变化。
最常见的反例是时间截取后才换时区、按 month 单独分组混合年份、把未知国家当成固定国家,以及正则点号匹配任何字符。修正时尽量保持原列或保留副本,列出具体失败值和理由。面试解释不是只说调用了 dt 或 str,而是说明输入含义、边界时刻和统计粒度如何保持一致。
版本与复现约定
本课程 pandas 示例以 2.3.3 为验证基线,页面运行记录给出实际版本。pandas 3.x 默认使用 Copy-on-Write:从表中取出的对象发生修改时,不据此假定原表同步变化。跨版本都应使用一次完整的 loc 赋值表达式;需要独立工作表时显式 copy。阅读网上的旧示例时,先核对版本、输入类型和索引,再判断结果能否复现。
完整示例与实际输出
from pathlib import Path
import pandas as pd
import numpy as np
from _shared import load
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "outputs"
OUT.mkdir(exist_ok=True)
users=load("users").copy()
users["signup_date"]=pd.to_datetime(users["signup_date"],errors="raise")
users["channel"]=users["channel"].astype("string").str.strip().str.lower()
users["country"]=users["country"].astype("category")
users["signup_month"]=users["signup_date"].dt.to_period("M").astype("string")
assert users["signup_date"].notna().all()
assert str(users["country"].dtype)=="category"
print(users[["user_id","signup_month","channel","country"]].head().to_string(index=False))
raw=pd.Series([" Email ",None,"PAID SEARCH"],dtype="string")
clean=raw.str.strip().str.lower()
assert clean.iloc[0]=="email" and pd.isna(clean.iloc[1])
print("字符串:",clean.tolist())
utc=pd.to_datetime(pd.Series(["2026-01-01T18:00:00Z"]),utc=True)
local=utc.dt.tz_convert("Asia/Shanghai")
assert local.dt.date.astype(str).iloc[0]=="2026-01-02"
print("上海日期:",local.dt.date.astype(str).iloc[0])
实际运行通过CPU Python execution.
user_id signup_month channel country
1 2026-01 ads US
2 2026-01 referral SG
3 2026-01 organic CN
4 2026-01 ads US
5 2026-01 referral SG
字符串: ['email', <NA>, 'paid search']
上海日期: 2026-01-02五道自测:先回答,再核对
01 · astype(str) 对缺失有什么风险?
查看提示
文字与缺失标记不同。
查看参考答案
可能生成 'nan' 等文本,isna 不再当作缺失;优先 string dtype。
02 · 按 month 分组有什么陷阱?
查看提示
跨年份。
查看参考答案
把不同年份同月混在一起,使用年月 period。
03 · tz_localize 与 tz_convert 的差别?
查看提示
是否已经有时区。
查看参考答案
前者给无时区时间声明时区,后者转换已有时区时刻的表示。
04 · 一天的时间范围怎样写?
查看提示
下一天作为上界。
查看参考答案
使用 [当日零点,次日零点),避免漏掉亚秒数据。
05 · observed=True 对类别分组做什么?
查看提示
实际出现的组合。
查看参考答案
仅输出实际观察到的类别组合,避免默认变化和未出现组合。
两项动手任务
任务 1 · 规范用户渠道并按注册年月展示。
提示
保留缺失,解析后使用 to_period。
完整参考解答与检查方式
from pathlib import Path
import pandas as pd
import numpy as np
from _shared import load
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "outputs"
OUT.mkdir(exist_ok=True)
users=load("users").copy()
users["signup_date"]=pd.to_datetime(users["signup_date"],errors="raise")
users["channel"]=users["channel"].astype("string").str.strip().str.lower()
users["country"]=users["country"].astype("category")
users["signup_month"]=users["signup_date"].dt.to_period("M").astype("string")
assert users["signup_date"].notna().all()
assert str(users["country"].dtype)=="category"
print(users[["user_id","signup_month","channel","country"]].head().to_string(index=False))
raw=pd.Series([" Email ",None,"PAID SEARCH"],dtype="string")
clean=raw.str.strip().str.lower()
assert clean.iloc[0]=="email" and pd.isna(clean.iloc[1])
print("字符串:",clean.tolist())
utc=pd.to_datetime(pd.Series(["2026-01-01T18:00:00Z"]),utc=True)
local=utc.dt.tz_convert("Asia/Shanghai")
assert local.dt.date.astype(str).iloc[0]=="2026-01-02"
print("上海日期:",local.dt.date.astype(str).iloc[0])
任务 2 · 检查包含字面点号的渠道,缺失不应被选中。
提示
contains 的 regex 和 na 参数。
完整参考解答与检查方式
from pathlib import Path
import pandas as pd
import numpy as np
from _shared import load
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "outputs"
OUT.mkdir(exist_ok=True)
s=pd.Series(["a.b","acb",None],dtype="string")
selected=s[s.str.contains(".",regex=False,na=False)]
assert selected.tolist()==["a.b"]
print(selected.tolist())
官方资料与掌握状态
完成两项任务后,按独立完成程度记录。状态会尽量保存在当前浏览器,可导出备份。
动手任务记录
掌握状态与复习
关掉资料独立完成后记 A;提示后完成记 B;需要重学记 C。
记录尽量保存在当前浏览器,建议定期导出备份。