很多人第一次做电力数据分析,会犯一个错误:
一拿到数据,就开始算平均值、画趋势图、做结论。
但真正有经验的数据分析人员,第一反应通常不是“赶紧分析”,而是先问:
这个数据能不能用?
比如你拿到一份现货电价表,里面有几千行数据。
你直接计算平均电价,结果发现某一天平均价特别高。
这时候你不能马上写结论:
该省现货电价显著上涨。
你要先检查:
这一列电价单位到底是元/MWh,还是元/kWh?
有没有缺失时段?
有没有重复记录?
有没有把日前价格和实时价格混在一起?
有没有把批发侧现货价格和用户侧零售电价混在一起?
有没有某一行录入错误,比如多写了一个 0?
有没有某些价格看起来极端,但其实是市场规则允许范围内的真实价格?
如果这些问题没处理,Python 算得越快,错误传播得也越快。
所以,本章我们先不追求模型。
不追求预测。
不追求策略。
先把最重要的基本功做好:
把一份原始电力数据,处理成一份能被分析的数据。
如果你是第一次接触电力数据清洗,这一章建议先收藏。
本章不用你做复杂模型,只要理解三件事:
第一,电力数据不能拿来就算;
第二,缺失值、重复值、单位和口径必须先检查;
第三,Python 可以把这些检查动作变成可复用流程。
文末我准备了第4章配套资料包,包括完整代码、模拟原始数据、清洗后结果、按日统计结果、字段字典和清洗检查清单。
本文约9000字,阅读需要30分钟。满满干货,建议先收藏起来,有空的时候慢慢看。
什么叫“数据处理”?
数据处理听起来很技术。
但简单来说,就是:
把乱的、不统一的、不完整的数据,整理成干净、统一、能计算、能追溯的数据。
比如你拿到一张电价表。
原始数据可能长这样:
这张表显然不能直接分析。
因为:
日期格式不统一。
价格里有文字。
价格里有看起来很极端的数值。
同一列可能混入了不该出现的内容。
经过处理以后,我们希望它变成这样:
也就是说,数据处理不是为了“把数据变好看”。
它是为了让后面的分析结果更可信。
这里要特别强调一点:
在电力市场数据处理中,看起来极端,不等于一定错误。
有些数据确实可能是录入错误。
有些数据是缺失或格式问题。
但也有些数据,可能是市场运行中真实出现的极端价格。
比如在某些市场规则下,可能出现负价格、高价格或接近价格上下限的价格。
所以,更稳妥的做法不是一上来就删除,而是:
先标记,再核验,再决定如何处理。
电力数据从哪里来?
这一节非常重要。
因为很多新手容易把“网上看到的数据”“公众号截图里的数据”“别人转发的 Excel”都当成可靠数据。
但电力交易数据有很强的规则属性和口径属性。
同样叫“电价”,可能完全不是一回事。
所以我们先建立一个原则:
数据来源越接近官方发布、交易平台、原始披露,可信度通常越高;
经过多次转载、截图、二次加工的数据,需要谨慎核验。
1. 国家能源局:适合看宏观统计和市场总体情况
国家能源局会发布全国电力工业统计、全社会用电量、电力市场交易电量等数据。
例如,国家能源局发布的数据显示,2025 年全社会用电量累计为 103682 亿千瓦时,同比增长 5.0%。
这类数据适合用来理解全国用电规模、产业用电结构和宏观趋势。
国家能源局也发布全国电力市场交易电量数据。
2025 年 1—12 月,全国累计完成电力市场交易电量 66394 亿千瓦时,占全社会用电量比重 64.0%;其中,中长期交易电量 63522 亿千瓦时,现货交易电量 2872 亿千瓦时。
这类数据适合做:
全国电力市场规模分析。
电力市场化交易趋势分析。
中长期、现货、绿电交易规模对比。
行业研究文章中的宏观背景引用。
但它不适合直接拿来做某个用户的电费测算。
因为宏观统计数据解决的是“全国总体情况”。
而用户电费测算需要的是:
具体省份、具体用户类别、具体电压等级、具体用电曲线、具体购电方式、具体价格口径、具体结算规则。
这两个场景不能混在一起。
2. 电力交易中心官网:适合查市场规则、公告、部分交易结果
如果你要研究某个省份的电力交易,优先应该看:
该省电力交易中心。
区域电力交易中心。
国家能源局派出机构网站。
省级发改委、能源局相关栏目。
因为电力交易规则具有很强的地方差异。
同样是现货市场,不同省份的运行阶段、出清机制、价格披露方式、数据下载格式,都可能不同。
《电力市场运行基本规则》明确,电力市场成员包括经营主体、电力市场运营机构和提供输配电服务的电网企业。
其中,电力市场运营机构包括电力交易机构和电力调度机构。
电力市场运营机构按职责负责电力市场交易、电力调度和交易结果执行,以及配套的准入注册、计量结算、信息披露等工作。
《电力市场运行基本规则》也明确,电力市场交易类型包括:
电能量交易、电力辅助服务交易、容量交易。
其中,电能量交易又按交易周期分为:
电力中长期交易、电力现货交易。
这说明,交易数据不是孤立存在的。
它一定和市场规则、交易类型、披露范围绑定在一起。
国家层面的基本规则还要求信息披露遵循“安全、及时、真实、准确、完整、易于使用”的原则,市场运营机构在确保信息安全基础上,按要求向经营主体和社会公众披露电力市场运行信息。
但这里要特别注意:
不是所有数据都对社会公众开放。
有些数据属于公众信息。
有些数据只对市场成员开放。
有些数据需要登录平台。
有些数据可能只能下载 PDF。
有些数据可能只能查看,不能批量下载。
3. 免费数据和靠谱数据不是一回事
很多免费数据能看,但不一定适合直接分析。
比如:
截图数据,不适合批量分析。
PDF 表格,可能需要人工整理。
网页表格,可能格式经常变。
公众号转载数据,必须回到原始来源核验。
商业平台展示数据,需要确认授权和口径。
对初学者来说,建议优先使用两类数据:
第一类:官方公开统计数据。
第二类:自己构造的教学用模拟数据。
前者适合做行业研究。
后者适合练习 Python。
本章实操就使用教学用模拟数据。
因为我们当前的重点不是判断某个省份真实价格,而是学会一套通用的数据处理方法。
Pandas 进阶:电力数据清洗
在 Python 里,处理表格数据最常用的工具仍然是 Pandas。
第3章我们已经用 Pandas 读取过电价数据。
本章开始,我们要做更接近真实工作的清洗。
常见清洗动作包括:
1. 统一列名
真实数据里,列名可能是中文:
省份、日期、时段、现货价格
也可能是英文:
province、date、time、price
也可能不同文件写法不一致:
价格、市场价格、日前价格、实时价格、节点电价、统一出清价格
为了方便代码复用,最好统一成固定列名。
比如:
provincedatetimeprice_yuan_mwh
这样后面的代码就不用反复改。
但真实业务中还应该多保留几个口径字段,例如:
market_typeprice_typedata_sourcepublish_date
比如:
market_type 可以记录是中长期、现货、绿电还是辅助服务。
price_type 可以记录是日前价格、实时价格、结算价格还是零售价格。
data_source 可以记录数据来源。
publish_date 可以记录发布时间。
这些字段看起来繁琐,但非常有用。
因为电力数据分析最怕的不是不会算,而是算错口径。
2. 处理缺失值
缺失值就是数据为空。
比如:
某个时段没有价格。
某一行日期为空。
某一行省份为空。
某一行价格写成了“缺失”。
缺失值不能直接忽略。
你要先判断它影响什么。
如果只是某一个时段价格缺失,可以先标记出来。
如果整行关键字段都缺失,可能需要删除。
如果做正式分析,还要回到原始来源核验。
小白阶段先记住一句话:
不要看到空值就随便填 0。
因为电价缺失不等于电价为 0。
负荷缺失也不等于用户没用电。
如果你把缺失价格填成 0,平均价格会被拉低。
后面的价差分析、收益测算、趋势判断都可能失真。
3. 处理疑似异常值
疑似异常值,就是看起来不太合理、需要进一步核验的数据。
比如:
价格字段出现文字。
电价多了一个 0。
单位搞错。
日期错位。
重复粘贴。
价格远远超出常见范围。
但电力市场里有一个特殊点:
有些看起来异常的价格,未必一定是错误。
比如在某些市场规则下,可能出现负价格、极端高价或接近限价的价格。
所以,正式业务中不能只凭感觉删除。
更稳妥的做法是:
先标记。
再核验。
再决定是否删除、修正、保留或单独说明。
本章代码会用一个教学规则:
价格小于 0 或大于 1500 元/MWh,先标记为“疑似异常/待核验”。
*注意:这只是为了演示 Pandas 如何筛选和标记数据。不代表任何省份真实现货市场价格上下限。真实业务中,负价格或高价格未必一定是错误值,必须结合当地市场规则、价格上下限、日前/实时口径、结算口径和原始披露文件进行核验。
Pandas 进阶:筛选与分组
清洗完数据后,我们通常要做筛选。
比如:
筛选某个省份。
筛选某一天。
筛选某个时间段。
筛选高价样本。
筛选疑似异常数据。
Pandas 很适合做这些事情。
比如:
df[df["province"] == "山西"]
这表示筛选省份为“山西”的数据。
再比如:
df[df["price_yuan_mwh"] > 500]
这表示筛选电价高于 500 元/MWh 的数据。
分组则适合做统计。
比如:
按日期统计样本平均电价。
按月份统计样本最高电价。
按省份统计价格波动。
本章实操会做一个最基础的分组:
按日期统计清洗后样本平均电价、样本最高电价、样本最低电价和样本价差。
这里要特别注意:
如果某一天原本应该有 24 个、48 个或 96 个价格点,但你的数据只有一部分,那么你算出来的平均值只能叫“样本平均值”。
不能直接叫“完整日均价”。
所以本章代码里会保留一个字段:
record_count
它用来提示当天实际参与统计的数据条数。
同时,代码里还会加入两个教学字段:
expected_record_countis_record_count_complete
expected_record_count 表示教学假设下当天应有的记录数。
is_record_count_complete 表示样本记录数是否完整。
这两个字段可以帮助你更直观地理解:
只有记录数完整,统计结果才更有解释基础;
记录数不完整时,只能谨慎称为样本统计结果。
电力数据的时间处理:为什么日期格式特别重要?
电力交易数据和普通表格最大的区别之一,是它非常依赖时间。
比如:
现货价格按小时或更短时段变化。
负荷数据可能是 15 分钟一个点。
分时电价有尖峰、峰、平、谷。
中长期交易有年、月、周、多日等周期。
如果时间处理错了,后面分析一定错。
比如:
你把:
2026/01/02 00:00
识别成文本,而不是时间。
那 Python 就没法按日期排序、按月份统计、按小时筛选。
所以本章会把日期和时段合并成一个标准时间字段:
datetime
比如:
2026-01-01 08:00
有了这个字段,后面才能继续做:
按日统计。
按月统计。
按小时筛选。
画时间序列图。
计算峰谷价差。
检查缺失时段。
示例数据说明
本章实操使用一份教学用模拟“省级现货电价数据”。
文件名:
spot_price_raw.csv
字段如下:
注意:
本章使用的是教学用模拟数据。
它不是任何省份真实现货电价。
不代表真实出清价格、结算价格或交易规则。
这里使用“元/MWh”,是因为现货市场价格数据常见单位之一是元/兆瓦时。
如果要换算成元/kWh,可以用:
元/kWh = 元/MWh ÷ 1000
比如:
500 元/MWh = 0.5 元/kWh
单位千万不能搞错。
元/MWh 和元/kWh 相差 1000 倍。
如果单位错了,后面所有电费计算、收益测算和价格比较都会错。
为了减少路径报错,建议把本章代码放在一个简单英文路径中,例如:
D:\python_power_trade\chapter04
本章代码会自动在这个文件夹中创建和读取:
spot_price_raw.csv
处理完成后,也会把结果文件导出到同一个文件夹中。
Python 完整代码
本章代码会自动创建一份带有“缺失值、疑似异常值、重复值”的模拟数据,然后完成读取、清洗、筛选、分组和导出。
先安装依赖:
python -m pip install pandas
如果 Windows 上 python 命令不生效,可以尝试:
py -m pip install pandas
如果下载较慢,可以使用国内镜像源:
python -m pip install pandas -i https://pypi.tuna.tsinghua.edu.cn/simple
或者:
py -m pip install pandas -i https://pypi.tuna.tsinghua.edu.cn/simple
新建文件:
chapter04_clean_spot_price.py
下面这段代码不要求你一次性全部看懂。
第4章的目标只有一个:
把一份带有缺失值、重复值、异常值和单位问题的模拟电价数据,清洗成可以继续分析的数据。
如果你不想手动整理文件,可以在文末私信关键词领取本章资料包,里面已经准备好代码、原始数据、清洗结果和运行说明。
复制下面代码运行:
# -*- coding: utf-8 -*-# 第4章实操:电力交易数据处理——从原始现货电价数据到可分析数据# 说明:# 1. 本案例使用教学用模拟数据,不代表任何省份真实现货电价或交易规则# 2. 本案例仅用于 Python 数据处理教学,不构成交易建议、投资建议或收益承诺from pathlib import Pathimport pandas as pdprint("=== Python电力交易第4章:电力交易数据清洗与筛选 ===")# 1. 找到当前 Python 文件所在的文件夹current_folder = Path(__file__).resolve().parent# 2. 设置输入和输出文件路径raw_file = current_folder / "spot_price_raw.csv"clean_output_file = current_folder / "spot_price_cleaned.csv"summary_output_file = current_folder / "spot_price_daily_summary.csv"# 3. 如果文件不存在,自动创建一份教学用模拟数据ifnot raw_file.exists(): sample_data = [ ["山西", "2026/01/01", "00:00", "320"], ["山西", "2026/01/01", "01:00", "310"], ["山西", "2026/01/01", "02:00", ""], # 缺失价格 ["山西", "2026/01/01", "03:00", "305"], ["山西", "2026/01/01", "04:00", "-50"], # 教学用疑似异常值 ["山西", "2026/01/01", "05:00", "299"], ["山西", "2026/01/01", "06:00", "420"], ["山西", "2026/01/01", "07:00", "580"], ["山西", "2026/01/01", "08:00", "760"], ["山西", "2026/01/01", "09:00", "99999"], # 教学用疑似异常值 ["山西", "2026/01/01", "10:00", "650"], ["山西", "2026/01/01", "10:00", "650"], # 重复记录 ["山西", "2026/01/02", "00:00", "280"], ["山西", "2026/01/02", "01:00", "275"], ["山西", "2026/01/02", "02:00", "缺失"], # 非数字 ["山西", "2026/01/02", "03:00", "260"], ["山西", "2026/01/02", "04:00", "300"], ["山西", "2026/01/02", "05:00", "360"], ["山东", "2026/01/01", "00:00", "350"], ["山东", "2026/01/01", "01:00", "340"], ["山东", "2026/01/01", "02:00", "330"], ["山东", "2026/01/01", "03:00", "390"], ] sample_df = pd.DataFrame( sample_data, columns=["province", "date", "time", "price_yuan_mwh"] ) sample_df.to_csv(raw_file, index=False, encoding="utf-8-sig") print(f"\n未发现 {raw_file.name},已自动创建教学用模拟数据。")else: print(f"\n已发现 {raw_file.name},将直接读取该文件。")# 4. 读取原始数据df = pd.read_csv(raw_file, encoding="utf-8-sig")# 5. 检查必要字段是否存在required_columns = ["province", "date", "time", "price_yuan_mwh"]for column in required_columns:if column notin df.columns:raise ValueError(f"CSV 文件中缺少必要字段:{column}")print("\n【1】原始数据预览:")print(df.head(10))print("\n原始数据行数:", len(df))# 6. 去除完全重复的记录df = df.drop_duplicates()print("\n【2】删除重复记录后的数据行数:", len(df))# 7. 把价格列转换为数字# errors='coerce' 的意思是:如果遇到无法转换成数字的内容,就变成空值 NaNdf["price_yuan_mwh"] = pd.to_numeric(df["price_yuan_mwh"], errors="coerce")print("\n【3】价格列转换为数字后的数据:")print(df.head(10))# 8. 合并 date 和 time,生成 datetime 字段df["datetime"] = pd.to_datetime( df["date"].astype(str) + " " + df["time"].astype(str), errors="coerce")# 9. 从 datetime 中提取日期、小时、月份df["trade_date"] = df["datetime"].dt.datedf["hour"] = df["datetime"].dt.hourdf["month"] = df["datetime"].dt.to_period("M")# 10. 标记缺失价格df["is_missing_price"] = df["price_yuan_mwh"].isna()# 11. 标记疑似异常价格# 注意:这里只是教学用筛选规则,不代表任何省份真实价格上下限# 在真实业务中,负价格或高价格未必一定是错误值,应回到原始披露和市场规则中核验df["is_suspicious_price"] = ( (df["price_yuan_mwh"] < 0) | (df["price_yuan_mwh"] > 1500))print("\n【4】缺失价格记录:")print(df[df["is_missing_price"] == True])print("\n【5】疑似异常价格记录:")print(df[df["is_suspicious_price"] == True])# 12. 生成清洗后的数据# 本案例中,为了演示后续统计流程,临时剔除价格缺失、疑似异常价格和时间无法识别的数据# 真实业务中不建议直接删除问题数据,应先标记、再核验、保留处理记录clean_df = df[ (df["is_missing_price"] == False) & (df["is_suspicious_price"] == False) & (df["datetime"].notna())].copy()print("\n【6】清洗后的样本数据预览:")print(clean_df.head(10))print("\n清洗后样本数据行数:", len(clean_df))# 13. 筛选某个省份的数据target_province = "山西"province_df = clean_df[clean_df["province"] == target_province].copy()print(f"\n【7】筛选 {target_province} 数据:")print(province_df.head(10))# 14. 筛选教学阈值下的高价样本# 这里设置教学用阈值:价格高于 600 元/MWh,标记为高价样本# 真实业务中不能把 600 元/MWh 直接作为通用高价判断标准high_price_df = province_df[province_df["price_yuan_mwh"] > 600].copy()print("\n【8】教学阈值下的高价样本:")print(high_price_df[["province", "datetime", "price_yuan_mwh"]])# 15. 按日期分组统计# 注意:这里统计的是清洗后样本数据,不代表完整自然日全部时段的真实日均价格daily_summary = province_df.groupby("trade_date").agg( sample_avg_price=("price_yuan_mwh", "mean"), sample_max_price=("price_yuan_mwh", "max"), sample_min_price=("price_yuan_mwh", "min"), record_count=("price_yuan_mwh", "count")).reset_index()# 16. 新增样本价差字段daily_summary["sample_price_spread"] = ( daily_summary["sample_max_price"] - daily_summary["sample_min_price"])# 17. 新增记录数完整性检查字段# 这里假设教学场景下,一天应有 24 个小时级价格点# 真实业务中应根据当地市场数据颗粒度改成 24、48、96 或其他值expected_record_count = 24daily_summary["expected_record_count"] = expected_record_countdaily_summary["is_record_count_complete"] = ( daily_summary["record_count"] == daily_summary["expected_record_count"])print("\n【9】按日样本统计结果:")print(daily_summary)print("\n提示:record_count 表示当天参与统计的样本记录数。")print("expected_record_count 表示教学假设下当天应有的记录数。")print("is_record_count_complete 用于提示样本记录数是否完整。")print("真实业务中,应根据当地市场数据颗粒度设置应有记录数,例如 24、48 或 96。")# 18. 新增价格单位换算字段clean_df["price_yuan_kwh"] = clean_df["price_yuan_mwh"] / 1000print("\n【10】价格单位换算示例:")print(clean_df[["province", "datetime", "price_yuan_mwh", "price_yuan_kwh"]].head())# 19. 导出清洗结果和按日统计结果clean_df.to_csv(clean_output_file, index=False, encoding="utf-8-sig")daily_summary.to_csv(summary_output_file, index=False, encoding="utf-8-sig")print("\n处理完成!已生成两个文件:")print(clean_output_file.name)print(summary_output_file.name)print("\n再次提醒:本章疑似异常规则和高价阈值仅用于教学。")print("真实业务中必须根据当地最新交易规则、价格上下限、数据口径和原始披露文件重新核验。")
代码解释
1. 文件编码声明
# -*- coding: utf-8 -*-
这一行表示代码文件使用 UTF-8 编码。
现在大多数新版编辑器默认支持 UTF-8,所以这行通常不是必须的。
但对零基础读者来说,保留这一行可以减少少数电脑上中文乱码的概率。
2. 导入工具库
from pathlib import Pathimport pandas as pd
这里导入了两个工具。
pathlib 是 Python 自带的路径处理工具,用来帮助程序找到文件位置。
pandas 用来读取和处理表格数据。
其中:
import pandas as pd
意思是把 pandas 简写成 pd。
这是数据分析中非常常见的写法。
3. 找到当前代码文件所在文件夹
current_folder = Path(__file__).resolve().parent
这行代码的意思是:
找到当前这个 Python 文件所在的文件夹。
为什么要这样写?
因为很多新手会遇到一个问题:
明明 CSV 文件和 Python 文件放在同一个文件夹里,但运行时还是提示找不到文件。
原因通常是:
Python 当前运行位置,和代码文件实际所在位置不一致。
使用这一行代码后,程序会从“当前 Python 文件所在文件夹”去生成、读取和导出文件,路径更稳定。
4. 设置输入和输出文件路径
raw_file = current_folder / "spot_price_raw.csv"clean_output_file = current_folder / "spot_price_cleaned.csv"summary_output_file = current_folder / "spot_price_daily_summary.csv"
这三行代码表示:
spot_price_raw.csv 是原始模拟数据文件。
spot_price_cleaned.csv 是清洗后的样本明细数据。
spot_price_daily_summary.csv 是按日样本统计结果。
它们都会放在当前 Python 文件所在文件夹中。
也就是说,代码运行后,你可以在同一个文件夹里看到:
chapter04_clean_spot_price.pyspot_price_raw.csvspot_price_cleaned.csvspot_price_daily_summary.csv
这样对新手更友好,不容易找不到文件。
5. 自动创建模拟数据
ifnot raw_file.exists():
这行代码的意思是:
如果当前文件夹里没有 spot_price_raw.csv,就自动创建一份模拟数据。
这样做是为了降低新手运行门槛。
你不需要先手动准备文件,只要复制代码,就能跑通。
*但要注意:自动创建的数据是教学用模拟数据,不是山西、山东或任何省份的真实现货价格。
6. 读取 CSV 文件
df = pd.read_csv(raw_file, encoding="utf-8-sig")
这行代码用 Pandas 读取 CSV 文件。
encoding="utf-8-sig" 是为了减少中文乱码问题。
读入之后,df 就是一张 Python 表格。
7. 检查必要字段是否存在
required_columns = ["province", "date", "time", "price_yuan_mwh"]for column in required_columns:if column notin df.columns:raise ValueError(f"CSV 文件中缺少必要字段:{column}")
这段代码的意思是:
检查 CSV 文件里是否有以下 4 个必要字段:
provincedatetimeprice_yuan_mwh
如果缺少某一列,程序会主动报错,并提示缺少哪个字段。
这样比直接出现 KeyError 更适合新手理解。
比如你把表头写成:
price
程序就会提示缺少:
price_yuan_mwh
小白阶段建议先不要改字段名,先完整跑通示例代码。
8. 删除重复记录
df = df.drop_duplicates()
这行代码会删除完全相同的重复行。
在电力数据中,重复记录很常见。
可能是人工复制导致的。
也可能是多次导出合并导致的。
如果不删除重复记录,平均价格、记录数量、分组统计都可能被影响。
9. 把价格列转换为数字
df["price_yuan_mwh"] = pd.to_numeric(df["price_yuan_mwh"], errors="coerce")
这行代码很重要。
原始数据里,价格列可能混入:
空白缺失-文字
这些内容不能直接参与计算。
errors="coerce" 的意思是:
如果某个值无法转换成数字,就把它变成空值。
这样后面就能统一处理缺失价格。
10. 合并日期和时段
df["datetime"] = pd.to_datetime( df["date"].astype(str) + " " + df["time"].astype(str), errors="coerce")
这段代码把 date 和 time 合并成一个完整时间。
比如:
2026/01/01 + 08:00
变成:
2026-01-01 08:00:00
有了 datetime 字段,后面才方便按小时、按日、按月统计。
11. 提取日期、小时、月份
df["trade_date"] = df["datetime"].dt.datedf["hour"] = df["datetime"].dt.hourdf["month"] = df["datetime"].dt.to_period("M")
这三行分别提取:
交易日期。
小时。
月份。
这在电力交易数据处理中非常常用。
例如:
按日统计样本平均电价。
按小时看高价样本分布。
按月比较价格波动。
12. 标记缺失价格
df["is_missing_price"] = df["price_yuan_mwh"].isna()
这行代码会新增一列:
is_missing_price
如果价格为空,就是 True。
如果价格不为空,就是 False。
这样做的好处是:
你不会悄悄把问题数据删掉。
而是先明确看到哪些数据有问题。
13. 标记疑似异常价格
df["is_suspicious_price"] = ( (df["price_yuan_mwh"] < 0) | (df["price_yuan_mwh"] > 1500))
这段代码会标记教学口径下的疑似异常价格。
这里用了两个条件:
价格小于 0。
价格大于 1500。
只要满足其中一个条件,就标记为疑似异常。
再次强调:
这只是教学规则。
真实业务中,负价格或高价格不一定是错误数据。
是否需要删除、修正或保留,必须结合:
省份规则、价格上下限、交易品种、日前或实时、出清价格或结算价格、数据单位、市场运行阶段、原始披露文件等。
不能照搬本章规则。
14. 清洗数据
clean_df = df[ (df["is_missing_price"] == False) & (df["is_suspicious_price"] == False) & (df["datetime"].notna())].copy()
这段代码保留三类数据:
价格不缺失。
没有被教学规则标记为疑似异常。
时间可以识别。
最终得到一份相对干净的数据:
clean_df
但要注意:
本章是为了教学演示,临时剔除了这些问题数据。
真实业务中不建议直接删除问题数据。
而是:先标记、再核验、保留处理记录。
最后再决定是否剔除、修正或单独说明。
15. 筛选某个省份
target_province = "山西"province_df = clean_df[clean_df["province"] == target_province].copy()
这段代码筛选出山西的数据。
如果你想看山东,只需要改成:
target_province = "山东"
这就是代码模板的价值。
不用重写整套代码,只改一个参数即可。
16. 筛选教学阈值下的高价样本
high_price_df = province_df[province_df["price_yuan_mwh"] > 600].copy()
这行代码筛选价格高于 600 元/MWh 的样本。
但这里的 600 只是教学阈值。
真实业务中不能简单认为:
高于 600 就一定高。
低于 600 就一定低。
你要看:
省份、日期、市场类型、价格单位、历史分布、交易规则、用户或项目的业务场景。
更不能写成:
高于 600 元/MWh 就适合储能套利。
这是典型的误导性表达。
17. 按日期分组统计
daily_summary = province_df.groupby("trade_date").agg( sample_avg_price=("price_yuan_mwh", "mean"), sample_max_price=("price_yuan_mwh", "max"), sample_min_price=("price_yuan_mwh", "min"), record_count=("price_yuan_mwh", "count")).reset_index()
这段代码按日期分组,并计算:
清洗后样本平均电价。
清洗后样本最高电价。
清洗后样本最低电价。
参与统计的样本记录数。
这里特别使用了 sample_ 前缀。
这是为了提醒你:
它统计的是清洗后的样本数据。
不是完整自然日全部时段的真实市场统计结果。
如果某一天缺少多个时段,或者部分时段被标记后剔除,那么当天的平均价、最高价、最低价,都只能作为样本统计结果。
18. 计算样本价差
daily_summary["sample_price_spread"] = ( daily_summary["sample_max_price"] - daily_summary["sample_min_price"])
这行代码计算:
样本价差 = 样本最高价 - 样本最低价
价差是电力市场分析里很常见的指标。
但注意:
看到价差大,不等于一定能套利。
是否能套利,还要考虑:
储能效率、充放电约束、容量、电量、循环次数、交易规则、辅助服务、损耗、成本、税费、结算方式、偏差考核、市场准入条件等。
所以,本章只讲数据处理,不做收益判断。
19. 记录数完整性检查
代码中有一个字段:
record_count
它表示当天参与统计的样本记录数。
这个字段不是装饰字段。
它非常重要。
本章代码里还新增了两个字段:
expected_record_countis_record_count_complete
其中:
expected_record_count 表示教学假设下当天应有的记录数。
is_record_count_complete 表示当天样本记录数是否完整。
代码如下:
expected_record_count = 24daily_summary["expected_record_count"] = expected_record_countdaily_summary["is_record_count_complete"] = ( daily_summary["record_count"] == daily_summary["expected_record_count"])
这里假设教学场景下,一天应有 24 个小时级价格点。
真实业务中,应根据当地市场数据颗粒度调整。
比如:
某市场一天是 24 个小时点。
某市场一天是 48 个半小时点。
某市场一天是 96 个 15 分钟点。
如果某个市场一天理论上应该有 24 个价格点。
但你清洗后只剩下 18 个价格点。
那这一天的平均价就不能直接当成完整日均价。
再比如某个市场一天理论上应该有 96 个价格点。
但你只拿到了 30 个点。
这时你更不能直接写结论。
正式业务中,你至少要检查:
应有记录数是多少。
实际记录数是多少。
缺失了哪些时段。
缺失原因是什么。
是否影响统计结果。
是否需要补充说明。
20. 价格单位换算
代码中新增了一列:
clean_df["price_yuan_kwh"] = clean_df["price_yuan_mwh"] / 1000
这行代码的意思是:
把元/MWh 换算成元/kWh。
换算关系是:
1 MWh = 1000 kWh
所以:
500 元/MWh = 0.5 元/kWh
这一步非常重要。
因为很多用户侧电价、电费计算,更习惯使用元/kWh。
而很多现货市场价格数据,常见单位是元/MWh。
如果单位不统一,后面的分析会全部错位。
运行结果说明
运行代码后,你会看到几类输出。
第一类是原始数据预览。
你会看到数据里故意设计了:
缺失价格。
非数字价格。
疑似异常价格。
重复记录。
第二类是缺失价格记录。
比如:
price_yuan_mwh 为空price_yuan_mwh 无法转换成数字
第三类是疑似异常价格记录。
比如:
-5099999
*注意:它们只是被本章教学规则标记为疑似异常,不代表真实业务中一定应该删除。
第四类是清洗后的样本数据。
这些数据已经临时剔除了重复记录、缺失价格、疑似异常价格和无法识别时间的记录。
第五类是高价样本。
你会看到价格高于 600 元/MWh 的样本记录。
*600 元/MWh 只是教学阈值,不是真实市场高价判断标准
第六类是按日样本统计结果。
你会看到类似字段:
trade_datesample_avg_pricesample_max_pricesample_min_pricerecord_countsample_price_spreadexpected_record_countis_record_count_complete
其中:
record_count 表示当天实际参与统计的样本记录数。
expected_record_count 表示教学假设下当天应有记录数。
is_record_count_complete 表示样本记录数是否完整。
*本章输出的 daily_summary 是基于清洗后样本数据得到的统计结果。如果某一天原始数据缺少多个时段,或者部分时段被标记为疑似异常并剔除,那么当天的平均价、最高价、最低价和价差只能作为教学样本结果,不能直接代表完整自然日的真实市场价格水平。在正式业务中,应先检查当天应有记录数和实际记录数是否一致。例如某市场一天应有 24 个、48 个或 96 个价格点,就要检查数据是否完整。
最后,代码会生成两个文件:
spot_price_cleaned.csvspot_price_daily_summary.csv
第一个是清洗后的样本明细数据。
第二个是按日样本统计结果。
这两个文件会生成在当前 Python 文件所在文件夹中。
这就完成了一个基础数据处理闭环:
原始数据进来,清洗结果出去,统计结果出去。
本章小练习
练习 1:修改目标省
把代码中的:
target_province = "山西"
改成:
target_province = "山东"
重新运行代码。
观察筛选结果和按日样本统计结果是否变化。
练习 2:修改高价阈值
把代码中的:
price_yuan_mwh > 600
改成:
price_yuan_mwh > 500
重新运行代码。
观察高价样本数量是否变多。
练习 3:新增一条疑似异常数据
在 spot_price_raw.csv 里新增一行:
山西,2026/01/02,06:00,88888
重新运行代码。
看看它是否会被标记为疑似异常值。
练习 4:理解价格单位换算
观察代码中这一行:
clean_df["price_yuan_kwh"] = clean_df["price_yuan_mwh"] / 1000
然后打印:
print(clean_df[["province", "datetime", "price_yuan_mwh", "price_yuan_kwh"]].head())
理解“元/MWh”和“元/kWh”的关系。
练习 5:观察记录数
查看 daily_summary 里的:
record_countexpected_record_countis_record_count_complete
思考一个问题:
如果某一天理论上应该有 24 个价格点,但你的 record_count 只有 8,这一天的样本平均电价还能不能直接代表完整日均价?
答案是:不能。
你只能说:这是基于现有样本计算得到的样本平均值。
本章小结
本章我们没有急着做模型,也没有直接画图分析价格趋势。
而是先完成了一件更基础、更重要的事:
把原始电力交易数据处理成相对可用的数据。
你至少需要记住以下几点:
第一,电力数据分析的第一步不是画图,而是检查数据能不能用。
第二,数据来源、发布时间、适用省份、交易品种、价格单位都必须记录清楚。
第三,缺失价格不等于 0。
第四,元/MWh 和元/kWh 相差 1000 倍,不能混用。
第五,现货日前价格、实时价格、中长期价格、零售价格、分时电价不能混在一起分析。
第六,负价格或极端价格不一定是错误数据,应先标记、再核验。
第七,清洗后样本平均值不等于完整自然日真实日均价。
第八,record_count 可以帮助你判断数据是否完整。
第九,Python 是工具,数据口径和业务规则才是分析可信度的基础。
这章看起来是在讲 Pandas。
但真正想让你建立的是一种工作习惯:
先查口径,再清洗数据;
先保留问题,再做判断;
先验证完整性,再输出结论。
这才是电力交易数据分析最重要的基本功。
资料领取
如果你已经跑通本章代码,欢迎在评论区回复:
清洗模板已跑通
如果你遇到问题,也可以直接留言报错信息,比如:
ValueError
FileNotFoundError
ModuleNotFoundError
日期转换失败
中文乱码
后续我会整理一篇:
《电力交易数据清洗最常见的10个坑》
如果你想直接领取本章配套资料,可以私信公众号关键词:
电力Python04
领取《Python电力交易》第4章数据清洗资料包,包括:
1.chapter04_clean_spot_price.py
2.spot_price_raw.csv 模拟原始数据
3.spot_price_cleaned.csv 清洗后样本数据
4.spot_price_daily_summary.csv 按日样本统计结果
5.第4章运行说明
6.电力交易常见数据字段字典
7.电价数据清洗检查清单
建议使用顺序:
先看运行说明 → 跑通完整代码 → 查看清洗前后数据变化 → 对照字段字典理解每一列含义 → 用清洗检查清单复盘自己的数据。
也欢迎你在评论区回答一个问题:
你平时拿到的电力数据,最常见的问题是什么?
A. 缺失值太多
B. 日期格式混乱
C. 表头不统一
D. 单位不清楚
E. PDF 复制困难
F. 不知道数据口径
G. 不知道哪些数据能删、哪些数据不能删