github-actions[bot] commited on
Commit
c0ba94e
·
1 Parent(s): 9791c30

Sync from main@8265c31

Browse files
Dockerfile CHANGED
@@ -28,7 +28,8 @@ RUN uv sync --locked --no-install-project
28
  # copy the rest of the files needed for inference
29
  COPY --chown=user . .
30
 
31
- RUN chmod +x predicting_outcomes_in_heart_failure/app/entrypoint.sh
 
32
 
33
  EXPOSE 7860
34
 
 
28
  # copy the rest of the files needed for inference
29
  COPY --chown=user . .
30
 
31
+ RUN sed -i 's/\r$//' predicting_outcomes_in_heart_failure/app/entrypoint.sh \
32
+ && chmod +x predicting_outcomes_in_heart_failure/app/entrypoint.sh
33
 
34
  EXPOSE 7860
35
 
README.md CHANGED
@@ -6,126 +6,256 @@ colorTo: gray
6
  sdk: docker
7
  app_port: 7860
8
  ---
 
9
  # Predicting Outcomes in Heart Failure
 
 
 
 
 
 
 
 
 
10
 
11
  ## Table of Contents
12
- 1. [Project Overview](#project-overview)
13
- 2. [Project Organization](#project-organization)
14
- 3. [DVC Pipeline Defined](#dvc-pipeline-defined)
15
- 4. [Milestones Summary](#milestones-summary)
 
16
  - [Milestone 1 - Inception](#milestone-1---inception)
17
  - [Milestone 2 - Reproducibility](#milestone-2---reproducibility)
18
  - [Milestone 3 - Quality Assurance](#milestone-3---quality-assurance)
19
  - [Milestone 4 - API Integration](#milestone-4---API-Integration)
20
  - [Milestone 5 - Deployment](#milestone-5---Deployment)
 
21
 
22
- ## Project Overview
23
- <a target="_blank" href="https://cookiecutter-data-science.drivendata.org/">
24
- <img src="https://img.shields.io/badge/CCDS-Project%20template-328F97?logo=cookiecutter" />
25
- </a>
26
 
27
- [![Ruff Linter](https://github.com/se4ai2526-uniba/CardioTrack/actions/workflows/ruff-linter.yml/badge.svg)](https://github.com/se4ai2526-uniba/CardioTrack/actions/workflows/ruff-linter.yml)
28
- [![PyNBLint](https://github.com/se4ai2526-uniba/CardioTrack/actions/workflows/pynblint.yml/badge.svg)](https://github.com/se4ai2526-uniba/CardioTrack/actions/workflows/pynblint.yml)
29
- [![Pytest & Great Expectations](https://github.com/se4ai2526-uniba/CardioTrack/actions/workflows/pytestAndGX.yml/badge.svg)](https://github.com/se4ai2526-uniba/CardioTrack/actions/workflows/pytestAndGX.yml)
30
- [![Deploy](https://github.com/se4ai2526-uniba/CardioTrack/actions/workflows/deploy.yml/badge.svg)](https://github.com/se4ai2526-uniba/CardioTrack/actions/workflows/deploy.yml)
31
- [![Python](https://img.shields.io/badge/Python-3.11-blue)](https://www.python.org/)
32
 
 
33
 
34
- This project develops a predictive pipeline for patient outcome prediction in heart failure, using a publicly available dataset of clinical records. The goal is to design and evaluate machine learning models within a reproducible workflow that can be integrated into larger systems for clinical decision support. The workflow addresses data heterogeneity, defines consistent preprocessing and feature engineering strategies, and explores alternative modeling approaches with systematic evaluation using clinically relevant metrics. It also emphasizes model transparency and auditability, ensuring that the resulting pipeline can be deployed as a reliable, adaptable software component in healthcare applications. The project aims not only to improve baseline predictive performance but also to demonstrate how data-driven models can be effectively integrated into end-to-end AI-enabled healthcare systems.
 
35
 
36
- ## Project Organization
 
 
 
37
 
 
 
 
 
 
38
  ```
39
- ├── LICENSE <- Open-source license if one is chosen
40
- ├── Makefile <- Makefile with convenience commands like `make data` or `make train`
41
- ├── README.md <- The top-level README for developers using this project.
42
- ├── data
43
- │ ├── external <- Data from third party sources.
44
- │ ├── interim <- Intermediate data that has been transformed.
45
- │ ├── processed <- The final, canonical data sets for modeling.
46
- │ └── raw <- The original, immutable data dump.
47
-
48
- ├── docs <- A default mkdocs project; see www.mkdocs.org for details
49
-
50
- ├── models <- Trained and serialized models, model predictions, or model summaries
51
-
52
- ├── notebooks <- Jupyter notebooks. Naming convention is a number (for ordering),
53
- │ the creator's initials, and a short `-` delimited description, e.g.
54
- │ `1.0-jqp-initial-data-exploration`.
55
-
56
- ├── pyproject.toml <- Project configuration file with package metadata for
57
- │ predicting_outcomes_in_heart_failure and configuration for tools like black
58
-
59
- ├── references <- Data dictionaries, manuals, and all other explanatory materials.
60
-
61
- ├── reports <- Generated analysis as HTML, PDF, LaTeX, etc.
62
- │ └── figures <- Generated graphics and figures to be used in reporting
63
-
64
- ├── requirements.txt <- The requirements file for reproducing the analysis environment, e.g.
65
- │ generated with `pip freeze > requirements.txt`
66
-
67
- ├── setup.cfg <- Configuration file for flake8
68
-
69
- └── predicting_outcomes_in_heart_failure <- Source code for use in this project.
70
-
71
- ├── __init__.py <- Makes predicting_outcomes_in_heart_failure a Python module
72
-
73
- ├── config.py <- Store useful variables and configuration
74
-
75
- ├── data
76
- │ ├── __init__.py
77
- │ ├── dataset.py <- Scripts to download or generate data
78
- | ├── preprocess.py <- Data preprocessing code
79
- │ └── split_data.py <- Split dataset into train and test code
80
-
81
- ├── features.py <- Code to create features for modeling
82
-
83
- ├── modeling
84
- │ ├── __init__.py
85
- │ ├── predict.py <- Code to run model inference with trained models
86
- │ └── train.py <- Code to train models
87
-
88
- └── plots.py <- Code to create visualizations
89
  ```
90
 
91
- ## DVC Pipeline defined
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  ```
93
- +---------------+
94
- | download_data |
95
- +---------------+
96
- *
97
- *
98
- *
99
- +---------------+
100
- | preprocessing |
101
- +---------------+
102
- *
103
- *
104
- *
105
- +------------+
106
- | split_data |
107
- +------------+
108
- *** ***
109
- * *
110
- ** ***
111
- +----------+ *
112
- | training | ***
113
- +----------+ *
114
- *** ***
115
- * *
116
- ** **
117
- +------------+
118
- | evaluation |
119
- +------------+
120
  ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
- ## Milestones Summary
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
  ### Milestone 1 - Inception
125
  During this milestone, the **CCDS Project Template** was used as the foundation for organizing the project.
126
  The main conceptual and structural components of the system were defined, following the template guidelines to ensure consistency and traceability.
127
 
128
- Additionally, a **Machine Learning Canvas** has been added in the [`docs/`](./docs) folder.
129
  It outlines the model objectives, the data to be used, and the key methodological aspects planned for the next phases of the project.
130
 
131
  ### Milestone 2 - Reproducibility
@@ -141,7 +271,7 @@ We initialized **DVC** and configured a full pipeline to automate the main steps
141
  - **Data splitting**
142
  - **Training** and **evaluation**
143
 
144
- The pipeline is fully reproducible and version-controlled through DVC.
145
 
146
  #### Model Training and Experiment Tracking
147
  We implemented the **training scripts** and integrated **MLflow** for experiment tracking.
@@ -150,7 +280,7 @@ Three models are trained and evaluated within this workflow:
150
  - Random Forest
151
  - Logistic Regression
152
 
153
- Each experiment is logged to MLflow.
154
 
155
  #### Model Registry and Thresholds
156
  Models that reach or exceed the predefined **performance thresholds** (as defined in the ML Canvas) are automatically **saved to the model registry**.
@@ -172,10 +302,18 @@ These validations help to:
172
 
173
  - detect anomalies or invalid values at the data source
174
  - prevent the propagation of data issues into downstream processes
 
175
 
176
- #### Code Quality
 
 
177
  We added automated **unit and integration tests** using **pytest**, covering the main modules and functionalities of the system.
 
 
 
 
178
 
 
179
 
180
  #### ML Pipeline Enhancements
181
  we applied the following enhancements to the ML pipeline:
@@ -197,7 +335,7 @@ We applied an explainability module:
197
 
198
  #### Risk Classification
199
  We added a **Risk Classification** analysis for the system in accordance with **IMDRF** and **AI Act** regulations.
200
- The documentation is available in the [`docs/`](./docs) folder.
201
 
202
 
203
  ### Milestone 4 - API Integration
@@ -271,8 +409,48 @@ The overall codebase quality was improved through automated linting and formatti
271
  - Introduction of *pytest* for automated testing.
272
  - Integration of *Great Expectations* for automated data quality checks.
273
 
 
 
 
 
 
 
 
 
 
274
 
275
  #### Continuos Deployment
276
  Automated deployment to *Hugging Face* was implemented through Github Actions workflow
277
  *Hugging Face Space*: [Check Here](https://huggingface.co/spaces/CardioTrack/CardioTrack)
278
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  sdk: docker
7
  app_port: 7860
8
  ---
9
+
10
  # Predicting Outcomes in Heart Failure
11
+ <a target="_blank" href="https://cookiecutter-data-science.drivendata.org/"><img src="https://img.shields.io/badge/CCDS-Project%20template-328F97?logo=cookiecutter" /></a>
12
+ [![Python](https://img.shields.io/badge/Python-3.11-blue)](https://www.python.org/)
13
+ [![HuggingFace](https://img.shields.io/badge/Hugging_Face-Space-yellow)](https://huggingface.co/spaces/CardioTrack/CardioTrack)
14
+ [![Better Stack Badge](https://uptime.betterstack.com/status-badges/v1/monitor/2cov5.svg)](https://uptime.betterstack.com/?utm_source=status_badge)
15
+
16
+ [![Ruff Linter](https://github.com/se4ai2526-uniba/CardioTrack/actions/workflows/ruff-linter.yml/badge.svg)](https://github.com/se4ai2526-uniba/CardioTrack/actions/workflows/ruff-linter.yml)
17
+ [![PyNBLint](https://github.com/se4ai2526-uniba/CardioTrack/actions/workflows/pynblint.yml/badge.svg)](https://github.com/se4ai2526-uniba/CardioTrack/actions/workflows/pynblint.yml)
18
+ [![Pytest & Great Expectations](https://github.com/se4ai2526-uniba/CardioTrack/actions/workflows/pytestAndGX.yml/badge.svg)](https://github.com/se4ai2526-uniba/CardioTrack/actions/workflows/pytestAndGX.yml)
19
+ [![Deploy](https://github.com/se4ai2526-uniba/CardioTrack/actions/workflows/deploy.yml/badge.svg)](https://github.com/se4ai2526-uniba/CardioTrack/actions/workflows/deploy.yml)
20
 
21
  ## Table of Contents
22
+ 1. [Project Summary](#project-summary)
23
+ 2. [Quick Start Guide](#quick-start-guide)
24
+ 3. [Project Organization](#project-organization)
25
+ 4. [CardioTrack Architecture](#cardiotrack-architecture)
26
+ 5. [Milestones Description](#milestones-description)
27
  - [Milestone 1 - Inception](#milestone-1---inception)
28
  - [Milestone 2 - Reproducibility](#milestone-2---reproducibility)
29
  - [Milestone 3 - Quality Assurance](#milestone-3---quality-assurance)
30
  - [Milestone 4 - API Integration](#milestone-4---API-Integration)
31
  - [Milestone 5 - Deployment](#milestone-5---Deployment)
32
+ - [Milestone 6 - Monitoring](#milestone-6---Monitoring)
33
 
34
+ ## Project Summary
 
 
 
35
 
36
+ This project develops a complete, reproducible pipeline for predicting patient outcomes in heart failure, leveraging a publicly available clinical dataset. It addresses the challenges of heterogeneous data and ensures consistent preprocessing, model training, and evaluation, with a strong focus on transparency, reliability, and clinical relevance. The system provides explainable predictions and risk classifications, making it both interpretable and trustworthy. A user-friendly interface allows easy interaction with the models, and the entire pipeline is deployed on a publicly accessible [Hugging Face Space](https://huggingface.co/spaces/CardioTrack/CardioTrack). Below, an example of interaction with the system is shown:
 
 
 
 
37
 
38
+ ![Hf Space Overview](reports/figures/hf_space_overview.gif)
39
 
40
+ ## Quick Start Guide
41
+ ### Prerequisites
42
 
43
+ - **Python 3.11**
44
+ - **uv** - Fast Python package manager ([Official website](https://docs.astral.sh/uv/getting-started/installation/))
45
+ - **DVC** - Data Version Control ([Official website](https://dvc.org/))
46
+ - **Docker** ([Official website](https://www.docker.com/))
47
 
48
+ ### 1. Clone the Repository
49
+
50
+ ```bash
51
+ git clone https://github.com/se4ai2526-uniba/CardioTrack.git
52
+ cd CardioTrack
53
  ```
54
+
55
+ ### 2. Environment Variables
56
+
57
+ Create a `.env` file in the project root with the following variables:
58
+
59
+ ```bash
60
+ RUN_DVC_PULL=1
61
+ AWS_ACCESS_KEY_ID=<your_dagshub_token>
62
+ AWS_SECRET_ACCESS_KEY=<your_dagshub_token>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  ```
64
 
65
+ ### 3. Launch the API Locally
66
+
67
+ #### Docker Compose (Full Stack)
68
+
69
+ This starts the API along with Prometheus, Grafana, and Locust for monitoring:
70
+
71
+ ```bash
72
+ docker-compose up --build
73
+ ```
74
+
75
+ Services available:
76
+
77
+ | Service | URL | Description |
78
+ |---------|-----|-------------|
79
+ | **CardioTrack API** | http://localhost:7860 | Main application with Gradio UI |
80
+ | **Prometheus** | http://localhost:9090 | Metrics collection |
81
+ | **Grafana** | http://localhost:4444 | Metrics dashboard |
82
+ | **Locust** | http://localhost:8089 | Load testing interface |
83
+
84
+ To stop all services:
85
+
86
+ ```bash
87
+ docker-compose down
88
  ```
89
+
90
+ > **Important:** For a more in-depth guide, if you want to modify code see the Developer Guide at [docs/Developer_Guide.md](docs/Developer_Guide.md).
91
+
92
+
93
+ ## Project Organization
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  ```
95
+ ├── Makefile <- Makefile with convenience commands
96
+ ├── README.md <- The top-level README for developers
97
+ ├── pyproject.toml <- Project configuration and dependencies
98
+ ├── uv.lock <- Lock file for uv package manager
99
+ ├── Dockerfile <- Docker container configuration
100
+ ├── docker-compose.yml <- Multi-service stack (API, Prometheus, Grafana, Locust)
101
+ ├── prometheus.yml <- Prometheus scraping configuration
102
+ ├── dvc.yaml <- DVC pipeline configuration
103
+ ├── dvc.lock <- DVC pipeline lock file
104
+ ├── .env.example <- Environment variables template
105
+ ├── .dvc/ <- DVC internal configuration
106
+ ├── .github/workflows/ <- GitHub Actions CI/CD workflows
107
+ │ ├── deploy.yml <- Deployment workflow
108
+ │ ├── pynblint.yml <- Notebook linting workflow
109
+ │ ├── pytestAndGX.yml <- Testing and Great Expectations workflow
110
+ │ └── ruff-linter.yml <- Ruff code linting workflow
111
+ ├── data/
112
+ │ ├── raw/ <- Original, immutable data dump
113
+ │ ├── interim/ <- Intermediate transformed data
114
+ │ │ └── preprocess_artifacts/ <- Preprocessing artifacts (scaler.joblib)
115
+ │ └── processed <- Final datasets for modeling (train/test splits)
116
+ ├── docs/ <- Project documentation
117
+ │ ├── CardioTrack_ML_Canvas.md <- ML project canvas
118
+ │ ├── Developer_Guide.md <- Developer setup guide
119
+ │ └── Risk_Classification.md <- Risk classification methodology
120
+ ├── grafana/
121
+ │ ├── dashboards/ <- Grafana dashboard definitions
122
+ │ └── provisioning/ <- Datasources and dashboard provisioning
123
+ ├── locust/
124
+ │ ├── Dockerfile <- Locust container build
125
+ │ └── locustfile.py <- Load-testing scenarios
126
+ ├── metrics/test <- Model evaluation metrics (JSON)
127
+ ├── models <- Trained models (.joblib files)
128
+ ├── notebooks/ <- Jupyter notebooks for exploration
129
+ ├── references/ <- Data dictionaries and explanatory materials
130
+ ├── reports/
131
+ │ ├── figures/ <- Generated graphics and figures
132
+ │ ├── great_expectations_reports/ <- Data quality validation reports
133
+ │ ├── pytest_report/ <- Pytest HTML test reports
134
+ │ ├── locust_reports/ <- Load testing reports
135
+ │ └── deepchecks_data_drift_reports/ <- Data drift analysis outputs
136
+ ├── predicting_outcomes_in_heart_failure/ <- Source code
137
+ │ ├── __init__.py
138
+ │ ├── config.py <- Configuration variables
139
+ │ ├── app/ <- FastAPI application
140
+ │ │ ├── main.py <- Application entry point
141
+ │ │ ├── monitoring.py <- Prometheus metrics
142
+ │ │ ├── schema.py <- Pydantic schemas
143
+ │ │ ├── utils.py <- Utility functions
144
+ │ │ ├── wrapper.py <- Wrapper class for UI
145
+ │ │ ├── entrypoint.sh <- Container entrypoint script
146
+ │ │ ├── routers/ <- API route handlers
147
+ │ │ │ ├── cards.py <- Cards endpoints
148
+ │ │ │ ├── general.py <- General endpoints
149
+ │ │ │ ├── model_info.py <- Model info endpoints
150
+ │ │ │ └── prediction.py <- Prediction endpoints
151
+ │ │ └── deepchecks_monitoring/ <- Data drift monitoring
152
+ │ │ ├── drift_runner.py <- Drift computation
153
+ │ │ ├── production_data_collector.py <- Production data logging
154
+ │ │ └── scheduler.py <- Scheduled drift jobs
155
+ │ ├── data/ <- Data processing modules
156
+ │ │ ├── dataset.py <- Data download scripts
157
+ │ │ ├── preprocess.py <- Preprocessing code
158
+ │ │ └── split_data.py <- Train/test splitting
159
+ │ └── modeling/ <- Model training and evaluation
160
+ │ ├── train.py <- Training code
161
+ │ ├── predict.py <- Inference code
162
+ │ ├── evaluate.py <- Evaluation metrics
163
+ │ └── explainability.py <- SHAP explainability
164
+ └── tests/ <- Test suite
165
+ ├── test_behavioral_model/ <- Behavioral testing
166
+ │ ├── directional_test.py <- Directional expectations
167
+ │ ├── invariance_test.py <- Model invariance tests
168
+ │ └── minimum_functionality_test.py <- Minimum functionality tests
169
+ ├── test_heart_data/ <- Data validation tests
170
+ │ ├── raw_test.py <- Raw data quality tests
171
+ │ ├── processed_test.py <- Processed data quality tests
172
+ │ └── util.py <- Testing utilities
173
+ └── test_predicting_outcomes_in_heart_failure/ <- Unit tests
174
+ ├── app/ <- API tests
175
+ │ ├── schema_test.py <- Schema validation tests
176
+ │ └── routers/ <- Router tests
177
+ │ ├── model_info_test.py <- Model info tests
178
+ │ └── prediction_test.py <- Prediction tests
179
+ ├── data/ <- Data module tests
180
+ │ ├── preprocess_test.py <- Preprocessing tests
181
+ │ └── split_data_test.py <- Data splitting tests
182
+ └── modeling/ <- Modeling tests
183
+ ├── conftest.py <- Pytest fixtures
184
+ ├── test_train.py <- Training tests
185
+ ├── test_predict.py <- Prediction tests
186
+ ├── test_evaluate.py <- Evaluation tests
187
+ └── test_explainability.py <- Explainability tests
188
+ ```
189
+
190
+ ## CardioTrack Architecture
191
+ ![CardioTrack Architecture](reports/figures/cardiotrack_architecture.png)
192
+
193
+ ### DVC Pipeline Defined
194
+ The project implements a **fully automated ML pipeline** using **DVC (Data Version Control)** to ensure reproducibility and traceability across all stages. The pipeline is structured into five sequential stages, each with a specific responsibility in the machine learning workflow.
195
+
196
+ 1. **download_data**
197
+ Automatically download the raw dataset from Kaggle, eliminating manual download steps and ensuring control of the exact data used.
198
+ 2. **preprocessing**
199
+ Applies data transformations including cleaning invalid values, encoding categorical variables, and standardizing numerical features.
200
+ 3. **split_data**
201
+ Divides the preprocessed data into **training (70%)** and **test (30%)** sets using stratified sampling. Splitting after preprocessing prevents data leakage by ensuring tuning hyperparameters is computed only on training data.
202
+ 4. **training**
203
+ Trains three models (Decision Tree, Random Forest, Logistic Regression) with a **cross-validation strategy** for hyperparameter tuning. **RandomOverSampler** addresses class imbalance.
204
+ 5. **evaluation**
205
+ Assesses model performance on the independent test set, computing F1 Score, Recall, Accuracy, and ROC-AUC.
206
 
207
+ ### Experiments
208
+
209
+ All experiments were tracked using **MLflow** and are available on [DagsHub platform](https://dagshub.com/se4ai2526-uniba/CardioTrack/experiments). For detailed metrics and run comparisons, please refer to the MLflow experiments dashboard.
210
+
211
+ #### Experimental Setup
212
+
213
+ We evaluated three classification algorithms:
214
+
215
+ - **Random Forest**
216
+ - **Decision Tree**
217
+ - **Logistic Regression**
218
+
219
+ ##### Handling Class Imbalance
220
+
221
+ The target variable presented a significant class imbalance. To address this issue, we applied **Random Oversampling** to balance data, ensuring the models could learn effectively from both classes.
222
+
223
+ Additionally, the **"sex" feature showed a severe imbalance** in the dataset. After analyzing the model performance with and without this feature, we found that it provided minimal predictive value while potentially introducing unnecessary gender bias. We also trained the models separately on only males and only females, but the performance was very poor, particularly for females. Consequently, we decided to remove the "sex" feature from the final model to ensure fairness without sacrificing performance.
224
+
225
+ #### Results Summary
226
+
227
+ | Model | Accuracy | F1 Score | Recall | ROC AUC |
228
+ |-------|----------|----------|--------|---------|
229
+ | **Random Forest** | ~0.87 | ~0.89 | ~0.88 | ~0.91 |
230
+ | Decision Tree | ~0.79 | ~0.75 | ~0.77 | ~0.81 |
231
+ | Logistic Regression | ~0.84 | ~0.81 | ~0.82 | ~0.89 |
232
+
233
+ #### Selected Model
234
+
235
+ The model deployed in production is **Random Forest without the "sex" feature**.
236
+
237
+ | Model | Accuracy | F1 Score | Recall | ROC AUC |
238
+ |-------|----------|----------|--------|---------|
239
+ | **Random Forest No Sex** | 0.8877 | 0.8990 | 0.9020 | 0.9400 |
240
+
241
+
242
+ ##### Rationale:
243
+
244
+ 1. **Best overall performance**: Random Forest consistently outperformed Decision Tree and Logistic Regression across all metrics.
245
+
246
+ 2. **Fairness considerations**: Removing the "sex" feature eliminates potential gender bias in predictions. The performance difference between the model with all features and the one without "sex" was negligible (< 1%).
247
+
248
+ 3. **Robustness**: Models trained on gender-specific subsets showed highly imbalanced performance, particularly poor results on the female subset due to data scarcity. The model without the "sex" feature generalizes better across both genders.
249
+
250
+ 4. **Ethical AI practices**: In medical applications, avoiding unnecessary use of sensitive attributes aligns with responsible AI principles and regulatory guidelines.
251
+
252
+ ## Milestones Description
253
 
254
  ### Milestone 1 - Inception
255
  During this milestone, the **CCDS Project Template** was used as the foundation for organizing the project.
256
  The main conceptual and structural components of the system were defined, following the template guidelines to ensure consistency and traceability.
257
 
258
+ Additionally, a **Machine Learning Canvas** has been added. To see it [docs/CardioTrack_ML_Canvas.md](docs/CardioTrack_ML_Canvas.md).
259
  It outlines the model objectives, the data to be used, and the key methodological aspects planned for the next phases of the project.
260
 
261
  ### Milestone 2 - Reproducibility
 
271
  - **Data splitting**
272
  - **Training** and **evaluation**
273
 
274
+ The pipeline is fully reproducible and version-controlled through DVC. Morover, dvc pipeline defined uses `foreach` directive for parallelization across 4 data variants (all, female, male, nosex) and 3 models. All dependencies are automatically tracked ensuring the correct execution order.
275
 
276
  #### Model Training and Experiment Tracking
277
  We implemented the **training scripts** and integrated **MLflow** for experiment tracking.
 
280
  - Random Forest
281
  - Logistic Regression
282
 
283
+ Each experiment is logged to MLflow and they are all available [here](https://dagshub.com/se4ai2526-uniba/CardioTrack.mlflow).
284
 
285
  #### Model Registry and Thresholds
286
  Models that reach or exceed the predefined **performance thresholds** (as defined in the ML Canvas) are automatically **saved to the model registry**.
 
302
 
303
  - detect anomalies or invalid values at the data source
304
  - prevent the propagation of data issues into downstream processes
305
+ > **Important**: Great Expectation reports are available here [reports/great_expectations_reports](reports/great_expectations_reports)
306
 
307
+ #### Tests
308
+
309
+ ##### Code Quality
310
  We added automated **unit and integration tests** using **pytest**, covering the main modules and functionalities of the system.
311
+ > **Important**: Pytest report is available here [reports/pytest_report](reports/pytest_report/)
312
+
313
+ ##### Model Behavioral Testing
314
+ We implemented **behavioral tests** to validate clinical correctness of predictions.
315
 
316
+ > **Important**: Pytest report is available here [reports/pytest_report](reports/pytest_report/)
317
 
318
  #### ML Pipeline Enhancements
319
  we applied the following enhancements to the ML pipeline:
 
335
 
336
  #### Risk Classification
337
  We added a **Risk Classification** analysis for the system in accordance with **IMDRF** and **AI Act** regulations.
338
+ > **Important**: The Risk Classification is available here: [docs/Risk_Classification.md](docs/Risk_Classification.md) folder.
339
 
340
 
341
  ### Milestone 4 - API Integration
 
409
  - Introduction of *pytest* for automated testing.
410
  - Integration of *Great Expectations* for automated data quality checks.
411
 
412
+ **CI Workflows:**
413
+
414
+ | Workflow | Trigger | Purpose |
415
+ |----------|---------|---------|
416
+ | ruff-linter.yml | Pull Request | Autofix + push style corrections |
417
+ | pynblint.yml | PR on notebooks/** | Notebook-specific linting |
418
+ | pytestAndGX.yml | PR (excludes main) | Tests + data quality validation |
419
+ | deploy.yml | Push to main | sync → test → deploy to HF |
420
+
421
 
422
  #### Continuos Deployment
423
  Automated deployment to *Hugging Face* was implemented through Github Actions workflow
424
  *Hugging Face Space*: [Check Here](https://huggingface.co/spaces/CardioTrack/CardioTrack)
425
 
426
+ **CD Workflow:**
427
+ 1. **test**: Run full test suite
428
+ 2. **sync**: Copy files from main to deploy branch
429
+ 3. **deploy-to-hf**: Push to HF Space + health check
430
+
431
+ ### Milestone 6 - Monitoring
432
+
433
+ In this milestone, we implemented:
434
+
435
+ ### Infrastructure
436
+
437
+ A multi-container monitoring stack was deployed using Docker Compose:
438
+ - **Prometheus** for metrics collection
439
+ - **Grafana** for visualization through a custom dashboard
440
+ - **Locust** for load testing
441
+
442
+ ### Resource Monitoring
443
+ Using the infrastructure defined above, we perform internal resource monitoring:
444
+ - **Prometheus** collects application metrics in real-time
445
+ - **Locust** simulates user traffic to evaluate system performance under load
446
+ - **Grafana** aggregates the most relevant metrics and displays them in a purpose-built dashboard for analysis
447
+
448
+ Additionally, we use **Uptime - Better Stack** for external uptime monitoring.
449
+
450
+ ### Performance Monitoring
451
+
452
+ Automated data drift detection was implemented:
453
+ - **APScheduler** for scheduled data collection from production
454
+ - **Deepchecks** for drift analysis on incoming data
455
+
456
+ > **Important**: Further information about tests and monitoring can be found in [reports/README.md](reports/README.md)
predicting_outcomes_in_heart_failure/app/deepchecks_monitoring/drift_runner.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from datetime import UTC, datetime
4
+ import json
5
+ from pathlib import Path
6
+
7
+ from deepchecks.tabular import Dataset
8
+ from deepchecks.tabular.checks import FeatureDrift
9
+ from loguru import logger
10
+ import pandas as pd
11
+ from predicting_outcomes_in_heart_failure.config import CAT_FEATURES
12
+
13
+
14
+ def _read_state(state_path: Path) -> dict:
15
+ if not state_path.exists():
16
+ return {"last_processed_rows": 0}
17
+ try:
18
+ return json.loads(state_path.read_text(encoding="utf-8"))
19
+ except Exception:
20
+ return {"last_processed_rows": 0}
21
+
22
+
23
+ def _write_state(state_path: Path, state: dict) -> None:
24
+ state_path.parent.mkdir(parents=True, exist_ok=True)
25
+ state_path.write_text(json.dumps(state, indent=2), encoding="utf-8")
26
+
27
+
28
+ def _coerce_bool_like(df: pd.DataFrame) -> pd.DataFrame:
29
+ """Converts columns with True/False values ​​to 0/1 int."""
30
+ bool_map = {"true": 1, "false": 0, "1": 1, "0": 0, True: 1, False: 0}
31
+ for col in df.columns:
32
+ s = df[col]
33
+
34
+ if s.dtype == "object":
35
+ lowered = s.astype(str).str.strip().str.lower()
36
+ uniq = set(lowered.dropna().unique().tolist())
37
+ if uniq.issubset({"true", "false", "0", "1"}):
38
+ df[col] = lowered.map(bool_map).astype("int64")
39
+ elif s.dtype == "bool":
40
+ df[col] = s.astype("int64")
41
+ return df
42
+
43
+
44
+ def _drift_score_to_float(drift_score: object) -> float:
45
+ if isinstance(drift_score, (int, float)):
46
+ return float(drift_score)
47
+
48
+ if isinstance(drift_score, dict):
49
+ if "value" in drift_score:
50
+ return float(drift_score["value"])
51
+ for v in drift_score.values():
52
+ if isinstance(v, (int, float)):
53
+ return float(v)
54
+
55
+ raise TypeError(f"Unsupported drift_score format: {type(drift_score)} -> {drift_score}")
56
+
57
+
58
+ def run_drift_if_enough_rows(
59
+ *,
60
+ reference_csv: Path,
61
+ production_csv: Path,
62
+ reports_dir: Path,
63
+ min_rows: int = 20,
64
+ feature_columns: list[str] | None = None,
65
+ state_path: Path | None = None,
66
+ ) -> dict:
67
+ if not production_csv.exists():
68
+ return {"ran": False, "reason": "production_csv_missing", "path": str(production_csv)}
69
+
70
+ try:
71
+ n_rows = sum(1 for _ in production_csv.open("r", encoding="utf-8")) - 1
72
+ except Exception as e:
73
+ logger.exception(f"Failed counting rows in {production_csv}: {e}")
74
+ return {"ran": False, "reason": "count_failed", "error": str(e)}
75
+
76
+ if n_rows < min_rows:
77
+ return {"ran": False, "reason": "not_enough_rows", "n_rows": n_rows, "min_rows": min_rows}
78
+
79
+ last_processed = 0
80
+ if state_path is not None:
81
+ state = _read_state(state_path)
82
+ last_processed = int(state.get("last_processed_rows", 0))
83
+ if n_rows == last_processed:
84
+ return {"ran": False, "reason": "no_new_rows", "n_rows": n_rows}
85
+
86
+ ref_df = pd.read_csv(reference_csv)
87
+ prod_df = pd.read_csv(production_csv)
88
+
89
+ ref_df = _coerce_bool_like(ref_df)
90
+ prod_df = _coerce_bool_like(prod_df)
91
+
92
+ if feature_columns is not None:
93
+ ref_df = ref_df[feature_columns]
94
+ prod_df = prod_df[feature_columns]
95
+
96
+ ref_ds = Dataset(ref_df, label=None, cat_features=CAT_FEATURES)
97
+ prod_ds = Dataset(prod_df, label=None, cat_features=CAT_FEATURES)
98
+
99
+ check = FeatureDrift()
100
+
101
+ try:
102
+ result = check.run(train_dataset=ref_ds, test_dataset=prod_ds)
103
+ except TypeError:
104
+ result = check.run(ref_ds, prod_ds)
105
+
106
+ raw = result.value
107
+
108
+ features = {name: _drift_score_to_float(info.get("Drift score")) for name, info in raw.items()}
109
+
110
+ threshold = 0.2
111
+ above = [k for k, v in features.items() if v >= threshold]
112
+
113
+ output = {
114
+ "check": "FeatureDrift",
115
+ "timestamp_utc": datetime.now(UTC).isoformat(),
116
+ "data": {
117
+ "reference_rows": len(ref_df),
118
+ "production_rows": len(prod_df),
119
+ },
120
+ "features": features,
121
+ "summary": {
122
+ "max_drift": max(features.values()),
123
+ "mean_drift": sum(features.values()) / len(features),
124
+ "threshold": threshold,
125
+ "features_above_threshold": above,
126
+ "n_features_above_threshold": len(above),
127
+ },
128
+ }
129
+ reports_dir.mkdir(parents=True, exist_ok=True)
130
+ ts = datetime.now(UTC).strftime("%Y-%m-%d_%H-%M-%S")
131
+
132
+ report_path = reports_dir / f"drift_result_{ts}.json"
133
+ report_path.write_text(json.dumps(output, indent=2), encoding="utf-8")
134
+
135
+ logger.success(f"Deepchecks drift report generated: {report_path}")
136
+
137
+ if state_path is not None:
138
+ _write_state(state_path, {"last_processed_rows": n_rows, "last_report": str(report_path)})
139
+
140
+ return {
141
+ "ran": True,
142
+ "n_rows": n_rows,
143
+ "report_path": str(report_path),
144
+ "passed": bool(getattr(result, "passed", True)),
145
+ }
predicting_outcomes_in_heart_failure/app/deepchecks_monitoring/production_data_collector.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from datetime import UTC, datetime
4
+ from pathlib import Path
5
+
6
+ from loguru import logger
7
+ import pandas as pd
8
+
9
+
10
+ def append_predictions_to_csv(
11
+ *,
12
+ csv_path: Path,
13
+ endpoint: str,
14
+ X: pd.DataFrame,
15
+ y_pred: list[int] | int,
16
+ feature_columns: list[str] | None = None,
17
+ ) -> None:
18
+ """
19
+ Appende su CSV i dati di produzione (input + prediction).
20
+ """
21
+ csv_path.parent.mkdir(parents=True, exist_ok=True)
22
+
23
+ # Normalize predictions
24
+ y_list = [y_pred] if isinstance(y_pred, int) else y_pred
25
+
26
+ df = X.copy()
27
+
28
+ if feature_columns is not None:
29
+ missing = [c for c in feature_columns if c not in df.columns]
30
+ if missing:
31
+ raise ValueError(f"Missing expected feature columns in production batch: {missing}")
32
+ df = df[feature_columns]
33
+
34
+ if len(df) != len(y_list):
35
+ raise ValueError(
36
+ f"Row mismatch: X has {len(df)} rows but predictions has {len(y_list)} items"
37
+ )
38
+ df.insert(0, "timestamp_utc", datetime.now(UTC).isoformat())
39
+ df.insert(1, "endpoint", endpoint)
40
+ df["prediction"] = y_list
41
+
42
+ write_header = not csv_path.exists()
43
+ df.to_csv(csv_path, mode="a", index=False, header=write_header)
44
+ logger.info(f"Appended {len(df)} rows to production CSV: {csv_path}")
predicting_outcomes_in_heart_failure/app/deepchecks_monitoring/scheduler.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from apscheduler.schedulers.asyncio import AsyncIOScheduler
4
+ from apscheduler.triggers.cron import CronTrigger
5
+ from loguru import logger
6
+ from predicting_outcomes_in_heart_failure.app.deepchecks_monitoring.drift_runner import (
7
+ run_drift_if_enough_rows,
8
+ )
9
+ from predicting_outcomes_in_heart_failure.config import (
10
+ INPUT_COLUMNS,
11
+ PRODUCTION_CSV_PATH,
12
+ REFERENCE_CSV,
13
+ REPORTS_DIR,
14
+ STATE_PATH,
15
+ )
16
+
17
+ scheduler = AsyncIOScheduler()
18
+
19
+
20
+ def drift_job() -> None:
21
+ try:
22
+ out = run_drift_if_enough_rows(
23
+ reference_csv=REFERENCE_CSV,
24
+ production_csv=PRODUCTION_CSV_PATH,
25
+ reports_dir=REPORTS_DIR,
26
+ min_rows=20,
27
+ feature_columns=list(INPUT_COLUMNS),
28
+ state_path=STATE_PATH,
29
+ )
30
+ logger.info(f"Drift job result: {out}")
31
+ except Exception as e:
32
+ logger.exception(f"Drift job failed: {e}")
33
+
34
+
35
+ def start_scheduler() -> None:
36
+ if scheduler.running:
37
+ return
38
+
39
+ scheduler.add_job(
40
+ drift_job,
41
+ trigger=CronTrigger(hour=21, minute=0),
42
+ id="deepchecks_drift_job",
43
+ replace_existing=True,
44
+ misfire_grace_time=120,
45
+ max_instances=1,
46
+ coalesce=True,
47
+ )
48
+ scheduler.start()
49
+ logger.info("Deepchecks drift scheduler started (every 5 minutes).")
50
+
51
+
52
+ def shutdown_scheduler() -> None:
53
+ if scheduler.running:
54
+ scheduler.shutdown(wait=False)
55
+ logger.info("Deepchecks drift scheduler stopped.")
predicting_outcomes_in_heart_failure/app/main.py CHANGED
@@ -6,6 +6,11 @@ import gradio as gr
6
  import joblib
7
  from loguru import logger
8
 
 
 
 
 
 
9
  from predicting_outcomes_in_heart_failure.app.routers import cards, model_info, prediction
10
  from predicting_outcomes_in_heart_failure.app.utils import load_page, update_patient_index_choices
11
  from predicting_outcomes_in_heart_failure.app.wrapper import Wrapper
@@ -22,10 +27,14 @@ async def lifespan(app: FastAPI):
22
  logger.info(f"Loading default model from {MODEL_PATH} ...")
23
  app.state.model = joblib.load(MODEL_PATH)
24
  logger.success(f"Default model loaded from {MODEL_PATH}")
 
 
25
 
26
  try:
27
  yield
28
  finally:
 
 
29
  app.state.model = None
30
  logger.info("Default model cleared on application shutdown")
31
 
@@ -45,6 +54,7 @@ app.include_router(prediction.router)
45
  app.include_router(model_info.router)
46
  app.include_router(cards.router)
47
 
 
48
 
49
  # UI Definition
50
  with gr.Blocks(title="CardioTrack") as io:
 
6
  import joblib
7
  from loguru import logger
8
 
9
+ from predicting_outcomes_in_heart_failure.app.deepchecks_monitoring.scheduler import (
10
+ shutdown_scheduler,
11
+ start_scheduler,
12
+ )
13
+ from predicting_outcomes_in_heart_failure.app.monitoring import instrumentator
14
  from predicting_outcomes_in_heart_failure.app.routers import cards, model_info, prediction
15
  from predicting_outcomes_in_heart_failure.app.utils import load_page, update_patient_index_choices
16
  from predicting_outcomes_in_heart_failure.app.wrapper import Wrapper
 
27
  logger.info(f"Loading default model from {MODEL_PATH} ...")
28
  app.state.model = joblib.load(MODEL_PATH)
29
  logger.success(f"Default model loaded from {MODEL_PATH}")
30
+ start_scheduler()
31
+ logger.info("Deepchecks scheduler started")
32
 
33
  try:
34
  yield
35
  finally:
36
+ shutdown_scheduler()
37
+ logger.info("Deepchecks scheduler stopped")
38
  app.state.model = None
39
  logger.info("Default model cleared on application shutdown")
40
 
 
54
  app.include_router(model_info.router)
55
  app.include_router(cards.router)
56
 
57
+ instrumentator.instrument(app).expose(app, include_in_schema=False, should_gzip=True)
58
 
59
  # UI Definition
60
  with gr.Blocks(title="CardioTrack") as io:
predicting_outcomes_in_heart_failure/app/monitoring.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ from prometheus_client import Counter, Histogram
4
+ from prometheus_fastapi_instrumentator import Instrumentator, metrics
5
+
6
+ NAMESPACE = os.environ.get("METRICS_NAMESPACE", "cardiotrack")
7
+ SUBSYSTEM = os.environ.get("METRICS_SUBSYSTEM", "api")
8
+
9
+ instrumentator = Instrumentator(
10
+ should_group_status_codes=True,
11
+ should_ignore_untemplated=True,
12
+ should_instrument_requests_inprogress=True,
13
+ excluded_handlers=["/metrics"],
14
+ inprogress_name="fastapi_inprogress",
15
+ inprogress_labels=True,
16
+ )
17
+
18
+ instrumentator.add(
19
+ metrics.request_size(
20
+ should_include_handler=True,
21
+ should_include_method=True,
22
+ should_include_status=True,
23
+ metric_namespace=NAMESPACE,
24
+ metric_subsystem=SUBSYSTEM,
25
+ )
26
+ )
27
+
28
+ instrumentator.add(
29
+ metrics.response_size(
30
+ should_include_handler=True,
31
+ should_include_method=True,
32
+ should_include_status=True,
33
+ metric_namespace=NAMESPACE,
34
+ metric_subsystem=SUBSYSTEM,
35
+ )
36
+ )
37
+
38
+ instrumentator.add(
39
+ metrics.latency(
40
+ should_include_handler=True,
41
+ should_include_method=True,
42
+ should_include_status=True,
43
+ metric_namespace=NAMESPACE,
44
+ metric_subsystem=SUBSYSTEM,
45
+ buckets=[0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5],
46
+ )
47
+ )
48
+
49
+ instrumentator.add(
50
+ metrics.requests(
51
+ should_include_handler=True,
52
+ should_include_method=True,
53
+ should_include_status=True,
54
+ metric_namespace=NAMESPACE,
55
+ metric_subsystem=SUBSYSTEM,
56
+ )
57
+ )
58
+
59
+ prediction_counter = Counter(
60
+ name=f"{NAMESPACE}_{SUBSYSTEM}_predictions_total",
61
+ documentation="Total number of prediction requests",
62
+ labelnames=["prediction_type", "endpoint"],
63
+ )
64
+
65
+ prediction_result_counter = Counter(
66
+ name=f"{NAMESPACE}_{SUBSYSTEM}_prediction_results_total",
67
+ documentation="Count of prediction results by class",
68
+ labelnames=["prediction_class", "endpoint"],
69
+ )
70
+
71
+ model_error_counter = Counter(
72
+ name=f"{NAMESPACE}_{SUBSYSTEM}_model_errors_total",
73
+ documentation="Total number of model loading or prediction errors",
74
+ labelnames=["error_type", "endpoint"],
75
+ )
76
+
77
+ explanation_counter = Counter(
78
+ name=f"{NAMESPACE}_{SUBSYSTEM}_explanations_total",
79
+ documentation="Total number of explanation requests",
80
+ labelnames=["status", "endpoint"],
81
+ )
82
+
83
+ batch_size_histogram = Histogram(
84
+ name=f"{NAMESPACE}_{SUBSYSTEM}_batch_size",
85
+ documentation="Distribution of batch prediction sizes",
86
+ labelnames=["endpoint"],
87
+ buckets=[1, 5, 10, 20, 50, 100, 200, 500],
88
+ )
89
+
90
+ prediction_processing_time = Histogram(
91
+ name=f"{NAMESPACE}_{SUBSYSTEM}_prediction_processing_seconds",
92
+ documentation="Time spent on prediction processing (excluding HTTP overhead)",
93
+ labelnames=["prediction_type", "endpoint"],
94
+ buckets=[0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0, 60.0, 120.0],
95
+ )
predicting_outcomes_in_heart_failure/app/routers/prediction.py CHANGED
@@ -1,15 +1,32 @@
1
  from http import HTTPStatus
 
2
  from typing import Any
3
 
4
  from fastapi import APIRouter, Request
5
  from loguru import logger
6
  import pandas as pd
 
 
 
 
 
 
 
 
 
 
 
7
  from predicting_outcomes_in_heart_failure.app.schema import HeartSample
8
  from predicting_outcomes_in_heart_failure.app.utils import (
9
  construct_response,
10
  get_model_from_state,
11
  )
12
- from predicting_outcomes_in_heart_failure.config import FIGURES_DIR, MODEL_PATH
 
 
 
 
 
13
  from predicting_outcomes_in_heart_failure.modeling.explainability import (
14
  explain_prediction,
15
  save_shap_waterfall_plot,
@@ -22,68 +39,127 @@ router = APIRouter()
22
  @router.post("/predictions", tags=["Prediction"])
23
  @construct_response
24
  def predict(request: Request, payload: HeartSample):
 
 
25
  model = get_model_from_state(request)
26
  if model is None:
 
27
  return {
28
  "message": HTTPStatus.SERVICE_UNAVAILABLE.phrase,
29
  "status-code": HTTPStatus.SERVICE_UNAVAILABLE,
30
  "data": {"detail": "Model is not loaded."},
31
  }
32
 
33
- X_raw = payload.to_dataframe()
34
- X = preprocessing(X_raw)
35
- y_pred = int(model.predict(X)[0])
 
 
36
 
37
- data: dict[str, Any] = {
38
- "input": payload.model_dump(),
39
- "prediction": y_pred,
40
- }
 
 
 
41
 
42
- logger.success("Prediction completed successfully for /predictions")
43
- return {
44
- "message": HTTPStatus.OK.phrase,
45
- "status-code": HTTPStatus.OK,
46
- "data": data,
47
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
 
50
  @router.post("/batch-predictions", tags=["Prediction"])
51
  @construct_response
52
  def predict_batch(request: Request, payload: list[HeartSample]):
 
 
 
 
53
  model = get_model_from_state(request)
54
  if model is None:
 
 
 
55
  return {
56
  "message": HTTPStatus.SERVICE_UNAVAILABLE.phrase,
57
  "status-code": HTTPStatus.SERVICE_UNAVAILABLE,
58
  "data": {"detail": "Model is not loaded."},
59
  }
60
 
61
- X_raw_list = [sample.to_dataframe() for sample in payload]
62
- X_raw = pd.concat(X_raw_list, ignore_index=True)
63
- X = preprocessing(X_raw)
 
 
64
 
65
- y_pred = [int(y) for y in model.predict(X)]
66
 
67
- results: list[dict[str, Any]] = []
68
- for idx, (sample, pred) in enumerate(zip(payload, y_pred, strict=True)):
69
- results.append(
70
- {
71
- "index": idx,
72
- "input": sample.model_dump(),
73
- "prediction": pred,
74
- }
75
  )
76
 
77
- data: dict[str, Any] = {
78
- "results": results,
79
- "batch_size": len(results),
80
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
 
82
- return {
83
- "message": HTTPStatus.OK.phrase,
84
- "status-code": HTTPStatus.OK,
85
- "data": data,
86
- }
 
 
 
 
 
 
87
 
88
 
89
  @router.post("/explanations", tags=["Explainability"])
@@ -91,6 +167,7 @@ def predict_batch(request: Request, payload: list[HeartSample]):
91
  def explain(request: Request, payload: HeartSample):
92
  model = get_model_from_state(request)
93
  if model is None:
 
94
  return {
95
  "message": HTTPStatus.SERVICE_UNAVAILABLE.phrase,
96
  "status-code": HTTPStatus.SERVICE_UNAVAILABLE,
@@ -102,6 +179,7 @@ def explain(request: Request, payload: HeartSample):
102
 
103
  data: dict[str, Any] = {"input": payload.model_dump()}
104
  model_type = MODEL_PATH.stem
 
105
 
106
  try:
107
  logger.info("Computing explanation for default model prediction...")
@@ -109,10 +187,14 @@ def explain(request: Request, payload: HeartSample):
109
  if explanations:
110
  data["explanations"] = explanations
111
  logger.success("Explanation computed successfully for default model.")
 
112
  else:
113
  logger.warning("No explanation available for default model.")
114
  except Exception as e:
115
  logger.exception(f"Failed to compute explanation: {e}")
 
 
 
116
 
117
  try:
118
  plot_path = FIGURES_DIR / f"shap_waterfall_default_{model_type}.png"
@@ -126,6 +208,12 @@ def explain(request: Request, payload: HeartSample):
126
  data["explanation_plot_url"] = f"/figures/{saved_path.name}"
127
  except Exception as e:
128
  logger.exception(f"Failed to generate explanation plot: {e}")
 
 
 
 
 
 
129
 
130
  logger.success("Explanation completed successfully for /explanations")
131
  return {
 
1
  from http import HTTPStatus
2
+ import time
3
  from typing import Any
4
 
5
  from fastapi import APIRouter, Request
6
  from loguru import logger
7
  import pandas as pd
8
+ from predicting_outcomes_in_heart_failure.app.deepchecks_monitoring import (
9
+ production_data_collector as pdc,
10
+ )
11
+ from predicting_outcomes_in_heart_failure.app.monitoring import (
12
+ batch_size_histogram,
13
+ explanation_counter,
14
+ model_error_counter,
15
+ prediction_counter,
16
+ prediction_processing_time,
17
+ prediction_result_counter,
18
+ )
19
  from predicting_outcomes_in_heart_failure.app.schema import HeartSample
20
  from predicting_outcomes_in_heart_failure.app.utils import (
21
  construct_response,
22
  get_model_from_state,
23
  )
24
+ from predicting_outcomes_in_heart_failure.config import (
25
+ FIGURES_DIR,
26
+ INPUT_COLUMNS,
27
+ MODEL_PATH,
28
+ PRODUCTION_CSV_PATH,
29
+ )
30
  from predicting_outcomes_in_heart_failure.modeling.explainability import (
31
  explain_prediction,
32
  save_shap_waterfall_plot,
 
39
  @router.post("/predictions", tags=["Prediction"])
40
  @construct_response
41
  def predict(request: Request, payload: HeartSample):
42
+ prediction_counter.labels(prediction_type="single", endpoint="/predictions").inc()
43
+
44
  model = get_model_from_state(request)
45
  if model is None:
46
+ model_error_counter.labels(error_type="model_not_loaded", endpoint="/predictions").inc()
47
  return {
48
  "message": HTTPStatus.SERVICE_UNAVAILABLE.phrase,
49
  "status-code": HTTPStatus.SERVICE_UNAVAILABLE,
50
  "data": {"detail": "Model is not loaded."},
51
  }
52
 
53
+ start_time = time.time()
54
+ try:
55
+ X_raw = payload.to_dataframe()
56
+ X = preprocessing(X_raw)
57
+ y_pred = int(model.predict(X)[0])
58
 
59
+ pdc.append_predictions_to_csv(
60
+ csv_path=PRODUCTION_CSV_PATH,
61
+ endpoint="/predictions",
62
+ X=X,
63
+ y_pred=y_pred,
64
+ feature_columns=list(INPUT_COLUMNS),
65
+ )
66
 
67
+ processing_time = time.time() - start_time
68
+ prediction_processing_time.labels(
69
+ prediction_type="single", endpoint="/predictions"
70
+ ).observe(processing_time)
71
+
72
+ prediction_result_counter.labels(
73
+ prediction_class=str(y_pred), endpoint="/predictions"
74
+ ).inc()
75
+
76
+ data: dict[str, Any] = {
77
+ "input": payload.model_dump(),
78
+ "prediction": y_pred,
79
+ }
80
+
81
+ logger.success("Prediction completed successfully for /predictions")
82
+ return {
83
+ "message": HTTPStatus.OK.phrase,
84
+ "status-code": HTTPStatus.OK,
85
+ "data": data,
86
+ }
87
+ except Exception as e:
88
+ model_error_counter.labels(error_type="prediction_error", endpoint="/predictions").inc()
89
+ logger.exception(f"Prediction error: {e}")
90
+ raise
91
 
92
 
93
  @router.post("/batch-predictions", tags=["Prediction"])
94
  @construct_response
95
  def predict_batch(request: Request, payload: list[HeartSample]):
96
+ prediction_counter.labels(prediction_type="batch", endpoint="/batch-predictions").inc()
97
+ batch_size = len(payload)
98
+ batch_size_histogram.labels(endpoint="/batch-predictions").observe(batch_size)
99
+
100
  model = get_model_from_state(request)
101
  if model is None:
102
+ model_error_counter.labels(
103
+ error_type="model_not_loaded", endpoint="/batch-predictions"
104
+ ).inc()
105
  return {
106
  "message": HTTPStatus.SERVICE_UNAVAILABLE.phrase,
107
  "status-code": HTTPStatus.SERVICE_UNAVAILABLE,
108
  "data": {"detail": "Model is not loaded."},
109
  }
110
 
111
+ start_time = time.time()
112
+ try:
113
+ X_raw_list = [sample.to_dataframe() for sample in payload]
114
+ X_raw = pd.concat(X_raw_list, ignore_index=True)
115
+ X = preprocessing(X_raw)
116
 
117
+ y_pred = [int(y) for y in model.predict(X)]
118
 
119
+ pdc.append_predictions_to_csv(
120
+ csv_path=PRODUCTION_CSV_PATH,
121
+ endpoint="/batch-predictions",
122
+ X=X,
123
+ y_pred=y_pred,
124
+ feature_columns=list(INPUT_COLUMNS),
 
 
125
  )
126
 
127
+ processing_time = time.time() - start_time
128
+ prediction_processing_time.labels(
129
+ prediction_type="batch", endpoint="/batch-predictions"
130
+ ).observe(processing_time)
131
+
132
+ for pred in y_pred:
133
+ prediction_result_counter.labels(
134
+ prediction_class=str(pred), endpoint="/batch-predictions"
135
+ ).inc()
136
+
137
+ results: list[dict[str, Any]] = []
138
+ for idx, (sample, pred) in enumerate(zip(payload, y_pred, strict=True)):
139
+ results.append(
140
+ {
141
+ "index": idx,
142
+ "input": sample.model_dump(),
143
+ "prediction": pred,
144
+ }
145
+ )
146
+
147
+ data: dict[str, Any] = {
148
+ "results": results,
149
+ "batch_size": len(results),
150
+ }
151
 
152
+ return {
153
+ "message": HTTPStatus.OK.phrase,
154
+ "status-code": HTTPStatus.OK,
155
+ "data": data,
156
+ }
157
+ except Exception as e:
158
+ model_error_counter.labels(
159
+ error_type="prediction_error", endpoint="/batch-predictions"
160
+ ).inc()
161
+ logger.exception(f"Batch prediction error: {e}")
162
+ raise
163
 
164
 
165
  @router.post("/explanations", tags=["Explainability"])
 
167
  def explain(request: Request, payload: HeartSample):
168
  model = get_model_from_state(request)
169
  if model is None:
170
+ explanation_counter.labels(status="error_model_not_loaded", endpoint="/explanations").inc()
171
  return {
172
  "message": HTTPStatus.SERVICE_UNAVAILABLE.phrase,
173
  "status-code": HTTPStatus.SERVICE_UNAVAILABLE,
 
179
 
180
  data: dict[str, Any] = {"input": payload.model_dump()}
181
  model_type = MODEL_PATH.stem
182
+ explanation_success = False
183
 
184
  try:
185
  logger.info("Computing explanation for default model prediction...")
 
187
  if explanations:
188
  data["explanations"] = explanations
189
  logger.success("Explanation computed successfully for default model.")
190
+ explanation_success = True
191
  else:
192
  logger.warning("No explanation available for default model.")
193
  except Exception as e:
194
  logger.exception(f"Failed to compute explanation: {e}")
195
+ explanation_counter.labels(
196
+ status="error_computation_failed", endpoint="/explanations"
197
+ ).inc()
198
 
199
  try:
200
  plot_path = FIGURES_DIR / f"shap_waterfall_default_{model_type}.png"
 
208
  data["explanation_plot_url"] = f"/figures/{saved_path.name}"
209
  except Exception as e:
210
  logger.exception(f"Failed to generate explanation plot: {e}")
211
+ explanation_counter.labels(
212
+ status="error_plot_generation_failed", endpoint="/explanations"
213
+ ).inc()
214
+
215
+ if explanation_success:
216
+ explanation_counter.labels(status="success", endpoint="/explanations").inc()
217
 
218
  logger.success("Explanation completed successfully for /explanations")
219
  return {
predicting_outcomes_in_heart_failure/config.py CHANGED
@@ -123,6 +123,24 @@ CARD_PATHS = {
123
  "model_card": MODELS_DIR / "README.md",
124
  }
125
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  # ----------------------------
127
  # API
128
  # ----------------------------
 
123
  "model_card": MODELS_DIR / "README.md",
124
  }
125
 
126
+ MONITORING_DIR = DATA_DIR / "monitoring"
127
+ PRODUCTION_CSV_PATH = MONITORING_DIR / "production_inputs.csv"
128
+ REFERENCE_CSV = PROCESSED_DATA_DIR / "nosex" / "train.csv"
129
+ REPORTS_DIR = MONITORING_DIR / "reports"
130
+ STATE_PATH = MONITORING_DIR / "state.json"
131
+
132
+ CAT_FEATURES = [
133
+ "ChestPainType_ASY",
134
+ "ChestPainType_ATA",
135
+ "ChestPainType_NAP",
136
+ "ChestPainType_TA",
137
+ "RestingECG_LVH",
138
+ "RestingECG_Normal",
139
+ "RestingECG_ST",
140
+ "ST_Slope_Down",
141
+ "ST_Slope_Flat",
142
+ "ST_Slope_Up",
143
+ ]
144
  # ----------------------------
145
  # API
146
  # ----------------------------
prometheus.yml ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ global:
2
+ scrape_interval: 15s
3
+
4
+ external_labels:
5
+ monitor: "codelab-monitor"
6
+
7
+ scrape_configs:
8
+ - job_name: "fastapi"
9
+ scrape_interval: 5s
10
+ static_configs:
11
+ - targets: ["app:7860"]
12
+ metrics_path: /metrics
13
+
14
+ - job_name: "prometheus"
15
+ static_configs:
16
+ - targets: ["localhost:9090"]
pyproject.toml CHANGED
@@ -16,11 +16,14 @@ classifiers = [
16
 
17
  ]
18
  dependencies = [
 
19
  "asttokens>=3.0.0",
20
  "boto3>=1.36.0",
21
  "botocore>=1.36.0",
22
  "dagshub>=0.6.3",
 
23
  "dvc-s3>=3.2.2",
 
24
  "gradio>=6.0.2",
25
  "great-expectations>=1.9.0",
26
  "httpx>=0.28.1",
@@ -32,15 +35,18 @@ dependencies = [
32
  "matplotlib>=3.10.7",
33
  "mkdocs",
34
  "mlflow==2.22.0",
35
- "numpy>=2.3.4",
36
  "pandas>=2.3.3",
37
  "pip",
 
 
38
  "pytest",
 
39
  "python-dotenv",
40
  "ruff",
41
  "scikit-learn>=1.7.2",
42
  "seaborn>=0.13.2",
43
- "shap>=0.50.0",
44
  "tqdm",
45
  "typer",
46
  ]
 
16
 
17
  ]
18
  dependencies = [
19
+ "apscheduler>=3.11.2",
20
  "asttokens>=3.0.0",
21
  "boto3>=1.36.0",
22
  "botocore>=1.36.0",
23
  "dagshub>=0.6.3",
24
+ "deepchecks>=0.19.1",
25
  "dvc-s3>=3.2.2",
26
+ "dvc[s3]>=3.64.2",
27
  "gradio>=6.0.2",
28
  "great-expectations>=1.9.0",
29
  "httpx>=0.28.1",
 
35
  "matplotlib>=3.10.7",
36
  "mkdocs",
37
  "mlflow==2.22.0",
38
+ "numpy<2",
39
  "pandas>=2.3.3",
40
  "pip",
41
+ "prometheus-client>=0.23.1",
42
+ "prometheus-fastapi-instrumentator>=7.1.0",
43
  "pytest",
44
+ "pytest-html>=4.1.1",
45
  "python-dotenv",
46
  "ruff",
47
  "scikit-learn>=1.7.2",
48
  "seaborn>=0.13.2",
49
+ "shap<0.50",
50
  "tqdm",
51
  "typer",
52
  ]
uv.lock CHANGED
@@ -214,6 +214,64 @@ wheels = [
214
  { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" },
215
  ]
216
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  [[package]]
218
  name = "asttokens"
219
  version = "3.0.0"
@@ -366,6 +424,23 @@ wheels = [
366
  { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080, upload-time = "2025-02-20T21:01:16.647Z" },
367
  ]
368
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
  [[package]]
370
  name = "celery"
371
  version = "5.6.0"
@@ -716,6 +791,35 @@ wheels = [
716
  { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" },
717
  ]
718
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
719
  [[package]]
720
  name = "defusedxml"
721
  version = "0.7.1"
@@ -846,6 +950,11 @@ wheels = [
846
  { url = "https://files.pythonhosted.org/packages/e1/e6/1782bcb8cdf82a971c1447d83f69efb3356945b68e28a28708fa4ddfa3c3/dvc-3.64.2-py3-none-any.whl", hash = "sha256:14e76baaef50cc10a43aaea788b49ea965835a59c16d0d63693a9ba1c2001090", size = 467981, upload-time = "2025-12-06T05:10:58.565Z" },
847
  ]
848
 
 
 
 
 
 
849
  [[package]]
850
  name = "dvc-data"
851
  version = "3.16.12"
@@ -1078,6 +1187,15 @@ wheels = [
1078
  { url = "https://files.pythonhosted.org/packages/c7/93/0dd45cd283c32dea1545151d8c3637b4b8c53cdb3a625aeb2885b184d74d/fonttools-4.60.1-py3-none-any.whl", hash = "sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb", size = 1143175, upload-time = "2025-09-29T21:13:24.134Z" },
1079
  ]
1080
 
 
 
 
 
 
 
 
 
 
1081
  [[package]]
1082
  name = "frozenlist"
1083
  version = "1.8.0"
@@ -1570,6 +1688,34 @@ wheels = [
1570
  { url = "https://files.pythonhosted.org/packages/91/d0/274fbf7b0b12643cbbc001ce13e6a5b1607ac4929d1b11c72460152c9fc3/ipython-8.37.0-py3-none-any.whl", hash = "sha256:ed87326596b878932dbcb171e3e698845434d8c61b8d8cd474bf663041a9dcf2", size = 831864, upload-time = "2025-05-31T16:39:06.38Z" },
1571
  ]
1572
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1573
  [[package]]
1574
  name = "iterative-telemetry"
1575
  version = "0.0.10"
@@ -1636,6 +1782,24 @@ wheels = [
1636
  { url = "https://files.pythonhosted.org/packages/1e/e8/685f47e0d754320684db4425a0967f7d3fa70126bffd76110b7009a0090f/joblib-1.5.2-py3-none-any.whl", hash = "sha256:4e1f0bdbb987e6d843c70cf43714cb276623def372df3c22fe5266b2670bc241", size = 308396, upload-time = "2025-08-27T12:15:45.188Z" },
1637
  ]
1638
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1639
  [[package]]
1640
  name = "jsonschema"
1641
  version = "4.25.1"
@@ -1651,6 +1815,19 @@ wheels = [
1651
  { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" },
1652
  ]
1653
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1654
  [[package]]
1655
  name = "jsonschema-specifications"
1656
  version = "2025.9.1"
@@ -1692,6 +1869,68 @@ wheels = [
1692
  { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" },
1693
  ]
1694
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1695
  [[package]]
1696
  name = "jupyterlab-pygments"
1697
  version = "0.3.0"
@@ -1701,6 +1940,15 @@ wheels = [
1701
  { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884, upload-time = "2023-11-23T09:26:34.325Z" },
1702
  ]
1703
 
 
 
 
 
 
 
 
 
 
1704
  [[package]]
1705
  name = "kagglehub"
1706
  version = "0.3.13"
@@ -1757,6 +2005,15 @@ wheels = [
1757
  { url = "https://files.pythonhosted.org/packages/14/d6/943cf84117cd9ddecf6e1707a3f712a49fc64abdb8ac31b19132871af1dd/kombu-5.6.1-py3-none-any.whl", hash = "sha256:b69e3f5527ec32fc5196028a36376501682973e9620d6175d1c3d4eaf7e95409", size = 214141, upload-time = "2025-11-25T11:07:31.54Z" },
1758
  ]
1759
 
 
 
 
 
 
 
 
 
 
1760
  [[package]]
1761
  name = "llvmlite"
1762
  version = "0.45.1"
@@ -2074,6 +2331,15 @@ wheels = [
2074
  { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
2075
  ]
2076
 
 
 
 
 
 
 
 
 
 
2077
  [[package]]
2078
  name = "nbclient"
2079
  version = "0.10.2"
@@ -2166,28 +2432,18 @@ wheels = [
2166
 
2167
  [[package]]
2168
  name = "numpy"
2169
- version = "2.3.4"
2170
- source = { registry = "https://pypi.org/simple" }
2171
- sdist = { url = "https://files.pythonhosted.org/packages/b5/f4/098d2270d52b41f1bd7db9fc288aaa0400cb48c2a3e2af6fa365d9720947/numpy-2.3.4.tar.gz", hash = "sha256:a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a", size = 20582187, upload-time = "2025-10-15T16:18:11.77Z" }
2172
- wheels = [
2173
- { url = "https://files.pythonhosted.org/packages/60/e7/0e07379944aa8afb49a556a2b54587b828eb41dc9adc56fb7615b678ca53/numpy-2.3.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e78aecd2800b32e8347ce49316d3eaf04aed849cd5b38e0af39f829a4e59f5eb", size = 21259519, upload-time = "2025-10-15T16:15:19.012Z" },
2174
- { url = "https://files.pythonhosted.org/packages/d0/cb/5a69293561e8819b09e34ed9e873b9a82b5f2ade23dce4c51dc507f6cfe1/numpy-2.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7fd09cc5d65bda1e79432859c40978010622112e9194e581e3415a3eccc7f43f", size = 14452796, upload-time = "2025-10-15T16:15:23.094Z" },
2175
- { url = "https://files.pythonhosted.org/packages/e4/04/ff11611200acd602a1e5129e36cfd25bf01ad8e5cf927baf2e90236eb02e/numpy-2.3.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1b219560ae2c1de48ead517d085bc2d05b9433f8e49d0955c82e8cd37bd7bf36", size = 5381639, upload-time = "2025-10-15T16:15:25.572Z" },
2176
- { url = "https://files.pythonhosted.org/packages/ea/77/e95c757a6fe7a48d28a009267408e8aa382630cc1ad1db7451b3bc21dbb4/numpy-2.3.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:bafa7d87d4c99752d07815ed7a2c0964f8ab311eb8168f41b910bd01d15b6032", size = 6914296, upload-time = "2025-10-15T16:15:27.079Z" },
2177
- { url = "https://files.pythonhosted.org/packages/a3/d2/137c7b6841c942124eae921279e5c41b1c34bab0e6fc60c7348e69afd165/numpy-2.3.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36dc13af226aeab72b7abad501d370d606326a0029b9f435eacb3b8c94b8a8b7", size = 14591904, upload-time = "2025-10-15T16:15:29.044Z" },
2178
- { url = "https://files.pythonhosted.org/packages/bb/32/67e3b0f07b0aba57a078c4ab777a9e8e6bc62f24fb53a2337f75f9691699/numpy-2.3.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7b2f9a18b5ff9824a6af80de4f37f4ec3c2aab05ef08f51c77a093f5b89adda", size = 16939602, upload-time = "2025-10-15T16:15:31.106Z" },
2179
- { url = "https://files.pythonhosted.org/packages/95/22/9639c30e32c93c4cee3ccdb4b09c2d0fbff4dcd06d36b357da06146530fb/numpy-2.3.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9984bd645a8db6ca15d850ff996856d8762c51a2239225288f08f9050ca240a0", size = 16372661, upload-time = "2025-10-15T16:15:33.546Z" },
2180
- { url = "https://files.pythonhosted.org/packages/12/e9/a685079529be2b0156ae0c11b13d6be647743095bb51d46589e95be88086/numpy-2.3.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:64c5825affc76942973a70acf438a8ab618dbd692b84cd5ec40a0a0509edc09a", size = 18884682, upload-time = "2025-10-15T16:15:36.105Z" },
2181
- { url = "https://files.pythonhosted.org/packages/cf/85/f6f00d019b0cc741e64b4e00ce865a57b6bed945d1bbeb1ccadbc647959b/numpy-2.3.4-cp311-cp311-win32.whl", hash = "sha256:ed759bf7a70342f7817d88376eb7142fab9fef8320d6019ef87fae05a99874e1", size = 6570076, upload-time = "2025-10-15T16:15:38.225Z" },
2182
- { url = "https://files.pythonhosted.org/packages/7d/10/f8850982021cb90e2ec31990291f9e830ce7d94eef432b15066e7cbe0bec/numpy-2.3.4-cp311-cp311-win_amd64.whl", hash = "sha256:faba246fb30ea2a526c2e9645f61612341de1a83fb1e0c5edf4ddda5a9c10996", size = 13089358, upload-time = "2025-10-15T16:15:40.404Z" },
2183
- { url = "https://files.pythonhosted.org/packages/d1/ad/afdd8351385edf0b3445f9e24210a9c3971ef4de8fd85155462fc4321d79/numpy-2.3.4-cp311-cp311-win_arm64.whl", hash = "sha256:4c01835e718bcebe80394fd0ac66c07cbb90147ebbdad3dcecd3f25de2ae7e2c", size = 10462292, upload-time = "2025-10-15T16:15:42.896Z" },
2184
- { url = "https://files.pythonhosted.org/packages/b1/b6/64898f51a86ec88ca1257a59c1d7fd077b60082a119affefcdf1dd0df8ca/numpy-2.3.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6e274603039f924c0fe5cb73438fa9246699c78a6df1bd3decef9ae592ae1c05", size = 21131552, upload-time = "2025-10-15T16:17:55.845Z" },
2185
- { url = "https://files.pythonhosted.org/packages/ce/4c/f135dc6ebe2b6a3c77f4e4838fa63d350f85c99462012306ada1bd4bc460/numpy-2.3.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d149aee5c72176d9ddbc6803aef9c0f6d2ceeea7626574fc68518da5476fa346", size = 14377796, upload-time = "2025-10-15T16:17:58.308Z" },
2186
- { url = "https://files.pythonhosted.org/packages/d0/a4/f33f9c23fcc13dd8412fc8614559b5b797e0aba9d8e01dfa8bae10c84004/numpy-2.3.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:6d34ed9db9e6395bb6cd33286035f73a59b058169733a9db9f85e650b88df37e", size = 5306904, upload-time = "2025-10-15T16:18:00.596Z" },
2187
- { url = "https://files.pythonhosted.org/packages/28/af/c44097f25f834360f9fb960fa082863e0bad14a42f36527b2a121abdec56/numpy-2.3.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:fdebe771ca06bb8d6abce84e51dca9f7921fe6ad34a0c914541b063e9a68928b", size = 6819682, upload-time = "2025-10-15T16:18:02.32Z" },
2188
- { url = "https://files.pythonhosted.org/packages/c5/8c/cd283b54c3c2b77e188f63e23039844f56b23bba1712318288c13fe86baf/numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e92defe6c08211eb77902253b14fe5b480ebc5112bc741fd5e9cd0608f847", size = 14422300, upload-time = "2025-10-15T16:18:04.271Z" },
2189
- { url = "https://files.pythonhosted.org/packages/b0/f0/8404db5098d92446b3e3695cf41c6f0ecb703d701cb0b7566ee2177f2eee/numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13b9062e4f5c7ee5c7e5be96f29ba71bc5a37fed3d1d77c37390ae00724d296d", size = 16760806, upload-time = "2025-10-15T16:18:06.668Z" },
2190
- { url = "https://files.pythonhosted.org/packages/95/8e/2844c3959ce9a63acc7c8e50881133d86666f0420bcde695e115ced0920f/numpy-2.3.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:81b3a59793523e552c4a96109dde028aa4448ae06ccac5a76ff6532a85558a7f", size = 12973130, upload-time = "2025-10-15T16:18:09.397Z" },
2191
  ]
2192
 
2193
  [[package]]
@@ -2266,6 +2522,15 @@ wheels = [
2266
  { url = "https://files.pythonhosted.org/packages/0f/dc/9484127cc1aa213be398ed735f5f270eedcb0c0977303a6f6ddc46b60204/orjson-3.11.4-cp311-cp311-win_arm64.whl", hash = "sha256:6bb6bb41b14c95d4f2702bce9975fda4516f1db48e500102fc4d8119032ff045", size = 126252, upload-time = "2025-10-24T15:49:08.869Z" },
2267
  ]
2268
 
 
 
 
 
 
 
 
 
 
2269
  [[package]]
2270
  name = "packaging"
2271
  version = "24.2"
@@ -2332,6 +2597,18 @@ wheels = [
2332
  { url = "https://files.pythonhosted.org/packages/9a/70/875f4a23bfc4731703a5835487d0d2fb999031bd415e7d17c0ae615c18b7/pathvalidate-3.3.1-py3-none-any.whl", hash = "sha256:5263baab691f8e1af96092fa5137ee17df5bdfbd6cff1fcac4d6ef4bc2e1735f", size = 24305, upload-time = "2025-06-15T09:07:19.117Z" },
2333
  ]
2334
 
 
 
 
 
 
 
 
 
 
 
 
 
2335
  [[package]]
2336
  name = "pexpect"
2337
  version = "4.9.0"
@@ -2388,6 +2665,19 @@ wheels = [
2388
  { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" },
2389
  ]
2390
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2391
  [[package]]
2392
  name = "pluggy"
2393
  version = "1.6.0"
@@ -2402,10 +2692,13 @@ name = "predicting-outcomes-in-heart-failure"
2402
  version = "0.0.1"
2403
  source = { editable = "." }
2404
  dependencies = [
 
2405
  { name = "asttokens" },
2406
  { name = "boto3" },
2407
  { name = "botocore" },
2408
  { name = "dagshub" },
 
 
2409
  { name = "dvc-s3" },
2410
  { name = "gradio" },
2411
  { name = "great-expectations" },
@@ -2421,7 +2714,10 @@ dependencies = [
2421
  { name = "numpy" },
2422
  { name = "pandas" },
2423
  { name = "pip" },
 
 
2424
  { name = "pytest" },
 
2425
  { name = "python-dotenv" },
2426
  { name = "ruff" },
2427
  { name = "scikit-learn" },
@@ -2439,10 +2735,13 @@ dev = [
2439
 
2440
  [package.metadata]
2441
  requires-dist = [
 
2442
  { name = "asttokens", specifier = ">=3.0.0" },
2443
  { name = "boto3", specifier = ">=1.36.0" },
2444
  { name = "botocore", specifier = ">=1.36.0" },
2445
  { name = "dagshub", specifier = ">=0.6.3" },
 
 
2446
  { name = "dvc-s3", specifier = ">=3.2.2" },
2447
  { name = "gradio", specifier = ">=6.0.2" },
2448
  { name = "great-expectations", specifier = ">=1.9.0" },
@@ -2455,15 +2754,18 @@ requires-dist = [
2455
  { name = "matplotlib", specifier = ">=3.10.7" },
2456
  { name = "mkdocs" },
2457
  { name = "mlflow", specifier = "==2.22.0" },
2458
- { name = "numpy", specifier = ">=2.3.4" },
2459
  { name = "pandas", specifier = ">=2.3.3" },
2460
  { name = "pip" },
 
 
2461
  { name = "pytest" },
 
2462
  { name = "python-dotenv" },
2463
  { name = "ruff" },
2464
  { name = "scikit-learn", specifier = ">=1.7.2" },
2465
  { name = "seaborn", specifier = ">=0.13.2" },
2466
- { name = "shap", specifier = ">=0.50.0" },
2467
  { name = "tqdm" },
2468
  { name = "typer" },
2469
  ]
@@ -2474,6 +2776,28 @@ dev = [
2474
  { name = "ruff", specifier = ">=0.14.2" },
2475
  ]
2476
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2477
  [[package]]
2478
  name = "prompt-toolkit"
2479
  version = "3.0.52"
@@ -2749,6 +3073,19 @@ wheels = [
2749
  { url = "https://files.pythonhosted.org/packages/86/30/9bcd030408ae80e3a516da13834065d667798a622309fb891d50e77d30d6/pynblint-0.1.6-py3-none-any.whl", hash = "sha256:8bb972696431144768ba6bf238a83f646c3faa4dac2810338ef87fb24d91742c", size = 24356, upload-time = "2024-08-12T08:53:20.164Z" },
2750
  ]
2751
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2752
  [[package]]
2753
  name = "pyparsing"
2754
  version = "3.2.5"
@@ -2774,6 +3111,32 @@ wheels = [
2774
  { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
2775
  ]
2776
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2777
  [[package]]
2778
  name = "python-dateutil"
2779
  version = "2.9.0.post0"
@@ -2795,6 +3158,15 @@ wheels = [
2795
  { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
2796
  ]
2797
 
 
 
 
 
 
 
 
 
 
2798
  [[package]]
2799
  name = "python-multipart"
2800
  version = "0.0.20"
@@ -2804,6 +3176,18 @@ wheels = [
2804
  { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" },
2805
  ]
2806
 
 
 
 
 
 
 
 
 
 
 
 
 
2807
  [[package]]
2808
  name = "pytz"
2809
  version = "2025.2"
@@ -2823,6 +3207,15 @@ wheels = [
2823
  { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" },
2824
  ]
2825
 
 
 
 
 
 
 
 
 
 
2826
  [[package]]
2827
  name = "pyyaml"
2828
  version = "6.0.3"
@@ -2929,6 +3322,39 @@ wheels = [
2929
  { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" },
2930
  ]
2931
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2932
  [[package]]
2933
  name = "rich"
2934
  version = "13.9.4"
@@ -3176,6 +3602,15 @@ wheels = [
3176
  { url = "https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746", size = 17912, upload-time = "2025-01-24T13:19:24.949Z" },
3177
  ]
3178
 
 
 
 
 
 
 
 
 
 
3179
  [[package]]
3180
  name = "setuptools"
3181
  version = "80.9.0"
@@ -3187,7 +3622,7 @@ wheels = [
3187
 
3188
  [[package]]
3189
  name = "shap"
3190
- version = "0.50.0"
3191
  source = { registry = "https://pypi.org/simple" }
3192
  dependencies = [
3193
  { name = "cloudpickle" },
@@ -3201,15 +3636,14 @@ dependencies = [
3201
  { name = "tqdm" },
3202
  { name = "typing-extensions" },
3203
  ]
3204
- sdist = { url = "https://files.pythonhosted.org/packages/2b/2c/9ccbfbdf5ceeb914317f9691ef1fca3118d4a997eb5e79bcd8992f56c938/shap-0.50.0.tar.gz", hash = "sha256:bdc559acf7f647bc3bb22c6a1fea9f50716ed357ad595bc357b43082ae4dc6b9", size = 4087800, upload-time = "2025-11-11T18:36:53.363Z" }
3205
  wheels = [
3206
- { url = "https://files.pythonhosted.org/packages/9b/ae/6231a667492887d775fddc0f248d48a9d6cc2cd981ea888217f0cd97acfa/shap-0.50.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:70a0e9c3b13a2b900ab4777a56d1e6cacddfd95f67cf382cdde24d376fbe13f4", size = 558401, upload-time = "2025-11-11T18:35:56.693Z" },
3207
- { url = "https://files.pythonhosted.org/packages/11/e0/e0f9916c5a709f06645187aa6431a6a3b707a66c551f5a8e87d5b5d2e01f/shap-0.50.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30f1fe9e75a948386d7a444e910bc472f2febf04d0f2175b5c03db9d5e0c2724", size = 556027, upload-time = "2025-11-11T18:35:58.766Z" },
3208
- { url = "https://files.pythonhosted.org/packages/91/91/962c63a6c9fc90210573b2194f773c7efcff1db12b2d13f6698ce90db483/shap-0.50.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a5ccc465c0f9e005dde341823b1cdad9dfcff6786f7d04e29606af6f2d51290", size = 1045372, upload-time = "2025-11-11T18:36:00.396Z" },
3209
- { url = "https://files.pythonhosted.org/packages/1b/c1/5166e6a382d6c2b83abd25b5c70dcd984ca37bedd304abc98e5ca9c6968c/shap-0.50.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e68f6129ddb59313da00d72843511352dc7e79512faec961644079504bbeb707", size = 1053133, upload-time = "2025-11-11T18:36:02.147Z" },
3210
- { url = "https://files.pythonhosted.org/packages/84/a6/fa0de80073efc607032a30edae53426baa1078d7c7d546ea2a8dd8c55408/shap-0.50.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8135bd24594f5fdf16f8fe5cbc2a81544c5c5c76f6b8a152eded16c361857da6", size = 2016590, upload-time = "2025-11-11T18:36:03.654Z" },
3211
- { url = "https://files.pythonhosted.org/packages/9b/4b/c5e104b11f04a08bd18730c5b75900fccac781d793557e9703f82254fbed/shap-0.50.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:807fc35ec82daba11da92d99607fbfc6471c211ec36b6329ae4611f355549bc8", size = 2086006, upload-time = "2025-11-11T18:36:05.822Z" },
3212
- { url = "https://files.pythonhosted.org/packages/77/03/58e199cf59056d68b4a227ce4b2b09eeb0c9bd1d002b9e28fb574eed6200/shap-0.50.0-cp311-cp311-win_amd64.whl", hash = "sha256:f5d0e2df7142393791c10a201947ca06972eb3c599c48a33697ef6c405413267", size = 547991, upload-time = "2025-11-11T18:36:07.402Z" },
3213
  ]
3214
 
3215
  [[package]]
@@ -3355,6 +3789,27 @@ wheels = [
3355
  { url = "https://files.pythonhosted.org/packages/51/da/545b75d420bb23b5d494b0517757b351963e974e79933f01e05c929f20a6/starlette-0.49.1-py3-none-any.whl", hash = "sha256:d92ce9f07e4a3caa3ac13a79523bd18e3bc0042bb8ff2d759a8e7dd0e1859875", size = 74175, upload-time = "2025-10-28T17:34:09.13Z" },
3356
  ]
3357
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3358
  [[package]]
3359
  name = "tabulate"
3360
  version = "0.9.0"
@@ -3373,6 +3828,20 @@ wheels = [
3373
  { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" },
3374
  ]
3375
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3376
  [[package]]
3377
  name = "threadpoolctl"
3378
  version = "3.6.0"
@@ -3547,6 +4016,15 @@ wheels = [
3547
  { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" },
3548
  ]
3549
 
 
 
 
 
 
 
 
 
 
3550
  [[package]]
3551
  name = "urllib3"
3552
  version = "2.5.0"
@@ -3626,6 +4104,15 @@ wheels = [
3626
  { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" },
3627
  ]
3628
 
 
 
 
 
 
 
 
 
 
3629
  [[package]]
3630
  name = "webencodings"
3631
  version = "0.5.1"
@@ -3635,6 +4122,15 @@ wheels = [
3635
  { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" },
3636
  ]
3637
 
 
 
 
 
 
 
 
 
 
3638
  [[package]]
3639
  name = "werkzeug"
3640
  version = "3.1.3"
@@ -3647,6 +4143,15 @@ wheels = [
3647
  { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498, upload-time = "2024-11-08T15:52:16.132Z" },
3648
  ]
3649
 
 
 
 
 
 
 
 
 
 
3650
  [[package]]
3651
  name = "win32-setctime"
3652
  version = "1.2.0"
 
214
  { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" },
215
  ]
216
 
217
+ [[package]]
218
+ name = "apscheduler"
219
+ version = "3.11.2"
220
+ source = { registry = "https://pypi.org/simple" }
221
+ dependencies = [
222
+ { name = "tzlocal" },
223
+ ]
224
+ sdist = { url = "https://files.pythonhosted.org/packages/07/12/3e4389e5920b4c1763390c6d371162f3784f86f85cd6d6c1bfe68eef14e2/apscheduler-3.11.2.tar.gz", hash = "sha256:2a9966b052ec805f020c8c4c3ae6e6a06e24b1bf19f2e11d91d8cca0473eef41", size = 108683, upload-time = "2025-12-22T00:39:34.884Z" }
225
+ wheels = [
226
+ { url = "https://files.pythonhosted.org/packages/9f/64/2e54428beba8d9992aa478bb8f6de9e4ecaa5f8f513bcfd567ed7fb0262d/apscheduler-3.11.2-py3-none-any.whl", hash = "sha256:ce005177f741409db4e4dd40a7431b76feb856b9dd69d57e0da49d6715bfd26d", size = 64439, upload-time = "2025-12-22T00:39:33.303Z" },
227
+ ]
228
+
229
+ [[package]]
230
+ name = "argon2-cffi"
231
+ version = "25.1.0"
232
+ source = { registry = "https://pypi.org/simple" }
233
+ dependencies = [
234
+ { name = "argon2-cffi-bindings" },
235
+ ]
236
+ sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" }
237
+ wheels = [
238
+ { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" },
239
+ ]
240
+
241
+ [[package]]
242
+ name = "argon2-cffi-bindings"
243
+ version = "25.1.0"
244
+ source = { registry = "https://pypi.org/simple" }
245
+ dependencies = [
246
+ { name = "cffi" },
247
+ ]
248
+ sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" }
249
+ wheels = [
250
+ { url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" },
251
+ { url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" },
252
+ { url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" },
253
+ { url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" },
254
+ { url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" },
255
+ { url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" },
256
+ { url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" },
257
+ { url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180, upload-time = "2025-07-30T10:01:57.759Z" },
258
+ { url = "https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715, upload-time = "2025-07-30T10:01:58.56Z" },
259
+ { url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" },
260
+ ]
261
+
262
+ [[package]]
263
+ name = "arrow"
264
+ version = "1.4.0"
265
+ source = { registry = "https://pypi.org/simple" }
266
+ dependencies = [
267
+ { name = "python-dateutil" },
268
+ { name = "tzdata" },
269
+ ]
270
+ sdist = { url = "https://files.pythonhosted.org/packages/b9/33/032cdc44182491aa708d06a68b62434140d8c50820a087fac7af37703357/arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7", size = 152931, upload-time = "2025-10-18T17:46:46.761Z" }
271
+ wheels = [
272
+ { url = "https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205", size = 68797, upload-time = "2025-10-18T17:46:45.663Z" },
273
+ ]
274
+
275
  [[package]]
276
  name = "asttokens"
277
  version = "3.0.0"
 
424
  { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080, upload-time = "2025-02-20T21:01:16.647Z" },
425
  ]
426
 
427
+ [[package]]
428
+ name = "category-encoders"
429
+ version = "2.9.0"
430
+ source = { registry = "https://pypi.org/simple" }
431
+ dependencies = [
432
+ { name = "numpy" },
433
+ { name = "pandas" },
434
+ { name = "patsy" },
435
+ { name = "scikit-learn" },
436
+ { name = "scipy" },
437
+ { name = "statsmodels" },
438
+ ]
439
+ sdist = { url = "https://files.pythonhosted.org/packages/29/59/1184ce74dca0c3e3450bccbb16edfce56f559c76dc794e2d52e1e63b467d/category_encoders-2.9.0.tar.gz", hash = "sha256:659311786e909013b8e8715fd1271244789a1dea278da44058828f88eeab5b40", size = 58005, upload-time = "2025-11-02T18:13:36.929Z" }
440
+ wheels = [
441
+ { url = "https://files.pythonhosted.org/packages/a6/06/afcae4dab08612dac244ace7f478543f4fb83bea94177231ef9b4f7bfa06/category_encoders-2.9.0-py3-none-any.whl", hash = "sha256:49c0e49cd3bd93b21c0bcc928ecbe9b3d09951a6f7fff8cc67f1f33967887227", size = 85859, upload-time = "2025-11-02T18:13:35.388Z" },
442
+ ]
443
+
444
  [[package]]
445
  name = "celery"
446
  version = "5.6.0"
 
791
  { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" },
792
  ]
793
 
794
+ [[package]]
795
+ name = "deepchecks"
796
+ version = "0.19.1"
797
+ source = { registry = "https://pypi.org/simple" }
798
+ dependencies = [
799
+ { name = "beautifulsoup4" },
800
+ { name = "category-encoders" },
801
+ { name = "ipykernel" },
802
+ { name = "ipython" },
803
+ { name = "ipywidgets" },
804
+ { name = "jsonpickle" },
805
+ { name = "jupyter-server" },
806
+ { name = "matplotlib" },
807
+ { name = "numpy" },
808
+ { name = "pandas" },
809
+ { name = "plotly" },
810
+ { name = "pynomaly" },
811
+ { name = "requests" },
812
+ { name = "scikit-learn" },
813
+ { name = "scipy" },
814
+ { name = "statsmodels" },
815
+ { name = "tqdm" },
816
+ { name = "typing-extensions" },
817
+ ]
818
+ sdist = { url = "https://files.pythonhosted.org/packages/ca/3b/a7154865564d939bd9eb1c6fd40132f3756aa6e66227dc4af5e47f224824/deepchecks-0.19.1.tar.gz", hash = "sha256:12d5602cc404c81050a47b5135b66c33322dd752f8e6cad4558add049292e763", size = 7463007, upload-time = "2024-12-15T15:25:33.973Z" }
819
+ wheels = [
820
+ { url = "https://files.pythonhosted.org/packages/93/33/5af4ad37a258db5aa60b73fda875f362ef29a7e4b6b5f7760367113aa9ee/deepchecks-0.19.1-py3-none-any.whl", hash = "sha256:839926b338aa76f97a0d1220fe0a9f7cf59c88de08fd06f228ef3aa9dd27abf4", size = 7815823, upload-time = "2024-12-15T15:25:31.176Z" },
821
+ ]
822
+
823
  [[package]]
824
  name = "defusedxml"
825
  version = "0.7.1"
 
950
  { url = "https://files.pythonhosted.org/packages/e1/e6/1782bcb8cdf82a971c1447d83f69efb3356945b68e28a28708fa4ddfa3c3/dvc-3.64.2-py3-none-any.whl", hash = "sha256:14e76baaef50cc10a43aaea788b49ea965835a59c16d0d63693a9ba1c2001090", size = 467981, upload-time = "2025-12-06T05:10:58.565Z" },
951
  ]
952
 
953
+ [package.optional-dependencies]
954
+ s3 = [
955
+ { name = "dvc-s3" },
956
+ ]
957
+
958
  [[package]]
959
  name = "dvc-data"
960
  version = "3.16.12"
 
1187
  { url = "https://files.pythonhosted.org/packages/c7/93/0dd45cd283c32dea1545151d8c3637b4b8c53cdb3a625aeb2885b184d74d/fonttools-4.60.1-py3-none-any.whl", hash = "sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb", size = 1143175, upload-time = "2025-09-29T21:13:24.134Z" },
1188
  ]
1189
 
1190
+ [[package]]
1191
+ name = "fqdn"
1192
+ version = "1.5.1"
1193
+ source = { registry = "https://pypi.org/simple" }
1194
+ sdist = { url = "https://files.pythonhosted.org/packages/30/3e/a80a8c077fd798951169626cde3e239adeba7dab75deb3555716415bd9b0/fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f", size = 6015, upload-time = "2021-03-11T07:16:29.08Z" }
1195
+ wheels = [
1196
+ { url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121, upload-time = "2021-03-11T07:16:28.351Z" },
1197
+ ]
1198
+
1199
  [[package]]
1200
  name = "frozenlist"
1201
  version = "1.8.0"
 
1688
  { url = "https://files.pythonhosted.org/packages/91/d0/274fbf7b0b12643cbbc001ce13e6a5b1607ac4929d1b11c72460152c9fc3/ipython-8.37.0-py3-none-any.whl", hash = "sha256:ed87326596b878932dbcb171e3e698845434d8c61b8d8cd474bf663041a9dcf2", size = 831864, upload-time = "2025-05-31T16:39:06.38Z" },
1689
  ]
1690
 
1691
+ [[package]]
1692
+ name = "ipywidgets"
1693
+ version = "8.1.8"
1694
+ source = { registry = "https://pypi.org/simple" }
1695
+ dependencies = [
1696
+ { name = "comm" },
1697
+ { name = "ipython" },
1698
+ { name = "jupyterlab-widgets" },
1699
+ { name = "traitlets" },
1700
+ { name = "widgetsnbextension" },
1701
+ ]
1702
+ sdist = { url = "https://files.pythonhosted.org/packages/4c/ae/c5ce1edc1afe042eadb445e95b0671b03cee61895264357956e61c0d2ac0/ipywidgets-8.1.8.tar.gz", hash = "sha256:61f969306b95f85fba6b6986b7fe45d73124d1d9e3023a8068710d47a22ea668", size = 116739, upload-time = "2025-11-01T21:18:12.393Z" }
1703
+ wheels = [
1704
+ { url = "https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl", hash = "sha256:ecaca67aed704a338f88f67b1181b58f821ab5dc89c1f0f5ef99db43c1c2921e", size = 139808, upload-time = "2025-11-01T21:18:10.956Z" },
1705
+ ]
1706
+
1707
+ [[package]]
1708
+ name = "isoduration"
1709
+ version = "20.11.0"
1710
+ source = { registry = "https://pypi.org/simple" }
1711
+ dependencies = [
1712
+ { name = "arrow" },
1713
+ ]
1714
+ sdist = { url = "https://files.pythonhosted.org/packages/7c/1a/3c8edc664e06e6bd06cce40c6b22da5f1429aa4224d0c590f3be21c91ead/isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9", size = 11649, upload-time = "2020-11-01T11:00:00.312Z" }
1715
+ wheels = [
1716
+ { url = "https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042", size = 11321, upload-time = "2020-11-01T10:59:58.02Z" },
1717
+ ]
1718
+
1719
  [[package]]
1720
  name = "iterative-telemetry"
1721
  version = "0.0.10"
 
1782
  { url = "https://files.pythonhosted.org/packages/1e/e8/685f47e0d754320684db4425a0967f7d3fa70126bffd76110b7009a0090f/joblib-1.5.2-py3-none-any.whl", hash = "sha256:4e1f0bdbb987e6d843c70cf43714cb276623def372df3c22fe5266b2670bc241", size = 308396, upload-time = "2025-08-27T12:15:45.188Z" },
1783
  ]
1784
 
1785
+ [[package]]
1786
+ name = "jsonpickle"
1787
+ version = "4.1.1"
1788
+ source = { registry = "https://pypi.org/simple" }
1789
+ sdist = { url = "https://files.pythonhosted.org/packages/e4/a6/d07afcfdef402900229bcca795f80506b207af13a838d4d99ad45abf530c/jsonpickle-4.1.1.tar.gz", hash = "sha256:f86e18f13e2b96c1c1eede0b7b90095bbb61d99fedc14813c44dc2f361dbbae1", size = 316885, upload-time = "2025-06-02T20:36:11.57Z" }
1790
+ wheels = [
1791
+ { url = "https://files.pythonhosted.org/packages/c1/73/04df8a6fa66d43a9fd45c30f283cc4afff17da671886e451d52af60bdc7e/jsonpickle-4.1.1-py3-none-any.whl", hash = "sha256:bb141da6057898aa2438ff268362b126826c812a1721e31cf08a6e142910dc91", size = 47125, upload-time = "2025-06-02T20:36:08.647Z" },
1792
+ ]
1793
+
1794
+ [[package]]
1795
+ name = "jsonpointer"
1796
+ version = "3.0.0"
1797
+ source = { registry = "https://pypi.org/simple" }
1798
+ sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" }
1799
+ wheels = [
1800
+ { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" },
1801
+ ]
1802
+
1803
  [[package]]
1804
  name = "jsonschema"
1805
  version = "4.25.1"
 
1815
  { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" },
1816
  ]
1817
 
1818
+ [package.optional-dependencies]
1819
+ format-nongpl = [
1820
+ { name = "fqdn" },
1821
+ { name = "idna" },
1822
+ { name = "isoduration" },
1823
+ { name = "jsonpointer" },
1824
+ { name = "rfc3339-validator" },
1825
+ { name = "rfc3986-validator" },
1826
+ { name = "rfc3987-syntax" },
1827
+ { name = "uri-template" },
1828
+ { name = "webcolors" },
1829
+ ]
1830
+
1831
  [[package]]
1832
  name = "jsonschema-specifications"
1833
  version = "2025.9.1"
 
1869
  { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" },
1870
  ]
1871
 
1872
+ [[package]]
1873
+ name = "jupyter-events"
1874
+ version = "0.12.0"
1875
+ source = { registry = "https://pypi.org/simple" }
1876
+ dependencies = [
1877
+ { name = "jsonschema", extra = ["format-nongpl"] },
1878
+ { name = "packaging" },
1879
+ { name = "python-json-logger" },
1880
+ { name = "pyyaml" },
1881
+ { name = "referencing" },
1882
+ { name = "rfc3339-validator" },
1883
+ { name = "rfc3986-validator" },
1884
+ { name = "traitlets" },
1885
+ ]
1886
+ sdist = { url = "https://files.pythonhosted.org/packages/9d/c3/306d090461e4cf3cd91eceaff84bede12a8e52cd821c2d20c9a4fd728385/jupyter_events-0.12.0.tar.gz", hash = "sha256:fc3fce98865f6784c9cd0a56a20644fc6098f21c8c33834a8d9fe383c17e554b", size = 62196, upload-time = "2025-02-03T17:23:41.485Z" }
1887
+ wheels = [
1888
+ { url = "https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl", hash = "sha256:6464b2fa5ad10451c3d35fabc75eab39556ae1e2853ad0c0cc31b656731a97fb", size = 19430, upload-time = "2025-02-03T17:23:38.643Z" },
1889
+ ]
1890
+
1891
+ [[package]]
1892
+ name = "jupyter-server"
1893
+ version = "2.17.0"
1894
+ source = { registry = "https://pypi.org/simple" }
1895
+ dependencies = [
1896
+ { name = "anyio" },
1897
+ { name = "argon2-cffi" },
1898
+ { name = "jinja2" },
1899
+ { name = "jupyter-client" },
1900
+ { name = "jupyter-core" },
1901
+ { name = "jupyter-events" },
1902
+ { name = "jupyter-server-terminals" },
1903
+ { name = "nbconvert" },
1904
+ { name = "nbformat" },
1905
+ { name = "overrides" },
1906
+ { name = "packaging" },
1907
+ { name = "prometheus-client" },
1908
+ { name = "pywinpty", marker = "os_name == 'nt'" },
1909
+ { name = "pyzmq" },
1910
+ { name = "send2trash" },
1911
+ { name = "terminado" },
1912
+ { name = "tornado" },
1913
+ { name = "traitlets" },
1914
+ { name = "websocket-client" },
1915
+ ]
1916
+ sdist = { url = "https://files.pythonhosted.org/packages/5b/ac/e040ec363d7b6b1f11304cc9f209dac4517ece5d5e01821366b924a64a50/jupyter_server-2.17.0.tar.gz", hash = "sha256:c38ea898566964c888b4772ae1ed58eca84592e88251d2cfc4d171f81f7e99d5", size = 731949, upload-time = "2025-08-21T14:42:54.042Z" }
1917
+ wheels = [
1918
+ { url = "https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl", hash = "sha256:e8cb9c7db4251f51ed307e329b81b72ccf2056ff82d50524debde1ee1870e13f", size = 388221, upload-time = "2025-08-21T14:42:52.034Z" },
1919
+ ]
1920
+
1921
+ [[package]]
1922
+ name = "jupyter-server-terminals"
1923
+ version = "0.5.3"
1924
+ source = { registry = "https://pypi.org/simple" }
1925
+ dependencies = [
1926
+ { name = "pywinpty", marker = "os_name == 'nt'" },
1927
+ { name = "terminado" },
1928
+ ]
1929
+ sdist = { url = "https://files.pythonhosted.org/packages/fc/d5/562469734f476159e99a55426d697cbf8e7eb5efe89fb0e0b4f83a3d3459/jupyter_server_terminals-0.5.3.tar.gz", hash = "sha256:5ae0295167220e9ace0edcfdb212afd2b01ee8d179fe6f23c899590e9b8a5269", size = 31430, upload-time = "2024-03-12T14:37:03.049Z" }
1930
+ wheels = [
1931
+ { url = "https://files.pythonhosted.org/packages/07/2d/2b32cdbe8d2a602f697a649798554e4f072115438e92249624e532e8aca6/jupyter_server_terminals-0.5.3-py3-none-any.whl", hash = "sha256:41ee0d7dc0ebf2809c668e0fc726dfaf258fcd3e769568996ca731b6194ae9aa", size = 13656, upload-time = "2024-03-12T14:37:00.708Z" },
1932
+ ]
1933
+
1934
  [[package]]
1935
  name = "jupyterlab-pygments"
1936
  version = "0.3.0"
 
1940
  { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884, upload-time = "2023-11-23T09:26:34.325Z" },
1941
  ]
1942
 
1943
+ [[package]]
1944
+ name = "jupyterlab-widgets"
1945
+ version = "3.0.16"
1946
+ source = { registry = "https://pypi.org/simple" }
1947
+ sdist = { url = "https://files.pythonhosted.org/packages/26/2d/ef58fed122b268c69c0aa099da20bc67657cdfb2e222688d5731bd5b971d/jupyterlab_widgets-3.0.16.tar.gz", hash = "sha256:423da05071d55cf27a9e602216d35a3a65a3e41cdf9c5d3b643b814ce38c19e0", size = 897423, upload-time = "2025-11-01T21:11:29.724Z" }
1948
+ wheels = [
1949
+ { url = "https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl", hash = "sha256:45fa36d9c6422cf2559198e4db481aa243c7a32d9926b500781c830c80f7ecf8", size = 914926, upload-time = "2025-11-01T21:11:28.008Z" },
1950
+ ]
1951
+
1952
  [[package]]
1953
  name = "kagglehub"
1954
  version = "0.3.13"
 
2005
  { url = "https://files.pythonhosted.org/packages/14/d6/943cf84117cd9ddecf6e1707a3f712a49fc64abdb8ac31b19132871af1dd/kombu-5.6.1-py3-none-any.whl", hash = "sha256:b69e3f5527ec32fc5196028a36376501682973e9620d6175d1c3d4eaf7e95409", size = 214141, upload-time = "2025-11-25T11:07:31.54Z" },
2006
  ]
2007
 
2008
+ [[package]]
2009
+ name = "lark"
2010
+ version = "1.3.1"
2011
+ source = { registry = "https://pypi.org/simple" }
2012
+ sdist = { url = "https://files.pythonhosted.org/packages/da/34/28fff3ab31ccff1fd4f6c7c7b0ceb2b6968d8ea4950663eadcb5720591a0/lark-1.3.1.tar.gz", hash = "sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905", size = 382732, upload-time = "2025-10-27T18:25:56.653Z" }
2013
+ wheels = [
2014
+ { url = "https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload-time = "2025-10-27T18:25:54.882Z" },
2015
+ ]
2016
+
2017
  [[package]]
2018
  name = "llvmlite"
2019
  version = "0.45.1"
 
2331
  { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
2332
  ]
2333
 
2334
+ [[package]]
2335
+ name = "narwhals"
2336
+ version = "2.14.0"
2337
+ source = { registry = "https://pypi.org/simple" }
2338
+ sdist = { url = "https://files.pythonhosted.org/packages/4a/84/897fe7b6406d436ef312e57e5a1a13b4a5e7e36d1844e8d934ce8880e3d3/narwhals-2.14.0.tar.gz", hash = "sha256:98be155c3599db4d5c211e565c3190c398c87e7bf5b3cdb157dece67641946e0", size = 600648, upload-time = "2025-12-16T11:29:13.458Z" }
2339
+ wheels = [
2340
+ { url = "https://files.pythonhosted.org/packages/79/3e/b8ecc67e178919671695f64374a7ba916cf0adbf86efedc6054f38b5b8ae/narwhals-2.14.0-py3-none-any.whl", hash = "sha256:b56796c9a00179bd757d15282c540024e1d5c910b19b8c9944d836566c030acf", size = 430788, upload-time = "2025-12-16T11:29:11.699Z" },
2341
+ ]
2342
+
2343
  [[package]]
2344
  name = "nbclient"
2345
  version = "0.10.2"
 
2432
 
2433
  [[package]]
2434
  name = "numpy"
2435
+ version = "1.26.4"
2436
+ source = { registry = "https://pypi.org/simple" }
2437
+ sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" }
2438
+ wheels = [
2439
+ { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554, upload-time = "2024-02-05T23:51:50.149Z" },
2440
+ { url = "https://files.pythonhosted.org/packages/1a/2e/151484f49fd03944c4a3ad9c418ed193cfd02724e138ac8a9505d056c582/numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", size = 13997127, upload-time = "2024-02-05T23:52:15.314Z" },
2441
+ { url = "https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", size = 14222994, upload-time = "2024-02-05T23:52:47.569Z" },
2442
+ { url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", size = 18252005, upload-time = "2024-02-05T23:53:15.637Z" },
2443
+ { url = "https://files.pythonhosted.org/packages/09/bf/2b1aaf8f525f2923ff6cfcf134ae5e750e279ac65ebf386c75a0cf6da06a/numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", size = 13885297, upload-time = "2024-02-05T23:53:42.16Z" },
2444
+ { url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567, upload-time = "2024-02-05T23:54:11.696Z" },
2445
+ { url = "https://files.pythonhosted.org/packages/d2/b7/a734c733286e10a7f1a8ad1ae8c90f2d33bf604a96548e0a4a3a6739b468/numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", size = 5968812, upload-time = "2024-02-05T23:54:26.453Z" },
2446
+ { url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913, upload-time = "2024-02-05T23:54:53.933Z" },
 
 
 
 
 
 
 
 
 
 
2447
  ]
2448
 
2449
  [[package]]
 
2522
  { url = "https://files.pythonhosted.org/packages/0f/dc/9484127cc1aa213be398ed735f5f270eedcb0c0977303a6f6ddc46b60204/orjson-3.11.4-cp311-cp311-win_arm64.whl", hash = "sha256:6bb6bb41b14c95d4f2702bce9975fda4516f1db48e500102fc4d8119032ff045", size = 126252, upload-time = "2025-10-24T15:49:08.869Z" },
2523
  ]
2524
 
2525
+ [[package]]
2526
+ name = "overrides"
2527
+ version = "7.7.0"
2528
+ source = { registry = "https://pypi.org/simple" }
2529
+ sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812, upload-time = "2024-01-27T21:01:33.423Z" }
2530
+ wheels = [
2531
+ { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832, upload-time = "2024-01-27T21:01:31.393Z" },
2532
+ ]
2533
+
2534
  [[package]]
2535
  name = "packaging"
2536
  version = "24.2"
 
2597
  { url = "https://files.pythonhosted.org/packages/9a/70/875f4a23bfc4731703a5835487d0d2fb999031bd415e7d17c0ae615c18b7/pathvalidate-3.3.1-py3-none-any.whl", hash = "sha256:5263baab691f8e1af96092fa5137ee17df5bdfbd6cff1fcac4d6ef4bc2e1735f", size = 24305, upload-time = "2025-06-15T09:07:19.117Z" },
2598
  ]
2599
 
2600
+ [[package]]
2601
+ name = "patsy"
2602
+ version = "1.0.2"
2603
+ source = { registry = "https://pypi.org/simple" }
2604
+ dependencies = [
2605
+ { name = "numpy" },
2606
+ ]
2607
+ sdist = { url = "https://files.pythonhosted.org/packages/be/44/ed13eccdd0519eff265f44b670d46fbb0ec813e2274932dc1c0e48520f7d/patsy-1.0.2.tar.gz", hash = "sha256:cdc995455f6233e90e22de72c37fcadb344e7586fb83f06696f54d92f8ce74c0", size = 399942, upload-time = "2025-10-20T16:17:37.535Z" }
2608
+ wheels = [
2609
+ { url = "https://files.pythonhosted.org/packages/f1/70/ba4b949bdc0490ab78d545459acd7702b211dfccf7eb89bbc1060f52818d/patsy-1.0.2-py2.py3-none-any.whl", hash = "sha256:37bfddbc58fcf0362febb5f54f10743f8b21dd2aa73dec7e7ef59d1b02ae668a", size = 233301, upload-time = "2025-10-20T16:17:36.563Z" },
2610
+ ]
2611
+
2612
  [[package]]
2613
  name = "pexpect"
2614
  version = "4.9.0"
 
2665
  { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" },
2666
  ]
2667
 
2668
+ [[package]]
2669
+ name = "plotly"
2670
+ version = "6.5.0"
2671
+ source = { registry = "https://pypi.org/simple" }
2672
+ dependencies = [
2673
+ { name = "narwhals" },
2674
+ { name = "packaging" },
2675
+ ]
2676
+ sdist = { url = "https://files.pythonhosted.org/packages/94/05/1199e2a03ce6637960bc1e951ca0f928209a48cfceb57355806a88f214cf/plotly-6.5.0.tar.gz", hash = "sha256:d5d38224883fd38c1409bef7d6a8dc32b74348d39313f3c52ca998b8e447f5c8", size = 7013624, upload-time = "2025-11-17T18:39:24.523Z" }
2677
+ wheels = [
2678
+ { url = "https://files.pythonhosted.org/packages/e7/c3/3031c931098de393393e1f93a38dc9ed6805d86bb801acc3cf2d5bd1e6b7/plotly-6.5.0-py3-none-any.whl", hash = "sha256:5ac851e100367735250206788a2b1325412aa4a4917a4fe3e6f0bc5aa6f3d90a", size = 9893174, upload-time = "2025-11-17T18:39:20.351Z" },
2679
+ ]
2680
+
2681
  [[package]]
2682
  name = "pluggy"
2683
  version = "1.6.0"
 
2692
  version = "0.0.1"
2693
  source = { editable = "." }
2694
  dependencies = [
2695
+ { name = "apscheduler" },
2696
  { name = "asttokens" },
2697
  { name = "boto3" },
2698
  { name = "botocore" },
2699
  { name = "dagshub" },
2700
+ { name = "deepchecks" },
2701
+ { name = "dvc", extra = ["s3"] },
2702
  { name = "dvc-s3" },
2703
  { name = "gradio" },
2704
  { name = "great-expectations" },
 
2714
  { name = "numpy" },
2715
  { name = "pandas" },
2716
  { name = "pip" },
2717
+ { name = "prometheus-client" },
2718
+ { name = "prometheus-fastapi-instrumentator" },
2719
  { name = "pytest" },
2720
+ { name = "pytest-html" },
2721
  { name = "python-dotenv" },
2722
  { name = "ruff" },
2723
  { name = "scikit-learn" },
 
2735
 
2736
  [package.metadata]
2737
  requires-dist = [
2738
+ { name = "apscheduler", specifier = ">=3.11.2" },
2739
  { name = "asttokens", specifier = ">=3.0.0" },
2740
  { name = "boto3", specifier = ">=1.36.0" },
2741
  { name = "botocore", specifier = ">=1.36.0" },
2742
  { name = "dagshub", specifier = ">=0.6.3" },
2743
+ { name = "deepchecks", specifier = ">=0.19.1" },
2744
+ { name = "dvc", extras = ["s3"], specifier = ">=3.64.2" },
2745
  { name = "dvc-s3", specifier = ">=3.2.2" },
2746
  { name = "gradio", specifier = ">=6.0.2" },
2747
  { name = "great-expectations", specifier = ">=1.9.0" },
 
2754
  { name = "matplotlib", specifier = ">=3.10.7" },
2755
  { name = "mkdocs" },
2756
  { name = "mlflow", specifier = "==2.22.0" },
2757
+ { name = "numpy", specifier = "<2" },
2758
  { name = "pandas", specifier = ">=2.3.3" },
2759
  { name = "pip" },
2760
+ { name = "prometheus-client", specifier = ">=0.23.1" },
2761
+ { name = "prometheus-fastapi-instrumentator", specifier = ">=7.1.0" },
2762
  { name = "pytest" },
2763
+ { name = "pytest-html", specifier = ">=4.1.1" },
2764
  { name = "python-dotenv" },
2765
  { name = "ruff" },
2766
  { name = "scikit-learn", specifier = ">=1.7.2" },
2767
  { name = "seaborn", specifier = ">=0.13.2" },
2768
+ { name = "shap", specifier = "<0.50" },
2769
  { name = "tqdm" },
2770
  { name = "typer" },
2771
  ]
 
2776
  { name = "ruff", specifier = ">=0.14.2" },
2777
  ]
2778
 
2779
+ [[package]]
2780
+ name = "prometheus-client"
2781
+ version = "0.23.1"
2782
+ source = { registry = "https://pypi.org/simple" }
2783
+ sdist = { url = "https://files.pythonhosted.org/packages/23/53/3edb5d68ecf6b38fcbcc1ad28391117d2a322d9a1a3eff04bfdb184d8c3b/prometheus_client-0.23.1.tar.gz", hash = "sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce", size = 80481, upload-time = "2025-09-18T20:47:25.043Z" }
2784
+ wheels = [
2785
+ { url = "https://files.pythonhosted.org/packages/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl", hash = "sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99", size = 61145, upload-time = "2025-09-18T20:47:23.875Z" },
2786
+ ]
2787
+
2788
+ [[package]]
2789
+ name = "prometheus-fastapi-instrumentator"
2790
+ version = "7.1.0"
2791
+ source = { registry = "https://pypi.org/simple" }
2792
+ dependencies = [
2793
+ { name = "prometheus-client" },
2794
+ { name = "starlette" },
2795
+ ]
2796
+ sdist = { url = "https://files.pythonhosted.org/packages/69/6d/24d53033cf93826aa7857699a4450c1c67e5b9c710e925b1ed2b320c04df/prometheus_fastapi_instrumentator-7.1.0.tar.gz", hash = "sha256:be7cd61eeea4e5912aeccb4261c6631b3f227d8924542d79eaf5af3f439cbe5e", size = 20220, upload-time = "2025-03-19T19:35:05.351Z" }
2797
+ wheels = [
2798
+ { url = "https://files.pythonhosted.org/packages/27/72/0824c18f3bc75810f55dacc2dd933f6ec829771180245ae3cc976195dec0/prometheus_fastapi_instrumentator-7.1.0-py3-none-any.whl", hash = "sha256:978130f3c0bb7b8ebcc90d35516a6fe13e02d2eb358c8f83887cdef7020c31e9", size = 19296, upload-time = "2025-03-19T19:35:04.323Z" },
2799
+ ]
2800
+
2801
  [[package]]
2802
  name = "prompt-toolkit"
2803
  version = "3.0.52"
 
3073
  { url = "https://files.pythonhosted.org/packages/86/30/9bcd030408ae80e3a516da13834065d667798a622309fb891d50e77d30d6/pynblint-0.1.6-py3-none-any.whl", hash = "sha256:8bb972696431144768ba6bf238a83f646c3faa4dac2810338ef87fb24d91742c", size = 24356, upload-time = "2024-08-12T08:53:20.164Z" },
3074
  ]
3075
 
3076
+ [[package]]
3077
+ name = "pynomaly"
3078
+ version = "0.3.4"
3079
+ source = { registry = "https://pypi.org/simple" }
3080
+ dependencies = [
3081
+ { name = "numpy" },
3082
+ { name = "python-utils" },
3083
+ ]
3084
+ sdist = { url = "https://files.pythonhosted.org/packages/bb/6c/bb98854999794f1d11d08885f19a740bf6650ce6f2bfd4db6235311d4ad3/PyNomaly-0.3.4.tar.gz", hash = "sha256:90c5e3b58fa3dc144cdcb145afdecb212131e269a5be67173293e18acdd73c6f", size = 8374, upload-time = "2024-10-18T15:46:06.537Z" }
3085
+ wheels = [
3086
+ { url = "https://files.pythonhosted.org/packages/03/00/0af76dc221eb34f0d98af54024955ddc16080057a69d4c19329811aabc03/PyNomaly-0.3.4-py3-none-any.whl", hash = "sha256:e653f1e1c9a70c92170829f7440841e3b004528e1a1bfaea5e830e53fca26822", size = 9149, upload-time = "2024-10-18T15:45:30.207Z" },
3087
+ ]
3088
+
3089
  [[package]]
3090
  name = "pyparsing"
3091
  version = "3.2.5"
 
3111
  { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
3112
  ]
3113
 
3114
+ [[package]]
3115
+ name = "pytest-html"
3116
+ version = "4.1.1"
3117
+ source = { registry = "https://pypi.org/simple" }
3118
+ dependencies = [
3119
+ { name = "jinja2" },
3120
+ { name = "pytest" },
3121
+ { name = "pytest-metadata" },
3122
+ ]
3123
+ sdist = { url = "https://files.pythonhosted.org/packages/bb/ab/4862dcb5a8a514bd87747e06b8d55483c0c9e987e1b66972336946e49b49/pytest_html-4.1.1.tar.gz", hash = "sha256:70a01e8ae5800f4a074b56a4cb1025c8f4f9b038bba5fe31e3c98eb996686f07", size = 150773, upload-time = "2023-11-07T15:44:28.975Z" }
3124
+ wheels = [
3125
+ { url = "https://files.pythonhosted.org/packages/c8/c7/c160021cbecd956cc1a6f79e5fe155f7868b2e5b848f1320dad0b3e3122f/pytest_html-4.1.1-py3-none-any.whl", hash = "sha256:c8152cea03bd4e9bee6d525573b67bbc6622967b72b9628dda0ea3e2a0b5dd71", size = 23491, upload-time = "2023-11-07T15:44:27.149Z" },
3126
+ ]
3127
+
3128
+ [[package]]
3129
+ name = "pytest-metadata"
3130
+ version = "3.1.1"
3131
+ source = { registry = "https://pypi.org/simple" }
3132
+ dependencies = [
3133
+ { name = "pytest" },
3134
+ ]
3135
+ sdist = { url = "https://files.pythonhosted.org/packages/a6/85/8c969f8bec4e559f8f2b958a15229a35495f5b4ce499f6b865eac54b878d/pytest_metadata-3.1.1.tar.gz", hash = "sha256:d2a29b0355fbc03f168aa96d41ff88b1a3b44a3b02acbe491801c98a048017c8", size = 9952, upload-time = "2024-02-12T19:38:44.887Z" }
3136
+ wheels = [
3137
+ { url = "https://files.pythonhosted.org/packages/3e/43/7e7b2ec865caa92f67b8f0e9231a798d102724ca4c0e1f414316be1c1ef2/pytest_metadata-3.1.1-py3-none-any.whl", hash = "sha256:c8e0844db684ee1c798cfa38908d20d67d0463ecb6137c72e91f418558dd5f4b", size = 11428, upload-time = "2024-02-12T19:38:42.531Z" },
3138
+ ]
3139
+
3140
  [[package]]
3141
  name = "python-dateutil"
3142
  version = "2.9.0.post0"
 
3158
  { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
3159
  ]
3160
 
3161
+ [[package]]
3162
+ name = "python-json-logger"
3163
+ version = "4.0.0"
3164
+ source = { registry = "https://pypi.org/simple" }
3165
+ sdist = { url = "https://files.pythonhosted.org/packages/29/bf/eca6a3d43db1dae7070f70e160ab20b807627ba953663ba07928cdd3dc58/python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f", size = 17683, upload-time = "2025-10-06T04:15:18.984Z" }
3166
+ wheels = [
3167
+ { url = "https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548, upload-time = "2025-10-06T04:15:17.553Z" },
3168
+ ]
3169
+
3170
  [[package]]
3171
  name = "python-multipart"
3172
  version = "0.0.20"
 
3176
  { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" },
3177
  ]
3178
 
3179
+ [[package]]
3180
+ name = "python-utils"
3181
+ version = "3.9.1"
3182
+ source = { registry = "https://pypi.org/simple" }
3183
+ dependencies = [
3184
+ { name = "typing-extensions" },
3185
+ ]
3186
+ sdist = { url = "https://files.pythonhosted.org/packages/13/4c/ef8b7b1046d65c1f18ca31e5235c7d6627ca2b3f389ab1d44a74d22f5cc9/python_utils-3.9.1.tar.gz", hash = "sha256:eb574b4292415eb230f094cbf50ab5ef36e3579b8f09e9f2ba74af70891449a0", size = 35403, upload-time = "2024-11-26T00:38:58.736Z" }
3187
+ wheels = [
3188
+ { url = "https://files.pythonhosted.org/packages/d4/69/31c82567719b34d8f6b41077732589104883771d182a9f4ff3e71430999a/python_utils-3.9.1-py2.py3-none-any.whl", hash = "sha256:0273d7363c7ad4b70999b2791d5ba6b55333d6f7a4e4c8b6b39fb82b5fab4613", size = 32078, upload-time = "2024-11-26T00:38:57.488Z" },
3189
+ ]
3190
+
3191
  [[package]]
3192
  name = "pytz"
3193
  version = "2025.2"
 
3207
  { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" },
3208
  ]
3209
 
3210
+ [[package]]
3211
+ name = "pywinpty"
3212
+ version = "3.0.2"
3213
+ source = { registry = "https://pypi.org/simple" }
3214
+ sdist = { url = "https://files.pythonhosted.org/packages/f3/bb/a7cc2967c5c4eceb6cc49cfe39447d4bfc56e6c865e7c2249b6eb978935f/pywinpty-3.0.2.tar.gz", hash = "sha256:1505cc4cb248af42cb6285a65c9c2086ee9e7e574078ee60933d5d7fa86fb004", size = 30669, upload-time = "2025-10-03T21:16:29.205Z" }
3215
+ wheels = [
3216
+ { url = "https://files.pythonhosted.org/packages/a6/a1/409c1651c9f874d598c10f51ff586c416625601df4bca315d08baec4c3e3/pywinpty-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:327790d70e4c841ebd9d0f295a780177149aeb405bca44c7115a3de5c2054b23", size = 2050304, upload-time = "2025-10-03T21:19:29.466Z" },
3217
+ ]
3218
+
3219
  [[package]]
3220
  name = "pyyaml"
3221
  version = "6.0.3"
 
3322
  { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" },
3323
  ]
3324
 
3325
+ [[package]]
3326
+ name = "rfc3339-validator"
3327
+ version = "0.1.4"
3328
+ source = { registry = "https://pypi.org/simple" }
3329
+ dependencies = [
3330
+ { name = "six" },
3331
+ ]
3332
+ sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" }
3333
+ wheels = [
3334
+ { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" },
3335
+ ]
3336
+
3337
+ [[package]]
3338
+ name = "rfc3986-validator"
3339
+ version = "0.1.1"
3340
+ source = { registry = "https://pypi.org/simple" }
3341
+ sdist = { url = "https://files.pythonhosted.org/packages/da/88/f270de456dd7d11dcc808abfa291ecdd3f45ff44e3b549ffa01b126464d0/rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055", size = 6760, upload-time = "2019-10-28T16:00:19.144Z" }
3342
+ wheels = [
3343
+ { url = "https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9", size = 4242, upload-time = "2019-10-28T16:00:13.976Z" },
3344
+ ]
3345
+
3346
+ [[package]]
3347
+ name = "rfc3987-syntax"
3348
+ version = "1.1.0"
3349
+ source = { registry = "https://pypi.org/simple" }
3350
+ dependencies = [
3351
+ { name = "lark" },
3352
+ ]
3353
+ sdist = { url = "https://files.pythonhosted.org/packages/2c/06/37c1a5557acf449e8e406a830a05bf885ac47d33270aec454ef78675008d/rfc3987_syntax-1.1.0.tar.gz", hash = "sha256:717a62cbf33cffdd16dfa3a497d81ce48a660ea691b1ddd7be710c22f00b4a0d", size = 14239, upload-time = "2025-07-18T01:05:05.015Z" }
3354
+ wheels = [
3355
+ { url = "https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f", size = 8046, upload-time = "2025-07-18T01:05:03.843Z" },
3356
+ ]
3357
+
3358
  [[package]]
3359
  name = "rich"
3360
  version = "13.9.4"
 
3602
  { url = "https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746", size = 17912, upload-time = "2025-01-24T13:19:24.949Z" },
3603
  ]
3604
 
3605
+ [[package]]
3606
+ name = "send2trash"
3607
+ version = "2.0.0"
3608
+ source = { registry = "https://pypi.org/simple" }
3609
+ sdist = { url = "https://files.pythonhosted.org/packages/62/6e/421803dec0c0dfbf5a27e66491ebe6643a461e4f90422f00ea4c68ae24aa/send2trash-2.0.0.tar.gz", hash = "sha256:1761421da3f9930bfe51ed7c45343948573383ad4c27e3acebc91be324e7770d", size = 17206, upload-time = "2025-12-31T04:12:48.664Z" }
3610
+ wheels = [
3611
+ { url = "https://files.pythonhosted.org/packages/1b/5a/f2f2e5eda25579f754acd83399c522ee03d6acbe001dfe53c8a1ec928b44/send2trash-2.0.0-py3-none-any.whl", hash = "sha256:e70d5ce41dbb890882cc78bc25d137478330b39a391e756fadf82e34da4d85b8", size = 17642, upload-time = "2025-12-31T04:12:45.336Z" },
3612
+ ]
3613
+
3614
  [[package]]
3615
  name = "setuptools"
3616
  version = "80.9.0"
 
3622
 
3623
  [[package]]
3624
  name = "shap"
3625
+ version = "0.49.1"
3626
  source = { registry = "https://pypi.org/simple" }
3627
  dependencies = [
3628
  { name = "cloudpickle" },
 
3636
  { name = "tqdm" },
3637
  { name = "typing-extensions" },
3638
  ]
3639
+ sdist = { url = "https://files.pythonhosted.org/packages/dc/c6/9823a7f483aa9f3179fc359c10d22da9e418b1a7a3fc99a42b705d05e82a/shap-0.49.1.tar.gz", hash = "sha256:1114ecd804fff29f50d522ce6031082fcf42fe4a32fb1b5da233b2415d784c8c", size = 4084725, upload-time = "2025-10-14T10:04:49.75Z" }
3640
  wheels = [
3641
+ { url = "https://files.pythonhosted.org/packages/1d/08/d433b7d18a8b51a7d10477120f78877d806d2eb86283cb1661318d865f3d/shap-0.49.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1e208a0129c721bd0eba6268a9ffac4610dbc8a833d07d2ad9f39541bb737f06", size = 558742, upload-time = "2025-10-14T10:04:17.45Z" },
3642
+ { url = "https://files.pythonhosted.org/packages/c2/35/72929fdad25e055aff9dfbeb48c044682fc3b815d90cee4036b90bd65f4c/shap-0.49.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0b878470bdf6800069c25d2a8598eb0548aa1e6826becd39cca253521cc14866", size = 556486, upload-time = "2025-10-14T10:04:18.934Z" },
3643
+ { url = "https://files.pythonhosted.org/packages/02/be/d92623be2c584784e99a8eb9a6cd02263b4eb363c9e49fa14c20f824bcbb/shap-0.49.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:118577d40c53f005268024e59f6a10cbcafbb6d03b3d97dce7c0c7510190ebaa", size = 1025978, upload-time = "2025-10-14T10:04:20.096Z" },
3644
+ { url = "https://files.pythonhosted.org/packages/14/e9/e4079b5de26a8269121ce38125e130c147dac7b59611e0bd94be10f9444e/shap-0.49.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f424465699aa2dda8057656c6b6d3cb927cf29b054c5bb01cfffcb9efa5dbf98", size = 1027831, upload-time = "2025-10-14T10:04:21.666Z" },
3645
+ { url = "https://files.pythonhosted.org/packages/49/ff/e22e1d899ed56384a2395d6121d6e21833c518c01c5b6c52fce3c0b0cbab/shap-0.49.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d505834fdf2a159e88b1dcdeddfd79f101fd789ba89d589faf0aaec060c0bad9", size = 2092627, upload-time = "2025-10-14T10:04:22.894Z" },
3646
+ { url = "https://files.pythonhosted.org/packages/17/48/bbcd638a391ac0fb30033398a3cca60ba5c36941d962dd74958e67069108/shap-0.49.1-cp311-cp311-win_amd64.whl", hash = "sha256:897c7e6fa98d66482282c8f898c97ade181d714ecaf581da0dab5c49adb9f62c", size = 546845, upload-time = "2025-10-14T10:04:24.238Z" },
 
3647
  ]
3648
 
3649
  [[package]]
 
3789
  { url = "https://files.pythonhosted.org/packages/51/da/545b75d420bb23b5d494b0517757b351963e974e79933f01e05c929f20a6/starlette-0.49.1-py3-none-any.whl", hash = "sha256:d92ce9f07e4a3caa3ac13a79523bd18e3bc0042bb8ff2d759a8e7dd0e1859875", size = 74175, upload-time = "2025-10-28T17:34:09.13Z" },
3790
  ]
3791
 
3792
+ [[package]]
3793
+ name = "statsmodels"
3794
+ version = "0.14.6"
3795
+ source = { registry = "https://pypi.org/simple" }
3796
+ dependencies = [
3797
+ { name = "numpy" },
3798
+ { name = "packaging" },
3799
+ { name = "pandas" },
3800
+ { name = "patsy" },
3801
+ { name = "scipy" },
3802
+ ]
3803
+ sdist = { url = "https://files.pythonhosted.org/packages/0d/81/e8d74b34f85285f7335d30c5e3c2d7c0346997af9f3debf9a0a9a63de184/statsmodels-0.14.6.tar.gz", hash = "sha256:4d17873d3e607d398b85126cd4ed7aad89e4e9d89fc744cdab1af3189a996c2a", size = 20689085, upload-time = "2025-12-05T23:08:39.522Z" }
3804
+ wheels = [
3805
+ { url = "https://files.pythonhosted.org/packages/a9/4d/df4dd089b406accfc3bb5ee53ba29bb3bdf5ae61643f86f8f604baa57656/statsmodels-0.14.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6ad5c2810fc6c684254a7792bf1cbaf1606cdee2a253f8bd259c43135d87cfb4", size = 10121514, upload-time = "2025-12-05T19:28:16.521Z" },
3806
+ { url = "https://files.pythonhosted.org/packages/82/af/ec48daa7f861f993b91a0dcc791d66e1cf56510a235c5cbd2ab991a31d5c/statsmodels-0.14.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:341fa68a7403e10a95c7b6e41134b0da3a7b835ecff1eb266294408535a06eb6", size = 10003346, upload-time = "2025-12-05T19:28:29.568Z" },
3807
+ { url = "https://files.pythonhosted.org/packages/a9/2c/c8f7aa24cd729970728f3f98822fb45149adc216f445a9301e441f7ac760/statsmodels-0.14.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdf1dfe2a3ca56f5529118baf33a13efed2783c528f4a36409b46bbd2d9d48eb", size = 10129872, upload-time = "2025-12-05T23:09:25.724Z" },
3808
+ { url = "https://files.pythonhosted.org/packages/40/c6/9ae8e9b0721e9b6eb5f340c3a0ce8cd7cce4f66e03dd81f80d60f111987f/statsmodels-0.14.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3764ba8195c9baf0925a96da0743ff218067a269f01d155ca3558deed2658ca", size = 10381964, upload-time = "2025-12-05T23:09:41.326Z" },
3809
+ { url = "https://files.pythonhosted.org/packages/28/8c/cf3d30c8c2da78e2ad1f50ade8b7fabec3ff4cdfc56fbc02e097c4577f90/statsmodels-0.14.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9e8d2e519852adb1b420e018f5ac6e6684b2b877478adf7fda2cfdb58f5acb5d", size = 10409611, upload-time = "2025-12-05T23:09:57.131Z" },
3810
+ { url = "https://files.pythonhosted.org/packages/bf/cc/018f14ecb58c6cb89de9d52695740b7d1f5a982aa9ea312483ea3c3d5f77/statsmodels-0.14.6-cp311-cp311-win_amd64.whl", hash = "sha256:2738a00fca51196f5a7d44b06970ace6b8b30289839e4808d656f8a98e35faa7", size = 9580385, upload-time = "2025-12-05T19:28:42.778Z" },
3811
+ ]
3812
+
3813
  [[package]]
3814
  name = "tabulate"
3815
  version = "0.9.0"
 
3828
  { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" },
3829
  ]
3830
 
3831
+ [[package]]
3832
+ name = "terminado"
3833
+ version = "0.18.1"
3834
+ source = { registry = "https://pypi.org/simple" }
3835
+ dependencies = [
3836
+ { name = "ptyprocess", marker = "os_name != 'nt'" },
3837
+ { name = "pywinpty", marker = "os_name == 'nt'" },
3838
+ { name = "tornado" },
3839
+ ]
3840
+ sdist = { url = "https://files.pythonhosted.org/packages/8a/11/965c6fd8e5cc254f1fe142d547387da17a8ebfd75a3455f637c663fb38a0/terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e", size = 32701, upload-time = "2024-03-12T14:34:39.026Z" }
3841
+ wheels = [
3842
+ { url = "https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0", size = 14154, upload-time = "2024-03-12T14:34:36.569Z" },
3843
+ ]
3844
+
3845
  [[package]]
3846
  name = "threadpoolctl"
3847
  version = "3.6.0"
 
4016
  { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" },
4017
  ]
4018
 
4019
+ [[package]]
4020
+ name = "uri-template"
4021
+ version = "1.3.0"
4022
+ source = { registry = "https://pypi.org/simple" }
4023
+ sdist = { url = "https://files.pythonhosted.org/packages/31/c7/0336f2bd0bcbada6ccef7aaa25e443c118a704f828a0620c6fa0207c1b64/uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7", size = 21678, upload-time = "2023-06-21T01:49:05.374Z" }
4024
+ wheels = [
4025
+ { url = "https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363", size = 11140, upload-time = "2023-06-21T01:49:03.467Z" },
4026
+ ]
4027
+
4028
  [[package]]
4029
  name = "urllib3"
4030
  version = "2.5.0"
 
4104
  { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" },
4105
  ]
4106
 
4107
+ [[package]]
4108
+ name = "webcolors"
4109
+ version = "25.10.0"
4110
+ source = { registry = "https://pypi.org/simple" }
4111
+ sdist = { url = "https://files.pythonhosted.org/packages/1d/7a/eb316761ec35664ea5174709a68bbd3389de60d4a1ebab8808bfc264ed67/webcolors-25.10.0.tar.gz", hash = "sha256:62abae86504f66d0f6364c2a8520de4a0c47b80c03fc3a5f1815fedbef7c19bf", size = 53491, upload-time = "2025-10-31T07:51:03.977Z" }
4112
+ wheels = [
4113
+ { url = "https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl", hash = "sha256:032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d", size = 14905, upload-time = "2025-10-31T07:51:01.778Z" },
4114
+ ]
4115
+
4116
  [[package]]
4117
  name = "webencodings"
4118
  version = "0.5.1"
 
4122
  { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" },
4123
  ]
4124
 
4125
+ [[package]]
4126
+ name = "websocket-client"
4127
+ version = "1.9.0"
4128
+ source = { registry = "https://pypi.org/simple" }
4129
+ sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" }
4130
+ wheels = [
4131
+ { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" },
4132
+ ]
4133
+
4134
  [[package]]
4135
  name = "werkzeug"
4136
  version = "3.1.3"
 
4143
  { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498, upload-time = "2024-11-08T15:52:16.132Z" },
4144
  ]
4145
 
4146
+ [[package]]
4147
+ name = "widgetsnbextension"
4148
+ version = "4.0.15"
4149
+ source = { registry = "https://pypi.org/simple" }
4150
+ sdist = { url = "https://files.pythonhosted.org/packages/bd/f4/c67440c7fb409a71b7404b7aefcd7569a9c0d6bd071299bf4198ae7a5d95/widgetsnbextension-4.0.15.tar.gz", hash = "sha256:de8610639996f1567952d763a5a41af8af37f2575a41f9852a38f947eb82a3b9", size = 1097402, upload-time = "2025-11-01T21:15:55.178Z" }
4151
+ wheels = [
4152
+ { url = "https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl", hash = "sha256:8156704e4346a571d9ce73b84bee86a29906c9abfd7223b7228a28899ccf3366", size = 2196503, upload-time = "2025-11-01T21:15:53.565Z" },
4153
+ ]
4154
+
4155
  [[package]]
4156
  name = "win32-setctime"
4157
  version = "1.2.0"