How to Analyze Experimental Data?

Note

Work in Progress: This guide is under active development and will be expanded with more detailed examples and workflows. Content may be updated in future releases.

Goal: By the end of this guide, you’ll understand how to apply CANN analysis tools to experimental neural data.

Estimated Reading Time: 10 minutes

Introduction

The previous guide showed how to analyze model-generated data. But what if you have real experimental recordings—neural spike trains, behavioral trajectories, or RNN dynamics from trained models?

The Data Analyzer provides tools to apply CANN-inspired analysis to experimental data, helping you:

  • Detect attractor-like activity patterns in neural recordings

  • Fit bump models to population activity

  • Perform topological data analysis (TDA) [15, 16] on time series

  • Analyze RNN fixed points and slow manifolds

Key difference from model analysis: You’re starting with observed data, not simulation outputs. The workflow is fundamentally different.

Experimental Data Analysis Workflow

Unlike model simulations where you control everything, analyzing experimental data follows this pattern:

Load Data → Preprocess/Format → Apply Analysis → Visualize Results

Let’s walk through this with a concrete example.

Example: 1D Bump Fitting

The most common analysis is bump fitting—detecting and tracking localized activity bumps in neural population data.

Step 1: Load Sample Data

The library provides example datasets via canns.data:

[ ]:
from canns.data import load_example_data

# Load example 1D neural activity data
data_dict = load_example_data('bump_1d_example')

print(f"Available keys: {data_dict.keys()}")
print(f"Activity shape: {data_dict['activity'].shape}")
print(f"Time points: {data_dict['time'].shape}")

Expected output:

Available keys: dict_keys(['activity', 'time', 'positions'])
Activity shape: (500, 128)  # 500 time points, 128 neurons
Time points: (500,)

Data structure:

  • activity: Neural firing rates or spike counts (time × neurons)

  • time: Time stamps for each sample

  • positions: Spatial positions of neurons (if available)

Step 2: Inspect the Data

Before analysis, visualize the raw data:

[ ]:
import matplotlib.pyplot as plt
import jax.numpy as jnp  # :cite:p:`jax2018github`

# Plot activity heatmap
plt.figure(figsize=(10, 4))
plt.imshow(data_dict['activity'].T, aspect='auto', cmap='viridis')
plt.xlabel('Time step')
plt.ylabel('Neuron index')
plt.title('Neural Population Activity')
plt.colorbar(label='Activity')
plt.show()

# Plot activity at one time point
plt.figure(figsize=(8, 3))
plt.plot(data_dict['positions'], data_dict['activity'][100])
plt.xlabel('Position (rad)')
plt.ylabel('Activity')
plt.title('Activity snapshot at t=100')
plt.grid(True)
plt.show()

What to look for:

  • Do you see localized bumps of activity?

  • Do they move over time?

  • Are there multiple bumps or just one?

Step 3: Apply Bump Fitting

Now use the Data Analyzer to fit a bump model:

[ ]:
from canns.analyzer.data import BumpAnalyzer1D

# Create analyzer
analyzer = BumpAnalyzer1D(positions=data_dict['positions'])

# Fit bumps to all time points
results = analyzer.fit_bumps(data_dict['activity'])

print(f"Detected bump centers: {results['centers'][:10]}")  # First 10
print(f"Bump widths: {results['widths'][:10]}")
print(f"Bump amplitudes: {results['amplitudes'][:10]}")

Results dictionary contains:

  • centers: Estimated bump center position for each time point

  • widths: Bump width (spatial spread)

  • amplitudes: Bump peak height

  • fit_quality: R² or goodness-of-fit metric

Step 4: Visualize Fitted Bumps

Plot the detected bump trajectory:

[ ]:
plt.figure(figsize=(10, 4))
plt.plot(data_dict['time'], results['centers'], linewidth=2)
plt.xlabel('Time (ms)')
plt.ylabel('Bump Position (rad)')
plt.title('Decoded Bump Trajectory')
plt.grid(True)
plt.show()

# Plot bump width over time
plt.figure(figsize=(10, 4))
plt.plot(data_dict['time'], results['widths'], linewidth=2, color='orange')
plt.xlabel('Time (ms)')
plt.ylabel('Bump Width (rad)')
plt.title('Bump Width Dynamics')
plt.grid(True)
plt.show()

Interpretation:

  • Stable bump position → stable attractor state

  • Smooth trajectory → continuous tracking

  • Varying width → dynamic tuning or state transitions

Step 5: Validate Fits

Check the quality of fits:

[ ]:
# Plot fit quality over time
plt.figure(figsize=(10, 4))
plt.plot(data_dict['time'], results['fit_quality'], linewidth=2)
plt.axhline(y=0.8, color='r', linestyle='--', label='Quality threshold')
plt.xlabel('Time (ms)')
plt.ylabel('Fit Quality (R²)')
plt.title('Bump Fit Quality')
plt.legend()
plt.grid(True)
plt.show()

# Identify low-quality fits
low_quality_indices = jnp.where(results['fit_quality'] < 0.8)[0]
print(f"Time points with poor fits: {len(low_quality_indices)} / {len(data_dict['time'])}")

Low-quality fits may indicate:

  • No clear bump present

  • Multiple overlapping bumps

  • Noisy or unreliable data

Complete Workflow Example

Here’s the full pipeline:

[ ]:
from canns.data import load_example_data
from canns.analyzer.data import BumpAnalyzer1D
import matplotlib.pyplot as plt

# 1. Load data
data = load_example_data('bump_1d_example')

# 2. Create analyzer
analyzer = BumpAnalyzer1D(positions=data['positions'])

# 3. Fit bumps
results = analyzer.fit_bumps(data['activity'])

# 4. Visualize trajectory
plt.figure(figsize=(12, 6))

# Subplot 1: Activity heatmap with fitted centers overlaid
plt.subplot(2, 1, 1)
plt.imshow(data['activity'].T, aspect='auto', cmap='viridis', extent=[0, len(data['time']), data['positions'][0], data['positions'][-1]])
plt.plot(range(len(results['centers'])), results['centers'], 'r-', linewidth=2, label='Fitted bump center')
plt.ylabel('Position (rad)')
plt.title('Neural Activity with Detected Bump Trajectory')
plt.legend()
plt.colorbar(label='Activity')

# Subplot 2: Bump position over time
plt.subplot(2, 1, 2)
plt.plot(data['time'], results['centers'], linewidth=2)
plt.xlabel('Time (ms)')
plt.ylabel('Bump Center (rad)')
plt.title('Decoded Position Trajectory')
plt.grid(True)

plt.tight_layout()
plt.savefig('experimental_bump_analysis.png', dpi=150)
plt.show()

print("Analysis complete! Results saved.")

Other Data Analysis Tools

Beyond bump fitting, the Data Analyzer provides:

Topological Data Analysis (TDA)

[ ]:
from canns.analyzer.data import TopologyAnalyzer

# Analyze topological features in neural dynamics
tda = TopologyAnalyzer()
persistence = tda.compute_persistence(data['activity'])

Use case: Detect ring-like or toroidal structures in high-dimensional activity

RNN Dynamics Analysis

[ ]:
from canns.analyzer.data import RNNAnalyzer

# Find fixed points :cite:p:`sussillo2013opening,golub2018fixedpointfinder` and slow manifolds in trained RNN models
rnn_analyzer = RNNAnalyzer(model=my_rnn)
fixed_points = rnn_analyzer.find_fixed_points()

Use case: Understand computational structure of trained recurrent networks

Key Differences: Model vs. Experimental Data Analysis

Aspect

Model Analysis

Experimental Data Analysis

Input

Simulation outputs

Neural recordings, trajectories

Control

Full control (parameters, inputs)

Observe only

Goal

Verify model behavior

Discover patterns in data

Challenges

Parameter tuning

Noise, missing data, artifacts

Workflow

Simulate → Analyze

Load → Preprocess → Analyze

When to use each:

  • Model analysis: Testing hypotheses, exploring parameter spaces, validating implementations

  • Data analysis: Interpreting experiments, detecting attractors in recordings, comparing models to biology

Loading Your Own Data

To analyze your own experimental data:

[ ]:
import jax.numpy as jnp  # :cite:p:`jax2018github`

# Load from numpy array, CSV, or other format
my_activity = jnp.load('my_experiment.npy')  # Shape: (time, neurons)
my_positions = jnp.linspace(-3.14, 3.14, num_neurons)

# Create analyzer with your neuron positions
analyzer = BumpAnalyzer1D(positions=my_positions)

# Analyze
results = analyzer.fit_bumps(my_activity)

Data requirements:

  • Activity should be (time_points, num_neurons) shape

  • Positions should match number of neurons

  • Values should be non-negative (firing rates or spike counts)

Common Issues

Q: My data has missing values or NaNs

Preprocess before analysis:

[ ]:
import jax.numpy as jnp  # :cite:p:`jax2018github`

# Remove NaN rows
valid_indices = ~jnp.isnan(activity).any(axis=1)
clean_activity = activity[valid_indices]

Q: Results don’t make sense

Check these:

  1. Data units (are firing rates in Hz or normalized?)

  2. Position range (should match neuron layout, e.g., -π to π for angular)

  3. Activity shape ((time, neurons) NOT (neurons, time))

Q: Fit quality is always low

Possible causes:

  • No clear bump structure in data

  • Need to adjust bump model parameters

  • Data is too noisy (try smoothing first)

Next Steps

Now you can analyze experimental data! Continue with:

  1. Train brain-inspired models—Learn Hebbian training for memory networks

  2. Core Concepts: Data Analyzer—Deep dive into analysis methods

  3. Full API Reference: Data Analyzer—Complete documentation of all analysis tools

Note

Feedback Welcome: This guide is being actively refined. If you have experimental data analysis use cases not covered here, please share them in GitHub Discussions!

Questions? Check Core Concepts: Data Analyzer or GitHub Discussions.