# Copyright 2026 Srikumar Krishnamoorthy
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""HUGIMLClassifier — C++ accelerated, scikit-learn compatible classifier.
``HUGIMLClassifier`` is the primary public class name.
``HUGIMLClassifierNative`` remains as a backward-compatible alias.
Implements the High Utility Gain Interpretable Machine Learning (HUG-IML)
algorithm from:
Krishnamoorthy, S. (2024). Interpretable Classifier Models for Decision
Support Using High Utility Gain Patterns. IEEE Access, 12, 126088–126107.
DOI: 10.1109/ACCESS.2024.3455563
Computationally intensive stages (discretisation, transaction construction,
pattern mining, matrix assembly) run at native speed via a compiled C++
extension with optional OpenMP parallelism. The Python layer handles
DataFrame ingestion, column-type detection, downstream estimation,
explanation methods, monitoring, and drift detection.
Architecture
------------
C++ extension (_hugiml_core):
Discretisation, transaction construction, top-K HUI pattern mining with
information-gain filtering, bitmap-accelerated matrix assembly, OpenMP
parallel pattern matching.
Python layer:
Column-type detection (prepareXy), NaN/Inf imputation, downstream sklearn
estimator (LogisticRegression default, with optional saga/SGD downstream solvers), explanation methods
(get_hug_features, get_pattern_info, feature_importances), versioned
model serialisation, prediction monitoring, multi-method drift detection,
latency SLA enforcement, and graceful degradation under memory pressure.
Quick start
-----------
Two usage paths are supported:
**Path A — prepareXy** (recommended when the full dataset is available upfront)::
from hugiml import HUGIMLClassifier
clf = HUGIMLClassifier()
X, y = clf.prepareXy(X_df, y_series)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, stratify=y)
clf.fit(X_tr, y_tr)
proba = clf.predict_proba(X_te)
print(clf.model_summary())
print(clf.feature_importances())
**Path B — allCols + origColumns** (cross-validation loops)::
clf = HUGIMLClassifier(
allCols=[int_cols, float_cols, cat_cols],
origColumns=X_df.columns.tolist(),
)
clf.fit(X_train, y_train)
Monitoring and drift detection::
clf.enable_monitoring()
clf.predict_proba(X_new)
print(clf.monitor.report())
drift = clf.detect_drift(X_new)
print(drift)
Versioned serialisation::
clf.save_model("model.hugiml")
clf2 = HUGIMLClassifier.load_model("model.hugiml")
"""
from __future__ import annotations
import threading
import warnings
from typing import Any
from sklearn.base import BaseEstimator, ClassifierMixin, TransformerMixin
from hugiml import _binning as _binning
from hugiml import _compat
from hugiml import monitoring as _monitoring
from hugiml import serialization as _serialization
try:
import _hugiml_core as _core
_CORE_AVAILABLE: bool = True
_CORE_IMPORT_ERROR: ImportError | None = None
except ImportError as _e:
_core = None
_CORE_AVAILABLE = False
_CORE_IMPORT_ERROR: ImportError | None = _e
warnings.warn(
f"hugiml-core: the compiled C++ extension '_hugiml_core' could not be imported "
f"({_e}). "
f"All classifier fit/transform operations will raise ImportError. "
f"On Linux x86_64 with glibc >= 2.17, a pre-built wheel should have been installed "
f"automatically — verify your platform matches a published wheel at "
f"https://pypi.org/project/hugiml-core/#files. "
f"To build from source: pip install . --no-build-isolation",
RuntimeWarning,
stacklevel=2,
)
from hugiml import _classifier_support as _support
from hugiml import _classifier_tuning as _tuning
from hugiml import exceptions as _exceptions
from hugiml._classifier_binning import _BinningMixin
from hugiml._classifier_estimator import _EstimatorMixin
from hugiml._classifier_features import _FeatureAssemblyMixin
from hugiml._classifier_inspection import _InspectionMixin
from hugiml._classifier_interpretation import _InterpretationMixin
from hugiml._classifier_prediction import _PredictionMixin
from hugiml._classifier_training import _TrainingMixin
from hugiml.hyperparameter_configs import DEFAULT_HUGIML_GRID_NAME, get_hugiml_grid
_adap_apply_edges = _binning._apply_edges
_adap_quantile_edges = _binning._quantile_edges
_adap_select_b = _binning._select_b
check_array = _compat.check_array
check_X_y = _compat.check_X_y
liblinear_penalty_kwargs = _compat.liblinear_penalty_kwargs
MIN_SCHEMA_VERSION = _serialization.MIN_SCHEMA_VERSION
MODEL_SCHEMA_VERSION = _serialization.MODEL_SCHEMA_VERSION
_load_model = _serialization.load_model
_save_model = _serialization.save_model
DriftDetector = _monitoring.DriftDetector
PredictionMonitor = _monitoring.PredictionMonitor
HUGIMLConvergenceWarning = _exceptions.HUGIMLConvergenceWarning
HUGIMLDtypeDriftWarning = _exceptions.HUGIMLDtypeDriftWarning
HUGIMLMemoryError = _exceptions.HUGIMLMemoryError
HUGIMLParamError = _exceptions.HUGIMLParamError
HUGIMLPredictionError = _exceptions.HUGIMLPredictionError
HUGIMLRangeWarning = _exceptions.HUGIMLRangeWarning
HUGIMLSchemaError = _exceptions.HUGIMLSchemaError
HUGIMLTimeoutError = _exceptions.HUGIMLTimeoutError
HUGIMLValidationError = _exceptions.HUGIMLValidationError
HUGIMLVersionError = _exceptions.HUGIMLVersionError
HUGIMLWarning = _exceptions.HUGIMLWarning
DEFAULT_AUGMENTED_PAIR_MAX_FEATURES = _support.DEFAULT_AUGMENTED_PAIR_MAX_FEATURES
DEFAULT_AUGMENTED_PAIR_UNBOUNDED_CAP = _support.DEFAULT_AUGMENTED_PAIR_UNBOUNDED_CAP
AUGMENTED_PAIR_OPS = _support.AUGMENTED_PAIR_OPS
AUGMENTED_PAIR_MODES = _support.AUGMENTED_PAIR_MODES
_II_N_BINS = _support._II_N_BINS
_NUMERIC_DTYPE_KINDS = _support._NUMERIC_DTYPE_KINDS
_PRESETS = _support._PRESETS
_tracemalloc_lock = _support._tracemalloc_lock
_resource = getattr(_support, "_resource", None)
_is_zero_variance_numeric_column = _support._is_zero_variance_numeric_column
FitMetadata = _support.FitMetadata
NativeAugmentedPairTransformBlock = _support.NativeAugmentedPairTransformBlock
_MemoryTracker = _support._MemoryTracker
_TransactionDataWrapper = _support._TransactionDataWrapper
_best_ig_score = _support._best_ig_score
_codes_from_edges = _support._codes_from_edges
_continuous_to_quantile_codes = _support._continuous_to_quantile_codes
_dense_full_csr = _support._dense_full_csr
_edge_information_gain = _support._edge_information_gain
_entropy_from_counts = _support._entropy_from_counts
_get_peak_rss_kb = _support._get_peak_rss_kb
_information_gain_from_codes = _support._information_gain_from_codes
_is_binary_feature_series = _support._is_binary_feature_series
_joint_information_gain_from_binned_columns = _support._joint_information_gain_from_binned_columns
_wire_hugiml_feature_metadata = _support._wire_hugiml_feature_metadata
HUGIMLTuneResult = _tuning.HUGIMLTuneResult
_hugiml_auc_score_for_fast_grid = _tuning._hugiml_auc_score_for_fast_grid
_hugiml_build_fast_tune_adaptive_context = _tuning._hugiml_build_fast_tune_adaptive_context
_hugiml_expand_grid_for_fast_tune = _tuning._hugiml_expand_grid_for_fast_tune
_hugiml_fast_grid_tune = _tuning._hugiml_fast_grid_tune
_hugiml_fit_downstream_estimator_from_template = (
_tuning._hugiml_fit_downstream_estimator_from_template
)
_hugiml_params_key = _tuning._hugiml_params_key
_hugiml_prepare_candidate_from_cached_base = _tuning._hugiml_prepare_candidate_from_cached_base
_hugiml_prepare_downstream_template_from_cached_base = (
_tuning._hugiml_prepare_downstream_template_from_cached_base
)
_hugiml_score_model_for_tune = _tuning._hugiml_score_model_for_tune
_hugiml_shallow_candidate_from_base = _tuning._hugiml_shallow_candidate_from_base
_hugiml_standard_grid_tune_one_split = _tuning._hugiml_standard_grid_tune_one_split
_hugiml_tune = _tuning._hugiml_tune
_hugiml_validate_fast_tune_grid = _tuning._hugiml_validate_fast_tune_grid
[docs]
class HUGIMLClassifier(
_EstimatorMixin,
_BinningMixin,
_TrainingMixin,
_FeatureAssemblyMixin,
_InterpretationMixin,
_PredictionMixin,
_InspectionMixin,
TransformerMixin,
ClassifierMixin,
BaseEstimator,
):
"""HUG-IML interpretable classifier — C++ accelerated, scikit-learn compatible.
Extracts High Utility Gain (HUG) patterns from labelled tabular data,
transforms the input into a binary pattern-presence matrix, and fits an
interpretable downstream classifier. The mined patterns are human-readable
and serve as the primary source of model explanations.
Parameters
----------
allCols : list of 3 lists, optional
``[int_col_names, float_col_names, cat_col_names]``.
Must be paired with ``origColumns``.
origColumns : list of str, optional
Ordered column names matching the columns of X passed to fit/predict.
B : int, default 8
Number of quantile bins per numerical feature when
``adaptive_binning=False``. With adaptive binning enabled, per-feature
bin counts are selected from ``b_candidates``.
L : int, default 1
Maximum HUG pattern length. 1 = singletons; 2 = pairs; -1 = unlimited.
G : float, default 1e-3
Minimum information-gain threshold.
topK : int, default 30
Maximum number of patterns to retain. -1 computes automatically.
base_estimator : sklearn estimator, optional
Downstream classifier trained on the selected representation.
Defaults to LogisticRegression. An explicit LogisticRegression using
the ``liblinear`` solver is fitted directly for binary targets and
through one-vs-rest classification for targets with three or more
classes.
lr_solver : {"auto", "saga", "sgd"}, default "auto"
Downstream linear classifier used when ``base_estimator`` is not supplied.
``"auto"`` uses L1-regularized logistic regression: binary classifiers use
the ``liblinear`` solver and multiclass classifiers use the ``saga`` solver. ``"saga"`` uses
``LogisticRegression(solver="saga")``. ``"sgd"`` uses
``SGDClassifier(loss="log_loss")`` so large sparse downstream matrices can
be trained with stochastic gradient descent. All built-in choices keep the
existing deterministic ``random_state=0`` and ``max_iter=500`` defaults.
n_jobs : int, default 1
Number of OpenMP threads. -1 uses all available cores.
max_predict_ms : float or None
Prediction latency budget in milliseconds.
max_fit_seconds : float or None
Backward-compatible alias for the mining-stage wall-clock budget.
Transaction preparation and downstream model fitting are not bounded
by this value. Prefer ``max_mining_seconds`` for new code.
max_mining_seconds : float or None
Wall-clock budget, in seconds, for native pattern mining. This is
especially useful for explicit high-order bounded mining such as
``L=4``/``L=5``/larger values. Use ``1800`` for a 30-minute mining
cap. When unset, ``max_fit_seconds`` is used for backward
compatibility. Partial patterns mined before timeout are retained, and
attempt-level details are recorded in ``mining_audit_log_``.
adaptive_binning : bool, default True
Select per-feature numeric bin counts using supervised information gain.
b_candidates : list of int or None
Candidate bin counts evaluated when adaptive binning is enabled.
min_marginal_gain_ratio : float, default 0.02
Elbow threshold for adaptive-binning marginal gain.
adaptive_binning_sample_frac : float or bool, default False
Fraction of training rows used for adaptive-bin selection. ``False``
uses all rows; a float in ``(0, 1]`` uses a deterministic stratified
sample for selecting edges before applying those edges to all rows.
adaptive_binning_sample_random_state : int, default 42
Random seed used when ``adaptive_binning_sample_frac`` requests a
stratified sample.
convert_binary_to_categorical : bool, default False
When enabled, numeric columns with exactly two observed values are
inferred as categorical indicators during automatic column detection.
The default keeps them numeric so they remain eligible for numeric
interaction and augmented-pair paths. The named performance grids
explicitly keep this disabled, while the named interpretability grids
enable it for the categorical pattern surface. Explicit ``allCols``
metadata takes precedence over this inference option.
feature_mode : {"patterns_only", "original_plus_patterns",
"original_plus_interactions"}, default "patterns_only"
Downstream representation used by fit/predict APIs. ``transform(X)``
always returns the HUG pattern matrix.
use_hotpath : bool, default True
Use the fused native ``L=1`` preparation/mining/matrix path when
eligible. Disable only for diagnostic equivalence checks against the
staged path.
augmented_pair_transforms : bool, default True
Enable downstream augmented-pair operator features for eligible
``L >= 2`` adaptive-binning models.
augmented_pair_mode : {"interaction_information", "marginal_ig"},
default "interaction_information"
Source-column scorer for augmented-pair features.
ii_partner_size : int or None
Optional partner-search bound for interaction-information scoring.
aug_feature_size : int, default 10
Number of source columns retained for augmented-pair candidate
generation in interaction-information mode.
max_pair_features : int, default 10
Source-column budget used by the marginal-IG augmented-pair mode.
augmented_pair_max_features : int or None
v1.1.11-compatible alias for the augmented-pair source budget. When
provided with default new budgets, it maps to both ``aug_feature_size``
and ``max_pair_features``.
topk_budget_strict : bool, default False
Apply one global ``topK`` cap across the constructed downstream
representation.
dense_downstream_max_width : int, default 200
Width threshold below which downstream matrices may stay dense.
execution_mode : {"audit", "production"}, default "audit"
Artifact-retention mode.
interaction_relaxed_mining : bool, default False
Allow interaction-information survivors to participate in native mining
as original-feature bins without creating augmented-pair operator
columns. Relaxed admission covers the root and its immediate first-child
pairing partner; deeper positions receive no new admission exemption.
The generic miner still requires the constructed child pattern to clear
its joint information-gain gate. Mutually exclusive with augmented-pair
transforms at ``L >= 2``.
interaction_relaxed_feature_size : int, default 10
Survivor-source budget for interaction-relaxed mining.
verbose : bool, default False
Emit INFO-level log messages during fit.
Attributes (available after fit)
----------------------------------
classes_ : ndarray — unique class labels.
n_features_in_ : int — number of input features.
feature_names_in_ : list or None — column names from training data.
cat_cols_mask_ : ndarray[bool] — True for categorical columns.
is_int_mask_ : ndarray[bool] — True for integer columns.
td_ : _TransactionDataWrapper — discretisation artefacts.
patterns_ : list — mined HUG patterns.
x_train_hup_ : csr_matrix — binary training pattern matrix.
model_ : Pipeline — fitted downstream estimator.
fit_metadata_ : FitMetadata — timings, memory, pattern stats.
monitor : PredictionMonitor or None — prediction statistics.
"""
_fit_lock: threading.RLock # per-instance, created in __init__
monitor: PredictionMonitor | None # set by enable_monitoring() / disable_monitoring()
feature_names_in_: list[str] | None # set by prepareXy / _resolve_col_meta after fit
# The grid values themselves live in hugiml.hyperparameter_configs, shared
# with the benchmark runner and dashboard rather than declared here only.
# See default_param_grid() below for the public, name-aware accessor.
DEFAULT_PARAM_GRID: dict[str, list] = get_hugiml_grid(DEFAULT_HUGIML_GRID_NAME)
def __init__(
self,
allCols: list | None = None,
origColumns: list | None = None,
B: int = 8,
L: int = 1,
G: float = 1e-3,
topK: int = 30,
base_estimator: Any = None,
lr_solver: str = "auto",
n_jobs: int = 1,
max_predict_ms: float | None = None,
max_fit_seconds: float | None = None,
max_mining_seconds: float | None = None,
verbose: bool = False,
# ── Adaptive binning ──────────────────────────────────────────────
# When adaptive_binning=True each numerical feature is pre-discretised
# to B_j quantile bins chosen by elbow-stopping IG search. The
# pre-binned columns are declared categorical before the C++ layer
# (global B is overridden to sentinel 2). Bin edges are stored in
# _bin_edges_ and reapplied identically at predict/transform time.
#
# adaptive_binning=True avoids requiring the user to guess a single
# global B: on internal benchmarks, accuracy with a constant B varied
# by several points of ROC-AUC depending on the value chosen (e.g.
# B=3 vs B=20), with no single constant value best across datasets.
# Adaptive binning lands at parity with a well-chosen constant B
# without requiring a search for one; head-to-head testing shows it
# roughly tied with, not uniformly ahead of, a well-chosen constant B,
# so the benefit is removing a sensitive hyperparameter from the
# quick-start path rather than a raw accuracy gain. Pass
# adaptive_binning=False to use a single constant B instead.
# ─────────────────────────────────────────────────────────────────
adaptive_binning: bool = True,
b_candidates: list | None = None,
min_marginal_gain_ratio: float = 0.02,
adaptive_binning_sample_frac: float | bool = False,
adaptive_binning_sample_random_state: int = 42,
# When True, any
# *numeric* column with exactly two distinct values is automatically
# treated as categorical during column-type detection (see
# _resolve_col_meta), the same as an explicitly categorical
# (object/string/pandas Categorical) column. This is sometimes
# desirable for genuinely nominal binary columns (e.g. a yes/no
# flag encoded as 0/1) -- set convert_binary_to_categorical=True to
# restore that behavior -- but the default (False) treats binary-
# valued numeric columns as numeric, matching their pandas dtype
# rather than their observed cardinality, because the previous
# default (True) silently excluded every such column from
# augmented-pair transforms: _numeric_feature_names_for_augmented_pairs()
# only considers columns NOT marked categorical, so a dataset made up
# entirely of 0/1-valued *measurements* (not categories) used to end
# up with augmented_pair_transforms constructing zero pairs
# regardless of augmented_pair_transforms=True, with no warning.
# Has no effect when column types are supplied explicitly via
# allCols/origColumns, since that path never auto-detects types.
convert_binary_to_categorical: bool = False,
feature_mode: str = "patterns_only",
use_hotpath: bool = True,
augmented_pair_transforms: bool = True,
augmented_pair_mode: str = "interaction_information",
ii_partner_size: int | None = None,
aug_feature_size: int = 10,
max_pair_features: int = 10,
augmented_pair_max_features: int | None = None,
topk_budget_strict: bool = False,
dense_downstream_max_width: int = 200,
execution_mode: str = "audit",
# When True, interaction-information survivors can enter native mining
# as ordinary source columns even when their marginal IG is weak. Relaxed
# admission covers the first two positions of the initial branch: the root
# and its immediate first-child pairing partner. In the generic miner, a
# survivor at either position can bypass its own singleton admission gates
# long enough for the joint child pattern to be scored; no new relaxed
# admission is introduced at item positions 2+, where survivor items must
# pass the ordinary gates. The specialized L=2 path likewise treats a pair
# as relaxed when either member is a survivor. See native/mining.hpp for
# the exact pruning and heap-routing scope.
# No augmented-pair operator features (sum/product/etc.) are generated
# by this path. The resulting HUG patterns remain conjunctions of
# original feature bins and are annotated for audit APIs as
# survivor-led patterns when they include a relaxed survivor column.
# Mutually exclusive with augmented_pair_transforms at L >= 2
# (validated in _validate_params). At L=1 this is a no-op because L=1
# uses the fused hotpath, which has no relaxed variant. Effective for
# L in {2, 3, -1}.
interaction_relaxed_mining: bool = False,
# Number of columns select_interaction_information_features returns
# for interaction_relaxed_mining's own column selection. Separate
# from aug_feature_size, which controls the unrelated
# augmented_pair_mode source-column count.
interaction_relaxed_feature_size: int = 10,
) -> None:
self.allCols = allCols
self.origColumns = origColumns
self.B = B
self.L = L
self.G = G
self.topK = topK
self.base_estimator = base_estimator
self.lr_solver = lr_solver
self.n_jobs = n_jobs
self.max_predict_ms = max_predict_ms
self.max_fit_seconds = max_fit_seconds
self.max_mining_seconds = max_mining_seconds
self.verbose = verbose
self.adaptive_binning = adaptive_binning
self.b_candidates = b_candidates
self.min_marginal_gain_ratio = min_marginal_gain_ratio
self.adaptive_binning_sample_frac = adaptive_binning_sample_frac
self.adaptive_binning_sample_random_state = adaptive_binning_sample_random_state
self.convert_binary_to_categorical = convert_binary_to_categorical
self.feature_mode = feature_mode
self.use_hotpath = use_hotpath
self.augmented_pair_transforms = augmented_pair_transforms
self.augmented_pair_mode = augmented_pair_mode
self.ii_partner_size = ii_partner_size
if isinstance(augmented_pair_max_features, int) and not isinstance(
augmented_pair_max_features, bool
):
if (
isinstance(aug_feature_size, int)
and not isinstance(aug_feature_size, bool)
and aug_feature_size == DEFAULT_AUGMENTED_PAIR_MAX_FEATURES
):
aug_feature_size = augmented_pair_max_features
if (
isinstance(max_pair_features, int)
and not isinstance(max_pair_features, bool)
and max_pair_features == DEFAULT_AUGMENTED_PAIR_MAX_FEATURES
):
max_pair_features = augmented_pair_max_features
self.aug_feature_size = aug_feature_size
self.max_pair_features = max_pair_features
self.augmented_pair_max_features = augmented_pair_max_features
self.topk_budget_strict = topk_budget_strict
self.dense_downstream_max_width = dense_downstream_max_width
# sklearn estimator compatibility: constructor and set_params must not
# validate parameter values. execution_mode is validated in fit/load.
self.execution_mode = execution_mode
self.interaction_relaxed_mining = interaction_relaxed_mining
self.interaction_relaxed_feature_size = interaction_relaxed_feature_size
self._fit_lock = threading.RLock()
NativeAugmentedPairTransformBlock.__module__ = __name__
FitMetadata.__module__ = __name__
_MemoryTracker.__module__ = __name__
_TransactionDataWrapper.__module__ = __name__
HUGIMLTuneResult.__module__ = __name__
HUGIMLClassifier.fast_grid_tune = classmethod(_hugiml_fast_grid_tune)
HUGIMLClassifier.tune = classmethod(_hugiml_tune)
HUGIMLClassifierNative = HUGIMLClassifier
__all__ = [
"HUGIMLClassifier",
"HUGIMLClassifierNative",
"FitMetadata",
"HUGIMLTuneResult",
]