Link Financial Analysts with IBES analys¶
Link non-corporate participants in transcript to IBES analys.
In [1]:
import pandas as pd
import numpy as np
import os, openpyxl, re, glob, nltk, multiprocessing
from fuzzywuzzy import fuzz
import dask.dataframe as dd
In [2]:
%matplotlib inline
%load_ext autoreload
%autoreload 2
In [3]:
pd.set_option('display.memory_usage', 'deep')
pd.set_option('display.precision', 2)
pd.set_option('display.width', 200)
pd.set_option('display.max_rows', 300)
pd.set_option('display.max_columns', None)
pd.set_option('display.max_colwidth', 80)
pd.options.display.float_format = '{:,.4f}'.format
In [4]:
from filepath_01 import * # change to reflect input and output file path
Load participants data
In [5]:
parlong = pd.read_pickle(filepath_PAR)
Add broker_id
In [6]:
temp_sc = pd.read_pickle(filepath_schedule)[['permno','broker_id_1','event_id']]
temp_sc = temp_sc.rename(columns={'permno':'permno_sc'})
In [7]:
temp = (pd.read_pickle(filepath_trans)
[['transcriptid','event_id', 'ric','date','permno']]
.merge(right=temp_sc, how='left', on=['event_id']))
Obtain non-corporate participants
In [8]:
OP = (parlong
.query('par_role=="op"')
[['transcriptid', 'par_name', 'par_firmtitle', '_broker', 'par_seq']]
.merge(right=temp, how='left', on=['transcriptid']).copy())
OP = OP.loc[OP.permno.notnull()].copy()
OP['cyear']=OP.date.dt.year
Remove unidentified audience member
In [9]:
OP['todelete'] = (OP.par_name.str.contains("unidentified", regex=True, flags=re.I))*1
OP = OP.query('todelete==0').drop(columns=['todelete'])
Clean broker information from participant title
In [10]:
def clean_firmtitle(string):
'''
Remove common words
'''
string2 = string
# remove common words
string2 = re.sub(r"analyst|senior analyst|research division|research dividsion|thomson reuters journalist|reuters journalist|thomson reuters media|video presentation|investment|holdings|research|advisor?|llc|\bmd\b", "", string, flags=re.I).strip()
return string2
In [11]:
def remove_special_char(string):
'''
Remove special characters
'''
string2=string
string2 = re.sub(r"[^a-zA-Z\d\s]", '', string2).strip()
string2 = re.sub(u'\xa0', u' ', string2).strip()
string2 = re.sub(r" ", " ", string2).strip()
string2 = string2 if len(string2)>3 else None
return string2
In [12]:
OP['_firmtitle'] = OP.apply(lambda x: clean_firmtitle(x['par_firmtitle']), axis=1)
OP['_firmtitle'] = OP.apply(lambda x: remove_special_char(x['_firmtitle']), axis=1)
Use forecast issuance information extracted from IBES (analyst-broker-firm-forecast date), merge other participants with IBES analysts. Analyst names were manually collected.
In [13]:
ibesdates=pd.read_stata(filepath_ibesdata)
ibesdates['analys_name']=ibesdates['analys_name'].str.lower()
ibesdates['fname']=ibesdates['fname'].str.lower()
ibesdates['lname']=ibesdates['lname'].str.lower()
Match using firm id, broker id, and analyst names. Manually clean and refine the match.
In [14]:
match1=(OP[['transcriptid','par_name','permno','broker_id_1','date']]
.copy()
.query('broker_id_1.notnull()')
.merge(right=ibesdates[['permno','analys','lname','fname','analys_name','broker_id_1','afemale','anndats']],
how='inner',on=['permno','broker_id_1']))
A = [d.date() for d in match1['date']]
B = [d.date() for d in match1['anndats']]
match1['gap'] = np.busday_count(A, B)
In [15]:
%%time
score1 = (dd.from_pandas(match1, npartitions = 4*multiprocessing.cpu_count())
.map_partitions(lambda row: row.apply((lambda row: fuzz.token_set_ratio(row.lname, row.par_name)), axis=1))
.compute(scheduler='processes'))
score2 = (dd.from_pandas(match1, npartitions = 4*multiprocessing.cpu_count())
.map_partitions(lambda row: row.apply((lambda row: fuzz.token_set_ratio(row.fname, row.par_name)), axis=1))
.compute(scheduler='processes'))
score3 = (dd.from_pandas(match1, npartitions = 4*multiprocessing.cpu_count())
.map_partitions(lambda row: row.apply((lambda row: fuzz.token_set_ratio(row.analys_name, row.par_name)), axis=1))
.compute(scheduler='processes'))
match1['score1'] = score1
match1['score2'] = score2
match1['score3'] = score3
CPU times: total: 14.6 s Wall time: 1min 15s
Various cutoff scores are determined via manual inspection
In [16]:
cuttoff1 = 80
match1 = match1.query('score1>=@cuttoff1 | score2>=@cuttoff1 | score3>=@cuttoff1')
match1['avgscore'] = match1.filter(regex="score").mean(axis=1)
In [17]:
match1['_pre'] = (match1['gap']<0)*1
match1['_post'] = (match1['gap']>0)*1
match1['_gap_pre'] = np.where(match1['gap']<0, match1['gap'], np.nan)
match1['_gap_post'] = np.where(match1['gap']>0, match1['gap'], np.nan)
match1['_gap_abs'] = abs(match1['gap'])
match1['_withinoneyear'] = (match1['_gap_abs']<=126)*1
In [18]:
match1 = match1.sort_values(by=['transcriptid', 'par_name', '_withinoneyear','avgscore', '_gap_abs'],
ascending=[True, True, False, False, True])
match1 = match1.drop_duplicates(subset=['transcriptid', 'par_name'])
In [19]:
match1.query('_gap_abs>126 & score1<80').sort_values(by=['score1'])
match1['DEL'] = ((match1._gap_abs>126) & (match1.score1<55))*1
match1 = match1.query('DEL==0').drop(columns=['DEL'])
For observations without broker ids, manually match based on broker names, firm, and analyst names. Next, manually inspect and clean matched observations from both steps.
In [20]:
# load results from manual inspection
result = pd.read_pickle("../data/manual/01_3A_result.pkl")
Merge back
In [21]:
selvars=['transcriptid','par_name','analys','broker_id_1','afemale','lname']
OP2 = OP.merge(right=result[selvars], on=['transcriptid','par_name'], how='left', indicator=True)
OP2['is_coveringanalyst'] = (OP2['_merge']=='both')*1
OP2 = OP2.drop(columns=['_merge'])
In [22]:
selvars =['transcriptid','par_name','par_seq',
'analys','is_coveringanalyst',
'afemale']
In [23]:
OP_final = OP2.copy()
In [24]:
OP_final[selvars].to_pickle(outputpath_op)
In [25]:
parlong2 = parlong.merge(right=OP_final[['transcriptid','par_name','analys','is_coveringanalyst', 'afemale']],
on=['transcriptid','par_name'], how='left')
In [26]:
parlong2['has_coveringanalyst'] = parlong2.groupby(['transcriptid'])['is_coveringanalyst'].transform('max')
parlong2 = parlong2.sort_values(by=['transcriptid','par_seq'])
In [27]:
covering_analys = parlong2.query('analys.notnull()')[['transcriptid','analys']].copy()
covering_analys['analys'] = covering_analys['analys'].astype(str)
covering_analys = covering_analys.groupby(['transcriptid'])['analys'].apply("|".join).reset_index().rename(columns={'analys':'covering_analys'})
In [28]:
parlong2.to_pickle(outputpath_par2)
In [ ]:
In [ ]: