Python >> Tutoriel Python >  >> Python Tag >> NumPy

Construire un cube basique avec numpy ?

Je pense numpy tableaux d'enregistrement peut être utilisé pour cette tâche, voici ma solution basée sur des tableaux d'enregistrement.

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)

Basé sur ce rec_array , compte tenu des données

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]

nous créons une instance

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

Si, par exemple, vous voulez les ventes de Roméo de Shakespeare, vous pouvez simplement le faire

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

la sortie est 46.0

ou, vous pouvez également faire

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

la sortie est 131.2


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

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)

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

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

Supposons en entrée un arr tableau :

[['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]]

Alors x_tab peut s'appeler ainsi :

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

Sortie

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

Notez que cette représentation (repr) est uniquement destinée à afficher les résultats, vous pouvez la modifier à votre guise. Ensuite, vous pouvez accéder aux cellules du cube comme suit :

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

Sortie

12.6
12.6
104.2

Notez que la majeure partie des opérations se trouve 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 sur cette ligne :

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

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ée ici. Ce qui précède est basé sur cet essentiel.

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


Voici un croquis d'une solution, évidemment vous intégreriez 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 dans la mesure où vous devez remplir un tableau à la taille maximale du plus grand nombre d'éléments différents. Sinon, le tableau est composé de zéros, alors 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 des éléments nuls.

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