MENoBiS main use cases¶
A compact applied overview of the fit → sample → filter → compare pipeline. Detailed routes (microcanonical sampling, fixed pairs, ensemble equivalence) live in the documentation pages linked at the end of this notebook.All constraints are derived from a generated non-binary network, so the fitted problems are feasible by construction.
import numpy as np
import pandas as pd
from menobis.analysis import compute_all_stats
from menobis.filtering import filter_model
from menobis.models import Constraint, Ensemble, ModelFamily, fit_model
from menobis.routing import sample_model
from menobis.utilities.synthetic import (
derive_synthetic_constraints,
generate_pa_geographic_network,
)
1. Generate an observed network¶
generate_pa_geographic_network builds a directed non-binary network with
projected XY coordinates. derive_synthetic_constraints extracts feasible
strengths, degrees, edge counts, and total events from it.
network = generate_pa_geographic_network(
node_count=30,
average_degree=6.0,
events_per_edge=8.0,
seed=2024,
self_loops=False,
)
constraints = derive_synthetic_constraints(network)
pd.DataFrame(
{
"nodes": [len(network.x)],
"observed_pairs": [network.edges.num_edges],
"total_events": [network.edges.total_events],
"total_cost": [constraints.total_cost],
}
)
| nodes | observed_pairs | total_events | total_cost | |
|---|---|---|---|---|
| 0 | 30 | 180 | 1440 | 516.19622 |
2. Fit a grand-canonical null¶
Fit a grand-canonical ME (Poisson) model constrained on the out/in
strength sequences. Always check fit.converged before proceeding.
fit = fit_model(
family=ModelFamily.ME,
constraint=Constraint.STRENGTH,
strength_out=constraints.strength_out,
strength_in=constraints.strength_in,
self_loops=False,
)
if not fit.converged:
raise RuntimeError(fit.status)
pd.DataFrame(
{"converged": [fit.converged], "status": [fit.status], "iterations": [fit.iterations]}
)
| converged | status | iterations | |
|---|---|---|---|
| 0 | True | solved | 5 |
3. Sample null networks¶
Each seeded draw is an EdgeTable with the canonical schema
source target occ_num. Different seeds give different samples; the total
occupation fluctuates around the fitted expectation (grand-canonical
semantics).
samples = [
sample_model(
ensemble=Ensemble.GRAND_CANONICAL,
family=ModelFamily.ME,
constraint=Constraint.STRENGTH,
fit=fit,
seed=seed,
)
for seed in range(100)
]
pd.DataFrame(
{
"samples": [len(samples)],
"occupied_pairs (mean)": [np.mean([s.num_edges for s in samples])],
"total_events (mean)": [np.mean([s.total_events for s in samples])],
"observed total_events": [network.edges.total_events],
}
)
| samples | occupied_pairs (mean) | total_events (mean) | observed total_events | |
|---|---|---|---|---|
| 0 | 100 | 496.54 | 1446.68 | 1440 |
4. Filter significant node pairs¶
filter_model classifies observed pairs by how surprising their occupation
is under the null. Here we use the upper tail with an FDR correction.
filtered = filter_model(
network.edges,
family=ModelFamily.ME,
constraint=Constraint.STRENGTH,
fit=fit,
alpha=0.05,
tail="upper",
correction="fdr",
)
pd.DataFrame(
{
"class": ["upper", "lower", "compatible", "absent_lower"],
"pairs": [
filtered.upper.edges.num_edges,
filtered.lower.edges.num_edges,
filtered.compatible.edges.num_edges,
filtered.absent_lower.edges.num_edges,
],
}
)
| class | pairs | |
|---|---|---|
| 0 | upper | 84 |
| 1 | lower | 0 |
| 2 | compatible | 96 |
| 3 | absent_lower | 0 |
5. Ensemble statistics¶
Compare an observed high-level statistic with its distribution over the sampled null networks. Here: the mean out-strength concentration (Y_2^{\mathrm{out}}) and the mean nearest-neighbour strength (s^{\mathrm{nn,out}}).
def network_magnitudes(edges):
stats = compute_all_stats(edges)
return {
"mean_y2_out": float(np.mean(np.nan_to_num(stats.y2_out))),
"mean_snn_out": float(np.mean(np.nan_to_num(stats.s_nn_out))),
}
observed = network_magnitudes(network.edges)
null_df = pd.DataFrame(network_magnitudes(s) for s in samples)
summary = null_df.agg(["mean", "std"])
summary.loc["p025"] = null_df.quantile(0.025)
summary.loc["p975"] = null_df.quantile(0.975)
summary.loc["observed"] = pd.Series(observed)
summary
| mean_y2_out | mean_snn_out | |
|---|---|---|
| mean | 0.118686 | 71.958493 |
| std | 0.014981 | 3.254181 |
| p025 | 0.092343 | 65.078706 |
| p975 | 0.149143 | 77.099658 |
| observed | 0.354960 | 69.736525 |
The observed value is inside (or outside) the null distribution at the chosen quantiles — the pivot of a null-model comparison. How many samples to generate follows from the precision you need (Monte Carlo standard error), not a fixed round number: see Ensemble statistics.
Where to go next¶
- Microcanonical sampling — exact-constraint routes (fixed strengths, degrees, E/T, strength+cost).
- Fixed / known pairs — freezing node pairs.
- Ensemble equivalence — GC vs MC differences; the practical comparison notebook is planned separately.
- Choose a model and Supported models for the decision order and the full supported matrix.