~13 min · reference-scale, bootstrapping, auditability, flags
Level 0원석
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
모든 계열은 역사 없이 시작한다. 설계 질문은 첫날에 뭘 보여주고 그걸 어떻게 라벨링하냐다.
부트스트랩 문제
백분위에는 역사가 필요해. 갓 나온 게이지한테는 없고. 방어적으로 처리할 예외 사례가 아니야. 이 제품의 모든 계열이 여길 통과해. 그리고 제일 눈에 띄는 둘이 이 퀘스트를 쓰는 동안 정확히 그 상태였어. 합성 지표랑 집중도. 둘 다 며칠 전에 시작했고, 둘 다 백분위 None 에 창은 0 년으로 보고 중이었지.
그럼 카드에 뭘 보여줘? 답 셋, 정직한 순서대로.
아무것도 안 보여준다. 변호 가능하고, 진짜로 쓸모 있는 새 측정이 역사가 여물 때까지 몇 달간 안 보인다는 뜻이야. 진짜 비용이고, 그 비용은 전부 독자가 내.
가진 표본 세 개로 계산한 백분위를 보여준다. 절대 하지 마. 이 코드베이스 바닥이 스무 개니까 셋은 아깝게 못 미친 것도 아니야. 한 세기짜리 백분위랑 똑같이 그려지고 둘을 구분할 시각적 신호가 없어. 독자가 그 페이지에서 뭐든 눈금 맞출 능력을 조용히 파괴하는 선택지야.
문서화된 기준 범위로 점수 매기고, 그렇다고 표시한다. 이제 그 숫자는 관측된 역사 안 위치가 아니라 누가 골라서 적어둔 범위 안 위치야. 더 약한 주장이고, 정직하게 라벨 붙은.
장식이 아닌 플래그
점수 함수는 경로 둘 중 하나를 타고 어느 쪽인지 보고해. 계열이 얻어낸 자기 역사 백분위, 아니면 그때까지는 문서화된 기준 척도, 플래그 붙여서. 독스트링이 이 플래그가 왜 있는지 정확히 말해. 기준 점수는 가정된 범위에 대한 주장이고, 측정된 것으로 절대 오해되면 안 된다.
그 문장이 진짜 일을 해. 플래그가 없으면 두 경로가 같은 모양 출력을 내고, 하류에서는 뭘 받았는지 아무도 몰라. 있으면 화면이 둘을 다르게 그릴 수 있고, 지시서가 그 구분을 인용할 수 있고, 결정적으로 합성 지표가 자기 구성원 중 어느 게 측정이 아니라 기준 점수였는지 보고할 수 있어.
값이 세기가 다른 두 경로로 도착할 수 있으면, 경로가 값의 일부야. 추정 대 측정, 캐시 대 신선, 기본값 대 전달값, 추론 대 명시. 코드에 양쪽 분기가 같은 타입을 내놓는 if-else 가 있을 때마다 물어. 소비자가 어느 분기가 돌았는지 알아야 하나? 보통은 알아야 하고, 보통은 아무도 안 알려줬어.
척도가 함수 아니라 숫자인 이유
기준 척도는 평범한 선형 매핑이야. 0 점 나오는 낮은 값, 100 점 나오는 높은 값. 왜 그런지 설명하는 주석이 이 코드베이스에서 제일 날카로운 줄 중 하나야. 람다는 저장도 서빙도 감사도 안 된다.
작은 점수 함수가 뭘 앗아가는지 생각해봐. DB 에 못 써. API 로 못 돌려줘. 위임 지시서에 인용 못 하고, 리뷰에서 의미 있게 diff 도 안 되고, 소스를 안 읽는 사람은 확인도 못 해. 숫자 쌍은 그걸 전부 할 수 있고. 게다가 대시보드가 이미 쓰던 척도가 마침 전부 선형이었으니까, 데이터로 표현하는 비용은 정확히 0 이었어.
일반형은 이거야. 절차가 자기 추가 능력을 실제로 안 쓰고 있으면, 절차적 표현보다 선언적 표현을 골라. 코드는 데이터보다 표현력이 세고, 그 표현력이 바로 인터프리터가 아닌 모든 도구한테 코드를 불투명하게 만드는 그거야.
Code
점수로 가는 경로 둘, 그리고 둘을 구분 가능하게 유지하는 플래그·python
# Reference scales as DATA -- lo scores 0, hi scores 100.
# Every scale the dashboard used was already linear, so a pair of
# numbers loses nothing, and a lambda cannot be stored, served,
# or audited.
{"key": "cape", "ref": {"lo": 0.0, "hi": 44.2}}
{"key": "vix", "ref": {"lo": 40.0, "hi": 8.0}} # inverted: hi < lo
{"key": "hy_oas", "ref": {"lo": 6.0, "hi": 1.5}} # tight spread = rich
def score_indicator(spec, row) -> dict[str, Any]:
"""One indicator's 0-10 score, and WHICH PATH produced it.
Own-history percentile when the series has earned one; the
card's documented reference scale until then, flagged
`reference: True`. The flag is not decoration -- a reference
score is a claim about an assumed range, and it must never be
mistaken for a measured one."""
if row["percentile"] is not None:
pct = (100.0 - row["percentile"]) if spec["invert"] \
else row["percentile"]
reference = False
else:
pct = _reference_score(row["value"], spec["ref"])
reference = True
return {
"key": spec["key"], "name": spec["name"],
"category": spec["category"], "value": row["value"],
"data_date": row["data_date"],
"percentile": row["percentile"], "points": row["points"],
"window_years": row["window_years"],
"score": round(round(pct) / 10.0, 2),
"reference": reference,
}
네 코드에서 폴백이 주 경로랑 같은 출력 타입을 내놓는 자리를 찾아봐. 기본값, 캐시된 결과, 추정치. 소비자가 뭘 받았는지 알 수 있는지 확인해. 모르면 플래그를 붙이고 소비자들이 어떻게 반응하는지 봐. 거기서 분기를 타기 시작하는 쪽은 네가 방금 눈에 보이게 만든 조용한 가정을 하고 있던 거야.
Hint
제일 값어치 있는 판은 측정이랑 가정을 구분하는 거야. 시스템은 도입할 땐 합리적이었던 가정을 쌓아가고, 그게 측정값이랑 똑같이 그려지는 순간 안 보이게 돼. 그리고 아무도 안 보이는 가정을 다시 들여다보지 않아.
Progress
Progress is local-only — sign in to sync across devices.