Searching Over Spaces That Are Not Grids
A grid search over several estimators is written as a list of dictionaries, with one dictionary for each estimator and its own hyperparameters.
Why One Dictionary Cannot Hold Two Models
So far, a grid search has tuned the settings of one estimator. In this article the search is allowed to choose between two different estimators as well as their settings. The question it answers is: which combination of model and settings scores best? Before reading the code, it helps to see the problem the design solves. A grid is a set of values to try for each setting. When two model families need different settings, one shared set of keys cannot describe both of them.
The source uses two candidate families for this. The first family uses an SVC classifier, optionally with a scaling step in front of it. The second family uses a RandomForestClassifier, which needs no scaling. The SVC has gamma and C as its settings, while the RandomForestClassifier has max_features. Those parameters belong to different estimators, so the search space has to keep the two families separate.
Tracing the Two Candidate Families
Read the grid below once before you look at anything else. It is a list containing two dictionaries. The first dictionary covers the SVC family. The second covers the RandomForest family. Each dictionary's keys are the settings for that family only. The values in the code are illustrative choices made for this article, not values taken from the source, so focus on the structure rather than the specific numbers.
The search is run with the same pattern you already know from single-estimator grid search. The pipeline object is passed to GridSearchCV together with the list of grids, and the data is split first so that the test set stays untouched by the search. Notice that the two dictionaries do not share their keys. The SVC dictionary has gamma and C, and the RandomForest dictionary has max_features. The diagram shows how the list contains the two branches.
Read the diagram from the top. The list sits at the root, and each dictionary is one branch. Each dictionary's keys are the settings for that branch. Every branch is searched separately, and the search compares the best result from each branch against the others. The source explains the reason for this design directly: the two classifiers have different parameters to tune and need different preprocessing, so a list of search grids is the natural container.
How Step Names Become Key Prefixes
The grid has to say which pipeline step each value belongs to. The rule is that the name of the step is used as the parameter name. That is why the key 'classifier' holds a list of estimator objects, and the key 'preprocessing' holds a list of preprocessing objects. The step name is the key itself, with no extra text.
Hyperparameters of an estimator are written differently. You write the step name, then two underscores, then the name of the hyperparameter. So 'classifier__gamma' means the gamma setting of whatever estimator sits in the classifier step, and 'classifier__C' means its C setting. The double underscore is the separator. It tells the search where the step name ends and the setting name begins.
Skipping a Step with None
Some estimators need a preprocessing step and some do not. When a step should be skipped, for example because a RandomForest does not need preprocessing, that step can be set to None. In the grid above, this is why the RandomForest dictionary lists preprocessing as [None] alone, while the SVC dictionary lists both StandardScaler() and None. The SVC family is therefore searched both with and without scaling, and the RandomForest family is searched without scaling only.
This is the practical reason the search is organized by dictionary. Different estimators need different preprocessing, and a single shared dictionary could not express that without trying combinations that make no sense. Putting a RandomForest with a scaler would be an invalid combination in this design, and separate dictionaries prevent it from being generated.
Counting Candidates Across Dictionaries
A single grid of values multiplies out: every combination of the listed values is one candidate. Each candidate is then cross-validated. The grid in this example uses cv=5, so every candidate is fitted five times. Count each dictionary separately, then add the totals. Do not count the preprocessing options once for the whole grid, because they belong only to the dictionaries that list them.
Counting the candidates in the grid above
How many candidate settings does the grid contain, and how many fits does a cv=5 search perform?
- 1Count the SVC dictionary
Preprocessing has 2 options (StandardScaler and None), gamma has 2 values, and C has 2 values. The product is $2 \times 2 \times 2 = 8$ candidates.
- 2Count the RandomForest dictionary
Preprocessing has 1 option (None), and
max_featureshas 2 values. The product is $1 \times 2 = 2$ candidates. Gamma and C do not appear in this dictionary. - 3Add the totals
The two dictionaries are separate families, so their counts are added: $8 + 2 = 10$ candidates.
- 4Apply cross-validation
Each candidate is fitted five times with cv=5, so $10 \times 5 = 50$ fits in total.
The grid contains 10 candidate settings, and the search performs 50 fits.
What do you think happens?
Before you count, estimate how many candidate settings the grid above contains. Which mistake would change your answer the most?
Reveal answer
10, counting each dictionary's product and then adding them
The SVC dictionary gives 2 times 2 times 2, which is 8. The RandomForest dictionary gives 1 times 2, which is 2. Adding them gives 10. Multiplying across both dictionaries would wrongly create combinations of SVC and RandomForest settings that never belong together.
Common mistake Forgetting that the preprocessing options multiply the count
What it looks likeCounting the SVC dictionary as 2 times 2 and ignoring the StandardScaler and None choice.
Why it failsEach preprocessing option creates its own candidates, so the count must include it.
Do this insteadMultiply the number of options in every key of the same dictionary, including preprocessing.
Common mistake Counting the RandomForest dictionary as if it had gamma and C values
What it looks likeMultiplying 1 times 2 times 2 for the RandomForest family.
Why it failsgamma and C belong to the SVC. The RandomForest dictionary has only
max_features.Do this insteadUse only the keys that appear in that dictionary.
Common mistake Putting both families in one dictionary
Why it failsA single dictionary forces every combination of its keys, which creates settings that do not apply to each other's estimator.
Do this insteadGive each family its own dictionary and place the dictionaries in a list.
Following One Candidate Through the Pipeline
Each candidate setting is a complete pipeline configuration. For a candidate from the SVC family, the training data passes through the preprocessing step, which is either a scaler or a pass-through, and then into the classifier step, which is an SVC with that gamma and C. Grid search does this for every candidate in turn, scores each one with cross-validation, and keeps track of the best.
The path through the pipeline is the same for every candidate. Only the objects inside the two steps change. This is why one pipeline object can serve both model families: the grid decides what fills each step, and the pipeline decides the order in which the steps run.
Where a Surprising Result Diverges
After fit, the printed results report three things. best_params_ shows the winning combination, best_score_ shows its cross-validation score, and grid.score on the held-out test data gives the final check. When a multi-estimator search gives a result you did not expect, the most useful question is where the divergence began. The path has three checkpoints: the grid definition, the pipeline step names, and the printed attributes. Check them in that order.
Work through the checkpoints in order. If a key has a typo in its step prefix, the grid definition is the place the divergence began, even though the printed best_params_ looks like a plausible answer. If the grid is correct, check whether the step names in the pipeline match the prefixes you used. If both are correct, compare best_score_ with the test score: best_score_ comes from cross-validation during the search, and grid.score comes from held-out test data, so the two numbers answer different questions.
Practicing the Search Grid
Given a grid with one dictionary that lists preprocessing as [StandardScaler(), None], classifier as [SVC()], classifier__C as [1, 10, 100], and a second dictionary that lists preprocessing as [None], classifier as [RandomForestClassifier()], and classifier__max_features as [2, 4, 6], how many candidate settings does the grid contain? Then say which key would be wrong if you wrote 'estimator__C' instead of 'classifier__C' in a pipeline whose step is named classifier.
Hints
- Count each dictionary's product separately, then add them.
- The prefix must match the name of a pipeline step.
Summary of the Search Space Design
- 1A search over several estimators is written as a list of dictionaries, one dictionary per estimator family with its own keys.
- 2A pipeline step name is the key that holds its estimators, and a hyperparameter is written as the step name, two underscores, and the setting name.
- 3None placed in a step's list skips that step, so the SVC family can be searched with and without scaling while the RandomForest is searched without scaling.
- 4Count each dictionary's candidates as a product of its value lists, add the dictionary totals, then multiply by the cv folds to get the fit count.
- 5When a result is unexpected, check the grid definition, then the step names, then best_params_, best_score_, and the test-set score, in that order.
At a glance
- A grid over several estimators is a list of dictionaries, one per estimator family, because each family has its own hyperparameters.
- The pipeline step name is the key that holds its estimators, and step__parameter names a hyperparameter of whatever estimator sits in that step.
- Setting a step to None skips it, which lets one family be searched with and without preprocessing while another is not.
- Count candidates per dictionary as a product of value-list lengths, then add the dictionary totals.
- Check the grid definition, then step names, then best_params_, best_score_, and grid.score to locate a surprising result.