메트릭스 팩
수행 작업
스타트업의 매출 데이터를 투자 분석가가 가장 먼저 확인하는 숫자, 즉 MRR, 코호트, NRR을 하나의 슬라이드로 변환하는 작업을 요청받았습니다. 모든 숫자는 metrics.py에서 비롯되며, 절대 사용자의 추측으로 작성해서는 안 됩니다.
입력을 선택하고, 헬퍼를 실행하고, CSV 파일을 읽고, 그 주위에 설명을 작성합니다.
입력 헬퍼 (metrics.py) 사용자
───── ─────────────────── ───
updates/ (a) ──┐
customer export (b) ─┼──► mrr.csv cohorts.csv ──► metrics.md (일반 영어)
contracts register (c) ┘ nrr.csv excluded.csv metrics-slide.png (슬라이드 1개)
현재 작업 디렉토리를 기준으로 <company-slug>/metrics-pack/ 디렉토리에 결과물을 생성합니다.
metrics.md– 노트. "일반 영어로:"라는 문장으로 시작합니다.mrr.csv,cohorts.csv,nrr.csv(및 계약 등록부의 경우excluded.csv) – 헬퍼가 작성하며, 사용자가 수정하지 않습니다.metrics-slide.png– 16:9 비율의 슬라이드 1개.
이 파일에서 사용되는 약어: 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) 계약 등록부 | 헤더 행 Client, Contract value, Start month, Term (months)가 포함된 CSV; 헤더 위의 줄은 건너뜀; 월은 May-26 형식; 값은 "1,550,000" 형식 | --contracts <csv> |
(a) <company-slug>/updates/ | 매출 라인이 포함된 월별 업데이트 파일, 고객 세부 정보 없음 | revenue-chart에서 하는 것처럼 읽고(2단계 참조), 하나의 고객 total과 함께 customer_id,month,revenue를 작성한 다음 --revenue |
입력 (a)의 경우 고객이 없습니다. 그러면 헬퍼는 cohorts.csv와 nrr.csv를 헤더만 포함하여 작성하고, mrr.csv의 new, expansion, contraction, churned 셀은 비워 둡니다. 이는 하나의 고객만으로는 총 성장률을 다시 설명할 수밖에 없기 때문입니다. 해당 열은 무시하고, 히트맵은 표시하지 않으며, "12개월 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 |
| 수익 모델 | 구독 · 거래 · 광고 · 일회성 · 해당 없음 |
| 시장 | 정확히 하나: 핀테크 및 보험 · 건강 및 바이오 · 커머스 및 소매 · 물류 및 공급망 · AI 및 데이터 인프라 · 엔터프라이즈 소프트웨어 · 사이버 보안 · 법률, 규정 준수 및 거버넌스 · 세일즈 및 마케팅 · HR 및 인재 · 교육 · 미디어, 게임 및 엔터테인먼트 · 부동산 및 건설 · 에너지, 기후 및 농업 · 산업 및 로봇공학 · 소비자 라이프스타일 및 서비스 · 기타 |
| 세부 분야 | 자유 텍스트 |
| 단계 | 프리시드 · 시드 · 시리즈 A · 시리즈 B+ · 해당 없음 |
| 지역 | 본사 국가; 판매 국가 |
| 조달 중인 라운드 | 금액 + 통화 + 수단, 또는 "명시되지 않음" |
| 사용 가능한 메트릭 | 위치 (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 | 코호트 = 고객이 매출을 올린 첫 번째 월; 오프셋 = 그 이후 개월 수 |
nrr.csv | start_month, months, nrr | 청구된 월만 기준으로 계산할 수 있는 모든 시작 월에 대한 12개월 NRR(--as-of 이후의 월은 절대 계산하지 않음); 계산할 수 없는 경우 헤더만 있음. 또한 단일 고객 내보내기 및 기록된 갱신이 없는 계약 등록부의 경우 헤더만 있음. 등록부의 경우, 일부 고객이 기록된 갱신(2단계 규칙)을 가진 시작 월만 나열됨 |
excluded.csv | client, value, start, term | --min-term 개월(기본값 3)보다 짧은 계약 |
헬퍼가 error:로 중단되면 잘못된 열 또는 값을 알려줍니다. 입력 소스에서 수정하거나(또는 소유자에게 문의) – 절대 입력을 우회하여 수정하지 마세요.
헬퍼가 가정하는 사항(각각을 노트에 명시):
- 계약 금액은 기간 전체의 총액이며, 시작 월부터 기간으로 균등 분할됩니다. 소유자가 월별 또는 연별 금액이라고 말하면 중단하고 질문하세요. 답변에 따라 모든 숫자가 달라집니다.
- 계약은 기간이
--min-term개월 이상인 경우에만 반복 가능합니다. 더 짧은 계약은 비반복:excluded.csv에 보고되며, MRR에 포함되지 않습니다. - 새 행이 명시하지 않는 한 아무것도 갱신되지 않습니다. 계약 기간이 종료되면 헬퍼는 매출이
churned(이탈)으로 표시됩니다. 계약 등록부에서는 계약 종료일이며, 입증된 이탈이 아닙니다 – 해당 월마다 이를 명시하세요. - 계약 등록부는 계약을 기록하며, 갱신을 기록하지 않습니다. 계약 종료는 매출 손실로 읽히므로, 헬퍼는 등록부에서 기록된 갱신이 있는 시작 월에 대해서만
nrr.csv를 계산합니다. 규칙: 갱신은 동일한 고객의 이후 반복 계약(기간이--min-term이상)으로, 시작 월이 이전 반복 계약의 종료 월(종료 월 = 시작 월 + 기간 - 1, 따라서 종료 다음 달에 시작하는 연속 계약은 허용되며, 간격도 허용됨) 이후 최소 1개월 후여야 합니다. 첫 번째 계약과 겹치는 두 번째 계약(업셀 또는 추가 기능)은 갱신이 아닙니다. 어떤 고객에게도 갱신이 없으면nrr.csv는 헤더만 있고 답변은 "아직 계산할 수 없음"입니다. 일부 고객에게 갱신이 있는 경우, 수치는 등록부가 기록하는 갱신만 반영하며, 노트와 슬라이드에 이를 명시해야 합니다. - 중단했다가 다시 시작하는 고객은 다시
new로 계산됩니다.
3단계 – 출력 읽기
csv 모듈을 사용하여 CSV를 읽으세요. 작성하는 모든 숫자는 CSV에서 비롯됩니다. 설명에서 아무것도 계산하지 마세요. CSV에 없는 숫자(예: 비율)가 필요한 경우 명령으로 계산하고 명령의 출력을 인용하세요.
- MRR. 마지막 청구 월의
mrr; 최고 월과 그status; 청구/계약 분할. 데이터의 첫 번째 월과 마지막 월을 명시하세요. - 런오프. 마지막 계약 시작 이후의 월은 데이터에 갱신이 없으므로 매출만 감소합니다. 이를 갱신 없는 계약의 런오프로 표시하고, 절대 이탈로 표시하지 마세요.
- 코호트. 청구된 매출이 최소 2개월 이상인 코호트를 계산하세요. 슬라이드의 히트맵에는 이러한 코호트가 최소 2개 필요합니다. 그렇지 않으면 히트맵을 건너뛰고 이유를 설명하세요.
- NRR. 먼저 입력을 확인하세요. 입력 (a) 또는 단일 고객 내보내기의 경우
nrr.csv,cohorts.csv및 변동 열을 무시하세요. 답변은 "아직 계산할 수 없음"입니다. 그렇지 않고nrr.csv에 데이터 행이 있으면 인용하세요. 계약 등록부의 경우 "등록부에 기록된 갱신만 계산"을 추가하세요. 헤더만 있는 경우 답변은 "12개월 NRR을 아직 계산할 수 없음": 시작 후 12개월 매출 월이 있는 고객이 없습니다. 절대 추정하지 말고, 더 짧은 기간으로 채우지 마세요. 이유를 한 줄로 설명하세요(예: "등록부의 모든 계약이 2026년에 시작되므로, 12개월을 운영하고 갱신한 계약이 없음"). - 제외됨. 제외된 계약 수, 총 가치, 그리고 제외 규칙.
4단계 – metrics-slide.png
matplotlib과 numpy가 필요합니다(python3 -c "import matplotlib, numpy"). 없으면 설치 전에 물어보세요. 가장 깔끔한 방법은 임시 가상 환경을 사용하는 것입니다. 기본은 revenue-chart의 차트 템플릿(다크 테마, 레이블 충돌 확인)입니다.
16:9 비율의 슬라이드 1개, 레이아웃은 다음과 같습니다.
┌─────────────────────────────────────────────────────────────┐
│ <회사명> – 매출 메트릭 기준일: <ISO 날짜> │
├──────────────────────────────┬──────────────────────────────┤
│ MRR 라인: 실선 = 청구됨, │ 코호트 히트맵 (청구 월만, │
│ 점선 + 음영 = 계약됨 │ 0월 대비 %) – 또는 히트맵이 │
│ │ 없을 경우 NRR 패널 │
├──────────────────────────────┴──────────────────────────────┤
│ NRR: <수치> 또는 "12개월 NRR: 아직 계산할 수 없음" │
│ 한 줄 요약 3개 │
└─────────────────────────────────────────────────────────────┘
슬라이드 규칙:
- 계약됨은 결코 청구됨으로 표시되지 않습니다.
--as-of이후의 월은 점선, 빈 마커, "계약됨, 아직 청구되지 않음"이라는 레이블이 있는 음영 밴드를 사용합니다. - 계약 등록부의 경우, 표시된 NRR 수치에는 "등록부에 기록된 갱신만 계산"이라는 주의 사항이 포함됩니다(스크립트가
source = "register"일 때 추가함). - NRR 라인은
nrr.csv의 수치 또는 정확한 문구 "12개월 NRR: 아직 계산할 수 없음"을 표시합니다. - 히트맵은 최소 2개의 코호트가 최소 2개의 청구 월을 가진 경우에만 표시합니다.
- 각각 CSV 행으로 추적 가능하고, 권장 사항이나 예측이 아닌, 한 줄 요약 3개.
- 축 제목에 통화 표시; 출처에 통화가 없는 경우 "통화 명시되지 않음".
- 슬라이드의 모든 수치는 스크립트가 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[j:], vals[j:], color=CONTR, zorder=4)
…
[그리기 완료를 위해 잘림]