Introduction
rumo is a tool that reads tabular data and runs rules on it.
It uses the nemo rule engine.
rumo has three parts:
- A Rust library.
- A command-line tool with the name
rumo. - A Python package with the name
pyrumo.
What rumo does
rumo can do these tasks:
- Show the structure of a table (the number of rows, the columns and the data types).
- Convert a table to RDF in the Turtle format.
- Run nemo rules on a table or on RDF data.
- Return the results of the rules as Python data, a pandas DataFrame or a Polars DataFrame.
How rumo runs rules
The diagram shows the data flow:
pandas / Polars DataFrame ──► Turtle (RDF) ──┐
├──► nemo rules ──► results
RDF text or RDF file ────────────────────────┘
rumo converts each DataFrame to RDF triples.
The nemo rules then read the triples and calculate new facts.
rumo returns these facts as tables.
Terms in this manual
| Term | Meaning |
|---|---|
| Rule program | A text in the nemo rule language. A rule file has the extension .rls. |
| Predicate | The name of a relation in a rule program, for example good. |
| Fact | One row of a predicate, for example good("Bob", 92.0). |
| Resource | A name that a rule program uses in an @import statement to read data. |
| Parameter | A global value in a rule program, for example $GOOD_SCORE. |
Example
import pandas as pd
from pyrumo import Rumo
df = pd.read_csv("examples/sample.csv")
rules = """
@prefix : <http://example.org/> .
good(?name, ?score) :- data(?r, :name, ?name), data(?r, :score, ?s),
?score = DOUBLE(?s), ?score > $GOOD_SCORE .
"""
rumo = Rumo(rules=rules, data=df, predicate="data")
print(rumo.run(params={"GOOD_SCORE": 90}).to_pandas("good"))
The output is:
name score
0 Bob 92.0
1 Dave 95.1
Installation
Install the Python package
Use pip to install the package:
pip install pyrumo
The package needs Python 3.9 or a later version. The package also installs Polars.
To use pandas DataFrames, install the pandas extra:
pip install "pyrumo[pandas]"
Install the command-line tool
Use a prebuilt binary
Each GitHub release has prebuilt binaries for these platforms:
| Operating system | Architecture |
|---|---|
| Linux | x86_64, aarch64 |
| Windows | x86_64 |
| macOS | x86_64, aarch64 |
To install a binary:
- Download the archive for your platform from the release page.
- Extract the archive.
- Move the
rumofile to a directory in yourPATH.
Build from the source code
The nemo rule engine needs the nightly Rust toolchain.
The file rust-toolchain.toml selects this toolchain automatically.
- Install Rust with rustup.
- Clone the repository.
- In the repository directory, run this command:
cargo install --path . --features rules
Note: If you do not set the
rulesfeature, therumo rulescommand is not available.
Cargo features
| Feature | Description |
|---|---|
rules | Adds the nemo rule engine. |
python | Adds the Python bindings. This feature also sets the rules feature. |
Command-line tool
The rumo command has three subcommands: describe, convert and rules.
To show the help, run this command:
rumo --help
Show the structure of a CSV file
Use the describe subcommand:
rumo describe --data examples/sample.csv
The output is:
DataFrame: 5 rows × 3 columns
- name (str)
- age (i64)
- score (f64)
| Option | Description |
|---|---|
--data <PATH> | The input file. This option is necessary. |
--format <FORMAT> | The input format. The default is csv. Only csv is available. |
Convert a CSV file to Turtle
Use the convert subcommand:
rumo convert --data examples/sample.csv --result-format turtle
| Option | Description |
|---|---|
--data <PATH> | The input file. This option is necessary. |
--format <FORMAT> | The input format. The default is csv. |
--result-format <FORMAT> | The output format. This option is necessary. Only turtle is available. |
--output <PATH> | The output file. If you do not set this option, rumo writes to the standard output. |
--base-url <IRI> | The prefix IRI. The default is http://example.org/. |
--stem <TEXT> | The start of the name of each row. The default is r. |
Refer to DataFrame to RDF conversion for the output format.
Run a rule file
Use the rules subcommand:
rumo rules --rules examples/sample.rls --data examples/sample.csv --param GOOD_SCORE=90
| Option | Description |
|---|---|
--rules <PATH> | The nemo rule file. This option is necessary. |
--data <PATH> | A data file. rumo uses the directory of this file to find the files in @import statements. |
--output <PATH> | rumo writes the @export files to the directory of this path. |
--param <KEY=VALUE> | A value for a global parameter. You can use this option more than one time. |
If you do not set --data, rumo finds the @import files in the directory of the rule file.
If you do not set --output, rumo writes the contents of all @export files to the standard output.
Note: The
rulessubcommand reads the files that the@importstatements name. It does not convert the--datafile. To run rules directly on a DataFrame, use the PythonRumoclass.
Python: describe and convert
The pyrumo module has three functions for Polars DataFrames.
import polars as pl
import pyrumo
df = pl.read_csv("examples/sample.csv")
Get the structure of a DataFrame
describe returns a text with the number of rows, the columns and the data types:
print(pyrumo.describe(df))
DataFrame: 5 rows × 3 columns
- name (str)
- age (i64)
- score (f64)
print_info writes the same text to the standard output:
pyrumo.print_info(df)
Convert a DataFrame to Turtle
to_turtle returns the DataFrame as a Turtle text:
ttl = pyrumo.to_turtle(df, "http://example.org/", "r")
print(ttl)
prefix : <http://example.org/>
:r0 :name "Alice" ;
:age 25 ;
:score 88.5 .
...
The second argument is the prefix IRI. The third argument is the start of the name of each row.
Refer to DataFrame to RDF conversion for the rules of the conversion.
Note: These three functions accept only Polars DataFrames. The
Rumoclass accepts pandas DataFrames and Polars DataFrames.
Python: run rules with Rumo
The Rumo class runs nemo rules on data.
A Rumo object keeps:
- One rule program.
- Zero or more data resources.
Procedure
- Make a
Rumoobject. - Load the rule program.
- Load the data.
- Run the rules.
- Read the results.
You can do steps 1 to 3 in one call:
from pyrumo import Rumo
rumo = Rumo(rules_file="rules.rls", data=df, predicate="data")
You can also do each step with a method:
rumo = Rumo()
rumo.read_rules_file("rules.rls")
rumo.read_dataframe(df, predicate="data")
Then run the rules and read the results:
results = rumo.run(params={"GOOD_SCORE": 90})
table = results.to_pandas("good")
Use one object more than one time
A Rumo object does not change when you run the rules.
You can run the rules again with different parameters:
high = rumo.run(params={"GOOD_SCORE": 90})
low = rumo.run(params={"GOOD_SCORE": 50})
You can also replace the rules or the data and run again.
Constructor arguments
| Argument | Description |
|---|---|
rules | The rule program as a string. |
rules_file | The path of a rule file. |
data | A pandas DataFrame, a Polars DataFrame or RDF data as a string. |
data_file | The path of a data file. |
format | The nemo import format of data or data_file. Keyword only. |
resource | The resource name of the data. Keyword only. |
predicate | If you set this name, Rumo imports the data into this predicate. Keyword only. |
base_url | The prefix IRI for a DataFrame. The default is http://example.org/. Keyword only. |
row_stem | The start of the name of each row of a DataFrame. The default is r. Keyword only. |
All arguments are optional.
Caution: Do not set
rulesandrules_filetogether. Do not setdataanddata_filetogether. If you do,Rumoraises aValueError.
The next sections give more information about each step.
Load rules
A Rumo object keeps one rule program.
When you load a new rule program, it replaces the old rule program.
Load rules from a string
rumo.read_rules_str("""
@prefix : <http://example.org/> .
adult(?name) :- data(?p, :name, ?name), data(?p, :age, ?age), ?age >= 18 .
""")
You can also use the rules argument of the constructor.
Load rules from a file
rumo.read_rules_file("examples/sample.rls")
You can also use the rules_file argument of the constructor.
Rumo reads the file immediately.
If you change the file later, load it again.
Paths in @import statements
A rule program can read files with @import statements.
Rumo finds these files in this sequence:
- The data resources of the
Rumoobject. Refer to Load data. - Files relative to a directory:
- For a rule file, this is the directory of the rule file.
- For a rule string, this is the current working directory.
Remove the rules
rumo.reset_rules()
If you run a Rumo object without rules, it raises a ValueError.
Errors in the rules
If the rule program has an error, run raises a RuntimeError.
The message shows the line and the column of the error:
[01] Error: expected `)`
╭─[ :1:8 ]
│
1 │ foo(?x :- .
│ │
│ ╰─ expected `)`
───╯
Load data
Rumo keeps the data in memory.
Rumo does not write temporary files.
Each data item has a resource name. The rule program uses this name to read the data.
Load a DataFrame
rumo.read_dataframe(df)
The DataFrame can be a pandas DataFrame or a Polars DataFrame.
Rumo converts the DataFrame to Turtle.
Refer to DataFrame to RDF conversion.
| Argument | Default | Description |
|---|---|---|
df | The pandas DataFrame or Polars DataFrame. | |
resource | "data.ttl" | The resource name. |
predicate | None | The predicate for the data. Refer to Connect the data to the rules. |
base_url | "http://example.org/" | The prefix IRI. |
row_stem | "r" | The start of the name of each row. |
Note: For a pandas DataFrame,
Rumouses Polars to convert the data. Ifpyarrowis available,Rumouses it. Ifpyarrowis not available,Rumouses Python lists.
Load data from a string
rumo.read_data_str("""
@prefix : <http://example.org/> .
:a :name "Alice" .
:b :name "Bob" .
""", predicate="data")
| Argument | Default | Description |
|---|---|---|
data | The data as text. | |
format | "turtle" | The nemo import format. |
resource | "data.ttl" | The resource name. |
predicate | None | The predicate for the data. |
Load data from a file
rumo.read_data_file("examples/sample.ttl", predicate="data")
| Argument | Default | Description |
|---|---|---|
path | The path of the file. | |
format | From the file extension | The nemo import format. |
resource | The file name | The resource name. |
predicate | None | The predicate for the data. |
Rumo reads the file immediately.
If you do not set format, Rumo uses the file extension:
| Extension | Format |
|---|---|
.nt | ntriples |
.nq | nquads |
.rdf, .xml | rdfxml |
.csv | csv |
.tsv | tsv |
.trig | trig |
.json | json |
| Other extensions | turtle |
Connect the data to the rules
There are two methods to connect the data to the rule program.
Method 1: Set a predicate
Set the predicate argument.
Rumo then adds an @import statement to the rule program.
For example, predicate="data" adds this statement:
@import data :- turtle { resource = "data.ttl" } .
For RDF data, the predicate has three columns: subject, property and value. Use it in the rules like this:
@prefix : <http://example.org/> .
named(?row, ?name) :- data(?row, :name, ?name) .
Use this method when you write the rules for Rumo.
Method 2: Use the resource name in the rules
Do not set the predicate argument.
Set the resource argument to the name in the @import statement of the rules.
For example, examples/sample.rls has this statement:
@import r :- turtle { resource = "sample.ttl" } .
To run this file on a DataFrame, set resource="sample.ttl":
rumo = Rumo(rules_file="examples/sample.rls", data=df, resource="sample.ttl")
Rumo gives the DataFrame to the rules. Rumo does not read the file sample.ttl.
Use this method when you have rule files that already have @import statements.
Load more than one data item
You can load more than one data item. Give each data item a different resource name:
rumo.read_dataframe(people, resource="people.ttl", predicate="person")
rumo.read_dataframe(orders, resource="orders.ttl", predicate="order", row_stem="o")
Caution: If you load data with a resource name that already exists, the new data replaces the old data.
Caution: Two DataFrames with the same
base_urlandrow_stemgive the same row names (:r0,:r1, …). To keep the rows different, set a differentrow_stemfor each DataFrame.
See the loaded data
The resources property gives the resource names:
print(rumo.resources) # ['people.ttl', 'orders.ttl']
Remove the data
rumo.reset_data()
Run the rules
Use the run method:
results = rumo.run(params={"GOOD_SCORE": 90}, predicates=["good"])
run returns a RumoResults object.
Refer to Use the results.
| Argument | Default | Description |
|---|---|---|
params | None | A dictionary with values for the global parameters of the rules. |
predicates | None | A list of the predicates to return. |
Parameters
A rule program can use global parameters, for example $GOOD_SCORE.
Give the values in the params dictionary.
Do not write the $ character in the key.
rumo.run(params={"GOOD_SCORE": 90, "CITY": "Oviedo"})
Rumo converts each Python value to a nemo value:
| Python value | nemo value | Example |
|---|---|---|
int | Integer | 90 → 90 |
float | Number | 80.5 → 80.5 |
bool | Boolean | True → "true"^^xsd:boolean |
str | String | "Bob" → "Bob" |
str with the form <...> | IRI | "<http://example.org/a>" → <http://example.org/a> |
Other Python types cause a ValueError.
Select the predicates
If you do not set predicates, run returns:
- The predicates in
@outputstatements and@exportstatements of the rules. - If the rules have no such statements, all predicates that the rules calculate.
To get a different set of predicates, set predicates:
results = rumo.run(predicates=["good", "data"])
You can select any predicate in the rule program, also an imported predicate.
If a predicate does not exist, run raises a RuntimeError.
Note:
rundoes not write the files of@exportstatements.runonly returns the facts.
Errors
| Error | Cause |
|---|---|
ValueError | There are no rules. A parameter has an incorrect type. |
RuntimeError | The rule program has an error. A predicate does not exist. nemo cannot read the data. |
Use the results
run returns a RumoResults object.
This object has one table for each predicate.
Column names
Rumo uses the variable names in the heads of the rules as column names.
For example, this rule gives the columns name and score:
good(?name, ?score) :- ...
If a column has no variable name, Rumo uses col0, col1, and so on.
This occurs for imported predicates and for constants in rule heads.
If more than one rule calculates the same predicate, Rumo uses the first available name for each column.
To set different column names, use the columns argument:
results.to_pandas("good", columns=["person", "points"])
The number of names must be the same as the number of columns.
If it is not, Rumo raises a ValueError.
Get a pandas DataFrame
df = results.to_pandas("good")
This method needs pandas.
Get a Polars DataFrame
df = results.to_polars("good")
Get a list of dictionaries
rows = results.to_dicts("good")
# [{'name': 'Bob', 'score': 92.0}, {'name': 'Dave', 'score': 95.1}]
Get a list of tuples
rows = results.rows("good")
# [('Bob', 92.0), ('Dave', 95.1)]
Get all the results
to_dict returns a dictionary with one list of dictionaries for each predicate:
results.to_dict()
# {'good': [{'name': 'Bob', 'score': 92.0}, ...]}
Omit the predicate name
If the results have only one predicate, you can omit the predicate name:
results.to_pandas()
If the results have more than one predicate, you must give the name.
If you do not, Rumo raises a ValueError.
Use the results as a mapping
A RumoResults object operates like a read-only dictionary:
results.predicates() # ['good']
results.columns("good") # ['name', 'score']
"good" in results # True
len(results) # 1
for predicate in results:
print(predicate, results[predicate])
results["good"] returns a list of dictionaries.
If the predicate does not exist, it raises a KeyError.
Value types
Rumo converts each nemo value to a Python value:
| nemo value | Python value |
|---|---|
| String | str |
| IRI | str without < and > |
| String with a language tag | str, for example "hola"@es |
| Integer | int |
| Double, float | float |
xsd:decimal | float |
| Boolean | bool |
| Other values | str in nemo syntax |
Note: Turtle reads numbers such as
88.5asxsd:decimal. Thus, numbers from a DataFrame becomefloatvalues in the results.
DataFrame to RDF conversion
rumo converts a DataFrame to RDF in the Turtle format.
The command-line tool, pyrumo.to_turtle and Rumo use the same conversion.
Conversion rules
- Each row becomes a subject. The name of the subject is
<base_url><row_stem><index>. The first index is 0. - Each column becomes a property. The name of the property is
<base_url><column name>. - Each cell becomes one triple: the row, the column property and the value.
Values
| DataFrame type | Turtle value | Example |
|---|---|---|
| String | Quoted string | "Alice" |
| Integer | Number without quotes | 25 |
| Float | Number with a decimal point | 92.0 |
| Null | Empty string | "" |
| Other types | The Polars text form | true |
Example
This table:
| name | age | score |
|---|---|---|
| Alice | 25 | 88.5 |
| Bob | 30 | 92.0 |
gives this Turtle text with base_url="http://example.org/" and row_stem="r":
prefix : <http://example.org/>
:r0 :name "Alice" ;
:age 25 ;
:score 88.5 .
:r1 :name "Bob" ;
:age 30 ;
:score 92.0 .
In a rule program, these triples have this form:
data(<http://example.org/r0>, <http://example.org/name>, "Alice")
Use a @prefix statement in the rules to write the IRIs in a short form:
@prefix : <http://example.org/> .
young(?name) :- data(?r, :name, ?name), data(?r, :age, ?age), ?age < 28 .
Limitations
Refer to Limitations and troubleshooting.
Python API reference
Module functions
| Function | Returns | Description |
|---|---|---|
describe(df) | str | Gives the structure of a Polars DataFrame. |
print_info(df) | None | Writes the structure of a Polars DataFrame to the standard output. |
to_turtle(df, base_url, row_stem) | str | Converts a Polars DataFrame to Turtle. |
Class Rumo
Rumo(rules=None, rules_file=None, data=None, data_file=None, *,
format=None, resource=None, predicate=None,
base_url="http://example.org/", row_stem="r")
Methods
| Method | Returns | Description |
|---|---|---|
read_rules_str(rules) | None | Loads the rule program from a string. |
read_rules_file(path) | None | Loads the rule program from a file. |
read_dataframe(df, resource=None, predicate=None, base_url="http://example.org/", row_stem="r") | None | Loads a pandas DataFrame or a Polars DataFrame. |
read_data_str(data, format="turtle", resource=None, predicate=None) | None | Loads data from a string. |
read_data_file(path, format=None, resource=None, predicate=None) | None | Loads data from a file. |
reset_rules() | None | Removes the rule program. |
reset_data() | None | Removes all data. |
run(params=None, predicates=None) | RumoResults | Runs the rules on the data. |
Properties
| Property | Type | Description |
|---|---|---|
resources | list[str] | The resource names of the loaded data. |
Default resource names
| Method | Default resource name |
|---|---|
read_dataframe | data.ttl |
read_data_str | data.ttl |
read_data_file | The file name, for example sample.ttl |
Class RumoResults
In this table, predicate is optional if the results have only one predicate.
| Method | Returns | Description |
|---|---|---|
predicates() | list[str] | The names of the predicates. |
columns(predicate=None) | list[str] | The column names of a predicate. |
rows(predicate=None) | list[tuple] | The facts as tuples. |
to_dicts(predicate=None, columns=None) | list[dict] | The facts as dictionaries. |
to_pandas(predicate=None, columns=None) | pandas.DataFrame | The facts as a pandas DataFrame. |
to_polars(predicate=None, columns=None) | polars.DataFrame | The facts as a Polars DataFrame. |
to_dict() | dict[str, list[dict]] | All facts of all predicates. |
results[predicate] | list[dict] | The facts as dictionaries. |
predicate in results | bool | True if the predicate is in the results. |
len(results) | int | The number of predicates. |
iter(results) | iterator | The names of the predicates. |
Exceptions
| Exception | Cause |
|---|---|
ValueError | An argument is not correct, or there are no rules. |
KeyError | A predicate is not in the results. |
RuntimeError | nemo cannot parse or run the rules. |
OSError | Rumo cannot read a file. |
ImportError | pandas is not installed and you call to_pandas. |
Limitations and troubleshooting
Limitations
DataFrame conversion
The DataFrame conversion has these limitations:
rumodoes not escape quotes or backslashes in strings. A string with a"character gives Turtle that is not correct.rumowrites column names directly as property names. A column name with a space or a special character gives Turtle that is not correct.rumowrites a null value as an empty string"". Thus, a rule finds a value for each cell, also for a cell with no data.
To prevent these problems, clean the DataFrame before you load it. For example, rename the columns and remove the null values.
Other limitations
Rumo.runkeeps the Python global interpreter lock (GIL) during the run. Other Python threads stop until the run is complete.Rumo.rundoes not write the files of@exportstatements. To write these files, use the command-line tool.- The
describe,print_infoandto_turtlefunctions accept only Polars DataFrames.
Troubleshooting
| Problem | Possible cause | Action |
|---|---|---|
| A predicate has no facts. | The rules do not find the data. | Make sure that the resource name in @import is the same as the resource argument. |
| A predicate has no facts. | The IRIs in the rules are different from the IRIs in the data. | Make sure that the @prefix of the rules is the same as base_url. |
| A comparison with a number fails. | The value is an xsd:decimal. | Use DOUBLE(?x) in the rule. |
RuntimeError: Unknown predicate | The predicate name in predicates is not in the rules. | Correct the predicate name. |
ValueError: no rules loaded | You did not load a rule program. | Use rules, rules_file, read_rules_str or read_rules_file. |
ValueError: results contain several predicates | You did not give a predicate name. | Give the predicate name, for example to_pandas("good"). |
ImportError from to_pandas | pandas is not installed. | Install pandas: pip install pandas. |
rumo rules shows "Rule support is not enabled". | The binary does not have the rules feature. | Build again with --features rules. |
Development
Repository structure
| Path | Contents |
|---|---|
src/lib.rs | The DataFrame structure and the Turtle conversion. |
src/rules.rs | The nemo rule runner. |
src/python.rs | The Python bindings (pyrumo). |
src/main.rs | The command-line tool. |
tests/ | The Rust tests and the Python tests. |
examples/ | Example data, rules and Python scripts. |
docs/ | This manual. |
Run the Rust tests
cargo test
cargo test --features rules
Build and test the Python package
-
Make a virtual environment:
python3 -m venv .venv source .venv/bin/activate -
Install maturin:
pip install maturin -
Build and install the package:
maturin develop --extras test -
Run the tests:
pytest
Build this manual
The manual uses mdBook.
-
Install mdBook:
cargo install mdbook -
Build the manual:
mdbook build docsThe HTML files are in
docs/book. -
To see the manual while you edit it, run:
mdbook serve docs --open
Publish this manual
The Docs workflow (.github/workflows/docs.yml) publishes the manual to GitHub Pages.
Before the first publication, do this procedure one time:
- On GitHub, open the repository settings.
- Select Pages.
- In Build and deployment, set Source to GitHub Actions.
The workflow starts in these conditions:
| Event | Result |
|---|---|
A push to master or main that changes docs/ | The workflow builds and publishes the manual. |
A pull request that changes docs/ | The workflow builds the manual. It does not publish it. |
| A manual start from the Actions tab | The workflow builds and publishes the manual. |
The manual is then at https://<owner>.github.io/<repository>/.
Writing style
Write the documentation in Simplified Technical English (ASD-STE100):
- Use short sentences. Write a maximum of 20 words in a procedure and 25 words in a description.
- Write one instruction in each sentence.
- Use the active voice.
- Use the imperative form in procedures.
- Use the same word for the same thing.
Make a release
- Change
versioninCargo.toml. This is also the version of the Python package. - Run
cargo checkto updateCargo.lock. - Commit and push these changes.
- Publish a GitHub release. The tag must be the version with a
vbefore it, for examplev0.2.0.
The Binaries workflow attaches the command-line archives to the release.
The Python workflow publishes the pyrumo wheels to PyPI.
Each workflow first compares the tag with the version in Cargo.toml.
If they are different, the workflow stops before it builds.
If the check fails, do these steps:
- Delete the GitHub release.
- Delete the tag:
git push --delete origin v0.2.0. - Change the version in
Cargo.toml, then commit and push. - Publish the release again.