Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Automatically generate estimates from QTO data. Apply pricing rules to BIM quantities for cost estimates.
.claude/skills/datadrivenconstruction-auto-estimate-generator/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 101% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 125% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 225% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 131% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 195% | 0% |
Manual estimate creation challenges:
Automated estimate generation from BIM/QTO data using configurable pricing rules and assembly mappings.
pythonimport pandas as pd from typing import Dict, Any, List, Optional, Callable from dataclasses import dataclass, field from enum import Enum class ElementType(Enum): WALL = "wall" FLOOR = "floor" CEILING = "ceiling" DOOR = "door" WINDOW = "window" COLUMN = "column" BEAM = "beam" FOUNDATION = "foundation" ROOF = "roof" STAIR = "stair" MEP = "mep" @dataclass class QTOItem: element_id: str element_type: ElementType name: str quantity: float unit: str properties: Dict[str, Any] = field(default_factory=dict) @dataclass class PricingRule: rule_id: str name: str element_type: ElementType conditions: Dict[str, Any] = field(default_factory=dict) unit_cost: float = 0 assembly_code: str = "" cost_breakdown: Dict[str, float] = field(default_factory=dict) @dataclass class EstimateItem: qto_element_id: str description: str quantity: float unit: str unit_cost: float total_cost: float rule_applied: str wbs_code: str = "" class AutoEstimateGenerator: """Generate estimates from QTO data automatically.""" def __init__(self, project_name: str): self.project_name = project_name self.pricing_rules: List[PricingRule] = [] self.qto_items: List[QTOItem] = [] self.estimate_items: List[EstimateItem] = [] self.unmapped_items: List[QTOItem] = [] def add_pricing_rule(self, rule: PricingRule): """Add pricing rule.""" self.pricing_rules.append(rule) def load_pricing_rules_from_df(self, df: pd.DataFrame): """Load pricing rules from DataFrame.""" for _, row in df.iterrows(): conditions = {} if 'material' in row: conditions['material'] = row['material'] if 'thickness_min' in row: conditions['thickness_min'] = row['thickness_min'] if 'thickness_max' in row: conditions['thickness_max'] = row['thickness_max'] rule = PricingRule( rule_id=row['rule_id'], name=row['name'], element_type=ElementType(row['element_type'].lower()), conditions=conditions, unit_cost=float(row['unit_cost']), assembly_code=row.get('assembly_code', ''), cost_breakdown={ 'labor': float(row.get('labor_pct', 0.4)), 'material': float(row.get('material_pct', 0.5)), 'equipment': float(row.get('equipment_pct', 0.1)) } ) self.add_pricing_rule(rule) def load_qto_from_df(self, df: pd.DataFrame): """Load QTO items from DataFrame.""" for _, row in df.iterrows(): properties = {} for col in df.columns: if col not in ['element_id', 'element_type', 'name', 'quantity', 'unit']: properties[col] = row[col] qto = QTOItem( element_id=str(row['element_id']), element_type=ElementType(row['element_type'].lower()), name=row['name'], quantity=float(row['quantity']), unit=row['unit'], properties=properties ) self.qto_items.append(qto) def find_matching_rule(self, qto_item: QTOItem) -> Optional[PricingRule]: """Find pricing rule that matches QTO item.""" matching_rules = [] for rule in self.pricing_rules: if rule.element_type != qto_item.element_type: continue # Check conditions match = True for key, value in rule.conditions.items(): if key.endswith('_min'): prop_name = key[:-4] if prop_name in qto_item.properties: if qto_item.properties[prop_name] < value: match = False elif key.endswith('_max'): prop_name = key[:-4] if prop_name in qto_item.properties: if qto_item.properties[prop_name] > value: match = False else: if key in qto_item.properties: if qto_item.properties[key] != value: match = False if match: matching_rules.append(rule) # Return most specific rule (most conditions) if matching_rules: return max(matching_rules, key=lambda r: len(r.conditions)) return None def generate_estimate(self) -> Dict[str, Any]: """Generate estimate from QTO items.""" self.estimate_items = [] self.unmapped_items = [] total_cost = 0 for qto in self.qto_items: rule = self.find_matching_rule(qto) if rule: item_cost = qto.quantity * rule.unit_cost self.estimate_items.append(EstimateItem( qto_element_id=qto.element_id, description=f"{qto.name} ({rule.name})", quantity=qto.quantity, unit=qto.unit, unit_cost=rule.unit_cost, total_cost=round(item_cost, 2), rule_applied=rule.rule_id, wbs_code=rule.assembly_code )) total_cost += item_cost else: self.unmapped_items.append(qto) return { 'project': self.project_name, 'total_qto_items': len(self.qto_items), 'mapped_items': len(self.estimate_items), 'unmapped_items': len(self.unmapped_items), 'mapping_rate': round(len(self.estimate_items) / len(self.qto_items) * 100, 1) if self.qto_items else 0, 'total_cost': round(total_cost, 2), 'items': self.estimate_items } def get_cost_by_element_type(self) -> Dict[str, float]: """Get cost breakdown by element type.""" by_type = {} for qto in self.qto_items: for est_item in self.estimate_items: if est_item.qto_element_id == qto.element_id: type_name = qto.element_type.value by_type[type_name] = by_type.get(type_name, 0) + est_item.total_cost return {k: round(v, 2) for k, v in by_type.items()} def get_unmapped_summary(self) -> pd.DataFrame: """Get summary of unmapped items.""" if not self.unmapped_items: return pd.DataFrame() data = [] for item in self.unmapped_items: data.append({ 'Element ID': item.element_id, 'Type': item.element_type.value, 'Name': item.name, 'Quantity': item.quantity, 'Unit': item.unit, 'Properties': str(item.properties) }) return pd.DataFrame(data) def export_to_excel(self, output_path: str) -> str: """Export estimate to Excel.""" result = self.generate_estimate() with pd.ExcelWriter(output_path, engine='openpyxl') as writer: # Summary summary_df = pd.DataFrame([{ 'Project': self.project_name, 'Total QTO Items': result['total_qto_items'], 'Mapped Items': result['mapped_items'], 'Unmapped Items': result['unmapped_items'], 'Mapping Rate %': result['mapping_rate'], 'Total Cost': result['total_cost'] }]) summary_df.to_excel(writer, sheet_name='Summary', index=False) # Estimate items items_df = pd.DataFrame([{ 'Element ID': item.qto_element_id, 'Description': item.description, 'Quantity': item.quantity, 'Unit': item.unit, 'Unit Cost': item.unit_cost, 'Total Cost': item.total_cost, 'WBS': item.wbs_code, 'Rule': item.rule_applied } for item in self.estimate_items]) items_df.to_excel(writer, sheet_name='Estimate', index=False) # By element type by_type_df = pd.DataFrame([ {'Element Type': k, 'Cost': v} for k, v in self.get_cost_by_element_type().items() ]) by_type_df.to_excel(writer, sheet_name='By Type', index=False) # Unmapped items unmapped_df = self.get_unmapped_summary() if not unmapped_df.empty: unmapped_df.to_excel(writer, sheet_name='Unmapped', index=False) return output_path def suggest_missing_rules(self) -> List[Dict[str, Any]]: """Suggest pricing rules for unmapped items.""" suggestions = [] seen_types = set() for item in self.unmapped_items: key = (item.element_type.value, str(item.properties)) if key not in seen_types: seen_types.add(key) suggestions.append({ 'element_type': item.element_type.value, 'sample_name': item.name, 'properties': item.properties, 'count': sum(1 for i in self.unmapped_items if i.element_type == item.element_type and str(i.properties) == str(item.properties)) }) return sorted(suggestions, key=lambda x: x['count'], reverse=True)
python# Initialize generator generator = AutoEstimateGenerator("Office Building A") # Add pricing rules generator.add_pricing_rule(PricingRule( rule_id="W-001", name="Interior Wall - Drywall", element_type=ElementType.WALL, conditions={"material": "Drywall"}, unit_cost=45.00, assembly_code="09.29.10" )) generator.add_pricing_rule(PricingRule( rule_id="W-002", name="Exterior Wall - Masonry", element_type=ElementType.WALL, conditions={"material": "Masonry"}, unit_cost=125.00, assembly_code="04.21.13" )) # Load QTO data generator.qto_items = [ QTOItem("W-001", ElementType.WALL, "Interior Wall L1", 500, "SF", {"material": "Drywall"}), QTOItem("W-002", ElementType.WALL, "Exterior Wall", 1200, "SF", {"material": "Masonry"}) ] # Generate estimate result = generator.generate_estimate() print(f"Total Cost: ${result['total_cost']:,.2f}") print(f"Mapping Rate: {result['mapping_rate']}%")
pythonby_type = generator.get_cost_by_element_type() for element_type, cost in by_type.items(): print(f"{element_type}: ${cost:,.2f}")
pythonunmapped = generator.get_unmapped_summary() print(unmapped)
pythonsuggestions = generator.suggest_missing_rules() for s in suggestions: print(f"Need rule for: {s['element_type']} ({s['count']} items)")
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-02 | fail→pass | 12,149 | 3,171 | -74% | 1 | 1 | 0% | 1,886 | 3,800 | +101% | 0 | 0 | — |
case-01 | fail→fail | 34,321 | 39,289 | +14% | 1 | 1 | 0% | 6,212 | 9,400 | +51% | 0 | 0 | — |
case-03 | fail→pass | 15,148 | 10,381 | -31% | 1 | 1 | 0% | 2,308 | 5,188 | +125% | 0 | 0 | — |
case-04 | pass→pass | 7,627 | 4,584 | -40% | 1 | 1 | 0% | 1,121 | 4,138 | +269% | 0 | 0 | — |
case-05 | pass→pass | 7,262 | 4,908 | -32% | 1 | 1 | 0% | 1,041 | 3,970 | +281% | 0 | 0 | — |
case-06 | pass→pass | 3,768 | 3,710 | -2% | 1 | 1 | 0% | 554 | 3,814 | +588% | 0 | 0 | — |
case-07 | fail→pass | 21,982 | 3,937 | -82% | 1 | 1 | 0% | 1,195 | 3,888 | +225% | 0 | 0 | — |
case-08 | fail→pass | 11,481 | 5,453 | -53% | 1 | 1 | 0% | 1,796 | 4,152 | +131% | 0 | 0 | — |
case-09 | pass→pass | 12,023 | 6,561 | -45% | 1 | 1 | 0% | 1,592 | 4,458 | +180% | 0 | 0 | — |
case-10 | fail→pass | 9,035 | 6,290 | -30% | 1 | 1 | 0% | 1,505 | 4,444 | +195% | 0 | 0 | — |
case-11 | fail→pass | 11,429 | 5,141 | -55% | 1 | 1 | 0% | 1,688 | 4,060 | +141% | 0 | 0 | — |
case-12 | fail→pass | 14,639 | 5,717 | -61% | 1 | 1 | 0% | 2,119 | 4,170 | +97% | 0 | 0 | — |
case-13 | fail→pass | 12,800 | 4,669 | -64% | 1 | 1 | 0% | 1,891 | 4,051 | +114% | 0 | 0 | — |
case-14 | fail→pass | 15,919 | 5,009 | -69% | 1 | 1 | 0% | 2,367 | 4,027 | +70% | 0 | 0 | — |
case-15 | fail→pass | 10,321 | 2,431 | -76% | 1 | 1 | 0% | 1,558 | 3,585 | +130% | 0 | 0 | — |
case-16 | fail→pass | 13,206 | 3,336 | -75% | 1 | 1 | 0% | 1,793 | 3,709 | +107% | 0 | 0 | — |
case-17 | fail→pass | 10,588 | 2,964 | -72% | 1 | 1 | 0% | 1,480 | 3,675 | +148% | 0 | 0 | — |
case-18 | pass→pass | 16,106 | 6,720 | -58% | 1 | 1 | 0% | 2,507 | 4,398 | +75% | 0 | 0 | — |
case-19 | fail→pass | 8,404 | 3,400 | -60% | 1 | 1 | 0% | 1,116 | 3,728 | +234% | 0 | 0 | — |
case-20 | pass→pass | 21,756 | 31,453 | +45% | 1 | 1 | 0% | 3,180 | 8,918 | +180% | 0 | 0 | — |
case-21 | fail→fail | 22,101 | 24,003 | +9% | 1 | 1 | 0% | 4,217 | 7,903 | +87% | 0 | 0 | — |
case-22 | fail→fail | 15,676 | 22,272 | +42% | 1 | 1 | 0% | 2,480 | 7,343 | +196% | 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, and 21 counted toward the lift figure. The other 1 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +59 percentage points is the difference between those two pass rates over the 21 comparable cases.
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.