Python & Scientific Computing
Level: 4 — Application (novel problems, no hints) Time limit: 60 minutes Total marks: 60
Answer all questions. Show reasoning, code, and outputs where asked. Assume import numpy as np, import pandas as pd are available unless stated otherwise.
Question 1 — Broadcasting & Vectorization (12 marks)
You are given a dataset of M points in R^d stored in array X of shape (M, d) and N centroids in array C of shape (N, d).
(a) Write a single vectorized NumPy expression (no Python loops) that produces a matrix D of shape (M, N) where D[i, j] is the squared Euclidean distance between point i and centroid j. Explain the broadcasting shapes involved. (6)
(b) Given X has shape (1000, 3) and C has shape (8, 3), state the shape of every intermediate array in your expression, and the final shape. (3)
(c) Using D, write one line that returns the index of the nearest centroid for each of the M points. (3)
Question 2 — Comprehensions & Generators (12 marks)
(a) Using a single dict comprehension, build a dictionary mapping each word in the list words = ["ml", "ai", "data", "ml", "ai", "ml"] to its frequency count. You may not import Counter. (4)
(b) Explain the difference in memory behaviour between
a = [x**2 for x in range(10**7)]
b = (x**2 for x in range(10**7))and give one situation where b is strictly preferable. (4)
(c) Write a generator function batched(iterable, n) that yields successive lists of length n (the last batch may be shorter). Show the output of list(batched(range(7), 3)). (4)
Question 3 — Pandas Data Wrangling (14 marks)
A CSV file sales.csv has columns: date (YYYY-MM-DD), region, product, units, price.
(a) Write code to load the file, parse date as datetime, and create a new column revenue = units * price. (4)
(b) Produce a table of total revenue per region per month, with regions as rows and months as columns. Name the pandas operation(s) you use. (5)
(c) A colleague reports that df.groupby("region")["revenue"].mean() gives a different answer than expected because some units values are stored as strings like "12". Explain why this silently corrupts the result and give one line to detect the problem and one line to fix it. (5)
Question 4 — Functions, Classes, Debugging (12 marks)
Consider this code intended to accumulate results:
def add_item(item, bucket=[]):
bucket.append(item)
return bucket
a = add_item(1)
b = add_item(2)(a) State the values of a and b after execution and explain the underlying cause. (4)
(b) Rewrite add_item so each call with no bucket starts fresh. (3)
(c) Design a small class RunningStats that supports .update(x) and exposes properties .mean and .count, computing the mean incrementally (without storing all values). Show the formula used for the incremental mean update. (5)
Question 5 — Environments, Git & Reproducibility (10 marks)
(a) A teammate cannot reproduce your results. Describe the exact sequence of commands you would give them to create an isolated environment and install the pinned dependencies from your requirements.txt. (4)
(b) You accidentally committed a 500 MB data file and a secrets file. The commit is already pushed. Describe the correct remediation and one preventative measure. (3)
(c) Explain the difference between git fetch and git pull, and when you would prefer fetch. (3)
Answer keyMark scheme & solutions
Question 1 (12)
(a) (6 marks)
D = ((X[:, None, :] - C[None, :, :])**2).sum(axis=2)X[:, None, :]has shape(M, 1, d)— (1)C[None, :, :]has shape(1, N, d)— (1)- Broadcasting aligns to
(M, N, d); element-wise subtract then square — (2) .sum(axis=2)reduces thedaxis giving(M, N)— (2)
Alternative accepted (identity ):
D = (X**2).sum(1)[:,None] - 2*X@C.T + (C**2).sum(1)[None,:]Full marks if correct and vectorized.
(b) (3 marks)
X[:, None, :]→(1000, 1, 3)— (0.5)C[None, :, :]→(8, 3)broadcast to(1, 8, 3)— (0.5)- difference/square →
(1000, 8, 3)— (1) - final
D→(1000, 8)— (1)
(c) (3 marks)
nearest = D.argmin(axis=1) # shape (M,)argmin over axis=1 (the centroid axis) — (3) (1 for correct axis).
Question 2 (12)
(a) (4 marks)
freq = {w: words.count(w) for w in set(words)}
# {'ml':3, 'ai':2, 'data':1}- dict comprehension over
set(words)to avoid repeats — (2) .countgives frequency — (1)- correct result — (1)
(b) (4 marks)
ais a list: all10^7squares are computed and stored in memory at once (~hundreds of MB) — (1.5)bis a generator: lazy, computes each value on demand, O(1) memory — (1.5)bpreferable when streaming/iterating once over huge data that need not be materialised (e.g. summing) — (1)
(c) (4 marks)
def batched(iterable, n):
batch = []
for x in iterable:
batch.append(x)
if len(batch) == n:
yield batch
batch = []
if batch:
yield batch- correct yielding of full batches — (2)
- final partial batch handled — (1)
list(batched(range(7),3))→[[0,1,2],[3,4,5],[6]]— (1)
Question 3 (14)
(a) (4 marks)
df = pd.read_csv("sales.csv", parse_dates=["date"])
df["revenue"] = df["units"] * df["price"]parse_dates— (2), new column — (2)
(b) (5 marks)
df["month"] = df["date"].dt.to_period("M")
table = df.pivot_table(index="region", columns="month",
values="revenue", aggfunc="sum")- extract month via
.dt— (1) pivot_table/groupby(...).sum().unstack()— (3)- correct orientation (region rows, month columns) — (1)
(c) (5 marks)
- If
unitsis stored as strings,units * pricemay do string repetition or the column dtype isobject;mean()may skip/misbehave or arithmetic silently produces wrong numeric values (concatenation/coercion). Because pandas does not raise, the error is silent — (2) - Detect:
df["units"].dtypeordf["units"].apply(type).value_counts()— (1.5) - Fix:
df["units"] = pd.to_numeric(df["units"], errors="coerce")— (1.5)
Question 4 (12)
(a) (4 marks)
a == [1, 2]andb == [1, 2]— they are the same list object — (2)- Cause: the default argument
bucket=[]is evaluated once at function definition, so the same list persists across calls (mutable default argument pitfall) — (2)
(b) (3 marks)
def add_item(item, bucket=None):
if bucket is None:
bucket = []
bucket.append(item)
return bucket- sentinel
Nonedefault — (2), fresh list per call — (1)
(c) (5 marks)
class RunningStats:
def __init__(self):
self._n = 0
self._mean = 0.0
def update(self, x):
self._n += 1
self._mean += (x - self._mean) / self._n
@property
def mean(self):
return self._mean
@property
def count(self):
return self._nIncremental (Welford) mean update:
- correct incremental formula — (2)
.updatelogic — (1)mean/countas properties — (2)
Question 5 (10)
(a) (4 marks)
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt(conda equivalent accepted). Each correct step — (1) each, activation noted — (1).
(b) (3 marks)
- Remove file from history (e.g.
git filter-repo/ BFG), force-push, and rotate the leaked secret (assume compromised) — (2) - Prevention: add patterns to
.gitignore(and usegit-secrets/ pre-commit hook) — (1)
(c) (3 marks)
git fetchdownloads remote commits/refs but does not modify your working branch — (1)git pull=fetch+merge(or rebase) into current branch — (1)- Prefer
fetchto inspect/review changes before integrating, avoiding surprise merges — (1)
[
{"claim":"Squared distance via broadcasting equals identity formula",
"code":"import numpy as np\nX=np.random.rand(5,3); C=np.random.rand(4,3)\nD1=((X[:,None,:]-C[None,:,:])**2).sum(2)\nD2=(X**2).sum(1)[:,None]-2*X@C.T+(C**2).sum(1)[None,:]\nresult=bool(np.allclose(D1,D2)) and D1.shape==(5,4)"},
{"claim":"argmin axis=1 gives nearest centroid shape (M,)",
"code":"import numpy as np\nX=np.random.rand(10,3); C=np.random.rand(4,3)\nD=((X[:,None,:]-C[None,:,:])**2).sum(2)\nnn=D.argmin(1)\nresult=nn.shape==(10,) and bool((D[np.arange(10),nn]==D.min(1)).all())"},
{"claim":"word frequency dict comprehension correct",
"code":"words=['ml','ai','data','ml','ai','ml']\nfreq={w:words.count(w) for w in set(words)}\nresult=freq=={'ml':3,'ai':2,'data':1}"},
{"claim":"batched(range(7),3) yields expected batches",
"code":"def batched(it,n):\n b=[]\n for x in it:\n b.append(x)\n if len(b)==n: yield b; b=[]\n if b: yield b\nresult=list(batched(range(7),3))==[[0,1,2],[3,4,5],[6]]"},
{"claim":"Welford incremental mean equals arithmetic mean",
"code":"import numpy as np\nxs=[3.0,5.0,7.0,10.0,2.0]\nn=0; m=0.0\nfor x in xs:\n n+=1; m+=(x-m)/n\nresult=abs(m-np.mean(xs))<1e-12 and n==5"}
]