Source code for AMBER.temporal_analysis

"""
Temporal analysis metrics for SOM classification results.

TemporalAnalysis takes a completed Classification object and computes
metrics that are only meaningful when the classified patterns form an
ordered time series:

  transition_matrix  — how often the SOM moves from neuron i to neuron j
  stability          — fraction of steps where the BMU does not change
  mean_path_length   — average Euclidean grid distance per step
  mean_chebyshev_jump — average Chebyshev grid distance per step
  temporal_coherence — fraction of steps with Chebyshev jump ≤ 1
  trajectory         — ordered sequence of (row, col) BMU positions
"""

import logging

import numpy as np

logger = logging.getLogger(__name__)


[docs] class TemporalAnalysis: """Temporal dynamics of a SOM classification result. Parameters ---------- classification : Classification A completed Classification instance. The patterns in classification.classification_data are assumed to be ordered in time (as they would be for windowed biosignals or audio). Attributes ---------- trajectory : list of (int, int) Ordered BMU positions [(row_0, col_0), (row_1, col_1), ...]. transition_matrix : ndarray, shape (n_neurons, n_neurons) Raw count of transitions; entry [i, j] = number of times the SOM moved from neuron i to neuron j between consecutive patterns. Neurons are linearised as index = row * map_size + col. transition_matrix_norm : ndarray, shape (n_neurons, n_neurons) Row-normalised transition matrix (transition probabilities). stability : float Fraction of consecutive pattern pairs that share the same BMU. mean_path_length : float Mean Euclidean grid distance between consecutive BMU positions. mean_chebyshev_jump : float Mean Chebyshev (L∞) grid distance between consecutive BMU positions. Chebyshev distance counts diagonal steps as 1, matching the 8-neighbour topology of the SOM grid. temporal_coherence : float Fraction of consecutive pattern pairs whose BMUs are the same neuron or immediate neighbours (Chebyshev distance ≤ 1). A value of 1.0 means every step stays within the local neighbourhood; a standard SOM on non-stationary data typically achieves 0.4–0.6, while a well-tuned RSOM approaches 1.0 on smooth temporal sequences. """ def __init__(self, classification): cm = classification.classification_map self.map_size = classification.activations_map.shape[0] n_neurons = self.map_size ** 2 # Build ordered trajectory self.trajectory = [ (int(cm['x'].iloc[i]), int(cm['y'].iloc[i])) for i in range(len(cm)) ] n = len(self.trajectory) # Transition matrix T = np.zeros((n_neurons, n_neurons), dtype=int) for t in range(n - 1): i = self.trajectory[t][0] * self.map_size + self.trajectory[t][1] j = self.trajectory[t + 1][0] * self.map_size + self.trajectory[t + 1][1] T[i, j] += 1 self.transition_matrix = T row_sums = T.sum(axis=1, keepdims=True) with np.errstate(invalid='ignore', divide='ignore'): self.transition_matrix_norm = np.where( row_sums > 0, T / row_sums, 0.0 ) # Stability if n > 1: same = sum( 1 for t in range(n - 1) if self.trajectory[t] == self.trajectory[t + 1] ) self.stability = same / (n - 1) else: self.stability = 1.0 # Mean path length (Euclidean grid distance) if n > 1: dists = [ np.sqrt( (self.trajectory[t][0] - self.trajectory[t + 1][0]) ** 2 + (self.trajectory[t][1] - self.trajectory[t + 1][1]) ** 2 ) for t in range(n - 1) ] self.mean_path_length = float(np.mean(dists)) else: self.mean_path_length = 0.0 # Chebyshev (L∞) jump distances and Temporal Coherence # TC = fraction of consecutive steps with Chebyshev distance ≤ 1, # i.e. the BMU stays in the same neuron or moves to an immediate # neighbour (including diagonals) — matching the 8-neighbour SOM grid. if n > 1: chebyshev_jumps = [ max(abs(self.trajectory[t][0] - self.trajectory[t + 1][0]), abs(self.trajectory[t][1] - self.trajectory[t + 1][1])) for t in range(n - 1) ] self.mean_chebyshev_jump = float(np.mean(chebyshev_jumps)) self.temporal_coherence = float( sum(j <= 1 for j in chebyshev_jumps) / (n - 1) ) else: self.mean_chebyshev_jump = 0.0 self.temporal_coherence = 1.0 # ------------------------------------------------------------------ # Derived views # ------------------------------------------------------------------
[docs] def most_frequent_transitions(self, top_k=10): """Return the top-k most frequent transitions as a list of dicts. Each dict has keys 'from' (row, col), 'to' (row, col), 'count'. """ flat = [ (i // self.map_size, i % self.map_size, j // self.map_size, j % self.map_size, self.transition_matrix[i, j]) for i in range(self.transition_matrix.shape[0]) for j in range(self.transition_matrix.shape[1]) if self.transition_matrix[i, j] > 0 ] flat.sort(key=lambda x: -x[4]) return [ {'from': (r1, c1), 'to': (r2, c2), 'count': cnt} for r1, c1, r2, c2, cnt in flat[:top_k] ]
[docs] def dwell_times(self): """Return a dict mapping each BMU (row, col) to its mean consecutive dwell time (number of steps the SOM stays on that neuron).""" dwell = {} t = 0 n = len(self.trajectory) while t < n: pos = self.trajectory[t] run = 1 while t + run < n and self.trajectory[t + run] == pos: run += 1 if pos not in dwell: dwell[pos] = [] dwell[pos].append(run) t += run return {pos: float(np.mean(runs)) for pos, runs in dwell.items()}
[docs] def summary(self) -> str: """Return a short human-readable summary string. The summary is also emitted at INFO level via the standard logging framework so library users can control visibility with their own logging configuration. :return: formatted summary string """ top = self.most_frequent_transitions(3) transitions = "\n".join( f" {tr['from']}{tr['to']} ({tr['count']} times)" for tr in top ) text = ( f"Sequence length : {len(self.trajectory)}\n" f"Unique BMUs visited : {len(set(self.trajectory))}\n" f"Stability : {self.stability:.3f}\n" f"Mean path length : {self.mean_path_length:.3f} grid units (Euclidean)\n" f"Mean Chebyshev jump : {self.mean_chebyshev_jump:.3f} grid units\n" f"Temporal Coherence : {self.temporal_coherence:.3f} " f"(fraction of steps with Chebyshev jump ≤ 1)\n" f"Top-3 transitions :\n{transitions}" ) logger.info(text) return text