Repository: cogeotiff/rio-tiler

Issue:
valid_percent in utils/get_array_statistics not correctly calculated, when using feature coverage array
If I calculate the coverage based on a geometry and hand it over to the get_array_statistics method, I would expect that the valid_percent is calculated based on the coverage array instead of the bounding box of the geometry.

![example_geometry](https://github.com/user-attachments/assets/cc3ab822-1bb5-4511-85bc-9c076df38591)

In this example valid_pixels is correctly calculated, but valid_percent is wrong: 
- 2 valid pixels, because one pixel is nodata. 
- As the geometry includes three pixels, I would expect that valid_percent **needs to be 66.67 %, instead it is 50 %.**  

Here is a full code running example:
```python
from rio_tiler.io import Reader

def calculate_statistics(filename: str, shape: dict) -> dict:
    with Reader(filename) as src:
        data = src.feature(shape)

        coverage_array = data.get_coverage_array(shape)

        return data.statistics(coverage=coverage_array)

if __name__ == "__main__":
    file = "https://rastless-tests.s3.eu-central-1.amazonaws.com/TUR_us-newyork_013032_EOMAP_20190424_153304_LSAT8_m0030_32bit.tif"

    aoi = {"type": "Polygon", "coordinates": [
        [[-74.046461218997848, 40.63946290226923], [-74.046464353686744, 40.638997869937697],
         [-74.045818607775814, 40.638991923211712], [-74.045817040431388, 40.639200058305974],
         [-74.046160288864613, 40.639204815671974], [-74.046161856209054, 40.63946290226923],
         [-74.046461218997848, 40.63946290226923]]]}


    statistics = calculate_statistics(file, aoi)

    valid_pixels = statistics["b1"]["valid_pixels"]
    valid_percent = statistics["b1"]["valid_percent"]

    print(f"Valid pixels: {valid_pixels}. Expected value: 2.0")
    print(f"Valid percent: {valid_percent} %. Expected value: 66.67 %")
```

From what I see in the code this line seems to be wrong, as it always calculates the valid_percent based on the full array size:
utils.py/get_array_statistics line 136
```python
valid_percent = round((valid_pixels / data[b].size) * 100, 2)
```

My suggestion to change it to:
```python
valid_percent = round((valid_pixels / np.count_nonzero(coverage)) * 100, 2)
```

Even if no coverage is handed over by the user the calculation is correct, because then it is created inside the method. 


I would be happy to create a PR if you agree.

Discussion:
@vincentsarago: thanks @MarcelCode I think the change proposed makes sense 🙏 

happy to review a PR 

Solution patch:
diff --git a/rio_tiler/utils.py b/rio_tiler/utils.py
index 9984353..581dd57 100644
--- a/rio_tiler/utils.py
+++ b/rio_tiler/utils.py
@@ -133,6 +133,8 @@ def get_array_statistics(
     else:
         coverage = numpy.ones((data.shape[1], data.shape[2]))
 
+    coverage_pixels = numpy.count_nonzero(coverage)
+
     # Avoid non masked nan/inf values
     numpy.ma.fix_invalid(data, copy=False)
 
@@ -143,7 +145,7 @@ def get_array_statistics(
 
         valid_pixels = float(numpy.ma.count(data[b]))
         masked_pixels = float(numpy.ma.count_masked(data[b]))
-        valid_percent = round((valid_pixels / data[b].size) * 100, 2)
+        valid_percent = round(min(valid_pixels / coverage_pixels, 1) * 100, 2)
 
         if categorical:
             out_dict = dict(zip(keys.tolist(), counts.tolist()))


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
index abe6553..bead8ef 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -645,6 +645,7 @@ def test_get_array_statistics_coverage():
     assert stats[0]["count"] == 1.75
     assert stats[0]["median"] == 3  # 2 in exactextract
     assert round(stats[0]["std"], 2) == 1.05
+    assert stats[0]["valid_percent"] == 100
 
     stats = utils.get_array_statistics(data)
     assert len(stats) == 1
@@ -652,6 +653,7 @@ def test_get_array_statistics_coverage():
     assert stats[0]["max"] == 4
     assert stats[0]["mean"] == 2.5
     assert stats[0]["count"] == 4
+    assert stats[0]["valid_percent"] == 100
 
     # same test as https://github.com/isciences/exactextract/blob/0883cd585d9c7b6b4e936aeca4aa84a15adf82d2/python/tests/test_exact_extract.py#L48-L110
     data = np.ma.arange(1, 10, dtype=np.int32).reshape(3, 3)
@@ -670,6 +672,14 @@ def test_get_array_statistics_coverage():
     assert stats[0]["percentile_75"] == 6
     assert stats[0]["std"] == math.sqrt(5)
 
+    # test correct calculation of valid percent with masked array and coverage array
+    data = np.ma.array(
+        [[[0, 1], [0, 5]]], mask=[[[True, False], [True, False]]], fill_value=0
+    )
+    coverage = np.array([[0, 0.5], [0.75, 1]])
+    stats = utils.get_array_statistics(data, coverage=coverage)
+    assert stats[0]["valid_percent"] == 66.67
+
 
 def test_get_vrt_transform_world_file(dataset_fixture):
     """Should return correct transform and size."""