Warren Buffett jest jednym z najbardziej znanych inwestorów na świecie. Uczeń słynnego Benjamina Grahama nosi przydomek „wyrocznia z Omaha”. Zawdzięcza go trafnym decyzjom inwestycyjnym, jakie podejmował na przestrzeni ostatnich kilkudziesięciu lat.
Większość wspomnianych decyzji odnosi się do Berkshire Hathaway, którym Buffett zarządza. Holding ten regularnie osiąga świetnie wyniki i z łatwością „pokonuje” rynek. Jako CEO, Buffett co roku dzieli się z akcjonariuszami otwartymi listami. Ich treść podsumowuje miniony rok i odnosi się do perspektyw na kolejne lata. Zatem być może właśnie w nich jest zawarta przynajmniej część recepty na sukces.
- Wstęp.
- Cel projektu
- Założenia dotyczące projektu
- Opis użytych danych
- Wczytanie słowników
- Pobranie i przygotowanie danych
- Eksploracyjna analiza danych
- Weryfikacja hipotez
- Podsumowanie
Wstęp
Jako zapalony inwestor natknąłem się kiedyś na analizę sentymentu listów Buffeta. Zgrabne podsumowanie pokazywało, jak nastrój danego listu odnosi się do wahań rynkowych w danym roku. Wtedy po raz pierwszy pomyślałem, że może warto by było spojrzeć na ten sam temat, ale z nieco innej perspektywy i sprawdzić kilka dodatkowych hipotez.
Szalenie mnie ciekawi, jaki był sentyment poszczególnych listów. Dodatkowo chciałbym sprawdzić, czy istnieje jakakolwiek zależność, pomiędzy wspomnianym sentymentem a wynikami, jakie osiągała amerykańska giełda i Berkshire Hathaway.
Cel projektu
Celem ogólnym projektu jest zbadanie sentymentu listów publikowanych przez Warrena Buffeta na stronie Berkshire Hathaway. Celem szczegółowym jest zbadanie czterech hipotez:
Hipoteza 1
Dzięki niej chciałbym odpowiedzieć na pytanie: czy sentyment listów jest skorelowany z wynikami S&P 500 z minionego roku?
- H0: Nie ma korelacji pomiędzy sentymentem listów a wynikami indeksu S&P 500 z minionego roku.
- H1: Istnieje korelacja pomiędzy sentymentem listów a wynikami indeksu S&P 500 z minionego roku.
Hipoteza 2
Dzięki niej chciałbym odpowiedzieć na pytanie dotyczące przyszłości: czy sentyment listów jest skorelowany z wynikami S&P 500 z kolejnego roku (chodzi o rok, w którym to list był publikowany)?
- H0: Nie ma korelacji pomiędzy sentymentem listów a wynikami indeksu S&P 500 z danego roku.
- H1: Istnieje korelacja pomiędzy sentymentem listów a wynikami indeksu S&P 500 z danego roku.
Hipoteza 3
Dzięki niej chciałbym odpowiedzieć na pytanie: czy sentyment listów jest skorelowany z wynikami, jakie osiągnął Berkshire Hathaway w minionym roku?
- H0: Nie ma korelacji pomiędzy sentymentem listów a wynikami giełdowymi Berkshire Hathaway z minionego roku.
- H1: Istnieje korelacja pomiędzy sentymentem listów a wynikami giełdowymi Berkshire Hathaway z minionego roku .
Hipoteza 4
Dzięki niej chciałbym odpowiedzieć na pytanie: czy sentyment listów jest skorelowany z wynikami, jakie osiągnął Berkshire Hathaway w kolejnym roku?
- H0: Nie istnieje korelacja pomiędzy sentymentem listów a wynikami giełdowymi Berkshire Hathaway z danego roku.
- H1: Istnieje korelacja pomiędzy sentymentem listów a wynikami giełdowymi Berkshire Hathaway z danego roku.
W mojej ocenie szczególnie interesująca jest hipoteza dotycząca korelacji sentymentu listu i przyszłych wahań kursu S&P 500. Jeśli udałoby się ją potwierdzić, to oznaczałoby to, można wnioskować, że Warrent Buffet przewidywał nastroje panujące na amerykańskiej giełdzie).
Założenia dotyczące projektu
Naiwnie zakładam, że S&P 500 obrazuje sytuację dla całego rynku amerykańskiego, co oczywiście nie musi być prawdą. Nie mniej indeks ten często jest „benchmarkiem” w różnego typu analizach rynkowych.
Do zbadania sentymentu listów użyję analizy sentymentu bing, opracowanej przez Bing Liu z University of Illinois w Chicago. To podejście do analizy sentymentu można opisać za pomocą następujących kroków:
- Wyczyszczenie tekstu poprzez usunięcie wszystkich liczb, niepotrzebnych znaków i „stopwords-ów”.
- Podzielenie dokumentu na zbiór unikatowych słów.
- Określenie sentymentu każdego słowa: negatywny, neutralny, pozytywny.
- Wyznaczenie współczynnika sentymentu tekstu, który jest wyrażony jako: (liczba słów pozytywnych – liczba słów negatywnych) / (liczba wszystkich słów).
Bing Liu udostępnił słownik wyrazów pozytywnych i negatywnych, z którego będę korzystać. Zawiera on 6790 wyrazów (2006 pozytywnych, 4784 negatywnych).
Opis użytych danych
Do przeprowadzenia analizy użyję następujących danych:
- Listów Warrena Buffetta do akcjonariuszy Berkshire Hathaway. Są dostępne na oficjalnej stronie internetowej BH. Dla przykładu: list widniejący na podstronie „2016”:
- podsumowuje rok 2016,
- mówi o perspektywach na rok 2017,
- został opublikowany w lutym 2017 roku.
- Wahań kursu Berkshire Hathaway na przestrzeni ostatnich 40 lat.
- Wahań kursu indeksu S&P 500 (dla tych którzy nie wiedzą: S&P 500 jest indeksem giełdy amerykańskiej. W jego skład wchodzi 500 przedsiębiorstw o największej kapitalizacji, notowanych na New York Stock Exchange i NASDAQ. Powinien on zatem odzwierciedlać ogólny stan amerykańskiego rynku w danym roku).
- Słownika wyrazów pozytywnych.
- Słownika wyrazów negatywnych.
- Słownika „stopwords-ów”.
Wczytanie słówników
Wczytuję słowniki wyrazów pozytywnych i negatywnych.
pos = open("data\\positive.txt","r") pos = list(pos.read().split('\n')) neg = open("data\\negative.txt","r") neg = list(neg.read().split('\n'))
Wczytuję słowniki stopwords z dwóch bibliotek dostępnych w Python: many_stop_words, stop_words.
# many_stop_words stop_words_1 = list(many_stop_words.get_stop_words('en')) # stop_words stop_words_2 = get_stop_words('en') # łączę oba zbiory stop_words_3 = stop_words_1 + stop_words_2
Pobranie i przygotowanie danych
Listy są umieszczane na stronie w dwóch formatach: pdf i html. Muszę zatem zdefiniować dwie funkcje.
Funkcja służąca do pobierania listów w formacie html:
def download_letters_html(start_year): a = [] for x in range(0,30): year = start_year+x print('Pobieram list: ' + str(year)) if year > 2001: break # prepare url if(year < 1998): url = 'http://www.berkshirehathaway.com/letters/' + str(year) + '.html' elif((year > 1997) & (year < 2000)): url = 'http://www.berkshirehathaway.com/letters/' + str(year) + 'htm.html' elif((year > 1999) & (year < 2002)): url = 'http://www.berkshirehathaway.com/' + str(year) + 'ar/' + str(year) + 'letter.html' elif(year==2002): break # download website page = requests.get(url) tree = html.fromstring(page.content) a.append([year,tree]) return a
Funkcja służąca do pobierania listów w formacie pdf:
def download_letters_pdf(start_year): a = [] for x in range(0,30): year = start_year+x print('Pobieram list: ' + str(year)) url = "http://www.berkshirehathaway.com/letters/" + str(year) + "ltr.pdf" pdf = pdfx.PDFx(url) text = pdf.get_text() a.append([year,text]) if(year==2016): break return a
Po pobraniu danych, będę musiał zbadać ich sentyment. W tym celu definiuję następującą funkcję:
def analyze_letters(letters): ret = [] words = [] if(letters[0][0]>2000): for letter in letters: blob = TextBlob(letter[1]) year = letter[0] print('Analizuję sentyment listu: ' + str(year)) num_pos_words = 0 num_neg_words = 0 num_neu_words = 0 for word in blob.words: # check if the word is really a word if(word not in stop_words_3 and word.isalnum() and word.isalpha() and len(word) > 1): if(word in pos): num_pos_words = num_pos_words + 1 sentiment = 'positive' elif(word in neg): num_neg_words = num_neg_words + 1 sentiment = 'negative' else: num_neu_words = num_neu_words + 1 sentiment = 'neutral' words.append([year,word,sentiment]) pred_year = year+1 ret.append([pred_year, num_pos_words, num_neg_words, num_neu_words]) else: for letter in letters: year = letter[0] blob = TextBlob('') print('Analizuję sentyment listu: ' + str(year)) sentiment = '' for x in letter[1].itertext(): blob_n = TextBlob(x) blob = blob + blob_n num_pos_words = 0 num_neg_words = 0 num_neu_words = 0 for word in blob.words: if(word not in stop_words_3 and word.isalnum() and word.isalpha() and len(word) > 1): if(word in pos): num_pos_words = num_pos_words + 1 elif(word in neg): num_neg_words = num_neg_words + 1 else: num_neu_words = num_neu_words + 1 sentiment = 'neutral' words.append([year,word,sentiment]) pred_year = year+1 ret.append([pred_year, num_pos_words, num_neg_words, num_neu_words]) return ret, words
Funkcja zawiera jedną „duzą” instrukcję warunkową if, która służy do podzielenia listów o różnych formatach (html, pdf). W zależności of formatu listu, nieco inaczej wygląda jego analiza.
Mając zdefiniowane wszystkie niezbędne funkcje mogę przejść do ich wywołania.
# html letters letters_html = download_letters_html(1977) letters_sent_html, words_1 = analyze_letters(letters_html) # pdf letter letters_pdf = download_letters_pdf(2003) letters_sent_pdf, words_2 = analyze_letters(letters_pdf)
Powyższy proces na moim komputerze trwa ok 7-8 minut. Po jego zakończeniu otrzymuję dwa obiekty typu list: letters_sent_html i letters_sent_pdf. W nich zapisane są wszystkie niezbędne dane do obliczenia sentymentu poszczególnych listów. Wczytuję je następnie do dataframe’a.
df = pd.DataFrame(letters_sent_html, columns=['Release_Year', 'Num_Pos_Words', 'Num_Neg_Words', 'Num_Neu_Words']) df = df.append(pd.DataFrame(letters_sent_pdf, columns=df.columns), ignore_index=True) df.head()
Release_Year Num_Pos_Words Num_Neg_Words Num_Neu_Words 0 1978 69 39 1332 1 1979 135 63 1701 2 1980 182 122 2562 3 1981 165 147 2975 4 1982 144 127 2673
UWAGA: w dalszej analizie pomijam dane z roku 2003. Niestety ale przy próbie pobieranie listu z 2003 roku otrzymywałem błąd z biblioteki pdfx.
Do pełni szczęścia brakuje mi jeszcze danych dotyczących wahań kursów Berkshire Hathaway i S&P 500. Dodaję je zatem ręcznie:
s_and_p__this_year = [0.064,0.182,0.323,-0.05,0.214,0.224,0.061,0.316,0.186,0.051,0.166,0.317,-0.031,0.305,0.076,0.101,0.013,0.376,0.23,0.334,0.286,0.21,-0.091,-0.119,-0.221,0.109,0.049,0.158,0.055,-0.37,0.265,0.151,0.021,0.16,0.324,0.137,0.014,0.0954,0.1952] b_h__this_year = [0.145,1.025,0.328,0.318,0.384,0.69,-0.027,0.937,0.142,0.046,0.593,0.846,-0.231,0.356,0.298,0.389,0.250,0.574,0.062,0.349,0.522,-0.199,0.266,0.065,-0.038,0.043,0.008,0.2410,0.2870,-0.3180,0.0270,0.2140,-0.047,0.168,0.3270,0.270,-0.125,0.23,0.2137] s_and_p__last_year = [-0.074,0.064,0.182,0.323,-0.05,0.214,0.224,0.061,0.316,0.186,0.051,0.166,0.317,-0.031,0.305,0.076,0.101,0.013,0.376,0.23,0.334,0.286,0.21,-0.091,-0.119,-0.221,0.109,0.049,0.158,0.055,-0.37,0.265,0.151,0.021,0.16,0.324,0.137,0.014,0.0954] b_h__last_year = [0.468, 0.145,1.025,0.328,0.318,0.384,0.69,-0.027,0.937,0.142,0.046,0.593,0.846,-0.231,0.356,0.298,0.389,0.250,0.574,0.062,0.349,0.522,-0.199,0.266,0.065,-0.038,0.043,0.008,0.2410,0.2870,-0.3180,0.0270,0.2140,-0.047,0.168,0.3270,0.270,-0.125,0.23] df['b_h__this_year'] = b_h__this_year df['s_and_p__this_year'] = s_and_p__this_year df['b_h__last_year'] = b_h__last_year df['s_and_p__last_year'] = s_and_p__last_year
Teraz, gdy mam już wszystkie dane, mogę przejść do obliczenia współczynnika sentymentu.
df['coeff'] = (df.Num_Pos_Words-df.Num_Neg_Words)/(df.Num_Pos_Words+df.Num_Neg_Words+df.Num_Neu_Words)
Teraz całość wygląda następująco:
Release_Year Num_Pos_Words Num_Neg_Words Num_Neu_Words coeff 0 1978 69 39 1332 0.020833 1 1979 135 63 1701 0.037915 2 1980 182 122 2562 0.020935 3 1981 165 147 2975 0.005476 4 1982 144 127 2673 0.005774
Powyższy proces zajmuje ok. 10 min, dlatego zapisuję jego wyniki do pliku csv.
# small backup df.to_csv("data_frame.csv")
Eksploracyjna analiza danych
W tym punkcie wykonam analizę poszczególnych zmiennych, zaczynając od współczynnika sentymentu. Sprawdźmy zatem jak zmieniał się on na przestrzeni lat.
sns.set(font_scale=1.3, style="whitegrid") f, ax = plt.subplots(figsize=(9, 15)) df['Release_Year'] = df.Release_Year.astype("category") sns.barplot(x="coeff", y="Release_Year", data=df, color='#e64845') ax.set(xlim=(-0.01, 0.05), ylabel="", title='Sentyment listów wg. roku', xlabel='Sentyment') plt.show()
Proszę zwróć uwagę na sentyment listów opublikowanych w roku 2002 i 2009. Mówi Ci to coś? 🙂 Tak, tak… rok 2001 to pęknięcie bańki internetowej (z ang. dot-com bubble). Rok 2008 to kryzys zapoczątkowany w USA, poprzez zapaść na rynku pożyczek hipotecznych (sytuacja ta została doskonale opisana w filmie Big short. Szczerze polecam 🙂 ). Hipoteza dotycząca korelacji pomiędzy sentyentem listów i sytuacją na rynku w minionym roku wydaje się być bardzo prawdopodobna. Za wcześnie jednak by ją potwierdzić. Potrzebuję na to twardych dowodów.
Teraz pora na analizę rozkładów poszczególnym zmiennych. Ma ona na celu zweryfikowanie „normalności” rozkładu (pod kątem analizy korelacji) i wyłapanie wartości odstających.
Dosyć równomierny rozkład. Lekka skośność. Żadnych wartości odstających.
Dwie obserwacje odstające. Prawoskośność.
Trzy obserwacje odstające. Prawoskośność.
Jedna obserwacja odstająca. Lewoskośność.
Dwie obserwacje odstające. Lewoskośność.
Sprawdzę jeszcze jak wyglądają histogramy dla poszczwególnych zmiennych. Dodatkowo zweryfikuję zależności pomiędzy poszczególnymi zmiennymi na wykresie punktowym.
Sentyment vs wahania kursu Berkshire Hathaway w minionym roku
Widać niewielką korelację i kilka obserwacji odstających.
Sentyment vs wahania kursu Berkshire Hathaway w danym roku
Korelacja niemalże niewidoczna.
Sentyment vs wahania indexu S&P 500 w minionym roku
Dosyć wysoka korelacja. Jesli któraś z hipotez zostanie potwierdzona to stawiam, że to będzie ta 🙂
Sentyment vs wahania indexu S&P 500 w danym roku
Niewielka korelacja.
Test normalności rozkładów
Zanim przejdę dalej muszę jeszcze sprawdzić testem statystycznych rozkłady zmiennych. Zgodnie z dokumentacją sciPy: „This function tests null hypothesis that a sample comes from a normal distribution„. Hipoteza zerowa mówi, że rozkład badanej próby jest rozkładem normalnym. Zbieram zatem dowody na odrzucenie hipotezy. Jako alfa wyznaczam klasyczną wartość 0,05.
stats.normaltest(df.coeff)[1] > 0.05 stats.normaltest(df.b_h__last_year)[1] > 0.05 stats.normaltest(df.b_h__this_year)[1] > 0.05 stats.normaltest(df.s_and_p__last_year)[1] > 0.05 stats.normaltest(df.s_and_p__this_year)[1] > 0.05
Wszędzie tam, gdzie p-value jest większy od 0.05, mam do czynienia z rozkładem normalnym. Wyniki wykonywania powyższego kodu wyglądają następująco:
True True True True False
A więc tylko w ostatnim przypadku mogę być pewny, że dane nie pochodzą z rozkładu normalnego. W dalszej analizie nie mogę zatem użyć korelacji Pearsona, gdyż ma ona założenie mówiące o normalności rozkładu obu analizowanych zmiennych. Jako zamiennika użyję korelacji rang Spearmana. Czemu analiza korelacji rang Spearmana? Powody są dwa:
- Nie ma ona założenia dotyczącego normalności rozkładów.
- Nie jest ona wrażliwa na wartości odstające. Korelacja Pearsona jest wrażliwa na występowanie obserwacji odsatjących (musiałbym usuwać „outliery”, a przy tak małej próbie oznaczałoby to ubytek na poziomie ok. 20%. Nie mogę sobie na to pozwolić).
Analiza korelacji rang Spearmana
Przechodzę teraz do badania korelacji pomiędzy zmiennymi.
cor = pd.DataFrame(stats.spearmanr(df[["coeff", "b_h__last_year", "b_h__this_year", "s_and_p__last_year", "s_and_p__this_year"]])[0], columns = df[["coeff", "b_h__last_year", "b_h__this_year", "s_and_p__last_year", "s_and_p__this_year"]].columns, index=df[["coeff", "b_h__last_year", "b_h__this_year", "s_and_p__last_year", "s_and_p__this_year"]].columns)
coeff b_h__last_year b_h__this_year \ coeff 1.000000 0.382186 -0.137247 b_h__last_year 0.382186 1.000000 -0.047166 b_h__this_year -0.137247 -0.047166 1.000000 s_and_p__last_year 0.454656 0.541700 0.018421 s_and_p__this_year 0.140688 -0.001619 0.587247 s_and_p__last_year s_and_p__this_year coeff 0.454656 0.140688 b_h__last_year 0.541700 -0.001619 b_h__this_year 0.018421 0.587247 s_and_p__last_year 1.000000 0.050810 s_and_p__this_year 0.050810 1.000000
Widać relatywnie wysoką korelację pomiędzy współczynnikiem coeff i zmiennymi: b_h__last_year, s_and_p__last_year. Kolejne przesłanki wskazóją zatem na korelację sentymentu listów Buffeta z wynikami S&P 500 i BH z przeszłości.
Weryfikacja hipotez
By zweryfikować hipotezy posłużę się wartościami p-value z korelacji rang Spearmana. Każdorazowo, wartość p_value < alfa oznaczać będzie odrzucenie hipotezy zerowej i przyjęcie hipotezy alternatywnej H1. Jako alfa przyjmuję poziom 0.05. Buduję maciej wartości p-value dla badanych zmiennych.
p_values = pd.DataFrame(stats.spearmanr(df[["coeff", "b_h__last_year", "b_h__this_year", "s_and_p__last_year", "s_and_p__this_year"]])[1], columns = df[["coeff", "b_h__last_year", "b_h__this_year", "s_and_p__last_year", "s_and_p__this_year"]].columns, index=df[["coeff", "b_h__last_year", "b_h__this_year", "s_and_p__last_year", "s_and_p__this_year"]].columns)
coeff b_h__last_year b_h__this_year \ coeff 0.000000 0.016349 0.404747 b_h__last_year 0.016349 0.000000 0.775547 b_h__this_year 0.404747 0.775547 0.000000 s_and_p__last_year 0.003640 0.000369 0.911374 s_and_p__this_year 0.392954 0.992193 0.000085 s_and_p__last_year s_and_p__this_year coeff 0.003640 0.392954 b_h__last_year 0.000369 0.992193 b_h__this_year 0.911374 0.000085 s_and_p__last_year 0.000000 0.758705 s_and_p__this_year 0.758705 0.000000
Hipoteza 1
- H0: Nie ma korelacji pomiędzy sentymentem listów a wynikami indeksu S&P 500 z minionego roku.
- H1: Istnieje korelacja pomiędzy sentymentem listów a wynikami indeksu S&P 500 z minionego roku.
Z maksymalnym prawdopodobiestwem popełnienia błędu, równym 0.3% odrzucam H0 i przyjmuję H1. Istnieje korelacja pomiędzy sentymentem listów, a wynikami indeksu S&P 500 z minionego roku.
Hipoteza 2
- H0: Nie ma korelacji pomiędzy sentymentem listów a wynikami indeksu S&P 500 z danego roku.
- H1: Istnieje korelacja pomiędzy sentymentem listów a wynikami indeksu S&P 500 z danego roku.
Nie znalazłem dowodów na odrzucenie H0. Przyjmuję zatem, że nie ma korelacji pomiędzy sentymentem listów, a wynikami indeksu S&P 500 z danego roku.
Hipoteza 3
- H0: Nie ma korelacji pomiędzy sentymentem listów a wynikami giełdowymi Berkshire Hathaway z minionego roku.
- H1: Istnieje korelacja pomiędzy sentymentem listów a wynikami giełdowymi Berkshire Hathaway z minionego roku.
Z maksymalnym prawdopodobiestwem popełnienia błędu, równym 1.6% odrzucam H0 i przyjmuję H1. Istnieje korelacja pomiędzy sentymentem listów, a wynikami giełdowymi Berkshire Hathaway z minionego roku.
Hipoteza 4
Dzięki niej chciałbym odpowiedzieć na pytanie: czy sentyment listów jest skorelowany z wynikami, jakie osiągnął Berkshire Hathaway w kolejnym roku?
- H0: Nie istnieje korelacja pomiędzy sentymentem listów a wynikami giełdowymi Berkshire Hathaway z danego roku.
- H1: Istnieje korelacja pomiędzy sentymentem listów a wynikami giełdowymi Berkshire Hathaway z danego roku.
Nie znalazłem dowodów na odrzucenie H0. Przyjmuję zatem, że nie ma korelacji pomiędzy sentymentem listów, a wynikami giełdowymi Berkshire Hathaway z danego roku.
Podsumowanie
Na podstawie powyższych danych i przeprowadzonej analizy odrzuciłem obie zerowe hipotezy dotyczących przeszłości. Niestety nie udało się odrzucić żadnej z zerowych hipotez, które dotyczyły przyszłości. Gdyby tak się stało, to mógłbym w pewnym stopniu przewidywać zachowania amerykańskiej giełdy (lub BH) w roku 2018. Podstawą do tego byłby sentyment listu Warrena Buffetta, który zostanie opublikowany za około miesiąc. W moim przypadku mogę jedynie wnioskować w przeciwną stronę. Patrząc na wyniki osiągane zarówno przez S&P 500, jak i Berkshire Hathaway w 2017, należy się spodziewać, że sentyment najbliższego listu będzie bardzo pozytywny 🙂
–
Wszystkie pliki utworzone przy projekcie można znaleźć na moim GitHub’ie.
Link do analizy, która była inspiracją tego projektu: klik.
photo: artsfon.com
PODOBAŁ CI SIĘ TEN ARTYKUŁ?
Jeśli tak, to zarejestruj się, by otrzymywać informacje o nowych wpisach.
Dodatkowo w prezencie wyślę Ci bezpłatny poradnik :-)
Dodaj komentarz