AI编程(六十二):折线图 — 把趋势讲成故事

上一篇你画出了人生第一张图,今天专攻所有图里最”会讲故事”的一种:折线图。只引入一个新概念——趋势展示:让图不只画出数字,还能讲清”涨了还是跌了、拐点在哪、稳不稳”。多线对比、真实日期轴、平滑趋势、关键点标注,一篇全配齐。从此你的图不再是”一堆点”,而是一句话就能讲完的故事。

一、真实场景:同样五个数,讲出来的故事完全不同

接着上一篇的账本说话。你导出了近半年的月支出:4210、3980、4520、4105、4503(元)。给家里人汇报,两种讲法——

讲法一:”六月4210,七月3980,八月4520……”

讲法二:”这半年支出总体在爬坡:七月最低,八月冲了个尖,九月回落,十月又涨回去了——整体比半年前多花了 300 块左右。”

讲法二就是趋势:方向(总体在涨)、拐点(八月那个尖)、波动(忽上忽下不稳)。同样五个数,讲法二让人听完就知道”该不该收紧开支”,讲法一听完只记得”一堆数”。

折线图就是把讲法二画出来。它跟上一篇的图看起来差不多,但讲究完全不同——上一篇的目标是”画出来”,今天的目标是“讲清楚”:线条的走势就是故事的主线,你的每个设计选择(几条线、什么颜色、标不标关键点)都在帮观众听懂这个故事。

为什么趋势这么值钱?因为人对”变化”极其敏感,对”数值”极其迟钝。”4503.5″你没感觉,”又涨回去了”你马上有反应。汇报、复盘、监控、对账——凡是涉及”随时间变化”的场合,折线图都是第一选择。今天学完,你上一篇埋的三个伏笔(日期轴骗人、双线讲究、annotate)全部兑现。

二、折线图的”世界观”:它只讲一种故事

先立规矩:折线图只适合”随时间(或有序量)变化”的数据。横轴必须有序——时间、次数、温度、进度,都行;”苹果、香蕉、橘子”这种无序分类不行(那是下一篇柱状图的地盘)。判断口诀就一句:横轴能说”之前、之后”,才配用折线图。

一个趋势故事只有三个要素,画图就是把这三要素画醒目:

  1. 方向:总体是涨是跌?——线的”大势”;
  2. 拐点:哪里开始转折?——线上”拐弯”的地方;
  3. 波动:稳不稳?——线上下”抖”的程度。

所以”好的折线图”不是画得漂亮,而是观众三秒钟能答出这三个问题。带着这个目标进入动手环节——每一步都在为三个要素服务。

三、动手:把趋势画成故事

第 1 步:多线对比 —— 故事需要”对手戏”

一条线讲”自己的故事”,两条线讲”对比的故事”。最常见的对比就是上一篇开过眼界的收支对比,今天把它做精:

# compare_trend.py — 收支双线对比:谁涨谁跌一目了然
import matplotlib.pyplot as plt

# —— 中文字体(上一篇的坑点疫苗2,直接复用)——
plt.rcParams["font.sans-serif"] = ["Noto Sans CJK SC", "SimHei", "WenQuanYi Zen Hei"]
plt.rcParams["axes.unicode_minus"] = False

months = ["6月", "7月", "8月", "9月", "10月", "11月"]
spending = [4210, 3980, 4520, 4105, 4503, 4780]
income   = [8000, 8000, 8500, 8000, 9200, 8800]

plt.plot(months, spending, marker="o", color="#d9534f", linewidth=2, label="支出")
plt.plot(months, income,   marker="s", color="#5bc0de", linewidth=2, label="收入")

plt.title("2026下半年收支走势")
plt.xlabel("月份")
plt.ylabel("金额(元)")
plt.legend(loc="best")                       # 图例:label 自动生成,loc="best" 自动挑不挡线的位置
plt.grid(True, linestyle="--", alpha=0.5)
plt.savefig("income_vs_spending.png", dpi=150, bbox_inches="tight")
plt.show()

几个新讲究快速过一遍:

  • 配色是”角色设定”:支出用红(#d9534f),收入用蓝(#5bc0de),一眼分清谁是谁。十六进制颜色码(# 开头六个字符)随便搜”color picker”就能挑;两条线的颜色要有明显明暗/冷暖差别,深蓝配浅蓝是给自己找麻烦;
  • marker 也参与区分:圆点是支出、方块是收入——就算色盲观众看图,靠形状也能分辨;
  • legend(loc="best"):图例自动找不挡数据的角落。图上超过一条线就必须有图例,这是铁律;
  • 线宽 2:默认的 1 稍细,报告里投影容易看不清,加粗到 2 稳妥。

第 2 步:真实日期轴 —— 兑现上一篇的坑4

上一篇说过:文字标签(”6月””7月”)是等距分类,不等距的真实日期会被画得”骗人”。今天正面解决——把日期变成真正的日期对象,matplotlib 就按真实间隔画:

# real_date_axis.py — 真实日期轴:间隔不均匀也不骗人
import matplotlib.pyplot as plt
from datetime import datetime

plt.rcParams["font.sans-serif"] = ["Noto Sans CJK SC", "SimHei", "WenQuanYi Zen Hei"]
plt.rcParams["axes.unicode_minus"] = False

# 不等距的真实记账日:月初月中各一笔,间隔有长有短
date_strings = ["2026-11-01", "2026-11-03", "2026-11-08", "2026-11-15", "2026-11-28"]
balance = [4500, 4310, 3980, 3720, 3100]

# 把字符串转成 datetime 对象(strptime = "按格式拆字符串")
dates = [datetime.strptime(d, "%Y-%m-%d") for d in date_strings]

plt.plot(dates, balance, marker="o", color="#5cb85c", linewidth=2)
plt.title("11月账户余额变化(真实日期轴)")
plt.xlabel("日期")
plt.ylabel("余额(元)")
plt.grid(True, linestyle="--", alpha=0.5)
plt.gcf().autofmt_xdate()   # 日期标签自动斜排,防止挤成一团
plt.savefig("balance_nov.png", dpi=150, bbox_inches="tight")
plt.show()

就多了一个动作:datetime.strptime(...) 把 "2026-11-03" 拆成日期对象。matplotlib 拿到真日期后,自动按”1号到3号”和”15号到28号”的真实间隔分配横向空间——28号那个点会明显靠右,骗人的等距没了。plt.gcf().autofmt_xdate() 是给日期标签”斜着排队”,防挤。

第 3 步:平滑趋势 —— 让”大势”浮出水面

真实数据天生”抖”:今天花 35、明天花 120、后天花 20……波动太大时,线像心电图,大势反而看不清。解决办法是滚动平均(也叫移动平均):每个点取”它和前 N 天的平均值”,抖动被抹平,大势浮出来:

# smooth_trend.py — 7日滚动平均:把心电图捋成趋势线
import matplotlib.pyplot as plt

plt.rcParams["font.sans-serif"] = ["Noto Sans CJK SC", "SimHei", "WenQuanYi Zen Hei"]
plt.rcParams["axes.unicode_minus"] = False

# 14天每日支出(元):抖得像心电图
daily = [35, 120, 20, 45, 88, 26, 52, 140, 30, 62, 55, 110, 40, 48]

def rolling_avg(values, window):
    """滚动平均:第 i 个点 = values[i-window+1 .. i] 的平均(不够 window 个就取已有的)"""
    result = []
    for i in range(len(values)):
        start = max(0, i - window + 1)
        chunk = values[start:i + 1]
        result.append(sum(chunk) / len(chunk))
    return result

days = list(range(1, len(daily) + 1))
smooth7 = rolling_avg(daily, 7)

plt.plot(days, daily, marker="o", alpha=0.4, color="gray", label="每日实际")
plt.plot(days, smooth7, linewidth=3, color="#d9534f", label="7日滚动平均")
plt.title("每日支出:抖动 vs 大势")
plt.xlabel("天数")
plt.ylabel("支出(元)")
plt.legend(loc="best")
plt.grid(True, linestyle="--", alpha=0.5)
plt.savefig("daily_smooth.png", dpi=150, bbox_inches="tight")
plt.show()

看点有三个:原始数据用灰色半透明(alpha=0.4)退居背景,趋势线用红色加粗站 C 位——图里的”主次”靠颜色和粗细说话;rolling_avg 是第7篇的函数手艺,纯循环+切片,没有任何新语法;窗口取 7(一周)是经验值,窗口越大线越平,但反应越迟钝——这个度在坑点疫苗2里敲警钟。

第 4 步:标注拐点 —— 让故事的”转折”自己跳出来

趋势故事的高潮是拐点,那就把拐点标出来。用上一篇预告过的 plt.annotate:

# annotate_turning.py — 标出最高点:让拐点自己跳出来
import matplotlib.pyplot as plt

plt.rcParams["font.sans-serif"] = ["Noto Sans CJK SC", "SimHei", "WenQuanYi Zen Hei"]
plt.rcParams["axes.unicode_minus"] = False

months = ["6月", "7月", "8月", "9月", "10月", "11月"]
spending = [4210, 3980, 4520, 4105, 4503, 4780]

plt.plot(months, spending, marker="o", color="#d9534f", linewidth=2)
plt.title("月支出走势(标注最高点)")
plt.xlabel("月份")
plt.ylabel("支出(元)")
plt.grid(True, linestyle="--", alpha=0.5)

# —— 标注最高点 ——
peak_idx = spending.index(max(spending))          # 1. 找到最高点的位置
plt.annotate(f"峰值 {max(spending)} 元",
             xy=(peak_idx, max(spending)),          # 2. 箭头指向的点
             xytext=(peak_idx - 1.5, max(spending) - 100),  # 3. 文字放哪(往左下挪,别压线)
             arrowprops=dict(arrowstyle="->", color="gray"),  # 4. 画个小箭头
             fontsize=11, color="#d9534f")

plt.savefig("spending_peak.png", dpi=150, bbox_inches="tight")
plt.show()

annotate 的逻辑一句话:在 xy 处画箭头,指向 xytext 处的文字(方向看着别扭,记”xy 是箭头尖、xytext 是纸条”就不混了)。代码是死的,思路是活的——max() 找最高点、index() 找位置,这两下是第8篇列表的手艺。同一套写法换 min() 就标最低点,换个条件就能标”第一次超过 4500 的月份”。

到这里,一张”会讲故事”的折线图的全套手艺都有了:对比(多线)→ 真实(日期轴)→ 大势(平滑)→ 高潮(标注)。

四、改进:跟AI一起升级

把上面的 compare_trend.py 发给 AI,这次的需求往”专业图表”上提:

“这是我的收支双线对比图(代码如下)。想升级成能放进月度汇报的水平:1)两条线之间的差额用色块填出来(结余好看、超支刺眼);2)收入和支出金额差好几倍,一条纵轴看不清波动,帮我改进;3)整体风格更像财经新闻的走势图。请先给方案说明每处改动的意图,确认后再给代码。”

它的方案里会冒出三个高级货,先混脸熟:

  • plt.fill_between:两条线之间”灌色块”——结余(收入高于支出)灌绿、缺口灌红,差额一眼可见;
  • 双纵轴(plt.twinx()):左轴看支出、右轴看收入。但注意坑点疫苗1,这招是双刃剑;
  • plt.style.use("fivethirtyeight"):又一套现成皮肤,财经图表那味儿。

逐条实现,每条跑一次对比效果。重点不是它给了什么代码,是你能说出”这处改动让趋势的哪个要素更清楚了”——说不出,就说明改动是花架子。

五、四个必踩的坑(先打疫苗)

  1. 双纵轴是”合法的骗术”,慎用。 左右两个纵轴的刻度可以任意拉伸——同一份数据,把右轴从”0~10000″改成”7900~9300″,收入那条线立刻”暴跌暴涨”,视觉冲击全是刻度造出来的。要用就守住两条:两轴都从 0 开始;标题里明说”左轴=支出,右轴=收入”。能用单轴就别用双轴,fill_between 表达差额通常更诚实。
  1. 平滑过度 = 骗自己。 滚动窗口拉到 30 天,什么波动都给你捋平——包括真实的风险信号。八月那笔超支可能就这样被”平均”没了。原则:原始线永远保留在图上(哪怕灰色半透明),平滑线只是帮你看大势的辅助线;窗口别超过数据跨度的三分之一。
  1. 无序数据别硬画折线。 “苹果、香蕉、橘子”的销量画折线,等于暗示”苹果→香蕉→橘子”有先后顺序和过渡关系——纯属编故事。判断法:把横轴两项调换位置,图的”故事”如果变了,这数据就不该用折线图,下一篇柱状图才是它的归宿。
  1. 日期挤成一团是”格式”问题,不是”数据”问题。 日期一多,x 轴标签必然重叠。除了 autofmt_xdate(),还可以让 AI 教你 matplotlib.dates 里的 DateFormatter(控制显示”11-03″还是”3日”)和 set_major_locator(控制隔几天标一个刻度)。但警惕一个更隐蔽的坑:横轴日期有大段空白时(比如只记了 11-01 和 11-28 两笔),要么接受真实空白,要么改用文字标签——千万别手工删空段,那是亲手把”骗人”请回来。

六、跟AI协作的正确姿势(趋势图特供版)

  1. 问”结论”,不要问”函数”。 “这张图能得出什么结论?”发给 AI,让它以读者视角回答——它答不出,说明图没讲清故事,改图比改问法重要。
  1. 把”观众是谁”告诉它。 同样一份数据,给家人的图要大字、少线、结论写脸上;给同事的图可以密一点、精确一点。AI 拿到”观众画像”,给的配色、字号、标注密度会完全不一样。
  1. 数据敏感性自己把关。 平滑窗口多大、双轴刻度怎么设,这些决定”图讲的故事是真是骗”——AI 会顺着你的要求做,但它不会替你守住”诚实”这条线。图美化是技术,图诚实是底线。

七、今日小练习

练习1:画出你的收支对比故事线。 用第1步的双线对比代码,换成你自己的数据(没有就编半年的收支),要求:双线、双色、双 marker、图例、网格、存 PNG(提示:plt.plot(..., label="支出") × 2 + plt.legend();颜色挑冷暖对比的一对,比如 #d9534f 配 #5bc0de)。

练习2:真实日期轴 + 平滑,双剑合璧。 找一组带日期的数据(每日支出、体重、步数都行),先画原始折线,再叠一条 7 日滚动平均(提示:datetime.strptime 转日期解决间隔问题,rolling_avg 函数直接从第3步搬;两条线一灰一红、一淡一浓,图例别忘了)。

练习3:把拐点标出来。 在练习1的图上,用 annotate 标出最高点和最低点各一处(提示:max()/min() 找值、index() 找位置;文字放在数据点的斜上方或斜下方,arrowprops 画个小箭头;标完自己问自己:这张图的”方向、拐点、波动”三要素,三秒钟能答出来吗?)。

下篇预告

下一篇(第63篇):柱状图 — 把对比拍在桌上。 折线图讲”变化”,柱状图讲”较量”:谁多谁少、差距多大、排名如何。下一篇学柱状图的看家本领——分组柱、堆叠柱、横向条形图,以及”柱子从哪儿起跑才不骗人”(y 轴截断的经典骗局)。两类图学完,你的”图表兵器库”就有一攻一守了。