Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Build KPI dashboards for construction projects. Track CPI, SPI, quality, safety metrics in real-time.
.claude/skills/datadrivenconstruction-kpi-dashboard/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-03 | ✗→✓ | ▲ Improved | 26% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 665% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 884% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 396% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 697% | 0% |
Project monitoring challenges:
Unified KPI dashboard system for construction projects with automated data collection, visualization, and alerting.
pythonimport pandas as pd from typing import Dict, Any, List, Optional, Callable from dataclasses import dataclass, field from datetime import date, datetime from enum import Enum class KPICategory(Enum): COST = "cost" SCHEDULE = "schedule" QUALITY = "quality" SAFETY = "safety" PRODUCTIVITY = "productivity" SUSTAINABILITY = "sustainability" class KPIStatus(Enum): ON_TARGET = "on_target" AT_RISK = "at_risk" CRITICAL = "critical" class TrendDirection(Enum): IMPROVING = "improving" STABLE = "stable" DECLINING = "declining" @dataclass class KPIDefinition: kpi_id: str name: str category: KPICategory unit: str target: float warning_threshold: float critical_threshold: float higher_is_better: bool = True formula: str = "" @dataclass class KPIValue: kpi_id: str value: float date: date status: KPIStatus trend: TrendDirection class KPIDashboard: """Build and manage KPI dashboards for construction projects.""" def __init__(self, project_name: str): self.project_name = project_name self.kpis: Dict[str, KPIDefinition] = {} self.history: Dict[str, List[KPIValue]] = {} self._define_standard_kpis() def _define_standard_kpis(self): """Define standard construction KPIs.""" standard_kpis = [ # Cost KPIs KPIDefinition("CPI", "Cost Performance Index", KPICategory.COST, "ratio", 1.0, 0.95, 0.90, True, "BCWP / ACWP"), KPIDefinition("CV", "Cost Variance", KPICategory.COST, "$", 0, -50000, -100000, True, "BCWP - ACWP"), KPIDefinition("BUDGET_USED", "Budget Utilization", KPICategory.COST, "%", 100, 105, 110, False), # Schedule KPIs KPIDefinition("SPI", "Schedule Performance Index", KPICategory.SCHEDULE, "ratio", 1.0, 0.95, 0.90, True, "BCWP / BCWS"), KPIDefinition("SV", "Schedule Variance", KPICategory.SCHEDULE, "days", 0, -7, -14, True), KPIDefinition("COMPLETION", "Project Completion", KPICategory.SCHEDULE, "%", 100, 95, 90, True), # Quality KPIs KPIDefinition("DEFECT_RATE", "Defect Rate", KPICategory.QUALITY, "per 1000 units", 0, 5, 10, False), KPIDefinition("FIRST_PASS", "First Pass Yield", KPICategory.QUALITY, "%", 95, 90, 85, True), KPIDefinition("REWORK", "Rework Percentage", KPICategory.QUALITY, "%", 0, 3, 5, False), # Safety KPIs KPIDefinition("TRIR", "Total Recordable Incident Rate", KPICategory.SAFETY, "per 200k hours", 0, 2, 4, False), KPIDefinition("LOST_DAYS", "Lost Time Injuries", KPICategory.SAFETY, "incidents", 0, 1, 3, False), KPIDefinition("SAFETY_OBSERVATIONS", "Safety Observations", KPICategory.SAFETY, "count", 50, 30, 20, True), # Productivity KPIs KPIDefinition("LABOR_PROD", "Labor Productivity", KPICategory.PRODUCTIVITY, "%", 100, 90, 80, True), KPIDefinition("EQUIP_UTIL", "Equipment Utilization", KPICategory.PRODUCTIVITY, "%", 85, 70, 60, True), ] for kpi in standard_kpis: self.kpis[kpi.kpi_id] = kpi self.history[kpi.kpi_id] = [] def add_custom_kpi(self, kpi: KPIDefinition): """Add custom KPI definition.""" self.kpis[kpi.kpi_id] = kpi self.history[kpi.kpi_id] = [] def record_value(self, kpi_id: str, value: float, record_date: date = None): """Record KPI value.""" if kpi_id not in self.kpis: return kpi = self.kpis[kpi_id] record_date = record_date or date.today() # Calculate status status = self._calculate_status(kpi, value) # Calculate trend trend = self._calculate_trend(kpi_id, value) kpi_value = KPIValue( kpi_id=kpi_id, value=value, date=record_date, status=status, trend=trend ) self.history[kpi_id].append(kpi_value) def _calculate_status(self, kpi: KPIDefinition, value: float) -> KPIStatus: """Calculate KPI status based on thresholds.""" if kpi.higher_is_better: if value >= kpi.target: return KPIStatus.ON_TARGET elif value >= kpi.warning_threshold: return KPIStatus.AT_RISK else: return KPIStatus.CRITICAL else: if value <= kpi.target: return KPIStatus.ON_TARGET elif value <= kpi.warning_threshold: return KPIStatus.AT_RISK else: return KPIStatus.CRITICAL def _calculate_trend(self, kpi_id: str, current_value: float) -> TrendDirection: """Calculate trend direction.""" history = self.history.get(kpi_id, []) if len(history) < 2: return TrendDirection.STABLE # Compare with average of last 3 values recent_values = [h.value for h in history[-3:]] avg = sum(recent_values) / len(recent_values) kpi = self.kpis[kpi_id] diff = current_value - avg if abs(diff) < avg * 0.05: # Within 5% return TrendDirection.STABLE elif (diff > 0 and kpi.higher_is_better) or (diff < 0 and not kpi.higher_is_better): return TrendDirection.IMPROVING else: return TrendDirection.DECLINING def get_current_values(self) -> Dict[str, KPIValue]: """Get most recent value for each KPI.""" current = {} for kpi_id, history in self.history.items(): if history: current[kpi_id] = history[-1] return current def get_dashboard_summary(self) -> Dict[str, Any]: """Get dashboard summary.""" current = self.get_current_values() summary = { 'project': self.project_name, 'date': date.today().isoformat(), 'total_kpis': len(self.kpis), 'by_status': {s.value: 0 for s in KPIStatus}, 'by_category': {}, 'alerts': [] } for kpi_id, value in current.items(): summary['by_status'][value.status.value] += 1 category = self.kpis[kpi_id].category.value if category not in summary['by_category']: summary['by_category'][category] = {'on_target': 0, 'at_risk': 0, 'critical': 0} summary['by_category'][category][value.status.value] += 1 if value.status == KPIStatus.CRITICAL: summary['alerts'].append({ 'kpi': self.kpis[kpi_id].name, 'value': value.value, 'target': self.kpis[kpi_id].target, 'status': 'critical' }) return summary def get_kpi_details(self, kpi_id: str) -> Dict[str, Any]: """Get detailed KPI information.""" if kpi_id not in self.kpis: return {} kpi = self.kpis[kpi_id] history = self.history.get(kpi_id, []) return { 'definition': { 'id': kpi.kpi_id, 'name': kpi.name, 'category': kpi.category.value, 'unit': kpi.unit, 'target': kpi.target, 'formula': kpi.formula }, 'current': { 'value': history[-1].value if history else None, 'status': history[-1].status.value if history else None, 'trend': history[-1].trend.value if history else None }, 'history': [ {'date': h.date.isoformat(), 'value': h.value, 'status': h.status.value} for h in history ] } def generate_html_dashboard(self) -> str: """Generate HTML dashboard.""" summary = self.get_dashboard_summary() current = self.get_current_values() html = f""" <!DOCTYPE html> <html> <head> <title>KPI Dashboard - {self.project_name}</title> <style> body {{ font-family: Arial, sans-serif; margin: 20px; }} .header {{ background: #2196F3; color: white; padding: 20px; margin-bottom: 20px; }} .kpi-grid {{ display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; }} .kpi-card {{ border: 1px solid #ddd; padding: 15px; border-radius: 5px; }} .on_target {{ border-left: 4px solid #4CAF50; }} .at_risk {{ border-left: 4px solid #FF9800; }} .critical {{ border-left: 4px solid #F44336; }} .kpi-value {{ font-size: 24px; font-weight: bold; }} .kpi-name {{ color: #666; font-size: 14px; }} </style> </head> <body> <div class="header"> <h1>{self.project_name} - KPI Dashboard</h1> <p>Last updated: {summary['date']}</p> </div> <div class="kpi-grid"> """ for kpi_id, value in current.items(): kpi = self.kpis[kpi_id] html += f""" <div class="kpi-card {value.status.value}"> <div class="kpi-name">{kpi.name}</div> <div class="kpi-value">{value.value:.2f} {kpi.unit}</div> <div>Target: {kpi.target} | Trend: {value.trend.value}</div> </div> """ html += "</div></body></html>" return html def export_to_excel(self, output_path: str) -> str: """Export dashboard to Excel.""" with pd.ExcelWriter(output_path, engine='openpyxl') as writer: # Summary summary = self.get_dashboard_summary() summary_df = pd.DataFrame([{ 'Project': summary['project'], 'Date': summary['date'], 'On Target': summary['by_status']['on_target'], 'At Risk': summary['by_status']['at_risk'], 'Critical': summary['by_status']['critical'] }]) summary_df.to_excel(writer, sheet_name='Summary', index=False) # Current values current = self.get_current_values() current_data = [] for kpi_id, value in current.items(): kpi = self.kpis[kpi_id] current_data.append({ 'KPI': kpi.name, 'Category': kpi.category.value, 'Value': value.value, 'Unit': kpi.unit, 'Target': kpi.target, 'Status': value.status.value, 'Trend': value.trend.value }) current_df = pd.DataFrame(current_data) current_df.to_excel(writer, sheet_name='Current KPIs', index=False) return output_path
python# Create dashboard dashboard = KPIDashboard("Office Building A") # Record KPI values dashboard.record_value("CPI", 0.95) dashboard.record_value("SPI", 1.02) dashboard.record_value("DEFECT_RATE", 3.5) dashboard.record_value("TRIR", 1.8) dashboard.record_value("LABOR_PROD", 92) # Get summary summary = dashboard.get_dashboard_summary() print(f"On Target: {summary['by_status']['on_target']}") print(f"Critical: {summary['by_status']['critical']}")
pythonhtml = dashboard.generate_html_dashboard() with open("dashboard.html", "w") as f: f.write(html)
pythondetails = dashboard.get_kpi_details("CPI") print(f"Current CPI: {details['current']['value']}")
pythondashboard.add_custom_kpi(KPIDefinition( "WASTE_DIVERSION", "Waste Diversion Rate", KPICategory.SUSTAINABILITY, "%", 75, 60, 50, True ))
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 12,016 | 11,231 | -7% | 1 | 1 | 0% | 2,548 | 5,551 | +118% | 0 | 0 | — |
case-02 | fail→fail | 9,121 | 8,318 | -9% | 1 | 1 | 0% | 1,862 | 5,463 | +193% | 0 | 0 | — |
case-03 | fail→pass | 25,824 | 18,106 | -30% | 1 | 1 | 0% | 6,223 | 7,850 | +26% | 0 | 0 | — |
case-04 | fail→pass | 3,337 | 4,437 | +33% | 1 | 1 | 0% | 589 | 4,503 | +665% | 0 | 0 | — |
case-05 | fail→pass | 2,619 | 3,608 | +38% | 1 | 1 | 0% | 440 | 4,329 | +884% | 0 | 0 | — |
case-06 | fail→pass | 5,542 | 4,543 | -18% | 1 | 1 | 0% | 923 | 4,580 | +396% | 0 | 0 | — |
case-07 | pass→pass | 9,034 | 4,904 | -46% | 1 | 1 | 0% | 1,507 | 4,608 | +206% | 0 | 0 | — |
case-08 | pass→pass | 9,385 | 6,623 | -29% | 1 | 1 | 0% | 1,980 | 5,020 | +154% | 0 | 0 | — |
case-09 | fail→pass | 3,857 | 5,152 | +34% | 1 | 1 | 0% | 571 | 4,549 | +697% | 0 | 0 | — |
case-10 | fail→pass | 2,309 | 4,980 | +116% | 1 | 1 | 0% | 339 | 4,643 | +1270% | 0 | 0 | — |
case-11 | fail→pass | 6,457 | 3,315 | -49% | 1 | 1 | 0% | 1,101 | 4,259 | +287% | 0 | 0 | — |
case-12 | fail→pass | 11,675 | 3,803 | -67% | 1 | 1 | 0% | 1,897 | 4,485 | +136% | 0 | 0 | — |
case-13 | pass→pass | 6,101 | 5,428 | -11% | 1 | 1 | 0% | 1,072 | 4,870 | +354% | 0 | 0 | — |
case-14 | fail→pass | 5,054 | 4,550 | -10% | 1 | 1 | 0% | 905 | 4,534 | +401% | 0 | 0 | — |
case-15 | fail→pass | 10,390 | 5,026 | -52% | 1 | 1 | 0% | 1,913 | 4,682 | +145% | 0 | 0 | — |
case-16 | fail→pass | 7,311 | 1,994 | -73% | 1 | 1 | 0% | 1,248 | 3,960 | +217% | 0 | 0 | — |
case-17 | pass→pass | 4,647 | 1,621 | -65% | 1 | 1 | 0% | 713 | 3,899 | +447% | 0 | 0 | — |
case-18 | pass→pass | 6,371 | 2,916 | -54% | 1 | 1 | 0% | 1,019 | 4,089 | +301% | 0 | 0 | — |
case-19 | pass→fail | 9,135 | 6,280 | -31% | 1 | 1 | 0% | 1,547 | 4,835 | +213% | 0 | 0 | — |
case-20 | pass→pass | 6,733 | 7,728 | +15% | 1 | 1 | 0% | 1,524 | 5,336 | +250% | 0 | 0 | — |
case-21 | pass→pass | 10,481 | 10,744 | +3% | 1 | 1 | 0% | 1,887 | 5,688 | +201% | 0 | 0 | — |
case-22 | pass→pass | 13,944 | 12,966 | -7% | 1 | 1 | 0% | 2,516 | 5,784 | +130% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +45 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.