Biodiversity analysis¶
Species (labels) counted in groups (grid cells, sites or other units): the
data behind analyses of diversity, endemism and phylogenetic patterns. They
live in the ls.bio module, a port of Biodiverse (Laffan, Lubarsky and
Rosauer 2010).
Reading data gives the same groups, labels and sample counts as Biodiverse
with the same settings: coordinates are snapped to the cell grid the same
way and groups and labels get the same names (coordinates or name parts joined
with :), so results can be compared one to one. The
biodiversity tutorial walks through a complete
study.
import landschaft as ls
bd = ls.bio.BaseData.from_table("sites.csv", group_columns=["x", "y"],
label_columns=["genus", "species"],
cell_sizes=[100000, 100000])
tree = ls.bio.read_tree("tree.nex")
bd.to_raster(bd.group_richness) # species richness map
# a GBIF download, in 50 km cells of an equal-area projection
bd = ls.bio.BaseData.from_occurrences("0012345-260928.zip", [50_000, 50_000],
to_crs="EPSG:6933", max_uncertainty=10_000)
# a stack of species distribution models, presence above 0.5
bd = ls.bio.BaseData.from_raster("sdm_stack.tif", threshold=0.5)
Sample counts per group¶
BaseData
¶
BaseData(
counts: spmatrix,
groups: Sequence[str],
labels: Sequence[str],
coords: ndarray,
cell_sizes: Sequence[float],
cell_origins: Sequence[float] | None = None,
crs: Any = None,
group_axes: Sequence[Sequence[str]] | None = None,
)
Groups × labels sample counts with the group coordinates.
Build one with from_table() (Biodiverse's text import),
from_dataframe(), from_sparse(), from_xarray(),
from_raster() or from_points().
Attributes:
| Name | Type | Description |
|---|---|---|
groups |
list of str
|
Group names, sorted (Biodiverse's names: coordinates joined with |
labels |
list of str
|
Label names, sorted. |
counts |
csr_matrix
|
Sample counts, |
coords |
ndarray
|
Group coordinates |
cell_sizes, cell_origins |
tuple of float
|
Cell size per axis (0: coordinates kept as given, negative: text axis) and grid origin. |
crs |
optional
|
Coordinate reference system of the group coordinates (anything
|
label_range
property
¶
Number of groups each label occurs in (Biodiverse's range).
label_sample_count
property
¶
Total sample count of each label (summed over groups in name order).
group_sample_count
property
¶
Total sample count of each group (summed over labels in name order).
set_label_properties
¶
Assign label properties (Biodiverse's assign_element_properties).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
properties
|
DataFrame
|
Indexed by label name, one column per property (see
|
required |
Examples:
>>> import pandas as pd
>>> import landschaft as ls
>>> df = pd.DataFrame({"sp": ["a", "b"], "x": [0.5, 1.5], "y": [0.5, 0.5]})
>>> bd = ls.bio.BaseData.from_dataframe(df, ["x", "y"], "sp", cell_sizes=[1, 1])
>>> bd.set_label_properties(pd.DataFrame({"MASS": [1.5, 20]}, index=["a", "b"]))
>>> bd.label_properties["MASS"].tolist()
['1.5', '20']
set_group_properties
¶
Assign group properties (Biodiverse's assign_element_properties).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
properties
|
DataFrame
|
Indexed by group name, one column per property (see
|
required |
Examples:
>>> import pandas as pd
>>> import landschaft as ls
>>> df = pd.DataFrame({"sp": ["a", "b"], "x": [0.5, 1.5], "y": [0.5, 0.5]})
>>> bd = ls.bio.BaseData.from_dataframe(df, ["x", "y"], "sp", cell_sizes=[1, 1])
>>> bd.set_group_properties(pd.DataFrame({"TEMP": [12.5, 14]}, index=bd.groups))
>>> bd.group_properties["TEMP"].tolist()
['12.5', '14']
labels_in_group
¶
Labels of group with their sample counts.
groups_with_label
¶
Groups where label occurs, with their sample counts.
group_axes
¶
Axis values of a group as Biodiverse stores them (strings).
to_table
¶
Long table: one row per (group, label) with its count and coordinates.
to_raster
¶
to_raster(
values: Mapping[str, float] | Series | ndarray,
name: str | None = None,
fill: float = nan,
)
Grid of per-group values (two numeric axes needed).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
values
|
Series (indexed by group), mapping or array (group order)
|
The values to place in each group's cell. |
required |
name
|
str
|
Name of the returned DataArray. |
None
|
fill
|
float
|
Value of cells without a group. |
nan
|
Returns:
| Type | Description |
|---|---|
DataArray
|
|
to_geodataframe
¶
Cells as polygons (needs geopandas), optionally with per-group values.
from_hex
classmethod
¶
from_hex(
data: DataFrame,
x: Hashable,
y: Hashable,
label_column: Hashable | Sequence[Hashable],
size: float,
*,
orientation: str = "pointy",
origin: Sequence[float] = (0.0, 0.0),
count_column: Hashable | None = None,
crs: Any = None,
) -> BaseData
Records aggregated into a hexagonal grid.
Groups are the hexagons' centres ("x:y"), so the distance-based
spatial conditions (sp_circle...) work on them; cells are drawn
as hexagons by to_geodataframe().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
One row per record. |
required |
x
|
column names
|
Record coordinates (projected units). |
required |
y
|
column names
|
Record coordinates (projected units). |
required |
label_column
|
column name(s)
|
Label (species) column(s). |
required |
size
|
float
|
Distance from a hexagon's centre to its vertices. |
required |
orientation
|
('pointy', 'flat')
|
|
"pointy"
|
origin
|
(float, float)
|
A hexagon centre. |
(0.0, 0.0)
|
count_column
|
column name
|
Sample counts (default 1 per record). |
None
|
crs
|
optional
|
Coordinate reference system of the coordinates. |
None
|
Examples:
from_h3
classmethod
¶
from_h3(
data: DataFrame,
lat: Hashable,
lon: Hashable,
label_column: Hashable | Sequence[Hashable],
resolution: int,
*,
count_column: Hashable | None = None,
) -> BaseData
Records aggregated into H3 cells (needs the h3 package).
Groups are the cells' centres ("lon:lat", EPSG:4326); the cell
ids are in grid["cells"] and the cells are drawn as hexagons by
to_geodataframe().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
One row per record. |
required |
lat
|
column names
|
Record coordinates in degrees. |
required |
lon
|
column names
|
Record coordinates in degrees. |
required |
label_column
|
column name(s)
|
|
required |
resolution
|
int
|
H3 resolution (0-15). |
required |
count_column
|
column name
|
|
None
|
Examples:
save
¶
Save to landschaft's native format (a zip of Parquet tables; needs pyarrow).
Examples:
>>> import tempfile, pathlib
>>> import pandas as pd
>>> import landschaft as ls
>>> df = pd.DataFrame({"sp": ["a", "b"], "x": [0.5, 1.5], "y": [0.5, 0.5]})
>>> bd = ls.bio.BaseData.from_dataframe(df, ["x", "y"], "sp", cell_sizes=[1, 1])
>>> path = pathlib.Path(tempfile.mkdtemp()) / "sites.lsbio"
>>> bd.save(path)
>>> ls.bio.BaseData.load(path).groups
['0.5:0.5', '1.5:0.5']
from_table
classmethod
¶
from_table(
source: str | PathLike | DataFrame,
group_columns: Sequence[int | str],
label_columns: Sequence[int | str] | None = None,
sample_count_columns: Sequence[int | str] | None = None,
cell_sizes: Sequence[float] | None = None,
cell_origins: Sequence[float] | None = None,
*,
sep: str | None = None,
quote: str | None = None,
matrix_form: bool = False,
label_start_col: int | str | None = None,
label_end_col: int | str | None = None,
include_columns: Sequence[int | str] | None = None,
exclude_columns: Sequence[int | str] | None = None,
binarise_counts: bool = False,
allow_empty_groups: bool = False,
allow_empty_labels: bool = False,
skip_lines_with_undef_groups: bool = True,
crs: Any = None,
) -> BaseData
Delimited text (or a DataFrame) read like Biodiverse's import_data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
path or DataFrame
|
CSV-like file (separator and quote guessed unless given) or a DataFrame (all values are read as text, like the file). |
required |
group_columns
|
sequence
|
Column positions (0-based, as in Biodiverse) or names. Several
label columns are joined with |
required |
label_columns
|
sequence
|
Column positions (0-based, as in Biodiverse) or names. Several
label columns are joined with |
required |
sample_count_columns
|
sequence
|
Column positions (0-based, as in Biodiverse) or names. Several
label columns are joined with |
required |
cell_sizes
|
sequence of float
|
One per group column: > 0 snaps to cells of that size, 0 keeps the values, < 0 makes a text axis. Default 0 for every axis. |
None
|
cell_origins
|
sequence of float
|
Grid origin per axis (default 0). |
None
|
matrix_form
|
bool
|
Site × species layout: the columns |
False
|
include_columns
|
sequence
|
Keep only records where any include column is true / drop records where any exclude column is true (Perl truth: not empty, not 0). |
None
|
exclude_columns
|
sequence
|
Keep only records where any include column is true / drop records where any exclude column is true (Perl truth: not empty, not 0). |
None
|
binarise_counts
|
bool
|
Counts become 1 (after summing per group and label). |
False
|
allow_empty_groups
|
bool
|
Keep groups/labels that only appear with zero counts. |
False
|
allow_empty_labels
|
bool
|
Keep groups/labels that only appear with zero counts. |
False
|
skip_lines_with_undef_groups
|
bool
|
Skip records with an empty or |
True
|
crs
|
optional
|
CRS of the coordinates. |
None
|
Returns:
| Type | Description |
|---|---|
BaseData
|
|
Examples:
>>> import pandas as pd
>>> import landschaft as ls
>>> df = pd.DataFrame({"sp": ["a", "b", "a"], "x": [10, 15, 150], "y": [5, 5, 5]})
>>> bd = ls.bio.BaseData.from_table(df, group_columns=["x", "y"], label_columns=["sp"],
... cell_sizes=[100, 100])
>>> bd.groups
['150:50', '50:50']
>>> bd.labels_in_group("50:50")
{'a': 1.0, 'b': 1.0}
from_dataframe
classmethod
¶
from_dataframe(
df: DataFrame,
group_columns: Sequence[Hashable],
label_column: Hashable | Sequence[Hashable] | None = None,
count_column: Hashable | None = None,
cell_sizes: Sequence[float] | None = None,
cell_origins: Sequence[float] | None = None,
*,
label_columns: Sequence[Hashable] | None = None,
binarise_counts: bool = False,
crs: Any = None,
) -> BaseData
Long table (one row per record) or wide table (labels as columns).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
Numeric coordinates in |
required |
group_columns
|
sequence
|
Coordinate columns (one axis each). |
required |
label_column
|
column name or list of names
|
Long format: label of each record (several columns are joined with
|
None
|
count_column
|
column name
|
Long format: sample count of each record (default 1). |
None
|
label_columns
|
sequence
|
Wide format: these columns are labels and their values the counts (zeros and missing values are skipped). |
None
|
cell_sizes
|
Sequence[float] | None
|
As in |
None
|
cell_origins
|
Sequence[float] | None
|
As in |
None
|
binarise_counts
|
Sequence[float] | None
|
As in |
None
|
crs
|
Sequence[float] | None
|
As in |
None
|
Examples:
>>> import pandas as pd
>>> import landschaft as ls
>>> wide = pd.DataFrame({"x": [0.5, 1.5], "y": [0.5, 0.5], "sp1": [2, 0], "sp2": [1, 3]})
>>> bd = ls.bio.BaseData.from_dataframe(wide, ["x", "y"], label_columns=["sp1", "sp2"],
... cell_sizes=[1, 1])
>>> bd.to_dataframe()
sp1 sp2
group
0.5:0.5 2.0 1.0
1.5:0.5 0.0 3.0
from_sparse
classmethod
¶
from_sparse(
counts: spmatrix | ndarray,
coords: ndarray,
labels: Sequence[str],
cell_sizes: Sequence[float] | None = None,
cell_origins: Sequence[float] | None = None,
*,
crs: Any = None,
) -> BaseData
Community matrix (sites × species) with site coordinates.
Sites falling in the same cell are merged (counts summed).
Examples:
from_xarray
classmethod
¶
from_xarray(
data,
label_dim: str | None = None,
cell_sizes: Sequence[float] | None = None,
cell_origins: Sequence[float] | None = None,
*,
threshold: float | None = None,
x: str = "x",
y: str = "y",
block_rows: int = 1024,
) -> BaseData
Stack of per-label grids (a DataArray with a label dimension, or a Dataset).
Each slice (or data variable) is a label and its cell values the counts
(Biodiverse's "labels as bands": e.g. a stack of species distribution
model outputs); with threshold values >= it count 1 (presence),
smaller values 0. NaN and nodata are skipped. Cells are aggregated to
cell_sizes (default: the grid resolution). The stack is read
block_rows rows at a time, so lazy (dask, NetCDF, Zarr) arrays
larger than memory work.
Examples:
>>> import numpy as np, xarray as xr
>>> import landschaft as ls
>>> da = xr.DataArray(np.array([[[0.9, 0.1]], [[0.2, 0.8]]]), dims=("species", "y", "x"),
... coords={"species": ["a", "b"], "y": [0.5], "x": [0.5, 1.5]})
>>> bd = ls.bio.BaseData.from_xarray(da, "species", threshold=0.5)
>>> bd.to_dataframe()
a b
group
0.5:0.5 1.0 0.0
1.5:0.5 0.0 1.0
from_raster
classmethod
¶
from_raster(
files: str | PathLike | Sequence[str | PathLike],
labels_as_bands: bool = True,
cell_sizes: Sequence[float] | None = None,
cell_origins: Sequence[float] | None = None,
*,
given_label: str | None = None,
strip_file_extensions: bool = True,
threshold: float | None = None,
block_rows: int = 1024,
) -> BaseData
Raster files read like Biodiverse's import_data_raster.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
files
|
path or list of paths
|
Any GDAL raster. |
required |
labels_as_bands
|
bool
|
True: each band is a label (its description, else the file name
for single-band files, else |
True
|
cell_sizes
|
optional
|
Group cells (default: the raster resolution, origin 0). |
None
|
cell_origins
|
optional
|
Group cells (default: the raster resolution, origin 0). |
None
|
given_label
|
str
|
Label for every band. |
None
|
strip_file_extensions
|
bool
|
Drop the extension of file names used as labels. |
True
|
threshold
|
float
|
With |
None
|
block_rows
|
int
|
Rows read at a time (Cloud-Optimised GeoTIFFs and other large rasters are never read whole). |
1024
|
from_vector
classmethod
¶
from_vector(
source,
label_columns: Hashable | Sequence[Hashable],
cell_sizes: Sequence[float],
cell_origins: Sequence[float] | None = None,
*,
count_columns: Sequence[Hashable] | None = None,
measure: str | None = None,
layer: str | None = None,
to_crs: Any = None,
binarise_counts: bool = False,
) -> BaseData
Vector features (points, lines, polygons) read like Biodiverse's feature import.
Points fall in the cell containing them. Lines and polygons are laid
over a grid of cells spanning the layer: by default every cell they
touch counts 1; measure="area" (polygons) or measure="length"
(lines) counts the area or length of the part inside each cell instead.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
GeoDataFrame or path
|
Read with geopandas ( |
required |
label_columns
|
column name or list
|
Label of each feature (several columns joined with |
required |
cell_sizes
|
sequence of 2 floats
|
Group cells in the units of the (possibly reprojected) CRS; 0 keeps point coordinates as they are. |
required |
cell_origins
|
sequence of 2 floats
|
Grid origin (default 0). |
None
|
count_columns
|
list of column names
|
Sample count of each feature (summed); default 1 per point/cell, or the measured area/length. |
None
|
measure
|
(None, 'area', 'length')
|
See above. |
None
|
layer
|
str
|
Layer of a multi-layer file. |
None
|
to_crs
|
optional
|
Reproject first. |
None
|
binarise_counts
|
bool
|
Counts become 1. |
False
|
Examples:
>>> import geopandas as gpd
>>> from shapely.geometry import box
>>> import landschaft as ls
>>> gdf = gpd.GeoDataFrame({"sp": ["a"]}, geometry=[box(0, 0, 15, 5)])
>>> ls.bio.BaseData.from_vector(gdf, "sp", cell_sizes=[10, 10]).groups
['15:5', '5:5']
>>> ls.bio.BaseData.from_vector(gdf, "sp", [10, 10], measure="area").labels_in_group("15:5")
{'a': 25.0}
from_points
classmethod
¶
from_points(
source,
label_columns: Hashable | Sequence[Hashable],
cell_sizes: Sequence[float],
cell_origins: Sequence[float] | None = None,
*,
count_column: Hashable | None = None,
layer: str | None = None,
to_crs: Any = None,
) -> BaseData
Point records (GeoDataFrame, GeoParquet, GeoPackage, shapefile...).
from_vector() for points; count_column gives the sample count
of each point (default 1).
from_occurrences
classmethod
¶
from_occurrences(
source,
cell_sizes: Sequence[float],
cell_origins: Sequence[float] | None = None,
*,
to_crs: Any = None,
label: str | Sequence[str] = "species",
count: str | None = None,
max_uncertainty: float | None = None,
basis_of_record: Iterable[str] | None = None,
exclude_issues: Iterable[str] | None = None,
present_only: bool = True,
drop_duplicates: bool = True,
chunksize: int = 500000,
) -> BaseData
Occurrence records of a GBIF download or Darwin Core Archive, in cells.
The records are cleaned by read_occurrences() (same arguments),
optionally reprojected from WGS 84 to to_crs, and snapped to cells
of cell_sizes (degrees without to_crs) as the text import does.
Examples:
>>> import pandas as pd
>>> import landschaft as ls
>>> occ = pd.DataFrame({"species": ["A a", "B b", "A a"],
... "decimalLongitude": [10.1, 10.4, 12.2],
... "decimalLatitude": [-5.2, -5.9, -5.1]})
>>> bd = ls.bio.BaseData.from_occurrences(occ, cell_sizes=[1, 1])
>>> bd.to_dataframe()
A a B b
group
10.5:-5.5 1.0 1.0
12.5:-5.5 1.0 0.0
>>> ls.bio.BaseData.from_occurrences(occ, [100_000, 100_000], to_crs="EPSG:3857").groups
['1150000:-550000', '1150000:-650000', '1350000:-550000']
snap_coordinates
¶
Cell centres as Biodiverse's text import computes them (before naming).
size * floor(round((x - origin) / size * 1e10) / 1e10) + size / 2 + origin.
example_data
¶
Path of Biodiverse's example data (GPL-3, Laffan et al.).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
('sites', 'tree')
|
|
"sites"
|
Returns:
| Type | Description |
|---|---|
Path
|
|
Examples:
>>> import landschaft as ls
>>> bd = ls.bio.BaseData.from_table(ls.bio.example_data("sites"), group_columns=["x", "y"],
... label_columns=["genus", "species"], cell_sizes=[100000, 100000])
>>> bd
BaseData(127 groups, 31 labels, cell_sizes=[100000.0, 100000.0], 365 records)
>>> len(ls.bio.read_tree(ls.bio.example_data("tree")).terminal_names)
31
Occurrence records¶
GBIF downloads (the "simple CSV" zip) and Darwin Core Archives are read in
chunks and cleaned (absences, missing coordinates, uncertainty, basis of
record, GBIF issues, duplicates) before they become sample counts per cell
with BaseData.from_occurrences.
read_occurrences
¶
read_occurrences(
source: str | PathLike | DataFrame,
*,
label: str | Sequence[str] = "species",
count: str | None = None,
max_uncertainty: float | None = None,
basis_of_record: Iterable[str] | None = None,
exclude_issues: Iterable[str] | None = None,
present_only: bool = True,
drop_duplicates: bool = True,
extra_columns: Sequence[str] = (),
chunksize: int = 500000,
) -> DataFrame
Occurrence records of a GBIF download or a Darwin Core Archive, cleaned.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
path or DataFrame
|
A GBIF "simple CSV" download (the zip or the tab-separated file), a
Darwin Core Archive (zip or folder with |
required |
label
|
str or list of str
|
Column(s) naming the taxon: |
'species'
|
count
|
str
|
Column with the number of individuals ( |
None
|
max_uncertainty
|
float
|
Drop records with |
None
|
basis_of_record
|
list of str
|
Keep only these |
None
|
exclude_issues
|
list of str
|
Drop records flagged with any of these GBIF issues (column |
None
|
present_only
|
bool
|
Drop records whose |
True
|
drop_duplicates
|
bool
|
Keep one record per label and coordinate pair. |
True
|
extra_columns
|
list of str
|
Other columns to keep. |
()
|
chunksize
|
int
|
Rows read at a time (large downloads never sit whole in memory). |
500000
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
|
Examples:
>>> import pandas as pd
>>> import landschaft as ls
>>> df = pd.DataFrame({"species": ["A a", "A a", "B b", None],
... "decimalLongitude": ["10.1", "10.1", "11.5", "12"],
... "decimalLatitude": ["-5.2", "-5.2", "-5.9", "-6"],
... "basisOfRecord": ["HUMAN_OBSERVATION"] * 4})
>>> ls.bio.read_occurrences(df)
label decimalLongitude decimalLatitude count
0 A a 10.1 -5.2 1.0
2 B b 11.5 -5.9 1.0
Neighbourhoods¶
Spatial conditions are written as in Biodiverse, in its Perl syntax:
sp_circle (radius => 200000), sp_block (size => 500000),
$D <= 3 && $nbr_y > $y, sp_match_text (text => 'wet', axis => 2)... They
select the neighbours of each processing group (up to two sets, the second
leaving out the first), optionally only for the groups passing a definition
query. The neighbour lists are the ones Biodiverse uses, including its reuse
of lists between groups for non-overlapping conditions.
neighbours
¶
neighbours(
bd,
conditions: str | SpatialCondition | Sequence[str | SpatialCondition],
definition_query: str | SpatialCondition | None = None,
n_threads: int | None = None,
*,
recycle: bool = True,
results_recyclable: bool | None = None,
) -> Neighbourhoods
Neighbour sets of every group, as Biodiverse's spatial analysis finds them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bd
|
BaseData
|
|
required |
conditions
|
condition or list of up to two conditions
|
The second neighbour set leaves out the groups of the first. |
required |
definition_query
|
condition
|
Only groups passing it are processed (all groups can be neighbours). |
None
|
n_threads
|
int
|
Threads (default: all cores but one). |
None
|
recycle
|
bool
|
Reuse neighbour lists as Biodiverse does for non-overlapping, text match and "always the same" conditions (its default; it changes the result when such a condition is combined with a location-dependent term). False evaluates every group. |
True
|
results_recyclable
|
bool
|
Whether the analysis copies results between groups sharing their neighbours (the default: when every set is recyclable), which decides which groups trigger the recycling. |
None
|
Returns:
| Type | Description |
|---|---|
Neighbourhoods
|
|
Examples:
>>> import pandas as pd
>>> import landschaft as ls
>>> df = pd.DataFrame({"sp": ["a"] * 4, "x": [0.5, 1.5, 2.5, 0.5], "y": [0.5, 0.5, 0.5, 1.5]})
>>> bd = ls.bio.BaseData.from_dataframe(df, ["x", "y"], "sp", cell_sizes=[1, 1])
>>> nb = ls.bio.neighbours(bd, ["sp_circle (radius => 1)", "sp_select_all()"])
>>> nb.of("0.5:0.5"), nb.of("0.5:0.5", 1)
(['0.5:0.5', '0.5:1.5', '1.5:0.5'], ['2.5:0.5'])
definition_query
¶
definition_query(
bd, query: str | SpatialCondition, n_threads: int | None = None
) -> ndarray
Boolean mask of the groups passing a definition query.
Examples:
SpatialCondition
¶
A Biodiverse spatial condition (Perl syntax) compiled for a BaseData.
The supported Perl: the variables $D $C $Dsqr $Csqr $d[i] $D[i] $c[i]
$C[i] $x $y $z $nbr_x $nbr_y $nbr_z $coord[i] $nbrcoord[i] $coord_id1
$coord_id2, my variables, arithmetic, comparisons, eq/ne,
&& || // ! and or not, ?:, abs sqrt sin cos atan2 log exp int
and the functions sp_circle sp_circle_cell sp_rectangle sp_annulus
sp_square sp_square_cell sp_ellipse sp_block sp_self_only sp_select_all
sp_select_element sp_select_sequence sp_is_left_of sp_is_right_of
sp_in_line_with sp_match_text sp_match_regex sp_group_not_empty
sp_richness_greater_than sp_redundancy_greater_than sp_point_in_poly
sp_point_in_poly_shape sp_points_in_same_poly_shape.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
The condition, e.g. |
required |
Examples:
>>> import landschaft as ls
>>> c = ls.bio.SpatialCondition("sp_circle (radius => 1.5)")
>>> c.max_distance
1.5
Neighbourhoods
¶
Neighbour sets of the processing groups.
Attributes:
| Name | Type | Description |
|---|---|---|
groups |
ndarray
|
Indices of the processing groups (those passing the definition query), in name order. |
sets |
list of (indptr, indices)
|
One CSR per condition: the neighbours of |
of
¶
Neighbour names of a processing group (stored: as Biodiverse keeps them).
Spatial analysis¶
Indices computed over the neighbourhood of every group, as in Biodiverse's
spatial analysis: richness, endemism (weighted, corrected, absolute,
hierarchical), rarity, taxonomic turnover (Sorenson, Jaccard, Bray-Curtis,
Kulczynski, S2, beta diversity, nestedness, range-weighted turnover),
Simpson and Shannon diversity, Rao's quadratic entropy, richness estimators
(Chao1, Chao2, ACE, ICE, Hurlbert's rarefaction) and label and sample count
summaries. With a tree (tree=): phylogenetic diversity (PD, local PD,
terminal lists), phylogenetic endemism (PE, central, single, lists, PD
endemism, corrected weighted endemism), clade contributions and losses,
evolutionary distinctiveness (ES, ED, AED, AED-T, corrected weighted rarity),
phylogenetic abundance, phylogenetic turnover (Sorenson, Jaccard, S2,
range-weighted), relative PD and PE (RPD, RPE, against trees with equalised
branch lengths), RWiBaLD and the PhyloCom indices (MPD, MNTD, NRI, NTI, net
VPD, with the exact expected values of Tsirogiannis et al. or, where
Biodiverse samples random sets of tips, its Mersenne Twister draws: pass
prng_seed). With label or group properties: their values, frequencies,
summary statistics, quantiles and Getis-Ord Gi* (also weighted by local
ranges); with numeric labels: their statistics, quantiles, harmonic and
geometric means, dissimilarity between neighbour sets, Gi* and the
inter-event interval statistics of BiodiverseX (iei_stats, iei_data: the
intervals between the numeric labels of each group); with a label
matrix (matrix=): matrix statistics, Rao's quadratic entropy and the
comparison of two neighbour sets. Values are identical to Biodiverse's.
SpatialResult.to_biodiverse_csv writes the results as Biodiverse's
"Delimited text" export does, the same text byte for byte.
spatial_analysis
¶
spatial_analysis(
bd,
conditions: str | SpatialCondition | Sequence[str | SpatialCondition],
calculations: str | Sequence[str],
definition_query: str | SpatialCondition | None = None,
*,
tree=None,
matrix=None,
prng_seed: int | None = None,
n_threads: int | None = None,
recycle: bool = True,
) -> SpatialResult
Biodiverse's spatial analysis: indices over the neighbourhood of every group.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bd
|
BaseData
|
|
required |
conditions
|
condition or list of up to two conditions
|
Neighbourhoods (see |
required |
calculations
|
str or list of str
|
Calculation names, with or without Biodiverse's |
required |
definition_query
|
condition
|
Only groups passing it are processed. |
None
|
tree
|
Tree
|
Phylogeny for the phylogenetic indices ( |
None
|
matrix
|
Matrix
|
Label matrix for the matrix indices ( |
None
|
prng_seed
|
int
|
Seed of the Mersenne Twister Biodiverse draws random sets of tips
with, for the expected NTI on trees that are not ultrametric and for
the net VPD (Biodiverse's |
None
|
n_threads
|
int
|
Threads (default: all cores but one). |
None
|
recycle
|
bool
|
Reuse neighbour lists as Biodiverse does (see |
True
|
Returns:
| Type | Description |
|---|---|
SpatialResult
|
|
Examples:
>>> import pandas as pd
>>> import landschaft as ls
>>> df = pd.DataFrame({"sp": ["a", "b", "a", "c"], "x": [0.5, 0.5, 1.5, 2.5], "y": [0.5] * 4})
>>> bd = ls.bio.BaseData.from_dataframe(df, ["x", "y"], "sp", cell_sizes=[1, 1])
>>> res = ls.bio.spatial_analysis(bd, "sp_circle (radius => 1)", ["richness", "endemism_whole"])
>>> res.table[["RICHNESS_ALL", "ENDW_WE"]]
RICHNESS_ALL ENDW_WE
group
0.5:0.5 2.0 2.0
1.5:0.5 3.0 3.0
2.5:0.5 2.0 1.5
list_calculations
¶
The calculations available to spatial_analysis().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_neighbour_sets
|
int
|
Only those valid with this many neighbour sets (some compare two sets). |
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
One row per calculation: its indices (scalar and list) and the number of neighbour sets it needs. |
Examples:
SpatialResult
¶
SpatialResult(
bd, table: DataFrame, lists: dict[str, DataFrame], nbrs, calculations: list[str]
)
Bases: Saveable
Results of spatial_analysis().
Attributes:
| Name | Type | Description |
|---|---|---|
table |
DataFrame
|
Scalar indices, one row per processed group (Biodiverse's names:
|
lists |
dict of str to pandas.DataFrame
|
List indices ( |
neighbourhoods |
Neighbourhoods
|
The neighbour sets used. |
to_geodataframe
¶
Group cells with their scalar indices (needs geopandas).
to_biodiverse_csv
¶
to_biodiverse_csv(
path,
list_name: str = "SPATIAL_RESULTS",
sep: str = ",",
quote: str = '"',
no_data_value=None,
) -> None
Write a list as Biodiverse's "Delimited text" export does.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str or path - like
|
Output file. |
required |
list_name
|
str
|
|
'SPATIAL_RESULTS'
|
sep
|
str
|
Separator and quote character ( |
','
|
quote
|
str
|
Separator and quote character ( |
','
|
no_data_value
|
optional
|
Written where a group has no value (empty by default). |
None
|
Notes
The text is the same as Biodiverse writes: one row per group of the
BaseData (sorted), columns ELEMENT, Axis_0... and the keys in
natural order. Keys missing from a group get no_data_value; an
array list (IEI_DATA_ARRAY) gives the count of each value.
Examples:
>>> import tempfile, pathlib
>>> import pandas as pd
>>> import landschaft as ls
>>> df = pd.DataFrame({"sp": ["a", "b", "a"], "x": [0.5, 1.5, 1.5], "y": [0.5] * 3})
>>> bd = ls.bio.BaseData.from_dataframe(df, ["x", "y"], "sp", cell_sizes=[1, 1])
>>> res = ls.bio.spatial_analysis(bd, "sp_self_only()", ["richness"])
>>> path = pathlib.Path(tempfile.mkdtemp()) / "richness.csv"
>>> res.to_biodiverse_csv(path)
>>> print(path.read_text())
ELEMENT,Axis_0,Axis_1,RICHNESS_ALL,RICHNESS_SET1
0.5:0.5,0.5,0.5,1,1
1.5:0.5,1.5,0.5,2,2
Randomisations¶
The basedata is randomised and the analysis run again with the same
neighbourhoods; per group and index, the comparisons give p-ranks, z-scores
and, with PE and RPE, the CANAPE categories. The randomisations follow
Biodiverse's (its Mersenne Twister, seeded with seed), so the same seed
gives Biodiverse's results.
randomise
¶
randomise(
result: SpatialResult,
function: str = "rand_csr_by_group",
iterations: int = 99,
*,
seed: int | None = None,
name: str | None = None,
n_threads: int | None = None,
**args,
) -> Randomisation
Randomise the basedata of a spatial analysis and compare the results.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
result
|
SpatialResult
|
The observed analysis (its neighbourhoods are kept). |
required |
function
|
str
|
Randomisation: |
'rand_csr_by_group'
|
iterations
|
int
|
Iterations to run now (see |
99
|
seed
|
int
|
Seed of Biodiverse's Mersenne Twister (the same seed gives Biodiverse's randomisations). Random when not given. |
None
|
name
|
str
|
Name of the randomisation (default: the function). |
None
|
n_threads
|
int
|
Threads for the analyses (default: all cores but one). |
None
|
**args
|
Arguments of the randomisation function. |
{}
|
Returns:
| Type | Description |
|---|---|
Randomisation
|
|
Examples:
>>> import pandas as pd
>>> import landschaft as ls
>>> df = pd.DataFrame({"sp": ["a", "b", "a", "c", "c"], "x": [0.5, 0.5, 1.5, 2.5, 3.5], "y": [0.5] * 5})
>>> bd = ls.bio.BaseData.from_dataframe(df, ["x", "y"], "sp", cell_sizes=[1, 1])
>>> res = ls.bio.spatial_analysis(bd, "sp_self_only()", ["richness"])
>>> rnd = ls.bio.randomise(res, "rand_csr_by_group", iterations=9, seed=1)
>>> rnd.p_rank()["RICHNESS_ALL"].round(2).tolist()
[0.56, 0.67, 0.89, 0.89]
Randomisation
¶
Randomisation(result: SpatialResult, function: str, name: str, seed: int, args: dict)
Bases: Saveable
Randomisations of a SpatialResult (see randomise()).
Attributes:
| Name | Type | Description |
|---|---|---|
result |
SpatialResult
|
The observed analysis. |
function |
str
|
Randomisation function. |
name |
str
|
Prefix of Biodiverse's result lists ( |
iterations |
int
|
Iterations run so far. |
run
¶
run(iterations: int = 1, n_threads: int | None = None) -> Randomisation
Run more iterations (the random stream continues).
comparison
¶
The counters: C (observed greater), Q (comparisons), T (ties), P
(C / Q), SUMX and SUMXX (sums of the randomised values and their squares).
For the scalar indices (list_name None) the columns are
C_<index>..., one row per group; for a list index, a long table.
p_rank
¶
Rank of the observed values among the randomised ones: P when above 0.5,
else the proportion of randomisations not below (Biodiverse's p_rank lists).
z_scores
¶
Standardised difference of the observed values from the randomised ones.
canape
¶
CANAPE categories (Mishler et al. 2014) from the p-ranks of PE and RPE.
Needs pe and phylo_rpe2 in the analysis (pe_central and
phylo_rpe_central for central); diff uses the RPE
difference instead of the null PE. Codes: 0 not significant, 1 neo,
2 palaeo, 3 mixed, 4 super.
Cluster analyses¶
Groups joined into a dendrogram by the dissimilarity of their labels
(cluster) or grown into regions that maximise an index (region_grower),
as Biodiverse does, including its tie handling (seeded by prng_seed),
spatially constrained clustering and the indices of each node's groups.
ClusterResult.cut splits the dendrogram into a number of clusters.
cluster
¶
cluster(
bd,
index: str = "SORENSON",
linkage: str = "link_average",
conditions: str | Sequence[str] | None = None,
definition_query: str | None = None,
*,
calculations: Sequence[str] | None = None,
tree=None,
matrix=None,
prng_seed: int | None = None,
tie_breaker: Sequence[str] | None = None,
clear_singletons: bool = False,
n_threads: int | None = None,
_region_grower: bool = False,
) -> ClusterResult
Cluster the groups by the dissimilarity of their labels (Biodiverse's cluster analysis).
Pairs of groups (or clusters) with the lowest index value are joined until one tree remains; cluster-to-cluster values follow the linkage. With several spatial conditions, pairs within the first condition are joined first, then the second and so on.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bd
|
BaseData
|
|
required |
index
|
str
|
Dissimilarity index ( |
'SORENSON'
|
linkage
|
str
|
|
'link_average'
|
conditions
|
str or list of str
|
Spatial conditions limiting the pairs (default |
None
|
definition_query
|
str
|
Only groups passing it are clustered. |
None
|
calculations
|
list of str
|
Indices computed for the groups of each node. |
None
|
tree
|
optional
|
Phylogeny and label matrix for the index and the calculations. |
None
|
matrix
|
optional
|
Phylogeny and label matrix for the index and the calculations. |
None
|
prng_seed
|
int
|
Seed of Biodiverse's Mersenne Twister, which breaks ties between equal values (random when not given). |
None
|
tie_breaker
|
list of str
|
Biodiverse's |
None
|
clear_singletons
|
bool
|
Leave out groups that joined nothing when joining several roots. |
False
|
n_threads
|
int
|
Threads for the index values and calculations. |
None
|
Returns:
| Type | Description |
|---|---|
ClusterResult
|
|
Examples:
>>> import pandas as pd
>>> import landschaft as ls
>>> df = pd.DataFrame({"sp": ["a", "b", "a", "b", "c", "c", "d"],
... "x": [0.5, 0.5, 1.5, 1.5, 2.5, 3.5, 3.5], "y": [0.5] * 7})
>>> bd = ls.bio.BaseData.from_dataframe(df, ["x", "y"], "sp", cell_sizes=[1, 1])
>>> res = ls.bio.cluster(bd, prng_seed=1)
>>> res.tree.to_newick(lengths=False)
"(('0.5:0.5','1.5:0.5')0___,('2.5:0.5','3.5:0.5')1___)2___;"
region_grower
¶
region_grower(
bd,
index: str = "RICHNESS_ALL",
conditions: str | Sequence[str] | None = None,
definition_query: str | None = None,
*,
calculations: Sequence[str] | None = None,
tree=None,
matrix=None,
prng_seed: int | None = None,
tie_breaker: Sequence[str] | None = None,
n_threads: int | None = None,
) -> ClusterResult
Grow regions by joining the pairs with the highest value of an index (Biodiverse's RegionGrower).
Like cluster(), but pairs are joined in decreasing order of an
index computed for the pair's groups together (such as RICHNESS_ALL
or PE_WE), recomputed for each merged region. With one condition
that selects all groups, joining stops once a region reaches the value
of all the groups together.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bd
|
BaseData
|
|
required |
index
|
str
|
The index to maximise. |
'RICHNESS_ALL'
|
conditions
|
str | Sequence[str] | None
|
As for |
None
|
definition_query
|
str | Sequence[str] | None
|
As for |
None
|
calculations
|
str | Sequence[str] | None
|
As for |
None
|
tree
|
str | Sequence[str] | None
|
As for |
None
|
matrix
|
str | Sequence[str] | None
|
As for |
None
|
prng_seed
|
str | Sequence[str] | None
|
As for |
None
|
tie_breaker
|
str | Sequence[str] | None
|
As for |
None
|
n_threads
|
str | Sequence[str] | None
|
As for |
None
|
Returns:
| Type | Description |
|---|---|
ClusterResult
|
|
Examples:
>>> import pandas as pd
>>> import landschaft as ls
>>> df = pd.DataFrame({"sp": ["a", "b", "a", "c", "d"], "x": [0.5, 0.5, 1.5, 2.5, 3.5], "y": [0.5] * 5})
>>> bd = ls.bio.BaseData.from_dataframe(df, ["x", "y"], "sp", cell_sizes=[1, 1])
>>> res = ls.bio.region_grower(bd, prng_seed=1)
>>> res.nodes.loc[res.tree.names[res.tree.root], "TOTAL_LENGTH"]
np.float64(4.0)
ClusterResult
¶
Bases: Saveable
Results of cluster().
Attributes:
| Name | Type | Description |
|---|---|---|
tree |
Tree
|
The dendrogram (terminals are the groups; internal nodes are named
|
nodes |
DataFrame
|
Per node: |
table |
DataFrame
|
Scalar indices of each node's groups ( |
lists |
dict
|
List indices of each node's groups. |
cut
¶
Cut the dendrogram into k clusters (the highest joins are undone first).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
k
|
int
|
Number of clusters (at most the number of groups). |
required |
Returns:
| Type | Description |
|---|---|
Series
|
Cluster number (1 to |
Examples:
>>> import pandas as pd
>>> import landschaft as ls
>>> df = pd.DataFrame({"sp": ["a", "b", "a", "b", "c", "c", "d"],
... "x": [0.5, 0.5, 1.5, 1.5, 2.5, 3.5, 3.5], "y": [0.5] * 7})
>>> bd = ls.bio.BaseData.from_dataframe(df, ["x", "y"], "sp", cell_sizes=[1, 1])
>>> ls.bio.cluster(bd, prng_seed=1).cut(2).to_dict()
{'0.5:0.5': 1, '1.5:0.5': 1, '2.5:0.5': 2, '3.5:0.5': 2}
Phylogenies¶
Tree
¶
Tree(
names: Sequence[str],
parent: Sequence[int],
length: Sequence[float],
children: Sequence[Sequence[int]],
name: str = "",
bootstrap: Sequence[str | None] | None = None,
)
A rooted phylogenetic tree.
Nodes are numbered in the order the reader creates them. names,
parent (-1 for the root), length and children describe the
topology; node_order lists the nodes sorted by name, the order in which
landschaft sums over nodes (Biodiverse's own order with sorted keys).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
names
|
Sequence[str]
|
Node names, parent index per node (-1 for the root), branch lengths and child indices per node (in the order they were given). |
required |
parent
|
Sequence[str]
|
Node names, parent index per node (-1 for the root), branch lengths and child indices per node (in the order they were given). |
required |
length
|
Sequence[str]
|
Node names, parent index per node (-1 for the root), branch lengths and child indices per node (in the order they were given). |
required |
children
|
Sequence[str]
|
Node names, parent index per node (-1 for the root), branch lengths and child indices per node (in the order they were given). |
required |
name
|
str
|
Tree name. |
''
|
bootstrap
|
list
|
The text in square brackets after each node, if any. |
None
|
total_length
property
¶
Sum of all branch lengths (root included), summed in name order.
postorder
¶
Nodes with every child before its parent (children in their given order).
to_biopython
¶
to_dendropy
¶
to_ete
¶
read_tree
¶
read_tree(
source: str | PathLike, format: str | None = None, index: int | str = 0
) -> Tree
read_trees
¶
read_trees(source: str | PathLike, format: str | None = None) -> list[Tree]
Read every tree in a Newick, Phylip (several Newicks) or Nexus file.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
str or path
|
File path, or the text itself. |
required |
format
|
('nexus', 'newick')
|
Detected from the text when omitted ( |
"nexus"
|
Returns:
| Type | Description |
|---|---|
list of Tree
|
|
Examples:
Dissimilarity matrices¶
Matrix
¶
A symmetric matrix of values between elements (usually labels).
Missing pairs are NaN. A pair holds the first value read for it (either
order), like Biodiverse; the diagonal is self_similarity unless a value
was given.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
elements
|
sequence of str
|
Element names (stored sorted). |
required |
values
|
array(n, n)
|
Values in the order of |
required |
self_similarity
|
float
|
Value of (a, a) pairs that were not given. |
None
|
n_pairs
property
¶
Number of defined pairs (each unordered pair once, diagonal included).
get
¶
Value of the pair (NaN if missing; self_similarity for a missing (a, a)).
read_matrix
¶
read_matrix(
source: str | PathLike,
element_columns: Sequence[int] = (1, 2),
values_start_col: int | None = None,
*,
sparse: bool = False,
row_columns: Sequence[int] | None = None,
col_columns: Sequence[int] | None = None,
value_column: int | None = None,
sep: str | None = None,
quote: str | None = None,
self_similarity: float | None = None,
) -> Matrix
Read a Biodiverse matrix file (full or lower/upper triangle, or sparse pairs).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
path
|
Delimited text. |
required |
element_columns
|
sequence of int
|
Columns forming the element name of each row (joined with |
(1, 2)
|
values_start_col
|
int
|
First value column (default: after the last element column). The value columns follow the order of the rows. |
None
|
sparse
|
bool
|
Sparse format: one pair per line, names in |
False
|
Returns:
| Type | Description |
|---|---|
Matrix
|
|
Grids and files¶
Records can be aggregated into hexagons (BaseData.from_hex) or H3 cells
(BaseData.from_h3, needs h3); cells are drawn as hexagons by
BaseData.to_geodataframe. Basedatas are saved with BaseData.save and
whole studies (basedatas, trees, matrices and results) with save_session,
as zips of Parquet tables. Trees convert to and from Bio.Phylo, DendroPy and
ete (Tree.from_biopython, Tree.to_dendropy...).
hex_centres
¶
Centres of the hexagons (circumradius size) containing each point.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array - like
|
Point coordinates (projected units). |
required |
y
|
array - like
|
Point coordinates (projected units). |
required |
size
|
float
|
Distance from a hexagon's centre to its vertices. |
required |
orientation
|
('pointy', 'flat')
|
Pointy-topped hexagons (rows) or flat-topped (columns). |
"pointy"
|
origin
|
(float, float)
|
Centre of the hexagon at axial coordinates (0, 0). |
(0.0, 0.0)
|
Returns:
| Type | Description |
|---|---|
(ndarray, ndarray)
|
The centre coordinates. |
Examples:
h3_cells
¶
save_session
¶
Save basedatas, trees, matrices and analysis results in one file.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
path
|
The file to write (a zip of Parquet tables and JSON). |
required |
**objects
|
Named |
{}
|
Examples:
>>> import tempfile, pathlib
>>> import pandas as pd
>>> import landschaft as ls
>>> df = pd.DataFrame({"sp": ["a", "b", "a"], "x": [0.5, 1.5, 1.5], "y": [0.5] * 3})
>>> bd = ls.bio.BaseData.from_dataframe(df, ["x", "y"], "sp", cell_sizes=[1, 1])
>>> res = ls.bio.spatial_analysis(bd, "sp_self_only()", ["richness"])
>>> path = pathlib.Path(tempfile.mkdtemp()) / "study.lsbio"
>>> ls.bio.save_session(path, sites=bd, richness=res)
>>> s = ls.bio.load_session(path)
>>> s["richness"].table["RICHNESS_ALL"].tolist()
[1.0, 2.0]
load_session
¶
Load the objects saved with save_session() (a dict by name).
Examples:
See save_session().
Element properties¶
Label and group properties (Biodiverse's element properties files) are
assigned with BaseData.set_label_properties and
BaseData.set_group_properties.
read_properties
¶
read_properties(
source: str | PathLike,
element_columns: Sequence[int],
property_columns: Mapping[str, int],
*,
sep: str | None = None,
quote: str | None = None,
) -> DataFrame
Read a Biodiverse element properties file (label or group properties).
Property names are upper-cased and only values that look like numbers are kept, as Biodiverse does; empty fields are kept as empty strings (the property is set but undefined: listed, not used in statistics). A later row for the same element overrides the earlier one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
path
|
Delimited text with a header line. |
required |
element_columns
|
sequence of int
|
Columns forming the element name (joined with |
required |
property_columns
|
mapping
|
Property name to column. |
required |
sep
|
str
|
Separator and quote character (guessed by default). |
None
|
quote
|
str
|
Separator and quote character (guessed by default). |
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
One row per element, one column per property; values are the strings
read ( |
Examples:
>>> import tempfile, pathlib
>>> import landschaft as ls
>>> path = pathlib.Path(tempfile.mkdtemp()) / "props.csv"
>>> _ = path.write_text("genus,species,mass\nGenus,sp1,1.5\nGenus,sp2,x\nGenus,sp3,\n")
>>> props = ls.bio.read_properties(path, [0, 1], {"mass": 2})
>>> props["MASS"].to_dict()
{'Genus:sp1': '1.5', 'Genus:sp3': ''}