créer une matrice NxN à partir d'une pandas de colonne


11

j'ai un dataframe avec chaque ligne ayant une valeur de liste.

id     list_of_value
0      ['a','b','c']
1      ['d','b','c']
2      ['a','b','c']
3      ['a','b','c']

je dois faire un calcul d'un score avec une ligne et contre toutes les autres lignes

Par exemple:

Step 1: Take value of id 0: ['a','b','c'],
Step 2: find the intersection between id 0 and id 1 , 
        resultant = ['b','c']
Step 3: Score Calculation => resultant.size / id.size

répétez l'étape 2,3 entre id 0 et id 1,2,3, de même pour tous les id.

et créer une trame de données N x N; tel que cela:

-  0  1    2  3
0  1  0.6  1  1
1  1  1    1  1 
2  1  1    1  1
3  1  1    1  1

À l'heure actuelle, mon code n'en a qu'une pour la boucle:

def scoreCalc(x,queryTData):
    #mathematical calculation
    commonTData = np.intersect1d(np.array(x),queryTData)
    return commonTData.size/queryTData.size

ids = list(df['feed_id'])
dfSim = pd.DataFrame()

for indexQFID in range(len(ids)):
    queryTData = np.array(df.loc[df['id'] == ids[indexQFID]]['list_of_value'].values.tolist())

    dfSim[segmentDfFeedIds[indexQFID]] = segmentDf['list_of_value'].apply(scoreCalc,args=(queryTData,))

Y a-t-il une meilleure manière de faire cela? puis-je simplement écrire une fonction d'application au lieu de faire une itération for-loop. puis-je faire plus vite?


1
a édité la question, @Babydesta
Sriram Arvind Lakshmanakumar

1
ce n'est pas 6, c'est 0,6, resultant.size = 2, id.size = 3
Sriram Arvind Lakshmanakumar

Combien de temps sont vos données? et totalement combien de valeurs se produisent dans list_of_value?
Quang Hoang

maximum de 20 valeurs dans chaque list_of_value
Sriram Arvind Lakshmanakumar

Pas dans chacun list_of_value. Je veux dire au total, sur toutes les lignes.
Quang Hoang

Réponses:


7

Si vos données ne sont pas trop grandes, vous pouvez utiliser get_dummiespour encoder les valeurs et faire une multiplication matricielle:

s = pd.get_dummies(df.list_of_value.explode()).sum(level=0)
s.dot(s.T).div(s.sum(1))

Production:

          0         1         2         3
0  1.000000  0.666667  1.000000  1.000000
1  0.666667  1.000000  0.666667  0.666667
2  1.000000  0.666667  1.000000  1.000000
3  1.000000  0.666667  1.000000  1.000000

Mise à jour : voici une brève explication du code. L'idée principale est de transformer les listes données en un codage à chaud:

   a  b  c  d
0  1  1  1  0
1  0  1  1  1
2  1  1  1  0
3  1  1  1  0

Une fois que nous avons que la taille d'intersection des deux lignes, disons, 0et 1est juste leur produit scalaire, car un personnage appartient aux deux lignes si et seulement si elle est représentée par 1les deux.

Dans cet esprit, première utilisation

df.list_of_value.explode()

pour transformer chaque cellule en une série et concaténer toutes ces séries. Production:

0    a
0    b
0    c
1    d
1    b
1    c
2    a
2    b
2    c
3    a
3    b
3    c
Name: list_of_value, dtype: object

Maintenant, nous utilisons pd.get_dummiessur cette série pour la transformer en une trame de données codée à chaud:

   a  b  c  d
0  1  0  0  0
0  0  1  0  0
0  0  0  1  0
1  0  0  0  1
1  0  1  0  0
1  0  0  1  0
2  1  0  0  0
2  0  1  0  0
2  0  0  1  0
3  1  0  0  0
3  0  1  0  0
3  0  0  1  0

Comme vous pouvez le voir, chaque valeur a sa propre ligne. Puisque nous voulons combiner ceux qui appartiennent à la même ligne d'origine à une ligne, nous pouvons simplement les additionner par l'index d'origine. Donc

s = pd.get_dummies(df.list_of_value.explode()).sum(level=0)

donne la trame de données codée binaire que nous voulons. La ligne suivante

s.dot(s.T).div(s.sum(1))

est tout comme votre logique: s.dot(s.T)calcule les produits scalaires par lignes, puis .div(s.sum(1))divise les nombres par lignes.


12k lignes de données
Sriram Arvind Lakshmanakumar

@SriramArvindLakshmanakumar avec 12k lignes, vous vous retrouveriez avec 12k x 12kdataframe. Ça devrait aller si vous avez environ quelques centaines de valeurs uniques.
Quang Hoang

pourrait aussi expliquer le code?
Sriram Arvind Lakshmanakumar

Bien sûr, mais ça marche?
Quang Hoang

1
@SriramArvindLakshmanakumar Merci d'avoir accepté ma solution. Veuillez consulter la mise à jour pour une explication et une logique de pensée.
Quang Hoang

3

Essaye ça

range_of_ids = range(len(ids))

def score_calculation(s_id1,s_id2):
    s1 = set(list(df.loc[df['id'] == ids[s_id1]]['list_of_value'])[0])
    s2 = set(list(df.loc[df['id'] == ids[s_id2]]['list_of_value'])[0])
    # Resultant calculation s1&s2
    return round(len(s1&s2)/len(s1) , 2)


dic = {indexQFID:  [score_calculation(indexQFID,ind) for ind in range_of_ids] for indexQFID in range_of_ids}
dfSim = pd.DataFrame(dic)
print(dfSim)

Production

     0        1      2       3
0   1.00    0.67    1.00    1.00
1   0.67    1.00    0.67    0.67
2   1.00    0.67    1.00    1.00
3   1.00    0.67    1.00    1.00

Vous pouvez également le faire comme suit

dic = {indexQFID:  [round(len(set(s1)&set(s2))/len(s1) , 2) for s2 in df['list_of_value']] for indexQFID,s1 in zip(df['id'],df['list_of_value']) }
dfSim = pd.DataFrame(dic)
print(dfSim)

2

Utilisez la compréhension des listes imbriquées sur la liste des ensembles s_list. Dans la compréhension de la liste, utilisez l' intersectionopération pour vérifier le chevauchement et obtenir la longueur de chaque résultat. Enfin, construisez la trame de données et divisez-la par la longueur de chaque listedf.list_of_value

s_list =  df.list_of_value.map(set)
overlap = [[len(s1 & s) for s1 in s_list] for s in s_list]

df_final = pd.DataFrame(overlap) / df.list_of_value.str.len().to_numpy()[:,None]

Out[76]:
          0         1         2         3
0  1.000000  0.666667  1.000000  1.000000
1  0.666667  1.000000  0.666667  0.666667
2  1.000000  0.666667  1.000000  1.000000
3  1.000000  0.666667  1.000000  1.000000

Dans le cas où il y a des valeurs en double dans chaque liste, vous devez utiliser à la collections.Counterplace de set. J'ai changé les données d'exemple id = 0 en ['a','a','c']et id = 1 en['d','b','a']

sample df:
id     list_of_value
0      ['a','a','c'] #changed
1      ['d','b','a'] #changed
2      ['a','b','c']
3      ['a','b','c']

from collections import Counter

c_list =  df.list_of_value.map(Counter)
c_overlap = [[sum((c1 & c).values()) for c1 in c_list] for c in c_list]

df_final = pd.DataFrame(c_overlap) / df.list_of_value.str.len().to_numpy()[:,None]


 Out[208]:
          0         1         2         3
0  1.000000  0.333333  0.666667  0.666667
1  0.333333  1.000000  0.666667  0.666667
2  0.666667  0.666667  1.000000  1.000000
3  0.666667  0.666667  1.000000  1.000000

2

Mise à jour

Puisqu'il y a beaucoup de solutions proposées, il semble que ce soit une bonne idée de faire une analyse temporelle. J'ai généré des données aléatoires avec 12k lignes comme demandé par l'OP, en gardant les 3 éléments par ensemble mais en augmentant la taille de l'alphabet disponible pour remplir les ensembles. Cela peut être ajusté pour correspondre aux données réelles.

Faites-moi savoir si vous avez une solution que vous aimeriez tester ou mettre à jour.

Installer

import pandas as pd
import random

ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'

def random_letters(n, n_letters=52):
    return random.sample(ALPHABET[:n_letters], n)

# Create 12k rows to test scaling.
df = pd.DataFrame([{'id': i, 'list_of_value': random_letters(3)} for i in range(12000)])

Gagnant actuel

def method_quang(df): 
    s = pd.get_dummies(df.list_of_value.explode()).sum(level=0) 
    return s.dot(s.T).div(s.sum(1)) 

%time method_quang(df)                                                                                                                                                                                                               
# CPU times: user 10.5 s, sys: 828 ms, total: 11.3 s
# Wall time: 11.3 s
# ...
# [12000 rows x 12000 columns]

Concurrents

def method_mcskinner(df):
    explode_df = df.set_index('id').list_of_value.explode().reset_index() 
    explode_df = explode_df.rename(columns={'list_of_value': 'value'}) 
    denom_df = explode_df.groupby('id').size().reset_index(name='denom') 
    numer_df = explode_df.merge(explode_df, on='value', suffixes=['', '_y']) 
    numer_df = numer_df.groupby(['id', 'id_y']).size().reset_index(name='numer') 
    calc_df = numer_df.merge(denom_df, on='id') 
    calc_df['score'] = calc_df['numer'] / calc_df['denom'] 
    return calc_df.pivot('id', 'id_y', 'score').fillna(0) 

%time method_mcskinner(df)
# CPU times: user 29.2 s, sys: 9.66 s, total: 38.9 s
# Wall time: 29.6 s
# ...
# [12000 rows x 12000 columns]
def method_rishab(df): 
    vals = [[len(set(val1) & set(val2)) / len(val1) for val2 in df['list_of_value']] for val1 in df['list_of_value']]
    return pd.DataFrame(columns=df['id'], data=vals)

%time method_rishab(df)                                                                                                                                                                                                              
# CPU times: user 2min 12s, sys: 4.64 s, total: 2min 17s
# Wall time: 2min 18s
# ...
# [12000 rows x 12000 columns]
def method_fahad(df): 
    ids = list(df['id']) 
    range_of_ids = range(len(ids)) 

    def score_calculation(s_id1,s_id2): 
        s1 = set(list(df.loc[df['id'] == ids[s_id1]]['list_of_value'])[0]) 
        s2 = set(list(df.loc[df['id'] == ids[s_id2]]['list_of_value'])[0]) 
        # Resultant calculation s1&s2 
        return round(len(s1&s2)/len(s1) , 2) 

    dic = {indexQFID:  [score_calculation(indexQFID,ind) for ind in range_of_ids] for indexQFID in range_of_ids} 
    return pd.DataFrame(dic) 

# Stopped manually after running for more than 10 minutes.

Message d'origine avec détails de la solution

Il est possible de le faire pandasavec une auto-jointure.

Comme d'autres réponses l'ont souligné, la première étape consiste à décompresser les données sous une forme plus longue.

explode_df = df.set_index('id').list_of_value.explode().reset_index()
explode_df = explode_df.rename(columns={'list_of_value': 'value'})
explode_df
#     id value
# 0    0     a
# 1    0     b
# 2    0     c
# 3    1     d
# 4    1     b
# ...

À partir de ce tableau, il est possible de calculer les nombres par ID.

denom_df = explode_df.groupby('id').size().reset_index(name='denom')
denom_df
#    id  denom
# 0   0      3
# 1   1      3
# 2   2      3
# 3   3      3

Et puis vient l'auto-jointure, qui se produit sur la valuecolonne. Cela associe les ID une fois pour chaque valeur d'intersection, de sorte que les ID appariés peuvent être comptés pour obtenir les tailles d'intersection.

numer_df = explode_df.merge(explode_df, on='value', suffixes=['', '_y'])
numer_df = numer_df.groupby(['id', 'id_y']).size().reset_index(name='numer')
numer_df
#     id  id_y  numer
# 0    0     0      3
# 1    0     1      2
# 2    0     2      3
# 3    0     3      3
# 4    1     0      2
# 5    1     1      3
# ...

Ces deux peuvent ensuite être fusionnés et un score calculé.

calc_df = numer_df.merge(denom_df, on='id')
calc_df['score'] = calc_df['numer'] / calc_df['denom']
calc_df
#     id  id_y  numer  denom     score
# 0    0     0      3      3  1.000000
# 1    0     1      2      3  0.666667
# 2    0     2      3      3  1.000000
# 3    0     3      3      3  1.000000
# 4    1     0      2      3  0.666667
# 5    1     1      3      3  1.000000
# ...

Si vous préférez la forme matricielle, c'est possible avec a pivot. Ce sera une représentation beaucoup plus grande si les données sont rares.

calc_df.pivot('id', 'id_y', 'score').fillna(0)
# id_y         0         1         2         3
# id                                          
# 0     1.000000  0.666667  1.000000  1.000000
# 1     0.666667  1.000000  0.666667  0.666667
# 2     1.000000  0.666667  1.000000  1.000000
# 3     1.000000  0.666667  1.000000  1.000000

1

Cette solution fonctionnera efficacement avec toute taille des données et tout type de valeurs dans votre listexemple son strou intou autrement, en prenant soin des valeurs répétitives le cas échéant.

# dummy data
df = pd.DataFrame({'id': [0, 1, 2, 3], 'list_of_value': [['a','b','c'],['d','b','c'], ['a','b','c'], ['a','b','c']]})
# calculating the target values using list comprehension
vals = [[len(set(val1) & set(val2)) / len(val1) for val2 in df['list_of_value']] for val1 in df['list_of_value']]
# new resultant Dataframe
df =  pd.DataFrame(columns=df['id'], data=vals)

Dans ce cas, la compréhension de la liste fonctionne mieux car elle n'a pas besoin de charger l'attribut append de la liste et de l'appeler en tant que fonction à chaque itération. En d'autres termes et en général, les compréhensions de liste fonctionnent plus rapidement car la suspension et la reprise du cadre d'une fonction, ou plusieurs fonctions dans d'autres cas, sont plus lentes que la création d'une liste à la demande.

Utiliser une compréhension de liste à la place d'une boucle qui ne crée pas de liste, accumuler de manière absurde une liste de valeurs dénuées de sens, puis jeter la liste, est souvent plus lent en raison de la surcharge de création et d'extension de la liste.

Résultat:

id         0         1         2         3
0   1.000000  0.666667  1.000000  1.000000
1   0.666667  1.000000  0.666667  0.666667
2   1.000000  0.666667  1.000000  1.000000
3   1.000000  0.666667  1.000000  1.000000

Temps d'exécution:

import timeit

def function():
    df = pd.DataFrame({'id': [0, 1, 2, 3], 'list_of_value': [['a','b','c'],['d','b','c'], ['a','b','c'], ['a','b','c']]})
    vals = [[len(set(val1) & set(val2)) / len(val1) for val2 in df['list_of_value']] for val1 in df['list_of_value']]
    df =  pd.DataFrame(columns=df['id'], data=vals)

print(timeit.timeit(f'{function()}', number=1000000))
# 0.010986731999999999

0

Vous pouvez convertir la liste en un ensemble et utiliser la fonction d'intersection pour vérifier le chevauchement:

(une seule fonction d'application est utilisée comme vous l'avez demandé :-))

(
    df.assign(s = df.list_of_value.apply(set))
    .pipe(lambda x: pd.DataFrame([[len(e&f)/len(e) for f in x.s] for e in x.s]))
)

    0           1           2           3
0   1.000000    0.666667    1.000000    1.000000
1   0.666667    1.000000    0.666667    0.666667
2   1.000000    0.666667    1.000000    1.000000
3   1.000000    0.666667    1.000000    1.000000

0

J'utiliserais productpour obtenir toutes les combinaisons. Ensuite, nous pouvons vérifier avec numpy.isinet numpy.mean:

from itertools import product
l = len(df)
new_df = pd.DataFrame(data = np.array(list(map(lambda arr: np.isin(*arr),
                                                product(df['list_of_value'],
                                                        repeat=2))))
                               .mean(axis=1).reshape(l,-1),
                      index = df['id'],
                      columns=df['id'])

id         0         1         2         3
id                                        
0   1.000000  0.666667  1.000000  1.000000
1   0.666667  1.000000  0.666667  0.666667
2   1.000000  0.666667  1.000000  1.000000
3   1.000000  0.666667  1.000000  1.000000

Échantillon de temps

%%timeit
l = len(df)
new_df = pd.DataFrame(data = np.array(list(map(lambda arr: np.isin(*arr),
                                                product(df['list_of_value'],
                                                        repeat=2))))
                               .mean(axis=1).reshape(l,-1),
                      index = df['id'],
                      columns=df['id'])
594 µs ± 5.05 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

0

Devrait être rapide, considérez également le doublon dans la liste

... import itertools
... from collections import Counter
... a=df.list_of_value.tolist()
... l=np.array([len(Counter(x[0]) & Counter(x[1]))for x in [*itertools.product(a,a)]]).reshape(len(df),-1)
... out=pd.DataFrame(l/df.list_of_value.str.len().values[:,None],index=df.id,columns=df.id)
... 
out
id         0         1         2         3
id                                        
0   1.000000  0.666667  1.000000  1.000000
1   0.666667  1.000000  0.666667  0.666667
2   1.000000  0.666667  1.000000  1.000000
3   1.000000  0.666667  1.000000  1.000000

0

Oui! Nous recherchons ici un produit cartésien, qui est donné dans cette réponse. Ceci peut être réalisé sans boucle for ou compréhension de liste

Ajoutons une nouvelle valeur répétée à notre bloc de données dfpour qu'elle ressemble à ceci:

df['key'] = np.repeat(1, df.shape[0])
df

  list_of_values  key
0      [a, b, c]    1
1      [d, b, c]    1
2      [a, b, c]    1
3      [a, b, c]    1

Fusionner ensuite avec lui-même

merged = pd.merge(df, df, on='key')[['list_of_values_x', 'list_of_values_y']]

Voici à quoi ressemble le cadre fusionné:

   list_of_values_x list_of_values_y
0         [a, b, c]        [a, b, c]
1         [a, b, c]        [d, b, c]
2         [a, b, c]        [a, b, c]
3         [a, b, c]        [a, b, c]
4         [d, b, c]        [a, b, c]
5         [d, b, c]        [d, b, c]
6         [d, b, c]        [a, b, c]
7         [d, b, c]        [a, b, c]
8         [a, b, c]        [a, b, c]
9         [a, b, c]        [d, b, c]
10        [a, b, c]        [a, b, c]
11        [a, b, c]        [a, b, c]
12        [a, b, c]        [a, b, c]
13        [a, b, c]        [d, b, c]
14        [a, b, c]        [a, b, c]
15        [a, b, c]        [a, b, c]

Ensuite, nous appliquons la fonction souhaitée à chaque ligne en utilisant axis=1

values = merged.apply(lambda x: np.intersect1d(x[0], x[1]).shape[0] / len(x[1]), axis=1)

Remodeler cela pour obtenir des valeurs au format souhaité

values.values.reshape(4, 4)
array([[1.        , 0.66666667, 1.        , 1.        ],
       [0.66666667, 1.        , 0.66666667, 0.66666667],
       [1.        , 0.66666667, 1.        , 1.        ],
       [1.        , 0.66666667, 1.        , 1.        ]])

J'espère que cela t'aides :)

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.