Skip to content

Commit 93ce1eb

Browse files
[ADD] Coverage calculation
1 parent 4493270 commit 93ce1eb

File tree

9 files changed

+269
-12
lines changed

9 files changed

+269
-12
lines changed

.codecov.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#see https://github.com/codecov/support/wiki/Codecov-Yaml
2+
codecov:
3+
notify:
4+
require_ci_to_pass: yes
5+
6+
coverage:
7+
precision: 2 # 2 = xx.xx%, 0 = xx%
8+
round: nearest # how coverage is rounded: down/up/nearest
9+
range: 10...90 # custom range of coverage colors from red -> yellow -> green
10+
status:
11+
# https://codecov.readme.io/v1.0/docs/commit-status
12+
project:
13+
default:
14+
against: auto
15+
target: 70% # specify the target coverage for each commit status
16+
threshold: 50% # allow this little decrease on project
17+
# https://github.com/codecov/support/wiki/Filtering-Branches
18+
# branches: master
19+
if_ci_failed: error
20+
# https://github.com/codecov/support/wiki/Patch-Status
21+
patch:
22+
default:
23+
against: auto
24+
target: 30% # specify the target "X%" coverage to hit
25+
threshold: 50% # allow this much decrease on patch
26+
changes: false
27+
28+
parsers:
29+
gcov:
30+
branch_detection:
31+
conditional: true
32+
loop: true
33+
macro: false
34+
method: false
35+
javascript:
36+
enable_partials: false
37+
38+
comment:
39+
layout: header, diff
40+
require_changes: false
41+
behavior: default # update if exists else create new
42+
branches: *

.github/workflows/pytest.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ jobs:
99
strategy:
1010
matrix:
1111
python-version: [3.6, 3.7, 3.8]
12-
fail-fast: false
12+
include:
13+
- python-version: 3.8
14+
code-cov: true
15+
fail-fast: false
1316
max-parallel: 2
1417

1518
steps:

autoPyTorch/api/base_task.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
STRING_TO_OUTPUT_TYPES,
3535
STRING_TO_TASK_TYPES,
3636
)
37+
from autoPyTorch.data.base_validator import BaseInputValidator
3738
from autoPyTorch.datasets.base_dataset import BaseDataset
3839
from autoPyTorch.datasets.resampling_strategy import CrossValTypes, HoldoutValTypes
3940
from autoPyTorch.ensemble.ensemble_builder import EnsembleBuilderManager
@@ -189,6 +190,8 @@ def __init__(
189190

190191
self._dask_client = None
191192

193+
self.InputValidator: Optional[BaseInputValidator] = None
194+
192195
self.search_space_updates = search_space_updates
193196
if search_space_updates is not None:
194197
if not isinstance(self.search_space_updates,
@@ -253,12 +256,6 @@ def get_pipeline_options(self) -> dict:
253256
"""
254257
return self.pipeline_options
255258

256-
# def set_search_space(self, search_space: ConfigurationSpace) -> None:
257-
# """
258-
# Update the search space.
259-
# """
260-
# raise NotImplementedError
261-
#
262259
def get_search_space(self, dataset: BaseDataset = None) -> ConfigurationSpace:
263260
"""
264261
Returns the current search space as ConfigurationSpace object.
@@ -272,8 +269,8 @@ def get_search_space(self, dataset: BaseDataset = None) -> ConfigurationSpace:
272269
include=self.include_components,
273270
exclude=self.exclude_components,
274271
search_space_updates=self.search_space_updates)
275-
raise Exception("No search space initialised and no dataset passed. "
276-
"Can't create default search space without the dataset")
272+
raise ValueError("No search space initialised and no dataset passed. "
273+
"Can't create default search space without the dataset")
277274

278275
def _get_logger(self, name: str) -> PicklableClientLogger:
279276
"""

autoPyTorch/datasets/base_dataset.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,10 @@ def __init__(
129129
if len(self.train_tensors) == 2 and self.train_tensors[1] is not None:
130130
self.output_type: str = type_of_target(self.train_tensors[1])
131131

132-
if STRING_TO_OUTPUT_TYPES[self.output_type] in CLASSIFICATION_OUTPUTS:
132+
if (
133+
self.output_type in STRING_TO_OUTPUT_TYPES
134+
and STRING_TO_OUTPUT_TYPES[self.output_type] in CLASSIFICATION_OUTPUTS
135+
):
133136
self.output_shape = len(np.unique(self.train_tensors[1]))
134137
else:
135138
self.output_shape = self.train_tensors[1].shape[-1] if self.train_tensors[1].ndim > 1 else 1

autoPyTorch/datasets/resampling_strategy.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,10 @@ def stratified_k_fold_cross_validation(random_state: np.random.RandomState,
162162
indices: np.ndarray,
163163
**kwargs: Any
164164
) -> List[Tuple[np.ndarray, np.ndarray]]:
165-
cv = StratifiedKFold(n_splits=num_splits, random_state=random_state)
165+
166+
shuffle = kwargs.get('shuffle', True)
167+
cv = StratifiedKFold(n_splits=num_splits, shuffle=shuffle,
168+
random_state=random_state if not shuffle else None)
166169
splits = list(cv.split(indices, kwargs["stratify"]))
167170
return splits
168171

test/test_api/test_api.py

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import sklearn
1616
import sklearn.datasets
17+
from sklearn.base import BaseEstimator
1718
from sklearn.base import clone
1819
from sklearn.ensemble import VotingClassifier, VotingRegressor
1920

@@ -26,6 +27,7 @@
2627
HoldoutValTypes,
2728
)
2829
from autoPyTorch.optimizer.smbo import AutoMLSMBO
30+
from autoPyTorch.pipeline.components.setup.traditional_ml.classifier_models import _classifiers
2931
from autoPyTorch.pipeline.components.training.metrics.metrics import accuracy
3032

3133

@@ -183,9 +185,12 @@ def test_tabular_classification(openml_id, resampling_strategy, backend, resampl
183185
assert len(estimator.ensemble_.identifiers_) == len(estimator.ensemble_.weights_)
184186

185187
y_pred = estimator.predict(X_test)
186-
187188
assert np.shape(y_pred)[0] == np.shape(X_test)[0]
188189

190+
# Make sure that predict proba has the expected shape
191+
probabilites = estimator.predict_proba(X_test)
192+
assert np.shape(probabilites) == (np.shape(X_test)[0], 2)
193+
189194
score = estimator.score(y_pred, y_test)
190195
assert 'accuracy' in score
191196

@@ -203,6 +208,9 @@ def test_tabular_classification(openml_id, resampling_strategy, backend, resampl
203208
restored_estimator = pickle.load(f)
204209
restored_estimator.predict(X_test)
205210

211+
# Test refit on dummy data
212+
estimator.refit(dataset=backend.load_datamanager())
213+
206214

207215
@pytest.mark.parametrize('openml_name', ("boston", ))
208216
@unittest.mock.patch('autoPyTorch.evaluation.train_evaluator.eval_function',
@@ -439,6 +447,12 @@ def test_do_dummy_prediction(dask_client, fit_dictionary_tabular):
439447
estimator._disable_file_output = []
440448
estimator._all_supported_metrics = False
441449

450+
original_memory_limit = estimator._memory_limit
451+
estimator._memory_limit = 500
452+
with pytest.raises(ValueError, match=r".*Dummy prediction failed with run state.*"):
453+
estimator._do_dummy_prediction()
454+
455+
estimator._memory_limit = original_memory_limit
442456
estimator._do_dummy_prediction()
443457

444458
# Ensure that the dummy predictions are not in the current working
@@ -464,3 +478,78 @@ def test_do_dummy_prediction(dask_client, fit_dictionary_tabular):
464478
estimator._clean_logger()
465479

466480
del estimator
481+
482+
483+
# TODO: Make faster when https://github.com/automl/Auto-PyTorch/pull/223 is incorporated
484+
@pytest.mark.parametrize("fit_dictionary_tabular", ['classification_categorical_only'], indirect=True)
485+
def test_do_traditional_pipeline(fit_dictionary_tabular):
486+
backend = fit_dictionary_tabular['backend']
487+
estimator = TabularClassificationTask(
488+
backend=backend,
489+
resampling_strategy=HoldoutValTypes.holdout_validation,
490+
ensemble_size=0,
491+
)
492+
493+
# Setup pre-requisites normally set by search()
494+
estimator._create_dask_client()
495+
estimator._metric = accuracy
496+
estimator._logger = estimator._get_logger('test')
497+
estimator._memory_limit = 5000
498+
estimator._time_for_task = 60
499+
estimator._disable_file_output = []
500+
estimator._all_supported_metrics = False
501+
502+
estimator._do_traditional_prediction(time_left=60, func_eval_time_limit_secs=30)
503+
504+
# The models should not be on the current directory
505+
assert not os.path.exists(os.path.join(os.getcwd(), '.autoPyTorch'))
506+
507+
# Then we should have fitted 5 classifiers
508+
# Maybe some of them fail (unlikely, but we do not control external API)
509+
# but we want to make this test robust
510+
at_least_one_model_checked = False
511+
for i in range(2, 7):
512+
pred_path = os.path.join(
513+
backend.temporary_directory, '.autoPyTorch', 'runs', f"1_{i}_50.0",
514+
f"predictions_ensemble_1_{i}_50.0.npy"
515+
)
516+
assert os.path.exists(pred_path)
517+
518+
model_path = os.path.join(backend.temporary_directory,
519+
'.autoPyTorch',
520+
'runs', f"1_{i}_50.0",
521+
f"1.{i}.50.0.model")
522+
523+
# Make sure the dummy model complies with scikit learn
524+
# get/set params
525+
assert os.path.exists(model_path)
526+
with open(model_path, 'rb') as model_handler:
527+
model = pickle.load(model_handler)
528+
clone(model)
529+
assert model.config == list(_classifiers.keys())[i - 2]
530+
at_least_one_model_checked = True
531+
if not at_least_one_model_checked:
532+
pytest.fail("Not even one single traditional pipeline was fitted")
533+
534+
estimator._close_dask_client()
535+
estimator._clean_logger()
536+
537+
del estimator
538+
539+
540+
@pytest.mark.parametrize("api_type", [TabularClassificationTask, TabularRegressionTask])
541+
def test_unsupported_msg(api_type):
542+
api = api_type()
543+
with pytest.raises(ValueError, match=r".*Dataset is incompatible for the given task.*"):
544+
api._get_required_dataset_properties('dummy')
545+
with pytest.raises(ValueError, match=r".*is only supported after calling search. Kindly .*"):
546+
api.predict(np.ones((10, 10)))
547+
548+
549+
@pytest.mark.parametrize("fit_dictionary_tabular", ['classification_categorical_only'], indirect=True)
550+
@pytest.mark.parametrize("api_type", [TabularClassificationTask, TabularRegressionTask])
551+
def test_build_pipeline(api_type, fit_dictionary_tabular):
552+
api = api_type()
553+
pipeline = api.build_pipeline(fit_dictionary_tabular['dataset_properties'])
554+
assert isinstance(pipeline, BaseEstimator)
555+
assert len(pipeline.steps) > 0

test/test_api/test_base_api.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import os
2+
import pathlib
3+
import pickle
4+
import sys
5+
import unittest
6+
from test.test_api.utils import dummy_do_dummy_prediction, dummy_eval_function
7+
8+
import numpy as np
9+
10+
import pandas as pd
11+
12+
import pytest
13+
14+
15+
import sklearn
16+
import sklearn.datasets
17+
from sklearn.base import clone
18+
from sklearn.ensemble import VotingClassifier, VotingRegressor
19+
20+
from smac.runhistory.runhistory import RunHistory
21+
22+
from autoPyTorch.api.tabular_classification import TabularClassificationTask
23+
from autoPyTorch.api.tabular_regression import TabularRegressionTask
24+
from autoPyTorch.datasets.resampling_strategy import (
25+
CrossValTypes,
26+
HoldoutValTypes,
27+
)
28+
from autoPyTorch.optimizer.smbo import AutoMLSMBO
29+
from autoPyTorch.pipeline.components.training.metrics.metrics import accuracy
30+
31+
from autoPyTorch.api.base_task import BaseTask
32+
33+
34+
# ====
35+
# Test
36+
# ====
37+
@pytest.mark.parametrize("fit_dictionary_tabular", ['classification_categorical_only'], indirect=True)
38+
def test_nonsupported_arguments(fit_dictionary_tabular):
39+
with pytest.raises(ValueError, match=r".*Expected search space updates to be of instance.*"):
40+
api = BaseTask(search_space_updates='None')
41+
42+
api = BaseTask()
43+
with pytest.raises(ValueError, match=r".*Invalid configuration arguments given.*"):
44+
api.set_pipeline_config(unsupported=True)
45+
with pytest.raises(ValueError, match=r".*No search space initialised and no dataset.*"):
46+
api.get_search_space()
47+
api.resampling_strategy = None
48+
with pytest.raises(ValueError, match=r".*Resampling strategy is needed to determine.*"):
49+
api._load_models()
50+
api.resampling_strategy = unittest.mock.MagicMock()
51+
with pytest.raises(ValueError, match=r".*Providing a metric to AutoPytorch is required.*"):
52+
api._load_models()
53+
api.ensemble_ = unittest.mock.MagicMock()
54+
with pytest.raises(ValueError, match=r".*No metric found. Either fit/search has not been.*"):
55+
api.score(np.ones(10), np.ones(10))
56+
api._metric = unittest.mock.MagicMock()
57+
with pytest.raises(ValueError, match=r".*No valid model found in run history.*"):
58+
api._load_models()
59+
dataset = fit_dictionary_tabular['backend'].load_datamanager()
60+
with pytest.raises(ValueError, match=r".*Incompatible dataset entered for current task.*"):
61+
api._search('accuracy', dataset)
62+
63+
def returnfalse():
64+
return False
65+
66+
api._load_models = returnfalse
67+
with pytest.raises(ValueError, match=r".*No ensemble found. Either fit has not yet.*"):
68+
api.predict(np.ones((10, 10)))
69+
with pytest.raises(ValueError, match=r".*No ensemble found. Either fit has not yet.*"):
70+
api.predict(np.ones((10, 10)))
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import numpy as np
2+
3+
from autoPyTorch.datasets.resampling_strategy import CrossValFuncs, HoldOutFuncs
4+
5+
6+
def test_holdoutfuncs():
7+
split = HoldOutFuncs()
8+
X = np.arange(10)
9+
y = np.ones(10)
10+
# Create a minority class
11+
y[:2] = 0
12+
train, val = split.holdout_validation(0, 0.5, X, shuffle=False)
13+
assert len(train) == len(val) == 5
14+
15+
# No shuffling
16+
np.testing.assert_array_equal(X, np.arange(10))
17+
18+
# Make sure the stratified version splits the minority class
19+
train, val = split.stratified_holdout_validation(0, 0.5, X, stratify=y)
20+
assert 0 in y[val]
21+
assert 0 in y[train]
22+
23+
24+
def test_crossvalfuncs():
25+
split = CrossValFuncs()
26+
X = np.arange(100)
27+
y = np.ones(100)
28+
# Create a minority class
29+
y[:11] = 0
30+
splits = split.shuffle_split_cross_validation(0, 10, X)
31+
assert len(splits) == 10
32+
assert all([len(s[1]) == 10 for s in splits])
33+
34+
# Make sure the stratified version splits the minority class
35+
splits = split.stratified_shuffle_split_cross_validation(0, 10, X, stratify=y)
36+
assert len(splits) == 10
37+
assert all([0 in y[s[1]] for s in splits])
38+
39+
#
40+
splits = split.stratified_k_fold_cross_validation(0, 10, X, stratify=y)
41+
assert len(splits) == 10
42+
assert all([0 in y[s[1]] for s in splits])

test/test_datasets/test_tabular_dataset.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import numpy as np
2+
13
import pytest
24

5+
from autoPyTorch.datasets.tabular_dataset import TabularDataset
36
from autoPyTorch.utils.pipeline import get_dataset_requirements
47

58

@@ -38,3 +41,8 @@ def test_get_dataset_properties(backend, fit_dictionary_tabular):
3841
assert datamanager.train_tensors[0].shape == fit_dictionary_tabular['X_train'].shape
3942
assert datamanager.train_tensors[1].shape == fit_dictionary_tabular['y_train'].shape
4043
assert datamanager.task_type == 'tabular_classification'
44+
45+
46+
def test_not_supported():
47+
with pytest.raises(ValueError, match=r".*A feature validator is required to build.*"):
48+
TabularDataset(np.ones(10), np.ones(10))

0 commit comments

Comments
 (0)