Skip to content

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_orderso_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.

facts=[
    {
        "name": "fct_orders",
        "metrics": ["purchase_intent"],
        "columns": [
            {"name": "order_id",    "type": "id"},
            {"name": "date_key",    "type": "ref.dim_date"},
            {"name": "customer_id", "type": "ref.dim_customer"},
            {"name": "total",       "type": "metric.purchase_intent"},
        ],
    },
]
facts:
  - name: fct_orders
    metrics: [purchase_intent]
    columns:
      - { name: order_id,    type: id }
      - { name: date_key,    type: ref.dim_date }
      - { name: customer_id, type: ref.dim_customer }
      - { name: total,       type: metric.purchase_intent }

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.

dimensions=[
    {
        "name": "dim_store",
        "per": "unit",
        "columns": [
            {"name": "store_id", "type": "id"},
            {"name": "city",     "type": "geo.city"},
            {"name": "country",  "type": "geo.country"},
            {"name": "lat",      "type": "geo.lat"},
            {"name": "lon",      "type": "geo.lon"},
        ],
    },
]
dimensions:
  - name: dim_store
    per: unit
    columns:
      - { name: store_id, type: id }
      - { name: city,     type: geo.city }
      - { name: country,  type: geo.country }
      - { name: lat,      type: geo.lat }
      - { name: lon,      type: geo.lon }

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.

dimensions=[
    {
        "name": "dim_user",
        "per": "unit",
        "count": 4,
        "columns": [
            {"name": "user_id",   "type": "id"},
            {"name": "company_id","type": "ref.dim_company"},
            {"name": "user_role", "type": "static.admin,member,member,viewer"},
        ],
    },
]
dimensions:
  - name: dim_user
    per: unit
    count: 4
    columns:
      - { name: user_id,    type: id }
      - { name: company_id, type: ref.dim_company }
      - { name: user_role,  type: "static.admin,member,member,viewer" }

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).

columns=[
    {"name": "discount_pct", "type": "range", "range": [0, 25]},     # integer
    {"name": "rating",       "type": "range", "range": [1.0, 5.0]},  # float
]
columns:
  - { name: discount_pct, type: range, range: [0, 25] }
  - { name: rating,       type: range, range: [1.0, 5.0] }

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.

columns=[
    {
        "name": "health_tier",
        "type": "bucket",
        "labels": ["red", "yellow", "green"],
    },
]
columns:
  - name: health_tier
    type: bucket
    labels: [red, yellow, green]

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).

columns=[
    {
        "name": "device_info",
        "type": "struct",
        "nested_schema": {"os": "string", "version": "string", "screen_height": "int"},
    },
    {
        "name": "feature_tags",
        "type": "array",
        "array_element_type": "string",
        "array_length": 5,
    },
]
columns:
  - name: device_info
    type: struct
    nested_schema:
      os: string
      version: string
      screen_height: int
  - name: feature_tags
    type: array
    array_element_type: string
    array_length: 5

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:

dimensions=[
    {
        "name": "dim_date",
        "per": "period",
        "columns": [
            {"name": "date_key", "type": "id"},
            {"name": "date",     "type": "date"},
            {"name": "year",     "type": "int"},
            {"name": "month",    "type": "int"},
            {"name": "quarter",  "type": "int"},
        ],
    },
]
dimensions:
  - name: dim_date
    per: period
    columns:
      - { name: date_key, type: id }
      - { name: date,     type: date }
      - { name: year,     type: int }
      - { name: month,    type: int }
      - { name: quarter,  type: int }

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