248 lines
9.6 KiB
Python
248 lines
9.6 KiB
Python
import pandas as pd
|
||
import matplotlib.pyplot as plt
|
||
from datetime import datetime
|
||
import tkinter as tk
|
||
from tkinter import filedialog
|
||
import os
|
||
import matplotlib.dates as mdates
|
||
from jinja2 import Template
|
||
from matplotlib import font_manager, rcParams
|
||
|
||
|
||
class TemperatureDataAnalyzer:
|
||
def __init__(self):
|
||
self.data = None
|
||
self.file_path = None
|
||
self.timestamps = []
|
||
self.temperatures = []
|
||
self.statuses = []
|
||
self._configure_chinese_font() # 配置中文字体,修复中文字符缺失警告
|
||
|
||
def _configure_chinese_font(self):
|
||
"""
|
||
配置 Matplotlib 中文字体,避免中文字符缺失的警告。
|
||
会尝试常见的中文字体并设置 axes.unicode_minus 为 False。
|
||
"""
|
||
try:
|
||
# 常见中文字体候选(跨平台)
|
||
candidates = [
|
||
"Microsoft YaHei", "Microsoft YaHei UI", # Windows
|
||
"SimHei", "SimSun", # Windows(黑体/宋体)
|
||
"PingFang SC", "Heiti SC", # macOS
|
||
"Noto Sans CJK SC", "Source Han Sans SC", "WenQuanYi Micro Hei", # Linux
|
||
"Arial Unicode MS" # 覆盖广的 Unicode 字体
|
||
]
|
||
available = {f.name for f in font_manager.fontManager.ttflist}
|
||
for name in candidates:
|
||
if name in available:
|
||
rcParams["font.sans-serif"] = [name]
|
||
rcParams["axes.unicode_minus"] = False
|
||
# 可选:打印使用的字体名称
|
||
# print(f"使用中文字体: {name}")
|
||
return
|
||
# 如果没有找到常见中文字体,给出提示
|
||
rcParams["axes.unicode_minus"] = False
|
||
print("未检测到常见中文字体,图中中文可能无法正常显示。建议安装 'Noto Sans CJK SC' 或 'Microsoft YaHei'。")
|
||
except Exception as e:
|
||
print(f"中文字体配置失败: {e}")
|
||
|
||
def select_file(self):
|
||
"""手动选择CSV文件"""
|
||
root = tk.Tk()
|
||
root.withdraw() # 隐藏主窗口
|
||
|
||
file_types = [("CSV files", "*.csv"), ("All files", "*.*")]
|
||
self.file_path = filedialog.askopenfilename(title="选择温度数据CSV文件", filetypes=file_types)
|
||
|
||
if not self.file_path:
|
||
print("未选择文件,程序退出")
|
||
return False
|
||
return True
|
||
|
||
def load_and_process_data(self):
|
||
"""加载和处理数据"""
|
||
try:
|
||
# 读取CSV文件,无表头
|
||
self.data = pd.read_csv(self.file_path, header=None)
|
||
|
||
# 重命名列以便于引用
|
||
self.data.columns = ['timestamp', 'temperature', 'status']
|
||
|
||
# 转换时间戳格式(文本例如:10/29/2025 2:20:41 PM)
|
||
self.data['datetime'] = pd.to_datetime(self.data['timestamp'], format='%m/%d/%Y %I:%M:%S %p')
|
||
|
||
# 提取处理后的数据
|
||
self.timestamps = self.data['datetime']
|
||
self.temperatures = self.data['temperature']
|
||
self.statuses = self.data['status']
|
||
|
||
print(f"成功加载 {len(self.data)} 条记录")
|
||
return True
|
||
|
||
except Exception as e:
|
||
print(f"数据处理错误: {e}")
|
||
return False
|
||
|
||
def create_scatter_plots(self):
|
||
"""创建散点图"""
|
||
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10))
|
||
|
||
# 温度散点图
|
||
sc1 = ax1.scatter(self.timestamps, self.temperatures, c=self.temperatures,
|
||
cmap='coolwarm', alpha=0.7, s=20)
|
||
ax1.set_title('温度随时间变化趋势')
|
||
ax1.set_ylabel('温度 (°C)')
|
||
ax1.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M'))
|
||
ax1.grid(True, linestyle='--', alpha=0.7)
|
||
ax1.tick_params(axis='x', rotation=45)
|
||
plt.colorbar(sc1, ax=ax1, label="温度(°C)")
|
||
|
||
# 状态散点图
|
||
sc2 = ax2.scatter(self.timestamps, self.statuses, c=self.statuses,
|
||
cmap='viridis', alpha=0.7, s=20)
|
||
ax2.set_title('状态随时间变化')
|
||
ax2.set_xlabel('时间')
|
||
ax2.set_ylabel('状态值')
|
||
ax2.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M'))
|
||
ax2.grid(True, linestyle='--', alpha=0.7)
|
||
ax2.tick_params(axis='x', rotation=45)
|
||
plt.colorbar(sc2, ax=ax2, label="状态值")
|
||
|
||
plt.tight_layout()
|
||
return fig
|
||
|
||
def generate_statistics_report(self):
|
||
"""生成统计报告"""
|
||
stats = {
|
||
'total_records': len(self.temperatures),
|
||
'avg_temperature': round(self.temperatures.mean(), 2),
|
||
'max_temperature': round(self.temperatures.max(), 2),
|
||
'min_temperature': round(self.temperatures.min(), 2),
|
||
'std_deviation': round(self.temperatures.std(), 2),
|
||
'temp_range': round(self.temperatures.max() - self.temperatures.min(), 2),
|
||
'start_time': self.timestamps.iloc[0].strftime('%Y-%m-%d %H:%M:%S'),
|
||
'end_time': self.timestamps.iloc[-1].strftime('%Y-%m-%d %H:%M:%S'),
|
||
'duration_hours': round((self.timestamps.iloc[-1] - self.timestamps.iloc[0]).total_seconds() / 3600, 2)
|
||
}
|
||
|
||
# 状态分布统计
|
||
status_counts = self.statuses.value_counts().to_dict()
|
||
stats['status_distribution'] = status_counts
|
||
|
||
return stats
|
||
|
||
def save_fig_to_html(self, fig, output_path):
|
||
"""将图形保存为HTML"""
|
||
import io
|
||
import base64
|
||
|
||
# 将图形转换为base64编码
|
||
buf = io.BytesIO()
|
||
fig.savefig(buf, format='png', dpi=150, bbox_inches='tight')
|
||
buf.seek(0)
|
||
img_str = base64.b64encode(buf.read()).decode('utf-8')
|
||
buf.close()
|
||
|
||
# HTML模板(修复了多余的 '}')
|
||
html_template = """
|
||
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>温度数据分析报告</title>
|
||
<style>
|
||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||
.header { background-color: #f0f0f0; padding: 15px; border-radius: 5px; }
|
||
.section { margin-bottom: 30px; }
|
||
.stats-table { width: 100%; border-collapse: collapse; }
|
||
.stats-table th, .stats-table td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
||
.stats-table th { background-color: #f2f2f2; }
|
||
.image-container { text-align: center; margin: 20px 0; }
|
||
h1, h2 { color: #333; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="header">
|
||
<h1>温度数据分析报告</h1>
|
||
<p><strong>数据文件:</strong> {{ file_name }}</p>
|
||
<p><strong>生成时间:</strong> {{ generation_time }}</p>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h2>数据概览</h2>
|
||
<table class="stats-table">
|
||
<tr><th>项目</th><th>数值</th></tr>
|
||
{% for key, value in statistics.items() %}
|
||
{% if key != 'status_distribution' %}
|
||
<tr><td>{{ key.replace('_', ' ').title() }}</td><td>{{ value }}</td></tr>
|
||
{% endif %}
|
||
{% endfor %}
|
||
</table>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h2>状态分布</h2>
|
||
<table class="stats-table">
|
||
<tr><th>状态值</th><th>出现次数</th></tr>
|
||
{% for status, count in statistics.status_distribution.items() %}
|
||
<tr><td>{{ status }}</td><td>{{ count }}</td></tr>
|
||
{% endfor %}
|
||
</table>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h2>温度与状态时序图</h2>
|
||
<div class="image-container">
|
||
<img src="data:image/png;base64,{{ image_data }}" alt="温度与状态时序图">
|
||
</div>
|
||
</div>
|
||
|
||
</body>
|
||
</html>
|
||
"""
|
||
|
||
template = Template(html_template)
|
||
rendered_html = template.render(
|
||
file_name=self.file_path,
|
||
generation_time=datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||
statistics=self.generate_statistics_report(),
|
||
image_data=img_str
|
||
)
|
||
|
||
with open(output_path, 'w', encoding='utf-8') as f:
|
||
f.write(rendered_html)
|
||
|
||
def run_analysis(self):
|
||
"""运行完整分析流程"""
|
||
if not self.select_file():
|
||
return
|
||
|
||
if not self.load_and_process_data():
|
||
return
|
||
|
||
# 创建图形
|
||
fig = self.create_scatter_plots()
|
||
|
||
# 生成输出文件名(保存到选择的文件所在文件夹)
|
||
base_filename = os.path.splitext(os.path.basename(self.file_path))[0]
|
||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||
output_filename = f"{base_filename}_{timestamp}.html"
|
||
output_dir = os.path.dirname(self.file_path)
|
||
output_path = os.path.join(output_dir, output_filename)
|
||
|
||
# 保存HTML报告到同一文件夹
|
||
self.save_fig_to_html(fig, output_path)
|
||
|
||
print(f"分析完成!报告已保存至: {output_path}")
|
||
|
||
# 显示统计摘要
|
||
stats = self.generate_statistics_report()
|
||
print("\n=== 数据统计摘要 ===")
|
||
for key, value in stats.items():
|
||
if key != 'status_distribution':
|
||
print(f"{key.replace('_', ' ').title()}: {value}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
analyzer = TemperatureDataAnalyzer()
|
||
analyzer.run_analysis() |