crea una matrice NxN da un panda di colonna


11

ho un frame di dati con ogni riga con un valore di elenco.

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

devo fare un calcolo di un punteggio con una riga e contro tutte le altre righe

Ad esempio:

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

ripetere il passaggio 2,3 tra id 0 e id 1,2,3, in modo simile per tutti gli ID.

e creare un frame di dati N x N; come questo:

-  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

In questo momento il mio codice ha solo uno per il ciclo:

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

C'è un modo migliore per farlo? posso semplicemente scrivere una funzione apply invece di eseguire un'iterazione for-loop. posso renderlo più veloce?


1
ha modificato la domanda, @Babydesta
Sriram Arvind Lakshmanakumar,

1
non è 6, è 0.6, resultant.size = 2, id.size = 3
Sriram Arvind Lakshmanakumar

Quanto durano i tuoi dati? e totalmente in quanti valori si verificano list_of_value?
Quang Hoang

massimo 20 valori in ogni list_of_value
Sriram Arvind Lakshmanakumar

Non in ciascuno list_of_value. Voglio dire in totale, su tutte le file.
Quang Hoang

Risposte:


7

Se i tuoi dati non sono troppo grandi, puoi usare get_dummiesper codificare i valori ed eseguire una moltiplicazione di matrici:

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

Produzione:

          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

Aggiornamento : ecco una breve spiegazione per il codice. L'idea principale è di trasformare le liste fornite in una codifica a caldo:

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

Una volta che abbiamo quello, diciamo, la dimensione dell'intersezione delle due righe 0ed 1è solo il loro prodotto punto, perché un carattere appartiene a entrambe le righe se e solo se è rappresentato da1 in entrambi.

Con questo in mente, primo utilizzo

df.list_of_value.explode()

per trasformare ogni cella in una serie e concatenare tutte quelle serie. Produzione:

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

Ora, utilizziamo pd.get_dummiesquella serie per trasformarlo in un frame di dati con codifica a caldo:

   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

Come puoi vedere, ogni valore ha la sua riga. Dal momento che vogliamo combinare quelli appartenenti alla stessa riga originale in una riga, possiamo semplicemente sommarli per l'indice originale. così

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

fornisce il frame di dati con codifica binaria che desideriamo. La riga successiva

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

è proprio come la tua logica: s.dot(s.T)calcola i prodotti punto per righe, quindi .div(s.sum(1))divide i conteggi per righe.


Dataframe 12k righe
Sriram Arvind Lakshmanakumar

@SriramArvindLakshmanakumar con 12k righe, 12k x 12kfiniresti con un frame di dati. Dovrebbe andare bene se hai circa alcune centinaia di valori univoci.
Quang Hoang,

potresti spiegare anche il codice?
Sriram Arvind Lakshmanakumar

Certo, ma funziona?
Quang Hoang

1
@SriramArvindLakshmanakumar Grazie per aver accettato la mia soluzione. Si prega di consultare l'aggiornamento per una spiegazione e una logica di pensiero.
Quang Hoang,

3

Prova questo

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)

Produzione

     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

Puoi anche farlo come segue

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

Utilizzare la comprensione dell'elenco nidificato nell'elenco di set s_list. Nella comprensione dell'elenco, utilizzare l' intersectionoperazione per verificare la sovrapposizione e ottenere la lunghezza di ciascun risultato. Infine, costruisci il frame di dati e dividilo per la lunghezza di ciascun elenco indf.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

Nel caso in cui ci siano valori duplicati in ogni elenco, è necessario utilizzare collections.Counterinvece di set. Ho cambiato i dati di esempio id = 0 in ['a','a','c']e id = 1 in['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

aggiornato

Dato che ci sono molte soluzioni candidate proposte, sembra una buona idea fare un'analisi dei tempi. Ho generato alcuni dati casuali con 12k righe come richiesto dall'OP, mantenendo i 3 elementi per set ma espandendo la dimensione dell'alfabeto disponibile per popolare i set. Questo può essere regolato per corrispondere ai dati effettivi.

Fammi sapere se hai una soluzione che vorresti testata o aggiornata.

Impostare

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

Vincitore attuale

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]

Contenders

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.

Post originale con dettagli sulla soluzione

È possibile farlo pandascon un self-join.

Come hanno indicato altre risposte, il primo passo è decomprimere i dati in una forma più lunga.

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

Da questa tabella è possibile calcolare i conteggi per 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

E poi arriva l'auto-join, che accade sulla valuecolonna. Questo accoppia gli ID una volta per ciascun valore di intersezione, quindi gli ID accoppiati possono essere contati per ottenere le dimensioni dell'intersezione.

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

Questi due possono quindi essere uniti e un punteggio calcolato.

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

Se si preferisce il modulo matrice, ciò è possibile con a pivot. Questa sarà una rappresentazione molto più ampia se i dati sono scarsi.

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

Questa soluzione funziona in modo efficiente con qualsiasi formato di dati e di qualsiasi tipo di valori nel listdire la sua stro into in altro modo, anche curando i valori ripetitivi eventuali.

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

In questo caso, la comprensione dell'elenco funziona meglio perché non è necessario caricare l'attributo append dell'elenco e chiamarlo come funzione ad ogni iterazione. In altre parole e in generale, le comprensioni degli elenchi funzionano più rapidamente perché la sospensione e la ripresa del frame di una funzione o più funzioni in altri casi sono più lente della creazione di un elenco su richiesta.

L'uso di una comprensione dell'elenco al posto di un ciclo che non crea un elenco, accumula insensatamente un elenco di valori insignificanti e quindi butta via l'elenco, è spesso più lento a causa del sovraccarico di creare ed estendere l'elenco.

Risultato:

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

Tempo di esecuzione:

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

È possibile convertire l'elenco in un set e utilizzare la funzione di intersezione per verificare la sovrapposizione:

(come richiesto, viene utilizzata solo 1 funzione di applicazione :-))

(
    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

Userei productper ottenere tutte le combinazioni. Quindi possiamo verificare con numpy.isine 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

Campione di tempo

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

Dovrebbe essere veloce, considera anche il duplicato nell'elenco

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

Sì! Stiamo cercando un prodotto cartesiano qui, che viene fornito in questa risposta. Questo può essere ottenuto senza un ciclo for o una comprensione dell'elenco

Aggiungiamo un nuovo valore ripetuto al nostro frame di dati in dfmodo che assomigli a questo:

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

Il prossimo si fonde con se stesso

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

Ecco come appare il frame unito:

   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]

Quindi applichiamo la funzione desiderata a ciascuna riga usando axis=1

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

Rimodellando questo per ottenere valori nel formato desiderato

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

Spero che sia di aiuto :)

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.