数据预处理pandas pd.json_normalize占用内存过大优化

文章讨论了在使用pandas处理从ES下载的大量JSON数据时,如何因数据解析导致内存占用剧增的问题。解决方法包括分批读取、指定字段类型和合理管理内存。

问题描述

从ES下载数据,数据格式为json,然后由pandas进行解析,json中的嵌套字段会进行展开作为列名(由于维度初期无法预测,所以根据数据有啥列就使用啥列,这是最方便的点),变成表格,方面了后续的处理,但在使用过程却发现原本6.xG的数据量在解析,预处理时候会变成60多G,甚至80G的内存占用,资源难以满足

解决

为了方便测试,使用了一个300MB大小的数据进行测试
1.为什么原本的300MB数据量会占用2G内存呢,仅仅下面一个操作

df = pd.json_normalize(datas)

思考:
(1)python是面向全对象语言,所以里面每一个数值都会是对象,这个对象很大
(2)数据集合json中的指标字段并不对齐,例如datas = [{field1:100},{“field1”:90,“field2”:12}],既然pandas是表格,总要填充
验证思考1
查询pd对象,果然用大的对象来存储,数据表中共有2732列,13列是等文本数据对应object,2675为float64,44列为int64,然而pd.json_normalize方法没有设置字段类型

print(df.info())
------------------------------------
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 100000 entries, 0 to 99999
Columns: 2732 entries, feild1 to feild2
dtypes: float64(2675), int64(44), object(13)
memory usage: 2.0+ GB

在这里插入图片描述
验证思考2
其中一个json有新的字段score,发现增加24个字节,说明每条8字节,做了填充

a1 = json.loads('{"name":"zhangsan","age":12}')
a2 = json.loads('{"name":"zhangsan","age":12}')
a3 = json.loads('{"name":"zhangsan","age":12}')
aa = list()
aa.append(a1)
aa.append(a2)
aa.append(a3)
df = pd.json_normalize(aa)
print(df.info())
---------------------------
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 3 entries, 0 to 2
Data columns (total 2 columns):
 #   Column  Non-Null Count  Dtype 
---  ------  --------------  ----- 
 0   name    3 non-null      object
 1   age     3 non-null      int64 
dtypes: int64(1), object(1)
memory usage: 176.0+ bytes
a1 = json.loads('{"name":"zhangsan","age":12}')
a2 = json.loads('{"name":"zhangsan","age":12}')
a3 = json.loads('{"name":"zhangsan","age":12,"scroe":100}')
print(df.info())
---------------------------
memory usage: 200.0+ bytes

解决问题:
(1)如果原始datas数据量太大,那么只能使用pd.json_normalize分批读取后保存csv,(后面合并的时候可能会涉及拼接,这里不展开)
(2)读取csv,指定字段和字段类型,例如读取浮点类型的字段,这里单精度float32就可以啦

pd.read_csv(path, usecols=["浮点列1","浮点列2"], dtype=np.float32)

2.可能中间过程还会涉及拆分训练集,验证集,标准化等,还可以使用del先释放不需要的内存(注意del的对象要确保无引用,否则del无效)

train_x, valid_x, train_y, valid_y = train_test_split(datas, y_index, y, test_size=0.3, random_state=42)
del datas #确保datas无其他引用
import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns from sklearn.preprocessing import MinMaxScaler from sklearn.impute import SimpleImputer import warnings warnings.filterwarnings('ignore') # 设置中文字体 plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans'] plt.rcParams['axes.unicode_minus'] = False class PowerDataProcessor: """多源电力数据预处理类:包含数据加载、清洗、特征工程功能""" def __init__(self, file_paths, time_col_config=None): """ 初始化函数 :param file_paths: 字典,包含各数据文件路径,格式如{'day_ahead':'电价_日前.xlsx', ...} :param time_col_config: 字典,各文件的时间列名称配置,默认所有文件时间列为"时间" """ self.file_paths = file_paths self.raw_data = {} # 存储原始数据 self.processed_data = None # 存储预处理后的数据 self.scaler = MinMaxScaler() # 归一化器 # 配置各文件的时间列名称,默认"时间",可自定义 self.time_col_config = time_col_config if time_col_config is not None else { 'day_ahead': '时间', 'real_time': '时间', 'wind': 'DATA_TIME', # 这里需要替换为风能文件实际的时间列名 'solar': 'DATA_TIME', 'load': 'DATA_TIME', 'weather': 'DATA_TIME' } def load_data(self): """加载所有数据源并存储到raw_data字典""" print("开始加载数据...") # 加载日前电价数据 if 'day_ahead' in self.file_paths: time_col = self.time_col_config['day_ahead'] self.raw_data['day_ahead'] = pd.read_excel( self.file_paths['day_ahead'], parse_dates=[time_col], index_col=time_col ) print(f"日前电价数据加载完成,数据量:{len(self.raw_data['day_ahead'])}条") # 加载实时电价数据 if 'real_time' in self.file_paths: time_col = self.time_col_config['real_time'] self.raw_data['real_time'] = pd.read_excel( self.file_paths['real_time'], parse_dates=[time_col], index_col=time_col ) print(f"实时电价数据加载完成,数据量:{len(self.raw_data['real_time'])}条") # 加载风能发电数据(关键修改:使用配置的时间列名) if 'wind' in self.file_paths: time_col = self.time_col_config['wind'] # 从配置中获取风能文件的时间列名 self.raw_data['wind'] = pd.read_excel( self.file_paths['wind'], parse_dates=[time_col], index_col=time_col ) print(f"风能发电数据加载完成,数据量:{len(self.raw_data['wind'])}条") # 加载光伏发电数据 if 'solar' in self.file_paths: time_col = self.time_col_config['solar'] self.raw_data['solar'] = pd.read_excel( self.file_paths['solar'], parse_dates=[time_col], index_col=time_col ) print(f"光伏发电数据加载完成,数据量:{len(self.raw_data['solar'])}条") # 加载高压侧总负荷数据 if 'load' in self.file_paths: time_col = self.time_col_config['load'] self.raw_data['load'] = pd.read_excel( self.file_paths['load'], parse_dates=[time_col], index_col=time_col ) print(f"高压侧总负荷数据加载完成,数据量:{len(self.raw_data['load'])}条") # 加载气象数据 if 'weather' in self.file_paths: time_col = self.time_col_config['weather'] self.raw_data['weather'] = pd.read_excel( self.file_paths['weather'], parse_dates=[time_col], index_col=time_col ) print(f"气象数据加载完成,数据量:{len(self.raw_data['weather'])}条") def merge_data(self, join_type='inner'): """ 合并多源数据(以时间为索引) :param join_type: 合并方式,默认inner(仅保留时间完全匹配的数据) :return: 合并后的DataFrame """ print(f"\n开始合并数据,合并方式:{join_type}") # 初始化合并结果为第一个数据源 merged_df = None for key, df in self.raw_data.items(): if merged_df is None: merged_df = df.copy() else: merged_df = merged_df.join(df, how=join_type, rsuffix=f'_{key}') # 重命名可能重复的列(如实时电价和日前电价均可能有"价格"列) price_cols = [col for col in merged_df.columns if '价格' in col or '电价' in col] if len(price_cols) > 1: merged_df.rename(columns={price_cols[0]: '日前电价', price_cols[1]: '实时电价'}, inplace=True) print(f"数据合并完成,合并后数据量:{len(merged_df)}条,字段数:{len(merged_df.columns)}个") self.processed_data = merged_df return merged_df def handle_missing_values(self, strategy='interpolate'): """ 缺失值处理 :param strategy: 处理策略:interpolate(线性插值)、median(中位数填充)、mode(众数填充) :return: 处理缺失值后的DataFrame """ if self.processed_data is None: raise ValueError("请先执行merge_data()合并数据") print(f"\n开始缺失值处理,策略:{strategy}") # 统计缺失值情况 missing_info = self.processed_data.isnull().sum() missing_info = missing_info[missing_info > 0] if len(missing_info) > 0: print("缺失值统计:") for col, count in missing_info.items(): missing_rate = count / len(self.processed_data) * 100 print(f" {col}: {count}条({missing_rate:.2f}%)") else: print("无缺失值,无需处理") return self.processed_data # 按策略处理缺失值 df_clean = self.processed_data.copy() numeric_cols = df_clean.select_dtypes(include=[np.number]).columns categorical_cols = df_clean.select_dtypes(include=['object', 'category']).columns if strategy == 'interpolate': # 数值型字段线性插值 df_clean[numeric_cols] = df_clean[numeric_cols].interpolate(method='linear', limit_direction='both') # 分类字段众数填充 for col in categorical_cols: mode_val = df_clean[col].mode()[0] df_clean[col] = df_clean[col].fillna(mode_val) elif strategy == 'median': # 数值型字段中位数填充 imputer = SimpleImputer(strategy='median') df_clean[numeric_cols] = imputer.fit_transform(df_clean[numeric_cols]) # 分类字段众数填充 for col in categorical_cols: mode_val = df_clean[col].mode()[0] df_clean[col] = df_clean[col].fillna(mode_val) elif strategy == 'mode': # 分类字段众数填充 for col in categorical_cols: mode_val = df_clean[col].mode()[0] df_clean[col] = df_clean[col].fillna(mode_val) # 数值型字段众数填充 for col in numeric_cols: mode_val = df_clean[col].mode()[0] df_clean[col] = df_clean[col].fillna(mode_val) print("缺失值处理完成") self.processed_data = df_clean return df_clean def handle_outliers(self, method='IQR', threshold=1.5): """ 异常值处理 :param method: 异常值检测方法,默认IQR(四分位距法) :param threshold: IQR阈值,默认1.5 :return: 处理异常值后的DataFrame """ if self.processed_data is None: raise ValueError("请先执行merge_data()合并数据") print(f"\n开始异常值处理,方法:{method},阈值:{threshold}") df_clean = self.processed_data.copy() numeric_cols = df_clean.select_dtypes(include=[np.number]).columns outlier_count = {} if method == 'IQR': for col in numeric_cols: # 计算四分位数 Q1 = df_clean[col].quantile(0.25) Q3 = df_clean[col].quantile(0.75) IQR = Q3 - Q1 # 异常值边界 lower_bound = Q1 - threshold * IQR upper_bound = Q3 + threshold * IQR # 统计异常值数量 outliers = df_clean[(df_clean[col] < lower_bound) | (df_clean[col] > upper_bound)] outlier_count[col] = len(outliers) # 用中位数替换异常值 median_val = df_clean[col].median() df_clean.loc[(df_clean[col] < lower_bound) | (df_clean[col] > upper_bound), col] = median_val # 输出异常值统计 if sum(outlier_count.values()) > 0: print("异常值统计(已用中位数替换):") for col, count in outlier_count.items(): outlier_rate = count / len(df_clean) * 100 print(f" {col}: {count}条({outlier_rate:.2f}%)") else: print("无异常值,无需处理") self.processed_data = df_clean return df_clean def feature_engineering(self): """ 特征工程:衍生时间特征、滞后特征、聚合特征 :return: 新增特征后的DataFrame """ if self.processed_data is None: raise ValueError("请先执行merge_data()合并数据") print("\n开始特征工程...") df_feat = self.processed_data.copy() # 1. 衍生时间特征 df_feat['小时'] = df_feat.index.hour df_feat['分钟'] = df_feat.index.minute df_feat['日期'] = df_feat.index.date df_feat['星期'] = df_feat.index.weekday # 0=周一,6=周日 df_feat['是否工作日'] = df_feat['星期'].apply(lambda x: 1 if x < 5 else 0) # 时段划分(根据电力负荷特性) df_feat['用电时段'] = pd.cut( df_feat['小时'], bins=[0, 6, 9, 12, 16, 21, 24], labels=['凌晨(0-6)', '早高峰(7-9)', '上午平段(10-12)', '下午平段(13-16)', '晚高峰(17-21)', '夜间(22-23)'] ) # 2. 衍生电价滞后特征(捕捉时间序列相关性) if '日前电价' in df_feat.columns: df_feat['日前电价_滞后15分钟'] = df_feat['日前电价'].shift(1) df_feat['日前电价_滞后30分钟'] = df_feat['日前电价'].shift(2) df_feat['日前电价_滞后1小时'] = df_feat['日前电价'].shift(4) # 15分钟/间隔,1小时=4个间隔 # 填充滞后特征产生的缺失值 df_feat['日前电价_滞后15分钟'] = df_feat['日前电价_滞后15分钟'].fillna(df_feat['日前电价'].median()) df_feat['日前电价_滞后30分钟'] = df_feat['日前电价_滞后30分钟'].fillna(df_feat['日前电价'].median()) df_feat['日前电价_滞后1小时'] = df_feat['日前电价_滞后1小时'].fillna(df_feat['日前电价'].median()) if '实时电价' in df_feat.columns: df_feat['实时电价_滞后15分钟'] = df_feat['实时电价'].shift(1) df_feat['实时电价_滞后30分钟'] = df_feat['实时电价'].shift(2) df_feat['实时电价_滞后1小时'] = df_feat['实时电价'].shift(4) df_feat['实时电价_滞后15分钟'] = df_feat['实时电价_滞后15分钟'].fillna(df_feat['实时电价'].median()) df_feat['实时电价_滞后30分钟'] = df_feat['实时电价_滞后30分钟'].fillna(df_feat['实时电价'].median()) df_feat['实时电价_滞后1小时'] = df_feat['实时电价_滞后1小时'].fillna(df_feat['实时电价'].median()) # 3. 衍生聚合特征(每小时统计值) hourly_agg = df_feat.groupby(['日期', '小时']).agg({ '日前电价': ['mean', 'max', 'min'], '实时电价': ['mean', 'max', 'min'], 'DATA_VALUE': ['mean', 'max'] # 假设负荷数据列名为"高压侧总负荷" }).reset_index() # 重命名聚合特征列 hourly_agg.columns = ['日期', '小时', '日前电价_小时均值', '日前电价_小时最值', '日前电价_小时最小值', '实时电价_小时均值', '实时电价_小时最值', '实时电价_小时最小值', '负荷_小时均值', '负荷_小时最值'] # 合并聚合特征 df_feat['日期_str'] = df_feat['日期'].astype(str) hourly_agg['日期_str'] = hourly_agg['日期'].astype(str) df_feat = df_feat.merge( hourly_agg.drop('日期', axis=1), on=['日期_str', '小时'], how='left' ) df_feat.drop('日期_str', axis=1, inplace=True) # 4. 分类变量编码(开停状态) if '开停' in df_feat.columns: df_feat['开停_编码'] = df_feat['开停'].map({'开': 1, '停': 0}) print(f"特征工程完成,新增特征后字段数:{len(df_feat.columns)}个") self.processed_data = df_feat return df_feat def normalize_data(self, cols_to_normalize=None): """ 数据归一化(Min-Max标准化到[0,1]) :param cols_to_normalize: 需要归一化的字段列表,默认对所有数值型字段归一化 :return: 归一化后的数据 """ if self.processed_data is None: raise ValueError("请先执行merge_data()合并数据") print("\n开始数据归一化...") df_norm = self.processed_data.copy() if cols_to_normalize is None: cols_to_normalize = df_norm.select_dtypes(include=[np.number]).columns # 排除不需要归一化的时间类特征 cols_to_normalize = [col for col in cols_to_normalize if col not in ['小时', '分钟', '星期', '是否工作日']] # 执行归一化 df_norm[cols_to_normalize] = self.scaler.fit_transform(df_norm[cols_to_normalize]) print(f"归一化完成,归一化字段:{cols_to_normalize}") self.processed_data = df_norm return df_norm def save_processed_data(self, save_path='processed_power_data.csv'): """ 保存预处理后的数据 :param save_path: 保存路径,默认CSV格式 """ if self.processed_data is None: raise ValueError("请先完成数据预处理流程") self.processed_data.to_csv(save_path, encoding='utf-8-sig') print(f"\n预处理后的数据已保存至:{save_path}") class PowerDataAnalyzer: """多源电力数据描述性分析类:包含单变量、双变量、时间序列分析及可视化""" def __init__(self, processed_data): """ 初始化函数 :param processed_data: 预处理后的DataFrame(来自PowerDataProcessor) """ self.data = processed_data # 定义关键分析字段(可根据实际数据调整) self.price_cols = [col for col in self.data.columns if '电价' in col] self.power_cols = [col for col in self.data.columns if '发电' in col or '负荷' in col] self.weather_cols = [col for col in self.data.columns if '温度' in col or '风速' in col or '湿度' in col or '光照' in col] self.time_cols = ['小时', '星期', '用电时段', '是否工作日'] def univariate_analysis(self, save_fig_path='单变量分析图/'): """ 单变量分析:统计描述+分布可视化 :param save_fig_path: 图表保存路径 """ import os os.makedirs(save_fig_path, exist_ok=True) print("\n=== 开始单变量分析 ===") # 1. 输出统计描述 print("\n1. 数值型变量统计描述:") numeric_data = self.data.select_dtypes(include=[np.number]) print(numeric_data.describe().round(2)) # 2. 电价分布分析 if len(self.price_cols) > 0: print(f"\n2. 电价分布分析(字段:{self.price_cols})") fig, axes = plt.subplots(len(self.price_cols), 2, figsize=(15, 5*len(self.price_cols))) axes = axes.flatten() if len(self.price_cols) > 1 else [axes[0], axes[1]] for i, col in enumerate(self.price_cols): # 直方图+核密度图 sns.histplot(self.data[col], kde=True, ax=axes[2*i], color='#2E86AB', alpha=0.7) axes[2*i].set_title(f'{col}分布直方图', fontsize=12, fontweight='bold') axes[2*i].set_xlabel(col) axes[2*i].set_ylabel('频次') axes[2*i].grid(alpha=0.3) # 箱线图 sns.boxplot(y=self.data[col], ax=axes[2*i+1], color='#A23B72', width=0.6) axes[2*i+1].set_title(f'{col}箱线图(异常值已处理)', fontsize=12, fontweight='bold') axes[2*i+1].set_ylabel(col) axes[2*i+1].grid(alpha=0.3) plt.tight_layout() plt.savefig(f'{save_fig_path}电价分布分析.png', dpi=300, bbox_inches='tight') plt.close() print(f"电价分布图表已保存至:{save_fig_path}电价分布分析.png") # 3. 开停状态分布 if '开停' in self.data.columns: print("\n3. 开停状态分布分析") status_count = self.data['开停'].value_counts() print("开停状态统计:") for status, count in status_count.items(): rate = count / len(self.data) * 100 print(f" {status}: {count}条({rate:.2f}%)") # 饼图 fig, ax = plt.subplots(figsize=(8, 8)) colors = ['#F18F01', '#C73E1D'] wedges, texts, autotexts = ax.pie( status_count.values, labels=status_count.index, autopct='%1.1f%%', colors=colors, startangle=90, textprops={'fontsize': 11} ) ax.set_title('电价交易开停状态分布', fontsize=14, fontweight='bold', pad=20) plt.savefig(f'{save_fig_path}开停状态分布.png', dpi=300, bbox_inches='tight') plt.close() print(f"开停状态图表已保存至:{save_fig_path}开停状态分布.png") # 4. 气象指标分布 if len(self.weather_cols) > 0: print(f"\n4. 气象指标分布分析(字段:{self.weather_cols})") # 计算子图行数(每行2个字段) n_rows = (len(self.weather_cols) + 1) // 2 fig, axes = plt.subplots(n_rows, 2, figsize=(15, 4*n_rows)) axes = axes.flatten() for i, col in enumerate(self.weather_cols): if i < len(axes): sns.boxplot(y=self.data[col], ax=axes[i], color='#6A994E', width=0.6) axes[i].set_title(f'{col}分布', fontsize=11, fontweight='bold') axes[i].set_ylabel(col) axes[i].grid(alpha=0.3) # 隐藏多余的子图 for i in range(len(self.weather_cols), len(axes)): axes[i].set_visible(False) plt.tight_layout() plt.savefig(f'{save_fig_path}气象指标分布.png', dpi=300, bbox_inches='tight') plt.close() print(f"气象指标图表已保存至:{save_fig_path}气象指标分布.png") def bivariate_analysis(self, save_fig_path='双变量分析图/'): """ 双变量分析:相关性分析+关键变量关系可视化 :param save_fig_path: 图表保存路径 """ import os os.makedirs(save_fig_path, exist_ok=True) print("\n=== 开始双变量分析 ===") # 1. 相关性分析 print("\n1. 数值型变量相关性分析") numeric_data = self.data.select_dtypes(include=[np.number]) corr_matrix = numeric_data.corr().round(2) # 输出与电价相关性Top10的变量 for price_col in self.price_cols: if price_col in corr_matrix.columns: price_corr = corr_matrix[price_col].sort_values(ascending=False) print(f"\n与{price_col}相关性Top10的变量:") print(price_corr.head(11)) # 包含自身,所以取前11个 # 相关性热力图 fig, ax = plt.subplots(figsize=(16, 14)) mask = np.triu(np.ones_like(corr_matrix, dtype=bool)) # 隐藏上三角 cmap = sns.diverging_palette(220, 10, as_cmap=True) sns.heatmap( corr_matrix, mask=mask, cmap=cmap, annot=False, square=True, linewidths=.5, cbar_kws={"shrink": .8}, ax=ax ) ax.set_title('多源电力数据相关性热力图', fontsize=16, fontweight='bold', pad=30) plt.savefig(f'{save_fig_path}相关性热力图.png', dpi=300, bbox_inches='tight') plt.close() print(f"相关性热力图已保存至:{save_fig_path}相关性热力图.png") # 2. 电价与开停状态关系 if '开停' in self.data.columns and len(self.price_cols) > 0: print("\n2. 电价与开停状态关系分析") fig, axes = plt.subplots(1, len(self.price_cols), figsize=(12*len(self.price_cols), 6)) axes = [axes] if len(self.price_cols) == 1 else axes for i, price_col in enumerate(self.price_cols): sns.boxplot( x='开停', y=price_col, data=self.data, ax=axes[i], palette=['#C73E1D', '#F18F01'], width=0.6 ) axes[i].set_title(f'{price_col}与开停状态关系', fontsize=12, fontweight='bold') axes[i].set_xlabel('开停状态') axes[i].set_ylabel(price_col) axes[i].grid(alpha=0.3) plt.tight_layout() plt.savefig(f'{save_fig_path}电价与开停状态关系.png', dpi=300, bbox_inches='tight') plt.close() print(f"电价与开停状态关系图已保存至:{save_fig_path}电价与开停状态关系.png") # 3. 电价与用电时段关系 if '用电时段' in self.data.columns and len(self.price_cols) > 0: print("\n3. 电价与用电时段关系分析") fig, axes = plt.subplots(1, len(self.price_cols), figsize=(14*len(self.price_cols), 7)) axes = [axes] if len(self.price_cols) == 1 else axes for i, price_col in enumerate(self.price_cols): sns.violinplot( x='用电时段', y=price_col, data=self.data, ax=axes[i], palette='Set3', inner='quartile' ) axes[i].set_title(f'{price_col}与用电时段关系', fontsize=12, fontweight='bold') axes[i].set_xlabel('用电时段') axes[i].set_ylabel(price_col) axes[i].tick_params(axis='x', rotation=45) axes[i].grid(alpha=0.3) plt.tight_layout() plt.savefig(f'{save_fig_path}电价与用电时段关系.png', dpi=300, bbox_inches='tight') plt.close() print(f"电价与用电时段关系图已保存至:{save_fig_path}电价与用电时段关系.png") # 4. 电价与关键气象指标关系(选择相关性最高的2个气象指标) if len(self.weather_cols) > 0 and len(self.price_cols) > 0: print("\n4. 电价与气象指标关系分析(选择相关性最高的2个气象指标)") # 选择与第一个电价字段相关性最高的2个气象指标 price_col = self.price_cols[0] weather_corr = corr_matrix[price_col][self.weather_cols].sort_values(ascending=False) top2_weather = weather_corr.head(2).index.tolist() if len(top2_weather) >= 2: fig, axes = plt.subplots(1, 2, figsize=(16, 6)) for i, weather_col in enumerate(top2_weather): sns.scatterplot( x=weather_col, y=price_col, data=self.data, ax=axes[i], color='#2E86AB', alpha=0.6, s=30 ) # 添加趋势线 z = np.polyfit(self.data[weather_col], self.data[price_col], 1) p = np.poly1d(z) axes[i].plot(self.data[weather_col], p(self.data[weather_col]), "r--", linewidth=2) axes[i].set_title(f'{price_col}与{weather_col}关系(相关系数:{corr_matrix.loc[price_col, weather_col]:.2f})', fontsize=11, fontweight='bold') axes[i].set_xlabel(weather_col) axes[i].set_ylabel(price_col) axes[i].grid(alpha=0.3) plt.tight_layout() plt.savefig(f'{save_fig_path}电价与气象指标关系.png', dpi=300, bbox_inches='tight') plt.close() print(f"电价与气象指标关系图已保存至:{save_fig_path}电价与气象指标关系.png") def time_series_analysis(self, save_fig_path='时间序列分析图/'): """ 时间序列分析:电价时序趋势+周期性分析 :param save_fig_path: 图表保存路径 """ import os os.makedirs(save_fig_path, exist_ok=True) print("\n=== 开始时间序列分析 ===") # 1. 日内电价趋势(按小时平均) print("\n1. 日内电价趋势分析") hourly_price = self.data.groupby('小时')[self.price_cols].mean().reset_index() fig, ax = plt.subplots(figsize=(14, 7)) for price_col in self.price_cols: ax.plot( hourly_price['小时'], hourly_price[price_col], marker='o', linewidth=2.5, label=price_col, markersize=6 ) # 添加时段分隔线 ax.axvline(x=6, color='gray', linestyle='--', alpha=0.5, label='时段分界') ax.axvline(x=9, color='gray', linestyle='--', alpha=0.5) ax.axvline(x=12, color='gray', linestyle='--', alpha=0.5) ax.axvline(x=16, color='gray', linestyle='--', alpha=0.5) ax.axvline(x=21, color='gray', linestyle='--', alpha=0.5) ax.set_title('日内电价变化趋势(24小时平均)', fontsize=14, fontweight='bold', pad=20) ax.set_xlabel('小时', fontsize=12) ax.set_ylabel('电价', fontsize=12) ax.set_xticks(range(0, 24)) ax.legend(fontsize=11) ax.grid(alpha=0.3) plt.savefig(f'{save_fig_path}日内电价趋势.png', dpi=300, bbox_inches='tight') plt.close() print(f"日内电价趋势图已保存至:{save_fig_path}日内电价趋势.png") # 2. 周内电价趋势(按星期平均) print("\n2. 周内电价趋势分析") weekday_price = self.data.groupby('星期')[self.price_cols].mean().reset_index() weekday_mapping = {0: '周一', 1: '周二', 2: '周三', 3: '周四', 4: '周五', 5: '周六', 6: '周日'} weekday_price['星期名称'] = weekday_price['星期'].map(weekday_mapping) fig, ax = plt.subplots(figsize=(12, 6)) for price_col in self.price_cols: ax.plot( weekday_price['星期名称'], weekday_price[price_col], marker='s', linewidth=2.5, label=price_col, markersize=8 ) # 标记工作日与周末 ax.axvspan(-0.5, 4.5, color='#E8F4FD', alpha=0.3, label='工作日') ax.axvspan(4.5, 6.5, color='#FFF2E8', alpha=0.3, label='周末') ax.set_title('周内电价变化趋势(按星期平均)', fontsize=14, fontweight='bold', pad=20) ax.set_xlabel('星期', fontsize=12) ax.set_ylabel('电价', fontsize=12) ax.legend(fontsize=11) ax.grid(alpha=0.3) plt.savefig(f'{save_fig_path}周内电价趋势.png', dpi=300, bbox_inches='tight') plt.close() print(f"周内电价趋势图已保存至:{save_fig_path}周内电价趋势.png") # 3. 电价与负荷时序同步性(选择第一个电价字段和负荷字段) if len(self.power_cols) > 0 and len(self.price_cols) > 0: print("\n3. 电价与负荷时序同步性分析") # 选择前7天数据(避免图表过于密集) sample_data = self.data.head(7*96) # 15分钟/间隔,1天=96个间隔 price_col = self.price_cols[0] load_col = self.power_cols[0] fig, ax1 = plt.subplots(figsize=(16, 8)) # 绘制电价曲线 color1 = '#2E86AB' ax1.set_xlabel('时间', fontsize=12) ax1.set_ylabel(price_col, color=color1, fontsize=12) ax1.plot(sample_data.index, sample_data[price_col], color=color1, linewidth=2, label=price_col) ax1.tick_params(axis='y', labelcolor=color1) ax1.grid(alpha=0.3) # 绘制负荷曲线(双Y轴) ax2 = ax1.twinx() color2 = '#A23B72' ax2.set_ylabel(load_col, color=color2, fontsize=12) ax2.plot(sample_data.index, sample_data[load_col], color=color2, linewidth=2, label=load_col) ax2.tick_params(axis='y', labelcolor=color2) # 添加图例 lines1, labels1 = ax1.get_legend_handles_labels() lines2, labels2 = ax2.get_legend_handles_labels() ax1.legend(lines1 + lines2, labels1 + labels2, loc='upper right', fontsize=11) plt.title('电价与负荷时序同步性(前7天)', fontsize=14, fontweight='bold', pad=20) plt.savefig(f'{save_fig_path}电价与负荷时序同步性.png', dpi=300, bbox_inches='tight') plt.close() print(f"电价与负荷时序同步性图已保存至:{save_fig_path}电价与负荷时序同步性.png") def generate_analysis_report(self, report_path='电力数据描述性分析报告.txt'): """生成分析报告""" print(f"\n=== 生成分析报告 ===") report = [] report.append("多源电力数据预处理与描述性分析报告") report.append("="*50) # 1. 数据基本信息 report.append(f"\n一、数据基本信息") report.append(f"1. 数据时间范围:{self.data.index.min()} ~ {self.data.index.max()}") report.append(f"2. 数据总量:{len(self.data)}条记录,{len(self.data.columns)}个字段") report.append(f"3. 核心字段分类:") report.append(f" - 电价字段:{self.price_cols}") report.append(f" - 发电/负荷字段:{self.power_cols}") report.append(f" - 气象字段:{self.weather_cols}") report.append(f" - 时间特征字段:{self.time_cols}") # 2. 预处理结果 report.append(f"\n二、数据预处理结果") report.append(f"1. 缺失值处理:无缺失值(已通过线性插值+众数填充处理)") report.append(f"2. 异常值处理:已通过IQR法识别并替换异常值(阈值1.5)") report.append(f"3. 特征工程:新增时间特征(时段、工作日)、滞后特征(15min/30min/1h)、聚合特征(小时均值/最值)") report.append(f"4. 数据归一化:已对{len([col for col in self.data.columns if '电价' in col or '发电' in col or '负荷' in col])}个核心字段进行Min-Max归一化") # 3. 关键发现 report.append(f"\n三、关键分析发现") # 3.1 电价分布特征 for price_col in self.price_cols: mean_val = self.data[price_col].mean() max_val = self.data[price_col].max() min_val = self.data[price_col].min() report.append(f"1. {price_col}分布:") report.append(f" - 均值:{mean_val:.2f},最值:{max_val:.2f},最小值:{min_val:.2f}") # 时段差异 hourly_max = self.data.groupby('用电时段')[price_col].mean().sort_values(ascending=False) report.append(f" - 最高电价时段:{hourly_max.index[0]}(均值:{hourly_max.iloc[0]:.2f})") report.append(f" - 最低电价时段:{hourly_max.index[-1]}(均值:{hourly_max.iloc[-1]:.2f})") # 3.2 开停状态影响 if '开停' in self.data.columns: on_price = self.data[self.data['开停'] == '开'][self.price_cols].mean() off_price = self.data[self.data['开停'] == '停'][self.price_cols].mean() report.append(f"\n2. 开停状态影响:") for price_col in self.price_cols: diff = (on_price[price_col] - off_price[price_col]) / off_price[price_col] * 100 report.append(f" - {price_col}:开状态均值{on_price[price_col]:.2f},停状态均值{off_price[price_col]:.2f},差异{diff:.2f}%") # 3.3 相关性特征 numeric_data = self.data.select_dtypes(include=[np.number]) corr_matrix = numeric_data.corr() report.append(f"\n3. 强相关性特征(|相关系数|>0.7):") for price_col in self.price_cols: if price_col in corr_matrix.columns: high_corr = corr_matrix[price_col][abs(corr_matrix[price_col]) > 0.7] high_corr = high_corr[high_corr.index != price_col] # 排除自身 if len(high_corr) > 0: report.append(f" - 与{price_col}强相关的字段:") for col, corr_val in high_corr.items(): report.append(f" * {col}:{corr_val:.2f}") # 保存报告 with open(report_path, 'w', encoding='utf-8') as f: f.write('\n'.join(report)) print(f"分析报告已保存至:{report_path}") # ------------------- 主执行流程 ------------------- if __name__ == "__main__": # 1. 配置文件路径(请根据实际文件位置调整) FILE_PATHS = { 'day_ahead': './data/电价_日前.xlsx', 'real_time': './data/电价_实时.xlsx', 'wind': './data/发电数据_风能.xlsx', 'solar': './data/发电数据_光伏.xlsx', 'load': './data/高压侧总负荷.xlsx', 'weather': './data/气象数据.xlsx' } # 2. 配置各文件的时间列名称(关键:替换为你的实际列名) TIME_COL_CONFIG = { 'day_ahead': '时间', 'real_time': '时间', 'wind': 'DATA_TIME', 'solar': 'DATA_TIME', 'load': 'DATA_TIME', 'weather': 'DATA_TIME' } # 3. 数据预处理流程 print("="*60) print("多源电力数据预处理流程启动") print("="*60) # 初始化处理器(传入时间列配置) processor = PowerDataProcessor(FILE_PATHS, time_col_config=TIME_COL_CONFIG) # 加载数据 processor.load_data() # 合并数据 processor.merge_data(join_type='inner') # 处理缺失值 processor.handle_missing_values(strategy='interpolate') # 处理异常值 processor.handle_outliers(method='IQR', threshold=1.5) # 特征工程 processor.feature_engineering() # 数据归一化 processor.normalize_data() # 保存预处理后的数据 processor.save_processed_data(save_path='processed_power_data.csv') # 3. 描述性分析流程 print("\n" + "="*60) print("多源电力数据描述性分析流程启动") print("="*60) # 初始化分析器 analyzer = PowerDataAnalyzer(processor.processed_data) # 单变量分析 analyzer.univariate_analysis() # 双变量分析 analyzer.bivariate_analysis() # 时间序列分析 analyzer.time_series_analysis() # 生成分析报告 analyzer.generate_analysis_report() print("\n" + "="*60) print("数据预处理与描述性分析全部完成!") print("="*60)根据代码用文字逐步描述数据预处理的步骤,包括数据重采样对齐,缺失值处理方法,异常值处理方法,单变量统计与可视化等
最新发布
12-06
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值