Improving Inference

This guide is a practical, hands-on companion to the Inference guide: how to take a working prediction and make it better. It uses one running example — predicting which employee should process an invoice (the processor field) — and walks through the levers that most improve quality: schema links, supporting fields, fuzzy $numeric matching, scoping the candidates, and priors for cold-start cases.

The example schema (the invoice dataset used in Aito's tests and demo):

{
  "companies": {
    "type": "table",
    "columns": {
      "id":            { "type": "Int" },
      "name":          { "type": "Text", "analyzer": "english" },
      "size":          { "type": "Int" },
      "employeeCount": { "type": "Int" }
    }
  },
  "invoices": {
    "type": "table",
    "columns": {
      "id":          { "type": "Int" },
      "date":        { "type": "String" },
      "company":     { "type": "Int", "link": "companies.id" },
      "sender":      { "type": "Text", "analyzer": "english" },
      "product":     { "type": "Text", "analyzer": "english" },
      "description": { "type": "Text", "analyzer": "english" },
      "glCode":      { "type": "Int", "link": "glCodes.id" },
      "processor":   { "type": "Int", "link": "employees.id" }
    }
  },
  "employees": {
    "type": "table",
    "columns": {
      "id":         { "type": "Int" },
      "name":       { "type": "Text", "analyzer": "english" },
      "role":       { "type": "String" },
      "department": { "type": "String" },
      "company":    { "type": "Int", "link": "companies.id" },
      "tags":       { "type": "Text", "analyzer": "english" }
    }
  }
}

Start with a baseline

The simplest processor prediction uses the free-text fields of the invoice:

{
  "from": "invoices",
  "where": {
    "product": "AWS cloud hosting",
    "description": "Monthly cloud infrastructure invoice"
  },
  "predict": "processor"
}

This already works, but it only knows invoices by their text. The sections below add structure so Aito can reason about who the processors are and which of them is plausible.

A "link" column turns a foreign key into a relationship Aito can reason across. Here processor links to employees, so Aito can use an employee's role, department, and name as evidence — not just the raw processor id.

"processor": { "type": "Int", "link": "employees.id" }

Links unlock three things:

  1. Predicting the link"predict": "processor" returns employee ids.
  2. Referencing linked fields in where with dot notation — e.g. "processor.department": "IT".
  3. Priors from the linked table — even a processor who has never appeared in the invoice history can be predicted from their employee attributes (see Lean on priors).

Link a column for every meaningful relationship: company → companies, glCode → glCodes, processor → employees. Two practical limits (details): the linked types must match exactly (Int link → Int id), and links resolve one hop onlyprocessor.department works, processor.company.industry does not.

Add supporting data and metadata

Aito does no manual feature engineering — every column becomes a potential feature, and it automatically selects the ones that predict the target. So the most direct way to improve a prediction is to give it more to work with:

  • More columns on invoicessender, product, description, glCode, date. A glCode or sender strongly correlated with a processor becomes a powerful signal.
  • Richer linked tablesrole, department, tags on employees let Aito learn "infrastructure invoices go to the IT department".

When the predicted column is a link, basedOn chooses which fields of the linked table feed the prior estimate. Start broad, then narrow to the fields that actually carry signal — unrelated fields add noise:

{
  "from": "invoices",
  "where": {
    "product": "AWS cloud hosting",
    "description": "Monthly cloud infrastructure invoice",
    "glCode": 4200
  },
  "predict": "processor",
  "basedOn": ["role", "department"]
}

If predictions look noisy, reduce the fields in basedOn rather than adding more. See the basedOn parameter.

Fuzzy numeric matching with $numeric

By default a number in a where clause is matched exactly"company.size": 500 is the feature "size equals exactly 500", which almost never recurs and so carries little signal. The $numeric operator makes the match inexact: "somewhere near 500", with a region whose width Aito derives from the spread and density of the data.

{
  "from": "invoices",
  "where": {
    "description": "Monthly cloud infrastructure invoice",
    "company.size": { "$numeric": 500 }
  },
  "predict": "processor"
}

Reach for $numeric on any continuous quantity — a company's size or employeeCount, an invoice amount, line counts, etc. It is the difference between a number being useless (exact match that never repeats) and being a genuine signal ("invoices from large companies tend to go to a senior processor"). Use exact matching only for numbers that are really categorical (status codes, a fixed glCode).

Scope the candidates with where constraints

This is the most important — and most misunderstood — point about where. Most where propositions are soft: Aito turns each into a feature, keeps the subset that best predicts the target, and ignores the rest. So "product" and "description" describe the situation and nudge the prediction; Aito decides how much to trust them.

But a where constraint on the prediction target's own linked attributes does more than nudge — it scopes the candidates. Because you are predicting processor (a link to employees), constraining processor.company or processor.department restricts the prediction to the employees that match:

{
  "from": "invoices",
  "where": {
    "product": "AWS cloud hosting",
    "description": "Monthly cloud infrastructure invoice",
    "processor.company": 42,
    "processor.department": "IT"
  },
  "predict": "processor",
  "basedOn": ["role", "department"]
}

Here the prediction is scoped to processors in the IT department of company 42 — no special filter operator is needed, just a plain statement about the target. This is exactly the pattern used in the invoice tests and in Scoped Employee Prediction. It both improves accuracy (no implausible candidates from other companies) and speeds the query up (a smaller option space).

A good rule of thumb:

  • Constrain the predicted field's own attributes (processor.company, processor.department) to restrict which employees are considered.
  • Add descriptive signals (product, description, glCode, amounts) to tell Aito about the invoice; it will weigh these automatically.

Lean on priors for cold-start cases

Aito always combines the direct evidence above with hierarchical priors drawn from the linked tables — which is what makes it work even when a processor has little or no invoice history. For a brand-new employee, the prior is built from their employees attributes (role, department, name, tags) and from word-matches between the invoice text and those attributes — an invoice mentioning "infrastructure" lifts employees in the IT department; an invoice addressed to "Jane" lifts employees named Jane.

You shape that prior with basedOn (which linked fields to use) and, when you need fields from the invoice table rather than the employee table inside a nested proposition, with $context. As historical data accumulates, direct evidence increasingly dominates and the prior matters less — so priors help most exactly when you have the least data. The conceptual background is in Hierarchical Priors.

Putting it together

A tuned processor prediction combines all of the above — links for reasoning about employees, supporting fields for signal, $numeric for the company size, a target constraint to scope the candidate set, and basedOn to shape the prior:

{
  "from": "invoices",
  "where": {
    "product": "AWS cloud hosting",
    "description": "Monthly cloud infrastructure invoice",
    "glCode": 4200,
    "company.size": { "$numeric": 500 },
    "processor.company": 42
  },
  "predict": "processor",
  "basedOn": ["role", "department", "name"]
}

Inspecting and iterating

Don't tune blind — measure each change:

  • Add "select": ["$p", "$why"] to a predict to see the probability and which features drove it ($why). If a field you expected to matter isn't in $why, it isn't carrying signal — or it's being ignored as noise.
  • $why also shows a calibration factor named support-tempering(auto): Aito tempers each prediction's confidence by the effective statistical support of its evidence (on by default). A value well below 1.0 tells you the prediction rests on thin or overlapping evidence — more or broader training data will both raise the confidence and make it trustworthy; a value ≈ 1.0 means the evidence is well supported and the probability is essentially raw. See Score Calibration for details and the opt-out.
  • Use the _evaluate endpoint (see the Evaluation guide) to measure accuracy, meanRank, and informationGain before and after each change — link a table, add basedOn fields, switch a number to $numeric, scope the candidates — and keep the changes that move the metrics.
  • If all candidate scores are similar, the where clause isn't distinguishing them: add more context, or scope the candidates with a target constraint.

See also the Inference guide for the underlying model and Schema design for getting links and types right in the first place.