{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "# импортируем необходимые библиотеки, классы и функции\n",
    "import numpy as np  \n",
    "import pandas as pd\n",
    "from sklearn.preprocessing import StandardScaler\n",
    "from sklearn.model_selection import train_test_split\n",
    "from sklearn.metrics import roc_auc_score"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "# загружаем данные\n",
    "data = pd.read_csv('Data/bankloan.csv', sep=';', decimal=',')\n",
    "\n",
    "# разбиваем данные на обучающие и тестовые: \n",
    "# получаем обучающий массив предикторов, \n",
    "# тестовый массив предикторов, обучающий\n",
    "# массив меток, тестовый массив меток\n",
    "X_train, X_test, y_train, y_test = train_test_split(\n",
    "    data.drop('default', axis=1), \n",
    "    data['default'], \n",
    "    test_size=0.3,\n",
    "    stratify=data['default'],\n",
    "    random_state=42)\n",
    "\n",
    "# создаем список количественных переменных\n",
    "num_cols = ['age', 'debtinc', 'creddebt', 'othdebt']\n",
    "\n",
    "# создаем экземпляр класса StandardScaler\n",
    "standardscaler = StandardScaler()\n",
    "\n",
    "# выполняем стандартизацию\n",
    "standardscaler.fit(X_train[num_cols])\n",
    "X_train[num_cols] = standardscaler.transform(X_train[num_cols])\n",
    "X_test[num_cols] = standardscaler.transform(X_test[num_cols])\n",
    "\n",
    "# выполняем дамми-кодирование\n",
    "X_train = pd.get_dummies(X_train)\n",
    "X_test = pd.get_dummies(X_test)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "# пишем собственный класс, строящий логистическую\n",
    "# регрессию с регуляризацией и использованием\n",
    "# градиентного спуска\n",
    "class RegularizedLogisticRegression_GD:\n",
    "    \"\"\"\n",
    "    Класс, строящий логистическую регрессию\n",
    "    с регуляризацией и использованием\n",
    "    градиентного спуска.\n",
    "    \n",
    "    Параметры\n",
    "    ----------\n",
    "    penalty: string, по умолчанию 'l2'\n",
    "        Метод регуляризации. \n",
    "        Можно выбрать 'l1' или 'l2'.\n",
    "    lr: float, по умолчанию 0.1\n",
    "        Темп обучения.\n",
    "    tol: float, по умолчанию 1e-5\n",
    "        Допуск сходимости.\n",
    "    max_iter: int, по умолчанию 1e7\n",
    "        Максимальное количество итераций \n",
    "        градиентного спуска.\n",
    "    lambda_: float, по умолчанию 0.001\n",
    "        Сила регуляризации.\n",
    "    fit_intercept: bool, по умолчанию True\n",
    "        Добавление константы.   \n",
    "    \"\"\"\n",
    "    def __init__(self, penalty='l2', lr=0.1, tol=1e-5, max_iter=1e7,\n",
    "                 lambda_=0.001, fit_intercept=True):\n",
    "        \n",
    "        # проверяем параметр penalty, задающий регуляризацию,\n",
    "        # на соответствие значениям 'l1' или 'l2'\n",
    "        if penalty not in ['l2', 'l1']:\n",
    "            raise ValueError(\n",
    "                \"penalty must be 'l1' or 'l2' \"\n",
    "                \"got '%s' instead\" % penalty\n",
    "            )\n",
    "        \n",
    "        # тип регуляризации (должен быть либо 'l1' , либо 'l2')\n",
    "        self.penalty = penalty\n",
    "        # темп обучения\n",
    "        self.lr = lr\n",
    "        # допуск сходимости\n",
    "        self.tol = tol\n",
    "        # максимальное количество итераций\n",
    "        self.max_iter = max_iter\n",
    "        #  штрафной коэффициент, т.е. вводим штраф за слишком\n",
    "        # большие оценки коэффициентов регрессии\n",
    "        self.lambda_ = lambda_\n",
    "        # добавление константы\n",
    "        self.fit_intercept = fit_intercept\n",
    "\n",
    "    # метод .fit() выполняет обучение\n",
    "    def fit(self, X, y):\n",
    "        # если задан параметр fit_intercept=True\n",
    "        if self.fit_intercept:\n",
    "            # то добавляем константу, т.е. добавляем\n",
    "            # первый столбец из единиц\n",
    "            X = np.c_[np.ones(X.shape[0]), X]\n",
    "\n",
    "        # инициализируем значение функции потерь на предыдущей\n",
    "        # итерации бесконечно большим значением\n",
    "        l_prev = np.inf\n",
    "        # инициализируем веса признаков нулями\n",
    "        self.beta = np.zeros(X.shape[1])\n",
    "\n",
    "        # выполняем градиентный спуск\n",
    "        for _ in range(int(self.max_iter)):\n",
    "            # применяем сигмоид-преобразование к скалярному произведению\n",
    "            # массива предикторов и вектора весов, получаем\n",
    "            # вероятности положительного класса\n",
    "            y_pred = self._sigmoid(np.dot(X, self.beta))\n",
    "            # вычисляем значение логистической функции потерь\n",
    "            loss = self._NLL(X, y, y_pred)\n",
    "            # если разница между предыдущим значением и текущим значением \n",
    "            # функции потерь меньше заданного порога (tol), то прерываем \n",
    "            # цикл, т.е. реализована ранняя остановка, которая ограничивает \n",
    "            # количество итераций (max_iter)\n",
    "            if l_prev - loss < self.tol:\n",
    "                return\n",
    "            # присваиваем функции потерь на предыдущей \n",
    "            # итерации текущее значение\n",
    "            l_prev = loss\n",
    "            # обновляем веса, вычитаем из текущего приближения вектора \n",
    "            # весов вектор градиента, умноженный на некоторый \n",
    "            # темп обучения\n",
    "            self.beta -= self.lr * self._NLL_grad(X, y, y_pred)\n",
    "\n",
    "    # метод _sigmoid вычисляет значение сигмоиды\n",
    "    def _sigmoid(self, z):\n",
    "        return 1 / (1 + np.exp(-z))\n",
    "\n",
    "    # метод _NLL вычисляет значение логистической функции потерь\n",
    "    def _NLL(self, X, y, y_pred):\n",
    "        # вычисляем значение логистической функции потерь без штрафа\n",
    "        nll = -np.log(np.where(y == 1, y_pred, 1 - y_pred)).sum()\n",
    "        \n",
    "        # вычисляем штрафное слагаемое, представляющее собой\n",
    "        # произведение штрафного коэффициента и L2-нормы весов \n",
    "        # (регрессионных коэффициентов), если order=2, возвращаем \n",
    "        # np.sum(np.abs(x)**2)**(1./2), т.е. квадратный корень из \n",
    "        # суммы квадратов модулей регрессионных коэффициентов и \n",
    "        # возводим в квадрат, чтобы получить сумму квадратов \n",
    "        # регрессионных коэффициентов\n",
    "        if self.penalty == 'l2': \n",
    "            # если первый элемент вектора весов - это константа,\n",
    "            # регуляризацию применяем ко всем элементам вектора\n",
    "            # весов, кроме первого\n",
    "            if self.fit_intercept:\n",
    "                penalty = (self.lambda_ / 2) * np.linalg.norm(\n",
    "                    self.beta[1:], ord=2) ** 2             \n",
    "            # если вектор весов не содержит константу, применяем\n",
    "            # регуляризацию ко всем элементам вектора весов\n",
    "            else:\n",
    "                penalty = (self.lambda_ / 2) * np.linalg.norm(\n",
    "                    self.beta, ord=2) ** 2\n",
    "        \n",
    "        # вычисляем штрафное слагаемое, представляющее собой \n",
    "        # произведение штрафного коэффициента и L1-нормы весов \n",
    "        # (регрессионных коэффициентов), если order=1, возвращаем \n",
    "        # np.sum(np.abs(x)), т.е. сумму модулей регрессионных\n",
    "        # коэффициентов\n",
    "        if self.penalty == 'l1':\n",
    "            # если первый элемент вектора весов - это константа,\n",
    "            # регуляризацию применяем ко всем элементам вектора\n",
    "            # весов, кроме первого\n",
    "            if self.fit_intercept:\n",
    "                penalty = self.lambda_ * np.linalg.norm(\n",
    "                    self.beta[1:], ord=1)\n",
    "            # если вектор весов не содержит константу, применяем\n",
    "            # регуляризацию ко всем элементам вектора весов\n",
    "            else:\n",
    "                penalty = self.lambda_ * np.linalg.norm(\n",
    "                    self.beta, ord=1)\n",
    "            \n",
    "        # вычисляем итоговое значение логистической функции потерь, \n",
    "        # прибавив к значению логистической функции потерь штрафное \n",
    "        # слагаемое, полученную сумму делим на количество наблюдений\n",
    "        return (penalty + nll) / X.shape[0]\n",
    "\n",
    "    # метод _NLL_grad вычисляет вектор градиента\n",
    "    def _NLL_grad(self, X, y, y_pred):\n",
    "        # если тип регуляризации l2\n",
    "        if self.penalty == 'l2':\n",
    "            # если первый элемент вектора весов - это константа,\n",
    "            # регуляризацию применяем ко всем элементам вектора\n",
    "            # весов, кроме первого\n",
    "            if self.fit_intercept:\n",
    "                # штрафуем все веса, кроме первого (константы)\n",
    "                d_penalty = self.lambda_ * self.beta[1:]\n",
    "                # подставляем константу в вектор оштрафованных весов\n",
    "                d_penalty = np.r_[self.beta[0], d_penalty]\n",
    "            # если вектор весов не содержит константу, применяем\n",
    "            # регуляризацию ко всем элементам вектора весов\n",
    "            else:\n",
    "                d_penalty = self.lambda_ * self.beta\n",
    "\n",
    "        # если тип регуляризации l1\n",
    "        if self.penalty == 'l1':\n",
    "            # если первый элемент вектора весов - это константа,\n",
    "            # регуляризацию применяем ко всем элементам вектора\n",
    "            # весов, кроме первого\n",
    "            if self.fit_intercept:\n",
    "                # штрафуем все веса, кроме первого (константы)\n",
    "                d_penalty = self.lambda_ * np.sign(self.beta[1:])\n",
    "                # подставляем константу в вектор оштрафованных весов\n",
    "                d_penalty = np.r_[self.beta[0], d_penalty]\n",
    "            # если вектор весов не содержит константу, применяем\n",
    "            # регуляризацию ко всем элементам вектора весов\n",
    "            else:\n",
    "                d_penalty = self.lambda_ * np.sign(self.beta)\n",
    "                \n",
    "        # получаем вектор градиента, для этого вычисляем скалярное \n",
    "        # произведение матрицы предикторов и вектора разностей между \n",
    "        # вероятностями положительного класса и фактическими значениями\n",
    "        # зависимой переменной, прибавляем к полученным результатам \n",
    "        # оштрафованные веса, берем итоги и делим на количество \n",
    "        # наблюдений\n",
    "        return (np.dot(y_pred - y, X) + d_penalty) / X.shape[0]\n",
    "\n",
    "    # метод .predict_proba() вычисляет вероятности\n",
    "    def predict_proba(self, X):\n",
    "        # если задано fit_intercept=True\n",
    "        if self.fit_intercept:\n",
    "            # добавляем константу\n",
    "            X = np.c_[np.ones(X.shape[0]), X]\n",
    "        return self._sigmoid(np.dot(X, self.beta))\n",
    "\n",
    "    # метод .predict() вычисляет прогнозы\n",
    "    def predict(self, X, threshold):\n",
    "        # получаем прогнозы в зависимости от установленного порога\n",
    "        return (self.predict_proba(X) >= threshold).astype(int)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "AUC-ROC на тестовой выборке: 0.788\n",
      "CPU times: user 930 ms, sys: 188 ms, total: 1.12 s\n",
      "Wall time: 142 ms\n"
     ]
    }
   ],
   "source": [
    "%%time\n",
    "\n",
    "# создаем экземпляр нашего класса\n",
    "# RegularizedLogisticRegression_GD\n",
    "model = RegularizedLogisticRegression_GD()\n",
    "# обучаем модель логистической регрессии\n",
    "model.fit(X_train, y_train)\n",
    "# вычислим AUC-ROC\n",
    "proba = model.predict_proba(X_test)\n",
    "print(\"AUC-ROC на тестовой выборке: %.3f\" % roc_auc_score(y_test, proba))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "# создаем игрушечный массив предикторов\n",
    "X_toy = np.array([[0.1, 0.2, 0.3], \n",
    "                  [0.7, 0.5, 0.2],\n",
    "                  [0.2, 0.4, 1.4],\n",
    "                  [0.4, 0.1, 0.5]])\n",
    "\n",
    "# создаем игрушечный массив значений\n",
    "# зависимой переменной\n",
    "y_toy = np.array([0, 1, 0, 0])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "# создаем вектор весов, инициализируем веса очень \n",
    "# небольшими положительными значениями, близкими к 0\n",
    "beta = np.array([0.01, 0.012, 0.015])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "# пишем функцию сигмоид-преобразования\n",
    "def sigmoid(z):\n",
    "    return 1 / (1 + np.exp(-z))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([0.50197499, 0.50399991, 0.50694955, 0.50317496])"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# к скалярному произведению матрицы предикторов и вектора весов\n",
    "# применяем сигмоид-преобразование и получаем вероятности \n",
    "# положительного класса\n",
    "y_pred = sigmoid(np.dot(X_toy, beta))\n",
    "y_pred"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "# задаем тип регуляризации\n",
    "penalty = 'l2'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "2.7889452861319945"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# вычисляем значение логистической функции потерь без штрафа\n",
    "nll = -np.log(np.where(y_toy == 1, y_pred, 1 - y_pred)).sum()\n",
    "nll"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "# задаем значение штрафного коэффициента\n",
    "lambda_ = 0.1"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0.00046899999999999996\n",
      "0.000469\n"
     ]
    }
   ],
   "source": [
    "# убеждаемся, что с помощью np.linalg.norm() возвращаем \n",
    "# сумму квадратов регрессионных коэффициентов\n",
    "print(np.linalg.norm(beta, ord=2) ** 2)\n",
    "print(np.sum(beta ** 2))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0.037\n",
      "0.037\n"
     ]
    }
   ],
   "source": [
    "# убеждаемся, что с помощью np.linalg.norm() возвращаем\n",
    "# сумму модулей регрессионных коэффициентов\n",
    "print(np.linalg.norm(beta, ord=1))\n",
    "print(np.sum(np.abs(beta)))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "2.345e-05"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# вычисляем штрафное слагаемое, представляющее собой\n",
    "# произведение штрафного коэффициента и L2-нормы весов \n",
    "# (регрессионных коэффициентов), если order=2, возвращаем \n",
    "# np.sum(np.abs(x)**2)**(1./2), т.е. квадратный корень из \n",
    "# суммы квадратов модулей регрессионных коэффициентов и \n",
    "# возводим в квадрат, чтобы получить сумму квадратов \n",
    "# регрессионных коэффициентов\n",
    "penalty_term = (lambda_ / 2) * np.linalg.norm(beta, ord=2) ** 2\n",
    "penalty_term"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "0.6972421840329986"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# вычисляем значение логистической функции потерь \n",
    "# со штрафом, прибавив к значению логистической \n",
    "# функции потерь штрафное слагаемое, полученную\n",
    "# сумму делим на количество наблюдений\n",
    "loss = (penalty_term + nll) / X_toy.shape[0]\n",
    "loss"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([0.001 , 0.0012, 0.0015])"
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# получаем оштрафованные веса, для этого\n",
    "# умножаем веса на штрафной коэффициент\n",
    "d_penalty = lambda_ * beta\n",
    "d_penalty"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([0.00166433, 0.02667307, 0.25355233])"
      ]
     },
     "execution_count": 17,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# получаем вектор градиента\n",
    "NLL_grad = (np.dot(y_pred - y_toy, X_toy) + d_penalty) / X_toy.shape[0]\n",
    "NLL_grad"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([ 0.00983357,  0.00933269, -0.01035523])"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# задаем темп обучения\n",
    "lr = 0.1\n",
    "\n",
    "# обновляем веса, вычитаем из текущего приближения вектора весов \n",
    "# вектор градиента, умноженный на некоторый темп обучения\n",
    "beta = beta - lr * NLL_grad\n",
    "beta"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [],
   "source": [
    "# пишем класс RegularizedLogisticRegression_Newton, \n",
    "# строим логистическую регрессию с L2-регуляризацией и\n",
    "# использованием ньютоновского метода\n",
    "class RegularizedLogisticRegression_Newton():\n",
    "    \"\"\"\n",
    "    Класс, строящий логистическую регрессию\n",
    "    с L2-регуляризацией и использованием\n",
    "    метода Ньютона.\n",
    "    \n",
    "    Параметры\n",
    "    ----------\n",
    "    tol: float, по умолчанию 1e-5\n",
    "        Допуск сходимости.\n",
    "    max_iter: int, по умолчанию 10\n",
    "        Максимальное количество итераций \n",
    "        ньютоновской оптимизации.\n",
    "    lambda_: float, по умолчанию 0.001\n",
    "        Сила регуляризации.\n",
    "    fit_intercept: bool, по умолчанию True\n",
    "        Добавление константы.   \n",
    "    \"\"\"\n",
    "    def __init__(self, tol=1e-5, max_iter=10, lambda_=0.001, \n",
    "                 fit_intercept=True):\n",
    "        # максимальное количество итераций\n",
    "        self.max_iter = max_iter\n",
    "        # допуск сходимости\n",
    "        self.tol = tol\n",
    "        #  штрафной коэффициент, т.е. вводим штраф за слишком\n",
    "        # большие оценки коэффициентов регресии\n",
    "        self.lambda_ = lambda_\n",
    "        # добавление константы\n",
    "        self.fit_intercept = fit_intercept\n",
    "\n",
    "    # метод .fit() выполняет обучение\n",
    "    def fit(self, X, y):\n",
    "        # если задан параметр fit_intercept=True\n",
    "        if self.fit_intercept:\n",
    "            # то добавляем константу, т.е. добавляем\n",
    "            # первый столбец из единиц\n",
    "            X = np.c_[np.ones(X.shape[0]), X]\n",
    "            \n",
    "        # инициализируем значение функции потерь на предыдущей\n",
    "        # итерации бесконечно большим значением\n",
    "        l_prev = np.inf\n",
    "        # инициализируем веса признаков нулями\n",
    "        self.beta = np.zeros(X.shape[1])\n",
    "\n",
    "        # выполняем ньютоновскую оптимизацию\n",
    "        for _ in range(int(self.max_iter)):\n",
    "            # применяем сигмоид-преобразование к скалярному произведению\n",
    "            # массива предикторов и вектора весов, по сути получаем\n",
    "            # вероятности положительного класса\n",
    "            y_pred = self._sigmoid(np.dot(X, self.beta))\n",
    "            \n",
    "            # вычисляем значение логистической функции потерь\n",
    "            loss = self._NLL(X, y, y_pred)\n",
    "            # если разница между предыдущим значением и текущим значением \n",
    "            # функции потерь меньше заданного порога (tol), то прерываем \n",
    "            # цикл, т.е. реализована ранняя остановка, которая ограничивает \n",
    "            # количество итераций (max_iter)\n",
    "            if l_prev - loss < self.tol:\n",
    "                return\n",
    "            # присваиваем функции потерь на предыдущей \n",
    "            # итерации текущее значение\n",
    "            l_prev = loss \n",
    "            # обновляем веса, вычитаем из текущего приближения вектора \n",
    "            # весов вектор градиента, умноженный на некоторый \n",
    "            # темп обучения\n",
    "            self.beta -= self._NLL_grad(X, y, y_pred)\n",
    "\n",
    "    # метод _sigmoid вычисляет значение сигмоиды\n",
    "    def _sigmoid(self, z):\n",
    "        return 1 / (1 + np.exp(-z))\n",
    "\n",
    "    # метод _NLL вычисляет значение логистической функции потерь\n",
    "    def _NLL(self, X, y, y_pred):\n",
    "        # вычисляем значение логистической функции потерь без штрафа\n",
    "        eps = 1e-15\n",
    "        y_pred = np.clip(y_pred, eps, 1 - eps)\n",
    "        nll = -np.log(np.where(y == 1, y_pred, 1 - y_pred)).sum()\n",
    "        \n",
    "        # вычисляем штрафное слагаемое, представляющее собой\n",
    "        # произведение штрафного коэффициента и L2-нормы весов \n",
    "        # (регрессионных коэффициентов), если order=2, возвращаем \n",
    "        # np.sum(np.abs(x)**2)**(1./2), т.е. квадратный корень из \n",
    "        # суммы квадратов модулей регрессионных коэффициентов и \n",
    "        # возводим в квадрат, чтобы получить сумму квадратов \n",
    "        # регрессионных коэффициентов\n",
    "       \n",
    "        # если первый элемент вектора весов - это константа,\n",
    "        # регуляризацию применяем ко всем элементам вектора\n",
    "        # весов, кроме первого\n",
    "        if self.fit_intercept:\n",
    "            penalty = (self.lambda_ / 2) * np.linalg.norm(\n",
    "                self.beta[1:], ord=2) ** 2             \n",
    "        # если вектор весов не содержит константу, применяем\n",
    "        # регуляризацию ко всем элементам вектора весов\n",
    "        else:\n",
    "            penalty = (self.lambda_ / 2) * np.linalg.norm(\n",
    "                self.beta, ord=2) ** 2\n",
    "            \n",
    "        # вычисляем итоговое значение логистической функции потерь, \n",
    "        # прибавив к значению логистической функции потерь штрафное \n",
    "        # слагаемое, полученную сумму делим на количество наблюдений\n",
    "        return (penalty + nll) / X.shape[0]\n",
    "    \n",
    "    # метод _NLL_grad вычисляет вектор градиента\n",
    "    def _NLL_grad(self, X, y, y_pred):      \n",
    "        # если первый элемент вектора весов - это константа,\n",
    "        # регуляризацию применяем ко всем элементам вектора\n",
    "        # весов, кроме первого\n",
    "        if self.fit_intercept:\n",
    "            d_penalty = np.r_[self.beta[0], self.lambda_ * self.beta[1:]]\n",
    "            lambda_diag = np.diag(np.r_[0, [self.lambda_] * len(\n",
    "                self.beta[1:])])\n",
    "        # если вектор весов не содержит константу, применяем\n",
    "        # регуляризацию ко всем элементам вектора весов\n",
    "        else:\n",
    "            d_penalty = self.lambda_ * self.beta\n",
    "            lambda_diag = np.diag(np.ones_like(self.beta) * self.lambda_)\n",
    "        \n",
    "        # получаем вектор градиента, для этого вычисляем скалярное \n",
    "        # произведение матрицы предикторов и вектора разностей между \n",
    "        # вероятностями положительного класса и фактическими значениями \n",
    "        # зависимой переменной, прибавляем к полученным результатам \n",
    "        # оштрафованные веса, берем итоги и делим на количество наблюдений\n",
    "        grad = (np.dot(X.T, y_pred - y) + d_penalty) / X.shape[0]\n",
    "        # вычисляем гессиан\n",
    "        X_ = (y_pred * (1 - y_pred))[:, np.newaxis] * X\n",
    "        hess = np.dot(X_.T, X) / X.shape[0] + lambda_diag  \n",
    "        # получаем обратный гессиан\n",
    "        inv_hess = np.linalg.pinv(hess)\n",
    "        # возвращаем произведение градиента и обратного гессиана\n",
    "        return np.dot(inv_hess, grad)\n",
    "    \n",
    "    # метод .predict_proba() вычисляет вероятности\n",
    "    def predict_proba(self, X):\n",
    "        # если задано fit_intercept=True\n",
    "        if self.fit_intercept:\n",
    "            # добавляем константу\n",
    "            X = np.c_[np.ones(X.shape[0]), X]\n",
    "        return self._sigmoid(np.dot(X, self.beta))\n",
    "\n",
    "    # метод .predict() вычисляет прогнозы\n",
    "    def predict(self, X, threshold):\n",
    "        # получаем прогнозы в зависимости от установленного порога\n",
    "        return (self.predict_proba(X) >= threshold).astype(int)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "AUC-ROC на тестовой выборке: 0.786\n",
      "CPU times: user 30.9 ms, sys: 23.2 ms, total: 54.1 ms\n",
      "Wall time: 7.27 ms\n"
     ]
    }
   ],
   "source": [
    "%%time\n",
    "\n",
    "# создаем экземпляр нашего класса\n",
    "model = RegularizedLogisticRegression_Newton()\n",
    "# обучаем модель логистической регрессии\n",
    "model.fit(X_train, y_train)\n",
    "# вычислим AUC-ROC\n",
    "proba = model.predict_proba(X_test)\n",
    "print(\"AUC-ROC на тестовой выборке: %.3f\" % roc_auc_score(y_test, proba))"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
