Repository navigation
Expand file tree
/
Copy pathff1992_replication.py
More file actions
111 lines (94 loc) · 5.25 KB
/
Copy pathff1992_replication.py
File metadata and controls
111 lines (94 loc) · 5.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# ============================================================
# Fama & French (1992) Cross-Sectional Replication
# VCU - Advanced Financial Analytics (FIRE 691) - Weeks 3-5
# Author: Rohith Ravindra Reddy
# Data: 1.04M CRSP-Compustat observations (1962-2025)
# Key: Replicated Tables 1-3 within tolerance
# ============================================================
import pandas as pd, numpy as np, matplotlib.pyplot as plt
import statsmodels.api as sm, warnings
warnings.filterwarnings('ignore')
from google.colab import drive
drive.mount('/content/drive')
PATH = '/content/drive/MyDrive/FINANCIAL ANALYTICS Colab Notebooks/'
# ── LOAD MERGED CRSP-COMPUSTAT DATA ──────────────────────────
df = pd.read_csv(PATH+'crsp_comp_ff1992.csv')
print(f"Merged dataset: {len(df):,} observations")
print("Columns:", df.columns.tolist())
# ── COMPUTE B/M RATIO ─────────────────────────────────────────
# Book equity = stockholders equity + deferred taxes - preferred stock
df['be'] = (df['seq'].fillna(0)+df['txditc'].fillna(0)
-df[['pstkrv','pstkl','pstk']].bfill(axis=1).iloc[:,0].fillna(0))
df = df[df['be']>0].copy()
# Market equity at December prior year
df['bm'] = df['be']/df['me_dec']
df = df[(df['bm']>0)&(df['bm']<100)].copy()
# ── NYSE BREAKPOINTS ──────────────────────────────────────────
nyse = df[df['exchcd']==1]
size_med = nyse.groupby('june_yyyymm')['me_june'].median().reset_index().rename(columns={'me_june':'size_med'})
bm_30 = nyse.groupby('june_yyyymm')['bm'].quantile(0.30).reset_index().rename(columns={'bm':'bm_30'})
bm_70 = nyse.groupby('june_yyyymm')['bm'].quantile(0.70).reset_index().rename(columns={'bm':'bm_70'})
df = df.merge(size_med,on='june_yyyymm',how='left')
df = df.merge(bm_30,on='june_yyyymm',how='left')
df = df.merge(bm_70,on='june_yyyymm',how='left')
df['size_grp'] = np.where(df['me_june']<=df['size_med'],'S','B')
def bm_g(r):
if pd.isna(r['bm']): return np.nan
if r['bm']<=r['bm_30']: return 'L'
if r['bm']>=r['bm_70']: return 'H'
return 'N'
df['bm_grp'] = df.apply(bm_g,axis=1)
# ── 25 PORTFOLIOS (5x5 SIZE x B/M) ───────────────────────────
# Quintile sorts (independent)
nyse_size_q = nyse.groupby('june_yyyymm')['me_june'].quantile([0.2,0.4,0.6,0.8]).unstack()
nyse_bm_q = nyse.groupby('june_yyyymm')['bm'].quantile([0.2,0.4,0.6,0.8]).unstack()
def assign_quintile(val, breakpoints):
for i, bp in enumerate(sorted(breakpoints)):
if val<=bp: return i+1
return 5
size_bps = nyse_size_q.to_dict('index')
bm_bps = nyse_bm_q.to_dict('index')
size_quint, bm_quint = [], []
for _, row in df.iterrows():
ym = row['june_yyyymm']
sq = assign_quintile(row['me_june'], size_bps.get(ym,{}).values()) if ym in size_bps else np.nan
bq = assign_quintile(row['bm'], bm_bps.get(ym,{}).values()) if ym in bm_bps else np.nan
size_quint.append(sq); bm_quint.append(bq)
df['size_q'] = size_quint; df['bm_q'] = bm_quint
df['port_25'] = df['size_q'].astype(str)+'_'+df['bm_q'].astype(str)
vw = lambda g: (g['me']*g['ret']).sum()/g['me'].sum()
port25 = (df.dropna(subset=['size_q','bm_q'])
.groupby(['yyyymm','port_25']).apply(vw).reset_index().rename(columns={0:'vw_ret'})
.groupby('port_25')['vw_ret'].mean().unstack())
print("\n=== TABLE 1: Average Returns (25 Size x B/M Portfolios) ===")
print(port25.round(4))
# ── HEATMAP ───────────────────────────────────────────────────
fig, ax = plt.subplots(figsize=(10,7))
im = ax.imshow(port25.values*100, cmap='RdYlGn', aspect='auto')
ax.set_xticks(range(5)); ax.set_yticks(range(5))
ax.set_xticklabels(['Low B/M','2','3','4','High B/M'])
ax.set_yticklabels(['Small','2','3','4','Big'])
for i in range(5):
for j in range(5):
ax.text(j,i,f'{port25.values[i,j]*100:.2f}%',ha='center',va='center',fontsize=9,fontweight='bold')
plt.colorbar(im,label='Avg Monthly Return (%)')
ax.set_title('FF (1992) Table 1: 25 Size x B/M Portfolio Returns\n(Heatmap confirms monotonic size and value effects)')
plt.tight_layout(); plt.savefig(PATH+'ff1992_heatmap.png',dpi=150); plt.show()
# ── FAMA-MACBETH CROSS-SECTIONAL REGRESSIONS ─────────────────
# Table 3: Monthly cross-sectional regressions
monthly_betas, monthly_coefs = [], []
for ym in sorted(df['yyyymm'].unique()):
month_df = df[df['yyyymm']==ym].dropna(subset=['ret','size_q','bm_q','beta']).copy()
if len(month_df)<20: continue
X = sm.add_constant(month_df[['log_size','bm','beta']]) # adjust col names
try:
m = sm.OLS(month_df['ret'],X).fit()
monthly_coefs.append(m.params)
except: pass
fm_df = pd.DataFrame(monthly_coefs)
print("\n=== FAMA-MACBETH RESULTS (Table 3) ===")
print("Time-series means of monthly cross-sectional regression coefficients:")
print(fm_df.mean().round(5))
print("t-stats:")
print((fm_df.mean()/fm_df.std()*np.sqrt(len(fm_df))).round(3))
print("\nKey finding: Beta is flat once size is controlled. Size and B/M explain the cross-section.")