首页 / 资讯中心 / 文章详情

pandas创建DataFrame的六种高效方法与避坑指南

pandas创建DataFrame的六种高效方法与避坑指南 ★ FEATURED ARTICLE
作为一个天天和数据打交道的人我几乎每天都要用pandas处理各种乱七八糟的数据。这几年下来我发现很多新手甚至一些有经验的朋友在“创建DataFrame”这一步就养成了不太好的习惯——要么只会用pd.read_csv()一条道走到黑要么手动构造数据时绕了许多弯路。先明确一个核心概念DataFrame是 pandas 里最重要的二维表格数据结构你可以把它理解成一张 Excel 表。平时我们遇到的数据格式五花八门——有可能是字典套列表、列表套字典、嵌套列表甚至是一段从接口返回的 JSON。如果只会一种创建方式遇到复杂输入时就会卡壳。这篇文章我打算围绕 6 种最常用的DataFrame创建方法展开结合我踩过的坑和优化经验把每种方法的适用场景、底层逻辑、性能差异一次说清楚。不管你是刚接触 pandas 的初学者还是已经写了半年脚本但想系统梳理基础的老手这篇都值得你花 10 分钟仔细看。特别是那些“代码能跑但总觉得别扭”的朋友这篇文章就是为你写的。1. 六种 DataFrame 创建方式从字典到外部文件的完整拆解1.1 最常用的pd.DataFrame() 字典为什么推荐从这种方式入门几乎 90% 的入门教程都会先讲pd.DataFrame()配合字典使用。原因很简单字典的键天然对应列名值对应整列数据这种结构对人类的直觉最友好。import pandas as pd data { 姓名: [张三, 李四, 王五], 城市: [杭州, 南京, 苏州], 年龄: [25, 30, 35], 入职年份: [2021, 2020, 2019], } df pd.DataFrame(data) print(df)运行结果是这样姓名 城市 年龄 入职年份 0 张三 杭州 25 2021 1 李四 南京 30 2020 2 王五 苏州 35 2019可以看到pandas 自动给每一行分配了默认索引 0、1、2。这里有一个新手最容易混淆的点传入的字典值是“一整列”还是“一整行”。在上面的例子里每个键对应的列表长度是 3代表这一列有 3 行数据。如果你传的是{姓名: 张三}这种单值pandas 会把它当标量广播处理生成的 DataFrame 只有一行一列。我个人建议初学者最优先掌握这种写法因为它的可读性最高。团队协作时别人拿到你的代码光看字典结构就能明白你的数据组织逻辑不用脑补。1.2 列表套列表当你手头已经是一堆原生数据时的直接方案第二种常见的创建方式是直接用嵌套列表也就是“列表里面套列表”。每个内层列表代表一行数据你需要额外传入columns参数指定列名。rows [ [张三, 杭州, 25, 2021], [李四, 南京, 30, 2020], [王五, 苏州, 35, 2019], ] df pd.DataFrame(rows, columns[姓名, 城市, 年龄, 入职年份]) print(df)输出和上面是一模一样的。这种写法最适合什么场景呢比如你用requests从某个接口拿到了一批 JSON 数据解析之后得到的就是列表套列表的结构直接丢给pd.DataFrame()就能转成表格。这里有一个很关键的细节和内层列表长度不一致会导致报错。我见过不少朋友写类似[[张三, 杭州], [李四, 南京, 30]]这种不齐的数据pandas 会在内部尝试对齐结果出现ValueError: All arrays must be of the same length。所以用列表套列表时先检查一下每行字段数是否一致这个习惯能帮你省掉大量调试时间。1.3 字典套列表 vs 列表套字典两种姿势的性能和取舍如果说上面两种是“最常用”那接下来这两种是“面试最爱考、实战最需谨慎”的。先说字典套列表也就是records风格rows_dicts [ {姓名: 张三, 城市: 杭州, 年龄: 25, 入职年份: 2021}, {姓名: 李四, 城市: 南京, 年龄: 30, 入职年份: 2020}, {姓名: 王五, 城市: 苏州, 年龄: 35, 入职年份: 2019}, ] df pd.DataFrame(rows_dicts) print(df)这种写法的灵活之处在于当你的数据本身是“一行一个字典”的结构比如 JSON 数组解析后的结果时直接传入即可pandas 会按字典的键自动对齐列名列的顺序默认按第一个字典的键序排列。那列表套字典和字典套列表到底有什么区别我整理了一张对照表对比维度字典套列表列表套字典数据结构外层字典键对应列名值为列表外层列表每个元素是一行字典直观程度列维度直观适合列多行少的场景行维度直观适合逐行追加的场景构建速度更快因为列数据批量赋值较慢因为需要逐行逐列查找键适用场景数据源是列式存储如 CSV 列数据源是行式 JSON 数组内存占用较低较高我自己的经验是列数较少且行数非常多时优先用字典套列表行数多、字段杂乱时优先用列表套字典。原因在于 pandas 构建时前者可以直接把整列数据一起处理减少了逐行解析的开销。1.4 通过pd.Series创建你真的会在实战中用到它吗很多人学了pd.Series创建 DataFrame 之后就忘在脑后了。我得老实说直接用 Series 创建 DataFrame 的频率确实不高但它在某些“动态拼接”的场景里非常有用。name_series pd.Series([张三, 李四, 王五], name姓名) city_series pd.Series([杭州, 南京, 苏州], name城市) df pd.DataFrame({name_series.name: name_series, city_series.name: city_series}) print(df)这段代码的核心技巧在于name属性充当了字典的键所以你再也不用额外写一遍字符串列名。当你的 Series 是程序动态生成的比如循环输出计算结果后转为 Series、再收集一批 Series 拼成大表这种写法就特别顺手。不过坦白说日常做数据分析、做报表、做特征工程Series 更多是作为 DataFrame 的单独一列存在极少需要把几个 Series 拼成一个新的 DataFrame。所以我把这个场景定义为“备选方案”——知道有这种写法即可不必强求掌握。1.5pd.read_csv()和pd.read_excel()外部文件读取的本质也是创建提起创建 DataFrame很多人第一反应是“手动构造”。但实际上你平时用的最多的创建方式是通过read_csv()和read_excel()这两个函数。它们的返回值本身就是一个崭新的 DataFrame底层也是调用了类似的构造逻辑。df pd.read_csv(sales_data.csv, encodingutf-8) df_excel pd.read_excel(sales_data.xlsx, sheet_nameSheet1, engineopenpyxl)很多人的误区在于以为read_csv只是“读文件”不是“创建”。其实从 pandas 的设计哲学来看读取外部文件是最重要的 DataFrame 来源。我建议所有初学者不仅要会read_csv还要刻意思考一个问题外部文件读进来之后列的数据类型是否符合预期。这一步经常是后续数据处理中所有怪异的 bug 的源头。read_csv有几个参数值得你反复记sep分隔符、encoding编码、dtype指定列的数据类型、parse_dates自动解析日期。尤其是dtype它能帮你省掉“读完之后手动 astype”的麻烦。1.6from_dict和from_records两个被低估的姐妹方法在 pandas 的 API 文档里pd.DataFrame.from_dict()和pd.DataFrame.from_records()是实例方法很多新手根本没注意到它们。它们和“直接用pd.DataFrame()传参”的区别主要在于额外参数的控制粒度。data_dict { 姓名: [张三, 李四, 王五], 入职年份: [2021, 2020, 2019], } df pd.DataFrame.from_dict(data_dict) print(df)from_dict最常用的一个参数是orient。默认是columns也就是键为列名如果设为index则键会被当成行索引你需要手动修改columns参数来指定列名。df pd.DataFrame.from_dict(data_dict, orientindex) print(df)此时输出变成一个横向表0 1 2 姓名 张三 李四 王五 入职年份 2021 2020 2019再来看from_records。它和“列表套字典”几乎等价但多了对索引和列名的精细控制。如果你有一些数据库游标返回的记录结构化行数据直接用from_records是最干净的。records [ (张三, 杭州, 25), (李四, 南京, 30), ] df pd.DataFrame.from_records(records, columns[姓名, 城市, 年龄]) print(df)看到区别了吗from_records特别适合元组列表形式的数据而普通pd.DataFrame()直接用元组列表也可以但from_records对类似 SQL 查询结果结构的数据处理更加干净利落。2. 创建过程中的数据类型推断为什么你以为的 int64 其实是 object2.1 pandas 内部的类型推断机制创建一个 DataFrame 远不只是“把数据摆进去”这么简单。pandas 在构建 DataFrame 时会对每一列的数据做类型推断。默认情况下它会遍历整列数据尝试判断应该是int64、float64、bool、object还是datetime64。举一个最常见的反直觉例子data {评分: [1, 2, 3, None]} df pd.DataFrame(data) print(df.dtypes)输出是评分 float64 dtype: object哪怕你原本心里想的是“这列明明是整数啊”因为包含了Nonepandas 会自动把整列升级为float64。从底层逻辑来讲pandas 需要用NaN浮点类型的缺失值标记来表示缺失而int64本身没有原生的 NaN 表示方式所以只能升级为 float64。如果你真的希望对整型列同时保留缺失值且保持 int 类型需要使用Int64Dtype注意大写 Idf pd.DataFrame({评分: [1, 2, 3, None]}, dtypeInt64) print(df.dtypes)输出就是评分 Int64 dtype: object这个Int64是 pandas 内置的“可空整数类型”它在底层用掩码来标记缺失区而不是用浮点的 NaN。它算是 pandas 近几个版本里最实用的一个改进。很多朋友在这个细节上翻车就是因为没搞明白 Python 的None和 pandas 的NaN并不是一个东西。2.2 混入字符串的那一列全变成 object 的必然结果另一个高频困惑是明明某一列数据看起来都是数字但dtypes显示是object。造成这种现象最常见的原因就是这一列里混入了一个字符串值。data { 价格: [100, 200, 300元, 400], } df pd.DataFrame(data) print(df.dtypes)结果价格列妥妥是object。出现这种情况时pandas 不可能自己去判断“如果去掉‘元’字就是数字”它只能选择最保守的处理方式——全部保留为通用对象形态也就是 Python 对象数组。那列表里真的混入字符串时怎么办给你两条处理路径一是先清洗数据把字符串中的元、美元等货币符号去掉再转类型二是直接靠pd.to_numeric()配合errorscoerce参数把无法转数字的值强行变成NaN。df[价格_clean] pd.to_numeric(df[价格].str.replace(元, ), errorscoerce) print(df)这段代码的意图是先通过.str.replace()把“元”去掉再用to_numeric()转成数字无法转换的置为 NaN。这样你就能继续做求和、均值等操作了。2.3dtype参数和astype()的正确使用姿势创建 DataFrame 时直接传dtype可以省掉后续转换操作。常见用法有两种要么整体指定要么用字典按列指定。df pd.DataFrame(data, dtypefloat64) df pd.DataFrame(data, dtype{年龄: int32, 薪水: float32})整体指定通常用于统一所有列的类型比如入模型之前把所有特征统一成 float32 节省内存。按列指定则更适合你已知每列类型的场景。如果你已经创建了 DataFrame那就要靠astype()了。这里有一个经验之谈调用astype()之后一定要记得赋值回去。astype()默认返回一个新对象不会原地修改原 DataFrame。很多初学者跑完代码发现dtypes没变就是因为忘了df df.astype(...)这一步。另外要提醒的是粗暴的astype(int64)在遇到缺失值的时候会直接报错。因为前面说了float64 里的 NaN 无法安全地转成 int64。所以如果你想在有缺失值的情况下转成整数优先选择astype(Int64)而不是到处找 workaround。3. 创建 DataFrame 时最容易踩的坑粘贴 csv 数据、索引列、内存占用3.1 复制的 Excel 表格数据总是变成一列我接到过不少次类似的提问“老师我从 Excel 复制了一段数据粘贴到 Python 字符串里再用 pandas 创建 DataFrame结果全挤在一列里了。”这个问题的核心在于当你直接粘贴多列数据时列与列之间其实是制表符\t分隔的而 Excel 的单元格内容如果本身带有空格看起来就会像“一整行文字”。正确的做法有两种第一种在字符串里用\t显式分隔各字段然后通过split(\t)拆分成行列表。第二种更简单粗暴直接在文本编辑器里把制表符替换成逗号再转为嵌套列表。raw_text 姓名 城市 年龄 张三 杭州 25 李四 南京 30 lines raw_text.strip().split(\n) data [line.split(\t) for line in lines] header data[0] rows data[1:] df pd.DataFrame(rows, columnsheader) print(df)这段代码其实是日常快速处理中非常实用的小工具。你只需要保证原始文案使用真正的制表符分隔。3.2indexFalse和index_col别让无关索引列混进你的数据集我发现一个真实且高频的坑用pd.read_csv()读取文件时pandas 默认会把第一列当作从 0 开始的Unnamed: 0列如果这份文件是由df.to_csv()导出时漏了indexFalse读回来就会多一列“索引”残留列。所以建议你从源头解决写文件时使用df.to_csv(output.csv, indexFalse)。而当你读别人给的含有索引列的文件时用index_col0作为参数让它成为行索引而不是数据列。df pd.read_csv(output.csv, index_col0)这样你的 DataFrame 就不会带着“Unnamed: 0”这种幽灵列到处跑了。另外手动创建DataFrame时如果用了旧 DataFrame 的索引也可能出现对齐问题。下面是一个经典的翻车案例df_old pd.DataFrame({数值: [100, 200, 300]}, index[a, b, c]) df_new pd.DataFrame({数值: [10, 20, 30]}, index[c, b, a]) df_combined pd.DataFrame({旧值: df_old[数值], 新值: df_new[数值]}) print(df_combined)输出结果里两个列会按照索引标签自动对齐而不会按位置拼在一起旧值 新值 a 100 30 b 200 20 c 300 10这就是 pandas 的索引对齐机制。如果你没意识到这一点可能会得到完全错位的数据。好在这个机制通常是你想要的但如果你真的想按原始顺序拼接必须手动忽略索引或使用.reset_index(dropTrue)。3.3 大 DataFrame 创建时的内存暴涨问题另一个很少在入门教程里被谈到的问题是内存优化。当你要创建一个几十万行、上百列的表时不同创建方式的性能差异会非常明显。我的建议有三条优先使用字典套列表的批量构建方式避免逐行append。逐行 append 在 pandas 历史上性能奇差现在虽然有所改善但pd.concat()依然是性能更好的选择。创建时直接通过dtype指定更紧凑的类型。比如能用int32就别用int64能用float32就别用float64。对于百万级数据省下来的是几十 MB 甚至上百 MB 的内存。用pd.DataFrame(..., copyFalse)避免不必要的复制。如果数据量达到亿万级建议放弃 DataFrame改用polars或duckdb这类更底层的方案。DataFrame 是方便但不是万能。4. 创建之后的切片、修改和转置一个容易被忽略的实战场景4.1.loc、.iloc和布尔索引创建完 DataFrame 后的第一步操作当你手工创建好一个 DataFrame 之后大概率马上就要做增删改查。这里快速说一下三种最常用的取值方式.loc[行标签, 列名]基于标签取数。.iloc[0, 1]基于位置取数[0 行第 1 列]。布尔索引比如df[df[年龄] 30]筛选年龄大于 30 的行。举个例子print(df.iloc[0, 2]) # 第一行第三列的值25 print(df.loc[0, 年龄]) # 标签为0的行年龄列的值25这里需要提醒一个容易踩的坑df[年龄]返回的是一个Series你无法直接对它进行多行多列操作。而df[[年龄, 城市]]返回的是一个新的 DataFrame注意这里用了双重中括号。很多人搞混这两者的返回类型后续操作就会出问题。4.2 新增列方法对比直接赋值、.assign还是.insert创建完 DataFrame 后你需要频繁地新增列。最简单的是直接赋值df[年龄分组] df[年龄].apply(lambda x: 青年 if x 30 else 中年)但如果想链式调用更多方法assign更合适df df.assign(明年年份lambda d: d[入职年份] 1)还有一个较少人关注但实用的insert方法它的作用不止新增列还可以指定新列插入的位置df.insert(loc1, column排名, value[1, 2, 3])三者的区别在于直接赋值最直觉、性能最高assign适合链式写法insert适合对列顺序有要求的场景比如为了导出美观的 Excel 报告。4.3 为什么要用pd.concat而不是循环 append最后也是最容易犯错的地方创建 DataFrame 时用循环 append。我见过有人这么写df pd.DataFrame() for i in range(10000): df df.append({a: i}, ignore_indexTrue) # 日常明确不推荐使用这么做的问题在于每次 append 都会重新分配内存、复制全部数据时间复杂度是 O(n^2)。到几万条时代码已经慢得像老牛拉车了。正确的思路分成两步先在循环里把部分结果收集到列表最后统一构建 DataFrame。temp_rows [] for i in range(10000): temp_rows.append((i, i * 2)) df pd.DataFrame(temp_rows, columns[序号, 两倍序号]) print(df.tail())这一步思维的转变是从“能用”到“高效”的分水岭。明白了为什么不能用 append再遇到大数据量时你就不会再犯这类性能错误了。5. 安装 pandas 的拦路虎清华源、版本冲突和常见报错全解析5.1 清华镜像源报错Could not find a version that satisfies the requirement pandas很多初学者在安装 pandas 时都会遇到这样一个报错ERROR: Could not find a version that satisfies the requirement pandas (from versions: none) ERROR: No matching distribution found for pandas这个报错信息看着挺吓人的但实际上 90% 的情况不是 pandas 本身的问题而是安装环境出了问题。最典型的原因有这么几个Python 版本太新或太旧官方源里找不到对应版本的 pandas wheel 包。默认的 PyPI 源在你当前网络环境下不稳定。系统的pip版本过旧导致无法正确解析所需轮子。缺少底层依赖如numpy的配套版本。手动配置的镜像源比如清华源暂时同步失败或地址写错。我的建议是先检查 Python 版本再看 pip 版本最后检查网络和镜像源。python --version pip --version然后直接用清华源安装并追加-i参数pip install pandas -i https://pypi.tuna.tsinghua.edu.cn/simple如果提示超时还可以加上--timeout 120pip install pandas -i https://pypi.tuna.tsinghua.edu.cn/simple --timeout 120顺便提醒一句不要因为一次失败就反复执行安装命令。建议先清理pip缓存再单独装依赖最后再装 pandas 本体。我自己碰到过很多次是pandas 依赖 numpy 的某个大版本而本地 numpy 版本过新导致 pandas 找不到匹配的版本组合。5.2 PyCharm 里手动安装 pandas 包的正确姿势PyCharm 用户最常用的安装方式有两种一种是在 Terminal 里执行pip install pandas另一种是通过 PyCharm 自带的包管理工具安装。在 PyCharm 的“Python Interpreter”设置里你可以直接搜索 pandas 并点 Install。这个方式的优点是可视化不用记命令缺点是你必须看清楚当前解释器是不是你项目对应的那个。很多人把系统 Python 和项目的虚拟环境 Python 搞混导致一个项目里装了另一个项目里找不到。我自己通常的做法是在 PyCharm 的 Terminal 里确认虚拟环境激活状态然后直接执行pip install pandas如果当前环境太慢就换成清华源用上面的命令安装。安装完成后在代码里执行import pandas验证一下。5.3 Python 3.13 等新版本下 pandas 安装的特殊应对如果你用的是比较新的 Python 版本比如 Python 3.12、3.13你可能会发现安装指令下去了但 pip 提示找不到 pandas 的对应 wheel 包。原因在于 pandas 的底层编译依赖非常重新版 Python 发布后pandas 的官方 wheel 往往需要几个月才能跟上。这时候有两条路第一检查是否有 commit 版本或预发布版本可安装比如pip install --pre pandas。第二如果不想等就把 Python 版本降到主流支持版本比如 3.10 或 3.11这两个版本是目前 pandas 生态兼容性最稳的。实测下来我的经验是不喜欢为了装一个包折腾太久的读者建议直接使用 conda 环境来管理。conda 的优点在于它不光管理 Python 包还会帮你处理底层依赖链上的二进制库版本。只要创建环境时指定好 Python 版本之后再conda install pandas基本是零折腾。我不鼓励一味追求最新版 Python。数据分析的稳定大于新奇。能用就行别给自己找不痛快。6. 最后的实操建议与个人心得写了这么多最后分享几条我自己多年实战沉淀下来的小经验希望你能直接带走用。第一创建 DataFrame 时先把列名想好再填数据。Python 不像 SQL 那样可以随时改表结构虽然 DataFrame 也能改但改列名、调列序、改 dtype 都要花额外的时间和算力。一次性把结构搭好后面省心很多。第二善用.info()和.dtypes检查手段。不管你是手动构建还是读取外部文件创建完 DataFrame 之后第一时间运行df.info()看一下有没有空值、看一下列类型是否符合预期。这个习惯可以帮助你在数据流程最开始就发现问题而不是等到下游统计时才发现 NaN 混在 int 列里导致全链路报错。第三遇到复杂的官方 API优先查阅help(pd.DataFrame)。很多朋友养成了“先百度再动手”的习惯但我更推荐“先试再查”。pandas 官方文档写得非常清楚尤其是参数默认值、属性返回类型这些细节。你花三分钟读一下文档往往比在网上找十分钟的答案要高效得多。第四控制数据规模意识。用 pandas 创建大表时有条件就提前指定dtype和copyFalse数据一旦超过内存极限果断换方案不要硬扛。它不是万能的但只要你了解它的边界它就是你做数据分析最顺手的一把刀。最后再分享一个特别实用的小技巧当你从接口拿到 JSON 数据而 JSON 的嵌套结构很复杂时不要直接扔给pd.DataFrame()而是先用json_normalize把嵌套字段展开再创建 DataFrame。这样你得到的是一个规整的平面表后续处理、可视化的难度都会大幅降低。import json from pandas import json_normalize raw_json [ {姓名: 张三, 地址: {城市: 杭州, 区: 西湖区}}, {姓名: 李四, 地址: {城市: 南京, 区: 玄武区}}, ] df pd.json_normalize(raw_json) print(df)输出就很漂亮姓名 地址.城市 地址.区 0 张三 杭州 西湖区 1 李四 南京 玄武区这种“一步到位”的做法能帮你省掉后续拆字段的大量手写逻辑。说实话DataFrame 的创建方法翻来覆去就那么多真正决定一个人水平高低的往往不是他会不会pd.DataFrame()而是他在实际操作中遇到异常数据、异常类型、异常结构时能不能快速定位并优雅处理。希望这篇文章能帮你把基础打得更扎实让你在之后的每一步数据处理中都走得更有底气。
阅读完成 · 觉得有帮助?
咨询建站