Escrutinio público y ratings ESG: Un enfoque de diferencias en diferencias

Pablo Vilas Naval
Junio 2023

Introducción

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

  • Esta es la base de datos que utilizaremos para efectuar los análisis.
  • Estas son las versiones de los paquetes que utilizaremos:

Antecedentes

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.

Objetivo

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:

  1. Escrutinio Público: Construir y desarrollar una metodología para crear un indicador de escrutinio público basado en Google Trends.

  2. Diff-in-diff: Construir un cuasi experimento para posteriormente aplicar un análisis de diferencias en diferencias que nos permita contrastar la existencia de una relación causal.

Indicador de Escrutinio Público

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.

In [1]:
import sqlite3
import pandas as pd

connection = sqlite3.connect('2022AttentionESGRatings.db')
df_sample = pd.read_sql('select * from sp500_subsample', connection)
df_sample.head()
Out[1]:
Year Refinitiv Id Company Legal Name Name in Refinitiv
0 2012 933185 AFLAC INCORPORATED AFLAC
1 2012 873886 ALTABA INC ALTABA
2 2012 992816 APPLE INC. APPLE
3 2012 929302 BEST BUY CO., INC. BEST BUY
4 2012 904818 BOEING CO BOEING

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).

In [2]:
from pytrends.request import TrendReq

pytrend = TrendReq()
df_suggestions = pd.DataFrame.from_dict(pytrend.suggestions("Apple"))
df_suggestions
Out[2]:
mid title type
0 /m/04st9hr Apple Topic
1 /m/014j1m Apple Fruit
2 /m/0k8z Apple Technology company
3 /g/11bc6hq8w2 Apple Topic
4 /m/0ckq2 Apple sauce Purée

PROCESO DE MACHEO:
Unimos los IDs de Refinitiv y Google Trends de forma automática mediante dos criterios:

  1. Descripción de la compañía: Únicamente consideramos aquellos temas sugeridos por Google Trends cuya descripción contenga palabras con un claro contenido empresarial (ej. company, conglomerate, corporation, enterprise, holding, manufacturer, organization etc.)
  2. Similitud entre nombres: Utilizamos la distancia de Levenshtein normalizada y la similitud del coseno para evaluar la similitud entre el nombre de la compañía en Refinitiv y en Google Trends. Si la similitud entre ambas cadenas de texto es superior a 0.5 en ambas medidas, asumimos que se trata de la misma empresa en Refinitiv y Google Trends.

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.

In [3]:
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]
Out[3]:
mid                 /m/0k8z
title                 Apple
type     Technology company
Name: 2, dtype: object

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.

In [4]:
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))
Normalized levenshtein distance: 0.56
Cosine Similarity: 0.71

📝 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.

Ranking de escrutinio público

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.

In [5]:
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()
Out[5]:
Apple Alone Netflix Alone Apple Together Netflix Together
date
2020-01-05 56 57 27 57
2020-01-12 54 52 26 52
2020-01-19 54 52 26 52
2020-01-26 53 51 26 51
2020-02-02 52 48 25 48

⚠️ Advertencia

  • La popularidad de diferentes empresas obtenidas en una consulta individual no son comparables.
  • En una solicitud conjunta, la serie temporal del término más buscado relativiza las series temporales del resto de términos.
  • Google Trends solo deja consultar un máximo de cinco temas en una misma solicitud.
  • Puede haber ligeras variaciones entre consultas ya que Google Trends calcula las series temporales en base a una muestra.

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.

In [6]:
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.

Descripción de los datos

A continuación, se describen brevemente las variables que utilizaremos para contrastar la hipótesis de estudio:

  • Variable de estudio o de tratamiento:
    • PublicAttention: Indica en rango percentil la posición que ocupa cada año la empresa de acuerdo con el número de búsquedas en Google Trends. Un valor de 1 indica que la empresa fue la más buscada ese año.
  • Variable respuesta o dependiente:
    • EnvScore: Mide el desempeño medioambiental de la compañía.
  • Variables de control:
    • LogAssets: Indica el tamaño de la compañía.
    • Leverage: Mide el apalancamiento de la compañía (relación entre recursos propios y ajenos).
    • ROA: Indica la rentabilidad generada por los activos de la compañía.
    • RD_Intensity: Mide la intensidad con la que la compañía invierte en investigación y desarrollo.
  • Variables identificativas:
    • AliasGoogleID: Alias del ID de las empresas en Google Trends.
    • AliasRefinitivId: Alias del ID de las empresas en Refinitiv.
    • Year: Indica el año.
    • ICBIndustry: Indica la industria a la que pertenece la compañía.

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).

In [7]:
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()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 3480 entries, 0 to 3479
Data columns (total 10 columns):
 #   Column            Non-Null Count  Dtype  
---  ------            --------------  -----  
 0   AliasRefinitivId  3480 non-null   object 
 1   AliasGoogleId     3480 non-null   object 
 2   Year              3480 non-null   int64  
 3   PublicAttention   3480 non-null   float64
 4   EnvScore          3333 non-null   float64
 5   LogTotalAssets    3472 non-null   float64
 6   Leverage          3472 non-null   float64
 7   ROA               3461 non-null   float64
 8   RDIntensity       3472 non-null   float64
 9   ICBIndustry       3480 non-null   object 
dtypes: float64(6), int64(1), object(3)
memory usage: 272.0+ KB

⚠️ 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.

Estadísticas descriptivas

A continuación, vamos a calcular algunas estadísticas descriptivas para nuestros datos:

Número de empresas analizadas cada año:

In [8]:
df_data.pivot_table(columns='Year', values='AliasRefinitivId', aggfunc='count')
Out[8]:
Year 2012 2013 2014 2015 2016 2017 2018 2019 2020
AliasRefinitivId 386 387 389 381 382 385 388 390 392

Matriz de correlación en mapa de calor:

In [9]:
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()

Enfoque cuasi-experimental: dif in dif

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).

In [10]:
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) 

Cuasi-experimento

A continuación, vamos a determinar el grupo de tratamiento y el grupo de control.

Grupo de tratamiento

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.

In [11]:
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.

In [12]:
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')

Grupo de control

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.

In [13]:
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.

Similitud entre grupos

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).

In [14]:
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    
Out[14]:
Shock Control p-val
EnvScore 50.034 52.373 0.400
ΔEnvScore 2.536 2.182 0.662
PublicAttention 0.470 0.492 0.315
LogTotalAssets 38.809 37.901 0.470
Leverage 36.596 36.213 0.802
ROA 47.222 46.535 0.745
RDIntensity 11.225 10.301 0.676
# 189 189 NaN

Estimación de la especificación diff in diff

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:

  • ΔEnvScore: es la variable dependiente que indica el aumento del rating medioambiental de la compañía i en el período t+1 en comparación con el período t.
  • Shock: es una variable dicotómica que toma el valor 1 cuando la empresa pertenece al grupo de tratamiento (aumento elevado de Public Attention respecto al año previo) y 0 cuando la empresa pertenece al grupo de control.
  • X: es una matriz que contiene diferentes variables de control en el período t (tanto variables cuantitativas como efectos fijos introducidos como dummies).

💡 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.
In [15]:
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:

    (1): Sin utilizar variables de control ni efectos fijos (matriz X de la regresión vacía).

    (2): Utilizando efectos fijos de industria y año.

    (3): Además de introducir los efectos fijos de industria y año, también utilizaremos las variables de control empleadas en el proceso de emparejamiento de la sección anterior.
In [16]:
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
Out[16]:
EnvScore
(1) (2) (3)
Intercept 1.33**\n(0.657) 0.05\n(2.274) 4.64\n(3.518)
Shock 2.44***\n(0.929) 2.37**\n(0.944) 2.02**\n(0.906)
EnvScore NaN NaN -0.12***\n(0.020)
ΔEnvScore NaN NaN 0.09\n(0.060)
PublicAttention NaN NaN 2.43\n(2.436)
LogTotalAssets NaN NaN 0.03\n(0.049)
Leverage NaN NaN -0.02\n(0.034)
ROA NaN NaN 0.01\n(0.028)
RDIntensity NaN NaN 0.08***\n(0.030)
Year NaN Yes Yes
Industry NaN Yes Yes
# 378 378 378
R2 1.80% 4.91% 15.80%

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.

Conclusiones

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.