# Construct Variables¶

Construct measures of analyst behaviors at conferences (Table 6)

In [1]:

    import pandas as pd
    import numpy as np
    import os, re

In [2]:

    import nltk, re
    nltk.download('punkt')
    from nltk.tokenize import sent_tokenize
    from nltk.tokenize import word_tokenize

    [nltk_data] Downloading package punkt to
    [nltk_data]     C:\Users\rache\AppData\Roaming\nltk_data...
    [nltk_data]   Package punkt is already up-to-date!

In [3]:

    %matplotlib inline
    %load_ext autoreload
    %autoreload 2

In [4]:

    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 [5]:

    from filepath_03 import * # change to reflect input and output file path

## 1. Load Files¶

In [6]:

    transinfo = pd.read_pickle(filepath_trans)

In [7]:

    complong = pd.read_pickle(filepath_complong)

In [8]:

    complong_sent = pd.read_pickle(filepath_complong_sent)

## 2. Identify QA Pairs from Transcript Components¶

Clean component-level data to identify Q&A exchanges.

In [9]:

    fullQA = (complong
              .query('deemedQA==1')
              [['transcriptid','section','seq','par_role','analys']]
              .sort_values(by=['transcriptid','section','seq']).copy())

Remove sections that are not Q&As based on various sanity checks.

In [10]:

    fullQA['isAud'] = np.where(fullQA['par_role']!="cp", 1, 0)
    fullQA['chk_Audfirst'] = fullQA.groupby(['transcriptid','section'])['isAud'].transform('first') # section starts with other participant speaking
    fullQA['chk_hasAud'] = fullQA.groupby(['transcriptid','section'])['isAud'].transform('max')
    fullQA['chk_continuous'] = np.where(fullQA.groupby(['transcriptid','section'])['seq'].shift(1)+1 == fullQA['seq'], 1, 0)
    # deal with the first observation
    fullQA['chk_continuous'] = np.where(fullQA['seq']==fullQA.groupby(['transcriptid','section'])['seq'].transform('first'), 1, fullQA['chk_continuous'])

In [11]:

    fullQA = (fullQA.query('chk_hasAud==1 & chk_continuous==1')
              .drop(columns=['chk_hasAud','chk_continuous']))

In [12]:

    # Inspect sections that do not start with other-participant speaking.
    # For these cases, corporate participant speeches that occur before the first other participant speaking are usually introductory remarks. Remove them.
    fullQA['isAud_cumsum'] = fullQA.groupby(['transcriptid','section'])['isAud'].cumsum()
    fullQA = fullQA.query('(chk_Audfirst==1) | (chk_Audfirst==0 & isAud_cumsum>0)')

In [13]:

    fullQA = fullQA.drop(columns=['chk_Audfirst'])
    fullQA['chk_Audfirst'] = fullQA.groupby(['transcriptid','section'])['isAud'].transform('first') # section starts with Aud speaking
    assert(fullQA['chk_Audfirst'].mean()==1)
    fullQA = fullQA.drop(columns=['chk_Audfirst','isAud_cumsum'])

Define groups of Q&A exchanges

In [14]:

    fullQA['Q'] = fullQA['isAud']
    fullQA['group'] = fullQA.groupby(['transcriptid','section'])['Q'].transform('cumsum')
    fullQA['convo_per_group'] = fullQA.groupby(['transcriptid','section','group'])['seq'].transform('count')
    fullQA['seq_in_group'] = fullQA.groupby(['transcriptid','section','group']).cumcount() + 1
    fullQA['num_q_per_group'] = fullQA.groupby(['transcriptid','section','group'])['Q'].transform('sum')

In [15]:

    fullQA['convo_per_group_sec'] = fullQA.groupby(['transcriptid','section'])['convo_per_group'].transform('max')

In [16]:

    fullQA2 = fullQA.query('convo_per_group>=2 & convo_per_group_sec<=50 & num_q_per_group==1').copy()
    fullQA2 = fullQA2[['transcriptid','section','seq','analys','Q','group','convo_per_group','seq_in_group']]

In [17]:

    fullQA2['analys2'] = fullQA2.groupby(['transcriptid','section','group'])['analys'].transform('max') # Fill up analys to conversation level

Keep Q&A exchange initiated by an IBES `analys`

In [18]:

    QA = fullQA2.query('analys2.notnull()').copy()

## 3. Compute Measures¶

In [19]:

    cols = ['transcriptid','section','seq','nwords','afemale','efemale','CEO','CFO','execid','text']
    QA_text = QA.copy()
    QA_text = QA_text.merge(right=complong[cols], on=['transcriptid','section','seq'], how='inner')
    QA_text = QA_text.merge(right=complong_sent.drop(columns=['nwords']), on=['transcriptid','section','seq'], how='inner')
    QA_text['i_pos'] = (QA_text['sentiment']>0)*1
    QA_text['i_neg'] = (QA_text['sentiment']<0)*1

In [20]:

    def directness(text):
        direct = 1
        if re.findall(r"i wonder|i (?:am|was|have been) wondering|i.{1,5} like to ask|i would like to ask|(?:can|may) i ask|(?:can|could|will|would) you", text, flags=re.I):
            direct = 0
        return direct
    def followup(text):
        '''
        Investor conferences typically involve a single hosting analyst, whereas earnings calls are attended by multiple analysts.
        It is common for the hosting analyst to speak right after the executive as the Q&A host
        Define follow-up questions as those that explicitly contain the terms “follow up,” “followup,” “follow-up,” or related variants.
        '''
        followup = 0
        if re.findall(r"followup|\bfollow\b.{0,3}\bup\b", text, flags=re.I):
            followup = 1
        return followup
    def negativeq(text):
        negativeq = 0
        sentences= sent_tokenize(text)

        for sent in sentences:
            if re.findall(r"\?$", sent):
                if re.findall(r"[wc]ouldn[\’\']t you|isn[\’\']t it|doesn[\’\']t it", sent, flags=re.I):
                    negativeq = 1

        return negativeq
    def preface(text):
        preface = 0
        sentences= sent_tokenize(text)

        for i in range(1, len(sentences)):
            sent_curr = sentences[i]
            sent_prev = sentences[i-1]
            if re.findall(r"\?$", sent_curr):
                if not re.findall(r"\?$", sent_prev) and not re.findall(r"\bhello\b|\bhi\b|\bhey\b|good morning|good afternoon|good evening", sent_prev):
                    # require the preceding sentence to contain at least five words to avoid capturing brief transitional remarks (e.g., “all right” or “let’s move on”) made by the hosting analyst
                    if len(sent_prev.split())>=5:
                        preface = 1
        if len(sentences) == 1:
            preface = 0
        return preface

In [21]:

    QA_text['directness'] = QA_text['text'].apply(directness)
    QA_text['followup'] = QA_text['text'].apply(followup)
    QA_text['negativeq'] = QA_text['text'].apply(negativeq)
    QA_text['preface'] = QA_text['text'].apply(preface)

In [22]:

    QA_text['aggrscore'] = QA_text[['directness','preface','followup','negativeq']].sum(axis=1)

Aggregate to `transcriptid`-`analys` level.

In [23]:

    transANA = (QA_text.query('analys.notnull()')
                .groupby(['transcriptid','analys'])
                .agg(num_qn = ('seq','count'),
                     nwords_qn = ('nwords','mean'),
                     sentiment = ('sentiment','mean'),
                     aggr_score = ('aggrscore', 'mean'),
                    ))

## 4. Aggregate and Save¶

In [24]:

    transANA2 = transANA.copy()

In [25]:

    trans_private = pd.read_pickle(filepath_private)

In [26]:

    transANA3 = transANA2.reset_index().merge(right=trans_private, how='left')
    transANA3['i_private'] =transANA3[['i_1on1','i_breakout']].max(axis=1)

In [27]:

    transANA3 = transANA3.merge(right=transinfo[['transcriptid', 'gvkey', 'permno', 'ticker', 'ncusip', 'date']],
                                on=['transcriptid'], how='left')

In [28]:

    transANA3.to_csv(outputpath_transANA_csv)
    transANA3.to_pickle(outputpath_transANA_pkl)

In [ ]:

In [ ]:

In [ ]:

In [ ]: