Skript optimieren

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
zyic
User
Beiträge: 5
Registriert: Dienstag 24. März 2020, 13:21

Hallo zusammen,

nach einiger Recherche und Leserei habe ich nun ein Skript, das Messdaten verschiedener Proben einliest und am Ende in einer Datei speichert. Dazu werden Dateien eines bestimmten Typs bzw. Namens bis zur zweiten Verzeichnisebene eingelesen, geöffnet und dann als pandas.DataFrame abgespeichert. Das funktioniert auch wunderbar. Allerdings habe ich den Eindruck, dass das Skript mit zunehmender Laufzeit langsamer wird und wenn ich so in den Taskmanager schaue, scheint die Festplatte viel mit dem Lesen unnötiger Dateien beschäftigt zu sein.

Hätte da vielleicht jemand einen Tipp, worauf ich schauen könnte? Ich habe schon gelesen, dass pandas effizienter ist, wenn man die Daten erst in einer list bzw. dict einliest und erst am Ende gesammelt in einen DataFrame schreibt.

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
import os
import pandas as pd
cwd = os.getcwd()
data_csv = 'daten.csv'
tier = 0
for _, dirs, files in os.walk(cwd):
    tier += 1
    if tier == 1:
        break

reads = 0
for dir in dirs:
    for file in os.listdir(os.path.realpath(dir)):
        if 'EIC' in file or 'BPC' in file:
            print('Ordner ' + dir[0:2] + ': ' + file[9:] + ' (Datei ' + str(reads) + ')')

            chromatogram = dir[:2] + file[22:31]
            sample = 'sample ' + dir[:2]
            sample_id = dir[-7:-2]
            data = {'time': [], sample: [], chromatogram: []}

            with open(dir + os.sep + file) as _file:
                for line in _file:
                    time, cps = line.split()
                    if reads < 1:
                        data['time'].append(float(time)/60)
                        data[sample].append(int(sample_id))
                        data[chromatogram].append(int(cps))
                    else:
                        data[sample].append(int(sample_id))
                        data[chromatogram].append(int(cps))

            if reads < 1:
                df = pd.DataFrame()
                df['time'] = pd.Series(data['time'][:2680])
                df[sample] = pd.Series(data[sample][:2680])
                df[chromatogram] = pd.Series(data[chromatogram][:2680])
            else:
                df = pd.read_csv(data_csv)
                df[sample] = pd.Series(data[sample][:2680])
                df[chromatogram] = pd.Series(data[chromatogram][:2680])
            df.to_csv(data_csv, index = False)
            reads += 1
print(reads)
Sirius3
User
Beiträge: 18272
Registriert: Sonntag 21. Oktober 2012, 17:20

`cwd` ist das aktuelle Verzeichnis. Das muß man nicht angeben. Statt Dir was mit os.walk und os.listdir zusammenzubasteln, und dann auch noch Pfade mit + zusammenzuketten, solltest Du Dir pathlib.Path anschauen.
Das kann mit seiner `Path().glob("*/*") auch alle Dateien in exakt der zweiten Verzeichnisebene auflisten.
Wie sind denn die Dateinamen aufgebaut? Diese magische Indexschieberei sieht nicht sehr robust aus.
Variablennamen mit führendem Unterstrich zeigen an, dass die Variable zwar syntaktisch da stehen muß, mit ihrem Wert aber nichts mehr gemacht wird. Bei `_file` ist das aber nicht der Fall.
Pandas hat Funktionen, um Daten zu lesen. Das muß man nicht händisch programmieren.

Und ja, Du hast Recht, da wird viel unnötig gelesen und geschrieben. Nämlich in jedem Schleifendurchlauf die kompletten bisherigen Daten.
Laß das doch einfach sein.

Ein erster Schritt wäre das hier (ungetestet):

Code: Alles auswählen

from pathlib import Path
import pandas as pd


filtered_paths = (p for p in Path().glob("*/*") if "EIC" in p.name or "BPC" in p.name)

data = {'time': []}
for index, path in enumerate(filtered_paths):
    dir = path.parents[0]
    filename = path.name
    print(f"Ordner {dir[0:2]}: {filename[9:]} (Datei {index})")

    chromatogram = dir[:2] + filename[22:31]
    sample = 'sample ' + dir[:2]
    sample_id = dir[-7:-2]
    data[sample] = []
    date[chromatogram] = []
    with path.open() as lines:
        for line in lines:
            time, cps = line.split()
            if index == 0:
                data['time'].append(float(time)/60)
            data[sample].append(int(sample_id))
            data[chromatogram].append(int(cps))

df = pd.DataFrame(data)
df.to_csv(data_csv, index=False)
Zuletzt geändert von Sirius3 am Dienstag 24. März 2020, 13:55, insgesamt 5-mal geändert.
Jankie
User
Beiträge: 592
Registriert: Mittwoch 26. September 2018, 14:06

Code: Alles auswählen

tier = 0
for _, dirs, files in os.walk(cwd):
    tier += 1
    if tier == 1:
        break
Wieso benutzt du eine Schleife, die du nach dem ersten Durchlauf abbrichst und die sonst nichts anderes macht?
Sirius3
User
Beiträge: 18272
Registriert: Sonntag 21. Oktober 2012, 17:20

@Jankie: diese Schleife ist dazu da `dirs` zu ermitteln.
Hätte man auch als

Code: Alles auswählen

_, dirs, _ = next(os.walk('.'))
schreiben können.
einfachTobi
User
Beiträge: 512
Registriert: Mittwoch 13. November 2019, 08:38

Wenn du uns die Ordner- und Dateistruktur (und Namen, da du ja irgendwie danach filterst) sowie die Datenstruktur verrätst, können wir sicher noch mehr helfen. Denn wie die Vorredner schon sagen, werden einige Sachen zu viel gemacht. Genaue Anpassungen sind aber nur mit den genannten Infos möglich.
zyic
User
Beiträge: 5
Registriert: Dienstag 24. März 2020, 13:21

Das sind schon sehr hilfreiche Vorschläge, vielen Dank! Ich bin noch nicht so fit damit.

Die Dateien liegen als Textdateien "exported chromatogram BPC 100.000+0.02 -.xy" in einem Verzeichnis das mit zwei Dezimalstellen beginnt und mit der Probennummer endet: Also z. B. "01_Text_12345.d". Die Namen werden durch das Programm, dass die Daten exportiert, vorgegeben. Um das später in matplotlib zuordnen zu können, braucht man die Info welche Daten (z. B. BPC 100.000) und die Probe 12345. Da die Probennummer etwas lang zum Eintippen ist, dienen die zwei führenden Dezimalstellen im Verzeichnisnamen als temporäre Proben-Id um die Daten anzusprechen. Also bisher reicht dann z. B. 01BPC 100 um den Daten "BPC 100.000+0.02 -" der Probe "01" bzw. "12345" anzusprechen.
Sirius3
User
Beiträge: 18272
Registriert: Sonntag 21. Oktober 2012, 17:20

Und was machst Du wenn sich an der Anzahl der Ziffern etwas ändert?
Bei `chromatogram` werden die letzten zwei Ziffern von 100.000 abgeschnitten. Das sieht nicht gewollt aus.

Beim Verzeichnis würde das ja noch recht einfach gehen:

Code: Alles auswählen

sample, _, sample_id = dir.split('_')
Aber beim Dateinamen sehe ich gerade keine Systematik.

Hast du irgendeine Möglichkeit, was an den Namen etwas zu ändern?
zyic
User
Beiträge: 5
Registriert: Dienstag 24. März 2020, 13:21

Die Daten werden über ein Skript im Programm des Herstellers generiert, welches ich zwar anschauen und unwichtige Sachen wie den Text "exported chromatogram" im Dateinnamen ändern kann, die zugrundeliegenden Funktionen sehe ich aber nicht. Sieht nicht nach Python aus.

Die Anzahl der Ziffern vor dem Komma variiert zwischen 3 und 4. Die erste Nachkommastelle ist nur manchmal relevant, wenn die Ziffern zweier Datensätze vor dem Komma identisch sind. Zum Plotten ist es natürlich einfacher, wenn man nicht die ganze Zahl angeben muss, um den Datensatz anzusprechen - daher wären die Stellen vor dem Komma, bis auf den genannten Sondernfall, ausreichend.
zyic
User
Beiträge: 5
Registriert: Dienstag 24. März 2020, 13:21

So nochmal ein Nachtrag: Dank des Vorschlags von Sirius3 geht das Ganze nun deutlich schneller. Interessanterweise quittierte Windows path.parents und path.name mit einem subscription-Error, Linux mochte dafür (f" ") als Formatierung nicht. Als string formatiert klappt es mit path.parents und path.name jetzt wunderbar.

Allerdings hängt es jetzt wieder daran, dass die arrays am Ende unterschiedlich lang sind. Das war wohl demnach der Grund warum der ursprüngliche Autor des Skripts die Werte bei 2680 Einträgen abgeschnitten hatte. Ich dachte, die Zeit könnte man als Index benutzen, weil das angeblich das Längenproblem umgehen sollte, aber das klappt auch wieder aufgrund der Länge nicht. Da muss ich wohl noch etwas rumprobieren.

Aber erst mal danke für die Vorschläge!
zyic
User
Beiträge: 5
Registriert: Dienstag 24. März 2020, 13:21

Noch ein update: Mit einer Schleife direkt vor dem DataFrame lassen sich die arrays auf die gleiche Länge bringen. Und der Geschwindigkeitsunterschied ist wirklich enorm! :-)

Code: Alles auswählen

# Daten auf gleiche Länge bringen
for key, value in data.items():
    data[key] = value[:2680]

# Liste in pandas-DataFrame speichern und als csv schreiben
df = pd.DataFrame(data)
df.to_csv(data_csv, index = False)
einfachTobi
User
Beiträge: 512
Registriert: Mittwoch 13. November 2019, 08:38

Noch etwas schneller dürftest du sein, wenn du das DataFrame direkt aus dem Dictionary erstellst und dann die Werte auffüllst, statt vorher einmal durch Dict zu laufen:

Code: Alles auswählen

df = pd.DataFrame.from_dict(data, orient='index').T
df = df.fillna(value= )  # hier Wert einsetzen oder nicht `value=` sondern `method='pad'`verwenden um zB nach hinten aufzufüllen
Mehr zu DataFrame.fillna(): https://pandas.pydata.org/pandas-docs/s ... ame.fillna
Antworten