<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>ai &#8211; paologironi blog</title>
	<atom:link href="https://www.gironi.it/blog/en/category/ai/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.gironi.it/blog</link>
	<description>Scattered notes on (retro) computing, data analysis, statistics, SEO, and things that change</description>
	<lastBuildDate>Mon, 12 Oct 2026 08:32:12 +0000</lastBuildDate>
	<language>en-GB</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	
	<item>
		<title>The Gradient Descent Algorithm Explained Clearly: From Intuition to Practice</title>
		<link>https://www.gironi.it/blog/en/the-gradient-descent-algorithm-explained-simply/</link>
					<comments>https://www.gironi.it/blog/en/the-gradient-descent-algorithm-explained-simply/#respond</comments>
		
		<dc:creator><![CDATA[paolo]]></dc:creator>
		<pubDate>Mon, 21 Aug 2023 12:25:14 +0000</pubDate>
				<category><![CDATA[statistics]]></category>
		<category><![CDATA[ai]]></category>
		<category><![CDATA[gradient descent]]></category>
		<guid isPermaLink="false">https://www.gironi.it/blog/?p=3266</guid>

					<description><![CDATA[A blindfolded person on a mountain Imagine standing on a mountainous terrain, completely blindfolded. Your goal: reach the lowest point in the valley. You can&#8217;t see anything, but you can feel the slope of the ground beneath your feet. What do you do? You move in the direction where the ground goes down, one step &#8230; <a href="https://www.gironi.it/blog/en/the-gradient-descent-algorithm-explained-simply/" class="more-link">Continue reading<span class="screen-reader-text"> "The Gradient Descent Algorithm Explained Clearly: From Intuition to Practice"</span></a>]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">A blindfolded person on a mountain</h2>



<p class="wp-block-paragraph">Imagine standing on a mountainous terrain, completely blindfolded. Your goal: reach the lowest point in the valley. You can&#8217;t see anything, but you can feel the slope of the ground beneath your feet. What do you do? You move in the direction where the ground goes down, one step at a time. If it slopes more steeply to the left, you go left. If it drops more to the right, you go right. With each step, you feel the slope again and redirect yourself.</p>



<p class="wp-block-paragraph">This strategy, so simple and natural, is exactly what neural networks use to learn. Every time an AI model improves &mdash; learning to recognize a face, translate a sentence, or generate text &mdash; it does so by descending through a mathematical landscape, one step at a time, following the slope.</p>



<p class="wp-block-paragraph">It&#8217;s called <strong>gradient descent</strong>, and it&#8217;s arguably the most important algorithm in modern machine learning.</p>



<figure style="margin: 1.5em 0;text-align: center"><img decoding="async" src="https://www.gironi.it/blog/wp-content/uploads/2026/03/slide-esploratore-bendato.jpg" alt="Infographic: the blindfolded explorer metaphor for gradient descent, with three steps: Sensor, Action, Cycle" style="max-width: 100%;border: 1px solid #e0e0e0;border-radius: 6px" /></figure>



<span id="more-3266"></span>



<h2 class="wp-block-heading" id="storia">From Cauchy to Neural Networks: A Brief History</h2>



<p class="wp-block-paragraph">The idea of following the slope to find a minimum has surprisingly ancient roots. In <strong>1847</strong>, the French mathematician Augustin-Louis Cauchy published a method for solving systems of equations that, in essence, is already gradient descent: compute the direction of steepest ascent of a function and move in the opposite direction. Cauchy was not thinking about neural networks &mdash; their appearance was more than a century away &mdash; but he had formalized the principle that still powers artificial intelligence today.</p>



<p class="wp-block-paragraph">For over a century the idea remained confined to pure mathematics. The breakthrough came in <strong>1951</strong>, when Herbert Robbins and Sutton Monro proposed a <strong>stochastic</strong> version of the method: instead of computing the gradient on the entire problem, it is estimated on a random sample. This is the seed of what we now call <em>Stochastic Gradient Descent</em> (SGD) &mdash; we will discuss it in the final section. A few years later, in <strong>1958</strong>, Frank Rosenblatt built the <strong>perceptron</strong>, the first machine learning model that uses gradient descent to learn from data. The excitement was enormous, but short-lived: in 1969, Minsky and Papert demonstrated the perceptron&#8217;s limitations, and interest in neural networks collapsed. This is the so-called <em>AI winter</em>.</p>



<p class="wp-block-paragraph">The revival came in <strong>1986</strong>, when David Rumelhart, Geoffrey Hinton, and Ronald Williams published the <strong>backpropagation</strong> algorithm: an efficient way to compute the gradient in neural networks with many layers. It was the key that unlocked deep learning. From that moment on, gradient descent was no longer a theoretical exercise but the practical tool used to train neural networks. The final piece of this story was set by Diederik Kingma and Jimmy Ba in <strong>2014</strong>, with the <strong>Adam</strong> optimizer &mdash; which we will encounter again in the final section of this article. Nearly two centuries separate Cauchy from Adam, yet the principle has remained the same: feel the slope, take a step in the opposite direction.</p>



<div style="border: 1px solid #ccc;padding: 1.2em 1.5em;margin: 1.5em 0;border-radius: 6px">
<h3 style="margin-top: 0">What We&#8217;ll Cover</h3>
<ul>
<li><a href="#storia">From Cauchy to Neural Networks: A Brief History</a></li>
<li><a href="#the-math">The Math, Explained Geometrically</a></li>
<li><a href="#learning-rate">The Learning Rate and Convergence</a></li>
<li><a href="#pitfalls">What Can Go Wrong: Geometric Intuition</a></li>
<li><a href="#scale">From a Parabola to ChatGPT</a></li>
<li><a href="#evolutions">The Evolutions: Better Shoes for Our Explorer</a></li>
</ul>
</div>



<h2 class="wp-block-heading" id="the-math">The Math, Explained Geometrically</h2>



<h3 class="wp-block-heading">The Cost Function: Measuring How Wrong the Model Is</h3>



<p class="wp-block-paragraph">Before we descend, we need to know <strong>what</strong> we are minimizing. The blindfolded person is looking for the lowest point in the valley &mdash; but in machine learning, what exactly is that valley?</p>



<p class="wp-block-paragraph">Let&#8217;s take a concrete example. Suppose we want to predict the price of a house knowing only its area. We have five houses for which we know the area and the actual price, and our model is the simplest possible: a line through the origin, <strong>price = m &times; area</strong>, where <em>m</em> is the only parameter to adjust.</p>



<p class="wp-block-paragraph">For each value of <em>m</em>, the model makes a prediction. If <em>m</em> is too low, the predictions underestimate the actual prices; if it is too high, they overestimate them. We need a number that tells us <em>how much</em> the model is getting wrong: this is the <strong>cost function</strong> (or <em>loss function</em>).</p>



<p class="wp-block-paragraph">The most widely used cost function is the <strong>Mean Squared Error</strong> (MSE): for each house, we compute the difference between the predicted price and the actual price, square it, and take the average of all these errors. In formula:</p>



<p class="wp-block-paragraph"><strong>MSE(m) = (1/n) &sum;<sub>i</sub> (price<sub>i</sub> &minus; m &times; area<sub>i</sub>)&sup2;</strong></p>



<p class="wp-block-paragraph">If we plot MSE(m) as <em>m</em> varies, we get a parabola-shaped curve: the same parabola we will use shortly as an example of gradient descent. This is no coincidence &mdash; <strong>the parabola is the cost function</strong>, and its lowest point is the value of <em>m</em> that makes the model as good as possible.</p>



<p class="wp-block-paragraph">The examples that follow are available in both R and Python &mdash; choose whichever language you&#8217;re more comfortable with.</p>



<p class="wp-block-paragraph">Let&#8217;s compute the cost function in R for our house example:</p>



<pre><code class="language-r"># Cost function: predicting the price of a house
area &lt;- c(50, 70, 90, 120, 150)
price &lt;- c(150, 200, 260, 340, 400)  # thousands of euros

# Model: price = m * area
# MSE cost function as m varies
m_values &lt;- seq(1, 4, by = 0.01)
mse &lt;- sapply(m_values, function(m) mean((price - m * area)^2))

plot(m_values, mse, type = "l", lwd = 2, col = "steelblue",
     xlab = "m (slope)", ylab = "MSE",
     main = "Cost function as m varies")
m_best &lt;- m_values[which.min(mse)]
abline(v = m_best, col = "red", lty = 2)
cat("The value of m that minimizes the error:", round(m_best, 2), "\n")</code></pre>



<p class="wp-block-paragraph">Let&#8217;s verify in Python:</p>



<pre><code class="language-python">import numpy as np
import matplotlib.pyplot as plt

# Cost function: predicting the price of a house
area = np.array([50, 70, 90, 120, 150])
price = np.array([150, 200, 260, 340, 400])  # thousands of euros

# Model: price = m * area
# MSE cost function as m varies
m_values = np.linspace(1, 4, 301)  # equivalent to seq(1, 4, by=0.01) in R
mse = np.array([np.mean((price - m * area)**2) for m in m_values])

plt.plot(m_values, mse, lw=2, color="steelblue")
plt.xlabel("m (slope)")
plt.ylabel("MSE")
plt.title("Cost function as m varies")
m_best = m_values[np.argmin(mse)]
plt.axvline(m_best, color="red", linestyle="--")
plt.show()
print(f"The value of m that minimizes the error: {m_best:.2f}")</code></pre>



<p class="wp-block-paragraph">Now we know what to minimize: the cost function. The gradient tells us <em>how</em>.</p>



<h3 class="wp-block-heading">Derivative: the slope beneath your feet</h3>



<p class="wp-block-paragraph">When you have a function of a single variable &mdash; think of it as a trail that goes up and down &mdash; the <strong>derivative</strong> at a point tells you how steep the trail is at that point. If the derivative is positive, you&#8217;re going uphill. If it&#8217;s negative, you&#8217;re going downhill. If it&#8217;s zero, you&#8217;re on flat ground: perhaps a peak, perhaps a dip.</p>



<p class="wp-block-paragraph">Concrete example: the function f(x) = x&sup2; describes a parabola. Its derivative is f'(x) = 2x. If you&#8217;re at x = 3, the derivative is 6: you&#8217;re climbing steeply. At x = -1, the derivative is -2: you&#8217;re going downhill. At x = 0, the derivative is zero: you&#8217;re at the lowest point.</p>



<figure style="margin: 1.5em 0;text-align: center"><img decoding="async" src="https://www.gironi.it/blog/wp-content/uploads/2026/03/slide-derivata-pendenza.jpg" alt="Graph of the parabola f(x)=x² with three annotated points: A (x=3, derivative=6, steep slope), B (x=-1, derivative=-2, gentle slope), C (x=0, derivative=0, minimum)" style="max-width: 100%;border: 1px solid #e0e0e0;border-radius: 6px" /></figure>



<h3 class="wp-block-heading">The gradient: a multidimensional compass</h3>



<p class="wp-block-paragraph">In practice, the functions we care about don&#8217;t depend on just one variable. A machine learning model can have hundreds, thousands, or billions of parameters. The landscape is no longer a trail but a surface in a high-dimensional space &mdash; impossible to visualize, but perfectly manageable with mathematics.</p>



<p class="wp-block-paragraph">The <strong>gradient</strong> is the vector that collects all partial derivatives of the function with respect to each parameter. If the function depends on two variables (x, y), the gradient is:</p>



<p class="wp-block-paragraph"><strong>&nabla;f = (&#8706;f/&#8706;x, &#8706;f/&#8706;y)</strong></p>



<p class="wp-block-paragraph">If it depends on a thousand variables, the gradient is a vector with a thousand components. In every case, the gradient points in the direction of steepest ascent. To find the minimum, just go in the opposite direction: <strong>minus the gradient</strong>.</p>



<p class="wp-block-paragraph">Here&#8217;s the fundamental formula of gradient descent:</p>



<p class="wp-block-paragraph"><strong>&theta;<sub>new</sub> = &theta;<sub>old</sub> &minus; &alpha; &middot; &nabla;f(&theta;)</strong></p>



<p class="wp-block-paragraph">Let&#8217;s break it down piece by piece:</p>



<ul class="wp-block-list">
<li><strong>&theta;</strong> represents the model&#8217;s parameters &mdash; the &#8220;knobs&#8221; the algorithm adjusts to improve</li>
<li><strong>&nabla;f(&theta;)</strong> is the gradient: it indicates the direction of steepest ascent at the current point</li>
<li><strong>&alpha;</strong> (alpha) is the <em>learning rate</em>: the size of the step we take at each iteration</li>
<li>The <strong>minus</strong> sign makes us go in the opposite direction of the gradient, i.e. downhill</li>
</ul>



<p class="wp-block-paragraph">That&#8217;s all there is to it. Compute where you&#8217;re going uphill, take a step in the opposite direction, repeat.</p>



<figure style="margin: 1.5em 0;text-align: center"><img decoding="async" src="https://www.gironi.it/blog/wp-content/uploads/2026/03/slide-anatomia-passo.jpg" alt="Visual breakdown of the gradient descent formula: theta (model knobs), alpha (learning rate as caliper), nabla f (gradient as compass), minus sign (opposite direction)" style="max-width: 100%;border: 1px solid #e0e0e0;border-radius: 6px" /></figure>



<h3 class="wp-block-heading">A numerical example in R</h3>



<p class="wp-block-paragraph">Let&#8217;s see gradient descent in action on the function f(x) = x&sup2;. We know the minimum is at x = 0. Can the algorithm find it starting from a random point?</p>



<pre><code class="language-r"># Gradient descent on f(x) = x^2
# The derivative is f'(x) = 2x

f &lt;- function(x) x^2         # objective function
grad_f &lt;- function(x) 2 * x  # derivative (gradient in 1D)

x &lt;- 10              # starting point
alpha &lt;- 0.1          # learning rate
n_iter &lt;- 50          # number of iterations
path &lt;- numeric(n_iter)

for (i in 1:n_iter) {
  path[i] &lt;- x
  x &lt;- x - alpha * grad_f(x)  # the fundamental rule
}

cat("Starting point: 10\n")
cat("After 50 iterations: x =", round(x, 8), "\n")
cat("Function value:", round(f(x), 10), "\n")

# Visualization
curve(x^2, from = -11, to = 11, lwd = 2, col = "steelblue",
      main = "Gradient descent on f(x) = x^2",
      xlab = "x", ylab = "f(x)")
points(path, path^2, col = "red", pch = 19, cex = 0.7)
lines(path, path^2, col = "red", lty = 2)</code></pre>



<p class="wp-block-paragraph">Let&#8217;s verify in Python:</p>



<pre><code class="language-python">import numpy as np
import matplotlib.pyplot as plt

# Gradient descent on f(x) = x^2
f = lambda x: x**2           # objective function
grad_f = lambda x: 2 * x     # derivative (gradient in 1D)

x = 10.0            # starting point
alpha = 0.1          # learning rate
n_iter = 50          # number of iterations
path = np.zeros(n_iter)

for i in range(n_iter):
    path[i] = x
    x = x - alpha * grad_f(x)  # the fundamental rule

print(f"Starting point: 10")
print(f"After 50 iterations: x = {x:.8f}")
print(f"Function value: {f(x):.10f}")

# Visualization
xs = np.linspace(-11, 11, 200)
plt.plot(xs, xs**2, lw=2, color="steelblue")
plt.plot(path, path**2, "ro--", markersize=4)
plt.title("Gradient descent on f(x) = x\u00b2")
plt.xlabel("x")
plt.ylabel("f(x)")
plt.show()</code></pre>



<p class="wp-block-paragraph">Running this code, you can see the algorithm start from x = 10 and quickly converge toward x = 0. The first steps are large (the slope is steep), then they shrink as we approach the bottom of the parabola. After 50 iterations, x is essentially zero.</p>



<h2 class="wp-block-heading" id="learning-rate">The Learning Rate: Big Steps or Small Steps?</h2>



<p class="wp-block-paragraph">Back to our blindfolded person. How big is the step they take at each iteration? This is exactly the question of the <strong>learning rate</strong> (&alpha;), and the answer is less obvious than it seems.</p>



<p class="wp-block-paragraph"><strong>Steps too small</strong> (&alpha; very low): the person moves with extreme caution, shifting their foot just a few centimeters at a time. They&#8217;ll eventually reach the bottom of the valley, but it could take forever. In machine learning, this means extremely long training times and high computational costs.</p>



<p class="wp-block-paragraph"><strong>Steps too large</strong> (&alpha; too high): the person takes enormous leaps. Instead of gently descending into the valley, they overshoot it, end up on the other side, bounce back, and keep oscillating without ever settling. In extreme cases, the jumps get bigger and bigger and the person ends up higher than where they started. In machine learning, this is called <em>divergence</em>: the model gets worse instead of better.</p>



<p class="wp-block-paragraph"><strong>Steps just right</strong>: a good learning rate allows you to descend quickly without oscillating. In practice, finding the right value requires experimentation. It&#8217;s one of the most artisanal aspects of machine learning.</p>



<figure style="margin: 1.5em 0;text-align: center"><img decoding="async" src="https://www.gironi.it/blog/wp-content/uploads/2026/03/slide-learning-rate.jpg" alt="Visual comparison of three learning rates on the parabola: alpha=0.01 too slow, alpha=0.1 efficient convergence, alpha=0.9 chaotic oscillation" style="max-width: 100%;border: 1px solid #e0e0e0;border-radius: 6px" /></figure>



<h3 class="wp-block-heading">Convergence: knowing when to stop</h3>



<p class="wp-block-paragraph">How does the blindfolded person know they&#8217;ve arrived? They feel the ground is flat in all directions: the gradient is (nearly) zero. In practice, the algorithm stops when the improvement between iterations becomes negligible, or when it has reached a maximum number of iterations.</p>



<p class="wp-block-paragraph">The most common stopping criteria are:</p>



<ul class="wp-block-list">
<li>The gradient norm drops below a minimum threshold (the terrain is nearly flat)</li>
<li>The difference in f(&theta;) between two consecutive iterations is smaller than a fixed tolerance</li>
<li>The maximum number of iterations has been reached (computational budget exhausted)</li>
</ul>



<h3 class="wp-block-heading">The effect of the learning rate: a visual comparison</h3>



<p class="wp-block-paragraph">This R code shows the effect of three different learning rate values on the same function:</p>



<pre><code class="language-r"># Comparing three learning rates on f(x) = x^2
gradient_descent &lt;- function(x0, alpha, n_iter = 30) {
  x &lt;- x0
  path &lt;- numeric(n_iter)
  for (i in 1:n_iter) {
    path[i] &lt;- x
    x &lt;- x - alpha * 2 * x  # theta_new = theta_old - alpha * grad
  }
  return(path)
}

x0 &lt;- 8  # same starting point for all

# Three different learning rates
slow   &lt;- gradient_descent(x0, alpha = 0.01)   # too small
right  &lt;- gradient_descent(x0, alpha = 0.1)    # good compromise
fast   &lt;- gradient_descent(x0, alpha = 0.9)    # nearly unstable

# Visualization
par(mfrow = c(1, 3))

# alpha = 0.01 (too slow)
curve(x^2, from = -10, to = 10, lwd = 2, col = "steelblue",
      main = expression(paste(alpha, " = 0.01 (too slow)")))
points(slow, slow^2, col = "red", pch = 19, cex = 0.6)
lines(slow, slow^2, col = "red", lty = 2)

# alpha = 0.1 (just right)
curve(x^2, from = -10, to = 10, lwd = 2, col = "steelblue",
      main = expression(paste(alpha, " = 0.1 (good compromise)")))
points(right, right^2, col = "darkgreen", pch = 19, cex = 0.6)
lines(right, right^2, col = "darkgreen", lty = 2)

# alpha = 0.9 (nearly unstable)
curve(x^2, from = -10, to = 10, lwd = 2, col = "steelblue",
      main = expression(paste(alpha, " = 0.9 (nearly unstable)")))
points(fast, fast^2, col = "orange", pch = 19, cex = 0.6)
lines(fast, fast^2, col = "orange", lty = 2)

par(mfrow = c(1, 1))</code></pre>



<p class="wp-block-paragraph">Let&#8217;s compare in Python:</p>



<pre><code class="language-python">import numpy as np
import matplotlib.pyplot as plt

def gradient_descent(x0, alpha, n_iter=30):
    x = x0
    path = np.zeros(n_iter)
    for i in range(n_iter):
        path[i] = x
        x = x - alpha * 2 * x  # theta_new = theta_old - alpha * grad
    return path

x0 = 8.0  # same starting point for all
slow  = gradient_descent(x0, alpha=0.01)   # too small
right = gradient_descent(x0, alpha=0.1)    # good compromise
fast  = gradient_descent(x0, alpha=0.9)    # nearly unstable

fig, axes = plt.subplots(1, 3, figsize=(14, 4))
xs = np.linspace(-10, 10, 200)
for ax, data, color, title in zip(axes,
        [slow, right, fast],
        ["red", "darkgreen", "orange"],
        ["\u03b1 = 0.01 (too slow)", "\u03b1 = 0.1 (good compromise)",
         "\u03b1 = 0.9 (nearly unstable)"]):
    ax.plot(xs, xs**2, lw=2, color="steelblue")
    ax.plot(data, data**2, "o--", color=color, markersize=4)
    ax.set_title(title)
plt.tight_layout()
plt.show()</code></pre>



<p class="wp-block-paragraph">With &alpha; = 0.01, the red dots move sluggishly: after 30 iterations we&#8217;re still far from the minimum. With &alpha; = 0.1, convergence is fast and clean. With &alpha; = 0.9, the algorithm oscillates visibly at each step, bouncing from one side of the parabola to the other before settling &mdash; a slightly higher learning rate would diverge entirely.</p>



<h2 class="wp-block-heading" id="pitfalls">What Can Go Wrong: Geometric Intuition</h2>



<p class="wp-block-paragraph">The mathematical landscape of a real model is not a nice symmetric parabola. It&#8217;s wild terrain, with secondary valleys, ridges, plateaus, and shapes that defy imagination. Here are the classic problems, explained through our landscape analogy.</p>



<figure style="margin: 1.5em 0;text-align: center"><img decoding="async" src="https://www.gironi.it/blog/wp-content/uploads/2026/03/slide-insidie-topografiche.jpg" alt="Three pitfalls of gradient descent visualized in 3D: local minimum (secondary valley), saddle point (zero gradient but not a minimum), narrow valley (inefficient zigzag)" style="max-width: 100%;border: 1px solid #e0e0e0;border-radius: 6px" /></figure>



<h3 class="wp-block-heading">Local minima: secondary valleys</h3>



<p class="wp-block-paragraph">Imagine terrain with multiple dips: one deep valley (the global minimum) and several shallower hollows (local minima). The blindfolded person has no way of knowing whether the valley they&#8217;re in is the deepest one. They feel flat ground beneath their feet and stop, convinced they&#8217;ve arrived. But they might be in a shallow hollow while the true minimum lies somewhere else entirely.</p>



<p class="wp-block-paragraph">In practice, this is a less severe problem than once thought. Modern neural networks have so many parameters that local minima tend to have objective function values similar to the global minimum. It&#8217;s like terrain with many valleys, but all at roughly the same altitude: ending up in any one of them works fine.</p>



<h3 class="wp-block-heading">Saddle points: the horse saddle</h3>



<p class="wp-block-paragraph">A more insidious problem are <strong>saddle points</strong>. Imagine sitting on a horse saddle: if you move forward or backward, you descend; if you move left or right, you ascend. At that point the gradient is zero &mdash; the terrain feels flat &mdash; but you&#8217;re not at a minimum. You&#8217;re at a point that is a minimum in some directions and a maximum in others.</p>



<p class="wp-block-paragraph">In high-dimensional spaces, saddle points are far more common than local minima. Fortunately, modern variants of gradient descent (with some noise or momentum, as we&#8217;ll see) generally manage to escape saddle points.</p>



<h3 class="wp-block-heading">Narrow valleys: the zigzag</h3>



<p class="wp-block-paragraph">Imagine a very narrow, elongated valley, like a canyon. The gradient points almost perpendicularly to the canyon walls, not along the canyon toward the bottom. The blindfolded person ends up bouncing from one wall to the other, making an inefficient zigzag instead of walking straight toward the bottom.</p>



<p class="wp-block-paragraph">This happens when the problem&#8217;s variables have very different scales: some change rapidly, others slowly. It&#8217;s a common issue in practice, and one of the main motivations for advanced optimizers like Adam, which we&#8217;ll see in the final section.</p>



<h2 class="wp-block-heading" id="scale">From a Parabola to ChatGPT</h2>



<p class="wp-block-paragraph">So far we&#8217;ve seen gradient descent on a parabola: a problem with a single variable. It&#8217;s the simplest possible case. But the beauty of this algorithm is that it works in exactly the same way at any scale.</p>



<h3 class="wp-block-heading">The scale of parameters</h3>



<p class="wp-block-paragraph">Here&#8217;s how the number of parameters grows as models become more complex:</p>



<ul class="wp-block-list">
<li><strong>Simple linear regression</strong>: 2 parameters (slope and intercept). The landscape is a 3D surface, easy to visualize.</li>
<li><strong>Neural network for handwritten digit recognition</strong>: ~100,000 parameters. The landscape has 100,000 dimensions.</li>
<li><strong>ResNet-50</strong> (image classification, 2015): ~25 million parameters.</li>
<li><strong>GPT-3</strong> (ChatGPT&#8217;s predecessor): 175 billion parameters.</li>
<li><strong>GPT-4</strong> and frontier models (2023-2025): estimated over one trillion parameters.</li>
</ul>



<p class="wp-block-paragraph">The principle is identical: compute the gradient, take a step in the opposite direction, repeat. What changes is the scale of the computation. GPT-4&#8217;s gradient is a vector with over a trillion components, computed on billions of text fragments, using thousands of processors in parallel. But the formula is the same one we saw on the parabola.</p>



<h3 class="wp-block-heading">Where you see it in action (without knowing it)</h3>



<p class="wp-block-paragraph">Every time you interact with an AI system, gradient descent has been working behind the scenes:</p>



<ul class="wp-block-list">
<li><strong>Netflix and Spotify</strong> recommending what to watch or listen to: recommendation models are trained with gradient descent on billions of user interactions</li>
<li><strong>Google Translate</strong> and automatic translators: neural networks with hundreds of millions of parameters, optimized with gradient descent on massive parallel text corpora</li>
<li><strong>Voice assistants</strong> (Siri, Alexa): speech recognition uses deep neural networks, trained with the same algorithm</li>
<li><strong>Self-driving cars</strong>: the networks that recognize pedestrians, traffic lights, and lane markings are trained with gradient descent variants</li>
<li><strong>ChatGPT, Claude, Gemini</strong>: Large Language Models are the most extreme case &mdash; gradient descent applied to billions of parameters on trillions of text tokens</li>
</ul>



<p class="wp-block-paragraph">The key message is this: <strong>the power of modern AI lies not in the complexity of the optimization algorithm, but in scale</strong>. Gradient descent is conceptually simple. What made the AI revolution possible is the ability to apply it to enormous models on enormous amounts of data, thanks to ever more powerful hardware.</p>



<h2 class="wp-block-heading" id="evolutions">The Evolutions: Better Shoes for Our Explorer</h2>



<p class="wp-block-paragraph">Vanilla gradient descent &mdash; what we&#8217;ve seen so far &mdash; works, but it has the limitations we described: it can be slow, it can oscillate, it can get stuck. Over the years, researchers have developed variants that address these problems. Without diving into the formulas, here are the key ideas.</p>



<h3 class="wp-block-heading">Stochastic Gradient Descent (SGD)</h3>



<p class="wp-block-paragraph">Instead of computing the gradient on the entire dataset at each step (computationally very expensive), SGD computes it on a small random sample (<em>mini-batch</em>). It&#8217;s as if the blindfolded person, instead of feeling the entire terrain around them, only sampled a few random points. The slope estimate is noisy but correct on average, and the computation speed is enormously greater. The noise, paradoxically, is also useful: it helps escape local minima and saddle points.</p>



<h3 class="wp-block-heading">Momentum</h3>



<p class="wp-block-paragraph">Imagine a ball rolling down the hill instead of a person walking. The ball builds up velocity: if the slope continues in the same direction, it accelerates. If the slope changes direction, the ball slows down before reversing. This is momentum: the algorithm &#8220;remembers&#8221; the direction it was moving and adds the current gradient to it. The result is that it crosses flat zones faster and oscillates less in narrow valleys.</p>



<h3 class="wp-block-heading">Adam: the Swiss army knife</h3>



<p class="wp-block-paragraph"><strong>Adam</strong> (<em>Adaptive Moment Estimation</em>) combines the momentum idea with a learning rate that adapts automatically for each parameter. Parameters that change little get larger steps; those that change a lot get smaller steps. It&#8217;s as if the blindfolded person had smart shoes that adjust the step length based on the terrain under each foot.</p>



<p class="wp-block-paragraph">Adam has become the de facto standard for training most modern neural networks. It&#8217;s robust, requires little manual tuning, and works well across a wide range of problems. Nearly all the models you use every day &mdash; from Spotify to ChatGPT &mdash; were trained with Adam or one of its variants.</p>



<h2 class="wp-block-heading">Back to the Mountain</h2>



<p class="wp-block-paragraph">The blindfolded person we started with now has better shoes. They have a ball that builds velocity instead of legs that take rigid steps. They have soles that automatically adapt to the terrain. And above all, they&#8217;re not walking on a mountain with two or three dimensions: they&#8217;re walking through a landscape with billions of dimensions.</p>



<p class="wp-block-paragraph">But the principle is exactly the same. Feel the slope. Take a step where it goes down. Repeat.</p>



<p class="wp-block-paragraph">Gradient descent is not a spectacular algorithm. It lacks the elegant complexity of a genetic algorithm or the narrative charm of adversarial networks. It&#8217;s a mechanical procedure, almost trivial. But it&#8217;s the mechanical procedure upon which the entire artificial intelligence revolution rests. From Netflix suggestions to image-generating models, from self-driving cars to simultaneous translation, it all comes down to this: a function to minimize, a gradient to compute, a step to take.</p>



<p class="wp-block-paragraph">The next time a voice assistant understands your question, or an automatic translator nails a nuance, remember: behind the scenes, a very sophisticated version of our blindfolded person has walked billions of steps through a landscape with billions of dimensions. And found a valley deep enough to be useful.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">Further Reading</h3>



<p class="wp-block-paragraph">To place gradient descent in the broader context of statistical learning, <a href="https://www.amazon.it/dp/1461471370?tag=consulenzeinf-21&#038;ascsubtag=the-gradient-descent-algorithm-explained-simply" rel="nofollow sponsored noopener" target="_blank"><em>An Introduction to Statistical Learning</em></a> by James, Witten, Hastie and Tibshirani is the read we recommend: rigorous, yet always practice-oriented.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.gironi.it/blog/en/the-gradient-descent-algorithm-explained-simply/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Logistic Regression: What It Is, How to Read Odds Ratios, and an Example in R</title>
		<link>https://www.gironi.it/blog/en/logistic-regression-predicting-the-outcome-of-an-event/</link>
					<comments>https://www.gironi.it/blog/en/logistic-regression-predicting-the-outcome-of-an-event/#respond</comments>
		
		<dc:creator><![CDATA[paolo]]></dc:creator>
		<pubDate>Thu, 12 Jan 2023 12:07:22 +0000</pubDate>
				<category><![CDATA[statistics]]></category>
		<category><![CDATA[ai]]></category>
		<category><![CDATA[logistic regression]]></category>
		<guid isPermaLink="false">https://www.gironi.it/blog/?p=3280</guid>

					<description><![CDATA[Sooner or later, every traffic analysis arrives at the question that really matters: did this visit turn into a customer, yes or no? We don&#8217;t care how much a session converted, we care whether it did. And right behind it comes the next question: which sessions are more likely to convert? The ones arriving from &#8230; <a href="https://www.gironi.it/blog/en/logistic-regression-predicting-the-outcome-of-an-event/" class="more-link">Continue reading<span class="screen-reader-text"> "Logistic Regression: What It Is, How to Read Odds Ratios, and an Example in R"</span></a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Sooner or later, every traffic analysis arrives at the question that really matters: did this visit turn into a customer, yes or no? We don&#8217;t care <em>how much</em> a session converted, we care <em>whether</em> it did. And right behind it comes the next question: which sessions are more likely to convert? The ones arriving from a search for the brand name? The ones browsing more pages? The ones on desktop?</p>



<p class="wp-block-paragraph">With <a href="https://www.gironi.it/blog/en/correlation-and-regression-analysis-linear-regression/">linear regression</a> we learned to predict a number. Here, though, the outcome is a switch, on or off, and a straight line is not the right tool: we&#8217;ll see it even ends up predicting negative probabilities. The right tool is <strong>logistic regression</strong>, one of the most widely used models of all, from marketing to medicine. Let&#8217;s build it together, starting from an example with conversions.</p>



<span id="more-3280"></span>



<p class="wp-block-paragraph"><strong>In short:</strong> <strong>logistic regression</strong> is the statistical model that estimates the <strong>probability of a binary outcome</strong> (converts or not, clicks or not) from one or more variables. Instead of a straight line it uses the <strong>logistic function</strong>, an S-shaped curve that always stays between 0 and 1. The coefficients are expressed in <em>log-odds</em>: exponentiated, they become <strong>odds ratios</strong>, the factor by which the odds of the event are multiplied when a variable increases by one unit. In R it is fitted with <code>glm(..., family = binomial)</code> and predicted probabilities come from <code>predict(..., type = "response")</code>. To classify, we then need a <strong>threshold</strong>, which with rare events has to be chosen with care: 0.5 is almost never the right one.</p>



<p class="wp-block-paragraph"><strong>What we&#8217;ll cover</strong>:</p>



<ul class="wp-block-list"><li><a href="#why-not-a-line">Why isn&#8217;t a straight line enough?</a></li><li><a href="#logistic-equation">From probability to odds to logit: the logistic equation</a></li><li><a href="#example-r">How do we fit a logistic regression in R?</a></li><li><a href="#odds-ratio">How do we read the coefficients? Odds ratios</a></li><li><a href="#threshold">From probabilities to decisions: where to put the threshold?</a></li><li><a href="#assumptions">The conditions for using it</a></li><li><a href="#faq">Frequently asked questions</a></li><li><a href="#try-it-yourself">Try it yourself</a></li><li><a href="#further-reading">Further reading</a></li></ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="why-not-a-line">Why isn&#8217;t a straight line enough?</h2>



<p class="wp-block-paragraph">Let&#8217;s take 5,000 organic sessions on an e-commerce site. For each one we know three things: how many pages it viewed, whether it came from a smartphone or a desktop, and whether the starting query contained the brand name. And we know whether the session ended with a purchase: 271 converted, 5.4%. The data are simulated, so we know the truth and can check whether the model recovers it.</p>



<p class="wp-block-paragraph">The most immediate idea is to treat conversion as a number, 1 if it happened and 0 if not, and run a least-squares line through it. It even has a name, the <em>linear probability model</em>, and in some cases it works acceptably. But let&#8217;s see what happens with our data.</p>



<figure class="wp-block-image size-large"><img decoding="async" src="https://www.gironi.it/blog/wp-content/uploads/2026/10/regressione-logistica-retta-curva-en.png" alt="Observed conversion rate by number of pages viewed (the bubbles, whose area shows how many sessions there are) with the least-squares line and the logistic curve. The line always adds 1.9 points per page and stays below the data on the right; the curve rises slowly and then speeds up, following the bubbles."/><figcaption class="wp-element-caption">Observed conversion rate by number of pages viewed (the bubbles, whose area shows how many sessions there are) with the least-squares line and the logistic curve. The line always adds 1.9 points per page and stays below the data on the right; the curve rises slowly and then speeds up, following the bubbles.</figcaption></figure>



<p class="wp-block-paragraph">The line says every extra page is always worth 1.9 percentage points of conversion, whether we go from 1 to 2 pages or from 8 to 9. The data tell another story: sessions with few pages convert very little and almost regardless, then the probability speeds up. And when we also add device and brand, things get worse: <strong>the linear model assigns 654 sessions out of 5,000 a negative conversion probability</strong>, down to −2.2%. A probability below zero has no meaning at all. At the other end, it gives the best profile (12 pages, desktop, brand query) 28.8%, where logistic regression, as we&#8217;ll see, estimates 72.9%.</p>



<p class="wp-block-paragraph">The problem is one of shape. A probability lives between 0 and 1, and near the edges it has to &#8220;brake&#8221;: it can&#8217;t go above 100% or below zero. A straight line, instead, goes on forever. We need a curve with built-in brakes.</p>



<h2 class="wp-block-heading" id="logistic-equation">From probability to odds to logit: the logistic equation</h2>



<p class="wp-block-paragraph">Logistic regression solves the problem with a change of scale in two steps. The first step is the <strong>odds</strong>, a word anyone familiar with betting knows well: instead of the probability that an event happens, we look at the ratio between the probability that it happens and the probability that it doesn&#8217;t.</p>



<p class="wp-block-paragraph">\(\text{odds} = \frac{p}{1-p}\)</p>



<p class="wp-block-paragraph">With a probability of 20%, the odds are 0.2 / 0.8 = 0.25: &#8220;one to four&#8221;, one success for every four failures. At 50% the odds are 1, and at 80% they rise to 4. The odds have no ceiling at 1: they run from zero to infinity.</p>



<p class="wp-block-paragraph">The second step is to take their logarithm. The <strong>logarithm of the odds</strong>, called the <em>logit</em>, also removes the floor at zero and can take any value, from minus infinity to plus infinity. And it is precisely on this unconstrained scale that logistic regression places its linear equation, identical in form to that of <a href="https://www.gironi.it/blog/en/multiple-regression-analysis-explained-simply/">multiple regression</a>:</p>



<p class="wp-block-paragraph">\(\ln\left(\frac{p}{1-p}\right) = b_0 + b_1 x_1 + b_2 x_2 + \dots + b_k x_k\)</p>



<p class="wp-block-paragraph">where p is the probability of the event, the x&#8217;s are the variables (the <em>predictors</em>) and the b&#8217;s are the coefficients to estimate. To go back to the probability we just reverse the path, and we get the <strong>logistic function</strong>, also called the <em>sigmoid</em> because of its S shape:</p>



<p class="wp-block-paragraph">\(p = \frac{1}{1 + e^{-(b_0 + b_1 x_1 + \dots + b_k x_k)}}\)</p>



<p class="wp-block-paragraph">Whatever value the expression in brackets takes, the result always stays between 0 and 1. These are the built-in brakes: <strong>logistic regression is a straight line on the log-odds scale, which becomes an S-shaped curve on the probability scale</strong>.</p>



<p class="wp-block-paragraph">The coefficients are not found by least squares but by <em>maximum likelihood</em>: among all possible curves, we pick the one that makes the data we actually observed most probable. The calculation is iterative, and we gladly leave it to the software.</p>



<h2 class="wp-block-heading" id="example-r">How do we fit a logistic regression in R?</h2>



<p class="wp-block-paragraph">Let&#8217;s simulate the 5,000 sessions. In real life the data would come from Google Analytics or the CRM; here we build them ourselves, with known effects, so we can check the result:</p>



<pre class="wp-block-code"><code>set.seed(7)
n &lt;- 5000
sessions &lt;- data.frame(
  pages  = 1 + rpois(n, 2.5),    # pages viewed in the session
  mobile = rbinom(n, 1, 0.6),    # 1 = smartphone, 0 = desktop
  brand  = rbinom(n, 1, 0.25)    # 1 = arrived from a query with the brand name
)
true_logit &lt;- -4.4 + 0.35 * sessions$pages - 0.5 * sessions$mobile + 1.2 * sessions$brand
sessions$converts &lt;- rbinom(n, 1, plogis(true_logit))

mean(sessions$converts)
# [1] 0.0542</code></pre>



<p class="wp-block-paragraph">The <code>plogis()</code> function is the logistic function itself: it turns the true logit into a probability, and <code>rbinom()</code> tosses a loaded coin with that probability for each session. That&#8217;s why <code>family = binomial</code> appears in the model: each session is a Bernoulli trial, the building block of the <a href="https://www.gironi.it/blog/en/probability-distributions-discrete-distributions-and-the-binomial/">binomial distribution</a>.</p>



<p class="wp-block-paragraph">Let&#8217;s fit the model:</p>



<pre class="wp-block-code"><code>model &lt;- glm(converts ~ pages + mobile + brand, data = sessions, family = binomial)
summary(model)
# Coefficients:
#             Estimate Std. Error z value Pr(&gt;|z|)
# (Intercept) -4.37566    0.18872 -23.187  &lt; 2e-16 ***
# pages        0.34742    0.03584   9.693  &lt; 2e-16 ***
# mobile      -0.50057    0.12834  -3.900 9.61e-05 ***
# brand        1.19691    0.12868   9.302  &lt; 2e-16 ***
#
#     Null deviance: 2107.0  on 4999  degrees of freedom
# Residual deviance: 1923.8  on 4996  degrees of freedom
# AIC: 1931.8</code></pre>



<p class="wp-block-paragraph">The model recovers almost perfectly the values we used to build the data: 0.347 against 0.35 for pages, −0.501 against −0.5 for mobile, 1.197 against 1.2 for brand. The columns read as in linear regression: estimate, standard error, the z statistic (here instead of t) and the <a href="https://www.gironi.it/blog/en/p-value-misconceptions/">p-value</a>. The <em>deviance</em> is the logistic counterpart of the residual sum of squares: it drops from 2107 (the model with no predictors) to 1924, a sign that the three variables explain something.</p>



<p class="wp-block-paragraph">The sign of the coefficients already tells us a lot: more pages and brand queries push the conversion probability up, smartphones push it down. But by how much? Here logistic regression asks for one more step.</p>



<h2 class="wp-block-heading" id="odds-ratio">How do we read the coefficients? Odds ratios</h2>



<p class="wp-block-paragraph">The coefficients are on the log-odds scale, which nobody is used to thinking in. The trick is to exponentiate them: <strong>e raised to the coefficient is the odds ratio</strong>, the number by which the odds of conversion are multiplied when the variable increases by one unit, holding the others fixed. Let&#8217;s compute them together with their <a href="https://www.gironi.it/blog/en/confidence-intervals-what-they-are-how-to-calculate-them-and-what-they-do-not-mean/">confidence intervals</a>:</p>



<pre class="wp-block-code"><code>round(exp(cbind(OR = coef(model), confint(model))), 3)
#                OR 2.5 % 97.5 %
# (Intercept) 0.013 0.009  0.018
# pages       1.415 1.319  1.519
# mobile      0.606 0.471  0.780
# brand       3.310 2.572  4.261</code></pre>



<p class="wp-block-paragraph">Let&#8217;s translate row by row:</p>



<ul class="wp-block-list"><li><strong>pages</strong>: each extra page viewed multiplies the odds of conversion by 1.415, which means they grow by about 41.5% (with an interval between +32% and +52%);</li><li><strong>mobile</strong>: on a smartphone the odds are 60.6% of those on desktop, about 40% lower;</li><li><strong>brand</strong>: a session arriving from a query with the brand name has odds 3.3 times those of a non-brand session.</li></ul>



<p class="wp-block-paragraph">None of the intervals contains 1, the value that would mean &#8220;no effect&#8221;.</p>



<div class="wp-block-group has-background" style="background-color:#f5f7f9;margin-top:2.5rem;margin-bottom:2.5rem;padding-top:1.5rem;padding-right:1.5rem;padding-bottom:1.5rem;padding-left:1.5rem"><div class="wp-block-group__inner-container is-layout-flow wp-block-group-is-layout-flow">
<p><strong>Watch out: the odds ratio is not a ratio of probabilities.</strong> &#8220;Odds 3.3 times higher&#8221; does not mean &#8220;3.3 times more likely&#8221;. The two almost coincide only when the event is rare, like our 5% conversions. With frequent events they drift far apart: going from odds of 1 to odds of 3.3 means going from a 50% to a 77% probability, which is certainly not triple. Above all, <strong>the same odds ratio produces different jumps in percentage points</strong>, depending on where we are on the curve.</p>
</div></div>



<p class="wp-block-paragraph">This last point deserves a chart. Let&#8217;s compute the predicted probabilities for four session profiles, with <code>type = "response"</code>, which returns probabilities instead of log-odds:</p>



<pre class="wp-block-code"><code>profiles &lt;- data.frame(pages  = c(3, 3, 8, 8),
                       mobile = c(1, 0, 1, 0),
                       brand  = c(0, 1, 0, 1))
round(predict(model, newdata = profiles, type = "response"), 3)
#     1     2     3     4
# 0.021 0.106 0.109 0.401</code></pre>



<p class="wp-block-paragraph">A smartphone session, non-brand, with 3 pages viewed converts with a 2.1% probability. The same number of pages on desktop and from a brand query is worth 10.6%. With 8 pages, desktop and brand, we reach 40.1%.</p>



<p class="wp-block-paragraph">Let&#8217;s check the first value by hand, to see there&#8217;s no magic. The logit is −4.376 + 0.347 × 3 − 0.501 × 1 + 1.197 × 0 = −3.834. The probability is 1 / (1 + e^3.834) = 1 / (1 + 46.2) = <strong>0.021</strong>.</p>



<figure class="wp-block-image size-large"><img decoding="async" src="https://www.gironi.it/blog/wp-content/uploads/2026/10/regressione-logistica-profili-en.png" alt="Conversion probability predicted by the model as a function of pages viewed, for four kinds of session: brand or non-brand query, desktop or mobile. The curves have the same shape but start from different levels, and the higher they are the further apart they get."/><figcaption class="wp-element-caption">Conversion probability predicted by the model as a function of pages viewed, for four kinds of session: brand or non-brand query, desktop or mobile. The curves have the same shape but start from different levels, and the higher they are the further apart they get.</figcaption></figure>



<p class="wp-block-paragraph">The four curves share the same odds ratio per page, 1.415. And yet <strong>in percentage points the effect of one extra page changes enormously</strong>: for a mobile non-brand session, going from 2 to 3 pages adds 0.6 points (from 1.5% to 2.1%); for a mobile session from a brand query, going from 8 to 9 adds 7.6 (from 28.9% to 36.5%). That&#8217;s the S-curve at work, and it&#8217;s why in reports it pays to show both the odds ratio and a few predicted probabilities for concrete profiles.</p>



<h2 class="wp-block-heading" id="threshold">From probabilities to decisions: where to put the threshold?</h2>



<p class="wp-block-paragraph">Often we want a decision from the model: which sessions to show an offer to, which users to put into a remarketing campaign. To turn a probability into a yes or a no we need a <strong>threshold</strong>, and the instinctive choice is 0.5: if the probability is above 50%, we predict a conversion.</p>



<p class="wp-block-paragraph">To judge the model honestly, though, we can&#8217;t evaluate it on the same data we fitted it on. Let&#8217;s split the sessions: we fit on 3,500 and measure the predictions on the other 1,500, which the model has never seen.</p>



<pre class="wp-block-code"><code>set.seed(11)
rows &lt;- sample(n, 3500)
train &lt;- sessions[rows, ]
test &lt;- sessions[-rows, ]
model_train &lt;- glm(converts ~ pages + mobile + brand, data = train, family = binomial)
p_test &lt;- predict(model_train, newdata = test, type = "response")

table(predicted = p_test &gt; 0.5, actual = test$converts)
#          actual
# predicted    0    1
#     FALSE 1410   89
#     TRUE     1    0
mean((p_test &gt; 0.5) == test$converts)
# [1] 0.94</code></pre>



<p class="wp-block-paragraph">94% accuracy: it looks like a great result. But let&#8217;s look at the table: of the 89 sessions that actually converted, the model identified <strong>none</strong>. It simply answered &#8220;no&#8221; almost every time. And with a 5.9% conversion rate, anyone who always answers &#8220;no&#8221;, with no model at all, gets 94.1% accuracy, better than us.</p>



<p class="wp-block-paragraph"><strong>With rare events, accuracy is a misleading metric.</strong> The model isn&#8217;t useless: it&#8217;s the threshold that&#8217;s wrong. In our data almost no session goes above a 50% probability, so a 0.5 threshold amounts to never flagging anything. Let&#8217;s lower it to 0.1:</p>



<pre class="wp-block-code"><code>table(predicted = p_test &gt; 0.1, actual = test$converts)
#          actual
# predicted    0    1
#     FALSE 1292   62
#     TRUE   119   27</code></pre>



<p class="wp-block-paragraph">Now the model flags 146 sessions and catches 27 that really convert: 30% of conversions (the <em>sensitivity</em>, or <em>recall</em>). Among the flagged sessions, 18.5% convert (the <em>precision</em>), more than three times the starting 5.9%. Accuracy drops to 88%, but the model has become useful.</p>



<figure class="wp-block-image size-large"><img decoding="async" src="https://www.gironi.it/blog/wp-content/uploads/2026/10/regressione-logistica-soglia-en.png" alt="Conversion probabilities predicted by the model for the 1,500 test sessions, split between those that did not convert (top) and those that converted (bottom). Almost everything sits below 30%: the 0.5 threshold catches no conversions, the 0.1 threshold catches 27 at the price of 119 false alarms."/><figcaption class="wp-element-caption">Conversion probabilities predicted by the model for the 1,500 test sessions, split between those that did not convert (top) and those that converted (bottom). Almost everything sits below 30%: the 0.5 threshold catches no conversions, the 0.1 threshold catches 27 at the price of 119 false alarms.</figcaption></figure>



<p class="wp-block-paragraph">The chart shows the trade-off. Converting sessions sit further to the right on average, but the two distributions overlap a lot: every threshold is a swap between conversions caught and false alarms. The right threshold isn&#8217;t decided by statistics, it&#8217;s decided by the cost of the errors: if showing an offer to someone who won&#8217;t buy costs little, a low threshold pays off.</p>



<p class="wp-block-paragraph">To judge the model independently of the threshold, the <strong>AUC</strong> is often used, the area under the ROC curve: it is the probability that, taking at random one session that converts and one that doesn&#8217;t, the model gives the first a higher probability. It&#8217;s 0.5 for a model that guesses and 1 for a perfect one. Let&#8217;s compute it with ranks, the same idea behind the <a href="https://www.gironi.it/blog/en/non-parametric-tests-the-wilcoxon-test-for-non-normal-data/">Wilcoxon test</a>:</p>



<pre class="wp-block-code"><code>r &lt;- rank(p_test)
n1 &lt;- sum(test$converts); n0 &lt;- sum(1 - test$converts)
round((sum(r[test$converts == 1]) - n1 * (n1 + 1) / 2) / (n1 * n0), 3)
# [1] 0.749</code></pre>



<p class="wp-block-paragraph">An AUC of 0.75 is a fair result: with only three variables, the model orders three pairs out of four correctly.</p>



<h2 class="wp-block-heading" id="assumptions">The conditions for using it</h2>



<p class="wp-block-paragraph">Logistic regression asks for less than linear regression (no normality of residuals, no constant variance), but a few conditions remain:</p>



<ul class="wp-block-list"><li><strong>the outcome is binary</strong>: only two categories (for three or more, see the frequently asked questions below);</li><li><strong>the observations are independent</strong>: ten sessions from the same user are not ten independent pieces of information;</li><li><strong>the relationship is linear on the logit scale</strong>: if the effect of pages grows up to a point and then flattens out, it has to be modelled (for example with bands of pages viewed);</li><li><strong>the predictors are not too correlated with each other</strong>, the multicollinearity problem we&#8217;ll discuss in the next article;</li><li><strong>there are enough events</strong>: a widespread rule of thumb asks for at least ten or so events (in our case, conversions) per predictor. We have 271 for 3 variables, well above that.</li></ul>



<p class="wp-block-paragraph">Finally there is a less known condition, and it&#8217;s not unusual to read the exact opposite: that logistic regression is used &#8220;when the data are linearly separable&#8221;. <strong>The opposite is true: if a variable perfectly separates the yeses from the noes, logistic regression breaks down.</strong> Maximum likelihood pushes the coefficient towards infinity to turn the curve into a perfect step, and the standard error explodes. It happens more often than we&#8217;d think, even with famous datasets.</p>



<div class="wp-block-group has-background" style="background-color:#f5f7f9;margin-top:2.5rem;margin-bottom:2.5rem;padding-top:1.5rem;padding-right:1.5rem;padding-bottom:1.5rem;padding-left:1.5rem"><div class="wp-block-group__inner-container is-layout-flow wp-block-group-is-layout-flow">
<p><strong>The Titanic case: when a coefficient explodes.</strong> The Titanic passenger dataset is the most widely used logistic regression example in the world. Let&#8217;s model survival as a function of class, sex, age, family on board, fare and port of embarkation, and a strange row shows up in the <code>summary()</code>: the intercept is 17.9 with a standard error of 607, and the three ports have coefficients around −12, also with huge errors.<br><br>The reason is two passengers, Amelie Icard and Martha Stone, whose port of embarkation is empty in the file. R treats the empty value as a fourth category, and uses it as the reference. Both survived: for that &#8220;category&#8221; the survival rate is 100%, a perfect separation. Historical sources show that both boarded at Southampton: assigning them &#8220;S&#8221;, the intercept goes back to 5.64 with a standard error of 0.64, and the ports become readable.<br><br>Two lessons. First: a standard error in the hundreds is almost always a sign of separation, not a result. Second: the same <code>glm()</code> silently dropped 177 passengers out of 891, those with no age. Before looking at the coefficients it&#8217;s always worth checking with <code>nobs(model)</code> how many rows it actually worked on.</p>
</div></div>



<h2 class="wp-block-heading" id="faq">Frequently asked questions</h2>



<h3 class="wp-block-heading">What is the difference between linear and logistic regression?</h3>



<p class="wp-block-paragraph">Linear regression predicts a continuous numeric variable, such as traffic or revenue. Logistic regression predicts the probability of a binary outcome, such as converts or doesn&#8217;t convert. To do so it models the logarithm of the odds linearly and returns probabilities that always lie between 0 and 1.</p>



<h3 class="wp-block-heading">What is the odds ratio in logistic regression?</h3>



<p class="wp-block-paragraph">It is the exponential of a coefficient: it tells by how much the odds of the event are multiplied when the variable increases by one unit, holding the others fixed. An odds ratio of 1.4 means odds 40% higher, one of 0.6 odds 40% lower, one equal to 1 no effect.</p>



<h3 class="wp-block-heading">How do we get predicted probabilities from a logistic regression?</h3>



<p class="wp-block-paragraph">By applying the logistic function to the linear combination of the coefficients: p = 1 / (1 + e^−(b0 + b1x1 + … + bkxk)). In R, <code>predict(model, newdata, type = "response")</code> returns the probabilities directly; without <code>type = "response"</code> it returns log-odds.</p>



<h3 class="wp-block-heading">What if there are more than two categories?</h3>



<p class="wp-block-paragraph">If the categories have no order (for example desktop, mobile and tablet), we use multinomial logistic regression, in R with <code>multinom()</code> from the nnet package. If they have a natural order (low, medium, high), we use ordinal logistic regression, in R with <code>polr()</code> from the MASS package.</p>



<h3 class="wp-block-heading">What is the right threshold for classifying?</h3>



<p class="wp-block-paragraph">It depends on the cost of the two kinds of error and on how frequent the event is. With rare events, such as conversions, a 0.5 threshold often leads to never flagging anything. It&#8217;s worth looking at the prediction table for several thresholds, or at the AUC to judge the model independently of the threshold.</p>



<h3 class="wp-block-heading">How much data does a logistic regression need?</h3>



<p class="wp-block-paragraph">What matters most is the number of events, the cases in the less frequent category. A widespread rule of thumb asks for at least ten or so per predictor: with 50 conversions, better not to go beyond four or five variables.</p>



<script type="application/ld+json">{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "What is the difference between linear and logistic regression?", "acceptedAnswer": {"@type": "Answer", "text": "Linear regression predicts a continuous numeric variable, such as traffic or revenue. Logistic regression predicts the probability of a binary outcome, such as converts or doesn't convert. To do so it models the logarithm of the odds linearly and returns probabilities that always lie between 0 and 1."}}, {"@type": "Question", "name": "What is the odds ratio in logistic regression?", "acceptedAnswer": {"@type": "Answer", "text": "It is the exponential of a coefficient: it tells by how much the odds of the event are multiplied when the variable increases by one unit, holding the others fixed. An odds ratio of 1.4 means odds 40% higher, one of 0.6 odds 40% lower, one equal to 1 no effect."}}, {"@type": "Question", "name": "How do we get predicted probabilities from a logistic regression?", "acceptedAnswer": {"@type": "Answer", "text": "By applying the logistic function to the linear combination of the coefficients: p = 1 / (1 + e^−(b0 + b1x1 + … + bkxk)). In R, predict(model, newdata, type = \"response\") returns the probabilities directly; without type = \"response\" it returns log-odds."}}, {"@type": "Question", "name": "What if there are more than two categories?", "acceptedAnswer": {"@type": "Answer", "text": "If the categories have no order (for example desktop, mobile and tablet), we use multinomial logistic regression, in R with multinom() from the nnet package. If they have a natural order (low, medium, high), we use ordinal logistic regression, in R with polr() from the MASS package."}}, {"@type": "Question", "name": "What is the right threshold for classifying?", "acceptedAnswer": {"@type": "Answer", "text": "It depends on the cost of the two kinds of error and on how frequent the event is. With rare events, such as conversions, a 0.5 threshold often leads to never flagging anything. It's worth looking at the prediction table for several thresholds, or at the AUC to judge the model independently of the threshold."}}, {"@type": "Question", "name": "How much data does a logistic regression need?", "acceptedAnswer": {"@type": "Answer", "text": "What matters most is the number of events, the cases in the less frequent category. A widespread rule of thumb asks for at least ten or so per predictor: with 50 conversions, better not to go beyond four or five variables."}}]}</script>



<h2 class="wp-block-heading" id="try-it-yourself">Try it yourself</h2>



<p class="wp-block-paragraph">The article&#8217;s code lends itself to a few experiments.</p>



<ul class="wp-block-list"><li>Try a 0.2 threshold instead of 0.1: how many conversions does the model catch, and with how many false alarms? Compare the precision with the one at 0.1.</li><li>Remove brand from the model (<code>converts ~ pages + mobile</code>) and look at the AUC on the test set: how much is the information about the starting query worth on its own?</li><li>Raise the true coefficient for pages from 0.35 to 0.7 in the simulation: how does the shape of the curve change, and what happens to the 0.5 threshold?</li></ul>



<p class="wp-block-paragraph">The last exercise shows well the link between the strength of the predictors and the difficulty of classification: a model with strong effects pushes the probabilities towards the edges, and there one threshold is as good as another.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">In our example the three variables are independent by construction: pages viewed don&#8217;t depend on the device, brand doesn&#8217;t depend on pages. In real life that&#8217;s almost never the case. People arriving from a search for the brand name also tend to browse more pages, and when predictors overlap the coefficients become unstable and hard to tell apart. That&#8217;s <em>multicollinearity</em>, and together with two other names that sound scarier than they deserve, <a href="https://www.gironi.it/blog/en/multicollinearity-heteroscedasticity-autocorrelation-three-difficult-sounding-concepts-explained-simply/">heteroscedasticity and autocorrelation</a>, it&#8217;s the next stop on our journey through regression.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading" id="further-reading">Further reading</h3>



<p class="wp-block-paragraph">To go further into logistic regression and classification, I recommend <a href="https://www.amazon.it/dp/1461471370?tag=consulenzeinf-21&amp;ascsubtag=logistic-regression" rel="nofollow sponsored noopener" target="_blank"><em>An Introduction to Statistical Learning</em></a> by James, Witten, Hastie and Tibshirani: its chapter on classification starts from exactly the question of why linear regression doesn&#8217;t fit a yes/no outcome, then walks through logistic regression, confusion matrices and thresholds, with R labs that let us reproduce every step.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.gironi.it/blog/en/logistic-regression-predicting-the-outcome-of-an-event/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
