utwórz macierz NxN z jednej pandy kolumnowej


11

Mam ramkę danych z każdym wierszem o wartości listy.

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

muszę obliczyć wynik dla jednego wiersza i dla wszystkich innych wierszy

Na przykład:

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

powtórz krok 2,3 między id 0 i id 1,2,3, podobnie dla wszystkich id.

i utwórz ramkę danych N x N; tak jak to:

-  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

W tej chwili mój kod ma tylko jeden dla pętli:

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

Czy jest na to lepszy sposób? czy mogę po prostu napisać jedną funkcję Apply zamiast wykonywać iterację dla pętli. czy mogę to zrobić szybciej?


1
zredagował pytanie, @Babydesta
Sriram Arvind Lakshmanakumar

1
to nie 6, to 0,6, wynikowy.size = 2, id.size = 3
Sriram Arvind Lakshmanakumar

Jak długie są twoje dane? i całkowicie w ilu wartościach występuje list_of_value?
Quang Hoang

maksymalnie 20 wartości w każdej wartości_listy
Sriram Arvind Lakshmanakumar

Nie w każdym list_of_value. Mam na myśli w sumie we wszystkich rzędach.
Quang Hoang

Odpowiedzi:


7

Jeśli dane nie są zbyt duże, możesz użyć get_dummiesdo zakodowania wartości i pomnożenia macierzy:

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

Wynik:

          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

Aktualizacja : Oto krótkie wyjaśnienie kodu. Główną ideą jest przekształcenie podanych list w kodowanie „na gorąco”:

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

Kiedy już to uzyskamy, rozmiar przecięcia dwóch rzędów, powiedzmy, 0i 1jest po prostu ich iloczynem kropkowym, ponieważ znak należy do obu wierszy wtedy i tylko wtedy, gdy jest reprezentowany przez 1oba.

Mając to na uwadze, pierwsze użycie

df.list_of_value.explode()

aby przekształcić każdą komórkę w serię i połączyć wszystkie te serie. Wynik:

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

Teraz używamy pd.get_dummiestej serii, aby przekształcić ją w ramkę danych zakodowaną na gorąco:

   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

Jak widać, każda wartość ma własny wiersz. Ponieważ chcemy połączyć te należące do tego samego oryginalnego wiersza do jednego wiersza, możemy po prostu zsumować je według oryginalnego indeksu. A zatem

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

daje kod danych binarnych, który chcemy. Następna linia

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

jest tak jak twoja logika: s.dot(s.T)oblicza iloczyn kropkowy według wierszy, a następnie .div(s.sum(1))dzieli liczby przez wiersze.


Rama danych z 12 tys. Wierszy
Sriram Arvind Lakshmanakumar

@ SriramArvindLakshmanakumar z 12k rzędami, skończyłbyś z 12k x 12kramką danych. Powinno być dobrze, jeśli masz około kilkuset unikalnych wartości.
Quang Hoang

mógłbyś również wyjaśnić kod?
Sriram Arvind Lakshmanakumar

Jasne, ale czy to działa?
Quang Hoang

1
@SriramArvindLakshmanakumar Dziękujemy za zaakceptowanie mojego rozwiązania. Proszę zobaczyć aktualizację dla wyjaśnienia i logiki myślenia.
Quang Hoang

3

Spróbuj tego

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)

Wynik

     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

Możesz to również zrobić w następujący sposób

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

Użyj funkcji zagnieżdżania listy na liście zestawów s_list. W ramach zrozumienia listy użyj intersectionoperacji, aby sprawdzić nakładanie się i uzyskać długość każdego wyniku. Na koniec skonstruuj ramkę danych i podziel ją przez długość każdej listydf.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

Jeśli na każdej liście znajdują się zduplikowane wartości, należy użyć collections.Counterzamiast set. Zmieniłem przykładowe dane id = 0 na ['a','a','c']i id = 1 na['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

Zaktualizowano

Ponieważ proponowanych jest wiele kandydujących rozwiązań, dobrym pomysłem wydaje się analiza czasowa. Wygenerowałem kilka losowych danych z 12 tys. Wierszy zgodnie z żądaniem OP, zachowując 3 elementy na zestaw, ale zwiększając rozmiar alfabetu dostępnego do zapełniania zbiorów. Można to dostosować do rzeczywistych danych.

Daj mi znać, jeśli masz rozwiązanie, które chcesz przetestować lub zaktualizować.

Ustawiać

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

Aktualny zwycięzca

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]

Zawodnicy

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.

Oryginalny post ze szczegółami rozwiązania

Można to zrobić pandasza pomocą samozłączenia.

Jak wskazały inne odpowiedzi, pierwszym krokiem jest rozpakowanie danych w dłuższą formę.

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

Z tej tabeli można obliczyć liczbę poszczególnych identyfikatorów.

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

A potem następuje samozłączenie, które dzieje się w valuekolumnie. To paruje identyfikatory raz dla każdej przecinającej się wartości, więc sparowane identyfikatory można policzyć, aby uzyskać rozmiary przecięcia.

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

Te dwa elementy można następnie połączyć i obliczyć wynik.

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

Jeśli wolisz formę macierzy, jest to możliwe dzięki pivot. Będzie to o wiele większa reprezentacja, jeśli dane będą rzadkie.

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

Takie rozwiązanie będzie działać skutecznie z dowolnego rozmiaru danych i jakiejkolwiek wartości w swojej listpowiedzieć, jego strlub intlub w inny sposób, a także dbanie o powtarzających się wartości, jeśli takie istnieją.

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

W tym przypadku zrozumienie listy działa lepiej, ponieważ nie trzeba ładować atrybutu dołączania listy i wywoływać go jako funkcję przy każdej iteracji. Innymi słowy, zrozumienie listy działa szybciej, ponieważ zawieszanie i wznawianie ramki funkcji lub wielu funkcji w innych przypadkach jest wolniejsze niż tworzenie listy na żądanie.

Używanie rozumienia listy zamiast pętli, która nie tworzy listy, nonsensowne gromadzenie listy nic nie znaczących wartości, a następnie wyrzucanie listy, jest często wolniejsze z powodu narzutu związanego z tworzeniem i rozszerzaniem listy.

Wynik:

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

Czas egzekucji:

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

Możesz przekonwertować listę na zestaw i użyć funkcji przecięcia, aby sprawdzić nakładanie się:

(użyto tylko 1 funkcji zastosuj zgodnie z zapytaniem :-))

(
    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

Użyłbym, productaby uzyskać wszystkie kombinacje. Następnie możemy sprawdzić za pomocą numpy.isini 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

Próbka czasu

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

Powinny być szybkie, weź również pod uwagę duplikat na liście

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

Tak! Szukamy tutaj produktu kartezjańskiego, który jest podany w tej odpowiedzi. Można to osiągnąć bez pętli for lub zrozumienia listy

Dodajmy nową powtarzaną wartość do naszej ramki danych, dfaby wyglądała następująco:

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

Następnie scal się ze sobą

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

Tak wygląda scalona ramka:

   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]

Następnie stosujemy pożądaną funkcję do każdego wiersza za pomocą axis=1

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

Przekształcanie tego, aby uzyskać wartości w żądanym formacie

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

Mam nadzieję że to pomoże :)

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.