Skip to content

Commit c5dbf3a

Browse files
travisjneumanclaude
andcommitted
feat: flesh out SOLUTION.md for all 30 level-8 and level-9 projects
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 52dabcb commit c5dbf3a

File tree

30 files changed

+8183
-1119
lines changed

30 files changed

+8183
-1119
lines changed
Lines changed: 285 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,314 @@
11
# Solution: Level 8 / Project 01 - Dashboard KPI Assembler
22

3-
> **STOP** Have you attempted this project yourself first?
3+
> **STOP** -- Have you attempted this project yourself first?
44
>
55
> Learning happens in the struggle, not in reading answers.
66
> Spend at least 20 minutes trying before reading this solution.
7-
> If you are stuck, try the [Walkthrough](./WALKTHROUGH.md) first it guides
7+
> If you are stuck, try the [Walkthrough](./WALKTHROUGH.md) first -- it guides
88
> your thinking without giving away the answer.
9+
>
10+
> [Back to project README](./README.md)
911
1012
---
1113

12-
1314
## Complete solution
1415

1516
```python
16-
# WHY percentile: [explain the design reason]
17-
# WHY compute_trend: [explain the design reason]
18-
# WHY load_kpi_definitions: [explain the design reason]
19-
# WHY load_metric_samples: [explain the design reason]
20-
# WHY aggregate_kpi: [explain the design reason]
21-
# WHY assemble_dashboard: [explain the design reason]
22-
# WHY dashboard_to_dict: [explain the design reason]
23-
# WHY parse_args: [explain the design reason]
24-
# WHY main: [explain the design reason]
25-
# WHY evaluate: [explain the design reason]
26-
27-
# [paste the complete working solution here]
28-
# Include WHY comments on every non-obvious line.
17+
"""Dashboard KPI Assembler -- aggregate metrics from multiple sources into a unified dashboard."""
18+
19+
from __future__ import annotations
20+
21+
import argparse
22+
import json
23+
import math
24+
from dataclasses import dataclass, field
25+
from enum import Enum
26+
from pathlib import Path
27+
from typing import Any
28+
29+
30+
# --- Domain types -------------------------------------------------------
31+
32+
# WHY Enum for status? -- Traffic-light status is a closed set of values.
33+
# Using an Enum prevents typos ("grren") and enables IDE autocomplete.
34+
# The .value attribute gives the JSON-friendly string when serializing.
35+
class KPIStatus(Enum):
36+
GREEN = "green"
37+
YELLOW = "yellow"
38+
RED = "red"
39+
40+
41+
# WHY embed evaluate() on KPIDefinition? -- This is the Information Expert
42+
# pattern: the object that holds the threshold data is the one that decides
43+
# the status colour. Putting evaluation logic elsewhere would scatter
44+
# knowledge about thresholds across multiple locations.
45+
@dataclass
46+
class KPIDefinition:
47+
name: str
48+
unit: str
49+
green_threshold: float # values <= this are green
50+
yellow_threshold: float # values <= this (but > green) are yellow; above is red
51+
52+
def evaluate(self, value: float) -> KPIStatus:
53+
if value <= self.green_threshold:
54+
return KPIStatus.GREEN
55+
if value <= self.yellow_threshold:
56+
return KPIStatus.YELLOW
57+
return KPIStatus.RED
58+
59+
60+
@dataclass
61+
class MetricSample:
62+
source: str
63+
kpi_name: str
64+
timestamp: str
65+
value: float
66+
67+
68+
@dataclass
69+
class KPISummary:
70+
name: str
71+
unit: str
72+
sample_count: int
73+
mean: float
74+
p95: float
75+
minimum: float
76+
maximum: float
77+
status: KPIStatus
78+
trend: str # "improving", "stable", "degrading"
79+
80+
81+
# WHY a Dashboard dataclass with count fields? -- Pre-computing red/yellow/green
82+
# counts at assembly time avoids re-iterating the KPI list every time a consumer
83+
# needs the summary. The overall_health field gives a single top-level verdict.
84+
@dataclass
85+
class Dashboard:
86+
title: str
87+
kpis: list[KPISummary] = field(default_factory=list)
88+
red_count: int = 0
89+
yellow_count: int = 0
90+
green_count: int = 0
91+
overall_health: str = "unknown"
92+
93+
94+
# --- Statistical helpers ------------------------------------------------
95+
96+
# WHY nearest-rank percentile? -- It is the simplest percentile method and
97+
# matches what most monitoring dashboards display. More complex interpolation
98+
# methods (e.g. linear) add precision but also complexity that distracts
99+
# from the core lesson of threshold-based evaluation.
100+
def percentile(values: list[float], pct: float) -> float:
101+
if not values:
102+
return 0.0
103+
sorted_v = sorted(values)
104+
# WHY math.ceil? -- Nearest-rank: the smallest value whose rank
105+
# is >= the requested percentile. ceil ensures we never undershoot.
106+
rank = math.ceil(pct / 100.0 * len(sorted_v)) - 1
107+
return sorted_v[max(0, rank)]
108+
109+
110+
# WHY split-half trend detection? -- Comparing first-half mean to second-half
111+
# mean is a lightweight approach that doesn't require scipy or numpy.
112+
# The 10% threshold avoids noise: small fluctuations report "stable".
113+
def compute_trend(values: list[float]) -> str:
114+
# WHY minimum 4 samples? -- With fewer than 4, the halves contain
115+
# 1-2 values each, making the comparison statistically meaningless.
116+
if len(values) < 4:
117+
return "stable"
118+
mid = len(values) // 2
119+
first_mean = sum(values[:mid]) / mid
120+
second_mean = sum(values[mid:]) / (len(values) - mid)
121+
if first_mean == 0:
122+
return "stable"
123+
change_pct = (second_mean - first_mean) / abs(first_mean) * 100
124+
# WHY "lower is better"? -- For latency-style KPIs, a decrease is
125+
# improvement. This convention matches how Grafana and Datadog render trends.
126+
if change_pct < -10:
127+
return "improving"
128+
if change_pct > 10:
129+
return "degrading"
130+
return "stable"
131+
132+
133+
# --- Core logic ---------------------------------------------------------
134+
135+
def load_kpi_definitions(raw: list[dict[str, Any]]) -> list[KPIDefinition]:
136+
# WHY parse into typed objects? -- Working with dicts throughout the
137+
# codebase invites KeyError bugs. Typed dataclasses catch missing fields
138+
# at construction time and give IDE support downstream.
139+
return [
140+
KPIDefinition(
141+
name=d["name"],
142+
unit=d.get("unit", ""),
143+
green_threshold=float(d["green_threshold"]),
144+
yellow_threshold=float(d["yellow_threshold"]),
145+
)
146+
for d in raw
147+
]
148+
149+
150+
def load_metric_samples(raw: list[dict[str, Any]]) -> list[MetricSample]:
151+
return [
152+
MetricSample(
153+
source=s["source"],
154+
kpi_name=s["kpi_name"],
155+
timestamp=s.get("timestamp", ""),
156+
value=float(s["value"]),
157+
)
158+
for s in raw
159+
]
160+
161+
162+
def aggregate_kpi(
163+
definition: KPIDefinition,
164+
samples: list[MetricSample],
165+
) -> KPISummary:
166+
# WHY filter samples by kpi_name here? -- Each call aggregates one KPI.
167+
# Filtering inside the function keeps the caller simple (pass all samples).
168+
values = [s.value for s in samples if s.kpi_name == definition.name]
169+
if not values:
170+
return KPISummary(
171+
name=definition.name, unit=definition.unit,
172+
sample_count=0, mean=0.0, p95=0.0,
173+
minimum=0.0, maximum=0.0,
174+
status=KPIStatus.GREEN, trend="stable",
175+
)
176+
mean_val = sum(values) / len(values)
177+
# WHY evaluate on mean? -- The mean is the primary aggregation for
178+
# threshold comparison. The p95 is reported for context but doesn't
179+
# drive the traffic-light status in this design.
180+
return KPISummary(
181+
name=definition.name,
182+
unit=definition.unit,
183+
sample_count=len(values),
184+
mean=round(mean_val, 2),
185+
p95=round(percentile(values, 95), 2),
186+
minimum=round(min(values), 2),
187+
maximum=round(max(values), 2),
188+
status=definition.evaluate(mean_val),
189+
trend=compute_trend(values),
190+
)
191+
192+
193+
def assemble_dashboard(
194+
title: str,
195+
definitions: list[KPIDefinition],
196+
samples: list[MetricSample],
197+
) -> Dashboard:
198+
dashboard = Dashboard(title=title)
199+
for defn in definitions:
200+
summary = aggregate_kpi(defn, samples)
201+
dashboard.kpis.append(summary)
202+
if summary.status == KPIStatus.RED:
203+
dashboard.red_count += 1
204+
elif summary.status == KPIStatus.YELLOW:
205+
dashboard.yellow_count += 1
206+
else:
207+
dashboard.green_count += 1
208+
209+
# WHY worst-status-wins for overall_health? -- A single red KPI means
210+
# the system needs attention. This mirrors how Grafana dashboards show
211+
# a red banner if any panel is alerting.
212+
if dashboard.red_count > 0:
213+
dashboard.overall_health = "critical"
214+
elif dashboard.yellow_count > 0:
215+
dashboard.overall_health = "warning"
216+
else:
217+
dashboard.overall_health = "healthy"
218+
return dashboard
219+
220+
221+
def dashboard_to_dict(dashboard: Dashboard) -> dict[str, Any]:
222+
# WHY a separate serialization function? -- Keeps the Dashboard dataclass
223+
# free of JSON concerns. You could swap this for Protobuf or MessagePack
224+
# without touching the domain model.
225+
return {
226+
"title": dashboard.title,
227+
"overall_health": dashboard.overall_health,
228+
"counts": {
229+
"red": dashboard.red_count,
230+
"yellow": dashboard.yellow_count,
231+
"green": dashboard.green_count,
232+
},
233+
"kpis": [
234+
{
235+
"name": k.name, "unit": k.unit,
236+
"sample_count": k.sample_count, "mean": k.mean,
237+
"p95": k.p95, "min": k.minimum, "max": k.maximum,
238+
"status": k.status.value, "trend": k.trend,
239+
}
240+
for k in dashboard.kpis
241+
],
242+
}
243+
244+
245+
# --- CLI ----------------------------------------------------------------
246+
247+
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
248+
parser = argparse.ArgumentParser(
249+
description="Assemble KPI data from multiple sources into a dashboard."
250+
)
251+
parser.add_argument("--input", default="data/sample_input.json")
252+
parser.add_argument("--output", default="data/dashboard_output.json")
253+
parser.add_argument("--title", default="Operations Dashboard")
254+
return parser.parse_args(argv)
255+
256+
257+
def main(argv: list[str] | None = None) -> None:
258+
args = parse_args(argv)
259+
input_path = Path(args.input)
260+
if not input_path.exists():
261+
raise SystemExit(f"Input file not found: {input_path}")
262+
263+
raw = json.loads(input_path.read_text(encoding="utf-8"))
264+
definitions = load_kpi_definitions(raw["kpi_definitions"])
265+
samples = load_metric_samples(raw["samples"])
266+
267+
dashboard = assemble_dashboard(args.title, definitions, samples)
268+
output = dashboard_to_dict(dashboard)
269+
270+
out_path = Path(args.output)
271+
out_path.parent.mkdir(parents=True, exist_ok=True)
272+
out_path.write_text(json.dumps(output, indent=2), encoding="utf-8")
273+
print(json.dumps(output, indent=2))
274+
275+
276+
if __name__ == "__main__":
277+
main()
29278
```
30279

31280
## Design decisions
32281

33282
| Decision | Why | Alternative considered |
34283
|----------|-----|----------------------|
35-
| percentile function | [reason] | [alternative] |
36-
| compute_trend function | [reason] | [alternative] |
37-
| load_kpi_definitions function | [reason] | [alternative] |
284+
| Evaluate status on mean, not p95 | Mean gives a stable central measure for threshold comparison; p95 is reported for context | Evaluate on p95 -- better for latency KPIs but overly sensitive for throughput metrics |
285+
| Split-half trend detection | Zero-dependency approach that works without numpy/scipy | Linear regression -- more accurate but adds a heavy dependency for a simple dashboard |
286+
| Worst-status-wins for overall health | A single failing KPI warrants attention; mirrors Grafana/Datadog behaviour | Majority voting -- hides critical issues if most KPIs are green |
287+
| Separate `dashboard_to_dict` function | Decouples domain model from serialization format | `to_dict()` method on Dashboard -- couples serialization to the dataclass |
38288

39289
## Alternative approaches
40290

41-
### Approach B: [Name]
291+
### Approach B: Pandas-based aggregation
42292

43293
```python
44-
# [Different valid approach with trade-offs explained]
294+
import pandas as pd
295+
296+
def aggregate_kpi_pandas(definition, samples_df):
297+
filtered = samples_df[samples_df["kpi_name"] == definition.name]
298+
return {
299+
"mean": filtered["value"].mean(),
300+
"p95": filtered["value"].quantile(0.95),
301+
"min": filtered["value"].min(),
302+
"max": filtered["value"].max(),
303+
}
45304
```
46305

47-
**Trade-off:** [When you would prefer this approach vs the primary one]
306+
**Trade-off:** Pandas makes aggregation one-liners but adds a 30MB dependency. For a dashboard assembler processing thousands of KPIs, the DataFrame overhead could actually be slower than pure Python list comprehensions. Use Pandas when you need complex groupby operations or already have it in the stack.
48307

49-
## What could go wrong
308+
## Common pitfalls
50309

51310
| Scenario | What happens | Prevention |
52311
|----------|-------------|------------|
53-
| [bad input] | [error/behavior] | [how to handle] |
54-
| [edge case] | [behavior] | [how to handle] |
55-
56-
## Key takeaways
57-
58-
1. [Most important lesson from this project]
59-
2. [Second lesson]
60-
3. [Connection to future concepts]
312+
| KPI with zero samples | Division by zero in mean calculation | Return a default KPISummary with 0.0 values and GREEN status |
313+
| Trend with fewer than 4 data points | Split-half comparison is meaningless with 1-2 values per half | Return "stable" for any KPI with fewer than 4 samples |
314+
| first_mean is exactly 0.0 | Division by zero in percentage change calculation | Guard with `if first_mean == 0: return "stable"` |

0 commit comments

Comments
 (0)