Home Artificial Intelligence Mastering Monte Carlo: Easy methods to Simulate Your Approach to Higher Machine Learning Models How a Scientist Playing Cards Ceaselessly Modified the Game of Statistics Playing Your Cards Right With Probabilistic Simulations Understanding Monte Carlo Simulations A Game of Darts Random Guessing vs. Monte Carlo Predicting Pi: The Power of Probability Monte Carlo Simulations and Hyperparameter Tuning: A Winning Combination Monte Carlo Simulations for Hyperparameter Tuning Setting the Pulse: Preprocessing the Data Implementing a Basic Machine Learning Model Hyperparameter Tuning with Grid Search Conclusion Parting Thoughts

Mastering Monte Carlo: Easy methods to Simulate Your Approach to Higher Machine Learning Models How a Scientist Playing Cards Ceaselessly Modified the Game of Statistics Playing Your Cards Right With Probabilistic Simulations Understanding Monte Carlo Simulations A Game of Darts Random Guessing vs. Monte Carlo Predicting Pi: The Power of Probability Monte Carlo Simulations and Hyperparameter Tuning: A Winning Combination Monte Carlo Simulations for Hyperparameter Tuning Setting the Pulse: Preprocessing the Data Implementing a Basic Machine Learning Model Hyperparameter Tuning with Grid Search Conclusion Parting Thoughts

0
Mastering Monte Carlo: Easy methods to Simulate Your Approach to Higher Machine Learning Models
How a Scientist Playing Cards Ceaselessly Modified the Game of Statistics
Playing Your Cards Right With Probabilistic Simulations
Understanding Monte Carlo Simulations
A Game of Darts
Random Guessing vs. Monte Carlo
Predicting Pi: The Power of Probability
Monte Carlo Simulations and Hyperparameter Tuning: A Winning Combination
Monte Carlo Simulations for Hyperparameter Tuning
Setting the Pulse: Preprocessing the Data
Implementing a Basic Machine Learning Model
Hyperparameter Tuning with Grid Search
Conclusion
Parting Thoughts

Within the Monte Carlo method, the pi estimate is predicated on the proportion of “darts” that land contained in the circle to the entire variety of darts thrown. The resulting estimated pi value is used to generate a circle. If the Monte Carlo estimate is inaccurate, the circle will again be the fallacious size. The width of the gap between this estimated circle and the unit circle gives a sign of the accuracy of the Monte Carlo estimate.

Nonetheless, since the Monte Carlo method generates more accurate estimates because the variety of “darts” increases, the estimated circle should converge towards the unit circle as more “darts” are thrown. Due to this fact, while each methods show a niche when the estimate is inaccurate, this gap should decrease more consistently with the Monte Carlo method because the variety of “darts” increases.

What makes Monte Carlo simulations so powerful is their ability to harness randomness to resolve deterministic problems. By generating numerous random scenarios and analyzing the outcomes, we are able to estimate the probability of various outcomes, even for complex problems that will be difficult to resolve analytically.

Within the case of estimating pi, the Monte Carlo method allows us to make a really accurate estimate, regardless that we’re just throwing darts randomly. As discussed, the more darts we throw, the more accurate our estimate becomes. That is an illustration of the law of huge numbers, a fundamental concept in probability theory that states that the typical of the outcomes obtained from numerous trials needs to be near the expected value, and can are inclined to turn out to be closer and closer as more trials are performed. Let’s see if this tends to be true for our six examples shown in Figures 2a-2f by plotting the variety of darts thrown against the difference between Monte Carlo-estimated pi and real pi. Basically, our graph (Figure 2g) should trend negative. Here’s the code to perform this:

# Calculate the differences between the true pi and the estimated pi
diff_pi = [abs(estimate - math.pi) for estimate in pi_estimates]

# Create the figure for the variety of darts vs difference in pi plot (Figure 2g)
fig2g = go.Figure(data=go.Scatter(x=num_darts_thrown, y=diff_pi, mode='lines'))

# Add title and labels to the plot
fig2g.update_layout(
title="Fig2g: Darts Thrown vs Difference in Estimated Pi",
xaxis_title="Variety of Darts Thrown",
yaxis_title="Difference in Pi",
)

# Display the plot
fig2g.show()

# Save the plot as a png
pio.write_image(fig2g, "fig2g.png")

Note that, even with only 6 examples, the final pattern is as expected: more darts thrown (more scenarios), a smaller difference between the estimated and real value, and thus a greater prediction.

Let’s say we throw 1,000,000 total darts, and permit ourselves 500 predictions. In other words, we’ll record the difference between the estimated and actual values of pi at 500 evenly spaced intervals throughout the simulation of 1,000,000 thrown darts. Slightly than generate 500 extra figures, let’s just skip to what we’re trying to verify: whether it’s indeed true that as more darts are thrown, the difference in our predicted value of pi and real pi gets lower. We’ll use a scatter plot (Figure 2h):

#500 Monte Carlo Scenarios; 1,000,000 darts thrown
import random
import math
import plotly.graph_objects as go
import numpy as np

# Total variety of darts to throw (1M)
num_darts = 1000000
darts_in_circle = 0

# Variety of scenarios to record (500)
num_scenarios = 500
darts_per_scenario = num_darts // num_scenarios

# Lists to store the info for every scenario
darts_thrown_list = []
pi_diff_list = []

# We'll throw plenty of darts
for i in range(num_darts):
# Generate random x, y coordinates between -1 and 1
x, y = random.uniform(-1, 1), random.uniform(-1, 1)

# Check if the dart is contained in the circle
# A dart is contained in the circle if the gap from the origin (0,0) is lower than or equal to 1
if math.sqrt(x**2 + y**2) <= 1:
darts_in_circle += 1

# If it is time to record a scenario
if (i + 1) % darts_per_scenario == 0:
# Estimate pi with Monte Carlo method
# The estimate is 4 times the variety of darts within the circle divided by the entire variety of darts
pi_estimate = 4 * darts_in_circle / (i + 1)

# Record the variety of darts thrown and the difference between the estimated and actual values of pi
darts_thrown_list.append((i + 1) / 1000) # Dividing by 1000 to display in 1000's
pi_diff_list.append(abs(pi_estimate - math.pi))

# Create a scatter plot of the info
fig = go.Figure(data=go.Scattergl(x=darts_thrown_list, y=pi_diff_list, mode='markers'))

# Update the layout of the plot
fig.update_layout(
title="Fig2h: Difference between Estimated and Actual Pi vs. Variety of Darts Thrown (in 1000's)",
xaxis_title="Variety of Darts Thrown (in 1000's)",
yaxis_title="Difference between Estimated and Actual Pi",
)

# Display the plot
fig.show()
# Save the plot as a png
pio.write_image(fig2h, "fig2h.png")

You could be considering to yourself at this point, “Monte Carlo is an interesting statistical tool, but how does it apply to machine learning?” The short answer is: in some ways. One in all the various applications of Monte Carlo simulations in machine learning is within the realm of hyperparameter tuning.

Hyperparameters are the knobs and dials that we (the humans) adjust when establishing machine learning algorithms. They control features of the algorithm’s behavior that, crucially, aren’t learned from the info. For instance, in a choice tree, the utmost depth of the tree is a hyperparameter. In a neural network, the educational rate and the variety of hidden layers are hyperparameters.

Selecting the correct hyperparameters could make the difference between a model that performs poorly and one which performs excellently. But how will we know which hyperparameters to decide on? That is where Monte Carlo simulations are available.

Traditionally, machine learning practitioners have used methods like grid search or random search to tune hyperparameters. These methods involve specifying a set of possible values for every hyperparameter, after which training and evaluating a model for each possible combination of hyperparameters. This might be computationally expensive and time-consuming, especially when there are numerous hyperparameters to tune or a wide variety of possible values each can take.

Monte Carlo simulations offer a more efficient alternative. As a substitute of exhaustively looking through all possible mixtures of hyperparameters, we are able to randomly sample from the space of hyperparameters in keeping with some probability distribution. This permits us to explore the hyperparameter space more efficiently and find good mixtures of hyperparameters faster.

In the following section, we’ll use an actual dataset to display the way to use Monte Carlo simulations for hyperparameter tuning in practice. Let’s start!

The Heartbeat of Our Experiment: The Heart Disease Dataset

On the earth of machine learning, data is the lifeblood that powers our models. For our exploration of Monte Carlo simulations in hyperparameter tuning, let’s take a look at a dataset that’s near the guts — quite literally. The Heart Disease dataset (CC BY 4.0) from the UCI Machine Learning Repository is a group of medical records from patients, a few of whom have heart disease.

The dataset comprises 14 attributes, including age, sex, chest pain type, resting blood pressure, levels of cholesterol, fasting blood sugar, and others. The goal variable is the presence of heart disease, making this a binary classification task. With a mixture of categorical and numerical features, it’s an interesting dataset for demonstrating hyperparameter tuning.

First, let’s take a take a look at our dataset to get a way of what we’ll be working with — at all times a superb place to start out.


#Load and think about first few rows of dataset

# Import obligatory libraries
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import roc_auc_score
import numpy as np
import plotly.graph_objects as go

# Load the dataset
# The dataset is on the market on the UCI Machine Learning Repository
# It is a dataset about heart disease and includes various patient measurements
url = "https://archive.ics.uci.edu/ml/machine-learning-databases/heart-disease/processed.cleveland.data"

# Define the column names for the dataframe
column_names = ["age", "sex", "cp", "trestbps", "chol", "fbs", "restecg", "thalach", "exang", "oldpeak", "slope", "ca", "thal", "target"]

# Load the dataset right into a pandas dataframe
# We specify the column names and in addition tell pandas to treat '?' as NaN
df = pd.read_csv(url, names=column_names, na_values="?")

# Print the primary few rows of the dataframe
# This provides us a fast overview of the info
print(df.head())

This shows us the primary 4 values in our dataset across all columns. When you’ve loaded the correct csv and named your columns as I even have, your output will appear like Figure 3.

Figure 3: First 4 rows of information from our dataset

Before we are able to use the Heart Disease dataset for hyperparameter tuning, we want to preprocess the info. This involves several steps:

  1. Handling missing values: Some records within the dataset have missing values. We’ll need to make a decision the way to handle these, whether by deleting the records, filling within the missing values, or another method.
  2. Encoding categorical variables: Many machine learning algorithms require input data to be numerical. We’ll have to convert categorical variables right into a numerical format.
  3. Normalizing numerical features: Machine learning algorithms often perform higher when numerical features are on an analogous scale. We’ll apply normalization to regulate the dimensions of those features.

Let’s start by handling missing values. In our Heart Disease dataset, we have now a couple of missing values within the ‘ca’ and ‘thal’ columns. We’ll fill these missing values with the median of the respective column. It is a common strategy for coping with missing data, because it doesn’t drastically affect the distribution of the info.

Next, we’ll encode the specific variables. In our dataset, the ‘cp’, ‘restecg’, ‘slope’, ‘ca’, and ‘thal’ columns are categorical. We’ll use label encoding to convert these categorical variables into numerical ones. Label encoding assigns each unique category in a column to a distinct integer.

Finally, we’ll normalize the numerical features. Normalization adjusts the dimensions of numerical features in order that all of them fall inside an analogous range. This may help improve the performance of many machine learning algorithms. We’ll use standard scaling for normalization, which transforms the info to have a mean of 0 and a typical deviation of 1.

Here’s the Python code that performs all of those preprocessing steps:

# Preprocess

# Import obligatory libraries
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import LabelEncoder

# Discover missing values within the dataset
# It will print the variety of missing values in each column
print(df.isnull().sum())

# Fill missing values with the median of the column
# The SimpleImputer class from sklearn provides basic strategies for imputing missing values
# We're using the 'median' strategy, which replaces missing values with the median of every column
imputer = SimpleImputer(strategy='median')

# Apply the imputer to the dataframe
# The result's a brand new dataframe where missing values have been filled in
df_filled = pd.DataFrame(imputer.fit_transform(df), columns=df.columns)

# Print the primary few rows of the filled dataframe
# This provides us a fast check to be certain the imputation worked appropriately
print(df_filled.head())

# Discover categorical variables within the dataset
# These are variables that contain non-numerical data
categorical_vars = df_filled.select_dtypes(include='object').columns

# Encode categorical variables
# The LabelEncoder class from sklearn converts each unique string into a singular integer
encoder = LabelEncoder()
for var in categorical_vars:
df_filled[var] = encoder.fit_transform(df_filled[var])

# Normalize numerical features
# The StandardScaler class from sklearn standardizes features by removing the mean and scaling to unit variance
scaler = StandardScaler()

# Apply the scaler to the dataframe
# The result's a brand new dataframe where numerical features have been normalized
df_normalized = pd.DataFrame(scaler.fit_transform(df_filled), columns=df_filled.columns)

# Print the primary few rows of the normalized dataframe
# This provides us a fast check to be certain the normalization worked appropriately
print(df_normalized.head())

The primary print statement shows us the variety of missing values in each column of the unique dataset. In our case, the ‘ca’ and ‘thal’ columns had a couple of missing values.

The second print statement shows us the primary few rows of the dataset after filling within the missing values. As discussed, we used the median of every column to fill within the missing values.

The third print statement shows us the primary few rows of the dataset after encoding the specific variables. After this step, all of the variables in our dataset are numerical.

The ultimate print statement shows us the primary few rows of the dataset after normalizing the numerical features, through which the info could have a mean of 0 and a typical deviation of 1. After this step, all of the numerical features in our dataset are on an analogous scale. Check that your output resembles Figure 4:

Figure 4: Preprocessing Print Statements Output

After running this code, we have now a preprocessed dataset that’s ready for modeling.

Now that we’ve preprocessed our data, we’re able to implement a basic machine learning model. It will function our baseline model, which we’ll later try to enhance through hyperparameter tuning.

We’ll use an easy logistic regression model for this task. Note that while it’s called “regression,” this is definitely some of the popular algorithms for binary classification problems, just like the one we’re coping with within the Heart Disease dataset. It’s a linear model that predicts the probability of the positive class.

After training our model, we’ll evaluate its performance using two common metrics: accuracy and ROC-AUC. Accuracy is the proportion of correct predictions out of all predictions, while ROC-AUC (Receiver Operating Characteristic — Area Under Curve) measures the trade-off between the true positive rate and the false positive rate.

But what does this need to do with Monte Carlo simulations? Well, machine learning models like logistic regression have several hyperparameters that might be tuned to enhance performance. Nonetheless, finding the very best set of hyperparameters might be like looking for a needle in a haystack. That is where Monte Carlo simulations are available. By randomly sampling different sets of hyperparameters and evaluating their performance, we are able to estimate the probability distribution of excellent hyperparameters and make an informed guess about the very best ones to make use of, similarly to how we picked higher values of pi in our dart-throwing exercise.

Here’s the Python code that implements and evaluates a basic logistic regression model for our newly pre-processed data:

# Logistic Regression Model - Baseline

# Import obligatory libraries
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, roc_auc_score

# Replace the 'goal' column within the normalized DataFrame with the unique 'goal' column
# This is finished since the 'goal' column was also normalized, which shouldn't be what we would like
df_normalized['target'] = df['target']

# Binarize the 'goal' column
# This is finished because the unique 'goal' column comprises values from 0 to 4
# We wish to simplify the issue to a binary classification problem: heart disease or no heart disease
df_normalized['target'] = df_normalized['target'].apply(lambda x: 1 if x > 0 else 0)

# Split the info into training and test sets
# The 'goal' column is our label, so we drop it from our features (X)
# We use a test size of 20%, meaning 80% of the info will likely be used for training and 20% for testing
X = df_normalized.drop('goal', axis=1)
y = df_normalized['target']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# Implement a basic logistic regression model
# Logistic Regression is an easy yet powerful linear model for binary classification problems
model = LogisticRegression()
model.fit(X_train, y_train)

# Make predictions on the test set
# The model has been trained, so we are able to now use it to make predictions on unseen data
y_pred = model.predict(X_test)

# Evaluate the model
# We use accuracy (the proportion of correct predictions) and ROC-AUC (a measure of how well the model distinguishes between classes) as our metrics
accuracy = accuracy_score(y_test, y_pred)
roc_auc = roc_auc_score(y_test, y_pred)

# Print the performance metrics
# These give us a sign of how well our model is performing
print("Baseline Model " + f'Accuracy: {accuracy}')
print("Baseline Model " + f'ROC-AUC: {roc_auc}')

With an accuracy of 0.885 and an ROC-AUC rating of 0.884, our basic logistic regression model has set a solid baseline for us to enhance upon. These metrics indicate that our model is performing quite well at distinguishing between patients with and without heart disease. Let’s see if we are able to make it higher.

In machine learning, a model’s performance can often be improved by tuning its hyperparameters. Hyperparameters are parameters that aren’t learned from the info, but are set prior to the beginning of the educational process. For instance, in logistic regression, the regularization strength ‘C’ and the kind of penalty ‘l1’ or ‘l2’ are hyperparameters.

Let’s perform hyperparameter tuning on our logistic regression model using grid search. We’ll tune the ‘C’ and ‘penalty’ hyperparameters, and we’ll use ROC-AUC as our scoring metric. Let’s see if we are able to beat our baseline model’s performance.

Now, let’s start with the Python code for this section.

# Grid Search

# Import obligatory libraries
from sklearn.model_selection import GridSearchCV

# Define the hyperparameters and their values
# 'C' is the inverse of regularization strength (smaller values specify stronger regularization)
# 'penalty' specifies the norm utilized in the penalization (l1 or l2)
hyperparameters = {'C': [0.001, 0.01, 0.1, 1, 10, 100, 1000],
'penalty': ['l1', 'l2']}

# Implement grid search
# GridSearchCV is a technique used to tune our model's hyperparameters
# We pass our model, the hyperparameters to tune, and the variety of folds for cross-validation
# We're using ROC-AUC as our scoring metric
grid_search = GridSearchCV(LogisticRegression(), hyperparameters, cv=5, scoring='roc_auc')
grid_search.fit(X_train, y_train)

# Get the very best hyperparameters
# GridSearchCV has found the very best hyperparameters for our model, so we print them out
best_params = grid_search.best_params_
print(f'Best hyperparameters: {best_params}')

# Evaluate the very best model
# GridSearchCV also gives us the very best model, so we are able to use it to make predictions and evaluate its performance
best_model = grid_search.best_estimator_
y_pred_best = best_model.predict(X_test)
accuracy_best = accuracy_score(y_test, y_pred_best)
roc_auc_best = roc_auc_score(y_test, y_pred_best)

# Print the performance metrics of the very best model
# These give us a sign of how well our model is performing after hyperparameter tuning
print("Grid Search Method " + f'Accuracy of the very best model: {accuracy_best}')
print("Grid Search Method " + f'ROC-AUC of the very best model: {roc_auc_best}')

With the very best hyperparameters found to be {‘C’: 0.1, ‘penalty’: ‘l2’}, our grid search has an accuracy of 0.852 and an ROC-AUC rating of 0.853 for the very best model. Interestingly, this performance is barely lower than our baseline model. This may very well be attributable to the undeniable fact that our baseline model’s hyperparameters were already well-suited to this particular dataset, or it may very well be a results of the randomness inherent within the train-test split. Regardless, it’s a priceless reminder that more complex models and techniques aren’t at all times higher.

Nonetheless, you may have also noticed that our grid search only explored a comparatively small variety of possible hyperparameter mixtures. In practice, the variety of hyperparameters and their potential values might be much larger, making grid search computationally expensive and even infeasible.

That is where the Monte Carlo method is available in. Let’s see if this more guided approach improves on either the unique baseline or grid search-based model’s performance:

#Monte Carlo

# Import obligatory libraries
from sklearn.metrics import accuracy_score, roc_auc_score
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
import numpy as np

# Split the info into training and test sets
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# Define the range of hyperparameters
# 'C' is the inverse of regularization strength (smaller values specify stronger regularization)
# 'penalty' specifies the norm utilized in the penalization (l1 or l2)
C_range = np.logspace(-3, 3, 7)
penalty_options = ['l1', 'l2']

# Initialize variables to store the very best rating and hyperparameters
best_score = 0
best_hyperparams = None

# Perform the Monte Carlo simulation
# We'll perform 1000 iterations. You may play with this number to see how the performance changes.
# Remember the Law of Large Numbers!
for _ in range(1000):

# Randomly select hyperparameters from the defined range
C = np.random.alternative(C_range)
penalty = np.random.alternative(penalty_options)

# Create and evaluate the model with these hyperparameters
# We're using 'liblinear' solver because it supports each L1 and L2 regularization
model = LogisticRegression(C=C, penalty=penalty, solver='liblinear')
model.fit(X_train, y_train)
y_pred = model.predict(X_test)

# Calculate the accuracy and ROC-AUC
accuracy = accuracy_score(y_test, y_pred)
roc_auc = roc_auc_score(y_test, y_pred)

# If this model's ROC-AUC is the very best up to now, store its rating and hyperparameters
if roc_auc > best_score:
best_score = roc_auc
best_hyperparams = {'C': C, 'penalty': penalty}

# Print the very best rating and hyperparameters
print("Monte Carlo Method " + f'Best ROC-AUC: {best_score}')
print("Monte Carlo Method " + f'Best hyperparameters: {best_hyperparams}')

# Train the model with the very best hyperparameters
best_model = LogisticRegression(**best_hyperparams, solver='liblinear')
best_model.fit(X_train, y_train)

# Make predictions on the test set
y_pred = best_model.predict(X_test)

# Calculate and print the accuracy of the very best model
accuracy = accuracy_score(y_test, y_pred)
print("Monte Carlo Method " + f'Accuracy of the very best model: {accuracy}')

Within the Monte Carlo method, we found that the very best ROC-AUC rating was 0.9014, with the very best hyperparameters being {‘C’: 0.1, ‘penalty’: ‘l1’}. The accuracy of the very best model was 0.9016.

Looks like Monte Carlo just pulled an ace from the deck — that is an improvement over each the baseline model and the model tuned using grid search. I encourage you to tweak the Python code to see the way it impacts the performance, remembering the principles discussed. See in case you can improve the grid search method by increasing the hyperparameter space, or compare the computation time to the Monte Carlo method. Increase and reduce the variety of iterations for our Monte Carlo method to see how that impacts performance.

The Monte Carlo method, born from a game of solitaire, has undoubtedly reshaped the landscape of computational mathematics and data science. Its power lies in its simplicity and flexibility, allowing us to tackle complex, high-dimensional problems with relative ease. From estimating the worth of pi with a game of darts to tuning hyperparameters in machine learning models, Monte Carlo simulations have proven to be a useful tool in our data science arsenal.

In this text, we’ve journeyed from the origins of the Monte Carlo method, through its theoretical underpinnings, and into its practical applications in machine learning. We’ve seen how it could actually be used to optimize machine learning models, with a hands-on exploration of hyperparameter tuning using a real-world dataset. We’ve also compared it with other methods, demonstrating its efficiency and effectiveness.

However the story of Monte Carlo is much from over. As we proceed to push the boundaries of machine learning and data science, the Monte Carlo method will undoubtedly proceed to play a vital role. Whether we’re developing sophisticated AI applications, making sense of complex data, or just playing a game of solitaire, the Monte Carlo method is a testament to the ability of simulation and approximation in solving complex problems.

As we move forward, let’s take a moment to understand the fantastic thing about this method — a technique that has its roots in an easy card game, yet has the ability to drive a few of the most advanced computations on the earth. The Monte Carlo method truly is a high-stakes game of probability and complexity, and up to now, it seems, the home at all times wins. So, keep shuffling the deck, keep playing your cards, and remember — in the sport of information science, Monte Carlo could just be your ace in the outlet.

Congratulations on making it to the top! We’ve journeyed through the world of probabilities, wrestled with complex models, and emerged with a newfound appreciation for the ability of Monte Carlo simulations. We’ve seen them in motion, simplifying intricate problems into manageable components, and even optimizing hyperparameters for machine learning tasks.

When you enjoy diving into the intricacies of ML problem-solving as much as I do, follow me on Medium and LinkedIn. Together, let’s navigate the AI labyrinth, one clever solution at a time.

Until our next statistical adventure, keep exploring, continue learning, and keep simulating! And in your data science and ML journey, may the chances be ever in your favor.

Note: All images, unless otherwise noted, are by the writer.

LEAVE A REPLY

Please enter your comment!
Please enter your name here