Schreiben großer ASCII-Dateien

mit matplotlib, NumPy, pandas, SciPy, SymPy und weiteren mathematischen Programmbibliotheken.
Antworten
schneitzmaster
User
Beiträge: 94
Registriert: Freitag 26. Oktober 2012, 15:35
Wohnort: Hamburg

Hi Leute,

ich habe einen sehr großen numpy Array und möchte diesen in eine Text-Datei schreiben. Leider dauert das sehr lang.
Ich habe bereits zwei Varianten ausprobiert und leider festgestellt das numpy.savetxt sogar fast doppelt so viel Zeit benötigt wie die "normale" list comprehension mit write().
Gibt es vielleicht eine Möglichkeit das Ganze noch schneller zu machen?
Hier mein Beispiel Code:

Code: Alles auswählen

import numpy as np
import time

nodes = np.random.rand(1000000,3)

start = time.time()
f = open('test1.txt', mode='w')
[f.write( '%d, %16.14F, %16.14F, %16.14F \n' % (i+1, node_i[0], node_i[1], node_i[2]) ) for i,node_i in enumerate(nodes)]
f.close()
print time.time()-start

start = time.time()
f = open('test1.txt', mode='w')
idx_nodes = np.column_stack( (np.arange(1,len(nodes)+1),nodes) )
np.savetxt(f, idx_nodes, fmt='%d, %16.14F, %16.14F, %16.14F', delimiter=', ', newline='\n')
f.close()
print time.time()-start
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Ich hoffe, Dir wurde schon einmal gesagt, dass man Dateien immer mit ``with`` öffnen sollte:

Code: Alles auswählen

with open(...) as f:
    # f ist in diesem Block Dein File Objekt
    # es wird *automatisch* geschlossen; auch bei Exception!
Was macht denn Zeile 14? Erzeug das einen Generator oder wird da wirklich eine große Struktur erzeugt? Bei letzterem hast Du schon die Erklärung, wieso die untere Variante länger dauert: Die Struktur muss ja dort zweimal durchlaufen werden. Ich würde die Zeile mal vor die Messung packen.

Für präzise Messungen gibt es übirgens das ``timeit`` Modul. Aber wenn wir hier von mehreren Sekunden reden, passt das auch so.
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
BlackJack

@schneitzmaster: Die „list comprehension” (LC) würde ich an der Stelle gleich wieder vergessen. Diese Ausdrücke sind dazu da um Listen zu erstellen. Du erstellst dort völlig ohne Grund eine Liste mit einer Million `None`-Werten und verwirfst die dann gleich wieder.

Der `delimiter` wird bei `savetxt()` gar nicht verwendet wenn man eine ”komplexe” Formatzeichenkette angibt. Die Kommas sind da ja schon enthalten.

@Hyperion: Zeile 14 dürfte von der Laufzeit her vernachlässigbar sein. Das ist Numpy. In C ein paar Arrays im Speicher herum zu schieben dauert einen kleinen Bruchteil einer Sekunde, das kann nicht mehrere Sekunden Laufzeitunterschied ausmachen.
Benutzeravatar
MagBen
User
Beiträge: 799
Registriert: Freitag 6. Juni 2014, 05:56
Wohnort: Bremen
Kontaktdaten:

Wenn Du nach np.savetxt reindebuggst, dann wirst Du sehen, dass das auch alles lediglich Python-Code ist. Du bekommst mit den Numpy Methoden savetxt und loadtxt lediglich Numpy-Komfort, aber nicht den üblichen Numpy-Performancevorteil.

Da die Numpy Methode allgemeiner ist als Deine Implementation macht sie im Schleifenkern etwas mehr, npyio.py, Zeile 1073:

Code: Alles auswählen

            for row in X:
                fh.write(asbytes(format % tuple(row) + newline))
a fool with a tool is still a fool, www.magben.de, YouTube
schneitzmaster
User
Beiträge: 94
Registriert: Freitag 26. Oktober 2012, 15:35
Wohnort: Hamburg

vielen dank für euere hinweise.
@Hyperion: Nein mir wurde noch nicht gesagt, dass man text-dateien immer m "with" verarbeiten sollte. Ich stelle mir aber die Frage warum? Ist der einzige Grund, dass man sich die Zeilte f.close() spart? Zu dem ist das mit dem with-statement sogar ein wenig langsamer (siehe Beispiel Code weiter unten).
In Zeile 14 generiere ich die Knotennummern, die in der Ausgabedatei benötigt werden.

@ BlackJack: Ich habe die Erfahrung gemacht das List-comprehensions immer schneller Laufen als reine For-Schleifen. Hier ist das auch so, auch wenn der Geschwindigkeitsunterschied marginal ist (ca. 2.5%). Auch wenn unnötiger Weise eine große Liste erzeugt wird ist es dennoch schneller. Also warum sollte ich das dann nicht ausnutzen?

Mich würde jetzt interessieren ob es einen noch schnelleren weg gibt den nodes array in eine textdatei zu schreiben.

Der folgende Code zum Vergleich der einzelnen schreib-methoden

Code: Alles auswählen

import numpy as np
import time

nodes = np.random.rand(1000000,3)

start = time.time()
with open('test1.txt', mode='w') as f:
    for i,node_i in enumerate(nodes):
        f.write( '%d, %16.14F, %16.14F, %16.14F \n' % (i+1, node_i[0], node_i[1], node_i[2]) )    
print 'standard for-schleife (with open...):',time.time()-start

start = time.time()
with open('test2.txt', mode='w') as f:
    [f.write( '%d, %16.14F, %16.14F, %16.14F \n' % (i+1, node_i[0], node_i[1], node_i[2]) ) for i,node_i in enumerate(nodes)]
print 'LC (with open...):',time.time()-start

start = time.time()
with open('test3.txt', mode='w') as f:
    idx_nodes = np.column_stack( (np.arange(1,len(nodes)+1),nodes) )
    np.savetxt(f, idx_nodes, fmt='%d, %16.14F, %16.14F, %16.14F', delimiter=', ', newline='\n')
print 'numpy savetxt (with open...):',time.time()-start
# ============================================================================
start = time.time()
f = open('test1.txt', mode='w')
for i,node_i in enumerate(nodes):
    f.write( '%d, %16.14F, %16.14F, %16.14F \n' % (i+1, node_i[0], node_i[1], node_i[2]) )    
f.close()
print 'standard for-schleife:',time.time()-start

start = time.time()
f = open('test2.txt', mode='w')
[f.write( '%d, %16.14F, %16.14F, %16.14F \n' % (i+1, node_i[0], node_i[1], node_i[2]) ) for i,node_i in enumerate(nodes)]
f.close()
print 'LC:',time.time()-start

start = time.time()
f = open('test3.txt', mode='w')
idx_nodes = np.column_stack( (np.arange(1,len(nodes)+1),nodes) )
np.savetxt(f, idx_nodes, fmt='%d, %16.14F, %16.14F, %16.14F', delimiter=', ', newline='\n')
f.close()
print 'numpy savetxt:',time.time()-start
gibt mir auf meinem Laptop folgendes aus:

Code: Alles auswählen

standard for-schleife (with open...): 4.68522500992
LC (with open...): 3.95728611946
numpy savetxt (with open...): 5.42520999908
standard for-schleife: 4.33999490738
LC: 3.94807004929
numpy savetxt: 5.56135797501
Da stellt sich mir die Frage ob es wirklich so sinnvoll ist mit dem with statement zu arbeiten
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Ich schrieb einen wesentlichen Vorteil ja in die Kommentare im Code-Schnipsel zum Ansatz mit ``with``: ``fobj.close()`` wird auch aufgerufen, sollte eine Exception auftreten. Ohne das, müsstest Du Dich selber darum kümmern, was den Code ziemlich aufbläht.

Grundsätzlich musst Du Dich fragen: Musst Du das optimieren, weil es aktuell zu lange dauert oder machst Du das nur zum "Spaß"? Das Grundsatzkredo sollte immer sein: Lesbarkeit (und damit idiomatischen Code) geht vor Geschwindigkeit. Nur in Ausnahmefällen sollte man davon abweichen. Insofern ein klares Ja zu ``with``! (Und ein klares Nein zu LCs, die die Ergebnisse wegwerfen)

Bei IO-Operationen kann man auch ganz einfach optimieren: Eine schnellere Festplatte verwenden ;-)

Deine Abweichungen bezüglich ``with`` scheinen mir irgend wie auch fast Messfehler zu sein; auf jeden Fall sind das so kleine Abweichungen im Vergleich zur Gesamtezit, dass Dich das nicht kümmern sollte.
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
schneitzmaster
User
Beiträge: 94
Registriert: Freitag 26. Oktober 2012, 15:35
Wohnort: Hamburg

okay. MIt exceptions bin ich bisher noch nicht in berührung gekommen.
Zum "spaß" mache ich das nicht. Ich steuere bloß via Python c-programme. Deren IO ist rasant schnell im vergleich zu dem von python. Da es ja schnittstellen wie z.b. numpy gibt die c-funktionalität verwenden und deren geschwindigkeitsvorteile weitergeben vermutete ich, dass selbiges auch für das schreiben von dateien existiert.

Eine schnellere Festplatte hab ich erst am Montag wieder zur Verfügung :)
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Du musst Dir klar machen, dass man Quellcode öfter liest, als schreibt! Insofern ist Lesbarkeit eines der Hauptkriterien für die Wartbarkeit. Neben Konventionen bezüglich der Formatierung (sogenannte "Style Guides", in Python PEP8) spielt dabei auch das Schreiben von idiomatischen Code bezüglich einer Sprache eine große Rolle. LCs verwendet man eben nicht zum Wegwerfen, sondern um etwas zu generieren. Schau Dir mal folgenden Python 3 Code an:

Code: Alles auswählen

 [print(x) for x in range(10)]
Klar kann ich das machen, um die Ausgabe in eine LC reinzuziehen - aber damit würde *niemand* rechnen und man müsste erst mehrfach hingucken, um zu kapieren, *was* das eigentlich soll. Insofern würde ich von derartigen Optimierungen immer abraten.

Sollte das ganze so kritisch sein, dass ohne eine derartige Optimierung das ganze Projekt in Frage gestellt ist, so würde ich auch in dem Fall eine *robuste* und *verlässliche* Lösung bevorzugen. Evtl. basiert Deine Erkenntnis nur auf einem Implementierungsdetail in der aktuellen Fassung von CPython? Evtl. sieht es unter PyPy oder anderen Interpretern anders aus? Evtl. ändert sich das auch bei der nächsten CPython-Version? Auf so etwas würde ich mich bei kritischen Stellen *nie* verlassen - und letztlich muss die Stelle ja kritisch sein, wenn man da wirklich die Optimierung braucht.

Als sinnvollerer Ausweg bleibt dann immer noch eine Lösung mittels C (oder Cython o.ä.), die sich um das Schreiben (und ggf. das Wandeln zuvor) kümmert.
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

Hier mal der "Beweis" dass Deine Messunterschiede zwischen LC und 'with' Rauschen ist:

Code: Alles auswählen

standard for-schleife (with open...): 5.69332003593
LC (with open...): 5.76606106758
numpy savetxt (with open...): 7.07917189598
standard for-schleife: 5.60146999359
LC: 5.79651784897
numpy savetxt: 7.06342411041
Die erste und sauberste Lösung ist schneller als Deine präferierte vorletzte Lösung.
schneitzmaster
User
Beiträge: 94
Registriert: Freitag 26. Oktober 2012, 15:35
Wohnort: Hamburg

@ Hyperion: Ja okay da gebe ich dir recht. Lesbarkeit kommt vor Geschwindigkeit. Wie gesagt, war ich eigentlich auf der Suche nach einer besseren möglichkeit die textdatei zu erstellen. Man kann sich ja nie sicher sein ob man nun einen akzeptablen code geschrieben hat oder ob es eine einfache standard-möglichkeiten gibt die aufgaben zu bewältigen. Besonders wenn man python nur als werkzeug benutzt. Ich glaube aber ich war schon ziemlich dicht am "normal" code zum schreiben in eine textdatei dran.
@Sirius: Ja ich hab das bei mir auch gerade noch mal durchlaufen lassen mit ähnlichem ergebnis. eigenartig dass das rauschen so einen einfluss hat.
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Ich erwähnte ja schon mal ``timeit`` - das ermittelt den Durchschnitt der Laufzeit von mehreren Durchgängen und minimiert somit Messfehler.

Kann man in IPython übrigens prima aufrufen:

Code: Alles auswählen

In [14]: values = list(range(1000000))

In [15]: %timeit sum(values)
1 loops, best of 3: 184 ms per loop
Wobei mir gerade selber nicht klar ist, wie man dan bei IO spezifischen Dingen vorgeht... immerhin muss man die Dateien ja auch wieder löschen zwischendurch. Evtl. wäre da ``tempfile.mkstemp`` eine Lösung...

@schneitzmeister: Gerade bei IO sind Messungen ja besonders stark von vielen externen und wenig beeinflussbaren Faktoren abhängig. Neben den Aktivitäten des OS oder anderer Programme spielen da ja auch Fragmentierung, Konfiguration des Dateisystems uvm. eine Rolle.
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
BlackJack

@schneitzmeister: Mit Cython wird's ein bisschen schneller:

Gerenatorausdruck + `file.writelines()`: 3.48566484451
`numpy.savetxt`: 5.95442891121
`_test.write_to_file()`: 1.69377708435

_test.pyx:

Code: Alles auswählen

cimport numpy as np
from libc cimport stdio


def void write_to_file(object out_file, np.ndarray[double, ndim=2] array):
    cdef unsigned int i
    cdef int byte_count = 0
    cdef stdio.FILE *f

    f = stdio.fdopen(out_file.fileno(), 'w')
    if f == NULL:
        raise IOError('can not write to file')
    try:
        for i in range(array.shape[0]):
            byte_count = stdio.fprintf(
                f,
                '%d, %16.14F, %16.14F, %16.14F\n',
                i + 1,
                array[i, 0],
                array[i, 1],
                array[i, 2]
            )
            if byte_count < 0:
                raise IOError('error while writing to file')
    finally:
        if byte_count >= 0:
            if stdio.fflush(f) != 0:
                raise IOError('error while flushing file')
Bei den `IOError` hätte man sich noch um `errno` kümmern müssen damit das schön(er) wird.

Sonst bliebe noch C und die Python/Numpy-C-API. Aber ob der ganze Boilerplate den man dann schreiben müsste den eventuellen kleinen Gewinn wett macht, bezweifle ich.
Antworten