Building Your First ML Model: A Step-by-Step Tutorial

A complete hands-on tutorial for building and deploying your first ML model.

Written by
March 28, 2026 7 min read 288 views

By the end of this tutorial, you will have a fully trained machine learning model that predicts wine quality based on chemical properties — and you will understand every line of code that made it work. We are building a Wine Quality classifier using scikit-learn, one of the most widely used ML libraries in the world. The only prerequisite is basic Python — if you can write a for loop and understand what a function is, you are ready. Let’s build something real.

Setting Up Your Environment

You have two options: run this locally or use Google Colab. Google Colab is a free, browser-based Python environment that requires no installation — just go to colab.research.google.com, create a new notebook, and you are ready. If you prefer to work locally, run the following:

pip install pandas numpy scikit-learn matplotlib seaborn joblib flask

Once installed, start with these imports:

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split, GridSearchCV, cross_val_score
from sklearn.preprocessing import StandardScaler, LabelEncoder
from sklearn.impute import SimpleImputer
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, confusion_matrix, classification_report
import joblib
import warnings
warnings.filterwarnings('ignore')
print("All libraries imported successfully!")
Python coding environment setup
A clean Python environment is the foundation of every successful ML project.

Step 1: Understanding and Loading Your Data

red_wine = pd.read_csv(
    'https://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-red.csv',
    sep=';'
)
white_wine = pd.read_csv(
    'https://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-white.csv',
    sep=';'
)
red_wine['wine_type'] = 'red'
white_wine['wine_type'] = 'white'
df = pd.concat([red_wine, white_wine], axis=0, ignore_index=True)
print(f"Dataset shape: {df.shape}")
print(df.info())
print(df.describe())
print("Missing values:", df.isnull().sum().sum())

Understanding the output: df.info() tells you column names, data types, and non-null counts. df.describe() shows min, max, mean, and quartiles for every numeric column — scan for anything surprising, like negative values in columns that should be positive, or an extremely wide range that might indicate outliers.

Step 2: Exploratory Data Analysis (EDA)

print("Quality score distribution:")
print(df['quality'].value_counts().sort_index())

import seaborn as sns
plain_df = df.select_dtypes(include=[np.number])
correlation_matrix = plain_df.corr()

print("nCorrelation with quality score:")
print(correlation_matrix['quality'].sort_values(ascending=False))
Data visualization charts
Visualizing your data before modeling reveals patterns that statistics alone might miss.

In this dataset, alcohol content typically shows the strongest positive correlation with quality, while volatile acidity shows strong negative correlation. These insights inform feature selection later.

Step 3: Data Preprocessing

def categorize_quality(score):
    if score <= 5:
        return 'Low'
    elif score == 6:
        return 'Medium'
    else:
        return 'High'

df['quality_label'] = df['quality'].apply(categorize_quality)

le = LabelEncoder()
df['wine_type_encoded'] = le.fit_transform(df['wine_type'])

le_target = LabelEncoder()
df['quality_encoded'] = le_target.fit_transform(df['quality_label'])

feature_columns = [
    'fixed acidity', 'volatile acidity', 'citric acid', 'residual sugar',
    'chlorides', 'free sulfur dioxide', 'total sulfur dioxide', 'density',
    'pH', 'sulphates', 'alcohol', 'wine_type_encoded'
]

X = df[feature_columns]
y = df['quality_encoded']

imputer = SimpleImputer(strategy='median')
X_imputed = imputer.fit_transform(X)

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X_imputed)

Step 4: Splitting and Training

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

model = RandomForestClassifier(
    n_estimators=100,
    random_state=42,
    n_jobs=-1
)

model.fit(X_train, y_train)
print(f"Training accuracy: {model.score(X_train, y_train):.4f}")

What’s happening under the hood: A Random Forest builds 100 individual decision trees, each trained on a random sample of your training data and a random subset of features. When predicting, each tree casts a vote, and the majority vote wins. This ensemble approach dramatically reduces the variance that plagues single decision trees.

Step 5: Evaluating Your Model

y_pred = model.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
baseline = max(pd.Series(y_test).value_counts(normalize=True))

print(f"Test Accuracy: {accuracy:.4f} ({accuracy*100:.1f}%)")
print(f"Baseline accuracy: {baseline:.4f} ({baseline*100:.1f}%)")
print(f"Improvement: {(accuracy - baseline)*100:.1f} percentage points")

print("nClassification Report:")
print(classification_report(y_test, y_pred, target_names=le_target.classes_))

Understanding the metrics: Accuracy is the percentage of all predictions that were correct. Precision measures how often the model is right when it predicts a class. Recall measures how many of the actual class instances the model found. F1 Score balances both precision and recall. Always compare your model to the baseline — if 70% accuracy and your model gives 72%, something is wrong.

Step 6: Improving Your Model

feature_importance = pd.DataFrame({
    'feature': feature_columns,
    'importance': model.feature_importances_
}).sort_values('importance', ascending=False)
print(feature_importance)

cv_scores = cross_val_score(model, X_scaled, y, cv=5, scoring='accuracy', n_jobs=-1)
print(f"Mean CV accuracy: {cv_scores.mean():.4f} (+/- {cv_scores.std() * 2:.4f})")

param_grid = {
    'n_estimators': [100, 200],
    'max_depth': [None, 10, 20],
    'min_samples_split': [2, 5]
}
grid_search = GridSearchCV(
    RandomForestClassifier(random_state=42, n_jobs=-1),
    param_grid, cv=5, scoring='accuracy', n_jobs=-1
)
grid_search.fit(X_train, y_train)
print(f"Best parameters: {grid_search.best_params_}")
print(f"Best CV accuracy: {grid_search.best_score_:.4f}")

Step 7: Saving and Deploying

joblib.dump(grid_search.best_estimator_, 'wine_quality_model.pkl')
joblib.dump(scaler, 'wine_quality_scaler.pkl')
joblib.dump(imputer, 'wine_quality_imputer.pkl')
joblib.dump(le_target, 'wine_quality_encoder.pkl')

# Flask API (app.py)
from flask import Flask, request, jsonify
import numpy as np
import joblib

app = Flask(__name__)
model = joblib.load('wine_quality_model.pkl')
scaler = joblib.load('wine_quality_scaler.pkl')
imputer = joblib.load('wine_quality_imputer.pkl')
encoder = joblib.load('wine_quality_encoder.pkl')

@app.route('/predict', methods=['POST'])
def predict():
    data = request.get_json()
    features = np.array(data['features']).reshape(1, -1)
    features_imputed = imputer.transform(features)
    features_scaled = scaler.transform(features_imputed)
    prediction = model.predict(features_scaled)
    label = encoder.inverse_transform(prediction)[0]
    confidence = model.predict_proba(features_scaled).max()
    return jsonify({'quality': label, 'confidence': round(float(confidence), 4)})

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Common Beginner Mistakes

Data leakage is the most dangerous mistake in ML. It happens when information from your test set influences your model during training. Always fit preprocessing objects on training data only, then use them to transform both training and test data. Use scikit-learn Pipeline objects to guarantee this order automatically.

Class imbalance — when one class has far more samples than others — causes models to ignore minority classes. Use class_weight='balanced' in your model, or SMOTE from the imbalanced-learn library.

Not checking the baseline is surprisingly common. Always compare your model to the simplest possible predictor — predicting the most common class every time. Your model should meaningfully beat baseline or it has not learned anything useful.

What to Build Next

  1. Titanic Survival Prediction — The classic beginner Kaggle competition. Binary classification with missing values and rich feature engineering opportunities.
  2. House Price Prediction — Your first regression problem. You will learn RMSE, MAE, and R-squared metrics.
  3. Spam Email Classifier — NLP meets classification. Learn TF-IDF vectorization and Naive Bayes classifiers.
  4. Customer Churn Prediction — Build something with direct business value using IBM Telco Customer Churn dataset on Kaggle.
  5. Fashion-MNIST Image Classifier — Your entry point into neural networks, classifying clothing images from grayscale pixel data.

The best way to learn ML is to build, break, and rebuild. Take the pipeline you built today, swap the RandomForestClassifier for a GradientBoostingClassifier or an SVC, and observe what changes. Experiment with feature engineering. Break something deliberately, trace the error, and fix it. That cycle of curiosity, experimentation, and debugging is exactly how every ML engineer you admire developed their intuition. You are already on the path.

Enjoyed this article?

Get weekly insights on Tech, AI & Beauty — straight to your inbox.

Leave a Comment

Your email address will not be published. Required fields are marked *