33ExactValue, Positive, Negative, SomeEffect, NoEffect"""
44
55from abc import ABC , abstractmethod
6- from collections .abc import Iterable
76
87import numpy as np
98
@@ -23,25 +22,27 @@ def apply(self, effect_estimate: EffectEstimate) -> bool:
2322 def __str__ (self ) -> str :
2423 return type (self ).__name__
2524
25+ def to_dict (self ):
26+ """
27+ Convert the expected effect to a python dictionary for easy serialisation as JSON.
28+
29+ :returns: A JSON serialisable dict representing the expected effect.
30+ """
31+ return {"name" : self .__class__ .__name__ }
32+
2633
2734class SomeEffect (CausalEffect ):
2835 """An extension of CausalEffect representing that the expected causal effect should not be zero."""
2936
3037 def apply (self , effect_estimate : EffectEstimate ) -> bool :
31- if effect_estimate .ci_low is None or effect_estimate .ci_high is None :
32- return None
33- if effect_estimate .type in ("risk_ratio" , "hazard_ratio" , "unit_odds_ratio" ):
34- return any (
35- 1 < ci_low < ci_high or ci_low < ci_high < 1
36- for ci_low , ci_high in zip (effect_estimate .ci_low , effect_estimate .ci_high )
37- )
38- if effect_estimate .type in ("coefficient" , "ate" ):
39- return any (
40- 0 < ci_low < ci_high or ci_low < ci_high < 0
41- for ci_low , ci_high in zip (effect_estimate .ci_low , effect_estimate .ci_high )
42- )
38+ if effect_estimate .type in ("risk_ratio" , "hazard_ratio" , "unit_odds_ratio" , "odds_ratio" ):
39+ value_to_check = 1
40+ elif effect_estimate .type in ("coefficient" , "ate" ):
41+ value_to_check = 0
42+ else :
43+ raise ValueError (f"Test Value type { effect_estimate .type } is not valid for this CausalEffect" )
4344
44- raise ValueError ( f"Test Value type { effect_estimate .type } is not valid for this CausalEffect" )
45+ return ( ~ (( effect_estimate .ci_low <= value_to_check ) & ( value_to_check <= effect_estimate . ci_high ))). all ( )
4546
4647
4748class NoEffect (CausalEffect ):
@@ -52,30 +53,30 @@ class NoEffect(CausalEffect):
5253 :param ctol: Categorical tolerance. The test will pass if this proportion of categories pass.
5354 """
5455
55- def __init__ (self , atol : float = 1e-10 , ctol : float = 0.05 ):
56+ def __init__ (self , atol : float = 0 , ctol : float = 0.0 ):
5657 self .atol = atol
5758 self .ctol = ctol
5859
5960 def apply (self , effect_estimate : EffectEstimate ) -> bool :
6061 if effect_estimate .type in ("risk_ratio" , "hazard_ratio" , "unit_odds_ratio" , "odds_ratio" ):
61- return any (
62- ci_low < 1 < ci_high or np .isclose (value , 1.0 , atol = self .atol )
63- for ci_low , ci_high , value in zip (
64- effect_estimate .ci_low , effect_estimate .ci_high , effect_estimate .value
65- )
66- )
67- if effect_estimate .type in ("coefficient" , "ate" ):
68- value = effect_estimate .value if isinstance (effect_estimate .ci_high , Iterable ) else [effect_estimate .value ]
69- return (
70- sum (
71- not ((ci_low < 0 < ci_high ) or abs (v ) < self .atol )
72- for ci_low , ci_high , v in zip (effect_estimate .ci_low , effect_estimate .ci_high , value )
73- )
74- / len (value )
75- < self .ctol
76- )
62+ value_to_check = 1
63+ elif effect_estimate .type in ("coefficient" , "ate" ):
64+ value_to_check = 0
65+ else :
66+ raise ValueError (f"Test Value type { effect_estimate .type } is not valid for this CausalEffect" )
67+
68+ return sum (
69+ ((effect_estimate .ci_low <= value_to_check ) & (value_to_check <= effect_estimate .ci_high ))
70+ | (np .isclose (effect_estimate .value , value_to_check , atol = self .atol ))
71+ ) / len (effect_estimate .value ) >= (1 - self .ctol )
72+
73+ def to_dict (self ):
74+ """
75+ Convert the expected effect to a python dictionary for easy serialisation as JSON.
7776
78- raise ValueError (f"Test Value type { effect_estimate .type } is not valid for this CausalEffect" )
77+ :returns: A JSON serialisable dict representing the expected effect.
78+ """
79+ return {"name" : self .__class__ .__name__ , "atol" : self .atol , "ctol" : self .ctol }
7980
8081
8182class ExactValue (CausalEffect ):
@@ -98,21 +99,37 @@ def __init__(self, value: float, atol: float = None, ci_low: float = None, ci_hi
9899 if self .value - self .atol < self .ci_low or self .value + self .atol > self .ci_high :
99100 raise ValueError (
100101 "Arithmetic tolerance falls outside the confidence intervals."
101- "Try specifying a smaller value of atol."
102+ f "Try specifying wider intervals or a value of atol smaller than the current vlaue { self . atol } ."
102103 )
103104
104105 def apply (self , effect_estimate : EffectEstimate ) -> bool :
105106 close = np .isclose (effect_estimate .value , self .value , atol = self .atol )
106107 if effect_estimate .ci_valid and self .ci_low is not None and self .ci_high is not None :
107- return all (
108- close and self .ci_low <= ci_low and self .ci_high >= ci_high
109- for ci_low , ci_high in zip (effect_estimate .ci_low , effect_estimate .ci_high )
108+ return (
109+ close .all ()
110+ and (self .ci_low <= effect_estimate .ci_low ).all ()
111+ and (self .ci_high >= effect_estimate .ci_high ).all ()
110112 )
111- return close
113+ return close . all ()
112114
113115 def __str__ (self ):
114116 return f"ExactValue: { self .value } ±{ self .atol } "
115117
118+ def to_dict (self ):
119+ """
120+ Convert the expected effect to a python dictionary for easy serialisation as JSON or CSV.
121+
122+ :returns: A JSON serialisable dict representing the expected effect.
123+ """
124+ effect = {"value" : self .value }
125+ if self .ci_low :
126+ effect ["ci_low" ] = self .ci_low
127+ if self .ci_low :
128+ effect ["ci_high" ] = self .ci_high
129+ if self .atol :
130+ effect ["atol" ] = self .atol
131+ return {"name" : self .__class__ .__name__ } | effect
132+
116133
117134class Positive (SomeEffect ):
118135 """An extension of CausalEffect representing that the expected causal effect should be positive.
0 commit comments