> ## Documentation Index
> Fetch the complete documentation index at: https://docs.woodwide.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Clustering

> Unsupervised grouping with human-readable cluster descriptions.

Clustering models automatically group rows in your data into meaningful clusters and generate human-readable descriptions for each cluster. The number of clusters is determined automatically.

## Training

Training fits the model to your data and discovers cluster structure. A portion of the training data is held out to compute validation metrics:

| Metric             | Description                                                                                                                              |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `n_clusters`       | Number of clusters discovered.                                                                                                           |
| `silhouette_score` | Silhouette coefficient measuring cluster separation (range -1 to 1; higher is better). Only computed when there are at least 2 clusters. |

These metrics are available on the model object via `GET /models/{model_id}` in the `current_metrics` field.

At training time, the platform also generates human-readable descriptions for each cluster, summarizing the distinguishing characteristics of rows in that cluster. These descriptions are included in inference output.

<CodeGroup>
  ```python Python theme={null}
  import os, time, requests

  api_key = os.getenv("WOODWIDE_API_KEY")
  base_url = "https://api.woodwide.ai"
  headers = {"Authorization": f"Bearer {api_key}"}

  # Upload data
  with open("customers.csv", "rb") as f:
      resp = requests.post(
          f"{base_url}/datasets",
          headers=headers,
          files={"file": ("customers.csv", f, "text/csv")},
          data={"dataset_name": "customers"},
      )
  dataset_id = resp.json()["dataset"]["id"]

  # Train a clustering model
  resp = requests.post(
      f"{base_url}/models/train",
      headers=headers,
      json={
          "model_name": "customer_segments",
          "model_type": "clustering",
          "dataset_id": dataset_id,
      },
  )
  model_id = resp.json()["model"]["id"]

  # Wait for training
  while True:
      model = requests.get(
          f"{base_url}/models/{model_id}", headers=headers
      ).json()
      if model["status"] == "ready":
          break
      time.sleep(5)

  print(model["current_metrics"])  # e.g. {"n_clusters": 4, "silhouette_score": 0.62}
  ```

  ```python Python (SDK) theme={null}
  import time
  from pathlib import Path
  from woodwide import WoodWide

  client = WoodWide()

  dataset = client.datasets.create(
      file=Path("customers.csv"),
      dataset_name="customers",
      override=True,
  )

  model = client.models.train(
      model_type="clustering",
      dataset_id=dataset.id,
  )
  model_id = model.id

  while True:
      model = client.models.retrieve(model_id)
      if model.status in {"ready", "failed"}:
          break
      time.sleep(5)

  if model.status == "failed":
      raise RuntimeError("Training failed")
  # Validation metrics: GET /models/{model_id} via HTTP (current_metrics)
  ```

  ```javascript JavaScript theme={null}
  const fs = require("fs");
  const FormData = require("form-data");

  const apiKey = process.env.WOODWIDE_API_KEY;
  const baseUrl = "https://api.woodwide.ai";
  const headers = { Authorization: `Bearer ${apiKey}` };

  // Upload data
  const uploadForm = new FormData();
  uploadForm.append("file", fs.createReadStream("customers.csv"), "customers.csv");
  uploadForm.append("dataset_name", "customers");

  const uploadResp = await fetch(`${baseUrl}/datasets`, {
    method: "POST",
    headers: { ...headers, ...uploadForm.getHeaders() },
    body: uploadForm,
  });
  const { dataset: { id: datasetId } } = await uploadResp.json();

  // Train a clustering model
  const trainResp = await fetch(`${baseUrl}/models/train`, {
    method: "POST",
    headers: { ...headers, "Content-Type": "application/json" },
    body: JSON.stringify({
      model_name: "customer_segments",
      model_type: "clustering",
      dataset_id: datasetId,
    }),
  });
  const { model: { id: modelId } } = await trainResp.json();

  // Wait for training
  let model;
  while (true) {
    const modelResp = await fetch(`${baseUrl}/models/${modelId}`, { headers });
    model = await modelResp.json();
    if (model.status === "ready") break;
    await new Promise((r) => setTimeout(r, 5000));
  }

  console.log(model.current_metrics);  // e.g. { n_clusters: 4, silhouette_score: 0.62 }
  ```

  ```bash curl theme={null}
  export WOODWIDE_API_KEY="your_api_key_here"
  export BASE_URL="https://api.woodwide.ai"

  # Upload data
  DATASET_ID=$(curl -s -X POST "$BASE_URL/datasets" \
    -H "Authorization: Bearer $WOODWIDE_API_KEY" \
    -F "file=@customers.csv" \
    -F "dataset_name=customers" | jq -r '.dataset.id')

  # Train a clustering model
  MODEL_ID=$(curl -s -X POST "$BASE_URL/models/train" \
    -H "Authorization: Bearer $WOODWIDE_API_KEY" \
    -H "Content-Type: application/json" \
    -d "{
      \"model_name\": \"customer_segments\",
      \"model_type\": \"clustering\",
      \"dataset_id\": \"$DATASET_ID\"
    }" | jq -r '.model.id')

  # Wait for training
  while true; do
    STATUS=$(curl -s "$BASE_URL/models/$MODEL_ID" \
      -H "Authorization: Bearer $WOODWIDE_API_KEY" | jq -r '.status')

    if [ "$STATUS" = "ready" ]; then break; fi
    if [ "$STATUS" = "failed" ]; then echo "Training failed."; exit 1; fi

    sleep 5
  done

  curl -s "$BASE_URL/models/$MODEL_ID" \
    -H "Authorization: Bearer $WOODWIDE_API_KEY" | jq '.current_metrics'
  # e.g. { "n_clusters": 4, "silhouette_score": 0.62 }
  ```
</CodeGroup>

## Inference

To get cluster assignments, run inference on your data. Since clustering is unsupervised, it is common to run inference on the same dataset you trained on -- this gives you the cluster assignment for each row. You can also run inference on new data, but rows will be assigned to the clusters that were discovered during training.

<CodeGroup>
  ```python Python theme={null}
  # Run inference on the training data to get cluster assignments
  with open("customers.csv", "rb") as f:
      resp = requests.post(
          f"{base_url}/models/{model_id}/infer",
          headers=headers,
          files={"file": ("customers.csv", f, "text/csv")},
          data={"output_type": "json"},
      )

  results = resp.json()["data"]
  print(results)
  ```

  ```python Python (SDK) theme={null}
  # Uses client and model_id from the training example above.
  from pathlib import Path

  result = client.models.infer(
      model_id,
      file=Path("customers.csv"),
      output_type="json",
  )
  print(result["data"])
  ```

  ```javascript JavaScript theme={null}
  // Run inference on the training data to get cluster assignments
  const form = new FormData();
  form.append("file", fs.createReadStream("customers.csv"), "customers.csv");
  form.append("output_type", "json");

  const resp = await fetch(`${baseUrl}/models/${modelId}/infer`, {
    method: "POST",
    headers: { ...headers, ...form.getHeaders() },
    body: form,
  });

  const { data: results } = await resp.json();
  console.log(results);
  ```

  ```bash curl theme={null}
  curl -s -X POST "$BASE_URL/models/$MODEL_ID/infer" \
    -H "Authorization: Bearer $WOODWIDE_API_KEY" \
    -F "file=@customers.csv" \
    -F "output_type=json" | jq '.data'
  ```
</CodeGroup>

See [Output Formats](/api-reference/inference#clustering) for the full output schema.
