ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

pandas与numpy的区别:从数据结构、索引到性能与选型指南

pandas与numpy的区别:从数据结构、索引到性能与选型指南 1. 一句话说不清的区别先别急着背概念看实际场景在数据分析相关的问题下pandas和numpy的区别大概是新手问得最多的一个。很多人在学Python之前根本没接触过“数组”和“表格”这两个词结果一上来就同时看到两个包名脑子里全是浆糊既然都能存数据为什么还要分两个我先说一个最常见的场景大家感受一下。假设你手上有一份CSV文件内容是某电商平台一周的订单记录包括订单号、用户ID、商品名称、单价、数量、下单时间这几列。你想算一下这周的总销售额是多少。用pandas做的话代码大致是这样import pandas as pd df pd.read_csv(orders.csv) df[销售额] df[单价] * df[数量] total df[销售额].sum() print(total)如果用numpy从头做就麻烦多了。你得先解析CSV把每一列取出来。而且商品名称、用户ID这种文本列和单价、数量这种数值列numpy处理起来并不顺手import numpy as np # 假设已经手动读出了三列价格、数量 # 如果这两列不是float类型还得先转换 prices np.array([…]) # 手动填充 quantities np.array([…]) # 逐元素相乘 sales prices * quantities total np.sum(sales) print(total)看到区别了吗pandas自带列的概念你可以直接用“单价”这个名字去引用那一列还能让两列按名字对齐相乘numpy只有“数组”这个概念它处理的是一堆数值的集合至于这一堆数值在业务上叫什么名字它不关心。但反过来说如果一个任务纯粹是“把一个 3×3 矩阵和另一个 3×3 矩阵做点积”用pandas写会显得很别扭用numpy就是几行代码的事。因为numpy生来就是算数学的矩阵运算、向量运算、广播机制统统是它的看家本领。所以一句话结论numpy 提供的是“高效的多维数组计算能力”pandas 提供的是“方便处理表格数据的高层接口”。pandas 的底层数据存储靠的其实就是 numpy 数组没有 numpy 就没有 pandas。但 pandas 在数组外面又包了一层“表格语义”行名、列名、索引对齐、缺失值处理、分组聚合、时间序列……这些都是 numpy 没有的。明白这两者的分工之后接下来的问题就是具体差在哪哪些差异会影响日常写代码我按自己平时踩过的坑逐个讲。2. 核心对象的三个维度对比结构、索引和类型约束2.1 结构差异ndarray、Series与DataFrame的分层设计numpy 最核心的对象叫ndarrayn-dimensional array多维数组。它就像一盒排列整齐的格子数据必须是同一类型的。虽然 numpy 也支持二维数组、三维甚至更高维但它没有“列名”也没有“行名”你只能用下标去访问一个格子比如arr[0, 1]。pandas 则有两个核心对象。一个是Series可以理解成一列数据它是一维的自带索引另一个是DataFrame可以理解成多列 Series 拼成的一张二维表每一列有列名每一行有行索引。举一个最直观的例子我们创建一个 numpy 数组再创建同一个数据的 pandas DataFrameimport numpy as np import pandas as pd # numpy arr np.array([[1, 2, 3], [4, 5, 6]]) print(arr[0, 1]) # 2 # pandas df pd.DataFrame( {A: [1, 4], B: [2, 5], C: [3, 6]}, index[第一行, 第二行] ) print(df.loc[第一行, B]) # 2在 numpy 里arr[0, 1]的意思是“第0行第1列”这纯粹是位置。而在 pandas 里你可以用df.loc[第一行, B]这种“行名列名”的方式取值也可以用df.iloc[0, 1]按位置取值。pandas 给了你两套访问方式一套按标签loc一套按位置ilocnumpy 只有位置这一套。这带来的第一个直接影响是代码可读性。看到df[单价] * df[数量]不用看数据就知道在算什么看到arr[:, 2] * arr[:, 3]你还得回头查第2列和第3列到底是什么。业务代码里标签索引带来的可读性提升远比“稍微多写几个字符”更重要。2.2 索引默认整数下标与能折腾的标签索引索引是 pandas 和 numpy 之间最大的设计分水岭。numpy 的“索引”是固定的第0轴是行第1轴是列下标从0开始。你要切片就写arr[1:3, 0:2]这是纯位置的切片跟内容是谁没有任何关系。pandas 的索引是用户自定义的。你可以在创建 DataFrame 的时候指定 index哪怕是字符串也行。更关键的是pandas 提供了一系列“对齐”能力两个 DataFrame 做加法时会按行索引和列索引自动对齐索引相同的位置才相加索引不同的位置变成缺失值。举一个让我印象深刻的例子。假设我有两个 DataFrame分别记录了周一和周二某商品的销量但行顺序完全不一样df1 pd.DataFrame({销量: [10, 20]}, index[A店, B店]) df2 pd.DataFrame({销量: [30, 40]}, index[B店, A店]) print(df1 df2)pandas 的结果是销量 A店 40 B店 50它自动把“B店的20”和“B店的30”加在一起而不是按照位置把第0行和第0行相加。这在实际业务中太有用了因为很多时候你并不知道两个表的数据行顺序是否完全一致按标签对齐能避免一堆低级错误。但这也带来了一个“陷阱”如果你没意识到 pandas 在做自动对齐就可能得到意外结果。比如两个 DataFrame 有相同的列但索引不重合相加之后会出现大量 NaN。而 numpy 永远只做位置运算arr1 arr2就是对应位置相加行顺序不同你就只能自己处理手动排序、手动对齐代码又脏又容易错。所以说索引机制的本质区别是numpy 把数据当作“数学对象”pandas 把数据当作“账本对象”。账本必须知道每一行是哪个店铺的记录数学对象只需知道它在第几行。2.3 类型约束同质数组与异构表格的取舍numpy 数组有一个硬性约束一个数组只允许一种数据类型。整型就是整型浮点就是浮点字符串就是字符串。如果往整型数组里塞一个浮点数要么自动转成浮点要么报错。这个约束对数学计算来说是优点因为同类型数据在内存里连续排列CPU 可以直接按固定步长遍历效率极高。但放到真实业务里就是缺点一份订单数据里既有订单号字符串、又有单价浮点、还有数量整数numpy 没法把它们放进同一个“表格”里最多只能 cast 成object类型。一旦变成object类型性能优势就基本消失了。pandas 的 DataFrame 可以每列一个类型。订单号那列是字符串单价那列是浮点数量那列是整数互不干扰。这一点是我认为 pandas 在“数据分析领域”能碾压 numpy 的根本原因真实数据从来不是清一色的数值pandas 允许异构等于允许你直接把真实世界的表搬进 Python 里。但要注意pandas 的“异构”是有代价的。每一列是一个独立的 numpy 数组内部还是要求同质DataFrame 靠列名把这些数组拼在一起。如果你把一个万行级别的 DataFrame 里的某列全转成object类型来存字符串那这一列的内存开销和性能都会比纯数值列更差。所以实践中能用数值表示的状态尽量不存成字符串这既是 pandas 的性能经验也是 numpy 的使用习惯。3. 被忽略但真正影响工作的隐性区别这一部分的内容在我刚学的时候基本没人讲全是在实际工作中踩了坑之后才反应过来。我一个个说。3.1 缺失值处理NaN、None 与真正的“缺失”概念numpy 对“缺失值”的支持非常有限。它有一个特殊的浮点数np.nan可以用来占位但有几个问题。第一np.nan是浮点类型。如果你有一个整型数组想用np.nan表示缺失numpy 会强制把整个数组转成浮点类型。看下面这个例子import numpy as np arr np.array([1, 2, 3]) arr[1] np.nan # 结果是 [1. nan 3.]整个数组变 float这在很多场景下是不想要的。年龄、订单数量这类整数列明明业务上不可能是浮点但为了存一个“缺失”整个列都变成浮点后面所有计算都得小心。第二numpy 的nan一旦参与计算结果会很“毒”。np.sum([1, np.nan])结果是nan不是 1。你如果不做np.nan_to_num或者先过滤整个聚合结果就废了。pandas 在这方面做得细致得多。它提供了pd.NA作为真正的缺失标记并且区分了NaN浮点缺失和None对象缺失。更实用的是pandas 的计算函数默认会跳过缺失值df[价格].sum()会自动忽略 NaN返回非缺失值的总和df[价格].mean()也会自动忽略 NaN。这省了非常多的预处理步骤。不过这里也有一个容易犯的错pandas 的“自动跳过”只对内置聚合函数有效。如果你写的是df[价格] 1缺失值不会自动变成“不参与计算”它会把 NaN 传播下去。所以该用fillna的时候还是得用fillna。3.2 数据对齐看似贴心实则也有坑前面提到 pandas 按索引自动对齐这是它最“智能”的地方但也可能是最容易出 bug 的地方。我印象很深的一次失误一个 DataFrame 的索引是默认的0, 1, 2, …另一个 DataFrame 的索引是经过排序、筛选之后变成的2, 5, 7, …。我对两者做减法时pandas 没有报错结果里几乎所有位置都是 NaN因为索引能匹配上的只有少数几行。我当时盯着结果看了好一阵才反应过来是自己忘了重置索引。用 numpy 就不会有这个问题因为 numpy 没有“对齐”这个机制位置对不上就直接按对应位置算根本不管谁是谁。问题在于如果两个 numpy 数组的行顺序真的不一样你得到的运算结果就是错误的而不只是 NaN属于“错得悄无声息”这可能更危险。所以我的建议是在 pandas 里做 DataFrame 之间的运算时先确认索引是否是你想要的如果只是位置运算先df.reset_index(dropTrue)再操作。而用 numpy 的时候反而要主动写清索引或列名在哪个位置避免出现两个数组含义不同的情况。3.3 广播与轴方向概念相似但方向经常搞反这两个库都支持“广播”broadcasting含义都是“维度不完全匹配时自动扩展”。但 pandas 的广播规则和 numpy 不完全一样。numpy 的广播规则是数学的从最后一个维度开始比较后一个维度相等或为 1 就可以扩展。例如(3, 1)数组和(3,)数组相加前者会在第二维上扩展成 3 列后者会扩展成 3 行。pandas 的广播规则还额外受列名和索引影响。DataFrame 和 Series 运算时如果 Series 的索引和 DataFrame 的列名不完全一致pandas 会执行“索引对齐式广播”跟 numpy 的纯位置广播完全不是一回事。一个非常常见的例子df pd.DataFrame({A: [1, 2], B: [3, 4]}) s pd.Series([1, 2], index[A, B]) print(df - s)这段代码的意义是每一列减去对应的“列均值”结果符合直觉。但如果 Series 的索引顺序反了只要索引名对得上pandas 也会给你正确的对齐numpy 则完全不管这些它只按位置做减法。这些隐性差异对新手很不友好因为很多时候代码不报错结果却“看起来不对”。要真正区分 pandas 和 numpy 的差异核心不在于背熟 API而在于理解“numpy 处理的是位置化的数值pandas 处理的是带标签的表格”。4. 性能与内存什么时候该用 numpy、什么时候该用 pandas4.1 谁更快减少开销的代价和收益很多人问“pandas 和 numpy 谁快”正确答案比较微妙纯数值计算上 numpy 快涉及表格组织、分组聚合等操作时 pandas 反而省事而且不一定慢。numpy 快的原因在于它直接操作连续内存中的同类型数组能充分利用 CPU 缓存和向量化指令。而 pandas 在每一层都有额外的开销索引对象、列元信息、数据类型检查、对齐逻辑……这些在亿级数据上会积累成明显差距。我做过一个简单测试两个长度为 1000 万的数组相加numpy 用时大约 20 毫秒pandas 两个 Series 相加大约 60 毫秒。差距 3 倍左右。如果只是做一次加法这点时间无所谓但在循环里跑几百次差距就出来了。但是如果比较的是“读 CSV、按某一列分组、求每组均值”这类完整流程pandas 的groupby是 C 实现的速度和易用性平衡得很好。你用 numpy 手动实现分组光写循环就慢得不行还会把代码写成天书。所以我的经验是能用 pandas 高层接口解决问题时不要为了“性能”擅自把数据拆成 numpy 数组。只有当计算本身是纯数值、数据规模大、并且你已经定位到瓶颈的确切位置时才切到 numpy。4.2 内存开销的来源索引、字符串类型和复制策略pandas 比 numpy 吃内存这是很多人逐渐感受到的另一个差异。最大的开销来源有三个。第一是索引。DataFrame 的每一行和每一列都带着索引对象如果是字符串索引内存开销更大。第二是字符串类型。pandas 的object列存储的是指向 Python 字符串对象的指针每个字符串对象本身还带类型头、引用计数等额外信息。同样是一万个“北京”这个字符串numpy 如果是固定长度 bytes 会省很多空间pandas 则每个元素都是独立对象。第三是复制。pandas 的某些操作比如按条件筛选会生成新的内存副本不像 numpy 切片那样容易做到视图形视图。这方面有一个实用的优化技巧如果 DataFrame 里有大量重复的字符串值可以先把它们转成category类型pandas 会为每个唯一值建一个字典数据内部只存整数编码内存占用大幅下降。这是纯 numpy 很难做到的“业务级优化”。4.3 混合使用的最佳实践用 numpy 算用 pandas 组织既然两个库各有所长最好的方案不是二选一而是让它们协同工作。我日常的写法通常是这样import pandas as pd import numpy as np df pd.read_csv(data.csv) # 用 pandas 完成数据清洗 df df.dropna(subset[价格]) df[时间] pd.to_datetime(df[时间]) # 切出需要的列转成 numpy 数组做核心计算 prices df[价格].to_numpy() quantities df[数量].to_numpy() revenue np.dot(prices, quantities) # 再把结果放回 DataFrame df[收入] prices * quantities用to_numpy()在 pandas 和 numpy 之间切换本质上是在“表格语义”和“数值计算语义”之间切换。清洗阶段、EDA 阶段用 pandas 的接口读列名、做筛选效率很高进行矩阵运算、向量内积、大规模线性代数时切到 numpy内存更紧凑速度更快。还有一个高频场景是在 pandas 里做条件逻辑时配合 numpy 的np.wheredf[状态] np.where(df[库存] 0, 有货, 无货)这个比用 apply 写 Python 循环快得多因为np.where是向量化实现的。5. 选型与协同一个真实数据分析任务中的分工我拿一个完整的例子把上面说的差异串起来。假设要分析某公司 100 万条销售记录字段包括 date日期、category商品类别、price单价、units销量。第一步用 pandas 读入数据df pd.read_csv(sales.csv, parse_dates[date])这个阶段直接“读成 DataFrame”因为你需要列名、日期解析、缺失值处理这些能力。第二步用 pandas 做筛选和聚合# 只保留 2024 年数据 df df[df[date].dt.year 2024] # 按类别求总销售额 df[销售额] df[price] * df[units] result df.groupby(category)[销售额].sum().reset_index()第三步把聚合后的数据转成 numpy 数组做进一步处理categories result[category].to_numpy() sales result[销售额].to_numpy() # 找出销售额最高的类别 top_index np.argmax(sales) print(categories[top_index])也可以用 numpy 计算各类之间的销售占比total_sales sales.sum() shares sales / total_sales如果把第 2 步和第 3 步都强行用 numpy 做你就得自己维护一个类别列表、一个数值列表还要自己写按类别分组合并的逻辑。而用 pandas 自带的groupby一行就解决了。反过来如果你手上本来就是一个 10000×5000 的 float 矩阵要做 PCA、做矩阵求逆、算特征值直接np.linalg.eigh(cov_matrix)就行没必要先塞进 DataFrame。DataFrame 会把矩阵拆成 5000 个 Series在性能和 API 上都显得笨重。所以选型逻辑应该是数据有列名、有行索引、有缺失值、需要按某个字段分组 → pandas数据是纯数值、矩阵形态、要做线性代数 → numpy。两者不是竞争关系而是上下游关系。6. 回到最初的问题我平时怎么跟人解释这件事每次有同事或群友问“pandas 和 numpy 的区别到底是什么”我很少直接抛定义而是喜欢用生活里的类比。假设你有一本记账本里面一页是日期、一页是项目名、一页是金额。numpy 相当于把记账本里所有金额的数字抽出来排成一条长列表然后用数学工具去算总和、平均值、方差。这些数字在账本第几页是什么项目它不关心。pandas 相当于整个记账本保留每一列的标题还能让某几页的数字自动按标题对齐随时可以按“项目名等于 XX”这个条件把对应的行筛出来再把筛选后的金额送进 numpy 去计算。为什么需要 numpy 这种“只看数字的东西”因为一旦涉及大规模矩阵运算带标签的账本太重了。你不想在每次算矩阵乘法时都带着一千个列名跑来跑去。所以 pandas 包着 numpy照顾业务numpy 做计算照顾性能。两者一个负责“方便”一个负责“快”。从学习顺序来说我依然建议先花一两周把 numpy 的基本概念摸熟尤其是 ndarray、切片、广播和向量化思维。因为 pandas 的底层也是这些概念理解了 numpy 的“位置计算”之后再看 pandas 的“标签计算”才能明白 pandas 到底在 numpy 之上加了一层什么。反过来如果一上来就抱着 pandas 狂学很容易在什么时候该用.loc、什么时候该用.iloc、为什么索引会神奇对齐这些细节上绕晕。最后送大家一个小建议写代码时不要盯着“哪个库更高级”而是盯住自己的数据形态。数据是表格就用 pandas 处理数据是矩阵就切到 numpy 算两个都沾边就在两者之间用to_numpy()和pd.DataFrame()干脆利落地来回切。真正常用的其实就这么几个接口剩下的都是在这两个语义之间反复横跳时积累出的经验。
返回列表