分析报告、时间序列图和图片导出
前置知识:多子图、标注及统计图 · 索引、切片与拷贝
本章目录
本章学习目标
- 区分每日、累计和基期归一化指标
- 保存适合报告的 PNG、SVG 和 PDF
- 给图表加入可复查的数据与方法说明
一、报告先确定读者需要的结论
一份分析报告的图应回答具体问题:近期收入变化、渠道结构差异或模型误差,不是展示掌握多少绘图函数。先写问题、指标定义、时间范围和数据来源,再选择图形。每日净额反映当天表现,累计净额反映截至该日的总量,二者不能共用含糊的 revenue 标签。报告结论需要区分观测、解释与假设,避免用漂亮图形代替论证。
假设三天净额十、十五、十二,累计为十、二十五、三十七。每天值上下波动,累计值一直增长不表示每天表现都在改善,因为累计量本身具有累加结构。两个指标可放在上下子图,共享时间轴、分别标明单位和口径;若放双纵轴必须特别谨慎,任意调整两个轴范围很容易制造看似相同的趋势,本课程默认采用分图。
二、时间序列必须先保证顺序与时点
Matplotlib 按给定顺序连接点,不会自动保证日期先后,因此先将记录按时间排序并保持数值对应。日期相同的多条记录应先明确是重复数据还是不同业务事件,再按口径聚合;不能为了让折线顺滑直接 drop duplicates。缺失日期可能表示无交易、无观测或系统问题,是否补零或留断点应由业务规则决定。
行情比较常把每条序列除以起始价格并乘一百,表示基期为一百的相对走势。必须使用共同可得日期,检查基期价格非零且有限;若一只股票基期缺失,不能悄悄用更晚日期而让读者以为起点相同。滚动均线要标明窗口按观测行还是日历天、最低有效数量和是否只用过去数据。绘图阶段不能掩盖前面时间对齐或信息泄漏错误。
三、导出格式影响使用方式
PNG 是位图,适合网页、邮件和常见报告;figsize 的英寸乘 dpi 决定大致像素尺寸,bbox_inches='tight' 可能改变最终外边界。SVG 和 PDF 通常保留矢量元素,缩放时线条文字更清晰,适合排版;但包含大量散点或网格时文件可能较大,必要时对密集图层 rasterize。不能只把低分辨率 PNG 文件扩展名改成 SVG 就获得矢量图。
文本在不同查看器中的字体可能不同。SVG 可把字体转成路径提高外观一致性,但文字搜索与编辑会受影响;PDF 的字体嵌入设置也需要检查,中文字体缺失时仍使用明确 fallback。透明背景适合某些排版,白背景适合跨软件通用展示;导出前根据用途确定,不要让深色界面上的白字在白底报告里不可见。本章示例使用白底和英文备用标签。
四、图表要有可复查的附件
报告图应对应一个小型结果表,保存日期、每日净额与累计净额,让读者能追溯线条上的数值。图注包含合成数据说明、纳入条件、单位和版本,真实项目则补充查询时间与数据范围。避免把个人路径、数据库凭据或内部标识自动放进图题。图题讲业务含义,日志记录运行环境,这两类信息各有位置。
配套脚本生成结果 CSV、PNG、SVG 和 PDF,还写一个 JSON 元信息文件记录 Python、NumPy、Matplotlib 版本。所有文件保存在课程 outputs 中,并可重复覆盖该示例自己的目标文件。构建结果表后用断言核对最终累计值等于每日总和;这个不变量比仅检查图片文件存在更能发现数据错误。图片还需视觉检查标题、刻度、图注与边界,不能用文件大小替代。
五、审查图表如何被误读
发布图表前检查:纵轴是否被不合理截断,时间间隔是否被当作等距类别,百分比是否混用百分数和小数,缺失是否被隐式连线,图例能否唯一对应系列,报告叙述是否超出数据支持范围。重复查看原始表与图表,从几个点手工核对,能比只欣赏整体趋势更有效地发现问题。
本章作业要求重新计算指标并输出多格式,不只更换颜色。对空数据应输出“无观测”说明或停止报告;对负净额应允许轴显示负值并解释退款集中发生的可能性,而不是裁掉负数。对长时间范围,应合理选择日期刻度,保留读者能看清的主要时间点。完成报告意味着数字、图形、注释和结论彼此一致,图表只承担它确实能说明的那部分证据。
完整示例与实际输出
from pathlib import Path
from datetime import date
import csv, json, sys
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib import font_manager
ROOT=Path(__file__).resolve().parents[1]; out=ROOT/'outputs'; out.mkdir(exist_ok=True)
names={f.name for f in font_manager.fontManager.ttflist}
font=next((f for f in ['Microsoft YaHei','SimHei','Noto Sans CJK SC'] if f in names),None)
if font:
plt.rcParams['font.sans-serif']=[font]; plt.rcParams['axes.unicode_minus']=False
else:
print('No CJK font: report uses English labels.')
raw=[('2026-01-03',12),('2026-01-01',10),('2026-01-02',15)]
rows=sorted(raw); dates=[date.fromisoformat(d) for d,_ in rows]
net=np.array([v for _,v in rows],dtype=float); cumulative=np.cumsum(net)
assert net.tolist()==[10,15,12] and cumulative.tolist()==[10,25,37]
assert np.isclose(cumulative[-1],net.sum())
table=out/'mpl-report-table.csv'
with table.open('w',encoding='utf-8',newline='') as f:
writer=csv.writer(f); writer.writerow(['date','daily_net','cumulative_net'])
writer.writerows((d.isoformat(),float(n),float(c)) for d,n,c in zip(dates,net,cumulative))
fig,axes=plt.subplots(2,1,figsize=(7,5),sharex=True,layout='constrained')
axes[0].plot(dates,net,marker='o'); axes[0].set(title='Daily net (paid/refunded, refund deducted)',ylabel='CNY / day')
axes[1].plot(dates,cumulative,marker='s'); axes[1].set(title='Cumulative net since 2026-01-01',ylabel='CNY',xlabel='Date')
for ax in axes:
ax.grid(alpha=.25)
fig.suptitle('Synthetic order report: 2026-01-01 to 2026-01-03')
for suffix in ['png','svg','pdf']:
p=out/f'mpl-report.{suffix}'
fig.savefig(p,dpi=160,bbox_inches='tight',facecolor='white')
assert p.is_file() and p.stat().st_size>1000
plt.close(fig)
meta={'synthetic':True,'python':sys.version.split()[0],'numpy':np.__version__,
'matplotlib':matplotlib.__version__,'total_net':float(net.sum()),'rows':len(rows)}
(out/'mpl-report-meta.json').write_text(json.dumps(meta,indent=2),encoding='utf-8')
print('daily',net.tolist(),'cumulative',cumulative.tolist(),'saved PNG/SVG/PDF/CSV/JSON')
实际运行通过CPU Python execution.
daily [10.0, 15.0, 12.0] cumulative [10.0, 25.0, 37.0] saved PNG/SVG/PDF/CSV/JSON

五道自测:先回答,再核对
01 · 累计收入持续增长能证明每日收入持续增长吗?
查看提示
累加结构。
查看参考答案
不能;每日可以下降,累计仍因正值不断增加。
02 · 乱序日期 plot 会自动按时间排序吗?
查看提示
绘图按输入顺序连线。
查看参考答案
不会,需要先排序成对记录。
03 · 基期价格缺失能悄悄换一天吗?
查看提示
比较是否同一基期?
查看参考答案
不能,应明确共同基期或说明不同起点,避免误导。
04 · PNG 和 SVG 的主要差别?
查看提示
像素与矢量。
查看参考答案
PNG 是位图,SVG 通常是矢量,可包含位图图层;用途和复杂度不同。
05 · 图片文件存在能代替视觉检查吗?
查看提示
标签、裁切和刻度。
查看参考答案
不能,还要打开检查排版与数据编码。
两项动手任务
任务 1 · 对每日净额 [10,-5,20] 计算累计并说明为何累计会下降。
提示
退款可能让当天净额为负。
完整参考解答与检查方式
累计为十、五、二十五;第二天负净额使累计下降,不应裁掉负值。
import numpy as np
net=np.array([10.,-5.,20.]); cumulative=np.cumsum(net)
assert cumulative.tolist()==[10,5,25]
assert np.isclose(cumulative[-1],net.sum())
print(net.tolist(),cumulative.tolist())任务 2 · 将两条价格 [10,12,11] 和 [100,110,120] 共同按第一天归一为100,导出双线图。
提示
先确认基期有限且非零。
完整参考解答与检查方式
两序列变成 [100,120,110] 与 [100,110,120],比较的是相对走势而非原价格。
from pathlib import Path
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
prices=np.array([[10.,100.],[12.,110.],[11.,120.]])
assert np.isfinite(prices[0]).all() and (prices[0]!=0).all()
indexed=prices/prices[0:1]*100
assert np.allclose(indexed,[[100,100],[120,110],[110,120]])
ROOT=Path(__file__).resolve().parents[1]; out=ROOT/'outputs'; out.mkdir(exist_ok=True)
fig,ax=plt.subplots()
for j,label in enumerate(['A','B']):
ax.plot([1,2,3],indexed[:,j],marker='o',label=label)
ax.set(title='Common day-1 base = 100',xlabel='Day',ylabel='Indexed price'); ax.legend()
p=out/'task-indexed.svg'; fig.savefig(p,bbox_inches='tight'); plt.close(fig)
assert p.is_file()
print(indexed.tolist())官方资料与掌握状态
完成两项任务后,按独立完成程度记录。状态会尽量保存在当前浏览器,可导出备份。
动手任务记录
掌握状态与复习
关掉资料独立完成后记 A;提示后完成记 B;需要重学记 C。
记录尽量保存在当前浏览器,建议定期导出备份。