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");
No description has been provided for this image
No description has been provided for this image

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.