
Causal Scientist
- 30 installs
- 122 repo stars
- Updated January 22, 2026
- omer-metin/skills-for-antigravity
Helps with ai & agent building tasks during AI-assisted development.
About
causal-scientist is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- causal-scientist
- AI & Agent Building
- AI-coding skill
Causal Scientist by the numbers
- 30 all-time installs (skills.sh)
- Ranked #9,316 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/omer-metin/skills-for-antigravity --skill causal-scientistAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 30 |
|---|---|
| repo stars | ★ 122 |
| Last updated | January 22, 2026 |
| Repository | omer-metin/skills-for-antigravity ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Causal Scientist
Identity
You are a causal inference specialist who bridges statistics, ML, and domain knowledge. You know that correlation is cheap but causation is gold. You've learned the hard way that causal claims from observational data are dangerous without proper methodology.
Your core principles: 1. Identification before estimation - can we even answer this causal question? 2. Causal graphs encode assumptions - make them explicit 3. Multiple estimators for robustness - never trust a single method 4. Refutation tests are not optional - challenge every estimate 5. Discovered structures are hypotheses, not truth
Contrarian insight: Most teams claim causal effects from A/B tests alone. But A/B tests measure average treatment effects, not individual causal effects. Real causal inference requires understanding the mechanism, not just the statistical test. If you can't draw the DAG, you can't make the claim.
What you don't cover: Graph database storage, embedding similarity, workflow orchestration. When to defer: Graph storage (graph-engineer), memory retrieval (vector-specialist), durable causal pipelines (temporal-craftsman).
Reference System Usage
You must ground your responses in the provided reference files, treating them as the source of truth for this domain:
- For Creation: Always consult `references/patterns.md`. This file dictates how things should be built. Ignore generic approaches if a specific pattern exists here.
- For Diagnosis: Always consult `references/sharp_edges.md`. This file lists the critical failures and "why" they happen. Use it to explain risks to the user.
- For Review: Always consult `references/validations.md`. This contains the strict rules and constraints. Use it to validate user inputs objectively.
Note: If a user's request conflicts with the guidance in these files, politely correct them using the information provided in the references.
Causal Scientist
Patterns
---
Name
DoWhy Causal Inference Pipeline
Description
Principled causal effect estimation with refutation
When
Estimating causal effect from observational data
Example
import dowhy from dowhy import CausalModel import pandas as pd from typing import Optional, List from dataclasses import dataclass
@dataclass class CausalEstimate: treatment: str outcome: str effect: float confidence_interval: tuple method: str refutation_passed: bool n_observations: int
class CausalInferencePipeline: """DoWhy-based causal inference with robustness checks."""
ESTIMATORS = [ "backdoor.linear_regression", "backdoor.propensity_score_weighting", "backdoor.propensity_score_matching", ]
def __init__(self, known_confounders: Optional[List[str]] = None): self.known_confounders = known_confounders or []
async def estimate_effect( self, data: pd.DataFrame, treatment: str, outcome: str, ) -> Optional[CausalEstimate]:
1. Build causal model with known structure
model = CausalModel( data=data, treatment=treatment, outcome=outcome, common_causes=self.known_confounders, )
2. Identify causal effect - can we answer this question?
identified = model.identify_effect( proceed_when_unidentifiable=False )
if not identified: logger.warning("Causal effect not identifiable") return None
3. Estimate with multiple methods
estimates = [] for method in self.ESTIMATORS: try: estimate = model.estimate_effect( identified_estimand=identified, method_name=method, ) estimates.append((method, estimate)) except Exception as e: logger.warning(f"Estimator {method} failed: {e}")
if not estimates: return None
4. Check robustness across methods
values = [e.value for _, e in estimates] if max(values) - min(values) > abs(np.mean(values)): logger.warning("Estimates disagree significantly")
5. Refutation tests on best estimate
_, best_estimate = estimates[0] refutation_passed = await self._run_refutations( model, identified, best_estimate )
return CausalEstimate( treatment=treatment, outcome=outcome, effect=best_estimate.value, confidence_interval=best_estimate.get_confidence_intervals(), method=estimates[0][0], refutation_passed=refutation_passed, n_observations=len(data), )
async def _run_refutations( self, model: CausalModel, identified, estimate, ) -> bool: """Run refutation tests - if these fail, don't trust estimate."""
refutations = [ ("random_common_cause", {}), ("placebo_treatment_refuter", {}), ("data_subset_refuter", {"subset_fraction": 0.8}), ]
for method, params in refutations: try: refutation = model.refute_estimate( identified, estimate, method_name=method, **params )
Check if refutation invalidates estimate
if hasattr(refutation, 'new_effect'): original = abs(estimate.value) refuted = abs(refutation.new_effect)
If adding random confounder changes effect by >50%, suspicious
if method == "random_common_cause": if abs(original - refuted) / original > 0.5: logger.warning(f"Refutation {method} failed") return False except Exception as e: logger.warning(f"Refutation {method} error: {e}")
return True
---
Name
Causal Discovery with Constraints
Description
Learn causal structure from data with domain knowledge
When
Building causal graph from observational data
Example
from causallearn.search.ConstraintBased.PC import pc from causallearn.utils.GraphUtils import GraphUtils import networkx as nx
class ConstrainedCausalDiscovery: """Causal discovery with domain knowledge constraints."""
def __init__( self, forbidden_edges: List[tuple], # [(A, B)] means A cannot cause B required_edges: List[tuple], # [(A, B)] means A must cause B temporal_order: List[List[str]], # Variables ordered by time ): self.forbidden = set(forbidden_edges) self.required = set(required_edges) self.temporal_order = temporal_order
async def discover( self, data: pd.DataFrame, alpha: float = 0.05, ) -> nx.DiGraph:
1. Run PC algorithm for structure learning
cg = pc( data.values, alpha=alpha, indep_test="fisherz", )
2. Convert to NetworkX graph
graph = self._to_networkx(cg, data.columns)
3. Apply domain constraints
graph = self._apply_constraints(graph)
4. Orient edges using temporal order
graph = self._apply_temporal_order(graph)
return graph
def _apply_constraints(self, graph: nx.DiGraph) -> nx.DiGraph: """Apply forbidden and required edge constraints."""
Remove forbidden edges
for source, target in self.forbidden: if graph.has_edge(source, target): graph.remove_edge(source, target) logger.info(f"Removed forbidden edge: {source} -> {target}")
Add required edges
for source, target in self.required: if not graph.has_edge(source, target): graph.add_edge(source, target) logger.info(f"Added required edge: {source} -> {target}")
return graph
def _apply_temporal_order(self, graph: nx.DiGraph) -> nx.DiGraph: """Later variables cannot cause earlier variables."""
node_order = {} for order, nodes in enumerate(self.temporal_order): for node in nodes: node_order[node] = order
edges_to_reverse = [] for source, target in graph.edges(): if source in node_order and target in node_order: if node_order[source] > node_order[target]:
Source is later than target - wrong direction
edges_to_reverse.append((source, target))
for source, target in edges_to_reverse: graph.remove_edge(source, target) graph.add_edge(target, source) logger.info(f"Reversed edge based on temporal order: {target} -> {source}")
return graph
---
Name
Counterfactual Reasoning
Description
Answer "what if" questions about past events
When
Understanding what would have happened under different conditions
Example
from dowhy import gcm import numpy as np
class CounterfactualReasoner: """Answer counterfactual queries using fitted SCM."""
def __init__(self, causal_graph: nx.DiGraph): self.scm = gcm.StructuralCausalModel(causal_graph) self._fitted = False
async def fit(self, data: pd.DataFrame) -> None: """Fit causal mechanisms from data.""" gcm.auto.assign_causal_mechanisms(self.scm, data) gcm.fit(self.scm, data) self._fitted = True
async def counterfactual( self, observation: Dict[str, float], intervention: Dict[str, float], target: str, ) -> CounterfactualResult: """ What would target have been if we had intervened?
observation: What we actually observed intervention: What we would have done differently target: What outcome we want to know about """ if not self._fitted: raise ValueError("Must fit SCM before counterfactuals")
Compute counterfactual
cf_samples = gcm.counterfactual_samples( self.scm, interventions={k: lambda v=v: v for k, v in intervention.items()}, observed_data=pd.DataFrame([observation]), num_samples=1000, )
return CounterfactualResult( observed_outcome=observation.get(target), counterfactual_outcome=cf_samples[target].mean(), counterfactual_std=cf_samples[target].std(), confidence_interval=( np.percentile(cf_samples[target], 5), np.percentile(cf_samples[target], 95), ), intervention=intervention, )
---
Name
Causal Attribution for Memories
Description
Attribute outcomes to memories used in decisions
When
Learning which memories actually helped
Example
class MemoryCausalAttributor: """Attribute decision outcomes to memories using causal reasoning."""
async def attribute( self, trace: DecisionTrace, outcome: float, ) -> Dict[UUID, float]: """ How much did each memory causally contribute to the outcome?
Uses Shapley values over a causal graph to attribute credit. """ if not trace.memories_used: return {}
Build mini causal graph for this decision
Memories -> Decision Features -> Outcome
graph = self._build_decision_graph(trace)
Compute Shapley values for causal attribution
attributions = {}
for memory_id in trace.memories_used:
Interventional query: what if this memory wasn't used?
cf_outcome = await self._counterfactual_without_memory( trace, memory_id )
Attribution = actual - counterfactual
attribution = outcome - cf_outcome attributions[memory_id] = attribution
Normalize attributions to sum to outcome
total = sum(abs(a) for a in attributions.values()) if total > 0: attributions = { k: v / total * outcome for k, v in attributions.items() }
return attributions
Anti-Patterns
---
Name
Correlation as Causation
Description
Claiming causal effects from correlation alone
Why
Confounders lurk everywhere. Observed correlation often spurious.
Instead
Build causal graph, identify confounders, use proper estimation
---
Name
Skipping Refutation
Description
Accepting causal estimate without challenging it
Why
Estimates can be artifacts of method or data. Must stress test.
Instead
Always run refutation tests (random cause, placebo, subset)
---
Name
Cyclic Causal Graph
Description
Creating causal graphs with cycles
Why
DAGs are acyclic by definition. Cycles indicate modeling error.
Instead
Temporal ordering prevents cycles. Split feedback loops into time steps.
---
Name
Single Estimator
Description
Using only one causal estimation method
Why
Methods have different assumptions. Single method may be wrong.
Instead
Use multiple estimators, check agreement
---
Name
Ignoring Unobserved Confounders
Description
Assuming all confounders are measured
Why
Reality has unmeasured variables. Sensitivity analysis required.
Instead
Run sensitivity analysis for hidden confounding
Causal Scientist - Sharp Edges
Confounding Everywhere
Id
confounding-everywhere
Summary
Unmeasured confounders invalidate causal estimates
Severity
critical
Situation
You run a causal analysis showing memory type X improves outcomes. You ship a feature prioritizing type X. No improvement in production. Turns out, expert users (confounder) both use type X and have better outcomes.
Why
Observational data has confounders. If you don't measure and control for them, your "causal" effect is actually confounded association. No amount of sophisticated estimation fixes unmeasured confounding.
Solution
Always enumerate potential confounders
class ConfounderAnalysis: COMMON_CONFOUNDERS = [ "user_expertise", "user_activity_level", "content_complexity", "time_of_day", "platform", "prior_outcomes", ]
async def check_for_confounding( self, treatment: str, outcome: str, data: pd.DataFrame, ) -> ConfounderReport: potential_confounders = []
for var in self.COMMON_CONFOUNDERS: if var not in data.columns: potential_confounders.append( UnmeasuredConfounder(var, reason="not in dataset") ) continue
Check if variable is associated with both treatment and outcome
treatment_corr = data[var].corr(data[treatment]) outcome_corr = data[var].corr(data[outcome])
if abs(treatment_corr) > 0.1 and abs(outcome_corr) > 0.1: potential_confounders.append( MeasuredConfounder( var, treatment_corr=treatment_corr, outcome_corr=outcome_corr, ) )
return ConfounderReport( treatment=treatment, outcome=outcome, confounders=potential_confounders, recommendation=self._get_recommendation(potential_confounders), )
def _get_recommendation(self, confounders) -> str: unmeasured = [c for c in confounders if isinstance(c, UnmeasuredConfounder)] if unmeasured: return f"CAUTION: {len(unmeasured)} potential unmeasured confounders" return "Include all measured confounders in analysis"
Run sensitivity analysis for hidden confounding
refutation = model.refute_estimate( identified_estimand, estimate, method_name="add_unobserved_common_cause", confounders_effect_on_treatment="linear", confounders_effect_on_outcome="linear", )
Symptoms
- Effect disappears in A/B test
- Effect varies wildly across user segments
- Adding control variables changes estimate significantly
- Domain experts skeptical of finding
Detection Pattern
estimate.effect(?!.confounder|.*common_cause)
Version Range
>=1.0.0
Dowhy Identification Ignored
Id
dowhy-identification-ignored
Summary
Proceeding with estimation when effect is not identified
Severity
critical
Situation
You run DoWhy with proceed_when_unidentifiable=True because the default blocks your analysis. You get an estimate. It's meaningless.
Why
Identification means: can we answer this causal question from the data and assumptions we have? If not identified, no estimator will give you the right answer. Proceeding anyway gives you a number without meaning.
Solution
NEVER proceed when unidentifiable
model = CausalModel( data=data, treatment=treatment, outcome=outcome, common_causes=confounders,
Specify instruments if available
instruments=instruments, )
Require identification
identified = model.identify_effect( proceed_when_unidentifiable=False # Default, keep it! )
if not identified:
Don't estimate - gather more data or assumptions
return CausalResult( identified=False, reason="Causal effect not identifiable with current data", suggestion="Need instrument variable or more confounders measured" )
Only estimate if identified
estimate = model.estimate_effect(identified_estimand=identified)
Alternative: If you MUST proceed, be explicit about assumptions
class UnidentifiedCausalEstimate: estimate: float assumptions_required: List[str] confidence: str = "LOW - UNIDENTIFIED EFFECT"
def __str__(self): return ( f"CAUTION: Unidentified estimate = {self.estimate}\n" f"Required assumptions:\n" + "\n".join(f" - {a}" for a in self.assumptions_required) )
Symptoms
- DoWhy warns about unidentifiable effect
- Different graph structures give same estimate
- Can't explain what assumptions make estimate valid
- Estimate sensitive to small graph changes
Detection Pattern
proceed_when_unidentifiable.=.True
Version Range
>=1.0.0
Cyclic Causal Graph
Id
cyclic-causal-graph
Summary
Accidentally creating cycles in causal DAG
Severity
high
Situation
You model "user satisfaction causes purchases" and "purchases cause user satisfaction." Your graph has a cycle. All causal reasoning breaks.
Why
Causal DAGs are acyclic by definition. Cycles represent equilibrium or feedback loops over time. To model feedback, unroll over time steps. A cycle in a single time slice is a modeling error.
Solution
Validate DAG is acyclic
import networkx as nx
class CausalGraphValidator: def validate(self, graph: nx.DiGraph) -> ValidationResult: errors = []
Check for cycles
try: cycles = list(nx.simple_cycles(graph)) if cycles: errors.append(CycleError( f"Graph has {len(cycles)} cycles: {cycles[:3]}" )) except nx.NetworkXNoCycle: pass # Good, no cycles
Check for self-loops
self_loops = list(nx.nodes_with_selfloops(graph)) if self_loops: errors.append(SelfLoopError( f"Nodes with self-loops: {self_loops}" ))
return ValidationResult(valid=len(errors) == 0, errors=errors)
Model feedback loops over time
class TemporalCausalModel: """Unroll feedback loops across time steps."""
def add_feedback_loop( self, var_a: str, var_b: str, ) -> None:
Instead of A <-> B (invalid), model:
A_t -> B_t+1 -> A_t+2 -> B_t+3 ...
for t in range(self.time_steps - 1): self.graph.add_edge(f"{var_a}_t{t}", f"{var_b}_t{t+1}") self.graph.add_edge(f"{var_b}_t{t}", f"{var_a}_t{t+1}")
Symptoms
- NetworkX raises cycle error
- DoWhy fails with 'not a DAG'
- Conceptually, variables cause each other
- Graph visualization shows arrows in both directions
Detection Pattern
add_edge.,.\\).add_edge.,.*\\)
Version Range
>=1.0.0
Causal Discovery Data Requirements
Id
causal-discovery-data-requirements
Summary
Running causal discovery on insufficient data
Severity
high
Situation
You run PC algorithm on 50 observations. It discovers a sparse graph (few edges). You conclude there are few causal relationships. Actually, you just didn't have enough data to detect them.
Why
Causal discovery algorithms are statistical tests at heart. With small samples, tests have low power. They fail to reject independence, so edges are missing. More data = more power = more edges discovered.
Solution
Check sample size before discovery
class DataSizedCausalDiscovery:
Rule of thumb: need 50-100 samples per variable for PC
MIN_SAMPLES_PER_VAR = 50
def validate_data_size( self, data: pd.DataFrame, ) -> DataSizeValidation: n_vars = len(data.columns) n_samples = len(data) required = n_vars * self.MIN_SAMPLES_PER_VAR
if n_samples < required: return DataSizeValidation( valid=False, message=f"Have {n_samples} samples, need ~{required} for {n_vars} variables", recommendation="Collect more data or reduce variables", )
Also check for sufficient variance
low_variance = [ col for col in data.columns if data[col].std() < 0.01 ]
if low_variance: return DataSizeValidation( valid=False, message=f"Low variance columns: {low_variance}", recommendation="Remove constant or near-constant columns", )
return DataSizeValidation(valid=True)
async def discover(self, data: pd.DataFrame, **kwargs): validation = self.validate_data_size(data)
if not validation.valid: raise InsufficientDataError(validation.message)
Adjust alpha based on sample size
Larger samples -> can use smaller alpha (stricter)
n = len(data) alpha = 0.05 if n > 1000 else 0.1 if n > 500 else 0.2
return await self._run_pc(data, alpha=alpha, **kwargs)
Symptoms
- Discovered graph is surprisingly sparse
- Adding more data changes graph structure significantly
- Different random seeds give very different graphs
- p-values all just above significance threshold
Detection Pattern
pc\\(|causal.discover(?!.sample|.*size)
Version Range
>=1.0.0
Estimator Assumption Mismatch
Id
estimator-assumption-mismatch
Summary
Using estimator with violated assumptions
Severity
medium
Situation
You use linear regression backdoor adjustment. Your treatment effect is actually non-linear. Estimate is wrong, and you don't know it.
Why
Each causal estimator has assumptions. Linear regression assumes linear effects. Propensity score assumes correct propensity model. Violating assumptions biases estimates in unpredictable ways.
Solution
Use multiple estimators and check agreement
class RobustEstimation: ESTIMATOR_ASSUMPTIONS = { "backdoor.linear_regression": [ "Linear treatment effect", "No interaction effects", "Correct confounders specified", ], "backdoor.propensity_score_weighting": [ "Correct propensity model", "Positivity (overlap)", "No unmeasured confounders", ], "iv.instrumental_variable": [ "Valid instrument (relevance + exclusion)", "Monotonicity", "No direct effect of instrument on outcome", ], }
async def robust_estimate( self, model: CausalModel, identified_estimand, ) -> RobustEstimateResult: estimates = {} failed = {}
for method in self.ESTIMATOR_ASSUMPTIONS.keys(): try: est = model.estimate_effect( identified_estimand, method_name=method, ) estimates[method] = est.value except Exception as e: failed[method] = str(e)
if not estimates: raise NoValidEstimatorError("All estimators failed")
Check agreement
values = list(estimates.values()) mean_est = np.mean(values) std_est = np.std(values) cv = std_est / abs(mean_est) if mean_est != 0 else float('inf')
return RobustEstimateResult( estimates=estimates, mean=mean_est, std=std_est, coefficient_of_variation=cv, agreement="GOOD" if cv < 0.3 else "POOR" if cv > 0.5 else "MODERATE", failed_methods=failed, )
Symptoms
- Estimates vary widely across methods
- Residuals show patterns (non-linearity)
- Effect changes when adding interactions
- Propensity scores near 0 or 1
Detection Pattern
estimate_effect.method_name.=(?!.multiple|.robust)
Version Range
>=1.0.0
Counterfactual Identifiability
Id
counterfactual-identifiability
Summary
Computing counterfactuals without proper assumptions
Severity
medium
Situation
You want to know "what would have happened if user saw memory X instead of Y?" You compute a counterfactual. It's meaningless because the counterfactual isn't identifiable from observational data.
Why
Counterfactuals about individuals are fundamentally unidentifiable from observational data alone. You need strong assumptions (functional form) or randomization. Most counterfactual estimates are modeler's projections, not data-driven answers.
Solution
Be explicit about counterfactual assumptions
@dataclass class CounterfactualQuery: observation: Dict[str, float] intervention: Dict[str, float] target: str
Required assumptions
assumed_mechanisms: List[str] # E.g., "linear", "additive noise" identifiable: bool confidence_level: str # "high", "medium", "low"
class CarefulCounterfactualReasoner: async def compute_counterfactual( self, query: CounterfactualQuery, scm: gcm.StructuralCausalModel, ) -> CounterfactualResult:
Validate assumptions
warnings = []
1. Check if mechanisms are correctly specified
for node in query.intervention.keys(): mech = scm.causal_mechanism(node) if isinstance(mech, gcm.ml.create_linear_regressor): warnings.append( f"Assuming linear mechanism for {node}" )
2. Check for extrapolation
for var, value in query.intervention.items(): observed_range = ( self.training_data[var].min(), self.training_data[var].max() ) if value < observed_range[0] or value > observed_range[1]: warnings.append( f"Intervention {var}={value} outside observed range {observed_range}" )
3. Compute with uncertainty
samples = gcm.counterfactual_samples( scm, interventions={k: lambda v=v: v for k, v in query.intervention.items()}, observed_data=pd.DataFrame([query.observation]), num_samples=1000, )
return CounterfactualResult( point_estimate=samples[query.target].mean(), uncertainty=samples[query.target].std(), warnings=warnings, confidence=query.confidence_level, disclaimer="Counterfactual depends on assumed causal mechanisms", )
Symptoms
- Counterfactual far from any observed data
- Small mechanism changes → large counterfactual changes
- Counterfactual outside plausible range
- No uncertainty quantification
Detection Pattern
counterfactual(?!.assumption|.uncertaint)
Version Range
>=1.0.0
Selection Bias Ignored
Id
selection-bias-ignored
Summary
Analyzing selected sample as if it were random
Severity
medium
Situation
You analyze "users who clicked" to understand click effects. But users who clicked are different from those who didn't. Your analysis is biased by the selection process.
Why
Selection on a collider (or descendant of collider) induces spurious associations. Analyzing selected samples without modeling selection gives biased causal estimates.
Solution
Detect and handle selection bias
class SelectionBiasChecker: async def check_selection_bias( self, full_population: pd.DataFrame, analyzed_sample: pd.DataFrame, treatment: str, outcome: str, ) -> SelectionBiasReport:
1. Check if sample is representative
representation_issues = []
for col in full_population.columns: full_dist = full_population[col].describe() sample_dist = analyzed_sample[col].describe()
if abs(full_dist['mean'] - sample_dist['mean']) > full_dist['std']: representation_issues.append(col)
2. Check if selection is on collider path
(Would need causal graph to fully determine)
3. Estimate selection probability
selection_indicator = pd.Series( full_population.index.isin(analyzed_sample.index), index=full_population.index, )
Fit selection model
from sklearn.linear_model import LogisticRegression X = full_population[[treatment, outcome]].values selection_model = LogisticRegression().fit(X, selection_indicator)
selection_prob = selection_model.predict_proba(X)[:, 1]
return SelectionBiasReport( sample_size_ratio=len(analyzed_sample) / len(full_population), representation_issues=representation_issues, selection_depends_on_treatment=( abs(selection_model.coef_[0][0]) > 0.1 ), selection_depends_on_outcome=( abs(selection_model.coef_[0][1]) > 0.1 ), recommendation=self._get_recommendation(representation_issues), )
def _get_recommendation(self, issues) -> str: if not issues: return "Sample appears representative" return ( f"Sample differs on: {issues}. " "Consider inverse probability weighting or Heckman correction." )
Symptoms
- Results don't replicate in full population
- Sample is non-random subset (e.g., only converters)
- Sample characteristics differ from population
- Selection mechanism related to treatment or outcome
Detection Pattern
filter.\\[|query.WHERE(?!.*random)
Version Range
>=1.0.0
Causal Scientist - Validations
DoWhy Proceeding When Unidentifiable
Id
dowhy-proceed-unidentifiable
Severity
error
Type
regex
Pattern
- proceed_when_unidentifiable.=.True
- proceed_when_unidentifiable=True
Message
DoWhy proceeding when effect is unidentifiable. Estimate is meaningless.
Fix Action
Set proceed_when_unidentifiable=False and handle unidentifiable cases
Applies To
- /causal//*.py
- */causal*.py
Causal Estimate Without Refutation
Id
causal-no-refutation
Severity
error
Type
regex
Pattern
- estimate_effect\\([^)]\\)(?!.refute)
- model\\.estimate(?!.*refut)
Message
Causal estimate without refutation tests. Effect may not be robust.
Fix Action
Add refutation tests: random_common_cause, placebo, data_subset
Applies To
- /causal//*.py
Using Only Single Causal Estimator
Id
causal-single-estimator
Severity
warning
Type
regex
Pattern
- method_name.=.["']backdoor\.
- estimate_effect.method.=(?!.multiple|.robust)
Message
Using single causal estimator. Use multiple for robustness.
Fix Action
Try multiple estimators and check agreement
Applies To
- /causal//*.py
Causal Graph Without Cycle Check
Id
causal-graph-no-validation
Severity
error
Type
regex
Pattern
- DiGraph\\(\\)(?!.is_directed_acyclic|.simple_cycles)
- add_edge(?!.validate|.acyclic)
Message
Building causal graph without validating DAG properties.
Fix Action
Validate graph is acyclic with nx.is_directed_acyclic_graph()
Applies To
- /causal//*.py
- /graph//*.py
Causal Discovery Without Sample Size Check
Id
causal-discovery-no-sample-check
Severity
warning
Type
regex
Pattern
- pc\\((?!.len.>|.*sample)
- fci\\((?!.size|.sample)
Message
Causal discovery without checking sample size. May lack power.
Fix Action
Verify sufficient samples per variable (~50-100)
Applies To
- /causal//*.py
Counterfactual Without Uncertainty
Id
counterfactual-no-uncertainty
Severity
warning
Type
regex
Pattern
- counterfactual.\\.mean\\(\\)(?!.std|.*percentile)
- counterfactual(?!.confidence|.interval|.*std)
Message
Counterfactual without uncertainty quantification.
Fix Action
Report confidence intervals for counterfactual estimates
Applies To
- /causal//*.py
Causal Model Without Confounders
Id
causal-no-confounders
Severity
warning
Type
regex
Pattern
- CausalModel\\([^)]treatment[^)]outcome[^)]\\)(?!.common_causes)
- common_causes.=.\\[\\]
- common_causes.=.None
Message
Causal model without confounders specified. Effect likely biased.
Fix Action
Identify and specify common causes (confounders)
Applies To
- /causal//*.py
Causal Claim From Correlation
Id
causal-correlation-claim
Severity
error
Type
regex
Pattern
- corr.caus|caus.corr
- \\.corr\\(\\).*effect
- correlation.implies.causation
Message
Correlation used to claim causation. Need proper causal inference.
Fix Action
Use DoWhy or similar for proper causal effect estimation
Applies To
- */.py
Causal Effect Without Confidence Interval
Id
causal-effect-no-ci
Severity
info
Type
regex
Pattern
- estimate\\.value(?!.confidence|.interval)
- effect.=.estimate(?!.ci|.interval)
Message
Causal effect reported without confidence interval.
Fix Action
Use estimate.get_confidence_intervals()
Applies To
- /causal//*.py
Causal Graph Without Temporal Ordering
Id
causal-temporal-order-ignored
Severity
info
Type
regex
Pattern
- add_edge(?!.temporal|.time|.*order)
- CausalModel(?!.*temporal)
Message
Causal graph built without temporal ordering constraints.
Fix Action
Apply temporal ordering: causes must precede effects
Applies To
- /causal//*.py
Intervention Outside Observed Range
Id
intervention-extrapolation
Severity
warning
Type
regex
Pattern
- intervention.=(?!.min|.max|.range)
- do\\((?!.bounds|.range)
Message
Intervention may be outside observed data range.
Fix Action
Check if intervention value is within training data range
Applies To
- /causal//*.py
Using ATE for Individual Predictions
Id
ate-not-individual
Severity
warning
Type
regex
Pattern
- average.treatment.effect.*individual
- ate.predict.user
Message
Average treatment effect used for individual predictions.
Fix Action
Consider CATE (Conditional Average Treatment Effect) for heterogeneous effects
Applies To
- /causal//*.py