mlrun 1.7.0rc13__py3-none-any.whl → 1.7.0rc15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mlrun might be problematic. Click here for more details.

Files changed (85) hide show
  1. mlrun/__main__.py +0 -105
  2. mlrun/artifacts/__init__.py +1 -2
  3. mlrun/artifacts/base.py +8 -250
  4. mlrun/artifacts/dataset.py +1 -190
  5. mlrun/artifacts/manager.py +2 -41
  6. mlrun/artifacts/model.py +1 -140
  7. mlrun/artifacts/plots.py +1 -375
  8. mlrun/common/schemas/model_monitoring/__init__.py +4 -0
  9. mlrun/common/schemas/model_monitoring/constants.py +24 -3
  10. mlrun/common/schemas/model_monitoring/model_endpoints.py +13 -1
  11. mlrun/common/schemas/project.py +1 -0
  12. mlrun/config.py +14 -4
  13. mlrun/data_types/to_pandas.py +4 -4
  14. mlrun/datastore/base.py +41 -9
  15. mlrun/datastore/datastore_profile.py +50 -3
  16. mlrun/datastore/hdfs.py +5 -0
  17. mlrun/datastore/inmem.py +2 -2
  18. mlrun/datastore/sources.py +43 -2
  19. mlrun/datastore/store_resources.py +2 -6
  20. mlrun/datastore/targets.py +125 -6
  21. mlrun/datastore/v3io.py +1 -1
  22. mlrun/db/auth_utils.py +152 -0
  23. mlrun/db/base.py +1 -1
  24. mlrun/db/httpdb.py +69 -33
  25. mlrun/feature_store/__init__.py +0 -2
  26. mlrun/feature_store/api.py +12 -47
  27. mlrun/feature_store/feature_set.py +9 -0
  28. mlrun/feature_store/retrieval/base.py +9 -4
  29. mlrun/feature_store/retrieval/conversion.py +4 -4
  30. mlrun/feature_store/retrieval/dask_merger.py +2 -0
  31. mlrun/feature_store/retrieval/job.py +2 -0
  32. mlrun/feature_store/retrieval/local_merger.py +2 -0
  33. mlrun/feature_store/retrieval/spark_merger.py +5 -0
  34. mlrun/frameworks/_dl_common/loggers/tensorboard_logger.py +5 -10
  35. mlrun/kfpops.py +5 -10
  36. mlrun/launcher/base.py +1 -1
  37. mlrun/launcher/client.py +1 -1
  38. mlrun/lists.py +2 -2
  39. mlrun/model.py +36 -9
  40. mlrun/model_monitoring/api.py +41 -18
  41. mlrun/model_monitoring/application.py +5 -305
  42. mlrun/model_monitoring/applications/__init__.py +11 -0
  43. mlrun/model_monitoring/applications/_application_steps.py +158 -0
  44. mlrun/model_monitoring/applications/base.py +282 -0
  45. mlrun/model_monitoring/applications/context.py +214 -0
  46. mlrun/model_monitoring/applications/evidently_base.py +211 -0
  47. mlrun/model_monitoring/applications/histogram_data_drift.py +92 -77
  48. mlrun/model_monitoring/applications/results.py +99 -0
  49. mlrun/model_monitoring/controller.py +3 -1
  50. mlrun/model_monitoring/db/stores/sqldb/models/base.py +7 -6
  51. mlrun/model_monitoring/db/stores/sqldb/sql_store.py +1 -1
  52. mlrun/model_monitoring/db/stores/v3io_kv/kv_store.py +67 -4
  53. mlrun/model_monitoring/evidently_application.py +6 -118
  54. mlrun/model_monitoring/helpers.py +1 -1
  55. mlrun/model_monitoring/model_endpoint.py +3 -2
  56. mlrun/model_monitoring/stream_processing.py +2 -3
  57. mlrun/model_monitoring/writer.py +69 -39
  58. mlrun/platforms/iguazio.py +2 -2
  59. mlrun/projects/pipelines.py +24 -7
  60. mlrun/projects/project.py +130 -65
  61. mlrun/render.py +2 -10
  62. mlrun/run.py +1 -4
  63. mlrun/runtimes/__init__.py +3 -3
  64. mlrun/runtimes/base.py +3 -3
  65. mlrun/runtimes/funcdoc.py +0 -28
  66. mlrun/runtimes/local.py +1 -1
  67. mlrun/runtimes/mpijob/__init__.py +0 -20
  68. mlrun/runtimes/mpijob/v1.py +1 -1
  69. mlrun/runtimes/nuclio/api_gateway.py +275 -153
  70. mlrun/runtimes/nuclio/function.py +1 -1
  71. mlrun/runtimes/pod.py +5 -5
  72. mlrun/runtimes/utils.py +1 -1
  73. mlrun/serving/states.py +53 -2
  74. mlrun/utils/helpers.py +27 -40
  75. mlrun/utils/notifications/notification/slack.py +31 -8
  76. mlrun/utils/notifications/notification_pusher.py +133 -14
  77. mlrun/utils/version/version.json +2 -2
  78. {mlrun-1.7.0rc13.dist-info → mlrun-1.7.0rc15.dist-info}/METADATA +2 -2
  79. {mlrun-1.7.0rc13.dist-info → mlrun-1.7.0rc15.dist-info}/RECORD +84 -79
  80. mlrun/runtimes/mpijob/v1alpha1.py +0 -29
  81. /mlrun/{runtimes → common/runtimes}/constants.py +0 -0
  82. {mlrun-1.7.0rc13.dist-info → mlrun-1.7.0rc15.dist-info}/LICENSE +0 -0
  83. {mlrun-1.7.0rc13.dist-info → mlrun-1.7.0rc15.dist-info}/WHEEL +0 -0
  84. {mlrun-1.7.0rc13.dist-info → mlrun-1.7.0rc15.dist-info}/entry_points.txt +0 -0
  85. {mlrun-1.7.0rc13.dist-info → mlrun-1.7.0rc15.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,282 @@
1
+ # Copyright 2023 Iguazio
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from abc import ABC, abstractmethod
16
+ from typing import Any, Union, cast
17
+
18
+ import numpy as np
19
+ import pandas as pd
20
+
21
+ import mlrun
22
+ import mlrun.model_monitoring.applications.context as mm_context
23
+ import mlrun.model_monitoring.applications.results as mm_results
24
+ from mlrun.serving.utils import StepToDict
25
+
26
+
27
+ class ModelMonitoringApplicationBaseV2(StepToDict, ABC):
28
+ """
29
+ A base class for a model monitoring application.
30
+ Inherit from this class to create a custom model monitoring application.
31
+
32
+ example for very simple custom application::
33
+ # mlrun: start-code
34
+ class MyApp(ApplicationBase):
35
+ def do_tracking(
36
+ self,
37
+ monitoring_context: mm_context.MonitoringApplicationContext,
38
+ ) -> ModelMonitoringApplicationResult:
39
+ monitoring_context.log_artifact(
40
+ TableArtifact(
41
+ "sample_df_stats", df=self.dict_to_histogram(sample_df_stats)
42
+ )
43
+ )
44
+ return ModelMonitoringApplicationResult(
45
+ name="data_drift_test",
46
+ value=0.5,
47
+ kind=mm_constant.ResultKindApp.data_drift,
48
+ status=mm_constant.ResultStatusApp.detected,
49
+ )
50
+
51
+
52
+ # mlrun: end-code
53
+ """
54
+
55
+ kind = "monitoring_application"
56
+
57
+ def do(
58
+ self, monitoring_context: mm_context.MonitoringApplicationContext
59
+ ) -> tuple[
60
+ list[
61
+ Union[
62
+ mm_results.ModelMonitoringApplicationResult,
63
+ mm_results.ModelMonitoringApplicationMetric,
64
+ ]
65
+ ],
66
+ mm_context.MonitoringApplicationContext,
67
+ ]:
68
+ """
69
+ Process the monitoring event and return application results & metrics.
70
+
71
+ :param monitoring_context: (MonitoringApplicationContext) The monitoring application context.
72
+ :returns: A tuple of:
73
+ [0] = list of application results that can be either from type
74
+ `ModelMonitoringApplicationResult`
75
+ or from type `ModelMonitoringApplicationResult`.
76
+ [1] = the original application event, wrapped in `MonitoringApplicationContext`
77
+ object
78
+ """
79
+ results = self.do_tracking(monitoring_context=monitoring_context)
80
+ if isinstance(results, dict):
81
+ results = [
82
+ mm_results.ModelMonitoringApplicationMetric(name=key, value=value)
83
+ for key, value in results.items()
84
+ ]
85
+ results = results if isinstance(results, list) else [results]
86
+ return results, monitoring_context
87
+
88
+ @abstractmethod
89
+ def do_tracking(
90
+ self,
91
+ monitoring_context: mm_context.MonitoringApplicationContext,
92
+ ) -> Union[
93
+ mm_results.ModelMonitoringApplicationResult,
94
+ list[
95
+ Union[
96
+ mm_results.ModelMonitoringApplicationResult,
97
+ mm_results.ModelMonitoringApplicationMetric,
98
+ ]
99
+ ],
100
+ dict[str, Any],
101
+ ]:
102
+ """
103
+ Implement this method with your custom monitoring logic.
104
+
105
+ :param monitoring_context: (MonitoringApplicationContext) The monitoring context to process.
106
+
107
+ :returns: (ModelMonitoringApplicationResult) or
108
+ (list[Union[ModelMonitoringApplicationResult,
109
+ ModelMonitoringApplicationMetric]])
110
+ or dict that contains the application metrics only (in this case the name of
111
+ each metric name is the key and the metric value is the corresponding value).
112
+ """
113
+ raise NotImplementedError
114
+
115
+
116
+ class ModelMonitoringApplicationBase(StepToDict, ABC):
117
+ """
118
+ A base class for a model monitoring application.
119
+ Inherit from this class to create a custom model monitoring application.
120
+
121
+ example for very simple custom application::
122
+ # mlrun: start-code
123
+ class MyApp(ApplicationBase):
124
+ def do_tracking(
125
+ self,
126
+ sample_df_stats: mlrun.common.model_monitoring.helpers.FeatureStats,
127
+ feature_stats: mlrun.common.model_monitoring.helpers.FeatureStats,
128
+ start_infer_time: pd.Timestamp,
129
+ end_infer_time: pd.Timestamp,
130
+ schedule_time: pd.Timestamp,
131
+ latest_request: pd.Timestamp,
132
+ endpoint_id: str,
133
+ output_stream_uri: str,
134
+ ) -> ModelMonitoringApplicationResult:
135
+ self.context.log_artifact(
136
+ TableArtifact(
137
+ "sample_df_stats", df=self.dict_to_histogram(sample_df_stats)
138
+ )
139
+ )
140
+ return ModelMonitoringApplicationResult(
141
+ name="data_drift_test",
142
+ value=0.5,
143
+ kind=mm_constant.ResultKindApp.data_drift,
144
+ status=mm_constant.ResultStatusApp.detected,
145
+ )
146
+
147
+
148
+ # mlrun: end-code
149
+ """
150
+
151
+ kind = "monitoring_application"
152
+
153
+ def do(
154
+ self, monitoring_context: mm_context.MonitoringApplicationContext
155
+ ) -> tuple[
156
+ list[mm_results.ModelMonitoringApplicationResult],
157
+ mm_context.MonitoringApplicationContext,
158
+ ]:
159
+ """
160
+ Process the monitoring event and return application results.
161
+
162
+ :param monitoring_context: (MonitoringApplicationContext) The monitoring context to process.
163
+ :returns: A tuple of:
164
+ [0] = list of application results that can be either from type
165
+ `ModelMonitoringApplicationResult` or from type
166
+ `ModelMonitoringApplicationResult`.
167
+ [1] = the original application event, wrapped in `MonitoringApplicationContext`
168
+ object
169
+ """
170
+ resolved_event = self._resolve_event(monitoring_context)
171
+ if not (
172
+ hasattr(self, "context") and isinstance(self.context, mlrun.MLClientCtx)
173
+ ):
174
+ self._lazy_init(monitoring_context)
175
+ results = self.do_tracking(*resolved_event)
176
+ results = results if isinstance(results, list) else [results]
177
+ return results, monitoring_context
178
+
179
+ def _lazy_init(self, monitoring_context: mm_context.MonitoringApplicationContext):
180
+ self.context = cast(mlrun.MLClientCtx, monitoring_context)
181
+
182
+ @abstractmethod
183
+ def do_tracking(
184
+ self,
185
+ application_name: str,
186
+ sample_df_stats: pd.DataFrame,
187
+ feature_stats: pd.DataFrame,
188
+ sample_df: pd.DataFrame,
189
+ start_infer_time: pd.Timestamp,
190
+ end_infer_time: pd.Timestamp,
191
+ latest_request: pd.Timestamp,
192
+ endpoint_id: str,
193
+ output_stream_uri: str,
194
+ ) -> Union[
195
+ mm_results.ModelMonitoringApplicationResult,
196
+ list[mm_results.ModelMonitoringApplicationResult],
197
+ ]:
198
+ """
199
+ Implement this method with your custom monitoring logic.
200
+
201
+ :param application_name: (str) the app name
202
+ :param sample_df_stats: (pd.DataFrame) The new sample distribution.
203
+ :param feature_stats: (pd.DataFrame) The train sample distribution.
204
+ :param sample_df: (pd.DataFrame) The new sample DataFrame.
205
+ :param start_infer_time: (pd.Timestamp) Start time of the monitoring schedule.
206
+ :param end_infer_time: (pd.Timestamp) End time of the monitoring schedule.
207
+ :param latest_request: (pd.Timestamp) Timestamp of the latest request on this endpoint_id.
208
+ :param endpoint_id: (str) ID of the monitored model endpoint
209
+ :param output_stream_uri: (str) URI of the output stream for results
210
+
211
+ :returns: (ModelMonitoringApplicationResult) or
212
+ (list[ModelMonitoringApplicationResult]) of the application results.
213
+ """
214
+ raise NotImplementedError
215
+
216
+ @classmethod
217
+ def _resolve_event(
218
+ cls,
219
+ monitoring_context: mm_context.MonitoringApplicationContext,
220
+ ) -> tuple[
221
+ str,
222
+ pd.DataFrame,
223
+ pd.DataFrame,
224
+ pd.DataFrame,
225
+ pd.Timestamp,
226
+ pd.Timestamp,
227
+ pd.Timestamp,
228
+ str,
229
+ str,
230
+ ]:
231
+ """
232
+ Converting the event into a single tuple that will be used for passing the event arguments to the running
233
+ application
234
+
235
+ :param monitoring_context: (MonitoringApplicationContext) The monitoring context to process.
236
+
237
+ :return: A tuple of:
238
+ [0] = (str) application name
239
+ [1] = (pd.DataFrame) current input statistics
240
+ [2] = (pd.DataFrame) train statistics
241
+ [3] = (pd.DataFrame) current input data
242
+ [4] = (pd.Timestamp) start time of the monitoring schedule
243
+ [5] = (pd.Timestamp) end time of the monitoring schedule
244
+ [6] = (pd.Timestamp) timestamp of the latest request
245
+ [7] = (str) endpoint id
246
+ [8] = (str) output stream uri
247
+ """
248
+ return (
249
+ monitoring_context.application_name,
250
+ cls.dict_to_histogram(monitoring_context.sample_df_stats),
251
+ cls.dict_to_histogram(monitoring_context.feature_stats),
252
+ monitoring_context.sample_df,
253
+ monitoring_context.start_infer_time,
254
+ monitoring_context.end_infer_time,
255
+ monitoring_context.latest_request,
256
+ monitoring_context.endpoint_id,
257
+ monitoring_context.output_stream_uri,
258
+ )
259
+
260
+ @staticmethod
261
+ def dict_to_histogram(
262
+ histogram_dict: mlrun.common.model_monitoring.helpers.FeatureStats,
263
+ ) -> pd.DataFrame:
264
+ """
265
+ Convert histogram dictionary to pandas DataFrame with feature histograms as columns
266
+
267
+ :param histogram_dict: Histogram dictionary
268
+
269
+ :returns: Histogram dataframe
270
+ """
271
+
272
+ # Create a dictionary with feature histograms as values
273
+ histograms = {}
274
+ for feature, stats in histogram_dict.items():
275
+ if "hist" in stats:
276
+ # Normalize to probability distribution of each feature
277
+ histograms[feature] = np.array(stats["hist"][0]) / stats["count"]
278
+
279
+ # Convert the dictionary to pandas DataFrame
280
+ histograms = pd.DataFrame(histograms)
281
+
282
+ return histograms
@@ -0,0 +1,214 @@
1
+ # Copyright 2024 Iguazio
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ import json
15
+ import typing
16
+
17
+ import numpy as np
18
+ import pandas as pd
19
+
20
+ import mlrun.common.helpers
21
+ import mlrun.common.model_monitoring.helpers
22
+ import mlrun.common.schemas.model_monitoring.constants as mm_constants
23
+ import mlrun.feature_store as fstore
24
+ from mlrun.artifacts.model import ModelArtifact, get_model
25
+ from mlrun.common.model_monitoring.helpers import FeatureStats, pad_features_hist
26
+ from mlrun.execution import MLClientCtx
27
+ from mlrun.model_monitoring.helpers import (
28
+ calculate_inputs_statistics,
29
+ get_endpoint_record,
30
+ )
31
+ from mlrun.model_monitoring.model_endpoint import ModelEndpoint
32
+
33
+
34
+ class MonitoringApplicationContext(MLClientCtx):
35
+ """
36
+ The monitoring context holds all the relevant information for the monitoring application,
37
+ and also it can be used for logging artifacts and results.
38
+ The monitoring context has the following attributes:
39
+
40
+ :param application_name: (str) the app name
41
+ :param sample_df_stats: (FeatureStats) The new sample distribution dictionary.
42
+ :param feature_stats: (FeatureStats) The train sample distribution dictionary.
43
+ :param sample_df: (pd.DataFrame) The new sample DataFrame.
44
+ :param start_infer_time: (pd.Timestamp) Start time of the monitoring schedule.
45
+ :param end_infer_time: (pd.Timestamp) End time of the monitoring schedule.
46
+ :param latest_request: (pd.Timestamp) Timestamp of the latest request on this endpoint_id.
47
+ :param endpoint_id: (str) ID of the monitored model endpoint
48
+ :param output_stream_uri: (str) URI of the output stream for results
49
+ :param model_endpoint: (ModelEndpoint) The model endpoint object.
50
+ :param feature_names: (list[str]) List of models feature names.
51
+ :param label_names: (list[str]) List of models label names.
52
+ :param model: (tuple[str, ModelArtifact, dict]) The model file, model spec object, and list of
53
+
54
+ """
55
+
56
+ def __init__(self, **kwargs):
57
+ super().__init__(**kwargs)
58
+
59
+ def __post_init__(self):
60
+ self.application_name: typing.Optional[str] = None
61
+ self.start_infer_time: typing.Optional[pd.Timestamp] = None
62
+ self.end_infer_time: typing.Optional[pd.Timestamp] = None
63
+ self.latest_request: typing.Optional[pd.Timestamp] = None
64
+ self.endpoint_id: typing.Optional[str] = None
65
+ self.output_stream_uri: typing.Optional[str] = None
66
+
67
+ self._sample_df: typing.Optional[pd.DataFrame] = None
68
+ self._model_endpoint: typing.Optional[ModelEndpoint] = None
69
+ self._feature_stats: typing.Optional[FeatureStats] = None
70
+ self._sample_df_stats: typing.Optional[FeatureStats] = None
71
+
72
+ @classmethod
73
+ def from_dict(
74
+ cls,
75
+ attrs: dict,
76
+ context=None,
77
+ model_endpoint_dict=None,
78
+ **kwargs,
79
+ ) -> "MonitoringApplicationContext":
80
+ """
81
+ Create an instance of the MonitoringApplicationContext from a dictionary.
82
+
83
+ :param attrs: The instance data dictionary.
84
+ :param context: The current application context.
85
+ :param model_endpoint_dict: Dictionary of model endpoints.
86
+
87
+ """
88
+
89
+ if not context:
90
+ self = (
91
+ super().from_dict(
92
+ attrs=attrs.get(mm_constants.ApplicationEvent.MLRUN_CONTEXT, {}),
93
+ **kwargs,
94
+ ),
95
+ )
96
+ else:
97
+ self = context
98
+ self.__post_init__()
99
+
100
+ self.start_infer_time = pd.Timestamp(
101
+ attrs.get(mm_constants.ApplicationEvent.START_INFER_TIME)
102
+ )
103
+ self.end_infer_time = pd.Timestamp(
104
+ attrs.get(mm_constants.ApplicationEvent.END_INFER_TIME)
105
+ )
106
+ self.latest_request = pd.Timestamp(
107
+ attrs.get(mm_constants.ApplicationEvent.LAST_REQUEST)
108
+ )
109
+ self.application_name = attrs.get(
110
+ mm_constants.ApplicationEvent.APPLICATION_NAME
111
+ )
112
+ self._feature_stats = json.loads(
113
+ attrs.get(mm_constants.ApplicationEvent.FEATURE_STATS, "{}")
114
+ )
115
+ self._sample_df_stats = json.loads(
116
+ attrs.get(mm_constants.ApplicationEvent.FEATURE_STATS, "{}")
117
+ )
118
+
119
+ self.endpoint_id = attrs.get(mm_constants.ApplicationEvent.ENDPOINT_ID)
120
+ self._model_endpoint = model_endpoint_dict.get(self.endpoint_id)
121
+
122
+ return self
123
+
124
+ @property
125
+ def sample_df(self) -> pd.DataFrame:
126
+ if not hasattr(self, "_sample_df") or self._sample_df is None:
127
+ feature_set = fstore.get_feature_set(
128
+ self.model_endpoint.status.monitoring_feature_set_uri
129
+ )
130
+ features = [f"{feature_set.metadata.name}.*"]
131
+ vector = fstore.FeatureVector(
132
+ name=f"{self.endpoint_id}_vector",
133
+ features=features,
134
+ with_indexes=True,
135
+ )
136
+ vector.metadata.tag = self.application_name
137
+ vector.feature_set_objects = {feature_set.metadata.name: feature_set}
138
+
139
+ offline_response = vector.get_offline_features(
140
+ start_time=self.start_infer_time,
141
+ end_time=self.end_infer_time,
142
+ timestamp_for_filtering=mm_constants.FeatureSetFeatures.time_stamp(),
143
+ )
144
+ self._sample_df = offline_response.to_dataframe().reset_index(drop=True)
145
+ return self._sample_df
146
+
147
+ @property
148
+ def model_endpoint(self) -> ModelEndpoint:
149
+ if not hasattr(self, "_model_endpoint") or not self._model_endpoint:
150
+ self._model_endpoint = ModelEndpoint.from_flat_dict(
151
+ get_endpoint_record(self.project, self.endpoint_id)
152
+ )
153
+ return self._model_endpoint
154
+
155
+ @property
156
+ def feature_stats(self) -> FeatureStats:
157
+ if not hasattr(self, "_feature_stats") or not self._feature_stats:
158
+ self._feature_stats = json.loads(self.model_endpoint.status.feature_stats)
159
+ pad_features_hist(self._feature_stats)
160
+ return self._feature_stats
161
+
162
+ @property
163
+ def sample_df_stats(self) -> FeatureStats:
164
+ """statistics of the sample dataframe"""
165
+ if not hasattr(self, "_sample_df_stats") or not self._sample_df_stats:
166
+ self._sample_df_stats = calculate_inputs_statistics(
167
+ self.feature_stats, self.sample_df
168
+ )
169
+ return self._sample_df_stats
170
+
171
+ @property
172
+ def feature_names(self) -> list[str]:
173
+ """The feature names of the model"""
174
+ feature_names = self.model_endpoint.spec.feature_names
175
+ return (
176
+ feature_names
177
+ if isinstance(feature_names, list)
178
+ else json.loads(feature_names)
179
+ )
180
+
181
+ @property
182
+ def label_names(self) -> list[str]:
183
+ """The label names of the model"""
184
+ label_names = self.model_endpoint.spec.label_names
185
+ return label_names if isinstance(label_names, list) else json.loads(label_names)
186
+
187
+ @property
188
+ def model(self) -> tuple[str, ModelArtifact, dict]:
189
+ """return model file, model spec object, and list of extra data items"""
190
+ return get_model(self.model_endpoint.spec.model_uri)
191
+
192
+ @staticmethod
193
+ def dict_to_histogram(
194
+ histogram_dict: mlrun.common.model_monitoring.helpers.FeatureStats,
195
+ ) -> pd.DataFrame:
196
+ """
197
+ Convert histogram dictionary to pandas DataFrame with feature histograms as columns
198
+
199
+ :param histogram_dict: Histogram dictionary
200
+
201
+ :returns: Histogram dataframe
202
+ """
203
+
204
+ # Create a dictionary with feature histograms as values
205
+ histograms = {}
206
+ for feature, stats in histogram_dict.items():
207
+ if "hist" in stats:
208
+ # Normalize to probability distribution of each feature
209
+ histograms[feature] = np.array(stats["hist"][0]) / stats["count"]
210
+
211
+ # Convert the dictionary to pandas DataFrame
212
+ histograms = pd.DataFrame(histograms)
213
+
214
+ return histograms