Deploy and Serve a Feature List for Model Predictions¶
Once you have a feature list ready, it's essential to serve these features effectively to make real-time or batch predictions using your machine learning model.
In this section, we'll take the feature list we previously crafted and explore two primary ways to serve its values:
REST API: Ideal for real-time predictions where you need instantaneous results. For instance, in applications where user interactions require immediate feedback based on model predictions.
Batch Processing: Best suited for scenarios where you have a bulk of data and don't require instant results.
Important Note for FeatureByte Enterprise Users¶
In Catalogs with Approval Flow enabled, moving features to production-ready status involves a comprehensive approval process.
This includes several evaluations, such as checking the feature's compliance with default cleaning operations and the feature job setting of its source tables. It also involves confirming the status of these tables and backtesting the feature job setting to prevent future training-serving inconsistencies. Additionally, essential details of the feature, particularly its feature definition file, are shared and subjected to a thorough review.
import featurebyte as fb
from datetime import datetime
# Set your profile to the tutorial environment
fb.use_profile("tutorial")
catalog_name = "Grocery Dataset Tutorial"
catalog = fb.Catalog.activate(catalog_name)
10:59:49 | WARNING | Service endpoint is inaccessible: http://featurebyte-server:8088/ 10:59:49 | INFO | Using profile: tutorial 10:59:50 | INFO | Using configuration file at: /Users/gxav/.featurebyte/config.yaml 10:59:50 | INFO | Active profile: tutorial (https://tutorials.featurebyte.com/api/v1) 10:59:50 | INFO | SDK version: 2.0.1.dev67 10:59:50 | INFO | No catalog activated. 10:59:50 | INFO | Catalog activated: Grocery Dataset Tutorial 16:13:11 | INFO | Using configuration file at: /Users/gxav/.featurebyte/config.yaml 16:13:11 | INFO | Active profile: tutorial (https://tutorials.featurebyte.com/api/v1) 16:13:11 | WARNING | Remote SDK version (1.1.0.dev7) is different from local (1.1.0.dev1). Update local SDK to avoid unexpected behavior. 16:13:11 | INFO | No catalog activated. 16:13:11 | INFO | Catalog activated: Grocery Dataset Tutorial
List feature lists in Catalog¶
catalog.list_feature_lists()
id | name | num_feature | status | deployed | readiness_frac | online_frac | tables | entities | primary_entity | created_at | |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 676239f4c3ba2239e045779a | Customer x ProductGroup Simple FeatureList | 9 | DRAFT | False | 0.0 | 0.0 | [GROCERYCUSTOMER, GROCERYINVOICE, INVOICEITEMS... | [customer, productgroup] | [customer, productgroup] | 2024-12-18T02:57:16.277000 |
Get a feature list from Catalog¶
simple_feature_list = catalog.get_feature_list("Customer x ProductGroup Simple FeatureList")
Loading Feature(s) |████████████████████████████████████████| 9/9 [100%] in 0.3s Loading Feature(s) |████████████████████████████████████████| 9/9 [100%] in 0.2s
Deploy feature list¶
# Create a deployment
deployment = simple_feature_list.deploy(
deployment_name="In-Store Customer x ProductGroup Spending 2w Spending - 9 features",
make_production_ready=True,
)
Done! |████████████████████████████████████████| 100% in 6.1s (0.17%/s) Done! |████████████████████████████████████████| 100% in 6.1s (0.17%/s) Done! |████████████████████████████████████████| 100% in 6.1s (0.17%/s) Done! |████████████████████████████████████████| 100% in 6.1s (0.17%/s)
# Enable deployment
deployment.enable()
Done! |████████████████████████████████████████| 100% in 2:53.2 (0.01%/s) Done! |████████████████████████████████████████| 100% in 1:49.2 (0.01%/s)
# Check that the deployment is enabled
catalog.list_deployments()
id | name | feature_list_name | feature_list_version | num_feature | enabled | |
---|---|---|---|---|---|---|
0 | 67623aadf49d772007a631ad | In-Store Customer x ProductGroup Spending 2w S... | Customer x ProductGroup Simple FeatureList | V241218 | 9 | True |
# Check status of the feature list
print("simple_feature_list.status:", simple_feature_list.status)
simple_feature_list.status: DEPLOYED
Get template for online serving¶
# Get a python template for consuming the feature serving API
deployment.get_online_serving_code(language="python")
from typing import Any, Dict
import pandas as pd
import requests
def request_features(entity_serving_names: Dict[str, Any]) -> pd.DataFrame:
"""
Send POST request to online serving endpoint
Parameters
----------
entity_serving_names: Dict[str, Any]
Entity serving name values to used for serving request
Returns
-------
pd.DataFrame
"""
response = requests.post(
url="https://tutorials.featurebyte.com/api/v1/deployment/67623aadf49d772007a631ad/online_features",
headers={"Content-Type": "application/json", "active-catalog-id": "676225fc990c9e1a061d72ab", "Authorization": "Bearer alZJY8HvLxoi_emU_eukghMpSMzxL0eQ4ufUSWxvsLk"},
json={"entity_serving_names": entity_serving_names},
)
assert response.status_code == 200, response.json()
return pd.DataFrame.from_dict(response.json()["features"])
request_features([{"GROCERYCUSTOMERGUID": "016d7230-8602-42ef-9861-c672f48fb010", "PRODUCTGROUP": "Gla\u00e7ons"}])
# Get shell script
deployment.get_online_serving_code(language="sh")
#!/bin/sh
curl -X POST \
-H 'Content-Type: application/json' \
-H 'active-catalog-id: 676225fc990c9e1a061d72ab' \
-H 'Authorization: Bearer alZJY8HvLxoi_emU_eukghMpSMzxL0eQ4ufUSWxvsLk' \
-d '{"entity_serving_names": [{"GROCERYCUSTOMERGUID": "016d7230-8602-42ef-9861-c672f48fb010", "PRODUCTGROUP": "Gla\u00e7ons"}]}' \
https://tutorials.featurebyte.com/api/v1/deployment/67623aadf49d772007a631ad/online_features
Create batch request table¶
Although batch request is not suitable for our "In-Store" use case, let's run a batch request for all current customers for a campaign for "Fromages".
# Get view of current customer
customer_table = catalog.get_table("GROCERYCUSTOMER")
customer_view = customer_table.get_view(view_mode="manual")
cond = customer_view.CurrentRecord == True
current_customer_view = customer_view[cond]
# Check primary entity of the deployed feature list to obtain its serving name
simple_feature_list.primary_entity
[<featurebyte.api.entity.Entity at 0x329ecaed0> { 'name': 'customer', 'created_at': '2024-12-18T02:40:54.536000', 'updated_at': '2024-12-18T02:40:58.932000', 'description': None, 'serving_names': [ 'GROCERYCUSTOMERGUID' ], 'catalog_name': 'Grocery Dataset Tutorial' }, <featurebyte.api.entity.Entity at 0x329ecaf70> { 'name': 'productgroup', 'created_at': '2024-12-18T02:40:55.766000', 'updated_at': '2024-12-18T02:41:00.663000', 'description': None, 'serving_names': [ 'PRODUCTGROUP' ], 'catalog_name': 'Grocery Dataset Tutorial' }]
# Add PRODUCTGROUP to the view with "Fromages" for a specific targeted campaign.
current_customer_view["PRODUCTGROUP"] = "Fromages"
# Create batch request table from the view
batch_request_table = current_customer_view.create_batch_request_table(
name="Current Customers for Fromages at " + datetime.now().strftime("%Y%m%d:%H%M"),
columns=["GroceryCustomerGuid", "PRODUCTGROUP"],
columns_rename_mapping={
"GroceryCustomerGuid": "GROCERYCUSTOMERGUID",
}
)
Done! |████████████████████████████████████████| 100% in 9.1s (0.11%/s) Done! |████████████████████████████████████████| 100% in 12.2s (0.08%/s)
# Get name of the batch request table
batch_request_table.name
'Current Customers for Fromages at 20241218:1103'
# List batch request tables in catalog
catalog.list_batch_request_tables()
id | name | type | shape | feature_store_name | created_at | |
---|---|---|---|---|---|---|
0 | 67623b6bf49d772007a631ae | Current Customers for Fromages at 20241218:1103 | view | [500, 2] | playground | 2024-12-18T03:03:13.469000 |
Compute batch feature table¶
# Get deployment and batch request table
deployment = catalog.get_deployment("In-Store Customer x ProductGroup Spending 2w Spending - 9 features")
batch_request_table = catalog.get_batch_request_table(batch_request_table.name)
# Compute batch features
batch_features = deployment.compute_batch_feature_table(
batch_request_table=batch_request_table,
batch_feature_table_name =
f"Customer Simple FeatureList for Spending forecast with {batch_request_table.name}"
)
Done! |████████████████████████████████████████| 100% in 9.1s (0.11%/s) Done! |████████████████████████████████████████| 100% in 12.1s (0.08%/s)
# List observation tables
catalog.list_batch_feature_tables()
id | name | feature_store_name | batch_request_table_name | shape | created_at | |
---|---|---|---|---|---|---|
0 | 67623b75f49d772007a631af | Customer Simple FeatureList for Spending forec... | playground | Current Customers for Fromages at 20241218:1103 | [500, 11] | 2024-12-18T03:03:21.865000 |
# Convert to pandas
batch_features.to_pandas()
Downloading table |████████████████████████████████████████| 500/500 [100%] in 0
GROCERYCUSTOMERGUID | PRODUCTGROUP | CUSTOMER_Age_band | CUSTOMER_Latest_invoice_Amount | CUSTOMER_Count_of_invoice_14d | CUSTOMER_Avg_of_invoice_Amount_14d | CUSTOMER_Std_of_invoice_Amount_14d | CUSTOMER_Latest_invoice_Amount_Z_Score_to_invoice_Amount_28d | CUSTOMER_vs_OVERALL_item_TotalCost_across_product_ProductGroups_26w | CUSTOMER_x_PRODUCTGROUP_Sum_of_item_TotalCost_14d | CUSTOMER_x_PRODUCTGROUP_Time_Since_Latest_Timestamp | |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 09f35825-38ef-4a01-8385-c41822f59de9 | Fromages | 50-54 | 41.78 | NaN | NaN | NaN | 1.000000 | 0.412312 | NaN | 1810.671115 |
1 | 7eee31c3-c152-4cc3-a87b-aeed2e015039 | Fromages | 70-74 | 8.00 | 1.0 | 8.000000 | 0.000000 | -1.000000 | 0.343774 | NaN | 5365.382226 |
2 | 6a9598c3-26ba-4784-8551-9c20819c5690 | Fromages | 70-74 | 7.28 | NaN | NaN | NaN | NaN | 0.610161 | NaN | 1735.358338 |
3 | 8440debb-6abc-4adc-8c6c-749928141fd0 | Fromages | 20-24 | 106.18 | 1.0 | 106.180000 | 0.000000 | 1.000000 | 0.643433 | 7.5 | 15.810838 |
4 | d3c61041-64b6-4f0b-8c89-c3aaf20aae61 | Fromages | 25-29 | 6.27 | NaN | NaN | NaN | NaN | 0.601962 | NaN | 2011.292782 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
495 | bed95154-2bd8-4947-9090-54d14da66643 | Fromages | 35-39 | 54.05 | NaN | NaN | NaN | NaN | 0.522650 | NaN | 1048.096115 |
496 | 8c3066bd-f278-4f46-871b-7737f8847aaa | Fromages | 40-44 | 7.58 | NaN | NaN | NaN | -1.000000 | 0.822351 | NaN | 565.231949 |
497 | 69134758-d8c6-46e2-b2e5-6c8a3095d1d4 | Fromages | 30-34 | 9.79 | 1.0 | 9.790000 | 0.000000 | NaN | 0.467934 | NaN | 1189.893338 |
498 | 1b49a621-805a-4626-9f92-30077d7ef9bf | Fromages | 65-69 | 6.49 | 2.0 | 4.840000 | 1.650000 | -0.491692 | 0.351327 | NaN | 397.866393 |
499 | 3f922a2f-529e-4d8b-84a2-b9b5847ba455 | Fromages | 65-69 | 9.01 | 3.0 | 8.226667 | 5.202079 | -0.344543 | 0.650721 | NaN | 2074.993893 |
500 rows × 11 columns
Downloading table |████████████████████████████████████████| 500/500 [100%] in 0
GROCERYCUSTOMERGUID | PRODUCTGROUP | CUSTOMER_Age_band | CUSTOMER_Latest_invoice_Amount | CUSTOMER_Count_of_invoice_14d | CUSTOMER_Avg_of_invoice_Amount_14d | CUSTOMER_Std_of_invoice_Amount_14d | CUSTOMER_Latest_invoice_Amount_Z_Score_to_invoice_Amount_28d | CUSTOMER_vs_OVERALL_item_TotalCost_across_product_ProductGroups_26w | CUSTOMER_x_PRODUCTGROUP_Sum_of_item_TotalCost_14d | CUSTOMER_x_PRODUCTGROUP_Time_Since_Latest_Timestamp | |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 14f94723-4cae-4041-a11a-a044df1a5a5f | Fromages | 60-64 | 14.28 | NaN | NaN | NaN | 1.000000 | 0.373275 | NaN | 1527.861052 |
1 | d4559f7d-eb28-42c6-b47d-847de24952c2 | Fromages | 65-69 | 20.04 | NaN | NaN | NaN | NaN | 0.370545 | NaN | 2177.017718 |
2 | 1d363f8c-88f4-49eb-a107-4f1e2fbe6722 | Fromages | 25-29 | 73.16 | NaN | NaN | NaN | NaN | 0.781391 | NaN | 1118.766885 |
3 | 768e57df-7513-412b-a466-effd270ad6a6 | Fromages | 65-69 | 7.16 | 3.0 | 8.246667 | 4.395121 | -0.644055 | 0.650126 | NaN | 1357.819107 |
4 | 48f6e550-ce63-457e-ad14-d511ee934661 | Fromages | 40-44 | 24.62 | 1.0 | 24.620000 | 0.000000 | -0.219845 | 0.753619 | NaN | 475.681329 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
495 | c1b3224a-5dbb-427d-87d3-3882c6bb1278 | Fromages | 50-54 | 6.69 | NaN | NaN | NaN | NaN | 0.104953 | NaN | NaN |
496 | 261d2979-0568-4544-8be0-32355049e565 | Fromages | 65-69 | 2.00 | NaN | NaN | NaN | NaN | 0.317982 | NaN | NaN |
497 | 825ee709-2f85-49e2-bec7-27bea06efe3a | Fromages | 45-49 | 3.00 | 2.0 | 5.995000 | 2.995000 | -0.990561 | 0.375129 | NaN | NaN |
498 | ea68ce6b-b3c3-4ba5-a471-f11b1c5bd892 | Fromages | 45-49 | 4.50 | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
499 | b8b00613-2e48-4eed-8971-61718990a994 | Fromages | 45-49 | 10.00 | 2.0 | 6.000000 | 4.000000 | 1.264961 | 0.579119 | NaN | NaN |
500 rows × 11 columns
# download parquet file
batch_features.download()
Downloading table |████████████████████████████████████████| 500/500 [100%] in 0
PosixPath('BATCH_FEATURE_TABLE_67623b7553faf734e68b1ed2.parquet')
Downloading table |████████████████████████████████████████| 500/500 [100%] in 0
PosixPath('BATCH_FEATURE_TABLE_666959284c98806ac301afa0.parquet')
# delete if not needed any more
batch_features.delete()
batch_request_table.delete()
Done! |████████████████████████████████████████| 100% in 9.2s (0.11%/s) Done! |████████████████████████████████████████| 100% in 6.1s (0.17%/s) Done! |████████████████████████████████████████| 100% in 6.1s (0.17%/s) Done! |████████████████████████████████████████| 100% in 6.1s (0.17%/s)
Manage Deployment¶
deployment = catalog.get_deployment("In-Store Customer x ProductGroup Spending 2w Spending - 9 features")
# get feature jobs status (this will produce meaningful results only once multiple jobs have been run)
# deployment.get_feature_jobs_status(job_history_window=12)
Job statistics (last 12 hours)
request_date | job_history_window | job_duration_tolerance | |
---|---|---|---|
0 | 2024-06-12T08:16:06.814724 | 12 | 60 |
feature_name | aggregation_hash | |
---|---|---|
0 | CUSTOMER_Avg_of_invoice_Amount_14d | 94bc6df7 |
1 | CUSTOMER_Count_of_invoice_14d | 2bc63818 |
2 | CUSTOMER_Latest_invoice_Amount | d6976a85 |
3 | CUSTOMER_Latest_invoice_Amount_Z_Score_to_invoice_Amount_28d | d6976a85 |
4 | CUSTOMER_Latest_invoice_Amount_Z_Score_to_invoice_Amount_28d | 94bc6df7 |
5 | CUSTOMER_Latest_invoice_Amount_Z_Score_to_invoice_Amount_28d | 906f9cb2 |
6 | CUSTOMER_Std_of_invoice_Amount_14d | 906f9cb2 |
7 | CUSTOMER_vs_OVERALL_item_TotalCost_across_product_ProductGroups_26w | 0b11a7c6 |
8 | CUSTOMER_vs_OVERALL_item_TotalCost_across_product_ProductGroups_26w | 76b8b9b9 |
9 | CUSTOMER_x_PRODUCTGROUP_Sum_of_item_TotalCost_14d | 1e1caeb1 |
10 | CUSTOMER_x_PRODUCTGROUP_Time_Since_Latest_Timestamp | e7a12f5d |
aggregation_hash | frequency(min) | completed_jobs | max_duration(s) | 95 percentile | frac_late | exceed_period | failed_jobs | incomplete_jobs | time_since_last | |
---|---|---|---|---|---|---|---|---|---|---|
0 | d6976a85 | 60 | 0 | NaN | NaN | NaN | 0 | 0 | 12 | NaT |
1 | 2bc63818 | 60 | 0 | NaN | NaN | NaN | 0 | 0 | 12 | NaT |
2 | 94bc6df7 | 60 | 0 | NaN | NaN | NaN | 0 | 0 | 12 | NaT |
3 | 906f9cb2 | 60 | 0 | NaN | NaN | NaN | 0 | 0 | 12 | NaT |
4 | 0b11a7c6 | 60 | 0 | NaN | NaN | NaN | 0 | 0 | 12 | NaT |
5 | 76b8b9b9 | 60 | 0 | NaN | NaN | NaN | 0 | 0 | 12 | NaT |
6 | 1e1caeb1 | 60 | 0 | NaN | NaN | NaN | 0 | 0 | 12 | NaT |
7 | e7a12f5d | 60 | 0 | NaN | NaN | NaN | 0 | 0 | 12 | NaT |
# Disable deployment
deployment.disable()
Done! |████████████████████████████████████████| 100% in 15.2s (0.07%/s) Done! |████████████████████████████████████████| 100% in 12.1s (0.08%/s)
# The deployment is still part of the catalog but disabled
catalog.list_deployments()
id | name | feature_list_name | feature_list_version | num_feature | enabled | |
---|---|---|---|---|---|---|
0 | 67623aadf49d772007a631ad | In-Store Customer x ProductGroup Spending 2w S... | Customer x ProductGroup Simple FeatureList | V241218 | 9 | False |