Spalteneinträge in großen Datensätzen vergleichen & entsprechende Zeilen löschen

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
de_crank
User
Beiträge: 5
Registriert: Dienstag 22. August 2023, 15:10

Hallo,
ich bin bei der Bearbeitung eines großen Datensatzes auf ein Problem gestoßen. Es handelt sich hierbei um einen Datensatz aus Excel mit der Größe 40.000x60.
Der Code für meine Zwecke liegt bereits in VBA vor, jedoch dauert die Ausführung mehrere Stunden (verständlicherweise).

Ich würde dies nun gern in Python optimieren.
Erst einmal der Code in VBA:

Code: Alles auswählen

Sub Liste()
       
    Z = 3
    counter = 0
    With Workbooks("Beispiel.xlsx").Sheets(1)
    
        Do Until .Cells(Z, 2) = ""
            
            If .Cells(Z, 2) = .Cells(Z - 1, 2) And .Cells(Z, 7).Text = .Cells(Z - 1, 7).Text Then
                .Cells(Z, 11) = "x"
                .Cells(Z - 1, 11) = "x"
            End If
            
            Z = Z + 1
        Loop
        
    End With

End Sub
Info: Der Datensatz hat in der ersten Zeile einen Header.

Es wird praktisch jede Zeile mit 'X' markiert, bei der bei aufeinanderfolgenden Zeilen die Spalteneinträge in Spalte 2 und 7 jeweils gleich sind. Anschließend wird händisch jede Zeile gelöscht, die kein 'X' enthält. (War in VBA einfacher umzusetzen)
Hier möchte ich auch optimieren, sodass direkt jede Zeile gelöscht wird, wenn die Spalteninhalte aus Spalte 2 und 7 bei mind. 2 aufeinanderfolgenden Zeilen gleich sind.

Was ich bis jetzt gemacht habe:
Ich habe die Datei eingelesen und erst einmal alle Zeilen gelöscht, in denen in Spalte 7 kein Eintrag war.

Code: Alles auswählen

import pandas as pd

# Öffnen der Arbeitsmappe

df = pd.read_excel('Testpy.xlsx', engine='openpyxl')

df_mod = df.copy()
df_mod.dropna(subset=['EQUIPMENT_NUMBER'], inplace=True)
df_mod.reset_index(drop=True,inplace=True)
print(df_mod)
Ich hatte mich nun gefragt, welche Methoden die kürzeste Laufzeit haben. Eventuell ein Array mit Numpy erstellen?

Über jegliche Gedanken wäre ich sehr dankbar.
Benutzeravatar
__blackjack__
User
Beiträge: 13199
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@de_crank: Ich sehe an dem gezeigten VBA-Code nichts was da mehrere Stunden Laufzeit selbstverständlich machen würde, egal wie man die 40.000×60 Grössenangabe liest. Also auch wenn das 40k-Zeilen sind, dauert die gezeigte Unterroutine keine Stunden.

Statt das in Python zu optimieren, sollte der erste Schritt sein eine lauffähige Lösung zu bekommen. 2,4 Millionen Werte sind bei einer solche einfachen Aufgabe und heutigen Rechnern keine Grössenordnung wo der erste Gedanke ist, dass man da hochoptimierten Code schreiben muss. Das gilt auch für Rechner von gestern und vorgestern. Erster Schritt sollte verständlicher Code sein. Wenn das dann *messbar* zu langsam ist oder zuviel Speicher verbraucht, kann man optimieren. Und nicht irgendwas oder alles, sondern dort wo man gemessen am meisten gewinnen kann.

Einerseits ist das explizite kopieren der eingelesenen Daten nicht sinnvoll, anderseits sollte man von den impliziten Kopien bei der Bearbeitung keine Angst haben, also nicht alles immer ”inplace” erzwingen, nur weil man denkt es muss optimiert sein/werden.

Die Beschreibung vom VBA und der manuellen Bearbeitung weicht von der gewünschten Beschreibung und dem Code ab, der dann folgt. Sollen die Datensätze mit "X" nun gelöscht oder behalten werden?

Mein erster Gedanke bei Pandas in diesem Fall wäre `groupby()` und eventuell auf dem Ergebnis davon die `filter()`-Methode.

Beispieldaten sind bei solchen Fragen oft hilfreich. Sowohl die Eingabe, als auch was als Ausgabe erwartet wird. Die Daten können zwar verändert/vereinfacht sein, sollten aber alle relevanten Randfälle abdecken.
“There will always be things we wish to say in our programs that in all known languages can only be said poorly.” — Alan J. Perlis
de_crank
User
Beiträge: 5
Registriert: Dienstag 22. August 2023, 15:10

@__blackjack__: Danke für die Antwort und die Infos!

Der Ansatz mit dem 'X' ist lediglich eine Vereinfachung gewesen. Für mein Vorhaben gilt jedoch: Jegliche Datensätze, die auf die if-Anweisung zutreffen, sollen gelöscht werden.

Zum Thema 'inplace': Ohne inplace=True wurde mir leider nicht der gewollte Output angezeigt. Auskennen tu ich mich hier leider wenig. Ist das setzen von 'True' also unerheblich?

Zum Thema Beispiel-Dateien:
Meine Input-Datei: Wichtig: In Spalte 2 und Spalte 4 sind B5 und D6 gleich in zwei aufeinanderfolgenden Zeilen. Diese sind hier wichtig. Im Code kann ich sie leider nicht fett markieren.

Code: Alles auswählen

Spaltenkopf1	Spaltenkopf2	Spaltenkopf3	Spaltenkopf4
A1		B1		C1		D1
A2		B2		C2	
A3		B3		C3		D3
A4		B4		C4		D4
A5		B5		C5		D5
A6		B5		C6		D5
A7		B7		C7	
A8		B8		C8		D8
1. Schritt: Zeilen mit Leerzellen in Spalte 4 löschen, da sonst der nächste Schritt nicht klappt m.M.n., da auf die vorherige Zeile verwiesen wird, und diese keine Leerzelle sein darf, da sonst Vergleich mit Leerzelle.

Code: Alles auswählen

Spaltenkopf1	Spaltenkopf2	Spaltenkopf3	Spaltenkopf4
A1		B1		C1		D1
A3		B3		C3		D3
A4		B4		C4		D4
A5		B5		C5		D5
A6		B5		C6		D5
A8		B8		C8		D8
gewünschter Output:

Code: Alles auswählen

Spaltenkopf1	Spaltenkopf2	Spaltenkopf3	Spaltenkopf4
A5		B5		C5		D5
A6		B5		C6		D5
Es werden nur die Zeilen behalten, die in mind. zwei aufeinanderfolgenden Zeilen die selben Einträge in Spalte 2 und 4 (hier beispielsweise) haben.

Ich hoffe, es ist nun verständlicher!
Benutzeravatar
__blackjack__
User
Beiträge: 13199
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@de_crank: Natürlich kann man nicht einfach ``inplace=True`` weg lassen und den Rest des Codes so belassen. ``inplace=True`` ist aber auch ungewöhnlich, also frage ich mich wie das überhaupt zustande gekommen ist. Genauso das hier absolut sinnlose `copy()`. Warum schreibst Du das so? Was hast Du Dir davon erhofft, statt einfach den ”normalen” Weg zu gehen? Normal im Sinne von da wird etwas benutzt was in der Dokumentation im Benutzerhandbuch in den drei ersten Kapiteln die man mindestens durchgearbeitet haben sollte, nicht mal ansatzweise vorkommt.

Die Zeilen mit Leerstellen in Spalte 4 wird man mit `dropna()` los. Dann kann man sich die beiden Spalten die für das Filtern relevant sind als DataFrame geben lassen, und den mit sich selbst um eine Zeile verschoben (`shift()`-Methode) auf Ungleichheit testen lassen (``!=``-Operator oder `ne()`-Methode), mit `any()` über die Spalten diese zu einer zusammenfassen, und mit der laufenden Summe der Wahrheitswerte (`cumsum()`-Methode) die Gruppen von gleichen Einträgen mit einer ID versehen. Das kann man dann mit `groupby()` nach dieser ID gruppieren, und mit `filter()` die Gruppen nach Länge ≥2 filtern. Das Ergebnis davon kann man dann als Index für das `loc`-Attribut entweder in den originalen `DataFrame` benutzen, oder den ohne die Zeilen mit den Leerstellen in Spalte 4. Eigentlich ganz einfach. 😈

Das Vorgehen ist komplett anders als bei VisualBasic oder auch bei Python ohne Pandas. Wichtig ist, dass man keine ``for``-Schleife benötigt. Wenn Du das machst benutzt Du Pandas nicht beziehungsweise falsch.
“There will always be things we wish to say in our programs that in all known languages can only be said poorly.” — Alan J. Perlis
Benutzeravatar
Kebap
User
Beiträge: 695
Registriert: Dienstag 15. November 2011, 14:20
Wohnort: Dortmund

Hier mal eine Lösung in Excel ohne VBA.
Sagen wir also, deine Daten sind in Spalten A-D, und in Spalte E landet das X.
Dann schreibe mal bitte folgende Formel in Zelle E2, also unter die Überschriften:

Code: Alles auswählen

=Wenn(Oder(Und(B2=B1;D2=D1);Und(B2=B3;D2=D3));"X";"-")
Diese Zelle kopierst du dann nach unten, so dass die ganze Spalte E ausgefüllt wird.
Mich würde mal interessieren, wie lange es dauert, die ganzen Formeln zu berechnen.
Ich tippe auf vielleicht so 2-3 Sekunden, keine Stunden, bei 40.000 Zeilen eher weniger.
Ergebnis:

Code: Alles auswählen

S1   S2   S3   S4   ok?    
A1   B1   C1   D1   -    
A2   B2   C2        -    
A3   B3   C3   D3   -    
A4   B4   C4   D4   -    
A5   B5   C5   D5   X    
A6   B5   C6   D5   X    
A7   B7   C7        -    
A8   B8   C8   D8   -    
Das Filtern & Löschen der Zeilen ohne X musst du so weiterhin manuell durchführen.
MorgenGrauen: 1 Welt, 8 Rassen, 13 Gilden, >250 Abenteuer, >5000 Waffen & Rüstungen,
>7000 NPC, >16000 Räume, >200 freiwillige Programmierer, nur Text, viel Spaß, seit 1992.
Benutzeravatar
Kebap
User
Beiträge: 695
Registriert: Dienstag 15. November 2011, 14:20
Wohnort: Dortmund

de_crank hat geschrieben: Dienstag 22. August 2023, 15:39 Über jegliche Gedanken wäre ich sehr dankbar.
Konntest du schon etwas ausprobieren?
MorgenGrauen: 1 Welt, 8 Rassen, 13 Gilden, >250 Abenteuer, >5000 Waffen & Rüstungen,
>7000 NPC, >16000 Räume, >200 freiwillige Programmierer, nur Text, viel Spaß, seit 1992.
Benutzeravatar
__blackjack__
User
Beiträge: 13199
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Eine mögliche Lösung mit Pandas:

Code: Alles auswählen

#!/usr/bin/env python3
import io

import pandas as pd

SOURCE = """\
Spaltenkopf1,Spaltenkopf2,Spaltenkopf3,Spaltenkopf4
A1,B1,C1,D1
A2,B2,C2,
A3,B3,C3,D3
A4,B4,C4,D5
A5,B5,C5,D5
A6,B5,C6,D5
A7,B7,C7,
A8,B8,C8,D8
A9,B5,C9,D5
"""


def main():
    data = pd.read_csv(io.StringIO(SOURCE)).dropna(subset="Spaltenkopf4")
    print(
        "Nach einlesen & entfernen von Zeilen mit leeren Einträgen in Spalte 4:"
    )
    print()
    print(data)

    grouping_data = data[["Spaltenkopf2", "Spaltenkopf4"]]
    group_ids = (
        (grouping_data != grouping_data.shift()).any(axis="columns").cumsum()
    )
    result = data.loc[
        group_ids.groupby(group_ids)
        .filter(lambda group: len(group) >= 2)
        .index
    ]
    print("\nErgebnis:")
    print()
    print(result)


if __name__ == "__main__":
    main()
“There will always be things we wish to say in our programs that in all known languages can only be said poorly.” — Alan J. Perlis
Benutzeravatar
__blackjack__
User
Beiträge: 13199
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Oder das ganze ohne Pandas mit Python-Bordmitteln:

Code: Alles auswählen

#!/usr/bin/env python3
import csv
import io
from itertools import groupby
from operator import itemgetter
from pprint import pprint

SOURCE = """\
Spaltenkopf1,Spaltenkopf2,Spaltenkopf3,Spaltenkopf4
A1,B1,C1,D1
A2,B2,C2,
A3,B3,C3,D3
A4,B4,C4,D5
A5,B5,C5,D5
A6,B5,C6,D5
A7,B7,C7,
A8,B8,C8,D8
A9,B5,C9,D5
"""


def filter_rows(rows):
    for _, group in groupby(
        filter(itemgetter("Spaltenkopf4"), rows),
        itemgetter("Spaltenkopf2", "Spaltenkopf4"),
    ):
        group = list(group)
        if len(group) >= 2:
            yield from group


def main():
    pprint(list(filter_rows(csv.DictReader(io.StringIO(SOURCE)))))


if __name__ == "__main__":
    main()
“There will always be things we wish to say in our programs that in all known languages can only be said poorly.” — Alan J. Perlis
de_crank
User
Beiträge: 5
Registriert: Dienstag 22. August 2023, 15:10

@__blackjack__ @Kebap: Vielen Dank für eure Hilfe. Ich war im Urlaub, weshalb ich mich erst jetzt melden kann.

Beide Ansätze von euch funktionieren, danke dafür!

Da mir die Lösung mit pandas am besten gefallen hat und ich in diesem Gebiet mehr lernen möchte, habe ich mich an einer Ausweiterung des Codes gesetzt.
Mein Ziel war es, in dem 'result'-Dataset nach 2 weiteren Spalten zu filtern. Hierbei handelt es sich um Datums-Einträge mit dem Format JJJJ-MM-TT. Nennen wir Sie Spaltenkopf 5 & 6. Hier eine Bespieldatei:

Input:

Code: Alles auswählen

Spaltenkopf1	Spaltenkopf2	Spaltenkopf3	Spaltenkopf4	Spaltenkopf5  	 Spaltenkopf6
A5		B5		C5		D5		2010-12-06		2014-03-14		
A6		B5		C6		D5		2014-04-13		2017-10-01
A7		B5		C7		D5		2017-11-01
A8		B7		C8		D7		2014-01-06		2014-03-14
A9		B7		C9		D7		2016-04-13		2017-10-01
A10		B10		C10		D10		2017-11-01		2017-12-04
A11		B10		C11		D10		2010-12-06		2014-03-14
A12		B12		C12		D12		2014-04-13		2017-10-01
A13		B12		C13		D12		2019-11-01		2020-03-14
Output:

Code: Alles auswählen

Spaltenkopf1	Spaltenkopf2	Spaltenkopf3	Spaltenkopf4	Spaltenkopf5 	Spaltenkopf6
A5		B5		C5		D5		2010-12-06		2014-03-14		
A6		B5		C6		D5		2014-04-13		2017-10-01
A7		B5		C7		D5		2017-11-01
Im Prinzip sollen immer nur die Werte mit der selben group-ids nach folgenden Kriterien verglichen werden:
Im Falle des Outputs:
1. Kriterium: (2014-04-13) - (2014-03-14) <70
2. Kriterium (2014-03-14) - (2010-12-06) >900.
In diesem Schema soll verglichen werden, also dementsprechend dann auch (2017-11-01) - (2017-10-01).
Nur wenn diese Kriterien erfüllt sind, möchte ich die ganze Zeile beibehalten. Zur Info: die 70 und die 900 sollen Tage darstellen.

Mein bisheriger Code nach stundenlangem Rumprobieren führte leider zu keinem richtigem Ergebnis, weil ich die eben genannten Kriterien nicht richtig formulieren kann.
Kann man hier wieder die groupby() und shift()-Funktion anwenden?

Zudem führten die Leerzellen in Spalte6 zu Problemen. Ich hätte die Leerzellen gerne zu 0 gesetzt, was jedoch nicht möglich ist, da es das Datumsformat haben muss. Hierbei ergibt sich leider wiederum das Problem, dass man 0000-00-00 nicht verwenden kann.

Mein bisheriger Code:

Code: Alles auswählen

import pandas as pd

def main():
    data = pd.read_excel('C:\\Users\\Flo\\Documents\\MITNEHMEN2.0\\Uni\\Python\\TEST JOB/Test.xlsx').dropna(subset="Baum7")
       
   
    fill_value = pd.to_datetime('2001-01-01', format='%Y-%m-%d')
    data['Baum4'].fillna(fill_value, inplace=True)
    
    data['Baum3'] = pd.to_datetime(data['Baum3'], format='%Y-%m-%d')
    data['Baum4'] = pd.to_datetime(data['Baum4'], format='%Y-%m-%d')
    
    
    grouping_data = data[["Baum2", "Baum7"]]
    group_ids = (
        (grouping_data != grouping_data.shift()).any(axis="columns").cumsum()
    )
    
    result = data.loc[
        group_ids.groupby(group_ids)
        .filter(lambda group: len(group) >= 2)
        .index   
    ]   
    
     
    # Zusätzliche Bedingungen
    condition1 = ((data['Baum3'].shift() - data['Baum4'])).dt.days < 70
    condition2 = ((data['Baum4'] - data['Baum3'])).dt.days > 900
    
    # Ersetzen von NaN-Werten in den Bedingungen
    condition1 = condition1.fillna(False)
    condition2 = condition2.fillna(False)

    combined_condition = condition1 & condition2
    
    
    # Filtern der Gruppen-IDs basierend auf den Bedingungen
    
    filtered_group_ids = group_ids[group_ids.map(lambda x: x if x in combined_condition.index else False)]
    #filtered_group_ids = group_ids[group_ids.map(combined_condition)]
    
    # Verwenden der gefilterten Gruppen-IDs, um das Ergebnis zu erstellen  
    
    result = data.loc[data.groupby(group_ids).filter(lambda group: group.name in filtered_group_ids.index).index]
    print("\nErgebnis:")
    print()
    print(result)

if __name__ == "__main__":
    main()
de_crank
User
Beiträge: 5
Registriert: Dienstag 22. August 2023, 15:10

Update: Baum3 soll Spaltenkopf5 darstellen und Baum4 soll Spaltenkopf6 darstellen, entschuldigt die Verwirrung!
Antworten