Метрики качества оценки вероятностей в бинарной классификации: опыт из ФинТеха

в 6:20, , рубрики: calibration, Hosmer-Lemeshow, Log Loss, ml, pr auc, roc auc, бинарная классификация, Качество классификации, классификация, Оценка вероятностей

Бинарная классификация — одна из самых распространённых задач машинного обучения, встречающаяся во множестве прикладных областей.

Однако, на практике цель таких задач часто выходит за рамки простого предсказания класса. Гораздо более важным оказывается умение модели оценить вероятность принадлежности объекта к тому или иному классу. Иными словами, нас интересует не только, какой класс выбрать, но и с какой вероятностью это решение принято.

Такие задачи встречаются часто. Например, в кредитном скоринге существует задача оценки вероятности дефолта клиента: предсказанию того, что клиент перестанет платить по кредиту. Банки используют такие модели, чтобы на основе рассчитанных вероятностей дефолта принимать решения — выдавать ли кредит, и если да, то на каких условиях. Здесь правильная оценка вероятностей становится ключевым фактором, влияющим на финансовые результаты.

Но как понять, насколько точны прогнозы модели? Обычные метрики, такие как точность, полнота или F-мера, не подходят для подобных задач. Нужны специализированные инструменты, которые позволят оценить качество предсказания вероятностей.

В этой статье я поделюсь практическим опытом оценки вероятностных предсказаний, расскажу о ключевых метриках, применяемых в работе, и объясню, как их интерпретировать и для каких целей они подходят.

Задача бинарной классификации формальным языком

Пусть у нас имеется выборка с l наблюдениями: X={(x_i, y_j)}_{i=1}^{l} , где x_i ∈R^nвектор признаков i-го объекта, n — количество признаков, y_j in { 0, 1 } - множество допустимых ответов.

И пускай мы обучили модель бинарной классификации a(x_i)→p(y_i=1|x_i), которая предсказывает p_i- вероятность того, что y_j=1для объекта x_i.

Оценка качества предсказания вероятностей

Давайте попробуем оценить качество предсказания вероятностей такого классификатора. Какими свойствами должна обладать идеально предсказанная вероятность?

Во-первых, вероятности должны качественно сортировать объекты по степени их принадлежности к классу. Это означает, что объект, обладающий признаками класса "1", должен иметь более высокую вероятность принадлежности к этому классу, чем объект, у которого таких признаков нет.

Во-вторых, вероятности должны быть откалиброванными, то есть соответствовать истинной частоте событий. Калибровка вероятностей предполагает, что предсказания модели отражают реальную вероятность события. Например, если модель предсказывает вероятность 0.8 для группы объектов, то в 80% случаев эти объекты действительно должны принадлежать к положительному классу. Таким образом, откалиброванная модель не только ранжирует объекты, но и дает осмысленные, интерпретируемые предсказания вероятностей.

Log Loss (Logarithmic Loss)

Оценивает, насколько сильно предсказанные вероятности p_i отличаются от истинных ответов y_i. Метрика вычисляется таким образом:

text{Log Loss}=-frac{1}{l} sum_{i=1}^l left(y_i log(p_i) + (1 - y_i) log(1 - p_i) right)

Чем ниже Log Loss, тем лучше модель предсказывает вероятности: от 0 (идеальные предсказания) до бесконечности (уверенные, но неверные предсказания). Метрика минимальна, когда для однозначных объектов модель предсказывает вероятность близкую к 1 для правильного класса и к 0 для остальных, а для объектов, обладающих свойствами обоих классов, вероятность отражает их неопределённость, например, ближе к 0.5.

Log Loss хорошо коррелирует с другими метриками оценки вероятности, но чувствителен к выбросам и сложен для интерпретации: например, значение Log Loss равное 0.8 не всегда можно однозначно назвать "хорошим" или "плохим".

Применение: Log Loss подходит для сравнения моделей между собой (выбирается модель с меньшим значением), но мало полезен для оценки абсолютного качества предсказаний отдельной модели.

from sklearn.metrics import log_loss
import numpy as np

# Пример истинных меток классов (0 или 1)
y_true = [0, 1, 1, 0, 1]
# Пример предсказанных вероятностей принадлежности к классу 1
y_pred_proba = [0.1, 0.9, 0.8, 0.3, 0.6]

# Расчет Log Loss
logloss = log_loss(y_true, y_pred_proba)

print(f"Log Loss: {logloss}")

ROC-сurve и ROC-AUC

Пожалуй одна из наиболее популярных метрик это ROC-AUC.

ROC-Curve (Receiver Operating Characteristic curve) — это график, координата X которого соответствует доле неверно принятых объектов FPR (False Positive Rate), а координата Y — доле верно принятых объектов TPR (True Positive Rate).

text{TPR}=frac{text{TP}}{text{TP} + text{FN}} text{; }  text{FPR}=frac{text{FP}}{text{FP} + text{TN}}

  • True Positives (TP): количество объектов, правильно классифицированных как класс "1".

  • False Positives (FP): количество объектов, ошибочно классифицированных как класс "1".

  • False Negatives (FN): количество объектов, ошибочно классифицированных как класс "0".

  • True Negatives (TN): количество объектов, правильно классифицированных как класс "0".

Метрики качества оценки вероятностей в бинарной классификации: опыт из ФинТеха - 15

Как построить ROC-сurve?

  1. Сортируем объекты
    Расставляем объекты в порядке убывания вероятности класса "1", предсказанной моделью.

  2. Начинаем с нуля
    Считаем, что все объекты — это класс "0", и рассчитываем TPR и FPR.

  3. Добавляем объекты по очереди Берем первый объект (с самой высокой вероятностью класса "1") и переносим его в класс "1". Считаем новые значения TPR и FPR. Повторяем это для следующего объекта, и так далее, пока все не окажутся в классе "1".

  4. Строим кривую
    Каждый раз, когда добавляем объект, записываем пару значений TPR и FPR и отмечаем её на графике.

Рок-кривая всегда проходит через точки (0,0) и (1,1), первую она проходит когда все объекты относятся к классу "0", а вторую когда все отнесены отнесены к классу "1".

Если кривая ближе к верхнему левому углу, значит, модель работает хорошо: она правильно разделяет классы, почти не ошибаясь. Если предсказания модели случайны, кривая будет идти по диагонали от (0,0) до (1,1). А если модель чаще путает классы (например, называет "0" классом "1" и наоборот), то кривая будет ближе к правому нижнему углу. Что-бы на основании этого построить числовую метрику, считают площадь про рок-кривой.

ROC-AUC (Area Under the ROC-Curve) — это площадь под ROC-кривой.

Чем больше эта площадь, тем лучше наша модель. ROC-AUC может принимать значения между "0" и "1".

  • ROC-AUC = 1: идеальная модель.

  • ROC-AUC = 0,5: случайные предсказания - модель не нашла закономерностей и просто угадывает.

  • ROC-AUC = 0: модель предсказывает классы наоборот.

ROC-AUC оценивает способность модели правильно сортировать объекты по вероятности принадлежности к классу, но не оценивает калибровку вероятностей - соответствие прогнозируемой вероятности истинной частоте событий. Например, если умножить все вероятности на 1000, они перестанут быть вероятностями, но ROC-AUC останется неизменной, так как умножение всех вероятностей на одно число не изменит порядок объектов.

При сильном нарушении баланса в размерах классов (например размер класса "1" всего несколько процентов от выборки) ROC-AUC может завышать качество модели, так как редкие ложноположительные срабатывания мало влияют на итоговую оценку.

Применение: ROC-AUC используется для оценки качества сортировки объектов по принадлежности к классу, не используется для оценки качества калибровки предсказанных вероятностей.

import numpy as np
from sklearn.metrics import roc_curve, roc_auc_score
import matplotlib.pyplot as plt

# Пример истинных меток классов
y_true = [0, 0, 1, 1, 0, 1, 1, 0, 1, 0]
# Пример предсказанных вероятностей принадлежности к классу 1
y_pred_proba = [0.1, 0.4, 0.35, 0.8, 0.2, 0.7, 0.6, 0.3, 0.9, 0.5]

# Построение ROC-кривой
fpr, tpr, thresholds = roc_curve(y_true, y_pred_proba)

# Вычисление AUC
roc_auc = roc_auc_score(y_true, y_pred_proba)

# Построение графика
plt.figure(figsize=(8, 6))
plt.plot(fpr, tpr, label=f'ROC curve (AUC = {roc_auc:.2f})')
plt.plot([0, 1], [0, 1], 'k--', label='Random guess')
plt.xlabel('False Positive Rate (FPR)')
plt.ylabel('True Positive Rate (TPR)')
plt.title('ROC Curve')
plt.legend(loc='lower right')
plt.grid()
plt.show()

PR-Curve и PR-AUC

PR-Curve (Precision-Recall Curve) — это график, который отображает зависимость точности (Precision) от полноты (Recall) для различных пороговых значений классификации.

  • Precision (точность) — это доля истинно положительных примеров среди всех, классифицированных как положительные.

    text{Precision}=frac{TP}{TP + FP}

  • Recall (полнота) — это доля истинно положительных примеров среди всех положительных примеров.

    {Recall}=frac{TP}{TP + FN}

  • True Positives (TP): количество объектов, правильно классифицированных как класс "1".

  • False Positives (FP): количество объектов, ошибочно классифицированных как класс "1".

  • False Negatives (FN): количество объектов, ошибочно классифицированных как класс "0".

Метрики качества оценки вероятностей в бинарной классификации: опыт из ФинТеха - 18

Как построить PR-сurve?

  1. Сортируем объекты
    Расставляем объекты в порядке убывания вероятности класса "1", предсказанной моделью.

  2. Начинаем с нуля
    Считаем, что все объекты — это класс "0", и рассчитываем Precision и Recall.

  3. Добавляем объекты по очереди Берем первый объект (с самой высокой вероятностью класса 1) и переносим его в класс "1". Считаем новые значения рассчитываем Precision и Recall. Повторяем это для следующего объекта, и так далее, пока все не окажутся в классе "1".

  4. Строим кривую
    Каждый раз, когда добавляем объект, записываем пару значений Precision и Recall и отмечаем её на графике.

Если кривая ближе к верхнему правому углу, это означает, что модель работает хорошо: она правильно определяет положительные классы, почти не ошибаясь. Если модель путает классы (например, называет класс "0" классом "1" и наоборот), то кривая будет ближе к диагонали (случайным предсказаниям). Для случайных предсказаний форма кривой будет зависеть от баланса классов в выборке. Чтобы на основании кривой получить числовую метрику, рассчитывают площадь под PR-кривой (Precision-Recall AUC).

PR-AUC (Area Under the PR-Curve) — это площадь под PR-кривой.

PR-AUC может принимать значения между 0 и 1. Чем больше эта площадь, тем лучше наша модель.

  • PR-AUC= 1: идеальная модель.

  • Если PR-AUC низкая, это свидетельствует о слабой способности модели выявлять класс "1".

Можно увидеть заметное сходство с ROC-AUC: PR-Curve также характеризует качество сортировки объектов относительно принадлежности к классу, но фокусируется на одном классе, игнорируя второй. PR-Curve следует использовать, если:

  • Один класс встречается значительно реже второго, PR-AUC лучше подчеркивает качество классификатора на редком классе.

  • Важнее правильно предсказывать один из классов, а не балансировать оба класса.

Применение: PR-AUC используется для оценки качества сортировки объектов по принадлежности к классу, когда один класс встречается значительно реже второго или нужно сконцентрироваться на одном из классов. Не используется для оценки качества предсказанных вероятностей.

import numpy as np
from sklearn.metrics import precision_score, recall_score, precision_recall_curve, auc
import matplotlib.pyplot as plt

# Пример данных
y_true = np.array([0, 1, 1, 0, 1, 0, 1, 0, 0, 1])  # Истинные метки (0 или 1)
y_scores = np.array([0.1, 0.9, 0.8, 0.3, 0.6, 0.4, 0.7, 0.2, 0.1, 0.85])  # Предсказанные вероятности для положительного класса

# 1. Вычисление Precision и Recall для фиксированного порога
threshold = 0.5  # Пример порога
y_pred = (y_scores >= threshold).astype(int)  # Прогнозы на основе порога

precision = precision_score(y_true, y_pred)
recall = recall_score(y_true, y_pred)

print(f"Precision (при пороге {threshold}): {precision:.2f}")
print(f"Recall (при пороге {threshold}): {recall:.2f}")

# 2. Построение PR-кривой
precision_vals, recall_vals, thresholds = precision_recall_curve(y_true, y_scores)

# Вычисление PR-AUC
pr_auc = auc(recall_vals, precision_vals)

print(f"PR-AUC: {pr_auc:.2f}")

# 3. Построение графика PR-кривой
plt.figure(figsize=(8, 6))
plt.plot(recall_vals, precision_vals, label=f'PR Curve (AUC = {pr_auc:.2f})', linewidth=2)
plt.xlabel('Recall', fontsize=12)
plt.ylabel('Precision', fontsize=12)
plt.title('Precision-Recall Curve', fontsize=14)
plt.legend(loc='best')
plt.grid(True)
plt.show()

Calibration Curve и Expected Calibration Error (ECE)

Reliability Diagram (Calibration Curve) и Expected Calibration Error (ECE) — это инструменты для оценки калибровки вероятностных моделей. Они используются для анализа того, насколько хорошо модель предсказывает вероятности исходов.

Calibration Curve - это график, который отображает связь между предсказанными вероятностями и фактической частотой успехов.

Как построить Calibration Curve?

  1. Сортируем объекты
    Расставляем объекты в порядке возрастания вероятности класса "1", предсказанной моделью.

  2. Считаем среднюю вероятность и частотуДля каждого бина рассчитывается:

  3. Строим кривую
    Значения P_g и E_g наносятся на график.

Метрики качества оценки вероятностей в бинарной классификации: опыт из ФинТеха - 21

В идеальной модели все точки Calibration Curve лежат на диагонали y=x. Это значит, что если модель предсказывает вероятность 0.7, то в 70% случаев событие действительно происходит. Если участки кривой находятся выше диагонали (как на графике выше), это указывает на то, что модель занижает реальную вероятность событий. И наоборот, если участки кривой находятся ниже диагонали, модель завышает вероятность событий, то есть она переоценивает свою уверенность.

Калибровочная кривая визуализирует насколько успешно модель справляется с предсказаниями вероятностей для разных диапазонов. По ней можно определить для каких групп объектов модель ошибается больше всего и в какую сторону. Например, на графике выше видно, что модель сильно занижает вероятности для объектов, которые она с большей вероятностью классифицирует как класс "1". Это означает, что она относит их к классу "1", но при этом недостаточно уверена в своих предсказаниях.

Калибровочная кривая не позволяет судить о том, насколько хорошо модель сортирует объекты по их принадлежности к классам. Поэтому её следует использовать вместе с ROC-кривой или PR-кривой.

Еще одним важным недостатком калибровочной кривой является принцип её построения. Поскольку алгоритм предполагает разбиение объектов на бины по значениям предсказанных вероятностей, может возникнуть ситуация, когда отдельные точки кривой будут основаны на малом числе объектов. Это снижает статистическую значимость оценок предсказанных и эмпирических вероятностей, делая такие участки кривой менее надёжными.

На основании точек калибровочной кривой можно посчитать метрику ECE.

Expected Calibration Error (ECE) — измеряет, насколько предсказания модели отличаются от реальных вероятностей. Это скалярное значение, которое агрегирует разницу между предсказанной вероятностью и фактической частотой успехов.

ECE=∑_{i-1}^B B_i/l​ *|P_g−E_g|​

где: B — количество бинов, B_i — число предсказаний в бине i, l — общее количество наблюдений.

Чем ниже ECE, тем лучше откалибрована модель. Метрика может принимать значения между "0" (для идеальной модели) и "1" (для очень плохой модели).

Применение: Calibration Curve используется для оценки калибровки вероятностных моделей. Они используются для анализа того, насколько хорошо модель предсказывает вероятности исходов. Так как метрика не чувствительна к качеству сортировке объектов, ее следует использовать вместе ROC-кривой или PR-кривой.

import numpy as np
import matplotlib.pyplot as plt
from sklearn.calibration import calibration_curve

# Пример данных (замените на ваши данные)
y_true = np.random.randint(0, 2, size=1000)  # Истинные метки (0 или 1)
y_pred = np.random.rand(1000)  # Предсказанные вероятности

# Число бинов для группировки
n_bins = 10

# Построение Reliability Diagram
prob_true, prob_pred = calibration_curve(y_true, y_pred, n_bins=n_bins, strategy='uniform')

# График Reliability Diagram
plt.figure(figsize=(8, 6))
plt.plot(prob_pred, prob_true, marker='o', label='Calibration Curve')
plt.plot([0, 1], [0, 1], linestyle='--', color='gray', label='Perfect Calibration')
plt.title('Reliability Diagram (Calibration Curve)')
plt.xlabel('Mean Predicted Probability')
plt.ylabel('Fraction of Positives')
plt.legend()
plt.grid()
plt.show()

# Вычисление Expected Calibration Error (ECE)
def compute_ece(y_true, y_pred, n_bins=10):
    """Вычисляет Expected Calibration Error (ECE)"""
    bins = np.linspace(0, 1, n_bins + 1)
    bin_indices = np.digitize(y_pred, bins, right=True)
    ece = 0.0

    for i in range(1, n_bins + 1):
        bin_mask = bin_indices == i
        bin_size = bin_mask.sum()
        if bin_size > 0:
            bin_confidence = y_pred[bin_mask].mean()
            bin_accuracy = y_true[bin_mask].mean()
            ece += (bin_size / len(y_true)) * abs(bin_accuracy - bin_confidence)
    return ece

# Рассчет ECE
ece = compute_ece(y_true, y_pred, n_bins=n_bins)
print(f"Expected Calibration Error (ECE): {ece:.4f}")

Кривые и статистика Hosmer-Lemeshow

Кривые и статистика Hosmer-Lemeshow — это инструменты для оценки калибровки вероятностных моделей и визуализации предсказания вероятностей.

Данные инструменты не часто встречаются в статьях или литературе и могут в разных источниках иметь разные названия. В своей практике графики мы называли Gain Chart, а статистику Hosmer-Lemeshow test. Однако этот инструмент показал свою эффективность на практике и является чуть ли не самым информативным с точки зрения визуализации качества предсказанной вероятности.

Как построить кривые Hosmer-Lemeshow?

  1. Сортируем объекты
    Расставляем объекты в порядке возрастания вероятности класса "1", предсказанной моделью.

  2. Делим на бины
    Объекты выборки делятся на 10 бинов равного размера.

  3. Считаем среднюю вероятность и частотуДля каждого бина рассчитывается:

    • Cредняя предсказанная вероятность: P_g=frac{1}{l} sum_{i=1}^l left(p_iright).

    • Доля истинных положительных результатов (эмпирическая вероятность): E_g=p_g/(p_g+n_g), где p_gколичество объектов класса "1" , а n_gколичество объектов класса "0" в бине g.

  4. Строим кривые
    Строятся два графика: зависимость P_gот номера бина и зависимость E_gот номера бина.

Метрики качества оценки вероятностей в бинарной классификации: опыт из ФинТеха - 33

Так как все бины получаются равного размера, здесь не возникает проблемы со статистической значимостью вычисляемых E_g​ и P_g​.

В данном случае анализ строится исходя из того, насколько кривые средней предсказанной вероятности и доли наблюдаемых событий соотносятся друг с другом. Как понять, что полученная модель хорошая:

  1. Кривая E_g(доля наблюдаемых событий) возрастает. Это значит, что прогнозируемая вероятность хорошо сортирует объекты по принадлежности к классу.

  2. Кривая E_g выгнута вниз, и её значения для первого и последнего бина "близки" к 0 и 1. Это означает, что модель уверенно разделяет классы между собой.

  3. Точки E_g​ и P_g(средняя предсказанная вероятность) находятся рядом друг с другом для соответствующих бинов. Это означает, что вероятности хорошо откалиброваны.

Таким образом, по данному графику можно понять насколько хорошо модель предсказывает вероятности и увидеть потенциальные проблемы.

Так, например, на графике выше изображена кривая для хорошей модели: она выгнута вниз, а бины отсортированы по возрастанию. В первом бине эмпирическая вероятность E_g близка к 0, а в последнем — к 1, что подтверждает уверенность модели в разделении классов. Однако видны определенные проблемы. С первого по седьмой бин наблюдается недооценка прогнозируемой вероятности — это свидетельствует о проблеме с калибровкой на объектах с меньшей вероятностью класса "1". Также заметно, что в 4-м бине доля объектов класса "1" E_g оказывается ниже, чем в предыдущих сегментах. Такое поведение может указывать на аномалии в данных, ошибки разметки или особенности распределения признаков. Все это повод взглянуть на эти бины отдельно и понять почему такое происходит.

На основании этих графиков строится статистика Hosmer-Lemeshow. Выглядит она так:

chi^2=sum_{g=1}^{B} frac{(P_g - E_g)^2}{P_g (1 - P_g)}*B_i

где: B — количество бинов, B_i — число обьектов в бине i.

Статистика Hosmer-Lemeshow сравнивается с критическим значением распределения хи_квадрат chi^2с B-2 степенями свободы. Но на практике используется редко и скорее как метрика сравнения двух моделей, чем как статистический тест.

Применение: Кривые Hosmer-Lemeshow - отличный инструмент для визуализации качества предсказания вероятности. Помогает увидеть как классификатор предсказывает вероятности и выявить проблемные участки.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.isotonic import IsotonicRegression

# Генерация данных с корреляцией между предсказаниями и истинными значениями
np.random.seed(42)
base_probs = np.random.rand(1000)
y_true = np.random.choice([0, 1], size=1000, p=[0.7, 0.3])
correlated_probs = np.where(y_true == 1, base_probs + 0.3, base_probs - 0.3)
pred_probs = np.clip(correlated_probs, 0, 1)

# Создание DataFrame
df = pd.DataFrame({
    'y_true': y_true,
    'pred_probs': pred_probs
})

# Применение изотонической регрессии для калибровки
iso_reg = IsotonicRegression(out_of_bounds='clip')
calibrated_probs = iso_reg.fit_transform(df['pred_probs'], df['y_true'])

# Обновление данных с откалиброванными предсказаниями
df['pred_probs_calibrated'] = calibrated_probs

# Пересортировка данных по откалиброванной вероятности
df_sorted_calibrated = df.sort_values(by='pred_probs_calibrated').reset_index(drop=True)

# Разделение на бины (децили) равного размера
df_sorted_calibrated['bin'] = pd.qcut(df_sorted_calibrated.index, q=10, labels=False)

# Подсчет статистик для каждого бина
bin_stats_calibrated = df_sorted_calibrated.groupby('bin').agg(
    mean_predicted_prob=('pred_probs_calibrated', 'mean'),  # Средняя откалиброванная вероятность
    count_class_1=('y_true', 'sum'),                       # Количество объектов класса "1"
    total_count=('y_true', 'count')                        # Общее количество объектов в бине
).reset_index()

# Добавление доли истинных положительных результатов
bin_stats_calibrated['empirical_prob'] = bin_stats_calibrated['count_class_1'] / bin_stats_calibrated['total_count']

# Построение графика для откалиброванных вероятностей
plt.figure(figsize=(12, 7))

# Гистограмма эмпирической вероятности
plt.bar(bin_stats_calibrated['bin'], bin_stats_calibrated['empirical_prob'], alpha=0.6, label="Эмпирическая вероятность", color='blue')

# Кривая средней откалиброванной вероятности
plt.plot(bin_stats_calibrated['bin'], bin_stats_calibrated['mean_predicted_prob'], marker='o', label="Средняя предсказанная вероятность (калибровка)", color='orange')

# Настройка осей и легенды
plt.xlabel('Номер бина')
plt.ylabel('Вероятность')
plt.title('Средняя откалиброванная вероятность и эмпирическая вероятность по бинам')
plt.legend()
plt.grid(axis='y', linestyle='--', alpha=0.7)

plt.show()

bin_stats_calibrated

Выводы

В зависимости от целей анализа можно использовать разные метрики, однако для полноценного понимания того, как модель предсказывает вероятности, лучше учитывать их все. Таким образом:

  • Для оценки качества разделения классов, то есть того, насколько хорошо модель сортирует объекты по принадлежности к классу, подойдут метрики: ROC-AUC и PR-AUC (особенно актуальна для задач с несбалансированными классами).

  • Для визуализации и проверки калибровки вероятностей: полезно использовать кривые Хосмера-Лемешва и Calibration Curve, которые наглядно показывают, насколько предсказанные вероятности соответствуют реальным результатам.

  • Для сравнения моделей: часто применяются метрики, такие как Log Loss, ROC-AUC, а также ECE (Expected Calibration Error), которая учитывает ошибки калибровки.

Такой подход позволяет комплексно оценивать модель, выявлять её сильные и слабые стороны и принимать обоснованные решения относительно качества вашей модели.

Автор: alex_terentiev_13

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js