import numpy as np class DecisionTreeClassifier: def __init__(self, criterion='gini', max_depth=None, min_samples_split=2, max_features=None): self.criterion = criterion self.max_depth = max_depth self.min_samples_split = min_samples_split self.max_features = max_features self.tree = None class Node: def __init__(self, feature=None, threshold=None, left=None, right=None, *, value=None): self.feature = feature self.threshold = threshold self.left = left self.right = right self.value = value # Leaf class label def fit(self, X, y): X, y = np.array(X), np.array(y) self.n_features_ = X.shape[1] self.tree = self._build_tree(X, y, depth=0) return self def _build_tree(self, X, y, depth): num_samples, _ = X.shape # Stop if conditions met if num_samples < self.min_samples_split or (self.max_depth is not None and depth >= self.max_depth) or len(np.unique(y)) == 1: leaf_val = self._majority_class(y) return DecisionTreeClassifier.Node(value=leaf_val) # Find best split feat_idx, thr = self._best_split(X, y) if feat_idx is None: leaf_val = self._majority_class(y) return DecisionTreeClassifier.Node(value=leaf_val) # Split data left_mask = X[:, feat_idx] <= thr left_node = self._build_tree(X[left_mask], y[left_mask], depth+1) right_node = self._build_tree(X[~left_mask], y[~left_mask], depth+1) return DecisionTreeClassifier.Node(feature=feat_idx, threshold=thr, left=left_node, right=right_node) def _best_split(self, X, y): best_gain = 0 best_feat, best_thr = None, None if self.criterion == 'gini': base_impurity = self._gini(y) else: base_impurity = self._entropy(y) n_features = X.shape[1] features = range(n_features) # Possibly sample subset of features (for Random Forest use) if self.max_features is not None: if isinstance(self.max_features, int) and self.max_features < n_features: features = np.random.choice(n_features, self.max_features, replace=False) elif isinstance(self.max_features, float): k = int(n_features * self.max_features) features = np.random.choice(n_features, k, replace=False) for feat in features: X_col = X[:, feat] unique_vals = np.unique(X_col) if len(unique_vals) <= 1: continue # Try midpoints between sorted unique values thresholds = (unique_vals[:-1] + unique_vals[1:]) / 2.0 for thr in thresholds: left_mask = X_col <= thr y_left, y_right = y[left_mask], y[~left_mask] if len(y_left) == 0 or len(y_right) == 0: continue # Compute impurity of the split if self.criterion == 'gini': imp_left = self._gini(y_left) imp_right = self._gini(y_right) else: imp_left = self._entropy(y_left) imp_right = self._entropy(y_right) p = float(len(y_left)) / len(y) gain = base_impurity - (p * imp_left + (1 - p) * imp_right) if gain > best_gain: best_gain, best_feat, best_thr = gain, feat, thr return best_feat, best_thr def _gini(self, y): _, counts = np.unique(y, return_counts=True) p = counts / counts.sum() return 1.0 - np.sum(p**2) def _entropy(self, y): _, counts = np.unique(y, return_counts=True) p = counts / counts.sum() p = p[p > 0] return -np.sum(p * np.log2(p)) def _majority_class(self, y): unique, counts = np.unique(y, return_counts=True) return unique[np.argmax(counts)] def predict(self, X): X = np.array(X) return np.array([self._predict_input(x, self.tree) for x in X]) def _predict_input(self, x, node): if node.value is not None: return node.value if x[node.feature] <= node.threshold: return self._predict_input(x, node.left) else: return self._predict_input(x, node.right) class DecisionTreeRegressor: def __init__(self, max_depth=None, min_samples_split=2, max_features=None): self.max_depth = max_depth self.min_samples_split = min_samples_split self.max_features = max_features self.tree = None class Node: def __init__(self, feature=None, threshold=None, left=None, right=None, *, value=None): self.feature = feature self.threshold = threshold self.left = left self.right = right self.value = value # Leaf output value def fit(self, X, y): X, y = np.array(X), np.array(y, dtype=float) self.n_features_ = X.shape[1] self.tree = self._build_tree(X, y, depth=0) return self def _build_tree(self, X, y, depth): num_samples, _ = X.shape if num_samples < self.min_samples_split or (self.max_depth is not None and depth >= self.max_depth) or np.var(y) == 0: leaf_val = np.mean(y) return DecisionTreeRegressor.Node(value=leaf_val) best_sse = float('inf') best_feat, best_thr = None, None n_features = X.shape[1] features = range(n_features) if self.max_features is not None: if isinstance(self.max_features, int) and self.max_features < n_features: features = np.random.choice(n_features, self.max_features, replace=False) elif isinstance(self.max_features, float): k = int(n_features * self.max_features) features = np.random.choice(n_features, k, replace=False) for feat in features: X_col = X[:, feat] unique_vals = np.unique(X_col) if len(unique_vals) <= 1: continue thresholds = (unique_vals[:-1] + unique_vals[1:]) / 2.0 for thr in thresholds: left_mask = X_col <= thr y_left, y_right = y[left_mask], y[~left_mask] if len(y_left) == 0 or len(y_right) == 0: continue # Compute sum of squared errors (SSE) left_mean, right_mean = np.mean(y_left), np.mean(y_right) sse_left = np.sum((y_left - left_mean) ** 2) sse_right = np.sum((y_right - right_mean) ** 2) sse = sse_left + sse_right if sse < best_sse: best_sse, best_feat, best_thr = sse, feat, thr if best_feat is None: leaf_val = np.mean(y) return DecisionTreeRegressor.Node(value=leaf_val) left_mask = X[:, best_feat] <= best_thr left_node = self._build_tree(X[left_mask], y[left_mask], depth+1) right_node = self._build_tree(X[~left_mask], y[~left_mask], depth+1) return DecisionTreeRegressor.Node(feature=best_feat, threshold=best_thr, left=left_node, right=right_node) def predict(self, X): X = np.array(X) return np.array([self._predict_input(x, self.tree) for x in X]) def _predict_input(self, x, node): if node.value is not None: return node.value if x[node.feature] <= node.threshold: return self._predict_input(x, node.left) else: return self._predict_input(x, node.right) # Random Forests (Classification and Regression) """ Random forests train an ensemble of decision trees on bootstrapped data subsets and average their outputs . For classification, the final class is the majority vote of all trees; for regression, the output is the average prediction . Key points: Bootstrap sampling: Each tree is trained on a random sample (with replacement) of the data. Feature randomness: When splitting, each node may consider only a random subset of features (parameter max_features). Aggregation: Classification uses mode of tree predictions; regression uses mean. This reduces overfitting compared to a single tree . """ import numpy as np from collections import Counter class RandomForestClassifier: def __init__(self, n_estimators=100, max_depth=None, min_samples_split=2, max_features='sqrt'): self.n_estimators = n_estimators self.max_depth = max_depth self.min_samples_split = min_samples_split self.max_features = max_features self.trees = [] def fit(self, X, y): X, y = np.array(X), np.array(y) n_samples, n_features = X.shape # Determine how many features to try at each split if self.max_features == 'sqrt': max_feats = int(np.sqrt(n_features)) elif self.max_features == 'log2': max_feats = int(np.log2(n_features)) elif isinstance(self.max_features, int): max_feats = self.max_features elif isinstance(self.max_features, float): max_feats = int(n_features * self.max_features) else: max_feats = n_features # Build trees for _ in range(self.n_estimators): indices = np.random.choice(n_samples, n_samples, replace=True) X_sample, y_sample = X[indices], y[indices] tree = DecisionTreeClassifier(criterion='gini', max_depth=self.max_depth, min_samples_split=self.min_samples_split, max_features=max_feats) tree.fit(X_sample, y_sample) self.trees.append(tree) return self def predict(self, X): X = np.array(X) # Collect predictions from all trees tree_preds = np.array([tree.predict(X) for tree in self.trees]).T # shape (n_samples, n_trees) y_pred = [] for preds in tree_preds: vote = Counter(preds).most_common(1)[0][0] y_pred.append(vote) return np.array(y_pred) class RandomForestRegressor: def __init__(self, n_estimators=100, max_depth=None, min_samples_split=2, max_features=None): self.n_estimators = n_estimators self.max_depth = max_depth self.min_samples_split = min_samples_split self.max_features = max_features self.trees = [] def fit(self, X, y): X, y = np.array(X), np.array(y, dtype=float) n_samples, n_features = X.shape if self.max_features == 'sqrt': max_feats = int(np.sqrt(n_features)) elif self.max_features == 'log2': max_feats = int(np.log2(n_features)) elif isinstance(self.max_features, int): max_feats = self.max_features elif isinstance(self.max_features, float): max_feats = int(n_features * self.max_features) else: max_feats = n_features for _ in range(self.n_estimators): indices = np.random.choice(n_samples, n_samples, replace=True) X_sample, y_sample = X[indices], y[indices] tree = DecisionTreeRegressor(max_depth=self.max_depth, min_samples_split=self.min_samples_split, max_features=max_feats) tree.fit(X_sample, y_sample) self.trees.append(tree) return self def predict(self, X): X = np.array(X) # Average predictions from all trees tree_preds = np.array([tree.predict(X) for tree in self.trees]) return np.mean(tree_preds, axis=0) # Gradient Boosting (Classification and Regression) """ Gradient boosting builds an additive ensemble of trees by fitting each new tree on the residuals (errors) of the existing model, effectively performing gradient descent on a loss function . At each stage m: Compute the pseudo-residuals r_{im} = -\partial L(y_i, F(x_i)) / \partial F(x_i) (the negative gradient) . Fit a tree h_m(x) to these residuals. Update the model: F_m(x) = F_{m-1}(x) + \gamma_m \, h_m(x) (with line-search multiplier \gamma_m or simply a learning rate). For binary classification, we use the logistic loss: initialize F_0 = \log(p/(1-p)) (log-odds of positive class) and repeatedly fit trees to y - \sigma(F). For multiclass, we fit one-vs-rest models (one boosting ensemble per class) and predict the class with highest score. Key concepts: Additive updates: Each tree’s prediction is scaled by a learning rate and added to the ensemble output. Regression loss: Typically squared-error (L2) for regression, logistic (cross-entropy) for classification. Weak learners: Trees are often shallow (small max_depth). """ import numpy as np from math import log, exp class GradientBoostingRegressor: def __init__(self, n_estimators=100, learning_rate=0.1, max_depth=3): self.n_estimators = n_estimators self.learning_rate = learning_rate self.max_depth = max_depth self.trees = [] self.gammas = [] self.initial_prediction = None def fit(self, X, y): X, y = np.array(X), np.array(y, dtype=float) # Initialize prediction with mean self.initial_prediction = np.mean(y) F = np.full_like(y, fill_value=self.initial_prediction, dtype=float) for _ in range(self.n_estimators): residual = y - F tree = DecisionTreeRegressor(max_depth=self.max_depth) tree.fit(X, residual) pred = tree.predict(X) # Line search for optimal multiplier gamma gamma = np.dot(residual, pred) / (np.dot(pred, pred) + 1e-8) F = F + self.learning_rate * gamma * pred self.trees.append(tree) self.gammas.append(gamma) return self def predict(self, X): X = np.array(X) F = np.full(X.shape[0], fill_value=self.initial_prediction, dtype=float) for tree, gamma in zip(self.trees, self.gammas): F += self.learning_rate * gamma * tree.predict(X) return F class GradientBoostingClassifier: def __init__(self, n_estimators=100, learning_rate=0.1, max_depth=3): self.n_estimators = n_estimators self.learning_rate = learning_rate self.max_depth = max_depth self.models = [] # For multiclass, one model per class def fit(self, X, y): X, y = np.array(X), np.array(y) self.classes_ = np.unique(y) if len(self.classes_) <= 2: # Binary classification (labels may be 0/1 or not) # Map labels to 0/1 if set(self.classes_) != {0, 1}: class0, class1 = self.classes_[0], self.classes_[1] y_bin = np.array([0 if yi==class0 else 1 for yi in y]) self.class_map = {0: class0, 1: class1} else: y_bin = y self.class_map = None # Initialize log-odds p = np.clip(np.mean(y_bin), 1e-6, 1-1e-6) F = np.full(y_bin.shape, fill_value=log(p/(1-p)), dtype=float) self.initial_F = F[0] self.trees = [] for _ in range(self.n_estimators): P = 1 / (1 + np.exp(-F)) residual = y_bin - P tree = DecisionTreeRegressor(max_depth=self.max_depth) tree.fit(X, residual) pred = tree.predict(X) F = F + self.learning_rate * pred self.trees.append(tree) else: # Multiclass one-vs-rest for cls in self.classes_: y_binary = (y == cls).astype(int) model = GradientBoostingClassifier(n_estimators=self.n_estimators, learning_rate=self.learning_rate, max_depth=self.max_depth) model.fit(X, y_binary) self.models.append(model) return self def predict(self, X): X = np.array(X) if len(self.classes_) <= 2: # Binary F = np.full(X.shape[0], fill_value=self.initial_F, dtype=float) for tree in self.trees: F += self.learning_rate * tree.predict(X) P = 1 / (1 + np.exp(-F)) y_pred = (P >= 0.5).astype(int) if self.class_map: inv_map = {v:k for k,v in self.class_map.items()} y_pred = np.array([inv_map[val] for val in y_pred]) return y_pred else: # Multiclass: compute score for each class scores = [] for model in self.models: F_cls = np.full(X.shape[0], fill_value=model.initial_F, dtype=float) for tree in model.trees: F_cls += self.learning_rate * tree.predict(X) scores.append(F_cls) scores = np.vstack(scores).T # shape (n_samples, n_classes) class_idx = np.argmax(scores, axis=1) return np.array([self.classes_[i] for i in class_idx]) # Classification Example: Generate a 2-class dataset, train models, and evaluate accuracy. import numpy as np import csv # Synthetic binary classification data np.random.seed(0) N = 100 # Class 0 centered at (-2, -2), Class 1 at (2, 2) X0 = np.random.randn(N, 2) - 2 X1 = np.random.randn(N, 2) + 2 X_clf = np.vstack([X0, X1]) y_clf = np.array([0]*N + [1]*N) # Shuffle data perm = np.random.permutation(len(y_clf)) X_clf, y_clf = X_clf[perm], y_clf[perm] # Train models dt_clf = DecisionTreeClassifier(max_depth=3) dt_clf.fit(X_clf, y_clf) rf_clf = RandomForestClassifier(n_estimators=10, max_depth=3) rf_clf.fit(X_clf, y_clf) gb_clf = GradientBoostingClassifier(n_estimators=20, learning_rate=0.1, max_depth=2) gb_clf.fit(X_clf, y_clf) # Predictions pred_dt = dt_clf.predict(X_clf) pred_rf = rf_clf.predict(X_clf) pred_gb = gb_clf.predict(X_clf) # Accuracy evaluation acc_dt = np.mean(pred_dt == y_clf) acc_rf = np.mean(pred_rf == y_clf) acc_gb = np.mean(pred_gb == y_clf) print(f"Decision Tree Accuracy: {acc_dt:.2f}") print(f"Random Forest Accuracy: {acc_rf:.2f}") print(f"Gradient Boosting Accuracy: {acc_gb:.2f}") # Export to CSV with open('classification_results.csv', 'w', newline='') as f: writer = csv.writer(f) writer.writerow(['GroundTruth', 'DT_Pred', 'RF_Pred', 'GB_Pred']) for true, d, r, g in zip(y_clf, pred_dt, pred_rf, pred_gb): writer.writerow([true, d, r, g]) # Regression Example: Generate a simple regression dataset, train models, and compute MSE. import numpy as np import csv # Synthetic regression data: y = 3*x1 - 2*x2 + noise np.random.seed(1) N = 200 X_reg = np.random.randn(N, 2) y_reg = 3 * X_reg[:,0] - 2 * X_reg[:,1] + np.random.randn(N) * 0.5 # Train models dt_reg = DecisionTreeRegressor(max_depth=4) dt_reg.fit(X_reg, y_reg) rf_reg = RandomForestRegressor(n_estimators=10, max_depth=4) rf_reg.fit(X_reg, y_reg) gb_reg = GradientBoostingRegressor(n_estimators=50, learning_rate=0.1, max_depth=2) gb_reg.fit(X_reg, y_reg) # Predictions pred_dt_r = dt_reg.predict(X_reg) pred_rf_r = rf_reg.predict(X_reg) pred_gb_r = gb_reg.predict(X_reg) # MSE evaluation mse_dt = np.mean((pred_dt_r - y_reg)**2) mse_rf = np.mean((pred_rf_r - y_reg)**2) mse_gb = np.mean((pred_gb_r - y_reg)**2) print(f"Decision Tree MSE: {mse_dt:.3f}") print(f"Random Forest MSE: {mse_rf:.3f}") print(f"Gradient Boosting MSE: {mse_gb:.3f}") # Export to CSV with open('regression_results.csv', 'w', newline='') as f: writer = csv.writer(f) writer.writerow(['GroundTruth', 'DT_Pred', 'RF_Pred', 'GB_Pred']) for true, d, r, g in zip(y_reg, pred_dt_r, pred_rf_r, pred_gb_r): writer.writerow([true, d, r, g])