Optimizing Website Conversion Rates with Machine Learning: A Comprehensive Guide using LightGBM

Anthony Therrien
7 min readAug 5, 2024

--

In today’s data-driven world, understanding website performance is critical for businesses aiming to maximize conversions and enhance user experiences. In this medium-length story, we’ll walk through a fascinating simulation where we generate synthetic web analytics data, preprocess it, and use machine learning to predict conversion rates. This journey not only highlights the power of data but also the intricate steps involved in building and optimizing a predictive model.

Generating Synthetic Web Analytics Data

The first step in our journey involves generating synthetic data that mirrors real-world web analytics. Imagine a website receiving thousands of visitors daily. Each visitor’s behavior — how many pages they view, how long they stay, and where they came from — affects the likelihood of them converting (making a purchase, signing up, etc.).

We used Python’s powerful libraries to create a dataset of 2,000 samples, simulating various features:

  • Page Views: The number of pages viewed per session, modeled using a Poisson distribution.
  • Session Duration: The total time spent on the website in minutes, modeled using an Exponential distribution.
  • Bounce Rate: The probability of leaving the website after viewing only one page, modeled using a Beta distribution.
  • Traffic Source: The origin of the traffic (Organic, Paid, Referral, Social, Direct), modeled using a categorical distribution.
  • Time on Page: The average time spent on a single page, modeled using a Gamma distribution.
  • Previous Visits: The number of previous visits by the user, modeled using a Poisson distribution.

The conversion rate, our target variable, was calculated as a complex function of these features with added noise to simulate real-world variability.

import pandas as pd
import numpy as np
import os

# Define the number of samples
num_samples = 2000

# Generate synthetic data
np.random.seed(42)
page_views = np.random.poisson(lam=5, size=num_samples)
session_duration = np.random.exponential(scale=3, size=num_samples)
bounce_rate = np.random.beta(a=2, b=5, size=num_samples)
traffic_source = np.random.choice(['Organic', 'Paid', 'Referral', 'Social', 'Direct'], size=num_samples, p=[0.4, 0.2, 0.15, 0.15, 0.1])
time_on_page = np.random.gamma(shape=2, scale=2, size=num_samples)
previous_visits = np.random.poisson(lam=2, size=num_samples)

# Conversion rate depends on the above features with added complexity and noise
conversion_rate = (0.05 * page_views +
0.2 * session_duration ** 0.5 +
0.3 * (1 - bounce_rate ** 2) +
0.1 * time_on_page +
0.15 * np.log1p(previous_visits) +
0.1 * np.where(traffic_source == 'Organic', 1, 0) +
0.05 * np.where(traffic_source == 'Paid', 1, 0) +
0.15 * np.where(traffic_source == 'Referral', 1, 0) +
0.1 * np.where(traffic_source == 'Social', 1, 0) +
0.1 * np.where(traffic_source == 'Direct', 1, 0) +
np.random.normal(scale=0.2, size=num_samples))

# Normalize conversion rate to be between 0 and 1
conversion_rate = np.clip(conversion_rate, 0, 1)

# Create DataFrame
data = {
'Page Views': page_views,
'Session Duration': session_duration,
'Bounce Rate': bounce_rate,
'Traffic Source': traffic_source,
'Time on Page': time_on_page,
'Previous Visits': previous_visits,
'Conversion Rate': conversion_rate
}

# Convert to dataframe
df = pd.DataFrame(data)

# Directory for the data
data_dir = 'data'

# Ensure the data directory exists
os.makedirs(data_dir, exist_ok=True)

# Save to CSV
filename = f'{data_dir}/website_wata.csv'
df.to_csv(filename, index=False)
print(f"Dataset generated and saved to {filename}")

Data Visualization

Before diving into model building, it’s crucial to understand the data through visual exploration. Visualizing the data helps identify patterns, outliers, and relationships between features, which can guide preprocessing and feature engineering steps.

Here’s how we visualized our synthetic web analytics data using Python’s visualization libraries:

To start, we set up the environment for plotting and ensure that the images are saved in an appropriate directory.

import os
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd

# Load the dataset
df = pd.read_csv('data/website_wata.csv')

# Set the style for seaborn
sns.set(style="whitegrid")

# Create the images folder if it doesn't exist
if not os.path.exists('images'):
os.makedirs('images')

Plotting the Distribution of Page Views

This plot shows the distribution of the number of pages viewed per session by the visitors. Understanding this distribution helps in identifying the typical user behavior in terms of page views.

# Plot the distribution of Page Views
plt.figure(figsize=(10, 6))
sns.histplot(df['Page Views'], kde=True, bins=30)
plt.title('Distribution of Page Views')
plt.xlabel('Page Views')
plt.ylabel('Frequency')
plt.savefig('images/page_views_distribution.png')
plt.close()

Plotting the Distribution of Session Duration

This plot shows how long visitors stay on the website per session. Session duration can indicate the engagement level of users.

# Plot the distribution of Session Duration
plt.figure(figsize=(10, 6))
sns.histplot(df['Session Duration'], kde=True, bins=30)
plt.title('Distribution of Session Duration')
plt.xlabel('Session Duration (minutes)')
plt.ylabel('Frequency')
plt.savefig('images/session_duration_distribution.png')
plt.close()

Plotting the Distribution of Bounce Rate

This plot shows the probability of visitors leaving the website after viewing only one page. A high bounce rate might indicate that visitors are not finding what they are looking for.

# Plot the distribution of Bounce Rate
plt.figure(figsize=(10, 6))
sns.histplot(df['Bounce Rate'], kde=True, bins=30)
plt.title('Distribution of Bounce Rate')
plt.xlabel('Bounce Rate')
plt.ylabel('Frequency')
plt.savefig('images/bounce_rate_distribution.png')
plt.close()

Plotting the Distribution of Traffic Source

This plot shows the distribution of different traffic sources. Knowing the source of traffic helps in understanding where your audience is coming from.

# Plot the distribution of Traffic Source
plt.figure(figsize=(10, 6))
sns.countplot(x='Traffic Source', data=df, palette='Set2')
plt.title('Distribution of Traffic Source')
plt.xlabel('Traffic Source')
plt.ylabel('Count')
plt.savefig('images/traffic_source_distribution.png')
plt.close()

Plotting the Distribution of Time on Page

This plot shows the average time visitors spend on a single page. This can help identify how engaging or useful the page content is.

# Plot the distribution of Time on Page
plt.figure(figsize=(10, 6))
sns.histplot(df['Time on Page'], kde=True, bins=30)
plt.title('Distribution of Time on Page')
plt.xlabel('Time on Page')
plt.ylabel('Frequency')
plt.savefig('images/time_on_page_distribution.png')
plt.close()

Plotting the Distribution of Previous Visits

This plot shows the number of previous visits by the users. Frequent visits could indicate loyalty or interest in the website’s content.

# Plot the distribution of Previous Visits
plt.figure(figsize=(10, 6))
sns.histplot(df['Previous Visits'], kde=True, bins=30)
plt.title('Distribution of Previous Visits')
plt.xlabel('Previous Visits')
plt.ylabel('Frequency')
plt.savefig('images/previous_visits_distribution.png')
plt.close()

Building the Predictive Model

With our dataset ready, we moved on to the exciting part: building a predictive model to estimate conversion rates based on our features. For this, we chose LightGBM, a gradient boosting framework that is known for its efficiency and accuracy.

We began by loading the dataset and separating it into features and labels. The features included all our generated metrics except the conversion rate, which was our label.

Next, we set up preprocessing steps to handle both numeric and categorical data. Numeric features were standardized using StandardScaler, while categorical features (traffic sources) were one-hot encoded.

We constructed a pipeline that included these preprocessing steps followed by the LightGBM regressor. To ensure we achieved the best possible performance, we used GridSearchCV to tune the model's hyperparameters across several folds of cross-validation.

import pandas as pd
from sklearn.model_selection import GridSearchCV
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
import lightgbm as lgb
import joblib

# Load the dataset
dataframe = pd.read_csv('data/website_wata.csv')

# Split the dataset into features and labels
features = dataframe.drop('Conversion Rate', axis=1)
labels = dataframe['Conversion Rate']

# Define the preprocessing steps for numeric and categorical features
numeric_feature_names = ['Page Views', 'Session Duration', 'Bounce Rate', 'Time on Page', 'Previous Visits']
categorical_feature_names = ['Traffic Source']

numeric_transformer = StandardScaler()
categorical_transformer = OneHotEncoder()

preprocessor = ColumnTransformer(
transformers=[
('numeric', numeric_transformer, numeric_feature_names),
('categorical', categorical_transformer, categorical_feature_names)
])

# Define the model pipeline
model_pipeline = Pipeline(steps=[
('preprocessor', preprocessor),
('regressor', lgb.LGBMRegressor(random_state=42))
])

# Define the parameter grid for GridSearchCV
param_grid = {
'regressor__n_estimators': [75, 100, 150],
'regressor__learning_rate': [0.01, 0.05, 0.1],
'regressor__num_leaves': [20, 30, 40],
'regressor__max_depth': [4, 6, 8]
}

# Define the GridSearchCV
grid_search = GridSearchCV(estimator=model_pipeline, param_grid=param_grid, cv=8, scoring='neg_mean_squared_error', n_jobs=-1)

# Train the model with GridSearchCV
grid_search.fit(features, labels)

# Get the best estimator
best_model = grid_search.best_estimator_

# Evaluate the model using cross-validation
cv_results = grid_search.cv_results_
mean_test_scores = cv_results['mean_test_score']
print(f'Mean Cross-Validation Scores: {mean_test_scores}')
print(f'Best Parameters: {grid_search.best_params_}')

# Save the best model
model_name = 'best_web_model.pkl'
joblib.dump(best_model, model_name)
print(f"Model saved as {model_name}")

Insights from Cross-Validation and Best Model Parameters

Our model training and hyperparameter tuning using GridSearchCV yielded interesting insights and results. The process involved evaluating various combinations of hyperparameters and selecting the best-performing model based on mean squared error.

Mean Cross-Validation Scores

The mean cross-validation scores provide a glimpse into the performance of different hyperparameter combinations. Here’s a summary of the key points:

  • Mean Scores Range: The mean cross-validation scores ranged from approximately -0.00354893 to -0.00313551. The negative values indicate the mean squared error (MSE), with smaller absolute values being better.
  • Performance Trends: The best scores, around -0.00313551, were achieved with specific hyperparameter settings. These scores help identify which combinations of parameters contributed to better model performance.

Best Hyperparameters

After extensive evaluation, the GridSearchCV identified the best hyperparameters for our LightGBM regressor:

  • Learning Rate: 0.05
  • Maximum Depth: 4
  • Number of Estimators: 100
  • Number of Leaves: 20

These hyperparameters were selected because they minimized the mean squared error during cross-validation, indicating a well-tuned balance between model complexity and performance.

Model Saving

The best model was saved for future use, ensuring that we can quickly deploy or further evaluate it as needed.

Conclusion

This comprehensive approach highlights the importance of rigorous model tuning and evaluation in achieving high performance and reliability in predictive analytics. By leveraging the power of LightGBM and GridSearchCV, we have developed a robust model capable of accurately predicting conversion rates based on various web analytics features.

--

--

Anthony Therrien

An AI and web developer. A passion to build RESTFul APIs and train models.