Python · SQL · Web Dev · Java · AI/ML tracks launching soon — your one platform for all of IT
Advanced+200 XP

Intro to NumPy and pandas

The bridge from core Python into data work — arrays, DataFrames, and why these libraries exist at all.

45 min August 2026
// Part 01 — Why Plain Python Loops Aren't Enough

The Problem NumPy Exists to Solve

A plain Python list can hold numbers, but every arithmetic operation on it requires an explicit loop — and each iteration carries real overhead, since Python objects (even a simple integer) are far heavier than the raw numeric values a lower-level language works with directly.

Plain Python — multiplying every element by 2
numbers = list(range(1_000_000))
doubled = [n * 2 for n in numbers]      # a full Python-level loop, one iteration at a time
NumPy — the same operation, vectorised
import numpy as np

numbers = np.arange(1_000_000)
doubled = numbers * 2                    # no explicit loop — operates on the WHOLE array at once

The NumPy version is not just shorter — it is typically 10-100x faster for numeric work at this scale. The reason is structural: a NumPy array stores its numbers as a single contiguous block of raw memory (much closer to how C or Java store an array of numbers) rather than as a list of individually boxed Python objects, and operations like * 2 run as a single, highly optimised loop written in C — this is called vectorisation, and it is the entire reason NumPy exists.

// Part 02 — NumPy Arrays

The ndarray — NumPy's Core Data Structure

Creating and inspecting arrays
import numpy as np

a = np.array([1, 2, 3, 4, 5])
print(a)             # [1 2 3 4 5]
print(a.shape)        # (5,) — a 1-dimensional array of 5 elements
print(a.dtype)         # int64 — every element shares ONE data type, unlike a Python list

matrix = np.array([[1, 2, 3], [4, 5, 6]])
print(matrix.shape)   # (2, 3) — 2 rows, 3 columns

The single most important structural difference from a Python list: every element of a NumPy array shares exactly one dtype — you cannot freely mix an int and a str in the same array the way a Python list allows. This uniformity is precisely what makes the contiguous-memory, vectorised-operation model possible in the first place.

Elementwise operations, no loop needed
prices = np.array([19.99, 29.99, 9.99, 49.99])

with_tax = prices * 1.08            # every element multiplied at once
print(with_tax)                      # [21.5892 32.3892 10.7892 53.9892]

print(prices[prices > 20])           # [29.99 49.99] — boolean indexing: filter by a condition, no loop

Broadcasting — operating on arrays of different shapes

Broadcasting is the rule set NumPy uses to apply an operation between arrays of different shapes without requiring you to manually resize either one — in the example above, the single number 1.08 is automatically "stretched" to apply to every element of the prices array. Broadcasting extends to combining full arrays of compatible shapes too, and is genuinely central to writing idiomatic, loop-free NumPy code.

// Part 03 — pandas DataFrames

Rows, Columns, and Labels — Built on Top of NumPy

NumPy arrays are excellent for pure numeric data, but real-world data is usually tabular — rows and named columns, often with mixed types (text, numbers, dates) in the same table. pandas is built on top of NumPy specifically to handle this shape of data, centred on two core structures: Series (a single labelled column of data) and DataFrame (a full table — a collection of aligned Series).

Creating a DataFrame
import pandas as pd

df = pd.DataFrame({
    "name": ["Keyboard", "Mouse", "Monitor"],
    "price": [79.99, 24.99, 249.99],
    "in_stock": [True, True, False],
})

print(df)
#        name   price  in_stock
# 0  Keyboard   79.99      True
# 1     Mouse   24.99      True
# 2   Monitor  249.99     False
Selecting, filtering, and computing
print(df["price"])              # a single column, as a Series
print(df[df["in_stock"]])       # rows where in_stock is True — boolean filtering, same idea as NumPy
print(df["price"].mean())       # 118.32333... — built-in aggregate methods
print(df["price"].sum())        # 354.97

df["price_with_tax"] = df["price"] * 1.08   # adding a new computed column, vectorised, no loop
// Part 04 — Reading Real Data

pandas vs the csv Module for Real Files

The CSV and JSON module covered earlier in this track handles files with plain Python data structures — lists of dicts. For genuinely tabular analysis work — filtering, aggregating, joining, computing statistics across thousands or millions of rows — pandas is usually the better tool, since it was purpose-built for exactly this.

Reading a CSV — pandas vs the csv module
import csv
import pandas as pd

# The csv module — you get a list of dicts, and write your own loops for everything
with open("sales.csv") as f:
    rows = list(csv.DictReader(f))
total = sum(float(row["amount"]) for row in rows)

# pandas — one line to load, built-in vectorised aggregation
df = pd.read_csv("sales.csv")
total = df["amount"].sum()

The csv module remains the right choice for simple row-by-row processing, especially in a script with no other pandas dependency — pulling in pandas for a five-line script that reads one small file is unnecessary weight. pandas earns its place once the work genuinely involves analysis: grouping, joining multiple sources, computing statistics, or handling data too large to comfortably reason about with manual loops.

Grouping and aggregating — a genuinely common real task

groupby — the pandas equivalent of a SQL GROUP BY
df = pd.DataFrame({
    "category": ["electronics", "electronics", "office", "office"],
    "amount": [79.99, 249.99, 4.99, 12.99],
})

print(df.groupby("category")["amount"].sum())
# category
# electronics    329.98
# office          17.98
// Part 05 — Common Gotchas

Two Warnings Every pandas Beginner Eventually Hits

SettingWithCopyWarning is pandas' most infamous warning, and it confuses nearly everyone the first time they see it — it appears when pandas cannot tell for certain whether you are modifying the original DataFrame or a temporary copy of a slice of it.

The warning in action
electronics = df[df["category"] == "electronics"]
electronics["amount"] = electronics["amount"] * 1.1
# SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame
The fix — be explicit about intent with .loc or .copy()
# If you intend to modify the ORIGINAL df:
df.loc[df["category"] == "electronics", "amount"] *= 1.1

# If you intend to work on an independent COPY:
electronics = df[df["category"] == "electronics"].copy()
electronics["amount"] = electronics["amount"] * 1.1   # no warning — pandas knows this is intentional
⚠️ Important
"Chained indexing" — writing two square-bracket lookups back to back, like df[condition]["column"] = value — is the root cause of most SettingWithCopyWarning cases. Whether the first bracket returns a view or a copy of the original data is not always guaranteed, which is exactly why the warning exists. Use a single .loc[row_condition, "column"] call instead whenever you intend to modify the original DataFrame.
// Part 06 — Real World
💼 What This Looks Like at Work

Replacing a 40-Minute Report Script With a 3-Second One, at a San Diego Marketing Analytics Company

Scenario — Marketing analytics company, San Diego · Reporting pipeline

A weekly campaign-performance report reads a 2-million-row CSV using the standard csv module, computing per-campaign totals and averages with hand-written Python loops and dictionaries. The script takes 40 minutes to run, and every new metric requested by the marketing team means writing another manual loop.

The original approach — manual loops over 2 million rows
totals = {}
counts = {}
with open("campaign_data.csv") as f:
    for row in csv.DictReader(f):
        campaign = row["campaign_id"]
        totals[campaign] = totals.get(campaign, 0) + float(row["spend"])
        counts[campaign] = counts.get(campaign, 0) + 1
averages = {c: totals[c] / counts[c] for c in totals}
Rewritten with pandas
df = pd.read_csv("campaign_data.csv")
summary = df.groupby("campaign_id")["spend"].agg(["sum", "mean", "count"])

Why the difference was dramatic, not just stylistic

The pandas version runs in roughly 3 seconds instead of 40 minutes — the vectorised groupby/agg operations are implemented in optimised C code operating on contiguous memory, exactly as described in Part 01, instead of 2 million individual Python-level dictionary lookups and updates. Just as significantly for the team's day-to-day work, adding a new requested metric became a one-line change to the .agg([...]) call instead of writing and testing an entirely new manual accumulation loop.

// Part 07 — Misconceptions

Four Misconceptions About NumPy and pandas

✕ ""NumPy arrays are basically just faster Python lists""
The performance difference comes from a genuinely different underlying structure — a contiguous block of uniformly-typed raw memory, versus a list of individually boxed Python objects — which is what makes true vectorised, loop-free operations possible in the first place, not just a speed tweak on the same underlying model.
✕ ""You should always reach for pandas instead of the csv module for anything involving a CSV file""
For simple row-by-row processing in a small script, the standard library csv module is lighter weight and perfectly sufficient. pandas earns its place specifically for genuine analysis work — grouping, aggregating, joining, or data large enough that manual loops become slow or unwieldy.
✕ ""SettingWithCopyWarning means your code definitely has a bug""
It means pandas cannot be CERTAIN whether you are modifying the original data or an unintended copy — it is often a false alarm, but common enough as a real bug that it should never be ignored without understanding why it fired. Using .loc[...] for intentional modification, or .copy() for an intentional independent copy, resolves the ambiguity either way.
✕ ""Vectorisation just means pandas/NumPy code runs on multiple CPU cores in parallel""
It means the operation runs as a single, tight, optimised loop written in C over contiguous memory — not necessarily multiple cores at all. That is a different, additional concept (parallelism, covered earlier in this track with multiprocessing) that some NumPy/pandas operations can ALSO take advantage of, but vectorisation itself is about avoiding slow Python-level looping, not about parallel cores specifically.
// Part 08 — Interview Prep

5 Interview Questions — With Complete Answers

Why is a NumPy array typically much faster than a Python list for numeric operations?
A NumPy array stores its elements as a contiguous block of uniformly-typed raw memory, and operations run as a single optimised loop implemented in C — versus a Python list, whose elements are individually boxed Python objects requiring a Python-level loop with real per-iteration overhead for equivalent operations. This is called vectorisation.
What is the relationship between pandas and NumPy?
pandas is built on top of NumPy, adding labelled rows/columns and support for tabular, often mixed-type data (Series for a single column, DataFrame for a full table) — NumPy alone is best suited to purely numeric array data without labels or mixed types.
When would you choose the standard library csv module over pandas for reading a CSV file?
For simple, small-scale row-by-row processing where pulling in pandas as a dependency is unnecessary weight — pandas is the better choice once the work involves genuine analysis (grouping, aggregating, joining) or data volumes where manual loops become slow.
What causes a SettingWithCopyWarning, and how do you resolve it?
It fires when pandas cannot determine with certainty whether an assignment is modifying the original DataFrame or an unintended temporary copy of a slice — commonly from "chained indexing" (df[condition]["col"] = value). Use .loc[row_condition, "col"] = value for an intentional modification of the original, or .copy() to work on an explicit, intentional independent copy.
What does "broadcasting" mean in NumPy?
The set of rules NumPy uses to apply an operation between arrays of different (but compatible) shapes without manually resizing either one — for example, multiplying an entire array by a single scalar number automatically applies that scalar to every element, without writing an explicit loop.
// Common Mistakes

NumPy & pandas Mistakes Beginners Make Constantly

Writing a manual Python for loop over a NumPy array or pandas Series/column
This throws away the entire performance benefit of vectorisation — nearly any elementwise operation you would write as a manual loop has a vectorised equivalent (arithmetic operators, boolean filtering, built-in aggregate methods like .sum()/.mean()).
Using chained indexing (two square-bracket lookups back to back) and ignoring the resulting warning
This is the classic trigger for SettingWithCopyWarning, and the assignment may silently fail to modify the original DataFrame at all. Use a single .loc[condition, column] call instead.
Assuming every column in a DataFrame loaded from a CSV has the type you expect
pandas infers types automatically from the file, and a column intended as numeric can silently load as text (object dtype) if even one row contains a non-numeric value (like a stray "N/A"). Check df.dtypes after loading, especially for columns you plan to do arithmetic on.
Mixing types in what should be a purely numeric NumPy array
np.array([1, 2, "3"]) silently upcasts every element to a string dtype, since a NumPy array requires one shared dtype — subsequent arithmetic then fails or behaves unexpectedly. Validate/clean the input data before constructing the array if its numeric-ness is not guaranteed.
// Error Library

Errors You Will Hit With NumPy & pandas — And Exactly Why

SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame
Cause: An assignment used chained indexing, and pandas cannot guarantee whether it modified the original DataFrame or a temporary copy.
Fix: Use .loc[row_condition, "column"] = value for an intended modification of the original, or .copy() when an independent copy is actually intended.
ValueError: operands could not be broadcast together with shapes (3,) (4,)
Cause: Two NumPy arrays with incompatible shapes were combined in an operation — broadcasting rules only allow certain shape combinations, and (3,) and (4,) are not among them.
Fix: Confirm both arrays are meant to be the same length, or reshape one so its dimensions are actually broadcast-compatible with the other.
KeyError: 'price'
Cause: Attempting to access a DataFrame column name that does not exist — often due to a typo, unexpected whitespace in the actual column header, or different capitalisation than expected.
Fix: Print df.columns to see the exact column names as pandas actually parsed them from the source file.
TypeError: can only concatenate str (not "int") to str
Cause: A column that was expected to be numeric was loaded as text (object dtype) because at least one row contained a non-numeric value, and an arithmetic operation was then attempted on it.
Fix: Use pd.to_numeric(df["column"], errors="coerce") to convert it properly, turning any genuinely non-numeric values into NaN rather than crashing.

🎯 Key Takeaways

  • NumPy arrays store uniformly-typed data in contiguous memory, enabling vectorised operations (a single optimised C loop) that are typically 10-100x faster than an equivalent Python-level loop.
  • pandas is built on top of NumPy, adding labelled rows/columns and mixed-type tabular data support via Series (one column) and DataFrame (a full table).
  • Prefer the standard library csv module for small, simple row-by-row scripts; reach for pandas once real analysis — grouping, aggregating, joining — or larger data volumes are involved.
  • groupby()/.agg() is pandas' equivalent of a SQL GROUP BY — computing per-group aggregates without writing manual accumulation loops.
  • SettingWithCopyWarning signals ambiguity about whether an assignment targets the original data or a copy — resolve it with .loc[...] for an intentional original-data edit, or .copy() for an intentional independent copy.
  • Vectorisation means avoiding slow Python-level looping via optimised C operations on contiguous memory — a distinct concept from multi-core parallelism, even though some operations can also benefit from both.

What comes next

Module 44 puts everything from this phase together in a full, real project — building a complete command-line tool from scratch with argparse.

Module 44 → Building a CLI Tool
Share

Discussion

0

Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.

Continue with GitHub
Loading...