Skip to content

Work with numbers

Use numeric expressions and the functions in std.math to compare values, convert between types, and apply common numeric transforms in your PyRel definitions. In this guide, you’ll learn patterns for working with numeric expressions in where() conditions and derived facts, and get tips for avoiding common pitfalls.

A numeric expression is any PyRel Expression that evaluates to a numeric value. You use numeric expressions in Model.where() conditions with Model.define() and Model.select().

Numeric expressions usually show up in one of these ways:

  • A numeric literal: A typed constant like numbers.integer(30).
  • A relationship chain that ends in a number: a traversal like Ticket.response_minutes.
  • A derived numeric expression: an operator or function call that returns a number, like Ticket.response_minutes - SLA_TARGET_MINUTES.

The following example shows one instance of each kind of numeric expression:

from relationalai.semantics import Integer, Model, String
from relationalai.semantics.std import numbers
m = Model("SupportModel")
Ticket = m.Concept("Ticket", identify_by={"id": Integer})
Ticket.subject = m.Property(f"{Ticket} has subject {String:subject}")
Ticket.response_minutes = m.Property(f"{Ticket} response minutes is {Integer:minutes}")
5 collapsed lines
# Base facts (collapsed)
m.define(
Ticket.new(id=101, subject="[P0] Outage", response_minutes=12),
Ticket.new(id=102, subject="Billing question", response_minutes=55),
)
SLA_TARGET_MINUTES = numbers.integer(30) # A literal
chain = Ticket.response_minutes
derived = Ticket.response_minutes - SLA_TARGET_MINUTES
df = m.select(
Ticket.id,
SLA_TARGET_MINUTES.alias("sla_target_minutes"),
chain.alias("response_minutes"),
derived.alias("minutes_over_target"),
).to_df()
print(df)

Use std.numbers.integer(), std.floats.float(), and std.numbers.number() to cast expressions and numeric literals into the right numeric type for your logic.

The following example casts an Integer property to a Float, then reuses the result in derived facts:

from relationalai.semantics import Float, Integer, Model, String
from relationalai.semantics.std import floats
m = Model("SupportModel")
Ticket = m.Concept("Ticket", identify_by={"id": Integer})
Ticket.response_minutes = m.Property(f"{Ticket} response minutes is {Integer:minutes}")
Ticket.response_hours = m.Property(f"{Ticket} response hours is {Float:hours}")
Ticket.sla_status = m.Relationship(f"{Ticket} has sla status {String:status}")
4 collapsed lines
m.define(
Ticket.new(id=201, response_minutes=45),
Ticket.new(id=202, response_minutes=120),
)
# Cast minutes to hours by dividing by 60.0 (a Float literal)
response_hours = floats.float(Ticket.response_minutes) / 60.0
# Define a new property based on the Float expression
m.define(Ticket.response_hours(response_hours))
df = m.select(
Ticket.id,
Ticket.response_minutes,
Ticket.response_hours,
).to_df()
print(df)
  • floats.float(Ticket.response_minutes) casts the Integer-typed response_minutes property to a Float.
  • m.define(...) reuses the response_hours expression to define a new property on Ticket.
  • The query selects the original Integer property and the new Float property side by side for comparison.

Use numbers.number() when you need a fixed-decimal Number constant. This is especially useful for money-like values where the number of decimal places matters. This example flags invoices that require approval if their amount exceeds 1000.00:

from relationalai.semantics import Integer, Model, Number, String
from relationalai.semantics.std import numbers
m = Model("BillingModel")
Invoice = m.Concept("Invoice", identify_by={"id": Integer})
Invoice.amount = m.Property(f"{Invoice} has amount {Number.size(12, 2):usd}")
Invoice.status = m.Relationship(f"{Invoice} has status {String:status}")
4 collapsed lines
m.define(
Invoice.new(id=1001, amount=numbers.number(249.99, precision=12, scale=2)),
Invoice.new(id=1002, amount=numbers.number(1500.00, precision=12, scale=2)),
)
APPROVAL_LIMIT = numbers.number(1000.00, precision=12, scale=2)
m.define(Invoice.status("needs_approval")).where(Invoice.amount > APPROVAL_LIMIT)
m.define(Invoice.status("auto_approve")).where(Invoice.amount <= APPROVAL_LIMIT)
df = m.select(
Invoice.id,
Invoice.amount,
Invoice.status,
).to_df()
print(df)
  • Number.size(12, 2) declares a fixed-decimal Number property with two decimal places (for example, a currency amount).
  • numbers.number(1000.00, precision=12, scale=2) creates a fixed-decimal Number constant that matches the property’s precision and scale.
  • numbers.number(...) returns a PyRel Variable, which you can reuse across definitions and optionally rename with .alias() in selects.

Use std.numbers.parse_number() to parse numeric text into a typed expression you can compare in conditions.

Keep in mind that:

  • Unlike Python’s int() and float(), numbers.parse_number() does not throw exceptions on invalid input.
  • If the input cannot be parsed, the result is treated as a missing value and may filter out of your conditions or show up as null in your outputs.

In the following example, SLA minute values arrive as text and are parsed into a Number-typed property for comparisons using numbers.parse_number():

from relationalai.semantics import Integer, Model, Number, String
from relationalai.semantics.std import numbers
m = Model("SupportModel")
Ticket = m.Concept("Ticket", identify_by={"id": Integer})
Ticket.sla_minutes_text = m.Property(f"{Ticket} has sla minutes text {String:text}")
Ticket.sla_minutes_parsed = m.Relationship(f"{Ticket} has sla minutes parsed {Number.size(38, 0):minutes}")
6 collapsed lines
# Base facts (collapsed)
m.define(
Ticket.new(id=301, sla_minutes_text="30"),
Ticket.new(id=302, sla_minutes_text="045"),
Ticket.new(id=303, sla_minutes_text="not_a_number"),
)
parsed = numbers.parse_number(Ticket.sla_minutes_text, precision=38, scale=0)
m.define(Ticket.sla_minutes_parsed(parsed))
df = m.select(
Ticket.id,
Ticket.sla_minutes_text,
parsed.alias("parsed_explicit"),
).to_df()
print(df)
print(df.dtypes)
  • numbers.parse_number(..., precision=38, scale=0) parses a whole-number string into a Number with explicit precision and scale.
  • Invalid inputs like "not_a_number" produce missing parsed values that filter out of conditions and show as null in outputs.

Use comparison operators (>, <, >=, <=) to filter facts based on numeric conditions:

from relationalai.semantics import Integer, Model, String
from relationalai.semantics.std import numbers
m = Model("SupportModel")
Ticket = m.Concept("Ticket", identify_by={"id": Integer})
Ticket.response_minutes = m.Property(f"{Ticket} response minutes is {Integer:minutes}")
Ticket.sla_state = m.Relationship(f"{Ticket} has sla state {String:state}")
Ticket.response_band = m.Relationship(f"{Ticket} has response band {String:band}")
6 collapsed lines
# Base facts (collapsed)
m.define(
Ticket.new(id=401, response_minutes=10),
Ticket.new(id=402, response_minutes=45),
Ticket.new(id=403, response_minutes=90),
)
SLA_TARGET_MINUTES = numbers.integer(30)
BAND_30 = numbers.integer(30)
BAND_60 = numbers.integer(60)
m.define(Ticket.sla_state("breached")).where(Ticket.response_minutes > SLA_TARGET_MINUTES)
m.define(Ticket.sla_state("ok")).where(Ticket.response_minutes <= SLA_TARGET_MINUTES)
m.define(Ticket.response_band("0-30")).where(Ticket.response_minutes <= BAND_30)
m.define(Ticket.response_band("31-60")).where(BAND_30 < Ticket.response_minutes, Ticket.response_minutes <= BAND_60)
m.define(Ticket.response_band("60+")).where(Ticket.response_minutes > BAND_60)
df = m.select(
Ticket.id,
Ticket.response_minutes,
Ticket.sla_state,
Ticket.response_band,
).to_df()
print(df)
  • The first two conditions define a simple breached/ok SLA status based on a threshold.
  • The next three conditions define response time bands with explicit boundaries.
  • BAND_30 < Ticket.response_minutes, Ticket.response_minutes <= BAND_60 shows how to combine multiple comparisons in the same where(). Filters in the same where() are implicitly combined with AND.

Use the functions in std.math to apply common numeric transforms like clipping, rounding, and absolute value in your definitions:

from relationalai.semantics import Float, Integer, Model
from relationalai.semantics.std import math
m = Model("SupportModel")
Ticket = m.Concept("Ticket", identify_by={"id": Integer})
Ticket.severity = m.Property(f"{Ticket} has severity {Integer:severity}")
Ticket.escalations = m.Property(f"{Ticket} has escalations {Integer:escalations}")
Ticket.response_minutes = m.Property(f"{Ticket} response minutes is {Integer:minutes}")
Ticket.priority_score_raw = m.Property(f"{Ticket} has raw priority score {Float:score}")
Ticket.priority_score = m.Property(f"{Ticket} has priority score {Float:score}")
5 collapsed lines
# Base facts (collapsed)
m.define(
Ticket.new(id=501, severity=5, escalations=2, response_minutes=10),
Ticket.new(id=502, severity=2, escalations=0, response_minutes=90),
)
raw_priority = (
Ticket.severity * 20.0
+ Ticket.escalations * 10.0
- Ticket.response_minutes / 2.0
)
priority_score = math.clip(raw_priority, 0.0, 100.0)
m.define(
Ticket.priority_score_raw(raw_priority),
Ticket.priority_score(priority_score)
)
df = m.select(
Ticket.id,
Ticket.priority_score_raw,
Ticket.priority_score,
).to_df()
print(df)
  • raw_priority is a derived numeric expression that combines multiple properties with arithmetic operators.
  • math.clip(raw_priority, 0.0, 100.0) bounds the priority score into a stable range between 0 and 100, even if the raw score goes outside that range.
  • std.math also includes other useful functions like round(), floor(), ceil(), and abs() that you can apply to numeric expressions in your definitions.
  • Some functions, like math.clip(), have type-dependent behavior. For example, math.clip() returns an Integer if the input is an Integer, and a Number if the input is a Number.

If a numeric condition does not match the rows you expect, avoid changing the formula first. Most “math bugs” are actually missing inputs, type mismatches, or scale/parsing issues.

Start with a quick verification select that shows the inputs you are comparing. Then work through these checks:

  • Confirm the value exists. Select the relationship chain directly. If it is missing for an entity, comparisons against it will not match.
  • Confirm both sides have the same type. Use numbers.integer(), floats.float(), or numbers.number() to make constants explicit.
  • For Number, confirm precision and scale match. A Number value like 1.50 is not the same as a whole-number Number with scale=0.
  • If you parse numeric text, inspect raw + parsed side by side. Parsing failures show up as missing values in outputs and can remove rows from where() filters.
  • Make range boundaries explicit. Decide whether each boundary is inclusive (<=) or exclusive (<) and apply it consistently.
  • Avoid exact equality for computed Float values. Prefer comparisons that allow a tolerance band or use integer/fixed-decimal types when exact equality matters.

Use this table to map common symptoms to likely causes and fixes:

SymptomLikely causeFix
A threshold comparison matches fewer rows than expectedThe relationship chain is missing for some entitiesSelect the chain directly (m.select()) and confirm it exists for the intended entities before you add the filter.
A condition matches zero rowsType mismatch between the property and the constantConstruct the constant with numbers.integer(), floats.float(), or numbers.number() to match the property type.
A range band does not include boundary valuesMixed strict and non-strict operators (< vs <=) or inconsistent typesMake boundaries explicit and keep both sides of the comparison the same type.
A fixed-decimal comparison looks wrongPrecision/scale mismatch between a Number property and a Number constantConstruct the constant with numbers.number() to match the property.
Parsed numeric values show up as nullText could not be parsed into the requested numeric typeSelect the raw text and parsed value together, then decide whether to clean inputs upstream or branch definitions based on “parsed value exists”.