Compare commits

..

121 Commits

Author SHA1 Message Date
13959e4b28 chore: bug fixes 2026-01-31 10:13:07 +01:00
Daniel Alves Rösel
2f481bd94b Merge branch 'agent-behavior-loader-developemen' into feat-strong-learning-implementation-with-data-contamination 2026-01-31 10:08:59 +01:00
72877439ca feat: contaminator and training 2026-01-31 09:48:20 +01:00
0f5f8affab chore: make lib backwards compatible 2026-01-31 09:48:20 +01:00
ee70f02a1f chore: export repeated methods into lib 2026-01-31 09:48:20 +01:00
22a2c255bd chore: remove boilerplate 2026-01-31 09:48:20 +01:00
ccc19f3493 acapting some architectures 2026-01-31 09:48:20 +01:00
00e3eff2fa migrating weak learning 2026-01-31 09:48:20 +01:00
440371dba4 feat: initial feature engineering of trajectories 2026-01-31 09:48:20 +01:00
b05b510f70 strong dataset gathering 2026-01-31 09:48:20 +01:00
04907df393 feat: weak train scaffold 2026-01-31 09:48:20 +01:00
b2f0746c01 chore: extra commenting 2026-01-31 09:48:20 +01:00
7b2d80ac4c feat: wip contaminator 2026-01-31 09:48:20 +01:00
0ce12fbc3b chore: ignores 2026-01-31 09:48:17 +01:00
e9cf5f0736 refactor models computations 2026-01-31 09:46:44 +01:00
82b54428b7 chore: refactor the loader class 2026-01-31 09:46:44 +01:00
87a35fad2c feat: joint loader 2026-01-31 09:46:44 +01:00
af23d2f736 feat: introduction of agentinc MDPs and KL divergence of > 2 2026-01-31 09:46:44 +01:00
9cb2b0fc44 feat: forgot airflow helper staging 2026-01-31 09:46:44 +01:00
7c330a19c6 feat: added a runner script for agent orchestration 2026-01-31 09:46:44 +01:00
Daniel Alves Rösel
eb95060380 Pre run web refactors (#43)
* chore: refactor date utilities

* feat: improve images of hotel rooms

* fix: adding date utils
2026-01-31 09:46:44 +01:00
61dd621532 chore: styling and title updates 2026-01-31 09:46:44 +01:00
4c368d48f2 chore: fixing visual bugs in cart 2026-01-31 09:46:44 +01:00
3c141a4b6c chore: better test consistency before agnet 2026-01-31 09:46:44 +01:00
e89cb263d4 planning 2026-01-31 09:46:44 +01:00
62a4008c29 feat: integration of pipeline hooks into testing 2026-01-31 09:46:44 +01:00
8b429b7a8e chore: refactor to better map end to end 2026-01-31 09:46:44 +01:00
f9bf3de71e pdf rendering 2026-01-31 09:46:44 +01:00
131323ef56 featuer: dot exporter 2026-01-31 09:46:44 +01:00
ec4cf074e6 feature: MDP behavior mappers (unlinked) 2026-01-31 09:46:44 +01:00
6a06a8af4a simple code cleanup 2026-01-31 09:46:44 +01:00
3fa98f375d refactor to align moer with research in the env sims 2026-01-31 09:46:44 +01:00
201c98bcac improved implementation 2026-01-31 09:46:44 +01:00
8a08458478 formlating the reward simply 2026-01-31 09:46:44 +01:00
7d09232e48 high level defintion 2026-01-31 09:46:44 +01:00
20132c084c initial environemnt definitions 2026-01-31 09:46:41 +01:00
26abff5864 chore: fixing tests with seed determinism 2026-01-30 13:57:40 +01:00
4c7d9362af chore: envs for e2e 2026-01-30 13:55:22 +01:00
ea45801845 chore: removing the lab byproduct 2026-01-30 13:22:22 +01:00
Daniel Alves Rösel
574e05d9e0 Merge pull request #50 from velocitatem/new-simulation-environment-development
New simulation environment development
2026-01-30 13:19:53 +01:00
52fe865598 feature: drafting studies directory 2026-01-30 13:18:20 +01:00
28d3f6853e chore: refactor wrapper 2026-01-30 13:17:12 +01:00
10e8397eec chore: bette rplotting 2026-01-29 13:11:52 +01:00
772772b5b9 chore: better wrapping amd more performant 2026-01-29 10:01:53 +01:00
6e06081d60 porting to better 2026-01-28 16:09:28 +01:00
83d9bb2552 chore: properly developing 2026-01-28 14:04:57 +01:00
fa2aca8b13 chore: rough migration of environment configuration 2026-01-26 14:12:41 +01:00
cd6c3d6006 chore: migrating thesis case definition 2026-01-26 13:19:55 +01:00
98a9a3738c fix: coi better defined and aligned and sac improved 2026-01-25 10:36:37 +01:00
1224841a82 preliminary improved runs 2026-01-24 23:51:57 +01:00
4033e73ba1 feat: consistent failure case 2026-01-24 15:16:41 +01:00
bae51daa1c chore: refactor session mapping 2026-01-24 14:21:35 +01:00
c5eae17924 simple baselines and training setup to be refactored 2026-01-24 13:20:42 +01:00
28669ea4c3 win: refomulated and re-inspired from library 2026-01-23 17:16:32 +01:00
b0a1647956 docs 2026-01-23 12:52:58 +01:00
19bb4fd517 chore; ignoreing build of docs 2026-01-23 10:37:48 +01:00
4e2e41d943 shock: defining new lab environment and formulation 2026-01-23 10:37:32 +01:00
a033e77697 intorducing jax for computation 2026-01-22 21:02:10 +01:00
40e0b201e6 chore: init code for jax core 2026-01-22 13:10:15 +01:00
a217d53556 feat: translating features to jax 2026-01-22 13:10:01 +01:00
a6e6cc5d60 feat: baseline setup for RL modeling 2026-01-22 12:52:41 +01:00
fa89347c4e feat: expanding market observation space 2026-01-22 11:48:24 +01:00
2b3d937be6 feat: fixing alignment w premiums and specific extraction of data 2026-01-22 11:46:32 +01:00
20c47fe85f review: planning environment refactoring 2026-01-22 11:40:47 +01:00
b7161573d7 chore: mini docs 2026-01-22 11:40:27 +01:00
c15bb1882e chore: training and data refactors 2026-01-22 11:40:12 +01:00
dee6f573e3 feat: contaminator and training 2026-01-21 19:12:56 +01:00
2ed200f870 chore: make lib backwards compatible 2026-01-21 19:12:35 +01:00
56308ecb10 chore: export repeated methods into lib 2026-01-21 19:12:11 +01:00
7fcd18c3cb chore: remove boilerplate 2026-01-21 19:11:54 +01:00
5f607a58eb acapting some architectures 2026-01-21 18:22:39 +01:00
6aad196234 migrating weak learning 2026-01-21 18:22:31 +01:00
e5060babfa feat: initial feature engineering of trajectories 2026-01-21 14:05:39 +01:00
80863e9b17 strong dataset gathering 2026-01-21 14:05:30 +01:00
a5029f2eab feat: weak train scaffold 2026-01-21 11:27:03 +01:00
c102ac482e chore: extra commenting 2026-01-21 11:11:49 +01:00
08ade8dc89 feat: wip contaminator 2026-01-20 21:00:47 +01:00
95d4f0cee2 chore: ignores 2026-01-13 19:50:36 +01:00
Daniel Alves Rösel
a9d73ccce5 Paper first fillout (#39)
* initial environemnt definitions

* high level defintion

* formlating the reward simply

* improved implementation

* tailored docker compose image for secondary tenaordboard

* preliminary desriptions and babble

* details on formulation and defintion of agent and its loop

* typos one

* more grammar issues

* fluidity improvements and refactors

* more decluttering and dnoising

* finalizing introduction review

* some methodology

* somehow this disappeared

* bit more of this and that

* methodology of how we do architectuer and online DP

* fix: compilation

* expanding on the taxonomy and economic references

* authoer notes

* acks + google GCP

* making space w new format nada lit review

* stronger lit review and more sources

* forgot about tables and graphs

* dedupe citations

* adding cloudflare

* fixing env vars

* updating docs with url

* upating embed

* fixing the url

* paper badge

* formaliztaion of rewards and adding definitions

* noisy formulations

* connecting some more dots here

* adding significant weight in prices

* fixing error

* fixing typos and consistency

* extra math formulations and refferenceot DRO

* fixing diagram of loops

* github mindmap

* fixing erro and thiknig about big picture

* enhancing the website

* goals methodology and gitignore

* some more references and theory links

* talking about some wtp

* feature: added wordcounter

* forcing latex builds and fixining the bib #

* refactor: update Cost of Information equations and notation for clarity

* some more math and refactors

* refactor: unify notation and improve clarity in COI equations

* refactor: generalize master function for demand estimation and pricing strategies

* we dont like math but we have to do it :(

* refactor: enhance Cost of Information framework with additional context and illustration

* refactor: enhance literature review and methodology sections with economic theory insights and system architecture details

* alining format to fit the rubric

* refactoring bibliography

* fix: align

* mdp additionally

* trying different title

* adding balance figure

* agentic givergence, finally

* fix: figure fonts adjusted to match
2026-01-13 17:07:29 +01:00
3072e5f46e refactor models computations 2026-01-13 16:51:00 +01:00
a1e3166322 chore: refactor the loader class 2026-01-13 16:46:17 +01:00
6f361b96a8 feat: joint loader 2026-01-13 16:42:50 +01:00
eea019ab3f feat: introduction of agentinc MDPs and KL divergence of > 2 2026-01-13 15:57:05 +01:00
a36973cb42 feat: forgot airflow helper staging 2026-01-13 15:37:06 +01:00
96180e9af1 feat: added a runner script for agent orchestration 2026-01-13 15:36:20 +01:00
Daniel Alves Rösel
e60c0c64e1 Pre run web refactors (#43)
* chore: refactor date utilities

* feat: improve images of hotel rooms

* fix: adding date utils
2026-01-13 15:35:27 +01:00
90f57cb9b9 chore: styling and title updates 2026-01-13 15:09:52 +01:00
d865357695 chore: fixing visual bugs in cart 2026-01-13 15:05:33 +01:00
961302a21a chore: better test consistency before agnet 2026-01-12 22:33:47 +01:00
0d214a469f planning 2026-01-12 20:59:09 +01:00
acf731efcb feat: integration of pipeline hooks into testing 2026-01-12 13:37:48 +01:00
9a8525a854 chore: refactor to better map end to end 2026-01-12 11:02:48 +01:00
29f51d56d1 pdf rendering 2026-01-12 11:02:48 +01:00
c56c7f6537 featuer: dot exporter 2026-01-12 11:02:48 +01:00
b1882b6049 feature: MDP behavior mappers (unlinked) 2026-01-12 11:02:48 +01:00
57a7e0c571 simple code cleanup 2026-01-12 11:02:48 +01:00
c8c44d0453 refactor to align moer with research in the env sims 2026-01-12 11:02:48 +01:00
f950565264 tailored docker compose image for secondary tenaordboard 2026-01-12 11:02:48 +01:00
aae124f5ea improved implementation 2026-01-12 11:02:48 +01:00
c5caee21b1 formlating the reward simply 2026-01-12 11:02:48 +01:00
fe7dafed0a high level defintion 2026-01-12 11:02:48 +01:00
fa65fe992d initial environemnt definitions 2026-01-12 11:02:48 +01:00
Daniel Alves Rösel
221e71a503 E2e testing of pricing (#42)
* a simp0le scaffold

* feature: simple npm setup

* feature: testing setup and dummy scenarios

* chore: dumping kafak just via backend

* chore: dcleaning gitignore

* features: boilerplate fixtures and stuff

* test: extra tests

* chore: update the test suite to be callable via makefile

* chore: cleaning

* chore: updating interactions setup

* small cleaning

* chore: cleaning shitty code
2026-01-12 11:02:18 +01:00
Daniel Alves Rösel
f2271e368e 34 initial discriminator of interaction data (#38)
* feat: training pipeline + tensorboard

* tesnorboard forgot

* chore: ml basic boilerplate

* feat: naive architecture as start

* eval setup

* chore: parquet exporting of data

* chore: updating requirements necesary

* feat: separating modules and adding training logs paths

* Update experiments/ml/train.py

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* fix: new path for runs

* fix: undoing ai slop code

* chore: modules and reqs

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-12-14 18:58:42 +01:00
Daniel Alves Rösel
a1916c966c 32 refine data pipeline training data construction (#37)
* feature: modularized feature engineering for ml setup (new pipeline)

* chore: updating imports properly

* test: updating fixtures with ua and meta

* chore: migrating code ignore groups

* chore: syntax cleaning and code quality

* chore: fixing pipeline data compatability

* Update experiments/procesing/steps/session.py

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* chore: refactoring and dixing path joining

* chore: refactoring function definition to avoid reinit

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-12-12 12:15:15 +01:00
Daniel Alves Rösel
a2a443c027 Update README with project badges and links
Added badges for build status, TPU support, and Vercel deployments.
2025-12-12 10:21:11 +01:00
Daniel Alves Rösel
ef98141ca8 Catchup airline (#31)
* chore: update provider and pricing snitch with agnostic system

* cloning pipelines per mode instance

* updating airline hero section

* fix: must keep airflow secretkey

* fix: fixture update to hotel not shop

* chore: refactored to factory design pattern of pipelines

* chore: clean up definition of composite class of providers
2025-12-11 21:56:12 +01:00
d45b344264 fixing public routing for store modes 2025-12-08 15:00:37 +01:00
a0b956b242 chore: rewriting airflow for railway 2025-12-06 18:04:18 +01:00
Daniel Alves Rösel
8751583764 Improving interface after experiment01 (#30)
* fix: fixes of backwords

* fixing hotel information with image placeholders

* chore: clean up product display in hotel and cleaner interfacing

* adding loader with historical data loading

* feature: cleaning up pipeline

* chore: simple surge pricer

* created new pricing pipeline

* adding a checkout page to both sites

* fix: fixing stale pacakge

* test: we wont be using elasticity anymore so its okay

* chore: cleaning elasticity references

* chore: store sting

* feature: e2e intro pipline surge pricing

* fix: CVE vulnerability patching
2025-12-06 17:47:14 +01:00
59d4fb7891 fix: unified provider container for standalone 2025-12-04 17:03:39 +01:00
7c2a819122 removing module provider summoning for provider 2025-12-04 16:19:26 +01:00
5941ffd085 small provider updates 2025-12-04 16:07:18 +01:00
955102090d feat: introduced cumulative features step for state definition 2025-11-29 22:28:40 +01:00
d654bbf4b4 static price reading 2025-11-29 20:13:38 +01:00
Daniel Alves Rösel
ad9423bf59 Airflow addition (#28)
* introducing airflow to run pipeline

* chore: updating dag with upload to registry

* introducing complete provider (non refactored and noisy)

* chore: removing old shit

* generic pricing baselines

* feature: super simple model registry (to be updated maybe third party OS software)

* chore: refactoring the providers docker config and requirements

* chore: refactored and broke down components (braking

* exporting all

* local pipeline excution working

* fix: fixing import structures from nonrelativistic

* chore: enables cross comm pickling with fully e2e pipeline compilation

* docs: what the pipeline is like now

* pipelines local running and pipeline high level definition

* cleaning old pipeline and vectorization

* leaked but fixing, not so important

* test: started with pipeline step testing

* chore: cleaning up provider of prices

* test: extra tests wit hsemantic meaning checks

* migrating pricers

* feature: introducing pricing predictors (pricers)

* chore: e2e is done with new pipeline

* extra session feature extraction

* feature: experiemntal sessin pricer and metrics(vibe)

* chore: redefined and connected pricers (#29)
2025-11-29 17:50:16 +01:00
Daniel Alves Rösel
2a0e44ab24 Add image and update links in README.md 2025-11-29 14:19:22 +01:00
Daniel Alves Rösel
c432c45343 First pricing implementation (#27)
* first implementation of elasticity demand computation

* chor: fixing test :(

* feature: rudemantary defintition of pricing pipeline

* chor: fixing cross product missing data

* add warning

* feature: e2e pricing pipeline with inference
2025-11-27 18:25:27 +01:00
Daniel Alves Rösel
8b76d24ade 6 catalog data and mode mappers (#25)
* supabase product proxy and rendering

* minor pipeline refactor

* refactoring and demand estimation

* trackion of date index searching

* fixing changes of imports

* data seeding

* chore: airline basic refactor

* feat: huge push of product changes and item review with cart

* refactored design

* chore: moving route elsewhere and align

* fix: build of web/

* chore: fixing paper build

* fixing chars
2025-11-25 11:00:31 +01:00
Daniel Alves Rösel
894ce87a5d introduced supabase and experiment management UI (#23)
* introduced supabase and experiment management UI

* fixing cookie import
2025-11-18 20:45:11 +01:00
Daniel Alves Rösel
ab8b8787a8 13 agentic behavior runner v1 (#14)
* baseline setup of agent abstract

* feat: new implementation of simple AI agent that can follow a goal and return

* refactored import structure and created full tests

* pytest setup a github workflow to run tests + more ignores

* singularity for pushing

* fixing builds of PDFs

* inital structure of docs

* init styles and docs

* basic style implementation

* 13 create outline for research paper draft (#18)

* updated outline for paper from issue

* extra paper sections and some formalization of series data

* algorithms and acknowledgements

* updated outline for paper from issue

* Refactor docker-compose services to use individual Dockerfiles (#20)

* Initial plan

* Refactor services into individual Dockerfiles

Co-authored-by: velocitatem <60182044+velocitatem@users.noreply.github.com>

* Add EXPOSE directives to all Dockerfiles with port documentation

Co-authored-by: velocitatem <60182044+velocitatem@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: velocitatem <60182044+velocitatem@users.noreply.github.com>

* 2 nextjs scaffold with store mode shop and admin session experiment wiring event emission v1 (#17)

* chore: cleaning gitignore

* formating and env documentation

* feat: context switching of hotel/airline depndent on env var via middleware

* fixed alignment and building

* wrong file

* prods

* fixed applying style

* better session cookie management

* tentative session storage with maybe using airtable

* migrated api of ingestion

* events and products apge

* fixing build

* 13 create outline for research paper draft (#18)

* updated outline for paper from issue

* extra paper sections and some formalization of series data

* algorithms and acknowledgements

* updated outline for paper from issue

* upadted text formating

* event unification

* refactor tracking to ues callbacks instead of refs

* implement a pricing display api with session passing

* moved middleware to proxy according to new changes in Nextjs

* refactoed kafka ingestion to go via backend not web-db

* Refactor docker-compose services to use individual Dockerfiles (#20)

* Initial plan

* Refactor services into individual Dockerfiles

Co-authored-by: velocitatem <60182044+velocitatem@users.noreply.github.com>

* Add EXPOSE directives to all Dockerfiles with port documentation

Co-authored-by: velocitatem <60182044+velocitatem@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: velocitatem <60182044+velocitatem@users.noreply.github.com>

* fixing small bugs and adding exepriments to tracking

* added some doc

* fixing prod

* prod kafka server logging

* topic auto create

* pytest setup a github workflow to run tests + more ignores

* getting data from agents properly

* proper pipeline to handle data and build matrices

* fixing backend dumping

* fixing agents and ignore

* fixing import for tests

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
2025-11-15 16:16:01 +01:00
178 changed files with 13982 additions and 1945 deletions

View File

@@ -19,10 +19,56 @@ jobs:
with:
root_file: main.tex
working_directory: paper/src
args: -pdf -interaction=nonstopmode -file-line-error -outdir=../build
args: -pdf -f -interaction=nonstopmode -file-line-error -outdir=../build
pre_compile: bash ../concat_code.sh
- name: Upload PDF
uses: actions/upload-artifact@v4
with:
name: thesis-pdf
path: paper/build/main.pdf
- name: Get current date
id: date
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Upload to Cloudflare R2
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_ENDPOINT_URL: ${{ secrets.R2_ENDPOINT }}
DATE: ${{ steps.date.outputs.date }}
BUCKET_NAME: ${{ secrets.R2_BUCKET_NAME }}
run: |
pip install boto3
python3 << 'EOF'
import boto3
import os
s3 = boto3.client('s3',
endpoint_url=os.environ['AWS_ENDPOINT_URL'],
aws_access_key_id=os.environ['AWS_ACCESS_KEY_ID'],
aws_secret_access_key=os.environ['AWS_SECRET_ACCESS_KEY']
)
date = os.environ['DATE']
bucket = os.environ['BUCKET_NAME']
# upload dated version
dated_filename = f"thesis-{date}.pdf"
s3.upload_file(
'paper/build/main.pdf',
bucket,
dated_filename,
ExtraArgs={'ContentType': 'application/pdf'}
)
print(f"Uploaded {dated_filename}")
# upload latest version
s3.upload_file(
'paper/build/main.pdf',
bucket,
'thesis-latest.pdf',
ExtraArgs={'ContentType': 'application/pdf'}
)
print(f"Uploaded thesis-latest.pdf")
EOF

30
.github/workflows/pytest.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: Run Tests
on:
push:
paths:
- 'experiments/**'
- 'backend/**'
- 'requirements.txt'
- '.github/workflows/pytest.yml'
pull_request:
paths:
- 'experiments/**'
- 'backend/**'
- 'requirements.txt'
- '.github/workflows/pytest.yml'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.13'
cache: 'pip'
- name: Install dependencies
run: |
python -m venv .venv
.venv/bin/pip install --upgrade pip
.venv/bin/pip install -r requirements.txt
- name: Run tests
run: .venv/bin/pytest -v

32
.gitignore vendored
View File

@@ -1,6 +1,32 @@
**/.env
**/.venv
PHANTOM.wiki/
**/__pycache__
**/.ipynb_checkpoints/
**/.virtual_documents/
**/__pycache__/
**/.ipynb_checkpoints/
**/session_*.svg
**/*graph.svg
**/auto/*.el
*.old
**/package-lock.json
**/*.parquet
**/_build/
paper/src/bib/auto
=======
**/_build/
paper/src/auto/*
paper/src/bib/auto
docs/goals/*.md
PHANTOM.wiki/
experiments/airflow/logs/*
experiments/airflow/logs/scheduler/
experiments/airflow/logs/dag_processor_manager/
experiments/collected_data/
experiments/agents/collected_data/
sim/rl/behavior_loader/*.dot
sim/rl/behavior_loader/*.png
sim/rl/behavior_loader/*.svg
sim/rl/behavior_loader/*.pdf
tests/e2e/node_modules/**
lab/case/thesis/runs*/
sim/case/thesis_simplified/runs*/

View File

@@ -4,36 +4,94 @@ BUILDDIR := build
TEX := main.tex
JOBNAME := main
PDF := paper/$(BUILDDIR)/$(JOBNAME).pdf
VENV := .venv
PYTHON := $(VENV)/bin/python
PIP := $(VENV)/bin/pip
PYTEST := $(VENV)/bin/pytest
.DEFAULT_GOAL := help
all: pdf
run.webapp:
@cd web && npm install && npm run dev
.PHONY: help
help:
@echo "pdf.build pdf.watch pdf.clean | test.backend test.e2e test.all | web.dev | install | stats.lines"
$(BUILDDIR):
mkdir -p paper/$(BUILDDIR)
pdf: $(BUILDDIR)
@echo "Concatenating source code..."
.PHONY: pdf.build
pdf.build: $(BUILDDIR)
@bash paper/concat_code.sh
@cd $(SRCDIR) && \
$(LATEXMK) -pdf -jobname=$(JOBNAME) \
-interaction=nonstopmode -file-line-error \
-outdir=../$(BUILDDIR) $(TEX)
watch: $(BUILDDIR)
@cd $(SRCDIR) && \
$(LATEXMK) -pvc -pdf -jobname=$(JOBNAME) \
$(LATEXMK) -pdf -jobname=$(JOBNAME) -f \
-interaction=nonstopmode -file-line-error \
-r ../.latexmkrc \
-outdir=../$(BUILDDIR) $(TEX)
clean:
.PHONY: pdf.watch
pdf.watch: $(BUILDDIR)
@cd $(SRCDIR) && \
$(LATEXMK) -pvc -pdf -jobname=$(JOBNAME) -f \
-interaction=nonstopmode -file-line-error \
-r ../.latexmkrc \
-outdir=../$(BUILDDIR) $(TEX)
.PHONY: pdf.clean
pdf.clean:
@cd $(SRCDIR) && \
$(LATEXMK) -C -jobname=$(JOBNAME) -outdir=../$(BUILDDIR) || true
rm -rf paper/$(BUILDDIR)/*
.PHONY: test.backend
test.backend: $(VENV)
$(PYTEST) -v
.PHONY: all pdf clean watch run.webapp
.PHONY: test.e2e
test.e2e:
@cd tests/e2e && npm install
@cd tests/e2e && npx playwright install chromium
@test -f tests/e2e/.env || cp tests/e2e/.env.example tests/e2e/.env
@timeout 30 bash -c 'until curl -sf http://localhost:5000/health > /dev/null 2>&1; do sleep 1; done' || (echo "Backend not ready" && exit 1)
@timeout 30 bash -c 'until curl -sf http://localhost:3000 > /dev/null 2>&1; do sleep 1; done' || (echo "Web app not ready" && exit 1)
@timeout 30 bash -c 'until curl -sf http://localhost:8085/health > /dev/null 2>&1; do sleep 1; done' || (echo "Airflow not ready" && exit 1)
@cd tests/e2e && npm test
.PHONY: test.all
test.all: test.backend test.e2e
.PHONY: web.dev
web.dev:
@cd web && npm install && npm run dev
$(VENV):
python3 -m venv $(VENV)
$(PIP) install --upgrade pip
.PHONY: install
install: $(VENV)
$(PIP) install -r requirements.txt
.PHONY: stats.lines
stats.lines:
@find . \( -path '*/node_modules' -o -path '*/.venv' -o -path '*/venv' \) -prune -o \
\( -name "*.ts" -o -name "*.py" \) -type f -print0 | xargs -0 cat | wc -l
.PHONY wordcount
wordcount:
@echo "Counting words in main text (excluding appendix)..."
@texcount -nosub -total -sum -1 \
$(SRCDIR)/chapters/01-intro.tex \
$(SRCDIR)/chapters/02-literature-review.tex \
$(SRCDIR)/chapters/03-methodology.tex \
$(SRCDIR)/chapters/04-results.tex \
$(SRCDIR)/chapters/05-discussion.tex \
$(SRCDIR)/chapters/06-conclusion.tex
.PHONY: pdf clean watch run.webapp test count-lines all
pdf: pdf.build
clean: pdf.clean
watch: pdf.watch
run.webapp: web.dev
test: test.backend
count-lines: stats.lines
all: pdf.build

View File

@@ -1,5 +1,94 @@
<img width="200" align="left" src="https://github.com/user-attachments/assets/d148b00d-e9f9-4280-89cc-0cc866e17251" />
### PHANTOM
[![Build PDF](https://github.com/velocitatem/PHANTOM/actions/workflows/latex.yml/badge.svg)](https://github.com/velocitatem/PHANTOM/actions/workflows/latex.yml)
[![Paper](https://img.shields.io/badge/Paper-PDF-red?logo=adobe-acrobat-reader)](https://pub-d5b94a3c29fd40c6b3881946e463fdb7.r2.dev/thesis-latest.pdf)
[![TPU Research Cloud](https://img.shields.io/badge/TPU%20Research%20Cloud-TRC%20supported-4285F4?logo=googlecloud&logoColor=white)](https://sites.research.google/trc/faq/)
[![Vercel Deploy](https://deploy-badge.vercel.app/?url=https://phantom-hotel.vercel.app&name=Hotel)](https://phantom-hotel.vercel.app)
[![Vercel Deploy](https://deploy-badge.vercel.app/?url=https://phantom-airline.vercel.app&name=Airline)](https://phantom-airline.vercel.app)
- https://phantom-hotel.vercel.app/
- https://phantom-airline.vercel.app/
```mermaid
mindmap
PHANTOM((PHANTOM Project))
North Star
Study how automated actors change markets
Build an experimentation platform for real-world-like commerce
Two-loop learning system
Online observation loop
Offline "defense gym" loop
Core Economic Questions
Price Discovery
How prices respond to demand signals
How signal quality changes with bots/agents
Demand & Elasticity
Shifts in willingness-to-pay
Short-run vs long-run elasticity
Market Efficiency & Welfare
Consumer surplus vs producer surplus
Deadweight loss from frictions/manipulation
Price Discrimination & Segmentation
Behavioral feature-based segmentation
Fairness vs profitability tradeoffs
Information Asymmetry
Agents amplify search and arbitrage
Sellers infer more about buyers; buyers infer more about sellers
Strategic Interaction
Consumers vs firms vs agents
Feedback loops: policy ↔ behavior ↔ price
Market Power & Competition
Algorithmic pricing as competitive tool
Risks: tacit coordination / "algorithmic collusion"
Externalities
Congestion and attention costs
Spillovers: one segments behavior affects others prices
System-Level View
Participants
Humans
Agents (automated buyers/actors)
Firms (pricing decision-makers)
Platform (measurement + control layer)
Markets Simulated
Repeated transactions
Limited inventory / capacity constraints (conceptually)
Time dynamics (learning over time)
Interventions
Pricing policies
Experiment assignment / randomized exposure
Agent behavioral policies (task-driven)
Measurement & Causal Inference
What is observed
Actions (search, click, purchase intent)
Context (product attributes, time, exposure)
Outcomes (conversion, revenue, churn proxies)
Identification strategy
A/B tests and randomization
Counterfactual baselines
Robustness checks (offline replay)
Key metrics
Revenue / profit proxies
Conversion & bounce
Price volatility / stability
Welfare proxies (e.g., dispersion, access)
Risk, Governance, and Ethics
Manipulation & Integrity
Bot-driven demand distortion
Measurement contamination
Fairness & Transparency
Differential pricing concerns
Explainability and auditability
Safety Constraints
Guardrails on price moves
Monitoring for runaway feedback loops
Outputs
Insights
When do agents raise/lower prices via behavior shifts?
Which market designs are robust to automation?
Defenses
Agent-aware pricing policies (robust control)
Detection + mitigation strategies (feature-level separability)
Platform Value
Reusable testbed for market + AI-agent research
```

112
backend/provider/app.py Normal file
View File

@@ -0,0 +1,112 @@
from fastapi import FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import Literal, Optional
import uvicorn, os, sys
from supabase import create_client, Client
from dotenv import load_dotenv
import numpy as np
import pandas as pd
load_dotenv()
# Local imports of registry and pricing function
sys.path.append(os.path.dirname(os.path.abspath(__file__))+ "/../../experiments/")
from procesing.providers import SupabaseProvider, BackendAPIProvider
from procesing.pricers import (
StaticPricer,
RandomPricer,
ElasticityBasedPricer
)
from procesing.steps import (
PredictPricesStep
)
from procesing import PipelineContext
sys.path.append(os.path.dirname(os.path.abspath(__file__))+ "/../../lib/")
print(os.path.dirname(os.path.abspath(__file__))+ "/../../lib/")
from lib.model_registry import ModelRegistry
# Config
app = FastAPI(title="PHANTOM Pricing Provider")
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
supabase: Client = create_client(os.getenv("NEXT_PUBLIC_SUPABASE_URL"), os.getenv("NEXT_PUBLIC_SUPABASE_ANON_KEY"))
registry = ModelRegistry()
class PriceResponse(BaseModel):
productId: str
price: float
base_price: float
markup: float
elasticity: Optional[float] = None
model_version: str = 'latest'
@app.get("/health")
def health() -> dict:
return {"status": "healthy", "redis": registry.health_check()}
@app.get("/api/{mode}/price/{productId}", response_model=PriceResponse)
def get_price(mode: Literal['hotel', 'airline'], productId: str, sessionId: Optional[str] = Query(None), experimentId: Optional[str] = Query(None)):
"""
THIS is the fast lookup service (mechanism).
Priority: session-keyed price > global optimal price > base price
"""
product = supabase.table(f'{mode}_products').select("metadata").eq('id', productId).execute().data[0]
if not product: raise HTTPException(404, f"Product {productId} not found")
metadata = product['metadata']
base_price = metadata.get('base_price', 100.0)
# PRIORITY 1: session-aware price (computed by Airflow worker)
if sessionId:
session_price = registry.get_session_price(sessionId, productId)
if session_price is not None:
return PriceResponse(
productId=productId,
price=session_price,
base_price=base_price,
markup=session_price/base_price,
elasticity=None,
model_version='session-aware'
)
# PRIORITY 2: global pre-computed prices (surge pricing)
prices_df = registry.get_prices('latest')
if prices_df is not None:
product_price_row = prices_df[prices_df['productId'] == productId]
if not product_price_row.empty:
optimal_price = float(product_price_row['optimal_price'].iloc[0])
return PriceResponse(
productId=productId,
price=optimal_price,
base_price=base_price,
markup=optimal_price/base_price,
elasticity=None,
model_version='surge'
)
# PRIORITY 3: fallback to base price
return PriceResponse(
productId=productId,
price=base_price,
base_price=base_price,
markup=1.0,
elasticity=None,
model_version='base'
)
@app.get("/models")
def list_models(): return registry.list_models()
@app.post("/models/reload")
def reload_models():
elasticity, pricing_model = registry.get_elasticity('latest'), registry.get_pricing_model('latest')
return {
"elasticity_loaded": bool(elasticity),
"n_products": len(elasticity) if elasticity is not None else 0,
"pricing_model_loaded": bool(pricing_model),
"model_class": pricing_model.__class__.__name__ if pricing_model else None
}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=int(os.getenv("PROVIDER_PORT", "5001")))

View File

@@ -0,0 +1,16 @@
fastapi
uvicorn[standard]
pydantic
numpy
pandas
scikit-learn
redis
supabase
confluent-kafka>=2.3.0
kafka-python
graphviz
python-dotenv>=1.0.0
requests>=2.31.0
typing-extensions>=4.8.0
pypickle
pymc

View File

@@ -7,10 +7,11 @@ import uvicorn
import os
import json
from datetime import datetime
from kafka import KafkaProducer, KafkaAdminClient
from kafka import KafkaProducer, KafkaAdminClient, KafkaConsumer
from kafka.admin import NewTopic
from kafka.errors import TopicAlreadyExistsError
from dotenv import load_dotenv
from supabase import create_client, Client
load_dotenv()
app = FastAPI()
@@ -18,11 +19,24 @@ app = FastAPI()
# kafka producer - lazy init
_producer: Optional[KafkaProducer] = None
# supabase client - lazy init
_supabase: Optional[Client] = None
def get_supabase() -> Client:
global _supabase
if _supabase is None:
url = os.getenv('NEXT_PUBLIC_SUPABASE_URL')
key = os.getenv('NEXT_PUBLIC_SUPABASE_ANON_KEY')
if not url or not key:
raise ValueError("Supabase credentials not configured")
_supabase = create_client(url, key)
return _supabase
def get_producer() -> KafkaProducer:
global _producer
if _producer is None:
host = os.getenv('KAFKA_HOST', 'localhost')
port = os.getenv('KAFKA_PORT', '29092') # use internal broker port
port = os.getenv('KAFKA_PORT', '9092')
broker = f'{host}:{port}' if port else host
print(f"[KAFKA_INIT] Connecting to broker: {broker}")
_producer = KafkaProducer(
@@ -41,6 +55,7 @@ def get_producer() -> KafkaProducer:
class EventPayload(BaseModel):
sessionId: str
experimentId: Optional[str] = None
eventName: str
page: str
productId: Optional[str] = None
@@ -49,6 +64,14 @@ class EventPayload(BaseModel):
userAgent: Optional[str] = None
ts: Optional[str] = None
class PriceLogPayload(BaseModel):
productId: str
price: float
sessionId: str
experimentId: Optional[str] = None
storeMode: str
ts: Optional[str] = None
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
@@ -61,7 +84,7 @@ app.add_middleware(
async def startup_event():
"""create kafka topics on startup"""
host = os.getenv('KAFKA_HOST', 'localhost')
port = os.getenv('KAFKA_PORT', '29092')
port = os.getenv('KAFKA_PORT', '9092')
broker = f'{host}:{port}'
try:
@@ -72,7 +95,8 @@ async def startup_event():
)
topics = [
NewTopic(name='user-interactions', num_partitions=3, replication_factor=1)
NewTopic(name='user-interactions', num_partitions=3, replication_factor=1),
NewTopic(name='price-logs', num_partitions=3, replication_factor=1)
]
admin.create_topics(new_topics=topics, validate_only=False)
@@ -124,11 +148,217 @@ async def ingest_logs(event: EventPayload):
print(traceback.format_exc())
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/kafka/price-log")
async def ingest_price_log(price_log: PriceLogPayload):
try:
if not price_log.ts:
price_log.ts = datetime.utcnow().isoformat() + 'Z'
producer = get_producer()
future = producer.send(
'price-logs',
key=price_log.productId,
value=price_log.model_dump()
)
future.add_errback(lambda e: print(f"[KAFKA_PRICE_LOG_ERROR] {e}"))
return {"success": True}
except Exception as e:
import traceback
print(f"[PRICE_LOG_ERROR] {e}")
print(traceback.format_exc())
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/kafka/dump")
def dump_logs():
# TODO: implement a dump of logs of time period t_start to t_end (params of get)
# OR: allow for params of last_n logs as a param - creating two modes of the dumping
pass
def dump_logs(
topic: str = 'user-interactions',
last_n: Optional[int] = None,
t_start: Optional[str] = None,
t_end: Optional[str] = None
):
"""dump all messages from specified kafka topic
params:
topic: kafka topic to dump (default: user-interactions)
last_n: return only last n messages (default: all)
t_start: filter by start timestamp iso format
t_end: filter by end timestamp iso format
"""
if topic not in ['user-interactions', 'price-logs']:
raise HTTPException(status_code=400, detail="Invalid topic")
host = os.getenv('KAFKA_HOST', 'localhost')
port = os.getenv('KAFKA_PORT', '9092')
broker = f'{host}:{port}'
try:
consumer = KafkaConsumer(
topic,
bootstrap_servers=[broker],
auto_offset_reset='earliest',
enable_auto_commit=False,
value_deserializer=lambda x: json.loads(x.decode('utf-8')),
consumer_timeout_ms=30000,
fetch_max_wait_ms=10000,
max_poll_records=1000
)
events = []
for msg in consumer:
events.append(msg.value)
if last_n and len(events) >= last_n * 2:
break
consumer.close()
# apply filters
if t_start or t_end:
filtered = []
for e in events:
ts = e.get('ts')
if ts:
if t_start and ts < t_start:
continue
if t_end and ts > t_end:
continue
filtered.append(e)
events = filtered
if last_n and last_n > 0:
events = events[-last_n:]
return {"success": True, "count": len(events), "data": events}
except Exception as e:
import traceback
print(f"[DUMP_ERROR] {e}")
print(traceback.format_exc())
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/products/{product_id}")
async def get_product_by_id(product_id: str):
"""fetch single product by id from either hotel_products or airline_products"""
try:
supabase = get_supabase()
# try hotel_products first
response = supabase.table('hotel_products').select('*').eq('id', product_id).execute()
if response.data and len(response.data) > 0:
return {"success": True, "data": response.data[0]}
# try airline_products
response = supabase.table('airline_products').select('*').eq('id', product_id).execute()
if response.data and len(response.data) > 0:
return {"success": True, "data": response.data[0]}
raise HTTPException(status_code=404, detail="Product not found")
except HTTPException:
raise
except Exception as e:
import traceback
print(f"[PRODUCT_BY_ID_ERROR] {e}")
print(traceback.format_exc())
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/products/type/{product_type}")
async def get_products(
product_type: str,
dateIndex: Optional[int] = None,
origin: Optional[str] = None,
destination: Optional[str] = None,
tripType: Optional[str] = None,
adults: Optional[int] = None,
children: Optional[int] = None,
infants: Optional[int] = None,
rooms: Optional[int] = None
):
"""fetch products from supabase based on type (hotel or airline)
params:
product_type: either 'hotel' or 'airline'
dateIndex: optional days offset from today (e.g., 0=today, 1=tomorrow, -1=yesterday)
origin: (airline) departure airport code
destination: (airline/hotel) arrival airport or hotel location
tripType: (airline) roundtrip, oneway, multicity
adults, children, infants: passenger counts
rooms: (hotel) number of rooms
"""
if product_type not in ['hotel', 'airline']:
raise HTTPException(status_code=400, detail="product_type must be 'hotel' or 'airline'")
try:
supabase = get_supabase()
table = f'{product_type}_products'
query = supabase.table(table).select('*')
# filter by exact date_index if provided
# dateIndex from frontend is days from today, convert to days since epoch
if dateIndex is not None:
query = query.eq('date_index', dateIndex)
response = query.execute()
results = response.data
# apply in-memory filters based on metadata for airline products
if product_type == 'airline' and results:
filtered = []
for product in results:
metadata = product.get('metadata', {})
# filter by origin airport
if origin:
dep = metadata.get('departure', {})
if dep.get('airport') != origin:
continue
# filter by destination airport
if destination:
arr = metadata.get('arrival', {})
if arr.get('airport') != destination:
continue
# passenger count validation (ensure total capacity)
if adults is not None or children is not None or infants is not None:
total_pax = (adults or 0) + (children or 0) + (infants or 0)
avail = product.get('availability', 0)
if avail < total_pax:
continue
filtered.append(product)
results = filtered
# apply in-memory filters for hotel products
elif product_type == 'hotel' and results:
filtered = []
for product in results:
metadata = product.get('metadata', {})
# filter by occupancy capacity
if adults is not None:
max_occ = metadata.get('max_occupancy', 2)
if max_occ < adults:
continue
# filter by room availability
if rooms is not None:
avail = product.get('availability', 0)
if avail < rooms:
continue
filtered.append(product)
results = filtered
return {"success": True, "count": len(results), "data": results}
except Exception as e:
import traceback
print(f"[PRODUCTS_ERROR] {e}")
print(traceback.format_exc())
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -3,3 +3,4 @@ uvicorn[standard]==0.24.0
kafka-python==2.0.2
pydantic==2.5.0
python-dotenv==1.0.0
supabase==2.9.1

View File

@@ -1,4 +1,24 @@
services:
tensorboard-rl:
image: tensorflow/tensorflow:latest
container_name: "PHANTOM-tensorboard-rl"
ports:
- "6007:6006"
volumes:
- ./sim/rl/runs:/logs
command: tensorboard --logdir=/logs --host=0.0.0.0 --port=6006
restart: unless-stopped
tensorboard-ml:
image: tensorflow/tensorflow:latest
container_name: "PHANTOM-tensorboard-ml"
ports:
- "6006:6006"
volumes:
- ./experiments/ml/runs:/logs
command: tensorboard --logdir=/logs --host=0.0.0.0 --port=6006
restart: unless-stopped
backend:
container_name: "PHANTOM-backend"
build:
@@ -9,6 +29,9 @@ services:
environment:
- KAFKA_HOST=kafka
- KAFKA_PORT=29092
- BACKEND_PORT=5000
- NEXT_PUBLIC_SUPABASE_URL=${NEXT_PUBLIC_SUPABASE_URL}
- NEXT_PUBLIC_SUPABASE_ANON_KEY=${NEXT_PUBLIC_SUPABASE_ANON_KEY}
depends_on:
- kafka
restart: unless-stopped
@@ -68,6 +91,149 @@ services:
- "${REDPANDA_CONSOLE_PORT:-8080}:8080"
restart: unless-stopped
postgres:
container_name: "PHANTOM-postgres"
image: postgres:13
environment:
- POSTGRES_USER=airflow
- POSTGRES_PASSWORD=airflow
- POSTGRES_DB=airflow
ports:
- "5433:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
airflow-init:
container_name: "PHANTOM-airflow-init"
build:
context: .
dockerfile: docker/Airflow.dockerfile
depends_on:
- postgres
environment:
- AIRFLOW__CORE__EXECUTOR=LocalExecutor
- AIRFLOW__DATABASE__SQL_ALCHEMY_CONN=postgresql+psycopg2://airflow:airflow@postgres/airflow
- AIRFLOW__CORE__FERNET_KEY=${AIRFLOW_FERNET_KEY}
- AIRFLOW__CORE__LOAD_EXAMPLES=false
- AIRFLOW__CORE__ENABLE_XCOM_PICKLING=true
- AIRFLOW__CORE__PARALLELISM=16
- AIRFLOW__CORE__DAG_CONCURRENCY=8
- AIRFLOW__CORE__MAX_ACTIVE_RUNS_PER_DAG=4
- _AIRFLOW_DB_MIGRATE=true
- _AIRFLOW_WWW_USER_CREATE=true
- _AIRFLOW_WWW_USER_USERNAME=admin
- _AIRFLOW_WWW_USER_PASSWORD=admin
- REDIS_HOST=redis
- REDIS_PORT=6379
command: version
restart: "no"
airflow-webserver:
container_name: "PHANTOM-airflow-webserver"
build:
context: .
dockerfile: docker/Airflow.dockerfile
depends_on:
- postgres
- airflow-init
- redis
environment:
- AIRFLOW__CORE__EXECUTOR=LocalExecutor
- AIRFLOW__DATABASE__SQL_ALCHEMY_CONN=postgresql+psycopg2://airflow:airflow@postgres/airflow
- AIRFLOW__CORE__FERNET_KEY=${AIRFLOW_FERNET_KEY}
- AIRFLOW__CORE__DAGS_ARE_PAUSED_AT_CREATION=true
- AIRFLOW__CORE__LOAD_EXAMPLES=false
- AIRFLOW__CORE__ENABLE_XCOM_PICKLING=true
- AIRFLOW__CORE__PARALLELISM=16
- AIRFLOW__CORE__DAG_CONCURRENCY=8
- AIRFLOW__CORE__MAX_ACTIVE_RUNS_PER_DAG=4
- AIRFLOW__SCHEDULER__MIN_FILE_PROCESS_INTERVAL=30
- AIRFLOW__SCHEDULER__DAG_DIR_LIST_INTERVAL=60
- AIRFLOW__WEBSERVER__EXPOSE_CONFIG=true
- AIRFLOW__WEBSERVER__SECRET_KEY=${AIRFLOW_SECRET_KEY}
- AIRFLOW__API__AUTH_BACKENDS=airflow.api.auth.backend.basic_auth
- KAFKA_HOST=kafka
- KAFKA_PORT=29092
- BACKEND_URL=http://backend:5000
- NEXT_PUBLIC_SUPABASE_URL=${NEXT_PUBLIC_SUPABASE_URL}
- NEXT_PUBLIC_SUPABASE_ANON_KEY=${NEXT_PUBLIC_SUPABASE_ANON_KEY}
- REDIS_HOST=redis
- REDIS_PORT=6379
ports:
- "${AIRFLOW_WEBSERVER_PORT:-8085}:8080"
command: webserver
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "--fail", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 5
start_period: 30s
airflow-scheduler:
container_name: "PHANTOM-airflow-scheduler"
build:
context: .
dockerfile: docker/Airflow.dockerfile
depends_on:
airflow-webserver:
condition: service_healthy
redis:
condition: service_started
environment:
- AIRFLOW__CORE__EXECUTOR=LocalExecutor
- AIRFLOW__DATABASE__SQL_ALCHEMY_CONN=postgresql+psycopg2://airflow:airflow@postgres/airflow
- AIRFLOW__CORE__FERNET_KEY=${AIRFLOW_FERNET_KEY}
- AIRFLOW__CORE__DAGS_ARE_PAUSED_AT_CREATION=true
- AIRFLOW__CORE__LOAD_EXAMPLES=false
- AIRFLOW__CORE__ENABLE_XCOM_PICKLING=true
- AIRFLOW__CORE__PARALLELISM=16
- AIRFLOW__CORE__DAG_CONCURRENCY=8
- AIRFLOW__CORE__MAX_ACTIVE_RUNS_PER_DAG=4
- AIRFLOW__SCHEDULER__MIN_FILE_PROCESS_INTERVAL=30
- AIRFLOW__SCHEDULER__DAG_DIR_LIST_INTERVAL=60
- AIRFLOW__SCHEDULER__PARSING_PROCESSES=2
- AIRFLOW__WEBSERVER__SECRET_KEY=${AIRFLOW_SECRET_KEY}
- AIRFLOW__API__AUTH_BACKENDS=airflow.api.auth.backend.basic_auth
- KAFKA_HOST=kafka
- KAFKA_PORT=29092
- BACKEND_URL=http://backend:5000
- NEXT_PUBLIC_SUPABASE_URL=${NEXT_PUBLIC_SUPABASE_URL}
- NEXT_PUBLIC_SUPABASE_ANON_KEY=${NEXT_PUBLIC_SUPABASE_ANON_KEY}
- REDIS_HOST=redis
- REDIS_PORT=6379
command: scheduler
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", 'airflow jobs check --job-type SchedulerJob --hostname "$${HOSTNAME}"']
interval: 30s
timeout: 10s
retries: 5
start_period: 30s
pricing-provider:
container_name: "PHANTOM-pricing-provider"
build:
context: .
dockerfile: docker/Provider.dockerfile
depends_on:
- redis
- kafka
environment:
- PROVIDER_PORT=5001
- REDIS_HOST=redis
- REDIS_PORT=6379
- KAFKA_HOST=kafka
- KAFKA_PORT=29092
- NEXT_PUBLIC_SUPABASE_URL=${NEXT_PUBLIC_SUPABASE_URL}
- NEXT_PUBLIC_SUPABASE_ANON_KEY=${NEXT_PUBLIC_SUPABASE_ANON_KEY}
- BACKEND_URL=http://localhost:5000
ports:
- "${PROVIDER_PORT:-5001}:5001"
restart: unless-stopped
volumes:
phantom_kafka_data:
phantom_redis_data:
postgres_data:

30
docker/Airflow.dockerfile Normal file
View File

@@ -0,0 +1,30 @@
FROM apache/airflow:2.7.3-python3.11
USER root
# install system deps if needed
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
USER airflow
# copy requirements for pipeline dependencies
COPY requirements.txt /tmp/requirements.txt
RUN pip install --no-cache-dir -r /tmp/requirements.txt
# install postgres driver and providers
RUN pip install --no-cache-dir \
psycopg2-binary \
apache-airflow-providers-postgres
# set airflow home
ENV AIRFLOW_HOME=/opt/airflow
COPY --chown=airflow:root experiments/airflow/dags ${AIRFLOW_HOME}/dags
COPY --chown=airflow:root experiments/procesing ${AIRFLOW_HOME}/procesing
COPY --chown=airflow:root lib ${AIRFLOW_HOME}/lib
# create logs and plugins dirs (airflow expects them)
RUN mkdir -p ${AIRFLOW_HOME}/logs ${AIRFLOW_HOME}/plugins

View File

@@ -0,0 +1,41 @@
FROM apache/airflow:2.7.3-python3.11
USER root
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
supervisor \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
USER airflow
COPY requirements.txt /tmp/requirements.txt
RUN pip install --no-cache-dir -r /tmp/requirements.txt
RUN pip install --no-cache-dir \
psycopg2-binary \
apache-airflow-providers-postgres
ENV AIRFLOW_HOME=/opt/airflow
ENV AIRFLOW__CORE__EXECUTOR=SequentialExecutor
ENV AIRFLOW__CORE__LOAD_EXAMPLES=false
ENV AIRFLOW__CORE__ENABLE_XCOM_PICKLING=true
ENV AIRFLOW__WEBSERVER__EXPOSE_CONFIG=true
# copy all code into image (standalone - no volume mounts needed)
COPY --chown=airflow:root experiments/airflow/dags ${AIRFLOW_HOME}/dags
COPY --chown=airflow:root experiments/procesing ${AIRFLOW_HOME}/procesing
COPY --chown=airflow:root lib ${AIRFLOW_HOME}/lib
RUN mkdir -p ${AIRFLOW_HOME}/logs ${AIRFLOW_HOME}/plugins
# copy entrypoint script
COPY --chown=airflow:root docker/airflow-railway-entrypoint.sh /entrypoint.sh
USER root
RUN chmod +x /entrypoint.sh
USER airflow
EXPOSE 8080
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -0,0 +1,26 @@
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies including graphviz
RUN apt-get update && apt-get install -y \
gcc \
g++ \
graphviz \
libgraphviz-dev \
&& rm -rf /var/lib/apt/lists/*
# Copy and install Python dependencies
COPY backend/provider/requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code into image
COPY lib/ /app/lib/
COPY experiments/procesing/ /app/procesing/
COPY backend/provider/ /app/provider/
ENV PYTHONPATH=/app:/app/lib:/app/procesing
WORKDIR /app/provider
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "5001"]

View File

@@ -0,0 +1,20 @@
#!/bin/bash
set -e
# init db and create admin user on first run
airflow db migrate
# create admin user if not exists
airflow users create \
--username "${AIRFLOW_ADMIN_USER:-admin}" \
--password "${AIRFLOW_ADMIN_PASSWORD:-admin}" \
--firstname Admin \
--lastname User \
--role Admin \
--email admin@example.com || true
# start scheduler in background
airflow scheduler &
# start webserver in foreground (Railway needs one foreground process)
exec airflow webserver --port ${PORT:-8080}

21
docs/goals/goals.csv Normal file
View File

@@ -0,0 +1,21 @@
store_mode,task_name,task_description,definition_of_done
airline,The Indecisive Executive (SEA-LAX),"You are traveling SEA to LAX for business. You prefer Business Class for the comfort, but you need to justify the expense to your company. 1) Find the Business Class option and check its price. 2) Compare it against the Economy option on the same route to see how much money you are saving or spending. 3) Spend some time weighing the pros and cons of the ""Flexible"" fare rule vs the standard one. 4) Ultimately, decide that your comfort is worth it and book the Business Class ticket.","Booking for SEA-LAX Business Class is completed."
airline,The Cross-Country Splurge (LAX-JFK),"You are flying LAX to JFK and want to treat yourself to First Class, but only if it's the right flight. 1) Find the First Class option. 2) thoroughly check the details (duration, arrival time). 3) Compare it with the Business Class option if available, or just look at other departure times to ensure this is the best schedule. 4) After confirming this is the absolute best option, proceed to book First Class.","Booking for LAX-JFK First Class is completed."
airline,The Budget Student (DFW-ORD),"You are a broke student flying DFW to ORD. You have a budget of roughly $200. 1) Find the cheapest Economy flight. 2) Before booking, frantically check if there are any other flights or if the ""Premium"" economy is somehow cheaper (it won't be, but you should check). 3) Hesitate for a moment to consider if you should just drive instead. 4) Resign yourself to the flight and book the Economy ticket.","Booking for DFW-ORD Economy Class is completed."
airline,The Quick Hop Commuter (LAX-SFO),"You need to get from LAX to SFO as fast as possible. Price is secondary to speed. 1) Search for flights and identify the one with the shortest duration (1h 30m). 2) Click into the details to verify the arrival time fits your schedule. 3) briefly explore if there's a Business Class upgrade available for this short flight. 4) Decide to stick with Economy since it's such a short trip and book it.","Booking for LAX-SFO is completed."
airline,The Status Chaser (SFO-SEA),"You are trying to earn airline points and need a ""Premium"" class ticket specifically. 1) Search SFO to SEA. 2) Filter or look for the Premium Economy option. 3) Compare the price gap between Premium and Standard Economy. 4) Browse the details to see if the ""Premium"" fare includes better baggage allowance. 5) Conclude it's worth the points and book the Premium seat.","Booking for SFO-SEA Premium Economy is completed."
airline,The Family Reunion (MIA-ATL),"You are booking for a family of 4 (2 adults, 2 children) flying MIA to ATL. 1) Search for 4 passengers. 2) You prefer Premium, but if the total is too high, you might settle for Economy. 3) Add Premium to your cart, look at the total, and hesitate. 4) Go back and check the Economy price for 4 people. 5) Decide to treat your family and go back to book the Premium option.","Booking for MIA-ATL (Premium) is completed."
airline,The Red Eye Skeptic (LAX-JFK),"You need to fly LAX to JFK but hate late arrivals. 1) Search for the flight and check the arrival time of the First Class option. 2) It arrives early morning (02:15), which worries you. 3) Spend some time looking for other flight options on different days to see if there's a better schedule. 4) Realize this is the only direct option that works and proceed to book it despite the time.","Booking for LAX-JFK is completed."
airline,The Refundable Requirement (ATL-DFW),"Your meeting in Dallas might get cancelled, so you strictly need a ""Refundable"" ticket. 1) Search ATL to DFW. 2) Find the First Class option and verify it lists ""Refundable"". 3) Check the Economy option to see if it is also refundable (it might not be). 4) Weigh the cost difference. 5) Choose the First Class Refundable option for peace of mind.","Booking for ATL-DFW First Class is completed."
airline,The Hub Connector (ORD-MIA),"You are flying ORD to MIA to catch a cruise. You cannot be late. 1) Search for the flight. 2) Verify the ""stops"" is 0 (Direct). 3) Click into details to check the duration. 4) Worry that 3h 30m might be too long in Economy. 5) Look for a Business class option. 6) Decide to save money for the cruise and book Economy.","Booking for ORD-MIA Economy is completed."
airline,The West Coast Hopper (SEA-LAX Business),"You fly this route often and usually pay around $700. 1) Search SEA to LAX. 2) Find the Business Class ticket. 3) Check if the price is near your usual $720 or if it's surged. 4) If it looks expensive, browse other dates to compare. 5) Return to your original desired date and book the Business Class seat.","Booking for SEA-LAX Business is completed."
hotel,The Honeymoon Suite (Presidential),"It is your honeymoon. You want the best room available, specifically one with a ""jacuzzi"". 1) Search for a room for 2 people. 2) Identify the ""Presidential Suite"". 3) Click details to confirm the amenities include a jacuzzi. 4) Browse the ""Executive Suite"" just to see what you are upgrading from. 5) Go back to the Presidential Suite, confirm it's the one you want, and book it.","Booking for the Presidential Suite is completed."
hotel,The Digital Nomad (Executive),"You are working remotely and strictly need a ""workspace"". 1) Search for a room. 2) Check the ""Executive Suite"" details for a workspace. 3) Check the ""Deluxe Room"" to see if it also has a workspace and is cheaper. 4) Compare the images (if available) or amenity lists of both. 5) Decide the Executive Suite looks more comfortable for a week of work and book it.","Booking for the Executive Suite is completed."
hotel,The Safety First (Superior),"You are traveling with valuables and need a ""safe"" in the room. 1) Search for a room. 2) Look at the ""Standard Room"" amenities. Does it have a safe? 3) Look at the ""Superior Room"". Verify it has a safe. 4) Compare the price difference. Is safety worth the extra cost? 5) Decide it is, and book the Superior Room.","Booking for the Superior Room is completed."
hotel,The Bachelor Party (Max Occupancy),"You are booking for 4 guys. You want everyone in one room if possible. 1) Search for 4 adults. 2) Find the room that fits 4 people (Presidential). 3) It looks expensive. Go back and search for 2 adults to see the price of a ""Standard Room"". 4) Calculate if booking two Standard Rooms is cheaper than one Presidential. 5) Decide it's too much hassle to manage two bookings and book the Presidential Suite.","Booking for the Presidential Suite is completed."
hotel,The Budget Refundable (Junior),"You want a cheap room but your dates might change, so it MUST be refundable. 1) Search for a room. 2) Sort by price or find the cheapest options. 3) Check the ""Standard"" and ""Superior"" rooms. Notice they are likely Non-Refundable. 4) Find the ""Junior Suite"" which is Refundable. 5) Grumble about the price difference but book the Junior Suite because you need the flexibility.","Booking for the Junior Suite is completed."
hotel,The View Hunter (Executive),"You want a room with a ""city_view"" or balcony. 1) Search for a room. 2) Check the amenities of the ""Deluxe Room"". 3) Check the amenities of the ""Executive Suite"". 4) Compare the prices. 5) Decide to treat yourself to the Executive Suite for the better view/balcony and book it.","Booking for the Executive Suite is completed."
hotel,The Just-A-Bed (Standard),"You just need a place to crash. Lowest price wins. 1) Search for a room. 2) Identify the absolute cheapest option (Standard Room). 3) Click details just to make sure it has ""wifi"". 4) Briefly glance at the ""Superior Room"" to see if the upgrade is <$10. 5) If not, go back and book the Standard Room immediately.","Booking for the Standard Room is completed."
hotel,The Family Vacation (Deluxe),"You are traveling with a child. You need a room that isn't too cramped but not a suite. 1) Search for 2 adults, 1 child. 2) Look at the ""Deluxe Room"". 3) Check the amenities for ""coffee_maker"" (parents need coffee). 4) Compare it with the ""Junior Suite"". 5) Decide the Deluxe Room is sufficient value and book it.","Booking for the Deluxe Room is completed."
hotel,The Long Stay (Junior),"You are staying for 7 nights. You want something nicer than a standard room but affordable. 1) Search for a room. 2) Look at the ""Junior Suite"". 3) Check the amenities for a ""mini_fridge"" or similar. 4) Compare the total cost for 7 nights against your budget. 5) Hesitate and look at the ""Standard Room"" price. 6) Decide the extra space of the Junior Suite is worth it for a long stay and book it.","Booking for the Junior Suite is completed."
hotel,The Last Minute Panic (Superior),"It's late and you need a room for tonight. 1) Search for a room for 1 person. 2) You recognize the ""Superior Room"" brand. 3) Click it. 4) Quickly verify check-in times or details. 5) Don't overthink it—book the Superior Room as fast as possible.","Booking for the Superior Room is completed."
1 store_mode task_name task_description definition_of_done
2 airline The Indecisive Executive (SEA-LAX) You are traveling SEA to LAX for business. You prefer Business Class for the comfort, but you need to justify the expense to your company. 1) Find the Business Class option and check its price. 2) Compare it against the Economy option on the same route to see how much money you are saving or spending. 3) Spend some time weighing the pros and cons of the "Flexible" fare rule vs the standard one. 4) Ultimately, decide that your comfort is worth it and book the Business Class ticket. Booking for SEA-LAX Business Class is completed.
3 airline The Cross-Country Splurge (LAX-JFK) You are flying LAX to JFK and want to treat yourself to First Class, but only if it's the right flight. 1) Find the First Class option. 2) thoroughly check the details (duration, arrival time). 3) Compare it with the Business Class option if available, or just look at other departure times to ensure this is the best schedule. 4) After confirming this is the absolute best option, proceed to book First Class. Booking for LAX-JFK First Class is completed.
4 airline The Budget Student (DFW-ORD) You are a broke student flying DFW to ORD. You have a budget of roughly $200. 1) Find the cheapest Economy flight. 2) Before booking, frantically check if there are any other flights or if the "Premium" economy is somehow cheaper (it won't be, but you should check). 3) Hesitate for a moment to consider if you should just drive instead. 4) Resign yourself to the flight and book the Economy ticket. Booking for DFW-ORD Economy Class is completed.
5 airline The Quick Hop Commuter (LAX-SFO) You need to get from LAX to SFO as fast as possible. Price is secondary to speed. 1) Search for flights and identify the one with the shortest duration (1h 30m). 2) Click into the details to verify the arrival time fits your schedule. 3) briefly explore if there's a Business Class upgrade available for this short flight. 4) Decide to stick with Economy since it's such a short trip and book it. Booking for LAX-SFO is completed.
6 airline The Status Chaser (SFO-SEA) You are trying to earn airline points and need a "Premium" class ticket specifically. 1) Search SFO to SEA. 2) Filter or look for the Premium Economy option. 3) Compare the price gap between Premium and Standard Economy. 4) Browse the details to see if the "Premium" fare includes better baggage allowance. 5) Conclude it's worth the points and book the Premium seat. Booking for SFO-SEA Premium Economy is completed.
7 airline The Family Reunion (MIA-ATL) You are booking for a family of 4 (2 adults, 2 children) flying MIA to ATL. 1) Search for 4 passengers. 2) You prefer Premium, but if the total is too high, you might settle for Economy. 3) Add Premium to your cart, look at the total, and hesitate. 4) Go back and check the Economy price for 4 people. 5) Decide to treat your family and go back to book the Premium option. Booking for MIA-ATL (Premium) is completed.
8 airline The Red Eye Skeptic (LAX-JFK) You need to fly LAX to JFK but hate late arrivals. 1) Search for the flight and check the arrival time of the First Class option. 2) It arrives early morning (02:15), which worries you. 3) Spend some time looking for other flight options on different days to see if there's a better schedule. 4) Realize this is the only direct option that works and proceed to book it despite the time. Booking for LAX-JFK is completed.
9 airline The Refundable Requirement (ATL-DFW) Your meeting in Dallas might get cancelled, so you strictly need a "Refundable" ticket. 1) Search ATL to DFW. 2) Find the First Class option and verify it lists "Refundable". 3) Check the Economy option to see if it is also refundable (it might not be). 4) Weigh the cost difference. 5) Choose the First Class Refundable option for peace of mind. Booking for ATL-DFW First Class is completed.
10 airline The Hub Connector (ORD-MIA) You are flying ORD to MIA to catch a cruise. You cannot be late. 1) Search for the flight. 2) Verify the "stops" is 0 (Direct). 3) Click into details to check the duration. 4) Worry that 3h 30m might be too long in Economy. 5) Look for a Business class option. 6) Decide to save money for the cruise and book Economy. Booking for ORD-MIA Economy is completed.
11 airline The West Coast Hopper (SEA-LAX Business) You fly this route often and usually pay around $700. 1) Search SEA to LAX. 2) Find the Business Class ticket. 3) Check if the price is near your usual $720 or if it's surged. 4) If it looks expensive, browse other dates to compare. 5) Return to your original desired date and book the Business Class seat. Booking for SEA-LAX Business is completed.
12 hotel The Honeymoon Suite (Presidential) It is your honeymoon. You want the best room available, specifically one with a "jacuzzi". 1) Search for a room for 2 people. 2) Identify the "Presidential Suite". 3) Click details to confirm the amenities include a jacuzzi. 4) Browse the "Executive Suite" just to see what you are upgrading from. 5) Go back to the Presidential Suite, confirm it's the one you want, and book it. Booking for the Presidential Suite is completed.
13 hotel The Digital Nomad (Executive) You are working remotely and strictly need a "workspace". 1) Search for a room. 2) Check the "Executive Suite" details for a workspace. 3) Check the "Deluxe Room" to see if it also has a workspace and is cheaper. 4) Compare the images (if available) or amenity lists of both. 5) Decide the Executive Suite looks more comfortable for a week of work and book it. Booking for the Executive Suite is completed.
14 hotel The Safety First (Superior) You are traveling with valuables and need a "safe" in the room. 1) Search for a room. 2) Look at the "Standard Room" amenities. Does it have a safe? 3) Look at the "Superior Room". Verify it has a safe. 4) Compare the price difference. Is safety worth the extra cost? 5) Decide it is, and book the Superior Room. Booking for the Superior Room is completed.
15 hotel The Bachelor Party (Max Occupancy) You are booking for 4 guys. You want everyone in one room if possible. 1) Search for 4 adults. 2) Find the room that fits 4 people (Presidential). 3) It looks expensive. Go back and search for 2 adults to see the price of a "Standard Room". 4) Calculate if booking two Standard Rooms is cheaper than one Presidential. 5) Decide it's too much hassle to manage two bookings and book the Presidential Suite. Booking for the Presidential Suite is completed.
16 hotel The Budget Refundable (Junior) You want a cheap room but your dates might change, so it MUST be refundable. 1) Search for a room. 2) Sort by price or find the cheapest options. 3) Check the "Standard" and "Superior" rooms. Notice they are likely Non-Refundable. 4) Find the "Junior Suite" which is Refundable. 5) Grumble about the price difference but book the Junior Suite because you need the flexibility. Booking for the Junior Suite is completed.
17 hotel The View Hunter (Executive) You want a room with a "city_view" or balcony. 1) Search for a room. 2) Check the amenities of the "Deluxe Room". 3) Check the amenities of the "Executive Suite". 4) Compare the prices. 5) Decide to treat yourself to the Executive Suite for the better view/balcony and book it. Booking for the Executive Suite is completed.
18 hotel The Just-A-Bed (Standard) You just need a place to crash. Lowest price wins. 1) Search for a room. 2) Identify the absolute cheapest option (Standard Room). 3) Click details just to make sure it has "wifi". 4) Briefly glance at the "Superior Room" to see if the upgrade is <$10. 5) If not, go back and book the Standard Room immediately. Booking for the Standard Room is completed.
19 hotel The Family Vacation (Deluxe) You are traveling with a child. You need a room that isn't too cramped but not a suite. 1) Search for 2 adults, 1 child. 2) Look at the "Deluxe Room". 3) Check the amenities for "coffee_maker" (parents need coffee). 4) Compare it with the "Junior Suite". 5) Decide the Deluxe Room is sufficient value and book it. Booking for the Deluxe Room is completed.
20 hotel The Long Stay (Junior) You are staying for 7 nights. You want something nicer than a standard room but affordable. 1) Search for a room. 2) Look at the "Junior Suite". 3) Check the amenities for a "mini_fridge" or similar. 4) Compare the total cost for 7 nights against your budget. 5) Hesitate and look at the "Standard Room" price. 6) Decide the extra space of the Junior Suite is worth it for a long stay and book it. Booking for the Junior Suite is completed.
21 hotel The Last Minute Panic (Superior) It's late and you need a room for tonight. 1) Search for a room for 1 person. 2) You recognize the "Superior Room" brand. 3) Click it. 4) Quickly verify check-in times or details. 5) Don't overthink it—book the Superior Room as fast as possible. Booking for the Superior Room is completed.

View File

@@ -47,7 +47,7 @@
<meta name="citation_author" content="Rösel, Daniel">
<meta name="citation_publication_date" content="2025">
<meta name="citation_conference_title" content="IE University Bachelor's Thesis">
<meta name="citation_pdf_url" content="TODO">
<meta name="citation_pdf_url" content="https://pub-d5b94a3c29fd40c6b3881946e463fdb7.r2.dev/thesis-latest.pdf">
<!-- Additional SEO -->
<meta name="theme-color" content="#2563eb">
@@ -233,14 +233,13 @@
<div class="is-size-5 publication-authors">
<span class="author-block">IE University<br>Bachelor's Thesis 2025</span>
<span class="eql-cntrb"><small><br>Advisor: <a href="SECOND AUTHOR PERSONAL LINK" target="_blank">Alberto Martín Izquierdo</a></small></span>
<span class="eql-cntrb"><small><br>Advisor: Alberto Martín Izquierdo</small></span>
</div>
<div class="column has-text-centered">
<div class="publication-links">
<!-- TODO: Update with your arXiv paper ID -->
<span class="link-block">
<a href="https://arxiv.org/pdf/<ARXIV PAPER ID>.pdf" target="_blank"
<a href="https://pub-d5b94a3c29fd40c6b3881946e463fdb7.r2.dev/thesis-latest.pdf" target="_blank"
class="external-link button is-normal is-rounded is-dark">
<span class="icon">
<i class="fas fa-file-pdf"></i>
@@ -315,7 +314,10 @@
<h2 class="title is-3">Abstract</h2>
<div class="content has-text-justified">
<p>
The primary objective of this thesis is to develop and validate pricing heuristics that protect e-commerce platforms from systematic exploitation by Large Language Model (LLM) agents within dynamic pricing environments. As AI agents increasingly mediate consumer transactions, they enable users to circumvent the Cost of Information (the price premium accumulated through demand signal expression) by conducting reconnaissance in isolated sessions before executing purchases through clean sessions at base prices. This research will make an anticipatory contribution by adapting recommendation system methodologies to distinguish between genuine human browsing behaviour and agent-orchestrated information gathering, thereby enabling pricing systems to maintain margin integrity without degrading the user experience for legitimate customers or getting rid of leads generated by LLMs.
This research establishes the following contributions: definition and formalization of non-human transactors in e-commerce platforms, development of a testing-ground for capturing the behavioral essence of these transactors across a large variety of digital systems, construction of a discriminative model to prove separability as a strong learner for downstream mitigation of contamination by non-human entities, translation of such learned separability into existing dynamic pricing machine learning loops, and establishment of a high-level KPI-affecting causal effect and cost-saving framework for the future of internet commerce in the presence of such non-human learners.
</p>
<p>
This work develops behavioral signature models using recommendation system techniques to profile session-level interaction, temporal engagement, and cross-session correlation. The AI Agent market is forecasted to grow from around USD 5-8 billion in 2025 to USD 42-52 billion by 2030, raising the question of how these systems should be designed for future robustness and how to maintain a competitive edge in the analytical components of e-commerce platforms.
</p>
</div>
</div>
@@ -433,8 +435,7 @@
<div class="container">
<h2 class="title">Poster</h2>
<!-- TODO: Replace with your poster PDF -->
<iframe src="static/pdfs/sample.pdf" width="100%" height="550">
<iframe src="https://pub-d5b94a3c29fd40c6b3881946e463fdb7.r2.dev/thesis-latest.pdf" width="100%" height="550">
</iframe>
</div>

66
engine/engine.py Normal file
View File

@@ -0,0 +1,66 @@
from sys import platform
import numpy as np
from .lib.demand import generate_demand, estimate_demand
from .lib.behavior import sample_behavior
from logging import INFO, getLogger
logger = getLogger(__name__)
logger.setLevel(INFO)
class MarketEngine():
def __init__(self,
alpha = 0.5,
N = 100,
demand_distribution = (50, 10),
demand_sampling_function = np.random.normal):
self.Nagents = int(N*alpha)
self.Nhumans = int(N*(1-alpha))
self.demand = (demand_sampling_function, demand_distribution)
def act(self, prices):
demand = generate_demand(prices, *self.demand)
sample_n = lambda n, human: [sample_behavior(demand, human=human) for _ in range(n)]
human_t, agent_t = sample_n(self.Nhumans, True), sample_n(self.Nagents, False)
trajectories = human_t + agent_t
demand_estimate = estimate_demand(trajectories)
return demand_estimate
def measure(self):
pass
class PricingEngine():
def __init__(self,
) -> None:
pass
def act(self, demand):
return np.random.uniform(low=25, high=100, size=10)
class Limbo():
def __init__(self,
platform,
market
) -> None:
self.platform_turn = True
self.platform = platform
self.market = market
self.output = None
def step(self):
# we could code golf this a little bit
if self.platform_turn:
self.output = self.platform.act(self.output)
else:
self.output = self.market.act(self.output)
print(self.output)
self.platform_turn = not self.platform_turn
if __name__ == "__main__":
platform = PricingEngine()
market = MarketEngine()
limbo = Limbo(platform, market)
for _ in range(10):
limbo.step()

3
engine/lib/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
from .demand import generate_demand, estimate_demand
from .behavior import sample_behavior
from .render import DashboardRenderer, style_axis

47
engine/lib/behavior.py Normal file
View File

@@ -0,0 +1,47 @@
from sim.rl.behavior_loader.models import BehaviorModel, AgentBehaviorModel, aggregate_event_transitions
import pandas as pd
import numpy as np
from .demand import generate_demand
base_dir = "/home/velocitatem/Documents/Projects/PHANTOM/experiments"
human_dir, agent_dir = f"{base_dir}/collected_data/", f"{base_dir}/agents/collected_data/"
_cache = {} # lazy cache for models and base pivots
def _get_base_pivot(human: bool):
key = 'human' if human else 'agent'
if key not in _cache:
model = BehaviorModel(human_dir) if human else AgentBehaviorModel(agent_dir)
mdp = model.build_MDP()
_cache[key] = pd.DataFrame(aggregate_event_transitions(mdp)).fillna(0.0)
return _cache[key]
def adjust_behavior_to_condition(condition, transition_matrix):
# expand NxN transition matrix to (N*P)x(N*P) weighted by demand condition
cond_norm = condition / np.sum(condition)
n_products = len(condition)
base_vals = transition_matrix.values
base_cols, base_rows = transition_matrix.columns.tolist(), transition_matrix.index.tolist()
# expand via kronecker-like tiling: each cell becomes a P*P block weighted by outer product of cond_norm
expanded = np.kron(base_vals, np.outer(cond_norm, cond_norm))
new_cols = [f"{c}_product{p}" for c in base_cols for p in range(n_products)]
new_rows = [f"{r}_product{p}" for r in base_rows for p in range(n_products)]
return pd.DataFrame(expanded, index=new_rows, columns=new_cols)
def sample_behavior(condition, human=True, max_len=40):
base_pivot = _get_base_pivot(human)
adjusted_transitions = adjust_behavior_to_condition(condition, base_pivot)
trajectory = [np.random.choice(adjusted_transitions.index)]
while len(trajectory) < max_len or 'checkout' in trajectory[-1]:
probs = adjusted_transitions.loc[trajectory[-1]].values
sample = np.random.choice(adjusted_transitions.columns, p=probs/np.sum(probs) if np.sum(probs) > 0 else None)
trajectory.append(sample)
return trajectory
if __name__ == "__main__":
t=sample_behavior(generate_demand(np.array([10,20,30])), human=True)
print(t)
t=sample_behavior(generate_demand(np.array([10,20,30])), human=False)
print(t)

45
engine/lib/demand.py Normal file
View File

@@ -0,0 +1,45 @@
import logging
import numpy as np
from logging import getLogger
logger = getLogger(__name__)
def generate_demand(prices, distribution_method = np.random.normal, distribution_params = (50.0, 10.0)):
# assumption 1: each product has an intrinsic valuation drawn from a normal distribution centered at 50
product_valuations = distribution_method(*distribution_params, size=len(prices))
# assumption 2: demand decreases as price increases, following a simple linear model
demand = np.maximum(0, product_valuations - prices) # demand cannot be negative
total = np.sum(demand)
demand = demand / total * 100 if total > 0 else demand # normalize to percentage, avoid div by zero
logger.info(f"Generated demand for prices {prices}: {demand} with valuations from distribution {distribution_params}")
return demand
def estimate_demand(trajectories):
demand_estimate = {}
for traj in trajectories:
for event in traj:
if 'view_product' in event:
product_id = int(event.split('_')[-1].replace('product', ''))
demand_estimate[product_id] = demand_estimate.get(product_id, 0) + 1
total_views = sum(demand_estimate.values())
for product_id in demand_estimate:
demand_estimate[product_id] = (demand_estimate[product_id] / total_views) * 100 # normalize to percentage
return demand_estimate
# Example usage
if __name__ == "__main__":
np.random.seed(42)
prices = np.array([20.0, 35.0, 50.0, 65.0])
demand = generate_demand(prices)
print("Generated Demand:", demand)
from .behavior import sample_behavior
N, alphat =200, 0.1
trajectories = []
for _ in range(int(N*(1 - alphat))):
trajectories.append(sample_behavior(demand, human=True))
for _ in range(int(N*alphat)):
trajectories.append(sample_behavior(demand, human=False))
demand_estimate = estimate_demand(trajectories)
print("Estimated Demand from Behavior:", demand_estimate)
delta = {k: demand_estimate.get(k, 0) - demand[i] for i, k in enumerate(range(len(prices)))}
delta = np.mean([np.abs(v) for v in delta.values()])
print("Demand Delta:", delta)

126
engine/lib/render.py Normal file
View File

@@ -0,0 +1,126 @@
"""rendering logic for PHANTOM environment dashboard"""
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
def style_axis(ax, title: str = None, xlabel: str = None, ylabel: str = None):
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
if title: ax.set_title(title, fontsize=11, fontweight='bold', pad=8)
if xlabel: ax.set_xlabel(xlabel, fontsize=9)
if ylabel: ax.set_ylabel(ylabel, fontsize=9)
class DashboardRenderer:
"""stateful renderer for PHANTOM market dynamics visualization"""
def __init__(self):
self.fig = None
self.gs = None
def render(self, env) -> None:
if self.fig is None:
plt.ion()
self.fig = plt.figure(figsize=(14, 10))
self.gs = GridSpec(3, 3, figure=self.fig, hspace=0.35, wspace=0.3,
left=0.07, right=0.95, top=0.92, bottom=0.08)
plt.show(block=False)
self.fig.clear()
self.fig.suptitle(f'PHANTOM Market Dynamics [t={env._step_count}, a={env.alpha:.2f}]',
fontsize=14, fontweight='bold')
demand_mat = np.array(env._demand_history).T
price_mat = np.array(env._price_history).T
elasticity = env._compute_elasticity()
self._render_scatter(env)
self._render_elasticity_bar(env, elasticity)
self._render_session_pie(env)
self._render_price_heatmap(price_mat)
self._render_demand_heatmap(demand_mat)
self._render_correlation(env.n_products, price_mat, demand_mat)
self._render_revenue(env)
self.fig.canvas.draw_idle()
self.fig.canvas.flush_events()
def _render_scatter(self, env):
ax = self.fig.add_subplot(self.gs[0, 0])
prices_flat = np.array(env._price_history).flatten()
demands_flat = np.array(env._demand_history).flatten()
product_ids = np.tile(np.arange(env.n_products), len(env._price_history))
ax.scatter(prices_flat, demands_flat, c=product_ids, cmap='plasma', alpha=0.6, s=15, edgecolors='none')
if len(prices_flat) > 1:
z = np.polyfit(prices_flat, demands_flat, 1)
p_line = np.linspace(prices_flat.min(), prices_flat.max(), 50)
ax.plot(p_line, np.polyval(z, p_line), '--', lw=1.5, alpha=0.8)
style_axis(ax, "Price-Demand Relationship", "Price ($)", "Demand")
def _render_elasticity_bar(self, env, elasticity):
ax = self.fig.add_subplot(self.gs[0, 1])
ax.barh(range(env.n_products), elasticity, alpha=0.8)
ax.axvline(0, lw=0.8, alpha=0.5)
ax.axvline(-1, lw=1, ls='--', alpha=0.5)
ax.set_yticks(range(env.n_products))
ax.set_yticklabels([f'P{i}' for i in range(env.n_products)], fontsize=7)
style_axis(ax, "Price Elasticity", "(dQ/dP)(P/Q)", None)
def _render_session_pie(self, env):
ax = self.fig.add_subplot(self.gs[0, 2])
n_h, n_a = env.market.Nhumans, env.market.Nagents
wedges, _ = ax.pie([n_h, n_a], startangle=90, wedgeprops={'linewidth': 2, 'edgecolor': 'white'})
ax.legend(wedges, [f'H ({n_h})', f'A ({n_a})'], loc='lower center', fontsize=8,
frameon=False, bbox_to_anchor=(0.5, -0.05))
ax.set_title("Session Mix", fontsize=11, fontweight='bold')
def _render_price_heatmap(self, price_mat):
ax = self.fig.add_subplot(self.gs[1, :2])
im = ax.imshow(price_mat, aspect='auto', cmap='viridis', origin='lower')
style_axis(ax, "Price Heatmap P(product, t)", "Step", "Product")
cbar = self.fig.colorbar(im, ax=ax, fraction=0.03, pad=0.02)
cbar.set_label('$', fontsize=8)
def _render_demand_heatmap(self, demand_mat):
ax = self.fig.add_subplot(self.gs[1, 2])
im = ax.imshow(demand_mat, aspect='auto', cmap='Blues', origin='lower')
style_axis(ax, "Demand Q(product, t)", "Step", None)
self.fig.colorbar(im, ax=ax, fraction=0.046, pad=0.02)
def _render_correlation(self, n_products, price_mat, demand_mat):
ax = self.fig.add_subplot(self.gs[2, 0])
if price_mat.shape[1] > 2:
corr = np.corrcoef(price_mat, demand_mat)[:n_products, n_products:]
im = ax.imshow(corr, cmap='RdBu', vmin=-1, vmax=1, aspect='auto')
ax.set_xticks(range(n_products))
ax.set_yticks(range(n_products))
ax.set_xticklabels([f'Q{i}' for i in range(n_products)], fontsize=6)
ax.set_yticklabels([f'P{i}' for i in range(n_products)], fontsize=6)
self.fig.colorbar(im, ax=ax, fraction=0.046, pad=0.02)
style_axis(ax, "Price-Demand Correlation", None, None)
def _render_revenue(self, env):
ax = self.fig.add_subplot(self.gs[2, 1:])
n_steps = len(env._revenue_history)
demand_std = [np.std(d) for d in env._demand_history]
ax.fill_between(range(n_steps), env._revenue_history, alpha=0.3)
ax.plot(env._revenue_history, linewidth=2, label='Revenue')
ax.set_xlim(0, max(n_steps, 1))
ax.set_ylim(0, max(env._revenue_history) * 1.1 if env._revenue_history else 1)
ax2 = ax.twinx()
ax2.plot(range(n_steps), demand_std, linewidth=2, ls='-', alpha=0.9, label='sigma(Demand)')
d_min, d_max = min(demand_std), max(demand_std)
margin = (d_max - d_min) * 0.2 if d_max > d_min else 0.5
ax2.set_ylim(max(0, d_min - margin), d_max + margin)
ax2.set_ylabel('Demand sigma', fontsize=9)
style_axis(ax, "Revenue & Demand Dispersion", "Step", "Revenue ($)")
ax.legend(loc='upper left', fontsize=7, frameon=False)
ax2.legend(loc='upper right', fontsize=7, frameon=False)
def close(self):
if self.fig:
plt.close(self.fig)
self.fig = None

34
engine/studies/factors.py Normal file
View File

@@ -0,0 +1,34 @@
"""shared factor definitions for experimental designs"""
import numpy as np
from dataclasses import dataclass, field
from typing import Callable, Any
@dataclass
class Factor:
name: str
levels: list
primary: bool = True # full cross vs sampled
# demand functions with compatible signatures
def demand_linear(mu, sigma, size): return np.maximum(0, np.random.normal(mu, sigma, size))
def demand_uniform(mu, sigma, size): return np.random.uniform(mu - sigma, mu + sigma, size)
def demand_exponential(mu, sigma, size): return np.random.exponential(mu, size)
def demand_logistic(mu, sigma, size): return np.random.logistic(mu, sigma, size)
DEMAND_FUNCTIONS = {
"linear": demand_linear,
"uniform": demand_uniform,
"exponential": demand_exponential,
"logistic": demand_logistic,
}
FACTORS = [
Factor("demand_fn", list(DEMAND_FUNCTIONS.keys()), primary=True),
Factor("alpha", [0.1, 0.3, 0.5, 0.7], primary=True),
Factor("n_products", [5, 15, 30, 50], primary=True),
Factor("demand_mu", [30.0, 50.0, 70.0], primary=False),
Factor("demand_sigma", [5.0, 10.0, 20.0], primary=False),
Factor("N", [100, 500, 1000], primary=False),
]
SEEDS_PER_CONFIG = 5

View File

@@ -0,0 +1,89 @@
"""full factorial design - all factor combinations"""
import sys
sys.path.insert(0, "..")
import logging
from itertools import product
import json
import hashlib
from pathlib import Path
from concurrent.futures import ProcessPoolExecutor
from .factors import FACTORS, DEMAND_FUNCTIONS, SEEDS_PER_CONFIG
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger(__name__)
def generate_configs():
"""generate all factor combinations with seeds"""
all_levels = [f.levels for f in FACTORS]
names = [f.name for f in FACTORS]
configs = []
for combo in product(*all_levels):
base = {names[i]: combo[i] for i in range(len(names))}
for seed in range(SEEDS_PER_CONFIG):
cfg = {**base, "seed": seed}
cfg["id"] = hashlib.md5(json.dumps(cfg, sort_keys=True).encode()).hexdigest()[:8]
configs.append(cfg)
return configs
def run_single(cfg: dict) -> dict:
"""execute one experiment config, return metrics"""
from engine.wrapper import PHANTOM
import numpy as np
np.random.seed(cfg["seed"])
demand_fn = DEMAND_FUNCTIONS[cfg["demand_fn"]]
env = PHANTOM(
n_products=cfg["n_products"],
alpha=cfg["alpha"],
N=cfg["N"],
)
env.market.demand = (demand_fn, (cfg["demand_mu"], cfg["demand_sigma"]))
obs, _ = env.reset()
total_reward, steps = 0.0, 0
for _ in range(100):
action = env.action_space.sample()
obs, reward, term, trunc, _ = env.step(action)
total_reward += reward
steps += 1
if term: break
env.close()
return {
"id": cfg["id"],
"config": cfg,
"total_reward": total_reward,
"avg_reward": total_reward / steps if steps > 0 else 0.0,
"steps": steps,
}
def run_study(max_workers: int = None, output: str = "results_full.jsonl"):
configs = generate_configs()
log.info(f"full factorial: {len(configs)} configs ({len(configs)//SEEDS_PER_CONFIG} unique × {SEEDS_PER_CONFIG} seeds)")
results = []
with ProcessPoolExecutor(max_workers=max_workers) as ex:
for i, result in enumerate(ex.map(run_single, configs)):
results.append(result)
if (i+1) % 100 == 0: log.info(f"progress: {i+1}/{len(configs)}")
Path(output).write_text("\n".join(json.dumps(r) for r in results))
log.info(f"wrote {len(results)} results to {output}")
return results
if __name__ == "__main__":
import argparse
p = argparse.ArgumentParser()
p.add_argument("--workers", type=int, default=None)
p.add_argument("--output", default="results_full.jsonl")
p.add_argument("--dry-run", action="store_true", help="only show design size")
args = p.parse_args()
configs = generate_configs()
log.info(f"design: {len(configs)} runs | factors: {[f.name for f in FACTORS]} | levels: {[len(f.levels) for f in FACTORS]}")
if not args.dry_run:
run_study(args.workers, args.output)

106
engine/studies/mixed_lh.py Normal file
View File

@@ -0,0 +1,106 @@
"""mixed design: full factorial on primary factors, latin hypercube on secondary"""
import sys
sys.path.insert(0, "..")
import logging
from itertools import product
import json
import hashlib
from pathlib import Path
from concurrent.futures import ProcessPoolExecutor
import numpy as np
from scipy.stats.qmc import LatinHypercube
from factors import FACTORS, DEMAND_FUNCTIONS, SEEDS_PER_CONFIG
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger(__name__)
LH_SAMPLES = 10
def generate_configs(lh_samples: int = LH_SAMPLES):
primary = [f for f in FACTORS if f.primary]
secondary = [f for f in FACTORS if not f.primary]
primary_grid = list(product(*[f.levels for f in primary]))
lhs = LatinHypercube(d=len(secondary), seed=42)
configs = []
for p_combo in primary_grid:
samples = lhs.random(n=lh_samples)
for s in samples:
sec_vals = {
secondary[i].name: secondary[i].levels[int(s[i] * len(secondary[i].levels))]
for i in range(len(secondary))
}
base = {primary[i].name: p_combo[i] for i in range(len(primary))}
base.update(sec_vals)
for seed in range(SEEDS_PER_CONFIG):
cfg = {**base, "seed": seed}
cfg["id"] = hashlib.md5(json.dumps(cfg, sort_keys=True).encode()).hexdigest()[:8]
configs.append(cfg)
return configs
def run_single(cfg: dict) -> dict:
from engine.wrapper import PHANTOM
import numpy as np
np.random.seed(cfg["seed"])
demand_fn = DEMAND_FUNCTIONS[cfg["demand_fn"]]
env = PHANTOM(
n_products=cfg["n_products"],
alpha=cfg["alpha"],
N=cfg["N"],
)
env.market.demand = (demand_fn, (cfg["demand_mu"], cfg["demand_sigma"]))
obs, _ = env.reset()
total_reward, steps = 0.0, 0
for _ in range(100):
action = env.action_space.sample()
obs, reward, term, trunc, _ = env.step(action)
total_reward += reward
steps += 1
if term: break
env.close()
return {
"id": cfg["id"],
"config": cfg,
"total_reward": total_reward,
"avg_reward": total_reward / steps,
"steps": steps,
}
def run_study(max_workers: int = None, output: str = "results_mixed.jsonl", lh_samples: int = LH_SAMPLES):
configs = generate_configs(lh_samples)
n_primary_cells = int(np.prod([len(f.levels) for f in FACTORS if f.primary]))
log.info(f"mixed LH: {len(configs)} configs ({n_primary_cells} primary × {lh_samples} LH × {SEEDS_PER_CONFIG} seeds)")
results = []
with ProcessPoolExecutor(max_workers=max_workers) as ex:
for i, result in enumerate(ex.map(run_single, configs)):
results.append(result)
if (i+1) % 100 == 0: log.info(f"progress: {i+1}/{len(configs)}")
Path(output).write_text("\n".join(json.dumps(r) for r in results))
log.info(f"wrote {len(results)} results to {output}")
return results
if __name__ == "__main__":
import argparse
p = argparse.ArgumentParser()
p.add_argument("--workers", type=int, default=None)
p.add_argument("--output", default="results_mixed.jsonl")
p.add_argument("--lh-samples", type=int, default=10)
p.add_argument("--dry-run", action="store_true", help="only show design size")
args = p.parse_args()
primary = [f for f in FACTORS if f.primary]
secondary = [f for f in FACTORS if not f.primary]
configs = generate_configs(args.lh_samples)
log.info(f"design: {len(configs)} runs | primary: {[f.name for f in primary]} | secondary (LH): {[f.name for f in secondary]}")
if not args.dry_run:
run_study(args.workers, args.output, args.lh_samples)

45
engine/train.py Normal file
View File

@@ -0,0 +1,45 @@
from stable_baselines3 import SAC
from stable_baselines3.common.callbacks import EvalCallback, BaseCallback
from .wrapper import PHANTOM
class RenderCallback(BaseCallback):
"""Renders environment on every step for live visualization."""
def __init__(self, env: PHANTOM):
super().__init__()
self.env = env
def _on_step(self) -> bool:
self.env.render()
return True
env = PHANTOM(n_products=10, alpha=0.3, render_mode="human")
eval_env = PHANTOM(n_products=10, alpha=0.3, render_mode=None)
model = SAC(
"MultiInputPolicy",
env,
verbose=1,
learning_rate=3e-4,
buffer_size=50000,
batch_size=256,
tau=0.005,
gamma=0.99,
)
render_cb = RenderCallback(env)
eval_cb = EvalCallback(eval_env, eval_freq=1000, n_eval_episodes=5, verbose=1)
model.learn(total_timesteps=50000, callback=[render_cb, eval_cb])
model.save("phantom_sac")
# test trained policy
env = PHANTOM(n_products=10, alpha=0.3, render_mode="human")
obs, _ = env.reset()
for _ in range(100):
action, _ = model.predict(obs, deterministic=True)
obs, reward, term, trunc, _ = env.step(action)
env.render()
if term or trunc: break
env.close()

118
engine/wrapper.py Normal file
View File

@@ -0,0 +1,118 @@
import gymnasium as gym
from gymnasium import spaces
import numpy as np
from .engine import Limbo, MarketEngine, PricingEngine
from .lib.render import DashboardRenderer
class PHANTOM(gym.Env):
"""Gymnasium wrapper for the Limbo pricing-market simulation. Platform sets prices, market responds with demand."""
metadata = {"render_modes": ["human", "ansi"]}
def __init__(self,
n_products: int = 10,
alpha: float = 0.3,
N: int = 100,
price_bounds: tuple = (10.0, 150.0),
lambda_coi: float = 0.1,
render_mode: str = None):
super().__init__()
self.n_products = n_products
self.price_bounds = price_bounds
self.lambda_coi = lambda_coi
self.render_mode = render_mode
self.alpha = alpha
self.N = N
self.market = MarketEngine(alpha=alpha, N=N)
self._platform_stub = PricingEngine()
self._limbo = Limbo(self._platform_stub, self.market)
self.action_space = spaces.Box(
low=price_bounds[0], high=price_bounds[1],
shape=(n_products,), dtype=np.float32
)
self.observation_space = spaces.Dict({
"demand": spaces.Box(low=0.0, high=100.0, shape=(n_products,), dtype=np.float32),
"prices": spaces.Box(low=price_bounds[0], high=price_bounds[1], shape=(n_products,), dtype=np.float32),
})
self._prices = None
self._demand = None
self._step_count = 0
self._demand_history = []
self._price_history = []
self._revenue_history = []
self._renderer = None
def _get_obs(self) -> dict:
demand_arr = np.array([self._demand.get(i, 0.0) for i in range(self.n_products)], dtype=np.float32)
return {"demand": demand_arr, "prices": self._prices.astype(np.float32)}
def _compute_reward(self, prices: np.ndarray, demand: dict) -> float:
revenue = np.sum(prices * np.array([demand.get(i, 0.0) for i in range(self.n_products)]))
# TODO: implement supra-competitive price punishment
return float(revenue)
def _record_history(self):
demand_arr = np.array([self._demand.get(i, 0.0) for i in range(self.n_products)])
self._demand_history.append(demand_arr)
self._price_history.append(self._prices.copy())
self._revenue_history.append(np.sum(self._prices * demand_arr))
def reset(self, seed=None, options=None):
super().reset(seed=seed)
self._prices = np.random.uniform(*self.price_bounds, size=self.n_products)
self._demand = self.market.act(self._prices)
self._step_count = 0
self._demand_history, self._price_history, self._revenue_history = [], [], []
self._record_history()
return self._get_obs(), {}
def step(self, action: np.ndarray):
self._prices = np.clip(action, *self.price_bounds)
self._demand = self.market.act(self._prices)
self._step_count += 1
self._record_history()
reward = self._compute_reward(self._prices, self._demand)
terminated = self._step_count >= 100
return self._get_obs(), reward, terminated, False, {"step": self._step_count}
def _compute_elasticity(self) -> np.ndarray:
"""point elasticity: e = (dQ/dP) * (P/Q) via finite differences, clipped to [-5, 5]"""
if len(self._price_history) < 2:
return np.zeros(self.n_products)
p, q = np.array(self._price_history), np.array(self._demand_history)
dp, dq = np.diff(p, axis=0), np.diff(q, axis=0)
valid = np.abs(dp) > 0.5
with np.errstate(divide='ignore', invalid='ignore'):
elasticity = np.where(valid, (dq / dp) * (p[:-1] / np.maximum(q[:-1], 1.0)), 0.0)
elasticity = np.nan_to_num(np.clip(elasticity, -5.0, 5.0), nan=0.0)
return np.mean(elasticity, axis=0) if len(elasticity) > 0 else np.zeros(self.n_products)
def render(self):
if self.render_mode == "human":
if self._renderer is None:
self._renderer = DashboardRenderer()
self._renderer.render(self)
elif self.render_mode == "ansi":
return f"step={self._step_count}, prices={self._prices}, demand={self._demand}"
return None
def close(self):
if self._renderer:
self._renderer.close()
self._renderer = None
if __name__ == "__main__":
env = PHANTOM(n_products=15, alpha=0.3, N=100, render_mode="human")
obs, _ = env.reset()
for step in range(100):
action = env.action_space.sample()
obs, reward, term, trunc, info = env.step(action)
env.render()
if term: break
env.close()

View File

@@ -0,0 +1,8 @@
# Products
# Agents
# Pipeline
Our pipeline technically should follow principles in a style like this:
- Each step should be defined as an inheriting child of an scikit pipeline step, the granularity of the steps is dictated by the following: a step should be a transformation, augmentation or computation independently, no single stage should run multiple in-itself. This way we can modularize properly all the components and track properly in airflow. A stage can be defined as an sklearn step but then must be transalted to a function that takes the context in our DAG of airflow. All parametrization must be done via contexts.

0
experiments/__init__.py Normal file
View File

View File

@@ -0,0 +1 @@
"""Agentic behavior runner for PHANTOM research platform."""

View File

@@ -0,0 +1,47 @@
from .base import Agent as BaseAgent
from browser_use import Browser, Agent, ChatOpenAI
from enum import Enum
class AgentTypes(str, Enum):
GENERIC_BROWSER_USE_AGENT = "generic_browser_use_agent"
def _build_prompt(goal : str, environment_url : str) -> str:
#TODO: Improve prompt engineering here and experiment with
return f"""You are an autonomous agent tasked with achieving the following goal: {goal}
You have access to a web browser to interact with the environment at {environment_url}.
Use the browser to navigate, gather information, and perform actions necessary to accomplish your goal.
Be thorough and ensure you complete the task fully."""
class GenericBrowserUseAgent(BaseAgent):
def __init__(self,
goal: str,
url: str = "http://localhost:3000",
timeout: int = 300,
llm_model: str = "gpt-5-mini",
headless: bool = True):
super().__init__(goal, url, timeout)
self.llm_model = ChatOpenAI(model=llm_model)
self.browser = Browser(headless=headless)
self.agent = Agent(task=_build_prompt(goal, url),
llm=self.llm_model,
browser=self.browser)
async def act(self) -> str:
self.result = await self.agent.run()
# https://github.com/browser-use/browser-use/blob/main/browser_use/agent/views.py#L301
return self.result.final_result()
def get_agent(agent_type: AgentTypes, **kwargs) -> Agent:
if agent_type == AgentTypes.GENERIC_BROWSER_USE_AGENT:
return GenericBrowserUseAgent(**kwargs)
else:
raise ValueError(f"Unknown agent type: {agent_type}")
if __name__ == "__main__":
import asyncio
JTBD= "Find me the cheapest room in Madrid for 2 people in the next two days, review each hotel room in detail and then add it to cart."
agent = get_agent(AgentTypes.GENERIC_BROWSER_USE_AGENT,
goal=JTBD,
url="http://localhost:3000/start-task?uuid=d10f5ab3-a7b7-4e97-8d94-ab06f1537c0a",
timeout=300)
R=asyncio.run(agent.act())
print(R)

View File

@@ -0,0 +1,19 @@
from abc import ABC, abstractmethod
from typing import Optional
class Agent(ABC):
"""Base interface for browser automation agents"""
def __init__(self, goal: str, url: str = "http://localhost:3000", timeout: int = 300):
self.goal = goal
self.url = url
self.timeout = timeout
self.result: Optional[str] = None
@abstractmethod
async def act(self) -> str:
"""Execute goal and return result text"""
pass
def final_result(self) -> Optional[str]:
return self.result

117
experiments/agents/run.py Normal file
View File

@@ -0,0 +1,117 @@
from supabase import create_client, Client
import os
import random
import asyncio
import json
from dotenv import load_dotenv
from experiments.agents.agent import get_agent, AgentTypes
from lib.kafka_client import get_interactions
load_dotenv()
RESULTS="/home/velocitatem/Documents/Projects/PHANTOM/experiments/agents/collected_data/"
client = create_client(
os.getenv("NEXT_PUBLIC_SUPABASE_URL"),
os.getenv("NEXT_PUBLIC_SUPABASE_ANON_KEY")
)
def pick_random_task():
mode = 'hotel'
tasks = client.table("tasks").select("*").execute().data
if mode == 'hotel':
# drop all that have 'flight' in the description
tasks = [task for task in tasks if 'flight' not in task['task_description'].lower()]
return random.choice(tasks) if tasks else None
def clear_kafka_data():
"""Delete and recreate Kafka topics to clear all data"""
from kafka.admin import KafkaAdminClient, NewTopic
from kafka.errors import UnknownTopicOrPartitionError
import time
kafka_host = os.getenv('KAFKA_HOST', 'localhost')
kafka_port = os.getenv('KAFKA_PORT', '9092')
broker = f'{kafka_host}:{kafka_port}'
admin = KafkaAdminClient(bootstrap_servers=broker)
topics = ['user-interactions', 'price-logs']
try:
admin.delete_topics(topics, timeout_ms=5000)
print(f"Deleted topics: {topics}")
time.sleep(2)
except UnknownTopicOrPartitionError:
print("Topics don't exist, skipping delete")
except Exception as e:
print(f"Error deleting topics: {e}")
new_topics = [
NewTopic(name='user-interactions', num_partitions=3, replication_factor=1),
NewTopic(name='price-logs', num_partitions=3, replication_factor=1)
]
try:
admin.create_topics(new_topics=new_topics, validate_only=False)
print(f"Recreated topics: {topics}")
except Exception as e:
print(f"Error creating topics: {e}")
finally:
admin.close()
def create_new_experiment(task_id):
import uuid
subject_name = f"agent_{str(uuid.uuid4())[:8]}"
experiment = {
"subject_name": subject_name,
"xp_human_only": False,
"xp_market_mode": "hotel",
"xp_task_id": task_id,
}
response = client.table("experiments").insert(experiment).execute()
return response.data[0] if response.data else None
if __name__ == "__main__":
clear_kafka_data()
task = pick_random_task()
if not task:
print("No tasks available")
exit(1)
experiment = create_new_experiment(task['id'])
exp_id = experiment['id']
exp_dir = f"{RESULTS}{exp_id}"
os.makedirs(exp_dir, exist_ok=True)
# construct experiment URL with uuid param
base_url = os.getenv('NEXT_PUBLIC_API_BASE', 'http://localhost:3000')
agent_url = f"{base_url}/start-task?uuid={exp_id}"
print(f"Created experiment {exp_id} for task {task['id']}")
print(f"Agent will interact with: {agent_url}")
# instantiate and run agent
agent = get_agent(
AgentTypes.GENERIC_BROWSER_USE_AGENT,
goal=task['task_description'],
url=agent_url,
timeout=300,
headless=True
)
result = asyncio.run(agent.act())
print(f"Agent result: {result}")
# export interaction and price data from kafka
interactions = get_interactions(topic='user-interactions', timeout_ms=3000)
prices = get_interactions(topic='price-logs', timeout_ms=3000)
with open(f"{exp_dir}/int.json", 'w') as f:
json.dump(interactions, f, indent=2)
with open(f"{exp_dir}/price.json", 'w') as f:
json.dump(prices, f, indent=2)
print(f"Experiment {exp_id} completed.")
print(f"Exported {len(interactions)} interactions and {len(prices)} price logs to {exp_dir}")

View File

@@ -0,0 +1,30 @@
import pytest
import asyncio
from experiments.agents.agent import get_agent, AgentTypes
import os
def test_agent_init():
agent = get_agent(AgentTypes.GENERIC_BROWSER_USE_AGENT, goal="test", url="http://example.com", timeout=100)
assert agent.goal == "test"
assert agent.url == "http://example.com"
assert agent.timeout == 100
def test_invalid_agent():
with pytest.raises(ValueError):
get_agent("invalid", goal="test")
@pytest.mark.asyncio
@pytest.mark.skipif("OPENAI_API_KEY" not in os.environ, reason="OPENAI_API_KEY not set")
async def test_agent_execution():
agent = get_agent(AgentTypes.GENERIC_BROWSER_USE_AGENT, goal="get page title", url="https://example.com", timeout=60)
result = await agent.act()
assert result
assert agent.final_result()
assert agent.final_result().history[-1].result[-1].is_done == True
assert isinstance(result, str)
assert "example" in result.lower()
assert len(result) > 0

View File

@@ -0,0 +1,115 @@
from airflow import DAG, Dataset
from airflow.decorators import task
from airflow.utils.dates import days_ago
from datetime import timedelta
import pandas as pd
import logging
import sys
import pickle
sys.path.insert(0, '/opt/airflow')
from procesing.context import PipelineContext
from procesing.providers import SupabaseProvider, BackendAPIProvider
from procesing.steps import (
FetchInteractionsStep,
ValidateDataStep,
ExtractSessionFeaturesStep,
JoinLabelsStep,
)
TRAINING_DATASET = Dataset('phantom://ml/training-data')
DEFAULT_ARGS = {
'owner': 'phantom-research',
'depends_on_past': False,
'email_on_failure': False,
'email_on_retry': False,
'retries': 2,
'retry_delay': timedelta(minutes=5),
}
class CompositeProvider(SupabaseProvider, BackendAPIProvider):
def __init__(self):
SupabaseProvider.__init__(self)
BackendAPIProvider.__init__(self)
def _get_context(store_mode: str = 'hotel') -> PipelineContext:
return PipelineContext(provider=CompositeProvider(), store_mode=store_mode)
with DAG(
'ml_training_pipeline',
default_args=DEFAULT_ARGS,
description='ML training data pipeline: fetch -> validate -> extract features -> label -> publish',
schedule=None,
start_date=days_ago(1),
catchup=False,
max_active_runs=1,
tags=['ml', 'training', 'features', 'research'],
) as dag:
@task
def fetch_interactions(**kwargs) -> bytes:
dag_conf = kwargs.get('dag_run').conf if kwargs.get('dag_run') else {}
ctx = _get_context(dag_conf.get('store_mode', 'hotel'))
df = FetchInteractionsStep(ctx).transform(None)
logging.info(f"Fetched {len(df)} interactions, {df['sessionId'].nunique()} sessions")
return pickle.dumps(df)
@task
def validate_data(raw_data: bytes, **kwargs) -> bytes:
df = pickle.loads(raw_data)
dag_conf = kwargs.get('dag_run').conf if kwargs.get('dag_run') else {}
ctx = _get_context(dag_conf.get('store_mode', 'hotel'))
validated = ValidateDataStep(ctx).transform(df)
report = ctx.get_cached('validation_report') or {}
logging.info(f"Validation: {report.get('status')}, {report.get('sessions', 0)} sessions")
return pickle.dumps(validated)
@task
def extract_session_features(validated_data: bytes, **kwargs) -> bytes:
df = pickle.loads(validated_data)
if df.empty:
logging.warning("Empty input, skipping feature extraction")
return pickle.dumps(pd.DataFrame())
dag_conf = kwargs.get('dag_run').conf if kwargs.get('dag_run') else {}
ctx = _get_context(dag_conf.get('store_mode', 'hotel'))
features = ExtractSessionFeaturesStep(ctx).transform(df)
logging.info(f"Extracted {len(features.columns)} features for {len(features)} sessions")
return pickle.dumps(features)
@task
def join_labels(features_data: bytes, **kwargs) -> bytes:
features_df = pickle.loads(features_data)
if features_df.empty:
logging.warning("Empty features, skipping label join")
return pickle.dumps(pd.DataFrame())
dag_conf = kwargs.get('dag_run').conf if kwargs.get('dag_run') else {}
ctx = _get_context(dag_conf.get('store_mode', 'hotel'))
labeled = JoinLabelsStep(ctx).transform(features_df)
n_agents = labeled['is_agent'].sum() if 'is_agent' in labeled.columns else 0
logging.info(f"Labeled {len(labeled)} sessions: {n_agents} agents")
return pickle.dumps(labeled)
@task(outlets=[TRAINING_DATASET])
def publish_training_data(labeled_data: bytes, **kwargs) -> dict:
labeled_df = pickle.loads(labeled_data)
if labeled_df.empty:
return {'status': 'skipped', 'reason': 'empty_data'}
dag_conf = kwargs.get('dag_run').conf if kwargs.get('dag_run') else {}
return {
'status': 'success',
'n_sessions': len(labeled_df),
'n_features': len([c for c in labeled_df.columns if c not in ['sessionId', 'experimentId', 'is_agent']]),
'store_mode': dag_conf.get('store_mode', 'hotel'),
'timestamp': pd.Timestamp.now().isoformat(),
}
raw = fetch_interactions()
validated = validate_data(raw)
features = extract_session_features(validated)
labeled = join_labels(features)
publish_training_data(labeled)

View File

@@ -0,0 +1,220 @@
from pandas.core.algorithms import factorize_array
from airflow import DAG
from airflow.operators.python import PythonOperator
from airflow.utils.dates import days_ago
from datetime import timedelta
import pandas as pd
import logging
import sys
import pickle
sys.path.insert(0, '/opt/airflow')
from procesing.context import PipelineContext
from procesing.providers import SupabaseProvider, BackendAPIProvider
from procesing.steps import (
FetchInteractionsStep,
FetchPriceLogsStep,
ComputeDemandStep,
AggregatePriceLogsStep,
JoinProductFeaturesStep,
)
from procesing.pricers.simple import SimpleSurgePricer
DEFAULT_ARGS = {
'owner': 'phantom-research',
'depends_on_past': False,
'email_on_failure': False,
'email_on_retry': False,
'retries': 2,
'retry_delay': timedelta(minutes=5),
}
class CompositeProvider(SupabaseProvider, BackendAPIProvider):
def __init__(self):
SupabaseProvider.__init__(self)
BackendAPIProvider.__init__(self)
def _get_provider():
return CompositeProvider()
def _make_task_callables(store_mode: str):
"""Generate task callables bound to a specific store_mode."""
def get_context(**kwargs):
return PipelineContext(provider=_get_provider(), store_mode=store_mode)
def fetch_interactions(**kwargs):
ctx = get_context(**kwargs)
df = FetchInteractionsStep(ctx).transform(None)
kwargs['ti'].xcom_push(key='interactions_raw', value=pickle.dumps(df))
logging.info(f"[{store_mode}] Fetched {len(df)} interaction records")
return len(df)
def fetch_price_logs(**kwargs):
ctx = get_context(**kwargs)
df = FetchPriceLogsStep(ctx).transform(None)
kwargs['ti'].xcom_push(key='price_logs_raw', value=pickle.dumps(df))
logging.info(f"[{store_mode}] Fetched {len(df)} price records")
return len(df)
def compute_demand(**kwargs):
ti = kwargs['ti']
df = pickle.loads(ti.xcom_pull(key='interactions_raw'))
ctx = get_context(**kwargs)
demand_df = ComputeDemandStep(ctx).transform(df)
ti.xcom_push(key='demand_data', value=pickle.dumps(demand_df))
logging.info(f"[{store_mode}] Computed demand for {len(demand_df)} products")
return len(demand_df)
def aggregate_price_logs(**kwargs):
ti = kwargs['ti']
df = pickle.loads(ti.xcom_pull(key='price_logs_raw'))
ctx = get_context(**kwargs)
price_df = AggregatePriceLogsStep(ctx).transform(df)
ti.xcom_push(key='price_data', value=pickle.dumps(price_df))
logging.info(f"[{store_mode}] Aggregated price logs for {len(price_df)} products")
return len(price_df)
def join_product_features(**kwargs):
ti = kwargs['ti']
demand_df = pickle.loads(ti.xcom_pull(key='demand_data'))
price_df = pickle.loads(ti.xcom_pull(key='price_data'))
ctx = get_context(**kwargs)
joined_df = JoinProductFeaturesStep(ctx).transform((demand_df, price_df))
ti.xcom_push(key='product_features', value=pickle.dumps(joined_df))
logging.info(f"[{store_mode}] Joined features for {len(joined_df)} products")
return len(joined_df)
def apply_surge_pricing(**kwargs):
ti = kwargs['ti']
product_features = pickle.loads(ti.xcom_pull(key='product_features'))
dag_conf = kwargs.get('dag_run').conf if kwargs.get('dag_run') else {}
data = product_features.rename(columns={'demand_score': 'demand'})
surge_pricer = SimpleSurgePricer(
high_threshold=dag_conf.get('high_threshold', 10),
low_threshold=dag_conf.get('low_threshold', 2),
surge_multiplier=dag_conf.get('surge_multiplier', 1.2),
discount_multiplier=dag_conf.get('discount_multiplier', 0.9)
)
surge_pricer.fit(data)
data['optimal_price'] = surge_pricer.predict()
prices_df = data[['productId', 'price', 'base_price', 'optimal_price', 'demand']].rename(columns={
'price': 'current_price', 'demand': 'demand_score'
})
ti.xcom_push(key='predicted_prices', value=pickle.dumps(prices_df))
logging.info(f"[{store_mode}] Applied surge pricing for {len(prices_df)} products")
return len(prices_df)
def publish_results(**kwargs):
ti = kwargs['ti']
prices_df = pickle.loads(ti.xcom_pull(key='predicted_prices'))
from lib.model_registry import ModelRegistry
registry = ModelRegistry()
dag_conf = kwargs.get('dag_run').conf if kwargs.get('dag_run') else {}
metadata = {
'timestamp': pd.Timestamp.now().isoformat(),
'store_mode': store_mode,
'dag_run_id': kwargs['dag_run'].run_id if kwargs.get('dag_run') else 'manual',
'pricing_method': 'surge',
'high_threshold': dag_conf.get('high_threshold', 10),
'low_threshold': dag_conf.get('low_threshold', 2),
'surge_multiplier': dag_conf.get('surge_multiplier', 1.2),
'discount_multiplier': dag_conf.get('discount_multiplier', 0.9)
}
registry.publish_prices(prices_df, model_name=f'{store_mode}_latest', metadata=metadata)
logging.info(f"[{store_mode}] Published surge pricing for {len(prices_df)} products")
return {
'n_products': len(prices_df),
'registry_status': 'success',
'store_mode': store_mode,
'mean_demand': float(prices_df['demand_score'].mean()) if 'demand_score' in prices_df.columns else None
}
return {
'fetch_interactions': fetch_interactions,
'fetch_price_logs': fetch_price_logs,
'compute_demand': compute_demand,
'aggregate_price_logs': aggregate_price_logs,
'join_product_features': join_product_features,
'apply_surge_pricing': apply_surge_pricing,
'publish_results': publish_results,
}
def create_surge_pricing_dag(store_mode: str) -> DAG:
"""Factory: generates a surge pricing DAG for a given store_mode."""
callables = _make_task_callables(store_mode)
dag = DAG(
f'surge_pricing_{store_mode}',
default_args=DEFAULT_ARGS,
description=f'Surge pricing pipeline for {store_mode} store mode',
schedule_interval='*/15 * * * *',
start_date=days_ago(1),
catchup=False,
max_active_runs=1,
tags=['pricing', 'surge', 'research', store_mode],
)
with dag:
t_fetch_interactions = PythonOperator(
task_id='fetch_interactions',
python_callable=callables['fetch_interactions'],
provide_context=True,
)
t_fetch_price_logs = PythonOperator(
task_id='fetch_price_logs',
python_callable=callables['fetch_price_logs'],
provide_context=True,
)
t_compute_demand = PythonOperator(
task_id='compute_demand',
python_callable=callables['compute_demand'],
provide_context=True,
)
t_aggregate_prices = PythonOperator(
task_id='aggregate_price_logs',
python_callable=callables['aggregate_price_logs'],
provide_context=True,
)
t_join_features = PythonOperator(
task_id='join_product_features',
python_callable=callables['join_product_features'],
provide_context=True,
)
t_surge_pricing = PythonOperator(
task_id='apply_surge_pricing',
python_callable=callables['apply_surge_pricing'],
provide_context=True,
)
t_publish = PythonOperator(
task_id='publish_results',
python_callable=callables['publish_results'],
provide_context=True,
)
t_fetch_interactions >> t_compute_demand
t_fetch_price_logs >> t_aggregate_prices
[t_compute_demand, t_aggregate_prices] >> t_join_features >> t_surge_pricing >> t_publish
return dag
# instantiate DAGs for Airflow to discover
dag_airline = create_surge_pricing_dag('airline')
dag_hotel = create_surge_pricing_dag('hotel')
# TODO: Refactor this factory from a surge pricing factory to a general pricing factory
# We will do this by passing a pricing strategy class to the factory, since the generic pipeline is:
# take all interaction data, group by sessionId and assign a new price vector to each session
# in the grouping we get a subset of the interactions per sessionId and we can map that to some Features
# we define a custom _get_features(interactions .) methodin the strategy class
# we then run only the inference which is the .predict(trajectory) per-session which will give us a new price vector
# this we then publish for each sessionId group
# this might include no deleting most of the pricers we have defined and starting with a super simple surge-pricing algorithm that is no-fit only predict. This we can then test end-to-end and observe changes to prices according to a desired strategy - we have to define this one as a very short term strategy because we run sessions that take only a few minutes.

View File

@@ -0,0 +1,253 @@
from airflow import DAG
from airflow.operators.python import PythonOperator
from airflow.utils.dates import days_ago
from datetime import timedelta
import pandas as pd
import logging
import sys
import pickle
import io
# add parent dir to path so procesing package can be imported
sys.path.insert(0, '/opt/airflow')
from procesing.context import PipelineContext
from procesing.providers import SupabaseProvider, BackendAPIProvider
from procesing.steps import (
FetchInteractionsStep,
FetchPriceLogsStep,
ComputeDemandStep,
AggregatePriceLogsStep,
JoinProductFeaturesStep,
)
from procesing.pricers.simple import SimpleSurgePricer
default_args = {
'owner': 'phantom-research',
'depends_on_past': False,
'email_on_failure': False,
'email_on_retry': False,
'retries': 2,
'retry_delay': timedelta(minutes=5),
}
def get_provider():
"""Factory to create composite provider"""
class CompositeProvider(SupabaseProvider, BackendAPIProvider): # TODO: Fix this into one global provider singelton instead of multiple inheritance declarations acoss the codebase
def __init__(self):
SupabaseProvider.__init__(self)
BackendAPIProvider.__init__(self)
return CompositeProvider()
def get_context(**kwargs):
"""Build pipeline context from Airflow config"""
dag_conf = kwargs.get('dag_run').conf if kwargs.get('dag_run') else {}
return PipelineContext(
provider=get_provider(),
store_mode=dag_conf.get('store_mode', 'hotel'),
)
# atomic task functions (each wraps one sklearn step)
def fetch_interactions(**kwargs):
"""Task: Fetch interaction data from Kafka"""
context = get_context(**kwargs)
step = FetchInteractionsStep(context)
df = step.transform(None)
kwargs['ti'].xcom_push(key='interactions_raw', value=pickle.dumps(df))
logging.info(f"Fetched {len(df)} interaction records")
return len(df)
def fetch_price_logs(**kwargs):
"""Task: Fetch price logs from Kafka"""
context = get_context(**kwargs)
step = FetchPriceLogsStep(context)
df = step.transform(None)
kwargs['ti'].xcom_push(key='price_logs_raw', value=pickle.dumps(df))
logging.info(f"Fetched {len(df)} price records")
return len(df)
def compute_demand(**kwargs):
"""Task: Compute demand scores from interactions"""
ti = kwargs['ti']
df = pickle.loads(ti.xcom_pull(key='interactions_raw'))
context = get_context(**kwargs)
step = ComputeDemandStep(context)
demand_df = step.transform(df)
# TODO: clear the xcom
ti.xcom_push(key='demand_data', value=pickle.dumps(demand_df))
logging.info(f"Computed demand for {len(demand_df)} products")
return len(demand_df)
def aggregate_price_logs(**kwargs):
"""Task: Aggregate price logs"""
ti = kwargs['ti']
df = pickle.loads(ti.xcom_pull(key='price_logs_raw'))
context = get_context(**kwargs)
step = AggregatePriceLogsStep(context)
price_df = step.transform(df)
ti.xcom_push(key='price_data', value=pickle.dumps(price_df))
logging.info(f"Aggregated price logs for {len(price_df)} products")
return len(price_df)
def join_product_features(**kwargs):
"""Task: Join demand and price data"""
ti = kwargs['ti']
demand_df = pickle.loads(ti.xcom_pull(key='demand_data'))
price_df = pickle.loads(ti.xcom_pull(key='price_data'))
context = get_context(**kwargs)
step = JoinProductFeaturesStep(context)
joined_df = step.transform((demand_df, price_df))
ti.xcom_push(key='product_features', value=pickle.dumps(joined_df))
logging.info(f"Joined features for {len(joined_df)} products")
return len(joined_df)
def apply_surge_pricing(**kwargs):
"""Task: Apply surge pricing rules to generate optimal prices"""
ti = kwargs['ti']
product_features = pickle.loads(ti.xcom_pull(key='product_features'))
dag_conf = kwargs.get('dag_run').conf if kwargs.get('dag_run') else {}
# rename demand_score to demand for pricer compatibility
data = product_features.rename(columns={'demand_score': 'demand'})
high_thresh = dag_conf.get('high_threshold', 10)
low_thresh = dag_conf.get('low_threshold', 2)
surge_mult = dag_conf.get('surge_multiplier', 1.2)
discount_mult = dag_conf.get('discount_multiplier', 0.9)
logging.info(f"Surge pricing config: high_thresh={high_thresh}, low_thresh={low_thresh}, surge_mult={surge_mult}, discount_mult={discount_mult}")
logging.info(f"Demand stats: min={data['demand'].min():.2f}, max={data['demand'].max():.2f}, mean={data['demand'].mean():.2f}")
logging.info(f"Products with high demand (>={high_thresh}): {(data['demand'] >= high_thresh).sum()}")
logging.info(f"Products with low demand (<={low_thresh}): {(data['demand'] <= low_thresh).sum()}")
surge_pricer = SimpleSurgePricer(
high_threshold=high_thresh,
low_threshold=low_thresh,
surge_multiplier=surge_mult,
discount_multiplier=discount_mult
)
surge_pricer.fit(data)
data['optimal_price'] = surge_pricer.predict()
base_avg = data['base_price'].mean()
optimal_avg = data['optimal_price'].mean()
price_change_pct = ((optimal_avg - base_avg) / base_avg) * 100
logging.info(f"Price adjustment: base_avg={base_avg:.2f}, optimal_avg={optimal_avg:.2f}, change={price_change_pct:+.1f}%")
prices_df = data[['productId', 'price', 'base_price', 'optimal_price', 'demand']].rename(columns={
'price': 'current_price',
'demand': 'demand_score'
})
ti.xcom_push(key='predicted_prices', value=pickle.dumps(prices_df))
logging.info(f"Applied surge pricing for {len(prices_df)} products")
return len(prices_df)
def publish_results(**kwargs):
"""Task: Publish surge pricing results to registry"""
ti = kwargs['ti']
prices_df = pickle.loads(ti.xcom_pull(key='predicted_prices'))
sys.path.insert(0, '/opt/airflow')
from lib.model_registry import ModelRegistry
registry = ModelRegistry()
dag_conf = kwargs.get('dag_run').conf if kwargs.get('dag_run') else {}
metadata = {
'timestamp': pd.Timestamp.now().isoformat(),
'store_mode': dag_conf.get('store_mode', 'hotel'),
'dag_run_id': kwargs['dag_run'].run_id if kwargs.get('dag_run') else 'manual',
'pricing_method': 'surge',
'high_threshold': dag_conf.get('high_threshold', 10),
'low_threshold': dag_conf.get('low_threshold', 2),
'surge_multiplier': dag_conf.get('surge_multiplier', 1.2),
'discount_multiplier': dag_conf.get('discount_multiplier', 0.9)
}
registry.publish_prices(prices_df, model_name='latest', metadata=metadata)
logging.info(f"Published surge pricing for {len(prices_df)} products")
return {
'n_products': len(prices_df),
'registry_status': 'success',
'mean_demand': float(prices_df['demand_score'].mean()) if 'demand_score' in prices_df.columns else None
}
# DAG definition
with DAG(
'surge_pricing_pipeline',
default_args=default_args,
description='Simple surge pricing pipeline: demand aggregation + rule-based pricing',
schedule_interval='*/15 * * * *',
start_date=days_ago(1),
catchup=False,
max_active_runs=1,
tags=['pricing', 'surge', 'research', 'simplified'],
) as dag:
# parallel data fetching
t_fetch_interactions = PythonOperator(
task_id='fetch_interactions',
python_callable=fetch_interactions,
provide_context=True,
)
t_fetch_price_logs = PythonOperator(
task_id='fetch_price_logs',
python_callable=fetch_price_logs,
provide_context=True,
)
# compute demand from interactions
t_compute_demand = PythonOperator(
task_id='compute_demand',
python_callable=compute_demand,
provide_context=True,
)
# aggregate price logs
t_aggregate_prices = PythonOperator(
task_id='aggregate_price_logs',
python_callable=aggregate_price_logs,
provide_context=True,
)
# join demand and prices
t_join_features = PythonOperator(
task_id='join_product_features',
python_callable=join_product_features,
provide_context=True,
)
# apply surge pricing
t_surge_pricing = PythonOperator(
task_id='apply_surge_pricing',
python_callable=apply_surge_pricing,
provide_context=True,
)
# publish to registry
t_publish = PythonOperator(
task_id='publish_results',
python_callable=publish_results,
provide_context=True,
)
# dependency graph: parallel fetch -> process -> join -> surge -> publish
t_fetch_interactions >> t_compute_demand
t_fetch_price_logs >> t_aggregate_prices
[t_compute_demand, t_aggregate_prices] >> t_join_features >> t_surge_pricing >> t_publish

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,21 @@
from .evals import evaluate
from .arch import (
XGBoostAgentClassifier,
LightGBMAgentClassifier,
ContrastiveWeakClassifier,
TrajectoryEncoder,
WeakClassifier,
contrastive_loss,
featurize_trajectory,
)
__all__ = [
'evaluate',
'XGBoostAgentClassifier',
'LightGBMAgentClassifier',
'ContrastiveWeakClassifier',
'TrajectoryEncoder',
'WeakClassifier',
'contrastive_loss',
'featurize_trajectory',
]

212
experiments/ml/arch.py Normal file
View File

@@ -0,0 +1,212 @@
# sklearn compatible models for agent detection
from sklearn.base import BaseEstimator, ClassifierMixin
from typing import Any, Optional, Tuple, Dict, List
from abc import ABC, abstractmethod
from collections import defaultdict
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
import torch.nn.functional as F
import sys
from pathlib import Path
# add lib to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent.parent / 'lib'))
from lib.features import (
transition_histogram as _lib_transition_histogram,
temporal_signature as _lib_temporal_signature,
state_coverage as _lib_state_coverage,
transition_entropy as _lib_transition_entropy,
featurize_trajectory as _lib_featurize_trajectory,
parse_timestamp
)
from lib.state import event_to_state, get_event_name, get_timestamp
TASK = 'classification'
LABELS = ['human', 'agent']
class WeakClassifier(BaseEstimator, ClassifierMixin, ABC):
# a simple contrastive machine learning model learns to distinguish human/agent behavior
# using weakly supervised contrastive learning + augmentation
def __init__(self, **kwargs):
super().__init__()
self.model = None
self.kwargs = kwargs
class TrajectoryEncoder(nn.Module):
"""Encode variable-length event sequences to fixed-dim embedding via bidirectional LSTM"""
def __init__(self, input_dim: int, embed_dim: int = 32, hidden_dim: int = 64):
super().__init__()
self.event_embed = nn.Linear(input_dim, hidden_dim)
self.lstm = nn.LSTM(hidden_dim, hidden_dim, batch_first=True, bidirectional=True)
self.proj = nn.Linear(hidden_dim * 2, embed_dim)
def forward(self, x: torch.Tensor) -> torch.Tensor: # x: (batch, seq_len, input_dim)
h = F.relu(self.event_embed(x))
_, (hn, _) = self.lstm(h)
hn = torch.cat([hn[-2], hn[-1]], dim=1) # concat bidirectional hidden states
return F.normalize(self.proj(hn), dim=1) # L2 normalized
class ContrastiveWeakClassifier(WeakClassifier):
"""Contrastive learning classifier for human/agent trajectory discrimination"""
def __init__(self, input_dim: int = 64, embed_dim: int = 32, margin: float = 1.0, **kwargs):
super().__init__(**kwargs)
self.input_dim = input_dim
self.embed_dim = embed_dim
self.margin = margin
self.encoder = TrajectoryEncoder(input_dim, embed_dim)
self.classifier = nn.Linear(embed_dim, 2)
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
self._fitted = False
def to_device(self):
self.encoder.to(self.device)
self.classifier.to(self.device)
return self
def encode(self, x: torch.Tensor) -> torch.Tensor:
return self.encoder(x.to(self.device))
def forward(self, x: torch.Tensor) -> torch.Tensor:
emb = self.encode(x)
return self.classifier(emb)
def fit(self, X, y=None): # sklearn interface - actual training in weak.train.py
self._fitted = True
return self
def predict(self, X: np.ndarray) -> np.ndarray:
self.encoder.eval()
self.classifier.eval()
with torch.no_grad():
x = torch.tensor(X, dtype=torch.float32).unsqueeze(1).to(self.device)
logits = self.forward(x)
return torch.argmax(logits, dim=1).cpu().numpy()
def predict_proba(self, X: np.ndarray) -> np.ndarray:
self.encoder.eval()
self.classifier.eval()
with torch.no_grad():
x = torch.tensor(X, dtype=torch.float32).unsqueeze(1).to(self.device)
logits = self.forward(x)
return F.softmax(logits, dim=1).cpu().numpy()
def contrastive_loss(anchor: torch.Tensor, positive: torch.Tensor, negative: torch.Tensor, margin: float = 0.3) -> torch.Tensor:
"""Triplet loss using cosine similarity (for L2-normalized embeddings). margin in [0,1] range."""
pos_sim = F.cosine_similarity(anchor, positive) # higher = more similar
neg_sim = F.cosine_similarity(anchor, negative)
return F.relu(neg_sim - pos_sim + margin).mean() # want pos_sim > neg_sim + margin
def nt_xent_loss(z_i: torch.Tensor, z_j: torch.Tensor, temperature: float = 0.5) -> torch.Tensor:
"""Normalized temperature-scaled cross entropy loss (SimCLR style)"""
batch_size = z_i.size(0)
z = torch.cat([z_i, z_j], dim=0) # (2N, embed_dim)
sim = F.cosine_similarity(z.unsqueeze(1), z.unsqueeze(0), dim=2) / temperature
mask = torch.eye(2 * batch_size, dtype=torch.bool, device=z.device)
sim.masked_fill_(mask, -float('inf'))
labels = torch.arange(batch_size, device=z.device)
labels = torch.cat([labels + batch_size, labels]) # positive pairs
return F.cross_entropy(sim, labels)
# feature extraction utilities - delegating to lib.features for unified implementation
# these wrappers maintain backwards compatibility for existing imports
def transition_histogram(events: List, state_fn, max_states: int = 50) -> np.ndarray:
"""Compute normalized histogram of state transitions in trajectory"""
return _lib_transition_histogram(events, state_fn, max_states)
def temporal_signature(events: List, ts_fn) -> np.ndarray:
"""Extract temporal features: mean/std/skew of inter-event times"""
return _lib_temporal_signature(events, ts_fn)
def state_coverage(events: List, state_fn, mdp_states: set) -> float:
"""Fraction of MDP states visited by trajectory"""
return _lib_state_coverage(events, state_fn, mdp_states)
def transition_entropy(events: List, state_fn) -> float:
"""Compute entropy of transition distribution (randomness of navigation)"""
return _lib_transition_entropy(events, state_fn)
def featurize_trajectory(events: List, mdp: Optional[Dict] = None, input_dim: int = 64) -> np.ndarray:
"""Convert trajectory to fixed-dim feature vector - uses lib.features implementation"""
mdp_states = set(mdp.get('states', [])) if mdp else set()
def _ts_fn(e):
return parse_timestamp(get_timestamp(e))
def _event_name_fn(e):
return get_event_name(e)
return _lib_featurize_trajectory(events, event_to_state, _ts_fn, _event_name_fn, mdp_states, input_dim)
# gradient boosting classifiers for comparison baselines
class XGBoostAgentClassifier(BaseEstimator, ClassifierMixin):
"""XGBoost classifier for human/agent detection from session features"""
def __init__(self, n_estimators: int = 100, max_depth: int = 6, learning_rate: float = 0.1, **kwargs):
self.n_estimators = n_estimators
self.max_depth = max_depth
self.learning_rate = learning_rate
self.model = None
self.kwargs = kwargs
def fit(self, X: np.ndarray, y: np.ndarray):
try:
import xgboost as xgb
self.model = xgb.XGBClassifier(n_estimators=self.n_estimators, max_depth=self.max_depth,
learning_rate=self.learning_rate, **self.kwargs)
self.model.fit(X, y)
except ImportError:
raise ImportError("xgboost required for XGBoostAgentClassifier")
return self
def predict(self, X: np.ndarray) -> np.ndarray:
if self.model is None:
raise ValueError("fit the model first")
return self.model.predict(X)
def predict_proba(self, X: np.ndarray) -> np.ndarray:
if self.model is None:
raise ValueError("fit the model first")
return self.model.predict_proba(X)
class LightGBMAgentClassifier(BaseEstimator, ClassifierMixin):
"""LightGBM classifier for human/agent detection from session features"""
def __init__(self, n_estimators: int = 100, max_depth: int = -1, learning_rate: float = 0.1, **kwargs):
self.n_estimators = n_estimators
self.max_depth = max_depth
self.learning_rate = learning_rate
self.model = None
self.kwargs = kwargs
def fit(self, X: np.ndarray, y: np.ndarray):
try:
import lightgbm as lgb
self.model = lgb.LGBMClassifier(n_estimators=self.n_estimators, max_depth=self.max_depth,
learning_rate=self.learning_rate, verbose=-1, **self.kwargs)
self.model.fit(X, y)
except ImportError:
raise ImportError("lightgbm required for LightGBMAgentClassifier")
return self
def predict(self, X: np.ndarray) -> np.ndarray:
if self.model is None:
raise ValueError("fit the model first")
return self.model.predict(X)
def predict_proba(self, X: np.ndarray) -> np.ndarray:
if self.model is None:
raise ValueError("fit the model first")
return self.model.predict_proba(X)

103
experiments/ml/evals.py Normal file
View File

@@ -0,0 +1,103 @@
from sklearn.metrics import (accuracy_score, precision_score, recall_score,
f1_score, roc_auc_score, confusion_matrix, roc_curve)
from torch.utils.tensorboard import SummaryWriter
from logging import getLogger
import numpy as np
import matplotlib.pyplot as plt
import io
from PIL import Image
logger = getLogger(__name__)
def log_feature_importance(writer, model, feature_names, epoch):
"""Visualize and log feature importance to TensorBoard"""
if not hasattr(model, 'feature_importances_') or model.feature_importances_ is None:
return
importance = model.feature_importances_
indices = np.argsort(importance)[::-1][:20] # top 20
top_features = [feature_names[i] for i in indices]
top_importance = importance[indices]
for i, (feat, imp) in enumerate(zip(top_features, top_importance)):
writer.add_scalar(f'FeatureImportance/{feat}', imp, epoch)
fig, ax = plt.subplots(figsize=(10, 8))
ax.barh(range(len(top_features)), top_importance, align='center')
ax.set_yticks(range(len(top_features)))
ax.set_yticklabels(top_features)
ax.invert_yaxis()
ax.set_xlabel('Importance')
ax.set_title(f'Top 20 Feature Importance (Epoch {epoch})')
ax.grid(axis='x', alpha=0.3)
buf = io.BytesIO()
plt.tight_layout()
plt.savefig(buf, format='png', dpi=100)
buf.seek(0)
img = Image.open(buf)
img_arr = np.array(img)
writer.add_image('FeatureImportance/Chart', img_arr, epoch, dataformats='HWC')
plt.close()
def evaluate(perdicted_class, predicted_proba, true_class, writer: SummaryWriter, epoch: int):
accuracy = accuracy_score(true_class, perdicted_class)
precision = precision_score(true_class, perdicted_class, zero_division=0)
recall = recall_score(true_class, perdicted_class, zero_division=0)
f1 = f1_score(true_class, perdicted_class, zero_division=0)
roc_auc = roc_auc_score(true_class, predicted_proba)
writer.add_scalar('Eval/Accuracy', accuracy, epoch)
writer.add_scalar('Eval/Precision', precision, epoch)
writer.add_scalar('Eval/Recall', recall, epoch)
writer.add_scalar('Eval/F1_Score', f1, epoch)
writer.add_scalar('Eval/ROC_AUC', roc_auc, epoch)
# confusion matrix
cm = confusion_matrix(true_class, perdicted_class)
tn, fp, fn, tp = cm.ravel()
writer.add_scalar('Eval/TrueNeg', tn, epoch)
writer.add_scalar('Eval/FalsePos', fp, epoch)
writer.add_scalar('Eval/FalseNeg', fn, epoch)
writer.add_scalar('Eval/TruePos', tp, epoch)
# specificity and sensitivity
specificity = tn / (tn + fp) if (tn + fp) > 0 else 0
sensitivity = recall # same as recall/TPR
writer.add_scalar('Eval/Specificity', specificity, epoch)
writer.add_scalar('Eval/Sensitivity', sensitivity, epoch)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
ax1.matshow(cm, cmap='Blues', alpha=0.7)
for i in range(2):
for j in range(2):
ax1.text(j, i, str(cm[i, j]), ha='center', va='center', fontsize=14)
ax1.set_xlabel('Predicted')
ax1.set_ylabel('True')
ax1.set_title(f'Confusion Matrix (Epoch {epoch})')
ax1.set_xticks([0, 1])
ax1.set_yticks([0, 1])
ax1.set_xticklabels(['Human', 'Agent'])
ax1.set_yticklabels(['Human', 'Agent'])
# ROC curve
fpr, tpr, _ = roc_curve(true_class, predicted_proba)
ax2.plot(fpr, tpr, label=f'AUC={roc_auc:.3f}', linewidth=2)
ax2.plot([0, 1], [0, 1], 'k--', label='Random')
ax2.set_xlabel('False Positive Rate')
ax2.set_ylabel('True Positive Rate')
ax2.set_title('ROC Curve')
ax2.legend()
ax2.grid(alpha=0.3)
buf = io.BytesIO()
plt.tight_layout()
plt.savefig(buf, format='png', dpi=100)
buf.seek(0)
img = Image.open(buf)
img_arr = np.array(img)
writer.add_image('Eval/Metrics', img_arr, epoch, dataformats='HWC')
plt.close()
logger.info(f"Eval {epoch}: Acc={accuracy:.4f} Prec={precision:.4f} Rec={recall:.4f} F1={f1:.4f} AUC={roc_auc:.4f}")

View File

@@ -0,0 +1,6 @@
torch
tensorboard
fastparquet
pyarrow
xgboost
lightgbm

137
experiments/ml/train.py Normal file
View File

@@ -0,0 +1,137 @@
from torch.utils.tensorboard import SummaryWriter
from sklearn.model_selection import train_test_split
from logging import getLogger
from pathlib import Path
import pandas as pd
import numpy as np
import joblib
from datetime import datetime
from ml.evals import evaluate, log_feature_importance
from ml.arch import XGBoostAgentClassifier, LightGBMAgentClassifier, LABELS
logger = getLogger(__name__)
FEATURE_COLS_EXCLUDE = ['sessionId', 'experimentId', 'is_agent', 'xp_human_only', 'xp_market_mode', 'browser_family']
RUNS_DIR = Path('ml/runs')
CHECKPOINTS_DIR = Path('ml/checkpoints')
def prepare_data(df):
"""
Prepare feature matrix and labels from raw dataframe
Handles missing labels, feature selection, and categorical encoding
Returns: (X, y, feature_cols)
"""
# drop rows with missing labels
n_before = len(df)
df = df[df['is_agent'].notna()].copy()
n_dropped = n_before - len(df)
if n_dropped > 0:
logger.warning(f"Dropped {n_dropped} sessions with missing labels")
if len(df) == 0:
logger.error("No labeled data available")
return None, None, None
feature_cols = [c for c in df.columns if c not in FEATURE_COLS_EXCLUDE]
# handle categorical browser_family via one-hot encoding
if 'browser_family' in df.columns:
browser_dummies = pd.get_dummies(df['browser_family'], prefix='browser', drop_first=True)
df = pd.concat([df, browser_dummies], axis=1)
feature_cols.extend(browser_dummies.columns.tolist())
X = df[feature_cols].fillna(0)
y = df['is_agent'].astype(int)
return X, y, feature_cols
def train(data_path=None, model_type='xgboost', test_size=0.2, random_state=42,
n_estimators=200, max_depth=6, learning_rate=0.05):
"""
Train agent detection classifier
Args:
data_path: path to labeled feature matrix CSV or parquet
model_type: 'xgboost' or 'lightgbm'
test_size: fraction for test split
random_state: seed for reproducibility
"""
RUNS_DIR.mkdir(exist_ok=True)
CHECKPOINTS_DIR.mkdir(exist_ok=True)
run_name = f"{model_type}_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
writer = SummaryWriter(log_dir=RUNS_DIR / run_name)
logger.info(f"Starting training run: {run_name}")
# load data
if data_path is None:
logger.error("data_path required")
return
df = pd.read_parquet(data_path)
logger.info(f"Loaded {len(df)} sessions from {data_path}")
# prepare features and labels
if 'is_agent' not in df.columns:
logger.error("Missing is_agent column")
return
X, y, feature_cols = prepare_data(df)
if X is None:
return
# class distribution
n_agents = y.sum()
n_humans = (y == 0).sum()
logger.info(f"Class distribution: {n_humans} humans, {n_agents} agents" + (f" (ratio {n_humans / n_agents:.2f})" if n_agents > 0 else ""))
# train/test split with stratification
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=test_size, random_state=random_state, stratify=y
)
logger.info(f"Train: {len(X_train)}, Test: {len(X_test)}")
# init model
if model_type == 'xgboost':
model = XGBoostAgentClassifier(
n_estimators=n_estimators,
max_depth=max_depth,
learning_rate=learning_rate
)
elif model_type == 'lightgbm':
model = LightGBMAgentClassifier(
n_estimators=n_estimators,
max_depth=max_depth,
learning_rate=learning_rate
)
else:
logger.error(f"Unknown model type: {model_type}")
return
# train with eval set for early stopping
model.fit(X_train, y_train, eval_set=[(X_test, y_test)])
logger.info("Training complete")
# evaluate on test set
y_pred = model.predict(X_test)
y_prob = model.predict_proba(X_test)[:, 1]
evaluate(y_pred, y_prob, y_test, writer, epoch=0)
# log feature importance
log_feature_importance(writer, model, X.columns.tolist(), epoch=0)
# save model
model_path = CHECKPOINTS_DIR / f"{run_name}.pkl"
joblib.dump({'model': model, 'feature_cols': X.columns.tolist(), 'run_name': run_name}, model_path)
logger.info(f"Model saved to {model_path}")
writer.close()
return model, X.columns.tolist()
if __name__ == "__main__":
import sys
data_path = sys.argv[1]
model_type = sys.argv[2] if len(sys.argv) > 2 else 'xgboost'
train(data_path, model_type=model_type)

View File

@@ -0,0 +1,246 @@
import sys
sys.path.insert(0, "/home/velocitatem/Documents/Projects/PHANTOM/sim/rl/behavior_loader")
sys.path.insert(0, "/home/velocitatem/Documents/Projects/PHANTOM/experiments/ml")
from sim.rl.behavior_loader.loader import AgentLoader, Loader, JointLoader, PayloadModel
from sim.rl.behavior_loader.models import JointBehaviorModel
from arch import ContrastiveWeakClassifier, contrastive_loss, featurize_trajectory
from typing import List, Optional, Dict
from datetime import datetime, timedelta
from copy import deepcopy
import numpy as np
import random
import torch
from torch.utils.data import Dataset, DataLoader
from torch.optim import Adam
from torch.utils.tensorboard import SummaryWriter
RUNS_DIR = "/home/velocitatem/Documents/Projects/PHANTOM/experiments/ml/runs"
agent_dir = "/home/velocitatem/Documents/Projects/PHANTOM/experiments/agents/collected_data/"
human_dir = "/home/velocitatem/Documents/Projects/PHANTOM/experiments/collected_data/"
def _perturb_ts(evt: PayloadModel, jitter_ms: int = 500) -> PayloadModel:
"""Add random jitter to event timestamp"""
new_evt = deepcopy(evt)
try:
ts = datetime.fromisoformat(evt.ts.replace('Z', '+00:00'))
delta = timedelta(milliseconds=random.randint(-jitter_ms, jitter_ms))
new_evt.ts = (ts + delta).isoformat()
except:
pass
return new_evt
def augment_trajectory(trajectory: List[PayloadModel], rate: float = 0.1) -> List[PayloadModel]:
"""Apply random augmentation to trajectory for contrastive learning"""
if len(trajectory) < 2:
return trajectory
aug_type = random.choice(['window', 'shuffle', 'noise', 'drop'])
if aug_type == 'window': # random contiguous sub-sequence (70-100% length)
min_len = max(2, int(len(trajectory) * 0.7))
sub_len = random.randint(min_len, len(trajectory))
start = random.randint(0, len(trajectory) - sub_len)
return trajectory[start:start + sub_len]
elif aug_type == 'shuffle': # swap adjacent pairs with probability rate
result = list(trajectory)
for i in range(len(result) - 1):
if random.random() < rate:
result[i], result[i + 1] = result[i + 1], result[i]
return result
elif aug_type == 'drop': # drop events with probability rate
result = [e for e in trajectory if random.random() > rate]
return result if len(result) >= 2 else trajectory[:2]
elif aug_type == 'noise': # perturb timestamps
return [_perturb_ts(e, jitter_ms=500) for e in trajectory]
return trajectory
class TripletDataset(Dataset):
"""Generate (anchor, positive, negative) triplets on-the-fly with augmentation"""
def __init__(self, data: Dict[str, List[PayloadModel]], mdp: Optional[Dict], augment_fn, input_dim: int = 64, multiplier: int = 10):
self.sessions = list(data.items())
self.human_ids = [i for i, (sid, _) in enumerate(self.sessions) if sid.startswith('human_')]
self.agent_ids = [i for i, (sid, _) in enumerate(self.sessions) if sid.startswith('agent_')]
self.mdp = mdp
self.augment = augment_fn
self.input_dim = input_dim
self.multiplier = multiplier
if not self.human_ids or not self.agent_ids:
raise ValueError(f"Need both human ({len(self.human_ids)}) and agent ({len(self.agent_ids)}) sessions")
def __len__(self) -> int:
return len(self.sessions) * self.multiplier
def __getitem__(self, idx: int):
anchor_idx = idx % len(self.sessions)
sid, events = self.sessions[anchor_idx]
is_human = sid.startswith('human_')
anchor = featurize_trajectory(events, self.mdp, self.input_dim)
positive = featurize_trajectory(self.augment(events), self.mdp, self.input_dim)
neg_pool = self.agent_ids if is_human else self.human_ids
neg_idx = random.choice(neg_pool)
negative = featurize_trajectory(self.sessions[neg_idx][1], self.mdp, self.input_dim)
label = 0 if is_human else 1 # 0=human, 1=agent
return (torch.tensor(anchor, dtype=torch.float32),
torch.tensor(positive, dtype=torch.float32),
torch.tensor(negative, dtype=torch.float32),
torch.tensor(label, dtype=torch.long))
def train(epochs: int = 100, lr: float = 1e-3, batch_size: int = 4, input_dim: int = 64,
embed_dim: int = 32, margin: float = 0.3, verbose: bool = True, run_name: str = None):
"""Train contrastive weak classifier on human/agent trajectories"""
joint = JointLoader(human_dir, agent_dir)
data = joint.get_data()
if verbose:
print(f"Loaded {len(data)} sessions")
joint_model = JointBehaviorModel(human_dir, agent_dir)
ref_mdp = joint_model.build_MDP()
dataset = TripletDataset(data, ref_mdp, augment_trajectory, input_dim=input_dim)
loader = DataLoader(dataset, batch_size=batch_size, shuffle=True, drop_last=True)
model = ContrastiveWeakClassifier(input_dim=input_dim, embed_dim=embed_dim, margin=margin)
model.to_device()
run_name = run_name or f"d{input_dim}_e{embed_dim}_lr{lr}_m{margin}_{datetime.now():%Y%m%d_%H%M%S}"
writer = SummaryWriter(f"{RUNS_DIR}/train/{run_name}")
optimizer = Adam(list(model.encoder.parameters()) + list(model.classifier.parameters()), lr=lr)
ce_loss_fn = torch.nn.CrossEntropyLoss()
best_loss = float('inf')
for epoch in range(epochs):
model.encoder.train()
model.classifier.train()
total_loss, n_batches = 0.0, 0
for anchor, positive, negative, labels in loader:
anchor, positive, negative, labels = [t.to(model.device) for t in [anchor, positive, negative, labels]]
z_a, z_p, z_n = [model.encoder(t.unsqueeze(1)) for t in [anchor, positive, negative]]
trip_loss = contrastive_loss(z_a, z_p, z_n, margin=model.margin)
ce = ce_loss_fn(model.classifier(z_a), labels)
loss = trip_loss + 0.5 * ce
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_loss += loss.item()
n_batches += 1
avg_loss = total_loss / max(n_batches, 1)
writer.add_scalar('loss', avg_loss, epoch)
if verbose and (epoch + 1) % 10 == 0:
print(f"Epoch {epoch+1}/{epochs}: loss={avg_loss:.4f}")
if avg_loss < best_loss:
best_loss = avg_loss
writer.close()
if verbose:
print(f"Done. Best={best_loss:.4f} TB:{RUNS_DIR}/train/{run_name}")
return model, ref_mdp
def evaluate_loocv(input_dim: int = 64, embed_dim: int = 32, epochs_per_fold: int = 50,
lr: float = 1e-3, margin: float = 0.3, run_name: str = None):
"""Leave-one-out cross-validation given limited samples"""
joint = JointLoader(human_dir, agent_dir)
data = joint.get_data()
session_ids = list(data.keys())
joint_model = JointBehaviorModel(human_dir, agent_dir)
ref_mdp = joint_model.build_MDP()
run_name = run_name or f"loocv_d{input_dim}_e{embed_dim}_m{margin}_{datetime.now():%Y%m%d_%H%M%S}"
writer = SummaryWriter(f"{RUNS_DIR}/eval/{run_name}")
predictions, actuals = [], []
for fold_idx, test_sid in enumerate(session_ids):
train_data = {k: v for k, v in data.items() if k != test_sid}
test_events = data[test_sid]
test_label = 0 if test_sid.startswith('human_') else 1
n_human = sum(1 for k in train_data if k.startswith('human_'))
n_agent = sum(1 for k in train_data if k.startswith('agent_'))
if n_human == 0 or n_agent == 0:
continue
try:
dataset = TripletDataset(train_data, ref_mdp, augment_trajectory, input_dim=input_dim, multiplier=5)
loader = DataLoader(dataset, batch_size=2, shuffle=True, drop_last=True)
model = ContrastiveWeakClassifier(input_dim=input_dim, embed_dim=embed_dim, margin=margin)
model.to_device()
optimizer = Adam(list(model.encoder.parameters()) + list(model.classifier.parameters()), lr=lr)
model.encoder.train()
model.classifier.train()
for _ in range(epochs_per_fold):
for anchor, positive, negative, labels in loader:
z_a, z_p, z_n = [model.encoder(t.unsqueeze(1).to(model.device)) for t in [anchor, positive, negative]]
loss = contrastive_loss(z_a, z_p, z_n, margin=margin)
optimizer.zero_grad()
loss.backward()
optimizer.step()
test_feat = featurize_trajectory(test_events, ref_mdp, input_dim)
pred = model.predict(test_feat.reshape(1, -1))[0]
predictions.append(pred)
actuals.append(test_label)
print(f" {test_sid[:12]}...: pred={pred}, actual={test_label}, {'OK' if pred == test_label else 'MISS'}")
except Exception as e:
print(f"Error: {e}")
if predictions:
acc = sum(p == a for p, a in zip(predictions, actuals)) / len(predictions)
tp = sum(1 for p, a in zip(predictions, actuals) if p == 1 and a == 1)
fp = sum(1 for p, a in zip(predictions, actuals) if p == 1 and a == 0)
fn = sum(1 for p, a in zip(predictions, actuals) if p == 0 and a == 1)
prec, rec = tp / max(tp + fp, 1), tp / max(tp + fn, 1)
f1 = 2 * prec * rec / max(prec + rec, 1e-10)
writer.add_scalar('accuracy', acc, 0)
writer.add_scalar('f1', f1, 0)
writer.add_scalar('precision', prec, 0)
writer.add_scalar('recall', rec, 0)
writer.close()
print(f"\nAccuracy: {acc:.2%} F1: {f1:.3f} TB:{RUNS_DIR}/eval/{run_name}")
return acc, predictions, actuals
writer.close()
return 0.0, [], []
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--mode', choices=['train', 'eval'], default='train')
parser.add_argument('--epochs', type=int, default=100)
parser.add_argument('--lr', type=float, default=1e-3)
parser.add_argument('--margin', type=float, default=0.3)
parser.add_argument('--input-dim', type=int, default=64)
parser.add_argument('--embed-dim', type=int, default=32)
parser.add_argument('--run-name', type=str, default=None)
args = parser.parse_args()
if args.mode == 'train':
model, mdp = train(epochs=args.epochs, lr=args.lr, input_dim=args.input_dim,
embed_dim=args.embed_dim, margin=args.margin, run_name=args.run_name)
else:
evaluate_loocv(input_dim=args.input_dim, embed_dim=args.embed_dim, epochs_per_fold=args.epochs,
lr=args.lr, margin=args.margin, run_name=args.run_name)

View File

@@ -0,0 +1,51 @@
from procesing.context import PipelineContext
from procesing.providers import DataProvider, SupabaseProvider, BackendAPIProvider
from procesing.steps import (
BaseContextStep,
FetchInteractionsStep,
FetchPriceLogsStep,
FetchExperimentsStep,
JoinExperimentsStep,
CreatePriceBucketsStep,
AugmentEventNamesStep,
ChunkByTimeWindowStep,
ComputeDemandStep,
ComputeDemandForChunksStep,
AggregatePriceLogsStep,
# StateSpace,
# BuildStateSpaceStep,
FitPricingFunctionStep,
PredictPricesStep,
)
from procesing.pipelines import (
interaction_extraction_pipeline,
price_extraction_pipeline,
pricing_pipeline,
full_pipeline,
)
__all__ = [
'PipelineContext',
'DataProvider',
'SupabaseProvider',
'BackendAPIProvider',
'BaseContextStep',
'FetchInteractionsStep',
'FetchPriceLogsStep',
'FetchExperimentsStep',
'JoinExperimentsStep',
'CreatePriceBucketsStep',
'AugmentEventNamesStep',
'ChunkByTimeWindowStep',
'ComputeDemandStep',
'ComputeDemandForChunksStep',
'AggregatePriceLogsStep',
# 'StateSpace',
# 'BuildStateSpaceStep',
'FitPricingFunctionStep',
'PredictPricesStep',
'interaction_extraction_pipeline',
'price_extraction_pipeline',
'pricing_pipeline',
'full_pipeline',
]

View File

@@ -0,0 +1,114 @@
from __future__ import annotations
import os
import random
from pathlib import Path
from types import SimpleNamespace
import pandas as pd
from lib.separability import estimate_alpha, load_artifacts, score_session
# use relative import when in package context, fallback for standalone
try:
from sim.rl.behavior_loader.models import AgentBehaviorModel
except ImportError:
import sys
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "sim" / "rl" / "behavior_loader"))
from models import AgentBehaviorModel
# paths should be configurable via environment or relative to project root
PROJECT_ROOT = Path(__file__).parent.parent.parent
AGENT_DATA_DIR = Path(os.getenv('PHANTOM_AGENT_DATA_DIR', PROJECT_ROOT / "experiments" / "agents" / "collected_data"))
try:
SEPARABILITY_ARTIFACTS = load_artifacts()
except FileNotFoundError:
SEPARABILITY_ARTIFACTS = None
def remap_schema(df: pd.DataFrame, mapping: dict, on: str = "event_type") -> pd.DataFrame:
"""remap column values according to mapping dict, preserving unmapped values"""
df = df.copy()
df[on] = df[on].map(mapping).fillna(df[on])
return df
def _states_to_events(states: list[str]) -> list[SimpleNamespace]:
events: list[SimpleNamespace] = []
for idx, state in enumerate(states):
parts = state.split("|") if isinstance(state, str) else ["page", "product", str(state)]
page = f"/{parts[0]}" if parts else "/"
product = parts[1] if len(parts) > 1 else "unknown"
event_name = parts[2] if len(parts) > 2 else parts[-1]
events.append(
SimpleNamespace(
eventName=event_name,
page=page,
productId=product,
ts=float(idx),
)
)
return events
def contaminate_dataset(df: pd.DataFrame, on: str = "event_type",
contamination_rate: float = 0.1,
agent_data_dir: Path = None) -> pd.DataFrame:
"""inject synthetic agent trajectories into a dataset
contamination_rate: fraction of final dataset that should be agent data (0.1 = 10% agents)
"""
data_dir = agent_data_dir or AGENT_DATA_DIR
model = AgentBehaviorModel(str(data_dir))
model.build_MDP() # ensure MDP is built before sampling
# compute event distribution from original data
event_dist = df[on].value_counts(normalize=True).to_dict()
total = sum(event_dist.values())
event_dist = {k: v / total for k, v in event_dist.items()}
# calculate how many synthetic events to add
N = len(df)
N_final = N / (1 - contamination_rate)
N_contaminate = int(N_final - N)
# sample start states weighted by original distribution
start_events = random.choices(list(event_dist.keys()), weights=list(event_dist.values()), k=N_contaminate)
# generate synthetic trajectories
new_rows = []
alpha_estimates = []
for start_event in start_events:
# sample trajectory from agent model, using a state that contains the event type
mdp_states = model.mdp.get('states', []) if model.mdp else []
matching_starts = [s for s in mdp_states if start_event in s]
if not matching_starts:
continue # skip if no matching start state
start_state = random.choice(matching_starts)
trajectory = model.sample_traj(start_state, max_len=20)
score_payload: list[SimpleNamespace] = []
score: dict[str, float] = {}
if SEPARABILITY_ARTIFACTS:
score_payload = _states_to_events(trajectory)
score = score_session(score_payload, SEPARABILITY_ARTIFACTS)
alpha_estimates.append(
estimate_alpha(score["prob_agent"], score["delta_h"], score["delta_a"], temperature=2.0)
)
for state in trajectory:
parts = state.split('|') if isinstance(state, str) else [start_event]
new_rows.append({
on: parts[-1] if parts else start_event,
'source': 'synthetic_agent',
'prob_agent': score.get('prob_agent') if SEPARABILITY_ARTIFACTS and score_payload else None,
'delta_h': score.get('delta_h') if SEPARABILITY_ARTIFACTS and score_payload else None,
'delta_a': score.get('delta_a') if SEPARABILITY_ARTIFACTS and score_payload else None,
})
if new_rows:
contaminate_df = pd.DataFrame(new_rows)
df = pd.concat([df, contaminate_df], ignore_index=True)
if alpha_estimates:
df['estimated_alpha'] = sum(alpha_estimates) / len(alpha_estimates)
return df

View File

@@ -0,0 +1,34 @@
from typing import Any, Dict
import pandas as pd
from procesing.providers.base import DataProvider
class PipelineContext:
"""
Context for pipeline execution holding config, provider, and cached data.
Enables dependency injection and eliminates global state.
"""
def __init__(self,
provider: DataProvider,
store_mode: str,
window_size: str = '30s',
**config):
self.provider = provider
self.store_mode = store_mode
self.window_size = window_size
self.config = config
self._cache: Dict[str, Any] = {}
def get_cached(self, key: str, default=None):
return self._cache.get(key, default)
def cache(self, key: str, value):
self._cache[key] = value
return value
@property
def products(self) -> pd.DataFrame:
"""Lazy-load and cache product catalog, single fetch per pipeline run"""
if 'products' not in self._cache:
self._cache['products'] = self.provider.fetch_products(self.store_mode)
return self._cache['products']

View File

@@ -0,0 +1,332 @@
import numpy as np
import pandas as pd
from typing import List, Dict, Optional
from sklearn.base import BaseEstimator, TransformerMixin
from supabase import create_client, Client
import os
SUPABASE_URL = os.getenv("NEXT_PUBLIC_SUPABASE_URL", "")
SUPABASE_KEY = os.getenv("NEXT_PUBLIC_SUPABASE_ANON_KEY", "")
supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
class TemporalElasticityEstimator(BaseEstimator, TransformerMixin):
"""
Compute price elasticity from time-series demand and price data.
Elasticity = (% change in quantity) / (% change in price)
Works with chunked time-window data from ChunkInteractionsIntoSteps.
"""
def __init__(self,
method:str='point',
min_observations:int=2,
smooth_window:Optional[int]=None):
"""
Args:
method: 'point' (point elasticity) or 'arc' (arc elasticity)
min_observations: min data points needed per product
smooth_window: if set, apply rolling avg smoothing to time series
"""
self.method = method
self.min_observations = min_observations
self.smooth_window = smooth_window
def fit(self, X):
return self
def transform(self,
demand_chunks: List[Dict],
price_chunks: List[Dict],
store_mode: str = 'hotel') -> pd.DataFrame:
"""
Args:
demand_chunks: list from ChunkInteractionsIntoSteps + DemandEstimator
each item: {'window_start', 'window_end', 'demand_vector'}
price_chunks: list of dicts with {'window_start', 'window_end', 'price_vector'}
store_mode: 'hotel' or 'airline' to fetch all products
Returns:
df with [productId, elasticity, std_error, n_observations]
"""
# fetch all products from database
all_products = supabase.table(f'{store_mode}_products').select("id").execute()
all_product_ids = [p['id'] for p in all_products.data]
aligned = self._align_chunks(demand_chunks, price_chunks)
if not aligned:
# return all products with zero elasticity
return pd.DataFrame({
'productId': all_product_ids,
'elasticity': 0.0,
'std_error': 0.0,
'n_obs': 0
})
# build time series per product
product_series = self._build_product_timeseries(aligned)
# compute elasticity per product
elasticities = []
for pid, series in product_series.items():
if len(series) < self.min_observations:
# assign 0 elasticity for products with insufficient data
elasticities.append({
'productId': pid,
'elasticity': 0.0,
'std_error': 0.0,
'n_obs': len(series)
})
continue
# apply smoothing if requested
if self.smooth_window and len(series) >= self.smooth_window:
series = self._smooth_series(series, self.smooth_window)
elast = self._compute_elasticity(series)
elasticities.append({
'productId': pid,
'elasticity': elast['value'],
'std_error': elast.get('std_error', 0.0),
'n_obs': len(series)
})
result_df = pd.DataFrame(elasticities)
# fill in missing products with zero elasticity
observed_pids = set(result_df['productId'].unique())
missing_pids = [pid for pid in all_product_ids if pid not in observed_pids]
if missing_pids:
missing_df = pd.DataFrame({
'productId': missing_pids,
'elasticity': 0.0,
'std_error': 0.0,
'n_obs': 0
})
result_df = pd.concat([result_df, missing_df], ignore_index=True)
return result_df
def _align_chunks(self, demand_chunks, price_chunks):
"""Align demand and price data by matching time windows."""
aligned = []
# create lookup for price chunks by window_start
price_lookup = {chunk['window_start']: chunk for chunk in price_chunks}
for demand_chunk in demand_chunks:
window_start = demand_chunk['window_start']
if window_start in price_lookup:
aligned.append({
'window_start': window_start,
'window_end': demand_chunk['window_end'],
'demand': demand_chunk['demand_vector'],
'prices': price_lookup[window_start]['price_vector']
})
return aligned
def _build_product_timeseries(self, aligned_chunks):
"""Build time series [price, quantity] per product."""
# vectorize chunk merging instead of iterating rows
all_merged = []
for chunk in aligned_chunks:
merged = chunk['demand'].merge(chunk['prices'], on='productId', how='inner')
merged['timestamp'] = chunk['window_start']
all_merged.append(merged[['productId', 'timestamp', 'price', 'demand_score']])
if not all_merged:
return {}
# concat all chunks and group by productId in one pass
combined = pd.concat(all_merged, ignore_index=True)
series_by_product = {
pid: group[['timestamp', 'price', 'demand_score']].rename(
columns={'demand_score': 'quantity'}
).to_dict('records')
for pid, group in combined.groupby('productId')
}
return series_by_product
def _smooth_series(self, series, window):
"""Apply rolling average smoothing."""
df = pd.DataFrame(series)
df['price_smooth'] = df['price'].rolling(window=window, center=True).mean()
df['quantity_smooth'] = df['quantity'].rolling(window=window, center=True).mean()
df = df.dropna()
return [{'timestamp': row['timestamp'],
'price': row['price_smooth'],
'quantity': row['quantity_smooth']}
for _, row in df.iterrows()]
def _compute_elasticity(self, series):
"""Compute elasticity from time series."""
if len(series) < 2:
return {'value': 0.0, 'std_error': 0.0}
prices = np.array([s['price'] for s in series])
quantities = np.array([s['quantity'] for s in series])
# filter out zero/negative values
valid = (prices > 0) & (quantities > 0)
if valid.sum() < 2:
return {'value': 0.0, 'std_error': 0.0}
prices = prices[valid]
quantities = quantities[valid]
if self.method == 'point':
return self._point_elasticity(prices, quantities)
elif self.method == 'arc':
return self._arc_elasticity(prices, quantities)
else:
raise ValueError(f"Unknown method: {self.method}")
def _point_elasticity(self, prices, quantities):
"""
Point elasticity using log-log regression.
log(Q) = a + b*log(P), elasticity = b
"""
if len(prices) < 2:
return {'value': 0.0, 'std_error': 0.0}
log_p = np.log(prices)
log_q = np.log(quantities)
# simple linear regression
if log_p.std() == 0:
return {'value': 0.0, 'std_error': 0.0}
cov = np.cov(log_p, log_q)[0, 1]
var = np.var(log_p)
b = cov / var
# std error estimate (avoid div by zero)
if len(prices) <= 2:
se_b = 0.0
else:
residuals = log_q - (log_q.mean() + b * (log_p - log_p.mean()))
mse = (residuals ** 2).sum() / (len(prices) - 2)
se_b = np.sqrt(mse / (len(prices) * var))
return {'value': b, 'std_error': se_b}
def _arc_elasticity(self, prices, quantities):
"""
Arc elasticity: average of period-over-period elasticities.
E_t = (ΔQ/Q_avg) / (ΔP/P_avg)
"""
elasticities = []
for i in range(1, len(prices)):
p1, p2 = prices[i-1], prices[i]
q1, q2 = quantities[i-1], quantities[i]
p_avg = (p1 + p2) / 2
q_avg = (q1 + q2) / 2
if p_avg == 0 or q_avg == 0:
continue
delta_p = p2 - p1
delta_q = q2 - q1
if delta_p == 0:
continue
e = (delta_q / q_avg) / (delta_p / p_avg)
elasticities.append(e)
if not elasticities:
return None
return {
'value': np.mean(elasticities),
'std_error': np.std(elasticities) / np.sqrt(len(elasticities))
}
def aggregate_price_logs(price_logs: pd.DataFrame,
window_size: str = '1H',
ts_col: str = 'ts',
store_mode : str = 'hotel') -> List[Dict]:
"""
Recover price vectors treating prices as persistent state changes.
Prices are set-operations that persist until next change. For each window:
- If price logs exist: average all changes within window
- If no logs: carry forward last price before window end
Args:
price_logs: df with [productId, price, ts, ...]
window_size: time window size matching ChunkInteractionsIntoSteps
ts_col: timestamp column name
Returns:
list of dicts with {'window_start', 'window_end', 'price_vector'}
where price_vector is df with [productId, price]
"""
if price_logs.empty:
return []
df = price_logs.copy()
if not pd.api.types.is_datetime64_any_dtype(df[ts_col]):
df[ts_col] = pd.to_datetime(df[ts_col])
df = df.sort_values([ts_col, 'productId'])
all_products=supabase.table(f'{store_mode}_products').select("id, room_type, date_index, metadata, availability").execute()
all_products = pd.DataFrame(all_products.data)
unique_products = all_products['id'].unique()
# generate windows across data range
min_time, max_time = df[ts_col].min(), df[ts_col].max()
windows = pd.date_range(
start=min_time.floor(window_size),
end=max_time,
freq=window_size
)
chunks = []
for window_start in windows:
window_end = window_start + pd.Timedelta(window_size)
price_vector = []
# all products with price history by window_end
#historical_products = df[df[ts_col] < window_end]['productId'].unique()
historical_products = unique_products.tolist()
for pid in historical_products:
product_data = df[df['productId'] == pid]
# logs within window
in_window = product_data[
(product_data[ts_col] >= window_start) &
(product_data[ts_col] < window_end)
]
if not in_window.empty:
# average changes within window
price = in_window['price'].mean()
else:
# carry forward: last price before window end
before_window = product_data[product_data[ts_col] < window_end]
if before_window.empty:
continue
price = before_window['price'].iloc[-1]
price_vector.append({'productId': pid, 'price': price})
if price_vector:
chunks.append({
'window_start': window_start,
'window_end': window_end,
'price_vector': pd.DataFrame(price_vector)
})
return chunks

View File

@@ -0,0 +1,245 @@
"""
Revenue and KPI benchmark framework for pricing strategies.
Computes session-level and aggregate metrics to compare pricing functions:
- Revenue: R_T = Σ P_t^T · Q_t
- Conversion rate
- Average order value (AOV)
- Agent exploitation loss: L_agent = R_oracle - R_observed
"""
from typing import Dict, List, Any, Optional
from dataclasses import dataclass, field, asdict
import pandas as pd
import numpy as np
@dataclass
class SessionMetrics:
"""KPIs for single session."""
session_id: str
experiment_id: Optional[str] = None
# interaction metrics
total_interactions: int = 0
page_views: int = 0
item_views: int = 0
searches: int = 0
cart_adds: int = 0
# revenue metrics
items_purchased: int = 0
total_revenue: float = 0.0
avg_item_price: float = 0.0
conversion_rate: float = 0.0
# pricing signals
total_price_shown: float = 0.0 # sum of all prices displayed
avg_markup: float = 0.0 # avg (price / base_price)
# behavioral features (for agent detection)
interaction_velocity: float = 0.0 # interactions per minute
session_duration_sec: float = 0.0
unique_products_viewed: int = 0
metadata: Dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> Dict[str, Any]:
return asdict(self)
@dataclass
class AggregateMetrics:
"""Aggregate KPIs across sessions/experiments."""
experiment_id: Optional[str] = None
n_sessions: int = 0
# revenue aggregates
total_revenue: float = 0.0
avg_revenue_per_session: float = 0.0
median_revenue_per_session: float = 0.0
# conversion aggregates
total_conversions: int = 0
conversion_rate: float = 0.0 # purchases / sessions
# pricing aggregates
avg_markup: float = 0.0
median_markup: float = 0.0
# agent exploitation metrics
estimated_agent_sessions: int = 0 # sessions flagged as agent-driven
agent_revenue: float = 0.0
human_revenue: float = 0.0
agent_loss: float = 0.0 # L_agent = R_oracle - R_observed (if available)
def to_dict(self) -> Dict[str, Any]:
return asdict(self)
class MetricsComputer:
"""Compute session and aggregate metrics from interaction/price logs."""
@staticmethod
def compute_session_metrics(
session_id: str,
interactions: pd.DataFrame,
price_logs: pd.DataFrame,
purchases: Optional[pd.DataFrame] = None,
experiment_id: Optional[str] = None
) -> SessionMetrics:
"""
Compute metrics for single session.
Args:
session_id: session identifier
interactions: user-interactions events for this session
price_logs: price-logs for this session
purchases: purchase events (if available)
experiment_id: experiment identifier
"""
metrics = SessionMetrics(session_id=session_id, experiment_id=experiment_id)
if interactions.empty:
return metrics
# interaction counts
event_counts = interactions['eventName'].value_counts().to_dict()
metrics.total_interactions = len(interactions)
metrics.page_views = event_counts.get('page_view', 0) + event_counts.get('view_item_page', 0)
metrics.item_views = event_counts.get('view_item_page', 0)
metrics.searches = event_counts.get('search', 0)
metrics.cart_adds = event_counts.get('add_item_to_cart', 0)
# unique products viewed
metrics.unique_products_viewed = interactions['productId'].dropna().nunique()
# session duration
if 'ts' in interactions.columns:
timestamps = pd.to_datetime(interactions['ts'])
metrics.session_duration_sec = (timestamps.max() - timestamps.min()).total_seconds()
if metrics.session_duration_sec > 0:
metrics.interaction_velocity = (metrics.total_interactions / metrics.session_duration_sec) * 60
# revenue from purchases
if purchases is not None and not purchases.empty:
metrics.items_purchased = len(purchases)
metrics.total_revenue = purchases['price'].sum() if 'price' in purchases.columns else 0.0
metrics.avg_item_price = metrics.total_revenue / metrics.items_purchased if metrics.items_purchased > 0 else 0.0
metrics.conversion_rate = 1.0 if metrics.items_purchased > 0 else 0.0
# pricing metrics
if not price_logs.empty:
metrics.total_price_shown = price_logs['price'].sum()
# compute markup if base_price available in price logs or join with product catalog
if 'base_price' in price_logs.columns:
valid_markup = price_logs[price_logs['base_price'] > 0]
if not valid_markup.empty:
metrics.avg_markup = (valid_markup['price'] / valid_markup['base_price']).mean()
return metrics
@staticmethod
def compute_aggregate_metrics(
session_metrics_list: List[SessionMetrics],
experiment_id: Optional[str] = None,
agent_detector_fn: Optional[callable] = None
) -> AggregateMetrics:
"""
Aggregate metrics across sessions.
Args:
session_metrics_list: list of SessionMetrics
experiment_id: experiment identifier
agent_detector_fn: optional function to classify session as agent (returns bool)
"""
agg = AggregateMetrics(experiment_id=experiment_id)
agg.n_sessions = len(session_metrics_list)
if agg.n_sessions == 0:
return agg
df = pd.DataFrame([m.to_dict() for m in session_metrics_list])
# revenue aggregates
agg.total_revenue = df['total_revenue'].sum()
agg.avg_revenue_per_session = df['total_revenue'].mean()
agg.median_revenue_per_session = df['total_revenue'].median()
# conversion aggregates
agg.total_conversions = (df['items_purchased'] > 0).sum()
agg.conversion_rate = agg.total_conversions / agg.n_sessions
# pricing aggregates
valid_markups = df[df['avg_markup'] > 0]
if not valid_markups.empty:
agg.avg_markup = valid_markups['avg_markup'].mean()
agg.median_markup = valid_markups['avg_markup'].median()
# agent detection (if detector provided)
if agent_detector_fn is not None:
agent_flags = [agent_detector_fn(m) for m in session_metrics_list]
agg.estimated_agent_sessions = sum(agent_flags)
agent_revenue = sum(m.total_revenue for m, is_agent in zip(session_metrics_list, agent_flags) if is_agent)
human_revenue = sum(m.total_revenue for m, is_agent in zip(session_metrics_list, agent_flags) if not is_agent)
agg.agent_revenue = agent_revenue
agg.human_revenue = human_revenue
return agg
@staticmethod
def compare_pricing_strategies(
experiments: Dict[str, List[SessionMetrics]],
baseline_experiment_id: Optional[str] = None
) -> pd.DataFrame:
"""
Compare multiple pricing strategies/experiments.
Args:
experiments: dict mapping experiment_id -> list of SessionMetrics
baseline_experiment_id: experiment to use as baseline for comparison
Returns:
DataFrame with comparative metrics
"""
results = []
baseline_agg = None
for exp_id, session_metrics in experiments.items():
agg = MetricsComputer.compute_aggregate_metrics(session_metrics, experiment_id=exp_id)
result = agg.to_dict()
if exp_id == baseline_experiment_id:
baseline_agg = agg
results.append(result)
df = pd.DataFrame(results)
# add relative metrics if baseline exists
if baseline_agg is not None:
df['revenue_lift_pct'] = ((df['total_revenue'] - baseline_agg.total_revenue) / baseline_agg.total_revenue * 100)
df['conversion_lift_pct'] = ((df['conversion_rate'] - baseline_agg.conversion_rate) / baseline_agg.conversion_rate * 100)
return df
def simple_agent_detector(session_metrics: SessionMetrics, velocity_threshold: float = 5.0) -> bool:
"""
Simple heuristic agent detector based on interaction velocity.
Args:
session_metrics: SessionMetrics instance
velocity_threshold: interactions per minute threshold (default: 5.0)
Returns:
True if session likely agent-driven
"""
# agents tend to have higher interaction velocity and lower session duration
if session_metrics.interaction_velocity > velocity_threshold:
return True
# agents often view many products quickly without converting
if session_metrics.unique_products_viewed > 10 and session_metrics.conversion_rate == 0:
return True
return False

View File

@@ -0,0 +1,174 @@
from sklearn.pipeline import Pipeline
import pandas as pd
from procesing.context import PipelineContext
from procesing.providers import SupabaseProvider, BackendAPIProvider
import os
from procesing.steps import (
FetchInteractionsStep,
FetchPriceLogsStep,
FetchExperimentsStep,
JoinExperimentsStep,
CreatePriceBucketsStep,
AugmentEventNamesStep,
ChunkByTimeWindowStep,
ComputeDemandForChunksStep,
AggregatePriceLogsStep,
FitPricingFunctionStep,
PredictPricesStep,
ComputeDemandStep,
JoinProductFeaturesStep,
ExtractSessionFeaturesStep,
JoinLabelsStep,
ValidateDataStep,
)
from procesing.pricers import SimpleSurgePricer
def interaction_extraction_pipeline(context: PipelineContext):
"""Pipeline for extracting and augmenting interaction data"""
return Pipeline([
('fetch', FetchInteractionsStep(context)),
('create_buckets', CreatePriceBucketsStep(context)),
('augment_events', AugmentEventNamesStep(context)),
])
def price_extraction_pipeline(context: PipelineContext):
"""Pipeline for extracting price logs"""
return Pipeline([
('fetch', FetchPriceLogsStep(context)),
])
def product_features_pipeline(context: PipelineContext,
interactions_df: pd.DataFrame,
price_logs_df: pd.DataFrame):
demand_step = ComputeDemandStep(context)
price_step = AggregatePriceLogsStep(context)
join_step = JoinProductFeaturesStep(context)
demand_data = demand_step.transform(interactions_df)
price_data= price_step.transform(price_logs_df)
joined_data = join_step.transform((demand_data, price_data))
return joined_data
def pricing_pipeline(context: "PipelineContext",
data: pd.DataFrame,
high_threshold: int = 10,
low_threshold: int = 2,
surge_multiplier: float = 1.2,
discount_multiplier: float = 0.9) -> pd.DataFrame:
if data.empty or 'productId' not in data.columns:
return pd.DataFrame()
surge_pricer = SimpleSurgePricer()
surge_pricer.fit(data)
data['optimal_price'] = surge_pricer.predict()
return data
def full_pipeline(context: PipelineContext,
high_threshold: int = 10,
low_threshold: int = 2,
surge_multiplier: float = 1.2,
discount_multiplier: float = 0.9):
"""
Complete end-to-end pipeline: data extraction -> demand/price aggregation -> surge pricing
Args:
context: Pipeline context
high_threshold: Demand threshold for surge pricing
low_threshold: Demand threshold for discounts
surge_multiplier: Price multiplier for high demand
discount_multiplier: Price multiplier for low demand
Returns:
tuple: (product_features_df, optimal_prices_df)
- product_features_df: [productId, demand_score, price]
- optimal_prices_df: [productId, current_price, optimal_price, demand_score]
"""
interaction_pipe = interaction_extraction_pipeline(context)
price_pipe = price_extraction_pipeline(context)
interactions_df = interaction_pipe.fit_transform(None)
price_logs_df = price_pipe.fit_transform(None)
product_features_df = product_features_pipeline(context, interactions_df, price_logs_df)
print(product_features_df.to_string())
# generate optimal prices using surge rules
optimal_prices_df = pricing_pipeline(context, product_features_df,
high_threshold=high_threshold,
low_threshold=low_threshold,
surge_multiplier=surge_multiplier,
discount_multiplier=discount_multiplier)
return product_features_df, optimal_prices_df
def ml_training_pipeline(context: PipelineContext) -> pd.DataFrame:
"""
Build labeled session-level feature matrix for ML model training.
Pipeline: fetch -> validate -> extract features -> join labels
Returns:
DataFrame with ~25 features per session + is_agent label
Columns: sessionId, experimentId, temporal/behavioral/product/ua features, is_agent
"""
# fetch raw interactions
interactions_df = FetchInteractionsStep(context).transform(None)
# validate data quality (report cached in context)
interactions_df = ValidateDataStep(context).transform(interactions_df)
if interactions_df.empty:
return pd.DataFrame()
# extract vectorized session features
features_df = ExtractSessionFeaturesStep(context).transform(interactions_df)
if features_df.empty:
return pd.DataFrame()
# join experiment labels (is_agent = ~xp_human_only)
labeled_df = JoinLabelsStep(context).transform(features_df)
return labeled_df
if __name__ == '__main__':
class ExperimentsProvider(SupabaseProvider, BackendAPIProvider):
def fetch_kafka_topic(self, topic: str) -> pd.DataFrame:
base_path = "/home/velocitatem/Documents/Projects/PHANTOM/experiments/collected_data/" # os.path.join(os.path.dirname(__file__), "collected_data")
if not os.path.isdir(base_path):
return pd.DataFrame()
files = {"user-interactions": "int.json", "price-logs": "price.json"}
file_to_read = files.get(topic, files["user-interactions"])
frames = []
for d in os.listdir(base_path):
full_path = os.path.join(base_path, d, file_to_read)
if not os.path.isfile(full_path):
continue
try:
data = pd.read_json(full_path)
payloads = pd.DataFrame([r['payload'] for r in data['value'].to_list()])
frames.append(payloads)
except Exception as e:
print(f"Warning: Could not process {full_path}: {e}")
return pd.concat(frames, ignore_index=True) if frames else pd.DataFrame()
# demo: run ML training pipeline
context = PipelineContext(provider=ExperimentsProvider(), store_mode='hotel')
features = ml_training_pipeline(context)
print(f"Feature matrix: {features.shape}")
print(features.head())
print(features.info())
features.to_parquet("features.parquet")

View File

@@ -0,0 +1,14 @@
from procesing.pricers.base import PricingFunction
from procesing.pricers.elasticity import ElasticityBasedPricer
from procesing.pricers.simple import StaticPricer, RandomPricer, SimpleSurgePricer
from procesing.pricers.session_aware import SessionAwarePricer, ProductSpecificSessionPricer
__all__ = [
'PricingFunction',
'ElasticityBasedPricer',
'StaticPricer',
'RandomPricer',
'SimpleSurgePricer',
'SessionAwarePricer',
'ProductSpecificSessionPricer'
]

View File

@@ -0,0 +1,67 @@
from abc import ABC, abstractmethod
from typing import Optional, Dict, Any, List
import numpy as np
import pandas as pd
class PricingFunction(ABC):
"""
Abstract base for pricing functions.
Objective:
maximize E[R_T] = E[Σ P_t^T · Q_t]
subject to:
Q_t = g(P_t, S_t) (demand response via elasticity)
P_t ≥ C (cost floor)
minimize L_agent = R_oracle - R_observed
"""
@abstractmethod
def fit(self, *kwargs):
"""
Offline training on historical data.
This is where we can think about some maximization of expected revenue
over historical trajectories to learn parameters of the pricing function.
(This however we cover move in the RL side of things)
"""
pass
@abstractmethod
def predict(self, *kwargs) -> np.ndarray:
"""
Generate optimal prices given current state.
This is an abstract method that transitions from τ -> P*
which is the mapping from the trajectory to optimal prices under
some subset of session grouping (so, per sessionId)
"""
pass
@abstractmethod
def _get_features(self, *kwargs) -> np.ndarray:
"""
Extract features from trajectory for pricing decision.
Returns:
np.ndarray of shape (n_products, n_features)
"""
pass
def update(self, observation: Dict[str, Any]):
"""
Online learning update (optional).
Args:
observation: dict with {state, action, reward, next_state}
- state: StateSpace before pricing decision
- action: prices shown (P_t)
- reward: revenue/conversion signal
- next_state: StateSpace after user interaction
"""
pass # default: no online learning
def get_params(self) -> Dict[str, Any]:
"""Return pricing function parameters for serialization."""
return {}
def set_params(self, params: Dict[str, Any]):
"""Load pricing function parameters from dict."""
pass

View File

@@ -0,0 +1,69 @@
import numpy as np
import pandas as pd
from procesing.pricers.base import PricingFunction
class ElasticityBasedPricer(PricingFunction):
"""
Pricing based on demand elasticity estimates.
f(Q, S) = base_price * (1 + alpha * elasticity * demand_deviation)
"""
def __init__(self, alpha: float = 0.1, price_floor: float = 0.0, price_ceil: float = np.inf):
self.alpha = alpha
self.price_floor = price_floor
self.price_ceil = price_ceil
self.elasticity = None
self.base_prices = None
self.mean_demand = None
def fit(self, historical_data: pd.DataFrame):
"""
Calibrate from historical elasticity estimates.
Expects: [productId, elasticity, base_price, mean_demand]
"""
if 'elasticity' not in historical_data.columns:
raise ValueError("historical_data must contain 'elasticity' column")
self.elasticity = historical_data['elasticity'].values
self.base_prices = (historical_data['base_price'].values
if 'base_price' in historical_data.columns
else np.ones(len(historical_data)) * 100)
self.mean_demand = (historical_data['mean_demand'].values
if 'mean_demand' in historical_data.columns
else np.ones(len(historical_data)) * 10)
return self
def predict(self, state_space) -> np.ndarray:
"""
Adjust prices based on demand deviation and elasticity.
Higher demand -> increase price (but less for elastic goods)
"""
if self.elasticity is None:
raise ValueError("Must call fit() before predict()")
demand = np.asarray(state_space.demand)
if len(demand) != len(self.elasticity):
raise ValueError(f"Demand vector size {len(demand)} != elasticity size {len(self.elasticity)}")
# compute demand deviation from mean
demand_dev = (demand - self.mean_demand) / (self.mean_demand + 1e-6)
# adjust price: if demand high and elastic, don't increase much
# if demand high and inelastic, increase more
price_multiplier = 1 + self.alpha * np.abs(self.elasticity) * demand_dev
prices = self.base_prices * price_multiplier
# enforce bounds
prices = np.clip(prices, self.price_floor, self.price_ceil)
return prices
def _get_features(self, state_space=None) -> np.ndarray:
"""Extract elasticity, demand, and demand deviation for each product"""
if state_space is None or self.elasticity is None:
n = len(self.elasticity) if self.elasticity is not None else 0
return np.zeros((n, 3))
demand = np.asarray(state_space.demand)
demand_dev = (demand - self.mean_demand) / (self.mean_demand + 1e-6)
return np.column_stack([self.elasticity, demand, demand_dev])

View File

@@ -0,0 +1,211 @@
"""
Session-aware pricing functions that leverage behavioral features S_t.
These pricers aim to minimize L_agent = R_oracle - R_observed.
"""
import numpy as np
import pandas as pd
from procesing.pricers.base import PricingFunction
from procesing.pricers.elasticity import ElasticityBasedPricer
class SessionAwarePricer(PricingFunction):
"""
Extends elasticity-based pricing with session behavioral signals.
f(Q, P, S) = base_price * elasticity_factor * session_factor
Where session_factor adjusts for:
- interaction_velocity (agent detection proxy)
- product_view_depth (interest signal)
- cart_to_view_ratio (conversion intent)
Strategy: charge higher prices to suspected agents (high velocity)
to recover oracle revenue from reconnaissance sessions.
"""
def __init__(self,
alpha: float = 0.1,
beta_velocity: float = 0.05,
beta_attention: float = 0.03,
agent_velocity_threshold: float = 5.0,
agent_markup: float = 1.2,
price_floor: float = 0.0,
price_ceil: float = np.inf):
"""
Args:
alpha: elasticity sensitivity
beta_velocity: interaction velocity weight
beta_attention: product attention weight
agent_velocity_threshold: velocity above which to apply agent markup
agent_markup: price multiplier for suspected agent sessions
price_floor, price_ceil: price bounds
"""
self.alpha = alpha
self.beta_velocity = beta_velocity
self.beta_attention = beta_attention
self.agent_velocity_threshold = agent_velocity_threshold
self.agent_markup = agent_markup
self.price_floor = price_floor
self.price_ceil = price_ceil
# fitted parameters
self.elasticity = None
self.base_prices = None
self.mean_demand = None
def fit(self, historical_data: pd.DataFrame, **kwargs):
"""Calibrate from historical elasticity data."""
if 'elasticity' not in historical_data.columns:
raise ValueError("historical_data must contain 'elasticity'")
self.elasticity = historical_data['elasticity'].values
self.base_prices = (historical_data['base_price'].values
if 'base_price' in historical_data.columns
else np.ones(len(historical_data)) * 100)
self.mean_demand = (historical_data['mean_demand'].values
if 'mean_demand' in historical_data.columns
else np.ones(len(historical_data)) * 10)
return self
def predict(self, state_space) -> np.ndarray:
"""Generate prices with session awareness."""
if self.elasticity is None:
raise ValueError("Must call fit() before predict()")
demand = np.asarray(state_space.demand)
n_products = len(demand)
# base elasticity-driven pricing
demand_dev = (demand - self.mean_demand) / (self.mean_demand + 1e-6)
elasticity_factor = 1 + self.alpha * np.abs(self.elasticity) * demand_dev
# session-aware adjustments
session_factor = np.ones(n_products)
if not state_space.session_features.empty:
sf = state_space.session_features.iloc[0] # single session features
# agent detection via velocity
velocity = sf.get('interaction_velocity', 0.0)
if velocity > self.agent_velocity_threshold:
# suspected agent: apply markup to recover oracle revenue
session_factor *= self.agent_markup
# attention signal: higher view depth -> user interested -> can charge more
view_depth = sf.get('product_view_depth', 0)
if view_depth > 0:
attention_boost = 1 + self.beta_attention * np.log1p(view_depth)
session_factor *= attention_boost
# cart presence: if user has items in cart, slightly increase prices
cart_to_view = sf.get('cart_to_view_ratio', 0.0)
if cart_to_view > 0.1:
session_factor *= (1 + 0.02) # small boost for conversion intent
prices = self.base_prices * elasticity_factor * session_factor
prices = np.clip(prices, self.price_floor, self.price_ceil)
return prices
def _get_features(self, state_space=None) -> np.ndarray:
"""Extract elasticity, demand, and session features"""
if state_space is None or self.elasticity is None:
n = len(self.elasticity) if self.elasticity is not None else 0
return np.zeros((n, 5))
demand = np.asarray(state_space.demand)
n_products = len(demand)
# extract session features
velocity = 0.0
view_depth = 0.0
cart_to_view = 0.0
if not state_space.session_features.empty:
sf = state_space.session_features.iloc[0]
velocity = sf.get('interaction_velocity', 0.0)
view_depth = sf.get('product_view_depth', 0.0)
cart_to_view = sf.get('cart_to_view_ratio', 0.0)
# broadcast session features to all products
features = np.column_stack([
self.elasticity,
demand,
np.full(n_products, velocity),
np.full(n_products, view_depth),
np.full(n_products, cart_to_view)
])
return features
class ProductSpecificSessionPricer(PricingFunction):
"""
Session-aware pricer with product-specific demand signals.
Uses S_t to extract per-product interaction counts and adjusts pricing
for products the user has already viewed/hovered.
Strategy: products viewed multiple times = high interest -> price up
"""
def __init__(self,
alpha: float = 0.1,
view_boost: float = 0.02,
max_view_boost: float = 0.15,
price_floor: float = 0.0,
price_ceil: float = np.inf):
self.alpha = alpha
self.view_boost = view_boost
self.max_view_boost = max_view_boost
self.price_floor = price_floor
self.price_ceil = price_ceil
self.elasticity = None
self.base_prices = None
self.mean_demand = None
self.product_ids = None
def fit(self, historical_data: pd.DataFrame, **kwargs):
if 'elasticity' not in historical_data.columns or 'productId' not in historical_data.columns:
raise ValueError("historical_data must contain 'elasticity' and 'productId'")
self.elasticity = historical_data['elasticity'].values
self.base_prices = (historical_data['base_price'].values
if 'base_price' in historical_data.columns
else np.ones(len(historical_data)) * 100)
self.mean_demand = (historical_data['mean_demand'].values
if 'mean_demand' in historical_data.columns
else np.ones(len(historical_data)) * 10)
self.product_ids = historical_data['productId'].values
return self
def predict(self, state_space) -> np.ndarray:
if self.elasticity is None:
raise ValueError("Must call fit() before predict()")
demand = np.asarray(state_space.demand)
n_products = len(demand)
# base pricing
demand_dev = (demand - self.mean_demand) / (self.mean_demand + 1e-6)
base_prices = self.base_prices * (1 + self.alpha * np.abs(self.elasticity) * demand_dev)
# product-specific session adjustments
if not state_space.session_features.empty and state_space.product_ids is not None:
# extract product interaction counts from session metadata
# (this would require session features to include per-product signals)
# for now, use uniform boost as placeholder
# TODO: extend session feature extraction to include product-specific counts
pass
prices = np.clip(base_prices, self.price_floor, self.price_ceil)
return prices
def _get_features(self, state_space=None) -> np.ndarray:
"""Extract elasticity and demand features for product-specific pricing"""
if state_space is None or self.elasticity is None:
n = len(self.elasticity) if self.elasticity is not None else 0
return np.zeros((n, 2))
demand = np.asarray(state_space.demand)
return np.column_stack([self.elasticity, demand])

View File

@@ -0,0 +1,158 @@
import numpy as np
import pandas as pd
from procesing.pricers.base import PricingFunction
def session_features_to_demand(session_features: pd.DataFrame) -> float:
"""
Map session behavioral features to demand proxy.
THIS is the critical θ̂ → D transformation for rule-based pricing.
Logic:
- High velocity → agent behavior → price up (revenue recovery)
- High cart ratio → purchase intent → price up
- Low activity → discount to convert
Returns: demand proxy score (0-20 range, higher = more demand)
"""
if session_features.empty:
return 1.0
feat = session_features.iloc[0] if len(session_features) > 0 else {}
velocity = feat.get('interaction_velocity', 0)
cart_ratio = feat.get('cart_to_view_ratio', 0)
item_views = feat.get('item_views', 0)
cart_adds = feat.get('cart_adds', 0)
# baseline demand
demand = 1.0
# agent detection: high velocity → treat as high "demand" to price up
if velocity > 2.0:
demand += 10.0 # strong agent signal
# conversion intent: cart interaction → price up
if cart_ratio > 0.1 or cart_adds > 0:
demand += 5.0
# browsing depth: many views → interest signal
if item_views > 3:
demand += min(item_views, 5.0)
return min(demand, 20.0) # cap at 20
class StaticPricer(PricingFunction):
"""Static pricing: always return fixed base prices"""
def __init__(self, base_prices: np.ndarray = None):
self.base_prices = base_prices
def fit(self, historical_data: pd.DataFrame):
"""Extract base prices from historical data"""
if 'base_price' in historical_data.columns:
self.base_prices = historical_data['base_price'].values
elif 'price' in historical_data.columns:
self.base_prices = historical_data['price'].values
else:
raise ValueError("historical_data must contain 'base_price' or 'price' column")
return self
def predict(self, state_space) -> np.ndarray:
"""Return static base prices regardless of state"""
if self.base_prices is None:
raise ValueError("Must call fit() or provide base_prices in constructor")
return self.base_prices.copy()
def _get_features(self, state_space=None) -> np.ndarray:
"""Static pricer uses no features, returns empty array"""
n = len(self.base_prices) if self.base_prices is not None else 0
return np.zeros((n, 0))
class RandomPricer(PricingFunction):
"""Random pricing within bounds (for baseline comparison)"""
def __init__(self, price_min: float = 50.0, price_max: float = 500.0, seed: int = None):
self.price_min = price_min
self.price_max = price_max
self.seed = seed
self.n_products = None
self.rng = np.random.default_rng(seed)
def fit(self, historical_data: pd.DataFrame):
"""Learn number of products"""
self.n_products = len(historical_data)
return self
def predict(self, state_space) -> np.ndarray:
"""Generate random prices"""
if self.n_products is None:
self.n_products = len(state_space.demand)
return self.rng.uniform(self.price_min, self.price_max, size=self.n_products)
def _get_features(self, state_space=None) -> np.ndarray:
"""Random pricer uses no features"""
n = self.n_products if self.n_products else 0
return np.zeros((n, 0))
class SimpleSurgePricer(PricingFunction):
"""
Rule-based surge pricer adjusting prices via demand thresholds.
Logic: if demand > high_threshold -> surge, if demand < low_threshold -> discount.
Simpler and more controllable than curve fitting approaches.
"""
def __init__(self,
base_prices: np.ndarray = None,
high_threshold: int = 10,
low_threshold: int = 2,
surge_multiplier: float = 1.2,
discount_multiplier: float = 0.9):
self.base_prices = base_prices
self.high_threshold = high_threshold
self.low_threshold = low_threshold
self.surge_multiplier = surge_multiplier
self.discount_multiplier = discount_multiplier
def fit(self, market_data: pd.DataFrame):
"""Extract base prices from product catalog or historical averages"""
self.base_prices = market_data['base_price'].to_numpy() if 'base_price' in market_data.columns else market_data['price'].values
return self
def predict(self, state_space) -> np.ndarray:
"""
Adjust prices based on current demand using surge rules.
state_space.demand: demand proxy per product (from session features)
state_space.prices: base prices
"""
demand = np.asarray(state_space.demand) if state_space and hasattr(state_space, 'demand') else np.array([0])
base = np.asarray(state_space.prices) if state_space and hasattr(state_space, 'prices') else self.base_prices
if base is None:
base = np.ones(len(demand)) * 99.99
# ensure float dtype to allow multiplication by float multipliers
new_prices = base.astype(np.float64).copy()
high_mask = demand >= self.high_threshold
new_prices[high_mask] *= self.surge_multiplier
low_mask = demand <= self.low_threshold
new_prices[low_mask] *= self.discount_multiplier
return new_prices
def _get_features(self, state_space=None) -> np.ndarray:
"""Extract demand and base price features for each product"""
if state_space is None:
n = len(self.base_prices) if self.base_prices is not None else 0
return np.zeros((n, 2))
demand = np.asarray(state_space.demand) if hasattr(state_space, 'demand') else np.array([0])
base = np.asarray(state_space.prices) if hasattr(state_space, 'prices') else self.base_prices
if base is None:
base = np.ones(len(demand)) * 99.99
return np.column_stack([demand, base])

View File

@@ -0,0 +1,272 @@
r"""
Our state space comes as:
$Q_t in R^n$ - our demand at a time t
$P_t in R^n$ - prices at time t
$S_t$ some form of interaction session features
This is a single sate which we map under
$f: (Q, S, H) \to P_{t+1}$
With:
$H_t = \{Q_{t-k}, P_{t-k}, S_{t-k}\}$
We can have f be literally anything, analytical or learned or rule based or an RL policy.
Our goal is to mazimize the expected revenue:
$E[R_T] = E[\sum_{t=1}^T P_t^T \dot Q_t]$
subject to Q_t = g(P_t, S_t) : demand response to price (estimated via elasticity) and P_t ≥ C : prices above cost floor and additionally minimizing the following:
$L_{agent} = R_{oracle} - R_{observed}
where: R_oracle = revenue if we knew agent intentions (from recon session) and R_observed = revenue under current pricing policy f
I would start be defning a pricing function interface and standardizing how to train that based on historical data and define how to make it behave for online training (if we do that)
We also need to develop a solid benchmark with mapping revenue and full KPIs from session interactions to measure differences between different price learning methods
"""
from abc import ABC, abstractmethod
from sklearn.base import BaseEstimator, TransformerMixin
import numpy as np
import pandas as pd
import os
from dotenv import load_dotenv
load_dotenv()
from supabase import create_client, Client
SUPABASE_URL = os.getenv("NEXT_PUBLIC_SUPABASE_URL", "")
SUPABASE_KEY = os.getenv("NEXT_PUBLIC_SUPABASE_ANON_KEY", "")
supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
def expected_revenue(prices: np.ndarray, demand: np.ndarray) -> float:
"""Returns: expected revenue R_t = P_t^T * Q_t"""
return float(np.dot(prices, demand))
class StateSpace:
def __init__(self,
demand : np.ndarray, # at time t, only values (assuming aligned by productId order)
prices : np.ndarray, # at time t, only values (assuming aligned by productId order)
session_features : pd.DataFrame):
self.demand = demand # Q_t
self.prices = prices # P_t
self.session_features = session_features # S_t
self.history = [] # H_t
class PricingFunction(BaseEstimator, TransformerMixin, ABC):
def __init__(self):
pass
def fit(self, historical_data):
"""
Train the pricing function based on historical data.
historical_data: list of StateSpace instances with known outcomes
"""
raise NotImplementedError("Train method must be implemented by subclass.")
def transform(self, state_space) -> np.ndarray:
"""
Predict the next prices given the current state space.
state_space: StateSpace instance
Returns: predicted prices P_{t+1}
"""
raise NotImplementedError("Predict method must be implemented by subclass.")
class SimpleLinearPricingFunction(PricingFunction):
def __init__(self, price_sensitivity: float = -0.1):
super().__init__()
self.price_sensitivity = price_sensitivity
def fit(self, historical_data):
return self
def transform(self, state_space: StateSpace) -> np.ndarray:
new_prices = state_space.prices + self.price_sensitivity * state_space.demand
return np.maximum(new_prices, 0)
class ElasticityBasedPricingFunction(PricingFunction):
"""
Revenue-maximizing pricing using elasticity estimates.
For each product, optimal price P* maximizes R = P * Q(P)
where Q(P) follows power law: Q(P) = Q_0 * (P/P_0)^ε
Taking derivative dR/dP = 0 gives optimal markup:
P* = P_0 * (1 + 1/ε) if ε < -1 (elastic)
For inelastic demand (|ε| < 1), we apply bounded markup.
"""
def __init__(self,
cost_floor: float = 0.5,
max_markup: float = 2.0,
min_markup: float = 1.0,
inelastic_markup: float = 1.3):
super().__init__()
self.cost_floor = cost_floor # prices as fraction of base
self.max_markup = max_markup # max price = base * max_markup
self.min_markup = min_markup # min price = base * min_markup
self.inelastic_markup = inelastic_markup # default for |ε| < 1
self.elasticity_map = {} # productId -> elasticity
def fit(self, elasticity_df: pd.DataFrame):
"""
Args:
elasticity_df: df with [productId, elasticity, std_error, n_obs]
"""
if elasticity_df is not None and not elasticity_df.empty:
self.elasticity_map = dict(zip(
elasticity_df['productId'],
elasticity_df['elasticity']
))
return self
def transform(self, state_space: StateSpace, product_ids: np.ndarray = None) -> np.ndarray:
"""
Args:
state_space: current state (prices = base prices)
product_ids: array of productIds aligned with state_space.prices
Returns:
optimized prices P_{t+1}
"""
base_prices = state_space.prices
if product_ids is None:
# fallback: use positional index as productId (not ideal)
product_ids = np.arange(len(base_prices))
new_prices = np.zeros_like(base_prices)
for i, (base_p, pid) in enumerate(zip(base_prices, product_ids)):
elasticity = self.elasticity_map.get(pid, 0.0)
if elasticity < -1: # elastic demand
# optimal markup: (1 + 1/ε)
markup = 1 + (1 / elasticity)
optimal_p = base_p * markup
elif elasticity > -1 and elasticity < 0: # inelastic
# conservative markup
optimal_p = base_p * self.inelastic_markup
else: # ε ≥ 0 (demand increases with price, or no data)
# no elasticity data or anomalous, keep base price
optimal_p = base_p
# apply bounds
optimal_p = np.clip(
optimal_p,
base_p * self.min_markup,
base_p * self.max_markup
)
optimal_p = max(optimal_p, self.cost_floor)
new_prices[i] = optimal_p
return new_prices
class ContextualElasticityPricing(PricingFunction):
"""
Revenue optimization with contextual adjustments based on session features.
Combines elasticity-based pricing with surge/demand-based multipliers.
"""
def __init__(self,
base_pricer: ElasticityBasedPricingFunction = None,
demand_sensitivity: float = 0.1,
surge_threshold: float = 0.7):
super().__init__()
self.base_pricer = base_pricer or ElasticityBasedPricingFunction()
self.demand_sensitivity = demand_sensitivity
self.surge_threshold = surge_threshold
def fit(self, elasticity_df: pd.DataFrame):
self.base_pricer.fit(elasticity_df)
return self
def transform(self, state_space: StateSpace, product_ids: np.ndarray = None) -> np.ndarray:
# get base optimal prices from elasticity
base_optimal = self.base_pricer.transform(state_space, product_ids)
# compute surge multiplier from demand
if len(state_space.demand) > 0:
demand_normalized = state_space.demand / (state_space.demand.max() + 1e-8)
surge_multiplier = 1 + self.demand_sensitivity * np.maximum(
demand_normalized - self.surge_threshold, 0
)
else:
surge_multiplier = np.ones_like(base_optimal)
return base_optimal * surge_multiplier
# Example usage:
if __name__ == "__main__":
from pipeline import interaction_pipeline, price_data_pipeline, elasticity_pipeline
store_mode = 'hotel'
interaction_data = interaction_pipeline.fit_transform(None)
price_data = price_data_pipeline.fit_transform(None)
elasticity_df = elasticity_pipeline(interaction_data, price_data, window_size="30s", store_mode=store_mode)
# fetch all products with base prices from database
products_resp = supabase.table(f'{store_mode}_products').select("id, metadata").execute()
products_df = pd.DataFrame(products_resp.data)
# extract base_price from metadata
products_df['base_price'] = products_df['metadata'].apply(lambda m: m.get('base_price', 0) if isinstance(m, dict) else 0)
products_df = products_df.rename(columns={'id': 'productId'})[['productId', 'base_price']]
# override with logged prices where available
if not price_data.empty:
if 'ts' in price_data.columns and not pd.api.types.is_datetime64_any_dtype(price_data['ts']):
price_data['ts'] = pd.to_datetime(price_data['ts'])
# get latest logged price per product
price_logs_agg = price_data.sort_values('ts').groupby('productId', as_index=False).last()
# merge: start with all products (base prices), override with logged prices
products_df = products_df.merge(
price_logs_agg[['productId', 'price']],
on='productId',
how='left'
)
products_df['final_price'] = products_df['price'].fillna(products_df['base_price'])
else:
products_df['final_price'] = products_df['base_price']
# merge with elasticity
if elasticity_df is not None and not elasticity_df.empty:
price_data_merged = products_df[['productId', 'final_price']].merge(
elasticity_df[['productId', 'elasticity']],
on='productId',
how='left'
).fillna({'elasticity': 0.0})
prices = price_data_merged['final_price'].values
elasticities = price_data_merged['elasticity'].values
else:
prices = np.array([])
elasticities = np.array([])
print(elasticities)
print(prices)
state_space = StateSpace(
demand=elasticities,
prices=prices,
session_features=interaction_data
)
pricing_function = SimpleLinearPricingFunction(price_sensitivity=-0.05)
pricing_function.fit([]) # No training data for simple model
predicted_prices = pricing_function.transform(state_space)
print("Predicted Prices:", predicted_prices)

View File

@@ -0,0 +1,5 @@
from procesing.providers.base import DataProvider
from procesing.providers.supabase import SupabaseProvider
from procesing.providers.backend import BackendAPIProvider
__all__ = ['DataProvider', 'SupabaseProvider', 'BackendAPIProvider']

View File

@@ -0,0 +1,19 @@
import os
import pandas as pd
import requests
from typing import List
from procesing.providers.base import DataProvider
class BackendAPIProvider(DataProvider):
"""Concrete backend API implementation"""
def __init__(self, backend_url: str = None):
self.backend_url = backend_url or os.getenv("BACKEND_URL", "http://localhost:5000")
def fetch_kafka_topic(self, topic: str) -> pd.DataFrame:
resp = requests.get(f"{self.backend_url}/api/kafka/dump?topic={topic}")
resp.raise_for_status()
data = resp.json()
if not data.get('success') or not data.get('data'):
return pd.DataFrame()
return pd.DataFrame(data['data'])

View File

@@ -0,0 +1,21 @@
from abc import ABC, abstractmethod
from typing import List
import pandas as pd
class DataProvider(ABC):
"""Abstract interface for data access, enables DI and testing"""
@abstractmethod
def fetch_products(self, store_mode: str) -> pd.DataFrame:
"""Fetch product catalog for given store mode"""
pass
@abstractmethod
def fetch_experiments(self, experiment_ids: List[str]) -> pd.DataFrame:
"""Fetch experiment metadata for given IDs"""
pass
@abstractmethod
def fetch_kafka_topic(self, topic: str) -> pd.DataFrame:
"""Fetch data from Kafka topic via backend API"""
pass

View File

@@ -0,0 +1,42 @@
import os
import pandas as pd
import requests
from typing import List
from supabase import create_client, Client
from procesing.providers.base import DataProvider
from dotenv import load_dotenv
class SupabaseProvider(DataProvider):
"""Concrete Supabase + backend API implementation"""
def __init__(self,
supabase_url: str = None,
supabase_key: str = None,):
load_dotenv()
self.supabase_url = supabase_url or os.getenv("NEXT_PUBLIC_SUPABASE_URL")
self.supabase_key = supabase_key or os.getenv("NEXT_PUBLIC_SUPABASE_ANON_KEY")
self.supabase: Client = create_client(self.supabase_url, self.supabase_key)
def fetch_products(self, store_mode: str) -> pd.DataFrame:
# hotel uses room_type, airline uses flight_type; select all and normalize
resp = self.supabase.table(f'{store_mode}_products').select("*").execute()
if not resp.data:
return pd.DataFrame()
df = pd.DataFrame(resp.data)
# normalize type column: hotel has room_type, airline has flight_type
if 'room_type' in df.columns:
df['product_type'] = df['room_type']
elif 'flight_type' in df.columns:
df['product_type'] = df['flight_type']
return df
def fetch_experiments(self, experiment_ids: List[str]) -> pd.DataFrame:
if not experiment_ids:
return pd.DataFrame()
resp = self.supabase.table('experiments').select(
'id, subject_name, xp_human_only, xp_market_mode, xp_task_id, '
'task:tasks(task_name, task_description, task_def_of_done)'
).in_('id', experiment_ids).execute()
return pd.DataFrame(resp.data) if resp.data else pd.DataFrame()

View File

@@ -0,0 +1,39 @@
from procesing.steps.base import BaseContextStep
from procesing.steps.fetch import FetchInteractionsStep, FetchPriceLogsStep, FetchExperimentsStep
from procesing.steps.join import JoinExperimentsStep, JoinProductFeaturesStep
from procesing.steps.augment import CreatePriceBucketsStep, AugmentEventNamesStep, AugmentInteractionsStep
from procesing.steps.chunk import ChunkByTimeWindowStep
from procesing.steps.demand import ComputeDemandStep, ComputeDemandForChunksStep
from procesing.steps.elasticity import AggregatePriceLogsStep
from procesing.steps.pricing import FitPricingFunctionStep, PredictPricesStep
from procesing.steps.session import (
ExtractSessionFeaturesStep, JoinLabelsStep, ValidateDataStep,
TemporalFeatureStep, BehavioralFeatureStep, ProductFeatureStep, UserAgentFeatureStep,
_extract_features_for_session
)
__all__ = [
'BaseContextStep',
'FetchInteractionsStep',
'FetchPriceLogsStep',
'FetchExperimentsStep',
'JoinExperimentsStep',
'JoinProductFeaturesStep',
'CreatePriceBucketsStep',
'AugmentEventNamesStep',
'AugmentInteractionsStep',
'ChunkByTimeWindowStep',
'ComputeDemandStep',
'ComputeDemandForChunksStep',
'AggregatePriceLogsStep',
'FitPricingFunctionStep',
'PredictPricesStep',
'ExtractSessionFeaturesStep',
'JoinLabelsStep',
'ValidateDataStep',
'TemporalFeatureStep',
'BehavioralFeatureStep',
'ProductFeatureStep',
'UserAgentFeatureStep',
'_extract_features_for_session',
]

View File

@@ -0,0 +1,140 @@
import numpy as np
import pandas as pd
from procesing.steps.base import BaseContextStep
class AugmentInteractionsStep(BaseContextStep):
"""
Consolidated step: create price buckets, augment event names, join experiments.
Input: (interactions_df, price_logs_df)
Output: enriched interactions_df
"""
def transform(self, data: tuple):
interactions_df, price_logs_df = data
if interactions_df.empty:
return interactions_df
# Step 1: Create price buckets
interactions_df = self._create_price_buckets(interactions_df)
# Step 2: Augment event names
interactions_df = self._augment_event_names(interactions_df)
# Step 3: Join experiments (optional)
if 'experimentId' in interactions_df.columns:
interactions_df = self._join_experiments(interactions_df)
return interactions_df
def _create_price_buckets(self, df: pd.DataFrame):
"""Create price bucket labels from price data"""
if 'metadata_price' not in df.columns:
df['price_bucket'] = ""
return df
n_buckets = self.context.config.get('n_price_buckets', 5)
if df['metadata_price'].notnull().sum() > 0:
try:
price_buckets = pd.qcut(
df['metadata_price'],
q=n_buckets,
labels=[f"PB_{i+1}" for i in range(n_buckets)],
duplicates='drop'
)
except ValueError:
# fallback for insufficient unique values
price_buckets = df['metadata_price'].apply(
lambda x: f"P_{int(x)}" if pd.notnull(x) else ""
)
else:
price_buckets = pd.Series([""] * len(df), index=df.index)
df['price_bucket'] = price_buckets
return df
def _augment_event_names(self, df: pd.DataFrame):
"""Augment event names with product and price bucket schema"""
# Create schema: _productId@price_bucket
has_product = df.get('productId', pd.Series()).notnull()
has_bucket = df.get('price_bucket', pd.Series()).notnull()
df['metadata_schema'] = np.where(
has_product & has_bucket,
"_" + df['productId'].astype(str) + "@" + df['price_bucket'].astype(str),
""
)
df['eventName'] = df['eventName'] + df['metadata_schema']
return df
def _join_experiments(self, df: pd.DataFrame):
"""Join experiment metadata if experimentId present"""
exp_ids = df['experimentId'].dropna().unique().tolist()
if not exp_ids:
return df
experiments_df = self.context.provider.fetch_experiments(exp_ids)
if experiments_df.empty:
return df
return df.merge(
experiments_df,
left_on='experimentId',
right_on='id',
how='left',
suffixes=('', '_exp')
)
class CreatePriceBucketsStep(BaseContextStep):
"""Create price bucket labels from price data"""
def transform(self, df: pd.DataFrame):
if df.empty or 'metadata_price' not in df.columns:
df['price_bucket'] = ""
return df
n_buckets = self.context.config.get('n_price_buckets', 5)
if df['metadata_price'].notnull().sum() > 0:
try:
price_buckets = pd.qcut(
df['metadata_price'],
q=n_buckets,
labels=[f"PB_{i+1}" for i in range(n_buckets)],
duplicates='drop'
)
except ValueError:
# fallback for insufficient unique values
price_buckets = df['metadata_price'].apply(
lambda x: f"P_{int(x)}" if pd.notnull(x) else ""
)
else:
price_buckets = pd.Series([""] * len(df), index=df.index)
df['price_bucket'] = price_buckets
return df
class AugmentEventNamesStep(BaseContextStep):
"""Augment event names with product and price bucket schema"""
def transform(self, df: pd.DataFrame):
if df.empty:
return df
# Create schema: _productId@price_bucket
has_product = df.get('productId', pd.Series()).notnull()
has_bucket = df.get('price_bucket', pd.Series()).notnull()
df['metadata_schema'] = np.where(
has_product & has_bucket,
"_" + df['productId'].astype(str) + "@" + df['price_bucket'].astype(str),
""
)
df['eventName'] = df['eventName'] + df['metadata_schema']
return df

View File

@@ -0,0 +1,32 @@
from abc import ABC, abstractmethod
from sklearn.base import BaseEstimator, TransformerMixin
from procesing.context import PipelineContext
from typing import Any
class BaseContextStep(BaseEstimator, TransformerMixin, ABC):
"""
Base for all pipeline steps.
Each step is stateless, context-driven, and performs ONE transformation.
"""
def __init__(self, context: PipelineContext):
self.context = context
def fit(self, X=None, y=None):
"""Most steps don't need training"""
return self
@abstractmethod
def transform(self, X) -> Any:
"""Transform input using context. Must be implemented by subclass."""
pass
def get_params(self, deep=True):
"""sklearn compatibility"""
return {'context': self.context}
def set_params(self, **params):
"""sklearn compatibility"""
if 'context' in params:
self.context = params['context']
return self

View File

@@ -0,0 +1,34 @@
import pandas as pd
from procesing.steps.base import BaseContextStep
class ChunkByTimeWindowStep(BaseContextStep):
"""
Chunk dataframe into time windows.
Returns list of dicts with window metadata.
"""
def transform(self, df: pd.DataFrame):
if df.empty:
return []
df = df.copy()
ts_col = self.context.config.get('ts_col', 'ts')
window_size = self.context.window_size
# ensure datetime
if not pd.api.types.is_datetime64_any_dtype(df[ts_col]):
df[ts_col] = pd.to_datetime(df[ts_col])
df = df.sort_values(ts_col)
df['_window'] = df[ts_col].dt.floor(window_size)
chunks = []
for idx, (window_start, group) in enumerate(df.groupby('_window')):
chunks.append({
'window_start': window_start,
'window_end': window_start + pd.Timedelta(window_size),
'window_idx': idx,
'data': group.drop(columns=['_window'])
})
return chunks

View File

@@ -0,0 +1,61 @@
import pandas as pd
from procesing.steps.base import BaseContextStep
class ComputeDemandStep(BaseContextStep):
"""
Compute demand vector for a single time window or dataframe.
Input: single chunk dict OR raw dataframe
Output: demand dataframe with [productId, demand_score]
"""
def transform(self, chunk):
# handle both chunk dict and raw dataframe
if isinstance(chunk, dict):
interactions = chunk['data']
window_meta = {k: v for k, v in chunk.items() if k != 'data'}
else:
interactions = chunk
window_meta = {}
products = self.context.products
unique_products = products['id'].unique()
# apply filters if configured
session_filter = self.context.config.get('session_filter')
experiment_filter = self.context.config.get('experiment_filter')
if session_filter and 'sessionId' in interactions.columns:
interactions = interactions[interactions['sessionId'] == session_filter]
if experiment_filter and 'experimentId' in interactions.columns:
interactions = interactions[interactions['experimentId'] == experiment_filter]
interactions_with_products = interactions.dropna(subset=['productId'])
if interactions_with_products.empty:
demand_df = pd.DataFrame({
'productId': unique_products,
'demand_score': 0
})
else:
# crosstab for simple demand count
demand_df = pd.crosstab(
interactions_with_products['productId'],
'count'
).reindex(unique_products, fill_value=0).reset_index()
demand_df.columns = ['productId', 'demand_score']
# attach window metadata if present
if window_meta:
return {**window_meta, 'demand_vector': demand_df}
return demand_df
class ComputeDemandForChunksStep(BaseContextStep):
"""Apply ComputeDemandStep to list of chunks"""
def transform(self, chunks: list):
if not chunks:
return []
demand_step = ComputeDemandStep(self.context)
return [demand_step.transform(chunk) for chunk in chunks]

View File

@@ -0,0 +1,42 @@
import numpy as np
import pandas as pd
from typing import Dict, List
from procesing.steps.base import BaseContextStep
class AggregatePriceLogsStep(BaseContextStep):
"""
Aggregate price logs into time windows using VECTORIZED operations.
Input: price_logs_df
Output: DataFrame with columns [productId, price]
"""
def transform(self, price_logs_df: pd.DataFrame):
if price_logs_df.empty:
return pd.DataFrame(columns=['productId', 'price'])
df = price_logs_df.copy()
ts_col = self.context.config.get('ts_col', 'ts')
#window_size = self.context.window_size WE ARE NOT USING CHUNKS ANYMORE
# ensure datetime
if not pd.api.types.is_datetime64_any_dtype(df[ts_col]):
df[ts_col] = pd.to_datetime(df[ts_col])
df = df.sort_values([ts_col, 'productId'])
products = self.context.products
# get base price from metadata if available 1) read the metadata col as json and get the base_price
products['base_price'] = products.apply(
lambda row: row['metadata'].get('base_price', 0) if isinstance(row['metadata'], dict) else 0,
axis=1
)
unique_products = products['id'].unique()
df_indexed = df.set_index(ts_col)
# we return a df of average price per product over the entire period
# TODO: maybe consider different opration to handle price aggregation over time
avg_prices = df_indexed.groupby('productId')['price'].mean().reindex(unique_products, fill_value=0).reset_index()
avg_prices.columns = ['productId', 'price']
# fill 0s with base_price from products
base_price_map = products.set_index('id')['base_price'].to_dict()
return avg_prices

View File

@@ -0,0 +1,81 @@
import pandas as pd
from procesing.steps.base import BaseContextStep
class FetchInteractionsStep(BaseContextStep):
"""Fetch raw interaction data from Kafka topic with optional time and store_mode filtering"""
def __init__(self, context, lookback: str = None):
super().__init__(context)
self.lookback = lookback
def transform(self, X=None):
df = self.context.provider.fetch_kafka_topic('user-interactions')
if df.empty:
return df
# Explode metadata JSON column
if 'metadata' in df.columns:
df = df.join(
pd.json_normalize(df.pop('metadata'), sep='.').add_prefix('metadata_')
)
df = df.dropna(subset=['eventName'])
# drop all where page has /admin/
df = df[~df['page'].str.contains('/admin/', na=False)]
# filter by store_mode from context
if 'storeMode' in df.columns:
df = df[df['storeMode'] == self.context.store_mode]
# Remap dateIndex if present
if 'metadata_dateIndex' in df.columns:
df['dateIndex'] = df['metadata_dateIndex'].astype('Int64')
# Apply time filtering if lookback specified
if self.lookback and 'ts' in df.columns:
df['ts'] = pd.to_datetime(df['ts'])
cutoff = pd.Timestamp.now() - pd.Timedelta(self.lookback)
df = df[df['ts'] >= cutoff]
return df
class FetchPriceLogsStep(BaseContextStep):
"""Fetch price log data from Kafka topic with optional time and store_mode filtering"""
def __init__(self, context, lookback: str = None):
super().__init__(context)
self.lookback = lookback
def transform(self, X=None):
df = self.context.provider.fetch_kafka_topic('price-logs')
if df.empty:
return df
# filter by store_mode from context
if 'storeMode' in df.columns:
df = df[df['storeMode'] == self.context.store_mode]
# Apply time filtering if lookback specified
if self.lookback and 'ts' in df.columns:
df['ts'] = pd.to_datetime(df['ts'])
cutoff = pd.Timestamp.now() - pd.Timedelta(self.lookback)
df = df[df['ts'] >= cutoff]
return df
class FetchExperimentsStep(BaseContextStep):
"""Fetch experiment metadata for given interaction data"""
def transform(self, interactions_df: pd.DataFrame):
if interactions_df.empty or 'experimentId' not in interactions_df.columns:
return pd.DataFrame()
exp_ids = interactions_df['experimentId'].dropna().unique().tolist()
if not exp_ids:
return pd.DataFrame()
return self.context.provider.fetch_experiments(exp_ids)

View File

@@ -0,0 +1,58 @@
import pandas as pd
from procesing.steps.base import BaseContextStep
class JoinExperimentsStep(BaseContextStep):
"""Join experiment metadata to interactions"""
def transform(self, data: tuple):
"""
Args:
data: (interactions_df, experiments_df)
Returns:
merged interactions dataframe
"""
interactions_df, experiments_df = data
if experiments_df.empty:
return interactions_df
# Flatten nested task field if present
if 'task' in experiments_df.columns and experiments_df['task'].notnull().any():
task_norm = pd.json_normalize(experiments_df['task'].dropna())
task_norm.index = experiments_df[experiments_df['task'].notnull()].index
experiments_df = experiments_df.drop('task', axis=1).join(task_norm, rsuffix='_task')
# Rename for clarity
experiments_df = experiments_df.rename(columns={
'id': 'experimentId',
'subject_name': 'exp_subject',
'xp_human_only': 'exp_human_only',
'xp_market_mode': 'exp_market_mode',
'xp_task_id': 'exp_task_id'
})
return interactions_df.merge(experiments_df, on='experimentId', how='left')
class JoinProductFeaturesStep(BaseContextStep):
"""Join product features to interactions"""
def transform(self, data: tuple):
"""
Args:
data: (interactions_df, products_df)
Returns:
merged interactions dataframe
"""
demand_df, price_df = data
# get base prices from products if available
products = self.context.products
products['base_price'] = products.apply(
lambda row: float(row['metadata'].get('base_price', 0.0)) if isinstance(row['metadata'], dict) else 0,
axis=1
)
products = products[['id', 'base_price']].rename(columns={'id': 'productId'})
if price_df.empty:
return demand_df
return demand_df.merge(price_df, on='productId', how='left').merge(products, on='productId', how='left')

View File

@@ -0,0 +1,55 @@
import numpy as np
import pandas as pd
from typing import Optional, List, Dict, Any
from dataclasses import dataclass, field
from procesing.pricers.simple import StaticPricer
from procesing.steps.base import BaseContextStep
from procesing.pricers import ElasticityBasedPricer
class State:
def __init__(self,
last_action : str,
last_productId : str,
last_price : float,
session_features : np.ndarray
):
pass
class FitPricingFunctionStep(BaseContextStep):
"""
Fit pricing function using data.
Input: pricing_data
Output: fitted pricing function instance
"""
def transform(self, pricing_data: pd.DataFrame):
pricing_class = self.context.config.get('pricing_function_class', StaticPricer)
pricing_params = self.context.config.get('pricing_function_params', {})
pricer = pricing_class(**pricing_params)
pricer.fit(pricing_data)
return pricer
class PredictPricesStep(BaseContextStep):
"""
Predict optimal prices using fitted pricing function.
Input: (pricer, state_space)
Output: prices_df [productId, predicted_price]
"""
def transform(self, data: tuple):
pricer, state_space = data
products = self.context.products
product_ids = products['id'].values
predicted_prices = pricer.predict(state_space)
return pd.DataFrame({
'productId': product_ids,
'predicted_price': predicted_prices
})

View File

@@ -0,0 +1,262 @@
"""
Session feature extraction for ML training pipeline.
"""
import pandas as pd
import numpy as np
import re
from typing import Dict, Any
from procesing.steps.base import BaseContextStep
EVENT_CATS = {
'page_view': ['page_view'],
'item_view': ['view_item_page', 'learn_more_about_item'],
'cart_add': ['add_item_to_cart'],
'purchase': ['purchase', 'checkout_complete'],
'hover': ['hover_over_title', 'hover_over_paragraph', 'hover_over_link', 'hover_over_button'],
# 'filter': ['filter', 'search', 'apply_filter'],
}
HEADLESS_RE = re.compile(r'HeadlessChrome|Headless|PhantomJS', re.I)
AUTOMATION_RE = re.compile(r'Selenium|Playwright|Puppeteer|WebDriver|chromedriver|geckodriver', re.I)
BROWSER_PATTERNS = [('Chrome', r'Chrome/[\d.]+'), ('Firefox', r'Firefox/[\d.]+'),
('Safari', r'Safari/[\d.]+'), ('Edge', r'Edg/[\d.]+')]
def _get_browser(s: str) -> str:
if pd.isna(s): return 'Unknown'
for name, pat in BROWSER_PATTERNS:
if re.search(pat, s): return name
return 'Other'
class TemporalFeatureStep(BaseContextStep):
"""Vectorized time-based features: durations, velocities, gaps."""
def __init__(self, context, timeout_sec: float = 900, velocity_window: str = '5min'):
super().__init__(context)
self.timeout_sec = timeout_sec
self.velocity_window = velocity_window
def transform(self, X: pd.DataFrame) -> pd.DataFrame:
df = X.copy()
if df.empty or 'ts' not in df.columns:
return pd.DataFrame(columns=pd.Series(['sessionId']))
df['ts_dt'] = pd.to_datetime(df['ts'])
df = df.sort_values(['sessionId', 'ts_dt'])
df['time_diff'] = df.groupby('sessionId')['ts_dt'].diff().dt.total_seconds()
df['active_diff'] = df['time_diff'].where(df['time_diff'] <= self.timeout_sec, 0)
agg = df.groupby('sessionId').agg(
session_duration_sec=('active_diff', 'sum'),
total_interactions=('sessionId', 'count'),
avg_time_between_events=('time_diff', 'mean'),
std_time_between_events=('time_diff', 'std'),
min_time_between_events=('time_diff', 'min'),
session_start_hour=('ts_dt', lambda x: x.min().hour),
).reset_index()
agg['std_time_between_events'] = agg['std_time_between_events'].fillna(0)
agg['interaction_velocity'] = np.where(
agg['session_duration_sec'] > 0,
(agg['total_interactions'] / agg['session_duration_sec']) * 60, 0)
vel = df.set_index('ts_dt').groupby('sessionId').resample(self.velocity_window, include_groups=False).size()
max_velocity = vel.groupby('sessionId').max().rename('max_velocity_5min')
agg = agg.merge(max_velocity, on='sessionId', how='left')
agg['max_velocity_5min'] = agg['max_velocity_5min'].fillna(0)
return agg
class BehavioralFeatureStep(BaseContextStep):
"""Vectorized event counts and ratios per session."""
def transform(self, X: pd.DataFrame) -> pd.DataFrame:
df = X.copy()
if df.empty or 'eventName' not in df.columns:
return pd.DataFrame(columns=pd.Series(['sessionId']))
for cat, events in EVENT_CATS.items():
df[f'is_{cat}'] = df['eventName'].isin(events)
df['is_hover'] = df['is_hover'] | df['eventName'].str.startswith('hover_over_')
agg = df.groupby('sessionId').agg(
total_events=('eventName', 'count'), unique_pages=('page', 'nunique'),
page_views=('is_page_view', 'sum'), item_views=('is_item_view', 'sum'),
cart_adds=('is_cart_add', 'sum'), purchases=('is_purchase', 'sum'),
hover_events=('is_hover', 'sum'),
# filter_events=('is_filter', 'sum'),
).reset_index()
agg['cart_to_view_ratio'] = np.where(agg['item_views'] > 0, agg['cart_adds'] / agg['item_views'], 0)
agg['conversion_rate'] = np.where(agg['item_views'] > 0, agg['purchases'] / agg['item_views'], 0)
agg['hover_intensity'] = np.where(agg['total_events'] > 0, agg['hover_events'] / agg['total_events'], 0)
return agg
class ProductFeatureStep(BaseContextStep):
"""Vectorized product interaction features: diversity, depth, price sensitivity."""
def transform(self, X: pd.DataFrame) -> pd.DataFrame:
df = X.copy()
if df.empty:
return pd.DataFrame(columns=pd.Series(['sessionId']))
price_col = next((c for c in ['metadata_base_price', 'metadata_price', 'base_price'] if c in df.columns), None)
df['price_seen'] = pd.to_numeric(df[price_col], errors='coerce') if price_col else np.nan
prod_df = df[df['productId'].notna()]
if prod_df.empty:
return pd.DataFrame(columns=pd.Series(['sessionId', 'unique_products_viewed', 'product_view_depth', 'avg_price_seen', 'min_price_seen', 'max_price_seen', 'price_range']))
agg = prod_df.groupby('sessionId').agg(
unique_products_viewed=('productId', 'nunique'),
product_view_depth=('productId', lambda x: x.value_counts().iloc[0] if len(x) > 0 else 0),
avg_price_seen=('price_seen', 'mean'), min_price_seen=('price_seen', 'min'),
max_price_seen=('price_seen', 'max'),
).reset_index()
agg['price_range'] = (agg['max_price_seen'] - agg['min_price_seen']).fillna(0)
return agg
class UserAgentFeatureStep(BaseContextStep):
"""Parse userAgent into bot-detection signals."""
def transform(self, X: pd.DataFrame) -> pd.DataFrame|pd.Series:
df = X.copy()
if df.empty or 'userAgent' not in df.columns:
return pd.DataFrame(columns=pd.Series(['sessionId']))
ua = df.groupby('sessionId')['userAgent'].first().reset_index()
ua['is_headless'] = ua['userAgent'].str.contains(HEADLESS_RE, na=False)
ua['is_automation'] = ua['userAgent'].str.contains(AUTOMATION_RE, na=False)
ua['browser_family'] = ua['userAgent'].apply(_get_browser)
return ua[['sessionId', 'is_headless', 'is_automation', 'browser_family']]
class ExtractSessionFeaturesStep(BaseContextStep):
"""
Vectorized session feature extraction - replaces O(n^2) per-row loop.
Input: interactions_df
Output: session-level feature matrix
THIS is our main mapping from tau (trajectory) to some features vector theta - we need to do this very well. This is what will go into demand esimation.
"""
def transform(self, X: pd.DataFrame) -> pd.DataFrame:
if X.empty:
return pd.DataFrame()
df = X.copy()
# run all feature steps and merge on sessionId
temporal = TemporalFeatureStep(self.context).transform(df)
behavioral = BehavioralFeatureStep(self.context).transform(df)
product = ProductFeatureStep(self.context).transform(df)
ua = UserAgentFeatureStep(self.context).transform(df)
result = temporal
for other in [behavioral, product, ua]:
if not other.empty and 'sessionId' in other.columns:
result = result.merge(other, on='sessionId', how='left')
# carry forward experimentId for label joining
if 'experimentId' in df.columns:
exp_map = df.groupby('sessionId')['experimentId'].first()
result = result.merge(exp_map, on='sessionId', how='left')
return result
class JoinLabelsStep(BaseContextStep):
"""
Join experiment labels to session features.
Input: (features_df, experiments_df) or features_df (fetches experiments)
Output: labeled feature matrix with is_agent column
"""
def transform(self, X : tuple) -> pd.DataFrame:
data = X;
if isinstance(data, tuple):
features_df, experiments_df = data
else:
features_df = data
if 'experimentId' not in features_df.columns:
return features_df
exp_ids = features_df['experimentId'].dropna().unique().tolist()
experiments_df = self.context.provider.fetch_experiments(exp_ids) if exp_ids else pd.DataFrame()
if features_df.empty:
return features_df
if experiments_df.empty:
features_df['is_agent'] = np.nan
return features_df
exp = experiments_df.copy()
if 'id' in exp.columns:
exp = exp.rename(columns={'id': 'experimentId'})
if 'xp_human_only' in exp.columns:
exp['is_agent'] = ~exp['xp_human_only']
cols = ['experimentId'] + [c for c in ['is_agent', 'xp_human_only', 'xp_market_mode'] if c in exp.columns]
return features_df.merge(exp[cols].drop_duplicates(), on='experimentId', how='left')
class ValidateDataStep(BaseContextStep):
"""
Data quality checks before training.
Input: df
Output: df (unchanged, but logs validation report to context)
"""
REQUIRED = ['sessionId', 'eventName', 'ts']
def transform(self, X: pd.DataFrame) -> pd.DataFrame:
df = X.copy()
report = {'status': 'valid', 'rows': len(df), 'sessions': 0}
if df.empty:
report['status'] = 'empty'
self.context.cache('validation_report', report)
return df
missing = [c for c in self.REQUIRED if c not in df.columns]
if missing:
report['status'] = 'invalid'
report['missing_cols'] = missing
report['sessions'] = df['sessionId'].nunique() if 'sessionId' in df.columns else 0
report['null_sessions'] = int(df['sessionId'].isna().sum()) if 'sessionId' in df.columns else 0
if 'experimentId' in df.columns:
report['null_experiments'] = int(df['experimentId'].isna().sum())
self.context.cache('validation_report', report)
return df
# legacy compat - kept for backwards compatibility with existing code
def _extract_features_for_session(session_df: pd.DataFrame, session_timeout_sec: float = 900) -> Dict[str, Any]:
"""Single-session feature extraction (legacy interface)."""
defaults = {k: 0 for k in ['total_interactions', 'page_views', 'item_views', 'searches',
'cart_adds', 'hovers', 'unique_products_viewed', 'product_view_depth',
'session_duration_sec', 'interaction_velocity',
'avg_time_between_events', 'std_time_between_events', 'cart_to_view_ratio']}
if session_df.empty:
return defaults
session_df = session_df.copy()
if 'sessionId' not in session_df.columns:
session_df['sessionId'] = 'tmp'
# use a dummy context for the steps
class DummyCtx: config = {} # should maybe inherit but whatever
ctx = DummyCtx()
t = TemporalFeatureStep(ctx, timeout_sec=session_timeout_sec).transform(session_df)
b = BehavioralFeatureStep(ctx).transform(session_df)
p = ProductFeatureStep(ctx).transform(session_df)
result = {}
for df in [t, b, p]:
if not df.empty:
for col in df.columns:
if col != 'sessionId':
result[col] = df[col].iloc[0] if len(df) > 0 else 0
remap = {'hover_events': 'hovers', 'filter_events': 'searches', 'unique_pages': 'unique_pages_visited'}
for old, new in remap.items():
if old in result:
result[new] = result.pop(old)
return result

View File

View File

@@ -0,0 +1,281 @@
import pytest
import pandas as pd
from typing import List
from procesing.providers.base import DataProvider
from procesing.context import PipelineContext
class MockProvider(DataProvider):
"""Mock provider for testing, holds in-memory fixtures"""
def __init__(self, products_df=None, experiments_df=None, kafka_data=None):
self._products = products_df if products_df is not None else pd.DataFrame()
self._experiments = experiments_df if experiments_df is not None else pd.DataFrame()
self._kafka_data = kafka_data if kafka_data is not None else {}
def fetch_products(self, store_mode: str) -> pd.DataFrame:
return self._products.copy()
def fetch_experiments(self, experiment_ids: List[str]) -> pd.DataFrame:
if self._experiments.empty:
return pd.DataFrame()
return self._experiments[
self._experiments['id'].isin(experiment_ids)
].copy()
def fetch_kafka_topic(self, topic: str) -> pd.DataFrame:
return self._kafka_data.get(topic, pd.DataFrame()).copy()
@pytest.fixture
def mock_products():
"""Standard product catalog fixture with realistic IDs from test data"""
return pd.DataFrame({
'id': [
'd018efc1-25e9-4284-b276-80386e048b25',
'51266ddb-5b07-47b7-89ee-5b5cae94bb11',
'2cd7f756-fc65-4ba0-ab01-74521c1fff43'
],
'name': ['Junior Suite', 'Superior Room', 'Deluxe Room'],
'base_price': [200.0, 150.0, 180.0]
})
@pytest.fixture
def mock_interactions_raw_kafka():
"""Raw Kafka message structure for interactions, matches production format"""
return [
{
'partitionID': 0, 'offset': 203, 'timestamp': 1764102082676,
'value': {
'payload': {
'sessionId': 'd423ce8a-77aa-4c9a-94d4-d1adddcc3472',
'experimentId': '53aefd07-f66a-4d7f-ba8b-7ea1fc562d35',
'eventName': 'learn_more_about_item',
'page': '/hotel/products/d018efc1-25e9-4284-b276-80386e048b25',
'productId': 'd018efc1-25e9-4284-b276-80386e048b25',
'metadata': {'type': 'hotel', 'dateIndex': 1, 'roomType': 'Junior Suite'},
'storeMode': 'hotel',
'ts': '2025-11-25T20:21:22.674Z'
}
}
},
{
'partitionID': 0, 'offset': 204, 'timestamp': 1764102086982,
'value': {
'payload': {
'sessionId': 'd423ce8a-77aa-4c9a-94d4-d1adddcc3472',
'experimentId': '53aefd07-f66a-4d7f-ba8b-7ea1fc562d35',
'eventName': 'page_view',
'page': '/hotel/products',
'productId': None,
'metadata': {'referrer': ''},
'storeMode': 'hotel',
'ts': '2025-11-25T20:21:26.947Z'
}
}
},
{
'partitionID': 0, 'offset': 205, 'timestamp': 1764102091825,
'value': {
'payload': {
'sessionId': 'd423ce8a-77aa-4c9a-94d4-d1adddcc3472',
'experimentId': '53aefd07-f66a-4d7f-ba8b-7ea1fc562d35',
'eventName': 'hover_over_title',
'page': '/hotel/products',
'productId': '51266ddb-5b07-47b7-89ee-5b5cae94bb11',
'metadata': {'elementText': 'Superior Room', 'dateIndex': 1, 'dwellTime': 1200},
'storeMode': 'hotel',
'ts': '2025-11-25T20:21:31.823Z'
}
}
},
{
'partitionID': 0, 'offset': 206, 'timestamp': 1764102094193,
'value': {
'payload': {
'sessionId': 'd423ce8a-77aa-4c9a-94d4-d1adddcc3472',
'experimentId': 'bbbbcccc-dddd-eeee-ffff-000011112222',
'eventName': 'hover_over_paragraph',
'page': '/hotel/products',
'productId': '51266ddb-5b07-47b7-89ee-5b5cae94bb11',
'metadata': {'elementText': 'price', 'dateIndex': 1, 'dwellTime': 1307},
'storeMode': 'hotel',
'ts': '2025-11-25T20:21:34.191Z'
}
}
},
{
'partitionID': 0, 'offset': 207, 'timestamp': 1764102101970,
'value': {
'payload': {
'sessionId': 'd423ce8a-77aa-4c9a-94d4-d1adddcc3472',
'experimentId': 'bbbbcccc-dddd-eeee-ffff-000011112222',
'eventName': 'hover_over_paragraph',
'page': '/hotel/products',
'productId': 'd018efc1-25e9-4284-b276-80386e048b25',
'metadata': {'elementText': 'price', 'dateIndex': 1, 'dwellTime': 1201},
'storeMode': 'hotel',
'ts': '2025-11-25T20:21:41.967Z'
}
}
}
]
@pytest.fixture
def mock_interactions(mock_interactions_raw_kafka):
"""Processed interaction DataFrame (what provider.fetch_kafka_topic returns)"""
records = [msg['value']['payload'] for msg in mock_interactions_raw_kafka]
df = pd.DataFrame(records)
df['timestamp'] = pd.to_datetime(df['ts'])
return df
@pytest.fixture
def mock_price_logs_raw_kafka():
"""Raw Kafka message structure for price logs, matches production format"""
return [
{
'partitionID': 0, 'offset': 32, 'timestamp': 1764104757969,
'value': {
'payload': {
'productId': '2cd7f756-fc65-4ba0-ab01-74521c1fff43',
'price': 162.47,
'sessionId': 'd423ce8a-77aa-4c9a-94d4-d1adddcc3472',
'experimentId': '53aefd07-f66a-4d7f-ba8b-7ea1fc562d35',
'storeMode': 'hotel',
'ts': '2025-11-25T21:05:57.967Z'
}
}
},
{
'partitionID': 0, 'offset': 33, 'timestamp': 1764104757995,
'value': {
'payload': {
'productId': '2ddabbfc-4127-48fc-86dc-ebc4c677efa2',
'price': 743.49,
'sessionId': 'd423ce8a-77aa-4c9a-94d4-d1adddcc3472',
'experimentId': '53aefd07-f66a-4d7f-ba8b-7ea1fc562d35',
'storeMode': 'hotel',
'ts': '2025-11-25T21:05:57.993Z'
}
}
},
{
'partitionID': 0, 'offset': 34, 'timestamp': 1764104758011,
'value': {
'payload': {
'productId': '2cd7f756-fc65-4ba0-ab01-74521c1fff43',
'price': 163.87,
'sessionId': 'd423ce8a-77aa-4c9a-94d4-d1adddcc3472',
'experimentId': '53aefd07-f66a-4d7f-ba8b-7ea1fc562d35',
'storeMode': 'hotel',
'ts': '2025-11-25T21:05:58.009Z'
}
}
},
{
'partitionID': 0, 'offset': 35, 'timestamp': 1764104758050,
'value': {
'payload': {
'productId': '2ddabbfc-4127-48fc-86dc-ebc4c677efa2',
'price': 397.46,
'sessionId': 'd423ce8a-77aa-4c9a-94d4-d1adddcc3472',
'experimentId': '53aefd07-f66a-4d7f-ba8b-7ea1fc562d35',
'storeMode': 'hotel',
'ts': '2025-11-25T21:05:58.049Z'
}
}
},
{
'partitionID': 0, 'offset': 36, 'timestamp': 1764104768865,
'value': {
'payload': {
'productId': '2cd7f756-fc65-4ba0-ab01-74521c1fff43',
'price': 401.66,
'sessionId': 'd423ce8a-77aa-4c9a-94d4-d1adddcc3472',
'experimentId': '53aefd07-f66a-4d7f-ba8b-7ea1fc562d35',
'storeMode': 'hotel',
'ts': '2025-11-25T21:06:08.864Z'
}
}
}
]
@pytest.fixture
def mock_price_logs(mock_price_logs_raw_kafka):
"""Processed price logs DataFrame (what provider.fetch_kafka_topic returns)"""
# extract payloads and flatten
records = [msg['value']['payload'] for msg in mock_price_logs_raw_kafka]
df = pd.DataFrame(records)
df['timestamp'] = pd.to_datetime(df['ts'])
return df
@pytest.fixture
def mock_experiments():
"""Standard experiment metadata fixture matching Supabase schema"""
return pd.DataFrame({
'id': ['53aefd07-f66a-4d7f-ba8b-7ea1fc562d35', 'bbbbcccc-dddd-eeee-ffff-000011112222'],
'created_at': pd.to_datetime(['2025-11-25T20:00:00Z', '2025-11-26T10:00:00Z']),
'subject_name': ['Session A', 'Session B'],
'xp_human_only': [True, False],
'xp_market_mode': ['hotel', 'airline'],
'xp_task_id': [None, None]
})
@pytest.fixture
def mock_provider(mock_products, mock_experiments, mock_interactions, mock_price_logs):
"""Fully configured mock provider"""
return MockProvider(
products_df=mock_products,
experiments_df=mock_experiments,
kafka_data={
'user-interactions': mock_interactions,
'price-logs': mock_price_logs
}
)
@pytest.fixture
def pipeline_context(mock_provider):
"""Standard pipeline context for testing"""
return PipelineContext(
provider=mock_provider,
store_mode='hotel',
window_size='30s',
n_price_buckets=3
)
@pytest.fixture
def empty_provider():
"""Provider with no data, for edge case testing"""
return MockProvider(
products_df=pd.DataFrame(columns=['id', 'name', 'base_price']),
experiments_df=pd.DataFrame(columns=['id', 'created_at', 'subject_name', 'xp_human_only', 'xp_market_mode', 'xp_task_id']),
kafka_data={'user-interactions': pd.DataFrame(), 'price-logs': pd.DataFrame()}
)
@pytest.fixture
def empty_context(empty_provider):
"""Context with empty provider"""
return PipelineContext(
provider=empty_provider,
store_mode='hotel',
window_size='30s'
)
@pytest.fixture
def session_interactions(mock_interactions):
"""Enriched interaction data for session feature extraction tests"""
df = mock_interactions.copy()
df['userAgent'] = ['Mozilla/5.0 Chrome/120', 'Mozilla/5.0 Chrome/120',
'HeadlessChrome/120', 'HeadlessChrome/120', 'HeadlessChrome/120']
df['metadata_base_price'] = [None, None, 150.0, 150.0, 200.0]
return df

View File

@@ -0,0 +1,45 @@
import pytest
import random
import pandas as pd
from procesing.steps import (
CreatePriceBucketsStep,
AugmentEventNamesStep
)
def test_bucketing(pipeline_context):
step = CreatePriceBucketsStep(context=pipeline_context)
# Test with normal price data
df = pd.DataFrame({
'metadata_price': random.sample(range(10, 1000), 100)
})
result = step.transform(df)
assert 'price_bucket' in result.columns
# test if is categorical
assert isinstance(result['price_bucket'].dtype, pd.CategoricalDtype)
assert result['price_bucket'].nunique() == 3 # as per context config
# distribution check
counts = result['price_bucket'].value_counts()
assert all(counts > 0)
assert counts.max() - counts.min() <= 10 # roughly equal distribution for 100 samples
# Test with empty DataFrame
df = pd.DataFrame()
result = step.transform(df)
assert 'price_bucket' in result.columns
assert result.empty
def test_augment_names(pipeline_context):
df = pd.DataFrame({
'eventName': ['click', 'view', 'purchase'],
'productId': ['prod_1', 'prod_2', None],
'price_bucket': ['PB_1', None, 'PB_3']
})
step = AugmentEventNamesStep(context=pipeline_context)
result = step.transform(df)
expected_event_names = [
'click_prod_1@PB_1',
'view',
'purchase'
]
assert result['eventName'].tolist() == expected_event_names

View File

@@ -0,0 +1,51 @@
import pytest
import random
import pandas as pd
from procesing.steps import (
ComputeDemandStep
)
def test_compute_demand(pipeline_context):
random.seed(42) # deterministic test
step = ComputeDemandStep(context=pipeline_context)
# Test with normal interaction data
df = pd.DataFrame({
'ts': pd.date_range(start='2023-01-01', periods=100, freq='h'),
'productId': random.choices([
'd018efc1-25e9-4284-b276-80386e048b25',
'51266ddb-5b07-47b7-89ee-5b5cae94bb11',
'2cd7f756-fc65-4ba0-ab01-74521c1fff43'
], k=100),
'eventName': random.choices(['view', 'click', 'purchase'], k=100)
})
result = step.transform(df)
assert type(result) == pd.DataFrame
assert not result.empty
assert set(result['productId']) == set(pipeline_context.products['id'])
assert all(result['demand_score'] > 100/3 -10)
def test_compute_demand_skewed(pipeline_context):
random.seed(42) # deterministic test
step = ComputeDemandStep(context=pipeline_context)
# Test with normal interaction data
df = pd.DataFrame({
'ts': pd.date_range(start='2023-01-01', periods=100, freq='h'),
'productId': random.choices([
'd018efc1-25e9-4284-b276-80386e048b25',
'51266ddb-5b07-47b7-89ee-5b5cae94bb11',
'2cd7f756-fc65-4ba0-ab01-74521c1fff43'
], weights=[0.7, 0.2, 0.1], k=100),
'eventName': random.choices(['view', 'click', 'purchase'], k=100)
})
result = step.transform(df)
assert type(result) == pd.DataFrame
assert not result.empty
assert set(result['productId']) == set(pipeline_context.products['id'])
# test for skewness
scores = result.set_index('productId')['demand_score'].to_dict()
assert scores['d018efc1-25e9-4284-b276-80386e048b25'] > \
scores['51266ddb-5b07-47b7-89ee-5b5cae94bb11'] > \
scores['2cd7f756-fc65-4ba0-ab01-74521c1fff43']

View File

@@ -0,0 +1,51 @@
import pytest
import pandas as pd
from procesing.steps import (
FetchInteractionsStep,
FetchPriceLogsStep,
FetchExperimentsStep,
)
def test_fetch_interactions_data(pipeline_context):
step = FetchInteractionsStep(pipeline_context)
data = step.transform(None)
assert data is not None
assert isinstance(data, pd.DataFrame)
expected_cols = [
"eventName",
"dateIndex",
"experimentId",
"storeMode",
"metadata_elementText"
]
for expected in expected_cols:
assert expected in data.columns
def test_fetch_price_logs(pipeline_context):
step = FetchPriceLogsStep(pipeline_context)
data = step.transform(None)
assert data is not None
assert isinstance(data, pd.DataFrame)
expected_cols = [
"price",
"productId"
]
for expected in expected_cols:
assert expected in data.columns
prices = data['price'].to_list()
assert min(prices) >= 0
assert max(prices) <= 9999
def test_experiments_fetching(pipeline_context):
interactions = FetchInteractionsStep(pipeline_context).transform(None)
assert interactions is not None
experiments = FetchExperimentsStep(pipeline_context)
experiment_data = experiments.transform(interactions)
assert experiment_data is not None
assert isinstance(experiment_data, pd.DataFrame)
assert not experiment_data.empty
assert 'id' in experiment_data.columns
assert len(experiment_data) == 2
assert '53aefd07-f66a-4d7f-ba8b-7ea1fc562d35' in experiment_data['id'].values

View File

@@ -0,0 +1,87 @@
import pytest
import pandas as pd
from procesing.pricers import (
StaticPricer,
RandomPricer,
ElasticityBasedPricer
)
def test_static_pricer_fit_and_predict():
# Sample historical data
historical_data = pd.DataFrame({
'product_id': [1, 2, 3],
'base_price': [100.0, 150.0, 200.0]
})
# Initialize and fit StaticPricer
pricer = StaticPricer()
pricer.fit(historical_data)
# Predict prices
predicted_prices = pricer.predict(None)
# Assert that predicted prices match base prices
expected_prices = historical_data['base_price'].values
assert all(predicted_prices == expected_prices), "Predicted prices do not match base prices"
def test_random_pricer_fit_and_predict():
# Sample historical data
historical_data = pd.DataFrame({
'product_id': [1, 2, 3],
'base_price': [100.0, 150.0, 200.0]
})
# Initialize and fit RandomPricer
pricer = RandomPricer(price_min=50.0, price_max=250.0, seed=42)
pricer.fit(historical_data)
# Predict prices
predicted_prices = pricer.predict(None)
# Assert that predicted prices are within bounds
assert predicted_prices.min() >= 50.0, "Predicted prices are below minimum bound"
assert predicted_prices.max() <= 250.0, "Predicted prices are above maximum bound"
# distribution check (not so strict)
assert len(set(predicted_prices)) > 1, "Predicted prices are not varied enough"
assert len(predicted_prices) == len(historical_data), "Number of predicted prices does not match number of products"
def test_elasticity_based_pricer_fit_and_predict():
# Sample historical data
historical_data = pd.DataFrame({
'productId': [1, 2, 3],
'elasticity': [-1.5, -0.5, -2.0],
'base_price': [100.0, 150.0, 200.0],
'mean_demand': [10, 20, 15]
})
# Initialize and fit ElasticityBasedPricer
pricer = ElasticityBasedPricer(alpha=0.1, price_floor=50.0, price_ceil=300.0)
pricer.fit(historical_data)
# Create a mock state space with demand deviations
class MockStateSpace:
def __init__(self, demand):
self.demand = demand
# Simulate demand higher than mean for all products
state_space = MockStateSpace(demand=[15, 25, 20])
# Predict prices
predicted_prices = pricer.predict(state_space)
# Assert that predicted prices are within bounds
assert predicted_prices.min() >= 50.0, "Predicted prices are below minimum bound"
assert predicted_prices.max() <= 300.0, "Predicted prices are above maximum bound"
assert len(predicted_prices) == len(historical_data), "Number of predicted prices does not match number of products"
# now we gotta check semantic validity
# since demand is higher than mean, prices should generally increase
for i, row in historical_data.iterrows():
base_price = row['base_price']
elasticity = row['elasticity']
expected_increase = base_price * (1 + 0.1 * abs(elasticity) * ((state_space.demand[i] - row['mean_demand']) / row['mean_demand']))
assert predicted_prices[i] >= base_price, f"Predicted price for product {row['productId']} did not increase as expected"
assert abs(predicted_prices[i] - expected_increase) < 1e-5, f"Predicted price for product {row['productId']} does not match expected calculation within 1e-5 tolerance"

8
experiments/pytest.ini Normal file
View File

@@ -0,0 +1,8 @@
[pytest]
pythonpath = .
testpaths = procesing/tests agents
python_files = test*.py
python_classes = Test*
python_functions = test_*
asyncio_mode = auto
asyncio_default_fixture_loop_scope = function

View File

@@ -0,0 +1,125 @@
import random
import json
import os
import logging
from dotenv import load_dotenv
from supabase import create_client, Client
from tqdm import tqdm
load_dotenv()
logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
log = logging.getLogger(__name__)
SUPABASE_URL = os.getenv("NEXT_PUBLIC_SUPABASE_URL")
SUPABASE_SERVICE_KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY")
if not SUPABASE_SERVICE_KEY:
log.error("SUPABASE_SERVICE_ROLE_KEY not found in environment")
raise ValueError("Missing SUPABASE_SERVICE_ROLE_KEY - required for admin operations")
supabase: Client = create_client(SUPABASE_URL, SUPABASE_SERVICE_KEY)
DAYS = 14
# hotel room configurations
ROOMS = {
"Presidential Suite": {'amenities': ['ocean_view', 'balcony', 'jacuzzi', 'butler_service', 'premium_minibar'], 'total': 1, 'image_url': "", "base_price": 450, 'name': 'Presidential Suite', 'refundable': True, 'max_occupancy': 4},
"Executive Suite": {'amenities': ['city_view', 'balcony', 'workspace', 'lounge_access'], 'total': 2, 'image_url': "", "base_price": 280, 'name': 'Executive Suite', 'refundable': True, 'max_occupancy': 3},
"Junior Suite": {'amenities': ['garden_view', 'mini_fridge', 'coffee_maker'], 'total': 5, 'image_url': "", "base_price": 180, 'name': 'Junior Suite', 'refundable': True, 'max_occupancy': 2},
"Deluxe Room": {'amenities': ['city_view', 'work_desk', 'coffee_maker'], 'total': 8, 'image_url': "", "base_price": 140, 'name': 'Deluxe Room', 'refundable': False, 'max_occupancy': 2},
"Superior Room": {'amenities': ['wifi', 'tv', 'safe'], 'total': 12, 'image_url': "", "base_price": 110, 'name': 'Superior Room', 'refundable': False, 'max_occupancy': 2},
"Standard Room": {'amenities': ['wifi', 'tv'], 'total': 20, 'image_url': "", "base_price": 85, 'name': 'Standard Room', 'refundable': False, 'max_occupancy': 2},
}
# flight configurations
FLIGHTS = {
"JFK-LAX-Economy": {'departure': {'time': '08:00', 'airport': 'JFK'}, 'arrival': {'time': '11:30', 'airport': 'LAX'}, 'duration': '5h 30m', 'stops': 0, 'cabin_class': 'economy', 'fare_rule': 'standard', 'refundable': False, 'total': 180, 'base_price': 250},
"JFK-LAX-Business": {'departure': {'time': '08:00', 'airport': 'JFK'}, 'arrival': {'time': '11:30', 'airport': 'LAX'}, 'duration': '5h 30m', 'stops': 0, 'cabin_class': 'business', 'fare_rule': 'flexible', 'refundable': True, 'total': 30, 'base_price': 850},
"ORD-MIA-Economy": {'departure': {'time': '14:15', 'airport': 'ORD'}, 'arrival': {'time': '18:45', 'airport': 'MIA'}, 'duration': '3h 30m', 'stops': 0, 'cabin_class': 'economy', 'fare_rule': 'basic', 'refundable': False, 'total': 200, 'base_price': 180},
"SFO-SEA-Premium": {'departure': {'time': '06:30', 'airport': 'SFO'}, 'arrival': {'time': '08:45', 'airport': 'SEA'}, 'duration': '2h 15m', 'stops': 0, 'cabin_class': 'premium', 'fare_rule': 'standard', 'refundable': False, 'total': 60, 'base_price': 420},
"ATL-DFW-First": {'departure': {'time': '16:00', 'airport': 'ATL'}, 'arrival': {'time': '17:30', 'airport': 'DFW'}, 'duration': '2h 30m', 'stops': 0, 'cabin_class': 'first', 'fare_rule': 'flexible', 'refundable': True, 'total': 12, 'base_price': 1600},
"LAX-SFO-Economy": {'departure': {'time': '10:00', 'airport': 'LAX'}, 'arrival': {'time': '11:30', 'airport': 'SFO'}, 'duration': '1h 30m', 'stops': 0, 'cabin_class': 'economy', 'fare_rule': 'standard', 'refundable': False, 'total': 150, 'base_price': 120},
"MIA-ATL-Premium": {'departure': {'time': '19:00', 'airport': 'MIA'}, 'arrival': {'time': '20:45', 'airport': 'ATL'}, 'duration': '1h 45m', 'stops': 0, 'cabin_class': 'premium', 'fare_rule': 'standard', 'refundable': True, 'total': 50, 'base_price': 380},
"DFW-ORD-Economy": {'departure': {'time': '07:30', 'airport': 'DFW'}, 'arrival': {'time': '10:15', 'airport': 'ORD'}, 'duration': '2h 45m', 'stops': 0, 'cabin_class': 'economy', 'fare_rule': 'basic', 'refundable': False, 'total': 190, 'base_price': 160},
"SEA-LAX-Business": {'departure': {'time': '13:00', 'airport': 'SEA'}, 'arrival': {'time': '15:30', 'airport': 'LAX'}, 'duration': '2h 30m', 'stops': 0, 'cabin_class': 'business', 'fare_rule': 'flexible', 'refundable': True, 'total': 40, 'base_price': 720},
"LAX-JFK-First": {'departure': {'time': '18:00', 'airport': 'LAX'}, 'arrival': {'time': '02:15', 'airport': 'JFK'}, 'duration': '5h 15m', 'stops': 0, 'cabin_class': 'first', 'fare_rule': 'flexible', 'refundable': True, 'total': 16, 'base_price': 1850},
}
def gen_hotel_products():
"""generate hotel room products for next DAYS days"""
data = []
for day in range(DAYS):
for room_type, rdata in ROOMS.items():
data.append({
'room_type': room_type,
'date_index': day + 1,
'metadata': rdata,
'availability': random.randint(0, rdata['total'])
})
return data
def gen_airline_products():
"""generate flight products for next DAYS days"""
data = []
for day in range(DAYS):
for flight_type, fdata in FLIGHTS.items():
data.append({
'flight_type': flight_type,
'date_index': day + 1,
'metadata': fdata,
'availability': random.randint(0, fdata['total'])
})
return data
def clear_table(table_name: str):
"""clear all records from a table"""
try:
resp = supabase.table(table_name).select('id').execute()
if resp.data:
ids = [row['id'] for row in resp.data]
chunk_size = 100
for i in tqdm(range(0, len(ids), chunk_size), desc=f"Clearing {table_name}", unit="chunk"):
chunk = ids[i:i+chunk_size]
supabase.table(table_name).delete().in_('id', chunk).execute()
log.info(f"Deleted {len(ids)} records from {table_name}")
else:
log.info(f"{table_name} already empty")
except Exception as e:
log.error(f"Failed to clear {table_name}: {e}")
raise
def seed_table(table_name: str, data: list[dict]):
"""insert records into a table"""
try:
chunk_size = 100
total = len(data)
for i in tqdm(range(0, total, chunk_size), desc=f"Seeding {table_name}", unit="chunk"):
chunk = data[i:i+chunk_size]
supabase.table(table_name).insert(chunk).execute()
log.info(f"Inserted {total} records into {table_name}")
except Exception as e:
log.error(f"Failed to seed {table_name}: {e}")
raise
def main():
log.info("Generating hotel products...")
hotel_products = gen_hotel_products()
log.info(f"Generated {len(hotel_products)} hotel products")
log.info("Generating airline products...")
airline_products = gen_airline_products()
log.info(f"Generated {len(airline_products)} airline products\n")
log.info("Clearing existing products...")
clear_table('hotel_products')
clear_table('airline_products')
log.info("Seeding products...")
seed_table('hotel_products', hotel_products)
seed_table('airline_products', airline_products)
if __name__ == "__main__":
main()

41
lib/__init__.py Normal file
View File

@@ -0,0 +1,41 @@
"""PHANTOM shared library
Exports unified utilities for features, state, config, kafka, and model registry
"""
from .config import (
PROJECT_ROOT, DATA_DIR, EXPERIMENTS_DIR,
AGENT_DATA_DIR, HUMAN_DATA_DIR, SIM_RUNS_DIR, MODEL_REGISTRY_DIR,
COLLECTED_DATA_DIR, NOTEBOOK_OUTPUT_DIR,
ensure_dir, get_data_path, get_experiments_path, get_sim_path,
KAFKA_HOST, KAFKA_PORT, KAFKA_BROKER,
REDIS_HOST, REDIS_PORT,
SUPABASE_URL, SUPABASE_ANON_KEY,
BACKEND_PORT, PROVIDER_PORT
)
from .state import (
make_state_repr, event_to_state, parse_state,
get_event_name, get_timestamp,
create_state_fn, create_event_name_fn, create_timestamp_fn
)
from .features import (
transition_histogram, temporal_signature, state_coverage, transition_entropy,
event_type_distribution, featurize_trajectory, parse_timestamp
)
__all__ = [
# config
'PROJECT_ROOT', 'DATA_DIR', 'EXPERIMENTS_DIR',
'AGENT_DATA_DIR', 'HUMAN_DATA_DIR', 'SIM_RUNS_DIR', 'MODEL_REGISTRY_DIR',
'COLLECTED_DATA_DIR', 'NOTEBOOK_OUTPUT_DIR',
'ensure_dir', 'get_data_path', 'get_experiments_path', 'get_sim_path',
'KAFKA_HOST', 'KAFKA_PORT', 'KAFKA_BROKER',
'REDIS_HOST', 'REDIS_PORT',
'SUPABASE_URL', 'SUPABASE_ANON_KEY',
'BACKEND_PORT', 'PROVIDER_PORT',
# state
'make_state_repr', 'event_to_state', 'parse_state',
'get_event_name', 'get_timestamp',
'create_state_fn', 'create_event_name_fn', 'create_timestamp_fn',
# features
'transition_histogram', 'temporal_signature', 'state_coverage', 'transition_entropy',
'event_type_distribution', 'featurize_trajectory', 'parse_timestamp',
]

65
lib/config.py Normal file
View File

@@ -0,0 +1,65 @@
"""Unified path configuration for PHANTOM project
All hardcoded paths should reference this module
Paths can be overridden via environment variables
"""
import os
from pathlib import Path
# project root (directory containing lib/, experiments/, sim/, web/, backend/)
PROJECT_ROOT = Path(__file__).parent.parent.resolve()
# data directories
DATA_DIR = Path(os.getenv('PHANTOM_DATA_DIR', PROJECT_ROOT / 'data'))
EXPERIMENTS_DIR = Path(os.getenv('PHANTOM_EXPERIMENTS_DIR', PROJECT_ROOT / 'experiments'))
# agent/human interaction data
AGENT_DATA_DIR = Path(os.getenv('PHANTOM_AGENT_DATA_DIR', DATA_DIR / 'agents'))
HUMAN_DATA_DIR = Path(os.getenv('PHANTOM_HUMAN_DATA_DIR', DATA_DIR / 'humans'))
# RL simulation runs
SIM_RUNS_DIR = Path(os.getenv('PHANTOM_SIM_RUNS_DIR', PROJECT_ROOT / 'sim' / 'rl' / 'runs'))
# model artifacts
MODEL_REGISTRY_DIR = Path(os.getenv('PHANTOM_MODEL_REGISTRY_DIR', DATA_DIR / 'models'))
# collected experiment data
COLLECTED_DATA_DIR = Path(os.getenv('PHANTOM_COLLECTED_DATA_DIR', EXPERIMENTS_DIR / 'agents' / 'collected_data'))
# notebook outputs
NOTEBOOK_OUTPUT_DIR = Path(os.getenv('PHANTOM_NOTEBOOK_OUTPUT_DIR', EXPERIMENTS_DIR / 'notebooks' / 'outputs'))
def ensure_dir(path: Path) -> Path:
"""ensure directory exists, create if needed"""
path.mkdir(parents=True, exist_ok=True)
return path
def get_data_path(*parts: str) -> Path:
"""construct path relative to DATA_DIR"""
return DATA_DIR.joinpath(*parts)
def get_experiments_path(*parts: str) -> Path:
"""construct path relative to EXPERIMENTS_DIR"""
return EXPERIMENTS_DIR.joinpath(*parts)
def get_sim_path(*parts: str) -> Path:
"""construct path relative to SIM_RUNS_DIR"""
return SIM_RUNS_DIR.joinpath(*parts)
# service configuration (from .env)
KAFKA_HOST = os.getenv('KAFKA_HOST', 'localhost')
KAFKA_PORT = os.getenv('KAFKA_PORT', '9092')
KAFKA_BROKER = f"{KAFKA_HOST}:{KAFKA_PORT}"
REDIS_HOST = os.getenv('REDIS_HOST', 'localhost')
REDIS_PORT = int(os.getenv('REDIS_PORT', '6379'))
SUPABASE_URL = os.getenv('NEXT_PUBLIC_SUPABASE_URL', '')
SUPABASE_ANON_KEY = os.getenv('NEXT_PUBLIC_SUPABASE_ANON_KEY', '')
BACKEND_PORT = int(os.getenv('BACKEND_PORT', '5000'))
PROVIDER_PORT = int(os.getenv('PROVIDER_PORT', '5001'))

125
lib/features.py Normal file
View File

@@ -0,0 +1,125 @@
"""Unified featurization utilities for trajectory -> feature vector conversion
Used by both experiments/ml/ and sim/rl/ components
"""
import numpy as np
from collections import defaultdict
from typing import List, Dict, Callable, Optional, Any, Set
from datetime import datetime
def transition_histogram(events: List, state_fn: Callable, max_states: int = 50) -> np.ndarray:
"""compute normalized histogram of state transitions in trajectory
events: list of event objects/dicts
state_fn: function mapping event -> state string
max_states: maximum dimensions for histogram
"""
if len(events) < 2:
return np.zeros(max_states, dtype=np.float32)
states = [state_fn(e) for e in events]
trans_counts = defaultdict(int)
for s, s_next in zip(states, states[1:]):
trans_counts[(s, s_next)] += 1
total = sum(trans_counts.values())
hist = np.array(list(trans_counts.values())[:max_states], dtype=np.float32)
hist = np.pad(hist, (0, max(0, max_states - len(hist))))
return hist / (total + 1e-10)
def temporal_signature(events: List, ts_fn: Callable) -> np.ndarray:
"""extract temporal features: mean/std/skew of inter-event times plus count
events: list of event objects/dicts
ts_fn: function mapping event -> timestamp (float seconds)
returns: [mean_dt, std_dt, skew, n_intervals] array
"""
if len(events) < 2:
return np.zeros(4, dtype=np.float32)
times = sorted([ts_fn(e) for e in events])
diffs = np.diff(times).astype(np.float32)
if len(diffs) == 0:
return np.zeros(4, dtype=np.float32)
mean_dt, std_dt = np.mean(diffs), np.std(diffs) + 1e-10
skew = np.mean(((diffs - mean_dt) / std_dt) ** 3) if std_dt > 1e-8 else 0.0
return np.array([mean_dt, std_dt, skew, len(diffs)], dtype=np.float32)
def state_coverage(events: List, state_fn: Callable, mdp_states: Set[str]) -> float:
"""fraction of MDP states visited by trajectory
events: list of event objects/dicts
state_fn: function mapping event -> state string
mdp_states: set of all possible MDP states
"""
if not mdp_states:
return 0.0
visited = set(state_fn(e) for e in events)
return len(visited & mdp_states) / len(mdp_states)
def transition_entropy(events: List, state_fn: Callable) -> float:
"""compute entropy of transition distribution (randomness of navigation)
higher entropy = more random browsing pattern
"""
if len(events) < 2:
return 0.0
states = [state_fn(e) for e in events]
trans_counts = defaultdict(int)
for s, s_next in zip(states, states[1:]):
trans_counts[(s, s_next)] += 1
total = sum(trans_counts.values())
probs = [c / total for c in trans_counts.values()]
return -sum(p * np.log(p + 1e-10) for p in probs)
def event_type_distribution(events: List, event_name_fn: Callable) -> np.ndarray:
"""compute proportions of different event type categories
returns: [page_view_ratio, hover_ratio, cart_ratio, purchase_ratio]
"""
if not events:
return np.zeros(4, dtype=np.float32)
n = len(events)
names = [event_name_fn(e).lower() for e in events]
return np.array([
sum(1 for nm in names if 'page' in nm or 'view' in nm) / n,
sum(1 for nm in names if 'hover' in nm) / n,
sum(1 for nm in names if 'cart' in nm) / n,
sum(1 for nm in names if 'purchase' in nm or 'checkout' in nm) / n
], dtype=np.float32)
def featurize_trajectory(events: List, state_fn: Callable, ts_fn: Callable,
event_name_fn: Callable, mdp_states: Optional[Set[str]] = None,
output_dim: int = 64) -> np.ndarray:
"""convert trajectory to fixed-dimension feature vector
events: list of event objects/dicts
state_fn: function mapping event -> state string
ts_fn: function mapping event -> timestamp (float)
event_name_fn: function mapping event -> event name string
mdp_states: optional set of all MDP states for coverage calculation
output_dim: desired output dimension (will pad/truncate)
"""
feats = []
feats.extend(transition_histogram(events, state_fn, max_states=40)) # 40 dims
feats.extend(temporal_signature(events, ts_fn)) # 4 dims
feats.append(state_coverage(events, state_fn, mdp_states or set())) # 1 dim
feats.append(transition_entropy(events, state_fn)) # 1 dim
feats.append(float(len(events))) # trajectory length
feats.append(float(len(set(state_fn(e) for e in events)))) # unique states
feats.extend(event_type_distribution(events, event_name_fn)) # 4 dims
feats = np.array(feats[:output_dim], dtype=np.float32)
if len(feats) < output_dim:
feats = np.pad(feats, (0, output_dim - len(feats)))
return feats
def parse_timestamp(ts: Any) -> float:
"""parse various timestamp formats to float seconds"""
if ts is None:
return 0.0
if isinstance(ts, (int, float)):
return float(ts)
if isinstance(ts, str):
try:
return datetime.fromisoformat(ts.replace('Z', '+00:00')).timestamp()
except ValueError:
return 0.0
return 0.0

54
lib/kafka_client.py Executable file
View File

@@ -0,0 +1,54 @@
from kafka import KafkaConsumer
import json
import os
from dotenv import load_dotenv
load_dotenv()
def get_interactions(
topic='user-interactions',
bootstrap_servers=None,
from_beginning=True,
max_records=None,
timeout_ms=5000
):
"""Consume interaction events from Kafka.
Args:
topic: Kafka topic name
bootstrap_servers: Kafka broker address (default from env)
from_beginning: Start from earliest offset if True
max_records: Max number of records to fetch (None = all available)
timeout_ms: Consumer poll timeout
Returns:
List of parsed interaction event dicts
"""
if not bootstrap_servers:
host = os.getenv('KAFKA_HOST', 'localhost')
port = os.getenv('KAFKA_PORT', '9092')
bootstrap_servers = f'{host}:{port}'
consumer = KafkaConsumer(
topic,
bootstrap_servers=bootstrap_servers,
auto_offset_reset='earliest' if from_beginning else 'latest',
enable_auto_commit=False,
value_deserializer=lambda m: json.loads(m.decode('utf-8')),
consumer_timeout_ms=timeout_ms
)
events = []
try:
for msg in consumer:
events.append(msg.value)
if max_records and len(events) >= max_records:
break
finally:
consumer.close()
return events
if __name__ == '__main__':
interactions = get_interactions(max_records=10)
for event in interactions:
print(event)

226
lib/model_registry.py Executable file
View File

@@ -0,0 +1,226 @@
import redis
import pickle
import json
import pandas as pd
from typing import Optional, Dict, Any
import os
import logging
log = logging.getLogger(__name__)
class ModelRegistry:
"""
Lightweight model registry using Redis for storing pricing models and elasticity data.
Models are serialized using pickle, metadata stored as JSON.
"""
def __init__(self, redis_host: str = None, redis_port: int = None):
host = redis_host or os.getenv('REDIS_HOST', 'localhost')
port = redis_port or int(os.getenv('REDIS_PORT', '6378'))
self.redis_client = redis.Redis(
host=host,
port=port,
db=0,
decode_responses=False
)
self.metadata_prefix = "model:meta:"
self.data_prefix = "model:data:"
self.elasticity_prefix = "elasticity:"
self.prices_prefix = "prices:"
def publish_elasticity(self,
elasticity_df: pd.DataFrame,
model_name: str = 'latest',
metadata: Optional[Dict[str, Any]] = None):
"""
Store elasticity estimates in registry.
Args:
elasticity_df: df with [productId, elasticity, std_error, n_obs]
model_name: identifier for this elasticity snapshot
metadata: additional info (timestamp, window_size, etc)
"""
key = f"{self.elasticity_prefix}{model_name}"
# serialize dataframe as JSON
data_json = elasticity_df.to_json(orient='records')
# store data
self.redis_client.set(key, data_json)
# store metadata
meta = metadata or {}
meta.update({
'n_products': len(elasticity_df),
'mean_elasticity': float(elasticity_df['elasticity'].mean()),
'model_type': 'elasticity_snapshot'
})
meta_key = f"{self.metadata_prefix}{model_name}"
self.redis_client.set(meta_key, json.dumps(meta))
log.info(f"Published elasticity model '{model_name}' with {len(elasticity_df)} products")
def get_elasticity(self, model_name: str = 'latest') -> Optional[pd.DataFrame]:
"""Retrieve elasticity estimates from registry."""
key = f"{self.elasticity_prefix}{model_name}"
data_json = self.redis_client.get(key)
if data_json is None:
return None
# decode bytes to string if needed
if isinstance(data_json, bytes):
data_json = data_json.decode('utf-8')
return pd.read_json(data_json, orient='records')
def publish_pricing_model(self,
pricing_function,
model_name: str = 'latest',
metadata: Optional[Dict[str, Any]] = None):
"""
Store a fitted pricing function object.
Args:
pricing_function: fitted PricingFunction instance
model_name: identifier
metadata: additional info
"""
key = f"{self.data_prefix}{model_name}"
# serialize object
model_bytes = pickle.dumps(pricing_function)
self.redis_client.set(key, model_bytes)
# store metadata
meta = metadata or {}
meta.update({
'model_class': pricing_function.__class__.__name__,
'model_type': 'pricing_function'
})
meta_key = f"{self.metadata_prefix}{model_name}"
self.redis_client.set(meta_key, json.dumps(meta))
log.info(f"Published pricing model '{model_name}' ({meta['model_class']})")
def get_pricing_model(self, model_name: str = 'latest'):
"""Retrieve a pricing function from registry."""
key = f"{self.data_prefix}{model_name}"
model_bytes = self.redis_client.get(key)
if model_bytes is None:
return None
return pickle.loads(model_bytes)
def list_models(self) -> Dict[str, Any]:
"""List all registered models with metadata."""
models = {}
for key in self.redis_client.scan_iter(f"{self.metadata_prefix}*"):
key_str = key.decode('utf-8') if isinstance(key, bytes) else key
model_name = key_str.replace(self.metadata_prefix, '')
meta_json = self.redis_client.get(key)
if meta_json:
if isinstance(meta_json, bytes):
meta_json = meta_json.decode('utf-8')
models[model_name] = json.loads(meta_json)
return models
def publish_prices(self,
prices_df: pd.DataFrame,
model_name: str = 'latest',
metadata: Optional[Dict[str, Any]] = None):
"""Store predicted prices in registry.
Args:
prices_df: df with [productId, predicted_price, ...]
model_name: identifier for this price snapshot
metadata: additional info
"""
key = f"{self.prices_prefix}{model_name}"
data_json = prices_df.to_json(orient='records')
self.redis_client.set(key, data_json)
meta = metadata or {}
meta.update({
'n_products': len(prices_df),
'model_type': 'predicted_prices'
})
meta_key = f"{self.metadata_prefix}prices_{model_name}"
self.redis_client.set(meta_key, json.dumps(meta))
log.info(f"Published prices '{model_name}' for {len(prices_df)} products")
def get_prices(self, model_name: str = 'latest') -> Optional[pd.DataFrame]:
"""Retrieve predicted prices from registry."""
key = f"{self.prices_prefix}{model_name}"
data_json = self.redis_client.get(key)
if data_json is None:
return None
if isinstance(data_json, bytes):
data_json = data_json.decode('utf-8')
return pd.read_json(data_json, orient='records')
def health_check(self) -> bool:
"""Check if Redis connection is alive."""
try:
self.redis_client.ping()
return True
except:
return False
def set_session_prices(self, session_id: str, prices: Dict[str, float], ttl: int = 1800):
"""
Store prices for a specific session.
THIS is the write path for session-aware pricing.
Args:
session_id: session identifier
prices: dict of {productId: price}
ttl: time-to-live in seconds (default 30min)
"""
if not prices:
return
key = f"session:{session_id}:prices"
# use Redis hash for O(1) lookup per product
self.redis_client.hset(key, mapping={k: str(v) for k, v in prices.items()})
self.redis_client.expire(key, ttl)
def get_session_price(self, session_id: str, product_id: str) -> Optional[float]:
"""
Lookup price for (sessionId, productId).
THIS is the read path for fast provider lookup.
Returns: price or None if not found
"""
key = f"session:{session_id}:prices"
price_str = self.redis_client.hget(key, product_id)
if price_str is None:
return None
return float(price_str.decode('utf-8') if isinstance(price_str, bytes) else price_str)
def get_session_all_prices(self, session_id: str) -> Dict[str, float]:
"""Get all prices for a session."""
key = f"session:{session_id}:prices"
prices_raw = self.redis_client.hgetall(key)
if not prices_raw:
return {}
return {
(k.decode('utf-8') if isinstance(k, bytes) else k): float(v.decode('utf-8') if isinstance(v, bytes) else v)
for k, v in prices_raw.items()
}

72
lib/state.py Normal file
View File

@@ -0,0 +1,72 @@
"""Unified state representation utilities for MDP state encoding
Used by both experiments/ and sim/ components for consistent state handling
"""
from typing import Any, Callable
def make_state_repr(page: str = None, product_id: str = None, event_name: str = None) -> str:
"""create canonical state representation string from components
format: page|productId|eventName
"""
p = page or 'unk'
pid = product_id or 'none'
en = event_name or 'unknown'
return f"{p}|{pid}|{en}"
def event_to_state(evt: Any) -> str:
"""convert event object/dict to state string
supports both object attributes and dict keys
"""
if isinstance(evt, dict):
return make_state_repr(
page=evt.get('page'),
product_id=evt.get('productId'),
event_name=evt.get('eventName') or evt.get('event_type')
)
return make_state_repr(
page=getattr(evt, 'page', None),
product_id=getattr(evt, 'productId', None),
event_name=getattr(evt, 'eventName', None) or getattr(evt, 'event_type', None)
)
def parse_state(state_str: str) -> dict:
"""parse state string back to components
returns: {'page': str, 'productId': str, 'eventName': str}
"""
parts = state_str.split('|')
return {
'page': parts[0] if len(parts) > 0 and parts[0] != 'unk' else None,
'productId': parts[1] if len(parts) > 1 and parts[1] != 'none' else None,
'eventName': parts[2] if len(parts) > 2 and parts[2] != 'unknown' else None
}
def get_event_name(evt: Any) -> str:
"""extract event name from event object/dict"""
if isinstance(evt, dict):
return evt.get('eventName') or evt.get('event_type') or ''
return getattr(evt, 'eventName', None) or getattr(evt, 'event_type', None) or ''
def get_timestamp(evt: Any) -> Any:
"""extract timestamp from event object/dict"""
if isinstance(evt, dict):
return evt.get('ts') or evt.get('timestamp')
return getattr(evt, 'ts', None) or getattr(evt, 'timestamp', None)
def create_state_fn() -> Callable:
"""factory for state representation function"""
return event_to_state
def create_event_name_fn() -> Callable:
"""factory for event name extraction function"""
return get_event_name
def create_timestamp_fn() -> Callable:
"""factory for timestamp extraction function (returns raw value, use features.parse_timestamp to convert)"""
return get_timestamp

View File

@@ -1,8 +1,6 @@
$pdf_mode = 1;
$pdflatex = 'pdflatex -synctex=1 -interaction=nonstopmode -file-line-error %O %S';
$aux_dir = 'build';
$out_dir = 'build';
$use_biber = 0; # force bibtex
$bibtex_use = 2; # run bibtex when needed
$bibtex = 'bibtex %O %B';
$pdf_previewer = 'zathura %O %S';
$clean_ext = 'synctex.gz bbl bcf run.xml fls fdb_latexmk glg glo gls ist blg lof lot out toc';

View File

@@ -16,11 +16,15 @@ mkdir -p "$(dirname "$OUTPUT_FILE")"
add_file() {
local filepath="$1"
local relpath="${filepath#$PROJECT_ROOT/}"
local escaped_path="${relpath//_/\\_}"
# Add section header and code listing (no language-specific highlighting)
echo "\\subsection{${relpath}}" >> "$OUTPUT_FILE"
echo "\\begin{lstlisting}[caption={${relpath}}]" >> "$OUTPUT_FILE"
cat "$filepath" >> "$OUTPUT_FILE"
echo "\\subsection{${escaped_path}}" >> "$OUTPUT_FILE"
echo "\\begin{lstlisting}[caption={${escaped_path}}]" >> "$OUTPUT_FILE"
# Convert to ASCII: transliterate what's possible, drop the rest
# LC_ALL=C forces ASCII locale for consistent behavior across environments
LC_ALL=C iconv -f UTF-8 -t ASCII//TRANSLIT//IGNORE "$filepath" 2>/dev/null >> "$OUTPUT_FILE" || \
LC_ALL=C tr -cd '\11\12\15\40-\176' < "$filepath" >> "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE"
echo "\\end{lstlisting}" >> "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE"
@@ -39,22 +43,22 @@ EOF
echo "Concatenating code from source directories..."
# Backend
find "$PROJECT_ROOT/backend" -type f \( -name "*.py" -o -name "*.js" -o -name "*.sh" -o -name "*.yml" -o -name "*.yaml" \) | sort | while read -r file; do
find "$PROJECT_ROOT/backend" -type d \( -name ".venv" -o -name "__pycache__" -o -name "*.egg-info" -o -name "node_modules" -o -name ".pytest_cache" \) -prune -o -type f \( -name "*.py" -o -name "*.js" -o -name "*.sh" -o -name "*.yml" -o -name "*.yaml" \) ! -name "*.pyc" ! -name "*.pyo" -print | sort | while read -r file; do
add_file "$file"
done
# Experiments
find "$PROJECT_ROOT/experiments" -type f \( -name "*.py" -o -name "*.js" -o -name "*.sh" -o -name "*.yml" -o -name "*.yaml" \) | sort | while read -r file; do
find "$PROJECT_ROOT/experiments" -type d \( -name ".venv" -o -name "__pycache__" -o -name "*.egg-info" -o -name "node_modules" -o -name ".pytest_cache" -o -name ".ipynb_checkpoints" \) -prune -o -type f \( -name "*.py" -o -name "*.js" -o -name "*.sh" -o -name "*.yml" -o -name "*.yaml" \) ! -name "*.pyc" ! -name "*.pyo" -print | sort | while read -r file; do
add_file "$file"
done
# Docker
find "$PROJECT_ROOT/docker" -type f \( -name "*.py" -o -name "*.sh" -o -name "*.yml" -o -name "*.yaml" -o -name "Dockerfile*" \) | sort | while read -r file; do
find "$PROJECT_ROOT/docker" -type d \( -name ".venv" -o -name "__pycache__" -o -name "node_modules" \) -prune -o -type f \( -name "*.py" -o -name "*.sh" -o -name "*.yml" -o -name "*.yaml" -o -name "Dockerfile*" \) ! -name "*.pyc" ! -name "*.pyo" -print | sort | while read -r file; do
add_file "$file"
done
# Web/src
find "$PROJECT_ROOT/web/src" -type f \( -name "*.js" -o -name "*.jsx" -o -name "*.ts" -o -name "*.tsx" \) | sort | while read -r file; do
find "$PROJECT_ROOT/web/src" -type d \( -name "node_modules" -o -name ".next" -o -name "dist" -o -name "build" \) -prune -o -type f \( -name "*.js" -o -name "*.jsx" -o -name "*.ts" -o -name "*.tsx" \) -print | sort | while read -r file; do
add_file "$file"
done

View File

@@ -6,15 +6,18 @@
(setq TeX-command-extra-options
"-file-line-error -interaction=nonstopmode")
(TeX-add-to-alist 'LaTeX-provided-class-options
'(("report" "12pt") ("article" "12pt") ("acmart" "sigconf" "nonacm" "natbib=false")))
'(("report" "12pt") ("acmart" "sigconf" "nonacm" "natbib=false" "manuscript") ("article" "12pt" "letterpaper")))
(TeX-run-style-hooks
"latex2e"
"preamble"
"acmart"
"acmart10")
(TeX-add-symbols
'("footnotetextcopyrightpermission" 1))
(LaTeX-add-labels
"research"))
"chapters/01-intro"
"chapters/02-literature-review"
"chapters/03-methodology"
"chapters/04-results"
"chapters/05-discussion"
"chapters/06-conclusion"
"../build/concatenated_code"
"article"
"art12"))
:latex)

View File

@@ -1,19 +1,34 @@
@techReport{,
abstract = {We consider a single product revenue management problem where, given an initial inventory, the objective is to dynamically adjust prices over a finite sales horizon to maximize expected revenues. Realized demand is observed over time, but the underlying functional relationship between price and mean demand rate that governs these observations (otherwise known as the demand function or demand curve), is not known. We consider two instances of this problem: i.) a setting where the demand function is assumed to belong to a known parametric family with unknown parameter values; and ii.) a setting where the demand function is assumed to belong to a broad class of functions that need not admit any parametric representation. In each case we develop policies that learn the demand function "on the fly," and optimize prices based on that. The performance of these algorithms is measured in terms of the regret: the revenue loss relative to the maximal revenues that can be extracted when the demand function is known prior to the start of the selling season. We derive lower bounds on the regret that hold for any admissible pricing policy, and then show that our proposed algorithms achieve a regret that is "close" to this lower bound. The magnitude of the regret can be interpreted as the economic value of prior knowledge on the demand function; manifested as the revenue loss due to model uncertainty.},
author = {Omar Besbes and Assaf Zeevi},
journal = {Operations Research},
keywords = {Revenue management,asymptotic analysis,estimation,exploration-exploitation,learning,pricing,value of information},
title = {Dynamic Pricing Without Knowing the Demand Function: Risk Bounds and Near-Optimal Algorithms *}
@article{arnoud_v_den_boer_dynamic_2015,
title = {Dynamic pricing and learning: {Historical} origins, current research, and new directions},
volume = {20},
url = {https://www.sciencedirect.com/science/article/pii/S1876735415000021},
doi = {10.1016/j.sorms.2015.03.001},
number = {1},
journal = {Surveys in Operations Research and Management Science},
author = {{Arnoud V. den Boer}},
month = jun,
year = {2015},
pages = {1--18},
file = {PDF:/home/velocitatem/Zotero/storage/NUAGDYER/memo2025.pdf:application/pdf},
}
@misc{Ghaffary,
author = {Shirin Ghaffary and Matt Day},
note = {Updated 2025-11-05},
title = {Amazon Sues to Stop Perplexity From Using AI Tool to Buy Stuff},
url = {https://www.bloomberg.com/news/articles/2025-11-04/amazon-demands-perplexity-stop-ai-agent-from-making-purchases}
@article{iliou_detection_2021,
title = {Detection of {Advanced} {Web} {Bots} by {Combining} {Web} {Logs} with {Mouse} {Behavioural} {Biometrics}},
volume = {2},
url = {https://dl.acm.org/doi/10.1145/3447815},
doi = {10.1145/3447815},
number = {3},
journal = {Digital Threats: Research and Practice},
author = {Iliou, Christos and Kostoulas, Theodoros and Tsikrika, Theodora and Katos, Vasilis and Vrochidis, Stefanos and Kompatsiaris, Ioannis},
year = {2021},
pages = {1--26},
file = {PDF:/home/velocitatem/Zotero/storage/Q7J5EBEJ/3447815.pdf:application/pdf},
}
@phdthesis{,
abstract = {Algorithmic pricing is an emerging business practice that uses computational algorithms to determine
@phdthesis{salassa_politecnico_nodate,
title = {Politecnico di {Torino} {Algorithmic} {Pricing} in the digital age "{Ethical} considerations on its economic and social implications, and an analysis of possible solutions to overcome its critical issues" {Tutor}: {Candidate}},
abstract = {Algorithmic pricing is an emerging business practice that uses computational algorithms to determine
the prices of products and services based on a number of dynamic factors. The aim of this thesis is to
draw attention to the existence of these business practices, and the ethical and social implications that
derive from them, and then focus on what could be effective solutions to increase the well-being of
@@ -32,75 +47,379 @@ Chapter 6, which will focus on the role of the government, as a regulatory, of t
must be encouraged to educate and inform himself about the use of these practices, and of the
company, as responsible for making its customers aware and acting in compliance with government
laws, for fair and non-discriminatory use.},
author = {Fabio Salassa and Paolo Pautassi},
school = {Politecnico di Torino},
title = {Politecnico di Torino Algorithmic Pricing in the digital age "Ethical considerations on its economic and social implications, and an analysis of possible solutions to overcome its critical issues" Tutor: Candidate},
url = {https://webthesis.biblio.polito.it/secure/31375/1/tesi.pdf}
urldate = {2025-11-12},
school = {Politecnico di Torino},
author = {Salassa, Fabio and Pautassi, Paolo},
file = {PDF:/home/velocitatem/Zotero/storage/L95WYQ8B/m-api-06aad998-d926-0d59-5593-82fdce5a678b.pdf:application/pdf},
}
@inproceedings{Mueller2019,
author = {Jonas W Mueller and Vasilis Syrgkanis and Matt Taddy},
booktitle = {Advances in Neural Information Processing Systems 32 (NeurIPS 2019)},
pages = {15442-15452},
title = {Low-Rank Bandit Methods for High-Dimensional Dynamic Pricing},
url = {https://proceedings.neurips.cc/paper/2019/file/0a3df70393993583a13c0dd6686f3f32-Paper.pdf},
year = {2019}
@inproceedings{mueller_low-rank_2019,
title = {Low-{Rank} {Bandit} {Methods} for {High}-{Dimensional} {Dynamic} {Pricing}},
booktitle = {Advances in {Neural} {Information} {Processing} {Systems} 32 ({NeurIPS} 2019)},
author = {Mueller, Jonas W and Syrgkanis, Vasilis and Taddy, Matt},
year = {2019},
pages = {15442--15452},
file = {PDF:/home/velocitatem/Zotero/storage/IZD3C5SR/m-api-26f6207c-cc89-4aed-29b6-34629f18fe9b.pdf:application/pdf},
}
@article{Amjad2017,
abstract = { In this paper, the question of interest is estimating true demand of a product at a given store location and time period in the retail environment based on a single noisy and potentially censored observation. To address this question, we introduce a %non-parametric framework to make inference from multiple time series. Somewhat surprisingly, we establish that the algorithm introduced for the purpose of "matrix completion" can be used to solve the relevant inference problem. Specifically, using the Universal Singular Value Thresholding (USVT) algorithm [7], we show that our estimator is consistent: the average mean squared error of the estimated average demand with respect to the true average demand goes to 0 as the number of store locations and time intervals increase to $\infty$. We establish naturally appealing properties of the resulting estimator both analytically as well as through a sequence of instructive simulations. Using a real dataset in retail (Walmart), we argue for the practical relevance of our approach. },
author = {Muhammad J. Amjad and Devavrat Shah},
doi = {10.1145/3154489},
issue = {2},
journal = {Proceedings of the ACM on Measurement and Analysis of Computing Systems},
month = {12},
pages = {1-28},
publisher = {Association for Computing Machinery (ACM)},
title = {Censored Demand Estimation in Retail},
volume = {1},
url = {https://par.nsf.gov/servlets/purl/10066022},
year = {2017}
@article{shahidi_coasean_nodate,
title = {The {Coasean} {Singularity}? {Demand}, {Supply}, and {Market} {Design} with {AI} {Agents}},
abstract = {AI agents—autonomous systems that perceive, reason, and act on behalf of human principals—are poised to transform digital markets by dramatically reducing transaction costs. This chapter evaluates the economic implications of this transition, adopting a consumeroriented view of agents as market participants that can search, negotiate, and transact directly. From the demand side, agent adoption reflects derived demand: users trade off decision quality against effort reduction, with outcomes mediated by agent capability and task context. On the supply side, firms will design, integrate, and monetize agents, with outcomes hinging on whether agents operate within or across platforms. At the market level, agents create efficiency gains from lower search, communication, and contracting costs, but also introduce frictions such as congestion and price obfuscation. By lowering the costs of preference elicitation, contract enforcement, and identity verification, agents expand the feasible set of market designs but also raise novel regulatory challenges. While the net welfare effects remain an empirical question, the rapid onset of AI-mediated transactions presents a unique opportunity for economic research to inform real-world policy and market design.},
language = {en},
author = {Shahidi, Peyman and Rusak, Gili and Manning, Benjamin S and Fradkin, Andrey and Horton, John J},
file = {PDF:/home/velocitatem/Zotero/storage/TQCAPJDP/Shahidi et al. - The Coasean Singularity Demand, Supply, and Market Design with AI Agents.pdf:application/pdf},
}
@article{Prez-Ricardo2025,
abstract = {The study aims to explore tourists' booking intentions by analyzing the price elasticity of demand in tourist accommodations. This analysis should reveal how changes in price affect booking behavior across different customer segments, using online booking records. A dataset was compiled from 106 hotels in Malaga, Spain, comprising 27,910 online bookings sourced exclusively from hotel websites. To understand the price elasticity of demand, a simple log-log regression was applied, segmenting the data based on key revenue-related variables. Subsequently, a cluster segmentation was performed using the Elbow method and K-means algorithm to identify distinct market segments. The findings highlighted that Family Travelers and Short Stay Travelers segments exhibited elastic demand, indicating higher sensitivity to price fluctuations. In contrast, Early Bookers and Mid-Season Long Stayers demonstrated inelastic demand, with lower responsiveness to changes in tourist accommodation prices. The number of variables analyzed in this study, along with the cluster analysis, represent a novelty and contribute to the existing literature on market segmentation and price elasticity of demand. This integration enriches both fields of research, offering mutual benefits and deeper insights that enhance the understanding of booking intention and pricing strategies.},
author = {Elizabeth del Carmen Pérez-Ricardo and Josefa García-Mestanza},
doi = {10.1016/j.iedeen.2025.100271},
issn = {24448834},
issue = {1},
journal = {European Research on Management and Business Economics},
keywords = {Booking intention,Price elasticity,Tourist segmentation},
month = {1},
publisher = {European Academy of Management and Business Economics},
title = {Exploring booking intentions through price elasticity of demand in tourism accommodations using large-scale data analytics},
volume = {31},
year = {2025}
@misc{byrnes_intro_2025,
title = {Intro to {Brain}-{Like}-{AGI} {Safety}},
url = {https://osf.io/fe36n_v1},
doi = {10.31219/osf.io/fe36n_v1},
abstract = {Suppose we someday build an Artificial General Intelligence (AGI) algorithm using similar principles of learning and cognition as the human brain. How would we use such an algorithm safely? I argue that this is an open technical problem, and my goal is to bring readers with no prior knowledge all the way up to the front-line of unsolved problems. Chapter 1 has background and motivation; Chapters 2-7 are on neuroscience, arguing for a picture of the brain that combines large-scale learning algorithms (e.g. in the cortex) and specific evolved reflexes (e.g. in the hypothalamus and brainstem); and Chapters 8-15 apply those neuroscience ideas to AGI safety. A major theme is the idea that the brain has something like a reinforcement learning reward function, which says that pain is bad, eating-when-hungry is good, etc. I argue that this reward function is centered around the hypothalamus and brainstem, and that all human desires—even "higher" desires for things like compassion and justice—come directly or indirectly from that innate reward function. If future programmers build brain-like AGI, they will likewise have a reward function slot in their source code, in which they can put whatever they want. If they put the wrong thing, the resulting AGI will wind up callously indifferent to human welfare. How might they avoid that? That's an open technical problem, but I will review some ideas and research directions.},
language = {en},
urldate = {2025-12-31},
publisher = {Open Science Framework},
author = {Byrnes, Steven J.},
month = mar,
year = {2025},
file = {PDF:/home/velocitatem/Zotero/storage/ZLJQ4DQ9/Byrnes - 2025 - Intro to Brain-Like-AGI Safety.pdf:application/pdf},
}
@article{Iliou2021,
author = {Christos Iliou and Theodoros Kostoulas and Theodora Tsikrika and Vasilis Katos and Stefanos Vrochidis and Ioannis Kompatsiaris},
doi = {10.1145/3447815},
issue = {3},
journal = {Digital Threats: Research and Practice},
pages = {1-26},
title = {Detection of Advanced Web Bots by Combining Web Logs with Mouse Behavioural Biometrics},
volume = {2},
url = {https://dl.acm.org/doi/10.1145/3447815},
year = {2021}
@article{shannon_mathematical_nodate,
title = {A {Mathematical} {Theory} of {Communication}},
language = {en},
author = {Shannon, C E},
file = {PDF:/home/velocitatem/Zotero/storage/FJRFRWK2/Shannon - A Mathematical Theory of Communication.pdf:application/pdf},
}
@article{ArnoudVdenBoer2015,
author = {Arnoud V. den Boer},
doi = {10.1016/j.sorms.2015.03.001},
issue = {1},
journal = {Surveys in Operations Research and Management Science},
month = {6},
pages = {1-18},
title = {Dynamic pricing and learning: Historical origins, current research, and new directions},
volume = {20},
url = {https://www.sciencedirect.com/science/article/pii/S1876735415000021},
year = {2015}
@misc{noauthor_order_stats_nodate,
title = {order\_stats},
file = {PDF:/home/velocitatem/Zotero/storage/D3QRGY9Z/order_stats.pdf:application/pdf},
}
@article{Calvano2018,
author = {Emilio Calvano and Giacomo Calzolari and Vincenzo Denicolo and Sergio Pastorello},
doi = {10.2139/ssrn.3304991},
journal = {SSRN Electronic Journal},
title = {Artificial Intelligence, Algorithmic Pricing and Collusion},
url = {https://papers.ssrn.com/sol3/papers.cfm?abstract_id=3304991},
year = {2018}
@article{devine_nonlinear_nodate,
title = {Nonlinear {Pricing} with {Costly} {Information} {Acquisition}},
abstract = {This paper examines a nonlinear pricing model where the firm can choose to acquire costly information prior to offering contract menus to consumers; such as paying a consultant or investing in machine learning technologies. Information provides the firm with a signal about consumers types, whose accuracy increases as the firm acquires larger amounts of information. We show that the firm chooses to acquire information, only if it can purchase a sufficient amount that could alter its initial prior beliefs. Relative to standard settings where firms cannot acquire information, we identify how information acquisition changes optimal contract offers, equilibrium profits, information rents, and welfare. A better-informed firm increases its expected profits, but it can also increase expected utility when the cost of information is intermediate. Our results recommend balanced online privacy laws.},
language = {en},
author = {Devine, Brett R and Munoz-Garcia, Felix},
file = {PDF:/home/velocitatem/Zotero/storage/GQ28KVBF/Devine and Munoz-Garcia - Nonlinear Pricing with Costly Information Acquisition.pdf:application/pdf},
}
@misc{wang_learning_2025,
title = {Learning {Optimal} {Distributionally} {Robust} {Stochastic} {Control} in {Continuous} {State} {Spaces}},
url = {http://arxiv.org/abs/2406.11281},
doi = {10.48550/arXiv.2406.11281},
abstract = {We study data-driven learning of robust stochastic control for infinite-horizon systems with potentially continuous state and action spaces. In many managerial settingssupply chains, finance, manufacturing, services, and dynamic gamesthe state-transition mechanism is determined by system design, while available data capture the distributional properties of the stochastic inputs from the environment. For modeling and computational tractability, a decision maker often adopts a Markov control model with i.i.d. environment inputs, which can render learned policies fragile to internal dependence or external perturbations. We introduce a distributionally robust stochastic control paradigm that promotes policy reliability by introducing adaptive adversarial perturbations to the environment input, while preserving the modeling, statistical, and computational tractability of the Markovian formulation. From a modeling perspective, we examine two adversary modelscurrent-action-aware and current-action-unawareleading to distinct dynamic behaviors and robust optimal policies. From a statistical learning perspective, we characterize optimal finite-sample minimax rates for uniform learning of the robust value function across a continuum of states under ambiguity sets defined by the fk-divergence and Wasserstein distance. To efficiently compute the optimal robust policies, we further propose algorithms inspired by deep reinforcement learning methodologies. Finally, we demonstrate the applicability of the framework to real managerial problems.},
language = {en},
urldate = {2025-12-29},
publisher = {arXiv},
author = {Wang, Shengbo and Meng, Jason and Si, Nian and Blanchet, Jose and Zhou, Zhengyuan},
month = nov,
year = {2025},
note = {arXiv:2406.11281 [stat]},
keywords = {Computer Science - Machine Learning, Statistics - Machine Learning},
file = {PDF:/home/velocitatem/Zotero/storage/RQ8XDSSG/Wang et al. - 2025 - Learning Optimal Distributionally Robust Stochastic Control in Continuous State Spaces.pdf:application/pdf},
}
@misc{ie_recsim_2019,
title = {{RecSim}: {A} {Configurable} {Simulation} {Platform} for {Recommender} {Systems}},
shorttitle = {{RecSim}},
url = {http://arxiv.org/abs/1909.04847},
doi = {10.48550/arXiv.1909.04847},
abstract = {We propose RecSim, a configurable platform for authoring simulation environments for recommender systems (RSs) that naturally supports sequential interaction with users. RecSim allows the creation of new environments that reflect particular aspects of user behavior and item structure at a level of abstraction well-suited to pushing the limits of current reinforcement learning (RL) and RS techniques in sequential interactive recommendation problems. Environments can be easily configured that vary assumptions about: user preferences and item familiarity; user latent state and its dynamics; and choice models and other user response behavior. We outline how RecSim offers value to RL and RS researchers and practitioners, and how it can serve as a vehicle for academic-industrial collaboration.},
urldate = {2025-12-29},
publisher = {arXiv},
author = {Ie, Eugene and Hsu, Chih-wei and Mladenov, Martin and Jain, Vihan and Narvekar, Sanmit and Wang, Jing and Wu, Rui and Boutilier, Craig},
month = sep,
year = {2019},
note = {arXiv:1909.04847 [cs]},
keywords = {Computer Science - Machine Learning, Statistics - Machine Learning, Computer Science - Human-Computer Interaction, Computer Science - Information Retrieval},
file = {Preprint PDF:/home/velocitatem/Zotero/storage/CJJI2VQF/Ie et al. - 2019 - RecSim A Configurable Simulation Platform for Recommender Systems.pdf:application/pdf;Snapshot:/home/velocitatem/Zotero/storage/8XJKJTHE/1909.html:text/html},
}
@misc{kuhn_wasserstein_2024,
title = {Wasserstein {Distributionally} {Robust} {Optimization}: {Theory} and {Applications} in {Machine} {Learning}},
shorttitle = {Wasserstein {Distributionally} {Robust} {Optimization}},
url = {http://arxiv.org/abs/1908.08729},
doi = {10.48550/arXiv.1908.08729},
abstract = {Many decision problems in science, engineering and economics are affected by uncertain parameters whose distribution is only indirectly observable through samples. The goal of data-driven decision-making is to learn a decision from finitely many training samples that will perform well on unseen test samples. This learning task is difficult even if all training and test samples are drawn from the same distribution—especially if the dimension of the uncertainty is large relative to the training sample size. Wasserstein distributionally robust optimization seeks data-driven decisions that perform well under the most adverse distribution within a certain Wasserstein distance from a nominal distribution constructed from the training samples. In this tutorial we will argue that this approach has many conceptual and computational benefits. Most prominently, the optimal decisions can often be computed by solving tractable convex optimization problems, and they enjoy rigorous out-of-sample and asymptotic consistency guarantees. We will also show that Wasserstein distributionally robust optimization has interesting ramifications for statistical learning and motivates new approaches for fundamental learning tasks such as classification, regression, maximum likelihood estimation or minimum mean square error estimation, among others.},
language = {en},
urldate = {2025-12-27},
publisher = {arXiv},
author = {Kuhn, Daniel and Esfahani, Peyman Mohajerin and Nguyen, Viet Anh and Shafieezadeh-Abadeh, Soroosh},
month = nov,
year = {2024},
note = {arXiv:1908.08729 [stat]},
keywords = {Computer Science - Machine Learning, Statistics - Machine Learning, Mathematics - Optimization and Control},
file = {PDF:/home/velocitatem/Zotero/storage/FAWJEK6J/Kuhn et al. - 2024 - Wasserstein Distributionally Robust Optimization Theory and Applications in Machine Learning.pdf:application/pdf},
}
@misc{arunachaleswaran_learning_2025,
title = {Learning to {Play} {Against} {Unknown} {Opponents}},
url = {http://arxiv.org/abs/2412.18297},
doi = {10.48550/arXiv.2412.18297},
abstract = {We consider the problem of a learning agent who has to repeatedly play a general sum game against a strategic opponent who acts to maximize their own payoff by optimally responding against the learners algorithm. The learning agent knows their own payoff function, but is uncertain about the payoff of their opponent (knowing only that it is drawn from some distribution D). What learning algorithm should the agent run in order to maximize their own total utility, either in expectation or in the worst-case over D? When the learning algorithm is constrained to be a no-regret algorithm, we demonstrate how to efficiently construct an optimal learning algorithm (asymptotically achieving the optimal utility) in polynomial time for both the in-expectation and worst-case problems, independent of any other assumptions. When the learning algorithm is not constrained to no-regret, we show how to construct an ε-optimal learning algorithm (obtaining average utility within ε of the optimal utility) for both the in-expectation and worst-case problems in time polynomial in the size of the input and 1/ε, when either the size of the game or the support of D is constant. Finally, for the special case of the maximin objective, where the learner wishes to maximize their minimum payoff over all possible optimizer types, we construct a learner algorithm that runs in polynomial time in each step and guarantees convergence to the optimal learner payoff. All of these results make use of recently developed machinery that converts the analysis of learning algorithms to the study of the class of corresponding geometric objects known as menus.},
language = {en},
urldate = {2025-12-27},
publisher = {arXiv},
author = {Arunachaleswaran, Eshwar Ram and Collina, Natalie and Schneider, Jon},
month = feb,
year = {2025},
note = {arXiv:2412.18297 [cs]},
keywords = {Computer Science - Machine Learning, Computer Science - Computer Science and Game Theory},
file = {PDF:/home/velocitatem/Zotero/storage/M6V9LLCS/Arunachaleswaran et al. - 2025 - Learning to Play Against Unknown Opponents.pdf:application/pdf},
}
@misc{li_distributionally_2025,
title = {Distributionally {Robust} {Optimization} with {Adversarial} {Data} {Contamination}},
url = {http://arxiv.org/abs/2507.10718},
doi = {10.48550/arXiv.2507.10718},
abstract = {Distributionally Robust Optimization (DRO) provides a framework for decision-making under distributional uncertainty, yet its effectiveness can be compromised by outliers in the training data. This paper introduces a principled approach to simultaneously address both challenges. We focus on optimizing Wasserstein-1 DRO objectives for generalized linear models with convex Lipschitz loss functions, where an \$ε\$-fraction of the training data is adversarially corrupted. Our primary contribution lies in a novel modeling framework that integrates robustness against training data contamination with robustness against distributional shifts, alongside an efficient algorithm inspired by robust statistics to solve the resulting optimization problem. We prove that our method achieves an estimation error of \$O({\textbackslash}sqrtε)\$ for the true DRO objective value using only the contaminated data under the bounded covariance assumption. This work establishes the first rigorous guarantees, supported by efficient computation, for learning under the dual challenges of data contamination and distributional shifts.},
language = {en},
urldate = {2025-12-27},
publisher = {arXiv},
author = {Li, Shuyao and Diakonikolas, Ilias and Diakonikolas, Jelena},
month = nov,
year = {2025},
note = {arXiv:2507.10718 [cs]},
keywords = {Computer Science - Machine Learning, Mathematics - Optimization and Control, Computer Science - Data Structures and Algorithms},
file = {PDF:/home/velocitatem/Zotero/storage/H6AXDTLX/Li et al. - 2025 - Distributionally Robust Optimization with Adversarial Data Contamination.pdf:application/pdf},
}
@misc{karten_llm_2025,
title = {{LLM} {Economist}: {Large} {Population} {Models} and {Mechanism} {Design} in {Multi}-{Agent} {Generative} {Simulacra}},
shorttitle = {{LLM} {Economist}},
url = {http://arxiv.org/abs/2507.15815},
doi = {10.48550/arXiv.2507.15815},
abstract = {We present the LLM Economist, a novel framework that uses agent-based modeling to design and assess economic policies in strategic environments with hierarchical decision-making. At the lower level, bounded rational worker agents—instantiated as persona-conditioned prompts sampled from U.S. Census-calibrated income and demographic statistics—choose labor supply to maximize text-based utility functions learned in-context. At the upper level, a planner agent employs in-context reinforcement learning to propose piecewise-linear marginal tax schedules anchored to the current U.S. federal brackets. This construction endows economic simulacra with three capabilities requisite for credible fiscal experimentation: (i) optimization of heterogeneous utilities, (ii) principled generation of large, demographically realistic agent populations, and (iii) mechanism design—the ultimate nudging problem—expressed entirely in natural language. Experiments with populations of up to one hundred interacting agents show that the planner converges near Stackelberg equilibria that improve aggregate social welfare relative to Saez solutions, while a periodic, persona-level voting procedure furthers these gains under decentralized governance. These results demonstrate that large language model-based agents can jointly model, simulate, and govern complex economic systems, providing a tractable test bed for policy evaluation at the societal scale to help build better civilizations.},
language = {en},
urldate = {2025-12-27},
publisher = {arXiv},
author = {Karten, Seth and Li, Wenzhe and Ding, Zihan and Kleiner, Samuel and Bai, Yu and Jin, Chi},
month = jul,
year = {2025},
note = {arXiv:2507.15815 [cs]},
keywords = {Computer Science - Machine Learning, Computer Science - Multiagent Systems},
file = {PDF:/home/velocitatem/Zotero/storage/U7A5Q78V/Karten et al. - 2025 - LLM Economist Large Population Models and Mechanism Design in Multi-Agent Generative Simulacra.pdf:application/pdf},
}
@techreport{mullapudi_reinforcement_nodate,
title = {A {Reinforcement} {Learning} {Approach} to {Dynamic} {Pricing}},
abstract = {Dynamic pricing represents a critical strategic challenge in modern e-commerce, where firms must navigate fluctuating demand, inventory constraints, and aggressive competitor actions. Traditional static and heuristic-based pricing models often fail to capture the complex, non-linear dynamics of competitive digital markets, leading to suboptimal profitability. This paper proposes a model-free reinforcement learning (RL) framework to address this challenge. Specifically, we design, implement, and evaluate a Q-learning agent capable of learning an optimal, state-dependent pricing policy. The agent is trained and evaluated within a simulated market environment constructed from the publicly available "Retail Price Optimization" dataset from Kaggle, which provides a rich feature set including historical sales, product characteristics, seasonality, and, crucially, competitor pricing data. The problem is formulated as a Markov Decision Process (MDP), where the agent's state incorporates its price position relative to competitors, competitor price trends, and seasonal factors. The agent's performance is benchmarked against three baseline strategies: static pricing, a reactive "follow-the-leader" heuristic, and random pricing. The results demonstrate that the Q-learning agent achieves a substantial increase in total cumulative profit over the evaluation period, outperforming all baselines by learning a nuanced policy that strategically balances price adjustments in response to market conditions. This work provides a practical and reproducible blueprint for applying reinforcement learning to optimize pricing decisions in a simulated yet realistic competitive retail environment, highlighting the potential of RL to automate complex strategic decision-making.},
author = {Mullapudi, Pavan},
note = {Publication Title: International Journal on Science and Technology (IJSAT) IJSAT25049558
Volume: 16
Issue: 4},
keywords = {Index Terms: Dynamic Pricing, Markov Decision Process, Price Optimization, Q-Learning, Reinforcement Learning, Retail Analytics},
file = {PDF:/home/velocitatem/Zotero/storage/G95TBLF7/9558.pdf:application/pdf},
}
@techreport{roughgarden_cs364a_2013,
title = {{CS364A}: {Algorithmic} {Game} {Theory} {Lecture} \#5: {Revenue}-{Maximizing} {Auctions} *},
author = {Roughgarden, Tim},
year = {2013},
file = {PDF:/home/velocitatem/Zotero/storage/C39VM7N9/l5.pdf:application/pdf},
}
@techreport{kuhn_distributionally_2025,
title = {Distributionally {Robust} {Optimization}},
abstract = {Distributionally robust optimization (DRO) studies decision problems under uncertainty where the probability distribution governing the uncertain problem parameters is itself uncertain. A key component of any DRO model is its ambiguity set, that is, a family of probability distributions consistent with any available structural or statistical information. DRO seeks decisions that perform best under the worst distribution in the ambiguity set. This worst case criterion is supported by findings in psychology and neuroscience, which indicate that many decision-makers have a low tolerance for distributional ambiguity. DRO is rooted in statistics, operations research and control theory, and recent research has uncovered its deep connections to regularization techniques and adversarial training in machine learning. This survey presents the key findings of the field in a unified and self-contained manner.},
author = {Kuhn, Daniel and Shafiee, Soroosh and Wiesemann, Wolfram},
year = {2025},
note = {arXiv: 2411.02549v3},
file = {PDF:/home/velocitatem/Zotero/storage/IXTTMD7G/full-text.pdf:application/pdf},
}
@article{parkes_economic_2015,
title = {Economic reasoning and artificial intelligence},
volume = {349},
issn = {10959203},
doi = {10.1126/science.aaa8403},
abstract = {The field of artificial intelligence (AI) strives to build rational agents capable of perceiving the world around them and taking actions to advance specified goals. Put another way, AI researchers aim to construct a synthetic homo economicus, the mythical perfectly rational agent of neoclassical economics.We review progress toward creating this new species of machine, machina economicus, and discuss some challenges in designing AIs that can reason effectively in economic contexts. Supposing that AI succeeds in this quest, or at least comes close enough that it is useful to think about AIs in rationalistic terms, we ask how to design the rules of interaction in multi-agent systems that come to represent an economy of AIs.Theories of normative design from economics may prove more relevant for artificial agents than human agents, with AIs that better respect idealized assumptions of rationality than people, interacting through novel rules and incentive systems quite distinct from those tailored for people.},
number = {6245},
journal = {Science},
author = {Parkes, David C. and Wellman, Michael P.},
month = jul,
year = {2015},
pmid = {26185245},
note = {Publisher: American Association for the Advancement of Science},
pages = {267--272},
file = {PDF:/home/velocitatem/Zotero/storage/27KLNFRU/_aiEcon.pdf:application/pdf},
}
@article{yokoo_effect_2004,
title = {The effect of false-name bids in combinatorial auctions: {New} fraud in internet auctions},
volume = {46},
issn = {08998256},
doi = {10.1016/S0899-8256(03)00045-9},
abstract = {We examine the effect of false-name bids on combinatorial auction protocols. False-name bids are bids submitted by a single bidder using multiple identifiers such as multiple e-mail addresses. The obtained results are summarized as follows: (1) the Vickrey-Clarke-Groves (VCG) mechanism, which is strategy-proof and Pareto efficient when there exists no false-name bid, is not false-name-proof; (2) there exists no false-name-proof combinatorial auction protocol that satisfies Pareto efficiency; (3) one sufficient condition where the VCG mechanism is false-name-proof is identified, i.e., the concavity of a surplus function over bidders. © 2003 Elsevier Inc. All rights reserved.},
number = {1},
journal = {Games and Economic Behavior},
author = {Yokoo, Makoto and Sakurai, Yuko and Matsubara, Shigeo},
year = {2004},
note = {Publisher: Academic Press Inc.},
keywords = {Auction, Mechanism design, Strategy-proof},
pages = {174--188},
file = {PDF:/home/velocitatem/Zotero/storage/LUVQV6WT/Yokoo04.pdf:application/pdf},
}
@inproceedings{feldman_free-riding_2004,
title = {Free-riding and whitewashing in peer-to-peer systems},
isbn = {1-58113-942-X},
doi = {10.1145/1016527.1016539},
abstract = {We develop a model to study the phenomenon of free-riding in peer-to-peer (P2P) systems. At the heart of our model is a user of a certain type, an intrinsic and private parameter that reflects the user's willingness to contribute resources to the system. A user decides whether to contribute or free-ride based on how the current contribution cost in the system compares to her type. When the societal generosity (i.e., the average type) is low, intervention is required in order to sustain the system. We present the effect of mechanisms that exclude low type users or, more realistic, penalize free-riders with degraded service. We also consider dynamic scenarios with arrivals and departures of users, and with whitewashers: users who leave the system and rejoin with new identities to avoid reputational penalties. We find that when penalty is imposed on all newcomers in order to avoid whitewashing, system performance degrades significantly only when the turnover rate among users is high.},
booktitle = {Proceedings of the {ACM} {SIGCOMM} 2004 {Workshops}},
publisher = {Association for Computing Machinery},
author = {Feldman, Michal and Papadimitriou, Christos and Chuang, John and Stoica, Ion},
year = {2004},
keywords = {Cheap pseudonyms, Cooperation, Equilibrium, Exclusion, Free-riding, Identity cost, Incentives, Peer-to-peer, Whitewashing},
pages = {228--235},
file = {PDF:/home/velocitatem/Zotero/storage/K32WH6SB/1016527.1016539.pdf:application/pdf},
}
@article{calvano_artificial_2018,
title = {Artificial {Intelligence}, {Algorithmic} {Pricing} and {Collusion}},
url = {https://papers.ssrn.com/sol3/papers.cfm?abstract_id=3304991},
doi = {10.2139/ssrn.3304991},
journal = {SSRN Electronic Journal},
author = {Calvano, Emilio and Calzolari, Giacomo and Denicolo, Vincenzo and Pastorello, Sergio},
year = {2018},
file = {PDF:/home/velocitatem/Zotero/storage/WYTSSZBR/ssrn-3304991.pdf:application/pdf},
}
@techreport{varian_economic_1995,
title = {Economic {Mechanism} {Design} for {Computerized} {Agents}},
abstract = {The eeld of economic mechanism design has been an active area of research in economics for at least 20 years. This eld uses the tools of economics and game theory to design {\textbackslash}rules of interaction" for economic transactions that will, in principle , yield some desired outcome. In this paper I provide an overview of this subject for an audience interested in applications to electronic commerce and discuss some special problems that arise in this context.},
author = {Varian, Hal R},
year = {1995},
file = {PDF:/home/velocitatem/Zotero/storage/S8635QX6/varian95a.pdf:application/pdf},
}
@book{russell_artificial_nodate,
title = {Artificial {Intelligence} {A} {Modern} {Approach} {Fourth} {Edition} {Global} {Edition}},
isbn = {978-1-292-40117-1},
author = {Russell, Stuart and Norvig, Peter},
file = {PDF:/home/velocitatem/Zotero/storage/6B8W8S27/efdd4d1d4c2087fe1cbe03d9ced67f34.pdf:application/pdf},
}
@techreport{wellman_price_2004,
title = {Price {Prediction} in a {Trading} {Agent} {Competition} {Yevgeniy} {Vorobeychik}},
abstract = {The 2002 Trading Agent Competition (TAC) presented a challenging market game in the domain of travel shopping. One of the pivotal issues in this domain is uncertainty about hotel prices, which have a significant influence on the relative cost of alternative trip schedules. Thus, virtually all participants employ some method for predicting hotel prices. We survey approaches employed in the tournament, finding that agents apply an interesting diversity of techniques, taking into account differing sources of evidence bearing on prices. Based on data provided by entrants on their agents' actual predictions in the TAC-02 finals and semifinals, we analyze the relative efficacy of these approaches. The results show that taking into account game-specific information about flight prices is a major distinguishing factor. Machine learning methods effectively induce the relationship between flight and hotel prices from game data, and a purely analytical approach based on competitive equilibrium analysis achieves equal accuracy with no historical data. Employing a new measure of prediction quality, we relate absolute accuracy to bottom-line performance in the game.},
author = {Wellman, Michael P and Reeves, Daniel M and Lochner, Kevin M and Edu, Yvorobey@umich},
year = {2004},
note = {Publication Title: Journal of Artificial Intelligence Research
Volume: 21},
pages = {19--36},
file = {PDF:/home/velocitatem/Zotero/storage/N9JNXFJW/live-1333-2265-jair.pdf:application/pdf},
}
@techreport{shoham_multiagent_nodate,
title = {Multiagent {Systems}: {Algorithmic}, {Game}-{Theoretic}, and {Logical} {Foundations}},
url = {http://www.masfoundations.org.},
author = {Shoham, Yoav and Leyton-Brown, Kevin},
keywords = {algorithms, auctions, communication, competition, cooperation, distributed problem solving, game theory, learning, logic, mechanism design, social choice},
file = {PDF:/home/velocitatem/Zotero/storage/QZVYS7V9/shoham09a.pdf:application/pdf},
}
@article{xia_evaluation-driven_2025,
title = {Evaluation-{Driven} {Development} and {Operations} of {LLM} {Agents}: {A} {Process} {Model} and {Reference} {Architecture}},
url = {http://arxiv.org/abs/2411.13768},
abstract = {Large Language Models (LLMs) have enabled the emergence of LLM agents, systems capable of pursuing under-specified goals and adapting after deployment. Evaluating such agents is challenging because their behavior is open ended, probabilistic, and shaped by system-level interactions over time. Traditional evaluation methods, built around fixed benchmarks and static test suites, fail to capture emergent behaviors or support continuous adaptation across the lifecycle. To ground a more systematic approach, we conduct a multivocal literature review (MLR) synthesizing academic and industrial evaluation practices. The findings directly inform two empirically derived artifacts: a process model and a reference architecture that embed evaluation as a continuous, governing function rather than a terminal checkpoint. Together they constitute the evaluation-driven development and operations (EDDOps) approach, which unifies offline (development-time) and online (runtime) evaluation within a closed feedback loop. By making evaluation evidence drive both runtime adaptation and governed redevelopment, EDDOps supports safer, more traceable evolution of LLM agents aligned with changing objectives, user needs, and governance constraints.},
author = {Xia, Boming and Lu, Qinghua and Zhu, Liming and Xing, Zhenchang and Zhao, Dehai and Zhang, Hao},
month = nov,
year = {2025},
note = {arXiv: 2411.13768},
file = {PDF:/home/velocitatem/Zotero/storage/H8IS64AW/2411.13768v2.pdf:application/pdf},
}
@techreport{xie_osworld_nodate,
title = {{OSWORLD}: {Benchmarking} {Multimodal} {Agents} for {Open}-{Ended} {Tasks} in {Real} {Computer} {Environments}},
url = {https://os-world.github.io},
abstract = {Autonomous agents that accomplish complex computer tasks with minimal human interventions have the potential to transform human-computer interaction, significantly enhancing accessibility and productivity. However, existing benchmarks either lack an interactive environment or are limited to environments specific to certain applications or domains, failing to reflect the diverse and complex nature of real-world computer use, thereby limiting the scope of tasks and agent scalability. To address this issue, we introduce OSWORLD, the first-of-its-kind scalable, real computer environment for multimodal agents, supporting task setup, execution-based evaluation, and interactive learning across various operating systems such as Ubuntu, Windows, and macOS. OSWORLD can serve as a unified, integrated computer environment for assessing open-ended computer tasks that involve arbitrary applications. Building upon OSWORLD, we create a benchmark of 369 computer tasks involving real web and desktop apps in open domains, OS file I/O, and workflows spanning multiple applications. Each task example is derived from real-world computer use cases and includes a detailed initial state setup configuration and a custom execution-based evaluation script for reliable, reproducible evaluation. Extensive evaluation of state-of-the-art LLM/VLM-based agents on OSWORLD reveals significant deficiencies in their ability to serve as computer assistants. While humans can accomplish over 72.36\% of the tasks, the best model achieves only 12.24\% success, primarily struggling with GUI grounding and operational knowledge. Comprehensive analysis using OSWORLD provides valuable insights for developing multimodal generalist agents that were not possible with previous benchmarks. Our code, environment, baseline models, and data are publicly available at https://os-world.github.io.},
author = {Xie, Tianbao and Zhang, Danyang and Chen, Jixuan and Li, Xiaochuan and Zhao, Siheng and Cao, Ruisheng and Jing Hua, Toh and Cheng, Zhoujun and Shin, Dongchan and Lei, Fangyu and Liu, Yitao and Xu, Yiheng and Zhou, Shuyan and Savarese, Silvio and Xiong, Caiming and Zhong, Victor and Yu, Tao},
note = {arXiv: 2404.07972v2},
file = {PDF:/home/velocitatem/Zotero/storage/LLRKXIC7/full-text.pdf:application/pdf},
}
@techreport{imperva_rapid_2025,
title = {The {Rapid} {Rise} of {Bots} and the {Unseen} {Risk} for {Business} \#{2025BADBOTREPORT}},
author = {{Imperva}},
year = {2025},
file = {PDF:/home/velocitatem/Zotero/storage/AWR9IQRD/2025-Bad-Bot-Report.pdf:application/pdf},
}
@article{perez-ricardo_exploring_2025,
title = {Exploring booking intentions through price elasticity of demand in tourism accommodations using large-scale data analytics},
volume = {31},
issn = {24448834},
doi = {10.1016/j.iedeen.2025.100271},
abstract = {The study aims to explore tourists' booking intentions by analyzing the price elasticity of demand in tourist accommodations. This analysis should reveal how changes in price affect booking behavior across different customer segments, using online booking records. A dataset was compiled from 106 hotels in Malaga, Spain, comprising 27,910 online bookings sourced exclusively from hotel websites. To understand the price elasticity of demand, a simple log-log regression was applied, segmenting the data based on key revenue-related variables. Subsequently, a cluster segmentation was performed using the Elbow method and K-means algorithm to identify distinct market segments. The findings highlighted that Family Travelers and Short Stay Travelers segments exhibited elastic demand, indicating higher sensitivity to price fluctuations. In contrast, Early Bookers and Mid-Season Long Stayers demonstrated inelastic demand, with lower responsiveness to changes in tourist accommodation prices. The number of variables analyzed in this study, along with the cluster analysis, represent a novelty and contribute to the existing literature on market segmentation and price elasticity of demand. This integration enriches both fields of research, offering mutual benefits and deeper insights that enhance the understanding of booking intention and pricing strategies.},
number = {1},
urldate = {2025-11-28},
journal = {European Research on Management and Business Economics},
author = {Pérez-Ricardo, Elizabeth del Carmen and García-Mestanza, Josefa},
month = jan,
year = {2025},
note = {Publisher: European Academy of Management and Business Economics},
keywords = {Booking intention, Price elasticity, Tourist segmentation},
file = {PDF:/home/velocitatem/Zotero/storage/QNXZJLRM/S2444883425000038.pdf:application/pdf},
}
@misc{ghaffary_amazon_nodate,
title = {Amazon {Sues} to {Stop} {Perplexity} {From} {Using} {AI} {Tool} to {Buy} {Stuff}},
url = {https://www.bloomberg.com/news/articles/2025-11-04/amazon-demands-perplexity-stop-ai-agent-from-making-purchases},
author = {Ghaffary, Shirin and Day, Matt},
file = {PDF:/home/velocitatem/Zotero/storage/IQL6FPWE/Amazon Sues to Stop Perplexity From Using AI Tool to Buy Stuff - Bloomberg.pdf:application/pdf},
}
@techreport{besbes_dynamic_nodate,
title = {Dynamic {Pricing} {Without} {Knowing} the {Demand} {Function}: {Risk} {Bounds} and {Near}-{Optimal} {Algorithms} *},
abstract = {We consider a single product revenue management problem where, given an initial inventory, the objective is to dynamically adjust prices over a finite sales horizon to maximize expected revenues. Realized demand is observed over time, but the underlying functional relationship between price and mean demand rate that governs these observations (otherwise known as the demand function or demand curve), is not known. We consider two instances of this problem: i.) a setting where the demand function is assumed to belong to a known parametric family with unknown parameter values; and ii.) a setting where the demand function is assumed to belong to a broad class of functions that need not admit any parametric representation. In each case we develop policies that learn the demand function "on the fly," and optimize prices based on that. The performance of these algorithms is measured in terms of the regret: the revenue loss relative to the maximal revenues that can be extracted when the demand function is known prior to the start of the selling season. We derive lower bounds on the regret that hold for any admissible pricing policy, and then show that our proposed algorithms achieve a regret that is "close" to this lower bound. The magnitude of the regret can be interpreted as the economic value of prior knowledge on the demand function; manifested as the revenue loss due to model uncertainty.},
author = {Besbes, Omar and Zeevi, Assaf},
note = {Publication Title: Operations Research},
keywords = {learning, asymptotic analysis, estimation, exploration-exploitation, pricing, Revenue management, value of information},
file = {PDF:/home/velocitatem/Zotero/storage/SBAIB4V2/Dp_wo_demand_risk_ob_az_posted.pdf:application/pdf},
}
@techreport{markntel_advisors_global_2025,
address = {Noida, Uttar Pradesh, India},
title = {Global {AI} {Agent} {Market} {Research} {Report}: {Forecast} (20262032)},
url = {https://www.marknteladvisors.com/research-library/ai-agent-market.html},
urldate = {2025-12-12},
institution = {MarkNtel Advisors},
author = {{MarkNtel Advisors}},
year = {2025},
}
@article{amjad_censored_2017,
title = {Censored {Demand} {Estimation} in {Retail}},
volume = {1},
url = {https://par.nsf.gov/servlets/purl/10066022},
doi = {10.1145/3154489},
abstract = {In this paper, the question of interest is estimating true demand of a product at a given store location and time period in the retail environment based on a single noisy and potentially censored observation. To address this question, we introduce a \%non-parametric framework to make inference from multiple time series. Somewhat surprisingly, we establish that the algorithm introduced for the purpose of "matrix completion" can be used to solve the relevant inference problem. Specifically, using the Universal Singular Value Thresholding (USVT) algorithm [7], we show that our estimator is consistent: the average mean squared error of the estimated average demand with respect to the true average demand goes to 0 as the number of store locations and time intervals increase to \${\textbackslash}infty\$. We establish naturally appealing properties of the resulting estimator both analytically as well as through a sequence of instructive simulations. Using a real dataset in retail (Walmart), we argue for the practical relevance of our approach.},
number = {2},
urldate = {2025-11-12},
journal = {Proceedings of the ACM on Measurement and Analysis of Computing Systems},
author = {Amjad, Muhammad J. and Shah, Devavrat},
month = dec,
year = {2017},
note = {Publisher: Association for Computing Machinery (ACM)},
pages = {1--28},
file = {PDF:/home/velocitatem/Zotero/storage/5ZYADDT4/10066022.pdf:application/pdf},
}
@misc{ganie_uncertainty_2025,
title = {Uncertainty in {Authorship}: {Why} {Perfect} {AI} {Detection} {Is} {Mathematically} {Impossible}},
shorttitle = {Uncertainty in {Authorship}},
url = {http://arxiv.org/abs/2509.11915},
doi = {10.48550/arXiv.2509.11915},
abstract = {As large language models (LLMs) become more advanced, it is increasingly difficult to distinguish between human-written and AI-generated text. This paper draws a conceptual parallel between quantum uncertainty and the limits of authorship detection in natural language. We argue that there is a fundamental trade-off: the more confidently one tries to identify whether a text was written by a human or an AI, the more one risks disrupting the text's natural flow and authenticity. This mirrors the tension between precision and disturbance found in quantum systems. We explore how current detection methods--such as stylometry, watermarking, and neural classifiers--face inherent limitations. Enhancing detection accuracy often leads to changes in the AI's output, making other features less reliable. In effect, the very act of trying to detect AI authorship introduces uncertainty elsewhere in the text. Our analysis shows that when AI-generated text closely mimics human writing, perfect detection becomes not just technologically difficult but theoretically impossible. We address counterarguments and discuss the broader implications for authorship, ethics, and policy. Ultimately, we suggest that the challenge of AI-text detection is not just a matter of better tools--it reflects a deeper, unavoidable tension in the nature of language itself.},
language = {en},
urldate = {2026-01-05},
publisher = {arXiv},
author = {Ganie, Aadil Gani},
month = sep,
year = {2025},
note = {arXiv:2509.11915 [cs]},
keywords = {Computer Science - Computation and Language},
file = {PDF:/home/velocitatem/Zotero/storage/3Z2XK4QC/Ganie - 2025 - Uncertainty in Authorship Why Perfect AI Detection Is Mathematically Impossible.pdf:application/pdf},
}

View File

@@ -8,9 +8,50 @@
\section{Introduction}
Research Objectives and Contribution: What are we making, why and who should care?
In this paper we present an exploration and defense against the presence of new commercial entities in digitally powered platforms, preserving market equilibrium in the age of AI. This research establishes the following contributions: definition and formalization of non-human transactors in e-commerce platforms, development of a testing-ground for capturing the behavioral essence of these transactors across a large variety of digital systems, construction of a discriminative model (to prove separability) as a strong learner for downstream mitigation of contamination by non-human entities, translation of such learned separability into existing dynamic pricing machine learning loops, and finally establishment of a high-level KPI-affecting causal effect and cost-saving framework for the future of internet commerce in the presence of such non-human learners.
This research effort touches a large variety of domains, spanning behavioral economics for understanding the rationality of behavior as theorized by the concept of homo economicus, agent-based modeling to translate our learned separability into disjoint dynamic pricing systems, reinforcement learning which serves as the SOTA for price-learners, and dynamic pricing and market equilibrium theory to understand the risks of possible supra-competitive pricing phenomena in cases of adversarial pricing systems driving the market out of equilibrium.
\subsection{Motivation and Market Context}
Current market dynamics and trends of dynamic pricing and AI agents. Future projections of AI agents. Key stakeholders that are discussing this and reporting on it (Thales). Who is most affected
The current innovation boom in generative artificial intelligence and its applications to knowledge-based work tasks has brought many competing technologies for browser-use automation, with benchmarks and evaluations \cite{xia_evaluation-driven_2025} motivating the development of capabilities focused on commercial research, understanding, and transaction execution \cite{xie_osworld_nodate}. The ``AI Agent'' market is forecasted to grow from around USD 5-8 billion in 2025 to USD 42-52 billion by 2030. This surge reflects adoption in e-commerce, customer service, and enterprise automation, where agents handle interactions previously done by humans, raising the question of how these systems should be designed for future robustness as well as how to maintain a competitive edge in the analytical components of e-commerce platforms \cite{markntel_advisors_global_2025}.
The key stakeholders affected by the threat of increasing agent-driven traffic include online businesses and platform operators (especially in bot-heavy sectors like retail, travel, and financial services), their security, fraud, and engineering teams, end users whose accounts and data are exposed and whose experience degrades, regulators and legal stakeholders responding to breaches and fraud, and the attackers or bot operators driving the automation \cite{imperva_rapid_2025}.
The industry has already seen legal action in cases like Amazon against Perplexity \cite{ghaffary_amazon_nodate}, stemming from the difficulty of identifying traffic from hybrid systems like the Commet browser. This paper explores such systems to better understand what the interaction data looks like and what it means for dynamic pricing and recommendation systems downstream. This observed impact indicates a need for prevention of secondary negative effects on the ``legacy'' systems which power modern revenue sources for many companies. Dynamic pricing algorithms rely on directly translating demand features $q$ to new price assignments $\hat{p}$ across a catalogue of products of size $N$. This opens opportunities to design a \textit{tabula rasa} of digital market mechanisms that will shape the future of commerce in the age of artificial intelligence.
\subsection{Solution Space Overview}
Different approaches and perspectives, here also add a preview of what will be developed and explored in the lit review.
Dynamic pricing systems, as presented in \cite{mueller_low-rank_2019}, often deal with sparse low-rank data of demand signals which, combined with contamination from agents, creates complex interactions that impact pricing. To further complicate the problem, certain commercial settings such as the one presented in \cite{amjad_censored_2017} must address the true demand of products under censored observations. This provides a formulation for handling demand in our case with multiple kinds of commercial mediators: $\hat{q} \gets q_A + q_H$ where $q_A$ represents the distribution of demand generated by agentic mediators and $q_H$ represents that of true human demand, these are two distinct populations with divergent objective functions.
We formally define interaction data as coming from some actor which can either be an agent ($A$) or human ($H$). For purposes of this research, an agent is an algorithmic loop with the ability to access a web platform and perform actions such as clicks, scrolls, and input field fills. The loop terminates when the internal large language model judges the provided task definition as complete. A detailed breakdown can be found in \cref{algagent-loop}.
\begin{algorithm}[t]
\DontPrintSemicolon
\SetKwInOut{Input}{Input}
\SetKwInOut{Output}{Output}
\Input{Goal $G$, Platform URL $u$, LLM $\mathcal{M}$}
\Output{Task completion result $r$}
Initialize browser instance $\mathcal{B}$ with connection to $u$\;
Construct prompt $\pi \gets \textsc{BuildPrompt}(G, u)$\;
$\text{done} \gets \text{False}$\;
\While{$\neg \text{done}$}{
Observe current page state $s_t$ from $\mathcal{B}$\;
Query $\mathcal{M}$ with $(\pi, s_t)$ to determine next action $a_t \in \{\text{click}, \text{scroll}, \text{fill}, \text{navigate}\}$\;
Execute $a_t$ on $\mathcal{B}$ to transition to state $s_{t+1}$\;
$\text{done} \gets \mathcal{M}.\textsc{JudgeCompletion}(G, s_{t+1})$\;
}
Extract final result $r$ from terminal state\;
\Return{$r$}\;
\caption{AI Agent's Interaction Loop}
\label{algagent-loop}
\end{algorithm}
The previously described goal of separability allows us to formulate a task which entails taking raw interaction data for either actor and creating a composite demand estimate $\hat{q}$. We propose a robust optimization objective defined in our methodology, transforming the pricing problem into a form of Distributionally Robust Optimization \cite{kuhn_distributionally_2025} where the learner must guard against adversarial contamination in observed demand distributors. In this setting we must learn to make decision that perform under the assumption of not having a single estimated probability distribution but under an ambiguity set of any distribution, of which we have limited information. In our case as stated is a mixture of distributions with a parameter which is unknown and non-stationary.

View File

@@ -1,15 +1,44 @@
\section{Literature Review}
\subsection{Foundational Concepts}
To better understand all wedges of the work, we must start by exploring the nature of agents and agentic computer use and web automation, complementing that with economic reasoning and strategic interaction. The final surface to cover, leads us to data-driven dynamic pricing under uncertainty. The key technical risk is not ``agents buying things'' per se, but agents shaping the behavioral and demand signals that downstream pricing systems consume and depend on. The introduction of these mediating actor entities into economic systems, is further creating a threat of false-name bidding \cite{yokoo_effect_2004}, which prior research has explored in a trading context. Other research on pseudonyms in dynamic systems, demonstrate whitewashing in AI agents which can ignore defensive mechanisms by re-entry with different identities \cite{feldman_free-riding_2004}. Dynamic pricing assumes demand proxies are behaviorally meaningful, while bot detection aims at security and access control. The missing bridge is a principled framework for separating non-human reconnaissance from genuine human demand expression and integrating that separation into pricing heuristics without degrading legitimate user experience (in our research tracked by the user-experience index). This gap, is what our contribution aims to address, particularly for the aforementioned stakeholder groups.
\subsection{Agent Taxonomy and Definitions}
An agent in the context of artificial intelligence is generally defined by anything that can reason and act upon observations of its environments (collected through some sensory inputs) and carry out actions through effectors. Moreover, a rational agent is an entity that is capable of perceiving the world around them and taking actions to advance specified goals. This definition by \cite{russell_artificial_nodate} is further developed in an economic context by \cite{parkes_economic_2015}, suggesting AI research attempts to construct a synthetic \textit{homo economicus}, which may also be termed \textit{machina economicus}.
A specific class or taxon of this \textit{machina economicus}, the Large Language Model (LLM) agent, is defined as an autonomous system capable of achieving goals and adapting post-training, often without needing explicit code or fundamental model changes. \cite{xia_evaluation-driven_2025}
We must however acknowledge the current SOTA as presented by OSWORLD simulations in \cite{xie_osworld_nodate} have demonstrated that multi-modal tasks across desktop and web interaction modes, have a top-performing score of only 12.24\% success, whereas humans have a higher 72\% success rate. This weakness matters for this research because it clarifies the near-term threat model: practical exploitation does not require a fully competent ``computer assistant'', only enough automation to perform high-volume reconnaissance actions (search/filter/open product pages, probe availability/price boundaries) that can contaminate behavioral signals. With the expected growth of these capabilities, this threat only becomes more perilous to revenue management systems.
We model an agent session as producing some events with lower in-session conversion levels relative to humans, this we state in our assumption that $P(\text{purchase} \vert A) \ll P(\text{purchase} \vert H)$ but with a potentially higher volatility in $\hat{q}$, which we observe through the look-to-book metrics in our simulation.
\subsection{Economic Agents: From Homo Economicus to Machina Economicus}
Existing behavioral economic models tend to be criticized for the assumption of rational behavior, as is embodied in the term of homo economicus. The definition of a machina economicus by \cite{parkes_economic_2015} is quite appropriate for our case, particularly because these assumptions of rationality have been argued to be a very adequate reference for AI research by \cite{varian_economic_1995}. For modeling this behavior, the trajectories of these agents can be formally defined to be partially observable Markov decision processes. \cite{xie_osworld_nodate} Agents are however not to be confused with web-bots which have previously been known as automated software applications or scrapers which are set with a purpose of carrying out specific tasks on the internet, without a higher level of internal judgement. \cite{imperva_rapid_2025} In our research, we refer to this actor simply as an Agent belonging to the distribution $A$.
This economic framing also helps separate two related but distinct phenomena of agents as buyers (changing market demand composition), and agents as information gatherers (changing the observed interactions used by pricing/recommendation systems). The thesis focuses on the second, where information acquisition strategically precedes purchase execution. We do not however dismiss the proposed expectation that existing economic systems serving humans, will not be populated by AIs across multiple channels and with various possibly misaligned goals as stated by \cite{parkes_economic_2015}.
What is the taxonomy and definition of an agent and an actor in this case, a bit more about interaction models in sessions and about dynamic pricing algorithms.
\subsection{Problem Evidence and Market Impact}
Documented instances of agent-driven market disruptions - Quantitative evidence of pricing manipulation - Case studies from affected industries
\subsection{Theoretical Foundations: Economic Prallels}
The statistical issue of contamination in dynamic pricing systems that observe demand features as a means to update prices has been documented in various previous contexts. The airline industry (which has accounted for 24\% of observed disruptions) has seen malicious activity with a measureable impact on skewing key performance indicators by behavior visible in the look-to-book metrics. Excessive reconnaissance traffic inflates search volume without corresponding completed bookings, thereby skewing demand forecasts and disrupting dynamic pricing models. Demand proxies have also been observed to cause significant threat to inventory management by creating artificial scarcity that distorts the demand-supply relationships in the enterprise model. Censored demand as shown in \cite{amjad_censored_2017} can also be observed in low-bias demand under-estimation caused by a distortion effect coming from non-human traffic data. \cite{imperva_rapid_2025}
When dynamic pricing algorithms operate on highly contaminated or noisy data, the risk grows significantly in creating inaccurate price inferences. The emergent mitigation driven by un-informed reward and regret signals might lead to price suppression for sales continuity which results in harming margins and resulting in a revenue loss. System that poorly fit undesired behavior might result in price gouging, which calls for strong guardrails while preserving targeted business strategy. \cite{mullapudi_reinforcement_nodate}
%Documented instances of agent-driven market disruptions - Quantitative evidence of pricing manipulation - Case studies from affected industries
\subsection{Theoretical Foundations: Economic Parallels}
Early hints of exploration of prices in a standard English auction explored in \cite{varian_economic_1995} which hints at exploration of prices in a sequential manner, which leads to a marginally different cost to the bidder than the reservation price of the seller. This is a setting in which there is no cost incured by the buyer for their actions or exploring prices in the market. They propose that any agent responsable for the pricing of a good must be imune to dynamic strategies which might extract private information from a market. A key take-away which relates to the Vickery auction mechanism (also called a \textit{direct mechanism}) suggests that not only would defenses against such exploitation be necessary, but the construction of a mechanism in which revelation of the true willingness to pay is the dominant strategy for commerce.
Like in classical revenue-maximizing auctions \cite{roughgarden_cs364a_2013} we assume that the human actor in our system has a private valuation $v$ which we formally draw from later defined distributions. The important note here is that the agent proxy does not have a mechanism to convey this private information into the demand data which directly impacts the pricing systems.
% Economic foundations: relating the problem to options pricing theory. Cost of Information (COI) concept and its relevance
% Link Coasean Singularity and other economic market theory and highlight specific information of supra competitive pricing.
Economic foundations: relating the problem to options pricing theory. Cost of Information (COI) concept and its relevance
\subsection{Landscape of Existing Work}

View File

@@ -1,68 +1,251 @@
\section{Methodology}
This section details the theoretical and practical framework developed to address dynamic pricing under the influence of non-human actors. We begin by formalizing the problem environment and the nature of the actors. We then derive the \textit{Cost of Information} (COI) theorem, proving the erosion of pricing power in the limit of agent saturation. Following this, we outline our generative contamination strategy using GOFAI-driven separability and transition probability learning. Finally, we formulate the robust control problem as a Stackelberg game solved via Distributionally Robust Reinforcement Learning (DR-RL) with constructed ambiguity sets.
\subsection{Problem Formalization}
Mathematical formalization of agent-induced pricing distortions. Formal definition of potential loss mechanisms $\alpha D$
We define a commercial environment where the platform interacts with a stream of sessions. Let $\mathcal{S}$ denote the set of all sessions. Each session $s \in \mathcal{S}$ is generated by an actor belonging to a latent class $Y_s \in \{H, A\}$, where $H$ denotes Human and $A$ denotes Agent.
We consider a business across time during which we have an evolving vector $p_t \in \Re^N$ where $N$ is the number of products in our catalogue. our price vector is directly dependent on a demand function $q_t$ which we define as a linear method of a price elasticity matrix $B_t$. This is the same setup that Microsoft created in their research.
Each session produces a trajectory of observable events $\tau_s = (e_{s,1}, \ldots, e_{s,L_s})$. An event $e_{s,k}$ is a tuple defined as:
\begin{equation}
e_{s,k} = (a_{s,k}, i_{s,k}, t_{s,k})
\end{equation}
where:
\begin{itemize}
\item $a_{s,k} \in \mathcal{A}$ is the action taken (e.g., \texttt{view\_item}, \texttt{add\_to\_cart}).
\item $i_{s,k} \in \{1, \ldots, N\}$ is the target item index.
\item $t_{s,k} \in \mathbb{R}_+$ is the continuous timestamp.
\end{itemize}
We gether interaction data from users interacting with a sample platform simulating a hotel/airline which generates interaction distributions $I_t = \{(p_t, q_t^\text{obs}, \pi_t)\}_{t=1}^T$
The platform does not directly observe the true underlying demand function $d(p)$. Instead, it observes a behavioral proxy $\hat{q}_t$, which is a composite signal derived from the mixture of actor types. We define the demand proxy for product $i$ at epoch $t$ as a weighted aggregation of events:
\begin{equation}
\hat{q}_{t,i} = \sum_{s \in \mathcal{S}_t} \sum_{k=1}^{L_s} \omega(a_{s,k}) \cdot \mathbb{1}[i_{s,k} = i]
\end{equation}
where $\omega: \mathcal{A} \to \mathbb{R}_+$ assigns weights to actions based on their signal strength regarding willingness to pay.
\subsubsection{Actor Types and Demand Curves}
We formalize the heterogeneity of actors by introducing a type space $\Theta$. An actor of class $Y_s$ is further parameterized by a type $\theta \sim \mathcal{D}_{Y}$. This type determines the actor's demand response function $d(p; \theta)$, sampled from a distribution of possible demand curves. The total observed demand is a stochastic process governed by the mixture:
\begin{equation}
Q(p) = (1-\alpha) \cdot \mathbb{E}_{\theta \sim \mathcal{D}_H}[d(p; \theta)] + \alpha \cdot \mathbb{E}_{\theta \sim \mathcal{D}_A}[d(p; \theta)] + \epsilon_t
\end{equation}
where $\alpha \in [0, 1]$ represents the contamination parameter (proportion of agents) and $\epsilon_t$ is non-stationary market noise.
\subsection{Cost of Information Framework}
Mathematical demonstration and validation of the COI and citation backed evidence, and framework overview + show harm to user via other cost distortions. Maybe split into 3.2.1 (COI Theory) and 3.2.2 (Framework Design)
\subsection{Cost of Information (COI) Framework}
The \textit{Cost of Information} (COI) represents the markup a pricing policy $\pi$ attempts to extract from the market by leveraging demand signals. We define COI as the expected premium over the minimum viable price $\underline{p}$ (or marginal cost). This also speaks to the financial urgency as a consequence of information asymmetry between the platform and the actors.
\begin{definition}[Cost of Information]
Let $\pi(\tau)$ be a pricing policy mapping interaction histories to prices. The COI is defined as:
\begin{align}
\text{COI} &= \mathbb{E}[P] - \underline{p} \\
&= \int_{\underline{p}}^{\bar{p}} (1 - F_\pi(p)) \, dp
\end{align}
where $F_\pi(p)$ is the cumulative distribution function of prices generated by $\pi$ under standard operating conditions.
\end{definition}
\subsection{System Architecture}
\begin{figure}[ht]
\centering
\begin{tikzpicture}[
node distance=1.5cm and 2.5cm,
box/.style={rectangle, draw, thick, minimum height=1cm, minimum width=3cm, align=center, fill=blue!10},
kafka/.style={rectangle, draw=orange, thick, minimum height=1cm, minimum width=3cm, align=center, fill=orange!15},
arrow/.style={thick,->,>=Stealth}
]
\centering
\begin{tikzpicture}[scale=1.2]
% Define the Gaussian function: centered at 2
\def\bellcurve(#1){1.5 * exp(-0.5*((#1-2)/0.6)^2)}
% Nodes
\node[box] (webapp) {Web Application \\ (Producer \& Consumer)};
\node[kafka, below=of webapp] (kafka) {Apache Kafka \\ Cluster};
\node[box, below=of kafka] (backend) {Backend Services / Microservices \\ (Producers and Consumers)};
% Draw the main axis
\draw[->, thick] (0, 0) -- (4.5, 0) node[right] {$p$};
\draw[->, thick] (0, 0) -- (0, 2) node[above] {Density};
% Connections
\draw[arrow] (webapp) to[out=210,in=150] node[above]{Publish} (kafka);
\draw[arrow] (kafka) to[out=50,in=330] node[below]{Consume} (webapp);
\draw[arrow] (backend) -- node[above]{Publish/Consume} (kafka);
\draw[thick, smooth, samples=100] plot[domain=0:4] (\x, {\bellcurve(\x)});
\node at (3.2, 1.2) {$f_\pi(p)$};
% Optional: Kafka internal components
%\node[below=0.7cm of kafka, align=center] (topics) {Topics \\ Partitions};
% Define p_min and E[p]
\def\pmin{0.8}
\def\mean{2}
% Optional background
\begin{scope}[on background layer]
\node[draw, rounded corners, fill=orange!5, fit=(kafka), inner sep=0.3cm] {};
\end{scope}
\end{tikzpicture}
\caption{Technical Diagram}
% Vertical lines
\draw[dashed] (\pmin, 0) -- (\pmin, 2.0);
\draw[dashed] (\mean, 0) -- (\mean, 2.0);
% Labels on axis
\node[below] at (\pmin, 0) {$\underline{p}$};
\node[below] at (\mean, 0) {$\mathbb{E}[p]$};
\draw[<->, thick, red] (\pmin, 2.0) -- (\mean, 2.0) node[midway, above] {COI};
\end{tikzpicture}
\caption{Illustration of the Cost of Information (COI). The COI is defined as the difference between the expected price $\mathbb{E}[p]$ realized by the policy and the minimum viable price $\underline{p}$.}
\label{fig:coi_illustration}
\end{figure}
High level overview of how it works
We now formally demonstrate that standard dynamic pricing mechanisms are not incentive-compatible with high-frequency agentic traffic. As the number of independent competitive agents $N$ querying the system grows, the platform's ability to sustain a COI vanishes.
\begin{theorem}[COI Erosion in the Limit]
Let $N$ be the number of independent, utility-maximizing agents querying the platform. Let $p_{(1)}$ be the first order statistic (minimum) of the prices offered to these agents. As $N \to \infty$, the Cost of Information converges to 0.
\end{theorem}
\begin{proof}
Let $p_1, \ldots, p_N$ be independent and identically distributed (i.i.d.) price samples drawn from the policy's distribution $F(p)$ with support $[\underline{p}, \bar{p}]$. The realizable price for an optimal searching agent is the first order statistic $p_{(1)} = \min(p_1, \ldots, p_N)$.
The survival function (or reliability function) of the minimum price is given by:
\begin{equation}
S_{p_{(1)}}(t) = P(p_{(1)} > t) = [1 - F(t)]^N
\end{equation}
To determine the expected value $\mathbb{E}[p_{(1)}]$, we recall the property that for any continuous random variable $X$ with support $[A, B]$, the expectation can be expressed as the lower bound plus the integral of the survival function:
\begin{equation}
\mathbb{E}[X] = A + \int_{A}^{B} P(X > t) \, dt
\end{equation}
Applying this to our pricing statistic where the lower bound is $\underline{p}$:
\begin{align}
\mathbb{E}[p_{(1)}] &= \underline{p} + \int_{\underline{p}}^{\bar{p}} P(p_{(1)} > t) \, dt \\
&= \underline{p} + \int_{\underline{p}}^{\bar{p}} [1 - F(t)]^N \, dt
\end{align}
Since $F(t)$ is a valid CDF, for any $t > \underline{p}$, we have strict inequality $F(t) > 0$, implying $0 \le 1 - F(t) < 1$. By the properties of limits, as $N \to \infty$, the term $[1 - F(t)]^N$ converges to 0 pointwise for all $t > \underline{p}$.
Applying the Lebesgue Dominated Convergence Theorem (noting that the integrand is bounded by 1 on the finite interval $[\underline{p}, \bar{p}]$):
\begin{equation}
\lim_{N \to \infty} \int_{\underline{p}}^{\bar{p}} [1 - F(t)]^N \, dt = \int_{\underline{p}}^{\bar{p}} 0 \, dt = 0
\end{equation}
Substituting this back into the expression for COI:
\begin{align}
\lim_{N \to \infty} \text{COI} &= \lim_{N \to \infty} (\mathbb{E}[p_{(1)}] - \underline{p}) \\
&= \lim_{N \to \infty} \left( (\underline{p} + 0) - \underline{p} \right) \\
&= 0
\end{align}
\end{proof}
This result proves that standard pricing policies $\pi$ fail to extract surplus in the presence of large-scale agentic search, necessitating a robust counter-mechanism.
% The DRO objective creates a lower bound on COI extraction, effectively guaranteeing a minimum margin even in the presence of adversarial agents. we need to prove this and demonstrate that in a theorem.
%Mathematical demonstration and validation of the COI and citation backed evidence, and framework overview + show harm to user via other cost distortions. Maybe split into 3.2.1 (COI Theory) and 3.2.2 (Framework Design)
\subsection{System Architecture: Hybrid Kappa-Lambda Architecture}
In order for our research to have grounding in interactions we built a robust e-commerce web-platform. We initially conducted a survey of the leading platforms of airlines and hotel booking sites to identify the specific interface patterns that effectively manage complex travel data. Our analysis revealed a clear industry standard: while both sectors rely on tabbed service selection and left-sidebar filtering to streamline navigation, they diverge in result presentation: airlines utilize visual date-price bars and multi-step wizards to optimize for logistical transparency, whereas hotel platforms leverage image-led cards and scarcity triggers to drive emotional engagement and urgency. Our web framework defines a highly agnostic boilerplate which can be seeded with any data-modality with an easy-to-tailor pattern, which we leverage to define a \texttt{hotel} and \texttt{airline} mode. Both modes are then individually deployed via an environment level argument which adjusts the proxy routing with a custom middleware inside next.js to render only the desired mode. The purpose of this was to create a baseline adaptable to any use-case or desired commercial application.
The architecture of this platform begins with the deployed web-apps posting interaction data to our backend which processes them and stores each ingested interaction into a kafka cluster. This serves as our data reservoir tracking and associating each interaction with its session and importantly with which experiment it belongs to. Not only do we track the behavioral interactions, but our pricing provider micro-service, once called by the frontend reports the observed/queried price-product into kafka. This kafka cluster is subscribed to by our pipeline which is configured on a schedule in Airflow, with the possibility of manual trigger. The final stage of the pricing pipeline, submits computed dynamic pricing results into a redis database for quick updates which is then read by the pricing provider and displayed on the webapp. This is a very generic end-to-end mechanism which is applicable to a variety of different e-commerce tasks. We intentionally put emphasis on the development of this infrastructure to establish a reproducible framework for interaction and to minimize any noise.
\subsubsection{DevOps Principles}
\subsubsection{Online Dynamic Pricing}
The dynamic pricing done is handled by a pipeline which computes a demand estimate on a per-product basis of a specific window of the data, defined by the period $T$ which by default is 5 minutes. This dynamic pricing pipeline computes a demand estimate vector $\hat{q} \in \mathbb{R}^N$ by a weighted sum of interactions for each product, it additionally computes a price elasticity vector $\hat{\epsilon}$ in the same dimensions as our demand. The final features matrix is of the size $N \times 2$ which we translate to a new price vector $\hat{p} \in \mathbb{R}^N$. The transformation that governs this dynamic pricing is a very simple surge-based pricing (a special case of our later defined policy $\pi$):
\begin{equation}
\hat{p}_i = \begin{cases}
p_{0,i} \cdot \lambda_{\text{surge}} & \text{if } \hat{q}_i \geq \theta_{\text{high}} \\
p_{0,i} \cdot \lambda_{\text{disc}} & \text{if } \hat{q}_i \leq \theta_{\text{low}} \\
p_{0,i} & \text{otherwise}
\end{cases}
\quad \forall i \in \{1, \ldots, N\}
\end{equation}
where $p_0 \in \mathbb{R}^N$ is the base price vector (which is seeded into our database distinctly for each mode of the commerce platform), $\theta_{\text{high}}, \theta_{\text{low}} \in \mathbb{R}$ are demand thresholds defining surge and discount regions, and $\lambda_{\text{surge}}, \lambda_{\text{disc}} \in \mathbb{R}^+$ are multiplicative factors with typical values $\lambda_{\text{surge}} = 1.2$ and $\lambda_{\text{disc}} = 0.9$. This piecewise function enables rapid price adjustment in response to observed demand without requiring complex elasticity estimation or historical calibration, allowing us to expose actors within our experiments to a system with a dynamic component of pricing.
We will for our offilne experimental intents generalize a master function for encompasing distinct demand estimation and pricing strategies.
\begin{align}
V(\cdot) = \max_{p_t} \min_{Q \in \mathcal{U}(\hat{d})}{\mathbb{E}_{d\sim Q} [p_t \times d(p_t, x_t ; \theta) + \psi V_{t+1}(\cdot)]}
\end{align}
We follow differnet substitutouns which will server as hyperparameters later on.
\subsection{Experimental Design}
Study methodology and approach. Data acquisition strategy. Defined objectives and success criteria. Observable metrics and KPIs
\subsection{Dynamic Pricing Algorithm Analysis}
Deep dive into how the algorithm works, different kinds and justification for chosen appraoches + agent impact modeling and quantification.
\subsection{Reinforcement Learning Formulation}
How do we define the state space, action space and reward function breakdown and algorithm benchmarking.
POSSIBLY: Expand into full subsections: 3.6.1 (State-Action Space), 3.6.2 (Reward Design), 3.6.3 (Benchmarking)
The experimentation begins with the design of goals, with careful consideration to assure a uniform spanning across different variables within each product-architecture of either the hotel or airline platforms. Our crafted collection of goals (jobs to be done) is then tracked in a postgress database with one table to track goals and another table to track different experiment runs, and their associated goals in a experiment-goal one-to-one relationship.
The purpose of this effort to gather data on interactions, is the first half of our research. With this collected data on behavioral characteristics, enhanced by our feature augmentation, we can create distribution separation into two bins $y \in \{A,H\}$ with a certain probability $p$ dependent on the session-specific features. To address the second loop of our system, we use this gained capability of discrimination to enhance the learner design involved in our surrogate dynamic pricing task which simulates an independent dynamic pricing scenario under which we can train a more controlled policy with the ability to account for true demand signals under conditions of contamination from non-human actors.
\begin{algorithm}[t]
\DontPrintSemicolon
\KwIn{stepsize $\eta$, smoothing $\delta$, rank $d$}
\For{$t=1$ \KwTo $T$}{
Sample $u_t$ on unit sphere; set $x_t^\prime=x_t+\delta u_t$\;
Set $p_t \gets U x_t^\prime$ and observe $q_t, R_t(p_t)$\;
$x_{t+1} \gets \Pi\_{\mathcal{X}}(x_t-\eta R_t(p_t) u_t)$\;
}
\caption{Online Pricing Optimization (template)}
\end{algorithm}
Our approach can be well summarized by a three-stage division, first we intend to observe and \textit{vectorize} the behavioral interaction data from our experiments, we then develop the separability which helps us deepen the semantic understanding of the behavioral patterns. Finally we use our newly gained learner to leverage a defensive mechanism within the simulation stage of a controlled dynamic pricing loop.
\begin{figure}[ht]
\resizebox{\columnwidth}{!}{%
\input{chapters/loop_figure.tex}
}
\caption{Overview of the Dynamic Pricing Tasks.}
\end{figure}
Study methodology and approach. Data acquisition strategy. Defined objectives and success criteria. Observable metrics and KPIs.
\subsection{Generative Contamination and Separability}
To develop a robust pricing agent, we require a simulation environment capable of generating realistic, contaminated interaction data. We achieve this by learning from our Phantom platform data using a two-stage approach.
\subsubsection{GOFAI-Based Separability}
We employ Good Old-Fashioned AI (GOFAI) heuristics to generate initial weak labels for separability. We define a set of rule-based predicates $\phi_j: \tau \to \{0, 1\}$ to partition the dataset $\mathcal{D}$ into high-confidence sets $\mathcal{D}_H$ and $\mathcal{D}_A$. We construct distinct MDPs per each behavioral profile of humans and agents and from those we establish $D_{KL}$. From initial findings we compute a KL divergence of $\approx 2.0236$ across transition probabilities between states which can be seen in \ref{fig:human_mdp_viz} and \ref{fig:agent_mdp_viz}.
\begin{figure}[ht]
\centering
\includegraphics[width=0.8\textwidth]{chapters/mdp_human.pdf}
\caption{Markov Decision Process visualization illustrating the behavioral transition dynamics for human actions.}
\label{fig:human_mdp_viz}
\end{figure}
\begin{figure}[ht]
\centering
\includegraphics[width=0.8\textwidth]{chapters/mdp_agent.pdf}
\caption{Markov Decision Process visualization illustrating the behavioral transition dynamics for \textbf{agent} behavior profiles. The state space and transition probabilities are learned from observed session trajectories to enable generative contamination.}
\label{fig:agent_mdp_viz}
\end{figure}
\subsubsection{Transition Probability Estimation}
For both subsets, we model the session dynamics as a Markov Decision Process (MDP) and estimate the transition kernel $\mathcal{T}$. The probability of transitioning to state $s'$ given state $s$ is estimated via maximum likelihood:
\begin{equation}
\hat{P}(s' \mid s) = \frac{N(s, s')}{\sum_{k \in \mathcal{S}} N(s, k)}
\end{equation}
where $N(s, s')$ is the count of observed transitions. This allows us to construct a \textit{Contamination Generator} $\mathcal{G}(\alpha)$. Given a clean trajectory dataset, $\mathcal{G}$ injects synthetic agent trajectories sampled from the learned transition matrix $\hat{P}_A$ until the effective mixing ratio reaches $\alpha$.
\subsection{Distributionally Robust Reinforcement Learning (DR-RL)}
We formulate the pricing problem as a Stackelberg Game where the Platform (Leader) sets prices $p_t$ and the Aggregate Demand (Follower) responds. However, the exact mixing parameter $\alpha$ and the demand distribution shift are non-stationary and unknown in online settings. Relying on a simple error term $\epsilon$ is insufficient. Instead, we adopt a Distributionally Robust Optimization (DRO) objective.
\subsubsection{Ambiguity Set Construction}
We define an ambiguity set $\mathcal{U}_p(\hat{P}_N)$ centered around our empirical reference distribution $\hat{P}_N$ (derived from the generator $\mathcal{G}$). We utilize the Wasserstein distance metric to define the set of plausible demand distributions the agent might face:
\begin{equation}
\mathcal{U}_\epsilon(\hat{P}_N) = \left\{ Q \in \mathcal{P}(\Xi) : W_p(Q, \hat{P}_N) \le \epsilon \right\}
\end{equation}
This set captures all distributions that are statistically close to our observed training data but allows for adversarial shifts (e.g., sudden bot spikes).
\subsubsection{The Min-Max Objective}
The robust policy $\pi^*$ is obtained by solving the maximin problem:
\begin{equation}
\pi^* = \arg \max_{\pi} \min_{Q \in \mathcal{U}_\epsilon} \mathbb{E}_{d \sim Q} \left[ R(p, d) - \lambda \cdot \text{COI}(p) \right]
\end{equation}
where $R(p, d)$ is the revenue function and $\lambda$ weighs the penalty for information leakage (COI).
\subsubsection{Actor Implementation}
In our simulation, the "Follower" is implemented as a set of Actors. Each Actor is initialized with a type $\theta$ which samples a specific demand curve $d(p; \theta)$ from the latent distribution. This formalization ensures that our DR-RL agent does not overfit to a single deterministic demand function but learns a policy robust to the distributional uncertainty defined by $\mathcal{U}_\epsilon$.
As part of our reward engineering we think about the UX factor ($UX \in [0,1]$) whic his our proxy for user experience degradation, this is computed as a mixture of contribution from the separability model metric of $\frac{1}{\text{Specificity}}$.
\begin{figure}[ht]
\centering
\resizebox{0.5\columnwidth}{!}{%
\input{chapters/balance_figure.tex}
}
\caption{Introducing the UX index allows us to better distinguish the kind of impact different methods have and allows us to compare them on this Pareto-like scale.}
\end{figure}
We also need to think about a policy like taxation to the agents Strategy-Proof Mechanism Design, specifically the Vickrey-Clarke-Groves (VCG) payment rule. We link and prove that this would create an incentive for the dominant strategy to become truth-telling.
\section{Heuristics as part of neuro-inspired steering systems}
Steve Burns, superior culliculus (face heuristics) we create this sort of part of the 'brain' + amortized inference.
We could say that a DQN for example is the learnin subsystem and then within our reward mechanism or some other computational method we introduce a steering subsystem which acts as the proposed ``pricing heuristic'' against the given non human transaction data.
\section{Market construction}

View File

@@ -1,5 +1,15 @@
\section{Discussion}
\subsection{Transition to Agentic Market Microstructure}
Our analysis of the interaction dynamics between the platform and non-human actors suggests that the current static pricing models are insufficient for an agent-mediated economy. If we assume a transition toward a direct revelation mechanism, where actors must reveal their true valuation of a good through bidding dynamics, we inevitably introduce significant stochasticity into the pricing system. Unlike traditional e-commerce where prices are relatively sticky, such a mechanism implies a high volatility characteristic of financial equity markets (without the fungability however).
However, ecommerce commodities differ fundamentally from financial securities: they possess a hard floor defined by unit economics and reservation prices. The market might react enthusiastically to an iPhone priced at \$1, such a transaction is not permissible. The platform must establish an initial valuation anchor ($P_{0}$) defined by the marginal cost plus a target margin, around which the market price is permitted to fluctuate. We propose the introduction of GenAI Agents as Institutional Market Makers.
This is also under the assumption of expected transactional capabilities being given to AI Agents.
\subsection{Risk Assessment and Limitations}
Acknowledge risks and constraints and data sizes.

View File

@@ -1,6 +1,6 @@
\section{Conclusion}
\subsection{Summary of contributions }
\subsection{Summary of contributions}
Restate the thesis and key findings with validation of research objectives.
\subsection{Future Works and Next Steps}

View File

@@ -0,0 +1,38 @@
\begin{tikzpicture}[
% Styles for consistency
axis/.style={->, >=Stealth, line width=1.2pt, color=black!85},
curve/.style={color=black, line width=2.5pt},
point/.style={circle, fill=black, inner sep=0pt, minimum size=6pt},
label_text/.style={font=\large, align=center, color=black},
annotation_line/.style={thick, -, color=black!60}
]
% Define Radius
\def\R{5}
% Draw Axes
% Extended slightly beyond radius (\R + 1)
\draw[axis] (0,0) -- (\R+1.5,0) node[midway, below=10pt, font=\bfseries\large] {UX Index};
\draw[axis] (0,0) -- (0,\R+1.5) node[midway, left=15pt, rotate=90, font=\bfseries\large] {Performance};
% Draw Perfect 1/4 Circle
% Syntax: arc (start_angle : end_angle : radius)
\draw[curve] (0,\R) arc (90:0:\R);
% 1. Paranoid (High Performance side) -> Angle 67.5 degrees
\node[point] (p1) at (75:\R) {};
\node[label_text, above right=0.1cm and 0.1cm of p1] (l1) {Paranoid};
\draw[annotation_line] (l1) -- (p1);
% 2. Perfect Detection (Exact Middle) -> Angle 45 degrees
\node[point] (p2) at (45:\R) {};
\node[label_text, above right=0.2cm and 0.2cm of p2] (l2) {Perfect Detection};
\draw[annotation_line] (l2) -- (p2);
% 3. No Detection (High UX side) -> Angle 22.5 degrees
\node[point] (p3) at (15:\R) {};
\node[label_text, right=0.5cm of p3] (l3) {No Detection};
\draw[annotation_line] (l3) -- (p3);
\end{tikzpicture}

View File

@@ -0,0 +1,65 @@
\begin{table}[ht]
\centering
\small
\resizebox{\columnwidth}{!}{%
\begin{tabular}{p{4.5cm}p{1.5cm}p{6cm}}
\hline
\textbf{Feature} & \textbf{Type} & \textbf{Description} \\
\hline
\multicolumn{3}{l}{\textit{Session Identifiers}} \\
sessionId & object & Unique identifier for user session \\
experimentId & object & Experiment run identifier \\
\hline
\multicolumn{3}{l}{\textit{Temporal Features}} \\
session\_duration\_sec & float & Total session duration in seconds \\
avg\_time\_between\_events & float & Mean inter-event time \\
std\_time\_between\_events & float & Standard deviation of inter-event times \\
min\_time\_between\_events & float & Minimum time between consecutive events \\
session\_start\_hour & int & Hour of day when session started \\
\hline
\multicolumn{3}{l}{\textit{Interaction Metrics}} \\
total\_interactions & int & Count of all user interactions \\
total\_events & int & Total number of tracked events \\
interaction\_velocity & float & Rate of interactions per time unit \\
max\_velocity\_5min & int & Peak interaction count in any 5-minute window \\
\hline
\multicolumn{3}{l}{\textit{Navigation Behavior}} \\
unique\_pages & int & Number of distinct pages visited \\
page\_views & int & Total page view events \\
\hline
\multicolumn{3}{l}{\textit{Product Engagement}} \\
item\_views & int & Number of product detail views \\
unique\_products\_viewed & int & Count of distinct products examined \\
product\_view\_depth & int & Repeat views of same products \\
\hline
\multicolumn{3}{l}{\textit{Conversion Funnel}} \\
cart\_adds & int & Number of items added to cart \\
purchases & int & Completed transactions \\
cart\_to\_view\_ratio & float & Ratio of cart additions to item views \\
conversion\_rate & float & Purchase to view conversion \\
\hline
\multicolumn{3}{l}{\textit{Interaction Quality}} \\
hover\_events & int & Mouse hover event count \\
hover\_intensity & float & Hover events per interaction \\
\hline
\multicolumn{3}{l}{\textit{Price Behavior}} \\
avg\_price\_seen & float & Mean price across viewed products \\
min\_price\_seen & float & Lowest price encountered \\
max\_price\_seen & float & Highest price encountered \\
price\_range & float & Difference between max and min prices seen \\
\hline
\multicolumn{3}{l}{\textit{Technical Fingerprinting}} \\
is\_headless & bool & Headless browser detection flag \\
is\_automation & bool & Automation framework detection flag \\
browser\_family & object & Browser type classification \\
\hline
\multicolumn{3}{l}{\textit{Experimental Labels}} \\
is\_agent & bool & Ground truth agent classification \\
xp\_human\_only & bool & Human-only experiment indicator \\
xp\_market\_mode & object & Market context (hotel/airline) \\
\hline
\end{tabular}%
}
\caption{Feature matrix schema for session-level behavioral classification (32 features total).}
\label{tab:features}
\end{table}

View File

@@ -0,0 +1,110 @@
\definecolor{mygreenfill}{RGB}{169, 234, 186}
\definecolor{mygreenborder}{RGB}{29, 145, 61}
\definecolor{mybluefill}{RGB}{204, 222, 255}
\definecolor{myblueborder}{RGB}{66, 106, 189}
\definecolor{mygray}{RGB}{150, 150, 150}
\begin{tikzpicture}[
node distance=2cm,
% Style for Green Nodes
greenbox/.style={
rectangle,
draw=mygreenborder,
fill=mygreenfill,
line width=1.2pt,
align=center,
minimum height=1cm
},
% Style for Blue Nodes
bluebox/.style={
rectangle,
draw=myblueborder,
fill=mybluefill,
line width=1.2pt,
align=center,
minimum height=1cm
},
% Style for Arrows
myarrow/.style={
->,
>={Stealth[length=3mm, width=2mm]},
draw=black!80,
line width=1.2pt,
rounded corners=5pt
},
% Style for Background Dashed Circles
dashedloop/.style={
dashed,
draw=mygray,
line width=1pt
}
]
% --- Coordinate Layout ---
% Defining a grid relative to the center
% Left Loop (Green) Nodes
\node[greenbox, minimum width=3.5cm] (commerce) at (-3.5, 2) {Commerce Experiment};
\node[greenbox, minimum width=1.5cm] (raw) at (-6.5, 0) {Raw\\Logs};
\node[greenbox, minimum width=1.5cm] (features) at (-4, -2.5) {Features};
\node[greenbox, minimum width=2.5cm] (classification) at (-1, -0.5) {Classification\\Training A/H};
% Right Loop (Blue) Nodes
\node[bluebox, minimum width=2.5cm] (trainedpricing) at (3.2, 2) {Trained Pricing};
\node[bluebox, minimum width=2.5cm] (policy) at (6.5, 0) {Trained Pricing\\Policy};
\node[bluebox, minimum width=2.5cm] (rlgym) at (3.2, -2.2) {RL Gym\\Training};
% --- Background Dashed Loops ---
\begin{scope}[on background layer]
% Left Loop Circle
\draw[dashedloop] (-3.5, 0) ellipse (3.5cm and 2.8cm);
% Right Loop Circle
\draw[dashedloop] (3.5, 0) ellipse (3.5cm and 2.8cm);
\end{scope}
% --- Arrows: Loop One (Green) ---
% Commerce -> Raw Logs
\draw[myarrow] (commerce.west) to[out=180, in=90] (raw.north);
% Raw Logs -> Features
\draw[myarrow] (raw.south) to[out=270, in=180] (features.west);
% Features -> Classification
\draw[myarrow] (features.east) to[out=0, in=250] (classification.south);
% Classification -> Commerce (Closing the loop)
\draw[myarrow] (classification.north) to[out=110, in=0] (commerce.east);
% --- Arrows: Loop Two (Blue) ---
% Classification (Green) -> RL Gym (Blue) - Crossing over
\draw[myarrow] (classification.east) to[out=0, in=180] (rlgym.west);
% RL Gym -> Policy
\draw[myarrow] (rlgym.east) to[out=0, in=270] (policy.south);
% Policy -> Trained Pricing
\draw[myarrow] (policy.north) to[out=90, in=0] (trainedpricing.east);
% Trained Pricing -> Commerce (Crossing back)
\draw[myarrow] (trainedpricing.west) -- node[above, font=\small, yshift=2pt] {New Pricing} (commerce.east);
% --- Text Labels ---
% Loop One Label
\node[align=center] at (-3.8, 0) {Loop One:\\Data \textit{(Online)}};
% Loop Two Label
\node[align=center] at (3.5, 0) {Loop Two:\\Defense Gym \textit{(Offline)}};
% Bottom Legend
\node[font=\small] (taskA) at (-4, -4) {Dynamic Pricing Task A};
\node[font=\small] (taskB) at (4, -4) {Dynamic Pricing Task B};
\node[font=\small] (indep) at (0, -4) {Independent};
% Arrows for bottom legend
\draw[->, >=Stealth, thick, darkgray] (indep.west) -- (taskA.east);
\draw[->, >=Stealth, thick, darkgray] (indep.east) -- (taskB.west);
\end{tikzpicture}

Binary file not shown.

Binary file not shown.

View File

@@ -1,93 +1,51 @@
% -*- TeX-master: t -*-
\documentclass[sigconf,nonacm,natbib=false]{acmart}
\documentclass[12pt,letterpaper]{article}
% Remove ACM copyright/conference info for thesis
\settopmatter{printacmref=false}
\renewcommand\footnotetextcopyrightpermission[1]{}
\pagestyle{plain}
\input{preamble}
\begin{document}
\title{First Proposal: Pricing Heuristics Against Non-human Transaction Orchestration Mechanisms}
\title{Adversarially Distributionally Robust Optimization and Reinforcement Learning for Informed Dynamic Pricing under Strategic Demand Contamination}
\author{Daniel Rösel}
\email{daniel@alves.world}
\affiliation{%
\institution{IE University}
\city{Madrid}
\country{Spain}
\author{
Daniel Rösel\thanks{Primary author and student researcher. Email: daniel@alves.world} \\
IE University, Madrid, Spain \\[1em]
Alberto Martín Izquierdo\thanks{Thesis advisor. Email: amartini@faculty.ie.edu} \\
IE University, Madrid, Spain
}
\author{Alberto Martín Izquierdo}
\email{amartini@faculty.ie.edu}
\affiliation{%
\institution{IE University}
\city{Madrid}
\country{Spain}
}
\begin{abstract}
The primary objective of this thesis is to develop and validate pricing heuristics that protect e-commerce platforms from systematic exploitation by Large Language Model (LLM) agents within dynamic pricing environments. As AI agents increasingly mediate consumer transactions, they enable users to circumvent the Cost of Information (the price premium accumulated through demand signal expression) by conducting reconnaissance in isolated sessions before executing purchases through clean sessions at base prices. This research will make an anticipatory contribution by adapting recommendation system methodologies to distinguish between genuine human browsing behaviour and agent-orchestrated information gathering, thereby enabling pricing systems to maintain margin integrity without degrading the user experience for legitimate customers or getting rid of leads generated by LLMs.
\end{abstract}
\date{\today}
\maketitle
\section{Preliminary literature review}
From very relevant news, the legal conflicts of agentic access to platforms have clearly indicated a need for prevention of secondary negative effects on ``legacy'' systems which power modern pricing systems \cite{Ghaffary}. Dynamic pricing algorithms rely on directly translating demand features $q$ to $\hat{p}$ new price assignments across a catalogue of products. This demand estimation does often take into account a small degree of error and noise from the data. However, adversarially introduced interactions, which are non-conducive to pricing optimization nor are a fully accurate representation of the driving human demand, have not been considered as part of the systems. Research such as \cite{Mueller2019} introduces very clear methodology for pricing algorithms backed by demand estimation for online pricing optimization which can be followed for proposing adjustments and improvements as highlighted in \ref{research}. Another often encountered demand distortion occurs through censored demand environments \cite{Amjad2017}.
Other efforts such as \cite{Calvano2018} explore ways of modeling the interactions between multiple pricing algorithms or agents which in an effort to maximize their reward drive the market to supra-competitive pricing which leaves the boundaries of the market equilibrium, creating a harmful effect on the customers by this process of algorithmic collusion. This harm can be directly translated to our setting where through interactions between two learners there is a potential of market destabilization.
\begin{abstract}
The primary objective of this thesis is to develop and validate pricing heuristics that protect e-commerce platforms from systematic exploitation by Large Language Model (LLM) agents within dynamic pricing environments. As AI agents increasingly mediate consumer transactions, they enable users to circumvent the Cost of Information (the price premium accumulated through demand signal expression) by conducting reconnaissance in isolated sessions before executing purchases through clean sessions at base prices. This research will make an anticipatory contribution by adapting recommendation system methodologies to distinguish between genuine human browsing behavior and agent-orchestrated information gathering, thereby enabling pricing systems to maintain margin integrity without degrading the user experience for legitimate customers or getting rid of leads generated by LLMs.
\end{abstract}
\section{Research question or objective} \label{research}
\begin{quote}
How do agent-generated interactions contaminate demand functions in dynamic pricing algorithms, and how significantly does this contamination affect key performance indicators ($\Delta$)?
\end{quote}
The objectives are to gather data on how humans ($H$) and agents ($A$) interact with commerce platforms, and to identify the most reliable methodology for true demand estimation to fuel the dynamic pricing algorithm. This discrimination task can be accomplished through three distinct approaches:
\begin{enumerate}
\item \textbf{Explicit filtering approach:} Decompose pipeline components and employ an estimator $P(A|s)$ (where $s$ represents session interaction data) to explicitly filter agent-generated interactions from the processing stream.
\item \textbf{Learned transformation approach:} Utilize a learned transformation on the product demand feature $B$, where $B = B_H + B_A$, with the goal of deriving a more representative demand feature $B_\text{clean} = B_H + W_\epsilon B_A$ that appropriately weights agent contributions.
\item \textbf{Reinforcement learning approach:} Frame the problem as a reinforcement learning task where interactions are modeled as environmental components, guiding the algorithm to learn an appropriate pricing policy that implicitly accounts for genuine human demand ($B_H$).
\end{enumerate}
\input{chapters/01-intro}
\input{chapters/02-literature-review}
\input{chapters/03-methodology}
\input{chapters/04-results}
\input{chapters/05-discussion}
\input{chapters/06-conclusion}
\section{Execution plan with approximate calendar}
This is a tentative execution plan for this research, keeping in mind a more agile approach rather than a waterfall-like set of goals and targets:
\begin{description}
\item[November 2024:] Complete platform deployment for data collection and observations (70\% complete). Implement user authentication system with magic link invites to enable participant enrollment.
\item[December 2024:] Gather initial interaction data and explore the separability of distributions between human and agentic interaction patterns. Begin testing online algorithms for session-based pricing optimizations.
\item[January 2025:] Conduct controlled experiments comparing human versus agent execution of identical tasks. Establish behavioral signature models and quantify contamination impact ($\Delta$). Develop and validate the explicit filtering approach using $P(A|s)$ estimator.
\item[February 2025:] Design and train the learned transformation model for demand feature adjustment ($B_\text{clean}$). Implement reinforcement learning framework and train pricing policy that implicitly accounts for genuine human demand.
\item[March 2025:] Conduct comparative evaluation across all three proposed approaches. Finalize experimental results and perform statistical analysis of revenue recovery and KPI improvements.
\item[April 2025:] Internal review, revisions, and thesis documentation finalization. Prepare for final submission.
\end{description}
\section{Desired measurable outcome or answer}
The first step is measuring how well we can separate human from agent session data. We can start with standard accuracy metrics as a baseline.
What really matters for the larger picture is the economic impact of accurate demand estimation. We measure this through revenue leakage and revenue recovery. For benchmarking, we need to compare scenarios under default pricing policies versus adjusted ones - this gives us lower and upper bounds for our performance.
Since we're also concerned with human-centric outcomes, we need to collect user friction ratings that compare more radical solutions (like CAPTCHAs) against minimal or no defenses.
\section*{Acknowledgments}
Eugene Bykovets, PhD - ETH for helping with problem formulation.
Research supported with Cloud TPUs from Google's TPU Research Cloud (TRC).
\printbibliography
% \clearpage
% \onecolumn
% \appendix
\clearpage
\appendix
\section{Terminology}
\begin{description}
\item[Agent $A$] An actor of non-human nature, powered by an LLM.
\item[Human $H$] An individual human with some job to be done.
\end{description}
\input{../build/concatenated_code}
\end{document}

View File

@@ -1,6 +1,25 @@
% acmart already includes: graphicx, hyperref, booktabs, amsmath, natbib
% Only load packages not included in acmart
% Math packages (load before fonts to avoid conflicts)
\usepackage{amsmath}
\usepackage{amsthm}
% Define theorem environments
\newtheorem{theorem}{Theorem}
\newtheorem{definition}{Definition}
\newtheorem{lemma}{Lemma}
\newtheorem{corollary}{Corollary}
% Font and spacing
\usepackage{newtxtext,newtxmath}
\usepackage{setspace}
\doublespacing
% Page geometry
\usepackage[margin=1in]{geometry}
% Essential packages
\usepackage{graphicx}
\usepackage{hyperref}
\usepackage{booktabs}
\usepackage{csquotes}
\usepackage{subcaption}
\usepackage{siunitx}
@@ -8,6 +27,10 @@
\usepackage{listings}
\usepackage{xcolor}
\usepackage[ruled,vlined]{algorithm2e}
\usepackage{cleveref}
% Configure cleveref for algorithm2e
\crefname{algocf}{Algorithm}{Algorithms}
\usetikzlibrary{positioning, shapes, arrows.meta, fit, backgrounds}
\lstset{

8
pytest.ini Normal file
View File

@@ -0,0 +1,8 @@
[pytest]
pythonpath = experiments
testpaths = experiments
python_files = test*.py
python_classes = Test*
python_functions = test_*
asyncio_mode = auto
asyncio_default_fixture_loop_scope = function

Some files were not shown because too many files have changed in this diff Show More