Vetiver: MLOps for Python
Want to share your content on python-bloggers? click here.
This post is the fourth in our series on MLOps with vetiver:
- Part 1: Vetiver: First steps in
MLOps - Part 2: Vetiver: Model
Deployment - Part 3: Vetiver: Monitoring Models in
Production - Part 4: Vetiver: MLOps for Python (this post)
Parts 1 to 3 introduced the {vetiver} package for R and outlined its
far-reaching applications in MLOps. But did you know that this package
is also available in Python? In this post we will provide a brief
outline to getting your Python models into production using vetiver for
Python.
Installation
Like any other Python package on PyPI, vetiver can
be installed using pip. Let’s set up a virtual environment and install
all of the packages that will be covered in this blog:
python -m venv venv/ source venv/bin/activate pip install vetiver pandas pyjanitor scikit-learn pins
Check out our previous blog about virtual environments in
Python for more
details.
Data
We will be working with the World Health Organisation Life
Expectancy
data which provides the annual average life expectancy in a number of
countries. This can be downloaded from
Kaggle:
import pandas as pd url = "https://www.kaggle.com/api/v1/datasets/download/kumarajarshi/life-expectancy-who" data = pd.read_csv(url, compression = "zip") data.head() #> Country Year ... Income composition of resources Schooling #> 0 Afghanistan 2015 ... 0.479 10.1 #> 1 Afghanistan 2014 ... 0.476 10.0 #> 2 Afghanistan 2013 ... 0.470 9.9 #> 3 Afghanistan 2012 ... 0.463 9.8 #> 4 Afghanistan 2011 ... 0.454 9.5 #> #> [5 rows x 22 columns]
Let’s drop missing data, clean up the column names and select a subset
of the variables to work with:
import janitor data = data.dropna() data = data.clean_names(strip_underscores=True) data = data[[ "life_expectancy", "percentage_expenditure", "total_expenditure", "population", "bmi", "schooling", ]] data.head() #> life_expectancy percentage_expenditure ... bmi schooling #> 0 65.0 71.279624 ... 19.1 10.1 #> 1 59.9 73.523582 ... 18.6 10.0 #> 2 59.9 73.219243 ... 18.1 9.9 #> 3 59.5 78.184215 ... 17.6 9.8 #> 4 59.2 7.097109 ... 17.2 9.5 #> #> [5 rows x 6 columns]
Vetiver is compatible with models built in
scikit-learn,
PyTorch,
XGBoost and
statsmodels. The actual modelling
process is not so important in this blog. We will be more interested in
how we go about taking this model into production using vetiver. So
let’s go with a simple K-Nearest Neighbour model built using
scikit-learn:
from sklearn.neighbors import KNeighborsRegressor from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler target = "life_expectancy" covariates = [ "percentage_expenditure", "total_expenditure", "population", "bmi", "schooling", ] y = data[target] X = data[covariates] model = Pipeline( [ ("transform", StandardScaler()), ("model", KNeighborsRegressor()), ] ) model.fit(X, y) #> Pipeline(steps=[('transform', StandardScaler()), #> ('model', KNeighborsRegressor())])
Let’s break down what’s happened here:
- We selected our target variable (life expectancy) and the covariates
(features) that will be used to predict the target. - We constructed a modelling pipeline which includes:
- Preprocessing of input data via standardisation.
- K-Nearest Neighbours regression.
- In the final step, we fitted our model to the training data.
Usually at this point we would evaluate how our model performs on some
unseen test data. However, for brevity we’ll now go straight to the
MLOps steps.
MLOps
In a typical MLOps workflow, we are setting up a continuous cycle in
which our trained model is deployed to a cloud environment, monitored in
this environment, and then retrained on the latest data. The cycle
repeats so that we are always maintaining a high model performance and
avoiding the dreaded model drift (more on this later).
From the diagram above, the crucial steps that set this workflow apart
from a typical data science project are model versioning, deployment and
monitoring. We will go through each of these in turn using vetiver.
Before we can begin, we must convert our scikit-learn model into a
“vetiver model”:
import vetiver v_model = vetiver.VetiverModel(model, model_name="KNN", prototype_data=X) print(type(v_model)) #> <class 'vetiver.vetiver_model.VetiverModel'> print(v_model.description) #> A scikit-learn Pipeline model print(v_model.metadata) #> VetiverMeta(user={}, version=None, url=None, required_pkgs=['scikit-learn'], python_version=(3, 10, 12, 'final', 0))
Our VetiverModel
object contains model metadata and dependencies
(including the Python packages used to train it and the current Python
version). The model_name
will be used to identify the model later on,
and the prototype_data
will provide some example data for the model
API (more on this below).
Model versioning
In a cycle where our model is continuously being retrained, it is
important to ensure that we can retrieve any models that have previously
been deployed. Vetiver utilises the
pins package for model
storage. A pin is simply a Python object (could be a variable, data
frame, function, …) which can be stored and retrieved at a later time.
Pins are stored in “pins boards”. Examples include:
- Local storage on your device
- Google Drive
- Amazon S3
- Posit Connect
Let’s set up a temporary pins board locally for storing our model:
from pins import board_temp model_board = board_temp( versioned=True, allow_pickle_read=True ) vetiver.vetiver_pin_write(model_board, v_model) #> Model Cards provide a framework for transparent, responsible reporting. #> Use the vetiver `.qmd` Quarto template as a place to start, #> with vetiver.model_card() #> Writing pin: #> Name: 'KNN' #> Version: 20250220T141808Z-af3d5
Enabling allow_pickle_read
will allow quick reloading of the model
later on, whenever we need it.
At this stage our VetiverModel
object is now stored as a pin, and we
can view the full list of “KNN” model versions using:
model_board.pin_versions("KNN") #> created hash version #> 0 2025-02-20 14:18:08 af3d5 20250220T141808Z-af3d5
As expected, we only have one version stored so far!
Model deployment
If we want to share our model with other users (colleagues,
stakeholders, customers) we should deploy it to an endpoint on the cloud
where it can be easily shared. To keep things simple for this blog, and
to ensure the code examples provided here are fully reproducible, we
will just deploy our model to the localhost.
First we have to construct a model API. This is a simple interface which
takes some input and gives us back some model predictions. Crucially,
APIs can be hosted on the cloud where they can receive input data via
HTTP requests.
Our VetiverModel
object already contains all of the info necessary to
build an API using the FastAPI
framework:
app = vetiver.VetiverAPI(v_model, check_prototype=True)
Running app.run(port=8080)
will start a local server for the model API
on port 8080. We are then presented with a simple graphical interface in
which we can run basic queries and generate predictions using our model.
The prototype_data
argument which we defined when constructing our
VetiverModel
(see above) is used here to provide some example input
data for queries:
Alternatively we can also submit queries from the command line. The
graphical interface above provides template curl
commands which can be
copied into the command line and executed against the model. For
example, the input data shown in the above screenshot can be fed into
the model via a POST request:
curl -X POST "http://127.0.0.1:8080/predict" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '[{"percentage_expenditure":71.27962362,"total_expenditure":8.16,"population":33736494,"bmi":19.1,"schooling":10.1}]' \
The same command would work for querying APIs on the cloud as long as
the IP address for the API endpoint (here it is http://127.0.0.1,
which points to the
localhost) is updated
accordingly.
Deploying your model locally is a great way to test that your API
behaves as you expect. What’s more, it’s free and does not require
setting up an account with a cloud provider! But how would we go about
deploying our model to the cloud?
If you already have a server on Posit
Connect, it’s just a
case of running vetiver.deploy_rsconnect()
(see the Posit vetiver
documentation for
more details). If you don’t have Posit Connect, not to worry! Instead
you can start by running:
vetiver.prepare_docker(model_board, "KNN")
This command is doing a lot of heavy lifting behind the scenes:
- Lists the Python package dependencies in a
vetiver_requirements.txt file. - Stores the Python code for the model API in an app.py file.
- Creates a Dockerfile containing the Python version requirement for
the model and the docker commands for building and running the API. An
example is shown below:
# # Generated by the vetiver package; edit with care # start with python base image FROM python:3.10 # create directory in container for vetiver files WORKDIR /vetiver # copy and install requirements COPY vetiver_requirements.txt /vetiver/requirements.txt # RUN pip install --no-cache-dir --upgrade -r /vetiver/requirements.txt # copy app file COPY app.py /vetiver/app/app.py # expose port EXPOSE 8080 # run vetiver API CMD ["uvicorn", "app.app:api", "--host", "0.0.0.0", "--port", "8080"]
With these files uploaded to the cloud server of your choosing, the
docker build
command will take care of the rest. This process can be
automated on AWS, Google Cloud Run, Azure, and many other cloud
platforms.
Model monitoring
Success! Your model is now deployed and your users are interacting with
it. But this is only the beginning…
Data changes! Over time you will notice various aspects of your data
changing in unexpected ways:
- The way the data is distributed may change (data drift).
- The relationship between the target variable and covariates may change
(concept drift).
These two processes will conspire to create model drift, where your
model predictions start to drift away from the true values. This is why
MLOps is not simply a one-off deployment. It is a continuous cycle in
which you will be retraining your model on the latest data on a regular
basis.
While we will not be providing a full worked example of model drift
here, we will just mention some helpful functions provided by vetiver to
deal with this problem:
vetiver.compute_metrics()
:
computes keys metrics at specified time intervals, allowing us to
understand how the model performance varies over time.vetiver.pin_metrics()
:
stores the model metrics in a pins board for future retrieval.vetiver.plot_metrics()
:
plots the metrics over time.
You can get an idea of how these Python methods can be used by reading
our previous blog post where we monitored the model’s performance using
vetiver for
R.
The metrics can be entirely defined by the user, and might include the
accuracy score for a classification model and the mean squared error for
a regression model. We can also make use of predefined scoring functions
from the sklearn.metrics
library.
For more on model monitoring, check out the Posit vetiver
documentation.
Summary
Hopefully by reading this post you will have a better understanding of
MLOps and how to get started with MLOps in Python. Most importantly, you
don’t have to be an expert in AWS or Azure to get started! Vetiver
provides intuitive, easy-to-use functions for learning the crucial steps
of MLOps including versioning your model, building a model API, and
deploying your model using docker or Posit Connect.
For some further reading, check out:
- Our previous blog posts on vetiver with
R. - The Posit vetiver documentation.
For updates and revisions to this article, see the original post
Want to share your content on python-bloggers? click here.