Describe the bug
dowhy.utils.graph_operations.daggity_to_dot() (used by build_graph_from_str() and therefore by CausalModel(graph=...) for Dagitty input) removes the Dagitty node flags exposure, outcome, adjusted and latent with plain substring substitutions over the whole graph string:
graph = re.sub("outcome,*,", "", graph)
graph = re.sub("adjusted,*", "", graph)
graph = re.sub("exposure,*", "", graph)
graph = re.sub("latent,*", 'observed="no",', graph)
Because the patterns are not anchored to the [...] attribute list, any node name that contains one of these words is rewritten too:
sun_exposure becomes sun_, unadjusted_x becomes un_x (silently renamed, so the node no longer matches the DataFrame column);
latent_u becomes observed="no",_u, which is not valid DOT, so parsing fails;
- a node literally named
exposure (or outcome when it carries other attributes) is deleted entirely, so parsing fails.
Naming the treatment exposure and the outcome outcome is the standard convention in epidemiology, which is Dagitty's main audience, so this is easy to hit with a graph exported straight from dagitty.net.
Steps to reproduce the behavior
from dowhy.graph import build_graph_from_str
from dowhy.utils.graph_operations import daggity_to_dot
dag = 'dag {\nexposure [exposure,pos="0,0"]\noutcome [outcome,pos="1,1"]\nexposure -> outcome\n}'
print(daggity_to_dot(dag))
# digraph { [pos="0,0"]; outcome [pos="1,1"]; -> outcome; }
build_graph_from_str(dag)
# pygraphviz: "Expected rbrace, found '['" -> falls back to pydot -> TypeError: 'NoneType' object is not subscriptable
dag = 'dag {\nsun_exposure [exposure,pos="0.3,0.5"]\nunadjusted_x [adjusted,pos="0.1,0.1"]\nlatent_u [latent,pos="0.5,0.8"]\nsun_exposure -> unadjusted_x\nlatent_u -> sun_exposure\n}'
print(daggity_to_dot(dag))
# digraph { sun_ [pos="0.3,0.5"]; un_x [pos="0.1,0.1"]; observed="no",_u [observed="no",pos="0.5,0.8"]; sun_ -> un_x; observed="no",_u -> sun_; }
Output on current main (b06369e):
case 1 DOT: 'digraph { [pos="0,0"]; outcome [pos="1,1"]; -> outcome; }'
case 1 FAILED: TypeError 'NoneType' object is not subscriptable
case 2 DOT: 'digraph { sun_ [pos="0.3,0.5"]; un_x [pos="0.1,0.1"]; observed="no",_u [observed="no",pos="0.5,0.8"]; sun_ -> un_x; observed="no",_u -> sun_; }'
case 2 FAILED: TypeError 'NoneType' object is not subscriptable
A side effect of the same regexes: Y [outcome] with no other attribute is not stripped at all (the pattern requires a trailing comma), so outcome is passed through to DOT as a bare attribute.
Expected behavior
Only the Dagitty flags inside a node's [...] attribute list should be removed/translated; node names and edge statements should be left untouched:
digraph { exposure [pos="0,0"]; outcome [pos="1,1"]; exposure -> outcome; }
digraph { sun_exposure [pos="0.3,0.5"]; unadjusted_x [pos="0.1,0.1"]; latent_u [observed="no",pos="0.5,0.8"]; sun_exposure -> unadjusted_x; latent_u -> sun_exposure; }
Version information:
Additional context
The fix is small: parse the content of each [...] block, split it on commas that are outside double quotes (so pos="0.1,0.2" survives), drop exposure/outcome/adjusted and map latent to observed="no" only when they are whole attributes. I have a patch with regression tests ready and can open a PR.
Disclosure: found, reproduced and the draft fix tested locally with the help of an AI coding agent (Claude Code); reviewed before filing.
Describe the bug
dowhy.utils.graph_operations.daggity_to_dot()(used bybuild_graph_from_str()and therefore byCausalModel(graph=...)for Dagitty input) removes the Dagitty node flagsexposure,outcome,adjustedandlatentwith plain substring substitutions over the whole graph string:Because the patterns are not anchored to the
[...]attribute list, any node name that contains one of these words is rewritten too:sun_exposurebecomessun_,unadjusted_xbecomesun_x(silently renamed, so the node no longer matches the DataFrame column);latent_ubecomesobserved="no",_u, which is not valid DOT, so parsing fails;exposure(oroutcomewhen it carries other attributes) is deleted entirely, so parsing fails.Naming the treatment
exposureand the outcomeoutcomeis the standard convention in epidemiology, which is Dagitty's main audience, so this is easy to hit with a graph exported straight from dagitty.net.Steps to reproduce the behavior
Output on current
main(b06369e):A side effect of the same regexes:
Y [outcome]with no other attribute is not stripped at all (the pattern requires a trailing comma), sooutcomeis passed through to DOT as a bare attribute.Expected behavior
Only the Dagitty flags inside a node's
[...]attribute list should be removed/translated; node names and edge statements should be left untouched:Version information:
mainat b06369e (also present in v0.14; the code is unchanged since the Dagitty support was added in Add support for daggity graphs #358)Additional context
The fix is small: parse the content of each
[...]block, split it on commas that are outside double quotes (sopos="0.1,0.2"survives), dropexposure/outcome/adjustedand maplatenttoobserved="no"only when they are whole attributes. I have a patch with regression tests ready and can open a PR.Disclosure: found, reproduced and the draft fix tested locally with the help of an AI coding agent (Claude Code); reviewed before filing.