Filling columns¶
What goes in each non-metric cell? Plotsim has a vocabulary of column types for keys, foreign keys, faker draws, ranges, pool sampling, buckets, geos, narratives, nested cells, and SCD. This page walks each one with a working example.
The vocabulary¶
Every column declares a name and a type. The type is a single
plain-language word (sometimes with a sub-field like
metric.<name> or ref.<dim>):
| Type | What it produces | Allowed on |
|---|---|---|
id |
Sequential primary key — c_0001, c_0002, ... |
Every table |
ref.<table> |
Foreign key into <table> |
Every table |
metric.<name> |
Metric value derived from trajectory | per_entity_per_period facts, per_period facts, bridges |
faker.<kind> |
Faker-generated string or int | Dims, facts, events, bridges |
geo.<field> |
Row-coherent geo bundle (city, country, lat, lon, ...) | Dims, facts, events |
static.<value> |
Constant value (or cycled comma-list) | Dims, facts, events |
pool.<attr> |
Per-entity choice from segment's attribute list | per_entity dims, per_entity_per_period facts, variable facts, per_parent_row children, events |
range |
Per-row uniform draw between numeric bounds | Every table |
bucket |
Trajectory-mapped band label | per_entity_per_period facts |
narrative |
Trajectory- + archetype-driven text | per_entity_per_period facts only |
struct / array |
Nested cell with declared schema | Facts |
scd |
SCD2 versioning trigger | per_entity dims only |
timestamp |
Per-row datetime within the period | Facts, events |
flag |
Boolean | Dims, facts, events |
date / int / string / float |
Bare dtype declarations | dim_date columns |
segment.count |
Per-entity copy of the segment's original count | Dims only |
The full validity matrix lives in Reference: Column types.
Primary keys and foreign keys¶
Every dim, fact, and event with a unique row should declare a primary
key column of type id. The engine derives the PK prefix from the
table name (fct_orders → o_0001, o_0002, ...). If two tables
collide on the same first character, both promote to their full stems
(orders, order_items).
Foreign keys use ref.<table>, where <table> is the parent dim
name. The column name should match the parent's PK column for
auto-derived joins.
Faker columns¶
A faker column draws from one of faker's providers. Common kinds:
| Kind | Output |
|---|---|
faker.name |
Person name ("Maria Lopez") |
faker.company |
Company name ("Acme Corp") |
faker.email |
Email ("maria@acme.com") |
faker.sentence |
One-sentence string |
faker.year |
Year integer (2019) |
faker.bothify |
Custom pattern via bothify |
The engine threads UserInput.locale (default "en_US") through every
faker instance. Pass a list for multi-locale draws.
config = create(
about="Multi-locale customers",
unit="customer",
window=("2024-01", "2024-12", "monthly"),
locale=["en_US", "en_GB", "es_ES", "fr_FR"],
metrics=[{"name": "engagement", "type": "score", "polarity": "positive"}],
segments=[{"name": "active", "count": 60, "archetype": "growth"}],
dimensions=[
{
"name": "dim_customer",
"per": "unit",
"columns": [
{"name": "customer_id", "type": "id"},
{"name": "customer_name", "type": "faker.name"},
{"name": "customer_email","type": "faker.email"},
],
},
],
)
about: Multi-locale customers
unit: customer
window: { start: "2024-01", end: "2024-12", every: monthly }
locale: [en_US, en_GB, es_ES, fr_FR]
metrics:
- { name: engagement, type: score, polarity: positive }
segments:
- { name: active, count: 60, archetype: growth }
dimensions:
- name: dim_customer
per: unit
columns:
- { name: customer_id, type: id }
- { name: customer_name, type: faker.name }
- { name: customer_email, type: faker.email }
Faker columns force the scalar fact-builder path (RNG order and faker consumption order are preserved).
Geo bundles¶
geo.<field> columns produce a row-coherent geo bundle. Multiple geo
columns on the same table draw the same city/country pair per row
— so latitude and longitude actually correspond to the city name on
the same row.
The lat and lon columns are float; the others are string.
Static columns¶
A static column emits the same value for every row, or cycles through
a comma-separated list:
| Form | Result |
|---|---|
static.active |
Every row gets "active" |
static.admin,member,viewer |
Cycle: row 1 admin, row 2 member, row 3 viewer, row 4 admin, ... |
Static values are numeric-coerced to float when possible, otherwise
string.
Pool columns¶
A pool.<attr> column samples per-entity from a list defined on each
segment's attributes. Each entity is bound to one choice from its
segment's pool for the column's lifetime.
config = create(
about="SaaS health",
unit="company",
window=("2024-01", "2024-12", "monthly"),
metrics=[{"name": "engagement", "type": "score", "polarity": "positive"}],
segments=[
{
"name": "rapid",
"count": 18,
"archetype": "growth",
"attributes": {
"industry": ["saas", "fintech"],
"region": ["na", "emea"],
},
},
{
"name": "steady",
"count": 24,
"archetype": "flat",
"attributes": {
"industry": ["healthcare", "manufacturing"],
"region": ["na"],
},
},
],
dimensions=[
{
"name": "dim_company",
"per": "unit",
"columns": [
{"name": "company_id", "type": "id"},
{"name": "company_name", "type": "faker.company"},
{"name": "industry", "type": "pool.industry"},
{"name": "region", "type": "pool.region"},
],
},
],
)
about: SaaS health
unit: company
window: { start: "2024-01", end: "2024-12", every: monthly }
metrics:
- { name: engagement, type: score, polarity: positive }
segments:
- name: rapid
count: 18
archetype: growth
attributes:
industry: [saas, fintech]
region: [na, emea]
- name: steady
count: 24
archetype: flat
attributes:
industry: [healthcare, manufacturing]
region: [na]
dimensions:
- name: dim_company
per: unit
columns:
- { name: company_id, type: id }
- { name: company_name, type: faker.company }
- { name: industry, type: pool.industry }
- { name: region, type: pool.region }
Every entity that produces rows in the target table must be covered by the segment's attribute list — missing keys raise at load.
Pool columns are dtype: string only; numerics in attributes are
stringified.
On a per_entity_per_period fact, pool draws happen per row (each
period draws independently from the entity's pool list) — not per
entity broadcast across periods. For "draw once, broadcast" semantics,
put the column on the per_entity dim.
Range columns¶
A per-row uniform draw between numeric bounds. Integer columns use
rng.integers(min, max + 1) (inclusive upper); float columns use
rng.uniform(min, max) (exclusive upper, per numpy).
range is valid on every table grain.
Bucket columns¶
A bucket column maps each row's trajectory position into one of N
evenly-spaced bands. Use for trajectory-driven categorical labels.
The first label covers [0, 1/N), the second [1/N, 2/N), ..., the
last covers [(N-1)/N, 1.0]. Bucket lists must have 2..20 unique
non-empty entries.
Bucket columns are per_entity_per_period fact only.
Narrative columns¶
A narrative column emits trajectory- and archetype-driven text from a
template with {slot} placeholders and per-archetype lexicons. The
lexicon keys are segment names, not archetype recipe words —
because the builder names each segment's archetype after the segment.
facts=[
{
"name": "fct_review",
"metrics": ["sentiment"],
"columns": [
{"name": "date_key", "type": "ref.dim_date"},
{"name": "customer_id","type": "ref.dim_customer"},
{"name": "sentiment", "type": "metric.sentiment"},
{
"name": "review_text",
"type": "narrative",
"template": "{verdict} — {detail}.",
"bands": ["low", "mid", "high"],
"lexicons": {
"fans": {
"verdict": {
"low": ["Disappointed"],
"mid": ["Decent"],
"high": ["Love it"],
},
"detail": {
"low": ["expected better"],
"mid": ["does the job"],
"high": ["best in class"],
},
},
"detractors": {
"verdict": {
"low": ["Awful"],
"mid": ["Mediocre"],
"high": ["Surprisingly fine"],
},
"detail": {
"low": ["wasted my money"],
"mid": ["not great"],
"high": ["pleasantly surprised"],
},
},
},
},
],
},
]
facts:
- name: fct_review
metrics: [sentiment]
columns:
- { name: date_key, type: ref.dim_date }
- { name: customer_id, type: ref.dim_customer }
- { name: sentiment, type: metric.sentiment }
- name: review_text
type: narrative
template: "{verdict} — {detail}."
bands: [low, mid, high]
lexicons:
fans:
verdict:
low: [Disappointed]
mid: [Decent]
high: [Love it]
detail:
low: [expected better]
mid: [does the job]
high: [best in class]
detractors:
verdict:
low: [Awful]
mid: [Mediocre]
high: [Surprisingly fine]
detail:
low: [wasted my money]
mid: [not great]
high: [pleasantly surprised]
The template's {slot} placeholders must match lexicon keys exactly.
Lexicons must cover every segment in use. Bands default to
("low", "mid", "high") if omitted. Per-band phrase lists capped at
100 entries.
Narratives force the scalar fact-builder path.
Struct and array columns¶
Nested cells in one column. Struct columns declare a nested_schema
dict of {field: dtype}; array columns declare array_element_type
and optional array_length (default 3, 1..100).
Struct columns are capped at 20 fields. Nested-of-nested is rejected (one level only). Struct/array columns force the scalar fact-builder path.
Nested cells survive Parquet (with an explicit pyarrow schema), JSONL (as native nested JSON), and SQL writes; CSV serializes the cell as a string repr.
Timestamp and flag¶
timestamp emits a per-row datetime. On facts, it anchors to the
period start; on events, it distributes uniformly within the period's
calendar extent (monthly: 1st through end-of-month; weekly: seven
days; daily: 24 hours).
flag is a boolean column. The engine emits True/False; CSV
writes them as bare strings.
Bare dtype words on dim_date¶
The dim_date columns accept bare dtype words — date, int,
string, float. The builder generates the source from date_key so
you don't have to spell out the engine source string:
Naming rules¶
Table and column names are SQL-safe identifiers: [A-Za-z_][A-Za-z0-9_]{0,127}.
The engine rejects anything else at load. Pool, narrative, range, and
faker columns inherit the rule.
Going deeper¶
- Designing tables — how columns sit inside table types and grains.
- Reference: Column types — full validity matrix per table grain.