1. 为什么拿学生成绩单来教DataFrame索引
凡是入门Pandas的人,几乎都绕不开DataFrame的行列索引。但很多教程一上来就用股票数据、电商订单这类抽象表格,新手还没搞清楚索引和列名是怎么回事,就被各种术语砸晕了。我带过的学员里,十个有八个都栽在同一个问题上:loc和iloc到底啥区别?为什么有时候用冒号切片,结果和别人不一样?还有一半人搞不清楚删除行和删除列时axis该写0还是1。
别急,这些问题用学生成绩单来理解,全都迎刃而解。
成绩单这种数据有天然的教学优势。第一,它足够生活化——每个人都经历过考试,知道成绩单长什么样:行是一个个学生,列是一门门科目,左上角是学号和姓名。第二,它的行和列语义极清晰——行索引是学生,列名是科目,没有复杂的时间序列格式,没有任何嵌套结构。第三,它天然带有“索引”的影子——现实中我们查成绩,要么按学号找(这就是标签索引),要么按排名找(这就是位置索引)。
所以拿成绩单做教学载体,不是随便选的,而是刻意为之:让表格结构、索引含义、操作语义三者直接对应,把抽象概念翻译成直观经验。
这篇内容围绕一份约200行的高一某班成绩数据,完整演示DataFrame的索引机制,包括:如何基于真实数据构造DataFrame、列索引(取列、加列、删列、改名)的完整玩法、行索引(loc、iloc、布尔筛选、排序后重排索引)的底层原理与实操区别,最后做一个综合性成绩分析案例,把索引操作串到真实工作流里。适合刚学过Series和DataFrame基本概念、想快速掌握行/列索引操作的同学,也适合那些用Pandas有一阵子但总在切片和筛选上怀疑人生的从业者。
2. 搭好环境,先把一份像样的成绩单构造出来
2.1 构造DataFrame的四种姿势
教索引之前,先在理解层把DataFrame是怎么来的搞清楚。构造一份成绩单,最常见的方式有四种,对应不同的数据来源场景。
第一种,用字典加列表。这种方式最直观,每一列是键,列的值是整个列表。比如把各科成绩放进字典,传给pd.DataFrame即可。第二种,用NumPy二维数组加列名。适合已有二维数组、想临时加上列名的场景,速度最快,但没有列名映射。第三种,从CSV或Excel读取。这是实操中最常用的方式,pd.read_csv或pd.read_excel直接读文件。第四种,用列表套字典。每一行是一个字典,适合逐条追加记录的真实业务场景。
这里有个细节值得注意:字典的键会被默认当成列名,字典的插入顺序在Python 3.7以后有保证,所以DataFrame的列顺序会按你写字典的顺序排列。但如果是读CSV进来,列顺序取决于文件的表头顺序,这一点在后续对列名做操作时要心里有数。
2.2 手工构造200行的陷阱
我见过很多人在教学时用一个小到只有5行数据的表格来演示,结果学到loc和iloc时感觉还行,一到真实数据就露馅。真正能触发索引深层问题的,是数据量到几百行的场景。所以我建议自己手工造一个足够大的成绩DataFrame来练。
这里需要提醒:不要用手敲200行的数据,那样既慢又容易出错。用NumPy的随机数生成器批量造数据是最常见的做法,同时为了让数据看起来真实,最好固定随机种子保证结果可以复现。造成绩单的时候一般还需要考虑:语文数学英语满分150分,物理化学生物满分100分这类现实约束,用对应分布范围模拟,而不是简单地用0到100均匀分布。
一段简洁的构造代码是这样的:
import pandas as pd import numpy as np np.random.seed(42) student_ids = ["S" + str(i).zfill(3) for i in range(1, 201)] names = ["同学" + str(i) for i in range(1, 201)] df = pd.DataFrame({ "学号": student_ids, "姓名": names, "语文": np.random.randint(60, 150, 200), "数学": np.random.randint(40, 150, 200), "英语": np.random.randint(50, 150, 200), "物理": np.random.randint(30, 100, 200), "化学": np.random.randint(35, 100, 200), "生物": np.random.randint(40, 100, 200), })注意这里的学号列直接放在DataFrame内部,还不是真正的行索引。现实中的成绩单,行索引可以用学号天然替代,因为学号本身是不重复的标签。如果直接用0到199的整数位置索引,虽然也能用,但没法体现“按学号查成绩”的真实语义。
所以教学演示时,通常会先把这个DataFrame的行索引设置为学号列,再用drop删除原有学号列,或者直接在构造时指定index参数。这两者的效果一样,但一个体现“先构造后设置索引”,一个体现“构造时指定索引”,建议都试一遍。
2.3 从CSV读到DataFrame的关键点
除了手工构造,还有一种绕不开的场景——从CSV文件读成绩单。很多新人导入后看到的列名带着莫名其妙的空格,或者第一列变成了Unnamed: 0,根本原因往往是对参数理解不到位。
实际读取成绩单时,有两个参数最值得调:一个是index_col,用于指定哪一列作为行索引,比如index_col="学号"就会把学号列变成索引而不是普通列;另一个是encoding,处理中文表头时经常要指定utf-8或者gbk。我自己在Windows上处理某些中文Excel转出的CSV时,遇到乱码的概率不算低,通常试一下encoding="utf-8-sig"基本能解决,这比手动改文件编码要省事得多。
这里也顺带分享一个处理经验:凡是遇到列名带空格、列名前缀多余字符的情况,最省力的办法是读进来后立刻用df.columns查看一遍,一次性清洗列名,比如把" 语文"改成"语文"、把"总分 "的尾部空格去掉。这能在源头上避免后续所有索引操作因列名不匹配而报错。
3. 吃透DataFrame的底层结构:标签、位置和数据是什么关系
3.1 axis=0和axis=1不要再靠死记
很多教程介绍索引时喜欢说“行就是axis=0,列就是axis=1”。话是没错,但光记住这句话没用,因为遇到drop、apply、concat这些函数时照样晕。
我自己给学生讲的时候喜欢用“方向感”来类比:想象你在表格里走路,一个方向是竖着向下走,跨越不同的行,这就是axis=0的方向;另一个方向是横着向右走,跨越不同的列,这就是axis=1的方向。所以轴的方向决定的是“你会跨过什么”。当你要删除多余的行(比如删掉某个学号),你需要的是竖着向下定向删掉一行,这时候axis=0。删除科目(例如删除“生物”列)是横向删掉整列,属于axis=1。
在DataFrame里经常看到shape的两个值,行数对应axis=0,列数对应axis=1。搞懂这个,后面所有函数里出现的axis参数就都不会再混淆了。
3.2 index、columns、values三件套是索引操作的灵魂
DataFrame这个对象在你看不见的地方其实分了三块存储:一块是行索引(index),一块是列名(columns),一块才是真正的数据(values)。这三块共同决定了DataFrame的结构。索引操作的本质,就是在和这三块打交道。
行索引默认是一个RangeIndex,也就是从0到行数减1的整数。当你把它换成学号以后,它就变成了一个Index或更具体的字符串索引。列索引类似,当你给列起好名字以后,这些名字就挂在columns上。底层数据则是一个二维NumPy数组,你可以用df.values拿到。
索引和列名的类型直接影响你能不能用标签操作。比如学号列是字符串,那你就必须用字符串去loc;如果学号被读成了整数,你就得用整数去loc。这种严格匹配关系是新手最容易踩坑的地方——明明看到了“S001”,结果loc的时候写成了1,不报错才怪。
3.3 位置索引和标签索引的“坐标系”差异
继续往深里说,DataFrame的行索引其实是两套系统。一套是标签系统,也就是你亲手设置的学号;另一套是位置系统,也就是从0开始的自然排名。这就引出了Pandas中最关键的两兄弟:loc和iloc。
类比来讲,loc像查字典:你按拼音或偏旁去查具体条目,直接对得上号就行。iloc像翻书页:你不管书里写的是什么内容,只要按页码翻就对了。所以loc依赖标签,iloc依赖位置,两者不能混用。
要特别注意:如果行索引恰好是默认的0到199整数,使用loc[5]和iloc[5]效果一模一样,因为标签和位置碰巧相等。可一旦你把学号设置为索引,loc["S005"]和iloc[4]的含义就完全不同了。而在真实的成绩单场景中学号大概率不连续,甚至可能有缺失,这时候标签和位置分离带来的迷惑感会很强,必须在环境搭建阶段就主动把这个差异说透。
4. 列索引实操:给成绩单加减改造的自由度
4.1 取一列、多列的正确和错误姿势
处理成绩单时,最频繁的操作是取列。取一列可以用df["语文"],也可以用df.语文(点属性方式)。区别在于:方括号方式可以处理任何列名,包括带空格或数字开头的列名;点属性方式写起来快,但列名必须是合法的Python标识符,且不能和方法名冲突。所以列名是“语文”时,两种方式都能用;但列名带空格时只有方括号能用。
取多列要用方括号加列表形式,注意嵌套结构:df[["语文", "数学"]]。中括号里放一个列表,这个列表包含你要取的所有列名。忘记这层嵌套是新手最常见的低级错误,写成了df["语文", "数学"]直接报错,因为Pandas会把“语文”和“数学”当成元组去索引列,这在列维度上是行不通的。
这背后的原理其实很单纯:DataFrame的单列返回一个Series,而多列返回一个新的DataFrame。你取一列去做运算得到的就是一列的结果,取两列得到的是一个子表,后续操作方法会有所差异。
4.2 新增列时怎么处理“总分”这类派生数据
成绩单里最典型的加列需求是算总分。总分不是独立存在的数据,而是各科成绩加出来的派生数据。如果你拿到的原始表里没有总分,那么需要自己加一列。
df["总分"] = df["语文"] + df["数学"] + df["英语"] + df["物理"] + df["化学"] + df["生物"]这里要理解Pandas的广播机制:加号两侧都是Series,且它们的索引标签是对齐的(都是学号),所以相加时会按标签进行匹配。如果索引没有对齐,结果中就会出现NaN。很多人算完总分发现第一行是NaN,十有八九是因为两个列的索引类型不一致或者顺序错乱。
另一种新增列的方式是使用df.assign(),它不会修改原DataFrame,而是返回一个新对象。这个方法适合链式操作,比如想一次性加两列时用assign会比较优雅。但很多刚接触的人有一个误区:以为写出df.assign(...)之后,原df就多了一列。错了,assign返回的是新表,原表不变。究竟是直接加列还是用assign生成新表,取决于你是在做探索性分析还是想保留一份可复现的处理流水线。
4.3 删除列时为什么明明写了axis=1还是会错
删除列的标准操作是df.drop("生物", axis=1, inplace=True),这里axis=1表示按列方向删除。有人喜欢在末尾写inplace=True,觉得是“原地修改,省内存”。但Pandas官方和一些资深用户的建议恰恰相反:inplace=True在部分场景下是坑,建议不要过度依赖。
我自己在早期项目里也被这个参数坑过:一次是在Jupyter里用df.drop("某列", inplace=True)删列,随后立刻用df.head()检查,发现列还在,最后排查半天才知道是Pandas版本导致的inplace返回值在链式调用时的行为怪异。自那以后,我基本统一改用df = df.drop(columns="某列"),不用inplace参数,每步操作都显式赋值,逻辑清清楚楚。
另外提醒一个容易被忽略的细节:drop操作在默认情况下是按标签删的,而不是按位置删的。所以想删第3列时写df.drop(2, axis=1)很可能报错或删错列。正确做法是先用df.columns定位到列名,再用列名删;确定要按位置删,则需拿到df.columns[2]再传给drop。
4.4 改名要怎么精确命中列
当需要修改列名时,两个常见做法:一个是用df.rename(columns={"语文": "语文成绩"}),另一个是直接改df.columns属性,比如df.columns = ["学号", "姓名", ...]。前者是映射式的精准修改,后者是整体替换。
实操中rename更适合只改一两列的场景,因为它不要求你写上所有的列名,没写到的会自动保持原样。而直接给columns赋值则要求列表长度等于列数,多一个少一个都会直接报错。这个对比能让思路更清楚:局部微调用rename,整体重命名用columns赋值。
我之前遇到过一种情况:读进来的表列名全是中文,但后面某个分析库只接受英文列名,于是我用一行字典映射把中文名全转成英文。这里要注意rename不会修改原表,它也是返回一个拷贝的,除非你显式重新赋值;如果希望直接在原表上改,可以用inplace=True,但更推荐直接赋值给原变量,因为这样每一步都留了记录,出错也好回溯。
5. 行索引实操:从loc到iloc再到复杂筛选的进阶路线
5.1 loc学号查人,iloc排名查人
回到那张200人的成绩单。现在要看学号是S005的学生的所有成绩,用loc最直接。而要看第5名(位置索引4)对应的学生——注意位置从0开始——用iloc最合适。
df.loc["S005"] # 取出学号为 S005 的那一行 df.iloc[4] # 取出第 5 行,也就是位置序号 4这两行代码的结果可能一样,也可能不一样,关键取决于行索引是否恰好等于位置序号。设置学号作为索引之后,loc["S005"]返回的是标签为S005的行;iloc[4]返回的是从顶部数第五行,无论那行的学号是什么。
理解这个差异的重要性在于:真实场景中,你经常是想按学号精确查某个学生(用loc),也想按排名位置查某个人(用iloc)。比如班级成绩排名之后,想看看第1名是谁、第10名是谁,用iloc[0]、iloc[9]会更自然。
5.2 loc切片包含尾巴,iloc切片不包含尾巴
切片是新手最容易在一片模糊中踩坑的地方,因为loc和iloc对于区间端点的处理规则不一样。
- loc["S005":"S010"],会包含两端的S005和S010,因为是闭区间,标签也都存在。
- iloc[4:10],取位置4到位置9的行,不包含位置10,和Python原生切片规则一致。
这个差异如果不记住,实际使用时容易多取一行或少取一行。如果非要找一句话概括:loc是“把从A到B的人全叫上,A和B都算”,iloc是“从第A个往后数到B之前,不含B”。
还有一个坑:loc["S010"]这种单个标签可以直接用;但loc["S010":"S005"]这种逆序切片是不允许的,因为Pandas要求标签切片从左到右。而iloc[10:4:-1]这种逆序则可以用步长实现反转。这一点在排名倒序场景特别实用。
5.3 布尔筛选背后其实也是索引对齐
除了按标签和位置取行,成绩单场景里最常用的是条件筛选。比如想筛出数学成绩大于120的学生:
df[df["数学"] > 120] df.loc[df["数学"] > 120]这两种写法基本等价。前者直接用中括号套一个布尔Series,后者用loc套同一条件,语义上更明确地表明“按条件过滤行”。很多新手这时候会怀疑:为什么df["数学"] > 120返回的不是数值,而是一串True/False?因为比较运算符作用在Series上时,Pandas会按索引对齐逐行比较,生成一个等长布尔掩码。
这个布尔掩码的高明之处在于:它和行索引是对齐的。也就是说,即便你把成绩单按照总分从高到低重新排序了,布尔掩码依然能准确对应到它原来的那行学生,而不是变成“按新位置匹配”。这种索引对齐特性,是Pandas有别于普通数组的底层基因。
5.4 多重条件筛选的优先级陷阱
常见需求是找“数学大于120且英语大于130”的人。写法容易踩的坑是忘了给每个条件打括号:
df[(df["数学"] > 120) & (df["英语"] > 130)]&是“并且”,|是“或者”,~是“取反”。逻辑运算符两侧必须加括号,否则Python的运算符优先级会导致结果完全错误。这个坑我见过太多次,尤其是从Excel转到Pandas的人,习惯了Excel中=AND(...),到这里突然要处理运算符优先级,很不适应。
顺带分享一个更贴近实战的经验:筛选“物理、化学、生物中至少一门不及格”这种条件,用布尔表达式组合会很冗长。更优雅的办法是选中这几列,再用df[["物理", "化学", "生物"]] < 60得到一个全是True/False的子表,再用.any(axis=1)判断每一行是否存在True。这里.any(axis=1)的含义是沿着列方向扫描每一行,每行只要有一列满足就为True。整个写法比暴力拼接&要清晰得多。
5.5 排序之后再取前几名的坑
成绩单里还有一个高频需求:按总分排序取前十名。很多人会先排序再取行,这里务必记住,sort_values默认返回一个新排序的DataFrame,原数据顺序不变。如果想保留排序结果,就得显式赋值给一个新变量。
top10 = df.sort_values("总分", ascending=False).head(10)这里有两个隐藏细节。head(10)取的是排序后最前面的10行,所以top10的结构是“总分从高到低的前十个学生”。但如果你接下来还想按学号去loc查询这10个人,可能会遇到索引不是学号而是原来的位置索引的困惑。
实际操作中,排序后要先确认行索引是否还是学号。如果是用sort_values返回的DataFrame,行索引会跟着原行一起被移动,也就是索引保留原学号标签,而不是重新生成0到9的编号。如果想要一个干净的0到9索引,需要reset_index(drop=True)。drop=True的意思是扔掉原来的索引列,否则原索引会变成一列。对这个细节不敏感的人,后面merge或join时经常会因为索引不一致而得到奇怪的结果。
6. 综合实操:一份200人成绩单的完整索引实战
6.1 需求拆解和分析路线
综合项目可以这样设计:某中学的高一班级提供了一份原始的200名学生统测成绩表,包含学号、姓名、语文、数学、英语、物理、化学、生物共八列。现在需要完成五项任务:
- 把学号设为行索引;
- 新增总分和平均分两列;
- 筛出“总分排名前20”和“数学不及格”的学生名单;
- 找出“英语超过120且物理低于60”的学生;
- 删掉生物列并重命名若干列。
这个需求设计得很有层次感:前两个是结构改造,第三、四个是筛选,最后一个是清理和规整,几乎覆盖了索引操作中80%以上的高频场景。每做一步都实时用df.head()或df.shape检查一下,这是必须养成的习惯。
6.2 完整实现步骤
先看第一步到第三步的实现代码:
# 1. 设置学号为行索引 df = df.set_index("学号") # 2. 新增总分和平均分 df["总分"] = df.sum(axis=1) df["平均分"] = df.mean(axis=1).round(2) # 3. 排序取前20 top20 = df.sort_values("总分", ascending=False).head(20)这里sum(axis=1)是沿着列方向计算每一行的总和,含义是“把六个科目的分数横着加起来”。mean(axis=1)同理。很多新手会把sum()的默认行为误认为是按行求和,其实Pandas默认axis=0是按列求和的,也就是纵向对每一列计算总和。成绩单场景中要算每个学生的总分,必须显式指定axis=1。
注意set_index("学号")之后,原来的学号列会从数据区域中移出,变成索引。如果想要保留学号列作为普通列(比如后续导出时还要包含学号),可以用set_index("学号", drop=False)。这个参数在日常处理中相当实用,但极少在入门教程里提到。
第4步和第5步用布尔筛选和结构清理完成:
# 4a. 数学不及格名单 fail_math = df[df["数学"] < 60] # 4b. 英语超过120但物理低于60 special = df[(df["英语"] > 120) & (df["物理"] < 60)] # 5. 删生物列,重命名 df = df.drop(columns="生物") df = df.rename(columns={"平均分": "均分", "语文": "语文成绩"})注意第4步输出的fail_math和special都保留了原学号索引。如果你想把这两个筛选结果导出成Excel或CSV,直接df.to_csv("xxx.csv")即可,默认会带索引。但如果此时索引是学号,导出的文件就天然包含学号列,不需要额外处理。
6.3 过程中为什么要不断检查shape和dtypes
整个流程里,每做一步最好都输出一下df.shape或df.dtypes。shape可以快速告诉你行数和列数是否和预期一致,dtypes可以确认新增列没有被错误地识别成object类型。
我遇到过不少新增“平均分”列算完后发现平均分全是NaN的情况,根源是合计列中包含NaN或文本型数字。在真实数据处理中,读进来的分数列经常被识别成字符串,所以先做一次astype(float)转换是必要的。判断dtypes的习惯能让你提前发现这类隐患,而不是等到算排名时才炸。
更隐蔽的一类问题是:set_index之前,学号列是object类型,里面可能带有空格或其他不可见字符,导致loc["S001"]查不到。用df["学号"].str.strip()清理后,再set_index,就不会出这种幺蛾子了。这里想强调的是:任何索引操作之前的清洗,决定了索引查询的稳定性。
6.4 怎么把结果写回到Excel做汇报
实操项目的最后一步,一般要求把分析结果导出成一份新的Excel成绩单。Pandas里一条代码就能完成:
df.to_excel("class_final_scores.xlsx", sheet_name="成绩单")这里有一个相当实用的点:导出时如果不希望索引出现在文件里,需要设置index=False;如果希望索引变成普通列而不是独立索引,可以设置index_label或直接reset_index()后再导出。
在真实汇报场景中,还有一类需求:一个表里导出多个Sheet,比如一个放完整成绩表,一个放前20名,一个放数学不及格名单。用pd.ExcelWriter和to_excel联合使用就很方便。这个操作不涉及索引本身,但它是把索引操作的成果落地的重要一环。
7. 避开这些“能跑但结果错”的索引坑
7.1 为什么链式赋值会引发警告
最经典的问题是那句警告:A value is trying to be set on a copy of a slice from a DataFrame。说人话就是:你在修改一个副本,而不是原始表。
出现这个场景往往是先df_sub = df[df["数学"] > 120],然后又执行df_sub["备注"] = "达标"。df_sub本身只是原始表某个筛选结果的视图或拷贝,Pandas无法确定你的下一步操作是否应该反馈到原始表里,于是发出警告。
规避这个坑的办法是遵循“一步到位”原则:需要加列时直接对原始df用loc加条件再加新列。比如df.loc[df["数学"] > 120, "备注"] = "达标",这行代码的语义是:只选中数学大于120的行,并在这些行的“备注”列写入值。这是Pandas推荐的安全用法,也是在成绩单分析中经常会用到的原地更新技巧。
7.2 loc切片包含端点,iloc不包含端点,这事儿值得说三遍
这个规则一定要刻在脑子里,因为它不仅影响读取结果,还影响删除。
比如你想删除第10行到第20行,写df.drop(df.index[10:21])是稳妥的,因为df.index[10:21]是位置切片,不包含21,实际上是第10到第20行。但有些人会写df.drop(df.index[10:20]),这样会少删第20行。
再比如你想保留前50个人的数据,既可以df.iloc[:50]取,也可以df.head(50)。前者靠位置切片,后者靠内置方法,结果一样,但前者更通用,能配合start/end任意截取区间。
实际工作中最坑的情况是:用loc做区间切片时,两个端点的标签必须都存在,否则会报KeyError。比如学号跳过了S007,直接用loc["S005":"S008"]就可能出错,因为S007是缺失的。这种字符串索引在真实学号中的断档情况非常常见,解决办法通常是改用位置索引,或者先用index的isin筛选来构造连续子集。
7.3 索引对齐的隐性力量
Pandas索引最强大的特性是自动对齐,这也是新手最难理解的一点。
举个例子:有两个Series,一个记录学号为S001到S100的数学成绩,另一个记录学号为S051到S150的英语成绩,它们的长度不同、顺序也不同。直接相加时,Pandas会自动按学号对齐,只保留两个Series都出现的学号,其他位置补NaN。这种机制在merge/join和算术运算中无处不在。
成绩单场景可以利用这个特性做“按学号匹配附加信息”,比如另外一个文件里有学生性别和班主任信息,直接按学号merge就能匹配。但如果索引没有清理干净,比如一边索引是字符串S001,另一边索引是整数1,那匹配结果将会全部落空,而且不会报错,只是返回一堆NaN。
排查这类问题时最快的方法是用df.index.dtype看看两边的索引类型是否一致,再用index.intersection看看重合了多少。这是我在实际业务里排查“为什么合并后全是空值”时的标准动作。
7.4 保存和读取后索引为什么会“复活”
还有一个特别容易踩的坑:to_csv之后再用read_csv读回来,原来的索引会变成新的一列,默认列名叫“Unnamed: 0”。比如你把学号设置为索引后导出,结果再次读取时学号变成了普通列,索引又变回了0到199的RangeIndex。
处理方式是读取时通过index_col参数指定哪一列作为索引。比如df = pd.read_csv("class_final_scores.csv", index_col=0),或者更明确一点,用index_col="学号"。这个参数对很多人来说用得少,一旦遇到“为啥我读出来的表结构变了”的问题,十次有九次是这个原因。
如果希望文件里包含学号但不作为索引,导出时用df.to_csv("xxx.csv", index=False),先reset_index()把学号转回普通列再导出,这样再次读取时不会丢失任何信息,也能按需重新set_index。这组操作看起来琐碎,却是日常数据交接中最容易产生混乱的环节。
8. 实操收尾:从成绩单的索引练习到真实数据分析的迁移
整套练习做下来,你会发现核心收获并不是背住了loc和iloc的语法,而是建立起了“标签、位置、布尔掩码”三个索引维度的心智模型。带着这套模型,去面对真实业务中的用户表、订单表、日志表,你只需要做一次“概念映射”就能快速上手:用户表里的用户ID就是学号,订单表里的订单号就是学号,日志表里的时间戳就是学号。所谓的行索引,本质上是“你如何唯一定位一条记录”的主键思想。
我建议你学完之后立刻做一个迁移练习:把自己手头任何一份CSV拿过来,先试着把唯一标识列设置为索引,再分别用loc和iloc完成三次查询、一次区间切片、一次条件筛选、一次排序取前N。这个练习做熟,DataFrame行/列索引这关就算彻底过了。
在这个教学场景里,200行是水,索引是被研究的鱼。用成绩单这条“鱼”去理解索引的“渔”,比任何抽象定义都管用。等你能在这份成绩单上熟练完成各类行列操作,意味着你已经具备了绝大多数数据分析任务所需要的基础拼接能力——剩下的,只是在更大、更脏、更复杂的数据上做同样的事。