We wcześniejszym wpisie opisałem narzędzie, z pomocą którego bardzo łatwo można narysować dynamiczne wykresy. Natomiast jak to często bywa dane trzeba przygotować, co już nie było takie proste. Dlaczego? Ponieważ w przypadku źródła, które wybrałem, większość danych była zapisana w formacie PDF.
Zacznijmy od początku, czyli gdzie znaleźć dane o kapitalizacji spółek.
Uwaga. Mam świadomość, że poniższy kod w Python można napisać o wiele lepiej. Przygotowałem go w taki sposób, jak umiałem, aby wykonał to, czego oczekiwałem. Natomiast jeśli wiesz, co mógłbym poprawić i chciałbyś się tą wiedzą podzielić, to daj mi znać 🙂
Dane pobrałem bezpośrednio ze strony Giełdy Papierów Wartościowych (GPW). Znajdują się tam miesięczne zestawienia. Pomyślałem super! To będzie szybka akcja.
Niestety… życie jest pełne niespodzianek. A jeśli chodzi o dane, to tych niespodzianek jest chyba ze sto razy więcej.
Pobranie danych z raportów EXCEL
Po szybkim rzucie oka na pliki okazało się, że dane w bardzo przyjaznym formacie Excelowym są dopiero od 2014. Wcześniejsze dane są w plikach PDF. Arkusze Excelowe bardzo łatwo się obrabia. Zatem zacznijmy od czegoś prostego.
Tutaj przykład, jak znając adres internetowy, można pobrać arkusz i wczytać konkretną zakładkę:
import pandas as pd
#przykład jak łatwo się dobrać do danych
url="https://www.gpw.pl/pub/GPW/statystyki/statystyki_miesieczne/201401_GPW.xls"
tmp=pd.read_excel(url, sheet_name='kap')
tmp.head()
To teraz pobierzmy całość pętlą. Dodatkowo podczas pobierania zauważyłem, że od 2018 roku doszła dodatkowa kolumna. Uwzględniłem to w poniższym wczytaniu dodając odpowiedni warunek.
## DANE dostępne od 2014.01 skąd pobieramy zakładkę z kapitałem.
df_part1 = []
for rok in list(range(2014, 2020)):
for miesiac in list(range(1, 13)):
if rok == 2019 and miesiac == 8: break
tmp_name = str(rok) + str('0'+str(miesiac))[-2:] + '_GPW.xls'
url='https://www.gpw.pl/pub/GPW/statystyki/statystyki_miesieczne/' + tmp_name
if miesiac ==1: print(url)
tmp_xls = pd.read_excel(url, sheet_name='kap')
tmp_xls = tmp_xls.iloc[6:,:]
if tmp_xls.shape[1] == 10: #od 2018 doszła jedna kolumna więcej, dlatego
#rozszerzamy zakres by można było połączyć dane
tmp_xls = pd.concat([tmp_xls.iloc[:,0], tmp_xls.iloc[:,1], tmp_xls.iloc[:,1:]], axis = 1)
tmp_xls['okres'] = str(rok) + '-' + str('0'+str(miesiac))[-2:] #dodajemy rok.miesiac
if len(df_part1) == 0:
df_part1 = tmp_xls
else:
df_part1.columns = tmp_xls.columns #by nazwy kolumn były takie same i nie było błędów
df_part1 = pd.concat([df_part1, tmp_xls])
df_part1.columns = ['lp','id','nazwa','wielkosc_emisji','mln_PLN','mln_EUR','%','wartosc_ksiegowa','C/Z','C/WK','stopa_dywidendy','okres']
file1 = df_part1[['nazwa','okres','mln_EUR']].drop_duplicates(subset=['nazwa','okres','mln_EUR'], keep=False)
file1.head()
Mamy pierwszą cześć. Na końcu wystarczy przygotować odpowiedni format wykorzystując funkcję PIVOT. Ale zrobimy to już dla całej historii.
PDF, i co teraz?
Dane miesięczne od 2002 do 2014 są zapisane w plikach PDF.
Podzielę się z Wami pewną radą: na jakąkolwiek przeszkodę traficie, na pewno ktoś też już taką miał i szukał odpowiedzi. W ten sposób natrafiłem na bibliotekę tabula, która pomaga wyodrębnić tabelę z pliku w formacie PDF do DataFrame w Python.
Zanim go użyjemy na początku należy pobrać wszystkie PDF’y.
import requests
for rok in list(range(2002, 2014)):
for miesiac in list(range(1, 13)):
tmp_name = str(rok) + str('0'+str(miesiac))[-2:] + '_GPW.pdf'
url = 'https://www.gpw.pl/pub/GPW/statystyki/statystyki_miesieczne/' + tmp_name
if miesiac == 1: print(url)
myfile = requests.get(url, allow_redirects=True)
open('data_raw/'+tmp_name, 'wb').write(myfile.content)
Przyglądając się pobranym plikom można zauważyć, że zmienił się ich format w 2008. Wobec tego wczytamy pliki na dwa razy: od 2002 do 2007 oraz od 2008 do 2013.
from tabula import read_pdf
import tabula
tabula.__version__
Wykorzystałem tabula w wersji 1.3.1.
df_part2 = []
for rok in list(range(2002, 2008)):
for miesiac in list(range(1, 13)):
path = r'data_raw\\' + str(rok) + str('0'+str(miesiac))[-2:] + '_GPW.pdf'
print(path)
for i in range(100):
tmp_pdf_page = read_pdf(path, pages = i, multiple_tables = False)
if 'kapita' in str(tmp_pdf_page.columns).lower():
print('ok: ' + str(i) + ' ' + str(tmp_pdf_page.columns))
tmp_pdf_page['okres'] = str(rok) + '-' + str('0'+str(miesiac))[-2:] #dodajemy rok.miesiac
if len(df_part2) == 0:
df_part2 = tmp_pdf_page
else:
df_part2 = pd.concat([df_part2, tmp_pdf_page])
break
Niestety dane nie zostały idealnie wczytane, zatem należy je ręcznie poprawić.
df_part2 = pd.read_csv('data_part2.csv')
# zauwazyłem w danych, że dla okresu 2006.09 - 2007.06 są w kolumnie na koniec miesiąca
df_part2['Kapitalizacja na koniec okresu (mln)'] = df_part2['Kapitalizacja na koniec okresu (mln)'].fillna(df_part2['Kapitalizacja na koniec miesiąca (mln)'])
df_part2 = df_part2.fillna(-1)
#wywalamy nadmiarowe rzeczy
df_part2 = df_part2[(df_part2['Akcje'] != -1) & (df_part2['Akcje'] != 'Shares')]
df_part2.head()
Coś tutaj nie pasuje… Spójrzmy jak to wygląda w orginalnym raporcie.
Widać, że pozostały kolumny połączone w jedną. Wobec tego należy znaleźć sposób, aby je rozdzielić. Kilka prób i poniżej przykładowe rozwiązanie:
def usun_za_ostatnia_spacja(mystring):
mystring = str(mystring)
spaces = re.findall('\s+', mystring)
if len(spaces)>0:
s = mystring[::-1]
m = re.search(' ', s)
pos = len(s) - m.start(0)
new_mystring = mystring[:pos-1]
else:
new_mystring = ''
return new_mystring
def podzial_liczb(mystring):
mystring = str(mystring)
spaces = re.findall('\s+', mystring)
new_mystring = ''
if len(spaces)==1:
new_mystring = mystring
if len(spaces)==2: #usuń pierwszą
m = re.search(' ', mystring)
pos = m.start()
new_mystring = mystring[:pos] + mystring[pos+1:]
if len(spaces)==3: #usuń pierwszą i trzecią
m = re.search(' ', mystring)
pos = m.start()
new_mystring = mystring[:pos] + mystring[pos+1:]
s = new_mystring[::-1]
m = re.search(' ', s)
pos = m.start(0)
new_mystring = s[:pos] + s[pos+1:]
new_mystring = new_mystring[::-1]
return new_mystring
def cleanData_part2(text):
final_text = []
for w in text:
w = usun_za_ostatnia_spacja(w)
w = podzial_liczb(w)
final_text.append(w)
return pd.Series(final_text, index=text.index)
Mamy pomocnicze funkcje, więc teraz je wykorzystajmy do oczyszczenia danych.
text_raw = df_part2['Kapitalizacja na koniec okresu (mln)']
df_part2['kapitalizacja_tmp'] = cleanData_part2(text_raw)
df_part2[['mln_PLN','mln_EUR']] = df_part2['kapitalizacja_tmp'].str.split(' ',expand=True)
df_part2['Akcje'] = df_part2['Akcje'].str.replace('\d+', '') #pozbycie się cyfr
file2 = df_part2[['Akcje','okres','mln_EUR']].drop_duplicates(subset=['Akcje','okres','mln_EUR'], keep=False)
file2.columns = ['nazwa','okres','mln_EUR']
file2.head()
Huraa… mamy. Zatem bierzemy się za ostatnią część z danymi.
df_part3 = []
for rok in list(range(2008, 2014)):
for miesiac in list(range(1, 13)):
path = r'data_raw\\' + str(rok) + str('0'+str(miesiac))[-2:] + '_GPW.pdf'
print(path)
for i in range(100):
tmp_pdf_page = read_pdf(path, pages = i, multiple_tables = False)
if 'kapita' in str(tmp_pdf_page.columns).lower():
print('ok: ' + str(i) + ' ' + str(tmp_pdf_page.columns))
tmp_pdf_page['okres'] = str(rok) + '-' + str('0'+str(miesiac))[-2:] #dodajemy rok.miesiac
if len(df_part3) == 0:
df_part3 = tmp_pdf_page
else:
df_part3 = pd.concat([df_part3, tmp_pdf_page])
break
Raport odrobinę się różni od poprzedniego, więc należy ponownie znaleźć sposób jak oczyścić dane i przygotować poprawne kolumny. Wykorzystamy podobny schemat, tylko musimy dodać kilka funkcji pomocniczych.
def usun_za_pierwszym_przecinkiem(mystring):
mystring = str(mystring)
comar = re.findall('\,+', mystring)
if len(comar)>0:
m = re.search(',', mystring)
new_mystring = mystring[m.start(0)+1:]
else:
new_mystring = ''
return new_mystring
def zostaw_za_pierwszym_przecinkiem(mystring):
mystring = str(mystring)
comar = re.findall('\,+', mystring)
if len(comar)>0:
m = re.search(',', mystring)
new_mystring = mystring[:m.start(0)]
else:
new_mystring = ''
return new_mystring
def usun_przed_pierwsza_spacja(mystring):
mystring = str(mystring)
spaces = re.findall('\s+', mystring)
if len(spaces)>0:
m = re.search(' ', mystring)
new_mystring = mystring[m.start(0)+1:]
else:
new_mystring = ''
return new_mystring
def cleanData_part3(text):
final_text = []
for w in text:
w = usun_za_pierwszym_przecinkiem(w)
w = usun_przed_pierwsza_spacja(w)
w = zostaw_za_pierwszym_przecinkiem(w)
w = usun_za_ostatnia_spacja(w)
w = podzial_liczb(w)
final_text.append(w)
return pd.Series(final_text, index=text.index)
Teraz należy tylko wykorzystać funkcje analogicznie jak wcześniej:
df_part3 = pd.read_csv('data_part3.csv', names = ['a','b','kapital','d','e','f','g','okres'])
#kilka kolumn zostało źle przypisanych
df_part3['kapital'] = df_part3['kapital'].fillna(df_part3['b'])
#zostawiamy dwie kolumny
df_part3 = df_part3[['kapital','okres']]
#usuwamy wszystko co nie zawiera cyfr (jakieś opisy kolumn się tak zczytały)
df_part3 = df_part3[df_part3['kapital'].str.contains(r'[0-9]')]
df_part3['nazwa'] = df_part3['kapital'].str.replace('[^a-zA-Z]', '')
text_raw = df_part3['kapital']
df_part3['kapitalizacja'] = cleanData_part3(text_raw)
df_part3[['mln_PLN','mln_EUR']] = df_part3['kapitalizacja'].str.split(' ',expand=True)
file3 = df_part3[['nazwa','okres','mln_EUR']].drop_duplicates(subset=['nazwa','okres','mln_EUR'], keep=False)
file3.head()
Ostateczne przygotowanie tabeli
Ostatnim krokiem jest połączenie wcześniej przygotowanych danych, oczyszczenie ich z niepotrzebnych informacji i stworzenie tabeli w odpowiedniej strukturze.
Przyjrzałem się danym na wykresie i ręcznie poagregowałem firmy notowane na GPW, przypisując im odpowiednią kategorię (np. połączyłem banki w jedną kategorię).
file = pd.concat([file1, file2, file3], axis = 0)
file['nazwa'] = file['nazwa'].str.strip()
file= file.pivot(index='nazwa', columns='okres', values='mln_EUR').fillna(0)
file.reset_index(inplace=True)
banki = ['ALIOR','GETINOBLE','PEKAO','PKOBP','SANPL','BZWBK','INGBSK','MBANK',
'MILLENNIUM','BGZBNP','HANDLOWY','KREDYTB','BANKBPH','BRE','BNPPPL','GETIN']
telekomunikacja = ['CYFRPLSAT','ORANGEPL','NETIA','PLAY','TPSA']
paliwa_gaz = ['PKNORLEN','PGNIG','LOTOS']
ubezpieczenia = ['PZU','WARTA']
energetyka = ['ENEA','PGE','TAURONPE','ENERGA']
gornictwo = ['KGHM','JSW','TAURONPE']
odziez_obuwie = ['LPP','CCC']
hutnictwo = ['KETY','BORYSZEW','STALPROD']
gry = ['CDPROJEKT']
napoje = ['ZYWIEC']
artykuly_codzienne = ['DINOPL','EUROCASH']
file['kategoria'] = file['nazwa'].apply(lambda x: 'banki' if x in banki
else ('telekomunikacja' if x in telekomunikacja
else ('paliwa i gaz' if x in paliwa_gaz
else ('ubezpieczenia' if x in ubezpieczenia
else ('energetyka' if x in energetyka
else ('górnictwo' if x in gornictwo
else ('odzież i obuwie' if x in odziez_obuwie
else ('hutnictwo' if x in hutnictwo
else ('gry' if x in gry
else ('napoje' if x in napoje
else ('spożywcze' if x in artykuly_codzienne
else 'inny')))))))))))
file = file[file['nazwa'] != 'BZWBK2']
cols = ['nazwa','kategoria'] + [col for col in file if col not in ['nazwa','kategoria']]
file = file[cols]
file.to_csv('last_file.csv',index=False)
file.head()
Pełny kod dostępny na GitHub
Gdybyście potrzebowali pełny kod w formie Notebook Jupyter’a, to udostępniam go na moim GitHubie.
Podsumowanie
Mam nadzieję, że przyda Wam się przykład obrabiania danych z PDFów.
Życzę zero problemów przy obrabianiu danych PDF 🙂