Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Generate Quantity Take-Off (QTO) reports from BIM/CAD data. Extract volumes, areas, counts by category. Group elements, apply calculation rules, and create cost estimates automatically.
.claude/skills/datadrivenconstruction-qto-report/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-10 | ✗→✓ | ▲ Improved | 154% | 0% |
| case-15 | ✗→✓ | ▲ Improved | 136% | 0% |
| case-19 | ✗→✓ | ▲ Improved | 74% | 0% |
| case-20 | ✗→✓ | ▲ Improved | 405% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 157% | 0% |
Based on DDC methodology (Chapter 3.2), this skill automates the extraction and grouping of quantities from BIM/CAD data. QTO is the foundation for cost estimation, scheduling, and project planning in construction.
Book Reference: "Quantity Take-Off и автоматическое создание смет" / "QTO and Automated Estimates"
> "QTO Quantity Take-Off: группировка данных по атрибутам позволяет автоматически извлекать объемы и количества из BIM-моделей для расчета стоимости." > — DDC Book, Chapter 3.2
The QTO process is central to 5D BIM:
pythonimport pandas as pd # Load BIM element data df = pd.read_csv("revit_export.csv") # Generate QTO by category qto = df.groupby('Category').agg({ 'Volume': 'sum', 'Area': 'sum', 'ElementId': 'count' }).rename(columns={'ElementId': 'Count'}) # Calculate cost (if unit prices available) qto['Unit_Price'] = [150, 80, 450, 200] # $/m³ qto['Total_Cost'] = qto['Volume'] * qto['Unit_Price'] qto.to_excel("qto_report.xlsx")
pythonimport pandas as pd def generate_qto(df, group_by='Category'): """ Generate Quantity Take-Off grouped by specified column Args: df: DataFrame with BIM elements group_by: Column(s) to group by Returns: QTO summary DataFrame """ # Define aggregations based on available columns agg_dict = {} if 'Volume' in df.columns: agg_dict['Volume'] = 'sum' if 'Area' in df.columns: agg_dict['Area'] = 'sum' if 'Length' in df.columns: agg_dict['Length'] = 'sum' if 'Count' in df.columns: agg_dict['Count'] = 'sum' else: agg_dict['ElementId'] = 'count' qto = df.groupby(group_by).agg(agg_dict) if 'ElementId' in agg_dict: qto = qto.rename(columns={'ElementId': 'Count'}) return qto.round(2) # Usage qto = generate_qto(df, group_by='Category') print(qto)
pythondef generate_multi_level_qto(df): """Generate QTO grouped by multiple levels""" qto = df.groupby(['Level', 'Category', 'Material']).agg({ 'Volume': ['sum', 'count'], 'Area': 'sum' }).round(2) # Flatten column names qto.columns = ['Volume_m3', 'Element_Count', 'Area_m2'] # Add percentages qto['Volume_Pct'] = (qto['Volume_m3'] / qto['Volume_m3'].sum() * 100).round(1) return qto.sort_values('Volume_m3', ascending=False) # Usage qto = generate_multi_level_qto(df) qto.to_excel("qto_multi_level.xlsx")
pythondef generate_qto_pivot(df, values='Volume', index='Level', columns='Category'): """Generate QTO as pivot table""" pivot = pd.pivot_table( df, values=values, index=index, columns=columns, aggfunc='sum', fill_value=0, margins=True, margins_name='TOTAL' ).round(2) return pivot # Usage - Volume by Level and Category qto_pivot = generate_qto_pivot(df, values='Volume') qto_pivot.to_excel("qto_pivot.xlsx")
pythondef calculate_cost_from_qto(qto_df, prices_df, quantity_col='Volume'): """ Calculate costs by applying unit prices to quantities Args: qto_df: QTO DataFrame with quantities prices_df: DataFrame with Category and Unit_Price quantity_col: Column containing quantities """ # Merge with prices result = qto_df.reset_index().merge( prices_df, on='Category', how='left' ) # Calculate costs result['Total_Cost'] = result[quantity_col] * result['Unit_Price'] result['Cost_Pct'] = (result['Total_Cost'] / result['Total_Cost'].sum() * 100).round(1) # Summary grand_total = result['Total_Cost'].sum() print(f"Grand Total: ${grand_total:,.2f}") return result # Unit prices database prices = pd.DataFrame({ 'Category': ['Wall', 'Floor', 'Column', 'Beam', 'Foundation'], 'Unit_Price': [150, 80, 450, 200, 120], # $/m³ 'Unit': ['m³', 'm³', 'm³', 'm³', 'm³'] }) # Calculate cost_estimate = calculate_cost_from_qto(qto, prices) cost_estimate.to_excel("cost_estimate.xlsx", index=False)
pythondef apply_excel_rules(df, rules_path): """ Apply calculation rules defined in Excel file Excel format: | Category | Formula_Type | Factor | Unit | | Wall | volume | 1.05 | m³ | | Floor | area | 1.10 | m² | """ rules = pd.read_excel(rules_path) results = [] for _, rule in rules.iterrows(): category = rule['Category'] formula_type = rule['Formula_Type'] factor = rule['Factor'] category_data = df[df['Category'] == category].copy() if formula_type == 'volume': category_data['Quantity'] = category_data['Volume'] * factor elif formula_type == 'area': category_data['Quantity'] = category_data['Area'] * factor elif formula_type == 'length': category_data['Quantity'] = category_data['Length'] * factor elif formula_type == 'count': category_data['Quantity'] = category_data.groupby('Category').ngroup() + 1 category_data['Unit'] = rule['Unit'] results.append(category_data) return pd.concat(results, ignore_index=True) # Usage df_with_quantities = apply_excel_rules(df, "calculation_rules.xlsx")
pythondef process_revit_export(csv_path): """Process standard Revit schedule export""" df = pd.read_csv(csv_path) # Standardize column names column_mapping = { 'Family and Type': 'Type', 'Volume': 'Volume', 'Area': 'Area', 'Count': 'Count', 'Level': 'Level', 'Category': 'Category' } df = df.rename(columns={ k: v for k, v in column_mapping.items() if k in df.columns }) # Convert volume from cubic feet to cubic meters (if needed) if 'Volume' in df.columns: # Revit exports in cubic feet by default df['Volume_m3'] = df['Volume'] * 0.0283168 return df # Usage df = process_revit_export("revit_schedule.csv") qto = generate_qto(df)
python# Using IfcOpenShell import ifcopenshell import pandas as pd def extract_qto_from_ifc(ifc_path): """Extract quantities from IFC file""" ifc = ifcopenshell.open(ifc_path) elements = [] for element in ifc.by_type("IfcBuildingElement"): # Get properties props = { 'GlobalId': element.GlobalId, 'Name': element.Name, 'Type': element.is_a(), 'Material': None, 'Volume': None, 'Area': None } # Extract quantities from property sets for definition in element.IsDefinedBy: if definition.is_a('IfcRelDefinesByProperties'): pset = definition.RelatingPropertyDefinition if pset.is_a('IfcElementQuantity'): for qty in pset.Quantities: if qty.is_a('IfcQuantityVolume'): props['Volume'] = qty.VolumeValue elif qty.is_a('IfcQuantityArea'): props['Area'] = qty.AreaValue elements.append(props) return pd.DataFrame(elements) # Usage df = extract_qto_from_ifc("model.ifc") qto = generate_qto(df, group_by='Type')
pythondef material_breakdown_qto(df): """Detailed breakdown by material type""" breakdown = df.groupby(['Category', 'Material', 'Type']).agg({ 'Volume': 'sum', 'Area': 'sum', 'ElementId': 'nunique' }).rename(columns={'ElementId': 'Unique_Elements'}) # Add subtotals for each category category_totals = df.groupby('Category').agg({ 'Volume': 'sum', 'Area': 'sum' }) breakdown['Category_Volume_Pct'] = breakdown.apply( lambda row: (row['Volume'] / category_totals.loc[row.name[0], 'Volume'] * 100), axis=1 ).round(1) return breakdown # Usage material_qto = material_breakdown_qto(df) material_qto.to_excel("material_breakdown.xlsx")
pythondef qto_with_waste(df, waste_factors): """ Apply waste factors to quantities Args: waste_factors: dict like {'Concrete': 1.05, 'Steel': 1.03} """ qto = df.groupby(['Category', 'Material']).agg({ 'Volume': 'sum' }).reset_index() # Apply waste factors qto['Waste_Factor'] = qto['Material'].map(waste_factors).fillna(1.0) qto['Net_Volume'] = qto['Volume'] qto['Gross_Volume'] = qto['Volume'] * qto['Waste_Factor'] qto['Waste_Volume'] = qto['Gross_Volume'] - qto['Net_Volume'] return qto # Usage waste = {'Concrete': 1.05, 'Brick': 1.08, 'Steel': 1.03} qto = qto_with_waste(df, waste)
pythondef compare_qto(design_df, asbuilt_df, group_by='Category'): """Compare designed vs as-built quantities""" design_qto = design_df.groupby(group_by)['Volume'].sum() asbuilt_qto = asbuilt_df.groupby(group_by)['Volume'].sum() comparison = pd.DataFrame({ 'Design': design_qto, 'AsBuilt': asbuilt_qto }) comparison['Difference'] = comparison['AsBuilt'] - comparison['Design'] comparison['Variance_%'] = ( (comparison['AsBuilt'] - comparison['Design']) / comparison['Design'] * 100 ).round(1) return comparison # Usage comparison = compare_qto(design_df, asbuilt_df) print(comparison)
pythondef export_qto_report(qto_df, base_name, include_charts=True): """Export QTO to Excel with formatting and charts""" from openpyxl import Workbook from openpyxl.chart import BarChart, Reference # Excel with multiple sheets with pd.ExcelWriter(f"{base_name}.xlsx", engine='openpyxl') as writer: # Summary sheet qto_df.to_excel(writer, sheet_name='Summary') # Detailed data if hasattr(qto_df, 'reset_index'): qto_df.reset_index().to_excel( writer, sheet_name='Details', index=False ) # CSV for integration qto_df.to_csv(f"{base_name}.csv") # JSON for API qto_df.reset_index().to_json( f"{base_name}.json", orient='records', indent=2 ) print(f"Exported: {base_name}.xlsx, .csv, .json") # Usage export_qto_report(qto, "project_qto")
| Task | Code | |------|------| | Basic QTO | df.groupby('Category')['Volume'].sum() | | Multi-column QTO | df.groupby(['Level', 'Category']).agg({...}) | | Pivot QTO | pd.pivot_table(df, values='Volume', ...) | | Apply prices | qto.merge(prices, on='Category') | | Calculate cost | df['Cost'] = df['Volume'] * df['Unit_Price'] | | Add waste factor | df['Gross'] = df['Net'] * waste_factor |
cost-estimation-resource for detailed cost calculationsauto-estimate-generator for automated estimate creationgantt-chart for 4D scheduling integrationco2-estimation for carbon footprint calculations| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-22 | fail→fail | 13,038 | 22,508 | +73% | 1 | 1 | 0% | 2,263 | 8,195 | +262% | 0 | 0 | — |
case-01 | pass→pass | 10,972 | 9,912 | -10% | 1 | 1 | 0% | 2,118 | 5,448 | +157% | 0 | 0 | — |
case-02 | pass→pass | 14,213 | 8,082 | -43% | 1 | 1 | 0% | 2,706 | 5,085 | +88% | 0 | 0 | — |
case-03 | pass→pass | 10,211 | 2,032 | -80% | 1 | 1 | 0% | 1,730 | 3,941 | +128% | 0 | 0 | — |
case-08 | pass→pass | 6,736 | 3,965 | -41% | 1 | 1 | 0% | 1,334 | 4,284 | +221% | 0 | 0 | — |
case-04 | pass→pass | 11,324 | 9,014 | -20% | 1 | 1 | 0% | 1,967 | 5,133 | +161% | 0 | 0 | — |
case-05 | pass→pass | 10,466 | 3,618 | -65% | 1 | 1 | 0% | 1,703 | 4,207 | +147% | 0 | 0 | — |
case-06 | pass→pass | 9,125 | 3,718 | -59% | 1 | 1 | 0% | 1,582 | 4,258 | +169% | 0 | 0 | — |
case-07 | pass→pass | 5,019 | 4,805 | -4% | 1 | 1 | 0% | 993 | 4,519 | +355% | 0 | 0 | — |
case-09 | pass→pass | 6,012 | 6,433 | +7% | 1 | 1 | 0% | 1,090 | 4,879 | +348% | 0 | 0 | — |
case-10 | fail→pass | 9,575 | 4,067 | -58% | 1 | 1 | 0% | 1,743 | 4,435 | +154% | 0 | 0 | — |
case-11 | pass→pass | 8,670 | 4,424 | -49% | 1 | 1 | 0% | 1,410 | 4,338 | +208% | 0 | 0 | — |
case-12 | pass→pass | 10,939 | 10,635 | -3% | 1 | 1 | 0% | 2,200 | 5,898 | +168% | 0 | 0 | — |
case-13 | pass→pass | 6,343 | 4,337 | -32% | 1 | 1 | 0% | 1,226 | 4,398 | +259% | 0 | 0 | — |
case-14 | pass→pass | 10,742 | 5,856 | -45% | 1 | 1 | 0% | 2,075 | 4,768 | +130% | 0 | 0 | — |
case-15 | fail→pass | 10,258 | 5,406 | -47% | 1 | 1 | 0% | 1,994 | 4,697 | +136% | 0 | 0 | — |
case-16 | pass→pass | 9,663 | 8,548 | -12% | 1 | 1 | 0% | 1,980 | 5,475 | +177% | 0 | 0 | — |
case-17 | pass→pass | 11,165 | 6,975 | -38% | 1 | 1 | 0% | 2,022 | 4,903 | +142% | 0 | 0 | — |
case-18 | pass→pass | 10,711 | 6,537 | -39% | 1 | 1 | 0% | 2,127 | 4,771 | +124% | 0 | 0 | — |
case-19 | fail→pass | 20,206 | 15,762 | -22% | 1 | 1 | 0% | 3,870 | 6,750 | +74% | 0 | 0 | — |
case-20 | fail→pass | 15,754 | 2,221 | -86% | 1 | 1 | 0% | 796 | 4,019 | +405% | 0 | 0 | — |
case-21 | fail→fail | 19,396 | 16,896 | -13% | 1 | 1 | 0% | 4,028 | 7,040 | +75% | 0 | 0 | — |
case-23 | fail→fail | 26,486 | 29,536 | +12% | 1 | 1 | 0% | 5,164 | 9,770 | +89% | 0 | 0 | — |
case-24 | fail→fail | 14,364 | 20,919 | +46% | 1 | 1 | 0% | 2,546 | 7,949 | +212% | 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. 24 cases were attempted, and 23 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 +17 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.