Syntax, patterns, and worked examples for the nine libraries that cover the full ML workflow — from raw arrays to deployed neural networks.
NumPy provides the ndarray — a fixed-type, multi-dimensional array that
executes
mathematical operations in compiled C rather than interpreted Python. This makes it 10–100× faster than
equivalent list-based code and forms the data representation layer beneath every other ML library.
Install with pip install numpy, import as import numpy as np.
Creating arrays
import numpy as np
# From Python sequences
a = np.array([1, 2, 3, 4, 5])
b = np.array([[1, 2, 3],
[4, 5, 6]]) # shape (2, 3)
# Factory functions
np.zeros((3, 4)) # all 0.0, float64
np.ones((2, 3)) # all 1.0
np.eye(4) # 4×4 identity matrix
np.full((2, 3), 7) # fill with constant
# Ranges
np.arange(0, 10, 2) # [0 2 4 6 8]
np.linspace(0, 1, 5) # 5 evenly spaced points in [0, 1]
# Random — set seed once for reproducibility
np.random.seed(42)
np.random.rand(3, 3) # uniform [0, 1)
np.random.randn(3, 3) # standard normal N(0,1)
np.random.randint(0, 10, (3, 3)) # integers
Inspection
| Attribute / Method | Returns |
|---|---|
a.shape |
Tuple of dimension sizes, e.g. (100, 3) |
a.ndim |
Number of axes — 1 for vector, 2 for matrix, 3+ for tensor |
a.dtype |
Element type — float64, int32, etc.
|
a.size |
Total element count (product of all shape values) |
a.nbytes |
Memory consumed in bytes |
Indexing and slicing
a = np.array([[10, 20, 30],
[40, 50, 60],
[70, 80, 90]])
a[0, 0] # 10 — row 0, col 0 (zero-indexed)
a[1, 2] # 60
a[-1, -1] # 90 — last row, last col
# Slicing [start : stop : step]
a[0, :] # [10 20 30] — entire row 0
a[:, 1] # [20 50 80] — entire column 1
a[0:2, 1:] # [[20 30], [50 60]]
# Boolean indexing
a[a > 50] # [60 70 80 90]
# Fancy indexing
a[[0, 2], :] # rows 0 and 2
Operations and aggregations
a = np.array([1.0, 2.0, 3.0, 4.0])
b = np.array([10.0, 20.0, 30.0, 40.0])
# Element-wise (no loop needed)
a + b # [11 22 33 44]
a * b # [10 40 90 160]
a ** 2 # [1 4 9 16]
a + 5 # [6 7 8 9] — scalar broadcast
# Aggregations
a.sum() # 10.0
a.mean() # 2.5
a.std() # standard deviation
a.min() # 1.0
a.max() # 4.0
a.argmin() # 0 — index of minimum
a.argmax() # 3 — index of maximum
# Axis-based — axis=0 collapses rows, axis=1 collapses columns
m = np.array([[1, 2], [3, 4]])
m.sum(axis=0) # [4 6] — sum each column
m.sum(axis=1) # [3 7] — sum each row
# Linear algebra
np.dot(a, a) # dot product
np.linalg.norm(a) # L2 norm
A = np.array([[1,2],[3,4]])
np.linalg.inv(A) # matrix inverse
np.linalg.det(A) # determinant
vals, vecs = np.linalg.eig(A)
Reshaping
a = np.arange(12) # [0 1 2 ... 11]
a.reshape(3, 4) # 3×4 — total elements must be equal
a.reshape(3, -1) # same — -1 infers the missing dimension
a.reshape(-1, 1) # column vector: shape (12, 1)
a.flatten() # 1D copy
# Stacking
x = np.array([1, 2, 3])
y = np.array([4, 5, 6])
np.hstack([x, y]) # [1 2 3 4 5 6]
np.vstack([x, y]) # [[1 2 3], [4 5 6]]
Pandas provides the DataFrame — a labelled two-dimensional table analogous
to a
spreadsheet — and the Series for a single column. It handles the entire
data wrangling phase: loading files, cleaning missing values, filtering rows, grouping, and merging
datasets before they reach a model. Install as pip install pandas,
import as import pandas as pd.
Creating and loading
import pandas as pd
# From a dict — keys become column names
df = pd.DataFrame({
'name': ['Alice', 'Bob', 'Carol'],
'age': [24, 32, 28],
'score': [88.5, 72.1, 95.0]
})
# File I/O
df = pd.read_csv('data.csv')
df = pd.read_csv('data.csv', index_col='id', nrows=1000)
df = pd.read_excel('data.xlsx', sheet_name=0)
df = pd.read_json('data.json')
df.to_csv('output.csv', index=False)
df.to_excel('output.xlsx', index=False)
Exploration — run these first on any new dataset
df.shape # (rows, cols)
df.dtypes # data type of each column
df.head() # first 5 rows
df.tail() # last 5 rows
df.sample(5) # 5 random rows
df.info() # column names, non-null counts, dtypes
df.describe() # count / mean / std / min / quartiles / max
df.isnull().sum() # missing values per column
df.duplicated().sum()
Selecting data
# Column selection
df['age'] # Series
df[['name', 'age']] # DataFrame
# iloc — position-based (integers)
df.iloc[0] # row 0
df.iloc[0:5] # rows 0–4
df.iloc[0:5, 1:3] # rows 0–4, columns 1–2
# loc — label-based
df.loc[0, 'age'] # row label 0, column 'age'
# Boolean filtering
df[df['age'] > 30]
df[df['score'].between(70, 90)]
df[(df['age'] > 25) & (df['score'] > 80)] # AND — use &, not 'and'
df[(df['age'] < 25) | (df['score'] > 90)] # OR — use |, not 'or'
df.query("age > 30 and score > 70")
Modifying data
# Add columns
df['grade'] = df['score'] / 100
df['passed'] = df['score'] >= 60
df['label'] = df['score'].apply(lambda x: 'A' if x >= 90 else 'B')
# Rename
df.rename(columns={'name': 'full_name'}, inplace=True)
# Drop
df.drop(columns=['grade'], inplace=True)
df.drop(index=[0, 1], inplace=True)
# Type conversion
df['age'] = df['age'].astype(float)
df['date'] = pd.to_datetime(df['date'])
# Sort
df.sort_values('score', ascending=False)
df.sort_values(['age', 'score'], ascending=[True, False])
Missing values
df.isnull().sum() # count per column
df.isnull().mean() # fraction missing per column
df.dropna() # drop any row with NaN
df.dropna(thresh=5) # keep rows with ≥5 non-NaN values
df.fillna(0)
df['age'].fillna(df['age'].mean())
df['city'].fillna('Unknown')
df.ffill() # forward fill
df.bfill() # backward fill
GroupBy
df.groupby('dept')['salary'].mean()
df.groupby('dept')['salary'].agg(['mean', 'min', 'max', 'count'])
df.groupby('dept')['salary'].sum().reset_index()
df.groupby('dept')['salary'].apply(lambda x: x.max() - x.min())
Merging
pd.merge(df1, df2, on='id', how='inner') # matching rows only
pd.merge(df1, df2, on='id', how='left') # all rows from df1
pd.merge(df1, df2, on='id', how='outer') # all rows from both
pd.concat([df1, df2], axis=0) # stack rows
pd.concat([df1, df2], axis=1) # stack columns
Matplotlib is Python's foundational plotting library. The pyplot interface
(import matplotlib.pyplot as plt) provides a stateful API for creating
figures. Most ML workflows use it directly for training curves, confusion matrices, and
feature distribution plots. Always call plt.tight_layout() before
saving and plt.show() to display.
Line, scatter, bar, histogram
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 10, 200)
# Line plot
plt.figure(figsize=(8, 4))
plt.plot(x, np.sin(x), color='steelblue', linewidth=2, label='sin')
plt.plot(x, np.cos(x), color='orange', linewidth=2, label='cos')
plt.xlabel('x')
plt.ylabel('y')
plt.title('Trigonometric functions')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('plot.png', dpi=150)
plt.show()
# Scatter plot
plt.figure(figsize=(6, 5))
plt.scatter(np.random.randn(100), np.random.randn(100),
alpha=0.6, c='steelblue', edgecolors='white', s=50)
plt.xlabel('x')
plt.ylabel('y')
plt.show()
# Bar chart
cats = ['A', 'B', 'C', 'D']
values = [23, 45, 12, 67]
plt.figure(figsize=(6, 4))
plt.bar(cats, values, color='steelblue')
plt.ylabel('Count')
plt.show()
# Histogram
data = np.random.randn(1000)
plt.figure(figsize=(7, 4))
plt.hist(data, bins=30, color='steelblue', edgecolor='white', alpha=0.85)
plt.xlabel('Value')
plt.ylabel('Frequency')
plt.show()
Subplots
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
axes[0].plot(x, np.sin(x), color='steelblue')
axes[0].set_title('Sine')
axes[0].set_xlabel('x')
axes[1].plot(x, np.cos(x), color='orange')
axes[1].set_title('Cosine')
axes[1].set_xlabel('x')
plt.suptitle('Functions', fontsize=14)
plt.tight_layout()
plt.show()
# 2×2 grid
fig, axes = plt.subplots(2, 2, figsize=(10, 8))
# Access panels as axes[row, col]
Heatmap — confusion matrix
cm = np.array([[85, 5, 10], [3, 90, 7], [2, 4, 94]])
labels = ['Cat', 'Dog', 'Bird']
fig, ax = plt.subplots(figsize=(5, 4))
im = ax.imshow(cm, cmap='Blues')
plt.colorbar(im, ax=ax)
for i in range(len(labels)):
for j in range(len(labels)):
ax.text(j, i, cm[i, j], ha='center', va='center', fontsize=12)
ax.set_xticks(range(len(labels))); ax.set_xticklabels(labels)
ax.set_yticks(range(len(labels))); ax.set_yticklabels(labels)
ax.set_xlabel('Predicted')
ax.set_ylabel('Actual')
ax.set_title('Confusion Matrix')
plt.tight_layout()
plt.show()
Seaborn is a statistical visualisation library built on top of Matplotlib. It accepts Pandas
DataFrames directly — pass column names instead of arrays — and renders publication-quality plots
with significantly less code. Call sns.set_theme() once at the top
of a notebook to apply the clean Seaborn style. Install as pip install seaborn.
import seaborn as sns
import matplotlib.pyplot as plt
sns.set_theme(style='whitegrid')
df = sns.load_dataset('penguins').dropna()
# Distribution — histogram + kernel density estimate
sns.histplot(df['flipper_length_mm'], bins=30, kde=True)
plt.show()
# Box plot — distribution by category, outliers visible
sns.boxplot(x='species', y='flipper_length_mm', data=df)
plt.show()
# Violin plot — box plot + full distribution shape
sns.violinplot(x='species', y='flipper_length_mm', data=df)
plt.show()
# Scatter with categorical colour
sns.scatterplot(x='bill_length_mm', y='bill_depth_mm',
hue='species', data=df, alpha=0.7)
plt.show()
# Correlation heatmap
corr = df.select_dtypes('number').corr()
sns.heatmap(corr, annot=True, fmt='.2f', cmap='coolwarm', center=0)
plt.show()
# Pair plot — scatterplot grid across all numeric columns
sns.pairplot(df, hue='species', plot_kws={'alpha': 0.5})
plt.show()
# Count plot — bar chart of category frequency
sns.countplot(x='species', data=df, palette='Set2')
plt.show()
# Regression line overlay
sns.regplot(x='body_mass_g', y='flipper_length_mm', data=df,
scatter_kws={'alpha': 0.4}, line_kws={'color': 'red'})
plt.show()
Scikit-learn is the standard library for classical ML. Every estimator — whether a linear model,
decision tree, or preprocessor — follows the same interface:
estimator.fit(X, y) to train,
estimator.predict(X) to infer,
estimator.transform(X) for preprocessors.
This consistency means switching between algorithms requires changing one line.
Train/test split and cross-validation
from sklearn.model_selection import train_test_split, cross_val_score
import numpy as np
X = np.random.rand(1000, 10)
y = np.random.randint(0, 2, 1000)
# 80/20 split — stratify keeps the class ratio identical in both splits
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
# 5-fold cross-validation
from sklearn.linear_model import LogisticRegression
model = LogisticRegression()
scores = cross_val_score(model, X, y, cv=5, scoring='accuracy')
print(f"{scores.mean():.3f} ± {scores.std():.3f}")
Preprocessing
from sklearn.preprocessing import StandardScaler, MinMaxScaler, LabelEncoder, OneHotEncoder
# Always fit on training data only, then transform both sets
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train) # fit + transform
X_test_scaled = scaler.transform(X_test) # transform only — no refit
# Label encoding (ordinal target variable)
le = LabelEncoder()
y_encoded = le.fit_transform(['cat', 'dog', 'cat', 'bird'])
le.inverse_transform([1, 2]) # back to string labels
# One-hot encoding (categorical features)
enc = OneHotEncoder(sparse_output=False)
enc.fit_transform([['red'], ['blue'], ['green']])
Common estimators
from sklearn.linear_model import LogisticRegression, LinearRegression, Ridge, Lasso
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.svm import SVC
# All follow the same fit / predict pattern
models = {
'Logistic Regression': LogisticRegression(max_iter=1000),
'Random Forest': RandomForestClassifier(n_estimators=100, random_state=42),
'KNN': KNeighborsClassifier(n_neighbors=5),
'SVM': SVC(kernel='rbf', C=1.0),
'Gradient Boosting': GradientBoostingClassifier(n_estimators=100),
}
for name, model in models.items():
model.fit(X_train, y_train)
print(f"{name}: {model.score(X_test, y_test):.3f}")
Evaluation metrics
from sklearn.metrics import (
accuracy_score, precision_score, recall_score, f1_score,
confusion_matrix, classification_report,
mean_absolute_error, mean_squared_error, r2_score
)
import numpy as np
y_true = [0, 1, 1, 0, 1, 1]
y_pred = [0, 1, 0, 0, 1, 1]
print(accuracy_score(y_true, y_pred)) # 0.833
print(precision_score(y_true, y_pred)) # TP / (TP + FP)
print(recall_score(y_true, y_pred)) # TP / (TP + FN)
print(f1_score(y_true, y_pred))
print(classification_report(y_true, y_pred))
# Regression
y_true_r = [3.0, -0.5, 2.0, 7.0]
y_pred_r = [2.5, 0.0, 2.0, 8.0]
print(mean_absolute_error(y_true_r, y_pred_r))
print(np.sqrt(mean_squared_error(y_true_r, y_pred_r))) # RMSE
print(r2_score(y_true_r, y_pred_r))
Pipelines and hyperparameter search
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV
# Pipeline applies steps in order — no data leakage between train and test
pipe = Pipeline([
('scaler', StandardScaler()),
('model', LogisticRegression(max_iter=1000))
])
param_grid = {
'model__C': [0.01, 0.1, 1, 10],
'model__max_iter': [500, 1000]
}
search = GridSearchCV(pipe, param_grid, cv=5, scoring='accuracy', n_jobs=-1)
search.fit(X_train, y_train)
print(search.best_params_)
print(f"{search.best_score_:.3f}")
XGBoost builds an additive ensemble of decision trees where each new tree is fitted to the residual
errors
of all previous trees. It applies L1 and L2 regularisation directly in the objective function and
uses second-order gradient information for faster convergence. It consistently wins on tabular data.
Install with pip install xgboost.
import xgboost as xgb
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
X, y = load_breast_cancer(return_X_y=True)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=42)
model = xgb.XGBClassifier(
n_estimators = 200,
max_depth = 4,
learning_rate = 0.1,
subsample = 0.8,
colsample_bytree = 0.8,
eval_metric = 'logloss',
random_state = 42
)
model.fit(
X_tr, y_tr,
eval_set = [(X_te, y_te)],
verbose = 50
)
print(f"Accuracy: {accuracy_score(y_te, model.predict(X_te)):.3f}")
# Feature importance
xgb.plot_importance(model, max_num_features=10)
# Persist
model.save_model('xgb.json')
loaded = xgb.XGBClassifier()
loaded.load_model('xgb.json')
Key hyperparameters
| Parameter | Effect | Typical range |
|---|---|---|
n_estimators |
Number of trees — more is better when paired with early stopping | 100–1000 |
max_depth |
Max tree depth — deeper means more complex, higher overfitting risk | 3–6 |
learning_rate |
Shrinkage applied to each tree's contribution | 0.01–0.3 |
subsample |
Fraction of rows sampled per tree | 0.6–1.0 |
colsample_bytree |
Fraction of features sampled per tree | 0.6–1.0 |
reg_alpha |
L1 regularisation — encourages sparse feature weights | 0–1 |
reg_lambda |
L2 regularisation — smooth weight shrinkage | 0–10 |
LightGBM uses histogram-based binning and leaf-wise (rather than level-wise) tree growth to train
dramatically faster than XGBoost on large datasets. It handles categorical features natively via
categorical_feature, uses significantly less memory, and supports
distributed training. On datasets above 100k rows it is generally the first choice.
Install with pip install lightgbm.
import lightgbm as lgb
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
X, y = load_breast_cancer(return_X_y=True)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=42)
# Sklearn API
model = lgb.LGBMClassifier(
n_estimators = 500,
num_leaves = 31, # primary complexity control in LightGBM
learning_rate = 0.05,
feature_fraction = 0.8,
bagging_fraction = 0.8,
bagging_freq = 5,
random_state = 42,
verbose = -1
)
model.fit(
X_tr, y_tr,
eval_set = [(X_te, y_te)],
callbacks = [lgb.early_stopping(50), lgb.log_evaluation(100)]
)
print(f"Accuracy: {accuracy_score(y_te, model.predict(X_te)):.3f}")
print(f"Best iteration: {model.best_iteration_}")
# Native API
train_data = lgb.Dataset(X_tr, label=y_tr)
val_data = lgb.Dataset(X_te, label=y_te)
params = {
'objective': 'binary',
'metric': 'binary_logloss',
'num_leaves': 31,
'learning_rate': 0.05,
'verbose': -1
}
bst = lgb.train(params, train_data, num_boost_round=500,
valid_sets=[val_data],
callbacks=[lgb.early_stopping(50)])
y_prob = bst.predict(X_te) # probabilities
y_pred = (y_prob > 0.5).astype(int)
PyTorch uses dynamic computation graphs (define-by-run), which makes debugging and custom model
architectures straightforward. Models are defined as Python classes inheriting
nn.Module. You write the training loop explicitly — this gives complete
control over every step. Install from
pytorch.org
to get the correct CUDA-matched build.
Tensors
import torch
import numpy as np
# Creation
x = torch.tensor([1.0, 2.0, 3.0])
x = torch.tensor([[1, 2], [3, 4]], dtype=torch.float32)
x = torch.zeros(3, 4)
x = torch.ones(3, 4)
x = torch.randn(3, 4)
# Metadata
x.shape # torch.Size([3, 4])
x.dtype # torch.float32
x.device # cpu or cuda
# Move to GPU
device = 'cuda' if torch.cuda.is_available() else 'cpu'
x = x.to(device)
# NumPy interop (CPU tensors only)
t = torch.from_numpy(np.array([1.0, 2.0]))
np_arr = t.numpy()
# Operations
a = torch.tensor([1., 2., 3.])
b = torch.tensor([4., 5., 6.])
a + b
torch.dot(a, b)
torch.matmul(a.reshape(1,3), b.reshape(3,1))
Defining a model
import torch.nn as nn
class MLP(nn.Module):
def __init__(self, in_features, hidden, out_features):
super().__init__()
self.net = nn.Sequential(
nn.Linear(in_features, hidden),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(hidden, hidden),
nn.ReLU(),
nn.Linear(hidden, out_features)
)
def forward(self, x):
return self.net(x)
model = MLP(10, 64, 1)
print(model)
print(sum(p.numel() for p in model.parameters()), 'parameters')
Training loop
import torch
import torch.optim as optim
from torch.utils.data import TensorDataset, DataLoader
X = torch.randn(500, 10)
y = (X.sum(dim=1) > 0).float().unsqueeze(1)
ds = TensorDataset(X, y)
tr_ds, val_ds = torch.utils.data.random_split(ds, [400, 100])
tr_loader = DataLoader(tr_ds, batch_size=32, shuffle=True)
val_loader = DataLoader(val_ds, batch_size=64)
device = 'cuda' if torch.cuda.is_available() else 'cpu'
model = MLP(10, 64, 1).to(device)
criterion = nn.BCEWithLogitsLoss()
optimizer = optim.Adam(model.parameters(), lr=1e-3)
for epoch in range(50):
model.train()
for xb, yb in tr_loader:
xb, yb = xb.to(device), yb.to(device)
optimizer.zero_grad()
loss = criterion(model(xb), yb)
loss.backward()
optimizer.step()
model.eval()
with torch.no_grad():
val_loss = sum(criterion(model(xb.to(device)), yb.to(device)).item()
for xb, yb in val_loader)
if (epoch + 1) % 10 == 0:
print(f"Epoch {epoch+1:3d} | val loss {val_loss/len(val_loader):.4f}")
# Persist
torch.save(model.state_dict(), 'model.pth')
model.load_state_dict(torch.load('model.pth'))
model.eval()
Keras is TensorFlow's high-level API. You describe a model declaratively in layers, call
model.compile() to set the loss and optimiser, then
model.fit() to run the training loop automatically. This makes
standard architectures faster to write than PyTorch, at the cost of less training-loop
flexibility. Install with pip install tensorflow.
Sequential model
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
import numpy as np
model = keras.Sequential([
layers.Input(shape=(10,)),
layers.Dense(64, activation='relu'),
layers.Dropout(0.2),
layers.Dense(64, activation='relu'),
layers.Dense(1, activation='sigmoid')
])
model.compile(
optimizer = 'adam',
loss = 'binary_crossentropy',
metrics = ['accuracy']
)
model.summary()
X_train = np.random.randn(800, 10).astype('float32')
y_train = (X_train.sum(axis=1) > 0).astype('float32')
X_val = np.random.randn(200, 10).astype('float32')
y_val = (X_val.sum(axis=1) > 0).astype('float32')
history = model.fit(
X_train, y_train,
epochs = 50,
batch_size = 32,
validation_data = (X_val, y_val),
callbacks = [
keras.callbacks.EarlyStopping(patience=5, restore_best_weights=True),
keras.callbacks.ModelCheckpoint('best.keras', save_best_only=True)
]
)
loss, acc = model.evaluate(X_val, y_val, verbose=0)
print(f"Val accuracy: {acc:.3f}")
y_prob = model.predict(X_val[:5])
y_pred = (y_prob > 0.5).astype(int)
Loss functions and output activations
| Task | Output activation | Loss |
|---|---|---|
| Binary classification | sigmoid |
binary_crossentropy |
| Multi-class (single label) | softmax |
sparse_categorical_crossentropy |
| Multi-label | sigmoid |
binary_crossentropy |
| Regression | none (linear) | mse or mae |
Plotting training history
import matplotlib.pyplot as plt
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
axes[0].plot(history.history['loss'], label='Train')
axes[0].plot(history.history['val_loss'], label='Val')
axes[0].set_title('Loss')
axes[0].legend()
axes[1].plot(history.history['accuracy'], label='Train')
axes[1].plot(history.history['val_accuracy'], label='Val')
axes[1].set_title('Accuracy')
axes[1].legend()
plt.tight_layout()
plt.show()
# Save and reload
model.save('model.keras')
loaded = keras.models.load_model('model.keras')