Skip to content

Commit 3e3ac92

Browse files
authored
Reports are created on grouped inference rows (#151)
* Reports are created on grouped inference rows * Changes: - Added granularity as a model property - Added checks in API for granularity validity - Updated docs - Fixed some typings and syntax * Added changes in model granularity to streamlit * Additions: - Tests - Changed start_time into the start of the day if not set by timestamp
1 parent 4161613 commit 3e3ac92

File tree

27 files changed

+546
-88
lines changed

27 files changed

+546
-88
lines changed

.coverage

0 Bytes
Binary file not shown.

docs/mkdocs/docs/sdk-docs.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,18 @@ This is the documentation for Whitebox's SDK. For an interactive experience, you
44

55
## Models
66

7-
**_create_model_**_(name, type, target_column, labels=None, description="")_
7+
**_create_model_**_(name, type, target_column, granularity, labels=None, description="")_
88

99
Creates a model in the database. This model works as placeholder for all the actual model's metadata.
1010

11-
| Parameter | Type | Description |
12-
| --------------- | ---------------- | ------------------------------------------------------------------------- |
13-
| **name** | `str` | The name of the model. |
14-
| **type** | `str` | The model's type. Possible values: `binary`, `multi_class`, `regression`. |
15-
| **target_column** | `str` | The name of the target column (y). |
16-
| **labels** | `Dict[str, int]` | The model's labels. Defaults to `None`. |
17-
| **description** | `str` | The model's description. Defaults to an empty string `""`. |
11+
| Parameter | Type | Description |
12+
| ----------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
13+
| **name** | `str` | The name of the model. |
14+
| **type** | `str` | The model's type. Possible values: `binary`, `multi_class`, `regression`. |
15+
| **target_column** | `str` | The name of the target column (y). |
16+
| **granularity** | `str` | The granularity depending on which the inference rows will be grouped by to create the reports. Must be a `str` containing the amount (`int`) and the type (e.g. "1D"). Possible values for granularity type: `T (minutes)`, `H (hours)`, `D (days)`, `W (weeks)`. |
17+
| **labels** | `Dict[str, int]` | The model's labels. Defaults to `None`. |
18+
| **description** | `str` | The model's description. Defaults to an empty string `""`. |
1819

1920
!!! info
2021

docs/mkdocs/docs/tutorial/sdk.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ wb.create_model(
7373
'additionalProp1': 0,
7474
'additionalProp2': 1
7575
},
76-
target_column="target"
76+
target_column="target",
77+
granularity="1D"
7778
)
7879
```
7980

whitebox/api/v1/inference_rows.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
from typing import Dict, List
22
from whitebox.middleware.auth import authenticate_user
3-
from whitebox.schemas.inferenceRow import InferenceRow, InferenceRowCreateDto
3+
from whitebox.schemas.inferenceRow import (
4+
InferenceRow,
5+
InferenceRowCreateDto,
6+
InferenceRowPreDb,
7+
)
48
from whitebox.analytics.xai_models.pipelines import (
59
create_xai_pipeline_per_inference_row,
610
)
@@ -31,7 +35,9 @@ async def create_row(
3135
) -> InferenceRow:
3236
"""Inserts an inference row into the database."""
3337

34-
new_inference_row = crud.inference_rows.create(db=db, obj_in=body)
38+
updated_body = InferenceRowPreDb(**dict(body), is_used=False)
39+
40+
new_inference_row = crud.inference_rows.create(db=db, obj_in=updated_body)
3541
return new_inference_row
3642

3743

@@ -58,7 +64,10 @@ async def create_many_inference_rows(
5864
f'Column "{model.target_column}" was not found in some or any of the rows in provided inference dataset. Please try again!'
5965
)
6066

61-
new_inference_rows = crud.inference_rows.create_many(db=db, obj_list=body)
67+
updated_body = [InferenceRowPreDb(**dict(x), is_used=False) for x in body]
68+
new_inference_rows = crud.inference_rows.create_many(
69+
db=db, obj_list=updated_body
70+
)
6271
return new_inference_rows
6372
else:
6473
return errors.not_found(f"Model with id: {dict(body[0])['model_id']} not found")

whitebox/api/v1/models.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,24 @@ async def create_model(
2828
) -> Model:
2929
"""Inserts a model into the database"""
3030

31+
granularity = body.granularity
32+
33+
try:
34+
granularity_amount = float(granularity[:-1])
35+
except ValueError:
36+
return errors.bad_request("Granularity amount that was given is not a number!")
37+
38+
if not granularity_amount.is_integer():
39+
return errors.bad_request(
40+
"Granularity amount should be an integer and not a float (e.g. 1D)!"
41+
)
42+
43+
granularity_type = granularity[-1]
44+
if granularity_type not in ["T", "H", "D", "W"]:
45+
return errors.bad_request(
46+
"Wrong granularity type. Accepted values: T (minutes), H (hours), D (days), W (weeks)"
47+
)
48+
3149
new_model = crud.models.create(db=db, obj_in=body)
3250
return new_model
3351

whitebox/api/v1/performance_metrics.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,15 @@ async def get_all_models_performance_metrics(
3838

3939
model = crud.models.get(db, model_id)
4040
if model:
41-
if vars(model)["type"] == ModelType.binary:
41+
if model.type == ModelType.binary:
4242
return crud.binary_classification_metrics.get_performance_metrics_by_model(
4343
db=db, model_id=model_id
4444
)
45-
elif vars(model)["type"] == ModelType.multi_class:
45+
elif model.type == ModelType.multi_class:
4646
return crud.multi_classification_metrics.get_performance_metrics_by_model(
4747
db=db, model_id=model_id
4848
)
49-
elif vars(model)["type"] == ModelType.regression:
49+
elif model.type == ModelType.regression:
5050
return crud.regression_metrics.get_performance_metrics_by_model(
5151
db=db, model_id=model_id
5252
)

whitebox/core/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class Settings(BaseSettings):
1010
VERSION: str = ""
1111
MODEL_PATH: str = ""
1212
SECRET_KEY: str = ""
13+
GRANULARITY: str = ""
1314

1415
class Config:
1516
env_file = f".env.{os.getenv('ENV')}" or ".env.dev"

whitebox/cron_tasks/monitoring_metrics.py

Lines changed: 132 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import time
44
from sqlalchemy import create_engine
55
from sqlalchemy.orm import sessionmaker, Session
6-
6+
from fastapi.encoders import jsonable_encoder
77
from whitebox import crud, entities
88
from whitebox.analytics.drift.pipelines import (
99
run_data_drift_pipeline,
@@ -19,7 +19,13 @@
1919
from whitebox.cron_tasks.shared import (
2020
get_all_models,
2121
get_model_dataset_rows_df,
22-
get_model_inference_rows_df,
22+
get_unused_model_inference_rows,
23+
group_inference_rows_by_timestamp,
24+
seperate_inference_rows,
25+
set_inference_rows_to_used,
26+
get_latest_drift_metrics_report,
27+
round_timestamp,
28+
get_used_inference_for_reusage,
2329
)
2430
from whitebox.schemas.model import Model, ModelType
2531
from whitebox.schemas.modelIntegrityMetric import ModelIntegrityMetricCreate
@@ -34,7 +40,7 @@
3440

3541

3642
async def run_calculate_drifting_metrics_pipeline(
37-
model: Model, inference_processed_df: pd.DataFrame
43+
model: Model, inference_processed_df: pd.DataFrame, timestamp: datetime
3844
):
3945
"""
4046
Run the pipeline to calculate the drifting metrics
@@ -67,18 +73,29 @@ async def run_calculate_drifting_metrics_pipeline(
6773
)
6874

6975
new_drifting_metric = entities.DriftingMetric(
70-
timestamp=str(datetime.utcnow()),
76+
timestamp=str(timestamp),
7177
model_id=model.id,
7278
concept_drift_summary=concept_drift_report,
7379
data_drift_summary=data_drift_report,
7480
)
7581

76-
crud.drifting_metrics.create(db, obj_in=new_drifting_metric)
82+
existing_report = crud.drifting_metrics.get_first_by_filter(
83+
db=db, model_id=model.id, timestamp=timestamp
84+
)
85+
if existing_report:
86+
crud.drifting_metrics.update(
87+
db=db, db_obj=existing_report, obj_in=jsonable_encoder(new_drifting_metric)
88+
)
89+
else:
90+
crud.drifting_metrics.create(db, obj_in=new_drifting_metric)
7791
logger.info("Drifting metrics calculated!")
7892

7993

8094
async def run_calculate_performance_metrics_pipeline(
81-
model: Model, inference_processed_df: pd.DataFrame, actual_df: pd.DataFrame
95+
model: Model,
96+
inference_processed_df: pd.DataFrame,
97+
actual_df: pd.DataFrame,
98+
timestamp: datetime,
8299
):
83100
"""
84101
Run the pipeline to calculate the performance metrics
@@ -121,11 +138,21 @@ async def run_calculate_performance_metrics_pipeline(
121138

122139
new_performance_metric = entities.BinaryClassificationMetrics(
123140
model_id=model.id,
124-
timestamp=str(datetime.utcnow()),
141+
timestamp=str(timestamp),
125142
**dict(binary_classification_metrics_report),
126143
)
127144

128-
crud.binary_classification_metrics.create(db, obj_in=new_performance_metric)
145+
existing_report = crud.binary_classification_metrics.get_first_by_filter(
146+
db=db, model_id=model.id, timestamp=timestamp
147+
)
148+
if existing_report:
149+
crud.binary_classification_metrics.update(
150+
db=db,
151+
db_obj=existing_report,
152+
obj_in=jsonable_encoder(new_performance_metric),
153+
)
154+
else:
155+
crud.binary_classification_metrics.create(db, obj_in=new_performance_metric)
129156

130157
elif model.type == ModelType.multi_class:
131158
multiclass_classification_metrics_report = (
@@ -136,11 +163,21 @@ async def run_calculate_performance_metrics_pipeline(
136163

137164
new_performance_metric = entities.MultiClassificationMetrics(
138165
model_id=model.id,
139-
timestamp=str(datetime.utcnow()),
166+
timestamp=str(timestamp),
140167
**dict(multiclass_classification_metrics_report),
141168
)
142169

143-
crud.multi_classification_metrics.create(db, obj_in=new_performance_metric)
170+
existing_report = crud.multi_classification_metrics.get_first_by_filter(
171+
db=db, model_id=model.id, timestamp=timestamp
172+
)
173+
if existing_report:
174+
crud.multi_classification_metrics.update(
175+
db=db,
176+
db_obj=existing_report,
177+
obj_in=jsonable_encoder(new_performance_metric),
178+
)
179+
else:
180+
crud.multi_classification_metrics.create(db, obj_in=new_performance_metric)
144181

145182
elif model.type == ModelType.regression:
146183
regression_metrics_report = create_regression_evaluation_metrics_pipeline(
@@ -149,17 +186,27 @@ async def run_calculate_performance_metrics_pipeline(
149186

150187
new_performance_metric = entities.RegressionMetrics(
151188
model_id=model.id,
152-
timestamp=str(datetime.utcnow()),
189+
timestamp=str(timestamp),
153190
**dict(regression_metrics_report),
154191
)
155192

156-
crud.regression_metrics.create(db, obj_in=new_performance_metric)
193+
existing_report = crud.regression_metrics.get_first_by_filter(
194+
db=db, model_id=model.id, timestamp=timestamp
195+
)
196+
if existing_report:
197+
crud.regression_metrics.update(
198+
db=db,
199+
db_obj=existing_report,
200+
obj_in=jsonable_encoder(new_performance_metric),
201+
)
202+
else:
203+
crud.regression_metrics.create(db, obj_in=new_performance_metric)
157204

158205
logger.info("Performance metrics calculated!")
159206

160207

161208
async def run_calculate_feature_metrics_pipeline(
162-
model: Model, inference_processed_df: pd.DataFrame
209+
model: Model, inference_processed_df: pd.DataFrame, timestamp: datetime
163210
):
164211
"""
165212
Run the pipeline to calculate the feature metrics
@@ -172,11 +219,22 @@ async def run_calculate_feature_metrics_pipeline(
172219
if feature_metrics_report:
173220
new_feature_metric = ModelIntegrityMetricCreate(
174221
model_id=model.id,
175-
timestamp=str(datetime.utcnow()),
222+
timestamp=str(timestamp),
176223
feature_metrics=feature_metrics_report,
177224
)
178225

179-
crud.model_integrity_metrics.create(db, obj_in=new_feature_metric)
226+
existing_report = crud.model_integrity_metrics.get_first_by_filter(
227+
db=db, model_id=model.id, timestamp=timestamp
228+
)
229+
if existing_report:
230+
crud.model_integrity_metrics.update(
231+
db=db,
232+
db_obj=existing_report,
233+
obj_in=jsonable_encoder(new_feature_metric),
234+
)
235+
else:
236+
crud.model_integrity_metrics.create(db, obj_in=new_feature_metric)
237+
180238
logger.info("Feature metrics calculated!")
181239

182240

@@ -190,24 +248,72 @@ async def run_calculate_metrics_pipeline():
190248
logger.info("No models found! Skipping pipeline")
191249
else:
192250
for model in models:
193-
(
194-
inference_processed_df,
195-
inference_nonprocessed_df,
196-
actual_df,
197-
) = await get_model_inference_rows_df(db, model_id=model.id)
198-
if inference_processed_df.empty:
251+
granularity = model.granularity
252+
granularity_amount = int(granularity[:-1])
253+
granularity_type = granularity[-1]
254+
255+
last_report = await get_latest_drift_metrics_report(db, model)
256+
257+
# We need to get the last report's timestamp as a base of grouping unless there's no report produced.
258+
# In this case, the base timestamp is considered the "now" rounded to the day so the intervals start from midnight
259+
# e.g. 12:00, 12:15, 12:30, 12:45 and so on if granularity is 15T.
260+
last_report_time = (
261+
last_report.timestamp
262+
if last_report
263+
else round_timestamp(datetime.utcnow(), "1D")
264+
)
265+
266+
unused_inference_rows_in_db = await get_unused_model_inference_rows(
267+
db, model_id=model.id
268+
)
269+
270+
if len(unused_inference_rows_in_db) == 0:
199271
logger.info(
200-
f"No inferences found for model {model.id}! Continuing with next model..."
272+
f"No new inferences found for model {model.id}! Continuing with next model..."
201273
)
202274
continue
203275
logger.info(f"Executing Metrics pipeline for model {model.id}...")
204-
await run_calculate_drifting_metrics_pipeline(model, inference_processed_df)
205276

206-
await run_calculate_performance_metrics_pipeline(
207-
model, inference_processed_df, actual_df
277+
used_inferences = get_used_inference_for_reusage(
278+
db,
279+
model.id,
280+
unused_inference_rows_in_db,
281+
last_report_time,
282+
granularity_amount,
283+
granularity_type,
284+
)
285+
286+
all_inferences = unused_inference_rows_in_db + used_inferences
287+
288+
grouped_inference_rows = await group_inference_rows_by_timestamp(
289+
all_inferences,
290+
last_report_time,
291+
granularity_amount,
292+
granularity_type,
208293
)
209294

210-
await run_calculate_feature_metrics_pipeline(model, inference_processed_df)
295+
for group in grouped_inference_rows:
296+
for timestamp, inference_group in group.items():
297+
inference_rows_ids = [x.id for x in inference_group]
298+
(
299+
inference_processed_df,
300+
inference_nonprocessed_df,
301+
actual_df,
302+
) = await seperate_inference_rows(inference_group)
303+
304+
await run_calculate_drifting_metrics_pipeline(
305+
model, inference_processed_df, timestamp
306+
)
307+
308+
await run_calculate_performance_metrics_pipeline(
309+
model, inference_processed_df, actual_df, timestamp
310+
)
311+
312+
await run_calculate_feature_metrics_pipeline(
313+
model, inference_processed_df, timestamp
314+
)
315+
316+
await set_inference_rows_to_used(db, inference_rows_ids)
211317

212318
logger.info(f"Ended Metrics pipeline for model {model.id}...")
213319

0 commit comments

Comments
 (0)