1. 先搞清楚这一篇到底在讲什么
先说个结论:pandas学到“基础三”这个阶段,重点已经不是“pandas是什么”或者“DataFrame怎么创建”,而是怎么把一个又脏又乱的原始表格,变成能干净计算、能导出、能画图的状态。前两篇基础讲的是看数据、选数据,这篇讲的是改数据、存数据、画数据,前后差别很大。这里的“脏”不只是缺失值和重复行,还包括列的类型不对、日期格式混乱、字符串里夹着数字这些看起来特别微观的问题。恰恰是这些问题,卡住了绝大多数刚学完基础二的人。
这篇内容适合三类人:正在在线实训平台刷“pandas初体验”“pandas数据结构创建”“pandas读写文本文件”这类关卡,刚做完前两关但对第三关无从下手的同学;已经能写简单pandas代码,但一遇到类型转换或文件读写的报错就要查半天的人;以及环境装了三天都装不上、只想赶紧看到代码跑起来的学习者。下面所有关键点我都会顺手解释一句“为什么这么做”,而不是扔一堆参数让你硬背。
1.1 基础一、基础二没掌握的话,基础三会非常吃力
不是我吓唬人。基础三默认你已经具备三个能力:能创建DataFrame、能通过列名选中一列或多列、能使用布尔索引做条件筛选。如果你连df[df['score'] > 80]这种写法都要现查,建议把前两篇先练熟,不然这一篇里“先筛选再转换”“筛选完重置索引”的连环操作会让你看得很累,甚至产生“我怎么全不会”的错觉。
更关键的是,前两篇的大多数操作是“只读”的,比如筛选、布尔判断、查看缺失值,这些操作不会改变原表。基础三完全不同,大量操作是“写入”的,最典型的就是df['col'] = df['col'].astype(float)。这一行会真实地改变这一列的类型,不留神就把原始数据覆盖了。这种“有没有赋值回去”的区别,是基础三阶段最需要形成肌肉记忆的第一件事。
1.2 装pandas这件事,仍然卡着一大批人
别笑,安装简单,但安装失败的理由千奇百怪。最常见的是在PyCharm的Terminal里执行pip install pandas,半天后报错Could not find a version that satisfies the requirement pandas (from versions: none)。99%的情况是pip默认访问的官方源在国内网络环境下载不了,换成清华源一行命令就能解决:
pip install pandas -i https://pypi.tuna.tsinghua.edu.cn/simple如果还提示no matching distribution found,那就要检查两件事:pip版本是不是太旧,以及你的Python版本和pandas版本是否匹配。先执行python -m pip install --upgrade pip再装一次,基本能解决大半问题。
还有一种场景也高频出现:有人在ArcGIS Pro这种自带Python环境的软件里,想用pandas却import失败。这种集成软件往往自带一套独立的Python解释器,系统Python里装的包它看不到,需要你在ArcGIS Pro自己的Python包管理器里装,或者手动指定解释器路径。这个问题和PyCharm里“右侧解释器选错”本质一样。
至于手机Python装pandas,我的态度很明确:能装就装,装不上别死磕。pandas带C扩展,手机环境普遍缺少编译工具,折腾成本极高。真想练习就开一个在线Python环境或者回到电脑上,这才符合实际开发节奏。
1.3 到基础三,你要在脑子里建立一条数据流水线
把视角拉高一点。基础三阶段你再处理一个文件,心里应该有一条流水线:读取文件,接着打印df.info()看列名和类型,然后做类型转换和清洗,再按业务逻辑筛选、排序、去重,最后画图或者导出文件。这条流水线前一步的产物是后一步的输入,缺失任何中间环节,后面都会返工。
为什么强调这件事?因为很多人在学基础三时会陷入“收集命令”的误区:今天记一个drop_duplicates,明天抄一个to_datetime,感觉自己学了好多,但真拿到一份乱糟糟的Excel时完全不知道从哪下手。真正有用的不是命令清单,而是顺序感——先看类型,再决定转不转、怎么转,转完之后才谈得上分析和导出。这一篇的章节顺序,就是按这条真实加工链路排的。
2. 数据类型转换:绕不开的三板斧
在pandas基础三阶段,类型转换是出现频率最高的操作。场景往往是这样:你用read_csv读进来一个“销售额”列,dtypes一看是object,直接sum()得到一串数字拼在一起的文本;或者你拿到一个“2024年5月1日”形式的日期列,它实际上是字符串,排序时按字典序排得乱七八糟。这些问题的本质都一样:pandas把数据按“看起来的样子”存了,而我们要按“实际应该是什么”来用。
日常80%的类型转换需求,其实落在三个函数上:astype、pd.to_numeric、pd.to_datetime,外加一个字符串专用通道str。把它们逐个讲透,比背二十个冷门参数有用。
2.1 astype:强制转换,但要求数据足够干净
astype是看起来最“硬核”的转换工具,直接把一列转成指定类型:
df['age'] = df['age'].astype(int) df['score'] = df['score'].astype(float) df['city'] = df['city'].astype('category')转数值型是它的常规用法;category类型倒被很多人忽视。当你的数据里某个文本列只有几种取值,比如城市名只有十几个,转成category后底层按整数存储,内存占用能降不少。如果你在处理几十万行级别的数据,这个优化是立竿见影的。
astype最需要记住的坑有两个。第一个,它返回的是一个副本,不会直接修改原DataFrame。必须写赋值:
df['col'] = df['col'].astype(float)直接写df['col'].astype(float)不管结果,列不会有任何变化。这是新手最容易忽视的一条。
第二个坑更致命:astype遇到脏数据直接抛异常。比如这一列里混进一个“不详”,整列转换直接中断,后面的代码全不执行。所以astype只适合你已经确认数据足够干净的场景。如果你没把握,更稳妥的方案是下一小节要说的pd.to_numeric。
2.2 pd.to_numeric:清洗脏数字的默认武器
和astype的“一刀切”不同,pd.to_numeric更像一个“宽容派”。它专门用来处理“看起来是数字,实际上不是纯数字”的列。比如单元格里写着“1,200.00”“800元”“N/A”这些内容,用astype一个都过不去,但to_numeric能配合errors参数把其中能转的转掉,转不了的置成NaN。
errors参数有三个取值:raise是默认值,遇到非法内容直接抛错;coerce会把转不过去的变成NaN;ignore则留着原样返回整列。日常使用我几乎只碰coerce:
df['amount'] = pd.to_numeric(df['amount'], errors='coerce')转完以后,紧跟着做一件关键的事:把NaN筛出来看。因为这些NaN位置就是脏数据的藏身之处:
print(df[df['amount'].isna()])这个“先转换,再排查NaN”的思路,比一开始就在源文本里各种replace来清洗高出不知道多少倍。它不用你预判脏数据长什么样,只需要转完看一眼,就知道哪些行需要人工处理,是删是改一目了然。我处理报表时遇到金额列混着“-”“不详”“N/A”,这套流程是默认进场动作,没有之一。
2.3 pd.to_datetime:日期时间列的正确打开方式
日期转换是基础三里最容易被轻视的一环。原因很好理解:一个“2024/5/1”看着就是日期,你觉得pandas肯定能读懂。大部分情况下它确实能读懂,但有一种情况会出大问题,那就是“01/05/2024”这种格式。pandas自动推断时很可能把日当成月、月当成日,也就是按美式习惯解析,结果你的2024年5月1日就变成了2024年1月5日。
这种错误相当隐蔽,因为代码不报错、结果也能跑,直到画图时趋势线完全错乱,你才会回头怀疑是日期顺序反了。所以我的建议是:遇到非标准格式,用format参数明说:
df['date'] = pd.to_datetime(df['date'], format='%d/%m/%Y')%d/%m/%Y表示“日月年”。常用格式无非%Y-%m-%d、%d/%m/%Y、%Y/%m/%d %H:%M:%S这些,记不牢就查表,重要的是养成“转换后打印head()看一眼”的习惯,别相信自动解析,也别相信肉眼。
日期转换的价值不止于“格式好看”。一旦列变成datetime64类型,你就能用.dt访问器提取各种衍生字段:
df['year'] = df['date'].dt.year df['weekday'] = df['date'].dt.dayofweek df['month'] = df['date'].dt.month拿月份字段做分组汇总,是后续做月度趋势分析最常见的操作。时间列不转,这些全免谈。
2.4 str通道:批量字符串清洗的日常操作
字符串处理和类型转换看起来不搭边,但本质是一回事:把“不适合计算”的列变成“适合计算”的列。这里的主角是.str访问器,它让整个Series像一串字符串对象一样批量操作。
最常用的几个操作包括:df['name'].str.strip()去掉首尾空格,df['city'].str.contains('北京')返回布尔Series配合条件筛选,df['phone'].str.startswith('189')做手机号段筛选,以及str.replace配合正则表达式做批量清理:
df['phone'] = df['phone'].str.replace(r'\D+', '', regex=True)这一行把电话列里所有非数字字符删光,只剩11位号码。类似场景很多,比如从地址里抽邮编、从备注里抠金额,都是先replace清理再to_numeric转换。字符串通道同样返回新对象,记得接住返回值。
我遇到过一个很典型的坑:某份客户名单从Excel导入后,包括列名在内的很多单元格都带着全角空格或不可见字符,导致df['城市'] == '北京'怎么筛都是False。最后用df.columns = df.columns.str.strip()清掉列名空格,再对城市列做strip,问题立刻消失。这种问题你不会在日常练习中遇到,但面对真实数据时几乎无法避免。
3. 数据结构创建与重塑:索引才是底层逻辑
热词里“pandas数据结构创建”出现频率很高,但基础三阶段再提创建,重点已经不是“怎么建一个DataFrame”,而是“索引是怎么影响你后续所有运算的”。这一节把创建、排序、去重和重置索引放在一起讲,因为它们共用同一套底层逻辑:索引决定了pandas怎么对齐数据和运算结果。
3.1 创建Series和DataFrame时,最容易忽略的索引顺序
pandas有一个底层特性叫“数据对齐”,大白话说就是:两个Series做加减乘除时,它们不按位置对齐,而是按索引标签对齐。这在处理乱序数据时很强大,但对新人来说极其容易懵。
看这个例子:
s1 = pd.Series([10, 20, 30], index=['a', 'b', 'c']) s2 = pd.Series([1, 2, 3], index=['c', 'b', 'a']) print(s1 + s2)结果不是按位置计算得到[12, 22, 33],而是按标签配对:a和a加、b和b加、c和c加,最终结果索引是a, b, c,值分别是11, 22, 33。明白这点以后,你再看合并两个DataFrame之后顺序乱掉、筛选后索引断裂导致运算出现一堆NaN,就不会抓狂了。
DataFrame创建时也有一个容易忽视的点:如果你用“列表里放字典”的方式创建,列的顺序严格按第一个字典的键顺序;但如果你直接用numpy数组创建,pandas根本不知道每列该叫什么名字,只能手动指定columns。在线平台上很多“数据结构创建”类题目,考的往往不是创建本身,而是创建后你能不能正确索引和运算。
3.2 排序、去重之后,为什么多数情况都要reset_index
sort_values排序很简单,记得升序降序用ascending控制,多列排序时传一个列表,比如df.sort_values(['date', 'amount']),它会先按date排,再按amount排。drop_duplicates去重时需要关注subset参数,它决定按哪些列判断重复:
df.drop_duplicates(subset=['date', 'user_id'], keep='first')这行的意思是“同一天同一个用户只保留第一条记录”。如果你不写subset,它会看整行是否完全重复,很多时候未必是你想要的。
真正容易出事的环节是索引。筛选、排序、去重之后,DataFrame的索引往往变成0、3、5、7这种不连续的样子。如果你不处理,直接拿它去和其他数据运算,pandas会按这个残缺的索引去匹配,匹配不上的位置全部是NaN。解决办法就一行:
df = df.reset_index(drop=True)drop=True特别重要,不写的话,旧索引会被保留成一行叫index的新列,很多时候你根本不需要它。我在实训里见过不少同学,明明数据就几行,导出的文件里却多出一列“index”,就是忘记加drop=True。
3.3 apply与applymap:自定义逻辑的入口
pandas内置方法再多,总有覆盖不到的自定义逻辑。这时候apply是万能备胎。它可以沿指定轴将一个函数应用到每一行或每一列。比如把华氏温度列转成摄氏温度:
def to_celsius(f): return (f - 32) * 5 / 9 df['celsius'] = df['fahrenheit'].apply(to_celsius)简单逻辑也能写lambda:df['celsius'] = df['fahrenheit'].apply(lambda x: (x-32)*5/9)。如果逻辑更复杂,传一个带if、for的自定义函数完全没问题。
但要记住一个社区共识:apply的性能远不如向量化运算。能用df['col'] * 2实现的需求,绝对不要写成df['col'].apply(lambda x: x*2)。apply的存在是为了兜底,不是用来替代正常运算的。
applymap则作用在整个DataFrame上,对所有元素应用同一个函数,比如把数值统一格式化成百分比字符串。基础三阶段知道有它就行,日常使用频率不算高。
4. 文件读写:read_csv到to_excel,成败都在细节里
文件读写是数据“进硬盘、出硬盘”的环节,也是热词里“pandas读写文本文件”反复被搜的原因。很多人以为read_csv就是把文件读进来,实际上决定成败的全是参数细节。这一节把csv和Excel两大类读写讲透。
4.1 read_csv:编码、分隔符、日期解析三大高频参数
读csv能不能跑通,最先影响成败的不是列名,而是编码。国内环境从Excel导出的csv很多是GBK编码,直接pd.read_csv('file.csv')大概率抛UnicodeDecodeError,加上encoding='gbk'就通了。如果文件是从系统导出的,往往是utf-8,不确定时可以先拿记事本打开看一眼,中文不乱码就说明当前编码判断没错。
第二个高频参数是sep。都知道“csv是逗号分隔”,但实际生产环境的csv可能用分号、制表符甚至竖线做分隔符。不指定sep,pandas只会按逗号切,整列数据会被揉成一团,那是真头大。
第三个是parse_dates,读取的同时直接把时间列解析成datetime类型,省得事后用to_datetime再转一次。顺带还可以用usecols只读特定列,对大文件来说能显著省内存。
把这些合起来,一个典型的读取语句长这样:
df = pd.read_csv( 'sales.csv', encoding='gbk', sep=',', parse_dates=['日期'], usecols=['日期', '金额'] )一行读取直接跳过了一半的类型转换问题。写csv也有两个固定习惯:to_csv时加index=False,否则索引会被写进文件第一列;导出给同事用Excel打开时加encoding='utf-8-sig',否则中文在Excel里会乱码。这两个参数组合,是我给业务方导出文件前的固定动作。
4.2 读写Excel:sheet、索引和ExcelWriter
Excel读写比csv多一层“sheet”的概念。读的时候很容易忽略sheet_name参数,不指定就默认读第一个sheet。文件里如果恰好有多个sheet且你要的不是第一个,数据全错,而且不会报错提醒你。
写Excel时,to_excel默认会把DataFrame的索引写成一列,所以同样要index=False。想把多个DataFrame写进同一个Excel的不同sheet,用ExcelWriter是最干净的方式:
with pd.ExcelWriter('report.xlsx') as writer: df_summary.to_excel(writer, sheet_name='汇总', index=False) df_detail.to_excel(writer, sheet_name='明细', index=False)这个写法会自动创建文件并把两个sheet写进去,比先写一个再openpyxl拼来拼去省事太多。有一点提醒:如果要在已有文件上追加sheet,有的pandas版本需要给ExcelWriter传mode='a',默认的mode='w'会覆盖原文件。旧版pandas甚至不支持mode='a',遇到这种需求第一反应是升级pandas,而不是硬折腾。
4.3 多文件合并:concat比循环追加靠谱
当你要合并十几份同结构csv,比如每个月一个销售文件,最省心的方案不是用循环一次次往一个空DataFrame里append,而是先收集全部DataFrame,再一次concat:
import glob files = glob.glob('data/2024_*.csv') frames = [pd.read_csv(f) for f in files] df_all = pd.concat(frames, ignore_index=True)concat默认纵向拼接,也就是把行堆叠起来。ignore_index=True表示拼接后重新生成0到n-1的连续索引,这个参数极其重要,不写的话合并结果会带着一堆重复的旧索引,给后续操作埋雷。如果要做横向拼接,传axis=1,这时要特别小心两边行的顺序是否一致,最稳妥的做法是都按同一列排序后再拼。
现在处理多份同结构数据,我基本都是这个套路,又快又不容易错。
5. pandas与matplotlib的配合练习
pandas和matplotlib是老搭档。一个负责把数据整理得服服帖帖,一个负责把结果画出来。基础三阶段,你不需要掌握很复杂的绘图,甚至不需要专门系统地学matplotlib,用好DataFrame自带的plot()方法就能覆盖大部分日常图表。
5.1 用plot()快速出折线图和柱状图
DataFrame.plot()默认画折线图。你把一个带索引的Series或DataFrame直接调用,再配一行plt.show(),图就出来了。举一个例子:按月汇总销售额后,得到一个月度和金额的DataFrame,直接df.plot()就是一张趋势折线图。
有一个极常见的坑:pandas本身不显示图片,它只是构建了matplotlib的底层对象,必须调用plt.show()才能弹出窗口,或者用plt.savefig('figure.png')保存成文件。我第一次跑图时,代码没报错,但窗口什么都不弹,查了半天才发现是忘了plt.show()。
柱状图也很简单,给plot加一个kind='bar'参数就行,比如按“客户类型”分组后的销量对比,一眼就能看出哪个组最高。在在线练习里,这类题多半要求你把处理好的分组结果画出来,核心其实在前面那步“分组聚合”,画图只是收尾。
5.2 hist与boxplot:数据分布一眼看透
描述数据的分布,看表格里的均值、方差远不如看直方图直观。df['price'].hist()能把价格字段的分布画成直方图,异常值会在右侧拖出长尾,或者在某几个区间突然堆积,肉眼一秒就能发现。这个能力在做数据质量检查时非常好用。
箱线图boxplot则适合比较多组数据的分布,比如不同城市价格分别长什么样。它用四分位数把数据的分布区间画出来,超出箱体范围的异常值会单独排在一边,清洗数据时拿着这个图去定位异常记录,效率很高。
基础三阶段我建议先把hist用熟,boxplot知道怎么调出来就行。这两张图对应着“单列分布”和“分组分布”两种最常见的观察需求,足够应付绝大多数报告场景。
5.3 画图前必须确认的三个前提
画图本身很少出错,出错大多是在前面的数据处理环节。我总结了三个反复踩的点。
第一,列类型没转好。时间列还是字符串就画x轴,横轴全是乱序文本;数值列没转成float,图上的线可能变成只有0和1两个取值。这件事再次印证了第2章的重要性:类型转换没做好,画出来的图都是错的。
第二,索引混乱。筛选、去重后忘了reset_index,图表的x轴会莫名其妙出现断点,因为索引不连续。解决方案还是那句:处理完数据记得reset_index(drop=True)。
第三,中文显示问题。matplotlib默认字体没有中文字符,坐标轴标题、图例凡是中文全变成方块。临时解决方案是设置中文字体,比如plt.rcParams['font.sans-serif'] = ['SimHei'],或者干脆把图表标题、坐标轴标签写成英文。每次画图前,我的习惯是先df.info()看一眼各列类型,确认都正常再动手。这习惯帮我避免了很多低级返工。
6. 常见问题排查与个人避坑速查
最后把基础三阶段最高频的问题整理成速查形式。这些不是从文档里抄来的,是我实际踩过的坑和验证过的解法。
6.1 安装报错的排查路径
Could not find a version that satisfies the requirement pandas (from versions: none)这个报错,几乎成了国内新手装pandas的第一道坎。我的排查顺序是:
- 先查pip版本:
pip --version,如果很旧就先python -m pip install --upgrade pip。 - 换镜像源:原命令后面加
-i https://pypi.tuna.tsinghua.edu.cn/simple。清华源对国内网络非常友好,成功率极高。 - 看Python版本和位数:pandas针对不同Python版本发布对应安装包,如果Python太老,新版pandas可能没有对应的预编译版本。
- 如果仍在报错,把pip和setuptools都更新一次,再重试。
还有一种PyCharm特有的迷惑场景:界面里明明显示装好了,运行却提示No module named 'pandas'。这多半是解释器选择错了,项目右下角或设置里选了A解释器,但实际安装包的却是B解释器。赶紧检查当前运行的Python路径,比重新装包有用。
6.2 运行期高频报错清单
把我在基础三阶段见过最多的报错整理成一张表,方便对照。
| 报错或现象 | 原因 | 建议处理 |
|---|---|---|
| UnicodeDecodeError | csv编码不是utf-8 | 改用encoding='gbk' |
| No module named 'pandas' | 解释器/环境不对 | 检查PyCharm解释器路径 |
| ValueError: could not convert string to float | astype遇到脏数据 | 改用to_numeric(..., errors='coerce') |
| DateFormatter found invalid dates | 时间列还是字符串 | 先pd.to_datetime转换 |
| 画图时中文全是方块 | matplotlib字体无中文 | 设置中文字体或改用英文标签 |
| 导出的csv第一列是数字 | 索引被写进文件 | to_csv(..., index=False) |
| 两个Series相加结果全是NaN | 索引对不上 | 确认对齐方式,必要时重置索引 |
| Excel导出后多出一列index | 忘了drop=True或index=False | 在相应方法里显式关闭索引 |
只要看到表里的前两列,基本就能对上后一列的处理方式。排错的关键不是重复“安装卸载”,而是先定位问题属于“环境问题”还是“数据类型问题”。
6.3 几条长期受用的日常习惯
第一,凡是对列做类型转换,结果必须重新赋值回去。pandas大部分操作返回新对象,不是原地修改。写的代码越多,越容易在这一点上翻车。
第二,读完文件先df.info()再看数据,没有例外。info()能同时暴露列名、非空数量、各列类型,相当于给接下来的处理做一次体检。跳过这一步直接分析,大概率会在中途被类型问题打断。
第三,一个步骤单独一行,别把十个操作链式写成一个超长表达式。链式写法看起来很酷,排错时却很痛苦。拆开写,每行做一件事,处理出错时一眼就能定位到哪一行。
第四,凡是导出给同事或外部看的文件,统一加index=False,需要中文不乱码就再加encoding='utf-8-sig'。这两件事成了肌肉记忆后,几乎没有人再因为你的导出文件翻过车。
我个人用了pandas几年之后回头看,基础三这个阶段真正宝贵的不是多记住了几个函数,而是终于建立起了“数据需要预处理”的意识。一旦你习惯了“拿到任何表格都先看类型、再想转不转、转完再加工”的节奏,后面学分组聚合、学多表关联、学可视化,都只是在这条流水线上加新环节而已。如果你此刻正卡在某个类型转换或者读文件报错上,按着上面的思路一步步排查,多半能走出来。