{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "1e4bc018f9b64464",
   "metadata": {},
   "source": [
    "# Kumo Relational Quickstart with structured-data-models-client\n",
    "\n",
    "**Kumo Relational** is a relational foundation model for\n",
    "machine learning on enterprise data. With your relational data and a\n",
    "Predictive Query Language (PQL) query, you can generate predictions without\n",
    "training a task-specific model.\n",
    "\n",
    "This quickstart uses four tables from Microsoft's **AdventureWorks** OLTP\n",
    "sample database: customers, products, sales-order headers, and sales-order\n",
    "details. Predictions are submitted to a deployed Kumo Relational NVIDIA NIM through\n",
    "`structured-data-models-client`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c333f1688de74b17",
   "metadata": {},
   "source": [
    "## Introduction\n",
    "\n",
    "Kumo Relational is grounded in three key world views:\n",
    "\n",
    "> **1. Enterprise data is a graph.**\n",
    "\n",
    "Enterprise 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",
    "\n",
    "Once 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",
    "\n",
    "By 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",
    "\n",
    "Major 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",
    "\n",
    "If 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": {},
   "source": [
    "**Let's get started!**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fc9ae315749f4538",
   "metadata": {},
   "source": [
    "## Step 1. Install structured-data-models-client\n",
    "\n",
    "Install 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": {},
   "outputs": [],
   "source": [
    "%pip install \"structured-data-models-client[relational]\""
   ]
  },
  {
   "cell_type": "code",
   "id": "6aac3912c4a144b8",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "from pathlib import Path\n",
    "\n",
    "import pandas as pd\n",
    "from sdm_client import SDMClient, Schema, PredictiveQuery, KumoRelational, Relationship"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1e6663f0c2574e5e",
   "metadata": {},
   "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": {},
   "source": [
    "## Step 2. Configure the Kumo Relational NIM endpoint\n",
    "\n",
    "Deploy 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)\n",
    "for deployment instructions.\n",
    "\n",
    "Set `KUMO_RELATIONAL_API_ENDPOINT` to the reachable NIM endpoint. The default below uses a\n",
    "local container on port `8000`. Set `KUMO_RELATIONAL_API_KEY` only when an\n",
    "authenticating gateway protects the endpoint."
   ]
  },
  {
   "cell_type": "code",
   "id": "e1bcc8f7a5024ff5",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "NIM_URL = os.environ.get(\n",
    "    \"KUMO_RELATIONAL_API_ENDPOINT\",\n",
    "    \"http://localhost:8000\",\n",
    ")\n",
    "NIM_API_KEY = os.environ.get(\"KUMO_RELATIONAL_API_KEY\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bb2867af180a486f",
   "metadata": {},
   "source": [
    "## Step 3. Connect to the NIM\n",
    "\n",
    "An `SDMClient` owns the HTTP connection to one NIM endpoint. Keep credentials\n",
    "in environment configuration rather than notebook source."
   ]
  },
  {
   "cell_type": "code",
   "id": "a1f6054ae0524596",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "client = SDMClient(\n",
    "    url=NIM_URL,\n",
    "    api_key=NIM_API_KEY,\n",
    ")\n",
    "\n",
    "print(\"Ready:\", client.health_ready())\n",
    "print(\"Capabilities:\", client.capabilities(KumoRelational()))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f83cc342e84b44f0",
   "metadata": {},
   "source": [
    "<div class=\"alert alert-warning\">\n",
    "<strong>Security:</strong> Never hard-code NIM or gateway credentials in a\n",
    "notebook. Use environment variables or your platform's secret manager.\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "456e3686d07948be",
   "metadata": {},
   "source": [
    "## Step 4. Data Preparation\n",
    "\n",
    "This notebook uses the **AdventureWorks OLTP sample database**, originally\n",
    "published 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",
    "\n",
    "Download 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.\n",
    "The repository's samples and templates are provided under the\n",
    "[MIT License](https://github.com/microsoft/sql-server-samples/blob/master/license.txt).\n",
    "\n",
    "Set `ADVENTURE_WORKS_DATA_DIR` to choose where the files are read from,\n",
    "e.g. `export ADVENTURE_WORKS_DATA_DIR=/path/to/adventure-works-cache`. If it is unset,\n",
    "the notebook uses `../rfm/adventure-works` relative to its working directory.\n",
    "The files are tab-delimited and do not include a header row, so each schema is\n",
    "assigned explicitly when the file is loaded.\n",
    "\n",
    "The sales-order header and detail tables remain separate. During preparation,\n",
    "only `CustomerID` and `OrderDate` are copied from each header to its detail\n",
    "rows using `SalesOrderID`."
   ]
  },
  {
   "cell_type": "code",
   "id": "9b4cbe03709f4914",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "DATA_DIR = (\n",
    "    Path(os.environ.get(\"ADVENTURE_WORKS_DATA_DIR\", \"../rfm/adventure-works\"))\n",
    "    .expanduser()\n",
    "    .resolve()\n",
    ")\n",
    "\n",
    "print(\"AdventureWorks data:\", DATA_DIR)\n",
    "\n",
    "CUSTOMER_COLUMNS = [\n",
    "    \"CustomerID\",\n",
    "    \"PersonID\",\n",
    "    \"StoreID\",\n",
    "    \"TerritoryID\",\n",
    "    \"AccountNumber\",\n",
    "    \"rowguid\",\n",
    "    \"ModifiedDate\",\n",
    "]\n",
    "\n",
    "PRODUCT_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",
    "\n",
    "SALES_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",
    "\n",
    "SALES_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",
    "\n",
    "def read_headerless_table(filename, columns):\n",
    "    path = DATA_DIR / filename\n",
    "    if not path.exists():\n",
    "        raise FileNotFoundError(\n",
    "            f\"{path} was not found. Download the AdventureWorks CSV files \"\n",
    "            \"listed above before running this cell.\"\n",
    "        )\n",
    "    return pd.read_csv(\n",
    "        path,\n",
    "        sep=\"\\t\",\n",
    "        header=None,\n",
    "        names=columns,\n",
    "        encoding=\"utf-8\",\n",
    "    )\n",
    "\n",
    "\n",
    "customers_df = read_headerless_table(\"Customer.csv\", CUSTOMER_COLUMNS)\n",
    "products_df = read_headerless_table(\"Product.csv\", PRODUCT_COLUMNS)\n",
    "sales_order_headers_df = read_headerless_table(\"SalesOrderHeader.csv\", SALES_HEADER_COLUMNS)\n",
    "sales_order_details_df = read_headerless_table(\"SalesOrderDetail.csv\", SALES_DETAIL_COLUMNS)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "55240ba7af5f4412",
   "metadata": {},
   "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": {},
   "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",
    "\n",
    "for _, (table_df, columns) in timestamp_columns.items():\n",
    "    for column in columns:\n",
    "        table_df[column] = pd.to_datetime(table_df[column], errors=\"coerce\")\n",
    "\n",
    "sales_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": {},
   "source": [
    "## Step 5. Build a schema from DataFrames\n",
    "\n",
    "`Schema.from_data()` accepts a dictionary of pandas DataFrames and infers\n",
    "each table's column types and semantic roles automatically. The AdventureWorks\n",
    "relationships are supplied explicitly so the example is reproducible.\n",
    "Kumo Relational still infers column data types and semantic metadata, which\n",
    "you should review before prediction."
   ]
  },
  {
   "cell_type": "code",
   "id": "d56d61bdd28e4630",
   "execution_count": null,
   "metadata": {},
   "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",
    "\n",
    "print(schema)\n",
    "for name in schema.tables:\n",
    "    print(schema[name])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9549e5a69d5e4576",
   "metadata": {},
   "source": [
    "Primary-key and relationship columns are automatically assigned the `ID`\n",
    "semantic 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",
    "\n",
    "For this known AdventureWorks schema, review identifier-like strings before\n",
    "prediction. Account numbers, product numbers, order numbers, approval codes,\n",
    "`rowguid` columns, and `CarrierTrackingNumber` identify records rather than\n",
    "categories, and several exceed the NIM's 10,000-value categorical cardinality\n",
    "limit in the full extracts, so this example marks them `\"ID\"`. Product names\n",
    "are typed `\"text\"` to reflect their content, though Kumo Relational currently\n",
    "ignores `text` features for prediction; see\n",
    "[Data Types](https://docs.nvidia.com/sdgm/kumo-relational/user-guide/prepare-data/data-types).\n",
    "Order quantity is a measured numerical value."
   ]
  },
  {
   "cell_type": "code",
   "id": "343f65d6fa1e4421",
   "execution_count": null,
   "metadata": {},
   "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.\n",
    "id_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",
    "\n",
    "for table_name, column_names in id_columns.items():\n",
    "    for column_name in column_names:\n",
    "        schema[table_name][column_name].stype = \"ID\"\n",
    "\n",
    "# Product names carry natural-language meaning; order quantity is measured.\n",
    "schema[\"products\"][\"Name\"].stype = \"text\"\n",
    "schema[\"sales_order_details\"][\"OrderQty\"].stype = \"numerical\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b8d9bde03c0043dd",
   "metadata": {},
   "source": [
    "Call `schema.check_consistent()` after all metadata changes. An incompatible\n",
    "semantic type, a missing column, or a primary key that is also configured as\n",
    "a time column raises an error."
   ]
  },
  {
   "cell_type": "code",
   "id": "76207d2f8c1440bc",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "schema.check_consistent()\n",
    "print(\"Schema OK.\")\n",
    "for name in schema.tables:\n",
    "    print(schema[name])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "daf381f78c0c42a3",
   "metadata": {},
   "source": [
    "## Step 6. Submit prediction requests\n",
    "\n",
    "The schema is a reusable description of the data. For each prediction:\n",
    "\n",
    "1. Write the task in PQL.\n",
    "2. Build a `PredictiveQuery` from the query text and the schema.\n",
    "3. Call `.materialize(indices=...)` to gather a `Request`.\n",
    "4. Call `client.predict(request, model=KumoRelational())`.\n",
    "\n",
    "The result's `to_pandas()` method returns a pandas DataFrame."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2018ab497db1403f",
   "metadata": {},
   "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": {},
   "source": [
    "#### PQL: How you describe a prediction task\n",
    "\n",
    "Learn more in the [query-writing guide](https://docs.nvidia.com/sdgm/kumo-relational/user-guide/make-predictions/writing-predictive-queries).\n",
    "\n",
    "The short reference below covers the entity, target, aggregation,\n",
    "prediction-window, and condition syntax used by the examples in this\n",
    "quickstart."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "77a82381c7c543fb",
   "metadata": {},
   "source": [
    "<details>\n",
    "<summary><b>\ud83d\udca1 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\n",
    "through `materialize()`'s `indices` argument. This keeps the prediction task separate\n",
    "from the entity batch and makes the same PQL reusable. Despite its name,\n",
    "`indices` contains source primary-key values, not DataFrame row numbers.\n",
    "If `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",
    "\n",
    "Supported 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",
    "\n",
    "Conditions 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": {},
   "source": [
    "### Example 1A: Forecast 30-day product demand\n",
    "\n",
    "Predict how many units of product `ProductID=870` will be ordered in the next 30 days."
   ]
  },
  {
   "cell_type": "code",
   "id": "6f6a049b1a2245c1",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "query = PredictiveQuery(\n",
    "    \"PREDICT SUM(sales_order_details.OrderQty, 0, 30, days) FOR EACH products.ProductID\",\n",
    "    schema,\n",
    ")\n",
    "\n",
    "request = query.materialize(indices=[870])\n",
    "result = client.predict(request, model=KumoRelational())\n",
    "result.to_pandas()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c909c8c8ce3f413a",
   "metadata": {},
   "source": [
    "How to interpret the result:\n",
    "\n",
    "1. `id`: the product with `ProductID=870`\n",
    "2. `prediction`: the predicted number of units ordered during the next 30 days\n",
    "\n",
    "This result can support inventory and demand planning."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bb42f92f2c2c4126",
   "metadata": {},
   "source": [
    "### Example 1B: Forecast from a historical anchor time\n",
    "\n",
    "By 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": {},
   "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.\n",
    "anchor_time = sales_order_headers_df[\"OrderDate\"].max() - pd.Timedelta(days=60)\n",
    "\n",
    "request = query.materialize(indices=[870], anchor_time=anchor_time)\n",
    "result = client.predict(request, model=KumoRelational())\n",
    "result.to_pandas()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1afe70c0bf7c4654",
   "metadata": {},
   "source": [
    "### Example 1C: Forecast for multiple steps"
   ]
  },
  {
   "cell_type": "code",
   "id": "43a67a8a138949ec",
   "execution_count": null,
   "metadata": {},
   "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",
    "\n",
    "request = query_multi_step.materialize(indices=[870])\n",
    "result = client.predict(request, model=KumoRelational())\n",
    "result.to_pandas()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2458e94f330c4ce0",
   "metadata": {},
   "source": [
    "A multi-step forecast returns one row per timeframe.\n",
    "`forecast_step` is the one-based sequence number and `prediction` is the\n",
    "forecast value for that period."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f4ecd48298c04830",
   "metadata": {},
   "source": [
    "### Example 2: Predict customer inactivity\n",
    "\n",
    "Predict 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": {},
   "outputs": [],
   "source": [
    "query_inactivity = PredictiveQuery(\n",
    "    \"PREDICT COUNT(sales_order_headers.*, 0, 90, days)=0 FOR EACH customers.CustomerID\",\n",
    "    schema,\n",
    ")\n",
    "\n",
    "request = query_inactivity.materialize(indices=[11176, 11091])\n",
    "result = client.predict(request, model=KumoRelational())\n",
    "result.to_pandas()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "89c2f6703bdc4af7",
   "metadata": {},
   "source": [
    "How to interpret the result:\n",
    "\n",
    "1. `id`: the customer represented by each `CustomerID`\n",
    "2. `prediction`: whether the zero-order event is predicted to occur\n",
    "3. `probability_*` columns: the estimated likelihood of each outcome\n",
    "\n",
    "This result can support retention or re-engagement workflows."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f4a9687b1c554efd",
   "metadata": {},
   "source": [
    "### Example 3: Recommend products\n",
    "\n",
    "Rank which of a small candidate set of products customer `CustomerID=11176` is most likely to order in the next 30 days.\n",
    "\n",
    "Ranking runs as one classification call per candidate, so the candidate set is\n",
    "named explicitly with `candidates=` rather than left for the client to infer.\n",
    "This example candidates the 15 most frequently ordered products so each has\n",
    "enough purchase history for the model to learn from; an arbitrary or rarely\n",
    "ordered candidate set can fail with \"nothing to rank\" because its context\n",
    "holds 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": {},
   "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",
    "\n",
    "query_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",
    "\n",
    "request = query_rank.materialize(\n",
    "    indices=[11176],\n",
    "    candidates=candidate_product_ids,\n",
    "    num_context_samples=5000,\n",
    ")\n",
    "result = client.predict(request, model=KumoRelational())\n",
    "print(result.to_pandas())\n",
    "print(\"Unscored candidates:\", result.unscored_candidates)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c591ab8a0d9948f8",
   "metadata": {},
   "source": [
    "How to interpret the result:\n",
    "\n",
    "1. `id`: the customer with `CustomerID=11176`\n",
    "2. `rankings`: the ranked candidates, each with a `candidate` product ID and a `score`\n",
    "\n",
    "A candidate whose sampled context holds only one outcome can't be scored and\n",
    "is left out rather than failing the whole ranking; check\n",
    "`result.unscored_candidates` for which ones were skipped."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9ccdfb90708143fb",
   "metadata": {},
   "source": [
    "### Example 4: Infer a missing product attribute\n",
    "\n",
    "Predict the missing `Color` value for product `ProductID=870`."
   ]
  },
  {
   "cell_type": "code",
   "id": "a872685251844a6c",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "query_attribute = PredictiveQuery(\n",
    "    \"PREDICT products.Color FOR EACH products.ProductID\",\n",
    "    schema,\n",
    ")\n",
    "\n",
    "request = query_attribute.materialize(indices=[870])\n",
    "result = client.predict(request, model=KumoRelational())\n",
    "result.to_pandas()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8f2a1fe04ce34513",
   "metadata": {},
   "source": [
    "How to interpret the result:\n",
    "\n",
    "1. `id`: the product with `ProductID=870`\n",
    "2. `prediction`: the predicted color category\n",
    "3. `probability_*` columns: the estimated likelihood of each category\n",
    "\n",
    "Attribute 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": {},
   "source": [
    "### Example 5: Predict customer churn\n",
    "\n",
    "Predict 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": {},
   "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",
    "\n",
    "request = query_churn.materialize(indices=[11176, 11091])\n",
    "result = client.predict(request, model=KumoRelational())\n",
    "result.to_pandas()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f217317179fc486f",
   "metadata": {},
   "source": [
    "How to interpret the result:\n",
    "\n",
    "1. `id`: the customer represented by each `CustomerID`\n",
    "2. `prediction`: whether the customer is predicted to place zero orders during the next 90 days\n",
    "3. `probability_*` columns: the estimated likelihood of each outcome\n",
    "\n",
    "This churn prediction can support retention or re-engagement workflows."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0af9353c28134884",
   "metadata": {},
   "source": [
    "## Step 7. Close the client\n",
    "\n",
    "Close the client when the notebook is finished to release its HTTP connection."
   ]
  },
  {
   "cell_type": "code",
   "id": "6f15f0bff032428e",
   "execution_count": null,
   "metadata": {},
   "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
}
