Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ jobs:
uv run graphfaker fraud --scale 0.001 --seed 42 --out ./bank --sink pyg --no-report --quiet
test -f bank/graph.pt
uv run python examples/pyg_baseline.py --scale 0.001 --epochs 5
- name: The same, for the coordination pack
# Its truth uses different column names (is_coordinated, event_id), so
# the label rules being a convention rather than the fraud pack's
# schema is the thing this checks.
run: |
uv run graphfaker generate coordination --scale 0.0004 --seed 42 --out ./platform --sink pyg --quiet
test -f platform/graph.pt
uv run python examples/coordination_pyg_baseline.py --scale 0.0006 --tradecraft medium --epochs 5

audit:
name: dependency audit
Expand Down
103 changes: 103 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,109 @@
History
=======

1.1.0 (unreleased)
------------------

The coordination domain pack: a social platform, and the coordinated behaviour
inside it. And the machinery both packs inject patterns with, lifted out of
them into the schema and the engine.

* ``Pattern`` and the injection driver moved into the engine, and a domain's
pattern catalogue into the schema. ``graphfaker.schema.PatternCatalog`` and
``PatternSpec`` declare what a pack injects: every shape with its share of
the budget, its natural span, and, for a decoy, the shape it imitates.
``Camouflage`` is the set of dials every pack has (signature blend, timing
spread, overlap, decoy ratio, activity camouflage, size scale), which
``HardnessProfile`` and ``TradecraftProfile`` now subclass and rename to
their own vocabulary. ``graphfaker.engine.injection`` holds the shared
bookkeeping: the pattern record, the recruitment ledger, the budget
allocator, the decoy orders and the driver loop that runs a catalogue. The
fraud and coordination packs keep their own drawing and lose about 200 lines
of duplicated machinery between them, including two byte-identical copies of
the budget allocator. Datasets are unchanged: the same seed gives the same
fingerprint in both packs at every hardness and tradecraft level.

The social domain has been a ``GraphSchema`` preset since 0.5 and nothing more:
entities and a topology, one latent factor as its only ground truth, and no
tests. The fraud pack, by contrast, has a temporal process, a labelled pattern
catalogue, a measured hardness dial and a scoring harness. ``coordination``
gives the social side the same treatment, as a domain of its own rather than by
changing ``social``, because the graph is a different one: accounts, topics and
devices rather than people, places and products.

* ``graphfaker generate coordination`` produces accounts, topics and devices;
a follow graph with heavy-tailed in- and out-degree, ~50% reciprocity and
recoverable communities; and an organic event stream of posts, reshares and
replies with a diurnal rhythm and a lurker majority.
* Eight labelled inauthentic playbooks: ``copypasta``,
``amplification_ring``, ``reply_brigade``, ``follow_farm``,
``hashtag_flood``, ``sockpuppet_cluster``, ``astroturf_campaign``,
``account_handover``. ``follow_farm`` is there because an event-only detector
cannot see it at all, and ``recall_by_playbook`` makes a one-signal detector
visible.
* **Organic decoys, which are the point.** Coordinated behaviour has a strong
legitimate twin: a fandom reacting to a release, a city reacting to an
earthquake and a paid amplification ring are structurally the same thing. A
dataset whose only labelled structures are inauthentic rewards any detector
that fires on synchrony, which is the detector that suspends fan clubs.
``fandom_burst``, ``breaking_news`` and ``mutual_follow_community`` are
generated by the same machinery, labelled ``is_coordinated=False`` and
counted as false positives when flagged. Each is matched to its twin on the
properties that are not the point, chiefly posting volume.
* ``tradecraft`` (``low`` / ``medium`` / ``high``) is an input to a
measurement, not an assertion. Mean per-playbook best-single-feature AUC
falls 0.984 / 0.852 / 0.789, and the evidence that still works broadens from
identity and timing to structure and content.
* ``hardness_report`` scores sixteen features in five families and reports
which family found each playbook. ``decoy_separability`` scores each organic
structure against the playbook it imitates, and names the feature
responsible, because a decoy separable by one feature is not a decoy.
* ``evaluate`` scores a detector at account, event and campaign granularity,
with ``organic_false_positive_rate`` reported separately: on a real platform
the cost of suspending a fan club is not symmetric with the cost of missing
one ring.
* ``examples/coordination_detectors.py`` scores five rules a platform would try
first, at all three tradecraft levels. The conclusion matches the fraud
pack's Cypher cookbook: single-signal detectors do badly. The best F1
anywhere is 0.499, at the easiest setting, and the co-occurrence rule goes
from 0.843 precision with no organic false positives at ``low`` to 0.118
precision and 15% of organic accounts flagged at ``medium``.
* No node attribute names the answer, and a test asserts that no single feature
separates any playbook perfectly — a perfectly separating feature means a
label has leaked into the graph. It caught two such leaks during development:
account handover left ``reshare_count`` at exactly zero, and every sockpuppet
cluster was separable at AUC 1.000 by device sharing until organic device
sharing was given a tail of its own.
* 43 tests, including the pack's own thesis as an assertion: a co-occurrence
detector must flag organic bursts, or the decoys are not doing their job.
* A PyTorch Geometric export for the pack, via the existing ``pyg`` sink:
``--sink pyg``, ``to_hetero_data`` and ``from_directory`` all work on a
coordination dataset. Account carries ``y`` (coordinated), ``decoy``
(organic) and stratified splits; ``POSTED``, ``RESHARED`` and ``REPLIED``
carry ``y`` and ``edge_time``; ``community`` is a latent tensor rather than a
feature.
* The sink's label rules are now a convention rather than the fraud pack's
column names: a truth frame keyed by ``<entity>_id`` with a single boolean
column labels those entities. ``accounts.is_fraud`` and
``accounts.is_coordinated`` both work, and a third domain following the same
shape gets labels without touching the module. Identifiers and foreign keys
are kept out of edge attributes — the coordination pack's interaction edges
carry the topic they are about, which one-hot encoded to 48 columns and would
reach thousands at scale. Matching a label frame on values alone picked
``campaigns.topic``, which holds real Topic ids next to a boolean, and
labelled every topic; the id column now has to end in ``_id`` as well.
* ``examples/coordination_pyg_baseline.py``: a heterogeneous GraphSAGE against
a logistic regression on account features alone, at all three tradecraft
levels. The graph helps everywhere — average precision more than doubles at
``medium``, 0.101 to 0.243 — and it **flags three times as many organic
accounts**, 4.0% against 12.0%, because it buys its power by learning
"tightly connected group acting together" and a fan club is exactly that.
Measuring AUC alone says "graphs win", which is true and incomplete; that gap
is what the organic decoys exist to make visible. The baseline reports the
organic share at a fixed operating point, so two models with different score
distributions compare fairly.
* ``docs/domains/coordination.md``; ``docs/pyg.md`` covers both packs.

1.0.1 (unreleased)
------------------

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ pip install "graphfaker[osm]" # the OpenStreetMap fetcher (osmnx and its
pip install "graphfaker[examples]" # adds matplotlib, ladybug and jupyter for the notebooks
```

The fraud pack, the social domain, schemas and every sink are in the base install. Database clients and the PyG export are extras: `[neo4j]`, `[ladybug]`, `[duckdb]`, `[pyg]`. `graphfaker info` shows which are installed.
The fraud pack, the coordination pack, the social domain, schemas and every sink are in the base install. Database clients and the PyG export are extras: `[neo4j]`, `[ladybug]`, `[duckdb]`, `[pyg]`. `graphfaker info` shows which are installed.

Or without a Python environment, as a container ([docs](https://graphfaker.readthedocs.io/en/latest/get-started/docker.html)):

Expand Down Expand Up @@ -98,6 +98,7 @@ run = fraud.generate(scale=0.01, hardness="medium", seed=42)
|---|---|---|
| `social` | people, places, organizations, events and products; a few hubs and many quiet nodes, friends who know each other's friends, communities whose members are alike, organizations as big as their headcount | `GraphFaker.generate_graph(source="faker")` or `graphfaker generate social` |
| `fraud` | a bank: customers, accounts, merchants, devices, counterparties; a realistic transaction process; eleven labelled laundering typologies with decoys and a measured hardness | `graphfaker fraud` or `graphfaker generate fraud` |
| `coordination` | a social platform: accounts, topics, devices; follows, posts, reshares and replies over time; eight labelled coordination playbooks, **organic bursts that look exactly like them**, and a measured tradecraft level | `graphfaker generate coordination` |
| your own | a `GraphSchema`, or a process with injected patterns | [docs/adding-a-domain.md](docs/adding-a-domain.md) |

Real-world sources, loaded rather than generated:
Expand Down
1 change: 1 addition & 0 deletions docs/_readme_pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

social
fraud
coordination
real-world
```
""",
Expand Down
40 changes: 40 additions & 0 deletions docs/adding-a-domain.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,46 @@ The contract for `generate.py`:

If your domain injects patterns, also provide a way to measure them. The fraud pack's `hardness_report` (single-feature AUCs against the truth) and `evaluate` (precision and recall at entity, event and pattern level) are written for that domain, but the approach transfers: list the naive rules someone would try first, and report how well each one does.

### Patterns you declare, drawing you write

Two packs inject patterns, and the parts they share are in the engine rather than copied between them.

Declare the catalogue with [`PatternCatalog`](reference/api.rst): every shape with its share of the budget and its natural span in days, then the decoys, each naming the shape it imitates.

```python
from graphfaker.schema import PatternCatalog, PatternSpec

CATALOG = PatternCatalog(
base=1_000, # patterns at scale 1.0
floor=2, # at least this many of every shape, so small datasets cover the catalogue
patterns=[
PatternSpec(name="fan_in", share=0.14, span_days=2.0),
PatternSpec(name="cycle", share=0.10, span_days=2.0),
PatternSpec(name="decoy_fan_in", imitates="fan_in", span_days=2.0),
],
)
```

`CATALOG.total(scale)` is how many patterns a dataset gets, `CATALOG.counts(total)` splits them across the shapes, and `CATALOG.decoys`, `CATALOG.twins` and `CATALOG.span_days(name)` are what the injector and the hardness report read.

Declare the dials with `Camouflage`, or a subclass that adds your own and renames these to whatever your readers call them. The fraud pack calls `signature_blend` `amount_blend`, because in a bank the signature is the amount; the coordination pack calls it `text_blend`.

Then write a context and the drawing. `InjectionContext` holds the dials, the period, who is already in a pattern and whether overlap is allowed; subclass it and add the methods that make your domain what it is.

```python
from graphfaker.engine.injection import InjectionContext, Pattern, round_robin_decoys, run_catalog

class MyContext(InjectionContext):
def pick(self, k): ... # recruitment, your rules
def emit(self, pattern, ...): ... # a row, then pattern.touch(timestamp)

patterns = run_catalog(ctx, counts, FUNCTIONS, make, round_robin_decoys(CATALOG, n_decoys))
```

`run_catalog` runs the shapes in catalogue order, numbers them, runs the decoys last with overlap off, and returns the records. What it deliberately does not do is draw anything: how many sources a fan-in has and what a copypasta posts is the part that does not generalise, and the part worth writing yourself.

The order that loop walks in is part of what a seed reproduces, so changing it changes every dataset the domain has produced. Both decoy orders are available (`round_robin_decoys`, `grouped_decoys`) because the two packs chose differently before the code was shared, and changing either would have rewritten published measurements for no gain.

## Registering

Built-in domains are registered in `graphfaker/domains/__init__.py`:
Expand Down
Loading
Loading