|
|
@@ -0,0 +1,205 @@
|
|
|
+{
|
|
|
+ "cells": [
|
|
|
+ {
|
|
|
+ "cell_type": "markdown",
|
|
|
+ "id": "58d00ccd-7950-4367-aec8-2c18aef079d1",
|
|
|
+ "metadata": {},
|
|
|
+ "source": [
|
|
|
+ "# Fisher exact tests\n",
|
|
|
+ "\n",
|
|
|
+ "<br>\n",
|
|
|
+ "Author: Martin Horvat, March 2026\n",
|
|
|
+ "\n",
|
|
|
+ "Ref:\n",
|
|
|
+ "* https://en.wikipedia.org/wiki/Fisher%27s_exact_test\n",
|
|
|
+ "* https://mathworld.wolfram.com/FishersExactTest.html"
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "cell_type": "markdown",
|
|
|
+ "id": "9b24f6f9-4bf2-4751-8312-f011afd3422c",
|
|
|
+ "metadata": {},
|
|
|
+ "source": [
|
|
|
+ "## Common"
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "cell_type": "code",
|
|
|
+ "execution_count": 18,
|
|
|
+ "id": "362e9a29-2caf-4155-8d90-a591005b4b25",
|
|
|
+ "metadata": {},
|
|
|
+ "outputs": [],
|
|
|
+ "source": [
|
|
|
+ "import numpy as np\n",
|
|
|
+ "from scipy.special import gammaln\n",
|
|
|
+ "\n",
|
|
|
+ "def _log_table_prob(a, b, c, d):\n",
|
|
|
+ " r1, r2 = a + b, c + d\n",
|
|
|
+ " c1, c2 = a + c, b + d\n",
|
|
|
+ " n = r1 + r2\n",
|
|
|
+ " return (\n",
|
|
|
+ " gammaln(r1 + 1) + gammaln(r2 + 1) + gammaln(c1 + 1) + gammaln(c2 + 1)\n",
|
|
|
+ " - (gammaln(a + 1) + gammaln(b + 1) + gammaln(c + 1) + gammaln(d + 1) + gammaln(n + 1))\n",
|
|
|
+ " )\n",
|
|
|
+ "\n",
|
|
|
+ "def calculate_table_prob(a, b, c, d):\n",
|
|
|
+ " return float(np.exp(_log_table_prob(int(a), int(b), int(c), int(d))))\n",
|
|
|
+ "\n",
|
|
|
+ "def arange_at_margins(r, c):\n",
|
|
|
+ " amin = max(0, r[0] - c[1])\n",
|
|
|
+ " amax = min(r[0], c[0])\n",
|
|
|
+ " return np.arange(amin, amax + 1, dtype=np.int64)\n",
|
|
|
+ "\n",
|
|
|
+ "def fisher_p_value(table, eps: float = 1e-12):\n",
|
|
|
+ " mat = np.asarray(table, dtype=np.int64)\n",
|
|
|
+ " if mat.shape != (2, 2) or np.any(mat < 0):\n",
|
|
|
+ " raise ValueError(\"table must be a 2x2 array of nonnegative counts\")\n",
|
|
|
+ "\n",
|
|
|
+ " a, b, c_, d = map(int, mat.ravel())\n",
|
|
|
+ " r = mat.sum(axis=1) # [r1, r2]\n",
|
|
|
+ " c = mat.sum(axis=0) # [c1, c2]\n",
|
|
|
+ "\n",
|
|
|
+ " a_vals = arange_at_margins(r, c)\n",
|
|
|
+ "\n",
|
|
|
+ " n = int(r[0] + r[1])\n",
|
|
|
+ " const = (\n",
|
|
|
+ " gammaln(int(r[0]) + 1) + gammaln(int(r[1]) + 1)\n",
|
|
|
+ " + gammaln(int(c[0]) + 1) + gammaln(int(c[1]) + 1)\n",
|
|
|
+ " - gammaln(n + 1)\n",
|
|
|
+ " )\n",
|
|
|
+ "\n",
|
|
|
+ " b_vals = r[0] - a_vals\n",
|
|
|
+ " c_vals = c[0] - a_vals\n",
|
|
|
+ " d_vals = r[1] - c_vals\n",
|
|
|
+ "\n",
|
|
|
+ " logp = const - (\n",
|
|
|
+ " gammaln(a_vals + 1) + gammaln(b_vals + 1)\n",
|
|
|
+ " + gammaln(c_vals + 1) + gammaln(d_vals + 1)\n",
|
|
|
+ " )\n",
|
|
|
+ "\n",
|
|
|
+ " logp_obs = _log_table_prob(a, b, c_, d)\n",
|
|
|
+ " observed_p = float(np.exp(logp_obs))\n",
|
|
|
+ "\n",
|
|
|
+ " mask = logp <= (logp_obs + np.log1p(eps))\n",
|
|
|
+ " two_sided_p = float(np.exp(logp[mask]).sum())\n",
|
|
|
+ "\n",
|
|
|
+ " return observed_p, two_sided_p"
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "cell_type": "markdown",
|
|
|
+ "id": "3b5109a3-acf0-4285-8cfd-227074cfb015",
|
|
|
+ "metadata": {},
|
|
|
+ "source": [
|
|
|
+ "## Check"
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "cell_type": "code",
|
|
|
+ "execution_count": 16,
|
|
|
+ "id": "9d0b1232-ff1a-41da-965e-7f27ed61cd77",
|
|
|
+ "metadata": {},
|
|
|
+ "outputs": [
|
|
|
+ {
|
|
|
+ "name": "stdout",
|
|
|
+ "output_type": "stream",
|
|
|
+ "text": [
|
|
|
+ "[0.01048951048951043, 0.11013986013986005, 0.33041958041957936, 0.3671328671328668, 0.1573426573426565, 0.023601398601398604, 0.0008741258741258679]\n",
|
|
|
+ "0.9999999999999976\n"
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ "source": [
|
|
|
+ "# Check \n",
|
|
|
+ "table = [[8, 2], [1, 5]]\n",
|
|
|
+ "\n",
|
|
|
+ "# margins\n",
|
|
|
+ "r = np.sum(table, axis = 1)\n",
|
|
|
+ "c = np.sum(table, axis = 0)\n",
|
|
|
+ "\n",
|
|
|
+ "# values of a\n",
|
|
|
+ "avals = arange_mat_margins(r, c)\n",
|
|
|
+ "\n",
|
|
|
+ "probs = [prob_mat_margins(x, r, c) for x in avals]\n",
|
|
|
+ "\n",
|
|
|
+ "print(probs)\n",
|
|
|
+ "print(sum(probs))"
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "cell_type": "markdown",
|
|
|
+ "id": "218076bd-89d8-4305-8656-41cde67de1b4",
|
|
|
+ "metadata": {},
|
|
|
+ "source": [
|
|
|
+ "## Example"
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "cell_type": "code",
|
|
|
+ "execution_count": 19,
|
|
|
+ "id": "5b861593-4786-4b00-bdce-da1710b68af1",
|
|
|
+ "metadata": {},
|
|
|
+ "outputs": [
|
|
|
+ {
|
|
|
+ "name": "stdout",
|
|
|
+ "output_type": "stream",
|
|
|
+ "text": [
|
|
|
+ "Odds Ratio: 20.0000\n",
|
|
|
+ "P-value: 0.0350\n",
|
|
|
+ "\n",
|
|
|
+ "Result is statistically significant (Reject H0)\n",
|
|
|
+ "\n",
|
|
|
+ "Observed Table Probability: 0.0236\n",
|
|
|
+ "Two-Sided P-Value: 0.0350\n"
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ "source": [
|
|
|
+ "# 2x2 Contingency Table\n",
|
|
|
+ "# Success | Failure\n",
|
|
|
+ "# Treatment A: 8 | 2\n",
|
|
|
+ "# Treatment B: 1 | 5\n",
|
|
|
+ "table = [[8, 2], [1, 5]]\n",
|
|
|
+ "\n",
|
|
|
+ "# Perform the test\n",
|
|
|
+ "# returns the odds ratio and the p-value\n",
|
|
|
+ "odds_ratio, p_value = scipy.stats.fisher_exact(table)\n",
|
|
|
+ "\n",
|
|
|
+ "print(f\"Odds Ratio: {odds_ratio:.4f}\")\n",
|
|
|
+ "print(f\"P-value: {p_value:.4f}\")\n",
|
|
|
+ "\n",
|
|
|
+ "# Interpretation\n",
|
|
|
+ "alpha = 0.05\n",
|
|
|
+ "if p_value < alpha:\n",
|
|
|
+ " print(\"\\nResult is statistically significant (Reject H0)\")\n",
|
|
|
+ "else:\n",
|
|
|
+ " print(\"\\nResult is not statistically significant (Fail to reject H0)\")\n",
|
|
|
+ "\n",
|
|
|
+ "obs_p, final_p = fisher_p_value(table)\n",
|
|
|
+ "print(f\"\\nObserved Table Probability: {obs_p:.4f}\")\n",
|
|
|
+ "print(f\"Two-Sided P-Value: {final_p:.4f}\")\n"
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ "metadata": {
|
|
|
+ "kernelspec": {
|
|
|
+ "display_name": "Python 3 (ipykernel)",
|
|
|
+ "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.10.12"
|
|
|
+ }
|
|
|
+ },
|
|
|
+ "nbformat": 4,
|
|
|
+ "nbformat_minor": 5
|
|
|
+}
|