Stocks with Outperform Ratings Beat the Market


I recently began investing and was wondering how good analysts are at predicting the future of a company. So here is a short data analysis of my curiosity!

In short, we will be answering these hypotheses:

  1. Price targets can accurately reflect the future price of a company.
  2. Some analysts can predict better than others.
  3. A “buy” or “outperform” rating will on average predict a stock moving up.
  4. Some analyst ratings are better than others.
  5. If we were to invest only in stocks with “buy”/”outperform” ratings, we can beat the market.

A price target is the price a financial analyst believes that a stock will reach in a year.

A performance rating is the rating a financial analyst assigns a stock that comes from their combined research and analysis of the company.


Since my investments are mostly in Canada, I will be focusing on Canadian equities. To reduce the amount of noise, I looked at companies with the following conditions:

  • Listed on the TSX
  • Market cap over $1 billion
  • Stock price over $5

The source data for companies can be found here: .

All source code can be found here:

Next, to get the price targets and performance ratings, I used Marketbeat and for stock price information, I used the “unofficial” Yahoo Finance api . One restriction is that Marketbeat only had ratings for the last 2 years but it should be enough data to look back at enough ratings.

For each analyst rating assignment, I looked at the 10 day average centered around when it was assigned and the 10 days average centered around a year in time.

After some webscraping and html/json parsing we have the dataframe with sample rows:

0RYTD Securities78.00Hold70.2828562016-03-0297.8328572017-03-02
2RYTD Securities80.00Buy69.0299992016-02-2597.6562512017-02-25

Each row corresponds to a rating issued by an analyst with the following attributes:

  • ticker: Ticker symbol
  • analyst: Analyst who rated
  • target: Target price issued by analyst
  • rating: Rating issued by analyst
  • aver_close_at_analysis: 10 days average stock price centered on analysis date
  • analysis_date: When the analysis was issued
  • aver_close_at_12m: 10 days average stock price centered 12 months from analysis date
  • 12m_date: 1 year from the analysis date


We can calculate the error between the target price and actual price as follows:


( t = ) target price,

(p_0 = ) price at analysis,

(p_1 = ) price at 12 years after analysis

(error = 100 times frac{t – p_0}{p_0} – frac{p_1 – p_0}{p_0} )

Intuitively, this is difference in percentage change from the prediction and the actual. For example, error = 5 means the target price was 5% higher than then actual percentage change.

In code:

df['target_perc'] = (df['target'] - df['aver_close_at_analysis'])/df['aver_close_at_analysis'] * 100
df['real_perc'] = (df['aver_close_at_12m'] - df['aver_close_at_analysis'])/df['aver_close_at_analysis'] * 100
df['error'] = (df['target_perc']-df['real_perc'])
df['abs_error'] = abs(df['error'])
0RYTD Securities78.0097.83285710.98012239.198750-28.218627
2RYTD Securities80.0097.65625115.89164341.469292-25.577649

A quick glance at the data shows that some of the ratings have very high variance. Therefore, we should try to reduce the noise of our error measurement by getting rid of some outliers. We will do so by removing outliers in the 10th and 90th percentiles. We also remove analysts with less than 100 ratings, so we can compare the most important analysts.

With pandas, we can easily group the data by analyst and aggregate attributes with different functions:

def filter_tail(data, p1=10, p2=90):
    q1 = np.percentile(data, p1)
    q3 = np.percentile(data, p2)
    return data[(data > q1) & (data  100].sort_values(by='mean_no_outliers')
48National Bank Financial16.365463-2.53216413.425469-40.09249130.074729251
3BMO Capital Markets18.982738-2.28236614.435614-55.86663735.678834248
7Barclays PLC16.120963-0.58609013.128664-35.65792037.853971212
12Canaccord Genuity18.1010544.70344714.967193-35.79579847.363220302
58Royal Bank of Canada18.1988465.54486415.402881-34.36224750.268754584
66TD Securities20.1790275.86884917.002451-44.25442747.655748551
54Raymond James Financial, Inc.21.5001477.58074818.711069-42.02528051.284896269

We can also plot the means and standard deviations as error plots:

From the aggregate table, we see that Barclays PLC has the least mean absolute error, i.e., its error is closest to 0 and is the most accurate. Barclays PLC also has the “tightest” standard deviation, so it is also the most precise. However, we see that the standard deviations for each analyst is very large; so the precision of each analyst is very low. Barclays PLC has a standard deviation of 16% which we can interpret as 95% of price targets will be +/- 32% off. For example, if TD Bank current stock price is $100 and Barclays PLC gives a price target for $100, all we can reasonably expect is the stock price to range from ~$70 to ~$130.

Thus we can answer our first two hypotheses:

  1. Analysts are on average, accurate in their predictions with their mean error close to 0. However, price targets cannot precisely predict the future of a company in 12 months.
  2. According to the data, Barclays PLC has the most accurate and precise price targets, but only by a small margin.

A more intuitive image of precision vs accuracy:

Next, we will look at analyst ratings and explore their relation to stock performance.

Using pandas again, we can easily filter out price targets with no rating and only take the ratings from analysts that care about (in the previous table). We can also easily group by each analyst and rating and aggregate with different functions on different attribute.

ratings = df[(df['rating'] != 'NaN') & (df['analyst'].isin(analysts['analyst']))]
ratings_agg = ratings.groupby(['analyst', 'rating'], as_index=False).agg({
        'error': {
            'mae': lambda xs: np.mean(np.abs(filter_tail(xs))),
        'real_perc': {
            'mean': 'mean',
            'median': 'median',
            '10p': lambda xs: np.percentile(xs, 10),
            '90p': lambda xs: np.percentile(xs, 90),
            'count': 'count',
        'target_perc': {
            'median': 'median',
            '10p': lambda xs: np.percentile(xs, 10),
            '90p': lambda xs: np.percentile(xs, 90),

ratings_agg.columns = list(map('_'.join, ratings_agg.columns.values))
ratings_agg[ratings_agg['real_perc_count'] > 10]


  • target_perc_10p: 10th percentile for price target change percentage
  • target_perc_90p: 90th percentile for price target change percentage
  • tarc_perc_median: median for perice target change percentage
  • error_mae: mean absolute error
  • real_perc_10p: 10th percentile for real price change percentage
  • real_perc_90p: 90th percentile for real price change percentage
  • real_perc_median: median for real price change percentage
  • real_perc_count: number of ratings
  • real_perc_mean: mean of real price change percentage

Sample rows:

0BMO Capital MarketsMarket Perform-0.24954223.0494167.82642917.048387-13.6240288229.30626986.57110911.962594
2BMO Capital MarketsOutperform9.33328934.09131018.86321613.267287-13.43022011622.21540164.71122315.109094
5Barclays PLCEqual Weight-5.96463610.8349894.67284411.317883-23.5408327211.51175437.4612449.239600

Now we take only analyst ratings with at least 20 and then sort by stock performance (change in stock price over a year). We can take the top 10 and perform more analysis on those.

top_ratings = ratings_agg[ratings_agg['real_perc_count'] > 20]
top_ratings = top_ratings.sort_values('real_perc_mean', ascending=False)
top_ratings['analyst_rating'] = top_ratings['analyst_'] + ' ' + top_ratings['rating_']
top_analyst_ratings = top_ratings['analyst_rating'].head(10)

Sorted by real price change percentage:

32National Bank FinancialSector Perform0.27782745.3371018.69565114.170428-5.7380968129.88759661.18229317.410111National Bank Financial Sector Perform
0BMO Capital MarketsMarket Perform-0.24954223.0494167.82642917.048387-13.6240288229.30626986.57110911.962594BMO Capital Markets Market Perform
54TD SecuritiesAction List Buy18.33372477.70195438.00183019.629657-0.8937053628.33403871.45949515.928740TD Securities Action List Buy
21Canaccord GenuityBuy8.34073561.22620324.08597418.796959-16.56173317727.08669086.23446418.039215Canaccord Genuity Buy
31National Bank FinancialOutperform7.70553953.57934318.92963312.407454-1.52692511524.52442859.09362822.213398National Bank Financial Outperform
2BMO Capital MarketsOutperform9.33328934.09131018.86321613.267287-13.43022011622.21540164.71122315.109094BMO Capital Markets Outperform
14CIBCSector Outperformer9.71464859.84648133.65224321.688542-14.5185624422.20502260.02093222.218981CIBC Sector Outperformer
43Royal Bank of CanadaSector Perform-0.67878836.51616811.10198313.099112-11.59006022021.33016051.28220110.498096Royal Bank of Canada Sector Perform
36Raymond James Financial, Inc.Outperform9.43780454.48369321.23652215.914385-18.69577610620.77383559.15984413.004491Raymond James Financial, Inc. Outperform
49ScotiabankOutperform7.23995550.40248519.08214115.926344-21.63287524618.40788551.02315914.897374Scotiabank Outperform

We can make an error plot for the mean and standard deviation of the real percentage change for each analyst rating:

We can see that stocks with the top analyst ratings go up on average 25% in a year which is very good. Based on the error plot, TD Security Action List Buy seems to perform the best in terms of high mean and lower variance. Although there is high variance, the mean is more meaningful in this case. If we were to invest $1000 in each of the stocks when were given the rating, we would make about $1250 on average after a year, which is what we really care about. The TSX index went up 11% and TSX index annualized return is 9.1%. So we’re actually beating the market by ~16% with this strategy!

However, keep in mind that this data is for the last 2 years and is not indicative of future performance. On the other hand, I believe this strategy could make sense since analysts put significant effort and research into their rating and also because of the influence of the rating. People probably trust the analysts and would likely invest knowing that the stock has a good rating thus self fulfilling the rating.

With this analysis, we can conclude our last 3 hypotheses:

  1. A buy or outperform rating will on average go up on average by 15-20%.
  2. TD Security Action List Buy appears to be the strongest indicator for a stock to perform well.

  3. If we buy stocks with the top 10 ratings when they get issued and sell in exactly one year, we will beat the market by ~16%.


  • Price targets aren’t a good indicator of where the price of a stock will go.
  • The top performance ratings are a good indicator for a stock performing well.
  • You could possibly beat the market by only buying stocks with sector outperforms or buy ratings and selling in one year.

Please keep in mind that I am by no means a financial expert and am not certified to give financial advice.

All the code can be found here:

Hacker News稿源:Hacker News (源链) | 关于 | 阅读提示

本站遵循[CC BY-NC-SA 4.0]。如您有版权、意见投诉等问题,请通过eMail联系我们处理。
酷辣虫 » 综合编程 » Stocks with Outperform Ratings Beat the Market

喜欢 (0)or分享给?

专业 x 专注 x 聚合 x 分享 CC BY-NC-SA 4.0

使用声明 | 英豪名录