{ "cells": [ { "cell_type": "markdown", "id": "score_search-0", "metadata": {}, "source": "# Score-based search (XGES / hill climbing / Tabu search / exact search / GIES)\n\n[](https://colab.research.google.com/github/Abel-ai-lab/andrey/blob/main/examples/score_search.ipynb) {download}`Download this notebook <../../../examples/score_search.ipynb>`\n\nXGES, hill climbing, and Tabu search look for the graph with the best BIC score. XGES searches\nover equivalence classes, as GES does, with deletions first. Hill climbing adds, removes, or\nreverses one edge of a DAG at a time and stops when no move improves the score; Tabu search keeps\ngoing past that point and returns the best DAG it visits. All three return a CPDAG and suit\nlinear-Gaussian data. Exact search finds the best-scoring DAG on a few variables, and GIES also\nlearns from interventions." }, { "cell_type": "markdown", "id": "score_search-1", "metadata": {}, "source": [ "## Start\n", "\n", "Choose a CPU runtime and run this cell first. In Colab it installs `andrey-core==0.9.0` from PyPI;\n", "local kernels use the installed environment." ] }, { "cell_type": "code", "execution_count": 1, "id": "score_search-2", "metadata": { "tags": [ "colab-setup" ] }, "outputs": [], "source": [ "import os\n", "import sys\n", "\n", "IN_COLAB = \"google.colab\" in sys.modules\n", "\n", "if IN_COLAB:\n", " os.environ[\"ANDREY_DEVICE\"] = \"cpu\"\n", " !pip install --quiet \"andrey-core[viz]==0.9.0\" pandas" ] }, { "cell_type": "code", "execution_count": 2, "id": "score_search-3", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import pandas as pd\n", "from IPython.display import display\n", "\n", "import andrey\n", "import andrey.metrics as metrics\n", "import andrey.viz\n", "from andrey.data import benchmarks" ] }, { "cell_type": "markdown", "id": "score_search-4", "metadata": {}, "source": [ "## Data\n", "\n", "`linear_gauss_er` draws an Erdos-Renyi graph with linear mechanisms and Gaussian noise: here 10\n", "variables and 2,000 samples." ] }, { "cell_type": "code", "execution_count": 3, "id": "score_search-5", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "2000 observations, 10 variables, seed 7\n" ] }, { "data": { "text/html": [ "" ], "text/plain": [ "''" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "seed = 7\n", "dataset = benchmarks.scm(\"linear_gauss_er\", d=10).sample(n=2000, seed=seed, scale=\"standardize\")\n", "X, truth = dataset.data, dataset.observed_graph\n", "print(f\"{X.shape[0]} observations, {X.shape[1]} variables, seed {seed}\")\n", "display(andrey.viz.draw(truth))" ] }, { "cell_type": "markdown", "id": "score_search-6", "metadata": {}, "source": [ "## Fit XGES, hill climbing, and Tabu search\n", "\n", "`metadata[\"score\"]` is the BIC score of the returned graph, which each search minimizes. A larger\n", "`lambda_value` weighs the BIC penalty more and gives a sparser graph. With\n", "`extended_search=False`, XGES skips its extended search for a faster fit, and `tabu_length` sets\n", "how many recently visited DAGs Tabu search may not return to." ] }, { "cell_type": "code", "execution_count": 4, "id": "score_search-7", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "XGES: cpdag, score -22406.2\n" ] }, { "data": { "text/html": [ "" ], "text/plain": [ "''" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "Hill climbing: cpdag, score -22356.9\n" ] }, { "data": { "text/html": [ "" ], "text/plain": [ "''" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "Tabu search: cpdag, score -22368.4\n" ] }, { "data": { "text/html": [ "" ], "text/plain": [ "''" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "results = {\n", " \"XGES\": andrey.xges(X),\n", " \"Hill climbing\": andrey.hc(X),\n", " \"Tabu search\": andrey.tabu(X),\n", "}\n", "for name, result in results.items():\n", " print(f\"{name}: {result.structure.kind}, score {result.metadata['score']:.1f}\")\n", " display(andrey.viz.draw(result.structure))" ] }, { "cell_type": "markdown", "id": "score_search-8", "metadata": {}, "source": [ "## Compare with GES over ten datasets\n", "\n", "Structural Hamming distance (SHD) counts errors against the truth's CPDAG; lower is better. The\n", "empty graph's SHD is the number of true edges, so an estimate at or above it has not improved on\n", "returning no edges. One dataset can favor any method, so the table covers ten seeds and their\n", "median." ] }, { "cell_type": "code", "execution_count": 5, "id": "score_search-9", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
| seed | \n", "0 | \n", "1 | \n", "2 | \n", "3 | \n", "4 | \n", "5 | \n", "6 | \n", "7 | \n", "8 | \n", "9 | \n", "median | \n", "
|---|---|---|---|---|---|---|---|---|---|---|---|
| Empty graph | \n", "15 | \n", "15 | \n", "15 | \n", "15 | \n", "15 | \n", "15 | \n", "15 | \n", "15 | \n", "15 | \n", "15 | \n", "15.0 | \n", "
| GES | \n", "21 | \n", "10 | \n", "0 | \n", "10 | \n", "17 | \n", "7 | \n", "11 | \n", "17 | \n", "0 | \n", "0 | \n", "10.0 | \n", "
| XGES | \n", "0 | \n", "8 | \n", "0 | \n", "9 | \n", "1 | \n", "0 | \n", "1 | \n", "6 | \n", "0 | \n", "0 | \n", "0.5 | \n", "
| Hill climbing | \n", "28 | \n", "6 | \n", "0 | \n", "8 | \n", "25 | \n", "16 | \n", "22 | \n", "11 | \n", "4 | \n", "13 | \n", "12.0 | \n", "
| Tabu search | \n", "6 | \n", "0 | \n", "0 | \n", "1 | \n", "20 | \n", "9 | \n", "16 | \n", "12 | \n", "0 | \n", "0 | \n", "3.5 | \n", "