01_create_tables.sql

-- Causal ROI Dashboard - BigQuery Schema
-- Run this after creating your dataset: bq mk --dataset ${PROJECT_ID}:causal_roi

-- ============================================================================
-- RAW EVENT TABLES (your logging layer writes here)
-- ============================================================================

CREATE TABLE IF NOT EXISTS `causal_roi.events_forecast` (
  path_id STRING NOT NULL,
  hypothesis_id STRING NOT NULL,
  t0 TIMESTAMP NOT NULL,
  features JSON,  -- flexible: {"channel": "email", "segment": "high_value", ...}
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP()
)
PARTITION BY DATE(t0)
CLUSTER BY path_id, hypothesis_id
OPTIONS(
  description="Forecasts/predictions issued by the system",
  labels=[("env", "prod"), ("domain", "attribution")]
);

CREATE TABLE IF NOT EXISTS `causal_roi.events_action` (
  path_id STRING NOT NULL,
  action_id STRING NOT NULL,
  channel STRING NOT NULL,
  spend FLOAT64 NOT NULL,
  t1 TIMESTAMP NOT NULL,
  metadata JSON,  -- e.g., {"campaign_id": "Q4_promo", "creative": "v2"}
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP()
)
PARTITION BY DATE(t1)
CLUSTER BY path_id, channel
OPTIONS(
  description="Actions taken (ad spend, emails sent, etc.)"
);

CREATE TABLE IF NOT EXISTS `causal_roi.events_outcome` (
  path_id STRING NOT NULL,
  kpi STRING NOT NULL,  -- "revenue", "conversions", "ltv"
  value FLOAT64 NOT NULL,
  t2 TIMESTAMP NOT NULL,
  cohort_id STRING,  -- "treatment" or "control_{geo}" or "control_{synthetic}"
  attribution_window_days INT64 DEFAULT 7,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP()
)
PARTITION BY DATE(t2)
CLUSTER BY path_id, cohort_id, kpi
OPTIONS(
  description="Observed outcomes with treatment/control labels"
);

CREATE TABLE IF NOT EXISTS `causal_roi.attribution_links` (
  path_id STRING NOT NULL,
  method STRING NOT NULL,  -- "shapley", "markov", "geo_holdout", "uplift_model"
  weight FLOAT64 NOT NULL,  -- normalized [0,1] probability score
  window_days INT64 DEFAULT 7,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP(),
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP()
)
PARTITION BY DATE(created_at)
CLUSTER BY path_id, method
OPTIONS(
  description="Attribution weights from different methods"
);

-- ============================================================================
-- AGGREGATED METRICS (written by daily job)
-- ============================================================================

CREATE TABLE IF NOT EXISTS `causal_roi.edge_metrics` (
  date DATE NOT NULL,
  edge_id STRING NOT NULL,
  path_id STRING NOT NULL,
  channel STRING,
  spend FLOAT64,
  rev_treatment FLOAT64,
  rev_control FLOAT64,
  delta_roi FLOAT64,  -- (rev_treat - rev_ctrl) / spend
  n_obs INT64,
  latency_ms INT64,  -- action→outcome lag
  std_error FLOAT64,  -- optional: for confidence intervals
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP()
)
PARTITION BY date
CLUSTER BY path_id, edge_id
OPTIONS(
  description="Daily edge-level causal metrics"
);

CREATE TABLE IF NOT EXISTS `causal_roi.path_metrics` (
  date DATE NOT NULL,
  path_id STRING NOT NULL,
  velocity_ms INT64,  -- median(t_action - t_forecast)
  p_causal FLOAT64,  -- from attribution_links (avg weight)
  pcr FLOAT64,  -- path confidence ratio
  accuracy FLOAT64,  -- if tracking hit/miss
  roi_delta FLOAT64,  -- avg edge delta_roi for this path
  n_actions INT64,
  n_outcomes INT64,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP()
)
PARTITION BY date
CLUSTER BY path_id
OPTIONS(
  description="Daily path-level rollup metrics"
);

-- ============================================================================
-- GRAPH STRUCTURE (for traversal/visualization)
-- ============================================================================

CREATE TABLE IF NOT EXISTS `causal_roi.nodes` (
  node_id STRING NOT NULL,
  node_type STRING NOT NULL,  -- "signal", "action", "outcome"
  label STRING,
  props JSON,  -- {"name": "email_open", "tier": "critical", ...}
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP()
)
CLUSTER BY node_id, node_type
OPTIONS(
  description="Knowledge graph nodes"
);

CREATE TABLE IF NOT EXISTS `causal_roi.edges` (
  edge_id STRING NOT NULL,
  src_node_id STRING NOT NULL,
  dst_node_id STRING NOT NULL,
  edge_type STRING NOT NULL,  -- "signal->action", "action->outcome"
  latest_delta_roi FLOAT64,
  latest_latency_ms INT64,
  decay_weight FLOAT64 DEFAULT 1.0,  -- exponential decay for self-learning
  n_observations INT64 DEFAULT 0,
  last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP(),
  props JSON
)
CLUSTER BY src_node_id, dst_node_id, edge_type
OPTIONS(
  description="Knowledge graph edges with learned weights"
);

-- ============================================================================
-- CONTROL/TREATMENT TRACKING (for experimentation)
-- ============================================================================

CREATE TABLE IF NOT EXISTS `causal_roi.control_cohorts` (
  cohort_id STRING NOT NULL,
  path_id STRING NOT NULL,
  method STRING NOT NULL,  -- "geo_holdout", "synthetic_control", "randomized"
  geo_region STRING,  -- if geo-based
  synthetic_weights JSON,  -- if synthetic control: {"region_A": 0.3, ...}
  quality_score FLOAT64,  -- match quality
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP()
)
PARTITION BY DATE(created_at)
CLUSTER BY path_id, cohort_id
OPTIONS(
  description="Control group definitions for causal inference"
);

-- ============================================================================
-- INDEXES (for fast queries)
-- ============================================================================

-- Note: BigQuery uses clustering instead of indexes
-- Already applied above via CLUSTER BY clauses
← All docsView source on GitHub →