Jak wczytać tabele z PDFów (przykład)?

We wcześniejszym wpisie opisałem narzędzie w którym bardzo łatwo można przygotować 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ło zapisanych 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 by 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 z 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. .

Natomiast podzielę się 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 pythonie.

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 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 🙂

.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *