From b2f5b6300d1fcff74f995ba91971984469ab7ded Mon Sep 17 00:00:00 2001 From: Alexander Fischer Date: Thu, 9 Jan 2025 23:16:21 +0100 Subject: [PATCH 1/6] add jax benchmark notebook --- benchmarks/gpu_pyfixest_errors.ipynb | 1478 ++++++++++++++++++++++++++ 1 file changed, 1478 insertions(+) create mode 100644 benchmarks/gpu_pyfixest_errors.ipynb diff --git a/benchmarks/gpu_pyfixest_errors.ipynb b/benchmarks/gpu_pyfixest_errors.ipynb new file mode 100644 index 000000000..3f6ee1481 --- /dev/null +++ b/benchmarks/gpu_pyfixest_errors.ipynb @@ -0,0 +1,1478 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "execution": { + "iopub.execute_input": "2025-01-09T00:23:28.917621Z", + "iopub.status.busy": "2025-01-09T00:23:28.917332Z", + "iopub.status.idle": "2025-01-09T00:23:29.477701Z", + "shell.execute_reply": "2025-01-09T00:23:29.477193Z", + "shell.execute_reply.started": "2025-01-09T00:23:28.917602Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[CpuDevice(id=0)]" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import jax\n", + "import jax.numpy as jnp\n", + "\n", + "jax.devices()" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "execution": { + "iopub.execute_input": "2025-01-09T00:23:30.540594Z", + "iopub.status.busy": "2025-01-09T00:23:30.540228Z", + "iopub.status.idle": "2025-01-09T00:23:30.739685Z", + "shell.execute_reply": "2025-01-09T00:23:30.739213Z", + "shell.execute_reply.started": "2025-01-09T00:23:30.540574Z" + }, + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{CpuDevice(id=0)}" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "jnp.ones(10).devices()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "execution": { + "iopub.execute_input": "2025-01-09T00:26:29.239253Z", + "iopub.status.busy": "2025-01-09T00:26:29.238947Z", + "iopub.status.idle": "2025-01-09T00:26:29.754752Z", + "shell.execute_reply": "2025-01-09T00:26:29.754158Z", + "shell.execute_reply.started": "2025-01-09T00:26:29.239235Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "'nvidia-smi' is not recognized as an internal or external command,\n", + "operable program or batch file.\n" + ] + } + ], + "source": [ + "!nvidia-smi" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "execution": { + "iopub.execute_input": "2025-01-09T00:23:44.232335Z", + "iopub.status.busy": "2025-01-09T00:23:44.231984Z", + "iopub.status.idle": "2025-01-09T00:23:45.388035Z", + "shell.execute_reply": "2025-01-09T00:23:45.387587Z", + "shell.execute_reply.started": "2025-01-09T00:23:44.232312Z" + }, + "id": "fHzEldNvR2_K" + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
\n", + " \n", + " " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "\n", + "
\n", + " \n", + " " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import time\n", + "from itertools import product\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import pandas as pd\n", + "import seaborn as sns\n", + "from scipy.stats import nbinom\n", + "from tqdm import tqdm\n", + "\n", + "import pyfixest as pf\n", + "from pyfixest.estimation.demean_ import demean\n", + "from pyfixest.estimation.demean_jax_ import demean_jax" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "execution": { + "iopub.execute_input": "2025-01-09T00:23:46.290548Z", + "iopub.status.busy": "2025-01-09T00:23:46.289898Z", + "iopub.status.idle": "2025-01-09T00:23:46.417097Z", + "shell.execute_reply": "2025-01-09T00:23:46.416504Z", + "shell.execute_reply.started": "2025-01-09T00:23:46.290525Z" + }, + "id": "XQjP2889YJxs", + "outputId": "3e686d7b-0774-4bb5-c1b9-28e5b9f286a9" + }, + "outputs": [], + "source": [ + "# %load_ext watermark\n", + "# %watermark --iversions" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "colab": { + "background_save": true + }, + "execution": { + "iopub.execute_input": "2025-01-09T00:23:49.545271Z", + "iopub.status.busy": "2025-01-09T00:23:49.545016Z", + "iopub.status.idle": "2025-01-09T00:23:49.552123Z", + "shell.execute_reply": "2025-01-09T00:23:49.551676Z", + "shell.execute_reply.started": "2025-01-09T00:23:49.545253Z" + }, + "id": "bxMmeyCxR3fb" + }, + "outputs": [], + "source": [ + "def generate_test_data(size: int, k: int = 2):\n", + " \"\"\"\n", + " Generate benchmark data for pyfixest on GPU (similar to the R fixest benchmark data).\n", + "\n", + " Args:\n", + " size (int): The number of observations in the data frame.\n", + " k (int): The number of covariates in the data frame.\n", + "\n", + " Returns\n", + " -------\n", + " pd.DataFrame: The generated data frame for the given size.\n", + " \"\"\"\n", + " # Constants\n", + " all_n = [1000 * 10**i for i in range(5)]\n", + " a = 1\n", + " b = 0.05\n", + "\n", + " n = all_n[size - 1]\n", + "\n", + " dum_all = []\n", + " nb_dum = [n // 20, int(np.sqrt(n)), int(n**0.33)]\n", + "\n", + " dum_all = np.zeros((n, 3))\n", + " dum_all[:, 0] = np.random.choice(nb_dum[0], n, replace=True)\n", + " dum_all[:, 1] = np.random.choice(nb_dum[1], n, replace=True)\n", + " dum_all[:, 2] = np.random.choice(nb_dum[2], n, replace=True)\n", + " dum_all = dum_all.astype(int)\n", + "\n", + " X1 = np.random.normal(size=n)\n", + " X2 = X1**2\n", + "\n", + " mu = a * X1 + b * X2\n", + "\n", + " for m in range(3):\n", + " coef_dum = np.random.normal(size=nb_dum[m])\n", + " mu += coef_dum[dum_all[:, m]]\n", + "\n", + " mu = np.exp(mu)\n", + " y = nbinom.rvs(0.5, 1 - (mu / (mu + 0.5)), size=n)\n", + "\n", + " X_full = np.column_stack((X1, X2))\n", + " base = pd.DataFrame(\n", + " {\n", + " \"y\": y,\n", + " \"ln_y\": np.log(y + 1),\n", + " \"X1\": X1,\n", + " \"X2\": X2,\n", + " }\n", + " )\n", + "\n", + " if k > 2:\n", + " X = np.random.normal(size=(n, k - 2))\n", + " X_df = pd.DataFrame(X, columns=[f\"X{i}\" for i in range(3, k + 1, 1)])\n", + " base = pd.concat([base, X_df], axis=1)\n", + " X_full = np.column_stack((X_full, X))\n", + "\n", + " for m in range(3):\n", + " base[f\"dum_{m + 1}\"] = dum_all[:, m]\n", + "\n", + " weights = np.random.uniform(0, 1, n)\n", + " return base, y, X_full, dum_all, weights" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "execution": { + "iopub.execute_input": "2025-01-09T00:23:50.285297Z", + "iopub.status.busy": "2025-01-09T00:23:50.284967Z", + "iopub.status.idle": "2025-01-09T00:23:50.460957Z", + "shell.execute_reply": "2025-01-09T00:23:50.460501Z", + "shell.execute_reply.started": "2025-01-09T00:23:50.285276Z" + }, + "id": "nzynhbqwR81H" + }, + "outputs": [], + "source": [ + "df, Y, X, f, weights = generate_test_data(1)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "execution": { + "iopub.execute_input": "2025-01-09T00:25:02.873750Z", + "iopub.status.busy": "2025-01-09T00:25:02.873239Z", + "iopub.status.idle": "2025-01-09T00:25:03.153458Z", + "shell.execute_reply": "2025-01-09T00:25:03.153005Z", + "shell.execute_reply.started": "2025-01-09T00:25:02.873732Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "###\n", + "\n", + "Estimation: OLS\n", + "Dep. var.: ln_y, Fixed effects: dum_1\n", + "Inference: CRV1\n", + "Observations: 1000\n", + "\n", + "| Coefficient | Estimate | Std. Error | t value | Pr(>|t|) | 2.5% | 97.5% |\n", + "|:--------------|-----------:|-------------:|----------:|-----------:|-------:|--------:|\n", + "| X1 | 0.436 | 0.046 | 9.440 | 0.000 | 0.343 | 0.529 |\n", + "---\n", + "RMSE: 1.067 R2: 0.242 R2 Within: 0.131 \n" + ] + } + ], + "source": [ + "m0 = pf.feols(\"ln_y ~ X1 | dum_1\", df, demeaner_backend=\"numba\")\n", + "m0.summary()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "execution": { + "iopub.execute_input": "2025-01-09T00:25:03.330646Z", + "iopub.status.busy": "2025-01-09T00:25:03.330367Z", + "iopub.status.idle": "2025-01-09T00:25:03.571916Z", + "shell.execute_reply": "2025-01-09T00:25:03.571482Z", + "shell.execute_reply.started": "2025-01-09T00:25:03.330629Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "###\n", + "\n", + "Estimation: OLS\n", + "Dep. var.: ln_y, Fixed effects: dum_1\n", + "Inference: CRV1\n", + "Observations: 1000\n", + "\n", + "| Coefficient | Estimate | Std. Error | t value | Pr(>|t|) | 2.5% | 97.5% |\n", + "|:--------------|-----------:|-------------:|----------:|-----------:|-------:|--------:|\n", + "| X1 | 0.436 | 0.046 | 9.440 | 0.000 | 0.343 | 0.529 |\n", + "---\n", + "RMSE: 1.067 R2: 0.242 R2 Within: 0.131 \n" + ] + } + ], + "source": [ + "m1 = pf.feols(\"ln_y ~ X1 | dum_1\", df, demeaner_backend=\"jax\")\n", + "m1.summary()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## function" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "execution": { + "iopub.execute_input": "2025-01-09T00:24:06.619552Z", + "iopub.status.busy": "2025-01-09T00:24:06.619273Z", + "iopub.status.idle": "2025-01-09T00:24:06.626298Z", + "shell.execute_reply": "2025-01-09T00:24:06.625727Z", + "shell.execute_reply.started": "2025-01-09T00:24:06.619534Z" + }, + "id": "29rZkULUR_A0" + }, + "outputs": [], + "source": [ + "def run_standard_benchmark(\n", + " fixed_effect,\n", + " demeaner_backend,\n", + " size=1,\n", + " k=1,\n", + " solver=\"np.linalg.lstsq\",\n", + " skip_demean_benchmark=True,\n", + "):\n", + " \"\"\"\n", + " Run the fixest standard benchmark fixed effect models. This is the function the benchmarks\n", + " will loop over.\n", + "\n", + " Args:\n", + " fixed_effect (str): The fixed effect to use. Must be a list of variables as \"dum_1\", \"dum_1+dum_2\", or \"dum_1+dum_2+dum_3\", etc.\n", + " demeaner_backend (str): The backend to use for demeaning. Must be \"numba\" or \"jax\".\n", + " size (int): The size of the data to generate. Must be between 1 and 5. For 1, N = 1000, for 2, N = 10000, etc.\n", + " k_vals (int): The number of covariates to generate.\n", + " solver (str): The solver to use for the estimation. Must be \"np.linalg.lstsq\". \"jax\" currently throws an error.\n", + " skip_demean_benchmark (bool): Whether to skip the \"pure\" demean benchmark. Default is True. Only the full call\n", + " to feols is benchmarked.\n", + "\n", + " \"\"\"\n", + " assert fixed_effect in [\"dum_1\", \"dum_1+dum_2\", \"dum_1+dum_2+dum_3\"]\n", + "\n", + " # one fixed effect\n", + " res = []\n", + "\n", + " fml_base = \"ln_y ~ X1\"\n", + " fml = f\"{fml_base} | {fixed_effect}\"\n", + "\n", + " # warmup\n", + " df, y, X, f, weights = generate_test_data(1)\n", + " pf.feols(\n", + " fml,\n", + " data=df,\n", + " demeaner_backend=demeaner_backend,\n", + " store_data=False,\n", + " copy_data=False,\n", + " solver=solver,\n", + " )\n", + "\n", + " if k > 1:\n", + " xfml = \"+\".join([f\"X{i}\" for i in range(2, k + 1, 1)])\n", + " fml = f\"{fml_base} + {xfml} | {fixed_effect}\"\n", + " else:\n", + " fml = f\"{fml_base} + X1 | {fixed_effect}\"\n", + "\n", + " for rep in range(1, 11):\n", + " df, Y, X, f, weights = generate_test_data(size=size, k=k)\n", + "\n", + " tic1 = time.time()\n", + " pf.feols(\n", + " fml,\n", + " data=df,\n", + " demeaner_backend=demeaner_backend,\n", + " store_data=False,\n", + " copy_data=False,\n", + " solver=solver,\n", + " )\n", + " tic2 = time.time()\n", + "\n", + " full_feols_timing = tic2 - tic1\n", + "\n", + " demean_timing = np.nan\n", + " if not skip_demean_benchmark:\n", + " YX = np.column_stack((Y.reshape(-1, 1), X))\n", + " tic3 = time.time()\n", + " if demeaner_backend == \"jax\":\n", + " _, _ = demean_jax(YX, f, weights, tol=1e-10)\n", + " else:\n", + " _, _ = demean(YX, f, weights, tol=1e-10)\n", + " tic4 = time.time()\n", + " demean_timing = tic4 - tic3\n", + "\n", + " res.append(\n", + " pd.Series(\n", + " {\n", + " \"method\": \"feols\",\n", + " \"solver\": solver,\n", + " \"demeaner_backend\": demeaner_backend,\n", + " \"n_obs\": df.shape[0],\n", + " \"k\": k,\n", + " \"G\": len(fixed_effect.split(\"+\")),\n", + " \"rep\": rep,\n", + " \"full_feols_timing\": full_feols_timing,\n", + " \"demean_timing\": demean_timing,\n", + " }\n", + " )\n", + " )\n", + "\n", + " return pd.concat(res, axis=1).T" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "execution": { + "iopub.execute_input": "2025-01-09T00:28:43.818536Z", + "iopub.status.busy": "2025-01-09T00:28:43.818246Z", + "iopub.status.idle": "2025-01-09T00:28:51.489202Z", + "shell.execute_reply": "2025-01-09T00:28:51.488591Z", + "shell.execute_reply.started": "2025-01-09T00:28:43.818520Z" + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
methodsolverdemeaner_backendn_obskGrepfull_feols_timingdemean_timing
0feolsnp.linalg.lstsqnumba10001110.150473NaN
1feolsnp.linalg.lstsqnumba10001120.147583NaN
2feolsnp.linalg.lstsqnumba10001130.186491NaN
3feolsnp.linalg.lstsqnumba10001140.190972NaN
4feolsnp.linalg.lstsqnumba10001150.162773NaN
5feolsnp.linalg.lstsqnumba10001160.171777NaN
6feolsnp.linalg.lstsqnumba10001170.166872NaN
7feolsnp.linalg.lstsqnumba10001180.158694NaN
8feolsnp.linalg.lstsqnumba10001190.185547NaN
9feolsnp.linalg.lstsqnumba100011100.158114NaN
\n", + "
" + ], + "text/plain": [ + " method solver demeaner_backend n_obs k G rep full_feols_timing \\\n", + "0 feols np.linalg.lstsq numba 1000 1 1 1 0.150473 \n", + "1 feols np.linalg.lstsq numba 1000 1 1 2 0.147583 \n", + "2 feols np.linalg.lstsq numba 1000 1 1 3 0.186491 \n", + "3 feols np.linalg.lstsq numba 1000 1 1 4 0.190972 \n", + "4 feols np.linalg.lstsq numba 1000 1 1 5 0.162773 \n", + "5 feols np.linalg.lstsq numba 1000 1 1 6 0.171777 \n", + "6 feols np.linalg.lstsq numba 1000 1 1 7 0.166872 \n", + "7 feols np.linalg.lstsq numba 1000 1 1 8 0.158694 \n", + "8 feols np.linalg.lstsq numba 1000 1 1 9 0.185547 \n", + "9 feols np.linalg.lstsq numba 1000 1 1 10 0.158114 \n", + "\n", + " demean_timing \n", + "0 NaN \n", + "1 NaN \n", + "2 NaN \n", + "3 NaN \n", + "4 NaN \n", + "5 NaN \n", + "6 NaN \n", + "7 NaN \n", + "8 NaN \n", + "9 NaN " + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# test run numba\n", + "run_standard_benchmark(fixed_effect=\"dum_1\", demeaner_backend=\"numba\", size=1, k=1)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "execution": { + "iopub.execute_input": "2025-01-09T00:28:43.818536Z", + "iopub.status.busy": "2025-01-09T00:28:43.818246Z", + "iopub.status.idle": "2025-01-09T00:28:51.489202Z", + "shell.execute_reply": "2025-01-09T00:28:51.488591Z", + "shell.execute_reply.started": "2025-01-09T00:28:43.818520Z" + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
methodsolverdemeaner_backendn_obskGrepfull_feols_timingdemean_timing
0feolsnp.linalg.lstsqjax10001110.122831NaN
1feolsnp.linalg.lstsqjax10001120.122887NaN
2feolsnp.linalg.lstsqjax10001130.136041NaN
3feolsnp.linalg.lstsqjax10001140.139644NaN
4feolsnp.linalg.lstsqjax10001150.136235NaN
5feolsnp.linalg.lstsqjax10001160.122477NaN
6feolsnp.linalg.lstsqjax10001170.123122NaN
7feolsnp.linalg.lstsqjax10001180.119589NaN
8feolsnp.linalg.lstsqjax10001190.122247NaN
9feolsnp.linalg.lstsqjax100011100.118353NaN
\n", + "
" + ], + "text/plain": [ + " method solver demeaner_backend n_obs k G rep full_feols_timing \\\n", + "0 feols np.linalg.lstsq jax 1000 1 1 1 0.122831 \n", + "1 feols np.linalg.lstsq jax 1000 1 1 2 0.122887 \n", + "2 feols np.linalg.lstsq jax 1000 1 1 3 0.136041 \n", + "3 feols np.linalg.lstsq jax 1000 1 1 4 0.139644 \n", + "4 feols np.linalg.lstsq jax 1000 1 1 5 0.136235 \n", + "5 feols np.linalg.lstsq jax 1000 1 1 6 0.122477 \n", + "6 feols np.linalg.lstsq jax 1000 1 1 7 0.123122 \n", + "7 feols np.linalg.lstsq jax 1000 1 1 8 0.119589 \n", + "8 feols np.linalg.lstsq jax 1000 1 1 9 0.122247 \n", + "9 feols np.linalg.lstsq jax 1000 1 1 10 0.118353 \n", + "\n", + " demean_timing \n", + "0 NaN \n", + "1 NaN \n", + "2 NaN \n", + "3 NaN \n", + "4 NaN \n", + "5 NaN \n", + "6 NaN \n", + "7 NaN \n", + "8 NaN \n", + "9 NaN " + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# test run jax\n", + "run_standard_benchmark(fixed_effect=\"dum_1\", demeaner_backend=\"jax\", size=1, k=1)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "execution": { + "iopub.execute_input": "2025-01-09T00:25:39.248695Z", + "iopub.status.busy": "2025-01-09T00:25:39.248317Z", + "iopub.status.idle": "2025-01-09T00:25:39.253652Z", + "shell.execute_reply": "2025-01-09T00:25:39.253000Z", + "shell.execute_reply.started": "2025-01-09T00:25:39.248671Z" + }, + "id": "HxsGRMSlR_jI" + }, + "outputs": [], + "source": [ + "def run_all_benchmarks(size_list, k_list):\n", + " \"\"\"\n", + " Run all the benchmarks.\n", + "\n", + " Args:\n", + " size_list (list): The list of sizes to run the benchmarks on. 1-> 1000, 2-> 10000, ..., 5-> 10_000_000\n", + " k_list (list): The list of k values to run the benchmarks on.\n", + " \"\"\"\n", + " res = pd.DataFrame()\n", + "\n", + " all_combinations = list(\n", + " product(\n", + " [\"numba\", \"jax\"], # demeaner_backend\n", + " [\"dum_1\", \"dum_1+dum_2\", \"dum_1+dum_2+dum_3\"], # fixef\n", + " size_list, # size\n", + " k_list, # k\n", + " [\"np.linalg.lstsq\"], # solver\n", + " )\n", + " )\n", + "\n", + " with tqdm(total=len(all_combinations), desc=\"Running Benchmarks\") as pbar:\n", + " for demeaner_backend, fixef, size, k, solver in all_combinations:\n", + " res = pd.concat(\n", + " [\n", + " res,\n", + " run_standard_benchmark(\n", + " solver=solver,\n", + " fixed_effect=fixef,\n", + " demeaner_backend=demeaner_backend,\n", + " size=size,\n", + " k=k,\n", + " ),\n", + " ],\n", + " axis=0,\n", + " )\n", + " pbar.update(1) # Update the progress bar after each iteration\n", + "\n", + " return res" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Run Benchmarks" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "colab": { + "background_save": true, + "base_uri": "https://localhost:8080/" + }, + "execution": { + "iopub.execute_input": "2025-01-09T00:25:39.962554Z", + "iopub.status.busy": "2025-01-09T00:25:39.962039Z", + "iopub.status.idle": "2025-01-09T00:26:25.319310Z", + "shell.execute_reply": "2025-01-09T00:26:25.318687Z", + "shell.execute_reply.started": "2025-01-09T00:25:39.962536Z" + }, + "id": "gki1mlqvSEIi", + "outputId": "3cb40095-df81-4e78-99a6-2410da237884" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Running Benchmarks: 100%|██████████| 24/24 [00:49<00:00, 2.06s/it]\n" + ] + } + ], + "source": [ + "res_all = run_all_benchmarks(\n", + " size_list=[1, 2, 3, 4, 5], # for N = 1000, 10_000, 100_000, 1_000_000, 10_000_000\n", + " k_list=[1, 10, 50, 100], # for k = 1, 10, 50, 100\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "colab": { + "background_save": true, + "base_uri": "https://localhost:8080/" + }, + "execution": { + "iopub.execute_input": "2025-01-09T00:26:25.320894Z", + "iopub.status.busy": "2025-01-09T00:26:25.320596Z", + "iopub.status.idle": "2025-01-09T00:26:26.391854Z", + "shell.execute_reply": "2025-01-09T00:26:26.391236Z", + "shell.execute_reply.started": "2025-01-09T00:26:25.320871Z" + }, + "id": "7zEIHj5nXXvq", + "outputId": "238dedce-a9f5-4a1b-b9a2-b84d4a89c091" + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
methoddemeaner_backendkGn_obsfull_feols_timingdemean_timing
0feolsjax1110000.127274NaN
1feolsjax11100000.142241NaN
2feolsjax1210000.134405NaN
3feolsjax12100000.168555NaN
4feolsjax1310000.139142NaN
5feolsjax13100000.189492NaN
6feolsjax5110000.144933NaN
7feolsjax51100000.147049NaN
8feolsjax5210000.147216NaN
9feolsjax52100000.175394NaN
10feolsjax5310000.15034NaN
11feolsjax53100000.194496NaN
12feolsnumba1110000.153849NaN
13feolsnumba11100000.183697NaN
14feolsnumba1210000.17201NaN
15feolsnumba12100000.16131NaN
16feolsnumba1310000.169463NaN
17feolsnumba13100000.169789NaN
18feolsnumba5110000.331457NaN
19feolsnumba51100000.172194NaN
20feolsnumba5210000.162562NaN
21feolsnumba52100000.184233NaN
22feolsnumba5310000.174623NaN
23feolsnumba53100000.166674NaN
\n", + "
" + ], + "text/plain": [ + " method demeaner_backend k G n_obs full_feols_timing demean_timing\n", + "0 feols jax 1 1 1000 0.127274 NaN\n", + "1 feols jax 1 1 10000 0.142241 NaN\n", + "2 feols jax 1 2 1000 0.134405 NaN\n", + "3 feols jax 1 2 10000 0.168555 NaN\n", + "4 feols jax 1 3 1000 0.139142 NaN\n", + "5 feols jax 1 3 10000 0.189492 NaN\n", + "6 feols jax 5 1 1000 0.144933 NaN\n", + "7 feols jax 5 1 10000 0.147049 NaN\n", + "8 feols jax 5 2 1000 0.147216 NaN\n", + "9 feols jax 5 2 10000 0.175394 NaN\n", + "10 feols jax 5 3 1000 0.15034 NaN\n", + "11 feols jax 5 3 10000 0.194496 NaN\n", + "12 feols numba 1 1 1000 0.153849 NaN\n", + "13 feols numba 1 1 10000 0.183697 NaN\n", + "14 feols numba 1 2 1000 0.17201 NaN\n", + "15 feols numba 1 2 10000 0.16131 NaN\n", + "16 feols numba 1 3 1000 0.169463 NaN\n", + "17 feols numba 1 3 10000 0.169789 NaN\n", + "18 feols numba 5 1 1000 0.331457 NaN\n", + "19 feols numba 5 1 10000 0.172194 NaN\n", + "20 feols numba 5 2 1000 0.162562 NaN\n", + "21 feols numba 5 2 10000 0.184233 NaN\n", + "22 feols numba 5 3 1000 0.174623 NaN\n", + "23 feols numba 5 3 10000 0.166674 NaN" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = (\n", + " res_all.drop([\"rep\", \"solver\"], axis=1)\n", + " .groupby([\"method\", \"demeaner_backend\", \"k\", \"G\", \"n_obs\"])\n", + " .mean()\n", + " .reset_index()\n", + ")\n", + "df" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab": { + "background_save": true + }, + "id": "VCn6O5MMXlBw" + }, + "source": [ + "## Visualize" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:matplotlib.category:Using categorical units to plot a list of strings that are all parsable as floats or dates. If these strings should be plotted as numbers, cast to the appropriate data type before plotting.\n", + "INFO:matplotlib.category:Using categorical units to plot a list of strings that are all parsable as floats or dates. If these strings should be plotted as numbers, cast to the appropriate data type before plotting.\n", + "INFO:matplotlib.category:Using categorical units to plot a list of strings that are all parsable as floats or dates. If these strings should be plotted as numbers, cast to the appropriate data type before plotting.\n", + "INFO:matplotlib.category:Using categorical units to plot a list of strings that are all parsable as floats or dates. If these strings should be plotted as numbers, cast to the appropriate data type before plotting.\n", + "INFO:matplotlib.category:Using categorical units to plot a list of strings that are all parsable as floats or dates. If these strings should be plotted as numbers, cast to the appropriate data type before plotting.\n", + "INFO:matplotlib.category:Using categorical units to plot a list of strings that are all parsable as floats or dates. If these strings should be plotted as numbers, cast to the appropriate data type before plotting.\n", + "INFO:matplotlib.category:Using categorical units to plot a list of strings that are all parsable as floats or dates. If these strings should be plotted as numbers, cast to the appropriate data type before plotting.\n", + "INFO:matplotlib.category:Using categorical units to plot a list of strings that are all parsable as floats or dates. If these strings should be plotted as numbers, cast to the appropriate data type before plotting.\n", + "INFO:matplotlib.category:Using categorical units to plot a list of strings that are all parsable as floats or dates. If these strings should be plotted as numbers, cast to the appropriate data type before plotting.\n", + "INFO:matplotlib.category:Using categorical units to plot a list of strings that are all parsable as floats or dates. If these strings should be plotted as numbers, cast to the appropriate data type before plotting.\n", + "INFO:matplotlib.category:Using categorical units to plot a list of strings that are all parsable as floats or dates. If these strings should be plotted as numbers, cast to the appropriate data type before plotting.\n", + "INFO:matplotlib.category:Using categorical units to plot a list of strings that are all parsable as floats or dates. If these strings should be plotted as numbers, cast to the appropriate data type before plotting.\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABioAAAMUCAYAAAAi7n9YAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAqptJREFUeJzs3Xt8z/X///H7e2MHOzmMbTTGkJwZFtGUMXKs5JCajVL5ymFFpJxzSg7hk5wiKVRSokXLKOQYnSTHUOZUNoaN7fX7w2/verdhe3vzmvdu18vlfWnv5+v5er4er/f7/dpT7/ter5fFMAxDAAAAAAAAAAAAJnAxuwAAAAAAAAAAAFBwEVQAAAAAAAAAAADTEFQAAAAAAAAAAADTEFQAAAAAAAAAAADTEFQAAAAAAAAAAADTEFQAAAAAAAAAAADTEFQAAAAAAAAAAADTEFQAAAAAAAAAAADTEFQAAAAAAAAAAADTEFQAAADcIhaLRSNGjDC7DEiKiYmRt7e32WXkWnx8vGrXri0PDw9ZLBadPXvWYWOPGDFCFotFp0+fdtiYd4qsfc+PFixYIIvFou3bt5tdSp68/vrrqlChglxdXVW7dm1JUkhIiGJiYm7pdhctWqQqVaqocOHCKlq06C3dVm7l5nd+YmKiLBaLPvroo9tTFAAAwB2CoAIAANzRsr7cy3oUKlRIZcqUUUxMjP74449bvv3Vq1cTRvx/TZs2lcViUdu2bbMtO3z4sCwWiyZNmmRCZXeWM2fOqFOnTvL09NTMmTO1aNEieXl5XXedn3/+WU888YTKlCkjd3d3lS5dWt26ddPPP/98m6rOPy5cuKARI0YoMTHR7FKc3po1azRo0CDdd999eueddzR27Njbst1ff/1VMTExCg0N1Zw5czR79uzbsl0AAADcOoXMLgAAAMARRo0apfLly+vSpUv67rvvtGDBAn377bf66aef5OHhccu2u3r1as2cOTPHsOLixYsqVKjg/XPr888/144dOxQWFmZ2KXekbdu26dy5cxo9erQiIyNv2H/58uXq2rWrihcvrp49e6p8+fI6fPiw5s2bp48++khLlizRww8/fBsqzx8uXLigkSNHSroanv3bK6+8osGDB5tQlXP6+uuv5eLionnz5snNzc3avnfvXrm43Lq/iUtMTFRmZqamTZumihUr3rLtAAAA4PYpeP/nDAAAnFKrVq1Ur149SdJTTz0lf39/TZgwQZ999pk6depkSk23MiDJr8qWLatz585p5MiR+uyzz8wu57YyDEOXLl2Sp6fnTY1z8uRJScrV5WwOHDigJ598UhUqVNCGDRtUsmRJ67J+/fqpSZMmevLJJ/XDDz+oQoUKN1WXo2VmZio9Pf22HieFChUqkOHhrXLy5El5enrahBSS5O7ufsu3K+XuGAEAAMCdgUs/AQAAp9SkSRNJV7/IzdK0adNsf2EtXb1/QUhIiPX5vy9TNHv2bIWGhsrd3V3169fXtm3bbNabOXOmJNlcfirLf69XnnV9/N9++01PPPGE/Pz8VLJkSb366qsyDENHjx5V+/bt5evrq8DAQL3xxhvZak1LS9Pw4cNVsWJFubu7Kzg4WIMGDVJaWtp1X48+ffrI29tbFy5cyLasa9euCgwMVEZGhiRp+/btioqKkr+/vzw9PVW+fHn16NHjuuNn8fHx0YABA7Ry5Urt3Lnzun2vdb+ArMt5HT582NoWEhKiNm3aKDExUfXq1ZOnp6dq1KhhvbzP8uXLVaNGDXl4eCgsLEzff/99jts8ePCgoqKi5OXlpdKlS2vUqFEyDMOmT2ZmpqZOnapq1arJw8NDAQEBeuaZZ/T333/b9Muq6csvv7TW9Pbbb193nz/88EOFhYXJ09NT/v7+euKJJ2wuUda0aVN1795dklS/fn1ZLJbrXuv/9ddf14ULFzR79mybkEKS/P399fbbbys1NVUTJ07Mtu7p06fVqVMn+fr6qkSJEurXr58uXbpk02ft2rVq3LixihYtKm9vb9199916+eWXbfrk9jNpsVjUp08fLV68WNWqVZO7u7tWrlyp4sWLKzY2Nlt9KSkp8vDw0IsvvihJSk9P17BhwxQWFiY/Pz95eXmpSZMmWrdunXWdw4cPW1+HkSNHWo/JrOMwp8/clStXNHr0aOtxHhISopdffjlb/Vnv97fffqsGDRrIw8NDFSpU0LvvvmvT7/Llyxo5cqQqVaokDw8PlShRQo0bN9batWuz7WNOLly4oGeeeUYlSpSQr6+voqOjbT573bt3l7+/vy5fvpxt3RYtWujuu+++7vhNmzZV9erV9csvv+iBBx5QkSJFVKZMmRw/I9djsVj0zjvvKDU11fo6L1iwQJLtPSoMw9ADDzygkiVLWgMG6er7WaNGDYWGhio1NdXa/t5771mPkeLFi6tLly46evSodXlISIiGDx8uSSpZsuQN7wvxww8/KCYmRhUqVJCHh4cCAwPVo0cPnTlzxqZf1mdj//79iomJUdGiReXn56fY2NhsvzfT0tI0YMAAlSxZUj4+PmrXrp2OHTuWp9fvv+O1adNGfn5+2rRpk93jAAAA3MkIKgAAgFPK+pK7WLFido/x/vvv6/XXX9czzzyjMWPG6PDhw3rkkUesXxA+88wzat68uaSrN3bNetxI586dlZmZqfHjxys8PFxjxozR1KlT1bx5c5UpU0YTJkxQxYoV9eKLL2rDhg3W9TIzM9WuXTtNmjRJbdu21fTp09WhQwdNmTJFnTt3vuE2U1NTtWrVKpv2CxcuaOXKlerYsaNcXV118uRJtWjRQocPH9bgwYM1ffp0devWTd99912uX7d+/fqpWLFiDr93x/79+/X444+rbdu2GjdunP7++2+1bdtWixcv1oABA/TEE09o5MiROnDggDp16qTMzEyb9TMyMtSyZUsFBARo4sSJCgsL0/Dhw61femZ55plnNHDgQN13332aNm2aYmNjtXjxYkVFRWX7cnjv3r3q2rWrmjdvrmnTpllvJpyTBQsWqFOnTnJ1ddW4ceP09NNPa/ny5WrcuLH1ZtlDhw5Vr169JF29nNmiRYv0zDPPXHPMlStXKiQkxBrM/df999+vkJCQbO+7JHXq1EmXLl3SuHHj9NBDD+nNN9+0blu6et+LNm3aKC0tTaNGjdIbb7yhdu3aaePGjdY+ef1Mfv311xowYIA6d+6sadOmqVKlSnr44Ye1YsUKpaen2/RdsWKF0tLS1KVLF0lXg4u5c+eqadOmmjBhgkaMGKFTp04pKipKu3btknT1i+u33npLkvTwww9bj8lHHnnkmq/hU089pWHDhqlu3bqaMmWKIiIiNG7cOOt2/23//v3q2LGjmjdvrjfeeEPFihVTTEyMzb1ARowYoZEjR+qBBx7QjBkzNHToUJUtW/aGwV2WPn36aM+ePRoxYoSio6O1ePFidejQwRqoPfnkkzpz5oy+/PJLm/WSkpL09ddf64knnrjhNv7++2+1bNlStWrV0htvvKEqVaropZde0hdffJGrGqWrv/OaNGkid3d36+t8//33Z+tnsVg0f/58Xbp0Sc8++6y1ffjw4fr555/1zjvvWO/B8tprryk6OlqVKlXS5MmT1b9/fyUkJOj++++3HiNTp061XsrsrbfeuuH7u3btWh08eFCxsbGaPn26unTpoiVLluihhx7KFlJKV4+Lc+fOady4cerUqZMWLFhgvZRYlqeeekpTp05VixYtNH78eBUuXFitW7fO9Wv3bxcvXlTbtm21adMmffXVV2rUqJFd4wAAANzxDAAAgDvYO++8Y0gyvvrqK+PUqVPG0aNHjY8++sgoWbKk4e7ubhw9etTaNyIiwoiIiMg2Rvfu3Y1y5cpZnx86dMiQZJQoUcL466+/rO2ffvqpIclYuXKlte3//u//jGv9k0qSMXz4cOvz4cOHG5KMXr16WduuXLli3HXXXYbFYjHGjx9vbf/7778NT09Po3v37ta2RYsWGS4uLsY333xjs51Zs2YZkoyNGzde83XKzMw0ypQpYzz66KM27cuWLTMkGRs2bDAMwzA++eQTQ5Kxbdu2a451LREREUa1atUMwzCMkSNHGpKMHTt2GIbxz2v6+uuvW/tnvR7/lfWeHjp0yNpWrlw5Q5KxadMma9uXX35pSDI8PT2N33//3dr+9ttvG5KMdevWWdu6d+9uSDKef/55m9ekdevWhpubm3Hq1CnDMAzjm2++MSQZixcvtqkpPj4+W3tWTfHx8Td8bdLT041SpUoZ1atXNy5evGht//zzzw1JxrBhw7Lt/43eg7NnzxqSjPbt21+3X7t27QxJRkpKimEY/7zu7dq1s+nXu3dvQ5Kxe/duwzAMY8qUKYYk62uTk7x8JiUZLi4uxs8//2zTN+t9/PdxZRiG8dBDDxkVKlSwPr9y5YqRlpZm0+fvv/82AgICjB49eljbTp06le3Yy/Lfz9yuXbsMScZTTz1l0+/FF180JBlff/21tS3r/c46VgzDME6ePGm4u7sbL7zwgrWtVq1aRuvWrbNt+0ay3vewsDAjPT3d2j5x4kRDkvHpp58ahmEYGRkZxl133WV07tzZZv3JkycbFovFOHjw4HW3ExERYUgy3n33XWtbWlqaERgYmO33w410797d8PLyytZerlw5m99dhvHPcfnee+8Z3333neHq6mr079/fuvzw4cOGq6ur8dprr9ms9+OPPxqFChWyac96H6/32cxy4cKFbG0ffPBBtvcya8x/f5YMwzAefvhho0SJEtbnWZ+Z3r172/R7/PHHr/m5+7d169YZkowPP/zQOHfunBEREWH4+/sb33///Q33BQAAwJlxRgUAAHAKkZGRKlmypIKDg9WxY0d5eXnps88+01133WX3mJ07d7Y5IyPrr9YPHjx4U7U+9dRT1p9dXV1Vr149GYahnj17WtuLFi2qu+++22ZbH374oe655x5VqVJFp0+ftj4efPBBSbK5BM5/WSwWPfbYY1q9erXOnz9vbV+6dKnKlCmjxo0bW7crXb0hdk6XlsmtrLMq/vuXyDejatWqatiwofV5eHi4JOnBBx9U2bJls7Xn9D716dPH+nPWpYjS09P11VdfSbr6Gvv5+al58+Y2r3FYWJi8vb2zvcbly5dXVFTUDWvfvn27Tp48qd69e9vck6F169aqUqVKjmc83Mi5c+ckXb3c1vVkLU9JSbFp/7//+z+b588//7ykqzeIl/75LHz66afZzk7JktfPZEREhKpWrWrT9uCDD8rf319Lly61tv39999au3atzVkZrq6u1nshZGZm6q+//tKVK1dUr169XJ+t8F9Z+xoXF2fT/sILL0hStvelatWqNmevlCxZMttxWrRoUf3888/at2+fXTX16tVLhQsXtj5/7rnnVKhQIWutLi4u6tatmz777DPrZ0CSFi9erEaNGql8+fI33Ia3t7fNmRdubm5q0KDBTf9uu55evXopKipKzz//vJ588kmFhoZq7Nix1uXLly9XZmamOnXqZPNZCgwMVKVKla77++16/n3PmEuXLun06dO69957JSnHz82/z/qQrv7eP3PmjPX4yXof+vbta9Ovf//+eaorOTlZLVq00K+//qrExMTrno0FAABQEBBUAAAApzBz5kytXbtWH330kR566CGdPn36pm/o+u8vv6V/LiP133sV3Oy4fn5+8vDwkL+/f7b2f29r3759+vnnn1WyZEmbR+XKlSXJ5vrvOencubMuXrxovcn1+fPntXr1aj322GPW6/ZHRETo0Ucf1ciRI+Xv76/27dvrnXfeueE9MP7Lz89P/fv312effXbN+0XkVU6vmyQFBwfn2P7f98nFxSXbDaWzXrusS4Xt27dPycnJKlWqVLbX+fz589le49x8KSxJv//+uyTleP+AKlWqWJfnRVYA8e8vq3NyrUCjUqVKNs9DQ0Pl4uJifS06d+6s++67T0899ZQCAgLUpUsXLVu2zCa0yOtnMqfXq1ChQnr00Uf16aefWj9n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w4dLkipVqqQZM2YoISFBzZs3v+56JUuWlLu7uzw9Pa97FsLbb7+tmjVrqm/fvlq+fLlGjBihsLAwSdKMGTNUp04dm3+Yzp8/X8HBwfrtt99UuXJl+fj4yNXV9brbKF269A1v3MYXNADg/JjTslu2bJm2bdumt99+O9frAADyB+a1fzRq1Eg7d+5UWlqaevXqpVGjRt1wHQCA8yOoAJxIzZo1bZ4HBQXp5MmTDhu/WLFimjdvnqKiotSoUSMNHjzYumz37t1at26dvL29s6134MABVa5cOVfbKFSokCpWrOiwmgEAdybmNFvr1q1TbGys5syZo2rVqjlkTADA7cO89o+lS5fq3Llz2r17twYOHKhJkyZp0KBBNz0uAODORlABOJHChQvbPLdYLMrMzHToNjZs2CBXV1cdP35cqamp8vHxkSSdP39ebdu21YQJE7KtExQUlOvxjxw5oqpVq163z8svv6yXX345b4UDAO4ozGn/WL9+vdq2baspU6YoOjo619sHAOQfzGv/CA4OliRVrVpVGRkZ6tWrl1544QW5urrmuhYAgPMhqACQa5s2bdKECRO0cuVKvfTSS+rTp48WLlwoSapbt64+/vhjhYSEqFAh+3+1cOknAMDtcKfMaYmJiWrTpo0mTJigXr162V0LAMC53Snz2n9lZmbq8uXLyszMJKgAgAKOoAJArpw7d05PPvmk+vbtq1atWumuu+5S/fr11bZtW3Xs2FH/93//pzlz5qhr164aNGiQihcvrv3792vJkiWaO3durv/R6YjTiX/55Relp6frr7/+0rlz56z/mK5du/ZNjQsAcA53ypy2bt06tWnTRv369dOjjz6qpKQkSZKbmxuhPQDA6k6Z1xYvXqzChQurRo0acnd31/bt2zVkyBB17tw52xknAICCh6ACQK7069dPXl5e1huw1ahRQ2PHjtUzzzyjhg0bqkyZMtq4caNeeukltWjRQmlpaSpXrpxatmwpFxeX21rrQw89pN9//936vE6dOpIkwzBuax0AgPzpTpnTFi5cqAsXLmjcuHEaN26ctT0iIkKJiYm3rQ4AQP52p8xrhQoV0oQJE/Tbb7/JMAyVK1dOffr00YABA25bDQCA/Mti8M0dAAAAAAAAAAAwye39M2cAAAAAAAAAAIB/IagACoDFixfL29s7x0e1atXMLg8AgFxjTgMAOBPm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JibaH4Z9/PhxeXn9cuHHm2++qezsbPXp08dhnYkTJ2rSpEmSpBdeeEFXr17Vk08+qYsXL6pz585KTEws0nMsAAAAAAAAkNear69YXYJH+f/al7e6BAC462zGGGN1Ee4mMzNTFStW1KVLl+Tv7291ObiNc0nLrS7BY1TtPsDqEgDcQfQ1AICnoKcB7omgwrUIKgCURE49o0KSjhw5ovHjx+vxxx/X6dOnJUmff/65vv32W5cVBwAAAAAAAAAAPJtTt37auHGjevbsqfvvv1+bNm3Syy+/rBo1amjPnj1atGiR/vWvf7m6TgAAAAAAAAAodrp166aC3tRm/fr1d7gawD05FVSMGzdOL7/8smJjY1WhQgX7eLdu3TR37lyXFQcAAAAAAAAAxVmrVq3sPx85ckQ7d+5U3759rSsIcENOBRV79+7VBx98kGe8Ro0aOnv2bJGLAgAAAAAAAABPkJCQIEn67rvv9NBDD+nMmTOqX7++nn76aYsrA9yHU8+oqFSpkk6dOpVnfNeuXapVq1aRiwIAAAAAAAAAT3Ho0CF16dJFjz76qDZv3qzx48fn+4vgQEnlVFAxYMAAvfjii0pPT5fNZlNubq42b96s559/XtHR0a6uEQAAAAAAAACKpdTUVD300EP64x//qPnz56tt27b6+OOP9fTTT+uzzz6zujzALTgVVLz66qtq3LixgoODdeXKFTVt2lQPPvigOnXqpPHjx7u6RgAAAAAAAAAolh566CH169dP//jHP+xjnTt31rJlyzRw4EALKwPch1PPqPDx8dHChQsVHx+vvXv36sqVK2rdurUaNGjg6voAAAAA4DedS1pudQkepWr3AVaXAACARxk0aJBmzpyZZ7xnz5568803LagIcD9OBRW3BAcHKzg4WDk5Odq7d68uXLigypUru6o2AAAAAAAAACjW8gspbomKirqLlQDuy6lbP40aNUqLFi2SJOXk5KhLly5q06aNgoODtWHDBlfWBwAAAAAAAAAAPJhTQcW//vUvtWzZUpL0ySef6Pvvv1dqaqpGjx6tv//97y4tEAAAAAAAAAAAeC6ngoqzZ88qMDBQkrRmzRr169dPDRs21NChQ7V3716XFggAAAAAAAAAADyXU0FFQECA9u/fr5ycHCUmJurhhx+WJF27dk3e3t4uLRAAAAAAAAAAAHgupx6mHRMTo379+qlm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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "df[\"G\"] = df[\"G\"].map({1: \"n_fixef = 1\", 2: \"n_fixef = 2\", 3: \"n_fixef = 3\"})\n", + "df[\"n_obs\"] = df[\"n_obs\"].astype(str)\n", + "\n", + "# Dynamically determine unique values for order and hue_order\n", + "n_obs_order = sorted(df[\"n_obs\"].unique(), key=lambda x: int(x)) # Sort as integers\n", + "demeaner_backend_order = df[\"demeaner_backend\"].unique()\n", + "\n", + "custom_palette = sns.color_palette(\"coolwarm\", n_colors=2)\n", + "\n", + "# Create the FacetGrid with reordered columns and rows\n", + "g = sns.FacetGrid(\n", + " df,\n", + " col=\"G\", # G (n_fixef) increases left to right\n", + " row=\"k\", # k increases top to bottom\n", + " margin_titles=True,\n", + " height=4,\n", + " aspect=1.2,\n", + " col_order=[\"n_fixef = 1\", \"n_fixef = 2\", \"n_fixef = 3\"], # Ensure correct order\n", + ")\n", + "\n", + "# Plot the bar chart for each facet with the custom palette\n", + "g.map(\n", + " sns.barplot,\n", + " \"n_obs\",\n", + " \"full_feols_timing\",\n", + " \"demeaner_backend\",\n", + " order=n_obs_order, # Dynamic order for n_obs\n", + " hue_order=demeaner_backend_order, # Dynamic hue order for demeaner_backend\n", + " errorbar=None, # Suppress error bars\n", + " palette=custom_palette,\n", + ")\n", + "\n", + "# Add legend and adjust layout\n", + "g.add_legend(title=\"Demeaner Backend\")\n", + "g.set_axis_labels(\"Number of Observations\", \"Runtime (seconds)\")\n", + "g.set_titles(row_template=\"k = {row_name}\", col_template=\"{col_name}\")\n", + "plt.subplots_adjust(top=0.9)\n", + "g.fig.suptitle(\"Runtime vs Number of Observations by n_fixef and k\")\n", + "\n", + "# Show plot\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "jax", + "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.8" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} From ffd3217020c6fe6ee4fb4eb0831fdfbbf9dfd597 Mon Sep 17 00:00:00 2001 From: Alexander Fischer Date: Tue, 14 Jan 2025 21:43:54 +0100 Subject: [PATCH 2/6] add jax module, JAXOLS --- benchmarks/gpu_pyfixest_errors.ipynb | 2 +- pyfixest/estimation/demean_.py | 2 +- pyfixest/estimation/jax/OLSJAX.py | 145 ++++++++++++++++++ pyfixest/estimation/{ => jax}/demean_jax_.py | 9 +- .../{ => jax}/detect_singletons_jax.py | 0 tests/test_demean.py | 2 +- tests/test_detect_singletons.py | 2 +- 7 files changed, 157 insertions(+), 5 deletions(-) create mode 100644 pyfixest/estimation/jax/OLSJAX.py rename pyfixest/estimation/{ => jax}/demean_jax_.py (92%) rename pyfixest/estimation/{ => jax}/detect_singletons_jax.py (100%) diff --git a/benchmarks/gpu_pyfixest_errors.ipynb b/benchmarks/gpu_pyfixest_errors.ipynb index 3f6ee1481..945569cd9 100644 --- a/benchmarks/gpu_pyfixest_errors.ipynb +++ b/benchmarks/gpu_pyfixest_errors.ipynb @@ -194,7 +194,7 @@ "\n", "import pyfixest as pf\n", "from pyfixest.estimation.demean_ import demean\n", - "from pyfixest.estimation.demean_jax_ import demean_jax" + "from pyfixest.estimation.jax.demean_jax_ import demean_jax" ] }, { diff --git a/pyfixest/estimation/demean_.py b/pyfixest/estimation/demean_.py index 5987dde43..ecff0b5aa 100644 --- a/pyfixest/estimation/demean_.py +++ b/pyfixest/estimation/demean_.py @@ -329,7 +329,7 @@ def _set_demeaner_backend(demeaner_backend: Literal["numba", "jax"]) -> Callable if demeaner_backend == "numba": return demean elif demeaner_backend == "jax": - from pyfixest.estimation.demean_jax_ import demean_jax + from pyfixest.estimation.jax.demean_jax_ import demean_jax return demean_jax else: diff --git a/pyfixest/estimation/jax/OLSJAX.py b/pyfixest/estimation/jax/OLSJAX.py new file mode 100644 index 000000000..bde7e29df --- /dev/null +++ b/pyfixest/estimation/jax/OLSJAX.py @@ -0,0 +1,145 @@ +import jax +import jax.numpy as jnp +from pyfixest.estimation.jax.demean_jax_ import demean_jax +from typing import Optional +import pandas as pd + +class OLSJAX: + + def __init__(self, X: jax.Array, Y: jax.Array, fe: Optional[jax.Array] = None, vcov: str = 'iid'): + + self.X_orignal = X + self.Y_orignal = Y + self.fe = fe + self.N = X.shape[0] + self.k = X.shape[1] + self.weights = jnp.ones(self.N) + self.vcov_type = vcov + + def fit(self): + + self.Y, self.X = self.demean(Y = self.Y_orignal, X = self.X_orignal, fe = self.fe, weights = self.weights) + self.beta = jnp.linalg.lstsq(self.X, self.Y)[0] + self.residuals + self.scores + self.vcov(vcov_type = self.vcov_type) + self.inference() + + def tidy(self): + + lb = 0.025 + ub = 0.975 + + return pd.DataFrame( + { + #"Coefficient": _coefnames, + "Estimate": self.beta.flatten(), + "Std. Error": self.se.flatten(), + "t value": self.tstat.flatten(), + "Pr(>|t|)": self.pvalue.flatten(), + f"{lb * 100:.1f}%": self.confint[:,0].flatten(), + f"{ub * 100:.1f}%": self.confint[:,1].flatten(), + } + ) + + @property + def residuals(self): + self.uhat = self.Y - self.X @ self.beta + return self.uhat + + def vcov(self, vcov_type: str): + + bread = self.bread + meat = self.meat(type = vcov_type) + if vcov_type == 'iid': + self.vcov = bread * meat + else: + self.vcov = bread @ meat @ bread + + return self.vcov + + @property + def bread(self): + return jnp.linalg.inv(self.X.T @ self.X) + + @property + def leverage(self): + return jnp.sum(self.X * (self.X @ jnp.linalg.inv(self.X.T @ self.X)), axis=1) + + @property + def scores(self): + return self.X * self.residuals + + def meat(self, type: str): + + if type == 'iid': + return self.meat_iid + elif type == 'HC1': + return self.meat_hc1 + elif type == 'HC2': + return self.meat_hc2 + elif type == 'HC3': + return self.meat_hc3 + elif type == 'CRV1': + return self.meat_crv1 + else: + raise ValueError("Invalid type") + + @property + def meat_iid(self): + return jnp.sum(self.uhat ** 2) / (self.N - self.k) + + @property + def meat_hc1(self): + + return self.scores.T @ self.scores + + def meat_hc2(self): + self.leverage + transformed_scores = self.scores / jnp.sqrt(1 - self.leverage) + return transformed_scores.T @ transformed_scores + + def meat_hc3(self): + self.leverage + transformed_scores = self.scores / (1 - self.leverage) + return transformed_scores.T @ transformed_scores + + @property + def meat_crv1(self): + raise NotImplementedError("CRV1 is not implemented") + + def predict(self, X): + X = jnp.array(X) + return X @ self.beta + + def demean(self, Y: jax.Array, X: jax.Array, fe: jax.Array, weights: jax.Array): + + if fe is not None: + if not jnp.issubdtype(fe.dtype, jnp.integer): + raise ValueError("Fixed effects must be integers") + + YX = jnp.concatenate((Y, X), axis = 1) + YXd, success = demean_jax(x = YX, flist = fe, weights = self.weights, output = "jax") + Yd = YXd[:, 0].reshape(-1, 1) + Xd = YXd[:, 1:] + + return Yd, Xd + + else: + + return Y, X + + def inference(self): + + self.se = jnp.sqrt(jnp.diag(self.vcov)).reshape(-1, 1) + self.tstat = self.beta / self.se + self.pvalue = 2 * (1 - jax.scipy.stats.norm.cdf(jnp.abs(self.tstat))) + self.confint = jnp.column_stack( + [ + self.beta - jax.scipy.stats.norm.ppf(1 - 0.05 / 2) * self.se, + self.beta + jax.scipy.stats.norm.ppf(1 - 0.05 / 2) * self.se, + ] + ) + + + return self.se, self.tstat, self.pvalue, self.confint diff --git a/pyfixest/estimation/demean_jax_.py b/pyfixest/estimation/jax/demean_jax_.py similarity index 92% rename from pyfixest/estimation/demean_jax_.py rename to pyfixest/estimation/jax/demean_jax_.py index 2d327fb2d..0c27ec0f3 100644 --- a/pyfixest/estimation/demean_jax_.py +++ b/pyfixest/estimation/jax/demean_jax_.py @@ -72,6 +72,7 @@ def demean_jax( weights: np.ndarray, tol: float = 1e-08, maxiter: int = 100_000, + output: str = "numpy", ) -> tuple[np.ndarray, bool]: """Fast and reliable JAX implementation with static shapes.""" # Enable float64 precision @@ -89,4 +90,10 @@ def demean_jax( result_jax, converged = _demean_jax_impl( x_jax, flist_jax, weights_jax, n_groups, tol, maxiter ) - return np.array(result_jax), converged + + if output == "numpy": + return np.array(result_jax), converged + elif output == "jax": + return result_jax, converged + else: + raise ValueError("Invalid output type") diff --git a/pyfixest/estimation/detect_singletons_jax.py b/pyfixest/estimation/jax/detect_singletons_jax.py similarity index 100% rename from pyfixest/estimation/detect_singletons_jax.py rename to pyfixest/estimation/jax/detect_singletons_jax.py diff --git a/tests/test_demean.py b/tests/test_demean.py index 973e5958e..a4c08aeed 100644 --- a/tests/test_demean.py +++ b/tests/test_demean.py @@ -4,7 +4,7 @@ import pytest from pyfixest.estimation.demean_ import _set_demeaner_backend, demean, demean_model -from pyfixest.estimation.demean_jax_ import demean_jax +from pyfixest.estimation.jax.demean_jax_ import demean_jax @pytest.mark.parametrize( diff --git a/tests/test_detect_singletons.py b/tests/test_detect_singletons.py index d6c2af329..8f5480c37 100644 --- a/tests/test_detect_singletons.py +++ b/tests/test_detect_singletons.py @@ -2,7 +2,7 @@ import pytest from pyfixest.estimation.detect_singletons_ import detect_singletons -from pyfixest.estimation.detect_singletons_jax import detect_singletons_jax +from pyfixest.jax.detect_singletons_jax import detect_singletons_jax input1 = np.array([[0, 2, 1], [0, 2, 1], [0, 1, 3], [0, 1, 2], [0, 1, 2]]) solution1 = np.array([False, False, True, False, False]) From 80bd6a4ce2cb67bfd89811645eda55e5ac698586 Mon Sep 17 00:00:00 2001 From: Alexander Fischer Date: Tue, 14 Jan 2025 21:44:55 +0100 Subject: [PATCH 3/6] add jaxols --- pyfixest/estimation/jax/OLSJAX.py | 58 ++++++++++++++++--------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/pyfixest/estimation/jax/OLSJAX.py b/pyfixest/estimation/jax/OLSJAX.py index bde7e29df..2458bbfa3 100644 --- a/pyfixest/estimation/jax/OLSJAX.py +++ b/pyfixest/estimation/jax/OLSJAX.py @@ -1,13 +1,20 @@ +from typing import Optional + import jax import jax.numpy as jnp -from pyfixest.estimation.jax.demean_jax_ import demean_jax -from typing import Optional import pandas as pd -class OLSJAX: +from pyfixest.estimation.jax.demean_jax_ import demean_jax - def __init__(self, X: jax.Array, Y: jax.Array, fe: Optional[jax.Array] = None, vcov: str = 'iid'): +class OLSJAX: + def __init__( + self, + X: jax.Array, + Y: jax.Array, + fe: Optional[jax.Array] = None, + vcov: str = "iid", + ): self.X_orignal = X self.Y_orignal = Y self.fe = fe @@ -17,28 +24,28 @@ def __init__(self, X: jax.Array, Y: jax.Array, fe: Optional[jax.Array] = None, v self.vcov_type = vcov def fit(self): - - self.Y, self.X = self.demean(Y = self.Y_orignal, X = self.X_orignal, fe = self.fe, weights = self.weights) + self.Y, self.X = self.demean( + Y=self.Y_orignal, X=self.X_orignal, fe=self.fe, weights=self.weights + ) self.beta = jnp.linalg.lstsq(self.X, self.Y)[0] self.residuals self.scores - self.vcov(vcov_type = self.vcov_type) + self.vcov(vcov_type=self.vcov_type) self.inference() def tidy(self): - lb = 0.025 ub = 0.975 return pd.DataFrame( { - #"Coefficient": _coefnames, + # "Coefficient": _coefnames, "Estimate": self.beta.flatten(), "Std. Error": self.se.flatten(), "t value": self.tstat.flatten(), "Pr(>|t|)": self.pvalue.flatten(), - f"{lb * 100:.1f}%": self.confint[:,0].flatten(), - f"{ub * 100:.1f}%": self.confint[:,1].flatten(), + f"{lb * 100:.1f}%": self.confint[:, 0].flatten(), + f"{ub * 100:.1f}%": self.confint[:, 1].flatten(), } ) @@ -48,10 +55,9 @@ def residuals(self): return self.uhat def vcov(self, vcov_type: str): - bread = self.bread - meat = self.meat(type = vcov_type) - if vcov_type == 'iid': + meat = self.meat(type=vcov_type) + if vcov_type == "iid": self.vcov = bread * meat else: self.vcov = bread @ meat @ bread @@ -71,27 +77,25 @@ def scores(self): return self.X * self.residuals def meat(self, type: str): - - if type == 'iid': + if type == "iid": return self.meat_iid - elif type == 'HC1': + elif type == "HC1": return self.meat_hc1 - elif type == 'HC2': + elif type == "HC2": return self.meat_hc2 - elif type == 'HC3': + elif type == "HC3": return self.meat_hc3 - elif type == 'CRV1': + elif type == "CRV1": return self.meat_crv1 else: raise ValueError("Invalid type") @property def meat_iid(self): - return jnp.sum(self.uhat ** 2) / (self.N - self.k) + return jnp.sum(self.uhat**2) / (self.N - self.k) @property def meat_hc1(self): - return self.scores.T @ self.scores def meat_hc2(self): @@ -113,24 +117,23 @@ def predict(self, X): return X @ self.beta def demean(self, Y: jax.Array, X: jax.Array, fe: jax.Array, weights: jax.Array): - if fe is not None: if not jnp.issubdtype(fe.dtype, jnp.integer): raise ValueError("Fixed effects must be integers") - YX = jnp.concatenate((Y, X), axis = 1) - YXd, success = demean_jax(x = YX, flist = fe, weights = self.weights, output = "jax") + YX = jnp.concatenate((Y, X), axis=1) + YXd, success = demean_jax( + x=YX, flist=fe, weights=self.weights, output="jax" + ) Yd = YXd[:, 0].reshape(-1, 1) Xd = YXd[:, 1:] return Yd, Xd else: - return Y, X def inference(self): - self.se = jnp.sqrt(jnp.diag(self.vcov)).reshape(-1, 1) self.tstat = self.beta / self.se self.pvalue = 2 * (1 - jax.scipy.stats.norm.cdf(jnp.abs(self.tstat))) @@ -141,5 +144,4 @@ def inference(self): ] ) - return self.se, self.tstat, self.pvalue, self.confint From 62da934c4bdb26ce763cbf6132983fc71f03b3d9 Mon Sep 17 00:00:00 2001 From: Alexander Fischer Date: Wed, 15 Jan 2025 20:41:55 +0100 Subject: [PATCH 4/6] interface --- pyfixest/estimation/FixestMulti_.py | 11 +++- pyfixest/estimation/jax/OLSJAX.py | 48 ++++++++------- pyfixest/estimation/jax/olsjax_interface.py | 65 +++++++++++++++++++++ 3 files changed, 102 insertions(+), 22 deletions(-) create mode 100644 pyfixest/estimation/jax/olsjax_interface.py diff --git a/pyfixest/estimation/FixestMulti_.py b/pyfixest/estimation/FixestMulti_.py index 1602d9072..275e451de 100644 --- a/pyfixest/estimation/FixestMulti_.py +++ b/pyfixest/estimation/FixestMulti_.py @@ -8,6 +8,7 @@ from pyfixest.estimation.fegaussian_ import Fegaussian from pyfixest.estimation.feiv_ import Feiv from pyfixest.estimation.felogit_ import Felogit +from pyfixest.estimation.jax.olsjax_interface import OLSJAX_API from pyfixest.estimation.feols_ import Feols, _check_vcov_input, _deparse_vcov_input from pyfixest.estimation.feols_compressed_ import FeolsCompressed from pyfixest.estimation.fepois_ import Fepois @@ -16,7 +17,6 @@ from pyfixest.utils.dev_utils import DataFrameType, _narwhals_to_pandas from pyfixest.utils.utils import capture_context - class FixestMulti: """A class to estimate multiple regression models with fixed effects.""" @@ -279,7 +279,11 @@ def _estimate_all_models( FIT: Union[Feols, Feiv, Fepois] if _method == "feols" and not _is_iv: - FIT = Feols( + + backend = "jax" + OLSCLASS = Feols if backend != "jax" else OLSJAX_API + + FIT = OLSCLASS( FixestFormula=FixestFormula, data=_data, ssc_dict=_ssc_dict, @@ -299,11 +303,14 @@ def _estimate_all_models( sample_split_value=sample_split_value, sample_split_var=_splitvar, ) + FIT.prepare_model_matrix() FIT.demean() FIT.to_array() FIT.drop_multicol_vars() FIT.wls_transform() + + elif _method == "feols" and _is_iv: FIT = Feiv( FixestFormula=FixestFormula, diff --git a/pyfixest/estimation/jax/OLSJAX.py b/pyfixest/estimation/jax/OLSJAX.py index 2458bbfa3..ed293da7b 100644 --- a/pyfixest/estimation/jax/OLSJAX.py +++ b/pyfixest/estimation/jax/OLSJAX.py @@ -6,22 +6,46 @@ from pyfixest.estimation.jax.demean_jax_ import demean_jax - class OLSJAX: def __init__( self, X: jax.Array, Y: jax.Array, fe: Optional[jax.Array] = None, - vcov: str = "iid", + weights: Optional[jax.Array] = None, + vcov: Optional[str, dict[str,str]] = None, ): + + """ + Class to run OLS regression in JAX. + + Parameters + ---------- + X : jax.Array + N x k matrix of independent variables. + Y : jax.Array + Dependent variable. N x 1 matrix. + fe : jax.Array, optional + Fixed effects. N x 1 matrix of integers. The default is None. + weights: jax.Array, optional + Weights. N x 1 matrix. The default is None. + vcov : str, optional + Type of covariance matrix. The default is None. Options are: + - "iid" (default): iid errors + - "HC1": heteroskedasticity robust + - "HC2": heteroskedasticity robust + - "HC3": heteroskedasticity robust + - "CRV1": cluster robust. In this case, please provide a dictionary + with the cluster variable as key and the name of the cluster variable as value. + """ + self.X_orignal = X self.Y_orignal = Y self.fe = fe self.N = X.shape[0] self.k = X.shape[1] - self.weights = jnp.ones(self.N) - self.vcov_type = vcov + self.weights = jnp.ones(self.N) if weights is None else weights + self.vcov_type = "iid" if vcov is None else vcov def fit(self): self.Y, self.X = self.demean( @@ -33,22 +57,6 @@ def fit(self): self.vcov(vcov_type=self.vcov_type) self.inference() - def tidy(self): - lb = 0.025 - ub = 0.975 - - return pd.DataFrame( - { - # "Coefficient": _coefnames, - "Estimate": self.beta.flatten(), - "Std. Error": self.se.flatten(), - "t value": self.tstat.flatten(), - "Pr(>|t|)": self.pvalue.flatten(), - f"{lb * 100:.1f}%": self.confint[:, 0].flatten(), - f"{ub * 100:.1f}%": self.confint[:, 1].flatten(), - } - ) - @property def residuals(self): self.uhat = self.Y - self.X @ self.beta diff --git a/pyfixest/estimation/jax/olsjax_interface.py b/pyfixest/estimation/jax/olsjax_interface.py new file mode 100644 index 000000000..a78b65d22 --- /dev/null +++ b/pyfixest/estimation/jax/olsjax_interface.py @@ -0,0 +1,65 @@ +from pyfixest.estimation.feols_ import Feols +from pyfixest.estimation.feols_ import Feols, _drop_multicollinear_variables +from pyfixest.estimation.FormulaParser import FixestFormula +import pandas as pd +from typing import Union, Optional, Mapping, Any, Literal +import jax.numpy as jnp + +class OLSJAX_API(Feols): + + def __init__( + self, + FixestFormula: FixestFormula, + data: pd.DataFrame, + ssc_dict: dict[str, Union[str, bool]], + drop_singletons: bool, + drop_intercept: bool, + weights: Optional[str], + weights_type: Optional[str], + collin_tol: float, + fixef_tol: float, + lookup_demeaned_data: dict[str, pd.DataFrame], + solver: Literal[ + "np.linalg.lstsq", "np.linalg.solve", "scipy.sparse.linalg.lsqr", "jax" + ] = "np.linalg.solve", + demeaner_backend: Literal["numba", "jax"] = "numba", + store_data: bool = True, + copy_data: bool = True, + lean: bool = False, + context: Union[int, Mapping[str, Any]] = 0, + sample_split_var: Optional[str] = None, + sample_split_value: Optional[Union[str, int]] = None, + ) -> None: + super().__init__( + FixestFormula=FixestFormula, + data=data, + ssc_dict=ssc_dict, + drop_singletons=drop_singletons, + drop_intercept=drop_intercept, + weights=weights, + weights_type=weights_type, + collin_tol=collin_tol, + fixef_tol=fixef_tol, + lookup_demeaned_data=lookup_demeaned_data, + solver=solver, + store_data=store_data, + copy_data=copy_data, + lean=lean, + sample_split_var=sample_split_var, + sample_split_value=sample_split_value, + context=context, + demeaner_backend=demeaner_backend, + ) + + + + def to_array(self): + + """ + Convert all relevant data to JAX arrays. + """ + + self._Y = jnp.asarray(self._Y) + self._X = jnp.asarray(self._X) + self._fe = jnp.asarray(self._fe) + self._weights = jnp.asarray(self._weights) From 2c92c3d4d740cee0033f6554bef3fa8bd39530b3 Mon Sep 17 00:00:00 2001 From: Alexander Fischer Date: Sun, 19 Jan 2025 15:19:49 +0100 Subject: [PATCH 5/6] POC hacky pure JAX OLS class via interface --- pyfixest/estimation/FixestMulti_.py | 38 ++++++++----- pyfixest/estimation/feols_.py | 1 + pyfixest/estimation/jax/OLSJAX.py | 28 ++++------ pyfixest/estimation/jax/olsjax_interface.py | 60 ++++++++++++++++++--- 4 files changed, 86 insertions(+), 41 deletions(-) diff --git a/pyfixest/estimation/FixestMulti_.py b/pyfixest/estimation/FixestMulti_.py index 275e451de..62c9666d9 100644 --- a/pyfixest/estimation/FixestMulti_.py +++ b/pyfixest/estimation/FixestMulti_.py @@ -304,11 +304,12 @@ def _estimate_all_models( sample_split_var=_splitvar, ) - FIT.prepare_model_matrix() - FIT.demean() - FIT.to_array() - FIT.drop_multicol_vars() - FIT.wls_transform() + if backend != "jax": + FIT.prepare_model_matrix() + FIT.demean() + FIT.to_array() + FIT.drop_multicol_vars() + FIT.wls_transform() elif _method == "feols" and _is_iv: @@ -337,6 +338,7 @@ def _estimate_all_models( FIT.to_array() FIT.drop_multicol_vars() FIT.wls_transform() + elif _method == "fepois": FIT = Fepois( FixestFormula=FixestFormula, @@ -482,17 +484,25 @@ def _estimate_all_models( FIT.get_fit() # if X is empty: no inference (empty X only as shorthand for demeaning) - if not FIT._X_is_empty: - # inference - vcov_type = _get_vcov_type(vcov, fval) - FIT.vcov(vcov=vcov_type, data=FIT._data) + if backend != "jax": + if not FIT._X_is_empty: + # inference + vcov_type = _get_vcov_type(vcov, fval) + FIT.vcov(vcov=vcov_type, data=FIT._data) + + FIT.get_inference() + # other regression stats + if _method == "feols" and not FIT._is_iv: + FIT.get_performance() + if isinstance(FIT, Feiv): + FIT.first_stage() + + else: + #import pdb; pdb.set_trace() + FIT.vcov(type = "iid") + FIT.convert_attributes_to_numpy() FIT.get_inference() - # other regression stats - if _method == "feols" and not FIT._is_iv: - FIT.get_performance() - if isinstance(FIT, Feiv): - FIT.first_stage() # delete large attributescl FIT._clear_attributes() diff --git a/pyfixest/estimation/feols_.py b/pyfixest/estimation/feols_.py index 113751fb9..dd4e1e4f8 100644 --- a/pyfixest/estimation/feols_.py +++ b/pyfixest/estimation/feols_.py @@ -816,6 +816,7 @@ def get_inference(self, alpha: float = 0.05) -> None: ------- None """ + _vcov = self._vcov _beta_hat = self._beta_hat _vcov_type = self._vcov_type diff --git a/pyfixest/estimation/jax/OLSJAX.py b/pyfixest/estimation/jax/OLSJAX.py index ed293da7b..44606a337 100644 --- a/pyfixest/estimation/jax/OLSJAX.py +++ b/pyfixest/estimation/jax/OLSJAX.py @@ -13,7 +13,7 @@ def __init__( Y: jax.Array, fe: Optional[jax.Array] = None, weights: Optional[jax.Array] = None, - vcov: Optional[str, dict[str,str]] = None, + vcov: Optional[str] = None, ): """ @@ -52,15 +52,17 @@ def fit(self): Y=self.Y_orignal, X=self.X_orignal, fe=self.fe, weights=self.weights ) self.beta = jnp.linalg.lstsq(self.X, self.Y)[0] - self.residuals + self.get_fit() self.scores self.vcov(vcov_type=self.vcov_type) self.inference() + def get_fit(self): + self.beta = jnp.linalg.lstsq(self.X, self.Y)[0] + @property def residuals(self): - self.uhat = self.Y - self.X @ self.beta - return self.uhat + return self.Y - self.X @ self.beta def vcov(self, vcov_type: str): bread = self.bread @@ -100,7 +102,7 @@ def meat(self, type: str): @property def meat_iid(self): - return jnp.sum(self.uhat**2) / (self.N - self.k) + return jnp.sum(self.residuals**2) / (self.N - self.k) @property def meat_hc1(self): @@ -125,13 +127,14 @@ def predict(self, X): return X @ self.beta def demean(self, Y: jax.Array, X: jax.Array, fe: jax.Array, weights: jax.Array): + if fe is not None: if not jnp.issubdtype(fe.dtype, jnp.integer): raise ValueError("Fixed effects must be integers") YX = jnp.concatenate((Y, X), axis=1) YXd, success = demean_jax( - x=YX, flist=fe, weights=self.weights, output="jax" + x=YX, flist=fe, weights=weights, output="jax" ) Yd = YXd[:, 0].reshape(-1, 1) Xd = YXd[:, 1:] @@ -140,16 +143,3 @@ def demean(self, Y: jax.Array, X: jax.Array, fe: jax.Array, weights: jax.Array): else: return Y, X - - def inference(self): - self.se = jnp.sqrt(jnp.diag(self.vcov)).reshape(-1, 1) - self.tstat = self.beta / self.se - self.pvalue = 2 * (1 - jax.scipy.stats.norm.cdf(jnp.abs(self.tstat))) - self.confint = jnp.column_stack( - [ - self.beta - jax.scipy.stats.norm.ppf(1 - 0.05 / 2) * self.se, - self.beta + jax.scipy.stats.norm.ppf(1 - 0.05 / 2) * self.se, - ] - ) - - return self.se, self.tstat, self.pvalue, self.confint diff --git a/pyfixest/estimation/jax/olsjax_interface.py b/pyfixest/estimation/jax/olsjax_interface.py index a78b65d22..643b692a2 100644 --- a/pyfixest/estimation/jax/olsjax_interface.py +++ b/pyfixest/estimation/jax/olsjax_interface.py @@ -2,8 +2,10 @@ from pyfixest.estimation.feols_ import Feols, _drop_multicollinear_variables from pyfixest.estimation.FormulaParser import FixestFormula import pandas as pd +import numpy as np from typing import Union, Optional, Mapping, Any, Literal import jax.numpy as jnp +from pyfixest.estimation.jax.OLSJAX import OLSJAX class OLSJAX_API(Feols): @@ -51,15 +53,57 @@ def __init__( demeaner_backend=demeaner_backend, ) + self.prepare_model_matrix() + self.to_jax_array() + + # later to be set in multicoll method + self._N, self._k = self._X_jax.shape + + self.olsjax = OLSJAX( + X=self._X_jax, + Y=self._Y_jax, + fe=self._fe_jax, + weights=self._weights_jax, + vcov="iid", + ) + #import pdb; pdb.set_trace() + self.olsjax.Y, self.olsjax.X = self.olsjax.demean(Y = self._Y_jax, X = self._X_jax, fe = self._fe_jax, weights = self._weights_jax.flatten()) + + def to_jax_array(self): + + self._X_jax = jnp.array(self._X) + self._Y_jax = jnp.array(self._Y) + self._fe_jax = jnp.array(self._fe) + self._weights_jax = jnp.array(self._weights) + + + def get_fit(self): + + self.olsjax.get_fit() + self._beta_hat = self.olsjax.beta.flatten() + self._u_hat = self.olsjax.residuals + self._scores = self.olsjax.scores + + def vcov(self, type: str): + + self._vcov_type = type + self.olsjax.vcov(vcov_type=type) + self._vcov = self.olsjax.vcov + + return self + + def convert_attributes_to_numpy(self): + "Convert core attributes from jax to numpy arrays." + attr = ["_beta_hat", "_u_hat", "_scores", "_vcov"] + for a in attr: + # convert to numpy + setattr(self, a, np.array(getattr(self, a))) + + + + + - def to_array(self): - """ - Convert all relevant data to JAX arrays. - """ - self._Y = jnp.asarray(self._Y) - self._X = jnp.asarray(self._X) - self._fe = jnp.asarray(self._fe) - self._weights = jnp.asarray(self._weights) From 3f595cd32feca8d4d1d64496382b41532ebfb970 Mon Sep 17 00:00:00 2001 From: Alexander Fischer Date: Sun, 19 Jan 2025 15:31:42 +0100 Subject: [PATCH 6/6] delete GPU benchmarks --- benchmarks/gpu_pyfixest_errors.ipynb | 1478 -------------------------- 1 file changed, 1478 deletions(-) delete mode 100644 benchmarks/gpu_pyfixest_errors.ipynb diff --git a/benchmarks/gpu_pyfixest_errors.ipynb b/benchmarks/gpu_pyfixest_errors.ipynb deleted file mode 100644 index 945569cd9..000000000 --- a/benchmarks/gpu_pyfixest_errors.ipynb +++ /dev/null @@ -1,1478 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "execution": { - "iopub.execute_input": "2025-01-09T00:23:28.917621Z", - "iopub.status.busy": "2025-01-09T00:23:28.917332Z", - "iopub.status.idle": "2025-01-09T00:23:29.477701Z", - "shell.execute_reply": "2025-01-09T00:23:29.477193Z", - "shell.execute_reply.started": "2025-01-09T00:23:28.917602Z" - } - }, - "outputs": [ - { - "data": { - "text/plain": [ - "[CpuDevice(id=0)]" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import jax\n", - "import jax.numpy as jnp\n", - "\n", - "jax.devices()" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "execution": { - "iopub.execute_input": "2025-01-09T00:23:30.540594Z", - "iopub.status.busy": "2025-01-09T00:23:30.540228Z", - "iopub.status.idle": "2025-01-09T00:23:30.739685Z", - "shell.execute_reply": "2025-01-09T00:23:30.739213Z", - "shell.execute_reply.started": "2025-01-09T00:23:30.540574Z" - }, - "scrolled": true - }, - "outputs": [ - { - "data": { - "text/plain": [ - "{CpuDevice(id=0)}" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "jnp.ones(10).devices()" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "execution": { - "iopub.execute_input": "2025-01-09T00:26:29.239253Z", - "iopub.status.busy": "2025-01-09T00:26:29.238947Z", - "iopub.status.idle": "2025-01-09T00:26:29.754752Z", - "shell.execute_reply": "2025-01-09T00:26:29.754158Z", - "shell.execute_reply.started": "2025-01-09T00:26:29.239235Z" - } - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "'nvidia-smi' is not recognized as an internal or external command,\n", - "operable program or batch file.\n" - ] - } - ], - "source": [ - "!nvidia-smi" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "execution": { - "iopub.execute_input": "2025-01-09T00:23:44.232335Z", - "iopub.status.busy": "2025-01-09T00:23:44.231984Z", - "iopub.status.idle": "2025-01-09T00:23:45.388035Z", - "shell.execute_reply": "2025-01-09T00:23:45.387587Z", - "shell.execute_reply.started": "2025-01-09T00:23:44.232312Z" - }, - "id": "fHzEldNvR2_K" - }, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "
\n", - " \n", - " " - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "\n", - "
\n", - " \n", - " " - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import time\n", - "from itertools import product\n", - "\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "import pandas as pd\n", - "import seaborn as sns\n", - "from scipy.stats import nbinom\n", - "from tqdm import tqdm\n", - "\n", - "import pyfixest as pf\n", - "from pyfixest.estimation.demean_ import demean\n", - "from pyfixest.estimation.jax.demean_jax_ import demean_jax" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "execution": { - "iopub.execute_input": "2025-01-09T00:23:46.290548Z", - "iopub.status.busy": "2025-01-09T00:23:46.289898Z", - "iopub.status.idle": "2025-01-09T00:23:46.417097Z", - "shell.execute_reply": "2025-01-09T00:23:46.416504Z", - "shell.execute_reply.started": "2025-01-09T00:23:46.290525Z" - }, - "id": "XQjP2889YJxs", - "outputId": "3e686d7b-0774-4bb5-c1b9-28e5b9f286a9" - }, - "outputs": [], - "source": [ - "# %load_ext watermark\n", - "# %watermark --iversions" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "colab": { - "background_save": true - }, - "execution": { - "iopub.execute_input": "2025-01-09T00:23:49.545271Z", - "iopub.status.busy": "2025-01-09T00:23:49.545016Z", - "iopub.status.idle": "2025-01-09T00:23:49.552123Z", - "shell.execute_reply": "2025-01-09T00:23:49.551676Z", - "shell.execute_reply.started": "2025-01-09T00:23:49.545253Z" - }, - "id": "bxMmeyCxR3fb" - }, - "outputs": [], - "source": [ - "def generate_test_data(size: int, k: int = 2):\n", - " \"\"\"\n", - " Generate benchmark data for pyfixest on GPU (similar to the R fixest benchmark data).\n", - "\n", - " Args:\n", - " size (int): The number of observations in the data frame.\n", - " k (int): The number of covariates in the data frame.\n", - "\n", - " Returns\n", - " -------\n", - " pd.DataFrame: The generated data frame for the given size.\n", - " \"\"\"\n", - " # Constants\n", - " all_n = [1000 * 10**i for i in range(5)]\n", - " a = 1\n", - " b = 0.05\n", - "\n", - " n = all_n[size - 1]\n", - "\n", - " dum_all = []\n", - " nb_dum = [n // 20, int(np.sqrt(n)), int(n**0.33)]\n", - "\n", - " dum_all = np.zeros((n, 3))\n", - " dum_all[:, 0] = np.random.choice(nb_dum[0], n, replace=True)\n", - " dum_all[:, 1] = np.random.choice(nb_dum[1], n, replace=True)\n", - " dum_all[:, 2] = np.random.choice(nb_dum[2], n, replace=True)\n", - " dum_all = dum_all.astype(int)\n", - "\n", - " X1 = np.random.normal(size=n)\n", - " X2 = X1**2\n", - "\n", - " mu = a * X1 + b * X2\n", - "\n", - " for m in range(3):\n", - " coef_dum = np.random.normal(size=nb_dum[m])\n", - " mu += coef_dum[dum_all[:, m]]\n", - "\n", - " mu = np.exp(mu)\n", - " y = nbinom.rvs(0.5, 1 - (mu / (mu + 0.5)), size=n)\n", - "\n", - " X_full = np.column_stack((X1, X2))\n", - " base = pd.DataFrame(\n", - " {\n", - " \"y\": y,\n", - " \"ln_y\": np.log(y + 1),\n", - " \"X1\": X1,\n", - " \"X2\": X2,\n", - " }\n", - " )\n", - "\n", - " if k > 2:\n", - " X = np.random.normal(size=(n, k - 2))\n", - " X_df = pd.DataFrame(X, columns=[f\"X{i}\" for i in range(3, k + 1, 1)])\n", - " base = pd.concat([base, X_df], axis=1)\n", - " X_full = np.column_stack((X_full, X))\n", - "\n", - " for m in range(3):\n", - " base[f\"dum_{m + 1}\"] = dum_all[:, m]\n", - "\n", - " weights = np.random.uniform(0, 1, n)\n", - " return base, y, X_full, dum_all, weights" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "execution": { - "iopub.execute_input": "2025-01-09T00:23:50.285297Z", - "iopub.status.busy": "2025-01-09T00:23:50.284967Z", - "iopub.status.idle": "2025-01-09T00:23:50.460957Z", - "shell.execute_reply": "2025-01-09T00:23:50.460501Z", - "shell.execute_reply.started": "2025-01-09T00:23:50.285276Z" - }, - "id": "nzynhbqwR81H" - }, - "outputs": [], - "source": [ - "df, Y, X, f, weights = generate_test_data(1)" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "execution": { - "iopub.execute_input": "2025-01-09T00:25:02.873750Z", - "iopub.status.busy": "2025-01-09T00:25:02.873239Z", - "iopub.status.idle": "2025-01-09T00:25:03.153458Z", - "shell.execute_reply": "2025-01-09T00:25:03.153005Z", - "shell.execute_reply.started": "2025-01-09T00:25:02.873732Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "###\n", - "\n", - "Estimation: OLS\n", - "Dep. var.: ln_y, Fixed effects: dum_1\n", - "Inference: CRV1\n", - "Observations: 1000\n", - "\n", - "| Coefficient | Estimate | Std. Error | t value | Pr(>|t|) | 2.5% | 97.5% |\n", - "|:--------------|-----------:|-------------:|----------:|-----------:|-------:|--------:|\n", - "| X1 | 0.436 | 0.046 | 9.440 | 0.000 | 0.343 | 0.529 |\n", - "---\n", - "RMSE: 1.067 R2: 0.242 R2 Within: 0.131 \n" - ] - } - ], - "source": [ - "m0 = pf.feols(\"ln_y ~ X1 | dum_1\", df, demeaner_backend=\"numba\")\n", - "m0.summary()" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "execution": { - "iopub.execute_input": "2025-01-09T00:25:03.330646Z", - "iopub.status.busy": "2025-01-09T00:25:03.330367Z", - "iopub.status.idle": "2025-01-09T00:25:03.571916Z", - "shell.execute_reply": "2025-01-09T00:25:03.571482Z", - "shell.execute_reply.started": "2025-01-09T00:25:03.330629Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "###\n", - "\n", - "Estimation: OLS\n", - "Dep. var.: ln_y, Fixed effects: dum_1\n", - "Inference: CRV1\n", - "Observations: 1000\n", - "\n", - "| Coefficient | Estimate | Std. Error | t value | Pr(>|t|) | 2.5% | 97.5% |\n", - "|:--------------|-----------:|-------------:|----------:|-----------:|-------:|--------:|\n", - "| X1 | 0.436 | 0.046 | 9.440 | 0.000 | 0.343 | 0.529 |\n", - "---\n", - "RMSE: 1.067 R2: 0.242 R2 Within: 0.131 \n" - ] - } - ], - "source": [ - "m1 = pf.feols(\"ln_y ~ X1 | dum_1\", df, demeaner_backend=\"jax\")\n", - "m1.summary()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## function" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "execution": { - "iopub.execute_input": "2025-01-09T00:24:06.619552Z", - "iopub.status.busy": "2025-01-09T00:24:06.619273Z", - "iopub.status.idle": "2025-01-09T00:24:06.626298Z", - "shell.execute_reply": "2025-01-09T00:24:06.625727Z", - "shell.execute_reply.started": "2025-01-09T00:24:06.619534Z" - }, - "id": "29rZkULUR_A0" - }, - "outputs": [], - "source": [ - "def run_standard_benchmark(\n", - " fixed_effect,\n", - " demeaner_backend,\n", - " size=1,\n", - " k=1,\n", - " solver=\"np.linalg.lstsq\",\n", - " skip_demean_benchmark=True,\n", - "):\n", - " \"\"\"\n", - " Run the fixest standard benchmark fixed effect models. This is the function the benchmarks\n", - " will loop over.\n", - "\n", - " Args:\n", - " fixed_effect (str): The fixed effect to use. Must be a list of variables as \"dum_1\", \"dum_1+dum_2\", or \"dum_1+dum_2+dum_3\", etc.\n", - " demeaner_backend (str): The backend to use for demeaning. Must be \"numba\" or \"jax\".\n", - " size (int): The size of the data to generate. Must be between 1 and 5. For 1, N = 1000, for 2, N = 10000, etc.\n", - " k_vals (int): The number of covariates to generate.\n", - " solver (str): The solver to use for the estimation. Must be \"np.linalg.lstsq\". \"jax\" currently throws an error.\n", - " skip_demean_benchmark (bool): Whether to skip the \"pure\" demean benchmark. Default is True. Only the full call\n", - " to feols is benchmarked.\n", - "\n", - " \"\"\"\n", - " assert fixed_effect in [\"dum_1\", \"dum_1+dum_2\", \"dum_1+dum_2+dum_3\"]\n", - "\n", - " # one fixed effect\n", - " res = []\n", - "\n", - " fml_base = \"ln_y ~ X1\"\n", - " fml = f\"{fml_base} | {fixed_effect}\"\n", - "\n", - " # warmup\n", - " df, y, X, f, weights = generate_test_data(1)\n", - " pf.feols(\n", - " fml,\n", - " data=df,\n", - " demeaner_backend=demeaner_backend,\n", - " store_data=False,\n", - " copy_data=False,\n", - " solver=solver,\n", - " )\n", - "\n", - " if k > 1:\n", - " xfml = \"+\".join([f\"X{i}\" for i in range(2, k + 1, 1)])\n", - " fml = f\"{fml_base} + {xfml} | {fixed_effect}\"\n", - " else:\n", - " fml = f\"{fml_base} + X1 | {fixed_effect}\"\n", - "\n", - " for rep in range(1, 11):\n", - " df, Y, X, f, weights = generate_test_data(size=size, k=k)\n", - "\n", - " tic1 = time.time()\n", - " pf.feols(\n", - " fml,\n", - " data=df,\n", - " demeaner_backend=demeaner_backend,\n", - " store_data=False,\n", - " copy_data=False,\n", - " solver=solver,\n", - " )\n", - " tic2 = time.time()\n", - "\n", - " full_feols_timing = tic2 - tic1\n", - "\n", - " demean_timing = np.nan\n", - " if not skip_demean_benchmark:\n", - " YX = np.column_stack((Y.reshape(-1, 1), X))\n", - " tic3 = time.time()\n", - " if demeaner_backend == \"jax\":\n", - " _, _ = demean_jax(YX, f, weights, tol=1e-10)\n", - " else:\n", - " _, _ = demean(YX, f, weights, tol=1e-10)\n", - " tic4 = time.time()\n", - " demean_timing = tic4 - tic3\n", - "\n", - " res.append(\n", - " pd.Series(\n", - " {\n", - " \"method\": \"feols\",\n", - " \"solver\": solver,\n", - " \"demeaner_backend\": demeaner_backend,\n", - " \"n_obs\": df.shape[0],\n", - " \"k\": k,\n", - " \"G\": len(fixed_effect.split(\"+\")),\n", - " \"rep\": rep,\n", - " \"full_feols_timing\": full_feols_timing,\n", - " \"demean_timing\": demean_timing,\n", - " }\n", - " )\n", - " )\n", - "\n", - " return pd.concat(res, axis=1).T" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": { - "execution": { - "iopub.execute_input": "2025-01-09T00:28:43.818536Z", - "iopub.status.busy": "2025-01-09T00:28:43.818246Z", - "iopub.status.idle": "2025-01-09T00:28:51.489202Z", - "shell.execute_reply": "2025-01-09T00:28:51.488591Z", - "shell.execute_reply.started": "2025-01-09T00:28:43.818520Z" - } - }, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
methodsolverdemeaner_backendn_obskGrepfull_feols_timingdemean_timing
0feolsnp.linalg.lstsqnumba10001110.150473NaN
1feolsnp.linalg.lstsqnumba10001120.147583NaN
2feolsnp.linalg.lstsqnumba10001130.186491NaN
3feolsnp.linalg.lstsqnumba10001140.190972NaN
4feolsnp.linalg.lstsqnumba10001150.162773NaN
5feolsnp.linalg.lstsqnumba10001160.171777NaN
6feolsnp.linalg.lstsqnumba10001170.166872NaN
7feolsnp.linalg.lstsqnumba10001180.158694NaN
8feolsnp.linalg.lstsqnumba10001190.185547NaN
9feolsnp.linalg.lstsqnumba100011100.158114NaN
\n", - "
" - ], - "text/plain": [ - " method solver demeaner_backend n_obs k G rep full_feols_timing \\\n", - "0 feols np.linalg.lstsq numba 1000 1 1 1 0.150473 \n", - "1 feols np.linalg.lstsq numba 1000 1 1 2 0.147583 \n", - "2 feols np.linalg.lstsq numba 1000 1 1 3 0.186491 \n", - "3 feols np.linalg.lstsq numba 1000 1 1 4 0.190972 \n", - "4 feols np.linalg.lstsq numba 1000 1 1 5 0.162773 \n", - "5 feols np.linalg.lstsq numba 1000 1 1 6 0.171777 \n", - "6 feols np.linalg.lstsq numba 1000 1 1 7 0.166872 \n", - "7 feols np.linalg.lstsq numba 1000 1 1 8 0.158694 \n", - "8 feols np.linalg.lstsq numba 1000 1 1 9 0.185547 \n", - "9 feols np.linalg.lstsq numba 1000 1 1 10 0.158114 \n", - "\n", - " demean_timing \n", - "0 NaN \n", - "1 NaN \n", - "2 NaN \n", - "3 NaN \n", - "4 NaN \n", - "5 NaN \n", - "6 NaN \n", - "7 NaN \n", - "8 NaN \n", - "9 NaN " - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# test run numba\n", - "run_standard_benchmark(fixed_effect=\"dum_1\", demeaner_backend=\"numba\", size=1, k=1)" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": { - "execution": { - "iopub.execute_input": "2025-01-09T00:28:43.818536Z", - "iopub.status.busy": "2025-01-09T00:28:43.818246Z", - "iopub.status.idle": "2025-01-09T00:28:51.489202Z", - "shell.execute_reply": "2025-01-09T00:28:51.488591Z", - "shell.execute_reply.started": "2025-01-09T00:28:43.818520Z" - } - }, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
methodsolverdemeaner_backendn_obskGrepfull_feols_timingdemean_timing
0feolsnp.linalg.lstsqjax10001110.122831NaN
1feolsnp.linalg.lstsqjax10001120.122887NaN
2feolsnp.linalg.lstsqjax10001130.136041NaN
3feolsnp.linalg.lstsqjax10001140.139644NaN
4feolsnp.linalg.lstsqjax10001150.136235NaN
5feolsnp.linalg.lstsqjax10001160.122477NaN
6feolsnp.linalg.lstsqjax10001170.123122NaN
7feolsnp.linalg.lstsqjax10001180.119589NaN
8feolsnp.linalg.lstsqjax10001190.122247NaN
9feolsnp.linalg.lstsqjax100011100.118353NaN
\n", - "
" - ], - "text/plain": [ - " method solver demeaner_backend n_obs k G rep full_feols_timing \\\n", - "0 feols np.linalg.lstsq jax 1000 1 1 1 0.122831 \n", - "1 feols np.linalg.lstsq jax 1000 1 1 2 0.122887 \n", - "2 feols np.linalg.lstsq jax 1000 1 1 3 0.136041 \n", - "3 feols np.linalg.lstsq jax 1000 1 1 4 0.139644 \n", - "4 feols np.linalg.lstsq jax 1000 1 1 5 0.136235 \n", - "5 feols np.linalg.lstsq jax 1000 1 1 6 0.122477 \n", - "6 feols np.linalg.lstsq jax 1000 1 1 7 0.123122 \n", - "7 feols np.linalg.lstsq jax 1000 1 1 8 0.119589 \n", - "8 feols np.linalg.lstsq jax 1000 1 1 9 0.122247 \n", - "9 feols np.linalg.lstsq jax 1000 1 1 10 0.118353 \n", - "\n", - " demean_timing \n", - "0 NaN \n", - "1 NaN \n", - "2 NaN \n", - "3 NaN \n", - "4 NaN \n", - "5 NaN \n", - "6 NaN \n", - "7 NaN \n", - "8 NaN \n", - "9 NaN " - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# test run jax\n", - "run_standard_benchmark(fixed_effect=\"dum_1\", demeaner_backend=\"jax\", size=1, k=1)" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": { - "execution": { - "iopub.execute_input": "2025-01-09T00:25:39.248695Z", - "iopub.status.busy": "2025-01-09T00:25:39.248317Z", - "iopub.status.idle": "2025-01-09T00:25:39.253652Z", - "shell.execute_reply": "2025-01-09T00:25:39.253000Z", - "shell.execute_reply.started": "2025-01-09T00:25:39.248671Z" - }, - "id": "HxsGRMSlR_jI" - }, - "outputs": [], - "source": [ - "def run_all_benchmarks(size_list, k_list):\n", - " \"\"\"\n", - " Run all the benchmarks.\n", - "\n", - " Args:\n", - " size_list (list): The list of sizes to run the benchmarks on. 1-> 1000, 2-> 10000, ..., 5-> 10_000_000\n", - " k_list (list): The list of k values to run the benchmarks on.\n", - " \"\"\"\n", - " res = pd.DataFrame()\n", - "\n", - " all_combinations = list(\n", - " product(\n", - " [\"numba\", \"jax\"], # demeaner_backend\n", - " [\"dum_1\", \"dum_1+dum_2\", \"dum_1+dum_2+dum_3\"], # fixef\n", - " size_list, # size\n", - " k_list, # k\n", - " [\"np.linalg.lstsq\"], # solver\n", - " )\n", - " )\n", - "\n", - " with tqdm(total=len(all_combinations), desc=\"Running Benchmarks\") as pbar:\n", - " for demeaner_backend, fixef, size, k, solver in all_combinations:\n", - " res = pd.concat(\n", - " [\n", - " res,\n", - " run_standard_benchmark(\n", - " solver=solver,\n", - " fixed_effect=fixef,\n", - " demeaner_backend=demeaner_backend,\n", - " size=size,\n", - " k=k,\n", - " ),\n", - " ],\n", - " axis=0,\n", - " )\n", - " pbar.update(1) # Update the progress bar after each iteration\n", - "\n", - " return res" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Run Benchmarks" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": { - "colab": { - "background_save": true, - "base_uri": "https://localhost:8080/" - }, - "execution": { - "iopub.execute_input": "2025-01-09T00:25:39.962554Z", - "iopub.status.busy": "2025-01-09T00:25:39.962039Z", - "iopub.status.idle": "2025-01-09T00:26:25.319310Z", - "shell.execute_reply": "2025-01-09T00:26:25.318687Z", - "shell.execute_reply.started": "2025-01-09T00:25:39.962536Z" - }, - "id": "gki1mlqvSEIi", - "outputId": "3cb40095-df81-4e78-99a6-2410da237884" - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Running Benchmarks: 100%|██████████| 24/24 [00:49<00:00, 2.06s/it]\n" - ] - } - ], - "source": [ - "res_all = run_all_benchmarks(\n", - " size_list=[1, 2, 3, 4, 5], # for N = 1000, 10_000, 100_000, 1_000_000, 10_000_000\n", - " k_list=[1, 10, 50, 100], # for k = 1, 10, 50, 100\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": { - "colab": { - "background_save": true, - "base_uri": "https://localhost:8080/" - }, - "execution": { - "iopub.execute_input": "2025-01-09T00:26:25.320894Z", - "iopub.status.busy": "2025-01-09T00:26:25.320596Z", - "iopub.status.idle": "2025-01-09T00:26:26.391854Z", - "shell.execute_reply": "2025-01-09T00:26:26.391236Z", - "shell.execute_reply.started": "2025-01-09T00:26:25.320871Z" - }, - "id": "7zEIHj5nXXvq", - "outputId": "238dedce-a9f5-4a1b-b9a2-b84d4a89c091" - }, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
methoddemeaner_backendkGn_obsfull_feols_timingdemean_timing
0feolsjax1110000.127274NaN
1feolsjax11100000.142241NaN
2feolsjax1210000.134405NaN
3feolsjax12100000.168555NaN
4feolsjax1310000.139142NaN
5feolsjax13100000.189492NaN
6feolsjax5110000.144933NaN
7feolsjax51100000.147049NaN
8feolsjax5210000.147216NaN
9feolsjax52100000.175394NaN
10feolsjax5310000.15034NaN
11feolsjax53100000.194496NaN
12feolsnumba1110000.153849NaN
13feolsnumba11100000.183697NaN
14feolsnumba1210000.17201NaN
15feolsnumba12100000.16131NaN
16feolsnumba1310000.169463NaN
17feolsnumba13100000.169789NaN
18feolsnumba5110000.331457NaN
19feolsnumba51100000.172194NaN
20feolsnumba5210000.162562NaN
21feolsnumba52100000.184233NaN
22feolsnumba5310000.174623NaN
23feolsnumba53100000.166674NaN
\n", - "
" - ], - "text/plain": [ - " method demeaner_backend k G n_obs full_feols_timing demean_timing\n", - "0 feols jax 1 1 1000 0.127274 NaN\n", - "1 feols jax 1 1 10000 0.142241 NaN\n", - "2 feols jax 1 2 1000 0.134405 NaN\n", - "3 feols jax 1 2 10000 0.168555 NaN\n", - "4 feols jax 1 3 1000 0.139142 NaN\n", - "5 feols jax 1 3 10000 0.189492 NaN\n", - "6 feols jax 5 1 1000 0.144933 NaN\n", - "7 feols jax 5 1 10000 0.147049 NaN\n", - "8 feols jax 5 2 1000 0.147216 NaN\n", - "9 feols jax 5 2 10000 0.175394 NaN\n", - "10 feols jax 5 3 1000 0.15034 NaN\n", - "11 feols jax 5 3 10000 0.194496 NaN\n", - "12 feols numba 1 1 1000 0.153849 NaN\n", - "13 feols numba 1 1 10000 0.183697 NaN\n", - "14 feols numba 1 2 1000 0.17201 NaN\n", - "15 feols numba 1 2 10000 0.16131 NaN\n", - "16 feols numba 1 3 1000 0.169463 NaN\n", - "17 feols numba 1 3 10000 0.169789 NaN\n", - "18 feols numba 5 1 1000 0.331457 NaN\n", - "19 feols numba 5 1 10000 0.172194 NaN\n", - "20 feols numba 5 2 1000 0.162562 NaN\n", - "21 feols numba 5 2 10000 0.184233 NaN\n", - "22 feols numba 5 3 1000 0.174623 NaN\n", - "23 feols numba 5 3 10000 0.166674 NaN" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "df = (\n", - " res_all.drop([\"rep\", \"solver\"], axis=1)\n", - " .groupby([\"method\", \"demeaner_backend\", \"k\", \"G\", \"n_obs\"])\n", - " .mean()\n", - " .reset_index()\n", - ")\n", - "df" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab": { - "background_save": true - }, - "id": "VCn6O5MMXlBw" - }, - "source": [ - "## Visualize" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "INFO:matplotlib.category:Using categorical units to plot a list of strings that are all parsable as floats or dates. If these strings should be plotted as numbers, cast to the appropriate data type before plotting.\n", - "INFO:matplotlib.category:Using categorical units to plot a list of strings that are all parsable as floats or dates. If these strings should be plotted as numbers, cast to the appropriate data type before plotting.\n", - "INFO:matplotlib.category:Using categorical units to plot a list of strings that are all parsable as floats or dates. If these strings should be plotted as numbers, cast to the appropriate data type before plotting.\n", - "INFO:matplotlib.category:Using categorical units to plot a list of strings that are all parsable as floats or dates. If these strings should be plotted as numbers, cast to the appropriate data type before plotting.\n", - "INFO:matplotlib.category:Using categorical units to plot a list of strings that are all parsable as floats or dates. If these strings should be plotted as numbers, cast to the appropriate data type before plotting.\n", - "INFO:matplotlib.category:Using categorical units to plot a list of strings that are all parsable as floats or dates. If these strings should be plotted as numbers, cast to the appropriate data type before plotting.\n", - "INFO:matplotlib.category:Using categorical units to plot a list of strings that are all parsable as floats or dates. If these strings should be plotted as numbers, cast to the appropriate data type before plotting.\n", - "INFO:matplotlib.category:Using categorical units to plot a list of strings that are all parsable as floats or dates. If these strings should be plotted as numbers, cast to the appropriate data type before plotting.\n", - "INFO:matplotlib.category:Using categorical units to plot a list of strings that are all parsable as floats or dates. If these strings should be plotted as numbers, cast to the appropriate data type before plotting.\n", - "INFO:matplotlib.category:Using categorical units to plot a list of strings that are all parsable as floats or dates. If these strings should be plotted as numbers, cast to the appropriate data type before plotting.\n", - "INFO:matplotlib.category:Using categorical units to plot a list of strings that are all parsable as floats or dates. If these strings should be plotted as numbers, cast to the appropriate data type before plotting.\n", - "INFO:matplotlib.category:Using categorical units to plot a list of strings that are all parsable as floats or dates. If these strings should be plotted as numbers, cast to the appropriate data type before plotting.\n" - ] - }, - { - "data": { - "image/png": "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OHfunBEREWH4+/sb33///Q33BQAAwJlxRgUAAHAKkZGRKlmypIKDg9WxY0d5eXnps88+01133WX3mJ07d7Y5IyPrr9YPHjx4U7U+9dRT1p9dXV1Vr149GYahnj17WtuLFi2qu+++22ZbH374oe655x5VqVJFp0+ftj4efPBBSbK5BM5/WSwWPfbYY1q9erXOnz9vbV+6dKnKlCmjxo0bW7crXb0hdk6XlsmtrLMq/vuXyDejatWqatiwofV5eHi4JOnBBx9U2bJls7Xn9D716dPH+nPWpYjS09P11VdfSbr6Gvv5+al58+Y2r3FYWJi8vb2zvcbly5dXVFTUDWvfvn27Tp48qd69e9vck6F169aqUqVKjmc83Mi5c+ckXb3c1vVkLU9JSbFp/7//+z+b588//7ykqzeIl/75LHz66afZzk7JktfPZEREhKpWrWrT9uCDD8rf319Lly61tv39999au3atzVkZrq6u1nshZGZm6q+//tKVK1dUr169XJ+t8F9Z+xoXF2fT/sILL0hStvelatWqNmevlCxZMttxWrRoUf3888/at2+fXTX16tVLhQsXtj5/7rnnVKhQIWutLi4u6tatmz777DPrZ0CSFi9erEaNGql8+fI33Ia3t7fNmRdubm5q0KDBTf9uu55evXopKipKzz//vJ588kmFhoZq7Nix1uXLly9XZmamOnXqZPNZCgwMVKVKla77++16/n3PmEuXLun06dO69957JSnHz82/z/qQrv7eP3PmjPX4yXof+vbta9Ovf//+eaorOTlZLVq00K+//qrExMTrno0FAABQEBBUAAAApzBz5kytXbtWH330kR566CGdPn36pm/o+u8vv6V/LiP133sV3Oy4fn5+8vDwkL+/f7b2f29r3759+vnnn1WyZEmbR+XKlSXJ5vrvOencubMuXrxovcn1+fPntXr1aj322GPW6/ZHRETo0Ucf1ciRI+Xv76/27dvrnXfeueE9MP7Lz89P/fv312effXbN+0XkVU6vmyQFBwfn2P7f98nFxSXbDaWzXrusS4Xt27dPycnJKlWqVLbX+fz589le49x8KSxJv//+uyTleP+AKlWqWJfnRVYA8e8vq3NyrUCjUqVKNs9DQ0Pl4uJifS06d+6s++67T0899ZQCAgLUpUsXLVu2zCa0yOtnMqfXq1ChQnr00Uf16aefWj9n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w4dLkipVqqQZM2YoISFBzZs3v+56JUuWlLu7uzw9Pa97FsLbb7+tmjVrqm/fvlq+fLlGjBihsLAwSdKMGTNUp04dm3+Yzp8/X8HBwfrtt99UuXJl+fj4yNXV9brbKF269A1v3MYXNADg/JjTslu2bJm2bdumt99+O9frAADyB+a1fzRq1Eg7d+5UWlqaevXqpVGjRt1wHQCA8yOoAJxIzZo1bZ4HBQXp5MmTDhu/WLFimjdvnqKiotSoUSMNHjzYumz37t1at26dvL29s6134MABVa5cOVfbKFSokCpWrOiwmgEAdybmNFvr1q1TbGys5syZo2rVqjlkTADA7cO89o+lS5fq3Llz2r17twYOHKhJkyZp0KBBNz0uAODORlABOJHChQvbPLdYLMrMzHToNjZs2CBXV1cdP35cqamp8vHxkSSdP39ebdu21YQJE7KtExQUlOvxjxw5oqpVq163z8svv6yXX345b4UDAO4ozGn/WL9+vdq2baspU6YoOjo619sHAOQfzGv/CA4OliRVrVpVGRkZ6tWrl1544QW5urrmuhYAgPMhqACQa5s2bdKECRO0cuVKvfTSS+rTp48WLlwoSapbt64+/vhjhYSEqFAh+3+1cOknAMDtcKfMaYmJiWrTpo0mTJigXr162V0LAMC53Snz2n9lZmbq8uXLyszMJKgAgAKOoAJArpw7d05PPvmk+vbtq1atWumuu+5S/fr11bZtW3Xs2FH/93//pzlz5qhr164aNGiQihcvrv3792vJkiWaO3durv/R6YjTiX/55Relp6frr7/+0rlz56z/mK5du/ZNjQsAcA53ypy2bt06tWnTRv369dOjjz6qpKQkSZKbmxuhPQDA6k6Z1xYvXqzChQurRo0acnd31/bt2zVkyBB17tw52xknAICCh6ACQK7069dPXl5e1huw1ahRQ2PHjtUzzzyjhg0bqkyZMtq4caNeeukltWjRQmlpaSpXrpxatmwpFxeX21rrQw89pN9//936vE6dOpIkwzBuax0AgPzpTpnTFi5cqAsXLmjcuHEaN26ctT0iIkKJiYm3rQ4AQP52p8xrhQoV0oQJE/Tbb7/JMAyVK1dOffr00YABA25bDQCA/Mti8M0dAAAAAAAAAAAwye39M2cAAAAAAAAAAIB/IagACoDFixfL29s7x0e1atXMLg8AgFxjTgMAOBPm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JibaH4Z9/PhxeXn9cuHHm2++qezsbPXp08dhnYkTJ2rSpEmSpBdeeEFXr17Vk08+qYsXL6pz585KTEws0nMsAAAAAAAAkNear69YXYJH+f/al7e6BAC462zGGGN1Ee4mMzNTFStW1KVLl+Tv7291ObiNc0nLrS7BY1TtPsDqEgDcQfQ1AICnoKcB7omgwrUIKgCURE49o0KSjhw5ovHjx+vxxx/X6dOnJUmff/65vv32W5cVBwAAAAAAAAAAPJtTt37auHGjevbsqfvvv1+bNm3Syy+/rBo1amjPnj1atGiR/vWvf7m6TgAAAAAAAAAodrp166aC3tRm/fr1d7gawD05FVSMGzdOL7/8smJjY1WhQgX7eLdu3TR37lyXFQcAAAAAAAAAxVmrVq3sPx85ckQ7d+5U3759rSsIcENOBRV79+7VBx98kGe8Ro0aOnv2bJGLAgAAAAAAAABPkJCQIEn67rvv9NBDD+nMmTOqX7++nn76aYsrA9yHU8+oqFSpkk6dOpVnfNeuXapVq1aRiwIAAAAAAAAAT3Ho0CF16dJFjz76qDZv3qzx48fn+4vgQEnlVFAxYMAAvfjii0pPT5fNZlNubq42b96s559/XtHR0a6uEQAAAAAAAACKpdTUVD300EP64x//qPnz56tt27b6+OOP9fTTT+uzzz6zujzALTgVVLz66qtq3LixgoODdeXKFTVt2lQPPvigOnXqpPHjx7u6RgAAAAAAAAAolh566CH169dP//jHP+xjnTt31rJlyzRw4EALKwPch1PPqPDx8dHChQsVHx+vvXv36sqVK2rdurUaNGjg6voAAAAA4DedS1pudQkepWr3AVaXAACARxk0aJBmzpyZZ7xnz5568803LagIcD9OBRW3BAcHKzg4WDk5Odq7d68uXLigypUru6o2AAAAAAAAACjW8gspbomKirqLlQDuy6lbP40aNUqLFi2SJOXk5KhLly5q06aNgoODtWHDBlfWBwAAAAAAAAAAPJhTQcW//vUvtWzZUpL0ySef6Pvvv1dqaqpGjx6tv//97y4tEAAAAAAAAAAAeC6ngoqzZ88qMDBQkrRmzRr169dPDRs21NChQ7V3716XFggAAAAAAAAAADyXU0FFQECA9u/fr5ycHCUmJurhhx+WJF27dk3e3t4uLRAAAAAAAAAAAHgupx6mHRMTo379+qlm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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "df[\"G\"] = df[\"G\"].map({1: \"n_fixef = 1\", 2: \"n_fixef = 2\", 3: \"n_fixef = 3\"})\n", - "df[\"n_obs\"] = df[\"n_obs\"].astype(str)\n", - "\n", - "# Dynamically determine unique values for order and hue_order\n", - "n_obs_order = sorted(df[\"n_obs\"].unique(), key=lambda x: int(x)) # Sort as integers\n", - "demeaner_backend_order = df[\"demeaner_backend\"].unique()\n", - "\n", - "custom_palette = sns.color_palette(\"coolwarm\", n_colors=2)\n", - "\n", - "# Create the FacetGrid with reordered columns and rows\n", - "g = sns.FacetGrid(\n", - " df,\n", - " col=\"G\", # G (n_fixef) increases left to right\n", - " row=\"k\", # k increases top to bottom\n", - " margin_titles=True,\n", - " height=4,\n", - " aspect=1.2,\n", - " col_order=[\"n_fixef = 1\", \"n_fixef = 2\", \"n_fixef = 3\"], # Ensure correct order\n", - ")\n", - "\n", - "# Plot the bar chart for each facet with the custom palette\n", - "g.map(\n", - " sns.barplot,\n", - " \"n_obs\",\n", - " \"full_feols_timing\",\n", - " \"demeaner_backend\",\n", - " order=n_obs_order, # Dynamic order for n_obs\n", - " hue_order=demeaner_backend_order, # Dynamic hue order for demeaner_backend\n", - " errorbar=None, # Suppress error bars\n", - " palette=custom_palette,\n", - ")\n", - "\n", - "# Add legend and adjust layout\n", - "g.add_legend(title=\"Demeaner Backend\")\n", - "g.set_axis_labels(\"Number of Observations\", \"Runtime (seconds)\")\n", - "g.set_titles(row_template=\"k = {row_name}\", col_template=\"{col_name}\")\n", - "plt.subplots_adjust(top=0.9)\n", - "g.fig.suptitle(\"Runtime vs Number of Observations by n_fixef and k\")\n", - "\n", - "# Show plot\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "colab": { - "provenance": [] - }, - "kernelspec": { - "display_name": "jax", - "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.8" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -}