{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "pairwise-0",
   "metadata": {},
   "source": [
    "# Pairwise direction (ANM / PNL)\n",
    "\n",
    "[![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Abel-ai-lab/andrey/blob/main/examples/pairwise.ipynb)\n",
    "\n",
    "ANM and PNL take two variables and test which one causes the other. Each fits a model in both\n",
    "directions and tests whether the estimated noise is independent of the proposed cause, which holds\n",
    "only in the causal direction. ANM fits an additive noise model, `y = f(x) + e`; PNL fits a\n",
    "post-nonlinear one, `y = f2(f1(x) + e)`. Both return a two-node DAG and both directions' p-values."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "pairwise-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": "pairwise-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": "pairwise-3",
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "from IPython.display import display\n",
    "\n",
    "import andrey\n",
    "import andrey.viz\n",
    "from andrey.data import sample_scm"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "pairwise-4",
   "metadata": {},
   "source": [
    "## Data\n",
    "\n",
    "`sample_scm` draws a cause-effect pair: two variables and one edge, in a direction the seed draws.\n",
    "Here the effect is a post-nonlinear function of the cause with Laplace noise, 500 samples."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "pairwise-5",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "500 observations of 2 variables, seed 3\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 360 240\" width=\"100%\" font-family=\"Inter, system-ui, sans-serif\"><rect width=\"360\" height=\"240\" fill=\"#fafbfc\"/><line x1=\"207.00\" y1=\"120.00\" x2=\"143.00\" y2=\"120.00\" stroke=\"#3a4658\" stroke-width=\"2.0\" stroke-linecap=\"round\"/><polygon points=\"217.00,120.00 207.00,115.00 207.00,125.00\" fill=\"#3a4658\" stroke-linejoin=\"round\"/><circle cx=\"230.00\" cy=\"120.00\" r=\"13.00\" fill=\"#ffffff\" stroke=\"#182233\" stroke-width=\"1.8\"/><text x=\"230.00\" y=\"120.00\" text-anchor=\"middle\" font-size=\"11\" font-weight=\"600\" fill=\"#182233\" dominant-baseline=\"central\">0</text><circle cx=\"130.00\" cy=\"120.00\" r=\"13.00\" fill=\"#ffffff\" stroke=\"#182233\" stroke-width=\"1.8\"/><text x=\"130.00\" y=\"120.00\" text-anchor=\"middle\" font-size=\"11\" font-weight=\"600\" fill=\"#182233\" dominant-baseline=\"central\">1</text></svg>"
      ],
      "text/plain": [
       "'<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 360 240\" width=\"100%\" font-family=\"Inter, system-ui, sans-serif\"><rect width=\"360\" height=\"240\" fill=\"#fafbfc\"/><line x1=\"207.00\" y1=\"120.00\" x2=\"143.00\" y2=\"120.00\" stroke=\"#3a4658\" stroke-width=\"2.0\" stroke-linecap=\"round\"/><polygon points=\"217.00,120.00 207.00,115.00 207.00,125.00\" fill=\"#3a4658\" stroke-linejoin=\"round\"/><circle cx=\"230.00\" cy=\"120.00\" r=\"13.00\" fill=\"#ffffff\" stroke=\"#182233\" stroke-width=\"1.8\"/><text x=\"230.00\" y=\"120.00\" text-anchor=\"middle\" font-size=\"11\" font-weight=\"600\" fill=\"#182233\" dominant-baseline=\"central\">0</text><circle cx=\"130.00\" cy=\"120.00\" r=\"13.00\" fill=\"#ffffff\" stroke=\"#182233\" stroke-width=\"1.8\"/><text x=\"130.00\" y=\"120.00\" text-anchor=\"middle\" font-size=\"11\" font-weight=\"600\" fill=\"#182233\" dominant-baseline=\"central\">1</text></svg>'"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "seed = 3\n",
    "pair = sample_scm(\n",
    "    graph=\"erdos_renyi\",\n",
    "    functional=\"post_nonlinear\",\n",
    "    noise=\"laplace\",\n",
    "    d=2,\n",
    "    n=500,\n",
    "    seed=seed,\n",
    "    scale=\"standardize\",\n",
    "    density=1.0,\n",
    ")\n",
    "X, truth = pair.data, pair.graph\n",
    "print(f\"{X.shape[0]} observations of 2 variables, seed {seed}\")\n",
    "display(andrey.viz.draw(truth))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "pairwise-6",
   "metadata": {},
   "source": [
    "## Fit ANM and PNL\n",
    "\n",
    "Column 0 is `x` and column 1 is `y`. `pval_forward` tests `x -> y` and `pval_backward` tests\n",
    "`y -> x`; by default the edge points in the direction with the larger p-value. Pass a significance\n",
    "level such as `alpha=0.05` to draw an edge only when exactly one direction's p-value is above it."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "pairwise-7",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "ANM  dag  |  2 nodes  |  1 edges (1 directed)\n",
      "\n",
      "  1 -> 0\n",
      "\n",
      "  pval_forward: 9.834e-13\n",
      "  pval_backward: 0.0021\n",
      "  stat_forward: 5.487\n",
      "  stat_backward: 2.195\n",
      "\n",
      "PNL  dag  |  2 nodes  |  1 edges (1 directed)\n",
      "\n",
      "  1 -> 0\n",
      "\n",
      "  pval_forward: 0.0001507\n",
      "  pval_backward: 0.01781\n",
      "  stat_forward: 2.71\n",
      "  stat_backward: 1.811\n"
     ]
    }
   ],
   "source": [
    "print(andrey.anm(X), end=\"\\n\\n\")\n",
    "print(andrey.pnl(X))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "pairwise-8",
   "metadata": {},
   "source": [
    "## Compare over ten pairs\n",
    "\n",
    "Each method runs on the pairs its model describes: ANM on additive-noise pairs with Gaussian noise,\n",
    "and PNL on post-nonlinear pairs with Laplace noise. One pair can favor either answer, so the table\n",
    "covers ten seeds."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "pairwise-9",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>ANM</th>\n",
       "      <th>PNL</th>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>seed</th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>true direction</td>\n",
       "      <td>true direction</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>true direction</td>\n",
       "      <td>true direction</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>true direction</td>\n",
       "      <td>true direction</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>true direction</td>\n",
       "      <td>true direction</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>true direction</td>\n",
       "      <td>true direction</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>reversed</td>\n",
       "      <td>true direction</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>true direction</td>\n",
       "      <td>true direction</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>true direction</td>\n",
       "      <td>true direction</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>reversed</td>\n",
       "      <td>true direction</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>true direction</td>\n",
       "      <td>true direction</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                 ANM             PNL\n",
       "seed                                \n",
       "0     true direction  true direction\n",
       "1     true direction  true direction\n",
       "2     true direction  true direction\n",
       "3     true direction  true direction\n",
       "4     true direction  true direction\n",
       "5           reversed  true direction\n",
       "6     true direction  true direction\n",
       "7     true direction  true direction\n",
       "8           reversed  true direction\n",
       "9     true direction  true direction"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "models = {\n",
    "    \"ANM\": (\"additive_noise\", \"gaussian\", lambda x: andrey.anm(x)),\n",
    "    \"PNL\": (\"post_nonlinear\", \"laplace\", lambda x: andrey.pnl(x)),\n",
    "}\n",
    "rows = {}\n",
    "for s in range(10):\n",
    "    rows[s] = {}\n",
    "    for name, (functional, noise, fit) in models.items():\n",
    "        draw = sample_scm(\n",
    "            graph=\"erdos_renyi\",\n",
    "            functional=functional,\n",
    "            noise=noise,\n",
    "            d=2,\n",
    "            n=500,\n",
    "            seed=s,\n",
    "            scale=\"standardize\",\n",
    "            density=1.0,\n",
    "        )\n",
    "        found = fit(draw.data).structure.oriented_edges()\n",
    "        if found == draw.graph.oriented_edges():\n",
    "            rows[s][name] = \"true direction\"\n",
    "        else:\n",
    "            rows[s][name] = \"reversed\" if found else \"no edge\"\n",
    "table = pd.DataFrame(rows).T\n",
    "table.index.name = \"seed\"\n",
    "table"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "pairwise-10",
   "metadata": {},
   "source": [
    "The [0.4.0 release card](https://andrey.abel.ai/docs/benchmarks/0.4.0.html) compares both\n",
    "methods with causal-learn on pairs of several sizes.\n",
    "\n",
    "## Use your own data\n",
    "\n",
    "Pass a two-column table, such as a DataFrame whose column names become the graph's labels, or\n",
    "change `seed` and `n` above and rerun."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
