Repository: scikit-learn/scikit-learn
Pull request: MAINT Add one-sided set differences for clarity in param validation
#### Reference Issues/PRs
Fixes #23744

#### What does this implement/fix? Explain your changes.
Add one-sided set differences for clarity in param validation

#### Any other comments?

Problem statement:
Add set difference to `validate_parameter_constraints` `ValueError`
### Describe the workflow you want to enable

Currently the `ValueError` protuced by `validate_parameter_constraints` can get very long and unruly when there are a lot of validation parameters in either the passed constraints, or in the params to be validated. This can make debugging a bit strained. I think it would be a huge help to also include the set differences to more clearly explain what may be happening.

### Describe your proposed solution

Something along the lines of:

```python
...
if params.keys() != parameter_constraints.keys():
    raise ValueError(
        f"The parameter constraints {list(parameter_constraints.keys())} do not "
        f"match the parameters to validate {list(params.keys())}.\n\n"
        f"Consider the extra constraints {set(parameter_constraints.keys())-set(params.keys())}"
        f" and missing expected constraints {set(params.keys())-set(parameter_constraints.keys())}"
    )
```

### Describe alternatives you've considered, if relevant

N/A

### Additional context

```python-traceback


ValueError: The parameter constraints ['fit_intercept', 'max_iter', 'tol', 'shuffle', 'verbose', 'random_state', 'warm_start', 'average', 'loss', 'n_jobs', 'class_weight', 'early_stopping', 'validation_fraction', 'n_iter_no_change'] do not match the parameters to validate ['alpha', 'class_weight', 'early_stopping', 'eta0', 'fit_intercept', 'l1_ratio', 'max_iter', 'n_iter_no_change', 'n_jobs', 'penalty', 'random_state', 'shuffle', 'tol', 'validation_fraction', 'verbose', 'warm_start'].
E
E           Consider the extra constraints {'average', 'loss'} and missing expected constraints {'l1_ratio', 'penalty', 'alpha', 'eta0'}
```

I believe the set differences make this **_much_** more readable, and should be a very small/simple inclusion.

Hints from issue discussion:
@jeremiedbb Wondering your opinion on this
I felt the same difficulty while reviewing some PRs. I think this would be a good addition the common tests `check_param_validation` and `def test_function_param_validation`

Commit message: Addressed review

Patch to review:
diff --git a/sklearn/tests/test_public_functions.py b/sklearn/tests/test_public_functions.py
index 20ab0665eec88..d4e645c052dab 100644
--- a/sklearn/tests/test_public_functions.py
+++ b/sklearn/tests/test_public_functions.py
@@ -45,11 +45,15 @@ def test_function_param_validation(func_module):
 
     # check that there is a constraint for each parameter
     if func_params:
+        validation_params = parameter_constraints.keys()
+        unexpected_params = set(validation_params) - set(func_params)
+        missing_params = set(func_params) - set(validation_params)
         err_msg = (
-            "Mismatch between the parameter constraints defined in the "
-            f"validate_params decorator and the parameters of {func_name}."
+            "Mismatch between _parameter_constraints and the parameters of"
+            f" {func_name}.\nConsider the unexpected parameters {unexpected_params} and"
+            f" expected but missing parameters {missing_params}\n"
         )
-        assert list(parameter_constraints.keys()) == func_params, err_msg
+        assert set(validation_params) == set(func_params), err_msg
 
     # this object does not have a valid type for sure for all params
     param_with_bad_type = type("BadType", (), {})()
diff --git a/sklearn/utils/_param_validation.py b/sklearn/utils/_param_validation.py
index e3118a07b5bfd..e37eba4e00b2c 100644
--- a/sklearn/utils/_param_validation.py
+++ b/sklearn/utils/_param_validation.py
@@ -43,13 +43,20 @@ def validate_parameter_constraints(parameter_constraints, params, caller_name):
     caller_name : str
         The name of the estimator or function or method that called this function.
     """
-    if params.keys() != parameter_constraints.keys():
+    if len(set(parameter_constraints.keys()) - set(params.keys())) != 0:
         raise ValueError(
-            f"The parameter constraints {list(parameter_constraints.keys())} do not "
-            f"match the parameters to validate {list(params.keys())}."
+            f"The parameter constraints {list(parameter_constraints.keys())}"
+            " contain unexpected parameters"
+            f" {set(parameter_constraints.keys())-set(params.keys())}"
         )
 
     for param_name, param_val in params.items():
+        # We allow parameters to not have a constraint so that third party estimators
+        # can inherit from sklearn estimators without having to necessarily use the
+        # validation tools.
+        if param_name not in parameter_constraints:
+            continue
+
         constraints = parameter_constraints[param_name]
 
         if constraints == "no_validation":
diff --git a/sklearn/utils/estimator_checks.py b/sklearn/utils/estimator_checks.py
index 69857117b4ab2..f6607113c3988 100644
--- a/sklearn/utils/estimator_checks.py
+++ b/sklearn/utils/estimator_checks.py
@@ -4045,10 +4045,15 @@ def check_param_validation(name, estimator_orig):
 
     # check that there is a constraint for each parameter
     if estimator_params:
+        validation_params = estimator_orig._parameter_constraints.keys()
+        unexpected_params = set(validation_params) - set(estimator_params)
+        missing_params = set(estimator_params) - set(validation_params)
         err_msg = (
             f"Mismatch between _parameter_constraints and the parameters of {name}."
+            f"\nConsider the unexpected parameters {unexpected_params} and expected but"
+            f" missing parameters {missing_params}"
         )
-        assert estimator_orig._parameter_constraints.keys() == estimator_params, err_msg
+        assert validation_params == estimator_params, err_msg
 
     # this object does not have a valid type for sure for all params
     param_with_bad_type = type("BadType", (), {})()
diff --git a/sklearn/utils/tests/test_param_validation.py b/sklearn/utils/tests/test_param_validation.py
index 0f8931944584e..55d81b58f5d23 100644
--- a/sklearn/utils/tests/test_param_validation.py
+++ b/sklearn/utils/tests/test_param_validation.py
@@ -403,11 +403,23 @@ def test_validate_params_match_error():
     def func(a, b):
         pass
 
-    match = r"The parameter constraints .* do not match the parameters to validate"
+    match = r"The parameter constraints .* contain unexpected parameters {'c'}"
     with pytest.raises(ValueError, match=match):
         func(1, 2)
 
 
+def test_validate_params_missing_params():
+    """Check that no error is raised when there are parameters without
+    constraints
+    """
+
+    @validate_params({"a": [int]})
+    def func(a, b):
+        pass
+
+    func(1, 2)
+
+
 def test_decorate_validated_function():
     """Check that validate_params functions can be decorated"""
     decorated_function = deprecated()(_func)


Code review comments:
diff_hunk: @@ -43,13 +43,20 @@ def validate_parameter_constraints(parameter_constraints, params, caller_name):
     caller_name : str
         The name of the estimator or function or method that called this function.
     """
-    if params.keys() != parameter_constraints.keys():
+    if len(set(parameter_constraints.keys()) - set(params.keys())) != 0:
         raise ValueError(
-            f"The parameter constraints {list(parameter_constraints.keys())} do not "
-            f"match the parameters to validate {list(params.keys())}."
+            f"The parameter constraints {list(parameter_constraints.keys())}"
+            " contain unexpected parameters"
+            f" {set(parameter_constraints.keys())-set(params.keys())}"
path: sklearn/utils/_param_validation.py
text: @user1:
linter is not complaining about spaces around operator ?
```suggestion
            f" {set(parameter_constraints) - set(params)}"
```

diff_hunk: @@ -43,13 +43,20 @@ def validate_parameter_constraints(parameter_constraints, params, caller_name):
     caller_name : str
         The name of the estimator or function or method that called this function.
     """
-    if params.keys() != parameter_constraints.keys():
+    if len(set(parameter_constraints.keys()) - set(params.keys())) != 0:
path: sklearn/utils/_param_validation.py
text: @user1:
```suggestion
    if len(set(parameter_constraints) - set(params)) != 0:
```

diff_hunk: @@ -43,13 +43,20 @@ def validate_parameter_constraints(parameter_constraints, params, caller_name):
     caller_name : str
         The name of the estimator or function or method that called this function.
     """
-    if params.keys() != parameter_constraints.keys():
+    if len(set(parameter_constraints.keys()) - set(params.keys())) != 0:
         raise ValueError(
-            f"The parameter constraints {list(parameter_constraints.keys())} do not "
-            f"match the parameters to validate {list(params.keys())}."
+            f"The parameter constraints {list(parameter_constraints.keys())}"
path: sklearn/utils/_param_validation.py
text: @user1:
```suggestion
            f"The parameter constraints {list(parameter_constraints)}"
```