.. DO NOT EDIT. .. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. .. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: .. "auto_examples/validation/_02_cross_validation.py" .. LINE NUMBERS ARE GIVEN BELOW. .. only:: html .. note:: :class: sphx-glr-download-link-note :ref:`Go to the end ` to download the full example code .. rst-class:: sphx-glr-example-title .. _sphx_glr_auto_examples_validation__02_cross_validation.py: .. _cross_validation: Cross Validation ================ Whenever using some sort of trainable algorithm it is important to clearly separate the training and the testing data to get an unbiased result. Usually this is achieved by a train-test split. However, if you don't have that much data, there is always a risk that one random train-test split, will provide better (or worse) results than another. In these cases it is a good idea to use cross-validation. In this procedure, you perform multiple train-test splits and average the results over all "folds". For more information see our :ref:`evaluation guide ` and the `sklearn guide on cross validation `_. In this example, we will learn how to use the :func:`~tpcp.validate.cross_validate` function implemented in tcpc. For this, we will redo the example on :ref:`optimizable pipelines ` but we will perform the final evaluation via cross-validation. If you want to have more information on how the dataset and pipeline is built, head over to this example. Here we will just copy the code over. .. GENERATED FROM PYTHON SOURCE LINES 26-27 Dataset .. GENERATED FROM PYTHON SOURCE LINES 27-38 .. code-block:: default from pathlib import Path from examples.datasets.datasets_final_ecg import ECGExampleData try: HERE = Path(__file__).parent except NameError: HERE = Path().resolve() data_path = HERE.parent.parent / "example_data/ecg_mit_bih_arrhythmia/data" example_data = ECGExampleData(data_path) .. GENERATED FROM PYTHON SOURCE LINES 39-40 Pipeline .. GENERATED FROM PYTHON SOURCE LINES 40-78 .. code-block:: default import pandas as pd from tpcp import OptimizableParameter, OptimizablePipeline, Parameter, cf from examples.algorithms.algorithms_qrs_detection_final import ( OptimizableQrsDetector, ) class MyPipeline(OptimizablePipeline): algorithm: Parameter[OptimizableQrsDetector] algorithm__min_r_peak_height_over_baseline: OptimizableParameter[float] r_peak_positions_: pd.Series def __init__( self, algorithm: OptimizableQrsDetector = cf(OptimizableQrsDetector()) ): self.algorithm = algorithm def self_optimize(self, dataset: ECGExampleData, **kwargs): ecg_data = [d.data["ecg"] for d in dataset] r_peaks = [d.r_peak_positions_["r_peak_position"] for d in dataset] # Note: We need to clone the algorithm instance, to make sure we don't leak any data between runs. algo = self.algorithm.clone() self.algorithm = algo.self_optimize( ecg_data, r_peaks, dataset.sampling_rate_hz ) return self def run(self, datapoint: ECGExampleData): # Note: We need to clone the algorithm instance, to make sure we don't leak any data between runs. algo = self.algorithm.clone() algo.detect(datapoint.data, datapoint.sampling_rate_hz) self.r_peak_positions_ = algo.r_peak_positions_ return self .. GENERATED FROM PYTHON SOURCE LINES 79-83 The Scorer ---------- The scorer is identical to the scoring function used in the other examples. The F1-score is still the most important parameter for our comparison. .. GENERATED FROM PYTHON SOURCE LINES 83-104 .. code-block:: default from examples.algorithms.algorithms_qrs_detection_final import ( match_events_with_reference, precision_recall_f1_score, ) def score(pipeline: MyPipeline, datapoint: ECGExampleData): # We use the `safe_run` wrapper instead of just run. This is always a good idea. # We don't need to clone the pipeline here, as GridSearch will already clone the pipeline internally and `run` # will clone it again. pipeline = pipeline.safe_run(datapoint) tolerance_s = 0.02 # We just use 20 ms for this example matches = match_events_with_reference( pipeline.r_peak_positions_.to_numpy(), datapoint.r_peak_positions_.to_numpy(), tolerance=tolerance_s * datapoint.sampling_rate_hz, ) precision, recall, f1_score = precision_recall_f1_score(matches) return {"precision": precision, "recall": recall, "f1_score": f1_score} .. GENERATED FROM PYTHON SOURCE LINES 105-112 Data Splitting -------------- Before performing a cross validation, we need to decide on the number of folds and type of splits. In `tpcp` we support all cross validation iterators provided in `sklearn `__. To keep the runtime low for this example, we are going to use a 3-fold CV. .. GENERATED FROM PYTHON SOURCE LINES 112-116 .. code-block:: default from sklearn.model_selection import KFold cv = KFold(n_splits=3) .. GENERATED FROM PYTHON SOURCE LINES 117-123 Cross Validation ---------------- Now we have all the pieces for the final cross validation. First we need to create instances of our data and pipeline. Then we need to wrap our pipeline instance into an :class:`~tpcp.optimize.Optimize` wrapper. Finally, we can call `tpcp.validate.cross_validate`. .. GENERATED FROM PYTHON SOURCE LINES 123-140 .. code-block:: default from tpcp.optimize import Optimize from tpcp.validate import cross_validate pipe = MyPipeline() optimizable_pipe = Optimize(pipe) results = cross_validate( optimizable_pipe, example_data, scoring=score, cv=cv, return_optimizer=True, return_train_score=True, ) result_df = pd.DataFrame(results) result_df .. rst-class:: sphx-glr-script-out .. code-block:: none CV Folds: 0%| | 0/3 [00:00
debug__score_time debug__optimize_time train__data_labels test__data_labels optimizer test__single__precision test__single__recall test__single__f1_score test__agg__precision test__agg__recall test__agg__f1_score train__single__precision train__single__recall train__single__f1_score train__agg__precision train__agg__recall train__agg__f1_score
0 0.313272 0.432160 [(group_2, 106), (group_3, 108), (group_1, 114... [(group_1, 100), (group_2, 102), (group_3, 104... Optimize(optimize_with_info=True, pipeline=MyP... [0.9995600527936648, 0.9723119520073835, 0.962... [0.9995600527936648, 0.9634202103337905, 0.968... [0.9995600527936648, 0.9678456591639871, 0.965... 0.975938 0.978213 0.977058 [0.9470529470529471, 0.9077277970011534, 0.919... [0.9353724716329551, 0.44639818491208166, 0.15... [0.9411764705882353, 0.5984790874524715, 0.260... 0.940084 0.775055 0.813250
1 0.302913 0.484173 [(group_1, 100), (group_2, 102), (group_3, 104... [(group_2, 106), (group_3, 108), (group_1, 114... Optimize(optimize_with_info=True, pipeline=MyP... [0.9192846785886902, 0.8467005076142132, 0.910... [0.938332511100148, 0.47305728871242203, 0.189... [0.9287109375, 0.6069868995633187, 0.313656387... 0.918491 0.647829 0.710830 [0.9995600527936648, 0.9725022914757103, 0.962... [0.9995600527936648, 0.9702789208962048, 0.968... [0.9995600527936648, 0.9713893339436942, 0.965... 0.954602 0.952627 0.953583
2 0.295439 0.492822 [(group_1, 100), (group_2, 102), (group_3, 104... [(group_3, 119), (group_1, 121), (group_2, 123... Optimize(optimize_with_info=True, pipeline=MyP... [0.9984909456740443, 1.0, 0.9993416721527321, ... [0.9989934574735783, 0.9243156199677939, 1.0, ... [0.99874213836478, 0.9606694560669455, 0.99967... 0.941766 0.909605 0.925066 [0.9995600527936648, 0.972183588317107, 0.9638... [0.9995600527936648, 0.9588477366255144, 0.968... [0.9995600527936648, 0.9654696132596684, 0.965... 0.965628 0.799168 0.833407


.. GENERATED FROM PYTHON SOURCE LINES 141-150 Understanding the Results ------------------------- The cross validation provides a lot of outputs (some of them can be disabled using the function parameters). To simplify things a little, we will split the output into four parts: The main output are the test set performance values. They are aggregated over all datapoints in each fold using the aggregation specified in the `scoring` function. They all are prefixed with ``test__agg__`` to easily filter them out within the results. Each row corresponds to performance in respective fold. .. GENERATED FROM PYTHON SOURCE LINES 150-153 .. code-block:: default performance = result_df.filter(like="test__agg__") performance .. raw:: html
test__agg__precision test__agg__recall test__agg__f1_score
0 0.975938 0.978213 0.977058
1 0.918491 0.647829 0.710830
2 0.941766 0.909605 0.925066


.. GENERATED FROM PYTHON SOURCE LINES 154-157 The final generalization performance you would report is usually the average over all folds. The STD can also be interesting, as it tells you how stable your optimization is and if your splits provide comparable data distributions. .. GENERATED FROM PYTHON SOURCE LINES 157-160 .. code-block:: default generalization_performance = performance.agg(["mean", "std"]) generalization_performance .. raw:: html
test__agg__precision test__agg__recall test__agg__f1_score
mean 0.945399 0.845216 0.870985
std 0.028895 0.174350 0.141113


.. GENERATED FROM PYTHON SOURCE LINES 161-166 If you need more insight into the results (e.g. when the std of your results is high), you can inspect the individual score for each data point. In this example this is only a list with a single element per score, as we only had a single datapoint per fold. In a real scenario, this will be a list of all datapoints. Inspecting this list can help to identify potential issues with certain parts of your dataset. .. GENERATED FROM PYTHON SOURCE LINES 166-169 .. code-block:: default single_performance = result_df.filter(like="test__single__") single_performance .. raw:: html
test__single__precision test__single__recall test__single__f1_score
0 [0.9995600527936648, 0.9723119520073835, 0.962... [0.9995600527936648, 0.9634202103337905, 0.968... [0.9995600527936648, 0.9678456591639871, 0.965...
1 [0.9192846785886902, 0.8467005076142132, 0.910... [0.938332511100148, 0.47305728871242203, 0.189... [0.9287109375, 0.6069868995633187, 0.313656387...
2 [0.9984909456740443, 1.0, 0.9993416721527321, ... [0.9989934574735783, 0.9243156199677939, 1.0, ... [0.99874213836478, 0.9606694560669455, 0.99967...


.. GENERATED FROM PYTHON SOURCE LINES 170-172 To link the performance values to a specific datapoint, you can look at the `test__data_labels` field. It is often quite handy to combine all the results into on df: .. GENERATED FROM PYTHON SOURCE LINES 172-180 .. code-block:: default exploded_results = ( single_performance.explode(single_performance.columns.to_list()) .rename_axis("fold") .set_index(result_df["test__data_labels"].explode(), append=True) ) exploded_results .. raw:: html
test__single__precision test__single__recall test__single__f1_score
fold test__data_labels
0 (group_1, 100) 0.99956 0.99956 0.99956
(group_2, 102) 0.972312 0.96342 0.967846
(group_3, 104) 0.962963 0.968147 0.965548
(group_1, 105) 0.968918 0.981726 0.97528
1 (group_2, 106) 0.919285 0.938333 0.928711
(group_3, 108) 0.846701 0.473057 0.606987
(group_1, 114) 0.910486 0.189462 0.313656
(group_2, 116) 0.997495 0.990464 0.993967
2 (group_3, 119) 0.998491 0.998993 0.998742
(group_1, 121) 1.0 0.924316 0.960669
(group_2, 123) 0.999342 1.0 0.999671
(group_3, 200) 0.769231 0.71511 0.741184


.. GENERATED FROM PYTHON SOURCE LINES 181-185 Even further insight is provided by the train results (if activated via ``return_train_score``). These are the performance results on the train set and can indicate if the training provided meaningful results and can also indicate over-fitting, if the performance of the test set is much worse than the performance on the train set. .. GENERATED FROM PYTHON SOURCE LINES 185-188 .. code-block:: default train_performance = result_df.filter(like="train__") train_performance .. raw:: html
train__data_labels train__single__precision train__single__recall train__single__f1_score train__agg__precision train__agg__recall train__agg__f1_score
0 [(group_2, 106), (group_3, 108), (group_1, 114... [0.9470529470529471, 0.9077277970011534, 0.919... [0.9353724716329551, 0.44639818491208166, 0.15... [0.9411764705882353, 0.5984790874524715, 0.260... 0.940084 0.775055 0.813250
1 [(group_1, 100), (group_2, 102), (group_3, 104... [0.9995600527936648, 0.9725022914757103, 0.962... [0.9995600527936648, 0.9702789208962048, 0.968... [0.9995600527936648, 0.9713893339436942, 0.965... 0.954602 0.952627 0.953583
2 [(group_1, 100), (group_2, 102), (group_3, 104... [0.9995600527936648, 0.972183588317107, 0.9638... [0.9995600527936648, 0.9588477366255144, 0.968... [0.9995600527936648, 0.9654696132596684, 0.965... 0.965628 0.799168 0.833407


.. GENERATED FROM PYTHON SOURCE LINES 189-191 The final level of debug information is provided via the timings (note the long runtime in fold 0 can be explained by the jit-compiler used in `BarthDtw`) ... .. GENERATED FROM PYTHON SOURCE LINES 191-194 .. code-block:: default timings = result_df.filter(like="debug__") timings .. raw:: html
debug__score_time debug__optimize_time
0 0.313272 0.432160
1 0.302913 0.484173
2 0.295439 0.492822


.. GENERATED FROM PYTHON SOURCE LINES 195-199 ... and the optimized pipeline object. This is the actual trained object generated in this fold. You can apply it to other data for testing or inspect the actual object for further debug information that might be stored on it. .. GENERATED FROM PYTHON SOURCE LINES 199-202 .. code-block:: default optimized_pipeline = result_df["optimizer"][0] optimized_pipeline .. rst-class:: sphx-glr-script-out .. code-block:: none Optimize(optimize_with_info=True, pipeline=MyPipeline(algorithm=OptimizableQrsDetector(high_pass_filter_cutoff_hz=1, max_heart_rate_bpm=200.0, min_r_peak_height_over_baseline=1.0, r_peak_match_tolerance_s=0.01)), safe_optimize=True) .. GENERATED FROM PYTHON SOURCE LINES 203-205 .. code-block:: default optimized_pipeline.optimized_pipeline_.get_params() .. rst-class:: sphx-glr-script-out .. code-block:: none {'algorithm__high_pass_filter_cutoff_hz': 1, 'algorithm__max_heart_rate_bpm': 200.0, 'algorithm__min_r_peak_height_over_baseline': np.float64(0.6322168257130579), 'algorithm__r_peak_match_tolerance_s': 0.01, 'algorithm': OptimizableQrsDetector(high_pass_filter_cutoff_hz=1, max_heart_rate_bpm=200.0, min_r_peak_height_over_baseline=np.float64(0.6322168257130579), r_peak_match_tolerance_s=0.01)} .. GENERATED FROM PYTHON SOURCE LINES 206-215 Further Notes ------------- We also support grouped cross validation. Check the :ref:`dataset guide ` on how you can group the data before cross-validation or generate data labels to be used with `GroupedKFold`. `Optimize` is just an example of an optimizer that can be passed to cross validation. You can pass any `tpcp` optimizer like `GridSearch` or `GridSearchCV` or custom optimizer that implement the `tpcp.optimize.BaseOptimize` interface. .. rst-class:: sphx-glr-timing **Total running time of the script:** (0 minutes 5.890 seconds) **Estimated memory usage:** 28 MB .. _sphx_glr_download_auto_examples_validation__02_cross_validation.py: .. only:: html .. container:: sphx-glr-footer sphx-glr-footer-example .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: _02_cross_validation.py <_02_cross_validation.py>` .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download Jupyter notebook: _02_cross_validation.ipynb <_02_cross_validation.ipynb>` .. only:: html .. rst-class:: sphx-glr-signature `Gallery generated by Sphinx-Gallery `_