Pandas数据读取与写入:read_csv、Excel 与数据库
做分析的第一步不是建模,是把数据完整地读进来。读进来那一刻列的 dtype 就定了,后面每一步都在这个基础上做:本该是数值的列读成了字符串,求均值时要么报错要么给出离谱结果;日期列读成了文本,画时间序列之前还得再转一次。read_csv 的参数有几十个,日常真正用到的不到十个。
一份 CSV 的往返
Section titled “一份 CSV 的往返”用 sklearn 自带的 iris 数据集走一遍完整流程。load_iris(as_frame=True) 直接返回 DataFrame,不需要自己拼列名。
import pandas as pdfrom sklearn.datasets import load_iris
iris = load_iris(as_frame=True)df = iris.frame
df.to_csv("iris.csv", index=False) # 写到当前目录df2 = pd.read_csv("iris.csv") # 再读回来
print(df2.shape)print(df2.dtypes)(150, 5)sepal length (cm) float64sepal width (cm) float64petal length (cm) float64petal width (cm) float64target int64dtype: object这些 dtype 是推断出来的:pandas 逐列扫描文本,看到 5.1 就认成浮点,看到 0 就认成整数。推断经常出错,出错的方式还很隐蔽。学号 007 读进来是整数 7,前导零没了;身份证号、手机号这类十几位的列推断成 int64 之后,一旦有缺失值整列会变成 float64,值还会显示成 1.234567890123457e+17。编号、编码、邮编这类列,读取时就显式指定 dtype=str,别等到发现对不上再回头改。
只读需要的列
Section titled “只读需要的列”usecols 和 dtype 是最值得先记住的两个参数。150 行的 iris 用不用它们没差别,几 GB 的问卷导出文件能省掉一半以上的内存和读取时间。
small = pd.read_csv( "iris.csv", usecols=["sepal length (cm)", "target"], dtype={"target": "int8"},)
print(small.head(3))print(small.dtypes) sepal length (cm) target0 5.1 01 4.9 02 4.7 0sepal length (cm) float64target int8dtype: objecttarget 只有 0、1、2 三个取值,用 int8 就够,读成 int64 是八倍的空间浪费。做交叉表、分组统计时这点空间不算什么,但如果这张表有几十列分类变量,差别就很明显了。
分隔符、缺失值与日期列
Section titled “分隔符、缺失值与日期列”真实文件很少是干净的逗号分隔。分号分隔在欧洲软件导出的 CSV 里很常见,缺失值可能是空串、NA、-、null 里的任意一种。为了不依赖外部文件,下面用 io.StringIO 把一段文本当文件读。
import ioimport pandas as pd
txt = "id;name;value;date\n1;alpha;NA;2024-01-02\n2;beta;3.5;2024-02-03\n3;gamma;-;2024-03-04\n"d = pd.read_csv(io.StringIO(txt), sep=";", na_values=["NA", "-"], parse_dates=["date"])
print(d)print(d.dtypes) id name value date0 1 alpha NaN 2024-01-021 2 beta 3.5 2024-02-032 3 gamma NaN 2024-03-04id int64name strvalue float64date datetime64[us]dtype: object不用 na_values 的话,- 会被当成普通字符串,整列 value 变成字符串类型,后面所有数值计算全部失效。parse_dates 则是把日期字符串转成 datetime64——只有转成了日期类型,.dt.month、按时间重采样、时间序列图才可用。这里显示成 datetime64[us](微秒精度),pandas 2.x 及更早版本会显示 datetime64[ns],只是精度标注不同,不影响使用。
中文 CSV 的编码问题
Section titled “中文 CSV 的编码问题”国内软件(Excel、SPSS、各类问卷平台)导出的 CSV 大多是 GBK 编码,直接读会抛错:
import pandas as pd
cn = pd.DataFrame({"姓名": ["张三", "李四", "王五"], "成绩": [88.5, 92.0, 79.5]})cn.to_csv("成绩.csv", index=False, encoding="gbk") # 模拟一份 GBK 文件
print(pd.read_csv("成绩.csv", encoding="gbk")) # 指定编码,正常读出 姓名 成绩0 张三 88.51 李四 92.02 王五 79.5不指定编码会看到这样的报错,栈信息里通常只有一行有用:
try: pd.read_csv("成绩.csv")except UnicodeDecodeError as e: print("UnicodeDecodeError:", e)UnicodeDecodeError: 'utf-8' codec can't decode byte 0xd0 in position 0: invalid continuation byte0xd0 是 GBK 里「姓」的半个字节。看到 UnicodeDecodeError,把 encoding 依次试 gbk、gb18030、utf-8-sig 基本能解决。gb18030 是 GBK 的超集,遇到更生僻的少数民族文字或繁体字时用它更保险。
反方向也有坑:用 UTF-8 写的中文 CSV 双击用 Excel 打开是乱码,因为 Excel 默认按本地编码解析。要让 Excel 认出来,写成 encoding="utf-8-sig",它会在开头加三个字节的 BOM 标记。
Excel 与数据库:多一个依赖
Section titled “Excel 与数据库:多一个依赖”read_excel 本身不含解析器,读 .xlsx 需要装 openpyxl,读旧版 .xls 需要 xlrd:
$ pip install openpyxl没装的话 pd.read_excel() 会直接抛 ImportError。除了这一点,接口和 read_csv 几乎一样,usecols、dtype、na_values 都能用:
df.to_excel("iris.xlsx", index=False)x = pd.read_excel("iris.xlsx", sheet_name="Sheet1", usecols=["sepal length (cm)", "petal length (cm)"])print(x.head(3)) sepal length (cm) petal length (cm)0 5.1 1.41 4.9 1.42 4.7 1.3sheet_name 除了填表名,也可以填工作表序号(从 0 开始),填 None 则一次读回所有工作表,返回一个字典。多工作表的工作簿用 pd.read_excel("f.xlsx", sheet_name=None) 一次拿全,比逐个表名试要快。
数据库走 read_sql,推荐配一个 SQLAlchemy 引擎。SQLite 也能直接传 sqlite3.connect() 拿到的连接对象,但那种写法把代码绑死在 SQLite 上,换库就要改代码;用 create_engine 加连接串,换数据库只改一行 URL(对应的驱动包还是要装):
from sqlalchemy import create_engine
engine = create_engine("sqlite://") # 内存库,换成真实连接串即可df.to_sql("iris", engine, index=False, if_exists="replace")
q = pd.read_sql( 'SELECT target, AVG("sepal length (cm)") AS mean_sepal, COUNT(*) AS n ' "FROM iris GROUP BY target", engine,)print(q) target mean_sepal n0 0 5.006 501 1 5.936 502 2 6.588 50能推给数据库的活就别拉到内存里做。几千万行的表在 SQL 里 GROUP BY 只要几秒,读成 DataFrame 再 groupby 可能直接把内存撑爆。判断标准很简单:能用一条 SQL 表达清楚的聚合,就在数据库里做。
to_csv 的索引坑
Section titled “to_csv 的索引坑”这是新手最容易踩的一个。to_csv() 默认把行索引也写进去,读回来就多出一列 Unnamed: 0:
df.to_csv("iris_index.csv") # 忘了 index=Falseback = pd.read_csv("iris_index.csv")
print(list(back.columns))print(back.head(2))['Unnamed: 0', 'sepal length (cm)', 'sepal width (cm)', 'petal length (cm)', 'petal width (cm)', 'target'] Unnamed: 0 sepal length (cm) ... petal width (cm) target0 0 5.1 ... 0.2 01 1 4.9 ... 0.2 0
[2 rows x 6 columns](列太多时 pandas 会省略中间几列,用 ... 占位,并在末尾给出形状。上面的输出说明这张表比 iris 原表多了一列。)
后果不是多一列那么轻。下次把这张表合并给别人,对方在这个 Unnamed: 0 上做连接,两边索引规则稍有不同就会得到错误结果,而且很难看出来。写到 CSV、Excel 一律加 index=False,除非你明确要用索引当一列数据。
读进来发现有 Unnamed: 0,两种处理都行:pd.read_csv("f.csv", index_col=0) 把它当索引读回去,或者读完之后 df = df.drop(columns="Unnamed: 0")。
大文件:chunksize 分块
Section titled “大文件:chunksize 分块”文件比内存大的时候,read_csv 会直接 MemoryError。chunksize 返回一个可迭代的读取器,每批给你指定行数:
total = 0for chunk in pd.read_csv("iris.csv", chunksize=60): total += len(chunk) # 每批只处理 60 行,处理完就释放
print(total)150分块适合「逐行过滤、逐批聚合」这类任务,比如从日志里挑出符合条件的两万行存成新文件。不适合需要全表排序、透视的操作——那种情况该换 DuckDB、Polars 或者直接丢给数据库。
数据读进来之后,下一步是把它筛成你要的那部分,见 Pandas数据筛选与选择。读取方式本身也是可重复研究的一部分:把路径、编码、dtype 都写进脚本,而不是手工在 Excel 里改完再另存一份。R 语言里 可重复研究 讲依赖管理的那部分思路完全通用,值得一并看看。