agentleFS
topprismdatatopprismdata/cultivating-ml-agent/skills/examples/domain-knowledge-constraints-trap/SKILL.md

domain-knowledge-constraints-trap

Skill5 starsChanged 35 days agoMITView on GitHub

Avoid applying domain knowledge constraints that hurt ML model performance. Use when: (1) Considering medical/physical/logical constraints on training data, (2) Adversarial validation AUC changes significantly after applying constraints (>0.10 shift), (3) CV score drops after adding "reasonable" domain rules, (4) Train/test distributions become misaligned after filtering.

The file

We split this skill into the rules it gives an agent

4 rules. About 0 are common, 4 are distinctive, and 0 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈4

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 0

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    If you MUST apply constraints:

    line 93
  2. Distinctive:

    If you filter training data but not test data

    line 265
  3. Distinctive:

    Don't change adversarial AUC significantly

    line 285
  4. Distinctive:

    Instead of filtering, consider:

    line 295

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Domain Knowledge Constraints Trap

Problem

Applying domain knowledge "constraints" or "rules" to filter training data can seem like a good idea for improving model quality, but often destroys model performance by changing the data distribution and removing valuable information.

Real Case: Adding 4 medical constraints to heart disease prediction:

  • Expected: Improve model with medical domain knowledge
  • Actual: CV AUC dropped from 0.970 to 0.943 (-2.8%)
  • Root cause: Adversarial AUC changed from 0.501 → 0.661 (distribution shift)

Context / Trigger Conditions

Use this skill when:

  • Working on ML projects with domain experts providing "rules"
  • Considering filtering data based on physical/medical/logical constraints
  • Adversarial validation shows train/test distribution change after filtering
  • CV score decreases after adding "reasonable" constraints

Symptoms:

  • Domain experts say "this data point is impossible"
  • Filtering "anomalies" or "outliers" based on domain rules
  • CV score drops after adding constraints
  • Train/test distributions become different (adversarial AUC ≠ 0.5)

Common Trap Examples:

  • Medical: "Heart rate can't exceed 220 - age" → Removes valid extreme cases
  • Physical: "Temperature can't be negative" → Removes sensor errors AND valid extremes
  • Business: "Customer can't spend >$10K/month" → Removes high-value outliers
  • Temporal: "Events can't happen in the future" → Removes data entry errors AND valid edge cases

Solution

Step 1: Quantify Distribution Impact

Before applying constraints, check adversarial validation:

from sklearn.model_selection import StratifiedKFold
import lightgbm as lgb
from sklearn.metrics import roc_auc_score

def check_adversarial_auc(train_df, test_df, features):
    """Check if train/test distributions are similar"""
    adv_train = train_df[features].copy()
    adv_train['is_test'] = 0

    adv_test = test_df[features].copy()
    adv_test['is_test'] = 1

    adv_combined = pd.concat([adv_train, adv_test], axis=0)

    # Train classifier to distinguish train vs test
    skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
    oof_pred = np.zeros(len(adv_combined))

    for tr, val in skf.split(adv_combined, adv_combined['is_test']):
        train_data = lgb.Dataset(adv_combined.iloc[tr][features],
                                  label=adv_combined.iloc[tr]['is_test'])
        val_data = lgb.Dataset(adv_combined.iloc[val][features],
                                label=adv_combined.iloc[val]['is_test'])

        model = lgb.train({'objective': 'binary', 'verbosity': -1},
                          train_data, num_boost_round=100,
                          valid_sets=[val_data],
                          callbacks=[lgb.early_stopping(stopping_rounds=10)])

        oof_pred[val] = model.predict(adv_combined.iloc[val][features])

    auc = roc_auc_score(adv_combined['is_test'], oof_pred)
    return auc

# Before constraints
auc_before = check_adversarial_auc(train, test, features)
print(f"Adversarial AUC (before): {auc_before:.5f}")
# Output: 0.50111 ≈ 0.5 (distributions are similar) ✅

Step 2: Apply Constraints Cautiously

If you MUST apply constraints:

def apply_constraints_with_validation(df, constraints):
    """Apply constraints and measure impact"""
    df_filtered = df.copy()
    removed = []

    for constraint_name, constraint_func in constraints:
        before = len(df_filtered)
        df_filtered = constraint_func(df_filtered)
        after = len(df_filtered)

        removed_count = before - after
        removed.append((constraint_name, removed_count))
        print(f"{constraint_name}: removed {removed_count} samples")

    total_removed = len(df) - len(df_filtered)
    print(f"Total removed: {total_removed} ({100*total_removed/len(df):.1f}%)")

    # Check adversarial AUC after filtering
    auc_after = check_adversarial_auc(df_filtered, test, features)
    print(f"Adversarial AUC (after): {auc_after:.5f}")

    # Warn if distribution changed significantly
    if abs(auc_after - 0.5) > 0.10:
        print("⚠️ WARNING: Distribution changed significantly!")
        print("   Constraints may be removing test-like samples.")
        print("   Consider NOT applying these constraints.")

    return df_filtered, removed

Step 3: Validate Impact on Model Performance

Always train with and without constraints to compare:

# Train WITHOUT constraints (baseline)
model_baseline = train_model(train, features)
cv_baseline = cross_val_score(model_baseline, X, y, cv=5, scoring='roc_auc')
print(f"CV (no constraints): {cv_baseline.mean():.5f}")

# Apply constraints
train_constrained, removal_stats = apply_constraints_with_validation(train, constraints)

# Train WITH constraints
model_constrained = train_model(train_constrained, features)
cv_constrained = cross_val_score(model_constrained,
                                  X_constrained, y_constrained,
                                  cv=5, scoring='roc_auc')
print(f"CV (with constraints): {cv_constrained.mean():.5f}")

# Compare
if cv_constrained.mean() < cv_baseline.mean():
    print("❌ Constraints HURT performance!")
    print(f"   Drop: {cv_baseline.mean() - cv_constrained.mean():.5f}")
else:
    print("✅ Constraints HELP performance")

Step 4: Decision Framework

Should you apply domain constraints?

ConditionAction
Adversarial AUC change < 0.05✅ Safe to apply
Adversarial AUC change 0.05-0.10⚠️ Proceed with caution
Adversarial AUC change > 0.10❌ DO NOT apply
CV score drops❌ DO NOT apply
Removes >20% of data❌ DO NOT apply
Removes >5% of data⚠️ Validate carefully

Real Example:

Adversarial AUC before constraints: 0.501 ✅
Adversarial AUC after constraints:  0.661 ❌ (Distribution changed!)
CV before constraints:              0.970
CV after constraints:               0.943 (-2.8%)

Conclusion: DO NOT apply these medical constraints

Verification

Good Signs (constraints are safe):

  • Adversarial AUC stays close to 0.5 (±0.05)
  • CV score improves or stays same
  • Removed samples are true errors (verified by domain expert)
  • Train/test distributions remain similar

Bad Signs (constraints are harmful):

  • Adversarial AUC moves away from 0.5 (>0.10)
  • CV score drops significantly
  • Many "edge case" samples are removed
  • Train/test distributions become different

Example

Medical Constraints for Heart Disease:

def apply_medical_constraints(df):
    """Apply 4 medical domain constraints"""
    df_filtered = df.copy()

    # Constraint 1: Max HR ≤ 220 - Age
    valid_hr = df_filtered['Max HR'] <= (220 - df_filtered['Age'])
    removed_1 = (~valid_hr).sum()
    df_filtered = df_filtered[valid_hr]
    print(f"Heart rate constraint: removed {removed_1} samples")

    # Constraint 2: Young low-risk patients don't have high vessel count
    young_high_risk = (
        (df_filtered['Age'] < 40) &
        (df_filtered['Vessels'] >= 3) &
        (df_filtered['Cholesterol'] < 160)
    )
    removed_2 = young_high_risk.sum()
    df_filtered = df_filtered[~young_high_risk]
    print(f"Young high-risk constraint: removed {removed_2} samples")

    # Constraint 3: ST depression consistency
    st_inconsistent = (
        (df_filtered['ST_depression'] > 2) &
        (df_filtered['ST_slope'] == 1)
    )
    removed_3 = st_inconsistent.sum()
    df_filtered = df_filtered[~st_inconsistent]
    print(f"ST consistency constraint: removed {removed_3} samples")

    # Constraint 4: Thallium-Vessels consistency
    thal_vessels_inconsistent = (
        (df_filtered['Thallium'] == 7) &
        (df_filtered['Vessels'] > 0)
    )
    removed_4 = thal_vessels_inconsistent.sum()
    df_filtered = df_filtered[~thal_vessels_inconsistent]
    print(f"Thallium-Vessels constraint: removed {removed_4} samples")

    total_removed = len(df) - len(df_filtered)
    print(f"Total: {total_removed} samples removed ({100*total_removed/len(df):.1f}%)")

    return df_filtered

# Apply constraints
train_constrained = apply_medical_constraints(train)

# Check impact on distribution
auc_before = 0.501  # Train/test are similar
auc_after = check_adversarial_auc(train_constrained, test, features)
# Result: 0.661 ❌ Distribution changed!

# Check impact on model performance
cv_before = 0.96998
cv_after = 0.94258  # Dropped by 0.027!

# Conclusion: Medical constraints are HARMFUL
# Decision: DO NOT use them

Notes

Why Domain Constraints Can Fail

1. Edge Cases Are Real

  • "Impossible" values may be valid edge cases
  • Removing them hurts model's ability to handle extremes
  • Example: High heart rate in athlete with heart condition

2. Train/Test Mismatch

  • If you filter training data but not test data
  • Model trains on "clean" data but tested on "real" data
  • Performance drops due to distribution shift

3. Information Loss

  • Even "impossible" values contain signal
  • They help model learn decision boundaries
  • Removing them reduces model capacity

4. Over-Filtering

  • Domain experts are often too conservative
  • "That can't be right" → data entry error
  • But it's actually a rare but valid case

When Constraints ARE Appropriate

Safe to use:

  • Clear data entry errors (e.g., negative age)
  • Impossible physical values (e.g., heart rate = 1000)
  • Verified with domain experts AND validated empirically
  • Don't change adversarial AUC significantly

Require validation:

  • "Unusual but possible" values
  • Removing >5% of data
  • Change train/test distribution
  • Drop CV performance

Alternatives to Hard Constraints

Instead of filtering, consider:

1. Weighting Examples

# Down-weight outliers instead of removing
sample_weights = np.ones(len(train))
sample_weights[outliers] = 0.5  # Down-weight, don't remove
model.fit(X, y, sample_weight=sample_weights)

2. Robust Models

# Use models that handle outliers well
from sklearn.ensemble import HistGradientBoostingClassifier
model = HistGradientBoostingClassifier()  # More robust to outliers

3. Feature Engineering

# Add indicator for "edge case" instead of filtering
train['is_unusual'] = (
    (train['Max HR'] > (220 - train['Age'])) |
    (train['Cholesterol'] > 400)
).astype(int)
# Model learns to handle unusual cases

4. Post-Processing Rules

# Apply domain rules AFTER prediction, not before
pred = model.predict(X)
# If prediction says "high risk" but HR is normal, flag for review
high_risk_unusual_hr = pred & (X['Max HR'] < 100)

References

Verified On:

  • Kaggle Playground Series S6E2 (Heart Disease)
  • Medical constraints reduced CV AUC from 0.970 to 0.943
  • Adversarial AUC shift from 0.501 to 0.661 confirmed distribution change

Related Skills:

  • adversarial-validation-implementation: For checking train/test distribution alignment
  • kaggle-auc-binary-submission-bug: For proper AUC submission format

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.