Chapter 1: Python Essentials
Every hedge fund, investment bank, and consulting firm running data-driven decisions today shares one thing in common: they are built on the same four Python libraries you will learn in this chapter. Understanding where these tools came from — and why they were built — will help you use them far more effectively.
The stack and its origins
The Python data science stack evolved in layers, each solving a specific problem the previous layer could not.
NumPy was born in 2005 when Travis Oliphant merged two competing projects — Numeric (Paul Dubois, 1995) and Numarray (Space Telescope Science Institute, 2001) — into a single, unified array library. Oliphant’s key insight was that Python needed a core array object that delegated mathematical operations to compiled Fortran and C routines from BLAS and LAPACK — the same numerical libraries that power MATLAB and scientific computing in physics labs. By bypassing Python’s interpreter overhead, NumPy operations run 10x to 100x faster than equivalent Python loops, thanks to vectorization: applying a single instruction to an entire block of contiguous memory rather than calling an interpreted function on each element one at a time. This technique exploits SIMD (Single Instruction, Multiple Data) hardware in modern CPUs and maximizes cache locality — keeping data in fast L1/L2 cache instead of fetching it from slow RAM.
pandas came next, created by Wes McKinney while he was a quantitative analyst at AQR Capital Management in 2008. McKinney was frustrated that Python lacked a data structure for working with labeled, heterogeneous tabular data — the kind that arrives from Bloomberg terminals, Excel spreadsheets, and SQL databases every morning. He built DataFrame and Series as labeled wrappers around NumPy arrays, adding the index alignment, missing-value handling, groupby aggregation, and time-series resampling that quant finance demands. His 2012 book Python for Data Analysis brought the library to a global audience. pandas is now the undisputed standard for data manipulation in quantitative finance, data science, and business analytics.
SciPy extended NumPy with domain-specific algorithms — statistics, optimization, signal processing, and numerical integration — turning Python into a full scientific computing environment. For business students, the most important submodule is scipy.stats, which gives you access to dozens of probability distributions and their cumulative distribution functions, percentage point functions, and random samplers.
Matplotlib was created in 2003 by John D. Hunter, a neurobiologist who needed to replicate MATLAB’s plotting capabilities in Python so he could visualize brain activity data without a MATLAB license. Its API is deliberately MATLAB-like: plt.plot(), plt.hist(), plt.scatter(). Today it is the foundational plotting library that pandas, seaborn, and many other tools build on top of.
Why this matters for business
This stack now powers operational infrastructure at some of the world’s largest financial institutions. Goldman Sachs’s internal risk systems, JP Morgan’s quantitative research platform, and BlackRock’s Aladdin risk engine all use variants of these tools. Hedge funds run Monte Carlo simulations for derivatives pricing in NumPy. Risk managers calculate Value at Risk (VaR) using SciPy distributions. Portfolio managers slice and filter position data using pandas Series and DataFrames. When regulators under Basel III require banks to report daily VaR figures, those figures are computed with tools like the ones in this chapter.
What you will learn
This chapter builds your foundation: Python lists for storing collections of business data, list comprehensions for transforming and labeling data efficiently, pandas Series for labeled one-dimensional analysis, NumPy arrays for fast vectorized computation and simulation, SciPy statistics for probability calculations and operational thresholds, and Matplotlib for creating professional charts. By the end, you will build a working restaurant performance analyzer from scratch using all of these tools together.
Foundation
Why Python?
Python has become the default working language for data-driven business roles for a combination of practical and cultural reasons. It is consistently ranked the most-used language in data science and quantitative finance, and the entire modern artificial-intelligence stack — TensorFlow, PyTorch, LangChain, and the libraries underneath nearly every commercial large language model — is written in Python or exposes a Python interface. Major employers from Goldman Sachs and JPMorgan to McKinsey expect new hires in analytical roles to be functional in Python from day one, and the language now dominates the curricula of the world’s top business schools.
The Language of Data Analytics
Surveys of data-science practitioners place Python’s share of usage somewhere near a third of the field, ahead of JavaScript, Java, R, and SQL. The table below gives a representative snapshot of language popularity in data work.
| Language | Share |
|---|---|
| Python | 31% |
| JavaScript | 24% |
| Java | 17% |
| R | 12% |
| SQL | 8% |
Mary Callahan Erdoes, head of asset and wealth management at JPMorgan, has put the point bluntly: coding is not just for technologists — it is for anyone who intends to run a competitive company in the twenty-first century. For a business analyst, fluency in Python is now closer to fluency in Excel than to a specialised technical skill.
What This Book Builds
This book moves through four topics in order: Python essentials, data processing with pandas, linear regression, and clustering. The present chapter covers the first of these. Its specific goal is to make you comfortable with three data containers — Python lists, NumPy arrays, and pandas Series — which together form the foundation for everything that follows.
| Topic 1 | Topic 2 | Topic 3 | Topic 4 |
|---|---|---|---|
| Python Essentials | Data Processing | Linear Regression | Clustering |
Setting Up
This book is written to be read interactively. You can run every code cell directly on this site (the Python runtime is embedded in your browser through Pyodide), or you can copy the code into Google Colab, or into a local Anaconda installation on your laptop. Each of these gives you a working Python environment with NumPy, pandas, Matplotlib, and SciPy pre-installed. From this point on, whenever the book refers to “your Python environment,” it means whichever of these three options you happen to be using — the code is identical in all three.
Python Setup
Installing Packages with pip
Python comes with basics, but for data analysis we need extra toolboxes (packages). Think of pip as an app store for Python.
# Standard data-analysis stack — install once on your local Python.
# (In this book the libraries are already loaded in your browser.)
pip install numpy pandas matplotlib scipyOn Google Colab and on a local Anaconda installation, these four libraries come pre-installed. Inside this interactive book they are already loaded in the browser runtime, so you can import and start working immediately.
Importing the Libraries You Need
A Python module is a toolbox: a self-contained collection of functions and classes that you load with an import statement before using. By long-standing convention, the data-analysis stack is imported under a fixed set of short aliases — np for NumPy (arrays, random numbers, mathematical functions), pd for pandas (Series, DataFrames, summary statistics), plt for matplotlib.pyplot (line, bar, and scatter plots), and stats from SciPy (probability distributions). Sticking to these aliases makes your code easier for others to read, because every Python tutorial, textbook, and Stack Overflow answer uses the same names.
import numpy as np # Math & arrays
import pandas as pd # DataFrames & Series
import matplotlib.pyplot as plt # Charts
from scipy import stats # Probability distributionsLists and Comprehensions
Lists: Python’s Built-in Container
Background: Why data structures matter
A data structure is a way of organizing information in memory so that operations on it are efficient. Computer scientists distinguish between two fundamental designs: arrays, which store elements in consecutive memory locations (so accessing any element by position takes constant time, O(1)), and linked lists, which chain elements together through pointers (so random access requires traversing the chain, O(n)). Python’s built-in list is actually a dynamic array — it stores references to objects in contiguous memory, which means index access (my_list[3]) is O(1) and appending to the end is amortized O(1). However, searching for a value by content ("TSLA" in tickers) requires scanning the entire list, which is O(n).
For business applications, this distinction is immediately practical. An order-management system that needs to iterate over 500 open shopping-cart records in a fixed order is perfectly served by a list. A system that needs to look up whether a specific SKU is in a flash-sale catalogue hundreds of times per second should use a set or dict instead (O(1) lookup via hashing). Knowing this helps you choose the right tool when performance matters.
In business, lists naturally represent ordered collections: a sequence of daily customer counts, a ranked list of products by review score, a sequence of transaction IDs, or a roster of branch locations. The ordered, mutable, mixed-type nature of Python lists makes them the right first container to learn before graduating to more specialized structures like pd.Series and np.array.
Logistics platforms often maintain a live list of open delivery orders as a Python list of order-ID strings or dictionaries. When an order is dispatched, the system appends it to the list; when a delivery is completed, it removes the entry. The simplicity and speed of list .append() and .remove() make this pattern extremely common in production order-management code.
The Anatomy of a List
A list stores an ordered collection of items — numbers, strings, or any mix of objects. In a business context this is exactly the right tool for a product catalogue, a roster of customer identifiers, a sequence of branch names, or a batch of survey responses: anything where order matters and the contents can grow or shrink over time.
Interpretation: The output ['Latte', 'Mocha', 'Espresso', 'Matcha'] confirms the list stores strings in insertion order — this is your café’s menu roster. The second list [42, 3.14, 'Marketing', True] demonstrates that Python lists are heterogeneous: an integer, a float, a string, and a boolean can coexist in the same container. This flexibility distinguishes Python lists from arrays in languages like C or Java, where all elements must share the same type. In practice you will usually keep lists homogeneous (all strings, or all numbers) so you can apply uniform operations later.
Mutable lists and aliasing. When you write b = a for a list, you do not copy the list — you create a second reference to the same list object. Modifying b will silently change a as well:
a = ["Latte", "Mocha"]
b = a # b points to the SAME list
b.append("Espresso")
print(a) # ["Latte", "Mocha", "Espresso"] — a was changed!To make an independent copy, use b = a.copy() or b = a[:].
Indexing and Slicing
Python counts from 0 (zero-indexed). Negative indices count from the end.
| index 0 | index 1 | index 2 | index 3 |
|---|---|---|---|
| Latte | Mocha | Espresso | Matcha |
| -4 | -3 | -2 | -1 |
Common List Operations
.append() adds, .remove() deletes, len() counts, .sort() orders (use reverse=True for descending).
Interpretation: After menu.append("Matcha"), the list grows from 3 to 4 elements. After menu.remove("Espresso"), it shrinks back to 3. The reviews.sort() call rearranges [4.2, 3.6, 4.8] into [3.6, 4.2, 4.8] in place — meaning the original list is modified permanently. This is important: .sort() returns None, not a new list. If you need to keep the original order intact, use sorted(reviews) instead, which returns a new sorted list without touching the original. The reverse sort [4.8, 4.2, 3.6] is how you would rank menu items from highest to lowest customer-review score.
Exercise: Working with Lists
Given monthly sales figures:
sales = [12000, 15000, 8000, 22000, 18000, 9000]
- What was the sales in month 3? (Hint: zero-indexed!)
- What were the sales in the last 2 months?
- Add
25000as next month’s sales - What is the total number of months now?
Debug Yourself: Day-over-Day Growth
The cell below intends to compute the day-over-day growth rate \(g_t = (v_t - v_{t-1}) / v_{t-1}\) for each pair of consecutive daily values, then average them. The data: daily new sign-ups on a fitness-app’s promotion landing page. It runs without an error — but the answer is wrong. A hidden test at the end will tell you when you have fixed it. Try to find the bug before you peek.
The trick: range(len(signups)) starts at index 0, but there is no growth rate on day 0 because there is no day -1 to compare it to. The first iteration computes signups[0] - signups[-1] which wraps around the list (Python’s negative-index quirk) and silently produces a spurious value. Changing range(len(signups)) to range(1, len(signups)) fixes it.
List Comprehensions
Background: Functional programming and why it’s faster
List comprehensions trace their lineage to functional programming — a paradigm pioneered by the Haskell language (1990) and languages like Lisp and ML before it. In functional programming, you describe what you want (a transformed list) rather than how to produce it step by step (a loop with a counter and an append call). Python’s list comprehension syntax [expr for item in iterable] is directly inspired by Haskell’s list comprehension notation [f x | x <- xs].
Beyond elegance, comprehensions are measurably faster than equivalent for-loop code for a concrete reason: the Python interpreter can optimize a comprehension into a single bytecode operation, avoiding the overhead of repeatedly calling .append() on a growing list and executing a loop body statement by statement. For lists of a few hundred items you will not notice the difference, but when processing thousands of rows of financial data the speedup is real.
In business analytics, the most important application of comprehensions is data transformation and labeling. Classifying customers as “Premium” or “Standard” based on annual spending, flagging product reviews as “Positive” or “Negative” based on star rating, tagging deliveries as “On-time” or “Late” against an SLA — all of these are one-line comprehensions rather than multi-line loops. This is not just about brevity: readable one-liners are easier to audit, debug, and hand off to colleagues, which matters when business rules need to be re-checked under time pressure.
Analytics teams at consumer-internet companies regularly use list comprehensions to label raw data before feeding it into a machine learning pipeline — for example, converting a list of session durations into ["Engaged", "Bounced"] labels for a classification model, or tagging orders as "Large" or "Small" based on basket value. This preprocessing step, trivial with comprehensions, would require a custom function and a loop in most other languages.
Plain Comprehension
Business problem: Apply a 10% price increase to all products.
Pattern: [expression for item in list]
Interpretation: Both the for-loop and the comprehension produce identical output: [96.8, 236.5, 147.4, 325.6, 177.1]. Every price in the original list has been multiplied by 1.10 to produce the adjusted prices. Notice the output contains floats (96.8) even though the inputs were integers (88) — Python automatically promotes the type when you multiply an integer by a float. In a pricing context, this is the correct behavior: prices and price changes should always be stored as floats to avoid rounding errors in subsequent calculations.
Filtered Comprehension
Business problem: Show only premium products (price > 150).
Before running, look at the list and the filter condition — which prices will survive?
Pattern: [expression for item in list if condition]
The output list can be shorter than the input — items are filtered out.
Conditional Comprehension
Business problem: Generate Buy/Sell signals based on price.
The rule is Buy if p > 50, Sell otherwise. List the five labels in order before revealing.
Pattern: [A if cond else B for item in list]
Output always has the same length as input — every item gets a value.
Interpretation: The output ['Sell', 'Buy', 'Sell', 'Buy', 'Buy'] maps directly to the input prices [45, 120, 30, 88, 200]. Prices at or below 50 (positions 0 and 2: values 45 and 30) receive a “Sell” signal; prices above 50 (positions 1, 3, 4: values 120, 88, 200) receive a “Buy” signal. The same pattern works for any rules-based classification: tagging customers as “Premium” if monthly spend exceeds a threshold, or marking deliveries as “Late” if their duration exceeds an SLA. The output list has exactly 5 elements — same as the input — because every input must receive a classification.
Classify exam scores: scores = [88, 45, 72, 95, 61]
Output "Pass" if score \(\geq\) 60, else "Fail".
Nested Conditional Comprehension
Business problem: Assign letter grades based on score thresholds.
You can chain if/else inside a comprehension. Read left to right: first condition checked first.
Avoid nesting more than 2–3 conditions — beyond that, use a helper function for readability.
pandas Series
From Lists to pandas Series
Background: Why labeled data matters
The pandas.Series is, at its core, a NumPy array with an index — a set of labels that identifies each value. This seemingly small addition is transformative. R’s fundamental data structure, the vector, already had this idea: named vectors allow x["price"] in addition to x[3]. Wes McKinney brought the same concept to Python and extended it to handle time-series indexes, non-contiguous integer indexes, and automatic alignment by label when combining two Series.
Why does labeling matter in business data? Consider two scenarios. First, daily gym check-ins: if you have a list of 365 daily totals, checkins[0] tells you nothing without external context — you must separately track which date corresponds to index 0. A pd.Series with a DatetimeIndex attaches the date to each value permanently, so checkins["2026-03-01"] is unambiguous and operations like resampling, rolling windows, and merges with other time series work correctly without manual bookkeeping. Second, cross-sectional data: if you have revenue figures for 10 branch locations, a Series with branch names as the index lets you write revenue["Causeway Bay"] rather than looking up which integer position that branch occupies.
The practical consequence is that pandas Series eliminate an entire class of bugs common in time-series code: misalignment errors. When you add two Series with different indexes (say, one with 250 weekdays and one with 252), pandas aligns them by label before computing, producing NaN where a date is missing in one Series rather than silently adding the wrong numbers together.
Across operations and marketing analytics, pandas Series are the standard container for time series of any kind: daily revenue, website session counts, delivery durations, and customer-support ticket volumes. The timestamp index is especially valuable because it allows direct querying by date range (s["2026-01-01":"2026-12-31"]) and joins with data that arrives at different frequencies.
The Limits of Plain Lists
Lists are general-purpose containers. As soon as you try to do analysis on them — arithmetic, summary statistics, or plotting — the gaps appear. If checkins = [100, 102, 98, 105, 101], then checkins + 10 raises a TypeError, checkins.mean() raises an AttributeError, and checkins.max() likewise has no such method. The table below summarises the contrast between a plain list and a pandas Series.
| Operation | List | Series |
|---|---|---|
checkins + 10 |
X | OK |
.mean() |
X | OK |
.max() |
X | OK |
.plot() |
X | OK |
Converting a List to a Series
Wrapping a list in pd.Series() immediately equips it with element-wise arithmetic, descriptive statistics, alignment, and one-line plotting. The same five-element list of daily gym check-ins, once converted, supports s + 10 (broadcasts the scalar across all elements), s.mean(), and s.max() — all of which would error out on the raw list.
pd.Series(list) converts a plain list into a powerful data object with built-in math, statistics, and plotting.
Conceptually, converting a list to a Series attaches an index label to each value. The raw list [100, 102, 98, 105, 101] becomes a labelled mapping such as Mon: 100, Tue: 102, Wed: 98, Thu: 105, Fri: 101, and from that point onward every operation respects those labels.
Interpretation: s + 10 produces a new Series where every element has been increased by 10 — [110, 112, 108, 115, 111]. This is broadcasting: the scalar 10 is automatically applied element-wise across all 5 values. s.mean() returns 101.2, the arithmetic average of the five days. s.max() returns 105, the busiest day in the series. None of these operations required a loop or a helper function — they are all built into pd.Series. Compare this to the list version where checkins + 10 raises a TypeError and checkins.mean() raises an AttributeError.
pandas Series alignment by index. When you perform arithmetic between two Series that have different indexes, pandas aligns them by label first. If a label exists in one Series but not the other, the result is NaN (Not a Number) — silently, without raising an error:
s1 = pd.Series([100, 102], index=["Mon", "Tue"])
s2 = pd.Series([5, 6], index=["Tue", "Wed"])
print(s1 + s2)
# Mon NaN
# Tue 108.0
# Wed NaNThis alignment is a feature, not a bug — it prevents you from adding mismatched data. But if you expected a 2-element result and got a 3-element result with two NaNs, you will be surprised. Always check .index before combining Series.
Series Attributes
Every Series exposes two kinds of accessors. Attributes — written without parentheses — describe the data: .dtype gives the element type, .shape gives the dimensions, .index returns the label axis, and .name returns the Series’s own label. Methods — written with parentheses — compute on the data: .mean(), .max(), .plot(), and so on. The distinction is consistent throughout pandas: anything that is intrinsic to the object is an attribute; anything that requires computation is a method.
Interpretation: s.dtype returns int64 (or float64 if any values are decimals), telling you pandas is using 64-bit integers to store the values — this is the default for whole numbers and provides ample precision for any business count. s.shape returns (5,) — a tuple showing 5 rows and no second dimension (Series is 1D). s.index returns the day labels you supplied: Index(['Mon', 'Tue', 'Wed', 'Thu', 'Fri']). The .name attribute "Gym daily check-ins" will appear as the axis label in plots and as the column header when this Series is inserted into a DataFrame — always set it to something meaningful.
pandas Series Methods
Once your data is in a Series, dozens of single-call methods give you summary statistics, sorting, counting, and basic plotting. The most-used ones are .mean(), .median(), .std(), .min(), .max(), .quantile(p), .value_counts(), .sort_values(), and .describe(). Most behave exactly as you would expect — but .std() has one subtle default that surprises almost every newcomer.
.std() defaults to the sample standard deviation, not the population one. Internally, pandas computes
\[\text{std}(x) = \sqrt{\dfrac{\sum_{i=1}^{n}(x_i - \bar{x})^2}{n - \text{ddof}}}\]
where ddof is the delta degrees of freedom. pandas uses ddof=1 by default (divide by \(n-1\), Bessel’s correction). NumPy’s np.std() uses ddof=0 by default (divide by \(n\)). The same data will give you two different numbers depending on which library you call, which is the single most common source of “I got a different answer than my classmate” bugs in this course.
When to use which:
ddof=1(default in pandas) — Use when your data is a sample drawn from a larger population and you want an unbiased estimate of the population variance. This is the right choice almost every time in business analytics: a month of daily delivery times is a sample of the delivery-time process; a survey of 500 customers is a sample of all customers.ddof=0— Use only when your data is the full population — e.g., the year-end salaries of every employee at a 200-person firm, where you have all 200 values and there is no larger population to infer about.
In this course, leave .std() at its default. The pandas convention (ddof=1) matches how Excel’s STDEV.S, statsmodels, and most statistics textbooks define standard deviation. If you ever see a tiny discrepancy between pandas and NumPy on the same data, the cause is almost always this ddof default — not a real numerical issue.
Computing Summary Statistics
To make the examples concrete, we construct a small synthetic Series of gym daily check-ins — the number of members visiting a fitness club each day over a 30-day window. Weekdays draw roughly 120 check-ins on average; weekends roughly 180. A reproducible random-number seed makes the example identical every time the cell runs.
.describe() gives you count, mean, std, min, 25%, 50%, 75%, max — the complete “profile” of your data in one line.
Counting and Sorting
Interpretation: .value_counts() returns the frequency of each category in descending order: “C” appears 4 times (the most common rating), “A” 3 times, “B” 2 times. In a business context this instantly tells you the modal response — e.g., “C-rated products dominate our catalog.” Note that the result is itself a Series, so you can chain further operations: ratings.value_counts().plot(kind="bar") would instantly produce a frequency bar chart with one additional call. .sort_values() on prices returns a new Series (not in-place, unlike .sort() on a list) with the same index labels but reordered by value — the index moves along with its value, preserving the label-value pairing.
Plotting Directly from a Series
pandas integrates directly with Matplotlib. Calling .plot() on a Series produces a line chart by default; calling .hist() produces a histogram. Behind the scenes, pandas reads the Series’s index as the x-axis and its values as the y-axis, then hands the figure off to Matplotlib for rendering. The same gym-check-in Series we used above can be charted with the familiar Matplotlib keyword arguments — figsize, color, title — without changing the structure of the call.
pandas integrates with matplotlib. One method call = one chart. Customize with figsize, color, title.
Same Methods, Different Context: K-pop Streams
The descriptive-statistics toolkit is not tied to finance. Any one-dimensional numerical series — daily streams of a song, hourly visitors to a website, weekly footfall in a Causeway Bay store — accepts the same .describe() and .plot() calls. Below is a small synthetic series of daily Spotify streams (in millions) for a hypothetical K-pop track during the first 20 days of its release. Same descriptive statistics methods, music industry context.
Interpretation: .describe() reports a mean of roughly 11.6 million streams per day, a sample standard deviation of about 2.5 million, a minimum of 7.5 million on Day 6, and a maximum of 16.7 million on Day 11. The bar chart of the first 14 days shows visible day-to-day variation but no obvious trend. The code is line-for-line identical to the gym check-ins example above; only the underlying business question has changed. Below we return to this series to draft a managerial recommendation.
NumPy Arrays
NumPy: Arrays and Vectorized Math
Background: Vectorization and why speed matters
When Python executes for x in values: result.append(x * 2), the interpreter processes each iteration one at a time: it fetches the loop variable, evaluates the expression, calls the append method, advances the counter, and checks the termination condition — dozens of individual operations per element. On a modern CPU this overhead is inconsequential for 10 elements but becomes prohibitive for millions.
NumPy solves this with vectorization. When you write values_array * 2, NumPy passes the entire array to a compiled C function that exploits two CPU-level optimizations. First, SIMD (Single Instruction, Multiple Data): modern CPUs have 256-bit or 512-bit vector registers that can perform the same arithmetic operation on 4, 8, or 16 numbers simultaneously in a single clock cycle. Second, cache locality: because NumPy stores all array elements contiguously in memory, the CPU can pre-fetch entire blocks into its fast L1/L2 cache and process them without waiting for slow RAM accesses. The combined effect is that array * 2 on a million elements runs 10x to 100x faster than the equivalent Python loop.
For large-scale business simulations — modelling customer-arrival patterns at thousands of retail outlets, simulating millions of website-session durations, or batch-pricing hotel rooms across dynamic-pricing scenarios — this speed difference is the boundary between “runs in a second” and “runs in several minutes.” NumPy’s np.random.normal() generates thousands of simulated scenarios in milliseconds, enabling the kind of large-scale simulation that underpins demand forecasting, capacity planning, and A/B-test power analysis.
Logistics teams use NumPy to run Monte Carlo simulations of delivery times across a courier network — often 100,000 paths, each with 24 hourly steps — to assess capacity under demand surges. The entire simulation (np.random.normal(mu, sigma, (100000, 24)).cumsum(axis=1)) runs in under a second on a laptop. The equivalent loop in pure Python would take minutes.
NumPy Arrays Compared to Lists
A NumPy array supports element-wise arithmetic — what NumPy calls broadcasting — in exactly the same way a pandas Series does, but without a named index. Multiplying a list by 2 repeats the list ([100, 102, ..., 100, 102, ...]), whereas multiplying a NumPy array by 2 doubles each element ([200, 204, 196, 210, 202]). This is the single most important behavioural difference between the two containers.
NumPy arrays support element-wise math (broadcasting), just like pd.Series — but without a named index.
Interpretation: checkins_list * 2 produces [100, 102, 98, 105, 101, 100, 102, 98, 105, 101] — the entire list repeated twice. Python’s * operator on a list means “concatenate this list with itself n times,” which is useful for initializing repeated structures but almost never what you want when you intend mathematical multiplication. checkins_array * 2 produces [200, 204, 196, 210, 202] — each value doubled, which is the intended computation. The same behavior applies to +: list + [1] appends a one-element list, while array + 1 adds 1 to every element. This is one of the most common bugs beginners encounter when transitioning from lists to numerical computing.
list * 2 repeats, array * 2 multiplies. These operators behave completely differently for lists vs. NumPy arrays:
| Expression | List result | Array result |
|---|---|---|
x * 2 |
repeats the list | multiplies each element by 2 |
x + [1] |
appends [1] to the list |
raises an error (shape mismatch) |
x + 1 |
raises TypeError |
adds 1 to every element |
Always convert to np.array() before doing mathematical operations on sequences of numbers.
Random Numbers — Simulate Daily Step Counts
Syntax: np.random.normal(μ, σ, n) — draw n samples from \(\mathcal{N}(\mu,\,\sigma^2)\)
Each call gives different random draws. The sample mean/std will be close to \(\mu\)/\(\sigma\) but not exact.
Interpretation: With only n = 100 draws, your sample mean will typically land somewhere between roughly 8200 and 8800 steps — close to the true \(\mu = 8500\) but not exactly equal. This reflects sampling variability: the standard error of the mean is \(\text{SE} = \sigma / \sqrt{n} = 1500 / \sqrt{100} = 150\), so roughly 68% of the time your sample mean will fall within one standard error of the true mean (between 8350 and 8650). With \(n = 10{,}000\) draws the standard error shrinks to 15 steps, and the sample mean becomes much tighter around 8500. The sample std will similarly hover near 1500 but not exactly equal it, especially with only 100 observations. This illustrates the Central Limit Theorem in action: larger samples produce more stable estimates.
Wearable-device manufacturers run large simulations of step-count distributions to design notification thresholds, set daily-goal recommendations, and detect anomalies in real user data. NumPy’s np.random.normal() is the workhorse: a simulation drawing 1 million daily step totals across 10,000 simulated users runs in well under a second on a laptop.
Plotting
Plotting with Matplotlib
Background: From neuroscience to the financial terminal
Matplotlib was written by John D. Hunter in 2003 while he was analyzing epileptic seizure data from electrocorticography recordings. He needed MATLAB-style interactive plots in Python — without the MATLAB license cost — and built Matplotlib essentially by reverse-engineering MATLAB’s plotting API. His guiding principle: any MATLAB plot command should have an almost identical Python equivalent. plt.plot(), plt.hist(), plt.scatter(), plt.xlabel(), plt.title() — every one of these maps directly to a MATLAB counterpart.
Matplotlib introduced the now-standard two-level architecture for plotting: the figure (the overall window or page) and axes (an individual coordinate system within the figure). plt.subplots(2, 2) creates a figure with a 2×2 grid of four axes objects, each of which can hold an independent chart. This architecture is powerful because it lets you build complex multi-panel dashboards — price chart, volume histogram, correlation scatter plot, and drawdown line — in a single figure with precise layout control.
For business communication, knowing which chart type to use is as important as knowing the code. Edward Tufte’s visualization principles provide useful guidance: use line charts for continuous data that evolves over time (daily check-in series), bar charts for comparing discrete categories (revenue by region), scatter plots for exploring the relationship between two continuous variables (advertising spend vs. sales), and histograms for understanding the distribution of a single variable (delivery-time frequency). Reserve pie charts for showing composition when you have fewer than 5 categories and the differences are large enough to see. Avoid 3D charts, gradients, and decorative elements that add ink without adding information.
For internal analysis and research, Matplotlib is the standard choice: it produces publication-quality static figures suitable for reports, presentations, and regulatory filings. For interactive client-facing dashboards (where users can zoom, filter, and hover for tooltips), analysts typically switch to Plotly or Bokeh. The two tools are complementary: learn Matplotlib’s API thoroughly and the concepts transfer directly to any Python visualization library.
Two Plotting Styles in Matplotlib
There are two ways to draw a chart in Matplotlib, and you will see both throughout this book. The first calls plt functions directly: you supply the x-values and y-values explicitly, as in plt.plot(days, value, color="blue"), plt.hist(durations, bins=30), or plt.scatter(x, y). This style works with any kind of input — lists, NumPy arrays, or pandas Series — and gives you complete control over every detail of the figure. The second style calls plotting methods directly on a pandas Series or DataFrame: checkins.plot(color="blue"), checkins.hist(bins=30), checkins.plot(kind="bar"). This style is shorter and well suited to quick exploration, because the Series already knows its own index (used automatically as the x-axis) and values. As a rule of thumb, use Series methods for quick exploration of pandas data and fall back to direct plt calls when you need full control or when the data is not in a Series.
# Way 1: plt (matplotlib directly) — works with any data
plt.plot(days, value, color="blue")
plt.hist(durations, bins=30)
plt.scatter(x, y)
# Way 2: Series methods — works with pandas Series/DataFrame
checkins.plot(color="blue")
checkins.hist(bins=30)
checkins.plot(kind="bar")Line Plots: Prices Over Time
Key params: color, linewidth, linestyle, marker, label.
Interpretation: The dashed line with circle markers makes each data point visible while connecting them to reveal the overall trend. The visitor count rises from 100 on Day 1 to 115 on Day 10, a 15 % gain over the period. The non-monotone path — with a dip to 99 on Day 3 and temporary plateaus — reflects realistic foot-traffic dynamics. In a real application, you would replace days with actual DatetimeIndex values and visitors with a pandas Series, but the Matplotlib code would be nearly identical. The plt.legend() call relies on the label="Store visitors" argument passed to plt.plot() — always label every line before calling plt.legend().
plt.show() and interactive vs. static backends. In a standard Python script, plt.show() opens an interactive window and blocks execution until you close it. In Jupyter notebooks and Colab, plt.show() is optional — the figure renders automatically at the end of a cell. If you call plt.show() in the middle of a cell, it renders and clears the current figure, so any subsequent plotting commands start on a fresh, empty canvas. Always complete all customization (titles, labels, legends) before calling plt.show().
Histograms: Seeing a Distribution
Histograms reveal shape: is the distribution symmetric? Are there long tails?
Interpretation: With np.random.seed(42) ensuring reproducibility, 500 delivery times drawn from \(\mathcal{N}(28, 6^2)\) should produce a bell-shaped histogram centered near 28 minutes. The red dashed vertical line marks the sample mean, which should sit very close to 28 with 500 observations. In practice, real delivery-time distributions are not perfectly normal: they tend to be right-skewed (a few orders take exceptionally long because of traffic or unavailable couriers) and leptokurtic (fatter tails than the normal distribution predicts). If your histogram shows a noticeable right tail or asymmetric shape, this is a signal that the normal distribution will understate the frequency of late deliveries — a key concern in service-level monitoring.
Scatter Plots: Two Variables at Once
Scatter plots reveal the direction and strength of association between two variables.
Interpretation: Because ad_spend and new_users were drawn independently (no correlation), the scatter plot should show a roughly circular cloud of points centered at the origin. There is no discernible linear pattern — the points are scattered in all directions with roughly equal density. In contrast, if ad spend genuinely drove sign-ups (with an elasticity around 1.5–2.0), you would see an elongated cloud slanting upward from lower-left to upper-right: when daily ad spend is higher than average, daily sign-ups tend to be higher too. The slope of that cloud is the regression coefficient, and the tightness of the cloud around the line is the R-squared. This simulation uses independent random draws specifically to illustrate the baseline “no-relationship” case.
Figures and Subplots
plt.subplots(r, c) returns a grid of Axes objects. Always call plt.tight_layout() to prevent overlap.
Interpretation: The four-panel figure demonstrates the standard workflow for exploratory data analysis: (1) the line chart reveals temporal trends and turning points in the visitor series; (2) the histogram shows the distribution shape of the 500 simulated delivery times — roughly bell-shaped and centered near 28 minutes; (3) the scatter plot confirms the independent relationship between the two simulated variables; and (4) the boxplot summarizes the delivery-time distribution compactly — the box spans the interquartile range (Q1 to Q3), the horizontal line inside the box is the median, and the “whiskers” extend to the most extreme non-outlier values. The plt.tight_layout() call is essential: without it, titles and axis labels from adjacent panels overlap and the figure becomes unreadable.
Plotting with Series Methods
Series methods (.plot(), .hist()) accept ax= to place them in subplots. No need to pass x and y separately.
Exercise: Matplotlib
- Generate 300 normal random delivery times:
np.random.normal(28, 6, 300). Plot the histogram. Add a vertical dashed line at the mean. - Simulate two related variables (e.g., advertising spend and new sign-ups). Create a scatter plot. Add axis labels and a title.
- Create a \(1 \times 2\) subplot: line chart of daily store visitors on the left, boxplot of delivery times on the right. Use
plt.tight_layout().
Probability and SciPy
SciPy for Statistics
Background: From Gauss to Operational Thresholds
The normal distribution has a history as old as modern science. Carl Friedrich Gauss formalized it in 1809 while developing the method of least squares to estimate planetary orbits, arguing that measurement errors tend to aggregate toward zero — an observation that is now called the Central Limit Theorem (CLT). The CLT states that the sum (or average) of a large number of independent random variables converges to a normal distribution, regardless of the underlying distribution of the individual variables. This is why the normal distribution appears so naturally in business operations: a single delivery time is the sum of many small independent stages (kitchen prep, courier dispatch, traffic, last-mile handoff), and under mild conditions the total time is approximately normal.
scipy.stats provides access to over 100 probability distributions and their key functions. For a normal distribution, the two most important are the CDF (cumulative distribution function) and the PPF (percent point function, also called the inverse CDF). The CDF answers: “Given a value \(x\), what fraction of outcomes fall below \(x\)?” The PPF answers the reverse: “Given a probability \(p\), what value \(x\) cuts off the bottom \(p\) of the distribution?” These two operations are the mathematical engines behind most statistical analyses in business.
A common business application is setting operational thresholds — service-level targets, alarm cutoffs, or refund triggers. The “95 % SLA” for a delivery service answers: “What is the time below which 95 % of orders arrive?” Computing this requires nothing more than calling scipy.stats.norm.ppf(0.95, loc=mu, scale=sigma) with the service’s estimated mean and standard deviation. The same two SciPy calls drive quality-control limits in manufacturing, staffing thresholds in call centres, and conversion-rate guardrails in online experiments.
Operations teams at delivery platforms and online retailers monitor a daily distribution of cycle times and use stats.norm.ppf at chosen percentiles to set automatic alerts. If the actual percentile drifts more than two standard errors from the target — say, the 95th-percentile delivery time creeps from 38 to 44 minutes — the on-call engineer is paged. Understanding scipy.stats therefore gives you direct access to the mathematical foundation of operational monitoring.
The CDF and the PPF
The CDF maps a value to a probability: given \(x = 10\), the shaded area under the normal density to the left of 10 is about 94.5% of the total area. The PPF runs the same mapping in reverse: given the probability 5%, the \(x\)-value that cuts off the left 5% of the distribution is approximately 1.89.
.cdf(x): value → probability \(\longleftrightarrow\) .ppf(p): probability → value
Interpretation: stats.norm.cdf(10, loc=6, scale=2.5) returns approximately 0.945, meaning that 94.5 % of observations from a \(\mathcal{N}(6, 2.5^2)\) distribution fall below 10. This is consistent with the empirical rule: 10 sits \((10 - 6)/2.5 = 1.6\) standard deviations above the mean, and roughly 94–95 % of probability mass lies below \(\mu + 1.6\sigma\). Conversely, stats.norm.ppf(0.05, loc=6, scale=2.5) returns approximately 1.89: the value at the 5th percentile of this distribution. In other words, 5 % of outcomes fall below 1.89. These are not just abstract mathematical facts — in service-level analysis, loc is the average wait time and scale is its standard deviation, making the PPF at 5 % the cutoff below which only the fastest 5 % of services complete.
Interactive Explorer: Move μ and σ
The two values \(\mu\) and \(\sigma\) at the top of the cell control a normal distribution of delivery times in minutes. Edit them, click Run, and watch how the density shifts left/right (with \(\mu\)) and stretches/compresses (with \(\sigma\)), and how the 5 % best-case cutoff — the time below which only 5 % of deliveries arrive — moves accordingly. Try \(\mu = 28, \sigma = 6\) for a typical food-delivery service; then \(\mu = 28, \sigma = 12\) to see how a less reliable courier network widens the spread and pushes the best-case cutoff further from the mean.
Probability and Quality-Control Thresholds
A Hong Kong food-delivery company tracks the time taken to fulfil each order. Historical data shows that delivery times are approximately normal with mean 28 minutes and standard deviation 6 minutes: \(T \sim \mathcal{N}(28, 6^2)\). Two operational questions arise. Question 1 — service-level alert: what fraction of deliveries take longer than 40 minutes, the threshold above which the customer-experience team is paged? Question 2 — fastest deliveries: what is the 5th-percentile delivery time, i.e., the cutoff below which only the quickest 5 % of deliveries arrive? The first is a stats.norm.cdf call; the second is a stats.norm.ppf call.
.cdf(threshold) answers “what fraction exceed the threshold?”; .ppf(probability) answers “what value cuts off that fraction?” Same two SciPy calls drive most operational threshold questions.
Interpretation: The first calculation says about 2.3 % of deliveries take longer than 40 minutes. If the operations team is paged on each such event, a courier handling 500 deliveries per day will trigger roughly 11 alerts daily — useful for staffing the on-call rota. The second calculation says the fastest 5 % of deliveries arrive within roughly 18 minutes; the company could use this as the cutoff to award a “Speedy Courier” bonus to drivers whose median delivery time falls below it. The p-value of 0.0107 for the unrelated one-tailed hypothesis test says: if the true mean were at most 100, the probability of observing a z-statistic as large as 2.3 purely by chance is only 1.07 %. Since this is below the conventional 5 % threshold, we reject the null hypothesis at the 5 % significance level.
Operations teams at Foodpanda, Deliveroo, and HKTVmall use exactly this stats.norm.cdf / stats.norm.ppf pattern to set SLA thresholds, courier incentive cutoffs, and refund-trigger boundaries. The same two SciPy calls also appear in manufacturing quality control (defect-rate thresholds), call-centre staffing (waiting-time quantiles), and hospital triage (length-of-stay percentiles). The mathematics is identical; only the units change.
Worked Example and Recap
Mini Project: A Restaurant Performance Analyzer
The remainder of this chapter pulls the pieces together. We will build a small Restaurant Performance Analyzer that uses lists, list comprehensions, f-strings, a pandas Series, and Matplotlib in a single workflow — the same logical pipeline a junior analyst at a restaurant chain would assemble for a weekly menu review.
Building the Analyzer
We start with four parallel lists for four menu items at a casual-dining outlet: the dish names, average daily customer count, average bill size in HKD, and the number of days each dish was on the menu during a 30-day window. List comprehensions with zip give us per-dish daily revenue; sum gives us total menu revenue; and a second comprehension converts dish revenue into a percentage share of the total.
Interpretation: The daily-revenue figures [3570, 6380, 2280, 3920] HKD represent how much each menu item brings in per day. Char Siu Rice contributes the largest revenue (HK$6,380) — high customer count and a mid-priced bill. The HK-style French Toast contributes the least (HK$2,280) despite a respectable bill size, because it serves the fewest customers. Total daily menu revenue is approximately HK$16,150. The revenue shares [22.1 %, 39.5 %, 14.1 %, 24.3 %] show Char Siu Rice as the menu’s anchor at nearly 40 % of revenue; the French Toast accounts for only 14 %. In menu engineering, this concentration would be flagged: an anchor dish exceeding 35 % of revenue exposes the outlet to supply-shock risk if its main ingredient becomes scarce or expensive.
Visualising and Classifying Results
This mini project combines lists, comprehensions, f-strings, pd.Series, and matplotlib — all in one workflow.
Interpretation: The bar chart makes Char Siu Rice’s dominance visually obvious — its bar towers over the French Toast. The pie chart reinforces this, with Char Siu Rice occupying roughly 40 % of the circle. The comprehension-based classification ['Below average', 'Above average', 'Below average', 'Below average'] flags Char Siu Rice as the only dish whose daily revenue exceeds the four-dish mean (≈ HK$4,038), while the other three sit below. In menu engineering, “above-average” dishes typically anchor the menu and justify prime placement, while “below-average” dishes are candidates for promotion, repricing, or removal in the next menu refresh. This classification scheme — implemented here in one line of Python — directly mirrors how restaurant operators triage their menus.
When to Use Which Tool
| Property | List | pd.Series | np.array |
|---|---|---|---|
| Mixed types | ✓ | ✗ | ✗ |
| Element-wise math | ✗ | ✓ | ✓ |
| Built-in stats | ✗ | ✓ | ✓ |
| Named index | ✗ | ✓ | ✗ |
| One-liner plots | ✗ | ✓ | ✗ |
| Fastest math | ✗ | ✗ | ✓ |
Rule of thumb: Start with a list for raw data. Convert to pd.Series for analysis. In this course, we mostly use pd.Series for data work. We only use np.array for generating random samples (e.g., np.random.normal()).
Chapter Wrap-up
Chapter Summary
This chapter introduced the core Python tools that underpin modern data analysis in business and finance. The progression from lists to Series to arrays mirrors the historical evolution of the Python data science stack: each layer was built because the previous one was insufficient for the demands of quantitative work.
Key concepts
| Python construct | Core capability | Business application |
|---|---|---|
list |
Ordered, mutable, mixed-type collection | Menu catalogue, transaction log, branch roster |
| List comprehension | Concise transformation and filtering | Pass/Fail labelling, customer segmentation |
pd.Series |
Labeled 1D array with built-in stats and plotting | Daily check-in time series, branch revenues, survey responses |
np.array |
Contiguous typed array with vectorized math | Monte Carlo simulation, demand forecasting, batch calculations |
scipy.stats.norm |
Normal distribution CDF and PPF | Service-level thresholds, hypothesis testing, confidence intervals |
matplotlib |
Flexible static charting | Time-series plots, distribution histograms, scatter plots |
When to use list vs. Series vs. array — decision guide
- Use a
listwhen your data is heterogeneous (mixed types), small, and you primarily need to add/remove elements rather than compute on them. Lists are Python’s general-purpose container. - Use a
pd.Serieswhen your data is numerical or categorical, you have meaningful labels (dates, ticker names, category names) for the index, and you want built-in statistics, alignment, and plotting. This is the default choice for data analysis. - Use a
np.arraywhen you need the absolute fastest computation — especially for large-scale numerical operations like simulating 100,000 delivery-time scenarios, solving a linear system, or computing a covariance matrix. NumPy arrays have no index overhead, which makes them faster than Series for pure computation. Convert back to a Series if you need labels afterward.
A practical rule: start with a list when you are collecting raw inputs. As soon as you need to analyze the data — compute means, plot charts, filter by condition, or combine with other data — convert to a pd.Series with pd.Series(my_list, index=my_labels). Only reach for np.array directly when you are writing simulation code or performance-critical numerical routines.
What’s next: Chapter 2 — DataFrames
A pd.DataFrame is a collection of pd.Series objects sharing the same index — essentially a spreadsheet or database table in Python. Chapter 2 covers loading data from CSV files, Excel workbooks, and SQL databases; exploring and cleaning DataFrames (handling missing values, type conversions, renaming); filtering rows and selecting columns; groupby aggregation; and time-series resampling. Every concept from Chapter 1 transfers directly: Series is the building block of DataFrame, and everything you learned about indexing, methods, and plotting applies.
Further reading
- Wes McKinney, Python for Data Analysis, 3rd ed. (O’Reilly, 2022) — the definitive reference for pandas, written by its creator. Chapter 3 covers Series in depth.
- Travis E. Oliphant, “A guide to NumPy” (2006) — the original technical paper describing NumPy’s architecture, vectorization model, and BLAS integration. Available free at
numpy.org/doc. - Edward R. Tufte, The Visual Display of Quantitative Information (Graphics Press, 2001) — the foundational text on data visualization principles, widely read by analysts and data scientists.
- John D. Hunter, “Matplotlib: A 2D graphics environment,” Computing in Science & Engineering (2007) — Hunter’s own account of Matplotlib’s design philosophy and architecture.
Working with an AI Copilot
You will spend a large fraction of this course typing prompts into an AI coding assistant. The assistant is fast at typing pandas and NumPy, but slow at remembering statistical defaults. A typical failure mode looks like this: you ask the assistant to “compute the standard deviation of these prices,” and it returns numpy.std(prices). The line runs without error. The answer is wrong for almost every business application, because numpy.std uses ddof=0 (population standard deviation, divide by \(n\)), whereas a quant analyst almost always wants the sample standard deviation ddof=1, which is prices.std() in pandas. The code is syntactically correct and statistically misleading.
Three rules keep this from biting you. First, prompt the AI to explain its choice, not just produce code — ask why this function and not another. Second, for any statistical computation, ask the AI which ddof, axis, and skipna defaults the function uses. Third, when in doubt, compute the same quantity two different ways — pandas and NumPy, or by hand on a small example — and confirm the answers agree.
Mistakes Library: The 2020 UK A-level Algorithm Scandal
In summer 2020, the UK examinations regulator OfQual cancelled in-person A-level exams because of COVID-19 and replaced them with an algorithm that predicted each student’s final grade from two inputs: the teacher’s predicted grade for that student, and the historical grade distribution of the student’s school. The algorithm pulled individual predictions toward the school’s historical mean — exactly the kind of group-level summary statistic this chapter has spent twenty pages computing. The result: roughly forty per cent of grades came back lower than the teacher’s prediction, and the downgrades fell disproportionately on students from disadvantaged schools, whose historical means were lower for reasons that had nothing to do with the student sitting in the room.
Public protest forced a complete U-turn within days, and OfQual reverted to teacher-predicted grades. The technical lesson is narrow and severe. Descriptive statistics computed at a group level — a school mean, a sample standard deviation, an industry median — summarise distributions. They do not score individuals. Mean and standard deviation are tools for understanding a population, not for deciding the fate of any one person inside it.
Decision Memo — Should the Label Push a Comeback?
Every analyst’s job ends with a memo of this shape, not a chart. A chart shows what happened; a memo recommends what to do. The rest of this course will repeat this template — recommendation first, evidence next, caveats third, next step last — across pandas, regression, and clustering. Learn the shape now.
To: JYP Entertainment, A&R Team From:
, junior analyst Subject: Recommend NOT pushing a paid comeback for Track A in week 3 Date: 2026-05-15 Recommendation: Hold the additional marketing budget. The track is performing within its expected variance and an additional push is unlikely to clear the cost.
Evidence: - 20-day mean: 11.6 M streams/day; sample std: 2.5 M (sample, n−1). - 95% VaR-style lower band ≈ 6.7 M/day → tail risk acceptable. - Linear trend over 20 days is flat (≈ +0.05 M / day, not significant).
Caveats: - Assumes future days are drawn from the same distribution. - 20-day window is short; weekly seasonality may dominate.
Next step: Re-run with 60 days of data and a fitted ARIMA before any budget decision.
Review Cards — Chapter 1
These cards are scheduled for spaced repetition. Click Show answer, rate how easily you recalled it, and the book will surface the harder cards again on a longer interval — exactly the Anki technique used to study language and medical school material. Your review history is stored privately in your browser; nothing is uploaded.
The last element of the list. Negative indices count from the end: -1 is the last item, -2 the second-to-last, and so on.
list.sort() sorts the list in place (mutates the original) and returns None. To get a new sorted list without modifying the original, use the built-in sorted(my_list).
signals = ["Buy" if p > 50 else "Sell" for p in prices] — note the conditional expression goes before the for, not after.
pandas uses \(n{-}1\) (sample std, ddof=1) by default. NumPy uses \(n\) (population std, ddof=0) by default. Same data gives slightly different numbers — the single most common cause of “I got a different answer than my classmate” in this course.
The 5th percentile of a Normal(\(\mu, \sigma^2\)) distribution: the value \(x\) such that \(P(X < x) = 0.05\). In risk management this is the 5 % left-tail Value at Risk.
a * 2 multiplies every element by 2 (vectorised math). L * 2 concatenates the list with itself, doubling its length. This is one of the most common bugs when first moving from lists to NumPy.
plt.plot(x, y) is the explicit matplotlib API — you supply both axes. s.plot() is the convenience method on a Series, which uses the Series’s index as the x-axis and the values as the y-axis. The two produce the same chart when the data is the same; the Series version is shorter for time-indexed data.