Master in Big Data — Technological Fundamentals in the Big Data World.
Parts one to three are implemented: the serial program and its two parallel versions, with multiprocessing and with threads. Part four, the written report, is not part of this repository.
| Part | Program | Parallelism |
|---|---|---|
| 1 | lab1-proteins-serial.py |
none |
| 2 | lab1-proteins-mp.py |
multiprocessing, Pool |
| 3 | lab1-proteins-th.py |
threading, Thread |
The three programs print exactly the same results — same seed, same initial
centroids, same arithmetic — and differ only in how the work is spread. That
was checked across the eighteen worker configurations measured below: all of
them printed the same k, the same cluster and the same averages.
The dataset is produced by proteins-generator.py, which writes proteins.csv
into the current directory:
python proteins-generator.py 50000 42 # development
python proteins-generator.py 2000000 42 # performance measurementsproteins.csv is not committed.
The generator is a rewrite of the one handed out with the course: same command
line, and for any <lines> <seed> the same output bytes, so the dataset behind
every measurement below is unchanged. It draws Mersenne Twister's words in bulk
instead of one random call at a time, which brings the two-million-row
dataset down from 67 s to 5.6 s.
python -m venv .venv
source .venv/bin/activate # fish: source .venv/bin/activate.fish
python -m pip install --group run # --group dev also installs the tooling
python lab1-proteins-serial.py # part one
python lab1-proteins-mp.py # part two
python lab1-proteins-th.py # part threeThe run group is the three libraries the programs import; dev adds ruff,
pyright and pre-commit, and is what the everyday
commands need.
The lab forbids absolute or relative paths to the dataset, so the programs
read proteins.csv from the working directory: run them from the directory
that holds the file.
LAB1.pdf spells the threaded program lab1-Proteins-th.py, with a capital
P. The file here is all lowercase: rename it before zipping the delivery if
the exact spelling is graded.
- Starts the clock.
- Reads
proteins.csv, keepingenzyme,hydrofoband the length of everysequence. - Standardizes
enzymeandhydrofob, so that both weigh the same in the euclidean distance. - Runs k-means for
k = 1..10and builds the elbow curve. - Picks the optimum
kas the point of the curve furthest from the straight line joining its two ends, with both axes scaled to[0, 1]first. - Clusters the proteins with that
k, and brings the centroids back to the original units to report them. - Finds the cluster holding the longest sequence — ties broken by the
hydrofobof the centroid — and averages the sequence lengths in it. - Stops the clock, prints the results and the execution time.
- Only then builds the three figures and shows them: elbow graph, clusters with their centroids, and a heat map of the centroid values.
Nothing blocks while the measured work is running, as the lab requires: no figure is created before the clock has been stopped.
k-means is implemented from scratch on top of NumPy: euclidean distance,
random initial centroids drawn from the data, and Lloyd's iteration until the
centroids move less than TOLERANCE or MAX_ITERATIONS is reached. The
random generator is always seeded with SEED = 42, so runs are reproducible.
Euclidean distance gives the wider feature the louder vote. On the
2,000,000-protein dataset hydrofob has mean 127.8 and standard deviation
53.5, against 5.50 and 2.87 for enzyme: on the raw columns a one-unit step
in enzyme is noise next to the spread of hydrofob, so the clusters are cut
along hydrofob almost on its own.
Each column is therefore centred on its mean and divided by its standard
deviation, both accumulated in double precision over the millions of rows.
The whole run happens in that space — elbow curve included, since the inertia
of a candidate k has to be comparable to the others — and the centroids are
multiplied back by the standard deviation and shifted by the mean before being
printed and plotted, so every reported value is in enzyme and hydrofob
units. The round trip is exact, and the scaling itself costs 12 ms of the 20 s
run.
This changes the answer rather than just tidying it up:
| Features | Optimum k |
Centroids (enzyme, hydrofob) |
|---|---|---|
| raw columns | 3 | (2.56, 65.12), (5.12, 128.37), (8.50, 185.23) |
| standardized | 2 | (2.98, 81.02), (7.97, 173.70) |
The generator ties the two columns together — it draws hydrofob from a range
chosen by the enzyme number — so the honest reading of the standardized result
is that the dataset holds two groups, low-enzyme/low-hydrofob and
high-enzyme/high-hydrofob, and that the third cluster the raw columns found
was a slice of the hydrofob axis rather than a group of proteins.
The centroids heat map colours the standardized value, which is what the algorithm actually compared, and annotates each cell with the real one. The elbow graph's inertia axis is in standardized units too, and says so.
Both follow the same data-parallel (SPMD) decomposition, applied to the inner loop of k-means, which is where all the time goes:
- Decomposition. The dataset is split once into one chunk per worker.
- Map. Every worker runs the same step over its own chunk: assign its points to the closest centroid and reduce them to three small partial results — points per cluster, sum of the features per cluster, and inertia.
- Reduce. The master adds the partial results up and moves the centroids. Only the centroids travel out and only the partial sums come back, so the data stays where it is for the whole run.
lab1-proteins-mp.py creates one mp.Pool(mp.cpu_count()), whose
initializer hands the dataset to every process once, and then calls
pool.starmap per iteration over CHUNKS_PER_PROCESS chunks per worker.
lab1-proteins-th.py starts os.cpu_count() threading.Threads per
iteration; they share the address space, so each one receives its chunk as a
NumPy view with no copy, and they collect their partial results in a shared
list protected by a lock. NumPy releases the GIL while it works, which is what
makes the threaded version gain anything at all.
Median of three interleaved runs on 2,000,000 proteins, SEED = 42, on an
otherwise idle i5-13500H (12 cores — 4 performance cores with SMT and 8
efficiency cores — 16 logical):
| Version | Time | Speedup | Karp-Flatt |
|---|---|---|---|
| serial | 20.20 s | 1.00 | — |
| multiprocessing | 4.72 s | 4.28 | 0.183 |
| threads | 6.00 s | 3.37 | 0.250 |
Measure on an idle machine, and compare absolute times only within one measurement session: under sustained load the CPU settles into a lower clock, and the serial program took 19.04 s as the first run of this session against 20.20 s once the machine had been busy for a few minutes. Speedups are unaffected.
Median of three runs, serial version:
| Phase | Time | Share |
|---|---|---|
| reading the CSV | 1.825 s | 8.9% |
| standardizing | 0.012 s | 0.1% |
elbow curve, k = 1..10 |
18.419 s | 90.0% |
| final k-means and reporting | 0.213 s | 1.0% |
Only the k-means steps are parallelized, so 9.0% of the work stays serial and Amdahl caps the speedup at 11.1× however many workers are added, and at 6.8× with the 16 this machine has. Multiprocessing reaches 4.28× against that ceiling of 6.8×, and the Karp-Flatt metric puts the experimentally determined serial fraction at 0.183 against the 0.090 actually measured: the gap between the two is parallel overhead — the per-iteration dispatch, and memory bandwidth, since the k-means steps move far more data than they compute.
Standardizing shortened the run rather than lengthening it. The scaling itself costs 12 ms, but Lloyd's iteration converges sooner on the scaled columns: building the elbow curve takes 142 iterations instead of 242, and the serial version measured back to back, same session, drops from 36.33 s to 20.12 s. The whole gain lands on the parallelizable part, which is why the speedups in the table above are lower than they were before the change: the 1.8 s CSV read is unchanged and now a larger share of a smaller total. The programs do the same amount of useful work in less time and scale slightly worse while doing it.
Both counts were measured rather than guessed, three runs per setting on the 2,000,000-protein dataset, interleaved and reported as medians.
One caveat for reading the spreads below: the clock drifts down as the machine heats, and a setting measured later in a sweep pays for it. The same configuration, measured at three positions of the same sweep, gave 4.72 s, 4.90 s and 5.06 s — so differences under about 7% are drift, not signal.
Processes, at four chunks per process: 7.87 s with 4, 5.95 s with 8, 5.09 s with 12, 4.90 s with 16. One process per logical core it is — the step from 12 to 16 is inside the noise, but it is free, and past 16 the machine has nothing left to give.
Chunks per process, with 16 processes: 5.75 s with one chunk each, 5.08 s with
two, 5.06 s with four, 4.97 s with six, 4.94 s with eight, 5.04 s with twelve.
One chunk each is the only setting that is clearly worse, by 12%; everything
from two to twelve is one flat optimum. The cores are not equal — four
performance cores and eight efficiency ones — so one equally sized chunk per
process makes every iteration wait for the slowest core. Cutting the data
finer lets the pool hand the extra chunks to whoever is free first, and
CHUNKS_PER_PROCESS = 4 sits in the middle of the flat part.
Threads: 6.87 s with 8, 6.19 s with 16, 6.17 s with 32, 6.98 s with 48, 7.90 s with 64. One thread per logical core again — 32 ties with 16 and there is no reason to prefer it. The threaded version cannot use the chunking trick: a chunk is a thread there, and threads are created and joined on every iteration, so extra chunks only add overhead instead of balancing the load. That, plus the parts of NumPy that keep the GIL, is why threads end up slower than processes.
.
├── .github/workflows/ci.yml
├── .gitignore
├── .pre-commit-config.yaml
├── authors.txt # one line per author, NIA;SURNAMES;NAME
├── lab1-proteins-mp.py # part two, multiprocessing
├── lab1-proteins-serial.py # part one, serial
├── lab1-proteins-th.py # part three, threads
├── proteins.csv # generated, not committed
├── proteins-generator.py # rewritten, same output bytes
├── pyproject.toml # every tool is configured here
└── README.md
The delivery expects standalone .py files, so each part is a single script
at the root of the repository rather than a package. The three programs
therefore repeat the code they have in common — reading the dataset,
standardizing, choosing the optimum k, reporting and plotting — instead of
importing it from a shared module that could not be delivered.
| Task | Command |
|---|---|
| Lint | ruff check . |
| Lint and autofix | ruff check --fix . |
| Format | ruff format . |
| Check formatting | ruff format --check . |
| Type-check | pyright |
| Run every hook | pre-commit run --all-files |
| Update hook pins | pre-commit autoupdate |
Javier Patricio Luján Romero, Elena Fernández Sedes, Miriam Alonso Cañete and Daniel Lozano Uceda