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 samplepositions: 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 pointwidths: Bump width (spatial spread)amplitudes: Bump peak heightfit_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)shapePositions 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:
Data units (are firing rates in Hz or normalized?)
Position range (should match neuron layout, e.g., -π to π for angular)
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:
Train brain-inspired models—Learn Hebbian training for memory networks
Core Concepts: Data Analyzer—Deep dive into analysis methods
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.