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 .npy is (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/.npz variants were not part of what we received).
  • Data-quality caveat (verified 2026-07-11): 80 of the 5,000 .npy files (40 sitting + 40 standing) are not arrays at all — they are PNG images (~35–36 KB, \x89PNG magic) mis-saved with a .npy extension (a systematic upstream export artifact, ~1 per 62 files). Every valid CSI instance is exactly 2,048,128 bytes; filter files[] on bytes == 2048128 to get the clean 4,920 (2,460 + 2,460). Loaders MUST byte-filter (or check the npy magic) or np.load raises UnpicklingError.

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, DOI 10.21227/aj4k-p777).
  • Load pattern: read _manifest.json (one GET), filter files[] by label, then stream the .npy instances in batches via boto3 — never sync the full 10 GB locally. See the datasets skill (.claude/skills/datasets/) and its s3_dataset_loader.py (iter_npy). Each .npy loads with np.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).