An imbalanced target can reward a classifier for favoring the common label while it misses most rare cases. Class weighting changes how strongly each training row contributes to the model's loss, giving the minority label more influence without duplicating or discarding observations.
Scikit-learn's LogisticRegression accepts explicit class weights through class_weight. compute_class_weight() derives inverse-frequency values from the training labels, and a stratified split keeps the class proportions comparable while leaving the test labels outside that calculation.
The weighted fit should be compared with an otherwise identical unweighted baseline on held-out data. Minority recall shows how many rare cases the model finds, while balanced accuracy averages recall across both classes so the majority label cannot dominate the score.
from collections import Counter import numpy as np from sklearn.datasets import make_classification from sklearn.linear_model import LogisticRegression from sklearn.metrics import balanced_accuracy_score, recall_score from sklearn.model_selection import train_test_split from sklearn.utils.class_weight import compute_class_weight X, y = make_classification( n_samples=1200, n_features=6, n_informative=4, n_redundant=0, weights=[0.92, 0.08], class_sep=0.65, flip_y=0.02, random_state=42, ) X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.25, stratify=y, random_state=42, )
Project features and labels can replace X and y. Weighting remains based on y_train so test labels do not influence the fitted model.
classes = np.unique(y_train) weights = compute_class_weight( class_weight="balanced", classes=classes, y=y_train, ) weight_map = { int(label): float(weight) for label, weight in zip(classes, weights) } display_weights = { label: round(weight, 3) for label, weight in weight_map.items() } class_counts = { int(label): int(count) for label, count in sorted(Counter(y_train).items()) }
models = { "unweighted": LogisticRegression(max_iter=1000), "weighted": LogisticRegression( class_weight=weight_map, max_iter=1000, ), }
Both estimators keep the same solver settings, so class_weight is the only fitting difference in this comparison.
print(f"Training class counts: {class_counts}") print(f"Applied class weights: {display_weights}") for name, model in models.items(): model.fit(X_train, y_train) predictions = model.predict(X_test) balanced_accuracy = balanced_accuracy_score(y_test, predictions) minority_recall = recall_score(y_test, predictions, pos_label=1) print( f"{name}: balanced_accuracy={balanced_accuracy:.3f}, " f"minority_recall={minority_recall:.3f}" )
$ python train_weighted_classifier.py
Training class counts: {0: 817, 1: 83}
Applied class weights: {0: 0.551, 1: 5.422}
unweighted: balanced_accuracy=0.550, minority_recall=0.107
weighted: balanced_accuracy=0.589, minority_recall=0.429
The minority label receives the larger weight, and the weighted model raises minority recall in this run. Cross-validation and project-specific error costs determine whether the tradeoff is suitable for production.