Coding Practices and Code Performance Analysis for ML
Prerequisite Knowledge
This lecture builds on the following concepts from earlier lectures. If any feel unfamiliar, review the linked notes before proceeding.
Previously Covered in This Subject
- Deterministic Nature of SE & Deterministic vs. Probabilistic Systems — covered in Lecture 1-2
- Robustness as a Quality Attribute — covered in Lecture 4
- Pipe and Filter Architectural Pattern (Modularity) — covered in Lecture 5
Coding Practices and Code Performance Analysis for ML
The way we write code in machine learning is fundamentally different from traditional software engineering. In SE, you write every rule explicitly; in ML, the algorithm learns rules from data. This lecture explores what that means for code quality — how to write good ML code, how to measure its performance, and how to find bottlenecks before they reach production.
9.1 Coding in Software Engineering vs. Machine Learning
Hook. You write if income > 50000: approve(). The machine writes model.fit(X, y). One gives you a rule you can read. The other gives you a box that works — but you cannot look inside. Why does this difference change everything about how we debug, test, and trust ML systems?
9.1.1 Core Difference: Explicit Rules vs. Learned Rules
Intuition + Analogy. Think of a cookbook versus a chef who learned by tasting. In traditional SE, you are the cookbook author — you write every instruction: "If the customer is over 60 and earns under 30,000, reject the loan." Every path through the code is one you drew by hand. In ML, you are the chef's teacher — you show the chef thousands of example dishes (data), and the chef internalizes patterns. After training, the chef can say "this loan smells like a rejection," but the reasoning is distributed across millions of tiny taste-memory weights, not written in a recipe.
The analogy breaks here: the ML "chef" is really just a mathematical function — it has no intuition, only statistical patterns. But the mapping holds: SE gives you inspectable recipes; ML gives you a trained palate you must trust (and verify).
In traditional software engineering (SE), a developer writes decision rules explicitly in code. For example, a loan approval function might contain:
if income > 50000:
approve()
Or a more detailed version: if the customer age is greater than 60 AND income is less than 30,000, then reject the loan. The developer defines every branching condition by hand. This is the fundamental nature of SE coding — rules are explicit, inspectable, and modifiable.
In machine learning, the rules are learned automatically from data. The developer writes:
model.fit(X_train, y_train)
model.predict(X_new)
The dataset contains customer details (age, income, credit score, existing loans, etc.) as features X, and labels Y (approved or not approved). The algorithm discovers patterns on its own. After training, the model might produce: "If income is high, customer age less than 60, credit score above 700, and existing loans are few, then probability of approval is 92%."
Core principle. ML code operates at a higher level of abstraction. You call library functions — the internal rules are not written by you, they are learned by the algorithm from the supplied data. Functions like model.fit are already optimized for performance and memory by the open-source community. There is no value in rewriting them from scratch.
Exam note: Understanding this difference is foundational — it shapes how you debug, how you measure quality, and how you think about code in ML systems.
9.1.2 Five Key Differences Between SE and ML Coding
| # | Aspect | Software Engineering | Machine Learning |
|---|---|---|---|
| 1 | Decision logic | Developer explicitly defines rules | Algorithm learns rules from data |
| 2 | Where logic lives | In application code and stored procedures | Encoded in the trained model's parameters (weights) |
| 3 | Primary focus | Implementing functionality correctly (payment processing, order handling) | Generalizing well to unseen data |
| 4 | Debugging scope | Code only (application code + database stored procedures) | Data (bias, variance), features (feature importance, feature engineering), training process, OR code — multiple levels |
| 5 | Rule inspectability | Easy — rules are in code, can be inspected line by line | Difficult — rules are embedded in learned parameters; this is why explainability and interpretability are critical non-functional requirements |
On generalization (point 3): In SE, functionality is supreme. In ML, the core concern is whether the model performs well on unseen production data — not just on the training or test set. A model may perform well on known data but fail on real-world inputs it has never seen. That gap is what "generalization" addresses.
Worked Example — Debugging in SE vs. ML. Imagine a loan approval system that wrongly rejects a qualified applicant.
- SE debugging: You open the code, trace the branching logic, find the line
if income < 30000: reject(), realize the threshold is too high, change it to25000, and redeploy. One file, one line, fixed. - ML debugging: You check the code —
model.fit()andmodel.predict()look fine. The code runs without errors. So you check the data: the training set had 90% rejections from a specific zip code (data bias). Then you check features: the model is using zip code as a feature, which it should not. Then you check the training process: the learning rate was too high. The fix might involve re-collecting data, dropping biased features, retraining with different hyperparameters — and you still may not know exactly which combination caused the problem.
On debugging (point 4): In SE, you fix the code, and the application works. In ML, you may fix the code and the application still does not give the right outcome. The root cause could be:
- Data problem: bias in the training data, high variance
- Features problem: you are using all features without checking feature importance; irrelevant features drag down accuracy
- Training process problem: hyperparameters, learning rate, number of epochs
- Code problem: only one of several possible root causes
On explainability (point 5): Because rules are embedded in model parameters, they are not intuitive to read line by line. You cannot trace "why this output?" through the code the way you can in SE. This is the motivation behind the entire field of explainable AI — providing rationale for model decisions (not just what happened, but why, and how the user can improve their outcome).
9.1.3 Abstraction at Two Levels in ML
ML code is abstracted at (at least) two levels:
- Python library level: Functions like
model.fit()in scikit-learn, PyTorch, or TensorFlow are pre-built, optimized, and industry-trusted. Developers worldwide use the same code in development and production. You call the function; what happens underneath is abstracted. - Model/API level: When using OpenAI APIs, Gemini APIs, or other cloud models, you call a single line of code and get a response. The entire model inference is a black box from the developer's perspective.
Real-world: This abstraction has accelerated dramatically. Building a chatbot today takes 5-6 lines of code using chat.completions.create, providing API keys, hyperparameters (temperature, etc.), and context documents. The chatbot works across PDFs, Word docs, CSVs, zip files — all handled by the underlying model. Previously, with rule-based frameworks like AIML or early Rasa, every intent and response had to be written manually.
9.1.4 The Evolution of Chatbots: Rule-Based to AI-Enabled
This example illustrates the migration from explicit rules to learned behavior — the same trajectory that defines the SE-to-ML shift:
- First generation (1956–1980s): Rule-based engines. Example: ELIZA (1956). Chatbots used AIML (an XML-based standard) where every intent → response mapping was written manually. If the user input contained certain words, execute a specific rule.
- Second generation (1980s–2010): AI-assisted. The NLP pipeline pre-processes input, understands intents automatically from natural language, handles vocabulary and grammar mistakes. Tools like early Rasa (before 2015-2016) used rules plus some AI.
- Third generation (2010–present): AI-enabled, RAG-based chatbots. Domain knowledge is embedded into a knowledge base. The AI engine, given an intent and entities from the user's input, queries the domain knowledge and generates responses. Examples: Amazon Lex, Google Dialogflow, modern Rasa (open-source Python framework).
Pitfalls — common traps when thinking about SE vs. ML code.
- Assuming ML code works like SE code. You cannot "just fix the bug" in an ML system by editing a line. The bug might be in the data, the features, or the training process — none of which live in the application code.
- Ignoring explainability until production. If your model denies a loan, the regulator will ask "why?" If you cannot answer because the rules are buried in weights, you have a compliance problem — not a code problem.
- Reinventing optimized libraries. Writing your own
fit()function or gradient descent loop when scikit-learn/PyTorch already provide battle-tested versions wastes time and often produces slower, buggier code. - Treating
Xandyas acceptable variable names in production. They are common in academic notebooks, but in team codebases they create ambiguity. Usetraining_dataandtraining_labelsinstead.
Scope & Assumptions. The SE-vs-ML distinction holds for systems where the model learns from data. It does not apply to:
- Hybrid systems that combine learned models with explicit business rules (e.g., a fraud detection pipeline where an ML model flags transactions AND a rule engine applies regulatory checks).
- Symbolic AI where rules are still hand-crafted (expert systems, theorem provers).
- Simple lookups — if your "model" is just a database query, you are back in SE territory.
Visual Intuition. Picture two flowcharts side by side. The SE flowchart (left) has clear diamond-shaped decision nodes with labels like "income > 50000?" and arrows labeled "yes" and "no." Every path is visible. The ML flowchart (right) has a single box labeled model.predict() that takes input features and outputs a decision — but inside the box is a tangled web of weighted connections with no labels. The takeaway: the ML box works, but you cannot trace a single decision through it the way you can through the SE diamond.
Recap. SE coding means you write the rules; ML coding means the data writes the rules. This single difference cascades into different debugging strategies, different quality metrics, and different expectations about inspectability.
Bridge. Now that we understand how ML code is different, the natural next question is: what makes ML code good? Section 9.2 defines five features of good code specifically for ML systems.
Real-World & Domain Connection. The SE-to-ML coding shift is most visible in modern chatbot platforms. Amazon Lex and Google Dialogflow handle millions of customer service conversations daily. Neither requires a developer to write a single if statement for intent matching. The developer provides example utterances and the platform learns the intent model. The same pattern appears in recommendation systems (Netflix, Spotify) and fraud detection (Stripe, PayPal). It also appears in medical diagnosis support tools. In all these places, the "rules" are too complex for a human to write by hand. Yet patterns exist in data.
9.2 What Makes Good Code — Five Features for ML
Hook. Your code runs. It produces the right accuracy. Then a teammate tries to add a feature — and spends three days just understanding what your code does. Was it really "good" code? Running correctly is the bare minimum. Good code survives contact with other people, changing requirements, and time.
9.2.1 Definition and Context
Intuition + Analogy. Good code is like a well-organized kitchen. You can find the spices (functions) quickly because they are labeled and grouped. The recipe book (documentation) matches what is actually in the cupboards. When a new ingredient arrives (changing requirement), you know exactly which shelf to put it on. A messy kitchen — spices scattered, unlabeled jars, recipes that don't match ingredients — is "bad code." It might still produce a meal, but at what cost in time and frustration?
Good code is not just code that runs. It must fulfill functional requirements and non-functional requirements. Additional factors include: readability, meaningful names, modularity, adherence to standards (SOLID principles, single responsibility, cohesion, coupling), performance, maintainability, configurability, and proper error handling.
Reference: Clean Code: A Handbook of Agile Software Craftsmanship by Robert C. Martin (Prentice Hall). This book covers naming, functions, comments, formatting, objects and data structures, error handling, tests, and classes. It is language-agnostic and is listed in the course references.
Five features of good code specifically for ML systems: simplicity, modularity, readability, performance, and robustness.
9.2.2 Simplicity — The DRY Principle
Definition: DRY — Don't Repeat Yourself. Every piece of knowledge or logic should exist in only one place in the codebase. Instead of copying the same code across multiple scripts, create reusable functions, classes, or pipelines and call them.
This principle comes from The Pragmatic Programmer by Hunt and Thomas. It is one of the most important concepts in writing maintainable code: if information is repeated in multiple places, one change means many updates — and you will forget at least one.
Where repetition happens in ML projects:
- Data preprocessing: code for cleaning data, handling missing values, scaling features — often repeated across experiments
- Feature engineering: same feature creation code used for multiple models (model 1, model 2, model 3)
- Model training: same training loops repeated
- Evaluation: same metrics computation repeated
- Prediction: same inference code repeated
Strategy for avoiding repetition:
- If a sequence of steps is repeated, abstract it as a function
- If a group of related functions is repeated, abstract it as a class
- If an entire pipeline (N sequential steps) is repeated, abstract it as a pipeline
Worked Example — Reading multiple CSV files.
Suppose you have three CSV files and you perform the same operations on each: read the CSV, drop some columns, and set the index.
Without DRY (repetitive):
# CSV 1
df1 = pd.read_csv("file1.csv")
df1 = df1.drop(["col_a", "col_b"], axis=1)
df1 = df1.set_index("id")
# CSV 2
df2 = pd.read_csv("file2.csv")
df2 = df2.drop(["col_a", "col_b"], axis=1)
df2 = df2.set_index("id")
# CSV 3
df3 = pd.read_csv("file3.csv")
df3 = df3.drop(["col_a", "col_b"], axis=1)
df3 = df3.set_index("id")
With DRY (using a function):
def load_and_clean(csv_file, columns_to_drop):
df = pd.read_csv(csv_file)
df = df.drop(columns_to_drop, axis=1)
df = df.set_index("id")
return df
df1 = load_and_clean("file1.csv", ["col_a", "col_b"])
df2 = load_and_clean("file2.csv", ["col_a", "col_b"])
df3 = load_and_clean("file3.csv", ["col_a", "col_b"])
What changed: 9 lines became 3 calls. If the column names change tomorrow, you update one function, not three code blocks. This is the essence of DRY.
Sense-check: If you wanted to add a fourth CSV, the non-DRY version needs 3 more copy-pasted lines. The DRY version needs one more function call.
Real-world example with AI-generated code: When using tools like ChatGPT or Claude to generate code for a data pipeline, give the entire requirement in one prompt. Include the instruction "use DRY principle." The result will be modularized code. Each preprocessing step (binning, cleaning, normalization, feature importance) becomes a separate .py file with a reusable function. A scheduler calls them sequentially. Adding a new step tomorrow only needs one new function and one registration. No duplication.
9.2.3 Modularity
Break down code into logical functions with well-defined inputs and outputs. Modular code has several advantages: it is easier to read, easier to locate where a problem comes from, and easier to reuse in your next project. It is also easier to test — each module can be tested independently.
Python supports modularity at:
- Function level: individual methods with clear signatures
- Class level: object-oriented organization of related functions
- Pipeline level: the pipe-and-filter pattern, where every filter is a modular unit
This connects directly to the pipe-and-filter architectural pattern covered earlier — every filter is a module at some level of abstraction.
Worked Example — Skeleton of a modular ML pipeline. Instead of one giant script, break your system into components:
def load_data(csv_file):
# Read and return raw data
pass
def clean_data(input_data, max_length):
# Remove noise, handle missing values
pass
def plot_data(clean_data, x_axis_limit, line_width):
# Visualize results
pass
Each function has a single responsibility, a clear input, and a clear output. If clean_data breaks, you know exactly where to look. If a teammate needs the plotting logic for another project, they can import plot_data without touching the rest.
9.2.4 Readability
Since ML applications are mostly written in Python, readability standards are essential. As PEP 8 states: "code is read much more often than it is written."
PEP 8 (Python Enhancement Proposal 8): The most popular Python style guide. Covers naming conventions, formatting, documentation, and layout. Established in 2001, it is the default standard for Python code.
Google Python Style Guide: An alternative standard, also widely used in industry.
Pylint: A static code analysis tool for Python. It checks code quality without executing the code — similar to SonarQube for Java/.NET. Key readability features:
- Checks coding style against PEP 8
- Suggests better variable and function names
- Detects unused variables and imports
- Warns about duplicate or overly complex code
- Checks for missing documentation at function and class level
Worked Example — Variable naming with Pylint.
Before (using generic variable names):
X = iris.data
Y = iris.target
m = DecisionTreeClassifier()
m.fit(X, Y)
p = m.predict(X)
Pylint flags: Variable name "X" doesn't conform to snake_case naming style (same for Y, m, p).
After (using descriptive snake_case names):
training_data = iris.data
training_labels = iris.target
model = DecisionTreeClassifier()
model.fit(training_data, training_labels)
predictions = model.predict(training_data)
What changed: X → training_data, Y → training_labels, m → model, p → predictions. A new teammate reading this code immediately knows what each variable holds — no guesswork.
Key insight: While X and Y are standard in academic ML demos (first sem, second sem), production code and team environments require descriptive names for consistency and maintainability. Snake case means lowercase with underscores between words.
9.2.5 Performance
Code must execute in an optimized manner within a well-defined time. How to measure this is the main topic of sections 9.3–9.9. Performance includes both speed (execution time) and memory usage.
Premature optimization warning. Donald Knuth's famous quote applies here: "Premature optimization is the root of all evil." Before applying any performance optimization, make sure your code works correctly first. Then measure to find the real bottlenecks — do not guess. The next sections (9.3–9.9) give you the tools to measure before you optimize.
9.2.6 Robustness
The code must handle unexpected inputs gracefully. Key aspects:
- Reproducibility: the same inputs always produce the same outputs
- Error handling: exceptions are caught and appropriate responses are returned — you make an explicit choice to crash, handle, or log
- Defensive coding: the code does not fail silently on edge cases
Pitfalls — common traps when writing ML code.
- Academic variable names in production.
X,y,m,pare fine in a Jupyter notebook but create confusion in a team codebase. Usetraining_data,training_labels,model,predictions. - Copy-paste syndrome. You write preprocessing for experiment 1, then copy it for experiment 2 with minor tweaks. Two weeks later you fix a bug in experiment 1 but forget experiment 2. DRY prevents this.
- One giant script. A 500-line script with no functions is a nightmare to debug. Break it into functions with clear inputs and outputs from day one.
- Skipping static analysis. Pylint catches unused imports, undefined variables, and style violations before the code ever runs. Running it takes seconds and saves hours of debugging.
Scope & Assumptions. The five features — simplicity, modularity, readability, performance, robustness — apply to any production ML code. They are less critical for:
- One-off analysis notebooks that will never be reused or shared
- Rapid prototypes where speed of experimentation matters more than code quality
- Tutorial/demo code where brevity helps learning
However, even prototypes often become production code. As the textbook notes: "even the code you write for a one-off demo is almost always run again or reused for another purpose."
Visual Intuition. Imagine a spectrum. On the left: a single 500-line Jupyter notebook with variables named x1, x2, temp, df3 — this is "bad code." On the right: modular .py files with descriptive names, each function testable in isolation — this is "good code." The five features are the qualities that move your code from left to right. The goal is not perfection — it is steady improvement.
Recap. Good ML code is simple (DRY), modular, readable (PEP 8 + Pylint), performant, and robust. These five features are not optional polish — they determine whether your code survives contact with teammates, changing requirements, and time.
Bridge. Among these five, performance is the one that requires specialized measurement tools. The rest of this lecture (9.3–9.9) gives you those tools — from simple timers to line-level profilers.
Real-World & Domain Connection. The DRY principle is not just academic. At companies like Stripe and Netflix, ML pipelines process millions of transactions daily. A duplicated preprocessing step that goes out of sync with the canonical version can silently corrupt model inputs for weeks before anyone notices. Teams use tools like Prefect and MLflow to enforce pipeline modularity and track exactly which version of each preprocessing function was used for every model training run. This is DRY at organizational scale — one source of truth, not three divergent copies.
9.3 Performance Analysis — Overview
Hook. Your ML pipeline takes 45 minutes to train. Is that normal? Is the bottleneck in data loading, feature engineering, or the model itself? Without measurement, you are guessing. With the right tools, you know exactly which line to fix.
9.3.1 What We Measure
Intuition + Analogy. Profiling your code is like a doctor running tests before prescribing medicine. The doctor does not guess — they measure temperature, blood pressure, and heart rate to find the problem. Similarly, you do not guess which part of your code is slow — you measure execution time and memory usage at every level, from the whole pipeline down to individual lines.
Performance in an ML system can be measured at two levels:
- End-to-end pipeline: from data collection to preprocessing to model training to evaluation — the entire ML workflow
- Individual component level: a single function, a single line of code, a single model training call
The goal is to find the bottleneck. When the system is slow, is it because of execution time (CPU-bound) or memory constraints (memory-bound)? Profiling answers this.
9.3.2 Ways to Make Python ML Code Faster
This lecture covers only the timing and memory measurement tools. The next session will cover optimization techniques:
- Choice of algorithm: different algorithms have different time/accuracy trade-offs
- Choice of data structure: list, tuple, dictionary, NumPy array, pandas DataFrame — using the right one for the task
- Using built-in functions: NumPy, pandas, scikit-learn functions are pre-optimized in C; avoid reinventing them
- Asynchronous code: for data loading, preprocessing, I/O tasks
- Parallel and distributed computing: multiple CPU cores, GPUs, distributed frameworks to accelerate training
Scope & Assumptions. The measurement tools in sections 9.4–9.8 are development-time tools. They help you find bottlenecks during coding and testing. In production, you use separate monitoring systems (MLOps, LLMOps, AgentOps) — these are designed for live environments with minimal overhead. Remove all profiling code before deploying.
Recap. Performance analysis starts with measurement, not optimization. The next five sections give you a hierarchy of tools: time (quick check) → timeit (reliable benchmark) → cProfile (which function?) → line_profiler (which line?) → memory_profiler (how much RAM?).
Bridge. We start with the simplest tool: Python's built-in time module.
Real-World & Domain Connection. At Netflix, a recommendation model that takes 200 ms instead of 100 ms to return results affects millions of users simultaneously. The difference between a snappy UI and a sluggish one is often a single slow function that a profiler can identify in minutes. The tools in this lecture are the same ones used by production ML engineers at companies like Spotify, Stripe, and Google to keep their pipelines fast.
9.4 The time Module — Single-Run Timing
Hook. How long does your model take to train? You could count "one-Mississippi, two-Mississippi" — or you could let Python's clock do it with microsecond precision. The time module is the simplest way to answer "how long?"
9.4.1 How It Works
Intuition + Analogy. Using time.time() is like using a stopwatch. You click "start" before the race and "stop" after. The difference is your elapsed time. Simple, immediate, but only one measurement — if someone tripped (background process), you would not know whether the time reflects the runner or the accident.
The time module is Python's simplest way to measure how long a piece of code takes. The pattern:
import time
start_time = time.time()
# ... code to measure ...
end_time = time.time()
execution_time = end_time - start_time
It gives the wall-clock time for one execution. This is useful for measuring a single activity — training a model, preprocessing data, or the entire script.
9.4.2 Worked Example — Iris Dataset with Decision Tree
Dataset: Iris (built into scikit-learn). Contains 150 samples, 4 features (sepal length, sepal width, petal length, petal width — all in centimeters), and 3 classes (setosa, versicolor, virginica). It is a multi-class classification problem.
import time
from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier
# Load dataset
iris = load_iris()
X, y = iris.data, iris.target
# Time the training
start_time = time.time()
model = DecisionTreeClassifier()
model.fit(X, y)
end_time = time.time()
training_time = end_time - start_time
print(f"Training time: {training_time:.6f} seconds")
Result: Approximately 0.0075 seconds (7.5 milliseconds — varies by machine).
Sense-check: The Iris dataset has only 150 samples and 4 features. A decision tree on this tiny dataset should train almost instantly. 7.5 ms confirms this operation runs extremely fast. No optimization is needed.
9.4.3 Limitations of the time Module
- Runs the code only once — susceptible to interference from background processes, CPU load fluctuations, and caching effects
- Does not give a statistical picture (no average, no standard deviation)
- Accuracy is lower than methods that average multiple runs — a single measurement might be an outlier
Recap. time.time() is the "quick spot-check" tool. Use it to get a rough sense of how long something takes. For reliable benchmarking, use timeit (section 9.5).
Bridge. The timeit module solves time's biggest weakness by running code many times and reporting statistics.
Real-World & Domain Connection. time.time() is commonly used in logging statements within production ML pipelines. Engineers sprinkle start = time.time() and elapsed = time.time() - start throughout their code to timestamp each stage of a pipeline (data load, preprocessing, inference). These timestamps feed into monitoring dashboards (Grafana, Datadog) that track pipeline health over days and weeks.
9.5 The timeit Module — Statistical Timing
Hook. You measured your training function once and got 7.5 ms. But was that a lucky run? An unlucky one? What if you need to guarantee that 99% of runs finish under 10 ms? A single stopwatch reading cannot answer that. You need statistics.
9.5.1 Why timeit Is Better
Intuition + Analogy. time is like measuring your commute once. timeit is like measuring it every day for a month and computing the average and variation. If your commute averages 25 minutes with a standard deviation of 2 minutes, you can confidently leave 30 minutes before a meeting. If the standard deviation is 15 minutes, you need a much bigger buffer — the route is unreliable.
The timeit module runs code multiple times and reports the average execution time along with the standard deviation. This gives a more reliable measurement because:
- Background process interference is averaged out
- You get a range (mean ± std dev), not just a single number
- Small standard deviation → consistent, stable performance
- Large standard deviation → inconsistent performance (possible CPU load, caching, or other issues to investigate)
9.5.2 Parameters: number and repeat
number: How many times the function is called in one "run." Example:number=100means the training function is called 100 times back-to-back.repeat: How many times the whole benchmark is repeated. Example:repeat=10means the 100-call experiment is done 10 separate times, yielding 10 averages.
The final result is the mean and standard deviation of those 10 averages.
9.5.3 Worked Example — Iris Dataset with Decision Tree
import timeit
import statistics
from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier
def train_model():
iris = load_iris()
X, y = iris.data, iris.target
model = DecisionTreeClassifier()
model.fit(X, y)
# Benchmark: 100 calls per run, repeated 10 times
times = timeit.repeat(train_model, number=100, repeat=10)
# Average time per single call
avg_times = [t / 100 for t in times]
mean_time = statistics.mean(avg_times)
std_dev = statistics.stdev(avg_times)
print(f"Average training time: {mean_time:.6f} seconds")
print(f"Standard deviation: {std_dev:.6f} seconds")
Sample result:
- Average training time: 0.001812 seconds (1.812 milliseconds)
- Standard deviation: 0.000041 seconds (0.041 milliseconds)
Interpretation: Most executions took between 1.771 ms and 1.853 ms (mean ± 1 std dev). The tiny standard deviation (only 2.3% of the mean) means the performance is extremely consistent and stable.
Sense-check: If the standard deviation were very large — say, 0.5 ms on a 1.8 ms average — you would investigate why: heavy CPU load, background processes, thermal throttling, or caching effects. A small std dev means your benchmark is trustworthy.
9.5.4 Using timeit in Google Colab / Jupyter Notebook
In Google Colab or any Jupyter Notebook (.ipynb), put %%timeit at the top of a cell:
%%timeit
# any code here
model = DecisionTreeClassifier()
model.fit(X, y)
This automatically runs the cell multiple times and reports the mean and standard deviation. Example output: 12.1 ms ± 3.64 ms per loop (mean ± std. dev. of 7 runs, 100 loops each).
Pitfall — %%timeit vs. timeit.repeat(). The cell magic %%timeit automatically chooses the number of runs to fit within about 2 seconds. For very fast code (like our Iris example), it may run thousands of loops. For slow code, it may run only a few. Use timeit.repeat() with explicit number and repeat when you need full control.
9.5.5 time vs. timeit — Summary
| Aspect | time module | timeit module |
|---|---|---|
| Runs | Single execution | Multiple executions (configurable) |
| Output | One time value | Mean and standard deviation |
| Accuracy | Lower (single sample) | Higher (statistical) |
| Use case | Quick spot-check | Reliable benchmarking |
| Colab syntax | import time | %%timeit cell magic |
Recap. timeit gives you statistical confidence — mean and standard deviation over many runs. Use it when you need reliable benchmarks, not just rough estimates.
Bridge. timeit tells you how long your code takes. But it does not tell you which function is the bottleneck. For that, you need a profiler — starting with cProfile (section 9.6).
Real-World & Domain Connection. Reliable benchmarking with timeit matters in ML model serving. When Stripe's fraud detection model serves predictions at 10,000 requests per second, a 1 ms slowdown per request means 10 seconds of cumulative delay per second. Engineers use timeit-style statistical benchmarking to ensure that model inference stays within strict latency SLAs (Service Level Agreements), typically measured at p99 (99th percentile) — which requires statistical distributions, not single measurements.
9.6 The cProfile Module — Function-Level Profiling
Hook. Your script has 20 functions. timeit tells you the whole thing takes 2.5 seconds — but which of the 20 functions is the culprit? You could time each one individually, or you could let a profiler answer the question in one run.
9.6.1 What Is a Profiler?
Intuition + Analogy. A profiler is like a security camera system for your code. A stopwatch at the finish line only gives total time. Cameras (profilers) record what happens inside every room (function). They track who entered (ncalls) and how long they stayed (tottime). They also show the total time, including everyone they called (cumtime). When something is slow, you review the footage. You see exactly which room caused the delay.
A profiler gives deeper performance insights than a timer. It tells you not just the total time, but which functions inside your code are fast and which are slow. The concept exists across domains — there are SQL profilers for databases, Java profilers, .NET profilers, and for Python: cProfile.
cProfile is called "C" profiler because it is a Python wrapper around a C-language profiling library (C is one of the fastest languages for execution). It is one of the most widely used profilers for Python applications.
9.6.2 What cProfile Shows
For a given function, cProfile reports:
- ncalls: number of times each sub-function was called
- tottime: total time spent in the function itself (excluding sub-calls)
- cumtime: cumulative time spent in the function and all functions it called
- percall: time per call
- filename:lineno(function): where each function is defined
9.6.3 Worked Example — Iris Dataset with Random Forest
This example uses a function that performs 5 activities: (1) load the dataset, (2) split the dataset, (3) create a model, (4) train the model, (5) evaluate the model.
import cProfile
import pstats
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
def train_model():
# 1. Load
iris = load_iris()
X, y = iris.data, iris.target
# 2. Split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42
)
# 3. Create model
model = RandomForestClassifier(n_estimators=5, random_state=42)
# 4. Train
model.fit(X_train, y_train)
# 5. Evaluate
accuracy = model.score(X_test, y_test)
return accuracy
# Profile
profiler = cProfile.Profile()
profiler.enable()
train_model()
profiler.disable()
# Print stats
stats = pstats.Stats(profiler)
stats.sort_stats('cumulative')
stats.print_stats()
Sample output interpretation (approximate values):
| Function | ncalls | tottime (s) | cumtime (s) |
|---|---|---|---|
train_model | 1 | 0.000 | 0.003 |
load_iris | 1 | 0.001 | 0.001 |
train_test_split | 1 | 0.000 | 0.002 |
fit (RandomForest) | 1 | 0.000 | 0.002 |
build_tree (parallel) | 5 | 0.002 | 0.002 |
score | 1 | 0.000 | 0.003 |
| Others (internal helpers) | ~12 | 0.000 | 0.003 |
Key observations:
- Total function calls: 12,156 — many more than the 5 visible activities because scikit-learn internally calls thousands of sub-functions. This is the abstraction at work.
ncallsforbuild_treeis 5 becausen_estimators=5was set — it builds 5 decision trees.ncallsfortrain_modelis 1 because the top-level function is called only once.cumtimeis the cumulative: thefitfunction's cumtime includes the time spent insidebuild_treeand other internal calls.
Sense-check: The total time is ~3 ms for training a Random Forest on 150 samples. That is fast — no bottleneck here. For a real bottleneck, you would look for functions where tottime or cumtime dominate the total.
9.6.4 When to Use cProfile
Use cProfile when you want to know which function is the bottleneck. It gives a high-level overview. If a function has a disproportionately high cumulative time, drill deeper into that function with line-level profiling (line_profiler, section 9.7).
Pitfall — interpreting cProfile output. The profiler shows thousands of internal function calls from libraries like scikit-learn and NumPy. Do not get lost in these. Focus on the functions you wrote — sort by cumtime and look for your own function names. The internal calls are usually already optimized.
Recap. cProfile answers "which function is slow?" by reporting ncalls, tottime, and cumtime for every function in your code — including library internals.
Bridge. Once cProfile identifies the slow function, line_profiler (section 9.7) zooms in to show which line inside that function is the problem.
Real-World & Domain Connection. cProfile is the go-to first-pass profiler at companies like Instagram and Dropbox when debugging slow Python services. Engineers attach it to a running production process (with caution), capture a few seconds of profiling data, and immediately see which function is eating CPU cycles. Combined with visualization tools like SnakeViz (which renders cProfile output as interactive flame graphs), it becomes a powerful debugging tool for distributed systems.
9.7 The line_profiler Module — Line-Level Profiling
Hook. cProfile told you train_model() is the bottleneck. But train_model() has 7 lines — loading data, splitting, creating the model, fitting, scoring, printing. Which of those 7 lines is the real problem? You need a microscope, not a camera.
9.7.1 Installation and Purpose
Intuition + Analogy. cProfile is the building's floor plan — it shows which room (function) the fire is in. line_profiler is the thermal camera pointed at that one room — it shows you exactly which object (line of code) is burning hottest. The floor plan gets you to the right room; the thermal camera tells you where to aim the fire extinguisher.
Install via pip install line_profiler. It tells you how much time each individual line of a function takes, and what percentage of the total execution time each line consumes.
9.7.2 Worked Example — Same Iris + Random Forest Code
from line_profiler import LineProfiler
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
def train_model():
iris = load_iris() # line 1
X, y = iris.data, iris.target # line 2
X_train, X_test, y_train, y_test = (
train_test_split(X, y, test_size=0.3, random_state=42)
) # line 3
model = RandomForestClassifier(
n_estimators=5, random_state=42
) # line 4
model.fit(X_train, y_train) # line 5
accuracy = model.score(X_test, y_test) # line 6
print(f"Accuracy: {accuracy:.4f}") # line 7
profiler = LineProfiler()
profiler.add_function(train_model)
profiler.run('train_model()')
profiler.print_stats()
Sample output:
| Line | Code | Time (s) | % of Total |
|---|---|---|---|
| 1 | iris = load_iris() | 0.0004 | 19.5% |
| 3 | train_test_split(...) | 0.0003 | 14.3% |
| 5 | model.fit(...) | 0.0013 | 62.7% |
| 6 | model.score(...) | 0.0001 | 4.8% |
| 7 | print(...) | 0.00003 | 1.3% |
Key insight: model.fit is the performance bottleneck — it consumes 62.7% of the total execution time. Nearly two-thirds of all time is spent in one line. This is the line you would focus on if you needed to optimize.
Sense-check: This makes intuitive sense. model.fit is where the Random Forest builds 5 decision trees, each requiring splits on multiple features. Loading data (19.5%) and splitting (14.3%) are secondary. The print statement (1.3%) is negligible — optimizing it would be a waste of time.
9.7.3 cProfile vs. line_profiler — When to Use Which
| Tool | Granularity | Shows | Best for |
|---|---|---|---|
cProfile | Function level | Which function is the bottleneck | Getting a high-level overview first |
line_profiler | Line level | Which line inside the function is the bottleneck | Deep-diving after cProfile identifies a slow function |
Recommended workflow:
- Start with
cProfileto identify the slowest function(s) - Then use
line_profileron that specific function to find the exact slow line
In the Iris example, the two tools gave similar output. That is because every function was only one line long. In larger applications, a single function might have 50 lines. The difference between function-level and line-level profiling then becomes clear. line_profiler pinpoints the 2 lines responsible for 80% of the time.
Pitfall — profiling is not free. line_profiler adds significant overhead because it measures every line. Do not leave it running in production. It is purely a development-time diagnostic tool. The overhead can make your code 10–100× slower while profiling is active.
Recap. line_profiler answers "which line is slow?" with per-line timing and percentage breakdown. Use it after cProfile identifies the slow function.
Bridge. Time is only half the story. The other half is memory — a function might be fast but consume so much RAM that it crashes your server. Section 9.8 covers memory profiling.
Real-World & Domain Connection. line_profiler is vital when debugging preprocessing pipelines. At Spotify, a feature engineering function normalizes audio features for millions of tracks. It might have 30 lines. line_profiler reveals that one list comprehension on line 17 causes 80% of the runtime. The fix is often one line: swap a Python loop for a vectorized NumPy operation. Without line_profiler, developers guess which line to optimize for hours.
9.8 Memory Profiling — Measuring Memory Usage
Hook. Your code finishes in 2 seconds — fast enough. But it consumed 8 GB of RAM, and your deployment container only has 4 GB. The code crashes in production. Time is not the only resource that matters. Memory can be the silent killer.
9.8.1 Two Libraries for Memory Profiling
Intuition + Analogy. Memory profiling is like tracking how much desk space each task consumes. You have a fixed desk (RAM). If one task spreads papers across 80% of the desk, the next task has nowhere to work and everything grinds to a halt. Memory profiling tells you which line of code is the desk hog — before it crashes the whole workspace.
| Library | Platform | Install |
|---|---|---|
memory_profiler | Windows | pip install memory_profiler |
memray | Mac and Linux | pip install memray |
The concept is the same across both — measure how much memory each line of code consumes. memray (developed by Bloomberg) also produces flamegraph visualizations for memory usage.
9.8.2 Memory Units
Memory profiler reports values in MiB (mebibytes). Conversion:
So 118 MiB ≈ approximately 124 MB.
The distinction matters because computers use base-2 (MiB = bytes) while storage marketing uses base-10 (MB = bytes). Memory profilers are honest — they use MiB.
9.8.3 Worked Example — Same Iris + Random Forest Code
from memory_profiler import profile
@profile
def train_model():
iris = load_iris()
X, y = iris.data, iris.target
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42
)
model = RandomForestClassifier(n_estimators=5, random_state=42)
model.fit(X_train, y_train)
accuracy = model.score(X_test, y_test)
print(f"Accuracy: {accuracy:.4f}")
train_model()
Sample output (approximate):
| Line | Code | Memory Usage (MiB) | Increment (MiB) | Occurrences |
|---|---|---|---|---|
| 1 | def train_model(): | 118.0 | — | 1 |
| 2 | iris = load_iris() | 118.1 | +0.1 | 1 |
| 3 | X, y = iris.data, iris.target | 118.1 | +0.0 | 1 |
| 4 | train_test_split(...) | 118.2 | +0.1 | 1 |
| 5 | model = RandomForestClassifier(...) | 118.2 | +0.0 | 1 |
| 6 | model.fit(...) | 118.6 | +0.4 | 1 |
| 7 | model.score(...) | 118.6 | +0.0 | 1 |
| 8 | print(...) | 118.6 | +0.0 | 1 |
Total memory used: 118.6 MiB to execute the entire train_model function.
How to read the columns:
- Memory Usage: cumulative memory at that line
- Increment: how much additional memory this line consumed
- Occurrences: how many times the line was executed (1 for each line in this simple example)
The biggest memory jump is model.fit (+0.4 MiB), consistent with it being the most computationally intensive step.
Sense-check: The Iris dataset is tiny (150 rows × 4 features). 118 MiB seems high for such a small dataset — but this includes the Python interpreter overhead and all loaded libraries (scikit-learn, NumPy, etc.). The increments are what matter: they show that model.fit uses 4× more memory than load_iris, confirming training is the dominant cost.
9.8.4 Why Memory Profiling Matters
Memory profiling is not just about curiosity — it feeds into algorithm and data structure selection. In production:
- If Random Forest takes significantly more memory than SVM for your dataset size
- And memory is a constraint in your deployment environment
- Then you may choose SVM even if Random Forest has slightly higher accuracy
Accuracy is not the only criterion for choosing an algorithm. Code performance (time) and memory consumption are equally important considerations in production ML systems.
9.8.5 Limitations in ML Code
Sometimes you cannot optimize certain ML lines. For example, model.fit is a library call — you cannot rewrite it. However, you CAN:
- Choose a different algorithm (SVM vs. Random Forest vs. XGBoost)
- Choose a more efficient data structure (NumPy arrays instead of Python lists)
- Use built-in functions rather than custom loops
- Add asynchronous processing where applicable
- Use GPU-accelerated training for larger datasets
The profiling data tells you what to change, even when you cannot change the internal implementation.
Pitfall — Python interpreter overhead. The baseline memory usage (118 MiB in our example) includes Python itself and all imported libraries. Do not panic at the absolute number — focus on the increments each line adds. A line with +0.0 MiB increment is not your problem even if the total is high.
Recap. memory_profiler (Windows) and memray (Mac/Linux) show you exactly which lines consume the most memory. Use this data to choose algorithms and data structures that fit your deployment environment's constraints.
Bridge. We now have five profiling tools. Section 9.9 summarizes them all and gives you a recommended workflow for putting them together.
Real-World & Domain Connection. Memory profiling is critical in edge deployment scenarios. When deploying ML models to mobile phones or IoT devices with 512 MB of RAM, every megabyte counts. TensorFlow Lite and ONNX Runtime use memory profiling data to decide which model architecture fits on-device. At Bloomberg, memray was developed specifically because their financial data processing pipelines were hitting memory limits on production servers — and existing Python memory profilers were not detailed enough to find the leaks.
9.9 Summary of Profiling Tools
9.9.1 Tool Comparison
| Tool | What it measures | Granularity | Platforms | Key output |
|---|---|---|---|---|
time | Wall-clock time | Activity/block | All | Single execution time in seconds |
timeit | Statistical timing | Activity/block | All (VS Code); %%timeit for Colab | Mean ± std dev over multiple runs |
cProfile | Function-level time | Per function call | All | ncalls, tottime, cumtime per function |
line_profiler | Line-level time | Per line of code | All (pip install) | Time and % total per line |
memory_profiler | Memory usage | Per line of code | Windows (pip install) | MiB usage and increment per line |
memray | Memory usage | Per line of code | Mac / Linux | Similar to memory_profiler; adds flamegraph |
Comparison — when to use which. The six tools form a hierarchy from simplest to most detailed. Each answers a progressively finer question:
time: "How long did that take?" (rough)timeit: "How long does it usually take?" (reliable)cProfile: "Which function is slow?" (broad)line_profiler: "Which line is slow?" (narrow)memory_profiler/memray: "How much memory does each line use?" (orthogonal dimension)
9.9.2 Recommended Profiling Workflow
- Start broad: Use
timeitto get a reliable average timing for the whole function/pipeline - Identify the bottleneck function: Use
cProfileto see which function takes the most cumulative time - Zoom in on the bottleneck: Use
line_profileron the slow function to find the exact slow line - Check memory: Use
memory_profiler(ormemray) to see if memory is the constraint - Optimize: Based on findings, choose a better algorithm, data structure, or approach
- Remove profiling code before production: All profiling code is for development only. Production monitoring uses separate metrics systems (MLOps, LLMOps, AgentOps)
Pitfall — profiling the wrong thing. Always profile on realistic data. Profiling on a 150-row Iris dataset tells you nothing about performance on your 10-million-row production dataset. Use a representative sample of production data for meaningful results.
Recap. The profiling hierarchy is: time (rough) → timeit (statistical) → cProfile (function-level) → line_profiler (line-level) → memory_profiler (memory per line). Use them in order — broad to narrow — and always measure before you optimize.
9.10 Student Questions and Answers
Q: Are there scenarios in ML where rule-based engines are used, and can models output the rules they learned?
A: Rule-based engines were the primary approach before modern ML, particularly in chatbots. The evolution went: first generation (1956–1980s) used rule-based engines like ELIZA and AIML where every intent→response mapping was handwritten. Second generation (1980s–2010) added AI to automatically understand intents from natural language. Third generation (today) uses RAG-based AI chatbots (Amazon Lex, Google Dialogflow, modern Rasa) where domain knowledge is embedded and the AI engine answers questions without explicit rules. Decision trees do produce explicit branching rules (e.g., income < 50000 → branch left), but these rules are embedded within the model code, not visibly written out. This is why explainability and interpretability are important — they surface the rationale for decisions (why the loan was rejected, what the user can do to improve their chances).
Q: When AI agents generate code, do they automatically follow coding standards like DRY and PEP 8? Do we need additional validation?
A: Modern code-generation models follow coding standards quite well by default — they have been trained on vast amounts of well-written code. PEP 8 is the default for Python generation. However:
- If you split your requirement into multiple small prompts, you may get duplicated code — give the full requirement in one shot and explicitly mention DRY
- You can provide your organization's specific coding standards in the prompt
- Follow the same validation process you use for human-written code: if your team uses SonarQube for static analysis, run AI-generated code through it too
- For the first few iterations, manual review is recommended — read the code line by line
- Modern models generate very good code; with a detailed 7-8 line problem statement, they can produce user stories, system architecture, code, and test cases end-to-end
Q: Can these profiling tools detect memory leaks?
A: The memory_profiler library shows memory usage and increment per line. Whether it can specifically detect memory leaks requires further exploration. The library gives you the data — you would need to look for lines where memory grows unexpectedly across repeated calls. For production-grade memory leak detection, tools like memray (with its flamegraph reports) or objgraph are better suited.
Q: Once you have memory statistics, what do you actually do with them? (Several students asked this.)
A: The goal is optimization through informed choice. You use the data to make decisions: if Random Forest takes more time/memory than SVM but both have similar accuracy, choose the faster/lighter one for production. In ML, accuracy is not the only factor — execution time and memory consumption are equally important quality attributes. However, for library functions like model.fit, you cannot modify the internal code — you can only choose a different algorithm or optimize the surrounding pipeline (asynchronous processing, GPU acceleration, data structure choices).
Q: Should we use both line_profiler and cProfile, or just one?
A: Use cProfile first for a high-level overview — it shows which function is the bottleneck. Then use line_profiler on that specific slow function to find the exact slow line. This is the recommended hierarchy: broad → narrow. The tools are complementary, not alternatives.
Q: Are these profiling techniques only for development? Do we remove them before production?
A: Yes, these are development-time tools only. Remove all profiling code before deploying to production. Profilers add significant overhead (10–100× slowdown for line_profiler). In production, you use separate monitoring systems: MLOps metrics, LLMOps metrics, AgentOps metrics — these are designed for live environments with minimal overhead and do not use the same profiling code.
Q: For Assignment 1 — what format should the submission be?
A: Submit as a single PDF file per group. Include: code (for ML components), screenshots of execution output, and any GR for ML diagrams. Diagrams can be drawn on paper and photographed, or created with any tool (Google Draw, Paint, StarUML, Visual Paradigm) — just ensure the correct notation symbols (ellipse, rectangle, etc.) are used. Non-ML components do not require code, only screenshots. The console/terminal output is acceptable — no need for the BITS virtual lab; local execution is fine for this offering.
Q: For the assignment question about implementing 2 architectural patterns — should we build a prototype?
A: Apply the patterns demonstrated in class (monolith, microservices, CQRS, RAG, etc.) to your own problem statement and domain. The code patterns are already shown — adapt them to your scenario. You are not building a full production system; you are demonstrating that you understand the pattern and can apply it to your domain.
9.11 Key Industry Applications and Tools Mentioned
| Tool / System | Context |
|---|---|
| SonarQube / SonarCloud | Static code analysis for Java/.NET — equivalent to Pylint for Python. Catches bugs, code smells, and security vulnerabilities before code reaches production. |
| AI Code Generation (Copilot, Claude, Codex) | AI code generation — follows PEP 8 by default, produces modular code. Best results come from detailed prompts with explicit standards (DRY, PEP 8, naming conventions). |
| Google Colab / Jupyter Notebook | Use %%timeit cell magic for quick performance benchmarking; %%prun for cProfile; %lprun for line_profiler. |
| Prefect | Workflow orchestration tool for DataOps — uses Python functions with DRY principle. Each pipeline step is a decorated Python function. |
| Rasa | Open-source Python chatbot framework — evolved from rule-based (AIML-era) to AI-enabled. Modern Rasa uses transformer models for intent classification. |
| Amazon Lex, Google Dialogflow | Cloud-based chatbot platforms using AI-driven intent understanding. No rule-writing required — provide example utterances and the platform learns. |
| AIML | XML-based rule engine for first-generation chatbots (e.g., ELIZA, 1956). Every intent→response pair was handwritten. |
| SnakeViz | Browser-based visualizer for cProfile output — renders interactive flame graphs and icicle charts from .prof files. |
| MLflow | Model registry — tracks experiments, models, and deployments. Integrates with the profiling workflow by logging timing metrics alongside model artifacts. |
| MEAN / MERN stacks | Full-stack SE frameworks (MongoDB, Express, Angular/React, Node.js) — contrast with ML stacks where the "backend" includes model serving infrastructure. |
| Flutter, React Native | Cross-platform mobile development frameworks — relevant when deploying ML models to mobile devices with memory constraints. |
| WebLogic | Legacy reporting tool — example of long-lived legacy systems where code readability and documentation are critical for maintenance. |
9.12 Exam and Assignment Guidance
Exam note: The midterm exam drew questions directly from class materials — nothing from outside. For the comprehensive exam, studying the provided PPTs, textbook chapters, and these notes is sufficient. Focus on understanding the why behind each profiling tool, not just the syntax.
- Assignment 1: Due date extended to July 7. Submit as a single PDF per group. Include code for ML components, screenshots of execution, and GR for ML diagrams (any tool or hand-drawn, with correct notation — ellipse, rectangle, etc.).
- Assignment 2 (upcoming): Will involve implementing the profiling techniques covered in this lecture —
time,timeit,cProfile,line_profiler,memory_profiler— applied to your own domain, dataset, and problem statement. Expect to profile your ML pipeline, identify bottlenecks, and justify optimization decisions based on the profiling data.
Exam note — likely question types:
- Compare SE coding vs. ML coding (the five differences table)
- Name and explain the five features of good code (simplicity, modularity, readability, performance, robustness)
- Given a code snippet, identify which DRY violations exist and rewrite it
- Describe the profiling hierarchy:
time→timeit→cProfile→line_profiler→memory_profiler - Interpret profiling output — given a
cProfileorline_profilertable, identify the bottleneck - Explain when to use each profiling tool and why you cannot use them in production
- Textbook reference: The content of sessions 9, 10, and 11 is primarily from Textbook 2 (uploaded on the course platform) — Chapters 1 (What Is Good Code) and 2 (Analyzing Code Performance).
- Reference book: Clean Code: A Handbook of Agile Software Craftsmanship by Robert C. Martin (Prentice Hall) — soft copy available in course references. Pay special attention to chapters on naming, functions, and comments.
9.13 Key Takeaways
- SE coding = explicit rules; ML coding = learned patterns from data. This fundamental difference affects debugging, quality measurement, and system design. In SE, you fix the code. In ML, the bug could be in the data, features, training process, OR code.
- Five features of good ML code: simplicity (DRY — Don't Repeat Yourself), modularity (break into logical functions), readability (PEP 8 / Pylint), performance (time + memory), and robustness (error handling, reproducibility).
- DRY principle: Abstract repeated logic into functions, classes, or pipelines. Data preprocessing, feature engineering, model training, and evaluation are common areas where repetition occurs. One canonical source of truth prevents divergent copies.
- Pylint is a static code analyzer — checks style against PEP 8, suggests better names, flags unused imports and duplicate code, and verifies documentation — all without running the code.
- Profiling hierarchy:
time(single rough measurement) →timeit(statistical mean ± std dev) →cProfile(which function is slow?) →line_profiler(which line in that function?) →memory_profiler(how much RAM per line?). timeitis more reliable thantimebecause it averages multiple runs and reports standard deviation. Small std dev = consistent performance; large std dev = investigate.model.fitis typically the bottleneck in ML code — and it is a library call you cannot rewrite. But you CAN choose a different algorithm, optimize data structures, use GPU acceleration, or restructure the surrounding pipeline.- Remove all profiling code before production. Profilers add 10–100× overhead. Use MLOps/LLMOps/AgentOps monitoring for live environments with minimal performance impact.
- Accuracy is not the only selection criterion for ML algorithms — execution time and memory consumption matter equally in production. A slightly less accurate model that fits in your deployment budget is better than an accurate model that crashes.
- Code generated by modern AI tools generally follows standards well — validate it with the same processes you use for human-written code (Pylint, SonarQube, manual review). Give complete requirements in one prompt and explicitly mention DRY for best results.
SEML Lecture 9 notes · Coding Practices and Code Performance Analysis for ML
Sections Breakdown
Explores the fundamental differences between writing explicit rules in SE and letting algorithms learn from data in ML, covering debugging, explainability, and the evolution of chatbots from rule-based to AI-enabled.
Defines simplicity (DRY), modularity, readability (PEP 8, Pylint), performance, and robustness as the five essential qualities of production-ready ML code.
Introduces the measurement-first approach to performance optimization and surveys the five ways to make Python ML code faster.
Covers Python's simplest stopwatch-style timer with a worked Iris dataset example and its limitations for reliable benchmarking.
Explains how timeit provides statistical confidence with mean and standard deviation over multiple runs, including Colab/Jupyter magic commands.
Dives into function-level profiling showing ncalls, tottime, and cumtime with a worked Random Forest example on the Iris dataset.
Zooms to line-level granularity, revealing that model.fit consumes 62.7% of runtime, with best-practice workflow using cProfile first.
Covers memory_profiler (Windows) and memray (Mac/Linux), explaining MiB units, per-line increments, and how profiling data guides algorithm selection.
Compares all six tools in a hierarchy from time (rough) to memory_profiler (per-line memory) with the recommended profiling workflow.
Addresses common student questions about rule-based engines, AI code generation standards, memory leak detection, and when to use each profiler.
Catalogs tools like SonarQube, Prefect, Rasa, Amazon Lex, SnakeViz, MLflow, and deployment platforms referenced throughout the lecture.
Provides exam tips, likely question types, assignment format requirements, and textbook references for focused study.
Distills ten critical points covering SE vs. ML coding differences, five code quality features, profiling hierarchy, and production best practices.
Exam Revision Notes
Below is the distilled, exam-ready core of this lecture. Every entry is built from the full textbook notes above. Use this section for rapid review — but if something doesn't make sense, go back to the full explanation in the main content.
SE Coding vs. ML Coding
Must-know: In SE, the developer writes explicit rules (if-else); in ML, the algorithm learns patterns from data. This affects debugging scope, rule inspectability, and how quality is measured. The five key differences table (decision logic, where logic lives, primary focus, debugging scope, rule inspectability) is a frequent exam question.
Top pitfall: Assuming ML code works like SE code — fixing the code may not fix the problem if the bug is in data bias, feature selection, or training hyperparameters.
Self-check: You fix a bug in an ML system's code. The output still gives wrong predictions. Where else should you investigate?
Connects to: Explainability and Interpretability, Debugging Workflow, Data Bias vs. Code Bugs
Five Features of Good ML Code
Must-know: Simplicity (DRY — Don't Repeat Yourself), Modularity, Readability, Performance, and Robustness. Modularity means logical functions with clear inputs and outputs.
Top pitfall: Using X, y, m, p as variable names in production — acceptable in notebooks but confusing in team codebases. Always use descriptive snake_case names.
Self-check: A preprocessing function is used identically in three different experiment scripts. Which principle is violated, and how do you fix it?
Connects to: DRY Principle, PEP 8 and Pylint, Premature Optimization Warning
The Profiling Tool Hierarchy
Must-know: The recommended workflow: time (rough single run) → timeit (statistical mean ± std dev) → cProfile (which function?) → line_profiler (which line?) → memory_profiler (how much RAM?). Always measure before you optimize. Profilers are development-time tools — remove them before production; use MLOps monitoring instead.
Top pitfall: Profiling on a tiny dataset (150-row Iris) and extrapolating to a production 10-million-row dataset. Always profile on representative data.
Self-check: Your code takes 45 minutes to run. You run cProfile. Which column do you sort by first to find the bottleneck?
Connects to: time Module, timeit Module, cProfile, line_profiler, memory_profiler, model.fit as Bottleneck
timeit Module
Must-know: timeit runs code multiple times and reports mean and standard deviation. number controls calls per run; repeat controls the number of separate experiments. A small std dev means consistent, trustworthy performance. In Colab/Jupyter, use %%timeit cell magic for quick benchmarks.
Top pitfall: Using %%timeit cell magic without understanding that it auto-chooses the number of runs. Use timeit.repeat() with explicit number and repeat for full control.
Self-check: Your timeit benchmark shows mean = 5 ms, std dev = 4 ms. Is this benchmark trustworthy?
Connects to: time Module, Statistical Benchmarking, Google Colab/Jupyter Notebook
cProfile — Function-Level Profiling
Must-know: cProfile reports ncalls (number of calls), tottime (time in the function excluding sub-calls), and cumtime (time in the function plus all functions it called). Sort by cumtime to find the bottleneck function. Focus on your own functions, not thousands of library internals.
Top pitfall: Getting lost in thousands of internal scikit-learn/NumPy calls. Focus on your own function names when interpreting cProfile output.
Self-check: In a Random Forest with n_estimators=10, what will the ncalls for build_tree be?
Connects to: line_profiler, Profiling Workflow, SnakeViz Visualization
line_profiler — Line-Level Profiling
Must-know: line_profiler shows per-line timing and percentage of total. In the Iris example, model.fit consumed 62.7% of runtime. Use cProfile first for overview, then line_profiler to pinpoint the exact slow line. Profilers add 10–100× overhead — never deploy them.
Top pitfall: Leaving line_profiler active in production — it adds 10–100× overhead and will dramatically slow down your running system.
Self-check: line_profiler shows a print statement taking 1.3% of runtime and model.fit taking 62.7%. Which line should you optimize?
Connects to: cProfile, model.fit as Bottleneck, Production Monitoring (MLOps/LLMOps)
memory_profiler — Measuring RAM Usage
Must-know: Reports memory in MiB (1 MiB = 1.05 MB). Focus on increment per line, not absolute totals which include Python interpreter overhead. Use profiling data to choose algorithms — accuracy is not the only criterion; memory and execution time matter equally in production.
Top pitfall: Panicking at high baseline memory (118 MiB) which includes Python interpreter + loaded libraries. Focus on increments per line — a +0.0 MiB increment is not your problem.
Self-check: Random Forest uses 500 MiB and SVM uses 200 MiB for your dataset. Accuracy difference is 1%. Which algorithm do you choose for a production container with 1 GB RAM?
Connects to: Algorithm Selection, Model Deployment, memray (Mac/Linux), Edge Deployment (TensorFlow Lite, ONNX Runtime)
Was this lecture useful?
BitsNotes AI Assistant
Subject Notes AssistantConfigure AI Chat
Choose how to access the chatbotSigned in as
Powered by BitsNotes — 20 messages per day. No API key needed. Want unlimited access? Use "Bring Your Own Key" mode.
Sign in to use AI Chat
Get 20 free AI messages per day to ask questions about your lecture notes. Sign in with Google or GitHub — it takes 5 seconds.
Sign In to BitsNotesSwitch to "Bring Your Own Key" tab above for unlimited access with any OpenAI-compatible provider.