指标包
你正在做什么
你被要求将一家初创公司的收入数据转化为基金分析师最先阅读的指标:MRR、队列分析、NRR——集中在一张幻灯片上。数字来自 metrics.py,绝不能来自你的计算。 你选择输入,运行辅助程序,读取其CSV文件,然后围绕它们撰写文字。
输入 辅助程序 (metrics.py) 你
───── ─────────────────── ───
updates/ (a) ──┐
客户导出 (b) ──┼──► mrr.csv cohorts.csv ──► metrics.md (简明英文)
合同登记册 (c) ┘ nrr.csv excluded.csv metrics-slide.png (一张幻灯片)
输出文件位于当前工作目录的 <company-slug>/metrics-pack/ 中:
metrics.md– 说明文档。开头为“用简明英文说明”。mrr.csv、cohorts.csv、nrr.csv(以及合同登记册用的excluded.csv)——由辅助程序生成,您无需修改。metrics-slide.png– 一张16:9的幻灯片。
本文件中使用的缩写:MRR = 月度经常性收入;NRR = 净收入留存率;CSV = 逗号分隔值文件;PNG = 图像文件;ISO = 国际日期格式(YYYY-MM-DD,或月份用 YYYY-MM);EUR = 欧元;USD = 美元;--as-of = 辅助程序选项,用于指定最后一个已开票月份。
输入
来自 $ARGUMENTS 或对话:一个公司名称,以及可选的一个文件。三种输入类型,优先级如下:
| 输入 | 说明 | 传递给辅助程序的方式 |
|---|---|---|
| (b) 按客户导出 | CSV 格式 customer_id,month,revenue,月份格式为 YYYY-MM | --revenue <csv> |
| (c) 合同登记册 | CSV 文件,包含标题行 Client, Contract value, Start month, Term (months);标题行以上的行被跳过;月份格式如 May-26;金额格式如 "1,550,000" | --contracts <csv> |
(a) <company-slug>/updates/ | 月度更新文件,包含一条收入行,无客户详细信息 | 按 revenue-chart 的方式读取(那里的步骤2),写入 customer_id,month,revenue,客户ID为 total,然后使用 --revenue |
使用输入 (a) 时没有客户。辅助程序随后只写入 cohorts.csv 和 nrr.csv 的标题行,并将 mrr.csv 中的 new、expansion、contraction 和 churned 单元格留空,因为只有一个客户时,这些只会重复总增长。忽略这些列,不显示热力图,并打印“12-month NRR:无法计算”(原因:缺少客户详情)。对于任何只有一个客户ID的 --revenue 文件也是如此。
如果公司名称模糊(两个公司同名),在读取任何内容之前先问清楚是哪一个。
第1步 – 公司卡片和输入
公司卡片
<company-slug>/company-profile.md – 首先读取。如果不存在,则创建。如果有字段为空且你了解到该信息,则填写并更新 updated / updated_by。切勿覆盖已填写的字段:如果发现不同的值,在 ## Conflicts 中添加一行,包含两个值及其来源。卡片以 ## Conflicts 部分结束(如果不存在,则创建),其表头必须为 | Field | Card value | Card source | New value | New source | Found by | Date |。
| 字段 | 允许的值 |
|---|---|
| 为客户完成的工作 | 一句话,引用公司的原话,并附上URL |
| 谁付款 | 自由文本 |
| 商业模式 | B2B · B2C · B2G |
| 收入模式 | 订阅 · 交易 · 广告 · 一次性 · n.a. |
| 市场 | 必须为以下之一:金融科技与保险 · 健康与生物 · 商业与零售 · 物流与供应链 · 人工智能与数据基础设施 · 企业软件 · 网络安全 · 法律、合规与治理 · 销售与营销 · 人力资源与人才 · 教育 · 媒体、游戏与娱乐 · 房地产与建筑 · 能源、气候与农业 · 工业与机器人 · 消费者生活与服务 · 其他 |
| 垂直领域 | 自由文本 |
| 阶段 | 种子前 · 种子轮 · A轮 · B轮+ · n.a. |
| 地理区域 | 总部所在国家;销售所在国家 |
| 正在融资的轮次 | 金额 + 货币 + 工具,或“未说明” |
| 可用的指标 | 位置(updates/、导出文件),或“无” |
前置元数据:company、slug、website、updated(ISO日期)、updated_by(技能名称)。卡片的表格有第三列 Source,每个字段都必须填写。
从卡片中获取公司名称、商业模式、收入模式和地理区域;地理区域提示预期的货币。在“可用的指标”中填写你使用的文件。如果收入模式不是订阅,在说明中说明MRR假设是经常性收入。
找到输入(上述表格)并将其复制到 <company-slug>/metrics-pack/input/ 中,以便运行过程可审计。除非所有者表示可以公开,否则切勿提交或发布客户级别的文件。货币: 从文件中获取;合同登记册通常不注明货币。如果没有注明,则在所有地方都写“货币未注明”,且绝不自行选择。
今天和 --as-of。 --as-of 是实际已开票的最后一个月——通常是从今天日期(ISO格式)算起的当月。此后的月份是已签约的:已签署但尚未开票。
第2步 – 运行辅助程序
使用 /usr/bin/python3 或任何 Python 3.9+;metrics.py 仅需标准库,不进行网络调用。它位于此文件旁边(skills/metrics-pack/metrics.py;在已安装的插件中为 ${CLAUDE_PLUGIN_ROOT}/skills/metrics-pack/metrics.py)。
python3 <path>/metrics.py --contracts <register.csv> --out-dir <company-slug>/metrics-pack \
--as-of 2026-11 --min-term 3
python3 <path>/metrics.py --revenue <export.csv> --out-dir <company-slug>/metrics-pack \
--as-of 2026-11
它写入的文件:
| 文件 | 列 | 含义 |
|---|---|---|
mrr.csv | month, mrr, new, expansion, contraction, churned, status | 每个日历月一行;status 在 --as-of 之前为 billed,之后为 contracted |
cohorts.csv | cohort, month_offset, revenue | 队列 = 客户有收入的第一个月;offset = 自该月以来的月数 |
nrr.csv | start_month, months, nrr | 对于每个可以仅使用已开票月份(--as-of 之后的月份不计入)计算出12个月NRR的开始月份,列出结果;如果都无法计算,则仅包含标题行。对于单一客户导出文件以及没有记录续费的合同登记册,也仅包含标题行;对于登记册,仅列出某些客户有记录续费(第2步中的规则)的开始月份 |
excluded.csv | client, value, start, term | 期限短于 --min-term 个月(默认3)的合同 |
如果辅助程序以 error: 停止,它会指明错误的列或值。在源文件中修复输入(或询问所有者)——切勿绕过它进行编辑。
辅助程序假设以下内容(在说明中逐一说明):
- 合同价值是整个期限的总金额,平均分摊:价值 ÷ 期限,从开始月份开始每月分摊。如果所有者说是每月或每年金额,请停下来询问——答案会改变所有数字。
- 合同只有在期限至少为
--min-term个月时才被视为经常性合同。较短的合同是非经常性的:在excluded.csv中报告,不计入MRR。 - 除非有新行表明,否则不会自动续费。当合同期限结束时,辅助程序将收入减少显示为
churned。在合同登记册中,这只是合同结束,而非证实的客户流失——对于此类月份,必须如此说明。 - 合同登记册记录的是合同,而非续费记录。 合同结束被视为收入损失,因此辅助程序仅针对某些客户有记录续费的开始月份从登记册计算
nrr.csv。规则:续费是指同一客户在较早的经常性合同(期限至少为--min-term)结束月份之后至少一个月开始的另一个经常性合同(结束月份 = 开始月份 + 期限 - 1;因此,在结束月份之后立即开始的无缝衔接合同也算,并且允许存在间隔)。与较早合同重叠(追加销售或附加合同)的第二个合同不是续费。如果没有客户有续费记录,则nrr.csv仅包含标题行,答案为“无法计算”。如果某些客户有,则该数字仅反映登记册记录的续费情况,且说明和幻灯片必须说明这一点。 - 停止后又回来的客户再次被视为
new。
第3步 – 读取输出
使用 csv 模块读取CSV文件。你编写的每个数字都来自于它们。不要在文字描述中进行计算;如果你需要CSV文件中没有的数字(例如比例),请使用命令计算并引用命令的输出。
- MRR。 最后一个已开票月份的
mrr;峰值月份及其status;已开票与已签约的拆分。指定数据中的第一个和最后一个月份。 - 收入递减。 最后一个合同开始月份之后的月份只会失去收入,因为数据中没有续费。将其标记为无续费合同的收入递减,绝不能标记为流失。
- 队列分析。 统计至少有两个月的已开票收入的队列。幻灯片上的热力图至少需要两个这样的队列;否则跳过并说明原因。
- NRR。 首先检查输入。对于输入 (a) 或单一客户导出文件,忽略
nrr.csv、cohorts.csv和变动列:答案为“无法计算”。否则,如果nrr.csv有数据行,则引用它们;对于合同登记册,添加“仅统计登记册中记录的续费”。如果它只有标题行,则答案为 “12个月NRR无法计算”:没有客户在开始之后有超过十二个月的收入月份。切勿估算它,切勿用更短的窗口填充它。用一行说明原因(例如“登记册中的所有合同均于2026年开始,因此没有任何合同运行满十二个月并续费”)。 - 排除项。 排除的合同数量、其总价值以及排除它们的规则。
第4步 – metrics-slide.png
需要 matplotlib 和 numpy(python3 -c "import matplotlib, numpy")。如果缺少,在安装任何内容之前询问;最简洁的方法是使用一个临时虚拟环境。基础是 revenue-chart 中的图表模板(深色主题,标签冲突检查)。
一张16:9的幻灯片,布局如下:
┌─────────────────────────────────────────────────────────────┐
│ <公司名称> – 收入指标 截至 <ISO日期> │
├──────────────────────────────┬──────────────────────────────┤
│ MRR折线图:实线=已开票, │ 队列热力图(仅已开票月份, │
│ 虚线+阴影=已签约 │ 月份0的百分比)——或NRR面板 │
│ │ (如果没有热力图) │
├──────────────────────────────┴──────────────────────────────┤
│ NRR: <数字> 或 "12-month NRR:无法计算" │
│ 三条一行式要点 │
└─────────────────────────────────────────────────────────────┘
幻灯片的规则:
- 已签约的绝不能样式化为已开票。
--as-of之后的月份使用虚线、空心标记和带标签的阴影区域“已签约,尚未开票”。 - 对于合同登记册,如果显示了NRR数字,则必须带有注释“仅统计登记册中记录的续费”(当
source = "register"时,脚本会自动添加)。 - NRR行显示来自
nrr.csv的数字,或确切文字“12-month NRR:无法计算”。 - 仅当至少有两个队列拥有至少两个已开票月份时,才显示热力图。
- 三条一行式要点,每条都可追溯到CSV中的一行,不得包含建议或预测。
- 轴标题中显示货币;如果来源未提供,则显示“货币未注明”。
- 幻灯片上的所有数字均由脚本从CSV中读取,而非手动输入。要点是唯一手动输入的文字:绘制后,对照CSV检查要点中的每个数字。
import csv, os
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
# 配置 ---------------------------------------------------------------
company = "公司名称"
folder = "<company-slug>/metrics-pack"
currency = "货币未注明" # 或 "EUR", "USD" ...
as_of = "2026-11" # 与 --as-of 相同的值
source = "register" # "register" (--contracts) 或 "export" (--revenue)
takeaways = [ # 三条一行式要点,每条都需对照CSV检查
"要点 1",
"要点 2",
"要点 3",
]
# -------------------------------------------------------------------------
def read(name):
with open(os.path.join(folder, name), newline="", encoding="utf-8") as fh:
return list(csv.DictReader(fh))
mrr, coh, nrr_rows = read("mrr.csv"), read("cohorts.csv"), read("nrr.csv")
months = [r["month"] for r in mrr]
vals = [float(r["mrr"]) for r in mrr]
billed = [r["status"] == "billed" for r in mrr]
x = np.arange(len(months))
nb = sum(billed) # 已开票月份排在前面
BG, FG, MUTED = '#0f1117', '#e0e0e0', '#9aa0b4'
BILLED, CONTR = '#4fc3f7', '#f2b84b'
fig = plt.figure(figsize=(13.333, 7.5), facecolor=BG) # 16:9
gs = fig.add_gridspec(3, 2, height_ratios=[0.5, 5.2, 1.9], width_ratios=[1.35, 1],
hspace=0.35, wspace=0.18, left=0.06, right=0.97, top=0.97, bottom=0.04)
ax_t = fig.add_subplot(gs[0, :]); ax_t.axis('off')
ax_t.text(0, 0.5, f"{company} – 收入指标", color='#fff', fontsize=20,
fontweight='bold', va='center')
ax_t.text(1, 0.5, f"已开票至 {as_of}", color=MUTED, fontsize=11, ha='right', va='center')
# --- MRR 折线图 ---
ax = fig.add_subplot(gs[1, 0]); ax.set_facecolor(BG)
if nb < len(months):
ax.axvspan(nb - 0.5, len(months) - 0.5, color=CONTR, alpha=0.10, zorder=0)
ax.text(nb - 0.35, 0.97, "已签约,\n尚未开票", color=CONTR, fontsize=9,
transform=ax.get_xaxis_transform(), va='top')
ax.plot(x[:nb], vals[:nb], color=BILLED, lw=2.6, marker='o', ms=5, label='已开票MRR', zorder=3)
if nb < len(months):
j = max(nb - 1, 0) # 连接两个线段
ax.plot(x[j:], vals[j:], color=CONTR, lw=2.2, ls='--',
label='已签约MRR(未开票)', zorder=3)
ax.scatter(x
…
[为翻译截断]
NRR值显示在幻灯片底部。示例:
- 12-month NRR: 92.3% (cycles 7 – 18),来自 4 个队列。 - 已签约MRR: €42,000(占总额的 23%)。 - 已排除: 2 份短期合同(总价值 €12,000)。