1. Create the Toy Dataset
letters_data <- tibble(
  letter_id = 1:10,
  manager_id = c(rep("Ackman", 5), rep("Loeb", 5)),
  quarter = as.Date(c("2020-03-31", "2020-06-30", "2020-09-30", "2020-12-31", "2021-03-31",
                      "2020-03-31", "2020-06-30", "2020-09-30", "2020-12-31", "2021-03-31")),
  letter_text = c(
    "Ackman Letter Q1 2020 example.",
    "Ackman Letter Q2 2020 example.",
    "Ackman Letter Q3 2020 example.",
    "Ackman Letter Q4 2020 example.",
    "Ackman Letter Q1 2021 example.",
    "Loeb Letter Q1 2020 example.",
    "Loeb Letter Q2 2020 example.",
    "Loeb Letter Q3 2020 example.",
    "Loeb Letter Q4 2020 example.",
    "Loeb Letter Q1 2021 example."
  ),
  n_positive = c(14, 18, 20, 16, 12, 10, 11, 14, 13, 15),
  n_negative = c(10, 12, 15, 9, 10, 8, 12, 13, 11, 10),
  n_total_words = c(400, 420, 410, 430, 390, 395, 415, 400, 405, 420),
  sentiment_score = (n_positive - n_negative) / n_total_words,
  manager_return = c(0.03, 0.02, 0.01, 0.05, 0.04, 0.01, 0.03, 0.04, 0.06, 0.07),
  sp500_return = c(-0.02, 0.08, 0.06, 0.09, 0.06, -0.02, 0.08, 0.06, 0.09, 0.06)
) %>%
  mutate(alpha = manager_return - sp500_return)

head(letters_data)
## # A tibble: 6 × 11
##   letter_id manager_id quarter    letter_text              n_positive n_negative
##       <int> <chr>      <date>     <chr>                         <dbl>      <dbl>
## 1         1 Ackman     2020-03-31 Ackman Letter Q1 2020 e…         14         10
## 2         2 Ackman     2020-06-30 Ackman Letter Q2 2020 e…         18         12
## 3         3 Ackman     2020-09-30 Ackman Letter Q3 2020 e…         20         15
## 4         4 Ackman     2020-12-31 Ackman Letter Q4 2020 e…         16          9
## 5         5 Ackman     2021-03-31 Ackman Letter Q1 2021 e…         12         10
## 6         6 Loeb       2020-03-31 Loeb Letter Q1 2020 exa…         10          8
## # ℹ 5 more variables: n_total_words <dbl>, sentiment_score <dbl>,
## #   manager_return <dbl>, sp500_return <dbl>, alpha <dbl>
summary(letters_data)
##    letter_id      manager_id           quarter           letter_text       
##  Min.   : 1.00   Length:10          Min.   :2020-03-31   Length:10         
##  1st Qu.: 3.25   Class :character   1st Qu.:2020-06-30   Class :character  
##  Median : 5.50   Mode  :character   Median :2020-09-30   Mode  :character  
##  Mean   : 5.50                      Mean   :2020-09-29                     
##  3rd Qu.: 7.75                      3rd Qu.:2020-12-31                     
##  Max.   :10.00                      Max.   :2021-03-31                     
##    n_positive      n_negative   n_total_words   sentiment_score    
##  Min.   :10.00   Min.   : 8.0   Min.   :390.0   Min.   :-0.002410  
##  1st Qu.:12.25   1st Qu.:10.0   1st Qu.:400.0   1st Qu.: 0.004970  
##  Median :14.00   Median :10.5   Median :407.5   Median : 0.007564  
##  Mean   :14.30   Mean   :11.0   Mean   :408.5   Mean   : 0.007988  
##  3rd Qu.:15.75   3rd Qu.:12.0   3rd Qu.:418.8   3rd Qu.: 0.012123  
##  Max.   :20.00   Max.   :15.0   Max.   :430.0   Max.   : 0.016279  
##  manager_return    sp500_return        alpha        
##  Min.   :0.0100   Min.   :-0.020   Min.   :-0.0600  
##  1st Qu.:0.0225   1st Qu.: 0.060   1st Qu.:-0.0475  
##  Median :0.0350   Median : 0.060   Median :-0.0250  
##  Mean   :0.0360   Mean   : 0.054   Mean   :-0.0180  
##  3rd Qu.:0.0475   3rd Qu.: 0.080   3rd Qu.: 0.0025  
##  Max.   :0.0700   Max.   : 0.090   Max.   : 0.0500
  1. Sentiment Score Over Time
ggplot(letters_data, aes(x = quarter, y = sentiment_score, color = manager_id)) +
  geom_line() +
  geom_point() +
  geom_smooth(se = FALSE, method = "loess") +
  facet_wrap(~ manager_id) +
  theme_minimal() +
  labs(
    title = "Sentiment Score Over Time by Manager",
    x = "Quarter",
    y = "Sentiment Score"
  )
## `geom_smooth()` using formula = 'y ~ x'
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : span too small.  fewer data values than degrees of freedom.
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : pseudoinverse used at 18350
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : neighborhood radius 184.83
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : reciprocal condition number 0
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : There are other near singularities as well. 33792
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : span too small.  fewer data values than degrees of freedom.
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : pseudoinverse used at 18350
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : neighborhood radius 184.83
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : reciprocal condition number 0
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : There are other near singularities as well. 33792

  1. Alpha Over Time (Manager Return minus S&P 500 Return)
ggplot(letters_data, aes(x = quarter, y = alpha, color = manager_id)) +
  geom_line() +
  geom_point() +
  geom_smooth(se = FALSE, method = "loess") +
  facet_wrap(~ manager_id) +
  theme_minimal() +
  labs(
    title = "Alpha Over Time by Manager",
    x = "Quarter",
    y = "Alpha (Manager Return - S&P500)"
  )
## `geom_smooth()` using formula = 'y ~ x'
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : span too small.  fewer data values than degrees of freedom.
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : pseudoinverse used at 18350
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : neighborhood radius 184.83
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : reciprocal condition number 0
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : There are other near singularities as well. 33792
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : span too small.  fewer data values than degrees of freedom.
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : pseudoinverse used at 18350
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : neighborhood radius 184.83
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : reciprocal condition number 0
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : There are other near singularities as well. 33792

  1. Panel Regressions (Fixed Effects)
# Regression 1: Alpha ~ Sentiment Score
model_alpha <- feols(alpha ~ sentiment_score | manager_id + quarter, data = letters_data, cluster = ~manager_id)

# Regression 2: Manager Return ~ Sentiment Score
model_manager_return <- feols(manager_return ~ sentiment_score | manager_id + quarter, data = letters_data, cluster = ~manager_id)

# Regression 3: S&P500 Return ~ Sentiment Score
model_sp500_return <- feols(sp500_return ~ sentiment_score | manager_id + quarter, data = letters_data, cluster = ~manager_id)

knitr::kable(head(letters_data), caption = "First few rows of letters_data")
First few rows of letters_data
letter_id manager_id quarter letter_text n_positive n_negative n_total_words sentiment_score manager_return sp500_return alpha
1 Ackman 2020-03-31 Ackman Letter Q1 2020 example. 14 10 400 0.0100000 0.03 -0.02 0.05
2 Ackman 2020-06-30 Ackman Letter Q2 2020 example. 18 12 420 0.0142857 0.02 0.08 -0.06
3 Ackman 2020-09-30 Ackman Letter Q3 2020 example. 20 15 410 0.0121951 0.01 0.06 -0.05
4 Ackman 2020-12-31 Ackman Letter Q4 2020 example. 16 9 430 0.0162791 0.05 0.09 -0.04
5 Ackman 2021-03-31 Ackman Letter Q1 2021 example. 12 10 390 0.0051282 0.04 0.06 -0.02
6 Loeb 2020-03-31 Loeb Letter Q1 2020 example. 10 8 395 0.0050633 0.01 -0.02 0.03
  1. Combined Regression Table
modelsummary(
  list(
    "Alpha" = model_alpha,
    "Manager Return" = model_manager_return,
    "S&P 500 Return" = model_sp500_return
  ),
  stars = TRUE,
  gof_omit = "IC|Log|Adj|F|RMSE",
  title = "Panel Regressions: Sentiment Predicting Returns",
  notes = "Standard errors clustered at manager level; Manager and Quarter fixed effects included."
)
Panel Regressions: Sentiment Predicting Returns
Alpha Manager Return S&P 500 Return
+ p < 0.1, * p < 0.05, ** p < 0.01, *** p < 0.001
Standard errors clustered at manager level; Manager and Quarter fixed effects included.
sentiment_score 0.514*** 0.514*** 0.000
(0.000) (0.000) (0.000)
Num.Obs. 10 10 10
R2 0.934 0.781 1.000
R2 Within 0.049 0.049 0.000
Std.Errors by: manager_id by: manager_id by: manager_id