Seite 1 von 1

Skript optimieren

Verfasst: Dienstag 24. März 2020, 13:29
von zyic
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)

Re: Skript optimieren

Verfasst: Dienstag 24. März 2020, 13:44
von Sirius3
`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)

Re: Skript optimieren

Verfasst: Dienstag 24. März 2020, 13:46
von Jankie

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?

Re: Skript optimieren

Verfasst: Dienstag 24. März 2020, 13:53
von Sirius3
@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.

Re: Skript optimieren

Verfasst: Dienstag 24. März 2020, 14:09
von einfachTobi
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.

Re: Skript optimieren

Verfasst: Dienstag 24. März 2020, 14:29
von zyic
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.

Re: Skript optimieren

Verfasst: Dienstag 24. März 2020, 14:54
von Sirius3
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?

Re: Skript optimieren

Verfasst: Dienstag 24. März 2020, 15:14
von zyic
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.

Re: Skript optimieren

Verfasst: Mittwoch 25. März 2020, 10:09
von zyic
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!

Re: Skript optimieren

Verfasst: Mittwoch 25. März 2020, 11:52
von zyic
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)

Re: Skript optimieren

Verfasst: Mittwoch 25. März 2020, 14:31
von einfachTobi
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