Environmental, Social, and Governance (ESG) factors have become a cornerstone of modern investing, reflecting the growing demand for sustainable and ethical business practices. ESG evaluates companies on their environmental impact, social responsibility, and governance standards, offering a comprehensive lens beyond traditional financial metrics. This approach is crucial as it helps investors identify long-term risks and opportunities while aligning portfolios with societal values. With mounting concerns over climate change, inequality, and corporate accountability, ESG fosters resilience, mitigates risks, and drives positive change. By integrating ESG principles, businesses not only enhance their reputation but also contribute to a sustainable future and long-term value creation.
A stock screener is a tool that filters stocks based on specific criteria, such as financial metrics or ESG factors, to streamline investment decisions. Investors should use them to:
- Identify companies meeting ESG and financial goals.
- Compare stocks across industries for sustainability performance.
- Avoid companies with poor environmental or governance practices.
- Customize filters to align with personal values and strategies.
Don’t forget also that integrating ESG principles into investing is not just about financial returns; it’s about fostering ethical responsibility. By prioritizing companies with strong ESG scores, investors can drive positive change, promote sustainability, and contribute to a more equitable future while aligning their portfolios with societal and environmental values.
In this article, we will develop a screener that includes some basic fundamental information, together with ESG data for the last two years.
This wouldn’t be possible without using a data provider like EODHD, which can provide an extensive data set; for each listed company, we can have a holistic 360 view from historical prices and fundamentals to dividends, earnings, splits, and many more. In bullet points, what you can expect in this article is to:
- Get a list of companies of NYSE
- Download some fundamental data
- Download the ESG scores of those companies, and
- finally, we will try to discover some patterns and correlations between company performance and ESG scores
Get the data!
First things first, let’s do our imports and define our API
import requests
import json
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
token = "<YOUR TOKEN HERE>"
Now we need to define our stock universe for which we will create the stock screener. For that, we will use the APIs that support the exchanges. First, let’s check how the API for the list of exchanges behaves:
url = 'https://eodhd.com/api/exchanges-list/'
querystring = {"api_token":token,"fmt":"json"}
response = requests.get(url, params=querystring).json()
df_exchanges = pd.DataFrame.from_dict(response)
df_exchanges[df_exchanges['CountryISO3'] == 'USA']
You will notice that if you search for exchanges based in the US, you will get one result. If you want to run the code of this article for countries other than the US, EODHD API returns a significant number of countries and exchanges like London, Toronto, Frankfurt, and Paris, as well as from Asia and Africa.
This means that to get all the symbols, listed in the NYSE, we should use the API that lists the symbols, and filter them to NYSE stock exchange. Additionally, we are going to look for equities, so we should further filter by type for ‘Common Stock’.
EXCHANGE_CODE = 'US'
url = f'https://eodhd.com/api/exchange-symbol-list/{EXCHANGE_CODE}'
querystring = {"api_token":token,"fmt":"json"}
response = requests.get(url, params=querystring).json()
df_symbols = pd.DataFrame.from_dict(response)
df_symbols = df_symbols[(df_symbols['Exchange'] == 'NYSE') & (df_symbols['Type'] == 'Common Stock')]
df_symbols
There are 2283 stocks in this list. We just need now to keep everything to a dictionary as we move along the code.
symbols = df_symbols.set_index('Code').to_dict(orient='index')
At this point, we will select some basic fundamentals that every screener should have, like market capitalization, PE ratio, profit margin, PriceSalesTTM, and Analyst Ratings. We will use the API for the fundamentals. EODHD API returns hundreds of data points, such as pre-recalculated technical metrics, valuations, and detailed insider transactions. With a small tweak in the code, you can easily add all this to the dataset below.
for symbol in symbols:
url = f'https://eodhd.com/api/fundamentals/{symbol}.{EXCHANGE_CODE}'
querystring = {"api_token":token,"fmt":"json"}
try:
response = requests.get(url, params=querystring).json()
symbols[symbol]['Sector'] = response.get('General', {}).get('Sector', None)
symbols[symbol]['Industry'] = response.get('General', {}).get('Industry', None)
symbols[symbol]['MarketCapitalization'] = response.get('Highlights', {}).get('MarketCapitalization', None)
symbols[symbol]['PERatio'] = response.get('Highlights', {}).get('PERatio', None)
symbols[symbol]['ProfitMargin'] = response.get('Highlights', {}).get('ProfitMargin', None)
symbols[symbol]['PriceSalesTTM'] = response.get('Valuation', {}).get('PriceSalesTTM', None)
symbols[symbol]['AnalystRating'] = response.get('AnalystRatings', {}).get('Rating', None)
except Exception as e:
print(f"Error for {symbol} with error {e}")
continue
However, as we said, this article focuses on the ESG data for each company. Using the ESG API, we will also gather the ESG rating for each stock. We will get only the last 2 years that are needed in our case, but EODHD API has more than 10 years of data with the detailed yearly and quarterly ESG score, together with the detailed ones like e (environmental), s (social), g (governance) data.
for symbol in symbols:
for year in [2022, 2023]:
url = f'https://eodhd.com/api/mp/investverte/esg/{symbol}'
querystring = {"api_token": token, "fmt": "json", 'frequency': 'FY', 'year': year}
try:
response = requests.get(url, params=querystring).json()
symbols[symbol][f'{str(year)}_esg'] = response[0].get('esg', None)
symbols[symbol][f'{str(year)}_e'] = response[0].get('e', None)
symbols[symbol][f'{str(year)}_s'] = response[0].get('s', None)
symbols[symbol][f'{str(year)}_g'] = response[0].get('g', None)
except Exception as e:
print(f"Error for {symbol} and {year} with error {e}")
continue
Now we will convert our data to a dataframe, and also we will drop some rows with missing data.
df = pd.DataFrame(symbols).transpose()
# Drop rows where 2022_esg or 2023_esg with no value
df.dropna(subset=['2022_esg', '2023_esg'], inplace=True)
df = df[(df['2022_esg'] != 0) & (df['2023_esg'] != 0)]
# Drop rows where sector with no value
df.dropna(subset=['Sector'], inplace=True)
df = df[df['Sector'] != ""]
Get our data ready for action!
Now that we have our data, we should pre-calculate some information that we consider crucial for our screener.
First, we will assign a ranking to the ESG score for each symbol. In fact, we will calculate the ranking globally (for the whole dataframe), as well as the ranking of the symbol in the sector itself. Additionally, we will calculate the difference in position from 2022 to 2023.
df['ESG_Rank_2022'] = df[['2022_esg']].rank(ascending=False)
df['ESG_Rank_2023'] = df[['2023_esg']].rank(ascending=False)
df['Places_Difference'] = df['ESG_Rank_2022'] - df['ESG_Rank_2023']
df['ESG_Rank_Sector_2022'] = df.groupby('Sector')['2022_esg'].rank(ascending=False)
df['ESG_Rank_Sector_2023'] = df.groupby('Sector')['2023_esg'].rank(ascending=False)
df['Places_Difference_Sector'] = df['ESG_Rank_Sector_2022'] - df['ESG_Rank_Sector_2023']
And before we go to the actual analysis, let’s do some grouping on the fundamental data for better understanding.
First, we will give an actual name to the capitalization of the stock. Based on their market capitalization, we will assign them to mega, large, mid, small, micro, and nano.
# Define the bins and labels for each capitalization category
bins = [0, 50_000_000, 300_000_000, 2_000_000_000, 10_000_000_000, 200_000_000_000, float('inf')]
labels = ['nano', 'micro', 'small', 'mid', 'large', 'mega']
# Filter out rows with None values in 'MarketCapitalization'
filtered_df = df.dropna(subset=['MarketCapitalization'])
# Apply pd.cut to the filtered DataFrame
filtered_df.loc[:, 'MarketCapitalization_Binned'] = pd.cut(filtered_df['MarketCapitalization'], bins=bins, labels=labels)
# Merge the result back to the original DataFrame to maintain the structure
df = df.drop(columns=['MarketCapitalization_Binned'], errors='ignore')
df = df.merge(filtered_df[['MarketCapitalization', 'MarketCapitalization_Binned']], how='left',
on='MarketCapitalization')
Some insights
For starters, we can check if we see any major differences between the sectors, with the use of a boxplot:
plt.figure(figsize=(12, 8))
sns.boxplot(x='Sector', y='2023_esg', data=df)
plt.title('ESG Score by Sector for 2023')
plt.xticks(rotation=90)
plt.xlabel('Sector')
plt.ylabel('ESG Score')
plt.show()
This gives us a very high-level understanding, that there are some sectors where their ESG score is higher than the rest of the sectors. In a nutshell:
- All of the sectors look like having a median of around 60+, which indicates a moderate sustainability performance
- The financial sector looks slightly higher than the rest, but without any major outliers either upwards or downwards. The reason behind it, is that the financial sector is more regulated than the rest mostly in their obligation to promote sustainable finance.
- Some sectors like Financials, Consumer Cyclical, and Basic Materials have tighter interquartile ranges, suggesting more consistent ESG performance within these sectors. In those cases, the competitive advantage of an ESG score does not appear to be particularly strong.
Now we should do the same based on the capitalization of the companies.
# Define the order of categories explicitly
category_order = ['nano', 'micro', 'small', 'mid', 'large', 'mega']
plt.figure(figsize=(12, 8))
sns.boxplot(x='MarketCapitalization_Binned', y='2023_esg', data=df, order=category_order)
plt.title('ESG Score by Market Capitalization for 2023')
plt.xticks(rotation=90)
plt.xlabel('Market Capitalization')
plt.ylabel('ESG Score')
plt.show()
This diagram does not show us as much as the one before. All different capitalizations, are the same, with probably the only comment that the mega ones are tighter, again indicating the fact that when you are big, you don’t want to be seen as the ‘bad guy,’ nor do you need to be ‘the best guy’…
The screener
But as promised, this article focuses on creating a custom screener, so we should not spend too much effort on boring charts, but let’s see how a screener should work.
First, you should save the screener in CSV format, as shown below, and with the help and magic of Excel, you can open this file and start filtering.
df.to_csv('screener.csv', index=False)
Then the limit is the sky. You can start filtering and sorting to identify opportunities to start your investigation. (apparently, this can be done also using Python and pandas, which we are also going to show).
As we said in the beginning, all of us have different approach when it comes to selecting the stocks that we believe have the potential for better profits. Of course, this is very broad, and cannot be described in detail, so we will aim to inspire you with ideas, by trying to answer questions that might come to you while having in front of you an Excel with thousands of data points.
Which stock has the best ESG score
# Which stock has the best ESG score
df[df['ESG_Rank_2023'] == 1]
The winner is Fresenius Medical Care Corporation (FMS) with an impressive 81.79.
With some Python, we will be able to get some quick answers
symbol = 'FMS'
selected = df[df['Symbol'] == symbol]
if not selected.empty:
# get the sector and its dataframe
sector = selected['Sector'].values[0]
sector_df = df[df['Sector'] == sector]
# ESG Report
esg_rank = selected['ESG_Rank_2023'].values[0]
place_diff = selected['Places_Difference'].values[0]
esg_rank_sector = selected['ESG_Rank_Sector_2023'].values[0]
place_diff_sector = selected['Places_Difference_Sector'].values[0]
# PE Ratio
pe_ratio = selected['PERatio'].values[0]
better_overall_pe_ratio = (df['PERatio'] > pe_ratio).sum() / len(df) * 100
better_sector_pe_ratio = (sector_df['PERatio'] > pe_ratio).sum() / len(sector_df) * 100
# Profit Margin
profit_margin = selected['ProfitMargin'].values[0]
better_overall_profit_margin = (df['ProfitMargin'] > profit_margin).sum() / len(df) * 100
better_sector_profit_margin = (sector_df['ProfitMargin'] > profit_margin).sum() / len(sector_df) * 100
# PriceSalesTTM
price_sales_ttm = selected['PriceSalesTTM'].values[0]
better_overall_price_sales_ttm = (df['PriceSalesTTM'] > price_sales_ttm).sum() / len(df) * 100
better_sector_price_sales_ttm = (sector_df['PriceSalesTTM'] > price_sales_ttm).sum() / len(sector_df) * 100
# AnalystRating
analyst_rating = selected['AnalystRating'].values[0]
better_overall_analyst_rating = (df['AnalystRating'] < analyst_rating).sum() / len(df) * 100
better_sector_analyst_rating = (sector_df['AnalystRating'] < analyst_rating).sum() / len(sector_df) * 100
print(f'===== ESG ======')
print(f"ESG Rank 2023: {esg_rank} ({place_diff:+.0f} from 2022)")
print(f"ESG Rank 2023 in {sector}: {esg_rank_sector} ({place_diff_sector:+.0f} from 2022)")
print(f'===== PE Ratio ======')
print(f"PE Ratio: {pe_ratio}")
print(f"Percentage of symbols with higher PE Ratio overall: {better_overall_pe_ratio:.2f}% and in {sector} {better_sector_pe_ratio:.2f}%")
print(f'===== Profit Margin ======')
print(f"Profit Margin: {profit_margin}")
print(f"Percentage of symbols with higher Profit Margin overall: {better_overall_profit_margin:.2f}% and in {sector} {better_sector_profit_margin:.2f}%")
print(f'===== PriceSalesTTM ======')
print(f"PriceSalesTTM: {price_sales_ttm}")
print(f"Percentage of symbols with higher PriceSalesTTM overall: {better_overall_price_sales_ttm:.2f}% and in {sector} {better_sector_price_sales_ttm:.2f}%")
print(f'===== AnalystRating ======')
print(f"AnalystRating: {analyst_rating}")
print(f"Percentage of symbols with lower AnalystRating overall: {better_overall_analyst_rating:.2f}% and in {sector} {better_sector_analyst_rating:.2f}%")
else:
print(f"Symbol '{symbol}' not found in the dataset.")
The results are:
===== ESG ======
ESG Rank 2023: 1.0 (+0 from 2022)
ESG Rank 2023 in Healthcare: 1.0 (+0 from 2022)
===== PE Ratio ======
PE Ratio: 18.7458
Percentage of symbols with higher PE Ratio overall: 46.80% and in Healthcare 51.72%
===== Profit Margin ======
Profit Margin: 0.0343
Percentage of symbols with higher Profit Margin overall: 65.51% and in Healthcare 50.57%
===== PriceSalesTTM ======
PriceSalesTTM: 0.6747
Percentage of symbols with higher PriceSalesTTM overall: 75.47% and in Healthcare 72.41%
===== AnalystRating ======
AnalystRating: 3.0
Percentage of symbols with lower AnalystRating overall: 5.83% and in Healthcare 3.45%
Fresenius Medical Care excels in ESG performance and valuation metrics, with a low P/S ratio, and looks like it is trading at a discount. However, modest profit margins and neutral analyst sentiment suggest operational challenges and limited near-term growth potential.
Which stock is the best of the mega stocks?
mega_stocks = df[df['MarketCapitalization_Binned'] == 'mega']
mega_stocks.loc[mega_stocks['2023_esg'].idxmax()]
Accenture plc (ACN) looks like a charm with 75.78. A quick look (using the above script) is below:
===== ESG ======
ESG Rank 2023: 9.0 (-2 from 2022)
ESG Rank 2023 in Technology: 1.0 (+0 from 2022)
===== PE Ratio ======
PE Ratio: 30.9668
Percentage of symbols with higher PE Ratio overall: 23.97% and in Technology 28.17%
===== Profit Margin ======
Profit Margin: 0.1119
Percentage of symbols with higher Profit Margin overall: 32.33% and in Technology 30.99%
===== PriceSalesTTM ======
PriceSalesTTM: 3.4155
Percentage of symbols with higher PriceSalesTTM overall: 24.53% and in Technology 26.76%
===== AnalystRating ======
AnalystRating: 4.0
Percentage of symbols with lower AnalystRating overall: 50.38% and in Technology 50.70%
Accenture’s high P/E and price-to-sales ratios reflect premium valuation, which is supported by solid profit margins and balanced analyst sentiment (4.0 rating).
Which has the best ESG Score for the Financial Sector?
We show before, that the financial sector is very close when it comes to ESG ratios, with only one overperforming in that area. Let’s see which one it is.
mega_stocks = df[df['Sector'] == 'Financial Services']
mega_stocks.loc[mega_stocks['2023_esg'].idxmax()]
Janus Henderson Group PLC (JHG) is the one, within a sector with most of them in the 60s, this one has an ESG ratio of 75.24.
===== ESG ======
ESG Rank 2023: 12.0 (+10 from 2022)
ESG Rank 2023 in Financial Services: 1.0 (+0 from 2022)
===== PE Ratio ======
PE Ratio: 17.5494
Percentage of symbols with higher PE Ratio overall: 50.47% and in Financial Services 48.41%
===== Profit Margin ======
Profit Margin: 0.175
Percentage of symbols with higher Profit Margin overall: 17.29% and in Financial Services 51.59%
===== PriceSalesTTM ======
PriceSalesTTM: 3.0314
Percentage of symbols with higher PriceSalesTTM overall: 28.20% and in Financial Services 69.84%
===== AnalystRating ======
AnalystRating: 3.0
Percentage of symbols with lower AnalystRating overall: 5.83% and in Financial Services 8.73%
A quick look at the fundamentals will show interesting results since it looks like the PE ratio is in a moderate state, while the company’s profit margin and price-to-sales are in a good state globally and average in a sector where those numbers are already on the high side. What is interesting is that it is not the analyst’s favorite. However, we are talking about a company that YTD (~11 months at the time of reading this article) has a 75% steady increase in their stock price.
Let’s wrap it up and takeaways
The aim of this article is to guide you through downloading data, gaining a basic understanding of it, and creating a dataset. This dataset can be explored using tools like Excel to experiment with different approaches and answer key questions. The goal is to empower you with insights for informed decision-making.
ESG Scores as a Strategic Filter. We tried to demonstrate how to integrate Environmental, Social, and Governance (ESG) scores into a stock screener. By ranking companies based on ESG performance, investors can identify businesses that align with sustainability goals and societal values. For example, sectors like Financial Services show consistent ESG performance due to regulatory obligations. Investors can use this information to focus on industries or companies with strong ESG practices, which often indicate long-term resilience and reduced risk exposure.
Sector and Market Capitalization Insights. Analysis of ESG scores by sector and market capitalization reveals trends that investors can leverage. For instance, mega-cap companies tend to have tighter ESG score ranges, reflecting their efforts to maintain reputations while avoiding extreme outliers. Similarly, sectors like Consumer Cyclical or Basic Materials exhibit narrower ESG performance ranges, suggesting less differentiation among peers. These insights help investors tailor strategies, such as targeting undervalued companies within high-performing sectors or avoiding sectors with limited ESG variability.
Customizable Screening for Investment Decisions. The article provides tools to create a custom stock screener that combines ESG data with key financial metrics like P/E ratios, profit margins, and analyst ratings. For instance, Fresenius Medical Care Corporation (FMS) stands out for its high ESG score but has modest profit margins, signaling potential operational challenges. Investors can use these screeners in Excel or Python to filter stocks based on personal criteria, enabling data-driven decisions that align with both financial goals and ethical considerations.
We are still far from proving that companies with top ESG scores will consistently outperform others. However, as methodologies and data improve, the connection between strong ESG practices and financial success will likely become clearer, paving the way for more reliable insights into sustainable investing. So as a closure, there is not much to say.. only 4 words. Invest green, profit clean!