Pandas read_html: Mehrere Textzeilen in einer Zelle

mit matplotlib, NumPy, pandas, SciPy, SymPy und weiteren mathematischen Programmbibliotheken.
Antworten
Freddy19911
User
Beiträge: 9
Registriert: Montag 23. August 2021, 11:37

Hallo,

vorweg: Ich bin erst vor kurzem in die Programmierung mit Python eingestiegen und mag deshalb noch recht unbeholfen wirken :roll:

Mit Hilfe von Pandas read_html Funktion möchte ich eine bestimmte Tabelle (nennt sich "Summary Compensation Table") aus einem bestimmten Formular (DEF 14A) der U.S. Börsenaufsichtsbehörde scrapen. Dieses Formular muss von börsennotierten Unternehmen regelmäßig ausgefüllt und die entsprechende Tabelle eingepflegt werden. Die genaue Form dieser Tabelle unterscheidet sich leider immer ein wenig.
Im folgenden Bild findet ihr ein Beispiel. Um den Aufbau der Tabellen ersichtlicher zu machen, findet ihr auf den Bildern zuerst eine "markierte" Version und zur besseren Lesbarkeit noch eine ohne Markierungen.
https://imgur.com/a/EnyGOLN

Wie man auf der markierten Version sieht, existieren in einigen Spalten mehrzeilige Texte. Als Resultat erhalte ich nach dem durchlaufen des Codes dann folgenden Frame:
https://imgur.com/a/ZUOiUBg

Nun würde ich natürlich gerne einen Zeilenumbruch innerhalb einer Zelle auch in meinem df darstellen, da die Informationen sonst natürlich ziemlich nutzlos sind. In der Spalte "Name and Principal Position" wird jede Textzeile auch in eine neue Tabellenzeile geschrieben. Hier funktioniert die Darstellung somit wie gewollt. In anderen Spalte wie "Year" oder "Salary" leider nicht.

Eine zweite Sache ist die der automatischen Erkennung der Überschriften. Diese funktioniert mal mehr mal weniger gut. In dem hier gezeigten Beispiel eher weniger, was vermutlich an der Multiindex Überschrift liegt.
Ich versuche mich hier gerade an einer Lösung der folgenden Logik: "Behandle über alle Spalten hinweg alles was über den ersten numerischen Wert in der Spalte Year steht als Überschrift". Leider waren meine bisherigen Versuche nicht wirklich von Erfolg gekrönt. Wenn jemand einen Vorschlag für eine Lösung hat wäre ich sehr dankbar.

Code: Alles auswählen

dfs = pd.read_html(resp.text) #Auswahl aller Tabellen auf der Seite

#Auswahl der korrekten Tabelle. Notiz: Geht bestimmt auch schöner/effizienter...
for df in dfs:
    df = df.applymap(str)
    for i in range(len(list(df))):
        if df.iloc[:,i].str.contains('Name').any():
            for k in range(len(list(df))):
                if df.iloc[:,k].str.contains('Year').any():
                    for f in range(len(list(df))):
                        if df.iloc[:,f].str.contains('Salary').any():
                            CompTab = df

#Anzeigeeinstellung
pd.set_option('display.max_columns', None)

#Aufbereitung der Tabelle#
#Versuch die Überschrift als solche zu erkennen
ind = CompTab[CompTab[0].str.contains('Name', na=False)].index[0] + 1
CompTab = CompTab[ind:].rename(columns={i: ' '.join(CompTab[i][:ind].dropna()) for i in CompTab.columns})

#Spalten/Zeilen entfernen, die nur aus nan bestehen
CompTab.replace('nan', np.nan, inplace=True)
CompTab.dropna(how='all', axis=1, inplace=True)
CompTab.dropna(how='all', axis=0, inplace=True)
CompTab.reset_index(drop=True, inplace=True)
Die Beispieltabelle findet ihr auch hier auf Seite 13: https://www.sec.gov/Archives/edgar/data ... def14a.htm

Liebe Grüße
Freddy19911
User
Beiträge: 9
Registriert: Montag 23. August 2021, 11:37

Edit: Hier eine angepasste Version des Codes, wo ich die Auswahl der Tabelle vereinfacht habe.

Code: Alles auswählen

dfs = pd.read_html(resp.text) #Auswahl aller Tabellen auf der Seite

#Auswahl der korrekten Tabelle
for df in dfs:
    try:
        if ('Name' in str(df)) and ('Years' in str(df)) and ('Salary' in str(df)):
            CompTab = df
        else:
            pass
    except:
        pass

#Anzeigeeinstellung
pd.set_option('display.max_columns', None)

#Aufbereitung der Tabelle#
#Versuch die Überschrift als solche zu erkennen
ind = CompTab[CompTab[0].str.contains('Name', na=False)].index[0] + 1
CompTab = CompTab[ind:].rename(columns={i: ' '.join(CompTab[i][:ind].dropna()) for i in CompTab.columns})

#Spalten/Zeilen entfernen, die nur aus nan bestehen
CompTab.replace('nan', np.nan, inplace=True)
CompTab.dropna(how='all', axis=1, inplace=True)
CompTab.dropna(how='all', axis=0, inplace=True)
CompTab.reset_index(drop=True, inplace=True)
Sirius3
User
Beiträge: 17741
Registriert: Sonntag 21. Oktober 2012, 17:20

Nackte excepts sollte man niemals verwenden. Welche Art Exception möchtest Du denn da abfangen?
In der String-Repräsentation eines Dataframes irgendwo einen Text zu suchen ist ziemlich schlecht. Weil der Text könnte ja irgendwo vorkommen. Du möchtest wahrscheinlich nur in den Überschriften suchen, also ein any('Name' in column for column in df.columns).
Ein `else: pass` macht nichts und kann weg.
Variablennamen schreibt man generell komplett klein. Benutze keine kryptischen Abkürzungen, was auch immer comp hier bedeuten mag.
Freddy19911
User
Beiträge: 9
Registriert: Montag 23. August 2021, 11:37

Danke für deine Anmerkungen.

Ich möchte den Code nachher mit einer Liste an Unternehmen füttern, für die dann die entsprechende Tabelle gescraped werden soll. Sollte es für ein Unternehmen mal doch nicht die Tabelle in dem Dokument geben, soll der Code weitergeführt und nicht abgebrochen werden. Daher habe ich an dieser Stelle mit except gearbeitet. Sorry für die Verwirrung.

Du hast recht, die Begriffe "Name", "Years" und "Salary" möchte ich wenn möglich nur in den Überschriften suchen. Das Problem ist jedoch, dass mein Code noch nicht in der Lage zu sein scheint die Überschriften der Tabelle im DataFrame auch korrekt als solche zu erkennen. Hier: https://imgur.com/a/ZUOiUBg ist das bspw. sehr schlecht gelungen und ein Teil der Überschrift findet sich als erster Tabelleneintrag wieder.
Das soll im besten Fall natürlich nicht so sein und ist den teilweise unterschiedlichen Layouts der Tabellen geschuldet. Leider habe ich noch keine Lösung finden können wie die Überschriften sauber erkannt werden, sodass ich auf die Methode zurückgreifen musste, in der die gesamte Tabelle nach diesen Begriffen durchsucht wird. Wenn hier noch jemand einen Vorschlag hat nehme ich den gerne entgegen. Ich dachte man könne sich die Tatsache zu nutzen machen, dass in jeder Tabelle in der Spalte "Year" der erste Eintrag in jedem Fall ein Jahr ist (s. zweite Frage des ursprünglichen Posts).
Benutzeravatar
__blackjack__
User
Beiträge: 13077
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Freddy19911: Ich denke die Tabellen sind nicht wirklich geeignet um mit Pandas eingelesen zu werden, weil da zu viel ”geschachteltes” dabei ist und viele Zellen mehr als ein Datum enthalten. Da wird man mehr Logik in das Parsen stecken müssen als Pandas das automagisch versucht auf einfache 2D-Strukturen anzuwenden, weil diese Tabelle(n) eben keine simplen 2D-Strukturen ohne verbundene Zellen und mit immer nur einem Datum pro Zelle sind.

Du hast die Ausnahmebehandlung dann aber an der falschen Stelle, denn Du möchtest ja nicht Fehler/Ausnahmen im Code der ein einzelnes Unternehmen bearbeitet ignorieren, sondern in dem Code der dann mehrere Unternehmen abarbeitet. Und da auch nicht ignorieren, sondern besser ausgeben und/oder in eine Datei protokollieren, inklusive dem kompletten Traceback, damit Du feststellen kannst bei welchen Unternehmen es ein Problem gab, und welches Problem das war, und wo genau im Code das aufgetreten ist. Denn man will dem dann ja in der Regel nachgehen und den Code entsprechend verbessern.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Freddy19911
User
Beiträge: 9
Registriert: Montag 23. August 2021, 11:37

@__blackjack__ Vielen Dank für deine Antwort, die war wirklich sehr hilfreich. Hältst du es für möglich ("sinnvoller") die Tabellen mit BeautifulSoup zu parsen? Ich arbeite parallel an einer Lösung unter der Verwendung von BS weil ich mir zu Beginn auch überhaupt nicht sicher war, ob pandas mit dieser Tabellenstruktur zurechtkommt.
August1328
User
Beiträge: 65
Registriert: Samstag 27. Februar 2021, 12:18

Hallo Freddy19911,

aus eigener Erfahrung empfehle ich die Text-Datei, die bei jedem SEC Filing vorhanden ist, zu parsen, entweder mit BeautifulSoup oder anderen verfügbaren Parsern (ich habe ein Beispiel gepostet, was ich nutze).

Ich habe ein trading Skript, was bei Bedarf Dateien vom Server der SEC einliest und nach bestimmten Informationen durchsucht. Das war nicht ganz einfach, weil das genutzte HTML Format echt altbacken ist und es keine einheitliche Struktur gibt.

Deshalb habe ich folgenden Ansatz gewählt, um an die gewünschten Infos zu kommen:
  • die Text Datei wird per requests.get() eingelesen
  • dann werden alle HTML Tags und unnötige Leerzeichen, Zeilenumbrüche etc. entfernt und der verbleibenden Text wird lowercase
  • danach wird per regex nach einem immer vorhandenen Textmuster am Anfang und Ende des Abschnittes gesucht, der die gewünschten Informationen enthält
  • dieser Text wird per regex und if / elif Abfragen mit unterschiedlichen Such-strings solange weiter zerlegt, bis die gewünschten Informationen in Variablen gespeichert sind
  • ich habe gerade mal in mein Skript geschaut, insgesamt nutzt das dazu 5x regex - finde ich zwar keine tolle Lösung, aber sie funktioniert
Ich habe mir die Datei hinter dem SEC Link oben mal kurz angesehen. Das ist jetzt nen Schnellschuss, aber Du könntest alles zwischen dem 2. "Summary Compensation Table" und "Stock Option Grants" einlesen und in einem nächsten Schritt die Fussnoten loswerden. Dann hast Du den Inhalt der Tabelle als plain text. Nun könntest Du gucken, ob es in allen DEF 14A ein Muster gibt, was man per Code auswerteb kann.
Antworten