Skip to content

Habitat networks

Graphs of habitat patches linked by distance or by dispersal probability, their connectivity indices (IIC, PC, ECA...), the importance of every patch and link, protected-area connectivity (ProtConn), grids and time series, and the network of MSPA cores and bridges. Tutorial: habitat networks.

The graph class is ls.HabitatGraph; the other functions are in the ls.conn module (import landschaft as ls, then ls.conn.protconn(...)).

Habitat graphs

HabitatGraph

HabitatGraph(
    attributes,
    connections,
    *,
    ids=None,
    kind: str = "distance",
    landscape_area: float | None = None,
    convention: str = "makurhini",
    restoration=None,
    n_threads: int | None = None,
)

Habitat patches (nodes with an attribute) and the connections between them.

The indices are those of Conefor Sensinode (Saura & Torné 2009): NC, NL, H, CCP, LCP, IIC, F, AWF and PC, with the importance of every node (dI, the loss of each index without the node) and the intra, flux and connector fractions of dIIC and dPC, as Makurhini's MK_dPCIIC reports them.

Parameters:

Name Type Description Default
attributes

Attribute of every node (usually its area).

required
connections

Either a square n × n matrix (the upper triangle is read; NaN = no connection; a square array is always read as a matrix) or a table of pairs: a DataFrame or (m, 3) array with the two node ids and the value. Pairs missing from a table are not connected.

required
ids

Node ids (default 1 .. n); pair tables refer to them.

None
kind str

What the values are: "distance" (the default), "probability" of direct dispersal, or "link" (1 = linked, 0 = not), as Conefor's connection types.

'distance'
landscape_area float | None

LA, the attribute of a landscape fully covered by habitat: IIC, PC and LCP are their numerators divided by LA². Without it Makurhini reports only the numerators (the indices are NaN here), Conefor uses LA = 1.

None
convention str

"makurhini" (default) follows MK_dPCIIC: pairs closer than the distance are linked (d < t), probabilities exp(d log(P) / t) below pij_min (default 0.01) are dropped and unconnected pairs count in IIC as 16443701 links apart. "conefor" follows Conefor Sensinode: links up to the distance (d <= t), no probability cut by default (pij_min is its minProb heuristic: probabilities up to it are dropped and paths no more probable are not extended) and unconnected pairs add nothing.

'makurhini'
restoration

1 for the existing nodes and 0 for nodes that could be added (for restoration), as Makurhini's restoration column; then node_importance() also rates the nodes to add (dPCres, dIICres: the gain in % of the graph with them, Makurhini; with convention="conefor", Conefor's "adding nodes" mode: the other indices are those of the existing nodes and every index gets its d...add gain in % of the existing graph).

None
n_threads int | None

Threads (default: all cores but one).

None
See Also

HabitatGraph.from_raster : nodes and distances from a map. HabitatGraph.from_vector : nodes and distances from polygons. HabitatGraph.from_conefor : Conefor's input files.

Examples:

Four patches (areas in ha) and the distances between them (m):

>>> import numpy as np
>>> import landschaft as ls
>>> areas = [100, 50, 20, 80]
>>> distances = np.array([[0, 800, 1500, 5000], [800, 0, 600, 4000],
...                       [1500, 600, 0, 3000], [5000, 4000, 3000, 0]])
>>> graph = ls.HabitatGraph(areas, distances, landscape_area=10_000)
>>> graph.n_nodes
4
>>> table = graph.overall(["NC", "PC"], distance=1000, probability=0.5)
>>> int(table["NC"].iloc[0]), round(float(table["EC(PC)"].iloc[0]), 1)
(2, 172.3)

The same as a table of pairs:

>>> pairs = [(1, 2, 800), (2, 3, 600), (1, 3, 1500)]
>>> sparse = ls.HabitatGraph(areas, pairs, landscape_area=10_000)

n_nodes property

n_nodes: int

Number of nodes.

Examples:

>>> import landschaft as ls
>>> ls.HabitatGraph([1.0, 2.0], [[0, 5], [5, 0]]).n_nodes
2

from_raster classmethod

from_raster(
    nodes,
    *,
    attribute=None,
    weighted: bool = False,
    area_unit: str = "m2",
    distance: str = "centroid",
    max_distance: float | None = None,
    distance_unit: str = "m",
    habitat=None,
    directions: int = 8,
    nodata=None,
    band=None,
    resistance=None,
    cost_directions: int = 8,
    cost_function: str = "makurhini",
    unreachable: str = "euclidean",
    **kwargs,
) -> "HabitatGraph"

Nodes from a raster of patch ids (or the patches of habitat classes).

The attributes come from [`node_table()`][landschaft.conn.node_table] (the patch
areas in ``area_unit`` by default) and the connections from
[`distance_table()`][landschaft.conn.distance_table] (``distance="centroid"``,
``"edge"``, ``"least-cost"`` or ``"commute-time"`` over ``resistance``, pairs up to
``max_distance`` in ``distance_unit``), as
Makurhini's ``MK_dPCIIC`` does with raster nodes. Other arguments go
to [`HabitatGraph`][landschaft.HabitatGraph]; `to_raster()` paints node values on the
patches.

Parameters:

Name Type Description Default
nodes

Raster of patch ids (NaN / NoData = no habitat), or of classes with habitat.

required
attribute

Attribute per patch (table or Series by id); default: the patch area.

None
weighted bool

Multiply the given attribute by the patch area.

False
area_unit str

"m2", "ha" or "km2" for the areas.

'm2'
distance str

"centroid", "edge", "least-cost" or "commute-time".

'centroid'
max_distance float | None

Keep the pairs up to this distance ("m" or "km").

None
distance_unit float | None

Keep the pairs up to this distance ("m" or "km").

None
habitat

Class values whose patches are the nodes (patches are labelled with directions 8 or 4).

None
nodata

Reading options.

None
band

Reading options.

None
resistance

Resistance raster for least-cost and commute-time distances.

None
cost_directions int

Neighbours of the cost surface (8 or 4), how the conductance between two cells is taken ("makurhini" or "mean"), and what unreachable pairs get ("euclidean": their centroid distance, or "drop").

8
cost_function int

Neighbours of the cost surface (8 or 4), how the conductance between two cells is taken ("makurhini" or "mean"), and what unreachable pairs get ("euclidean": their centroid distance, or "drop").

8
unreachable int

Neighbours of the cost surface (8 or 4), how the conductance between two cells is taken ("makurhini" or "mean"), and what unreachable pairs get ("euclidean": their centroid distance, or "drop").

8
**kwargs

HabitatGraph options (landscape_area, convention...).

{}

Returns:

Type Description
HabitatGraph

Examples:

>>> import landschaft as ls
>>> landcover = ls.load_raster(ls.example_data("clc"))
>>> graph = ls.HabitatGraph.from_raster(landcover, habitat=[23, 24, 25], area_unit="ha",
...                                     distance="edge", max_distance=5000,
...                                     landscape_area=landcover.size * 25 * 25 / 1e4)
>>> graph.n_nodes
158
>>> importance = graph.node_importance("PC", distance=1000, probability=0.5)
>>> dpc_map = graph.to_raster(importance, "dPC")

from_vector classmethod

from_vector(
    nodes,
    *,
    id: str | None = None,
    attribute=None,
    weighted: bool = False,
    area_unit: str = "m2",
    distance: str = "centroid",
    max_distance: float | None = None,
    distance_unit: str = "m",
    resistance=None,
    cost_directions: int = 8,
    cost_function: str = "makurhini",
    unreachable: str = "euclidean",
    **kwargs,
) -> "HabitatGraph"

Nodes from polygons or points (GeoDataFrame), as Makurhini's MK_dPCIIC.

``id`` names the column of node ids, ``attribute`` a column (default:
the area in ``area_unit``); distances are ``"centroid"``, ``"edge"``
(GEOS), ``"least-cost"`` or ``"commute-time"`` over ``resistance``, pairs up to ``max_distance``. `to_geodataframe()` joins
node values to the geometries.

Parameters:

Name Type Description Default
nodes

GeoDataFrame of polygons or points, in a metric CRS.

required
id str | None

Column of node ids (default: 1..n).

None
attribute

As in from_raster().

None
weighted

As in from_raster().

None
area_unit

As in from_raster().

None
distance

As in from_raster().

None
max_distance

As in from_raster().

None
distance_unit

As in from_raster().

None
resistance

As in from_raster().

None
cost_directions

As in from_raster().

None
cost_function

As in from_raster().

None
unreachable

As in from_raster().

None
**kwargs

HabitatGraph options.

{}

Returns:

Type Description
HabitatGraph

Examples:

>>> import geopandas as gpd
>>> import shapely
>>> import landschaft as ls
>>> patches = gpd.GeoDataFrame({"name": ["a", "b", "c"]}, crs="EPSG:3035", geometry=[
...     shapely.box(0, 0, 500, 500), shapely.box(900, 0, 1400, 400), shapely.box(3000, 0, 3300, 300)])
>>> graph = ls.HabitatGraph.from_vector(patches, id="name", area_unit="ha", distance="edge")
>>> graph.components(distance=1000).tolist()
[1, 1, 2]

from_samc classmethod

from_samc(
    nodes,
    resistance,
    *,
    dispersal_distance: float | None = None,
    absorption=None,
    directions: int = 8,
    cost_function: str = "mean",
    vertices: int | None = 10,
    vertex_fraction: float | None = None,
    settle: str = "sum",
    id: str | None = None,
    attribute=None,
    area_unit: str = "m2",
    **kwargs,
) -> "HabitatGraph"

Polygons connected by settlement probabilities of a spatial absorbing Markov chain (MK_dPC_SAMC).

The probabilities are [`settlement_probabilities()`][landschaft.conn.settlement_probabilities]
(samc's random walk over ``resistance``, absorbed with ``absorption``
or Fletcher et al.'s rate for ``dispersal_distance``); the attribute is
the area in ``area_unit`` or a column. The graph has
``kind="probability"``: ``node_importance("PC", pij_min=0.001)`` is
Makurhini's ``MK_dPC_SAMC`` (which cuts at 0.001).

Parameters:

Name Type Description Default
nodes

GeoDataFrame of habitat polygons.

required
resistance

Resistance raster the random walk moves on.

required
dispersal_distance float | None

Mean dispersal distance (for the default absorption rate) or the absorption per cell.

None
absorption float | None

Mean dispersal distance (for the default absorption rate) or the absorption per cell.

None
directions int

Neighbours of the walk and how the step probabilities are derived.

8
cost_function int

Neighbours of the walk and how the step probabilities are derived.

8
vertices int | None

Boundary vertices sampled per polygon, how settlement is summed, node ids and attributes; see settlement_probabilities().

10
vertex_fraction int | None

Boundary vertices sampled per polygon, how settlement is summed, node ids and attributes; see settlement_probabilities().

10
settle int | None

Boundary vertices sampled per polygon, how settlement is summed, node ids and attributes; see settlement_probabilities().

10
id int | None

Boundary vertices sampled per polygon, how settlement is summed, node ids and attributes; see settlement_probabilities().

10
attribute int | None

Boundary vertices sampled per polygon, how settlement is summed, node ids and attributes; see settlement_probabilities().

10
area_unit int | None

Boundary vertices sampled per polygon, how settlement is summed, node ids and attributes; see settlement_probabilities().

10
**kwargs

HabitatGraph options.

{}

Returns:

Type Description
HabitatGraph

With kind="probability".

Examples:

>>> import geopandas as gpd
>>> import numpy as np
>>> import shapely
>>> import xarray as xr
>>> import landschaft as ls
>>> from rasterio.transform import from_origin
>>> resistance = xr.DataArray(np.ones((60, 60)), dims=("y", "x")).rio.write_transform(
...     from_origin(0, 600, 10, 10)).rio.write_crs("EPSG:3035")
>>> patches = gpd.GeoDataFrame(geometry=[shapely.box(50, 50, 150, 150), shapely.box(400, 400, 520, 520)],
...                            crs="EPSG:3035")
>>> graph = ls.HabitatGraph.from_samc(patches, resistance, dispersal_distance=300, area_unit="ha")
>>> graph.n_nodes
2

from_conefor classmethod

from_conefor(node_file, connection_file, **kwargs) -> 'HabitatGraph'

Read Conefor's input files.

``node_file`` holds ``id attribute`` per line; ``connection_file`` either
``id id value`` per line (pairs not listed are not connected) or the
``n × n`` matrix in the order of the nodes. Other arguments go to
[`HabitatGraph`][landschaft.HabitatGraph] (``kind``, ``convention``, ``landscape_area``...).

Parameters:

Name Type Description Default
node_file

Node file: id attribute per line.

required
connection_file

Connection file: id id value per line, or an n x n matrix.

required
**kwargs

HabitatGraph options.

{}

Returns:

Type Description
HabitatGraph

Examples:

>>> import numpy as np
>>> import landschaft as ls
>>> distances = np.array([[0, 800, 1500, 5000], [800, 0, 600, 4000],
...                       [1500, 600, 0, 3000], [5000, 4000, 3000, 0]])
>>> graph = ls.HabitatGraph([100, 50, 20, 80], distances, landscape_area=10_000)
>>> graph.to_conefor("nodes.txt", "distances.txt")
>>> again = ls.HabitatGraph.from_conefor("nodes.txt", "distances.txt", landscape_area=10_000)
>>> again.n_nodes
4

overall

overall(
    metrics: Sequence[str] | str | None = None,
    *,
    distance=None,
    probability: float | None = None,
    pij_min: float | None = None,
) -> DataFrame

Indices of the whole graph, one row per distance.

Parameters:

Name Type Description Default
metrics Sequence[str] | str | None

Any of "NC", "NL", "H", "CCP", "LCP", "IIC", "F", "AWF", "PC" (default: all that the connection kind allows).

None
distance

Distance (or distances) of the links and of probability (Makurhini's distance_thresholds; with kind="probability" the minimum probability of a link). Makurhini's default is the median distance.

None
probability float | None

Dispersal probability at distance (default 0.5).

None
pij_min float | None

Probability cut (default 0.01 for Makurhini, none for Conefor).

None

Returns:

Type Description
DataFrame

distance and the indices: NC, NL, H, CCP, LCP, LCPnum, IIC, IICnum, EC(IIC), IICintra(%), IICdirect(%), IICstep(%), F, AWF, PC, PCnum, EC(PC), PCintra(%), PCdirect(%), PCstep(%).

Examples:

>>> import numpy as np
>>> import landschaft as ls
>>> distances = np.array([[0, 800, 1500, 5000], [800, 0, 600, 4000],
...                       [1500, 600, 0, 3000], [5000, 4000, 3000, 0]])
>>> graph = ls.HabitatGraph([100, 50, 20, 80], distances, landscape_area=10_000)
>>> table = graph.overall(["NC", "IIC", "PC"], distance=[500, 1000], probability=0.5)
>>> table["distance"].tolist()
[500, 1000]

node_importance

node_importance(
    metrics: Sequence[str] | str | None = None,
    *,
    distance=None,
    probability: float | None = None,
    pij_min: float | None = None,
) -> DataFrame

Importance of every node: the loss of each index (in %) without it.

Parameters are those of overall().

Returns:

Type Description
DataFrame

id, distance and, for the metrics asked: dA, dNC, dNL, dH, dCCP, dLCP, dIIC with dIICintra/dIICflux/dIICconnector, dF, dAWF, dPC with dPCintra/dPCflux/dPCconnector and varPC (the loss of PC itself). Several distances give one block of rows per distance. Asked for by name (not in the default), Conefor's betweenness centralities: "BC" (Freeman's, normalised), "BCIIC" (varBC_IIC: over ordered pairs the share of their shortest paths through the node times a_i a_j / (1 + nl_ij); dBC_IIC in % of IICnum) and "BCPC" (varBC_PC: a_i a_j p*_ij of the pairs whose most probable path runs through it; dBC_PC in % of PCnum), Bodin & Saura (2010).

Examples:

>>> import numpy as np
>>> import landschaft as ls
>>> distances = np.array([[0, 800, 1500, 5000], [800, 0, 600, 4000],
...                       [1500, 600, 0, 3000], [5000, 4000, 3000, 0]])
>>> graph = ls.HabitatGraph([100, 50, 20, 80], distances, landscape_area=10_000)
>>> importance = graph.node_importance("PC", distance=1000, probability=0.5)
>>> importance.columns.tolist()
['id', 'distance', 'dPC', 'dPCintra', 'dPCflux', 'dPCconnector', 'varPC']
link_importance(
    metric: str = "PC",
    *,
    distance=None,
    probability: float | None = None,
    pij_min: float | None = None,
) -> DataFrame

Importance of every link: the loss of the index (in %) once it is removed.

Makurhini's MK_dPCIIC_links (removal): the links of the IIC graph, or the edges of the PC graph (pairs above pij_min). Other pairs change nothing (0; for IIC Makurhini reports its constant share of the unconnected pairs, as for the links). Parameters as in overall().

Returns:

Type Description
DataFrame

from, to (node ids), distance and dIIC_removal / dPC_removal.

Examples:

>>> import numpy as np
>>> import landschaft as ls
>>> distances = np.array([[0, 800, 1500, 5000], [800, 0, 600, 4000],
...                       [1500, 600, 0, 3000], [5000, 4000, 3000, 0]])
>>> graph = ls.HabitatGraph([100, 50, 20, 80], distances, landscape_area=10_000)
>>> links = graph.link_importance("PC", distance=1000, probability=0.5)
>>> links.columns.tolist()
['from', 'to', 'distance', 'dPC_removal']
link_change(
    changes,
    metric: str = "PC",
    *,
    distance=None,
    probability: float | None = None,
    pij_min: float | None = None,
) -> DataFrame

Change of the index when the distance of a pair changes (Makurhini's MK_dPCIIC_links(change=)).

Parameters:

Name Type Description Default
changes

New values of some pairs: a (from, to, value) table, or an n × n matrix (the pairs whose value differs are evaluated).

required
metric str

As in link_importance().

'PC'
distance str

As in link_importance().

'PC'
probability str

As in link_importance().

'PC'
pij_min str

As in link_importance().

'PC'

Returns:

Type Description
DataFrame

from, to, distance and dIIC_change / dPC_change: 100 (I - I') / I as Makurhini (negative for a gain). IIC: a pair that comes closer than distance gains a link, any other change counts 0. PC: the pair's edge takes the probability of its new value (not cut by pij_min).

Examples:

>>> import numpy as np
>>> import landschaft as ls
>>> distances = np.array([[0, 800, 1500, 5000], [800, 0, 600, 4000],
...                       [1500, 600, 0, 3000], [5000, 4000, 3000, 0]])
>>> graph = ls.HabitatGraph([100, 50, 20, 80], distances, landscape_area=10_000)
>>> changes = [(1, 2, 2000)]                    # the pair 1-2 becomes 2 km apart
>>> change = graph.link_change(changes, "PC", distance=1000, probability=0.5)
>>> list(change.columns)
['from', 'to', 'distance', 'dPC_change']

components

components(distance=None) -> Series

Connected component of every node.

Parameters:

Name Type Description Default
distance

Links are the pairs closer than this (distance graphs).

None

Returns:

Type Description
Series

Component number (1, 2, ...) indexed by node id.

Examples:

>>> import numpy as np
>>> import landschaft as ls
>>> distances = np.array([[0, 800, 1500, 5000], [800, 0, 600, 4000],
...                       [1500, 600, 0, 3000], [5000, 4000, 3000, 0]])
>>> graph = ls.HabitatGraph([100, 50, 20, 80], distances, landscape_area=10_000)
>>> graph.components(distance=1000).tolist()
[1, 1, 1, 2]

centrality

centrality(
    distance=None, *, probability: float | None = None, binary: bool = True
) -> DataFrame

Radial and medial centrality of every node, as Makurhini's MK_RMCentrality (igraph).

Parameters:

Name Type Description Default
distance

Distance (or distances) of the links (binary) or of probability (default: the median distance).

None
probability float | None

Dispersal probability at distance; giving it makes the graph weighted, as in Makurhini (None: exp(-d / distance)).

None
binary bool

True: the graph of links, pairs closer than distance (up to it with convention="conefor"). False: every pair, weighted by its probability p, with 1 / p as the length of the edge and the weight of strength and eigenvector centrality.

True

Returns:

Type Description
DataFrame

id, distance, degree (binary) or strength (the sum of 1 / p), eigen (eigenvector centrality, the largest 1), close (closeness: 1 / the sum of the distances to the nodes it reaches; weighted, igraph's normalised closeness, times their number), BWC (betweenness; as igraph, paths equal within 1e-10 share it) and cluster (component). With python-igraph installed also memb_rw (walktrap communities, weighted by p); Makurhini's Louvain communities are random and left out.

Examples:

>>> import numpy as np
>>> import landschaft as ls
>>> distances = np.array([[0, 800, 1500, 5000], [800, 0, 600, 4000],
...                       [1500, 600, 0, 3000], [5000, 4000, 3000, 0]])
>>> graph = ls.HabitatGraph([100, 50, 20, 80], distances, landscape_area=10_000)
>>> table = graph.centrality(1000)
>>> table.columns.tolist()
['id', 'distance', 'degree', 'eigen', 'close', 'BWC', 'cluster']

to_raster

to_raster(values, name: str | None = None)

Node values painted on the patches of from_raster().

Parameters:

Name Type Description Default
values

One value per node (in node order), a Series indexed by node id, or a table such as node_importance() (with name naming the column).

required
name str | None

Column of values to paint.

None

Returns:

Type Description
DataArray or ndarray

The patch map with the values, NaN elsewhere.

Examples:

>>> import landschaft as ls
>>> landcover = ls.load_raster(ls.example_data("clc"))
>>> graph = ls.HabitatGraph.from_raster(landcover, habitat=[23, 24, 25], area_unit="ha",
...                                     distance="edge", max_distance=5000,
...                                     landscape_area=landcover.size * 25 * 25 / 1e4)
>>> components = graph.components(distance=1000)
>>> component_map = graph.to_raster(components)

to_geodataframe

to_geodataframe(values, name: str | None = None)

Node values joined to the geometries of from_vector().

Parameters:

Name Type Description Default
values

A node_importance() table (all its columns; one block per distance), a Series indexed by node id, or one value per node.

required
name str | None

Name of the column for a plain sequence of values.

None

Returns:

Type Description
GeoDataFrame

Examples:

>>> import geopandas as gpd
>>> import shapely
>>> import landschaft as ls
>>> patches = gpd.GeoDataFrame(crs="EPSG:3035", geometry=[
...     shapely.box(0, 0, 500, 500), shapely.box(900, 0, 1400, 400), shapely.box(3000, 0, 3300, 300)])
>>> graph = ls.HabitatGraph.from_vector(patches, area_unit="ha", distance="edge",
...                                     landscape_area=100)
>>> table = graph.to_geodataframe(graph.node_importance("PC", distance=1000, probability=0.5))
>>> "dPC" in table.columns
True

to_conefor

to_conefor(node_file, connection_file) -> None

Write Conefor's input files.

Parameters:

Name Type Description Default
node_file

id attribute lines.

required
connection_file

id id value lines, connected pairs only.

required

Examples:

>>> import numpy as np
>>> import landschaft as ls
>>> distances = np.array([[0, 800, 1500, 5000], [800, 0, 600, 4000],
...                       [1500, 600, 0, 3000], [5000, 4000, 3000, 0]])
>>> graph = ls.HabitatGraph([100, 50, 20, 80], distances, landscape_area=10_000)
>>> graph.to_conefor("nodes.txt", "distances.txt")
>>> open("nodes.txt").read().split()[:2]
['1', '100.0']

Nodes and distances

node_table

node_table(
    nodes,
    *,
    attribute=None,
    weighted: bool = False,
    area_unit: str = "m2",
    id: str | None = None,
    habitat=None,
    directions: int = 8,
    nodata=None,
    band=None,
    n_threads=None,
) -> DataFrame

Node ids and attributes, as Makurhini's nodesfile.

Parameters:

Name Type Description Default
nodes

A raster of patch ids (path, DataArray or array; NA or nodata = no habitat) or polygons / points (GeoDataFrame).

required
attribute

Raster: one value per patch (in the order of the sorted ids) used as the attribute, or multiplied by the patch area when weighted. Polygons: a column name. By default the area.

None
area_unit str

Unit of the areas: "m2", "ha", "km2"... (Makurhini's factors; the map units must be metres).

'm2'
id str | None

Polygons: the column with the node ids (default 1 .. n).

None
habitat

Raster: the classes whose patches (directions-connected) are the nodes, instead of a raster of patch ids.

None
directions

Raster: the classes whose patches (directions-connected) are the nodes, instead of a raster of patch ids.

None

Returns:

Type Description
DataFrame

id and attribute, one row per node.

Examples:

Forest patches (classes 23-25) of the example map, areas in hectares:

>>> import landschaft as ls
>>> landcover = ls.load_raster(ls.example_data("clc"))
>>> nodes = ls.conn.node_table(landcover, habitat=[23, 24, 25], area_unit="ha")
>>> len(nodes)
158
>>> nodes.head(2)
   id  attribute
0   1     73.250
1   2     89.125

distance_table

distance_table(
    nodes,
    *,
    distance: str = "centroid",
    id: str | None = None,
    max_distance: float | None = None,
    distance_unit: str = "m",
    habitat=None,
    directions: int = 8,
    nodata=None,
    band=None,
    resistance=None,
    cost_directions: int = 8,
    cost_function: str = "makurhini",
    unreachable: str = "euclidean",
    n_threads=None,
) -> DataFrame

Euclidean distances between nodes, as Makurhini's distancefile.

Parameters:

Name Type Description Default
nodes

A raster of patch ids or polygons / points (see node_table()).

required
distance str

"centroid": between centroids (rasters: the mean of the cell centres; polygons: the centroid of the largest part, or a point on the surface when it falls outside). "edge": between the closest edges (0 for touching patches). "least-cost": the cheapest path between the cells of the centroids over resistance, as gdistance's costDistance does for Makurhini: the cost of a step between neighbouring cells (cost_directions 4 or 8) is the step length (between cell centres) over the conductance, 1 / the resistance of the cell of larger index (cost_function="makurhini", Makurhini's default) or 1 / the mean of both ("mean"). Distances are in cost units (resistance × map units); distance_unit does not apply. "commute-time": gdistance's commuteDistance over the same conductances (1 / the step cost): the expected time of a random walk from one node's cell to the other's and back, the effective resistance between them (as in Circuitscape) times the total conductance.

'centroid'
max_distance float | None

Keep only the pairs up to this distance (in distance_unit). Makurhini's threshold keeps them this way in pair tables.

None
distance_unit str

"m", "km"... (the map units must be metres).

'm'
resistance

The resistance raster of "least-cost" (path, DataArray or array; NA cells cannot be crossed).

None
unreachable str

"least-cost" or "commute-time" pairs without a path: "euclidean" gives them the distance between their centroids, as Makurhini does, "drop" leaves them unconnected.

'euclidean'

Returns:

Type Description
DataFrame

from, to (node ids, from first in node order) and distance.

Examples:

>>> import landschaft as ls
>>> landcover = ls.load_raster(ls.example_data("clc"))
>>> pairs = ls.conn.distance_table(landcover, habitat=[23, 24, 25], distance="edge",
...                                max_distance=1000)
>>> len(pairs)
126
>>> pairs.head(2)
   from  to    distance
0     3  13  989.002022
1     4   5  206.155281

settlement_probabilities

settlement_probabilities(
    nodes,
    resistance,
    *,
    dispersal_distance: float | None = None,
    absorption=None,
    directions: int = 8,
    cost_function: str = "mean",
    vertices: int | None = 10,
    vertex_fraction: float | None = None,
    settle: str = "sum",
    id: str | None = None,
    n_threads: int | None = None,
)

Probabilities of settling in one patch from another under a spatial absorbing Markov chain.

Makurhini's MK_dPC_SAMC (samc_settlement, samc's random walk): the cells with a resistance are the transient states; from a cell the walk steps to a neighbour (directions 4 or 8) with probability (1 − absorption) × w / Σw, w = 1 / the mean resistance of both cells (cost_function="mean") or 1 / the neighbour's ("makurhini"), over √2 for diagonal steps, and is absorbed (settles) with the cell's absorption. The probability of settling in cell d when starting in cell o is F[o, d] × absorption[d], F = (I − Q)⁻¹. Between two patches it is the settle ("sum", Makurhini's default, or "mean") of those values from the boundary vertices of the later patch to those of the earlier one (boundary_vertices(); vertices on NA cells are left out).

Parameters:

Name Type Description Default
nodes

Polygons (GeoDataFrame).

required
resistance

Resistance raster (path, DataArray or array; NA cells are not states).

required
dispersal_distance float | None

Mean dispersal distance (map units) for the default absorption: 0 in the patches (cells whose centre is inside), exp(−1.247 − 1.788 × log(distance / cell size)) elsewhere (Fletcher et al. 2023).

None
absorption

Absorption raster (0-1, the resistance's grid) instead.

None
directions int

4 or 8 neighbours.

8
cost_function str

"mean" (1 / the mean resistance) or "makurhini" (1 / the neighbour's resistance).

'mean'
vertices int | None

Number of boundary vertices per patch (default 10), or their fraction.

10
vertex_fraction int | None

Number of boundary vertices per patch (default 10), or their fraction.

10
settle str

"sum" or "mean" of the probabilities between the vertices.

'sum'
id str | None

Column with the node ids (default 1..n).

None
n_threads int | None

Threads (default: all cores but one).

None

Returns:

Type Description
(ids, ndarray)

The node ids and the n × n matrix of probabilities (1 on the diagonal).

See Also

HabitatGraph.from_samc : a probability graph from these probabilities.

Examples:

Two patches on a uniform resistance surface of 60 x 60 cells of 10 m:

>>> import geopandas as gpd
>>> import numpy as np
>>> import shapely
>>> import xarray as xr
>>> import landschaft as ls
>>> from rasterio.transform import from_origin
>>> resistance = xr.DataArray(np.ones((60, 60)), dims=("y", "x")).rio.write_transform(
...     from_origin(0, 600, 10, 10)).rio.write_crs("EPSG:3035")
>>> patches = gpd.GeoDataFrame(geometry=[shapely.box(50, 50, 150, 150),
...                                      shapely.box(400, 400, 520, 520)], crs="EPSG:3035")
>>> ids, probabilities = ls.conn.settlement_probabilities(patches, resistance,
...                                                       dispersal_distance=300)
>>> probabilities.shape
(2, 2)

boundary_vertices

boundary_vertices(
    geometry, n: int | None = 10, fraction: float | None = None
) -> ndarray

Vertices of a polygon picked as Makurhini's SR_vertices does (systematic).

All coordinates of the geometry in order (every ring and part); when the first equals the last, the last is dropped and repeated points removed. n of them (or round(fraction × count)) at round(seq(1, count, length.out = n)), all when there are no more.

Parameters:

Name Type Description Default
geometry

A shapely polygon (or multipolygon).

required
n int | None

Number of vertices (None: use fraction).

10
fraction float | None

Fraction of the vertices instead.

None

Returns:

Type Description
ndarray

(k, 2) array of x, y coordinates.

Examples:

>>> import shapely
>>> import landschaft as ls
>>> ls.conn.boundary_vertices(shapely.box(0, 0, 10, 10), n=3)
array([[10.,  0.],
       [10., 10.],
       [ 0.,  0.]])

Protected-area connectivity

protconn

Protected Connected land (ProtConn) and its fractions, as Makurhini's MK_ProtConn (Saura et al. 2017, 2018).

ProtConnNodes dataclass

ProtConnNodes(nodes: object, pieces: ndarray, clipped: object, la: float)

The nodes of a ProtConn analysis of one region (protconn_nodes()).

Attributes:

Name Type Description
nodes GeoDataFrame

type ("Non-Transboundary", "Transboundary" or "Region") and attribute (the area in the region; 0 for the others).

pieces ndarray

Areas of the protected areas' pieces in the region before they are dissolved (ProtConn[Within]).

clipped GeoDataFrame

Every protected area cut to the region (the nodes of the importance of each area).

la float

The area of the region.

Examples:

>>> import geopandas as gpd
>>> import shapely
>>> import landschaft as ls
>>> reserves = gpd.GeoDataFrame({"name": ["a", "b", "c", "d"]}, crs="EPSG:3035", geometry=[
...     shapely.box(0, 0, 2000, 2000), shapely.box(3000, 0, 4500, 1500),
...     shapely.box(0, 5000, 1000, 6000), shapely.box(9000, 9000, 11000, 11000)])
>>> region = gpd.GeoDataFrame(geometry=[shapely.box(-1000, -1000, 10000, 10000)], crs="EPSG:3035")
>>> nodes = ls.conn.protconn_nodes(reserves, region)
>>> len(nodes.nodes), len(nodes.pieces)
(4, 4)

protconn_nodes

protconn_nodes(
    protected_areas,
    region,
    *,
    transboundary: float | None = None,
    transboundary_type: str = "nodes",
    bound: bool = False,
    area_unit: str = "m2",
    preselect: bool = True,
) -> ProtConnNodes

Nodes of ProtConn in a region, as Makurhini's Protconn_nodes builds them.

The protected areas that overlap the region are cut to it, dissolved and split into separate polygons (the nodes, with their area); with transboundary (a distance) the parts of those areas outside the region and the other protected areas within that distance of them (transboundary_type="nodes") or of the region ("region"), cut to that zone, become transboundary nodes (attribute 0: stepping stones); bound adds the parts of the region itself (ProtConn[Bound]), as Makurhini does only when there are transboundary nodes.

preselect=False uses all the given areas as candidates (the grid's selection, made once for its whole region).

Makurhini simplifies the polygons of the zones with mapshaper (5 % of the vertices) before buffering them; landschaft buffers the exact polygons (with GEOS' 30 segments per quarter circle, as sf).

Parameters:

Name Type Description Default
protected_areas

Polygons of the protected areas (GeoDataFrame, projected in metres).

required
region

The region (one or more polygons, merged).

required
transboundary float | None

As in protconn().

None
transboundary_type float | None

As in protconn().

None
bound float | None

As in protconn().

None
area_unit float | None

As in protconn().

None
preselect bool

Only use the protected areas near the region.

True

Returns:

Type Description
ProtConnNodes
See Also

protconn_indicators : ProtConn from these nodes and their distances.

Examples:

>>> import geopandas as gpd
>>> import shapely
>>> import landschaft as ls
>>> reserves = gpd.GeoDataFrame({"name": ["a", "b", "c", "d"]}, crs="EPSG:3035", geometry=[
...     shapely.box(0, 0, 2000, 2000), shapely.box(3000, 0, 4500, 1500),
...     shapely.box(0, 5000, 1000, 6000), shapely.box(9000, 9000, 11000, 11000)])
>>> region = gpd.GeoDataFrame(geometry=[shapely.box(-1000, -1000, 10000, 10000)], crs="EPSG:3035")
>>> nodes = ls.conn.protconn_nodes(reserves, region, area_unit="ha")
>>> nodes.nodes["type"].tolist()
['Non-Transboundary', 'Non-Transboundary', 'Non-Transboundary', 'Non-Transboundary']
>>> float(nodes.la)
12100.0

protconn_indicators

protconn_indicators(
    nodes,
    distances,
    *,
    distance: float,
    la: float,
    pieces=None,
    probability: float | None = 0.5,
    transboundary_weight: float = 1.0,
    pij_min: float | None = None,
) -> dict

ProtConn and its fractions from prepared nodes and distances (Makurhini's get_protconn_grid).

Parameters:

Name Type Description Default
nodes

Table with type ("Non-Transboundary", "Transboundary", "Region") and attribute, one row per node (e.g. protconn_nodes()).

required
distances

Distances between the nodes: n × n matrix (NaN = none) or (from, to, distance) table with node positions 1..n.

required
distance float

The dispersal distance and its probability (None: exp(-1)).

required
probability float

The dispersal distance and its probability (None: exp(-1)).

required
la float

The area of the region.

required
pieces

Areas of the protected pieces before dissolving (ProtConn[Within]); default: the node attributes.

None
transboundary_weight float

a_i a_j of the pairs with a transboundary (or region) node: Makurhini uses 1; 0 counts them as stepping stones only (Saura et al. 2017).

1.0
pij_min float | None

Links with a lower probability are cut (None: none).

None

Returns:

Type Description
dict

The indicators of protconn() (ECA, PC, Prot, ProtConn, its fractions...) for this distance.

Examples:

Distances between the nodes of protconn_nodes(), then the indicators:

>>> import geopandas as gpd
>>> import shapely
>>> import landschaft as ls
>>> reserves = gpd.GeoDataFrame({"name": ["a", "b", "c", "d"]}, crs="EPSG:3035", geometry=[
...     shapely.box(0, 0, 2000, 2000), shapely.box(3000, 0, 4500, 1500),
...     shapely.box(0, 5000, 1000, 6000), shapely.box(9000, 9000, 11000, 11000)])
>>> region = gpd.GeoDataFrame(geometry=[shapely.box(-1000, -1000, 10000, 10000)], crs="EPSG:3035")
>>> import numpy as np
>>> nodes = ls.conn.protconn_nodes(reserves, region)
>>> geometry = nodes.nodes.geometry
>>> distances = np.array([[a.distance(b) if i != j else np.nan for j, b in enumerate(geometry)]
...                       for i, a in enumerate(geometry)])
>>> result = ls.conn.protconn_indicators(nodes.nodes, distances, distance=2000, la=nodes.la,
...                                      pieces=nodes.pieces)
>>> round(float(result["ProtConn"]), 2)
5.25

protconn

protconn(
    protected_areas,
    region,
    *,
    distance,
    probability: float | None = 0.5,
    transboundary: float | None = None,
    transboundary_type: str = "nodes",
    bound: bool = False,
    area_unit: str = "m2",
    distance_type: str = "edge",
    resistance=None,
    delta: bool = False,
    transboundary_weight: float = 1.0,
    cost_directions: int = 8,
    cost_function: str = "makurhini",
    preselect: bool = True,
    pij_min: float | None = None,
)

Protected Connected land (ProtConn) of a region, as Makurhini's MK_ProtConn.

Parameters:

Name Type Description Default
protected_areas

Polygons of the protected areas (GeoDataFrame, projected in metres).

required
region

The region (one or more polygons, merged).

required
distance

Dispersal distance(s) (one row per distance).

required
probability float | None

Dispersal probability at the distance (default 0.5).

0.5
transboundary float | None

Distance of the transboundary zone (None: no transboundary nodes).

None
transboundary_type str

"nodes" (the zone around the region's protected areas, Saura et al. 2017) or "region" (around the region).

'nodes'
bound bool

Also the ProtUnconn[Design] and ProtConn[Bound] fractions.

False
area_unit str

Unit of the areas ("m2", "ha", "km2"...).

'm2'
distance_type str

"edge" (between polygon edges) or "least-cost" over resistance (between centroids, gdistance's cost).

'edge'
delta bool

Also return the importance of every protected area: dProt, dProtConn and varProtConn once it is removed.

False
transboundary_weight float

a_i a_j of the pairs with transboundary nodes (Makurhini: 1; 0 for Saura's definition, stepping stones only).

1.0
resistance

Resistance raster of distance_type="least-cost", its neighbours (8 or 4) and conductance rule, as in distance_table().

None
cost_directions

Resistance raster of distance_type="least-cost", its neighbours (8 or 4) and conductance rule, as in distance_table().

None
cost_function

Resistance raster of distance_type="least-cost", its neighbours (8 or 4) and conductance rule, as in distance_table().

None
preselect bool

Only use the protected areas near the region (False: all of them).

True
pij_min float | None

Links with a lower probability are cut (None: none).

None

Returns:

Type Description
pandas.DataFrame, or (DataFrame, GeoDataFrame) with ``delta``

One row per distance: distance, ECA, PC, LA, Protected_surface, Prot, ProtConn, ProtUnconn, RelConn, ProtUnconn_Design, ProtConn_Bound, Unprotected and the fractions ProtConn_Prot, _Trans, _Unprot, _Within, _Contig and their _land shares (in % of the region).

See Also

protconn_regions : one row per region. protconn_raster : protected areas given as a raster. connectivity_grid : ProtConn of every cell of a grid.

Examples:

Four protected areas in a region of 11 x 11 km:

>>> import geopandas as gpd
>>> import shapely
>>> import landschaft as ls
>>> reserves = gpd.GeoDataFrame({"name": ["a", "b", "c", "d"]}, crs="EPSG:3035", geometry=[
...     shapely.box(0, 0, 2000, 2000), shapely.box(3000, 0, 4500, 1500),
...     shapely.box(0, 5000, 1000, 6000), shapely.box(9000, 9000, 11000, 11000)])
>>> region = gpd.GeoDataFrame(geometry=[shapely.box(-1000, -1000, 10000, 10000)], crs="EPSG:3035")
>>> table = ls.conn.protconn(reserves, region, distance=2000, probability=0.5)
>>> table[["distance", "Prot", "ProtConn"]].round(2)
   distance  Prot  ProtConn
0      2000  6.82      5.25

With delta, the loss of ProtConn when each area is removed:

>>> table, areas = ls.conn.protconn(reserves, region, distance=2000, delta=True)
>>> "dProtConn" in areas.columns
True

protconn_regions

protconn_regions(
    protected_areas, regions, *, id: str | None = None, **kwargs
) -> DataFrame

ProtConn of several regions, one row per region and distance (Makurhini's MK_ProtConnMult).

Each region (a row of regions) is analysed as protconn() does, with the same protected areas.

Parameters:

Name Type Description Default
protected_areas

Polygons of the protected areas (GeoDataFrame, projected in metres).

required
regions

GeoDataFrame with one region per row.

required
id str | None

Column of the region ids (default 1 .. n).

None
**kwargs

Options of protconn() (distance is required).

{}

Returns:

Type Description
DataFrame

region and the columns of protconn(), one row per region and distance.

Examples:

>>> import geopandas as gpd
>>> import shapely
>>> import landschaft as ls
>>> reserves = gpd.GeoDataFrame({"name": ["a", "b", "c", "d"]}, crs="EPSG:3035", geometry=[
...     shapely.box(0, 0, 2000, 2000), shapely.box(3000, 0, 4500, 1500),
...     shapely.box(0, 5000, 1000, 6000), shapely.box(9000, 9000, 11000, 11000)])
>>> region = gpd.GeoDataFrame(geometry=[shapely.box(-1000, -1000, 10000, 10000)], crs="EPSG:3035")
>>> regions = gpd.GeoDataFrame({"zone": ["west", "east"]}, crs="EPSG:3035", geometry=[
...     shapely.box(-1000, -1000, 4000, 10000), shapely.box(4000, -1000, 12000, 12000)])
>>> table = ls.conn.protconn_regions(reserves, regions, id="zone", distance=2000)
>>> table[["region", "distance", "Prot", "ProtConn"]].round(2)
  region  distance   Prot  ProtConn
0   west      2000  11.82     10.17
1   east      2000   4.57      3.95

protconn_raster

protconn_raster(
    protected_areas,
    region,
    *,
    distance,
    probability: float | None = 0.5,
    transboundary: float | None = None,
    transboundary_type: str = "nodes",
    bound: bool = False,
    area_unit: str = "m2",
    pij_min: float | None = None,
    nodata=None,
    band=None,
    n_threads: int | None = None,
) -> DataFrame

ProtConn of a region from a raster of protected areas, as Makurhini's MK_ProtConn_raster.

Parameters:

Name Type Description Default
protected_areas

Raster of protected-area ids (NA / nodata elsewhere).

required
region

Polygons of the region (rasterized on the protected areas' grid, cells whose centre is inside; with bound each part is a region node), or a raster of it (its 4-connected patches).

required
distance

As in protconn().

required
probability

As in protconn().

required
transboundary

As in protconn().

required
transboundary_type

As in protconn().

required
bound

As in protconn().

required
area_unit

As in protconn().

required
pij_min

As in protconn().

required
nodata

Reading options of the rasters.

None
band

Reading options of the rasters.

None

Returns:

Type Description
DataFrame

The columns of protconn(), one row per distance.

Notes

Every protected-area id inside the region is a node (not dissolved), its attribute the area of its cells there; transboundary nodes are the ids outside the region within transboundary of the region's protected cells ("nodes") or of the region ("region"), when there are at least two of them (as Makurhini). Distances run between the nodes' centroids (R's means of the cell centres). Makurhini's quirks are kept: ProtConn[Within] sums every node's attribute (transboundary and region ones included). Makurhini pairs the attributes (by id) with centroids in the order ids first appear in the raster; landschaft keeps each id with its own centroid (the same when ids appear in increasing order).

Examples:

The broad-leaved forest of the example map as protected areas (each patch with its own id):

>>> import landschaft as ls
>>> landcover = ls.load_raster(ls.example_data("clc"))
>>> import geopandas as gpd
>>> import shapely
>>> labels = ls.Landscape(landcover.where(landcover == 23)).labels()
>>> region = gpd.GeoDataFrame(geometry=[shapely.box(*landcover.rio.bounds())], crs=landcover.rio.crs)
>>> table = ls.conn.protconn_raster(labels, region, distance=1000, area_unit="ha")
>>> table[["distance", "Prot", "ProtConn"]].round(2)
   distance  Prot  ProtConn
0      1000  7.71      2.76

protconn_regions

protconn_regions(
    protected_areas, regions, *, id: str | None = None, **kwargs
) -> DataFrame

ProtConn of several regions, one row per region and distance (Makurhini's MK_ProtConnMult).

Each region (a row of regions) is analysed as protconn() does, with the same protected areas.

Parameters:

Name Type Description Default
protected_areas

Polygons of the protected areas (GeoDataFrame, projected in metres).

required
regions

GeoDataFrame with one region per row.

required
id str | None

Column of the region ids (default 1 .. n).

None
**kwargs

Options of protconn() (distance is required).

{}

Returns:

Type Description
DataFrame

region and the columns of protconn(), one row per region and distance.

Examples:

>>> import geopandas as gpd
>>> import shapely
>>> import landschaft as ls
>>> reserves = gpd.GeoDataFrame({"name": ["a", "b", "c", "d"]}, crs="EPSG:3035", geometry=[
...     shapely.box(0, 0, 2000, 2000), shapely.box(3000, 0, 4500, 1500),
...     shapely.box(0, 5000, 1000, 6000), shapely.box(9000, 9000, 11000, 11000)])
>>> region = gpd.GeoDataFrame(geometry=[shapely.box(-1000, -1000, 10000, 10000)], crs="EPSG:3035")
>>> regions = gpd.GeoDataFrame({"zone": ["west", "east"]}, crs="EPSG:3035", geometry=[
...     shapely.box(-1000, -1000, 4000, 10000), shapely.box(4000, -1000, 12000, 12000)])
>>> table = ls.conn.protconn_regions(reserves, regions, id="zone", distance=2000)
>>> table[["region", "distance", "Prot", "ProtConn"]].round(2)
  region  distance   Prot  ProtConn
0   west      2000  11.82     10.17
1   east      2000   4.57      3.95

protconn_raster

protconn_raster(
    protected_areas,
    region,
    *,
    distance,
    probability: float | None = 0.5,
    transboundary: float | None = None,
    transboundary_type: str = "nodes",
    bound: bool = False,
    area_unit: str = "m2",
    pij_min: float | None = None,
    nodata=None,
    band=None,
    n_threads: int | None = None,
) -> DataFrame

ProtConn of a region from a raster of protected areas, as Makurhini's MK_ProtConn_raster.

Parameters:

Name Type Description Default
protected_areas

Raster of protected-area ids (NA / nodata elsewhere).

required
region

Polygons of the region (rasterized on the protected areas' grid, cells whose centre is inside; with bound each part is a region node), or a raster of it (its 4-connected patches).

required
distance

As in protconn().

required
probability

As in protconn().

required
transboundary

As in protconn().

required
transboundary_type

As in protconn().

required
bound

As in protconn().

required
area_unit

As in protconn().

required
pij_min

As in protconn().

required
nodata

Reading options of the rasters.

None
band

Reading options of the rasters.

None

Returns:

Type Description
DataFrame

The columns of protconn(), one row per distance.

Notes

Every protected-area id inside the region is a node (not dissolved), its attribute the area of its cells there; transboundary nodes are the ids outside the region within transboundary of the region's protected cells ("nodes") or of the region ("region"), when there are at least two of them (as Makurhini). Distances run between the nodes' centroids (R's means of the cell centres). Makurhini's quirks are kept: ProtConn[Within] sums every node's attribute (transboundary and region ones included). Makurhini pairs the attributes (by id) with centroids in the order ids first appear in the raster; landschaft keeps each id with its own centroid (the same when ids appear in increasing order).

Examples:

The broad-leaved forest of the example map as protected areas (each patch with its own id):

>>> import landschaft as ls
>>> landcover = ls.load_raster(ls.example_data("clc"))
>>> import geopandas as gpd
>>> import shapely
>>> labels = ls.Landscape(landcover.where(landcover == 23)).labels()
>>> region = gpd.GeoDataFrame(geometry=[shapely.box(*landcover.rio.bounds())], crs=landcover.rio.crs)
>>> table = ls.conn.protconn_raster(labels, region, distance=1000, area_unit="ha")
>>> table[["distance", "Prot", "ProtConn"]].round(2)
   distance  Prot  ProtConn
0      1000  7.71      2.76

protconn_nodes

protconn_nodes(
    protected_areas,
    region,
    *,
    transboundary: float | None = None,
    transboundary_type: str = "nodes",
    bound: bool = False,
    area_unit: str = "m2",
    preselect: bool = True,
) -> ProtConnNodes

Nodes of ProtConn in a region, as Makurhini's Protconn_nodes builds them.

The protected areas that overlap the region are cut to it, dissolved and split into separate polygons (the nodes, with their area); with transboundary (a distance) the parts of those areas outside the region and the other protected areas within that distance of them (transboundary_type="nodes") or of the region ("region"), cut to that zone, become transboundary nodes (attribute 0: stepping stones); bound adds the parts of the region itself (ProtConn[Bound]), as Makurhini does only when there are transboundary nodes.

preselect=False uses all the given areas as candidates (the grid's selection, made once for its whole region).

Makurhini simplifies the polygons of the zones with mapshaper (5 % of the vertices) before buffering them; landschaft buffers the exact polygons (with GEOS' 30 segments per quarter circle, as sf).

Parameters:

Name Type Description Default
protected_areas

Polygons of the protected areas (GeoDataFrame, projected in metres).

required
region

The region (one or more polygons, merged).

required
transboundary float | None

As in protconn().

None
transboundary_type float | None

As in protconn().

None
bound float | None

As in protconn().

None
area_unit float | None

As in protconn().

None
preselect bool

Only use the protected areas near the region.

True

Returns:

Type Description
ProtConnNodes
See Also

protconn_indicators : ProtConn from these nodes and their distances.

Examples:

>>> import geopandas as gpd
>>> import shapely
>>> import landschaft as ls
>>> reserves = gpd.GeoDataFrame({"name": ["a", "b", "c", "d"]}, crs="EPSG:3035", geometry=[
...     shapely.box(0, 0, 2000, 2000), shapely.box(3000, 0, 4500, 1500),
...     shapely.box(0, 5000, 1000, 6000), shapely.box(9000, 9000, 11000, 11000)])
>>> region = gpd.GeoDataFrame(geometry=[shapely.box(-1000, -1000, 10000, 10000)], crs="EPSG:3035")
>>> nodes = ls.conn.protconn_nodes(reserves, region, area_unit="ha")
>>> nodes.nodes["type"].tolist()
['Non-Transboundary', 'Non-Transboundary', 'Non-Transboundary', 'Non-Transboundary']
>>> float(nodes.la)
12100.0

ProtConnNodes dataclass

ProtConnNodes(nodes: object, pieces: ndarray, clipped: object, la: float)

The nodes of a ProtConn analysis of one region (protconn_nodes()).

Attributes:

Name Type Description
nodes GeoDataFrame

type ("Non-Transboundary", "Transboundary" or "Region") and attribute (the area in the region; 0 for the others).

pieces ndarray

Areas of the protected areas' pieces in the region before they are dissolved (ProtConn[Within]).

clipped GeoDataFrame

Every protected area cut to the region (the nodes of the importance of each area).

la float

The area of the region.

Examples:

>>> import geopandas as gpd
>>> import shapely
>>> import landschaft as ls
>>> reserves = gpd.GeoDataFrame({"name": ["a", "b", "c", "d"]}, crs="EPSG:3035", geometry=[
...     shapely.box(0, 0, 2000, 2000), shapely.box(3000, 0, 4500, 1500),
...     shapely.box(0, 5000, 1000, 6000), shapely.box(9000, 9000, 11000, 11000)])
>>> region = gpd.GeoDataFrame(geometry=[shapely.box(-1000, -1000, 10000, 10000)], crs="EPSG:3035")
>>> nodes = ls.conn.protconn_nodes(reserves, region)
>>> len(nodes.nodes), len(nodes.pieces)
(4, 4)

protconn_indicators

protconn_indicators(
    nodes,
    distances,
    *,
    distance: float,
    la: float,
    pieces=None,
    probability: float | None = 0.5,
    transboundary_weight: float = 1.0,
    pij_min: float | None = None,
) -> dict

ProtConn and its fractions from prepared nodes and distances (Makurhini's get_protconn_grid).

Parameters:

Name Type Description Default
nodes

Table with type ("Non-Transboundary", "Transboundary", "Region") and attribute, one row per node (e.g. protconn_nodes()).

required
distances

Distances between the nodes: n × n matrix (NaN = none) or (from, to, distance) table with node positions 1..n.

required
distance float

The dispersal distance and its probability (None: exp(-1)).

required
probability float

The dispersal distance and its probability (None: exp(-1)).

required
la float

The area of the region.

required
pieces

Areas of the protected pieces before dissolving (ProtConn[Within]); default: the node attributes.

None
transboundary_weight float

a_i a_j of the pairs with a transboundary (or region) node: Makurhini uses 1; 0 counts them as stepping stones only (Saura et al. 2017).

1.0
pij_min float | None

Links with a lower probability are cut (None: none).

None

Returns:

Type Description
dict

The indicators of protconn() (ECA, PC, Prot, ProtConn, its fractions...) for this distance.

Examples:

Distances between the nodes of protconn_nodes(), then the indicators:

>>> import geopandas as gpd
>>> import shapely
>>> import landschaft as ls
>>> reserves = gpd.GeoDataFrame({"name": ["a", "b", "c", "d"]}, crs="EPSG:3035", geometry=[
...     shapely.box(0, 0, 2000, 2000), shapely.box(3000, 0, 4500, 1500),
...     shapely.box(0, 5000, 1000, 6000), shapely.box(9000, 9000, 11000, 11000)])
>>> region = gpd.GeoDataFrame(geometry=[shapely.box(-1000, -1000, 10000, 10000)], crs="EPSG:3035")
>>> import numpy as np
>>> nodes = ls.conn.protconn_nodes(reserves, region)
>>> geometry = nodes.nodes.geometry
>>> distances = np.array([[a.distance(b) if i != j else np.nan for j, b in enumerate(geometry)]
...                       for i, a in enumerate(geometry)])
>>> result = ls.conn.protconn_indicators(nodes.nodes, distances, distance=2000, la=nodes.la,
...                                      pieces=nodes.pieces)
>>> round(float(result["ProtConn"]), 2)
5.25

Grids

make_grid

make_grid(
    region,
    cell_area: float,
    *,
    hexagonal: bool = True,
    clip: bool = False,
    grid_boundary: bool = False,
)

Grid of cells of cell_area over a region, as Makurhini's make_grid.

Hexagons come from sf's st_make_grid(square = FALSE) (the same coordinates), squares from a raster over the region's extent grown by one cell; the cells that touch the region are kept. clip cuts them to the region, grid_boundary keeps only whole cells inside it.

Parameters:

Name Type Description Default
region

Polygons of the region (GeoDataFrame, projected).

required
cell_area float

Area of each cell, in square map units.

required
hexagonal bool

Hexagons (default) or squares.

True
clip bool

Cut the cells to the region.

False
grid_boundary bool

Keep only the cells entirely inside the region.

False

Returns:

Type Description
GeoDataFrame

OBJECTID and the cell polygons.

Examples:

Hexagons of 25 km² over a region of 11 x 11 km:

>>> import geopandas as gpd
>>> import shapely
>>> import landschaft as ls
>>> region = gpd.GeoDataFrame(geometry=[shapely.box(-1000, -1000, 10000, 10000)], crs="EPSG:3035")
>>> grid = ls.conn.make_grid(region, cell_area=25e6)
>>> len(grid)
12

connectivity_grid

connectivity_grid(
    protected_areas,
    region,
    *,
    distance: float,
    grid=None,
    cell_area: float | None = None,
    hexagonal: bool = True,
    protconn_index: bool = True,
    probability: float = 0.5,
    transboundary: float | None = None,
    area_unit: str = "m2",
    pij_min: float | None = None,
)

ProtConn (or PC and ECA) in every cell of a grid, as Makurhini's MK_Connect_grid.

Parameters:

Name Type Description Default
protected_areas

Polygons of the protected areas (or habitat patches).

required
region

The study region: the grid covers it, and only the areas within transboundary of it are used.

required
distance float

Dispersal distance and its probability.

required
probability float

Dispersal distance and its probability.

required
grid

A grid of polygons, or the area of the cells of a new one (make_grid()).

None
cell_area

A grid of polygons, or the area of the cells of a new one (make_grid()).

None
hexagonal

A grid of polygons, or the area of the cells of a new one (make_grid()).

None
protconn_index bool

True: ProtConn and its fractions of each cell (the cell as the region, the areas in the other cells transboundary with transboundary). False: PC, ECA and normalised ECA of the patches dissolved in each cell.

True
pij_min float | None

ProtConn: links below this probability are cut. Makurhini's MK_Connect_grid turns its default (NULL) into infinity, which cuts every link (each node alone): pij_min=np.inf gives its values.

None
transboundary float | None

ProtConn: distance of the transboundary zone of each cell.

None
area_unit str

Unit of the areas.

'm2'

Returns:

Type Description
GeoDataFrame

The grid with OBJECTID and, per cell, the columns of protconn() (or ECA, PC and Normalized_ECA with protconn_index=False).

Examples:

>>> import geopandas as gpd
>>> import shapely
>>> import landschaft as ls
>>> reserves = gpd.GeoDataFrame({"name": ["a", "b", "c", "d"]}, crs="EPSG:3035", geometry=[
...     shapely.box(0, 0, 2000, 2000), shapely.box(3000, 0, 4500, 1500),
...     shapely.box(0, 5000, 1000, 6000), shapely.box(9000, 9000, 11000, 11000)])
>>> region = gpd.GeoDataFrame(geometry=[shapely.box(-1000, -1000, 10000, 10000)], crs="EPSG:3035")
>>> cells = ls.conn.connectivity_grid(reserves, region, distance=2000, cell_area=25e6)
>>> cells.columns.tolist()[:6]
['OBJECTID', 'ECA', 'PC', 'LA', 'Protected_surface', 'Prot']
>>> len(cells)
12

eca_change_grid

eca_change_grid(
    times,
    region,
    *,
    distance: float,
    metric: str = "IIC",
    grid=None,
    cell_area: float | None = None,
    hexagonal: bool = True,
    probability: float | None = None,
    pij_min: float | None = None,
    names=None,
    area_unit: str = "m2",
)

ECA change between times in every cell of a grid, as Makurhini's MK_dECA_grid.

Parameters:

Name Type Description Default
times

Habitat polygons (GeoDataFrames), one per time.

required
region required
grid required
cell_area required
hexagonal required
distance float

As in eca_change() (centroid distances between the pieces of the patches in each cell).

required
metric float

As in eca_change() (centroid distances between the pieces of the patches in each cell).

required
probability float

As in eca_change() (centroid distances between the pieces of the patches in each cell).

required
pij_min float

As in eca_change() (centroid distances between the pieces of the patches in each cell).

required
names

Names of the times (default T1, T2, ...); pairs are named "T1.T2".

None

Returns:

Type Description
GeoDataFrame

One row per cell and pair of consecutive times: time, ECA_i, ECA_j, ECAnorm_i, ECAnorm_j (in % of the cell), dA, dECA, rECA, comparison, change. Makurhini's rules for cells without habitat at one time are kept: habitat that disappears gives dA = dECA = -100 (and ECA_i the area of its pieces when there are two or fewer), habitat that appears gives ECA_j = the cell's area and dA = dECA = 100, and none at both times gives NA.

Examples:

>>> import geopandas as gpd
>>> import shapely
>>> import landschaft as ls
>>> reserves = gpd.GeoDataFrame({"name": ["a", "b", "c", "d"]}, crs="EPSG:3035", geometry=[
...     shapely.box(0, 0, 2000, 2000), shapely.box(3000, 0, 4500, 1500),
...     shapely.box(0, 5000, 1000, 6000), shapely.box(9000, 9000, 11000, 11000)])
>>> region = gpd.GeoDataFrame(geometry=[shapely.box(-1000, -1000, 10000, 10000)], crs="EPSG:3035")
>>> later = reserves.copy()
>>> later.loc[0, "geometry"] = shapely.box(0, 0, 2800, 2000)       # area "a" grows
>>> cells = ls.conn.eca_change_grid([reserves, later], region, distance=2000, cell_area=25e6)
>>> cells.columns.tolist()[:4]
['OBJECTID', 'time', 'ECA_i', 'ECA_j']

Change and neighbourhoods

eca_change

eca_change(
    graphs: Sequence[HabitatGraph],
    metric: str = "PC",
    *,
    distance,
    landscape_area: float,
    probability: float | None = None,
    pij_min: float | None = None,
    names=None,
) -> DataFrame

ECA of habitat graphs over time and its change, as Makurhini's MK_dECA.

Parameters:

Name Type Description Default
graphs Sequence[HabitatGraph]

One HabitatGraph per time (or scenario), in order.

required
metric str

"PC" or "IIC": the ECA is EC(PC) or EC(IIC).

'PC'
distance

As in overall() (several distances give one block of rows per distance).

required
probability

As in overall() (several distances give one block of rows per distance).

required
pij_min

As in overall() (several distances give one block of rows per distance).

required
landscape_area float

LA, the maximum landscape attribute: the first time is compared with it, and the ECA cannot exceed it.

required
names

Names of the times (default 1, 2, ...).

None

Returns:

Type Description
DataFrame

time, LA, area (habitat area: the areas of the nodes of graphs made with from_raster / from_vector, else the sum of the attributes), distance, ECA, ECA_LA and ECA_area (ECA in % of LA and of the habitat area), dA and dECA (change in % from the previous time, the first from LA), rECA (dECA / dA) and Makurhini's two readings of them, comparison and change.

Examples:

Area "a" grows between two times:

>>> import geopandas as gpd
>>> import shapely
>>> import landschaft as ls
>>> reserves = gpd.GeoDataFrame({"name": ["a", "b", "c", "d"]}, crs="EPSG:3035", geometry=[
...     shapely.box(0, 0, 2000, 2000), shapely.box(3000, 0, 4500, 1500),
...     shapely.box(0, 5000, 1000, 6000), shapely.box(9000, 9000, 11000, 11000)])
>>> region = gpd.GeoDataFrame(geometry=[shapely.box(-1000, -1000, 10000, 10000)], crs="EPSG:3035")
>>> later = reserves.copy()
>>> later.loc[0, "geometry"] = shapely.box(0, 0, 2800, 2000)
>>> graphs = [ls.HabitatGraph.from_vector(g, id="name", area_unit="ha", distance="edge",
...                                       landscape_area=12100) for g in (reserves, later)]
>>> table = ls.conn.eca_change(graphs, "PC", distance=2000, probability=0.5,
...                            landscape_area=12100)
>>> table[["time", "dA", "dECA", "change"]].round(1)
   time    dA  dECA                        change
0     1 -90.7 -93.8           + Connectivity loss
1     2  14.2  22.5  Habitat or connectivity gain

focal_nodes

focal_nodes(
    nodes,
    *,
    distance,
    search_buffer,
    metric: str = "IIC",
    probability: float | None = None,
    pij_min: float | None = 0.01,
    id: str | None = None,
    attribute=None,
    area_unit: str = "ha",
    threshold: float | None = None,
    n_threads: int | None = None,
) -> DataFrame

IIC or PC of every node's neighbourhood and the node's importance in it (MK_Focal_nodes).

For every node (the focal node), the nodes whose polygons intersect its buffer of search_buffer form a graph (distances between their points on surface, the landscape area the area of their bounding box); with three or more nodes its IIC or PC and the focal node's dIIC or dPC (and fractions) are those of Makurhini's MK_dPCIIC.

Parameters:

Name Type Description Default
nodes

Polygons (GeoDataFrame or GeoSeries), in a projected CRS.

required
distance

Dispersal distance (or distances) of the links or of probability.

required
search_buffer

Radius of the neighbourhood, one for all distances or one per distance.

required
metric str

"IIC" (Makurhini's default) or "PC".

'IIC'
probability float | None

Probability at distance for PC (default 0.5).

None
pij_min float | None

Probability cut of PC (Makurhini's 0.01).

0.01
id str | None

Column with the node ids (default 1..n).

None
attribute

Column name or values of the node attribute (default: the area in area_unit).

None
area_unit str

Unit of the areas and of the landscape areas (Makurhini: always ha).

'ha'
threshold float | None

Pairs farther apart than this are not connected (Makurhini's threshold).

None
n_threads int | None

Threads (default: all cores but one).

None

Returns:

Type Description
DataFrame

One row per node and distance: id, distance, attribute, n_nodes (the neighbourhood), EC(IIC), IIC, dIIC, dIICintra, dIICflux, dIICconnector (PC: the same with PC) and IComp = IIC × dIIC, the composite connectivity index. Nodes with fewer than three nodes around them have NaN.

Notes

Makurhini simplifies the focal polygon (mapshaper, 90 % of the vertices) before buffering it; landschaft buffers the polygon itself, which only changes the neighbourhood when a node lies at the very edge of the buffer.

Examples:

>>> import geopandas as gpd
>>> import shapely
>>> import landschaft as ls
>>> reserves = gpd.GeoDataFrame({"name": ["a", "b", "c", "d"]}, crs="EPSG:3035", geometry=[
...     shapely.box(0, 0, 2000, 2000), shapely.box(3000, 0, 4500, 1500),
...     shapely.box(0, 5000, 1000, 6000), shapely.box(9000, 9000, 11000, 11000)])
>>> region = gpd.GeoDataFrame(geometry=[shapely.box(-1000, -1000, 10000, 10000)], crs="EPSG:3035")
>>> table = ls.conn.focal_nodes(reserves, id="name", distance=2000, search_buffer=12000)
>>> table[["id", "n_nodes", "dIIC"]].round(1)
  id  n_nodes  dIIC
0  a        4  42.0
1  b        4  13.3
2  c        4   2.6
3  d        4  42.0

Networks of MSPA cores and bridges

gtn_components

gtn_components(
    data: DataArray | ndarray, *, pixel_size: float | tuple[float, float] | None = None
) -> GTNComponentsResult

Components of the habitat network formed by MSPA Cores and Bridges.

The network of GuidosToolbox's nw_components: Core patches are the nodes and Bridges the links. Pixels of Core or Bridge are grouped into components (8-connected); only Core pixels count as habitat area. For the whole map:

  • ECA (Equivalent Connected Area) = sqrt(sum of the squared core areas of the components);
  • ECA_max, the ECA if all the core area formed one component;
  • DOC (degree of coherence) = ECA / ECA_max in %.

Parameters:

Name Type Description Default
data DataArray | ndarray

MSPA map (mspa() classes), DataArray or array.

required
pixel_size float | tuple[float, float] | None

Pixel size in metres (e.g. 25 or (25, 25)) for the areas in hectares; read from a georeferenced DataArray when omitted.

None

Returns:

Type Description
GTNComponentsResult
See Also

gtn_importance : importance of every link and stepping-stone node. HabitatGraph : networks of patches with distance-based links.

Examples:

>>> import landschaft as ls
>>> forest = ls.load_raster(ls.example_data("input"))
>>> morph = ls.mspa(forest)
>>> network = ls.gtn_components(morph)
>>> network.num_components, round(network.doc, 2)
(418, 56.71)
>>> network.stats.head(3)
   component_id  core_pixels  bridge_pixels  total_pixels  has_bridge
0             1          199             21           220        True
1             2          103              0           103       False
2             3           12             10            22        True

GTNComponentsResult dataclass

GTNComponentsResult(
    components: DataArray | ndarray,
    colored_map: DataArray | ndarray,
    stats: DataFrame,
    eca: float,
    eca_pixels: float,
    eca_max: float,
    eca_max_pixels: int,
    doc: float,
    num_components: int,
    num_isolated_cores: int,
    num_connected_components: int,
    num_nodes: int,
    num_links: int,
    pixel_area_ha: float | None = None,
)

Bases: Saveable

Result of gtn_components().

Attributes:

Name Type Description
components DataArray or ndarray

Component id of every Core and Bridge pixel (0 elsewhere).

colored_map DataArray or ndarray

Map for display: connected networks and isolated cores in their colours.

stats DataFrame

One row per component: component_id, core_pixels, bridge_pixels, total_pixels, has_bridge and the areas.

eca, eca_pixels

Equivalent Connected Area, in hectares (when the pixel size is known) and in pixels.

eca_max, eca_max_pixels

ECA if all Core pixels formed one component.

doc float

Degree of coherence, ECA / ECA_max in %.

num_components, num_isolated_cores, num_connected_components

Number of components, of components without bridges and of networks.

num_nodes, num_links

Number of Core patches and Bridge links.

pixel_area_ha float or None

Area of a pixel in hectares.

Examples:

>>> import landschaft as ls
>>> forest = ls.load_raster(ls.example_data("input"))
>>> morph = ls.mspa(forest)
>>> network = ls.gtn_components(morph)
>>> network.num_components
418
>>> print(network.summary())
Network components: 418
...

summary

summary() -> str

Text summary, as GuidosToolbox's log.

Returns:

Type Description
str

gtn_importance

gtn_importance(
    data: DataArray | ndarray, *, components: GTNComponentsResult | None = None
) -> GTNImportanceResult

Importance of every link (Bridge) and stepping-stone node (Core) of the MSPA network.

GuidosToolbox's nw_importance: the importance of a link is the loss of connectivity (dPC: the drop of the numerator of PC, in %) when it is cut; the importance of a node with two or more links, its contribution to connecting the others. Maps show links positive and nodes negative, and classify the top 1 %, 5 % and 10 %.

Parameters:

Name Type Description Default
data DataArray | ndarray

MSPA map (mspa() classes), DataArray or array.

required
components GTNComponentsResult | None

A gtn_components() result of the same map (computed when omitted).

None

Returns:

Type Description
GTNImportanceResult

Examples:

>>> import landschaft as ls
>>> forest = ls.load_raster(ls.example_data("input"))
>>> morph = ls.mspa(forest)
>>> importance = ls.gtn_importance(morph)
>>> importance.links.columns.tolist()
['link_id', 'component_id', 'pixels', 'dpc', 'connected_nodes', 'normalized_importance']
>>> importance.nodes.head(2)
   node_id  component_id  pixels       dpc  degree  normalized_importance
0        6             4     129  0.000011       2               0.000054
1        7             4      30  0.000007       2               0.000037

GTNImportanceResult dataclass

GTNImportanceResult(
    importance_map: DataArray | ndarray,
    node_link_ids: DataArray | ndarray,
    colored_map: DataArray | ndarray,
    links: DataFrame,
    nodes: DataFrame,
    max_link_dpc: float,
    max_node_dpc: float,
    num_components: int,
    num_links: int,
    num_nodes: int,
)

Bases: Saveable

Result of gtn_importance().

Attributes:

Name Type Description
importance_map DataArray or ndarray

dPC of every link (Bridge, positive) and stepping-stone node (Core, negative) on their pixels.

node_link_ids DataArray or ndarray

Link ids (positive) and node ids (negative) on their pixels.

colored_map DataArray or ndarray

Importance classes for display (top 1 %, 5 %, 10 %...).

links DataFrame

One row per link: link_id, component_id, pixels, dpc, connected_nodes, normalized_importance.

nodes DataFrame

One row per stepping-stone node (degree 2 or more): node_id, component_id, pixels, dpc, degree, normalized_importance.

max_link_dpc, max_node_dpc float

Largest dPC of a link and of a node.

num_components, num_links, num_nodes int

Counts of the network.

Examples:

>>> import landschaft as ls
>>> forest = ls.load_raster(ls.example_data("input"))
>>> morph = ls.mspa(forest)
>>> importance = ls.gtn_importance(morph)
>>> round(importance.max_link_dpc, 2)
19.87

summary_text

summary_text(title: str = 'landschaft Connectivity Importance') -> str

Text report, as GuidosToolbox's _cs.txt.

Parameters:

Name Type Description Default
title str

First line of the report.

'landschaft Connectivity Importance'

Returns:

Type Description
str

conefor_inputs

conefor_inputs(
    data: DataArray | ndarray,
    output_dir: str | Path | None = None,
    prefix: str = "mspa",
) -> tuple[DataFrame, DataFrame]

Nodes and links of the MSPA network as Conefor Sensinode input files.

GuidosToolbox's nw_cs22 / batch_mspaci: nodes are the Core patches (negative ids, area in pixels) and the Bridges (positive ids, area 0); links join each Bridge to the nodes it touches, at topological distance 1.

Parameters:

Name Type Description Default
data DataArray | ndarray

MSPA map (mspa() classes), DataArray or array.

required
output_dir str | Path | None

Folder where <prefix>_nodes_mspa.txt and <prefix>_links_mspa.txt are written (nothing is written when omitted).

None
prefix str

Prefix of the file names.

'mspa'

Returns:

Type Description
nodes, links : pandas.DataFrame

id and pixels per node; link_id, node_id and distance per link.

Examples:

>>> import landschaft as ls
>>> forest = ls.load_raster(ls.example_data("input"))
>>> morph = ls.mspa(forest)
>>> nodes, links = ls.conefor_inputs(morph, output_dir=".")
>>> nodes.head(3)
   id  pixels
0  -1     194
1  -2     103
2  -3      10
>>> links.head(3)
   link_id  node_id  distance
0        1      -12         1
1        2       -3         1
2        2       -4         1