aspi6246/data-profiler
> Systematic dataset profiling protocol for empirical research. Use this skill when the user has a new dataset and wants to understand it before analysis — including unit of observation, variable definitions, panel structure, data quality, and descriptive statistics. Trigger on phrases like "explore this data", "profile this dataset", "what's in this data", "understand this dataset", "describe this data", "what are the variables", "check the panel structure", or any request to examine a dataset before running regressions.
npx skills add https://github.com/aspi6246/Claude-Code-Skills-for-Academics --skill data-profiler
This skill implements a structured protocol for profiling a dataset before
any analysis begins. The goal is to produce a dataset profile document
that records your understanding of the data and becomes permanent reference
material for the project.
Do not skip phases. Do not jump to regressions. The boring profiling work
done here prevents data quality bugs from surfacing three weeks into the
analysis.
tidyverse for all data manipulation (only data.table for large datasets)arrow for reading Parquet files, fread() for CSVBefore looking at any numbers, understand what you are dealing with.
# Format, size, dimensions
file.info("path/to/data")
dt <- fread("path/to/data") # or read_parquet()
dim(dt)
Report: file format, file size, number of rows, number of columns.
# Names, types, first few values
str(dt)
head(dt)
sapply(dt, class)
Produce a column inventory table:
| Column | Type | Example Values | Apparent Purpose |
|--------|------|---------------|-----------------|
| ... | ... | ... | ... |
For each column, make an initial guess about what it represents based on
the name and values. Flag any ambiguous names.
Look for columns that might serve as row identifiers or panel keys:
in the name
# Check cardinality of candidate identifiers
dt[, .(n_unique = uniqueN(.SD[[1]])), .SDcols = candidate_cols]
This is the most important phase. Get this wrong and every subsequent
analysis is built on sand.
State a hypothesis: "Each row appears to represent a [firm-year /
well-month / trade / person-quarter / ...]"
# Test whether candidate key is unique
candidate_key <- c("firm_id", "year")
n_total <- nrow(dt)
n_unique <- uniqueN(dt, by = candidate_key)
cat(sprintf("Rows: %d | Unique keys: %d | Duplicates: %d\n",
n_total, n_unique, n_total - n_unique))
If duplicates exist, investigate:
# Show examples of duplicate keys
dupes <- dt[duplicated(dt, by = candidate_key) |
duplicated(dt, by = candidate_key, fromLast = TRUE)]
head(dupes[order(candidate_key)], 20)
# Entities and time periods
n_entities <- uniqueN(dt, by = "firm_id")
n_periods <- uniqueN(dt, by = "year")
cat(sprintf("Entities: %d | Periods: %d | Expected if balanced: %d | Actual: %d\n",
n_entities, n_periods, n_entities * n_periods, nrow(dt)))
# Entry and exit
entity_spans <- dt[, .(first = min(year), last = max(year), n_obs = .N),
by = firm_id]
summary(entity_spans)
Report:
Profile every variable. For datasets with many columns (>30), profile all
identifier and key variables, then ask the user which remaining variables
to profile in detail.
For each continuous variable, report:
dt[, .(
n = .N,
n_miss = sum(is.na(var)),
pct_miss = round(100 * mean(is.na(var)), 1),
mean = round(mean(var, na.rm = TRUE), 4),
sd = round(sd(var, na.rm = TRUE), 4),
min = min(var, na.rm = TRUE),
p1 = quantile(var, 0.01, na.rm = TRUE),
p25 = quantile(var, 0.25, na.rm = TRUE),
median = median(var, na.rm = TRUE),
p75 = quantile(var, 0.75, na.rm = TRUE),
p99 = quantile(var, 0.99, na.rm = TRUE),
max = max(var, na.rm = TRUE)
)]
Flag:
stored as decimals vs. whole numbers)
For each categorical variable:
# Frequency table (top values + count)
dt[, .N, by = var][order(-N)][1:min(.N, 15)]
# Number of distinct values
uniqueN(dt$var)
Flag:
dt[, .(
min_date = min(date_var, na.rm = TRUE),
max_date = max(date_var, na.rm = TRUE),
n_miss = sum(is.na(date_var)),
n_unique = uniqueN(date_var)
)]
Flag:
# Overall missingness by column
miss <- dt[, lapply(.SD, function(x) round(100 * mean(is.na(x)), 1))]
sort(unlist(miss), decreasing = TRUE)
Flag:
the data comes from different sources or vintages)
(PERMNO for CRSP, GVKEY for Compustat, API number for wells)?
# Examples of checks — adapt to the specific dataset
dt[price < 0] # Negative prices
dt[percentage > 100 | percentage < 0] # Out-of-range percentages
dt[start_date > end_date] # Inverted date ranges
dt[age < 0 | age > 150] # Impossible ages
For key variable pairs, check whether relationships make sense:
# Correlation matrix for key continuous variables
cor(dt[, .(var1, var2, var3)], use = "pairwise.complete.obs")
If the data will be merged with standard datasets, check identifier
compatibility:
After completing all phases, produce a structured Markdown profile
document. Read references/profile-template.md for the full template.
Save the profile at: [project_root]/data/processed/dataset_profile.md
(or wherever the user prefers — ask before writing).
This document becomes institutional memory for the project. It should
be comprehensive enough that someone unfamiliar with the data can read
it and understand what they're working with.
Take aspi6246/data-profiler from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.