From 5560a108cdc18f4ff83313882eabf18f787a264f Mon Sep 17 00:00:00 2001 From: Michael Van de Steene <124588413+michael-nml@users.noreply.github.com> Date: Wed, 28 Jun 2023 13:24:56 +0200 Subject: [PATCH 01/18] Enable invalidating stored calculators in runner (#312) --- nannyml/config.py | 1 + nannyml/runner.py | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/nannyml/config.py b/nannyml/config.py index a1103b42..adc43b45 100644 --- a/nannyml/config.py +++ b/nannyml/config.py @@ -66,6 +66,7 @@ class StoreConfig(BaseModel): path: str credentials: Optional[Dict[str, Any]] filename: Optional[str] + invalidate: bool = False class CalculatorConfig(BaseModel): diff --git a/nannyml/runner.py b/nannyml/runner.py index d014eeae..3f6f928a 100644 --- a/nannyml/runner.py +++ b/nannyml/runner.py @@ -149,10 +149,15 @@ def run( # noqa: C901 f"[{context.current_step}/{context.total_steps}] '{context.current_calculator}': " f"loading calculator from store" ) - calc = store.load(filename=calculator_config.store.filename, as_type=calc_cls) + if calculator_config.store.invalidate: + calc = None + else: + calc = store.load(filename=calculator_config.store.filename, as_type=calc_cls) + if calc is None: + reason = 'invalidated' if calculator_config.store.invalidate else 'not found in store' run_logger.log( - f"calculator '{context.current_calculator}' not found in store. " + f"calculator '{context.current_calculator}' {reason}. " f"Creating, fitting and storing new instance", log_level=logging.DEBUG, ) From e400726b14a04055a4a755213555b779adb37575 Mon Sep 17 00:00:00 2001 From: Niels Nuyttens Date: Mon, 3 Jul 2023 14:34:43 +0200 Subject: [PATCH 02/18] Add kubernetes runtime environment check in usage logging Signed-off-by: Niels Nuyttens --- nannyml/usage_logging.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nannyml/usage_logging.py b/nannyml/usage_logging.py index 54bd52ef..9c915d81 100644 --- a/nannyml/usage_logging.py +++ b/nannyml/usage_logging.py @@ -266,6 +266,10 @@ def _is_running_in_docker(): return False +def is_running_in_kubernetes(): + return Path('/var/run/secrets/kubernetes.io/').exists() + + # Inspired by # https://github.com/zenml-io/zenml/blob/275109da08b783d5d2cd508b5f703aed0c66e485/src/zenml/environment.py#L182 # and https://stackoverflow.com/a/39662359 From c0766609cfd1fa9c6e95f57c8c6e30affc7a37d1 Mon Sep 17 00:00:00 2001 From: Niels Nuyttens Date: Mon, 3 Jul 2023 15:00:51 +0200 Subject: [PATCH 03/18] Add kubernetes runtime environment check in usage logging Signed-off-by: Niels Nuyttens --- README.md | 282 --------------------------------------- nannyml/usage_logging.py | 6 +- 2 files changed, 4 insertions(+), 284 deletions(-) delete mode 100644 README.md diff --git a/README.md b/README.md deleted file mode 100644 index ccb94b28..00000000 --- a/README.md +++ /dev/null @@ -1,282 +0,0 @@ -

- -

-

- - - - - - - - - - - - - - Documentation Status - - PyPI - License -
-
- - NannyML - OSS Python library for detecting silent ML model failure | Product Hunt - - -

- -

- - Website - • - Docs - • - Community Slack - -

- -

- animated -

- -# 💡 What is NannyML? - -NannyML is an open-source python library that allows you to **estimate post-deployment model performance** (without access to targets), detect data drift, and intelligently link data drift alerts back to changes in model performance. Built for data scientists, NannyML has an easy-to-use interface, interactive visualizations, is completely model-agnostic and currently supports all tabular use cases, classification and **regression**. - -The core contributors of NannyML have researched and developed multiple novel algorithms for estimating model performance: [confidence-based performance estimation (CBPE)](https://nannyml.readthedocs.io/en/stable/how_it_works/performance_estimation.html#confidence-based-performance-estimation-cbpe) and [direct loss estimation (DLE)](https://nannyml.readthedocs.io/en/stable/how_it_works/performance_estimation.html#direct-loss-estimation-dle). -The nansters also invented a new approach to detect [multivariate data drift](https://nannyml.readthedocs.io/en/stable/how_it_works/data_reconstruction.html) using PCA-based data reconstruction. - -If you like what we are working on, be sure to become a Nanster yourself, join our [community slack](https://join.slack.com/t/nannymlbeta/shared_invite/zt-16fvpeddz-HAvTsjNEyC9CE6JXbiM7BQ) and support us with a GitHub star ⭐. - -# ☔ Why use NannyML? - -NannyML closes the loop with performance monitoring and post deployment data science, empowering data scientist to quickly understand and **automatically detect silent model failure**. By using NannyML, data scientists can finally maintain complete visibility and trust in their deployed machine learning models. -Allowing you to have the following benefits: - -- End sleepless nights caused by not knowing your model performance 😴 -- Analyse data drift and model performance **over time** -- Discover the **root cause** to why your models are not performing as expected -- **No alert fatigue!** React only when necessary if model performance is impacted -- **Painless** setup in any environment - -# 🧠 GO DEEP - -| NannyML Resources | Description | -| --------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -| ☎️ **[NannyML 101]** | New to NannyML? Start here! | -| 🔮 **[Performance estimation]** | How the magic works. | -| 🌍 **[Real world example]** | Take a look at a real-world example of NannyML. | -| 🔑 **[Key concepts]** | Glossary of key concepts we use. | -| 🔬 **[Technical reference]** | Monitor the performance of your ML models. | -| 🔎 **[Blog]** | Thoughts on post-deployment data science from the NannyML team. | -| 📬 **[Newsletter]** | All things post-deployment data science. Subscribe to see the latest papers and blogs. | -| 💎 **[New in v0.9.0]** | New features, bug fixes. | -| 🧑‍💻 **[Contribute]** | How to contribute to the NannyML project and codebase. | -| **[Join slack]** | Need help with your specific use case? Say hi on slack! | - -[nannyml 101]: https://nannyml.readthedocs.io/en/stable/ -[performance estimation]: https://nannyml.readthedocs.io/en/stable/how_it_works/performance_estimation.html -[key concepts]: https://nannyml.readthedocs.io/en/stable/glossary.html -[technical reference]: https://nannyml.readthedocs.io/en/stable/nannyml/modules.html -[new in v0.9.0]: https://github.com/NannyML/nannyml/releases/latest/ -[real world example]: https://nannyml.readthedocs.io/en/stable/examples/california_housing.html -[blog]: https://www.nannyml.com/blog -[newsletter]: https://mailchi.mp/022c62281d13/postdeploymentnewsletter -[join slack]: https://join.slack.com/t/nannymlbeta/shared_invite/zt-16fvpeddz-HAvTsjNEyC9CE6JXbiM7BQ -[contribute]: https://github.com/NannyML/nannyml/blob/main/CONTRIBUTING.rst - -# 🔱 Features - -### 1. Performance estimation and monitoring - -When the actual outcome of your deployed prediction models is delayed, or even when post-deployment target labels are completely absent, you can use NannyML's [CBPE-algorithm](https://nannyml.readthedocs.io/en/stable/how_it_works/performance_estimation.html#confidence-based-performance-estimation-cbpe) to **estimate model performance** for classification or NannyML's [DLE-algorithm](https://nannyml.readthedocs.io/en/stable/how_it_works/performance_estimation.html#direct-loss-estimation-dle) for regression. These algorithms provide you with any estimated metric you would like, i.e. ROC AUC or RSME. Rather than estimating the performance of future model predictions, CBPE and DLE estimate the expected model performance of the predictions made at inference time. - -

- -NannyML can also **track the realised performance** of your machine learning model once targets are available. - -### 2. Data drift detection - -To detect **multivariate feature drift** NannyML uses [PCA-based data reconstruction](https://nannyml.readthedocs.io/en/main/how_it_works/data_reconstruction.html). Changes in the resulting reconstruction error are monitored over time and data drift alerts are logged when the reconstruction error in a certain period exceeds a threshold. This threshold is calculated based on the reconstruction error observed in the reference period. - -

- -NannyML utilises statistical tests to detect **univariate feature drift**. We have just added a bunch of new univariate tests including Jensen-Shannon Distance and L-Infinity Distance, check out the [comprehensive list](https://nannyml.readthedocs.io/en/stable/how_it_works/univariate_drift_detection.html#methods-for-continuous-features). The results of these tests are tracked over time, properly corrected to counteract multiplicity and overlayed on the temporal feature distributions. (It is also possible to visualise the test-statistics over time, to get a notion of the drift magnitude.) - -

- -NannyML uses the same statistical tests to detected **model output drift**. - -

- -**Target distribution drift** can also be monitored using the same statistical tests. Bear in mind that this operation requires the presence of actuals. - -

- -### 3. Intelligent alerting - -Because NannyML can estimate performance, it is possible to weed out data drift alerts that do not impact expected performance, combatting alert fatigue. Besides linking data drift issues to drops in performance it is also possible to prioritise alerts according to other criteria using NannyML's Ranker. - -# 🚀 Getting started - -### Install NannyML - -NannyML depends on [LightGBM](https://github.com/microsoft/LightGBM). This might require you to set install additional -OS-specific binaries. You can follow the [official installation guide](https://lightgbm.readthedocs.io/en/latest/Installation-Guide.html). - -From PyPI: - -```bash -pip install nannyml -``` - -From Conda: - -```bash - conda install -c conda-forge nannyml -``` - -Running via [Docker](https://hub.docker.com/r/nannyml/nannyml): - -```bash -docker -v /local/config/dir/:/config/ run nannyml/nannyml nml run -``` - -**Here be dragons!** Use the latest development version of NannyML at your own risk: - -```bash -python -m pip install git+https://github.com/NannyML/nannyml -``` - -### Quick Start - -_The following snippet is based on our [latest release](https://github.com/NannyML/nannyml/releases/latest)_. - -```python -import nannyml as nml -import pandas as pd -from IPython.display import display - -# Load real-world data: -df_reference, df_analysis, _ = nml.load_us_census_ma_employment_data() -display(df_reference.head()) -display(df_analysis.head()) - -# Choose a chunker or set a chunk size: -chunk_size = 5000 - -# initialize, specify required data columns, fit estimator and estimate: -estimator = nml.CBPE( - problem_type='classification_binary', - y_pred_proba='predicted_probability', - y_pred='prediction', - y_true='employed', - metrics=['roc_auc'], - chunk_size=chunk_size, -) -estimator = estimator.fit(df_reference) -estimated_performance = estimator.estimate(df_analysis) - -# Show results: -figure = estimated_performance.plot() -figure.show() - -# Define feature columns: -features = ['AGEP', 'SCHL', 'MAR', 'RELP', 'DIS', 'ESP', 'CIT', 'MIG', 'MIL', 'ANC', - 'NATIVITY', 'DEAR', 'DEYE', 'DREM', 'SEX', 'RAC1P'] - -# Initialize the object that will perform the Univariate Drift calculations: -univariate_calculator = nml.UnivariateDriftCalculator( - column_names=features, - chunk_size=chunk_size -) - -univariate_calculator.fit(df_reference) -univariate_drift = univariate_calculator.calculate(df_analysis) - -# Get features that drift the most with count-based ranker: -alert_count_ranker = nml.AlertCountRanker() -alert_count_ranked_features = alert_count_ranker.rank(univariate_drift) -display(alert_count_ranked_features.head()) - -# Plot drift results for top 3 features: -figure = univariate_drift.filter(column_names=['RELP','AGEP', 'SCHL']).plot() -figure.show() - -# Compare drift of a selected feature with estimated performance -uni_drift_AGEP_analysis = univariate_drift.filter(column_names=['AGEP'], period='analysis') -figure = estimated_performance.compare(uni_drift_AGEP_analysis).plot() -figure.show() - -# Plot distribution changes of the selected features: -figure = univariate_drift.filter(period='analysis', column_names=['RELP','AGEP', 'SCHL']).plot(kind='distribution') -figure.show() - -# Get target data, calculate, plot and compare realized performance with estimated performance: -_, _, analysis_targets = nml.load_us_census_ma_employment_data() - -df_analysis_with_targets = pd.concat([df_analysis, analysis_targets], axis=1) -display(df_analysis_with_targets.head()) - -performance_calculator = nml.PerformanceCalculator( - problem_type='classification_binary', - y_pred_proba='predicted_probability', - y_pred='prediction', - y_true='employed', - metrics=['roc_auc'], - chunk_size=chunk_size) - -performance_calculator.fit(df_reference) -calculated_performance = performance_calculator.calculate(df_analysis_with_targets) - -figure = estimated_performance.filter(period='analysis').compare(calculated_performance).plot() -figure.show() - -``` - -# 📖 Documentation - -- Performance monitoring - - [Estimated performance](https://nannyml.readthedocs.io/en/main/tutorials/performance_estimation.html) - - [Realized performance](https://nannyml.readthedocs.io/en/main/tutorials/performance_calculation.html) -- Drift detection - - [Multivariate feature drift](https://nannyml.readthedocs.io/en/main/tutorials/detecting_data_drift/multivariate_drift_detection.html) - * [Univariate feature drift](https://nannyml.readthedocs.io/en/main/tutorials/detecting_data_drift/univariate_drift_detection.html) - -# 🦸 Contributing and Community - -We want to build NannyML together with the community! The easiest to contribute at the moment is to propose new features or log bugs under [issues](https://github.com/NannyML/nannyml/issues). For more information, have a look at [how to contribute](CONTRIBUTING.rst). - -# 🙋 Get help - -The best place to ask for help is in the [community slack](https://join.slack.com/t/nannymlbeta/shared_invite/zt-16fvpeddz-HAvTsjNEyC9CE6JXbiM7BQ). Feel free to join and ask questions or raise issues. Someone will definitely respond to you. - -# 🥷 Stay updated - -If you want to stay up to date with recent changes to the NannyML library, you can subscribe to our [release notes](https://nannyml.substack.com). For thoughts on post-deployment data science from the NannyML team, feel free to visit our [blog](https://www.nannyml.com/blog). You can also sing up for our [newsletter](https://mailchi.mp/022c62281d13/postdeploymentnewsletter), which brings together the best papers, articles, news, and open-source libraries highlighting the ML challenges after deployment. - -# 📍 Roadmap - -Curious what we are working on next? Have a look at our [roadmap](https://bit.ly/nannymlroadmap). If you have any questions or if you would like to see things prioritised in a different way, let us know! - -# 📝 Citing NannyML - -To cite NannyML in academic papers, please use the following BibTeX entry. - -### Version 0.9.0 - -``` - @misc{nannyml, - title = {{N}anny{ML} (release 0.9.0)}, - howpublished = {\url{https://github.com/NannyML/nannyml}}, - month = mar, - year = 2023, - note = {NannyML, Belgium, OHL.}, - key = {NannyML} - } -``` - -# 📄 License - -NannyML is distributed under an Apache License Version 2.0. A complete version can be found [here](LICENSE). All contributions will be distributed under this license. diff --git a/nannyml/usage_logging.py b/nannyml/usage_logging.py index 9c915d81..00af95c9 100644 --- a/nannyml/usage_logging.py +++ b/nannyml/usage_logging.py @@ -247,7 +247,9 @@ def _get_system_information() -> Dict[str, Any]: def _get_runtime_environment(): - if _is_running_in_docker(): + if _is_running_in_kubernetes(): + return 'kubernetes' + elif _is_running_in_docker(): return 'docker' elif _is_running_in_notebook(): return 'notebook' @@ -266,7 +268,7 @@ def _is_running_in_docker(): return False -def is_running_in_kubernetes(): +def _is_running_in_kubernetes(): return Path('/var/run/secrets/kubernetes.io/').exists() From 5dda38a199378601c3bbac88219b7df4157f59d6 Mon Sep 17 00:00:00 2001 From: Niels Nuyttens Date: Mon, 3 Jul 2023 16:15:08 +0200 Subject: [PATCH 04/18] [skip ci] Re-add the mysteriously disappeared README Signed-off-by: Niels Nuyttens --- README.md | 282 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..ccb94b28 --- /dev/null +++ b/README.md @@ -0,0 +1,282 @@ +

+ +

+

+ + + + + + + + + + + + + + Documentation Status + + PyPI - License +
+
+ + NannyML - OSS Python library for detecting silent ML model failure | Product Hunt + + +

+ +

+ + Website + • + Docs + • + Community Slack + +

+ +

+ animated +

+ +# 💡 What is NannyML? + +NannyML is an open-source python library that allows you to **estimate post-deployment model performance** (without access to targets), detect data drift, and intelligently link data drift alerts back to changes in model performance. Built for data scientists, NannyML has an easy-to-use interface, interactive visualizations, is completely model-agnostic and currently supports all tabular use cases, classification and **regression**. + +The core contributors of NannyML have researched and developed multiple novel algorithms for estimating model performance: [confidence-based performance estimation (CBPE)](https://nannyml.readthedocs.io/en/stable/how_it_works/performance_estimation.html#confidence-based-performance-estimation-cbpe) and [direct loss estimation (DLE)](https://nannyml.readthedocs.io/en/stable/how_it_works/performance_estimation.html#direct-loss-estimation-dle). +The nansters also invented a new approach to detect [multivariate data drift](https://nannyml.readthedocs.io/en/stable/how_it_works/data_reconstruction.html) using PCA-based data reconstruction. + +If you like what we are working on, be sure to become a Nanster yourself, join our [community slack](https://join.slack.com/t/nannymlbeta/shared_invite/zt-16fvpeddz-HAvTsjNEyC9CE6JXbiM7BQ) and support us with a GitHub star ⭐. + +# ☔ Why use NannyML? + +NannyML closes the loop with performance monitoring and post deployment data science, empowering data scientist to quickly understand and **automatically detect silent model failure**. By using NannyML, data scientists can finally maintain complete visibility and trust in their deployed machine learning models. +Allowing you to have the following benefits: + +- End sleepless nights caused by not knowing your model performance 😴 +- Analyse data drift and model performance **over time** +- Discover the **root cause** to why your models are not performing as expected +- **No alert fatigue!** React only when necessary if model performance is impacted +- **Painless** setup in any environment + +# 🧠 GO DEEP + +| NannyML Resources | Description | +| --------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | +| ☎️ **[NannyML 101]** | New to NannyML? Start here! | +| 🔮 **[Performance estimation]** | How the magic works. | +| 🌍 **[Real world example]** | Take a look at a real-world example of NannyML. | +| 🔑 **[Key concepts]** | Glossary of key concepts we use. | +| 🔬 **[Technical reference]** | Monitor the performance of your ML models. | +| 🔎 **[Blog]** | Thoughts on post-deployment data science from the NannyML team. | +| 📬 **[Newsletter]** | All things post-deployment data science. Subscribe to see the latest papers and blogs. | +| 💎 **[New in v0.9.0]** | New features, bug fixes. | +| 🧑‍💻 **[Contribute]** | How to contribute to the NannyML project and codebase. | +| **[Join slack]** | Need help with your specific use case? Say hi on slack! | + +[nannyml 101]: https://nannyml.readthedocs.io/en/stable/ +[performance estimation]: https://nannyml.readthedocs.io/en/stable/how_it_works/performance_estimation.html +[key concepts]: https://nannyml.readthedocs.io/en/stable/glossary.html +[technical reference]: https://nannyml.readthedocs.io/en/stable/nannyml/modules.html +[new in v0.9.0]: https://github.com/NannyML/nannyml/releases/latest/ +[real world example]: https://nannyml.readthedocs.io/en/stable/examples/california_housing.html +[blog]: https://www.nannyml.com/blog +[newsletter]: https://mailchi.mp/022c62281d13/postdeploymentnewsletter +[join slack]: https://join.slack.com/t/nannymlbeta/shared_invite/zt-16fvpeddz-HAvTsjNEyC9CE6JXbiM7BQ +[contribute]: https://github.com/NannyML/nannyml/blob/main/CONTRIBUTING.rst + +# 🔱 Features + +### 1. Performance estimation and monitoring + +When the actual outcome of your deployed prediction models is delayed, or even when post-deployment target labels are completely absent, you can use NannyML's [CBPE-algorithm](https://nannyml.readthedocs.io/en/stable/how_it_works/performance_estimation.html#confidence-based-performance-estimation-cbpe) to **estimate model performance** for classification or NannyML's [DLE-algorithm](https://nannyml.readthedocs.io/en/stable/how_it_works/performance_estimation.html#direct-loss-estimation-dle) for regression. These algorithms provide you with any estimated metric you would like, i.e. ROC AUC or RSME. Rather than estimating the performance of future model predictions, CBPE and DLE estimate the expected model performance of the predictions made at inference time. + +

+ +NannyML can also **track the realised performance** of your machine learning model once targets are available. + +### 2. Data drift detection + +To detect **multivariate feature drift** NannyML uses [PCA-based data reconstruction](https://nannyml.readthedocs.io/en/main/how_it_works/data_reconstruction.html). Changes in the resulting reconstruction error are monitored over time and data drift alerts are logged when the reconstruction error in a certain period exceeds a threshold. This threshold is calculated based on the reconstruction error observed in the reference period. + +

+ +NannyML utilises statistical tests to detect **univariate feature drift**. We have just added a bunch of new univariate tests including Jensen-Shannon Distance and L-Infinity Distance, check out the [comprehensive list](https://nannyml.readthedocs.io/en/stable/how_it_works/univariate_drift_detection.html#methods-for-continuous-features). The results of these tests are tracked over time, properly corrected to counteract multiplicity and overlayed on the temporal feature distributions. (It is also possible to visualise the test-statistics over time, to get a notion of the drift magnitude.) + +

+ +NannyML uses the same statistical tests to detected **model output drift**. + +

+ +**Target distribution drift** can also be monitored using the same statistical tests. Bear in mind that this operation requires the presence of actuals. + +

+ +### 3. Intelligent alerting + +Because NannyML can estimate performance, it is possible to weed out data drift alerts that do not impact expected performance, combatting alert fatigue. Besides linking data drift issues to drops in performance it is also possible to prioritise alerts according to other criteria using NannyML's Ranker. + +# 🚀 Getting started + +### Install NannyML + +NannyML depends on [LightGBM](https://github.com/microsoft/LightGBM). This might require you to set install additional +OS-specific binaries. You can follow the [official installation guide](https://lightgbm.readthedocs.io/en/latest/Installation-Guide.html). + +From PyPI: + +```bash +pip install nannyml +``` + +From Conda: + +```bash + conda install -c conda-forge nannyml +``` + +Running via [Docker](https://hub.docker.com/r/nannyml/nannyml): + +```bash +docker -v /local/config/dir/:/config/ run nannyml/nannyml nml run +``` + +**Here be dragons!** Use the latest development version of NannyML at your own risk: + +```bash +python -m pip install git+https://github.com/NannyML/nannyml +``` + +### Quick Start + +_The following snippet is based on our [latest release](https://github.com/NannyML/nannyml/releases/latest)_. + +```python +import nannyml as nml +import pandas as pd +from IPython.display import display + +# Load real-world data: +df_reference, df_analysis, _ = nml.load_us_census_ma_employment_data() +display(df_reference.head()) +display(df_analysis.head()) + +# Choose a chunker or set a chunk size: +chunk_size = 5000 + +# initialize, specify required data columns, fit estimator and estimate: +estimator = nml.CBPE( + problem_type='classification_binary', + y_pred_proba='predicted_probability', + y_pred='prediction', + y_true='employed', + metrics=['roc_auc'], + chunk_size=chunk_size, +) +estimator = estimator.fit(df_reference) +estimated_performance = estimator.estimate(df_analysis) + +# Show results: +figure = estimated_performance.plot() +figure.show() + +# Define feature columns: +features = ['AGEP', 'SCHL', 'MAR', 'RELP', 'DIS', 'ESP', 'CIT', 'MIG', 'MIL', 'ANC', + 'NATIVITY', 'DEAR', 'DEYE', 'DREM', 'SEX', 'RAC1P'] + +# Initialize the object that will perform the Univariate Drift calculations: +univariate_calculator = nml.UnivariateDriftCalculator( + column_names=features, + chunk_size=chunk_size +) + +univariate_calculator.fit(df_reference) +univariate_drift = univariate_calculator.calculate(df_analysis) + +# Get features that drift the most with count-based ranker: +alert_count_ranker = nml.AlertCountRanker() +alert_count_ranked_features = alert_count_ranker.rank(univariate_drift) +display(alert_count_ranked_features.head()) + +# Plot drift results for top 3 features: +figure = univariate_drift.filter(column_names=['RELP','AGEP', 'SCHL']).plot() +figure.show() + +# Compare drift of a selected feature with estimated performance +uni_drift_AGEP_analysis = univariate_drift.filter(column_names=['AGEP'], period='analysis') +figure = estimated_performance.compare(uni_drift_AGEP_analysis).plot() +figure.show() + +# Plot distribution changes of the selected features: +figure = univariate_drift.filter(period='analysis', column_names=['RELP','AGEP', 'SCHL']).plot(kind='distribution') +figure.show() + +# Get target data, calculate, plot and compare realized performance with estimated performance: +_, _, analysis_targets = nml.load_us_census_ma_employment_data() + +df_analysis_with_targets = pd.concat([df_analysis, analysis_targets], axis=1) +display(df_analysis_with_targets.head()) + +performance_calculator = nml.PerformanceCalculator( + problem_type='classification_binary', + y_pred_proba='predicted_probability', + y_pred='prediction', + y_true='employed', + metrics=['roc_auc'], + chunk_size=chunk_size) + +performance_calculator.fit(df_reference) +calculated_performance = performance_calculator.calculate(df_analysis_with_targets) + +figure = estimated_performance.filter(period='analysis').compare(calculated_performance).plot() +figure.show() + +``` + +# 📖 Documentation + +- Performance monitoring + - [Estimated performance](https://nannyml.readthedocs.io/en/main/tutorials/performance_estimation.html) + - [Realized performance](https://nannyml.readthedocs.io/en/main/tutorials/performance_calculation.html) +- Drift detection + - [Multivariate feature drift](https://nannyml.readthedocs.io/en/main/tutorials/detecting_data_drift/multivariate_drift_detection.html) + * [Univariate feature drift](https://nannyml.readthedocs.io/en/main/tutorials/detecting_data_drift/univariate_drift_detection.html) + +# 🦸 Contributing and Community + +We want to build NannyML together with the community! The easiest to contribute at the moment is to propose new features or log bugs under [issues](https://github.com/NannyML/nannyml/issues). For more information, have a look at [how to contribute](CONTRIBUTING.rst). + +# 🙋 Get help + +The best place to ask for help is in the [community slack](https://join.slack.com/t/nannymlbeta/shared_invite/zt-16fvpeddz-HAvTsjNEyC9CE6JXbiM7BQ). Feel free to join and ask questions or raise issues. Someone will definitely respond to you. + +# 🥷 Stay updated + +If you want to stay up to date with recent changes to the NannyML library, you can subscribe to our [release notes](https://nannyml.substack.com). For thoughts on post-deployment data science from the NannyML team, feel free to visit our [blog](https://www.nannyml.com/blog). You can also sing up for our [newsletter](https://mailchi.mp/022c62281d13/postdeploymentnewsletter), which brings together the best papers, articles, news, and open-source libraries highlighting the ML challenges after deployment. + +# 📍 Roadmap + +Curious what we are working on next? Have a look at our [roadmap](https://bit.ly/nannymlroadmap). If you have any questions or if you would like to see things prioritised in a different way, let us know! + +# 📝 Citing NannyML + +To cite NannyML in academic papers, please use the following BibTeX entry. + +### Version 0.9.0 + +``` + @misc{nannyml, + title = {{N}anny{ML} (release 0.9.0)}, + howpublished = {\url{https://github.com/NannyML/nannyml}}, + month = mar, + year = 2023, + note = {NannyML, Belgium, OHL.}, + key = {NannyML} + } +``` + +# 📄 License + +NannyML is distributed under an Apache License Version 2.0. A complete version can be found [here](LICENSE). All contributions will be distributed under this license. From 94f527eb726f646844ada24cf65e387829ffaaf0 Mon Sep 17 00:00:00 2001 From: Niels Nuyttens Date: Wed, 5 Jul 2023 20:46:41 +0200 Subject: [PATCH 05/18] Fix broken Docker builds Signed-off-by: Niels Nuyttens --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 6e9f0701..564e66af 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,7 +36,7 @@ RUN apt-get update && \ build-essential # Install Poetry - respects $POETRY_VERSION & $POETRY_HOME -RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py | python +RUN curl -sSL https://install.python-poetry.org | python3 - --version $POETRY_VERSION ENV PATH="$POETRY_HOME/bin:$PATH" # Import our project files From 60ef79d4040999fe7873fff9c4fa6ce7360cc600 Mon Sep 17 00:00:00 2001 From: Michael Van de Steene <124588413+michael-nml@users.noreply.github.com> Date: Wed, 12 Jul 2023 19:41:24 +0200 Subject: [PATCH 06/18] Update mendable search to version 0.0.114 (#319) --- docs/_static/js/mendablesearch.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_static/js/mendablesearch.js b/docs/_static/js/mendablesearch.js index f0b2487e..40193c73 100644 --- a/docs/_static/js/mendablesearch.js +++ b/docs/_static/js/mendablesearch.js @@ -66,7 +66,7 @@ document.addEventListener("DOMContentLoaded", () => { "https://unpkg.com/react-dom@17/umd/react-dom.production.min.js", () => { loadScript( - "https://unpkg.com/@mendable/search@0.0.102/dist/umd/mendable.min.js", + "https://unpkg.com/@mendable/search@0.0.114/dist/umd/mendable.min.js", initializeMendable ); } From ebf754b144d231b0046cb7f6061a6fcc378201d7 Mon Sep 17 00:00:00 2001 From: Nikolaos Perrakis <89025229+nikml@users.noreply.github.com> Date: Wed, 12 Jul 2023 21:52:39 +0300 Subject: [PATCH 07/18] Library Updates (#311) * update whats next for binary perf est * upd remaining perf est docs * remove unneded conf band variabls --- .../compare_estimated_and_realized_performance.rst | 2 ++ .../business_value_estimation.rst | 7 ++----- .../confusion_matrix_estimation.rst | 7 ++----- .../custom_metric_estimation.rst | 10 ++-------- .../standard_metric_estimation.rst | 7 ++----- .../multiclass_performance_estimation.rst | 6 ++---- .../regression_performance_estimation.rst | 7 ++----- .../performance_estimation/confidence_based/cbpe.py | 2 -- 8 files changed, 14 insertions(+), 34 deletions(-) diff --git a/docs/tutorials/compare_estimated_and_realized_performance.rst b/docs/tutorials/compare_estimated_and_realized_performance.rst index cb388a8c..b2549469 100644 --- a/docs/tutorials/compare_estimated_and_realized_performance.rst +++ b/docs/tutorials/compare_estimated_and_realized_performance.rst @@ -20,6 +20,8 @@ When the :term:`targets` become available, the quality of estimations pr The beginning of the code below is similar to the one in :ref:`tutorial on performance calculation with binary classification data`. +while this tutorial uses the **roc_auc** metric, any metric estimated and calculated by NannyML can +be used for comparison. For simplicity this guide is based on a synthetic dataset included in the library, where the monitored model predicts whether a customer will repay a loan to buy a car. diff --git a/docs/tutorials/performance_estimation/binary_performance_estimation/business_value_estimation.rst b/docs/tutorials/performance_estimation/binary_performance_estimation/business_value_estimation.rst index d9303c59..beff3271 100644 --- a/docs/tutorials/performance_estimation/binary_performance_estimation/business_value_estimation.rst +++ b/docs/tutorials/performance_estimation/binary_performance_estimation/business_value_estimation.rst @@ -160,8 +160,5 @@ What's next ----------- The :ref:`Data Drift` functionality can help us to understand whether data drift is causing the performance problem. -When the target values become available they can be :ref:`compared with the estimated -results`. - -You can learn more about the Confidence Based Performance Estimation and its limitations in the -:ref:`How it Works page`. +When the target values become available we can +:ref:`compared realized and estimated business value results`. diff --git a/docs/tutorials/performance_estimation/binary_performance_estimation/confusion_matrix_estimation.rst b/docs/tutorials/performance_estimation/binary_performance_estimation/confusion_matrix_estimation.rst index a7174261..e1d95710 100644 --- a/docs/tutorials/performance_estimation/binary_performance_estimation/confusion_matrix_estimation.rst +++ b/docs/tutorials/performance_estimation/binary_performance_estimation/confusion_matrix_estimation.rst @@ -161,8 +161,5 @@ What's next ----------- The :ref:`Data Drift` functionality can help us to understand whether data drift is causing the performance problem. -When the target values become available they can be :ref:`compared with the estimated -results`. - -You can learn more about the Confidence Based Performance Estimation and its limitations in the -:ref:`How it Works page`. +When the target values become available we can +:ref:`compared realized and estimated confusion matrix results`. diff --git a/docs/tutorials/performance_estimation/binary_performance_estimation/custom_metric_estimation.rst b/docs/tutorials/performance_estimation/binary_performance_estimation/custom_metric_estimation.rst index bf7b6b2c..ed918cd6 100644 --- a/docs/tutorials/performance_estimation/binary_performance_estimation/custom_metric_estimation.rst +++ b/docs/tutorials/performance_estimation/binary_performance_estimation/custom_metric_estimation.rst @@ -135,11 +135,5 @@ What's next ----------- The :ref:`Data Drift` functionality can help us to understand whether data drift is causing the performance problem. -When the target values become available they can be :ref:`compared with the estimated -results`. - -You can learn more about the Confidence Based Performance Estimation and its limitations in the -:ref:`How it Works page`. - -And if targets are available or become available, you can learn more about *calculating* confusion -matrix elements in the :ref:`confusion-matrix-calculation` tutorial. +When the target values become available we can +:ref:`compared realized and estimated custom performance metric results`. diff --git a/docs/tutorials/performance_estimation/binary_performance_estimation/standard_metric_estimation.rst b/docs/tutorials/performance_estimation/binary_performance_estimation/standard_metric_estimation.rst index 283ec9da..f6acf359 100644 --- a/docs/tutorials/performance_estimation/binary_performance_estimation/standard_metric_estimation.rst +++ b/docs/tutorials/performance_estimation/binary_performance_estimation/standard_metric_estimation.rst @@ -157,8 +157,5 @@ What's next ----------- The :ref:`Data Drift` functionality can help us to understand whether data drift is causing the performance problem. -When the target values become available they can be :ref:`compared with the estimated -results`. - -You can learn more about the Confidence Based Performance Estimation and its limitations in the -:ref:`How it Works page`. +When the target values become available we can +:ref:`compared realized and estimated performance results`. diff --git a/docs/tutorials/performance_estimation/multiclass_performance_estimation.rst b/docs/tutorials/performance_estimation/multiclass_performance_estimation.rst index 61d90ef6..a3200f9b 100644 --- a/docs/tutorials/performance_estimation/multiclass_performance_estimation.rst +++ b/docs/tutorials/performance_estimation/multiclass_performance_estimation.rst @@ -140,7 +140,5 @@ What's next ----------- The :ref:`Data Drift` functionality can help us to understand whether data drift is causing the performance problem. -When the target results become available they can be :ref:`compared with the estimated results`. - -You can learn more about the Confidence Based Performance Estimation and its limitations in the -:ref:`How it Works page`. +When the target values become available we can +:ref:`compared realized and performance results`. diff --git a/docs/tutorials/performance_estimation/regression_performance_estimation.rst b/docs/tutorials/performance_estimation/regression_performance_estimation.rst index 3747c5b3..d7f5d33f 100644 --- a/docs/tutorials/performance_estimation/regression_performance_estimation.rst +++ b/docs/tutorials/performance_estimation/regression_performance_estimation.rst @@ -164,11 +164,8 @@ What's next ----------- The :ref:`Data Drift` functionality can help us to understand whether data drift is causing the performance problem. -When the target values become available they can be :ref:`compared with the estimated -results`. - -You can learn more about Direct Error Estimation and its limitations in the -:ref:`How it Works page`. +When the target values become available we can +:ref:`compared realized and estimated performance results`. .. _LGBMRegressor defaults: https://lightgbm.readthedocs.io/en/latest/pythonapi/lightgbm.LGBMRegressor.html diff --git a/nannyml/performance_estimation/confidence_based/cbpe.py b/nannyml/performance_estimation/confidence_based/cbpe.py index 27da22ff..cd7909b4 100644 --- a/nannyml/performance_estimation/confidence_based/cbpe.py +++ b/nannyml/performance_estimation/confidence_based/cbpe.py @@ -284,8 +284,6 @@ def __init__( ) ) - self.confidence_upper_bound = 1 - self.confidence_lower_bound = 0 self.needs_calibration: bool = False if calibrator is None: From eaa5f17fe9c9a0df02e0952a279c9c6d2fef1cee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santiago=20V=C3=ADquez?= Date: Wed, 12 Jul 2023 20:54:36 +0200 Subject: [PATCH 08/18] New Example Notebook to Docs (#316) * add green taxi notebook example * update google colab badge link * add google colab badge to quicstart * update notebook to run on google colab * add green_taxi to toctree * add pcs reconstruction image --- docs/_static/example_green_taxi_all_udc.svg | 1 + docs/_static/example_green_taxi_dle.svg | 1 + .../example_green_taxi_dle_vs_realized.svg | 1 + .../example_green_taxi_feature_importance.svg | 1286 +++ .../example_green_taxi_location_udc.svg | 1 + docs/_static/example_green_taxi_model_val.png | Bin 0 -> 28445 bytes docs/_static/example_green_taxi_pca_error.svg | 1 + .../_static/example_green_taxi_pickup_udc.svg | 1 + .../example_green_taxi_tip_amount_boxplot.svg | 8459 +++++++++++++++++ ...ple_green_taxi_tip_amount_distribution.svg | 834 ++ docs/_static/pca_reconstruction_error.svg | 326 + .../Examples Green Taxi.ipynb | 520 + docs/examples.rst | 1 + docs/examples/green_taxi.rst | 316 + docs/quick.rst | 6 + 15 files changed, 11754 insertions(+) create mode 100644 docs/_static/example_green_taxi_all_udc.svg create mode 100644 docs/_static/example_green_taxi_dle.svg create mode 100644 docs/_static/example_green_taxi_dle_vs_realized.svg create mode 100644 docs/_static/example_green_taxi_feature_importance.svg create mode 100644 docs/_static/example_green_taxi_location_udc.svg create mode 100644 docs/_static/example_green_taxi_model_val.png create mode 100644 docs/_static/example_green_taxi_pca_error.svg create mode 100644 docs/_static/example_green_taxi_pickup_udc.svg create mode 100644 docs/_static/example_green_taxi_tip_amount_boxplot.svg create mode 100644 docs/_static/example_green_taxi_tip_amount_distribution.svg create mode 100644 docs/_static/pca_reconstruction_error.svg create mode 100644 docs/example_notebooks/Examples Green Taxi.ipynb create mode 100644 docs/examples/green_taxi.rst diff --git a/docs/_static/example_green_taxi_all_udc.svg b/docs/_static/example_green_taxi_all_udc.svg new file mode 100644 index 00000000..e934f8f4 --- /dev/null +++ b/docs/_static/example_green_taxi_all_udc.svg @@ -0,0 +1 @@ +Dec 112016Dec 18Dec 25Jan 120170100200300400500600700Dec 112016Dec 18Dec 25Jan 12017−50510152025Dec 112016Dec 18Dec 25Jan 12017050100150Dec 112016Dec 18Dec 25Jan 1201700.20.40.60.81Dec 112016Dec 18Dec 25Jan 1201700.20.40.60.81Dec 112016Dec 18Dec 25Jan 1201700.20.40.60.81DOLocationIDOther1817441427PULocationIDOther255754174166VendorID21Column distributionsTimeTimeTimeTimeTimeTimeValuesValuesValuesValuesValuesValuesfare_amount distribution (alerts for Jensen-Shannon distance)pickup_time distribution (alerts for Jensen-Shannon distance)trip_distance distribution (alerts for Jensen-Shannon distance)DOLocationID distribution (alerts for Jensen-Shannon distance)PULocationID distribution (alerts for Jensen-Shannon distance)VendorID distribution (alerts for Jensen-Shannon distance)ReferenceAnalysisReferenceAnalysisReferenceAnalysis \ No newline at end of file diff --git a/docs/_static/example_green_taxi_dle.svg b/docs/_static/example_green_taxi_dle.svg new file mode 100644 index 00000000..0669a292 --- /dev/null +++ b/docs/_static/example_green_taxi_dle.svg @@ -0,0 +1 @@ +Dec 112016Dec 18Dec 25Jan 120170.9511.051.11.151.2MetricAlertThresholdConfidence bandEstimated performance (DLE)TimeMAEEstimated MAEReferenceAnalysis \ No newline at end of file diff --git a/docs/_static/example_green_taxi_dle_vs_realized.svg b/docs/_static/example_green_taxi_dle_vs_realized.svg new file mode 100644 index 00000000..47f0f0ba --- /dev/null +++ b/docs/_static/example_green_taxi_dle_vs_realized.svg @@ -0,0 +1 @@ +Dec 182016Dec 21Dec 24Dec 27Dec 300.9511.051.11.151.21.25MAE (estimated MAE)Confidence bandrealized MAE, MAE, maeAlertEstimated performance (DLE) vs. Realized performanceChunkMAE (estimated MAE) vs. realized MAE, MAE, mae \ No newline at end of file diff --git a/docs/_static/example_green_taxi_feature_importance.svg b/docs/_static/example_green_taxi_feature_importance.svg new file mode 100644 index 00000000..01ea59f3 --- /dev/null +++ b/docs/_static/example_green_taxi_feature_importance.svg @@ -0,0 +1,1286 @@ + + + + + + + + 2023-07-03T17:30:44.653290 + image/svg+xml + + + Matplotlib v3.7.1, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_static/example_green_taxi_location_udc.svg b/docs/_static/example_green_taxi_location_udc.svg new file mode 100644 index 00000000..24eda5c4 --- /dev/null +++ b/docs/_static/example_green_taxi_location_udc.svg @@ -0,0 +1 @@ +Dec 112016Dec 18Dec 25Jan 1201700.20.40.60.81DOLocationIDOther1817441427Column distributionsTimeValuesDOLocationID distribution (alerts for Jensen-Shannon distance)ReferenceAnalysis \ No newline at end of file diff --git a/docs/_static/example_green_taxi_model_val.png b/docs/_static/example_green_taxi_model_val.png new file mode 100644 index 0000000000000000000000000000000000000000..baa854e9571c04fa102c62a69cfdb9c24d1e4c04 GIT binary patch literal 28445 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 literal 0 HcmV?d00001 diff --git a/docs/_static/example_green_taxi_pca_error.svg b/docs/_static/example_green_taxi_pca_error.svg new file mode 100644 index 00000000..1cb90a64 --- /dev/null +++ b/docs/_static/example_green_taxi_pca_error.svg @@ -0,0 +1 @@ +Dec 112016Dec 18Dec 25Jan 1201711.11.21.31.4MetricConfidence bandMultivariate drift (PCA reconstruction error)TimeData reconstruction driftReferenceAnalysis \ No newline at end of file diff --git a/docs/_static/example_green_taxi_pickup_udc.svg b/docs/_static/example_green_taxi_pickup_udc.svg new file mode 100644 index 00000000..db3bd741 --- /dev/null +++ b/docs/_static/example_green_taxi_pickup_udc.svg @@ -0,0 +1 @@ +Dec 112016Dec 18Dec 25−50510152025Column distributionsTimeValuespickup_time distribution (alerts for Jensen-Shannon distance) \ No newline at end of file diff --git a/docs/_static/example_green_taxi_tip_amount_boxplot.svg b/docs/_static/example_green_taxi_tip_amount_boxplot.svg new file mode 100644 index 00000000..2d2b58ba --- /dev/null +++ b/docs/_static/example_green_taxi_tip_amount_boxplot.svg @@ -0,0 +1,8459 @@ + + + + + + + + 2023-07-03T18:22:52.669705 + image/svg+xml + + + Matplotlib v3.7.1, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_static/example_green_taxi_tip_amount_distribution.svg b/docs/_static/example_green_taxi_tip_amount_distribution.svg new file mode 100644 index 00000000..5ff769dd --- /dev/null +++ b/docs/_static/example_green_taxi_tip_amount_distribution.svg @@ -0,0 +1,834 @@ + + + + + + + + 2023-07-03T18:22:53.112697 + image/svg+xml + + + Matplotlib v3.7.1, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_static/pca_reconstruction_error.svg b/docs/_static/pca_reconstruction_error.svg new file mode 100644 index 00000000..58c3ad82 --- /dev/null +++ b/docs/_static/pca_reconstruction_error.svg @@ -0,0 +1,326 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/example_notebooks/Examples Green Taxi.ipynb b/docs/example_notebooks/Examples Green Taxi.ipynb new file mode 100644 index 00000000..e6fd892a --- /dev/null +++ b/docs/example_notebooks/Examples Green Taxi.ipynb @@ -0,0 +1,520 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "89298ce0", + "metadata": {}, + "outputs": [], + "source": [ + "# Uncomment if you are runnning this on Google Colab\n", + "# !pip install nannyml\n", + "# !pip install numpy==1.22" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3c0635e7", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "3c0635e7", + "outputId": "30d1af78-5f35-4a98-a9f7-86472a947f0d" + }, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "\n", + "from sklearn.metrics import mean_absolute_error\n", + "from lightgbm import LGBMRegressor, plot_importance\n", + "\n", + "import nannyml as nml" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6e90071a", + "metadata": { + "id": "6e90071a" + }, + "outputs": [], + "source": [ + "# Read data from url\n", + "url = \"https://d37ci6vzurychx.cloudfront.net/trip-data/green_tripdata_2016-12.parquet\"\n", + "columns = ['lpep_pickup_datetime', 'PULocationID', 'DOLocationID', 'trip_distance', 'VendorID', 'payment_type', 'fare_amount', 'tip_amount']\n", + "data = pd.read_parquet(url, columns=columns)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9843aa09", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 206 + }, + "id": "9843aa09", + "outputId": "6a2c8bdd-0a25-4ab2-abbe-6852d905d784" + }, + "outputs": [], + "source": [ + "print(data.head(3).to_markdown(tablefmt=\"grid\"))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a678285e", + "metadata": { + "id": "a678285e" + }, + "outputs": [], + "source": [ + "# Choose only payments from Credit Cards\n", + "data = data.loc[data['payment_type'] == 1,].drop(columns='payment_type') # Credit card\n", + "# Choose only positive tip amounts\n", + "data = data[data['tip_amount'] >= 0]\n", + "\n", + "# Sort data by pick up date\n", + "data = data.sort_values('lpep_pickup_datetime').reset_index(drop=True)\n", + "# Flag categoric columns as categoric\n", + "categoric_columns = ['PULocationID', 'DOLocationID', 'VendorID']\n", + "data[categoric_columns] = data[categoric_columns].astype('category')\n", + "\n", + "# Create column with pick up time\n", + "data['pickup_time'] = data['lpep_pickup_datetime'].dt.hour" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0e1af9ee", + "metadata": { + "id": "0e1af9ee" + }, + "outputs": [], + "source": [ + "# Create data partition\n", + "data['partition'] = pd.cut(\n", + " data['lpep_pickup_datetime'],\n", + " bins= [pd.to_datetime('2016-12-01'),\n", + " pd.to_datetime('2016-12-08'),\n", + " pd.to_datetime('2016-12-16'),\n", + " pd.to_datetime('2017-01-01')],\n", + " right=False,\n", + " labels= ['train', 'test', 'prod']\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "B2-H3Ra8GCYl", + "metadata": { + "id": "B2-H3Ra8GCYl" + }, + "outputs": [], + "source": [ + "# Set target and features\n", + "target = 'tip_amount'\n", + "features = [col for col in data.columns if col not in [target, 'lpep_pickup_datetime', 'partition']]\n", + "\n", + "# Split the data\n", + "X_train = data.loc[data['partition'] == 'train', features]\n", + "y_train = data.loc[data['partition'] == 'train', target]\n", + "\n", + "X_test = data.loc[data['partition'] == 'test', features]\n", + "y_test = data.loc[data['partition'] == 'test', target]\n", + "\n", + "X_prod = data.loc[data['partition'] == 'prod', features]\n", + "y_prod = data.loc[data['partition'] == 'prod', target]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d68c2328", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 300 + }, + "id": "d68c2328", + "outputId": "ff8bdcf6-8083-4882-dc02-4342210b5023", + "scrolled": true + }, + "outputs": [], + "source": [ + "display(y_train.describe().to_frame())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fa4dac07", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 865 + }, + "id": "fa4dac07", + "outputId": "3aef959b-5c61-4cbd-9b55-eb007b20c826" + }, + "outputs": [], + "source": [ + "y_train.plot(kind='box')\n", + "plt.savefig('../_static/example_green_taxi_tip_amount_boxplot.svg', format='svg')\n", + "plt.show()\n", + "\n", + "y_train.clip(lower=0, upper=y_train.quantile(0.8)).to_frame().hist()\n", + "plt.savefig('../_static/example_green_taxi_tip_amount_distribution.svg', format='svg')\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "528fbf0f", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 75 + }, + "id": "528fbf0f", + "outputId": "0ecfe8b0-9917-47e5-f7cc-1b3c8f7174e2" + }, + "outputs": [], + "source": [ + "# Fit the model\n", + "model = LGBMRegressor(random_state=111)\n", + "model.fit(X_train, y_train)\n", + "\n", + "# Make predictions\n", + "y_pred_train = model.predict(X_train)\n", + "y_pred_test = model.predict(X_test)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4fd0fe9b", + "metadata": { + "id": "4fd0fe9b" + }, + "outputs": [], + "source": [ + "# Make baseline predictions\n", + "y_pred_train_baseline = np.ones_like(y_train) * y_train.mean()\n", + "y_pred_test_baseline = np.ones_like(y_test) * y_train.mean()\n", + "\n", + "# Measure train, test and baseline performance\n", + "mae_train = mean_absolute_error(y_train, y_pred_train).round(4)\n", + "mae_test = mean_absolute_error(y_test, y_pred_test).round(4)\n", + "\n", + "mae_train_baseline = mean_absolute_error(y_train, y_pred_train_baseline).round(4)\n", + "mae_test_baseline = mean_absolute_error(y_test, y_pred_test_baseline).round(4)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "beb7b032", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 410 + }, + "id": "beb7b032", + "outputId": "f750cf58-636f-4f80-aee9-c01dc30c87e7", + "scrolled": false + }, + "outputs": [], + "source": [ + "# Create performance report\n", + "fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(12,4))\n", + "\n", + "title1 = 'Train MAE: {} (<> {})'.format(mae_train, mae_train_baseline)\n", + "ax1.set(title=title1, xlabel='y_train', ylabel='y_pred')\n", + "ax1.plot(y_train, y_train, color='red', linestyle=':')\n", + "ax1.scatter(y_train, y_pred_train, alpha=0.1)\n", + "\n", + "title2 = 'Test MAE: {} (<> {})'.format(mae_test, mae_test_baseline)\n", + "ax2.set(title=title2, xlabel='y_test', ylabel='y_pred')\n", + "ax2.plot(y_test, y_test, color='red', linestyle=':')\n", + "ax2.scatter(y_test, y_pred_test, alpha=0.1)\n", + "\n", + "plt.show()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5BlWsneHW_eY", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 472 + }, + "id": "5BlWsneHW_eY", + "outputId": "eb12f11d-000e-48b0-c812-302f21200669" + }, + "outputs": [], + "source": [ + "# plot the feature importance\n", + "fig, ax = plt.subplots()\n", + "plot_importance(model, ax=ax)\n", + "plt.savefig('../_static/example_green_taxi_feature_importance.svg', format='svg')\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a84a4a2d", + "metadata": { + "id": "a84a4a2d" + }, + "outputs": [], + "source": [ + "y_pred_prod = model.predict(X_prod)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dd5e6ef3", + "metadata": { + "id": "dd5e6ef3" + }, + "outputs": [], + "source": [ + "reference = X_test.copy() # using the test set as a reference\n", + "reference['y_pred'] = y_pred_test # reference predictions\n", + "reference['tip_amount'] = y_test # ground truth (currect targets)\n", + "reference = reference.join(data['lpep_pickup_datetime']) # date\n", + "\n", + "analysis = X_prod.copy() # features\n", + "analysis['y_pred'] = y_pred_prod # prod predictions\n", + "analysis = analysis.join(data['lpep_pickup_datetime']) # date" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3dfbd7a4", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "3dfbd7a4", + "outputId": "8d3b7567-66e8-4c09-8654-096ca2c5fa90", + "scrolled": false + }, + "outputs": [], + "source": [ + "dle = nml.DLE(\n", + " metrics=['mae'],\n", + " y_true='tip_amount',\n", + " y_pred='y_pred',\n", + " feature_column_names=features,\n", + " timestamp_column_name='lpep_pickup_datetime',\n", + " chunk_period='d' # perform an estimation daily\n", + ")\n", + "\n", + "dle.fit(reference) # fit on the reference (test) data\n", + "estimated_performance = dle.estimate(analysis) # estimate on the prod data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e4b34a9a", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 517 + }, + "id": "e4b34a9a", + "outputId": "7ee26cfd-7bfb-4500-8aa6-42ece6766ce5" + }, + "outputs": [], + "source": [ + "figure = estimated_performance.plot()\n", + "figure.write_image(f'../_static/example_green_taxi_dle.svg')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3a7b6877", + "metadata": { + "id": "3a7b6877", + "scrolled": false + }, + "outputs": [], + "source": [ + "drdc = nml.DataReconstructionDriftCalculator(\n", + " column_names=features,\n", + " timestamp_column_name='lpep_pickup_datetime',\n", + " chunk_period='d',\n", + ")\n", + "\n", + "drdc.fit(reference)\n", + "multivariate_data_drift = drdc.calculate(analysis)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "74a8f9c4", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 542 + }, + "id": "74a8f9c4", + "outputId": "5ebcc144-07e7-4cdb-c136-2b6e20d4aa1e" + }, + "outputs": [], + "source": [ + "figure = multivariate_data_drift.plot()\n", + "figure.write_image(f'../_static/example_green_taxi_pca_error.svg')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "79fed665", + "metadata": { + "id": "79fed665" + }, + "outputs": [], + "source": [ + "udc = nml.UnivariateDriftCalculator(\n", + " column_names=features,\n", + " timestamp_column_name='lpep_pickup_datetime',\n", + " chunk_period='d',\n", + ")\n", + "\n", + "udc.fit(reference)\n", + "univariate_data_drift = udc.calculate(analysis)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "GnGnV5v0d7Fp", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 517 + }, + "id": "GnGnV5v0d7Fp", + "outputId": "8b4933d6-c799-4f70-9d38-fe138e26d588" + }, + "outputs": [], + "source": [ + "figure = univariate_data_drift.filter(period='all', metrics='jensen_shannon', column_names=['DOLocationID']).plot(kind='distribution')\n", + "figure.write_image(f'../_static/example_green_taxi_location_udc.svg')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ofutS6MwgFEd", + "metadata": { + "colab": { + "background_save": true + }, + "id": "ofutS6MwgFEd", + "outputId": "5c9f19f2-6452-422e-8620-8bf38065a6c3" + }, + "outputs": [], + "source": [ + "figure = univariate_data_drift.filter(period='all', metrics='jensen_shannon', column_names=['pickup_time']).plot(kind='distribution')\n", + "figure.write_image(f'../_static/example_green_taxi_pickup_udc.svg')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "QCIMHtwkhG9K", + "metadata": { + "colab": { + "background_save": true, + "base_uri": "https://localhost:8080/", + "height": 1000 + }, + "id": "QCIMHtwkhG9K", + "outputId": "f63aa06d-d290-469d-858c-4ee0fd98b168" + }, + "outputs": [], + "source": [ + "figure = univariate_data_drift.filter(period='all', metrics='jensen_shannon').plot(kind='distribution')\n", + "\n", + "figure.write_image(f'../_static/example_green_taxi_all_udc.svg')\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "DdFamecl4JPi", + "metadata": { + "colab": { + "background_save": true, + "base_uri": "https://localhost:8080/", + "height": 517 + }, + "id": "DdFamecl4JPi", + "outputId": "5aa21588-5335-46e6-9760-0b954e988267" + }, + "outputs": [], + "source": [ + "perfc = nml.PerformanceCalculator(\n", + " metrics=['mae'],\n", + " y_true='tip_amount',\n", + " y_pred='y_pred',\n", + " problem_type='regression',\n", + " timestamp_column_name='lpep_pickup_datetime',\n", + " chunk_period='d'\n", + ")\n", + "\n", + "perfc.fit(reference)\n", + "realized_performance = perfc.calculate(analysis.assign(tip_amount = y_prod))\n", + "\n", + "figure = estimated_performance.filter(period='analysis').compare(realized_performance).plot()\n", + "figure.write_image(f'../_static/example_green_taxi_dle_vs_realized.svg')" + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/examples.rst b/docs/examples.rst index dd6db8cf..83435e52 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -7,3 +7,4 @@ Examples :maxdepth: 2 examples/california_housing + examples/green_taxi \ No newline at end of file diff --git a/docs/examples/green_taxi.rst b/docs/examples/green_taxi.rst new file mode 100644 index 00000000..7910c944 --- /dev/null +++ b/docs/examples/green_taxi.rst @@ -0,0 +1,316 @@ +============================================================= +Full Monitoring Workflow - Regression: NYC Green Taxi Dataset +============================================================= +.. raw:: html + + + Open In Colab + + +In this tutorial, we will use the `NYC Green Taxi Dataset `_ to build a machine-learning model that predicts the tip amount a passenger +will leave after a taxi ride. Later, we will use NannyML to monitor this model and measure its performance with unseen production data. Additionally, +we will investigate plausible reasons for the performance drop using data drift detection methods. + + +Import libraries +================ + +The following cell will import the necessary libraries plus install NannyML. NannyML is an open-source library to do post-deployment data science. +We will use it to estimate the model's performance with unseen data and run multivariate and univariate drift tests. + +.. nbimport:: + :path: ./example_notebooks/Examples Green Taxi.ipynb + :cells: 2 + +Load the data +============= + +We will be using the following columns from the NYC Taxi Dataset: + +* lpep_pickup_datetime: pick-up datetime. +* PULocationID: ID of the pick-up location. +* DOLocationID: ID of the drop-out location. +* trip_distance: Trip distance in Miles. +* VendorID: Vendor ID. +* payment_type: Payment Type. We will be using only credit cards. +* fare_amount: Total fare amount in USD. +* tip_amount: Tip amount in USD. This column will be the target. + +Other columns were omitted because of having multiple missing values, having the same value for every record, or being directly associated with the target variable. + +.. nbimport:: + :path: ./example_notebooks/Examples Green Taxi.ipynb + :cells: 3 + +.. nbtable:: + :path: ./example_notebooks/Examples Green Taxi.ipynb + :cell: 4 + + +Preprocessing the data +====================== + +Before modeling, we will do some preprocessing: + +1. We'll only consider trips paid with a credit card as a payment type because they are the only ones with a tip amount in the dataset. +2. Choose only examples with positive tip amounts. Since negative tip amounts are not relevant for this use case, given that they may be related to chargebacks or possible errors in the data quality pipeline. +3. We will sort the data by pick-up date. This will be helpful later on when we have to partition our dataset into train, test, and production sets. +4. We will create an extra feature containing only the information about the pick-up time. + +.. nbimport:: + :path: ./example_notebooks/Examples Green Taxi.ipynb + :cells: 5 + +Now, let's split the data. When training an ML model, we often split the data into 2 (train, test) or 3 (train, validation, test) sets. But, since the final goal of +this tutorial is to learn how to monitor an ML model with unseen "production" data, we will split the original data into three parts: + +- train: data from the **first week** of December 2016 +- test: data from the **second week** of December 2016 +- prod: data from **the third and fourth weeks** of December 2016 + +The production dataset will help us simulate a real-case scenario where a trained model is used in a production environment. Typically, production data don't contain targets. +This is why monitoring the model performance on it is a challenging task. + +But let's not worry too much about it (yet). We will return later to this when learning how to estimate model performance. + +.. nbimport:: + :path: ./example_notebooks/Examples Green Taxi.ipynb + :cells: 6 + +.. nbimport:: + :path: ./example_notebooks/Examples Green Taxi.ipynb + :cells: 7 + +Exploring the training data +=========================== + +Let's quickly explore the train data to ensure we understand it and check that everything makes sense. Since we are building a model that can predict the tip amount +that the customers will leave at the end of the ride is essential that we look at how the distribution looks. + +The table below shows that the most common tip amount is close to \$2. However, we also observe a high max value of \$250, meaning there are probably some outliers. +So, let's take a closer look by plotting a box plot and a histogram of the tip amount column. + +.. nbimport:: + :path: ./example_notebooks/Examples Green Taxi.ipynb + :cells: 8 + :show_output: + +.. image:: ../_static/example_green_taxi_tip_amount_boxplot.svg + +.. image:: ../_static/example_green_taxi_tip_amount_distribution.svg + +Indeed we see some outliers. There are several tips amounts bigger than $50. We are still going to consider them since these are completely reasonable amounts. +Maybe some clients are very generous! + +Looking at the histogram below, we see that many passengers don't tip. This is something that we would expect in this kind of scenario. +A big group of people does not leave tips, and another one does. We can see a gap between both groups, meaning tipping very low is uncommon. + +.. nbimport:: + :path: ./example_notebooks/Examples Green Taxi.ipynb + :cells: 9 + :show_output: + +Training a model +================ + +We will train an LGBMRegressor with its default parameters. + +.. nbimport:: + :path: ./example_notebooks/Examples Green Taxi.ipynb + :cells: 10 + +Evaluating the model +==================== + +To evaluate the model, we will compare its train and test Mean Absolute Error with a baseline model that always predicts the mean of the training tip amount. + +.. nbimport:: + :path: ./example_notebooks/Examples Green Taxi.ipynb + :cells: 11 + +Below we plotted two scatter plots, one with the actual and predicted values for training and a similar one with the predicted values for the testing data. +Both mean absolute errors are relatively low, meaning the model performs well enough for this use case. + +.. nbimport:: + :path: ./example_notebooks/Examples Green Taxi.ipynb + :cells: 12 + +.. image:: ../_static/example_green_taxi_model_val.png + +It makes sense that the most relevant feature is the fare amount since the tip is often a percentage of it. +Interestingly, the drop-out location is more important than the pick-up location. Let's try to reason why. + +People often pick up a taxi in crowded places like cities and business centers. So, pick-up locations tend to be similar and less variable. +In contrast, drop-out locations can be very variable since people often take a taxi to their houses, restaurants, offices, etc. One could argue that +the drop-out location contains/encodes some information about the economic and social status of the passenger. Explaining why the drop-out location is more relevant +to predict the tip amount than the pick-up location. + +.. nbimport:: + :path: ./example_notebooks/Examples Green Taxi.ipynb + :cells: 13 + +.. image:: ../_static/example_green_taxi_feature_importance.svg + +Deploying the model +=================== + +To simulate that we are in a production environment, we will use the trained model to make predictions on unseen production data. + +We will later use NannyML to check how well the model performs on this data. + +.. nbimport:: + :path: ./example_notebooks/Examples Green Taxi.ipynb + :cells: 14 + :show_output: + +Analysing ML model performance in production +============================================ + +We need to create a reference and analysis set to properly analyze the model performance in production. + +* **Reference dataset:** The reference dataset should be one where the model behaves as expected. Ideally, one that the model did not see during training, but we know the correct targets and the model's predictions. This dataset allows us to establish a baseline for every metric we want to monitor. Ideally, we use the test set as a reference set, which is what we use in the code cell below. +* **Analysis dataset:** The analysis dataset is typically the latest production data up to a desired point in the past, which should be after the reference period ends. The analysis period is not required to have targets available. The analysis period is where NannyML analyzes/monitors the model's performance and data drift of the model using the knowledge gained from the reference set. + +.. nbimport:: + :path: ./example_notebooks/Examples Green Taxi.ipynb + :cells: 15 + +Estimating the model's performance +================================== + +Once an ML model is in production, we would like to get a view of how the model is performing. The tricky part is that we can not always measure the actual performance. +To measure it, we need the correct targets, in this case, the tip amounts. But these targets may take a while before they are updated in the system. +The tip goes straight to the taxi drivers, so we will only know the actual values when they report it. + +The good news is that we can leverage probabilistic methods to *estimate* the model performance. So instead of waiting for data to have targets, we will use a method +called `DLE `_, short for Direct Loss Estimation, to *estimate* the model +performance. + +The idea behind DLE is to train an extra ML model whose task is to estimate the value of the loss function of the monitored model. This can be later used to estimate +the original's model performance. DLE works for regression tasks like the one we are working on in this tutorial. But if you are interested in estimating the model +performance for a classification task, +check out `Estimating Performance for Classification `_. + +.. nbimport:: + :path: ./example_notebooks/Examples Green Taxi.ipynb + :cells: 16 + +.. nbimport:: + :path: ./example_notebooks/Examples Green Taxi.ipynb + :cells: 17 + +.. image:: ../_static/example_green_taxi_dle.svg + +The plot above shows that the estimated performance exceeded the threshold during some days of the last week of December, which means that the model failed to make +reliable predictions during those days. + +The next step is to go down the rabbit hole and figure out what went wrong during those days and see if we can find the root cause of these issues. + +We will use multivariate and univariate data drift detection methods to achieve this. They will allow us to check if a drift in the data caused the performance issue. + +Detecting multivariate data drift +================================= + +Multivariate data drift detection gives us a general overview of changes across the entire feature space. It detects if there is a drift in the general distribution of all +the features. So, instead of looking at the distribution of each feature independently, it looks at all features at once. + +This method allows us to look for more subtle changes in the data structure that univariate approaches cannot detect, such as changes in the linear relationships between +features. + +.. image:: ../_static/pca_reconstruction_error.svg + +To do this, we use the method `DataReconstructionDriftCalculator` which compresses the **reference feature space** to a latent space using a PCA algorithm. +The algorithm later decompresses the latent space data and reconstructs it with some error. This error is called the reconstruction error. + +We can later use the learned compressor/decompressor to transform the **production** +set and measure its reconstruction error. If the reconstruction error is bigger than a threshold, the structure learned by PCA no longer +accurately resembles the underlying structure of the analysis data. This indicates that there is data drift in the analysis/production data. + +To learn more about how this works, check out our +documentation `Data Reconstruction with PCA Deep Dive `_. + +.. nbimport:: + :path: ./example_notebooks/Examples Green Taxi.ipynb + :cells: 18 + +.. nbimport:: + :path: ./example_notebooks/Examples Green Taxi.ipynb + :cells: 19 + +.. image:: ../_static/example_green_taxi_pca_error.svg + +We don't see any multivariate drift happening. This may occur because the linear relationships between features did not change much, even though some features may have changed. + +Imagine the points moving from an area with an average reconstruction error of 1.2 to another that is ≈1.2 instead of one that is 2 x 1.2. +In this case, the reconstruction error wouldn't change. `DataReconstructionDriftCalculator` is not expected to always capture the drift. We need both multivariate and +univariate to have the full picture. + +Let's analyze it at a feature level and run the univariate drift detection methods. + +Detecting univariate data drift +=============================== + +Univariate drift detection allows us to perform a more granular investigation. This time we will look at each feature individually and compare the reference and +analysis periods in search for drift in any relevant feature. + +.. nbimport:: + :path: ./example_notebooks/Examples Green Taxi.ipynb + :cells: 20 + +.. nbimport:: + :path: ./example_notebooks/Examples Green Taxi.ipynb + :cells: 21 + +.. image:: ../_static/example_green_taxi_location_udc.svg + +.. nbimport:: + :path: ./example_notebooks/Examples Green Taxi.ipynb + :cells: 22 + +.. image:: ../_static/example_green_taxi_pickup_udc.svg + +On the plots above, we see some drift happening for the `DOLocationID` and the `pickup_time` columns around Dec 18th and the week of Christmas. + +Looking back at the performance estimation plot, we see that the performance did not drop on Dec 18th. This means that the drift on this date is a false alarm. + +What is more interesting is the week of the 25th. Again, we see a drift in the pick-up location and pick-up time that correlates with the dates of the performance drop. + +For this example, we picked the plots of the `DOLocationID` and the `pickup_time` since they are the two most important features showing data drift. + +But, If you want to check if the other features drifted, you can run the following code and analyze each column distribution. + +.. nbimport:: + :path: ./example_notebooks/Examples Green Taxi.ipynb + :cells: 23 + +.. image:: ../_static/example_green_taxi_all_udc.svg + +Bonus: Comparing realized and estimated performance +=================================================== + +When targets become available, we can calculate the actual model performance on production data. Also called realized performance. +In the cell below, we calculate the realized performance and compare it with NannyML's estimation. + +.. nbimport:: + :path: ./example_notebooks/Examples Green Taxi.ipynb + :cells: 24 + +.. image:: ../_static/example_green_taxi_dle_vs_realized.svg + + +In the plot above, the estimated performance is usually close to the realized one. Except for some points during the holidays where the performance degradation is bigger +than estimated. + +This may be because we have less than a year of data, so the model has no notion of what a holiday is and what it looks like. This is a sign of concept drift. +Currently, NannyML's algorithms don't support concept drift. But, the good news is that concept drift often coincides with data drift, +so in this case, `DLE `_ was able to pick up some of the degradation +issues during the holidays. + +Conclusion +========== + +We built an ML model to predict the tip amount a passenger will leave after a taxi ride. Then, we used this model to make predictions on actual production data. +And we applied NannyML's performance estimation to spot performance degradation patterns. We also used data drift detection methods to explain these performance issues. + +After finding what is causing the performance degradation issues, we need to figure out how to fix it. +Check out our previous blog post to learn six ways `to address data distribution shift `_. \ No newline at end of file diff --git a/docs/quick.rst b/docs/quick.rst index 87d6d36d..53a1abed 100644 --- a/docs/quick.rst +++ b/docs/quick.rst @@ -8,6 +8,12 @@ Quickstart What is NannyML? ---------------- +.. raw:: html + + + Open In Colab + + .. include:: ./common/quickstart_what_is_nannyml.rst From d9cd081b34788007bba52f5b900e4058b9014fdc Mon Sep 17 00:00:00 2001 From: Kishan Savant <66986430+NeoKish@users.noreply.github.com> Date: Thu, 13 Jul 2023 00:42:08 +0530 Subject: [PATCH 09/18] Updated the image src link (#317) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ccb94b28..895e7494 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ NannyML can also **track the realised performance** of your machine learning mod To detect **multivariate feature drift** NannyML uses [PCA-based data reconstruction](https://nannyml.readthedocs.io/en/main/how_it_works/data_reconstruction.html). Changes in the resulting reconstruction error are monitored over time and data drift alerts are logged when the reconstruction error in a certain period exceeds a threshold. This threshold is calculated based on the reconstruction error observed in the reference period. -

+

NannyML utilises statistical tests to detect **univariate feature drift**. We have just added a bunch of new univariate tests including Jensen-Shannon Distance and L-Infinity Distance, check out the [comprehensive list](https://nannyml.readthedocs.io/en/stable/how_it_works/univariate_drift_detection.html#methods-for-continuous-features). The results of these tests are tracked over time, properly corrected to counteract multiplicity and overlayed on the temporal feature distributions. (It is also possible to visualise the test-statistics over time, to get a notion of the drift magnitude.) From 4734931d34f30741ed7854a5007d3c2f677d493e Mon Sep 17 00:00:00 2001 From: Nikolaos Perrakis <89025229+nikml@users.noreply.github.com> Date: Wed, 12 Jul 2023 22:14:27 +0300 Subject: [PATCH 10/18] Library Updates (#318) * make threshold limits work for edge cases * update calculator parameters docs --- docs/tutorials/data_quality/missing.rst | 22 +++++++---- docs/tutorials/data_quality/unseen.rst | 22 +++++++---- .../multivariate_drift_detection.rst | 39 +++++++++++++------ .../univariate_drift_detection.rst | 34 ++++++++++++---- docs/tutorials/summary_stats/avg.rst | 20 +++++++--- docs/tutorials/summary_stats/count.rst | 18 ++++++--- docs/tutorials/summary_stats/median.rst | 20 +++++++--- docs/tutorials/summary_stats/std.rst | 20 +++++++--- docs/tutorials/summary_stats/sum.rst | 20 +++++++--- nannyml/thresholds.py | 4 +- 10 files changed, 155 insertions(+), 64 deletions(-) diff --git a/docs/tutorials/data_quality/missing.rst b/docs/tutorials/data_quality/missing.rst index 586e33cc..7cbf608a 100644 --- a/docs/tutorials/data_quality/missing.rst +++ b/docs/tutorials/data_quality/missing.rst @@ -38,14 +38,22 @@ The :class:`~nannyml.data_quality.missing.calculator.MissingValuesCalculator` cl the functionality needed for missing values calculations. We need to instantiate it with appropriate parameters: -- The names of the columns to be evaluated. -- Optionally, a boolean option indicating whether we want the absolute count of the missing +- **column_names:** A list with the names of columns to be evaluated. +- **normalize (Optional):** Optionally, a boolean option indicating whether we want the absolute count of the missing value instances or their relative ratio. By default it is set to true. -- Optionally, the name of the column containing the observation timestamps. -- Optionally, a chunking approach or a predefined chunker. If neither is provided, the default - chunker creating 10 chunks will be used. -- Optionally, a threshold strategy to modify the default one. See available threshold options - :ref:`here`. +- **timestamp_column_name (Optional):** The name of the column in the reference data that + contains timestamps. +- **chunk_size (Optional):** The number of observations in each chunk of data + used. Only one chunking argument needs to be provided. For more information about + :term:`chunking` configurations check out the :ref:`chunking tutorial`. +- **chunk_number (Optional):** The number of chunks to be created out of data provided for each + :ref:`period`. +- **chunk_period (Optional):** The time period based on which we aggregate the provided data in + order to create chunks. +- **chunker (Optional):** A NannyML :class:`~nannyml.chunk.Chunker` object that will handle the aggregation + provided data in order to create chunks. +- **thresholds (Optional):** The threshold strategy used to calculate the alert threshold limits. + For more information about thresholds, check out the :ref:`thresholds tutorial`. .. nbimport:: :path: ./example_notebooks/Tutorial - Missing Values.ipynb diff --git a/docs/tutorials/data_quality/unseen.rst b/docs/tutorials/data_quality/unseen.rst index de9fd104..a5127bf3 100644 --- a/docs/tutorials/data_quality/unseen.rst +++ b/docs/tutorials/data_quality/unseen.rst @@ -40,14 +40,22 @@ The :class:`~nannyml.data_quality.unseen.calculator.UnseenValuesCalculator` clas the functionality needed for unseen values calculations. We need to instantiate it with appropriate parameters: -- The names of the columns to be evaluated. They need to be categorical columns. -- Optionally, a boolean option indicating whether we want the absolute count of the unseen +- **column_names:** A list with the names of columns to be evaluated. They need to be categorical columns. +- **normalize (Optional):** Optionally, a boolean option indicating whether we want the absolute count of the missing value instances or their relative ratio. By default it is set to true. -- Optionally, the name of the column containing the observation timestamps. -- Optionally, a chunking approach or a predefined chunker. If neither is provided, the default - chunker creating 10 chunks will be used. -- Optionally, a threshold strategy to modify the default one. See available threshold options - :ref:`here`. +- **timestamp_column_name (Optional):** The name of the column in the reference data that + contains timestamps. +- **chunk_size (Optional):** The number of observations in each chunk of data + used. Only one chunking argument needs to be provided. For more information about + :term:`chunking` configurations check out the :ref:`chunking tutorial`. +- **chunk_number (Optional):** The number of chunks to be created out of data provided for each + :ref:`period`. +- **chunk_period (Optional):** The time period based on which we aggregate the provided data in + order to create chunks. +- **chunker (Optional):** A NannyML :class:`~nannyml.chunk.Chunker` object that will handle the aggregation + provided data in order to create chunks. +- **thresholds (Optional):** The threshold strategy used to calculate the alert threshold limits. + For more information about thresholds, check out the :ref:`thresholds tutorial`. .. warning:: diff --git a/docs/tutorials/detecting_data_drift/multivariate_drift_detection.rst b/docs/tutorials/detecting_data_drift/multivariate_drift_detection.rst index 39d28cff..19f40366 100644 --- a/docs/tutorials/detecting_data_drift/multivariate_drift_detection.rst +++ b/docs/tutorials/detecting_data_drift/multivariate_drift_detection.rst @@ -50,12 +50,30 @@ Let's start by loading some synthetic data provided by the NannyML package and s :cell: 2 The :class:`~nannyml.drift.multivariate.data_reconstruction.calculator.DataReconstructionDriftCalculator` -module implements this functionality. We need to instantiate it with appropriate parameters - the column names of the features we want to run drift detection on, -and the timestamp column name. The features can be passed in as a simple list of strings. Alternatively, we can create a list by excluding the columns in the dataframe that are not features, -and pass them into the argument. - -Next, the :meth:`~nannyml.base.AbstractCalculator.fit` method needs to be called on the reference data, which the results will be based on. -Then the +module implements this functionality. We need to instantiate it with appropriate parameters: + +- **column_names:** A list with the column names of the features we want to run drift detection on. +- **timestamp_column_name (Optional):** The name of the column in the reference data that + contains timestamps. +- **n_components (Optional):** The n_components parameter as passed to the sklearn `PCA constructor`_. +- **chunk_size (Optional):** The number of observations in each chunk of data + used. Only one chunking argument needs to be provided. For more information about + :term:`chunking` configurations check out the :ref:`chunking tutorial`. +- **chunk_number (Optional):** The number of chunks to be created out of data provided for each + :ref:`period`. +- **chunk_period (Optional):** The time period based on which we aggregate the provided data in + order to create chunks. +- **chunker (Optional):** A NannyML :class:`~nannyml.chunk.Chunker` object that will handle the aggregation + provided data in order to create chunks. +- **imputer_categorical (Optional):** An sklearn `SimpleImputer`_ object specifying an appropriate strategy + for imputing missing values for categorical features. +- **imputer_continuous (Optional):** An sklearn `SimpleImputer`_ object specifying an appropriate strategy + for imputing missing values for continuous features. +- **threshold (Optional):** The threshold strategy used to calculate the alert threshold limits. + For more information about thresholds, check out the :ref:`thresholds tutorial`. + +Next, the :meth:`~nannyml.base.AbstractCalculator.fit` method needs to be called on the reference data, +which the results will be based on. Then the :meth:`~nannyml.base.AbstractCalculator.calculate` method will calculate the multivariate drift results on the provided data. @@ -101,11 +119,8 @@ NannyML can also visualize the multivariate drift results in a plot. Our plot co * The purple step plot shows the reconstruction error in each chunk of the analysis period. Thick squared point markers indicate the middle of these chunks. - * The low-saturated purple area around the reconstruction error indicates the :ref:`sampling error`. - * The red horizontal dashed lines show upper and lower thresholds for alerting purposes. - * If the reconstruction error crosses the upper or lower threshold an alert is raised which is indicated with a red, low-saturated background across the whole width of the relevant chunk. A red, diamond-shaped point marker additionally indicates this in the middle of the chunk. @@ -118,9 +133,6 @@ NannyML can also visualize the multivariate drift results in a plot. Our plot co The multivariate drift results provide a concise summary of where data drift is happening in our input data. -.. _SimpleImputer: https://scikit-learn.org/stable/modules/generated/sklearn.impute.SimpleImputer.html - - Insights -------- @@ -137,3 +149,6 @@ estimate the impact of the observed changes. For more information on how multivariate drift detection works, the :ref:`Data Reconstruction with PCA` explanation page gives more details. + +.. _`PCA constructor`: https://scikit-learn.org/stable/modules/generated/sklearn.decomposition.PCA.html +.. _`SimpleImputer`: https://scikit-learn.org/stable/modules/generated/sklearn.impute.SimpleImputer.html diff --git a/docs/tutorials/detecting_data_drift/univariate_drift_detection.rst b/docs/tutorials/detecting_data_drift/univariate_drift_detection.rst index c210433e..fd1e3a4b 100644 --- a/docs/tutorials/detecting_data_drift/univariate_drift_detection.rst +++ b/docs/tutorials/detecting_data_drift/univariate_drift_detection.rst @@ -49,14 +49,34 @@ We begin by loading some synthetic data provided in the NannyML package. This is The :class:`~nannyml.drift.univariate.calculator.UnivariateDriftCalculator` class implements the functionality needed for univariate drift detection. First, we need to instantiate it with the appropriate parameters: -- The names of the columns to be evaluated. -- A list of methods to use on continuous columns. You can chose from :ref:`kolmogorov_smirnov`, - :ref:`jensen_shannon`, :ref:`wasserstein` - and :ref:`hellinger`. -- A list of methods to use on categorical columns. You can choose from :ref:`chi2`, :ref:`jensen_shannon`, +- **column_names:** A list with the names of columns to be evaluated. +- **treat_as_categorical (Optional):** A list of column names to treat as categorical columns. +- **timestamp_column_name (Optional):** The name of the column in the reference data that + contains timestamps. +- **categorical_methods (Optional):** A list of methods to use on categorical columns. + You can choose from :ref:`chi2`, :ref:`jensen_shannon`, :ref:`l_infinity`, and :ref:`hellinger`. -- Optionally, the name of the column containing the observation timestamps. -- Optionally, a chunking approach or a predefined chunker. If neither is provided, the default chunker creating 10 chunks will be used. +- **continuous_methods (Optional):** A list of methods to use on continuous columns. + You can chose from :ref:`kolmogorov_smirnov`, + :ref:`jensen_shannon`, + :ref:`wasserstein` + and :ref:`hellinger`. +- **chunk_size (Optional):** The number of observations in each chunk of data + used. Only one chunking argument needs to be provided. For more information about + :term:`chunking` configurations check out the :ref:`chunking tutorial`. +- **chunk_number (Optional):** The number of chunks to be created out of data provided for each + :ref:`period`. +- **chunk_period (Optional):** The time period based on which we aggregate the provided data in + order to create chunks. +- **chunker (Optional):** A NannyML :class:`~nannyml.chunk.Chunker` object that will handle the aggregation + provided data in order to create chunks. +- **thresholds (Optional):** A dictionary allowing users to set a custom threshold strategy for each method. + It links a `Threshold` subclass to a method name. + For more information about thresholds, check out the :ref:`thresholds tutorial`. +- **computation_params (Optional):** A dictionary which allows users to specify whether they want drift calculated on + the exact reference data or an estimated distribution of the reference data obtained + using binning techniques. Applicable only to Kolmogorov-Smirnov and Wasserstein. For more information look + :class:`~nannyml.drift.univariate.calculator.UnivariateDriftCalculator`. .. nbimport:: :path: ./example_notebooks/Tutorial - Drift - Univariate.ipynb diff --git a/docs/tutorials/summary_stats/avg.rst b/docs/tutorials/summary_stats/avg.rst index 5f4882fa..88874f14 100644 --- a/docs/tutorials/summary_stats/avg.rst +++ b/docs/tutorials/summary_stats/avg.rst @@ -36,12 +36,20 @@ The :class:`~nannyml.stats.avg.calculator.SummaryStatsAvgCalculator` class imple the functionality needed for mean values calculations. We need to instantiate it with appropriate parameters: -- The names of the columns to be evaluated. -- Optionally, the name of the column containing the observation timestamps. -- Optionally, a chunking approach or a predefined chunker. If neither is provided, the default - chunker creating 10 chunks will be used. -- Optionally, a threshold strategy to modify the default one. See available threshold options - :ref:`here`. +- **column_names:** A list with the names of columns to be evaluated. +- **timestamp_column_name (Optional):** The name of the column in the reference data that + contains timestamps. +- **chunk_size (Optional):** The number of observations in each chunk of data + used. Only one chunking argument needs to be provided. For more information about + :term:`chunking` configurations check out the :ref:`chunking tutorial`. +- **chunk_number (Optional):** The number of chunks to be created out of data provided for each + :ref:`period`. +- **chunk_period (Optional):** The time period based on which we aggregate the provided data in + order to create chunks. +- **chunker (Optional):** A NannyML :class:`~nannyml.chunk.Chunker` object that will handle the aggregation + provided data in order to create chunks. +- **threshold (Optional):** The threshold strategy used to calculate the alert threshold limits. + For more information about thresholds, check out the :ref:`thresholds tutorial`. .. nbimport:: :path: ./example_notebooks/Tutorial - Stats - Avg.ipynb diff --git a/docs/tutorials/summary_stats/count.rst b/docs/tutorials/summary_stats/count.rst index ae2194c2..b3e22fbd 100644 --- a/docs/tutorials/summary_stats/count.rst +++ b/docs/tutorials/summary_stats/count.rst @@ -34,11 +34,19 @@ The :class:`~nannyml.stats.count.calculator.SummaryStatsRowCountCalculator` clas the functionality needed for row count calculations. We need to instantiate it with appropriate *optional* parameters: -- The name of the column containing the observation timestamps. -- A chunking approach or a predefined chunker. If neither is provided, the default - chunker creating 10 chunks will be used. -- A threshold strategy to modify the default one. See available threshold options - :ref:`here`. +- **timestamp_column_name (Optional):** The name of the column in the reference data that + contains timestamps. +- **chunk_size (Optional):** The number of observations in each chunk of data + used. Only one chunking argument needs to be provided. For more information about + :term:`chunking` configurations check out the :ref:`chunking tutorial`. +- **chunk_number (Optional):** The number of chunks to be created out of data provided for each + :ref:`period`. +- **chunk_period (Optional):** The time period based on which we aggregate the provided data in + order to create chunks. +- **chunker (Optional):** A NannyML :class:`~nannyml.chunk.Chunker` object that will handle the aggregation + provided data in order to create chunks. +- **threshold (Optional):** The threshold strategy used to calculate the alert threshold limits. + For more information about thresholds, check out the :ref:`thresholds tutorial`. .. nbimport:: :path: ./example_notebooks/Tutorial - Stats - Count.ipynb diff --git a/docs/tutorials/summary_stats/median.rst b/docs/tutorials/summary_stats/median.rst index d13fd454..c9d96d48 100644 --- a/docs/tutorials/summary_stats/median.rst +++ b/docs/tutorials/summary_stats/median.rst @@ -36,12 +36,20 @@ The :class:`~nannyml.stats.avg.calculator.SummaryStatsMedianCalculator` class im the functionality needed for median values calculations. We need to instantiate it with appropriate parameters: -- The names of the columns to be evaluated. -- Optionally, the name of the column containing the observation timestamps. -- Optionally, a chunking approach or a predefined chunker. If neither is provided, the default - chunker creating 10 chunks will be used. -- Optionally, a threshold strategy to modify the default one. See available threshold options - :ref:`here`. +- **column_names:** A list with the names of columns to be evaluated. +- **timestamp_column_name (Optional):** The name of the column in the reference data that + contains timestamps. +- **chunk_size (Optional):** The number of observations in each chunk of data + used. Only one chunking argument needs to be provided. For more information about + :term:`chunking` configurations check out the :ref:`chunking tutorial`. +- **chunk_number (Optional):** The number of chunks to be created out of data provided for each + :ref:`period`. +- **chunk_period (Optional):** The time period based on which we aggregate the provided data in + order to create chunks. +- **chunker (Optional):** A NannyML :class:`~nannyml.chunk.Chunker` object that will handle the aggregation + provided data in order to create chunks. +- **threshold (Optional):** The threshold strategy used to calculate the alert threshold limits. + For more information about thresholds, check out the :ref:`thresholds tutorial`. .. nbimport:: :path: ./example_notebooks/Tutorial - Stats - Median.ipynb diff --git a/docs/tutorials/summary_stats/std.rst b/docs/tutorials/summary_stats/std.rst index 849a8e41..142bd8e1 100644 --- a/docs/tutorials/summary_stats/std.rst +++ b/docs/tutorials/summary_stats/std.rst @@ -36,12 +36,20 @@ The :class:`~nannyml.stats.std.calculator.SummaryStatsStdCalculator` class imple the functionality needed for standard deviation values calculations. We need to instantiate it with appropriate parameters: -- The names of the columns to be evaluated. -- Optionally, the name of the column containing the observation timestamps. -- Optionally, a chunking approach or a predefined chunker. If neither is provided, the default - chunker creating 10 chunks will be used. -- Optionally, a threshold strategy to modify the default one. See available threshold options - :ref:`here`. +- **column_names:** A list with the names of columns to be evaluated. +- **timestamp_column_name (Optional):** The name of the column in the reference data that + contains timestamps. +- **chunk_size (Optional):** The number of observations in each chunk of data + used. Only one chunking argument needs to be provided. For more information about + :term:`chunking` configurations check out the :ref:`chunking tutorial`. +- **chunk_number (Optional):** The number of chunks to be created out of data provided for each + :ref:`period`. +- **chunk_period (Optional):** The time period based on which we aggregate the provided data in + order to create chunks. +- **chunker (Optional):** A NannyML :class:`~nannyml.chunk.Chunker` object that will handle the aggregation + provided data in order to create chunks. +- **threshold (Optional):** The threshold strategy used to calculate the alert threshold limits. + For more information about thresholds, check out the :ref:`thresholds tutorial`. .. nbimport:: :path: ./example_notebooks/Tutorial - Stats - Std.ipynb diff --git a/docs/tutorials/summary_stats/sum.rst b/docs/tutorials/summary_stats/sum.rst index da2c4179..634d9062 100644 --- a/docs/tutorials/summary_stats/sum.rst +++ b/docs/tutorials/summary_stats/sum.rst @@ -36,12 +36,20 @@ The :class:`~nannyml.stats.sum.calculator.SummaryStatsSumCalculator` class imple the functionality needed for sum values calculations. We need to instantiate it with appropriate parameters: -- The names of the columns to be evaluated. -- Optionally, the name of the column containing the observation timestamps. -- Optionally, a chunking approach or a predefined chunker. If neither is provided, the default - chunker creating 10 chunks will be used. -- Optionally, a threshold strategy to modify the default one. See available threshold options - :ref:`here`. +- **column_names:** A list with the names of columns to be evaluated. +- **timestamp_column_name (Optional):** The name of the column in the reference data that + contains timestamps. +- **chunk_size (Optional):** The number of observations in each chunk of data + used. Only one chunking argument needs to be provided. For more information about + :term:`chunking` configurations check out the :ref:`chunking tutorial`. +- **chunk_number (Optional):** The number of chunks to be created out of data provided for each + :ref:`period`. +- **chunk_period (Optional):** The time period based on which we aggregate the provided data in + order to create chunks. +- **chunker (Optional):** A NannyML :class:`~nannyml.chunk.Chunker` object that will handle the aggregation + provided data in order to create chunks. +- **threshold (Optional):** The threshold strategy used to calculate the alert threshold limits. + For more information about thresholds, check out the :ref:`thresholds tutorial`. .. nbimport:: :path: ./example_notebooks/Tutorial - Stats - Sum.ipynb diff --git a/nannyml/thresholds.py b/nannyml/thresholds.py index 667b7de6..b39e7bff 100644 --- a/nannyml/thresholds.py +++ b/nannyml/thresholds.py @@ -264,7 +264,7 @@ def calculate_threshold_values( if ( lower_threshold_value_limit is not None and lower_threshold_value is not None - and lower_threshold_value < lower_threshold_value_limit + and lower_threshold_value <= lower_threshold_value_limit ): override_value = None if override_using_none else lower_threshold_value_limit if logger: @@ -277,7 +277,7 @@ def calculate_threshold_values( if ( upper_threshold_value_limit is not None and upper_threshold_value is not None - and upper_threshold_value > upper_threshold_value_limit + and upper_threshold_value >= upper_threshold_value_limit ): override_value = None if override_using_none else upper_threshold_value_limit if logger: From 63bcccef4d27feef9278c6679f3ef49f732c2629 Mon Sep 17 00:00:00 2001 From: niels Date: Wed, 12 Jul 2023 21:49:31 +0200 Subject: [PATCH 11/18] [skip ci] Update CHANGELOG.md Signed-off-by: niels --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b5e2500..f88fd2dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,25 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.9.1] - 2023-07-12 + +### Changed + +- Updated Mendable client library version to deal with styling overrides in the RTD documentation theme +- Removed superfluous limits for confidence bands in the CBPE class (these are present in the metric classes instead) +- Threshold value limiting behaviour (e.g. overriding a value and emitting a warning) will be triggered not only when +the value crosses the threshold but also when it is equal to the threshold value. This is because we interpret the +threshold as a theoretical maximum. + +### Added + +- Added a new example notebook walking through a full use case using the NYC Green Taxi dataset, based on the blog of [@santiviquez](https://github.com/santiviquez) + +### Fixed + +- Fixed broken Docker container build due to changes in public Poetry installation procedure +- Fixed broken image source link in the README, thanks [@NeoKish](https://github.com/NeoKish)! + ## [0.9.0] - 2023-06-26 ### Changed From 989b9020f5acef7ef498685c837959ed16777321 Mon Sep 17 00:00:00 2001 From: niels Date: Wed, 12 Jul 2023 21:50:00 +0200 Subject: [PATCH 12/18] =?UTF-8?q?Bump=20version:=200.9.0=20=E2=86=92=200.9?= =?UTF-8?q?.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- README.md | 8 ++++---- nannyml/__init__.py | 2 +- pyproject.toml | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 60546e68..90bc589f 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.9.0 +current_version = 0.9.1 commit = True tag = True diff --git a/README.md b/README.md index 895e7494..5947f39b 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ Allowing you to have the following benefits: | 🔬 **[Technical reference]** | Monitor the performance of your ML models. | | 🔎 **[Blog]** | Thoughts on post-deployment data science from the NannyML team. | | 📬 **[Newsletter]** | All things post-deployment data science. Subscribe to see the latest papers and blogs. | -| 💎 **[New in v0.9.0]** | New features, bug fixes. | +| 💎 **[New in v0.9.1]** | New features, bug fixes. | | 🧑‍💻 **[Contribute]** | How to contribute to the NannyML project and codebase. | | **[Join slack]** | Need help with your specific use case? Say hi on slack! | @@ -79,7 +79,7 @@ Allowing you to have the following benefits: [performance estimation]: https://nannyml.readthedocs.io/en/stable/how_it_works/performance_estimation.html [key concepts]: https://nannyml.readthedocs.io/en/stable/glossary.html [technical reference]: https://nannyml.readthedocs.io/en/stable/nannyml/modules.html -[new in v0.9.0]: https://github.com/NannyML/nannyml/releases/latest/ +[new in v0.9.1]: https://github.com/NannyML/nannyml/releases/latest/ [real world example]: https://nannyml.readthedocs.io/en/stable/examples/california_housing.html [blog]: https://www.nannyml.com/blog [newsletter]: https://mailchi.mp/022c62281d13/postdeploymentnewsletter @@ -264,11 +264,11 @@ Curious what we are working on next? Have a look at our [roadmap](https://bit.ly To cite NannyML in academic papers, please use the following BibTeX entry. -### Version 0.9.0 +### Version 0.9.1 ``` @misc{nannyml, - title = {{N}anny{ML} (release 0.9.0)}, + title = {{N}anny{ML} (release 0.9.1)}, howpublished = {\url{https://github.com/NannyML/nannyml}}, month = mar, year = 2023, diff --git a/nannyml/__init__.py b/nannyml/__init__.py index 0bcc5a7d..a139f7fe 100644 --- a/nannyml/__init__.py +++ b/nannyml/__init__.py @@ -31,7 +31,7 @@ # Dev branch marker is: 'X.Y.dev' or 'X.Y.devN' where N is an integer. # 'X.Y.dev0' is the canonical version of 'X.Y.dev' # -__version__ = '0.9.0' +__version__ = '0.9.1' import logging diff --git a/pyproject.toml b/pyproject.toml index 113ad8bb..cdac9cfa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool] [tool.poetry] name = "nannyml" -version = "0.9.0" +version = "0.9.1" homepage = "https://github.com/nannyml/nannyml" description = "NannyML, Your library for monitoring model performance." authors = ["Niels Nuyttens "] From 461058febee5996898be47cac495f12e573b34ed Mon Sep 17 00:00:00 2001 From: Michael Van de Steene <124588413+michael-nml@users.noreply.github.com> Date: Thu, 13 Jul 2023 16:35:19 +0200 Subject: [PATCH 13/18] Fix issues for building docs (#320) * Fix incorrect notebook references * Add computed output to green taxi notebook --- .../Examples Green Taxi.ipynb | 207 +++++++++++++++--- docs/examples/green_taxi.rst | 11 +- .../multiclass_performance_estimation.rst | 2 +- docs/tutorials/thresholds.rst | 2 - docs/tutorials/working_with_results.rst | 6 +- 5 files changed, 185 insertions(+), 43 deletions(-) diff --git a/docs/example_notebooks/Examples Green Taxi.ipynb b/docs/example_notebooks/Examples Green Taxi.ipynb index e6fd892a..8eaf721d 100644 --- a/docs/example_notebooks/Examples Green Taxi.ipynb +++ b/docs/example_notebooks/Examples Green Taxi.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "89298ce0", "metadata": {}, "outputs": [], @@ -14,7 +14,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "3c0635e7", "metadata": { "colab": { @@ -37,7 +37,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "6e90071a", "metadata": { "id": "6e90071a" @@ -52,7 +52,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "9843aa09", "metadata": { "colab": { @@ -62,14 +62,30 @@ "id": "9843aa09", "outputId": "6a2c8bdd-0a25-4ab2-abbe-6852d905d784" }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+----+------------------------+----------------+----------------+-----------------+------------+----------------+---------------+--------------+\n", + "| | lpep_pickup_datetime | PULocationID | DOLocationID | trip_distance | VendorID | payment_type | fare_amount | tip_amount |\n", + "+====+========================+================+================+=================+============+================+===============+==============+\n", + "| 0 | 2016-12-01 00:13:25 | 225 | 65 | 2.79 | 2 | 2 | 11 | 0 |\n", + "+----+------------------------+----------------+----------------+-----------------+------------+----------------+---------------+--------------+\n", + "| 1 | 2016-12-01 00:06:47 | 255 | 255 | 0.45 | 2 | 1 | 3.5 | 0.96 |\n", + "+----+------------------------+----------------+----------------+-----------------+------------+----------------+---------------+--------------+\n", + "| 2 | 2016-12-01 00:29:45 | 41 | 42 | 1.2 | 1 | 3 | 6 | 0 |\n", + "+----+------------------------+----------------+----------------+-----------------+------------+----------------+---------------+--------------+\n" + ] + } + ], "source": [ "print(data.head(3).to_markdown(tablefmt=\"grid\"))" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "a678285e", "metadata": { "id": "a678285e" @@ -93,7 +109,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "0e1af9ee", "metadata": { "id": "0e1af9ee" @@ -114,7 +130,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "id": "B2-H3Ra8GCYl", "metadata": { "id": "B2-H3Ra8GCYl" @@ -138,7 +154,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "id": "d68c2328", "metadata": { "colab": { @@ -149,14 +165,91 @@ "outputId": "ff8bdcf6-8083-4882-dc02-4342210b5023", "scrolled": true }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
tip_amount
count141568.000000
mean2.363484
std2.817078
min0.000000
25%1.060000
50%1.960000
75%3.000000
max250.700000
\n", + "
" + ], + "text/plain": [ + " tip_amount\n", + "count 141568.000000\n", + "mean 2.363484\n", + "std 2.817078\n", + "min 0.000000\n", + "25% 1.060000\n", + "50% 1.960000\n", + "75% 3.000000\n", + "max 250.700000" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "display(y_train.describe().to_frame())" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "id": "fa4dac07", "metadata": { "colab": { @@ -166,7 +259,28 @@ "id": "fa4dac07", "outputId": "3aef959b-5c61-4cbd-9b55-eb007b20c826" }, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "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zhEfHy9JcrlcbY63Hm+tAxAYriqgzJ8/X4WFhdq0aZMGDhzoOx4XF6dz586ptrbWr76yslJxcXG+mm9f1dO631rzbaGhoYqMjPTbAHQPGRkZSkpK0vLly+XxePzGPB6PcnNzlZycrIyMDIM6BGCEdgUUr9er+fPna/369fr888+VnJzsN37rrbeqR48e2rhxo+/Y/v37deTIEaWlpUmS0tLStGfPHlVVVflqNmzYoMjISI0YMeJafhcAXZDVatWKFStUWFgoh8PhdxWPw+FQYWGh8vLyWCALBJh2XcXzxBNPaO3atfrwww91ww03+I7bbDaFhYVJkh5//HF98sknWrNmjSIjI7VgwQJJ0hdffCHpwor9kSNHKiEhQS+++KIqKip0//3366GHHtLy5cuvqA+u4gG6n/z8fC1ZskSlpaW+Y8nJycrLy/O7VQGArqs939/tCigWi6XN42+//bbmzp0r6cKN2pYsWaL33ntPZ8+e1eTJk7Vq1Sq/0zeHDx/W448/rqKiIvXq1Utz5szRr3/9awUHX9mNbQkoQPfEnWSB7u26BRSzIKAAAND1dNp9UAAAAK4HAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADCddgeULVu2aPr06UpISJDFYlFBQYHf+Ny5c2WxWPy2KVOm+NWcPHlSs2fPVmRkpKKiovTggw+qoaHhmn4RAADQfbQ7oDQ2NuqWW27RG2+88Z01U6ZMUXl5uW977733/MZnz56tvXv3asOGDSosLNSWLVv0yCOPtL97AADQLQW39wV33XWX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5uZq0qRJslgs2rFjByEFCEDXdIrHYrFo/fr1cjgcki7MniQkJGjJkiVaunSppAtTtrGxsVqzZo1mzpypffv2acSIEdqxY4dGjRolSfr00081depUHTt2TAkJCZf9XE7xAN1HQ0ODevfuLYvFooEDB+ro0aO+scTERB07dkxer1f19fWKiIgwsFMA18qwO8keOnRIFRUVmjhxou+YzWbT2LFj5XQ6JUlOp1NRUVG+cCJJEydOVFBQkLZv397m+549e1Zut9tvA9A93H///ZIu/IEzcuRIv6t4Ro4cqda/oVrrAASGDg0oFRUVkqTY2Fi/47Gxsb6xiooKxcTE+I0HBwcrOjraV/Ntubm5stlsvi0xMbEj2wZgoAMHDki6cD+UgoICjRs3ThERERo3bpwKCgp89z9prQMQGLrEfVBycnJUV1fn2745BQyga2u9SqdXr15tPosnLCzMrw5AYOjQgBIXFydJqqys9DteWVnpG4uLi1NVVZXfeHNzs06ePOmr+bbQ0FBFRkb6bQC6hwcffFCS9Ne//tVvMb0kNTU1acOGDX51AAJDhwaU5ORkxcXFaePGjb5jbrdb27dvV1pamiQpLS1NtbW12rVrl6/m888/l8fj0dixYzuyHQBdQHJysqQLa1DCw8N155136le/+pXuvPNOhYeH+9agtNYBCAztvpNsQ0OD37ngQ4cOaffu3YqOjtagQYO0cOFCvfDCCxo6dKiSk5P1zDPPKCEhwXelz/DhwzVlyhQ9/PDDWr16tc6fP6/58+dr5syZV3QFD4DupfVZPFVVVTp9+rQ+++wzffbZZ77x8PBwxcbG8iweIMC0O6Ds3LlTmZmZvv3FixdLkubMmaM1a9bopz/9qRobG/XII4+otrZWt912mz799FP17NnT95p3331X8+fP1x133KGgoCDNmDFDr776agf8OgC6GqvVqn//93/XSy+9pH79+mnAgAE6d+6cQkJCdPz4cZ04cULZ2dk8iwcIMNzqHoChWlpaZLfb1a9fP1VXV+vw4cO+saSkJPXr1081NTUqKSkhpABdXHu+v3lYIABDFRcXq7S0VO+9955Gjx6t4uJilZeXKz4+XhkZGfqf//kfpaenq7i4WBMmTDC6XQCdhIACwFDl5eWSpJSUFFmt1otCSEpKil8dgMDQJe6DAqD7io+PlyS5XC61tLSoqKhI7733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ffq0fvWrX+mPf/yjtm7dqtraWs2cOfOKPqOhoUFTp07Vxo0b9eWXX2rKlCmaPn26jhw54lf3yiuvaPz48fryyy81bdo03X///XrggQd033336R//+IeGDBmiBx54wPeI9l27dulHP/qRZs6cqT179ui5557TM8884wsf7fGf//mfWrp0qXbv3q1hw4bp3nvvVXNzs9LT07Vy5UpFRkb6/n2WLl3a7vcHcJW8AALWv/3bv3n/4z/+w7e/adMmryTvqVOnvF6v1/v22297JXm3bdvmq9m3b59Xknf79u1X9Zk33XST97XXXvPtDx482Hvffff59svLy72SvM8884zvmNPp9ErylpeXe71er3fWrFneO++80+99n3zySe+IESN8+5K869ev96ux2Wzet99+2+v1er2HDh3ySvL+/ve/943v3bvXK8m7b98+3+9vs9mu6vcEcG2YQQFwScHBwRo9erRv/8Ybb1RUVJT27dt32dc2NDRo6dKlGj58uKKiohQREaF9+/ZdNINy8803+36OjY2VJKWmpl50rKqqSpK0b98+jR8/3u89xo8fr5KSErW0tLTr9/vmZ8fHx/t9DgDjsEgWwHWzdOlSbdiwQXl5ebLb7QoLC1N2drbOnTvnV9ejRw/fzxaL5TuPeTyeK/5si8XiOyXU6vz58xfVXevnALg+CChAAAsJCbnsjENzc7N27typMWPGSJL279+v2tpaDR8+/LLvv3XrVs2dO1f33HOPpAszKqWlpdfc9/Dhw7V169aLPmvYsGGyWq2SpP79+6u8vNw3XlJSotOnT7frc67k3wfA9cEpHiCAJSUlafv27SotLdWJEyfanDno0aOHFixYoO3bt2vXrl2aO3euxo0b5wsslzJ06FDl5+dr9+7d+t///V/NmjWrQ2YnlixZoo0bN+qXv/yl/vnPf+qdd97R66+/7reI9Qc/+IFef/11ffnll9q5c6cee+wxv9mSK5GUlKSGhgZt3LhRJ06caHfAAXD1CChAAFu6dKmsVqtGjBih/v37X7Q2RJLCw8P11FNPadasWRo/frwiIiL0pz/96Yre/+WXX1afPn2Unp6u6dOna/Lkyfr+979/zX1///vf1wcffKD3339fKSkpevbZZ/WLX/xCc+fO9dWsWLFCiYmJysjI0KxZs7R06VKFh4e363PS09P12GOP6cc//rH69++vF1988Zp7B3BlLN5vn6QFgP9vzZo1WrhwIbe+B9DpmEEBAACmQ0ABcNVuuukmRUREtLm9++67RrcHoAvjFA+Aq3b48OE2L92VLty7pHfv3p3cEYDugoACAABMh1M8AADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdAgoAADAdP4f8jvVs8nWLC0AAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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tGnKyMgwl8vKyhQTE6PExMRGN/BynE6n8vPzNWTIEAUHB3ttXH9EL1zohQu9cEc/XOiFS+1edMvKu/KNvmf2ZyV5ZZzavah5B+dqeByAunTpouLiYp0+fVrvv/++UlJStGXLlqueyLVgs9lks9nqrA8ODvbJE81X4/ojeuFCL1zohTv64UIvXGp6UVEV0NRT8Zi3f4c1vfDGuB4HoJCQEHXu3FmSFB8fr48//lgLFizQY489psrKSp06dcrtKFBpaamioqIkSVFRUXWu1qq5SuzSmtpXjpWWlio8PFzNmjVTUFCQgoKCLltTMwYAAEBDrvpzgKqrq1VRUaH4+HgFBweroKDA3Hbo0CEdO3ZMdrtdkmS327Vv3z63q7Xy8/MVHh6uuLg4s+bSMWpqasYICQlRfHy8W011dbUKCgrMGgAAgIZ4dARo2rRpGjp0qDp06KAzZ85o2bJl2rx5s/Ly8hQREaExY8YoIyNDrVu3Vnh4uJ577jnZ7Xb169dPkpSYmKi4uDg99dRTmjNnjhwOh6ZPn67U1FTzrakJEyZo4cKFmjJlip599llt2rRJK1as0Nq1a815ZGRkKCUlRX369FHfvn312muvqby83LwqDAAAoCEeBaATJ07o6aef1vHjxxUREaEePXooLy9PQ4YMkSTNnz9fgYGBGjlypCoqKpSUlKQ33njDvH1QUJDWrFmjiRMnym63q3nz5kpJSdFLL71k1sTGxmrt2rWaNGmSFixYoPbt2+utt94yL4GXpMcee0xff/21MjMz5XA41KtXL61fv77OidEAAACX41EAevvttxvcHhoaquzsbGVnZ9db07FjR61bt67Bcfr37689e/Y0WJOWlqa0tLQGawAAAC6H7wIDAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACW41EAmj17tu666y7deOONatu2rUaMGKFDhw651fTv318BAQFuPxMmTHCrOXbsmJKTkxUWFqa2bdtq8uTJunjxolvN5s2bdeedd8pms6lz587Kzc2tM5/s7Gx16tRJoaGhSkhI0M6dOz15OAAAwKI8CkBbtmxRamqqduzYofz8fDmdTiUmJqq8vNytbty4cTp+/Lj5M2fOHHNbVVWVkpOTVVlZqe3bt+udd95Rbm6uMjMzzZqSkhIlJydrwIABKi4uVnp6usaOHau8vDyzZvny5crIyNCMGTO0e/du9ezZU0lJSTpx4kRjewEAACziBk+K169f77acm5urtm3bqqioSPfff7+5PiwsTFFRUZcdY8OGDfrkk0+0ceNGRUZGqlevXpo1a5ZeeOEFZWVlKSQkRDk5OYqNjdXcuXMlSV27dtW2bds0f/58JSUlSZLmzZuncePGafTo0ZKknJwcrV27VosXL9bUqVM9eVgAAMBiPApAtZ0+fVqS1Lp1a7f1S5cu1bvvvquoqCgNHz5cL774osLCwiRJhYWF6t69uyIjI836pKQkTZw4UQcOHFDv3r1VWFiowYMHu42ZlJSk9PR0SVJlZaWKioo0bdo0c3tgYKAGDx6swsLCeudbUVGhiooKc7msrEyS5HQ65XQ6G9GBy6sZy5tj+it64UIvXOiFO/rhQi9cavfCFmQ05XQaxVu/x9q98Ma4jQ5A1dXVSk9P1z333KNu3bqZ65944gl17NhR0dHR2rt3r1544QUdOnRIf/7znyVJDofDLfxIMpcdDkeDNWVlZTp//rxOnjypqqqqy9YcPHiw3jnPnj1bM2fOrLN+w4YNZkDzpvz8fK+P6a/ohQu9cKEX7uiHC71wqenFnL5NPJFGWLdunVfHq+nFuXPnrnqsRgeg1NRU7d+/X9u2bXNbP378ePPf3bt3V7t27TRo0CAdOXJEt9xyS+Nn6gXTpk1TRkaGuVxWVqaYmBglJiYqPDzca/fjdDqVn5+vIUOGKDg42Gvj+iN64UIvXOiFO/rhQi9caveiW1belW/0PbM/K8kr49TuRc07OFejUQEoLS1Na9as0datW9W+ffsGaxMSEiRJhw8f1i233KKoqKg6V2uVlpZKknneUFRUlLnu0prw8HA1a9ZMQUFBCgoKumxNfeceSZLNZpPNZquzPjg42CdPNF+N64/ohQu9cKEX7uiHC71wqelFRVVAU0/FY97+Hdb0whvjenQVmGEYSktL06pVq7Rp0ybFxsZe8TbFxcWSpHbt2kmS7Ha79u3b53a1Vn5+vsLDwxUXF2fWFBQUuI2Tn58vu90uSQoJCVF8fLxbTXV1tQoKCswaAACA+nh0BCg1NVXLli3TBx98oBtvvNE8ZyciIkLNmjXTkSNHtGzZMg0bNkxt2rTR3r17NWnSJN1///3q0aOHJCkxMVFxcXF66qmnNGfOHDkcDk2fPl2pqanm0ZkJEyZo4cKFmjJlip599llt2rRJK1as0Nq1a825ZGRkKCUlRX369FHfvn312muvqby83LwqDAAAoD4eBaBFixZJ+u+HHV5qyZIleuaZZxQSEqKNGzeaYSQmJkYjR47U9OnTzdqgoCCtWbNGEydOlN1uV/PmzZWSkqKXXnrJrImNjdXatWs1adIkLViwQO3bt9dbb71lXgIvSY899pi+/vprZWZmyuFwqFevXlq/fn2dE6MBAABq8ygAGUbDl+DFxMRoy5YtVxynY8eOVzwzvH///tqzZ0+DNWlpaUpLS7vi/QEAAFyK7wIDAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWc0NTT+B61i0rTxVVAU09je/s6MvJTT0FAACuCY4AAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAy/EoAM2ePVt33XWXbrzxRrVt21YjRozQoUOH3GouXLig1NRUtWnTRi1atNDIkSNVWlrqVnPs2DElJycrLCxMbdu21eTJk3Xx4kW3ms2bN+vOO++UzWZT586dlZubW2c+2dnZ6tSpk0JDQ5WQkKCdO3d68nAAAIBFeRSAtmzZotTUVO3YsUP5+flyOp1KTExUeXm5WTNp0iT99a9/1cqVK7VlyxZ99dVXeuSRR8ztVVVVSk5OVmVlpbZv36533nlHubm5yszMNGtKSkqUnJysAQMGqLi4WOnp6Ro7dqzy8vLMmuXLlysjI0MzZszQ7t271bNnTyUlJenEiRNX0w8AAGABN3hSvH79erfl3NxctW3bVkVFRbr//vt1+vRpvf3221q2bJkGDhwoSVqyZIm6du2qHTt2qF+/ftqwYYM++eQTbdy4UZGRkerVq5dmzZqlF154QVlZWQoJCVFOTo5iY2M1d+5cSVLXrl21bds2zZ8/X0lJSZKkefPmady4cRo9erQkKScnR2vXrtXixYs1derUq24MAAC4fnkUgGo7ffq0JKl169aSpKKiIjmdTg0ePNisuf3229WhQwcVFhaqX79+KiwsVPfu3RUZGWnWJCUlaeLEiTpw4IB69+6twsJCtzFqatLT0yVJlZWVKioq0rRp08ztgYGBGjx4sAoLC+udb0VFhSoqKszlsrIySZLT6ZTT6WxkF+qqGcsWaHhtzGvBmz2oPaYvxvY39MKFXrijHy70wqV2L2xB/vU3RfLe77F2L7wxbqMDUHV1tdLT03XPPfeoW7dukiSHw6GQkBC1bNnSrTYyMlIOh8OsuTT81Gyv2dZQTVlZmc6fP6+TJ0+qqqrqsjUHDx6sd86zZ8/WzJkz66zfsGGDwsLCvsOj9sysPtVeH9OX1q1b57Ox8/PzfTa2v6EXLvTCHf1woRcuNb2Y07eJJ9II3v67UtOLc+fOXfVYjQ5Aqamp2r9/v7Zt23bVk7hWpk2bpoyMDHO5rKxMMTExSkxMVHh4uNfux+l0Kj8/Xy/uClRFdYDXxvW1/VlJXh+zphdDhgxRcHCw18f3J/TChV64ox8u9MKldi+6ZeVd+UbfM976u1K7FzXv4FyNRgWgtLQ0rVmzRlu3blX79u3N9VFRUaqsrNSpU6fcjgKVlpYqKirKrKl9tVbNVWKX1tS+cqy0tFTh4eFq1qyZgoKCFBQUdNmamjEux2azyWaz1VkfHBzskydaRXWAKqr8JwD58sXGVz32R/TChV64ox8u9MKlphf+9Pekhrd/hzW98Ma4Hl0FZhiG0tLStGrVKm3atEmxsbFu2+Pj4xUcHKyCggJz3aFDh3Ts2DHZ7XZJkt1u1759+9yu1srPz1d4eLji4uLMmkvHqKmpGSMkJETx8fFuNdXV1SooKDBrAAAA6uPREaDU1FQtW7ZMH3zwgW688UbznJ2IiAg1a9ZMERERGjNmjDIyMtS6dWuFh4frueeek91uV79+/SRJiYmJiouL01NPPaU5c+bI4XBo+vTpSk1NNY/OTJgwQQsXLtSUKVP07LPPatOmTVqxYoXWrl1rziUjI0MpKSnq06eP+vbtq9dee03l5eXmVWEAAAD18SgALVq0SJLUv39/t/VLlizRM888I0maP3++AgMDNXLkSFVUVCgpKUlvvPGGWRsUFKQ1a9Zo4sSJstvtat68uVJSUvTSSy+ZNbGxsVq7dq0mTZqkBQsWqH379nrrrbfMS+Al6bHHHtPXX3+tzMxMORwO9erVS+vXr69zYjQAAEBtHgUgw7jyJXihoaHKzs5WdnZ2vTUdO3a84pnh/fv31549exqsSUtLU1pa2hXnBAAAcCm+CwwAAFgOAQgAAFgOAQgAAFgOAQgAAFgOAQgAAFgOAQgAAFjOVX0bPAB8n3WauvbKRd8DtiBDc/pK3bLydOhX/9PU0wEsgSNAAADAcghAAADAcghAAADAcghAAADAcghAAADAcghAAADAcghAAADAcghAAADAcghAAADAcghAAADAcghAAADAcghAAADAcghAAADAcghAAADAcghAAADAcghAAADAcghAAADAcghAAADAcghAAADAcghAAADAcghAAADAcghAAADAcghAAADAcghAAADAcghAAADAcghAAADAcghAAADAcghAAADAcm5o6gkA8A+dpq712li2IENz+krdsvJUURXgtXEB4LviCBAAALAcAhAAALAcAhAAALAcAhAAALAcAhAAALAcjwPQ1q1bNXz4cEVHRysgIECrV6922/7MM88oICDA7efBBx90q/n222/15JNPKjw8XC1bttSYMWN09uxZt5q9e/fqvvvuU2hoqGJiYjRnzpw6c1m5cqVuv/12hYaGqnv37lq3bp2nDwcAAFiQxwGovLxcPXv2VHZ2dr01Dz74oI4fP27+/PGPf3Tb/uSTT+rAgQPKz8/XmjVrtHXrVo0fP97cXlZWpsTERHXs2FFFRUV65ZVXlJWVpTfffNOs2b59ux5//HGNGTNGe/bs0YgRIzRixAjt37/f04cEAAAsxuPPARo6dKiGDh3aYI3NZlNUVNRlt3366adav369Pv74Y/Xp00eS9Prrr2vYsGF69dVXFR0draVLl6qyslKLFy9WSEiI7rjjDhUXF2vevHlmUFqwYIEefPBBTZ48WZI0a9Ys5efna+HChcrJybnsfVdUVKiiosJcLisrkyQ5nU45nU7PGtGAmrFsgYbXxrwWvNmD2mP6Ymx/4++9sAV5b3+ueW7423PEVy7th7/uH97i788Tb6rdC28+B68Vb/0ea/fCG+MGGIbR6I4GBARo1apVGjFihLnumWee0erVqxUSEqJWrVpp4MCB+uUvf6k2bdpIkhYvXqznn39eJ0+eNG9z8eJFhYaGauXKlXr44Yf19NNPq6yszO3ttY8++kgDBw7Ut99+q1atWqlDhw7KyMhQenq6WTNjxgytXr1a//znPy8736ysLM2cObPO+mXLliksLKyxbQAAANfQuXPn9MQTT+j06dMKDw9v1Bhe/yToBx98UI888ohiY2N15MgR/fznP9fQoUNVWFiooKAgORwOtW3b1n0SN9yg1q1by+FwSJIcDodiY2PdaiIjI81trVq1ksPhMNddWlMzxuVMmzZNGRkZ5nJZWZliYmKUmJjY6AZejtPpVH5+vl7cFaiKav/5lNv9WUleH7OmF0OGDFFwcLDXx/cn/t6Lbll5XhvLFmhoVp9qv3uO+Mql/SjKfPDKN7iO+fvzxJtq98Kbz8FrxVt/V2r3ouYdnKvh9QA0atQo89/du3dXjx49dMstt2jz5s0aNGiQt+/OIzabTTabrc764OBgnzzRKqoD/Opj/n35YuOrHvsjf+2FL/Zlf3uO+FpFdYBf7hu+4K/PE1+o6YU/Ple8/Tus6YU3xvX5ZfA/+MEPdNNNN+nw4cOSpKioKJ04ccKt5uLFi/r222/N84aioqJUWlrqVlOzfKWa+s49AgAAqOHzAPTvf/9b33zzjdq1aydJstvtOnXqlIqKisyaTZs2qbq6WgkJCWbN1q1b3U5yys/PV5cuXdSqVSuzpqCgwO2+8vPzZbfbff2QAACAn/M4AJ09e1bFxcUqLi6WJJWUlKi4uFjHjh3T2bNnNXnyZO3YsUNHjx5VQUGBHnroIXXu3FlJSf99H7Br16568MEHNW7cOO3cuVN///vflZaWplGjRik6OlqS9MQTTygkJERjxozRgQMHtHz5ci1YsMDt/J2f/exnWr9+vebOnauDBw8qKytLu3btUlpamhfaAgAArmceB6Bdu3apd+/e6t27tyQpIyNDvXv3VmZmpoKCgrR371798Ic/1G233aYxY8YoPj5ef/vb39zOvVm6dKluv/12DRo0SMOGDdO9997r9hk/ERER2rBhg0pKShQfH6/nn39emZmZbp8VdPfdd2vZsmV688031bNnT73//vtavXq1unXrdjX9AAAAFuDxSdD9+/dXQ1fO5+Vd+Sz11q1ba9myZQ3W9OjRQ3/7298arHn00Uf16KOPXvH+AAAALsV3gQEAAMshAAEAAMshAAEAAMshAAEAAMshAAEAAMshAAEAAMshAAEAAMshAAEAAMshAAEAAMshAAEAAMshAAEAAMshAAEAAMshAAEAAMshAAEAAMshAAEAAMshAAEAAMshAAEAAMshAAEAAMshAAEAAMshAAEAAMu5oaknAFyNTlPXNvUUvjNbkKE5fZt6FgAAiSNAAADAgghAAADAcghAAADAcghAAADAcghAAADAcghAAADAcghAAADAcghAAADAcghAAADAcghAAADAcghAAADAcghAAADAcghAAADAcvg2eAD4Huk0dW1TT8FjR19ObuopAB7jCBAAALAcAhAAALAcAhAAALAcAhAAALAcAhAAALAcjwPQ1q1bNXz4cEVHRysgIECrV692224YhjIzM9WuXTs1a9ZMgwcP1meffeZW8+233+rJJ59UeHi4WrZsqTFjxujs2bNuNXv37tV9992n0NBQxcTEaM6cOXXmsnLlSt1+++0KDQ1V9+7dtW7dOk8fDgAAsCCPA1B5ebl69uyp7Ozsy26fM2eOfvvb3yonJ0f/+Mc/1Lx5cyUlJenChQtmzZNPPqkDBw4oPz9fa9as0datWzV+/Hhze1lZmRITE9WxY0cVFRXplVdeUVZWlt58802zZvv27Xr88cc1ZswY7dmzRyNGjNCIESO0f/9+Tx8SAACwGI8/B2jo0KEaOnToZbcZhqHXXntN06dP10MPPSRJ+v3vf6/IyEitXr1ao0aN0qeffqr169fr448/Vp8+fSRJr7/+uoYNG6ZXX31V0dHRWrp0qSorK7V48WKFhITojjvuUHFxsebNm2cGpQULFujBBx/U5MmTJUmzZs1Sfn6+Fi5cqJycnEY1AwAAWINXPwixpKREDodDgwcPNtdFREQoISFBhYWFGjVqlAoLC9WyZUsz/EjS4MGDFRgYqH/84x96+OGHVVhYqPvvv18hISFmTVJSkn7zm9/o5MmTatWqlQoLC5WRkeF2/0lJSXXekrtURUWFKioqzOWysjJJktPplNPpvNqHb6oZyxZoeG3Ma8GbPag9pi/GliRbkP/0uGZ/8FUvfM2bva7phb89R3zF3/vhi9dPf32eeFPtXvjT610Nb/0ea/fCG+N6NQA5HA5JUmRkpNv6yMhIc5vD4VDbtm3dJ3HDDWrdurVbTWxsbJ0xara1atVKDoejwfu5nNmzZ2vmzJl11m/YsEFhYWHf5SF6ZFafaq+P6Uu+PIcqPz/fJ+PO6euTYX3KV73wNV/02t+eI77mr/3wxWuHvz5PfKGmF/74euftfaOmF+fOnbvqsSz1VRjTpk1zO2pUVlammJgYJSYmKjw83Gv343Q6lZ+frxd3BaqiOsBr4/ra/qwkr49Z04shQ4YoODjY6+N3y8rz+pi+Ygs0NKtPtc964Wve7HVNL/ztOeIr/t4Pb752+Po1w5/U7oU/vd7V8Na+UbsXNe/gXA2vBqCoqChJUmlpqdq1a2euLy0tVa9evcyaEydOuN3u4sWL+vbbb83bR0VFqbS01K2mZvlKNTXbL8dms8lms9VZHxwc7JMnWkV1gCqq/OfFzJcvNj7rsR/1t4aveuFrvui1vz1HfM1f++GL/dlfnye+UNML9g1XL7wxrlc/Byg2NlZRUVEqKCgw15WVlekf//iH7Ha7JMlut+vUqVMqKioyazZt2qTq6molJCSYNVu3bnV7jy8/P19dunRRq1atzJpL76empuZ+AAAA6uPxEaCzZ8/q8OHD5nJJSYmKi4vVunVrdejQQenp6frlL3+pW2+9VbGxsXrxxRcVHR2tESNGSJK6du2qBx98UOPGjVNOTo6cTqfS0tI0atQoRUdHS5KeeOIJzZw5U2PGjNELL7yg/fv3a8GCBZo/f755vz/72c/0wAMPaO7cuUpOTtZ7772nXbt2uV0qD3wfdcvK88v/kwOA64nHAWjXrl0aMGCAuVxzTk1KSopyc3M1ZcoUlZeXa/z48Tp16pTuvfderV+/XqGhoeZtli5dqrS0NA0aNEiBgYEaOXKkfvvb35rbIyIitGHDBqWmpio+Pl433XSTMjMz3T4r6O6779ayZcs0ffp0/fznP9ett96q1atXq1u3bo1qBAAAsA6PA1D//v1lGPVfihcQEKCXXnpJL730Ur01rVu31rJlyxq8nx49euhvf/tbgzWPPvqoHn300YYnDAAAUAvfBQYAACyHAAQAACyHAAQAACyHAAQAACyHAAQAACzHUl+FAQDwvk5T13ptLFuQoTl9ff95WUdfTvbZ2PAPBCAAgOV4M7T5yrUKg1bFW2AAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByCEAAAMByvB6AsrKyFBAQ4PZz++23m9svXLig1NRUtWnTRi1atNDIkSNVWlrqNsaxY8eUnJyssLAwtW3bVpMnT9bFixfdajZv3qw777xTNptNnTt3Vm5urrcfCgAAuE755AjQHXfcoePHj5s/27ZtM7dNmjRJf/3rX7Vy5Upt2bJFX331lR555BFze1VVlZKTk1VZWant27frnXfeUW5urjIzM82akpISJScna8CAASouLlZ6errGjh2rvLw8XzwcAABwnbnBJ4PecIOioqLqrD99+rTefvttLVu2TAMHDpQkLVmyRF27dtWOHTvUr18/bdiwQZ988ok2btyoyMhI9erVS7NmzdILL7ygrKwshYSEKCcnR7GxsZo7d64kqWvXrtq2bZvmz5+vpKQkXzwkAABwHfFJAPrss88UHR2t0NBQ2e12zZ49Wx06dFBRUZGcTqcGDx5s1t5+++3q0KGDCgsL1a9fPxUWFqp79+6KjIw0a5KSkjRx4kQdOHBAvXv3VmFhodsYNTXp6ekNzquiokIVFRXmcllZmSTJ6XTK6XR64ZHLHE+SbIGG18a8FrzZg9pj+mJsSbIF+U+Pa/YHf9svfIFeuKMfLvTC5Xrohbde+2v/LfHGuF4PQAkJCcrNzVWXLl10/PhxzZw5U/fdd5/2798vh8OhkJAQtWzZ0u02kZGRcjgckiSHw+EWfmq212xrqKasrEznz59Xs2bNLju32bNna+bMmXXWb9iwQWFhYY16vA2Z1afa62P60rp163w2dn5+vk/GndPXJ8P6lL/tF75EL9zRDxd64eLPvfD235WavyXnzp276rG8HoCGDh1q/rtHjx5KSEhQx44dtWLFinqD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C3rvvfcMm81m5ObmGp988okxfvx4o2XLlobD4TAMwzCeeuopY+rUqWb93//+d+OGG24wXn31VePTTz81ZsyYYQQHBxv79u1rqofgNZ72YubMmUZeXp5x5MgRo6ioyBg1apQRGhpqHDhwoKkegtecOXPG2LNnj7Fnzx5DkjFv3jxjz549xhdffGEYhmFMnTrVeOqpp8z6zz//3AgLCzMmT55sfPrpp0Z2drYRFBRkrF+/vqkegtd42ov58+cbq1evNj777DNj3759xs9+9jMjMDDQ2LhxY1M9BK+ZOHGiERERYWzevNk4fvy4+XPu3DmzxiqvGY3pxfX6mjF16lRjy5YtRklJibF3715j6tSpRkBAgLFhwwbDMKyzTxiG573w1j5BAGqk119/3ejQoYMREhJi9O3b19ixY4e57YEHHjBSUlLc6lesWGHcdtttRkhIiHHHHXcYa9euvcYz9h1PepGenm7WRkZGGsOGDTN2797dBLP2vppLuWv/1Dz+lJQU44EHHqhzm169ehkhISHGD37wA2PJkiXXfN6+4GkvfvOb3xi33HKLERoaarRu3dro37+/sWnTpqaZvJddrg+S3H7XVnnNaEwvrtfXjGeffdbo2LGjERISYtx8883GoEGDzD/4hmGdfcIwPO+Ft/aJAMMwDM+OGQEAAPg3zgECAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACWQwACAACW8/8BXceURS8O4SsAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "y_train.plot(kind='box')\n", "plt.savefig('../_static/example_green_taxi_tip_amount_boxplot.svg', format='svg')\n", @@ -179,7 +293,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "id": "528fbf0f", "metadata": { "colab": { @@ -202,7 +316,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "id": "4fd0fe9b", "metadata": { "id": "4fd0fe9b" @@ -223,7 +337,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "id": "beb7b032", "metadata": { "colab": { @@ -234,7 +348,18 @@ "outputId": "f750cf58-636f-4f80-aee9-c01dc30c87e7", "scrolled": false }, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "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lQ61K++2+zCn0r/TvowaNcqw2WzlPta0aVNj8ODBe/39I444wjjkkEOihtT5fD6jVatWBmC8//77kf2XX365ce+99xoffPCB8cYbbxgDBgwwAOOCCy7YZ5xvvvlmuUO4y3PJJZcYjRs33uPjwWDQ+PTTT41zzjnHsNvths1mM/r162e8//77UcPmyvPHH38Yt9xyi9G8eXMDMA455BDjkUceMTZu3LjPuMpTmeHxGzduNFJTUyPDEydMmFDu8PhgMGiMHj3acDgcke+JzWYzXnrppajjnnjiCQMwZs+eHbX/jjvuMADjrLPOKhNDr169jA4dOuxX3CLScO3ens+ePdsAjEmTJkUdN3369Kj9H330Ubn/v+2qssPj47HN3t3+Do/v169f5P3tqqioyACMO+64wzAMw1iwYIEBGG+88UaZY2+99VYDiDqv2tVvv/1mAMZtt91WoZh69uxppKWlGdu3b9/rcYFAwLBYLMbNN9+8x2O2bNliPPnkk8bhhx9uAEaTJk2MG2+8cZ9T3AKBgDF16lTjnHPOMRwOh2Gz2YwzzzzT+PDDD/d5XrCnOPbnO2kY5tTAQYMGGa+99poxdepUY9y4cUbjxo2NhIQE46effoocN2XKFAMwvvvuuzLPcf755xuZmZll9l911VWR757VajXOO++8MtMCNm3aZJx++ulR39MmTZoYc+bMKTfehx9+2ADKTKGT2qWedqnTXC4Xw4cPL7N/1x7fgoICtm7dyimnnEJxcXGFhp1deOGFNGrUKHL/lFNOAWDlypUVjq1Xr15kZGTwzjvvYBgG77zzDhdddNEej8/NzeWLL76IOmbQoEFYLJY9DnH64IMPmDFjRtRtwoQJUcd88803FRrmVVJSUqaHtVRCQgIlJSV7/f1rr72Wv/76ixEjRvDHH3/w22+/cemll0aK4+z6+6+99hr33HMPAwcOZOjQoUydOpUrr7yS9957r9zKqKVKr/Z26dKFYcOG7fM95ebmRn2Ou/rXv/5Fq1at6N+/P0uXLuXBBx9k3bp1TJs2jUGDBu1z2HqHDh147LHHWL9+PVOnTqVDhw7cfffdtGzZknPOOYclS5bsM76quv322znooIO44oor9nqczWajbdu29O7dm9dff513332X/v37c91110UN/7/44otxu91cfvnlzJgxg9WrV/PKK6/w4osvApT7HWjUqBFbt26t1vclIg3HlClTcLvdnHHGGVE90J06dSIlJYWvv/4a2Fmsbdq0aRUe9bY/4q3NrqqSkhJcLleZ/aVF90r/vy/9WZFjd1c66mDXofF78vDDDzNz5kweeeSRfRbm27ZtG4ZhlNu+//XXX1xwwQUccMAB3HrrrRx44IG8//77bNiwgaeeeoojjzxyr89tt9sZMGAAH330EevXr+fRRx9lzZo1DBw4kBYtWnDrrbfWyPdvVyeddBLvv/8+l19+OQMGDOCOO+5g3rx5WCyWqNFx+/psyvtcbrzxRmbMmMHrr79O3759CYVC+P3+qGOSkpI49NBDGTZsGFOmTOH//u//yMrKYuDAgSxfvrzMc5Z+DjoXiC1Vj5c67YADDig30fz999+56667+Oqrr8rMO/Z4PPt83l3nbcHO/5B2n1+3Nw6Hg/PPP5/JkydzwgknsG7dur0Os3v33XcJBAIcc8wxUf8pdu7cmUmTJjFq1Kgyv9OtW7cKDSOriMTExDL/cZfyer3lDn3f1dVXX826det47LHHeP311wFzqP1tt93GQw89VG7F3F3dfPPN/Pe//2XmzJmceOKJZR7PycmhX79+uN1u3n///QpXi93Tyc/DDz8MmCcTzz333B6T+30pbeD79+/Pe++9x8iRI5k6dSo9evTY58lBVcybN48333yTWbNmYbXu/frqI488wjPPPMPff/8d+RwuuOACTj31VEaNGsVZZ52F3W4nMzOTTz75hKFDh9KrVy/AnMf23HPPMWzYsHI/Q8MwKj2nUkTk77//xuPx0KxZs3If37x5MwDdu3dn0KBB3HfffTz11FP06NGDc845h4svvrjcpGV/xVubXVWJiYnlrjZSuuRaaZtf+rMix+7KMAwmT55Mx44d99kWvvvuu9x1112MGDGCa665psLvobz2fc6cOUyZMoXk5GReeeUVhg4dus82ck+aNWvGzTffzJVXXsnNN9/Mq6++yuOPP86//vWvSlf8r6x27dpx9tln8+GHHxIKhbDZbPv8bMr7XNq3b0/79u0BcypGr1696N+/P/Pnz4+05eeffz52uz1qObizzz6bgw8+mH/961+8++67Uc9Z+jnoXCC21NMudVp5/yHl5eXRvXt3fvnlF+6//34+/fRTZsyYwaOPPgpQoSXe9pQQ7u/V74svvpjFixdz7733ctRRR3HYYYft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8dBDD5GRkRE59vnnn+e+++7j66+/pkePHgB899133H///fTq1YvGjRszb948JkyYQJ8+faKWCPnpp5+46KKLuOiii2jXrl2k1/qHH35g5MiRUe/v5ptv5pNPPqF///5s27aNt956KyrmSy65ZK/vqWvXrjRu3JiZM2dy2mmn7fVYi8XCGWecwRlnnEFubi5vvvkmr732GnPmzIksgQPmcoAXXnghV1xxBZ07d97rc+7LkiVL+OSTTwBYvnw5Ho+HBx98EDDXg+/fvz9gLrnToUMHhg0bxsSJEwEic853Vdqz3r1796glYO644w4uueQSOnfuzMiRI0lMTOTtt99m0aJFPPjgg1FF97p3706XLl1o164dOTk5vPLKKxQWFjJt2rQy8wJnzpyJYRj7/H6LiOxJ9+7dueqqqxg3bhyLFy+mV69eOBwO/v77b6ZMmcIzzzzDeeedx+uvv86LL77IueeeS9u2bSkoKOC///0vaWlpkdFhiYmJHHbYYbz77rsccsghZGRk0LFjRzp27FjheOKlzQb49NNP+eWXXwCzF3TJkiWRNmTAgAGRueQfffQRw4cPZ8KECVx22WUAtGjRghtvvJHHHnuMQCDA8ccfz8cff8zs2bOZNGlS1DTBO++8kylTpnDqqadyww03UFhYyGOPPcYRRxxRbjHgL774gtzc3D0OjS8oKKB3795s376dW2+9lf/9739Rj7dt25YuXbrs9b2fffbZvPnmm/z111/7nH7ndru59tprufbaa/n555959dVXmTx5Mk8++WSkLtKiRYsiheeGDRu2xxoLFfXmm2+yZs0aiouLAfMcqfSzGTp0KK1btwbKP5+68MILSUxM5KSTTqJZs2b88ccfvPLKKyQlJfHII49Evc5DDz3ESSedRPfu3Rk5ciTr16/niSeeoFevXvTp0ydy3FVXXUV+fj7dunXjgAMOICcnh0mTJvHnn3/yxBNPRL6XTZs25fLLL+fVV1/l9NNPZ+DAgRQUFPDiiy9SUlISVQgPzJoTS5YsKbeGg9Sy2F4zEDHtT0+7YZjLZZx44olGYmKikZ2dbdx2223GF198EZOe9vLsGvuiRYsMwLj77rv3ePzq1asNwLjpppsMw9j78jG7v8f9XT7mlVdeMQ499FDD6XQabdu2NZ566qkyvRulr7/r6yxfvtzo1auX0aRJE8Plchnt27c3xo0bF3Ul2zAMY+XKlcb5559vHHjggUZCQoKRlJRkdOrUyRg/fnyZ1ymNfU+3irj++uuNdu3aVfj9766wsHCv96uidIm28m679piUfi/3tZzPnpZ8Mwxz+aTu3bsbTZo0MZxOp3HEEUeUu1ziTTfdZBx00EGGy+UymjZtalx88cXGihUryn29Cy+80Ojatet+vWcRadjKa88Nw2x7OnXqZCQmJhqpqanGEUccYdx2222RHu+ffvrJuOiii4xWrVoZLpfLaNasmXHWWWcZCxcujHqeOXPmGJ06dTKcTuc+2+14b7OHDRu2x+ecMGFC5LjStmHXfYZhGKFQyHj44YeN1q1bG06n0zj88MONt956q9zX+u2334xevXoZSUlJRnp6ujFkyBAjJyen3GMHDx5sOByOqBF6uypt0yrS/u2Jz+czmjRpYjzwwAP7PLY8JSUlUaPHqrNtN4y9n7/s+nmXdz71zDPPGCeccIKRkZFh2O12Iysry7jkkkuMv//+u9zXmj17tnHSSScZCQkJRtOmTY1Ro0ZF9bwbhmG8/fbbRs+ePY3mzZsbdrvdaNSokdGzZ09j6tSpZZ4vEAgYzz33nHH00UcbKSkpRkpKinHqqacaX331VZljX3rpJSMpKanM60ntsxhGjLonRUSqwcqVK2nfvj2ff/45p59+eqzDqTdycnJo06YN77zzjnraRUSk1j3wwANMmDCBv//+u8Irykj1OuaYY+jRowdPPfVUrENp8JS0i0jcu+aaa1i+fDkzZsyIdSj1xh133MFXX33Fjz/+GOtQRESkASosLOSggw7iqaeeqtBa8FK9pk+fznnnncfKlSurPJ1Aqk5Ju4iIiIiIiEgdpXXaRUREREREROooJe0iIiIiIiIidZSSdhEREREREZE6Skm7iIiIiIiISB1lj3UAdUE4HGbDhg2kpqZisVhiHY6IiAiGYVBQUEB2djZWq66xV5XaehERqWsq2tYraQc2bNhAy5YtYx2GiIhIGevWraNFixaxDiPuqa0XEZG6al9tvZJ2IDU1FTD/WGlpaTGORkREBPLz82nZsmWkjZKqUVsvIiJ1TUXbeiXtEBkml5aWpoZcRETqFA3lrh5q60VEpK7aV1uvSXIiIiIiIiIidZSSdhEREREREZE6Skm7iIiIiIiISB2lpF1ERERERESkjlLSLiIiIiIiIlJHKWkXERERERERqaOUtIuIiIiIiIjUUUraRUREREREROqomCbt48aN4/jjjyc1NZVmzZpxzjnnsGzZsqhjevTogcViibpdffXVUcesXbuWfv36kZSURLNmzbj11lsJBoO1+VZEREREREREql1Mk/Zvv/2WUaNGMW/ePGbMmEEgEKBXr14UFRVFHXfllVeycePGyO0///lP5LFQKES/fv3w+/3MmTOH119/nYkTJ/Lvf/+7tt+OiIg0ZF9+CeedB35/rCMRERGRmrB2Lbz2GmzfXqsvG9Okffr06Vx22WUcfvjhHHXUUUycOJG1a9eyaNGiqOOSkpLIzMyM3NLS0iKPffnll/zxxx+89dZbHH300fTt25cHHniAF154Af8eTpx8Ph/5+flRNxERkUqbNAl694YPPoBx42IdTZ1SkVF1Xq+XUaNG0bhxY1JSUhg0aBCbNm2KOkaj6kREJKaKiqB1a7jiCnjssVp96To1p93j8QCQkZERtX/SpEk0adKEjh07MnbsWIqLiyOPzZ07lyOOOILmzZtH9vXu3Zv8/Hx+//33cl9n3LhxuN3uyK1ly5Y18G5ERKTeC4XMnwccABaLuT1mTOziqYMqMqrupptu4tNPP2XKlCl8++23bNiwgYEDB0Ye16g6ERGJueRkuO02c3vw4Fp9aYthGEatvuIehMNhBgwYQF5eHt9//31k/yuvvELr1q3Jzs5myZIl3H777Zxwwgl8+OGHAIwcOZI1a9bwxRdfRH6nuLiY5ORkPvvsM/r27VvmtXw+Hz6fL3I/Pz+fli1b4vF4onrxRURE9uijj+COO+B//4N27eCHH6BLF7BWz/Xw/Px83G53vWubtmzZQrNmzfj222/p1q0bHo+Hpk2bMnnyZM477zwA/vzzTzp06MDcuXM58cQT+fzzzznrrLPYsGFD5CL9+PHjuf3229myZQtOp7PM66itFxGRKvvgA3j6aZg2DdxucwqczwepqdXy9BVt6+tMT/uoUaP47bffeOedd6L2jxw5kt69e3PEEUcwZMgQ3njjDT766CNWrFhR6ddyuVykpaVF3URERCrE7zevtA8cCH/9Bffea+4/+eRqS9jrs91H1S1atIhAIEDPnj0jx7Rv355WrVoxd+5cQKPqREQkBiZPNmvVfP+9eZEewOmstoR9f9SJs4vRo0czbdo0vv76a1q0aLHXYzt37gzA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82DHOtNa6z/4gz/Qb3/723f5msO9zg5Fi7Wt35fv73w6lPoNb3/72/URRxyh0zTVWi/sel/M/Y+Z+gI7uvfeezWgN2zY0H5s1apV+q//+q8PbOFmYbH3ZWZT/y972cv0i170ommPLZT6X+j9IplpnyNhGPLAAw9w7rnnth8zDINzzz2Xu+++ex5LtnA8/vjjLF26lLVr13L55ZezceNGAB544AGiKJpWd8ceeywrV66Uupu0fv16tmzZMq2OKpUK69ata9fR3XffTUdHB8973vPa15x77rkYhsE999xz0Mu8ENx111309fVxzDHH8Ja3vIWRkZH2c4d7fU1MTADQ1dUFzO738O677+bEE0+kv7+/fc35559PtVrlkUceOYilnx871tmUf/qnf6Knp4cTTjiB6667jmaz2X7ucK+zQ81ibuv35fs73w6FfkMYhnzhC1/gta99LUqp9uMLud63d6j1PyYmJlBK0dHRMe3xj370o3R3d3PKKadw8803L6gtTIdKX2ZoaIjbb7+d173udTs9txDqf6H3i6w5fbfD2PDwMEmSTPufBtDf389vfvObeSrVwrFu3Tpuu+02jjnmGAYHB/ngBz/I85//fH71q1+xZcsWHMfZ6Qba39/Pli1b5qfAC8xUPcz0/Zp6bsuWLfT19U173rIsurq6Dst6vOCCC3jFK17BmjVrePLJJ3n/+9/PhRdeyN13341pmod1faVpyp/+6Z/y+7//+5xwwgkAs/o93LJly4zfwannDmUz1RnAZZddxqpVq1i6dCm/+MUveO9738tjjz3G1772NeDwrrND0WJt6/f1+zufDpV+wze+8Q3Gx8e58sor248t5Hrf0aHU//B9n/e+971ceumllMvl9uNve9vbOPXUU+nq6uKnP/0p1113HYODg3zsYx+bx9JmDqW+zOc+9zlKpdJO21gWQv0vhn6RBO3ioLjwwgvbfz/ppJNYt24dq1at4itf+Qq5XG4eSyYOVa9+9avbfz/xxBM56aSTOOKII7jrrrs455xz5rFk8+/qq6/mV7/61bT9oWL3dlVn2+8bPPHEE1myZAnnnHMOTz75JEccccTBLqYQM1qM399Dpd/w6U9/mgsvvJClS5e2H1vI9X6oiqKIV73qVWit+eQnPzntuWuvvbb995NOOgnHcXjTm97ERz7yEVzXPdhFneZQ6st85jOf4fLLL8fzvGmPL4T6Xwz9IlkeP0d6enowTXOnjIJDQ0MMDAzMU6kWro6ODo4++mieeOIJBgYGCMOQ8fHxaddI3T1rqh529/0aGBjYKRFSHMeMjo5KPQJr166lp6eHJ554Ajh86+uaa67hW9/6Fj/4wQ9Yvnx5+/HZ/B4ODAzM+B2ceu5Qtas6m8m6desApn3PDsc6O1QtxrZ+f76/C8li7Dds2LCB733ve7z+9a/f7XULud4Phf7HVMC+YcMG7rjjjmmz7DNZt24dcRzz1FNPHZwC7oXF2pf5r//6Lx577LE9/i7Awa//xdIvkqB9jjiOw2mnncadd97ZfixNU+68807OPPPMeSzZwlSv13nyySdZsmQJp512GrZtT6u7xx57jI0bN0rdTVqzZg0DAwPT6qharXLPPfe06+jMM89kfHycBx54oH3N97//fdI0bXcIDmfPPPMMIyMjLFmyBDj86ktrzTXXXMPXv/51vv/977NmzZppz8/m9/DMM8/kl7/85bQOwlQH6DnPec7B+SAH0Z7qbCYPPfQQwLTv2eFUZ4e6xdTWz8X3dyFZjP2Gz372s/T19fGSl7xkt9ct5Hpf7P2PqYD98ccf53vf+x7d3d17fM1DDz2EYRg7LTtfCBZrX+bTn/40p512GieffPIerz1Y9b/o+kVzmtbuMPelL31Ju66rb7vtNv3oo4/qN77xjbqjo2NaRsHD1Tvf+U5911136fXr1+uf/OQn+txzz9U9PT1669atWmut3/zmN+uVK1fq73//+/r+++/XZ555pj7zzDPnudQHV61W0z//+c/1z3/+cw3oj33sY/rnP/95O8PpRz/6Ud3R0aG/+c1v6l/84hf6ZS97mV6zZo1utVrt97jgggv0Kaecou+55x794x//WB911FH60ksvna+PdEDtrr5qtZp+17vepe+++269fv16/b3vfU+feuqp+qijjtK+77ff43Cqr7e85S26Uqnou+66Sw8ODrb/NJvN9jV7+j2M41ifcMIJ+rzzztMPPfSQ/s53vqN7e3v1ddddNx8f6YDbU5098cQT+kMf+pC+//779fr16/U3v/lNvXbtWv2CF7yg/R6HW50dDhZLWz8X39/5tNj7DUmS6JUrV+r3vve90x5fiPW+mPsfuyt7GIb6pS99qV6+fLl+6KGHpv0eTGX2/ulPf6r/+q//Wj/00EP6ySef1F/4whd0b2+vvuKKKw542fdU/sXQl9nTd0drrScmJnQ+n9ef/OQnd3r9fNb/YusXSdA+x/7mb/5Gr1y5UjuOo8844wz9s5/9bL6LtCBccsklesmSJdpxHL1s2TJ9ySWX6CeeeKL9fKvV0m9961t1Z2enzufz+uUvf7keHBycxxIffD/4wQ80sNOf17zmNVrr7NiVD3zgA7q/v1+7rqvPOecc/dhjj017j5GREX3ppZfqYrGoy+Wyvuqqq3StVpuHT3Pg7a6+ms2mPu+883Rvb6+2bVuvWrVKv+ENb9ipU3041ddMdQXoz372s+1rZvN7+NRTT+kLL7xQ53I53dPTo9/5znfqKIoO8qc5OPZUZxs3btQveMELdFdXl3ZdVx955JH63e9+t56YmJj2PodTnR0uFkNbP1ff3/my2PsN3/3udzWwUzu9EOt9Mfc/dlf29evX7/L34Ac/+IHWWusHHnhAr1u3TlcqFe15nj7uuOP0X/zFX0wLiuer/IuhL7On747WWv/93/+9zuVyenx8fKfXz2f9L7Z+kZostBBCCCGEEEIIIRYY2dMuhBBCCCGEEEIsUBK0CyGEEEIIIYQQC5QE7UIIIYQQQgghxAIlQbsQQgghhBBCCLFASdAuhBBCCCGEEEIsUBK0CyGEEEIIIYQQC5QE7UIIIYQQQgghxAIlQbsQQgghhBBCCLFASdAuhJhXV155JRdddNF8F0MIIYQQQogFSYJ2IcReufHGG3nuc587Z+/38Y9/nNtuu23O3k8IIYQQc2eu232A2267jY6Ojjl9TyEOZdZ8F0AIcWiKogjbtvd4XaVSOQilEUIIIYQQYnGSmXYhDkOf//zn6e7uJgiCaY9fdNFF/PEf//EuX3fbbbfxwQ9+kIcffhilFEqp9iy5UopPfvKTvPSlL6VQKPDnf/7nJEnC6173OtasWUMul+OYY47h4x//+LT33HF5/Nlnn83b3vY23vOe99DV1cXAwAA33njjXH10IYQQ4rBzINr98fFxXv/619Pb20u5XOZFL3oRDz/8cPu1Dz/8MC984QsplUqUy2VOO+007r//fu666y6uuuoqJiYm2u8p7bwQuydBuxCHoYsvvpgkSfi3f/u39mNbt27l9ttv57Wvfe0uX3fJJZfwzne+k+OPP57BwUEGBwe55JJL2s/feOONvPzlL+eXv/wlr33ta0nTlOXLl/PVr36VRx99lOuvv573v//9fOUrX9lt+T73uc9RKBS45557uOmmm/jQhz7EHXfcsf8fXAghhDgMHYh2/+KLL2br1q18+9vf5oEHHuDUU0/lnHPOYXR0FIDLL7+c5cuXc9999/HAAw/wvve9D9u2Oeuss7jlllsol8vt93zXu951YCtAiEVOlscLcRjK5XJcdtllfPazn+Xiiy8G4Atf+AIrV67k7LPP3u3risUilmUxMDCw0/OXXXYZV1111bTHPvjBD7b/vmbNGu6++26+8pWv8KpXvWqXP+ekk07ihhtuAOCoo47ib//2b7nzzjt58YtfvDcfUwghhBDMfbv/4x//mHvvvZetW7fiui4A/+f//B++8Y1v8C//8i+88Y1vZOPGjbz73e/m2GOPBbL2fEqlUkEpNWNfQgixMwnahThMveENb+D0009n06ZNLFu2jNtuu40rr7wSpdQ+v+fznve8nR679dZb+cxnPsPGjRtptVqEYbjHhDYnnXTStH8vWbKErVu37nO5hBBCiMPdXLb7Dz/8MPV6ne7u7mmPt1otnnzySQCuvfZaXv/61/OP//iPnHvuuVx88cUcccQRc/JZhDjcSNAuxGHqlFNO4eSTT+bzn/885513Ho888gi33377fr1noVCY9u8vfelLvOtd7+Kv/uqvOPPMMymVStx8883cc889u32fHRPYKaVI03S/yiaEEEIczuay3a/X6yxZsoS77rprp+emssLfeOONXHbZZdx+++18+9vf5oYbbuBLX/oSL3/5y/fjUwhxeJKgXYjD2Otf/3puueUWNm3axLnnnsuKFSv2+BrHcUiSZFbv/5Of/ISzzjqLt771re3HpkbghRBCCHFwzVW7f+qpp7JlyxYsy2L16tW7fO3RRx/N0UcfzTve8Q4uvfRSPvvZz/Lyl798r/oSQghJRCfEYe2yyy7jmWee4R/+4R92m4hme6tXr2b9+vU89NBDDA8P75SJdntHHXUU999/P9/97nf57W9/ywc+8AHuu+++uSq+EEIIIfbCXLX75557LmeeeSYXXXQR//mf/8lTTz3FT3/6U/7sz/6M+++/n1arxTXXXMNdd93Fhg0b+MlPfsJ9993Hcccd137Per3OnXfeyfDwMM1m80B+bCEWPQnahTiMVSoVXvnKV1IsFqcdu7Y7r3zlK7ngggt44QtfSG9vL1/84hd3ee2b3vQmXvGKV3DJJZewbt06RkZGps26CyGEEOLgmat2XynFf/zHf/CCF7yAq666iqOPPppXv/rVbNiwgf7+fkzTZGRkhCuuuIKjjz6aV73qVVx44YXt5LRnnXUWb37zm7nkkkvo7e3lpptuOoCfWojFT2mt9XwXQggxf8455xyOP/54PvGJT8x3UYQQQghxgEm7L8TiI0G7EIepsbEx7rrrLv7oj/6IRx99lGOOOWa+iySEEEKIA0TafSEWL0lEJ8Rh6pRTTmFsbIy//Mu/nNZwH3/88WzYsGHG1/z93/89l19++cEqohBCCCHmiLT7QixeMtMuhJhmw4YNRFE043P9/f2USqWDXCIhhBBCHCjS7gux8EnQLoQQQgghhBBCLFCSPV4IIYQQQgghhFigJGgXQgghhBBCCCEWKAnahRBCCCGEEEKIBUqCdiGEEEIIIYQQYoGSoF0IIYQQQgghhFigJGgXQgghhBBCCCEWKAnahRBCCCGEEEKIBer/A7X9aV20yRfOAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "# Create performance report\n", "fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(12,4))\n", @@ -254,7 +379,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "id": "5BlWsneHW_eY", "metadata": { "colab": { @@ -264,7 +389,18 @@ "id": "5BlWsneHW_eY", "outputId": "eb12f11d-000e-48b0-c812-302f21200669" }, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "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RERERLIxRBIRERGRbAyRRERERCQbQyQRERERycYQSURERESyMUQSERERkWwMkUREREQkG0MkEREREcnGEElEREREsjFEEhEREZFsDJFEREREJBtDJBERERHJxhBJRERERLIxRBIRERGRbAyRRERERCQbQyQRERERycYQSURERESyMUQSERERkWwMkUREREQkm0FJF/A2iImJQWhoKB48eFDSpcjmNS0O2QZmJV1GmaXUF5jRGKgTvh1ZOYqSLqdMYy8LljK9PcLDwxEREaE1v3r16jh37hxSUlLg6uqa72t/+eUXfPzxxwCAq1evYsCAAdi9ezfMzc0RFBSEadOmwcCAbxVElFeJHokMDg6GQqGAQqGAoaEh7O3t0aZNGyxbtgwajUZr7IEDBxAQEABra2sYGxujbt26+Oabb5CTk6M1TqFQYOPGjcW4F9pcXFwQFRWlNe+TTz7B+fPndbqdlJQUKBQKJCYmak3nflhYWKB27doYNGgQLly4oNNtE1HpVLt2baSmpkof+/fvBwA4OTlJ865evYro6GhMmDAB5ubmaNeuHQAgJycH7du3x5MnT3DgwAEsX74cMTExmDBhQknuEhGVYiV+Otvf3x+pqalISUnB1q1b0apVKwwZMgTvv/8+srOzAQAbNmyAj48PKlWqhN27d+PcuXMYMmQIJk+ejK5du0IIUcJ78WImJiYoX758sWxr586dSE1NxfHjxzF16lScPXsW9erVQ1xcXLFsn4hKjoGBARwcHKSPcuXKAQD09fW15ltbW2PTpk3o0qULzM3NAQA7duzAmTNnsHLlStSvXx/t2rXDpEmTsGDBAjx58qQkd4uISqkSD5FKpRIODg6oWLEiGjRogK+++gqbNm3C1q1bERMTg0ePHqFPnz744IMPsGjRItSvXx8uLi744osvsHz5cqxduxa//PJLobal0WgQGRmJSpUqQalUon79+ti2bZvWmH/++QfdunWDjY0NzMzM0KhRIxw+fBgAcOnSJXTo0AH29vYwNzfHO++8g507d0qvbdmyJa5cuYKhQ4dKRwSBp6ezraystLazcOFCVKlSBUZGRqhevTp+/PFHreUKhQJLlixBp06dYGpqCnd3d/z2228v3UdbW1s4ODjAzc0NHTp0wM6dO+Hl5YXevXvnOWpLRG+WCxcuwNHREW5ubujRoweuXr2a77iLFy/i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m0pEWloaLC0tcefOHdja2pZ0OWWWWq3Gli1bEBAQwNM0r4i91A32UXfYS91gH3VHrVZj7dq16N69Ox4+fAiVSvVat1cqnxNJRERERKUbQyQRERERycYQSURERESyMUQSERERkWwMkUREREQkG0MkEREREcnGEElEREREsjFEEhEREZFsDJFEREREJBtDJBERERHJxhBJRERERLIxRBIRERGRbAyRRERERCQbQyQRERERycYQSURERESyMUQSERERkWwMkUREREQkG0MkEREREcnGEElEREREsjFEEhEREZFsDJFEREREJBtDJBERERHJxhBJRERERLIxRBIRERGRbAyRRERERCQbQyQRERERycYQSURERESyMUQSERERkWwMkUREREQkG0MkEREREcnGEElEREREsjFEEhEREZFsDJFEREREJBtDJBERERHJxhBJRERERLIxRBIRERGRbAyRRERERCQbQyQRERERycYQSURERESyMUQSERERkWwMkUREREQkm0FJF1BU4eHh2LhxIxITE1/bNlq2bIn69esjKioKAODi4oLQ0FCEhoa+tm2WNl7T4pBtYFbSZZRZSn2BGY2BOuHbkZWjKOlyyjT2Mq+U6e0BPP19GBERobWsevXqOHfunDR98OBBjB07FocPH4YQAg0bNsSOHTtgYmIijfnjjz8QGRmJEydOwNjYGD4+Pti4cWOx7AsRlT2lLkQ+H9wKMmLECHz55ZfFU9T/O3r0KMzMCheo3sbASUQlp3bt2ti5c6c0bWDwv1/vBw8ehL+/P8aMGYNvvvkGBw4cgJWVFfT0/ncyat26dejTpw+mTp2K1q1bIzs7G6dOnSrWfSCisqXUhciXEUIgJycH5ubmMDc3L9Zt29nZFev2iIgKy8DAAA4ODvkuGzp0KAYPHozRo0dDrVbjypUrCAgIgKGhIQAgOzsbQ4YMwcyZM9G7d2/pdbVq1SqW2omobCpV10QGBwdj7969mDt3LhQKBRQKBWJiYqBQKLB161Y0bNgQSqUS+/fvR3h4OOrXr6/12o4dOyIiIgJ2dnZQqVTo378/njx5UqhtP3r0CD179oS5uTkqVKiA2bNn5xnj4uIiHSEVQiA8PByVK1eGUqmEo6MjBg8eDODp0dQrV65g6NCh0n4AwN27d9GtWzdUrFgRpqamqFu3LlavXq21jZYtW2Lw4MEY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t5FarcaWLVtwKtyPR8+IiOiNohDPX/xXSrVs2RL169fX+Y0rRSXnzyyWRWlpabC0tMSdO3cYIl9BbogMCAhgiHxF7KVusI+6w17qBvuoO2q1GmvXrkX37t3x8OHDIh08kqNMnc4mIiIiotKBIfI1WLVqlfQnDJ//yP2bwURERERlWZm5JnLPnj0lXYKWF13D+MEHH8DLyyvfZTxUT0RERG+CMhMiyxILC4s8D/smIiIiepPwdDYRERERycYQSURERESyMUQSERERkWwMkUREREQkG0MkEREREcnGEElEREREsjFEEhEREZFsDJFEREREJBtDJBERERHJxhBJRERERLIxRBIRERGRbAyRRERERCQbQyQRERERycYQSURERESyMUQSERERkWwMkUREREQkG0MkEREREcnGEElEREREsjFEEhEREZFsDJFEREREJBtDJBERERHJxhBJRERERLIxRBIRERGRbAyRRERERCQbQyQRERERycYQSURERESyMUQSERERkWwMkUREREQkG0MkEREREcnGEElEREREsjFEEhEREZFsDJFEREREJBtDJBERERHJxhBJRERERLIxRBIRERGRbAyRRERERCQbQyS9VgsXLoSHhwdUKhVUKhW8vb2xdetWafmlS5fQqVMn2NnZQaVSoUuXLvj3339LsGIiIiIqDIOSLuBtkpKSAldXVyQkJKB+/folXU6heE2LQ7aBWZFemzK9PSpVqoTp06fD3d0dQggsX74cHTp0QEJCAlxcXNC2bVvUq1cPu3btAgCMHz8egYGBOHToEPT0+H8cIiKi0uqtfJcODAyEv79/vsv+/PNPKBQKnDhxopirksfFxQVRUVFa0wqFAgqFAiYmJnBxcUGXLl2kcFZSAgMDERAQAHd3d1SrVg1TpkyBubk5Dh06hPj4eKSkpCAmJgZ169ZF3bp1sXz5chw7dqzE6yYiIqIXeytDZO/evREbG4t//vknz7Lo6Gg0atQIHh4eJVDZyz158qTAZZGRkUhNTUVSUhJWrFgBKysr+Pr6YsqUKcVYYcFycnKwZs0aPHr0CN7e3sjKyoJCoYBSqZTGGBsbQ09PD/v37y/BSomIiOhl3soQ+f7778POzg4xMTFa8zMyMvDrr7+id+/e2L9/P5o3bw4TExM4OTlh8ODBePTokTTWxcUFU6dORa9evWBhYYHKlStj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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "# plot the feature importance\n", "fig, ax = plt.subplots()\n", @@ -275,7 +411,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "id": "a84a4a2d", "metadata": { "id": "a84a4a2d" @@ -287,7 +423,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "id": "dd5e6ef3", "metadata": { "id": "dd5e6ef3" @@ -306,7 +442,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "id": "3dfbd7a4", "metadata": { "colab": { @@ -316,7 +452,16 @@ "outputId": "8d3b7567-66e8-4c09-8654-096ca2c5fa90", "scrolled": false }, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/mvds/nannyml/repos/nannyml/.venv/lib/python3.9/site-packages/lightgbm/basic.py:2065: UserWarning: Using categorical_feature in Dataset.\n", + " _log_warning('Using categorical_feature in Dataset.')\n" + ] + } + ], "source": [ "dle = nml.DLE(\n", " metrics=['mae'],\n", @@ -333,7 +478,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "id": "e4b34a9a", "metadata": { "colab": { @@ -351,7 +496,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 18, "id": "3a7b6877", "metadata": { "id": "3a7b6877", @@ -371,7 +516,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 19, "id": "74a8f9c4", "metadata": { "colab": { @@ -389,7 +534,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 20, "id": "79fed665", "metadata": { "id": "79fed665" @@ -408,7 +553,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 21, "id": "GnGnV5v0d7Fp", "metadata": { "colab": { @@ -426,7 +571,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 22, "id": "ofutS6MwgFEd", "metadata": { "colab": { @@ -443,7 +588,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 23, "id": "QCIMHtwkhG9K", "metadata": { "colab": { @@ -463,7 +608,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 24, "id": "DdFamecl4JPi", "metadata": { "colab": { @@ -512,7 +657,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.4" + "version": "3.9.16" } }, "nbformat": 4, diff --git a/docs/examples/green_taxi.rst b/docs/examples/green_taxi.rst index 7910c944..37ea4473 100644 --- a/docs/examples/green_taxi.rst +++ b/docs/examples/green_taxi.rst @@ -7,8 +7,8 @@ Full Monitoring Workflow - Regression: NYC Green Taxi Dataset Open In Colab -In this tutorial, we will use the `NYC Green Taxi Dataset `_ to build a machine-learning model that predicts the tip amount a passenger -will leave after a taxi ride. Later, we will use NannyML to monitor this model and measure its performance with unseen production data. Additionally, +In this tutorial, we will use the `NYC Green Taxi Dataset `_ to build a machine-learning model that predicts the tip amount a passenger +will leave after a taxi ride. Later, we will use NannyML to monitor this model and measure its performance with unseen production data. Additionally, we will investigate plausible reasons for the performance drop using data drift detection methods. @@ -68,7 +68,7 @@ this tutorial is to learn how to monitor an ML model with unseen "production" da - test: data from the **second week** of December 2016 - prod: data from **the third and fourth weeks** of December 2016 -The production dataset will help us simulate a real-case scenario where a trained model is used in a production environment. Typically, production data don't contain targets. +The production dataset will help us simulate a real-case scenario where a trained model is used in a production environment. Typically, production data don't contain targets. This is why monitoring the model performance on it is a challenging task. But let's not worry too much about it (yet). We will return later to this when learning how to estimate model performance. @@ -87,7 +87,7 @@ Exploring the training data Let's quickly explore the train data to ensure we understand it and check that everything makes sense. Since we are building a model that can predict the tip amount that the customers will leave at the end of the ride is essential that we look at how the distribution looks. -The table below shows that the most common tip amount is close to \$2. However, we also observe a high max value of \$250, meaning there are probably some outliers. +The table below shows that the most common tip amount is close to \$2. However, we also observe a high max value of \$250, meaning there are probably some outliers. So, let's take a closer look by plotting a box plot and a histogram of the tip amount column. .. nbimport:: @@ -161,7 +161,6 @@ We will later use NannyML to check how well the model performs on this data. .. nbimport:: :path: ./example_notebooks/Examples Green Taxi.ipynb :cells: 14 - :show_output: Analysing ML model performance in production ============================================ @@ -313,4 +312,4 @@ We built an ML model to predict the tip amount a passenger will leave after a ta And we applied NannyML's performance estimation to spot performance degradation patterns. We also used data drift detection methods to explain these performance issues. After finding what is causing the performance degradation issues, we need to figure out how to fix it. -Check out our previous blog post to learn six ways `to address data distribution shift `_. \ No newline at end of file +Check out our previous blog post to learn six ways `to address data distribution shift `_. diff --git a/docs/tutorials/performance_estimation/multiclass_performance_estimation.rst b/docs/tutorials/performance_estimation/multiclass_performance_estimation.rst index a3200f9b..49e575b0 100644 --- a/docs/tutorials/performance_estimation/multiclass_performance_estimation.rst +++ b/docs/tutorials/performance_estimation/multiclass_performance_estimation.rst @@ -19,7 +19,7 @@ Just The Code .. nbimport:: :path: ./example_notebooks/Tutorial - Estimating Performance - Multiclass Classification.ipynb - :cells: 1 3 4 6 8 + :cells: 1 3 4 6 .. admonition:: **Advanced configuration** :class: hint diff --git a/docs/tutorials/thresholds.rst b/docs/tutorials/thresholds.rst index 3070c6e5..81e30496 100644 --- a/docs/tutorials/thresholds.rst +++ b/docs/tutorials/thresholds.rst @@ -46,7 +46,6 @@ This snippet shows how to create an instance of the :class:`~nannyml.thresholds. .. nbimport:: :path: ./example_notebooks/Tutorial - Thresholds.ipynb :cells: 2 - :show_output: .. _thresholds_std: @@ -72,7 +71,6 @@ This snippet shows how to create an instance of the :class:`~nannyml.thresholds. .. nbimport:: :path: ./example_notebooks/Tutorial - Thresholds.ipynb :cells: 3 - :show_output: Setting custom thresholds for calculators and estimators diff --git a/docs/tutorials/working_with_results.rst b/docs/tutorials/working_with_results.rst index b59d4ed4..57ecd978 100644 --- a/docs/tutorials/working_with_results.rst +++ b/docs/tutorials/working_with_results.rst @@ -208,16 +208,16 @@ the database, in this case, an `SQLite` database. .. nbimport:: :path: ./example_notebooks/Tutorial - Working with results.ipynb - :cells: 11 + :cells: 20 A quick inspection shows that the database was populated and contains the univariate drift calculation results. .. nbimport:: :path: ./example_notebooks/Tutorial - Working with results.ipynb - :cells: 12 + :cells: 21 :show_output: .. nbimport:: :path: ./example_notebooks/Tutorial - Working with results.ipynb - :cells: 13 + :cells: 22 :show_output: From 072e3f5820c2fd0a63386e46f5c95ee4984bdee7 Mon Sep 17 00:00:00 2001 From: Michael Van de Steene <124588413+michael-nml@users.noreply.github.com> Date: Wed, 26 Jul 2023 12:56:24 +0200 Subject: [PATCH 14/18] Improve handling `object` dtype for perf calculation (#321) * Add test case for bool target with missing values * Fix filtering missing values in y_pred[_proba] * Infer object types after dropping NaN values Some methods, e.g. sklearn `roc_auc_score` error out when provided a column with dtype `object`. This commit infers the dtype for object columns after missing values have been dropped. This allows pandas to choose a more appropriate dtype, and should result in successful calculation. * Tiny refactor Signed-off-by: Niels Nuyttens * Fix for tiny refactor --------- Signed-off-by: Niels Nuyttens Co-authored-by: Niels Nuyttens Co-authored-by: Niels Nuyttens --- .../performance_calculation/metrics/base.py | 4 ++++ .../confidence_based/metrics.py | 23 ++++++++++++------- .../test_performance_calculator.py | 21 +++++++++++++++++ 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/nannyml/performance_calculation/metrics/base.py b/nannyml/performance_calculation/metrics/base.py index c917ed8e..49da27ff 100644 --- a/nannyml/performance_calculation/metrics/base.py +++ b/nannyml/performance_calculation/metrics/base.py @@ -272,4 +272,8 @@ def _common_data_cleaning(y_true: pd.Series, y_pred: Union[pd.Series, pd.DataFra y_pred = y_pred[~y_true.isna()] y_true.dropna(inplace=True) + # NaN values have been dropped. Try to infer types again + y_pred = y_pred.infer_objects() + y_true = y_true.infer_objects() + return y_true, y_pred diff --git a/nannyml/performance_estimation/confidence_based/metrics.py b/nannyml/performance_estimation/confidence_based/metrics.py index e2488701..4f9620ac 100644 --- a/nannyml/performance_estimation/confidence_based/metrics.py +++ b/nannyml/performance_estimation/confidence_based/metrics.py @@ -237,17 +237,24 @@ def _common_cleaning( y_pred_proba = data[y_pred_proba_column_name] y_pred = data[self.y_pred] + y_true = data[self.y_true] if clean_targets else None - y_pred_proba.dropna(inplace=True) + # Create mask to filter out NaN values + mask = ~(y_pred.isna() | y_pred_proba.isna()) + if clean_targets: + mask = mask | ~(y_true.isna()) + # Drop missing values (NaN/None) + y_pred_proba = y_pred_proba[mask] + y_pred = y_pred[mask] if clean_targets: - y_true = data[self.y_true] - y_true = y_true[~y_pred_proba.isna()] - y_pred_proba = y_pred_proba[~y_true.isna()] - y_pred = y_pred[~y_true.isna()] - y_true.dropna(inplace=True) - else: - y_true = None + y_true = y_true[mask] + + # NaN values have been dropped. Try to infer types again + y_pred_proba = y_pred_proba.infer_objects() + y_pred = y_pred.infer_objects() + if clean_targets: + y_true = y_true.infer_objects() return y_pred_proba, y_pred, y_true diff --git a/tests/performance_calculation/test_performance_calculator.py b/tests/performance_calculation/test_performance_calculator.py index 23d72cb6..2d7e887b 100644 --- a/tests/performance_calculation/test_performance_calculator.py +++ b/tests/performance_calculation/test_performance_calculator.py @@ -180,6 +180,27 @@ def test_calculator_calculate_should_include_target_completeness_rate(data): # assert sut.loc[1, ('chunk', 'targets_missing_rate')] == 0.9 +def test_calculator_calculate_should_support_partial_bool_targets(data, performance_calculator): + """Test that the calculator supports partial bool targets. + + Pandas converts bool columns to object dtype when they contain NaN values. This previously resulted in problems + when calculating the performance metrics. This test ensures that the calculator supports partial bool targets. + """ + ref_data = data[0] + analysis_data = data[1].merge(data[2], on='identifier') + + # Convert target column to bool dtype + analysis_data = analysis_data.astype({'work_home_actual': 'bool'}) + + # Drop 10% of the target values in the first chunk + analysis_data.loc[0:499, 'work_home_actual'] = np.NAN + + performance_calculator.fit(reference_data=ref_data) + performance_calculator.calculate(analysis_data) + + # No further checks needed, if the above code runs without errors, the test passes. + + @pytest.mark.parametrize( 'custom_thresholds', [ From b276a8f927eec5fda9ab784d78a5d1d740db2d50 Mon Sep 17 00:00:00 2001 From: Niels <94110348+nnansters@users.noreply.github.com> Date: Fri, 22 Sep 2023 23:11:33 +0200 Subject: [PATCH 15/18] Feat/premium support (#325) * Support detection of AKS/EKS * Support detecting nannyML cloud * Add Runner support for the closed-source nannyml-premium package, including new calculators and estimators. * Linting fixes * Add types-requests to dev dependencies --- .pre-commit-config.yaml | 1 + nannyml/__init__.py | 7 +++++++ nannyml/runner.py | 35 ++++++++++++++++++++++------------- nannyml/usage_logging.py | 35 +++++++++++++++++++++++++++++++++++ pyproject.toml | 2 ++ setup.cfg | 1 + 6 files changed, 68 insertions(+), 13 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 73ca55b7..4859e253 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,3 +38,4 @@ repos: - types-python-dateutil - types-click - sqlmodel + - types-requests diff --git a/nannyml/__init__.py b/nannyml/__init__.py index a139f7fe..c2b5ca97 100644 --- a/nannyml/__init__.py +++ b/nannyml/__init__.py @@ -64,6 +64,13 @@ ) from .usage_logging import UsageEvent, disable_usage_logging, enable_usage_logging, log_usage +try: + import nannyml_premium + + logging.getLogger().debug('loaded "nannyml_premium" package') +except Exception: + pass + # read any .env files to import environment variables load_dotenv() diff --git a/nannyml/runner.py b/nannyml/runner.py index 3f6f928a..6f7d2f23 100644 --- a/nannyml/runner.py +++ b/nannyml/runner.py @@ -7,12 +7,12 @@ import logging from contextlib import contextmanager from dataclasses import dataclass -from typing import Any, Callable, Dict, List, Optional, Tuple, Type +from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union import pandas as pd from rich.console import Console -from nannyml._typing import Result +from nannyml._typing import Calculator, Estimator, Result from nannyml.config import Config, InputDataConfig, StoreConfig, WriterConfig from nannyml.data_quality.missing import MissingValuesCalculator from nannyml.data_quality.unseen import UnseenValuesCalculator @@ -59,18 +59,27 @@ def run_context(config: Config): ) -_registry: Dict[str, Type] = { - 'univariate_drift': UnivariateDriftCalculator, - 'multivariate_drift': DataReconstructionDriftCalculator, - 'performance': PerformanceCalculator, - 'cbpe': CBPE, - 'dle': DLE, - 'missing_values': MissingValuesCalculator, - 'unseen_values': UnseenValuesCalculator, -} _logger = logging.getLogger(__name__) +class CalculatorFactory: + """A factory class that produces Metric instances based on a given magic string or a metric specification.""" + + registry: Dict[str, Type] = { + 'univariate_drift': UnivariateDriftCalculator, + 'multivariate_drift': DataReconstructionDriftCalculator, + 'performance': PerformanceCalculator, + 'cbpe': CBPE, + 'dle': DLE, + 'missing_values': MissingValuesCalculator, + 'unseen_values': UnseenValuesCalculator, + } + + @classmethod + def register(cls, name: str, calculator_type: Union[Type[Calculator], Type[Estimator]]): + cls.registry[name] = calculator_type + + class RunnerLogger: def __init__(self, logger: logging.Logger, console: Optional[Console] = None): self.logger = logger @@ -138,12 +147,12 @@ def run( # noqa: C901 store = get_store(calculator_config.store, run_logger) - if calculator_config.type not in _registry: + if calculator_config.type not in CalculatorFactory.registry: raise InvalidArgumentsException(f"unknown calculator type '{calculator_config.type}'") # first step: load or (create + fit) calculator context.increase_step() - calc_cls = _registry[calculator_config.type] + calc_cls = CalculatorFactory.registry[calculator_config.type] if store and calculator_config.store: run_logger.log( f"[{context.current_step}/{context.total_steps}] '{context.current_calculator}': " diff --git a/nannyml/usage_logging.py b/nannyml/usage_logging.py index 00af95c9..d242c387 100644 --- a/nannyml/usage_logging.py +++ b/nannyml/usage_logging.py @@ -243,10 +243,19 @@ def _get_system_information() -> Dict[str, Any]: "runtime_environment": _get_runtime_environment(), "python_version": platform.python_version(), "nannyml_version": __version__, + "nannyml_cloud": _is_nannyml_cloud(), } +def _is_nannyml_cloud(): + return 'NML_CLOUD' in os.environ + + def _get_runtime_environment(): + if _is_running_in_aks(): + return 'aks' + if _is_running_in_eks(): + return 'eks' if _is_running_in_kubernetes(): return 'kubernetes' elif _is_running_in_docker(): @@ -272,6 +281,32 @@ def _is_running_in_kubernetes(): return Path('/var/run/secrets/kubernetes.io/').exists() +def _is_running_in_aks(): + import requests + + try: + metadata = requests.get( + 'http://169.254.169.254/metadata/instance?api-version=2021-02-01', headers={'Metadata': 'true'} + ) + return metadata.status_code == 200 + except Exception: + return False + + +def _is_running_in_eks(): + import requests + + try: + token = requests.put( + 'http://169.254.169.254/latest/api/token', headers={'X-aws-ec2-metadata-token-ttl-seconds': 21600} + ).raw() + + metadata = requests.get('http://169.254.169.254/latest/meta-data/', headers={'X-aws-ec2-metadata-token': token}) + return metadata.status_code == 200 + except Exception: + return False + + # Inspired by # https://github.com/zenml-io/zenml/blob/275109da08b783d5d2cd508b5f703aed0c66e485/src/zenml/environment.py#L182 # and https://stackoverflow.com/a/39662359 diff --git a/pyproject.toml b/pyproject.toml index cdac9cfa..ce0ed419 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -103,6 +103,8 @@ pytest-lazy-fixture = "^0.6.3" types-click = "^7.1.8" types-python-dateutil = "^2.8.19.6" types-PyYAML = "^6.0" +types-requests = "^2.31.0.3" + [tool.black] line-length = 120 diff --git a/setup.cfg b/setup.cfg index e0143fc6..af714661 100644 --- a/setup.cfg +++ b/setup.cfg @@ -98,6 +98,7 @@ deps = types-click types-python-dateutil types-PyYAML + types-requests commands = flake8 nannyml tests mypy nannyml tests From 4f582cc419dc49cf6eaf4d2c3abb6da1f2fd3939 Mon Sep 17 00:00:00 2001 From: Bernardo Date: Mon, 25 Sep 2023 16:14:34 +0200 Subject: [PATCH 16/18] DatabaseWriter support for results coming from `MissingValuesCalculator` and `UnseenValuesCalculator` (#314) * init commit to open a draft PR * add tests to database writer in time, adding the fixtures to the other writers should be added. * add mappers for unseen and missing values calcs * fix: correct method name of data quality calculators * fix: add column_name to data quality Metric entities * fix: write custom logic to get column_name in mapper * add the data quality fixtures to io tests * Fixing flak8 & mypy Signed-off-by: niels * A bit of typing (expose the generic DbMetric interface) + initialize vars outside of the loop to ensure they exist * Added some better tests that actually check the output of the DB writers. The new DQ mappers were not actually returning any values, fixed the issue --------- Signed-off-by: niels Co-authored-by: niels --- nannyml/io/db/entities.py | 26 ++++++++ nannyml/io/db/mappers.py | 131 +++++++++++++++++++++++++++++++++++++- tests/io/test_writers.py | 93 +++++++++++++++++++++++++++ 3 files changed, 247 insertions(+), 3 deletions(-) diff --git a/nannyml/io/db/entities.py b/nannyml/io/db/entities.py index 4417ed0b..f282b31e 100644 --- a/nannyml/io/db/entities.py +++ b/nannyml/io/db/entities.py @@ -161,3 +161,29 @@ class DLEPerformanceMetric(Metric, table=True): # type: ignore[call-arg] #: The lower alerting threshold value lower_threshold: Optional[float] + + +class UnseenValuesMetric(Metric, table=True): + __tablename__ = "unseen_values_metrics" + + #: The name of the column this metric belongs to + column_name: str + + #: The upper alerting threshold value + upper_threshold: Optional[float] + + #: The lower alerting threshold value + lower_threshold: Optional[float] + + +class MissingValuesMetric(Metric, table=True): + __tablename__ = "missing_values_metrics" + + #: The name of the column this metric belongs to + column_name: str + + #: The upper alerting threshold value + upper_threshold: Optional[float] + + #: The lower alerting threshold value + lower_threshold: Optional[float] diff --git a/nannyml/io/db/mappers.py b/nannyml/io/db/mappers.py index f558915d..2254124d 100644 --- a/nannyml/io/db/mappers.py +++ b/nannyml/io/db/mappers.py @@ -6,13 +6,20 @@ from datetime import datetime from typing import Any, Callable, Dict, List, Optional, Type +from nannyml.data_quality.missing.result import Result as MissingValuesResult +from nannyml.data_quality.unseen.result import Result as UnseenValuesResult from nannyml.drift.multivariate.data_reconstruction.result import Result as DataReconstructionDriftResult from nannyml.drift.univariate import Result as UnivariateDriftResult from nannyml.exceptions import InvalidArgumentsException from nannyml.io.db.entities import CBPEPerformanceMetric, DataReconstructionFeatureDriftMetric, DLEPerformanceMetric from nannyml.io.db.entities import Metric from nannyml.io.db.entities import Metric as DbMetric -from nannyml.io.db.entities import RealizedPerformanceMetric, UnivariateDriftMetric +from nannyml.io.db.entities import ( + MissingValuesMetric, + RealizedPerformanceMetric, + UnivariateDriftMetric, + UnseenValuesMetric, +) from nannyml.performance_calculation.result import Result as RealizedPerformanceResult from nannyml.performance_estimation.confidence_based.results import Result as CBPEResult from nannyml.performance_estimation.direct_loss_estimation.result import Result as DLEResult @@ -225,7 +232,7 @@ def _parse( 'timestamp column to be specified and present' ) - res: List[Metric] = [] + res: List[DbMetric] = [] for metric in [metric.column_name for metric in result.metrics]: res += ( @@ -333,7 +340,7 @@ def _parse( 'timestamp column to be specified and present' ) - res: List[Metric] = [] + res: List[DbMetric] = [] for metric in [metric.column_name for metric in result.metrics]: res += ( @@ -353,3 +360,121 @@ def _parse( ) return res + + +@MapperFactory.register(UnseenValuesResult) +class UnseenValuesResultMapper: + def map_to_entity(self, result, **metric_args) -> List[DbMetric]: + def _parse( + column_name: str, + start_date: datetime, + end_date: datetime, + value, + upper_threshold, + lower_threshold, + alert: bool, + ) -> UnseenValuesMetric: + timestamp = start_date + (end_date - start_date) / 2 + + return UnseenValuesMetric( + column_name=column_name, + metric_name="count", + start_timestamp=start_date, + end_timestamp=end_date, + timestamp=timestamp, + value=value, + upper_threshold=upper_threshold, + lower_threshold=lower_threshold, + alert=alert, + **metric_args, + ) + + if result.timestamp_column_name is None: + raise NotImplementedError( + 'no timestamp column was specified. Listing metrics currently requires a ' + 'timestamp column to be specified and present' + ) + + columns: List[str] = list( + filter(lambda col: col != 'chunk', result.to_df().columns.get_level_values(0).drop_duplicates()) + ) + + res: List[DbMetric] = [] + + for column in columns: + res += ( + result.filter(period='analysis') + .to_df()[ + [ + ('chunk', 'start_date'), + ('chunk', 'end_date'), + (column, 'value'), + (column, 'upper_threshold'), + (column, 'lower_threshold'), + (column, 'alert'), + ] + ] + .apply(lambda r: _parse(column, *r), axis=1) + .to_list() + ) + + return res + + +@MapperFactory.register(MissingValuesResult) +class MissingValuesResultMapper: + def map_to_entity(self, result, **metric_args) -> List[DbMetric]: + def _parse( + column_name: str, + start_date: datetime, + end_date: datetime, + value, + upper_threshold, + lower_threshold, + alert: bool, + ) -> MissingValuesMetric: + timestamp = start_date + (end_date - start_date) / 2 + + return MissingValuesMetric( + column_name=column_name, + metric_name="count", + start_timestamp=start_date, + end_timestamp=end_date, + timestamp=timestamp, + value=value, + upper_threshold=upper_threshold, + lower_threshold=lower_threshold, + alert=alert, + **metric_args, + ) + + if result.timestamp_column_name is None: + raise NotImplementedError( + 'no timestamp column was specified. Listing metrics currently requires a ' + 'timestamp column to be specified and present' + ) + + columns: List[str] = list( + filter(lambda col: col != 'chunk', result.to_df().columns.get_level_values(0).drop_duplicates()) + ) + + res: List[DbMetric] = [] + + for column in columns: + res += ( + result.filter(period='analysis') + .to_df()[ + [ + ('chunk', 'start_date'), + ('chunk', 'end_date'), + (column, 'value'), + (column, 'upper_threshold'), + (column, 'lower_threshold'), + (column, 'alert'), + ] + ] + .apply(lambda r: _parse(column, *r), axis=1) + .to_list() + ) + + return res diff --git a/tests/io/test_writers.py b/tests/io/test_writers.py index 0f39244c..b05379ef 100644 --- a/tests/io/test_writers.py +++ b/tests/io/test_writers.py @@ -1,13 +1,17 @@ # Author: Niels Nuyttens # # License: Apache Software License 2.0 +import os import tempfile import pytest from pytest_lazyfixture import lazy_fixture +from nannyml.data_quality.missing import MissingValuesCalculator +from nannyml.data_quality.unseen import UnseenValuesCalculator from nannyml.datasets import ( load_synthetic_binary_classification_dataset, + load_synthetic_car_loan_data_quality_dataset, load_synthetic_car_price_dataset, load_synthetic_multiclass_classification_dataset, ) @@ -195,6 +199,30 @@ def dle_estimated_performance_for_regression_result(): return result +@pytest.fixture(scope='module') +def missing_values_for_binary_classification_result(): + reference_df, analysis_df, analysis_targets_df = load_synthetic_car_loan_data_quality_dataset() + calc = MissingValuesCalculator( + column_names=[col for col in reference_df if col not in ['timestamp', 'y_pred', 'y_true']], + timestamp_column_name='timestamp', + ).fit(reference_df) + result = calc.calculate(analysis_df.join(analysis_targets_df)) + return result + + +@pytest.fixture(scope='module') +def unseen_values_for_binary_classification_result(): + reference_df, analysis_df, analysis_targets_df = load_synthetic_car_loan_data_quality_dataset() + calc = UnseenValuesCalculator( + # categorical features as described in + # https://nannyml.readthedocs.io/en/stable/datasets/binary_car_loan.html#dataset-description + column_names=['salary_range', 'repaid_loan_on_prev_car', 'size_of_downpayment'], + timestamp_column_name='timestamp', + ).fit(reference_df) + result = calc.calculate(analysis_df.join(analysis_targets_df)) + return result + + @pytest.mark.parametrize( 'result', [ @@ -210,6 +238,8 @@ def dle_estimated_performance_for_regression_result(): lazy_fixture('cbpe_estimated_performance_for_binary_classification_result'), lazy_fixture('cbpe_estimated_performance_for_multiclass_classification_result'), lazy_fixture('dle_estimated_performance_for_regression_result'), + lazy_fixture('missing_values_for_binary_classification_result'), + lazy_fixture('unseen_values_for_binary_classification_result'), ], ) def test_raw_files_writer_raises_no_exceptions_when_writing_to_parquet(result): @@ -236,6 +266,8 @@ def test_raw_files_writer_raises_no_exceptions_when_writing_to_parquet(result): lazy_fixture('cbpe_estimated_performance_for_binary_classification_result'), lazy_fixture('cbpe_estimated_performance_for_multiclass_classification_result'), lazy_fixture('dle_estimated_performance_for_regression_result'), + lazy_fixture('missing_values_for_binary_classification_result'), + lazy_fixture('unseen_values_for_binary_classification_result'), ], ) def test_raw_files_writer_raises_no_exceptions_when_writing_to_csv(result): @@ -262,6 +294,8 @@ def test_raw_files_writer_raises_no_exceptions_when_writing_to_csv(result): lazy_fixture('cbpe_estimated_performance_for_binary_classification_result'), lazy_fixture('cbpe_estimated_performance_for_multiclass_classification_result'), lazy_fixture('dle_estimated_performance_for_regression_result'), + lazy_fixture('missing_values_for_binary_classification_result'), + lazy_fixture('unseen_values_for_binary_classification_result'), ], ) def test_database_writer_raises_no_exceptions_when_writing(result): @@ -287,6 +321,8 @@ def test_database_writer_raises_no_exceptions_when_writing(result): lazy_fixture('cbpe_estimated_performance_for_binary_classification_result'), lazy_fixture('cbpe_estimated_performance_for_multiclass_classification_result'), lazy_fixture('dle_estimated_performance_for_regression_result'), + lazy_fixture('missing_values_for_binary_classification_result'), + lazy_fixture('unseen_values_for_binary_classification_result'), ], ) def test_pickle_file_writer_raises_no_exceptions_when_writing(result): @@ -296,3 +332,60 @@ def test_pickle_file_writer_raises_no_exceptions_when_writing(result): writer.write(result, filename='export.pkl') except Exception as exc: pytest.fail(f"an unexpected exception occurred: {exc}") + + +@pytest.mark.parametrize( + 'result, table_name, expected_row_count', + [ + (lazy_fixture('univariate_drift_for_binary_classification_result'), 'univariate_drift_metrics', 110), + (lazy_fixture('univariate_drift_for_multiclass_classification_result'), 'univariate_drift_metrics', 110), + (lazy_fixture('univariate_drift_for_regression_result'), 'univariate_drift_metrics', 80), + ( + lazy_fixture('data_reconstruction_drift_for_binary_classification_result'), + 'data_reconstruction_feature_drift_metrics', + 10, + ), + ( + lazy_fixture('data_reconstruction_drift_for_multiclass_classification_result'), + 'data_reconstruction_feature_drift_metrics', + 10, + ), + ( + lazy_fixture('data_reconstruction_drift_for_regression_result'), + 'data_reconstruction_feature_drift_metrics', + 10, + ), + (lazy_fixture('realized_performance_for_binary_classification_result'), 'realized_performance_metrics', 40), + ( + lazy_fixture('realized_performance_for_multiclass_classification_result'), + 'realized_performance_metrics', + 40, + ), + (lazy_fixture('realized_performance_for_regression_result'), 'realized_performance_metrics', 40), + (lazy_fixture('cbpe_estimated_performance_for_binary_classification_result'), 'cbpe_performance_metrics', 20), + ( + lazy_fixture('cbpe_estimated_performance_for_multiclass_classification_result'), + 'cbpe_performance_metrics', + 20, + ), + (lazy_fixture('dle_estimated_performance_for_regression_result'), 'dle_performance_metrics', 20), + (lazy_fixture('missing_values_for_binary_classification_result'), 'missing_values_metrics', 90), + (lazy_fixture('unseen_values_for_binary_classification_result'), 'unseen_values_metrics', 30), + ], +) +def test_database_writer_exports_correctly(result, table_name, expected_row_count): + try: + writer = DatabaseWriter(connection_string='sqlite:///test.db', model_name='test') + writer.write(result) + + import sqlite3 + + with sqlite3.connect("test.db", uri=True) as db: + res = db.cursor().execute(f"SELECT COUNT(*) FROM {table_name}").fetchone() + assert res[0] == expected_row_count + + except Exception as exc: + pytest.fail(f"an unexpected exception occurred: {exc}") + + finally: + os.remove('test.db') From 7dd53d3e56385321af486e9c3b3fbef3178875ab Mon Sep 17 00:00:00 2001 From: Nikolaos Perrakis <89025229+nikml@users.noreply.github.com> Date: Mon, 25 Sep 2023 17:43:05 +0300 Subject: [PATCH 17/18] Fix Reconstruction Error Plot and y-axis label (#323) --- .../multivariate_drift_detection/pca-reconstruction-error.svg | 2 +- docs/example_notebooks/Tutorial - Drift - Multivariate.ipynb | 2 +- nannyml/drift/multivariate/data_reconstruction/result.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/_static/tutorials/detecting_data_drift/multivariate_drift_detection/pca-reconstruction-error.svg b/docs/_static/tutorials/detecting_data_drift/multivariate_drift_detection/pca-reconstruction-error.svg index 78ed7f79..7cf8738d 100644 --- a/docs/_static/tutorials/detecting_data_drift/multivariate_drift_detection/pca-reconstruction-error.svg +++ b/docs/_static/tutorials/detecting_data_drift/multivariate_drift_detection/pca-reconstruction-error.svg @@ -1 +1 @@ -Jan 2018Jul 2018Jan 2019Jul 20191.11.151.21.25MetricAlertConfidence bandMultivariate drift (PCA reconstruction error)TimeData reconstruction driftReferenceAnalysis \ No newline at end of file +Jan 2018Jul 2018Jan 2019Jul 20191.11.151.21.25MetricAlertConfidence bandMultivariate Drift (PCA Reconstruction Error)TimeReconstruction ErrorReferenceAnalysis \ No newline at end of file diff --git a/docs/example_notebooks/Tutorial - Drift - Multivariate.ipynb b/docs/example_notebooks/Tutorial - Drift - Multivariate.ipynb index e58b6bf4..58c8e610 100644 --- a/docs/example_notebooks/Tutorial - Drift - Multivariate.ipynb +++ b/docs/example_notebooks/Tutorial - Drift - Multivariate.ipynb @@ -923,7 +923,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.8" + "version": "3.11.4" } }, "nbformat": 4, diff --git a/nannyml/drift/multivariate/data_reconstruction/result.py b/nannyml/drift/multivariate/data_reconstruction/result.py index 00cd4154..399fbd38 100644 --- a/nannyml/drift/multivariate/data_reconstruction/result.py +++ b/nannyml/drift/multivariate/data_reconstruction/result.py @@ -105,8 +105,8 @@ def plot(self, kind: str = 'drift', *args, **kwargs) -> go.Figure: if kind == 'drift': return plot_metric( self, - title='Multivariate drift (PCA reconstruction error)', - metric_display_name='Data reconstruction drift', + title='Multivariate Drift (PCA Reconstruction Error)', + metric_display_name='Reconstruction Error', metric_column_name='reconstruction_error', ) else: From dd20ef7630d60c167e6ba848460034a04b700acd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santiago=20V=C3=ADquez?= Date: Tue, 3 Oct 2023 13:48:56 +0200 Subject: [PATCH 18/18] warn when metric contains NaN (#326) --- .../metrics/binary_classification.py | 12 ++++++++++++ .../metrics/multiclass_classification.py | 7 +++++++ 2 files changed, 19 insertions(+) diff --git a/nannyml/performance_calculation/metrics/binary_classification.py b/nannyml/performance_calculation/metrics/binary_classification.py index 298bb244..2b4d97bc 100644 --- a/nannyml/performance_calculation/metrics/binary_classification.py +++ b/nannyml/performance_calculation/metrics/binary_classification.py @@ -6,6 +6,7 @@ import numpy as np import pandas as pd from sklearn.metrics import confusion_matrix, f1_score, precision_score, recall_score, roc_auc_score +import warnings from nannyml._typing import ProblemType from nannyml.base import _list_missing @@ -99,6 +100,7 @@ def _calculate(self, data: pd.DataFrame): y_true, y_pred = _common_data_cleaning(y_true, y_pred) if y_true.nunique() <= 1: + warnings.warn("Calculated ROC-AUC score contains NaN values.") return np.nan else: return roc_auc_score(y_true, y_pred) @@ -167,6 +169,7 @@ def _calculate(self, data: pd.DataFrame): y_true, y_pred = _common_data_cleaning(y_true, y_pred) if (y_true.nunique() <= 1) or (y_pred.nunique() <= 1): + warnings.warn("Calculated F1-score contains NaN values.") return np.nan else: return f1_score(y_true, y_pred) @@ -234,6 +237,7 @@ def _calculate(self, data: pd.DataFrame): y_true, y_pred = _common_data_cleaning(y_true, y_pred) if (y_true.nunique() <= 1) or (y_pred.nunique() <= 1): + warnings.warn("Calculated Precision score contains NaN values.") return np.nan else: return precision_score(y_true, y_pred) @@ -301,6 +305,7 @@ def _calculate(self, data: pd.DataFrame): y_true, y_pred = _common_data_cleaning(y_true, y_pred) if (y_true.nunique() <= 1) or (y_pred.nunique() <= 1): + warnings.warn("Calculated Recall score contains NaN values.") return np.nan else: return recall_score(y_true, y_pred) @@ -373,6 +378,7 @@ def _calculate(self, data: pd.DataFrame): y_true, y_pred = _common_data_cleaning(y_true, y_pred) if (y_true.nunique() <= 1) or (y_pred.nunique() <= 1): + warnings.warn("Calculated Specificity score contains NaN values.") return np.nan else: tn, fp, fn, tp = confusion_matrix(y_true, y_pred).ravel() @@ -446,6 +452,7 @@ def _calculate(self, data: pd.DataFrame): y_true, y_pred = _common_data_cleaning(y_true, y_pred) if (y_true.nunique() <= 1) or (y_pred.nunique() <= 1): + warnings.warn("Calculated Accuracy score contains NaN values.") return np.nan else: tn, fp, fn, tp = confusion_matrix(y_true, y_pred).ravel() @@ -564,6 +571,7 @@ def _calculate(self, data: pd.DataFrame): business_value = num_tp * tp_value + num_tn * tn_value + num_fp * fp_value + num_fn * fn_value if (y_true.nunique() <= 1) or (y_pred.nunique() <= 1): + warnings.warn("Calculated Business Value contains NaN values.") return np.nan else: if self.normalize_business_value is None: @@ -745,6 +753,7 @@ def _calculate_true_positives(self, data: pd.DataFrame) -> float: y_true, y_pred = _common_data_cleaning(y_true, y_pred) if y_true.empty or y_pred.empty: + warnings.warn("Calculated true_positives contain NaN values.") return np.nan num_tp = np.sum(np.logical_and(y_pred, y_true)) @@ -773,6 +782,7 @@ def _calculate_true_negatives(self, data: pd.DataFrame) -> float: y_true, y_pred = _common_data_cleaning(y_true, y_pred) if y_true.empty or y_pred.empty: + warnings.warn("Calculated true_negatives contain NaN values.") return np.nan num_tn = np.sum(np.logical_and(np.logical_not(y_pred), np.logical_not(y_true))) @@ -801,6 +811,7 @@ def _calculate_false_positives(self, data: pd.DataFrame) -> float: y_true, y_pred = _common_data_cleaning(y_true, y_pred) if y_true.empty or y_pred.empty: + warnings.warn("Calculated false_positives contain NaN values.") return np.nan num_fp = np.sum(np.logical_and(y_pred, np.logical_not(y_true))) @@ -829,6 +840,7 @@ def _calculate_false_negatives(self, data: pd.DataFrame) -> float: y_true, y_pred = _common_data_cleaning(y_true, y_pred) if y_true.empty or y_pred.empty: + warnings.warn("Calculated false_negatives contain NaN values.") return np.nan num_fn = np.sum(np.logical_and(np.logical_not(y_pred), y_true)) diff --git a/nannyml/performance_calculation/metrics/multiclass_classification.py b/nannyml/performance_calculation/metrics/multiclass_classification.py index 8568a0aa..63510120 100644 --- a/nannyml/performance_calculation/metrics/multiclass_classification.py +++ b/nannyml/performance_calculation/metrics/multiclass_classification.py @@ -11,6 +11,7 @@ import numpy as np import pandas as pd +import warnings from sklearn.metrics import ( accuracy_score, f1_score, @@ -127,6 +128,7 @@ def _calculate(self, data: pd.DataFrame): y_true, y_pred = _common_data_cleaning(y_true, y_pred) if y_true.nunique() <= 1: + warnings.warn("Calculated ROC-AUC score contains NaN values.") return np.nan else: return roc_auc_score(y_true, y_pred, multi_class='ovr', average='macro', labels=labels) @@ -214,6 +216,7 @@ def _calculate(self, data: pd.DataFrame): y_true, y_pred = _common_data_cleaning(y_true, y_pred) if (y_true.nunique() <= 1) or (y_pred.nunique() <= 1): + warnings.warn("Calculated F1-score contains NaN values.") return np.nan else: return f1_score(y_true, y_pred, average='macro', labels=labels) @@ -301,6 +304,7 @@ def _calculate(self, data: pd.DataFrame): y_true, y_pred = _common_data_cleaning(y_true, y_pred) if (y_true.nunique() <= 1) or (y_pred.nunique() <= 1): + warnings.warn("Calculated Precision score contains NaN values.") return np.nan else: return precision_score(y_true, y_pred, average='macro', labels=labels) @@ -388,6 +392,7 @@ def _calculate(self, data: pd.DataFrame): y_true, y_pred = _common_data_cleaning(y_true, y_pred) if (y_true.nunique() <= 1) or (y_pred.nunique() <= 1): + warnings.warn("Calculated Recall score contains NaN values.") return np.nan else: return recall_score(y_true, y_pred, average='macro', labels=labels) @@ -475,6 +480,7 @@ def _calculate(self, data: pd.DataFrame): y_true, y_pred = _common_data_cleaning(y_true, y_pred) if (y_true.nunique() <= 1) or (y_pred.nunique() <= 1): + warnings.warn("Calculated Specificity score contains NaN values.") return np.nan else: MCM = multilabel_confusion_matrix(y_true, y_pred, labels=labels) @@ -558,6 +564,7 @@ def _calculate(self, data: pd.DataFrame): y_true, y_pred = _common_data_cleaning(y_true, y_pred) if (y_true.nunique() <= 1) or (y_pred.nunique() <= 1): + warnings.warn("Calculated Accuracy score contains NaN values.") return np.nan else: return accuracy_score(y_true, y_pred)