explainiverse 0.1.1a1__py3-none-any.whl → 0.2.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 (32) hide show
  1. explainiverse/__init__.py +45 -1
  2. explainiverse/adapters/__init__.py +9 -0
  3. explainiverse/adapters/base_adapter.py +25 -25
  4. explainiverse/adapters/sklearn_adapter.py +32 -32
  5. explainiverse/core/__init__.py +22 -0
  6. explainiverse/core/explainer.py +31 -31
  7. explainiverse/core/explanation.py +24 -24
  8. explainiverse/core/registry.py +563 -0
  9. explainiverse/engine/__init__.py +8 -0
  10. explainiverse/engine/suite.py +142 -142
  11. explainiverse/evaluation/__init__.py +8 -0
  12. explainiverse/evaluation/metrics.py +232 -232
  13. explainiverse/explainers/__init__.py +41 -0
  14. explainiverse/explainers/attribution/__init__.py +10 -0
  15. explainiverse/explainers/attribution/lime_wrapper.py +90 -63
  16. explainiverse/explainers/attribution/shap_wrapper.py +89 -66
  17. explainiverse/explainers/attribution/treeshap_wrapper.py +434 -0
  18. explainiverse/explainers/counterfactual/__init__.py +8 -0
  19. explainiverse/explainers/counterfactual/dice_wrapper.py +302 -0
  20. explainiverse/explainers/global_explainers/__init__.py +23 -0
  21. explainiverse/explainers/global_explainers/ale.py +191 -0
  22. explainiverse/explainers/global_explainers/partial_dependence.py +192 -0
  23. explainiverse/explainers/global_explainers/permutation_importance.py +123 -0
  24. explainiverse/explainers/global_explainers/sage.py +164 -0
  25. explainiverse/explainers/rule_based/__init__.py +8 -0
  26. explainiverse/explainers/rule_based/anchors_wrapper.py +350 -0
  27. explainiverse-0.2.1.dist-info/METADATA +264 -0
  28. explainiverse-0.2.1.dist-info/RECORD +30 -0
  29. explainiverse-0.1.1a1.dist-info/METADATA +0 -128
  30. explainiverse-0.1.1a1.dist-info/RECORD +0 -19
  31. {explainiverse-0.1.1a1.dist-info → explainiverse-0.2.1.dist-info}/LICENSE +0 -0
  32. {explainiverse-0.1.1a1.dist-info → explainiverse-0.2.1.dist-info}/WHEEL +0 -0
@@ -0,0 +1,563 @@
1
+ # src/explainiverse/core/registry.py
2
+ """
3
+ ExplainerRegistry - A plugin system for XAI methods.
4
+
5
+ This module provides a flexible registry that allows:
6
+ - Registration of explainers with rich metadata
7
+ - Filtering/discovery by scope, model type, data type, task type
8
+ - Easy instantiation with dependency injection
9
+ - Decorator-based registration for clean syntax
10
+ - Recommendations based on use case
11
+
12
+ Example usage:
13
+ from explainiverse.core.registry import default_registry, ExplainerMeta
14
+
15
+ # List available explainers
16
+ print(default_registry.list_explainers())
17
+
18
+ # Filter by criteria
19
+ local_tabular = default_registry.filter(scope="local", data_type="tabular")
20
+
21
+ # Create an explainer
22
+ explainer = default_registry.create("lime", model=adapter, training_data=X, ...)
23
+
24
+ # Register a custom explainer
25
+ @default_registry.register_decorator(
26
+ name="my_explainer",
27
+ meta=ExplainerMeta(scope="local", description="My custom explainer")
28
+ )
29
+ class MyExplainer(BaseExplainer):
30
+ ...
31
+ """
32
+
33
+ from dataclasses import dataclass, field
34
+ from typing import Dict, List, Optional, Any, Type, Callable
35
+ from explainiverse.core.explainer import BaseExplainer
36
+
37
+
38
+ @dataclass
39
+ class ExplainerMeta:
40
+ """
41
+ Metadata for an explainer, used for discovery and recommendations.
42
+
43
+ Attributes:
44
+ scope: "local" (instance-level) or "global" (model-level)
45
+ model_types: List of compatible model types ("any", "tree", "linear", "neural", "ensemble")
46
+ data_types: List of compatible data types ("tabular", "image", "text", "time_series")
47
+ task_types: List of compatible tasks ("classification", "regression")
48
+ description: Human-readable description of the explainer
49
+ paper_reference: Citation for the original paper
50
+ complexity: Computational complexity (e.g., "O(n)", "O(n^2)")
51
+ requires_training_data: Whether the explainer needs training data
52
+ supports_batching: Whether the explainer can process batches efficiently
53
+ """
54
+ scope: str # "local" or "global"
55
+ model_types: List[str] = field(default_factory=lambda: ["any"])
56
+ data_types: List[str] = field(default_factory=lambda: ["tabular"])
57
+ task_types: List[str] = field(default_factory=lambda: ["classification", "regression"])
58
+ description: str = ""
59
+ paper_reference: Optional[str] = None
60
+ complexity: Optional[str] = None
61
+ requires_training_data: bool = False
62
+ supports_batching: bool = False
63
+
64
+ def matches(
65
+ self,
66
+ scope: Optional[str] = None,
67
+ model_type: Optional[str] = None,
68
+ data_type: Optional[str] = None,
69
+ task_type: Optional[str] = None
70
+ ) -> bool:
71
+ """Check if this metadata matches the given criteria."""
72
+ if scope is not None and self.scope != scope:
73
+ return False
74
+
75
+ if model_type is not None:
76
+ if "any" not in self.model_types and model_type not in self.model_types:
77
+ return False
78
+
79
+ if data_type is not None:
80
+ if data_type not in self.data_types:
81
+ return False
82
+
83
+ if task_type is not None:
84
+ if task_type not in self.task_types:
85
+ return False
86
+
87
+ return True
88
+
89
+
90
+ class ExplainerRegistry:
91
+ """
92
+ Central registry for all explainers in Explainiverse.
93
+
94
+ Provides:
95
+ - Registration (programmatic and decorator-based)
96
+ - Discovery and filtering
97
+ - Instantiation with dependency injection
98
+ - Recommendations based on use case
99
+ """
100
+
101
+ def __init__(self):
102
+ self._registry: Dict[str, Dict[str, Any]] = {}
103
+
104
+ def register(
105
+ self,
106
+ name: str,
107
+ explainer_class: Type[BaseExplainer],
108
+ meta: ExplainerMeta,
109
+ override: bool = False
110
+ ) -> None:
111
+ """
112
+ Register an explainer class with metadata.
113
+
114
+ Args:
115
+ name: Unique identifier for the explainer (e.g., "lime", "shap")
116
+ explainer_class: The explainer class (must inherit from BaseExplainer)
117
+ meta: Metadata describing the explainer's capabilities
118
+ override: If True, allows overwriting existing registration
119
+
120
+ Raises:
121
+ ValueError: If name is already registered and override=False
122
+ """
123
+ if name in self._registry and not override:
124
+ raise ValueError(f"Explainer '{name}' is already registered. Use override=True to replace.")
125
+
126
+ self._registry[name] = {
127
+ "class": explainer_class,
128
+ "meta": meta
129
+ }
130
+
131
+ def unregister(self, name: str) -> None:
132
+ """
133
+ Remove an explainer from the registry.
134
+
135
+ Args:
136
+ name: The explainer name to remove
137
+
138
+ Raises:
139
+ KeyError: If the explainer is not registered
140
+ """
141
+ if name not in self._registry:
142
+ raise KeyError(f"Explainer '{name}' is not registered.")
143
+ del self._registry[name]
144
+
145
+ def get(self, name: str) -> Dict[str, Any]:
146
+ """
147
+ Get the explainer class and metadata by name.
148
+
149
+ Args:
150
+ name: The explainer name
151
+
152
+ Returns:
153
+ Dict with "class" and "meta" keys
154
+
155
+ Raises:
156
+ KeyError: If the explainer is not registered
157
+ """
158
+ if name not in self._registry:
159
+ raise KeyError(f"Explainer '{name}' is not registered. Available: {list(self._registry.keys())}")
160
+ return self._registry[name]
161
+
162
+ def get_meta(self, name: str) -> ExplainerMeta:
163
+ """
164
+ Get just the metadata for an explainer.
165
+
166
+ Args:
167
+ name: The explainer name
168
+
169
+ Returns:
170
+ ExplainerMeta instance
171
+
172
+ Raises:
173
+ KeyError: If the explainer is not registered
174
+ """
175
+ return self.get(name)["meta"]
176
+
177
+ def list_explainers(self, with_meta: bool = False) -> Any:
178
+ """
179
+ List all registered explainers.
180
+
181
+ Args:
182
+ with_meta: If True, return dict with metadata; if False, return list of names
183
+
184
+ Returns:
185
+ List of names or dict of {name: {"class": ..., "meta": ...}}
186
+ """
187
+ if with_meta:
188
+ return dict(self._registry)
189
+ return list(self._registry.keys())
190
+
191
+ def filter(
192
+ self,
193
+ scope: Optional[str] = None,
194
+ model_type: Optional[str] = None,
195
+ data_type: Optional[str] = None,
196
+ task_type: Optional[str] = None
197
+ ) -> List[str]:
198
+ """
199
+ Filter explainers by criteria.
200
+
201
+ Args:
202
+ scope: "local" or "global"
203
+ model_type: "any", "tree", "linear", "neural", "ensemble"
204
+ data_type: "tabular", "image", "text", "time_series"
205
+ task_type: "classification" or "regression"
206
+
207
+ Returns:
208
+ List of matching explainer names
209
+ """
210
+ results = []
211
+ for name, entry in self._registry.items():
212
+ meta: ExplainerMeta = entry["meta"]
213
+ if meta.matches(scope, model_type, data_type, task_type):
214
+ results.append(name)
215
+ return results
216
+
217
+ def create(self, name: str, **kwargs) -> BaseExplainer:
218
+ """
219
+ Instantiate an explainer by name with the given arguments.
220
+
221
+ Args:
222
+ name: The explainer name
223
+ **kwargs: Arguments to pass to the explainer constructor
224
+
225
+ Returns:
226
+ Instantiated explainer
227
+
228
+ Raises:
229
+ KeyError: If the explainer is not registered
230
+ """
231
+ entry = self.get(name)
232
+ explainer_class = entry["class"]
233
+ return explainer_class(**kwargs)
234
+
235
+ def register_decorator(
236
+ self,
237
+ name: str,
238
+ meta: ExplainerMeta
239
+ ) -> Callable[[Type[BaseExplainer]], Type[BaseExplainer]]:
240
+ """
241
+ Decorator for registering an explainer class.
242
+
243
+ Usage:
244
+ @registry.register_decorator(
245
+ name="my_explainer",
246
+ meta=ExplainerMeta(scope="local")
247
+ )
248
+ class MyExplainer(BaseExplainer):
249
+ ...
250
+
251
+ Args:
252
+ name: Unique identifier for the explainer
253
+ meta: Metadata describing the explainer
254
+
255
+ Returns:
256
+ Decorator function that registers the class and returns it unchanged
257
+ """
258
+ def decorator(cls: Type[BaseExplainer]) -> Type[BaseExplainer]:
259
+ self.register(name, cls, meta)
260
+ return cls
261
+ return decorator
262
+
263
+ def summary(self) -> str:
264
+ """
265
+ Generate a human-readable summary of all registered explainers.
266
+
267
+ Returns:
268
+ Formatted string summary
269
+ """
270
+ lines = ["=" * 60, "Explainiverse - Registered Explainers", "=" * 60, ""]
271
+
272
+ # Group by scope
273
+ local = []
274
+ global_ = []
275
+
276
+ for name, entry in self._registry.items():
277
+ meta: ExplainerMeta = entry["meta"]
278
+ info = f" {name}: {meta.description or '(no description)'}"
279
+ if meta.scope == "local":
280
+ local.append(info)
281
+ else:
282
+ global_.append(info)
283
+
284
+ if local:
285
+ lines.append("LOCAL EXPLAINERS (instance-level):")
286
+ lines.extend(local)
287
+ lines.append("")
288
+
289
+ if global_:
290
+ lines.append("GLOBAL EXPLAINERS (model-level):")
291
+ lines.extend(global_)
292
+ lines.append("")
293
+
294
+ lines.append(f"Total: {len(self._registry)} explainers")
295
+ lines.append("=" * 60)
296
+
297
+ return "\n".join(lines)
298
+
299
+ def recommend(
300
+ self,
301
+ model_type: Optional[str] = None,
302
+ data_type: Optional[str] = None,
303
+ task_type: Optional[str] = None,
304
+ scope_preference: Optional[str] = None,
305
+ max_results: int = 5
306
+ ) -> List[str]:
307
+ """
308
+ Recommend explainers based on criteria.
309
+
310
+ This is a smarter version of filter() that ranks results
311
+ by compatibility and preference.
312
+
313
+ Args:
314
+ model_type: The type of model being explained
315
+ data_type: The type of data
316
+ task_type: The ML task type
317
+ scope_preference: Preferred scope ("local" or "global")
318
+ max_results: Maximum number of recommendations
319
+
320
+ Returns:
321
+ List of recommended explainer names, ranked by relevance
322
+ """
323
+ candidates = self.filter(
324
+ model_type=model_type,
325
+ data_type=data_type,
326
+ task_type=task_type
327
+ )
328
+
329
+ # Score candidates
330
+ scored = []
331
+ for name in candidates:
332
+ meta = self.get_meta(name)
333
+ score = 0
334
+
335
+ # Prefer matching scope
336
+ if scope_preference and meta.scope == scope_preference:
337
+ score += 10
338
+
339
+ # Prefer specific model types over "any"
340
+ if model_type and model_type in meta.model_types:
341
+ score += 5
342
+
343
+ # Prefer explainers with documentation
344
+ if meta.description:
345
+ score += 1
346
+ if meta.paper_reference:
347
+ score += 2
348
+
349
+ scored.append((name, score))
350
+
351
+ # Sort by score descending
352
+ scored.sort(key=lambda x: x[1], reverse=True)
353
+
354
+ return [name for name, _ in scored[:max_results]]
355
+
356
+
357
+ # =============================================================================
358
+ # Default Global Registry
359
+ # =============================================================================
360
+
361
+ def _create_default_registry() -> ExplainerRegistry:
362
+ """Create and populate the default global registry."""
363
+ from explainiverse.explainers.attribution.lime_wrapper import LimeExplainer
364
+ from explainiverse.explainers.attribution.shap_wrapper import ShapExplainer
365
+ from explainiverse.explainers.attribution.treeshap_wrapper import TreeShapExplainer
366
+ from explainiverse.explainers.rule_based.anchors_wrapper import AnchorsExplainer
367
+ from explainiverse.explainers.global_explainers.permutation_importance import PermutationImportanceExplainer
368
+ from explainiverse.explainers.global_explainers.partial_dependence import PartialDependenceExplainer
369
+ from explainiverse.explainers.global_explainers.ale import ALEExplainer
370
+ from explainiverse.explainers.global_explainers.sage import SAGEExplainer
371
+ from explainiverse.explainers.counterfactual.dice_wrapper import CounterfactualExplainer
372
+
373
+ registry = ExplainerRegistry()
374
+
375
+ # =========================================================================
376
+ # Local Explainers (instance-level)
377
+ # =========================================================================
378
+
379
+ # Register LIME
380
+ registry.register(
381
+ name="lime",
382
+ explainer_class=LimeExplainer,
383
+ meta=ExplainerMeta(
384
+ scope="local",
385
+ model_types=["any"],
386
+ data_types=["tabular", "text", "image"],
387
+ task_types=["classification", "regression"],
388
+ description="Local Interpretable Model-agnostic Explanations",
389
+ paper_reference="Ribeiro et al., 2016 - 'Why Should I Trust You?'",
390
+ complexity="O(n_samples * n_features)",
391
+ requires_training_data=True,
392
+ supports_batching=False
393
+ )
394
+ )
395
+
396
+ # Register SHAP (KernelSHAP)
397
+ registry.register(
398
+ name="shap",
399
+ explainer_class=ShapExplainer,
400
+ meta=ExplainerMeta(
401
+ scope="local",
402
+ model_types=["any"],
403
+ data_types=["tabular"],
404
+ task_types=["classification", "regression"],
405
+ description="SHapley Additive exPlanations (KernelSHAP)",
406
+ paper_reference="Lundberg & Lee, 2017 - 'A Unified Approach to Interpreting Model Predictions'",
407
+ complexity="O(2^n_features) approximated",
408
+ requires_training_data=True,
409
+ supports_batching=True
410
+ )
411
+ )
412
+
413
+ # Register TreeSHAP (optimized for tree models)
414
+ registry.register(
415
+ name="treeshap",
416
+ explainer_class=TreeShapExplainer,
417
+ meta=ExplainerMeta(
418
+ scope="local",
419
+ model_types=["tree", "ensemble"],
420
+ data_types=["tabular"],
421
+ task_types=["classification", "regression"],
422
+ description="TreeSHAP - exact SHAP values for tree-based models (RandomForest, XGBoost, etc.)",
423
+ paper_reference="Lundberg et al., 2018 - 'Consistent Individualized Feature Attribution for Tree Ensembles'",
424
+ complexity="O(TLD^2) - polynomial in tree depth",
425
+ requires_training_data=False,
426
+ supports_batching=True
427
+ )
428
+ )
429
+
430
+ # Register Anchors
431
+ registry.register(
432
+ name="anchors",
433
+ explainer_class=AnchorsExplainer,
434
+ meta=ExplainerMeta(
435
+ scope="local",
436
+ model_types=["any"],
437
+ data_types=["tabular"],
438
+ task_types=["classification"],
439
+ description="High-precision rule-based explanations using beam search",
440
+ paper_reference="Ribeiro et al., 2018 - 'Anchors: High-Precision Model-Agnostic Explanations' (AAAI)",
441
+ complexity="O(beam_size * n_features * n_samples)",
442
+ requires_training_data=True,
443
+ supports_batching=False
444
+ )
445
+ )
446
+
447
+ # Register Counterfactual (DiCE-style)
448
+ registry.register(
449
+ name="counterfactual",
450
+ explainer_class=CounterfactualExplainer,
451
+ meta=ExplainerMeta(
452
+ scope="local",
453
+ model_types=["any"],
454
+ data_types=["tabular"],
455
+ task_types=["classification"],
456
+ description="Diverse counterfactual explanations via gradient-free optimization",
457
+ paper_reference="Mothilal et al., 2020 - 'Explaining ML Classifiers through Diverse Counterfactual Explanations' (FAT*)",
458
+ complexity="O(n_counterfactuals * optimization_steps)",
459
+ requires_training_data=True,
460
+ supports_batching=False
461
+ )
462
+ )
463
+
464
+ # =========================================================================
465
+ # Global Explainers (model-level)
466
+ # =========================================================================
467
+
468
+ # Register Permutation Importance
469
+ registry.register(
470
+ name="permutation_importance",
471
+ explainer_class=PermutationImportanceExplainer,
472
+ meta=ExplainerMeta(
473
+ scope="global",
474
+ model_types=["any"],
475
+ data_types=["tabular"],
476
+ task_types=["classification", "regression"],
477
+ description="Feature importance via permutation-based performance degradation",
478
+ paper_reference="Breiman, 2001 - 'Random Forests' (Machine Learning)",
479
+ complexity="O(n_features * n_repeats * n_samples)",
480
+ requires_training_data=True,
481
+ supports_batching=False
482
+ )
483
+ )
484
+
485
+ # Register Partial Dependence
486
+ registry.register(
487
+ name="partial_dependence",
488
+ explainer_class=PartialDependenceExplainer,
489
+ meta=ExplainerMeta(
490
+ scope="global",
491
+ model_types=["any"],
492
+ data_types=["tabular"],
493
+ task_types=["classification", "regression"],
494
+ description="Marginal effect of features on predictions (PDP)",
495
+ paper_reference="Friedman, 2001 - 'Greedy Function Approximation' (Annals of Statistics)",
496
+ complexity="O(grid_resolution * n_samples)",
497
+ requires_training_data=True,
498
+ supports_batching=True
499
+ )
500
+ )
501
+
502
+ # Register ALE
503
+ registry.register(
504
+ name="ale",
505
+ explainer_class=ALEExplainer,
506
+ meta=ExplainerMeta(
507
+ scope="global",
508
+ model_types=["any"],
509
+ data_types=["tabular"],
510
+ task_types=["classification", "regression"],
511
+ description="Accumulated Local Effects - unbiased alternative to PDP for correlated features",
512
+ paper_reference="Apley & Zhu, 2020 - 'Visualizing the Effects of Predictor Variables' (JRSS-B)",
513
+ complexity="O(n_bins * n_samples)",
514
+ requires_training_data=True,
515
+ supports_batching=True
516
+ )
517
+ )
518
+
519
+ # Register SAGE
520
+ registry.register(
521
+ name="sage",
522
+ explainer_class=SAGEExplainer,
523
+ meta=ExplainerMeta(
524
+ scope="global",
525
+ model_types=["any"],
526
+ data_types=["tabular"],
527
+ task_types=["classification", "regression"],
528
+ description="Shapley Additive Global importancE - global feature importance via Shapley values",
529
+ paper_reference="Covert et al., 2020 - 'Understanding Global Feature Contributions' (NeurIPS)",
530
+ complexity="O(n_permutations * n_features * n_samples)",
531
+ requires_training_data=True,
532
+ supports_batching=False
533
+ )
534
+ )
535
+
536
+ return registry
537
+
538
+
539
+ # Lazy initialization to avoid circular imports
540
+ _default_registry: Optional[ExplainerRegistry] = None
541
+
542
+
543
+ def get_default_registry() -> ExplainerRegistry:
544
+ """Get the default global registry (lazy initialization)."""
545
+ global _default_registry
546
+ if _default_registry is None:
547
+ _default_registry = _create_default_registry()
548
+ return _default_registry
549
+
550
+
551
+ # For convenience, expose as module-level variable
552
+ # This will be initialized on first access
553
+ class _LazyRegistry:
554
+ """Lazy proxy for the default registry."""
555
+
556
+ def __getattr__(self, name):
557
+ return getattr(get_default_registry(), name)
558
+
559
+ def __contains__(self, item):
560
+ return item in get_default_registry().list_explainers()
561
+
562
+
563
+ default_registry = _LazyRegistry()
@@ -0,0 +1,8 @@
1
+ # src/explainiverse/engine/__init__.py
2
+ """
3
+ Explanation engine - high-level orchestration of explainers.
4
+ """
5
+
6
+ from explainiverse.engine.suite import ExplanationSuite
7
+
8
+ __all__ = ["ExplanationSuite"]