☰
Python 数据处理入门(二)Series 与 DataFrame:把一张表格装进内存
2026/10/15 7:11:50 网站建设 项目流程

先确认示例数据在手边:

import os import mkdata # 第 1 篇里的 mkdata.py ​ if not os.path.exists("data/sales.csv"): mkdata.main() # 没生成过就先跑一次 print("示例数据就绪:", os.path.exists("data/sales.csv"))

这段会检查第 1 篇生成的data/目录在不在,不在就自动补一次;已经有数据时只会打印一行示例数据就绪: True,不影响后面的结果。

写在前面

本系列已更新到第 2 篇。

上一篇我们把 Pandas 装上、跑通了第一个程序,还生成了后面 11 篇要用的data/示例数据。这一篇解决一个更底层的问题:Pandas 里的表格到底是什么东西。搞不清这一点,后面每一次报错你都只能靠猜;搞清了,取数、改数、算数基本就是顺手的事。

一、Series:一列数据 + 一排标签

一句话:Series 就是带标签的一列数据。左边是标签(索引),右边是值,每个值都能被自己的标签叫出来。

import pandas as pd ​ # 数据 + 索引(标签),顺手起个名字 s = pd.Series([120, 95, 60, 88], index=["杭州", "上海", "北京", "广州"], name="销量") print(s) print("----") print("索引:", s.index.tolist()) print("数值:", s.values) print("按标签取一个:", s["上海"]) print("按标签取多个:", s[["杭州", "广州"]].tolist()) print("合计:", s.sum())
杭州 120 上海 95 北京 60 广州 88 Name: 销量, dtype: int64 ---- 索引: ['杭州', '上海', '北京', '广州'] 数值: [120 95 60 88] 按标签取一个: 95 按标签取多个: [120, 88] 合计: 363

打印结果左侧是标签、右侧是值,底部的Name: 销量是它的名字,dtype: int64是数值类型。注意最后两行的取值写法:取一个用一层方括号,取多个要再套一层方括号。这个"多一层方括号表示多选"的规则,DataFrame 完全一样,先记牢它。

二、用字典造一张 DataFrame

DataFrame 可以理解成共用同一套行索引的好几列 Series 拼起来的一张表。最快的造表方式是给一个字典:键是列名,值是这一列的内容。

import pandas as pd ​ df = pd.DataFrame( { "城市": ["杭州", "上海", "北京", "广州"], "销量": [120, 95, 60, 88], "单价": [49.9, 29.9, 89.0, 599.0], } ) print(df.to_string(index=False)) print("----") # 另一种常见写法:列表套列表,再手动指定列名 df2 = pd.DataFrame([["杭州", 120, 49.9], ["上海", 95, 29.9]], columns=["城市", "销量", "单价"]) print(df2.to_string(index=False)) print("----") print("index 行标签:", df.index.tolist()) print("columns 列名:", df.columns.tolist()) print("shape(行数, 列数):", df.shape) print("values 纯数据(只取数值列):") print(df[["销量", "单价"]].values)
城市 销量 单价 杭州 120 49.9 上海 95 29.9 北京 60 89.0 广州 88 599.0 ---- 城市 销量 单价 杭州 120 49.9 上海 95 29.9 ---- index 行标签: [0, 1, 2, 3] columns 列名: ['城市', '销量', '单价'] shape(行数, 列数): (4, 3) values 纯数据(只取数值列): [[120. 49.9] [ 95. 29.9] [ 60. 89. ] [ 88. 599. ]]

三个地方值得停一下:

  • 字典里每列的列表长度必须一样,少写一个值就会报ValueError: All arrays must be of the same length,这是新手最高频的造表报错;

  • index/columns/values是看骨架的三件套:前两个是行标签和列名,values是去掉标签后的纯数据(numpy 数组)。整张表混了文本时values会退化成 object 数组,所以一般只对数值列用它;

  • to_string(index=False)是为了打印时不显示左边那列 0、1、2 的行号,看起来更像 Excel。

三、取一列、取多列

取列只有两种写法,区别就在方括号的层数上。

import pandas as pd ​ df = pd.DataFrame( { "城市": ["杭州", "上海", "北京", "广州"], "销量": [120, 95, 60, 88], "单价": [49.9, 29.9, 89.0, 599.0], } ) # 一层方括号:取一列,得到 Series col = df["城市"] print("df['城市'] 是", type(col).__name__, ":", col.tolist()) # 两层方括号:取多列,得到 DataFrame two = df[["城市", "销量"]] print("df[['城市', '销量']] 是", type(two).__name__, ":") print(two.to_string(index=False))
df['城市'] 是 Series : ['杭州', '上海', '北京', '广州'] df[['城市', '销量']] 是 DataFrame : 城市 销量 杭州 120 上海 95 北京 60 广州 88

df["城市"]是 Series,df[["城市"]]是只有一列的 DataFrame。什么时候必须区分?当你要接着调head()、to_excel()这类"表"才有的方法时,就得是 DataFrame;只是看数值、求和、比较,Series 更省事。

四、加一列、删一列:列与列直接算

给一个不存在的列名赋值就是新增一列;更舒服的是,列与列之间能直接做四则运算,不用写 for 循环。

import pandas as pd ​ df = pd.DataFrame( { "城市": ["杭州", "上海", "北京", "广州"], "销量": [120, 95, 60, 88], "单价": [49.9, 29.9, 89.0, 599.0], } ) # 新增一列:两列直接相乘,整列一起算 df["金额"] = (df["销量"] * df["单价"]).round(2) print(df.to_string(index=False)) print("----") # 删除列:drop 默认返回新表,不动原表 df.drop(columns=["单价"]) print("只写 drop 不赋值,列还在:", df.columns.tolist()) df_new = df.drop(columns=["单价"]) print("赋值给新变量之后:", df_new.columns.tolist()) print("----") # 删除行用 index 参数 print(df.drop(index=[0, 2]).to_string(index=False))
城市 销量 单价 金额 杭州 120 49.9 5988.0 上海 95 29.9 2840.5 北京 60 89.0 5340.0 广州 88 599.0 52712.0 ---- 只写 drop 不赋值,列还在: ['城市', '销量', '单价', '金额'] 赋值给新变量之后: ['城市', '销量', '金额'] ---- 城市 销量 单价 金额 上海 95 29.9 2840.5 广州 88 599.0 52712.0

这段有两个新手必踩的点:一是df["金额"] = ...是直接改原表,而drop是返回新表,不赋值等于白删;二是整列相乘靠的是向量化运算,几十万行也是一瞬间,千万不要用 for 循环一行行去算。

五、loc 与 iloc:按名字取,还是按位置取

规则一句话:loc 认标签,iloc 认位置。两者的写法都是[行, 列],只写行就是取整行。

import pandas as pd ​ df = pd.DataFrame( { "城市": ["杭州", "上海", "北京", "广州"], "销量": [120, 95, 60, 88], "单价": [49.9, 29.9, 89.0, 599.0], } ) df["金额"] = (df["销量"] * df["单价"]).round(2) ​ print("默认索引是 0、1、2、3 时,loc[1] 和 iloc[1] 看起来一样:") print(df.loc[1].to_string()) print("单个格子:", df.loc[1, "金额"]) print("----") print("切片就分道扬镳了:loc[0:2] 是", df.loc[0:2].shape[0], "行(含右端点),iloc[0:2] 是", df.iloc[0:2].shape[0], "行(不含)") print("----") city_df = df.set_index("城市") print("把城市设成索引后,loc 可以直接用城市名取:") print(city_df.loc[["杭州", "广州"], ["销量", "金额"]].to_string()) print("iloc 依然按位置取第 2 行:") print(city_df.iloc[1].to_string()) print("----") df3 = df.copy() df3.index = [10, 20, 30, 40] print("索引改成 [10, 20, 30, 40] 后,loc[10] 取到:", df3.loc[10, "城市"]) print("索引里还有标签 0 吗:", 0 in df3.index.tolist())
默认索引是 0、1、2、3 时,loc[1] 和 iloc[1] 看起来一样: 城市 上海 销量 95 单价 29.9 金额 2840.5 单个格子: 2840.5 ---- 切片就分道扬镳了:loc[0:2] 是 3 行(含右端点),iloc[0:2] 是 2 行(不含) ---- 把城市设成索引后,loc 可以直接用城市名取: 销量 金额 城市 杭州 120 5988.0 广州 88 52712.0 iloc 依然按位置取第 2 行: 销量 95.0 单价 29.9 金额 2840.5 ---- 索引改成 [10, 20, 30, 40] 后,loc[10] 取到: 杭州 索引里还有标签 0 吗: False

最后两行是重点:索引变成 10 开头之后,df3.loc[0]会直接报KeyError: 0,因为它找的是"标签 0",而表里根本没有这个标签;这时只有iloc[0]能拿到第一行。只要索引不是默认的 0、1、2、3,用 loc 就必须写真实标签——从 Excel 读进来带了自定义索引的表,最容易在这里翻车。

六、索引对齐:NaN 是怎么冒出来的

Pandas 最容易让人懵的行为来了:两个 Series 相加时,它先按标签配对,再相加,对不上的标签直接给 NaN。它不看顺序,只看标签。

import pandas as pd ​ s1 = pd.Series([1, 2, 3], index=["杭州", "上海", "北京"]) s2 = pd.Series([10, 20], index=["上海", "广州"]) print(s1 + s2) print("----") # 给表加一列时也是按标签对齐:顺序打乱也能对上 df = pd.DataFrame({"销量": [120, 95, 60]}, index=["杭州", "上海", "北京"]) price = pd.Series([89.0, 49.9, 29.9], index=["北京", "杭州", "上海"]) df["单价"] = price print(df.to_string()) print("----") # 索引里多了个表里没有的"成都",对不上就是 NaN df["单价2"] = pd.Series([49.9, 29.9], index=["杭州", "成都"]) print(df.to_string())
上海 12.0 北京 NaN 广州 NaN 杭州 NaN dtype: float64 ---- 销量 单价 杭州 120 49.9 上海 95 29.9 北京 60 89.0 ---- 销量 单价 单价2 杭州 120 49.9 49.9 上海 95 29.9 NaN 北京 60 89.0 NaN

s1 + s2只有"上海"两边都有,所以只有它算出 12.0,其余三行全是 NaN。第二段里price的顺序被故意打乱,但对齐之后 49.9 依然准确落在杭州——对齐认标签,不认顺序。第三段最贴近实战:从别处拿来的 Series 索引和主表对不上,加进去就是一片 NaN。所以后面第 4 篇做数据探查时要专门数一遍缺失值,第 6 篇还要分清"本来就缺"和"对不齐造出来的缺"。

七、拿 sales.csv 对照一遍

内存里的小表看懂了,换成真表来一遍。这次我们读第 1 篇生成的销售明细,用列运算把它原有的金额列验算一遍。

import pandas as pd ​ sales = pd.read_csv("data/sales.csv", encoding="utf-8-sig") print("列名:", sales.columns.tolist()) print(sales[["产品", "数量", "单价", "金额"]].head(5).to_string(index=False)) print("----") # 列与列相乘:自己重算一遍金额 sales["重算金额"] = (sales["数量"] * sales["单价"]).round(2) bad = sales[sales["金额"] != sales["重算金额"]] print("和原金额对不上的行:") print(bad[["订单日期", "产品", "金额", "重算金额"]].to_string(index=False))
列名: ['订单日期', '城市', '产品', '数量', '单价', '销售员', '金额'] 产品 数量 单价 金额 A-笔记本 3 49.9 149.7 B-鼠标 5 29.9 149.5 B-鼠标 5 29.9 149.5 C-键盘 2 89.0 178.0 A-笔记本 4 49.9 199.6 ---- 和原金额对不上的行: 订单日期 产品 金额 重算金额 2024-02-11 B-鼠标 NaN 179.4 2024-04-18 D-显示器 NaN 1198.0 2024-05-20 D-显示器 -599.0 2995.0

真表里正好三种情况都齐了:两行原金额是空的(NaN),一行金额是负数(录入错了),而它们用「数量 × 单价」重算出来的数字都没问题。这也说明 DataFrame 的列运算不只是算数,还能顺手当数据质量检查用。

常见问题

Q1:df[0]为什么取不到第一行?

方括号里写整数时,Pandas 会把它当列名去查,报KeyError: 0。想按位置取第一行,用df.iloc[0];想取某一列,用df["列名"]。记住:方括号里是列名,不是行号。

Q2:df.drop(columns=["单价"])执行了,为什么表没变?

drop、head、sort_values这些方法默认返回新对象,不改原表。想留下结果就赋值:df = df.drop(columns=["单价"])。养成"返回新表 → 赋值给变量"的习惯,比记一堆原地修改的参数省心得多。

Q3:为什么我的dtypes显示str,文章里是object?

object是 Pandas 里"Python 字符串对象"的经典写法,早期版本统一这么显示;pandas 3.x 起文本列默认换成了新的str类型,于是在新版本上跑就会看到str。两者是同一个意思,在.str字符串方法、比较运算、read_csv上的用法完全一样,看到哪个都不用慌。真要判断"这列是不是文本",稳妥的写法是select_dtypes(exclude="number"),第 4 篇会用到。

小结

这一篇把 Pandas 的两个核心对象摸了一遍:Series 是带标签的一列,DataFrame 是共用一套行索引的多列;取列靠方括号的层数,加列靠赋值,删列删行靠drop;loc 认标签、iloc 认位置;而所有跨对象的运算都会先按标签对齐,对不上就填 NaN。

这几个规则看着简单,但它们是后面筛选、清洗、分组、合并的地基——你之后遇到的绝大多数"莫名其妙的结果",都能从这几条里找到解释。

下一篇我们聊怎么把 CSV 和 Excel 文件正确地读进来:编码、路径、表头、只读部分列、多 sheet、大文件分块,一次讲清。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询