Pandas: Aucun moyen de construire un DataFrame de type mixte sans copie totale, solution proposée

Créé le 9 janv. 2015  ·  58Commentaires  ·  Source: pandas-dev/pandas

Après des heures à m'arracher les cheveux, je suis arrivé à la conclusion qu'il est impossible de créer un DataFrame de type mixte sans copier toutes ses données. C'est-à-dire, peu importe ce que vous faites, si vous voulez créer un DataFrame de type mixte , vous créerez inévitablement une version temporaire des données (par exemple en utilisant np.empty), et les différents constructeurs de DataFrame feront toujours des copies de cette version temporaire. Ce problème a déjà été soulevé, il y a un an : https://github.com/pydata/pandas/issues/5902.

Ceci est particulièrement terrible pour l'interopérabilité avec d'autres langages de programmation. Si vous envisagez de remplir les données dans le DataFrame à partir, par exemple, d'un appel à C, le moyen le plus simple de le faire est de loin de créer le DataFrame en python, d'obtenir des pointeurs vers les données sous-jacentes, qui sont np.arrays, et de passer ces np .arrays le long de sorte qu'ils puissent être peuplés. Dans cette situation, vous ne vous souciez tout simplement pas des données avec lesquelles le DataFrame commence, l'objectif est simplement d'allouer la mémoire afin que vous sachiez vers quoi vous copiez.

C'est aussi généralement frustrant car cela implique qu'en principe (en fonction potentiellement de la situation spécifique et des spécificités de l'implémentation, etc.), il est difficile de garantir que vous n'utiliserez pas deux fois plus de mémoire que vous le devriez vraiment.

Cela a une solution extrêmement simple qui est déjà ancrée dans la pile python quantitative : avoir une méthode analogue à numpy vide. Cela alloue l'espace, mais ne perd pas de temps à écrire ou à copier quoi que ce soit. Comme vide est déjà pris, je proposerais d'appeler la méthode from_empty. Il accepterait un index (obligatoire, le cas d'utilisation le plus courant serait de passer np.arange(N)), des colonnes (obligatoire, généralement une liste de chaînes), des types (liste de types acceptables pour les colonnes, même longueur que les colonnes). La liste des types doit inclure la prise en charge de tous les types numériques numpy (ints, floats), ainsi que des colonnes Pandas spéciales telles que DatetimeIndex et Categorical.

En prime, étant donné que la mise en œuvre est dans une méthode complètement distincte, cela n'interférera pas du tout avec l'API existante.

API Design Constructors Dtypes

Commentaire le plus utile

Il existe de nombreux threads sur SO demandant cette fonctionnalité.

Il me semble que tous ces problèmes proviennent de BlockManager consolidant des colonnes séparées en un seul bloc de mémoire (les "blocs").
La solution la plus simple ne serait-elle pas de ne pas consolider les données en blocs lorsque copy=False est spécifié.

J'ai un BlockManager patché par singe non consolidé :
https://stackoverflow.com/questions/45943160/can-memmap-pandas-series-what-about-a-dataframe
que j'avais l'habitude de contourner ce problème.

Tous les 58 commentaires

vous pouvez simplement créer un cadre vide avec un index et des colonnes
puis assignez des ndarrays - ceux-ci ne vous copieront pas tous d'un type particulier à la fois

vous pouvez les créer avec np.empty si vous le souhaitez

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

On dirait que ça me copie. À moins que le code que j'ai écrit ne corresponde à ce que vous vouliez dire, ou que la copie qui s'est produite n'est pas la copie que vous pensiez que j'essayais d'éliminer.

tu as changé le type
c'est pourquoi il a copié essayer avec un flotteur

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

Le dtype est un objet, il est donc modifié, que j'utilise un float ou un int.

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

Construire sans copie sur un type mixte peut être fait mais est assez délicat. Le problème est que certains types nécessitent une copie (par exemple un objet pour éviter les problèmes de contention de mémoire). Et la structure interne consolide différents types, donc l'ajout d'un nouveau type nécessitera une copie. Éviter une copie est assez difficile dans la plupart des cas.

Vous devez simplement créer ce dont vous avez besoin, obtenir des pointeurs vers les données, puis les écraser. Pourquoi c'est un problème?

Le problème est que pour créer ce dont j'ai besoin, je dois copier des trucs du bon type, dont je n'ai pas l'intention d'utiliser les données. Même en supposant que votre suggestion de créer un DataFrame vide n'utilise pas de RAM significative, cela ne réduit pas le coût de la copie. Si je veux créer un DataFrame de 1 gigaoctet et le remplir ailleurs, je devrai payer le coût de la copie d'un gigaoctet de déchets en mémoire, ce qui est totalement inutile. Ne voyez-vous pas cela comme un problème?

Oui, je comprends que la structure interne consolide différents types. Je ne sais pas exactement ce que vous entendez par problèmes de contention de mémoire, mais dans tous les cas, les objets ne sont pas vraiment ce qui nous intéresse ici.

En fait, bien qu'éviter les copies en général soit un problème difficile, les éviter de la manière que j'ai suggérée est assez facile car je fournis toutes les informations nécessaires dès le départ. C'est identique à la construction à partir de données, sauf qu'au lieu de déduire les types et le nombre de lignes à partir des données et de copier les données, vous spécifiez directement les types et le nombre de lignes, et faites tout le reste exactement comme vous l'auriez fait sans la copie.

Vous avez besoin d'un constructeur "vide" pour chaque type de colonne pris en charge. Pour les types numériques numpy, cela est évident, il faut un travail non nul pour Categorical, pas sûr de DatetimeIndex.

passer un dict au constructeur et copy=False devrait fonctionner

Donc cela fonctionnera. Mais vous devez être sûr que les tableaux que vous passez sont des types distincts. Et une fois que vous faites quoi que ce soit, cela pourrait copier les données sous-jacentes. Alors YMMV. vous pouvez bien sûr passer des np.empty place des uns/zéros que je suis.

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)

_Tentative initiale supprimée car ne fonctionne pas car reindex force le casting, ce qui est une "fonctionnalité" étrange._

Obligé d'utiliser 'method', ce qui rend cette tentative un peu moins satisfaisante :

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

Si vous êtes vraiment préoccupé par les performances, je ne sais pas pourquoi on n'utiliserait pas autant que possible numpy car c'est conceptuellement beaucoup plus simple.

jreback, merci pour votre solution. Cela semble fonctionner, même pour les catégoriques (ce qui m'a surpris). Si je rencontre des problèmes, je vous le ferai savoir. Je ne suis pas sûr de ce que vous entendez par : si vous faites quoi que ce soit, cela pourrait copier. Qu'est-ce que tu veux dire par quoi que ce soit ? À moins qu'il n'y ait une sémantique COW, je pense que ce que vous voyez est ce que vous obtenez en ce qui concerne les copies profondes et les copies superficielles, au moment de la construction.

Je pense toujours qu'un constructeur from_empty devrait être implémenté, et je ne pense pas que ce serait si difficile, bien que cette technique fonctionne, elle implique beaucoup de surcharge de code. En principe, cela pourrait être fait en spécifiant un seul type composite et un certain nombre de lignes.

bashtage, ces solutions écrivent toujours dans l'ensemble du DataFrame. Étant donné que l'écriture est généralement plus lente que la lecture, cela signifie au mieux qu'elle économise moins de la moitié de la surcharge en question.

Évidemment, si je n'ai pas utilisé numpy, c'est parce que les pandas ont de nombreuses fonctionnalités et capacités impressionnantes que j'aime, et je ne veux pas les abandonner. Est-ce que vous demandiez vraiment, ou insinuiez simplement que je devrais utiliser numpy si je ne veux pas prendre ce coup de performance ?

Grattez ceci, s'il vous plaît, erreur d'utilisateur et mes excuses. reindex_axis with copy=False a parfaitement fonctionné.

bashtage, ces solutions écrivent toujours dans l'ensemble du DataFrame. Étant donné que l'écriture est généralement plus lente que la lecture, cela signifie au mieux qu'elle économise moins de la moitié de la surcharge en question.

C'est vrai, mais tout ce dont vous avez besoin est un nouveau method pour reindex qui ne se remplira de rien et vous pourrez ensuite allouer un tableau typé avec des types de colonnes arbitraires sans écriture/copie.

Évidemment, si je n'ai pas utilisé numpy, c'est parce que les pandas ont de nombreuses fonctionnalités et capacités impressionnantes que j'aime, et je ne veux pas les abandonner. Est-ce que vous demandiez vraiment, ou insinuiez simplement que je devrais utiliser numpy si je ne veux pas prendre ce coup de performance ?

C'était un peu rhétorique - bien qu'il s'agisse également d'une suggestion sérieuse du point de vue des performances, car numpy permet de se rapprocher beaucoup plus facilement de l'accès aux données en tant que goutte de mémoire qui est important si vous essayez d'écrire très code haute performance. Vous pouvez toujours passer de numpy à pandas lorsque la simplicité du code est plus importante que les performances.

J'entends ce que vous dites. Je pense toujours que cela devrait faire partie de l'interface plutôt que d'une solution de contournement, mais au fur et à mesure que les solutions de contournement disparaissent, c'est une bonne solution et facile à mettre en œuvre.

Pandas met toujours l'accent sur la performance comme l'un de ses principaux objectifs. De toute évidence, il a des fonctionnalités de niveau supérieur par rapport à numpy, et celles-ci doivent être payées. Ce dont nous parlons n'a rien à voir avec ces fonctionnalités de niveau supérieur, et il n'y a aucune raison pour que l'on paie pour des copies massives dans des endroits où vous n'en avez pas besoin. Votre suggestion serait appropriée si quelqu'un craignait le coût de la configuration des colonnes, de l'index, etc., ce qui est complètement différent de cette discussion.

Je pense que vous surestimez le coût d'écriture par rapport au code d'allocation de mémoire en Python - la partie coûteuse est l'allocation de mémoire. La création de l'objet est également coûteuse.

Les deux allouent 1 Go de mémoire, un vide et un zéro.

%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 pour la remise à zéro de 150 000 000 valeurs.

Maintenant, comparez-les pour un DataFrame trivial.

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

Environ 200 fois plus lent pour trivial. Mais c'est bien pire pour les grandes baies.

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

Maintenant , il faut de 275 m - note que cela ne copie rien. Le coût réside dans la configuration de l'index, etc., ce qui est clairement très lent lorsque le tableau n'est pas trivialement grand.

Cela me semble une optimisation prématurée car les autres frais généraux dans les pandas sont si importants que le composant malloc + filliing est proche du coût de 0.

Il semble que si vous souhaitez allouer quoi que ce soit dans une boucle serrée, il doit s'agir d'un tableau numpy pour des raisons de performances.

ok, voici ce que je pense que nous devrions faire, @quicknir si vous souhaitez apporter des améliorations. 2 problèmes.

  • #4464 - il s'agit essentiellement d'autoriser un type composé dans le constructeur DataFrame , puis de faire demi-tour et d'appeler from_records() , qui peut également être appelé si le tableau transmis est un tableau rec/structuré - ceci ferait essentiellement de from_records le chemin de traitement du tableau rec/structuré
  • passez le mot-clé copy= à from_records
  • from_records peut alors utiliser le concat soln que je montre ci-dessus, plutôt que de diviser le rec-array, de les désinfecter (en série) puis de les remettre ensemble (en blocs dtype ; cette partie est fait en interne).

Ceci est légèrement non trivial mais permettrait alors de passer assez facilement un ndarray déjà créé (peut être vide) avec des types mixtes. Notez que cela ne traiterait probablement (dans une implémentation de première passe) que (int/float/string). car datetime/timedelta nécessite une désinfection spéciale et rendrait cela un peu plus compliqué.

donc @bashtage a raison du point de vue des performances. Il est très logique de simplement construire le cadre comme vous le souhaitez, puis de modifier les ndarrays (mais vous DEVEZ le faire en saisissant les blocs, sinon vous obtiendrez des copies).

Ce que je voulais dire ci-dessus, c'est ceci. Pandas regroupe tout type de type similaire (par exemple, int64, int32 sont différents) dans un "bloc" (2-d dans un cadre). Il s'agit d'un ndarray de mémoire contigu (qui est nouvellement alloué, à moins qu'il ne soit simplement passé dans lequel ne fonctionne actuellement que pour un seul dtype). Si vous faites ensuite un setitem, par exemple df['new_columns'] = 5 et que vous avez déjà un bloc int64, alors cette nouvelle colonne lui sera finalement concaténée (résultant en une nouvelle allocation de mémoire pour ce type). Si vous utilisiez une référence comme point de vue à ce sujet, elle ne sera plus valide. C'est pourquoi ce n'est pas une stratégie que vous pouvez utiliser sans peering avec les internes de DataFrame.

@bashtage yeh le gros coût est l'indice comme vous l'avez noté. un RangeIndex (voir #939) résoudrait complètement ce problème. (c'est en fait presque fait dans une branche latérale, a juste besoin d'un peu de dépoussiérage).

Même avec un RangeIndex optimisé, ce sera toujours 2 ordres de grandeur plus lent que la construction d'un tableau NumPy, ce qui est assez juste compte tenu de la nature beaucoup plus lourde et des capacités supplémentaires d'un DataFrame .

Je pense que cela ne peut être considéré que comme une fonction de commodité, et non comme un problème de performances. Il pourrait être utile d'initialiser un type mixte DataFrame ou Panel comme.

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])

ce n'est qu'un problème d'API / de commodité

d'accord la perf est vraiment un problème accessoire (et non le conducteur)

@bashtage

%timeit pd.DataFrame(np.empty((100, 1000000)))
100 boucles, meilleur de 3 : 15,6 ms par boucle

%timeit pd.DataFrame(np.empty((100, 1000000)), copy=True)
1 boucles, meilleur de 3 : 302 ms par boucle

Ainsi, la copie dans un dataframe semble prendre 20 fois plus de temps que tous les autres travaux impliqués dans la création du DataFrame, c'est-à-dire que la copie (et l'allocation supplémentaire) est 95% du temps. Les références que vous avez faites ne correspondent pas à la bonne chose. Que la copie elle-même ou l'allocation soit ce qui prend du temps n'a pas vraiment d'importance, le fait est que si je pouvais éviter les copies pour un DataFrame de type multiple comme je le peux pour un DataFrame de type unique, je pourrais gagner énormément de temps.

Votre raisonnement à deux ordres de grandeur est également trompeur. Ce n'est pas la seule opération effectuée, il existe d'autres opérations effectuées qui prennent du temps, comme les lectures de disque. À l'heure actuelle, la copie supplémentaire que je dois faire pour créer le DataFrame prend environ la moitié du temps dans mon programme simple qui lit simplement les données du disque et dans un DataFrame. Si cela prenait 1/20 ème de temps, alors la lecture du disque serait dominante (comme elle devrait l'être) et de nouvelles améliorations n'auraient presque aucun effet.

Je tiens donc à nouveau à souligner à tous les deux : c'est un vrai problème de performance.

jreback, étant donné que la stratégie de concaténation ne fonctionne pas pour les catégories, ne pensez pas que les améliorations que vous avez suggérées ci-dessus fonctionneront. Je pense qu'un meilleur point de départ serait la réindexation. Le problème en ce moment est que la réindexation fait beaucoup de choses supplémentaires. Mais en principe, un DataFrame avec zéro ligne possède toutes les informations nécessaires pour permettre la création d'un DataFrame avec le bon nombre de lignes, sans faire de travail inutile. Au fait, cela me donne vraiment l'impression que les pandas ont besoin d'un objet de schéma, mais c'est une discussion pour un autre jour.

Je pense que nous devrons accepter de ne pas être d'accord. Les DataFrames IMO ne sont pas des objets aux performances extrêmes dans l'écosystème numérique, comme le montre la différence d'ordre de grandeur entre un tableau numpy de base et une création DataFrame.

%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

À l'heure actuelle, la copie supplémentaire que je dois faire pour créer le DataFrame prend environ la moitié du temps dans mon programme simple qui lit simplement les données du disque et dans un DataFrame. Si cela prenait 1/20 ème de temps, alors la lecture du disque serait dominante (comme elle devrait l'être) et de nouvelles améliorations n'auraient presque aucun effet.

Je pense que c'est encore moins une raison de se soucier des performances de DataFrame - même si vous pouvez le rendre 100% gratuit, la durée totale du programme ne diminue que de 50%.

Je conviens qu'il est possible pour vous de faire un PR ici pour résoudre ce problème, que vous vouliez le considérer comme un problème de performance ou comme un problème de commodité. D'après mon POV, je le vois comme ce dernier puisque j'utiliserai toujours un tableau numpy quand je me soucie de la performance. Numpy fait d'autres choses comme ne pas utiliser un gestionnaire de blocs qui est relativement efficace pour certaines choses (comme agrandir le tableau en ajoutant des colonnes). mais mauvais d'autres points de vue.

Il pourrait y avoir deux options. Le premier, un constructeur vide comme dans l'exemple que j'ai donné ci-dessus. Cela ne copierait rien, mais serait probablement Null-fill pour être cohérent avec d'autres choses dans les pandas. Le remplissage nul est assez bon marché et n'est pas à l'origine du problème OMI.

L'autre serait d'avoir une méthode DataFrame.from_blocks qui prendrait les blocs préformés pour les passer directement au gestionnaire de blocs. Quelque chose comme

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))

Une méthode de ce type imposerait que les blocs aient une forme compatible, que tous les blocs aient des types uniques, ainsi que les vérifications habituelles de la forme de l'index et des colonnes. Ce type de méthode ne ferait rien aux données et les utiliserait dans le BlockManger.

@quicknir, vous essayez de combiner des choses assez compliquées. Les catégoriques n'existent pas dans numpy, ils sont plutôt un type composé comme c'est une construction de pandas. Vous devez construire et attribuer ensuite séparément (ce qui est en fait assez bon marché - ceux-ci ne sont pas combinés en blocs comme les autres types singuliers).

@bashtage soln semble raisonnable. Cela pourrait fournir quelques vérifications simples et simplement transmettre les données (et être appelé par les autres routines internes). Normalement, l'utilisateur n'a pas besoin de se préoccuper de la représentation interne. Puisque vous le voulez vraiment, vous devez en être conscient.

Cela dit, je ne sais toujours pas pourquoi vous ne créez pas simplement un cadre exactement comme vous le souhaitez. Ensuite, saisissez les pointeurs de bloc et modifiez les valeurs. Cela coûte la même mémoire, et comme le souligne @bashtage , il est assez bon marché de créer essentiellement un cadre nul (qui a tous les dtype, index, colonnes) déjà définis.

Je ne sais pas ce que vous entendez par constructeur vide, mais si vous voulez dire construire une trame de données sans lignes et le schéma souhaité et appeler la réindexation, c'est le même temps que la création avec copy=True.

Votre deuxième proposition est raisonnable, mais seulement si vous pouvez comprendre comment faire des catégoriques. A ce sujet, je parcourais le code et je me suis rendu compte que les catégoriques ne sont pas consolidables. Ainsi, par intuition, j'ai créé un tableau d'entiers et deux séries catégorielles, j'ai ensuite créé trois DataFrames et les ai concaténés tous les trois. Effectivement, il n'a pas effectué de copie même si deux des DataFrames avaient le même dtype. Je vais essayer de voir comment faire fonctionner cela pour Datetime Index.

@jreback Je ne suis toujours pas ce que vous entendez par créer le cadre exactement comme vous le souhaitez.

@quicknir pourquoi ne pas montrer un exemple de code/pseudo-code de ce que vous essayez réellement de faire.

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

Le code précédent construisait d'abord un dictionnaire de tableaux numpy, puis construisait un DataFrame à partir de cela, car il copiait tout. Environ la moitié du temps était consacré à cela. J'essaie donc de le changer pour ce schéma. Le fait est que construire df comme ci-dessus, même lorsque vous ne vous souciez pas du contenu, est extrêmement coûteux.

@quicknir dict des tableaux np nécessite beaucoup de copies.

Vous devriez simplement faire ceci :

# 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(...)

si tu fais ça par dtype ce sera pas cher

ensuite.

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

car ces valeurs de bloc sont des vues dans des tableaux numpy

La composition des types n'est pas connue à l'avance en général, et dans le cas d'utilisation le plus courant, il existe un mélange sain de floats et d'ints. Je suppose que je ne comprends pas comment cela sera bon marché, si j'ai 30 colonnes float et 10 colonnes int, alors oui, les floats seront très bon marché. Mais lorsque vous faites les ints, à moins qu'il n'y ait un moyen de les faire tous en même temps qui me manque, chaque fois que vous ajoutez une colonne supplémentaire d'ints, tout le bloc int sera réaffecté.

La solution que vous m'avez donnée précédemment est sur le point de fonctionner, je n'arrive pas à la faire fonctionner pour DatetimeIndex.

Je ne sais pas ce que vous entendez par constructeur vide, mais si vous voulez dire construire une trame de données sans lignes et le schéma souhaité et appeler la réindexation, c'est le même temps que la création avec copy=True.

Un constructeur vide ressemblerait à

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

Cela produirait le même résultat que

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))

seulement, il ne copierait pas les données.

Fondamentalement, le constructeur autoriserait un dtype mixte pour l'appel suivant qui fonctionne mais un seul dtype de base.

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

La seule autre _fonctionnalité_ serait de l'empêcher de remplir des tableaux int null, ce qui a pour effet secondaire de les convertir en type d'objet puisqu'il n'y a pas de valeur manquante pour les ints.

Votre deuxième proposition est raisonnable, mais seulement si vous pouvez comprendre comment faire des catégoriques. A ce sujet, je parcourais le code et je me suis rendu compte que les catégoriques ne sont pas consolidables. Ainsi, par intuition, j'ai créé un tableau d'entiers et deux séries catégorielles, j'ai ensuite créé trois DataFrames et les ai concaténés tous les trois. Effectivement, il n'a pas effectué de copie même si deux des DataFrames avaient le même dtype. Je vais essayer de voir comment faire fonctionner cela pour Datetime Index.

La méthode from_block devrait connaître les règles de consolidation, de sorte qu'elle autoriserait plusieurs catégoriques, mais un seul des autres types de base.

oui... ce n'est pas si difficile à faire... je cherche quelqu'un qui veut avoir une introduction douce aux internes... indice. indice. indice.... :)

Haha, je suis prêt à faire du travail de mise en œuvre, ne vous méprenez pas. J'essaierai d'examiner les composants internes ce week-end et d'avoir une idée du constructeur le plus facile à implémenter. Tout d'abord, je dois traiter certains problèmes de DatetimeIndex que je rencontre dans un fil distinct.

@quicknir Avez-vous trouvé une solution à cela?

Je cherche un moyen d'allouer à moindre coût (mais pas de remplir) une trame de données de type mixte pour permettre le remplissage sans copie des colonnes d'une bibliothèque cython.

Ce serait formidable si vous vouliez partager n'importe quel code que vous avez (même semi-fonctionnel) pour m'aider à démarrer.

Est-ce que ce qui suit serait une approche sensée? J'ai évité de recréer la logique de blocage en travaillant à partir d'un prototype de cadre de données.

Quels types ont besoin d'un traitement spécial en dehors des catégories ?

Bien sûr, l'utilisation du cadre de données créé n'est pas sûr tant qu'il n'a pas été rempli...

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 pas vraiment sûr de l'objectif final
peux-tu donner un exemple simple

concat supplémentaire avec copy=False écartera généralement cela

@jreback Je souhaite utiliser une bibliothèque cython pour lire colonne par colonne des données volumineuses à partir d'un magasin de données compressées que je souhaite décompresser directement dans une trame de données sans copie intermédiaire pour des raisons de performances.

Empruntant à la solution numpy habituelle dans de tels cas, je souhaite pré-allouer la mémoire pour une trame de données afin que je puisse passer des pointeurs vers ces régions de mémoire allouées à ma bibliothèque cython qui peut ensuite utiliser des c-pointeurs/c-arrays ordinaires correspondant à ces régions de mémoire pour remplir directement la trame de données sans étapes de copie intermédiaires (ou génération d'objets python intermédiaires). L'option de remplir les dataframes avec plusieurs threads cython en parallèle avec gil publié serait un avantage marginal.

En pseudo-code (simplifié), l'idom serait quelque chose comme :

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)

Cela a-t-il un sens pour vous ?

Si je comprends bien, concat avec copy=False ne fusionnera pas les colonnes avec des types identiques en blocs, mais les opérations en aval déclencheront cela - ce qui entraînera la copie que j'essaie d'éviter. Ou ai-je mal compris le fonctionnement interne des pandas ?

Bien que j'aie fait des progrès avec l'instanciation de grandes trames de données (non remplies) (facteur ~ 6,7), je suis encore loin des vitesses numpy. Il ne reste plus qu'un autre facteur de ~90...

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

Un autre espoir était que cette approche pourrait également permettre une instanciation répétée plus rapide de DataFrames de forme identique lorsque des données de petit volume sont lues fréquemment. Cela n'a pas été l'objectif principal jusqu'à présent mais par inadvertance j'ai mieux progressé avec cela: seulement un facteur d'environ 4,8 pour passer à la vitesse numpy.

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

Éditer

Les timings ci-dessus brossent un tableau beaucoup trop pessimiste car ils comparent les pommes aux oranges : alors que la colonne de chaîne numpy est créée en tant que chaînes natives de longueur fixe, la colonne équivalente dans les pandas sera créée en tant que tableau d'objets python. La comparaison entre semblables pousse l'instanciation DataFrame à des vitesses numpy à l'exception de la génération d'index qui est responsable d'environ 92% du temps d'instanciation.

@ ARF1 si vous voulez des vitesses numpy, utilisez simplement numpy. Je ne suis pas sûr de ce que vous faites réellement ou de ce que vous faites en cython. Les solutions habituelles sont de fragmenter vos calculs, de transmettre des types uniques à cython ou simplement d'obtenir une machine plus grosse.

Les DataFrames font bien plus que numpy sur la façon dont ils décrivent et manipulent les données. Ce n'est pas ce que vous faites réellement avec eux.

presque toutes les opérations de pandas sont copiées. (comme la plupart des opérations numpy), donc vous ne savez pas ce que vous recherchez.

@jreback J'utilise actuellement numpy mais j'ai des types mixtes qui ne peuvent (commodément) être gérés qu'avec des tableaux structurés. Les tableaux structurés sont cependant intrinsèquement ordonnés par lignes principales, ce qui entre en conflit avec ma dimension d'analyse typique, ce qui entraîne de mauvaises performances. Pandas ressemble à l'alternative naturelle en raison de son ordre de colonne majeure - si je peux obtenir les données dans la trame de données à une bonne vitesse.

Bien sûr, l'alternative serait d'utiliser un dict de tableaux numpy de types différents, mais cela rend l'analyse difficile car le découpage, etc. n'est plus possible.

Les solutions habituelles sont de fragmenter vos calculs, de passer des types uniques à cython.

C'est ce que je fais avec la variable block_arrays dans mon exemple.

ou tout simplement obtenir une plus grande machine.

Un facteur 100+ plus rapide est un peu un défi financier pour moi. ;-)

@ARF1, vous avez un modèle très étrange de la façon dont les choses fonctionnent. Généralement, vous créez un petit nombre de blocs de données, puis vous travaillez dessus. La vitesse de création est une infime fraction de tout calcul ou manipulation réel.

@jreback : ce n'est pas un modèle étrange. C'est peut-être un modèle étrange si vous voyez les choses d'un point de vue python pur. Si vous travaillez avec du code C++, le moyen le plus simple de lire des données dans des objets python est de lui transmettre des pointeurs vers des objets python préexistants. Si vous faites cela dans un contexte sensible aux performances, vous voulez un moyen peu coûteux et stable (au sens de l'emplacement de la mémoire) de créer l'objet python.

Honnêtement, je ne sais pas pourquoi cette attitude est courante sur les planches de pandas. Je pense que c'est malheureux, dans la mesure où je comprends que les pandas sont une construction de plus haut niveau que numpy, il pourrait toujours être plus facile pour les gens de se développer "au-dessus" des pandas. Le DataFrame pandas est de loin le type le plus souhaitable si vous avez du code C qui veut cracher des données tabulaires en python, donc cela semble vraiment être un cas d'utilisation important.

S'il vous plaît, ne prenez pas ce que j'écris négativement, si je ne pensais pas que les pandas DataFrames étaient si géniaux, j'utiliserais simplement des enregistrements numpy ou quelque chose comme ça et j'en finirais avec ça.

@ARF1 : En fin de compte, je ne me souviens pas des raisons, mais le mieux que j'ai pu faire était de créer un DataFrame pour chaque type numérique à partir d'un tableau numpy avec Copy=False, puis d'utiliser à nouveau pandas.concat avec Copy=False pour les concaténer. Lorsque vous créez un DataFrame de type unique à partir d'un tableau numpy, faites très attention à l'orientation du tableau numpy. Si l'orientation est incorrecte, les tableaux numpy correspondant à chaque colonne seront achalandés de manière non triviale, et les pandas n'aimeront pas cela et feront une copie à la première occasion. Vous pouvez ajouter les catégoriques à la fin, car ils ne sont pas consolidés et ne devraient déclencher aucune copie du reste du cadre.

Je recommande d'écrire des tests unitaires qui effectuent cette opération étape par étape et récupèrent continuellement les pointeurs vers les données sous-jacentes (via l'

@quicknir si vous le dites. Je pense que vous devriez simplement profiler avant d'essayer d'optimiser les choses. Comme je l'ai déjà dit, et le prob sera à nouveau. Le temps de construction ne doit rien dominer. Si c'est le cas, alors vous utilisez simplement le DataFrame pour contenir des choses, alors quel est l'intérêt de l'utiliser en premier lieu ? S'il ne domine pas, alors quel est le problème ?

@jreback Vous écrivez cela, en supposant que je n'ai pas déjà profilé. En fait, j'ai. Nous avons du code c++ et python qui désérialisent tous les deux les données tabulaires du même format de données. Alors que je m'attendais à ce que le code python ait un peu de surcharge, j'ai pensé que la différence devrait être faible, car le temps de lecture du disque devrait dominer. Ce n'était pas le cas, avant d'y aller et de retravailler extrêmement soigneusement les choses pour minimiser les copies, la version python prenait deux fois plus de temps ou pire par rapport au code C++, et presque toute la surcharge était simplement liée à la création du DataFrame. En d'autres termes, il a fallu à peu près autant de temps pour créer simplement un DataFrame d'une certaine taille très grande dont le contenu ne m'intéressait pas du tout, que pour lire, décompresser et écrire les données qui m'intéressaient dans ce DataFrame. C'est une performance extrêmement médiocre.

Si j'étais un utilisateur final de ce code avec des opérations spécifiques en tête, peut-être que ce que vous dites à propos de la construction non dominante serait valable. En réalité, je suis un développeur, et les utilisateurs finaux de ce code sont d'autres personnes. Je ne sais pas exactement ce qu'ils feront avec le DataFrame, le DataFrame est le seul moyen d'obtenir une représentation en mémoire des données sur le disque. S'ils veulent faire quelque chose de très simple avec les données sur disque, ils doivent quand même passer par le format DataFrame.

Évidemment, je pourrais prendre en charge plus de moyens d'accéder aux données (par exemple, les constructions numpy), mais cela augmenterait considérablement la création de branches dans le code et rendrait les choses beaucoup plus difficiles pour moi en tant que développeur. S'il y avait une raison fondamentale pour laquelle les DataFrames doivent être si lents, je comprendrais et déciderais de prendre en charge DataFrame, numpy ou les deux. Mais il n'y a aucune vraie raison pour laquelle il doit être si lent. On pourrait écrire une méthode DataFrame.empty qui prend un tableau de tuples où chaque tuple contient le nom et le type de colonne, et le nombre de lignes.

C'est la différence que je veux dire entre soutenir les utilisateurs et les rédacteurs de bibliothèque. Il est plus facile d'écrire son propre code que d'écrire une bibliothèque. Et il est plus facile que votre bibliothèque ne prenne en charge que les utilisateurs plutôt que les autres rédacteurs de la bibliothèque. Je pense simplement que dans ce cas, l'allocation vide de DataFrames serait un fruit à portée de main chez les pandas qui faciliterait la vie de personnes comme moi et @ARF1 .

Eh bien, si vous souhaitez avoir un soln documenté testé raisonnable, toutes les oreilles. pandas a pas mal d'utilisateurs/développeurs. C'est la raison pour laquelle le DataFrame est si polyvalent et la même raison pour laquelle il a besoin de beaucoup de vérifications d'erreurs et d'inférences. Vous êtes invités à voir ce que vous pouvez faire comme décrit ci-dessus.

Je suis prêt à consacrer du temps à la mise en œuvre, mais seulement s'il existe un consensus raisonnable sur la conception de quelques-uns des développeurs de pandas. Si je soumets une pull request et qu'il y a certaines choses que les gens veulent changer, c'est cool. Ou si je me rends compte après y avoir consacré dix heures qu'il n'y a aucun moyen de faire quelque chose proprement, et que la seule façon de le faire pourrait impliquer quelque chose que les gens pensent être répréhensible, c'est cool aussi. Mais je ne suis pas vraiment d'accord pour passer X heures et se faire dire que ce n'est pas si utile, l'implémentation est désordonnée, nous ne pensons pas que cela puisse vraiment être nettoyé, complique la base de code, etc. Je ne sais pas si Je suis loin de ce sentiment, je n'ai jamais fait de contributions majeures à un projet OSS auparavant, donc je ne sais pas comment cela fonctionne. C'est juste que dans mon post initial j'ai commencé par proposer cette chose même, et puis franchement j'ai eu l'impression de ta part que c'était en quelque sorte "hors de portée" pour les pandas.

Si vous le souhaitez, je peux ouvrir un nouveau numéro, créer une proposition de conception aussi spécifique que possible, et une fois qu'il y aura des commentaires/une approbation provisoire, je travaillerai dessus lorsque je pourrai.

@quicknir l'essentiel est qu'il doit réussir toute la suite de tests, ce qui est assez complet.

Ce n'est pas hors de portée des pandas, mais l'API doit être quelque peu conviviale.

Je ne sais pas pourquoi tu n'as pas aimé

concat(list_of_arrays,axis=1,copy=False) Je pense que cela fait exactement ce que vous voulez (et sinon, ce que vous voulez réellement n'est pas clair).

J'ai fini par utiliser une technique similaire, mais avec une liste de DataFrames créés à partir d'un seul tableau numpy, chacun de types différents.

Tout d'abord, je pense que j'ai encore rencontré quelques copies lorsque j'ai fait cette technique. Comme je l'ai dit, les pandas n'honorent pas toujours copy=False, il est donc très épuisant de voir si votre code copie ou non. Je souhaite vraiment que pour les pandas 17, les développeurs envisagent de faire de copy=True la valeur par défaut, puis de copier=False lorsqu'une copie ne peut pas être supprimée. Mais de toute façon.

Deuxièmement, un autre problème était de devoir réorganiser les colonnes par la suite. C'était étonnamment maladroit, la seule façon que j'ai pu trouver pour le faire sans qu'une copie soit faite était de faire à l'origine des noms de colonnes des entiers qui ont été classés dans l'ordre final souhaité. J'ai ensuite fait un tri d'index en place. J'ai ensuite changé les noms des colonnes.

Troisièmement, j'ai constaté que les copies étaient inévitables pour les types d'horodatage (numpy datetime64).

J'ai écrit ce code il y a quelque temps donc il n'est pas frais dans ma tête. Il est possible que j'aie fait des erreurs, mais je l'ai parcouru assez attentivement et ce sont les résultats que j'ai obtenus à l'époque.

Le code que vous donnez ci-dessus ne fonctionne même pas pour les tableaux numpy. Il échoue avec : TypeError : ne peut pas concaténer un objet non-NDFrame. Vous devez d'abord en faire des DataFrames.

Ce n'est pas que je n'aime pas la solution que vous avez donnée ici ou ci-dessus. Je n'en ai pas encore vu un simple qui fonctionne.

@quicknir bien mon exemple ci-dessus fonctionne. Veuillez fournir exactement ce que vous faites et je peux essayer de vous aider.

pd.concat([np.zeros((2,2))], axe=1, copie=Faux)

Je suis sur pandas 0.15.2, alors peut-être que cela a commencé à fonctionner en 0.16 ?

veuillez lire la doc-string de pd.concat . vous devez passer un DataFrame

d'ailleurs copy=True EST la valeur par défaut

Exact, c'est ce que j'ai écrit. L'extrait de code que vous avez écrit ci-dessus avait list_of_arrays, pas list_of_dataframes. En tout cas, je pense qu'on se comprend. J'ai fini par utiliser la méthode pd.concat, mais ce n'est pas trivial, il y a tout un tas de trucs pour faire trébucher les gens :

1) Vous devez créer une liste de DataFrames. Chaque DataFrame doit avoir exactement un type distinct. Vous devez donc rassembler tous les différents types avant de commencer.

2) Chaque DataFrame doit être créé à partir d'un seul tableau numpy du type souhaité, du même nombre de lignes, du nombre souhaité de colonnes et de l'indicateur order = 'F' ; si order='C' (par défaut), alors les pandas feront souvent des copies alors qu'ils ne le feraient pas autrement.

3) Ignorer 1) pour les catégoriques, ils ne sont pas fusionnés en un bloc, vous pouvez donc les ajouter plus tard.

4) Lorsque vous créez tous les DataFrames individuels, les colonnes doivent être nommées à l'aide d'entiers qui représentent l'ordre dans lequel vous les souhaitez. Sinon, il peut être impossible de modifier l'ordre des colonnes sans déclencher des copies.

5) Après avoir créé votre liste de DataFrames, utilisez concat. Vous devrez soigneusement vérifier que vous n'avez rien gâché, car copy=False ne lancera pas si une copie ne peut pas être élidée, mais plutôt une copie silencieuse.

6) Triez l'index des colonnes pour obtenir l'ordre souhaité, puis renommez les colonnes.

J'ai appliqué cette procédure avec rigueur. Ce n'est pas une seule ligne, il y a beaucoup d'endroits où faire des erreurs, je suis presque sûr que cela ne fonctionnait toujours pas pour les horodatages, et il y a beaucoup de frais généraux inutiles qui pourraient être éliminés en n'utilisant pas uniquement l'interface. Si vous le souhaitez, je peux rédiger un brouillon de ce à quoi ressemble cette fonction en utilisant uniquement l'API publique, peut-être combiné avec quelques tests pour voir s'il s'agit vraiment de copies élidantes, et pour quels types.

De plus, copy=False est la valeur par défaut pour, par exemple, le constructeur DataFrame. Mon point principal est plus qu'une fonction qui ne peut pas honorer ses arguments devrait lancer plutôt que "faire quelque chose de raisonnable". C'est-à-dire que si copy=False ne peut pas être honoré, une exception doit être levée afin que l'utilisateur sache qu'il doit soit changer d'autres entrées pour que l'élision de copie puisse avoir lieu, soit changer copy en True. Une copie ne devrait jamais se faire en silence lorsque copy=False, c'est plus surprenant et moins propice à un utilisateur soucieux de la performance de trouver des bogues.

vous avez beaucoup d'étapes ici qui ne sont pas nécessaires
veuillez montrer un exemple réel comme je l'ai fait ci-dessus

vous comprenez qu'une vue numpy peut renvoyer une copie par des opérations de remodelage très simples (parfois) et pas d'autres

il n'y aura jamais qu'une garantie logicielle sur la copie car ce n'est généralement pas possible sans beaucoup d'introspection pour garantir ce qui, par définition, va à l'encontre de l'objectif d'un simple code puissant et performant

Le comportement de copy=False dans la construction DataFrame est cohérent avec la fonction np.array numpy (par exemple, si vous fournissez une liste de tableaux, les données finales feront toujours une copie).

Il semble qu'il s'agisse d'un manque de fonctionnalités malheureux chez les pandas. À mon humble avis, nous n'aurons jamais de solution satisfaisante avec le modèle actuel pour les internes des pandas (qui consolide les blocs). Malheureusement, ce n'est pas vraiment une option pour les pandas, car les pandas doivent toujours fonctionner pour les personnes qui créent des DataFrames avec beaucoup de colonnes.

Ce dont nous avons besoin, c'est d'une autre implémentation DataFrame conçue spécifiquement pour travailler avec des données ordonnées qui stockent chaque colonne indépendamment sous forme de tableaux numpy 1D. C'est en fait quelque peu similaire au modèle de données dans xray , sauf que nous permettons aux colonnes d'être des tableaux à N dimensions.

Je pense qu'un constructeur général de cadres de données pandas hautes performances qui n'alloue que de l'espace n'est pas trivial compte tenu du large éventail de types de colonnes différents qu'il devrait prendre en charge.

Cela étant dit, il semble assez simple pour les rédacteurs de bibliothèques qui souhaitent utiliser des cadres de données pandas comme conteneur de données hautes performances d'implémenter un constructeur de cadres de données d'allocation uniquement limité aux types de colonnes dont ils ont besoin.

Le segment de code suivant peut servir d'inspiration. Il permet l'instanciation de trames de données non remplies d'allocation uniquement à des vitesses proches des numpy. Notez que le code nécessite PR #9977 :

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)

Avec l'exemple de constructeur allocate_like() la pénalité de performance cf numpy n'est que de x2.3 (normalement x333) pour les grands tableaux et x3.3 (normalement x8.9) pour les tableaux de taille zéro :

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

Désolé, j'ai perdu la trace pendant un moment. @ ARF1 , merci beaucoup pour l'exemple de code ci-dessus. Très bien, avec les mesures de performance.

Je pense vraiment que créer une classe qui correspond à la disposition du DataFrame, sans aucune des données, rendra le code comme ci-dessus beaucoup plus naturel, et peut-être aussi plus performant. Cette classe peut également être réutilisée, par exemple lors de la réindexation des lignes.

Ce que je propose en gros, c'est quelque chose comme ça : une classe appelée DataFrameLayout, qui encapsule les dtypes, les noms de colonnes et l'ordre des colonnes. Par exemple, il pourrait stocker un dict de dtype aux numéros de colonne (pour le classement) et un tableau séparé avec tous les noms. A partir de cette mise en page, vous pouvez voir qu'une itération simple et élégante sur le dict permettrait de créer rapidement les gestionnaires de blocs. Cette classe pourrait ensuite être utilisée à des endroits comme un constructeur vide ou dans des opérations de réindexation.

Je pense que de telles abstractions sont nécessaires pour des données plus complexes. Dans un certain sens, DataFrame est un type de données composite, et un DataFrameLayout spécifierait la nature exacte de la composition.

À propos, je pense que quelque chose de similaire est nécessaire pour les catégoriques ; c'est-à-dire qu'il doit y avoir une abstraction CategoricalType qui stocke les catégories, qu'elles soient ordonnées ou non, le type de tableau de support, etc. C'est-à-dire tout sauf les données réelles. En fait, lorsque vous pensez à un DataFrameLayout, vous vous rendez compte que toutes les colonnes doivent avoir des types entièrement spécifiés, ce qui est actuellement problématique pour les Categoricals.

Que pensent les gens de ces deux classes ?

@quicknir Nous avons déjà une classe CategoricalDtype -- je suis d'accord qu'elle pourrait être étendue à la totalité des CategoricalType vous décrivez.

Je ne suis pas tout à fait sûr de la classe DataFrameLayout . Fondamentalement, je pense que nous pourrions utiliser un modèle de données alternatif et plus simple pour les blocs de données (plus similaire à la façon dont ils sont effectués dans R ou Julia). Il y a un certain intérêt pour ce genre de chose et je soupçonne que cela finira par arriver sous une forme ou une autre, mais probablement pas de sitôt (et peut-être jamais dans le cadre du projet pandas).

@quicknir yeh, DataFrameLayout réinvente la roue ici. Nous avons déjà une spécification de type, par exemple

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

@jreback Il ne s'agit pas de réinventer la roue, car la spécification dtype présente plusieurs problèmes majeurs :

1) Autant que je sache, to_records() effectuera une copie complète de l'intégralité du DataFrame. Obtenir la spécification (j'utiliserai simplement ce terme à partir de maintenant) pour le DataFrame devrait être simple et bon marché.

2) La sortie de to_records est de type numpy. Une implication de ceci est que je ne vois pas comment cela pourrait jamais être étendu pour prendre en charge correctement les catégoriques.

3) Cette méthode de stockage interne de la spécification n'est pas facilement compatible avec la façon dont les données sont stockées à l'intérieur du DataFrame (c'est-à-dire dans des blocs de type dtype). La création des blocs à partir d'une telle spécification implique beaucoup de travail supplémentaire qui pourrait être éliminé en stockant la spécification d'une manière comme je l'ai suggéré, avec un dict de dtype aux numéros de colonne. Lorsque vous avez un DataFrame avec 2000 colonnes, cela coûtera cher.

En bref, le dtype de la représentation de l'enregistrement est plutôt une solution de contournement pour l'absence d'une spécification appropriée. Il manque plusieurs fonctionnalités clés et est beaucoup moins performant en termes de performances.

Il existe de nombreux threads sur SO demandant cette fonctionnalité.

Il me semble que tous ces problèmes proviennent de BlockManager consolidant des colonnes séparées en un seul bloc de mémoire (les "blocs").
La solution la plus simple ne serait-elle pas de ne pas consolider les données en blocs lorsque copy=False est spécifié.

J'ai un BlockManager patché par singe non consolidé :
https://stackoverflow.com/questions/45943160/can-memmap-pandas-series-what-about-a-dataframe
que j'avais l'habitude de contourner ce problème.

Cette page vous a été utile?
0 / 5 - 0 notes