Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Analyze budget vs actual cost variances. Identify overruns, forecast final costs, and generate variance reports.
.claude/skills/datadrivenconstruction-budget-variance-analyzer/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 54% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 95% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 182% | 0% |
| case-20 | ✗→✓ | ▲ Improved | 256% | 0% |
| case-21 | ✗→✓ | ▲ Improved | 294% | 0% |
Analyze construction budget variances between estimated and actual costs. Track cost performance by category, identify concerning trends, forecast final costs, and provide actionable insights for cost control.
┌─────────────────────────────────────────────────────────────────┐
│ BUDGET VARIANCE ANALYSIS │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Budget vs Actual = Variance → Forecast │
│ ────── ────── ──────── ──────── │
│ 📋 Original 💰 Spent 📊 Over/Under 🔮 EAC │
│ 📝 Revised 📈 Committed 📉 Trend 📋 ETC │
│ 🎯 Baseline 🧾 Invoiced ⚠️ Alerts 📊 VAC │
│ │
│ EAC = Estimate at Completion │
│ ETC = Estimate to Complete │
│ VAC = Variance at Completion │
│ │
└─────────────────────────────────────────────────────────────────┘pythonfrom dataclasses import dataclass, field from typing import List, Dict, Optional, Tuple from datetime import datetime, timedelta from enum import Enum import statistics class CostCategory(Enum): LABOR = "labor" MATERIALS = "materials" EQUIPMENT = "equipment" SUBCONTRACTOR = "subcontractor" GENERAL_CONDITIONS = "general_conditions" OVERHEAD = "overhead" CONTINGENCY = "contingency" FEE = "fee" class VarianceStatus(Enum): ON_BUDGET = "on_budget" UNDER_BUDGET = "under_budget" OVER_BUDGET = "over_budget" CRITICAL = "critical" @dataclass class CostCode: code: str description: str category: CostCategory original_budget: float revised_budget: float = 0.0 committed: float = 0.0 actual: float = 0.0 forecast: float = 0.0 percent_complete: float = 0.0 @property def budget(self) -> float: return self.revised_budget if self.revised_budget else self.original_budget @property def variance(self) -> float: return self.budget - self.actual @property def variance_percent(self) -> float: return (self.variance / self.budget * 100) if self.budget else 0 @property def committed_variance(self) -> float: return self.budget - self.committed @property def estimate_to_complete(self) -> float: if self.percent_complete >= 100: return 0 return self.forecast - self.actual @property def variance_at_completion(self) -> float: return self.budget - self.forecast @dataclass class CostSnapshot: date: datetime cost_code: str actual: float committed: float forecast: float @dataclass class VarianceReport: report_date: datetime project_name: str total_budget: float total_actual: float total_committed: float total_forecast: float variance: float variance_percent: float eac: float etc: float vac: float variances_by_category: Dict[str, Dict] critical_items: List[CostCode] trend: str class BudgetVarianceAnalyzer: """Analyze construction budget variances.""" # Thresholds for variance status VARIANCE_THRESHOLDS = { "under_budget": 0.05, # 5% under "on_budget": 0.00, "over_budget": -0.05, # 5% over "critical": -0.10 # 10% over } def __init__(self, project_name: str, original_budget: float): self.project_name = project_name self.original_budget = original_budget self.cost_codes: Dict[str, CostCode] = {} self.snapshots: List[CostSnapshot] = [] self.contingency_used = 0.0 def add_cost_code(self, code: str, description: str, category: CostCategory, budget: float) -> CostCode: """Add cost code to budget.""" cost_code = CostCode( code=code, description=description, category=category, original_budget=budget, revised_budget=budget, forecast=budget ) self.cost_codes[code] = cost_code return cost_code def import_budget(self, items: List[Dict]) -> int: """Import budget from list of items.""" count = 0 for item in items: self.add_cost_code( item['code'], item['description'], CostCategory(item.get('category', 'other')), item['budget'] ) count += 1 return count def update_actual(self, code: str, actual: float) -> CostCode: """Update actual cost for cost code.""" if code not in self.cost_codes: raise ValueError(f"Cost code {code} not found") cost_code = self.cost_codes[code] cost_code.actual = actual # Record snapshot self._record_snapshot(code) return cost_code def update_committed(self, code: str, committed: float) -> CostCode: """Update committed cost (contracts, POs).""" if code not in self.cost_codes: raise ValueError(f"Cost code {code} not found") cost_code = self.cost_codes[code] cost_code.committed = committed # Update forecast if committed exceeds current forecast if committed > cost_code.forecast: cost_code.forecast = committed self._record_snapshot(code) return cost_code def update_forecast(self, code: str, forecast: float, percent_complete: float = None) -> CostCode: """Update forecast for cost code.""" if code not in self.cost_codes: raise ValueError(f"Cost code {code} not found") cost_code = self.cost_codes[code] cost_code.forecast = forecast if percent_complete is not None: cost_code.percent_complete = percent_complete self._record_snapshot(code) return cost_code def revise_budget(self, code: str, new_budget: float, reason: str = "") -> CostCode: """Revise budget for cost code.""" if code not in self.cost_codes: raise ValueError(f"Cost code {code} not found") cost_code = self.cost_codes[code] cost_code.revised_budget = new_budget cost_code.forecast = new_budget return cost_code def use_contingency(self, amount: float, target_code: str, reason: str = "") -> float: """Use contingency to cover variance.""" self.contingency_used += amount if target_code in self.cost_codes: cc = self.cost_codes[target_code] cc.revised_budget += amount return self.contingency_used def _record_snapshot(self, code: str): """Record cost snapshot for trending.""" if code not in self.cost_codes: return cc = self.cost_codes[code] snapshot = CostSnapshot( date=datetime.now(), cost_code=code, actual=cc.actual, committed=cc.committed, forecast=cc.forecast ) self.snapshots.append(snapshot) def get_variance_status(self, variance_percent: float) -> VarianceStatus: """Determine variance status.""" if variance_percent >= self.VARIANCE_THRESHOLDS["under_budget"]: return VarianceStatus.UNDER_BUDGET elif variance_percent >= self.VARIANCE_THRESHOLDS["over_budget"]: return VarianceStatus.ON_BUDGET elif variance_percent >= self.VARIANCE_THRESHOLDS["critical"]: return VarianceStatus.OVER_BUDGET else: return VarianceStatus.CRITICAL def analyze_by_category(self) -> Dict[str, Dict]: """Analyze variances by cost category.""" by_category = {} for category in CostCategory: codes = [cc for cc in self.cost_codes.values() if cc.category == category] if not codes: continue budget = sum(cc.budget for cc in codes) actual = sum(cc.actual for cc in codes) committed = sum(cc.committed for cc in codes) forecast = sum(cc.forecast for cc in codes) variance = budget - actual variance_pct = (variance / budget * 100) if budget else 0 by_category[category.value] = { "budget": budget, "actual": actual, "committed": committed, "forecast": forecast, "variance": variance, "variance_percent": variance_pct, "status": self.get_variance_status(variance_pct / 100).value } return by_category def identify_critical_items(self, threshold: float = -0.10) -> List[CostCode]: """Identify cost codes with critical variance.""" critical = [] for cc in self.cost_codes.values(): variance_pct = cc.variance_percent / 100 if variance_pct < threshold: critical.append(cc) return sorted(critical, key=lambda x: x.variance_percent) def calculate_cpi(self) -> float: """Calculate Cost Performance Index.""" total_budget = sum(cc.budget * cc.percent_complete / 100 for cc in self.cost_codes.values()) total_actual = sum(cc.actual for cc in self.cost_codes.values()) if total_actual == 0: return 1.0 return total_budget / total_actual def forecast_eac(self, method: str = "cpi") -> float: """Forecast Estimate at Completion.""" total_budget = sum(cc.budget for cc in self.cost_codes.values()) total_actual = sum(cc.actual for cc in self.cost_codes.values()) if method == "cpi": cpi = self.calculate_cpi() if cpi == 0: return total_budget return total_actual + (total_budget - total_actual * cpi) / cpi elif method == "forecast": return sum(cc.forecast for cc in self.cost_codes.values()) elif method == "committed": return sum(max(cc.committed, cc.actual) for cc in self.cost_codes.values()) return total_budget def analyze_trend(self, code: str = None) -> Dict: """Analyze variance trend over time.""" if code: snapshots = [s for s in self.snapshots if s.cost_code == code] else: snapshots = self.snapshots if len(snapshots) < 2: return {"trend": "insufficient_data", "slope": 0} # Group by week weekly_actuals = {} for s in snapshots: week = s.date.isocalendar()[1] if week not in weekly_actuals: weekly_actuals[week] = [] weekly_actuals[week].append(s.actual) # Calculate trend weeks = sorted(weekly_actuals.keys()) if len(weeks) < 2: return {"trend": "insufficient_data", "slope": 0} week_avgs = [statistics.mean(weekly_actuals[w]) for w in weeks] # Simple slope calculation n = len(weeks) x_mean = sum(range(n)) / n y_mean = sum(week_avgs) / n numerator = sum((i - x_mean) * (week_avgs[i] - y_mean) for i in range(n)) denominator = sum((i - x_mean) ** 2 for i in range(n)) slope = numerator / denominator if denominator else 0 if slope > 1000: trend = "increasing_rapidly" elif slope > 0: trend = "increasing" elif slope < -1000: trend = "decreasing_rapidly" elif slope < 0: trend = "decreasing" else: trend = "stable" return {"trend": trend, "slope": slope, "data_points": len(snapshots)} def generate_variance_report(self) -> VarianceReport: """Generate comprehensive variance report.""" total_budget = sum(cc.budget for cc in self.cost_codes.values()) total_actual = sum(cc.actual for cc in self.cost_codes.values()) total_committed = sum(cc.committed for cc in self.cost_codes.values()) total_forecast = sum(cc.forecast for cc in self.cost_codes.values()) variance = total_budget - total_actual variance_pct = (variance / total_budget * 100) if total_budget else 0 eac = self.forecast_eac() etc = eac - total_actual vac = total_budget - eac trend_analysis = self.analyze_trend() return VarianceReport( report_date=datetime.now(), project_name=self.project_name, total_budget=total_budget, total_actual=total_actual, total_committed=total_committed, total_forecast=total_forecast, variance=variance, variance_percent=variance_pct, eac=eac, etc=etc, vac=vac, variances_by_category=self.analyze_by_category(), critical_items=self.identify_critical_items(), trend=trend_analysis["trend"] ) def generate_report_markdown(self, report: VarianceReport) -> str: """Generate markdown report.""" lines = [ "# Budget Variance Report", "", f"**Project:** {report.project_name}", f"**Report Date:** {report.report_date.strftime('%Y-%m-%d')}", "", "## Executive Summary", "", f"| Metric | Amount |", f"|--------|--------|", f"| Total Budget | ${report.total_budget:,.0f} |", f"| Actual to Date | ${report.total_actual:,.0f} |", f"| Committed | ${report.total_committed:,.0f} |", f"| **Variance** | **${report.variance:,.0f} ({report.variance_percent:.1f}%)** |", "", "## Forecast", "", f"| Metric | Amount |", f"|--------|--------|", f"| Estimate at Completion (EAC) | ${report.eac:,.0f} |", f"| Estimate to Complete (ETC) | ${report.etc:,.0f} |", f"| Variance at Completion (VAC) | ${report.vac:,.0f} |", f"| CPI | {self.calculate_cpi():.2f} |", f"| Trend | {report.trend} |", "", "## By Category", "", "| Category | Budget | Actual | Variance | Status |", "|----------|--------|--------|----------|--------|" ] for cat, data in report.variances_by_category.items(): status_icon = "🟢" if data["status"] == "under_budget" else "🟡" if data["status"] == "on_budget" else "🔴" lines.append( f"| {cat} | ${data['budget']:,.0f} | ${data['actual']:,.0f} | " f"${data['variance']:,.0f} ({data['variance_percent']:.1f}%) | {status_icon} |" ) if report.critical_items: lines.extend([ "", "## Critical Items (>10% Over Budget)", "", "| Code | Description | Budget | Actual | Variance |", "|------|-------------|--------|--------|----------|" ]) for cc in report.critical_items[:10]: lines.append( f"| {cc.code} | {cc.description[:25]} | ${cc.budget:,.0f} | " f"${cc.actual:,.0f} | ${cc.variance:,.0f} ({cc.variance_percent:.1f}%) |" ) return "\n".join(lines)
python# Initialize analyzer analyzer = BudgetVarianceAnalyzer("Office Tower", 5000000) # Add cost codes analyzer.add_cost_code("01-100", "Project Management", CostCategory.GENERAL_CONDITIONS, 150000) analyzer.add_cost_code("03-100", "Concrete", CostCategory.MATERIALS, 400000) analyzer.add_cost_code("05-100", "Structural Steel", CostCategory.SUBCONTRACTOR, 800000) analyzer.add_cost_code("15-100", "Mechanical", CostCategory.SUBCONTRACTOR, 600000) analyzer.add_cost_code("16-100", "Electrical", CostCategory.SUBCONTRACTOR, 450000) # Update actuals analyzer.update_actual("01-100", 120000) analyzer.update_actual("03-100", 380000) analyzer.update_actual("05-100", 850000) # Over budget analyzer.update_actual("15-100", 300000) analyzer.update_actual("16-100", 200000) # Update committed analyzer.update_committed("05-100", 900000) # Update forecast analyzer.update_forecast("05-100", 920000, percent_complete=85) # Analyze cpi = analyzer.calculate_cpi() print(f"Cost Performance Index: {cpi:.2f}") critical = analyzer.identify_critical_items() print(f"Critical items: {len(critical)}") # Generate report report = analyzer.generate_variance_report() print(analyzer.generate_report_markdown(report))
bashpip install (no external dependencies)
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 22,554 | 15,625 | -31% | 1 | 1 | 0% | 5,275 | 8,148 | +54% | 0 | 0 | — |
case-02 | fail→fail | 24,025 | 16,634 | -31% | 1 | 1 | 0% | 5,164 | 8,492 | +64% | 0 | 0 | — |
case-03 | fail→fail | 22,692 | 18,796 | -17% | 1 | 1 | 0% | 4,714 | 8,750 | +86% | 0 | 0 | — |
case-04 | pass→pass | 7,484 | 8,469 | +13% | 1 | 1 | 0% | 1,490 | 6,102 | +310% | 0 | 0 | — |
case-05 | pass→pass | 14,474 | 15,886 | +10% | 1 | 1 | 0% | 2,593 | 7,323 | +182% | 0 | 0 | — |
case-06 | pass→pass | 12,569 | 11,240 | -11% | 1 | 1 | 0% | 2,485 | 7,105 | +186% | 0 | 0 | — |
case-07 | pass→pass | 5,930 | 7,385 | +25% | 1 | 1 | 0% | 1,207 | 6,115 | +407% | 0 | 0 | — |
case-08 | pass→pass | 5,365 | 5,812 | +8% | 1 | 1 | 0% | 1,114 | 5,808 | +421% | 0 | 0 | — |
case-09 | pass→pass | 10,120 | 7,697 | -24% | 1 | 1 | 0% | 1,801 | 5,992 | +233% | 0 | 0 | — |
case-10 | pass→pass | 9,268 | 9,159 | -1% | 1 | 1 | 0% | 1,894 | 6,646 | +251% | 0 | 0 | — |
case-11 | pass→pass | 10,161 | 10,771 | +6% | 1 | 1 | 0% | 1,804 | 6,659 | +269% | 0 | 0 | — |
case-12 | fail→pass | 18,585 | 10,955 | -41% | 1 | 1 | 0% | 3,374 | 6,592 | +95% | 0 | 0 | — |
case-13 | pass→pass | 10,532 | 9,164 | -13% | 1 | 1 | 0% | 2,279 | 6,661 | +192% | 0 | 0 | — |
case-14 | pass→pass | 6,581 | 3,110 | -53% | 1 | 1 | 0% | 1,185 | 5,297 | +347% | 0 | 0 | — |
case-15 | pass→pass | 3,678 | 5,967 | +62% | 1 | 1 | 0% | 560 | 5,558 | +893% | 0 | 0 | — |
case-16 | pass→pass | 4,935 | 3,524 | -29% | 1 | 1 | 0% | 783 | 5,317 | +579% | 0 | 0 | — |
case-17 | pass→pass | 8,699 | 8,595 | -1% | 1 | 1 | 0% | 1,641 | 6,340 | +286% | 0 | 0 | — |
case-18 | fail→pass | 29,273 | 10,768 | -63% | 1 | 1 | 0% | 2,295 | 6,470 | +182% | 0 | 0 | — |
case-19 | pass→pass | 11,359 | 3,846 | -66% | 1 | 1 | 0% | 2,034 | 5,389 | +165% | 0 | 0 | — |
case-20 | fail→pass | 10,597 | 5,290 | -50% | 1 | 1 | 0% | 1,601 | 5,697 | +256% | 0 | 0 | — |
case-21 | fail→pass | 17,435 | 3,810 | -78% | 1 | 1 | 0% | 1,355 | 5,337 | +294% | 0 | 0 | — |
case-22 | pass→pass | 7,717 | 2,836 | -63% | 1 | 1 | 0% | 1,320 | 5,158 | +291% | 0 | 0 | — |
case-23 | fail→pass | 8,950 | 3,713 | -59% | 1 | 1 | 0% | 1,440 | 5,382 | +274% | 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. 23 cases were attempted. The headline lift of +26 percentage points is the difference between those two pass rates over the 23 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.