Publishing models as APIs

Lecture 15

Dr. Benjamin Soltoff

Cornell University
INFO 4940/5940 - Fall 2025

October 21, 2025

Announcements

Announcements

TODO

Learning objectives

  • Create bundled model objects that can be saved to disk
  • Implement versioning for model objects
  • Review application programming interfaces (APIs)
  • Generate a REST API for a model using Vetiver

TODO test on Posit Workbench

Application exercise

ae-14

Instructions

  • Go to the course GitHub org and find your ae-14 (repo name will be suffixed with your GitHub name).
  • Clone the repo in Positron, run renv::restore() to install the required packages, open the Quarto document in the repo, and follow along and complete the exercises.
  • Render, commit, and push your edits by the AE deadline – end of the day

MLOps

MLOps with {vetiver}

Vetiver, the oil of tranquility, is used as a stabilizing ingredient in perfumery to preserve more volatile fragrances.

If you develop a model…

you can operationalize that model!

If you develop a model…

you likely should be the one to operationalize that model!

Tompkins County housing data

Tompkins County housing data

  • Home sale prices for Tompkins County, NY between 2022-23
  • Can certain measurements be used to predict the sale price?
  • Data collected from Redfin

Tompkins County housing data

R

housing <- read_csv("data/tompkins-home-sales.csv")

Python

import pandas as pd
housing = pd.read_csv('data/tompkins-home-sales.csv')

. . .

  • N = 1,225
  • A numeric outcome, price
  • Other variables to use for prediction:
    • beds, baths, area, and year_built are numeric predictors
    • town and municipality could be nominal predictors
    • sold_date could be a date predictor

Home prices in Tompkins County, NY

sold_date price beds baths area lot_size year_built hoa_month town municipality long lat
2022-08-16 335500 3 2.0 1957 4.50000000 1880 NA Ulysses Unincorporated -76.67680 42.53255
2022-11-14 331500 3 2.0 1416 0.58999082 1930 NA Lansing Unincorporated -76.50347 42.53340
2022-03-31 302385 3 1.5 1476 0.20000000 1900 NA Ithaca Ithaca city -76.50439 42.44250
2022-09-28 285000 3 2.0 1728 0.46999541 2002 NA Dryden Dryden village -76.29495 42.48415
2022-07-22 350000 4 1.0 1698 0.12396694 1925 NA Ithaca Ithaca city -76.50146 42.43264
2023-11-28 225000 2 1.5 1047 0.08000459 1939 NA Ithaca Ithaca city -76.50576 42.43373
2023-09-13 285000 3 2.0 2311 1.26999541 1965 NA Caroline Unincorporated -76.33375 42.39048
2023-06-23 145000 2 2.0 1215 0.03999082 1990 NA Danby Unincorporated -76.49228 42.38340
2023-11-27 90900 5 3.0 2238 0.38000459 1880 NA Groton Groton village -76.36311 42.58533
2022-11-09 467500 6 4.0 2304 0.13000459 2017 NA Ithaca Ithaca city -76.50205 42.43136

Time for building a model!

Spend your data budget

R

library(tidymodels)
set.seed(123)

housing_split <- housing |>
  mutate(price = log10(price)) |>
  initial_split(prop = 0.8)

housing_train <- training(housing_split)
housing_test <- testing(housing_split)

Python

from sklearn import model_selection
import numpy as np
np.random.seed(123)
X, y = housing[["beds", "baths", "area", "year_built"]], np.log10(housing["price"])
X_train, X_test, y_train, y_test = model_selection.train_test_split(
    X, y,
    test_size = 0.2
)

Fit a linear regression model 🚀

Or your model of choice!

R

housing_fit <-
  workflow(
    price ~ beds + baths + area + year_built,
    linear_reg()
  ) |>
  fit(data = housing_train)

Python

from sklearn import linear_model
housing_fit = linear_model.LinearRegression().fit(X_train, y_train)

⏱️ Your turn

Activity

Split your data in training and testing.

Fit a model to your training data.

05:00

Create a deployable bundle

Deploy preprocessors and models together

Create a deployable model object

R

library(vetiver)
v <- vetiver_model(housing_fit, "tompkins-housing")
v

── tompkins-housing ─ <bundled_workflow> model for deployment 
A lm regression modeling workflow using 4 features

Python

from vetiver import VetiverModel
v = VetiverModel(housing_fit, "tompkins-housing", prototype_data = X_train)
v.description
'A scikit-learn LinearRegression model'

Deploy preprocessors and models together

What is wrong with this?

⏱️ Your turn

Activity

Create your vetiver model object.

Check out the default description that is created, and try out using a custom description.

Show your custom description to your neighbor.

05:00

Version your model

How could you share your resources?

Data, models, R/Python objects, etc.

❌ Email
❌ GitHub

🫤 Shared network drive
🫤 Dropbox, Google Drive, Box.com, etc.

✅ Amazon S3
✅ Azure
✅ Google Cloud
✅ Microsoft 365

pins 📌

The pins package publishes data, models, and other R and Python objects, making it easy to share them across projects and with your colleagues.

You can pin objects to a variety of pin boards, including:

  • a local folder (like a network drive or even a temporary directory)
  • Posit Connect
  • Amazon S3
  • Azure Storage
  • Google Cloud

Pin your model

library(pins)

board <- board_temp()
board |> vetiver_pin_write(v)
Creating new version '20251017T160311Z-ed6ba'
Writing to pin 'tompkins-housing'

Create a Model Card for your published model
• Model Cards provide a framework for transparent, responsible reporting
• Use the vetiver `.Rmd` template as a place to start
from pins import board_temp
from vetiver import vetiver_pin_write

board = board_temp(allow_pickle_read = True)
vetiver_pin_write(board, v)
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: 'tompkins-housing'
Version: 20251017T120311Z-1ee78

⏱️ Your turn

Activity

Pin your vetiver model object to a temporary board.

Retrieve the model metadata with pin_meta().

05:00

Version your model

Fit a random forest

rf_rec <- recipe(
  price ~ beds + baths + area + year_built + town,
  data = housing_train
) |>
  step_impute_mean(all_numeric_predictors()) |>
  step_impute_mode(all_nominal_predictors())

housing_fit <- workflow() |>
  add_recipe(rf_rec) |>
  add_model(rand_forest(trees = 200, mode = "regression")) |>
  fit(data = housing_train)
from sklearn.ensemble import RandomForestRegressor
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline

# Define feature columns
numeric_features = ["beds", "baths", "area", "year_built"]
categorical_features = ["town"]

# Create preprocessing steps
numeric_transformer = SimpleImputer(strategy="mean")
categorical_transformer = Pipeline(steps=[
    ("imputer", SimpleImputer(strategy="most_frequent")),
    ("onehot", OneHotEncoder(handle_unknown="ignore"))
])

# Combine preprocessors
preprocessor = ColumnTransformer(
    transformers=[
        ("num", numeric_transformer, numeric_features),
        ("cat", categorical_transformer, categorical_features)
    ]
)

# Create pipeline with preprocessor and model
housing_fit = Pipeline(steps=[
    ("preprocessor", preprocessor),
    ("regressor", RandomForestRegressor(n_estimators=200, random_state=123))
])

# Prepare training data with all features
X_train_full = housing.loc[X_train.index, numeric_features + categorical_features]
housing_fit.fit(X_train_full, y_train)
Pipeline(steps=[('preprocessor',
                 ColumnTransformer(transformers=[('num', SimpleImputer(),
                                                  ['beds', 'baths', 'area',
                                                   'year_built']),
                                                 ('cat',
                                                  Pipeline(steps=[('imputer',
                                                                   SimpleImputer(strategy='most_frequent')),
                                                                  ('onehot',
                                                                   OneHotEncoder(handle_unknown='ignore'))]),
                                                  ['town'])])),
                ('regressor',
                 RandomForestRegressor(n_estimators=200, random_state=123))])
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.

Version your model

R

library(pins)
library(vetiver)

board <- board_temp()
v <- vetiver_model(housing_fit, "tompkins-housing", versioned = TRUE)
board |> vetiver_pin_write(v)

Python

from pins import board_temp
from vetiver import vetiver_pin_write

board = board_temp(Versioned = True, allow_pickle_read = True)
v = VetiverModel(housing_fit, "tompkins-housing", prototype_data = X_train)
vetiver_pin_write(board, v)

⏱️ Your turn

Activity

Create a new vetiver model object using your linear regression model that is explicitly versioned = TRUE and pin to your temporary board.

Then train a random forest model and create a new vetiver model object that is also versioned = TRUE with the same name.

Write this new version of your model to the same pin, and see what versions you have with pin_versions().

05:00

Make it easy to do the right thing

Make it easy to do the right thing

  • Robust and human-friendly checking of new data
  • Track and document software dependencies of models
  • Model cards for transparent, responsible reporting

You can deploy your model as a…

REST API

Application programming interface (API)

An interface that can connect applications in a standard way

  • Representational State Transfer (REST)
  • Uniform Resource Location (URL)

RESTful queries

  1. Submit request to server via URL
  2. Return result in a structured format
  3. Parse results into a local format

Create a vetiver REST API

R

library(plumber)

pr() |>
  vetiver_api(v) |>
  pr_run()

Python

api = VetiverAPI(v)
api.run()

⏱️ Your turn

Activity

TODO need to run on separate ports on Posit Workbench

Create a vetiver API for your model and run it locally.

Explore the visual documentation.

How many endpoints are there?

Discuss what you notice with your neighbor.

05:00

What does “deploy” mean?

What does “deploy” mean?

Where can vetiver deploy?

  • Enterprise software platforms such as Posit Connect and AWS SageMaker
  • A public or private cloud, using Docker

How do you make a request of your new API?

R

library(httr2)
url <- "https://info4940.infosci.cornell.edu/tompkins-housing/metadata"

request(url) |>
  req_perform() |>
  resp_body_json()

Python

import requests

url = "https://pub.demo.posit.team/public/seattle-housing-python/metadata" 
print(requests.get(url).content)

How do you make a request of your new API?

  • Python or R packages like requests or {httr2}
  • curl
  • There is special support in vetiver for the /predict endpoint

Any tool that can make an HTTP request can be used to interact with your model API!

Create a vetiver endpoint

You can treat your model API much like it is a local model in memory!

R

library(vetiver)

url <- "https://info4940.infosci.cornell.edu/tompkins-housing/predict"
endpoint <- vetiver_endpoint(url)
predict(endpoint, slice_sample(housing_test, n = 5))

Python

from vetiver.server import predict, vetiver_endpoint

url = "https://info4940.infosci.cornell.edu/tompkins-housing/predict"
endpoint = vetiver_endpoint(url)
predict(endpoint = endpoint, data = X_test.head(5))

⏱️ Your turn

Activity

Create a vetiver endpoint object for your API.

Predict with your endpoint for new data.

Optional: call another endpoint like /ping or /metadata.

05:00

Wrap-up

Recap

  • ML models can be deployed as APIs
  • Use pins to share your models
  • vetiver can help you bundle, version, and deploy your models

Acknowledgments