Locy Use Case: Fraud Risk Propagation¶
Propagate account risk backward over transfer edges and isolate clean accounts.
This notebook uses schema-first mode (recommended): labels, edge types, and typed properties are defined before ingest.
How To Read This Notebook¶
- Step 1 initializes an isolated local database.
- Step 2 defines schema (the recommended production path).
- Step 3 seeds a minimal graph for this use case.
- Step 4 declares Locy rules and query statements.
- Steps 5-6 evaluate and inspect command/query outputs.
- Step 7 tells you what to look for in the results.
1) Setup¶
Creates a temporary database directory so the example is reproducible and leaves no state behind.
import os
import shutil
import tempfile
from pprint import pprint
import uni_db
DB_DIR = tempfile.mkdtemp(prefix="uni_locy_")
print("DB_DIR:", DB_DIR)
db = uni_db.Uni.open(DB_DIR)
session = db.session()
DB_DIR: /tmp/uni_locy_0j1xeu9v
2) Define Schema (Recommended)¶
Define labels, property types, and edge types before inserting data.
(
db.schema()
.label("Account")
.property("id", "string")
.property("flagged", "bool")
.done()
.edge_type("TRANSFER", ["Account"], ["Account"])
.done()
.apply()
)
print('Schema created')
Schema created
3) Seed Graph Data¶
Insert only the entities/relationships needed for this scenario so rule behavior stays easy to inspect.
tx = session.tx()
tx.execute("CREATE (:Account {id: 'A1', flagged: true})")
tx.execute("CREATE (:Account {id: 'A2', flagged: false})")
tx.execute("CREATE (:Account {id: 'A3', flagged: false})")
tx.execute("CREATE (:Account {id: 'A4', flagged: false})")
tx.execute("MATCH (a1:Account {id:'A1'}), (a2:Account {id:'A2'}) CREATE (a1)-[:TRANSFER]->(a2)")
tx.execute("MATCH (a2:Account {id:'A2'}), (a3:Account {id:'A3'}) CREATE (a2)-[:TRANSFER]->(a3)")
tx.execute("MATCH (a4:Account {id:'A4'}), (a3:Account {id:'A3'}) CREATE (a4)-[:TRANSFER]->(a3)")
tx.commit()
print('Seeded graph data')
Seeded graph data
4) Locy Program¶
CREATE RULE defines derived relations. QUERY ... WHERE ... RETURN ... reads from those relations.
program = r'''
CREATE RULE risky_seed AS
MATCH (a:Account)
WHERE a.flagged = true
YIELD KEY a
CREATE RULE risky AS
MATCH (a:Account)
WHERE a IS risky_seed
YIELD KEY a
CREATE RULE risky AS
MATCH (a:Account)-[:TRANSFER]->(b:Account)
WHERE b IS risky
YIELD KEY a
CREATE RULE clean AS
MATCH (a:Account)
WHERE a IS NOT risky
YIELD KEY a
QUERY risky WHERE a.id = a.id RETURN a.id AS risky_account
QUERY clean WHERE a.id = a.id RETURN a.id AS clean_account
'''
print(program)
CREATE RULE risky_seed AS
MATCH (a:Account)
WHERE a.flagged = true
YIELD KEY a
CREATE RULE risky AS
MATCH (a:Account)
WHERE a IS risky_seed
YIELD KEY a
CREATE RULE risky AS
MATCH (a:Account)-[:TRANSFER]->(b:Account)
WHERE b IS risky
YIELD KEY a
CREATE RULE clean AS
MATCH (a:Account)
WHERE a IS NOT risky
YIELD KEY a
QUERY risky WHERE a.id = a.id RETURN a.id AS risky_account
QUERY clean WHERE a.id = a.id RETURN a.id AS clean_account
5) Evaluate Locy Program¶
Run the program, then inspect materialization stats (iterations, strata, and executed queries).
out = session.locy(program)
print("Derived relations:", list(out.derived.keys()))
stats = out.stats
print("Iterations:", stats.total_iterations)
print("Strata:", stats.strata_evaluated)
print("Queries executed:", stats.queries_executed)
Derived relations: ['clean', 'risky', 'risky_seed']
Iterations: 2
Strata: 3
Queries executed: 14
6) Inspect Command Results¶
Each command result can contain rows; this is the easiest way to verify your rule outputs and query projections.
print("Derived relation snapshots:")
for rel_name, rel_rows in out.derived.items():
print(f"\\n{rel_name}: {len(rel_rows)} row(s)")
pprint(rel_rows)
if out.command_results:
print("\\nCommand results:")
for i, cmd in enumerate(out.command_results, start=1):
print(f"\\nCommand #{i}:", cmd.command_type)
rows = getattr(cmd, 'rows', None)
if rows is not None:
pprint(rows)
if not out.command_results:
print("\\nNo QUERY/EXPLAIN/ABDUCE command outputs in this program.")
Derived relation snapshots:
\nclean: 3 row(s)
[{'a': Node(id=1, labels=["Account"], properties={'id': 'A2', 'flagged': False})},
{'a': Node(id=2, labels=["Account"], properties={'flagged': False, 'id': 'A3'})},
{'a': Node(id=3, labels=["Account"], properties={'flagged': False, 'id': 'A4'})}]
\nrisky: 1 row(s)
[{'a': Node(id=0, labels=["Account"], properties={'flagged': True, 'id': 'A1'})}]
\nrisky_seed: 1 row(s)
[{'a': Node(id=0, labels=["Account"], properties={'flagged': True, 'id': 'A1'})}]
\nCommand results:
\nCommand #1: query
[{'risky_account': 'A1'}]
\nCommand #2: query
[{'clean_account': 'A2'}, {'clean_account': 'A3'}, {'clean_account': 'A4'}]
7) What To Expect¶
Use these checks to validate output after evaluation:
- A1 is risky by seed; A2 and A4 become risky by backward propagation through TRANSFER.
- A3 should remain in clean because it does not transfer to a risky account.
- Two query result blocks should appear: one for risky, one for clean.
8) Cleanup¶
Delete the temporary database directory created in setup.
Cleaned up /tmp/uni_locy_0j1xeu9v