Why NumPy, before pandas
It's tempting to skip straight to pandas - it's where the fun is. But pandas is a friendly wrapper around NumPy, and the day something is slow, or a dtype surprises you, or a filter behaves oddly, the answer is almost always one layer down. Spend 45 minutes here and every fast pandas operation you write later will make sense instead of feeling like a spell. If you can write a Python loop, you already know enough - we're going to teach you to stop writing them.
Arrays vs Python lists 5 min live
A Python list can hold anything - ints, strings, other lists - so Python stores each item as a separate boxed object scattered across memory. A NumPy ndarray holds one type in one contiguous block, so the CPU can rip through it in tight C loops. That single design choice is why arrays are fast.
LiveThe array is a typed block3 min▶
An array carries three facts about itself, and you'll check all three constantly: shape (how many rows and columns), ndim (how many dimensions), and dtype (the one type every element shares). Because the type is fixed, NumPy knows exactly how many bytes each element takes and can jump straight to element number 90,000 without walking the list.
When you later run jobs["salary"].values on the LinkedIn data, pandas hands you back exactly this: a NumPy array with a dtype. If that dtype comes back as object instead of float64, you've just learned the salary column has junk in it - text mixed with numbers. The dtype is the first tell.
Self-studyMaking arrays - the constructor table2 min read▶
You rarely type arrays by hand. These five constructors cover almost everything:
| Constructor | What it makes | Example |
|---|---|---|
| np.array(list) | Array from a Python list or nested lists | np.array([1, 2, 3]) |
| np.arange(start, stop, step) | Evenly spaced values, like range but an array | np.arange(0, 10, 2) |
| np.zeros(shape) | All zeros - a blank canvas to fill | np.zeros((3, 4)) |
| np.ones(shape) | All ones - handy for scaling | np.ones(5) |
| np.linspace(start, stop, n) | n evenly spaced points between two bounds | np.linspace(0, 1, 11) |
arange takes a step size, linspace takes a count. Reach for linspace when you know how many points you want (say, 11 salary bins) and arange when you know the spacing.
Vectorization - loops without loops 5 min live
This is the big idea of the whole session. Instead of looping over a million numbers in Python, you write one expression on the whole array and NumPy runs the loop for you in C. Same result, roughly 100x faster, and far easier to read.
LiveElementwise ops, ufuncs, and aggregations3 min▶
Arithmetic on arrays happens elementwise: arr * 2 doubles every element at once. The same goes for the built-in universal functions (ufuncs) like np.sqrt, np.log, and np.exp - they map over the whole array in C. And when you want to collapse an array to a summary, the aggregations - sum, mean, std, min, max - do it in one call.
For 2-D arrays, axis picks the direction: axis=0 collapses down the rows (a per-column summary), axis=1 collapses across the columns (a per-row summary). Get this straight now - it's the exact same axis you'll use in pandas groupby.
Our LinkedIn data lists some pay as hourly. To compare fairly, you convert 124,000 salaries from hourly to yearly in one line - yearly = hourly * 40 * 52 - no loop, no wait. That single expression is what makes analysis on 124k rows feel instant instead of sluggish.
Self-studyWhy ~100x - a quick mental model2 min read▶
A Python loop pays a tax on every single iteration: check the type, unbox the object, do the math, re-box the result. Over a million elements that tax dominates. A vectorized op skips all of it - one type check up front, then a tight C loop over the contiguous block. The speed-up is typically 50-200x, and it grows with array size.
%timeit and watch milliseconds turn into microseconds on your own machine.
Indexing, slicing, broadcasting 4 min live
Three ways to reach into an array. Basic slicing (fast, but returns a view). Boolean masks (how you filter data). Broadcasting (how NumPy makes mismatched shapes line up). The middle one is the one you'll use every single day.
LiveSlicing returns views, not copies2 min▶
Basic slicing (arr[1:4]) gives you a view onto the same memory, not a fresh copy - so writing into a slice changes the original. That's fast and occasionally surprising. When you need an independent copy, say so with .copy().
Fancy indexing with a list of positions - arr[[0, 3, 7]] - does return a copy, and lets you pull arbitrary elements in any order.
LiveBoolean masks are how you filter data2 min▶
Compare an array to a value and you get back an array of True/False - a mask. Index with that mask and you get only the elements where it's True. This is the single most important move in the session, because it is exactly how you'll filter DataFrames in Session 3.
jobs[jobs["salary"] > 80000] is this exact pattern - a boolean mask indexing a table. Learn it once here on a flat array and pandas filtering will feel like something you already know.
Vectorized math, timed ★ 8 min · everyone builds
New notebook cell. Build a fake salary array to practice on - we'll use the real column next session.
Do vectorized math: a 5% raise, and an hourly-to-yearly conversion. Notice there is no loop anywhere.
Now prove the speed. Time a plain Python loop against the vectorized version with %timeit.
That timing gap is exactly why analysts can iterate on 124k LinkedIn postings interactively - tweak a formula, re-run, see the answer in a heartbeat. If every operation looped in Python, a working session would be a coffee break between cells.
Boolean masking on salaries ★ 8 min · everyone builds
Take a salary array. Find the median, then build a mask for the "high earners" above it.
Count how many are in the high group, and compute that group's mean - all with masks, no loops.
This is the whole shape of a real question on the LinkedIn data: "of postings that list pay above the median, what's the average, and how many are there?" You'll ask exactly this next session with jobs[jobs["salary"] > mid] - same mask, just on a DataFrame instead of a flat array.
Normalize in one line ★ 5 min · follow along
Normalizing - subtract the mean, divide by the standard deviation - is a broadcasting classic. The scalars stretch across the whole array automatically.
Read the expression out loud: sal.mean() is one number, but subtracting it from the array applies to every element - that is broadcasting doing the work.
On a 2-D array you'd pass axis=0 to normalize each column independently. Same idea, one axis deeper.
This week ◐ 30 min total
- Do a handful of the NumPy 100-exercises (the classic
numpy-100set) - pick five from the beginner section. Reps build the muscle. - Re-implement one loop as vectorized. Take any Python loop you wrote in Session 1 of learn-python-with-phoebe and rewrite it as a single array expression. Time both.
- Practice masks: make an array of 100 random salaries, then answer with masks - how many above 100k, mean of the bottom quartile, count between 60k and 90k.
- Read McKinney ch4 (NumPy Basics) end to end once. It's the canonical reference and it's free and open access.
- Optional: skim Kaggle's free "Intro to Deep Learning" first page - not for the models, just to see NumPy arrays everywhere underneath.
Three questions before you go 🎯 ◐ 90 seconds
1 · Why is a NumPy array faster than a Python list for numeric work?
One dtype in one contiguous block lets NumPy loop in C instead of paying Python's per-element tax. That's the ~100x.
2 · What does arr[arr > 50000] return?
arr > 50000 is a True/False mask; indexing with it keeps only the True elements. This is exactly how pandas filtering works.
3 · What is broadcasting?
A scalar (or smaller array) gets "stretched" to fit, so sal - sal.mean() subtracts one number from every element with no loop.
Official sources covered
This session teaches the working content of the official NumPy documentation and Wes McKinney's open-access Python for Data Analysis (3e). Certificates, graded exercises, and the full API reference stay on those platforms - links provided.