learn-python-data-analysis-with-phoebe / Session 2 of 8
Learn Python Data Analysis with Phoebe · Session 2 of 8

NumPy foundations

Every pandas column you touch for the rest of this course IS a NumPy array underneath. Learn the base - arrays, dtypes, vectorization, broadcasting, boolean masks - and pandas stops feeling like magic and starts feeling like something you can predict. We stay on the 124,000 LinkedIn postings: today we practice the moves on salary-shaped arrays.

🟢 Easy Builders Vectorized thinking 45 min live + self-study
0-3 · Welcome 3-18 · Concepts 18-40 · Build the moves 40-45 · Q&A
Part 0

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.

Live - presented in session Self-study - read after class ★ Build-along demo The project: the data job market
★ What you walk out with today Real comfort with arrays, dtypes, vectorization, broadcasting, and boolean indexing - the exact mental model behind every fast pandas operation. You'll convert a whole array of salaries in one line, filter it with a boolean mask, and normalize it without a single loop.
Part 1 · covers NumPy absolute beginners - arrays

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.

Python list boxes scattered in memory, one slow Python loop obj obj obj obj each item is a separate pointer + Python object NumPy array one typed block, vectorized C under the hood 64 72 88 91 55 67 contiguous · homogeneous dtype (int64) · no per-item boxing Same numbers, different memory - the array layout is the whole speed story.
🔍 Click to zoom - a list is scattered boxes; an array is one typed block the CPU loves
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.

★ Three facts about any arrayimport numpy as np a = np.array([64000, 72000, 88000, 91000]) a.shape # (4,) -> one dimension, four elements a.ndim # 1 a.dtype # dtype('int64') - every element is an int64
Real world

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:

ConstructorWhat it makesExample
np.array(list)Array from a Python list or nested listsnp.array([1, 2, 3])
np.arange(start, stop, step)Evenly spaced values, like range but an arraynp.arange(0, 10, 2)
np.zeros(shape)All zeros - a blank canvas to fillnp.zeros((3, 4))
np.ones(shape)All ones - handy for scalingnp.ones(5)
np.linspace(start, stop, n)n evenly spaced points between two boundsnp.linspace(0, 1, 11)
arange vs linspace 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.
Part 2 · covers NumPy fundamentals - ufuncs & aggregation

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.

Before - Python loop out = [] for x in salaries: out.append(x * 1.05) 1,000,000 slow Python iterations ~100x faster After - vectorized out = salaries * 1.05 one expression, NumPy loops in C If you're writing a for-loop over an array, there's almost always a one-line vectorized version.
🔍 Click to zoom - the same math, a Python loop vs one vectorized expression
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.

★ Elementwise math + ufuncs + aggregationsimport numpy as np sal = np.array([64000, 72000, 88000, 91000, 55000]) sal * 1.05 # 5% raise, every value at once np.log(sal) # ufunc: natural log of each element sal.mean() # 74000.0 - one number summarizing the array sal.std() # spread around the mean sal.max() - sal.min() # the range

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.

Real world

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.

Read a timing before you trust the claim Don't take "100x" on faith - in Demo 1 you'll time a loop against the vectorized version with %timeit and watch milliseconds turn into microseconds on your own machine.
Part 3 · covers NumPy indexing + broadcasting

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.

Broadcasting: one scalar stretched across the array + 1000 a single scalar 64k 72k 88k 91k array of 4 salaries = 65k 73k 89k 92k 1000 added to every element The scalar has no shape of its own, so NumPy "stretches" it to match - that's the broadcasting rule in one picture. Same idea aligns a (3,1) column with a (1,4) row into a (3,4) grid when shapes are compatible. No loop, no manual repeat - broadcasting is how scalar-and-array math just works.
🔍 Click to zoom - a scalar broadcast across every element of an array
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().

★ Slice, and the view gotchaarr = np.arange(10) arr[2:5] # array([2, 3, 4]) - a view piece = arr[2:5] piece[0] = 99 # this ALSO changes arr! (it's a view) safe = arr[2:5].copy() # .copy() when you want independence

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.

★ The boolean mask patternsal = np.array([64000, 72000, 88000, 91000, 55000]) sal > 80000 # array([False, False, True, True, False]) - the mask sal[sal > 80000] # array([88000, 91000]) - only the matching elements (sal > 80000).sum() # 2 - True counts as 1, so this counts matches
Foreshadow: pandas filtering Next session, 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.
Demo 1 of 3

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.

★ Build an array + vectorized mathimport numpy as np # 1,000,000 fake yearly salaries between 40k and 200k rng = np.random.default_rng(42) sal = rng.integers(40000, 200000, size=1_000_000) raised = sal * 1.05 # 5% raise, whole array at once hourly = np.array([28.5, 41.0, 60.0]) yearly = hourly * 40 * 52 # unit conversion, no loop
★ Time the loop vs the vectorized op%timeit [x * 1.05 for x in sal] # Python loop over 1M values %timeit sal * 1.05 # vectorized - watch the units drop # loop -> tens of milliseconds; vectorized -> hundreds of microseconds
Real world

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.

Demo 2 of 3

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.

★ Mask, count, and summarize a sub-groupsal = rng.integers(40000, 200000, size=1_000_000) mid = np.median(sal) # the middle salary high_mask = sal > mid # boolean array: True where above median high_mask.sum() # how many are above the median? sal[high_mask].mean() # average salary OF the high group sal[high_mask].shape # size of the filtered array
Real world

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.

Demo 3 of 3

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.

★ Standardize (z-score) with broadcastingsal = np.array([64000, 72000, 88000, 91000, 55000], dtype=float) z = (sal - sal.mean()) / sal.std() # broadcasting: two scalars over the array z.mean() # ~0.0 - centered z.std() # ~1.0 - unit spread

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.

Why normalize at all? When we build the salary model in Session 8, features on wildly different scales confuse many algorithms. Standardizing puts them on common footing - and it's this exact one-liner, just applied to a DataFrame column.
After the session

This week ◐ 30 min total

Check yourself

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.

Source material

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.

NumPy absolute beginners - arrays, dtype, indexingcreate arrays, shape/ndim/dtype, slicing - Parts 1 & 3
NumPy fundamentals - broadcasting, ufuncs, aggregationvectorized math, axis, boolean masks - Parts 2 & 3
McKinney ch4 - NumPy Basics: Arrays and Vectorized Computationthe canonical reference for everything today
Structured / record arraysself-study - rarely needed once you have pandas DataFrames

Session 2 cheat sheet · pin this

Create arraysnp.array([...]) · np.arange(0,10,2) · np.zeros((3,4)) · np.ones(5) · np.linspace(0,1,11).
dtype / shapea.shape (rows, cols) · a.ndim · a.dtype - check these first; an object dtype means junk in the column.
Vectorized math + ufuncsarr * 1.05, arr + arr2, np.sqrt(arr), np.log(arr) - elementwise, no loops, ~100x faster.
Aggregation with axisarr.sum() / .mean() / .std() / .min() / .max(); axis=0 down columns, axis=1 across rows.
Boolean maskingmask = arr > x; arr[mask] keeps matches; mask.sum() counts them. The filtering pattern.
Broadcasting(arr - arr.mean()) / arr.std() - scalars stretch to match the array shape automatically.