17
votes

Construire un cube de base avec numpy?

Je me demandais si numpy pouvait être utilisé pour construire le modèle de cube le plus basique où toutes les combinaisons croisées et leur valeur calculée sont stockées.

Prenons l'exemple de données suivant:

                             YEAR                  TOTAL
AUTHOR            BOOK       2000       2001         
(ALL)             (ALL)      142.8      130.6      273.4
Shakespeare       (ALL)      131.2      118.0      249.2
Dante             (ALL)      11.6       12.6       24.2
Shakespeare       Hamlet     104.2      99.0       203.2
Shakespeare       Romeo      27.0       19.0       46.0
Dante             Inferno    11.6       12.6       24.2

Et pour pouvoir construire quelque chose comme:

AUTHOR         BOOK          YEAR        SALES
Shakespeare    Hamlet        2000        104.2
Shakespeare    Hamlet        2001        99.0
Shakespeare    Romeo         2000        27.0
Shakespeare    Romeo         2001        19.0
Dante          Inferno       2000        11.6
Dante          Inferno       2001        12.6

J'espère que l'utilisation de quelque chose comme meshgrid pourrait m'amener à 75%. Fondamentalement, j'aimerais voir s'il est possible de construire une structure de toutes les valeurs précalculées avec numpy (pas pandas) pour construire une structure afin que je puisse récupérer le résultat ci-dessus de toutes les combinaisons possibles. Par souci de simplicité, considérons uniquement la SUM comme le seul calcul possible. Peut-être est un moyen roundable de demander, mais pourrait numpy être l'épine dorsale de faire cela, ou dois - je besoin d'utiliser autre chose?

Et enfin, si ce n'est pas possible dans numpy comment cela pourrait-il être stocké dans un MDA?


7 commentaires

Qu'est-ce qu'un "modèle de cube"? Y a-t-il du matériel de référence que vous pourriez lier ici?


@KarlKnechtel, il est difficile de résumer en quelques phrases, mais en gros, c'est comme un tableau croisé dynamique dans Excel, où les `` dimensions '' (telles que l'auteur, le livre et l'année) sont utilisées pour regrouper les données, puis les `` mesures '' sont utilisés pour calculer quelque chose pour chaque combinaison de dimensions. Mais en bref, c'est un "tableau croisé dynamique". Voici le wikipedia qui a un résumé assez décent: en.wikipedia.org/wiki/OLAP_cube .


numpy est conçu pour fonctionner avec des types numériques non structurés. J'aurais pensé que cela le disqualifie automatiquement de quelque chose comme ça


Cela ressemble plus à un travail pour Pandas , qui repose sur NumPy. Êtes-vous engagé envers NumPy ou est-ce que ce serait une option?


@Timus Je parie que les pandas pourraient le faire facilement, mais j'aimerais voir un niveau plus bas que celui où il n'y a pas autant d'abstractions (pandas.pivot est un bon début et je l'utilise un peu).


Je pense que vous voudrez peut-être explorer la xarray


@ norok2 cool! Vous voulez montrer un exemple dans une réponse?


4 Réponses :


4
votes

Voici une esquisse d'une solution, évidemment, vous encapsuleriez des fonctions d'assistance et des classes pour fournir une interface simple. L'idée est de mapper chaque nom unique à un index (séquentiel ici pour plus de simplicité), puis de l'utiliser comme index pour stocker la valeur dans un tableau. Il est sous-optimal en ce sens que vous devez remplir un tableau à la taille maximale du plus grand nombre d'éléments différents. Le tableau est des zéros sinon ne soyez pas inclus dans les sommes. Vous pouvez envisager des tableaux de masques et une somme de masques si vous souhaitez éviter d'ajouter zéro élément.

import numpy as np

def get_dict(x):
    return {a:i for i, a in enumerate(set(x))}

#Mapping name to unique contiguous numbers (obviously put in a fn or class)
author = 4*["Shakespeare"]+ 2*["Dante"]
book = 2*["Hamlet"] + 2*["Romeo"] + 2*["Inferno"]
year = 3*["2000", "2001"]
sales = [104.2, 99.0, 27.0, 19.0, 11.6, 12.6]

#Define dictonary of indices
d = get_dict(author)
d.update(get_dict(book))
d.update(get_dict(year)) 

#Index values to put in multi-dimension array
ai = [d[i] for i in author]
bi = [d[i] for i in book]
yi = [d[i] for i in year]

#Pad array up to maximum size
A = np.zeros([np.max(ai)+1, np.max(bi)+1, np.max(yi)+1])

#Store elements with unique name as index in 3D datacube
for n in range(len(sales)):
    i = ai[n]; j = bi[n]; k = yi[n]
    A[i,j,k] = sales[n]

#Now we can get the various sums, for example all sales
print("Total=", np.sum(A))

#All shakespeare (0)
print("All shakespeare=", np.sum(A[d["Shakespeare"],:,:]))

#All year 2001
print("All year 2001", np.sum(A[:,:,d["2001"]]))

#All Shakespeare in 2000
print("All Shakespeare in 2000", np.sum(A[d["Shakespeare"],:,d["2000"]]))


2 commentaires

approche intéressante, merci! Pouvez-vous expliquer brièvement ce que sont les variables ai , bi , yi ?


Désolé, mauvais nom. Juste des tableaux d'indices correspondant à des chaînes uniques, c'est-à-dire l'indice de l'auteur (ai), l'indice du livre (bi) et l'indice de l'année (yi). Vous utiliseriez une fonction pour générer ce hachage à partir des chaînes, mais cela doit être séquentiel ou vous obtenez des lacunes dans vos tableaux (et l'avantage des tableaux numpy sont des données contiguës)



8
votes

Je pense numpy les tableaux d'enregistrements numpy peuvent être utilisés pour cette tâche, numpy ma solution basée sur des tableaux d'enregistrements.

test.get_view([["author","==","Shakespeare"],["year","<=","2000"]])

Basé sur ce rec_array , étant donné les données

test.get_view([["author","==","Shakespeare"],["book","==","Romeo"]])

nous créons une instance

test = rec_array()
test.add_record(author,book,year,sales)

Si, par exemple, vous voulez les ventes du Romeo de Shakespeare, vous pouvez simplement le faire

author = 4*["Shakespeare"]+ 2*["Dante"]
book = 2*["Hamlet"] + 2*["Romeo"] + 2*["Inferno"]
year = 3*["2000", "2001"]
sales = [104.2, 99.0, 27.0, 19.0, 11.6, 12.6]

la sortie est 46,0

ou, vous pouvez aussi faire

class rec_array():
    
    def __init__(self,author=None,book=None,year=None,sales=None):
        self.dtype = [('author','<U20'), ('book','<U20'),('year','<U20'),('sales',float)]
        self.rec_array = np.rec.fromarrays((author,book,year,sales),dtype=self.dtype)
        
    def add_record(self,author,book,year,sales):
        new_rec = np.rec.fromarrays((author,book,year,sales),dtype=self.dtype)
        if not self.rec_array.shape == ():
            self.rec_array = np.hstack((self.rec_array,new_rec))
        else:
            self.rec_array = new_rec
    
    def get_view(self,conditions):
        """
        conditions: 
            A list of conditions, for example 
            [["author",<,"Shakespeare"],["year","<=","2000"]]
        """
        mask = np.ones(self.rec_array.shape[0]).astype(bool)
        for item in conditions:
            field,op,target = item
            field_op = "self.rec_array['%s'] %s '%s'" % (field,op,target)
            mask &= eval(field_op)
        
        selected_sales = self.rec_array['sales'][mask]
        
        return np.sum(selected_sales)

la sortie est 131,2


0 commentaires

4
votes

Pour la structure de données, vous pouvez définir la classe suivante:

np.add.at(xt, idx, values)

Fondamentalement, un wrapper léger autour d'un tableau numpy à deux dimensions. Pour calculer la tabulation croisée, vous pouvez faire quelque chose comme ceci:

12.6
12.6
104.2

Supposons en entrée un tableau arr :

print(result[('Dante', 'ALL'), 2001])
print(result[('Dante', 'Inferno'), 2001])
print(result[('Shakespeare', 'Hamlet'), 2000])

Ensuite, x_tab peut être appelé comme ceci:

array([[142.8, 130.6, 273.4],
       [ 11.6,  12.6,  24.2],
       [131.2, 118. , 249.2],
       [ 11.6,  12.6,  24.2],
       [104.2,  99. , 203.2],
       [ 27. ,  19. ,  46. ]])

Production

result = x_tab(arr[:, [0, 1]], arr[:, 2], arr[:, 3])
print(result)

Notez que cette représentation (repr) est juste dans le but d'afficher les résultats, vous pouvez la changer à votre guise. Ensuite, vous pouvez accéder aux cellules du cube comme suit:

[['Shakespeare' 'Hamlet' 2000 104.2]
 ['Shakespeare' 'Hamlet' 2001 99.0]
 ['Shakespeare' 'Romeo' 2000 27.0]
 ['Shakespeare' 'Romeo' 2001 19.0]
 ['Dante' 'Inferno' 2000 11.6]
 ['Dante' 'Inferno' 2001 12.6]]

Production

def _x_tab(rows, columns, values):
    """Function for computing the cross-tab of simple arrays"""
    unique_values_all_cols, idx = zip(*(np.unique(col, return_inverse=True) for col in [rows, columns]))

    shape_xt = [uniq_vals_col.size for uniq_vals_col in unique_values_all_cols]

    xt = np.zeros(shape_xt, dtype=np.float)
    np.add.at(xt, idx, values)

    return unique_values_all_cols, xt


def make_index(a, r):
    """Make array of tuples"""
    l = [tuple(row) for row in a[:, r]]
    return make_object_array(l)


def make_object_array(l):
    a = np.empty(len(l), dtype=object)
    a[:] = l
    return a


def fill_label(ar, le):
    """Fill missing parts with ALL label"""
    missing = tuple(["ALL"] * le)
    return [(e + missing)[:le] for e in ar]

def x_tab(rows, cols, values):
    """Main function for cross tabulation"""
    _, l_cols = rows.shape

    total_agg = []
    total_idx = []
    for i in range(l_cols + 1):
        (idx, _), agg = _x_tab(make_index(rows, list(range(i))), cols, values)
        total_idx.extend(fill_label(idx, l_cols))
        total_agg.append(agg)

    stacked_agg = np.vstack(total_agg)
    stacked_agg_total = stacked_agg.sum(axis=1).reshape(-1, 1)

    return Cube(total_idx, list(dict.fromkeys(cols)), np.concatenate((stacked_agg, stacked_agg_total), axis=1))

Notez que la plupart des opérations se trouvent dans la fonction _x_tab, qui utilise des fonctions numpy pures. En même temps, il fournit une interface flexible pour toute fonction d'agrégation que vous choisissez, changez simplement l' ufunc à cette ligne:

class Cube:

    def __init__(self, row_index, col_index, data):
        self.row_index = {r: i for i, r in enumerate(row_index)}
        self.col_index = {c: i for i, c in enumerate(col_index)}
        self.data = data

    def __getitem__(self, item):
        row, col = item
        return self.data[self.row_index[row] , self.col_index[col]]

    def __repr__(self):
        return repr(self.data)

par tout autre de cette liste . Pour plus d'informations, consultez la documentation sur l'opérateur at .

Une copie de travail du code peut être trouvéeici . Ce qui précède est basé sur cet essentiel .

Remarque Cela suppose que vous transmettez plusieurs colonnes pour l'index (paramètre de lignes).


0 commentaires

3
votes

Juste l'initialisation de la classe:

import numpy as np

class Olap:
    def __init__(self, values, headers, *locators):
        self.labels = []
        self.indices = []
        self.headers = headers
        self.shape = (len(l) for l in locators)
        for loc in locators:
            unique, ix = np.unique(loc, return_inverse = True)
            self.labels.append(unique)
            self.indices.append(ix)
        self.arr   = np.zeros(self.shape)
        self.count = np.zeros(self.shape, dtype = int)
        np.add.at(self.arr, tuple(self.indices), values)
        np.add.at(self.count, tuple(self.indices), np.ones(values.shape))

author = 4*["Shakespeare"]+ 2*["Dante"]
book = 2*["Hamlet"] + 2*["Romeo"] + 2*["Inferno"]
year = 3*["2000", "2001"]
sales = [104.2, 99.0, 27.0, 19.0, 11.6, 12.6]


olap = Olap(sales, ["author", "book", "year"], author, book, year)

À partir de là, vous pouvez créer des fonctions de sommation en utilisant self.arr.sum() long de différents axes, et même faire la moyenne en utilisant self.count.sum() . Vous voudrez probablement un moyen d'ajouter plus de données (encore une fois en utilisant np.add.at pour les mettre dans arr ) - mais votre structure de données est maintenant Nd au lieu de tabulaire, ce qui devrait lui donner les mêmes avantages pour les données de grande dimension ce pivot fait.

Pas sur le point de mettre tout cela dans le code (même pour 400 rep) mais cela ne semble pas trop complexe une fois que vous avez créé la structure de données multidimensionnelle.


0 commentaires