Skip to main content

Lead-Lag Dynamics of Capital Share and Real GDP per Capita

Profit Extraction and Mean Reversion (1947–2026)

1947~2026년 미국 거시 데이터를 통해 실질 1인당 GDP 충격이 발생할 때 자본분배율과 노동분배율 간에 나타나는 즉각적인 초과이윤 추출과 시차를 둔 평균회귀 동학을 공적분 및 VAR 모형으로 실증 분석한다.
Author

Sam Lee

Published

September 17, 2026

Keywords

capital share, labor share, real gdp per capita, lead-lag, cointegration, granger causality, mean reversion, var

1 Introduction

Macroeconomic output \(Y_t\) decomposes exhaustively into labor income and capital income (operating surplus):

\[Y_t = \text{Labor Income}_t + \text{Capital Income}_t \implies LS_t + \alpha_t \equiv 1\]

where \(LS_t\) denotes the labor share of income and \(\alpha_t\) represents the capital share. The central question of this study is straightforward: When real economic growth (\(\Delta gdp\)) accelerates, who captures the surplus first, and through what dynamic adjustment horizon is it redistributed?

1.1 The Extraction and Mean Reversion Hypothesis

  1. Contemporaneous Profit Extraction (\(k = 0\)):
    • Due to contractual wage stickiness and hiring lags, labor compensation cannot respond immediately to quarterly output shocks.
    • Consequently, productivity surges flow almost entirely into firm operating profits, driving an immediate surge in the capital share (\(\alpha_t\)) and a simultaneous decline in the labor share.
  2. Medium-Term Mean Reversion and Lagged Windfalls (\(k = +1 \sim +20\) quarters):
    • The initial profit surge represents an unsustainable overshoot.
    • Over a 1-to-5-year horizon, tightening labor markets, delayed wage renegotiations, and inter-firm competition erode these excess margins, forcing the capital share to mean-revert.
    • Labor income does not initiate growth; it passively absorbs a delayed windfall as capital margins normalize.

This paper tests these dynamics using quarterly data from the U.S. Bureau of Labor Statistics (BLS labor share index PRS85006173) and Bureau of Economic Analysis (BEA real GDP per capita A939RX0Q048SBEA) spanning 1947 to 2026 (318 quarters).

\[\begin{aligned} ls_t &= \ln(\text{PRS85006173}_t) \\ gdp_t &= \ln(\text{A939RX0Q048SBEA}_t) \\ \Delta ls_t &= ls_t - ls_{t-1} \\ \Delta \alpha_t &\approx -\Delta ls_t \\ \Delta gdp_t &= gdp_t - gdp_{t-1} \end{aligned}\]


2 Data Pipeline

Underlying series are fetched from the FRED API and stored in a DuckDB analytical database (capital_share_vs_gdp.duckdb) for reproducible pipeline execution.

Code
import os, io, requests, warnings
import duckdb
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from arch.unitroot import PhillipsPerron
from statsmodels.tsa.stattools import adfuller, grangercausalitytests
from statsmodels.tsa.vector_ar.vecm import coint_johansen
from statsmodels.tsa.api import VAR

warnings.filterwarnings("ignore")

# 1. DuckDB connection path
DB_PATH = "capital_share_vs_gdp.duckdb" if os.path.exists("capital_share_vs_gdp.duckdb") else "docs/capital_share_vs_gdp.duckdb"

def load_env_var(key, default=None):
    for env_path in [".env", "../.env"]:
        if os.path.exists(env_path):
            with open(env_path) as f:
                for line in f:
                    line = line.strip()
                    if line and not line.startswith("#") and "=" in line:
                        k, v = line.split("=", 1)
                        if k.strip() == key:
                            return v.strip().strip("\"'")
    return os.environ.get(key, default)

fred_key = load_env_var("FRED_API_KEY")

# 2. Table initialization and data ingestion
symbols = {
    "PRS85006173": "labor_share",
    "A939RX0Q048SBEA": "real_gdp_pc"
}

with duckdb.connect(DB_PATH) as con:
    con.execute("""
        CREATE TABLE IF NOT EXISTS fred_series (
            series_id VARCHAR,
            date DATE,
            value DOUBLE,
            PRIMARY KEY (series_id, date)
        )
    """)
    
    if fred_key:
        for s_id in symbols.keys():
            try:
                url = f"https://api.stlouisfed.org/fred/series/observations?series_id={s_id}&api_key={fred_key}&file_type=json"
                r = requests.get(url, timeout=10)
                obs = r.json().get("observations", [])
                if obs:
                    df_raw = pd.DataFrame(obs)[["date", "value"]]
                    df_raw["value"] = pd.to_numeric(df_raw["value"], errors="coerce")
                    df_raw["date"] = pd.to_datetime(df_raw["date"])
                    df_raw["series_id"] = s_id
                    df_raw = df_raw.dropna(subset=["date", "value"])
                    con.register("tmp_raw", df_raw[["series_id", "date", "value"]])
                    con.execute("INSERT OR REPLACE INTO fred_series SELECT series_id, date, value FROM tmp_raw")
            except Exception:
                pass

    # 3. Load aligned series
    df_ls = con.execute("SELECT date, value as labor_share FROM fred_series WHERE series_id = 'PRS85006173' ORDER BY date").df()
    df_gdp = con.execute("SELECT date, value as real_gdp_pc FROM fred_series WHERE series_id = 'A939RX0Q048SBEA' ORDER BY date").df()

df = pd.merge(df_ls, df_gdp, on="date").dropna()
df["date"] = pd.to_datetime(df["date"])
df = df.set_index("date")

# Variable transformations: log levels, first differences, capital share proxy
df["ln_ls"] = np.log(df["labor_share"])
df["ln_gdp"] = np.log(df["real_gdp_pc"])
df["d_ln_ls"] = df["ln_ls"].diff()
df["d_ln_gdp"] = df["ln_gdp"].diff()

# Capital share change proxy: d_alpha = -d_ln_ls
df["d_alpha"] = -df["d_ln_ls"]

df_clean = df.dropna()

2.1 Historical Trajectories: Factor Shares vs. Real Output

Code
fig, axes = plt.subplots(2, 2, figsize=(13, 8), dpi=150)
plt.subplots_adjust(hspace=0.35, wspace=0.25)

# (A) ln(Labor Share)
axes[0, 0].plot(df_clean.index, df_clean["ln_ls"], color="#1f77b4", lw=1.8)
axes[0, 0].set_title(r"(A) $\ln(\mathrm{Labor\ Share})$ ($ls_t$)", fontsize=11, fontweight="bold")
axes[0, 0].grid(True, alpha=0.3)
axes[0, 0].set_ylabel("Log Index")

# (B) ln(Real GDP per capita)
axes[0, 1].plot(df_clean.index, df_clean["ln_gdp"], color="#2ca02c", lw=1.8)
axes[0, 1].set_title(r"(B) $\ln(\mathrm{Real\ GDP\ pc})$ ($gdp_t$)", fontsize=11, fontweight="bold")
axes[0, 1].grid(True, alpha=0.3)
axes[0, 1].set_ylabel("Log Chained 2017 Dollars")

# (C) Capital Share Growth Proxy (d_alpha = -d_ln_ls)
axes[1, 0].plot(df_clean.index, df_clean["d_alpha"], color="#d62728", lw=1.2, alpha=0.85)
axes[1, 0].axhline(0, color="gray", ls="--", lw=0.8)
axes[1, 0].set_title(r"(C) $\Delta \mathrm{Capital\ Share\ Proxy}$ ($\Delta \alpha_t \approx -\Delta ls_t$)", fontsize=11, fontweight="bold")
axes[1, 0].grid(True, alpha=0.3)
axes[1, 0].set_ylabel("Quarterly Proxy Change")

# (D) d_ln(Real GDP per capita)
axes[1, 1].plot(df_clean.index, df_clean["d_ln_gdp"], color="#2ca02c", lw=1.2, alpha=0.85)
axes[1, 1].axhline(0, color="gray", ls="--", lw=0.8)
axes[1, 1].set_title(r"(D) $\Delta \ln(\mathrm{Real\ GDP\ pc})$ ($\Delta gdp_t$)", fontsize=11, fontweight="bold")
axes[1, 1].grid(True, alpha=0.3)
axes[1, 1].set_ylabel("Quarterly Growth")

plt.show()
Figure 1: U.S. Factor Shares and Real GDP per Capita Dynamics (1947–2026)

3 Unit Root and Stationarity Tests

The integration order of each series is evaluated using the Augmented Dickey-Fuller (ADF) and Phillips-Perron (PP) tests.

Code
records = []
test_vars = [
    ("ln(Labor Share)", df_clean["ln_ls"]),
    ("Δ ln(Labor Share)", df_clean["d_ln_ls"]),
    ("Δ Capital Share Proxy", df_clean["d_alpha"]),
    ("ln(Real GDP pc)", df_clean["ln_gdp"]),
    ("Δ ln(Real GDP pc)", df_clean["d_ln_gdp"]),
]

for name, s in test_vars:
    adf_c = adfuller(s, regression="c", autolag="AIC")
    adf_ct = adfuller(s, regression="ct", autolag="AIC")
    pp_c = PhillipsPerron(s, trend="c")
    pp_ct = PhillipsPerron(s, trend="ct")
    
    records.append({
        "Variable": name,
        "ADF (c) Stat": f"{adf_c[0]:.3f}",
        "ADF (c) p-val": f"{adf_c[1]:.4f}",
        "ADF (ct) Stat": f"{adf_ct[0]:.3f}",
        "ADF (ct) p-val": f"{adf_ct[1]:.4f}",
        "PP (c) Stat": f"{pp_c.stat:.3f}",
        "PP (c) p-val": f"{pp_c.pvalue:.4f}",
        "PP (ct) Stat": f"{pp_ct.stat:.3f}",
        "PP (ct) p-val": f"{pp_ct.pvalue:.4f}",
    })

df_unitroot = pd.DataFrame(records)
df_unitroot
Table 1: Unit Root Test Statistics (ADF and Phillips-Perron)
Variable ADF (c) Stat ADF (c) p-val ADF (ct) Stat ADF (ct) p-val PP (c) Stat PP (c) p-val PP (ct) Stat PP (ct) p-val
0 ln(Labor Share) -0.186 0.9402 -2.038 0.5808 0.088 0.9653 -2.730 0.2238
1 Δ ln(Labor Share) -23.225 0.0000 -23.252 0.0000 -24.297 0.0000 -24.643 0.0000
2 Δ Capital Share Proxy -23.225 0.0000 -23.252 0.0000 -24.297 0.0000 -24.643 0.0000
3 ln(Real GDP pc) -1.410 0.5775 -2.236 0.4696 -1.455 0.5558 -1.760 0.7239
4 Δ ln(Real GDP pc) -15.615 0.0000 -15.655 0.0000 -15.488 0.0000 -15.549 0.0000

3.1 Inference

  • The log-level series (\(ls_t, gdp_t\)) are non-stationary with unit roots.
  • First-differenced series (\(\Delta ls_t, \Delta \alpha_t, \Delta gdp_t\)) reject the null hypothesis of a unit root at the 1% significance level (\(p < 0.0001\)).
  • All variables are strictly \(I(1)\) processes.

4 Cointegration Test

Code
joh_res = coint_johansen(df_clean[["ln_ls", "ln_gdp"]], det_order=0, k_ar_diff=4)

df_joh = pd.DataFrame({
    "Hypothesized r": ["r = 0 (No Cointegration)", "r <= 1"],
    "Trace Stat": [f"{joh_res.lr1[0]:.3f}", f"{joh_res.lr1[1]:.3f}"],
    "Trace 90% Crit": [f"{joh_res.cvt[0, 0]:.3f}", f"{joh_res.cvt[1, 0]:.3f}"],
    "Trace 95% Crit": [f"{joh_res.cvt[0, 1]:.3f}", f"{joh_res.cvt[1, 1]:.3f}"],
    "Max-Eigen Stat": [f"{joh_res.lr2[0]:.3f}", f"{joh_res.lr2[1]:.3f}"],
    "Max-Eigen 95% Crit": [f"{joh_res.cvm[0, 1]:.3f}", f"{joh_res.cvm[1, 1]:.3f}"],
})
df_joh
Table 2: Johansen Cointegration Test Statistics (k_ar_diff=4)
Hypothesized r Trace Stat Trace 90% Crit Trace 95% Crit Max-Eigen Stat Max-Eigen 95% Crit
0 r = 0 (No Cointegration) 4.791 13.429 15.494 4.507 14.264
1 r <= 1 0.284 2.705 3.841 0.284 3.841

4.1 Inference

  • The Trace statistic (\(4.791\)) falls far below the 95% critical value (\(15.494\)), confirming the absence of cointegration (\(r = 0\)).
  • No static linear attractor anchors log output to factor shares in levels. Instead, distribution dynamics are driven by transitory growth innovations and subsequent mean-reverting adjustments, validating a Vector Autoregression (VAR) in first differences.

5 Cross-Correlation Analysis: Profit Extraction vs. Mean Reversion

To characterize the lead-lag timing, the cross-correlation coefficients between real GDP growth (\(\Delta gdp_t\)) and shifted factor share changes (\(\Delta \alpha_{t+k}, \Delta ls_{t+k}\)) are evaluated over \(k \in [-20, +20]\) (a 5-year window):

\[\rho_{\alpha}(k) = \operatorname{Corr}(\Delta gdp_t, \Delta \alpha_{t+k}) = -\operatorname{Corr}(\Delta gdp_t, \Delta ls_{t+k}) = -\rho_{ls}(k)\]

Code
T = len(df_clean)
ci_bound = 1.96 / np.sqrt(T)

lags = np.arange(-20, 21)
ccf_alpha = []
ccf_ls = []

for k in lags:
    if k > 0:
        c_alpha = np.corrcoef(df_clean["d_ln_gdp"].iloc[:-k], df_clean["d_alpha"].iloc[k:])[0, 1]
        c_ls = np.corrcoef(df_clean["d_ln_gdp"].iloc[:-k], df_clean["d_ln_ls"].iloc[k:])[0, 1]
    elif k < 0:
        pos_k = abs(k)
        c_alpha = np.corrcoef(df_clean["d_ln_gdp"].iloc[pos_k:], df_clean["d_alpha"].iloc[:-pos_k])[0, 1]
        c_ls = np.corrcoef(df_clean["d_ln_gdp"].iloc[pos_k:], df_clean["d_ln_ls"].iloc[:-pos_k])[0, 1]
    else:
        c_alpha = np.corrcoef(df_clean["d_ln_gdp"], df_clean["d_alpha"])[0, 1]
        c_ls = np.corrcoef(df_clean["d_ln_gdp"], df_clean["d_ln_ls"])[0, 1]
    ccf_alpha.append(c_alpha)
    ccf_ls.append(c_ls)

fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8), dpi=150, sharex=True)
plt.subplots_adjust(hspace=0.25)

# 1. Capital Share CCF
m1, s1, b1 = ax1.stem(lags, ccf_alpha, linefmt="#d62728", markerfmt="o", basefmt="gray")
plt.setp(m1, markersize=5, color="#d62728")
plt.setp(s1, lw=1.5, color="#d62728")
ax1.axhline(ci_bound, color="black", ls="--", lw=1.0, label=f"95% Confidence Band (±{ci_bound:.3f})")
ax1.axhline(-ci_bound, color="black", ls="--", lw=1.0)
ax1.axhline(0, color="gray", lw=0.8)
ax1.set_ylabel(r"$\rho_{\alpha}(k)$", fontsize=11, fontweight="bold", color="#d62728")
ax1.set_title(r"Capital Share Dynamic: $\operatorname{Corr}(\Delta \ln(\mathrm{Real\ GDP\ pc})_t, \Delta \alpha_{t+k})$ [Extraction $\to$ Mean Reversion]", fontsize=11, fontweight="bold")
ax1.grid(True, alpha=0.3)
ax1.legend(loc="upper right", frameon=True)

# 2. Labor Share CCF
m2, s2, b2 = ax2.stem(lags, ccf_ls, linefmt="#1f77b4", markerfmt="s", basefmt="gray")
plt.setp(m2, markersize=5, color="#1f77b4")
plt.setp(s2, lw=1.5, color="#1f77b4")
ax2.axhline(ci_bound, color="black", ls="--", lw=1.0)
ax2.axhline(-ci_bound, color="black", ls="--", lw=1.0)
ax2.axhline(0, color="gray", lw=0.8)
ax2.set_xticks(np.arange(-20, 21, 2))
ax2.set_xlabel("Quarterly Lag (k): Negative = Share leads GDP, Positive = GDP leads Share", fontsize=10)
ax2.set_ylabel(r"$\rho_{ls}(k)$", fontsize=11, fontweight="bold", color="#1f77b4")
ax2.set_title(r"Labor Share Dynamic: $\operatorname{Corr}(\Delta \ln(\mathrm{Real\ GDP\ pc})_t, \Delta ls_{t+k})$ [Delayed Windfall]", fontsize=11, fontweight="bold")
ax2.grid(True, alpha=0.3)

plt.show()
Figure 2: Cross-Correlation Functions: Immediate Extraction and Lagged Mean Reversion

5.1 Observed Lead-Lag Mechanics

  1. Contemporaneous Capture (\(k = 0\)): \(\rho_{\alpha} = +0.537\)
    • In the quarter growth occurs, the capital share surges in lockstep with output.
    • Wage contracts cannot adjust instantly, allowing firms to absorb productivity windfalls directly into net margins.
  2. Lagged Reversion (\(k = +1 \sim +20\)): \(\rho_{\alpha} < 0\), \(\rho_{ls} > 0\)
    • From quarter \(k = 1\) through year 5 (\(k = 20\)), cross-correlations turn persistently negative for capital and positive for labor.
    • Initial windfall margins are gradually renegotiated away through compensation increases, bonus disbursements, and recruitment drives.
    • Labor gains strictly as a secondary reaction to earlier capital expansion.

6 Granger Causality Tests

We test whether real GDP growth leads factor share adjustments across multi-year lag horizons (\(p = 4\) to \(p = 20\) quarters).

  • Hypothesis A: \(\Delta gdp \not\to \Delta \alpha\) (\(\Delta ls\)) — Output growth does not Granger-cause factor share changes.
  • Hypothesis B: \(\Delta \alpha\) (\(\Delta ls\)) \(\not\to \Delta gdp\) — Factor share changes do not Granger-cause output growth.
Code
gc_lags = [4, 8, 12, 16, 20]
gc_records = []

gc_gdp_to_ls = grangercausalitytests(df_clean[["d_ln_ls", "d_ln_gdp"]], maxlag=20)
gc_ls_to_gdp = grangercausalitytests(df_clean[["d_ln_gdp", "d_ln_ls"]], maxlag=20)

for lag in gc_lags:
    f_gdp_ls, p_gdp_ls = gc_gdp_to_ls[lag][0]["ssr_ftest"][:2]
    f_ls_gdp, p_ls_gdp = gc_ls_to_gdp[lag][0]["ssr_ftest"][:2]
    
    gc_records.append({
        "Lag (Q)": lag,
        "Horizon": f"{lag // 4} Year(s)",
        "GDP -> Share Adjustment (F)": f"{f_gdp_ls:.3f}",
        "GDP -> Share (p-val)": f"{p_gdp_ls:.4f}",
        "Share -> GDP Growth (F)": f"{f_ls_gdp:.3f}",
        "Share -> GDP (p-val)": f"{p_ls_gdp:.4f}",
        "Directionality": "GDP leads Distribution" if p_gdp_ls < 0.05 and p_ls_gdp >= 0.05 else "None"
    })

df_gc = pd.DataFrame(gc_records)
df_gc

Granger Causality
number of lags (no zero) 1
ssr based F test:         F=7.5459  , p=0.0064  , df_denom=313, df_num=1
ssr based chi2 test:   chi2=7.6182  , p=0.0058  , df=1
likelihood ratio test: chi2=7.5278  , p=0.0061  , df=1
parameter F test:         F=7.5459  , p=0.0064  , df_denom=313, df_num=1

Granger Causality
number of lags (no zero) 2
ssr based F test:         F=5.0860  , p=0.0067  , df_denom=310, df_num=2
ssr based chi2 test:   chi2=10.3360 , p=0.0057  , df=2
likelihood ratio test: chi2=10.1700 , p=0.0062  , df=2
parameter F test:         F=5.0860  , p=0.0067  , df_denom=310, df_num=2

Granger Causality
number of lags (no zero) 3
ssr based F test:         F=10.4861 , p=0.0000  , df_denom=307, df_num=3
ssr based chi2 test:   chi2=32.1757 , p=0.0000  , df=3
likelihood ratio test: chi2=30.6318 , p=0.0000  , df=3
parameter F test:         F=10.4861 , p=0.0000  , df_denom=307, df_num=3

Granger Causality
number of lags (no zero) 4
ssr based F test:         F=9.8169  , p=0.0000  , df_denom=304, df_num=4
ssr based chi2 test:   chi2=40.4303 , p=0.0000  , df=4
likelihood ratio test: chi2=38.0242 , p=0.0000  , df=4
parameter F test:         F=9.8169  , p=0.0000  , df_denom=304, df_num=4

Granger Causality
number of lags (no zero) 5
ssr based F test:         F=8.5104  , p=0.0000  , df_denom=301, df_num=5
ssr based chi2 test:   chi2=44.1071 , p=0.0000  , df=5
likelihood ratio test: chi2=41.2552 , p=0.0000  , df=5
parameter F test:         F=8.5104  , p=0.0000  , df_denom=301, df_num=5

Granger Causality
number of lags (no zero) 6
ssr based F test:         F=6.5769  , p=0.0000  , df_denom=298, df_num=6
ssr based chi2 test:   chi2=41.1828 , p=0.0000  , df=6
likelihood ratio test: chi2=38.6752 , p=0.0000  , df=6
parameter F test:         F=6.5769  , p=0.0000  , df_denom=298, df_num=6

Granger Causality
number of lags (no zero) 7
ssr based F test:         F=6.2568  , p=0.0000  , df_denom=295, df_num=7
ssr based chi2 test:   chi2=46.0246 , p=0.0000  , df=7
likelihood ratio test: chi2=42.9126 , p=0.0000  , df=7
parameter F test:         F=6.2568  , p=0.0000  , df_denom=295, df_num=7

Granger Causality
number of lags (no zero) 8
ssr based F test:         F=5.3274  , p=0.0000  , df_denom=292, df_num=8
ssr based chi2 test:   chi2=45.1007 , p=0.0000  , df=8
likelihood ratio test: chi2=42.0982 , p=0.0000  , df=8
parameter F test:         F=5.3274  , p=0.0000  , df_denom=292, df_num=8

Granger Causality
number of lags (no zero) 9
ssr based F test:         F=4.8298  , p=0.0000  , df_denom=289, df_num=9
ssr based chi2 test:   chi2=46.3263 , p=0.0000  , df=9
likelihood ratio test: chi2=43.1565 , p=0.0000  , df=9
parameter F test:         F=4.8298  , p=0.0000  , df_denom=289, df_num=9

Granger Causality
number of lags (no zero) 10
ssr based F test:         F=4.2118  , p=0.0000  , df_denom=286, df_num=10
ssr based chi2 test:   chi2=45.2101 , p=0.0000  , df=10
likelihood ratio test: chi2=42.1757 , p=0.0000  , df=10
parameter F test:         F=4.2118  , p=0.0000  , df_denom=286, df_num=10

Granger Causality
number of lags (no zero) 11
ssr based F test:         F=3.8962  , p=0.0000  , df_denom=283, df_num=11
ssr based chi2 test:   chi2=46.3414 , p=0.0000  , df=11
likelihood ratio test: chi2=43.1508 , p=0.0000  , df=11
parameter F test:         F=3.8962  , p=0.0000  , df_denom=283, df_num=11

Granger Causality
number of lags (no zero) 12
ssr based F test:         F=3.3525  , p=0.0001  , df_denom=280, df_num=12
ssr based chi2 test:   chi2=43.8224 , p=0.0000  , df=12
likelihood ratio test: chi2=40.9466 , p=0.0001  , df=12
parameter F test:         F=3.3525  , p=0.0001  , df_denom=280, df_num=12

Granger Causality
number of lags (no zero) 13
ssr based F test:         F=3.3212  , p=0.0001  , df_denom=277, df_num=13
ssr based chi2 test:   chi2=47.3842 , p=0.0000  , df=13
likelihood ratio test: chi2=44.0352 , p=0.0000  , df=13
parameter F test:         F=3.3212  , p=0.0001  , df_denom=277, df_num=13

Granger Causality
number of lags (no zero) 14
ssr based F test:         F=3.1085  , p=0.0002  , df_denom=274, df_num=14
ssr based chi2 test:   chi2=48.1250 , p=0.0000  , df=14
likelihood ratio test: chi2=44.6651 , p=0.0000  , df=14
parameter F test:         F=3.1085  , p=0.0002  , df_denom=274, df_num=14

Granger Causality
number of lags (no zero) 15
ssr based F test:         F=2.8111  , p=0.0004  , df_denom=271, df_num=15
ssr based chi2 test:   chi2=46.9903 , p=0.0000  , df=15
likelihood ratio test: chi2=43.6744 , p=0.0001  , df=15
parameter F test:         F=2.8111  , p=0.0004  , df_denom=271, df_num=15

Granger Causality
number of lags (no zero) 16
ssr based F test:         F=2.7551  , p=0.0004  , df_denom=268, df_num=16
ssr based chi2 test:   chi2=49.5103 , p=0.0000  , df=16
likelihood ratio test: chi2=45.8362 , p=0.0001  , df=16
parameter F test:         F=2.7551  , p=0.0004  , df_denom=268, df_num=16

Granger Causality
number of lags (no zero) 17
ssr based F test:         F=2.5169  , p=0.0010  , df_denom=265, df_num=17
ssr based chi2 test:   chi2=48.4380 , p=0.0001  , df=17
likelihood ratio test: chi2=44.9034 , p=0.0003  , df=17
parameter F test:         F=2.5169  , p=0.0010  , df_denom=265, df_num=17

Granger Causality
number of lags (no zero) 18
ssr based F test:         F=2.6092  , p=0.0005  , df_denom=262, df_num=18
ssr based chi2 test:   chi2=53.5976 , p=0.0000  , df=18
likelihood ratio test: chi2=49.3002 , p=0.0001  , df=18
parameter F test:         F=2.6092  , p=0.0005  , df_denom=262, df_num=18

Granger Causality
number of lags (no zero) 19
ssr based F test:         F=2.4522  , p=0.0009  , df_denom=259, df_num=19
ssr based chi2 test:   chi2=53.6066 , p=0.0000  , df=19
likelihood ratio test: chi2=49.2950 , p=0.0002  , df=19
parameter F test:         F=2.4522  , p=0.0009  , df_denom=259, df_num=19

Granger Causality
number of lags (no zero) 20
ssr based F test:         F=2.3628  , p=0.0012  , df_denom=256, df_num=20
ssr based chi2 test:   chi2=54.8247 , p=0.0000  , df=20
likelihood ratio test: chi2=50.3120 , p=0.0002  , df=20
parameter F test:         F=2.3628  , p=0.0012  , df_denom=256, df_num=20

Granger Causality
number of lags (no zero) 1
ssr based F test:         F=1.7692  , p=0.1845  , df_denom=313, df_num=1
ssr based chi2 test:   chi2=1.7861  , p=0.1814  , df=1
likelihood ratio test: chi2=1.7811  , p=0.1820  , df=1
parameter F test:         F=1.7692  , p=0.1845  , df_denom=313, df_num=1

Granger Causality
number of lags (no zero) 2
ssr based F test:         F=0.3907  , p=0.6769  , df_denom=310, df_num=2
ssr based chi2 test:   chi2=0.7941  , p=0.6723  , df=2
likelihood ratio test: chi2=0.7931  , p=0.6726  , df=2
parameter F test:         F=0.3907  , p=0.6769  , df_denom=310, df_num=2

Granger Causality
number of lags (no zero) 3
ssr based F test:         F=0.2267  , p=0.8778  , df_denom=307, df_num=3
ssr based chi2 test:   chi2=0.6956  , p=0.8742  , df=3
likelihood ratio test: chi2=0.6949  , p=0.8744  , df=3
parameter F test:         F=0.2267  , p=0.8778  , df_denom=307, df_num=3

Granger Causality
number of lags (no zero) 4
ssr based F test:         F=0.3577  , p=0.8386  , df_denom=304, df_num=4
ssr based chi2 test:   chi2=1.4730  , p=0.8314  , df=4
likelihood ratio test: chi2=1.4696  , p=0.8320  , df=4
parameter F test:         F=0.3577  , p=0.8386  , df_denom=304, df_num=4

Granger Causality
number of lags (no zero) 5
ssr based F test:         F=1.5255  , p=0.1816  , df_denom=301, df_num=5
ssr based chi2 test:   chi2=7.9062  , p=0.1615  , df=5
likelihood ratio test: chi2=7.8076  , p=0.1672  , df=5
parameter F test:         F=1.5255  , p=0.1816  , df_denom=301, df_num=5

Granger Causality
number of lags (no zero) 6
ssr based F test:         F=1.3035  , p=0.2553  , df_denom=298, df_num=6
ssr based chi2 test:   chi2=8.1619  , p=0.2265  , df=6
likelihood ratio test: chi2=8.0567  , p=0.2340  , df=6
parameter F test:         F=1.3035  , p=0.2553  , df_denom=298, df_num=6

Granger Causality
number of lags (no zero) 7
ssr based F test:         F=1.0625  , p=0.3877  , df_denom=295, df_num=7
ssr based chi2 test:   chi2=7.8156  , p=0.3491  , df=7
likelihood ratio test: chi2=7.7187  , p=0.3580  , df=7
parameter F test:         F=1.0625  , p=0.3877  , df_denom=295, df_num=7

Granger Causality
number of lags (no zero) 8
ssr based F test:         F=1.0682  , p=0.3854  , df_denom=292, df_num=8
ssr based chi2 test:   chi2=9.0431  , p=0.3387  , df=8
likelihood ratio test: chi2=8.9133  , p=0.3497  , df=8
parameter F test:         F=1.0682  , p=0.3854  , df_denom=292, df_num=8

Granger Causality
number of lags (no zero) 9
ssr based F test:         F=1.0191  , p=0.4246  , df_denom=289, df_num=9
ssr based chi2 test:   chi2=9.7750  , p=0.3690  , df=9
likelihood ratio test: chi2=9.6231  , p=0.3819  , df=9
parameter F test:         F=1.0191  , p=0.4246  , df_denom=289, df_num=9

Granger Causality
number of lags (no zero) 10
ssr based F test:         F=1.0369  , p=0.4123  , df_denom=286, df_num=10
ssr based chi2 test:   chi2=11.1307 , p=0.3474  , df=10
likelihood ratio test: chi2=10.9337 , p=0.3627  , df=10
parameter F test:         F=1.0369  , p=0.4123  , df_denom=286, df_num=10

Granger Causality
number of lags (no zero) 11
ssr based F test:         F=0.9181  , p=0.5232  , df_denom=283, df_num=11
ssr based chi2 test:   chi2=10.9197 , p=0.4500  , df=11
likelihood ratio test: chi2=10.7294 , p=0.4662  , df=11
parameter F test:         F=0.9181  , p=0.5232  , df_denom=283, df_num=11

Granger Causality
number of lags (no zero) 12
ssr based F test:         F=0.6893  , p=0.7615  , df_denom=280, df_num=12
ssr based chi2 test:   chi2=9.0105  , p=0.7020  , df=12
likelihood ratio test: chi2=8.8800  , p=0.7131  , df=12
parameter F test:         F=0.6893  , p=0.7615  , df_denom=280, df_num=12

Granger Causality
number of lags (no zero) 13
ssr based F test:         F=0.5823  , p=0.8677  , df_denom=277, df_num=13
ssr based chi2 test:   chi2=8.3078  , p=0.8230  , df=13
likelihood ratio test: chi2=8.1963  , p=0.8306  , df=13
parameter F test:         F=0.5823  , p=0.8677  , df_denom=277, df_num=13

Granger Causality
number of lags (no zero) 14
ssr based F test:         F=0.4957  , p=0.9346  , df_denom=274, df_num=14
ssr based chi2 test:   chi2=7.6740  , p=0.9056  , df=14
likelihood ratio test: chi2=7.5784  , p=0.9101  , df=14
parameter F test:         F=0.4957  , p=0.9346  , df_denom=274, df_num=14

Granger Causality
number of lags (no zero) 15
ssr based F test:         F=0.6127  , p=0.8639  , df_denom=271, df_num=15
ssr based chi2 test:   chi2=10.2413 , p=0.8043  , df=15
likelihood ratio test: chi2=10.0714 , p=0.8152  , df=15
parameter F test:         F=0.6127  , p=0.8639  , df_denom=271, df_num=15

Granger Causality
number of lags (no zero) 16
ssr based F test:         F=0.5403  , p=0.9241  , df_denom=268, df_num=16
ssr based chi2 test:   chi2=9.7097  , p=0.8813  , df=16
likelihood ratio test: chi2=9.5564  , p=0.8887  , df=16
parameter F test:         F=0.5403  , p=0.9241  , df_denom=268, df_num=16

Granger Causality
number of lags (no zero) 17
ssr based F test:         F=0.5872  , p=0.9005  , df_denom=265, df_num=17
ssr based chi2 test:   chi2=11.3013 , p=0.8405  , df=17
likelihood ratio test: chi2=11.0936 , p=0.8517  , df=17
parameter F test:         F=0.5872  , p=0.9005  , df_denom=265, df_num=17

Granger Causality
number of lags (no zero) 18
ssr based F test:         F=0.6300  , p=0.8749  , df_denom=262, df_num=18
ssr based chi2 test:   chi2=12.9410 , p=0.7951  , df=18
likelihood ratio test: chi2=12.6688 , p=0.8109  , df=18
parameter F test:         F=0.6300  , p=0.8749  , df_denom=262, df_num=18

Granger Causality
number of lags (no zero) 19
ssr based F test:         F=0.6345  , p=0.8784  , df_denom=259, df_num=19
ssr based chi2 test:   chi2=13.8715 , p=0.7911  , df=19
likelihood ratio test: chi2=13.5583 , p=0.8088  , df=19
parameter F test:         F=0.6345  , p=0.8784  , df_denom=259, df_num=19

Granger Causality
number of lags (no zero) 20
ssr based F test:         F=0.6195  , p=0.8970  , df_denom=256, df_num=20
ssr based chi2 test:   chi2=14.3754 , p=0.8110  , df=20
likelihood ratio test: chi2=14.0383 , p=0.8285  , df=20
parameter F test:         F=0.6195  , p=0.8970  , df_denom=256, df_num=20
Table 3: Multi-Horizon Granger Causality Tests (1 to 5 Years)
Lag (Q) Horizon GDP -> Share Adjustment (F) GDP -> Share (p-val) Share -> GDP Growth (F) Share -> GDP (p-val) Directionality
0 4 1 Year(s) 9.817 0.0000 0.358 0.8386 GDP leads Distribution
1 8 2 Year(s) 5.327 0.0000 1.068 0.3854 GDP leads Distribution
2 12 3 Year(s) 3.353 0.0001 0.689 0.7615 GDP leads Distribution
3 16 4 Year(s) 2.755 0.0004 0.540 0.9241 GDP leads Distribution
4 20 5 Year(s) 2.363 0.0012 0.620 0.8970 GDP leads Distribution

6.1 Inference

  • Strict Unidirectional Causality: Real GDP growth strongly Granger-causes distribution shifts across all horizons, from 1 year (\(F=9.817, p<0.0001\)) to 5 years (\(F=2.363, p=0.0012\)).
  • Reverse causality from factor shares to output growth is decisively rejected at every lag (\(p \in [0.38, 0.92]\)).
  • Growth dictates subsequent distribution shifts; distribution changes do not drive aggregate growth.

7 VAR Estimation and Impulse Response Functions

A bivariate VAR model in first differences is fitted with lag order selected by the Bayesian Information Criterion (BIC).

Code
var_model = VAR(df_clean[["d_ln_gdp", "d_ln_ls"]])
var_fit = var_model.fit(maxlags=8, ic="bic")
Code
irf = var_fit.irf(20)

fig, axes = plt.subplots(2, 2, figsize=(13, 8), dpi=150)
plt.subplots_adjust(hspace=0.35, wspace=0.25)

periods = np.arange(21)

# IRF calculations
irf_ls = irf.irfs[:, 1, 0]
cum_irf_ls = irf.cum_effects[:, 1, 0]

irf_alpha = -irf_ls
cum_irf_alpha = -cum_irf_ls

# 1. GDP Shock -> Capital Share Proxy Response
axes[0, 0].plot(periods, irf_alpha, color="#d62728", lw=2, marker="o", markersize=4)
axes[0, 0].axhline(0, color="gray", ls="--", lw=0.8)
axes[0, 0].set_title(r"(A) Capital Share Response: $\Delta gdp \to \Delta \alpha$", fontsize=11, fontweight="bold")
axes[0, 0].set_xlabel("Quarters Ahead")
axes[0, 0].set_ylabel("Marginal Response")
axes[0, 0].grid(True, alpha=0.3)

# 2. GDP Shock -> Cumulative Labor Share Response
axes[0, 1].plot(periods, cum_irf_ls, color="#1f77b4", lw=2, marker="s", markersize=4)
axes[0, 1].axhline(0, color="gray", ls="--", lw=0.8)
axes[0, 1].set_title(r"(B) Cumulative Labor Response: $\Delta gdp \to \text{Cum}(\Delta ls)$", fontsize=11, fontweight="bold")
axes[0, 1].set_xlabel("Quarters Ahead")
axes[0, 1].set_ylabel("Cumulative Response")
axes[0, 1].grid(True, alpha=0.3)

# 3. Share Shock -> GDP Response
axes[1, 0].plot(periods, irf.irfs[:, 0, 1], color="gray", lw=1.8, marker="o", markersize=4)
axes[1, 0].axhline(0, color="gray", ls="--", lw=0.8)
axes[1, 0].set_title(r"(C) Feedback to GDP: $\Delta ls \to \Delta gdp$ (Zero Bound)", fontsize=11, fontweight="bold")
axes[1, 0].set_xlabel("Quarters Ahead")
axes[1, 0].set_ylabel("Response")
axes[1, 0].grid(True, alpha=0.3)

# 4. Share Shock -> Cumulative GDP Response
axes[1, 1].plot(periods, irf.cum_effects[:, 0, 1], color="gray", lw=1.8, marker="s", markersize=4)
axes[1, 1].axhline(0, color="gray", ls="--", lw=0.8)
axes[1, 1].set_title(r"(D) Cumulative Feedback to GDP (Zero Bound)", fontsize=11, fontweight="bold")
axes[1, 1].set_xlabel("Quarters Ahead")
axes[1, 1].set_ylabel("Cumulative Response")
axes[1, 1].grid(True, alpha=0.3)

plt.show()
Figure 3: 20-Quarter Cholesky Impulse Response Functions: Factor Share Reaction to GDP Shock

7.1 Impulse Response Dynamics

  1. Capital Surges and Reverts: A 1-standard-deviation positive GDP shock triggers an immediate jump in the capital share. By quarter 1, this response collapses into negative territory, beginning a steady return to baseline.
  2. Delayed Labor Windfall: As excess profit margins decay, cumulative labor share gains expand over quarters 4 to 8 before leveling off.
  3. Absence of Reverse Feedback: Distributional shocks exert zero cumulative impact on output growth.

8 Conclusion

Empirical evidence from 1947 to 2026 confirms three fundamental macroeconomic properties:

  1. Capital captures initial growth windfalls: In the quarter growth occurs (\(k = 0\)), contractual wage stickiness channels virtually the entire output surge into firm margins (\(\rho_\alpha = +0.537\)).
  2. Excess margins mean-revert over 1 to 5 years: Tightening labor markets and competitive adjustments systematically erode the initial profit overshoot.
  3. Trickle-down is a mechanical, lagged windfall: Labor share gains do not initiate growth. Rather, labor passively reaps a delayed benefit as capital’s temporary profit spike reverts toward equilibrium.