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))
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))
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))
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)