Compose vector distributions out of your SQL queries, then find the entities that do not behave like anybody else.
vec.trace turns the tables of your PostgreSQL databases into one vector per entity — a customer, a device, a merchant — and runs outlier detection on the result. You describe what a behaviour is with ordinary SQL; vec.trace takes care of joining the descriptions, normalising them, choosing the right detector, scoring every entity and explaining why the anomalous ones are anomalous.
Everything you build (connections, blocks, distributions, tuning profiles, runs and their per-entity results) is persisted per user in a PostgreSQL service database, so an analysis is a reproducible object, not a notebook you lose.
source DB ──query──> block ──┐
source DB ──query──> block ──┼──join on entity id──> distribution ──> run ──> outliers + why
source DB ──query──> block ──┘ (profile = tuning)
Two frontends share one core library: a command line (vec.cli) and a Streamlit
application (vec.app). Neither contains domain logic.
| Concept | What it is |
|---|---|
| Connection | a source PostgreSQL database, reachable through a read-only role; its password is encrypted at rest |
| Block | one SELECT + the column that holds the entity id + a map that renames the numeric result columns into components |
| Distribution | blocks joined on the entity id (inner, left or outer): a matrix of n entities × d components |
| Profile | a saved set of algorithm parameters (scaling, imputation, PCA, contamination, detector…), stored partially so it keeps inheriting the defaults |
| Run | one detection execution, with the effective parameters snapshotted and the per-entity score, percentile, flag and explanation |
The workflow follows a fixed sequence, and every step reports what it decided:
- Prepare — drop constant components, impute the gaps left by a
left/outerjoin, scale withRobustScaler(median / IQR), rescaling by the standard deviation the components whose IQR is zero (sentinel values, sparse counters). - Reduce — above
max_dim(50) components, project with PCA. The target is the number of components whose eigenvalue crosses the Marchenko-Pastur noise edge, and the projection is skipped when the components are uncorrelated: truncating a flat spectrum only mixes signal into noise. - Test the shape — is the distribution unimodal or multimodal? A Gaussian-mixture BIC comparison on a low-dimensional projection plus a KDE peak count, both run on a trimmed sample so that the outliers themselves do not masquerade as a second population.
- Detect — ECOD for a unimodal distribution, IsolationForest for a multimodal one (a partitioning detector also replaces ECOD when the matrix handed to the detector is still very wide, because an additive marginal score is diluted by the number of components).
- Interpret — the percentile of every score, the flag from the contamination threshold, and for the most anomalous entities a median-counterfactual attribution: each component is reset to its median, the fitted model scores the entity again, and the score drop is that component's share of the anomaly.
Steps 2 and 4 deviate from the literal rule in the original specification for
measured reasons, and both are one parameter away from the literal behaviour
(pca_adaptive = false, high_dim_algorithm = null). The numbers behind that
choice are in docs/ALGORITHMS.md.
docker compose up -d --buildThat starts PostgreSQL (service database and an e-commerce demo dataset) and
the UI on http://localhost:8501. No .env needed: every variable has a
default. The first account you create from the sign-in page becomes the
administrator.
The demo database contains 3,012 customers with 12 planted anomalous accounts, and three ready-to-paste block queries. A detection run over them puts 10 of the 12 in the top 20, in under a second:
distribution 'customer-behaviour'
entities 3012
components 40
components used 39
modality distribution judged multimodal by combined test
[gmm=multimodal, kde=multimodal]: best mixture k=4
algorithm IsolationForest (iforest)
outliers 13 of 3012 (0.4%)
why '900001' (rank 4, percentile 99.9, score 0.6456):
1. 'orders_total' = 360, at the 100.0th percentile of the distribution,
robust z = +32.36, median = 4, restoring the median lowers the score
by 0.010 (21% of the anomaly)
2. 'tickets_total' = 6, at the 99.9th percentile, robust z = +6.00, ...
3. 'events_total' = 420, at the 99.9th percentile, robust z = +4.15, ...
Ports, credentials, the demo schema and the queries: docker/README.md.
python -m venv .venv && .venv\Scripts\activate # source .venv/bin/activate
pip install -e ".[dev]"
copy .env.example .env # cp .env.example .env
vec.cli db init
vec.cli user create alice --password - # the first user is the admin
vec.cli auth login --username alice
vec.cli conn add shop --host localhost --port 5433 --database vectrace_demo \
--username vec_reader --password - --search-path shop
vec.cli block create orders --connection shop --query-file orders.sql \
--entity-id customer_id --map orders_90d --map revenue --verify
vec.cli block create web --connection shop --query-file web.sql \
--entity-id customer_id --map events_total --map night_event_share
vec.cli dist create customer-behaviour --blocks orders,web --join left
vec.cli detect run customer-behaviour --top 15
vec.cli detect explain <run-id> 900001
vec.app # the same workflow in the browser- Blocks as first-class objects — a query, its entity id and its component mapping are persisted, versioned by name and reusable across distributions.
- Composition by join —
inner,leftandouteron the entity id, with per-component missing-value reporting and five imputation strategies. - A detector chosen from the data, not from a config file: unimodal → ECOD, multimodal → IsolationForest, with an explainable modality verdict (BIC and KDE evidence stored in the run).
- Adaptive dimensionality reduction driven by random matrix theory instead of a blind truncation.
- Explanations, not just scores — percentile, rank, robust z, empirical tail percentile and a model-aware counterfactual attribution expressed in the original components (never in principal components).
- Reproducible runs — every run stores its effective parameters, its notes, its timings and its per-entity result; re-running a distribution months later shows exactly what was used.
- Multi-user and safe — PBKDF2 password hashing, revocable session tokens, Fernet-encrypted source credentials, per-user ownership, read-only SQL validation and read-only server transactions with a statement timeout.
- Two frontends, one core — a scriptable CLI with
--json/--csvoutput and meaningful exit codes, and a Streamlit app with an interactive block builder. - Portable — a
docker compose upaway, and the test-suite runs on SQLite for the core and on PostgreSQL for the connector.
| Document | Contents |
|---|---|
| docs/USER_MANUAL.md | concepts, installation, the UI page by page, the full CLI reference, a worked example, how to read a result, tuning recipes, troubleshooting |
| docs/ALGORITHMS.md | the mathematics: robust scaling, imputation, PCA and the Marchenko-Pastur edge, the modality tests, ECOD and IsolationForest, thresholds, the counterfactual attribution, complexity, the parameter reference and the limitations |
| docs/SOFTWARE.md | architecture, module map, the service database schema, the settings cascade, the security model, the CLI and the app internals, configuration, packaging, extension points |
| docker/README.md | the compose stack, the two databases and their roles, the demo dataset, the 12 planted anomalies, three example block queries |
| spec.txt | the original (Italian) requirements this project was built from |
src/vec/
trace/ the core library — no UI knowledge
config.py settings and every algorithm parameter
models.py the service database schema (SQLAlchemy 2.0)
db.py repository.py engines, sessions, per-user persistence
auth.py security.py users, tokens, password hashing, credential encryption
connectors/ read-only SQL validation + the PostgreSQL connector
blocks.py query -> entity id + numeric components
distributions.py join the blocks into the matrix
pipeline/ preprocessing, reduction, modality, detectors,
explanation, engine
service.py the VecTrace facade used by both frontends
cli/ vec.cli: argparse commands, output formatting
app/ vec.app: Streamlit entry point + 8 pages
tests/ 248 tests: units on SQLite, pages via AppTest,
integration on PostgreSQL
docker/ compose init scripts, demo dataset, deployment notes
pip install -e ".[dev]"
pytest # 234 tests, no external service needed
set VECTRACE_TEST_SERVICE_DSN=postgresql+psycopg://vectrace:vectrace@localhost:5433/vectrace
pytest # + the 14 PostgreSQL integration tests
ruff check src tests| Module | Tests | Covers |
|---|---|---|
test_pipeline.py |
78 | preparation, modality, reduction, ECOD/IsolationForest, thresholds, explanations, end-to-end detection |
test_core_units.py |
57 | configuration, parameter validation, security, SQL safety, block materialisation |
test_service_sqlite.py |
37 | persistence, ownership, the settings cascade, the run lifecycle |
test_cli.py |
35 | argument handling, exit codes, the whole command surface |
test_distributions.py |
14 | join semantics and composition errors |
test_postgres_integration.py |
14 | the real connector, the read-only transaction, a run on PostgreSQL |
test_app.py |
13 | every Streamlit page, headlessly, via streamlit.testing.v1.AppTest |
The integration suite creates and drops its own schema, so it is safe to run against the demo database.
- Source queries are validated (one statement, read-only keyword) and
executed inside
SET LOCAL transaction_read_only = onwith a statement timeout. Register your connections with a role that can onlySELECT. - Source passwords are encrypted with
VECTRACE_ENCRYPTION_KEYbefore being stored. Set that variable explicitly in any shared deployment; changing it later makes the stored passwords unreadable. - vec.trace reports statistical outliers. A flagged entity is unusual, not proven guilty — the explanation exists so a human can decide.
0.1.0 — the core workflow (composition + outlier detection), both frontends,
the container deployment and the demo dataset are complete and tested. Foreseen
by the design and not implemented yet: SPARQL and HTTP-API sources, operations
on distributions other than outlier detection, incremental materialisation,
schema migrations.
Apache License 2.0 — see LICENSE.