{"id":51462031,"url":"https://github.com/bionetslab/bosperrus-package","last_synced_at":"2026-07-06T06:31:23.346Z","repository":{"id":354129612,"uuid":"1189522705","full_name":"bionetslab/bosperrus-package","owner":"bionetslab","description":null,"archived":false,"fork":false,"pushed_at":"2026-06-25T08:11:35.000Z","size":160,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-06-25T10:08:34.129Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bionetslab.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-23T12:07:23.000Z","updated_at":"2026-06-25T08:11:39.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/bionetslab/bosperrus-package","commit_stats":null,"previous_names":["bionetslab/bosporus-package","bionetslab/bosperrus-package"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/bionetslab/bosperrus-package","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bionetslab%2Fbosperrus-package","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bionetslab%2Fbosperrus-package/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bionetslab%2Fbosperrus-package/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bionetslab%2Fbosperrus-package/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bionetslab","download_url":"https://codeload.github.com/bionetslab/bosperrus-package/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bionetslab%2Fbosperrus-package/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35180933,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-06T02:00:07.184Z","response_time":106,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-06T06:31:22.438Z","updated_at":"2026-07-06T06:31:23.340Z","avatar_url":"https://github.com/bionetslab.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# BOSPERRUS: BOundary effects in SPatial graphs: ERRor modeling and Untangling Strategies\n\n## Tutorial — Getting Started with `Flow`\n\nBOSPERRUS detects and corrects **boundary effects** in spatial graphs. Nodes close to the border of a tissue sample or point cloud tend to have systematically lower centrality scores than nodes in the center — not because they are biologically different, but because they have fewer neighbours. BOSPERRUS models this distance-dependent bias and optionally corrects for it.\n\n---\n\n## Table of Contents\n\n1. [Installation](#installation)\n2. [Key concepts](#key-concepts)\n3. [Quick-start](#quick-start)\n4. [Step-by-step walkthrough](#step-by-step-walkthrough)\n   - [Step 1 – Create a toy dataset](#step-1--create-a-toy-dataset)\n   - [Step 2 – Initialise `Flow`](#step-2--initialise-flow)\n   - [Step 3 – Run model fitting and correction](#step-3--run-model-fitting-and-correction)\n5. [Reading the results](#reading-the-results)\n6. [Choosing a graph type](#choosing-a-graph-type)\n7. [Choosing a distance function](#choosing-a-distance-function)\n8. [Visualising the correction (optional)](#visualising-the-correction-optional)\n9. [API reference summary](#api-reference-summary)\n\n---\n\n## Installation\nWithin a conda environment, run\n\n```bash\npip install git+https://github.com/bionetslab/bosperrus-package.git\n```\n\nPip dependencies (`scikit-learn`, `scipy`, `numpy`, `pandas`) are installed automatically. Within the environment, you also need to \n```bash\nconda install graph-tool -c conda-forge\n```\n---\n\n## Key concepts\n\nEntry points / input types:\n![Entry points](https://github.com/bionetslab/bosperrus-package/blob/master/plots_readme/bosperrus_flow.svg?raw=true)\n\n`Flow` enables multiple entry points depending on what you already have:   \n🟣 Given coordinates, a distance function, a graph type (Delaunay, k-nearest neighbor, or radius nearest neighbor), and centrality measures to compute.   \n🩷 Given coordinates, a distance function, and a pre-built edge list.   \n🟡 Given coordinates, a distance function, and pre-computed centrality scores.   \n☘️ Given pre-computed distances and scores only (no coordinates needed).   \n\n\nDistance functions:   \n![Distance functions](https://github.com/bionetslab/bosperrus-package/blob/master/plots_readme/distance_functions.svg?raw=true)\n\n---\n## Quick-start\n\n```python\nimport numpy as np\nfrom bosperrus import Flow\nfrom bosperrus.distances import distance_to_convex_hull\n\nrng = np.random.default_rng(42)\ncoords = rng.uniform(0, 100, size=(500, 2))   # 500 random 2-D points\n\nflow = Flow.from_coords(\n    coordinates=coords,\n    distance_fn=distance_to_convex_hull,\n    measures=[\"degree\", \"closeness\", \"betweenness\", \"clustering\", \"pagerank\"],\n    graph_type=\"delaunay\",\n)\nflow.flow()\n\n# Inspect results\nprint(flow.fit_quality)\nprint(flow.observations.head())   # contains raw + corrected centrality columns\n```\n\n---\n\n## Step-by-step walkthrough\n\n### Step 1 – Create a toy dataset\n\nWe simulate a disc-shaped point cloud to produce an obvious boundary effect: nodes at the\nperimeter will always have fewer neighbours than nodes at the centre.\n\n```python\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom bosperrus import Flow\nfrom bosperrus.distances import distance_to_convex_hull\n\nrng = np.random.default_rng(0)\n\n# Sample uniformly inside a disc of radius 50\nn = 600\nangles = rng.uniform(0, 2 * np.pi, n)\nradii  = 50 * np.sqrt(rng.uniform(0, 1, n))   # sqrt for uniform area sampling\ncoords = np.stack([radii * np.cos(angles),\n                   radii * np.sin(angles)], axis=1)\n```\n\n### Step 2 – Initialise `Flow`\n\n`Flow` is initialised via one of four factory classmethods depending on what you already have. For a full pipeline from coordinates:\n\n```python\nmeasures = [\"degree\", \"closeness\", \"betweenness\", \"harmonic\", \"clustering\", \"pagerank\"]\n\nflow = Flow.from_coords(\n    coordinates=coords,\n    distance_fn=distance_to_convex_hull,\n    measures=measures,\n    graph_type=\"delaunay\",   # or \"knn\" / \"rnn\", see below\n)\n```\n\nIf you already have an edge list, use `Flow.from_coords_and_edgelist`. If you already have centrality scores, use `Flow.from_coords_and_scores`. If you have neither coordinates nor a graph, use `Flow.from_distances_and_scores`.\n\nAll factory methods populate `flow.observations`: a DataFrame with one row per node containing the centrality scores and the distance column.\n\n### Step 3 – Run model fitting and correction\n\n```python\nfrom bosperrus.fit import ConstantFit, PiecewiseLinearFit, ExponentialSaturationFit, MichaelisMentenFit\n\nflow.flow(\n    measures=measures,\n    fits=[ConstantFit, PiecewiseLinearFit, ExponentialSaturationFit, MichaelisMentenFit],\n    calculate_rel_ll_to_baseline=ConstantFit,   # ConstantFit = \"no boundary effect\" null model\n)\n```\n\n`flow()` selects the best model per measure by AIC and writes corrected values back into `flow.observations`. All arguments are optional — omitting them runs all four models on all measures with `ConstantFit` as baseline.\n\nAfter this step:\n\n- `flow.fit_quality` — a DataFrame with one row per measure summarising the best model,\n  AIC-based statistics, effect strength, and half-life.\n- `flow.observations` — now also contains columns named `\"BOSPERRUS corrected \u003cmeasure\u003e\"` for each\n  centrality.\n\n---\n\n## Reading the results\n\n```python\n# Overview table\nprint(flow.fit_quality.T[[\n    \"best_fit_type\",\n    \"observed_effect_strength\",   # signed relative drop from border to centre\n    \"observed_half_life\",         # distance at which ~50% of the effect has decayed\n    \"affected samples\",           # fraction of nodes still in the boundary-affected zone\n]])\n```\n\n**`observed_effect_strength`** is defined as `(c_border − c_centre) / (c_border + c_centre)`.\nA value close to 0 means negligible boundary bias; a value near ±1 means strong bias.\n\n**`best_fit_type`** tells you which model won the AIC comparison:\n\n| Model | Interpretation |\n|---|---|\n| `Constant Fit` | No detectable boundary effect |\n| `Piecewise Linear Fit` | Linear drop up to a breakpoint, then flat plateau |\n| `Exponential Saturation Fit` | Smooth exponential approach to a plateau |\n| `Michaelis-Menten Fit` | Hyperbolic saturation (analogous to enzyme kinetics) |\n\nTo access the corrected values:\n\n```python\n# All corrected columns at once\ncorrected_cols = [c for c in flow.observations.columns if c.startswith(\"BOSPERRUS corrected\")]\nprint(flow.observations[corrected_cols].head())\n\n# Single measure\nflow.observations[[\"degree\", \"BOSPERRUS corrected degree\"]].head()\n```\n\nTo access fit parameters directly:\n\n```python\ndegree_fit = flow.best_fits[\"degree\"]\nprint(degree_fit.name)      # e.g. \"Exponential Saturation Fit\"\nprint(degree_fit.params)    # fitted parameter dict\nprint(degree_fit.AIC)\n```\n\n---\n\n## Choosing a graph type\n\n| Graph type | Key parameter | Notes |\n|---|---|---|\n| `\"delaunay\"` | none | Default choice; produces a natural triangulation without isolated nodes |\n| `\"knn\"` | `k` (int) | Directed, asymmetric — pass via `graph_kwargs={\"k\": 6}` |\n| `\"rnn\"` | `r` (float, same units as coords) | Undirected — pass via `graph_kwargs={\"r\": 15.0}` |\n\n```python\n# k-nearest-neighbour example\nflow = Flow.from_coords(\n    coordinates=coords,\n    distance_fn=distance_to_convex_hull,\n    measures=measures,\n    graph_type=\"knn\",\n    graph_kwargs={\"k\": 6},\n)\n```\n\n---\n\n## Choosing a distance function\n\n| Function | Use case |\n|---|---|\n| `distance_to_convex_hull(coords)` | General default; works for any convex or near-convex tissue shape (2-D and 3-D) |\n| `distance_to_rectangular_border(coords)` | Rectangular imaging window / biopsy (2-D only) |\n| `distance_to_pointset(coords, pointset)` | Border defined by a set of explicit landmark coordinates |\n| `distance_to_mask(coords, mask)` | Binary image mask defining the tissue boundary |\n\nAll functions are importable from `bosperrus.distances` and return a named `pd.Series` with one value per node.\n\n---\n\n## Visualising the correction (optional)\n\n```python\nimport matplotlib.pyplot as plt\n\nfig, axes = plt.subplots(1, 2, figsize=(12, 5))\n\nsc = axes[0].scatter(coords[:, 0], coords[:, 1],\n                     c=flow.observations[\"degree\"], cmap=\"viridis\", s=8)\naxes[0].set_title(\"Raw degree centrality\")\nplt.colorbar(sc, ax=axes[0])\n\nsc2 = axes[1].scatter(coords[:, 0], coords[:, 1],\n                      c=flow.observations[\"BOSPERRUS corrected degree\"], cmap=\"viridis\", s=8)\naxes[1].set_title(\"BOSPERRUS-corrected degree centrality\")\nplt.colorbar(sc2, ax=axes[1])\n\nplt.tight_layout()\nplt.savefig(\"bosperrus_correction.png\", dpi=150)\nplt.show()\n```\n\nNodes near the border should appear more uniform after correction.\n\n---\n\n## API reference summary\n\n### `Flow` — construction\n\n| Factory method | When to use |\n|---|---|\n| `Flow.from_coords(coordinates, distance_fn, measures, graph_type, distance_kwargs=None, graph_kwargs=None)` | Full pipeline from scratch |\n| `Flow.from_coords_and_edgelist(coordinates, distance_fn, measures, edge_list, distance_kwargs=None)` | Skip graph construction |\n| `Flow.from_coords_and_scores(coordinates, distance_fn, scores, distance_kwargs=None)` | Pre-computed centralities |\n| `Flow.from_distances_and_scores(distances, scores)` | No coords needed |\n\n### `Flow` — methods\n\n| Method | Signature | Description |\n|---|---|---|\n| `flow` | `(measures=None, fits=None, calculate_rel_ll_to_baseline=None)` | Fit all models, select best by AIC, write corrected columns to `observations` |\n\n### Key attributes after a full run\n\n| Attribute | Type | Content |\n|---|---|---|\n| `flow.observations` | `pd.DataFrame` | All node-level data: centralities, distance column, corrected values |\n| `flow.fit_quality` | `pd.DataFrame` | Per-measure model selection summary (columns = measures) |\n| `flow.best_fits` | `dict[str, Fit]` | Best `Fit` object per measure |\n| `flow._edge_list` | `set` | Set of edges built during construction (available for Paths 1 and 2) |\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbionetslab%2Fbosperrus-package","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbionetslab%2Fbosperrus-package","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbionetslab%2Fbosperrus-package/lists"}