1. 为什么非学Pandas不可原生Python处理数据的真实痛点先聊一个我经常遇到的场景。假设你手上有一份销售记录只有三列城市、日期、销售额。数据结构不难就是一行一行的记录。用Python原生写法你可能会写一个循环按城市累加最后再算一个平均值。这种写法本身没错但一旦数据量从几十行涨到几十万行循环的效率会立刻让你难受起来。更麻烦的是当你需要按多列分组、再拼接、再补缺失值原生列表和字典的组合就会迅速变得难以维护。我第一次真正意识到这个问题是在处理一份上百行的人员信息表时。当时的写法是两层嵌套循环加两个字典代码写了一屏半调试了差不多一个小时。后来用Pandas重写同样的逻辑只花了五行跑得还更快。从那以后我的个人观点是在Python生态里做数据分析绕开Pandas几乎是不现实的。Pandas是Python数据分析领域最基础也最核心的库它的两个主力数据结构——Series和DataFrame分别解决了一维数据和二维表格数据的建模问题。Series可以理解成一个带名字的序列DataFrame则可以理解成一张带列名和行索引的表格。几乎所有后续的数据清洗、筛选、聚合、可视化都是在这两个结构上展开的。这篇文章不做面面俱到的文档罗列而是从创建到高频基本操作把Series和DataFrame最常用、最容易被卡住的点一次讲清楚。适合读这篇内容的人是那种已经能写简单Python脚本、但对Pandas还处于看过文档但用不溜状态的初学者。如果你连Python基础都不太熟我会尽量用生活化的说法解释每个概念你可以边看边敲代码。准备好了就直接往下走所有的代码示例都是在Python 3.8以上版本、Pandas 1.5以上版本的环境里实测过的。2. Series底层逻辑从创建到索引的每一个细节2.1 Series的三种常规创建方式Series是Pandas里的一维数据结构它比Python自带的列表或者字典多了一层标签的概念。最直白的理解是Series 一列数据 每个数据对应的行号。这里的行号不一定是0、1、2这样的位置序号它可以是任意字符串或者数字统称为索引。第一种创建方式最简单直接传入列表import pandas as pd s1 pd.Series([85, 92, 78]) print(s1)输出会是0 85 1 92 2 78 dtype: int64你没传索引Pandas会默认给0到n-1的位置序号。这看起来和列表没区别但Series真正的威力来自自定义索引。传入一个index参数s1 pd.Series([85, 92, 78], index[小明, 小红, 小刚], name数学成绩) print(s1)这次输出变成了小明 85 小红 92 小刚 78 Name: 数学成绩, dtype: int64这里的name参数是给整个序列起的名字打印的时候会显示在最后一行将来放进DataFrame时也会自动变成列名。很多初学者会忽略这个参数等到后面要从Series构造DataFrame时才回头补其实一开始就养成设置name的习惯会很省事。第二种创建方式是从字典创建。Pandas会把字典的键作为索引把值作为数据本身s2 pd.Series({杭州: 1500, 成都: 2200, 青岛: 1800}) print(s2)这里我强调的是当索引顺序对你不重要时字典创建是最省心的方式。但是要注意在Python 3.7之前字典是无序的版本升级后字典保持插入顺序所以Series的顺序会对应字典的插入顺序而不是字母顺序。如果你对顺序有要求还是老老实实把键列表传进index参数。第三种创建方式是从标量创建。传入一个数字和一组索引Pandas会把这个数字重复填充到每个索引位置上s3 pd.Series(0, index[一月, 二月, 三月]) print(s3)这种写法通常用于预分配内存或者初始化默认值比如准备一个全是0的月份序列后面再逐月赋值。这三种方式里列表加自定义索引、字典创建这两种用得最多标量创建属于特定场景的小技巧。2.2 索引对齐Series最容易被忽略的特征如果说Series有一个必须深刻理解的特性那一定是索引对齐。什么叫对齐就是两个Series在做加减乘除时Pandas不按位置顺序运算而是按索引标签配对运算。这个特性我第一次用的时候没有意识到结果得到了很多NaN排查了半天才明白过来。看个例子s1 pd.Series([100, 200], index[A, B]) s2 pd.Series([30, 40], index[B, A]) print(s1 s2)如果按位置来算10130和20140才叫按位置相加。但Pandas的加法输出是A 140 B 230 dtype: int64它把索引A对应s1的100和s2的40相加索引B对应s1的200和s2的30相加而不是按位置顺序相加。在所有常规运算中比如加法、减法、比较操作Pandas默认都是这种按索引对齐的行为。这个设计有好有坏。好的方面是它能让来自不同顺序、不同来源的数据自动匹配比如两个表都有一列工号但它们行顺序完全不同你不需要手动重排直接拿对应的列做运算就会匹配上。坏的方面是如果两个Series的索引标签有交集但不完全一致运算结果的索引会是两者的并集不匹配的位置会被填成NaN。s3 pd.Series([100, 200], index[A, B]) s4 pd.Series([30, 40], index[B, C]) print(s3 s4)结果是A NaN B 230.0 C NaN dtype: float64索引B两边都有正常相加A和C只有一边有值另一边缺失结果是NaN。这就是对齐运算产生NaN最常见的场景之一。如果你在数据处理时发现多出一堆NaN先别急着认为是数据本身脏检查一下是不是索引没对齐。这个习惯能帮你省下大量的排查时间。2.3 loc与iloc两种索引方式的选择规则Series的索引提取有两条路线很多人一开始会被它们搞混。loc是标签索引按索引的名字取值iloc是位置索引按排在第几位取值。两者用法在写法上非常相似但含义完全不同。s pd.Series([10, 20, 30], index[x, y, z]) print(s.loc[y]) # 输出20按名字 print(s.iloc[1]) # 输出20按位置只要理解了标签和位置这两个概念的区别这一对用法就永远不会弄混。还有一点值得提一下如果你给Series设置的是0、1、2这样的默认索引那么s.loc[1]和s.iloc[1]在正常情况下都返回同一个值这就更容易让人误以为两种写法等价。一旦你后来把索引改成了字符串或者其他不连续的数字这种错觉就会立刻造成错误。所以我的建议是日常养成明确区分两者的习惯凡是想取名字叫xx的那一行就用loc凡是想取排在第x位的那一行就用iloc。Series还支持切片比如s.iloc[1:3]表示取第1、2个位置的元素s.loc[y:z]表示取y到z的范围。要注意的是loc切片是闭区间会包含结尾标签而iloc切片和Python列表切片一致是左闭右开不会包含结束位置。这个差异也是新手经常踩的坑。如果你记不住就在动手前先想清楚自己用的是哪种索引方式。3. DataFrame的七种创建姿势与适用场景3.1 从字典列表创建数据科学家的默认姿势DataFrame是Pandas里最常见的二维数据结构。如果非要用一句话描述我会说DataFrame 多列Series拼成的表格。每一列其实就是一个Series列名就是Series的name行索引就是Series的索引。把这句话刻在脑子里后面很多操作都会豁然开朗。创建DataFrame的方式很多但实际工作中大约七成情况都用同一种从字典列表创建。所谓字典列表就是一个列表里面每个元素都是一个字典字典的键是列名值是这一行的数据。df pd.DataFrame([ {姓名: 张三, 城市: 杭州, 销售额: 1500}, {姓名: 李四, 城市: 成都, 销售额: 2200}, {姓名: 王五, 城市: 杭州, 销售额: 1800}, ]) print(df)输出是一张规整的表格三行三列。Pandas会自动完成两件事把字典的键汇总成列名把每一行的值按字典的键放入对应位置。如果一个字典缺少其他字典有的键那这个位置的取值就是NaN。这种创建方式最贴近一行一条记录的日常认知数据源如果是从接口或爬虫拿到的JSON格式往往就是这种结构可以直接塞进DataFrame几乎不用做额外转换。我个人的习惯是只要数据是逐条记录的格式一律优先用字典列表因为可读性好、出错时也好定位。3.2 从Series字典创建与索引并集问题第二种常见创建方式是从Series字典创建。这里说的字典是键是列名值是一整个Series。math_scores pd.Series({张三: 88, 李四: 92, 王五: 76}) english_scores pd.Series({张三: 71, 王五: 84, 李四: 90}) df pd.DataFrame({ 数学: math_scores, 英语: english_scores, }) print(df)这个例子中两个Series的索引顺序不一样。张三、李四、王五的出场顺序两边是不同的但Pandas会自动按索引对齐所以生成的DataFrame长这样数学 英语 张三 88 71 李四 92 90 王五 76 84行索引是张三、李四、王五的并集列是数学、英语。这个特性在拼接不同来源的数据时特别方便你不必手动统一顺序。但反过来也要小心如果两个Series的索引不是同一套体系比如一个是数字一个是字符串并集的结果就会包括所有索引缺失值满天飞。比如s1 pd.Series({A: 1, B: 2}) s2 pd.Series({1: 3, 2: 4}) df pd.DataFrame({col1: s1, col2: s2}) print(df)行索引会变成[A, B, 1, 2]col2只有1和2有值col1只有A和B有值。这个现象新手看起来很奇怪但本质就是索引对齐规则在DataFrame层面的体现。遇到这种情况先停下来看看两个Series的索引类型是否统一再决定是做索引映射还是重新构造数据。3.3 从二维列表和文件中读入快捷路径与隐藏陷阱从二维列表创建DataFrame也很常见尤其在临时处理一些手头数据时。写法是传入一个嵌套列表再用columns参数指定列名data [ [张三, 88], [李四, 92], ] df pd.DataFrame(data, columns[姓名, 分数]) print(df)如果你不传columnsPandas默认会用0、1、2这样的数字当列名。我强烈建议随手把columns参数写好否则后面写df[0]这种代码可读性极差而且很容易和索引位置混淆。从二维列表创建主要适合数据量小、列名明确、行顺序已经整理好的场景比如手工录入的表格。真正工作中最多的创建方式其实不是手工构造而是从文件读取。其中CSV尤其常见df pd.read_csv(sales.csv, encodingutf-8)read_csv是整个Pandas里参数最丰富的函数之一我挑几个高频参数说一下。sep用来指定分隔符默认是逗号如果是制表符就传sep\tencoding用来处理中文乱码日常使用utf-8如果读出来是乱码试试gbk或者gb18030parse_dates可以把某一列解析成日期类型比如parse_dates[日期]index_col可以把某一列设为行索引比如index_col工号。Excel读取也是一样的思路df pd.read_excel(data.xlsx, sheet_nameSheet1)不过要注意读Excel需要安装额外的库。Pandas本身不会自带操作Excel文件的能力读xlsx需要openpyxl读xls需要xlrd缺了会直接报ImportError。很多初学者在这一步卡住其实提示信息已经写得明明白白装上相应的库就能解决。文件读取这块我的建议是能导成CSV就优先用CSV文件格式简单、读取速度快、跨平台兼容性好。Excel适合给别人看CSV适合给程序处理。4. 日常使用频率最高的DataFrame操作视图、筛选、增删列4.1 先看懂DataFrame的三层结构拿到一个DataFrame我建议你先做三件事看前五行、看行列数和看列名与数据类型。df.head(5) # 看前5行 df.shape # 返回(行数, 列数) df.columns # 看所有列名 df.dtypes # 看每列的数据类型这套组合拳可以在几秒内让你了解数据的基本面貌。shape返回的是一个元组第一个元素是行数第二个是列数这个顺序别搞反因为下面用shape[0]和shape[1]时会直接影响到代码逻辑。dtypes尤其重要——某列看起来是数字但实际是字符串很多时候就是从这里暴露出来的。字符串列在数据计算时不会报错但会产生一堆诡异的拼接结果比如12得到12。养成拿到数据先看dtypes的习惯能避掉很多后面才出现的隐性错误。还有一个常见需求是查看统计信息df.describe()会对数值列给出计数、均值、标准差、最小值、四分位数、最大值。这对快速判断有没有异常值非常有用。比如某列的最大值和均值的差距大到离谱就要怀疑是否存在极端异常点。我会把head、shape、dtypes、describe这四件套称为数据体检四件套每次拿到新数据先做一遍再开始写业务逻辑。4.2 筛选与切片方括号什么时候能驾驭DataFrame的筛选操作可能是新手最先接触、也最早产生混淆的环节。方括号直接选列是最容易理解的df[姓名] # 返回一个Series df[[姓名, 销售额]] # 返回一个DataFrame注意单列和双列的区别。单个列名用字符串结果是一个Series多个列名要用一个列表包起来结果才是DataFrame。很多人刚学会觉得反正是选列单双都行然后写df[[姓名]]时发现返回的是DataFrame不自觉地开始compare几个方法其实只需要记住这层包装关系就行。行筛选则有三种主流方式。第一种是按行标签用locdf.loc[1] # 取行索引为1的那一行返回Series df.loc[1:3] # 取行索引1到3闭区间第二种是按位置用ilocdf.iloc[1] # 取第1行的数据 df.iloc[1:3] # 取第1、2行的数据不包含3再次强调那个闭区间和开区间的差异。loc是标签切片1到3会把1、2、3全取出来iloc是位置切片1到3只取位置1和2。第三种是布尔筛选这是实际项目里用得最多的df[df[销售额] 1000]这个写法的执行逻辑是先计算df[销售额] 1000得到一个由True和False组成的布尔序列然后把这个布尔序列传给DataFrame的方括号只保留True对应的行。如果想加多条件踩坑率最高的地方就来了——多个条件之间不能用Python的and或or要用和|并且每个条件都要用括号包起来df[(df[销售额] 1000) (df[城市] 杭州)]为什么不能用and因为and在做布尔判断时要求两边的对象都能转换成True或False而Series的布尔值是一个数组没法直接转成单个布尔值所以会报ValueError。这个报错信息我见过无数次几乎每个初学Pandas的人都会碰到一次。和|则是逐元素的逻辑运算返回一个等长的布尔序列符合筛选需求。如果你能把这个背后的原因理解了以后再遇到类似报错就不会慌而是想着哦这是个数组运算不能用Python关键字。4.3 添加列与删除列的正确步骤添加列的方法很直接直接用一个新列名赋值即可df[利润率] df[销售额] * 0.2这个操作会为DataFrame增加一列右边可以是一个标量、一个列表或者一个Series。如果右边是Series会按照索引对齐规则匹配行而不是按位置匹配。我遇到过一种情况想添加一列右边Series的索引顺序和DataFrame不一致但索引标签能对上有交集结果数据被重新排列了看起来错位其实是对的。如果你希望按位置强行赋值方法是把Series转换成列表df[新列] s.tolist()。删除列有三种常见方式。第一种是drop方法df df.drop(利润率, axis1)这里的axis1是关键。我当年学的时候老是忘axis到底是1还是0后来是这样记的axis0代表行方向往下走axis1代表列方向往右走。要删列就是沿着列方向做操作所以是axis1。如果你写axis0它会试图去删一个叫利润率的行结果就是KeyError。drop默认不修改原对象而是返回一个新的DataFrame。所以写法上要接收返回值或者用inplaceTrue。个人建议优先接收返回值因为链式写法更清晰也少一些不可预期的影响。第二种删除方式是del关键字del df[利润率]直接改原对象。第三种是popdf.pop(利润率)会把这一列取出来并删掉适合你想保留删除列的数据继续用的场景。这三种方式的差别和适用场景我用一个表整理了一下方式是否修改原对象是否能保留被删数据典型场景df df.drop(列, axis1)否否最通用、可链式调用del df[列]是否简单粗暴删列df.pop(列)是是删列并取出该列复用4.4 重命名列名与调整列顺序重命名列名是清洗数据时常用的一步。当你从文件里读出来的标头是Name、Age而你的业务习惯是姓名、年龄时rename就是最好的工具df df.rename(columns{Name: 姓名, Age: 年龄})rename也遵循不修改原对象的规则记得接收返回值。columns参数接受一个字典键是旧列名值是新列名。如果你想把所有列名统一改成大写或者小写可以用df.columns [col.strip() for col in df.columns]这种列表推导式直接赋值给columns属性也是一种修改方式它会原地生效。调整列顺序也是常见需求。DataFrame的列顺序就是columns这个列表的顺序所以调整列顺序其实只是在重排columnsdf df[[城市, 姓名, 销售额]]用一个新的列名列表去切数据框列就会按这个顺序排出来。如果你只想把某一列挪到最前面可以写first_col 姓名 df df[[first_col] [c for c in df.columns if c ! first_col]]这种写法不需要依赖任何高级API纯粹利用列表操作就能完成。有些教程会推荐sort_index之类的方法但我觉得最简单的方括号重排反而最直观也最容易理解和维护。5. 数据清洗三板斧缺失值、重复值、类型转换5.1 缺失值的识别、统计与处理策略数据清洗是Pandas的绝对主场。大多数真实数据拿到手里第一件事不是分析而是清理。清理的第一步永远是识别缺失值。df.isnull().sum()isnull()会把整个DataFrame映射成True/FalseTrue表示该位置是空值sum()按列统计True的个数也就是每列的缺失值数量。运行结果一目了然哪列有多缺、哪列没有缺全部暴露出来。与之配套的是notnull()它是isnull的反向操作。识别完之后怎么处理缺失值主要看场景。两种最常用的方案dropna和fillna。df df.dropna() # 删掉任何包含缺失值的行 df_clean df.dropna(subset[销售额]) # 只根据某列判断该列有缺失才删除dropna默认是把含有任何一个NaN的行整个删掉。这个行为有时过于激进比如100列里只有1列某个单元格缺失结果整行没了数据损失太大。所以实际使用中我很少直接df.dropna()而是加subset参数指定按哪些关键列判断是否删除。缺失率低的列可以容忍缺失率高的列就可以考虑整列删除比如df.dropna(axis1, thresh100)表示保留非空值数量超过100的列。这种阈值控制方式在处理宽表时非常实用。fillna是另一种思路把缺失值填充成某个值而不是删除整行。df[城市] df[城市].fillna(未知) df[销售额] df[销售额].fillna(df[销售额].median())分类型列通常填充众数或一个明确的占位符数值型列可以使用均值或中位数。我自己更偏向中位数而非均值因为中位数对异常值不敏感。比如某城市销售额最大的一笔是10万其他都是几十到几百均值会被拉高很多而中位数能反映一般水平。填充时要注意一点fillna也是一次性返回新对象的行为所以你赋值给新变量或者别忘接收。什么时候该删、什么时候该填没有绝对标准。我的经验是如果缺失值占比极小比如1%以内删除是最省事的如果关键字段缺失但记录本身有价值优先填充如果你要做模型训练填充方式要谨慎因为未知这个类别可能会影响模型的分布判断。处理缺失值波及的面很大但它本质上就是一个取舍题核心逻辑是尽量减少数据损失。5.2 重复值的识别与去重技巧重复值在真实数据里出现的频率比你想象的高得多尤其是从多个人工录入的表格合并而来时。识别重复值的标准做法是duplicateddf.duplicated().sum()这里的duplicated()判断的是整行是否与前面的行完全重复。如果只想按某几列判断比如按姓名和城市判断是否重复就传subset参数df.duplicated(subset[姓名, 城市]).sum()去重操作是drop_duplicatesdf df.drop_duplicates(subset[姓名, 城市], keepfirst)keep参数有三种取值first表示保留第一条出现的记录last表示保留最后一条False表示把重复的记录全部删掉。日常业务中keepfirst最常用。这里要特别提醒的是duplicated和drop_duplicates的默认行为都是整行完全重复如果你传入了subset它们的语义就变成了按这几列的值判断重复。两个函数在参数风格上保持一致但很多人会忘了在duplicated里面写subset导致识别结果对不上。重复值的判断有一个隐蔽陷阱NaN参与判断时两个NaN也会被认为是重复的。比如两行数据姓名都是NaN城市都是杭州按城市和姓名判断重复时它们是重复的。这个问题见仁见智但从业务角度讲缺失值导致的重复往往不是真正的重复记录可能是两条不同的人因为某个字段缺失而已。处理这种数据时可以先填充再判断或者加一个能区分的字段一起判断。5.3 数据类型转换astype、to_datetime与分类变量的处理类型转换是数据清洗里最容易被忽略、却最影响后续分析的一环。最常见的需求是把一个数字型列转换成浮点数或整数df[销售额] df[销售额].astype(float)如果把字符串1500转换成整数astype(int)通常也能工作但会遇到一个问题字符串里一旦混入了1500元这种带单位的写法astype就会直接抛异常。更稳健的做法是先用字符串替换把多余字符去掉再astype。这就是为什么很多清洗代码里都会包含str.replace和astype连用的写法。日期字符串转成日期类型也是一个高频操作。如果你从Excel或者爬虫里拿到的日期列实际上是2024-01-15这样的字符串用to_datetime能直接解析df[日期] pd.to_datetime(df[日期])转换之后列的类型变为datetime64你才能通过df[日期].dt.year拿到年份、dt.month拿到月份做时间范围筛选、按月聚合都会方便许多。to_datetime的errors参数也很常用errorscoerce表示把无法解析的日期转成NaT时间缺失值而不是直接报错。我处理脏数据时几乎总是开着coerce因为这样一步到位之后再结合isnull对解析失败的行单独处理。分类变量也是值得一提的类型。Pandas里有专门的category类型当你有一列重复度极高的字符串比如城市列只有杭州、成都、青岛三个值但数据有十万行把它转成category类型能显著节省内存某些聚合操作也会更快df[城市] df[城市].astype(category)不过category类型在参与字符串操作时偶尔会遇到一些细微的不便比如str访问器某些方法在category上表现不同。所以我的建议是小数据量的场景没必要特地去转category内存还没紧张到那个程度大数据量或做机器学习特征工程时再考虑这一步。6. 新手最容易踩的五个坑一次讲透6.1 SettingWithCopyWarning链式赋值的警告这个警告应该是Pandas新手遇到频率最高的一个。典型触发场景是这样df2 df[df[销售额] 1000] df2[标记] 1第二行代码运行后你会看到类似SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame的提示。这个提示实际上是Pandas在告诉你你正在修改的可能是一个副本而不是原数据框修改结果可能不生效也可能影响原数据行为具有不确定性。我初学的时候直接把这个警告忽略了后来发现标记列写了一堆原数据却纹丝不动彻底被坑了一次。正确做法是不想改原数据就用.copy()显式创建副本想改原数据就不要先筛选再赋值而是直接用loc在筛选条件下设置df2 df[df[销售额] 1000].copy() df2[标记] 1或者df.loc[df[销售额] 1000, 标记] 1第二种写法是Pandas官方强烈推荐的。它的含义是在df中找到销售额1000的行把标记列设为1。整个过程只动一次df没有中间副本也就没有模糊之处。以后看到SettingWithCopyWarning不要想着怎么关闭警告而是回头想想自己的赋值逻辑是不是应该换用loc。6.2 行索引不唯一时loc返回的不再是标量默认情况下DataFrame的行索引是0到n-1的唯一整数。但一旦你用index_col指定了某个业务字段作为索引或者手工构造了带重复索引的DataFrame索引可能就不唯一了。比如这样一份数据df pd.DataFrame({ 城市: [杭州, 成都, 杭州, 青岛], 销售额: [1500, 2200, 1800, 1600], }, index[a, b, a, c])此时df.loc[a]返回的是两行数据的DataFrame而不是一个Series。很多人在这里会惊讶明明是同一个写法为什么之前返回Series现在返回DataFrame原因就是索引标签有重复。只要索引不唯一Pandas就无法确定你要的是哪一行只能把所有的a行全部返回。这个行为在很多场景下反而是有用的比如你想一次性取出所有杭州的数据只要把城市列做成索引df.loc[杭州]就能拿到所有匹配行。但如果你习惯性地取行后接着做类似row[销售额]这样的操作就可能报错因为row本身变成了DataFrame。遇到这种问题检查索引是否唯一是第一位的df.index.is_unique可以直接判断。如果确实需要唯一索引用df.reset_index()重置或者新增一个唯一的ID列。6.3 drop、fillna、astype默认不修改原对象我想强调的第三个坑是所有Pandas方法调用时对原对象的态度。大部分Pandas方法比如drop、fillna、rename、astype、sort_values默认都返回新对象不改变原DataFrame。这就导致了一种经典错误df.drop(销售额, axis1) # 运行完发现df还是老样子你确实执行了drop但返回值没有接收原df没被修改所以删除没有发生。Pandas这样设计是有意的它能避免方法调用时意外破坏原数据让代码更容易调试和链式书写。但代价就是新手极易忘记接收。解决方案清晰的两种一是用变量接收返回值df df.drop(...)二是设置inplaceTrue。我个人的风格是尽量不依赖inplace因为inplace在Pandas内部某些方法上仍然有性能上的微小差别而且链式写法中它很难用。如果你已经习惯接收新对象那这套逻辑统一起来之后代码会不容易出bug。6.4 条件筛选的与|陷阱这个坑其实在4.2节已经详细说过我这里再补一个实战提醒。多条件筛选时不仅要用和|还要注意每个条件都必须用括号括起来df[(df[城市] 杭州) | (df[城市] 成都)]如果把括号去掉写成df[df[城市] 杭州 | df[城市] 成都]Python会因为运算符优先级问题报错。这个优先级问题很隐蔽|的优先级低于吗实际上在Python中比较运算符的优先级高于位运算符|所以df[城市] 杭州 | df[城市] 成都会先运算右边的结果整个表达式变成不可预测的东西。简单粗暴的解决就是永远加括号不要试图去背优先级的细节。另一种等价的写法是用isindf[df[城市].isin([杭州, 成都])]当你要判断的候选值不止两三个时isin比一长串|条件简洁得多也不容易错。类似的取反操作可以用~df[~df[城市].isin([杭州, 成都])]~就是按位取反相当于不在这些城市中。这种写法在业务代码里非常常见建议直接用。6.5 read_csv读出来的列名带空格、中文乱码从文件读取数据的坑相对冷门但一旦遇到会很头痛。最常见的是CSV文件标头列名带空格比如姓名 和姓名其实是两列。打印df.columns时你看到的是带空格的字符串肉眼不容易发现但代码里写df[姓名]就会KeyError因为真正列名是姓名 。解决办法有两个读取时指定skipinitialspaceTrue自动去除分隔符前后的空格或者读取后用列表推导式清洗列名df.columns [col.strip() for col in df.columns]中文乱码的通用解决思路是调整encoding参数。utf-8不行就试gbkgbk不行试试gb18030再不行可以把encodingutf-8-sig。utf-8-sig专门用来处理带BOM的文件这种文件在某些Windows环境下导出CSV时很常见直接utf-8解析会出来一个隐藏在第一个列名前的字符。每次遇到乱码先看文件本身的编码再看Pandas的encoding参数是否匹配来回试几次绝大多数都能解决。7. 用一个真实场景串起所有操作讲了这么多基础操作最后我想用一个连贯的小案例把前面所有内容串起来看一遍。模拟场景你拿到一个销售明细CSV里面列有姓名、城市、日期、销售额文件名sales.csv。你要做的事是读入数据、体检数据、清洗缺失和重复、筛选出指定城市的记录、按城市聚合、最后输出一份整洁的汇总表。第一步读入并体检import pandas as pd df pd.read_csv(sales.csv, encodingutf-8) print(df.head(5)) print(df.shape) print(df.dtypes) print(df.isnull().sum())假设体检结果发现日期的dtype是object说明是字符串销售额有3个缺失值城市列没有缺失但可能有的城市是杭州 这种带空格的写法。第二步清洗df[销售额] df[销售额].astype(float) df[日期] pd.to_datetime(df[日期], errorscoerce) df[城市] df[城市].str.strip() df df.dropna(subset[销售额, 日期]) df df.drop_duplicates(subset[姓名, 日期], keepfirst)第三步筛选加聚合df_sub df[df[城市].isin([杭州, 成都])] result df_sub.groupby(城市)[销售额].agg([sum, mean, count]) print(result)这里的groupby是Pandas里最常用的分组聚合操作它按城市分组对销售额分别计算总和、均值、数量。输出就是一张按行索引为城市名、列为聚合指标的表格。如果你想把结果保存成新的CSVresult.to_csv(city_summary.csv, encodingutf-8-sig)整个流程下来你会看到一开始原始数据可能有一堆问题经过体检、清洗、筛选、聚合最后得到一张可以直接给业务方看的干净汇总表。全程代码量大概二十多行如果完全用原生Python实现同样逻辑至少要多写两三倍还要自己处理各种边界情况。这就是Pandas的价值所在。我在实际项目里的体会是学Pandas最忌讳死记API最好的方式是带着数据问题去敲代码。一个命令敲不熟就反复敲敲到条件反射为止。Series和DataFrame的基础操作看起来多但常用的就那么几个把loc、iloc、布尔筛选、dropna、fillna、groupby这六件事练熟你已经能覆盖绝大多数日常分析工作。以后再遇到更高级的主题比如merge、pivot_table、rolling窗口计算也都是在这些基本功之上叠加而已。你可以把这篇内容当成自己的第一份速查手册看完后马上找一份真实的表格跑一遍第七节的流程遇到不理解的再翻回来。这样过一遍比看十遍文档都管用。