Μηχανική Μάθηση στην Python: Πλήρης Οδηγός με Scikit-Learn, Pipelines και Πρακτικά Παραδείγματα

Πλήρης οδηγός μηχανικής μάθησης στην Python με scikit-learn 1.8: παλινδρόμηση, ταξινόμηση, Pipelines, XGBoost, LightGBM, hyperparameter tuning και ολοκληρωμένο end-to-end παράδειγμα πρόβλεψης έγκρισης δανείου.

Αν μας ακολουθείτε σε αυτή τη σειρά οδηγών, έχετε ήδη κάνει αρκετά σημαντικά βήματα. Ξεκινήσαμε με τον πλήρη οδηγό του pandas 3.0 — Copy-on-Write, νέοι τύποι δεδομένων, εκφράσεις pd.col(). Μετά, βουτήξαμε στον καθαρισμό δεδομένων με Pandas, Scikit-Learn και Pyjanitor: ελλιπείς τιμές, διπλότυπα, μετασχηματισμοί, pipelines προεπεξεργασίας. Και στη συνέχεια, μάθαμε να οπτικοποιούμε τα δεδομένα μας με Matplotlib και Seaborn — γραφήματα που αποκαλύπτουν μοτίβα, ανωμαλίες και ιστορίες κρυμμένες στους αριθμούς.

Τώρα λοιπόν, έρχεται το επόμενο φυσικό βήμα: να διδάξουμε τον υπολογιστή να βρίσκει μόνος του αυτά τα μοτίβα. Να κάνει προβλέψεις. Να ταξινομεί. Να μαθαίνει από τα δεδομένα. Αυτή είναι η μηχανική μάθηση — και το εργαλείο που θα χρησιμοποιήσουμε είναι το scikit-learn, η πιο δημοφιλής βιβλιοθήκη μηχανικής μάθησης στην Python.

Σε αυτόν τον οδηγό θα καλύψουμε τα πάντα: από τα βασικά (τι είναι η επιβλεπόμενη μάθηση, πώς χωρίζουμε τα δεδομένα) μέχρι τα πιο προχωρημένα (Pipelines, Gradient Boosting με XGBoost και LightGBM, hyperparameter tuning, cross-validation). Θα δουλέψουμε με πρακτικά παραδείγματα κώδικα σε κάθε βήμα, και θα ρίξουμε μια ματιά στα νέα χαρακτηριστικά του scikit-learn 1.8 που κυκλοφόρησε τον Δεκέμβριο του 2025 — από υποστήριξη Array API για GPU computing μέχρι free-threaded Python.

Ας ξεκινήσουμε λοιπόν.

Ρύθμιση Περιβάλλοντος

Πρώτα απ' όλα, χρειαζόμαστε τα σωστά εργαλεία. Αν ακολουθήσατε τους προηγούμενους οδηγούς, πιθανότατα έχετε ήδη εγκατεστημένα τα pandas, numpy, matplotlib και seaborn. Τώρα προσθέτουμε το βασικό ML stack:

# Εγκατάσταση βασικού ML stack
pip install scikit-learn==1.8.0
pip install xgboost lightgbm

# Αν θέλετε GPU υποστήριξη μέσω Array API
pip install cupy-cuda12x  # για NVIDIA GPUs
pip install torch          # εναλλακτικά μέσω PyTorch

Και τα imports που θα χρησιμοποιούμε σε ολόκληρο τον οδηγό:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

# Scikit-Learn: βασικά modules
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.metrics import (
    mean_squared_error, r2_score, mean_absolute_error,
    accuracy_score, precision_score, recall_score, f1_score,
    confusion_matrix, classification_report
)

# Μοντέλα
from sklearn.linear_model import LinearRegression, LogisticRegression
from sklearn.ensemble import RandomForestClassifier, GradientBoostingRegressor
from sklearn.tree import DecisionTreeClassifier

# Gradient Boosting εξωτερικές βιβλιοθήκες
from xgboost import XGBClassifier, XGBRegressor
from lightgbm import LGBMClassifier, LGBMRegressor

# Hyperparameter Tuning
from sklearn.model_selection import GridSearchCV, RandomizedSearchCV

# Επιβεβαίωση εκδόσεων
import sklearn
print(f"scikit-learn: {sklearn.__version__}")
print(f"pandas: {pd.__version__}")
print(f"numpy: {np.__version__}")

Αν τρέχετε σε Jupyter notebook, προσθέστε στην αρχή %matplotlib inline για να εμφανίζονται τα γραφήματα inline. Κι ένα tip: δημιουργήστε ένα virtual environment αποκλειστικά για αυτόν τον οδηγό. Ειλικρινά, σας γλιτώνει πονοκεφάλους με συγκρούσεις εκδόσεων.

Βασικές Έννοιες Μηχανικής Μάθησης

Πριν γράψουμε κώδικα, ας ξεκαθαρίσουμε μερικές βασικές έννοιες. Αν τις γνωρίζετε ήδη, μπορείτε να πηδήξετε στο επόμενο κεφάλαιο — αλλά μια γρήγορη ανασκόπηση δεν βλάπτει κανέναν.

Επιβλεπόμενη vs Μη Επιβλεπόμενη Μάθηση

Στην επιβλεπόμενη μάθηση (supervised learning), τα δεδομένα μας περιλαμβάνουν μεταβλητές εισόδου (features) και τη σωστή απάντηση (target/label). Ο στόχος είναι το μοντέλο να μάθει τη σχέση μεταξύ εισόδου και εξόδου, ώστε να κάνει σωστές προβλέψεις σε δεδομένα που δεν έχει ξαναδεί. Παραδείγματα: πρόβλεψη τιμής ακινήτου (παλινδρόμηση), ταξινόμηση email ως spam ή μη (ταξινόμηση).

Στην μη επιβλεπόμενη μάθηση (unsupervised learning), δεν υπάρχει target. Ουσιαστικά ψάχνουμε μοτίβα, ομαδοποιήσεις ή δομές μέσα στα ίδια τα δεδομένα. Παραδείγματα: clustering πελατών, μείωση διαστάσεων, ανίχνευση ανωμαλιών.

Σε αυτόν τον οδηγό θα εστιάσουμε κυρίως στην επιβλεπόμενη μάθηση — παλινδρόμηση και ταξινόμηση — γιατί εκεί βρίσκεται η πλειονότητα των πρακτικών εφαρμογών. Τα unsupervised θέματα τα αφήνουμε για μελλοντικό οδηγό.

Train/Test Split: Γιατί Χωρίζουμε τα Δεδομένα

Ένα θεμελιώδες πρόβλημα: πώς αξιολογούμε αν ένα μοντέλο δουλεύει πραγματικά; Αν το δοκιμάσουμε στα ίδια δεδομένα που εκπαιδεύτηκε, θα φαίνεται τέλειο — αλλά μπορεί να αποτύχει παταγωδώς σε νέα δεδομένα. Αυτό λέγεται overfitting και, πιστέψτε με, είναι ίσως ο πιο συνηθισμένος λόγος που μοντέλα αποτυγχάνουν στην πράξη.

Η λύση είναι απλή: χωρίζουμε τα δεδομένα σε training set (συνήθως 70-80%) και test set (20-30%). Εκπαιδεύουμε στο training, αξιολογούμε στο test. Έτσι μετράμε την πραγματική ικανότητα γενίκευσης.

Bias-Variance Tradeoff

Αυτή η έννοια εξηγεί γιατί δεν υπάρχει «τέλειο» μοντέλο. Ένα πολύ απλό μοντέλο (high bias) δεν μπορεί να αποτυπώσει τα μοτίβα — κάνει underfitting. Ένα πολύ σύνθετο μοντέλο (high variance) απομνημονεύει τα training data μαζί με τον θόρυβο — κάνει overfitting.

Ο στόχος μας; Η ισορροπία. Αρκετά σύνθετο για να μαθαίνει, αρκετά απλό για να γενικεύει. Ακούγεται εύκολο στη θεωρία, αλλά στην πράξη χρειάζεται πειραματισμό.

Θα δούμε πρακτικά πώς εντοπίζουμε αν ένα μοντέλο κάνει overfitting ή underfitting στα κεφάλαια της αξιολόγησης και του cross-validation.

Πρώτο Μοντέλο: Γραμμική Παλινδρόμηση

Ας ξεκινήσουμε με το πιο κλασικό ML μοντέλο: τη γραμμική παλινδρόμηση. Θα δουλέψουμε με ένα ρεαλιστικό σενάριο — πρόβλεψη τιμής ακινήτου βάσει χαρακτηριστικών — χρησιμοποιώντας ένα συνθετικό dataset. Ομολογώ ότι τα real-world datasets είναι πιο μπερδεμένα, αλλά για εκπαιδευτικούς σκοπούς αυτό μας κάνει μια χαρά.

# Δημιουργία συνθετικού dataset ακινήτων
np.random.seed(42)
n_samples = 500

# Χαρακτηριστικά
area = np.random.uniform(40, 200, n_samples)          # τετραγωνικά μέτρα
rooms = np.random.randint(1, 6, n_samples)             # αριθμός δωματίων
age = np.random.uniform(0, 50, n_samples)              # ηλικία κτιρίου σε χρόνια
distance_center = np.random.uniform(0.5, 20, n_samples) # απόσταση από κέντρο (km)

# Τιμή (target) με κάποιο θόρυβο
price = (
    area * 2500
    + rooms * 15000
    - age * 1000
    - distance_center * 5000
    + np.random.normal(0, 20000, n_samples)  # θόρυβος
)

# Δημιουργία DataFrame
df_houses = pd.DataFrame({
    'area': area,
    'rooms': rooms,
    'age': age,
    'distance_center': distance_center,
    'price': price
})

print(df_houses.head())
print(f"\nΜέγεθος dataset: {df_houses.shape}")
print(f"Μέση τιμή: €{df_houses['price'].mean():,.0f}")

Τώρα, ας χωρίσουμε τα δεδομένα και ας εκπαιδεύσουμε το μοντέλο:

# Διαχωρισμός features (X) και target (y)
X = df_houses[['area', 'rooms', 'age', 'distance_center']]
y = df_houses['price']

# Train/Test split — 80% εκπαίδευση, 20% αξιολόγηση
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

print(f"Training set: {X_train.shape[0]} δείγματα")
print(f"Test set: {X_test.shape[0]} δείγματα")

# Εκπαίδευση μοντέλου γραμμικής παλινδρόμησης
model = LinearRegression()
model.fit(X_train, y_train)

# Προβλέψεις στο test set
y_pred = model.predict(X_test)

# Αξιολόγηση
mse = mean_squared_error(y_test, y_pred)
rmse = np.sqrt(mse)
mae = mean_absolute_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)

print(f"\n--- Μετρικές Αξιολόγησης ---")
print(f"MSE:  {mse:,.0f}")
print(f"RMSE: {rmse:,.0f}")
print(f"MAE:  {mae:,.0f}")
print(f"R²:   {r2:.4f}")

Ας κατανοήσουμε τι σημαίνει κάθε μετρική:

  • MSE (Mean Squared Error): Μέσος τετραγωνικός σφάλμα. Τιμωρεί περισσότερο τα μεγάλα σφάλματα λόγω του τετραγώνου.
  • RMSE (Root MSE): Η ρίζα του MSE — στις ίδιες μονάδες με τον target (εδώ, ευρώ). Πολύ πιο ερμηνεύσιμο στην πράξη.
  • MAE (Mean Absolute Error): Μέσο απόλυτο σφάλμα. Πιο ανθεκτικό σε ακραίες τιμές από το MSE.
  • R² (Coefficient of Determination): Πόσο καλά εξηγεί το μοντέλο τη μεταβλητότητα. 1.0 = τέλειο, 0 = δεν εξηγεί τίποτα.

Ας δούμε και τους συντελεστές — δηλαδή, τι «έμαθε» τελικά το μοντέλο:

# Συντελεστές του μοντέλου
for feature, coef in zip(X.columns, model.coef_):
    print(f"{feature}: {coef:,.2f}")
print(f"Intercept: {model.intercept_:,.2f}")

# Οπτικοποίηση: Πραγματικές vs Προβλεπόμενες τιμές
plt.figure(figsize=(8, 6))
plt.scatter(y_test, y_pred, alpha=0.5, edgecolors='k', linewidths=0.5)
plt.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()],
         'r--', lw=2, label='Τέλεια πρόβλεψη')
plt.xlabel('Πραγματική Τιμή (€)')
plt.ylabel('Προβλεπόμενη Τιμή (€)')
plt.title('Γραμμική Παλινδρόμηση: Πραγματικές vs Προβλεπόμενες Τιμές')
plt.legend()
plt.tight_layout()
plt.show()

Αν τα δεδομένα πέφτουν κοντά στην κόκκινη διαγώνιο, το μοντέλο κάνει καλή δουλειά. Αποκλίσεις δείχνουν σημεία όπου δυσκολεύεται — και αυτά τα σημεία αξίζει να τα εξετάσετε πιο προσεκτικά.

Ταξινόμηση: Από Logistic Regression μέχρι Random Forest

Η παλινδρόμηση προβλέπει αριθμητικές τιμές. Η ταξινόμηση προβλέπει κατηγορίες: ναι/όχι, spam/ham, ασθενής/υγιής. Ας δούμε πώς δουλεύει στην πράξη με ένα σενάριο που συναντάμε πολύ συχνά — πρόβλεψη αποχώρησης πελατών (customer churn).

# Συνθετικό dataset πελατών (churn prediction)
np.random.seed(42)
n_customers = 1000

tenure = np.random.uniform(1, 72, n_customers)           # μήνες πελάτης
monthly_charges = np.random.uniform(20, 120, n_customers) # μηνιαία χρέωση
support_calls = np.random.poisson(2, n_customers)          # κλήσεις υποστήριξης
contract_type = np.random.choice([0, 1, 2], n_customers)   # 0=μηνιαίο, 1=ετήσιο, 2=διετές

# Πιθανότητα αποχώρησης (λογιστική σχέση)
log_odds = (
    -2.0
    - 0.03 * tenure
    + 0.02 * monthly_charges
    + 0.3 * support_calls
    - 0.8 * contract_type
    + np.random.normal(0, 0.5, n_customers)
)
churn_prob = 1 / (1 + np.exp(-log_odds))
churn = (churn_prob > 0.5).astype(int)

df_churn = pd.DataFrame({
    'tenure': tenure,
    'monthly_charges': monthly_charges,
    'support_calls': support_calls,
    'contract_type': contract_type,
    'churn': churn
})

print(f"Κατανομή churn:\n{df_churn['churn'].value_counts()}")
print(f"Ποσοστό αποχώρησης: {df_churn['churn'].mean():.1%}")

Ωραία. Ας εκπαιδεύσουμε τώρα τρία μοντέλα και ας τα συγκρίνουμε μεταξύ τους:

# Προετοιμασία δεδομένων
X = df_churn[['tenure', 'monthly_charges', 'support_calls', 'contract_type']]
y = df_churn['churn']

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# Κανονικοποίηση (σημαντική για Logistic Regression)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

# --- Μοντέλο 1: Logistic Regression ---
lr = LogisticRegression(random_state=42, max_iter=1000)
lr.fit(X_train_scaled, y_train)
y_pred_lr = lr.predict(X_test_scaled)

# --- Μοντέλο 2: Decision Tree ---
dt = DecisionTreeClassifier(random_state=42, max_depth=5)
dt.fit(X_train, y_train)  # Δεν χρειάζεται scaling
y_pred_dt = dt.predict(X_test)

# --- Μοντέλο 3: Random Forest ---
rf = RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=-1)
rf.fit(X_train, y_train)
y_pred_rf = rf.predict(X_test)

# Σύγκριση αποτελεσμάτων
models = {
    'Logistic Regression': y_pred_lr,
    'Decision Tree': y_pred_dt,
    'Random Forest': y_pred_rf
}

print(f"{'Μοντέλο':<25} {'Accuracy':>10} {'Precision':>10} {'Recall':>10} {'F1':>10}")
print("-" * 65)

for name, y_pred in models.items():
    acc = accuracy_score(y_test, y_pred)
    prec = precision_score(y_test, y_pred)
    rec = recall_score(y_test, y_pred)
    f1 = f1_score(y_test, y_pred)
    print(f"{name:<25} {acc:>10.4f} {prec:>10.4f} {rec:>10.4f} {f1:>10.4f}")

Ας κατανοήσουμε τις μετρικές ταξινόμησης, γιατί δεν είναι όλες ίδιες:

  • Accuracy: Ποσοστό σωστών προβλέψεων συνολικά. Προσοχή όμως: μπορεί να είναι παραπλανητικό σε ασύμμετρα datasets!
  • Precision: Από αυτά που προβλέψαμε ως θετικά, πόσα ήταν πράγματι θετικά;
  • Recall: Από τα πραγματικά θετικά, πόσα καταφέραμε να βρούμε;
  • F1 Score: Αρμονικός μέσος Precision και Recall — πολύ χρήσιμο για ασύμμετρα datasets.

Κοιτάξτε τώρα τον confusion matrix για το καλύτερο μοντέλο:

# Confusion Matrix για Random Forest
cm = confusion_matrix(y_test, y_pred_rf)

plt.figure(figsize=(6, 5))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
            xticklabels=['Παραμένει', 'Αποχωρεί'],
            yticklabels=['Παραμένει', 'Αποχωρεί'])
plt.xlabel('Πρόβλεψη')
plt.ylabel('Πραγματικό')
plt.title('Confusion Matrix — Random Forest')
plt.tight_layout()
plt.show()

# Αναλυτική αναφορά ταξινόμησης
print(classification_report(y_test, y_pred_rf,
      target_names=['Παραμένει', 'Αποχωρεί']))

Ο confusion matrix είναι ένα πολύ ισχυρό εργαλείο: δείχνει πού ακριβώς κάνει λάθη το μοντέλο. Σε σενάρια churn, για παράδειγμα, ένα false negative (πελάτης που φεύγει αλλά δεν τον προβλέψαμε) είναι πολύ πιο κοστοβόρο από ένα false positive. Αυτό είναι κάτι που πρέπει να το λαμβάνετε σοβαρά υπόψη στην επιλογή μετρικής αξιολόγησης — δεν είναι απλά θεωρητική λεπτομέρεια.

Scikit-Learn Pipelines: Η Καρδιά της Παραγωγικής ML

Μέχρι τώρα κάναμε τα βήματα ξεχωριστά: scaling, εκπαίδευση, πρόβλεψη. Στην πράξη όμως, αυτό είναι επικίνδυνο. Γιατί; Δύο λόγοι κυρίως:

  1. Data leakage: Αν κάνετε fit τον scaler σε ολόκληρο το dataset πριν το split, ο scaler «βλέπει» πληροφορίες από το test set. Τα αποτελέσματα φαίνονται καλύτερα απ' ό,τι είναι στην πραγματικότητα.
  2. Αναπαραγωγιμότητα: Σε production, πρέπει να εφαρμόσετε τα ίδια ακριβώς βήματα με την ίδια σειρά. Ξεχωριστά βήματα σημαίνει ξεχωριστά λάθη.

Η λύση: Pipelines. Ένα Pipeline ενώνει όλα τα βήματα σε ένα αντικείμενο. Κάνετε fit μία φορά, predict μία φορά — τα πάντα γίνονται αυτόματα στη σωστή σειρά. Ειλικρινά, αφότου άρχισα να τα χρησιμοποιώ δεν γυρνάω πίσω.

# Απλό Pipeline: Scaling + Logistic Regression
simple_pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('classifier', LogisticRegression(random_state=42, max_iter=1000))
])

# Fit στο training set — ο scaler μαθαίνει ΜΟΝΟ από το training
simple_pipeline.fit(X_train, y_train)

# Predict στο test set — ο scaler εφαρμόζει transform (ΟΧΙ fit) αυτόματα
y_pred_pipe = simple_pipeline.predict(X_test)

print(f"Accuracy με Pipeline: {accuracy_score(y_test, y_pred_pipe):.4f}")

Τώρα ας δούμε κάτι πιο ρεαλιστικό. Στην πράξη, τα δεδομένα σχεδόν πάντα έχουν διαφορετικούς τύπους στηλών: αριθμητικές που χρειάζονται scaling, κατηγορικές που χρειάζονται encoding. Εδώ μπαίνει στο παιχνίδι ο ColumnTransformer:

# Δημιουργία dataset με μικτούς τύπους
df_mixed = df_churn.copy()
df_mixed['contract_type'] = df_mixed['contract_type'].map({
    0: 'monthly', 1: 'annual', 2: 'biennial'
})

X_mixed = df_mixed[['tenure', 'monthly_charges', 'support_calls', 'contract_type']]
y_mixed = df_mixed['churn']

X_train_m, X_test_m, y_train_m, y_test_m = train_test_split(
    X_mixed, y_mixed, test_size=0.2, random_state=42, stratify=y_mixed
)

# Ορισμός στηλών κατά τύπο
numeric_features = ['tenure', 'monthly_charges', 'support_calls']
categorical_features = ['contract_type']

# Preprocessor: διαφορετική επεξεργασία ανά τύπο στήλης
preprocessor = ColumnTransformer(
    transformers=[
        ('num', Pipeline([
            ('imputer', SimpleImputer(strategy='median')),
            ('scaler', StandardScaler())
        ]), numeric_features),
        ('cat', Pipeline([
            ('imputer', SimpleImputer(strategy='most_frequent')),
            ('encoder', OneHotEncoder(handle_unknown='ignore', sparse_output=False))
        ]), categorical_features)
    ]
)

# Πλήρες Pipeline: Preprocessing + Μοντέλο
full_pipeline = Pipeline([
    ('preprocessor', preprocessor),
    ('classifier', RandomForestClassifier(n_estimators=100, random_state=42))
])

# Ένα fit, μία πρόβλεψη — τα πάντα μαζί
full_pipeline.fit(X_train_m, y_train_m)
y_pred_full = full_pipeline.predict(X_test_m)

print(f"Accuracy πλήρους Pipeline: {accuracy_score(y_test_m, y_pred_full):.4f}")
print(f"F1 Score: {f1_score(y_test_m, y_pred_full):.4f}")

Αυτό εδώ είναι η καρδιά της παραγωγικής ML. Ο ColumnTransformer σας επιτρέπει να εφαρμόζετε διαφορετικούς μετασχηματισμούς σε διαφορετικές στήλες, και το Pipeline τα δένει όλα σε μία αλυσίδα.

Αν αυτό ήταν το μόνο πράγμα που θα κρατούσατε από αυτόν τον οδηγό, θα ήταν αρκετό. Σοβαρά.

Gradient Boosting: XGBoost & LightGBM

Τα μοντέλα Gradient Boosting είναι σήμερα τα πιο δημοφιλή τόσο σε competitions όσο και σε παραγωγικά συστήματα. Η βασική ιδέα: αντί για ένα μεγάλο δέντρο, χτίζουμε πολλά μικρά δέντρα, το καθένα διορθώνοντας τα σφάλματα του προηγούμενου. Κομψό, σωστά;

Δύο βιβλιοθήκες κυριαρχούν εδώ: XGBoost και LightGBM. Και οι δύο ενσωματώνονται πλήρως στο scikit-learn API — μπαίνουν κατευθείαν σε Pipelines χωρίς κανένα πρόβλημα.

# XGBoost μέσα σε Pipeline
xgb_pipeline = Pipeline([
    ('preprocessor', preprocessor),
    ('classifier', XGBClassifier(
        n_estimators=200,
        max_depth=5,
        learning_rate=0.1,
        random_state=42,
        eval_metric='logloss',
        n_jobs=-1
    ))
])

xgb_pipeline.fit(X_train_m, y_train_m)
y_pred_xgb = xgb_pipeline.predict(X_test_m)

# LightGBM μέσα σε Pipeline
lgbm_pipeline = Pipeline([
    ('preprocessor', preprocessor),
    ('classifier', LGBMClassifier(
        n_estimators=200,
        max_depth=5,
        learning_rate=0.1,
        random_state=42,
        verbose=-1,
        n_jobs=-1
    ))
])

lgbm_pipeline.fit(X_train_m, y_train_m)
y_pred_lgbm = lgbm_pipeline.predict(X_test_m)

# Σύγκριση
print(f"{'Μοντέλο':<20} {'Accuracy':>10} {'F1':>10}")
print("-" * 42)
print(f"{'Random Forest':<20} {accuracy_score(y_test_m, y_pred_full):>10.4f} "
      f"{f1_score(y_test_m, y_pred_full):>10.4f}")
print(f"{'XGBoost':<20} {accuracy_score(y_test_m, y_pred_xgb):>10.4f} "
      f"{f1_score(y_test_m, y_pred_xgb):>10.4f}")
print(f"{'LightGBM':<20} {accuracy_score(y_test_m, y_pred_lgbm):>10.4f} "
      f"{f1_score(y_test_m, y_pred_lgbm):>10.4f}")

Μερικές πρακτικές σημειώσεις για τη σύγκριση αυτών των δύο:

  • XGBoost: Ώριμο, σταθερό, με εξαιρετική τεκμηρίωση. Υποστηρίζει GPU εγγενώς. Ιδανικό για tabular data σε production.
  • LightGBM: Ταχύτερο στην εκπαίδευση, ειδικά σε μεγάλα datasets. Χρησιμοποιεί histogram-based splitting. Εξαιρετικό σε datasets με πολλές κατηγορικές μεταβλητές.
  • Πρακτική συμβουλή: Δοκιμάστε και τα δύο. Στο 2026, και τα δύο είναι εξαιρετικά και η διαφορά απόδοσης είναι συχνά αμελητέα. Η τελική επιλογή μπορεί να γίνει βάσει ταχύτητας εκπαίδευσης ή ευκολίας hyperparameter tuning.

Υπερπαραμετροποίηση (Hyperparameter Tuning)

Κάθε μοντέλο έχει υπερπαραμέτρους — ρυθμίσεις που δεν μαθαίνει μόνο του, αλλά τις ορίζουμε εμείς πριν την εκπαίδευση. Πόσα δέντρα; Τι βάθος; Τι learning rate; Η σωστή επιλογή κάνει τεράστια διαφορά στο αποτέλεσμα.

GridSearchCV: Εξαντλητική Αναζήτηση

Ο GridSearchCV δοκιμάζει κάθε δυνατό συνδυασμό παραμέτρων που του δίνουμε, χρησιμοποιώντας cross-validation:

# GridSearchCV σε Pipeline
param_grid = {
    'classifier__n_estimators': [100, 200, 300],
    'classifier__max_depth': [3, 5, 7, 10],
    'classifier__learning_rate': [0.01, 0.05, 0.1, 0.2]
}

grid_search = GridSearchCV(
    xgb_pipeline,
    param_grid,
    cv=5,                  # 5-fold cross-validation
    scoring='f1',          # Μετρική βελτιστοποίησης
    n_jobs=-1,             # Παράλληλη εκτέλεση
    verbose=1
)

grid_search.fit(X_train_m, y_train_m)

print(f"Καλύτεροι υπερπαράμετροι: {grid_search.best_params_}")
print(f"Καλύτερο F1 (CV): {grid_search.best_score_:.4f}")

# Αξιολόγηση στο test set
y_pred_tuned = grid_search.predict(X_test_m)
print(f"F1 στο test set: {f1_score(y_test_m, y_pred_tuned):.4f}")

Το πρόβλημα; Αν έχετε πολλές παραμέτρους, ο αριθμός συνδυασμών εκρήγνυται. Στο παράδειγμα πάνω: 3 x 4 x 4 = 48 συνδυασμοί x 5 folds = 240 εκπαιδεύσεις. Και αυτό με σχετικά λίγες παραμέτρους.

RandomizedSearchCV: Πιο Αποδοτικός

Ο RandomizedSearchCV δοκιμάζει ένα τυχαίο υποσύνολο συνδυασμών — πιο γρήγορος και, κατά κανόνα, εξίσου αποτελεσματικός:

from scipy.stats import uniform, randint

# RandomizedSearchCV — ευρύτερος χώρος αναζήτησης
param_distributions = {
    'classifier__n_estimators': randint(50, 500),
    'classifier__max_depth': randint(2, 15),
    'classifier__learning_rate': uniform(0.001, 0.3),
    'classifier__subsample': uniform(0.6, 0.4),
    'classifier__colsample_bytree': uniform(0.6, 0.4),
    'classifier__min_child_weight': randint(1, 10)
}

random_search = RandomizedSearchCV(
    xgb_pipeline,
    param_distributions,
    n_iter=50,             # 50 τυχαίοι συνδυασμοί
    cv=5,
    scoring='f1',
    random_state=42,
    n_jobs=-1,
    verbose=1
)

random_search.fit(X_train_m, y_train_m)

print(f"Καλύτεροι υπερπαράμετροι: {random_search.best_params_}")
print(f"Καλύτερο F1 (CV): {random_search.best_score_:.4f}")
print(f"F1 στο test set: {f1_score(y_test_m, random_search.predict(X_test_m)):.4f}")

Η εμπειρία μου δείχνει ότι ο RandomizedSearchCV με 50-100 επαναλήψεις βρίσκει σχεδόν πάντα αποτελέσματα εξίσου καλά με τον εξαντλητικό GridSearchCV, σε κλάσμα του χρόνου. Στο 2026, αυτή είναι η προεπιλεγμένη σύσταση για τους περισσότερους use cases.

Αξιολόγηση Μοντέλων & Cross-Validation

Ένα μόνο train/test split δίνει μία εκτίμηση. Αλλά αν τύχει το split να είναι «εύκολο» ή «δύσκολο», η εκτίμησή σας θα είναι παραπλανητική. Η λύση; Cross-Validation.

K-Fold Cross-Validation

Χωρίζουμε τα δεδομένα σε k ίσα τμήματα (folds). Εκπαιδεύουμε σε k-1, αξιολογούμε στο 1 που μένει. Επαναλαμβάνουμε k φορές — κάθε fold παίζει τον ρόλο του test set ακριβώς μία φορά. Τελικό αποτέλεσμα: μέσος όρος (και τυπική απόκλιση) των k σκορ.

# K-Fold Cross-Validation
cv_scores = cross_val_score(
    full_pipeline,
    X_mixed, y_mixed,
    cv=5,
    scoring='f1',
    n_jobs=-1
)

print(f"F1 scores ανά fold: {cv_scores}")
print(f"Μέσο F1: {cv_scores.mean():.4f} ± {cv_scores.std():.4f}")

Σημείωση: η τυπική απόκλιση είναι εξίσου σημαντική με τον μέσο. Μεγάλη τυπική απόκλιση σημαίνει ασταθές μοντέλο — η απόδοσή του εξαρτάται πολύ από τα συγκεκριμένα δεδομένα που του δίνεις.

Stratified K-Fold

Σε προβλήματα ταξινόμησης με ασύμμετρες κλάσεις (π.χ. 90% μη-churn, 10% churn), ο κανονικός K-Fold μπορεί να δημιουργήσει folds χωρίς καθόλου δείγματα της μειοψηφικής κλάσης. Ο Stratified K-Fold εγγυάται ότι κάθε fold έχει περίπου την ίδια αναλογία κλάσεων:

from sklearn.model_selection import StratifiedKFold

# Stratified K-Fold — σημαντικό για ασύμμετρα datasets
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

stratified_scores = cross_val_score(
    full_pipeline,
    X_mixed, y_mixed,
    cv=skf,
    scoring='f1',
    n_jobs=-1
)

print(f"Stratified F1: {stratified_scores.mean():.4f} ± {stratified_scores.std():.4f}")

Learning Curves: Διάγνωση Overfitting/Underfitting

Οι learning curves δείχνουν πώς αλλάζει η απόδοση καθώς αυξάνονται τα training data. Αν το training score είναι πολύ υψηλό αλλά το validation χαμηλό, έχετε overfitting. Αν και τα δύο κολλάνε σε χαμηλά επίπεδα, underfitting.

from sklearn.model_selection import learning_curve

# Υπολογισμός learning curves
train_sizes, train_scores, val_scores = learning_curve(
    full_pipeline,
    X_mixed, y_mixed,
    train_sizes=np.linspace(0.1, 1.0, 10),
    cv=5,
    scoring='f1',
    n_jobs=-1
)

# Οπτικοποίηση
train_mean = train_scores.mean(axis=1)
train_std = train_scores.std(axis=1)
val_mean = val_scores.mean(axis=1)
val_std = val_scores.std(axis=1)

plt.figure(figsize=(10, 6))
plt.plot(train_sizes, train_mean, 'o-', color='blue', label='Training score')
plt.fill_between(train_sizes, train_mean - train_std, train_mean + train_std,
                 alpha=0.15, color='blue')
plt.plot(train_sizes, val_mean, 'o-', color='red', label='Validation score')
plt.fill_between(train_sizes, val_mean - val_std, val_mean + val_std,
                 alpha=0.15, color='red')
plt.xlabel('Μέγεθος Training Set')
plt.ylabel('F1 Score')
plt.title('Learning Curves — Random Forest')
plt.legend(loc='lower right')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

Αν οι δύο καμπύλες συγκλίνουν σε υψηλό σκορ, τέλεια — το μοντέλο γενικεύει καλά. Αν υπάρχει μεγάλο κενό μεταξύ τους, σκεφτείτε: χρειάζεστε περισσότερα δεδομένα (αν overfitting) ή πιο σύνθετο μοντέλο (αν underfitting).

Νέα Χαρακτηριστικά scikit-learn 1.8

Τον Δεκέμβριο του 2025 κυκλοφόρησε το scikit-learn 1.8 — μια αρκετά σημαντική αναβάθμιση που φέρνει τη βιβλιοθήκη πιο κοντά στη σύγχρονη εποχή. Ας δούμε τα highlights.

Array API: GPU Computing χωρίς Αλλαγές Κώδικα

Αυτή ίσως είναι η μεγαλύτερη αλλαγή: πολλοί estimators υποστηρίζουν τώρα το Array API, που σημαίνει ότι μπορείτε να τρέξετε scikit-learn σε GPU χωρίς να αλλάξετε ούτε μία γραμμή κώδικα στο μοντέλο σας. Απλώς περνάτε δεδομένα ως CuPy arrays ή PyTorch tensors αντί για NumPy arrays.

# GPU υποστήριξη μέσω Array API (απαιτεί CuPy + NVIDIA GPU)
import sklearn
sklearn.set_config(array_api_dispatch=True)

# Αν έχετε CuPy εγκατεστημένο
import cupy as cp

# Μετατροπή σε CuPy arrays (GPU memory)
X_train_gpu = cp.asarray(X_train.values)
y_train_gpu = cp.asarray(y_train.values)
X_test_gpu = cp.asarray(X_test.values)

# Ο ΙΔΙΟΣ κώδικας — τρέχει στην GPU!
from sklearn.linear_model import LogisticRegression
model_gpu = LogisticRegression(random_state=42)
model_gpu.fit(X_train_gpu, y_train_gpu)
y_pred_gpu = model_gpu.predict(X_test_gpu)

# Αποτελέσματα πίσω σε CPU
print(f"Accuracy (GPU): {float(accuracy_score(y_train_gpu, model_gpu.predict(X_train_gpu))):.4f}")

Αυτό αλλάζει πολλά. Μέχρι πρόσφατα, αν θέλατε GPU acceleration στο scikit-learn, χρειαζόσαστε cuML ή κάποια άλλη βιβλιοθήκη. Πλέον, η ίδια η βιβλιοθήκη μπορεί να εκμεταλλευτεί GPU hardware μέσω του Array API standard.

Temperature Scaling για Calibration

Πολλά μοντέλα δίνουν «πιθανότητες» που δεν είναι στ' αλήθεια βαθμονομημένες. Δηλαδή, ένα μοντέλο μπορεί να λέει «80% πιθανότητα churn» αλλά στην πράξη μόνο το 60% αυτών αποχωρούν. Η Temperature Scaling στο scikit-learn 1.8 βοηθάει να διορθώσουμε αυτό:

from sklearn.calibration import CalibratedClassifierCV

# Temperature Scaling — νέα μέθοδος στο 1.8
calibrated_model = CalibratedClassifierCV(
    estimator=rf,
    method='sigmoid',   # Platt scaling
    cv=5
)
calibrated_model.fit(X_train, y_train)

# Πιθανότητες πλέον βαθμονομημένες
probas_uncalibrated = rf.predict_proba(X_test)[:, 1]
probas_calibrated = calibrated_model.predict_proba(X_test)[:, 1]

print(f"Μέση πιθανότητα (πριν): {probas_uncalibrated.mean():.3f}")
print(f"Μέση πιθανότητα (μετά): {probas_calibrated.mean():.3f}")
print(f"Πραγματικό ποσοστό churn: {y_test.mean():.3f}")

Free-Threaded Python (No-GIL)

Το scikit-learn 1.8 υποστηρίζει πλέον free-threaded Python (PEP 703) — δηλαδή Python χωρίς το περιβόητο Global Interpreter Lock. Αληθινό parallelism σε Python level, χωρίς τους περιορισμούς του GIL. Αν χρησιμοποιείτε Python 3.13+ με free-threading enabled, τα n_jobs=-1 στο scikit-learn αξιοποιούν πλήρως πολλαπλούς πυρήνες χωρίς το overhead του multiprocessing.

Αυτή η αλλαγή είναι ιδιαίτερα σημαντική για workflows με εκτεταμένο cross-validation ή hyperparameter tuning — εκεί που περνάτε ώρες περιμένοντας.

ClassicalMDS για Manifold Learning

Νέα προσθήκη στο sklearn.manifold: η κλάση ClassicalMDS (Multidimensional Scaling). Χρήσιμη για μείωση διαστάσεων όταν θέλετε να διατηρήσετε τις αποστάσεις μεταξύ σημείων:

from sklearn.manifold import ClassicalMDS

# Μείωση διαστάσεων με ClassicalMDS
mds = ClassicalMDS(n_components=2, random_state=42)
X_2d = mds.fit_transform(X_train_scaled)

# Οπτικοποίηση σε 2D
plt.figure(figsize=(8, 6))
scatter = plt.scatter(X_2d[:, 0], X_2d[:, 1], c=y_train, cmap='RdYlBu',
                      alpha=0.6, edgecolors='k', linewidths=0.3)
plt.colorbar(scatter, label='Churn')
plt.xlabel('MDS Διάσταση 1')
plt.ylabel('MDS Διάσταση 2')
plt.title('ClassicalMDS — Οπτικοποίηση Δεδομένων σε 2D')
plt.tight_layout()
plt.show()

Πρακτικό Παράδειγμα End-to-End

Ώρα να ενώσουμε τα πάντα σε μία ολοκληρωμένη ροή εργασίας — από τη φόρτωση δεδομένων μέχρι ένα μοντέλο έτοιμο για production. Θα δουλέψουμε βήμα-βήμα, ακριβώς όπως θα κάνατε σε ένα πραγματικό project. Ομολογώ ότι αυτό το παράδειγμα είναι λίγο μακρύ, αλλά αξίζει τον κόπο γιατί συνδέει τα πάντα μεταξύ τους.

# ==============================================
# ΠΛΗΡΕΣ END-TO-END ML WORKFLOW
# Σενάριο: Πρόβλεψη εγκριτικής δανείου
# ==============================================

import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split, cross_val_score, StratifiedKFold
from sklearn.preprocessing import StandardScaler, OneHotEncoder, OrdinalEncoder
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, f1_score
from xgboost import XGBClassifier
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')

# --- Βήμα 1: Φόρτωση / Δημιουργία Δεδομένων ---
np.random.seed(42)
n = 2000

df = pd.DataFrame({
    'age': np.random.randint(21, 65, n),
    'income': np.random.lognormal(10.5, 0.5, n).astype(int),
    'employment_years': np.random.randint(0, 30, n),
    'loan_amount': np.random.randint(5000, 500000, n),
    'credit_score': np.random.randint(300, 850, n),
    'education': np.random.choice(
        ['Λύκειο', 'Πτυχίο', 'Μεταπτυχιακό', 'Διδακτορικό'], n,
        p=[0.3, 0.4, 0.2, 0.1]
    ),
    'employment_type': np.random.choice(
        ['Μισθωτός', 'Ελεύθερος Επαγγελματίας', 'Άνεργος', 'Συνταξιούχος'], n,
        p=[0.5, 0.25, 0.1, 0.15]
    ),
    'has_property': np.random.choice([0, 1], n, p=[0.4, 0.6]),
    'previous_defaults': np.random.choice([0, 1, 2, 3], n, p=[0.7, 0.15, 0.1, 0.05])
})

# Εισαγωγή ελλιπών τιμών (ρεαλιστικό)
mask = np.random.random(n) < 0.05
df.loc[mask, 'income'] = np.nan
mask2 = np.random.random(n) < 0.08
df.loc[mask2, 'credit_score'] = np.nan

# Target: έγκριση δανείου (με λογική βάση)
log_odds = (
    -3.0
    + 0.02 * df['age'].fillna(35)
    + 0.00003 * df['income'].fillna(df['income'].median())
    + 0.05 * df['employment_years']
    - 0.000005 * df['loan_amount']
    + 0.005 * df['credit_score'].fillna(650)
    + 0.5 * df['has_property']
    - 0.8 * df['previous_defaults']
)
df['approved'] = (1 / (1 + np.exp(-log_odds)) + np.random.normal(0, 0.1, n) > 0.5).astype(int)

print(f"Μέγεθος dataset: {df.shape}")
print(f"Ελλιπείς τιμές:\n{df.isnull().sum()}")
print(f"\nΚατανομή target:\n{df['approved'].value_counts(normalize=True)}")

# --- Βήμα 2: Εξερευνητική Ανάλυση (Σύντομη) ---
print(f"\n{df.describe()}")

# --- Βήμα 3: Ορισμός Features και Target ---
feature_cols = ['age', 'income', 'employment_years', 'loan_amount',
                'credit_score', 'education', 'employment_type',
                'has_property', 'previous_defaults']

X = df[feature_cols]
y = df['approved']

# --- Βήμα 4: Train/Test Split ---
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# --- Βήμα 5: Ορισμός Preprocessing Pipeline ---
numeric_features = ['age', 'income', 'employment_years',
                    'loan_amount', 'credit_score', 'has_property',
                    'previous_defaults']
categorical_features = ['education', 'employment_type']

numeric_transformer = Pipeline([
    ('imputer', SimpleImputer(strategy='median')),
    ('scaler', StandardScaler())
])

categorical_transformer = Pipeline([
    ('imputer', SimpleImputer(strategy='most_frequent')),
    ('encoder', OneHotEncoder(handle_unknown='ignore', sparse_output=False))
])

preprocessor = ColumnTransformer(
    transformers=[
        ('num', numeric_transformer, numeric_features),
        ('cat', categorical_transformer, categorical_features)
    ]
)

# --- Βήμα 6: Δημιουργία και Σύγκριση Μοντέλων ---
models = {
    'Random Forest': RandomForestClassifier(
        n_estimators=200, random_state=42, n_jobs=-1
    ),
    'XGBoost': XGBClassifier(
        n_estimators=200, max_depth=5, learning_rate=0.1,
        random_state=42, eval_metric='logloss', n_jobs=-1
    )
}

results = {}
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

for name, model in models.items():
    pipeline = Pipeline([
        ('preprocessor', preprocessor),
        ('classifier', model)
    ])

    # Cross-validation στο training set
    cv_scores = cross_val_score(pipeline, X_train, y_train, cv=skf,
                                scoring='f1', n_jobs=-1)

    # Εκπαίδευση στο πλήρες training set
    pipeline.fit(X_train, y_train)
    y_pred = pipeline.predict(X_test)
    test_f1 = f1_score(y_test, y_pred)

    results[name] = {
        'cv_mean': cv_scores.mean(),
        'cv_std': cv_scores.std(),
        'test_f1': test_f1,
        'pipeline': pipeline
    }

    print(f"\n{name}:")
    print(f"  CV F1: {cv_scores.mean():.4f} ± {cv_scores.std():.4f}")
    print(f"  Test F1: {test_f1:.4f}")

# --- Βήμα 7: Επιλογή Καλύτερου Μοντέλου ---
best_model_name = max(results, key=lambda k: results[k]['test_f1'])
best_pipeline = results[best_model_name]['pipeline']
print(f"\nΚαλύτερο μοντέλο: {best_model_name}")

# --- Βήμα 8: Αναλυτική Αξιολόγηση ---
y_pred_best = best_pipeline.predict(X_test)
print(f"\nΑναλυτική Αξιολόγηση ({best_model_name}):")
print(classification_report(y_test, y_pred_best,
      target_names=['Απορρίφθηκε', 'Εγκρίθηκε']))

# --- Βήμα 9: Feature Importance ---
if hasattr(best_pipeline.named_steps['classifier'], 'feature_importances_'):
    # Ονόματα features μετά τον preprocessing
    feature_names = (numeric_features +
                    list(best_pipeline.named_steps['preprocessor']
                         .named_transformers_['cat']
                         .named_steps['encoder']
                         .get_feature_names_out(categorical_features)))

    importances = best_pipeline.named_steps['classifier'].feature_importances_

    feat_imp = pd.Series(importances, index=feature_names).sort_values(ascending=True)

    plt.figure(figsize=(10, 6))
    feat_imp.tail(10).plot(kind='barh', color='steelblue')
    plt.xlabel('Σημαντικότητα')
    plt.title(f'Top 10 Features — {best_model_name}')
    plt.tight_layout()
    plt.show()
# --- Βήμα 10: Αποθήκευση Pipeline για Production ---
import joblib

# Αποθήκευση ολόκληρου του pipeline (preprocessing + μοντέλο)
joblib.dump(best_pipeline, 'loan_approval_pipeline.joblib')
print("Pipeline αποθηκεύτηκε!")

# Φόρτωση και χρήση σε production
loaded_pipeline = joblib.load('loan_approval_pipeline.joblib')

# Νέος πελάτης — πρόβλεψη σε πραγματικό χρόνο
new_customer = pd.DataFrame({
    'age': [35],
    'income': [55000],
    'employment_years': [8],
    'loan_amount': [150000],
    'credit_score': [720],
    'education': ['Πτυχίο'],
    'employment_type': ['Μισθωτός'],
    'has_property': [1],
    'previous_defaults': [0]
})

prediction = loaded_pipeline.predict(new_customer)
probability = loaded_pipeline.predict_proba(new_customer)

print(f"\nΠρόβλεψη: {'Εγκρίνεται' if prediction[0] == 1 else 'Απορρίπτεται'}")
print(f"Πιθανότητα έγκρισης: {probability[0][1]:.1%}")

Αυτό είναι μια πλήρης ροή εργασίας ML — από ακατέργαστα δεδομένα μέχρι ένα pipeline που μπορεί να κάνει προβλέψεις σε production. Παρατηρήστε ότι κάθε βήμα ζει μέσα σε Pipeline, ώστε ολόκληρη η διαδικασία (imputation, scaling, encoding, πρόβλεψη) να γίνεται με μία μόνο κλήση. Αυτό είναι που κάνει τη διαφορά ανάμεσα σε ένα notebook experiment και ένα production-ready σύστημα.

Βέλτιστες Πρακτικές & Συμβουλές

Μετά από αρκετά projects (και αρκετά λάθη, να είμαι ειλικρινής), ορισμένα μαθήματα επαναλαμβάνονται. Εδώ είναι οι πιο σημαντικές συμβουλές:

Κανόνες που Αξίζει να Θυμάστε

  • Πάντα χρησιμοποιείτε Pipelines: Ποτέ μην κάνετε preprocessing ξεχωριστά. Τα Pipelines αποτρέπουν data leakage και εξασφαλίζουν αναπαραγωγιμότητα.
  • Μην αγνοείτε τα δεδομένα: Κανένα μοντέλο δεν μπορεί να σώσει κακά δεδομένα. Ξοδέψτε χρόνο στην εξερεύνηση και τον καθαρισμό — όπως καλύψαμε στον προηγούμενο οδηγό μας.
  • Ξεκινήστε απλά: Πριν δοκιμάσετε XGBoost, τρέξτε μια Logistic Regression ή ένα Random Forest. Αν ένα απλό μοντέλο δουλεύει καλά, μήπως δεν χρειάζεστε κάτι πιο σύνθετο;
  • Cross-validation παντού: Ποτέ μην εμπιστεύεστε ένα μόνο train/test split. Τουλάχιστον 5-fold CV.
  • Προσέξτε τις ασύμμετρες κλάσεις: Αν η μια κλάση είναι πολύ σπανιότερη, χρησιμοποιήστε F1, Precision, Recall — όχι Accuracy.
  • Βαθμονομήστε τις πιθανότητες: Αν χρησιμοποιείτε πιθανότητες για ranking ή thresholding, calibration είναι απαραίτητο.

Συνήθη Λάθη που Πρέπει να Αποφύγετε

  1. Data leakage: Fit τον scaler πριν το split, χρήση μελλοντικών δεδομένων στο training. Η λύση; Pipelines, πάντα Pipelines.
  2. Overfitting στο test set: Αν κοιτάτε το test set πολλές φορές και αλλάζετε το μοντέλο κάθε φορά, κάνετε indirect leakage. Κρατήστε ένα holdout set στο οποίο κοιτάτε μόνο στο τέλος.
  3. Αγνόηση feature engineering: Η προσθήκη σωστών features συχνά βελτιώνει περισσότερο από την αλλαγή μοντέλου. Σκεφτείτε αναλογίες (loan_amount/income), αλληλεπιδράσεις, πολυωνυμικά features.
  4. Μη αναπαραγώγιμα αποτελέσματα: Ορίζετε πάντα random_state σε κάθε στοχαστικό βήμα. Μπορεί να φαίνεται λεπτομέρεια, αλλά σας γλιτώνει πονοκεφάλους αργότερα.
  5. Χρήση accuracy σε ασύμμετρα datasets: Με 95% negative, ένα μοντέλο που λέει πάντα «no» έχει 95% accuracy. Χρησιμοποιήστε F1 ή AUC-ROC αντί γι' αυτό.

Τι Έρχεται στο ML Stack του 2026

Μερικές τάσεις που αξίζει να παρακολουθείτε:

  • GPU-accelerated ML παντού: Με το Array API στο scikit-learn και την ωρίμανση του cuML, η GPU acceleration γίνεται mainstream ακόμα και για κλασικό ML.
  • Free-threaded Python: Η αφαίρεση του GIL (Python 3.13+) αλλάζει τα δεδομένα για παράλληλη εκτέλεση — ιδιαίτερα σε hyperparameter tuning και cross-validation.
  • AutoML ωρίμανση: Εργαλεία σαν FLAML, Auto-sklearn και TPOT γίνονται πιο αξιόπιστα. Μπορούν να χρησιμοποιηθούν ως baseline ή για γρήγορο prototyping.
  • ML + LLMs: Ο συνδυασμός κλασικού ML (για tabular data) με LLMs (για text, εξηγήσεις) γίνεται ολοένα πιο συχνός σε production systems. Ενδιαφέρουσα εποχή, πράγματι.
  • MLOps και reproducibility: Εργαλεία σαν MLflow, DVC και Weights & Biases γίνονται standard — η αναπαραγωγιμότητα δεν είναι πλέον πολυτέλεια αλλά βασική απαίτηση.

Σύνοψη

Σε αυτόν τον οδηγό καλύψαμε ολόκληρο τον κύκλο ζωής ενός ML project με scikit-learn:

  • Ξεκινήσαμε με τις βασικές έννοιες — επιβλεπόμενη μάθηση, train/test split, bias-variance tradeoff.
  • Χτίσαμε ένα μοντέλο παλινδρόμησης για πρόβλεψη τιμών ακινήτων και μάθαμε τις μετρικές MSE, RMSE, MAE, R².
  • Δοκιμάσαμε ταξινόμηση με Logistic Regression, Decision Tree και Random Forest, συγκρίνοντας Accuracy, Precision, Recall και F1.
  • Ανακαλύψαμε τα Pipelines — τον πυρήνα κάθε αξιόπιστου ML workflow — μαζί με τον ColumnTransformer για μικτούς τύπους δεδομένων.
  • Ενσωματώσαμε XGBoost και LightGBM σε scikit-learn Pipelines.
  • Εφαρμόσαμε hyperparameter tuning με GridSearchCV και RandomizedSearchCV.
  • Μάθαμε cross-validation, Stratified K-Fold και learning curves για αξιόπιστη αξιολόγηση μοντέλων.
  • Εξερευνήσαμε τα νέα χαρακτηριστικά του scikit-learn 1.8: Array API, temperature scaling, free-threaded Python, ClassicalMDS.
  • Και τέλος, χτίσαμε ένα πλήρες end-to-end παράδειγμα — πρόβλεψη έγκρισης δανείου — από τα ακατέργαστα δεδομένα μέχρι ένα αποθηκευμένο pipeline έτοιμο για production.

Αν ακολουθήσατε αυτή τη σειρά από την αρχή, τώρα έχετε ένα ολοκληρωμένο toolkit: μπορείτε να διαχειριστείτε δεδομένα με pandas 3.0, να τα καθαρίσετε με Pandas και Scikit-Learn, να τα οπτικοποιήσετε με Matplotlib και Seaborn, και να χτίσετε ML μοντέλα που γενικεύουν και είναι έτοιμα για production. Δεν είναι λίγο αυτό.

Στον επόμενο οδηγό θα εμβαθύνουμε σε θέματα MLOps — πώς να κάνετε deploy τα μοντέλα σας, πώς να τα παρακολουθείτε σε production, και πώς να αυτοματοποιήσετε ολόκληρο τον κύκλο ζωής. Μέχρι τότε, πειραματιστείτε με τα παραδείγματα, αλλάξτε τα δεδομένα, σπάστε τον κώδικα, και μάθετε από τα λάθη — αυτή είναι η ουσία της μηχανικής μάθησης, τελικά.

Σχετικά με τον Συγγραφέα Editorial Team

Our team of expert writers and editors.