En este notebook analizaremos si las empresas sujetas a un mayor escrutinio público tienden a mejorar sus estándares ambientales y sociales. El escrutinio público es medido mediante el número de búsquedas web que recibe una empresa de acuerdo con Google Trends y el nivel de desempeño ambiental y social de la compañía es medido con los ratings ESG que proporciona Refinitiv. La muestra que usaremos se compone de las compañías que formaron parte del índice bursátil S&P 500 en el periodo 2012-2020.
💡 Nota
Los ratings ESG miden el desempeño ambiental, social y de gobierno corporativo de una empresa.
⚠️ Importante
La teoría de la legitimidad defiende que las empresas para su supervivencia necesitan una percepción generalizada de que sus acciones son deseables y apropiades dentro de un sistema de normas y valores. Bajo este marco teórico, si las empresas sufren presiones de legitimidad o están sujetas a un gran escrutinio público tratarán de mejorar sus estándares ambientales y sociales para así alinear sus acciones con las normas y valores de la sociedad. Sin embargo, no es sencillo demostrar esta hipótesis debido a que no es fácil medir el escrutinio público al que está sujeto una compañía. Para contrastar esta hipótesis, utilizaremos el número de búsquedas web que recibe una compañía para medir su escrutinio público.
El objetivo de este notebook es analizar la existencia de una relación causal entre aumentos del escrutinio público y posteriores variaciones en el desempeño medioambiental de la compañía. Para lograrlo, abordaremos de forma práctica los siguientes aspectos metodológicos:
En esta sección veremos cómo construir un indicador de Escrutinio Público mediante el uso de Google Trends.
Google Trends es una herramienta que mide la popularidad de un tema de búsqueda. A partir del número de búsquedas, Google Trends genera una serie temporal que se encuentra en una escala relativa de 0 a 100. En esta escala, 100 representa el punto más alto de búsquedas, mientras que 50 indica la mitad de las búsquedas en relación con ese máximo. En el ámbito académico esta serie temporal se denomina search volumen index (SVI) y su uso se popularizo tras el trabajo seminal de Da et al. (2010).
A continuación, veremos como unir la información que proporciona Refinitiv con la información que proporciona Google Trends.
REFINITIV:
En la siguiente pieza de código creamos la conexión a la base de datos y accedemos a una submuestra de las empresas que forman o formaron parte del índice bursátil S&P 500 en el periodo 2012-2020. Los campos Company Legal Name y Name in Refintiv nos permitirán cruzar la información de Refinitiv (datos financieros y de ESG) con la información de Google Trends.
import sqlite3
import pandas as pd
connection = sqlite3.connect('2022AttentionESGRatings.db')
df_sample = pd.read_sql('select * from sp500_subsample', connection)
df_sample.head()
GOOGLE TRENDS:
Dado un string Goolge Trends sugiere diferentes temas de búsqueda que tiene categorizados. A continuación, vemos los temas de búsqueda que nos sugiere para la cadena de texto “Apple” (nombre de la compañía en Refinitiv). En nuestro estudio, nos interesa “Apple” la “Technology company” con id /m/0k8z. Se puede observar, que con este proceso podemos unir el id de Apple en Refinitiv (992816) con el id de Apple en Goolge Trends (/m/0k8z).
from pytrends.request import TrendReq
pytrend = TrendReq()
df_suggestions = pd.DataFrame.from_dict(pytrend.suggestions("Apple"))
df_suggestions
PROCESO DE MACHEO:
Unimos los IDs de Refinitiv y Google Trends de forma automática mediante dos criterios:
En el siguiente fragmento de código, construimos una función que devuelve la primera sugerencia indicada por Google Trends(df_suggestions) que contenga una de las palabras de contenido empresarial que hemos definido.
def has_word(name):
for word in ['company', 'conglomerate', 'corporation', 'enterprise']:
if word.lower() in name.lower():
return True
return False
df_suggestions[df_suggestions['type'].apply(lambda name: has_word(name))].iloc[0]
A continuación, creamos dos funciones para calcular la distancia normalizada de levenshtein y la similitud de coseno. En ambas funciones eliminamos los signos de puntación y, en el caso de la similitud del coseno, vectorizamos las cadenas de texto.
import string
import textdistance
from sklearn.metrics.pairwise import cosine_similarity
from sklearn.feature_extraction.text import CountVectorizer
def normalized_leven(word_1, word_2):
word_1=''.join([word for word in word_1 if word not in string.punctuation])
word_2=''.join([word for word in word_2 if word not in string.punctuation])
similarity= textdistance.levenshtein.normalized_similarity(word_1.lower(), word_2.lower())
return similarity
def word_cosine_similarity(word_1, word_2):
word_1=''.join([word for word in word_1 if word not in string.punctuation]) #eliminate punctuation
word_2=''.join([word for word in word_2 if word not in string.punctuation])
vec=CountVectorizer().fit_transform([word_1.lower(),word_2.lower()]).toarray() #vectorize
similarity=cosine_similarity(vec) #cosine similarity between words
return similarity[1,0]
leven = normalized_leven('Apple INC', 'Apple')
cosine = word_cosine_similarity('Apple INC', 'Apple')
print('Normalized levenshtein distance: %s' % np.round(leven,2))
print('Cosine Similarity: %s' % np.round(cosine,2))
📝 Consejo
Con el uso de un bucle for, y gracias a las funciones que hemos creado, seria sencillo escalar este proceso para toda la muestra.
Es necesario determinar que empresas fueron las más buscadas cada año para construir nuestro indicador de escrutinio público. En el siguiente fragmento de código, realizamos tres solicitudes a Google Trends para determinar la popularidad de Netflix y Apple en 2020. En las primeras dos solicitudes, obtenemos la popularidad de Apple y Netflix de forma individual, mientras que en la tercera solicitud consultamos su popularidad de forma conjunta.
df_svi = pd.DataFrame()
start_date = '2020-01-01'
end_date = '2020-12-31'
# SVI Apple
pytrend.build_payload(['/m/0k8z',], timeframe= start_date+" "+end_date )
df = pytrend.interest_over_time()[['/m/0k8z']].rename(columns={'/m/0k8z':'Apple Alone'})
df_svi = pd.concat([df_svi, df], axis=1)
# SVI Netflix
pytrend.build_payload(['/m/017rf_',], timeframe= start_date+" "+end_date )
df = pytrend.interest_over_time()[['/m/017rf_']].rename(columns={'/m/017rf_':'Netflix Alone'})
df_svi = pd.concat([df_svi, df ], axis=1)
# SVI Apple & Netflix
pytrend.build_payload(['/m/0k8z','/m/017rf_'], timeframe= start_date+" "+end_date )
df = pytrend.interest_over_time()[['/m/0k8z', '/m/017rf_']].rename(columns={'/m/0k8z' : 'Apple Together',
'/m/017rf_':'Netflix Together', })
df_svi = pd.concat([df_svi, df ], axis=1)
df_svi.head()
⚠️ Advertencia
En la siguiente pieza de código, realizamos la visualización de la información obtenida. Debido a la relativización ya mencionada, la serie temporal de Apple del primer gráfico difiere con la del tercer gráfico. En el tercer gráfico, la serie temporal de la empresa más buscada (Netflix) relativiza la serie temporal de la empresa menos buscada (Apple). Al obtener las series temporales en una misma solicitud, podemos determinar qué empresa ha sido más buscada. Repitiendo este proceso para todas las compañías y años, podemos ordenar a las empresas de acuerdo con su número de búsquedas.
from plotly.subplots import make_subplots
import plotly.graph_objects as go
fig = make_subplots(rows=1, cols=3, subplot_titles=('(1) SVI Apple', '(2) SVI Netflix', '(3) SVI Together'),
shared_xaxes=True, shared_yaxes=True, horizontal_spacing=0.02)
# Plot (1)
fig.add_trace(go.Scatter(y=df_svi['Apple Alone'], x=df_svi.index, name='Apple Alone', ), row=1, col=1)
# Plot (2)
fig.add_trace(go.Scatter(y=df_svi['Netflix Alone'], x=df_svi.index, name='Netflix Alone'), row=1, col=2)
# Plot (3)
fig.add_trace( go.Scatter(y=df_svi['Apple Together'], x=df_svi.index, name='Apple Together' ), row=1, col=3)
fig.add_trace( go.Scatter(y=df_svi['Netflix Together'], x=df_svi.index, name='Netflix Together'), row=1, col=3 )
fig.update_layout(height=450, width=930,template='plotly_white', font={'size':11}, title_font={'size':8},
legend=dict(orientation="h", xanchor="center", x=0.5, y=-0.12))
fig.update_traces(line=dict(width=3))
fig.show()
📝 Consejo
Para ordenar a las empresas de acuerdo con su número de búsquedas, es crucial utilizar un algoritmo de ordenación, de esta manera se minimiza la cantidad de comparaciones (consultas a Google Trends) y evitamos realizar N×N comparaciones. Este fichero contiene las comparaciones realizadas para crear el ranking de 2012.
⚠️ Advertencia
Google Trends tiene un límite de consultas, por lo que realizar un alto número de consultas puede llevar a bloqueos. Una opción para superar parcialmente esta limitación es utilizar una VPN o cambiar los encabezados de nuestras peticiones con, por ejemplo, fake-useragent.
A continuación, se describen brevemente las variables que utilizaremos para contrastar la hipótesis de estudio:
En el siguiente fragmento de código, accedemos a estas variables mediante una consulta SQL que combina la información de la tabla refinitiv_data (que contiene variables financieras y no financieras extraídas de Refinitiv) con la información de la tabla google_ranking (que almacena la posición de cada empresa según su número de búsquedas).
connection = sqlite3.connect('2022AttentionESGRatings.db')
query = """
SELECT r.AliasRefinitivId, r.AliasGoogleId, r.Year, g.PublicAttention, r.EnvScore,
r.LogTotalAssets, r.Leverage, r.ROA, r.RDIntensity, r.ICBIndustry
FROM refinitiv_data as r
INNER JOIN google_ranking as g
ON r.AliasGoogleId = g.AliasGoogleId
AND r.Year = g.Year
"""
df_data = pd.read_sql(sql=query, con=connection)
df_data.info()
⚠️ Advertencia
Para garantizar la privacidad de la información, he anonimizado los identificadores de cada empresa. Además, las variables EnvScore, LogTotalAssets, Leverage, ROA, RDIntensity han sido reescaladas en un rango de 0 a 100.
A continuación, vamos a calcular algunas estadísticas descriptivas para nuestros datos:
Número de empresas analizadas cada año:
df_data.pivot_table(columns='Year', values='AliasRefinitivId', aggfunc='count')
Matriz de correlación en mapa de calor:
import plotly.express as px
colors = ['#d3100f', '#f13937', '#f67b7a', '#fabdbc', '#ffffff', '#ddf1cb', '#bbe398', '#99d564', '#78c335']
df = df_data.drop(columns=['Year', 'AliasGoogleId', 'AliasRefinitivId', 'ICBIndustry']).corr().round(2)
fig = px.imshow(df, text_auto=True, zmin=-1, color_continuous_scale=colors)
fig.show()
Para analizar si las empresas mejoran sus estándares ambientales ante un aumento del escrutinio público realizaremos un cuasi-experimento para posteriormente aplicar un análisis de diferencias en diferencias.
A continuación, en relación con el período t, calculamos el incremento del indicador ambiental en comparación con el año anterior (ΔEnvScore), y el incremento para el próximo año (NextΔEnvScore).
df_data = df_data.set_index(['AliasRefinitivId', 'Year']).sort_index().reset_index()
df_data['ΔEnvScore'] = df_data.groupby('AliasRefinitivId')['EnvScore'].diff()
df_data['NextΔEnvScore'] = df_data.groupby('AliasRefinitivId')['ΔEnvScore'].shift(-1)
A continuación, vamos a determinar el grupo de tratamiento y el grupo de control.
En un enfoque de causi-experimento la asignación a un grupo de control o de tratamiento no es aleatoria, sino que se basa en alguna marca o corte. En este caso, se propone identificar el 10% de los mayores aumentos de la variable PublicAttention para establecer el grupo de tratamiento. De este modo, determinamos las empresas que sufrieron un shock en sus niveles de escrutinio público.
En el siguiente fragmento de código, obtenemos el incremento en la variable PublicAttention para cada compañía en el periodo t. Luego, dividimos dicha variable en 10 grupos, donde el grupo 9 contiene las empresas que sufrieron un mayor incremento en su escrutinio público. Por lo tanto, el grupo 9, es nuestro grupo de tratamiento.
df_data['ΔPublicAttention'] = df_data.groupby('AliasRefinitivId')['PublicAttention'].diff()
df_data['Group'] = df_data.dropna(subset=['ΔPublicAttention']
)[['ΔPublicAttention']].apply(lambda x: pd.qcut(x, 10, labels=False))
A continuación, visualizamos para cada grupo el valor promedio y la mediana de la variable NextΔEnvScore (ΔEnvScoreit+1). Como se puede observar, el grupo de tratamiento (Grupo 9) es el que experimenta el mayor crecimiento en su desempeño ambiental en el período t+1. No obstante, es importante destacar que no podemos asegurar el efecto causal de nuestro "tratamiento" hasta que no hayamos estimado la regresión mencionada anteriormente.
df = df_data.pivot_table(index='Group', values=['NextΔEnvScore',], aggfunc=['mean','median']
).round(2).droplevel(level=1, axis=1).reset_index()
df['Treatment'] = df['Group'].apply(lambda x: 'Treatment' if x==9 else 'Other Groups')
px.bar(df.melt(id_vars=['Group', 'Treatment'], value_name='NextΔEnvScore'),
x='Group', y='NextΔEnvScore',
template='plotly_white',
facet_col='variable', color='Treatment')
Determinamos el grupo de control utilizando el algoritmo del vecino más cercano: cada empresa tratada se empareja con una empresa de control los más parecida posible. Las candidatas a ser una empresa de control son todas aquellas observaciones que no forman parte del grupo de tratamiento. Además, una empresa tratada en el periodo t, solo será emparejada con empresas de control en t. Las variables utilizadas para realizar el emparejamiento son: EnvScoret, ΔEnvScoret, PublicAttentiont, LogAssetst, Leveraget, ROAt, RD_Intensityt.
💡 Consejo
Para evitar que aquellas variables en escalas más grandes dominen el proceso de emparejamiento es importante estandarizar las variables.
A continuación, definimos la función encargada de realizar el emparejamiento (get_matching_pairs) y ejecutamos esta función para cada uno de los años analizados. El resultado es un dataframe llamado df_regression que contiene los datos necesarios para llevar a cabo el análisis de diferencias en diferencias.
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import NearestNeighbors
def get_matching_pairs(treated, non_treated, scaler=True):
if scaler:
scaler = StandardScaler()
scaler.fit(pd.concat([treated, non_treated]))
treated_x = scaler.transform(treated)
non_treated_x = scaler.transform(non_treated)
nbrs = NearestNeighbors(n_neighbors=1, algorithm='ball_tree').fit(non_treated_x)
distances, indices = nbrs.kneighbors(treated_x)
indices = indices.reshape(indices.shape[0])
control_pairs = non_treated.iloc[indices]
return control_pairs
matching_variables = ['EnvScore', 'ΔEnvScore', 'PublicAttention', 'LogTotalAssets', 'Leverage', 'ROA', 'RDIntensity']
other_vars =['NextΔEnvScore', 'AliasRefinitivId', 'ICBIndustry','Year']
df_regression = pd.DataFrame()
for year in df_data.dropna(subset=['ΔEnvScore','NextΔEnvScore'])['Year'].unique():
df_shock = df_data[(df_data['Group']==9) & (df_data['Year']==year)
][matching_variables+other_vars].dropna()
df_control = df_data[(df_data['Group'].isin(range(0,9)))& (df_data['Year']==year)
][matching_variables+other_vars].dropna()
# matching using nearest algorithm
pairs = get_matching_pairs(df_shock[matching_variables], df_control[matching_variables], scaler=True)
df_control = df_control.loc[pairs.index,]
df_control['Shock'] =0
df_shock['Shock']= 1
df_regression = pd.concat([df_regression,df_control,df_shock])
💡 Nota
La función get_matching_pairs toma dos dataframes como entrada: uno para el grupo tratado y otro para el grupo de control. A continuación, se normalizan los dataframes utilizando un StandardScaler y luego se aplica el algoritmo del vecino más cercano para obtener la pareja de control para cada empresa tratada.
En la siguiente pieza de código, para cada una de las variables utilizadas en el proceso de matching, realizamos un test de diferencia de medias entre grupo de control y el grupo de tratamiento. Este análisis muestra que el grupo de tratamiento y el de control son muy similares en el momento t de recibir el tratamiento (aumento del escrutinio público).
from scipy import stats
df_ttest = pd.DataFrame(columns=['Shock', 'Control', 'p-val'],index=matching_variables )
for var in matching_variables: # +['NextΔEnvScore']
shock = df_regression[df_regression['Shock']==1][var]
control = df_regression[df_regression['Shock']==0][var]
test = stats.ttest_ind(shock, control)
test = '{:.3f}'.format(test[1].round(3))
df_ttest.loc[var] = ['{:.3f}'.format(shock.mean().round(3)),
'{:.3f}'.format(control.mean().round(3)),test]
df_ttest.loc['#'] = ['{:,}'.format(shock.count()), '{:,}'.format(control.count()),np.nan]
df_ttest
Para contrastar nuestra hipótesis vamos a estimar la siguiente regresión, la cual se corresponde con una especificación de diferencias en diferencias:
$$ {ΔEnvScore}_{it+1} = {\alpha}+{Shock}_{it}+{\beta} X_{it}+{\epsilon}_{it} $$
Donde:
💡 Nota
El análisis de diferencias en diferencias es una metodología ampliamente utilizada en ciencias sociales que permite comparar los cambios en un grupo de tratamiento (empresas sujetas a un aumento de escrutinio público) con los cambios en un grupo de control (empresas no sujetas a dicho escrutinio) antes y después de una intervención o evento.
Antes de usar el paquete statsmodels para estimar la mencionada ecuación, vamos a definir la siguientes dos funciones:
get_significance: Dado un p-valor, esta función nos devuelve la significancia estadística en formato de investigación, donde *** indica una significancia al 1%, ** indica al 5% y * indica al 10%.results_research_format: Esta función toma una regresión estimada con el paquete statsmodels y devuelve los resultados en el formato comúnmente utilizado en publicaciones de ciencias sociales.def get_significance(p_val):
if p_val <=0.05 and p_val > 0.01 :
significance = '**'
elif p_val <=0.01 and p_val > 0.001:
significance = '***'
elif p_val <=0.001 :
significance = '***'
else:
significance= ''
significance = significance + "\n"
return significance
def results_research_format (res, effects, std_error=True):
# coeficients results std_errors res.tstats
df_results = pd.concat([res.params,res.bse, res.pvalues ],
keys = ["Parameter", "Std.Err.", 'P-value'], axis= 1)
for effect in effects:
effect = effect.replace('C(','').replace(')','')
df_results = df_results.filter(axis=0, regex ='^((?!'+ effect + ').)*$')
# Results research format
df_results = ( df_results["Parameter"].round(2).map('{:.2f}'.format).astype(str) +
df_results['P-value'].apply(lambda x: get_significance(x)) +
df_results["Std.Err."].round(3).map('{:.3f}'.format).astype(str).apply(lambda x: "("+ x + ")" )
)
if std_error == False:
df_results=df_results.apply(lambda x: re.sub(r'\(.*$', '', x)[:-1])
df_results['#'] = '{:,}'.format(int(res.nobs))
df_results['R2'] ='{:.2%}'.format(np.round(res.rsquared,4))
for effect in effects:
df_results[effect] = 'Yes'
return df_results
A continuación, vamos a estimar la especificación diff en diff de tres formas diferentes:
import statsmodels.formula.api as smf
equations = {
'(1)': 'NextΔEnvScore ~ Shock',
'(2)': 'NextΔEnvScore ~ Shock + C(Year) +C(ICBIndustry)',
'(3)': 'NextΔEnvScore ~ Shock + C(Year) +C(ICBIndustry) + '+' + '.join(matching_variables)
}
fixed_effects = [[],['Year','Industry'],['Year','Industry']]
df_results = pd.DataFrame(columns=pd.MultiIndex.from_product([['EnvScore'], ['(1)', '(2)', '(3)']]),
index= ['Intercept', 'Shock']+ matching_variables + ['Year','Industry']+['#','R2'])
for equation, effect in zip(equations, fixed_effects):
res =smf.ols(equations[equation], df_regression).fit()
df_results.loc[:,('EnvScore',equation)] = results_research_format(res, effect)
df_results
La significatividad estadística de la variable Shock nos indica que las compañías mejoran sus estándares ambientales y sociales tras un shock de escrutinio público.
En este notebook, hemos unido la información de Refenitiv con los datos de Google Trends para crear un indicador de escrutinio público basado en el número de búsquedas web. Además, hemos definido un cuasi experimento y aplicado un análisis de diferencias en diferencias. Los resultados de este análisis demuestran que las empresas mejoran sus estándares ambientales para hacer frente a presiones de legitimidad.
Estos hallazgos tienen implicaciones tanto para los profesionales como para los reguladores: el hecho de estar bajo el foco de la opinión pública tiene un impacto positivo en el comportamiento corporativo de las empresas. Por lo tanto, la señalización se revela como una estrategia eficaz para presionar a las empresas a mejorar su desempeño medioambiental.
Si te ha gustado este contenido, puedes acompañarme en LinkedIn y GitHub
o explorar más recursos en mi página web.