Pandas: Keine Möglichkeit, einen gemischten dtype-DataFrame ohne vollständige Kopie zu erstellen, vorgeschlagene Lösung

Erstellt am 9. Jan. 2015  ·  58Kommentare  ·  Quelle: pandas-dev/pandas

Nach stundenlangem Zerreißen meiner Haare bin ich zu dem Schluss gekommen, dass es unmöglich ist, einen gemischten dtype-DataFrame zu erstellen, ohne alle seine Daten hineinzukopieren. Das heißt, egal was Sie tun, wenn Sie einen gemischten dtype-DataFrame erstellen möchten , erstellen Sie unweigerlich eine temporäre Version der Daten (zB mit np.empty), und die verschiedenen DataFrame-Konstruktoren erstellen immer Kopien dieser temporären Version. Dieses Thema wurde bereits vor einem Jahr angesprochen: https://github.com/pydata/pandas/issues/5902.

Dies ist besonders schlimm für die Interoperabilität mit anderen Programmiersprachen. Wenn Sie vorhaben, die Daten in den DataFrame von zB einem Aufruf von C zu füllen, ist dies bei weitem der einfachste Weg, den DataFrame in Python zu erstellen, Zeiger auf die zugrunde liegenden Daten zu erhalten, die np.arrays sind, und diese np zu übergeben .arrays entlang, damit sie gefüllt werden können. In dieser Situation ist es Ihnen einfach egal, mit welchen Daten der DataFrame beginnt, das Ziel besteht nur darin, den Speicher so zuzuweisen, dass Sie wissen, wohin Sie kopieren.

Dies ist auch nur im Allgemeinen frustrierend, da es im Prinzip (in Abhängigkeit von der spezifischen Situation und den Implementierungsdetails usw.)

Dies hat eine extrem einfache Lösung, die bereits im quantitativen Python-Stack verankert ist: Verwenden Sie eine Methode analog zu numpy's empty. Dies weist den Speicherplatz zu, verschwendet jedoch keine Zeit damit, etwas zu schreiben oder zu kopieren. Da leer bereits belegt ist, würde ich vorschlagen, die Methode from_empty aufzurufen. Es würde einen Index akzeptieren (obligatorisch, der häufigste Anwendungsfall wäre die Übergabe von np.arange(N)), Spalten (obligatorisch, normalerweise eine Liste von Strings), Typen (Liste akzeptabler Typen für Spalten, gleiche Länge wie Spalten). Die Liste der Typen sollte Unterstützung für alle numerischen Numpy-Typen (ints, floats) sowie spezielle Pandas-Spalten wie DatetimeIndex und Categorical enthalten.

Als zusätzlicher Bonus wird die vorhandene API nicht beeinträchtigt, da die Implementierung in einer völlig separaten Methode erfolgt.

API Design Constructors Dtypes

Hilfreichster Kommentar

Es gibt viele viele Threads auf SO, die nach dieser Funktion fragen.

Mir scheint, dass all diese Probleme darauf zurückzuführen sind, dass BlockManager separate Spalten in einzelne Speicherblöcke (die 'Blöcke') konsolidiert.
Wäre nicht die einfachste Lösung, Daten nicht in Blöcke zu konsolidieren, wenn copy=False angegeben ist.

Ich habe einen nicht konsolidierenden Affen-gepatchten BlockManager:
https://stackoverflow.com/questions/45943160/can-memmap-pandas-series-what-about-a-dataframe
dass ich dieses Problem umgangen habe.

Alle 58 Kommentare

Sie können einfach einen leeren Rahmen mit einem Index und Spalten erstellen
dann weise ndarrays zu - diese kopieren nicht von dir, weisen alle eines bestimmten dtypes auf einmal zu

Sie können diese mit np.empty erstellen, wenn Sie möchten

df = pd.DataFrame(index=range(2), columns=["dude", "wheres"])

df
Out[12]:
  dude wheres
0  NaN    NaN
1  NaN    NaN

x = np.empty(2, np.int32)

x
Out[14]: array([6, 0], dtype=int32)

df.dude = x

df
Out[16]:
   dude wheres
0     6    NaN
1     0    NaN

x[0] = 0

x
Out[18]: array([0, 0], dtype=int32)

df
Out[19]:
   dude wheres
0     6    NaN
1     0    NaN

Sieht für mich so aus, als ob es kopiert wird. Es sei denn, der Code, den ich geschrieben habe, ist nicht das, was Sie gemeint haben, oder das Kopieren, das aufgetreten ist, ist nicht die Kopie, von der Sie dachten, dass ich sie entfernen wollte.

du hast den dtype geändert
deshalb kopiert es versuch mit einem float

y = np.empty(2, np.float64)

df
Out[21]:
   dude wheres
0     6    NaN
1     0    NaN

df.wheres = y

y
Out[23]: array([  2.96439388e-323,   2.96439388e-323])

y[0] = 0

df
Out[25]:
   dude         wheres
0     6  2.964394e-323
1     0  2.964394e-323

df = pd.DataFrame(index=range(2), columns=["dude", "wheres"])

df.dtypes
Out[27]:
dude      object
wheres    object
dtype: object

Der dtype ist object, also ändert er sich unabhängig davon, ob ich einen float oder einen int verwende.

In [25]: arr = np.ones((2,3))

In [26]: df = DataFrame(arr,columns=['a','b','c'])

In [27]: arr[0,1] = 5

In [28]: df
Out[28]: 
   a  b  c
0  1  5  1
1  1  1  1

Das Erstellen von Kopien ohne Kopie auf gemischtem Typ könnte möglich sein, ist aber ziemlich knifflig. Das Problem besteht darin, dass einige Typen eine Kopie erfordern (z. B. ein Objekt, um Probleme mit Speicherkonflikten zu vermeiden). Und die interne Struktur konsolidiert verschiedene Typen, sodass das Hinzufügen eines neuen Typs eine Kopie erfordert. Eine Kopie zu vermeiden ist in den meisten Fällen ziemlich schwierig.

Sie sollten einfach erstellen, was Sie brauchen, Zeiger auf die Daten erhalten und diese dann überschreiben. Warum ist das ein Problem?

Das Problem ist, dass ich, um das zu erstellen, was ich brauche, Sachen des richtigen dtype hineinkopieren muss, deren Daten ich nicht verwenden möchte. Selbst unter der Annahme, dass Ihr Vorschlag, einen leeren DataFrame zu erstellen, keinen signifikanten RAM-Speicher verwendet, verringert dies die Kopierkosten nicht. Wenn ich einen 1-Gigabyte-DataFrame erstellen und an anderer Stelle auffüllen möchte, muss ich die Kosten für das Kopieren eines Gigabyte an Müll im Speicher bezahlen, was völlig unnötig ist. Sehen Sie das nicht als Problem?

Ja, ich verstehe, dass die interne Struktur verschiedene Typen konsolidiert. Ich bin mir nicht sicher, was Sie mit Speicherkonflikten meinen, aber auf jeden Fall sind Objekte hier nicht wirklich von Interesse.

Obwohl das Vermeiden von Kopien im Allgemeinen ein schwieriges Problem ist, ist es ziemlich einfach, sie auf die von mir vorgeschlagene Weise zu vermeiden, da ich alle notwendigen Informationen von Anfang an bereitstelle. Es ist identisch mit dem Konstruieren aus Daten, außer dass Sie die dtypes und die Anzahl der Zeilen nicht aus den Daten ableiten und die Daten kopieren, sondern die dtypes und die Anzahl der Zeilen direkt angeben und alles andere genau so machen, wie Sie es ohne die Kopie getan hätten.

Sie benötigen für jeden unterstützten Spaltentyp einen "leeren" Konstruktor. Für numpy numerische Typen ist dies offensichtlich, es erfordert nicht-null Arbeit für Categorical, unsicher bezüglich DatetimeIndex.

ein dict an den Konstruktor übergeben und copy=False sollte funktionieren

Das wird also funktionieren. Sie müssen jedoch SICHER sein, dass die übergebenen Arrays unterschiedliche dtypes sind. Und sobald Sie etwas daran tun, könnten die zugrunde liegenden Daten kopiert werden. Also YMMV. Sie können natürlich np.empty anstelle der Einsen/Nullen eingeben, die ich bin.

In [75]: arr = np.ones((2,3))

In [76]: arr2 = np.zeros((2,2),dtype='int32')

In [77]: df = DataFrame(arr,columns=list('abc'))

In [78]: df2 = DataFrame(arr2,columns=list('de'))

In [79]: result = pd.concat([df,df2],axis=1,copy=False)

In [80]: arr2[0,1] = 20

In [81]: arr[0,1] = 10

In [82]: result
Out[82]: 
   a   b  c  d   e
0  1  10  1  0  20
1  1   1  1  0   0

In [83]: result._data
Out[83]: 
BlockManager
Items: Index([u'a', u'b', u'c', u'd', u'e'], dtype='object')
Axis 1: Int64Index([0, 1], dtype='int64')
FloatBlock: slice(0, 3, 1), 3 x 2, dtype: float64
IntBlock: slice(3, 5, 1), 2 x 2, dtype: int32

In [84]: result._data.blocks[0].values.base
Out[84]: 
array([[  1.,  10.,   1.],
       [  1.,   1.,   1.]])

In [85]: result._data.blocks[1].values.base
Out[85]: 
array([[ 0, 20],
       [ 0,  0]], dtype=int32)

_Erstversuch gelöscht, da funktioniert nicht, da reindex Casting erzwingt, was ein seltsames "Feature" ist._

Muss 'Methode' verwenden, was diesen Versuch etwas weniger zufriedenstellend macht:

arr = np.empty(1, dtype=[('x', np.float), ('y', np.int)])
df = pd.DataFrame.from_records(arr).reindex(np.arange(100))

Wenn Sie sich wirklich Sorgen um die Leistung machen, bin ich mir nicht sicher, warum man nicht so oft wie möglich numpy verwenden sollte, da es konzeptionell viel einfacher ist.

jreback, danke für deine Lösung. Dies scheint sogar für Kategorische zu funktionieren (was mich überrascht hat). Wenn ich auf Probleme stoße, lasse ich es Sie wissen. Ich bin mir nicht sicher, was Sie damit meinen: Wenn Sie etwas daran tun, könnte es kopiert werden. Was meinst du mit irgendwas? Wenn es keine COW-Semantik gibt, würde ich denken, dass Sie zur Konstruktionszeit das sehen, was Sie in Bezug auf tiefe vs. flache Kopien sehen.

Ich denke immer noch, dass ein from_empty-Konstruktor implementiert werden sollte, und ich denke, es wäre nicht so schwierig, obwohl diese Technik funktioniert, erfordert sie viel Code-Overhead. Im Prinzip könnte dies durch die Angabe eines einzelnen zusammengesetzten dtypes und mehrerer Zeilen erfolgen.

bashtage schreiben diese Lösungen immer noch in den gesamten DataFrame. Da das Schreiben im Allgemeinen langsamer ist als das Lesen, bedeutet dies, dass es bestenfalls weniger als die Hälfte des betreffenden Overheads einspart.

Wenn ich Numpy nicht benutzt habe, liegt das natürlich daran, dass Pandas viele tolle Funktionen und Fähigkeiten haben, die ich liebe, und ich möchte diese nicht aufgeben. Haben Sie wirklich gefragt oder nur angedeutet, dass ich Numpy verwenden sollte, wenn ich diesen Leistungseinbruch nicht hinnehmen möchte?

Bitte kratzen Sie dies ab, Benutzerfehler und ich entschuldige mich. reindex_axis mit copy=False hat einwandfrei funktioniert.

bashtage schreiben diese Lösungen immer noch in den gesamten DataFrame. Da das Schreiben im Allgemeinen langsamer ist als das Lesen, bedeutet dies, dass es bestenfalls weniger als die Hälfte des betreffenden Overheads einspart.

Stimmt, aber alles, was Sie brauchen, um ein neues method für reindex erstellen, das mit nichts gefüllt wird, und dann können Sie ein typisiertes Array mit beliebigen Spaltentypen ohne Schreiben/Kopieren zuweisen.

Wenn ich Numpy nicht benutzt habe, liegt das natürlich daran, dass Pandas viele tolle Funktionen und Fähigkeiten haben, die ich liebe, und ich möchte diese nicht aufgeben. Haben Sie wirklich gefragt oder nur angedeutet, dass ich Numpy verwenden sollte, wenn ich diesen Leistungseinbruch nicht hinnehmen möchte?

Es war ein bisschen rhetorisch - obwohl auch aus Performance-Sicht ein ernstzunehmender Vorschlag, da numpy es viel einfacher macht, in die Nähe des Daten-als-Blob-of-Memory-Zugriffs zu kommen, der wichtig ist, wenn Sie versuchen, sehr zu schreiben Hochleistungscode. Sie können immer von numpy in pandas konvertieren, wenn die Einfachheit des Codes wichtiger ist als die Leistung.

Ich verstehe, was du sagst. Ich denke immer noch, dass es sauberer Teil der Benutzeroberfläche sein sollte als eine Problemumgehung, aber als Problemumgehung ist es gut und einfach zu implementieren.

Pandas betont Leistung immer noch als eines seiner Hauptziele. Offensichtlich hat es im Vergleich zu numpy höherwertige Funktionen, und diese müssen bezahlt werden. Worüber wir sprechen, hat nichts mit diesen höherwertigen Funktionen zu tun, und es gibt keinen Grund, warum man für massive Kopien an Orten bezahlen sollte, an denen Sie sie nicht benötigen. Ihr Vorschlag wäre angebracht, wenn jemand nach den Kosten für die Einrichtung der Spalten, des Index usw. stank, was sich von dieser Diskussion völlig unterscheidet.

Ich denke, Sie überschätzen die Kosten für das Schreiben im Vergleich zum Code für die Speicherzuweisung in Python - der teure Teil ist die Speicherzuweisung. Auch die Objekterstellung ist teuer.

Beide weisen 1 GB Speicher zu, einer leer und einer mit Nullen.

%timeit np.empty(1, dtype=[('x', float), ('y', int), ('z', float)])
100000 loops, best of 3: 2.44 µs per loop

%timeit np.zeros(1, dtype=[('x', float), ('y', int), ('z', float)])
100000 loops, best of 3: 2.47 µs per loop

%timeit np.zeros(50000000, dtype=[('x', float), ('y', int), ('z', float)])
100000 loops, best of 3: 11.7 µs per loop

%timeit np.empty(50000000, dtype=[('x', float), ('y', int), ('z', float)])
100000 loops, best of 3: 11.4 µs per loop

3µs zum Nullsetzen von 150.000.000 Werten.

Vergleichen Sie diese nun für einen trivialen DataFrame.

%timeit pd.DataFrame([[0]])
1000 loops, best of 3: 426 µs per loop

Etwa 200 mal langsamer für trivial. Bei größeren Arrays ist es jedoch weitaus schlimmer.

%timeit pd.DataFrame(np.empty((50000000, 3)),copy=False)
1 loops, best of 3: 275 ms per loop

Jetzt dauert es 275 m s - beachten Sie, dass dies nicht alles zu kopieren ist. Die Kosten entstehen durch das Einrichten des Index usw., was eindeutig sehr langsam ist, wenn das Array nicht trivial groß ist.

Dies fühlt sich für mich wie eine verfrühte Optimierung an, da die anderen Gemeinkosten in Pandas so groß sind, dass die Malloc + Füllungskomponente fast 0 kostet.

Es scheint, dass, wenn Sie etwas in einer engen Schleife zuweisen möchten, dies aus Leistungsgründen ein numpy-Array sein muss.

ok, ich denke, wir sollten Folgendes tun, möchten . 2 Probleme.

  • #4464 - Dies erlaubt im Wesentlichen einen zusammengesetzten dtype im DataFrame Konstruktor und dann das Umdrehen und Aufrufen von from_records() , das auch aufgerufen werden kann, wenn das übergebene Array ein rec/strukturiertes Array ist - das würde im Grunde from_records zum recycelten/strukturierten Array-Verarbeitungspfad machen
  • Übergeben Sie das Schlüsselwort copy= an from_records
  • from_records kann dann die concat Lösung verwenden, die ich oben gezeigt habe, anstatt das Rec-Array aufzuteilen, sie zu desinfizieren (als Serie) und sie dann wieder zusammenzusetzen (in dtype-Blöcke; dieser Teil) erfolgt intern).

Dies ist etwas nicht trivial, würde es dann aber ermöglichen, ein bereits erstelltes ndarray (könnte leer sein) mit gemischten Typen ziemlich einfach zu übergeben. Beachten Sie, dass dies wahrscheinlich (in einer Implementierung des ersten Durchgangs) nur (int/float/string) behandeln würde. da datetime/timedelta eine spezielle Bereinigung benötigen und dies etwas komplizierter machen würden.

@bashtage hat also aus Perf-Perspektive Recht. Es ist sehr sinnvoll, den Rahmen einfach so zu konstruieren, wie Sie möchten, und dann die ndarrays zu ändern (aber Sie müssen dies tun, indem Sie die Blöcke greifen, sonst erhalten Sie Kopien).

Was ich oben meinte ist folgendes. Pandas gruppieren jeden Like-dType (zB int64,int32 sind unterschiedlich) in einen 'Block' (2-d in einem Frame). Dies sind zusammenhängende Speicher-ndarrays (die neu zugewiesen werden, es sei denn, sie werden einfach übergeben, die derzeit nur für einen einzelnen dtype funktionieren). Wenn Sie dann ein Setitem ausführen, zB df['new_columns'] = 5 und Sie bereits einen int64-Block haben, dann wird diese neue Spalte letztendlich damit verknüpft (was zu einer neuen Speicherzuweisung für diesen dtype führt). Wenn Sie eine Referenz als Ansicht dafür verwendet haben, ist diese nicht mehr gültig. Aus diesem Grund ist dies keine Strategie, die Sie ohne Peering auf die DataFrame-Interna anwenden können.

@bashtage ja, die großen Kosten sind der Index, wie Sie bemerkt haben. ein RangeIndex (siehe #939) würde dieses Problem vollständig lösen. (es ist eigentlich fast in einem Seitenzweig fertig, muss nur abgestaubt werden).

Selbst mit einem optimierten RangeIndex es immer noch 2 Größenordnungen langsamer sein als das Konstruieren eines NumPy-Arrays, was angesichts des viel schwereren Gewichts und der zusätzlichen Fähigkeiten eines DataFrame fair genug ist.

Ich denke, dies kann nur als Komfortfunktion und nicht als Leistungsproblem angesehen werden. Es könnte nützlich sein, einen gemischten Typ DataFrame oder Panel wie zu initialisieren.

dtype=np.dtype([('GDP', np.float64), ('Population', np.int64)])
pd.Panel(items=['AU','AT'],
         major_axis=['1972','1973'],
         minor_axis=['GDP','Population'], 
         dtype=[np.float, np.int64])

Dies ist nur ein API / Convenience-Problem

stimmte zu, dass die Perf wirklich ein zufälliges Problem ist (und nicht der Fahrer)

@bashtage

%timeit pd.DataFrame(np.empty((100, 1000000)))
100 Loops, Best of 3: 15,6 ms pro Loop

%timeit pd.DataFrame(np.empty((100, 1000000)), copy=True)
1 Schleifen, Best of 3: 302 ms pro Schleife

Das Kopieren in einen Datenrahmen scheint also 20-mal länger zu dauern als alle anderen Arbeiten, die bei der Erstellung des Datenrahmens anfallen, dh das Kopieren (und die zusätzliche Zuweisung) beträgt 95 % der Zeit. Die Benchmarks, die Sie durchgeführt haben, messen nicht das Richtige. Ob das Kopieren selbst oder die Zuweisung Zeit in Anspruch nimmt, spielt keine Rolle. Der Punkt ist, dass ich viel Zeit sparen könnte, wenn ich Kopien für einen DataFrame mit mehreren Dtypes so vermeiden könnte, wie ich es für einen einzelnen Dtype-DataFrame kann.

Ihre Argumentation in zwei Größenordnungen täuscht ebenfalls. Dies ist nicht der einzige ausgeführte Vorgang, es werden auch andere Vorgänge ausgeführt, die Zeit in Anspruch nehmen, wie z. B. das Lesen von Datenträgern. Im Moment dauert die zusätzliche Kopie, die ich machen muss, um den DataFrame zu erstellen, ungefähr die Hälfte der Zeit in meinem einfachen Programm, das nur die Daten von der Festplatte und in einen DataFrame liest. Wenn es 1/20 so lange dauern würde, wäre das Lesen der Festplatte dominant (wie es sein sollte) und weitere Verbesserungen hätten fast keine Wirkung.

Daher möchte ich euch beiden noch einmal betonen: Dies ist ein echtes Leistungsproblem.

jreback Angesichts der Tatsache, dass die Verkettungsstrategie für Kategorien nicht funktioniert, glauben Sie nicht, dass die oben vorgeschlagenen Verbesserungen funktionieren werden. Ich denke, ein besserer Ausgangspunkt wäre eine Neuindizierung. Das Problem im Moment ist, dass Reindex viele zusätzliche Dinge macht. Aber im Prinzip hat ein DataFrame mit null Zeilen alle nötigen Informationen, um ohne unnötigen Aufwand einen DataFrame mit der richtigen Zeilenanzahl erstellen zu können. Übrigens, das gibt mir wirklich das Gefühl, dass Pandas ein Schemaobjekt brauchen, aber das ist eine Diskussion für einen anderen Tag.

Ich denke, wir müssen uns einigen, um anderer Meinung zu sein. IMO-DataFrames sind keine extremen Leistungsobjekte im numerischen Ökosystem, wie der Größenunterschied zwischen einem einfachen Numpy-Array und einer DataFrame-Erstellung zeigt.

%timeit np.empty((1000000, 100))
1000 loops, best of 3: 1.61 ms per loop

%timeit pd.DataFrame(np.empty((1000000,100)))
100 loops, best of 3: 15.3 ms per loop

Im Moment dauert die zusätzliche Kopie, die ich machen muss, um den DataFrame zu erstellen, ungefähr die Hälfte der Zeit in meinem einfachen Programm, das nur die Daten von der Festplatte und in einen DataFrame liest. Wenn es 1/20 so lange dauern würde, wäre das Lesen der Festplatte dominant (wie es sein sollte) und weitere Verbesserungen hätten fast keine Wirkung.

Ich denke, dies ist noch weniger Grund, sich um die DataFrame-Leistung zu kümmern - selbst wenn Sie es zu 100% kostenlos machen können, verringert sich die Gesamtprogrammzeit nur um 50%.

Ich stimme zu, dass Sie hier die Möglichkeit haben, dieses Problem zu lösen, unabhängig davon, ob Sie es als Leistungsproblem oder als Komfortproblem betrachten. Aus meiner Sicht sehe ich es als letzteres, da ich immer ein numpy-Array verwenden werde, wenn mir die Leistung wichtig ist. Numpy macht andere Dinge, wie zum Beispiel keinen Blockmanager zu verwenden, der für einige Dinge relativ effizient ist (wie das Array durch Hinzufügen von Spalten zu vergrößern). aber aus anderer Sicht schlecht.

Es könnte zwei Möglichkeiten geben. Der erste, ein leerer Konstruktor wie im obigen Beispiel. Dies würde nichts kopieren, aber wahrscheinlich mit Null auffüllen, um mit anderen Dingen in Pandas konsistent zu sein. Nullfüllung ist ziemlich billig und ist nicht die Wurzel des Problems IMO.

Die andere wäre, eine Methode DataFrame.from_blocks , die vorgefertigte Blöcke benötigt, um direkt an den Blockmanager zu übergeben. Etwas wie

DataFrame.from_blocks([np.empty((100,2)), 
                       np.empty((100,3), dtype=np.float32), 
                       np.empty((100,1), dtype=np.int8)],
                     columns=['f8_0','f8_1','f4_0','f4_1','f4_2','i1_0'],
                     index=np.arange(100))

Ein Verfahren dieser Art würde erzwingen, dass die Blöcke eine kompatible Form haben, alle Blöcke eindeutige Typen haben, sowie die üblichen Prüfungen der Form des Index und der Spalten. Diese Art von Methode würde den Daten nichts anhaben und sie im BlockManger verwenden.

@quicknir du versuchst ziemlich komplizierte Dinge zu kombinieren. Kategorische existieren nicht in numpy, sondern sind ein zusammengesetzter dtype, so wie es sich um ein Pandas-Konstrukt handelt. Sie müssen dann separat konstruieren und zuweisen (was eigentlich recht billig ist - diese werden nicht wie andere singuläre dtypes zu Blöcken zusammengefasst).

@bashtage soln scheint vernünftig. Dies könnte einige einfache Überprüfungen ermöglichen und die Daten einfach passieren (und von den anderen internen Routinen aufgerufen werden). Normalerweise braucht sich der Benutzer nicht um die interne Darstellung zu kümmern. Da Sie es wirklich wollen, müssen Sie sich dessen bewusst sein.

Trotzdem bin ich mir immer noch nicht sicher, warum Sie nicht einfach einen Rahmen erstellen, der genau so ist, wie Sie es möchten. Greifen Sie dann die Blockzeiger und ändern Sie die Werte. Es kostet den gleichen Speicher, und wie @bashtage betont , ist es ziemlich billig, im Wesentlichen einen Null-Frame zu erstellen (der alle dtype,index,columns bereits festgelegt hat).

Ich bin mir nicht sicher, was Sie mit dem leeren Konstruktor meinen, aber wenn Sie meinen, einen Datenrahmen ohne Zeilen und das gewünschte Schema zu konstruieren und Reindex aufzurufen, ist dies dieselbe Zeit wie die Erstellung mit copy=True.

Ihr zweiter Vorschlag ist vernünftig, aber nur, wenn Sie herausfinden können, wie Kategoricals erstellt werden. Zu diesem Thema ging ich den Code durch und stellte fest, dass Kategorisierungen nicht konsolidierbar sind. Also habe ich aus einer Ahnung heraus ein Integer-Array und zwei kategoriale Serien erstellt, dann drei DataFrames erstellt und alle drei verkettet. Tatsächlich wurde keine Kopie durchgeführt, obwohl zwei der DataFrames denselben dtype hatten. Ich werde versuchen zu sehen, wie dies für den Datetime-Index funktioniert.

@jreback Ich

@quicknir warum zeigen Sie nicht ein Code- / Pseudocode-Beispiel von dem, was Sie tatsächlich versuchen.

def read_dataframe(filename, ....):
   f = my_library.open(filename)
   schema = f.schema()
   row_count = f.row_count()
   df = pd.DataFrame.from_empty(schema, row_count)
   dict_of_np_arrays = get_np_arrays_from_DataFrame(df)
   f.read(dict_of_np_arrays)
   return df

Der vorherige Code erstellte zuerst ein Wörterbuch mit numpy Arrays und dann daraus einen DataFrame, da alles kopiert wurde. Dafür wurde etwa die Hälfte der Zeit aufgewendet. Also versuche ich, es in dieses Schema zu ändern. Die Sache ist die, dass es extrem teuer ist, df wie oben zu konstruieren, auch wenn Sie sich nicht um den Inhalt kümmern.

@quicknir dict of np arrays erfordert viel Kopieren.

Sie sollten einfach dies tun:

# construct your biggest block type (e.g. say you have mostly floats)
df = DataFrame(np.empty((....)),index=....,columns=....)

# then add in other things you need (say strings)
df['foo'] = np.empty(.....)

# say ints
df['foo2'] = np.empty(...)

Wenn Sie dies mit dtype tun, wird es billig sein

dann.

for dtype, block in df.as_blocks():
    # fill the values
    block.values[0,0] = 1

da diese Blockwerte Ansichten in numpy Arrays sind

Die Zusammensetzung der Typen ist im Allgemeinen nicht im Voraus bekannt, und im häufigsten Anwendungsfall gibt es eine gesunde Mischung aus Floats und Ints. Ich denke, ich kann nicht verfolgen, wie das billig sein wird, wenn ich 30 Float-Spalten und 10 Int-Spalten habe, dann ja, die Floats werden sehr billig sein. Aber wenn Sie die Ints ausführen, wird jedes Mal, wenn Sie eine weitere Spalte mit Ints hinzufügen, der gesamte int-Block neu zugewiesen, es sei denn, es gibt eine Möglichkeit, sie alle auf einmal zu tun, was mir fehlt.

Die Lösung, die Sie mir zuvor gegeben haben, ist kurz vor der Arbeit, ich kann es anscheinend nicht für DatetimeIndex schaffen.

Ich bin mir nicht sicher, was Sie mit dem leeren Konstruktor meinen, aber wenn Sie meinen, einen Datenrahmen ohne Zeilen und das gewünschte Schema zu konstruieren und Reindex aufzurufen, ist dies dieselbe Zeit wie die Erstellung mit copy=True.

Ein leerer Konstruktor würde so aussehen

dtype=np.dtype([('a', np.float64), ('b', np.int64), ('c', np.float32)])
df = pd.DataFrame(columns='abc',index=np.arange(100),dtype=dtype)

Dies würde die gleiche Ausgabe erzeugen wie

dtype=np.dtype([('a', np.float64), ('b', np.int64), ('c', np.float32)])
arr = np.empty(100, dtype=dtype)
df = pd.DataFrame.from_records(arr, index=np.arange(100))

nur es würde keine Daten kopieren.

Grundsätzlich würde der Konstruktor einen gemischten dtype für den folgenden Aufruf zulassen, der funktioniert, aber nur einen einzigen grundlegenden dtype.

df = pd.DataFrame(columns=['a','b','c'],index=np.arange(100), dtype=np.float32)

Das einzige andere _feature_ wäre, es daran zu hindern, int-Arrays mit Nullen zu füllen, was den Nebeneffekt hat, dass sie in Objekt-dtype konvertiert werden, da kein Wert für ints fehlt.

Ihr zweiter Vorschlag ist vernünftig, aber nur, wenn Sie herausfinden können, wie Kategoricals erstellt werden. Zu diesem Thema ging ich den Code durch und stellte fest, dass Kategorisierungen nicht konsolidierbar sind. Also habe ich aus einer Ahnung heraus ein Integer-Array und zwei kategoriale Serien erstellt, dann drei DataFrames erstellt und alle drei verkettet. Tatsächlich wurde keine Kopie durchgeführt, obwohl zwei der DataFrames denselben dtype hatten. Ich werde versuchen zu sehen, wie dies für den Datetime-Index funktioniert.

Die Methode from_block müsste die Konsolidierungsregeln kennen, damit sie mehrere kategoriale Typen zulässt, aber nur einen der anderen Grundtypen.

yep...das ist gar nicht so schwer....ich suche jemanden, der eine sanfte Einführung in die Interna haben möchte.....hint.hint.hint.... :)

Haha, ich bin bereit, etwas Implementierungsarbeit zu leisten, versteh mich nicht falsch. Ich werde dieses Wochenende versuchen, mir die Interna anzuschauen und ein Gefühl dafür zu bekommen, welcher Konstruktor einfacher zu implementieren ist. Zuerst muss ich mich jedoch mit einigen DatetimeIndex-Problemen befassen, die ich in einem separaten Thread habe.

@quicknir Hast du dafür eine Lösung gefunden?

Ich suche nach einer Möglichkeit, einen Mixed-Dtype-Datenrahmen kostengünstig zuzuweisen (aber nicht zu füllen), um das kopierlose Füllen der Spalten aus einer Cython-Bibliothek zu ermöglichen.

Es wäre großartig, wenn Sie bereit wären, jeden Code, den Sie haben (auch halb funktionierend), zu teilen, um mir den Einstieg zu erleichtern.

Wäre folgende Vorgehensweise sinnvoll? Ich habe die Blockierungslogik neu erstellt, indem ich mit einem Prototyp-Datenrahmen gearbeitet habe.

Welche Dtypes bedürfen einer besonderen Behandlung außer kategorialen?

Natürlich ist die Verwendung des erstellten Datenrahmens nicht sicher, bis er gefüllt ist...

import numpy as np
from pandas.core.index import _ensure_index
from pandas.core.internals import BlockManager
from pandas.core.generic import NDFrame
from pandas.core.frame import DataFrame
from pandas.core.common import CategoricalDtype
from pandas.core.categorical import Categorical
from pandas.core.index import Index

def allocate_like(df, size, keep_categories=False):
    # define axes (waiting for #939 (RangeIndex))
    axes = [df.columns.values.tolist(), Index(np.arange(size))]

    # allocate and create blocks
    blocks = []
    for block in df._data.blocks:
        # special treatment for non-ordinary block types
        if isinstance(block.dtype, CategoricalDtype):
            if keep_categories:
                categories = block.values.categories
            else:
                categories = Index([])
            values = Categorical(values=np.empty(shape=block.values.shape,
                                                 dtype=block.values.codes.dtype),
                                 categories=categories,
                                 fastpath=True)
        # ordinary block types
        else:
            new_shape = (block.values.shape[0], size)
            values = np.empty(shape=new_shape, dtype=block.dtype)

        new_block = block.make_block_same_class(values=values,
                                                placement=block.mgr_locs.as_array)
        blocks.append(new_block)

    # create block manager
    mgr = BlockManager(blocks, axes)

    # create dataframe
    return DataFrame(mgr)


# create a prototype dataframe
import pandas as pd
a = np.empty(0, dtype=('i4,i4,f4,f4,f4,a10'))
df = pd.DataFrame(a)
df['cat_col'] = pd.Series(list('abcabcdeff'), dtype="category")

# allocate an alike dataframe
df1 = allocate_like(df, size=10)

@ARF1 nicht wirklich sicher, was das Endziel ist
kannst du ein einfaches beispiel geben?

weiteres concat mit copy=False wird dies im Allgemeinen umgehen

@jreback Ich möchte eine Cython-Bibliothek verwenden, um großvolumige Daten spaltenweise aus einem komprimierten Datenspeicher zu lesen, die ich aus Leistungsgründen ohne zwischengeschaltetes Kopieren direkt in einen Datenrahmen dekomprimieren möchte.

In Anlehnung an die übliche numpy-Lösung in solchen Fällen möchte ich den Speicher für einen Datenrahmen vorab zuweisen, damit ich Zeiger auf diese zugewiesenen Speicherbereiche an meine Cython-Bibliothek übergeben kann, die dann normale c-Zeiger/c-Arrays entsprechend verwenden kann diese Speicherbereiche, um den Datenrahmen direkt ohne zwischengeschaltete Kopierschritte (oder die Erzeugung von Zwischenpython-Objekten) zu füllen. Die Option, die Datenrahmen mit mehreren Cython-Threads parallel zu freigegebenem Gil zu füllen, wäre ein Nebeneffekt.

In (vereinfachtem) Pseudocode wäre das Idom etwa so:

df = fn_to_allocate_memory()
colums = df.columns.values
column_indexes = []
for i in xrange(len(df._data.blocks)):
    column_indexes.extend(df._data.blocks[i].mgr_locs.as_array)
block_arrays = [df._data.blocks[i].values for i in len(df._data.blocks)]

some_cython_library.fill_dataframe_with_content(columns, column_indexes, block_arrays)

Macht das für Sie Sinn?

Wie ich verstehe, werden concat mit copy=False Spalten mit identischen Dtypes nicht zu Blöcken zusammenfassen, aber Operationen auf der ganzen Linie lösen dies aus - was zu dem Kopieren führt, das ich versuche zu vermeiden. Oder habe ich die interne Funktionsweise von Pandas falsch verstanden?

Obwohl ich bei der Instanziierung großer (nicht gefüllter) Datenrahmen (Faktor ~6,7) einige Fortschritte gemacht habe, bin ich immer noch weit von langweiligen Geschwindigkeiten entfernt. Nur noch ein Faktor von ~90 zu gehen...

In [157]: a = np.empty(int(1e6), dtype=('i4,i4,f4,f4,f4,a10'))

In [158]: df = pd.DataFrame(a)

In [162]: %timeit np.empty(int(1e6), dtype=('i8,i4,i4,f4,f4,f4,a10'))
1000 loops, best of 3: 247 µs per loop

In [163]: %timeit allocate_like(df, size=int(1e6))
10 loops, best of 3: 22.4 ms per loop

In [164]: %timeit pd.DataFrame(np.empty(int(1e6), dtype=('i4,i4,f4,f4,f4,a10')))

10 loops, best of 3: 150 ms per loop

Eine weitere Hoffnung war, dass dieser Ansatz auch eine schnellere wiederholte Instanziierung von identisch geformten DataFrames ermöglichen könnte, wenn häufig kleine Datenmengen gelesen werden. Das war bisher nicht das Hauptziel, aber versehentlich bin ich damit besser vorangekommen: nur ein Faktor von ~4,8, um auf taube Geschwindigkeit zu kommen.

In [157]: a = np.empty(int(1e6), dtype=('i4,i4,f4,f4,f4,a10'))

In [158]: df = pd.DataFrame(a)

In [159]: %timeit np.empty(0, dtype=('i8,i4,i4,f4,f4,f4,a10'))
10000 loops, best of 3: 79.9 µs per loop

In [160]: %timeit allocate_like(df, size=0)
1000 loops, best of 3: 379 µs per loop

In [161]: %timeit pd.DataFrame(np.empty(0, dtype=('i4,i4,f4,f4,f4,a10')))
1000 loops, best of 3: 983 µs per loop

Bearbeiten

Die obigen Timings zeichnen ein viel zu pessimistisches Bild, wenn sie Äpfel mit Orangen vergleichen: Während die numpy-String-Spalte als native Strings mit fester Länge erstellt wird, wird die entsprechende Spalte in Pandas als Python-Objektarray erstellt. Der Vergleich von gleich zu gleich treibt die Instanziierung von DataFrames auf nueppige Geschwindigkeiten, mit Ausnahme der Indexgenerierung, die für etwa 92 % der Instanziierungszeit verantwortlich ist.

@ARF1 Wenn Sie numpy-Geschwindigkeiten wünschen, verwenden Sie einfach numpy. Ich bin mir nicht sicher, was Sie tatsächlich tun oder was Sie in Cython tun. Die üblichen Lösungen sind, Ihre Berechnungen aufzuteilen, einzelne Dtypes an Cython zu übergeben oder einfach eine größere Maschine zu bekommen.

DataFrames tun viel mehr als nur albern, wie sie Daten beschreiben und manipulieren. Es ist nicht das, was Sie tatsächlich mit ihnen tun.

fast alle Pandas Operationen kopieren. (wie die meisten numpy Operationen), also nicht sicher, was Sie wollen.

@jreback Ich verwende derzeit numpy, aber ich habe gemischte Dtypes, die nur (praktisch) mit strukturierten Arrays behandelt werden können. Strukturierte Arrays sind jedoch von Natur aus nach Zeilen geordnet, was mit meiner typischen Analysedimension kollidiert und zu einer schlechten Leistung führt. Pandas sieht aufgrund seiner Spalten-Major-Anordnung wie die natürliche Alternative aus - wenn ich die Daten mit einer guten Geschwindigkeit in den Datenrahmen bekomme.

Die Alternative wäre natürlich die Verwendung eines Diktiers mit unterschiedlich typisierten numpy Arrays, aber das macht die Analyse mühsam, da Slicing usw. nicht mehr möglich ist.

Die üblichen Lösungen sind, Ihre Berechnungen zu teilen und einzelne dtypes an cython zu übergeben.

Das mache ich in meinem Beispiel mit der Variablen block_arrays .

oder nimm einfach eine größere Maschine.

Ein Faktor von 100+ schneller ist für mich eine kleine finanzielle Herausforderung. ;-)

@ARF1 Sie haben ein sehr seltsames Modell dafür, wie die Dinge funktionieren. Normalerweise erstellen Sie eine kleine Anzahl von Datenrahmen und bearbeiten sie dann. Die Erstellungsgeschwindigkeit ist ein winziger Bruchteil jeder echten Berechnung oder Manipulation.

@jreback : Dies ist kein seltsames Modell. Vielleicht ist es ein seltsames Modell, wenn Sie die Dinge aus einer reinen Python-Perspektive betrachten. Wenn Sie mit C++-Code arbeiten, können Sie Daten am einfachsten in Python-Objekte einlesen, indem Sie Zeiger an bereits vorhandene Python-Objekte übergeben. Wenn Sie dies in einem leistungskritischen Kontext tun, möchten Sie eine kostengünstige und stabile (im Sinne des Speicherorts) Möglichkeit, das Python-Objekt zu erstellen.

Ich bin mir ehrlich gesagt nicht sicher, warum diese Einstellung auf den Pandas-Boards üblich ist. Ich denke, es ist bedauerlich, insofern, als ich verstehe, dass Pandas ein Konstrukt auf höherer Ebene als Numpy sind, es für die Leute immer noch einfacher sein könnte, sich "auf" Pandas zu entwickeln. Der Pandas DataFrame ist bei weitem der wünschenswerteste Typ, mit dem Sie arbeiten können, wenn Sie C-Code haben, der tabellarische Daten in Python ausgeben möchte, daher scheint dies wirklich ein wichtiger Anwendungsfall zu sein.

Bitte nehmen Sie das, was ich schreibe, nicht negativ auf, wenn ich Pandas DataFrames nicht so toll finden würde, würde ich einfach numpy Records oder ähnliches verwenden und fertig.

@ARF1 : Letztendlich erinnere ich mich nicht an die Gründe, aber das Beste, was ich tun konnte, war, einen DataFrame für jeden numerischen Typ aus einem numpy-Array mit Copy=False zu erstellen und dann pandas.concat mit Copy=False erneut zu verwenden sie zu verketten. Wenn Sie einen DataFrame mit einem einzigen Typ aus einem numpy-Array erstellen, achten Sie sehr auf die Ausrichtung des numpy-Arrays. Wenn die Ausrichtung falsch ist, werden die numpy-Arrays, die jeder Spalte entsprechen, nicht trivial geschritten, und Pandas mag dies nicht und erstellen bei der ersten Gelegenheit eine Kopie. Sie können die Kategorisierungen am Ende anheften, da sie nicht konsolidiert werden und keine Kopien des Rests des Frames auslösen sollten.

Ich empfehle, einige Unit-Tests zu schreiben, die diese Operation Schritt für Schritt ausführen und die Zeiger auf die zugrunde liegenden Daten kontinuierlich erfassen (über das array_interface des zugrunde liegenden numpy-Arrays) und überprüfen, ob sie gleich sind, um sicherzustellen, dass die Kopie tatsächlich gelöscht wird. Es ist eine sehr unglückliche Entscheidung von Pandas, dass Copy/Inplace-Parameter NICHT beachtet werden müssen. Das heißt, selbst wenn Sie zB copy=False für einen DataFrame-Konstruktor setzen, führt Pandas immer noch eine Kopie durch, wenn es entscheidet, dass dies erforderlich ist, um den DataFrame zu konstruieren. Die Tatsache, dass Pandas dies tun, anstatt zu werfen, wenn Argumente nicht berücksichtigt werden können, macht das zuverlässige Schreiben von Code, der Kopien ausschließt, sehr anstrengend und erfordert extreme Methodik. Wenn Sie keine Unit-Tests schreiben, um dies zu überprüfen, können Sie später aus Versehen etwas ändern, das dazu führt, dass eine Kopie erstellt wird, und dies geschieht geräuschlos und beeinträchtigt Ihre Leistung.

@quicknir wenn du das sagst. Ich denke, Sie sollten einfach ein Profil erstellen, bevor Sie versuchen, Dinge zu optimieren. Wie ich schon sagte, und prob wird es wieder tun. Die Bauzeit sollte nichts dominieren. Wenn dies der Fall ist, verwenden Sie den DataFrame nur zum Halten von Dingen. Wenn es nicht dominiert, was ist dann das Problem?

@jreback Das schreibst du, vorausgesetzt, ich habe mich noch nicht profiliert. Tatsächlich habe ich. Wir haben C++- und Python-Code, die beide Tabellendaten aus demselben Datenformat deserialisieren. Obwohl ich erwartet hatte, dass der Python-Code etwas Overhead haben würde, dachte ich, dass der Unterschied gering sein sollte, da die Lesezeit der Festplatte dominieren sollte. Dies war nicht der Fall, bevor ich die Dinge extrem sorgfältig überarbeitete, um die Kopien zu minimieren, dauerte die Python-Version im Vergleich zum C++-Code doppelt so lange oder schlimmer und fast der gesamte Overhead lag nur in der Erstellung des DataFrame. Mit anderen Worten, es dauerte ungefähr so ​​lange, einfach einen DataFrame einer bestimmten sehr großen Größe zu erstellen, dessen Inhalt mich überhaupt nicht interessierte, wie das Lesen, Dekomprimieren und Schreiben der Daten, die mir wichtig waren, in diesen DataFrame. Das ist eine extrem schlechte Leistung.

Wenn ich ein Endbenutzer dieses Codes wäre, der bestimmte Operationen im Hinterkopf hat, wäre vielleicht das, was Sie über die nicht dominierende Konstruktion sagen, gültig. In Wirklichkeit bin ich ein Entwickler und die Endbenutzer dieses Codes sind andere Leute. Ich weiß nicht genau, was sie mit dem DataFrame machen werden, der DataFrame ist die einzige Möglichkeit, eine Darstellung der Daten auf der Festplatte im Speicher zu erhalten. Wenn sie etwas ganz Einfaches mit den Daten auf der Festplatte machen wollen, müssen sie immer noch das DataFrame-Format durchlaufen.

Offensichtlich könnte ich mehr Möglichkeiten unterstützen, an die Daten zu kommen (zB numpy-Konstrukte), aber dies würde die Verzweigung im Code stark erhöhen und die Dinge für mich als Entwickler viel schwieriger machen. Wenn es einen grundlegenden Grund dafür gäbe, warum DataFrames so langsam sein müssen, würde ich das verstehen und entscheiden, ob DataFrame, Numpy oder beides unterstützt werden soll. Aber es gibt keinen wirklichen Grund, warum es so langsam sein muss. Man könnte eine DataFrame.empty-Methode schreiben, die ein Array von Tupeln verwendet, wobei jedes Tupel den Spaltennamen und -typ sowie die Anzahl der Zeilen enthält.

Dies ist der Unterschied, den ich zwischen unterstützenden Benutzern und Bibliotheksautoren meine. Es ist einfacher, eigenen Code zu schreiben, als eine Bibliothek zu schreiben. Und es ist einfacher, dass Ihre Bibliothek nur Benutzer unterstützt, anstatt andere Bibliotheksautoren. Ich denke nur, in diesem Fall wäre eine leere Zuweisung von DataFrames eine niedrig hängende Frucht in Pandas, die das Leben von Menschen wie mir und @ARF1 einfacher machen würde.

naja wenn du eine vernünftig geprüfte dokumentierte lösung haben möchtest, ganz Ohr. pandas hat einige Benutzer/Entwickler. Das ist der Grund, warum der DataFrame so vielseitig ist und aus dem gleichen Grund viele Fehlerprüfungen und Rückschlüsse erforderlich sind. Sie können gerne sehen, was Sie wie oben beschrieben tun können.

Ich bin bereit, etwas Zeit für die Umsetzung zu investieren, aber nur, wenn einige der Panda-Entwickler einen vernünftigen Konsens über das Design haben. Wenn ich einen Pull-Request einreiche und es bestimmte Dinge gibt, die die Leute ändern möchten, ist das cool. Oder wenn ich nach zehn Stunden merke, dass es keine Möglichkeit gibt, etwas sauber zu machen, und die einzige Möglichkeit, etwas zu tun, das die Leute für anstößig halten, beinhalten kann, ist das auch cool. Aber ich finde es nicht wirklich cool, X Stunden zu verbringen und zu erfahren, dass dies nicht so nützlich ist, die Implementierung ist chaotisch, wir glauben nicht, dass es wirklich aufgeräumt werden kann, die Codebasis verkompliziert usw. Ich weiß nicht, ob Ich bin mit dieser Meinung völlig daneben, ich habe noch nie zuvor einen großen Beitrag zu einem OSS-Projekt geleistet, daher weiß ich nicht, wie es funktioniert. Es ist nur so, dass ich in meinem ersten Post damit begonnen habe, genau dieses Ding vorzuschlagen, und dann habe ich ehrlich gesagt den Eindruck von Ihnen, dass es für Pandas irgendwie "außerhalb des Rahmens" liegt.

Wenn Sie möchten, kann ich eine neue Ausgabe eröffnen, einen möglichst spezifischen Entwurfsvorschlag erstellen, und sobald es Feedback / vorläufige Genehmigung gibt, werde ich daran arbeiten, wenn ich dazu in der Lage bin.

@quicknir Das

Dies ist nicht außerhalb des Umfangs von Pandas, aber die API muss einigermaßen benutzerfreundlich sein.

Ich bin mir nicht sicher, warum es dir nicht gefallen hat

concat(list_of_arrays,axis=1,copy=False) Ich glaube, das macht genau das, was Sie wollen (und wenn nicht, dann ist nicht klar, was Sie eigentlich wollen).

Am Ende habe ich eine ähnliche Technik verwendet, aber mit einer Liste von DataFrames, die aus einem einzigen numpy-Array erstellt wurden, von denen jeder unterschiedliche Typen hat.

Zunächst einmal glaube ich, dass ich bei dieser Technik immer noch auf einige Kopien gestoßen bin. Wie gesagt, Pandas respektieren nicht immer copy=False, daher ist es sehr anstrengend zu sehen, ob Ihr Code tatsächlich kopiert oder nicht. Ich wünschte wirklich, dass Entwickler für Pandas 17 in Betracht ziehen würden, copy=True als Standard zu machen und dann copy=False auszulösen, wenn eine Kopie nicht gelöscht werden kann. Aber trotzdem.

Zweitens musste die Spalten nachträglich neu angeordnet werden. Dies war überraschend umständlich. Die einzige Möglichkeit, dies zu tun, ohne dass eine Kopie erstellt wurde, bestand darin, die Spaltennamen ursprünglich zu ganzen Zahlen zu machen, die in der gewünschten endgültigen Reihenfolge geordnet wurden. Ich habe dann eine Indexsortierung durchgeführt. Ich habe dann die Spaltennamen geändert.

Drittens stellte ich fest, dass Kopien für Zeitstempeltypen (numpy datetime64) unvermeidlich waren.

Ich habe diesen Code vor einer Weile geschrieben, also ist er nicht mehr frisch in meinem Kopf. Es ist möglich, dass ich Fehler gemacht habe, aber ich bin es ziemlich sorgfältig durchgegangen und das waren die Ergebnisse, die ich damals hatte.

Der oben angegebene Code funktioniert nicht einmal für numpy-Arrays. Es schlägt fehl mit: TypeError: kann ein Nicht-NDFrame-Objekt nicht verketten. Sie müssen sie zuerst zu DataFrames machen.

Es ist nicht so, dass mir die Lösung, die Sie hier oder oben gegeben haben, nicht gefällt. Ich muss nur noch ein einfaches sehen, das funktioniert.

@quicknir gut, mein Beispiel oben funktioniert. Bitte geben Sie genau an, was Sie tun, und ich kann versuchen, Ihnen zu helfen.

pd.concat([np.zeros((2,2))], axis=1, copy=False)

Ich bin auf Pandas 0.15.2, also hat das vielleicht in 0.16 angefangen zu funktionieren?

Bitte lesen Sie den doc-String von pd.concat . du musst eine DataFrame

btw copy=True IST die Standardeinstellung

Stimmt, das habe ich geschrieben. Das Code-Snippet, das Sie oben geschrieben haben, hatte list_of_arrays, nicht list_of_dataframes. Ich glaube jedenfalls, wir verstehen uns. Am Ende habe ich die pd.concat-Methode verwendet, aber es ist ziemlich nicht trivial, es gibt eine ganze Reihe von Fallstricken, um Leute zum Stolpern zu bringen:

1) Sie müssen eine Liste von DataFrames erstellen. Jeder DataFrame muss genau einen eindeutigen dtype haben. Sie müssen also alle verschiedenen dtypes sammeln, bevor Sie beginnen.

2) Jeder DataFrame muss aus einem einzelnen numpy-Array des gewünschten dtype, der gleichen Anzahl von Zeilen, der gewünschten Anzahl von Spalten und dem Flag order = 'F' erstellt werden; if order='C' (Standard) dann machen Pandas oft Kopien, wenn dies sonst nicht der Fall wäre.

3) Ignorieren Sie 1) für Kategorische, sie werden nicht zu einem Block zusammengefasst, damit Sie sie später anheften können.

4) Wenn Sie alle einzelnen DataFrames erstellen, sollten die Spalten mit ganzen Zahlen benannt werden, die die gewünschte Reihenfolge darstellen. Andernfalls kann die Spaltenreihenfolge möglicherweise nicht geändert werden, ohne dass Kopien ausgelöst werden.

5) Nachdem Sie Ihre DataFrame-Liste erstellt haben, verwenden Sie concat. Sie müssen sorgfältig überprüfen, ob Sie nichts vermasselt haben, da copy=False nicht ausgelöst wird, wenn eine Kopie nicht gelöscht werden kann, sondern im Hintergrund kopiert.

6) Sortieren Sie den Spaltenindex, um die gewünschte Reihenfolge zu erreichen, und benennen Sie dann die Spalten um.

Dieses Verfahren habe ich konsequent angewendet. Es ist kein Einzeiler, es gibt viele Stellen, an denen man Fehler machen kann, ich bin mir ziemlich sicher, dass es bei Zeitstempeln immer noch nicht funktioniert hat, und es gibt viel unnötigen Overhead, der vermieden werden könnte, wenn man nicht nur die Benutzeroberfläche verwendet. Wenn Sie möchten, kann ich einen Entwurf schreiben, wie diese Funktion nur mit der öffentlichen API aussieht, vielleicht kombiniert mit einigen Tests, um zu sehen, ob es wirklich Kopien gibt und für welche Dtypes.

Außerdem ist copy=False die Vorgabe zB für den DataFrame-Konstruktor. Mein Hauptpunkt ist mehr, dass eine Funktion, die ihre Argumente nicht berücksichtigen kann, eher werfen sollte, als "etwas Vernünftiges zu tun". Das heißt, wenn copy=False nicht berücksichtigt werden kann, sollte eine Ausnahme ausgelöst werden, damit der Benutzer weiß, dass er entweder andere Eingaben ändern muss, damit die Kopie eliminiert werden kann, oder dass er copy auf True ändern muss. Eine Kopie sollte niemals stillschweigend erfolgen, wenn copy=False ist, dies ist überraschender und weniger förderlich für einen leistungsbewussten Benutzer, der Fehler findet.

Sie haben hier viele Schritte, die nicht notwendig sind
Bitte zeigen Sie ein tatsächliches Beispiel, wie ich es oben getan habe

Sie verstehen, dass eine numpy-Ansicht eine Kopie durch sehr einfache Umformungsoperationen (manchmal) und andere nicht zurückgeben kann

Es wird immer nur eine weiche Garantie für das Kopieren geben, da dies normalerweise nicht ohne viel Selbstbeobachtung möglich ist, um dies zu garantieren, was per Definition den Zweck eines einfachen leistungsstarken performanten Codes verfehlt

Das Verhalten von copy=False in der DataFrame-Konstruktion stimmt mit der np.array Funktion von numpy überein (zB wenn Sie eine Liste von Arrays angeben, werden die endgültigen Daten immer kopiert).

Es scheint, dass dies eine bedauerliche Funktionslücke bei Pandas ist. IMHO, wir werden mit dem aktuellen Modell für Pandas-Interna (das Blöcke konsolidiert) nie eine zufriedenstellende Lösung haben. Leider ist dies für Pandas nicht wirklich eine Option, da Pandas immer noch für Leute funktionieren müssen, die DataFrames mit vielen Spalten erstellen.

Was wir brauchen, ist eine alternative DataFrame-Implementierung, die speziell für die Arbeit mit sauberen Daten entwickelt wurde und jede Spalte unabhängig als 1D-numpy-Arrays speichert. Dies ist dem Datenmodell in xray tatsächlich etwas ähnlich, außer dass Spalten N-dimensionale Arrays sein können.

Ich glaube, dass ein allgemeiner Hochleistungs-Pandas-Datenrahmenkonstruktor, der nur Speicherplatz zuweist, angesichts der Vielzahl verschiedener Spaltentypen, die er unterstützen müsste, nicht trivial ist.

Davon abgesehen scheint es für Bibliotheksautoren, die Pandas-Datenrahmen als Hochleistungsdatencontainer verwenden möchten, ziemlich einfach zu sein, einen Nur-Zuweisungs-Datenrahmen-Konstruktor zu implementieren, der auf die benötigten Spaltentypen beschränkt ist.

Als Inspiration kann das folgende Codesegment dienen. Es ermöglicht die Instanziierung von Nur-Zuweisungs-, nicht gefüllten Datenrahmen mit nahezu numpy Geschwindigkeiten. Beachten Sie, dass der Code PR #9977 erfordert:

import numpy as np
from pandas.core.index import _ensure_index
from pandas.core.internals import BlockManager
from pandas.core.generic import NDFrame
from pandas.core.frame import DataFrame
from pandas.core.common import CategoricalDtype
from pandas.core.categorical import Categorical
from pandas.core.index import RangeIndex

def allocate_like(df, size, keep_categories=False):
    # define axes (uses PR #9977)
    axes = [df.columns.values.tolist(), RangeIndex(size)]

    # allocate and create blocks
    blocks = []
    for block in df._data.blocks:
        # special treatment for non-ordinary block types
        if isinstance(block.dtype, CategoricalDtype):
            if keep_categories:
                categories = block.values.categories
            else:
                categories = Index([])
            values = Categorical(values=np.empty(shape=block.values.shape,
                                                 dtype=block.values.codes.dtype),
                                 categories=categories,
                                 fastpath=True)
        # ordinary block types
        else:
            new_shape = (block.values.shape[0], size)
            values = np.empty(shape=new_shape, dtype=block.dtype)

        new_block = block.make_block_same_class(values=values,
                                                placement=block.mgr_locs.as_array)
        blocks.append(new_block)

    # create block manager
    mgr = BlockManager(blocks, axes)

    # create dataframe
    return DataFrame(mgr)

Mit dem Beispielkonstruktor allocate_like() beträgt die Leistungseinbuße von cf numpy nur x2.3 (normalerweise x333) für große Arrays und x3.3 (normalerweise x8.9) für nullgroße Arrays:

In [2]: import numpy as np

In [3]: import pandas as pd

In [4]: a = np.empty(int(1e6), dtype=('i4,i4,f4,f4,f4'))

# create template-dataframe
In [5]: df = pd.DataFrame(a)

# large dataframe timings
In [6]: %timeit np.empty(int(1e6), dtype=('i4,i4,f4,f4,f4'))
1000 loops, best of 3: 212 µs per loop

In [7]: %timeit allocate_like(df, size=int(1e6))
1000 loops, best of 3: 496 µs per loop

In [8]: %timeit pd.DataFrame(np.empty(int(1e6), dtype=('i4,i4,f4,f4,f4')))
10 loops, best of 3: 70.6 ms per loop

# zero-size dataframe timing
In [9]: %timeit np.empty(0, dtype=('i4,i4,f4,f4,f4'))
10000 loops, best of 3: 108 µs per loop

In [10]: %timeit allocate_like(df, size=0)
1000 loops, best of 3: 360 µs per loop

In [11]: %timeit pd.DataFrame(np.empty(0, dtype=('i4,i4,f4,f4,f4')))
1000 loops, best of 3: 959 µs per loop

Entschuldigung, ich habe das für eine Weile aus den Augen verloren. @ARF1 , vielen Dank für das obige Codebeispiel. Sehr schön, zusammen mit den Leistungskennzahlen.

Ich bin wirklich der Meinung, dass das Erstellen einer Klasse, die dem Layout des DataFrame entspricht, ohne Daten, Code wie oben viel natürlicher und möglicherweise auch performanter macht. Diese Klasse könnte auch wiederverwendet werden, zB bei der Neuindizierung der Zeilen.

Was ich im Grunde vorschlage, ist ungefähr Folgendes: eine Klasse namens DataFrameLayout, die die Dtypes, Spaltennamen und die Spaltenreihenfolge umschließt. Es könnte zum Beispiel ein Diktat von dtype bis zu Spaltennummern (zum Ordnen) und ein separates Array mit allen Namen speichern. An diesem Layout können Sie sehen, dass eine einfache, elegante Iteration über das Diktat eine schnelle Erstellung der Blockmanager ermöglichen würde. Diese Klasse könnte dann an Stellen wie einem leeren Konstruktor oder bei Neuindizierungsvorgängen verwendet werden.

Ich denke, solche Abstraktionen sind für komplexere Daten notwendig. In gewisser Weise ist DataFrame ein zusammengesetzter Datentyp, und ein DataFrameLayout würde die genaue Art der Komposition angeben.

Übrigens denke ich, dass für Kategorische etwas Ähnliches notwendig ist; das heißt, es muss eine CategoricalType-Abstraktion geben, die die Kategorien speichert, unabhängig davon, ob sie geordnet sind oder nicht, den Typ des Backing-Arrays usw. Das heißt, alles außer den eigentlichen Daten. Wenn Sie über ein DataFrameLayout nachdenken, stellen Sie tatsächlich fest, dass alle Spalten vollständig spezifizierte Typen haben müssen, und dies ist derzeit für Categoricals problematisch.

Was halten die Leute von diesen beiden Klassen?

@quicknir Wir haben bereits eine CategoricalDtype -Klasse – ich stimme zu, dass sie auf die CategoricalType Ihnen beschriebenen

Bei der Klasse DataFrameLayout bin ich mir nicht ganz sicher. Grundsätzlich denke ich, dass wir ein alternatives, einfacheres Datenmodell für Datenrahmen verwenden könnten (ähnlich wie in R oder Julia). Es gibt ein gewisses Interesse an dieser Art von Dingen und ich vermute, dass es irgendwann in irgendeiner Form passieren wird, aber wahrscheinlich nicht in absehbarer Zeit (und vielleicht nie als Teil des Pandas-Projekts).

@quicknir ja , DataFrameLayout erfindet hier das Rad neu. Wir haben bereits eine dtype-Spezifikation, zB

In [14]: tm.makeMixedDataFrame().to_records().dtype
Out[14]: dtype([('index', '<i8'), ('A', '<f8'), ('B', '<f8'), ('C', 'O'), ('D', '<M8[ns]')])

@jreback Es erfindet das Rad nicht neu, da die dtype-Spezifikation mehrere große Probleme hat:

1) Soweit ich sehen kann, führt to_records() eine tiefe Kopie des gesamten DataFrame durch. Die Spezifikation (ich werde diesen Begriff von nun an nur noch verwenden) für den DataFrame zu bekommen, sollte billig und einfach sein.

2) Die Ausgabe von to_records ist ein numpy-Typ. Eine Implikation daraus ist, dass ich nicht sehe, wie dies jemals erweitert werden könnte, um Kategorisierungen richtig zu unterstützen.

3) Diese Methode des internen Speicherns der Spezifikation ist nicht leicht mit der Art und Weise kompatibel, wie die Daten im DataFrame gespeichert werden (dh in Blöcken wie dtype). Das Erstellen der Blöcke aus einer solchen Spezifikation erfordert viel zusätzliche Arbeit, die vermieden werden könnte, indem die Spezifikation wie von mir vorgeschlagen gespeichert wird, mit einem Diktat von dtype zu Spaltennummern. Wenn Sie einen DataFrame mit 2000 Spalten haben, wird dies teuer.

Kurz gesagt, der dtype der Datensatzdarstellung ist eher eine Problemumgehung für das Fehlen einer richtigen Spezifikation. Es fehlen einige wichtige Funktionen und die Leistung ist viel schlechter.

Es gibt viele viele Threads auf SO, die nach dieser Funktion fragen.

Mir scheint, dass all diese Probleme darauf zurückzuführen sind, dass BlockManager separate Spalten in einzelne Speicherblöcke (die 'Blöcke') konsolidiert.
Wäre nicht die einfachste Lösung, Daten nicht in Blöcke zu konsolidieren, wenn copy=False angegeben ist.

Ich habe einen nicht konsolidierenden Affen-gepatchten BlockManager:
https://stackoverflow.com/questions/45943160/can-memmap-pandas-series-what-about-a-dataframe
dass ich dieses Problem umgangen habe.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen