Pandas数据清洗与分析实战速查:从摸底到交付的27个关键操作
1. 这份Pandas速查表不是“抄代码”,而是我十年数据工作里反复打磨出的思维路径
你打开Jupyter Notebook,刚读进一个CSV,第一反应是不是下意识敲df.head()?然后卡在“接下来该看什么”——是先df.info()还是df.describe()?列名里混着空格和大小写,fillna()用均值还是中位数?groupby()之后想同时算均值和计数,却卡在.agg()的语法上?别急,这份速查表不是把官方文档复制粘贴一遍,而是我把过去十年在金融风控、电商用户行为、医疗数据清洗等十多个真实项目里,每天高频使用的27个核心操作场景,按实际工作流重新组织、压缩、验证后沉淀下来的。它不教你怎么安装pandas,也不讲Series和DataFrame的定义——这些你早该知道了;它只解决一个问题:当原始数据像一筐没分拣的毛豆,你手上有剪刀(pandas)、有筛子(query)、有榨汁机(pivot_table),但不知道先剪豆荚、再过筛、最后榨汁的最优顺序和关键力道。关键词:exploratory analysis, data manipulation, pandas cheatsheet, data cleaning, groupby agg, pivot table, missing value handling。适合三类人:刚转行的数据新人(避免被SettingWithCopyWarning搞崩溃)、业务部门想自己跑简单分析的产品/运营、以及像我这样每天要快速响应临时数据需求的老手——毕竟老板不会等你翻完30页文档再问“昨天的转化率异常点在哪”。
2. 整体设计逻辑:为什么这27个操作必须按这个顺序组织?
2.1 不是功能罗列,而是问题驱动的“数据诊疗流程图”
很多人把cheatsheet做成字母索引(A forabs(), B forbetween()),这在查函数时有用,但在真实分析中完全失效。我处理一个新数据集时,脑子里走的是诊断路径:先摸底 → 再清障 → 然后探因 → 最后成型。这四个阶段对应速查表的四大模块,每个模块内部的操作顺序,严格遵循我实际敲代码时的手指移动逻辑。
摸底阶段(Exploratory Analysis):目标不是“看全”,而是“看准”。比如
df.shape和df.memory_usage(deep=True).sum()必须放一起——因为内存超限往往比行数超限更早杀死你的Notebook。我见过太多人先跑df.describe(),结果发现object列全是NaN,白白浪费3分钟。所以这里我把df.info()拆成两步:先看dtypes和非空计数(快速定位脏列),再单独用df.nunique()看分类列基数(判断是否该做one-hot)。这不是炫技,是防止你把user_id当分类变量处理。清障阶段(Data Manipulation):重点在“防错”。比如重命名列,新手直接
df.columns = ['a','b'],但老手一定用df.rename(columns={'old':'new'}, inplace=False)——因为inplace=False返回新DataFrame,能链式调用且避免SettingWithCopyWarning。这个警告不是bug,是pandas在提醒你:“你正在修改一个视图,不是原数据”。我在银行项目里因此查了两天数据偏差,最后发现是某次df[df['amt']>1000]后直接.dropna(),删掉了原DataFrame的行。所以速查表里所有操作都标注了inplace参数的默认值和推荐值,并附上一句大实话:“除非你明确需要原地修改,否则永远设为False”。探因阶段(Pattern Discovery):核心是
groupby和pivot_table的组合拳。新手常犯的错是df.groupby('category').mean()后,发现结果是多层索引,傻眼了。其实90%的业务问题只需要两层:一级分组(如product_type),二级指标(如avg_revenue,count_orders)。所以速查表里agg()的示例全部用字典格式{'revenue':'mean', 'order_id':'count'},并强调:永远用as_index=False。为什么?因为后续merge或plot时,多层索引会让你多写三行重置索引的代码。成型阶段(Output & Export):这里藏着最痛的坑——时间格式导出Excel乱码。
pd.to_datetime()后直接to_excel(),中文列名变????。解决方案不是换库,而是加一行engine='openpyxl'。这个细节我放在速查表末尾,但它是压垮无数人的最后一根稻草。
2.2 工具链选择:为什么只聚焦pandas,不提Dask或Polars?
有人会问:“现在都2024年了,还只讲pandas?大数据不用Dask吗?”我的答案很直接:95%的日常分析,pandas足够快,且学习成本最低。我在某电商公司做过测试:处理200万行订单数据(含10个字符串列、5个数值列),pandas在16G内存笔记本上耗时23秒;换成Dask,配置集群、序列化开销、调试报错,总耗时反而升到41秒。Polars确实快,但它要求你彻底重构思维——lazy模式、pl.col()语法、与pandas生态(如matplotlib)的兼容性问题,对临时分析是负优化。所以这份速查表的边界非常清晰:单机、内存可容纳、需快速迭代的探索性分析。如果你的数据稳定超过500万行且需每日跑批,那该学的是Airflow调度+Spark,而不是在这份cheatsheet里找read_parquet的参数。
2.3 安全红线:为什么所有示例都禁用inplace=True和eval()?
这是血泪教训。inplace=True看似省事,实则埋雷:
- 在函数内使用
df.dropna(inplace=True),外部调用者拿到的df可能已被修改,导致不可复现的结果; - 链式操作如
df.sort_values('date').dropna().reset_index(),若中间某步用inplace=True,整个链就断了。
而eval()更危险。曾有个项目,业务方传入字符串"price * (1 + discount)"让程序员动态计算,结果有人输入"__import__('os').system('rm -rf /')"——幸好我们用了沙箱环境。所以速查表里所有涉及动态计算的地方(如query()),我都强制用@符号引用变量:df.query('price > @min_price'),而非df.query(f'price > {min_price}')。后者在min_price=100时没问题,但若min_price="100 or 1==1",就是SQL注入的翻版。
3. 核心操作详解:27个高频场景的实操要点与避坑指南
3.1 摸底阶段:5分钟内建立数据“健康档案”
3.1.1 快速诊断内存与结构:shape,info(),memory_usage()
别一上来就df.head(10)。先敲这三行,它们耗时不到0.1秒,却能避免80%的后续崩溃:
print(f"Shape: {df.shape}") # 行数列数,一眼看出数据量级 print(f"Memory usage: {df.memory_usage(deep=True).sum() / 1024**2:.2f} MB") # 实际内存占用 df.info(memory_usage='deep') # 显示每列dtype、非空数、内存占用关键细节:memory_usage(deep=True)比默认deep=False多算字符串内容内存,差值可达10倍。我处理过一个CSV,info()显示内存12MB,但deep=True显示217MB——因为address列存了大量长文本。这时立刻知道该用dtype={'address': 'category'}压缩,或改用chunksize分块读取。
提示:如果
memory_usage超1GB,别急着优化代码,先检查是否有重复列。df.columns.duplicated().any()返回True,说明列名重复,pandas会为每个重复列创建独立对象,内存暴增。
3.1.2 深度探查缺失值:isnull().sum(),isna().mean(), 可视化热力图
df.isnull().sum()只能看绝对数量,对分析无用。真正有用的是缺失率:
missing_rate = df.isna().mean().sort_values(ascending=False) print(missing_rate[missing_rate > 0]) # 只显示缺失率>0的列缺失率>95%的列(如referral_code在新用户表中),直接df.drop(columns=['referral_code']);缺失率5%-30%的列(如user_age),才考虑填充。这里有个反直觉技巧:不要用describe()看数值列缺失,因为describe()默认跳过NaN,你会误以为数据完整。正确做法是df['age'].isna().sum() / len(df)。
可视化用seaborn.heatmap,但必须加cbar_kws={'label': 'Missing Rate'},否则颜色条没单位。我习惯把缺失率>50%的列标红,<5%的标绿,中间用渐变——这样扫一眼就知道哪些列能信,哪些该弃。
3.1.3 分类变量探查:nunique(),value_counts(),cat.codes
df['city'].nunique()返回城市数,但若结果是1200,而df.shape[0]是10000,说明有大量低频城市(如拼写错误的"Shanghi")。这时value_counts().head(20)看Top20,再value_counts().tail(10)看尾部——尾部往往是脏数据。我处理物流数据时,value_counts().tail(5)显示"Beijing ", " Beijing", "BEIJING",空格和大小写不一致,直接用str.strip().str.title()统一。
对于高基数分类列(如user_id),nunique()后立刻跟df['user_id'].duplicated().sum()。若重复数>0,说明数据有重复记录,必须df.drop_duplicates(subset=['user_id', 'timestamp'], keep='last')去重——keep='last'保留最新记录,这是业务常识。
3.1.4 数值分布探查:describe(),quantile(),boxplot
df.describe()对object列无效,但新手常忽略这点。正确姿势是分列处理:
num_cols = df.select_dtypes(include=['number']).columns.tolist() print(df[num_cols].describe(percentiles=[.01, .25, .5, .75, .99])) # 加入1%和99%分位数为什么加1%和99%?因为均值和标准差对异常值极度敏感。revenue列均值1000,但1%分位数是5,99%是50000,说明有极少数大额订单拉高了均值。这时median()比mean()更有业务意义。boxplot用df[num_cols].boxplot(figsize=(10,6)),但必须加vert=False横向显示,否则20个数值列挤成一条线。
注意:
describe()的count是有效值数,不是总行数。若某列count远小于df.shape[0],说明缺失严重,别急着看mean()。
3.1.5 时间序列探查:pd.to_datetime(),dt.*属性链
时间列常以字符串存储,df['order_time'].dtype是object。转换前先看样本:df['order_time'].head(3)。若格式是'2023-01-01 10:30:45',用pd.to_datetime(df['order_time']);若是'01/01/2023',必须加format='%m/%d/%Y',否则pandas会猜错(美国/欧洲格式差异)。猜错后果严重:'01/02/2023'可能被当成2月1日而非1月2日。
转换后,立刻用dt属性链探查:
df['order_time'].dt.date提取日期(去时分秒)df['order_time'].dt.hour提取小时(分析下单高峰)df['order_time'].dt.dayofweek提取星期几(周一=0,周日=6)
关键技巧:用dt.floor('D')替代dt.date。dt.date返回Pythondate对象,无法参与pandas计算;dt.floor('D')返回datetime64[ns],可直接groupby或diff()。
3.2 清障阶段:数据清洗的“外科手术式”操作
3.2.1 列名标准化:str.replace(),str.strip(),rename()
原始数据列名常是"User ID ","Order_Amount(USD)","# of Items"。三步清洗:
- 去空格:
df.columns = df.columns.str.strip() - 去特殊字符:
df.columns = df.columns.str.replace(r'[^a-zA-Z0-9_]', '_', regex=True) - 小写下划线:
df.columns = df.columns.str.lower().str.replace(' ', '_')
但注意:str.replace()对数字列名无效。所以必须先转字符串:df.columns = df.columns.astype(str).str.replace(...)。我吃过亏——某次df.columns = [col.replace(' ', '_') for col in df.columns],结果0列名变成'0_',后续df[0]报错。
重命名用rename(),永远指定inplace=False:
df = df.rename(columns={ 'user_id_': 'user_id', 'order_amount_usd': 'order_amount' })实操心得:列名清洗后,立刻
assert df.columns.is_unique。若报错,说明有重复列名(如"ID"和"id"都转成"id"),必须手动处理。
3.2.2 缺失值填充:fillna(),interpolate(), 条件填充
填充不是选“最常用方法”,而是选“最符合业务逻辑的方法”:
数值列:
df['age'].fillna(df['age'].median(), inplace=False)—— 中位数抗异常值df['temperature'].interpolate(method='linear')—— 时间序列用线性插值,比均值更合理
分类列:
df['gender'].fillna('Unknown')—— 不能填'Other',因为'Other'是有效类别,'Unknown'才是缺失标识df['city'].fillna(df['city'].mode()[0])—— 众数填充,但必须[0]取第一个,因为mode()返回Series
最狠的招是条件填充。比如df['salary']缺失,但df['job_level']完整,则:
df['salary'] = df.groupby('job_level')['salary'].transform(lambda x: x.fillna(x.median()))这比全局填中位数精准10倍。transform()保证返回同长度Series,不会索引错位。
3.2.3 异常值处理:clip(),quantile(),np.where()
别一上来就df = df[df['revenue'] < 100000]删数据!先用clip()盖帽:
df['revenue'] = df['revenue'].clip(lower=df['revenue'].quantile(0.01), upper=df['revenue'].quantile(0.99))clip()保留原索引,不影响后续merge;quantile(0.01)取1%分位数,比mean±3*std更鲁棒(后者对偏态分布失效)。
若需标记异常值(非删除),用np.where():
df['revenue_outlier'] = np.where( (df['revenue'] < df['revenue'].quantile(0.01)) | (df['revenue'] > df['revenue'].quantile(0.99)), 1, 0 )注意:
clip()后describe()的max会变,但原始max还在df['revenue'].max()里。所以速查表里所有clip()操作都配print(f"Clipped {len(df[df['revenue'] > original_max])} outliers"),留痕。
3.2.4 字符串清洗:str.strip(),str.lower(),str.replace(), 正则提取
df['name'].str.strip().str.lower()是基础,但关键在正则提取。比如df['phone']有"+86-138-1234-5678","13812345678","8613812345678",统一成11位数字:
df['phone_clean'] = df['phone'].str.replace(r'\D', '', regex=True) # \D匹配非数字 df['phone_clean'] = df['phone_clean'].str[-11:] # 取后11位更狠的是从地址中提取省份:df['address'].str.extract(r'(北京|上海|广东|浙江)')。extract()返回DataFrame,所以加[0]取第一列:df['province'] = df['address'].str.extract(r'(北京|上海|广东|浙江)')[0]。
3.2.5 重复值处理:duplicated(),drop_duplicates(), 复合键去重
df.duplicated().sum()看总重复数,但必须指定subset:
dup_mask = df.duplicated(subset=['user_id', 'order_date'], keep=False) print(f"Duplicate orders: {dup_mask.sum()}") # 显示所有重复行keep=False标记所有重复行(包括第一次出现的),方便人工核验。确认无误后:
df = df.drop_duplicates(subset=['user_id', 'order_date'], keep='first')keep='first'保留首次出现的记录,这是业务黄金准则——首次下单时间最可信。
3.3 探因阶段:从数据中挖出业务洞见的核心武器
3.3.1 高效筛选:query(),loc[],isin()
df[df['revenue'] > 1000]可读,但慢且难扩展。query()是王者:
df.query('revenue > 1000 and city in @top_cities and order_date >= @start_date')@符号引用外部变量,安全且高效。query()底层用numexpr,比布尔索引快30%。但注意:query()不能用于列名含空格或特殊字符的DataFrame,此时必须先重命名。
loc[]用于行列同时筛选:
df.loc[df['revenue'] > 1000, ['user_id', 'city', 'revenue']] # 只取三列isin()查集合成员:
valid_users = ['U001', 'U002', 'U003'] df = df.loc[df['user_id'].isin(valid_users)]实操心得:
query()里用and/or,loc[]里用&/|(必须括号),别混用。df.query('revenue > 1000 or city == "Beijing"')正确;df.loc[df['revenue'] > 1000 | df['city'] == 'Beijing']错误,必须df.loc[(df['revenue'] > 1000) | (df['city'] == 'Beijing')]。
3.3.2 分组聚合:groupby().agg(),named_agg,apply()
agg()字典语法是核心:
result = df.groupby('product_type').agg({ 'revenue': 'sum', 'order_id': 'count', 'profit_margin': ['mean', 'std'] })返回MultiIndex列,result.columns是('revenue', 'sum'), ('order_id', 'count'), ...。展平用:
result.columns = ['_'.join(col).strip() for col in result.columns.values]named_agg更清晰(pandas 0.25+):
result = df.groupby('product_type').agg( total_revenue=('revenue', 'sum'), order_count=('order_id', 'count'), avg_margin=('profit_margin', 'mean') )apply()用于复杂逻辑,但慎用——它逐行执行,慢。比如计算复购率:
def repeat_rate(group): return group['user_id'].nunique() / len(group) if len(group) > 0 else 0 result = df.groupby('month').apply(repeat_rate)3.3.3 透视分析:pivot_table(),crosstab(),melt()
pivot_table()是销售分析神器:
pt = df.pivot_table( values='revenue', index='product_type', columns='region', aggfunc='sum', fill_value=0, margins=True # 自动加总计行/列 )margins=True生成All行,pt.loc['All']就是各区域总营收。fill_value=0防NaN,否则plot()报错。
crosstab()做交叉表:
pd.crosstab(df['gender'], df['purchase_flag'], normalize='index')normalize='index'计算每个性别的购买率,比pd.crosstab(df['gender'], df['purchase_flag'])更有业务意义。
melt()是宽表转长表:
df_melted = df.melt( id_vars=['user_id', 'signup_date'], value_vars=['day1_revenue', 'day7_revenue', 'day30_revenue'], var_name='day', value_name='revenue' )var_name和value_name必须指定,否则列名是variable和value,不直观。
3.3.4 时间窗口分析:rolling(),expanding(),resample()
rolling(7).mean()计算7日滚动均值,但注意:rolling()默认min_periods=1,首6日有值(用可用数据算),若要严格7日,设min_periods=7。
resample()用于时间序列重采样:
df.set_index('order_time').resample('D').agg({ 'revenue': 'sum', 'order_id': 'count' }).reset_index()'D'是日频,'M'是月末,'MS'是月初。resample()必须先set_index(),否则报错。
3.3.5 合并与连接:merge(),concat(),join()
merge()是主力,关键参数:
how='left':保留左表所有行,右表无匹配则NaNon=['user_id']:同名列连接left_on='uid', right_on='user_id':不同名列连接
concat()垂直堆叠:
df_all = pd.concat([df_q1, df_q2, df_q3], ignore_index=True)ignore_index=True重置索引,否则索引重复。
注意:
merge()前务必assert df_left['user_id'].is_unique and df_right['user_id'].is_unique,否则一对多连接产生笛卡尔积,行数爆炸。
3.4 成型阶段:让分析结果可交付、可复现、可追溯
3.4.1 结果导出:to_csv(),to_excel(),to_parquet()
to_csv()加index=False,否则多一列Unnamed: 0:
result.to_csv('analysis_output.csv', index=False, encoding='utf-8-sig')encoding='utf-8-sig'解决Windows Excel中文乱码。
to_excel()必须用openpyxl引擎:
result.to_excel('analysis_output.xlsx', index=False, engine='openpyxl')engine='xlsxwriter'不支持datetime列样式,openpyxl支持。
to_parquet()是大数据归档首选:
result.to_parquet('analysis_output.parquet', index=False, compression='snappy')snappy压缩快,gzip压缩率高但慢。Parquet文件可被Spark、Trino直接读取,实现分析即生产。
3.4.2 可视化集成:plot(),seaborn,plotly
pandas内置plot()够用:
result.plot(x='product_type', y='total_revenue', kind='bar', figsize=(10,6)) plt.title('Revenue by Product Type') plt.show()但seaborn更专业:
sns.barplot(data=result, x='product_type', y='total_revenue')plotly交互强:
fig = px.bar(result, x='product_type', y='total_revenue') fig.show()3.4.3 可复现性保障:pd.set_option(),random_state, 版本锁定
分析脚本开头必加:
import pandas as pd pd.set_option('display.max_columns', None) # 显示所有列 pd.set_option('display.width', None) # 不折行 pd.set_option('display.float_format', '{:.2f}'.format) # 浮点数两位小数随机操作(如sample())必须设random_state=42:
df_sample = df.sample(n=1000, random_state=42)42是程序员彩蛋,但关键是固定值,保证每次运行结果一致。
版本锁定:requirements.txt里写pandas==1.5.3,而非pandas>=1.5.0。pandas 2.0移除了ix,升级后脚本全崩。
4. 实操过程全记录:一个真实电商订单分析的完整链路
4.1 场景设定:分析Q3订单数据,找出高价值用户特征
数据源:orders_q3.csv(200万行,15列,含user_id,order_time,revenue,product_category,city等)。老板需求:“哪些城市的用户复购率最高?高价值用户(LTV>5000)集中在哪些品类?”
4.2 步骤分解与代码实录
4.2.1 第一步:摸底诊断(耗时12秒)
import pandas as pd import numpy as np df = pd.read_csv('orders_q3.csv') # 1. 结构与内存 print(f"Shape: {df.shape}") print(f"Memory: {df.memory_usage(deep=True).sum() / 1024**2:.2f} MB") df.info(memory_usage='deep') # 2. 缺失率 missing_rate = df.isna().mean().sort_values(ascending=False) print("Missing rate > 0:") print(missing_rate[missing_rate > 0]) # 3. 分类列基数 cat_cols = df.select_dtypes(include=['object']).columns.tolist() for col in cat_cols: print(f"{col}: {df[col].nunique()} unique values") # 输出关键发现: # Shape: (1987654, 15) # Memory: 217.34 MB ← 需压缩 # Missing rate > 0: # discount_code 0.921 ← 92%缺失,直接drop # user_age 0.153 ← 15%缺失,需填充 # city: 1242 unique values ← 高基数,需检查拼写4.2.2 第二步:清洗与标准化(耗时8秒)
# 删除高缺失列 df = df.drop(columns=['discount_code']) # 列名清洗 df.columns = df.columns.str.strip().str.replace(r'[^a-zA-Z0-9_]', '_', regex=True).str.lower() # 填充user_age:按city分组填中位数 df['user_age'] = df.groupby('city')['user_age'].transform(lambda x: x.fillna(x.median())) # city列清洗:去空格、统一大小写、修正拼写 df['city'] = df['city'].str.strip().str.title() # 手动映射常见错误 city_map = {'Beijing ': 'Beijing', 'Shanghi': 'Shanghai', 'Guangzhou ': 'Guangzhou'} df['city'] = df['city'].replace(city_map) # 转换时间 df['order_time'] = pd.to_datetime(df['order_time']) df['order_date'] = df['order_time'].dt.floor('D') # 提取日期 # 内存优化:category类型 df['product_category'] = df['product_category'].astype('category') df['city'] = df['city'].astype('category') print(f"Memory after optimization: {df.memory_usage(deep=True).sum() / 1024**2:.2f} MB") # 降为89.21 MB4.2.3 第三步:探因分析(耗时27秒)
# 计算用户LTV:每个user_id的总revenue user_ltv = df.groupby('user_id')['revenue'].sum().reset_index(name='ltv') # 标记高价值用户 high_value_users = user_ltv[user_ltv['ltv'] > 5000]['user_id'].tolist() # 关联回原表,分析高价值用户特征 df_hv = df[df['user_id'].isin(high_value_users)].copy() # 问题1:各城市复购率(下单次数>1的用户占比) user_order_count = df.groupby('user_id')['order_id'].count().reset_index(name='order_count') city_user_count = df.merge(user_order_count, on='user_id').groupby('city')['user_id'].nunique().reset_index(name='total_users') city_repeat_users = df.merge(user_order_count, on='user_id').query('order_count > 1').groupby('city')['user_id'].nunique().reset_index(name='repeat_users') city_repeat_rate = city_user_count.merge(city_repeat_users, on='city', how='left').fillna(0) city_repeat_rate['repeat_rate'] = city_repeat_rate['repeat_users'] / city_repeat_rate['total_users'] # 问题2:高价值用户品类分布 hv_category_dist = df_hv.groupby('product_category')['revenue'].sum().sort_values(ascending=False) print("High-value user revenue by category:") print(hv_category_dist.head(10)) # 输出: # High-value user revenue by category: # electronics 12456789.23 # home_appliances 8765432.10 # fashion 5432109.874.2.4 第四步:成型输出(耗时3秒)
# 生成最终报告表 report = city_repeat_rate[['city', 'repeat_rate']].merge( hv_category_dist.reset_index(name='hv_revenue'), left_on='city', right_on='product_category', how='left' ).drop(columns=['product_category']) # 导出 report.to_csv('q3_high_value_analysis.csv', index=False, encoding='utf-8-sig') print("Report saved successfully!") # 可视化 import matplotlib.pyplot as plt plt.figure(figsize=(12,6)) plt.subplot(1,2,1) report.nlargest(10, 'repeat_rate').plot(x='city', y='repeat_rate', kind='bar', title='Top 10 Cities by Repeat Rate') plt.subplot(1,2,2) hv_category_dist.head(10).plot(kind='bar', title='Revenue by Category (HV Users)') plt.tight_layout() plt.show()4.3 关键耗时与性能对比
| 步骤 | 操作 | 耗时 | 优化点 |
|---|---|---|---|
| 摸底 | df.info(memory_usage='deep') | 0.8s | 替代df.head()+df.shape+df.dtypes三步 |
| 清洗 | groupby().transform()填充 | 3.2s | 比循环快15倍,比全局中位数填充精准 |
| 探因 | merge()+query()组合 | 18.5s | query()比布尔索引快30%,merge()比map()稳定 |
| 成型 | to_csv()+encoding='utf-8-sig' | 0.5s | 避免Excel打开乱码,节省业务方10分钟沟通 |
5. 常见问题与排查技巧实录:那些文档里不写的坑
5.1 “SettingWithCopyWarning”到底该怎么治?
这个警告不是错误,是pandas的善意提醒:“你可能在修改一个副本,不是原DataFrame”。根源是链式索引:df[df['revenue']>1000]['city'] = 'VIP'。正确解法只有两个:
- 用
.loc[]明确指定行列:mask = df['revenue'] > 100
