Erstellen Sie eine NxN-Matrix aus Pandas mit einer Spalte


11

Ich habe Datenrahmen mit jeder Zeile mit einem Listenwert.

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

Ich muss eine Punktzahl mit einer Zeile und gegen alle anderen Zeilen berechnen

Zum Beispiel:

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

Wiederholen Sie Schritt 2,3 zwischen ID 0 und ID 1,2,3, ähnlich für alle IDs.

und einen N x N Datenrahmen erstellen; wie das:

-  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

Im Moment hat mein Code nur eine for-Schleife:

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

Gibt es einen besseren Weg, dies zu tun? Kann ich einfach eine Apply-Funktion schreiben, anstatt eine For-Loop-Iteration durchzuführen? kann ich es schneller machen?


1
bearbeitet die Frage, @Babydesta
Sriram Arvind Lakshmanakumar

1
es ist nicht 6, es ist 0,6, resultant.size = 2, id.size = 3
Sriram Arvind Lakshmanakumar

Wie lang sind Ihre Daten? und wie viele Werte kommen insgesamt vor list_of_value?
Quang Hoang

Maximal 20 Werte in jeder Werteliste
Sriram Arvind Lakshmanakumar

Nicht in jedem list_of_value. Ich meine insgesamt über alle Zeilen hinweg.
Quang Hoang

Antworten:


7

Wenn Ihre Daten nicht zu groß sind, können Sie get_dummiesdie Werte codieren und eine Matrixmultiplikation durchführen:

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

Ausgabe:

          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

Update : Hier ist eine kurze Erklärung für den Code. Die Hauptidee besteht darin, die angegebenen Listen in eine Hot-Codierung umzuwandeln:

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

Sobald wir das haben, sagen , dass die Größe des Schnittpunktes der beiden Reihen, 0und 1nur ihr Punktprodukt, weil ein Zeichen für beiden Reihen gehört , wenn und nur wenn sie durch vertreten ist 1in beide.

In diesem Sinne zuerst verwenden

df.list_of_value.explode()

um jede Zelle in eine Reihe zu verwandeln und alle diese Reihen zu verketten. Ausgabe:

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

Jetzt verwenden wir pd.get_dummiesdiese Serie, um sie in einen One-Hot-codierten Datenrahmen umzuwandeln:

   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

Wie Sie sehen können, hat jeder Wert eine eigene Zeile. Da wir diese, die zu derselben ursprünglichen Zeile gehören, zu einer Zeile kombinieren möchten, können wir sie einfach durch den ursprünglichen Index summieren. Somit

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

gibt den gewünschten binär codierten Datenrahmen an. Die nächste Zeile

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

ist genau wie Ihre Logik: s.dot(s.T)Berechnet Punktprodukte durch Zeilen und .div(s.sum(1))teilt dann die Anzahl durch Zeilen.


12k Zeilen Datenrahmen
Sriram Arvind Lakshmanakumar

@SriramArvindLakshmanakumar mit 12k Zeilen, würden Sie mit 12k x 12kDatenrahmen enden. Sollte in Ordnung sein, wenn Sie ungefähr ein paar hundert eindeutige Werte haben.
Quang Hoang

könnte den Code auch erklären?
Sriram Arvind Lakshmanakumar

Sicher, aber funktioniert es?
Quang Hoang

1
@SriramArvindLakshmanakumar Vielen Dank, dass Sie meine Lösung akzeptiert haben. Eine Erklärung und eine Gedankenlogik finden Sie im Update.
Quang Hoang

3

Versuche dies

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)

Ausgabe

     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

Sie können dies auch wie folgt tun

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

Verwenden Sie das Verständnis verschachtelter Listen in der Liste der Mengen s_list. Verwenden Sie im Rahmen des Listenverständnisses die intersectionOperation, um Überlappungen zu überprüfen und die Länge jedes Ergebnisses zu ermitteln. Erstellen Sie abschließend den Datenrahmen und teilen Sie ihn durch die Länge jeder Liste 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

Falls jede Liste doppelte Werte enthält, sollten Sie collections.Counteranstelle von verwenden set. Ich habe die Beispieldaten id = 0 in ['a','a','c']und id = 1 in geändert['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

Aktualisiert

Da viele Kandidatenlösungen vorgeschlagen werden, scheint es eine gute Idee zu sein, eine Timing-Analyse durchzuführen. Ich habe einige zufällige Daten mit 12.000 Zeilen generiert, wie vom OP angefordert, wobei die 3 Elemente pro Satz beibehalten wurden, aber die Größe des verfügbaren Alphabets zum Auffüllen der Sätze erweitert wurde. Dies kann an die tatsächlichen Daten angepasst werden.

Lassen Sie mich wissen, ob Sie eine Lösung haben, die Sie testen oder aktualisieren möchten.

Installieren

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

Aktueller Gewinner

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]

Anwärter

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.

Originalbeitrag mit Lösungsdetails

Es ist möglich, dies pandasmit einem Self-Join zu tun .

Wie andere Antworten gezeigt haben, besteht der erste Schritt darin, die Daten in eine längere Form zu entpacken.

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

Aus dieser Tabelle ist es möglich, die Anzahl pro ID zu berechnen.

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

Und dann kommt die Selbstverbindung, die in der valueSpalte stattfindet. Dadurch werden IDs für jeden Schnittwert einmal gepaart, sodass die gepaarten IDs gezählt werden können, um die Schnittgrößen zu erhalten.

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

Diese beiden können dann zusammengeführt und eine Punktzahl berechnet werden.

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

Wenn Sie die Matrixform bevorzugen, ist dies mit a möglich pivot. Dies ist eine viel größere Darstellung, wenn die Daten spärlich sind.

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

Diese Lösung wird effizient mit jeder Größe von Daten arbeiten , und jede Art von Werten in Ihrem listsagt sein stroder intoder auf andere Weise, auch den repetitiven Wertes , wenn eine zu kümmern.

# 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 diesem Fall ist das Listenverständnis besser, da das Append-Attribut der Liste nicht geladen und bei jeder Iteration als Funktion aufgerufen werden muss. Mit anderen Worten und im Allgemeinen ist das Listenverständnis schneller, da das Anhalten und Fortsetzen des Funktionsrahmens oder in anderen Fällen mehrere Funktionen langsamer sind als das Erstellen einer Liste bei Bedarf.

Die Verwendung eines Listenverständnisses anstelle einer Schleife, die keine Liste erstellt, eine Liste sinnloser Werte unsinnig akkumuliert und die Liste dann wegwirft, ist häufig langsamer, da das Erstellen und Erweitern der Liste aufwändig ist.

Ergebnis:

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

Ausführungszeit:

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

Sie können die Liste in eine Menge konvertieren und mithilfe der Schnittfunktion nach Überlappungen suchen:

(Nur 1 Apply-Funktion wird verwendet, wie Sie gefragt haben :-))

(
    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

Ich würde verwenden product, um alle Kombinationen zu bekommen. Dann können wir überprüfen mit numpy.isinund 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

Zeitprobe

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

Sollte schnell sein, berücksichtigen Sie auch das Duplikat in der 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

Ja! Wir suchen hier nach einem kartesischen Produkt, das in dieser Antwort angegeben ist. Dies kann ohne eine for-Schleife oder ein Listenverständnis erreicht werden

Fügen wir unserem Datenrahmen einen neuen wiederholten Wert hinzu, dfdamit er so aussieht:

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

Weiter mit sich selbst verschmelzen

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

So sieht der zusammengeführte Frame aus:

   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]

Dann wenden wir mit jeder Zeile die gewünschte Funktion an axis=1

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

Umformen, um Werte im gewünschten Format zu erhalten

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

Hoffe das hilft :)

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.