What it is
Real WiFi-CSI dataset for sitting-vs-standing posture/movement detection (William Taylor, University of Glasgow — 2026), captured on Raspberry-Pi + Nexmon commodity hardware. A clean, balanced single-person binary-posture set; relevant as a real-hardware posture/motion reference on the Pi/Nexmon chipset family the lab deploys (IP-112), not as a crowd-counting resource.
Verified specs
(from direct inspection of the delivered .npy files, 2026-07-11 — not the upstream README)
- Hardware: Raspberry Pi 3 Model B+, Broadcom BCM43455c0, Nexmon-CSI framework.
- Band / bandwidth: 2.4 GHz, Channel 7, 20 MHz, 64 subcarriers.
- Scale: 5,000 instances — perfectly balanced 2,500 sitting + 2,500 standing.
- Per-instance tensor: each
.npyis(2000, 64)complex128— 2,000 packets × 64 subcarriers of raw complex CFR (amplitude and phase, not pre-reduced to amplitude). - Labels: binary sitting / standing, encoded in the filename prefix
(
sitting_<n>.npy/standing_<n>.npy) and in_manifest.json. - Format on S3:
csi_data/*.npy(the delivered payload is pure.npy; the upstream listing's.csv/.npzvariants were not part of what we received). - Data-quality caveat (verified 2026-07-11): 80 of the 5,000
.npyfiles (40 sitting + 40 standing) are not arrays at all — they are PNG images (~35–36 KB,\x89PNGmagic) mis-saved with a.npyextension (a systematic upstream export artifact, ~1 per 62 files). Every valid CSI instance is exactly 2,048,128 bytes; filterfiles[]onbytes == 2048128to get the clean 4,920 (2,460 + 2,460). Loaders MUST byte-filter (or check the npy magic) ornp.loadraisesUnpicklingError.
Acquisition & storage
- Status: acquired — full 5,000-file payload mirrored to project S3
(
s3://monad-knowledge/datasets/csi-wifi-human-movement-detection/, ~10.1 GB, DOI10.21227/aj4k-p777). - Load pattern: read
_manifest.json(one GET), filterfiles[]bylabel, then stream the.npyinstances in batches via boto3 — never sync the full 10 GB locally. See thedatasetsskill (.claude/skills/datasets/) and itss3_dataset_loader.py(iter_npy). Each.npyloads withnp.load(BytesIO(get_object(...))). - Regime caveat: 2.4 GHz is the presence/coarse-motion band (occupancy-csi-variance); expect strong sitting-vs-standing separation but no bearing on graded count.
Empirical findings (2026-07-11) — no leakage; amplitude-only features ≈ chance
(reduction: monad_knowledge/notebooks/python/csi_realdata_posture_features.py; n = 1,200 balanced
clean instances, 5-fold ROC-AUC + bootstrap CI, contaminants excluded)
Three amplitude-derived feature families (raw phase deliberately excluded — CFO/SFO-corrupted on a
single Nexmon stream), each evaluated under a random split and a temporal split (train on
low-index half of each class, test on high-index half — a held-out-session proxy):
| family | random AUC (RF) | temporal-split AUC |
|---|---|---|
| static fingerprint (per-sc mean |CSI|) | 0.54 [0.51–0.57] | 0.52 |
| temporal-variance (per-sc STD) | 0.49 [0.45–0.52] | 0.54 |
| Doppler spectral-shape | 0.57 [0.54–0.60] | 0.54 |
| all combined | 0.57 [0.54–0.60] | 0.56 |
Read-outs:
- No session-fingerprint leakage. random ≈ temporal split throughout (|gap| ≤ 0.06), unlike csi-wifi-human-detection where naive CV hit AUC 1.0. The honest numbers here are real.
- Static amplitude fingerprint and temporal-variance magnitude are at chance (~0.5). Sitting vs standing is not encoded in per-subcarrier mean amplitude, and — against the fallani2026 prior — not in the magnitude of amplitude fluctuation either.
- The only above-chance signal (AUC ≈ 0.57) lives in the Doppler/spectral shape of the per-subcarrier time series, and it drives the combined model. Direction-consistent with count-signal-in-temporal-doppler (what little there is, is temporal/Doppler-shaped, not static level), but weak. Caveat: single subject, amplitude-only handcrafted features; phase and sequence models (the fallani2026 phase-difference result, deep HAR on the full (2000,64) tensor) are untested here and are the obvious next step before any claim.
Follow-up (2026-07-12): phase + PSD deepening — a permutation-validated NULL
(reduction: monad_knowledge/notebooks/python/csi_realdata_posture_phase.py; n = 1,600 balanced)
Added Sen/Wang-calibrated phase (per-packet linear detrend of the unwrapped phase across
subcarriers — removes CFO/SFO/STO on the single stream), its temporal derivative, and Welch-PSD
band-shape features, then compared amplitude vs phase, static vs dynamic:
| family (dim) | logreg AUC | RF AUC | temporal-split |
|---|---|---|---|
| amp static (cal mean, 64) | 0.50 | 0.49 | 0.51 |
| amp dynamic (std+PSD, 68) | 0.49 | 0.53 | 0.51 |
| phase static (cal mean, 64) | 0.49 | 0.51 | 0.50 |
| phase dynamic (std+PSD, 68) | 0.47 | 0.46 | 0.46 |
| phase all (132) | 0.47 | 0.48 | 0.48 |
| amp+phase all (264) | 0.49 | 0.54 | 0.50 |
- Phase does not rescue it. Every family is at/below chance; the best (amp+phase RF 0.54) does not survive the temporal split (0.50). The larger n also collapsed the R2 0.57 to ~0.53 — that was near-noise.
- Permutation test (200 perms, full 264-feature RF): true AUC = 0.524 vs null 0.500 ± 0.020, p = 0.109 — not significant. Best univariate AUC across all 264 features = 0.528.
- Not a curation bug: 59/64 subcarriers have distinct class medians, 0 cross-class duplicate arrays. The difference is real but tiny — KS on per-subcarrier temporal-STD gives D ≈ 0.05–0.06 (p 0.10–0.20), standing marginally more variable than sitting (consistent with fallani2026's qualitative note, far too small to classify).
CNN follow-up (2026-07-12): posture IS there, but only in 2-D time-frequency structure
(reduction: monad_knowledge/notebooks/python/csi_realdata_posture_cnn.py; n = 1,000 balanced)
A small 2-D CNN over Doppler spectrograms (amplitude + calibrated-phase channels; STFT over the
packet axis, averaged across subcarriers) separates the postures where handcrafted features could not:
held-out temporal-split test AUC = 0.688 [0.641, 0.736] (random split 0.859 [0.814, 0.900]). On
the same temporal split the handcrafted families scored 0.50, so the signal lives in the 2-D
time-frequency structure that per-subcarrier marginals discard — not a per-instance fingerprint.
Verdict (revised): sitting vs standing is real but representation-bound — invisible to interpretable per-subcarrier features (permutation-null), recoverable only by a learned spatio-temporal model at ~0.69 held-out (0.86 random is optimistic — within-recording adjacent-frame leakage). Caveats keep it off the critical path: single subject, and the temporal split is a held-out portion of one recording (not an independent session), so cross-session/subject generalisation is untested. With the 80-PNG contamination, treat as a modelling curiosity, not a deployable posture reference. Use wimans (multi-activity, multi-subject, labelled) for motion/feature-type work; reserve first-party multi-session capture (IP-112) for deployable claims.
Why it matters here
A posture-labelled Pi/Nexmon set on 2.4 GHz — a controlled real-hardware motion reference that could sanity-check the motion-type feature (feature-type-not-modality) on the deployment chipset, and a candidate fixture for the IP-112 measurement protocol. Because it ships raw complex CSI (phase retained), it is also a small, honest test bed for phase-sanitisation / Doppler features on the exact BCM43455c0 hardware. Low priority relative to the crowd-count anchors (wimans, meneghello-80mhz-csi, wiflow).