[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6EKTFACFHDJ0ZW3XSTBHPCD":3,"skill-same:01M6EKTFACFHDJ0ZW3XSTBHPCD":91},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":36,"linkOnly":37,"markdownContent":38,"markdownBody":39,"frontMatter":40,"refName":68,"commitSha":69,"skillDirectory":70,"forksCount":71,"repositoryHtmlUrl":72,"repositoryHomepageUrl":6,"ownerHtmlUrl":73,"ownerBlogUrl":74,"ownerDescription":75,"updatedAt":76,"install":77,"resources":90},"01M6EKTFACFHDJ0ZW3XSTBHPCD","hsls09-causal-conventions",null,"HSLS-specific causal conventions — treatment-relevant variable inventory, pre\u002Fpost-treatment temporal classification, MNAR diagnostics for postsecondary outcomes, and clustered-SE handling for causal estimators.","cgpan\u002Fedm-ars-public","cgpan","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F118746117?v=4","01M6EKT5TG36Q3GH340QN8RH08","Chenguang Pan","01M6EKT5YTQVRARGJF0VPA1SY7",5,"MIT","https:\u002F\u002Fgithub.com\u002Fcgpan\u002Fedm-ars-public\u002Fblob\u002Fmain\u002Fskills\u002Fdataset\u002Fhsls09-causal-conventions\u002FSKILL.md",[],[19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35],"conventions","hsls09 causal conventions","classification","critic","public","outcomes","writer","se","diagnostics","analyst","id","dataset","post","cluster","temporal","relevant","inventory","2026-11-10 10:12:30",false,"---\nname: hsls09-causal-conventions\nlayer: dataset\ndescription: HSLS-specific causal conventions — treatment-relevant variable inventory, pre\u002Fpost-treatment temporal classification, MNAR diagnostics for postsecondary outcomes, and clustered-SE handling for causal estimators.\ntrigger_keywords:\n  - hsls\n  - hsls09\n  - causal\n  - treatment\n  - mnar\n  - clustered\n  - cluster-robust\n  - pre-treatment\n  - post-treatment\napplicable_task_types:\n  - causal_soo\n  - causal_itr\napplicable_datasets:\n  - hsls09_public\napplicable_stages:\n  - ProblemFormulator\n  - DataEngineer\n  - Analyst\n  - Critic\n  - Writer\npriority: 1\nreferences_skills:\n  - cluster-id-reconstruction-from-fingerprints\nresources: []\nversion: \"1.0\"\nrule_severity: mandatory\n---\n\n# HSLS:09 Causal Conventions\n\nHSLS-specific extensions to the cross-dataset causal methodology\nskills (G1–G5). Covers the four areas where causal analysis on HSLS\nneeds concrete, dataset-anchored rules: treatment-variable choice,\npre\u002Fpost temporal classification, attrition-driven MNAR for\npostsecondary outcomes, and clustered-SE handling at the school\nlevel. Pre\u002Fpost misclassification is silent corruption (introduces\npost-treatment bias), so this skill is `rule_severity: mandatory`.\n\n## Treatment-relevant variable inventory\n\n### Continuous attitudinal scales (psychometric)\n\nThese are **psychometric scales** that require thresholding or binning\ninto a defined contrast. **No \"per-unit\" causal interpretation** is\nadmissible — the unit is a standardized factor score, not a behavior.\n\n| Variable | Label |\n|---|---|\n| `X1MTHID` | Math identity scale |\n| `X1MTHEFF` | Math self-efficacy scale |\n| `X1SCHOOLBEL` | School belonging scale |\n| `X1SCIID` | Science identity scale |\n| ... | other `X1*` attitudinal composites in the registry |\n\nA causal study that uses one of these as a treatment **must** declare\nits `treatment_contrast` in `research_spec.treatment_contrast`\n(median split, `>= threshold`, `+1 SD`, etc., per G2's contrast\nschema).\n\n### Course-taking \u002F attainment indicators\n\nThese are observable behavioral indicators usable as treatments\nwithout further binning:\n\n| Variable | Label |\n|---|---|\n| `X3TCREDMAT` | Math credits earned |\n| `X4HSCOMPSTAT` | High school credential status |\n| `X4EVRATNDCLG` | Ever attended college |\n\n### Variables NEVER suitable as treatments\n\n| Pattern | Reason |\n|---|---|\n| `W*` (e.g., `W4W1W2W3STU`) | Sampling weights; not causally manipulable. |\n| `*QSTAT` (e.g., `X1SQSTAT`) | Survey-status indicators; reflect missingness, not treatment. |\n| `X1SEX`, `X1RACE` | Demographic protected attributes; **use only as moderators** in CATE \u002F subgroup analyses, never as treatments under selection-on-observables. |\n\n## Pre\u002Fpost-treatment temporal classification\n\n| Treatment wave | Pre-treatment covariates allowed |\n|---|---|\n| Any X1 treatment | `X1` baseline variables AND demographics. |\n| Any X2 treatment | X1 + X2_baseline (excluding X2 outcomes). |\n| Any X3 treatment | X1 + X2 + X3_baseline. |\n| Any X4 treatment | X1 + X2 + X3 + X4_baseline. |\n\nAnything from a wave **after** the treatment wave is **post-treatment**\nand forbidden in the adjustment set per G1's back-door criterion (no\ndescendants of `T`).\n\n**Mandatory rule:** every covariate in `research_spec.adjustment_set`\nmust have its temporal status declared in\n`research_spec.covariate_temporal_table`. The wave-prefix mapping is\ndefined in `hsls09-temporal-ordering` (existing V2.0 dataset skill);\nthis skill extends it for the causal-adjustment-set use case.\n\n## Selection-bias-from-attrition rules for postsecondary outcomes\n\n| Outcome wave | Approximate missingness | MAR\u002FMNAR risk |\n|---|---|---|\n| `X4*` (2016 update panel) | ~26% | MAR-leaning, with treatment-correlated patterns |\n| `X5*` (2017 postsecondary records) | 50%+ | MAR-to-MNAR, structurally |\n\nFor causal targets on postsecondary outcomes:\n\n- The analytic sample is **structurally restricted to respondents** —\n  attrition is not random across treatment groups.\n- Report **both** an ITT-analogue (X4SQSTAT-respondents only) and the\n  as-treated equivalent. Divergence between the two is informative\n  about selection bias.\n- Recommend running parallel **IPW-for-missingness** analyses (see M4\n  `causal-aipw-tmle`'s missingness-augmented branch) to bound the\n  missingness-induced bias.\n\n## Clustered-SE handling for causal estimators\n\nThis section extends `cluster-id-reconstruction-from-fingerprints`\n(existing V2.0 methodology skill) for the causal context — the\npseudo-school-IDs reconstructed there flow into the causal-estimator\nSE machinery here.\n\n| Estimator | Clustered-SE recipe |\n|---|---|\n| **IPW (M3)** | `statsmodels` weighted regression with `cov_type='cluster'`, `cluster_groups=pseudo_school_id`. |\n| **PSM (M2)** | Cluster-bootstrap on matched pairs aggregated at school level (matched pair as resampling unit, school as cluster). |\n| **AIPW \u002F TMLE (M4)** | Influence-function variance with clustered correction (sum within clusters, then variance across clusters). |\n| **Causal forest (M5)** | `econml`'s `inference=\"auto\"` does NOT natively cluster — note this limitation; recommend cluster-bootstrap of the ATE estimate (computationally expensive — see audit Open Question #9). |\n\n## Survey-weights handling\n\n**V3.0 default: do NOT apply HSLS survey weights.**\n\nThe estimand is the **analytic-sample marginal effect**, not the\npopulation marginal effect. The Writer must state this explicitly in\nboth §Methods and §Limitations.\n\nThis skill composes `hsls09-survey-weights-limitations-paragraph`\n(existing V2.0 writing skill) so the Writer's limitation prose stays\nconsistent with the prediction-task convention.\n\n## Mandatory tagging\n\n`rule_severity: mandatory`. Pre\u002Fpost misclassification is silent\ncorruption — the resulting \"causal\" estimate absorbs post-treatment\nmediation into the treatment effect, biasing the headline number in a\ndirection that is hard to recover from after the fact.\n\n## Python implementation guidance\n\n**Primary library:** `pandas` + `statsmodels` (`cov_type='cluster'`).\n\n**Reference helpers:**\n\n```python\ndef cluster_bootstrap_ate(\n    df: pd.DataFrame,\n    cluster_col: str,\n    ate_estimator_fn: Callable[[pd.DataFrame], float],\n    n_boot: int = 1000,\n    random_state: int = 42,\n) -> tuple[float, float]: ...  # returns (ci_lower, ci_upper)\n\ndef declare_covariate_temporal_status(\n    covariates: list[str],\n    treatment_wave: str,\n) -> dict[str, Literal[\"pre\", \"post\", \"contemporaneous\"]]: ...\n```\n\n**Library pitfalls:**\n\n- `statsmodels` `cov_type='cluster'` requires `groups` argument as\n  a 1-D array of cluster IDs; pseudo-school-IDs must be aligned by\n  `iloc` to the regression's exogenous matrix.\n- The pseudo-school-IDs come from\n  `cluster-id-reconstruction-from-fingerprints` — that skill's\n  diagnostics MUST pass before its IDs are used here, or the\n  clustered SE inflates without basis.\n\n## Validation criteria\n\nThe SKILL contract requires that:\n\n1. The treatment-relevant variable inventory (with examples) is\n   present.\n2. The pre\u002Fpost-treatment temporal table is present.\n3. The postsecondary-attrition rules are present.\n4. The clustered-SE handling table per estimator is present.\n5. The no-survey-weights default is stated.\n6. `rule_severity: mandatory` is set in frontmatter.\n7. `references_skills` includes `cluster-id-reconstruction-from-fingerprints`.\n\nA Writer using this skill must be able to produce a §Methods\nsubsection naming (a) the analytic-sample estimand, (b) the\nno-survey-weights choice, and (c) the school-clustering correction\nmethod actually used.\n\nAn Analyst code artifact using this skill must produce:\n\n- `data_report.causal_covariate_temporal_table`,\n- `results.causal_estimate.cluster_se_method`,\n- `results.causal_estimate.cluster_se_ci`.\n\n## Encoded-column lookup (mandatory for analysis steps)\n\nCategorical variables in HSLS:09 (e.g., X1RACE, X1PAREDU, X1SEX, X1LOCALE, X1CONTROL,\nX1STUEDEXPCT) are one-hot encoded by the DataEngineer stage. The encoded columns are\nnamed `\u003Coriginal>_\u003Clevel>` — for example, `X1RACE` becomes `X1RACE_1` ... `X1RACE_8`.\n\n**Rule (mandatory):** when an analysis step references an adjustment-set variable that\nwas originally categorical, do NOT search train_X.csv for the original name. Instead,\ndiscover the encoded columns by prefix match:\n\n```python\ndef resolve_encoded_columns(varname: str, df_columns: list[str]) -> list[str]:\n    \"\"\"Return all encoded column names for a single original categorical variable.\"\"\"\n    direct = [c for c in df_columns if c == varname]\n    if direct:\n        return direct  # variable was not encoded (continuous or already binary)\n    encoded = [c for c in df_columns if c.startswith(varname + \"_\")]\n    if not encoded:\n        raise ValueError(f\"Adjustment-set variable {varname!r} not found in df.columns\")\n    return encoded\n```\n\nThe full adjustment set passed to a regression \u002F propensity model \u002F weight estimator is the\nunion of `resolve_encoded_columns(v, df.columns)` over all v in the spec's `adjustment_set`.\n\n**Failure mode this prevents (F-COVARIATE-SET-MISMATCH):** if the adjustment set declares\n`X1RACE` and the Analyst code searches for `\"X1RACE\" in df.columns`, the lookup fails\nsilently and the propensity \u002F outcome model is fit without race covariates — invalidating\nthe no-unmeasured-confounding assumption. LSAR will catch this as a FATAL issue.\n\n## Encoding-type discipline (mandatory for DataEngineer)\n\nThe previous \"Encoded-column lookup\" section governs the **Analyst-side**\nprefix-match lookup after encoding has already happened. The encoding step\nitself — what the DataEngineer writes to `train_X.csv` — is what this\nsection governs. The two rules are complementary: the DataEngineer chooses\nwhich variables to one-hot; the Analyst looks up whichever choice was made.\n\nThe HSLS:09 variable registry (`data_registry\u002Fdatasets\u002Fhsls09_public.yaml`)\ntags each variable with a `type` field. The DataEngineer MUST respect the\nregistry's `type` when constructing the analytic CSV. The 3b.6 D1 work\nintroduced the registry but did not codify a DataEngineer-side encoding\nrule against it; that gap is what this section closes.\n\n### Continuous variables — pass through, do NOT one-hot encode\n\nVariables tagged `type=continuous` in the registry represent measurements\nor scales with natural ordering and meaningful distances. Examples from\nthe registry that have appeared in adjustment-sets in prior runs:\n\n| Variable | Registry `type` | What it is |\n|---|---|---|\n| `X1MTHEFF` | continuous | Math self-efficacy scale (Wave-1 psychometric factor score, range −2.92 to 1.62) |\n| `X1MTHID` | continuous | Math identity scale (range −1.73 to 1.76) |\n| `X1MTHINT` | continuous | Math course interest scale (range −2.46 to 2.08) |\n| `X1MTHUTI` | continuous | Math utility scale (range −3.51 to 1.31) |\n| `X1SCIID` | continuous | Science identity scale (range −1.57 to 2.15) |\n| `X1SCHOOLBEL` | continuous | School belonging scale (range −4.35 to 1.59) |\n| `X1SES` | continuous | Socioeconomic-status composite (range −1.93 to 2.88) |\n| `X1TXMTSCOR` | continuous | Standardized math IRT score (range 24.0 to 82.2) |\n\nThese variables MUST be retained as scalar columns in `train_X.csv` and\n`test_X.csv`. The DataEngineer MUST NOT call `pd.get_dummies(...)` on them,\nMUST NOT bin them into categories, and MUST NOT one-hot encode their\ndistinct values — even when the underlying factor-score machinery\nproduces a finite discrete set of unique values that *looks* categorical.\nA psychometric scale with 56 distinct factor scores is still a continuous\nscale; encoding it as 56 binary dummies discards the scale information\nand inflates the predictor matrix in proportions that overfit downstream\npropensity models.\n\n### Categorical \u002F binary variables — one-hot encode\n\nVariables tagged `type=categorical` or `type=binary` ARE one-hot encoded.\nConcrete examples from the locked-spec adjustment-set space:\n\n| Variable | Registry `type` | What it is |\n|---|---|---|\n| `X1RACE` | categorical | Race\u002Fethnicity composite (8 unordered categories) |\n| `X1PAREDU` | categorical | Parents' highest education (7 ordered codes, but registry-tagged categorical) |\n| `X1STUEDEXPCT` | categorical | Student educational expectations (11 ordered codes, registry-tagged categorical) |\n| `X1LOCALE` | categorical | School urbanicity (4 unordered categories) |\n| `X1REGION` | categorical | Census region (4 unordered categories) |\n| `X1CONTROL` | binary | Public vs Catholic\u002Fprivate |\n| `X1SEX` | binary | Student sex |\n\nUse `pd.get_dummies(df[col], prefix=col, drop_first=False)` (no reference-\ncategory dropping; downstream models handle the collinearity, and the\n3b.6 `resolve_encoded_columns` Analyst-side lookup expects all levels\npresent).\n\n### Ordinal-but-registry-tagged-categorical — use the registry tag\n\nWhen the registry tags a variable `categorical` even though it has natural\nordering (e.g., `X1PAREDU` 1–7 on an education ladder, `X1STUEDEXPCT`\n1–11), respect the registry tag. The registry authors chose `categorical`\nbecause the codebook codes carry semantically-meaningful labels (`1 =\nLess than high school`, `2 = HS diploma\u002FGED`, etc.) and the ordering\nbetween adjacent codes is not numerically comparable. One-hot encoding\npreserves the option for downstream models to fit non-linear category\neffects.\n\n### Operationalization is separate from encoding\n\nIf the research_spec specifies an operationalization for a continuous\ntreatment (e.g., `median_split_binary` for `X1MTHEFF`), apply the\noperationalization per the `causal-data-engineer-contract` skill (3b.12).\nThe operationalization produces ONE additional column\n(`X1MTHEFF_binary`); it does NOT trigger one-hot encoding of the\nunderlying continuous variable. The continuous variable may itself be\nretained as a separate column, dropped, or kept-only-as-operationalized\nper the spec's intent.\n\n### Prescriptive recipe\n\n```python\nimport warnings\nimport pandas as pd\n\ndef encode_for_causal_soo(\n    df: pd.DataFrame,\n    feature_cols: list[str],\n    variable_registry: dict[str, dict],\n) -> pd.DataFrame:\n    \"\"\"Encode adjustment-set + treatment columns respecting registry types.\n\n    Args:\n        df: source DataFrame containing all `feature_cols`.\n        feature_cols: variable names from research_spec\n            (treatment + adjustment_set, in any order).\n        variable_registry: map from variable name to its registry entry,\n            including the `type` field.\n\n    Returns:\n        DataFrame with continuous\u002Fordinal-as-continuous columns passed\n        through as scalars and categorical\u002Fbinary columns one-hot\n        encoded with explicit prefixes.\n    \"\"\"\n    out = pd.DataFrame(index=df.index)\n    for col in feature_cols:\n        entry = variable_registry.get(col, {})\n        var_type = entry.get(\"type\", \"unknown\")\n\n        if var_type == \"continuous\":\n            # Pass through. Do NOT one-hot encode, even if df[col] has\n            # a small number of unique values (factor scores often do).\n            out[col] = df[col]\n\n        elif var_type in (\"categorical\", \"binary\"):\n            # One-hot encode. drop_first=False keeps all levels so the\n            # Analyst's resolve_encoded_columns prefix-match (3b.6 rule)\n            # finds every encoded variant.\n            dummies = pd.get_dummies(df[col], prefix=col, drop_first=False)\n            out = pd.concat([out, dummies], axis=1)\n\n        else:\n            # Conservative: pass through and warn. Registry-incomplete\n            # tagging is a registry concern, not a DataEngineer concern;\n            # do not silently re-classify.\n            warnings.warn(\n                f\"Variable {col!r} has registry type {var_type!r}; \"\n                f\"passing through as scalar. Update the registry if \"\n                f\"this should be one-hot encoded.\"\n            )\n            out[col] = df[col]\n\n    return out\n```\n\nThe function takes a `variable_registry` parameter rather than reading the\nYAML directly so the rule is testable in isolation. The DataEngineer's\ncode loads the YAML once and passes the appropriate slice (typically the\nfull `variables.predictors.*` flat dict) to this function.\n\n### Failure mode this prevents (F-3b15-DE-CONTINUOUS-AS-CATEGORICAL)\n\nThe 3b.15 \u002F 3b.17 evidence chain (recoverable in some runs by cycle-1\nCritic feedback, not recoverable in others):\n\n| Run | Cycle 0 cols | Cycle 1 cols | Mis-encoded continuous variables (cycle 0) | Outcome |\n|---|---:|---:|---|---|\n| 3b.13 | 56 | 56 | `X1SCIID` (16 dummies) | Clean — fewer continuous vars in PF's adjustment_set masked the issue |\n| 3b.15 | 109 | 11 | `X1MTHID` (16), `X1MTHUTI` (~70) | Cycle-1 over-corrected; LSAR 7.0 Accept |\n| 3b.17 | 116 | 115 | `X1MTHID` (17), `X1MTHUTI` (56), `X1STUEDEXPCT` (11) | Cycle-1 did NOT recover; LSAR 6.0 Borderline; 21% extreme-tail positivity violation flagged as \"fundamental identification failure\" (`F-3b15-DE-CONTINUOUS-AS-CATEGORICAL`) |\n\nThe mechanism is the same across runs: the DataEngineer defaults to\n`pd.get_dummies(...)` on every adjustment-set variable that isn't already\nbinary, regardless of registry `type`. When a continuous variable has a\nfinite set of distinct factor-score values (psychometric scales typically\ndo — IRT estimators produce a discrete grid), the DE interprets the\nfinite-value set as categorical and one-hot encodes. The effect compounds\nwhen multiple continuous scales appear in the adjustment_set: 3b.13's\nsingle mis-encoded scale produced a manageable 56-column matrix; 3b.17's\nthree mis-encoded scales produced a 115-column matrix that overfit the\npropensity model and broke positivity.\n\nThe Analyst-side `resolve_encoded_columns` rule (3b.6) is the wrong fix\nfor this — it correctly handles whatever encoding the DE chose, but it\ncannot un-do an incorrect encoding choice. The DE-side rule above is the\nfix: registry-type dispatch produces deterministic correct behavior\nindependent of which continuous variables happen to be in the\nadjustment_set.\n\n**Cross-reference to existing D1 rules:**\n- The Analyst-side `resolve_encoded_columns` lookup (above) reads\n  whatever encoding the DataEngineer wrote and is unaffected by this\n  rule.\n- The temporal-ordering rule in `hsls09-temporal-ordering` (V2.0\n  dataset skill) is independent — temporal classification of a\n  covariate happens before encoding decisions are made.\n- The `causal-data-engineer-contract` rule (3b.12) governs which\n  columns the DE writes (treatment + adjustment_set + outcome).\n  This rule governs HOW each adjustment_set column is encoded.\n\n## Source provenance\n\nCanonical source: the v3.0 causal-methods specification (internal) §3.6\n(D1 per-skill specification). HSLS variable details cross-reference\n`data_registry\u002Fdatasets\u002Fhsls09_public.yaml` and the existing V2.0\nskills `hsls09-temporal-ordering`, `hsls09-structural-mnar-outcomes`,\n`hsls09-school-fingerprints`, and\n`cluster-id-reconstruction-from-fingerprints`.\n","# HSLS:09 Causal Conventions\n\nHSLS-specific extensions to the cross-dataset causal methodology\nskills (G1–G5). Covers the four areas where causal analysis on HSLS\nneeds concrete, dataset-anchored rules: treatment-variable choice,\npre\u002Fpost temporal classification, attrition-driven MNAR for\npostsecondary outcomes, and clustered-SE handling at the school\nlevel. Pre\u002Fpost misclassification is silent corruption (introduces\npost-treatment bias), so this skill is `rule_severity: mandatory`.\n\n## Treatment-relevant variable inventory\n\n### Continuous attitudinal scales (psychometric)\n\nThese are **psychometric scales** that require thresholding or binning\ninto a defined contrast. **No \"per-unit\" causal interpretation** is\nadmissible — the unit is a standardized factor score, not a behavior.\n\n| Variable | Label |\n|---|---|\n| `X1MTHID` | Math identity scale |\n| `X1MTHEFF` | Math self-efficacy scale |\n| `X1SCHOOLBEL` | School belonging scale |\n| `X1SCIID` | Science identity scale |\n| ... | other `X1*` attitudinal composites in the registry |\n\nA causal study that uses one of these as a treatment **must** declare\nits `treatment_contrast` in `research_spec.treatment_contrast`\n(median split, `>= threshold`, `+1 SD`, etc., per G2's contrast\nschema).\n\n### Course-taking \u002F attainment indicators\n\nThese are observable behavioral indicators usable as treatments\nwithout further binning:\n\n| Variable | Label |\n|---|---|\n| `X3TCREDMAT` | Math credits earned |\n| `X4HSCOMPSTAT` | High school credential status |\n| `X4EVRATNDCLG` | Ever attended college |\n\n### Variables NEVER suitable as treatments\n\n| Pattern | Reason |\n|---|---|\n| `W*` (e.g., `W4W1W2W3STU`) | Sampling weights; not causally manipulable. |\n| `*QSTAT` (e.g., `X1SQSTAT`) | Survey-status indicators; reflect missingness, not treatment. |\n| `X1SEX`, `X1RACE` | Demographic protected attributes; **use only as moderators** in CATE \u002F subgroup analyses, never as treatments under selection-on-observables. |\n\n## Pre\u002Fpost-treatment temporal classification\n\n| Treatment wave | Pre-treatment covariates allowed |\n|---|---|\n| Any X1 treatment | `X1` baseline variables AND demographics. |\n| Any X2 treatment | X1 + X2_baseline (excluding X2 outcomes). |\n| Any X3 treatment | X1 + X2 + X3_baseline. |\n| Any X4 treatment | X1 + X2 + X3 + X4_baseline. |\n\nAnything from a wave **after** the treatment wave is **post-treatment**\nand forbidden in the adjustment set per G1's back-door criterion (no\ndescendants of `T`).\n\n**Mandatory rule:** every covariate in `research_spec.adjustment_set`\nmust have its temporal status declared in\n`research_spec.covariate_temporal_table`. The wave-prefix mapping is\ndefined in `hsls09-temporal-ordering` (existing V2.0 dataset skill);\nthis skill extends it for the causal-adjustment-set use case.\n\n## Selection-bias-from-attrition rules for postsecondary outcomes\n\n| Outcome wave | Approximate missingness | MAR\u002FMNAR risk |\n|---|---|---|\n| `X4*` (2016 update panel) | ~26% | MAR-leaning, with treatment-correlated patterns |\n| `X5*` (2017 postsecondary records) | 50%+ | MAR-to-MNAR, structurally |\n\nFor causal targets on postsecondary outcomes:\n\n- The analytic sample is **structurally restricted to respondents** —\n  attrition is not random across treatment groups.\n- Report **both** an ITT-analogue (X4SQSTAT-respondents only) and the\n  as-treated equivalent. Divergence between the two is informative\n  about selection bias.\n- Recommend running parallel **IPW-for-missingness** analyses (see M4\n  `causal-aipw-tmle`'s missingness-augmented branch) to bound the\n  missingness-induced bias.\n\n## Clustered-SE handling for causal estimators\n\nThis section extends `cluster-id-reconstruction-from-fingerprints`\n(existing V2.0 methodology skill) for the causal context — the\npseudo-school-IDs reconstructed there flow into the causal-estimator\nSE machinery here.\n\n| Estimator | Clustered-SE recipe |\n|---|---|\n| **IPW (M3)** | `statsmodels` weighted regression with `cov_type='cluster'`, `cluster_groups=pseudo_school_id`. |\n| **PSM (M2)** | Cluster-bootstrap on matched pairs aggregated at school level (matched pair as resampling unit, school as cluster). |\n| **AIPW \u002F TMLE (M4)** | Influence-function variance with clustered correction (sum within clusters, then variance across clusters). |\n| **Causal forest (M5)** | `econml`'s `inference=\"auto\"` does NOT natively cluster — note this limitation; recommend cluster-bootstrap of the ATE estimate (computationally expensive — see audit Open Question #9). |\n\n## Survey-weights handling\n\n**V3.0 default: do NOT apply HSLS survey weights.**\n\nThe estimand is the **analytic-sample marginal effect**, not the\npopulation marginal effect. The Writer must state this explicitly in\nboth §Methods and §Limitations.\n\nThis skill composes `hsls09-survey-weights-limitations-paragraph`\n(existing V2.0 writing skill) so the Writer's limitation prose stays\nconsistent with the prediction-task convention.\n\n## Mandatory tagging\n\n`rule_severity: mandatory`. Pre\u002Fpost misclassification is silent\ncorruption — the resulting \"causal\" estimate absorbs post-treatment\nmediation into the treatment effect, biasing the headline number in a\ndirection that is hard to recover from after the fact.\n\n## Python implementation guidance\n\n**Primary library:** `pandas` + `statsmodels` (`cov_type='cluster'`).\n\n**Reference helpers:**\n\n```python\ndef cluster_bootstrap_ate(\n    df: pd.DataFrame,\n    cluster_col: str,\n    ate_estimator_fn: Callable[[pd.DataFrame], float],\n    n_boot: int = 1000,\n    random_state: int = 42,\n) -> tuple[float, float]: ...  # returns (ci_lower, ci_upper)\n\ndef declare_covariate_temporal_status(\n    covariates: list[str],\n    treatment_wave: str,\n) -> dict[str, Literal[\"pre\", \"post\", \"contemporaneous\"]]: ...\n```\n\n**Library pitfalls:**\n\n- `statsmodels` `cov_type='cluster'` requires `groups` argument as\n  a 1-D array of cluster IDs; pseudo-school-IDs must be aligned by\n  `iloc` to the regression's exogenous matrix.\n- The pseudo-school-IDs come from\n  `cluster-id-reconstruction-from-fingerprints` — that skill's\n  diagnostics MUST pass before its IDs are used here, or the\n  clustered SE inflates without basis.\n\n## Validation criteria\n\nThe SKILL contract requires that:\n\n1. The treatment-relevant variable inventory (with examples) is\n   present.\n2. The pre\u002Fpost-treatment temporal table is present.\n3. The postsecondary-attrition rules are present.\n4. The clustered-SE handling table per estimator is present.\n5. The no-survey-weights default is stated.\n6. `rule_severity: mandatory` is set in frontmatter.\n7. `references_skills` includes `cluster-id-reconstruction-from-fingerprints`.\n\nA Writer using this skill must be able to produce a §Methods\nsubsection naming (a) the analytic-sample estimand, (b) the\nno-survey-weights choice, and (c) the school-clustering correction\nmethod actually used.\n\nAn Analyst code artifact using this skill must produce:\n\n- `data_report.causal_covariate_temporal_table`,\n- `results.causal_estimate.cluster_se_method`,\n- `results.causal_estimate.cluster_se_ci`.\n\n## Encoded-column lookup (mandatory for analysis steps)\n\nCategorical variables in HSLS:09 (e.g., X1RACE, X1PAREDU, X1SEX, X1LOCALE, X1CONTROL,\nX1STUEDEXPCT) are one-hot encoded by the DataEngineer stage. The encoded columns are\nnamed `\u003Coriginal>_\u003Clevel>` — for example, `X1RACE` becomes `X1RACE_1` ... `X1RACE_8`.\n\n**Rule (mandatory):** when an analysis step references an adjustment-set variable that\nwas originally categorical, do NOT search train_X.csv for the original name. Instead,\ndiscover the encoded columns by prefix match:\n\n```python\ndef resolve_encoded_columns(varname: str, df_columns: list[str]) -> list[str]:\n    \"\"\"Return all encoded column names for a single original categorical variable.\"\"\"\n    direct = [c for c in df_columns if c == varname]\n    if direct:\n        return direct  # variable was not encoded (continuous or already binary)\n    encoded = [c for c in df_columns if c.startswith(varname + \"_\")]\n    if not encoded:\n        raise ValueError(f\"Adjustment-set variable {varname!r} not found in df.columns\")\n    return encoded\n```\n\nThe full adjustment set passed to a regression \u002F propensity model \u002F weight estimator is the\nunion of `resolve_encoded_columns(v, df.columns)` over all v in the spec's `adjustment_set`.\n\n**Failure mode this prevents (F-COVARIATE-SET-MISMATCH):** if the adjustment set declares\n`X1RACE` and the Analyst code searches for `\"X1RACE\" in df.columns`, the lookup fails\nsilently and the propensity \u002F outcome model is fit without race covariates — invalidating\nthe no-unmeasured-confounding assumption. LSAR will catch this as a FATAL issue.\n\n## Encoding-type discipline (mandatory for DataEngineer)\n\nThe previous \"Encoded-column lookup\" section governs the **Analyst-side**\nprefix-match lookup after encoding has already happened. The encoding step\nitself — what the DataEngineer writes to `train_X.csv` — is what this\nsection governs. The two rules are complementary: the DataEngineer chooses\nwhich variables to one-hot; the Analyst looks up whichever choice was made.\n\nThe HSLS:09 variable registry (`data_registry\u002Fdatasets\u002Fhsls09_public.yaml`)\ntags each variable with a `type` field. The DataEngineer MUST respect the\nregistry's `type` when constructing the analytic CSV. The 3b.6 D1 work\nintroduced the registry but did not codify a DataEngineer-side encoding\nrule against it; that gap is what this section closes.\n\n### Continuous variables — pass through, do NOT one-hot encode\n\nVariables tagged `type=continuous` in the registry represent measurements\nor scales with natural ordering and meaningful distances. Examples from\nthe registry that have appeared in adjustment-sets in prior runs:\n\n| Variable | Registry `type` | What it is |\n|---|---|---|\n| `X1MTHEFF` | continuous | Math self-efficacy scale (Wave-1 psychometric factor score, range −2.92 to 1.62) |\n| `X1MTHID` | continuous | Math identity scale (range −1.73 to 1.76) |\n| `X1MTHINT` | continuous | Math course interest scale (range −2.46 to 2.08) |\n| `X1MTHUTI` | continuous | Math utility scale (range −3.51 to 1.31) |\n| `X1SCIID` | continuous | Science identity scale (range −1.57 to 2.15) |\n| `X1SCHOOLBEL` | continuous | School belonging scale (range −4.35 to 1.59) |\n| `X1SES` | continuous | Socioeconomic-status composite (range −1.93 to 2.88) |\n| `X1TXMTSCOR` | continuous | Standardized math IRT score (range 24.0 to 82.2) |\n\nThese variables MUST be retained as scalar columns in `train_X.csv` and\n`test_X.csv`. The DataEngineer MUST NOT call `pd.get_dummies(...)` on them,\nMUST NOT bin them into categories, and MUST NOT one-hot encode their\ndistinct values — even when the underlying factor-score machinery\nproduces a finite discrete set of unique values that *looks* categorical.\nA psychometric scale with 56 distinct factor scores is still a continuous\nscale; encoding it as 56 binary dummies discards the scale information\nand inflates the predictor matrix in proportions that overfit downstream\npropensity models.\n\n### Categorical \u002F binary variables — one-hot encode\n\nVariables tagged `type=categorical` or `type=binary` ARE one-hot encoded.\nConcrete examples from the locked-spec adjustment-set space:\n\n| Variable | Registry `type` | What it is |\n|---|---|---|\n| `X1RACE` | categorical | Race\u002Fethnicity composite (8 unordered categories) |\n| `X1PAREDU` | categorical | Parents' highest education (7 ordered codes, but registry-tagged categorical) |\n| `X1STUEDEXPCT` | categorical | Student educational expectations (11 ordered codes, registry-tagged categorical) |\n| `X1LOCALE` | categorical | School urbanicity (4 unordered categories) |\n| `X1REGION` | categorical | Census region (4 unordered categories) |\n| `X1CONTROL` | binary | Public vs Catholic\u002Fprivate |\n| `X1SEX` | binary | Student sex |\n\nUse `pd.get_dummies(df[col], prefix=col, drop_first=False)` (no reference-\ncategory dropping; downstream models handle the collinearity, and the\n3b.6 `resolve_encoded_columns` Analyst-side lookup expects all levels\npresent).\n\n### Ordinal-but-registry-tagged-categorical — use the registry tag\n\nWhen the registry tags a variable `categorical` even though it has natural\nordering (e.g., `X1PAREDU` 1–7 on an education ladder, `X1STUEDEXPCT`\n1–11), respect the registry tag. The registry authors chose `categorical`\nbecause the codebook codes carry semantically-meaningful labels (`1 =\nLess than high school`, `2 = HS diploma\u002FGED`, etc.) and the ordering\nbetween adjacent codes is not numerically comparable. One-hot encoding\npreserves the option for downstream models to fit non-linear category\neffects.\n\n### Operationalization is separate from encoding\n\nIf the research_spec specifies an operationalization for a continuous\ntreatment (e.g., `median_split_binary` for `X1MTHEFF`), apply the\noperationalization per the `causal-data-engineer-contract` skill (3b.12).\nThe operationalization produces ONE additional column\n(`X1MTHEFF_binary`); it does NOT trigger one-hot encoding of the\nunderlying continuous variable. The continuous variable may itself be\nretained as a separate column, dropped, or kept-only-as-operationalized\nper the spec's intent.\n\n### Prescriptive recipe\n\n```python\nimport warnings\nimport pandas as pd\n\ndef encode_for_causal_soo(\n    df: pd.DataFrame,\n    feature_cols: list[str],\n    variable_registry: dict[str, dict],\n) -> pd.DataFrame:\n    \"\"\"Encode adjustment-set + treatment columns respecting registry types.\n\n    Args:\n        df: source DataFrame containing all `feature_cols`.\n        feature_cols: variable names from research_spec\n            (treatment + adjustment_set, in any order).\n        variable_registry: map from variable name to its registry entry,\n            including the `type` field.\n\n    Returns:\n        DataFrame with continuous\u002Fordinal-as-continuous columns passed\n        through as scalars and categorical\u002Fbinary columns one-hot\n        encoded with explicit prefixes.\n    \"\"\"\n    out = pd.DataFrame(index=df.index)\n    for col in feature_cols:\n        entry = variable_registry.get(col, {})\n        var_type = entry.get(\"type\", \"unknown\")\n\n        if var_type == \"continuous\":\n            # Pass through. Do NOT one-hot encode, even if df[col] has\n            # a small number of unique values (factor scores often do).\n            out[col] = df[col]\n\n        elif var_type in (\"categorical\", \"binary\"):\n            # One-hot encode. drop_first=False keeps all levels so the\n            # Analyst's resolve_encoded_columns prefix-match (3b.6 rule)\n            # finds every encoded variant.\n            dummies = pd.get_dummies(df[col], prefix=col, drop_first=False)\n            out = pd.concat([out, dummies], axis=1)\n\n        else:\n            # Conservative: pass through and warn. Registry-incomplete\n            # tagging is a registry concern, not a DataEngineer concern;\n            # do not silently re-classify.\n            warnings.warn(\n                f\"Variable {col!r} has registry type {var_type!r}; \"\n                f\"passing through as scalar. Update the registry if \"\n                f\"this should be one-hot encoded.\"\n            )\n            out[col] = df[col]\n\n    return out\n```\n\nThe function takes a `variable_registry` parameter rather than reading the\nYAML directly so the rule is testable in isolation. The DataEngineer's\ncode loads the YAML once and passes the appropriate slice (typically the\nfull `variables.predictors.*` flat dict) to this function.\n\n### Failure mode this prevents (F-3b15-DE-CONTINUOUS-AS-CATEGORICAL)\n\nThe 3b.15 \u002F 3b.17 evidence chain (recoverable in some runs by cycle-1\nCritic feedback, not recoverable in others):\n\n| Run | Cycle 0 cols | Cycle 1 cols | Mis-encoded continuous variables (cycle 0) | Outcome |\n|---|---:|---:|---|---|\n| 3b.13 | 56 | 56 | `X1SCIID` (16 dummies) | Clean — fewer continuous vars in PF's adjustment_set masked the issue |\n| 3b.15 | 109 | 11 | `X1MTHID` (16), `X1MTHUTI` (~70) | Cycle-1 over-corrected; LSAR 7.0 Accept |\n| 3b.17 | 116 | 115 | `X1MTHID` (17), `X1MTHUTI` (56), `X1STUEDEXPCT` (11) | Cycle-1 did NOT recover; LSAR 6.0 Borderline; 21% extreme-tail positivity violation flagged as \"fundamental identification failure\" (`F-3b15-DE-CONTINUOUS-AS-CATEGORICAL`) |\n\nThe mechanism is the same across runs: the DataEngineer defaults to\n`pd.get_dummies(...)` on every adjustment-set variable that isn't already\nbinary, regardless of registry `type`. When a continuous variable has a\nfinite set of distinct factor-score values (psychometric scales typically\ndo — IRT estimators produce a discrete grid), the DE interprets the\nfinite-value set as categorical and one-hot encodes. The effect compounds\nwhen multiple continuous scales appear in the adjustment_set: 3b.13's\nsingle mis-encoded scale produced a manageable 56-column matrix; 3b.17's\nthree mis-encoded scales produced a 115-column matrix that overfit the\npropensity model and broke positivity.\n\nThe Analyst-side `resolve_encoded_columns` rule (3b.6) is the wrong fix\nfor this — it correctly handles whatever encoding the DE chose, but it\ncannot un-do an incorrect encoding choice. The DE-side rule above is the\nfix: registry-type dispatch produces deterministic correct behavior\nindependent of which continuous variables happen to be in the\nadjustment_set.\n\n**Cross-reference to existing D1 rules:**\n- The Analyst-side `resolve_encoded_columns` lookup (above) reads\n  whatever encoding the DataEngineer wrote and is unaffected by this\n  rule.\n- The temporal-ordering rule in `hsls09-temporal-ordering` (V2.0\n  dataset skill) is independent — temporal classification of a\n  covariate happens before encoding decisions are made.\n- The `causal-data-engineer-contract` rule (3b.12) governs which\n  columns the DE writes (treatment + adjustment_set + outcome).\n  This rule governs HOW each adjustment_set column is encoded.\n\n## Source provenance\n\nCanonical source: the v3.0 causal-methods specification (internal) §3.6\n(D1 per-skill specification). HSLS variable details cross-reference\n`data_registry\u002Fdatasets\u002Fhsls09_public.yaml` and the existing V2.0\nskills `hsls09-temporal-ordering`, `hsls09-structural-mnar-outcomes`,\n`hsls09-school-fingerprints`, and\n`cluster-id-reconstruction-from-fingerprints`.\n",{"name":5,"layer":30,"version":41,"priority":42,"resources":43,"description":7,"rule_severity":44,"trigger_keywords":45,"applicable_stages":55,"references_skills":61,"applicable_datasets":63,"applicable_task_types":65},"1.0",1,[],"mandatory",[46,47,48,49,50,51,52,53,54],"hsls","hsls09","causal","treatment","mnar","clustered","cluster-robust","pre-treatment","post-treatment",[56,57,58,59,60],"ProblemFormulator","DataEngineer","Analyst","Critic","Writer",[62],"cluster-id-reconstruction-from-fingerprints",[64],"hsls09_public",[66,67],"causal_soo","causal_itr","main","1aeaead66d2bf876109f3b101005d2eed54041d9","skills\u002Fdataset\u002Fhsls09-causal-conventions",0,"https:\u002F\u002Fgithub.com\u002Fcgpan\u002Fedm-ars-public","https:\u002F\u002Fgithub.com\u002Fcgpan","https:\u002F\u002Fcpan.ai","Ph.D. candidate in Measurement, Evaluation, and Statistics.","2026-09-28 20:39:06",{"source":8,"skillName":5,"commands":78,"prompt":88,"zipAvailable":89},[79,82,85],{"tool":80,"command":81},"npx","npx skills add cgpan\u002Fedm-ars-public --skill hsls09-causal-conventions",{"tool":83,"command":84},"bunx","bunx skills add cgpan\u002Fedm-ars-public --skill hsls09-causal-conventions",{"tool":86,"command":87},"pnpm","pnpm dlx skills add cgpan\u002Fedm-ars-public --skill hsls09-causal-conventions","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fcgpan\u002Fedm-ars-public\u002Fblob\u002Fmain\u002Fskills\u002Fdataset\u002Fhsls09-causal-conventions\u002FSKILL.md",true,[],[92,114,127,136,149,159],{"skillNo":93,"name":94,"nameZh":6,"description":95,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":96,"categories":97,"keywords":108,"publishedAt":36},"01M6EM1ZTQZVAP90DZTVBHCCFB","acm-fixture","Fixture writing skill for ACM acmart sigconf paper output.","https:\u002F\u002Fgithub.com\u002Fcgpan\u002Fedm-ars-public\u002Fblob\u002Fmain\u002Ftests\u002Ffixtures\u002Fskills\u002Fwriting\u002Facm-fixture\u002FSKILL.md",[98,103],{"categoryNo":99,"categoryName":100,"taxonomyType":101,"taxonomyTypeDesc":102},"01JBSEEDCAT000000000000018","写作与文案","SCENARIO","使用场景",{"categoryNo":104,"categoryName":105,"taxonomyType":106,"taxonomyTypeDesc":107},"01JBSEEDCAT000000000000036","内容创作者","PROFESSION","适用职业",[109,110,111,112,25,113],"acm fixture","fixture","writing","LaTeX","paper",{"skillNo":115,"name":116,"nameZh":6,"description":117,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":118,"categories":119,"keywords":120,"publishedAt":36},"01M6EM1XNFETAZFGPHQPAXKBYN","prediction-fixture","Fixture skill for the prediction task type; references shap-fixture.","https:\u002F\u002Fgithub.com\u002Fcgpan\u002Fedm-ars-public\u002Fblob\u002Fmain\u002Ftests\u002Ffixtures\u002Fskills\u002Ftask-type\u002Fprediction-fixture\u002FSKILL.md",[],[110,121,122,123,124,125,126,28],"prediction fixture","task","type","references","fixture skill","shap",{"skillNo":128,"name":129,"nameZh":6,"description":130,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":131,"categories":132,"keywords":133,"publishedAt":36},"01M6EM1V4MQXYGQ7K2ZDJRAPSM","causal-fixture","Fixture skill for the causal_inference task type.","https:\u002F\u002Fgithub.com\u002Fcgpan\u002Fedm-ars-public\u002Fblob\u002Fmain\u002Ftests\u002Ffixtures\u002Fskills\u002Ftask-type\u002Fcausal-fixture\u002FSKILL.md",[],[110,134,123,122,135,125],"causal fixture","inference",{"skillNo":137,"name":138,"nameZh":6,"description":139,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":140,"categories":141,"keywords":142,"publishedAt":36},"01M6EM1SCWFZZ2ACSAP65GNWK9","shap-fixture","Fixture methodology for SHAP feature importance analysis.","https:\u002F\u002Fgithub.com\u002Fcgpan\u002Fedm-ars-public\u002Fblob\u002Fmain\u002Ftests\u002Ffixtures\u002Fskills\u002Fmethodology\u002Fshap-fixture\u002FSKILL.md",[],[143,110,126,144,145,146,147,148],"shap fixture","feature importance","methodology","analysis","explainer","feature",{"skillNo":150,"name":151,"nameZh":6,"description":152,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":153,"categories":154,"keywords":155,"publishedAt":36},"01M6EM1PZC1MK1G0D1YFVYAQ9W","cluster-recon-fixture","Fixture methodology for reconstructing cluster IDs from fingerprint variables.","https:\u002F\u002Fgithub.com\u002Fcgpan\u002Fedm-ars-public\u002Fblob\u002Fmain\u002Ftests\u002Ffixtures\u002Fskills\u002Fmethodology\u002Fcluster-recon-fixture\u002FSKILL.md",[],[156,157,32,110,158,145],"recon","cluster recon fixture","ids",{"skillNo":160,"name":161,"nameZh":6,"description":162,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":163,"categories":164,"keywords":165,"publishedAt":36},"01M6EM1MZVKMSWEXZ7TJ45DS04","hsls09-fixture","Fixture skill for HSLS:09 with a bundled YAML resource and one cross-reference.","https:\u002F\u002Fgithub.com\u002Fcgpan\u002Fedm-ars-public\u002Fblob\u002Fmain\u002Ftests\u002Ffixtures\u002Fskills\u002Fdataset\u002Fhsls09-fixture\u002FSKILL.md",[],[110,166,167,23,168,125,30,156,32,169],"hsls09 fixture","yaml","reference","registry"]