Getting it into your agent
One page per mod, every tool's command on it. A separate URL per tool would split the same page into five that compete with each other.
npx skills add datadrivenconstruction/DDC_Skills_for_AI_Agents_in_Construction --skill df-mergergit clone --depth 1 https://github.com/datadrivenconstruction/DDC_Skills_for_AI_Agents_in_ConstructionWrote this? Show the measurements
A badge with what this costs and how it scanned, read live from this page, so it follows the numbers instead of freezing them. Markdown for a README, HTML for a documentation site or a project page.
[](https://agentmods.dev/skills/datadrivenconstruction/ddc_skills_for_ai_agents_in_construction/df-merger)<a href="https://agentmods.dev/skills/datadrivenconstruction/ddc_skills_for_ai_agents_in_construction/df-merger"><img src="https://agentmods.dev/badge/skills/datadrivenconstruction/ddc_skills_for_ai_agents_in_construction/df-merger/github.svg" alt="Measured on agentmods" height="20"></a>Or the 80×15 button, for a site that already has a row of RSS and ATOM ones. Only the verdict fits; the numbers stay here.
<a href="https://agentmods.dev/skills/datadrivenconstruction/ddc_skills_for_ai_agents_in_construction/df-merger"><img src="https://agentmods.dev/badge/skills/datadrivenconstruction/ddc_skills_for_ai_agents_in_construction/df-merger.svg" alt="Reviewed on agentmods" width="80" height="20"></a>- NVIDIA SkillSpector pass
What it costs to keep this loaded
Counted locally with the o200k_base tokenizer, which is exact for GPT models; Claude uses its own tokenizer and its counts differ. Treat this as one consistent yardstick across the catalogue rather than a bill. Prices are per million input tokens.
| Model | Per session | Once invoked |
|---|---|---|
| Fable 5.1 | $0.00024 | $0.02386 |
| Opus 5 | $0.00012 | $0.01193 |
| Sonnet 5 | $0.00005 | $0.00477 |
| Haiku 4.5 | $0.00002 | $0.00239 |
Grade A, and why
df-merger scanned grade A with 0 findings against 26 rules in 11 categories — prompt injection, anti-refusal, data exfiltration, privilege escalation, supply chain, agent snooping, system-prompt leakage, SSRF and excessive agency — measured 9d ago.
A static scan of the body, not an audit. Every finding is printed with the line that produced it so you can judge whether it matters here. A mod is markdown that instructs an agent; that is exactly why what it instructs is worth reading.
Nothing flagged
None of the 26 patterns this scan looks for appear in this file: no shell pipes, no recursive deletes, no credential paths, no hidden text, no instruction-override or anti-refusal phrasing, no agent-config snooping. That is not a guarantee, it is the absence of the things that are checkable.
Copies of this mod
1 near-identical copy found in the catalogue:
- df-merger — 100% identical, 0 lines differ
How it starts
The opening of the file, as written. The whole thing — 317 lines — stays where its author put it; the contents beside it link to each section on GitHub.
DataFrame Merger for Construction Data
Overview
Construction projects combine data from BIM, schedules, costs, and sensors. This skill merges DataFrames from disparate sources with intelligent key matching and schema reconciliation.
Python Implementation
import pandas as pd
import numpy as np
from typing import Dict, Any, List, Optional, Tuple
from dataclasses import dataclass
from enum import Enum
from difflib import SequenceMatcher
class MergeStrategy(Enum):
"""DataFrame merge strategies."""
INNER = "inner" # Only matching rows
LEFT = "left" # All left, matching right
RIGHT = "right" # Matching left, all right
OUTER = "outer" # All rows from both
CROSS = "cross" # Cartesian product
@dataclass
class MergeResult:
"""Result of merge operation."""
merged_df: pd.DataFrame
matched_rows: int
left_only: int
right_only: int
merge_quality: float # 0-1 score
class ConstructionDFMerger:
"""Merge DataFrames from construction sources."""
# Common construction column name mappings
COLUMN_MAPPINGS = {
'element_id': ['elementid', 'elem_id', 'id', 'guid', 'globalid'],
'type_name': ['typename', 'type', 'element_type', 'category'],
'level': ['level', 'floor', 'storey', 'building_storey'],
'material': ['material', 'mat', 'material_name'],
'volume': ['volume', 'vol', 'volume_m3', 'qty_volume'],
'area': ['area', 'surface_area', 'qty_area', 'area_m2'],
'cost': ['cost', 'price', 'total_cost', 'amount'],
'task_id': ['task_id', 'activity_id', 'wbs', 'activity'],
'start_date': ['start', 'start_date', 'planned_start', 'begin'],
'end_date': ['end', 'end_date', 'planned_finish', 'finish']
}
def __init__(self):
self.column_cache: Dict[str, str] = {}
def find_common_key(self, df1: pd.DataFrame,
df2: pd.DataFrame) -> Optional[str]:
"""Find common key column between DataFrames."""
# Check exact matches first
common = set(df1.columns) & set(df2.columns)
if common:
# Prefer ID-like columns
for col in common:
if 'id' in col.lower() or 'code' in col.lower():
return col
return list(common)[0]
# Try semantic matching
for col1 in df1.columns:
for col2 in df2.columns:
if self._columns_match(col1, col2):
return col1
return None
def _columns_match(self, col1: str, col2: str) -> bool:
"""Check if column names are semantically similar."""
col1_lower = col1.lower().replace('_', '').replace('-', '')
col2_lower = col2.lower().replace('_', '').replace('-', '')
# Exact match after normalization
if col1_lower == col2_lower:
return True
# Check against mappings
for standard, variants in self.COLUMN_MAPPINGS.items():
if col1_lower in variants and col2_lower in variants:
return True
# Similarity check
similarity = SequenceMatcher(None, col1_lower, col2_lower).ratio()
return similarity > 0.8
def harmonize_columns(self, df: pd.DataFrame) -> pd.DataFrame:
"""Standardize column names."""
df = df.copy()
rename_map = {}
for col in df.columns:
col_lower = col.lower().replace('_', '').replace('-', '')
for standard, variants in self.COLUMN_MAPPINGS.items():
if col_lower in variants:
rename_map[col] = standard
break
return df.rename(columns=rename_map)
def merge(self, left: pd.DataFrame,
right: pd.DataFrame,
on: Optional[str] = None,
left_on: Optional[str] = None,
right_on: Optional[str] = None,
how: MergeStrategy = MergeStrategy.LEFT,
harmonize: bool = True) -> MergeResult:
"""Merge two DataFrames."""
if harmonize:
left = self.harmonize_columns(left)
right = self.harmonize_columns(right)
# Determine merge keys
if on is None and left_on is None and right_on is None:
common_key = self.find_common_key(left, right)
if common_key is None:
raise ValueError("No common key found. Specify merge key manually.")
on = common_key
# Perform merge
merged = pd.merge(
left, right,
on=on,
left_on=left_on,
right_on=right_on,
how=how.value,
indicator=True,
suffixes=('_left', '_right')
)
# Calculate statistics
matched = len(merged[merged['_merge'] == 'both'])
left_only = len(merged[merged['_merge'] == 'left_only'])
right_only = len(merged[merged['_merge'] == 'right_only'])
# Quality score
total = len(left) + len(right)
quality = (matched * 2) / total if total > 0 else 0
# Clean up
merged = merged.drop('_merge', axis=1)
return MergeResult(
merged_df=merged,
matched_rows=matched,
left_only=left_only,
right_only=right_only,
merge_quality=round(quality, 2)
)
def merge_multiple(self, dfs: List[pd.DataFrame],
on: Optional[str] = None,
how: MergeStrategy = MergeStrategy.OUTER) -> pd.DataFrame:
"""Merge multiple DataFrames sequentially."""
if not dfs:
return pd.DataFrame()
result = dfs[0].copy()
for i, df in enumerate(dfs[1:], 1):
result_obj = self.merge(result, df, on=on, how=how)
result = result_obj.merged_df
return result
def fuzzy_merge(self, left: pd.DataFrame,
right: pd.DataFrame,
left_on: str,
right_on: str,
threshold: float = 0.8) -> pd.DataFrame:
"""Merge using fuzzy string matching."""
matches = []
left_values = left[left_on].dropna().unique()
right_values = right[right_on].dropna().unique()
for lval in left_values:
best_match = None
best_score = 0
for rval in right_values:
score = SequenceMatcher(None, str(lval).lower(),
str(rval).lower()).ratio()
if score > best_score and score >= threshold:
best_score = score
best_match = rval
if best_match:
matches.append({
'left_key': lval,
'right_key': best_match,
'match_score': best_score
})
match_df = pd.DataFrame(matches)
# Join using match mapping
left_with_key = left.merge(match_df, left_on=left_on, right_on='left_key', how='left')
result = left_with_key.merge(right, left_on='right_key', right_on=right_on, how='left')
return result
class BIMScheduleMerger(ConstructionDFMerger):
"""Specialized merger for BIM and schedule data."""
def merge_bim_schedule(self, bim_df: pd.DataFrame,
schedule_df: pd.DataFrame,
bim_type_col: str = 'Type Name',
schedule_wbs_col: str = 'WBS') -> pd.DataFrame:
"""Merge BIM elements with schedule activities."""
# This typically requires a mapping table
# For now, use fuzzy matching on descriptions
bim_df = self.harmonize_columns(bim_df)
schedule_df = self.harmonize_columns(schedule_df)
# Try to match type names to WBS descriptions
result = self.fuzzy_merge(
bim_df, schedule_df,
left_on=bim_type_col,
right_on=schedule_wbs_col,
threshold=0.6
)
return result
class CostQTOMerger(ConstructionDFMerger):
"""Merge cost data with quantity takeoffs."""
def merge_cost_qto(self, cost_df: pd.DataFrame,
qto_df: pd.DataFrame) -> pd.DataFrame:
"""Merge cost rates with QTO quantities."""
cost_df = self.harmonize_columns(cost_df)
qto_df = self.harmonize_columns(qto_df)
# Try common merge keys
for key in ['work_item_code', 'type_name', 'material', 'element_id']:
if key in cost_df.columns and key in qto_df.columns:
result = self.merge(cost_df, qto_df, on=key)
# Calculate extended costs
result.merged_df['extended_cost'] = (
result.merged_df.get('quantity', 0) *
result.merged_df.get('unit_price', 0)
)
return result.merged_df
# Fallback to fuzzy merge
return self.fuzzy_merge(
qto_df, cost_df,
left_on='type_name' if 'type_name' in qto_df.columns else qto_df.columns[0],
right_on='description' if 'description' in cost_df.columns else cost_df.columns[0]
)
What ships with it
2 files beside SKILL.md in the same directory: the scripts, references and assets a skill reads on demand. Not counted in the per-session cost; read them before you install if any of them is executable.
What this file has done since we first saw it
Hashed on every crawl. A supply-chain change to an agent config is a question of when, not whether, so the history is kept rather than the latest state alone.
- 9d ago First seen · 317 lines · 24 tokens per session scan A 0d55b6db055b
df-merger is a skill published in the GitHub repository datadrivenconstruction/DDC_Skills_for_AI_Agents_in_Construction (310 stars, last pushed 21d ago), licensed MIT. It adds 24 tokens to every session and 2,386 once invoked, about $0.0001 per session on Opus 5. A static security scan graded it A with 0 findings. No closer match exists in the catalogue, so it is treated as the original; first seen 2026-09-03.
Other skills, from other repositories
skill-builder
Automatically detect source types and build AI skills using Skill Seekers. Use when the user wants to create skills from documentation, repos, PDFs, videos, or other knowledge sources.
session-deep-dive
Deep qualitative analysis of high-signal sessions. Spawns subagents with v2 template, synthesizes patterns, compares against known findings. Use after /session-scan.
brainstorm
Brainstorm Elixir/Phoenix features — explore ideas, compare approaches, gather requirements. Use when vague idea, not sure how to approach, or want to discuss before plan.
ecto-patterns
Ecto patterns — schemas, changesets, queries, migrations, Multi, associations, preloads, upserts. Use when editing Repo calls, Ecto.Query, or schema fields. Skip for Ash.
phx-research
Research Elixir/Phoenix/Ecto topics or evaluate Hex libraries (--library). Use when learning about libraries, patterns, or comparing approaches. Searches HexDocs, ElixirForum, GitHub.
document
Generate @moduledoc/@doc for tested Elixir features; may update their README section or ADR. Not for docs lookup, documentation audits/reviews, or capturing standalone decisions.