1. 从数据结构定位说起:为什么Series运算值得单独开一篇
做数据分析的朋友都知道,Pandas里有两个最主要的容器:DataFrame和Series。DataFrame看起来更像一张表,Series看起来就是一列数据加上一个行标签。很多初学者习惯把精力都放在DataFrame上,觉得Series太简单、不值得花时间,真正开始做数据处理后就会发现,DataFrame里几乎所有列级别的操作,本质上都是把某一列取出来当作Series,再对Series做运算。你掌握得越扎实,后面写复杂聚合、写清洗逻辑的时候就越顺手。
这一篇我们聚焦Series的“常用运算”,不聊创建、索引这些基础(前面的文章已经完整覆盖过),就聊拿一个Series在手之后,怎么把它用活。包括算术运算、比较运算、统计运算、布尔筛选、去重排序、缺失值处理,以及这些操作在实际项目中常见的坑。我把每个知识点都配合代码演示和踩坑记录写清楚,方便你直接照着练,也能用在真实的分析任务里。
先提醒一句:如果你的Pandas版本还停留在1.x,部分输出格式略有差异,但不影响逻辑。我本地环境是Python 3.10 + Pandas 2.0.3,读这篇文章的时候建议至少Pandas 1.5以上,尽量贴近新版本,因为很多行为优化是结合新版本调整的。
import numpy as np import pandas as pd pd.set_option('display.max_columns', 50) pd.set_option('display.width', 150)2. 算术运算与广播机制:用好“对齐”才能少掉头发
2.1 Series与标量运算:一个循环都别写
先说说最简单也最高频的场景:对整个Series做统一变换。比如订单金额从元换算成万元、价格乘以折扣系数、对百分比字段做归一化,这些操作直接用运算符就行。
price = pd.Series([199, 299, 159, 399, 249], name='price') print(price * 0.85)0 169.15 1 254.15 2 135.15 3 339.15 4 211.65 Name: price, dtype: float64这个结果背后核心机制是广播,Pandas会自动把标量0.85扩展到每个元素上。整个过程是向量化的,底层走的是NumPy的循环,比Python原生写法高效得多。同样道理,加、减、乘、除、取余、幂运算都可以直接用:
s = pd.Series([10, 20, 30]) print(s + 1) print(s - 2) print(s / 4) print(s // 7) print(s ** 2)0 11 1 21 2 31 dtype: int64 0 -1 1 18 2 28 dtype: int64 0 2.5 1 5.0 2 7.5 dtype: float64 0 1 1 2 2 4 dtype: int64 0 100 1 400 2 900 dtype: int64有个细节值得注意:整数Series做除法时会自动转成浮点数,因为结果是小数,Pandas不会强行取整。但是取整除法和取余则会保持整数类型,这一点在做金额分账、分页计算的时候要提前想清楚,避免类型隐式转换引发的误判。
这里还有一个小坑,就是Series为整型时,如果标量是负数,取模运算结果和Python原生行为一致,但和C、Java等语言不同。遇到混用的情况,先确认符号是否符合预期。
2.2 Series与Series运算:索引对齐才是精髓
两个Series做运算,最核心的机制是索引对齐。也就是Pandas会把标签相同的元素配对计算,标签不同的位置结果为NaN。这个特性非常强大,也特别容易踩坑。
s1 = pd.Series([1, 2, 3], index=['a', 'b', 'c']) s2 = pd.Series([10, 20, 30], index=['b', 'c', 'd']) print(s1 + s2)a NaN b 22.0 c 33.0 d NaN dtype: float64s1里有a、b、c,s2里有b、c、d,最终结果是四个索引的并集。a和d只有一个系列有值,结果就是NaN。这里面的b是1+20=22,c是3+30=33,都没问题。
很多人第一次遇到这种情况会以为数据算错了,其实这正是Pandas的设计核心之一。真实项目中经常遇到两个不同来源的数据,需要按日期、按用户、按品类对齐,这时候直接用加号,比写循环匹配省太多事。
但反过来说,如果没意识到索引对齐,就会在自己没注意的时候把数据算成NaN。尤其两个Series的索引顺序不一样但元素数量一样时,Pandas仍然是按标签匹配,而不是按位置匹配。这是个高频陷阱。
left = pd.Series([1, 2, 3], index=['x', 'y', 'z']) right = pd.Series([4, 5, 6], index=['z', 'y', 'x']) print(left + right)x 7 y 7 z 7 dtype: int64这里索引标签虽然顺序不同,但因为每个标签都能对上,所以结果全部有值。如果内心默认“按顺序加”,一旦遇到索引顺序不一致的数据就会出错,而且这种错误往往很隐蔽,肉眼不容易察觉。
2.3 与NumPy数组、列表运算:位置对齐规则要分清
Series与列表或NumPy数组运算时,不按索引对齐,而是按位置对齐。也就是说Pandas会忽略Series的索引标签,直接把序列值和传入的序列逐个位置相加减。如果长度不一致,会直接报错。
s = pd.Series([1, 2, 3], index=['a', 'b', 'c']) arr = np.array([10, 20, 30]) print(s + arr)a 11 b 22 c 33 dtype: int64再对比一下:
arr_reversed = np.array([30, 20, 10]) print(s + arr_reversed)a 31 b 22 c 13 dtype: int64看出区别了吗?传入NumPy数组时,标签a对应的1加的是30而不是10,说明是位置对齐。这在实际项目中很常见:你从数据库取到一列数据,又拿到一份外部计算好的数组,两者含义是一一对应的,但顺序可能一致也可能不一致,结果就会天差地别。所以用数组做运算前,务必确认顺序确定有序,或者直接用索引对齐的方式合并成Series再算。
2.4 缺失值一旦出现,算术结果全变NaN
当Series本身含有NaN,任何算术运算都会让结果变成NaN,这就涉及“NaN传染”。
s = pd.Series([1, np.nan, 3, 4]) print(s * 2)0 2.0 1 NaN 2 6.0 3 8.0 dtype: float64这符合数据分析直觉:缺失值无法参与计算。包括加法、乘法、幂运算一概如此。
在数据清洗阶段,这是最折磨人的部分。你以为数据都填补干净了,结果一跑统计全是NaN,多半就是某列还有残留缺失值。所以做任何运算前,先检查isna().sum()和dropna(),这个习惯必须养成。
另外,Series加Series时,如果一边缺失,结果就是NaN,不会自动把非缺失那一侧的值保留下来。想实现“有值用值、没值用另一边”的效果,得用combine_first或fillna,后面会专门讲。
3. 缺失值处理:没有NaN意识,统计结果全白搭
3.1 缺失值的来源:别等结果异常了才回头查
缺失值最常见来源有四种:一是原始数据本身就是空的,数据库里存的是NULL,导入Pandas后就变成NaN;二是索引对齐时两端标签不一致,补出来的位置是NaN;三是左连接、外连接这种方式合并数据表之后,NaN是一种常态;四是我们自己用赋值的方式指定的缺失标记,比如把某些异常值替换成np.nan,后面统一处理。
s = pd.Series([100, 200, None, 400]) print(s)0 100.0 1 200.0 2 NaN 3 400.0 dtype: float64Python自带的None传入Series后,Pandas自动识别并转成NaN,输出时展示为NaN。这一行为在Pandas 2.0里更加严格,建议日常处理中尽量统一使用np.nan或pd.NA,避免None和NaN混用导致判断异常。
3.2 判断缺失值:isna还是isnull?别选错
Pandas提供了两组方法:isna/isnull、notna/notnull。它们的作用完全一样,isna与isnull是别名关系。个人习惯用isna,写起来短,而且和DataFrame的interpolate、fillna这些方法命名风格一致,语义更清晰。
s = pd.Series([1, np.nan, 3]) print(s.isna())0 False 1 True 2 False dtype: bool布尔结果的Series最常用在两个场景:一是统计缺失个数,直接sum()即可;二是作为布尔索引,只保留非缺失或只保留缺失的部分。
data = pd.Series([10, np.nan, 30, np.nan, 50]) valid = data.notna() print(data[valid])0 10.0 2 30.0 4 50.0 dtype: float64这比在循环里判断再过滤元素快得多,也更符合Pandas的向量化思维。
3.3 缺失值剔除与填充:两个策略三条路
处理缺失值一般两条路:要么删掉,要么填上。删掉直接用dropna(),默认是丢掉所有含NaN的行:
s = pd.Series([1, np.nan, 3, np.nan, 5]) print(s.dropna())0 1.0 2 3.0 4 5.0 dtype: float64dropna返回的是一个新的Series,不会原地修改原数据。如果你希望直接改原对象,用inplace=True,或者赋回去。实测中inplace=True在很多Pandas新版本里已经被标记为即将弃用,建议直接用赋值方式。
填充用fillna(value),可以把所有缺失值替换成固定值。最常见的几种场景:
s = pd.Series([1, np.nan, 3, np.nan, 5]) print(s.fillna(0)) print(s.fillna(method='ffill')) print(s.fillna(method='bfill'))0 1.0 1 0.0 2 3.0 3 0.0 4 5.0 dtype: float64 0 1.0 1 1.0 2 3.0 3 3.0 4 5.0 dtype: float64 0 1.0 1 3.0 2 3.0 3 5.0 4 5.0 dtype: float64ffill表示用前一个有效值填充,bfill表示用后一个有效值填充。fillna(method='ffill')在Pandas 2.0中已经被调整为使用fillna(method=...),但在某些版本会有告警,更推荐的方式是:
print(s.ffill()) print(s.bfill())ffill和bfill这两个方法在时间序列处理里特别有用。比如传感器数据每隔几秒记录一次,中间偶尔丢包,用ffill把上一个观测值延续到缺失位置,是最自然的填补策略。
但注意一点:如果缺失值出现在最前面,ffill无法填补,因为前面没有有效值;bfill同理无法填补最后面的缺失值。这时可以组合使用:
print(s.ffill().bfill())先向前填充,再向后填充,这样首尾缺失都能处理,但也要结合实际场景确认这样做是否合理。
3.4 还有一条路:插值法
比固定值和前后填充更精细的是插值法。Series.interpolate()默认使用线性插值,它根据缺失值前后两个有效值的坐标,计算中间等距位置的估计值。
s = pd.Series([1, np.nan, np.nan, 4]) print(s.interpolate())0 1.0 1 2.0 2 3.0 3 4.0 dtype: float64这里两个NaN被线性插值为2.0和3.0,刚好落在1到4的等分点上。插值法适合数据本身有连续变化趋势的场景,比如温度曲线、股票价格序列、网页访问量的时间序列。如果数据是离散分类变量,插值就不合适了。
还有一点提醒:插值默认按索引数值大小进行。如果索引是字符串或非数值类型,部分插值方法会失效,使用前先检查索引类型。
4. 比较运算与布尔索引:几乎所有筛选逻辑都离不开它
4.1 比较运算的结果是什么
Series与标量做比较,返回的是一个布尔Series,每个位置是True或False。这个布尔Series几乎总是用来做下一步筛选的。
score = pd.Series([88, 92, 75, 60, 45], index=['A', 'B', 'C', 'D', 'E']) print(score > 60)A True B True C True D False E False dtype: bool很多人以为score > 60会像普通数值运算一样返回一个新Series的分数结果,其实不是,它返回的是布尔掩码。要拿到筛选后的具体分数,还需要用这个掩码去索引原Series:
print(score[score > 60])A 88 B 92 C 75 dtype: int64这种“布尔掩码”的思维是Pandas的核心用法,跟NumPy完全一致。多条件组合时,必须用&(与)、|(或)、~(非)连接,不能用Python的and、or、not,因为后者只能用于标量布尔值,不能用于数组或Series。
print(score[(score > 60) & (score < 90)]) print(score[(score < 60) | (score > 90)])A 88 C 75 dtype: int64 B 92 E 45 dtype: int64注意每个条件都要用括号包起来,因为Pandas的运算符优先级跟普通算术不同,忘记加括号会直接报错或给出奇怪的结果。
4.2 isin与between:两个高频替代手工条件的方法
有时候筛选条件不是“大于某个值”或“小于某个值”,而是“是否属于某个集合”。比如筛选指定几个城市的数据、指定几个商品类别的数据,用isin就非常简单。
city = pd.Series(['北京', '上海', '广州', '深圳', '杭州']) target = ['北京', '杭州', '成都'] print(city.isin(target))0 True 1 False 2 False 3 False 4 True dtype: bool结合布尔索引直接取出目标城市:
print(city[city.isin(target)])0 北京 4 杭州 dtype: objectbetween方法用于判断是否在某个区间内,闭区间两端都包含。这个方法比写上两个比较条件再加&更简洁。
age = pd.Series([18, 25, 33, 42, 56]) print(age.between(20, 40))0 False 1 True 2 True 3 False 4 False dtype: boolbetween也支持inclusive参数,用来控制边界是否包含:
print(age.between(20, 40, inclusive='left'))inclusive取值有'both'、'left'、'right'、'neither',默认是'both',即两端都包含。想开区间时用'neither',左开右闭用'right',非常灵活。
4.3 any与all:批量判断的快捷方式
当得到一个布尔Series后,有时我们不需要逐行处理,只需要判断整体上是否存在满足条件的数据,或者是否全部满足。这时候用any和all:
score = pd.Series([88, 92, 75, 60, 45]) print(score.gt(100).any()) # 是否有大于100的分数 print(score.gt(40).all()) # 是否所有分数都大于40False True在数据质量检查场景中非常有用。比如检查某一列是否有负数、是否有空字符串、是否所有值都在合理区间内,一次any/all调用就出结论,不需要写循环。
4.4 布尔索引配合赋值:定向修改数值
布尔索引不止用于筛选展示,还能直接配合赋值实现定向修改。比如把低于60分的成绩改成60分:
score[score < 60] = 60 print(score)0 88 1 92 2 75 3 60 4 60 dtype: int64这个操作在数据清洗里很常用,比如把异常温度、异常价格直接设置成NaN,或者把超过某一阈值的值截断。
但这里有一个高频警告:如果你对DataFrame的列做类似操作时,触发的是链式赋值,可能会遇到SettingWithCopyWarning。虽然那个警告在DataFrame里更常见,但Series如果来自筛选出来的子视图,也可能遇到类似问题。建议做赋值之前,先确认当前Series是原对象的视图还是副本。最稳妥的办法是先用copy()显式复制:
score_copy = score.copy() score_copy[score_copy < 60] = 60虽然会多占一份内存,但对于数据量不大、分析任务追求稳定的场景,完全可接受。后面我会单独讲这个坑的成因和排查方式。
5. 统计运算与聚合:一个函数摸清数据全貌
5.1 描述性统计速查表
Series里内置了大量统计方法,用法统一,都是返回标量。我把最常用的整理成一张表:
| 方法 | 功能 | 备注 |
|---|---|---|
| sum() | 求和 | |
| mean() | 平均值 | |
| median() | 中位数 | 抗离群值能力强 |
| min() / max() | 最小值 / 最大值 | |
| std() | 样本标准差 | 分母为n-1 |
| var() | 样本方差 | 分母为n-1 |
| count() | 非缺失值个数 | 不含NaN |
| skew() | 偏度 | 判断分布对称性 |
| kurt() | 峰度 | 判断分布尖峭程度 |
| quantile(q) | 分位数 | q为0~1之间的小数 |
| cumsum() | 累积和 | 返回Series |
| cummax() | 累积最大值 | 返回Series |
| describe() | 综合统计描述 | 返回Series |
实操中几个细节要提一下。第一,mean、sum、std这些方法默认会跳过NaN,这是Pandas的默认行为。也就是说直接调用s.mean(),并不会因为存在NaN就返回NaN。这一点和前面的算术运算不同,很多人一时没想明白。
s = pd.Series([1, 2, np.nan, 4, 5]) print(s.mean())3.0结果是3.0,也就是(1+2+4+5)/4。如果想遇到NaN就返回NaN,可以加上skipna=False:
print(s.mean(skipna=False))nan第二,std和var的计算默认用样本公式,分母是n-1,而不是总体公式的n。如果你要算的是总体标准差,要自己手动处理或用相应参数,不同业务场景对“标准差”的定义并不一样,金融风控和一些质量统计会更严格,务必先确认口径。
5.2 describe:不止count、mean、std这几个指标
describe()能一键输出多个统计量,适合快速看数据分布。对于数值型Series,默认输出count、mean、std、min、25%、50%、75%、max。
price = pd.Series([199, 299, 159, 399, 249]) print(price.describe())count 5.000000 mean 261.000000 std 90.013888 min 159.000000 25% 199.000000 50% 249.000000 75% 299.000000 max 399.000000 dtype: float64注意25%、50%、75%这几个分位数默认使用线性插值算法。如果你需要更精确的自定义分位点,可以用quantile方法:
print(price.quantile([0.1, 0.9]))0.1 167.0 0.9 379.0 dtype: float64这里0.1分位数是167.0,0.9分位数是379.0,说明大部分价格集中在中低区间,高分位点拉高了整体均值。这种信息在定价策略和异常点识别中非常有用。
5.3 累计运算的常见用法
cumsum是累计和,也就是每个位置的值等于当前元素及之前所有元素的和。这在分析销售额累计、播放量累计、里程累计的时候非常常用。
sales = pd.Series([100, 200, 150, 300]) print(sales.cumsum())0 100 1 300 2 450 3 750 dtype: int64cummax和cummin则常用来判断当前值是否突破了历史最高或最低点,比如股票价格创出阶段新高:
high = pd.Series([10, 12, 11, 15, 14]) print(high.cummax())0 10 1 12 2 12 3 15 4 15 dtype: int64另外,cumprod是累积乘积,在计算复利增长、环比连乘的场景中会用到,但要注意溢出风险和数据量大小。
5.4 与NumPy通用函数的配合
有些统计逻辑Series没有内置方法,但可以用NumPy的通用函数来做。比如np.sqrt对每个元素开方、np.log取对数、np.abs取绝对值、np.round四舍五入。
s = pd.Series([1, 4, 9, 16]) print(np.sqrt(s))0 1.0 1 2.0 2 3.0 3 4.0 dtype: float64这也是Pandas与NumPy无缝衔接的体现。对数值型Series做np.log时,如果元素包含0或负数,会产生-inf或NaN,要提前考虑是否需要偏移,比如log1p,即log(x+1),是处理非负偏态数据的常用手段。
6. 去重、排序、排名与值频次统计:清洗与观察的一把梭
6.1 unique、nunique与drop_duplicates:三个去重别混用
unique()返回的是去重后的数组,类型是NumPy ndarray,不是Series。
s = pd.Series(['苹果', '香蕉', '苹果', '橙子', '香蕉', '苹果']) print(s.unique())['苹果' '香蕉' '橙子']nunique()返回去重后的元素个数,是标量整数。特别适合快速查看分类变量有多少个类别。
print(s.nunique())3drop_duplicates()返回的是去掉重复行之后的Series,保留的是每个重复值第一次出现的位置。
print(s.drop_duplicates())0 苹果 1 香蕉 3 橙子 dtype: object三者应用场景不同:想列出去重后的所有取值用unique;只想知道有几个类别用nunique;想在保留原Series完整索引体系的前提下过滤重复项用drop_duplicates。
6.2 value_counts:分类数据的黄金方法
value_counts()是分类变量统计中最高频的方法。它统计每个值出现的次数,默认按次数降序排列。
city = pd.Series(['北京', '上海', '北京', '广州', '北京', '上海']) print(city.value_counts())北京 3 上海 2 广州 1 dtype: int64实际工作中常用normalize=True把频次转换成占比:
print(city.value_counts(normalize=True))北京 0.500000 上海 0.333333 广州 0.166667 dtype: float64这样直接得出“北京占比50%”等结论,尤其适合做品类占比、渠道占比、用户分层占比等分析。
value_counts还能按区间统计,比如把连续年龄切成年龄段。先用pd.cut把连续值离散化,再调用value_counts。
ages = pd.Series([23, 45, 31, 28, 52, 36, 41, 29]) bins = pd.cut(ages, bins=[0, 30, 40, 60]) print(bins.value_counts())(0, 30] 4 (30, 40] 2 (40, 60] 2 dtype: int64这个方法在分析用户年龄分布、收入分层、时长分桶时很实用。
6.3 sort_values与sort_index:别忽略排序的稳定性
sort_values按值排序,sort_index按索引排序。默认都是升序。
s = pd.Series([88, 92, 75, 60, 45], index=['D', 'A', 'E', 'B', 'C']) print(s.sort_values()) print(s.sort_index())B 60 C 45 A 92 D 88 E 75 dtype: int64 A 92 B 60 C 45 D 88 E 75 dtype: int64下面说一个真实项目里容易忽略的点:sort_values默认使用快速排序算法(quicksort),它不是稳定排序。这意味着如果Series里有多个相同的值,排序后它们的原始相对顺序可能被改变。如果你需要保持相同值的先后关系,可以指定kind='mergesort'。
s = pd.Series([1, 3, 2, 3, 1]) print(s.sort_values(kind='mergesort'))0 1 4 1 2 2 1 3 3 3 dtype: int64可见两个1的原始位置0和4、两个3的原始位置1和3,在mergesort下都保持了原有顺序。这个细节在日常排序里用得不多,但在某些需要可复现排序结果的场景下值得注意。
6.4 rank方法:并列时的排名策略
rank()用于计算排名,它和sort_values不同,sort_values是排序,rank是给每个元素一个名次。
score = pd.Series([90, 85, 90, 76, 88]) print(score.rank())0 4.5 1 2.0 2 4.5 3 1.0 4 3.0 dtype: float64这里的默认方法是average,也就是两个90分并列第4和第5名,平均后都取4.5。如果希望并列时都取最低名次,用method='min',都取最高名次用method='max',按出现顺序给名次用method='first'。
print(score.rank(method='min')) print(score.rank(method='max')) print(score.rank(method='first'))0 4.0 1 2.0 2 4.0 3 1.0 4 3.0 dtype: float64 0 5.0 1 2.0 2 5.0 3 1.0 4 3.0 dtype: int64 0 4.0 1 2.0 2 5.0 3 1.0 4 3.0 dtype: float64做竞赛排名、业绩排行、分数等级划分时,选对排名策略很重要。默认的average会让人疑惑“排名怎么会有小数”,其实这是Pandas为了让并列名次公平而设计的处理方式。
6.5 idxmax与idxmin:取极值对应的索引
除了知道最大、最小值是多少,很多时候更需要知道“最大/最小值发生在哪一行”。比如找出销量最高的月份、温度最高的日期、价格最低的商品。
monthly_sales = pd.Series( [120, 150, 110, 180, 160], index=['1月', '2月', '3月', '4月', '5月'] ) print(monthly_sales.idxmax()) print(monthly_sales.idxmin())4月 3月这让结论更有业务意义:不只是“最高销量是180”,而是“最高销量出现在4月,达到180”。在项目汇报里,这个信息比单纯数字直观得多。
需要注意,如果Series中存在多个相同的最大值,idxmax默认返回第一个最高值所在的位置。这一点和nlargest(1)行为不同,后者会返回排序后的结果,而idxmax只返回索引标签。
nlargest和nsmallest也是高频方法,它们不排序整个Series,而是直接取出最大的前n个或最小的前n个。
print(monthly_sales.nlargest(2)) print(monthly_sales.nsmallest(2))4月 180 5月 160 dtype: int64 3月 110 1月 120 dtype: int64数据量很大时,nlargest比sort_values().head(n)效率更高,因为底层用了堆排序,不需要把全部数据完整排序。
7. 常见问题与排查技巧实录
7.1 索引不匹配导致结果全是NaN
这是我见过新手最容易踩的坑,也是最难排查的一类问题。两个Series明明数值看起来差不多,一相加结果却全是NaN,第一反应往往是数据类型有问题,打开数据一看又觉得没事,最后才意识到是索引没对齐。
s1 = pd.Series([100, 200, 300], index=[0, 1, 2]) s2 = pd.Series([400, 500, 600], index=[1, 2, 3]) print(s1 + s2)0 NaN 1 600.0 2 800.0 3 NaN dtype: float64为什么会这样?因为Pandas的算术运算按索引标签配对,0和3这两个标签只在一边存在,所以结果NaN。解决办法是先对齐索引,再填充缺失值,或者使用add方法的参数:
print(s1.add(s2, fill_value=0))0 100.0 1 600.0 2 800.0 3 600.0 dtype: float64这里fill_value=0告诉Pandas遇到缺失的索引时按0参与运算,两个缺失的位置都补齐了。但要注意,这种处理是否合理完全取决于业务语义,如果缺失代表“没有数据”,用0填充是合适的;如果缺失是“采集异常”,填充0会歪曲结果。
7.2 布尔索引的优先级问题:and与&的混乱
我见过很多人在多个条件筛选时报错,报错信息五花八门:ValueError、TypeError都有,最终原因大多是混用了Python原生的and与Pandas的&。
s = pd.Series([10, 20, 30, 40]) # 错误写法 # s[(s > 10) and (s < 40)] # ValueError # 正确写法 print(s[(s > 10) & (s < 40)])1 20 2 30 dtype: int64这里有两个教训:第一,对Series做元素级逻辑运算,必须用&、|、~;第二,每个子条件都必须加括号,因为&的优先级高于比较运算符,不加括号会被解释成先做位运算再做比较,结果完全错误。
7.3 dtype为object导致运算异常
当一个Series的dtype是object时,其实就是Python对象的容器。它可能是字符串、混合类型或者包含None。对它做数学运算时,结果往往不符合预期。
s = pd.Series(['10', '20', '30']) # s + 5 会报错 TypeError:只能将str(不是int)连接到str try: print(s + 5) except TypeError as e: print(f"TypeError: {e}")解决办法是先转成数值类型:
s_numeric = pd.to_numeric(s) print(s_numeric + 5)0 15 1 25 2 35 dtype: int64pd.to_numeric还可以加errors='coerce'参数,把无法转换的值直接变成NaN,这在清洗脏数据时特别有用。比如一列“金额”字段里混入了字符"待定"、"N/A",直接to_numeric会报错,加coerce后自动变成NaN,再配合fillna处理。
7.4 inplace的坑:赋值不生效
Pandas里很多方法默认返回新对象,不修改原对象。新手常犯的错误是直接调用s.sort_values()却不接收返回值,导致排序“没生效”。
s = pd.Series([3, 1, 2]) s.sort_values() print(s)0 3 1 1 2 2 dtype: int64从输出看,s没有变化,因为sort_values返回的是新对象,原s还是原来的顺序。正确的做法有两种:
s_sorted = s.sort_values() print(s_sorted)或
s.sort_values(inplace=True) print(s)在新版本Pandas中,inplace=True虽然还能用,但官方更推荐返回新对象的写法。我个人建议一律使用第一种,因为链式调用更清晰,也不会因为误用inplace导致视图副本问题。
7.5 设置Pandas显示选项避免输出被截断
处理较长的Series时,Pandas默认只显示前后部分,中间用省略号代替。这在调试时很烦人。可以用pd.set_option调整:
pd.set_option('display.max_rows', 100) pd.set_option('display.max_columns', 50) pd.set_option('display.width', 200)这几个选项分别控制最大行数、最大列数、整体显示宽度。数据分析实战里,调大这些参数能减少很多不必要的疑惑。
8. 问题速查表:一边写代码一边查
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 两个Series相加结果出现大量NaN | 索引没有对齐 | 用add(fill_value=0)或先对齐索引 |
| s > 60结果不是数字而是True/False | 比较运算返回布尔Series | 用这个布尔Series做索引筛选 |
| 多个条件用and连接报错 | Python and不能用于Series | 改用&并给每个条件加括号 |
| series.unique()返回的不是Series | unique返回ndarray | 用pd.Series(s.unique())转换 |
| value_counts结果顺序不对 | 默认按频次降序 | 用sort_index或sort_values调整 |
| 字符串列做加法报错 | dtype是object | 用pd.to_numeric(..., errors='coerce') |
| fillna后原数据没变 | 方法默认返回新对象 | 赋值给新变量或使用inplace |
| describe没显示某些统计量 | 列可能是object类型 | 先to_numeric或astype('float') |
| 索引是日期类型但运算报错 | 类型不匹配 | 用pd.to_datetime统一转换 |
| sort_values后索引乱序 | 排序后索引跟着值移动 | 用reset_index(drop=True)重建索引 |
| 布尔索引赋值出现警告 | 链式赋值导致视图副本问题 | 先用copy()再赋值 |
| 包含NaN的Series求sum结果不是NaN | 统计方法默认跳过NaN | 用skipna=False或先检查缺失 |
这张表是我实际排查时最常用的索引,遇到问题优先对照一下,能省不少时间。
最后分享一个我自己的习惯:拿到一个陌生Series时,先跑一遍dtype、isna().sum()、describe()、head(),这四个操作能覆盖80%的数据质量判断。然后才是具体的业务运算。顺序对了,后面少踩很多坑。