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.
- PyRel is installed and importable in Python. See Set Up Your Environment for instructions.
- You are comfortable deriving facts with
Model.define()and filtering withModel.where(). See Derive facts with logic.
Understand numeric expressions
Section titled “Understand numeric expressions”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, Stringfrom 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 literalchain = Ticket.response_minutesderived = 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)Cast numeric values
Section titled “Cast numeric values”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, Stringfrom 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 expressionm.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 theInteger-typedresponse_minutesproperty to aFloat.m.define(...)reuses theresponse_hoursexpression to define a new property onTicket.- The query selects the original
Integerproperty and the newFloatproperty 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, Stringfrom 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-decimalNumberproperty with two decimal places (for example, a currency amount).numbers.number(1000.00, precision=12, scale=2)creates a fixed-decimalNumberconstant that matches the property’s precision and scale.
numbers.number(...)returns a PyRelVariable, which you can reuse across definitions and optionally rename with.alias()in selects.
Parse numeric text safely
Section titled “Parse numeric text safely”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()andfloat(),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, Stringfrom 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 aNumberwith 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.
Write comparisons and ranges
Section titled “Write comparisons and ranges”Use comparison operators (>, <, >=, <=) to filter facts based on numeric conditions:
from relationalai.semantics import Integer, Model, Stringfrom 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_60shows how to combine multiple comparisons in the samewhere(). Filters in the samewhere()are implicitly combined with AND.
Apply std.math transforms
Section titled “Apply std.math transforms”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, Modelfrom 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_priorityis 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.mathalso includes other useful functions likeround(),floor(),ceil(), andabs()that you can apply to numeric expressions in your definitions.
- Some functions, like
math.clip(), have type-dependent behavior. For example,math.clip()returns anIntegerif the input is anInteger, and aNumberif the input is aNumber.
Avoid common numeric pitfalls
Section titled “Avoid common numeric pitfalls”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(), ornumbers.number()to make constants explicit. - For
Number, confirm precision and scale match. ANumbervalue like1.50is not the same as a whole-numberNumberwithscale=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
Floatvalues. 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:
| Symptom | Likely cause | Fix |
|---|---|---|
| A threshold comparison matches fewer rows than expected | The relationship chain is missing for some entities | Select the chain directly (m.select()) and confirm it exists for the intended entities before you add the filter. |
| A condition matches zero rows | Type mismatch between the property and the constant | Construct the constant with numbers.integer(), floats.float(), or numbers.number() to match the property type. |
| A range band does not include boundary values | Mixed strict and non-strict operators (< vs <=) or inconsistent types | Make boundaries explicit and keep both sides of the comparison the same type. |
| A fixed-decimal comparison looks wrong | Precision/scale mismatch between a Number property and a Number constant | Construct the constant with numbers.number() to match the property. |
| Parsed numeric values show up as null | Text could not be parsed into the requested numeric type | Select the raw text and parsed value together, then decide whether to clean inputs upstream or branch definitions based on “parsed value exists”. |