Sustainable Portfolio Selection - An Approach To Impact Modelling


Dr. Caroline Loebhard
Founder, Goldmarie Finanzen GmbH Founder, Goldmarie Finanzen GmbH
Dr. Jennifer Rasch
Founder, Goldmarie Finanzen GmbH Founder, Goldmarie Finanzen GmbH

In our article Sustainable Portfolio Selection -- Markowitz goes ESG we describe the importance of ecological, social and governance (ESG) aspects of financial services and give a first approach, how ESG measures can be included into portfolio selection strategies. Basically, ESG-ratings of financial instruments, like for instance Refinitiv's ESG Scores, can be mathematically treated similar to mean historical returns. As a result, one can balance estimates for risk and/or return with ESG-ratings and compute portfolios of financial instruments with high average ESG Score as well as good risk measures.

In this follow-up article we adhere to the fact that high ESG-ratings can have a different impact in different industrial sectors or countries. We will show, how impact data can be integrated into portfolio selection models with ESG-awareness.


In this tutorial you can learn

  • to get business sector information from Refinitiv and to plot the sector distribution of a weighted portfolio;
  • how to balance volatility as a risk measure with portfolio returns and ESG-measures in portfolio selection;
  • about the basics of impact modeling, and how sector impact information can be included into portfolio selection.

We build on the steps in Sustainable Portfolio Selection -- Markowitz goes ESG, where basically a classical Markowitz model is employed, but returns are replaced by ESG-ratings. This tutorial is structured as follows:

  • Step 1 Get data via eikon, prepare the basic data structures.
  • Step 2 Build the minimum volatility portfolio (MVP) and analyze its business sector distribution.
  • Step 3 Build a portfolio, where volatility is balanced with return and also the ESG-score.
  • Step 4 Build a model for impact-ESG and utilize it to select a portfolio, where volatility is balanced with return and the impact-ESG-score.

Technical prerequisites


import pandas

import numpy

import random

import matplotlib.pyplot as plt

import scipy.optimize as sco 

import os

import refinitiv.dataplatform.eikon as ek


ek.set_app_key('YOUR APPKEY HERE')

Data acquisition and universe selection

Our portfolio will be built from a universe (or a pre-selection) of shares. We will work based on the ETF iShares Core MSCI World UCITS ETF USD which refers to the MSCI World index. In difference to our earlier article, we enforce a lower bound on the ESG rating of financial instruments in the portfolio universe just from the beginning.

Reading the universe from Eikon

We use Eikon to get the constituents of the index. For simplicity we will reduce the list of entries here to 350. Besides the instrument identifer RIC, we load company names, their TR ESG Scores as well as the NACE Classification data which refers to the "Statistical Classification of Economic Activities in the European Community". Lateron, this data will be used to constitute an sector-related ESG impact.


N = 350

constituents, err = ek.get_data(['IWDA.L'], ['TR.ETPConstituentRIC', 'TR.ETPConstituentName'])

constituents.rename(columns={'Constituent RIC': 'ric', 'Constituent Name': 'name'}, inplace=True)

constituents = constituents[['ric','name']][0:N].drop_duplicates(subset=['ric'])

ric_list = list(constituents.ric[constituents.ric.astype(bool)])


df_esg, err = ek.get_data(ric_list, ['TR.CommonName', 'TR.TRESGScore','TR.BusinessSummary','TR.NACEClassification'])

df_esg = df_esg.rename(columns={'Company Common Name':'name', 'Instrument':'ric', 'ESG Score':'esg', 'NACE Classification':'nace'})

df_esg = df_esg.drop_duplicates(subset=['ric']).set_index('ric')

Constructing industrial sector information

There exist different classification systems mapping the economic activities of a company to a business sector. Refinitiv offers a bunch of these business sector information. One can, amongst others, download the sector with respect to the North American Industry Classification System (NAICS), the The Refinitiv Business Classification (TRBC), or -- as it is done here -- the NACE. The data item TR.NACEClassification is composed as follows,


We will utilize the business division encoded in XX and thus extract the string positions [-6:-4]. This data is included to the instruments' pandas dataframe.


nace_code_list =[]

for instr in ric_list:

    df_instr = df_esg.loc[instr]



df_nace = pandas.DataFrame(data=[ric_list,nace_code_list],index=['ric','nace_code']).T.set_index('ric')

df_esg_nace = pandas.concat([df_esg,df_nace],axis=1)[df_nace['nace_code'].map(len)>0]

Note that in the last line of code we exclude instruments where the NACE division code is missing.

Filter those instruments with high ESG Score

We now enforce a strict lower bound on the ESG Score of instruments for the portfolio to ensure a basic quality by means of ecological and social aspects.


esg_bound = 70

df_esg_constraint = df_esg_nace[(df_esg_nace.esg >= esg_bound)].replace('','nan')

K = len(df_esg_constraint.index)

print('Selected {K} out of {N} instruments by filtering ESG Score >= {esg}\n'.format(K=K,N=N,esg=esg_bound))


Selected 113 out of 350 instruments by filtering ESG Score >= 70

name esg Business Description nace nace_code
FITB.OQ Fifth Third Bancorp 74.45776 Fifth Third Bancorp is a bank holding company.... Other monetary intermediation (NACE) (64.19) 64
HOLX.OQ Hologic Inc 77.681436 Hologic, Inc. is a developer, manufacturer and... Manufacture of irradiation, electromedical and... 26
8630.T Sompo Holdings Inc 77.448841 Sompo Holdings, Inc. is a Japan-based company ... Non-life insurance (NACE) (65.12) 65
BAMa.TO Brookfield Asset Management Inc 77.97256 Brookfield Asset Management Inc. is an alterna... Renting and operating of own or leased real es... 68
BCE.TO BCE Inc 71.008589 BCE Inc. is a Canada-based communications comp... Wired telecommunications activities (NACE) (61... 61
... ... ... ... ... ...
LUMI.TA Bank Leumi Le Israel BM 76.909491 Bank Leumi Le Israel BM (the Bank) is an Israe... Other monetary intermediation (NACE) (64.19) 64
LOW.N Lowe's Companies Inc 85.596615 Lowe's Companies, Inc. (Lowe's) is a home impr... Retail sale of hardware, paints and glass in s... 47
3382.T Seven & i Holdings Co Ltd 86.572966 Seven & i Holdings Co Ltd is a Japan-based com... Retail sale in non-specialised stores with foo... 47
VWS.CO Vestas Wind Systems A/S 77.767678 Vestas Wind Systems A/S is a Denmark-based com... Manufacture of engines and turbines, except ai... 28
JNJ.N Johnson & Johnson 89.605162 Johnson & Johnson is a holding company, which ... Manufacture of pharmaceutical preparations (NA... 21

113 rows × 5 columns

Reading instruments time series price data

For the selcted instruments we get the time series data for one year. We need to remove the NA data in order to prevent problems with calculating the covariance matrix.


print('Loading timeseries data from eikon')



instruments = df_esg_constraint.index

timeseries_data =pandas.DataFrame()

for r in instruments:


        ts1 = ek.get_timeseries(r,'CLOSE',start_date=start,end_date=end,interval='daily')

        ts1.rename(columns = {'CLOSE' : r}, inplace=True)

        timeseries_data =pandas.concat([timeseries_data, ts1], axis=1)




timeseries_data = timeseries_data.dropna()


Loading timeseries data from eikon

2020-01-07 29.85 52.49 4280.0 49.729029 60.48 59.29 54.13 91.58 54.86 102.944218 ... 5090 77.24 2.4 2.92704 42940 2491 119.63 3969 127.92 144.98
2020-01-08 29.92 52.29 4188.0 49.934193 60.64 55.83 54.34 91.74 55.32 104.929418 ... 5090 77.51 2.4 2.934662 42640 2484 121.53 3942 129.08 144.96
2020-01-09 30.25 53.275 4228.0 50.377611 60.29 54.68 54.42 93.04 55.9 107.415388 ... 5035 77.8 2.413 2.92704 43380 2483 122.05 4022 128.32 145.39
2020-01-14 29.74 53.53 4213.0 51.972594 60.89 54.62 54.92 93.12 55.58 105.179803 ... 5145 76.87 2.419 3.003265 43200 2472 120.07 4285 126.52 146.52
2020-01-15 29.03 53.75 4215.0 52.435866 61.11 54.43 55.24 93.18 55.42 104.839994 ... 5030 76.66 2.384 2.980398 43070 2446 119.52 4249 129.48 147.01
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
2020-12-21 27.165 75.23 4038.0 50.609222 54.75 40.67 52.08 89.2 53.52 104.580666 ... 6354 80.57 1.599 2.492557 65250 1816 164.33 3528 270.0 153.02
2020-12-22 26.93 74.67 4034.0 50.738277 54.6 39.27 52.18 90.14 53.76 105.877306 ... 6422 79.33 1.63 2.4392 64400 1827 164.61 3503 280.6 152.72
2020-12-23 27.73 75.06 4068.0 51.433186 54.66 39.87 52.24 90.48 54.32 108.783566 ... 6416 80.02 1.632 2.462067 64480 1841 162.43 3509 278.2 151.94
2020-12-29 27.28 71.74 4186.0 52.495404 54.69 39.41 52.54 92.46 54.64 109.767224 ... 6594 79.24 1.614 2.507802 65860 1900 160.54 3710 291.8 154.14
2020-12-30 27.28 71.75 4173.0 52.336567 54.6 39.34 52.38 92.3 54.14 108.425872 ... 6536 79.82 1.608 2.50018 65830 1904 160.56 3659 287.9 156.05

Minimum Volatility Portfolio (MVP) on the ESG-constrained universe

We compute the classical MVP in the same way as in our previous article -- please consult that tutorial for details. Given the pre-filter on the constituents' ESG Score, the MVP satisfies a lower bound



returns = timeseries_data.pct_change().replace(numpy.inf, numpy.nan).dropna()

covMatrix = returns.cov()


def risk_measure(covMatrix, weights):

    return,, weights))


bounds = K * [(0, 1)]


constraints = {'type': 'eq', 'fun': lambda weights: weights.sum() - 1}


mvp = sco.minimize(lambda x: risk_measure(covMatrix, x),  # function to be minimized

                   K * [1 / K],  # initial guess

                   bounds=bounds,  # box constraints

                   constraints =constraints,  # equality constraints



print('''MVP in a universe with {K} instruments

Number of selected instruments: {n}

Minimum weight: {minw}

Maximum weight: {maxw}

Historical risk measure: {risk}

Historical return p.a.: {r}

ESG score: {esg}'''.format(K=K,




                           risk=risk_measure(covMatrix, mvp['x']),


                 ['x'], df_esg_constraint['esg'])))

MVP in a universe with 113 instruments
Number of selected instruments: 45
Minimum weight: 7.503984557423134e-21
Maximum weight: 0.06185783693486125
Historical risk measure: 0.00015195017397695131
Historical return p.a.: 0.11247632176102468
ESG score: 78.49331458983875

You may use the the code in the previous article to get further insights into the structure of the resulting MVP. We now go further and plot the distribution of the investment on the NACE divisions:

Pie chart of the business sectors distribution of the MVP

Given the weight (portion) of each instrument within the (ESG-constrained) portfolio, the portion of the investment within each business sector is accumulated by the function calcWeightsForPieChart(df). Then, a pie chart illustrates, how the money is allotted to business sectors.


nace_dict ={'01': 'Crop and animal production, hunting and related service activities', '02': 'Forestry and logging', '03': 'Fishing and aquaculture', '05': 'Mining of coal and lignite', '06': 'Extraction of crude petroleum and natural gas', '07': 'Mining of metal ores', '08': 'Other mining and quarrying', '09': 'Mining support service activities', '10': 'Manufacture of food products', '11': 'Manufacture of beverages', '12': 'Manufacture of tobacco products', '13': 'Manufacture of textiles', '14': 'Manufacture of wearing apparel', '15': 'Manufacture of leather and related products', '16': 'Manufacture of wood and of products of wood and cork, except furniture; manufacture of articles of straw and plaiting materials', '17': 'Manufacture of paper and paper products', '18': 'Printing and reproduction of recorded media', '19': 'Manufacture of coke and refined petroleum products', '20': 'Manufacture of chemicals and chemical products', '21': 'Manufacture of basic pharmaceutical products and pharmaceutical preparations', '22': 'Manufacture of rubber and plastic products', '23': 'Manufacture of other non-metallic mineral products', '24': 'Manufacture of basic metals', '25': 'Manufacture of fabricated metal products, except machinery and equipment', '26': 'Manufacture of computer, electronic and optical products', '27': 'Manufacture of electrical equipment', '28': 'Manufacture of machinery and equipment n.e.c.', '29': 'Manufacture of motor vehicles, trailers and semi-trailers', '30': 'Manufacture of other transport equipment', '31': 'Manufacture of furniture', '32': 'Other manufacturing', '33': 'Repair and installation of machinery and equipment', '35': 'Electricity, gas, steam and air conditioning supply', '36': 'Water collection, treatment and supply', '37': 'Sewerage', '38': 'Waste collection, treatment and disposal activities; materials recovery', '39': 'Remediation activities and other waste management services', '41': 'Construction of buildings', '42': 'Civil engineering', '43': 'Specialised construction activities', '45': 'Wholesale and retail trade and repair of motor vehicles and motorcycles', '46': 'Wholesale trade, except of motor vehicles and motorcycles', '47': 'Retail trade, except of motor vehicles and motorcycles', '49': 'Land transport and transport via pipelines', '50': 'Water transport', '51': 'Air transport', '52': 'Warehousing and support activities for transportation', '53': 'Postal and courier activities', '55': 'Accommodation', '56': 'Food and beverage service activities', '58': 'Publishing activities', '59': 'Motion picture, video and television programme production, sound recording and music publishing activities', '60': 'Programming and broadcasting activities', '61': 'Telecommunications', '62': 'Computer programming, consultancy and related activities', '63': 'Information service activities', '64': 'Financial service activities, except insurance and pension funding', '65': 'Insurance, reinsurance and pension funding, except compulsory social security', '66': 'Activities auxiliary to financial services and insurance activities', '68': 'Real estate activities', '69': 'Legal and accounting activities', '70': 'Activities of head offices; management consultancy activities', '71': 'Architectural and engineering activities; technical testing and analysis', '72': 'Scientific research and development ', '73': 'Advertising and market research', '74': 'Other professional, scientific and technical activities', '75': 'Veterinary activities', '77': 'Rental and leasing activities', '78': 'Employment activities', '79': 'Travel agency, tour operator and other reservation service and related activities', '80': 'Security and investigation activities', '81': 'Services to buildings and landscape activities', '82': 'Office administrative, office support and other business support activities', '84': 'Public administration and defence; compulsory social security', '85': 'Education', '86': 'Human health activities', '87': 'Residential care activities', '88': 'Social work activities without accommodation', '90': 'Creative, arts and entertainment activities', '91': 'Libraries, archives, museums and other cultural activities', '92': 'Gambling and betting activities', '93': 'Sports activities and amusement and recreation activities', '94': 'Activities of membership organisations', '95': 'Repair of computers and personal and household goods', '96': 'Other personal service activities', '97': 'Activities of households as employers of domestic personnel', '98': 'Undifferentiated goods- and services-producing activities of private households for own use', '99': 'Activities of extraterritorial organisations and bodies', 'nan': 'Others'}


def calcWeightsForPieChart(df):

    #calculate weights for pie chart

    df_nan = df.replace('','nan')

    sections = {item for item in list(df_nan['nace_code']) if len(item)>0}

    piesizes = {}

    for s in sections:

        piesizes[str(s)] = df_nan[df_nan['nace_code']==s]['weight'].values.sum()


    return piesizes


def plotPieChartWithNACELabels(nace_dict,piesizes):

    ## piechart for nace codes

    labels = []

    sizes = []

    for x, y in piesizes.items():

        labels.append('{d} ({c})'.format(d=nace_dict[x],c=str(x)))


    plt.figure(figsize=(10, 10), dpi=80)

    plt.pie(sizes, labels=labels)



df_esg_constraint['weight'] = list(mvp['x'])

piesizes = calcWeightsForPieChart(df_esg_constraint)

plotPieChartWithNACELabels(nace_dict, piesizes)

The minimum volatility portfolio is nicely diversified and covers a large number of business sectors. Note that using e.g. address or currency data from Refinitiv, you can also plot the distribution over countries or currencies.

Balancing volatility with historical return and ESG-score

In addition to a small risk, previously estimated by a small historical volatility, one may aim for a high return and high ESG ratings in addition to the lower bound of 70 that was imposed on the optimization universe. The following code minimizes historical volatility and in addition, minimizes - 0.0002 * historical return and - 0.00002* ESG-rating of a portfolio. There is thus a trade-of between a higher return or higher ESG Score with low volatility. The factors 0.0002 and 0.00002 mirror the importance of each of the criteria. With a higher factor, e.g. with the return term, the portfolio is selected focusing more on the return, and less on the the other measures.


esg_ret_mvp = sco.minimize(lambda x: risk_measure(covMatrix, x) - 0.0002*, returns.sum()) - 0.00002*, df_esg_constraint['esg']),  # function to be minimized

                   K * [1 / K],  # initial guess

                   bounds=bounds,  # box constraints

                   constraints =constraints,  # equality constraints



print('''Solution to balanced volatility, return, ESG problem in a universe with {K} instruments

Number of selected instruments: {n}

Minimum weight: {minw}

Maximum weight: {maxw}

Historical risk measure: {risk}

Historical return p.a.: {r}

ESG score: {esg}'''.format(K=K,




                           risk=risk_measure(covMatrix, esg_ret_mvp['x']),


                 ['x'], df_esg_constraint['esg'])))

Solution to balanced volatility, return, ESG problem in a universe with 113 instruments
Number of selected instruments: 28
Minimum weight: 3.3825682612790066e-20
Maximum weight: 0.10165960999890983
Historical risk measure: 0.00018671090425783444
Historical return p.a.: 0.17080291589692786
ESG score: 85.61815983112916

We gain some insight into the business sector diversification from the according pie chart: Notice, that we now have a trade-of between volatility and returns as well as ESG-scores. Since the latter terms do not diversify, but rather select high weights for companies with high return and/or ESG-score, the current portfolio is less distributed over the business sectors than the MVP from before.


df_esg_constraint['weight'] = list(esg_ret_mvp['x'])


piesizes = calcWeightsForPieChart(df_esg_constraint)

plotPieChartWithNACELabels(nace_dict, piesizes)

An approach to impact modeling

The idea behind impact investment is to invest specifically in companies that have a positive impact on social or environmental goals. Depending on the location or on the sector of economic activities, a high ESG Score can have a different impact -- for instance, a company that cares for clean drinking water has more impact in countries where clean water is actually an issue. Or using only renewable energies has much more impact in energy intense business sectors. We leave an evaluation of this kind of impact to experts in the field, and show here, how impact expertise can be included into our portfolio selection model.

Organization of impact information

We assume that wise people provide a score between 0 and 10 for each of the sectors covered by our universe. The score 0 means that an ESG-Rating exceeding the lower bound of 70 has no impact in this sector, whereas the score 10 implies a high impact of extra ESG points. To keep our example simple and illustrative, we just select one sector (construction of buildings, no. 41) which has the impact 10, and assume that all others have the impact 0 (for whatever reason). This impact information is assumed to be given by experts in a dictionary nace_dict with 2 digit NACE codes as keys and the respective impact score as values. You may use the commented line with random numbers for a more realistic setting of nace_dict. The impact scores are included into the instruments dataframe in the last line of the following code block.


nace_impact_dict ={key: 0 for key in nace_dict.keys()}


#nace_impact_dict ={key: random.randint(0, 10) for key in nace_dict.keys()}

df_esg_constraint['nace_impact'] = df_esg_constraint['nace_code'].apply(lambda x: nace_impact_dict[x])

Computation of the impact-ESG score of a portfolio

The impact-ESG score of a company now combines the ESG-Score with the sector impact score as follows:


This formula is applied to each company in a dataframe of all companies in the optimization universe in the following line,

            impact_esg_score = lambda df: df['nace_impact']*(df['esg']-esg_bound)

and we compute some impact-ESG-scores of weighted portfolios in a scalar product as follows,


mvp_impact_esg =['x'], impact_esg_score(df_esg_constraint))

mvp_ret_impact_esg =['x'], impact_esg_score(df_esg_constraint))

print('ESG of MVP: {mvpesg}\nImpact-ESG of MVP: {mvpiesg}\n'.format(['x'], df_esg_constraint['esg']), mvpiesg=mvp_impact_esg))

print('ESG of balanced: {balesg}\nImpact-ESG of balanced: {baliesg}'.format(['x'], df_esg_constraint['esg']), baliesg=mvp_ret_impact_esg))

ESG of MVP: 78.49331458983875
Impact-ESG of MVP: 2.87839973147472

ESG of balanced: 85.61815983112916
Impact-ESG of balanced: 3.0997556428568394

Remember that the ESG-aware portfolio has a higher ESG-score than the basic MVP. But nevertheless, the impact-ESG score (which, in our example, sais that ESG-ratings higher than 70 make sense only for companies constructing buildings) is even decreased compared to the MVP.

Impact-ESG-aware portfolio selection

In order to build an impact-ESG-aware portfolio, we exchange the simple ESG-term with the impact-ESG term in the minimization problem.


impact_esg_mvp = sco.minimize(lambda x: risk_measure(covMatrix, x) - 0.0002*, returns.sum()) - 0.000004*, impact_esg_score(df_esg_constraint)),  # function to be minimized

                   K * [1 / K],  # initial guess

                   bounds=bounds,  # boundary conditions

                   constraints =constraints,  # equality constraints




print('''Solution to weighted impact ESG problem in a universe with {K} instruments

Number of selected instruments: {n}

Minimum weight: {minw}

Maximum weight: {maxw}

Historical risk measure: {risk}

Historical return p.a.: {r}

ESG score: {esg}'''.format(K=K,




                           risk=risk_measure(covMatrix, impact_esg_mvp['x']),


                 ['x'], df_esg_constraint['esg'])))

Solution to weighted impact ESG problem in a universe with 113 instruments
Number of selected instruments: 14
Minimum weight: 1.9572764759356897e-21
Maximum weight: 0.433838334307341
Historical risk measure: 0.00020403642361264564
Historical return p.a.: 0.10575171137227271
ESG score: 80.22443849877946

We compare the ESG- and impact-ESG-ratings of the three portfolios:


mvp_impact_esg =['x'], impact_esg_score(df_esg_constraint))

mvp_ret_impact_esg =['x'], impact_esg_score(df_esg_constraint))


print('ESG of MVP: {esg}\nImpact-ESG of MVP: {iesg}\n'.format(['x'], df_esg_constraint['esg']), 



print('ESG of return and ESG balanced: {esg}\nImpact-ESG of return and ESG balanced: {iesg}\n'.format(['x'], df_esg_constraint['esg']),


print('ESG of return and impact-ESG balanced: {esg}\nImpact-ESG of return and impact-ESG balanced: {iesg}\n'.format(['x'], df_esg_constraint['esg']),['x'], impact_esg_score(df_esg_constraint))))

ESG of MVP: 78.49331458983875
Impact-ESG of MVP: 2.87839973147472

ESG of return and ESG balanced: 85.61815983112916
Impact-ESG of return and ESG balanced: 3.0997556428568394

ESG of return and impact-ESG balanced: 80.22443849877946
Impact-ESG of return and impact-ESG balanced: 56.944799960584405

In fact, the impact-ESG optimization was successful: The respective portfolio achieves a much higher impact-ESG-score than the competing portfolios.

We also plot the business sectors pie chart and of course, the impact-ESG-aware portfolio has a special focus on the construction of buildings:


df_esg_constraint['weight'] = impact_esg_mvp['x']


piesizes = calcWeightsForPieChart(df_esg_constraint)

plotPieChartWithNACELabels(nace_dict, piesizes)

Summary and outlook

We hope that we could show, how different aspects and criteria can be considered in a portfolio selection strategy. We have constructed 3 portfolios: All of them satisfy a lower bound on the ESG-score in every constituent. By using specific objective functions in the minimization problem, we could steer the distribution of the investment to have low volatility in the first one, and a trade-of between low volatility but also high returns and high ESG-scores in the second one. For the third portfolio, we showed a model for impact-ESG scores and included it into the optimization goal.

This tutorial aims to show some key techniques and thus scratches only the surface of ESG- and risk-aware portfolio selection. One can refine the models in many aspects, and one could go for more details in the analysis of the results. We refer to the literature and to the large pool of Refinitiv tutorials in the developers' community.

For more information about the authors, check out!

Further Resources for Eikon Data API

For Content Navigation in Eikon - please use the Data Item Browser Application: Type 'DIB' into Eikon Search Bar.