Act I · The tension
A $10,000 raise means very different things at a $30,000 salary and at a $300,000 salary.
So the number we usually want is a percentage, not a dollar amount — a return to education, a price elasticity, a growth rate. The natural logarithm is the one transformation that turns a regression slope directly into that percentage.
pyfixestLogs let a single regression slope be read as a percentage — that is the whole point.
Act II · The investigation
For small changes, the change in \(\ln(x)\) is the proportionate change in \(x\) — multiply by 100 to get a percent.
import numpy as np # numpy → logs, exponentials, arrays
x0, x1 = 40, 40.4 # x rises from 40 to 40.4 (a 1% increase)
exact = (x1 - x0) / x0 # exact proportionate change = 0.010000
approx = np.log(x1) - np.log(x0) # log approximation Δln(x) = 0.009950
print(f"exact proportionate change: {exact:.6f} ({exact*100:.2f}%)")
print(f"log approximation Δln(x): {approx:.6f} ({approx*100:.2f}%)")
print(f"difference: {abs(exact-approx):.6f}") # tiny — the approximation is excellent for small moves| Form | Model | Slope \(\beta_1\) reads as |
|---|---|---|
| Linear | \(y = \beta_0 + \beta_1 x\) | \(\Delta y\) in units per unit of \(x\) |
| Log-linear | \(\ln y = \beta_0 + \beta_1 x\) | \(100\beta_1 \approx \%\Delta y\) per unit of \(x\) |
| Log-log | \(\ln y = \beta_0 + \beta_1 \ln x\) | $_1 = $ elasticity (% per %) |
| Linear-log | \(y = \beta_0 + \beta_1 \ln x\) | \(\beta_1/100 = \Delta y\) per 1% of \(x\) |
171 full-time women workers aged 30 (2010). Earnings are right-skewed (mean $41,413, median $36,000, max $172,000), so the log scale pulls in the long tail. We take logs and fit all four forms with pyfixest.
!pip install -q pyfixest # pyfixest is NOT preinstalled in Colab (-q = quiet)
import pandas as pd # tabular data
import numpy as np # np.log for the transforms
import pyfixest as pf # OLS with R-style formulas
url = "https://raw.githubusercontent.com/quarcs-lab/data-open/master/AED/AED_EARNINGS.DTA"
data = pd.read_stata(url) # 171 women aged 30: earnings, education
data["lnearn"] = np.log(data["earnings"]) # log of the outcome
data["lneduc"] = np.log(data["education"].astype(float)) # log of the regressor (int8 → float first)
m_lin = pf.feols("earnings ~ education", data=data) # 1. linear: $ per year
m_loglin = pf.feols("lnearn ~ education", data=data) # 2. log-linear: semi-elasticity
m_loglog = pf.feols("lnearn ~ lneduc", data=data) # 3. log-log: elasticity
m_linlog = pf.feols("earnings ~ lneduc", data=data) # 4. linear-log: $ per 1%
print(100 * m_loglin.coef()["education"]) # 13.1 → ~13.1% more earnings per extra year
m_loglin.summary() # β0, β1, SE, t, p, R²Each panel fits one specification. The log transforms straighten the curvature the linear model misses.
Log-linear fits best among the \(\ln\)(earnings) models (R² = 0.334); logging only \(x\) bends the line into diminishing returns.
Compare \(R^2\) only across models with the same dependent variable — not across level and log outcomes.
\(x_t = x_0(1+r)^t\) curves upward in levels, but \(\ln(x_t) = \ln(x_0) + \ln(1+r)\,t \approx \ln(x_0) + r\,t\) is linear — so the slope of \(\ln x\) on time estimates \(\ln(1+r) \approx r\); the exact rate is \(e^{\text{slope}} - 1\).
!pip install -q pyfixest # self-contained cell: install pyfixest (quiet)
import pandas as pd # tabular data
import numpy as np # np.exp for the exact growth rate
import pyfixest as pf # OLS with R-style formulas
url = "https://raw.githubusercontent.com/quarcs-lab/data-open/master/AED/AED_SP500INDEX.DTA"
data = pd.read_stata(url) # S&P 500 index, 1927–2019 (lnsp500 precomputed)
fit = pf.feols("lnsp500 ~ year", data=data) # slope = ln(1+r) ≈ growth rate r
r = fit.coef()["year"] # ≈ 0.0653
print(f"log slope: {100*r:.2f}% per year") # ≈ 6.53% (log approximation)
print(f"exact growth: {100*(np.exp(r)-1):.2f}% per year") # ≈ 6.75% per year
print(f"Rule of 72: doubles every {72/(100*r):.0f} years") # ≈ 11 yearsThe same S&P 500 series: an exponential curve in levels (left), a near-straight line in logs (right).
Log slope 0.065 → exact growth of about 6.75% per year over 1927–2019 → the index doubles about every 11 years (Rule of 72).
Act III · The resolution
Objection. “The log-linear slope is 0.131, so the return to a year of education is exactly 13.1%.”
Response. \(100\beta_1\) is an approximation that holds for small coefficients. At \(\beta_1 = 0.131\) the exact effect is \(100\,(e^{0.131}-1) \approx 14.0\%\) — close, but not identical. And \(\ln\) is undefined at zero or negative values, so logs only apply to strictly positive variables.
Report the exact \(100(e^{\beta_1}-1)\) once \(|\beta_1|\) clears about 0.10; never log a variable that can be zero.