hsls09-causal-conventions
HSLS-specific causal conventions — treatment-relevant variable inventory, pre/post-treatment temporal classification, MNAR diagnostics for postsecondary outcomes, and clustered-SE handling for causal estimators.
- License 许可
- MIT
- 最近更新
- 2026-09-28
- Stars
- ★ 5
- Forks
- 0
01 前言 Front Matter
- name
- hsls09-causal-conventions
- layer
- dataset
- version
- 1.0
- priority
- 1
- resources
- []
- description
- HSLS-specific causal conventions — treatment-relevant variable inventory, pre/post-treatment temporal classification, MNAR diagnostics for postsecondary outcomes, and clustered-SE handling for causal estimators.
- rule_severity
- mandatory
- trigger_keywords
- [ "hsls", "hsls09", "causal", "treatment", "mnar", "clustered", "cluster-robust", "pre-treatment", "post-treatment" ]
- applicable_stages
- [ "ProblemFormulator", "DataEngineer", "Analyst", "Critic", "Writer" ]
- references_skills
- [ "cluster-id-reconstruction-from-fingerprints" ]
- applicable_datasets
- [ "hsls09_public" ]
- applicable_task_types
- [ "causal_soo", "causal_itr" ]
03 SKILL.md 正文
HSLS:09 Causal Conventions
HSLS-specific extensions to the cross-dataset causal methodology
skills (G1–G5). Covers the four areas where causal analysis on HSLS
needs concrete, dataset-anchored rules: treatment-variable choice,
pre/post temporal classification, attrition-driven MNAR for
postsecondary outcomes, and clustered-SE handling at the school
level. Pre/post misclassification is silent corruption (introduces
post-treatment bias), so this skill is rule_severity: mandatory.
Treatment-relevant variable inventory
Continuous attitudinal scales (psychometric)
These are psychometric scales that require thresholding or binning into a defined contrast. No "per-unit" causal interpretation is admissible — the unit is a standardized factor score, not a behavior.
| Variable | Label |
|---|---|
X1MTHID |
Math identity scale |
X1MTHEFF |
Math self-efficacy scale |
X1SCHOOLBEL |
School belonging scale |
X1SCIID |
Science identity scale |
| ... | other X1* attitudinal composites in the registry |
A causal study that uses one of these as a treatment must declare
its treatment_contrast in research_spec.treatment_contrast
(median split, >= threshold, +1 SD, etc., per G2's contrast
schema).
Course-taking / attainment indicators
These are observable behavioral indicators usable as treatments without further binning:
| Variable | Label |
|---|---|
X3TCREDMAT |
Math credits earned |
X4HSCOMPSTAT |
High school credential status |
X4EVRATNDCLG |
Ever attended college |
Variables NEVER suitable as treatments
| Pattern | Reason |
|---|---|
W* (e.g., W4W1W2W3STU) |
Sampling weights; not causally manipulable. |
*QSTAT (e.g., X1SQSTAT) |
Survey-status indicators; reflect missingness, not treatment. |
X1SEX, X1RACE |
Demographic protected attributes; use only as moderators in CATE / subgroup analyses, never as treatments under selection-on-observables. |
Pre/post-treatment temporal classification
| Treatment wave | Pre-treatment covariates allowed |
|---|---|
| Any X1 treatment | X1 baseline variables AND demographics. |
| Any X2 treatment | X1 + X2_baseline (excluding X2 outcomes). |
| Any X3 treatment | X1 + X2 + X3_baseline. |
| Any X4 treatment | X1 + X2 + X3 + X4_baseline. |
Anything from a wave after the treatment wave is post-treatment
and forbidden in the adjustment set per G1's back-door criterion (no
descendants of T).
Mandatory rule: every covariate in research_spec.adjustment_set
must have its temporal status declared in
research_spec.covariate_temporal_table. The wave-prefix mapping is
defined in hsls09-temporal-ordering (existing V2.0 dataset skill);
this skill extends it for the causal-adjustment-set use case.
Selection-bias-from-attrition rules for postsecondary outcomes
| Outcome wave | Approximate missingness | MAR/MNAR risk |
|---|---|---|
X4* (2016 update panel) |
~26% | MAR-leaning, with treatment-correlated patterns |
X5* (2017 postsecondary records) |
50%+ | MAR-to-MNAR, structurally |
For causal targets on postsecondary outcomes:
- The analytic sample is structurally restricted to respondents — attrition is not random across treatment groups.
- Report both an ITT-analogue (X4SQSTAT-respondents only) and the as-treated equivalent. Divergence between the two is informative about selection bias.
- Recommend running parallel IPW-for-missingness analyses (see M4
causal-aipw-tmle's missingness-augmented branch) to bound the missingness-induced bias.
Clustered-SE handling for causal estimators
This section extends cluster-id-reconstruction-from-fingerprints
(existing V2.0 methodology skill) for the causal context — the
pseudo-school-IDs reconstructed there flow into the causal-estimator
SE machinery here.
| Estimator | Clustered-SE recipe |
|---|---|
| IPW (M3) | statsmodels weighted regression with cov_type='cluster', cluster_groups=pseudo_school_id. |
| PSM (M2) | Cluster-bootstrap on matched pairs aggregated at school level (matched pair as resampling unit, school as cluster). |
| AIPW / TMLE (M4) | Influence-function variance with clustered correction (sum within clusters, then variance across clusters). |
| 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). |
Survey-weights handling
V3.0 default: do NOT apply HSLS survey weights.
The estimand is the analytic-sample marginal effect, not the population marginal effect. The Writer must state this explicitly in both §Methods and §Limitations.
This skill composes hsls09-survey-weights-limitations-paragraph
(existing V2.0 writing skill) so the Writer's limitation prose stays
consistent with the prediction-task convention.
Mandatory tagging
rule_severity: mandatory. Pre/post misclassification is silent
corruption — the resulting "causal" estimate absorbs post-treatment
mediation into the treatment effect, biasing the headline number in a
direction that is hard to recover from after the fact.
Python implementation guidance
Primary library: pandas + statsmodels (cov_type='cluster').
Reference helpers:
def cluster_bootstrap_ate(
df: pd.DataFrame,
cluster_col: str,
ate_estimator_fn: Callable[[pd.DataFrame], float],
n_boot: int = 1000,
random_state: int = 42,
) -> tuple[float, float]: ... # returns (ci_lower, ci_upper)
def declare_covariate_temporal_status(
covariates: list[str],
treatment_wave: str,
) -> dict[str, Literal["pre", "post", "contemporaneous"]]: ...
Library pitfalls:
statsmodelscov_type='cluster'requiresgroupsargument as a 1-D array of cluster IDs; pseudo-school-IDs must be aligned byilocto the regression's exogenous matrix.- The pseudo-school-IDs come from
cluster-id-reconstruction-from-fingerprints— that skill's diagnostics MUST pass before its IDs are used here, or the clustered SE inflates without basis.
Validation criteria
The SKILL contract requires that:
- The treatment-relevant variable inventory (with examples) is present.
- The pre/post-treatment temporal table is present.
- The postsecondary-attrition rules are present.
- The clustered-SE handling table per estimator is present.
- The no-survey-weights default is stated.
rule_severity: mandatoryis set in frontmatter.references_skillsincludescluster-id-reconstruction-from-fingerprints.
A Writer using this skill must be able to produce a §Methods subsection naming (a) the analytic-sample estimand, (b) the no-survey-weights choice, and (c) the school-clustering correction method actually used.
An Analyst code artifact using this skill must produce:
data_report.causal_covariate_temporal_table,results.causal_estimate.cluster_se_method,results.causal_estimate.cluster_se_ci.
Encoded-column lookup (mandatory for analysis steps)
Categorical variables in HSLS:09 (e.g., X1RACE, X1PAREDU, X1SEX, X1LOCALE, X1CONTROL,
X1STUEDEXPCT) are one-hot encoded by the DataEngineer stage. The encoded columns are
named <original>_<level> — for example, X1RACE becomes X1RACE_1 ... X1RACE_8.
Rule (mandatory): when an analysis step references an adjustment-set variable that was originally categorical, do NOT search train_X.csv for the original name. Instead, discover the encoded columns by prefix match:
def resolve_encoded_columns(varname: str, df_columns: list[str]) -> list[str]:
"""Return all encoded column names for a single original categorical variable."""
direct = [c for c in df_columns if c == varname]
if direct:
return direct # variable was not encoded (continuous or already binary)
encoded = [c for c in df_columns if c.startswith(varname + "_")]
if not encoded:
raise ValueError(f"Adjustment-set variable {varname!r} not found in df.columns")
return encoded
The full adjustment set passed to a regression / propensity model / weight estimator is the
union of resolve_encoded_columns(v, df.columns) over all v in the spec's adjustment_set.
Failure mode this prevents (F-COVARIATE-SET-MISMATCH): if the adjustment set declares
X1RACE and the Analyst code searches for "X1RACE" in df.columns, the lookup fails
silently and the propensity / outcome model is fit without race covariates — invalidating
the no-unmeasured-confounding assumption. LSAR will catch this as a FATAL issue.
Encoding-type discipline (mandatory for DataEngineer)
The previous "Encoded-column lookup" section governs the Analyst-side
prefix-match lookup after encoding has already happened. The encoding step
itself — what the DataEngineer writes to train_X.csv — is what this
section governs. The two rules are complementary: the DataEngineer chooses
which variables to one-hot; the Analyst looks up whichever choice was made.
The HSLS:09 variable registry (data_registry/datasets/hsls09_public.yaml)
tags each variable with a type field. The DataEngineer MUST respect the
registry's type when constructing the analytic CSV. The 3b.6 D1 work
introduced the registry but did not codify a DataEngineer-side encoding
rule against it; that gap is what this section closes.
Continuous variables — pass through, do NOT one-hot encode
Variables tagged type=continuous in the registry represent measurements
or scales with natural ordering and meaningful distances. Examples from
the registry that have appeared in adjustment-sets in prior runs:
| Variable | Registry type |
What it is |
|---|---|---|
X1MTHEFF |
continuous | Math self-efficacy scale (Wave-1 psychometric factor score, range −2.92 to 1.62) |
X1MTHID |
continuous | Math identity scale (range −1.73 to 1.76) |
X1MTHINT |
continuous | Math course interest scale (range −2.46 to 2.08) |
X1MTHUTI |
continuous | Math utility scale (range −3.51 to 1.31) |
X1SCIID |
continuous | Science identity scale (range −1.57 to 2.15) |
X1SCHOOLBEL |
continuous | School belonging scale (range −4.35 to 1.59) |
X1SES |
continuous | Socioeconomic-status composite (range −1.93 to 2.88) |
X1TXMTSCOR |
continuous | Standardized math IRT score (range 24.0 to 82.2) |
These variables MUST be retained as scalar columns in train_X.csv and
test_X.csv. The DataEngineer MUST NOT call pd.get_dummies(...) on them,
MUST NOT bin them into categories, and MUST NOT one-hot encode their
distinct values — even when the underlying factor-score machinery
produces a finite discrete set of unique values that looks categorical.
A psychometric scale with 56 distinct factor scores is still a continuous
scale; encoding it as 56 binary dummies discards the scale information
and inflates the predictor matrix in proportions that overfit downstream
propensity models.
Categorical / binary variables — one-hot encode
Variables tagged type=categorical or type=binary ARE one-hot encoded.
Concrete examples from the locked-spec adjustment-set space:
| Variable | Registry type |
What it is |
|---|---|---|
X1RACE |
categorical | Race/ethnicity composite (8 unordered categories) |
X1PAREDU |
categorical | Parents' highest education (7 ordered codes, but registry-tagged categorical) |
X1STUEDEXPCT |
categorical | Student educational expectations (11 ordered codes, registry-tagged categorical) |
X1LOCALE |
categorical | School urbanicity (4 unordered categories) |
X1REGION |
categorical | Census region (4 unordered categories) |
X1CONTROL |
binary | Public vs Catholic/private |
X1SEX |
binary | Student sex |
Use pd.get_dummies(df[col], prefix=col, drop_first=False) (no reference-
category dropping; downstream models handle the collinearity, and the
3b.6 resolve_encoded_columns Analyst-side lookup expects all levels
present).
Ordinal-but-registry-tagged-categorical — use the registry tag
When the registry tags a variable categorical even though it has natural
ordering (e.g., X1PAREDU 1–7 on an education ladder, X1STUEDEXPCT
1–11), respect the registry tag. The registry authors chose categorical
because the codebook codes carry semantically-meaningful labels (1 = Less than high school, 2 = HS diploma/GED, etc.) and the ordering
between adjacent codes is not numerically comparable. One-hot encoding
preserves the option for downstream models to fit non-linear category
effects.
Operationalization is separate from encoding
If the research_spec specifies an operationalization for a continuous
treatment (e.g., median_split_binary for X1MTHEFF), apply the
operationalization per the causal-data-engineer-contract skill (3b.12).
The operationalization produces ONE additional column
(X1MTHEFF_binary); it does NOT trigger one-hot encoding of the
underlying continuous variable. The continuous variable may itself be
retained as a separate column, dropped, or kept-only-as-operationalized
per the spec's intent.
Prescriptive recipe
import warnings
import pandas as pd
def encode_for_causal_soo(
df: pd.DataFrame,
feature_cols: list[str],
variable_registry: dict[str, dict],
) -> pd.DataFrame:
"""Encode adjustment-set + treatment columns respecting registry types.
Args:
df: source DataFrame containing all `feature_cols`.
feature_cols: variable names from research_spec
(treatment + adjustment_set, in any order).
variable_registry: map from variable name to its registry entry,
including the `type` field.
Returns:
DataFrame with continuous/ordinal-as-continuous columns passed
through as scalars and categorical/binary columns one-hot
encoded with explicit prefixes.
"""
out = pd.DataFrame(index=df.index)
for col in feature_cols:
entry = variable_registry.get(col, {})
var_type = entry.get("type", "unknown")
if var_type == "continuous":
# Pass through. Do NOT one-hot encode, even if df[col] has
# a small number of unique values (factor scores often do).
out[col] = df[col]
elif var_type in ("categorical", "binary"):
# One-hot encode. drop_first=False keeps all levels so the
# Analyst's resolve_encoded_columns prefix-match (3b.6 rule)
# finds every encoded variant.
dummies = pd.get_dummies(df[col], prefix=col, drop_first=False)
out = pd.concat([out, dummies], axis=1)
else:
# Conservative: pass through and warn. Registry-incomplete
# tagging is a registry concern, not a DataEngineer concern;
# do not silently re-classify.
warnings.warn(
f"Variable {col!r} has registry type {var_type!r}; "
f"passing through as scalar. Update the registry if "
f"this should be one-hot encoded."
)
out[col] = df[col]
return out
The function takes a variable_registry parameter rather than reading the
YAML directly so the rule is testable in isolation. The DataEngineer's
code loads the YAML once and passes the appropriate slice (typically the
full variables.predictors.* flat dict) to this function.
Failure mode this prevents (F-3b15-DE-CONTINUOUS-AS-CATEGORICAL)
The 3b.15 / 3b.17 evidence chain (recoverable in some runs by cycle-1 Critic feedback, not recoverable in others):
| Run | Cycle 0 cols | Cycle 1 cols | Mis-encoded continuous variables (cycle 0) | Outcome |
|---|---|---|---|---|
| 3b.13 | 56 | 56 | X1SCIID (16 dummies) |
Clean — fewer continuous vars in PF's adjustment_set masked the issue |
| 3b.15 | 109 | 11 | X1MTHID (16), X1MTHUTI (~70) |
Cycle-1 over-corrected; LSAR 7.0 Accept |
| 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) |
The mechanism is the same across runs: the DataEngineer defaults to
pd.get_dummies(...) on every adjustment-set variable that isn't already
binary, regardless of registry type. When a continuous variable has a
finite set of distinct factor-score values (psychometric scales typically
do — IRT estimators produce a discrete grid), the DE interprets the
finite-value set as categorical and one-hot encodes. The effect compounds
when multiple continuous scales appear in the adjustment_set: 3b.13's
single mis-encoded scale produced a manageable 56-column matrix; 3b.17's
three mis-encoded scales produced a 115-column matrix that overfit the
propensity model and broke positivity.
The Analyst-side resolve_encoded_columns rule (3b.6) is the wrong fix
for this — it correctly handles whatever encoding the DE chose, but it
cannot un-do an incorrect encoding choice. The DE-side rule above is the
fix: registry-type dispatch produces deterministic correct behavior
independent of which continuous variables happen to be in the
adjustment_set.
Cross-reference to existing D1 rules:
- The Analyst-side
resolve_encoded_columnslookup (above) reads whatever encoding the DataEngineer wrote and is unaffected by this rule. - The temporal-ordering rule in
hsls09-temporal-ordering(V2.0 dataset skill) is independent — temporal classification of a covariate happens before encoding decisions are made. - The
causal-data-engineer-contractrule (3b.12) governs which columns the DE writes (treatment + adjustment_set + outcome). This rule governs HOW each adjustment_set column is encoded.
Source provenance
Canonical source: the v3.0 causal-methods specification (internal) §3.6
(D1 per-skill specification). HSLS variable details cross-reference
data_registry/datasets/hsls09_public.yaml and the existing V2.0
skills hsls09-temporal-ordering, hsls09-structural-mnar-outcomes,
hsls09-school-fingerprints, and
cluster-id-reconstruction-from-fingerprints.
04 同仓库更多 Skills
acm-fixture
Fixture writing skill for ACM acmart sigconf paper output.
prediction-fixture
Fixture skill for the prediction task type; references shap-fixture.
causal-fixture
Fixture skill for the causal_inference task type.
shap-fixture
Fixture methodology for SHAP feature importance analysis.
cluster-recon-fixture
Fixture methodology for reconstructing cluster IDs from fingerprint variables.
hsls09-fixture
Fixture skill for HSLS:09 with a bundled YAML resource and one cross-reference.