Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite report aggregation of indicator to use mean value of indicator result class values #372

Merged
merged 28 commits into from
Oct 11, 2022
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f81fe31
fix typing in report base class
matthiasschaub Jul 6, 2022
2973803
make indicator/layer combinations an attribute
matthiasschaub Jul 6, 2022
532383a
rewrite report aggregation of indicator
matthiasschaub Jul 6, 2022
2b5ddc3
remove empty lines
matthiasschaub Jul 6, 2022
dbd3676
fix typing in report base class
matthiasschaub Jul 6, 2022
05f32cd
make indicator/layer combinations an attribute
matthiasschaub Jul 6, 2022
687e072
rewrite report aggregation of indicator
matthiasschaub Jul 6, 2022
bb2c654
remove empty lines
matthiasschaub Jul 6, 2022
a562953
Merge branch 'report-aggregation' of https://github.com/GIScience/ohs…
Gigaszi Aug 9, 2022
0b0b55c
Merge branch 'main' into report-aggregation
Gigaszi Sep 7, 2022
869d9a4
fix: add initialization of blocking_red and blocking_undefined
Gigaszi Sep 7, 2022
d39232e
fix: report init call
Gigaszi Sep 9, 2022
74f5768
fix: label in result
Gigaszi Sep 11, 2022
be7d8a3
fix: class_ was float not int
Gigaszi Sep 11, 2022
e804983
add: Changelog entry
Gigaszi Sep 11, 2022
92640d0
Merge branch 'main' into report-aggregation
Gigaszi Sep 11, 2022
73f9d9d
fix: Update workers/ohsome_quality_analyst/base/report.py
Gigaszi Sep 20, 2022
5f3e96d
fix: Update workers/ohsome_quality_analyst/base/report.py
Gigaszi Sep 20, 2022
7a4124a
Merge branch 'main' into report-aggregation
Gigaszi Sep 20, 2022
97340f8
fix: add new report creation to multilevelCurrentness
Gigaszi Sep 20, 2022
650394b
fix: test for multilevel currentness report
Gigaszi Sep 20, 2022
9fb4e25
Merge branch 'main' into report-aggregation
Gigaszi Sep 23, 2022
8db7ca2
chore: styling rules
Gigaszi Sep 26, 2022
709829e
chore: update CHANGELOG
matthiasschaub Sep 27, 2022
3db2c46
test: add test for blocking_red and blocking_undefined
Gigaszi Sep 27, 2022
a0183d5
chore: Merge branch 'main' into report-aggregation
Gigaszi Sep 27, 2022
be3b1a1
fix: call __init__ instead of set_indicator_layer for report creation
Gigaszi Sep 27, 2022
f0b44bd
Merge branch 'main' into report-aggregation
matthiasschaub Oct 11, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## Current Main

### Bug Fixes

- Reports take result class of indicators into account ([#372] [#369])

### New Features

- Substitute result values of reports by introducing a result class value ([#372])

[#372]: https://github.com/GIScience/ohsome-quality-analyst/pull/372

## 0.12.0

Expand Down
77 changes: 52 additions & 25 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ bpolys = {
],
}
parameters = {
"name": "MinimalTestReport",
"name": "Minimal",
"bpolys": bpolys,
"includeSvg": False, # Optional
"includeHtml": False, # Optional
Expand All @@ -282,7 +282,7 @@ response = requests.post(url, json=parameters)

```json
{
"apiVersion": "0.10.0",
"apiVersion": "0.11.0",
"attribution": {
"url": "https://github.com/GIScience/ohsome-quality-analyst/blob/main/data/COPYRIGHTS.md",
"text": "© OpenStreetMap contributors"
Expand All @@ -293,29 +293,56 @@ response = requests.post(url, json=parameters)
"coordinates": [ ... ]
},
"properties": {
"report.metadata.name": "Minimal",
"report.metadata.description": "This report shows the quality for two indicators:\nMapping Saturation and Currentness.\nIt's main function is to test the interactions between\ndatabase, api and website.\n",
"report.result.label": "green",
"report.result.value": 0.8902104187552007,
"report.result.description": "All indicators show a good quality.\nThe data in this regions seems to be completely mapped.\n",
"indicators.0.metadata.name": "Mapping Saturation",
"indicators.0.metadata.description": "Mapping Saturation",
"indicators.0.layer.name": "Building Count",
"indicators.0.layer.description": "All buildings as defined by all objects tagged with 'building=*'.\n",
"indicators.0.result.timestamp_oqt": "2022-07-10T15:18:05.208875+00:00",
"indicators.0.result.timestamp_osm": "2022-03-01T00:00:00+00:00",
"indicators.0.result.label": "green",
"indicators.0.result.value": 0.9804208375104013,
"indicators.0.result.description": "The saturation of the last 3 years is 98.04%.\nHigh saturation has been reached (97% < Saturation ≤ 100%).\n",
"indicators.1.metadata.name": "Currentness",
"indicators.1.metadata.description": "Currentness",
"indicators.1.layer.name": "Building Count",
"indicators.1.layer.description": "All buildings as defined by all objects tagged with 'building=*'.\n",
"indicators.1.result.timestamp_oqt": "2022-07-10T15:33:36.449060+00:00",
"indicators.1.result.timestamp_osm": "2022-07-03T20:00:00+00:00",
"indicators.1.result.label": "green",
"indicators.1.result.value": 0.8,
"indicators.1.result.description": "Over 50% of the 40811.0 features (Building Count) were edited in the last 4 years.\nThis is a rather high value and indicates that the map features \nare very unlike to be outdated. This refers to good data quality in \nrespect to currentness.\n"
"report": {
"metadata": {
"name": "Minimal",
"description": "This report shows the quality for two indicators:\nMapping Saturation and Currentness.\nIt's main function is to test the interactions between\ndatabase, api and website.\n"
},
"result": {
"class_": 5,
"description": "All indicators show a good quality.\nThe data in this regions seems to be completely mapped.\n"
}
},
"indicators": [
{
"metadata": {
"name": "Mapping Saturation",
"description": "Calculate if mapping has saturated.\nHigh saturation has been reached if the growth of the fitted curve is minimal.\n"
},
"layer": {
"key": "building_count",
"name": "Building Count",
"description": "All buildings as defined by all objects tagged with 'building=*'.\n"
},
"result": {
"description": "The saturation of the last 3 years is 98.4%.\nHigh saturation has been reached (97% < Saturation ≤ 100%).\n",
"timestamp_oqt": "2022-09-07T20:00:24.897112+00:00",
"timestamp_osm": "2022-08-01T00:00:00+00:00",
"value": 0.9839766580301241,
"label": "green",
"class": 5
}
},
{
"metadata": {
"name": "Currentness",
"description": "Ratio of all contributions that have been edited since 2008 until the current day in relation with years without mapping activities in the same\ntime range.\nRefers to data quality in respect to currentness.\n"
},
"layer": {
"key": "building_count",
"name": "Building Count",
"description": "All buildings as defined by all objects tagged with 'building=*'.\n"
},
"result": {
"description": "Over 50% of the 41638.0 features (Building Count) were edited in the last 4 years.\nThis is a rather high value and indicates that the map features \nare very unlike to be outdated. This refers to good data quality in \nrespect to currentness.\n",
"timestamp_oqt": "2022-09-07T20:00:24.897112+00:00",
"timestamp_osm": "2022-08-28T08:00:00+00:00",
"value": 0.8,
"label": "green",
"class": 5
}
}
]
}
}
```
90 changes: 52 additions & 38 deletions workers/ohsome_quality_analyst/base/report.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from __future__ import annotations # superfluous in Python 3.10

import logging
from abc import ABCMeta, abstractmethod
from dataclasses import asdict, dataclass
from statistics import mean
from typing import Dict, List, Literal, NamedTuple, Tuple
from typing import List, Literal, NamedTuple, Tuple

import numpy as np
from dacite import from_dict
from geojson import Feature

Expand All @@ -22,17 +24,21 @@ class Metadata:

name: str
description: str
label_description: Dict
label_description: dict


@dataclass
class Result:
"""The result of the Report."""

label: Literal["green", "yellow", "red", "undefined"]
value: float
description: str
html: str
class_: Literal[1, 2, 3, 4, 5] | None = None
description: str = ""
html: str = ""

@property
def label(self) -> Literal["green", "yellow", "red", "undefined"]:
labels = {1: "red", 2: "yellow", 3: "yellow", 4: "green", 5: "green"}
return labels.get(self.class_, "undefined")


class IndicatorLayer(NamedTuple):
Expand All @@ -41,30 +47,36 @@ class IndicatorLayer(NamedTuple):


class BaseReport(metaclass=ABCMeta):
"""Subclass has to create and append indicator objects to indicators list."""

def __init__(self, feature: Feature = None):
def __init__(
self,
feature: Feature,
indicator_layer: Tuple[IndicatorLayer] = None,
):
self.feature = feature
self.indicator_layer = indicator_layer # Defines indicator+layer combinations

# Defines indicator+layer combinations
self.indicator_layer: Tuple[IndicatorLayer] = []
self.indicators: List[BaseIndicator] = []

metadata = get_metadata("reports", type(self).__name__)
self.metadata: Metadata = from_dict(data_class=Metadata, data=metadata)
self.blocking_undefined = False
self.blocking_red = False
# Results will be written during the lifecycle of the report object (combine())
self.result = Result(None, None, None, None)
self.result = Result()

def as_feature(self, flatten: bool = False, include_data: bool = False) -> Feature:
"""Returns a GeoJSON Feature object.

The properties of the Feature contains the attributes of all indicators.
The geometry (and properties) of the input GeoJSON object is preserved.
"""
result = asdict(self.result) # only attributes, no properties
result["label"] = self.result.label # label is a property
if result["class_"] is not None:
result["class_"] = self.result.class_
properties = {
"report": {
"metadata": asdict(self.metadata),
"result": asdict(self.result),
"result": result,
},
"indicators": [],
}
Expand All @@ -90,33 +102,35 @@ def combine_indicators(self) -> None:
"""Combine indicators results and create the report result object."""
logging.info(f"Combine indicators for report: {self.metadata.name}")

values = []
for indicator in self.indicators:
if indicator.result.label != "undefined":
values.append(indicator.result.value)
else:
values.append(0.0)

if all(val == 0.0 for val in values):
self.result.value = None
self.result.label = "undefined"
self.result.description = "Could not derive quality level"
return None
else:
self.result.value = mean(values)
if self.blocking_undefined:
if any(i.result.class_ is None for i in self.indicators):
self.result.description = self.metadata.label_description["undefined"]
return

if self.blocking_red:
if any(i.result.class_ == 1 for i in self.indicators):
self.result.class_ = 1
self.result.description = self.metadata.label_description["red"]
return

self.result.class_ = round(
np.mean(
[
i.result.class_
for i in self.indicators
if i.result.class_ is not None
]
)
)

if (
all(indicator.result.label == "green" for indicator in self.indicators)
or self.result.value >= 1
):
self.result.label = "green"
if self.result.class_ in (4, 5):
self.result.description = self.metadata.label_description["green"]
elif self.result.value >= 0.5:
self.result.label = "yellow"
elif self.result.class_ in (2, 3):
self.result.description = self.metadata.label_description["yellow"]
elif self.result.value < 0.5:
self.result.label = "red"
elif self.result.class_ == 1:
self.result.description = self.metadata.label_description["red"]
else:
self.result.description = self.metadata.label_description["undefined"]

@abstractmethod
def set_indicator_layer(self) -> None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def calculate(self) -> None:
# self.result.value (ratio) can be of type float, NaN if no features of filter1
# are in the region or None if the layer has no filter2
if self.result.value == "NaN" or self.result.value is None:
self.result.value = None
return
description = Template(self.metadata.result_description).substitute(
result=round(self.result.value, 1),
Expand Down
13 changes: 8 additions & 5 deletions workers/ohsome_quality_analyst/reports/building_report/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@

class BuildingReport(BaseReport):
def set_indicator_layer(self):
self.indicator_layer = (
IndicatorLayer("MappingSaturation", "building_count"),
IndicatorLayer("Currentness", "building_count"),
IndicatorLayer("TagsRatio", "building_count"),
IndicatorLayer("BuildingCompleteness", "building_area"),
super().__init__(
indicator_layer=(
IndicatorLayer("MappingSaturation", "building_count"),
IndicatorLayer("Currentness", "building_count"),
IndicatorLayer("TagsRatio", "building_count"),
IndicatorLayer("BuildingCompleteness", "building_area"),
),
feature=self.feature,
)

def combine_indicators(self) -> None:
Expand Down
59 changes: 31 additions & 28 deletions workers/ohsome_quality_analyst/reports/jrc_requirements/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,37 @@

class JrcRequirements(BaseReport):
def set_indicator_layer(self) -> None:
self.indicator_layer = (
IndicatorLayer("MappingSaturation", "jrc_health_count"),
IndicatorLayer("MappingSaturation", "jrc_mass_gathering_sites_count"),
IndicatorLayer("MappingSaturation", "jrc_railway_length"),
IndicatorLayer("MappingSaturation", "jrc_road_length"),
IndicatorLayer("MappingSaturation", "jrc_education_count"),
IndicatorLayer("Currentness", "jrc_health_count"),
IndicatorLayer("Currentness", "jrc_education_count"),
IndicatorLayer("Currentness", "jrc_road_count"),
IndicatorLayer("Currentness", "jrc_railway_count"),
IndicatorLayer("Currentness", "jrc_airport_count"),
IndicatorLayer("Currentness", "jrc_water_treatment_plant_count"),
IndicatorLayer("Currentness", "jrc_power_generation_plant_count"),
IndicatorLayer("Currentness", "jrc_cultural_heritage_site_count"),
IndicatorLayer("Currentness", "jrc_bridge_count"),
IndicatorLayer("Currentness", "jrc_mass_gathering_sites_count"),
# TODO define thresholds for GhsPopComparison with jrc
# layer topics
# IndicatorLayer("GhsPopComparison", "jrc_road_length"),
# IndicatorLayer("GhsPopComparison", "jrc_railway_length"),
IndicatorLayer("TagsRatio", "jrc_health_count"),
IndicatorLayer("TagsRatio", "jrc_education_count"),
IndicatorLayer("TagsRatio", "jrc_road_length"),
IndicatorLayer("TagsRatio", "jrc_airport_count"),
IndicatorLayer("TagsRatio", "jrc_power_generation_plant_count"),
IndicatorLayer("TagsRatio", "jrc_cultural_heritage_site_count"),
IndicatorLayer("TagsRatio", "jrc_bridge_count"),
IndicatorLayer("TagsRatio", "jrc_mass_gathering_sites_count"),
super().__init__(
indicator_layer=(
IndicatorLayer("MappingSaturation", "jrc_health_count"),
IndicatorLayer("MappingSaturation", "jrc_mass_gathering_sites_count"),
IndicatorLayer("MappingSaturation", "jrc_railway_length"),
IndicatorLayer("MappingSaturation", "jrc_road_length"),
IndicatorLayer("MappingSaturation", "jrc_education_count"),
IndicatorLayer("Currentness", "jrc_health_count"),
IndicatorLayer("Currentness", "jrc_education_count"),
IndicatorLayer("Currentness", "jrc_road_count"),
IndicatorLayer("Currentness", "jrc_railway_count"),
IndicatorLayer("Currentness", "jrc_airport_count"),
IndicatorLayer("Currentness", "jrc_water_treatment_plant_count"),
IndicatorLayer("Currentness", "jrc_power_generation_plant_count"),
IndicatorLayer("Currentness", "jrc_cultural_heritage_site_count"),
IndicatorLayer("Currentness", "jrc_bridge_count"),
IndicatorLayer("Currentness", "jrc_mass_gathering_sites_count"),
# TODO define thresholds for GhsPopComparison with jrc
# layer topics
# IndicatorLayer("GhsPopComparison", "jrc_road_length"),
# IndicatorLayer("GhsPopComparison", "jrc_railway_length"),
IndicatorLayer("TagsRatio", "jrc_health_count"),
IndicatorLayer("TagsRatio", "jrc_education_count"),
IndicatorLayer("TagsRatio", "jrc_road_length"),
IndicatorLayer("TagsRatio", "jrc_airport_count"),
IndicatorLayer("TagsRatio", "jrc_power_generation_plant_count"),
IndicatorLayer("TagsRatio", "jrc_cultural_heritage_site_count"),
IndicatorLayer("TagsRatio", "jrc_bridge_count"),
IndicatorLayer("TagsRatio", "jrc_mass_gathering_sites_count"),
),
feature=self.feature,
)

def combine_indicators(self) -> None:
Expand Down
25 changes: 14 additions & 11 deletions workers/ohsome_quality_analyst/reports/map_action_poc/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@

class MapActionPoc(BaseReport):
def set_indicator_layer(self) -> None:
self.indicator_layer = (
IndicatorLayer("MappingSaturation", "mapaction_settlements_count"),
IndicatorLayer("MappingSaturation", "mapaction_major_roads_length"),
IndicatorLayer("MappingSaturation", "mapaction_rail_length"),
IndicatorLayer("MappingSaturation", "mapaction_lakes_area"),
IndicatorLayer("MappingSaturation", "mapaction_rivers_length"),
IndicatorLayer("Currentness", "mapaction_settlements_count"),
IndicatorLayer("Currentness", "mapaction_major_roads_length"),
IndicatorLayer("Currentness", "mapaction_rail_length"),
IndicatorLayer("Currentness", "mapaction_lakes_count"),
IndicatorLayer("Currentness", "mapaction_rivers_length"),
super().__init__(
indicator_layer=(
IndicatorLayer("MappingSaturation", "mapaction_settlements_count"),
IndicatorLayer("MappingSaturation", "mapaction_major_roads_length"),
IndicatorLayer("MappingSaturation", "mapaction_rail_length"),
IndicatorLayer("MappingSaturation", "mapaction_lakes_area"),
IndicatorLayer("MappingSaturation", "mapaction_rivers_length"),
IndicatorLayer("Currentness", "mapaction_settlements_count"),
IndicatorLayer("Currentness", "mapaction_major_roads_length"),
IndicatorLayer("Currentness", "mapaction_rail_length"),
IndicatorLayer("Currentness", "mapaction_lakes_count"),
IndicatorLayer("Currentness", "mapaction_rivers_length"),
),
feature=self.feature,
)

def combine_indicators(self) -> None:
Expand Down
Loading