Open-source agentic AI for transaction monitoring. Just ask Jooby.What's Jube? →

Gaussian Mixture Density Anomaly Scoring

A Gaussian mixture models the data as a weighted sum of several Gaussian components; read as a density rather than as clusters, the fitted mixture gives every row a likelihood under the model, and a row with unusually low likelihood is flagged as anomalous.

R equivalent

Package installed beyond the shared set: mclust.

library(mclust)

set.seed(1)
n <- 200
amount <- rnorm(n, mean = 50, sd = 15)
velocity <- rpois(n, 2)
normal <- scale(data.frame(amount, velocity))

model <- densityMclust(normal, G = 3, plot = FALSE)
print(summary(model))

new_cases <- data.frame(amount = c(48, 900), velocity = c(2, 40))
new_scaled <- scale(new_cases, center = attr(normal, "scaled:center"),
                     scale = attr(normal, "scaled:scale"))
predict(model, newdata = new_scaled, what = "dens")

Console output:

Package 'mclust' version 6.1.3
Type 'citation("mclust")' for citing this R package in publications.
------------------------------------------------------- 
Density estimation via Gaussian finite mixture modeling 
------------------------------------------------------- 

Mclust EII (spherical, equal volume) model with 3 components: 

 log-likelihood   n df       BIC       ICL
      -561.0504 200  9 -1169.786 -1245.712
[1] 0.1748917 0.0000000

Tool equivalent: Jube.Service.Repository.AnalyticsTrainingRun.AdvancedAnalyticsGaussianMixtureDensityTrainingService / tool SubmitGaussianMixtureDensityTraining — Kind = DensityEstimator, recall class GaussianMixtureDensityRecall. Components, Standardise, MaxIterations and Tolerance match the Gaussian mixture clusterer’s DTO; the extra RegularizationEpsilon (default 1e-6) is the same covariance ridge idea as Mahalanobis’s RidgeEpsilon, needed here because a density read of a mixture is far more sensitive to a near-singular component covariance blowing up the likelihood than a clustering read is. This family is always unsupervised — there is no Labels field on the DTO at all, unlike isolation forest and one-class SVM above, which both accept optional labels purely for calibration.

Code audit

Same fitting machinery as the clustering family, genuinely repurposed

Jube.Engine/AdvancedAnalytics/Density/GaussianMixtureDensityTrainer.cs:166-200 and Jube.Engine/AdvancedAnalytics/Clustering/GaussianMixtureClustererTrainer.cs:151-183 both construct new Accord.MachineLearning.GaussianMixtureModel(options.Components) with an identical options shape — Options = new NormalOptions { Regularization = ... }, MaxIterations, Tolerance, ComputeLogLikelihood = true — and fit it with .Learn(trainDesign) against a GaussianClusterCollection. This is the same Accord.NET class and the same EM fitting call in both families; the two training services are separate, independently-maintained C# files (the code is duplicated, not shared through a common helper), but the underlying algorithm and its parameterisation are identical. What differs is what happens to the fit afterwards: the clustering family keeps Accord’s own GaussianClusterCollection and reads cluster assignment and its own mixtureModel.LogLikelihood off it directly, while the density family discards Accord’s own scoring path after extracting each component’s weight, mean and covariance (GaussianMixtureDensityTrainer.cs:208-260) and rebuilds its own scorer from scratch: each component’s inverse covariance and log-determinant are computed via the same SmallLinearAlgebra.Cholesky module Mahalanobis uses (GaussianMixtureDensityTrainer.cs:217-260), and Jube.Engine/AdvancedAnalytics/Density/GaussianMixtureDensityPipeline.cs:76-109 re-derives the multivariate normal log-density itself — ComponentLogPdf computes -0.5 * (d * ln(2π) + logDet + quadratic) per component, and LogDensity combines the components via a numerically stable log-sum-exp of log(weight_k) + ComponentLogPdf_k. So this is genuinely a density-estimation use of the identical mixture machinery, not literally the same code path re-labelled — the fitting call is duplicated verbatim, but the scoring code is independently written and never calls back into Accord.

What the anomaly score actually is

GaussianMixtureDensityRecall.Predict (GaussianMixtureDensityRecall.cs:69-74) returns _pipeline.LogDensity(input) directly: the raw natural-log density of the fitted mixture at that row. It is not negated — this is log p(x), not -log p(x), and there is no calibration step anywhere in this family (GaussianMixtureDensityOptions.cs has no Calibrate field, and no Calibrator/Platt-scaling call appears in GaussianMixtureDensityTrainer.cs). A lower (more negative) value means a less likely, more anomalous row; there is no fixed cutoff shipped with the model — only the train/validation/test mean log-likelihood plus BIC and AIC (GaussianMixtureDensityTrainer.cs:282-297,311-323) as diagnostics for whether the fit itself is trustworthy, not as a score threshold.

Regularisation is layered, not single-mechanism

Two independent regularisation paths exist. First, Accord’s own EM fit is handed NormalOptions { Regularization = options.RegularizationEpsilon } (GaussianMixtureDensityTrainer.cs:176) — Accord’s own internal safeguard against singular component covariances during iteration. Second, after fitting, Jube separately attempts to invert each component’s covariance itself; only if that inversion fails does it add options.RegularizationEpsilon * averageVariance to the component’s diagonal and retry (GaussianMixtureDensityTrainer.cs:218-244), the same conditional-ridge-on-singularity pattern as Mahalanobis, using the same RegularizationEpsilon default of 1e-6 (GaussianMixtureDensityOptions.cs:29). A warning fires if any component needed this second regularisation (GaussianMixtureDensityTrainer.cs:263-267).

Defaults and guards

Components defaults to 3 (GaussianMixtureDensityOptions.cs:21), MaxIterations to 100 and Tolerance to 1e-5 (lines 25, 27). Training requires at least Components * (d + 1) rows, where d is the active variable count (GaussianMixtureDensityTrainer.cs:149-155), and warns if there are fewer than MinimumRowsPerComponentRatio * (d + 1) rows per component (default ratio 5, GaussianMixtureDensityTrainer.cs:34,157-162). Non-convergence — the EM loop consuming its full iteration budget — is reported explicitly (converged = MaxIterations <= 0 || iterations < MaxIterations, GaussianMixtureDensityTrainer.cs:202-206).