Python Practical — Line-by-Line Explanations
NLP Tokenization & NER · Probability Distributions · Hypothesis Testing · Linear Regression
1. Tokenization Comparison: NLTK vs spaCy
import nltk
import spacy
These load the two libraries you're comparing - NLTK and spaCy - both used for NLP (Natural Language
Processing) tasks like tokenization.
nlp = [Link]("en_core_web_sm")
This loads spaCy's small English model into a variable called nlp. This model contains the rules and
trained data spaCy uses to understand English text - split sentences, tag words, etc.
def tokenize(sentence):
Defines a function called tokenize that takes one sentence as input and will return two versions of
tokenized output (one from each library).
nltk_tokens = nltk.word_tokenize(sentence)
Uses NLTK's word_tokenize function to split the sentence into a list of words/punctuation, based on
NLTK's built-in rules (mostly whitespace and punctuation patterns).
spacy_tokens = [[Link] for token in nlp(sentence)]
Runs the sentence through spaCy's model (nlp(sentence)), which produces a Doc object containing token
objects. This line loops through each token and pulls out just the text ([Link]), collecting them into a
list. Since spaCy uses a trained model rather than fixed rules, it can be smarter about edge cases like
abbreviations.
return nltk_tokens, spacy_tokens
Sends both lists back to whoever called the function.
sentences = [
"The quick brown fox jumps over the lazy dog.",
"Let's go to the park, it's a beautiful day!",
"Mr. Smith went to Washington D.C.",
"He's going to the U.S.A.",
]
A list of four test sentences, each chosen to test a different tricky case: a simple sentence, contractions
(Let's, it's), abbreviations (Mr., D.C.), and both combined (He's, U.S.A.).
for i in range(len(sentences)):
Page 1
Loops through the indices of the sentences list (0, 1, 2, 3), so you can process each sentence one at a
time.
nltk_tok, spacy_tok = tokenize(sentences[i])
Calls your tokenize function on the current sentence, and unpacks the two returned lists into nltk_tok and
spacy_tok.
print(f"\nSentence {i+1}: {sentences[i]}")
Prints a blank line, then the sentence number (starting from 1, not 0) and the original sentence text.
print(f" NLTK : {nltk_tok}")
print(f" spaCy : {spacy_tok}")
Prints the tokenized output from each library side by side, so you can visually compare how NLTK vs
spaCy handled contractions and abbreviations differently.
Overall point
The script demonstrates that NLTK tends to split text using simpler rule-based patterns, while spaCy uses
a trained model - this often shows up clearly in how each handles things like it's (one token vs. split into it +
's) or U.S.A. (kept together vs. broken up).
Page 2
2. Named Entity Recognition using NLTK
import nltk
Loads the NLTK library.
[Link]('punkt_tab', quiet=True)
Downloads the punkt_tab data package, which NLTK needs to split text into words/sentences (used by
word_tokenize). quiet=True suppresses the download progress messages.
[Link]('averaged_perceptron_tagger_eng', quiet=True)
Downloads a pre-trained model that tags each word with its part of speech (noun, verb, proper noun, etc.)
- needed for pos_tag.
[Link]('maxent_ne_chunker_tab', quiet=True)
Downloads the model used for Named Entity Recognition (NER) - this is what identifies things like people,
organizations, and locations in text.
[Link]('words', quiet=True)
Downloads a large dictionary of English words, which the NE chunker uses as a reference to help decide
what counts as a recognizable word/entity.
sentences = [
"Elon Musk founded SpaceX in California.",
"Barack Obama served as President of the United States.",
"Google LLC is a technology company based in Mountain View.",
"Microsoft acquired LinkedIn for $26.2 billion in 2016.",
"Tim Cook is the CEO of Apple in Cupertino.",
]
Five test sentences, each containing multiple named entities - people (Elon Musk, Barack Obama, Tim
Cook), organizations (SpaceX, Google LLC, Microsoft, LinkedIn, Apple), and places (California, Mountain
View, Cupertino) - so you can see how well NLTK detects them.
for i, sentence in enumerate(sentences, 1):
Loops through each sentence. enumerate(sentences, 1) gives you both the sentence and a counter i that
starts at 1 (instead of the default 0), so your printed output reads Sentence 1, Sentence 2, etc.
tokens = nltk.word_tokenize(sentence)
Splits the sentence into individual words/punctuation tokens (same as before).
pos_tags = nltk.pos_tag(tokens)
Tags each token with its part of speech. For example, "Elon" might get tagged NNP (proper noun,
singular). This step is required before NER, since the chunker relies on POS tags to guess which words
Page 3
might be entities.
tree = nltk.ne_chunk(pos_tags)
Runs Named Entity Recognition on the POS-tagged words. This returns a tree structure: regular words
stay as flat leaves, but recognized entities (like "Elon Musk") get grouped together into labeled branches
(e.g., labeled PERSON).
print(f"\nSentence {i}: {sentence}")
Prints a blank line, then the sentence number and original text.
for subtree in tree:
Loops through each element of the tree. Some elements are just plain words (leaves), others are labeled
entity branches (subtrees).
if hasattr(subtree, "label"):
Checks whether this element is a labeled entity branch rather than a plain word. Only entity branches have
a .label() method (like PERSON, ORGANIZATION, GPE for geo-political entity) - ordinary tokens don't, so
this filters them out.
entity = " ".join(word for word, tag in [Link]())
Each entity branch (subtree) contains its own leaves - pairs of (word, tag). This line pulls out just the words
from those pairs and joins them back into a single string, e.g., combining "Elon" and "Musk" into "Elon
Musk".
print(f" [{[Link]()}] -> {entity}")
Prints the entity's category label (like PERSON or ORGANIZATION) alongside the actual text recognized
as that entity.
Overall point
This script shows NLTK's built-in NER pipeline in action - tokenize, tag parts of speech, chunk into named
entities - and prints out which words got recognized as people, places, or organizations in each sentence.
Note that NLTK's NER is fairly basic compared to modern tools like spaCy, so it may miss things (like
"$26.2 billion" as a monetary value) or mislabel some proper nouns.
Page 4
3. Probability Distributions: Normal, Binomial, Poisson,
Exponential
import numpy as np
import [Link] as plt
import [Link] as stats
Loads three libraries: NumPy for generating random samples and numerical operations, Matplotlib for
plotting, and [Link] for the exact mathematical formulas (PDF/PMF curves) of each distribution.
1. Normal Distribution
mean = 0
std_dev = 1
num_samples = 1000
Sets up parameters for the normal (bell curve) distribution: centered at 0, with a standard deviation of 1,
and 1000 random samples to draw.
normal_samples = [Link](mean, std_dev, num_samples)
Generates 1000 random numbers following a normal distribution with the given mean and standard
deviation.
[Link](figsize=(12, 5))
Creates a new figure (plotting canvas) sized 12x5 inches, wide enough to hold two side-by-side plots.
[Link](1, 2, 1)
Sets up a grid of 1 row x 2 columns of plots, and selects the first slot to draw into.
[Link](normal_samples, bins=30, density=True, alpha=0.6, color='g')
Draws a histogram of the 1000 random samples, split into 30 bins. density=True scales it so the histogram
represents a probability density (area under bars sums to 1) rather than raw counts - this makes it
comparable to the theoretical curve. alpha=0.6 makes the bars semi-transparent, color='g' makes them
green.
x = [Link](mean - 3 * std_dev, mean + 3 * std_dev, 100)
Creates 100 evenly-spaced x-values spanning 3 standard deviations below and above the mean - this
range covers ~99.7% of a normal distribution.
[Link](x, [Link](x, mean, std_dev), 'k', linewidth=2)
Calculates the exact theoretical PDF (Probability Density Function) curve for the normal distribution at
each x-value, and overlays it as a black line on top of the histogram - so you can visually compare your
random samples against the "true" mathematical shape.
[Link]('Normal Distribution')
Page 5
[Link]('Value')
[Link]('Probability Density')
Labels the subplot with a title and axis names.
2. Binomial Distribution
n = 10 # Number of trials
p = 0.5 # Probability of success
num_samples = 1000
Parameters for the binomial distribution: 10 trials per experiment (n), 50% probability of success on each
trial (p), and 1000 experiments to simulate.
binomial_samples = [Link](n, p, num_samples)
Simulates 1000 experiments, each one being "flip a coin 10 times and count the successes" - so each
value in this array is a number between 0 and 10.
[Link](1, 2, 2)
Switches to the second slot in the 1x2 grid (right side of the figure).
[Link](binomial_samples, bins=[Link](0, n + 2) - 0.5, density=True, alpha=0.6,
color='b')
Draws a histogram of the results. The bin edges ([Link](0, n+2) - 0.5) are deliberately offset by 0.5 so
that each bar is centered exactly on its integer value (0, 1, 2, ... 10) instead of straddling between two
integers - important since binomial outcomes are discrete counts, not continuous values.
x = [Link](0, n + 1)
Creates the list of possible outcomes: 0 through 10.
[Link](x, [Link](x, n, p), 'r', linewidth=2, marker='o')
Calculates the theoretical PMF (Probability Mass Function - the discrete version of a PDF) for each
possible outcome, and overlays it as a red line with circular markers at each point.
[Link]('Binomial Distribution')
[Link]('Number of Successes')
[Link]('Probability')
Labels this subplot.
plt.tight_layout()
[Link]()
tight_layout() automatically adjusts spacing so labels/titles don't overlap between the two subplots. show()
renders and displays the completed figure (both Normal and Binomial plots side by side).
3. Poisson Distribution
Page 6
lam = 3 # Average rate of events
num_samples = 1000
Sets the Poisson distribution's rate parameter (lam, short for lambda) to 3 - meaning events happen at an
average rate of 3 per interval - and generates 1000 samples.
poisson_samples = [Link](lam, num_samples)
Simulates 1000 random draws from a Poisson process, e.g., "how many customers arrive in an hour" if the
average is 3/hour.
[Link](figsize=(12, 5))
Starts a new figure (separate from the first one) for the Poisson and Exponential plots.
[Link](1, 2, 1)
Selects the left slot of this new 1x2 grid.
[Link](poisson_samples, bins=[Link](0, max(poisson_samples) + 2) - 0.5, density=True,
alpha=0.6, color='y')
Same idea as the binomial histogram - bins offset by 0.5 so each bar centers on an integer count, ranging
from 0 up to the highest value actually observed in the samples. Bars are colored yellow.
x = [Link](0, max(poisson_samples) + 1)
Creates the list of possible outcome values, from 0 up to the maximum observed count.
[Link](x, [Link](x, lam), 'm', linewidth=2, marker='o')
Overlays the theoretical Poisson PMF curve in magenta with circular markers.
[Link]('Poisson Distribution')
[Link]('Number of Events')
[Link]('Probability')
Labels this subplot.
4. Exponential Distribution
scale = 2 # Mean of the distribution
num_samples = 1000
Sets the exponential distribution's scale parameter (mean) to 2, and generates 1000 samples. The
exponential distribution often models "time until the next event" (like time between customer arrivals).
exponential_samples = [Link](scale, num_samples)
Generates 1000 random values from this distribution - these will mostly be small numbers with a long tail
of larger, less frequent values.
Page 7
[Link](1, 2, 2)
Selects the right slot in this figure's grid.
[Link](exponential_samples, bins=30, density=True, alpha=0.6, color='c')
Draws a histogram with 30 bins (no need for integer-centering here since exponential values are
continuous, not discrete counts). Bars are cyan.
x = [Link](0, max(exponential_samples), 100)
Creates 100 evenly-spaced x-values from 0 up to the highest sampled value.
[Link](x, [Link](x, scale=scale), 'k', linewidth=2)
Overlays the theoretical exponential PDF curve in black.
[Link]("Exponential Distribution")
[Link]("Value")
[Link]("Probability Density")
Labels this subplot.
plt.tight_layout()
[Link]()
Adjusts spacing and displays this second figure (Poisson and Exponential plots side by side).
Overall point
The script demonstrates four common probability distributions by generating random samples, plotting
them as histograms, and overlaying the exact mathematical curve on top - so you can visually confirm that
your randomly generated data matches the theoretical shape it's supposed to follow.
Page 8
4. Hypothesis Testing: t-tests and Chi-Square Tests
import numpy as np
import [Link] as stats
Loads NumPy for handling arrays of numbers, and [Link] which contains all the hypothesis testing
functions used below.
1. One-Sample t-test
A one-sample t-test checks whether a sample's average is significantly different from some
known/hypothesized value.
sample_data = [Link]([22, 25, 28, 23, 27, 26, 24, 25, 29, 28])
population_mean = 25
Creates an array of 10 observed data points, and sets the value we want to compare the sample's mean
against (25).
t_stat, p_value = stats.ttest_1samp(sample_data, population_mean)
Runs the one-sample t-test. This compares the mean of sample_data against population_mean (25) and
returns two things: the t-statistic (how many standard errors the sample mean is away from 25) and the
p-value (the probability of seeing a difference this large purely by chance, if there's actually no real
difference).
print("One-Sample t-test:")
print(f"t-statistic: {t_stat}")
print(f"p-value: {p_value}")
Prints both results.
alpha = 0.05
Sets the significance threshold. This is the conventional cutoff - if there's less than a 5% chance the
observed result happened randomly, we consider it "statistically significant."
if p_value < alpha:
print("Reject the null hypothesis.")
else:
print("Fail to reject the null hypothesis.")
The core decision rule: the "null hypothesis" here is "the sample mean is NOT actually different from 25 -
any difference is just random noise." If p_value is smaller than 0.05, that's strong enough evidence to
reject that assumption (i.e., conclude there IS a real difference). Otherwise, we don't have enough
evidence, so we "fail to reject" it (this is NOT the same as proving there's no difference - it just means we
can't confidently claim there is one).
2. Two-Sample Independent t-test
This checks whether two separate, unrelated groups have significantly different means from each other.
group1 = [Link]([22, 25, 28, 23, 27])
Page 9
group2 = [Link]([26, 24, 25, 29, 28])
Two independent groups of 5 data points each - e.g., imagine two different classes' test scores.
t_stat_ind, p_value_ind = stats.ttest_ind(group1, group2)
Runs the independent two-sample t-test, comparing whether group1's mean and group2's mean are
significantly different from each other. Returns the t-statistic and p-value, same idea as before.
print("Two-Sample Independent t-test:")
print(f"t-statistic: {t_stat_ind}")
print(f"p-value: {p_value_ind}")
if p_value_ind < alpha:
print("Reject the null hypothesis.")
else:
print("Fail to reject the null hypothesis.")
Prints results and applies the same decision rule - null hypothesis here is "group1 and group2 have the
same true mean."
3. Chi-Square Test (Goodness of Fit)
This checks whether an observed distribution of categories matches an expected/theoretical distribution.
observed_frequencies = [Link]([45, 55, 60, 40])
expected_frequencies = [Link]([50, 50, 50, 50])
observed_frequencies might represent, say, how many people chose each of 4 options in a survey.
expected_frequencies is what you'd expect if all 4 options were equally likely (200 total responses split
evenly into 50 each).
chi2_stat, p_value_chi2 = [Link](observed_frequencies,
expected_frequencies)
Runs the chi-square goodness-of-fit test, measuring how far the observed counts deviate from the
expected counts. Returns the chi-square statistic and p-value.
print("Chi-Square Goodness of Fit Test:")
print(f"Chi-square statistic: {chi2_stat}")
print(f"p-value: {p_value_chi2}")
if p_value_chi2 < alpha:
print("Reject the null hypothesis.")
else:
print("Fail to reject the null hypothesis.")
Prints results. Null hypothesis: "the observed frequencies match the expected/equal distribution" (i.e., no
real preference exists among the 4 options).
4. Chi-Square Test (Independence)
This checks whether two categorical variables are related/associated, or independent of each other (e.g.,
"does gender affect product preference?").
Page 10
observed_table = [Link]([[20, 30], [25, 45]])
A 2x2 contingency table - e.g., rows could represent two groups (Male/Female) and columns two
categories (Prefers A / Prefers B), with each cell being a count.
chi2_stat_ind, p_value_ind_table, dof, expected_table = stats.chi2_contingency(
observed_table)
Runs the chi-square test for independence on this table. It returns four things: the chi-square statistic, the
p-value, the degrees of freedom (dof - related to the table's dimensions), and the expected_table (what the
counts would look like if the two variables were truly independent).
print("Chi-Square Test for Independence:")
print(f"Chi-square statistic: {chi2_stat_ind}")
print(f"p-value: {p_value_ind_table}")
print(f"Degrees of freedom: {dof}")
Prints the results.
if p_value_ind_table < alpha:
print("Reject the null hypothesis.")
else:
print("Fail to reject the null hypothesis.")
Applies the decision rule. Null hypothesis here: "the two categorical variables are independent" (no real
relationship between them).
Overall point
The script walks through four classic hypothesis tests - comparing a sample mean to a fixed value,
comparing two independent groups, checking if observed category counts match expected ones, and
checking if two categorical variables are related - each time computing a test statistic and p-value, then
using the standard 0.05 threshold to decide whether the evidence is strong enough to reject the "nothing's
going on" (null) hypothesis.
Page 11
5. Linear Regression: Car Fuel Efficiency
import numpy as np
import [Link] as plt
Loads NumPy for numerical calculations and Matplotlib for plotting.
Dataset
engine = [Link]([1.0,1.2,1.4,1.6,1.8,2.0,2.2,2.5,2.8,3.0,3.2,3.5,3.8,4.0,4.5,5.0,5.5,6.0])
mpg = [Link]([45, 42, 40, 38, 36, 34, 31, 29, 27, 25, 23, 22, 21, 19, 18, 16, 15, 14])
Two arrays representing paired data points: engine is engine size in liters (the input/independent variable),
and mpg is fuel efficiency in miles-per-gallon (the output/dependent variable we want to predict). The
pattern is intuitive - bigger engines tend to give worse mileage.
OLS Regression
OLS = "Ordinary Least Squares," the standard method for fitting a straight line through data by minimizing
squared errors. This section manually calculates the line's slope and intercept using the underlying math
formula, rather than calling a built-in regression function.
x_mean = [Link](engine)
y_mean = [Link](mpg)
Calculates the average engine size and average mpg across all data points.
slope = [Link]((engine - x_mean) * (mpg - y_mean)) / [Link]((engine - x_mean) ** 2)
This is the OLS slope formula. For each data point, it multiplies how far that engine size is from the mean
by how far that mpg is from the mean, sums those products up (numerator - this captures how the two
variables move together), then divides by the sum of squared deviations of engine size from its mean
(denominator - this captures how spread out the engine sizes are). The result is the slope: how much mpg
changes per unit increase in engine size (this will be negative, since mpg decreases as engine size
grows).
intercept = y_mean - slope * x_mean
Once you have the slope, the line must pass through the point (x_mean, y_mean) - this rearranges the line
equation y = intercept + slope*x to solve for the intercept using that fact.
mpg_pred = intercept + slope * engine
Uses the fitted line equation to calculate a predicted mpg value for every engine size in the dataset - this
generates the straight line's y-values for plotting later and for computing error metrics.
Metrics
r2 = 1 - [Link]((mpg - mpg_pred) ** 2) / [Link]((mpg - y_mean) ** 2)
Calculates R-squared, which tells you what fraction of the variation in mpg is explained by the model. The
numerator sums up squared prediction errors (actual mpg minus predicted mpg). The denominator sums
up squared deviations from the mean (total variation in mpg, if you'd just guessed the average every time
Page 12
with no model at all). 1 - (error/total variation) gives a value between 0 and 1 - closer to 1 means the line
fits very well.
rmse = [Link]([Link]((mpg - mpg_pred) ** 2))
Calculates RMSE (Root Mean Squared Error) - takes the average of the squared prediction errors, then
takes the square root to bring it back to the original units (mpg). This tells you, on average, how far off your
predictions are in real mpg terms - easier to interpret intuitively than R-squared.
print(f"Equation : MPG = {intercept:.2f} + ({slope:.2f}) x Engine_Size")
print(f"R2 : {r2:.4f}")
print(f"RMSE: {rmse:.4f}")
Prints the fitted equation (rounded to 2 decimal places) and the two metrics (rounded to 4 decimal places).
Plot
[Link](figsize=(7, 5))
Creates a new figure sized 7x5 inches.
[Link](engine, mpg, color='steelblue', label='Actual Data')
Plots the real data points as a scatter plot - each dot is one car's (engine size, mpg) pair, colored blue,
labeled "Actual Data" for the legend.
[Link](engine, mpg_pred, color='red', lw=2, label=f'Fit (R2={r2:.2f})')
Draws the fitted regression line in red (lw=2 sets line width to 2), connecting the predicted mpg values
across all engine sizes. The label includes the R-squared value formatted to 2 decimals, so it shows up
directly in the legend.
[Link]("Engine Size (L)")
[Link]("MPG")
[Link]("Car Fuel Efficiency - Linear Regression")
Labels the x-axis, y-axis, and adds a chart title.
[Link]()
Displays the legend box showing what the blue dots and red line represent.
[Link](alpha=0.3)
Adds a light background grid (alpha=0.3 makes it 30% opacity, so it's subtle rather than overpowering the
data).
plt.tight_layout()
[Link]()
Adjusts spacing to avoid clipped labels, then renders and displays the final chart.
Page 13
Overall point
The script manually implements simple linear regression from scratch (rather than using a library like
scikit-learn), fits a straight line predicting MPG from engine size, evaluates how good the fit is using
R-squared and RMSE, and visualizes the actual data points alongside the fitted line.
Page 14
6. Linear Regression: Student Exam Score Prediction with
Grading
Dataset
hours =
[Link]([0.5,1.0,1.5,2.0,2.5,3.0,3.5,4.0,4.5,5.0,5.5,6.0,6.5,7.0,7.5,8.0,8.5,9.0,9.5,10.0])
score = [Link]([32, 35, 40, 42, 45, 50, 52, 55, 58, 61, 64, 67, 70, 73, 75, 78, 82, 85, 88,
92])
Two paired arrays: hours is study time in hours (input variable), score is the exam score achieved (output
variable). The pattern shows more study hours generally leading to higher scores.
OLS Regression
x_mean = [Link](hours)
y_mean = [Link](score)
Calculates the average study hours and average score across all 20 data points.
slope = [Link]((hours - x_mean) * (score - y_mean)) / [Link]((hours - x_mean) ** 2)
The OLS slope formula: for each point, multiply how far its hours-value is from the mean by how far its
score is from the mean, sum these products (this captures how the two variables move together), then
divide by the sum of squared deviations of hours from its mean (this captures the spread of the input
variable). Result: how much the score changes per additional hour of study - expect this to be positive
here.
intercept = y_mean - slope * x_mean
Since the fitted line must pass through (x_mean, y_mean), this rearranges y = intercept + slope*x to solve
for the intercept.
score_pred = intercept + slope * hours
Applies the fitted line equation to every hours value in the dataset, generating predicted scores for each -
used later for plotting and error metrics.
Metrics
r2 = 1 - [Link]((score - score_pred) ** 2) / [Link]((score - y_mean) ** 2)
R-squared measures how much of the variation in scores is explained by the model. Numerator: sum of
squared errors (actual score minus predicted). Denominator: total variation in scores around their mean
(as if you had no model at all). 1 - error/total gives a value near 1 for a good fit.
rmse = [Link]([Link]((score - score_pred) ** 2))
RMSE: average squared prediction error, then square-rooted to bring it back into "score points" units - an
intuitive measure of typical prediction error size.
print(f"Equation : Score = {intercept:.2f} + {slope:.2f} x Study_Hours")
print(f"R2 : {r2:.4f}")
Page 15
print(f"RMSE: {rmse:.4f}")
Prints the fitted equation and both metrics.
Predictions with Grade
print(f"\n{'Study Hours':<14} {'Predicted Score':>16} {'Grade':>7}")
Prints a header row for a small table. {'Study Hours':<14} left-aligns the text "Study Hours" within a
14-character-wide column; {'Predicted Score':>16} right-aligns "Predicted Score" within 16 characters;
{'Grade':>7} right-aligns "Grade" within 7 characters. The \n adds a blank line before the header for
spacing.
print("-" * 38)
Prints a horizontal line of 38 dashes as a visual separator under the header (38 roughly matches the
combined column widths).
for h in [2.0, 4.0, 6.0, 8.0, 10.0]:
Loops through five specific study-hour values you want to generate predictions for (not the whole dataset -
just these five sample points).
p = intercept + slope * h
Uses the fitted regression equation to predict the score for this particular value of h.
g = "A" if p>=80 else "B" if p>=70 else "C" if p>=60 else "D" if p>=50 else "F"
Converts the predicted numeric score into a letter grade using chained conditional logic (essentially a
compact if/elif/else): 80+ -> A, 70-79 -> B, 60-69 -> C, 50-59 -> D, below 50 -> F. Python evaluates this left
to right - it checks p>=80 first, and only if that's false does it check the next condition, and so on.
print(f"{h:<14.1f} {p:>16.2f} {g:>7}")
Prints one row of the table: h left-aligned in a 14-character column with 1 decimal place, p right-aligned in a
16-character column with 2 decimal places, and g (the grade letter) right-aligned in a 7-character column.
This alignment keeps all rows visually lined up into neat columns.
Plot
[Link](figsize=(7, 5))
Creates a new figure sized 7x5 inches.
[Link](hours, score, color='mediumpurple', label='Actual Data')
Plots the real data points as a purple scatter plot, labeled "Actual Data" for the legend.
[Link](hours, score_pred, color='red', lw=2, label=f'Fit (R2={r2:.2f})')
Page 16
Draws the fitted regression line in red with line width 2, with the R-squared value (2 decimal places)
embedded directly in its legend label.
[Link]("Study Hours")
[Link]("Exam Score")
[Link]("Student Exam Score - Linear Regression")
[Link]()
[Link](alpha=0.3)
plt.tight_layout()
[Link]()
Labels the axes and title, shows the legend, adds a subtle grid (30% opacity), tidies up spacing, and
renders the final chart.
Overall point
Same manual OLS regression approach as the car mpg example - fit a line by hand using the
slope/intercept formulas, evaluate it with R-squared and RMSE - but this version adds a practical
extension: taking specific study-hour inputs, predicting scores, and converting those predictions into letter
grades using a simple threshold-based rule.
Page 17
7. Train-Test Split: Pass/Fail Prediction Dataset
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
Loads NumPy for random number generation, pandas for building and handling the dataset as a table
(DataFrame), and scikit-learn's train_test_split function for splitting data into training and test sets.
Build a simple dataset
[Link](42)
Sets a fixed "seed" for NumPy's random number generator. This makes all the random numbers
generated afterward reproducible - running the script again gives you the exact same "random" data
instead of different values each time.
df = [Link]({
"StudyHours": [Link]([Link](1, 10, 100), 1),
"Attendance": [Link](60, 100, 100),
"PrevScore": [Link](40, 95, 100),
})
Creates a pandas DataFrame (table) with 100 rows and three columns: StudyHours is 100 random
decimal values between 1 and 10, rounded to 1 decimal place ([Link] generates continuous
random numbers, [Link](..., 1) rounds them). Attendance is 100 random whole numbers between 60
and 99 (randint's upper bound is exclusive, so max is 99). PrevScore is 100 random whole numbers
between 40 and 94 (previous exam score).
df["Result"] = ((df["StudyHours"] * 5 + df["Attendance"] * 0.3 + df["PrevScore"] * 0.5) >
90).astype(int)
Creates a new column called Result - this is the target/label you'll eventually want to predict. It computes a
weighted combination of the three features (study hours weighted heaviest at x5, attendance at x0.3,
previous score at x0.5), then checks if that combined value exceeds 90. The result of that comparison is
True/False, which .astype(int) converts into 1 (Pass) or 0 (Fail). This artificially creates a pattern in the data
so that higher study hours, attendance, and previous scores lead to a "Pass."
print("\n[A] Dataset (first 5 rows):")
print([Link]())
Prints a blank line, a label, then the first 5 rows of the dataset using .head() - useful for a quick visual
check of what the data looks like.
print(f"\nTotal rows : {len(df)}")
print(f"Pass count : {df['Result'].sum()}")
print(f"Fail count : {(df['Result'] == 0).sum()}")
Prints summary stats: total number of rows (100), how many rows are labeled Pass (sum() works here
because Pass=1, Fail=0, so summing the column counts the 1s), and how many are labeled Fail (counts
how many rows equal 0).
Page 18
Features and Target
X = df[["StudyHours", "Attendance", "PrevScore"]]
y = df["Result"]
Splits the DataFrame into two parts: X holds the input features (the three predictor columns) that a model
would use to make predictions, and y holds the target/label (Result) that the model is trying to predict. This
is a standard convention in machine learning - capital X for the feature matrix, lowercase y for the target
vector.
Basic 80-20 split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
Splits both X and y into training and test sets simultaneously, keeping the feature-label pairs aligned
correctly. test_size=0.2 means 20% of the data goes into the test set (20 rows) and 80% into training (80
rows). random_state=42 fixes the randomness of which rows get picked for each set, so the split is
reproducible.
print("\n[B] Basic 80-20 Split:")
print(f" Training set : {X_train.shape[0]} samples")
print(f" Test set : {X_test.shape[0]} samples")
Prints how many rows ended up in each set. .shape[0] gives the number of rows in that DataFrame
(should be 80 for training, 20 for test).
Stratified split (preserves class ratio)
X_tr, X_te, y_tr, y_te = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
Does the same 80-20 split, but adds stratify=y. This tells scikit-learn to make sure the ratio of Pass to Fail
in the original data is preserved proportionally in both the training and test sets. Without this, a random
split could accidentally put too many Passes in the test set and too few in training (especially risky with
imbalanced data) - stratifying prevents that.
print("\n[C] Stratified Split (class ratio preserved):")
print(f" Train — Pass: {y_tr.sum()} Fail: {(y_tr==0).sum()}")
print(f" Test — Pass: {y_te.sum()} Fail: {(y_te==0).sum()}")
Prints how many Pass/Fail rows landed in each set after stratifying - you'd expect the Pass:Fail ratio here
to closely match the ratio in the full dataset (unlike the basic split, which doesn't guarantee this).
Sample of training data
print("\n[D] First 5 rows of training features:")
print(X_train.head())
Shows the first 5 rows of the training features (from the basic 80-20 split, not the stratified one) - a sanity
check on what the model would actually be trained on.
Page 19
print("\n[E] First 5 training labels:")
print(y_train.head().values)
Shows the first 5 corresponding labels for those same training rows. .values converts the pandas Series
into a plain NumPy array for printing, so it displays as a simple list of numbers (e.g., [1 0 1 0 1]) rather than
a pandas Series with an index column alongside it.
Overall point
The script builds a synthetic pass/fail dataset from three features, then demonstrates two ways of splitting
data for machine learning - a basic random 80/20 split, and a stratified split that preserves the original
Pass/Fail ratio in both subsets. This distinction matters a lot in real ML workflows: stratifying prevents a
"lucky" or "unlucky" random split from making one class over/under-represented in training or testing,
which could otherwise mislead you about how well a model is actually performing.
Page 20