Skip to content

Fully manual example: setup to predictions

This walkthrough generates a repository, implements its three supervised methods by hand, creates synthetic inputs, and runs preprocessing → training → prediction locally. No coding agent, MEDS-DEV installation, clinical dataset, GPU or cluster is needed. All commands appear in execution order below.

The example is a constant-prevalence baseline, not a clinical algorithm reproduction. It learns one smoothed probability from the training labels and emits it for every requested held-out key. It copies events during preprocessing but does not use event features to predict. Additional predicates are included to demonstrate composition and provenance, not to claim feature-dependent behavior.

Setup and expected result

Use Bash on Linux/macOS or inside WSL on Windows, with Git and uv installed and network access for Python/packages. Run the commands in a new workspace; the script refuses an existing directory. uv python install 3.11 installs the tutorial's Python, and uv sync --extra test installs the generated package, core, Polars and the conformance dependencies into its .venv. No global Python environment is modified. Copier and the template revision are pinned so the tutorial can be reproduced.

Run the numbered sections below in order, in the same Bash session. Alternatively, expand All commands at the end to copy the complete script, or download the same script and run bash manual-supervised.sh /absolute/path/to/a/new/tutorial-directory.

The generated package is manual_baseline, its class is ManualBaseline, and its named command is manual-baseline. meds-algorithm is an equivalent entry point. Each lifecycle command starts a fresh process, using the same config_file. There are three training outcomes (true, true, false) and a smoothing value of 1, so the resulting probability is (2 + 1) / (3 + 2) = 0.6 for both held-out rows.


1. Create the workspace and generate the repository

Choose a new directory and render the supervised profile. These commands create manual-baseline/ and change into it; all subsequent paths are relative to that generated repository.

# A complete local tutorial. Prerequisites: Bash, Git, uv, and network access for dependencies.
set -euo pipefail
command -v git >/dev/null
command -v uv >/dev/null

# Use a NEW directory. Nothing is deleted or overwritten outside this tutorial.
WORKSPACE="${1:-$PWD/manual-meds-example}"
if [[ -e "$WORKSPACE" ]]; then
  echo "Choose a new directory: $WORKSPACE already exists" >&2
  exit 1
fi
mkdir -p "$WORKSPACE"
cd "$WORKSPACE"
WORKSPACE="$PWD"

# 1. Render a named, single-profile repository from a reproducible template revision.
cat > answers.yaml <<'YAML'
algorithm_name: Manual baseline
algorithm_slug: manual_baseline
algorithm_class: ManualBaseline
algorithm_description: Synthetic tutorial estimating training-label prevalence.
profiles: [supervised]
uses_predicates: true
implementation_source: This tutorial; not a reproduction of a published clinical algorithm.
YAML
uvx --from 'copier==9.17.1' copier copy --defaults --trust \
  --vcs-ref=55ddd290d20a39dd19954321580c616cc11a74ae \
  --data-file answers.yaml \
  https://github.com/McDermottHealthAI/MEDS_algorithm_template.git manual-baseline
cd manual-baseline

2. Install the environment

Install Python and the generated package, then inspect its profiles, commands and installed contract.

# 2. Create the Python environment and install the generated package and test dependencies.
uv python install 3.11
uv sync --python 3.11 --extra test
uv run manual-baseline profiles
uv run manual-baseline commands --profile supervised
uv run meds-algorithm-spec > installed-contract.txt

3. Implement the algorithm

The code-writing commands below intentionally replace the generated tutorial stubs. The template alone does not contain an implemented algorithm. Leave its completion gate in place: this short tutorial does not implement tests/e2e.py, the full implementation report, or the MEDS-DEV/demo acceptance tiers. The final pytest command runs only the generated contract and operations checks; its success does not mean a production port has passed the complete workflow.

Typed configuration

Define the smoothing parameter and validate it when configuration is loaded.

cat > src/manual_baseline/config.py <<'PY'
from dataclasses import dataclass
from meds_algorithm import AlgorithmConfig


@dataclass(frozen=True)
class ManualBaselineConfig(AlgorithmConfig):
    smoothing: float = 1.0

    def validate(self):
        if self.smoothing <= 0:
            raise ValueError("smoothing must be positive")
PY

Algorithm class

Implement preprocessing, supervised training and prediction. The learned probability is saved to disk so prediction can run in a separate process.

cat > src/manual_baseline/manual_baseline.py <<'PY'
import json
from pathlib import Path

import polars as pl
import yaml

from meds_algorithm import AlgorithmRepository, PayloadMetadata, SupervisedAlgorithm
from meds_algorithm.requests import (
    PreprocessDataRequest, TrainSupervisedRequest, PredictSupervisedRequest,
)
from .config import ManualBaselineConfig


class ManualBaseline(SupervisedAlgorithm):
    name = "manual_baseline"
    config_type = ManualBaselineConfig

    def preprocess_data(self, request: PreprocessDataRequest) -> PayloadMetadata:
        events = pl.read_parquet(str(request.meds_dir / "data" / "*.parquet"))
        events.write_parquet(request.output.directory / "events.parquet")
        # Core supplies the composed feature predicates; keep its reserved file unchanged.
        predicates = yaml.safe_load(request.predicates_file.read_text())["predicates"]
        (request.output.directory / "summary.json").write_text(json.dumps({
            "events": events.height, "predicate_names": sorted(predicates),
        }))
        return PayloadMetadata("tutorial-events")

    def train_supervised(self, request: TrainSupervisedRequest) -> PayloadMetadata:
        outcomes = request.labels.read("train")["boolean_value"]
        if len(outcomes) == 0:
            raise ValueError("Training labels must not be empty")
        alpha = self.config.smoothing
        probability = (outcomes.sum() + alpha) / (len(outcomes) + 2 * alpha)
        (request.output.directory / "probability.json").write_text(json.dumps(probability))
        return PayloadMetadata("tutorial-prevalence")

    def predict_supervised(self, request: PredictSupervisedRequest) -> PayloadMetadata:
        probability = json.loads((request.fitted_algorithm.directory / "probability.json").read_text())
        request.index.read().with_columns(
            pl.lit(probability).alias("predicted_boolean_probability")
        ).write_parquet(request.output.directory / "predictions.parquet")
        return PayloadMetadata("tutorial-predictions")


IMPLEMENTATIONS = {"supervised": ManualBaseline}
PROFILES = tuple(IMPLEMENTATIONS)
# Keep the completion gate: this tutorial does not implement the full conformance adapters/report.
STUB_PROFILES = set(PROFILES)


def create_repository():
    return AlgorithmRepository(IMPLEMENTATIONS, repo=Path(__file__).resolve().parents[2])
PY

4. Prepare synthetic inputs and dataset settings

Create the event table, separate training and held-out label/index files, base and additional predicates, and the dataset configuration that refers to those additions.

# 4. Create tiny synthetic MEDS events and an upstream label/index bundle.
# Real work should reuse a reviewed task bundle, not derive labels inside the algorithm.
mkdir -p inputs/meds/data inputs/labels dataset_configs predicates
uv run python - <<'PY'
from datetime import datetime
import polars as pl

pl.DataFrame({
    "subject_id": [1, 2, 3, 4, 5],
    "time": [datetime(2020, 1, 1)] * 5,
    "code": ["DEMO//encounter"] * 5,
    "numeric_value": [None] * 5,
}, schema_overrides={"numeric_value": pl.Float64}).write_parquet("inputs/meds/data/events.parquet")
for split, ids, outcomes in (
    ("train", [1, 2, 3], [True, True, False]),
    ("held_out", [4, 5], [True, False]),
):
    pl.DataFrame({
        "subject_id": ids,
        "prediction_time": [datetime(2020, 1, 2)] * len(ids),
        "boolean_value": outcomes,
    }).write_parquet(f"inputs/labels/{split}.parquet")
PY
cat > predicates/base.yaml <<'YAML'
predicates:
  encounter:
    code: DEMO//encounter
YAML
cat > predicates/demo_additional.yaml <<'YAML'
predicates:
  demo_measurement:
    code: DEMO//measurement
YAML
cat > dataset_configs/demo.yaml <<'YAML'
smoothing: 1.0
additional_predicates_file: ../predicates/demo_additional.yaml
YAML

5. Run each lifecycle step

Set the shared paths once. Each command below then starts its own process.

# 5. Run the complete supervised lifecycle, one independent process per step.
CONFIG="$PWD/dataset_configs/demo.yaml"
MEDS="$PWD/inputs/meds"
LABELS="$PWD/inputs/labels"
BASE_PREDICATES="$PWD/predicates/base.yaml"
ARTIFACTS="$PWD/runs/manual/supervised/artifacts"

Preprocess the data

Compose the feature predicates and write the prepared data artifact.

uv run manual-baseline preprocess_data --profile supervised \
  "config_file=$CONFIG" "external_meds_dir=$MEDS" \
  "external_predicates_file=$BASE_PREDICATES" "output_data_dir=$ARTIFACTS/data"

Train the algorithm

Estimate the smoothed probability from the training outcomes and persist it in the algorithm artifact.

uv run manual-baseline train_supervised --profile supervised \
  "config_file=$CONFIG" "input_data_dir=$ARTIFACTS/data" \
  "external_labels_dir=$LABELS" "output_supervised_algorithm_dir=$ARTIFACTS/algorithm"

Predict on the held-out index

Load the saved probability and emit one prediction per requested key. The runner attaches evaluation labels after scoring.

uv run manual-baseline predict_supervised --profile supervised \
  "config_file=$CONFIG" "input_data_dir=$ARTIFACTS/data" \
  "input_supervised_algorithm_dir=$ARTIFACTS/algorithm" "external_index_dir=$LABELS" \
  'splits=[held_out]' "output_predictions_dir=$ARTIFACTS/predictions" \
  "external_evaluation_labels_dir=$LABELS"

6. Inspect and verify the outputs

Check the two expected probabilities, the prediction schema, artifact metadata and composed predicates. Then run the generated contract and operations checks.

# 6. Inspect and verify the outputs. Only the runner attaches the held-out truth after scoring.
uv run python - <<'PY'
from pathlib import Path
import polars as pl
import yaml
from meds_algorithm import read_manifest
from meds_algorithm.conformance.assertions import assert_prediction_schema
from manual_baseline import ManualBaseline, ManualBaselineConfig

root = Path("runs/manual/supervised/artifacts")
predictions = pl.read_parquet(root / "predictions/predictions.parquet")
assert predictions["subject_id"].to_list() == [4, 5]
assert predictions["predicted_boolean_probability"].to_list() == [0.6, 0.6]
assert predictions["boolean_value"].to_list() == [True, False]
assert_prediction_schema(predictions)
print(predictions)
for stage in ("data", "algorithm", "predictions"):
    manifest = read_manifest(root / stage)
    assert (root / stage / "config.json").is_file()
    assert (root / stage / "request.json").is_file()
    print(stage, manifest["command"]["name"], manifest["resources"]["wall_seconds"])
resolved = yaml.safe_load((root / "data/resolved_feature_predicates.yaml").read_text())
assert set(resolved["predicates"]) == {"encounter", "demo_measurement"}
config = ManualBaselineConfig.load(root / "algorithm/config.json")
assert ManualBaseline(config).name == "manual_baseline"
PY
uv run pytest -q tests/test_contract.py tests/test_operations.py
printf 'Tutorial completed in %s\n' "$PWD"

What was produced

Under manual-meds-example/manual-baseline/ (or your chosen workspace):

Path Contents
src/manual_baseline/manual_baseline.py Concrete algorithm methods and profile association
src/manual_baseline/config.py Named, typed configuration with positive smoothing validation
dataset_configs/demo.yaml Dataset-specific settings and relative additional-predicates path
inputs/meds/data/, inputs/labels/ Synthetic events and the separate label/index bundle
runs/manual/supervised/artifacts/data/ Prepared events, summary, resolved feature predicates and core metadata
runs/manual/supervised/artifacts/algorithm/ Persisted learned probability and core metadata
runs/manual/supervised/artifacts/predictions/ Held-out probabilities, post-score truth and core metadata

Each artifact has manifest.yaml, config.json and request.json. The manifest records measurements, input identities and the configuration. Because this example does not create a Git commit for the generated implementation, its commit provenance may be unavailable; commit reviewed implementation code before reproducible production runs. Keep input data and artifact payloads out of source control.

The index and evaluation labels happen to share a directory in this example. Prediction reads only keys through request.index.read(); the runner attaches boolean_value afterward. In real work, reuse the upstream reviewed task bundle, respect all temporal cutoffs, and do not create labels inside the algorithm. Additional feature predicates must not redefine the evaluated task.

To rerun, choose fresh output paths. Explicit do_overwrite=true opts into replacing an artifact with rollback; do not add it automatically to every command. Other profiles have different intermediate steps: use the profile lifecycle table and per-step arguments when adapting this example.


All commands

This is the complete script used by the sections above. Run it from a fresh workspace instead of running it again in the directory you just created.

All commands — complete runnable script
#!/usr/bin/env bash
# A complete local tutorial. Prerequisites: Bash, Git, uv, and network access for dependencies.
set -euo pipefail
command -v git >/dev/null
command -v uv >/dev/null

# Use a NEW directory. Nothing is deleted or overwritten outside this tutorial.
WORKSPACE="${1:-$PWD/manual-meds-example}"
if [[ -e "$WORKSPACE" ]]; then
  echo "Choose a new directory: $WORKSPACE already exists" >&2
  exit 1
fi
mkdir -p "$WORKSPACE"
cd "$WORKSPACE"
WORKSPACE="$PWD"

# 1. Render a named, single-profile repository from a reproducible template revision.
cat > answers.yaml <<'YAML'
algorithm_name: Manual baseline
algorithm_slug: manual_baseline
algorithm_class: ManualBaseline
algorithm_description: Synthetic tutorial estimating training-label prevalence.
profiles: [supervised]
uses_predicates: true
implementation_source: This tutorial; not a reproduction of a published clinical algorithm.
YAML
uvx --from 'copier==9.17.1' copier copy --defaults --trust \
  --vcs-ref=55ddd290d20a39dd19954321580c616cc11a74ae \
  --data-file answers.yaml \
  https://github.com/McDermottHealthAI/MEDS_algorithm_template.git manual-baseline
cd manual-baseline

# 2. Create the Python environment and install the generated package and test dependencies.
uv python install 3.11
uv sync --python 3.11 --extra test
uv run manual-baseline profiles
uv run manual-baseline commands --profile supervised
uv run meds-algorithm-spec > installed-contract.txt

# 3. Implement the generated stubs manually. No agent or MEDS-DEV runner is involved.
cat > src/manual_baseline/config.py <<'PY'
from dataclasses import dataclass
from meds_algorithm import AlgorithmConfig


@dataclass(frozen=True)
class ManualBaselineConfig(AlgorithmConfig):
    smoothing: float = 1.0

    def validate(self):
        if self.smoothing <= 0:
            raise ValueError("smoothing must be positive")
PY

cat > src/manual_baseline/manual_baseline.py <<'PY'
import json
from pathlib import Path

import polars as pl
import yaml

from meds_algorithm import AlgorithmRepository, PayloadMetadata, SupervisedAlgorithm
from meds_algorithm.requests import (
    PreprocessDataRequest, TrainSupervisedRequest, PredictSupervisedRequest,
)
from .config import ManualBaselineConfig


class ManualBaseline(SupervisedAlgorithm):
    name = "manual_baseline"
    config_type = ManualBaselineConfig

    def preprocess_data(self, request: PreprocessDataRequest) -> PayloadMetadata:
        events = pl.read_parquet(str(request.meds_dir / "data" / "*.parquet"))
        events.write_parquet(request.output.directory / "events.parquet")
        # Core supplies the composed feature predicates; keep its reserved file unchanged.
        predicates = yaml.safe_load(request.predicates_file.read_text())["predicates"]
        (request.output.directory / "summary.json").write_text(json.dumps({
            "events": events.height, "predicate_names": sorted(predicates),
        }))
        return PayloadMetadata("tutorial-events")

    def train_supervised(self, request: TrainSupervisedRequest) -> PayloadMetadata:
        outcomes = request.labels.read("train")["boolean_value"]
        if len(outcomes) == 0:
            raise ValueError("Training labels must not be empty")
        alpha = self.config.smoothing
        probability = (outcomes.sum() + alpha) / (len(outcomes) + 2 * alpha)
        (request.output.directory / "probability.json").write_text(json.dumps(probability))
        return PayloadMetadata("tutorial-prevalence")

    def predict_supervised(self, request: PredictSupervisedRequest) -> PayloadMetadata:
        probability = json.loads((request.fitted_algorithm.directory / "probability.json").read_text())
        request.index.read().with_columns(
            pl.lit(probability).alias("predicted_boolean_probability")
        ).write_parquet(request.output.directory / "predictions.parquet")
        return PayloadMetadata("tutorial-predictions")


IMPLEMENTATIONS = {"supervised": ManualBaseline}
PROFILES = tuple(IMPLEMENTATIONS)
# Keep the completion gate: this tutorial does not implement the full conformance adapters/report.
STUB_PROFILES = set(PROFILES)


def create_repository():
    return AlgorithmRepository(IMPLEMENTATIONS, repo=Path(__file__).resolve().parents[2])
PY

# 4. Create tiny synthetic MEDS events and an upstream label/index bundle.
# Real work should reuse a reviewed task bundle, not derive labels inside the algorithm.
mkdir -p inputs/meds/data inputs/labels dataset_configs predicates
uv run python - <<'PY'
from datetime import datetime
import polars as pl

pl.DataFrame({
    "subject_id": [1, 2, 3, 4, 5],
    "time": [datetime(2020, 1, 1)] * 5,
    "code": ["DEMO//encounter"] * 5,
    "numeric_value": [None] * 5,
}, schema_overrides={"numeric_value": pl.Float64}).write_parquet("inputs/meds/data/events.parquet")
for split, ids, outcomes in (
    ("train", [1, 2, 3], [True, True, False]),
    ("held_out", [4, 5], [True, False]),
):
    pl.DataFrame({
        "subject_id": ids,
        "prediction_time": [datetime(2020, 1, 2)] * len(ids),
        "boolean_value": outcomes,
    }).write_parquet(f"inputs/labels/{split}.parquet")
PY
cat > predicates/base.yaml <<'YAML'
predicates:
  encounter:
    code: DEMO//encounter
YAML
cat > predicates/demo_additional.yaml <<'YAML'
predicates:
  demo_measurement:
    code: DEMO//measurement
YAML
cat > dataset_configs/demo.yaml <<'YAML'
smoothing: 1.0
additional_predicates_file: ../predicates/demo_additional.yaml
YAML

# 5. Run the complete supervised lifecycle, one independent process per step.
CONFIG="$PWD/dataset_configs/demo.yaml"
MEDS="$PWD/inputs/meds"
LABELS="$PWD/inputs/labels"
BASE_PREDICATES="$PWD/predicates/base.yaml"
ARTIFACTS="$PWD/runs/manual/supervised/artifacts"

uv run manual-baseline preprocess_data --profile supervised \
  "config_file=$CONFIG" "external_meds_dir=$MEDS" \
  "external_predicates_file=$BASE_PREDICATES" "output_data_dir=$ARTIFACTS/data"

uv run manual-baseline train_supervised --profile supervised \
  "config_file=$CONFIG" "input_data_dir=$ARTIFACTS/data" \
  "external_labels_dir=$LABELS" "output_supervised_algorithm_dir=$ARTIFACTS/algorithm"

uv run manual-baseline predict_supervised --profile supervised \
  "config_file=$CONFIG" "input_data_dir=$ARTIFACTS/data" \
  "input_supervised_algorithm_dir=$ARTIFACTS/algorithm" "external_index_dir=$LABELS" \
  'splits=[held_out]' "output_predictions_dir=$ARTIFACTS/predictions" \
  "external_evaluation_labels_dir=$LABELS"

# 6. Inspect and verify the outputs. Only the runner attaches the held-out truth after scoring.
uv run python - <<'PY'
from pathlib import Path
import polars as pl
import yaml
from meds_algorithm import read_manifest
from meds_algorithm.conformance.assertions import assert_prediction_schema
from manual_baseline import ManualBaseline, ManualBaselineConfig

root = Path("runs/manual/supervised/artifacts")
predictions = pl.read_parquet(root / "predictions/predictions.parquet")
assert predictions["subject_id"].to_list() == [4, 5]
assert predictions["predicted_boolean_probability"].to_list() == [0.6, 0.6]
assert predictions["boolean_value"].to_list() == [True, False]
assert_prediction_schema(predictions)
print(predictions)
for stage in ("data", "algorithm", "predictions"):
    manifest = read_manifest(root / stage)
    assert (root / stage / "config.json").is_file()
    assert (root / stage / "request.json").is_file()
    print(stage, manifest["command"]["name"], manifest["resources"]["wall_seconds"])
resolved = yaml.safe_load((root / "data/resolved_feature_predicates.yaml").read_text())
assert set(resolved["predicates"]) == {"encounter", "demo_measurement"}
config = ManualBaselineConfig.load(root / "algorithm/config.json")
assert ManualBaseline(config).name == "manual_baseline"
PY
uv run pytest -q tests/test_contract.py tests/test_operations.py
printf 'Tutorial completed in %s\n' "$PWD"