Python实战:用Pandas搞定200万行家庭用电数据清洗(附完整代码)

最近几年,身边不少朋友开始对家庭能耗数据感兴趣,有的是为了节能减排,有的则是想通过数据分析来优化电费支出。我手头恰好有一个法国某家庭近四年的用电数据集,每分钟一条记录,总量超过200万行。这听起来有点吓人,但用Python的Pandas库来处理,其实并没有想象中那么复杂。今天,我就带你走一遍完整的实战流程,从数据加载、清洗、特征工程到初步的可视化分析,把那些隐藏在庞大数据里的家庭用电模式给挖出来。无论你是刚接触数据分析的新手,还是想找个具体案例练手的中级开发者,这篇文章都能给你提供一套可以直接上手的操作指南。

1. 数据加载与初步探索:认识你的“电表”

拿到一个陌生的数据集,第一步永远是先看看它长什么样。这个家庭用电数据集来自一个公开的机器学习仓库,是一个以分号为分隔符的文本文件。直接使用pd.read_csv读取时,有几个参数设置对后续处理效率影响很大。

import pandas as pd
import numpy as np

# 加载数据,关键参数设置
file_path = 'household_power_consumption.txt'
df = pd.read_csv(
    file_path,
    sep=';',                    # 指定分隔符为分号
    low_memory=False,           # 关闭分块读取,避免混合类型推断问题
    parse_dates={'datetime': ['Date', 'Time']}, # 合并日期和时间列
    index_col='datetime',       # 将合并后的日期时间设为索引
    na_values=['?', '??'],      # 将'?'等字符直接识别为缺失值
    infer_datetime_format=True  # 加速日期解析
)

这里有几个细节值得注意。low_memory=False参数在数据量较大时非常有用,它让Pandas一次性推断整个文件的列数据类型,避免了因内存分块导致的前后类型不一致问题。parse_dates参数将原始的“Date”和“Time”两列合并为一个datetime类型的索引,这为后续按时间序列进行分析打下了基础。na_values参数则直接在读取阶段就将数据中的问号标记识别为NaN,省去了后续替换的步骤。

数据加载后,先用几个基本方法快速“摸个底”:

print(f"数据集形状:{df.shape}")
print(f"数据预览:")
print(df.head())
print("\n数据类型概览:")
print(df.dtypes)
print("\n基本统计信息:")
print(df.describe())

在我的实践中,这个数据集返回的形状是(2075259, 7),意味着有超过207万行记录和7个测量维度。各列的含义如下:

  • Global_active_power: 家庭总有功功率(千瓦)
  • Global_reactive_power: 家庭总无功功率(千瓦)
  • Voltage: 实时电压(伏特)
  • Global_intensity: 总电流强度(安培)
  • Sub_metering_1: 厨房区域电能(瓦时),涵盖洗碗机、烤箱等
  • Sub_metering_2: 洗衣房区域电能(瓦时),包含洗衣机、烘干机等
  • Sub_metering_3: 电热水器与空调电能(瓦时)

注意:原始数据中的功率单位是千瓦(KW),而分项电能(Sub_metering)的单位是瓦时(Wh)。在进行计算时,务必注意单位换算,1千瓦等于1000瓦。

2. 深度数据清洗:处理缺失值与异常值

面对200多万行数据,缺失值和异常值几乎是必然存在的。这一步处理得好不好,直接决定了后续分析的可靠性。我们先系统性地检查一下数据质量。

2.1 定位与量化数据问题

首先,全面检查缺失值情况:

# 检查缺失值
missing_summary = df.isnull().sum()
missing_percentage = (missing_summary / len(df)) * 100

print("缺失值统计:")
for col in df.columns:
    if missing_summary[col] > 0:
        print(f"  {col}: {missing_summary[col]} 行缺失,占比 {missing_percentage[col]:.2f}%")

在我的数据中,Sub_metering_3这一列(电热水器和空调)大约有2万多个缺失值,占比约1%。这个比例不算太高,但需要谨慎处理。除了显式的NaN,我们还要留意数据中可能存在的“隐形”异常值,比如超出物理常识的数值(负的功率、极高的电流等)。

2.2 制定并实施清洗策略

对于时间序列数据,尤其是高频采集的用电数据,直接删除缺失行可能会破坏时间连续性。我通常采用的策略是,先尝试用前后相邻时刻的值进行填充(前向或后向填充),如果不行,再考虑用插值法。

# 策略1:对于短时间缺失,使用前向填充(用上一个有效值填充)
df_filled = df.copy()
df_filled.ffill(inplace=True, limit=3)  # 最多向前填充3个连续缺失值

# 策略2:检查填充后是否还有缺失,若有则用线性插值
if df_filled.isnull().sum().sum() > 0:
    print("前向填充后仍有缺失,尝试线性插值...")
    # 注意:对于非数值列或索引不连续的情况,插值需谨慎
    # 这里假设索引是规则的时间序列
    df_filled.interpolate(method='time', inplace=True)  # 按时间索引进行插值

print(f"清洗后缺失值总数:{df_filled.isnull().sum().sum()}")

对于异常值,我习惯先基于业务常识设定合理的阈值范围进行筛选。例如,一个普通家庭的瞬时总有功功率一般不会超过10千瓦,电流强度通常在一定范围内。

# 定义各列的合理物理范围(根据常识和数据集描述估算)
reasonable_ranges = {
    'Global_active_power': (0, 10),        # 单位:千瓦
    'Global_reactive_power': (0, 2),       # 单位:千瓦
    'Voltage': (200, 260),                 # 欧洲标准电压范围
    'Global_intensity': (0, 50),           # 单位:安培
    'Sub_metering_1': (0, 100),            # 单位:瓦时
    'Sub_metering_2': (0, 100),
    'Sub_metering_3': (0, 100)
}

# 标记异常值
outlier_mask = pd.Series(False, index=df_filled.index)
for col, (low, high) in reasonable_ranges.items():
    col_outliers = (df_filled[col] < low) | (df_filled[col] > high)
    outlier_count = col_outliers.sum()
    if outlier_count > 0:
        print(f"列 '{col}' 发现 {outlier_count} 个超出范围 ({low}, {high}) 的值。")
        outlier_mask = outlier_mask | col_outliers

print(f"总共标记了 {outlier_mask.sum()} 行数据包含潜在异常值。")

发现异常值后,是删除、替换还是保留,需要根据具体情况判断。如果是明显的传感器错误(如负功率),可以考虑用NaN替换后再填充。如果是真实但罕见的高耗电事件(比如家庭聚会同时开很多电器),则可能值得保留并单独分析。

3. 特征工程:从原始数据中提取洞察

原始数据只告诉我们每分钟的瞬时功率和分项电能,但很多有价值的模式隐藏在更高维度的特征里。特征工程就是把这些隐藏信息“制造”出来的过程。

3.1 构造衍生特征

首先,我们可以根据已有的物理量,计算一些新的指标。例如,计算家庭中除三个分项计量外的“其他电路”总耗电量。

# 确保所有相关列都是数值类型
df_clean = df_filled.apply(pd.to_numeric, errors='coerce')

# 计算其他电路耗电(单位:瓦时)
# 公式:总有功电能 (Wh) = Global_active_power (KW) * 1000 / 60 * 时间(1分钟)
# 减去三个分项计量,得到其他电路耗电
df_clean['Other_appliances'] = (df_clean['Global_active_power'] * 1000 / 60) - \
                                 (df_clean['Sub_metering_1'] + \
                                  df_clean['Sub_metering_2'] + \
                                  df_clean['Sub_metering_3'])

# 处理计算可能产生的微小负值(由于测量误差或四舍五入),将其设为0
df_clean['Other_appliances'] = df_clean['Other_appliances'].clip(lower=0)

print("新增特征 'Other_appliances' 的统计信息:")
print(df_clean['Other_appliances'].describe())

这个新特征Other_appliances很有意思,它代表了那些没有被单独监控的电器耗电,比如照明、电脑、电视、充电器等。这部分电量往往占比不小,且波动模式与人的活动规律高度相关。

3.2 提取时间维度特征

时间序列数据的索引是datetime类型,这让我们可以轻松提取出丰富的时序特征。

# 从时间索引中提取多种时间特征
df_clean['hour'] = df_clean.index.hour
df_clean['day_of_week'] = df_clean.index.dayofweek  # 周一=0, 周日=6
df_clean['month'] = df_clean.index.month
df_clean['year'] = df_clean.index.year
df_clean['is_weekend'] = df_clean['day_of_week'].isin([5, 6]).astype(int)  # 周末标记
df_clean['part_of_day'] = pd.cut(df_clean['hour'],
                                  bins=[-1, 6, 12, 18, 24],
                                  labels=['Night', 'Morning', 'Afternoon', 'Evening'])

# 查看新增的时间特征
print(df_clean[['hour', 'day_of_week', 'is_weekend', 'part_of_day']].head(10))

这些特征能帮助我们回答很多业务问题:用电高峰在一天中的哪个时段?工作日和周末的用电模式有何不同?夏季和冬季的用电习惯有什么变化?

3.3 构造统计与滞后特征

对于预测任务,过去时刻的值(滞后特征)和统计量(如滚动均值)往往是强有力的预测因子。

# 计算每小时的平均有功功率作为平滑特征
df_clean['Global_active_power_rolling_1h'] = df_clean['Global_active_power'].rolling(window=60, min_periods=1).mean()

# 创建滞后特征,例如1小时前、24小时前(昨天同一时刻)的功率
df_clean['lag_1h'] = df_clean['Global_active_power'].shift(60)
df_clean['lag_24h'] = df_clean['Global_active_power'].shift(60*24)

# 计算与昨天同一时刻的差值(日同比变化)
df_clean['diff_vs_yesterday'] = df_clean['Global_active_power'] - df_clean['lag_24h']

print("滚动平均与滞后特征示例:")
print(df_clean[['Global_active_power', 'Global_active_power_rolling_1h', 'lag_24h', 'diff_vs_yesterday']].tail())

提示:创建滞后特征时,shift函数会引入缺失值(因为最开始的时间点没有“过去”的数据)。在用于建模前,需要妥善处理这些缺失值,比如删除或填充。

4. 数据可视化:直观理解用电行为

数字表格是冰冷的,图表却能让我们一眼看出模式。可视化不仅是分析的结果,也是探索过程中不可或缺的工具。我们用Matplotlib和Seaborn来画几个关键图表。

4.1 长时间趋势与周期性分析

首先,看看四年来的总功率变化趋势,感受一下数据的宏观面貌。

import matplotlib.pyplot as plt
import seaborn as sns
plt.style.use('seaborn-v0_8-darkgrid')  # 使用美观的样式

# 绘制2007年1月第一周的数据,避免图形过于密集
sample_week = df_clean.loc['2007-01-01':'2007-01-07']

fig, axes = plt.subplots(3, 1, figsize=(14, 10), sharex=True)

# 总有功功率
axes[0].plot(sample_week.index, sample_week['Global_active_power'], color='royalblue', linewidth=0.8)
axes[0].set_ylabel('Active Power (KW)')
axes[0].set_title('Total Active Power - First Week of 2007')
axes[0].grid(True, alpha=0.3)

# 分项电能(厨房、洗衣房、热水空调)
axes[1].plot(sample_week.index, sample_week['Sub_metering_1'], label='Kitchen', alpha=0.7)
axes[1].plot(sample_week.index, sample_week['Sub_metering_2'], label='Laundry', alpha=0.7)
axes[1].plot(sample_week.index, sample_week['Sub_metering_3'], label='Water Heater & AC', alpha=0.7)
axes[1].set_ylabel('Energy (Wh)')
axes[1].set_title('Sub-metered Energy Consumption')
axes[1].legend(loc='upper left')
axes[1].grid(True, alpha=0.3)

# 电压
axes[2].plot(sample_week.index, sample_week['Voltage'], color='green', linewidth=0.8)
axes[2].set_ylabel('Voltage (V)')
axes[2].set_xlabel('Date')
axes[2].set_title('Voltage')
axes[2].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

从这幅图中,你能清晰地看到每日的用电周期:夜间用电低,清晨开始上升,白天维持在一定水平,傍晚出现一个高峰。厨房用电(Sub_metering_1)在餐时间会有明显的脉冲,而热水器和空调(Sub_metering_3)的耗电则显得更持续和平稳。

4.2 用电模式的多维度对比

为了更系统地比较不同时间维度的模式,我们可以用聚合统计和子图矩阵来展示。

# 按小时和星期类型(工作日/周末)聚合分析
hourly_weekday = df_clean[df_clean['is_weekend']==0].groupby('hour')['Global_active_power'].mean()
hourly_weekend = df_clean[df_clean['is_weekend']==1].groupby('hour')['Global_active_power'].mean()

fig, ax = plt.subplots(figsize=(12, 6))
ax.plot(hourly_weekday.index, hourly_weekday.values, marker='o', label='Weekday', linewidth=2)
ax.plot(hourly_weekend.index, hourly_weekend.values, marker='s', label='Weekend', linewidth=2)
ax.set_xlabel('Hour of Day')
ax.set_ylabel('Average Active Power (KW)')
ax.set_title('Average Hourly Power Consumption: Weekday vs. Weekend')
ax.legend()
ax.grid(True, alpha=0.3)
ax.set_xticks(range(0, 24, 2))
plt.show()

这个对比图往往能揭示有趣的现象。在我分析的这个数据集里,工作日的用电高峰通常出现在早上7-9点(准备上班上学)和晚上6-9点(下班回家后),而周末的用电高峰则可能推迟并拉长,且午间的用电量明显高于工作日,这很可能是因为周末家人在家做饭、休闲活动更多。

4.3 分布分析与异常检测

了解数据的分布形态,有助于我们发现异常和选择合适的数据转换方法。

# 绘制关键变量的分布直方图与箱线图
fig, axes = plt.subplots(2, 3, figsize=(15, 10))
variables_to_plot = ['Global_active_power', 'Global_reactive_power', 'Voltage',
                     'Sub_metering_1', 'Sub_metering_2', 'Sub_metering_3']
colors = ['skyblue', 'lightcoral', 'lightgreen', 'gold', 'violet', 'orange']

for idx, var in enumerate(variables_to_plot):
    row, col = idx // 3, idx % 3
    # 直方图
    axes[row, col].hist(df_clean[var].dropna(), bins=50, color=colors[idx], edgecolor='black', alpha=0.7)
    axes[row, col].set_title(f'Distribution of {var}')
    axes[row, col].set_xlabel(var)
    axes[row, col].set_ylabel('Frequency')
    # 在子图右上角添加中位数和标准差文本
    median_val = df_clean[var].median()
    std_val = df_clean[var].std()
    axes[row, col].text(0.95, 0.95, f'Median: {median_val:.2f}\nStd: {std_val:.2f}',
                        transform=axes[row, col].transAxes,
                        verticalalignment='top',
                        horizontalalignment='right',
                        bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5))

plt.tight_layout()
plt.show()

从分布图可以看出,总有功功率Global_active_power的分布呈现明显的双峰形态。一个峰值在0.3千瓦左右(低功耗待机状态),另一个峰值在1.3千瓦左右(常规活动状态)。这种双峰分布非常符合家庭用电的特点:大部分时间处于低功耗状态,当有主要电器(如空调、电热水器)启动时,功率跃升到另一个水平。电压Voltage的分布则接近正态分布,集中在235伏左右,符合电网的稳定输送特性。

5. 数据重塑与输出:为建模做准备

清洗和探索后的数据,最终需要保存下来,供后续的机器学习或深度学习模型使用。这里有几个实用的处理步骤。

5.1 处理缺失值与数据规整

尽管我们之前已经处理过缺失值,但在构建特征(如滞后特征)后,数据集的起始部分可能会产生新的NaN。在保存前,我们需要做一个最终清理。

# 最终数据清理:删除因创建滞后特征等产生的缺失行
df_final = df_clean.dropna().copy()
print(f"最终可用于建模的数据行数:{len(df_final)}")

# 重置索引,将datetime索引变为普通列(某些建模库需要)
df_final_reset = df_final.reset_index()
print("数据最终列名:", df_final_reset.columns.tolist())

5.2 数据采样与存储

原始数据是每分钟一条,对于某些预测任务(如预测明天整天的总用电量)来说可能过于精细,导致数据量庞大且噪声多。这时,可以考虑按小时或按天进行重采样聚合。

# 示例:按小时重采样,计算每小时的平均功率和总电能
df_hourly = df_final.resample('H').agg({
    'Global_active_power': 'mean',
    'Global_reactive_power': 'mean',
    'Voltage': 'mean',
    'Global_intensity': 'mean',
    'Sub_metering_1': 'sum',  # 电能求和
    'Sub_metering_2': 'sum',
    'Sub_metering_3': 'sum',
    'Other_appliances': 'sum',
    'is_weekend': 'first'     # 取该小时内的第一个值
}).dropna()

print(f"按小时聚合后数据形状:{df_hourly.shape}")
print(df_hourly.head())

重采样后,数据量从200多万行减少到约3.5万行,处理起来会快得多,且更容易看出宏观趋势。

最后,将处理好的数据保存为更通用的格式,如CSV或Feather格式。

# 保存最终清洗后的数据
output_path_csv = 'household_power_consumption_cleaned.csv'
df_final.to_csv(output_path_csv, index=True)  # 保留时间索引
print(f"数据已保存至:{output_path_csv}")

# 如果需要更快的读写速度,可以保存为Feather或Parquet格式
try:
    df_final.to_parquet('household_power_consumption_cleaned.parquet', index=True)
    print("同时保存为Parquet格式完成。")
except Exception as e:
    print(f"保存Parquet时出错:{e},确保已安装pyarrow或fastparquet库。")

5.3 构建可供建模的数据集

如果你计划进行预测任务,比如预测未来24小时的家庭总功率,那么需要将数据构造成监督学习问题的格式。这里提供一个简单的函数,用于创建包含滞后特征的数据框。

def create_lagged_features(df, target_col, lags_list):
    """
    为时间序列创建滞后特征。
    参数:
        df: 输入DataFrame,索引需为时间戳。
        target_col: 需要创建滞后特征的目标列名。
        lags_list: 滞后步长列表,如 [1, 2, 24, 168] 表示滞后1小时、2小时、1天、1周。
    返回:
        包含原始特征和滞后特征的新DataFrame。
    """
    df_lagged = df[[target_col]].copy()
    for lag in lags_list:
        df_lagged[f'{target_col}_lag_{lag}'] = df[target_col].shift(lag)
    # 删除因创建滞后特征产生的缺失行
    df_lagged.dropna(inplace=True)
    return df_lagged

# 示例:为小时级数据创建滞后特征
lags = [1, 2, 3, 24, 48, 168]  # 滞后1,2,3小时,1天,2天,1周
df_for_modeling = create_lagged_features(df_hourly, 'Global_active_power', lags)
print("为建模准备的数据预览(包含滞后特征):")
print(df_for_modeling.head())

这个df_for_modeling就可以直接作为许多机器学习模型(如线性回归、随机森林、梯度提升树)的输入,其中每一行包含了当前时刻的功率值以及过去多个时刻的功率值作为特征。

处理200万行数据,听起来是个大工程,但拆解成加载、清洗、特征工程、可视化、输出这几个步骤后,每一步都有清晰的Pandas操作与之对应。最关键的是理解数据背后的业务逻辑——家庭用电的周期性与突发性。当你看到图表中傍晚的用电高峰,或周末与工作日的模式差异时,你分析的就不再是冷冰冰的数字,而是一个家庭真实的生活节奏。这些清洗和探索后的数据,无论是用于简单的能耗报告,还是作为复杂预测模型的基石,其质量和深度都决定了最终结果的价值。

Logo

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

更多推荐