spine_compare¶
Run the same data under several measures and see whether they agree.
The measure is the modelling step, and two defensible measures can call opposite objects atypical. Nothing else in the package lets you see that happening.
Syntax and options¶
from entropy_spine import spine_compare
help(spine_compare)
Help on function spine_compare in module entropy_spine.interface.compare:
spine_compare(
data,
measures,
orientation=None,
*,
report: 'bool' = True,
**options
)
spine_compare: run the same data under several measures and see
whether they agree.
table, detail = spine_compare(data, measures, orientation)
The measure is the modelling step, and two defensible measures can call
opposite objects atypical. Nothing else in the package lets you see that
happening. This does.
``table`` is a list of dicts, one per measure: how many strata it found, how
many objects it flagged, and the first cut. ``detail`` carries the rest:
``results`` (one Result per measure), ``measures``, ``agreement`` (pairwise
Jaccard overlap of the flagged sets), and ``flagged`` (one boolean column per
measure). A report is printed unless ``report=False``.
AGREEMENT IS THE POINT. If two measures flag the same objects, the structure
is robust to how you asked. If they disagree, the disagreement is telling you
something real about your data, and choosing between them is a scientific
decision rather than a default.
When measures disagree¶
Three planted groups, chosen so that “far from the centre” and “in a sparse neighbourhood” are genuinely different questions: a bulk, a halo displaced a little and spread widely, and an island displaced a long way and packed tight.
%matplotlib inline
import numpy as np
from entropy_spine import spine_compare
rng = np.random.default_rng(4242)
bulk = rng.standard_normal((200, 2))
ang = 2 * np.pi * rng.random(30)
halo = 5 * np.column_stack([np.cos(ang), np.sin(ang)]) \
+ 2 * rng.standard_normal((30, 2))
island = [12, 12] + 0.25 * rng.standard_normal((20, 2))
D = np.vstack([bulk, halo, island])
table, detail = spine_compare(
D, ["dist2mean", "robustrownorm", "knn", "dist2median"], "upper", k=5)
SPINE comparison over 4 measures
measure strata flagged fraction first cut
dist2mean 2 27 0.108 6.485
robustrownorm 2 42 0.168 3.157
knn 3 32 0.128 1.071
dist2median 2 41 0.164 4.157
agreement on which objects are atypical (Jaccard)
dist2mean robustrownorm knn dist2median
dist2mean - 0.64 0.13 0.66
robustrownorm 0.64 - 0.42 0.98
knn 0.13 0.42 - 0.40
dist2median 0.66 0.98 0.40 -
dist2mean and knn overlap on only 13% of what they flag.
They are answering different questions. Deciding which one you
meant is the modelling step, not a detail. See the measure guide.
Read the agreement matrix, not the strata counts. knn and dist2mean overlap on a small fraction of what they flag: the island is far away but densely packed, so its nearest-neighbour distances are small. Neither is wrong. They are answering different questions, and choosing between them is the modelling step.
Side by side, the difference is visible rather than tabulated.
from entropy_spine import spine_data_plot
_, C = spine_compare(D, ["dist2mean", "knn"], "upper", k=5, report=False)
spine_data_plot(C["results"][0], title="dist2mean: far from the centre");
spine_data_plot(C["results"][1], title="knn: in a sparse neighbourhood");
When measures agree¶
Here the structure is robust to how it was asked for, and the choice of measure does not matter much.
rng = np.random.default_rng(7)
E = np.vstack([rng.standard_normal((200, 3)),
rng.standard_normal((15, 3)) + 12])
spine_compare(E, ["dist2mean", "dist2median", "robustrownorm"], "upper");
SPINE comparison over 3 measures
measure strata flagged fraction first cut
dist2mean 2 15 0.070 4.935
dist2median 2 15 0.070 3.75
robustrownorm 2 15 0.070 3.838
agreement on which objects are atypical (Jaccard)
dist2mean dist2median robustrownorm
dist2mean - 1.00 1.00
dist2median 1.00 - 1.00
robustrownorm 1.00 1.00 -
All measures overlap on at least 100% of what they flag, so the
structure is robust to how it was asked for.
What comes back¶
table carries what each measure found, one dict per measure; detail carries the results, the flagged sets, and the pairwise Jaccard agreement.
table, detail = spine_compare(D, ["dist2mean", "knn"], "upper", k=5,
report=False)
detail["agreement"].round(2)
array([[1. , 0.13],
[0.13, 1. ]])
See also¶
spine for a single partition, spine_summary for what one found, and the
measure guide on the documentation site for how to choose in the first place.