Skip to main content

Continuous Models

Updated today

Continuous models are automatically executed when a profile is loaded (via the web, mobile, or CTV) or when relevant profile properties are updated (from any source). All outputs defined by the model are stored as profile properties on the profile.

Use continuous models to power use cases such as:

  • Look-alike modeling: identify new high-value audiences based on real-time behavior.

  • Propensity modeling: predict the likelihood to buy, churn, or click in the moment.


Before you begin

Before you upload or create a continuous model, confirm the following:

  • You have access to the AI Workbench in BlueConic.

  • Your model is in ONNX format, or you have the source model ready to convert.

  • The profile properties you plan to use as model inputs exist in BlueConic.

  • The profile properties where model outputs should be stored exist in BlueConic.


Continuous model format

Continuous models are stored in the ONNX format. The input of the model is a profile vector, and the model can have one or more outputs. Each output corresponds to a profile property that should be updated by the model.

Inputs

profile (optional float[]): a float tensor containing the vectorized profile, based on the profileProperties and featureNames metadata. If no profile input is defined, the model will be executed without any profile input.

Outputs

The model does not have any fixed outputs. Instead, each output corresponds to a profile property to be updated by the model. Outputs can be scalar values or one-dimensional tensors. Multi-dimensional tensors are not supported because they do not map directly to the profile data model.

Metadata

Each continuous model requires the following metadata:

Metadata field

Python API name

Description

profilePropertyIds

property_ids

The profile property IDs that are used as input features for the model (for example ['age', 'city=nijmegen']).

featureNames

feature_names

The names of the features that are relevant for the model. The profile input will be filled based on these feature names.

segmentId

segment_id

(Optional) An optional segment ID that restricts model execution to profiles that are members of this segment. If not provided, the model is executed for all profiles.


Upload a continuous model

You can upload a continuous model to BlueConic using the UI, the AI Workbench Python API, or the REST API.

Using the UI

  1. Log into BlueConic and navigate to More.

  2. Select AI Workbench from the drop down menu.

  3. Go to the Models tab in the AI Workbench.

  4. Click Add Model.

  5. Set the model type to Continuous.

  6. Upload your ONNX model file.

  7. Select one or more Profile properties to use as inputs for the model.

  8. Copy the Feature names.

  9. (Optional) Select a Segment to restrict model execution to specific profiles.

  10. Click Save.

Using the AI Workbench (Python notebook)

If you plan to run the AI Workbench notebook on a schedule to regularly retrain your model, we recommend using the update_model method in combination with a model parameter.

First, define your parameters:

import blueconic

bc = blueconic.Client()

# the profile properties to train the model on

PROFILE_PROPERTY_IDS = bc.get_blueconic_parameter_values(

"Profile properties", "profile_property"

)

if not PROFILE_PROPERTY_IDS:

raise ValueError("Please configure profile properties to train model on")

# where the model should be stored

MODEL_ID = bc.get_blueconic_parameter_value("Model", "model")

if not MODEL_ID:

raise ValueError("Please configure a model")

# restrict scoring to this specific segment

SCORING_SEGMENT_ID = bc.get_blueconic_parameter_value("Scoring segment", "segment")

# the profile property that should contain the score from the model

OUTPUT_PROPERTY_ID = bc.get_blueconic_parameter_value(

"Score property", "profile_property"

)

if not OUTPUT_PROPERTY_ID:

raise ValueError("Please configure a Score property")

Then, once you've trained a vectorizer (in this example called my_dict_vectorizer) and a model (in this example called my_onnx_model):

model = bc.get_model(MODEL_ID)

model.type = blueconic.domain.ModelType.CONTINUOUS

model.model = my_onnx_model

model.property_ids = PROFILE_PROPERTY_IDS

model.feature_names = my_dict_vectorizer.get_feature_names_out().tolist()

model.segment_id = SCORING_SEGMENT_ID

bc.update_model(model)

Note: For more information, see the Models Python API documentation. For details on how to create my_onnx_model, see Converting the model to ONNX format below.

Using the REST API

Continuous models can also be pushed to the CDP through the REST API, for example as part of an external model pipeline.

Note: For more information, see the Models REST API documentation.


Create a new continuous model

To build a continuous model from scratch, follow these steps:

  1. Convert profiles to feature dictionaries and labels.

  2. Train a DictVectorizer and convert feature dictionaries to feature vectors.

  3. Train a model on the resulting data.

  4. Convert the model to ONNX format.

Step 1: Convert profiles to feature dictionaries

You can use the profile_to_feature_dict method to convert a BlueConic Profile object to a feature dictionary. We recommend using this method to ensure that the data your model is trained on matches the data that will be passed to the model when it is executed.

from blueconic.utils import profile_to_feature_dict

# retrieve training data

profile_feature_dicts = []

for profile in bc.get_profiles():

profile_feature_dicts.append(profile_to_feature_dict(profile))

Note: Storing all data in memory, as in the example above, only works for small datasets. For larger segments you will likely need to store data on disk, for example using sqlite.

Step 2: Convert feature dictionaries to feature vectors

Once you have transformed profiles into feature dictionaries, the next step is to turn those dictionaries into feature vectors. You can typically use the standard scikit-learn DictVectorizer for this purpose:

from sklearn.feature_extraction import DictVectorizer

# vectorize the profile data

vectorizer = DictVectorizer(sparse=False)

X = vectorizer.fit_transform(profile_feature_dicts)

del profile_feature_dicts

Step 3: Convert the model to ONNX format

Once trained, convert your model to ONNX format. BlueConic supports two approaches.

Converting a scikit-learn model to ONNX format

Assuming you have trained your model using the scikit-learn package, you can use sklearn-onnx to convert the model to ONNX. After conversion, make the following adjustments:

  1. Change the input name to profile.

  2. Disable returning a dictionary by setting the zipmap option to False (for classifiers).

  3. Remove the class label output (for classifiers).

  4. Rename the output to the profile property ID where the value should be stored.

from skl2onnx import to_onnx

from skl2onnx.common.data_types import FloatTensorType

from onnx import helper, TensorProto

OUTPUT_PROPERTY_ID = bc.get_blueconic_parameter_value("Propensity property", "profile_property")

if not OUTPUT_PROPERTY_ID:

raise ValueError("Please configure a Propensity property")

def convert_sklearn_model(model, propensity_property_id: str):

"""

Convert a scikit-learn propensity model to a model that can be used in BlueConic.

"""

onnx_model = to_onnx(

model=model,

# rename the input to "profile"

initial_types=[("profile", FloatTensorType([model.n_features_in_]))],

# disable returning a dictionary, we want the propensity output to be a number

options={"zipmap": False}

)

# create a constant containing 1

# this constant will be used both to gather the positive propensity

# and to squeeze the result

const_one_node = helper.make_node(

"Constant",

inputs=[],

outputs=["const_one"],

value=helper.make_tensor(

name="const_one",

data_type=TensorProto.INT64,

dims=[1],

vals=[1],

)

)

onnx_model.graph.node.append(const_one_node)

# retrieve the probability of the positive class from the model

gather_node = helper.make_node(

"Gather",

inputs=["probabilities", "const_one"],

outputs=["positive_probability_tensor"],

axis=1,

)

onnx_model.graph.node.append(gather_node)

# convert a multidimensional tensor to an array

squeeze_node = helper.make_node(

"Squeeze",

inputs=["positive_probability_tensor", "const_one"],

outputs=[propensity_property_id]

)

onnx_model.graph.node.append(squeeze_node)

# remove the existing outputs

del onnx_model.graph.output[:]

# add the propensity property output

onnx_model.graph.output.append(helper.make_tensor_value_info(

propensity_property_id, TensorProto.FLOAT, [1]

))

return onnx_model

my_onnx_model = convert_sklearn_model(pipeline, OUTPUT_PROPERTY_ID)

Create an ONNX model from scratch

You can also use the onnx package to directly define your computational graph. The example below builds a simple RFM model that, based on pre-calculated thresholds, calculates a score between 1 and 5 for recency, frequency, and monetary value.

from onnx import helper, TensorProto

import numpy as np

def create_rfm_onnx_model(

recency_thresholds, frequency_thresholds, monetary_thresholds

):

"""

Create an ONNX model that scores a customer between 1-5 for three aspects:

* Recency: how long since the last order?

* Frequency: how many orders?

* Monetary value: how much did they spend?

"""

# profile feature vector — contains:

# 1. the number of days since the last order

# 2. the number of orders

# 3. the total spend

# these three profile properties should be added to the

# `property_ids` and `feature_names` metadata of the model

profile_input = helper.make_tensor_value_info("profile", TensorProto.FLOAT, [3])

# define separate outputs for the scores

# profile properties with IDs matching the output name must exist

recency_score_output = helper.make_tensor_value_info("recency_score", TensorProto.INT64, [1])

frequency_score_output = helper.make_tensor_value_info("frequency_score", TensorProto.INT64, [1])

monetary_score_output = helper.make_tensor_value_info("monetary_score", TensorProto.INT64, [1])

nodes = []

# define thresholds as constants (the 20th, 40th, 60th, and 80th percentiles)

for col, thresholds in [

("recency", recency_thresholds),

("frequency", frequency_thresholds),

("monetary", monetary_thresholds),

]:

nodes.append(helper.make_node(

op_type="Constant", inputs=[], outputs=[f"{col}_thresholds"],

value=helper.make_tensor(

name=f"{col}_thresholds", data_type=TensorProto.FLOAT,

dims=[4], vals=thresholds[1:5],

),

))

# constants used in scoring calculations

for name, val in [("five", 5), ("zero", 0), ("one", 1)]:

nodes.append(helper.make_node(

op_type="Constant", inputs=[], outputs=[name],

value=helper.make_tensor(name=name, data_type=TensorProto.INT64, dims=[1], vals=[val]),

))

# split the profile vector into R, F, M

nodes.append(helper.make_node(

op_type="Split", inputs=["profile"],

outputs=["recency_raw", "frequency_raw", "monetary_raw"],

axis=0, num_outputs=3,

))

for col in ["recency", "frequency", "monetary"]:

nodes.append(helper.make_node("Greater", inputs=[f"{col}_raw", f"{col}_thresholds"], outputs=[f"{col}_bools"]))

nodes.append(helper.make_node("Cast", inputs=[f"{col}_bools"], outputs=[f"{col}_integers"], to=TensorProto.INT64))

nodes.append(helper.make_node("ReduceSum", inputs=[f"{col}_integers", "zero"], outputs=[f"{col}_sum"], keepdims=1))

if col == "recency":

nodes.append(helper.make_node("Sub", inputs=["five", f"{col}_sum"], outputs=[f"{col}_score"]))

else:

nodes.append(helper.make_node("Add", inputs=["one", f"{col}_sum"], outputs=[f"{col}_score"]))

graph = helper.make_graph(

nodes=nodes, name="RFM", inputs=[profile_input],

outputs=[recency_score_output, frequency_score_output, monetary_score_output],

)

return helper.make_model(

graph=graph, ir_version=10, opset_imports=[helper.make_opsetid("", 21)]

)

# create an ONNX RFM model based on example thresholds

onnx_model = create_rfm_onnx_model(

recency_thresholds=[0, 24, 48, 72, 168, 1000],

frequency_thresholds=[0, 1, 2, 5, 10, 100],

monetary_thresholds=[0, 10, 50, 100, 500, 10000],

)

Note: When creating your own ONNX model, it's important to set the ir_version and opsetid to ensure your model is compatible with the platform.

Step 4: Unit test your model

It's good practice to validate the output of your ONNX model to ensure the output matches your expectations. You can use the onnxruntime package to execute ONNX models.

First, import the required packages and set up a unit testing framework:

import onnxruntime as ort

import ipytest

ipytest.autoconfig(raise_on_error=True)

Then add your unit tests. For example, to test the RFM model above:

%%ipytest

def test_rfm_model():

onnx_model = create_rfm_onnx_model(

recency_thresholds=[0, 24, 48, 72, 168, 1000],

frequency_thresholds=[0, 1, 2, 5, 10, 100],

monetary_thresholds=[0, 10, 50, 100, 500, 10000]

)

session = ort.InferenceSession(onnx_model.SerializeToString())

output = session.run(output_names=None, input_feed={"profile": [80, 3, 60]})

assert output[0] == [2]

assert output[1] == [3]

assert output[2] == [3]


FAQ

When does a continuous model run?

  • A continuous model runs automatically whenever a profile is loaded via web, mobile, or CTV, or when relevant profile properties are updated from any source.

Can I restrict a model to a specific segment?

  • Yes. Set the segmentId (segment_id in the Python API) metadata field to a segment ID. The model only runs for profiles that are members of that segment.

What output types does a continuous model support?

  • Outputs can be scalar values or one-dimensional tensors. Multi-dimensional tensors are not supported because they don't map directly to the profile data model.

Is the profile input required?

  • No. The profile input is optional. If you don't define it, the model runs without any profile data as input.

Which ONNX conversion tools does BlueConic support?

BlueConic works with any valid ONNX model. Common conversion tools include:

Did this answer your question?