Загрузка данных


import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from itertools import product
from IPython.display import display

from sklearn.model_selection import train_test_split
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import OneHotEncoder
from sklearn.tree import (
    DecisionTreeClassifier,
    DecisionTreeRegressor,
    plot_tree,
)
from sklearn.metrics import (
    accuracy_score, precision_score, recall_score, f1_score,
    classification_report, ConfusionMatrixDisplay,
    mean_absolute_error, mean_squared_error, r2_score,
)

sns.set_theme(style="whitegrid")
pd.set_option("display.max_columns", 100)
pd.set_option("display.float_format", lambda v: f"{v:.4f}")

# ============================================================
# 0. Загрузка датасетов
# ============================================================
USE_SAVED_FILES = False  # True, если есть CSV из ЛР №2

from sklearn.datasets import fetch_openml

def load_dataset(name, version="active", fallback_names=None):
    """Пытается загрузить датасет по имени, при ошибке перебирает fallback."""
    candidates = [name] + (fallback_names or [])
    last_err = None
    for cand in candidates:
        try:
            print(f"Пробуем загрузить '{cand}'...")
            data = fetch_openml(name=cand, version=version, as_frame=True, parser="auto")
            df = data.frame.copy()
            target_columns = list(data.target_names)
            X = df.drop(columns=target_columns)
            y = df[target_columns[0]] if len(target_columns) == 1 else df[target_columns]
            print(f"  OK: {cand}, shape={df.shape}, target={target_columns}")
            return X, y, df, target_columns
        except Exception as e:
            print(f"  Не удалось: {cand} -> {type(e).__name__}: {e}")
            last_err = e
    raise RuntimeError(f"Не удалось загрузить ни один из {candidates}") from last_err

# --- Регрессия: Bike Sharing ---
if USE_SAVED_FILES:
    reg_df = pd.read_csv("regression_tree_dataset.csv")
    REG_TARGET = "cnt"   # замените на имя целевой переменной из ЛР №2
    X_reg = reg_df.drop(columns=[REG_TARGET])
    y_reg = reg_df[REG_TARGET].copy()
else:
    X_reg, y_reg, reg_df, reg_target_cols = load_dataset(
        "bike-sharing",
        fallback_names=["Bike_Sharing_Demand", "bike_sharing", "Bike-Sharing-Demand"],
    )
    REG_TARGET = reg_target_cols[0]
    # Целевая переменная — количество арендованных велосипедов.
    # В Bike Sharing обычно есть cnt (общее), casual + registered.
    # Оставляем cnt как target; casual/registered убираем из X, чтобы не было утечки.
    for leak in ["casual", "registered", "cnt", "count"]:
        if leak in X_reg.columns and leak != REG_TARGET:
            X_reg = X_reg.drop(columns=[leak])
    # Если target называется иначе, попробуем найти cnt
    if REG_TARGET not in y_reg.name and "cnt" in reg_df.columns:
        REG_TARGET = "cnt"
        y_reg = reg_df[REG_TARGET].copy()
        X_reg = reg_df.drop(columns=[c for c in ["cnt", "casual", "registered"] if c in reg_df.columns])

# --- Классификация: ILPD ---
if USE_SAVED_FILES:
    cls_df = pd.read_csv("classification_tree_dataset.csv")
    CLS_TARGET = "target"  # замените на имя целевой переменной из ЛР №2
    X_cls = cls_df.drop(columns=[CLS_TARGET])
    y_cls = cls_df[CLS_TARGET].copy()
else:
    X_cls, y_cls, cls_df, cls_target_cols = load_dataset(
        "ilpd",
        fallback_names=["ILPD", "Indian_Liver_Patient_Dataset", "liver-disorders", "liver_disorders"],
    )
    CLS_TARGET = cls_target_cols[0]

print("\n=== Регрессия: размер ===", reg_df.shape if 'reg_df' in dir() else X_reg.shape)
print("=== Классификация: размер ===", cls_df.shape if 'cls_df' in dir() else X_cls.shape)

# ============================================================
# ЧАСТЬ I. КЛАССИФИКАЦИЯ (ILPD)
# ============================================================
print("\n" + "="*60)
print("ЧАСТЬ I. КЛАССИФИКАЦИЯ — ILPD")
print("="*60)

CLS_DROP_COLUMNS = []  # при необходимости добавьте id-подобные столбцы

# Убираем строки без target
classification_df = pd.concat([X_cls, y_cls.rename(CLS_TARGET)], axis=1) \
    if CLS_TARGET not in X_cls.columns else cls_df.copy()
classification_df = classification_df.dropna(subset=[CLS_TARGET]).copy()

print("Размер датасета:", classification_df.shape)
display(classification_df.head())
display(classification_df.dtypes.rename("dtype").to_frame())

missing_report = pd.DataFrame({
    "missing_count": classification_df.isna().sum(),
    "missing_percent": (classification_df.isna().mean() * 100).round(2),
})
display(missing_report.sort_values("missing_count", ascending=False))

print("\nРаспределение классов:")
display(classification_df[CLS_TARGET].value_counts(dropna=False))
display(
    classification_df[CLS_TARGET]
    .value_counts(normalize=True, dropna=False)
    .mul(100).round(2)
)

plt.figure(figsize=(8, 4))
sns.countplot(data=classification_df, x=CLS_TARGET)
plt.title("Распределение классов (ILPD)")
plt.xlabel("Класс"); plt.ylabel("Количество объектов")
plt.tight_layout(); plt.show()

# --- X, y ---
X_cls = classification_df.drop(
    columns=[CLS_TARGET, *CLS_DROP_COLUMNS], errors="ignore"
)
y_cls = classification_df[CLS_TARGET].copy()

numeric_features_cls = X_cls.select_dtypes(include=np.number).columns.tolist()
categorical_features_cls = X_cls.select_dtypes(
    include=["object", "category", "bool"]
).columns.tolist()

print("Числовые признаки:", numeric_features_cls)
print("Категориальные признаки:", categorical_features_cls)

# --- Train / Val / Test ---
X_train_val_cls, X_test_cls, y_train_val_cls, y_test_cls = train_test_split(
    X_cls, y_cls, test_size=0.20, random_state=42, stratify=y_cls
)
X_train_cls, X_val_cls, y_train_cls, y_val_cls = train_test_split(
    X_train_val_cls, y_train_val_cls, test_size=0.25, random_state=42,
    stratify=y_train_val_cls
)
print("Train:", X_train_cls.shape)
print("Validation:", X_val_cls.shape)
print("Test:", X_test_cls.shape)

# --- Препроцессор ---
numeric_pipeline_cls = Pipeline([
    ("imputer", SimpleImputer(strategy="median")),
])
categorical_pipeline_cls = Pipeline([
    ("imputer", SimpleImputer(strategy="most_frequent")),
    ("encoder", OneHotEncoder(handle_unknown="ignore", sparse_output=False)),
])
preprocessor_cls = ColumnTransformer([
    ("numeric", numeric_pipeline_cls, numeric_features_cls),
    ("categorical", categorical_pipeline_cls, categorical_features_cls),
], verbose_feature_names_out=False)

X_train_cls_processed = preprocessor_cls.fit_transform(X_train_cls)
X_val_cls_processed   = preprocessor_cls.transform(X_val_cls)
X_test_cls_processed  = preprocessor_cls.transform(X_test_cls)
feature_names_cls     = preprocessor_cls.get_feature_names_out()

print("Признаков после обработки:", len(feature_names_cls))

# --- Базовое дерево ---
base_classifier = DecisionTreeClassifier(criterion="entropy", random_state=42)
base_classifier.fit(X_train_cls_processed, y_train_cls)

base_val_pred = base_classifier.predict(X_val_cls_processed)

print("Глубина:", base_classifier.get_depth())
print("Листьев:", base_classifier.get_n_leaves())
print("Узлов:", base_classifier.tree_.node_count)

print("\nМетрики на Validation:")
print("Accuracy:",  round(accuracy_score(y_val_cls, base_val_pred), 4))
print("Precision:", round(precision_score(y_val_cls, base_val_pred, average="weighted", zero_division=0), 4))
print("Recall:",    round(recall_score(y_val_cls, base_val_pred, average="weighted", zero_division=0), 4))
print("F1:",        round(f1_score(y_val_cls, base_val_pred, average="weighted", zero_division=0), 4))
print("\n", classification_report(y_val_cls, base_val_pred, zero_division=0))

ConfusionMatrixDisplay.from_predictions(y_val_cls, base_val_pred)
plt.title("Матрица ошибок (базовое дерево, ILPD)")
plt.tight_layout(); plt.show()

# --- Train vs Val ---
train_pred = base_classifier.predict(X_train_cls_processed)
train_accuracy = accuracy_score(y_train_cls, train_pred)
val_accuracy   = accuracy_score(y_val_cls, base_val_pred)
print("Accuracy Train:", round(train_accuracy, 4))
print("Accuracy Validation:", round(val_accuracy, 4))

# --- Визуализация ---
plt.figure(figsize=(26, 14))
plot_tree(
    base_classifier,
    feature_names=feature_names_cls,
    class_names=[str(c) for c in base_classifier.classes_],
    filled=True, rounded=True, max_depth=3, fontsize=8,
)
plt.title("Базовое дерево классификации (ILPD, первые 3 уровня)")
plt.savefig("classification_tree_base.png", dpi=150, bbox_inches="tight")
plt.show()

# --- Перебор гиперпараметров ---
max_depth_values = [1, 2, 3, 4, 5, 7, 10, None]
min_leaf_values  = [1, 2, 5, 10, 20]
min_split_values = [2, 5, 10, 20]

classification_results = []
for max_depth, min_leaf, min_split in product(
    max_depth_values, min_leaf_values, min_split_values
):
    model = DecisionTreeClassifier(
        criterion="entropy",
        max_depth=max_depth,
        min_samples_leaf=min_leaf,
        min_samples_split=min_split,
        random_state=42,
    )
    model.fit(X_train_cls_processed, y_train_cls)
    tr_pred = model.predict(X_train_cls_processed)
    va_pred = model.predict(X_val_cls_processed)

    classification_results.append({
        "max_depth": max_depth,
        "min_samples_leaf": min_leaf,
        "min_samples_split": min_split,
        "train_accuracy": accuracy_score(y_train_cls, tr_pred),
        "val_accuracy": accuracy_score(y_val_cls, va_pred),
        "train_f1": f1_score(y_train_cls, tr_pred, average="weighted", zero_division=0),
        "val_f1":   f1_score(y_val_cls, va_pred, average="weighted", zero_division=0),
        "actual_depth": model.get_depth(),
        "leaves": model.get_n_leaves(),
        "nodes": model.tree_.node_count,
    })

classification_results = pd.DataFrame(classification_results)
display(classification_results.head())

classification_results_sorted = classification_results \
    .sort_values("val_f1", ascending=False).reset_index(drop=True)
print("\nТОП-10 по val_f1:")
display(classification_results_sorted.head(10))
print("ХУДШИЕ-10 по val_f1:")
display(classification_results_sorted.tail(10))

classification_results["overfit_gap"] = (
    classification_results["train_f1"] - classification_results["val_f1"]
)
print("\nНаибольший разрыв train-val (переобучение):")
display(
    classification_results
    .sort_values("overfit_gap", ascending=False)
    .head(10)
)

# --- Влияние глубины ---
depth_summary_cls = (
    classification_results
    .groupby("actual_depth", as_index=False)
    .agg(train_f1=("train_f1", "mean"), val_f1=("val_f1", "mean"))
)
plt.figure(figsize=(9, 5))
plt.plot(depth_summary_cls["actual_depth"], depth_summary_cls["train_f1"], marker="o", label="Train")
plt.plot(depth_summary_cls["actual_depth"], depth_summary_cls["val_f1"],  marker="o", label="Validation")
plt.xlabel("Фактическая глубина дерева"); plt.ylabel("Средний F1-score")
plt.title("Качество в зависимости от глубины (ILPD)")
plt.legend(); plt.tight_layout(); plt.show()

# --- Лучшее и худшее дерево ---
def restore_max_depth(v):
    return None if pd.isna(v) else int(v)

best_cls_row  = classification_results.sort_values("val_f1", ascending=False).iloc[0]
worst_cls_row = classification_results.sort_values("val_f1", ascending=True).iloc[0]
print("\nЛУЧШЕЕ дерево (ILPD):")
display(best_cls_row.to_frame("value"))
print("НАИХУДШЕЕ дерево (ILPD):")
display(worst_cls_row.to_frame("value"))

best_classifier = DecisionTreeClassifier(
    criterion="entropy",
    max_depth=restore_max_depth(best_cls_row["max_depth"]),
    min_samples_leaf=int(best_cls_row["min_samples_leaf"]),
    min_samples_split=int(best_cls_row["min_samples_split"]),
    random_state=42,
)
best_classifier.fit(X_train_cls_processed, y_train_cls)

worst_classifier = DecisionTreeClassifier(
    criterion="entropy",
    max_depth=restore_max_depth(worst_cls_row["max_depth"]),
    min_samples_leaf=int(worst_cls_row["min_samples_leaf"]),
    min_samples_split=int(worst_cls_row["min_samples_split"]),
    random_state=42,
)
worst_classifier.fit(X_train_cls_processed, y_train_cls)

best_test_pred  = best_classifier.predict(X_test_cls_processed)
worst_test_pred = worst_classifier.predict(X_test_cls_processed)

final_cls_comparison = pd.DataFrame([
    {
        "model": "Лучшее дерево",
        "accuracy": accuracy_score(y_test_cls, best_test_pred),
        "f1": f1_score(y_test_cls, best_test_pred, average="weighted", zero_division=0),
        "depth": best_classifier.get_depth(),
        "leaves": best_classifier.get_n_leaves(),
    },
    {
        "model": "Наихудшее дерево",
        "accuracy": accuracy_score(y_test_cls, worst_test_pred),
        "f1": f1_score(y_test_cls, worst_test_pred, average="weighted", zero_division=0),
        "depth": worst_classifier.get_depth(),
        "leaves": worst_classifier.get_n_leaves(),
    },
])
print("\nСравнение на TEST (ILPD):")
display(final_cls_comparison)

# Визуализация лучшего/худшего
for name, model, fname in [
    ("Лучшее дерево классификации", best_classifier,  "classification_tree_best.png"),
    ("Наихудшее дерево классификации", worst_classifier, "classification_tree_worst.png"),
]:
    plt.figure(figsize=(28, 15))
    plot_tree(
        model, feature_names=feature_names_cls,
        class_names=[str(c) for c in model.classes_],
        filled=True, rounded=True, max_depth=4, fontsize=8,
    )
    plt.title(f"{name} (ILPD, первые 4 уровня)")
    plt.savefig(fname, dpi=150, bbox_inches="tight")
    plt.show()

# ============================================================
# ЧАСТЬ II. РЕГРЕССИЯ (Bike Sharing)
# ============================================================
print("\n" + "="*60)
print("ЧАСТЬ II. РЕГРЕССИЯ — Bike Sharing")
print("="*60)

REGRESSION_DROP_COLUMNS = []  # при необходимости

if USE_SAVED_FILES:
    regression_df = pd.read_csv("regression_tree_dataset.csv")
else:
    regression_df = pd.concat([X_reg, y_reg.rename(REG_TARGET)], axis=1) \
        if REG_TARGET not in X_reg.columns else reg_df.copy()

regression_df = regression_df.dropna(subset=[REG_TARGET]).copy()

print("Размер датасета:", regression_df.shape)
display(regression_df.head())
display(regression_df.dtypes.rename("dtype").to_frame())

reg_missing_report = pd.DataFrame({
    "missing_count": regression_df.isna().sum(),
    "missing_percent": (regression_df.isna().mean() * 100).round(2),
})
display(reg_missing_report.sort_values("missing_count", ascending=False))

print("\nОписание target:")
display(regression_df[REG_TARGET].describe())

plt.figure(figsize=(9, 5))
sns.histplot(data=regression_df, x=REG_TARGET, kde=True)
plt.title("Распределение целевой переменной (Bike Sharing)")
plt.tight_layout(); plt.show()

# --- X, y ---
X_reg = regression_df.drop(columns=[REG_TARGET, *REGRESSION_DROP_COLUMNS], errors="ignore")
y_reg = regression_df[REG_TARGET].copy()

numeric_features_reg = X_reg.select_dtypes(include=np.number).columns.tolist()
categorical_features_reg = X_reg.select_dtypes(
    include=["object", "category", "bool"]
).columns.tolist()

print("Числовые признаки:", numeric_features_reg)
print("Категориальные признаки:", categorical_features_reg)

# --- Train / Val / Test ---
X_train_val_reg, X_test_reg, y_train_val_reg, y_test_reg = train_test_split(
    X_reg, y_reg, test_size=0.20, random_state=42
)
X_train_reg, X_val_reg, y_train_reg, y_val_reg = train_test_split(
    X_train_val_reg, y_train_val_reg, test_size=0.25, random_state=42
)
print("Train:", X_train_reg.shape)
print("Validation:", X_val_reg.shape)
print("Test:", X_test_reg.shape)

# --- Препроцессор ---
numeric_pipeline_reg = Pipeline([
    ("imputer", SimpleImputer(strategy="median")),
])
categorical_pipeline_reg = Pipeline([
    ("imputer", SimpleImputer(strategy="most_frequent")),
    ("encoder", OneHotEncoder(handle_unknown="ignore", sparse_output=False)),
])
preprocessor_reg = ColumnTransformer([
    ("numeric", numeric_pipeline_reg, numeric_features_reg),
    ("categorical", categorical_pipeline_reg, categorical_features_reg),
], verbose_feature_names_out=False)

X_train_reg_processed = preprocessor_reg.fit_transform(X_train_reg)
X_val_reg_processed   = preprocessor_reg.transform(X_val_reg)
X_test_reg_processed  = preprocessor_reg.transform(X_test_reg)
feature_names_reg     = preprocessor_reg.get_feature_names_out()

# --- Базовое дерево ---
base_regressor = DecisionTreeRegressor(criterion="squared_error", random_state=42)
base_regressor.fit(X_train_reg_processed, y_train_reg)
base_val_pred_reg = base_regressor.predict(X_val_reg_processed)

print("Глубина:", base_regressor.get_depth())
print("Листьев:", base_regressor.get_n_leaves())

base_reg_metrics = {
    "MAE":  mean_absolute_error(y_val_reg, base_val_pred_reg),
    "RMSE": np.sqrt(mean_squared_error(y_val_reg, base_val_pred_reg)),
    "R2":   r2_score(y_val_reg, base_val_pred_reg),
}
print("\nМетрики базового дерева (Validation):")
display(pd.DataFrame([base_reg_metrics]))

train_pred_reg = base_regressor.predict(X_train_reg_processed)
train_rmse = np.sqrt(mean_squared_error(y_train_reg, train_pred_reg))
val_rmse   = np.sqrt(mean_squared_error(y_val_reg, base_val_pred_reg))
print("Train RMSE:", round(train_rmse, 4))
print("Validation RMSE:", round(val_rmse, 4))

plt.figure(figsize=(7, 7))
plt.scatter(y_val_reg, base_val_pred_reg, alpha=0.6)
mn = min(y_val_reg.min(), base_val_pred_reg.min())
mx = max(y_val_reg.max(), base_val_pred_reg.max())
plt.plot([mn, mx], [mn, mx], linestyle="--")
plt.xlabel("Фактическое"); plt.ylabel("Предсказанное")
plt.title("Фактические и предсказанные (Bike Sharing)")
plt.tight_layout(); plt.show()

# --- Визуализация базового дерева регрессии ---
plt.figure(figsize=(28, 15))
plot_tree(
    base_regressor, feature_names=feature_names_reg,
    filled=True, rounded=True, max_depth=3, fontsize=8,
)
plt.title("Базовое дерево регрессии (Bike Sharing, первые 3 уровня)")
plt.savefig("regression_tree_base.png", dpi=150, bbox_inches="tight")
plt.show()

# --- Перебор гиперпараметров ---
regression_results = []
for max_depth, min_leaf, min_split in product(
    max_depth_values, min_leaf_values, min_split_values
):
    model = DecisionTreeRegressor(
        criterion="squared_error",
        max_depth=max_depth,
        min_samples_leaf=min_leaf,
        min_samples_split=min_split,
        random_state=42,
    )
    model.fit(X_train_reg_processed, y_train_reg)
    tr_pred = model.predict(X_train_reg_processed)
    va_pred = model.predict(X_val_reg_processed)

    regression_results.append({
        "max_depth": max_depth,
        "min_samples_leaf": min_leaf,
        "min_samples_split": min_split,
        "train_rmse": np.sqrt(mean_squared_error(y_train_reg, tr_pred)),
        "val_rmse":   np.sqrt(mean_squared_error(y_val_reg, va_pred)),
        "val_mae":    mean_absolute_error(y_val_reg, va_pred),
        "val_r2":     r2_score(y_val_reg, va_pred),
        "actual_depth": model.get_depth(),
        "leaves": model.get_n_leaves(),
        "nodes": model.tree_.node_count,
    })

regression_results = pd.DataFrame(regression_results)
display(regression_results.head())

regression_results_sorted = regression_results \
    .sort_values("val_rmse", ascending=True).reset_index(drop=True)
print("\nТОП-10 по val_rmse:")
display(regression_results_sorted.head(10))
print("ХУДШИЕ-10 по val_rmse:")
display(regression_results_sorted.tail(10))

regression_results["overfit_gap"] = (
    regression_results["val_rmse"] - regression_results["train_rmse"]
)
print("\nНаибольший разрыв train-val RMSE (переобучение):")
display(
    regression_results
    .sort_values("overfit_gap", ascending=False)
    .head(10)
)

depth_summary_reg = (
    regression_results
    .groupby("actual_depth", as_index=False)
    .agg(train_rmse=("train_rmse", "mean"), val_rmse=("val_rmse", "mean"))
)
plt.figure(figsize=(9, 5))
plt.plot(depth_summary_reg["actual_depth"], depth_summary_reg["train_rmse"], marker="o", label="Train")
plt.plot(depth_summary_reg["actual_depth"], depth_summary_reg["val_rmse"],  marker="o", label="Validation")
plt.xlabel("Фактическая глубина дерева"); plt.ylabel("Средний RMSE")
plt.title("Ошибка в зависимости от глубины (Bike Sharing)")
plt.legend(); plt.tight_layout(); plt.show()

# --- Лучшее и худшее дерево регрессии ---
best_reg_row  = regression_results.sort_values("val_rmse", ascending=True).iloc[0]
worst_reg_row = regression_results.sort_values("val_rmse", ascending=False).iloc[0]
print("\nЛУЧШЕЕ дерево (Bike Sharing):")
display(best_reg_row.to_frame("value"))
print("НАИХУДШЕЕ дерево (Bike Sharing):")
display(worst_reg_row.to_frame("value"))

best_regressor = DecisionTreeRegressor(
    criterion="squared_error",
    max_depth=restore_max_depth(best_reg_row["max_depth"]),
    min_samples_leaf=int(best_reg_row["min_samples_leaf"]),
    min_samples_split=int(best_reg_row["min_samples_split"]),
    random_state=42,
)
best_regressor.fit(X_train_reg_processed, y_train_reg)

worst_regressor = DecisionTreeRegressor(
    criterion="squared_error",
    max_depth=restore_max_depth(worst_reg_row["max_depth"]),
    min_samples_leaf=int(worst_reg_row["min_samples_leaf"]),
    min_samples_split=int(worst_reg_row["min_samples_split"]),
    random_state=42,
)
worst_regressor.fit(X_train_reg_processed, y_train_reg)

best_test_pred_reg  = best_regressor.predict(X_test_reg_processed)
worst_test_pred_reg = worst_regressor.predict(X_test_reg_processed)

final_reg_comparison = pd.DataFrame([
    {
        "model": "Лучшее дерево",
        "MAE":  mean_absolute_error(y_test_reg, best_test_pred_reg),
        "RMSE": np.sqrt(mean_squared_error(y_test_reg, best_test_pred_reg)),
        "R2":   r2_score(y_test_reg, best_test_pred_reg),
        "depth": best_regressor.get_depth(),
        "leaves": best_regressor.get_n_leaves(),
    },
    {
        "model": "Наихудшее дерево",
        "MAE":  mean_absolute_error(y_test_reg, worst_test_pred_reg),
        "RMSE": np.sqrt(mean_squared_error(y_test_reg, worst_test_pred_reg)),
        "R2":   r2_score(y_test_reg, worst_test_pred_reg),
        "depth": worst_regressor.get_depth(),
        "leaves": worst_regressor.get_n_leaves(),
    },
])
print("\nСравнение на TEST (Bike Sharing):")
display(final_reg_comparison)

for name, model, fname in [
    ("Лучшее дерево регрессии", best_regressor,  "regression_tree_best.png"),
    ("Наихудшее дерево регрессии", worst_regressor, "regression_tree_worst.png"),
]:
    plt.figure(figsize=(28, 15))
    plot_tree(
        model, feature_names=feature_names_reg,
        filled=True, rounded=True, max_depth=4, fontsize=8,
    )
    plt.title(f"{name} (Bike Sharing, первые 4 уровня)")
    plt.savefig(fname, dpi=150, bbox_inches="tight")
    plt.show()