Pandas DataFrame.drop() 实战:5种常见数据清洗场景与避坑指南
Pandas DataFrame.drop() 实战:5种常见数据清洗场景与避坑指南
如果你经常和电商销售数据、金融交易记录或者用户行为日志打交道,肯定遇到过这样的时刻:面对一个包含几十上百列的庞大表格,真正有用的信息可能就那么几项。那些冗余的ID列、调试用的临时字段、或者格式混乱的备注信息,不仅让数据文件变得臃肿,更会在后续的分析和建模过程中带来各种意想不到的麻烦。这时候,一个看似简单的drop()操作,就成了数据清洗流程中不可或缺的“手术刀”。
但别小看这把手术刀,用好了能精准切除“病灶”,用不好可能“误伤”关键数据,甚至引发连锁错误。我见过不少数据分析师,在处理多级索引的复杂表格时,因为axis参数没设对,把本该删除的列误删了行;也遇到过有人反复调用drop()却发现原数据纹丝不动,最后才发现是忘了设置inplace=True。这些看似低级的错误,在实际工作中却屡见不鲜,往往是因为对drop()方法的理解还停留在基础语法层面,缺乏针对真实场景的深度掌握。
这篇文章,我就结合自己在电商风控和金融数据分析中的实际经验,带你深入DataFrame.drop()的实战应用。我们不会重复那些官方文档里就能查到的参数说明,而是聚焦于五个最典型的数据清洗场景,从电商订单表清理到金融时间序列处理,每个场景都配有可直接复用的代码示例。更重要的是,我会重点剖析那些容易踩坑的细节——比如多级索引下的层级选择、批量删除时的性能考量、以及inplace参数背后的内存管理逻辑。无论你是刚开始接触Pandas的新手,还是想进一步提升数据清洗效率的资深分析师,相信都能在这里找到实用的技巧和新的启发。
1. 场景一:电商数据清洗——批量删除无效列与自动化列筛选
处理电商平台的订单导出数据时,你拿到的CSV文件往往包含大量对分析无用的字段。比如,系统自动生成的内部ID、物流跟踪码的原始字符串、支付网关返回的冗长响应信息等。这些列不仅占用内存,还会在后续进行数据透视或聚合计算时干扰视线。手动一列列删除显然不现实,这时候就需要批量操作的技巧。
假设我们有一份来自某电商平台的订单明细表,包含以下列:order_id, user_id, product_sku, product_name, quantity, unit_price, total_amount, payment_gateway_response_raw, logistics_tracking_code_encrypted, internal_system_flag, created_at, updated_at。其中,payment_gateway_response_raw和logistics_tracking_code_encrypted是长达数百字符的JSON字符串或加密文本,internal_system_flag是内部系统使用的标记位,这些都不需要出现在我们的分析模型中。
最直观的做法是使用columns参数直接指定要删除的列名列表:
import pandas as pd
# 模拟电商订单数据
df_orders = pd.DataFrame({
'order_id': [1001, 1002, 1003],
'user_id': [201, 202, 203],
'product_sku': ['SKU123', 'SKU456', 'SKU789'],
'product_name': ['手机', '耳机', '充电宝'],
'quantity': [1, 2, 1],
'unit_price': [2999.0, 399.0, 199.0],
'total_amount': [2999.0, 798.0, 199.0],
'payment_gateway_response_raw': ['{"code":"SUCCESS","msg":"...很长..."}', '{"code":"SUCCESS","msg":"..."}', '{"code":"PENDING","msg":"..."}'],
'logistics_tracking_code_encrypted': ['a1b2c3d4e5f6', 'g7h8i9j0k1l2', 'm3n4o5p6q7r8'],
'internal_system_flag': [0, 1, 0],
'created_at': pd.to_datetime(['2023-10-01 10:00', '2023-10-01 11:30', '2023-10-01 14:15']),
'updated_at': pd.to_datetime(['2023-10-01 10:05', '2023-10-01 11:35', '2023-10-01 14:20'])
})
# 批量删除不需要的列
columns_to_drop = [
'payment_gateway_response_raw',
'logistics_tracking_code_encrypted',
'internal_system_flag'
]
df_cleaned = df_orders.drop(columns=columns_to_drop)
print(f"原始列数: {df_orders.shape[1]}, 清理后列数: {df_cleaned.shape[1]}")
print(df_cleaned.columns.tolist())
但实际工作中,列名可能不会这么规整。比如,有些临时列的名字带有特定前缀或后缀,像tmp_、_bak、debug_等。这时候,我们可以结合列表推导式和字符串方法进行模式匹配删除:
# 假设数据中混入了多个调试和临时列
df_orders['tmp_calculation_1'] = [1, 2, 3]
df_orders['debug_price_check'] = [True, False, True]
df_orders['backup_quantity'] = df_orders['quantity']
# 自动识别并删除以特定前缀/后缀开头的列
pattern_prefixes = ('tmp_', 'debug_', 'backup_')
columns_to_drop_by_pattern = [col for col in df_orders.columns
if col.startswith(pattern_prefixes)]
df_cleaned_auto = df_orders.drop(columns=columns_to_drop_by_pattern)
print(f"通过模式匹配删除的列: {columns_to_drop_by_pattern}")
注意:使用
inplace=True参数可以直接修改原DataFrame,节省内存。但这也意味着原始数据会被覆盖,无法撤销操作。对于重要的原始数据,我通常先保留副本,或者在不重要的数据清洗步骤中才使用inplace=True。
# 使用inplace=True直接修改原数据框
df_orders.drop(columns=columns_to_drop_by_pattern, inplace=True)
# 此时df_orders已经被修改,columns_to_drop_by_pattern中的列已不存在
除了按名称删除,有时我们还需要根据列的数据类型进行筛选删除。比如,数据分析中可能只需要数值型和类别型数据,想要删除所有object类型的文本列(除非是必要的ID或名称字段)。这时可以结合select_dtypes方法:
# 删除所有object类型的列(文本列),但保留必要的ID列
text_columns = df_cleaned.select_dtypes(include=['object']).columns
# 假设我们只想保留product_sku和product_name这两个object列
text_columns_to_drop = [col for col in text_columns
if col not in ['product_sku', 'product_name']]
df_numeric_focused = df_cleaned.drop(columns=text_columns_to_drop)
电商数据清洗中还有一个常见需求:删除完全为空(全部是NaN)的列。这些列可能是数据导出时的格式错误产生的。虽然dropna()方法专门处理缺失值,但用drop()也能实现,只需要一点小技巧:
# 添加一个全为NaN的测试列
df_cleaned['empty_column'] = pd.NA
# 找出全部为NA的列
empty_cols = df_cleaned.columns[df_cleaned.isna().all()].tolist()
if empty_cols:
df_cleaned.drop(columns=empty_cols, inplace=True)
print(f"已删除空列: {empty_cols}")
2. 场景二:金融时间序列处理——安全删除行与索引操作陷阱
金融数据分析中,我们经常处理时间序列数据,比如股票价格、交易量、财务指标等。这类数据对完整性和连续性要求很高,但原始数据中难免会有异常记录、测试数据或重复条目。删除行操作在这里尤为关键,而且必须谨慎,因为错误删除一行可能影响后续的收益率计算、波动率分析等。
假设我们有一份股票日频交易数据,包含日期、开盘价、最高价、最低价、收盘价和成交量。数据中可能混入了一些异常值或非交易日的数据:
# 创建金融时间序列数据示例
dates = pd.date_range('2023-01-01', '2023-01-10', freq='D')
stock_data = pd.DataFrame({
'open': [100.0, 102.5, 101.8, 104.2, 103.9, 105.0, 106.2, 104.8, 107.0, 108.5],
'high': [102.0, 104.0, 103.5, 105.8, 105.2, 106.5, 107.8, 106.0, 108.5, 110.0],
'low': [99.5, 101.8, 100.5, 103.5, 102.8, 104.2, 105.5, 103.9, 106.2, 107.8],
'close': [101.5, 103.8, 102.9, 105.0, 104.5, 105.8, 107.0, 105.5, 108.0, 109.2],
'volume': [1000000, 1200000, 950000, 1500000, 1100000, 1300000, 1400000, 1250000, 1600000, 1800000]
}, index=dates)
stock_data.index.name = 'trade_date'
print(stock_data.head())
第一个常见需求是删除特定日期的数据。比如,我们知道2023-01-04的数据存在采集错误,需要移除:
# 删除单个日期的行
date_to_remove = pd.Timestamp('2023-01-04')
if date_to_remove in stock_data.index:
stock_data_corrected = stock_data.drop(index=date_to_remove)
print(f"删除 {date_to_remove} 后的数据形状: {stock_data_corrected.shape}")
第二个需求是删除连续日期范围的数据。例如,删除元旦假期后非交易日的数据(假设1月2日、3日是非交易日):
# 删除多个日期的行
dates_to_remove = [pd.Timestamp('2023-01-02'), pd.Timestamp('2023-01-03')]
stock_data_corrected = stock_data.drop(index=dates_to_remove, errors='ignore')
print(f"删除多个日期后的数据形状: {stock_data_corrected.shape}")
这里使用了errors='ignore'参数,这是一个很实用的安全选项。当尝试删除的标签在索引中不存在时,默认会抛出KeyError。设置errors='ignore'后,Pandas会忽略不存在的标签,只删除存在的那些。这在处理不确定是否存在的日期时特别有用。
金融数据中经常遇到的另一个问题是重复的索引。可能是数据源推送重复,或者合并多个数据源时产生的重复:
# 人为添加重复行
stock_data_with_duplicates = stock_data.copy()
stock_data_with_duplicates.loc[pd.Timestamp('2023-01-05')] = [105.5, 107.0, 104.8, 106.2, 1350000]
print(f"添加重复行后的索引重复情况: {stock_data_with_duplicates.index.duplicated().sum()} 个重复")
# 删除重复索引的行(保留第一个出现)
stock_data_deduped = stock_data_with_duplicates[~stock_data_with_duplicates.index.duplicated(keep='first')]
# 或者使用drop方法,但需要先识别出重复的索引
duplicate_indices = stock_data_with_duplicates.index[stock_data_with_duplicates.index.duplicated()]
if len(duplicate_indices) > 0:
stock_data_deduped = stock_data_with_duplicates.drop(index=duplicate_indices)
重要提示:直接使用
drop()删除行时,如果DataFrame的索引不是唯一的,可能会遇到意想不到的行为。比如,有多个行共享同一个索引值,那么drop(index=some_label)会删除所有具有该索引的行,而不仅仅是一行。在处理金融时间序列时,确保索引唯一性是首要任务。
对于按条件删除行(比如删除波动率异常高的交易日),更常见的做法是使用布尔索引而不是drop():
# 计算每日价格波动范围(最高-最低)
stock_data['daily_range'] = stock_data['high'] - stock_data['low']
# 找出波动异常大的交易日(假设超过平均波动范围的2倍)
avg_range = stock_data['daily_range'].mean()
abnormal_days = stock_data[stock_data['daily_range'] > 2 * avg_range].index
# 删除这些异常日
stock_data_clean = stock_data.drop(index=abnormal_drames)
但这里有个性能上的考虑:对于大型DataFrame,先创建布尔掩码再使用drop()可能不如直接使用布尔索引高效。下面两种方式是等价的,但第二种通常更快:
# 方式1:使用drop()
mask = stock_data['volume'] < 1000000
low_volume_dates = stock_data[mask].index
result1 = stock_data.drop(index=low_volume_dates)
# 方式2:直接使用布尔索引(更高效)
result2 = stock_data[~mask]
# 验证结果相同
print(f"两种方式结果相同: {result1.equals(result2)}")
在处理金融面板数据(多个标的的时间序列)时,我们可能会遇到多级索引。这时候删除行就需要指定level参数,这是最容易出错的地方之一:
# 创建多级索引示例(股票代码 + 日期)
symbols = ['AAPL', 'GOOGL']
multi_index = pd.MultiIndex.from_product(
[symbols, pd.date_range('2023-01-01', '2023-01-05')],
names=['symbol', 'date']
)
panel_data = pd.DataFrame({
'open': np.random.uniform(100, 200, 10),
'close': np.random.uniform(105, 205, 10),
'volume': np.random.randint(1000000, 5000000, 10)
}, index=multi_index)
print("多级索引数据前几行:")
print(panel_data.head())
# 删除特定股票的所有行(第一级索引)
panel_without_aapl = panel_data.drop(index='AAPL', level=0)
print(f"删除AAPL后形状: {panel_without_aapl.shape}")
# 删除特定日期的所有行(第二级索引)
panel_without_date = panel_data.drop(index=pd.Timestamp('2023-01-03'), level=1)
print(f"删除2023-01-03后形状: {panel_without_date.shape}")
注意,在多级索引中,level参数是从0开始计数的整数,或者是指定层级名称的字符串。如果不指定level,Pandas会尝试在所有层级中匹配给定的标签,这可能不是你想要的行为。
3. 场景三:多级索引与复杂数据结构——精准删除与层级控制
多级索引(MultiIndex)是Pandas中处理高维数据的强大工具,在电商分析(商品类别+子类别)、金融分析(股票+日期)、运营数据(部门+团队+个人)等场景中非常常见。但多级索引下的数据删除操作也更为复杂,需要精确控制删除的层级和标签。
让我们深入一个电商库存管理的例子。假设我们有一个包含商品类别、子类别、商品ID三级索引的库存数据:
# 创建三级索引的电商库存数据
categories = ['电子产品', '服装']
subcategories = ['手机', '电脑', '上衣', '裤子']
product_ids = ['P001', 'P002', 'P003']
# 创建多级索引
index_tuples = []
for cat in categories:
for subcat in subcategories:
if (cat == '电子产品' and subcat in ['手机', '电脑']) or \
(cat == '服装' and subcat in ['上衣', '裤子']):
for pid in product_ids:
index_tuples.append((cat, subcat, pid))
multi_index = pd.MultiIndex.from_tuples(index_tuples, names=['category', 'subcategory', 'product_id'])
inventory_data = pd.DataFrame({
'stock_quantity': np.random.randint(0, 100, len(multi_index)),
'unit_cost': np.random.uniform(50, 500, len(multi_index)),
'last_restock_date': pd.date_range('2023-09-01', periods=len(multi_index), freq='D')
}, index=multi_index)
print("多级索引库存数据:")
print(inventory_data.head(10))
print(f"\n索引层级: {inventory_data.index.names}")
print(f"索引级别数: {inventory_data.index.nlevels}")
场景3.1:删除整个类别的所有数据
假设我们决定不再跟踪“服装”类别的库存,需要删除所有相关记录:
# 删除'服装'类别的所有行(第一级索引)
inventory_no_clothing = inventory_data.drop(index='服装', level='category')
print(f"删除服装类别后数据形状: {inventory_no_clothing.shape}")
print(f"剩余类别: {inventory_no_clothing.index.get_level_values('category').unique()}")
场景3.2:删除特定子类别的数据,但不影响其他层级
现在假设“手机”这个子类别下的商品全部下架,需要删除所有手机相关的库存记录:
# 只删除'手机'子类别,无论它属于哪个主类别
inventory_no_phones = inventory_data.drop(index='手机', level='subcategory')
print(f"删除手机子类别后数据形状: {inventory_no_phones.shape}")
这里有一个关键点:level参数指定了在哪个层级上查找要删除的标签。如果不指定level,Pandas会在所有层级中搜索匹配的标签,这可能导致误删:
# 如果不指定level,可能会误删
# 假设有个商品ID正好叫'手机'(虽然不太可能)
# inventory_data.drop(index='手机') # 这会在所有层级搜索'手机',可能不是我们想要的
场景3.3:删除特定组合的数据
更精细的操作是删除特定索引组合的数据。比如,只删除“电子产品”类别下“手机”子类别中商品ID为“P001”的特定商品:
# 删除特定组合 - 方法1:使用元组指定完整路径
inventory_specific_drop = inventory_data.drop(index=('电子产品', '手机', 'P001'))
print(f"删除特定组合后数据形状: {inventory_specific_drop.shape}")
# 方法2:使用布尔索引(更灵活,可以基于条件)
mask = (inventory_data.index.get_level_values('category') == '电子产品') & \
(inventory_data.index.get_level_values('subcategory') == '手机') & \
(inventory_data.index.get_level_values('product_id') == 'P001')
inventory_alternative = inventory_data[~mask]
场景3.4:使用droplevel()简化索引结构
有时我们不需要完整的多级索引,想要降低维度。比如,在完成按类别的分析后,我们可能想移除类别层级,只保留子类别和产品ID。这时候droplevel()比drop()更合适:
# 移除第一级索引(category),但保留数据
inventory_reduced_index = inventory_data.droplevel('category')
print("移除category层级后的索引:")
print(inventory_reduced_index.index[:5])
注意droplevel()和drop()的区别:droplevel()移除索引层级但保留该层级的所有数据,只是索引结构变简单了;而drop()是移除符合条件的数据行。
场景3.5:处理多级列索引
多级索引不仅适用于行,也适用于列。这在处理宽格式数据时很常见,比如每个月的销售额作为列,又有实际值、预算值、增长率等子列:
# 创建多级列索引的销售数据
months = ['Jan', 'Feb', 'Mar']
metrics = ['actual', 'budget', 'growth']
col_tuples = [(month, metric) for month in months for metric in metrics]
multi_cols = pd.MultiIndex.from_tuples(col_tuples, names=['month', 'metric'])
sales_data = pd.DataFrame(
np.random.randn(5, 9) * 1000 + 5000,
index=['Region_' + str(i) for i in range(1, 6)],
columns=multi_cols
)
print("多级列索引销售数据:")
print(sales_data.head())
# 删除特定月份的所有列(第一级列索引)
sales_no_jan = sales_data.drop(columns='Jan', level=0)
print(f"\n删除Jan月份后列索引: {sales_no_jan.columns.tolist()[:6]}...")
# 删除特定指标的所有列(第二级列索引)
sales_no_growth = sales_data.drop(columns='growth', level=1)
print(f"删除growth指标后列数: {sales_no_growth.shape[1]}")
多级索引删除操作的核心是明确指定level参数。下面这个表格总结了不同场景下的参数设置:
| 删除目标 | 索引类型 | 关键参数 | 示例代码 |
|---|---|---|---|
| 特定类别所有数据 | 多级行索引 | level=0 或 level='category' | df.drop(index='服装', level='category') |
| 特定子类别数据 | 多级行索引 | level=1 或 level='subcategory' | df.drop(index='手机', level='subcategory') |
| 特定组合数据 | 多级行索引 | 不指定level,使用元组 | df.drop(index=('电子产品','手机','P001')) |
| 特定月份列 | 多级列索引 | level=0, axis=1 | df.drop(columns='Jan', level=0, axis=1) |
| 特定指标列 | 多级列索引 | level=1, axis=1 | df.drop(columns='growth', level=1, axis=1) |
4. 场景四:性能优化与大规模数据处理——高效删除策略
当处理GB级别的大型数据集时,drop()操作的性能就变得至关重要。不恰当的删除策略可能导致内存溢出、执行时间过长等问题。我曾在处理一个包含3000万行、200列的电商用户行为数据集时,因为一个简单的删除操作就让整个Jupyter内核崩溃。经过多次优化尝试,总结出以下几个关键策略。
策略4.1:避免链式操作,特别是涉及inplace=False时
Pandas的许多方法默认返回新的DataFrame副本,而不是修改原对象。这在drop()中表现为inplace=False(默认值)。链式调用多个drop()会导致创建多个中间副本,消耗大量内存:
# 不推荐的链式操作(创建多个中间副本)
df_large = pd.DataFrame(np.random.randn(1000000, 50)) # 100万行×50列
df_large.columns = [f'col_{i}' for i in range(50)]
# 这种方式会创建多个中间DataFrame
df_cleaned = df_large.drop(columns=['col_0', 'col_1'])\
.drop(columns=['col_10', 'col_11'])\
.drop(columns=['col_20', 'col_21'])
更好的做法是一次性指定所有要删除的列:
# 推荐:一次性删除所有不需要的列
cols_to_drop = ['col_0', 'col_1', 'col_10', 'col_11', 'col_20', 'col_21']
df_cleaned = df_large.drop(columns=cols_to_drop)
如果因为业务逻辑必须分步删除,考虑使用inplace=True(但要注意这会修改原数据):
# 如果必须分步,使用inplace=True避免创建副本
df_large_copy = df_large.copy()
df_large_copy.drop(columns=['col_0', 'col_1'], inplace=True)
df_large_copy.drop(columns=['col_10', 'col_11'], inplace=True)
策略4.2:使用列索引而不是列名进行批量删除
当需要删除大量列时,按位置索引删除可能比按列名删除更快,特别是当列名很长或包含特殊字符时:
# 按位置索引删除列(假设要删除前5列)
df_by_position = df_large.drop(columns=df_large.columns[:5])
# 或者使用iloc风格的索引
column_indices_to_drop = [0, 1, 10, 11, 20, 21]
df_by_indices = df_large.drop(columns=df_large.columns[column_indices_to_drop])
策略4.3:对于超大型数据集,考虑使用dtype优化
删除操作本身不改变数据类型,但如果删除大量列后数据仍然很大,可以考虑优化剩余列的数据类型来减少内存占用:
# 删除列后优化数据类型
df_optimized = df_cleaned.copy()
# 将float64转换为float32(如果精度允许)
for col in df_optimized.select_dtypes(include=['float64']).columns:
df_optimized[col] = df_optimized[col].astype('float32')
# 将int64转换为int32或int16
for col in df_optimized.select_dtypes(include=['int64']).columns:
max_val = df_optimized[col].max()
min_val = df_optimized[col].min()
if min_val >= 0 and max_val < 65535:
df_optimized[col] = df_optimized[col].astype('uint16')
elif min_val >= -32768 and max_val < 32767:
df_optimized[col] = df_optimized[col].astype('int16')
print(f"优化前内存使用: {df_cleaned.memory_usage(deep=True).sum() / 1024**2:.2f} MB")
print(f"优化后内存使用: {df_optimized.memory_usage(deep=True).sum() / 1024**2:.2f} MB")
策略4.4:使用errors='ignore'避免不必要的异常处理开销
在循环或批量删除操作中,如果某些标签可能不存在,使用errors='ignore'可以避免频繁的异常处理开销:
# 假设我们有一个可能包含不存在列名的列表
potential_columns_to_drop = ['col_0', 'col_1', 'col_100', 'col_101'] # col_100可能不存在
# 不使用errors='ignore',需要异常处理
try:
df_safe = df_large.drop(columns=potential_columns_to_drop)
except KeyError as e:
print(f"列不存在: {e}")
# 需要额外处理
# 使用errors='ignore'更简洁高效
df_safe_ignore = df_large.drop(columns=potential_columns_to_drop, errors='ignore')
策略4.5:分块处理超大文件
对于内存无法一次性加载的超大文件,可以结合分块读取和删除操作:
# 分块处理CSV文件的示例
chunk_size = 100000
cols_to_keep = [col for col in df_large.columns if col not in cols_to_drop]
# 假设从CSV读取
processed_chunks = []
for chunk in pd.read_csv('huge_dataset.csv', chunksize=chunk_size):
# 在每块中删除不需要的列
chunk_reduced = chunk.drop(columns=cols_to_drop, errors='ignore')
processed_chunks.append(chunk_reduced)
# 合并处理后的块
df_final = pd.concat(processed_chunks, ignore_index=True)
为了直观比较不同删除策略的性能差异,我在一个中等规模数据集上做了简单测试:
import time
# 性能比较测试
test_df = pd.DataFrame(np.random.randn(500000, 100)) # 50万行×100列
test_df.columns = [f'col_{i}' for i in range(100)]
# 测试1:链式删除 vs 批量删除
start = time.time()
result1 = test_df.drop(columns=['col_0', 'col_1'])\
.drop(columns=['col_10', 'col_11'])\
.drop(columns=['col_20', 'col_21'])
time1 = time.time() - start
start = time.time()
cols_to_drop = ['col_0', 'col_1', 'col_10', 'col_11', 'col_20', 'col_21']
result2 = test_df.drop(columns=cols_to_drop)
time2 = time.time() - start
print(f"链式删除时间: {time1:.4f}秒")
print(f"批量删除时间: {time2:.4f}秒")
print(f"性能提升: {(time1 - time2)/time1*100:.1f}%")
在我的测试环境中,批量删除通常比链式删除快30-50%,具体取决于数据集大小和删除的列数。
5. 场景五:高级技巧与替代方案——超越基础drop()的解决方案
虽然drop()是删除行和列的主要方法,但在某些特定场景下,其他方法可能更合适、更高效或更易读。了解这些替代方案能让你在数据清洗工具箱中有更多选择。
方案5.1:使用del语句删除单列
对于单个列的删除,Python内置的del语句是最直接的方式,而且内存效率高,因为它直接修改原对象而不创建副本:
df = pd.DataFrame(np.random.randn(100, 5), columns=['A', 'B', 'C', 'D', 'E'])
# 使用del删除单列
del df['A']
print(f"使用del后列: {df.columns.tolist()}")
# 与drop(inplace=True)对比
df_copy = pd.DataFrame(np.random.randn(100, 5), columns=['A', 'B', 'C', 'D', 'E'])
df_copy.drop(columns=['A'], inplace=True)
print(f"使用drop(inplace=True)后列: {df_copy.columns.tolist()}")
del的局限性在于它不能删除多列,也不能删除行。语法上也不支持链式操作或条件删除。
方案5.2:使用pop()删除列并获取其值
pop()方法在删除列的同时返回该列的数据,这在需要临时移除某列进行计算,然后又可能加回的场景中很有用:
df = pd.DataFrame({
'name': ['Alice', 'Bob', 'Charlie'],
'age': [25, 30, 35],
'salary': [50000, 60000, 70000]
})
# 使用pop移除age列并获取该列数据
age_series = df.pop('age')
print(f"删除age列后的DataFrame:")
print(df)
print(f"\n弹出的age列数据:")
print(age_series)
# 如果需要,可以将age列加回(但顺序会变)
df['age'] = age_series
方案5.3:使用布尔索引"删除"行
实际上,布尔索引并不真正删除数据,而是创建一个不包含某些行的新视图。但在效果上等同于删除,而且语法更直观,特别是基于条件的删除:
# 创建示例数据
df = pd.DataFrame({
'product': ['A', 'B', 'C', 'D', 'E'],
'sales': [100, 200, 50, 300, 150],
'returns': [5, 10, 20, 2, 8]
})
# 使用布尔索引"删除"退货率大于10%的行
return_rate = df['returns'] / df['sales']
df_low_return = df[return_rate <= 0.1] # 保留退货率≤10%的行
print(f"删除高退货率产品后的数据:")
print(df_low_return)
# 对比使用drop()的等价操作
high_return_mask = return_rate > 0.1
indices_to_drop = df[high_return_mask].index
df_drop_method = df.drop(index=indices_to_drop)
布尔索引的优势在于可以直接在条件中引用多列,代码更易读。缺点是对于大型数据集,如果条件复杂,可能会创建临时数组占用内存。
方案5.4:使用filter()方法按模式保留列
filter()方法可以按列名模式选择列,结合like、regex或items参数,特别适合保留或删除具有特定命名模式的列:
df = pd.DataFrame({
'sales_2021': [100, 200, 150],
'sales_2022': [120, 210, 160],
'profit_2021': [20, 40, 30],
'profit_2022': [25, 45, 35],
'region': ['North', 'South', 'East']
})
# 使用filter保留包含'sales'的列
df_sales_only = df.filter(like='sales')
print("只包含sales的列:")
print(df_sales_only)
# 使用正则表达式排除特定模式的列
df_no_profit = df.filter(regex='^(?!profit).*$')
print("\n排除profit列后的数据:")
print(df_no_profit)
方案5.5:使用dropna()删除缺失值行/列
虽然dropna()不是drop()的直接替代,但在删除包含缺失值的行或列时,它是更专业的选择:
df_with_nulls = pd.DataFrame({
'A': [1, 2, None, 4],
'B': [5, None, None, 8],
'C': [9, 10, 11, 12]
})
# 删除任何包含NaN的行
df_no_nulls_rows = df_with_nulls.dropna()
print("删除包含NaN的行:")
print(df_no_nulls_rows)
# 删除任何包含NaN的列
df_no_nulls_cols = df_with_nulls.dropna(axis=1)
print("\n删除包含NaN的列:")
print(df_no_nulls_cols)
# 只删除全部为NaN的列
df_no_all_nulls = df_with_nulls.dropna(axis=1, how='all')
方案5.6:使用drop_duplicates()删除重复行
删除重复行是数据清洗的常见需求,drop_duplicates()为此提供了丰富的选项:
df_with_dups = pd.DataFrame({
'id': [1, 2, 2, 3, 3, 3],
'value': ['A', 'B', 'B', 'C', 'C', 'D']
})
# 删除完全重复的行
df_unique = df_with_dups.drop_duplicates()
print("删除完全重复行:")
print(df_unique)
# 基于特定列删除重复(保留第一个)
df_unique_by_id = df_with_dups.drop_duplicates(subset=['id'], keep='first')
print("\n基于id列删除重复,保留第一个:")
print(df_unique_by_id)
# 基于特定列删除重复(保留最后一个)
df_unique_by_id_last = df_with_dups.drop_duplicates(subset=['id'], keep='last')
print("\n基于id列删除重复,保留最后一个:")
print(df_unique_by_id_last)
下面这个表格总结了不同场景下的最佳选择:
| 使用场景 | 推荐方法 | 优点 | 注意事项 |
|---|---|---|---|
| 删除单列 | del df['col'] | 内存高效,语法简洁 | 不能用于多列或行 |
| 删除多列 | df.drop(columns=[...]) | 灵活,支持批量操作 | 默认创建副本 |
| 删除行 | df.drop(index=[...]) 或布尔索引 | 明确指定要删除的行 | 确保索引唯一性 |
| 基于条件删除行 | 布尔索引 df[condition] | 代码直观,易读 | 可能创建临时数组 |
| 删除包含NaN的行/列 | df.dropna() | 专门处理缺失值 | 有多种删除策略 |
| 删除重复行 | df.drop_duplicates() | 丰富的去重选项 | 注意subset和keep参数 |
| 按模式选择列 | df.filter(like=...) | 基于名称模式 | 只能按列名模式 |
在实际工作中,我通常根据具体情况混合使用这些方法。比如,先用filter()按模式删除一批列,再用del删除单个明确不需要的列,最后用布尔索引基于业务逻辑删除异常行。关键是要理解每种方法的适用场景和性能特征,而不是死记硬背语法。
更多推荐
所有评论(0)