Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Authored by Souvik Mandal, Ph.D.

Project Leader & Instructor, Computational Behavioral Sciences, LS100, FAS, Harvard University | Linkedin ID: souvik-mandal-phd


Last updated: 2026-08-25

Open in Colab Open in GitHub Codespaces

In Notebooks 01 and 02 you learned to store data, automate repetitive work with loops and conditionals, and wrap logic into functions. That is already enough to get research done. This notebook is about getting fluent and efficient — writing code that is compact, readable, and fast enough to handle real research data.

Why does this matter? Imagine you record 200 minutes of video at 30 frames per second. That is 360,000 frames, each with dozens of numbers. A small, careless choice — like searching a list over and over — can turn a two-second job into a two-hour one. The difference is not the computer; it is how you think about the problem. That way of thinking is called computational thinking, and it rests on two pillars:

  1. Choosing the right data structure — the container you store data in determines how fast you can ask questions of it.

  2. Recognising algorithmic patterns — most problems are variations of a handful of patterns (counting, sorting, windowing, ...). Once you can name the pattern, the solution follows.

To build these skills we will solve a series of LeetCode / HackerRank-style problems, framed around the kind of data you will meet in this course. By the end you will also see why bundling data together with the functions that act on it leads naturally to Classes, which is exactly where Notebook 05 picks up.

How to Efficiently Use This Notebook

  • Run every code cell yourself, top to bottom. Reading code is not the same as understanding it.

  • When you meet a new function, pause and print() the intermediate values. Curiosity here pays off.

  • For each problem, try it on paper or in your head first, then read the solution.

  • Many problems are shown in two versions: a first, explicit version that mirrors how you would reason it out, and a second, more Pythonic version. Compare them — the gap between the two is exactly the fluency we are building.

  • Mini Challenges and Checkpoints are for you. Do not skip them; they are where the learning sticks.

A note on framing: a few classic problems (a staircase, a cipher) are not about behaviour at all. They are here because they are clean, famous exercises for a specific pattern. Treat them as gym reps — once the pattern is in your fingers, you will reuse it on messy research data.


Section 0: Setup — Check Your Interpreter

Just as in the earlier notebooks, we begin by confirming which Python is running this notebook. If a later cell complains that a package is missing, the first question is always: am I in the environment I think I am in?

Interpreter path: /Users/souvikmandal/python_venv/StatEnv/bin/python
Python version  : 3.12.12 (main, Oct  9 2025, 11:07:00) [Clang 16.0.0 (clang-1600.0.26.6)]

Section 1: Fluency Boosters — Writing Less, Saying More

Fluency is the difference between spelling out every word and speaking in sentences. The four tools in this section — richer slicing, comprehensions, string methods, and sorting with a key — let you express common operations in a single, readable line instead of a multi-line loop. None of them are new concepts; each is a compact form of something you already know.

1.1. Concept: Slicing, Deeper

You already know that my_list[0] grabs the first element. Slicing generalises this to grab a range of elements with the syntax sequence[start:stop:step].

Key Characteristics:

  • start is included, stop is excluded (so [0:2] gives elements 0 and 1).

  • Any of the three parts can be omitted: [:3] means “from the beginning”, [3:] means “to the end”.

  • A negative index counts from the end: [-1] is the last element.

  • A negative step, [::-1], walks the sequence backwards — the cleanest way to reverse.

Slicing works the same way on lists, tuples, and strings, because all three are ordered sequences.

Whole tuple     : (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
Single element  : 2
Last element    : 10
First two       : (1, 2)
A middle window : (4, 5, 6)
Reversed        : (10, 9, 8, 7, 6, 5, 4, 3, 2, 1)
Every 2nd item  : (1, 3, 5, 7, 9)

Two common ways to reverse the first six items of a list — one with slicing, one with the .reverse() method. Notice that slicing leaves the original list untouched, while .reverse() changes the list in place.

Slice reverse : [6, 5, 4, 3, 2, 1]
Original list : [1, 2, 3, 4, 5, 6] (unchanged)
In-place      : [6, 5, 4, 3, 2, 1]

Mini Challenge 1.1. Given frames = list(range(0, 100)) (a stand-in for frame numbers), use a single slice to grab every 10th frame, and a second slice to grab the last 5 frames in reverse order.

1.2. Concept: Comprehensions — The Compact Loop

A comprehension builds a new list (or dictionary, or set) from an existing iterable in a single expression. It is the compact form of a for loop whose only job is to collect results.

The pattern reads almost like English:

[ <what to keep>  for <item> in <iterable>  if <optional condition> ]

Key Characteristics:

  • The result is a brand-new list; the original is untouched.

  • The optional if at the end filters items.

  • It is usually faster and easier to read than the equivalent loop — as long as it stays on one logical line. If the logic gets complicated, prefer a normal loop.

Loop      : [25, 40, 33, 18]
Comp      : [25, 40, 33, 18]
In months : [144, 300, 204, 480, 96, 396, 216]

A dictionary comprehension uses the same idea but builds key: value pairs. Here we map each participant ID to a pass/fail label in one line.

{'P01': 'pass', 'P02': 'fail', 'P03': 'pass', 'P04': 'pass'}
Did P02 pass? -> fail

Mini Challenge 1.2. You have reaction_times = [0.45, 1.20, 0.33, 0.98, 1.51] (seconds). Use one list comprehension to keep only times under 1.0 s, and one dictionary comprehension to label each trial index (0, 1, 2, ...) as "fast" or "slow" using the same 1.0 s threshold.

1.3. Concept: The String-Method Toolkit

In behavioural research, metadata often hides inside text: a filename like athlete03_session2_pushup.mp4 encodes the subject, the session, and the task. String methods are built-in tools attached to every string that let you inspect and reshape that text.

A few you will reach for constantly:

  • .isupper(), .islower(), .isdigit(), .isalpha() — ask a question about the characters (return True/False).

  • .startswith(...), .endswith(...), and the in keyword — test for substrings.

  • .lower(), .upper(), .strip() — clean and normalise text.

  • .split(sep) — break a string into a list of pieces.

  • "sep".join(list) — the inverse: glue a list of strings back together.

'saveChangesInTheEditor' contains 5 words.
Subject: athlete03
Session: session2
Task   : pushup
Label  : athlete03 | session2 | pushup

Mini Challenge 1.3. Given path = "trial_07_subjectB_correct.csv", extract the trial number, the subject, and the outcome. Then build a tidy label "subjectB - trial 07 - correct" using .join() or an f-string.

1.4. Concept: Sorting with a Key

sorted(iterable) returns a new, sorted list. The real power comes from the key= argument: a function that tells Python what to sort by. The same idea works for min() and max().

Key Characteristics:

  • sorted(...) returns a new list; the list method .sort() sorts in place.

  • key= takes a function that is applied to each item to produce the value used for comparison.

  • reverse=True flips the order (largest first).

  • A lambda is a tiny, throwaway one-line function — perfect for a key.

Ranked by score:
   P03 -> 91
   P01 -> 88
   P02 -> 42
Fastest responder: P03

Checkpoint 1. You should now be able to: take a slice with a step, rewrite a collecting loop as a comprehension, pull fields out of a string with .split(), and sort a list of dictionaries by any field. If any of those feels shaky, re-run that subsection before moving on — Section 2 leans on all four.


Section 2: Choosing the Right Data Structure

Every container in Python is good at some things and slow at others. Picking the right one is the single biggest lever you have over how fast — and how readable — your analysis code is. This section compares the four workhorses and then shows, with a stopwatch, why the choice matters.

2.1. Concept: List vs. Tuple vs. Set vs. Dict

StructureLooks likeOrdered?Duplicates?Best at
list[1, 2, 2, 3]yesyesan ordered, changeable sequence
tuple(1, 2, 3)yesyesa fixed record that must not change
set{1, 2, 3}nonofast membership tests; removing duplicates
dict{"a": 1}yes*keys uniquefast lookup of a value by a key

*Dictionaries remember insertion order in modern Python, but you still access values by key, not by position.

The two that beginners under-use are set and dict — and they are exactly the two that make code fast. The next subsections show why.

list : [22, 45, 22, 38, 45]
tuple: (22, 45, 22, 38, 45)
set  : {45, 38, 22}   <- duplicates removed, order not guaranteed
dict : {'sensorA': 22, 'sensorB': 45}
Value for sensorB: 45

2.2. Concept: Sets for Fast Membership

The question “is this item in the collection?” comes up everywhere — is this a valid label? have we seen this ID before? With a list, Python must walk through every element to answer, so the cost grows with the size of the list. With a set, the answer is effectively instant, no matter how large the set is. (This near-instant lookup is what computer scientists call O(1); we will unpack that in 2.4.)

Use a set when you mainly need to (a) test membership quickly, or (b) remove duplicates.

'run'  valid?  True
'fly'  valid?  False
Unique labels used: {'run', 'walk', 'rest'}
How many distinct behaviours?  3

2.3. Concept: Dictionaries for Lookup and Counting

A dictionary maps a key to a value, and looks the value up almost instantly. Two patterns dominate research code:

  • Lookup — store something once, retrieve it by name many times (e.g. a subject’s group assignment).

  • Counting — tally how often each item appears (e.g. how many times each behaviour occurred).

Two tools make this clean:

  • dict.get(key, default) — fetch a value, but return a fallback instead of crashing if the key is missing.

  • collections.Counter — a ready-made dictionary built specifically for counting.

Manual counts : {'walk': 3, 'rest': 2, 'run': 1, 'feed': 1}
Counter       : {'walk': 3, 'rest': 2, 'run': 1, 'feed': 1}
Most common   : [('walk', 3), ('rest', 2)]

Mini Challenge 2.3. Given votes = ["A", "B", "A", "C", "B", "A"], build a count dictionary two ways — once with .get() and once with Counter — and print which option received the most votes.

2.4. Concept: A First Taste of Big-O

Big-O is shorthand for how the running time grows as the data grows. You do not need any maths to use it well — just an intuition for three cases:

  • O(1) — constant. The work does not depend on the size of the data. Looking up a key in a dict, or testing membership in a set.

  • O(n) — linear. The work grows in step with the data. Scanning a list once.

  • O(n²) — quadratic. The work grows with the square of the data — a loop inside a loop. Often a sign you should be using a set or dict instead. At 360,000 frames, O(n²) is catastrophic; O(n) is fine.

The classic trap: calling list.count(x) inside a loop over the same list. Each .count() is itself an O(n) scan, so the whole thing becomes O(n²). Doing the tally with a dictionary is O(n). Let us measure the difference.

Same answer?  True
O(n^2) .count() :     0.92 ms
O(n)   Counter  :     0.59 ms
Speed-up        :      2x faster

Checkpoint 2. The takeaway is not “memorise Big-O notation” — it is a habit: whenever you find yourself searching a list inside a loop, ask whether a set or dict would let you do it in one pass. That single reflex will save you hours on real datasets.


Section 3: Algorithmic Patterns for Research Data

Most problems you will face are variations of a few patterns. Learn to recognise the pattern and you have already done 80% of the work. This section walks through six patterns, each with a short, classic exercise and a note on where it reappears in this course.

3.1. Pattern: Counting and Frequency

When to reach for it: any time the question is “how many” or “what fraction” — the share of trials that were correct, the proportion of frames in each behaviour, the most frequent event.

We will warm up on the HackerRank classic Plus Minus: given a list of numbers, report the fraction that are positive, negative, and zero. The same logic answers “what fraction of trials were rewarded / punished / neutral?”.

Positive: 0.400000
Negative: 0.400000
Zero    : 0.200000

A close cousin — Birthday Cake Candles: how many values equal the maximum? This is the “how many tied for the top” question (e.g. how many athletes hit the best score).

Number tied for tallest: 2

3.2. Pattern: Sorting-Based Solutions

When to reach for it: when order unlocks the answer — the top-k results, the median, the smallest gap. Sorting once (O(n log n)) often turns a hard question into an obvious one.

The exercise is Mini-Max Sum: given five numbers, find the smallest and largest sums obtainable from any four of them. Once the list is sorted, the answer is trivial: drop the largest for the min, drop the smallest for the max.

min: 16  max: 24

3.3. Pattern: Windowing over Sequences

When to reach for it: when meaning lives in a fixed-size chunk of a sequence — a 3-frame smoothing window, a beat of music, a repeating code block. You step through the sequence one window at a time.

This is the pattern behind much of your later pose work: a moving window over frames is how you smooth jitter and detect events. We will practise on Mars Exploration: a signal should be the message "SOS" repeated; count how many characters were corrupted in transmission.

Corrupted characters: 3

3.4. Pattern: Stacks

When to reach for it: when the most recent item is the one you need to compare against or undo — matching brackets, parsing, “cancel out adjacent duplicates”. A stack is just a list used from one end: .append() to push, .pop() to remove the last item.

The exercise: repeatedly remove adjacent matching letters until none remain ("aaabccddd" -> "abd"). Each incoming letter either cancels the top of the stack or sits on top of it.

abd
Empty String

3.5. Pattern: Nested and 2D Data

When to reach for it: whenever data is a grid — a frame of pixels, a matrix of joint coordinates, a table of trials. The key skill is moving cleanly between a Python list-of-lists and a NumPy 2D array.

First, a gotcha: max() on a list-of-lists does not give the largest number. It compares the inner lists to each other (by their first element). To get the overall maximum you need a two-step “max of each row, then max of those”.

Rows   : 3
Columns: 5
NumPy shape: (3, 5)
Overall maximum: 18

A clean 2D exercise — Diagonal Difference: the absolute difference between the sums of a square matrix’s two diagonals. The primary diagonal is arr[i][i]; the secondary is arr[i][n-1-i].

Diagonal difference: 15

3.6. Pattern: Loops vs. Recursion (a brief aside)

Almost everything in this course is best written as a loop. But you will occasionally meet recursion — a function that calls itself on a smaller piece of the problem until it hits a base case that stops it. It is worth recognising, even if you rarely write it.

The two snippets below count to 5 identically. Note the recursive version’s base case (current >= stop): without it, the function would call itself forever.

--- loop ---
Count: 0
Count: 1
Count: 2
Count: 3
Count: 4
--- recursion ---
Count: 0
Count: 1
Count: 2
Count: 3
Count: 4

Checkpoint 3. For each pattern, try to state in one sentence when you would use it. If you can say “I’d use windowing when the answer lives in a fixed-size chunk of a sequence,” the pattern is yours.


Section 4: Practice Problems (LeetCode / HackerRank-style)

Now you assemble the tools. For each problem: read it, plan an approach in words, then write it. Where two solutions are shown, study the gap between the explicit version (how you reason it out) and the Pythonic version (how fluency expresses it).

Problem 1 — Staircase

Print a right-aligned staircase of height n built from #, where row i has i hashes padded on the left by spaces. Pattern: string building.

   #
  ##
 ###
####

Problem 2 — Time Conversion

Convert a 12-hour time string ("07:05:45AM") to 24-hour format. The only tricky cases are 12 AM -> 00 and 12 PM -> 12. Pattern: string parsing + careful conditionals.

07:05:45
00:00:00
12:00:00

Problem 3 — Password Strength (explicit vs. Pythonic)

A password is strong if it is at least 6 characters and contains a digit, a lowercase letter, an uppercase letter, and a special character. Return the minimum number of characters to add to make it strong. Pattern: track several conditions in one pass.

This is the showcase comparison of the notebook — the same problem solved twice.

Explicit: 0
Pythonic: 0

Read the two side by side. Version B is shorter not because it is clever, but because it uses the right tools: booleans as 0/1, a set for fast lookup, and a single max() to combine the two requirements. That is fluency.

Problem 4 — Caesar Cipher

Shift every letter forward by k, wrapping z -> a, leaving non-letters untouched. Pattern: index arithmetic with the modulo (%) operator for wrap-around.

plggoh-Rxwc

Problem 5 — Longest Alternating Substring

Given a string, choose two letters; delete all others; you are left with a string of just those two. What is the longest such string that alternates (no two neighbours equal)? Pattern: try all pairs (a double loop), then validate — and notice the cost.

Longest alternating length: 5

Mini Challenge 4. Re-frame Plus Minus (Problem set, Section 3.1) for your own data: given a list of frame-by-frame angle changes, report the fraction of frames where the angle increased, decreased, or stayed the same. You already have the pattern — only the story changes.


Section 5: Appendix (Optional) — Pattern Matching with Regex

Regular expressions (regex) are a mini-language for describing text patterns. They are powerful but dense, so we keep them optional: reach for them only when plain string methods (Section 1.3) are not enough. Skip this on a first pass and come back when you actually need to parse messy text.

The re module is the tool. Three jobs cover most needs: find all matches, split on a pattern, and extract a captured group.

Word count : 22
Top 3 words: [('the', 4), ('dna', 3), ('sequence', 2)]
- Hello class!
- Is this a test, Dr.
- Mandal?
- Yes, it is.
dQw4w9WgXcQ   <- https://www.youtube.com/watch?v=dQw4w9WgXcQ
dQw4w9WgXcQ   <- https://youtu.be/dQw4w9WgXcQ
S0Nib79VNUE   <- https://www.youtube.com/shorts/S0Nib79VNUE

Section 6: Bridge to Notebook 05 — From Functions to Classes

Look back at how we have been working. We keep a piece of data — a research sample, an event — as a dictionary, and we write separate functions that each take that dictionary as input. Here is the recurring shape:

Selected IDs: [1, 4]
   Animal 1: age 12, weight 4.2 kg
   Animal 4: age 20, weight 2.2 kg

Notice the friction. The data ({"id":..., "age":...}) and the behaviour (select_samples, describe) live apart. We must keep passing the same dictionaries into every function, and nothing stops us from typo-ing a key (a["wieght"]) or building a malformed animal in the first place. As a project grows, this gets fragile.

The natural question is: what if each animal carried its own behaviour with it — so that animal.describe() just worked, and an animal could only ever be created with valid fields? That bundling of data + the functions that act on it into a single object is exactly what a Class is, and it is where Notebook 05 begins.

Weekly Challenge. Combine three patterns from this notebook into one small pipeline on a behavioural dataset of your choice (real or invented):

  1. Parse a list of filenames like "subjectA_session1_walk.mp4" into structured records (Section 1.3).

  2. Count how many sessions each subject contributed, using a dictionary or Counter (Section 2.3).

  3. Rank the subjects from most to fewest sessions with sorted(..., key=...) (Section 1.4).

Write it first with explicit loops, then rewrite the collecting steps as comprehensions. Keep both versions — the difference is the fluency you have gained.


End of Notebook 04. Next: Notebook 05 — Introduction to Python Classes & Decorators, where the data and behaviour you have been juggling finally move into one place.