Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Strategic statistical modeling, experimentation, and causal inference
.claude/skills/brycewang-stanford-modeling-strategy-guide/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-10 | ✗→✓ | ▲ Improved | 52% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 144% | 0% |
| case-22 | ✗→✓ | ▲ Improved | 82% | 0% |
| case-09 | ✓→✗ | ▼ Worse | 82% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 55% | 0% |
A skill for strategic statistical modeling applied to academic research. Covers advanced modeling decisions, experimental design, causal inference, feature engineering, and the critical thinking required to move from data to defensible conclusions.
Senior data scientists distinguish themselves not by knowing more algorithms but by asking better questions, designing cleaner experiments, and being honest about what the data can and cannot tell them. This skill translates that professional discipline into a research context, helping academics apply modern data science practices to their empirical work. It covers the strategic decisions that matter most: when to use simple models versus complex ones, how to establish causality rather than mere correlation, and how to communicate uncertainty honestly.
The skill is particularly useful for researchers working with observational data who need causal inference techniques, those designing randomized experiments who need proper power calculations and analysis plans, and anyone building predictive models who needs to avoid common overfitting and leakage pitfalls.
Decision Framework:
1. Start with the simplest model that could answer your question
2. Add complexity only when diagnostics reveal inadequacy
3. Prefer interpretable models unless prediction accuracy is the sole goal
4. Always have a baseline (mean, majority class, last observation)
Model Complexity Ladder:
Level 1: Descriptive statistics, cross-tabulations
Level 2: Linear/logistic regression
Level 3: Regularized regression (Lasso, Ridge, Elastic Net)
Level 4: Tree ensembles (Random Forest, Gradient Boosting)
Level 5: Deep learning (only with sufficient data and clear justification)pythonimport pandas as pd import numpy as np def engineer_features(df: pd.DataFrame, config: dict) -> pd.DataFrame: """ Apply systematic feature engineering based on domain knowledge. config example: { 'log_transform': ['income', 'citations'], 'interactions': [('experience', 'education')], 'polynomial': {'age': 2}, 'time_features': 'date_column', 'lag_features': {'metric': [1, 7, 30]} } """ df = df.copy() # Log transforms for right-skewed variables for col in config.get('log_transform', []): df[f'{col}_log'] = np.log1p(df[col]) # Interaction terms for col_a, col_b in config.get('interactions', []): df[f'{col_a}_x_{col_b}'] = df[col_a] * df[col_b] # Polynomial features for col, degree in config.get('polynomial', {}).items(): for d in range(2, degree + 1): df[f'{col}_pow{d}'] = df[col] ** d # Time-based features if 'time_features' in config: time_col = config['time_features'] df[time_col] = pd.to_datetime(df[time_col]) df[f'{time_col}_month'] = df[time_col].dt.month df[f'{time_col}_dayofweek'] = df[time_col].dt.dayofweek df[f'{time_col}_quarter'] = df[time_col].dt.quarter return df
| Method | When to Use | Key Assumption | |--------|-----------|---------------| | Randomized experiment | You can randomly assign treatment | Proper randomization, no attrition | | Difference-in-differences | Policy change affects one group | Parallel trends pre-treatment | | Regression discontinuity | Treatment assigned by cutoff | No manipulation near cutoff | | Instrumental variables | Endogeneity present | Valid instrument (relevance + exclusion) | | Propensity score matching | Observational data, many confounders | No unobserved confounders | | Synthetic control | Single treated unit, many controls | Good pre-treatment fit |
pythonfrom sklearn.linear_model import LogisticRegression from sklearn.neighbors import NearestNeighbors def propensity_score_match(df, treatment_col, covariates, caliper=0.05): """ Match treated and control units based on propensity scores. """ # Estimate propensity scores X = df[covariates].values y = df[treatment_col].values lr = LogisticRegression(max_iter=1000, random_state=42) lr.fit(X, y) df['pscore'] = lr.predict_proba(X)[:, 1] # Match using nearest neighbor within caliper treated = df[df[treatment_col] == 1] control = df[df[treatment_col] == 0] nn = NearestNeighbors(n_neighbors=1, metric='euclidean') nn.fit(control[['pscore']].values) distances, indices = nn.kneighbors(treated[['pscore']].values) # Apply caliper valid = distances.flatten() < caliper matched_treated = treated[valid].index.tolist() matched_control = control.iloc[indices.flatten()[valid]].index.tolist() return { 'matched_treated': matched_treated, 'matched_control': matched_control, 'n_matched': sum(valid), 'n_unmatched': sum(~valid), 'balance_check': 'Run standardized mean differences on covariates' }
pythonfrom scipy import stats import numpy as np def design_experiment(baseline_rate, mde, alpha=0.05, power=0.80): """ Calculate required sample size for a two-proportion z-test. Args: baseline_rate: Current conversion/success rate mde: Minimum detectable effect (absolute change) alpha: Significance level power: Statistical power """ from statsmodels.stats.power import NormalIndPower effect_size = mde / np.sqrt(baseline_rate * (1 - baseline_rate)) analysis = NormalIndPower() n = analysis.solve_power( effect_size=effect_size, alpha=alpha, power=power, ratio=1.0 ) return { 'sample_size_per_group': int(np.ceil(n)), 'total_sample_size': int(np.ceil(n)) * 2, 'baseline_rate': baseline_rate, 'minimum_detectable_effect': mde, 'alpha': alpha, 'power': power }
Before running any experiment, document:
| Data Type | Recommended CV | Rationale | |-----------|---------------|-----------| | i.i.d. data | Stratified K-fold (K=5 or 10) | Preserves class balance | | Time series | Time-series split (expanding window) | Prevents look-ahead bias | | Grouped data | Group K-fold | Prevents data leakage across groups | | Small dataset (n<200) | Leave-one-out or repeated K-fold | Maximizes training data | | Spatial data | Spatial blocking | Prevents spatial autocorrelation leakage |
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 28,977 | 19,922 | -31% | 1 | 1 | 0% | 5,816 | 5,558 | -4% | 0 | 0 | — |
case-02 | fail→fail | 27,373 | 15,170 | -45% | 1 | 1 | 0% | 5,546 | 5,097 | -8% | 0 | 0 | — |
case-03 | pass→pass | 23,966 | 23,198 | -3% | 1 | 1 | 0% | 3,851 | 5,971 | +55% | 0 | 0 | — |
case-04 | fail→fail | 16,388 | 18,101 | +10% | 1 | 1 | 0% | 3,366 | 5,829 | +73% | 0 | 0 | — |
case-05 | pass→pass | 14,158 | 11,238 | -21% | 1 | 1 | 0% | 2,422 | 3,997 | +65% | 0 | 0 | — |
case-06 | pass→pass | 5,482 | 8,076 | +47% | 1 | 1 | 0% | 904 | 3,405 | +277% | 0 | 0 | — |
case-07 | pass→pass | 8,429 | 10,301 | +22% | 1 | 1 | 0% | 1,431 | 3,761 | +163% | 0 | 0 | — |
case-08 | fail→fail | 10,048 | 13,939 | +39% | 1 | 1 | 0% | 1,735 | 4,574 | +164% | 0 | 0 | — |
case-09 | pass→fail | 19,354 | 22,565 | +17% | 1 | 1 | 0% | 3,105 | 5,660 | +82% | 0 | 0 | — |
case-10 | fail→pass | 21,916 | 19,059 | -13% | 1 | 1 | 0% | 3,241 | 4,933 | +52% | 0 | 0 | — |
case-16 | pass→pass | 15,077 | 18,975 | +26% | 1 | 1 | 0% | 2,500 | 5,244 | +110% | 0 | 0 | — |
case-11 | pass→pass | 13,747 | 18,594 | +35% | 1 | 1 | 0% | 2,247 | 5,336 | +137% | 0 | 0 | — |
case-12 | fail→fail | 19,752 | 28,785 | +46% | 1 | 1 | 0% | 3,151 | 6,135 | +95% | 0 | 0 | — |
case-13 | pass→pass | 10,248 | 8,939 | -13% | 1 | 1 | 0% | 1,743 | 3,581 | +105% | 0 | 0 | — |
case-14 | pass→pass | 9,794 | 10,543 | +8% | 1 | 1 | 0% | 1,571 | 3,764 | +140% | 0 | 0 | — |
case-15 | pass→pass | 11,285 | 17,257 | +53% | 1 | 1 | 0% | 1,892 | 5,075 | +168% | 0 | 0 | — |
case-17 | fail→fail | 17,622 | 19,812 | +12% | 1 | 1 | 0% | 2,811 | 4,960 | +76% | 0 | 0 | — |
case-18 | fail→pass | 15,761 | 20,856 | +32% | 1 | 1 | 0% | 2,248 | 5,494 | +144% | 0 | 0 | — |
case-19 | pass→pass | 24,559 | 26,934 | +10% | 1 | 1 | 0% | 5,215 | 7,778 | +49% | 0 | 0 | — |
case-20 | pass→pass | 15,798 | 16,862 | +7% | 1 | 1 | 0% | 2,821 | 4,969 | +76% | 0 | 0 | — |
case-21 | pass→pass | 14,929 | 15,594 | +4% | 1 | 1 | 0% | 2,826 | 4,774 | +69% | 0 | 0 | — |
case-22 | fail→pass | 15,203 | 16,810 | +11% | 1 | 1 | 0% | 2,901 | 5,288 | +82% | 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 +9 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
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.