syy7comai5php,AI For Trading: Project of NLP on Financial Statements (94)

Project 5: NLP on Financial Statements

Instructions

Each problem consists of a function to implement and instructions on how to implement the function. The parts of the function that need to be implemented are marked with a # TODO comment. After implementing the function, run the cell to test it against the unit tests we've provided. For each problem, we provide one or more unit tests from our project_tests package. These unit tests won't tell you if your answer is correct, but will warn you of any major errors. Your code will be checked for the correct solution when you submit it to Udacity.

Packages

When you implement the functions, you'll only need to you use the packages you've used in the classroom, like Pandas and Numpy. These packages will be imported for you. We recommend you don't add any import statements, otherwise the grader might not be able to run your code.

The other packages that we're importing are project_helper and project_tests. These are custom packages built to help you solve the problems. The project_helper module contains utility functions and graph functions. The project_tests contains the unit tests for all the problems.

Install Packages

import sys

!{sys.executable} -m pip install -r requirements.txt

Requirement already satisfied: alphalens==0.3.2 in /opt/conda/lib/python3.6/site-packages (from -r requirements.txt (line 1)) (0.3.2)

Requirement already satisfied: ipython-genutils in /opt/conda/lib/python3.6/site-packages (from traitlets>=4.2->IPython>=3.2.3->alphalens==0.3.2->-r requirements.txt (line 1)) (0.2.0)

Load Packages

import nltk

import numpy as np

import pandas as pd

import pickle

import pprint

import project_helper

import project_tests

from tqdm import tqdm

Download NLP Corpora

You'll need two corpora to run this project: the stopwords corpus for removing stopwords and wordnet for lemmatizing.

nltk.download('stopwords')

nltk.download('wordnet')

[nltk_data] Downloading package stopwords to /root/nltk_data...

[nltk_data] Package stopwords is already up-to-date!

[nltk_data] Downloading package wordnet to /root/nltk_data...

[nltk_data] Package wordnet is already up-to-date!

True

Get 10ks

We'll be running NLP analysis on 10-k documents. To do that, we first need to download the documents. For this project, we'll download 10-ks for a few companies. To lookup documents for these companies, we'll use their CIK. If you would like to run this against other stocks, we've provided the dict additional_cik for more stocks. However, the more stocks you try, the long it will take to run.

cik_lookup = {

'AMZN': '0001018724',

'BMY': '0000014272',

'CNP': '0001130310',

'CVX': '0000093410',

'FL': '0000850209',

'FRT': '0000034903',

'HON': '0000773840'}

additional_cik = {

'AEP': '0000004904',

'AXP': '0000004962',

'BA': '0000012927',

'BK': '0001390777',

'CAT': '0000018230',

'DE': '0000315189',

'DIS': '0001001039',

'DTE': '0000936340',

'ED': '0001047862',

'EMR': '0000032604',

'ETN': '0001551182',

'GE': '0000040545',

'IBM': '0000051143',

'IP': '0000051434',

'JNJ': '0000200406',

'KO': '0000021344',

'LLY': '0000059478',

'MCD': '0000063908',

'MO': '0000764180',

'MRK': '0000310158',

'MRO': '0000101778',

'PCG': '0001004980',

'PEP': '0000077476',

'PFE': '0000078003',

'PG': '0000080424',

'PNR': '0000077360',

'SYY': '0000096021',

'TXN': '0000097476',

'UTX': '0000101829',

'WFC': '0000072971',

'WMT': '0000104169',

'WY': '0000106535',

'XOM': '0000034088'}

Get list of 10-ks

The SEC has a limit on the number of calls you can make to the website per second. In order to avoid hiding that limit, we've created the SecAPI class. This will cache data from the SEC and prevent you from going over the limit.

sec_api = project_helper.SecAPI()

With the class constructed, let's pull a list of filled 10-ks from the SEC for each company.

from bs4 import BeautifulSoup

def get_sec_data(cik, doc_type, start=0, count=60):

newest_pricing_data = pd.to_datetime('2018-01-01')

rss_url = 'https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany' \

'&CIK={}&type={}&start={}&count={}&owner=exclude&output=atom' \

.format(cik, doc_type, start, count)

sec_data = sec_api.get(rss_url)

feed = BeautifulSoup(sec_data.encode('ascii'), 'xml').feed

entries = [

(

entry.content.find('filing-href').getText(),

entry.content.find('filing-type').getText(),

entry.content.find('filing-date').getText())

for entry in feed.find_all('entry', recursive=False)

if pd.to_datetime(entry.content.find('filing-date').getText()) <= newest_pricing_data]

return entries

Let's pull the list using the get_sec_data function, then display some of the results. For displaying some of the data, we'll use Amazon as an example.

example_ticker = 'AMZN'

sec_data = {}

for ticker, cik in cik_lookup.items():

sec_data[ticker] = get_sec_data(cik, '10-K')

pprint.pprint(sec_data[example_ticker][:5])

[('https://www.sec.gov/Archives/edgar/data/1018724/000101872417000011/0001018724-17-000011-index.htm',

'10-K',

'2017-02-10'),

('https://www.sec.gov/Archives/edgar/data/1018724/000101872416000172/0001018724-16-000172-index.htm',

'10-K',

'2016-01-29'),

('https://www.sec.gov/Archives/edgar/data/1018724/000101872415000006/0001018724-15-000006-index.htm',

'10-K',

'2015-01-30'),

('https://www.sec.gov/Archives/edgar/data/1018724/000101872414000006/0001018724-14-000006-index.htm',

'10-K',

'2014-01-31'),

('https://www.sec.gov/Archives/edgar/data/1018724/000119312513028520/0001193125-13-028520-index.htm',

'10-K',

'2013-01-30')]

Download 10-ks

As you see, this is a list of urls. These urls point to a file that contains metadata related to each filling. Since we don't care about the metadata, we'll pull the filling by replacing the url with the filling url.

raw_fillings_by_ticker = {}

for ticker, data in sec_data.items():

raw_fillings_by_ticker[ticker] = {}

for index_url, file_type, file_date in tqdm(data, desc='Downloading {} Fillings'.format(ticker), unit='filling'):

if (file_type == '10-K'):

file_url = index_url.replace('-index.htm', '.txt').replace('.txtl', '.txt')

raw_fillings_by_ticker[ticker][file_date] = sec_api.get(file_url)

print('Example Document:\n\n{}...'.format(next(iter(raw_fillings_by_ticker[example_ticker].values()))[:1000]))

Downloading AMZN Fillings: 100%|██████████| 22/22 [00:03<00:00, 6.04filling/s]

Downloading BMY Fillings: 100%|██████████| 27/27 [00:06<00:00, 4.39filling/s]

Downloading CNP Fillings: 100%|██████████| 19/19 [00:05<00:00, 3.78filling/s]

Downloading CVX Fillings: 100%|██████████| 25/25 [00:06<00:00, 3.74filling/s]

Downloading FL Fillings: 100%|██████████| 22/22 [00:03<00:00, 6.80filling/s]

Downloading FRT Fillings: 100%|██████████| 29/29 [00:04<00:00, 6.14filling/s]

Downloading HON Fillings: 100%|██████████| 25/25 [00:07<00:00, 3.47filling/s]

Example Document:

0001018724-17-000011.txt : 20170210

0001018724-17-000011.hdr.sgml : 20170210

20170209175636

ACCESSION NUMBER: 0001018724-17-000011

CONFORMED SUBMISSION TYPE: 10-K

PUBLIC DOCUMENT COUNT: 92

CONFORMED PERIOD OF REPORT: 20161231

FILED AS OF DATE: 20170210

DATE AS OF CHANGE: 20170209

FILER:

COMPANY DATA:

COMPANY CONFORMED NAME: AMAZON COM INC

CENTRAL INDEX KEY: 0001018724

STANDARD INDUSTRIAL CLASSIFICATION: RETAIL-CATALOG & MAIL-ORDER HOUSES [5961]

IRS NUMBER: 911646860

STATE OF INCORPORATION: DE

FISCAL YEAR END: 1231

FILING VALUES:

FORM TYPE: 10-K

SEC ACT: 1934 Act

SEC FILE NUMBER: 000-22513

FILM NUMBER: 17588807

BUSINESS ADDRESS:

STREET 1: 410 TERRY AVENUE NORTH

CITY: SEATTLE

STATE: WA

ZIP: 98109

BUSINESS PHONE: 2062661000

MAIL ADDRESS:

STREET 1: 410 TERRY AVENUE NORTH

CITY: SEATTLE

STATE: WA

ZIP: 98109

10-K

1

Get Documents

With theses fillings downloaded, we want to break them into their associated documents. These documents are sectioned off in the fillings with the tags for the start of each document and for the end of each document. There's no overlap with these documents, so each tag should come after the with no tag in between.

Implement get_documents to return a list of these documents from a filling. Make sure not to include the tag in the returned document text.

import re

def get_documents(text):

"""

Extract the documents from the text

Parameters

----------

text : str

The text with the document strings inside

Returns

-------

extracted_docs : list of str

The document strings found in `text`

"""

# write regexes

doc_start_pattern = re.compile(r'')

doc_end_pattern = re.compile(r'')

doc_start_is = [x.end() for x in doc_start_pattern.finditer(text)]

doc_end_is = [x.start() for x in doc_end_pattern.finditer(text)]

extra_docs = []

for doc_start_i, doc_end_i in zip(doc_start_is, doc_end_is):

doc = text[doc_start_i:doc_end_i]

extra_docs.append(doc)

return extra_docs

project_tests.test_get_documents(get_documents)

Tests Passed

With the get_documents function implemented, let's extract all the documents.

filling_documents_by_ticker = {}

for ticker, raw_fillings in raw_fillings_by_ticker.items():

filling_documents_by_ticker[ticker] = {}

for file_date, filling in tqdm(raw_fillings.items(), desc='Getting Documents from {} Fillings'.format(ticker), unit='filling'):

filling_documents_by_ticker[ticker][file_date] = get_documents(filling)

print('\n\n'.join([

'Document {} Filed on {}:\n{}...'.format(doc_i, file_date, doc[:200])

for file_date, docs in filling_documents_by_ticker[example_ticker].items()

for doc_i, doc in enumerate(docs)][:3]))

Getting Documents from AMZN Fillings: 100%|██████████| 17/17 [00:00<00:00, 83.03filling/s]

Getting Documents from BMY Fillings: 100%|██████████| 23/23 [00:00<00:00, 40.42filling/s]

Getting Documents from CNP Fillings: 100%|██████████| 15/15 [00:00<00:00, 35.94filling/s]

Getting Documents from CVX Fillings: 100%|██████████| 21/21 [00:00<00:00, 25.70filling/s]

Getting Documents from FL Fillings: 100%|██████████| 16/16 [00:00<00:00, 29.16filling/s]

Getting Documents from FRT Fillings: 100%|██████████| 19/19 [00:00<00:00, 27.12filling/s]

Getting Documents from HON Fillings: 100%|██████████| 20/20 [00:00<00:00, 20.94filling/s]

Document 0 Filed on 2017-02-10:

10-K

1

amzn-20161231x10k.htm

FORM 10-K

Document 1 Filed on 2017-02-10:

EX-12.1

2

amzn-20161231xex121.htm

COMPUTATION OF RATIO OF EARNINGS TO FIXED CHARGES

//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/h...

Get Document Types

Now that we have all the documents, we want to find the 10-k form in this 10-k filing. Implement the get_document_type function to return the type of document given. The document type is located on a line with the tag. For example, a form of type "TEST" would have the line TEST. Make sure to return the type as lowercase, so this example would be returned as "test".

def get_document_type(doc):

"""

Return the document type lowercased

Parameters

----------

doc : str

The document string

Returns

-------

doc_type : str

The document type lowercased

"""

# TODO: Implement

type_pattern = re.compile(r'[^\n]+')

doc_type = [x[len(''):] for x in type_pattern.findall(doc)]

return doc_type[0].lower()

project_tests.test_get_document_type(get_document_type)

Tests Passed

With the get_document_type function, we'll filter out all non 10-k documents.

ten_ks_by_ticker = {}

for ticker, filling_documents in filling_documents_by_ticker.items():

ten_ks_by_ticker[ticker] = []

for file_date, documents in filling_documents.items():

for document in documents:

if get_document_type(document) == '10-k':

ten_ks_by_ticker[ticker].append({

'cik': cik_lookup[ticker],

'file': document,

'file_date': file_date})

project_helper.print_ten_k_data(ten_ks_by_ticker[example_ticker][:5], ['cik', 'file', 'file_date'])

[

{

cik: '0001018724'

file: '\n10-K\n1\namzn-2016123...

file_date: '2017-02-10'},

{

cik: '0001018724'

file: '\n10-K\n1\namzn-2015123...

file_date: '2016-01-29'},

{

cik: '0001018724'

file: '\n10-K\n1\namzn-2014123...

file_date: '2015-01-30'},

{

cik: '0001018724'

file: '\n10-K\n1\namzn-2013123...

file_date: '2014-01-31'},

{

cik: '0001018724'

file: '\n10-K\n1\nd445434d10k....

file_date: '2013-01-30'},

]

Preprocess the Data

Clean Up

As you can see, the text for the documents are very messy. To clean this up, we'll remove the html and lowercase all the text.

def remove_html_tags(text):

text = BeautifulSoup(text, 'html.parser').get_text()

return text

def clean_text(text):

text = text.lower()

text = remove_html_tags(text)

return text

Using the clean_text function, we'll clean up all the documents.

for ticker, ten_ks in ten_ks_by_ticker.items():

for ten_k in tqdm(ten_ks, desc='Cleaning {} 10-Ks'.format(ticker), unit='10-K'):

ten_k['file_clean'] = clean_text(ten_k['file'])

project_helper.print_ten_k_data(ten_ks_by_ticker[example_ticker][:5], ['file_clean'])

Cleaning AMZN 10-Ks: 100%|██████████| 17/17 [00:35<00:00, 2.07s/10-K]

Cleaning BMY 10-Ks: 100%|██████████| 23/23 [01:13<00:00, 3.19s/10-K]

Cleaning CNP 10-Ks: 100%|██████████| 15/15 [00:54<00:00, 3.61s/10-K]

Cleaning CVX 10-Ks: 100%|██████████| 21/21 [01:52<00:00, 5.34s/10-K]

Cleaning FL 10-Ks: 100%|██████████| 16/16 [00:26<00:00, 1.63s/10-K]

Cleaning FRT 10-Ks: 100%|██████████| 19/19 [00:53<00:00, 2.83s/10-K]

Cleaning HON 10-Ks: 100%|██████████| 20/20 [00:59<00:00, 2.98s/10-K]

[

{

file_clean: '\n10-k\n1\namzn-20161231x10k.htm\nform 10-k\n\n\n...},

{

file_clean: '\n10-k\n1\namzn-20151231x10k.htm\nform 10-k\n\n\n...},

{

file_clean: '\n10-k\n1\namzn-20141231x10k.htm\nform 10-k\n\n\n...},

{

file_clean: '\n10-k\n1\namzn-20131231x10k.htm\nform 10-k\n\n\n...},

{

file_clean: '\n10-k\n1\nd445434d10k.htm\nform 10-k\n\n\nform 1...},

]

Lemmatize

With the text cleaned up, it's time to distill the verbs down. Implement the lemmatize_words function to lemmatize verbs in the list of words provided.

from nltk.stem import WordNetLemmatizer

from nltk.corpus import wordnet

def lemmatize_words(words):

"""

Lemmatize words

Parameters

----------

words : list of str

List of words

Returns

-------

lemmatized_words : list of str

List of lemmatized words

"""

# TODO: Implement

lemmatized_words = [WordNetLemmatizer().lemmatize(w, pos='v') for w in words]

# print(lemmatized_words)

return lemmatized_words

project_tests.test_lemmatize_words(lemmatize_words)

Tests Passed

With the lemmatize_words function implemented, let's lemmatize all the data.

word_pattern = re.compile('\w+')

for ticker, ten_ks in ten_ks_by_ticker.items():

for ten_k in tqdm(ten_ks, desc='Lemmatize {} 10-Ks'.format(ticker), unit='10-K'):

ten_k['file_lemma'] = lemmatize_words(word_pattern.findall(ten_k['file_clean']))

project_helper.print_ten_k_data(ten_ks_by_ticker[example_ticker][:5], ['file_lemma'])

Lemmatize AMZN 10-Ks: 100%|██████████| 17/17 [00:04<00:00, 3.5310-K/s]

Lemmatize BMY 10-Ks: 100%|██████████| 23/23 [00:10<00:00, 2.1710-K/s]

Lemmatize CNP 10-Ks: 100%|██████████| 15/15 [00:08<00:00, 1.7110-K/s]

Lemmatize CVX 10-Ks: 100%|██████████| 21/21 [00:10<00:00, 2.0610-K/s]

Lemmatize FL 10-Ks: 100%|██████████| 16/16 [00:04<00:00, 3.8510-K/s]

Lemmatize FRT 10-Ks: 100%|██████████| 19/19 [00:06<00:00, 2.9610-K/s]

Lemmatize HON 10-Ks: 100%|██████████| 20/20 [00:06<00:00, 3.3110-K/s]

[

{

file_lemma: '['10', 'k', '1', 'amzn', '20161231x10k', 'htm', '...},

{

file_lemma: '['10', 'k', '1', 'amzn', '20151231x10k', 'htm', '...},

{

file_lemma: '['10', 'k', '1', 'amzn', '20141231x10k', 'htm', '...},

{

file_lemma: '['10', 'k', '1', 'amzn', '20131231x10k', 'htm', '...},

{

file_lemma: '['10', 'k', '1', 'd445434d10k', 'htm', 'form', '1...},

]

Remove Stopwords

from nltk.corpus import stopwords

lemma_english_stopwords = lemmatize_words(stopwords.words('english'))

for ticker, ten_ks in ten_ks_by_ticker.items():

for ten_k in tqdm(ten_ks, desc='Remove Stop Words for {} 10-Ks'.format(ticker), unit='10-K'):

ten_k['file_lemma'] = [word for word in ten_k['file_lemma'] if word not in lemma_english_stopwords]

print('Stop Words Removed')

Remove Stop Words for AMZN 10-Ks: 100%|██████████| 17/17 [00:02<00:00, 8.1010-K/s]

Remove Stop Words for BMY 10-Ks: 100%|██████████| 23/23 [00:04<00:00, 5.0410-K/s]

Remove Stop Words for CNP 10-Ks: 100%|██████████| 15/15 [00:03<00:00, 3.9710-K/s]

Remove Stop Words for CVX 10-Ks: 100%|██████████| 21/21 [00:04<00:00, 4.6810-K/s]

Remove Stop Words for FL 10-Ks: 100%|██████████| 16/16 [00:01<00:00, 9.0510-K/s]

Remove Stop Words for FRT 10-Ks: 100%|██████████| 19/19 [00:02<00:00, 6.6010-K/s]

Remove Stop Words for HON 10-Ks: 100%|██████████| 20/20 [00:02<00:00, 7.6110-K/s]

Stop Words Removed

Analysis on 10ks

Loughran McDonald Sentiment Word Lists

We'll be using the Loughran and McDonald sentiment word lists. These word lists cover the following sentiment:

Negative

Positive

Uncertainty

Litigious

Constraining

Superfluous

Modal

This will allow us to do the sentiment analysis on the 10-ks. Let's first load these word lists. We'll be looking into a few of these sentiments.

import os

sentiments = ['negative', 'positive', 'uncertainty', 'litigious', 'constraining', 'interesting']

sentiment_df = pd.read_csv(os.path.join('..', '..', 'data', 'project_5_loughran_mcdonald', 'loughran_mcdonald_master_dic_2016.csv'))

sentiment_df.columns = [column.lower() for column in sentiment_df.columns] # Lowercase the columns for ease of use

# Remove unused information

sentiment_df = sentiment_df[sentiments + ['word']]

sentiment_df[sentiments] = sentiment_df[sentiments].astype(bool)

sentiment_df = sentiment_df[(sentiment_df[sentiments]).any(1)]

# Apply the same preprocessing to these words as the 10-k words

sentiment_df['word'] = lemmatize_words(sentiment_df['word'].str.lower())

sentiment_df = sentiment_df.drop_duplicates('word')

sentiment_df.head()

negative

positive

uncertainty

litigious

constraining

interesting

word9

True

False

False

False

False

False

abandon

12

True

False

False

False

False

False

abandonment

13

True

False

False

False

False

False

abandonments

51

True

False

False

False

False

False

abdicate

54

True

False

False

False

False

False

abdication

Bag of Words

using the sentiment word lists, let's generate sentiment bag of words from the 10-k documents. Implement get_bag_of_words to generate a bag of words that counts the number of sentiment words in each doc. You can ignore words that are not in sentiment_words.

from collections import defaultdict, Counter

from sklearn.feature_extraction.text import CountVectorizer

def get_bag_of_words(sentiment_words, docs):

"""

Generate a bag of words from documents for a certain sentiment

Parameters

----------

sentiment_words: Pandas Series

Words that signify a certain sentiment

docs : list of str

List of documents used to generate bag of words

Returns

-------

bag_of_words : 2-d Numpy Ndarray of int

Bag of words sentiment for each document

The first dimension is the document.

The second dimension is the word.

"""

# TODO: Implement

# @example https://blog.csdn.net/weixin_38278334/article/details/82320307

cnt_vec = CountVectorizer(vocabulary=sentiment_words)

bag_of_words = cnt_vec.fit_transform(docs).toarray()

return bag_of_words

project_tests.test_get_bag_of_words(get_bag_of_words)

Tests Passed

Using the get_bag_of_words function, we'll generate a bag of words for all the documents.

sentiment_bow_ten_ks = {}

for ticker, ten_ks in ten_ks_by_ticker.items():

lemma_docs = [' '.join(ten_k['file_lemma']) for ten_k in ten_ks]

sentiment_bow_ten_ks[ticker] = {

sentiment: get_bag_of_words(sentiment_df[sentiment_df[sentiment]]['word'], lemma_docs)

for sentiment in sentiments}

project_helper.print_ten_k_data([sentiment_bow_ten_ks[example_ticker]], sentiments)

[

{

negative: '[[0 0 0 ..., 0 0 0]\n [0 0 0 ..., 0 0 0]\n [0 0 0...

positive: '[[16 0 0 ..., 0 0 0]\n [16 0 0 ..., 0 0 ...

uncertainty: '[[0 0 0 ..., 1 1 3]\n [0 0 0 ..., 1 1 3]\n [0 0 0...

litigious: '[[0 0 0 ..., 0 0 0]\n [0 0 0 ..., 0 0 0]\n [0 0 0...

constraining: '[[0 0 0 ..., 0 0 2]\n [0 0 0 ..., 0 0 2]\n [0 0 0...

interesting: '[[2 0 0 ..., 0 0 0]\n [2 0 0 ..., 0 0 0]\n [2 0 0...},

]

Jaccard Similarity

Using the bag of words, let's calculate the jaccard similarity on the bag of words and plot it over time. Implement get_jaccard_similarity to return the jaccard similarities between each tick in time. Since the input, bag_of_words_matrix, is a bag of words for each time period in order, you just need to compute the jaccard similarities for each neighboring bag of words. Make sure to turn the bag of words into a boolean array when calculating the jaccard similarity.

from sklearn.metrics import jaccard_similarity_score

def get_jaccard_similarity(bag_of_words_matrix):

"""

Get jaccard similarities for neighboring documents

Parameters

----------

bag_of_words : 2-d Numpy Ndarray of int

Bag of words sentiment for each document

The first dimension is the document.

The second dimension is the word.

Returns

-------

jaccard_similarities : list of float

Jaccard similarities for neighboring documents

"""

# TODO: Implement

# print(bag_of_words_matrix)

# jaccard_similarity_score

bag_of_words = bag_of_words_matrix.astype(bool)

# print(bag_of_words)

jaccard_similarities = [jaccard_similarity_score(u,v) for u, v in zip(bag_of_words,bag_of_words[1:])]

return jaccard_similarities

project_tests.test_get_jaccard_similarity(get_jaccard_similarity)

Tests Passed

Using the get_jaccard_similarity function, let's plot the similarities over time.

# Get dates for the universe

file_dates = {

ticker: [ten_k['file_date'] for ten_k in ten_ks]

for ticker, ten_ks in ten_ks_by_ticker.items()}

jaccard_similarities = {

ticker: {

sentiment_name: get_jaccard_similarity(sentiment_values)

for sentiment_name, sentiment_values in ten_k_sentiments.items()}

for ticker, ten_k_sentiments in sentiment_bow_ten_ks.items()}

project_helper.plot_similarities(

[jaccard_similarities[example_ticker][sentiment] for sentiment in sentiments],

file_dates[example_ticker][1:],

'Jaccard Similarities for {} Sentiment'.format(example_ticker),

sentiments)

399d1ee7a90da259aec48ba30aa309f0.png

TFIDF

using the sentiment word lists, let's generate sentiment TFIDF from the 10-k documents. Implement get_tfidf to generate TFIDF from each document, using sentiment words as the terms. You can ignore words that are not in sentiment_words.

from sklearn.feature_extraction.text import TfidfVectorizer

def get_tfidf(sentiment_words, docs):

"""

Generate TFIDF values from documents for a certain sentiment

Parameters

----------

sentiment_words: Pandas Series

Words that signify a certain sentiment

docs : list of str

List of documents used to generate bag of words

Returns

-------

tfidf : 2-d Numpy Ndarray of float

TFIDF sentiment for each document

The first dimension is the document.

The second dimension is the word.

"""

# TODO: Implement

# @doc TfidfVectorizer https://www.jianshu.com/p/e2a0aea3630c

vectorizer = TfidfVectorizer(vocabulary = sentiment_words)

tfidf = vectorizer.fit_transform(docs).toarray()

return tfidf

project_tests.test_get_tfidf(get_tfidf)

Tests Passed

Using the get_tfidf function, let's generate the TFIDF values for all the documents.

sentiment_tfidf_ten_ks = {}

for ticker, ten_ks in ten_ks_by_ticker.items():

lemma_docs = [' '.join(ten_k['file_lemma']) for ten_k in ten_ks]

sentiment_tfidf_ten_ks[ticker] = {

sentiment: get_tfidf(sentiment_df[sentiment_df[sentiment]]['word'], lemma_docs)

for sentiment in sentiments}

project_helper.print_ten_k_data([sentiment_tfidf_ten_ks[example_ticker]], sentiments)

[

{

negative: '[[ 0. 0. 0. ..., 0. ...

positive: '[[ 0.22288432 0. 0. ..., 0. ...

uncertainty: '[[ 0. 0. 0. ..., 0.005...

litigious: '[[ 0. 0. 0. ..., 0. 0. 0.]\n [ 0. 0. 0. .....

constraining: '[[ 0. 0. 0. ..., 0. ...

interesting: '[[ 0.01673784 0. 0. ..., 0. ...},

]

Cosine Similarity

Using the TFIDF values, we'll calculate the cosine similarity and plot it over time. Implement get_cosine_similarity to return the cosine similarities between each tick in time. Since the input, tfidf_matrix, is a TFIDF vector for each time period in order, you just need to computer the cosine similarities for each neighboring vector.

from sklearn.metrics.pairwise import cosine_similarity

def get_cosine_similarity(tfidf_matrix):

"""

Get cosine similarities for each neighboring TFIDF vector/document

Parameters

----------

tfidf : 2-d Numpy Ndarray of float

TFIDF sentiment for each document

The first dimension is the document.

The second dimension is the word.

Returns

-------

cosine_similarities : list of float

Cosine similarities for neighboring documents

"""

# TODO: Implement

# print(tfidf_matrix)

cos_similarity= cosine_similarity(tfidf_matrix[0:], tfidf_matrix[1:])

return cos_similarity[0].tolist()

project_tests.test_get_cosine_similarity(get_cosine_similarity)

Tests Passed

Let's plot the cosine similarities over time.

cosine_similarities = {

ticker: {

sentiment_name: get_cosine_similarity(sentiment_values)

for sentiment_name, sentiment_values in ten_k_sentiments.items()}

for ticker, ten_k_sentiments in sentiment_tfidf_ten_ks.items()}

project_helper.plot_similarities(

[cosine_similarities[example_ticker][sentiment] for sentiment in sentiments],

file_dates[example_ticker][1:],

'Cosine Similarities for {} Sentiment'.format(example_ticker),

sentiments)

4db3a4769d5520c16fc1ca96acabd4c7.png

Evaluate Alpha Factors

Just like we did in project 4, let's evaluate the alpha factors. For this section, we'll just be looking at the cosine similarities, but it can be applied to the jaccard similarities as well.

Price Data

Let's get yearly pricing to run the factor against, since 10-Ks are produced annually.

pricing = pd.read_csv('../../data/project_5_yr/yr-quotemedia.csv', parse_dates=['date'])

pricing = pricing.pivot(index='date', columns='ticker', values='adj_close')

pricing.tail(5)

ticker

A

AA

AAAP

AABA

AAC

AADR

AAIT

AAL

AAMC

AAME

...

ZUMZ

ZUO

ZVZZC

ZVZZCNX

ZX

ZXYZ_A

ZYME

ZYNE

ZZK

ZZZ

date2014-01-01

39.43238724

nan

nan

50.51000000

30.92000000

36.88899069

32.90504993

51.89970750

310.12000000

3.94778326

...

38.63000000

nan

nan

nan

1.43000000

nan

nan

nan

100.00000000

0.50000000

2015-01-01

40.79862571

nan

31.27000000

33.26000000

19.06000000

38.06921608

30.53000000

41.33893271

17.16000000

4.91270038

...

15.12000000

nan

nan

nan

0.79000000

14.10000000

nan

10.07000000

100.00000000

1.00000000

2016-01-01

44.93909238

28.08000000

26.76000000

38.67000000

7.24000000

39.81959334

nan

46.08991196

53.50000000

4.05320152

...

21.85000000

nan

100.01000000

14.26000000

1.19890000

8.00000000

nan

15.59000000

100.00000000

25.00000000

2017-01-01

66.65391782

53.87000000

81.62000000

69.85000000

9.00000000

58.83570736

nan

51.80358470

81.60000000

3.37888199

...

20.82500000

nan

100.10000000

nan

1.21650000

nan

7.59310000

12.52000000

25.00000000

0.01000000

2018-01-01

61.80000000

46.88000000

81.63000000

73.35000000

9.81000000

52.88000000

nan

37.99000000

67.90000000

2.57500000

...

24.30000000

26.74000000

nan

nan

1.38000000

nan

15.34000000

9.17000000

nan

nan

5 rows × 11941 columns

Dict to DataFrame

The alphalens library uses dataframes, so we we'll need to turn our dictionary into a dataframe.

cosine_similarities_df_dict = {'date': [], 'ticker': [], 'sentiment': [], 'value': []}

for ticker, ten_k_sentiments in cosine_similarities.items():

for sentiment_name, sentiment_values in ten_k_sentiments.items():

for sentiment_values, sentiment_value in enumerate(sentiment_values):

cosine_similarities_df_dict['ticker'].append(ticker)

cosine_similarities_df_dict['sentiment'].append(sentiment_name)

cosine_similarities_df_dict['value'].append(sentiment_value)

cosine_similarities_df_dict['date'].append(file_dates[ticker][1:][sentiment_values])

cosine_similarities_df = pd.DataFrame(cosine_similarities_df_dict)

cosine_similarities_df['date'] = pd.DatetimeIndex(cosine_similarities_df['date']).year

cosine_similarities_df['date'] = pd.to_datetime(cosine_similarities_df['date'], format='%Y')

cosine_similarities_df.head()

date

ticker

sentiment

value0

2016-01-01

AMZN

negative

0.98065125

1

2015-01-01

AMZN

negative

0.95001565

2

2014-01-01

AMZN

negative

0.93176312

3

2013-01-01

AMZN

negative

0.89701433

4

2012-01-01

AMZN

negative

0.85838968

Alphalens Format

In order to use a lot of the alphalens functions, we need to aligned the indices and convert the time to unix timestamp. In this next cell, we'll do just that.

import alphalens as al

factor_data = {}

skipped_sentiments = []

for sentiment in sentiments:

cs_df = cosine_similarities_df[(cosine_similarities_df['sentiment'] == sentiment)]

cs_df = cs_df.pivot(index='date', columns='ticker', values='value')

try:

data = al.utils.get_clean_factor_and_forward_returns(cs_df.stack(), pricing, quantiles=5, bins=None, periods=[1])

factor_data[sentiment] = data

except:

skipped_sentiments.append(sentiment)

if skipped_sentiments:

print('\nSkipped the following sentiments:\n{}'.format('\n'.join(skipped_sentiments)))

factor_data[sentiments[0]].head()

/opt/conda/lib/python3.6/site-packages/statsmodels/compat/pandas.py:56: FutureWarning: The pandas.core.datetools module is deprecated and will be removed in a future version. Please use the pandas.tseries module instead.

from pandas.core import datetools

Dropped 0.0% entries from factor data: 0.0% in forward returns computation and 0.0% in binning phase (set max_loss=0 to see potentially suppressed Exceptions).

max_loss is 35.0%, not exceeded: OK!

Dropped 0.0% entries from factor data: 0.0% in forward returns computation and 0.0% in binning phase (set max_loss=0 to see potentially suppressed Exceptions).

max_loss is 35.0%, not exceeded: OK!

Dropped 0.0% entries from factor data: 0.0% in forward returns computation and 0.0% in binning phase (set max_loss=0 to see potentially suppressed Exceptions).

max_loss is 35.0%, not exceeded: OK!

Dropped 0.0% entries from factor data: 0.0% in forward returns computation and 0.0% in binning phase (set max_loss=0 to see potentially suppressed Exceptions).

max_loss is 35.0%, not exceeded: OK!

Dropped 0.0% entries from factor data: 0.0% in forward returns computation and 0.0% in binning phase (set max_loss=0 to see potentially suppressed Exceptions).

max_loss is 35.0%, not exceeded: OK!

Dropped 0.0% entries from factor data: 0.0% in forward returns computation and 0.0% in binning phase (set max_loss=0 to see potentially suppressed Exceptions).

max_loss is 35.0%, not exceeded: OK!

1D

factor

factor_quantile

date

asset1994-01-01

BMY

0.53264104

0.44665176

1

CVX

0.22211880

0.64730294

5

FRT

0.17159556

0.46696523

3

1995-01-01

BMY

0.32152919

0.43571094

3

CVX

0.28478156

0.62523346

5

Alphalens Format with Unix Time

Alphalen's factor_rank_autocorrelation and mean_return_by_quantile functions require unix timestamps to work, so we'll also create factor dataframes with unix time.

unixt_factor_data = {

factor: data.set_index(pd.MultiIndex.from_tuples(

[(x.timestamp(), y) for x, y in data.index.values],

names=['date', 'asset']))

for factor, data in factor_data.items()}

Factor Returns

Let's view the factor returns over time. We should be seeing it generally move up and to the right.

ls_factor_returns = pd.DataFrame()

for factor_name, data in factor_data.items():

ls_factor_returns[factor_name] = al.performance.factor_returns(data).iloc[:, 0]

(1 + ls_factor_returns).cumprod().plot()

2551d047a7ebbe832a706aa8e07de67a.png

Basis Points Per Day per Quantile

It is not enough to look just at the factor weighted return. A good alpha is also monotonic in quantiles. Let's looks the basis points for the factor returns.

qr_factor_returns = pd.DataFrame()

for factor_name, data in unixt_factor_data.items():

qr_factor_returns[factor_name] = al.performance.mean_return_by_quantile(data)[0].iloc[:, 0]

(10000*qr_factor_returns).plot.bar(

subplots=True,

sharey=True,

layout=(5,3),

figsize=(14, 14),

legend=False)

array([[,

,

],

[,

,

],

[,

,

],

[,

,

],

[,

,

]], dtype=object)

88c76d2461dc530d4a86d5e35b36c197.png

Turnover Analysis

Without doing a full and formal backtest, we can analyze how stable the alphas are over time. Stability in this sense means that from period to period, the alpha ranks do not change much. Since trading is costly, we always prefer, all other things being equal, that the ranks do not change significantly per period. We can measure this with the Factor Rank Autocorrelation (FRA).

ls_FRA = pd.DataFrame()

for factor, data in unixt_factor_data.items():

ls_FRA[factor] = al.performance.factor_rank_autocorrelation(data)

ls_FRA.plot(title="Factor Rank Autocorrelation")

7b55ab3cc13a50ea74bced36b74a0488.png

Sharpe Ratio of the Alphas

The last analysis we'll do on the factors will be sharpe ratio. Let's see what the sharpe ratio for the factors are. Generally, a Sharpe Ratio of near 1.0 or higher is an acceptable single alpha for this universe.

daily_annualization_factor = np.sqrt(252)

(daily_annualization_factor * ls_factor_returns.mean() / ls_factor_returns.std()).round(2)

negative -0.24000000

positive -1.33000000

uncertainty 0.82000000

litigious 1.34000000

constraining -2.99000000

interesting -2.47000000

dtype: float64

That's it! You've successfully done sentiment analysis on 10-ks!

为者常成,行者常至

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值