Working with NWB Files using h5py and pynwb
Author
Neurodata Without Borders (NWB) is a community standard for sharing neurophysiology data. An NWB file is, on disk, an HDF5 file with a hierarchy tailored for neuroscience data: raw recordings live under /acquisition, derived signals under processing, subject and experiment metadata under /general, trial structure under /intervals/trials, and spike-sorted units under /units. Because NWB is just HDF5 with a schema, you can open it two ways:
- With h5py, to inspect groups, datasets, and attributes directly. This is useful when you want to see how the file is laid out or when you suspect a file is malformed.
- With pynwb, which reads the HDF5 file into typed Python objects (
NWBFile,ElectricalSeries,Units, …) that know about units, time bases, and electrode references. This is what you’ll normally use for analysis.
In this session you’ll do both on a small synthetic dataset.
Setup
Import the libraries we’ll need. h5py is the low-level HDF5 reader; pynwb is the NWB-aware library; numpy, pandas, and matplotlib are for working with the data once we’ve read it.
import h5py
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from uuid import uuid4
from pynwb import NWBHDF5IOThe synthetic NWB file
In the exercises in this notebook you will read from a small synthetic file, synthetic_session.nwb. Run the cell below to generate the data and construct the file. It imitates data from a single mouse subject with data from an 8-channel probe split across V1 and hippocampus.
!python generate_synthetic_nwb_data.pyWrote synthetic_session.nwbSection 1: Exploring NWB Structure with h5py
Background
An NWB file is a single HDF5 file with. HDF5 organises data hierarchically: groups are like folders, datasets are like files holding numeric or string arrays, and both groups and datasets can carry attributes — short key/value pairs that store metadata (units, descriptions, neurodata types). When you open an NWB file with h5py, you see this raw layout, not the NWB-typed objects. Common top-level groups are:
/acquisition— raw recorded signals (e.g. extracellular voltage)/processing— derived data organised into modules (e.g. LFP, behaviour)/general— subject, devices, electrode metadata/intervals/trials— trial table/units— sorted-unit spike times
Opening an NWB file with h5py is a quick way to confirm what’s inside.
Exercises
| Code | Description |
|---|---|
with NWBHDF5IO('synthetic_session.nwb', 'r') as io:# read content in f |
Open an NWB file for reading with a context manager |
io = NWBHDF5IO('synthetic_session.nwb', 'r') |
Opens an nwb file object for reading without a context manager |
io.close() |
Closes the nwb file after reading without a context manager. |
list(f.keys()) |
List the top-level groups |
list(f['acquisition'].keys()) |
List the entries in the acquisition group |
dset = f['acquisition/ElectricalSeries/data'] |
Read a dataset and assign it to a variable named dset |
f['acquisition/ElectricalSeries/data'].shape |
Read a dataset’s shape |
f['acquisition/ElectricalSeries/data'].dtype |
Read a dataset’s dtype |
dict(f['acquisition/ElectricalSeries/data'].attrs) |
Read attributes of a dataset |
f.visit(lambda name: print(name)) |
Walk every group and dataset recursively |
f['general/subject/species'][()] |
Read a scalar/string dataset (returns bytes for strings) |
f['session_description'][()].decode() |
Decode a string dataset to str |
Example: open the file and print a list of the top-level groups.
with h5py.File("synthetic_session.nwb", "r") as f:
print(list(f.keys()))['acquisition', 'analysis', 'file_create_date', 'general', 'identifier', 'intervals', 'processing', 'session_description', 'session_start_time', 'specifications', 'stimulus', 'timestamps_reference_time', 'units']Alternative solution:
f = h5py.File("synthetic_session.nwb", "r")
print(list(f.keys()))
f.close()['acquisition', 'analysis', 'file_create_date', 'general', 'identifier', 'intervals', 'processing', 'session_description', 'session_start_time', 'specifications', 'stimulus', 'timestamps_reference_time', 'units']Exercise: print the list of entries inside the acquisition group.
Solution
with h5py.File("synthetic_session.nwb", "r") as f:
print(list(f["acquisition"].keys()))['ElectricalSeries']Exercise: print the list of entries inside the general group.
Solution
with h5py.File("synthetic_session.nwb", "r") as f:
print(list(f["general"].keys()))['devices', 'experimenter', 'extracellular_ephys', 'institution', 'lab', 'session_id', 'subject']Exercise: read and print the shape and dtype of the raw signal dataset at acquisition/ElectricalSeries/data.
Solution
with h5py.File("synthetic_session.nwb", "r") as f:
dset = f["acquisition/ElectricalSeries/data"]
print(dset.shape, dset.dtype)(5000, 8) float32Exercise: read the attributes attached to the same dataset. Which attribute tells you the physical unit of the recorded values?
Solution
with h5py.File("synthetic_session.nwb", "r") as f:
attrs = dict(f["acquisition/ElectricalSeries/data"].attrs)
print(attrs)
# The "unit" attribute holds the physical unit (here, 'volts').{'conversion': np.float64(1.0), 'offset': np.float64(0.0), 'resolution': np.float64(-1.0), 'unit': 'volts'}Exercise: walk the entire file with visit and print every group and dataset path. This gives you a one-shot map of what’s stored.
Solution
with h5py.File("synthetic_session.nwb", "r") as f:
f.visit(lambda name: print(name))acquisition
acquisition/ElectricalSeries
acquisition/ElectricalSeries/data
acquisition/ElectricalSeries/electrodes
acquisition/ElectricalSeries/starting_time
analysis
file_create_date
general
general/devices
general/devices/probe-A
general/experimenter
general/extracellular_ephys
general/extracellular_ephys/electrodes
general/extracellular_ephys/electrodes/filtering
general/extracellular_ephys/electrodes/group
general/extracellular_ephys/electrodes/group_name
general/extracellular_ephys/electrodes/id
general/extracellular_ephys/electrodes/location
general/extracellular_ephys/electrodes/x
general/extracellular_ephys/electrodes/y
general/extracellular_ephys/electrodes/z
general/extracellular_ephys/shank-HPC
general/extracellular_ephys/shank-V1
general/institution
general/lab
general/session_id
general/subject
general/subject/age
general/subject/description
general/subject/sex
general/subject/species
general/subject/subject_id
identifier
intervals
intervals/trials
intervals/trials/id
intervals/trials/start_time
intervals/trials/stim_type
intervals/trials/stop_time
processing
processing/behavior
processing/behavior/BehavioralTimeSeries
processing/behavior/BehavioralTimeSeries/pupil_diameter
processing/behavior/BehavioralTimeSeries/pupil_diameter/data
processing/behavior/BehavioralTimeSeries/pupil_diameter/starting_time
processing/behavior/BehavioralTimeSeries/running_speed
processing/behavior/BehavioralTimeSeries/running_speed/data
processing/behavior/BehavioralTimeSeries/running_speed/starting_time
processing/ecephys
processing/ecephys/LFP
processing/ecephys/LFP/lfp
processing/ecephys/LFP/lfp/data
processing/ecephys/LFP/lfp/starting_time
session_description
session_start_time
specifications
specifications/core
specifications/core/2.9.0
specifications/core/2.9.0/namespace
specifications/core/2.9.0/nwb.base
specifications/core/2.9.0/nwb.behavior
specifications/core/2.9.0/nwb.device
specifications/core/2.9.0/nwb.ecephys
specifications/core/2.9.0/nwb.epoch
specifications/core/2.9.0/nwb.file
specifications/core/2.9.0/nwb.icephys
specifications/core/2.9.0/nwb.image
specifications/core/2.9.0/nwb.misc
specifications/core/2.9.0/nwb.ogen
specifications/core/2.9.0/nwb.ophys
specifications/core/2.9.0/nwb.retinotopy
specifications/hdmf-common
specifications/hdmf-common/1.8.0
specifications/hdmf-common/1.8.0/base
specifications/hdmf-common/1.8.0/namespace
specifications/hdmf-common/1.8.0/sparse
specifications/hdmf-common/1.8.0/table
specifications/hdmf-experimental
specifications/hdmf-experimental/0.5.0
specifications/hdmf-experimental/0.5.0/experimental
specifications/hdmf-experimental/0.5.0/namespace
specifications/hdmf-experimental/0.5.0/resources
stimulus
stimulus/presentation
stimulus/templates
timestamps_reference_time
units
units/id
units/spike_times
units/spike_times_indexExercise: read the subject’s species from general/subject/species, assign it to a variable named species, and print it.
Solution
with h5py.File("synthetic_session.nwb", "r") as f:
species = f["general/subject/species"][()]
print(species)b'Mus musculus'Below is a simplified file tree to illustrate the structure of the different contents of an NWB file. It can be handy to look back on if you are wondering about the relationship between different parts of the NWB file throughout the next sections.
NWBFile ──────────────────── the whole session
│
├── acquisition ───────────── raw signals, by name
│ └── ElectricalSeries ← TimeSeries (data + time base + unit)
│
├── processing ────────────── ProcessingModules (derived data)
│ └── "ecephys" ── ProcessingModule
│ └── LFP ──────────── a container / data interface
│ └── ElectricalSeries "lfp"
| └── "behavior" ── ProcessingModule
│ └── BehavioralTimeSeries ──────────── a container / data interface
│ └── TimeSeries "running_speed"
│
├── intervals/trials ──────── DynamicTable (one row per trial)
├── units ────────────────── DynamicTable (one row per neuron)
└── general/ ─────────────── metadata
├── subject ← Subject
├── devices/ …
└── extracellular_ephys/electrodes ← DynamicTable (one row per electrode)Section 2: Reading Data with pynwb
Background
h5py is great for inspection, but for analysis of NWB-files you usually want typed objects (objects with information about what the data is). For example, instead of just a raw array with no information about sampling rates and what kind of data it is, you get an ElectricalSeries object that, together with the data array, contains a table of electrodes and metadata you can access by attribute.
pynwb.NWBHDF5IO opens an NWB file and io.read() reads it and returns an NWBFile object, which is a a typed view of the same HDF5 data you just explored.
Important: keep the file open while you access lazily-loaded data. Once the NWBHDF5IO with-block exits, slicing .data[...] may fail. You can either read inside the with block to automatically close the file after you have read what you wanted to read, or you can explictly call io.close() to close it.
Exercises
| Code | Description |
|---|---|
with NWBHDF5IO('synthetic_session.nwb', 'r') as io:# read content in f |
Open an NWB file for reading with a context manager |
io = NWBHDF5IO('synthetic_session.nwb', 'r') |
Opens an nwb file object for reading without a context manager |
io.close() |
Closes the nwb file after reading without a context manager. |
nwb = io.read() |
Read into an NWBFile object |
nwb.session_description |
Read the session description string |
nwb.subject |
Get the subject object |
es = nwb.acquisition['ElectricalSeries'] |
Get an acquisition time series by name and assign it to a variable. |
es.rate, es.starting_time |
Sampling rate and starting time of the series |
es.unit |
Physical unit of the data |
es.data[:10, 0] |
Access the first 10 samples of the first channel in the data. |
edf = nwb.electrodes.to_dataframe() |
Get electrode table as pandas DataFrame and assign it to variable edf |
edf.head() |
Get the first 5 rows of the pandas DataFrame |
edf["location"].value_counts() |
Count rows per category in a column |
Example: open the file with pynwb, read it into an NWBFile, and look at the ElectricalSeries in acquisition by printing it.
with NWBHDF5IO("synthetic_session.nwb", "r") as io:
nwb = io.read()
print(nwb.acquisition["ElectricalSeries"])ElectricalSeries pynwb.ecephys.ElectricalSeries at 0x124534297604848
Fields:
comments: no comments
conversion: 1.0
data: <HDF5 dataset "data": shape (5000, 8), type "<f4">
description: no description
electrodes: electrodes <class 'hdmf.common.table.DynamicTableRegion'>
offset: 0.0
rate: 1000.0
resolution: -1.0
starting_time: 0.0
starting_time_unit: seconds
unit: voltsAlternative solution:
io = NWBHDF5IO("synthetic_session.nwb", "r")
nwb = io.read()
print(nwb.acquisition["ElectricalSeries"])
io.close()ElectricalSeries pynwb.ecephys.ElectricalSeries at 0x124534297799856
Fields:
comments: no comments
conversion: 1.0
data: <HDF5 dataset "data": shape (5000, 8), type "<f4">
description: no description
electrodes: electrodes <class 'hdmf.common.table.DynamicTableRegion'>
offset: 0.0
rate: 1000.0
resolution: -1.0
starting_time: 0.0
starting_time_unit: seconds
unit: voltsExercise: open the file with pynwb, read it into an NWBFile, and print nwb.session_description.
Solution
with NWBHDF5IO("synthetic_session.nwb", "r") as io:
nwb = io.read()
print(nwb.session_description)Synthetic ephys session for course exercisesExercise: open the file and print nwb.subject. Which species was recorded?
Solution
with NWBHDF5IO("synthetic_session.nwb", "r") as io:
nwb = io.read()
print(nwb.subject)subject pynwb.file.Subject at 0x124534296111344
Fields:
age: P60D
age__reference: birth
description: Synthetic subject for the course
sex: M
species: Mus musculus
subject_id: mouse-01Exercise: assign the raw signal in acquisition/ElectricalSeries to a variable es. Print the sampling rate, starting time, and unit of the raw signal in es.
Solution
with NWBHDF5IO("synthetic_session.nwb", "r") as io:
nwb = io.read()
es = nwb.acquisition["ElectricalSeries"]
print(f"rate: {es.rate} Hz; starting_time: {es.starting_time}, unit: {es.unit}")rate: 1000.0 Hz; starting_time: 0.0, unit: voltsExercise: Print the shape and data type of the raw signal in acquisition/ElectricalSeries. Note that for shape and dtype you need to access the data attribute of the ElectricalSeries object, i.e. es.data.shape and es.data.dtype.
Solution
print(es.data.shape, es.data.dtype)(5000, 8) float32Exercise: get the first 200 samples of channel 0 of the raw signal in acquisition/ElectricalSeries and assign to a variable named y. Hint: es.data to get the data. Then, run the cell below to plot the signal.
Solution
with NWBHDF5IO("synthetic_session.nwb", "r") as io:
nwb = io.read()
es = nwb.acquisition["ElectricalSeries"]
y = es.data[:200, 0]fig, ax = plt.subplots()
ax.plot(y)
ax.set_ylabel(f"voltage ({es.unit})")
ax.set_title("Channel 0, first 200 samples")
plt.show()Exercise: convert the electrode table to a pandas DataFrame, assign it to a variable named edf, and display the first few rows.
Solution
with NWBHDF5IO("synthetic_session.nwb", "r") as io:
nwb = io.read()
edf = nwb.electrodes.to_dataframe()
edf.head()| location | group | group_name | x | y | z | filtering | |
|---|---|---|---|---|---|---|---|
| id | |||||||
| 0 | V1 | shank-V1 pynwb.ecephys.ElectrodeGroup at 0x124... | shank-V1 | 0.0 | 0.0 | 0.0 | none |
| 1 | V1 | shank-V1 pynwb.ecephys.ElectrodeGroup at 0x124... | shank-V1 | 1.0 | 0.0 | 0.0 | none |
| 2 | V1 | shank-V1 pynwb.ecephys.ElectrodeGroup at 0x124... | shank-V1 | 2.0 | 0.0 | 0.0 | none |
| 3 | V1 | shank-V1 pynwb.ecephys.ElectrodeGroup at 0x124... | shank-V1 | 3.0 | 0.0 | 0.0 | none |
| 4 | HPC | shank-HPC pynwb.ecephys.ElectrodeGroup at 0x12... | shank-HPC | 0.0 | 100.0 | 0.0 | none |
Exercise: from the same electrode DataFrame, count how many electrodes are in V1 and how many are in HPC. Hint: See value_counts() in code reference table.
Solution
with NWBHDF5IO("synthetic_session.nwb", "r") as io:
nwb = io.read()
edf = nwb.electrodes.to_dataframe()
print(edf["location"].value_counts())location
V1 4
HPC 4
Name: count, dtype: int64Section 3: DynamicTable - for Tabular Data like Trial Metadata and Unit Spike Events
Background
A DynamicTable is a flexible row-and-column data structure used to store, among other things, trial data and units data (activity of neurons). It has certain required columns (e.g. start_time, stop_time for trials; spike_times for units) as well as optional user-defined columns. The easiest way to work with DynamicTables is to call .to_dataframe() and treat it like any other pandas DataFrame.
In our synthetic file:
- The table
nwb.trialshas 10 trials, each with astart_time,stop_time, and astim_typeof either'A'or'B'. - The table
nwb.unitshas 3 sorted units, each with an array of spike times in seconds.
Exercises
| Code | Description |
|---|---|
with h5py.File('synthetic_session.nwb', 'r') as f:# read content in f |
Open an HDF5/NWB file for reading |
nwb.trials.to_dataframe() |
Trial table as pandas DataFrame |
nwb.trials.colnames |
Column names of the trial table |
nwb.units.to_dataframe() |
Units table as pandas DataFrame |
nwb.units["spike_times"][i] |
Spike times of the i-th unit |
nwb.units["spike_times"][:] |
Spike times of all units (list of arrays) |
len(nwb.units["spike_times"][i]) |
Number of spikes of the i-th unit |
len(nwb.units) |
Number of units |
Example: load the trial table as a pandas DataFrame, and display it.
Solution
with NWBHDF5IO("synthetic_session.nwb", "r") as io:
nwb = io.read()
trials_df = nwb.trials.to_dataframe()
trials_df| start_time | stop_time | stim_type | |
|---|---|---|---|
| id | |||
| 0 | 0.0 | 0.3 | A |
| 1 | 0.5 | 0.8 | B |
| 2 | 1.0 | 1.3 | A |
| 3 | 1.5 | 1.8 | B |
| 4 | 2.0 | 2.3 | A |
| 5 | 2.5 | 2.8 | B |
| 6 | 3.0 | 3.3 | A |
| 7 | 3.5 | 3.8 | B |
| 8 | 4.0 | 4.3 | A |
| 9 | 4.5 | 4.8 | B |
Exercise: print the trial table’s column names.
with NWBHDF5IO("synthetic_session.nwb", "r") as io:
nwb = io.read()
trial_col_names = nwb.trials.colnames
trial_col_names('start_time', 'stop_time', 'stim_type')Exercise: load the units table as a pandas DataFrame, and display it.
Solution
with NWBHDF5IO("synthetic_session.nwb", "r") as io:
nwb = io.read()
units_df = nwb.units.to_dataframe()
units_df| spike_times | |
|---|---|
| id | |
| 0 | [0.41953509185521043, 0.4911704840309844, 0.74... |
| 1 | [0.05668210404311813, 0.11834107349926404, 0.1... |
| 2 | [1.1718332859068643, 1.4985834102884006, 1.641... |
Example: get the spike times of unit 0 and print the first five timestamps.
with NWBHDF5IO("synthetic_session.nwb", "r") as io:
nwb = io.read()
st0 = nwb.units["spike_times"][0]
print(st0[:5])[0.41953509 0.49117048 0.74386423 1.40682199 1.53924686]Exercise: get the spike times of unit 2 and print the first five timestamps.
Solution
with NWBHDF5IO("synthetic_session.nwb", "r") as io:
nwb = io.read()
st2 = nwb.units["spike_times"][2]
print(st2[:5])[1.17183329 1.49858341 1.64159373 2.0096868 2.10774683]Exercise: get the spike times of all units and assign them to a variable named sts. Print the first five timestamps of unit 2 to check that you get the same result as in the previous exercise.
Solution
with NWBHDF5IO("synthetic_session.nwb", "r") as io:
nwb = io.read()
sts = nwb.units["spike_times"][:]
print(sts[0][:5])[0.41953509 0.49117048 0.74386423 1.40682199 1.53924686]Demo: Run the cell below to read the spikes from all units and make a spike raster plot.
with NWBHDF5IO("synthetic_session.nwb", "r") as io:
nwb = io.read()
spikes = [nwb.units["spike_times"][i] for i in range(len(nwb.units))]
fig, ax = plt.subplots()
ax.eventplot(spikes, lineoffsets=np.arange(len(spikes)), linelengths=0.8)
ax.set_xlabel("time (s)")
ax.set_ylabel("unit")
ax.set_yticks(np.arange(len(spikes)))
ax.set_title("Spike raster")
plt.show()Section 4: Processed Data: LFP and Behavioral Variables
Background
Data processed from the raw signal, like filtered LFP, behavioural variables, and spike-sorting output, is stored in nwb.processing.
Processed data is grouped by topic into ProcessingModules, one per kind of data (for example one for electrophysiology-derived signals, one for behaviour). Within a module, the data is put in typed containers (such as LFP or BehavioralTimeSeries) that wrap the time series. Reading processed data is therefore a matter of navigating from nwb.processing to the right module, then to the container, and finally to the series inside it.
In our synthetic file, there are two processing modules:
nwb.processing['ecephys']contains anLFPcontainer whoseelectrical_series['lfp']is the raw signal downsampled to 250 Hz.nwb.processing['behavior']contains aBehavioralTimeSeriescontainer with two behavioural signals sampled at 50 Hz:running_speed(in cm/s) andpupil_diameter(in mm).
Exercises
| Code | Description |
|---|---|
with h5py.File('synthetic_session.nwb', 'r') as f:# read content in f |
Open an HDF5/NWB file for reading |
list(nwb.processing.keys()) |
List processing modules |
nwb.processing['ecephys'].data_interfaces |
Get items inside a module |
nwb.processing['ecephys']['LFP'] |
Get the LFP container |
lfp.electrical_series['lfp'] |
Get the LFP time series object ElectricalSeries |
es_lfp.starting_time + np.arange(n) / es_lfp.rate |
Build a time axis for the LFP samples |
data.mean(axis=0) |
Per-channel mean across time |
edf.groupby("location")["lfp_mean"].mean() |
Average a per-channel value by region |
nwb.processing['behavior'] |
Get the behaviour module |
bts = nwb.processing['behavior']['BehavioralTimeSeries'] |
Get the BehavioralTimeSeries container |
list(bts.time_series.keys()) |
List the behavioural series in the container |
rs = bts['running_speed'] |
Get a behavioural TimeSeries by name |
rs.data[:], rs.rate, rs.unit |
Data array, sampling rate, and unit of a series |
rs.starting_time + np.arange(n) / rs.rate |
Build a time axis for a behavioural series |
np.corrcoef(a, b)[0, 1] |
Pearson correlation between two 1-D arrays |
Example: list the processing modules in the file.
with NWBHDF5IO("synthetic_session.nwb", "r") as io:
nwb = io.read()
print(list(nwb.processing.keys()))['behavior', 'ecephys']Exercise: list the data interfaces inside the ecephys module.
Solution
with NWBHDF5IO("synthetic_session.nwb", "r") as io:
nwb = io.read()
print(list(nwb.processing["ecephys"].data_interfaces.keys()))['LFP']Exercise: get the LFP time series ElectricalSeries and print its sampling rate. How does it compare to the raw signal’s 1000 Hz?
Solution
with NWBHDF5IO("synthetic_session.nwb", "r") as io:
nwb = io.read()
es_lfp = nwb.processing["ecephys"]["LFP"].electrical_series["lfp"]
print("LFP rate:", es_lfp.rate, "Hz")LFP rate: 250.0 HzExercise: plot LFP channel 0 over time.
Solution
with NWBHDF5IO("synthetic_session.nwb", "r") as io:
nwb = io.read()
es_lfp = nwb.processing["ecephys"]["LFP"].electrical_series["lfp"]
y = es_lfp.data[:, 0]
t = es_lfp.starting_time + np.arange(y.shape[0]) / es_lfp.rate
fig, ax = plt.subplots()
ax.plot(t, y)
ax.set_xlabel("time (s)")
ax.set_ylabel(f"LFP ({es_lfp.unit})")
ax.set_title("LFP channel 0")
plt.show()Now that you have read the LFP from the ecephys module, turn to the behavioural variables stored in the behavior module.
Example: get the behavior processing module and list the data interfaces (containers) inside it.
with NWBHDF5IO("synthetic_session.nwb", "r") as io:
nwb = io.read()
behavior = nwb.processing["behavior"]
print(list(behavior.data_interfaces.keys()))['BehavioralTimeSeries']Exercise: get the BehavioralTimeSeries container from the behavior module and list the behavioural series inside it. Hint: a BehavioralTimeSeries exposes its series through .time_series, just like the LFP container exposes .electrical_series.
Solution
with NWBHDF5IO("synthetic_session.nwb", "r") as io:
nwb = io.read()
bts = nwb.processing["behavior"]["BehavioralTimeSeries"]
print(list(bts.time_series.keys()))['pupil_diameter', 'running_speed']Exercise: get the running_speed series from the container and print its unit.
Solution
with NWBHDF5IO("synthetic_session.nwb", "r") as io:
nwb = io.read()
bts = nwb.processing["behavior"]["BehavioralTimeSeries"]
rs = bts["running_speed"]
print(f"Running speed unit: {rs.unit}")Running speed unit: cm/sExercise: plot running speed over time. Build the time axis from starting_time and rate the same way you did for the LFP.
Solution
with NWBHDF5IO("synthetic_session.nwb", "r") as io:
nwb = io.read()
rs = nwb.processing["behavior"]["BehavioralTimeSeries"]["running_speed"]
y = rs.data[:]
t = rs.starting_time + np.arange(y.shape[0]) / rs.rate
fig, ax = plt.subplots()
ax.plot(t, y)
ax.set_xlabel("time (s)")
ax.set_ylabel(f"running speed ({rs.unit})")
ax.set_title("Running speed")
plt.show()Exercise: plot the pupil_diameter over time.
Solution
with NWBHDF5IO("synthetic_session.nwb", "r") as io:
nwb = io.read()
pupil = nwb.processing["behavior"]["BehavioralTimeSeries"]["pupil_diameter"]
y = pupil.data[:]
t = pupil.starting_time + np.arange(y.shape[0]) / pupil.rate
fig, ax = plt.subplots()
ax.plot(t, y)
ax.set_xlabel("time (s)")
ax.set_ylabel(f"pupil diameter ({pupil.unit})")
ax.set_title("Pupil diameter")
plt.show()(Extra Challenge) Exercise: compute the mean LFP amplitude per electrode and group the result by brain region (V1 vs HPC) using the electrode table.
Solution
with NWBHDF5IO("synthetic_session.nwb", "r") as io:
nwb = io.read()
es_lfp = nwb.processing["ecephys"]["LFP"].electrical_series["lfp"]
data = es_lfp.data[:]
edf = nwb.electrodes.to_dataframe()
per_channel_mean = data.mean(axis=0)
edf = edf.copy()
edf["lfp_mean"] = per_channel_mean
print(edf.groupby("location")["lfp_mean"].mean())location
HPC -5.126826e-07
V1 -1.076278e-06
Name: lfp_mean, dtype: float32