Windowed Analysis Tutorial#
This tutorial provides a deep dive into Neurodent’s windowed analysis capabilities for extracting features from continuous EEG data.
Overview#
Windowed Analysis Results (WAR) is the core feature extraction system in Neurodent. It:
Divides continuous EEG data into time windows
Computes features for each window
Aggregates results across time and channels
Provides filtering and quality control methods
This approach is efficient for long recordings and enables parallel processing.
import logging
import tempfile
from datetime import datetime
from pathlib import Path
from IPython.display import display
from neurodent import (
AnimalOrganizer,
AnimalAnalyzer,
WindowAnalysisResult,
ZeitgeberAnalysisResult,
constants,
set_channel_map,
)
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger()
/home/runner/work/neurodent/neurodent/.venv/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
from .autonotebook import tqdm as notebook_tqdm
set_channel_map({
"LMot": ["C-015", "D-015"],
"RMot": ["C-016", "D-016"],
"LBar": ["C-014", "D-014"],
"RBar": ["C-017", "D-017"],
"LHip": ["C-012", "D-012"],
"RHip": ["C-019", "D-019"],
"LAud": ["C-009", "D-009"],
"RAud": ["C-022", "D-022"],
"LVis": ["C-010", "D-010"],
"RVis": ["C-021", "D-021"],
})
# Per-animal genotype and sex. Every WindowAnalysisResult re-reads this at construction.
constants.ANIMAL_METADATA = {
"A10": {"genotype": "WT", "sex": "Male"},
"F22": {"genotype": "KO", "sex": "Female"},
}
1. Feature Categories#
Neurodent extracts three main categories of features:
Linear Features (per channel)#
Single-value metrics for each channel in each time window:
# Available linear features
print("Linear features:")
for feature in constants.LINEAR_FEATURES:
print(f" - {feature}")
Linear features:
- rms
- ampvar
- psdtotal
- nspike
- logrms
- logampvar
- logpsdtotal
- lognspike
Band Features (per frequency band)#
Features computed for each frequency band (delta, theta, alpha, beta, gamma):
# Available band features
print("\nBand features:")
for feature in constants.BAND_FEATURES:
print(f" - {feature}")
# Frequency bands
print("\nFrequency bands:")
for band, (lo, hi) in constants.FREQ_BANDS.items():
print(f" {band.capitalize()}: {lo}-{hi} Hz")
Band features:
- psdband
- psdfrac
- logpsdband
- logpsdfrac
Frequency bands:
Delta: 1-4 Hz
Theta: 4-8 Hz
Alpha: 8-13 Hz
Beta: 13-25 Hz
Gamma: 25-40 Hz
Matrix Features (connectivity)#
Features measuring relationships between channels:
# Available matrix features
print("\nMatrix features:")
for feature in constants.MATRIX_FEATURES:
print(f" - {feature}")
Matrix features:
- cohere
- zcohere
- imcoh
- zimcoh
- pcorr
- zpcorr
2. Computing Windowed Analysis#
Basic Usage#
from neurodent.data import sample_dataset
SAMPLE_DATA = sample_dataset()
Using your own data#
Replace SAMPLE_DATA with your own directory and set extract_func for your format.
Data Loading covers readers and patterns.
# Using the included example data.
# The paired ColMajor .bin + Meta .csv recordings are 60 seconds long, which at the default
# 5 s window gives 12 windows. (The .edf files in the same folder are only 5 seconds long
# and would yield a single window.)
animal_id = "A10"
ao = AnimalOrganizer(
pattern=[
str(SAMPLE_DATA / "{animal}" / "*_ColMajor.bin"),
str(SAMPLE_DATA / "{animal}" / "*_Meta.csv"),
],
animal_id=animal_id,
lro_kwargs={
"mode": "si", # mode options: 'si' (SpikeInterface), 'mne', or None
"extract_func": "neurodent.readers:read_bin_csv_pair",
"manual_datetimes": datetime(2023, 12, 13, 12, 0),
},
)
analyzer = AnimalAnalyzer(ao)
analyzer.compute_bad_channels()
# Compute all features
war_all = analyzer.compute_windowed_analysis(
features=['all'],
exclude=['nspike', 'lognspike'], # Exclude spike features if no spikes
multiprocess_mode='serial'
)
print(f"Computed {len(war_all.result)} windows")
Computed 12 windows
INFO:root:Processing manual_datetimes configuration
INFO:root:Processing global manual datetimes starting at 2023-12-13 12:00:00
INFO:root:Computing continuous timeline for 1 animaldays (1 total items) starting at 2023-12-13 12:00:00
INFO:root:Ordered items for timeline: ['Cage 2 A10-0_ColMajor.bin...']
INFO:root:Recording already at target sampling rate (1000 Hz) or unable to determine, no resampling needed
INFO:root:Finalizing file timestamps
INFO:root:Using manual timestamps: 1 file end times specified
INFO:root:Item Cage 2 A10-0_ColMajor.bin...: duration = 60.0s (loaded with manual timestamp)
INFO:root:Timeline computed: 1 items, total duration 60.0s
INFO:root:Recording already at target sampling rate (1000 Hz) or unable to determine, no resampling needed
INFO:root:Finalizing file timestamps
INFO:root:Using manual timestamps: 1 file end times specified
INFO:root:AnimalOrganizer Timeline Summary:
LRO 0: 2023-12-13 12:00:00 -> 2023-12-13 12:01:00 (duration: 60.0s, items: 1, item: Cage 2 A10-0_ColMajor.bin...)
INFO:root:Computing bad channels for 1 recordings with threshold=None
INFO:root:Computed n_neighbors for LOF computation: 4
INFO:root:LOF computation successful: 10 channels
INFO:root:Computed LOF scores for 10 channels
INFO:root:Computing windowed analysis for Cage 2 A10-0_ColMajor.bin...
Processing rows: 0%| | 0/12 [00:00<?, ?it/s]
Processing rows: 8%|▊ | 1/12 [00:00<00:01, 8.34it/s]
Processing rows: 17%|█▋ | 2/12 [00:00<00:01, 8.30it/s]
Processing rows: 25%|██▌ | 3/12 [00:00<00:01, 8.19it/s]
Processing rows: 33%|███▎ | 4/12 [00:00<00:00, 8.25it/s]
Processing rows: 42%|████▏ | 5/12 [00:00<00:00, 8.34it/s]
Processing rows: 50%|█████ | 6/12 [00:00<00:00, 8.40it/s]
Processing rows: 58%|█████▊ | 7/12 [00:00<00:00, 8.35it/s]
Processing rows: 67%|██████▋ | 8/12 [00:00<00:00, 8.27it/s]
Processing rows: 75%|███████▌ | 9/12 [00:01<00:00, 8.30it/s]
Processing rows: 83%|████████▎ | 10/12 [00:01<00:00, 8.33it/s]
Processing rows: 92%|█████████▏| 11/12 [00:01<00:00, 8.30it/s]
Processing rows: 100%|██████████| 12/12 [00:01<00:00, 8.19it/s]
Processing rows: 100%|██████████| 12/12 [00:01<00:00, 8.27it/s]
INFO:root:Added LOF scores for A10 WT Dec-13-2023: 10 channels
INFO:root:Total LOF scores collected: 1 animal days
INFO:root:Channel names: ['C-009', 'C-010', 'C-012', 'C-014', 'C-015', 'C-016', 'C-017', 'C-019', 'C-021', 'C-022']
INFO:root:Channel abbreviations: ['LAud', 'LVis', 'LHip', 'LBar', 'LMot', 'RMot', 'RBar', 'RHip', 'RVis', 'RAud']
Access information in a WindowAnalysisResult#
You can access the summary of a WindowAnalysisResult using the get_info method, and access the computed features in the WindowedAnalysis object using the get_result method.
war_info = war_all.get_info()
print(war_info)
result = war_all.get_result(
features=["all"],
exclude=['nspike', 'lognspike']
)
display(result)
feature names: rms, ampvar, psdtotal, logrms, logampvar, logpsdtotal, psdslope, psdband, psdfrac, logpsdband, logpsdfrac, cohere, zcohere, imcoh, zimcoh, pcorr, zpcorr, psd
animaldays: A10 WT Dec-13-2023
animal_id: A10
genotype: WT
sex: Male
channel_names: C-009, C-010, C-012, C-014, C-015, C-016, C-017, C-019, C-021, C-022
| animalday | animal | day | genotype | sex | duration | endfile | timestamp | isday | rms | ... | psdfrac | logpsdband | logpsdfrac | cohere | zcohere | imcoh | zimcoh | pcorr | zpcorr | psd | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | A10 WT Dec-13-2023 | A10 | Dec-13-2023 | WT | Male | 5.0 | None | 2023-12-13 12:00:00 | True | [41.579033, 97.92353, 32.154404, 81.64278, 47.... | ... | {'delta': [0.8394922767504306, 0.4122420385170... | {'delta': [7.2359638544339795, 8.1178127325940... | {'delta': [0.6094895969947209, 0.3451785397614... | {'delta': [[1.0, 0.052847900502539104, 0.88179... | {'delta': [[7.254328619247669, 0.0528971827573... | {'delta': [[0.0, -0.03406602221005716, -0.1608... | {'delta': [[0.0, -0.03407920919663371, -0.1622... | [[0.9999999999999999, 0.037751309952844855, 0.... | [[7.254328619247669, 0.037769259206984866, 1.2... | ([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0,... |
| 1 | A10 WT Dec-13-2023 | A10 | Dec-13-2023 | WT | Male | 5.0 | None | 2023-12-13 12:00:05 | True | [18.560076, 86.24687, 18.165136, 83.53615, 44.... | ... | {'delta': [0.6200069814913471, 0.4619650289122... | {'delta': [5.095568039701494, 8.03335933336877... | {'delta': [0.4824304587975665, 0.3797814410088... | {'delta': [[1.0, 0.28191164512960565, 0.268630... | {'delta': [[7.254328619247669, 0.2897575485749... | {'delta': [[0.0, 0.28147437765598293, 0.033544... | {'delta': [[0.0, 0.28928259290640757, 0.033557... | [[1.0, 0.03593432770315108, 0.2715310170582232... | [[7.254328619247669, 0.03594980674141906, 0.27... | ([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0,... |
| 2 | A10 WT Dec-13-2023 | A10 | Dec-13-2023 | WT | Male | 5.0 | None | 2023-12-13 12:00:10 | True | [19.573357, 94.37088, 15.520227, 77.23291, 44.... | ... | {'delta': [0.6664808269422602, 0.4228496877060... | {'delta': [5.296787114959302, 8.1735999121245,... | {'delta': [0.5107141137143321, 0.3526616829634... | {'delta': [[1.0, 0.35228960370774015, 0.070811... | {'delta': [[7.254328619247669, 0.3680553802191... | {'delta': [[0.0, 0.10105174993825916, -0.02241... | {'delta': [[0.0, 0.10139783468166505, -0.02242... | [[1.0, 0.23704244634315202, -0.050766569631348... | [[7.254328619247669, 0.24163814343114923, -0.0... | ([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0,... |
| 3 | A10 WT Dec-13-2023 | A10 | Dec-13-2023 | WT | Male | 5.0 | None | 2023-12-13 12:00:15 | True | [17.732212, 95.69531, 18.73452, 85.2761, 55.07... | ... | {'delta': [0.6975066221336597, 0.5597212438180... | {'delta': [5.048427524966383, 8.49682649145467... | {'delta': [0.5291604815527718, 0.4445071154342... | {'delta': [[1.0, 0.3380611405834255, 0.1572849... | {'delta': [[7.254328619247669, 0.3519018704262... | {'delta': [[0.0, 0.3086031970729731, 0.0754813... | {'delta': [[0.0, 0.31900084106873033, 0.075625... | [[0.9999999999999999, 0.06950962974335761, 0.2... | [[7.254328619247669, 0.06962190270977588, 0.20... | ([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0,... |
| 4 | A10 WT Dec-13-2023 | A10 | Dec-13-2023 | WT | Male | 5.0 | None | 2023-12-13 12:00:20 | True | [23.24282, 98.44116, 22.914125, 79.751144, 47.... | ... | {'delta': [0.6354314032139892, 0.3662477844212... | {'delta': [4.977935125893092, 8.11192566768140... | {'delta': [0.4919066247223133, 0.3120681388577... | {'delta': [[1.0, 0.20878301302165034, 0.408559... | {'delta': [[7.254328619247669, 0.2118985540641... | {'delta': [[0.0, -0.012069868112807969, -0.128... | {'delta': [[0.0, -0.012070454283746026, -0.129... | [[1.0, 0.180824926280874, 0.4286710799713663, ... | [[7.254328619247669, 0.18283536852262838, 0.45... | ([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0,... |
| 5 | A10 WT Dec-13-2023 | A10 | Dec-13-2023 | WT | Male | 5.0 | None | 2023-12-13 12:00:25 | True | [17.241781, 105.428406, 15.519405, 89.85128, 3... | ... | {'delta': [0.555034942875196, 0.25361302340113... | {'delta': [4.557901322309303, 7.88980286901682... | {'delta': [0.44149801668109834, 0.226029800806... | {'delta': [[1.0, 0.11647650789946269, 0.269612... | {'delta': [[7.254328619247669, 0.1170075744981... | {'delta': [[0.0, 0.015849253203784112, -0.2093... | {'delta': [[0.0, 0.01585058050844544, -0.21248... | [[0.9999999999999998, 0.02292007189141059, -0.... | [[7.254328619247669, 0.02292408668840441, -0.1... | ([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0,... |
| 6 | A10 WT Dec-13-2023 | A10 | Dec-13-2023 | WT | Male | 5.0 | None | 2023-12-13 12:00:30 | True | [18.340324, 99.834656, 13.69452, 90.62198, 41.... | ... | {'delta': [0.6967644672134189, 0.1747165310925... | {'delta': [4.929977486985523, 7.36238936310540... | {'delta': [0.5287231829914419, 0.1610268683566... | {'delta': [[1.0, 0.33374518791917396, 0.096689... | {'delta': [[7.254328619247669, 0.3470369982934... | {'delta': [[0.0, 0.3311516384378194, 0.0781511... | {'delta': [[0.0, 0.3441211850020442, 0.0783108... | [[1.0, 0.07892201842892256, -0.006921286611618... | [[7.254328619247669, 0.07908649367718537, -0.0... | ([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0,... |
| 7 | A10 WT Dec-13-2023 | A10 | Dec-13-2023 | WT | Male | 5.0 | None | 2023-12-13 12:00:35 | True | [31.301493, 88.19247, 27.26588, 86.201485, 53.... | ... | {'delta': [0.6801389965180997, 0.5458004522639... | {'delta': [6.281870498019785, 8.21696607518608... | {'delta': [0.51887652601541, 0.435541868266561... | {'delta': [[1.0, 0.37488754421787956, 0.739668... | {'delta': [[7.254328619247669, 0.3940978289650... | {'delta': [[0.0, 0.2466721913699601, 0.1272191... | {'delta': [[0.0, 0.2518662817082191, 0.1279121... | [[1.0, 0.09881790273408117, 0.7073891646313437... | [[7.254328619247669, 0.09914145207704674, 0.88... | ([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0,... |
| 8 | A10 WT Dec-13-2023 | A10 | Dec-13-2023 | WT | Male | 5.0 | None | 2023-12-13 12:00:40 | True | [21.571564, 86.56506, 16.248945, 79.39301, 37.... | ... | {'delta': [0.729925378199047, 0.32151271477140... | {'delta': [5.240733686776997, 7.61770640842085... | {'delta': [0.5480782735904001, 0.2787770764850... | {'delta': [[1.0, 0.1616439333013485, 0.2224047... | {'delta': [[7.254328619247669, 0.1630742769455... | {'delta': [[0.0, 0.047986595018648465, 0.06744... | {'delta': [[0.0, 0.04802347911572332, 0.067545... | [[1.0, 0.1706744034102583, 0.10969440766749147... | [[7.254328619247669, 0.1723612193120727, 0.110... | ([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0,... |
| 9 | A10 WT Dec-13-2023 | A10 | Dec-13-2023 | WT | Male | 5.0 | None | 2023-12-13 12:00:45 | True | [16.36986, 95.80101, 15.843812, 81.52692, 38.3... | ... | {'delta': [0.555917453436529, 0.24234958660659... | {'delta': [4.709318000199209, 7.56266356846839... | {'delta': [0.4420653738583102, 0.2170044146035... | {'delta': [[1.0, 0.27845051413190525, 0.086616... | {'delta': [[7.254328619247669, 0.2860015622225... | {'delta': [[0.0, -0.09476109967286789, -0.0613... | {'delta': [[0.0, -0.09504627875334003, -0.0614... | [[1.0, 0.24908234176163924, 0.1258532235047629... | [[7.254328619247669, 0.2544342155874704, 0.126... | ([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0,... |
| 10 | A10 WT Dec-13-2023 | A10 | Dec-13-2023 | WT | Male | 5.0 | None | 2023-12-13 12:00:50 | True | [18.14612, 98.15744, 17.273647, 102.89021, 43.... | ... | {'delta': [0.7641315933507349, 0.3023807939300... | {'delta': [5.320599994112459, 7.96377095602449... | {'delta': [0.5676585542073808, 0.2641939694821... | {'delta': [[1.0, 0.30248645485167047, 0.135491... | {'delta': [[7.254328619247669, 0.3122542205764... | {'delta': [[0.0, 0.15899144259343576, -0.12144... | {'delta': [[0.0, 0.16035181233815562, -0.12204... | [[0.9999999999999998, 0.23249225432116594, -0.... | [[7.254328619247669, 0.23682252493527328, -0.0... | ([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0,... |
| 11 | A10 WT Dec-13-2023 | A10 | Dec-13-2023 | WT | Male | 5.0 | None | 2023-12-13 12:00:55 | True | [19.386366, 117.82576, 17.906065, 101.979576, ... | ... | {'delta': [0.7129156158323636, 0.4462875424681... | {'delta': [5.208454290292114, 8.70067213744830... | {'delta': [0.5381969570850472, 0.3689999576802... | {'delta': [[1.0, 0.15730605449840065, 0.096550... | {'delta': [[7.254328619247669, 0.1586231891513... | {'delta': [[0.0, -0.04976984568280636, 0.04931... | {'delta': [[0.0, -0.04981100079115302, 0.04935... | [[1.0, 0.0915998635572403, 0.00489768571212678... | [[7.254328619247669, 0.09185735170411383, 0.00... | ([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0,... |
12 rows × 27 columns
Selective Feature Computation#
For faster processing, compute only needed features:
# Compute specific features
war_selective = AnimalAnalyzer(ao).compute_windowed_analysis(
features=['rms', 'logrms', 'psdband', 'cohere'],
multiprocess_mode='serial'
)
result = war_selective.get_result(
features=['rms', 'logrms', 'psdband', 'cohere']
)
print(f"Result columns: {list(result.keys())}")
Result columns: ['animalday', 'animal', 'day', 'genotype', 'sex', 'duration', 'endfile', 'timestamp', 'isday', 'rms', 'logrms', 'psdband', 'cohere']
INFO:root:Computing windowed analysis for Cage 2 A10-0_ColMajor.bin...
Processing rows: 0%| | 0/12 [00:00<?, ?it/s]
Processing rows: 42%|████▏ | 5/12 [00:00<00:00, 41.91it/s]
Processing rows: 83%|████████▎ | 10/12 [00:00<00:00, 42.63it/s]
Processing rows: 100%|██████████| 12/12 [00:00<00:00, 42.46it/s]
INFO:root:Added LOF scores for A10 WT Dec-13-2023: 10 channels
INFO:root:Total LOF scores collected: 1 animal days
INFO:root:Channel names: ['C-009', 'C-010', 'C-012', 'C-014', 'C-015', 'C-016', 'C-017', 'C-019', 'C-021', 'C-022']
INFO:root:Channel abbreviations: ['LAud', 'LVis', 'LHip', 'LBar', 'LMot', 'RMot', 'RBar', 'RHip', 'RVis', 'RAud']
Parallel Processing#
multiprocess_mode accepts two values: 'serial' and 'dask'. Serial is the right choice
for debugging and for small recordings like the example data.
'dask' builds one dask.delayed task per window and calls dask.compute(). In the cell below,
dask.compute() uses Dask’s threaded scheduler instead of distributing anything. To get
process-level parallelism, start a cluster and a client, then run the
analysis inside that context. This is the approach that the Snakemake pipeline uses for real datasets.
See Parallel Processing in the configuration guide for
a LocalCluster example.
The Dask path also drops the last fragment unconditionally, even when it is a full window, so the count below is one lower than the serial run above.
# Dask mode, for distributed computing.
war_dask = AnimalAnalyzer(ao).compute_windowed_analysis(
features=['rms', 'psdband'],
multiprocess_mode='dask'
)
print(f"Dask run produced {len(war_dask.result)} windows")
Dask run produced 11 windows
INFO:root:Computing windowed analysis for Cage 2 A10-0_ColMajor.bin...
INFO:root:Added LOF scores for A10 WT Dec-13-2023: 10 channels
INFO:root:Total LOF scores collected: 1 animal days
INFO:root:Channel names: ['C-009', 'C-010', 'C-012', 'C-014', 'C-015', 'C-016', 'C-017', 'C-019', 'C-021', 'C-022']
INFO:root:Channel abbreviations: ['LAud', 'LVis', 'LHip', 'LBar', 'LMot', 'RMot', 'RBar', 'RHip', 'RVis', 'RAud']
3. Data Quality and Filtering#
Method Chaining#
Apply multiple filters in sequence. This is the recommended format.
war_filtered = (
war_all
.filter_logrms_range(z_range=3) # Remove outliers (±3 SD)
.filter_high_rms(max_rms=500) # Remove high amplitude artifacts
.filter_low_rms(min_rms=10) # Remove low amplitude periods
# .filter_high_beta(max_beta_prop=0.4) # Remove high beta activity
.filter_reject_channels_by_session() # Reject bad channels
)
print("Filtering completed!")
Filtering completed!
INFO:root:logrms_range: filtered 0/120
INFO:root:high_rms: filtered 0/120
INFO:root:low_rms: filtered 0/120
INFO:root:reject_channels_by_session: filtered 0/120
Configuration-Driven Filtering#
An alternative approach uses a configuration dictionary:
filter_config = {
'logrms_range': {'z_range': 3},
'high_rms': {'max_rms': 500},
'low_rms': {'min_rms': 10},
# 'high_beta': {'max_beta_prop': 0.4},
'reject_channels_by_session': {},
'morphological_smoothing': {'smoothing_seconds': 4.0}
}
war_filtered_config = war_all.apply_filters(
filter_config,
min_valid_channels=3
)
INFO:root:logrms_range: filtered 0/120
INFO:root:high_rms: filtered 0/120
INFO:root:low_rms: filtered 0/120
INFO:root:reject_channels_by_session: filtered 0/120
INFO:root:morphological_smoothing: applied (post-mask)
Available Filters#
Chainable methods on a WindowAnalysisResult, each returning a new result:
filter_logrms_range(z_range=3): Remove outliers based on log RMSfilter_high_rms(max_rms=500): Remove high amplitude artifactsfilter_low_rms(min_rms=50): Remove low amplitude periodsfilter_high_beta(max_beta_prop=0.4): Remove high beta activity (muscle artifacts)filter_reject_channels(bad_channels): Reject named channels everywherefilter_reject_channels_by_session(bad_channels_dict): Reject channels per recording sessionfilter_all(...): Apply the standard set in one call
Morphological smoothing works differently from the rest.
get_filter_morphological_smoothing() takes an existing mask rather than producing one,
since it post-processes. It is normally applied through the morphological_smoothing key
of apply_filters() (shown above).
4. Data Aggregation#
Average across time windows, producing a single row per group (e.g., per recording session and light/dark phase). Channel information is preserved.
aggregate_time_windows() modifies the object in place and returns None, so work on a
copy if you still need the per-window data afterwards, as the circadian section below does.
# Aggregate time windows on a copy, keeping war_filtered per-window for later sections
war_aggregated = war_filtered.copy()
war_aggregated.aggregate_time_windows()
print(f"Before aggregation: {len(war_filtered.result)} rows")
print(f"After aggregation: {len(war_aggregated.result)} rows")
Before aggregation: 12 rows
After aggregation: 1 rows
INFO:root:Channel names: ['C-009', 'C-010', 'C-012', 'C-014', 'C-015', 'C-016', 'C-017', 'C-019', 'C-021', 'C-022']
INFO:root:Channel abbreviations: ['LAud', 'LVis', 'LHip', 'LBar', 'LMot', 'RMot', 'RBar', 'RHip', 'RVis', 'RAud']
/home/runner/work/neurodent/neurodent/src/neurodent/results/window_analysis_result.py:561: FutureWarning: DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.
aggregated_df = result_grouped.apply(
INFO:root:Setting suppress_short_interval_error to True
5. Channel Management#
Reorder and Pad Channels#
Ensure consistent channel ordering across animals:
# Define standard channel order
standard_channels = [
"LMot", "RMot", # Motor cortex
"LBar", "RBar", # Barrel cortex
"LAud", "RAud", # Auditory cortex
"LVis", "RVis", # Visual cortex
"LHip", "RHip" # Hippocampus
]
war_filtered.reorder_and_pad_channels(
standard_channels,
use_abbrevs=True # Use abbreviated channel names
)
print(f"Channels: {war_filtered.channel_names}")
Channels: ['LMot', 'RMot', 'LBar', 'RBar', 'LAud', 'RAud', 'LVis', 'RVis', 'LHip', 'RHip']
6. Accessing Computed Features#
WAR objects store features in a pandas DataFrame. Use get_result() to retrieve features with full channel information, or get_channel_averaged_result() to average across channels (or channel pairs for connectivity features), producing scalar values per time window.
# Get the full result DataFrame
result_df = war_filtered.get_result(
features=['rms', 'psdband', 'cohere']
)
print(f"Result columns: {list(result_df.columns)}")
print(f"Result shape: {result_df.shape}")
print(f"\nRMS values (per-channel arrays):\n{result_df['rms'].iloc[0]}")
# Get channel-averaged result (scalars per time window)
df_avg = war_filtered.get_channel_averaged_result(
features=['logrms', 'logpsdband', 'zcohere']
)
print(f"\nChannel-averaged columns: {list(df_avg.columns)}")
print(f"\nSample logrms value: {df_avg['logrms'].iloc[0]}") # single float
Result columns: ['animalday', 'animal', 'day', 'genotype', 'sex', 'duration', 'endfile', 'timestamp', 'isday', 'rms', 'psdband', 'cohere']
Result shape: (12, 12)
RMS values (per-channel arrays):
[47.1286506652832, 66.3082046508789, 81.64277648925781, 60.684078216552734, 41.57903289794922, 69.76606750488281, 97.92353057861328, 137.0225372314453, 32.15440368652344, 47.130401611328125]
Channel-averaged columns: ['animalday', 'animal', 'day', 'genotype', 'sex', 'duration', 'endfile', 'timestamp', 'isday', 'logrms', 'logpsdband_delta', 'logpsdband_theta', 'logpsdband_alpha', 'logpsdband_beta', 'logpsdband_gamma', 'zcohere_delta', 'zcohere_theta', 'zcohere_alpha', 'zcohere_beta', 'zcohere_gamma']
Sample logrms value: 4.152730774879456
7. Metadata and Grouping Variables#
WAR objects contain metadata for grouping and analysis:
# Access metadata
print(f"Animal ID: {war_filtered.animal_id}")
print(f"Genotype: {war_filtered.genotype}")
print(f"Animal days: {war_filtered.animaldays}")
print(f"Channel names: {war_filtered.channel_names}")
Animal ID: A10
Genotype: WT
Animal days: ['A10 WT Dec-13-2023']
Channel names: ['LMot', 'RMot', 'LBar', 'RBar', 'LAud', 'RAud', 'LVis', 'RVis', 'LHip', 'RHip']
8. Circadian Analysis (ZeitgeberAnalysisResult)#
Once your data is filtered and metadata (like genotype and timestamps) is verified, you can analyze circadian rhythms.
The ZeitgeberAnalysisResult wrapper uses this metadata to:
Shift Timestamps: Converts absolute timestamps to Zeitgeber Time (ZT), where ZT0 is “Lights On”. The ZT coordinate lands in the
zt_minutescolumn.Define Baseline: Subtracts a baseline period (e.g., the first 12 hours of the light phase) to normalize the data, adding
*_nobasecolumns.
Baseline subtraction only applies to numeric columns, so use get_channel_averaged_result()
rather than get_result() when you want the *_nobase columns.
Double-plotted 48-hour actograms are produced at render time by ZeitgeberPlotter, not by
this wrapper. For plotting these results, see the Visualization Tutorial.
# Wrap the result for circadian analysis
zar = ZeitgeberAnalysisResult(
war_filtered,
baseline_hours=12,
zeitgeber_shift_hours=6,
)
# Channel-average so baseline subtraction has numeric columns to work on
df_zar = zar.get_channel_averaged_result(features=['rms'])
timestamps = war_filtered.result['timestamp']
print(f"Original Time Range: {timestamps.min()} to {timestamps.max()}")
print(f"ZT Coordinate Range: {df_zar.zt_minutes.min()} to {df_zar.zt_minutes.max()} min")
print(f"Baseline-Corrected Columns: {[c for c in df_zar.columns if '_nobase' in c]}")
Original Time Range: 2023-12-13 12:00:00 to 2023-12-13 12:00:55
ZT Coordinate Range: 360.0 to 360.0 min
Baseline-Corrected Columns: ['duration_nobase', 'rms_nobase']
INFO:neurodent.results.zeitgeber:Adding binned zeitgeber time columns with 60min bins
INFO:neurodent.results.zeitgeber:Running zeitgeber analysis pipeline...
INFO:neurodent.results.zeitgeber:Using default baseline: first 12 hours (0-720 min)
INFO:neurodent.results.zeitgeber:Processed data for 1 unique animals
9. Saving and Loading#
Save WAR objects for later analysis:
# Use a temporary directory for demonstration purposes
with tempfile.TemporaryDirectory() as tmpdir:
output_path = Path(tmpdir) / animal_id
output_path.mkdir(parents=True, exist_ok=True)
# Save WAR as parquet + json
war_filtered.save_parquet_and_json(output_path)
print(f"Saved to {output_path}")
# Load WAR
war_loaded = WindowAnalysisResult.load_parquet_and_json(output_path)
print(f"Loaded {len(war_loaded.result)} rows from {output_path}")
Saved to /tmp/tmp41b4kg_l/A10
Loaded 12 rows from /tmp/tmp41b4kg_l/A10
INFO:root:Saved WAR to /tmp/tmp41b4kg_l/A10/war.parquet
INFO:root:Saved WAR to /tmp/tmp41b4kg_l/A10/war.json
INFO:root:Channel names: ['LMot', 'RMot', 'LBar', 'RBar', 'LAud', 'RAud', 'LVis', 'RVis', 'LHip', 'RHip']
INFO:root:Channel abbreviations: ['LMot', 'RMot', 'LBar', 'RBar', 'LAud', 'RAud', 'LVis', 'RVis', 'LHip', 'RHip']
Next Steps#
Visualization Tutorial: Plot and analyze WAR results
Spike Analysis Tutorial: Detect population spikes and fold counts into a WAR