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 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,7 @@
1
+
2
+ from pde.engine import PatternDiscoveryEngine
3
+
4
+ __version__ = "1.0.0"
5
+ __all__ = [
6
+ "PatternDiscoveryEngine"
7
+ ]
@@ -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)