Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Diagnose missing data patterns and apply appropriate imputation strategies
.claude/skills/brycewang-stanford-missing-data-handling/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | -25% | 0% |
| case-22 | ✓→✓ | = Same ✓ | 103% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 60% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 152% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 72% | 0% |
A skill for diagnosing missing data mechanisms, selecting appropriate imputation strategies, and conducting sensitivity analyses. Covers everything from simple imputation to multiple imputation and modern machine learning approaches.
Understanding the mechanism determines the appropriate handling strategy:
| Mechanism | Definition | Example | Implication | |-----------|-----------|---------|-------------| | MCAR | Missingness unrelated to any variable | Lab sample randomly contaminated | Listwise deletion is unbiased (but loses power) | | MAR | Missingness related to observed variables | Higher-income respondents skip income question less | Multiple imputation appropriate | | MNAR | Missingness related to the missing value itself | Depressed patients drop out of depression study | Requires sensitivity analysis; no simple fix |
pythonimport pandas as pd import numpy as np from scipy import stats def diagnose_missing_data(df: pd.DataFrame) -> dict: """ Diagnose missing data patterns and mechanism. """ n_rows, n_cols = df.shape results = { 'total_cells': n_rows * n_cols, 'total_missing': df.isnull().sum().sum(), 'pct_missing': (df.isnull().sum().sum() / (n_rows * n_cols)) * 100, 'by_column': {} } for col in df.columns: n_missing = df[col].isnull().sum() pct = n_missing / n_rows * 100 results['by_column'][col] = { 'n_missing': n_missing, 'pct_missing': round(pct, 2) } # Little's MCAR test approximation # Compare means of other variables between missing/non-missing groups mcar_tests = {} for col in df.columns: if df[col].isnull().sum() > 0: missing_mask = df[col].isnull() for other_col in df.select_dtypes(include=[np.number]).columns: if other_col != col and df[other_col].isnull().sum() == 0: group_missing = df.loc[missing_mask, other_col] group_observed = df.loc[~missing_mask, other_col] if len(group_missing) > 1 and len(group_observed) > 1: t_stat, p_val = stats.ttest_ind(group_missing, group_observed) mcar_tests[f'{col}_vs_{other_col}'] = { 't': round(t_stat, 3), 'p': round(p_val, 4) } significant_diffs = sum(1 for v in mcar_tests.values() if v['p'] < 0.05) results['mcar_assessment'] = ( 'Likely MCAR' if significant_diffs == 0 else f'Likely NOT MCAR ({significant_diffs} significant differences found)' ) results['mcar_tests'] = mcar_tests return results
pythondef simple_imputation(df: pd.DataFrame, strategy: str = 'mean') -> pd.DataFrame: """ Apply simple imputation strategies. Args: strategy: 'mean', 'median', 'mode', 'constant', or 'forward_fill' """ imputed = df.copy() for col in imputed.columns: if imputed[col].isnull().any(): if strategy == 'mean' and np.issubdtype(imputed[col].dtype, np.number): imputed[col].fillna(imputed[col].mean(), inplace=True) elif strategy == 'median' and np.issubdtype(imputed[col].dtype, np.number): imputed[col].fillna(imputed[col].median(), inplace=True) elif strategy == 'mode': imputed[col].fillna(imputed[col].mode()[0], inplace=True) elif strategy == 'forward_fill': imputed[col].ffill(inplace=True) return imputed
The gold standard for MAR data:
pythonfrom sklearn.experimental import enable_iterative_imputer from sklearn.impute import IterativeImputer from sklearn.linear_model import BayesianRidge def multiple_imputation(df: pd.DataFrame, n_imputations: int = 20, max_iter: int = 50) -> list[pd.DataFrame]: """ Perform Multiple Imputation by Chained Equations (MICE). Args: df: DataFrame with missing values (numeric columns only) n_imputations: Number of imputed datasets (>=20 recommended) max_iter: Maximum iterations per imputation Returns: List of completed DataFrames """ imputed_datasets = [] for i in range(n_imputations): imputer = IterativeImputer( estimator=BayesianRidge(), max_iter=max_iter, random_state=i, sample_posterior=True # Important for proper MI ) imputed_data = imputer.fit_transform(df) imputed_df = pd.DataFrame(imputed_data, columns=df.columns, index=df.index) imputed_datasets.append(imputed_df) return imputed_datasets def pool_mi_results(estimates: list[float], variances: list[float]) -> dict: """ Pool results across multiply imputed datasets using Rubin's rules. Args: estimates: Parameter estimate from each imputed dataset variances: Variance of estimate from each imputed dataset """ m = len(estimates) q_bar = np.mean(estimates) # Pooled estimate u_bar = np.mean(variances) # Within-imputation variance b = np.var(estimates, ddof=1) # Between-imputation variance # Total variance total_var = u_bar + (1 + 1/m) * b # Degrees of freedom (Barnard-Rubin) lambda_hat = ((1 + 1/m) * b) / total_var df_old = (m - 1) / lambda_hat**2 se = np.sqrt(total_var) ci = (q_bar - 1.96*se, q_bar + 1.96*se) return { 'pooled_estimate': q_bar, 'pooled_se': se, 'ci_95': ci, 'fraction_missing_info': lambda_hat, 'relative_efficiency': 1 / (1 + lambda_hat/m) }
pythondef detect_outliers(series: pd.Series, method: str = 'iqr') -> pd.Series: """ Detect outliers using specified method. Returns boolean mask where True indicates an outlier. """ if method == 'iqr': q1 = series.quantile(0.25) q3 = series.quantile(0.75) iqr = q3 - q1 lower = q1 - 1.5 * iqr upper = q3 + 1.5 * iqr return (series < lower) | (series > upper) elif method == 'zscore': z = np.abs((series - series.mean()) / series.std()) return z > 3 elif method == 'mad': median = series.median() mad = np.median(np.abs(series - median)) modified_z = 0.6745 * (series - median) / (mad + 1e-10) return np.abs(modified_z) > 3.5 else: raise ValueError(f"Unknown method: {method}")
When reporting missing data handling in a paper:
Never simply delete missing data without justification. Even for MCAR data, listwise deletion reduces statistical power and is rarely the best choice.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-22 | pass→pass | 9,695 | 9,267 | -4% | 1 | 1 | 0% | 1,868 | 3,786 | +103% | 0 | 0 | — |
case-01 | fail→fail | 24,074 | 35,342 | +47% | 1 | 1 | 0% | 4,915 | 6,970 | +42% | 0 | 0 | — |
case-02 | fail→pass | 38,371 | 20,213 | -47% | 1 | 1 | 0% | 8,277 | 6,207 | -25% | 0 | 0 | — |
case-03 | pass→pass | 12,982 | 10,909 | -16% | 1 | 1 | 0% | 2,731 | 4,357 | +60% | 0 | 0 | — |
case-04 | pass→pass | 9,918 | 14,127 | +42% | 1 | 1 | 0% | 1,940 | 4,892 | +152% | 0 | 0 | — |
case-05 | pass→pass | 15,397 | 14,680 | -5% | 1 | 1 | 0% | 2,676 | 4,606 | +72% | 0 | 0 | — |
case-06 | pass→pass | 20,951 | 18,925 | -10% | 1 | 1 | 0% | 3,623 | 5,419 | +50% | 0 | 0 | — |
case-07 | pass→pass | 7,561 | 4,797 | -37% | 1 | 1 | 0% | 1,581 | 3,001 | +90% | 0 | 0 | — |
case-08 | pass→pass | 11,323 | 8,429 | -26% | 1 | 1 | 0% | 2,148 | 3,684 | +72% | 0 | 0 | — |
case-09 | pass→pass | 8,076 | 8,181 | +1% | 1 | 1 | 0% | 1,507 | 3,656 | +143% | 0 | 0 | — |
case-10 | pass→pass | 11,914 | 10,873 | -9% | 1 | 1 | 0% | 2,390 | 4,192 | +75% | 0 | 0 | — |
case-11 | pass→pass | 7,968 | 4,967 | -38% | 1 | 1 | 0% | 1,576 | 2,954 | +87% | 0 | 0 | — |
case-12 | pass→pass | 8,951 | 8,501 | -5% | 1 | 1 | 0% | 1,545 | 3,502 | +127% | 0 | 0 | — |
case-13 | pass→pass | 10,988 | 5,122 | -53% | 1 | 1 | 0% | 1,452 | 2,976 | +105% | 0 | 0 | — |
case-14 | pass→pass | 12,206 | 10,200 | -16% | 1 | 1 | 0% | 1,998 | 3,800 | +90% | 0 | 0 | — |
case-15 | pass→pass | 7,444 | 5,271 | -29% | 1 | 1 | 0% | 1,268 | 2,882 | +127% | 0 | 0 | — |
case-16 | pass→pass | 10,875 | 16,143 | +48% | 1 | 1 | 0% | 1,721 | 4,730 | +175% | 0 | 0 | — |
case-17 | pass→pass | 16,615 | 33,236 | +100% | 1 | 1 | 0% | 2,719 | 8,017 | +195% | 0 | 0 | — |
case-18 | pass→pass | 12,740 | 12,698 | -0% | 1 | 1 | 0% | 1,914 | 4,176 | +118% | 0 | 0 | — |
case-19 | fail→fail | 10,492 | 11,312 | +8% | 1 | 1 | 0% | 1,600 | 4,185 | +162% | 0 | 0 | — |
case-20 | pass→pass | 15,183 | 16,618 | +9% | 1 | 1 | 0% | 2,693 | 5,004 | +86% | 0 | 0 | — |
case-21 | pass→pass | 16,852 | 16,973 | +1% | 1 | 1 | 0% | 2,571 | 4,719 | +84% | 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. The headline lift of +5 percentage points is the difference between those two pass rates over the 22 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.