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.
Files changed (38) hide show
  1. tigramite/__init__.py +0 -0
  2. tigramite/causal_effects.py +1525 -0
  3. tigramite/causal_mediation.py +1592 -0
  4. tigramite/data_processing.py +1574 -0
  5. tigramite/graphs.py +1509 -0
  6. tigramite/independence_tests/LBFGS.py +1114 -0
  7. tigramite/independence_tests/__init__.py +0 -0
  8. tigramite/independence_tests/cmiknn.py +661 -0
  9. tigramite/independence_tests/cmiknn_mixed.py +1397 -0
  10. tigramite/independence_tests/cmisymb.py +286 -0
  11. tigramite/independence_tests/gpdc.py +664 -0
  12. tigramite/independence_tests/gpdc_torch.py +820 -0
  13. tigramite/independence_tests/gsquared.py +190 -0
  14. tigramite/independence_tests/independence_tests_base.py +1310 -0
  15. tigramite/independence_tests/oracle_conditional_independence.py +1582 -0
  16. tigramite/independence_tests/pairwise_CI.py +383 -0
  17. tigramite/independence_tests/parcorr.py +369 -0
  18. tigramite/independence_tests/parcorr_mult.py +485 -0
  19. tigramite/independence_tests/parcorr_wls.py +451 -0
  20. tigramite/independence_tests/regressionCI.py +403 -0
  21. tigramite/independence_tests/robust_parcorr.py +403 -0
  22. tigramite/jpcmciplus.py +966 -0
  23. tigramite/lpcmci.py +3649 -0
  24. tigramite/models.py +2257 -0
  25. tigramite/pcmci.py +3935 -0
  26. tigramite/pcmci_base.py +1218 -0
  27. tigramite/plotting.py +4735 -0
  28. tigramite/rpcmci.py +467 -0
  29. tigramite/toymodels/__init__.py +0 -0
  30. tigramite/toymodels/context_model.py +261 -0
  31. tigramite/toymodels/non_additive.py +1231 -0
  32. tigramite/toymodels/structural_causal_processes.py +1201 -0
  33. tigramite/toymodels/surrogate_generator.py +319 -0
  34. tigramite_fast-5.2.10.1.dist-info/METADATA +182 -0
  35. tigramite_fast-5.2.10.1.dist-info/RECORD +38 -0
  36. tigramite_fast-5.2.10.1.dist-info/WHEEL +5 -0
  37. tigramite_fast-5.2.10.1.dist-info/licenses/license.txt +621 -0
  38. 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)