
Business Intelligence
- 1.4k installs
- 451 repo stars
- Updated July 21, 2026
- borghei/claude-skills
business-intelligence is an agent skill for design bi dashboards, kpi frameworks, sql analytics, and executive reporting workflows.
About
The business-intelligence skill is designed for design BI dashboards, KPI frameworks, SQL analytics, and executive reporting workflows. Business Intelligence The agent operates as a senior BI specialist, designing dashboards, defining KPI frameworks, automating reporting pipelines, and translating data into executive-ready narratives. Clarify First Before designing the dashboard, confirm these inputs. Invoke when the user designs dashboards, KPIs, SQL analytics, or executive BI reports.
- [ ] Audience — executive, operational, or self-service (sets the layout, altitude, and metric count per page).
- [ ] Limit visualizations per page (5-8 max).
- [ ] Use data extracts or materialized views instead of live connections for heavy dashboards.
- [ ] Minimize calculated fields in the visualization layer; push logic to the semantic layer or warehouse.
- [ ] Apply context filters to reduce query scope.
Business Intelligence by the numbers
- 1,358 all-time installs (skills.sh)
- +23 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #221 of 2,064 Data Science & ML skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
business-intelligence capabilities & compatibility
- Capabilities
- [ ] audience — executive, operational, or self s · [ ] limit visualizations per page (5 8 max) · [ ] use data extracts or materialized views inst · [ ] minimize calculated fields in the visualizat
What business-intelligence says it does
Business intelligence across dashboard design, visualization, and reporting automation. Use when designing dashboards, building KPI frameworks, automating reports, creating data st
Business intelligence across dashboard design, visualization, and reporting automation. Use when designing dashboards, building KPI frameworks, automating repor
npx skills add https://github.com/borghei/claude-skills --skill business-intelligenceAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.4k |
|---|---|
| repo stars | ★ 451 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 21, 2026 |
| Repository | borghei/claude-skills ↗ |
How do I design bi dashboards, kpi frameworks, sql analytics, and executive reporting workflows?
Design BI dashboards, KPI frameworks, SQL analytics, and executive reporting workflows.
Who is it for?
Analysts building KPI dashboards and SQL-driven business intelligence.
Skip if: Skip for ML model training without BI reporting or dashboard scope.
When should I use this skill?
User designs dashboards, KPIs, SQL analytics, or executive BI reports.
What you get
Completed business-intelligence workflow with documented commands, files, and expected deliverables.
- Dashboard layout specification
- Chart and filter mapping
- Optional JSON export
By the numbers
- CLI supports 2-column layout via --layout 2-column flag
Files
Business Intelligence
The agent operates as a senior BI specialist, designing dashboards, defining KPI frameworks, automating reporting pipelines, and translating data into executive-ready narratives.
Clarify First
Before designing the dashboard, confirm these inputs. If any is unknown or vague, ASK — do not assume:
- [ ] Audience — executive, operational, or self-service (sets the layout, altitude, and metric count per page)
- [ ] Key questions + refresh cadence — what decisions the dashboard drives and how fresh the data must be (scopes the metrics and the live-vs-extract choice)
- [ ] KPI definitions — formula, data source, owner, and RAG thresholds per metric (these are the exact fields the KPI template and
metric_validator.pyrequire)
Stop rule: ask only the 2-3 that most change the output. If the user says "just draft it," proceed and list your assumptions at the top of the artifact.
Workflow
1. Clarify the reporting need -- Identify the audience (executive, operational, self-service), the key questions the dashboard must answer, and the refresh cadence. Validate that required data sources exist and are accessible. 2. Define KPIs and metrics -- For each metric, specify the formula, data source, granularity, owner, and RAG thresholds using the KPI definition template below. 3. Design the dashboard layout -- Apply the visual hierarchy (most important metric top-left, summary-to-detail flow top-to-bottom). Select chart types using the chart selection matrix. Limit to 5-8 visualizations per page. 4. Build the semantic layer -- Define metric calculations, hierarchies, and row-level security in the BI tool's semantic model so consumers get consistent numbers. 5. Automate reporting -- Configure scheduled delivery (PDF/email, Slack alerts) and threshold-based alerts with the patterns below. 6. Validate and iterate -- Confirm KPI values match source-of-truth queries. Check dashboard load time (<5 s target). Gather stakeholder feedback and refine.
KPI Definition Template
# Copy and fill for each metric
kpi:
name: "Monthly Recurring Revenue"
owner: "Finance"
purpose: "Track subscription revenue health"
formula: "SUM(subscription_amount) WHERE status = 'active'"
data_source: "billing.subscriptions"
granularity: "monthly"
target: 1200000
warning_threshold: 1080000 # 90% of target
critical_threshold: 960000 # 80% of target
dimensions: ["region", "plan_tier", "cohort_month"]
caveats:
- "Excludes one-time setup fees"
- "Currency normalized to USD at month-end rate"Dashboard Design Principles
Visual hierarchy: 1. Most important metrics at top-left 2. Summary cards flow into trend charts flow into detail tables (top to bottom) 3. Related metrics grouped; white space separates logical sections 4. RAG status colors: Green #28A745 | Yellow #FFC107 | Red #DC3545 | Gray #6C757D
Chart selection matrix:
| Data question | Chart type | Alternative |
|---|---|---|
| Trend over time | Line | Area |
| Part of whole | Donut / Treemap | Stacked bar |
| Comparison across categories | Bar / Column | Bullet |
| Distribution | Histogram | Box plot |
| Relationship | Scatter | Bubble |
| Geographic | Choropleth | Filled map |
Executive Dashboard Example
+------------------------------------------------------------+
| EXECUTIVE SUMMARY |
| Revenue: $12.4M (+15% YoY) Pipeline: $45.2M (+22% QoQ) |
| Customers: 2,847 (+340 MTD) NPS: 72 (+5 pts) |
+------------------------------------------------------------+
| REVENUE TREND (12-mo line) | REVENUE BY SEGMENT (donut) |
+-------------------------------+-----------------------------+
| TOP 10 ACCOUNTS (table) | KPI STATUS (RAG cards) |
+-------------------------------+-----------------------------+Report Automation Patterns
Scheduled report (cron-style):
report:
name: Weekly Sales Report
schedule: "0 8 * * MON"
recipients: [sales-team@company.com, leadership@company.com]
format: PDF
pages: [Executive Summary, Pipeline Analysis, Rep Performance]Threshold alert:
alert:
name: Revenue Below Target
metric: daily_revenue
condition: "actual < target * 0.9"
channels:
email: finance@company.com
slack: "#revenue-alerts"
message: "Daily revenue ${actual} is ${pct_diff}% below target. Top factors: ${top_factors}"Automated generation workflow (Python):
def generate_report(config: dict) -> str:
"""Generate and distribute a scheduled report."""
# 1. Refresh data sources
refresh_data_sources(config["sources"])
# 2. Calculate metrics
metrics = calculate_metrics(config["metrics"])
# 3. Create visualizations
charts = create_visualizations(metrics, config["charts"])
# 4. Compile into report
report = compile_report(metrics=metrics, charts=charts, template=config["template"])
# 5. Distribute
distribute_report(report, recipients=config["recipients"], fmt=config["format"])
return report.pathSelf-Service BI Maturity Model
| Level | Capability | Users can... |
|---|---|---|
| 1 - Consumers | View & filter | Open dashboards, apply filters, export data |
| 2 - Explorers | Ad-hoc queries | Write simple queries, create basic charts, share findings |
| 3 - Builders | Design dashboards | Combine data sources, create calculated fields, publish reports |
| 4 - Modelers | Define data models | Create semantic models, define metrics, optimize performance |
Performance Optimization Checklist
- [ ] Limit visualizations per page (5-8 max)
- [ ] Use data extracts or materialized views instead of live connections for heavy dashboards
- [ ] Minimize calculated fields in the visualization layer; push logic to the semantic layer or warehouse
- [ ] Apply context filters to reduce query scope
- [ ] Aggregate at source when granularity allows
- [ ] Schedule data refreshes during off-peak hours
- [ ] Monitor and log query execution times; target < 5 s per dashboard load
Query optimization example:
-- Before: full table scan
SELECT * FROM large_table WHERE date >= '2024-01-01';
-- After: partitioned, filtered, and column-pruned
SELECT order_id, customer_id, amount
FROM large_table
WHERE partition_date >= '2024-01-01'
AND status = 'active'
LIMIT 10000;Data Storytelling Structure
The agent frames every insight using Situation-Complication-Resolution:
1. Situation -- "Last quarter we targeted 10% retention improvement." 2. Complication -- "Enterprise churn rose 5%, driven by 30-day onboarding delays." 3. Resolution -- "Reducing onboarding to 14 days correlates with 40% lower churn and could save $2M annually."
Governance
security_model:
row_level_security:
- rule: region_access
filter: "region = user.region"
object_permissions:
- role: viewer
permissions: [view, export]
- role: editor
permissions: [view, export, edit]
- role: admin
permissions: [view, export, edit, delete, publish]Reference Materials
references/dashboard_patterns.md-- Dashboard design patternsreferences/visualization_guide.md-- Chart selection guidereferences/kpi_library.md-- Standard KPI definitionsreferences/storytelling.md-- Data storytelling techniques
Scripts
python scripts/kpi_tracker.py --definitions kpis.json --data sales.csv
python scripts/kpi_tracker.py --definitions kpis.json --data sales.csv --json
python scripts/dashboard_spec_generator.py --definitions kpis.json --title "Sales Dashboard"
python scripts/dashboard_spec_generator.py --definitions kpis.json --layout 3-column --json
python scripts/metric_validator.py --definitions metrics.json --strict
python scripts/metric_validator.py --definitions metrics.json --jsonTool Reference
| Tool | Purpose | Key Flags |
|---|---|---|
kpi_tracker.py | Calculate KPIs from data against targets; report RAG status and variance | --definitions <json>, --data <csv/json>, --json |
dashboard_spec_generator.py | Generate dashboard layout specs (chart types, positions, filters) from KPI definitions | --definitions <json>, --title, --layout 2-column/3-column, --json |
metric_validator.py | Validate metric definitions for completeness, naming, threshold logic, and consistency | --definitions <json>, --strict, --json |
Troubleshooting
| Problem | Likely Cause | Resolution |
|---|---|---|
| Dashboard loads slowly (> 5 s) | Too many visualizations or live-connection queries hitting raw tables | Reduce widgets to 5-8 per page; switch to extracts or materialized views for heavy dashboards |
| KPI values differ between dashboard and source query | Dashboard applies additional filters, currency conversion, or calculated fields not in the semantic layer | Centralize all metric logic in the semantic layer; remove dashboard-level computed fields |
| RAG thresholds trigger false alerts | Warning/critical percentages are miscalibrated for seasonal patterns | Adjust thresholds per season or use rolling baselines; validate with metric_validator.py --strict |
| Stakeholders ignore dashboards | Dashboard answers the wrong questions or lacks actionable context | Redesign using the Situation-Complication-Resolution storytelling framework; add annotations and targets |
| Row-level security hides data unexpectedly | Security rules are too broad or user-role mapping is incorrect | Audit RLS rules; test with a sample user from each role; log filtered row counts |
| Scheduled report emails land in spam | Large PDF attachments or sender reputation issues | Reduce attachment size; switch to embedded links; work with IT to whitelist the sender domain |
metric_validator.py reports formula-aggregation mismatch | The formula field (e.g., "SUM(...)") does not match the declared aggregation | Align the two fields; the aggregation field drives the tool while the formula documents intent |
Success Criteria
- Dashboard load time is under 5 seconds for 95% of page views.
- KPI definitions pass
metric_validator.py --strictwith zero errors before production deployment. - Executive dashboards follow the visual hierarchy: summary cards at top-left, trends in the middle, detail tables at the bottom.
- Every KPI has a defined owner, target, and RAG thresholds documented in the definitions file.
- Self-service BI adoption reaches Level 2 (Explorers) for at least 60% of target users within 90 days.
- Scheduled reports are delivered within 15 minutes of the configured schedule window.
- Data storytelling follows the What / So What / Now What structure with quantified impact in every insight.
Scope & Limitations
In scope: Dashboard design and layout, KPI framework definition, report automation patterns, data storytelling, self-service BI enablement, row-level security configuration, and visualization best practices.
Out of scope: Data warehouse infrastructure, ETL/ELT pipeline development, raw data ingestion, machine learning model building, and BI tool installation or licensing.
Limitations: The Python tools (kpi_tracker.py, dashboard_spec_generator.py, metric_validator.py) operate on local JSON and CSV files only -- they do not connect to live databases or BI platforms. All scripts use the Python standard library with no external dependencies. Dashboard specifications are platform-agnostic and require manual translation to specific BI tools (Tableau, Power BI, Looker, etc.).
Integration Points
- Analytics Engineer (
data-analytics/analytics-engineer): Provides the mart models and semantic-layer metrics that dashboards consume; schema changes require dashboard updates. - Data Analyst (
data-analytics/data-analyst): Creates ad-hoc analyses that may evolve into repeatable dashboards; shares visualization standards. - Product Team (
product-team/): Defines product KPIs and user-facing analytics requirements. - C-Level Advisor (
c-level-advisor/): Executive dashboards translate strategic objectives into measurable KPIs. - Finance (
finance/): Financial KPIs (MRR, CAC, LTV) require alignment between BI dashboards and finance team definitions.
#!/usr/bin/env python3
"""Generate dashboard layout specifications from KPI definitions.
Reads a KPI definitions file and produces a structured dashboard
specification with chart types, layout positions, filters, and
recommended visualizations.
Usage:
python dashboard_spec_generator.py --definitions kpis.json
python dashboard_spec_generator.py --definitions kpis.json --title "Sales Dashboard" --json
python dashboard_spec_generator.py --definitions kpis.json --layout 2-column
KPI definition format:
[
{
"name": "Monthly Revenue",
"type": "currency",
"aggregation": "sum",
"dimensions": ["region", "product"],
"time_grain": "monthly",
"target": 100000,
"chart_hint": "trend"
}
]
"""
import argparse
import json
import os
import sys
from datetime import datetime
# ---------------------------------------------------------------------------
# Chart type selection logic
# ---------------------------------------------------------------------------
CHART_RULES = {
"trend": {"chart": "line", "alt": "area", "reason": "Shows metric change over time"},
"comparison": {"chart": "bar", "alt": "column", "reason": "Compares values across categories"},
"composition": {"chart": "donut", "alt": "stacked_bar", "reason": "Shows parts of a whole"},
"distribution": {"chart": "histogram", "alt": "box_plot", "reason": "Shows data distribution"},
"kpi_card": {"chart": "scorecard", "alt": "gauge", "reason": "Highlights a single key metric"},
"table": {"chart": "data_table", "alt": "pivot_table", "reason": "Shows detailed records"},
"geographic": {"chart": "choropleth", "alt": "bubble_map", "reason": "Maps data geographically"},
}
def _infer_chart_type(kpi: dict) -> str:
"""Infer best chart type from KPI definition."""
if kpi.get("chart_hint"):
return kpi["chart_hint"]
agg = kpi.get("aggregation", "sum")
dims = kpi.get("dimensions", [])
time_grain = kpi.get("time_grain")
if time_grain and not dims:
return "trend"
if time_grain and dims:
return "trend" # trend with dimension breakdown
if len(dims) == 0:
return "kpi_card"
if len(dims) == 1:
return "comparison"
if any(d in ("region", "country", "state", "city") for d in dims):
return "geographic"
return "comparison"
def _generate_widget(kpi: dict, position: int, layout: str) -> dict:
chart_key = _infer_chart_type(kpi)
chart_info = CHART_RULES.get(chart_key, CHART_RULES["kpi_card"])
# Calculate grid position
if layout == "2-column":
cols = 2
elif layout == "3-column":
cols = 3
else:
cols = 2
row = position // cols
col = position % cols
widget = {
"id": f"widget_{position + 1}",
"title": kpi["name"],
"chart_type": chart_info["chart"],
"alt_chart_type": chart_info["alt"],
"chart_rationale": chart_info["reason"],
"metric": {
"name": kpi["name"],
"aggregation": kpi.get("aggregation", "sum"),
"type": kpi.get("type", "number"),
},
"layout": {
"row": row,
"col": col,
"width": 1,
"height": 1,
},
}
if kpi.get("dimensions"):
widget["dimensions"] = kpi["dimensions"]
if kpi.get("time_grain"):
widget["time_grain"] = kpi["time_grain"]
if kpi.get("target") is not None:
widget["target"] = kpi["target"]
widget["show_target_line"] = True
if kpi.get("filters"):
widget["filters"] = kpi["filters"]
return widget
def generate_dashboard_spec(kpi_defs: list, title: str, layout: str) -> dict:
widgets = []
# KPI cards come first (top row)
kpi_cards = []
detail_widgets = []
for kpi in kpi_defs:
chart_type = _infer_chart_type(kpi)
if chart_type == "kpi_card":
kpi_cards.append(kpi)
else:
detail_widgets.append(kpi)
position = 0
for kpi in kpi_cards:
w = _generate_widget(kpi, position, layout)
w["section"] = "summary"
widgets.append(w)
position += 1
for kpi in detail_widgets:
w = _generate_widget(kpi, position, layout)
w["section"] = "detail"
widgets.append(w)
position += 1
# Global filters
all_dims = set()
for kpi in kpi_defs:
all_dims.update(kpi.get("dimensions", []))
time_grains = set()
for kpi in kpi_defs:
if kpi.get("time_grain"):
time_grains.add(kpi["time_grain"])
filters = []
if time_grains:
filters.append({
"name": "date_range",
"type": "date_range",
"default": "last_30_days",
})
for dim in sorted(all_dims):
filters.append({
"name": dim,
"type": "multi_select",
"default": "all",
})
spec = {
"dashboard": {
"title": title,
"layout": layout,
"generated_at": datetime.now().strftime("%Y-%m-%dT%H:%M:%S"),
"total_widgets": len(widgets),
"sections": {
"summary": sum(1 for w in widgets if w.get("section") == "summary"),
"detail": sum(1 for w in widgets if w.get("section") == "detail"),
},
},
"global_filters": filters,
"widgets": widgets,
"design_notes": [
"Place summary KPI cards in the top row for immediate visibility.",
"Trend charts should show comparison to target or prior period.",
"Limit dashboard to 5-8 widgets per page for readability.",
"Use consistent color palette: GREEN #28A745, YELLOW #FFC107, RED #DC3545.",
"Dashboard load time target: < 5 seconds.",
],
}
return spec
def _format_text(spec: dict) -> str:
lines = []
d = spec["dashboard"]
lines.append(f"Dashboard Specification: {d['title']}")
lines.append("=" * 60)
lines.append(f"Layout: {d['layout']} | Widgets: {d['total_widgets']} | Generated: {d['generated_at']}")
lines.append(f"Summary cards: {d['sections']['summary']} | Detail charts: {d['sections']['detail']}")
lines.append("")
if spec.get("global_filters"):
lines.append("Global Filters:")
for f in spec["global_filters"]:
lines.append(f" - {f['name']} ({f['type']}, default: {f['default']})")
lines.append("")
lines.append("Widgets:")
lines.append("-" * 60)
for w in spec["widgets"]:
target_str = f" Target: {w['target']}" if w.get("target") else ""
dims_str = f" Dimensions: {', '.join(w['dimensions'])}" if w.get("dimensions") else ""
lines.append(f" [{w['id']}] {w['title']}")
lines.append(f" Chart: {w['chart_type']} (alt: {w['alt_chart_type']})")
lines.append(f" Reason: {w['chart_rationale']}")
lines.append(f" Position: row={w['layout']['row']}, col={w['layout']['col']}")
if target_str:
lines.append(f" {target_str}")
if dims_str:
lines.append(f" {dims_str}")
if w.get("time_grain"):
lines.append(f" Time grain: {w['time_grain']}")
lines.append("")
lines.append("Design Notes:")
for note in spec.get("design_notes", []):
lines.append(f" - {note}")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(description="Generate dashboard layout specifications from KPI definitions.")
parser.add_argument("--definitions", required=True, help="Path to KPI definitions JSON file")
parser.add_argument("--title", default="Analytics Dashboard", help="Dashboard title")
parser.add_argument("--layout", choices=["2-column", "3-column"], default="2-column", help="Layout style")
parser.add_argument("--json", action="store_true", help="Output as JSON")
args = parser.parse_args()
if not os.path.exists(args.definitions):
print(f"Error: File not found: {args.definitions}", file=sys.stderr)
sys.exit(1)
with open(args.definitions, "r") as f:
kpi_defs = json.load(f)
if not isinstance(kpi_defs, list) or not kpi_defs:
print("Error: Definitions file must contain a non-empty JSON array.", file=sys.stderr)
sys.exit(1)
spec = generate_dashboard_spec(kpi_defs, args.title, args.layout)
if args.json:
print(json.dumps(spec, indent=2))
else:
print(_format_text(spec))
sys.exit(0)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Calculate and track KPIs from data files against targets and thresholds.
Reads a KPI definition file (JSON) and a data file (CSV/JSON), computes
each metric, evaluates RAG status, and reports trends.
Usage:
python kpi_tracker.py --definitions kpis.json --data sales.csv
python kpi_tracker.py --definitions kpis.json --data sales.csv --json
python kpi_tracker.py --definitions kpis.json --data sales.csv --period monthly
KPI definition format (kpis.json):
[
{
"name": "Monthly Revenue",
"column": "revenue",
"aggregation": "sum",
"target": 100000,
"warning_pct": 0.9,
"critical_pct": 0.8,
"higher_is_better": true
}
]
"""
import argparse
import csv
import json
import math
import os
import sys
from collections import defaultdict
# ---------------------------------------------------------------------------
# Aggregation functions
# ---------------------------------------------------------------------------
def _agg_sum(values: list) -> float:
return sum(values)
def _agg_mean(values: list) -> float:
return sum(values) / len(values) if values else 0
def _agg_count(values: list) -> float:
return float(len(values))
def _agg_count_distinct(values: list) -> float:
return float(len(set(values)))
def _agg_min(values: list) -> float:
return min(values) if values else 0
def _agg_max(values: list) -> float:
return max(values) if values else 0
def _agg_median(values: list) -> float:
s = sorted(values)
n = len(s)
if n == 0:
return 0.0
mid = n // 2
return (s[mid - 1] + s[mid]) / 2.0 if n % 2 == 0 else s[mid]
AGG_MAP = {
"sum": _agg_sum,
"mean": _agg_mean,
"average": _agg_mean,
"count": _agg_count,
"count_distinct": _agg_count_distinct,
"min": _agg_min,
"max": _agg_max,
"median": _agg_median,
}
# ---------------------------------------------------------------------------
# Core
# ---------------------------------------------------------------------------
def load_data(file_path: str) -> list:
ext = os.path.splitext(file_path)[1].lower()
if ext == ".csv":
with open(file_path, "r", newline="") as f:
return list(csv.DictReader(f))
elif ext == ".json":
with open(file_path, "r") as f:
data = json.load(f)
return data if isinstance(data, list) else []
else:
print(f"Error: Unsupported file type '{ext}'.", file=sys.stderr)
sys.exit(1)
def _extract_numeric(data: list, column: str) -> list:
values = []
for row in data:
v = row.get(column)
if v is not None:
try:
values.append(float(str(v)))
except ValueError:
pass
return values
def _extract_raw(data: list, column: str) -> list:
return [str(row.get(column, "")) for row in data if row.get(column) is not None]
def _rag_status(actual: float, target: float, warning_pct: float, critical_pct: float, higher_is_better: bool) -> str:
if higher_is_better:
if actual >= target:
return "GREEN"
elif actual >= target * warning_pct:
return "YELLOW"
elif actual >= target * critical_pct:
return "YELLOW"
else:
return "RED"
else:
if actual <= target:
return "GREEN"
elif actual <= target / warning_pct:
return "YELLOW"
else:
return "RED"
def compute_kpi(kpi_def: dict, data: list) -> dict:
name = kpi_def["name"]
column = kpi_def["column"]
agg_name = kpi_def.get("aggregation", "sum")
target = kpi_def.get("target")
warning_pct = kpi_def.get("warning_pct", 0.9)
critical_pct = kpi_def.get("critical_pct", 0.8)
higher_is_better = kpi_def.get("higher_is_better", True)
agg_fn = AGG_MAP.get(agg_name)
if not agg_fn:
return {"name": name, "error": f"Unknown aggregation: {agg_name}"}
if agg_name == "count_distinct":
raw = _extract_raw(data, column)
actual = agg_fn(raw)
else:
values = _extract_numeric(data, column)
if not values:
return {"name": name, "error": f"No numeric values found in column '{column}'."}
actual = agg_fn(values)
result = {
"name": name,
"column": column,
"aggregation": agg_name,
"actual": round(actual, 2),
}
if target is not None:
result["target"] = target
result["variance"] = round(actual - target, 2)
result["variance_pct"] = round((actual - target) / target * 100, 2) if target != 0 else 0
result["status"] = _rag_status(actual, target, warning_pct, critical_pct, higher_is_better)
else:
result["status"] = "NO_TARGET"
return result
def compute_all_kpis(kpi_defs: list, data: list) -> dict:
results = []
for kpi_def in kpi_defs:
results.append(compute_kpi(kpi_def, data))
green = sum(1 for r in results if r.get("status") == "GREEN")
yellow = sum(1 for r in results if r.get("status") == "YELLOW")
red = sum(1 for r in results if r.get("status") == "RED")
return {
"total_kpis": len(results),
"green": green,
"yellow": yellow,
"red": red,
"health_score": round(green / len(results) * 100, 1) if results else 0,
"kpis": results,
}
def main():
parser = argparse.ArgumentParser(description="Calculate and track KPIs against targets.")
parser.add_argument("--definitions", required=True, help="Path to KPI definitions JSON file")
parser.add_argument("--data", required=True, help="Path to data file (CSV or JSON)")
parser.add_argument("--json", action="store_true", help="Output as JSON")
args = parser.parse_args()
for path, label in [(args.definitions, "Definitions"), (args.data, "Data")]:
if not os.path.exists(path):
print(f"Error: {label} file not found: {path}", file=sys.stderr)
sys.exit(1)
with open(args.definitions, "r") as f:
kpi_defs = json.load(f)
if not isinstance(kpi_defs, list):
print("Error: KPI definitions must be a JSON array.", file=sys.stderr)
sys.exit(1)
data = load_data(args.data)
if not data:
print("Error: No data rows found.", file=sys.stderr)
sys.exit(1)
report = compute_all_kpis(kpi_defs, data)
if args.json:
print(json.dumps(report, indent=2))
else:
print("KPI Tracker Report")
print("=" * 65)
print(f"KPIs: {report['total_kpis']} | GREEN: {report['green']} YELLOW: {report['yellow']} RED: {report['red']} | Health: {report['health_score']}%")
print()
print(f"{'KPI':<30} {'Actual':>12} {'Target':>12} {'Var %':>8} {'Status':<8}")
print("-" * 65)
for kpi in report["kpis"]:
if "error" in kpi:
print(f" {kpi['name']:<28} ERROR: {kpi['error']}")
continue
target_str = str(kpi.get("target", "-"))
var_str = f"{kpi.get('variance_pct', 0):+.1f}%" if "variance_pct" in kpi else "-"
status = kpi.get("status", "?")
marker = {"GREEN": "[OK]", "YELLOW": "[!!]", "RED": "[XX]"}.get(status, "[ ]")
print(f" {kpi['name']:<28} {kpi['actual']:>12,.2f} {target_str:>12} {var_str:>8} {marker}")
sys.exit(0)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Validate metric definitions for consistency, completeness, and correctness.
Reads a metric definitions file (JSON) and checks for common issues:
missing fields, conflicting aggregations, naming violations, undefined
dimensions, and threshold logic errors.
Usage:
python metric_validator.py --definitions metrics.json
python metric_validator.py --definitions metrics.json --strict --json
Metric definition format:
[
{
"name": "Monthly Revenue",
"formula": "SUM(amount)",
"data_source": "orders",
"column": "amount",
"aggregation": "sum",
"owner": "Finance",
"dimensions": ["region", "product_line"],
"target": 100000,
"warning_pct": 0.9,
"critical_pct": 0.8,
"higher_is_better": true,
"granularity": "monthly"
}
]
"""
import argparse
import json
import os
import re
import sys
# ---------------------------------------------------------------------------
# Validation rules
# ---------------------------------------------------------------------------
REQUIRED_FIELDS = ["name", "aggregation", "data_source"]
RECOMMENDED_FIELDS = ["owner", "formula", "granularity", "dimensions"]
VALID_AGGREGATIONS = {"sum", "mean", "average", "count", "count_distinct", "min", "max", "median", "ratio", "rate"}
VALID_GRANULARITIES = {"daily", "weekly", "monthly", "quarterly", "yearly", "hourly", "real_time"}
NAME_PATTERN = re.compile(r"^[A-Za-z][A-Za-z0-9 _-]{2,60}$")
def _validate_metric(metric: dict, index: int, strict: bool = False) -> list:
issues = []
prefix = f"Metric #{index + 1}"
name = metric.get("name", f"(unnamed #{index + 1})")
# Required fields
for field in REQUIRED_FIELDS:
if not metric.get(field):
issues.append({
"metric": name,
"severity": "error",
"rule": "MISSING_REQUIRED_FIELD",
"message": f"Missing required field '{field}'.",
})
# Recommended fields
if strict:
for field in RECOMMENDED_FIELDS:
if not metric.get(field):
issues.append({
"metric": name,
"severity": "warning",
"rule": "MISSING_RECOMMENDED_FIELD",
"message": f"Missing recommended field '{field}'.",
})
# Name format
if metric.get("name") and not NAME_PATTERN.match(metric["name"]):
issues.append({
"metric": name,
"severity": "warning",
"rule": "INVALID_NAME_FORMAT",
"message": "Metric name should be 3-60 chars, start with a letter, and use only alphanumeric, spaces, hyphens, or underscores.",
})
# Aggregation
agg = metric.get("aggregation", "").lower()
if agg and agg not in VALID_AGGREGATIONS:
issues.append({
"metric": name,
"severity": "error",
"rule": "INVALID_AGGREGATION",
"message": f"Aggregation '{agg}' is not recognized. Valid: {', '.join(sorted(VALID_AGGREGATIONS))}.",
})
# Granularity
gran = metric.get("granularity", "").lower()
if gran and gran not in VALID_GRANULARITIES:
issues.append({
"metric": name,
"severity": "warning",
"rule": "INVALID_GRANULARITY",
"message": f"Granularity '{gran}' is not standard. Valid: {', '.join(sorted(VALID_GRANULARITIES))}.",
})
# Threshold logic
target = metric.get("target")
warning_pct = metric.get("warning_pct")
critical_pct = metric.get("critical_pct")
higher = metric.get("higher_is_better", True)
if target is not None:
if warning_pct is not None and critical_pct is not None:
if higher:
if warning_pct <= critical_pct:
issues.append({
"metric": name,
"severity": "error",
"rule": "THRESHOLD_ORDER",
"message": f"warning_pct ({warning_pct}) should be > critical_pct ({critical_pct}) when higher_is_better=true.",
})
if warning_pct is not None and (warning_pct <= 0 or warning_pct > 1):
issues.append({
"metric": name,
"severity": "error",
"rule": "THRESHOLD_RANGE",
"message": f"warning_pct ({warning_pct}) must be between 0 and 1.",
})
if critical_pct is not None and (critical_pct <= 0 or critical_pct > 1):
issues.append({
"metric": name,
"severity": "error",
"rule": "THRESHOLD_RANGE",
"message": f"critical_pct ({critical_pct}) must be between 0 and 1.",
})
# Dimensions validation
dims = metric.get("dimensions", [])
if not isinstance(dims, list):
issues.append({
"metric": name,
"severity": "error",
"rule": "INVALID_DIMENSIONS",
"message": "Dimensions must be a list.",
})
elif len(dims) > 10:
issues.append({
"metric": name,
"severity": "warning",
"rule": "EXCESSIVE_DIMENSIONS",
"message": f"Metric has {len(dims)} dimensions; consider limiting to <=10 for dashboard usability.",
})
# Formula vs aggregation consistency
formula = metric.get("formula", "").upper()
if formula and agg:
expected_agg = None
if formula.startswith("SUM("):
expected_agg = "sum"
elif formula.startswith("AVG(") or formula.startswith("AVERAGE("):
expected_agg = "mean"
elif formula.startswith("COUNT(DISTINCT"):
expected_agg = "count_distinct"
elif formula.startswith("COUNT("):
expected_agg = "count"
if expected_agg and expected_agg != agg:
issues.append({
"metric": name,
"severity": "warning",
"rule": "FORMULA_AGG_MISMATCH",
"message": f"Formula suggests '{expected_agg}' but aggregation is set to '{agg}'.",
})
return issues
def validate_all(metrics: list, strict: bool = False) -> dict:
all_issues = []
for i, m in enumerate(metrics):
all_issues.extend(_validate_metric(m, i, strict))
# Cross-metric checks: duplicate names
names = [m.get("name", "") for m in metrics]
seen = set()
for n in names:
if n in seen:
all_issues.append({
"metric": n,
"severity": "error",
"rule": "DUPLICATE_NAME",
"message": f"Metric name '{n}' appears more than once.",
})
seen.add(n)
errors = sum(1 for i in all_issues if i["severity"] == "error")
warnings = sum(1 for i in all_issues if i["severity"] == "warning")
return {
"total_metrics": len(metrics),
"total_issues": len(all_issues),
"errors": errors,
"warnings": warnings,
"valid": errors == 0,
"issues": all_issues,
}
def main():
parser = argparse.ArgumentParser(description="Validate metric definitions for consistency and completeness.")
parser.add_argument("--definitions", required=True, help="Path to metric definitions JSON file")
parser.add_argument("--strict", action="store_true", help="Enable strict mode (flag missing recommended fields)")
parser.add_argument("--json", action="store_true", help="Output as JSON")
args = parser.parse_args()
if not os.path.exists(args.definitions):
print(f"Error: File not found: {args.definitions}", file=sys.stderr)
sys.exit(1)
with open(args.definitions, "r") as f:
metrics = json.load(f)
if not isinstance(metrics, list):
print("Error: Definitions must be a JSON array.", file=sys.stderr)
sys.exit(1)
result = validate_all(metrics, args.strict)
if args.json:
print(json.dumps(result, indent=2))
else:
print("Metric Validation Report")
print("=" * 55)
status = "PASS" if result["valid"] else "FAIL"
print(f"Status: [{status}] | Metrics: {result['total_metrics']} | Errors: {result['errors']} Warnings: {result['warnings']}")
print()
if not result["issues"]:
print("All metric definitions are valid.")
else:
for issue in result["issues"]:
sev = issue["severity"].upper()
print(f" [{sev}] {issue['metric']}: {issue['rule']}")
print(f" {issue['message']}")
sys.exit(1 if result["errors"] > 0 else 0)
if __name__ == "__main__":
main()
Related skills
How it compares
Use business-intelligence for layout spec generation from KPI JSON, not for running queries or building ETL.
FAQ
What does business-intelligence do?
Design BI dashboards, KPI frameworks, SQL analytics, and executive reporting workflows.
When should I use business-intelligence?
User designs dashboards, KPIs, SQL analytics, or executive BI reports.
Is business-intelligence safe to install?
Review the Security Audits panel on this page before installing in production.