Analysis and visualization of RFID reader logs from a group-housed mouse colony (4 pens × lower/upper levels, ~20 tagged animals, multi-zone antenna grids).
Turns raw, cumulative RFID exports into a clean per-read dataset and an interactive explorer (Jee's individual mouse explorer) of each mouse's behaviour. Mice roam across all pens via bridges, so the unit is the individual mouse, tracked across pens:
- Activity & circadian — locomotor activity (zone transitions) over time, day vs. night.
- Space use — occupancy per zone / pen / floor, and vertical (upper/lower) use.
- Movement — zone transitions, pen-to-pen roaming (bridge crossings), stair ascents/descents.
- Resource use — time at the food antenna over time.
The GUI and scripts/export_outcomes.py produce a per-mouse behavioral profile.
Examples below are for one mouse (M147) from the sample data.
Roaming timeline — every mouse's pen over time (colour = pen 🔴 P1 · 🟢 P2 · 🔵 P3 · 🟡 P4, dark = lower / light = upper floor; trace below each strip = zone-transition rate, dim band = lights-off):
Actogram — activity by day × hour, with a red ▽ at every food-antenna visit:
Where it is over time (pen · floor) · pen · floor movement graph (stairs = vertical, bridges = top row; impossible jumps routed through the chain):
Circadian activity profile (mouse vs. group mean) · occupancy by zone over time:
All figures are generated from src/rfid (behavior.py + plots.py); the GUI is the
interactive front-end and the export tab/script save the full set with a reproducibility manifest.
Raw CSVs live in data/. Each read is zone_label, uid, tag_no, timestamp.
Two things the pipeline handles automatically:
- Cumulative exports. Repeated exports of the same pen/level re-contain all
earlier reads. The loader de-duplicates on
(pen, level, zone, uid, timestamp)— in this dataset that collapses ~308k raw rows to ~124k unique reads. - Noise reads. Electrical misreads (garbage UIDs, no
tag_no) are dropped and tallied in the QC report. Real tags match000C12xxxxxxand carry atag_no.
Pen number, level, and export time are parsed from the filename (both
rfid-Pen1-lower-... and Pen3-upper-...-rfid naming conventions are supported).
Works on macOS, Linux, and Windows (Python 3.9+).
macOS / Linux
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtWindows (PowerShell)
python -m venv .venv
.venv\Scripts\Activate.ps1
pip install -r requirements.txtpython scripts/build_dataset.py # data/ -> outputs/reads_clean.parquet (+ roster, report)
python scripts/make_figures.py # outputs/reads_clean.parquet -> outputs/figures/*.pngAn interactive local app (Streamlit) to explore each mouse's behavioral pattern.
streamlit run app/rfid_gui.pyIt opens in your browser. Upload your RFID CSVs (or point to a folder) and click Load data — the mouse and time-window menus then populate from that dataset. Mice roam across all pens, so you pick a mouse, not a pen. Set the light cycle, time bin (Δt), dwell cap, and date range — every metric depends on them, so they're exposed rather than hidden. Tabs:
- Overview — signature metrics (located %, moves/day, dwell % per pen, food dwell/day, ascents/descents) + the roaming timeline.
- Activity & Circadian — activity over time, food-dwell trace, actogram (▽ = food tags), circadian profile.
- Space use — pen/floor occupancy over time, per-zone occupancy, floor dwell.
- Movement — zone transition matrix + pen-to-pen network (arrows + counts).
- Export — save outcomes (see below).
- Data / QC — per-mouse summary + cleaning stats.
Compute/figures live in src/rfid/behavior.py, plots.py, report.py (shared
with the scripts); the GUI is the thin UI on top.
See SAVE_STRATEGY.md. Every export is a timestamped, reproducible
folder under outputs/exports/ with the cleaned data, figures, summary, and a
manifest.json recording the parameters + zone map used.
- From the GUI Export tab: summary CSV, per-mouse figure ZIP, or full run to disk.
- Headless:
python scripts/export_outcomes.py --data data --bin 60min --dwell-cap 300
Review these assumptions before trusting the output:
light_cycle— lights-on/off hours used to label day vs. night. Set to your vivarium's schedule (default 07:00–19:00).zones—usbNlabels are mapped to zoneN. If your USB-port numbering doesn't match physical zone numbering, addoverrides.social.contact_window_seconds— how close in time two reads at the same zone must be to count as a contact (default 30 s).roster— optional: attach sex / genotype / cage pertag_no.
config.yaml assumptions (light cycle, zones, contact window, roster)
data/ raw RFID CSV exports
src/rfid/
loader.py discover + parse + de-duplicate raw files
clean.py zone normalization, animal mapping, noise filtering, time features
activity.py activity levels & circadian profiles
social.py co-location / contact detection & network
space.py dwell-time occupancy & spatial preference
qc.py data-quality summaries
scripts/
build_dataset.py raw -> clean parquet
make_figures.py clean parquet -> figures
outputs/ processed data, roster, report, figures/
- Activity is measured both as raw reads and as zone transitions (movement between antennas), which is less inflated by an animal lingering under one reader.
- Occupancy uses estimated dwell time (gap to each animal's next read, capped at 5 min) rather than read counts, to better reflect time spent.
- Contacts are only counted within the same pen+level+zone, so animals in different pens are never treated as together.





