Tutorial
This tutorial provides a deep dive into the Online Retail Simulator, walking through the three-phase data generation process, exploring the generated data, and demonstrating how to inject and measure treatment effects using the enrichment layer.
The simulator operates in three phases. First, the products phase generates a product catalog with a category and a price for each product. Second, the product_details phase enriches each product with content: a brand name, title, description, and feature list. Third, the metrics phase simulates sales transactions with realistic temporal patterns and conversion metrics. This tutorial uses rule-based generation, which is deterministic and interpretable. The simulator also supports synthesizer-based generation, which learns patterns from your own data. An enrichment layer then injects a known treatment effect into a subset of products, so you can check whether a causal method recovers the true effect size.
Simulate product data
We start by importing the Online Retail Simulator alongside a few standard libraries.
[1]:
# Standard Library
import inspect
# Third-party packages
from IPython.display import Code
from online_retail_simulator import enrich, simulate, load_job_results
# Local imports
from support import (
plot_revenue_by_category,
plot_daily_metrics_trend,
plot_conversion_funnel,
plot_treatment_effect,
display_product_details,
)
Let’s start by simulating 100 products. The configuration is defined using YAML and organizes the three-phase simulation process.
The STORAGE section specifies where to save output files.
The RULE section contains two subsections—PRODUCTS and METRICS—each defining a FUNCTION to call and a PARAMS subsection for function-specific parameters like num_products, date_start, date_end, and seed. The sale_prob parameter sets the daily probability that a product receives any shopper traffic at all, adjusted up or down by the product’s content quality.
The PRODUCT_DETAILS section specifies the FUNCTION used to enrich products with additional content.
[2]:
! cat "config_tutorial.yaml"
STORAGE:
PATH: output
RULE:
PRODUCTS:
FUNCTION: simulate_products_rule_based
PARAMS:
num_products: 100
seed: 42
METRICS:
FUNCTION: simulate_metrics_rule_based
PARAMS:
date_start: "2024-11-01"
date_end: "2024-11-30"
sale_prob: 0.7
seed: 42
PRODUCT_DETAILS:
FUNCTION: simulate_product_details_mock
To understand how the simulation functions work under the hood, you can explore their source code directly. Python’s inspect module lets you view the implementation right inside the notebook, or you can browse the source files on GitHub (products,
product_details, metrics) or the API documentation (products,
product_details, metrics).
[3]:
from online_retail_simulator.simulate.products_rule_based import simulate_products_rule_based
Code(inspect.getsource(simulate_products_rule_based), language="python")
[3]:
def simulate_products_rule_based(config: Dict) -> pd.DataFrame:
"""
Generate synthetic products (rule-based).
Args:
config: Complete configuration dictionary
Returns:
DataFrame of products
"""
params = config["RULE"]["PRODUCTS"]["PARAMS"]
num_products, seed = params["num_products"], params["seed"]
rng = np.random.default_rng(seed)
products: List[Dict] = []
for i in range(num_products):
category = rng.choice(_CATEGORIES)
price_min, price_max = _PRICE_RANGES[category]
price = round(rng.uniform(price_min, price_max), 2)
products.append(
{
"product_identifier": generate_random_product_identifier(rng),
"category": category,
"price": price,
}
)
return pd.DataFrame(products)
[4]:
from online_retail_simulator.simulate.product_details_mock import simulate_product_details_mock
Code(inspect.getsource(simulate_product_details_mock), language="python")
[4]:
def simulate_product_details_mock(
products_df: pd.DataFrame,
seed: int = None,
prompt_path: str = None,
treatment_mode: bool = False,
) -> pd.DataFrame:
"""Generate mock product details (rule-based).
Args:
products_df: Input products with product_identifier, category, price
seed: Random seed for reproducibility
prompt_path: Ignored for mock backend (accepted for API compatibility)
treatment_mode: If True, generate "improved" product details
Returns:
DataFrame with added title, description, brand, features
"""
# prompt_path is ignored for mock backend but accepted for API compatibility
_ = prompt_path
rng = np.random.default_rng(seed)
results = []
for product in products_df.to_dict("records"):
category = product.get("category", "General")
data = _get_mock_data(category, treatment_mode=treatment_mode)
# Preserve existing brand if present, otherwise generate new one
brand = product.get("brand") or str(rng.choice(data["brands"]))
adj = rng.choice(data["adjectives"])
num_features = min(4, len(data["features"]))
features = rng.choice(data["features"], size=num_features, replace=False).tolist()
title = f"{brand} {adj} {category} Item"
if treatment_mode:
description = f"Premium {category.lower()} product with exceptional quality. {features[0]}. {features[1]}."
else:
description = f"Quality {category.lower()} product for everyday use. {features[0]}. {features[1]}."
result = {
**product,
"title": title,
"description": description,
"brand": brand,
"features": features,
}
result["quality_score"] = calculate_quality_score(result)
results.append(result)
return pd.DataFrame(results)
Calling simulate("config_tutorial.yaml") triggers a three-phase data generation process. First, it reads and validates the YAML configuration, then runs the products phase to create a product catalog with the specified number of products, assigning each a unique identifier, category, and price. Second, it runs the product_details phase to enrich each product with brand names, titles, descriptions, and feature lists. Third, it runs the metrics phase, simulating a shopper
conversion funnel (impressions, visits, cart additions, orders, and revenue) for each product across the defined date range. The function writes the generated DataFrames to disk and returns a JobInfo object that tracks where the results are stored—which you then pass to load_job_results() to retrieve the data.
The products data represents the catalog of items available for sale. Each product starts with core attributes: a unique product_identifier, a category (such as Electronics, Clothing, or Books), and a price. The product_details phase then adds brand, title, description, and features, and scores the content with a quality_score between 0 and 1. These attributes influence the simulated sales behavior—for example, higher-priced items may have different
conversion patterns than low-cost products, and categories can exhibit distinct purchasing dynamics.
[5]:
job_info = simulate("config_tutorial.yaml")
results = load_job_results(job_info)
products = results["products"]
metrics = results["metrics"]
Products
[6]:
products.head()
[6]:
| product_identifier | category | price | title | description | brand | features | quality_score | |
|---|---|---|---|---|---|---|---|---|
| 0 | B1P4DZHDS9 | Electronics | 686.37 | TechPro Smart Electronics Item | Quality electronics product for everyday use. ... | TechPro | ['Bluetooth connectivity', 'Voice control', 'L... | 0.824 |
| 1 | B1SE4QSNG7 | Toys & Games | 80.75 | ValueBrand Essential Toys & Games Item | Quality toys & games product for everyday use.... | ValueBrand | ['Great value', 'Durable construction', 'Easy ... | 0.862 |
| 2 | BXTPQIDT5C | Food & Beverage | 42.02 | BestChoice Essential Food & Beverage Item | Quality food & beverage product for everyday u... | BestChoice | ['High quality materials', 'Durable constructi... | 0.928 |
| 3 | B3F1ZMC8Q6 | Food & Beverage | 33.42 | BestChoice Essential Food & Beverage Item | Quality food & beverage product for everyday u... | BestChoice | ['Durable construction', 'High quality materia... | 0.928 |
| 4 | B2NQRBTF0Y | Toys & Games | 27.52 | BestChoice Professional Toys & Games Item | Quality toys & games product for everyday use.... | BestChoice | ['Long lasting', 'High quality materials', 'Du... | 0.890 |
Let’s examine a single product to understand the structure better.
[7]:
example_product = products.iloc[0]
example_product
[7]:
product_identifier B1P4DZHDS9
category Electronics
price 686.37
title TechPro Smart Electronics Item
description Quality electronics product for everyday use. ...
brand TechPro
features ['Bluetooth connectivity', 'Voice control', 'L...
quality_score 0.824
Name: 0, dtype: object
This product has a unique product_identifier for tracking, belongs to the category Electronics with a price of $686.37, and includes enriched content: a brand name (TechPro), a descriptive title, a marketing-focused description, and a list of product features. Its quality_score of 0.824 summarizes how complete and informative that content is, and it nudges the product’s simulated conversion rates. These attributes combine catalog characteristics with content
details to create a realistic product listing.
Metrics
[8]:
metrics.sort_values(["product_identifier", "date"]).head()
[8]:
| product_identifier | category | price | title | description | brand | features | quality_score | date | impressions | visits | cart_adds | ordered_units | revenue | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 73 | B0D4QF30BG | Sports & Outdoors | 194.55 | PrimePick Quality Sports & Outdoors Item | Quality sports & outdoors product for everyday... | PrimePick | ['Long lasting', 'Great value', 'Easy to use',... | 0.863 | 2024-11-01 | 100 | 14 | 2 | 1 | 194.55 |
| 173 | B0D4QF30BG | Sports & Outdoors | 194.55 | PrimePick Quality Sports & Outdoors Item | Quality sports & outdoors product for everyday... | PrimePick | ['Long lasting', 'Great value', 'Easy to use',... | 0.863 | 2024-11-02 | 200 | 28 | 8 | 1 | 194.55 |
| 273 | B0D4QF30BG | Sports & Outdoors | 194.55 | PrimePick Quality Sports & Outdoors Item | Quality sports & outdoors product for everyday... | PrimePick | ['Long lasting', 'Great value', 'Easy to use',... | 0.863 | 2024-11-03 | 50 | 7 | 1 | 0 | 0.00 |
| 373 | B0D4QF30BG | Sports & Outdoors | 194.55 | PrimePick Quality Sports & Outdoors Item | Quality sports & outdoors product for everyday... | PrimePick | ['Long lasting', 'Great value', 'Easy to use',... | 0.863 | 2024-11-04 | 0 | 0 | 0 | 0 | 0.00 |
| 473 | B0D4QF30BG | Sports & Outdoors | 194.55 | PrimePick Quality Sports & Outdoors Item | Quality sports & outdoors product for everyday... | PrimePick | ['Long lasting', 'Great value', 'Easy to use',... | 0.863 | 2024-11-05 | 0 | 0 | 0 | 0 | 0.00 |
Let’s examine a single metric record to understand the conversion funnel structure.
[9]:
example_metric = metrics.iloc[0]
example_metric
[9]:
product_identifier B1P4DZHDS9
category Electronics
price 686.37
title TechPro Smart Electronics Item
description Quality electronics product for everyday use. ...
brand TechPro
features ['Bluetooth connectivity', 'Voice control', 'L...
quality_score 0.824
date 2024-11-01
impressions 25
visits 4
cart_adds 1
ordered_units 0
revenue 0.0
Name: 0, dtype: object
This metric record tracks the shopper journey as a conversion funnel for one product on one date. It repeats the product’s catalog and content columns, adds the date, then follows shoppers through each stage: impressions (product shown), visits (clicked to view), cart_adds (added to cart), ordered_units (purchased), and revenue (total sales). This funnel structure captures how shoppers drop off at each stage of the purchase process.
Exploration
Now that we understand the structure of both DataFrames, let’s explore the generated data through visualizations and summary statistics.
Things to explore
What is the date range?
How many categories?
What is the total revenue?
How is revenue distributed across product categories?
[10]:
category_revenue = metrics.groupby("category")["revenue"].sum().sort_values()
plot_revenue_by_category(category_revenue)
[11]:
Code(inspect.getsource(plot_revenue_by_category), language="python")
[11]:
def plot_revenue_by_category(category_revenue):
"""
Plot horizontal bar chart of revenue by category.
Parameters
----------
category_revenue : pandas.Series
Series with category names as index and revenue values.
"""
fig, ax = plt.subplots(figsize=(10, 6))
category_revenue.plot(kind="barh", ax=ax, color=sns.color_palette("viridis", len(category_revenue)))
ax.set_xlabel("Revenue ($)")
ax.set_ylabel("Category")
ax.set_title("Total Revenue by Category")
ax.xaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f"${x:,.0f}"))
plt.tight_layout()
plt.show()
How do metrics vary over time?
[12]:
daily_metrics = metrics.groupby("date").agg({"ordered_units": "sum", "revenue": "sum"}).reset_index()
plot_daily_metrics_trend(daily_metrics)
How do shoppers move through the purchase journey?
[13]:
plot_conversion_funnel(metrics)
Things to explore
What fraction of impressions become visits?
What fraction of visits add to cart?
What fraction of cart adds become orders?
What is the overall conversion rate?
Enrich product data
Real-world e-commerce teams constantly face a critical question: Does improving product content quality increase sales? To help you explore this with known ground truth, the simulator lets you inject treatment effects into a subset of products starting at a specific date, creating synthetic experiments where you know the true causal impact.
The enrichment process operates through two coordinated mechanisms:
Content Enhancement: Generate upgraded product copy with more compelling titles, descriptions, and feature highlights, while keeping each product’s brand, category, and price
Sales Impact: Apply the corresponding sales boost that this improved content would realistically produce
The IMPACT section of the configuration controls the treatment parameters. The effect_size sets the increase in ordered units (0.5 = 50% increase), enrichment_fraction determines what share of products receive treatment (0.3 = 30%), and enrichment_start sets when the intervention begins. The ramp_days parameter models a gradual rollout where the effect increases linearly over the specified number of days, mimicking real-world deployment patterns. We set it to 0 here, so the
full effect applies from the first day.
[14]:
! cat "config_tutorial_enrichment.yaml"
IMPACT:
FUNCTION: product_detail_boost
PARAMS:
effect_size: 0.5
ramp_days: 0
enrichment_fraction: 0.3
enrichment_start: "2024-11-15"
seed: 42
backend: "mock"
[15]:
from online_retail_simulator.enrich.enrichment_library import product_detail_boost
Code(inspect.getsource(product_detail_boost), language="python")
[15]:
def product_detail_boost(metrics: list, **kwargs) -> tuple:
"""
Product detail regeneration and metrics boost for enrichment experiments.
Selects a fraction of products for treatment, regenerates their product
details (title, description, features) while preserving brand/category/price,
and applies metrics boost effect.
Args:
metrics: List of metric record dictionaries
**kwargs: Parameters including:
- job_info: JobInfo for saving product artifacts (required for saving)
- products: List of product dictionaries (required for product details)
- effect_size: Percentage increase in ordered units (default: 0.5)
- ramp_days: Number of days for ramp-up period (default: 7)
- enrichment_fraction: Fraction of products to enrich (default: 0.3)
- enrichment_start: Start date of enrichment (default: "2024-11-15")
- seed: Random seed for product selection (default: 42)
- prompt_path: Path to custom prompt template file (optional)
- backend: Backend to use for regeneration ("mock" or "ollama", default: "mock")
Returns:
Tuple of (treated_metrics, potential_outcomes_df):
- treated_metrics: List of modified metric dictionaries with treatment applied
- potential_outcomes_df: DataFrame with Y0_revenue and Y1_revenue for all products
"""
job_info = kwargs.get("job_info")
products = kwargs.get("products")
effect_size = kwargs.get("effect_size", 0.5)
ramp_days = kwargs.get("ramp_days", 7)
enrichment_fraction = kwargs.get("enrichment_fraction", 0.3)
enrichment_start = kwargs.get("enrichment_start", "2024-11-15")
seed = kwargs.get("seed", 42)
prompt_path = kwargs.get("prompt_path")
backend = kwargs.get("backend", "mock")
quality_boost = kwargs.get("quality_boost", 0.0)
rng = np.random.default_rng(seed)
# 1. Save original product details
if job_info and products:
job_info.save_df("product_details_original", pd.DataFrame(products))
# 2. Select treatment products
if products:
unique_product_ids = sorted(set(p.get("product_identifier", p.get("product_id")) for p in products))
else:
unique_product_ids = sorted(set(record["product_id"] for record in metrics))
n_treatment = int(len(unique_product_ids) * enrichment_fraction)
treatment_ids = set(rng.choice(unique_product_ids, size=n_treatment, replace=False))
# 3. Regenerate product details for treatment products
if products and job_info:
updated_products = _regenerate_product_details(
products, treatment_ids, prompt_path, backend, seed, quality_boost
)
job_info.save_df("product_details_enriched", pd.DataFrame(updated_products))
# 4. Apply metrics boost effect and calculate potential outcomes
treated_metrics = []
potential_outcomes = {} # {(product_id, date): {'Y0_revenue': x, 'Y1_revenue': y}}
start_date = datetime.strptime(enrichment_start, "%Y-%m-%d")
for record in metrics:
record_copy = copy.deepcopy(record)
product_id = record_copy.get("product_id", record_copy.get("product_identifier"))
record_date_str = record_copy["date"]
record_date = datetime.strptime(record_date_str, "%Y-%m-%d")
is_enriched = product_id in treatment_ids
record_copy["enriched"] = is_enriched
# Calculate Y(0) - baseline revenue (no treatment)
y0_revenue = record_copy["revenue"]
# Calculate Y(1) - revenue if treated (for ALL products, with ramp-up)
unit_price = record_copy.get("unit_price", record_copy.get("price"))
if record_date >= start_date:
days_since_start = (record_date - start_date).days
ramp_factor = 1.0 if ramp_days <= 0 else min(1.0, days_since_start / ramp_days)
adjusted_effect = effect_size * ramp_factor
original_quantity = record_copy["ordered_units"]
boosted_quantity = _boost_units(original_quantity, adjusted_effect, rng)
y1_revenue = round(boosted_quantity * unit_price, 2)
else:
# Before treatment start, Y(1) = Y(0)
y1_revenue = y0_revenue
# Store potential outcomes for ALL products
key = (product_id, record_date_str)
potential_outcomes[key] = {"Y0_revenue": y0_revenue, "Y1_revenue": y1_revenue}
# Apply factual outcome (only for treated products)
if is_enriched and record_date >= start_date:
_apply_boost(record_copy, boosted_quantity, y1_revenue)
treated_metrics.append(record_copy)
# Build potential outcomes DataFrame
potential_outcomes_df = pd.DataFrame(
[
{
"product_identifier": pid,
"date": d,
"Y0_revenue": v["Y0_revenue"],
"Y1_revenue": v["Y1_revenue"],
}
for (pid, d), v in potential_outcomes.items()
]
)
return treated_metrics, potential_outcomes_df
Calling enrich("config_tutorial_enrichment.yaml", job_info) applies the treatment effect to the simulated data. The product_detail_boost() function randomly selects 30% of products for treatment and performs two operations. First, it regenerates the product details of the treated products using treatment_mode=True, creating enhanced titles, descriptions, and feature highlights. Second, it increases their ordered units by 50% from November 15th onward and recomputes revenue. The
function saves four artifacts: the original product details, the enhanced product details, the enriched sales data, and the potential outcomes for every product. We load and examine each of them to understand both the content improvements and their impact on sales.
[16]:
enriched_job = enrich("config_tutorial_enrichment.yaml", job_info)
results = load_job_results(enriched_job)
Changes
[17]:
enriched = results["enriched"]
enriched.head()
[17]:
| product_identifier | category | price | title | description | brand | features | quality_score | date | impressions | visits | cart_adds | ordered_units | revenue | enriched | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | B1P4DZHDS9 | Electronics | 686.37 | TechPro Smart Electronics Item | Quality electronics product for everyday use. ... | TechPro | ['Bluetooth connectivity', 'Voice control', 'L... | 0.824 | 2024-11-01 | 25 | 4 | 1 | 0 | 0.00 | False |
| 1 | B1SE4QSNG7 | Toys & Games | 80.75 | ValueBrand Essential Toys & Games Item | Quality toys & games product for everyday use.... | ValueBrand | ['Great value', 'Durable construction', 'Easy ... | 0.862 | 2024-11-01 | 200 | 33 | 9 | 1 | 80.75 | True |
| 2 | BXTPQIDT5C | Food & Beverage | 42.02 | BestChoice Essential Food & Beverage Item | Quality food & beverage product for everyday u... | BestChoice | ['High quality materials', 'Durable constructi... | 0.928 | 2024-11-01 | 10 | 1 | 0 | 0 | 0.00 | False |
| 3 | B3F1ZMC8Q6 | Food & Beverage | 33.42 | BestChoice Essential Food & Beverage Item | Quality food & beverage product for everyday u... | BestChoice | ['Durable construction', 'High quality materia... | 0.928 | 2024-11-01 | 0 | 0 | 0 | 0 | 0.00 | False |
| 4 | B2NQRBTF0Y | Toys & Games | 27.52 | BestChoice Professional Toys & Games Item | Quality toys & games product for everyday use.... | BestChoice | ['Long lasting', 'High quality materials', 'Du... | 0.890 | 2024-11-01 | 10 | 1 | 0 | 0 | 0.00 | False |
The enriched dataset contains the same metrics as before (impressions, visits, cart_adds, ordered_units, revenue) but now includes an enriched column indicating which products received the treatment. Products with enriched=True have enhanced content and boosted sales metrics starting from the treatment date.
[18]:
example_enriched = enriched.iloc[0]
example_enriched
[18]:
product_identifier B1P4DZHDS9
category Electronics
price 686.37
title TechPro Smart Electronics Item
description Quality electronics product for everyday use. ...
brand TechPro
features ['Bluetooth connectivity', 'Voice control', 'L...
quality_score 0.824
date 2024-11-01
impressions 25
visits 4
cart_adds 1
ordered_units 0
revenue 0.0
enriched False
Name: 0, dtype: object
This record shows all the sales funnel metrics plus the enriched indicator. The treatment assignment is random but deterministic (controlled by the seed parameter), so the 70% of products that are not enriched form a control group in a reproducible experiment where we know the ground truth effect size.
The enrichment process modifies both sales metrics and product content. Let’s pick one treated product and compare its content before and after enrichment.
[19]:
original_details = results["product_details_original"].set_index("product_identifier")
enriched_details = results["product_details_enriched"].set_index("product_identifier")
treated_id = enriched_details.index[enriched_details["enriched"]][0]
display_product_details(original_details.loc[treated_id], "BEFORE ENRICHMENT (Original Content)")
======================================================================
BEFORE ENRICHMENT (Original Content)
======================================================================
Brand: ValueBrand
Title: ValueBrand Essential Toys & Games Item
Description: Quality toys & games product for everyday use. Great value. Durable construction.
Features: ['Great value', 'Durable construction', 'Easy to use', 'Long lasting']
[20]:
display_product_details(enriched_details.loc[treated_id], "AFTER ENRICHMENT (Enhanced Content)", add_newline=True)
======================================================================
AFTER ENRICHMENT (Enhanced Content)
======================================================================
Brand: ValueBrand
Title: ValueBrand Award-Winning Toys & Games Item
Description: Premium toys & games product with exceptional quality. Exceptional value. Intuitive design.
Features: ['Exceptional value', 'Intuitive design', 'Enhanced durability', 'Extended lifespan']
Things to explore
How do the titles differ between control and treatment?
What makes the treatment description more compelling?
How are the features enhanced in the treatment version?
Impact
The treatment effect visualization compares daily revenue between the original and enriched datasets. Before the treatment start date, the lines overlap perfectly. After November 15th, the enriched dataset shows higher revenue, because the treated 30% of products now sell 50% more units.
[21]:
plot_treatment_effect(metrics, enriched, "2024-11-15")
Ground truth
Because we simulated the data, we know what would have happened to every product both with and without the treatment. The potential_outcomes table records, for each product and date, the revenue without enrichment (Y0_revenue) and with enrichment (Y1_revenue). Real data only ever reveals one of the two for each product, which is the fundamental problem of causal inference.
[22]:
potential_outcomes = results["potential_outcomes"]
potential_outcomes.head()
[22]:
| product_identifier | date | Y0_revenue | Y1_revenue | |
|---|---|---|---|---|
| 0 | B1P4DZHDS9 | 2024-11-01 | 0.00 | 0.00 |
| 1 | B1SE4QSNG7 | 2024-11-01 | 80.75 | 80.75 |
| 2 | BXTPQIDT5C | 2024-11-01 | 0.00 | 0.00 |
| 3 | B3F1ZMC8Q6 | 2024-11-01 | 0.00 | 0.00 |
| 4 | B2NQRBTF0Y | 2024-11-01 | 0.00 | 0.00 |
Comparing the two columns for the treated products after the start date gives the true effect of the treatment on the treated. It is close to the configured 50%, but not exactly 50%: units are whole numbers, so each boosted value is rounded up or down at random, and the realized effect carries some sampling noise just like a real experiment.
[23]:
treated_ids = enriched.loc[enriched["enriched"], "product_identifier"].unique()
post_treated = potential_outcomes[
potential_outcomes["product_identifier"].isin(treated_ids) & (potential_outcomes["date"] >= "2024-11-15")
]
true_effect = post_treated["Y1_revenue"].sum() / post_treated["Y0_revenue"].sum() - 1
print(f"True revenue effect on treated products: {true_effect:+.1%}")
True revenue effect on treated products: +45.1%
Things to explore
How does the true effect change if you set a different seed?
What happens to the daily revenue plot if you set ramp_days to 7?
Can you recover the true effect using only the enriched data, without looking at the potential outcomes?