{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "1e4bc018f9b64464",
      "metadata": {
        "codeCollapsed": true
      },
      "source": "# Kumo Relational Quickstart with structured-data-models-client\n\n**Kumo Relational** is a relational foundation model for\nmachine learning on enterprise data. With your relational data and a\nPredictive Query Language (PQL) query, you can generate predictions without\ntraining a task-specific model.\n\nThis quickstart uses four tables from Microsoft's **AdventureWorks** OLTP\nsample database: customers, products, sales-order headers, and sales-order\ndetails. Predictions are submitted to a deployed Kumo Relational NVIDIA NIM through\n`structured-data-models-client`.\n\nEach user is responsible for checking the content of datasets and the applicable licenses and determining if suitable for the intended use."
    },
    {
      "cell_type": "markdown",
      "id": "c333f1688de74b17",
      "metadata": {
        "codeCollapsed": true
      },
      "source": "## Introduction\n\nKumo Relational is grounded in three key world views:\n\n> **1. Enterprise data is a graph.**\n\nEnterprise data is a graph where tables are connected by keys. In this notebook, `sales_order_headers` connects customers to orders, while `sales_order_details` connects each order to its products. Together, these relationships give Kumo Relational the business context surrounding every transaction.\n\nOnce we structure enterprise data this way, we can apply pre-trained Relational Graph Transformers to extract insights and patterns.\n\n> **2. With timestamps, we place events on a timeline.**\n\nBy placing events on a timeline, we unlock the ability to model how things evolve. We can select any point in time and predict what is likely to happen next from the sequence and patterns in the historical data.\n\n> **3. Machine learning tasks can be described via predictive queries.**\n\nMajor machine learning tasks, including regression, classification, forecasting, and recommendation, can be expressed using *Predictive Query Language (PQL)*. A PQL query identifies the outcome to predict, the entity to predict it for, and, for temporal tasks, the future prediction window.\n\nIf you know SQL, PQL should feel familiar. Learn more in the [query-writing guide](https://docs.nvidia.com/sdgm/kumo-relational/user-guide/make-predictions/writing-predictive-queries)."
    },
    {
      "cell_type": "markdown",
      "id": "454eac1b23f249df",
      "metadata": {
        "codeCollapsed": true
      },
      "source": "**Let's get started!**"
    },
    {
      "cell_type": "markdown",
      "id": "fc9ae315749f4538",
      "metadata": {
        "codeCollapsed": true
      },
      "source": "## Step 1. Install structured-data-models-client\n\nInstall the SDK with the relational extra. See [Deploy, Install, and Connect](https://docs.nvidia.com/sdgm/kumo-relational/get-started/sdk-getting-started) for the supported Python versions and platforms."
    },
    {
      "cell_type": "code",
      "id": "a24f971d5d724e15",
      "execution_count": null,
      "metadata": {
        "language": "python"
      },
      "outputs": [],
      "source": "%pip install \"structured-data-models-client[relational]\""
    },
    {
      "cell_type": "code",
      "id": "6aac3912c4a144b8",
      "execution_count": null,
      "metadata": {
        "language": "python"
      },
      "outputs": [],
      "source": "import os\nfrom pathlib import Path\n\nimport pandas as pd\nfrom sdm_client import SDMClient, Schema, PredictiveQuery, KumoRelational, Relationship"
    },
    {
      "cell_type": "markdown",
      "id": "1e6663f0c2574e5e",
      "metadata": {
        "codeCollapsed": true
      },
      "source": "Build schemas and queries through `sdm_client`'s top-level names: `Schema`, `Relationship`, and `PredictiveQuery`. Submit predictions through `SDMClient`, naming the model with `KumoRelational()`."
    },
    {
      "cell_type": "markdown",
      "id": "40af00956c7342d2",
      "metadata": {
        "codeCollapsed": true
      },
      "source": "## Step 2. Configure the Kumo Relational NIM endpoint\n\nDeploy Kumo Relational as an NVIDIA NIM before running this notebook. See\n[Deploy, Install, and Connect](https://docs.nvidia.com/sdgm/kumo-relational/get-started/sdk-getting-started)\nfor deployment instructions.\n\nSet `KUMO_RELATIONAL_API_ENDPOINT` to the reachable NIM endpoint. The default below uses a\nlocal container on port `8000`. Set `KUMO_RELATIONAL_API_KEY` only when an\nauthenticating gateway protects the endpoint."
    },
    {
      "cell_type": "code",
      "id": "e1bcc8f7a5024ff5",
      "execution_count": null,
      "metadata": {
        "language": "python"
      },
      "outputs": [],
      "source": "NIM_URL = os.environ.get(\n    \"KUMO_RELATIONAL_API_ENDPOINT\",\n    \"http://<dns_name>:8000\",\n)\nNIM_API_KEY = os.environ.get(\"KUMO_RELATIONAL_API_KEY\")"
    },
    {
      "cell_type": "markdown",
      "id": "bb2867af180a486f",
      "metadata": {
        "codeCollapsed": true
      },
      "source": "## Step 3. Connect to the NIM\n\nAn `SDMClient` owns the HTTP connection to one NIM endpoint. Keep credentials\nin environment configuration rather than notebook source."
    },
    {
      "cell_type": "code",
      "id": "a1f6054ae0524596",
      "execution_count": null,
      "metadata": {
        "language": "python"
      },
      "outputs": [],
      "source": "client = SDMClient(\n    url=NIM_URL,\n    api_key=NIM_API_KEY,\n)\n\nprint(\"Ready:\", client.health_ready())\nprint(\"Capabilities:\", client.capabilities(KumoRelational()))"
    },
    {
      "cell_type": "markdown",
      "id": "f83cc342e84b44f0",
      "metadata": {
        "codeCollapsed": true
      },
      "source": "<div class=\"alert alert-warning\">\n<strong>Security:</strong> Never hard-code NIM or gateway credentials in a\nnotebook. Use environment variables or your platform's secret manager.\n</div>"
    },
    {
      "cell_type": "markdown",
      "id": "456e3686d07948be",
      "metadata": {
        "codeCollapsed": true
      },
      "source": "## Step 4. Data Preparation\n\nThis notebook uses the **AdventureWorks OLTP sample database**, originally\npublished by Microsoft as a SQL Server sample. The four source extracts are:\n\n- `Customer.csv`\n- `Product.csv`\n- `SalesOrderHeader.csv`\n- `SalesOrderDetail.csv`\n\nDownload the four `*.csv` extracts from the AdventureWorks OLTP install scripts in the\n[Microsoft SQL Server samples repository](https://github.com/microsoft/sql-server-samples/tree/master/samples/databases/adventure-works/oltp-install-script).\n\n> Each user is responsible for checking the content of datasets and the\n> applicable licenses and determining if suitable for the intended use.\nThe repository's samples and templates are provided under the\n[MIT License](https://github.com/microsoft/sql-server-samples/blob/master/license.txt).\n\nSet `DATA_LOCATION` to the Snowflake database and schema that hold the\nAdventureWorks tables (`RUTH.ADVENTUREWORKS` in this account). Each table is\nread with the Snowpark session, and only the listed columns that exist in the\ntable are kept.\n\nThe sales-order header and detail tables remain separate. During preparation,\nonly `CustomerID` and `OrderDate` are copied from each header to its detail\nrows using `SalesOrderID`."
    },
    {
      "cell_type": "code",
      "id": "9b4cbe03709f4914",
      "execution_count": null,
      "metadata": {
        "language": "python"
      },
      "outputs": [],
      "source": "from snowflake.snowpark.context import get_active_session\n\nsession = get_active_session()\n\nDATA_LOCATION = \"<database>.<schema>\"\nprint(\"AdventureWorks data:\", DATA_LOCATION)\n\nCUSTOMER_COLUMNS = [\n    \"CustomerID\",\n    \"PersonID\",\n    \"StoreID\",\n    \"TerritoryID\",\n    \"AccountNumber\",\n    \"rowguid\",\n    \"ModifiedDate\",\n]\n\nPRODUCT_COLUMNS = [\n    \"ProductID\",\n    \"Name\",\n    \"ProductNumber\",\n    \"MakeFlag\",\n    \"FinishedGoodsFlag\",\n    \"Color\",\n    \"SafetyStockLevel\",\n    \"ReorderPoint\",\n    \"StandardCost\",\n    \"ListPrice\",\n    \"Size\",\n    \"SizeUnitMeasureCode\",\n    \"WeightUnitMeasureCode\",\n    \"Weight\",\n    \"DaysToManufacture\",\n    \"ProductLine\",\n    \"Class\",\n    \"Style\",\n    \"ProductSubcategoryID\",\n    \"ProductModelID\",\n    \"SellStartDate\",\n    \"SellEndDate\",\n    \"DiscontinuedDate\",\n    \"rowguid\",\n    \"ModifiedDate\",\n]\n\nSALES_HEADER_COLUMNS = [\n    \"SalesOrderID\",\n    \"RevisionNumber\",\n    \"OrderDate\",\n    \"DueDate\",\n    \"ShipDate\",\n    \"Status\",\n    \"OnlineOrderFlag\",\n    \"SalesOrderNumber\",\n    \"PurchaseOrderNumber\",\n    \"AccountNumber\",\n    \"CustomerID\",\n    \"SalesPersonID\",\n    \"TerritoryID\",\n    \"BillToAddressID\",\n    \"ShipToAddressID\",\n    \"ShipMethodID\",\n    \"CreditCardID\",\n    \"CreditCardApprovalCode\",\n    \"CurrencyRateID\",\n    \"SubTotal\",\n    \"TaxAmt\",\n    \"Freight\",\n    \"TotalDue\",\n    \"Comment\",\n    \"rowguid\",\n    \"ModifiedDate\",\n]\n\nSALES_DETAIL_COLUMNS = [\n    \"SalesOrderID\",\n    \"SalesOrderDetailID\",\n    \"CarrierTrackingNumber\",\n    \"OrderQty\",\n    \"ProductID\",\n    \"SpecialOfferID\",\n    \"UnitPrice\",\n    \"UnitPriceDiscount\",\n    \"LineTotal\",\n    \"rowguid\",\n    \"ModifiedDate\",\n]\n\n\ndef read_headerless_table(filename, columns):\n    path = f'{DATA_LOCATION}.\"{Path(filename).stem}\"'\n    return session.table(path).to_pandas()\n\n\ncustomers_df = read_headerless_table(\"Customer.csv\", CUSTOMER_COLUMNS)\nproducts_df = read_headerless_table(\"Product.csv\", PRODUCT_COLUMNS)\nsales_order_headers_df = read_headerless_table(\"SalesOrderHeader.csv\", SALES_HEADER_COLUMNS)\nsales_order_details_df = read_headerless_table(\"SalesOrderDetail.csv\", SALES_DETAIL_COLUMNS)"
    },
    {
      "cell_type": "markdown",
      "id": "55240ba7af5f4412",
      "metadata": {
        "codeCollapsed": true
      },
      "source": "Parse the timestamp fields, then enrich each sales-order detail with only its parent order's `CustomerID` and `OrderDate`. The header and detail dataframes remain separate."
    },
    {
      "cell_type": "code",
      "id": "5e56b1a7770c46d9",
      "execution_count": null,
      "metadata": {
        "language": "python"
      },
      "outputs": [],
      "source": "timestamp_columns = {\n    \"customers\": (customers_df, [\"ModifiedDate\"]),\n    \"products\": (\n        products_df,\n        [\"SellStartDate\", \"SellEndDate\", \"DiscontinuedDate\", \"ModifiedDate\"],\n    ),\n    \"sales_order_headers\": (\n        sales_order_headers_df,\n        [\"OrderDate\", \"DueDate\", \"ShipDate\", \"ModifiedDate\"],\n    ),\n    \"sales_order_details\": (sales_order_details_df, [\"ModifiedDate\"]),\n}\n\nfor _, (table_df, columns) in timestamp_columns.items():\n    for column in columns:\n        if column in table_df:  # skip columns not present in the Snowflake table\n            table_df[column] = pd.to_datetime(table_df[column], errors=\"coerce\")\n\nsales_order_details_df = sales_order_details_df.merge(\n    sales_order_headers_df[[\"SalesOrderID\", \"CustomerID\", \"OrderDate\"]],\n    on=\"SalesOrderID\",\n    how=\"left\",\n    validate=\"many_to_one\",\n)"
    },
    {
      "cell_type": "markdown",
      "id": "e383e679775c4356",
      "metadata": {
        "codeCollapsed": true
      },
      "source": "## Step 5. Build a schema from DataFrames\n\n`Schema.from_data()` accepts a dictionary of pandas DataFrames and infers\neach table's column types and semantic roles automatically. The AdventureWorks\nrelationships are supplied explicitly so the example is reproducible.\nKumo Relational still infers column data types and semantic metadata, which\nyou should review before prediction."
    },
    {
      "cell_type": "code",
      "id": "d56d61bdd28e4630",
      "execution_count": null,
      "metadata": {
        "language": "python"
      },
      "outputs": [],
      "source": "schema = Schema.from_data(\n    {\n        \"customers\": customers_df,\n        \"products\": products_df,\n        \"sales_order_headers\": sales_order_headers_df,\n        \"sales_order_details\": sales_order_details_df,\n    },\n    primary_keys={\n        \"customers\": \"CustomerID\",\n        \"products\": \"ProductID\",\n        \"sales_order_headers\": \"SalesOrderID\",\n        \"sales_order_details\": \"SalesOrderDetailID\",\n    },\n    time_columns={\n        # These two tables contain datetime columns but do not represent\n        # event timelines, so no time column is assigned to them.\n        \"customers\": None,\n        \"products\": None,\n        \"sales_order_headers\": \"OrderDate\",\n        \"sales_order_details\": \"OrderDate\",\n    },\n    relationships=[\n        Relationship(source_table=\"sales_order_headers\", source_column=\"CustomerID\", target_table=\"customers\", target_column=\"CustomerID\"),\n        Relationship(source_table=\"sales_order_details\", source_column=\"SalesOrderID\", target_table=\"sales_order_headers\", target_column=\"SalesOrderID\"),\n        Relationship(source_table=\"sales_order_details\", source_column=\"ProductID\", target_table=\"products\", target_column=\"ProductID\"),\n        Relationship(source_table=\"sales_order_details\", source_column=\"CustomerID\", target_table=\"customers\", target_column=\"CustomerID\"),\n    ],\n)\n\nprint(schema)\nfor name in schema.tables:\n    print(schema[name])"
    },
    {
      "cell_type": "markdown",
      "id": "9549e5a69d5e4576",
      "metadata": {
        "codeCollapsed": true
      },
      "source": "Primary-key and relationship columns are automatically assigned the `ID`\nsemantic type. Review the remaining columns before prediction:\n\n- Override a column's semantic type with\n  `schema[\"table_name\"][\"column_name\"].stype = \"<type>\"`.\n- Available semantic types: `\"numerical\"`, `\"categorical\"`,\n  `\"multicategorical\"`, `\"ID\"`, `\"text\"`, `\"timestamp\"`.\n\nFor this known AdventureWorks schema, review identifier-like strings before\nprediction. Account numbers, product numbers, order numbers, approval codes,\n`rowguid` columns, and `CarrierTrackingNumber` identify records rather than\ncategories, and several exceed the NIM's 10,000-value categorical cardinality\nlimit in the full extracts, so this example marks them `\"ID\"`. Product names\nare typed `\"text\"` to reflect their content, though Kumo Relational currently\nignores `text` features for prediction; see\n[Data Types](https://docs.nvidia.com/sdgm/kumo-relational/user-guide/prepare-data/data-types).\nOrder quantity is a measured numerical value."
    },
    {
      "cell_type": "code",
      "id": "343f65d6fa1e4421",
      "execution_count": null,
      "metadata": {
        "language": "python"
      },
      "outputs": [],
      "source": "# Correct semantic types that are specific to this known schema. These values\n# identify business records rather than categories or free-text documents.\nid_columns = {\n    \"customers\": [\"AccountNumber\", \"rowguid\"],\n    \"products\": [\"ProductNumber\", \"rowguid\"],\n    \"sales_order_headers\": [\n        \"SalesOrderNumber\",\n        \"PurchaseOrderNumber\",\n        \"AccountNumber\",\n        \"CreditCardApprovalCode\",\n        \"rowguid\",\n    ],\n    \"sales_order_details\": [\"CarrierTrackingNumber\", \"rowguid\"],\n}\n\nfor table_name, column_names in id_columns.items():\n    for column_name in column_names:\n        if column_name in schema[table_name].columns:  # skip columns not in the Snowflake table\n            schema[table_name][column_name].stype = \"ID\"\n\n# Product names carry natural-language meaning; order quantity is measured.\n# This NIM does not read the 'text' stype, so Name is typed categorical.\nschema[\"products\"][\"Name\"].stype = \"categorical\"\nschema[\"sales_order_details\"][\"OrderQty\"].stype = \"numerical\""
    },
    {
      "cell_type": "markdown",
      "id": "b8d9bde03c0043dd",
      "metadata": {
        "codeCollapsed": true
      },
      "source": "Call `schema.check_consistent()` after all metadata changes. An incompatible\nsemantic type, a missing column, or a primary key that is also configured as\na time column raises an error."
    },
    {
      "cell_type": "code",
      "id": "76207d2f8c1440bc",
      "execution_count": null,
      "metadata": {
        "language": "python"
      },
      "outputs": [],
      "source": "schema.check_consistent()\nprint(\"Schema OK.\")\nfor name in schema.tables:\n    print(schema[name])"
    },
    {
      "cell_type": "markdown",
      "id": "daf381f78c0c42a3",
      "metadata": {
        "codeCollapsed": true
      },
      "source": "## Step 6. Submit prediction requests\n\nThe schema is a reusable description of the data. For each prediction:\n\n1. Write the task in PQL.\n2. Build a `PredictiveQuery` from the query text and the schema.\n3. Call `.materialize(indices=...)` to gather a `Request`.\n4. Call `client.predict(request, model=KumoRelational())`.\n\nThe result's `to_pandas()` method returns a pandas DataFrame."
    },
    {
      "cell_type": "markdown",
      "id": "2018ab497db1403f",
      "metadata": {
        "codeCollapsed": true
      },
      "source": "**Note:** AdventureWorks is sample data, and the queries and results below are intended for demonstration. Benchmark Kumo Relational on data representative of your own use case before drawing performance conclusions."
    },
    {
      "cell_type": "markdown",
      "id": "9a3b840a14124567",
      "metadata": {
        "codeCollapsed": true
      },
      "source": "#### PQL: How you describe a prediction task\n\nLearn more in the [query-writing guide](https://docs.nvidia.com/sdgm/kumo-relational/user-guide/make-predictions/writing-predictive-queries).\n\nThe short reference below covers the entity, target, aggregation,\nprediction-window, and condition syntax used by the examples in this\nquickstart."
    },
    {
      "cell_type": "markdown",
      "id": "77a82381c7c543fb",
      "metadata": {
        "codeCollapsed": true
      },
      "source": "<details>\n<summary><b>💡 Click here for a short introduction to Predictive Query!</b></summary>\n\n**Entities** can be specified in three ways:\n\n* `FOR <entity_table>.<entity_primary_key>=1` embeds one entity ID in PQL.\n* `FOR <entity_table>.<entity_primary_key> IN (1, 2, 3)` embeds several\n  entity IDs in PQL.\n* `FOR EACH <entity_table>.<entity_primary_key>` declares the entity column,\n  while `materialize(indices=[1, 2, 3])` supplies the entity IDs separately.\n\n**Preferred:** For application code, use `FOR EACH` and pass primary-key values\nthrough `materialize()`'s `indices` argument. This keeps the prediction task separate\nfrom the entity batch and makes the same PQL reusable. Despite its name,\n`indices` contains source primary-key values, not DataFrame row numbers.\nIf `indices` is provided, it overrides entity IDs embedded in the PQL.\n\n**Targets** can be specified in two common ways:\n\n* `PREDICT <target_table>.<target_column>` imputes a missing value.\n* `PREDICT <aggregation>(<target>, <start_offset>, <end_offset>, <time_unit>)` predicts an aggregate in a future window.\n\nSupported aggregations include `COUNT`, `SUM`, `AVG`, `MIN`, `MAX`, and `LIST_DISTINCT`. For example, `PREDICT COUNT(sales_order_headers.*, 0, 7, days)` predicts the number of orders in the next seven days.\n\nConditions can turn an aggregate into an event. For example, appending `=0` predicts the probability that no orders occur in the specified window.\n</details>"
    },
    {
      "cell_type": "markdown",
      "id": "7bdf5317192d401c",
      "metadata": {
        "codeCollapsed": true
      },
      "source": "### Example 1A: Forecast 30-day product demand\n\nPredict how many units of product `ProductID=870` will be ordered in the next 30 days."
    },
    {
      "cell_type": "code",
      "id": "6f6a049b1a2245c1",
      "execution_count": null,
      "metadata": {
        "language": "python"
      },
      "outputs": [],
      "source": "query = PredictiveQuery(\n    \"PREDICT SUM(sales_order_details.OrderQty, 0, 30, days) FOR EACH products.ProductID\",\n    schema,\n)\n\nrequest = query.materialize(indices=[870])\nresult = client.predict(request, model=KumoRelational())\nresult.to_pandas()"
    },
    {
      "cell_type": "markdown",
      "id": "c909c8c8ce3f413a",
      "metadata": {
        "codeCollapsed": true
      },
      "source": "How to interpret the result:\n\n1. `id`: the product with `ProductID=870`\n2. `prediction`: the predicted number of units ordered during the next 30 days\n\nThis result can support inventory and demand planning."
    },
    {
      "cell_type": "markdown",
      "id": "bb42f92f2c2c4126",
      "metadata": {
        "codeCollapsed": true
      },
      "source": "### Example 1B: Forecast from a historical anchor time\n\nBy default, predictions use the maximum timestamp in the temporal schema. Set a historical `anchor_time` to simulate what the prediction would have looked like at that point in time. Kumo Relational uses only information available before the anchor time."
    },
    {
      "cell_type": "code",
      "id": "fb4d3b45bdfb439e",
      "execution_count": null,
      "metadata": {
        "language": "python"
      },
      "outputs": [],
      "source": "# Pin the anchor instead of letting it be derived. It is taken from the\n# data rather than written as a literal: AdventureWorks is re-dated\n# upstream, so a hardcoded date stops matching the file you just read.\nanchor_time = sales_order_headers_df[\"OrderDate\"].max() - pd.Timedelta(days=60)\n\nrequest = query.materialize(indices=[870], anchor_time=anchor_time)\nresult = client.predict(request, model=KumoRelational())\nresult.to_pandas()"
    },
    {
      "cell_type": "markdown",
      "id": "1afe70c0bf7c4654",
      "metadata": {
        "codeCollapsed": true
      },
      "source": "### Example 1C: Forecast for multiple steps"
    },
    {
      "cell_type": "code",
      "id": "43a67a8a138949ec",
      "execution_count": null,
      "metadata": {
        "language": "python"
      },
      "outputs": [],
      "source": "query_multi_step = PredictiveQuery(\n    \"PREDICT SUM(sales_order_details.OrderQty, 0, 30, days) \"\n    \"FORECAST 3 TIMEFRAMES FOR EACH products.ProductID\",\n    schema,\n)\n\nrequest = query_multi_step.materialize(indices=[870])\nresult = client.predict(request, model=KumoRelational())\nresult.to_pandas()"
    },
    {
      "cell_type": "markdown",
      "id": "2458e94f330c4ce0",
      "metadata": {
        "codeCollapsed": true
      },
      "source": "A multi-step forecast returns one row per timeframe.\n`forecast_step` is the one-based sequence number and `prediction` is the\nforecast value for that period."
    },
    {
      "cell_type": "markdown",
      "id": "f4ecd48298c04830",
      "metadata": {
        "codeCollapsed": true
      },
      "source": "### Example 2: Predict customer inactivity\n\nPredict the likelihood that customers `CustomerID=11176` and `CustomerID=11091` will place zero orders in the next 90 days."
    },
    {
      "cell_type": "code",
      "id": "68b73fba28a54daf",
      "execution_count": null,
      "metadata": {
        "language": "python"
      },
      "outputs": [],
      "source": "query_inactivity = PredictiveQuery(\n    \"PREDICT COUNT(sales_order_headers.*, 0, 90, days)=0 FOR EACH customers.CustomerID\",\n    schema,\n)\n\nrequest = query_inactivity.materialize(indices=[11176, 11091])\nresult = client.predict(request, model=KumoRelational())\nresult.to_pandas()"
    },
    {
      "cell_type": "markdown",
      "id": "89c2f6703bdc4af7",
      "metadata": {
        "codeCollapsed": true
      },
      "source": "How to interpret the result:\n\n1. `id`: the customer represented by each `CustomerID`\n2. `prediction`: whether the zero-order event is predicted to occur\n3. `probability_*` columns: the estimated likelihood of each outcome\n\nThis result can support retention or re-engagement workflows."
    },
    {
      "cell_type": "markdown",
      "id": "f4a9687b1c554efd",
      "metadata": {
        "codeCollapsed": true
      },
      "source": "### Example 3: Recommend products\n\nRank which of a small candidate set of products customer `CustomerID=11176` is most likely to order in the next 30 days.\n\nRanking runs as one classification call per candidate, so the candidate set is\nnamed explicitly with `candidates=` rather than left for the client to infer.\nThis example candidates the 15 most frequently ordered products so each has\nenough purchase history for the model to learn from; an arbitrary or rarely\nordered candidate set can fail with \"nothing to rank\" because its context\nholds only one outcome. See\n[Prediction Types](https://docs.nvidia.com/sdgm/kumo-relational/user-guide/make-predictions/prediction-types#recommendations-ranking)."
    },
    {
      "cell_type": "code",
      "id": "62cadc402ad84193",
      "execution_count": null,
      "metadata": {
        "language": "python"
      },
      "outputs": [],
      "source": "candidate_product_ids = (\n    sales_order_details_df[\"ProductID\"]\n    .value_counts()\n    .head(15)\n    .index.to_series()\n    .reset_index(drop=True)\n)\n\nquery_rank = PredictiveQuery(\n    \"PREDICT LIST_DISTINCT(sales_order_details.ProductID, 0, 30, days) \"\n    \"RANK TOP 10 FOR EACH customers.CustomerID\",\n    schema,\n)\n\nrequest = query_rank.materialize(\n    indices=[11176],\n    candidates=candidate_product_ids,\n    num_context_samples=5000,\n)\nresult = client.predict(request, model=KumoRelational())\nprint(result.to_pandas())\nprint(\"Unscored candidates:\", result.unscored_candidates)"
    },
    {
      "cell_type": "markdown",
      "id": "c591ab8a0d9948f8",
      "metadata": {
        "codeCollapsed": true
      },
      "source": "How to interpret the result:\n\n1. `id`: the customer with `CustomerID=11176`\n2. `rankings`: the ranked candidates, each with a `candidate` product ID and a `score`\n\nA candidate whose sampled context holds only one outcome can't be scored and\nis left out rather than failing the whole ranking; check\n`result.unscored_candidates` for which ones were skipped."
    },
    {
      "cell_type": "markdown",
      "id": "9ccdfb90708143fb",
      "metadata": {
        "codeCollapsed": true
      },
      "source": "### Example 4: Infer a missing product attribute\n\nPredict the missing `Color` value for product `ProductID=870`."
    },
    {
      "cell_type": "code",
      "id": "a872685251844a6c",
      "execution_count": null,
      "metadata": {
        "language": "python"
      },
      "outputs": [],
      "source": "query_attribute = PredictiveQuery(\n    \"PREDICT products.Color FOR EACH products.ProductID\",\n    schema,\n)\n\nrequest = query_attribute.materialize(indices=[870])\nresult = client.predict(request, model=KumoRelational())\nresult.to_pandas()"
    },
    {
      "cell_type": "markdown",
      "id": "8f2a1fe04ce34513",
      "metadata": {
        "codeCollapsed": true
      },
      "source": "How to interpret the result:\n\n1. `id`: the product with `ProductID=870`\n2. `prediction`: the predicted color category\n3. `probability_*` columns: the estimated likelihood of each category\n\nAttribute inference can help enrich incomplete catalog records, but predicted values should be reviewed before being written back to a source system."
    },
    {
      "cell_type": "markdown",
      "id": "863c7f0fa50d4557",
      "metadata": {
        "codeCollapsed": true
      },
      "source": "### Example 5: Predict customer churn\n\nPredict the likelihood that customers `CustomerID=11176` and `CustomerID=11091` will place zero orders in the next 90 days, reusing Example 2's query with one addition. The `WHERE` condition does not filter which entities are scored, both `indices` are still predicted; it filters which rows qualify as labeled context examples, restricting the model's learned comparison to customers who themselves placed at least one order in the previous 90 days."
    },
    {
      "cell_type": "code",
      "id": "1574bbb37ce747d6",
      "execution_count": null,
      "metadata": {
        "language": "python"
      },
      "outputs": [],
      "source": "query_churn = PredictiveQuery(\n    \"PREDICT COUNT(sales_order_headers.*, 0, 90, days) = 0 \"\n    \"FOR EACH customers.CustomerID \"\n    \"WHERE COUNT(sales_order_headers.*, -90, 0, days) > 0\",\n    schema,\n)\n\nrequest = query_churn.materialize(indices=[11176, 11091])\nresult = client.predict(request, model=KumoRelational())\nresult.to_pandas()"
    },
    {
      "cell_type": "markdown",
      "id": "f217317179fc486f",
      "metadata": {
        "codeCollapsed": true
      },
      "source": "How to interpret the result:\n\n1. `id`: the customer represented by each `CustomerID`\n2. `prediction`: whether the customer is predicted to place zero orders during the next 90 days\n3. `probability_*` columns: the estimated likelihood of each outcome\n\nThis churn prediction can support retention or re-engagement workflows."
    },
    {
      "cell_type": "markdown",
      "id": "0af9353c28134884",
      "metadata": {
        "codeCollapsed": true
      },
      "source": "## Step 7. Close the client\n\nClose the client when the notebook is finished to release its HTTP connection."
    },
    {
      "cell_type": "code",
      "id": "6f15f0bff032428e",
      "execution_count": null,
      "metadata": {
        "language": "python"
      },
      "outputs": [],
      "source": "client.close()"
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "name": "python",
      "pygments_lexer": "ipython3"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 5
}