This repo contains a teaching demo for data analytics students. It was developed with assistance from ChatGPT, GitHub Copilot, GitHub Workspaces, GitHub Actions, Google Cloud Run, and Google Cloud Build.
You start with raw events (website sessions and purchases) and gradually turn them into a trustworthy metric behind a Streamlit app and a Cloud Run URL.
You don’t need to start as a software engineer to work through this, but the goal is to gently pull you in that direction. Most of the work is changing parameters, running notebooks, and reading charts – plus a bit of real coding that shows you the "back of the dashboard" and builds the habits of a full‑stack analytics engineer.
The bigger theme: modern analytics is not only Power BI / dashboards. With a bit of Python and SQL, you can build your own small but robust analytics products: local apps, hosted apps, and even CI/CD pipelines with role-based access and AI assistance – all using the same core ideas you already know from analytics work.
By working through this repo, students see the full story:
- Event data → tables
Simulated sessions and purchases land in DuckDB asraw_sessionsandraw_conversions. - Metric "contract" (whatever your client wants to see as KPIs -- metric definitions as code)
A SQL view (f_attribution) defines when a purchase “counts” as attributed to a session (within N days). - Quality checks
Simple rules catch bad data (negative revenue, orphan conversions, broken timestamps) before it ships. - Product surface
A Streamlit app (“NorthPeak Retail”) shows the same engine behind a URL with sliders and charts. - Optional AI cleaner
An AI "cleaning agent" (OpenAI or a local Gemma model) proposes how to fix bad rows, and an AI judge checks the result.
The goal is to move from an Excel mindset (“numbers just appear”) to a systems mindset (“numbers come from code, metric definitions, and checks”).
This is a suggested 1‑hour lesson plan if you are teaching this workshop.
- Show two conflicting numbers for “conversions” from different dashboards.
- Explain that both are “correct” for their own definition, but confusing together.
- Open the Streamlit app URL (local or Cloud Run). This is the product students are trying to understand.
Q&A (2–3 minutes):
Ask: “Where have you seen ‘dueling dashboards’ in real life?”
- Generate deterministic
session_startandpurchaseevents. - Register them as
raw_sessionsandraw_conversionsin DuckDB. - Discuss why event data is messy for business users (multiple timestamps, missing links, strange edge cases).
Q&A (3–5 minutes):
Ask: “Which of these raw columns would you not show directly to a VP, and why?”
- Build
f_attributionas a semantic view on top of the raw tables. - Define the "contract": a purchase only counts if it happens within N days of a session.
- Compare:
- naive conversions (every purchase)
- trusted conversions (within the "contract" window)
- out-of-window conversions.
- Flip the window from 7 → 30 days and see how “truth” changes.
Q&A (3–5 minutes):
Ask: “If Finance and Marketing disagree, which ‘truth’ should win—and who decides?”
- Add and run simple checks:
- negative revenue
- orphan conversions (no matching session)
- invalid or future timestamps.
- Message: if checks fail, we don’t ship the metric.
- Open
app.pyand point out it uses the same generator and SQL view. - In the Streamlit app, change:
- History window (days)
- Attribution window (days)
- Toggle “Inject demo anomalies” and watch quality metrics react.
Q&A (3–5 minutes):
Ask: “Which check would you add next for your own company’s data?”
- Briefly show that the AI cleaner (OpenAI or local Gemma) proposes a plan and that a judge reviews it.
- Emphasize that the AI is constrained by your definitions and quality rules.
- Connect to real teams: how this pattern maps to BI tools, data platforms, and ML systems students might see on co‑op.
Final close (2–3 minutes):
- One-line summary: Metrics are code, "contracts" (KPIs as code) are governance, and products are URLs.
- Invite questions about how they might adapt this pattern to their own internships or projects.
markettech_workshop.py/markettech_workshop.ipynb
The main workshop notebook (Python script + Jupyter notebook). You can run either version.app.py
The Streamlit app (NorthPeak Retail). Uses the same data generator and SQL "contract" as the notebook.ai_cleaning_agent.py
Optional agentic loop: planner model, deterministic cleaning code, and judge model.test_engine.py
A few small tests that prove:- data generation is deterministic
- the metric "contract" behaves as described
- the AI cleaner only runs when a key is present.
.github/workflows/ci.yml
CI pipeline: runs tests and, onmain, builds and deploys to Cloud Run.docker.yml
GitHub Actions workflow that builds the Docker image (no push) for quick feedback.Dockerfile
How the app is containerized for Cloud Run.main.tf,variables.tf,versions.tf
Terraform files that describe the Cloud Run service and Artifact Registry.deploy_cloud_run.sh,cleanup_cloud_run.sh,set_openai_key_cloud_run.sh
Helper scripts for instructors to deploy / tear down resources and configure the OpenAI key in Secret Manager.
You can safely ignore the Terraform and shell scripts if you are just a student following the workshop. An instructor or DevOps engineer will usually prepare the Cloud Run URL for you.
The Dashboard (instructor to provide link to the published dashboard)
This keeps Python packages for the workshop separate from your system.
python -m venv .venv
# Windows
.venv\Scripts\activate
# macOS / Linux
source .venv/bin/activateYou can use the Makefile for standardized setup:
make setupOr manually:
pip install -r requirements.txt
pip install -r requirements-dev.txtmake testOr manually:
pytest -m "not ai"This runs all tests except AI-dependent tests (which require API keys). If this passes, your environment matches the expected behavior.
make runOr manually:
streamlit run app.pyYour browser should open at http://localhost:8501 (or Streamlit will show
you the exact URL). This is the NorthPeak Retail app.
What to try in the app:
- Change the History window (days) to see how much raw data you replay.
- Change the Attribution window (days) to see how the metric definition changes conversions.
- Turn on Inject demo anomalies to introduce a small amount of bad data.
- (If enabled) click Run AI cleaner + judge to see an AI plan and verdict.
The repository follows production-ready best practices:
markettech-demo/
├── src/ # Core application logic (isolates namespace)
│ ├── __init__.py
│ ├── config.py # Environment variable configuration
│ ├── logger.py # Centralized logging setup
│ ├── ai_cleaning_agent.py # OpenAI-based cleaning agent
│ ├── gemma_cleaning_agent.py # Local Gemma LLM cleaning agent
│ └── markettech_workshop.py # Core workshop logic
├── app.py # Streamlit application entry point
├── test_engine.py # Test suite
├── requirements.txt # Production dependencies (pinned versions)
├── requirements-dev.txt # Development dependencies
├── Makefile # Standard commands (setup, test, run, clean)
├── pytest.ini # Test configuration with AI markers
├── .env.example # Environment variable template
├── Dockerfile # Container configuration
├── .github/workflows/ # CI/CD pipelines
│ ├── ci.yml # Test and deploy workflow
│ └── docker.yml # Docker build validation
└── main.tf, variables.tf # Terraform infrastructure
- src/ layout: Core logic is isolated in the
src/package to prevent import collisions and enforce modular design (production standard, not "educational" flat structure) - Type hints: All functions have complete type annotations for maintainability
- Logging: Structured logging instead of print statements for production observability
- Config management: Environment variables loaded via
src/config.py(no hardcoded values) - Test markers: Tests marked with
@pytest.mark.aifor AI/LLM tests that can be excluded in CI (make testexcludes them,make test-allincludes them)
There are two ways to run the AI cleaning loop:
- OpenAI-hosted models (default, requires an OpenAI API key)
- Local Gemma llamafile from Mozilla AI (runs fully in this Codespace)
If you want to explore the OpenAI-based AI cleaner, you need an OpenAI API key. Never commit this key to Git or share it in screenshots.
-
Set your key in the terminal:
export OPENAI_API_KEY="YOUR_REAL_KEY_HERE"
On Windows PowerShell:
$env:OPENAI_API_KEY = "YOUR_REAL_KEY_HERE"
-
Use the AI cleaner:
- In the notebook (
markettech_workshop.py/.ipynb), find the AI cleaning phase and run those cells. - In the Streamlit app, turn on Run AI cleaner + judge in the sidebar.
- In the notebook (
Behind the scenes:
- The planner model suggests a small set of allowed operations (e.g. “drop rows with negative revenue”).
- Your Python code applies those steps deterministically using DuckDB.
- The judge model checks before/after quality checks and decides whether the cleaning is acceptable.
If OPENAI_API_KEY is not set (and no local Gemma server is configured),
the app will simply skip the AI part and explain that AI is optional.
You can also run the AI cleaning loop entirely locally using the
google_gemma-3-4b-it-Q6_K.llamafile from Mozilla AI.
-
Download the Gemma llamafile (once):
chmod +x download_gemma_llamafile.sh ./download_gemma_llamafile.sh
This fetches the llamafile into the repo and marks it executable.
-
Start the local Gemma HTTP server in a terminal:
chmod +x run_gemma_llamafile.sh ./run_gemma_llamafile.sh
By default this starts an OpenAI-compatible server at
http://127.0.0.1:8080/v1. You can make that explicit for the Python code via:export GEMMA_API_BASE=http://127.0.0.1:8080/v1 -
Use the Gemma-based cleaning agent:
-
In the notebook / script, the helper
_run_ai_demoinmarkettech_workshop.pywill automatically prefer the Gemma-based agent whenGEMMA_API_BASEis set. -
Programmatically, you can call the local agent directly:
import datetime as dt from markettech_workshop import generate_stream, inject_corruption from gemma_cleaning_agent import run_agentic_cleaning_loop_gemma df_sess, df_conv, df_chan = generate_stream(days=14, start_date=dt.date(2025, 9, 1)) df_sess_bad, df_conv_bad = inject_corruption(df_sess, df_conv) result = run_agentic_cleaning_loop_gemma( sessions=df_sess_bad, conversions=df_conv_bad, channels=df_chan, max_iters=2, ) print(result["plan"]) print(result["judge"])
-
The gemma_cleaning_agent.py module mirrors the behavior of
ai_cleaning_agent.py but talks to the local llamafile via an
OpenAI-style /v1/chat/completions HTTP endpoint instead of the
hosted OpenAI API.
Running the Gemma 4B llamafile entirely inside a GitHub Codespace (or on your own machine) means:
- No event data leaves your environment for AI planning/judging. You can experiment with agentic data cleaning without sending payloads to a third party API.
- You practice thinking about data residency and governance: which workloads are safe to push to external services, and which should stay close to the data.
- In classroom or corporate settings where external AI services are restricted, you can still use this workshop with a fully local model.
For teaching purposes, you can frame the choice of backend (OpenAI vs local Gemma) as part of the architectural trade‑offs students should learn to reason about: latency and model quality vs. control and privacy.
You do not need this section to learn analytics concepts. This is for people setting up the hosted version of the app.
- Terraform files (
main.tf,variables.tf,versions.tf) describe:- enabling required Google Cloud APIs
- an Artifact Registry repository
- a Cloud Run service.
- GitHub Actions workflow
.github/workflows/ci.yml:- runs tests on every push / PR
- on
main, builds the Docker image with Cloud Build - deploys to Cloud Run using Workload Identity Federation.
- The OpenAI key is stored only in Google Secret Manager as
openai-api-key. Cloud Run reads it viaOPENAI_API_KEYusing--update-secrets. The key never appears in GitHub logs.
- Create the Artifact Registry repo via Cloud Shell
# Make sure you are on the right project
gcloud config set project studio-1697788595-a34f5
# Create a Docker Artifact Registry repo named "markettech" in us-central1
gcloud artifacts repositories create markettech \
--repository-format=docker \
--location=us-central1 \
--description="Docker repo for MarketTech demo"-
In Google Cloud
- Create or choose a project (e.g.
studio-1697788595-a34f5). - Enable Artifact Registry, Cloud Run, and Cloud Build.
- Create a Docker repo (e.g.
markettechinus-central1). - Create a Secret Manager secret
openai-api-keyand add your key as version 1. - Grant the Cloud Run service account
roles/secretmanager.secretAccessor.
- Create or choose a project (e.g.
-
Set up Workload Identity Federation (once)
- Create a Workload Identity Pool and OIDC provider for GitHub Actions.
- Allow that pool to impersonate a deployer service account
(e.g.
github-actions-deployer@...).
-
In GitHub repo settings
- Add repository secrets/variables for:
GCP_PROJECT_ID,GCP_REGION,CLOUD_RUN_SERVICE,ARTIFACT_REPOGCP_SERVICE_ACCOUNT,WORKLOAD_IDENTITY_PROVIDER.
- Add repository secrets/variables for:
-
Let Actions do the rest
- On push to
main,.github/workflows/ci.ymlwill:- authenticate to GCP via OIDC (no long-lived keys)
- run tests
- build and push the image
- deploy to Cloud Run with
OPENAI_API_KEYwired from Secret Manager.
- On push to
If you prefer a one-off manual deploy instead of CI/CD, you can still use
deploy_cloud_run.sh, cleanup_cloud_run.sh, and set_openai_key_cloud_run.sh
from Cloud Shell, but the recommended path is the GitHub Actions pipeline.
When you are done with the workshop environment, you can clean up GCP resources in two ways:
The repository includes a cleanup script that uses Terraform to properly tear down all managed resources:
# Set your GCP project ID
export PROJECT_ID="your-project-id"
# Optional: Override defaults if you used custom values
export REGION="us-central1"
export REPO="markettech"
export SERVICE="markettech-truth-engine"
# Run the cleanup script
./cleanup_cloud_run.shThe script will:
- Initialize Terraform
- Destroy all Terraform-managed resources (Cloud Run service, Artifact Registry)
- Provide optional commands for further cleanup (billing, project deletion)
Testing the cleanup script:
Before running cleanup on a real project, you can verify the script is properly configured:
# Run all infrastructure script tests
./test_infrastructure_scripts.sh
# Or test cleanup script only
./test_cleanup_script.shYou can also clean up resources manually from Cloud Shell:
# Delete the Cloud Run service (replace with your actual service name if different)
gcloud run services delete markettech-truth-engine \
--region=us-central1 \
--quiet
# Delete the Artifact Registry repo used by this demo (irreversible)
gcloud artifacts repositories delete markettech \
--location=us-central1 \
--quiet
# (Optional) Delete the OpenAI API key from Secret Manager
gcloud secrets delete openai-api-key --quietAfter you finish reviewing this demo, it is worth stepping back and noticing what you have actually done:
- You started from raw event data and used SQL to define a clear, repeatable metric "contract" (KPIs as code).
- You added simple but powerful quality checks so that silent data problems do not turn into silent business problems.
- You wrapped that logic in a real app (Streamlit) that non-technical stakeholders can use.
- You saw that the same app can run locally on your laptop or behind a Cloud Run URL.
In production teams, people layer on more engineering practices (CI/CD, RBAC, multiple databases, etc.), but the core ideas do not change:
- metrics are defined in code,
- "contracts" (whatever you negotiated the KPIs are) and checks protect true representations of business states,
- and products are just user-friendly ways to surface that logic.
The point is not to turn every analyst into a platform engineer. The point is to show that you can participate in building robust analytics systems, not just consume dashboards, and that the tools you already know (SQL, basic Python) scale surprisingly far.