用pandas做数据分析,第一步永远是创建DataFrame。很多初学者从教程里抄一段示例代码,df = pd.DataFrame({'name': ['张三', '李四'], 'age': [23, 31]}),一按回车就觉得自己会了,可一旦面对真实场景——订单表里混着千分位金额、日期字段读进来变成字符串、Excel表带两行表头、数据库查出来一堆空值——立刻被各种"神秘报错"卡住,只能到处搜索"pandas为什么报KeyError""DataFrame类型怎么转换"这类问题。这篇文章就是一份围绕pandas创建DataFrame的完整实操笔记:先把最常用的几种创建方式、示例代码背后的参数逻辑讲透,再通过一个电商订单数据的完整案例,把"从造数据到建表再到清洗成型"的整条链路走一遍,最后把我在实际项目中反复踩过的坑和排查经验整理成速查表。适合刚接触pandas的Python数据初学者,也适合写过一些代码但总在数据结构、类型转换细节上卡壳的同学参考。
1. 先想清楚:创建DataFrame到底在做什么
1.1 DataFrame的本质,就是一张会"自校正"的电子表格
理解DataFrame,最好的类比是Excel里的一张工作表:有行、有列、有表头,每个单元格都能填不同类型的内容。但DataFrame比Excel更严格——同一列只允许一种数据类型,行和列都有对应的标签(index和columns),而这种"强约束"恰恰是它能高效做筛选、聚合、关联的前提。
所以创建DataFrame的核心,不是机械地把数据塞进某种"结构",而是必须回答三个问题:数据从哪里来?是手写构造、程序生成、文件读取,还是数据库查询;行与列如何对齐?每列叫什么名字,每行用什么索引,数据与标签如何一一对应;每个字段的类型是什么?是字符、整数、浮点、日期,还是类别。这三个问题想清楚了,代码怎么写都是顺理成章;想不清楚,后面必然要花大量时间在数据类型转换、缺失值清理上。
我见过不少同事一边写一边改,代码看起来像在追着报错跑。经验是:创建DataFrame时多花三十秒想清楚结构和类型,比事后用十行astype救场划算得多。尤其是"创建时日期列是字符串、金额列是object"这种问题,前期定好类型,后面可以直接省掉一整段清洗代码。pandas里有一句流传很久的话——"DataFrame创建决定了数据分析的下限",这句话虽然略带夸张,但方向是对的。
1.2 几种创建方式的选型逻辑
pandas创建DataFrame的常见来源大致分五类:从字典创建、从列表或二维数组创建、从NumPy数组创建、从外部文件读取、从数据库查询。先看一张对照表,再逐个解释为什么选它:
| 创建方式 | 典型代码 | 适合场景 | 注意点 |
|---|---|---|---|
| 字典创建 | pd.DataFrame({'a': [1,2], 'b': [3,4]}) | 字段明确、数据量小、手工构造 | 字典键就是列名,列顺序由传入顺序决定 |
| 列表创建 | pd.DataFrame([(1,2),(3,4)], columns=['a','b']) | 元组或列表组成的行数据 | 必须显式给columns,否则列名是0、1、2 |
| NumPy数组创建 | pd.DataFrame(arr, columns=['a','b'], index=...) | 矩阵计算、机器学习特征 | 维度、列名、列数必须严格匹配 |
| 外部文件读取 | pd.read_csv('data.csv') | 真实项目中最常用 | 返回对象本身就是DataFrame,要处理编码与类型 |
| 数据库查询 | pd.read_sql('select * from t', conn) | 数据在数据库里 | 需要SQLAlchemy或DBAPI连接对象 |
选型逻辑其实很简单:数据在内存里就选前三种,数据在磁盘或数据库就选后两种。但有一个原则我反复强调——不到万不得已,不要在循环里逐行向DataFrame追加数据。原因是DataFrame的内存结构是"按块存储"的,每append一次就要重新分配并复制一次数据,几百行看不出来,几万行就开始明显卡顿。正确做法是先把所有行收集到普通列表,最后一次性交给pd.DataFrame(),或者用pd.concat批量合并。这个习惯如果从第一天就养成,后面处理大文件时会轻松很多。
2. 核心细节解析与实操要点
2.1 从字典创建DataFrame:两种写法,场景完全不同
最常见的字典创建写法,是"字典的值是一列数据":
import pandas as pd df = pd.DataFrame({ 'name': ['张三', '李四', '王五'], 'age': [23, 31, 28], 'city': ['北京', '上海', '广州'] }) print(df)输出如下:
name age city 0 张三 23 北京 1 李四 31 上海 2 王五 28 广州这里的逻辑是:字典的键会成为列名,每个值是一个列表,代表这一列的所有数据。pandas会自动为每行分配0、1、2这样的RangeIndex,不需要你手动去数行数。这种"纵向列传值"的写法最贴近人脑对表格的认知——先有列名,再逐列填内容,很适合手工构造小型测试集。
另一种写法是"每行一个字典",把每个字典看作一条记录:
df = pd.DataFrame([ {'name': '张三', 'age': 23, 'city': '北京'}, {'name': '李四', 'age': 31, 'city': '上海'}, {'name': '王五', 'age': 28, 'city': '广州'}, ])这种方式在实际项目中反而更常用,因为从爬虫接口拿回来的JSON数据天然就是list[dict]结构,每条记录一个字典,直接传给pd.DataFrame()即可完成转换。反过来,如果你手里是一张从Excel复制出来的二维表,则更适合用dict of list组织数据。两种写法没有优劣之分,只看数据源长什么样。
创建时还可以显式指定columns和index。columns参数一方面控制列的顺序,另一方面也能起到筛选列的作用——如果数据字典里没有对应列,pandas会自动补NaN;index参数则可以为行指定索引,常用于时间序列数据,比如pd.DataFrame(data, index=pd.date_range('2024-01-01', periods=3))。我自己的习惯是:只要数据是"有顺序含义"的,就第一时间把index做好,而不是等后面再set_index,否则中间步骤容易因为索引错位导致莫名其妙的结果。
提示:用字典创建时,所有字典值的长度必须一致,否则pandas会直接报错
ValueError: All arrays must be of the same length。这个错误本质上就是"你的表格缺了一列没填完",检查一下数据源哪条记录不完整即可。
2.2 从列表或二维数组创建DataFrame
当数据本身就是二维结构时,用列表或NumPy二维数组来创建最直接:
import numpy as np import pandas as pd # 二维列表:每行是一条记录 data_rows = [ ['2024-01-01', 'A001', 399.00], ['2024-01-01', 'A002', 159.00], ['2024-01-02', 'A003', 219.50], ] df = pd.DataFrame(data_rows, columns=['date', 'order_id', 'amount']) print(df) # NumPy二维数组:矩阵计算结果直接转表 arr = np.random.randn(100, 4) df_arr = pd.DataFrame(arr, columns=['f1', 'f2', 'f3', 'f4']) print(df_arr.head())df_arr这个例子里,np.random.randn(100, 4)生成一个100行4列的随机数矩阵,pd.DataFrame直接把它包装成带列名的表格。索引仍然是默认的0到99,列名则由columns参数指定。这种方式的优点是"原生态"——从CSV读进来、从矩阵计算拿到结果、从旧系统导出,数据天然就是二维结构;缺点是列名语义完全靠代码维护,一不留神就会列错误位。
用二维数组创建时有个最常见的坑:数组的shape和columns数量不一致。np.random.randn(100, 4)有4列,你如果只传3个列名,pandas不会自动帮你截断,而是直接抛ValueError。排查方法很简单,先打印arr.shape看几个维度,再数一遍columns列表长度,两边对齐再创建。另一个细节是:用pd.DataFrame(rows)时如果不指定columns,列名会变成0、1、2这样的数字,这在真实项目中几乎没有任何语义价值,所以千万别偷懒,列名一定要给。
2.3 创建时就把类型定好:省掉后面一堆astype
创建DataFrame时的高频问题,是data里全是字符串,后面要算均值、排序,结果一运行就报错。这里要养成一个习惯——创建阶段能定类型就别拖到使用阶段。
手动创建时可以用dtype参数指定默认类型,但注意pd.DataFrame构造器本身只支持一个全局dtype,并不能按列分别指定(这是很多教程容易误导的地方),所以更稳妥的方式是创建后立刻用astype逐列转换:
df = pd.DataFrame({ 'date': ['2024-01-01', '2024-01-02'], 'order_id': ['A001', 'A002'], 'amount': ['399.00', '159.00'] }) # 立刻做类型校正 df['amount'] = df['amount'].astype(float) df['date'] = pd.to_datetime(df['date']) df['order_id'] = df['order_id'].astype('string') print(df.dtypes)输出:
date datetime64[ns] order_id string amount float64 dtype: object读文件时在源头转换效率更高,也是最推荐的做法:
df = pd.read_csv( 'orders.csv', parse_dates=['date'], # 日期字段自动解析为datetime64 dtype={'order_id': 'string', 'amount': 'float64'} )parse_dates参数会把指定列自动尝试解析成时间类型,省去手动to_datetime;dtype参数则强制指定字段类型。这里尤其要提防的是订单编号这类字段——如果不在读取时指定dtype={'order_id': 'string'},pandas极有可能把"001"读成整数1,导致编号丢失前导零。类似的问题在账号、电话号码列上也特别常见,别等分析结果不对劲了才回头查原始数据。
至于字符串转数值、字符串转日期这两个操作,标准套路是把errors='coerce'写进去:
df['amount'] = pd.to_numeric(df['amount'], errors='coerce') df['date'] = pd.to_datetime(df['date'], errors='coerce')errors='coerce'的意思是:遇到无法解析的值,不抛异常,而是转成NaN保留下来。这样做的价值在于,你可以在后续步骤统一查看哪些值有问题,而不是被一个脏数据打断整条流程。不过要记住,被coerce成NaN的值本身就是脏数据,后续一定要单独处理,否则它会悄悄影响平均值和汇总结果。
2.4 文件读取:真实项目中最常用的"创建方式"
真实项目里,DataFrame通常不是手敲出来的,而是读进来的。pd.read_csv是数据侧最核心的入口,理解它的常用参数,比多背几种构造方法更有价值:
df = pd.read_csv('sales.csv', encoding='utf-8-sig')第一个参数是文件路径,实际使用中我会顺手加三个常用参数:encoding='utf-8-sig',兼容Excel导出的带BOM的CSV,避免中文乱码;sep,默认分隔符是逗号,遇到分号或制表符就传sep=';'或sep='\t';header,默认0表示第一行是列名,如果文件没有表头,传header=None并手动指定names参数。还有两个我几乎每次都会用的参数:usecols只读取需要的列,避免大文件占满内存;nrows先读前几千行做数据侦查,确认格式无误后再全量加载。
除了CSV,另外两个高频入口是read_excel和read_sql:
df_excel = pd.read_excel('data.xlsx', sheet_name='订单', header=0) df_sql = pd.read_sql('SELECT id, name, amount FROM orders', conn)read_excel需要安装openpyxl或xlrd库,sheet_name可以直接传工作表名称;read_sql需要SQLAlchemy或DBAPI连接对象,返回的同样是DataFrame。所以理解"创建DataFrame"不能只盯着pd.DataFrame构造函数,读写文件、读写数据库,本质上都是在创建DataFrame,而且往往是更常见的方式。
顺便提一句环境配置:很多人在pycharm里新建项目后装pandas总是超时,换用清华PyPI镜像站加速安装通常能立刻解决。安装命令大致是pip install pandas -i https://pypi.tuna.tsinghua.edu.cn/simple,这是国内正常的软件源加速方式,日常开发中非常实用。
3. 实操过程与核心环节实现:一个订单数据的完整例子
3.1 场景设定:先造一批数据
实战环节,我们模拟一个电商订单场景。目标是把一批零散的订单明细整理成DataFrame,并完成初步的类型校正与清洗。第一步永远是从无到有造一批测试数据,毕竟真实数据涉及隐私,而且造数据的过程本身就能帮你理解"数据长什么样才算合理"。
import numpy as np import pandas as pd np.random.seed(42) n = 1000 # 生成等间隔时间戳,模拟订单发生时间 dates = pd.date_range('2024-01-01', periods=n, freq='h') # 生成订单编号、金额和区域,顺便塞20个缺失金额模拟脏数据 order_ids = [f'OD{10000+i}' for i in range(n)] amounts = np.round(np.random.uniform(50, 5000, n), 2) regions = np.random.choice(['华东', '华北', '华南', '西南'], n) amounts[np.random.choice(np.arange(n), 20, replace=False)] = np.nan sales_df = pd.DataFrame({ 'order_date': dates, 'order_id': order_ids, 'amount': amounts, 'region': regions }) print(sales_df.head()) print(sales_df.info())这里有几个细节值得展开。pd.date_range('2024-01-01', periods=n, freq='h')生成的是从2024年1月1日0点开始、每小时一条的等间隔时间序列,目的是模拟真实订单时间戳;np.random.uniform(50, 5000, n)生成连续金额后四舍五入到两位小数,符合"钱只保留分位"的业务习惯;故意往amount列塞20个NaN,是为了让后续清洗环节有东西可操作。np.random.seed(42)固定随机种子,保证每次跑出来的数据完全一致——这在写演示代码和测试时特别重要,数据每次不同会让你怀疑是自己代码写错了还是随机性导致的差异。
从"创建"的角度看,这段代码用的就是字典创建和预生成NumPy数组的组合拳,同时提前把order_date列做成了datetime64[ns]类型,金额列也成了float64。这正是第2节反复提到的思路:创建时尽量把类型定好,后面才不需要疯狂astype。
3.2 从创建走向清洗:把表变成干净可用的样子
DataFrame创建完成后,别急着做统计分析,第一步用df.info()做一次"体检":
print(sales_df.info())正常情况下你会看到类似下面的信息:
<class 'pandas.core.frame.DataFrame'> RangeIndex: 1000 entries, 0 to 999 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 order_date 1000 non-null datetime64[ns] 1 order_id 1000 non-null object 2 amount 980 non-null float64 3 region 1000 non-null object信息一目了然:1000行、4列,amount列只有980个非空值,说明有20个NaN。接下来做三项常规清洗:处理缺失值、修正类型、对分类字段做优化存储。
对缺失金额,最保守的做法是dropna(subset=['amount'])删掉缺失行;如果业务上允许填充,也可以用fillna配合分组均值填充。先看数量再决定方案——缺失比例在5%以内且没有明显规律,删掉通常不影响大局;如果缺失集中在某一天或某个区域,那就要排查业务原因了。类型修正方面,假设字段是从外部文件读进来的字符串,标准套路就是对金额执行pd.to_numeric(..., errors='coerce'),对日期执行pd.to_datetime(..., errors='coerce')。
sales_df_clean = sales_df.dropna(subset=['amount']) sales_df_clean['region'] = sales_df_clean['region'].astype('category') # 如果是从文件读进来的字符串,补上这两行 # sales_df_clean['amount'] = pd.to_numeric(sales_df_clean['amount'], errors='coerce') # sales_df_clean['order_date'] = pd.to_datetime(sales_df_clean['order_date']) print(sales_df_clean.dtypes)把区域region转成category类型,是一个容易被忽视的优化点:当某个列只有少数几个取值,object类型会占用更多内存,而category类型会把重复值映射成整数编号,在几百万行数据上能省下可观的内存,分组聚合时也会更快。处理完类型,再检查一下索引是否连续:
sales_df_clean = sales_df_clean.reset_index(drop=True)reset_index(drop=True)会把删除行之后断裂的索引重新编号,这样后续按位置切片、合并时不会出现莫名其妙的索引空洞。这一步看起来小,但在多人协作的项目里,索引不连续常常是"为什么我的结果少了4行"的罪魁祸首。
3.3 结果校验与导出
清洗完成后,最后一步是校验和导出。很多初学者洗完数据直接print看一下觉得对就交差了,但真实项目里我更推荐用断言把核心约束写进脚本:
# 核心校验:类型、行数、无缺失 assert sales_df_clean['amount'].dtype == np.float64 assert len(sales_df_clean) == 980 assert sales_df_clean['amount'].notna().all() # 导出为CSV sales_df_clean.to_csv('sales_clean.csv', index=False, encoding='utf-8-sig')导出的两个参数值得解释:index=False表示不写索引列,否则下次读回来时会多出一列莫名其妙的Unnamed: 0;encoding='utf-8-sig'则是为了兼容Excel——加了BOM头之后,用Excel直接打开中文CSV不会乱码。这两个参数几乎每次导出都会用到,属于固定记忆项。
到这里,一个完整的"创建DataFrame → 体检 → 清洗 → 校验 → 导出"链路就通顺了。对刚上手的同学,建议把这段代码在自己的环境里逐行敲一遍,重点观察df.info()输出的Dtype列在清洗前后发生了什么变化——类型对了,后面统计和绘图才稳得住,否则画图画到一半发现X轴全是字符串乱序,那种体验真的很磨人。
4. 常见问题与排查技巧实录
4.1 高频问题速查表
根据我在实际项目中反复遇到的报错,我把最典型的几个问题、现象、原因和解决方案整理成速查表,建议截图存一份:
| 问题 | 现象 | 常见原因 | 解决方案 |
|---|---|---|---|
| KeyError: '列名' | df['amount']找不到列 | 列名含空格或大小写不一致;CSV表头被当成第一行数据 | 打印df.columns确认真实列名;CSV用header=0或header=None+names |
| ValueError: All arrays must be of the same length | 字典创建时列长度不一致 | 某个键对应的列表比其他短 | 检查数据源,补全缺失记录或删除不完整行 |
| dtype是object | 数值列sum()报错 | 文件里混入逗号、百分号、空字符串 | pd.to_numeric(..., errors='coerce') |
| 日期筛选失效 | 按月份筛选报错或结果为空 | 日期列被读成字符串 | read_csv加parse_dates,或pd.to_datetime |
| 中文乱码 | 读CSV中文乱码 | 文件编码不是utf-8或带BOM | encoding='utf-8-sig'或'gbk' |
| df.append不存在 | AttributeError | pandas 2.0已移除append方法 | 用pd.concat替代 |
| SettingWithCopyWarning | 对切片后的df赋值时报警告 | 链式赋值如df[df.a>0]['b']=1 | 用.loc,或先copy()再操作 |
4.2 三个最典型的坑,重点排雷
坑一:列名和数据"隐形错位"。很多Excel导出的CSV,第0行并不是真正的表头,而是一条标题或空行。如果不指定header和skiprows,列名就会变成"订单明细表"这种人类标题,真实表头反而成了数据区第一行,后续访问字段会全部错乱。我的习惯是拿到任何文件都不先写业务代码,而是先打印df.head(5)和df.columns.tolist(),花十秒钟确认表头长什么样,再决定要不要加skiprows。
坑二:把"文本型数字"直接强转。金额带千分位"3,900.00"读进来就是字符串,新手直接df['amount'].astype(float)会当场炸掉,因为float不认识逗号。正确顺序是:先pd.to_numeric(df['amount'], errors='coerce')软转换,把非法值变成NaN,再观察哪些记录有问题,最后统一填充或删除。这个"先软转、再找脏值、最后处理"的顺序不能反——先coerce,再查NaN,绝大多数文本型数字问题都能解决。
坑三:列明明存在,却提示KeyError。这种情况常见于列名带有空格、中文括号、大小写不统一,比如Excel自动把列名命名为"order date"而不是"order_date"。你写df['order_date']时自然找不到。排查方法极度简单:打印df.columns把每一列名字原样看清楚,然后复制粘贴到代码里,不要凭记忆手敲。还有一个变种情况是用df.列名这种属性访问方式——只要列名里有空格或特殊字符就不行,而且列名和已有的方法同名也不行,老老实实用df['列名']。
4.3 一条实用经验:创建之前先"侦查"
最后分享一个我自己踩过多次坑之后养成的习惯。拿到任何数据源,我不会直接写pd.DataFrame或pd.read_csv,而是先做一次最小规模的"侦查":如果是文件,用Python内置的open读前3行,看看分隔符和表头长什么样;如果是接口返回,先print响应中第一条记录的字典结构;如果是数据库,先执行SELECT * FROM 表 LIMIT 5,观察字段名和值类型。侦查完,再确定创建方案和dtype策略。
这不是浪费时间,而是用两分钟避免后面两小时的返工。创建DataFrame的能力,并不在于能背出多少种构造方法,而在于面对真实、杂乱的数据时,能快速判断它应该以什么结构、什么类型进入pandas。这个判断力靠的是日常积累和主动排坑,不是看几篇教程就能获得的。
我再多说一句日常感触:每次看到刚入门的同学在论坛里问"为什么我的DataFrame做不了groupby""为什么日期排序全乱了",十有八九都是创建阶段就把类型搞成了object、把日期搞成了字符串。这篇文章我没有展开讲透视、合并这些高阶操作,只围绕"创建"这一个动作反复讲——把结构、类型、来源想明白,后面真的可以少掉一半的debug时间。