Simplify decision tree removing redundant decisions

### Describe the workflow you want to enable

Description: Add a new method simplify() to the decision tree Class that returns a simplified version of the decision tree by pruning redundant leaves that do not add new decision paths. This simplification method will create a new instance of the tree with fewer nodes, allowing users to obtain a more concise and interpretable tree.

Motivation: Decision trees are known for their ability to provide easily interpretable models, but sometimes they can grow to a size that makes interpretation challenging. A simplified version of the decision tree will make it easier for users to understand the model, especially in situations where the tree has a large number of redundant leaves.

Example:
![image](https://user-images.githubusercontent.com/11466701/218794306-eac085c6-2426-4e1f-aaf9-d563b2561922.png)
Can be simplified to
![image](https://user-images.githubusercontent.com/11466701/218795262-0a625e63-633d-44e0-97b2-653b0b8fc27a.png)

Related Work: There are related issues in [Scikit-learn #10810](https://github.com/scikit-learn/scikit-learn/issues/10810), [IBM Taxinomitis #226](https://github.com/IBM/taxinomitis/issues/226) and [Stack Overfflow](https://stackoverflow.com/questions/51397109/prune-unnecessary-leaves-in-sklearn-decisiontreeclassifier) that point to the need for decision tree simplification. Scikit-learn has added post-pruning methods based on cost, as far as I know, they do not achieve the same level of simplification proposed in this feature.



### Describe your proposed solution

Proposed Solution: Add a simplify() method to the decision tree module that returns a simplified version of the tree. The method will prune redundant leaves that do not add new decision paths. The new tree will have fewer nodes and be easier to interpret.

Example Usage:
```Python
from sklearn.tree import DecisionTreeClassifier

clf = DecisionTreeClassifier()

...

clf.fit(X, y)

tree = clf.tree_
simplified_tree = tree.simplify()
```


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

An alternative way is to create ad hoc functions to achieve this simplification. But I believe this feature is important enough to be in the main repository

### Additional context

I can work on this feature request and send a PR, if approved by members.

COMMENT:
Hi Glemaitre. As far as I understand it, this is a special case of Reduced Error Pruning (REP), setting the *pruning set* as the *training set*.

If keeping the original algorithm is a hard requirement. I suggest including a `pruning_set` argument and set it's default to None or "training_set", in this case the behavior will be as described in the first comment. e.g.: 

```Python
def simplify(self, pruning_set=None):...
```

and the usage would be something like:

```Python
from sklearn.tree import DecisionTreeClassifier

clf = DecisionTreeClassifier()

...

clf.fit(X, y)

tree = clf.tree_
simplified_tree = tree.simplify(pruning_set=X) # Same behavior as tree.simplify()
```

The main references I used:
*Simplifying decision trees*, JR Quinlan 1987, [Cited by 3349](https://scholar.google.com/scholar?cluster=7122165111549956239&hl=en&as_sdt=2005&sciodt=0,5).
*An analysis of reduced error pruning*, T Elomaa, M Kaariainen, 2001, [Cited by 158](https://scholar.google.com/scholar?hl=en&as_sdt=0,5&q=An+Analysis+of+Reduced+Error+Pruning).
*Simplifying decision trees: A survey*, LA Breslow, DW Aha, 1997, [Cited by 499](https://scholar.google.com/scholar?hl=en&as_sdt=0%2C5&q=Simplifying+decision+trees+a+survey&btnG=).

The main characteristic of this method  according to a quote from the third reference is "REP is guaranteed to produce the smallest pruned tree with the lowest error rate with respect to the *pruning set*".

Apart from these reasons, REP is a classical pruning algorithm that is not present in scikit-learn at the moment (as far as I know).

Labels: New Feature, module:tree, Needs Investigation