cp-pde 1.0.0__tar.gz
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.
- cp_pde-1.0.0/LICENSE +21 -0
- cp_pde-1.0.0/PKG-INFO +102 -0
- cp_pde-1.0.0/README.md +78 -0
- cp_pde-1.0.0/pyproject.toml +53 -0
- cp_pde-1.0.0/src/pde/__init__.py +7 -0
- cp_pde-1.0.0/src/pde/association/__init__.py +10 -0
- cp_pde-1.0.0/src/pde/association/discretizer.py +157 -0
- cp_pde-1.0.0/src/pde/association/graph.py +200 -0
- cp_pde-1.0.0/src/pde/association/rules.py +170 -0
- cp_pde-1.0.0/src/pde/calibration/__init__.py +8 -0
- cp_pde-1.0.0/src/pde/calibration/conformal.py +142 -0
- cp_pde-1.0.0/src/pde/calibration/selective.py +118 -0
- cp_pde-1.0.0/src/pde/clustering/__init__.py +10 -0
- cp_pde-1.0.0/src/pde/clustering/gower.py +168 -0
- cp_pde-1.0.0/src/pde/clustering/kmedoids.py +174 -0
- cp_pde-1.0.0/src/pde/clustering/knee.py +59 -0
- cp_pde-1.0.0/src/pde/core/__init__.py +8 -0
- cp_pde-1.0.0/src/pde/core/profiler.py +170 -0
- cp_pde-1.0.0/src/pde/core/router.py +42 -0
- cp_pde-1.0.0/src/pde/drift/__init__.py +6 -0
- cp_pde-1.0.0/src/pde/drift/monitor.py +95 -0
- cp_pde-1.0.0/src/pde/engine.py +438 -0
- cp_pde-1.0.0/src/pde/geometry/__init__.py +10 -0
- cp_pde-1.0.0/src/pde/geometry/collective.py +127 -0
- cp_pde-1.0.0/src/pde/geometry/ensemble.py +109 -0
- cp_pde-1.0.0/src/pde/geometry/models.py +179 -0
- cp_pde-1.0.0/src/pde/persistence/__init__.py +6 -0
- cp_pde-1.0.0/src/pde/persistence/serializers.py +96 -0
- cp_pde-1.0.0/src/pde/sequential/__init__.py +6 -0
- cp_pde-1.0.0/src/pde/sequential/markov.py +185 -0
cp_pde-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Carlos Padilla Labella
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
cp_pde-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cp-pde
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Pattern Discovery Engine: A unified library for pattern discovery, anomaly detection, and conformal calibration.
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Author: cpadlab
|
|
8
|
+
Author-email: cpadlab@gmail.com
|
|
9
|
+
Requires-Python: >=3.12,!=3.14.1
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
16
|
+
Requires-Dist: hdbscan (>=0.8.44,<0.9.0)
|
|
17
|
+
Requires-Dist: mlxtend (>=0.25.0,<0.26.0)
|
|
18
|
+
Requires-Dist: networkx (>=3.7,<4.0)
|
|
19
|
+
Requires-Dist: numpy (>=2.5.4,<3.0.0)
|
|
20
|
+
Requires-Dist: pandas (>=3.1.1,<4.0.0)
|
|
21
|
+
Requires-Dist: scikit-learn (>=1.9.1,<2.0.0)
|
|
22
|
+
Requires-Dist: scipy (>=1.18.1,<2.0.0)
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# Pattern Discovery Engine
|
|
26
|
+
|
|
27
|
+
A production-ready Python library for classical machine learning, pattern discovery, and statistical anomaly detection with distribution-free false positive rate control.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Overview
|
|
32
|
+
|
|
33
|
+
**Pattern Discovery Engine (`cp-pde`)** discovers underlying patterns and detects anomalies across diverse data modalities without relying on arbitrary heuristic score thresholds.
|
|
34
|
+
|
|
35
|
+
By integrating **inductive conformal prediction**, `cp-pde` provides rigorous statistical Type I error bounds (False Positive Rate $\le \alpha$) and delivers a structured tripartite decision framework (`anomaly`, `review`, and `no_alert`) to safely manage borderline cases.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Core Capabilities
|
|
40
|
+
|
|
41
|
+
- **Continuous Tabular Data**: Geometric ensemble combining HDBSCAN, Isolation Forest, Local Outlier Factor (LOF), and One-Class SVM with Bonferroni-corrected conformal bounds.
|
|
42
|
+
- **Heterogeneous / Mixed-Type Data**: Leakage-free Gower distance computation paired with robust k-Medoids++ clustering and automated elbow detection (`KneeLocator`).
|
|
43
|
+
- **Sequential & Temporal Dynamics**: Semi-Markovian modeling coupling Laplace-smoothed transition matrices with state-conditional log-normal holding time distributions.
|
|
44
|
+
- **Symbolic Association & Rule Mining**: Quantile-based feature discretization, Apriori rule extraction, contextual violation scoring, and dependency graphs.
|
|
45
|
+
- **Conformal Calibration**: Distribution-free, non-parametric p-value inference with mathematically bounded false positive rates.
|
|
46
|
+
- **Distribution Drift Monitoring**: Real-time Kolmogorov-Smirnov goodness-of-fit testing tracking shifts from uniform p-value distributions.
|
|
47
|
+
- **Lossless Persistence**: Native JSON serialization and restoration preserving exact numerical state across save/load cycles.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
Install the package via `pip`:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install cp-pde
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Requirements
|
|
61
|
+
|
|
62
|
+
* Python `>= 3.12` (excluding `3.14.1`)
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Package Structure
|
|
67
|
+
|
|
68
|
+
| Module | Core Responsibility |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| `pde.geometry` | Continuous geometric anomaly detection suites and Bonferroni ensembles |
|
|
71
|
+
| `pde.clustering` | Gower distance metrics, robust k-Medoids++, and optimal $k$ selection |
|
|
72
|
+
| `pde.sequential` | Semi-Markovian trajectory modeling (state transitions and holding times) |
|
|
73
|
+
| `pde.association` | Discretization, association rule mining, and contextual violation scoring |
|
|
74
|
+
| `pde.calibration` | Non-parametric conformal calibration and selective tripartite classification |
|
|
75
|
+
| `pde.drift` | Streaming Kolmogorov-Smirnov distribution drift monitoring |
|
|
76
|
+
| `pde.persistence` | Full engine serialization and deserialization via JSON |
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Development
|
|
81
|
+
|
|
82
|
+
Clone the repository and install development dependencies using [Poetry](https://python-poetry.org/):
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
git clone [https://github.com/cpadlab/pde.git](https://github.com/cpadlab/pde.git)
|
|
86
|
+
cd pde
|
|
87
|
+
poetry install
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Run test suite and coverage reports:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
poetry run pytest
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
This project is licensed under the MIT License. See the [LICENSE](https://www.google.com/search?q=LICENSE) file for details.
|
cp_pde-1.0.0/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Pattern Discovery Engine
|
|
2
|
+
|
|
3
|
+
A production-ready Python library for classical machine learning, pattern discovery, and statistical anomaly detection with distribution-free false positive rate control.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
**Pattern Discovery Engine (`cp-pde`)** discovers underlying patterns and detects anomalies across diverse data modalities without relying on arbitrary heuristic score thresholds.
|
|
10
|
+
|
|
11
|
+
By integrating **inductive conformal prediction**, `cp-pde` provides rigorous statistical Type I error bounds (False Positive Rate $\le \alpha$) and delivers a structured tripartite decision framework (`anomaly`, `review`, and `no_alert`) to safely manage borderline cases.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Core Capabilities
|
|
16
|
+
|
|
17
|
+
- **Continuous Tabular Data**: Geometric ensemble combining HDBSCAN, Isolation Forest, Local Outlier Factor (LOF), and One-Class SVM with Bonferroni-corrected conformal bounds.
|
|
18
|
+
- **Heterogeneous / Mixed-Type Data**: Leakage-free Gower distance computation paired with robust k-Medoids++ clustering and automated elbow detection (`KneeLocator`).
|
|
19
|
+
- **Sequential & Temporal Dynamics**: Semi-Markovian modeling coupling Laplace-smoothed transition matrices with state-conditional log-normal holding time distributions.
|
|
20
|
+
- **Symbolic Association & Rule Mining**: Quantile-based feature discretization, Apriori rule extraction, contextual violation scoring, and dependency graphs.
|
|
21
|
+
- **Conformal Calibration**: Distribution-free, non-parametric p-value inference with mathematically bounded false positive rates.
|
|
22
|
+
- **Distribution Drift Monitoring**: Real-time Kolmogorov-Smirnov goodness-of-fit testing tracking shifts from uniform p-value distributions.
|
|
23
|
+
- **Lossless Persistence**: Native JSON serialization and restoration preserving exact numerical state across save/load cycles.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
Install the package via `pip`:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install cp-pde
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Requirements
|
|
37
|
+
|
|
38
|
+
* Python `>= 3.12` (excluding `3.14.1`)
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Package Structure
|
|
43
|
+
|
|
44
|
+
| Module | Core Responsibility |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| `pde.geometry` | Continuous geometric anomaly detection suites and Bonferroni ensembles |
|
|
47
|
+
| `pde.clustering` | Gower distance metrics, robust k-Medoids++, and optimal $k$ selection |
|
|
48
|
+
| `pde.sequential` | Semi-Markovian trajectory modeling (state transitions and holding times) |
|
|
49
|
+
| `pde.association` | Discretization, association rule mining, and contextual violation scoring |
|
|
50
|
+
| `pde.calibration` | Non-parametric conformal calibration and selective tripartite classification |
|
|
51
|
+
| `pde.drift` | Streaming Kolmogorov-Smirnov distribution drift monitoring |
|
|
52
|
+
| `pde.persistence` | Full engine serialization and deserialization via JSON |
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Development
|
|
57
|
+
|
|
58
|
+
Clone the repository and install development dependencies using [Poetry](https://python-poetry.org/):
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
git clone [https://github.com/cpadlab/pde.git](https://github.com/cpadlab/pde.git)
|
|
62
|
+
cd pde
|
|
63
|
+
poetry install
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Run test suite and coverage reports:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
poetry run pytest
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
This project is licensed under the MIT License. See the [LICENSE](https://www.google.com/search?q=LICENSE) file for details.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "cp-pde"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Pattern Discovery Engine: A unified library for pattern discovery, anomaly detection, and conformal calibration."
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "cpadlab", email = "cpadlab@gmail.com"}
|
|
7
|
+
]
|
|
8
|
+
license = {text = "MIT"}
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12,!=3.14.1"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"numpy (>=2.5.4,<3.0.0)",
|
|
13
|
+
"scipy (>=1.18.1,<2.0.0)",
|
|
14
|
+
"pandas (>=3.1.1,<4.0.0)",
|
|
15
|
+
"scikit-learn (>=1.9.1,<2.0.0)",
|
|
16
|
+
"hdbscan (>=0.8.44,<0.9.0)",
|
|
17
|
+
"mlxtend (>=0.25.0,<0.26.0)",
|
|
18
|
+
"networkx (>=3.7,<4.0)"
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
23
|
+
build-backend = "poetry.core.masonry.api"
|
|
24
|
+
|
|
25
|
+
[dependency-groups]
|
|
26
|
+
dev = [
|
|
27
|
+
"pytest (>=9.2.0,<10.0.0)",
|
|
28
|
+
"pytest-cov (>=7.1.0,<8.0.0)",
|
|
29
|
+
"pysonar (>=1.11.0.6012,<2.0.0.0)"
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[tool.poetry]
|
|
33
|
+
packages = [{ include = "pde", from = "src" }]
|
|
34
|
+
|
|
35
|
+
[tool.pytest.ini_options]
|
|
36
|
+
minversion = "9.2.0"
|
|
37
|
+
testpaths = ["tests"]
|
|
38
|
+
python_files = ["test_*.py"]
|
|
39
|
+
python_classes = ["Test*"]
|
|
40
|
+
python_functions = ["test_*"]
|
|
41
|
+
addopts = "-v --strict-markers --cov=pde --cov-report=term-missing --cov-report=xml --junitxml=test-results.xml"
|
|
42
|
+
|
|
43
|
+
[tool.coverage.run]
|
|
44
|
+
source = ["src/pde"]
|
|
45
|
+
branch = true
|
|
46
|
+
|
|
47
|
+
[tool.coverage.report]
|
|
48
|
+
show_missing = true
|
|
49
|
+
skip_covered = false
|
|
50
|
+
fail_under = 90
|
|
51
|
+
|
|
52
|
+
[tool.coverage.xml]
|
|
53
|
+
output = "coverage.xml"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
|
|
2
|
+
from pde.association.discretizer import QuantileDiscretizer
|
|
3
|
+
from pde.association.rules import AssociationRuleScorer
|
|
4
|
+
from pde.association.graph import DependencyGraphEngine
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"QuantileDiscretizer",
|
|
8
|
+
"AssociationRuleScorer",
|
|
9
|
+
"DependencyGraphEngine"
|
|
10
|
+
]
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
|
|
2
|
+
from typing import Dict, List, Optional, Sequence
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class QuantileDiscretizer:
|
|
8
|
+
"""
|
|
9
|
+
Discretizer for numerical features fitted on the training dataset.
|
|
10
|
+
|
|
11
|
+
Bins continuous variables into discrete intervals using empirical
|
|
12
|
+
quantiles computed from training data, and transforms inputs into
|
|
13
|
+
one-hot encoded boolean indicator features.
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
n_bins (int): Number of quantile bins to partition the feature space into.
|
|
17
|
+
labels (List[str]): Ordered categorical labels assigned to the bins.
|
|
18
|
+
bin_edges_ (Dict[str, np.ndarray]): Mapping from feature names to their
|
|
19
|
+
calculated bin boundary arrays, bounded by [-inf, inf].
|
|
20
|
+
feature_names_ (List[str]): List of column names selected and fitted
|
|
21
|
+
for discretization.
|
|
22
|
+
is_fitted_ (bool): Flag indicating whether the discretizer has been
|
|
23
|
+
fitted with training data.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def __init__(self, n_bins: int = 3, labels: Optional[Sequence[str]] = None):
|
|
28
|
+
"""
|
|
29
|
+
Initializes the QuantileDiscretizer instance.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
n_bins (int, optional): Number of quantile intervals to form.
|
|
33
|
+
Defaults to 3.
|
|
34
|
+
labels (Optional[Sequence[str]], optional): Category names for each
|
|
35
|
+
bin ordered from lowest to highest value. If None, defaults to
|
|
36
|
+
`["Low", "Medium", "High"]`. Length must match `n_bins`.
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
ValueError: If the number of provided labels does not match `n_bins`.
|
|
40
|
+
"""
|
|
41
|
+
self.n_bins = int(n_bins)
|
|
42
|
+
self.labels = list(labels) if labels else ["Low", "Medium", "High"]
|
|
43
|
+
|
|
44
|
+
if len(self.labels) != self.n_bins:
|
|
45
|
+
raise ValueError(f"The number of labels ({len(self.labels)}) must match n_bins ({self.n_bins}).")
|
|
46
|
+
|
|
47
|
+
self.bin_edges_: Dict[str, np.ndarray] = {}
|
|
48
|
+
self.feature_names_: List[str] = []
|
|
49
|
+
self.is_fitted_: bool = False
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def fit(self, df: pd.DataFrame, features: Optional[Sequence[str]] = None) -> "QuantileDiscretizer":
|
|
53
|
+
"""
|
|
54
|
+
Computes bin cutoffs using empirical quantiles strictly from training data.
|
|
55
|
+
|
|
56
|
+
If quantile boundaries collapse due to heavily concentrated data, falls
|
|
57
|
+
back to linearly spaced thresholds between the column's minimum and
|
|
58
|
+
maximum values. The outer edges are extended to `-inf` and `inf` to
|
|
59
|
+
accommodate out-of-distribution values at inference time.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
df (pd.DataFrame): Training DataFrame used to compute feature
|
|
63
|
+
quantiles.
|
|
64
|
+
features (Optional[Sequence[str]], optional): Specific numeric column
|
|
65
|
+
names to discretize. If None, automatically selects all numeric
|
|
66
|
+
columns in `df`. Defaults to None.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
QuantileDiscretizer: The fitted discretizer instance (`self`).
|
|
70
|
+
"""
|
|
71
|
+
self.feature_names_ = list(features) if features else [c for c in df.columns if pd.api.types.is_numeric_dtype(df[c])]
|
|
72
|
+
self.bin_edges_ = {}
|
|
73
|
+
|
|
74
|
+
quantiles = np.linspace(0, 1, self.n_bins + 1)
|
|
75
|
+
|
|
76
|
+
for col in self.feature_names_:
|
|
77
|
+
edges = np.quantile(df[col].to_numpy(), quantiles)
|
|
78
|
+
edges = np.unique(edges)
|
|
79
|
+
|
|
80
|
+
if len(edges) < self.n_bins + 1:
|
|
81
|
+
edges = np.linspace(df[col].min(), df[col].max(), self.n_bins + 1)
|
|
82
|
+
|
|
83
|
+
edges[0] = -np.inf
|
|
84
|
+
edges[-1] = np.inf
|
|
85
|
+
self.bin_edges_[col] = edges
|
|
86
|
+
|
|
87
|
+
self.is_fitted_ = True
|
|
88
|
+
|
|
89
|
+
return self
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def transform(self, df: pd.DataFrame) -> pd.DataFrame:
|
|
93
|
+
"""
|
|
94
|
+
Transforms continuous features into one-hot encoded boolean indicators.
|
|
95
|
+
|
|
96
|
+
Applies learned bin intervals to partition continuous variables and
|
|
97
|
+
generates binary dummy variables for each bin category.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
df (pd.DataFrame): DataFrame containing the columns to discretize.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
pd.DataFrame: Concatenated DataFrame composed of boolean one-hot
|
|
104
|
+
indicator columns with names formatted as `{column}_{label}`.
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
RuntimeError: If called before fitting the discretizer with `fit`.
|
|
108
|
+
"""
|
|
109
|
+
if not self.is_fitted_:
|
|
110
|
+
raise RuntimeError("You must call .fit() before transforming the data.")
|
|
111
|
+
|
|
112
|
+
one_hot_parts = []
|
|
113
|
+
for col in self.feature_names_:
|
|
114
|
+
binned = pd.cut(df[col], bins=self.bin_edges_[col], labels=self.labels, include_lowest=True)
|
|
115
|
+
dummies = pd.get_dummies(binned, prefix=col, dtype=bool)
|
|
116
|
+
one_hot_parts.append(dummies)
|
|
117
|
+
|
|
118
|
+
return pd.concat(one_hot_parts, axis=1)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def to_dict(self) -> dict:
|
|
122
|
+
"""
|
|
123
|
+
Serializes the discretizer configuration and learned bin edges.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
dict: Dictionary containing `n_bins`, `labels`, `feature_names`,
|
|
127
|
+
`bin_edges` (with values serialized to lists of floats), and
|
|
128
|
+
`is_fitted`.
|
|
129
|
+
"""
|
|
130
|
+
return {
|
|
131
|
+
"n_bins": self.n_bins,
|
|
132
|
+
"labels": self.labels,
|
|
133
|
+
"feature_names": self.feature_names_,
|
|
134
|
+
"bin_edges": {k: v.tolist() for k, v in self.bin_edges_.items()},
|
|
135
|
+
"is_fitted": self.is_fitted_,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@classmethod
|
|
140
|
+
def from_dict(cls, data: dict) -> "QuantileDiscretizer":
|
|
141
|
+
"""
|
|
142
|
+
Reconstructs a QuantileDiscretizer instance from a serialized dictionary.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
data (dict): Dictionary with keys `'n_bins'`, `'labels'`,
|
|
146
|
+
`'feature_names'`, `'bin_edges'`, and `'is_fitted'`.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
QuantileDiscretizer: A restored discretizer instance with loaded
|
|
150
|
+
features and bin boundary thresholds.
|
|
151
|
+
"""
|
|
152
|
+
inst = cls(n_bins=data["n_bins"], labels=data["labels"])
|
|
153
|
+
inst.feature_names_ = data["feature_names"]
|
|
154
|
+
inst.bin_edges_ = {k: np.array(v) for k, v in data["bin_edges"].items()}
|
|
155
|
+
inst.is_fitted_ = bool(data["is_fitted"])
|
|
156
|
+
|
|
157
|
+
return inst
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
|
|
2
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
3
|
+
import networkx as nx
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from scipy.stats import spearmanr
|
|
7
|
+
from sklearn.feature_selection import mutual_info_regression
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DependencyGraphEngine:
|
|
11
|
+
"""
|
|
12
|
+
Maps variable relationship networks and computes topological centrality metrics.
|
|
13
|
+
|
|
14
|
+
Provides static utilities to construct weighted directed association graphs
|
|
15
|
+
from mining rules, derive structural graph centrality measures, and compute
|
|
16
|
+
pairwise continuous dependency metrics including Spearman's rank correlation
|
|
17
|
+
and mutual information.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def _upsert_edge(graph: nx.DiGraph, u: Any, v: Any, lift: float, conf: float, supp: float) -> None:
|
|
23
|
+
"""
|
|
24
|
+
Inserts a directed edge or updates its attributes if a higher lift is observed.
|
|
25
|
+
|
|
26
|
+
Checks whether an edge from node `u` to node `v` already exists in `graph`.
|
|
27
|
+
If present, updates its `'lift'`, `'confidence'`, and `'support'` attributes
|
|
28
|
+
only when the incoming `lift` exceeds the existing edge's lift. If the edge
|
|
29
|
+
does not exist, adds it with the specified metrics.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
graph (nx.DiGraph): Target directed graph to modify in place.
|
|
33
|
+
u (Any): Source node identifier representing the rule antecedent.
|
|
34
|
+
v (Any): Target node identifier representing the rule consequent.
|
|
35
|
+
lift (float): Association rule lift value.
|
|
36
|
+
conf (float): Association rule confidence value.
|
|
37
|
+
supp (float): Association rule support value.
|
|
38
|
+
"""
|
|
39
|
+
if graph.has_edge(u, v):
|
|
40
|
+
if lift > graph[u][v]["lift"]:
|
|
41
|
+
graph[u][v]["lift"] = lift
|
|
42
|
+
graph[u][v]["confidence"] = conf
|
|
43
|
+
graph[u][v]["support"] = supp
|
|
44
|
+
else:
|
|
45
|
+
graph.add_edge(u, v, lift=lift, confidence=conf, support=supp)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def _process_rule_edges(
|
|
50
|
+
cls,
|
|
51
|
+
graph: nx.DiGraph,
|
|
52
|
+
edge_rows: List[Dict[str, Any]],
|
|
53
|
+
rule_series: pd.Series,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Processes antecedent-consequent pairs for a single association rule.
|
|
57
|
+
|
|
58
|
+
Iterates through the Cartesian product of antecedents and consequents
|
|
59
|
+
for a given rule, discarding self-loops (`u == v`), updating the graph
|
|
60
|
+
via `_upsert_edge`, and appending an edge attribute dictionary to
|
|
61
|
+
`edge_rows`.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
graph (nx.DiGraph): Target directed association graph modified in place.
|
|
65
|
+
edge_rows (List[Dict[str, Any]]): Accumulator list of edge dictionary records
|
|
66
|
+
to which each valid directed connection is appended.
|
|
67
|
+
rule_series (pd.Series): Pandas Series containing rule attributes with expected
|
|
68
|
+
keys `'antecedents'`, `'consequents'`, `'lift'`, `'confidence'`, and
|
|
69
|
+
`'support'`.
|
|
70
|
+
"""
|
|
71
|
+
lift = float(rule_series["lift"])
|
|
72
|
+
conf = float(rule_series["confidence"])
|
|
73
|
+
supp = float(rule_series["support"])
|
|
74
|
+
|
|
75
|
+
for u in rule_series["antecedents"]:
|
|
76
|
+
for v in rule_series["consequents"]:
|
|
77
|
+
if u == v:
|
|
78
|
+
continue
|
|
79
|
+
cls._upsert_edge(graph, u, v, lift, conf, supp)
|
|
80
|
+
edge_rows.append({"source": u, "target": v, "lift": lift, "confidence": conf, "support": supp})
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def build_association_graph(cls, rules_df: pd.DataFrame) -> Tuple[nx.DiGraph, pd.DataFrame]:
|
|
85
|
+
"""
|
|
86
|
+
Constructs a directed graph weighted by lift from association rules.
|
|
87
|
+
|
|
88
|
+
Iterates over antecedent-consequent pairs extracted from association rules,
|
|
89
|
+
adding directed edges weighted by lift, confidence, and support. If an edge
|
|
90
|
+
already exists, its attributes are updated if a higher lift value is found.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
rules_df (pd.DataFrame): DataFrame containing association rules. Expected
|
|
94
|
+
columns are `'antecedents'`, `'consequents'`, `'lift'`,
|
|
95
|
+
`'confidence'`, and `'support'`.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Tuple[nx.DiGraph, pd.DataFrame]: A tuple containing:
|
|
99
|
+
- **graph** (`nx.DiGraph`): Directed network graph where nodes represent
|
|
100
|
+
items and edges store association metric attributes.
|
|
101
|
+
- **edges_df** (`pd.DataFrame`): DataFrame containing deduplicated edge
|
|
102
|
+
records with columns `'source'`, `'target'`, `'lift'`,
|
|
103
|
+
`'confidence'`, and `'support'`.
|
|
104
|
+
"""
|
|
105
|
+
graph = nx.DiGraph()
|
|
106
|
+
edge_rows: List[Dict[str, Any]] = []
|
|
107
|
+
|
|
108
|
+
if rules_df.empty:
|
|
109
|
+
return graph, pd.DataFrame()
|
|
110
|
+
|
|
111
|
+
for _, r in rules_df.iterrows():
|
|
112
|
+
cls._process_rule_edges(graph, edge_rows, r)
|
|
113
|
+
|
|
114
|
+
edges_df = pd.DataFrame(edge_rows).drop_duplicates(subset=["source", "target"])
|
|
115
|
+
|
|
116
|
+
return graph, edges_df
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def compute_centrality_metrics(graph: nx.DiGraph) -> pd.DataFrame:
|
|
121
|
+
"""
|
|
122
|
+
Computes network centrality metrics for nodes in a directed graph.
|
|
123
|
+
|
|
124
|
+
Evaluates weighted in-degree, weighted out-degree, betweenness centrality,
|
|
125
|
+
and closeness centrality for each node in the network.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
graph (nx.DiGraph): Directed association graph with weighted edges.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
pd.DataFrame: DataFrame sorted by `'betweenness_centrality'` in
|
|
132
|
+
descending order, containing the following columns:
|
|
133
|
+
- `'node'`: Identifier of the node.
|
|
134
|
+
- `'in_degree_weighted'`: Sum of incoming edge lift weights.
|
|
135
|
+
- `'out_degree_weighted'`: Sum of outgoing edge lift weights.
|
|
136
|
+
- `'betweenness_centrality'`: Normalized betweenness centrality score.
|
|
137
|
+
- `'closeness_centrality'`: Closeness centrality score.
|
|
138
|
+
Returns an empty DataFrame if `graph` has no nodes.
|
|
139
|
+
"""
|
|
140
|
+
if graph.number_of_nodes() == 0:
|
|
141
|
+
return pd.DataFrame()
|
|
142
|
+
|
|
143
|
+
in_deg = dict(graph.in_degree(weight="lift"))
|
|
144
|
+
out_deg = dict(graph.out_degree(weight="lift"))
|
|
145
|
+
betweenness = nx.betweenness_centrality(graph, weight="lift", normalized=True)
|
|
146
|
+
closeness = nx.closeness_centrality(graph)
|
|
147
|
+
|
|
148
|
+
rows = []
|
|
149
|
+
|
|
150
|
+
for node in graph.nodes():
|
|
151
|
+
rows.append({
|
|
152
|
+
"node": node,
|
|
153
|
+
"in_degree_weighted": float(in_deg.get(node, 0.0)),
|
|
154
|
+
"out_degree_weighted": float(out_deg.get(node, 0.0)),
|
|
155
|
+
"betweenness_centrality": float(betweenness.get(node, 0.0)),
|
|
156
|
+
"closeness_centrality": float(closeness.get(node, 0.0)),
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
return pd.DataFrame(rows).sort_values(by="betweenness_centrality", ascending=False)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@staticmethod
|
|
163
|
+
def compute_continuous_dependencies(df: pd.DataFrame, features: List[str], seed: int = 42) -> pd.DataFrame:
|
|
164
|
+
"""
|
|
165
|
+
Computes pairwise continuous multivariate dependencies between features.
|
|
166
|
+
|
|
167
|
+
Calculates Spearman's rank correlation coefficient (rho) and non-linear
|
|
168
|
+
mutual information for all unique feature pairs.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
df (pd.DataFrame): Dataset containing continuous numerical variables.
|
|
172
|
+
features (List[str]): List of column names to evaluate pairwise.
|
|
173
|
+
seed (int, optional): Random seed used for mutual information
|
|
174
|
+
regression estimation. Defaults to 42.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
pd.DataFrame: DataFrame containing pairwise dependency measures with columns:
|
|
178
|
+
- `'var_x'`: First variable name.
|
|
179
|
+
- `'var_y'`: Second variable name.
|
|
180
|
+
- `'spearman_rho'`: Monotonic correlation coefficient in [-1.0, 1.0].
|
|
181
|
+
- `'mutual_information'`: Estimated non-negative mutual information score.
|
|
182
|
+
"""
|
|
183
|
+
rows = []
|
|
184
|
+
|
|
185
|
+
for i, col1 in enumerate(features):
|
|
186
|
+
for j, col2 in enumerate(features):
|
|
187
|
+
if i >= j:
|
|
188
|
+
continue
|
|
189
|
+
|
|
190
|
+
rho, _ = spearmanr(df[col1], df[col2])
|
|
191
|
+
mi = mutual_info_regression(df[[col1]], df[col2], random_state=seed, n_neighbors=5)[0]
|
|
192
|
+
|
|
193
|
+
rows.append({
|
|
194
|
+
"var_x": col1,
|
|
195
|
+
"var_y": col2,
|
|
196
|
+
"spearman_rho": float(rho),
|
|
197
|
+
"mutual_information": float(mi),
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
return pd.DataFrame(rows)
|