hypertopos 0.9.0__tar.gz → 0.9.2__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.
- {hypertopos-0.9.0 → hypertopos-0.9.2}/CHANGELOG.md +30 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/PKG-INFO +69 -3
- {hypertopos-0.9.0 → hypertopos-0.9.2}/README.md +63 -2
- {hypertopos-0.9.0 → hypertopos-0.9.2}/SECURITY.md +1 -1
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/README.md +1 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/api-reference.md +1 -1
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/concepts.md +1 -1
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/configuration.md +4 -1
- hypertopos-0.9.2/docs/examples/spark-quickstart.ipynb +222 -0
- hypertopos-0.9.2/docs/images/demo-spark.svg +1 -0
- hypertopos-0.9.2/docs/spark.md +253 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/__init__.py +1 -1
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/builder/builder.py +64 -11
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/builder/derived.py +1 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/cli/build.py +1 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/cli/main.py +6 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/cli/schema.py +16 -1
- hypertopos-0.9.2/hypertopos/cli/spark_cmd.py +108 -0
- hypertopos-0.9.2/hypertopos/datasets.py +252 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/engine/geometry.py +2 -1
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/model/sphere.py +7 -3
- hypertopos-0.9.2/hypertopos/scoring.py +253 -0
- hypertopos-0.9.2/hypertopos/spark/__init__.py +19 -0
- hypertopos-0.9.2/hypertopos/spark/_api.py +232 -0
- hypertopos-0.9.2/hypertopos/spark/_build.py +142 -0
- hypertopos-0.9.2/hypertopos/spark/_explain.py +208 -0
- hypertopos-0.9.2/hypertopos/spark/_plan.py +444 -0
- hypertopos-0.9.2/hypertopos/spark/_result.py +48 -0
- hypertopos-0.9.2/hypertopos/spark/_score.py +70 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/storage/reader.py +1 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/pyproject.toml +10 -1
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/conftest.py +24 -0
- hypertopos-0.9.2/tests/golden/spark_plan_full.txt +27 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_builder_chunking.py +13 -0
- hypertopos-0.9.2/tests/test_cli_spark.py +145 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_cluster_a_label_aware_depth.py +1 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_cluster_d2_influencer_auto_discovery.py +1 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_counterfactual_frozen_trajectory.py +2 -0
- hypertopos-0.9.2/tests/test_datasets.py +82 -0
- hypertopos-0.9.2/tests/test_dim_blocks_build.py +98 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_m1_1_builder_wiring.py +2 -0
- hypertopos-0.9.2/tests/test_precomputed_dim_kind.py +118 -0
- hypertopos-0.9.2/tests/test_scoring.py +196 -0
- hypertopos-0.9.2/tests/test_spark_build.py +219 -0
- hypertopos-0.9.2/tests/test_spark_connect_safe.py +94 -0
- hypertopos-0.9.2/tests/test_spark_docs.py +69 -0
- hypertopos-0.9.2/tests/test_spark_explain.py +116 -0
- hypertopos-0.9.2/tests/test_spark_find_anomalies.py +162 -0
- hypertopos-0.9.2/tests/test_spark_plan.py +333 -0
- hypertopos-0.9.2/tests/test_spark_quickstart.py +42 -0
- hypertopos-0.9.2/tests/test_spark_score.py +89 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/.github/workflows/publish-pypi.yml +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/.github/workflows/test.yml +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/.gitignore +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/LICENSE.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/architecture.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/LICENSE +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/README.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/README.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/results/0.1.0/SUMMARY.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/results/0.1.0/r1/0.1.0-exp-a-report.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/results/0.1.0/r1/0.1.0-exp-a-scoring.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/results/0.1.0/r1/0.1.0-exp-b-report.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/results/0.1.0/r1/0.1.0-exp-b-scoring.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/results/0.1.0/r1/0.1.0-settings.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/results/0.1.0/r2/0.1.0-exp-a-report.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/results/0.1.0/r2/0.1.0-exp-a-scoring.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/results/0.1.0/r2/0.1.0-exp-b-report.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/results/0.1.0/r2/0.1.0-exp-b-scoring.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/results/0.1.0/r2/0.1.0-settings.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/results/0.1.0/r3/0.1.0-exp-a-report.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/results/0.1.0/r3/0.1.0-exp-a-scoring.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/results/0.1.0/r3/0.1.0-exp-b-report.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/results/0.1.0/r3/0.1.0-exp-b-scoring.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/results/0.1.0/r3/0.1.0-settings.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/results/0.1.0/r4/0.1.0-exp-a-report.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/results/0.1.0/r4/0.1.0-exp-a-scoring.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/results/0.1.0/r4/0.1.0-exp-b-report.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/results/0.1.0/r4/0.1.0-exp-b-scoring.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/results/0.1.0/r4/0.1.0-settings.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/results/0.1.0/r5/0.1.0-exp-a-report.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/results/0.1.0/r5/0.1.0-exp-a-scoring.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/results/0.1.0/r5/0.1.0-exp-b-report.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/results/0.1.0/r5/0.1.0-exp-b-scoring.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/results/0.1.0/r5/0.1.0-settings.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/results/0.1.0/r6/0.1.0-exp-a-report.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/results/0.1.0/r6/0.1.0-exp-a-scoring.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/results/0.1.0/r6/0.1.0-exp-b-report.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/results/0.1.0/r6/0.1.0-exp-b-scoring.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/results/0.1.0/r6/0.1.0-settings.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/sphere/prepare_accounts.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/sphere/prepare_cpty_banks.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/sphere/prepare_districts.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/sphere/prepare_loan_accounts.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/sphere/prepare_operations.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/sphere/prepare_orders.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/sphere/prepare_transactions.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/sphere/prepare_tx_types.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/berka/sphere/sphere.yaml +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/ibm-aml/README.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/ibm-aml/_layer2_cross_eval.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/ibm-aml/_layer3_cross_eval.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/ibm-aml/hi_small_sphere/prepare_accounts.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/ibm-aml/hi_small_sphere/prepare_currencies.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/ibm-aml/hi_small_sphere/prepare_payment_formats.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/ibm-aml/hi_small_sphere/prepare_transactions.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/ibm-aml/hi_small_sphere/sphere.yaml +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/ibm-aml/layer1_passive_scan.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/ibm-aml/layer2_xgboost_sweep.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/ibm-aml/layer3_typology_filter.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/ibm-aml/li_small_sphere/prepare_accounts.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/ibm-aml/li_small_sphere/prepare_currencies.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/ibm-aml/li_small_sphere/prepare_payment_formats.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/ibm-aml/li_small_sphere/prepare_transactions.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/ibm-aml/li_small_sphere/sphere.yaml +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/ibm-aml/passive-scan-calibration.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/nyc-taxi/README.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/nyc-taxi/results/0.1.0/SUMMARY.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-exp-a-report.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-exp-a-scoring.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-exp-b-report.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-exp-b-scoring.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-exp-c-report.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/nyc-taxi/results/0.1.0/r1/0.1.0-settings.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-exp-a-report.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-exp-a-scoring.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-exp-b-report.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-exp-b-scoring.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-exp-c-report.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/nyc-taxi/results/0.1.0/r2/0.1.0-settings.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-exp-a-report.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-exp-a-scoring.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-exp-b-report.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-exp-b-scoring.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-exp-c-report.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/nyc-taxi/results/0.1.0/r3/0.1.0-settings.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/nyc-taxi/sphere/prepare_trips.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmark/nyc-taxi/sphere/sphere.yaml +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/benchmarks.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/container-deployment.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/data-format.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/external-chains-as-anchor-line.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/images/build-pipeline.svg +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/images/core-objects.svg +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/images/cutting-plane.svg +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/images/demo-drift-solid.svg +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/images/demo-find-anomalies.svg +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/images/directory-layout.svg +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/images/hypertopos-overview.svg +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/images/model-lifecycle.svg +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/images/navigation-primitives.svg +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/images/sessions-and-branches.svg +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/images/three-scales.svg +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/introduction.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/quickstart.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/docs/refutations.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/_file_formats.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/builder/__init__.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/builder/_bootstrap.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/builder/_bregman.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/builder/_scatter.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/builder/_stats.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/builder/_theta_sensitivity.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/builder/_writer.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/builder/conformance.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/builder/conformance_mapping.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/builder/dim_blocks.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/builder/mapping.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/builder/temporal_bucket.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/cli/__init__.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/cli/__main__.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/cli/info.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/cli/sources.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/cli/sphere_ops.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/engine/__init__.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/engine/adjacency.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/engine/aggregation.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/engine/calibration.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/engine/calibration_label_aware.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/engine/chains.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/engine/composition.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/engine/conformance.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/engine/counterfactual.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/engine/density_gaps.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/engine/diagnostics.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/engine/dim_audit.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/engine/edge_features.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/engine/explanation.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/engine/fdr.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/engine/forecast.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/engine/graph.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/engine/graph_algorithms.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/engine/heredity.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/engine/hop_predicate.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/engine/invalidation.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/engine/investigation.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/engine/lance_sql_agg.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/engine/p_value_calibration.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/engine/selection.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/engine/structuring.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/engine/topology.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/model/__init__.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/model/manifest.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/model/objects.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/navigation/__init__.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/navigation/navigator.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/navigation/scanner.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/py.typed +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/sphere.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/storage/__init__.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/storage/_lance_branches.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/storage/_lance_session.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/storage/_lance_versions.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/storage/_schemas.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/storage/_tag_timestamps.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/storage/cache.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/storage/calibration_history.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/storage/exceptions.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/storage/topology_cache.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/storage/writer.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/utils/__init__.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/utils/arrow.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/utils/gc_pause.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/hypertopos/utils/time.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/.gitkeep +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/_golden_py.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/fixtures/generate_fixtures.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/fixtures/synthetic_chain_sphere.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/golden/README.md +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/golden/built_aggregate.json +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/golden/built_attract_anomaly_accounts.json +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/golden/built_attract_anomaly_transactions.json +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/golden/built_attract_drift.json +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/golden/built_calibration.json +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/golden/built_check_alerts.json +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/golden/built_explain_anomaly.json +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/golden/built_sphere_overview.json +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/golden/chain_coherent_anomaly.json +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/golden/chain_for_entity.json +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/golden/chain_propagation_and_typology.json +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/golden/extract_chains_engine.json +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/golden/sales_calibration_and_anomaly.json +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_adjacency.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_adjacency_order.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_adjacency_vectorized.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_aggregate_group_by_relation.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_aggregate_tie_order.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_aggregation_engine.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_anomaly_influence_cache.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_anomaly_propagation_in_chain.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_arrow_utils.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_atomic_meta_writes.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_audit_label_alignment.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_bootstrap.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_branch_ingest.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_branch_recalibrate.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_branch_sessions.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_branches.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_bregman.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_builder.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_builder_alias.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_builder_contagion.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_builder_derived.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_builder_edges.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_builder_fdr_hierarchy.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_builder_fdr_hierarchy_inject.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_builder_fdr_yaml_end_to_end.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_builder_rolling_z.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_builder_scatter.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_builder_temporal_bucket.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_builder_temporal_perf.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_cache.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_cache_threads.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_calibration.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_calibration_drift.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_calibration_history.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_calibration_history_builder.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_calibration_history_reader.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_calibration_label_aware.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_centroid_drift_labels.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_chain_coherent_anomaly.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_chain_coherent_synthetic.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_chain_drift_trajectory.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_chain_edge_potentials.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_chain_extraction_parity.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_chain_investigation_summary.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_chain_signed_confidence_rollup.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_chain_typology_and_extend.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_chain_witness_intersection.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_chains.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_cli.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_cli_alias.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_cli_branch.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_cli_inprocess.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_cli_label_aware_flag.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_cli_mcp_contract.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_cli_sources.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_cli_subprocess.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_cluster_c1_sphere_overview_ergonomics.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_cluster_c3_detector_closures.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_cluster_d1_graph_phase3.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_compaction.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_compare_branches.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_composition.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_conformance.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_conformance_before_write.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_conformance_mapping.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_conftest_magicmock_restore.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_continuous_event_dims.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_count_geometry_rows_missing.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_counterfactual.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_counterfactual_metric.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_data_quality_prop.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_decompose_drift.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_delta_dim_aggregation_consumers.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_delta_norm_signed.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_density_gaps_engine.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_derived_std_exact.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_detection.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_diagnostics.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_dim_audit.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_dim_blocks.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_dim_quality_warnings.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_distinct_neighbor_count.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_edge_dim_aggregations.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_edge_dim_aggregations_builder.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_edge_dim_aggregations_chain.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_edge_dim_aggregations_engine.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_edge_dim_aggregations_parity.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_edge_dim_aggregations_roundtrip.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_edge_dimensions_builder.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_edge_dimensions_yaml.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_edge_features.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_edge_features_pairs.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_edge_features_reader.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_edge_potential.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_edge_table.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_engine.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_epoch_seconds.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_fdr.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_fdr_multi_resolution.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_fdr_non_finite.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_fdr_per_dim.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_file_formats.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_find_calibration_influencers.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_find_density_gaps.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_find_diverse_explanations.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_find_lead_lag.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_find_lead_lag_significance.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_find_motif_by_hops.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_forecast.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_fts.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_generate_sar_rationale.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_geometric_path.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_geometry_stats.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_golden_core.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_goto_keyed_read.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_gradient_alignment.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_graph_algorithms.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_graph_communities_deterministic.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_graph_components.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_graph_edge_arrays.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_graph_geometry_tension.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_group_aggregate.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_heredity.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_hop_predicate_engine.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_hub_safety_caps.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_hunt3_branches_sessions.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_hunt3_builder_cli.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_hunt3_navigator.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_hybrid_search.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_incremental.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_invalidation.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_investigate_chain.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_investigate_entity.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_investigation.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_kind_detect.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_kind_tag_mismatch_warning.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_label_audit_schema.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_lance_session.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_lance_sql_agg.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_lance_sql_pinned_version.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_lance_write_policy.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_local_trajectory_shape.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_mapping.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_metric_arithmetic_guard.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_model.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_motif.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_mvcc_session.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_native_mvcc_geometry.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_navigation.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_navigator_alerts.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_navigator_extract_chains.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_navigator_fdr_resolution.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_navigator_fdr_select.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_neighbors_window.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_no_global_rng.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_observability.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_p_value_calibration.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_pattern_fdr_hierarchy.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_perf_budget.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_points_btree_lazy.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_precision_stack.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_profile_motif_perf_smoke.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_promote_branch.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_query_determinism.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_rank_stats_rewrite.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_reader_pinned_open.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_reader_quoted_literals.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_reader_sampling.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_recalibrate_column_write.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_recalibrate_derived_columns.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_recalibrate_epochs.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_reliability_flags.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_reputation.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_root_cause_trace.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_row_id_cache.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_scanner.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_scanner_order.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_scatter_columns.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_schema_chain_eda_validation.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_schema_mahalanobis_group_by.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_schema_mahalanobis_weights.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_seed_scoped_motif.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_selection.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_session_metadata_pin.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_session_ref_ratchet.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_shapes_from_deltas.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_short_path_epoch_tag.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_signed_confidence_ranking.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_simulate_dimension_change.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_slice_deltas.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_smoke_round_navigator.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_sphere.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_sphere_format_version.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_sphere_overview_perf.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_sphere_validation_cheap_tier.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_stats_guard.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_storage.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_stored_delta_metric.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_streaming_reads.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_structuring_engine.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_tag_timestamps.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_temporal_bucket_median.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_temporal_centroids.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_temporal_filter_offsets.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_temporal_timestamps.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_theta_sensitivity.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_time_utils.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_topology.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_topology_anomalies.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_topology_cache.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_topology_trajectory_anomalies.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_trajectory_active_windows.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_trajectory_ann.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_trajectory_detector.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_vector_index_health.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_witness_cohort.py +0 -0
- {hypertopos-0.9.0 → hypertopos-0.9.2}/tests/test_witness_set.py +0 -0
|
@@ -7,6 +7,36 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.9.2] — 2026-10-25
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- A Spark quickstart notebook (`docs/examples/spark-quickstart.ipynb`): labelled synthetic payments, `find_anomalies` per segment, a comparison with the labels, `explain`, `score` on a later period and the hand-off to an agent; runs on Databricks or a local Spark session.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- `sphere_of(result).lance_path()`, `mcp_config()` and `serve()` work on Windows: the sphere is copied into place without a directory rename, which Windows refused right after the copy.
|
|
19
|
+
- `find_anomalies` in a Databricks serverless notebook no longer fails with `CONFIG_NOT_AVAILABLE` for `hypertopos.sphere_root`: a Spark conf that the compute refuses to read counts as not set, and the calibration goes to `HYPERTOPOS_SPHERE_ROOT` or the driver's temp directory.
|
|
20
|
+
- The Spark docs and the quickstart notebook say what `No matching distribution found for hypertopos` means on Databricks (a Python older than 3.12) and how to pick a newer environment.
|
|
21
|
+
|
|
22
|
+
## [0.9.1] — 2026-10-25
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- `hypertopos.spark.find_anomalies(df, entity, counterparty, amount, time)`: one call from a Spark DataFrame of events to a DataFrame with one row per entity - distance from the population, anomaly flag, conformal p-value, rank and the top three reasons. The per-entity aggregation runs on the executors; the driver only calibrates the per-entity table and scores it. Optional per-group populations (`group_by`, with small groups pooled and a `calibration_scope` column), a calibration cache keyed by the input, and shares, spreads and ratios measured on few events pulled toward the population mean so that one or two events cannot make an entity extreme. Runs on classic clusters and on Spark Connect sessions, including Databricks serverless (environment 3 or later, Python 3.12). Install with `pip install "hypertopos[spark]"` (PySpark 3.5 or 4); see the new Spark page in the docs.
|
|
27
|
+
- `hypertopos.scoring.FrozenCalibration`: fit a population calibration on an entity table and score any table against it without recalibrating; plain numpy, picklable.
|
|
28
|
+
- `hypertopos spark find-anomalies` CLI: the same call from the shell, on a local Spark session or as a Databricks Python wheel task; reads a Parquet, CSV or Delta path or a table, and writes Parquet, a table, or prints the top entities (`--show N`).
|
|
29
|
+
- `hypertopos.spark.explain(result, entities=None, top_n=50, dims=None)`: one row per entity and dimension - value, population mean and standard deviation (the group's, with `group_by`), signed deviation, share of the distance and direction - for the most distant entities or named ones. `hypertopos.spark.sphere_of(result)` returns the calibration behind a result: its per-entity geometry as a table, written to a table with `to_delta`, or built into a navigable sphere for the MCP server (`lance_path`, `mcp_config`, `serve`) whose geometry matches the result.
|
|
30
|
+
- `hypertopos.spark.score(df, like=result)`: new events scored against a frozen calibration, aggregated and scored on the executors (no recalibration; `p_value` and `rank_pct` against the original population; unseen `group_by` groups fall back to the global calibration).
|
|
31
|
+
- `hypertopos.datasets.synthetic_aml(n_accounts, n_days, laundering_share, seed)`: a deterministic, labelled synthetic payments dataset (retail and business accounts; fan-out, fan-in, cycle, scatter-gather, structuring and pass-through laundering patterns) for trying the library and comparing detectors, generated on the fly.
|
|
32
|
+
- `kind:` on `precomputed_dimensions` entries in `sphere.yaml` (and `kind=` on `GDSBuilder.add_precomputed_dimension`) sets the Bregman kind of the dimension - `gaussian`, `poisson` or `bernoulli` - instead of the auto-detected one; an unknown value fails the config.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- On Linux the builder's memory budget now counts reclaimable page cache (`MemAvailable`), so a machine with a warm file cache no longer looks out of memory and builds are not split into needlessly small chunks.
|
|
37
|
+
- Patterns with `metric_properties`, `geo_properties` or `semantic_dim` blocks: `Pattern.dim_labels` and `Pattern.delta_dim()` now include the block dimensions (`t:<col>`, `g:<col>`, `s:pc<k>`), so labels and widths match the stored geometry; a pattern above the chunked-build size without `group_by` no longer drops those dimensions; and a pattern made only of dimension blocks is accepted.
|
|
38
|
+
- `explain_anomaly` and other polygon reads on patterns with `metric_properties`, `geo_properties` or `semantic_dim` blocks use the stored geometry instead of failing with a shape mismatch.
|
|
39
|
+
|
|
10
40
|
## [0.9.0] — 2026-10-25
|
|
11
41
|
|
|
12
42
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hypertopos
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.2
|
|
4
4
|
Summary: Understand the structure of your data — without training machine learning models
|
|
5
5
|
Project-URL: Homepage, https://github.com/hypertopos/hypertopos-py
|
|
6
6
|
Project-URL: Documentation, https://github.com/hypertopos/hypertopos-py/tree/main/docs
|
|
@@ -29,16 +29,21 @@ Provides-Extra: dev
|
|
|
29
29
|
Requires-Dist: filelock; extra == 'dev'
|
|
30
30
|
Requires-Dist: igraph>=0.11; extra == 'dev'
|
|
31
31
|
Requires-Dist: mypy==1.19.1; extra == 'dev'
|
|
32
|
+
Requires-Dist: pyspark<3.6,>=3.5; extra == 'dev'
|
|
32
33
|
Requires-Dist: pytest; extra == 'dev'
|
|
33
34
|
Requires-Dist: pytest-asyncio; extra == 'dev'
|
|
34
35
|
Requires-Dist: pytest-cov; extra == 'dev'
|
|
35
36
|
Requires-Dist: pytest-xdist; extra == 'dev'
|
|
36
37
|
Requires-Dist: ruff==0.15.2; extra == 'dev'
|
|
38
|
+
Requires-Dist: setuptools; (python_version >= '3.12') and extra == 'dev'
|
|
37
39
|
Provides-Extra: graph
|
|
38
40
|
Requires-Dist: igraph>=0.11; extra == 'graph'
|
|
39
41
|
Provides-Extra: mcp
|
|
40
42
|
Requires-Dist: mcp[cli]<3,>=2.3; extra == 'mcp'
|
|
41
43
|
Requires-Dist: tzdata>=2024.1; extra == 'mcp'
|
|
44
|
+
Provides-Extra: spark
|
|
45
|
+
Requires-Dist: pyspark<5,>=3.5; extra == 'spark'
|
|
46
|
+
Requires-Dist: setuptools; (python_version >= '3.12') and extra == 'spark'
|
|
42
47
|
Provides-Extra: topology
|
|
43
48
|
Requires-Dist: persim>=0.3.5; extra == 'topology'
|
|
44
49
|
Requires-Dist: ripser>=0.6.14; extra == 'topology'
|
|
@@ -56,7 +61,7 @@ Description-Content-Type: text/markdown
|
|
|
56
61
|
[](https://arrow.apache.org/docs/python/)
|
|
57
62
|
[](https://github.com/lance-format/lance)
|
|
58
63
|
[](https://modelcontextprotocol.io)
|
|
59
|
-
[](pyproject.toml)
|
|
60
65
|
|
|
61
66
|
Describe your tables in a YAML file, run `hypertopos build`, and every entity gets a position relative to its own population — so "out of place", "similar", "drifting" and "different between groups" become distances you read instead of queries you write.
|
|
62
67
|
|
|
@@ -162,6 +167,60 @@ my_sphere/
|
|
|
162
167
|
> [!NOTE]
|
|
163
168
|
> Paths under `sources:` resolve relative to the YAML file, not to your shell. On Windows, `python -m hypertopos.cli.main …` works wherever the `hypertopos` script is not on `PATH`.
|
|
164
169
|
|
|
170
|
+
## On Spark
|
|
171
|
+
|
|
172
|
+
Event tables that live in Spark (Databricks, a lakehouse, a cluster) need no YAML: one call goes from a DataFrame of events to a DataFrame of scored entities. The per-entity aggregation runs on the executors, and only one row per entity reaches the driver.
|
|
173
|
+
|
|
174
|
+
<p align="center">
|
|
175
|
+
<img src="docs/images/demo-spark.svg" width="900"
|
|
176
|
+
alt="A data scientist calls find_anomalies on a Spark DataFrame of payment events grouped by segment, shows the three most distant entities, then calls explain on A000335">
|
|
177
|
+
</p>
|
|
178
|
+
<p align="center"><sub><i>Real output on synthetic data. A000335 lands outside its segment's θ ring; <code>explain</code> names the dimension that moved it.</i></sub></p>
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
pip install "hypertopos[spark]" # on Databricks, plain `%pip install hypertopos`
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
from hypertopos.datasets import synthetic_aml
|
|
186
|
+
from hypertopos.spark import explain, find_anomalies
|
|
187
|
+
|
|
188
|
+
data = synthetic_aml(n_accounts=1_000, seed=1) # labelled synthetic payments, no download
|
|
189
|
+
tx = spark.createDataFrame(data.transactions.to_pandas())
|
|
190
|
+
accounts = spark.createDataFrame(data.accounts.to_pandas())
|
|
191
|
+
events = tx.join(accounts.select("account", "segment"), tx.from_account == accounts.account)
|
|
192
|
+
|
|
193
|
+
result = find_anomalies(events, entity="from_account", counterparty="to_account",
|
|
194
|
+
amount="amount", time="ts", group_by="segment")
|
|
195
|
+
result.orderBy("delta_norm", ascending=False).show(3)
|
|
196
|
+
explain(result, entities=["A000335"]).show(2)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Real output (local Spark, two cores):
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
+-------+----------+----------+-------+--------+------------------------------------------+
|
|
203
|
+
| entity|delta_norm|is_anomaly|p_value| segment| top_reasons|
|
|
204
|
+
+-------+----------+----------+-------+--------+------------------------------------------+
|
|
205
|
+
|A000335| 10.616| true| 0.001| retail|[{burst_max_daily, 5.31, 25.0}, ... |
|
|
206
|
+
|A000259| 10.215| true| 0.002| retail|[{n_out, 5.62, 30.3}, ... |
|
|
207
|
+
|A000179| 10.208| true| 0.003|business|[{active_days, -4.80, 22.1}, ... |
|
|
208
|
+
+-------+----------+----------+-------+--------+------------------------------------------+
|
|
209
|
+
|
|
210
|
+
+-------+---------------+-----+-----+-----+-----+------------+---------+
|
|
211
|
+
|entity |dim |value|mu |sigma|delta|pct_of_total|direction|
|
|
212
|
+
+-------+---------------+-----+-----+-----+-----+------------+---------+
|
|
213
|
+
|A000335|burst_max_daily|20.0 |7.683|2.320|5.31 |25.0 |above |
|
|
214
|
+
|A000335|amount_std_out |7.583|3.771|0.779|4.89 |21.2 |above |
|
|
215
|
+
+-------+---------------+-----+-----+-----+-----+------------+---------+
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`A000335` made 20 payments on its busiest day, against about 8 for retail accounts (+5.3 σ), and its payment amounts vary far more than theirs (`amount_std_out` is log-scaled). In the labels of `data.accounts` it is one of the planted fan-out accounts. The other two are not laundering, only unusual, which is what a distance from the population measures: `p_value` 0.001 means one entity in a thousand is as far out. The labels let you measure how a detector does on the same data. `explain` shows where every number comes from. `score(new_events, like=result)` scores the next batch against this calibration on the executors, and `sphere_of(result)` hands the same geometry to an agent through the MCP server.
|
|
219
|
+
> [!TIP]
|
|
220
|
+
> On a laptop, create the session with `.master("local[2]").config("spark.sql.shuffle.partitions", "4")`. The default of 200 shuffle partitions is sized for a cluster.
|
|
221
|
+
|
|
222
|
+
Everything else, from column roles and the 23 dimensions to time handling, the cache and the CLI, is in [hypertopos on Spark](docs/spark.md). To run the whole flow on labelled synthetic data, open the [quickstart notebook](docs/examples/spark-quickstart.ipynb) on Databricks or locally.
|
|
223
|
+
|
|
165
224
|
## See it in action
|
|
166
225
|
|
|
167
226
|
Real output from the YAML above on a 40-customer, 313-order CSV pair (three orders carry amounts an order of magnitude above the rest):
|
|
@@ -280,6 +339,9 @@ hypertopos sphere branch list my_sphere # what exists, w
|
|
|
280
339
|
hypertopos sphere branch create my_sphere trial --purpose "recalibrate without top anomaly"
|
|
281
340
|
hypertopos sphere branch promote my_sphere trial # replay the branch's recipes on main, then delete it
|
|
282
341
|
hypertopos sphere branch delete my_sphere trial # discard it; main is unchanged
|
|
342
|
+
|
|
343
|
+
hypertopos spark find-anomalies --input tx.parquet --entity from_account \
|
|
344
|
+
--counterparty to_account --amount amount --time ts --show 20 # events on Spark -> scored entities (hypertopos[spark])
|
|
283
345
|
```
|
|
284
346
|
|
|
285
347
|
Every `hypertopos sphere …` subcommand takes `--json` for machine-readable output. See the [full command reference](#full-command-reference) below.
|
|
@@ -313,7 +375,7 @@ Every capability below comes from one decision: an entity is a point in a space
|
|
|
313
375
|
|---|---|---|
|
|
314
376
|
| Population-relative coordinates | `delta = (shape − μ) / σ` per dimension; one vector serves anomaly, clustering, drift and comparison | Learned embeddings with latent dimensions that need retraining when the data shifts |
|
|
315
377
|
| Self-calibrating threshold | θ is a percentile of the population's own `delta_norm` — per group when a pattern declares `group_by_property` | Choosing a contamination rate or a hand-tuned cut-off |
|
|
316
|
-
| Distribution-aware dimensions | Each dimension declares a kind (gaussian, poisson, bernoulli); `sphere_overview` flags dead, sparse, non-normal and dominant dimensions with concrete advice | One metric for every feature, and silent garbage when a feature breaks its assumption |
|
|
378
|
+
| Distribution-aware dimensions | Each dimension declares a kind (gaussian, poisson, bernoulli), detected or set with `kind:` on precomputed dimensions; `sphere_overview` flags dead, sparse, non-normal and dominant dimensions with concrete advice | One metric for every feature, and silent garbage when a feature breaks its assumption |
|
|
317
379
|
| Calibration epochs | Every build and `recalibrate` records an epoch; `compare_calibrations` shows per-dimension μ / σ / θ drift, `theta_sensitivity` shows whether θ sits near a cliff | No record of how the coordinate system itself moved |
|
|
318
380
|
| Hidden influencers | `find_calibration_influencers` measures each entity's leave-one-out impact on the calibration and names the ones that distort it | Explaining predictions while the baseline itself is contaminated |
|
|
319
381
|
|
|
@@ -360,6 +422,9 @@ Every capability below comes from one decision: an entity is a point in a space
|
|
|
360
422
|
| Incremental ingest | `incremental_update` adds, changes and removes entities without a full rebuild | Rebuilding from source for every batch |
|
|
361
423
|
| Operations CLI | `hypertopos sphere health`, `validate` and `diff` give CI gates and drift between two builds; vector-index health is reported before trajectory tools rely on it | Hand-rolled validation scripts |
|
|
362
424
|
| An agent surface | The MCP server runs over stdio or HTTP with a session per agent and compact, strict-JSON responses sized for a context window | Wrapping a database in ad-hoc tools |
|
|
425
|
+
| On Spark | `hypertopos.spark.find_anomalies(df, entity=…, …)` aggregates events per entity on the executors and returns a DataFrame with distance, flag, p-value and reasons; `explain` breaks it down, `sphere_of` hands it to an agent. Runs on classic clusters and Spark Connect, including Databricks serverless | Exporting events, building features by hand and running a detector outside the cluster |
|
|
426
|
+
| Frozen scoring | `FrozenCalibration` (and `hypertopos.spark.score` on executors) scores new data against a fixed population without recalibrating | Refitting the detector on every batch, so yesterday's and today's scores are not comparable |
|
|
427
|
+
| Data to try it on | `hypertopos.datasets.synthetic_aml` generates a labelled payments dataset with six laundering patterns | Hunting for a public dataset before the first call |
|
|
363
428
|
|
|
364
429
|
### What changes in practice
|
|
365
430
|
|
|
@@ -434,6 +499,7 @@ See [Configuration](docs/configuration.md) for the YAML reference and [Container
|
|
|
434
499
|
| [Data Format](docs/data-format.md) | On-disk storage format, including branches |
|
|
435
500
|
| [External Chains as Anchor Lines](docs/external-chains-as-anchor-line.md) | Ingest chains found by other systems and investigate them with the same primitives |
|
|
436
501
|
| [Container Deployment](docs/container-deployment.md) | Run the CLI as one-shot container jobs |
|
|
502
|
+
| [hypertopos on Spark](docs/spark.md) | From a Spark DataFrame of events to per-entity geometry |
|
|
437
503
|
| [Benchmarks](docs/benchmarks.md) | Validation results — banking, AML, transportation |
|
|
438
504
|
|
|
439
505
|
## Status
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
[](https://arrow.apache.org/docs/python/)
|
|
10
10
|
[](https://github.com/lance-format/lance)
|
|
11
11
|
[](https://modelcontextprotocol.io)
|
|
12
|
-
[](pyproject.toml)
|
|
13
13
|
|
|
14
14
|
Describe your tables in a YAML file, run `hypertopos build`, and every entity gets a position relative to its own population — so "out of place", "similar", "drifting" and "different between groups" become distances you read instead of queries you write.
|
|
15
15
|
|
|
@@ -115,6 +115,60 @@ my_sphere/
|
|
|
115
115
|
> [!NOTE]
|
|
116
116
|
> Paths under `sources:` resolve relative to the YAML file, not to your shell. On Windows, `python -m hypertopos.cli.main …` works wherever the `hypertopos` script is not on `PATH`.
|
|
117
117
|
|
|
118
|
+
## On Spark
|
|
119
|
+
|
|
120
|
+
Event tables that live in Spark (Databricks, a lakehouse, a cluster) need no YAML: one call goes from a DataFrame of events to a DataFrame of scored entities. The per-entity aggregation runs on the executors, and only one row per entity reaches the driver.
|
|
121
|
+
|
|
122
|
+
<p align="center">
|
|
123
|
+
<img src="docs/images/demo-spark.svg" width="900"
|
|
124
|
+
alt="A data scientist calls find_anomalies on a Spark DataFrame of payment events grouped by segment, shows the three most distant entities, then calls explain on A000335">
|
|
125
|
+
</p>
|
|
126
|
+
<p align="center"><sub><i>Real output on synthetic data. A000335 lands outside its segment's θ ring; <code>explain</code> names the dimension that moved it.</i></sub></p>
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
pip install "hypertopos[spark]" # on Databricks, plain `%pip install hypertopos`
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from hypertopos.datasets import synthetic_aml
|
|
134
|
+
from hypertopos.spark import explain, find_anomalies
|
|
135
|
+
|
|
136
|
+
data = synthetic_aml(n_accounts=1_000, seed=1) # labelled synthetic payments, no download
|
|
137
|
+
tx = spark.createDataFrame(data.transactions.to_pandas())
|
|
138
|
+
accounts = spark.createDataFrame(data.accounts.to_pandas())
|
|
139
|
+
events = tx.join(accounts.select("account", "segment"), tx.from_account == accounts.account)
|
|
140
|
+
|
|
141
|
+
result = find_anomalies(events, entity="from_account", counterparty="to_account",
|
|
142
|
+
amount="amount", time="ts", group_by="segment")
|
|
143
|
+
result.orderBy("delta_norm", ascending=False).show(3)
|
|
144
|
+
explain(result, entities=["A000335"]).show(2)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Real output (local Spark, two cores):
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
+-------+----------+----------+-------+--------+------------------------------------------+
|
|
151
|
+
| entity|delta_norm|is_anomaly|p_value| segment| top_reasons|
|
|
152
|
+
+-------+----------+----------+-------+--------+------------------------------------------+
|
|
153
|
+
|A000335| 10.616| true| 0.001| retail|[{burst_max_daily, 5.31, 25.0}, ... |
|
|
154
|
+
|A000259| 10.215| true| 0.002| retail|[{n_out, 5.62, 30.3}, ... |
|
|
155
|
+
|A000179| 10.208| true| 0.003|business|[{active_days, -4.80, 22.1}, ... |
|
|
156
|
+
+-------+----------+----------+-------+--------+------------------------------------------+
|
|
157
|
+
|
|
158
|
+
+-------+---------------+-----+-----+-----+-----+------------+---------+
|
|
159
|
+
|entity |dim |value|mu |sigma|delta|pct_of_total|direction|
|
|
160
|
+
+-------+---------------+-----+-----+-----+-----+------------+---------+
|
|
161
|
+
|A000335|burst_max_daily|20.0 |7.683|2.320|5.31 |25.0 |above |
|
|
162
|
+
|A000335|amount_std_out |7.583|3.771|0.779|4.89 |21.2 |above |
|
|
163
|
+
+-------+---------------+-----+-----+-----+-----+------------+---------+
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`A000335` made 20 payments on its busiest day, against about 8 for retail accounts (+5.3 σ), and its payment amounts vary far more than theirs (`amount_std_out` is log-scaled). In the labels of `data.accounts` it is one of the planted fan-out accounts. The other two are not laundering, only unusual, which is what a distance from the population measures: `p_value` 0.001 means one entity in a thousand is as far out. The labels let you measure how a detector does on the same data. `explain` shows where every number comes from. `score(new_events, like=result)` scores the next batch against this calibration on the executors, and `sphere_of(result)` hands the same geometry to an agent through the MCP server.
|
|
167
|
+
> [!TIP]
|
|
168
|
+
> On a laptop, create the session with `.master("local[2]").config("spark.sql.shuffle.partitions", "4")`. The default of 200 shuffle partitions is sized for a cluster.
|
|
169
|
+
|
|
170
|
+
Everything else, from column roles and the 23 dimensions to time handling, the cache and the CLI, is in [hypertopos on Spark](docs/spark.md). To run the whole flow on labelled synthetic data, open the [quickstart notebook](docs/examples/spark-quickstart.ipynb) on Databricks or locally.
|
|
171
|
+
|
|
118
172
|
## See it in action
|
|
119
173
|
|
|
120
174
|
Real output from the YAML above on a 40-customer, 313-order CSV pair (three orders carry amounts an order of magnitude above the rest):
|
|
@@ -233,6 +287,9 @@ hypertopos sphere branch list my_sphere # what exists, w
|
|
|
233
287
|
hypertopos sphere branch create my_sphere trial --purpose "recalibrate without top anomaly"
|
|
234
288
|
hypertopos sphere branch promote my_sphere trial # replay the branch's recipes on main, then delete it
|
|
235
289
|
hypertopos sphere branch delete my_sphere trial # discard it; main is unchanged
|
|
290
|
+
|
|
291
|
+
hypertopos spark find-anomalies --input tx.parquet --entity from_account \
|
|
292
|
+
--counterparty to_account --amount amount --time ts --show 20 # events on Spark -> scored entities (hypertopos[spark])
|
|
236
293
|
```
|
|
237
294
|
|
|
238
295
|
Every `hypertopos sphere …` subcommand takes `--json` for machine-readable output. See the [full command reference](#full-command-reference) below.
|
|
@@ -266,7 +323,7 @@ Every capability below comes from one decision: an entity is a point in a space
|
|
|
266
323
|
|---|---|---|
|
|
267
324
|
| Population-relative coordinates | `delta = (shape − μ) / σ` per dimension; one vector serves anomaly, clustering, drift and comparison | Learned embeddings with latent dimensions that need retraining when the data shifts |
|
|
268
325
|
| Self-calibrating threshold | θ is a percentile of the population's own `delta_norm` — per group when a pattern declares `group_by_property` | Choosing a contamination rate or a hand-tuned cut-off |
|
|
269
|
-
| Distribution-aware dimensions | Each dimension declares a kind (gaussian, poisson, bernoulli); `sphere_overview` flags dead, sparse, non-normal and dominant dimensions with concrete advice | One metric for every feature, and silent garbage when a feature breaks its assumption |
|
|
326
|
+
| Distribution-aware dimensions | Each dimension declares a kind (gaussian, poisson, bernoulli), detected or set with `kind:` on precomputed dimensions; `sphere_overview` flags dead, sparse, non-normal and dominant dimensions with concrete advice | One metric for every feature, and silent garbage when a feature breaks its assumption |
|
|
270
327
|
| Calibration epochs | Every build and `recalibrate` records an epoch; `compare_calibrations` shows per-dimension μ / σ / θ drift, `theta_sensitivity` shows whether θ sits near a cliff | No record of how the coordinate system itself moved |
|
|
271
328
|
| Hidden influencers | `find_calibration_influencers` measures each entity's leave-one-out impact on the calibration and names the ones that distort it | Explaining predictions while the baseline itself is contaminated |
|
|
272
329
|
|
|
@@ -313,6 +370,9 @@ Every capability below comes from one decision: an entity is a point in a space
|
|
|
313
370
|
| Incremental ingest | `incremental_update` adds, changes and removes entities without a full rebuild | Rebuilding from source for every batch |
|
|
314
371
|
| Operations CLI | `hypertopos sphere health`, `validate` and `diff` give CI gates and drift between two builds; vector-index health is reported before trajectory tools rely on it | Hand-rolled validation scripts |
|
|
315
372
|
| An agent surface | The MCP server runs over stdio or HTTP with a session per agent and compact, strict-JSON responses sized for a context window | Wrapping a database in ad-hoc tools |
|
|
373
|
+
| On Spark | `hypertopos.spark.find_anomalies(df, entity=…, …)` aggregates events per entity on the executors and returns a DataFrame with distance, flag, p-value and reasons; `explain` breaks it down, `sphere_of` hands it to an agent. Runs on classic clusters and Spark Connect, including Databricks serverless | Exporting events, building features by hand and running a detector outside the cluster |
|
|
374
|
+
| Frozen scoring | `FrozenCalibration` (and `hypertopos.spark.score` on executors) scores new data against a fixed population without recalibrating | Refitting the detector on every batch, so yesterday's and today's scores are not comparable |
|
|
375
|
+
| Data to try it on | `hypertopos.datasets.synthetic_aml` generates a labelled payments dataset with six laundering patterns | Hunting for a public dataset before the first call |
|
|
316
376
|
|
|
317
377
|
### What changes in practice
|
|
318
378
|
|
|
@@ -387,6 +447,7 @@ See [Configuration](docs/configuration.md) for the YAML reference and [Container
|
|
|
387
447
|
| [Data Format](docs/data-format.md) | On-disk storage format, including branches |
|
|
388
448
|
| [External Chains as Anchor Lines](docs/external-chains-as-anchor-line.md) | Ingest chains found by other systems and investigate them with the same primitives |
|
|
389
449
|
| [Container Deployment](docs/container-deployment.md) | Run the CLI as one-shot container jobs |
|
|
450
|
+
| [hypertopos on Spark](docs/spark.md) | From a Spark DataFrame of events to per-entity geometry |
|
|
390
451
|
| [Benchmarks](docs/benchmarks.md) | Validation results — banking, AML, transportation |
|
|
391
452
|
|
|
392
453
|
## Status
|
|
@@ -21,4 +21,5 @@ hypertopos turns relational data into navigable geometric space — a coordinate
|
|
|
21
21
|
| [Data Format](data-format.md) | On-disk layout, Lance schemas, branches, sphere.json |
|
|
22
22
|
| [External Chains as Anchor Lines](external-chains-as-anchor-line.md) | Ingesting chains found by other systems and navigating them with the same primitives |
|
|
23
23
|
| [Container Deployment](container-deployment.md) | Running the CLI as one-shot container jobs |
|
|
24
|
+
| [hypertopos on Spark](spark.md) | From a Spark DataFrame of events to per-entity geometry: column roles, dimensions, time handling |
|
|
24
25
|
| [Benchmarks](benchmarks.md) | Validation results — banking, AML, transportation |
|
|
@@ -263,7 +263,7 @@ builder = GDSBuilder(
|
|
|
263
263
|
| `add_event_dimension(pattern_id, column, edge_max)` | Add a continuous dimension to an event pattern (amounts, quantities) |
|
|
264
264
|
| `add_derived_dimension(anchor_line, event_line, anchor_fk, metric, metric_col, dimension_name)` | Dimension derived from event aggregation (count, sum, max, std, mean) |
|
|
265
265
|
| `add_composite_line(anchor_line, event_line, anchor_fk, ...)` | Create composite anchor line from event-anchor join |
|
|
266
|
-
| `add_precomputed_dimension(anchor_line, dimension_name, edge_max)` | Dimension from a column already on the entity table |
|
|
266
|
+
| `add_precomputed_dimension(anchor_line, dimension_name, edge_max="auto", percentile=99.0, display_name=None, kind=None)` | Dimension from a column already on the entity table; `kind` overrides the Bregman kind (`gaussian`, `poisson`, `bernoulli`) |
|
|
267
267
|
| `add_graph_features(anchor_line, event_line, from_col, to_col, features)` | Auto-compute graph structural features (degree, reciprocity, pagerank, betweenness, community, clustering, components) |
|
|
268
268
|
| `add_chain_line(line_id, chains, features)` | Create anchor line from extracted chain dicts |
|
|
269
269
|
| `add_alias(alias_id, base_pattern_id, cutting_plane_dimension, cutting_plane_threshold)` | Register an alias with a cutting plane for sub-population analysis |
|
|
@@ -102,7 +102,7 @@ Every dimension in a pattern carries a `kind` tag — the distribution family us
|
|
|
102
102
|
| `graph_features` pagerank, betweenness, clustering_coefficient | auto |
|
|
103
103
|
| `graph_features` community, connected_component | auto |
|
|
104
104
|
|
|
105
|
-
Override via `kind:` on
|
|
105
|
+
Override via `kind:` on precomputed dimension entries in `sphere.yaml`. The resulting kind list is stored in `Pattern.dimension_kinds` and surfaced in `sphere_overview` as `dimension_kinds`.
|
|
106
106
|
|
|
107
107
|
### Dim-quality warnings
|
|
108
108
|
|
|
@@ -728,7 +728,7 @@ The builder assigns a `kind` tag to every dimension at build time. The tag drive
|
|
|
728
728
|
| `graph_features` (`community`, `connected_component`) | auto | Integer IDs (categorical) |
|
|
729
729
|
| `geo_properties`, `metric_properties` | `gaussian` | Continuous measurements |
|
|
730
730
|
|
|
731
|
-
**Override via `kind:`** on
|
|
731
|
+
**Override via `kind:`** on precomputed dimension entries (`gaussian`, `poisson` or `bernoulli`). The override takes precedence over auto-detection; an unknown value fails the config. Derived and event dimensions use auto-detection only.
|
|
732
732
|
|
|
733
733
|
---
|
|
734
734
|
|
|
@@ -788,6 +788,8 @@ patterns:
|
|
|
788
788
|
- column: structuring_pct
|
|
789
789
|
edge_max: 1
|
|
790
790
|
display_name: structuring # Optional label.
|
|
791
|
+
- column: n_counterparties
|
|
792
|
+
kind: poisson # Optional Bregman kind override.
|
|
791
793
|
```
|
|
792
794
|
|
|
793
795
|
| Field | Type | Default | Description |
|
|
@@ -796,6 +798,7 @@ patterns:
|
|
|
796
798
|
| `edge_max` | int or `"auto"` | `"auto"` | Fixed normalization cap, or auto-compute from percentile. |
|
|
797
799
|
| `percentile` | float | `99.0` | Percentile for auto edge_max computation. |
|
|
798
800
|
| `display_name` | string | `null` | Human-readable dimension label. |
|
|
801
|
+
| `kind` | string | auto | Bregman kind: `gaussian`, `poisson` or `bernoulli`. Auto gives `bernoulli` for `edge_max: 1` and `gaussian` otherwise. |
|
|
799
802
|
|
|
800
803
|
---
|
|
801
804
|
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cells": [
|
|
3
|
+
{
|
|
4
|
+
"cell_type": "markdown",
|
|
5
|
+
"id": "cell-00",
|
|
6
|
+
"metadata": {},
|
|
7
|
+
"source": [
|
|
8
|
+
"# hypertopos on Spark: quickstart\n",
|
|
9
|
+
"\n",
|
|
10
|
+
"From a table of payments to the accounts that stand out from their population, why they do, and how to keep scoring new data against the same baseline. Runs on Databricks (Runtime 16+ or serverless environment 3+) or on a local Spark session. The data is synthetic and labelled, so you can see which flagged accounts take part in a laundering pattern."
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"cell_type": "markdown",
|
|
15
|
+
"id": "cell-01",
|
|
16
|
+
"metadata": {},
|
|
17
|
+
"source": [
|
|
18
|
+
"On Databricks PySpark is already installed, so install hypertopos alone. hypertopos needs Python 3.12: on serverless, pick environment version 3 or later in the **Environment** side panel first. Locally, install it with the Spark extra instead: `%pip install \"hypertopos[spark]\"`."
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"cell_type": "code",
|
|
23
|
+
"execution_count": null,
|
|
24
|
+
"id": "cell-02",
|
|
25
|
+
"metadata": {},
|
|
26
|
+
"outputs": [],
|
|
27
|
+
"source": [
|
|
28
|
+
"%pip install hypertopos"
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"cell_type": "code",
|
|
33
|
+
"execution_count": null,
|
|
34
|
+
"id": "cell-03",
|
|
35
|
+
"metadata": {},
|
|
36
|
+
"outputs": [],
|
|
37
|
+
"source": [
|
|
38
|
+
"# On Databricks `spark` already exists. Locally, start a small session:\n",
|
|
39
|
+
"# few shuffle partitions, because the default of 200 is sized for a cluster.\n",
|
|
40
|
+
"if \"spark\" not in globals():\n",
|
|
41
|
+
" from pyspark.sql import SparkSession\n",
|
|
42
|
+
"\n",
|
|
43
|
+
" spark = (\n",
|
|
44
|
+
" SparkSession.builder.master(\"local[2]\")\n",
|
|
45
|
+
" .config(\"spark.sql.shuffle.partitions\", \"4\")\n",
|
|
46
|
+
" .getOrCreate()\n",
|
|
47
|
+
" )"
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"cell_type": "markdown",
|
|
52
|
+
"id": "cell-04",
|
|
53
|
+
"metadata": {},
|
|
54
|
+
"source": [
|
|
55
|
+
"## 1. Data\n",
|
|
56
|
+
"\n",
|
|
57
|
+
"`synthetic_aml` generates retail and business accounts and their payments over 60 days. About 1 % of the accounts take part in one of six laundering patterns (fan-out, fan-in, cycle, scatter-gather, structuring, pass-through). Each payment carries the sender's segment, so the accounts can be compared within their segment."
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"cell_type": "code",
|
|
62
|
+
"execution_count": null,
|
|
63
|
+
"id": "cell-05",
|
|
64
|
+
"metadata": {},
|
|
65
|
+
"outputs": [],
|
|
66
|
+
"source": [
|
|
67
|
+
"from hypertopos.datasets import synthetic_aml\n",
|
|
68
|
+
"\n",
|
|
69
|
+
"data = synthetic_aml(n_accounts=5_000, seed=1)\n",
|
|
70
|
+
"tx = spark.createDataFrame(data.transactions.to_pandas())\n",
|
|
71
|
+
"accounts = spark.createDataFrame(data.accounts.to_pandas())\n",
|
|
72
|
+
"events = tx.join(accounts.select(\"account\", \"segment\"), tx.from_account == accounts.account)\n",
|
|
73
|
+
"events.select(\"ts\", \"from_account\", \"to_account\", \"amount\", \"segment\").show(5)"
|
|
74
|
+
]
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"cell_type": "markdown",
|
|
78
|
+
"id": "cell-06",
|
|
79
|
+
"metadata": {},
|
|
80
|
+
"source": [
|
|
81
|
+
"## 2. Find anomalies\n",
|
|
82
|
+
"\n",
|
|
83
|
+
"Four column roles: who the event belongs to, the other side, the amount and the time. `group_by=\"segment\"` compares business accounts with businesses and retail with retail."
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"cell_type": "code",
|
|
88
|
+
"execution_count": null,
|
|
89
|
+
"id": "cell-07",
|
|
90
|
+
"metadata": {},
|
|
91
|
+
"outputs": [],
|
|
92
|
+
"source": [
|
|
93
|
+
"from hypertopos.spark import find_anomalies\n",
|
|
94
|
+
"\n",
|
|
95
|
+
"result = find_anomalies(\n",
|
|
96
|
+
" events,\n",
|
|
97
|
+
" entity=\"from_account\",\n",
|
|
98
|
+
" counterparty=\"to_account\",\n",
|
|
99
|
+
" amount=\"amount\",\n",
|
|
100
|
+
" time=\"ts\",\n",
|
|
101
|
+
" group_by=\"segment\",\n",
|
|
102
|
+
")\n",
|
|
103
|
+
"result.orderBy(\"delta_norm\", ascending=False).show(10, truncate=False)"
|
|
104
|
+
]
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"cell_type": "markdown",
|
|
108
|
+
"id": "cell-08",
|
|
109
|
+
"metadata": {},
|
|
110
|
+
"source": [
|
|
111
|
+
"`delta_norm` is the distance from the segment's centre in standard deviations, `is_anomaly` marks the accounts beyond the segment's 99th percentile, `p_value` is the share of the population at least as far out, and `top_reasons` names the dimensions that move the account most.\n",
|
|
112
|
+
"\n",
|
|
113
|
+
"## 3. Compare with the labels\n",
|
|
114
|
+
"\n",
|
|
115
|
+
"The threshold flags about 1 % of every population by construction, so the flagged set always has a size; the labels show what it contains."
|
|
116
|
+
]
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"cell_type": "code",
|
|
120
|
+
"execution_count": null,
|
|
121
|
+
"id": "cell-09",
|
|
122
|
+
"metadata": {},
|
|
123
|
+
"outputs": [],
|
|
124
|
+
"source": [
|
|
125
|
+
"from pyspark.sql import functions as F\n",
|
|
126
|
+
"\n",
|
|
127
|
+
"labels = accounts.select(F.col(\"account\").alias(\"entity\"), \"is_laundering_account\", \"typology\")\n",
|
|
128
|
+
"flagged = result.where(\"is_anomaly\").join(labels, \"entity\", \"left\")\n",
|
|
129
|
+
"flagged.groupBy(\"segment\", \"is_laundering_account\").count().orderBy(\"segment\").show()\n",
|
|
130
|
+
"flagged.where(\"is_laundering_account\").groupBy(\"typology\").count().show()"
|
|
131
|
+
]
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"cell_type": "markdown",
|
|
135
|
+
"id": "cell-10",
|
|
136
|
+
"metadata": {},
|
|
137
|
+
"source": [
|
|
138
|
+
"## 4. Why an account stands out\n",
|
|
139
|
+
"\n",
|
|
140
|
+
"`explain` gives one row per account and dimension: the value, the segment's mean and standard deviation, the signed deviation and its share of the distance."
|
|
141
|
+
]
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"cell_type": "code",
|
|
145
|
+
"execution_count": null,
|
|
146
|
+
"id": "cell-11",
|
|
147
|
+
"metadata": {},
|
|
148
|
+
"outputs": [],
|
|
149
|
+
"source": [
|
|
150
|
+
"from hypertopos.spark import explain\n",
|
|
151
|
+
"\n",
|
|
152
|
+
"explain(result, top_n=3).show(15, truncate=False)"
|
|
153
|
+
]
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"cell_type": "markdown",
|
|
157
|
+
"id": "cell-12",
|
|
158
|
+
"metadata": {},
|
|
159
|
+
"source": [
|
|
160
|
+
"## 5. Score the next period against the same baseline\n",
|
|
161
|
+
"\n",
|
|
162
|
+
"`score` measures new events against the population `result` was calibrated on, without recalibrating, so scores stay comparable from one period to the next. Here a second synthetic period stands in for next month's payments."
|
|
163
|
+
]
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"cell_type": "code",
|
|
167
|
+
"execution_count": null,
|
|
168
|
+
"id": "cell-13",
|
|
169
|
+
"metadata": {},
|
|
170
|
+
"outputs": [],
|
|
171
|
+
"source": [
|
|
172
|
+
"from hypertopos.spark import score\n",
|
|
173
|
+
"\n",
|
|
174
|
+
"later = synthetic_aml(n_accounts=5_000, seed=2)\n",
|
|
175
|
+
"later_tx = spark.createDataFrame(later.transactions.to_pandas())\n",
|
|
176
|
+
"later_accounts = spark.createDataFrame(later.accounts.to_pandas())\n",
|
|
177
|
+
"later_events = later_tx.join(\n",
|
|
178
|
+
" later_accounts.select(\"account\", \"segment\"), later_tx.from_account == later_accounts.account\n",
|
|
179
|
+
")\n",
|
|
180
|
+
"scored = score(later_events, like=result)\n",
|
|
181
|
+
"scored.where(\"is_anomaly\").orderBy(\"delta_norm\", ascending=False).show(10, truncate=False)"
|
|
182
|
+
]
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"cell_type": "markdown",
|
|
186
|
+
"id": "cell-14",
|
|
187
|
+
"metadata": {},
|
|
188
|
+
"source": [
|
|
189
|
+
"## 6. Hand the result to an agent\n",
|
|
190
|
+
"\n",
|
|
191
|
+
"`sphere_of` returns the calibration behind the result: its geometry as a table, or a sphere for the [hypertopos MCP server](https://github.com/hypertopos/hypertopos-mcp) with the same distances and flags, so an agent can investigate the flagged accounts."
|
|
192
|
+
]
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"cell_type": "code",
|
|
196
|
+
"execution_count": null,
|
|
197
|
+
"id": "cell-15",
|
|
198
|
+
"metadata": {},
|
|
199
|
+
"outputs": [],
|
|
200
|
+
"source": [
|
|
201
|
+
"from hypertopos.spark import sphere_of\n",
|
|
202
|
+
"\n",
|
|
203
|
+
"sphere = sphere_of(result)\n",
|
|
204
|
+
"geometry = sphere.geometry() # pyarrow table: values, deviations and scores per account\n",
|
|
205
|
+
"print(geometry.num_rows, \"accounts,\", len(geometry.column_names), \"columns\")\n",
|
|
206
|
+
"sphere.mcp_config() # put under mcpServers in an MCP client's configuration"
|
|
207
|
+
]
|
|
208
|
+
}
|
|
209
|
+
],
|
|
210
|
+
"metadata": {
|
|
211
|
+
"kernelspec": {
|
|
212
|
+
"display_name": "Python 3",
|
|
213
|
+
"language": "python",
|
|
214
|
+
"name": "python3"
|
|
215
|
+
},
|
|
216
|
+
"language_info": {
|
|
217
|
+
"name": "python"
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
"nbformat": 4,
|
|
221
|
+
"nbformat_minor": 5
|
|
222
|
+
}
|