admit | gre | gpa | rank | |
---|---|---|---|---|
0 | 0 | 380 | 3.61 | 3 |
1 | 1 | 660 | 3.67 | 3 |
2 | 1 | 800 | 4.00 | 1 |
3 | 1 | 640 | 3.19 | 4 |
4 | 0 | 520 | 2.93 | 4 |
5 | 1 | 760 | 3.00 | 2 |
6 | 1 | 560 | 2.98 | 1 |
7 | 0 | 400 | 3.08 | 2 |
8 | 1 | 540 | 3.39 | 3 |
9 | 0 | 700 | 3.92 | 2 |
Logistic Regression
Introduction
So far we have seen linear regression:
- a continuous valued observation is estimated as a linear (or affine) function of the independent variables.
Now we will look at the following situation.
Imagine that you are observing a binary variable – value 0 or 1.
That is, these could be pass/fail, admit/reject, Democrat/Republican, etc.
Assume there is some probability of observing a 1, and that probability is a function of certain independent variables.
So the key properties of a problem that make it appropriate for logistic regression are:
- You are trying to predict a categorical variable
- You want to estimate a probability of seeing a particular value of the categorical variable.
Example: Grad School Admission
The following example was adapted from this URL which seems to be no longer available. There is an archive of the page and an archive of the dataset.
Let’s consider this question:
What is the probability I will be admitted to Grad School?
Let’s see how variables, such as,
- GRE (Graduate Record Exam scores),
- GPA (grade point average), and
- prestige of the undergraduate institution
affect admission into graduate school.
The response variable, admit/don’t admit, is a binary variable.
So there are three predictor variables: gre, gpa and rank.
- We will treat the variables gre and gpa as continuous.
- The variable rank takes on the values 1 through 4 with 1 being the highest prestige.
Let’s look at 10 lines of the data:
and some summary statistics:
Code
df.describe()
admit | gre | gpa | rank | |
---|---|---|---|---|
count | 400.000000 | 400.000000 | 400.000000 | 400.00000 |
mean | 0.317500 | 587.700000 | 3.389900 | 2.48500 |
std | 0.466087 | 115.516536 | 0.380567 | 0.94446 |
min | 0.000000 | 220.000000 | 2.260000 | 1.00000 |
25% | 0.000000 | 520.000000 | 3.130000 | 2.00000 |
50% | 0.000000 | 580.000000 | 3.395000 | 2.00000 |
75% | 1.000000 | 660.000000 | 3.670000 | 3.00000 |
max | 1.000000 | 800.000000 | 4.000000 | 4.00000 |
We can also plot histograms of the variables:
Code
= (10, 7)); df.hist(figsize
Let’s look at how each independent variable affects admission probability by plotting the mean admission probability as a function of the independent variable.
We add error bars to the means to indicate the standard error of the mean.
First, rank:
Code
import numpy as np
# Calculate mean and standard error
= df.groupby('rank')['admit']
grouped = grouped.mean()
means = grouped.std() / np.sqrt(grouped.count())
errors
# Plot with error bars
= means.plot(marker='o', yerr=errors, fontsize=12, capsize=5)
ax 'P[admit]', fontsize=16)
ax.set_ylabel('Rank', fontsize=16); ax.set_xlabel(
Next, GRE:
Code
= df.groupby('gre')['admit']
grouped_gre = grouped_gre.mean()
means_gre = grouped_gre.std() / np.sqrt(grouped_gre.count())
errors_gre
= means_gre.plot(marker='o', yerr=errors_gre, fontsize=12, capsize=5)
ax 'P[admit]', fontsize=16)
ax.set_ylabel('GRE', fontsize=16); ax.set_xlabel(
Finally, GPA (for this visualization, we aggregate GPA into 8 bins):
Code
= np.linspace(df.gpa.min(), df.gpa.max(), 8)
bins = (bins[:-1] + bins[1:]) / 2
bin_centers = df.groupby(np.digitize(df.gpa, bins)).mean()['admit']
grouped_gpa = grouped_gpa.plot(marker='o', fontsize=12)
ax 'P[admit]', fontsize=16)
ax.set_ylabel('GPA', fontsize=16)
ax.set_xlabel(range(1, len(bin_centers) + 1))
ax.set_xticks(f'{center:.2f}' for center in bin_centers], rotation=45); ax.set_xticklabels([
Finally, we plot admission value for each data point for each of the four ranks:
Code
= df[df['rank']==1]
df1 = df[df['rank']==2]
df2 = df[df['rank']==3]
df3 = df[df['rank']==4]
df4
= plt.figure(figsize = (10, 5))
fig
= fig.add_subplot(221)
ax1 'gre','admit', ax = ax1)
df1.plot.scatter('Rank 1 Institutions')
plt.title(
= fig.add_subplot(222)
ax2 'gre','admit', ax = ax2)
df2.plot.scatter('Rank 2 Institutions')
plt.title(
= fig.add_subplot(223, sharex = ax1)
ax3 'gre','admit', ax = ax3)
df3.plot.scatter('Rank 3 Institutions')
plt.title(
= fig.add_subplot(224, sharex = ax2)
ax4 'Rank 4 Institutions')
plt.title('gre','admit', ax = ax4); df4.plot.scatter(
What we want to do is to fit a model that predicts the probability of admission as a function of these independent variables.
Logistic Regression
Logistic regression is concerned with estimating a probability.
However, all that is available are categorical observations, which we will code as 0/1.
That is, these could be pass/fail, admit/reject, Democrat/Republican, etc.
Now, a linear function like \(\beta_0 + \beta_1 x\) cannot be used to predict probability directly, because the linear function takes on all values (from -\(\infty\) to +\(\infty\)), and probability only ranges over \([0, 1]\).
Odds and Log-Odds
However, there is a transformation of probability that works: it is called log-odds.
For any probabilty \(p\), the odds is defined as \(p/(1-p)\), which is the ratio of the probability of an event to the probability of the non-event.
Notice that odds vary from 0 to \(\infty\), and odds < 1 indicates that \(p < 1/2\).
Now, there is a good argument that to fit a linear function, instead of using odds, we should use log-odds.
That is simply \(\log p/(1-p)\) which is also called the logit function, which is an abbreviation for logistic unit.
Code
= np.linspace(0.01, 0.99, 100)
pvec = plt.figure(figsize = (6, 4)).add_subplot()
ax / (1-pvec)))
ax.plot(pvec, np.log(pvec =12)
ax.tick_params(labelsize'Probability', fontsize = 14)
ax.set_xlabel('Log-Odds', fontsize = 14)
ax.set_ylabel('Logit Function: $\log (p/1-p)$', fontsize = 16); ax.set_title(
So, logistic regression does the following: it does a linear regression of \(\beta_0 + \beta_1 x\) against \(\log p/(1-p)\).
That is, it fits:
\[ \begin{aligned} \beta_0 + \beta_1 x &= \log \frac{p(x)}{1-p(x)} \\ e^{\beta_0 + \beta_1 x} &= \frac{p(x)}{1-p(x)} \quad \text{(exponentiate both sides)} \\ e^{\beta_0 + \beta_1 x} (1-p(x)) &= p(x) \quad \text{(multiply both sides by $1-p(x)$)} \\ e^{\beta_0 + \beta_1 x} &= p(x) + p(x)e^{\beta_0 + \beta_1 x} \quad \text{(distribute $p(x)$)} \\ \frac{e^{\beta_0 + \beta_1 x}}{1 +e^{\beta_0 + \beta_1 x}} &= p(x) \end{aligned} \]
So, logistic regression fits a probability of the following form:
\[ p(x) = P(y=1\mid x) = \frac{e^{\beta_0+\beta_1 x}}{1+e^{\beta_0+\beta_1 x}}. \]
This is a sigmoid function; when \(\beta_1 > 0\), \(x\rightarrow \infty\), then \(p(x)\rightarrow 1\) and when \(x\rightarrow -\infty\), then \(p(x)\rightarrow 0\).
Code
= [-4, -8,-12,-20]
alphas = [0.4,0.4,0.6,1]
betas = np.arange(40)
x = plt.figure(figsize=(8, 6))
fig = plt.subplot(111)
ax
for i in range(len(alphas)):
= alphas[i]
a = betas[i]
b = np.exp(a+b*x)/(1+np.exp(a+b*x))
y # plt.plot(x,y,label=r"$\frac{e^{%d + %3.1fx}}{1+e^{%d + %3.1fx}}\;\beta_0=%d, \beta_1=%3.1f$" % (a,b,a,b,a,b))
=r"$\beta_0=%d,$ $\beta_1=%3.1f$" % (a,b))
ax.plot(x,y,label=12)
ax.tick_params(labelsize'x', fontsize = 14)
ax.set_xlabel('$p(x)$', fontsize = 14)
ax.set_ylabel(='center left', bbox_to_anchor=(1, 0.5), prop={'size': 16})
ax.legend(loc'Logistic Functions', fontsize = 16); ax.set_title(
Parameter \(\beta_1\) controls how fast \(p(x)\) raises from \(0\) to \(1\)
The value of -\(\beta_0\)/\(\beta_1\) shows the value of \(x\) for which \(p(x)=0.5\)
Another interpretation of \(\beta_0\) is that it gives the base rate – the unconditional probability of a 1. That is, if you knew nothing about a particular data item, then \(p(x) = 1/(1+e^{-\beta_0})\).
The function \(f(x) = \log (x/(1-x))\) is called the logit function.
So a compact way to describe logistic regression is that it finds regression coefficients \(\beta_0, \beta_1\) to fit:
\[ \text{logit}\left(p(x)\right)=\log\left(\frac{p(x)}{1-p(x)} \right) = \beta_0 + \beta_1 x. \]
Note also that the inverse logit function is:
\[ \text{logit}^{-1}(x) = \frac{e^x}{1 + e^x}. \]
Somewhat confusingly, this is called the logistic function.
So, the best way to think of logistic regression is that we compute a linear function:
\[ \beta_0 + \beta_1 x, \]
and then map that to a probability using the inverse \(\text{logit}\) function:
\[ \frac{e^{\beta_0+\beta_1 x}}{1+e^{\beta_0+\beta_1 x}}. \]
Logistic vs Linear Regression
Let’s take a moment to compare linear and logistic regression.
In Linear regression we fit
\[ y_i = \beta_0 +\beta_1 x_i + \epsilon_i. \]
We do the fitting by minimizing the sum of squared errors \(\Vert\epsilon\Vert\). This can be done in closed form using either geometric arguments or by calculus.
Now, if \(\epsilon_i\) comes from a normal distribution with mean zero and some fixed variance, then minimizing the sum of squared errors is exactly the same as finding the maximum likelihood of the data with respect to the probability of the errors.
So, in the case of linear regression, it is a lucky fact that the MLE of \(\beta_0\) and \(\beta_1\) can be found by a closed-form calculation.
In Logistic regression we fit
\[ \text{logit}(p(x_i)) = \beta_0 + \beta_1 x_i. \]
with \(\text{P}(y_i=1\mid x_i)=p(x_i).\)
How should we choose parameters?
Here too, we use Maximum Likelihood Estimation of the parameters.
That is, we choose the parameter values that maximize the likelihood of the data given the model.
\[ \text{P}(y_i \mid x_i) = \left\{\begin{array}{lr}\text{logit}^{-1}(\beta_0 + \beta_1 x_i)& \text{if } y_i = 1\\ 1 - \text{logit}^{-1}(\beta_0 + \beta_1 x_i)& \text{if } y_i = 0\end{array}\right. \]
We can write this as a single expression:
\[ \text{P}(y_i \mid x_i) = \text{logit}^{-1}(\beta_0 + \beta_1 x_i)^{y_i} (1-\text{logit}^{-1}(\beta_0 + \beta_1 x_i))^{1-y_i}. \]
We then use this to compute the likelihood of parameters \(\beta_0\), \(\beta_1\):
\[ L(\beta_0, \beta_1 \mid x_i, y_i) = \text{logit}^{-1}(\beta_0 + \beta_1 x_i)^{y_i} (1-\text{logit}^{-1}(\beta_0 + \beta_1 x_i))^{1-y_i}, \]
which is a function that we can maximize, for example, with gradient descent.
Logistic Regression In Practice
So, in summary, we have:
Input pairs \((x_i,y_i)\)
Output parameters \(\widehat{\beta_0}\) and \(\widehat{\beta_1}\) that maximize the likelihood of the data given these parameters for the logistic regression model.
Method Maximum likelihood estimation, obtained by gradient descent.
The standard package will give us a coefficient \(\beta_i\) for each independent variable (feature).
If we want to include a constant (i.e., \(\beta_0\)) we need to add a column of 1s (just like in linear regression).
Code
'intercept'] = 1.0
df[= df.columns[1:]
train_cols train_cols
Index(['gre', 'gpa', 'rank', 'intercept'], dtype='object')
Code
= sm.Logit(df['admit'], df[train_cols])
logit
# fit the model
= logit.fit() result
Optimization terminated successfully.
Current function value: 0.574302
Iterations 6
Code
result.summary()
Dep. Variable: | admit | No. Observations: | 400 |
Model: | Logit | Df Residuals: | 396 |
Method: | MLE | Df Model: | 3 |
Date: | Sun, 27 Oct 2024 | Pseudo R-squ.: | 0.08107 |
Time: | 11:10:16 | Log-Likelihood: | -229.72 |
converged: | True | LL-Null: | -249.99 |
Covariance Type: | nonrobust | LLR p-value: | 8.207e-09 |
coef | std err | z | P>|z| | [0.025 | 0.975] | |
gre | 0.0023 | 0.001 | 2.101 | 0.036 | 0.000 | 0.004 |
gpa | 0.7770 | 0.327 | 2.373 | 0.018 | 0.135 | 1.419 |
rank | -0.5600 | 0.127 | -4.405 | 0.000 | -0.809 | -0.311 |
intercept | -3.4495 | 1.133 | -3.045 | 0.002 | -5.670 | -1.229 |
Notice that all of our independent variables are considered significant (no confidence intervals contain zero).
Using the Model
Note that by fitting a model to the data, we can make predictions for inputs that were not in the training data.
Furthermore, we can make a prediction of a probability for cases where we don’t have enough data to estimate the probability directly – e.g., for specific parameter values.
Let’s see how well the model fits the data.
We have three independent variables, so in each case we’ll use average values for the two that we aren’t evaluating.
GPA:
Code
= np.linspace(df.gpa.min(), df.gpa.max(), 10)
bins = df.groupby(np.digitize(df.gpa, bins))
groups = [result.predict([600, b, 2.5, 1.0]) for b in bins]
prob = plt.figure(figsize = (7, 5)).add_subplot()
ax
ax.plot(bins, prob)'o')
ax.plot(bins,groups.admit.mean(),=12)
ax.tick_params(labelsize'gpa', fontsize = 14)
ax.set_xlabel('P[admit]', fontsize = 14)
ax.set_ylabel('Marginal Effect of GPA', fontsize = 16); ax.set_title(
GRE Score:
Code
= [result.predict([b, 3.4, 2.5, 1.0]) for b in sorted(df.gre.unique())]
prob = plt.figure(figsize = (7, 5)).add_subplot()
ax sorted(df.gre.unique()), prob)
ax.plot('gre').mean()['admit'],'o')
ax.plot(df.groupby(=12)
ax.tick_params(labelsize'gre', fontsize = 14)
ax.set_xlabel('P[admit]', fontsize = 14)
ax.set_ylabel('Marginal Effect of GRE', fontsize = 16); ax.set_title(
Institution Rank:
Code
= [result.predict([600, 3.4, b, 1.0]) for b in range(1,5)]
prob = plt.figure(figsize = (7, 5)).add_subplot()
ax range(1,5), prob)
ax.plot('rank').mean()['admit'],'o')
ax.plot(df.groupby(=12)
ax.tick_params(labelsize'Rank', fontsize = 14)
ax.set_xlabel(0.5,4.5])
ax.set_xlim(['P[admit]', fontsize = 14)
ax.set_ylabel('Marginal Effect of Rank', fontsize = 16); ax.set_title(
Logistic Regression in Perspective
At the start of lecture we emphasized that logistic regression is concerned with estimating a probability model for discrete (0/1) data.
However, it may well be the case that we want to do something with the probability that amounts to classification.
For example, we may classify data items using a rule such as “Assign item \(x_i\) to Class 1 if \(p(x_i) > 0.5\)”.
For this reason, logistic regression could be considered a classification method.
In fact, that is what we did with Naive Bayes – we used it to estimate something like a probability, and then chose the class with the maximum value to create a classifier.
Let’s use our logistic regression as a classifier.
We want to ask whether we can correctly predict whether a student gets admitted to graduate school.
Let’s separate our training and test data:
Code
= model_selection.train_test_split(
X_train, X_test, y_train, y_test 'admit'],
df[train_cols], df[=0.4, random_state=1) test_size
Now, there are some standard metrics used when evaluating a binary classifier.
Let’s say our classifier is outputting “yes” when it thinks the student will be admitted.
There are four cases:
- Classifier says “yes”, and student is admitted: True Positive.
- Classifier says “yes”, and student is not admitted: False Positive.
- Classifier says “no”, and student is admitted: False Negative.
- Classifier says “no”, and student is not admitted: True Negative.
Precision is the fraction of “yes” classifications that are correct:
\[ \mbox{Precision} = \frac{\mbox{True Positives}}{\mbox{True Positives + False Positives}}. \]
Recall is the fraction of admits that we say “yes” to:
\[ \mbox{Recall} = \frac{\mbox{True Positives}}{\mbox{True Positives + False Negatives}}. \]
Code
def evaluate(y_train, X_train, y_test, X_test, threshold):
# learn model on training data
= sm.Logit(y_train, X_train)
logit = logit.fit(disp=False)
result
# make probability predictions on test data
= result.predict(X_test)
y_pred
# threshold probabilities to create classifications
= y_pred > threshold
y_pred
# report metrics
= metrics.precision_score(y_test, y_pred)
precision = metrics.recall_score(y_test, y_pred)
recall return precision, recall
= evaluate(y_train, X_train, y_test, X_test, 0.5)
precision, recall
print(f'Precision: {precision:0.3f}, Recall: {recall:0.3f}')
Precision: 0.586, Recall: 0.340
Now, let’s get a sense of average accuracy:
Code
= []
PR for i in range(20):
= model_selection.train_test_split(
X_train, X_test, y_train, y_test 'admit'],
df[train_cols], df[=0.4)
test_size0.5)) PR.append(evaluate(y_train, X_train, y_test, X_test,
Code
= np.mean([f[0] for f in PR])
avgPrec = np.mean([f[1] for f in PR])
avgRec print(f'Average Precision: {avgPrec:0.3f}, Average Recall: {avgRec:0.3f}')
Average Precision: 0.568, Average Recall: 0.230
Sometimes we would like a single value that describes the overall performance of the classifier.
For this, we take the harmonic mean of precision and recall, called F1 Score:
\[ \mbox{F1 Score} = 2 \;\;\frac{\mbox{Precision} \cdot \mbox{Recall}}{\mbox{Precision} + \mbox{Recall}}. \]
Using this, we can evaluate other settings for the threshold.
Code
import warnings
"ignore")
warnings.filterwarnings(def evalThresh(df, thresh):
= []
PR for i in range(20):
= model_selection.train_test_split(
X_train, X_test, y_train, y_test 'admit'],
df[train_cols], df[=0.4)
test_size
PR.append(evaluate(y_train, X_train, y_test, X_test, thresh))= np.mean([f[0] for f in PR])
avgPrec = np.mean([f[1] for f in PR])
avgRec return 2 * (avgPrec * avgRec) / (avgPrec + avgRec), avgPrec, avgRec
= np.linspace(0.05, 0.8, 50)
tvals = [evalThresh(df, tval)[0] for tval in tvals] f1vals
Code
plt.plot(tvals,f1vals)'F1 Score')
plt.ylabel('Threshold for Classification')
plt.xlabel('F1 as a function of Threshold'); plt.title(
Based on this plot, we can say that the best classification threshold appears to be around 0.3, where precision and recall are:
Code
= evalThresh(df, 0.3)
F1, Prec, Rec print('Best Precision: {:0.3f}, Best Recall: {:0.3f}'.format(Prec, Rec))
Best Precision: 0.418, Best Recall: 0.683
The example here is based on http://blog.yhathq.com/posts/logistic-regression-and-python.html where you can find additional details.
Recap
- Logistic regression is used to predict a probability.
- It is a linear model for the log-odds.
- It is fit by maximum likelihood.
- It can be evaluated as a classifier.