Skip to content

Commit b876a77

Browse files
add result class to indicators
Generalize result values of all Indicators by introducing a result class value. Every Indicator determines a result class value for itself. The result class value range is 1 up to 5 and maps to the result labels. Rewrite using pytest instead of unittest
1 parent f4e2943 commit b876a77

File tree

21 files changed

+163
-176
lines changed

21 files changed

+163
-176
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Current Main
44

5+
### New Features
6+
7+
- Generalize result values of all Indicators by introducing a result class value ([#369])
8+
59
### Breaking Changes
610

711
- Rename environment variable `OHSOME_API` to `OQT_OHSOME_API` ([#255])
@@ -40,7 +44,9 @@
4044
- Rename environment variable `OHSOME_API` `OQT_OHSOME_API` ([#255])
4145
- Make sure to rename the API query parameter `layerName` to `layerKey` and API endpoint `listLayerNames` to `listLayerKeys` ([#376])
4246
- To continue to retrieve the properties of the GeoJSON API response as flat list, you need to set the API request parameter `flattem` to `True` ([#375])
47+
- If you run your own database, please delete the result table before upgrading ([#369])
4348
- Rename endpoints ([#397]):
49+
4450
| old | new |
4551
| --- | --- |
4652
| `indicatorLayerCombinations` | `indicator-layer-combinations` |
@@ -54,6 +60,7 @@
5460
[#342]: https://github.com/GIScience/ohsome-quality-analyst/pull/342
5561
[#356]: https://github.com/GIScience/ohsome-quality-analyst/pull/356
5662
[#357]: https://github.com/GIScience/ohsome-quality-analyst/pull/357
63+
[#369]: https://github.com/GIScience/ohsome-quality-analyst/pull/369
5764
[#370]: https://github.com/GIScience/ohsome-quality-analyst/pull/370
5865
[#375]: https://github.com/GIScience/ohsome-quality-analyst/pull/375
5966
[#376]: https://github.com/GIScience/ohsome-quality-analyst/pull/376
@@ -74,6 +81,7 @@
7481
[#379]: https://github.com/GIScience/ohsome-quality-analyst/pull/379
7582

7683

84+
7785
## 0.10.0
7886

7987
### Bug Fixes

docs/indicator_creation.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ As you can see, the indicator you are trying to create should inherit from BaseI
1515

1616
### Result
1717

18-
The result object can hold 4 values.
19-
20-
1. label: This should be a member of `TrafficLightQualityLevels` found in [workers/ohsome_quality_analyst/utils/definitions.py](/workers/ohsome_quality_analyst/utils/definitions.py)
21-
2. value: TBD
22-
3. description: label description for `TrafficLightQualityLevel` (see metadata.yaml in part 2)
23-
4. svg: unique file path which is **automatically** created upon object initialization by the `BaseIndicator`
24-
18+
The result object consists of following attributes:
19+
20+
- `timestamp_oqt (datetime)`: Timestamp of the creation of the indicator
21+
- `timestamp_osm (datetime)`: Timestamp of the used OSM data (e.g. the latest timestamp of the ohsome API results)
22+
- `label (str)`: Traffic lights like quality label: `green`, `yellow` or `red`. The value is determined by the result classes
23+
- `value (float)`: The result value
24+
- `class (int)`: The result class. An integer between 1 and 5. It maps to the result labels: `1` maps to `red`, `2`/`3` map to `yellow` and `4`/`5` map to `green`. This value is used by the reports to determine an overall result.
25+
- `description (str)`: The result description.
26+
- `svg (str)`: Figure of the result as SVG
2527

2628
### Layer
2729

workers/ohsome_quality_analyst/base/indicator.py

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
"""
2-
TODO:
3-
Describe this module and how to implement child classes
4-
"""
1+
"""The base classes on which every indicator class is based on."""
52

63
import json
74
from abc import ABCMeta, abstractmethod
@@ -41,19 +38,28 @@ class Result:
4138
timestamp_oqt (datetime): Timestamp of the creation of the indicator
4239
timestamp_osm (datetime): Timestamp of the used OSM data
4340
(e.g. Latest timestamp of the ohsome API results)
44-
label (str): Traffic lights like quality label
45-
value (float): The result value as float ([0, 1])
46-
description (str): Description of the result
41+
label (str): Traffic lights like quality label: `green`, `yellow` or `red`. The
42+
value is determined by the result classes
43+
value (float): The result value
44+
class_ (int): The result class. An integer between 1 and 5. It maps to the
45+
result labels. This value is used by the reports to determine an overall
46+
result.
47+
description (str): The result description.
4748
svg (str): Figure of the result as SVG
4849
"""
4950

50-
timestamp_oqt: datetime
51-
timestamp_osm: Optional[datetime]
52-
label: Literal["green", "yellow", "red", "undefined"]
53-
value: Optional[float]
5451
description: str
5552
svg: str
5653
html: str
54+
timestamp_oqt: datetime = datetime.now(timezone.utc) # UTC datetime object
55+
timestamp_osm: Optional[datetime] = None
56+
value: Optional[float] = None
57+
class_: Optional[Literal[1, 2, 3, 4, 5]] = None
58+
59+
@property
60+
def label(self) -> Literal["green", "yellow", "red", "undefined"]:
61+
labels = {1: "red", 2: "yellow", 3: "yellow", 4: "green", 5: "green"}
62+
return labels.get(self.class_, "undefined")
5763

5864

5965
class BaseIndicator(metaclass=ABCMeta):
@@ -70,11 +76,6 @@ def __init__(
7076
metadata = get_metadata("indicators", type(self).__name__)
7177
self.metadata: Metadata = from_dict(data_class=Metadata, data=metadata)
7278
self.result: Result = Result(
73-
# UTC datetime object representing the current time.
74-
timestamp_oqt=datetime.now(timezone.utc),
75-
timestamp_osm=None,
76-
label="undefined",
77-
value=None,
7879
description=self.metadata.label_description["undefined"],
7980
svg=self._get_default_figure(),
8081
html="",
@@ -90,6 +91,9 @@ def as_feature(self, flatten: bool = False, include_data: bool = False) -> Featu
9091
flatten (bool): If true flatten the properties.
9192
include_data (bool): If true include additional data in the properties.
9293
"""
94+
result = asdict(self.result) # only attributes, no properties
95+
result["label"] = self.result.label # label is a property
96+
result["class"] = result.pop("class_")
9397
properties = {
9498
"metadata": {
9599
"name": self.metadata.name,
@@ -100,7 +104,7 @@ def as_feature(self, flatten: bool = False, include_data: bool = False) -> Featu
100104
"name": self.layer.name,
101105
"description": self.layer.description,
102106
},
103-
"result": asdict(self.result),
107+
"result": result,
104108
**self.feature.properties,
105109
}
106110
if include_data:

workers/ohsome_quality_analyst/geodatabase/client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ async def save_indicator_results(
7878
feature_id,
7979
indicator.result.timestamp_oqt,
8080
indicator.result.timestamp_osm,
81-
indicator.result.label,
81+
indicator.result.class_,
8282
indicator.result.value,
8383
indicator.result.description,
8484
indicator.result.svg,
@@ -128,7 +128,7 @@ async def load_indicator_results(
128128

129129
indicator.result.timestamp_oqt = query_result["timestamp_oqt"]
130130
indicator.result.timestamp_osm = query_result["timestamp_osm"]
131-
indicator.result.label = query_result["result_label"]
131+
indicator.result.class_ = query_result["result_class"]
132132
indicator.result.value = query_result["result_value"]
133133
indicator.result.description = query_result["result_description"]
134134
indicator.result.svg = query_result["result_svg"]

workers/ohsome_quality_analyst/geodatabase/create_results_table.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ CREATE TABLE IF NOT EXISTS results (
55
fid text,
66
timestamp_oqt timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
77
timestamp_osm timestamp with time zone,
8-
result_label text,
8+
result_class integer,
99
result_value float, -- VALUE is an SQL keyword
1010
result_description text,
1111
result_svg text,

workers/ohsome_quality_analyst/geodatabase/load_results.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ SELECT
55
fid,
66
timestamp_oqt,
77
timestamp_osm,
8-
result_label,
8+
result_class,
99
result_value,
1010
result_description,
1111
result_svg,

workers/ohsome_quality_analyst/geodatabase/save_results.sql

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ INSERT INTO results (
55
fid,
66
timestamp_oqt,
77
timestamp_osm,
8-
result_label,
8+
result_class,
99
result_value,
1010
result_description,
1111
result_svg,
@@ -31,14 +31,14 @@ ON CONFLICT (
3131
(
3232
timestamp_oqt,
3333
timestamp_osm,
34-
result_label,
34+
result_class,
3535
result_value,
3636
result_description,
3737
result_svg,
3838
feature) = (
3939
excluded.timestamp_oqt,
4040
excluded.timestamp_osm,
41-
excluded.result_label,
41+
excluded.result_class,
4242
excluded.result_value,
4343
excluded.result_description,
4444
excluded.result_svg,

workers/ohsome_quality_analyst/indicators/building_completeness/indicator.py

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -137,32 +137,26 @@ def calculate(self) -> None:
137137
self.completeness_ratio,
138138
weights=self.building_area_prediction,
139139
)
140-
description = Template(self.metadata.result_description).substitute(
141-
building_area_osm=round(sum(self.building_area_osm), 2),
142-
building_area_prediction=round(sum(self.building_area_prediction), 2),
143-
completeness_ratio=round(self.result.value * 100, 2),
144-
)
145140
if self.result.value >= self.threshhold_green():
146-
self.result.label = "green"
147-
self.result.description = (
148-
description + self.metadata.label_description["green"]
149-
)
141+
self.result.class_ = 5
150142
elif self.result.value >= self.threshhold_yellow():
151-
self.result.label = "yellow"
152-
self.result.description = (
153-
description + self.metadata.label_description["yellow"]
154-
)
143+
self.result.class_ = 3
155144
elif 0.0 <= self.result.value < self.threshhold_yellow():
156-
self.result.label = "red"
157-
self.result.description = (
158-
description + self.metadata.label_description["red"]
159-
)
145+
self.result.class_ = 1
160146
else:
161147
raise ValueError(
162148
"Result value (percentage mapped) is an unexpected value: {}".format(
163149
self.result.value
164150
)
165151
)
152+
description = Template(self.metadata.result_description).substitute(
153+
building_area_osm=round(sum(self.building_area_osm), 2),
154+
building_area_prediction=round(sum(self.building_area_prediction), 2),
155+
completeness_ratio=round(self.result.value * 100, 2),
156+
)
157+
self.result.description = (
158+
description + self.metadata.label_description[self.result.label]
159+
)
166160

167161
def create_figure(self) -> None:
168162
if self.result.label == "undefined":

workers/ohsome_quality_analyst/indicators/currentness/indicator.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,17 +131,17 @@ def calculate(self) -> None:
131131
)
132132

133133
if self.result.value >= self.threshold_yellow:
134-
self.result.label = "green"
134+
self.result.class_ = 5
135135
self.result.description = (
136136
self.result.description + self.metadata.label_description["green"]
137137
)
138138
elif self.result.value >= self.threshold_red:
139-
self.result.label = "yellow"
139+
self.result.class_ = 3
140140
self.result.description = (
141141
self.result.description + self.metadata.label_description["yellow"]
142142
)
143143
elif self.result.value < self.threshold_red:
144-
self.result.label = "red"
144+
self.result.class_ = 1
145145
self.result.description = (
146146
self.result.description + self.metadata.label_description["red"]
147147
)

workers/ohsome_quality_analyst/indicators/ghs_pop_comparison_buildings/indicator.py

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ def __init__(self, layer: Layer, feature: Feature) -> None:
2525
self.area = None
2626
self.pop_count_per_sqkm = None
2727
self.feature_count = None
28-
self.feature_count_per_sqkm = None
2928

3029
@classmethod
3130
def attribution(cls) -> str:
@@ -57,50 +56,41 @@ async def preprocess(self) -> None:
5756
self.feature_count = query_results["result"][0]["value"]
5857
timestamp = query_results["result"][0]["timestamp"]
5958
self.result.timestamp_osm = dateutil.parser.isoparse(timestamp)
60-
self.feature_count_per_sqkm = self.feature_count / self.area
6159
self.pop_count_per_sqkm = self.pop_count / self.area
6260

6361
def calculate(self) -> None:
62+
self.result.value = self.feature_count / self.area # feature_count_per_sqkm
6463
description = Template(self.metadata.result_description).substitute(
6564
pop_count=round(self.pop_count),
6665
area=round(self.area, 1),
6766
pop_count_per_sqkm=round(self.pop_count_per_sqkm, 1),
68-
feature_count_per_sqkm=round(self.feature_count_per_sqkm, 1),
67+
feature_count_per_sqkm=round(self.result.value, 1),
6968
)
7069

7170
if self.pop_count_per_sqkm == 0:
7271
return
7372

74-
elif self.feature_count_per_sqkm <= self.yellow_threshold_function(
73+
elif self.result.value <= self.yellow_threshold_function(
7574
self.pop_count_per_sqkm
7675
):
77-
self.result.value = (
78-
self.feature_count_per_sqkm
79-
/ self.yellow_threshold_function(self.pop_count_per_sqkm)
80-
) * (0.5)
76+
self.result.class_ = 1
8177
self.result.description = (
8278
description + self.metadata.label_description["red"]
8379
)
84-
self.result.label = "red"
8580

86-
elif self.feature_count_per_sqkm <= self.green_threshold_function(
81+
elif self.result.value <= self.green_threshold_function(
8782
self.pop_count_per_sqkm
8883
):
89-
green = self.green_threshold_function(self.pop_count_per_sqkm)
90-
yellow = self.yellow_threshold_function(self.pop_count_per_sqkm)
91-
fraction = (self.feature_count_per_sqkm - yellow) / (green - yellow) * 0.5
92-
self.result.value = 0.5 + fraction
84+
self.result.class_ = 3
9385
self.result.description = (
9486
description + self.metadata.label_description["yellow"]
9587
)
96-
self.result.label = "yellow"
9788

9889
else:
99-
self.result.value = 1.0
90+
self.result.class_ = 5
10091
self.result.description = (
10192
description + self.metadata.label_description["green"]
10293
)
103-
self.result.label = "green"
10494

10595
def create_figure(self) -> None:
10696
if self.result.label == "undefined":
@@ -148,15 +138,15 @@ def create_figure(self) -> None:
148138
ax.fill_between(
149139
x,
150140
y1,
151-
max(max(y1), self.feature_count_per_sqkm),
141+
max(max(y1), self.result.value),
152142
alpha=0.5,
153143
color="green",
154144
)
155145

156146
# Plot pont as circle ("o").
157147
ax.plot(
158148
self.pop_count_per_sqkm,
159-
self.feature_count_per_sqkm,
149+
self.result.value,
160150
"o",
161151
color="black",
162152
label="location",

0 commit comments

Comments
 (0)