New function to export output from Decision Tree: `sklearn.tree.export_dataframe`

### Describe the workflow you want to enable

When building a Decision Tree, I'd like the ability to export the internals of the tree as a Pandas Data frame.

This function would operate syntactically similar to the current internal functions:

- [`sklearn.tree.export_text`][export_text]
- [`sklearn.tree.export_graphviz`][export_graphviz]

However, instead, the output would be as a pandas DataFrame object.

[export_text]: https://scikit-learn.org/stable/modules/generated/sklearn.tree.export_text.html
[export_graphviz]: https://scikit-learn.org/stable/modules/generated/sklearn.tree.export_graphviz.html

### Describe your proposed solution

**Classification Example**

We can set up our model as follows:

```py
>>> # Import packages
>>> from sklearn.model_selection import train_test_split
>>> from sklearn.datasets import load_iris
>>> from sklearn.tree import DecisionTreeClassifier
>>> from sklearn import tree

>>> # Prepare data
>>> iris = load_iris()
>>> X = iris.data
>>> y = iris.target
>>> X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)

>>> # Build model
>>> clf = DecisionTreeClassifier(max_leaf_nodes=3, random_state=0)
>>> clf.fit(X_train, y_train)
```

And when we can print the model as follows:

```py
>>> tree.plot_tree(
...     decision_tree=clf,
...     feature_names=iris.feature_names,
... )
```
![image](https://github.com/scikit-learn/scikit-learn/assets/44449504/b2b8573a-cf87-43d2-be7c-febff94000be)

And the text output:

```py
>>> print(
...     tree.export_text(
...         decision_tree=clf,
...         feature_names=iris.feature_names,
...     )
... )
|--- petal width (cm) <= 0.80
|   |--- class: 0
|--- petal width (cm) >  0.80
|   |--- petal length (cm) <= 4.95
|   |   |--- class: 1
|   |--- petal length (cm) >  4.95
|   |   |--- class: 2
```

Then, once build, we should be able to export the model as a DataFrame like this:

```py
tree.export_dataframe(
    decision_tree=clf,
    feature_names=iris.feature_names,
)
```

With the expected output to look like this:

```md
| node_id | parent_node_id | var               |  gini | samples | value        | decision                |
|---------|----------------|-------------------|-------|---------|--------------|-------------------------|
|       0 |           None | petal width (cm)  | 0.665 |     112 | [37, 34, 40] | None                    |
|       1 |              0 | <leaf>            |     0 |      37 | [37, 0, 0]   | <parent> value <= 0.8   |
|       2 |              0 | petal length (cm) | 0.496 |      75 | [0, 34, 41]  | <parent> value > 0.8    |
|       3 |              2 | <leaf>            | 0.153 |      36 | [0, 33, 3]   | <parent> value <= 0.495 |
|       4 |              2 | <leaf>            |  0.05 |      39 | [0, 1, 38]   | <parent> value > 0.495  |
```

**Regression Problem**

We can set up our model as follows:

```py
>>> import numpy as np
>>> from matplotlib import pyplot as plt

>>> from sklearn.model_selection import train_test_split
>>> from sklearn.datasets import load_diabetes
>>> from sklearn.tree import DecisionTreeRegressor
>>> from sklearn import tree

>>> diabetes = load_diabetes()
>>> X = diabetes.data
>>> y = diabetes.target
>>> X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)

>>> clf = DecisionTreeRegressor(max_leaf_nodes=3, random_state=0)
>>> clf.fit(X_train, y_train)
```

And when we can print the model as follows:

```py
>>> tree.plot_tree(
...     decision_tree=clf,
...     feature_names=diabetes.feature_names,
... )
```

![image](https://github.com/scikit-learn/scikit-learn/assets/44449504/829e623e-87dc-4dee-9c37-04f541d579d8)

And the text output:

```py
>>> print(
...     tree.export_text(
...         decision_tree=clf,
...         feature_names=diabetes.feature_names,
...     )
... )
|--- s5 <= 0.02
|   |--- bmi <= 0.01
|   |   |--- value: [103.13]
|   |--- bmi >  0.01
|   |   |--- value: [172.95]
|--- s5 >  0.02
|   |--- value: [217.29]
```

Then, once build, we should be able to export the model as a DataFrame like this:

```py
>>> tree.export_dataframe(
...     decision_tree=clf,
...     feature_names=diabetes.feature_names,
... )
```

With the expected output to look like this:

```text
| node_id | parent_node_id | var    | squared_error | samples | value   | decision                |
|---------|----------------|--------|---------------|---------|---------|-------------------------|
|       0 |           None | s5     |      6253.474 |     331 | 151.921 | None                    |
|       1 |              0 | bmi    |      4195.113 |     225 | 121.124 | <parent> value <= 0.022 |
|       2 |              0 | <leaf> |      4336.037 |     106 | 217.292 | <parent> value > 0.002  |
|       3 |              1 | <leaf> |      2523.068 |     167 | 103.126 | <parent> value <= 0.005 |
|       4 |              1 | <leaf> |       5390.98 |      58 | 172.948 | <parent> value > 0.005  |
```

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

Similar concepts have been proposed in the following issues:

- #20051
- #20428

However, no issue has yet been raised for the functionality to export the output from a Decision Tree as a pandas DataFrame.

### Additional context

**Firstly**, there is already some documentation in `sklearn` to 'traverse' a tree and compile it's output. See: [Understanding the decision tree structure][tree_structure].

**Secondly**, there's already some underlying data in the `plot_tree()` function that we can leverage. Because when I run the function like this:

```py
tree.plot_tree(
    decision_tree=clf,
    feature_names=diabetes.feature_names,
    rounded=True,
)
```
(the only thing that has changed here from the above is the inclusion of the `rounded` parameter).

The output looks like this:

```text
[Text(0.6, 0.8333333333333334, 's5 <= 0.022\nsquared_error = 6253.474\nsamples = 331\nvalue = 151.921'),
 Text(0.4, 0.5, 'bmi <= 0.005\nsquared_error = 4195.113\nsamples = 225\nvalue = 121.124'),
 Text(0.2, 0.16666666666666666, 'squared_error = 2523.068\nsamples = 167\nvalue = 103.126'),
 Text(0.6, 0.16666666666666666, 'squared_error = 5390.98\nsamples = 58\nvalue = 172.948'),
 Text(0.8, 0.5, 'squared_error = 4336.037\nsamples = 106\nvalue = 217.292')]
```

So, we may be able to leverage this information in the generation of a `pd.DataFrame` in the output for a new `export_dataframe()` function.

**Thirdly**, the inspiration of this functionality arose when I was using R's `rpart` function, and found that it had an internal attribute called `frame` which contained the data at each node in the Tree. You can find the documentation for this functionality here: [rdocumentation: rpart.object][rpart.object].

The process to generate this using R would be like this:

```r
> # Load packages
> library(rpart)

> # Load data
> data(iris)

> # Create model
> model <- rpart(Species ~ ., data=iris)
```

With the plot like this:

```r
> # Plot model
> rpart.plot(model)
```

![image](https://github.com/scikit-learn/scikit-learn/assets/44449504/43b4a64f-68cd-4f8a-b272-5c7e56b137f5)

And the internal frame like this:

```r
> # Check frame
> print(model$frame)
           var   n  wt dev yval complexity ncompete nsurrogate
1 Petal.Length 150 150 100    1       0.50        3          3 
2       <leaf>  50  50   0    1       0.01        0          0
3  Petal.Width 100 100  50    2       0.44        3          3
6       <leaf>  54  54   5    2       0.00        0          0
7       <leaf>  46  46   1    3       0.01        0          0
```

[tree_structure]: https://scikit-learn.org/stable/auto_examples/tree/plot_unveil_tree_structure.html
[rpart.object]: https://www.rdocumentation.org/packages/rpart/topics/rpart.object

COMMENT:
During the triage meeting, we discussed the inclusion of this proposal.

We think that exporting the tree structure in a machine-readable format would be quite useful. However, we cannot depend on `pandas`. So it would be better to implement an `export_dict` function that could be fed to `pd.DataFrame` constructor. So we would favor: https://github.com/scikit-learn/scikit-learn/issues/20051#issuecomment-1613428528

I am closing this issue as a duplicate of #20051. @chrimaho Feel free to submit a Pull-Request that implements the `export_dict` functionality. This PR could have an associated example to show how to export it using `pandas`/build a dataframe.

Labels: New Feature, Needs Triage