Example #1
0
    def test_compute_zonal_statistics_arr(self):
        za = pls.ZonalAnalysis(self.landscape_fp, self.masks_arr)

        # test that the array has the proper shape
        zs_arr = za.compute_zonal_statistics_arr('patch_density')
        zs_arr.shape = za.landscape_meta['height'], za.landscape_meta['width']

        # test that a zonal statistics array computed at the class level has
        # at least as many nan values than its landscape-level counterpart
        self.assertGreaterEqual(
            np.isnan(zs_arr).sum(),
            np.isnan(
                za.compute_zonal_statistics_arr(
                    'patch_density', class_val=za.present_classes[0])).sum())

        # test that the zonal statistics when excluding boundaries should be
        # less or equal than including them
        self.assertLessEqual(
            np.nansum(za.compute_zonal_statistics_arr('total_edge')),
            np.nansum(
                za.compute_zonal_statistics_arr(
                    'total_edge', metric_kws={'count_boundary': True})))

        # test that passing `dst_filepath` dumps a raster file
        dst_filepath = path.join(self.tmp_dir, 'foo.tif')
        za.compute_zonal_statistics_arr('patch_density',
                                        dst_filepath=dst_filepath)
        self.assertTrue(path.exists(dst_filepath))
Example #2
0
    def test_zonal_init(self):
        # test that the attribute names and values are consistent with the
        # provided `masks_arr`
        za = pls.ZonalAnalysis(self.landscape, self.masks_arr)
        self.assertEqual(za.attribute_name, 'attribute_values')
        self.assertEqual(len(za), len(self.masks_arr))
        self.assertEqual(len(za), len(za.attribute_values))

        # test that if we init a `ZonalAnalysis` from filepaths, Landscape
        # instances are automaticaly built, and the attribute names and values
        # are also consistent with the provided `masks_arr`
        za = pls.ZonalAnalysis(self.landscape_fp, self.masks_arr)
        for landscape in za.landscapes:
            self.assertIsInstance(landscape, pls.Landscape)
        self.assertEqual(za.attribute_name, 'attribute_values')
        self.assertEqual(len(za), len(self.masks_arr))
        self.assertEqual(len(za), len(za.attribute_values))
Example #3
0
    def test_zonal_plot_metrics(self):
        za = pls.ZonalAnalysis(self.landscape_fp, self.masks_arr)

        # test for `None` (landscape-level) and an existing class (class-level)
        for class_val in [None, za.present_classes[0]]:
            # test that the x data of the line corresponds to the attribute
            # values
            self.assertTrue(
                np.all(
                    za.plot_metric('patch_density', class_val=class_val).
                    lines[0].get_xdata() == za.attribute_values))
Example #4
0
    def test_zonal_init(self):
        # test that the attribute names and values are consistent with the
        # provided `masks_arr`
        za = pls.ZonalAnalysis(self.landscape, masks=self.masks_arr)
        self.assertEqual(za.attribute_name, 'attribute_values')
        self.assertEqual(len(za), len(self.masks_arr))
        self.assertEqual(len(za), len(za.attribute_values))

        # test that if we init a `ZonalAnalysis` from filepaths, Landscape
        # instances are automaticaly built, and the attribute names and values
        # are also consistent with the provided `masks_arr`
        za = pls.ZonalAnalysis(self.landscape_fp, masks=self.masks_arr)
        for landscape in za.landscapes:
            self.assertIsInstance(landscape, pls.Landscape)
        self.assertEqual(za.attribute_name, 'attribute_values')
        self.assertEqual(len(za), len(self.masks_arr))
        self.assertEqual(len(za), len(za.attribute_values))

        # test passing GeoSeries, GeoDataFrame and geopandas files as `masks`
        masks_gdf = gpd.read_file(self.masks_fp)
        masks_index_col = 'GMDNAME'

        # first test the GeoSeries, which works like the others except that we
        # cannot set a column as the zone index
        masks_gser = masks_gdf['geometry'].copy()
        za = pls.ZonalAnalysis(self.landscape_fp, masks=masks_gser)
        self.assertLessEqual(len(za), len(masks_gdf))
        za = pls.ZonalAnalysis(self.landscape_fp,
                               masks=masks_gser,
                               masks_index_col=masks_index_col)
        self.assertLessEqual(len(za), len(masks_gdf))
        # also test that attribute name is properly set when using geoseries
        # as `masks` note that if a non-None `attriubte_name` is provided, it
        # always takes precedence
        attribute_name = 'foo'
        # first test for a geoseries with name and unnamed index
        masks_gser.name = 'bar'
        za = pls.ZonalAnalysis(self.landscape_fp, masks=masks_gser)
        self.assertEqual(za.attribute_name, masks_gser.name)
        za = pls.ZonalAnalysis(self.landscape_fp,
                               masks=masks_gser,
                               attribute_name=attribute_name)
        self.assertEqual(za.attribute_name, attribute_name)
        # now test that for a geoseries with name and named index, the
        # geoseries name takes precedence
        masks_gser.index.name = 'name'
        za = pls.ZonalAnalysis(self.landscape_fp, masks=masks_gser)
        self.assertEqual(za.attribute_name, masks_gser.name)
        za = pls.ZonalAnalysis(self.landscape_fp,
                               masks=masks_gser,
                               attribute_name=attribute_name)
        self.assertEqual(za.attribute_name, attribute_name)
        # finally test that for an unnamed geoseries with named index, the
        # geoseries index name is taken
        masks_gser.name = None
        za = pls.ZonalAnalysis(self.landscape_fp, masks=masks_gser)
        self.assertEqual(za.attribute_name, masks_gser.index.name)
        za = pls.ZonalAnalysis(self.landscape_fp,
                               masks=masks_gser,
                               attribute_name=attribute_name)
        self.assertEqual(za.attribute_name, attribute_name)

        # now test the GeoDataFrame and geopandas file
        for masks in self.masks_fp, masks_gdf:
            # test init
            za = pls.ZonalAnalysis(self.landscape_fp, masks=masks)
            self.assertLessEqual(len(za), len(masks_gdf))
            self.assertTrue(
                np.all(np.isin(getattr(za, za.attribute_name),
                               masks_gdf.index)))
            # test that we can set a column as the zone index
            za = pls.ZonalAnalysis(self.landscape_fp,
                                   masks=masks,
                                   masks_index_col=masks_index_col)
            self.assertTrue(
                np.all(
                    np.isin(getattr(za, masks_index_col),
                            masks_gdf[masks_index_col])))
def _analyse_fragmentation(
    landcover: Union[os.PathLike, xr.DataArray],
    rois: Optional[gpd.GeoDataFrame] = None,
    target_crs: Optional[Union[str, pyproj.CRS]] = None,
    target_x_res: float = 300,
    target_y_res: float = 300,
    no_data: int = 0,
    rois_index_col: str = "name",
    **kwargs
) -> pd.DataFrame:
    """
    Compute pylandstats class fragmentation metrics for ROIs on a landcover map.

    For a list of all computable metrics, see:
    https://pylandstats.readthedocs.io/en/latest/landscape.html

    Args:
        landcover (Union[os.PathLike, xr.DataArray]): The landcover data to use.
        rois (Optional[gpd.GeoDataFrame], optional): A geopandas dataframe which
            contains the list of the geometries for which a class fragmentation analysis
            should be performed. Defaults to None.
        target_crs (Optional[Union[str, pyproj.CRS]], optional): The coordinate
            reference system to use for the class metric computation. For interpretable
            results a CRS with units of meter (e.g. UTM) should be used.
            Defaults to None.
        target_x_res (float, optional): The target pixel resolution along the
            x-direction in the target coordinate reference system. If the CRS has units
            of meter, this corresponds to meters per pixel. Up/downsampling is
            performed via nearest-neighbor sampling with rasterio. Defaults to 300.
        target_y_res (float, optional): The target pixel resolution along the
            y-direction in the target coordinate reference system. If the CRS has units
            of meter, this corresponds to meters per pixel. Up/downsampling is
            performed via nearest-neighbor sampling with rasterio. Defaults to 300.
        no_data (int, optional): The no-data value for the landcover data.
            Defaults to 0.
        rois_index_col (str, optional): Name of the attribute that will distinguish
            region of interest in `rois`. Defaults to "name".
        **kwargs: Keyword arguments of the `compute_class_metrics_df` of pylandstats

    Returns:
        pd.DataFrame: The pandas dataframe containing the computed metrics for each
            landcover class in `landcover` and region of interest given in `rois`
    """

    # 1 Load the data
    if isinstance(landcover, os.PathLike):
        data_original = rxr.open_rasterio(pathlib.Path(landcover))
    elif isinstance(landcover, xr.DataArray):
        data_original = copy(landcover)

    # 2 Reproject to relevant CRS and resolution
    data_reprojected = data_original.rio.reproject(
        target_crs, resolution=(target_x_res, target_y_res)
    )

    # 3 Calculate final resolution
    x_data, y_data = (data_reprojected.x.data, data_reprojected.y.data)
    x_res = abs(x_data[-1] - x_data[0]) / len(x_data)
    y_res = abs(y_data[-1] - y_data[0]) / len(y_data)
    # Free up memory
    del data_original

    # 4 Perform pylandstats analysis on clipped, reprojected region
    # Convert to pylandstats landscape
    data_landscape = pls.Landscape(
        data_reprojected.data.squeeze(),
        res=(x_res, y_res),
        nodata=no_data,
        transform=data_reprojected.rio.transform(),
    )

    # Perform zonal analysis of the rois
    if rois is None:
        zonal_analyser = data_landscape
    else:
        zonal_analyser = pls.ZonalAnalysis(
            data_landscape,
            landscape_crs=target_crs,
            masks=rois,
            masks_index_col=rois_index_col,
        )
    return zonal_analyser.compute_class_metrics_df(**kwargs)