Knowledge Graph Specialist at Data Treehouse
A Norwegian startup building high-performance knowledge graph tooling, entirely on open standards.
Industries: Oil & Gas, Electrical Grids, Energy, Rail, Public Sector, Defence, Aquaculture, Maritime, Finance.
Published benchmarks
| Workload | vs. | Speedup |
|---|---|---|
| SHACL validation | Jena, RDF4J, TopBraid | 4–22× |
| Time-series + graph | Ontop | 10–85× |
| SPARQL load & query | 11 frameworks (Trainmarks) | leading |
Peer-reviewed: IEEE Access, Expert Systems & Applications. Trainmarks: datatreehouse.github.io/trainmarks
Selected clients: AkerBP, Elvia, BKK, Bane NOR, DNV, DVLA (UK), EU Agency for Railways, Brønnøysundregistrene, Mattilsynet, Riksantikvaren, Nasjonalarkivet.
Python. DataFrames. Pipelines. Templates that look like SQL views.
DataFrame in. Knowledge graph out. Polars-backed. Rust under the hood.
Build something tonight. Query, validate, reason on the same Model.
Data engineer
Knowledge engineer
subject predicate object, not <key, value>.
http://data.treehouse.example/book/Dune.
As a DataFrame
shape: (1, 4)
┌───────┬───────────────┬─────────────┬──────┐
│ title │ book_iri │ isbn │ year │
╞═══════╪═══════════════╪═════════════╪══════╡
│ Dune │ book:Dune │ 97804411... │ 1965 │
└───────┴───────────────┴─────────────┴──────┘
One row. Four columns. Schema lives in your head.
As triples (Turtle)
book:Dune rdfs:label "Dune" .
book:Dune :isbn "9780441172719" .
book:Dune :publicationYear 1965 .
Three statements. Subject, predicate, object. Schema lives in the data.
A knowledge graph is just a lot of these, sharing subjects and objects. Connections emerge from reuse, not from schema.
$ pip install maplib
from maplib import Model
import polars as pl
m = Model() # empty graph
m.size() # 0 triples
A Model is one in-memory knowledge graph. That is the whole entry point.
import polars as pl
from maplib import Model
ns = "http://data.treehouse.example/book/"
df = pl.read_csv("data/books.csv")
# build the subject IRI column
df = df.with_columns(
(ns + pl.col("title")).alias("book_iri")
)
m = Model()
m.map_default(df, "book_iri")
m.size() # N triples, ready to query
Four lines that matter:
iri column. Your subjects live there.Model() creates the empty graph.map_default turns every other column into a predicate.Want more control over predicates and types? Write an stOTTR template and use m.map(...) instead.
df = m.query("""
PREFIX def: <urn:maplib_default:>
SELECT ?title ?year
WHERE {
?b def:title ?title ;
def:publicationYear ?year .
FILTER(?year >= 1980)
}
""")
type(df) # polars.DataFrame
SPARQL is the SQL of graphs.
SELECT returns a Polars DataFrame.CONSTRUCT inserts new facts into the graph.DELETE and INSERT mutate the graph.Pattern-match across all your data in the Model. No JOINs to write. The predicates do that work.
Already standardised, already maintained:
# Load the ontology into the Model
m.read("ontology.ttl")
# Book rdfs:subClassOf bibo:Book
# Author rdfs:subClassOf schema:Person
# Type your book IRIs as :Book
m.update("""
PREFIX : <http://data.treehouse.example/>
PREFIX def: <urn:maplib_default:>
INSERT { ?s a :Book }
WHERE { ?s def:title ?title }
""")
Your data is now connected to a knowledge layer that someone else maintains.
# authors, series, books, adaptations -> one Model
m.map_default(df_authors, "author_iri")
m.map_default(df_series, "series_iri")
m.map_default(df_books, "book_iri")
m.map_default(df_adaptations, "adaptation_iri")
df = m.query("""
PREFIX : <http://data.treehouse.example/>
SELECT ?author (COUNT(?adaptation) AS ?n)
WHERE {
?adaptation :basedOn ?book .
?author :wrote ?book .
} GROUP BY ?author
""")
# Tolkien 4 · Adams 2 · Herbert 2 · ...
Four datasets joined themselves through shared IRIs.
And there's more:
?b :nextInSeries+ ?later traverses a series to any depth. One line. No recursive CTE.SERVICE <http://dbpedia.org/sparql> { ... } reaches into a remote endpoint as if it were local.# sh.ttl: every Book must have a title,
# a 13-digit ISBN, a publication year,
# and belong to a Series.
m.read("ttl/sh.ttl")
report = m.validate()
report.results()
Schema, but declarative.
results() returns them as a DataFrame.Note: validate is licensed for commercial use. Free for academic, personal, and evaluation use.
Datalog rules with m.infer
# If an author wrote a book, then the
# book was written by that author.
[?book, :writtenBy, ?author] :-
[?author, :wrote, ?book] .
m.infer(rules)
Rules-based. Inverse properties, transitive closures, role propagation. Inferred triples are first-class.
Recursive SPARQL CONSTRUCT with m.infer
m.infer(sparql="""
CONSTRUCT { ?b a :ModernEraBook }
WHERE {
?b a :Book ;
:publicationYear ?year .
FILTER(?year >= 1980)
}""")
Pattern, filter, materialize, repeat until fixpoint. Each round can match facts the previous round produced.
Note: infer is licensed for commercial use. Free for academic, personal, and evaluation use.
Your graph models the books, the authors, the series.
Daily sales live in a time-series store. Millions of rows. You don't want them in your graph.
Don't copy them. Reference them.
# Link a time-series source to the graph
m.add_virtualization("config.yaml")
df = m.query("""
SELECT ?author (SUM(?copies) AS ?total)
WHERE {
?author :wrote ?book .
?book :salesSeries ?ts .
?ts :reading [ :date ?d ;
:copiesSold ?copies ] .
FILTER(?d >= "2026-01-01"^^xsd:date)
} GROUP BY ?author
""")
Three systems, three schemas, one concept of customer. The graph is where they finally agree, through shared IRIs, not ETL.
The business definition of active subscription changes quarterly. Templates and rules are easier to edit than star schemas.
LLMs are great at language, bad at facts. A knowledge graph gives an agent deterministic, queryable, auditable answers.
Open standards: RDF, OWL, SHACL, SPARQL. Portable across vendors. Auditable by design. Lineage you can query.
If your hardest problem is "fast scans over one big table", stay in Polars. If it is "what does this mean, across everything we have", reach for the graph.
$ pip install maplib
Where to go next: