tigramite-fast 5.2.10.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tigramite/__init__.py +0 -0
- tigramite/causal_effects.py +1525 -0
- tigramite/causal_mediation.py +1592 -0
- tigramite/data_processing.py +1574 -0
- tigramite/graphs.py +1509 -0
- tigramite/independence_tests/LBFGS.py +1114 -0
- tigramite/independence_tests/__init__.py +0 -0
- tigramite/independence_tests/cmiknn.py +661 -0
- tigramite/independence_tests/cmiknn_mixed.py +1397 -0
- tigramite/independence_tests/cmisymb.py +286 -0
- tigramite/independence_tests/gpdc.py +664 -0
- tigramite/independence_tests/gpdc_torch.py +820 -0
- tigramite/independence_tests/gsquared.py +190 -0
- tigramite/independence_tests/independence_tests_base.py +1310 -0
- tigramite/independence_tests/oracle_conditional_independence.py +1582 -0
- tigramite/independence_tests/pairwise_CI.py +383 -0
- tigramite/independence_tests/parcorr.py +369 -0
- tigramite/independence_tests/parcorr_mult.py +485 -0
- tigramite/independence_tests/parcorr_wls.py +451 -0
- tigramite/independence_tests/regressionCI.py +403 -0
- tigramite/independence_tests/robust_parcorr.py +403 -0
- tigramite/jpcmciplus.py +966 -0
- tigramite/lpcmci.py +3649 -0
- tigramite/models.py +2257 -0
- tigramite/pcmci.py +3935 -0
- tigramite/pcmci_base.py +1218 -0
- tigramite/plotting.py +4735 -0
- tigramite/rpcmci.py +467 -0
- tigramite/toymodels/__init__.py +0 -0
- tigramite/toymodels/context_model.py +261 -0
- tigramite/toymodels/non_additive.py +1231 -0
- tigramite/toymodels/structural_causal_processes.py +1201 -0
- tigramite/toymodels/surrogate_generator.py +319 -0
- tigramite_fast-5.2.10.1.dist-info/METADATA +182 -0
- tigramite_fast-5.2.10.1.dist-info/RECORD +38 -0
- tigramite_fast-5.2.10.1.dist-info/WHEEL +5 -0
- tigramite_fast-5.2.10.1.dist-info/licenses/license.txt +621 -0
- tigramite_fast-5.2.10.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1592 @@
|
|
|
1
|
+
"""Tigramite causal inference for time series."""
|
|
2
|
+
|
|
3
|
+
# Authors: Martin Rabel, Jakob Runge <jakob@jakob-runge.com>
|
|
4
|
+
#
|
|
5
|
+
# License: GNU General Public License v3.0
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from sklearn.neighbors import KNeighborsRegressor, KernelDensity
|
|
9
|
+
from functools import partial
|
|
10
|
+
import tigramite.toymodels.non_additive as toy_setup
|
|
11
|
+
from tigramite.causal_effects import CausalEffects
|
|
12
|
+
from tigramite.toymodels.non_additive import _Fct_on_grid, _Fct_smoothed
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
"""-------------------------------------------------------------------------------------------
|
|
17
|
+
----------------------------- Helpers for Mixed Data Fitting -----------------------------
|
|
18
|
+
-------------------------------------------------------------------------------------------"""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MixedData:
|
|
22
|
+
"""Namescope for mixed-data helpers.
|
|
23
|
+
|
|
24
|
+
Mostly for internal use.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def IsPurelyCategorical(vars_and_data):
|
|
29
|
+
"""Check if all variables are categorical
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
vars_and_data : dictionary< toy_setup.VariableDescription, np.array(N) >
|
|
34
|
+
Samples with meta-data.
|
|
35
|
+
|
|
36
|
+
Returns
|
|
37
|
+
-------
|
|
38
|
+
purely categorical : bool
|
|
39
|
+
If all variables in vars_and_data are categorical, return true, otherwise false.
|
|
40
|
+
"""
|
|
41
|
+
for var in vars_and_data.keys():
|
|
42
|
+
if not var.Id().is_categorical:
|
|
43
|
+
return False
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def _FixZeroDim(continuous_data, categorical_data):
|
|
48
|
+
"""[INTERNAL] Ensure consistent shape (0,N) for batches containing zero categorical or continuous variables"""
|
|
49
|
+
if len(continuous_data) == 0:
|
|
50
|
+
assert len(categorical_data) != 0
|
|
51
|
+
continuous_data = np.empty([0, np.shape(categorical_data)[1]], dtype="float64")
|
|
52
|
+
categorical_data = np.array(categorical_data)
|
|
53
|
+
elif len(categorical_data) == 0:
|
|
54
|
+
categorical_data = np.empty([0, np.shape(continuous_data)[1]], dtype="int32")
|
|
55
|
+
continuous_data = np.array(continuous_data)
|
|
56
|
+
else:
|
|
57
|
+
continuous_data = np.array(continuous_data)
|
|
58
|
+
categorical_data = np.array(categorical_data)
|
|
59
|
+
return continuous_data, categorical_data
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def Get_data_via_var_ids(x, var_ids):
|
|
63
|
+
"""For a subset of variables, get separate batches for continuous and categorical data, and category-shape
|
|
64
|
+
|
|
65
|
+
Parameters
|
|
66
|
+
----------
|
|
67
|
+
x : dictionary< toy_setup.VariableDescription, np.array(N) >
|
|
68
|
+
Samples and meta-data.
|
|
69
|
+
var_ids : *iterable* <toy_setup.VariableDescription>
|
|
70
|
+
Subset of variables to extract data for.
|
|
71
|
+
|
|
72
|
+
Returns
|
|
73
|
+
-------
|
|
74
|
+
continuous_data : np.array( # continuous variables in var_ids, # samples )
|
|
75
|
+
Continuous entries of the data.
|
|
76
|
+
categorical_data : np.array( # categorical variables in var_ids, # samples )
|
|
77
|
+
Categorical entries of the data.
|
|
78
|
+
category_shape : list <uint>
|
|
79
|
+
List of the number of categories for each categorical variable in var_ids in the same ordering.
|
|
80
|
+
"""
|
|
81
|
+
category_shape = []
|
|
82
|
+
categorical_data = []
|
|
83
|
+
continuous_data = []
|
|
84
|
+
for desc in var_ids:
|
|
85
|
+
if desc.is_categorical:
|
|
86
|
+
category_shape.append(desc.categories)
|
|
87
|
+
categorical_data.append(x[desc])
|
|
88
|
+
else:
|
|
89
|
+
continuous_data.append(x[desc])
|
|
90
|
+
return MixedData._FixZeroDim(continuous_data, categorical_data) + (category_shape,)
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def Split_data_into_categorical_and_continuous(cls, x):
|
|
94
|
+
"""Get separate batches for continuous and categorical data, and category-shape
|
|
95
|
+
|
|
96
|
+
(see Get_data_via_var_ids)
|
|
97
|
+
"""
|
|
98
|
+
return cls.Get_data_via_var_ids(x, x.keys())
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def CategoryCount(category_shape):
|
|
102
|
+
"""Get total number of categories in shape.
|
|
103
|
+
|
|
104
|
+
Parameters
|
|
105
|
+
----------
|
|
106
|
+
category_shape : list<uint>
|
|
107
|
+
Number of categories per variable.
|
|
108
|
+
|
|
109
|
+
Returns
|
|
110
|
+
-------
|
|
111
|
+
total : uint
|
|
112
|
+
Total number of categories in the product.
|
|
113
|
+
"""
|
|
114
|
+
result = 1
|
|
115
|
+
for c in category_shape:
|
|
116
|
+
result *= c
|
|
117
|
+
return result
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def Get_Len(continuous, categorical):
|
|
121
|
+
"""Extract number of samples from data"""
|
|
122
|
+
if np.shape(continuous)[0] != 0:
|
|
123
|
+
return np.shape(continuous)[1]
|
|
124
|
+
else:
|
|
125
|
+
assert np.shape(categorical)[0] != 0
|
|
126
|
+
return np.shape(categorical)[1]
|
|
127
|
+
|
|
128
|
+
@staticmethod
|
|
129
|
+
def SimplifyIfTrivialVector(x):
|
|
130
|
+
"""Make shapes consistent."""
|
|
131
|
+
result = np.squeeze(x)
|
|
132
|
+
if np.shape(result) == ():
|
|
133
|
+
return result[()] # turn a scalar from array(x) of shape () to an actual scalar
|
|
134
|
+
else:
|
|
135
|
+
return result
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
def Call_map(cls, f, x):
|
|
139
|
+
"""Execute call consistently."""
|
|
140
|
+
if hasattr(f, '__iter__'):
|
|
141
|
+
result = []
|
|
142
|
+
for coord in f:
|
|
143
|
+
result.append(cls.Call_map(coord, x))
|
|
144
|
+
return np.array(result)
|
|
145
|
+
if callable(f):
|
|
146
|
+
return cls.SimplifyIfTrivialVector(f(x))
|
|
147
|
+
else:
|
|
148
|
+
return cls.SimplifyIfTrivialVector(f.predict(x))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class FitProvider_Continuous_Default:
|
|
152
|
+
r"""Helper for fitting continuous maps.
|
|
153
|
+
|
|
154
|
+
See "Technical Appendix B" of the Causal Mediation Tutorial.
|
|
155
|
+
|
|
156
|
+
Parameters
|
|
157
|
+
----------
|
|
158
|
+
fit_map : *callable* (np.array(N, dim x), np.array(N))
|
|
159
|
+
A callable, that given data (x,y) fits a regressor f s.t. y = f(x)
|
|
160
|
+
fit_map_1d : *None* or *callable* (np.array(N, 1), np.array(N))
|
|
161
|
+
overwrite fit_map in the case of a 1-dimensional domain.
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
def __init__(self, fit_map, fit_map_1d=None):
|
|
165
|
+
self.fit_map = fit_map
|
|
166
|
+
self.fit_map_1d = fit_map_1d
|
|
167
|
+
|
|
168
|
+
@classmethod
|
|
169
|
+
def UseSklearn(cls, neighbors=10, weights='uniform'):
|
|
170
|
+
"""Use an sci-kit learn KNeighborsRegressor
|
|
171
|
+
|
|
172
|
+
Parameters
|
|
173
|
+
----------
|
|
174
|
+
neighbors : uint
|
|
175
|
+
The number of neighbors to consider
|
|
176
|
+
weights : string
|
|
177
|
+
Either 'uniform' or 'radius', see sci-kit learn.
|
|
178
|
+
|
|
179
|
+
Returns
|
|
180
|
+
-------
|
|
181
|
+
K-Neighbors Fit-Provider : FitProvider_Continuous_Default
|
|
182
|
+
Fit-Provider for use with FitSetup.
|
|
183
|
+
"""
|
|
184
|
+
return cls(lambda x, y: KNeighborsRegressor(n_neighbors=neighbors, weights=weights).fit(x, y))
|
|
185
|
+
|
|
186
|
+
@classmethod
|
|
187
|
+
def UseSklearn_GC(cls, **kwargs):
|
|
188
|
+
"""Use an sci-kit learn GaussianProcessRegressor
|
|
189
|
+
|
|
190
|
+
Parameters
|
|
191
|
+
----------
|
|
192
|
+
... : any-type
|
|
193
|
+
forwarded to sci-kit
|
|
194
|
+
|
|
195
|
+
Returns
|
|
196
|
+
-------
|
|
197
|
+
GC Fit-Provider : FitProvider_Continuous_Default
|
|
198
|
+
Fit-Provider for use with FitSetup.
|
|
199
|
+
"""
|
|
200
|
+
from sklearn.gaussian_process import GaussianProcessRegressor
|
|
201
|
+
return cls(lambda x, y: GaussianProcessRegressor(**kwargs).fit(x, y))
|
|
202
|
+
|
|
203
|
+
def Get_Fit_Continuous(self, x, y):
|
|
204
|
+
"""Produce a fit
|
|
205
|
+
|
|
206
|
+
Parameters
|
|
207
|
+
----------
|
|
208
|
+
x : np.array(N, dim x)
|
|
209
|
+
Predictor values
|
|
210
|
+
y : np.array(N)
|
|
211
|
+
Training values
|
|
212
|
+
|
|
213
|
+
Returns
|
|
214
|
+
-------
|
|
215
|
+
Continuous Fit : *callable* (x)
|
|
216
|
+
The fitted predictor.
|
|
217
|
+
"""
|
|
218
|
+
dim_of_domain = np.shape(x)[1]
|
|
219
|
+
if dim_of_domain == 0:
|
|
220
|
+
return lambda x_dim_zero: np.mean(y)
|
|
221
|
+
elif dim_of_domain == 1 and self.fit_map_1d is not None:
|
|
222
|
+
return self.fit_map_1d(x, y) # use splines?
|
|
223
|
+
else:
|
|
224
|
+
return self.fit_map(x, y)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class FitProvider_Density_Default:
|
|
228
|
+
r"""Helper for fitting continuous densities.
|
|
229
|
+
|
|
230
|
+
See "Technical Appendix B" of the Causal Mediation Tutorial.
|
|
231
|
+
|
|
232
|
+
Parameters
|
|
233
|
+
----------
|
|
234
|
+
fit_density : *callable* (np.array(N, dim x))
|
|
235
|
+
A callable, that given data (x) fits a density estimator p s.t. x ~ p
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
def __init__(self, fit_density):
|
|
239
|
+
self.fit_density = fit_density
|
|
240
|
+
|
|
241
|
+
@classmethod
|
|
242
|
+
def UseSklearn(cls, kernel='gaussian', bandwidth=0.2):
|
|
243
|
+
"""Use an sci-kit learn KernelDensity
|
|
244
|
+
|
|
245
|
+
Parameters
|
|
246
|
+
----------
|
|
247
|
+
kernel : string
|
|
248
|
+
E.g. 'gaussian', see sci-kit learn documentation.
|
|
249
|
+
bandwidth : float
|
|
250
|
+
See sci-kit learn documentation.
|
|
251
|
+
|
|
252
|
+
Returns
|
|
253
|
+
-------
|
|
254
|
+
Density Estimator : FitProvider_Density_Default
|
|
255
|
+
Fit-Provider for use with FitSetup.
|
|
256
|
+
"""
|
|
257
|
+
return cls(lambda x: KernelDensity(kernel=kernel, bandwidth=bandwidth).fit(x))
|
|
258
|
+
|
|
259
|
+
def Get_Fit_Density(self, x_train):
|
|
260
|
+
"""Produce a fit
|
|
261
|
+
|
|
262
|
+
Parameters
|
|
263
|
+
----------
|
|
264
|
+
x_train : np.array(N, dim x)
|
|
265
|
+
Training samples.
|
|
266
|
+
|
|
267
|
+
Returns
|
|
268
|
+
-------
|
|
269
|
+
Density Estimate : *callable* (x)
|
|
270
|
+
The fitted predictor.
|
|
271
|
+
"""
|
|
272
|
+
dim_of_domain = np.shape(x_train)[1]
|
|
273
|
+
if dim_of_domain == 0:
|
|
274
|
+
return lambda x_dim_zero: np.ones(np.shape(x_dim_zero)[0])
|
|
275
|
+
elif np.shape(x_train)[0] > 0:
|
|
276
|
+
model = self.fit_density(x_train)
|
|
277
|
+
return lambda x_predict: np.exp(model.score_samples(x_predict))
|
|
278
|
+
else:
|
|
279
|
+
return lambda x_predict: np.zeros(np.shape(x_predict)[0])
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class MixedMap:
|
|
283
|
+
"""Helper to evaluate fitted maps on mixed data.
|
|
284
|
+
|
|
285
|
+
Used internally, should not normally be required in user-code.
|
|
286
|
+
"""
|
|
287
|
+
error_value = float('nan')
|
|
288
|
+
|
|
289
|
+
def __init__(self, maps, var_ids, dtype):
|
|
290
|
+
self.maps = maps
|
|
291
|
+
self.var_ids = var_ids
|
|
292
|
+
self.dtype = dtype
|
|
293
|
+
|
|
294
|
+
def predict(self, x):
|
|
295
|
+
"""Given x, predict y."""
|
|
296
|
+
return self(x)
|
|
297
|
+
|
|
298
|
+
def __call__(self, x):
|
|
299
|
+
"""Given x, predict y."""
|
|
300
|
+
continuous_data, categorical_data, category_shape = MixedData.Get_data_via_var_ids(x, self.var_ids)
|
|
301
|
+
category_count = MixedData.CategoryCount(category_shape)
|
|
302
|
+
if category_count == 1: # avoid errors in ravel_multi_index and improve performance by treating separately
|
|
303
|
+
return MixedData.Call_map(self.maps, continuous_data.T)
|
|
304
|
+
else:
|
|
305
|
+
categories = np.ravel_multi_index(categorical_data, category_shape)
|
|
306
|
+
result = np.empty(MixedData.Get_Len(continuous_data, categorical_data), dtype=self.dtype)
|
|
307
|
+
for cX in range(category_count):
|
|
308
|
+
filter_x = (categories == cX)
|
|
309
|
+
if np.count_nonzero(filter_x) > 0:
|
|
310
|
+
# Avoid errors if asking for prediction on data with cX not occurring
|
|
311
|
+
# (Queries must be validated elsewhere)
|
|
312
|
+
filtered_x = continuous_data[:, filter_x]
|
|
313
|
+
if self.maps[cX] is not None:
|
|
314
|
+
result[filter_x] = MixedData.Call_map(self.maps[cX], filtered_x.T)
|
|
315
|
+
else:
|
|
316
|
+
result[filter_x] = self.error_value
|
|
317
|
+
return result
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class MixedMarkovKernel:
|
|
321
|
+
"""Helper to evaluate fitted densities on mixed data.
|
|
322
|
+
|
|
323
|
+
Used internally, should not normally be required in user-code.
|
|
324
|
+
"""
|
|
325
|
+
def __init__(self, transfers, var_ids, dtype, output_category_count):
|
|
326
|
+
self.transfers = transfers
|
|
327
|
+
self.var_ids = var_ids
|
|
328
|
+
self.dtype = dtype
|
|
329
|
+
self.output_category_count = output_category_count
|
|
330
|
+
|
|
331
|
+
def predict(self, x):
|
|
332
|
+
"""Given np.array x, predict probability of each sample."""
|
|
333
|
+
return self(x)
|
|
334
|
+
|
|
335
|
+
def __call__(self, x):
|
|
336
|
+
"""Given np.array x, predict probability of each sample."""
|
|
337
|
+
continuous_data, categorical_data, category_shape = MixedData.Get_data_via_var_ids(x, self.var_ids)
|
|
338
|
+
category_count = MixedData.CategoryCount(category_shape)
|
|
339
|
+
categories = None
|
|
340
|
+
if category_count == 1: # avoid errors in ravel_multi_index and improve performance by treating separately
|
|
341
|
+
categories = np.zeros(np.shape(continuous_data)[1])
|
|
342
|
+
else:
|
|
343
|
+
categories = np.ravel_multi_index(categorical_data, category_shape)
|
|
344
|
+
|
|
345
|
+
values = np.zeros([self.output_category_count, MixedData.Get_Len(continuous_data, categorical_data)])
|
|
346
|
+
for cX in range(category_count):
|
|
347
|
+
filter_x = (categories == cX)
|
|
348
|
+
if np.count_nonzero(filter_x) > 0:
|
|
349
|
+
# Avoid errors if asking for prediction on data with cX not occurring
|
|
350
|
+
# (Queries must be validated elsewhere)
|
|
351
|
+
filtered_x = continuous_data[:, filter_x]
|
|
352
|
+
for cY in range(self.output_category_count):
|
|
353
|
+
transfer = self.transfers[cX][cY]
|
|
354
|
+
values[cY][filter_x] = transfer(filtered_x.T)
|
|
355
|
+
normalize = np.sum(values, axis=0)
|
|
356
|
+
return (values / normalize).T
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
class FitSetup:
|
|
360
|
+
"""Helper to fit mixed data.
|
|
361
|
+
|
|
362
|
+
See "Technical Appendix B" of the Causal Mediation Tutorial.
|
|
363
|
+
"""
|
|
364
|
+
def __init__(self, fit_map=FitProvider_Continuous_Default.UseSklearn(),
|
|
365
|
+
fit_density=FitProvider_Density_Default.UseSklearn()):
|
|
366
|
+
self.fit_map = fit_map
|
|
367
|
+
self.fit_density = fit_density
|
|
368
|
+
|
|
369
|
+
def Fit(self, domain, target, dont_wrap_1d_y_in_vector=True):
|
|
370
|
+
"""Fit a mixed data mapping.
|
|
371
|
+
|
|
372
|
+
See "Technical Appendix B" of the Causal Mediation Tutorial.
|
|
373
|
+
"""
|
|
374
|
+
dont_wrap = dont_wrap_1d_y_in_vector and len(target) == 1
|
|
375
|
+
result = {}
|
|
376
|
+
for y, train in target.items():
|
|
377
|
+
_next = None
|
|
378
|
+
if y.is_categorical:
|
|
379
|
+
_next = self.Fit_CategoricalTarget_MarkovKernel(domain, train, y.categories)
|
|
380
|
+
else:
|
|
381
|
+
_next = self.Fit_ContinuousTarget(domain, train, y.dimension)
|
|
382
|
+
if dont_wrap:
|
|
383
|
+
return _next
|
|
384
|
+
else:
|
|
385
|
+
result[y] = _next
|
|
386
|
+
return result
|
|
387
|
+
|
|
388
|
+
def Fit_ContinuousTarget(self, x, y, dim_y):
|
|
389
|
+
"""Fit a mixed-domain mapping with continuous target."""
|
|
390
|
+
if dim_y == 1:
|
|
391
|
+
return self.Fit_ContinuousTarget_1D(x, y)
|
|
392
|
+
else:
|
|
393
|
+
result = []
|
|
394
|
+
for i in range(dim_y):
|
|
395
|
+
result.append(self.Fit_ContinuousTarget_1D(x, y[:, i]))
|
|
396
|
+
return result
|
|
397
|
+
|
|
398
|
+
def Fit_ContinuousTarget_1D(self, x, y):
|
|
399
|
+
"""Fit a mixed-domain mapping with 1-dimensional continuous target."""
|
|
400
|
+
continuous_data, categorical_data, category_shape = MixedData.Split_data_into_categorical_and_continuous(x)
|
|
401
|
+
|
|
402
|
+
category_count = MixedData.CategoryCount(category_shape)
|
|
403
|
+
if category_count == 1:
|
|
404
|
+
# Avoid errors in np.ravel_multi_index and improve performance by treating this case separately
|
|
405
|
+
_map = self.fit_map.Get_Fit_Continuous(continuous_data.T, y)
|
|
406
|
+
return MixedMap(_map, var_ids=x.keys(), dtype=y.dtype)
|
|
407
|
+
else:
|
|
408
|
+
# Fit once per category
|
|
409
|
+
category_index = np.ravel_multi_index(categorical_data, category_shape)
|
|
410
|
+
maps = {}
|
|
411
|
+
for cX in range(category_count):
|
|
412
|
+
filter_x = (category_index == cX)
|
|
413
|
+
filtered_y = y[filter_x]
|
|
414
|
+
filtered_x = continuous_data[:, filter_x]
|
|
415
|
+
if np.count_nonzero(filter_x) > 1:
|
|
416
|
+
maps[cX] = self.fit_map.Get_Fit_Continuous(filtered_x.T, filtered_y)
|
|
417
|
+
else:
|
|
418
|
+
maps[cX] = None
|
|
419
|
+
return MixedMap(maps, var_ids=x.keys(), dtype=y.dtype)
|
|
420
|
+
|
|
421
|
+
def Fit_CategoricalTarget_MarkovKernel(self, x, y, y_category_count):
|
|
422
|
+
"""Fit a mixed-domain probabilistic mapping (a Markov-kernel) with categorical target."""
|
|
423
|
+
continuous_data, categorical_data, category_shape = MixedData.Split_data_into_categorical_and_continuous(x)
|
|
424
|
+
|
|
425
|
+
category_count = MixedData.CategoryCount(category_shape)
|
|
426
|
+
category_index = None
|
|
427
|
+
if category_count == 1:
|
|
428
|
+
# Avoid errors in np.ravel_multi_index and improve performance by treating this case separately
|
|
429
|
+
category_index = np.zeros(np.shape(continuous_data)[1])
|
|
430
|
+
else:
|
|
431
|
+
category_index = np.ravel_multi_index(categorical_data, category_shape)
|
|
432
|
+
|
|
433
|
+
# Compute the transfer-matrix
|
|
434
|
+
transfer_matrix = self._Fit_Enriched_TransferMatrix(continuous_data,
|
|
435
|
+
category_index, category_count,
|
|
436
|
+
y, y_category_count)
|
|
437
|
+
return MixedMarkovKernel(transfer_matrix, var_ids=x, dtype=y.dtype, output_category_count=y_category_count)
|
|
438
|
+
|
|
439
|
+
@staticmethod
|
|
440
|
+
def _Fit_TransferMatrix(x_categorical, x_category_count, y_categorical, y_category_count):
|
|
441
|
+
transfers = np.zeros([x_category_count, y_category_count])
|
|
442
|
+
for cX in range(x_category_count):
|
|
443
|
+
filter_x = (x_categorical == cX)
|
|
444
|
+
filtered_y = y_categorical[filter_x]
|
|
445
|
+
normalization = np.count_nonzero(filter_x)
|
|
446
|
+
if normalization != 0:
|
|
447
|
+
for cY in range(y_category_count):
|
|
448
|
+
transfers[cX, cY] = np.count_nonzero(filtered_y == cY) / normalization
|
|
449
|
+
else:
|
|
450
|
+
transfers[cX, cY] = MixedMap.error_value
|
|
451
|
+
return transfers
|
|
452
|
+
|
|
453
|
+
def _Fit_Enriched_TransferMatrix(self, x_continuous,
|
|
454
|
+
x_categorical, x_category_count,
|
|
455
|
+
y_categorical, y_category_count):
|
|
456
|
+
transfers = []
|
|
457
|
+
for cX in range(x_category_count):
|
|
458
|
+
filter_x = (x_categorical == cX)
|
|
459
|
+
normalization = np.count_nonzero(filter_x)
|
|
460
|
+
transfers_from_cX = []
|
|
461
|
+
transfers.append(transfers_from_cX)
|
|
462
|
+
if normalization != 0:
|
|
463
|
+
for cY in range(y_category_count):
|
|
464
|
+
# Use Bayes' Theorem to avoid fitting densities conditional on continuous variables
|
|
465
|
+
filter_both = np.logical_and(x_categorical == cX, y_categorical == cY)
|
|
466
|
+
assert len(filter_both.shape) == 1
|
|
467
|
+
p_y_given_discrete_x = np.count_nonzero(filter_both) / normalization
|
|
468
|
+
p_continuous_x_given_discrete_x_and_y = self.fit_density.Get_Fit_Density(
|
|
469
|
+
x_continuous[:, filter_both].T)
|
|
470
|
+
# Avoid weird bugs in lambda-captures combined with for-loop scopes by using "functools->partial"
|
|
471
|
+
transfers_from_cX.append(partial(self._EvalFromBayes, p_y_given_discrete_x=p_y_given_discrete_x,
|
|
472
|
+
p_continuous_x_given_discrete_x_and_y=p_continuous_x_given_discrete_x_and_y))
|
|
473
|
+
# transfers_from_cX.append(lambda x_c: self._EvalFromBayes(x_c, p_y_given_discrete_x,
|
|
474
|
+
# p_continuous_x_given_discrete_x_and_y))
|
|
475
|
+
else:
|
|
476
|
+
for cY in range(y_category_count):
|
|
477
|
+
transfers_from_cX.append(lambda x_c: np.full(np.shape(x_c)[0], MixedMap.error_value))
|
|
478
|
+
return transfers
|
|
479
|
+
|
|
480
|
+
@staticmethod
|
|
481
|
+
def _EvalFromBayes(x_c, p_y_given_discrete_x, p_continuous_x_given_discrete_x_and_y):
|
|
482
|
+
# P( y | x_c, x_d ) = P( x_c | x_d, y ) P( Y | x_d ) / normalization independent of y
|
|
483
|
+
return p_continuous_x_given_discrete_x_and_y(x_c) * p_y_given_discrete_x
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
"""-------------------------------------------------------------------------------------------
|
|
490
|
+
-------------------------------- Natural Effect Estimation -------------------------------
|
|
491
|
+
-------------------------------------------------------------------------------------------"""
|
|
492
|
+
|
|
493
|
+
class NaturalEffects_StandardMediationSetup:
|
|
494
|
+
r"""Setup for estimating natural effects in a "standard" mediation (triangle) graph.
|
|
495
|
+
|
|
496
|
+
Methods for the non-parametric estimation of mediation effects.
|
|
497
|
+
For (more) general graphs, see NaturalEffects_GraphMediation.
|
|
498
|
+
|
|
499
|
+
Actual fit-models can be chosen independently, for details see
|
|
500
|
+
technical appendix B in the mediation-tutorial.
|
|
501
|
+
|
|
502
|
+
See references and tigramite tutorial for an in-depth introduction.
|
|
503
|
+
|
|
504
|
+
References
|
|
505
|
+
----------
|
|
506
|
+
|
|
507
|
+
J. Pearl. Direct and indirect effects. Proceedings of the Seventeenth Conference
|
|
508
|
+
on Uncertainty in Artificial intelligence, 2001.
|
|
509
|
+
|
|
510
|
+
J. Pearl. Interpretation and identification of causal mediation. Psychological
|
|
511
|
+
methods, 19(4):459, 2014.
|
|
512
|
+
|
|
513
|
+
I. Shpitser and T. J. VanderWeele. A complete graphical criterion for the adjust-
|
|
514
|
+
ment formula in mediation analysis. The international journal of biostatistics,
|
|
515
|
+
7(1), 2011.
|
|
516
|
+
|
|
517
|
+
Parameters
|
|
518
|
+
----------
|
|
519
|
+
fit_setup : fit model
|
|
520
|
+
A fit model to use internally, e.g. mixed_fit.FitSetup(). See there or technical
|
|
521
|
+
appendix B of the tutorial.
|
|
522
|
+
source : toy_setup.VariableDescription
|
|
523
|
+
The (description of, e.g. toy_setup.ContinuousVariable()) the effect-source.
|
|
524
|
+
target : toy_setup.VariableDescription
|
|
525
|
+
The (description of, e.g. toy_setup.ContinuousVariable()) the effect-target.
|
|
526
|
+
mediator : toy_setup.VariableDescription
|
|
527
|
+
The (description of, e.g. toy_setup.ContinuousVariable()) the effect-mediator.
|
|
528
|
+
data : dictionary ( keys=toy_setup.VariableDescription, values=np.array(N) )
|
|
529
|
+
The data as map variable-description -> samples; e.g. toy_setup.world.Observables(),
|
|
530
|
+
or toy_setup.VariablesFromDataframe( tigramite dataframe )
|
|
531
|
+
"""
|
|
532
|
+
|
|
533
|
+
def __init__(self, fit_setup, source, target, mediator, data):
|
|
534
|
+
self.fit_setup = fit_setup
|
|
535
|
+
self.X = source
|
|
536
|
+
self.Y = target
|
|
537
|
+
self.M = mediator
|
|
538
|
+
self.x = data[source]
|
|
539
|
+
self.y = data[target]
|
|
540
|
+
self.m = data[mediator]
|
|
541
|
+
X = source
|
|
542
|
+
Y = target
|
|
543
|
+
M = mediator
|
|
544
|
+
x = data[source]
|
|
545
|
+
y = data[target]
|
|
546
|
+
m = data[mediator]
|
|
547
|
+
|
|
548
|
+
self._E_Y_XM = None
|
|
549
|
+
self._P_Y_XM = None
|
|
550
|
+
self._P_M_X = None
|
|
551
|
+
|
|
552
|
+
def E_Y_XM_fixed_x_obs_m(self, x):
|
|
553
|
+
"""[internal] Provide samples Y( fixed x, observed m ) from fit of E[Y|X,M]
|
|
554
|
+
|
|
555
|
+
Parameters
|
|
556
|
+
----------
|
|
557
|
+
x : single value of same type as single sample for X (float, int, or bool)
|
|
558
|
+
The fixed value of X.
|
|
559
|
+
|
|
560
|
+
Returns
|
|
561
|
+
-------
|
|
562
|
+
Y( fixed x, observed m ) : np.array(N)
|
|
563
|
+
Samples of Y estimated from observations and fit.
|
|
564
|
+
"""
|
|
565
|
+
if self._E_Y_XM is None:
|
|
566
|
+
self._E_Y_XM = self.fit_setup.Fit({self.X: self.x, self.M: self.m}, {self.Y: self.y})
|
|
567
|
+
return self._E_Y_XM({self.X: np.full_like(self.x, x), self.M: self.m})
|
|
568
|
+
|
|
569
|
+
def E_Y_XM_fixed_x_all_m(self, x):
|
|
570
|
+
"""For categorical M, provide samples Y( fixed x, m_i ) for all categories m_i of M from fit of E[Y|X,M]
|
|
571
|
+
|
|
572
|
+
Parameters
|
|
573
|
+
----------
|
|
574
|
+
x : single value of same type as single sample for X (float, int, or bool)
|
|
575
|
+
The fixed value of X.
|
|
576
|
+
|
|
577
|
+
Returns
|
|
578
|
+
-------
|
|
579
|
+
Y( fixed x, all m_i ) : np.array(# of categories of M)
|
|
580
|
+
Samples of Y estimated from fit for all categories of the mediator M.
|
|
581
|
+
"""
|
|
582
|
+
assert self.M.is_categorical
|
|
583
|
+
if self._E_Y_XM is None:
|
|
584
|
+
self._E_Y_XM = self.fit_setup.Fit({self.X: self.x, self.M: self.m}, {self.Y: self.y})
|
|
585
|
+
return self._E_Y_XM({self.X: np.full(self.M.categories, x), self.M: np.arange(self.M.categories)})
|
|
586
|
+
|
|
587
|
+
def P_Y_XM_fixed_x_obs_m(self, x):
|
|
588
|
+
"""Provide for all samples of M the likelihood P( Y | fixed x, observed m ) from fit of P[Y|X,M]
|
|
589
|
+
|
|
590
|
+
Parameters
|
|
591
|
+
----------
|
|
592
|
+
x : single value of same type as single sample for X (float, int, or bool)
|
|
593
|
+
The fixed value of X.
|
|
594
|
+
|
|
595
|
+
Returns
|
|
596
|
+
-------
|
|
597
|
+
P( Y | fixed x, observed m ) : np.array(N, # categories of Y)
|
|
598
|
+
Likelihood of each category of Y given x and observations of m from fit.
|
|
599
|
+
"""
|
|
600
|
+
if self._P_Y_XM is None:
|
|
601
|
+
self._P_Y_XM = self.fit_setup.Fit({self.X: self.x, self.M: self.m}, {self.Y: self.y})
|
|
602
|
+
return self._P_Y_XM({self.X: np.full_like(self.x, x), self.M: self.m})
|
|
603
|
+
|
|
604
|
+
def P_M_X(self, x):
|
|
605
|
+
"""Provide the likelihood P( M | fixed x ) from fit of P[M|X]
|
|
606
|
+
|
|
607
|
+
Parameters
|
|
608
|
+
----------
|
|
609
|
+
x : single value of same type as single sample for X (float, int, or bool)
|
|
610
|
+
The fixed value of X.
|
|
611
|
+
|
|
612
|
+
Returns
|
|
613
|
+
-------
|
|
614
|
+
P( M | fixed x ) : np.array( # categories of M )
|
|
615
|
+
Likelihood of each category of M given x and observations of m from fit.
|
|
616
|
+
"""
|
|
617
|
+
if self._P_M_X is None:
|
|
618
|
+
self._P_M_X = self.fit_setup.Fit({self.X: self.x}, {self.M: self.m})
|
|
619
|
+
return self._P_M_X({self.X: x})
|
|
620
|
+
|
|
621
|
+
def _ValidateRequest(self, change_from, change_to):
|
|
622
|
+
"""Sanity-check parameters passed to effect-estimation
|
|
623
|
+
|
|
624
|
+
Parameters
|
|
625
|
+
----------
|
|
626
|
+
cf. NDE, NIE
|
|
627
|
+
|
|
628
|
+
Throws
|
|
629
|
+
------
|
|
630
|
+
Raises and exception if parameters are not meaningful
|
|
631
|
+
"""
|
|
632
|
+
if self.X.is_categorical:
|
|
633
|
+
# both_int = isinstance(change_from, int) and isinstance(change_to, int) does not work with numpy
|
|
634
|
+
both_int = change_from % 1 == 0.0 and change_to % 1 == 0.0
|
|
635
|
+
if not both_int:
|
|
636
|
+
raise Exception("Categorical variables can only be intervened to integer values.")
|
|
637
|
+
if change_from >= self.X.categories or change_to >= self.X.categories or change_from < 0 or change_to < 0:
|
|
638
|
+
raise Exception("Intervention on categorical variable was outside of valid category-range.")
|
|
639
|
+
|
|
640
|
+
def NDE(self, change_from, change_to, fct_of_nde=None):
|
|
641
|
+
"""Compute Natural Direct Effect (NDE)
|
|
642
|
+
|
|
643
|
+
Parameters
|
|
644
|
+
----------
|
|
645
|
+
change_from : single value of same type as single sample for X (float, int, or bool)
|
|
646
|
+
Reference-value to which X is set by intervention in the world seen by the mediator.
|
|
647
|
+
|
|
648
|
+
change_to : single value of same type as single sample for X (float, int, or bool)
|
|
649
|
+
Post-intervention-value to which X is set by intervention in the world seen by the effect (directly).
|
|
650
|
+
|
|
651
|
+
fct_of_nde : *callable* or None
|
|
652
|
+
Also in the case of a continuous Y the distribution, not just the expectation is identified.
|
|
653
|
+
However, a density-fit is not usually reasonable to do in practise. However, instead of
|
|
654
|
+
computing E[Y] can compute E[f(Y)] efficiently for any f. Assume f=id if None.
|
|
655
|
+
|
|
656
|
+
Throws
|
|
657
|
+
------
|
|
658
|
+
Raises and exception if parameters are not meaningful
|
|
659
|
+
|
|
660
|
+
Returns
|
|
661
|
+
-------
|
|
662
|
+
NDE : If Y is categorical -> np.array( # categories Y, 2 )
|
|
663
|
+
The probabilities the categories of Y (after, before) changing the interventional value of X
|
|
664
|
+
as "seen" by Y from change_from to change_to, while keeping M as if X remained at change_from.
|
|
665
|
+
|
|
666
|
+
NDE : If Y is continuous -> float
|
|
667
|
+
The change in the expectation-value of Y induced by changing the interventional value of X
|
|
668
|
+
as "seen" by Y from change_from to change_to, while keeping M as if X remained at change_from.
|
|
669
|
+
"""
|
|
670
|
+
self._ValidateRequest(change_from, change_to)
|
|
671
|
+
if self.Y.is_categorical:
|
|
672
|
+
assert fct_of_nde is None, "Categorical estimate returns full density-estimate anyway."
|
|
673
|
+
return self._NDE_categorical_target(change_from, change_to)
|
|
674
|
+
else:
|
|
675
|
+
return self._NDE_continuous_target(change_from, change_to, fct_of_nde)
|
|
676
|
+
|
|
677
|
+
def NIE(self, change_from, change_to):
|
|
678
|
+
"""Compute Natural Indirect Effect (NIE)
|
|
679
|
+
|
|
680
|
+
Parameters
|
|
681
|
+
----------
|
|
682
|
+
change_from : single value of same type as single sample for X (float, int, or bool)
|
|
683
|
+
Reference-value to which X is set by intervention in the world seen by the effect (directly).
|
|
684
|
+
|
|
685
|
+
change_to : single value of same type as single sample for X (float, int, or bool)
|
|
686
|
+
Post-intervention-value to which X is set by intervention in the world seen by the mediator.
|
|
687
|
+
|
|
688
|
+
Throws
|
|
689
|
+
------
|
|
690
|
+
Raises and exception if parameters are not meaningful
|
|
691
|
+
|
|
692
|
+
Returns
|
|
693
|
+
-------
|
|
694
|
+
NIE : If Y is categorical -> np.array( # categories Y, 2 )
|
|
695
|
+
The probabilities the categories of Y (after, before) changing the interventional value of X
|
|
696
|
+
as "seen" by M from change_from to change_to, while keeping the value as (directly) seen by Y,
|
|
697
|
+
as if X remained at change_from.
|
|
698
|
+
|
|
699
|
+
NIE : If Y is continuous -> float
|
|
700
|
+
The change in the expectation-value of Y induced by changing the interventional value of X
|
|
701
|
+
as "seen" by M from change_from to change_to, while keeping the value as (directly) seen by Y,
|
|
702
|
+
as if X remained at change_from.
|
|
703
|
+
"""
|
|
704
|
+
self._ValidateRequest(change_from, change_to)
|
|
705
|
+
if self.Y.is_categorical:
|
|
706
|
+
return self._NIE_categorical_target(change_from, change_to)
|
|
707
|
+
else:
|
|
708
|
+
return self._NIE_continuous_target(change_from, change_to)
|
|
709
|
+
|
|
710
|
+
def _NDE_continuous_target(self, change_from, change_to, fct_of_nde=None):
|
|
711
|
+
"""Compute NDE (continuous Y)
|
|
712
|
+
|
|
713
|
+
See 'NDE' above.
|
|
714
|
+
|
|
715
|
+
Computed from mediation-formula
|
|
716
|
+
(see [Pearl 2001] or [Shpitser, VanderWeele], see references above)
|
|
717
|
+
by "double"-regression.
|
|
718
|
+
"""
|
|
719
|
+
difference = self.E_Y_XM_fixed_x_obs_m(change_to) - self.E_Y_XM_fixed_x_obs_m(change_from)
|
|
720
|
+
|
|
721
|
+
if fct_of_nde is not None:
|
|
722
|
+
difference = fct_of_nde(difference)
|
|
723
|
+
|
|
724
|
+
# sklearn predicts nan if too far from data ... (but might be irrelevant, check separately)
|
|
725
|
+
valid_samples = np.isfinite(difference)
|
|
726
|
+
Difference = toy_setup.ContinuousVariable()
|
|
727
|
+
X = self.X
|
|
728
|
+
x = self.x
|
|
729
|
+
E_Difference_X = self.fit_setup.Fit({X: x[valid_samples]}, {Difference: difference[valid_samples]})
|
|
730
|
+
return E_Difference_X({X: [change_from]})
|
|
731
|
+
|
|
732
|
+
def _NDE_categorical_target_full_density(self, actual_value, counterfactual_value):
|
|
733
|
+
"""Compute NDE as full density (categorical Y)
|
|
734
|
+
|
|
735
|
+
See 'NDE' above.
|
|
736
|
+
|
|
737
|
+
Computed from mediation-formula
|
|
738
|
+
(see [Pearl 2001] or [Shpitser, VanderWeele], see references above)
|
|
739
|
+
by "double"-regression.
|
|
740
|
+
|
|
741
|
+
Note: According to (see p.13)
|
|
742
|
+
[Shpitser, VanderWeele: A Complete Graphical Criterion for theAdjustment Formula in Mediation Analysis]
|
|
743
|
+
not just the expectation-value, but the full counterfactual distribution can be obtained via mediation-formula.
|
|
744
|
+
"""
|
|
745
|
+
P_Y = toy_setup.ContinuousVariable(dimension=self.Y.categories)
|
|
746
|
+
P_Y_samples = self.P_Y_XM_fixed_x_obs_m(actual_value)
|
|
747
|
+
P_Y_X = self.fit_setup.Fit({self.X: self.x}, {P_Y: P_Y_samples})
|
|
748
|
+
|
|
749
|
+
return MixedData.Call_map(P_Y_X, {self.X: [counterfactual_value]})
|
|
750
|
+
|
|
751
|
+
def _NDE_categorical_target(self, actual_value, counterfactual_value):
|
|
752
|
+
"""Compute NDE by evaluating density (categorical Y)
|
|
753
|
+
|
|
754
|
+
See 'NDE' and '_NDE_categorical_target_full_density' above.
|
|
755
|
+
"""
|
|
756
|
+
# returns (counterfactual probabilities, total effect)
|
|
757
|
+
return np.array([self._NDE_categorical_target_full_density(counterfactual_value, actual_value),
|
|
758
|
+
self._NDE_categorical_target_full_density(actual_value, actual_value)]).T
|
|
759
|
+
|
|
760
|
+
def _NIE_continuous_target(self, change_from, change_to):
|
|
761
|
+
"""Compute NIE (continuous Y)
|
|
762
|
+
|
|
763
|
+
See 'NIE' above.
|
|
764
|
+
|
|
765
|
+
Computed from mediation-formula
|
|
766
|
+
(see [Pearl 2001] or [Shpitser, VanderWeele], see references above)
|
|
767
|
+
by "double"-regression.
|
|
768
|
+
|
|
769
|
+
If M is categorical, after fixing X=x, the fitted P( Y | X=x, M=m ), is actually
|
|
770
|
+
categorical (a transfer matrix), because it takes values only in
|
|
771
|
+
im( P ) = { P( Y | X=x, M=m_0 ), ..., P( Y | X=x, M=m_k ) } where
|
|
772
|
+
m_0, ..., m_k are the categories of M. This is clearly a finite set.
|
|
773
|
+
Since the distribution over this finite subset of the continuous Val(Y)
|
|
774
|
+
is very non-gaussian, "max likelihood by least square estimation" can fail horribly.
|
|
775
|
+
Hence we fit a transfer-matrix instead.
|
|
776
|
+
"""
|
|
777
|
+
X = self.X
|
|
778
|
+
M = self.M
|
|
779
|
+
x = self.x
|
|
780
|
+
m = self.m
|
|
781
|
+
|
|
782
|
+
if M.is_categorical:
|
|
783
|
+
# if the image of the mapping in double-regression is finite, use a density fit instead
|
|
784
|
+
p_M_X01 = self.P_M_X([change_from, change_to])
|
|
785
|
+
E_YX0 = self.E_Y_XM_fixed_x_all_m(change_from)
|
|
786
|
+
# sum over finite M:
|
|
787
|
+
return np.dot(E_YX0, p_M_X01[1] - p_M_X01[0])
|
|
788
|
+
|
|
789
|
+
else:
|
|
790
|
+
Estimate = toy_setup.ContinuousVariable()
|
|
791
|
+
y_at_original_x = self.E_Y_XM_fixed_x_obs_m(change_from)
|
|
792
|
+
ModifiedY = self.fit_setup.Fit({X: x}, {Estimate: y_at_original_x})
|
|
793
|
+
return (MixedData.Call_map(ModifiedY, {X: [change_to]})
|
|
794
|
+
- MixedData.Call_map(ModifiedY, {X: [change_from]}))
|
|
795
|
+
|
|
796
|
+
def _NIE_categorical_target(self, change_from, change_to):
|
|
797
|
+
"""Compute NIE (continuous Y)
|
|
798
|
+
|
|
799
|
+
See 'NIE' above.
|
|
800
|
+
|
|
801
|
+
Computed from mediation-formula
|
|
802
|
+
(see [Pearl 2001] or [Shpitser, VanderWeele], see references above)
|
|
803
|
+
by "double"-regression.
|
|
804
|
+
|
|
805
|
+
Similar to before (see _NIE_continuous_target), treat categorical M differently.
|
|
806
|
+
"""
|
|
807
|
+
X = self.X
|
|
808
|
+
M = self.M
|
|
809
|
+
x = self.x
|
|
810
|
+
m = self.m
|
|
811
|
+
py_at_original_x = self.P_Y_XM_fixed_x_obs_m(change_from)
|
|
812
|
+
|
|
813
|
+
if M.is_categorical:
|
|
814
|
+
# if the image of the mapping in double-regression is finite, use a density fit instead
|
|
815
|
+
p_M_X01 = self.P_M_X([change_from, change_to])
|
|
816
|
+
# sum over finite M:
|
|
817
|
+
result = np.zeros([self.Y.categories, 2]) # 2 is for TE & CF
|
|
818
|
+
for cM in range(M.categories):
|
|
819
|
+
result += np.outer(np.mean(py_at_original_x[m == cM], axis=0), p_M_X01[:, cM])
|
|
820
|
+
return result
|
|
821
|
+
|
|
822
|
+
else:
|
|
823
|
+
Estimate = toy_setup.ContinuousVariable(dimension=self.Y.categories)
|
|
824
|
+
ModifiedY = self.fit_setup.Fit({X: x}, {Estimate: py_at_original_x})
|
|
825
|
+
return np.array([MixedData.Call_map(ModifiedY, {X: [change_to]}),
|
|
826
|
+
MixedData.Call_map(ModifiedY, {X: [change_from]})]).T
|
|
827
|
+
|
|
828
|
+
def NDE_grid(self, list_of_points, cf_delta=0.5, normalize_by_delta=False, fct_of_nde=None):
|
|
829
|
+
"""Compute NDE as grided (unsmoothed) function
|
|
830
|
+
|
|
831
|
+
Parameters
|
|
832
|
+
----------
|
|
833
|
+
list_of_points : np.array( K )
|
|
834
|
+
List of reference-values at which to estimate NDEs
|
|
835
|
+
|
|
836
|
+
cf_delta : float
|
|
837
|
+
The change from reference-value to effect-value (change_from=reference, change_to=ref + delta)
|
|
838
|
+
|
|
839
|
+
normalize_by_delta : bool
|
|
840
|
+
Normalize the effect by dividing by cf_delta.
|
|
841
|
+
|
|
842
|
+
fct_of_nde : *callable* or None
|
|
843
|
+
Also in the case of a continuous Y the distribution, not just the expectation is identified.
|
|
844
|
+
However, a density-fit is not usually reasonable to do in practise. However, instead of
|
|
845
|
+
computing E[Y] can compute E[f(Y)] efficiently for any f. Assume f=id if None.
|
|
846
|
+
|
|
847
|
+
Throws
|
|
848
|
+
------
|
|
849
|
+
Raises and exception if parameters are not meaningful
|
|
850
|
+
|
|
851
|
+
Returns
|
|
852
|
+
-------
|
|
853
|
+
NDE : If Y is categorical -> np.array( K = # grid points, # categories Y, 2 )
|
|
854
|
+
For each grid-point:
|
|
855
|
+
The probabilities the categories of Y (after, before) changing the interventional value of X
|
|
856
|
+
as "seen" by Y from change_from to change_to, while keeping M as if X remained at change_from.
|
|
857
|
+
|
|
858
|
+
NDE : If Y is continuous -> np.array( K = # grid points )
|
|
859
|
+
For each grid-point:
|
|
860
|
+
The change in the expectation-value of Y induced by changing the interventional value of X
|
|
861
|
+
as "seen" by Y from change_from to change_to, while keeping M as if X remained at change_from.
|
|
862
|
+
"""
|
|
863
|
+
return _Fct_on_grid(self.NDE, list_of_points, cf_delta, normalize_by_delta, fct_of_nde=fct_of_nde)
|
|
864
|
+
|
|
865
|
+
def NIE_grid(self, list_of_points, cf_delta=0.5, normalize_by_delta=False):
|
|
866
|
+
"""Compute NIE as grided (unsmoothed) function
|
|
867
|
+
|
|
868
|
+
Parameters
|
|
869
|
+
----------
|
|
870
|
+
list_of_points : np.array( K )
|
|
871
|
+
List of reference-values at which to estimate NIEs
|
|
872
|
+
|
|
873
|
+
cf_delta : float
|
|
874
|
+
The change from reference-value to effect-value (change_from=reference, change_to=ref + delta)
|
|
875
|
+
|
|
876
|
+
normalize_by_delta : bool
|
|
877
|
+
Normalize the effect by dividing by cf_delta.
|
|
878
|
+
|
|
879
|
+
Throws
|
|
880
|
+
------
|
|
881
|
+
Raises and exception if parameters are not meaningful
|
|
882
|
+
|
|
883
|
+
Returns
|
|
884
|
+
-------
|
|
885
|
+
NIE : If Y is categorical -> np.array( K = # grid points, # categories Y, 2 )
|
|
886
|
+
For each grid-point:
|
|
887
|
+
The probabilities the categories of Y (after, before) changing the interventional value of X
|
|
888
|
+
as "seen" by M from change_from to change_to, while keeping the value of X "seen" (directly)
|
|
889
|
+
by Y as if X remained at change_from.
|
|
890
|
+
|
|
891
|
+
NIE : If Y is continuous -> np.array( K = # grid points )
|
|
892
|
+
For each grid-point:
|
|
893
|
+
The change in the expectation-value of Y induced by changing the interventional value of X
|
|
894
|
+
as "seen" by M from change_from to change_to, while keeping the value of X "seen" (directly)
|
|
895
|
+
by Y as if X remained at change_from.
|
|
896
|
+
"""
|
|
897
|
+
return _Fct_on_grid(self.NIE, list_of_points, cf_delta, normalize_by_delta)
|
|
898
|
+
|
|
899
|
+
def NDE_smoothed(self, min_x, max_x, cf_delta=0.5, steps=100, smoothing_gaussian_sigma_in_steps=5,
|
|
900
|
+
normalize_by_delta=False, fct_of_nde=None):
|
|
901
|
+
"""Compute NDE as smoothed function
|
|
902
|
+
|
|
903
|
+
Parameters
|
|
904
|
+
----------
|
|
905
|
+
min_x : float
|
|
906
|
+
Lower bound of interval on which reference-values for X are taken
|
|
907
|
+
|
|
908
|
+
max_x : float
|
|
909
|
+
Upper bound of interval on which reference-values for X are taken
|
|
910
|
+
|
|
911
|
+
cf_delta : float
|
|
912
|
+
The change from reference-value to effect-value (change_from=reference, change_to=ref + delta)
|
|
913
|
+
|
|
914
|
+
steps : uint
|
|
915
|
+
Number of intermediate values to compute in the interval [min_x, max_x]
|
|
916
|
+
|
|
917
|
+
smoothing_gaussian_sigma_in_steps : uint
|
|
918
|
+
The width of the Gauß-kernel used for smoothing, given in steps.
|
|
919
|
+
|
|
920
|
+
normalize_by_delta : bool
|
|
921
|
+
Normalize the effect by dividing by cf_delta.
|
|
922
|
+
|
|
923
|
+
fct_of_nde : *callable* or None
|
|
924
|
+
Also in the case of a continuous Y the distribution, not just the expectation is identified.
|
|
925
|
+
However, a density-fit is not usually reasonable to do in practise. However, instead of
|
|
926
|
+
computing E[Y] can compute E[f(Y)] efficiently for any f. Assume f=id if None.
|
|
927
|
+
|
|
928
|
+
Throws
|
|
929
|
+
------
|
|
930
|
+
Raises and exception if parameters are not meaningful
|
|
931
|
+
|
|
932
|
+
Returns
|
|
933
|
+
-------
|
|
934
|
+
NDE : If Y is categorical -> np.array( # steps, # categories Y, 2 )
|
|
935
|
+
For each grid-point:
|
|
936
|
+
The probabilities the categories of Y (after, before) changing the interventional value of X
|
|
937
|
+
as "seen" by Y from change_from to change_to, while keeping M as if X remained at change_from.
|
|
938
|
+
|
|
939
|
+
NDE : If Y is continuous -> np.array( # steps )
|
|
940
|
+
For each grid-point:
|
|
941
|
+
The change in the expectation-value of Y induced by changing the interventional value of X
|
|
942
|
+
as "seen" by Y from change_from to change_to, while keeping M as if X remained at change_from.
|
|
943
|
+
"""
|
|
944
|
+
return _Fct_smoothed(self.NDE, min_x, max_x, cf_delta, steps, smoothing_gaussian_sigma_in_steps,
|
|
945
|
+
normalize_by_delta, fct_of_nde=fct_of_nde)
|
|
946
|
+
|
|
947
|
+
def NIE_smoothed(self, min_x, max_x, cf_delta=0.5, steps=100, smoothing_gaussian_sigma_in_steps=5,
|
|
948
|
+
normalize_by_delta=False):
|
|
949
|
+
"""Compute NIE as smoothed function
|
|
950
|
+
|
|
951
|
+
Parameters
|
|
952
|
+
----------
|
|
953
|
+
min_x : float
|
|
954
|
+
Lower bound of interval on which reference-values for X are taken
|
|
955
|
+
|
|
956
|
+
max_x : float
|
|
957
|
+
Upper bound of interval on which reference-values for X are taken
|
|
958
|
+
|
|
959
|
+
cf_delta : float
|
|
960
|
+
The change from reference-value to effect-value (change_from=reference, change_to=ref + delta)
|
|
961
|
+
|
|
962
|
+
steps : uint
|
|
963
|
+
Number of intermediate values to compute in the interval [min_x, max_x]
|
|
964
|
+
|
|
965
|
+
smoothing_gaussian_sigma_in_steps : uint
|
|
966
|
+
The width of the Gauß-kernel used for smoothing, given in steps.
|
|
967
|
+
|
|
968
|
+
normalize_by_delta : bool
|
|
969
|
+
Normalize the effect by dividing by cf_delta.
|
|
970
|
+
|
|
971
|
+
Throws
|
|
972
|
+
------
|
|
973
|
+
Raises and exception if parameters are not meaningful
|
|
974
|
+
|
|
975
|
+
Returns
|
|
976
|
+
-------
|
|
977
|
+
NIE : If Y is categorical -> np.array( # steps, # categories Y, 2 )
|
|
978
|
+
For each grid-point:
|
|
979
|
+
The probabilities the categories of Y (after, before) changing the interventional value of X
|
|
980
|
+
as "seen" by M from change_from to change_to, while keeping the value of X "seen" (directly)
|
|
981
|
+
by Y as if X remained at change_from.
|
|
982
|
+
|
|
983
|
+
NDE : If Y is continuous -> np.array( # steps )
|
|
984
|
+
For each grid-point:
|
|
985
|
+
The change in the expectation-value of Y induced by changing the interventional value of X
|
|
986
|
+
as "seen" by M from change_from to change_to, while keeping the value of X "seen" (directly)
|
|
987
|
+
by Y as if X remained at change_from.
|
|
988
|
+
"""
|
|
989
|
+
return _Fct_smoothed(self.NIE, min_x, max_x, cf_delta, steps, smoothing_gaussian_sigma_in_steps,
|
|
990
|
+
normalize_by_delta)
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
class NaturalEffects_GraphMediation:
|
|
994
|
+
r"""Setup for estimating natural effects in a (general) causal graph.
|
|
995
|
+
|
|
996
|
+
Methods for the non-parametric estimation of mediation effects by adjustment.
|
|
997
|
+
|
|
998
|
+
Actual fit-models can be chosen independently, for details see
|
|
999
|
+
technical appendix B in the mediation-tutorial.
|
|
1000
|
+
|
|
1001
|
+
See references and tigramite tutorial for an in-depth introduction.
|
|
1002
|
+
|
|
1003
|
+
References
|
|
1004
|
+
----------
|
|
1005
|
+
|
|
1006
|
+
J. Pearl. Direct and indirect effects. Proceedings of the Seventeenth Conference
|
|
1007
|
+
on Uncertainty in Artificial intelligence, 2001.
|
|
1008
|
+
|
|
1009
|
+
J. Pearl. Interpretation and identification of causal mediation. Psychological
|
|
1010
|
+
methods, 19(4):459, 2014.
|
|
1011
|
+
|
|
1012
|
+
I. Shpitser and T. J. VanderWeele. A complete graphical criterion for the adjust-
|
|
1013
|
+
ment formula in mediation analysis. The international journal of biostatistics,
|
|
1014
|
+
7(1), 2011.
|
|
1015
|
+
|
|
1016
|
+
Parameters
|
|
1017
|
+
----------
|
|
1018
|
+
graph : np.array( [N, N] or [N, N, tau_max+1] depending on graph_type ) of 3-character patterns
|
|
1019
|
+
The causal graph, see 'Causal Effects' tutorial. E.g. returned by causal discovery method
|
|
1020
|
+
(see "Tutorials/Causal Discovery/CD Overview") or by a toymodel (see toy_setup.Model.GetGroundtruthGraph or
|
|
1021
|
+
the Mediation tutorial).
|
|
1022
|
+
graph_type : string
|
|
1023
|
+
The type of graph, tested for 'dag' and 'stationary_dag' (time-series). See 'Causal Effects' tutorial.
|
|
1024
|
+
tau_max : uint
|
|
1025
|
+
Maximum lag to be considered (can be relevant for adjustment sets, passed to 'Causal Effects' class).
|
|
1026
|
+
fit_setup : fit model
|
|
1027
|
+
A fit model to use internally, e.g. mixed_fit.FitSetup(). See there or technical
|
|
1028
|
+
appendix B of the tutorial.
|
|
1029
|
+
observations_data : dictionary ( keys=toy_setup.VariableDescription, values=np.array(N) )
|
|
1030
|
+
The data as map variable-description -> samples; e.g. toy_setup.world.Observables(),
|
|
1031
|
+
or toy_setup.VariablesFromDataframe( tigramite dataframe )
|
|
1032
|
+
effect_source : toy_setup.VariableDescription or (idx, -lag)
|
|
1033
|
+
The (description of, e.g. toy_setup.ContinuousVariable()) the effect-source.
|
|
1034
|
+
effect_target : toy_setup.VariableDescriptionor (idx, -lag)
|
|
1035
|
+
The (description of, e.g. toy_setup.ContinuousVariable()) the effect-target.
|
|
1036
|
+
blocked_mediators : iterable of Variable-descriptions or 'all'
|
|
1037
|
+
Which mediators to 'block' (consider indirect), *un*\ blocked mediators are considered as
|
|
1038
|
+
contributions to the *direct* effect.
|
|
1039
|
+
adjustment_set : iterable of Variable-descriptions or 'auto'
|
|
1040
|
+
Adjustment-set to use. Will be validated if specified explicitly, if 'auto', will try
|
|
1041
|
+
to use an 'optimal' set, fall back to [Perkovic et al]'s adjustment-set (which should always
|
|
1042
|
+
work if single-set adjustment as in [Shpitser, VanderWeele] is possible; this follows
|
|
1043
|
+
from combining results of [Shpitser, VanderWeele] and [Perkovic et al]).
|
|
1044
|
+
See 'Causal Effects' and its tutorial for more info and references on (optimal) adjustment.
|
|
1045
|
+
only_check_validity : bool
|
|
1046
|
+
If True, do not set up an estimator, only check if an optimal adjustment-set exists (or the
|
|
1047
|
+
explicitly specified one is valid). Call this.Valid() to extract the result.
|
|
1048
|
+
fall_back_to_total_effect : bool
|
|
1049
|
+
If True, if no mediators are blocked, use mediation implementation to estimate the total effect.
|
|
1050
|
+
In this case, estimating the total effect through the 'Causal Effects' class might be easier,
|
|
1051
|
+
however, for comparison to other estimates, using this option might yield more consistent results.
|
|
1052
|
+
_internal_provide_cfx : *None* or tigramite.CausalEffects
|
|
1053
|
+
Set to None. Used when called from CausalMediation, which already has a causal-effects class.
|
|
1054
|
+
enable_dataframe_based_preprocessing : bool
|
|
1055
|
+
Enable (and enforce) data-preprocessing through the tigramite::dataframe, makes missing-data
|
|
1056
|
+
and other features available to the mediation analysis. Custom (just in time) handling
|
|
1057
|
+
of missing data might be more sample-efficient.
|
|
1058
|
+
"""
|
|
1059
|
+
|
|
1060
|
+
def __init__(self, graph, graph_type, tau_max, fit_setup, observations_data, effect_source, effect_target,
|
|
1061
|
+
blocked_mediators="all", adjustment_set="auto", only_check_validity=False,
|
|
1062
|
+
fall_back_to_total_effect=False, _internal_provide_cfx=None, enable_dataframe_based_preprocessing=True):
|
|
1063
|
+
|
|
1064
|
+
data = toy_setup.DataHandler(observations_data, dataframe_based_preprocessing=enable_dataframe_based_preprocessing)
|
|
1065
|
+
|
|
1066
|
+
self.Source = data.GetVariableAuto(effect_source, "Source")
|
|
1067
|
+
self.Target = data.GetVariableAuto(effect_target, "Target")
|
|
1068
|
+
|
|
1069
|
+
if blocked_mediators != "all":
|
|
1070
|
+
blocked_mediators = data.GetVariablesAuto(blocked_mediators, "Mediator")
|
|
1071
|
+
if adjustment_set != "auto":
|
|
1072
|
+
adjustment_set = data.GetVariablesAuto(adjustment_set, "Adjustment")
|
|
1073
|
+
|
|
1074
|
+
X = data[self.Source]
|
|
1075
|
+
Y = data[self.Target]
|
|
1076
|
+
|
|
1077
|
+
# Use tigramite's total effect estimation utility to help generate adjustment-sets and mediators
|
|
1078
|
+
cfx_xy = _internal_provide_cfx if _internal_provide_cfx is not None else\
|
|
1079
|
+
CausalEffects(graph, graph_type=graph_type, X=[X], Y=[Y])
|
|
1080
|
+
|
|
1081
|
+
# ----- MEDIATORS -----
|
|
1082
|
+
# Validate "blocked mediators" are actually mediators, or find "all"
|
|
1083
|
+
all_mediators = blocked_mediators == "all"
|
|
1084
|
+
if all_mediators:
|
|
1085
|
+
M = cfx_xy.M
|
|
1086
|
+
blocked_mediators = data.ReverseLookupMulti(M, "Mediator")
|
|
1087
|
+
else:
|
|
1088
|
+
M = data[blocked_mediators]
|
|
1089
|
+
if not set(M) <= set(cfx_xy.M):
|
|
1090
|
+
raise Exception("Blocked mediators, if specified, must actually be mediators, try using"
|
|
1091
|
+
"set-intersection with causal_effects_instance_xy.M instead.")
|
|
1092
|
+
if len(M) == 0 and not fall_back_to_total_effect:
|
|
1093
|
+
raise Exception("There are no mediators, use total-effect estimation instead or set "
|
|
1094
|
+
"fall_back_to_total_effect=True!")
|
|
1095
|
+
|
|
1096
|
+
# ----- ADJUSTMENT -----
|
|
1097
|
+
# Use tigramite's total effect estimation utility to help validate adjustment-sets and mediators
|
|
1098
|
+
cfx_xm = CausalEffects(graph, graph_type=graph_type, X=[X], Y=M)
|
|
1099
|
+
cfx_xm_y = CausalEffects(graph, graph_type=graph_type, X=[X] + list(M), Y=[Y])
|
|
1100
|
+
|
|
1101
|
+
def valid(S):
|
|
1102
|
+
return cfx_xm._check_validity(S) and cfx_xm_y._check_validity(S)
|
|
1103
|
+
|
|
1104
|
+
adjustment_set_auto = (adjustment_set == "auto")
|
|
1105
|
+
|
|
1106
|
+
if adjustment_set_auto:
|
|
1107
|
+
# first try optimal set (for small cf_delta, optimality for the causal effect should imply
|
|
1108
|
+
# optimality for the nde by continuity for the estimator variance)
|
|
1109
|
+
Z = cfx_xy.get_optimal_set()
|
|
1110
|
+
|
|
1111
|
+
if not valid(Z) and adjustment_set_auto:
|
|
1112
|
+
# fall back to adjust, which should work if any single adjustmentset works
|
|
1113
|
+
Z = cfx_xy._get_adjust_set()
|
|
1114
|
+
|
|
1115
|
+
adjustment_set = data.ReverseLookupMulti(Z, "Adjustment")
|
|
1116
|
+
|
|
1117
|
+
else:
|
|
1118
|
+
Z = data[adjustment_set]
|
|
1119
|
+
|
|
1120
|
+
self.valid = valid(Z)
|
|
1121
|
+
if only_check_validity:
|
|
1122
|
+
return
|
|
1123
|
+
|
|
1124
|
+
# Output appropriate error msgs if not valid
|
|
1125
|
+
if not self.valid:
|
|
1126
|
+
if adjustment_set_auto:
|
|
1127
|
+
raise Exception(
|
|
1128
|
+
"The graph-effect you are trying to estimate is not identifiable via one-step adjustment, "
|
|
1129
|
+
"try using a different query "
|
|
1130
|
+
"or refine the causal graph by expert knowledge. "
|
|
1131
|
+
"For the implemented method, there must be an adjustment-set, valid for both X u M -> Y and "
|
|
1132
|
+
"X -> Y. If such a set exists, Perkovic's Adjust(X,Y) is valid, which was tried as "
|
|
1133
|
+
"fallback because adjustment-set='auto' was used.")
|
|
1134
|
+
else:
|
|
1135
|
+
raise Exception(
|
|
1136
|
+
"The adjustment-set specified is not valid for one-step adjustment, "
|
|
1137
|
+
"try using or a different adjustment-set (or set it to 'auto'), a different query "
|
|
1138
|
+
"or refine the causal graph by expert knowledge. "
|
|
1139
|
+
"For the implemented method, there must be an adjustment-set, valid for both X u M -> Y and "
|
|
1140
|
+
"X -> Y. If such a set exists, Perkovic's Adjust(X,Y) is valid, which is tried as "
|
|
1141
|
+
"fallback if adjustment-set='auto' is used.")
|
|
1142
|
+
|
|
1143
|
+
|
|
1144
|
+
# lock in mediators and adjustment for preprocessing
|
|
1145
|
+
self.BlockedMediators = data.ReverseLookupMulti(M)
|
|
1146
|
+
self.AdjustmentSet = data.ReverseLookupMulti(Z)
|
|
1147
|
+
|
|
1148
|
+
# ----- STORE RESULTS ON INSTANCE -----
|
|
1149
|
+
self.X, self.sources = data.Get("Source", [X], tau_max=tau_max)
|
|
1150
|
+
self.X = self.X[0] # currently univariate anyway
|
|
1151
|
+
self.Y, self.targets = data.Get("Target", [Y], tau_max=tau_max)
|
|
1152
|
+
self.Y = self.Y[0]
|
|
1153
|
+
if len(M) > 0:
|
|
1154
|
+
self.M_ids, self.mediators = data.Get("Mediator", M, tau_max=tau_max)
|
|
1155
|
+
else:
|
|
1156
|
+
self.M_ids = None
|
|
1157
|
+
self.mediators = {}
|
|
1158
|
+
|
|
1159
|
+
|
|
1160
|
+
if len(Z) > 0:
|
|
1161
|
+
self.Z_ids, self.adjustment = data.Get("Adjustment", Z, tau_max=tau_max)
|
|
1162
|
+
else:
|
|
1163
|
+
self.Z_ids = None
|
|
1164
|
+
self.adjustment = {}
|
|
1165
|
+
|
|
1166
|
+
|
|
1167
|
+
self.fit_setup = fit_setup
|
|
1168
|
+
self._E_Y_XMZ = None
|
|
1169
|
+
self._P_Y_XMZ = None
|
|
1170
|
+
|
|
1171
|
+
def E_Y_XMZ_fixed_x_obs_mz(self, x):
|
|
1172
|
+
"""Provide samples Y( fixed x, observed m, z ) from fit of E[Y|X,M,Z]
|
|
1173
|
+
|
|
1174
|
+
Parameters
|
|
1175
|
+
----------
|
|
1176
|
+
x : single value of same type as single sample for X (float, int, or bool)
|
|
1177
|
+
The fixed value of X.
|
|
1178
|
+
|
|
1179
|
+
Returns
|
|
1180
|
+
-------
|
|
1181
|
+
Y( fixed x, observed m, z ) : np.array(N)
|
|
1182
|
+
Samples of Y estimated from observations and fit.
|
|
1183
|
+
"""
|
|
1184
|
+
assert not self.Y.is_categorical
|
|
1185
|
+
if self._E_Y_XMZ is None:
|
|
1186
|
+
self._E_Y_XMZ = self.fit_setup.Fit({**self.sources, **self.mediators, **self.adjustment}, self.targets)
|
|
1187
|
+
|
|
1188
|
+
return self._E_Y_XMZ({self.X: np.full_like(self.sources[self.X], x),
|
|
1189
|
+
**self.mediators, **self.adjustment})
|
|
1190
|
+
|
|
1191
|
+
def P_Y_XMZ_fixed_x_obs_mz(self, x):
|
|
1192
|
+
"""Provide for all samples of M, Z the likelihood P( Y | fixed x, observed m, z ) from fit of P[Y|X,M,Z]
|
|
1193
|
+
|
|
1194
|
+
Parameters
|
|
1195
|
+
----------
|
|
1196
|
+
x : single value of same type as single sample for X (float, int, or bool)
|
|
1197
|
+
The fixed value of X.
|
|
1198
|
+
|
|
1199
|
+
Returns
|
|
1200
|
+
-------
|
|
1201
|
+
P( Y | fixed x, observed m, z ) : np.array(N, # categories of Y)
|
|
1202
|
+
Likelihood of each category of Y given x and observations of m, z from fit.
|
|
1203
|
+
"""
|
|
1204
|
+
assert self.Y.is_categorical
|
|
1205
|
+
if self._P_Y_XMZ is None:
|
|
1206
|
+
self._P_Y_XMZ = self.fit_setup.Fit({**self.sources, **self.mediators, **self.adjustment}, self.targets)
|
|
1207
|
+
return self._P_Y_XMZ({self.X: np.full_like(self.sources[self.X], x),
|
|
1208
|
+
**self.mediators, **self.adjustment})
|
|
1209
|
+
|
|
1210
|
+
def Valid(self):
|
|
1211
|
+
"""Get validity of adjustment-set
|
|
1212
|
+
|
|
1213
|
+
Returns
|
|
1214
|
+
-------
|
|
1215
|
+
Valid : bool
|
|
1216
|
+
Validity of adjustment set. (see constructor-parameter 'only_check_validity')
|
|
1217
|
+
"""
|
|
1218
|
+
return self.valid
|
|
1219
|
+
|
|
1220
|
+
def NDE(self, change_from, change_to):
|
|
1221
|
+
"""Compute Natural Direct Effect (NDE)
|
|
1222
|
+
|
|
1223
|
+
Parameters
|
|
1224
|
+
----------
|
|
1225
|
+
change_from : single value of same type as single sample for X (float, int, or bool)
|
|
1226
|
+
Reference-value to which X is set by intervention in the world seen by the (blocked) mediators.
|
|
1227
|
+
|
|
1228
|
+
change_to : single value of same type as single sample for X (float, int, or bool)
|
|
1229
|
+
Post-intervention-value to which X is set by intervention in the world seen by the effect (directly).
|
|
1230
|
+
|
|
1231
|
+
Throws
|
|
1232
|
+
------
|
|
1233
|
+
Raises and exception if parameters are not meaningful or if adjustment-set is not valid.
|
|
1234
|
+
|
|
1235
|
+
Returns
|
|
1236
|
+
-------
|
|
1237
|
+
NDE : If Y is categorical -> np.array( # categories Y, 2 )
|
|
1238
|
+
The probabilities the categories of Y (after, before) changing the interventional value of X
|
|
1239
|
+
as "seen" by Y from change_from to change_to, while keeping (blocked) M as if X remained at change_from.
|
|
1240
|
+
|
|
1241
|
+
NDE : If Y is continuous -> float
|
|
1242
|
+
The change in the expectation-value of Y induced by changing the interventional value of X
|
|
1243
|
+
as "seen" by Y from change_from to change_to, while keeping (blocked) M as if X remained at change_from.
|
|
1244
|
+
"""
|
|
1245
|
+
if not self.valid:
|
|
1246
|
+
raise Exception("Valid adjustment-set is required!")
|
|
1247
|
+
if not (self.X.ValidValue(change_from) and self.X.ValidValue(change_to)):
|
|
1248
|
+
raise Exception("NDE change must be at valid values of the source-variable (e.g. categorical, and within "
|
|
1249
|
+
"the range [0,num-categories).")
|
|
1250
|
+
if self.Y.is_categorical:
|
|
1251
|
+
return np.array([self._NDE_categorical_target_full_density(change_to, change_from),
|
|
1252
|
+
self._NDE_categorical_target_full_density(change_from, change_from)]).T
|
|
1253
|
+
else:
|
|
1254
|
+
return self._NDE_continuous_target(change_from, change_to)
|
|
1255
|
+
|
|
1256
|
+
def _NDE_continuous_target(self, change_from, change_to):
|
|
1257
|
+
"""Compute NDE (continuous Y)
|
|
1258
|
+
|
|
1259
|
+
See 'NDE' above.
|
|
1260
|
+
|
|
1261
|
+
Computed from mediation-formula
|
|
1262
|
+
(see [Pearl 2001] or [Shpitser, VanderWeele], see references above)
|
|
1263
|
+
by "triple"-regression.
|
|
1264
|
+
"""
|
|
1265
|
+
difference = (self.E_Y_XMZ_fixed_x_obs_mz(change_to)
|
|
1266
|
+
- self.E_Y_XMZ_fixed_x_obs_mz(change_from))
|
|
1267
|
+
|
|
1268
|
+
Difference = toy_setup.ContinuousVariable()
|
|
1269
|
+
E_Difference_X = self.fit_setup.Fit({**self.sources, **self.adjustment},
|
|
1270
|
+
{Difference: difference})
|
|
1271
|
+
|
|
1272
|
+
E_NDE_per_c = E_Difference_X({self.X: np.full_like(self.sources[self.X], change_from), **self.adjustment})
|
|
1273
|
+
|
|
1274
|
+
return np.mean(E_NDE_per_c)
|
|
1275
|
+
|
|
1276
|
+
def _NDE_categorical_target_full_density(self, cf_x, reference_x):
|
|
1277
|
+
"""Compute NDE as full density (categorical Y)
|
|
1278
|
+
|
|
1279
|
+
See 'NDE' above.
|
|
1280
|
+
|
|
1281
|
+
Computed from mediation-formula
|
|
1282
|
+
(see [Pearl 2001] or [Shpitser, VanderWeele], see references above)
|
|
1283
|
+
by "double"-regression.
|
|
1284
|
+
|
|
1285
|
+
Note: According to (see p.13)
|
|
1286
|
+
[Shpitser, VanderWeele: A Complete Graphical Criterion for theAdjustment Formula in Mediation Analysis]
|
|
1287
|
+
not just the expectation-value, but the full counterfactual distribution can be obtained via mediation-formula.
|
|
1288
|
+
|
|
1289
|
+
Note: If all M and Z are categorical, after fixing X=x, the fitted P( Y | X=x, M=m ), is actually
|
|
1290
|
+
categorical (a transfer matrix), because it takes values only in
|
|
1291
|
+
im( P ) = { P( Y | X=x, M=m_0 ), ..., P( Y | X=x, M=m_k ) } where
|
|
1292
|
+
m_0, ..., m_k are the categories of M. This is clearly a finite set.
|
|
1293
|
+
Since the distribution over this finite subset of the continuous Val(Y)
|
|
1294
|
+
is very non-gaussian, "max likelihood by least square estimation" can fail horribly.
|
|
1295
|
+
Hence we fit a transfer-matrix instead.
|
|
1296
|
+
"""
|
|
1297
|
+
p_y_values = self.P_Y_XMZ_fixed_x_obs_mz(cf_x)
|
|
1298
|
+
|
|
1299
|
+
if (MixedData.IsPurelyCategorical({**self.mediators, **self.adjustment})
|
|
1300
|
+
and len({**self.mediators, **self.adjustment}) > 0):
|
|
1301
|
+
# If there are mediators or adjustment
|
|
1302
|
+
# and they are purely categorical, then the mapping (M u Z) -> P_Y
|
|
1303
|
+
# has finite image, treating it as categorical gives better results
|
|
1304
|
+
|
|
1305
|
+
# different numpy-versions behave differently wrt this call:
|
|
1306
|
+
# https://numpy.org/devdocs/release/2.0.0-notes.html#np-unique-return-inverse-shape-for-multi-dimensional-inputs
|
|
1307
|
+
# see also https://github.com/numpy/numpy/issues/26738
|
|
1308
|
+
labels_y, transformed_y_numpy_version_dependent = np.unique(p_y_values, return_inverse=True, axis=0)
|
|
1309
|
+
transformed_y = transformed_y_numpy_version_dependent.squeeze()
|
|
1310
|
+
|
|
1311
|
+
P_Y = toy_setup.CategoricalVariable(categories=labels_y)
|
|
1312
|
+
P_P_Y_xz = self.fit_setup.Fit({**self.sources, **self.adjustment}, {P_Y: transformed_y})
|
|
1313
|
+
|
|
1314
|
+
C_NDE_per_c = MixedData.Call_map(P_P_Y_xz,
|
|
1315
|
+
{self.X: np.full_like(self.sources[self.X], reference_x),
|
|
1316
|
+
**self.adjustment})
|
|
1317
|
+
print(labels_y)
|
|
1318
|
+
print(C_NDE_per_c.shape)
|
|
1319
|
+
print(labels_y.shape)
|
|
1320
|
+
P_NDE_per_c = np.matmul(C_NDE_per_c, labels_y)
|
|
1321
|
+
return np.mean(P_NDE_per_c, axis=0) # Axis 1 is p of different categories
|
|
1322
|
+
|
|
1323
|
+
else:
|
|
1324
|
+
P_Y = toy_setup.ContinuousVariable(dimension=self.Y.categories)
|
|
1325
|
+
E_P_Y_xz = self.fit_setup.Fit({**self.sources, **self.adjustment}, {P_Y: p_y_values})
|
|
1326
|
+
|
|
1327
|
+
P_NDE_per_c = MixedData.Call_map(E_P_Y_xz,
|
|
1328
|
+
{self.X: np.full_like(self.sources[self.X], reference_x),
|
|
1329
|
+
**self.adjustment})
|
|
1330
|
+
|
|
1331
|
+
return np.mean(P_NDE_per_c, axis=1) # Axis 0 is p of different categories
|
|
1332
|
+
|
|
1333
|
+
def NDE_smoothed(self, min_x, max_x, cf_delta=0.5, steps=100, smoothing_gaussian_sigma_in_steps=5,
|
|
1334
|
+
normalize_by_delta=False):
|
|
1335
|
+
"""Compute NDE as smoothed function
|
|
1336
|
+
|
|
1337
|
+
Parameters
|
|
1338
|
+
----------
|
|
1339
|
+
min_x : float
|
|
1340
|
+
Lower bound of interval on which reference-values for X are taken
|
|
1341
|
+
|
|
1342
|
+
max_x : float
|
|
1343
|
+
Upper bound of interval on which reference-values for X are taken
|
|
1344
|
+
|
|
1345
|
+
cf_delta : float
|
|
1346
|
+
The change from reference-value to effect-value (change_from=reference, change_to=ref + delta)
|
|
1347
|
+
|
|
1348
|
+
steps : uint
|
|
1349
|
+
Number of intermediate values to compute in the interval [min_x, max_x]
|
|
1350
|
+
|
|
1351
|
+
smoothing_gaussian_sigma_in_steps : uint
|
|
1352
|
+
The width of the Gauß-kernel used for smoothing, given in steps.
|
|
1353
|
+
|
|
1354
|
+
normalize_by_delta : bool
|
|
1355
|
+
Normalize the effect by dividing by cf_delta.
|
|
1356
|
+
|
|
1357
|
+
Throws
|
|
1358
|
+
------
|
|
1359
|
+
Raises and exception if parameters are not meaningful, adjustment-set is not valid or
|
|
1360
|
+
normalization requested makes no sense (normalizing probabilites by delta).
|
|
1361
|
+
|
|
1362
|
+
Returns
|
|
1363
|
+
-------
|
|
1364
|
+
NDE : If Y is categorical -> np.array( # steps, # categories Y, 2 )
|
|
1365
|
+
For each grid-point:
|
|
1366
|
+
The probabilities the categories of Y (after, before) changing the interventional value of X
|
|
1367
|
+
as "seen" by Y from change_from to change_to, while keeping (blocked) M as if X remained at change_from.
|
|
1368
|
+
|
|
1369
|
+
NDE : If Y is continuous -> np.array( # steps )
|
|
1370
|
+
For each grid-point:
|
|
1371
|
+
The change in the expectation-value of Y induced by changing the interventional value of X
|
|
1372
|
+
as "seen" by Y from change_from to change_to, while keeping (blocked) M as if X remained at change_from.
|
|
1373
|
+
"""
|
|
1374
|
+
if self.Y.is_categorical and normalize_by_delta:
|
|
1375
|
+
raise Exception("Do not normalize categorical output-probabilities by delta. (They are probabilities, "
|
|
1376
|
+
"so normalizing them in this way makes no sense.) Normalize the difference instead.")
|
|
1377
|
+
return _Fct_smoothed(self.NDE, min_x, max_x, cf_delta, steps, smoothing_gaussian_sigma_in_steps,
|
|
1378
|
+
normalize_by_delta)
|
|
1379
|
+
|
|
1380
|
+
def PrintInfo(self, detail=1):
|
|
1381
|
+
"""Print info about estimator.
|
|
1382
|
+
|
|
1383
|
+
Helper to quickly print blocked mediators, adjustment-set used and source->target.
|
|
1384
|
+
"""
|
|
1385
|
+
print(f"Estimator for the effect of {self.Source.Info(detail)} on {self.Target.Info(detail)}")
|
|
1386
|
+
self.PrintMediators(detail)
|
|
1387
|
+
self.PrintAdjustmentSet(detail)
|
|
1388
|
+
|
|
1389
|
+
def PrintMediators(self, detail=1):
|
|
1390
|
+
""" Print info about blocked mediators. """
|
|
1391
|
+
print("Blocked Mediators:")
|
|
1392
|
+
for m in self.BlockedMediators:
|
|
1393
|
+
print(" - " + m.Info(detail))
|
|
1394
|
+
|
|
1395
|
+
def PrintAdjustmentSet(self, detail=1):
|
|
1396
|
+
""" Print info about adjustment-set. """
|
|
1397
|
+
print("Adjustment Set:")
|
|
1398
|
+
for z in self.AdjustmentSet:
|
|
1399
|
+
print(" - " + z.Info(detail))
|
|
1400
|
+
|
|
1401
|
+
|
|
1402
|
+
|
|
1403
|
+
|
|
1404
|
+
"""-------------------------------------------------------------------------------------------
|
|
1405
|
+
------------------------------- Expose Tigramite Interface -------------------------------
|
|
1406
|
+
-------------------------------------------------------------------------------------------"""
|
|
1407
|
+
|
|
1408
|
+
|
|
1409
|
+
|
|
1410
|
+
class CausalMediation(CausalEffects):
|
|
1411
|
+
"""Non-linear, non-additive causal mediation analysis.
|
|
1412
|
+
|
|
1413
|
+
See the tutorial on Causal Mediation.
|
|
1414
|
+
|
|
1415
|
+
Extends the tigramite.CausalEffects class by natural-effect estimation for counter-factual mediation analysis.
|
|
1416
|
+
Effects are estimated by adjustment, where adjustment-sets can be generated automatically (if they exist).
|
|
1417
|
+
|
|
1418
|
+
Actual fit-models can be chosen independently, for details see
|
|
1419
|
+
technical appendix B in the mediation-tutorial.
|
|
1420
|
+
|
|
1421
|
+
See references and tigramite tutorial for an in-depth introduction.
|
|
1422
|
+
|
|
1423
|
+
References
|
|
1424
|
+
----------
|
|
1425
|
+
J. Pearl. Direct and indirect effects. Proceedings of the Seventeenth Conference
|
|
1426
|
+
on Uncertainty in Artificial intelligence, 2001.
|
|
1427
|
+
|
|
1428
|
+
J. Pearl. Interpretation and identification of causal mediation. Psychological
|
|
1429
|
+
methods, 19(4):459, 2014.
|
|
1430
|
+
|
|
1431
|
+
I. Shpitser and T. J. VanderWeele. A complete graphical criterion for the adjust-
|
|
1432
|
+
ment formula in mediation analysis. The international journal of biostatistics,
|
|
1433
|
+
7(1), 2011.
|
|
1434
|
+
|
|
1435
|
+
Parameters
|
|
1436
|
+
----------
|
|
1437
|
+
graph : np.array( [N, N] or [N, N, tau_max+1] depending on graph_type ) of 3-character patterns
|
|
1438
|
+
The causal graph, see 'Causal Effects' tutorial. E.g. returned by causal discovery method
|
|
1439
|
+
(see "Tutorials/Causal Discovery/CD Overview") or by a toymodel (see toy_setup.Model.GetGroundtruthGraph or
|
|
1440
|
+
the Mediation tutorial).
|
|
1441
|
+
graph_type : string
|
|
1442
|
+
The type of graph, tested for 'dag' and 'stationary_dag' (time-series). See 'Causal Effects' tutorial.
|
|
1443
|
+
X : (idx, -lag)
|
|
1444
|
+
Index of the effect-source.
|
|
1445
|
+
Y : (idx, -lag)
|
|
1446
|
+
Index of the effect-target.
|
|
1447
|
+
S : None
|
|
1448
|
+
Reserved. Must be None in current version.
|
|
1449
|
+
hidden_variables : None
|
|
1450
|
+
Reserved. Must be None in current version.
|
|
1451
|
+
verbosity : uint
|
|
1452
|
+
Tigramite.CausalEffects verbosity setting.
|
|
1453
|
+
"""
|
|
1454
|
+
def __init__(self, graph, graph_type, X, Y, S=None, hidden_variables=None, verbosity=0):
|
|
1455
|
+
super().__init__(graph, graph_type, X, Y, S, hidden_variables, False, verbosity)
|
|
1456
|
+
self.BlockedMediators = None
|
|
1457
|
+
self.MediationEstimator = None
|
|
1458
|
+
assert hidden_variables is None
|
|
1459
|
+
|
|
1460
|
+
def fit_natural_direct_effect(self, dataframe, mixed_data_estimator=FitSetup(),
|
|
1461
|
+
blocked_mediators='all', adjustment_set='auto',
|
|
1462
|
+
use_mediation_impl_for_total_effect_fallback=False,
|
|
1463
|
+
enable_dataframe_based_preprocessing=True):
|
|
1464
|
+
"""Fit a natural direct effect.
|
|
1465
|
+
|
|
1466
|
+
Parameters
|
|
1467
|
+
----------
|
|
1468
|
+
dataframe : tigramite.Dataframe
|
|
1469
|
+
Observed data.
|
|
1470
|
+
mixed_data_estimator : mixed_fit.FitSetup
|
|
1471
|
+
The fit-configuration to use. See mixed_fit.FitSetup and the Mediation tutorial, Appendix B.
|
|
1472
|
+
blocked_mediators : 'all' or *iterable* of < (idx, -lag) >
|
|
1473
|
+
Which mediators to 'block' (consider indirect), *un*\ blocked mediators are considered as
|
|
1474
|
+
contributions to the *direct* effect.
|
|
1475
|
+
adjustment_set : 'auto' or None or *iterable* < (idx, -lag) >
|
|
1476
|
+
Adjustment-set to use. Will be validated if specified explicitly, if 'auto' or None, will try
|
|
1477
|
+
to use an 'optimal' set, fall back to [Perkovic et al]'s adjustment-set (which should always
|
|
1478
|
+
work if single-set adjustment as in [Shpitser, VanderWeele] os possible; this follows
|
|
1479
|
+
from combining results of [Shpitser, VanderWeele] and [Perkovic et al]).
|
|
1480
|
+
See 'Causal Effects' and its tutorial for more info and references on (optimal) adjustment.
|
|
1481
|
+
use_mediation_impl_for_total_effect_fallback : bool
|
|
1482
|
+
If True, if no mediators are blocked, use mediation implementation to estimate the total effect.
|
|
1483
|
+
In this case, estimating the total effect through the 'Causal Effects' class might be easier,
|
|
1484
|
+
however, for comparison to other estimates, using this option might yield more consistent results.
|
|
1485
|
+
enable_dataframe_based_preprocessing : bool
|
|
1486
|
+
Enable (and enforce) data-preprocessing through the tigramite::dataframe, makes missing-data
|
|
1487
|
+
and other features available to the mediation analysis. Custom (just in time) handling
|
|
1488
|
+
of missing data might be more sample-efficient.
|
|
1489
|
+
|
|
1490
|
+
Returns
|
|
1491
|
+
-------
|
|
1492
|
+
estimator : NaturalEffects_GraphMediation
|
|
1493
|
+
Typically, use predict_natural_direct_effect or predict_natural_direct_effect_function
|
|
1494
|
+
to use the fitted estimator.
|
|
1495
|
+
The internal NaturalEffects_GraphMediation (if needed).
|
|
1496
|
+
"""
|
|
1497
|
+
if adjustment_set is None:
|
|
1498
|
+
adjustment_set = 'auto'
|
|
1499
|
+
self.BlockedMediators = blocked_mediators
|
|
1500
|
+
if len(self.X) != 1:
|
|
1501
|
+
raise NotImplementedError("Currently only implemented for univariate effects (source).")
|
|
1502
|
+
if len(self.Y) != 1:
|
|
1503
|
+
raise NotImplementedError("Currently only implemented for univariate effects (target).")
|
|
1504
|
+
[source] = self.X
|
|
1505
|
+
[target] = self.Y
|
|
1506
|
+
self.MediationEstimator = NaturalEffects_GraphMediation(
|
|
1507
|
+
graph=self.graph, graph_type=self.graph_type, tau_max=self.tau_max,
|
|
1508
|
+
fit_setup=mixed_data_estimator, observations_data=dataframe,
|
|
1509
|
+
effect_source=source, effect_target=target,
|
|
1510
|
+
blocked_mediators=self.BlockedMediators, adjustment_set=adjustment_set, only_check_validity=False,
|
|
1511
|
+
fall_back_to_total_effect=use_mediation_impl_for_total_effect_fallback,
|
|
1512
|
+
_internal_provide_cfx=self, enable_dataframe_based_preprocessing=enable_dataframe_based_preprocessing)
|
|
1513
|
+
# return a NDE_Graph Estimator, but also remember it for predict_nde
|
|
1514
|
+
return self.MediationEstimator
|
|
1515
|
+
|
|
1516
|
+
def predict_natural_direct_effect(self, reference_value_x, cf_intervention_value_x):
|
|
1517
|
+
"""*After fitting* a natural direct effect, predict its value at a specific point.
|
|
1518
|
+
See also predict_natural_direct_effect_function.
|
|
1519
|
+
|
|
1520
|
+
Parameters
|
|
1521
|
+
----------
|
|
1522
|
+
reference_value_x : single value of same type as single sample for X (float, int, or bool)
|
|
1523
|
+
Reference-value to which X is set by intervention in the world seen by the (blocked) mediators.
|
|
1524
|
+
|
|
1525
|
+
cf_intervention_value_x : single value of same type as single sample for X (float, int, or bool)
|
|
1526
|
+
Post-intervention-value to which X is set by intervention in the world seen by the effect (directly).
|
|
1527
|
+
|
|
1528
|
+
Throws
|
|
1529
|
+
------
|
|
1530
|
+
Raises and exception if parameters are not meaningful or if adjustment-set is not valid.
|
|
1531
|
+
|
|
1532
|
+
Returns
|
|
1533
|
+
-------
|
|
1534
|
+
NDE : If Y is categorical -> np.array( # categories Y, 2 )
|
|
1535
|
+
The probabilities the categories of Y (after, before) changing the interventional value of X
|
|
1536
|
+
as "seen" by Y from change_from to change_to, while keeping (blocked) M as if X remained at change_from.
|
|
1537
|
+
|
|
1538
|
+
NDE : If Y is continuous -> float
|
|
1539
|
+
The change in the expectation-value of Y induced by changing the interventional value of X
|
|
1540
|
+
as "seen" by Y from change_from to change_to, while keeping (blocked) M as if X remained at change_from.
|
|
1541
|
+
"""
|
|
1542
|
+
if self.MediationEstimator is None:
|
|
1543
|
+
raise Exception("Call fit_natural_direct_effect_x before using predict_natural_direct_effect.")
|
|
1544
|
+
return self.MediationEstimator.NDE(change_from=reference_value_x, change_to=cf_intervention_value_x)
|
|
1545
|
+
|
|
1546
|
+
def predict_natural_direct_effect_function(self, min_x, max_x, cf_delta=0.5,
|
|
1547
|
+
steps=100, smoothing_gaussian_sigma_in_steps=5,
|
|
1548
|
+
normalize_by_delta=False):
|
|
1549
|
+
"""Compute NDE as *smoothed* function
|
|
1550
|
+
|
|
1551
|
+
Parameters
|
|
1552
|
+
----------
|
|
1553
|
+
min_x : float
|
|
1554
|
+
Lower bound of interval on which reference-values for X are taken
|
|
1555
|
+
|
|
1556
|
+
max_x : float
|
|
1557
|
+
Upper bound of interval on which reference-values for X are taken
|
|
1558
|
+
|
|
1559
|
+
cf_delta : float
|
|
1560
|
+
The change from reference-value to effect-value (change_from=reference, change_to=ref + delta)
|
|
1561
|
+
|
|
1562
|
+
steps : uint
|
|
1563
|
+
Number of intermediate values to compute in the interval [min_x, max_x]
|
|
1564
|
+
|
|
1565
|
+
smoothing_gaussian_sigma_in_steps : uint
|
|
1566
|
+
The width of the Gauß-kernel used for smoothing, given in steps.
|
|
1567
|
+
|
|
1568
|
+
normalize_by_delta : bool
|
|
1569
|
+
Normalize the effect by dividing by cf_delta.
|
|
1570
|
+
|
|
1571
|
+
Throws
|
|
1572
|
+
------
|
|
1573
|
+
Raises and exception if parameters are not meaningful, adjustment-set is not valid or
|
|
1574
|
+
normalization requested makes no sense (normalizing probabilites by delta).
|
|
1575
|
+
|
|
1576
|
+
Returns
|
|
1577
|
+
-------
|
|
1578
|
+
NDE : If Y is categorical -> np.array( # steps, # categories Y, 2 )
|
|
1579
|
+
For each grid-point:
|
|
1580
|
+
The probabilities the categories of Y (after, before) changing the interventional value of X
|
|
1581
|
+
as "seen" by Y from change_from to change_to, while keeping (blocked) M as if X remained at change_from.
|
|
1582
|
+
|
|
1583
|
+
NDE : If Y is continuous -> np.array( # steps )
|
|
1584
|
+
For each grid-point:
|
|
1585
|
+
The change in the expectation-value of Y induced by changing the interventional value of X
|
|
1586
|
+
as "seen" by Y from change_from to change_to, while keeping (blocked) M as if X remained at change_from.
|
|
1587
|
+
"""
|
|
1588
|
+
if self.MediationEstimator is None:
|
|
1589
|
+
raise Exception("Call fit_natural_direct_effect before using predict_natural_direct_effect_x.")
|
|
1590
|
+
return self.MediationEstimator.NDE_smoothed(min_x, max_x, cf_delta,
|
|
1591
|
+
steps, smoothing_gaussian_sigma_in_steps,
|
|
1592
|
+
normalize_by_delta)
|