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_rawlogistics_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__bakdebug_等。这时候,我们可以结合列表推导式和字符串方法进行模式匹配删除:

# 假设数据中混入了多个调试和临时列
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=0level='category'df.drop(index='服装', level='category')
特定子类别数据多级行索引level=1level='subcategory'df.drop(index='手机', level='subcategory')
特定组合数据多级行索引不指定level,使用元组df.drop(index=('电子产品','手机','P001'))
特定月份列多级列索引level=0, axis=1df.drop(columns='Jan', level=0, axis=1)
特定指标列多级列索引level=1, axis=1df.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()方法可以按列名模式选择列,结合likeregexitems参数,特别适合保留或删除具有特定命名模式的列:

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()丰富的去重选项注意subsetkeep参数
按模式选择列df.filter(like=...)基于名称模式只能按列名模式

在实际工作中,我通常根据具体情况混合使用这些方法。比如,先用filter()按模式删除一批列,再用del删除单个明确不需要的列,最后用布尔索引基于业务逻辑删除异常行。关键是要理解每种方法的适用场景和性能特征,而不是死记硬背语法。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐