✦ Day 1 · NumPy Foundations

Indexing, Slicing
& Dicing Neural Data

Master the eight essential NumPy operations for navigating a 5-dimensional EEG dataset — from a single integer index to the critical memory difference between a view and a copy.

🧠 5-D Array: (G, T, S, C, F) 📡 32 Channels · 6 Features 👥 3 Groups · 30 Subjects ⏱ 3 Timepoints 🐍 Pure NumPy

Every indexing operation in this guide targets a single array: data, loaded from neural_data.npy. Its five axes encode the full experimental structure. Shape: (G=3, T=3, S=30, C=32, F=6).

axis 0

G = 3
Groups
Control · Patient · Treatment

axis 1

T = 3
Timepoints
Baseline · Task · Rest

axis 2

S = 30
Subjects
per group

axis 3

C = 32
Channels
EEG electrodes

axis 4

F = 6
Features
δ θ α β γ broadband


1

Single-element access

Provide one integer per axis to pull out a single scalar. Python counts from zero, and negative indices count backwards from the end-1 is always the last element on that axis.

Python
# First element on every axis → scalar
v = data[0, 0, 0, 0, 0]

# Patient(1) · timepoint 2 · subject 14 · channel 9 · alpha(2)
v2 = data[1, 2, 14, 9, 2]

# Negative indices count from the end
data[-1, -1, -1, -1, -1]   # last Treatment · last time · last subject · Oz · gamma

Mental model: Think of each integer as locking a door on that axis. Five integers → five locked doors → one scalar falls out. The result has zero dimensions — it is a pure Python number.


2

Start & stop along one axis

Replace an integer with start:stop to select a range of elements along that axis. Use bare : to select all elements. Omitting start defaults to 0; omitting stop defaults to the axis length.

Python
# ':' means ALL elements along that axis
ts = data[1, :, :, 9, 2]       # shape (3, 30) — Patient, all time, all subjects, Cz, alpha

# First half of channels
first_half = data[1, 0, 0, :C//2, 2]   # shape (16,)

# Last half of channels
secnd_half = data[1, 0, 0, C//2:, 2]  # shape (16,)

# Controls · Baseline · all subjects · all channels · alpha
epoch = data[0, 0:2, :, :, 2]            # shape (2, 30, 32)

Stop is exclusive. data[..., :C//2, ...] returns indices 0 … C//2 - 1. This is why :C//2 and C//2: partition the axis perfectly without overlap or gap.


3

Combining slices across all dims

Any mix of integers and slices can appear across the five axes simultaneously. NumPy resolves each axis independently and returns the tensor product of the surviving index sets. A common EEG pattern: lock group + timepoint, slice subjects + channels, lock feature.

Python
fron = regions["frontal"]

# Patient · Baseline · all subjects · frontal channels only · alpha
fron_alpha_incorrect = data[1, 0, :, fron, 2]
fron_alpha_correct = data[1, 0, :, 2:7, 2]     # shape (30, 5)

# All groups · patient+treatment windows · first 10 subjects · all ch · theta+alpha
combo = data[:, 1:3, :10, :, 1:3]      # shape (3, 2, 10, 32, 2)

# Compare occipital vs frontal alpha across ALL data
occ_alpha = data[:, :, :, 17:20, 2].mean()   # scalar
fro_alpha = data[:, :, :,  2:7,  2].mean()   # scalar

Output shape rule: Count the colons — each one contributes a dimension to the output. Integers reduce the rank; slices preserve it. data[1, 0, :, 2:7, 2] has 2 surviving axes → shape (30, 5).


4

Downsampling & reversal

Add a third number to the slice syntax: start:stop:step. A step of 2 takes every other element. A step of -1 reverses the axis — perfect for flipping the time axis to inspect causality or symmetry. Both operations return views.

Python
# Take every alternate subject
ds4 = data[:, :, 0::2, :, :]          # shape (3, 3, 15, 32, 6)

# Every alternate (odd-indexed) subject
ds4_odd  = data[:, :, 1::2, :, :]    # shape (3, 3, 15, 32, 6)

# Reverse the time axis for Control · sub0 · channel0 · all features
rev = data[0, ::-1, 0, 0, :]         # shape (3, 6)  — time runs backward

⤵ Downsampling

::2 → indices 0, 2, 4, …
1::2 → indices 1, 3, 5, …
Together they perfectly partition subjects into even and odd cross-validation folds.

↺ Reversal

::-1 → step is -1, start defaults to the last index. Produces a flipped view — no data is copied and no extra memory is allocated.


5

Filter by value condition

A comparison like data > 1.0 produces a boolean array of the same shape. Passing it back into data[mask] extracts only the elements where the mask is True, returning a 1-D array of surviving values.

np.where(condition) (single argument) returns the indices of True positions — useful for downstream operations that need coordinates, not values.

Python
# What fraction of values could be artifacts (> 1.0)?
art_mask = data > 1.0
art_mask.sum()                            # total artifact cells

# Per-subject mean alpha for Control group  →  shape (30,)
ctrl_alpha = data[0, :, :, :, 2]          # (3, 30, 32)
subj_mean  = ctrl_alpha.mean(axis=(0,2))  # collapse time & channels → (30,)
threshold  = subj_mean.mean() + subj_mean.std()
high_alpha = np.where(subj_mean > threshold)[0]  # high-alpha subject indices

# Find timepoints where Patient broadband spikes above mean + 2σ
pat_bb  = data[1, :, :, :, 5].mean(axis=(1,2))   # (3,)
thr2    = pat_bb.mean() + 2 * pat_bb.std()
spike_t = np.where(pat_bb > thr2)[0]             # spike timepoint indices
print(f"  Broadband spikes (Patient)  : {len(spike_t)} timepoints, first few={spike_t[:5]}")

# Compound condition: high alpha AND high beta simultaneously
hi_a = data[1,:,:,:,2] > data[1,:,:,:,2].mean() + data[1,:,:,:,2].std()
hi_b = data[1,:,:,:,3] > data[1,:,:,:,3].mean() + data[1,:,:,:,3].std()
print(f"  High-alpha & high-beta (Patient): {(hi_a & hi_b).sum():,} cells  "
      f"({100*(hi_a & hi_b).mean():.2f}%)")

Boolean indexing always returns a copy. The output is a flat 1-D array, because there is no guarantee the surviving elements form a regular rectangular block. Modifying it does not affect data.


6

np.where, np.take & np.clip

Three closely related utilities extend the indexing toolbox beyond pure subscripting:

  • np.where(cond, a, b) — element-wise ternary; same shape as cond
  • np.where(cond) — returns index arrays of True positions (like find())
  • np.take(arr, indices, axis) — gather non-contiguous elements along one axis
  • np.clip(arr, lo, hi) — clamp all values into [lo, hi]
Python
# np.where(cond, true_val, false_val) — zero out artifacts
clean = np.where(data > 1.0, 0.0, data)   # clean.max() ≤ 1.0

# np.clip — related utility for clamping (not indexing but often paired)
clamped = np.clip(data, 0, 1.0)

# np.where with one arg returns indices (like find())
idxs = np.where(subj_mean > threshold)    # tuple of arrays, one per dim
print(f"  np.where(condition) → indices: {idxs[0]}")

# np.take — gather along one axis with an index array
tba = np.take(data, indices=[1, 2, 3], axis=4)

np.where (3-arg)

Operates element-by-element. Returns an array of the same shape. Ideal for artifact zeroing without losing the original array layout.

np.take vs fancy index

np.take(data, [1,2,3], axis=4) equals data[..., [1,2,3]]. Both produce a copy. np.take is preferred when the axis is chosen at runtime.


7

Changing array shape

reshape reinterprets the memory layout without touching the data — it returns a view when possible. The total number of elements must stay constant. ravel() flattens to 1-D, and np.newaxis inserts a length-1 dimension for broadcasting or framework compatibility.

Python
# Some analysis pipelines want the data in specific order
# Bring subjects axis next to groups → (G, S, T, C, F)
d_swap = data.transpose(0, 2, 1, 3, 4)

# Move features axis first: (G,T,S,C,F) → (F,G,T,S,C)
feat_first = data.transpose(4, 0, 1, 2, 3)

# Step 2: flatten groups × subjects into rows, rest into columns
X = d_swap.reshape(G * S, T * C * F)
print(f"  Design matrix (G*S, T*C*F)  : {X.shape}  — ready for PCA / sklearn")

# Flatten completely to 1-D
flat = data.ravel()

# Merge subjects & channels into one axis (common for connectivity analyses)
merged = data.reshape(G, T, S * C, F)
print(f"  Merge S×C                    : {merged.shape}")
📊

Design Matrix for scikit-learn

X.shape = (90, 576)

90 = 3 groups × 30 subjects  •  576 = 3 times × 32 channels × 6 features

Always transpose before reshape when collapsing multiple axes together. reshape merges axes in C-order (row-major), so the axes you want to merge must be adjacent first. A transpose rearranges them into the right order.


8

Assigning data to a variable & memory issues

When assigning array slices to a variable, understanding the distinction between a VIEW and a COPY is critical to prevent accidental data mutation.

Shared Google Doc (VIEW): Basic slicing (e.g. grades[0:2]) creates a view. Both variables share the same physical memory space. Modifying the view changes the original array too, just like two people editing a shared Google Doc online.

Downloaded Word Doc (COPY): Fancy indexing (e.g. grades[[0, 1]]) or explicit .copy() creates an independent copy in memory. Like downloading a Google Doc to your local machine as a Word document, you can modify it without altering the original.

👁 Basic Slicing = "Shared Google Doc" (VIEW)
  • Basic slice: top_two = grades[0:2]
  • Shares same physical memory
  • Mutating slice mutates original! 🚨
📋 Fancy Indexing = "Downloaded Doc" (COPY)
  • Fancy index: first_two = grades[[0, 1]]
  • Explicit copy: grades[0:2].copy()
  • Independent memory — original SAFE! ✅
Python
# A simple 1D array of student grades
grades = np.array([85, 90, 75, 100, 95])
print(f"Original Grades: {grades}\n")

# BASIC SLICING = "Shared Google Doc" (VIEW)
# Slicing creates a VIEW. They share the same memory.
top_two = grades[0:2]

top_two[0] = 10  # Oh no, the teacher made a typo!
print(f"The View changed to:    {top_two}")
print(f"The Original array is:  {grades}  <-- MUTATED! 🚨\n")

# Reset grades array for second scenario
grades = np.array([85, 90, 75, 100, 95])

# FANCY INDEXING = "Downloaded Doc" (COPY)
# Passing a list of indices creates a COPY. Independent memory.
first_two = grades[[0, 1]]

first_two[0] = 10
print(f"The Copy changed to:    {first_two}")
print(f"The Original array is:  {grades}  <-- SAFE! ✅\n")

# Best practice: explicitly demand a copy
first_two_grades = grades[0:2].copy()  # Explicit copy

The silent-mutation bug: Basic slicing works like a Shared Google Doc. If you pass a slice to a function and that function modifies it in-place, your original array is permanently altered — with no warning or error. Always call .copy() to create an independent copy when modifying data.

💡

The Golden Rule

Basic slicing grades[0:2]VIEW (Shared Doc)
Fancy indexing grades[[0,1]] or .copy()COPY (Downloaded Doc)

When in doubt: arr.base is not None indicates arr is a view.


Day 1 Cheat-Sheet

Section Syntax example Returns View / Copy
1 Integer data[0,0,0,0,0] Scalar (0-D) View
2 Range slice data[1, :, 0, :16, 2] N-D array View
3 Multi-axis data[:, 1:3, :10, :, 1:3] N-D array View
4 Step slice data[:, :, ::2, :, :] N-D array View
5 Boolean mask data[data > 1.0] 1-D array Copy
6 np.where / np.take np.where(cond, a, b) Same shape Copy
7 Reshape / Ravel data.reshape(G*S, -1) N-D array View (usually)
8 Variable assignment grades[0:2] vs grades[[0,1]] View / Copy Memory gotcha
📚

Further reading

  1. Advent of code 🎥
    A programming puzzles advent calendar video walkthrough.
  2. Software carpentry programming with python 📖
    A great introduction to programming in Python.
  3. Python Data Science Handbook by Jake Vanderplas 📖
    A comprehensive guide to doing data science in Python.
  4. You can't do data science in a GUI by Hadley Wickham 🎥
    Hadley Wickham's insightful talk on why programmatic data science scales better.