Trillboards Attribution Data Model Ontology

version 1.0.0

Machine-readable schema definitions for the 6-layer DOOH attribution data model. These schemas map directly to PostgreSQL tables created in migrations 092-096.

Data Flow: From Signal to Insight


Layer 1: Signal Ingestion
  device_signals (Intuizi mobile pings) + poi_reference (brand locations)
      |
Layer 2: Exposure Matching
  exposure_device_matches (PostGIS ST_DWithin: device near screen during ad play)
      |
Layer 3: Attention Enrichment
  attention_exposure_matches (FEIN edge AI: face count, gaze, emotion per exposure)
      |
Layer 4: Identity Resolution
  identity_nodes + identity_edges + household_clusters (cross-device graph)
      |
Layer 5: Attribution
  touchpoint_events + attribution_chains (multi-touch credit across channels)
      |
Layer 6: Measurement
  incrementality_experiments + incrementality_results (Bayesian uplift)
  creative_attribution_performance (creative x outcome correlation)
  campaign_attribution_summary (daily rollup)

Data Handling

Partitioning Strategy

Three tables are range-partitioned by timestamp for performance:


Schemas

16 schemas. Every one is derived from docs/openapi/attribution-data-models.yaml at request time.

AttentionCampaignMetrics · AttentionExposureMatch · AttributionChain · CampaignAttributionSummary · CreativeAttributionPerformanceRecord · DataProviderIngestionLog · DeviceSignal · DeviceVisitation · ExposureDeviceMatch · HouseholdCluster · IdentityEdge · IdentityNode · IncrementalityExperiment · IncrementalityResultRecord · POIReference · TouchpointEvent

AttentionCampaignMetrics

Daily rollup of attention-weighted campaign metrics. One row per campaign + creative + date. Used for aCPM calculations and attention timeseries visualizations.

Table: attention_campaign_metrics Migration: 243_create_attention_attribution_tables.sql

PropertyTypeRequiredDescription
metric_idinteger
campaign_idstring
creative_idstring, nullable
advertiser_idstring, nullable
metric_datestring
total_impressionsinteger
unique_devicesinteger
total_attention_secondsnumberSum of (attention_score dwell_seconds face_count) across all impressions
avg_attention_scorenumber
median_attention_scorenumber
p90_attention_scorenumber
avg_dwell_secondsnumber
avg_face_countnumber
standard_cpmnumberTraditional CPM (cost / impressions * 1000)
attention_cpmnumberaCPM = cost / (total_attention_seconds / 1000)
cost_per_attentive_reachnumber
cost_per_attentive_visitnumber
avg_emotion_engagementnumber
positive_emotion_pctnumberPercentage of impressions with happy/surprised/interested emotions
attention_qualified_pctnumberPercentage of impressions with attention_score >= 0.5
daypart_breakdownobject
emotion_breakdownobject
venue_type_breakdownobject

AttentionExposureMatch

Exposure match enriched with FEIN edge AI inference signals. Joins exposure_device_matches with inference_outcomes to attach attention, emotion, and demographic data to each exposure.

What the row MODELS, stated precisely: a device-to-play match from exposure_device_matches, carrying the edge-AI inference recorded for that play — face count, gaze, emotion classification and dwell. It is a join, not an independent observation: the exposure leg and the attention leg are produced by different subsystems, and a row exists only where both fired for the same play.

Population is a property of the deployment, not of the schema. Before building on any field here, count the table for your own environment and check matched_at freshness — a schema contract cannot tell you whether the pipeline that fills it is running.

Table: attention_exposure_matches (partitioned monthly by matched_at) Migration: 243_create_attention_attribution_tables.sql

PropertyTypeRequiredDescription
match_idinteger
play_idstring
maid_hashstring
campaign_idstring, nullable
creative_idstring, nullable
screen_mongo_idstring
face_countinteger, nullableNumber of faces detected during this ad play
attention_scorenumber, nullableFraction of faces looking at screen (0.0-1.0)
emotion_scorenumber, nullableEmotional engagement intensity (0.0-1.0)
dominant_emotionstring, nullableMost common emotion detected (happy, surprised, neutral, etc.)
dwell_secondsnumber, nullableHow long faces were visible in the camera FOV
engagement_scorenumber, nullableBody engagement from FEIN (posture, orientation)
demographics_agestring, nullableAge bucket estimate (e.g., '25-34').
demographics_genderstring, nullableGender estimate (male/female/unknown).
base_confidencenumberOriginal confidence from exposure_device_matches
attention_weightnumberAttention weight factor (0-1). Derived from attention_score, face_count, and dwell_seconds. Used to compute weighted_confidence.
weighted_confidencenumberbase_confidence * (1 + attention_weight). This is the DEFINITIVE confidence score for this exposure. Higher = more certain the person saw AND paid attention to the ad.
venue_typestring, nullable
daypartstring, nullable
weatherstring, nullable
ambient_luxnumber, nullableAmbient light level at the screen location
matched_atstring

AttributionChain

Complete user journey from DOOH exposure to conversion. Links multiple touchpoint_events into a sequence with multi-touch credit allocation.

Table: attribution_chains Migration: 096

PropertyTypeRequiredDescription
chain_idstring
campaign_idstring
advertiser_idstring
maid_hashstring, nullable
household_idstring, nullable
touchpoint_countintegerNumber of touchpoints in this journey
channelsarray of stringOrdered list of channels in the journey
first_touch_atstring
last_touch_atstring
conversion_atstring, nullableWhen conversion occurred (store visit, purchase, etc.)
conversion_typestring, nullable
dooh_creditnumberFraction of conversion credit attributed to DOOH (0-1)
mobile_creditnumber
web_creditnumber
store_creditnumber
attribution_modelstring — one of time_decay, position_based, attention_weighted, shapley
conversion_value_centsinteger, nullable
attributed_revenue_centsinteger, nullable
total_confidencenumber, nullable
is_deterministicbooleanWhether all identity links are deterministic (vs probabilistic)

CampaignAttributionSummary

Daily rollup of campaign-level attribution metrics. This is the aggregate view consumed by the campaign summary endpoint.

Table: campaign_attribution_summary Migration: 093

PropertyTypeRequiredDescription
summary_idinteger
campaign_idstring
advertiser_idstring
summary_datestring
cohort_idstring, nullable
unique_devices_exposedinteger
total_exposuresinteger
avg_frequencynumber
total_media_costnumber
cost_per_unique_reachnumber
exposed_visitorsinteger
control_visitorsinteger
exposed_visit_ratenumber
control_visit_ratenumber
incremental_lift_pctnumber
incremental_visitsinteger
cost_per_incremental_visitnumber
control_group_sizeinteger
avg_match_confidencenumber
statistical_significancenumber
avg_time_to_visit_hnumber
venue_breakdownobject
daypart_breakdownobject
attribution_window_daysinteger

CreativeAttributionPerformanceRecord

Links creative execution to attribution outcomes. One row per campaign + creative + variant + date.

Answers: "Which creative x venue x daypart x weather combination drives the most store visits at the lowest cost?"

Table: creative_attribution_performance Migration: 096

PropertyTypeRequiredDescription
perf_idinteger
campaign_idstring
creative_idstring
variantstring, nullable
metric_datestring
avg_attention_scorenumber
avg_dwell_secondsnumber
avg_emotion_engagementnumber
dominant_emotionstring, nullable
store_visitsinteger
visit_ratenumber
incremental_lift_pctnumber
cost_per_visitnumber
best_venue_typestring, nullable
best_daypartstring, nullable
best_weatherstring, nullable
context_performanceobjectDetailed breakdown by context interaction

DataProviderIngestionLog

Audit trail for batch data ingestion runs. Tracks file processing status, row counts, and rejection reasons.

Table: data_provider_ingestion_log Migration: 092

PropertyTypeRequiredDescription
log_idinteger
providerstring
file_typestring
s3_bucketstring
s3_keystring
file_size_bytesinteger
rows_totalinteger
rows_insertedinteger
rows_rejectedinteger
rejection_reasonsobject
statusstring — one of pending, processing, completed, failed
error_messagestring, nullable
started_atstring, nullable
completed_atstring, nullable

DeviceSignal

Mobile device location observation from Intuizi. Each row = one device location ping (MAID + lat/lng + timestamp). MAIDs are stored as SHA-256 hashes (maid_hash).

Table: device_signals (partitioned monthly by observed_at) Migration: 092

PropertyTypeRequiredDescription
signal_idintegerAuto-incrementing primary key
maid_hashstringSHA-256 hash of the mobile advertising ID (IDFA/GAID)
locationstringPostGIS GEOGRAPHY(POINT, 4326) — used for ST_DWithin spatial joins
latitudenumber
longitudenumber
accuracy_mnumberGPS accuracy in meters (lower = more precise)
geohash_6stringGeohash-6 cell for fast spatial pruning before PostGIS join
observed_atstringWhen the device was observed at this location (partition key)
providerstringData provider name
signal_typestring — one of location, visitation, beacon
device_osstring — one of ios, android, unknown
dma_codestring, nullableNielsen DMA code for geo-holdout experiments
ingested_atstring
batch_idstring, nullableIngestion batch identifier for audit trail

DeviceVisitation

Observed visit to a POI by a mobile device (from Intuizi visitation data).

This table holds TWO different attribution legs and visit_role is the discriminator. Always filter by it. conversion_poi rows are the outcome leg — the device visited the advertiser's location. screen_exposure rows are the exposure leg — the device was near one of our screens. They answer opposite halves of the question, and a query that omits the predicate silently mixes them.

The column is NOT NULL DEFAULT 'conversion_poi' with a CHECK constraint (migration 20260618061000_add_device_visitation_visit_role.sql), and the table carries a dedicated partial index per role, which is the clearest statement that both are expected to be queried separately.

visit_role was absent from this schema entirely until 2026-09-01, so the document described the whole table as the outcome leg.

Table: device_visitation (partitioned monthly by visited_at) Migration: 092 (visit_role added in 20260618061000_add_device_visitation_visit_role.sql)

PropertyTypeRequiredDescription
visit_rolestring — one of conversion_poi, screen_exposureWhich attribution leg this row belongs to. NOT NULL, defaulting to conversion_poi. Filter on it in every query — see the schema description above.
visitation_idinteger
maid_hashstring
poi_idstringReferences poi_reference.poi_id
external_poi_idstring
distance_mnumberDistance from device to POI centroid in meters
dwell_secondsintegerHow long the device stayed at the POI
visited_atstringWhen the visit occurred (partition key)
providerstring
confidencenumberVisit confidence score (0-1)
ingested_atstring
batch_idstring, nullable

ExposureDeviceMatch

Core truth table: device X was near screen Y during ad play Z. Created by the exposure matching service using PostGIS ST_DWithin to join device_signals with play_logs based on spatial and temporal proximity.

Table: exposure_device_matches (partitioned monthly by matched_at) Migration: 093

PropertyTypeRequiredDescription
match_idinteger
play_idstringReferences play_logs — which ad was playing
maid_hashstringSHA-256 hashed MAID of the device
screen_mongo_idstringScreen identifier
campaign_idstring, nullable
creative_idstring, nullable
advertiser_idstring, nullable
distance_mnumberDistance between device and screen in meters
time_delta_sintegerSeconds between device observation and ad play
device_accuracy_mnumber, nullable
screen_latitudenumber
screen_longitudenumber
device_latitudenumber
device_longitudenumber
confidence_scorenumberMatch confidence (0-1). Based on inverse distance and time delta. Closer + more simultaneous = higher confidence.
match_methodstringMatching algorithm used
venue_typestring, nullable
screen_typestring, nullable
matched_atstringTimestamp of the match (partition key)

HouseholdCluster

Pre-computed household grouping from co-location analysis. Multiple MAIDs that consistently appear at the same location (same geohash, same time windows) are clustered into households.

Table: household_clusters Migration: 243_create_attention_attribution_tables.sql

PropertyTypeRequiredDescription
cluster_idstring
household_node_idstringReferences identity_nodes (node_type = 'household')
maid_hashstring
cluster_confidencenumberConfidence that this MAID belongs to this household (0-1)
colocation_countintegerNumber of times this MAID was co-located with household members
colocation_daysintegerNumber of distinct days with co-location observations
device_osstring, nullable
dma_codestring, nullable
first_colocatedstring, nullable
last_colocatedstring, nullable

IdentityEdge

Relationship between two identity nodes in the graph.

Table: identity_edges Migration: 243_create_attention_attribution_tables.sql

PropertyTypeRequiredDescription
edge_idstring
source_node_idstring
target_node_idstring
edge_typestring — one of household_member, fein_observation, cross_channel_link, ip_colocation, device_graph_partner, wifi_colocation, device_signal, pixel_ip_bridge, … (18 total)Relationship type. This list is the FULL identity_edges_edge_type_check constraint as of migration 20260621000800_add_maid_bridge_edge_type.sql. It previously declared five values; thirteen were missing, and the omitted set includes the edge types that carry the overwhelming majority of the table. - household_member: Devices in the same household (co-location evidence) - fein_observation: Devices seen together by FEIN cameras - cross_channel_link: MAID matched to pixel visitor or transaction - ip_colocation: Devices sharing the same IP address - device_graph_partner: Third-party device graph match - wifi_colocation: Devices seen on the same Wi-Fi BSSID - device_signal: Link asserted by a device-reported signal - pixel_ip_bridge: Pixel visitor joined to a device by IP - temporal_fingerprint: Devices linked by co-occurrence in time - email_hash_link: Link through a hashed email address - uid2_token: Link through a UID2 token - qr_scan: Link established by a QR scan - ble_proximity: Bluetooth-LE proximity between devices - network_device: Devices on the same network - pixel_match: Direct pixel-to-identity match - csi_proximity: Proximity from the Wi-Fi CSI radio path - maid_bridge: PROBABILISTIC bridge between co-present MAIDs - face_device: Face cluster linked to a device confidence is not comparable across edge types. maid_bridge in particular is derived as 1/N over co-present MAIDs and capped at 0.5, so a 0.5 there is the CEILING of a probabilistic inference, not the same quantity as a 0.5 on a deterministic edge.
confidencenumberEdge confidence (0-1). Higher = stronger evidence of relationship.
evidence_countintegerNumber of independent observations supporting this edge
evidence_typestring, nullable
first_linked_atstring
last_linked_atstring
metadataobject

IdentityNode

Vertex in the identity graph. Each node represents a device, household, FEIN cluster, pixel visitor, or other identity type.

Table: identity_nodes Migration: 243_create_attention_attribution_tables.sql

PropertyTypeRequiredDescription
node_idstring
node_typestring — one of maid, household, fein_cluster, ip_hash, wifi_bssid, fingerprint, pixel_id, transaction_id, … (15 total)Type of identity. This list is the FULL identity_nodes_node_type_check constraint as of migration 20260727191655_canonical_device_extended_identities.sql. It previously declared six values; nine were missing, including the three that carry most of the table. - maid: Mobile advertising ID (hashed) - household: Inferred household from co-location analysis - fein_cluster: Group of devices observed together by FEIN cameras - ip_hash: Hashed IP for coarse identity linking - wifi_bssid: Hashed Wi-Fi BSSID observed near a screen - fingerprint: Browser/device fingerprint - pixel_id: Web conversion pixel visitor - transaction_id: Purchase event identifier - email_hash: Hashed email address - qr_fingerprint: Fingerprint derived from a QR scan - csi_device: Device sensed by the Wi-Fi CSI radio path - screen: A Trillboards screen, as a node in the graph - canonical_visitor: The resolved visitor a cluster collapses to - face_cluster: Camera-derived face cluster - uid2_token: UID2 token identifier_hash is an HMAC-SHA256 keyed by pepper_kid, not a bare SHA-256 — you cannot reproduce it from the raw identifier alone.
identifier_hashstringSHA-256 hash of the actual identifier
device_osstring — one of ios, android, unknown
dma_codestring, nullable
geohash_6string, nullable
first_seen_atstring
last_seen_atstring
signal_countintegerNumber of times this identity has been observed
metadataobject

IncrementalityExperiment

Experiment definition for causal incrementality measurement. Supports geo-holdout, ghost ads, PSA control, and matched market designs.

Table: incrementality_experiments Migration: 096

PropertyTypeRequiredDescription
experiment_idstring
campaign_idstring
advertiser_idstring
experiment_typestring — one of geo_holdout, ghost_ad, psa_control, matched_market
statusstring — one of draft, active, measuring, completed, cancelled
treatment_dmasarray of stringDMA codes where ads are shown
control_dmasarray of stringDMA codes where ads are withheld
holdout_pctnumber
required_sample_sizeinteger, nullable
target_mdenumberMinimum detectable effect (10% = can detect a 10% lift)
target_powernumber
target_alphanumber
matching_covariatesobjectCovariates used for propensity score matching
caliper_widthnumber
started_atstring, nullable
ended_atstring, nullable

IncrementalityResultRecord

Daily measurement result for an incrementality experiment. Contains both Bayesian and frequentist statistical results.

Table: incrementality_results Migration: 096

PropertyTypeRequiredDescription
result_idinteger
experiment_idstring
measurement_datestring
treatment_devicesinteger
treatment_visitorsinteger
treatment_visit_ratenumber
control_devicesinteger
control_visitorsinteger
control_visit_ratenumber
absolute_liftnumber
relative_lift_pctnumber
incremental_visitsinteger
chi_squarenumber
p_valuenumber
confidence_interval_lowernumber
confidence_interval_uppernumber
posterior_prob_positivenumberBayesian P(lift > 0)
expected_upliftnumber
credible_interval_lowernumber
credible_interval_uppernumber
avg_propensity_treatmentnumber
avg_propensity_controlnumber
standardized_mean_diffnumberShould be < 0.1 for good covariate balance

POIReference

Advertiser/brand point-of-interest location from Intuizi. Used as the "destination" in footfall attribution — did an exposed device later visit one of these POIs?

The catalogue is scoped to the brands under active measurement, not to Intuizi's whole POI universe. A volume figure is deliberately not quoted here: this document is a schema contract, and the previous "~500K locations" parenthetical overstated the live table by more than an order of magnitude. Count poi_reference if you need the current size.

Table: poi_reference Migration: 092

PropertyTypeRequiredDescription
poi_idstring
external_idstringProvider's unique identifier
namestringLocation name (e.g., "Starbucks - 42nd & Broadway")
brandstring, nullableBrand name (e.g., "Starbucks")
categorystringBusiness category (e.g., "coffee_shop", "gym", "retail")
subcategorystring, nullable
addressstring, nullable
citystring, nullable
statestring, nullable
zipstring, nullable
dma_codestring, nullable
locationstringPostGIS GEOGRAPHY(POINT, 4326)
latitudenumber
longitudenumber
geohash_6string
providerstring
statusstring
metadataobjectFlexible JSON metadata

TouchpointEvent

Normalized cross-channel event from any attribution source. All channels (DOOH, mobile, web, app, store, purchase) are represented in a single table for unified multi-touch attribution.

Table: touchpoint_events (partitioned monthly by occurred_at) Migration: 096

PropertyTypeRequiredDescription
touchpoint_idstring
maid_hashstring, nullable
household_idstring, nullable
pixel_visitor_idstring, nullableSHA-256(IP+UA) for web pixel visitors
channelstring — one of dooh, mobile_ad, web_visit, app_install, store_visit, purchase
event_typestringEvent subtype (impression, click, pixel_fire, transaction, etc.)
play_idstring, nullableDOOH-specific — which ad play triggered this event
screen_mongo_idstring, nullable
campaign_idstring, nullable
creative_idstring, nullable
advertiser_idstring, nullable
confidence_scorenumber, nullable
dsp_partnerstring, nullableDSP name for mobile/digital events
placement_idstring, nullable
click_urlstring, nullable
poi_idstring, nullable
transaction_amount_centsinteger, nullable
basket_itemsarray of object
latitudenumber, nullable
longitudenumber, nullable
dma_codestring, nullable
occurred_atstringWhen the event happened (partition key)
attributedbooleanWhether this event has been assigned to an attribution chain
attribution_chain_idstring, nullable