data-profiling

data-profiling is a skill for Claude Code, Codex from zpower426/datapowers. It costs 42 tokens per session (3,408 once invoked), scanned A, original, MIT.

A procedure that summarizes a dataset’s structure, contents, and basic patterns without exposing personally identifying information. The summary is given to analysis agents instead of the original rows.

In plain words
What is it for?
Preparing data for analysis, handing a dataset to another agent, checking what changed in a new data version, and documenting its shape.
Why use it?
It prevents agents from loading large datasets unnecessarily and reduces the risk of exposing names, contact details, addresses, or precise locations.

Skill for Claude CodeCodex

Part of the datapowers plugin — 20 skills, 3 commands, 3 agents, 1 hook shipped together

Install

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.

agentmods
npx agentmods add skills/zpower426/datapowers/data-profiling
Any agent
npx skills add zpower426/datapowers --skill data-profiling
Clone the repo
git clone --depth 1 https://github.com/zpower426/datapowers

Made for: Claude Code, Codex.

Or install datapowers, the plugin that ships this one along with the rest of its 20 skills, 3 commands, 3 agents, 1 hook.

Wrote 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.

agentmods badge for data-profiling

README.md
[![agentmods](https://agentmods.dev/badge/skills/zpower426/datapowers/data-profiling.svg)](https://agentmods.dev/skills/zpower426/datapowers/data-profiling)
Your own site
<a href="https://agentmods.dev/skills/zpower426/datapowers/data-profiling"><img src="https://agentmods.dev/badge/skills/zpower426/datapowers/data-profiling.svg" alt="Measured on agentmods" height="20"></a>
Per session 42 Skills are progressive disclosure: only the name and description are preloaded; the body loads when the skill is used.
When invoked 3,408 The whole file, excluding the scripts and references it only reads on demand.
Security scan A 0 findings. Scan, not verified.
Origin original No closer match found in the catalogue.
Token cost

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.

ModelPer sessionOnce invoked
Fable 5 $0.00042 $0.03408
Opus 5 $0.00021 $0.01704
Sonnet 5 $0.00008 $0.00682
Haiku 4.5 $0.00004 $0.00341

Measured 3d ago against content hash 3e881b3e2c01, method: parsed. Prices are Anthropic first-party input rates as of 2026-08-30, from the pricing page.

Security

Grade A, and why

data-profiling 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 3d 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.

skills/data-profiling/SKILL.md · 327 lines

How it starts

The opening of the file, as written. The whole thing — 327 lines — stays where its author put it; the contents beside it link to each section on GitHub.

Data Profiling

Generate a compact, structured data profile before dispatching any analysis subagent. Subagents must never load the full dataset to understand its shape — the profile replaces that need.

Why profiles: Full dataset reads pollute subagent context with raw rows. A profile gives the same analytical picture in ~200 lines of Markdown, enabling reproducible context injection across all downstream tasks.

When to Use

Use this skill when:

  • Starting any analysis session with a new dataset
  • Dispatching a subagent that needs to reason about the data structure
  • A downstream skill asks "what does the data look like?"
  • The dataset has changed (new pipeline run, new data slice)

Iron Laws

  • NO SUBAGENT RECEIVES RAW DATA ROWS — only the profile.
  • STRICT PII FILTERING: No real names, detailed addresses, phone numbers, emails, or precise GPS coordinates allowed in the profile.

Step-by-Step Procedure

Step 1 — Run the profiler script

Execute the following Python to generate the profile. Do NOT modify the output paths.

import pandas as pd
import numpy as np
import json
from pathlib import Path

HIDDEN_NULL_STRINGS = {"unknown", "n/a", "na", "none", "null", "nan", "-", "--", "?", "missing", "not available", "not applicable"}

def profile_dataset(data_path: str, output_path: str = "data_profile.md", sample_n: int = 50000, target_col: str = None):
    """Generate a high-density, PII-free data profile.
    
    Args:
        data_path:  path to CSV or Parquet file
        output_path: where to write the Markdown profile
        sample_n:   max rows to sample (keeps profile fast on large files)
        target_col: name of the target/label column (enables Target Correlation section)
    """
    df = pd.read_csv(data_path, nrows=sample_n) if data_path.endswith(".csv") else pd.read_parquet(data_path)
    n_rows_total = len(df)

    lines = [
        f"# Data Profile: `{Path(data_path).name}`",
        f"",
        f"**Rows (sampled):** {n_rows_total:,}  |  **Columns:** {df.shape[1]}  |  **Generated:** {pd.Timestamp.now().strftime('%Y-%m-%d %H:%M')}",
        f"",
        f"**⚠️ PRIVACY NOTICE:** This profile contains sampled example values. Ensure all PII (names, emails, IDs) is masked or excluded before sharing with subagents.",
        f"",
        f"## Schema Overview",
        f"",
        f"| Column | Dtype | Non-Null % | Unique | Example Values (Sampled) |",
        f"|--------|-------|-----------|--------|--------------------------|",
    ]

    for col in df.columns:
        dtype = str(df[col].dtype)
        non_null_pct = f"{(df[col].notna().mean() * 100):.1f}%"
        n_unique = df[col].nunique()
        # Sample 3 non-null values — MUST manually verify no PII exposure
        sample_vals = df[col].dropna().sample(min(3, df[col].notna().sum()), random_state=42).tolist()
        sample_str = str(sample_vals)[:60]
        lines.append(f"| `{col}` | {dtype} | {non_null_pct} | {n_unique:,} | {sample_str} |")

    lines += ["", "## Numeric Column Statistics", ""]
    num_cols = df.select_dtypes(include=[np.number]).columns.tolist()
    if num_cols:
        lines += [
            "| Column | Mean | Std | Min | p25 | Median | p75 | Max | Skewness | Outlier % |",
            "|--------|------|-----|-----|-----|--------|-----|-----|----------|-----------|",
        ]
        for col in num_cols:
            s = df[col].dropna()
            if len(s) == 0:
                continue
            q1, q3 = s.quantile(0.25), s.quantile(0.75)
            iqr = q3 - q1
            outlier_pct = f"{((s < (q1 - 1.5*iqr)) | (s > (q3 + 1.5*iqr))).mean()*100:.1f}%"
            lines.append(
                f"| `{col}` | {s.mean():.3g} | {s.std():.3g} | {s.min():.3g} | "
                f"{q1:.3g} | {s.median():.3g} | {q3:.3g} | {s.max():.3g} | "
                f"{s.skew():.2f} | {outlier_pct} |"
            )

    lines += ["", "## Categorical Column Top-10 Frequencies", ""]
    cat_cols = df.select_dtypes(include=["object", "category"]).columns.tolist()
    for col in cat_cols:
        counts = df[col].value_counts(normalize=True).head(10)
        lines.append(f"### `{col}` ({df[col].nunique():,} unique)")
        lines.append("")
        lines.append("| Value | Frequency |")
        lines.append("|-------|-----------|")
        for val, freq in counts.items():
            lines.append(f"| {str(val)[:40]} | {freq:.1%} |")
        lines.append("")

    # Hidden null detection: string values that represent missingness but aren't NaN
    lines += ["", "## Hidden Null Detection", ""]
    hidden_null_found = []
    for col in cat_cols:
        col_lower = df[col].dropna().astype(str).str.strip().str.lower()
        for sentinel in HIDDEN_NULL_STRINGS:
            count = (col_lower == sentinel).sum()
            if count > 0:
                hidden_null_found.append({
                    "col": col,
                    "sentinel": df[col].dropna().astype(str).str.strip()[col_lower == sentinel].iloc[0],
                    "count": count,
                    "pct": count / len(df),
                })
    if hidden_null_found:
        lines += [
            "| Column | Sentinel Value | Count | % of Rows | Action |",
            "|--------|---------------|-------|-----------|--------|",
        ]
        for h in hidden_null_found:
            lines.append(f"| `{h['col']}` | `{h['sentinel']}` | {h['count']:,} | {h['pct']:.1%} | **HIDDEN_NULL** — replace with `np.nan` before modeling |")
    else:
        lines.append("No hidden null sentinels detected.")

    lines += ["", "## Missing Value Heatmap (columns with > 0% missing)", ""]
    missing = df.isnull().mean()
    missing = missing[missing > 0].sort_values(ascending=False)
    if len(missing) > 0:
        lines += [
            "| Column | Missing % | Pattern |",
            "|--------|-----------|---------|",
        ]
        for col, pct in missing.items():
            pattern = "MCAR (suspect)" if pct < 0.01 else "MAR/MNAR" if pct > 0.20 else "MAR"
            lines.append(f"| `{col}` | {pct:.1%} | {pattern} |")
    else:
        lines.append("No missing values detected.")

    lines += ["", "## Correlation Heatmap (|r| > 0.5)", ""]
    if len(num_cols) >= 2:
        corr = df[num_cols].corr()
        high_corr = []
        for i in range(len(num_cols)):
            for j in range(i+1, len(num_cols)):
                r = corr.iloc[i, j]
                if abs(r) > 0.5:
                    high_corr.append((num_cols[i], num_cols[j], r))
        if high_corr:
            lines += ["| Col A | Col B | Pearson r |", "|-------|-------|-----------|"]
            for a, b, r in sorted(high_corr, key=lambda x: -abs(x[2])):
                lines.append(f"| `{a}` | `{b}` | {r:.3f} |")
        else:
            lines.append("No high correlations (|r| > 0.5) detected.")

    # Target correlation: only when target column is specified
    if target_col and target_col in df.columns:
        lines += ["", f"## Target Correlation Top 10 (target: `{target_col}`)", ""]
        from scipy.stats import spearmanr

        def cramers_v(x, y):
            """Cramér's V for categorical vs categorical association."""
            from scipy.stats import chi2_contingency
            contingency = pd.crosstab(x, y)
            chi2, _, _, _ = chi2_contingency(contingency)
            n = contingency.sum().sum()
            r, k = contingency.shape
            return np.sqrt(chi2 / (n * (min(r, k) - 1))) if min(r, k) > 1 else 0.0

        target = df[target_col].dropna()
        correlations = []
        for col in df.columns:
            if col == target_col:
                continue
            shared = df[[col, target_col]].dropna()
            if len(shared) < 30:
                continue
            col_vals = shared[col]
            tgt_vals = shared[target_col]
            try:
                if col in num_cols and target_col in num_cols:
                    r, _ = spearmanr(col_vals, tgt_vals)
                    method = "Spearman r"
                    score = round(r, 4)
                elif col in cat_cols and target_col in cat_cols:
                    score = round(cramers_v(col_vals, tgt_vals), 4)
                    method = "Cramér's V"
                elif col in num_cols and target_col in cat_cols:
                    r, _ = spearmanr(col_vals, tgt_vals)
                    method = "Spearman r"
                    score = round(r, 4)
                else:
                    r, _ = spearmanr(col_vals.astype("category").cat.codes, tgt_vals)
                    method = "Spearman r (encoded)"
                    score = round(r, 4)
                correlations.append((col, score, method))
            except Exception:
                pass

        top10 = sorted(correlations, key=lambda x: -abs(x[1]))[:10]
        if top10:
            lines += [
                "| Rank | Feature | Score | Method | Note |",
                "|------|---------|-------|--------|------|",
            ]
            for rank, (col, score, method) in enumerate(top10, 1):
                flag = " ⚠️ LEAKAGE SUSPECT" if abs(score) > 0.9 else ""
                lines.append(f"| {rank} | `{col}` | {score} | {method} |{flag} |")
        else:
            lines.append("Could not compute target correlations (insufficient data).")

    lines += ["", "## Data Quality Flags", ""]
    flags = []
    for col in df.columns:
        pct_miss = df[col].isnull().mean()
        if pct_miss > 0.30:
            flags.append(f"- **HIGH MISSING** `{col}`: {pct_miss:.1%} missing")
    for col in num_cols:
        s = df[col].dropna()
        if len(s) > 0:
            q1, q3 = s.quantile(0.25), s.quantile(0.75)
            iqr = q3 - q1
            out_pct = ((s < (q1 - 1.5*iqr)) | (s > (q3 + 1.5*iqr))).mean()
            if out_pct > 0.10:
                flags.append(f"- **HIGH OUTLIER RATE** `{col}`: {out_pct:.1%} outliers")
    for col in cat_cols:
        top_freq = df[col].value_counts(normalize=True).iloc[0] if df[col].notna().any() else 0
        if top_freq > 0.90:
            flags.append(f"- **HIGH CARDINALITY DOMINANCE** `{col}`: top value = {top_freq:.1%}")

    if flags:
        lines += flags
    else:
        lines.append("No critical data quality flags.")

    profile_text = "\n".join(lines)
    Path(output_path).write_text(profile_text)
    print(f"Profile written to: {output_path}")
    return profile_text

profile_dataset(
    data_path="<YOUR_DATA_PATH>",
    output_path="artifacts/data_profile.md",
    target_col="<TARGET_COLUMN_OR_None>",  # set to None if target not yet identified
)

Read the full file on GitHub · 327 lines

Changes

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.

  1. 3d ago First seen · 327 lines · 42 tokens per session scan A 3e881b3e2c01

Subscribe to this mod's changes

data-profiling is a skill published in the GitHub repository zpower426/datapowers (1 stars, last pushed 5mo ago), licensed MIT. It adds 42 tokens to every session and 3,408 once invoked, about $0.0002 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-08-31.

Related

Other skills, from other repositories

peer-conversation

Многотуровый диалог писателя (Claude) с одним или несколькими напарниками (любой набор из kimi/codex/hermes/claude-headless) по задаче пилота (DP.SC.154). Ведёт turn-loop (2 участника) или round-loop (3+, WP-509), обнаруживает CONSENSUS/ESCALATE, после консенсуса — Decision Gate (зафиксировать vs реализовать → ревью →…

TserenTserenov/FMT-exocortex-template · 118 tokens

kimi-peer-writer

Peer-сессия DP.SC.154 где Kimi = писатель, Claude = напарник. Запускается простой фразой. Включает ОРЗ Opening и Closing, turn-loop, эскалации, Decision Gate (зафиксировать vs реализовать → ревью → проверить → задеплоить), отложенную финализацию и верификацию.

TserenTserenov/FMT-exocortex-template · 84 tokens

pack-new

Create a new Pack — guided flow through SPF: choose domain, name Pack, scaffold structure, fill roadmap.

TserenTserenov/FMT-exocortex-template · 25 tokens

apply-captures

Разбор extraction-reports со status pending-review — решение R15 (accept/reject/defer) ЖИВЫМ ПИЛОТОМ, запись в Pack, обновление статуса, коммит. Вызывать при Close при наличии N>0 pending-review отчётов.

TserenTserenov/FMT-exocortex-template · 63 tokens

archgate

Оценка архитектурного решения по 7 характеристикам ЭМОГССБ (v3.1 — фильтр допуска, атрибут-сценарии, совет затронутых сторон; профиль без агрегатного балла, conjunctive screening). Используй когда пользователь предлагает архитектурное решение, новый инструмент или системное изменение.

TserenTserenov/FMT-exocortex-template · 75 tokens

bottleneck-pick

Аналитик ограничений (DP.ROLE.054): находит главное ограничение (bottleneck) конкретного конвейера через TOC Five Steps + EC + NBR и строит Stage Dependency Map. Используй ТОЛЬКО при работе с конкретным WP, эпиком, проектом или weekplan (--target WP-NNN|weekplan|pilot:id). НЕ используй для общих вопросов приоритизации…

TserenTserenov/FMT-exocortex-template · 104 tokens