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/.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/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 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 diff --git a/README.md b/README.md index ccb94b28..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 @@ -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.) @@ -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/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 00000000..baa854e9 Binary files /dev/null and b/docs/_static/example_green_taxi_model_val.png differ 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/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 ); } 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/_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/Examples Green Taxi.ipynb b/docs/example_notebooks/Examples Green Taxi.ipynb new file mode 100644 index 00000000..8eaf721d --- /dev/null +++ b/docs/example_notebooks/Examples Green Taxi.ipynb @@ -0,0 +1,665 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "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": 2, + "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": 3, + "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": 4, + "id": "9843aa09", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 206 + }, + "id": "9843aa09", + "outputId": "6a2c8bdd-0a25-4ab2-abbe-6852d905d784" + }, + "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": 5, + "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": 6, + "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": 7, + "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": 8, + "id": "d68c2328", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 300 + }, + "id": "d68c2328", + "outputId": "ff8bdcf6-8083-4882-dc02-4342210b5023", + "scrolled": true + }, + "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": 9, + "id": "fa4dac07", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 865 + }, + "id": "fa4dac07", + "outputId": "3aef959b-5c61-4cbd-9b55-eb007b20c826" + }, + "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+PlyS5XC61tLSoqKhI7733noqKitTS0iKXy+VXByAwsAYFgKG+uQblxIkTKi0t9Y2xBgXoXgx7Fg8AtFfrVTw7d+7UkSNH/MaOHDminTt3chUPEIAIKAAM1dLSonfeeUeSFBIS4jcWGhoqSXrnnXfU0tLS6b0BMA4BBYChioqKVFVVpdtuu01ut1ubNm3S2rVrtWnTJtXV1Wn8+PGqqqpSUVGR0a0C6ERcxQPAUK3B4/nnn1ePHj0uuornueee05133qmioiLdcccdnd8gAEMQUACYxrlz57Rq1SodPHhQQ4YM0RNPPGF0SwAMwlU8AAy1ceNGTZw4UQMGDFBFRYXfWhOr1aq4uDgdP35cn332GTMoQBfHVTwAuowJEyYoLCxMx48fl9Vq1dNPP62SkhI9/fTTslqtOn78uMLCwriLLBBgmEEBYKhz584pLCxMHo9HYWFhOnPmjG+sdT8oKEhnzpy56CofAF0LMygAuoxVq1bJ4/Ho8ccfV0xMjN9YbGysHnvsMXk8Hq1atcqgDgEYgUWyAAx18OBBSdKzzz6r11577aKHBVZWVmr16tW+OgCBgYACwFBDhgyRJBUWFuqhhx66aK1JYWGhXx2AwMAaFACGOnfunHr16qW+fftq586dGjVqlGpraxUVFeXbr6mpUWNjI2tQgC6ONSgAuoyQkBAtWrRIlZWVSkxMVGVlpc6ePeu3v2jRIsIJEGCYQQFguF69eun06dPfOR4eHq7GxsZO7AjA9dCe72/WoAAwVEVFhS+cTJo0ScOHD9eZM2cUFhamffv26W9/+5tOnz6tiooKxcXFGdwtgM7CDAoAQ8XGxqqqqkrh4eGqra3V1q1bfVfxjB8/XjabTWfOnFFMTIwqKyuNbhfANWAGBUCXcerUKUmSw+HQsGHDVFpa6htLSkrS9OnT9cEHH/jqAAQGZlAAGMpms8ntdkvSd95JVpIiIyNVV1dnSI8AOgZX8QDoMn7/+9/7fs7IyJDT6VR9fb2cTqcyMjLarAPQ/XGKB4ChoqKifD//7W9/01dffaWsrCzl5+eroqKizToA3R8zKAAMtWXLFr/9iooKrVq1yi+ctFUHoHsjoAAwjZ49e/rth4WFGdQJAKNxigeAoVrXmURHR6usrExOp9N3mXFaWpri4+N16tQpv/UoALo/ZlAAGMpqtUqSTp48qRkzZmjv3r06c+aM9u7dqxkzZvguL26tAxAYmEEBYKiqqirfz5988ok+/vhj377FYmmzDkD3xwwKAEPFx8f7fg4NDfUb++aalG/WAej+mEEBYKj09HQFBwerb9++Onz48EVrUAYPHqyamhqlp6cb3SqATkRAAWCoL774Qs3NzaqsrNSMGTM0ZMgQNTU1qWfPnsrLy/M9f+eLL77QhAkTjG0WQKchoAAwVHl5uSRp2rRpfutPWrUeb60DEBgIKAAM1bq25OOPP1ZMTIwmTJigXr16qbGxUUVFRb7QwhoUILAQUAAYauzYsZKk4OBghYSE6IMPPvCNDRw4UMHBwWpubvbVAQgMXMUDwFBvvfWWJKm5uVllZWV+Y2VlZWpubvarAxAYCCgADFVSUuL7+VKXGX+zDkD3R0ABYCiv1ytJstvtqqur06ZNm7R27Vpt2rRJtbW1GjJkiF8dgMDAGhQAhrLZbJKkmpoaWSwWv0uJm5ubdfLkSb86AIGBGRQAhgoOvvB30qlTpzRw4ED97ne/U1lZmX73u99p4MCBvmfxtNYBCAwEFACGap0xGTBggGpqavToo49qwIABevTRR1VTU6MBAwb41QEIDPxJAsBQEyZMUExMjI4fP66pU6fKbrf77iR74MABffLJJ777owAIHAQUAIayWq168803lZ2drc8//1yffPKJbywsLEwWi0VvvvmmrFargV0C6Gyc4gFguKysLI0aNUpNTU1+x8+cOaNRo0YpKyvLoM4AGIWAAsBwDodDO3bsaHNsx44dcjgcndsQAMMRUAAY6syZM/rwww8lXXgwoNPpVH19vZxOp6ZNmyZJ+vDDD3XmzBkj2wTQyQgoAAy1ZMkSSRdu1LZ+/Xo1NTXpo48+UlNTk9avX++7UVtrHYDAwCJZAIZqPbXjcDg0bNgwlZaW+saSkpKUlZWll19++TtPAQHonggoAAzVp08fSVJeXp7CwsL8xiorK/Xyyy/71QEIDAQUAIZatGiRNmzYIEm6/fbbFRERoVOnTqlPnz5qaGjQX//6V18dgMBBQAFgqG/e36Q1jFyuDkD3xyJZAIYqLi7u0DoA3QMBBYChzp8/L0kKCgpSYmKi39igQYMUFBTkVwcgMBBQABjq66+/lnRhEWxrGGllsVgUFRXlVwcgMLAGBYChTp8+LUmqqalRTU2N39jhw4cvqgMQGJhBAWAou93eoXUAuocODyjPPfecLBaL33bjjTf6xpuamjRv3jz17dtXERERmjFjhiorKzu6DQBdxNSpUzu0DkD3cF1mUG666SaVl5f7tr///e++sUWLFumjjz7Sn//8Z23evFllZWU8qRQIYO+//77ffnR0tB599FFFR0dfsg5A93Zd1qAEBwcrLi7uouN1dXX6wx/+oLVr1+oHP/iBJOntt9/W8OHDtW3bNo0bN+56tAPAxA4cOOC3f/LkSb311luXrQPQvV2XGZSSkhIlJCToe9/7nmbPnq0jR45Iknbt2qXz589r4sSJvtobb7xRgwYNktPp/M73O3v2rNxut98GoHvYt2+fJCksLEwNDQ2aN2+eJk2apHnz5qmhoUE9e/b0qwMQGDo8oIwdO1Zr1qzRp59+qjfffFOHDh1SRkaG6uvrVVFRoZCQEN9lg61iY2NVUVHxne+Zm5srm83m2759rwQAXZfX65UknTlzRjNnztTs2bO1bt06zZ49WzNnzlRTU5NfHYDA0OGneO666y7fzzfffLPGjh2rwYMH64MPPrjoQWBXKicnR4sXL/btu91uQgrQTQwaNEh79+6VJH3yyScqLCz0jX3zviiDBg3q9N4AGOe6X2YcFRWlYcOG6cCBA4qLi9O5c+dUW1vrV1NZWdnmmpVWoaGhioyM9NsAdA9btmzx/ezxePzGvrn/zToA3d91DygNDQ06ePCg4uPjdeutt6pHjx7auHGjb3z//v06cuSI0tLSrncrAEwoOjpasbGxl6yJjY296KoeAN1bh5/iWbp0qaZPn67BgwerrKxMP//5z2W1WnXvvffKZrPpwQcf1OLFixUdHa3IyEgtWLBAaWlpXMEDBLBBgwZd8n5InN4BAk+HB5Rjx47p3nvvVU1Njfr376/bbrtN27ZtU//+/SVJr7zyioKCgjRjxgydPXtWkydP1qpVqzq6DQBdRENDg3bs2HHJmh07dqihoUERERGd1BUAo1m8XXBpvNvtls1mU11dHetRgC7unnvuUUFBgaQLp3JeeOEF3X333SosLNTPfvYz38yKw+HQ+vXrDewUwLVqz/c3DwsEYKiSkhJJUt++fXXs2DEFB1/4b+mhhx7S3LlzFRMTo1OnTvnqAAQGHhYIwFAWi0WSNHToUHk8Hq1cuVILFizQypUr5fF4NHToUL86AIGBGRQAhkpLS5PL5dK2bdsUGhrqN7Zo0SK/OgCBgxkUAIYaMWJEh9YB6B5YJAvAUA0NDerdu/dl6+rr67mKB+ji2vP9zQwKAEO98sorHVoHoHtgBgWAoaKjo3Xq1KnL1vXp00cnT57shI4AXC9cZgygyzh9+rSkC/dAKS0t1erVq3Xw4EENGTJEjz32mJKSklRZWemrAxAYOMUDwFCt60+qq6vV1NSkzZs3+7ampiZVV1f71QEIDAQUAIa6//77JV14cnGfPn1UUFCgPXv2qKCgQH369PE90bi1DkBgIKAAMNS0adM6tA5A90BAAWCoUaNGdWgdgO6BgALAUHPnzu3QOgDdAwEFgKEOHjzYoXUAugcCCgBDDRgwoEPrAHQPBBQAhiopKenQOgDdAwEFgKE4xQOgLQQUAABgOgQUAIayWq0dWgegeyCgADBUS0tLh9YB6B4IKAAAwHQIKAAAwHQIKAAMFRYW1qF1ALoHAgoAQzU3N3doHYDugYACwFDnz5/v0DoA3QMBBQAAmA4BBQAAmA4BBQAAmA4BBQAAmA4BBQAAmA4BBQAAmA4BBQAAmA4BBQAAmA4BBQAAmA4BBQAAmA4BBQAAmA4BBQAAmA4BBQAAmA4BBQAAmA4BBQAAmA4BBQAAmA4BBQAAmA4BBQAAmA4BBQAAmA4BBQAAmA4BBQAAmA4BBQAAmA4BBQAAmA4BBQAAmA4BBQAAmA4BBQAAmE6w0Q0A6PrOnGvRweqG6/45ruN1V/W6If0jFBZi7eBuAFxPBBQA1+xgdYPufu3v1/1zrvYzChfcppQBtg7uBsD1ZPF6vV6jm2gvt9stm82muro6RUZGGt0OEPCuZQYldWDUFdfuOVZ7VZ/BDApgDu35/mYGBcA1CwuxdsoMBbMgQOAgoAAB7tCJRjWebTbs8/ccq72iWZQ9x2qveg1KR+gVGqzkfr0M+3wg0Bh6iueNN97QSy+9pIqKCt1yyy167bXXNGbMmMu+jlM8QMc4dKJRmXlFRrchSTr8m7u/c2zwU4Wd2Ml327R0AiEFuAZd4hTPn/70Jy1evFirV6/W2LFjtXLlSk2ePFn79+9XTEyMUW0BAeXk6QYF9TyupXcOU2J0uLHNzNmuH931bxcd/uAvmw1oxt/Rk6eVt+GfOnm6QckioACdwbAZlLFjx2r06NF6/fXXJUkej0eJiYlasGCBnn766Uu+lhkUoGOs27NNz//jYaPb6DKWj/mDpg+//CwvgLaZfgbl3Llz2rVrl3JycnzHgoKCNHHiRDmdzovqz549q7Nnz/r23W53p/QJdHdnGqPVeGiB0W10GUOnDDG6BSBgGBJQTpw4oZaWFsXGxvodj42N1ddff31RfW5urp5//vnOag8IGNNSk9QjaIqGxEQorMfVX4Z7oKpBC/+0u+Ma62ArfzxS9piIa3oPFskCnatLXMWTk5OjxYsX+/bdbrcSExMN7AjoHqJ7hWjmmEHX/D5D+keocMFtHdCR1HS+RcdOndHAPmHqeQ2h6Zu4DwrQ9RgSUPr16yer1arKykq/45WVlYqLi7uoPjQ0VKGhoZ3VHoB26uj7oIxK6rC3AtBFGfKwwJCQEN16663auHGj75jH49HGjRuVlpZmREsAAMBEDDvFs3jxYs2ZM0ejRo3SmDFjtHLlSjU2NuonP/mJUS0BAACTMCyg/PjHP1Z1dbWeffZZVVRUaOTIkfr0008vWjgLAAACDw8LBAAAnaI939+GrEEBAAC4FAIKAAAwHQIKAAAwHQIKAAAwHQIKAAAwHQIKAAAwHQIKAAAwHQIKAAAwHQIKAAAwHcNudX8tWm9+63a7De4EAABcqdbv7Su5iX2XDCj19fWSpMTERIM7AQAA7VVfXy+bzXbJmi75LB6Px6OysjL17t1bFovF6HYAdCC3263ExEQdPXqUZ20B3YzX61V9fb0SEhIUFHTpVSZdMqAA6L54GCgAiUWyAADAhAgoAADAdAgoAEwlNDRUP//5zxUaGmp0KwAMxBoUAABgOsygAAAA0yGgAAAA0yGgAAAA0yGgAPApKiqSxWJRbW2t0a0ACHAEFCCATZgwQQsXLvTtp6enq7y8/LK3oA4Ua9asUVRUlNFtAAGpSz6LB8D1ERISori4OKPbAABmUIBANXfuXG3evFm//e1vZbFYZLFYtGbNGr9TPK0zCAUFBRo6dKh69uypyZMn6+jRo1f0GQcPHtQPf/hDxcbGKiIiQqNHj9Znn33mV5OUlKQXXnhBDzzwgCIiIjR48GD993//t6qrq/XDH/5QERERuvnmm7Vz506/1/3Xf/2XbrrpJoWGhiopKUkrVqzwG7dYLCooKPA7FhUVpTVr1kiSSktLZbFYlJ+fr8zMTIWHh+uWW26R0+mUdOF0109+8hPV1dX5/n2ee+65K/vHBXDNCChAgPrtb3+rtLQ0PfzwwyovL1d5eXmbTwg/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/rtHjx5KSEhQx44dtWLFinqDybUybdo0ZWRkmMtlZWWKiYlRYmKiwsPDvXY/TqdT+fn5enFXoCqqA7w2rq/tz/L+24c1vRgyZIiCg4O9Pn63LP8578sWaGhWn2q/2y98gV64ox8u9MLleuiFt/6u1P5bUvMOztXwyVtgl2rZsqVuu+02HT58WEOGDFFlZaVOnTrldhSotLTUPGcoKipKO3fudBuj5iqxS2tqXzlWWlqq8PDwBkOWzWaTzWarsz44ONgnf5wrqgNUUeU/O60venDp2D7psR/1t4a/7Re+RC/c0Q8XeuHiz73w9ut+zd8Sb4zr888BOnv2rI4cOaJ27dopPj5ewcHBKigoMLcfOnRIx44dk91ulyTZ7Xbt27dPJ06cMGvy8/MVHh6uuLg4s+bSMWpqasYAAABoiNcD0P/7f/9PW7Zs0dGjR7V9+3Y9/PDDCgoK0uOPP66IiAiNGTNGGRkZ+uijj1RUVKTRo0fLbrerX79+kqTExETFxcXpqaee0j//+U/l5eVp+vTpSk1NNY/eTJgwQZ9//rmmTJmigwcP6o033tCKFSs0adIkbz8cAABwHfL6W2D//ve/9fjjj+ubb77RzTffrHvvvVc7duzQzTffLEmaP3++AgMDNXLkSFVUVCgpKUlvvPGGefugoCCtWbNGEydOlN1uV/PmzZWSkqKXXnrJrImNjdXatWs1adIkLViwQO3bt9dbb73FJfAAAOA78XoAeu+99xrcHhoaquzsbGVnZ9db07FjxyueOd6/f3/t2bOnUXMEAADWxneBAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAyyEAAQAAy7mhqSeA749OU9d6fUxbkKE5faVuWXmqqArw+vgAADQGR4AAAIDlEIAAAIDl+H0Ays7OVqdOnRQaGqqEhATt3LmzqacEAAC+5/w6AC1fvlwZGRmaMWOGdu/erZ49eyopKUknTpxo6qkBAIDvMb8OQPPmzdO4ceM0evRoxcXFKScnR2FhYVq8eHFTTw0AAHyP+e1VYJWVlSoqKtK0adPMdYGBgRo8eLAKCwsve5uKigpVVFSYy6dPn5Ykffvtt3I6nV6bm9Pp1Llz53SDM1BV1da+8umGakPnzlXTC9GLS9ELd/TDhV64XA+9+Oabb7wyTs3f1W+++UbBwcE6c+aMJMkwjEaP6bcB6D//+Y+qqqoUGRnptj4yMlIHDx687G1mz56tmTNn1lkfGxvrkzniv55o6gl8j9ALF3rhjn640AsXf+/FTXN9O/6ZM2cUERHRqNv6bQBqjGnTpikjI8Ncrq6u1rfffqs2bdooIMB76bqsrEwxMTH68ssvFR4e7rVx/RG9cKEXLvTCHf1woRcu9MKldi8Mw9CZM2cUHR3d6DH9NgDddNNNCgoKUmlpqdv60tJSRUVFXfY2NptNNpvNbV3Lli19NUWFh4dbfqetQS9c6IULvXBHP1zohQu9cLm0F4098lPDb0+CDgkJUXx8vAoKCsx11dXVKigokN1ub8KZAQCA7zu/PQIkSRkZGUpJSVGfPn3Ut29fvfbaayovL9fo0aObemoAAOB7zK8D0GOPPaavv/5amZmZcjgc6tWrl9avX1/nxOhrzWazacaMGXXebrMieuFCL1zohTv64UIvXOiFiy96EWBczTVkAAAAfshvzwECAABoLAIQAACwHAIQAACwHAIQAACwHAIQAACwHAJQI2VnZ6tTp04KDQ1VQkKCdu7c2WD9ypUrdfvttys0NFTdu3fXunXrrtFMfc+TXuTm5iogIMDtJzQ09BrO1ne2bt2q4cOHKzo6WgEBAVq9evUVb7N582bdeeedstls6ty5s3Jzc30+z2vB015s3ry5zn4REBAgh8NxbSbsQ7Nnz9Zdd92lG2+8UW3bttWIESN06NChK97uenzNaEwvrtfXjEWLFqlHjx7mJxvb7XZ9+OGHDd7metwnJM974a19ggDUCMuXL1dGRoZmzJih3bt3q2fPnkpKStKJEycuW799+3Y9/vjjGjNmjPbs2aMRI0ZoxIgR2r9//zWeufd52gvpvx9lfvz4cfPniy++uIYz9p3y8nL17NlT2dnZ36m+pKREycnJGjBggIqLi5Wenq6xY8cqLy/PxzP1PU97UePQoUNu+0bbtm19NMNrZ8uWLUpNTdWOHTuUn58vp9OpxMRElZeX13ub6/U1ozG9kK7P14z27dvr5ZdfVlFRkXbt2qWBAwfqoYce0oEDBy5bf73uE5LnvZC8tE8Y8Fjfvn2N1NRUc7mqqsqIjo42Zs+efdn6H//4x0ZycrLbuoSEBOMnP/mJT+d5LXjaiyVLlhgRERHXaHZNR5KxatWqBmumTJli3HHHHW7rHnvsMSMpKcmHM7v2vksvPvroI0OScfLkyWsyp6Z04sQJQ5KxZcuWemuu59eMS32XXljlNcMwDKNVq1bGW2+9ddltVtknajTUC2/tExwB8lBlZaWKioo0ePBgc11gYKAGDx6swsLCy96msLDQrV6SkpKS6q33F43phSSdPXtWHTt2VExMzBVT/vXset0vrkavXr3Url07DRkyRH//+9+bejo+cfr0aUlS69at662xyr7xXXohXf+vGVVVVXrvvfdUXl5e73dZWmWf+C69kLyzTxCAPPSf//xHVVVVdb5uIzIyst7zFRwOh0f1/qIxvejSpYsWL16sDz74QO+++66qq6t1991369///ve1mPL3Sn37RVlZmc6fP99Es2oa7dq1U05Ojv70pz/pT3/6k2JiYtS/f3/t3r27qafmVdXV1UpPT9c999yjbt261Vt3vb5mXOq79uJ6fs3Yt2+fWrRoIZvNpgkTJmjVqlWKi4u7bO31vk940gtv7RN+/V1g8D92u90t1d99993q2rWrfve732nWrFlNODM0pS5duqhLly7m8t13360jR45o/vz5+sMf/tCEM/Ou1NRU7d+/X9u2bWvqqTS579qL6/k1o0uXLiouLtbp06f1/vvvKyUlRVu2bKn3D//1zJNeeGufIAB56KabblJQUJBKS0vd1peWlioqKuqyt4mKivKo3l80phe1BQcHq3fv3jp8+LAvpvi9Vt9+ER4ermbNmjXRrL4/+vbte10FhbS0NK1Zs0Zbt25V+/btG6y9Xl8zanjSi9qup9eMkJAQde7cWZIUHx+vjz/+WAsWLNDvfve7OrXX+z7hSS9qa+w+wVtgHgoJCVF8fLwKCgrMddXV1SooKKj3/Uq73e5WL0n5+fkNvr/pDxrTi9qqqqq0b98+tWvXzlfT/N66XvcLbykuLr4u9gvDMJSWlqZVq1Zp06ZNio2NveJtrtd9ozG9qO16fs2orq5WRUXFZbddr/tEfRrqRW2N3ieu+jRqC3rvvfcMm81m5ObmGp988okxfvx4o2XLlobD4TAMwzCeeuopY+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", + "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": 10, + "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": 11, + "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": 12, + "id": "beb7b032", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 410 + }, + "id": "beb7b032", + "outputId": "f750cf58-636f-4f80-aee9-c01dc30c87e7", + "scrolled": false + }, + "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+eQnPzntuWuvvbb995NOOgnHcXjTm97ERz7yEVzXPdhFneZQ6st85jOf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a0nTlOXLl/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", + "\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": 13, + "id": "5BlWsneHW_eY", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 472 + }, + "id": "5BlWsneHW_eY", + "outputId": "eb12f11d-000e-48b0-c812-302f21200669" + }, + "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", + "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": 14, + "id": "a84a4a2d", + "metadata": { + "id": "a84a4a2d" + }, + "outputs": [], + "source": [ + "y_pred_prod = model.predict(X_prod)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "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": 16, + "id": "3dfbd7a4", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "3dfbd7a4", + "outputId": "8d3b7567-66e8-4c09-8654-096ca2c5fa90", + "scrolled": false + }, + "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", + " 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": 17, + "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": 18, + "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": 19, + "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": 20, + "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": 21, + "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": 22, + "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": 23, + "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": 24, + "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.9.16" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} 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/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..37ea4473 --- /dev/null +++ b/docs/examples/green_taxi.rst @@ -0,0 +1,315 @@ +============================================================= +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 + +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 `_. 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 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/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/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 704ed504..49e575b0 100644 --- a/docs/tutorials/performance_estimation/multiclass_performance_estimation.rst +++ b/docs/tutorials/performance_estimation/multiclass_performance_estimation.rst @@ -4,26 +4,141 @@ Estimating Performance for Multiclass Classification ==================================================== -We currently support the following **standard** metrics for multiclass classification performance estimation: +This tutorial explains how to use NannyML to estimate the performance of binary classification +models in the absence of target data. To find out how :class:`~nannyml.performance_estimation.confidence_based.cbpe.CBPE` estimates performance, read the :ref:`explanation of Confidence-based +Performance Estimation`. - * **roc_auc** - one-vs-the-rest, macro-averaged - * **f1** - macro-averaged - * **precision** - macro-averaged - * **recall** - macro-averaged - * **specificity** - macro-averaged - * **accuracy** +.. note:: + The following example uses :term:`timestamps`. + These are optional but have an impact on the way data is chunked and results are plotted. + You can read more about them in the :ref:`data requirements`. -For more information about estimating these metrics, refer to the :ref:`standard-metric-estimation` section. -We also support the following *complex* metrics for multiclass classification performance estimation: +Just The Code +------------- - * **confusion_matrix** +.. nbimport:: + :path: ./example_notebooks/Tutorial - Estimating Performance - Multiclass Classification.ipynb + :cells: 1 3 4 6 -For more information about estimating the confusion matrix for multiclass problems, -refer to the :ref:`multiclass-confusion-matrix-estimation` section. +.. admonition:: **Advanced configuration** + :class: hint -.. toctree:: - :maxdepth: 2 + - To learn how :class:`~nannyml.chunk.Chunk` works and to set up custom chunkings check out the :ref:`chunking tutorial ` + - To learn how :class:`~nannyml.thresholds.ConstantThreshold` works and to set up custom threshold check out the :ref:`thresholds tutorial ` - multiclass_performance_estimation/standard_metric_estimation - multiclass_performance_estimation/confusion_matrix_estimation \ No newline at end of file +Walkthrough +----------- + + +For simplicity this guide is based on a synthetic dataset where the monitored model predicts +which type of credit card product new customers should be assigned to. +Check out :ref:`Credit Card Dataset` to learn more about this dataset. + +In order to monitor a model, NannyML needs to learn about it and set expectations from a reference dataset. +Then it can monitor the data that is subject to actual analysis, provided as the analysis dataset. +You can read more about this in our section on :ref:`data periods`. + +.. nbimport:: + :path: ./example_notebooks/Tutorial - Estimating Performance - Multiclass Classification.ipynb + :cells: 1 + +.. nbtable:: + :path: ./example_notebooks/Tutorial - Estimating Performance - Multiclass Classification.ipynb + :cell: 2 + +Next we create the Confidence-based Performance Estimation (:class:`~nannyml.performance_estimation.confidence_based.cbpe.CBPE`) +estimator with a list of metrics, and an optional :term:`chunking` specification. For more information about +chunking check out the :ref:`chunking tutorial` and it's :ref:`advanced guide`. + +.. note:: + The list of metrics specifies which performance metrics of the monitored model will be estimated. + The following metrics are currently supported: + + - ``roc_auc`` - one-vs-the-rest, macro-averaged + - ``f1`` - macro-averaged + - ``precision`` - macro-averaged + - ``recall`` - macro-averaged + - ``specificity`` - macro-averaged + - ``accuracy`` + + +.. nbimport:: + :path: ./example_notebooks/Tutorial - Estimating Performance - Multiclass Classification.ipynb + :cells: 3 + +The :class:`~nannyml.performance_estimation.confidence_based.cbpe.CBPE` +estimator is then fitted using the +:meth:`~nannyml.performance_estimation.confidence_based.cbpe.CBPE.fit` method on the reference data. + +The fitted ``estimator`` can be used to estimate performance on other data, for which performance cannot be calculated. +Typically, this would be used on the latest production data where target is missing. In our example this is +the ``analysis_df`` data. + +NannyML can then output a dataframe that contains all the results. Let's have a look at the results for analysis period +only. + +.. nbimport:: + :path: ./example_notebooks/Tutorial - Estimating Performance - Multiclass Classification.ipynb + :cells: 4 + +.. nbtable:: + :path: ./example_notebooks/Tutorial - Estimating Performance - Multiclass Classification.ipynb + :cell: 5 + +Apart from chunk-related data, the results data have the following columns for each metric +that was estimated: + + - **value** - the estimate of a metric for a specific chunk. + - **sampling_error** - the estimate of the :term:`Sampling Error`. + - **realized** - when **target** values are available for a chunk, the realized performance metric will also + be calculated and included within the results. + - **upper_confidence_boundary** and **lower_confidence_boundary** - These values show the :term:`Confidence Band` of the relevant metric + and are equal to estimated value +/- 3 times the estimated :term:`Sampling Error`. + - **upper_threshold** and **lower_threshold** - crossing these thresholds will raise an alert on significant + performance change. The thresholds are calculated based on the actual performance of the monitored model on chunks in + the reference partition. By default, the thresholds are 3 standard deviations away from the mean performance calculated on + chunks. They are calculated during ``fit`` phase. You can also set up custom thresholds using constant or standard deviations thresholds, + to learn more about it check out our :ref:`tutorial on thresholds`. + - **alert** - flag indicating potentially significant performance change. ``True`` if estimated performance crosses + upper or lower threshold. + + +These results can be also plotted. Our plot contains several key elements. + +* The purple dashed step plot shows the estimated performance in each chunk of the analysis period. Thick squared point + markers indicate the middle of these chunks. + +* The black vertical line splits the reference and analysis periods. + +* The low-saturated colored area around the estimated performance indicates the :ref:`sampling error`. + +* The red horizontal dashed lines show upper and lower thresholds for alerting purposes. + +* If the estimated performance crosses the upper or lower threshold an alert is raised which is indicated with a red + diamond-shaped point marker in the middle of the chunk. + +Description of tabular results above explains how the +:term:`confidence bands` and thresholds are calculated. Additional information is shown in the hover (these are +interactive plots, though only static views are included here). + + +.. nbimport:: + :path: ./example_notebooks/Tutorial - Estimating Performance - Multiclass Classification.ipynb + :cells: 6 + +.. image:: ../../_static/tutorials/performance_estimation/multiclass_synthetic.svg + +Insights +-------- + +After reviewing the performance estimation results, we should be able to see any indications of performance change that +NannyML has detected based upon the model's inputs and outputs alone. + + +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 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/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/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: diff --git a/nannyml/__init__.py b/nannyml/__init__.py index 0bcc5a7d..c2b5ca97 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 @@ -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/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/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: 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/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_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 0fd2840c..30b067c2 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, @@ -131,6 +132,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) @@ -218,6 +220,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) @@ -305,6 +308,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) @@ -392,6 +396,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) @@ -479,6 +484,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) @@ -562,6 +568,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) 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: diff --git a/nannyml/performance_estimation/confidence_based/metrics.py b/nannyml/performance_estimation/confidence_based/metrics.py index 02a85a00..d52c9044 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/nannyml/runner.py b/nannyml/runner.py index d014eeae..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,21 +147,26 @@ 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}': " 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, ) diff --git a/nannyml/thresholds.py b/nannyml/thresholds.py index 16b748ac..68cf1af8 100644 --- a/nannyml/thresholds.py +++ b/nannyml/thresholds.py @@ -265,7 +265,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: @@ -278,7 +278,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: diff --git a/nannyml/usage_logging.py b/nannyml/usage_logging.py index 54bd52ef..d242c387 100644 --- a/nannyml/usage_logging.py +++ b/nannyml/usage_logging.py @@ -243,11 +243,22 @@ 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_docker(): + 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(): return 'docker' elif _is_running_in_notebook(): return 'notebook' @@ -266,6 +277,36 @@ def _is_running_in_docker(): return False +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 113ad8bb..ce0ed419 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 "] @@ -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 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') 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', [