Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Clean, normalize, and prepare raw datasets for analysis or ML. Covers missing value handling, deduplication, outlier treatment, type normalization, categorical encoding, and transformation logging.
.claude/skills/mkurman-dataset-cleaning/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | -6% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 2% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 15% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 61% | 0% |
| case-16 | ✗→✓ | ▲ Improved | 63% | 0% |
Dataset cleaning transforms raw, messy data into a consistent, analysis-ready form. Every operation must be documented: what was changed, why, and how many rows/columns were affected. This is not a one-shot script — it's a reproducible pipeline.
Use this skill when:
dropna().Do not use for:
exploratory-data-analysis first.dataset-splitting.dataset-versioning.Choose and document ONE strategy per column:
| Strategy | When to use | Risk | ||-------------|------| | Drop rows | < 5% missing, rows are independent | Loss of rare cases | | Drop column | > 40% missing and not critical | Loss of signal | | Mean/median imputation | Continuous, symmetric distribution | Underestimates variance | | Mode imputation | Categorical, dominant class clear | Over-represents majority | | Constant fill | Domain-knowledge default exists | May introduce bias | | Model-based imputation | High missingness, strong predictors | Leakage if not careful | | Indicator flag | Missingness itself is informative | Adds dimensionality |
pythonimport pandas as pd import numpy as np # NEVER do this silently: # df.dropna(inplace=True) # Instead — audit first: missing = df.isnull().sum() missing_pct = df.isnull().mean() * 100 print(missing_pct[missing_pct > 0].sort_values(ascending=False)) # Documented imputation with audit trail: audit = {} mask = df['age'].isnull() audit['age_imputed_count'] = mask.sum() df.loc[mask, 'age'] = df['age'].median() df['age_imputed'] = mask.astype(int) # flag for downstream awareness
python# Define identity columns explicitly before deduping identity_cols = ['user_id', 'timestamp'] n_before = len(df) df = df.drop_duplicates(subset=identity_cols, keep='first') audit['duplicates_removed'] = n_before - len(df) # Near-duplicate detection (fuzzy): from difflib import SequenceMatcher # Use for text fields where exact match is too strict
python# Date/time normalization df['created_at'] = pd.to_datetime(df['created_at'], utc=True, errors='coerce') # String normalization df['category'] = df['category'].str.strip().str.lower().str.replace(r'\s+', '_', regex=True) # Numeric coercion with audit original = df['price'].copy() df['price'] = pd.to_numeric(df['price'], errors='coerce') audit['price_coerced_nulls'] = df['price'].isnull().sum() - original.isnull().sum()
python# Domain-based capping, not arbitrary percentiles # Example: age cannot be < 0 or > 120 df.loc[df['age'] < 0, 'age'] = np.nan df.loc[df['age'] > 120, 'age'] = 120 # cap, don't drop # For statistical outliers — use IQR with domain validation: Q1 = df['value'].quantile(0.25) Q3 = df['value'].quantile(0.75) IQR = Q3 - Q1 lower = Q1 - 3.0 * IQR # wider fence for less aggressive removal upper = Q3 + 3.0 * IQR outliers = (df['value'] < lower) | (df['value'] > upper) audit['outliers_flagged'] = outliers.sum() df['outlier_flag'] = outliers.astype(int)
python# One-hot for < 20 categories, label encoding otherwise n_unique = df['category'].nunique() if n_unique <= 20: df = pd.get_dummies(df, columns=['category'], drop_first=True) else: df['category_code'] = df['category'].astype('category').cat.codes
Always produce a structured audit log:
pythonaudit = { 'rows_before': n_before, 'rows_after': n_after, 'columns_before': cols_before, 'columns_after': cols_after, 'missing_handled': {col: strategy for col, strategy in missing_strategies.items()}, 'duplicates_removed': dupes, 'outliers_flagged': outliers, 'type_coercions': type_changes, }
Save audit alongside the cleaned dataset as cleaning_audit.json.
After cleaning, verify:
For datasets beyond simple tabular cleaning, combine with:
embedding-analysis skill for meaning-based near-duplicate removal at scale (NeMo Curator SemDedup, LSHBloom).embedding-analysis skill for GRAPE-style quality scoring with a reference language model.llm-assisted-curation skill for clarity/correctness/usefulness scoring per example.hf-datasets skill for Arrow-backed streaming when data exceeds RAM.embedding-analysis skill for JS divergence and Wasserstein distance between splits.These are referenced in the parent guideline dataset-creation-curation-task and should be applied after standard cleaning when dataset size or quality demands warrant.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 22,339 | 15,646 | -30% | 1 | 1 | 0% | 4,405 | 5,383 | +22% | 0 | 0 | — |
case-02 | fail→pass | 22,860 | 14,417 | -37% | 1 | 1 | 0% | 4,933 | 4,644 | -6% | 0 | 0 | — |
case-03 | fail→pass | 22,379 | 15,727 | -30% | 1 | 1 | 0% | 4,832 | 4,926 | +2% | 0 | 0 | — |
case-04 | pass→pass | 17,773 | 16,615 | -7% | 1 | 1 | 0% | 3,905 | 5,028 | +29% | 0 | 0 | — |
case-05 | pass→pass | 15,479 | 12,200 | -21% | 1 | 1 | 0% | 2,988 | 3,900 | +31% | 0 | 0 | — |
case-06 | pass→pass | 17,103 | 17,972 | +5% | 1 | 1 | 0% | 3,021 | 4,984 | +65% | 0 | 0 | — |
case-07 | pass→pass | 12,534 | 7,455 | -41% | 1 | 1 | 0% | 2,129 | 2,879 | +35% | 0 | 0 | — |
case-08 | pass→pass | 16,463 | 11,584 | -30% | 1 | 1 | 0% | 2,762 | 3,546 | +28% | 0 | 0 | — |
case-09 | pass→pass | 9,180 | 7,363 | -20% | 1 | 1 | 0% | 1,695 | 2,907 | +72% | 0 | 0 | — |
case-10 | fail→pass | 13,960 | 7,101 | -49% | 1 | 1 | 0% | 2,445 | 2,800 | +15% | 0 | 0 | — |
case-11 | pass→pass | 11,694 | 7,243 | -38% | 1 | 1 | 0% | 2,176 | 2,813 | +29% | 0 | 0 | — |
case-12 | fail→pass | 13,361 | 11,556 | -14% | 1 | 1 | 0% | 2,285 | 3,669 | +61% | 0 | 0 | — |
case-13 | pass→pass | 8,057 | 7,081 | -12% | 1 | 1 | 0% | 1,444 | 2,801 | +94% | 0 | 0 | — |
case-14 | pass→pass | 3,781 | 3,285 | -13% | 1 | 1 | 0% | 680 | 2,077 | +205% | 0 | 0 | — |
case-15 | pass→pass | 10,068 | 11,426 | +13% | 1 | 1 | 0% | 1,801 | 3,165 | +76% | 0 | 0 | — |
case-16 | fail→pass | 6,555 | 1,669 | -75% | 1 | 1 | 0% | 1,053 | 1,718 | +63% | 0 | 0 | — |
case-17 | pass→pass | 12,759 | 8,439 | -34% | 1 | 1 | 0% | 2,325 | 3,030 | +30% | 0 | 0 | — |
case-18 | pass→pass | 16,405 | 12,519 | -24% | 1 | 1 | 0% | 3,141 | 3,731 | +19% | 0 | 0 | — |
case-19 | fail→pass | 21,514 | 5,695 | -74% | 1 | 1 | 0% | 939 | 2,502 | +166% | 0 | 0 | — |
case-20 | fail→pass | 13,310 | 14,092 | +6% | 1 | 1 | 0% | 2,463 | 4,074 | +65% | 0 | 0 | — |
case-21 | pass→pass | 12,792 | 8,283 | -35% | 1 | 1 | 0% | 2,109 | 3,106 | +47% | 0 | 0 | — |
case-22 | pass→pass | 13,403 | 11,759 | -12% | 1 | 1 | 0% | 2,295 | 3,420 | +49% | 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 +32 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.