File size: 9,668 Bytes
1212e16
 
 
 
 
 
 
 
 
 
 
 
 
239e124
 
1212e16
239e124
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8be2bba
239e124
 
 
 
 
 
 
1212e16
 
 
 
239e124
1212e16
 
 
 
 
 
239e124
 
1212e16
239e124
1212e16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85251d6
1212e16
85251d6
1212e16
239e124
 
 
 
 
 
1212e16
239e124
1212e16
 
 
 
 
 
239e124
1212e16
 
 
239e124
1212e16
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
import pandas as pd
import numpy as np
from scipy.sparse import csr_matrix
import gensim.downloader as api
from sentence_transformers import SentenceTransformer, util
from sklearn.feature_extraction.text import CountVectorizer
from gensim.utils import simple_preprocess
from concurrent.futures import ThreadPoolExecutor
import torch
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

from sentence_transformers import SentenceTransformer

# where we saved the models. due to not able to connect to huggingface.co, we are running the models offline mode
model_path = './models'

def cosineSimilarity(input_df, mdr_df, model): # model is the model instance, not model name
   
    # Encode the existing descriptions from the MDR dataset into embeddings
    definitionEmbeddingsB = model.encode(input_df['description'].tolist(), convert_to_tensor=True).to(device)
    definitionEmbeddingsA = model.encode(mdr_df["definition"].tolist(), convert_to_tensor=True).to(device)
    
    # Initialize a list to store results
    results = []
    
    for i, embeddingA in enumerate(definitionEmbeddingsA):
        similarities = util.pytorch_cos_sim(embeddingA, definitionEmbeddingsB)
        most_similar_idx = similarities.argmax().item()
        similarity_score = round(similarities[0][most_similar_idx].item() * 100, 1)
        result = {
            "input_name": input_df.iloc[most_similar_idx][["variable"][0]] if ["variable"] and ["variable"][0] in input_df.columns else None,
            "input_descr": input_df['description'].iloc[most_similar_idx],
            "mdr_name": mdr_df.iloc[i][["name"][0]] if ["name"] and ["name"][0] in mdr_df.columns else None,
            "mdr_descr": mdr_df["definition"].iloc[i],
            "similarity_score": similarity_score
        }
    
        results.append(result)
        
    result_df = pd.DataFrame(results)
    
    # result_df = pd.read_csv(r"C:\Users\hamme040\Documents\work\ECON\sematch\data_for_testing\results\abs_outcome_noflag.csv") # for demo, faster display
    
    return result_df


def fast_jaccard_similarity(X1, X2):
    # intersection is a sparse matrix
    intersection = X2 @ X1.T  # Fast sparse matrix multiplication
    # X1 and X2 are numpy.matrix, convert to ndarray for newer version of numpy
    # then convert X3 to back to sparse matrix
    X3 = np.asarray(X2.sum(axis=1)[:, None]) + np.asarray(X1.sum(axis=1)[None, :])
    union = csr_matrix(X3[0]) - intersection
    return intersection / union  # Keep it as a sparse matrix
    
 
def cosineJaccardSimilarity(input_df, mdr_df, model): # model is the model instance, not model name
    
    # Encode the existing descriptions from the MDR dataset into embeddings
    dfBEmbeddings = model.encode(input_df['description'].tolist(), convert_to_tensor=True).to(device)
    dfAEmbeddings = model.encode(mdr_df["definition"].tolist(), convert_to_tensor=True).to(device)
        
    # Compute cosine similarity
    similarity_matrix = util.cos_sim(dfAEmbeddings, dfBEmbeddings)
    
    dfA = mdr_df
    dfB = input_df
    
    descriptionB='description'
    descriptionA="definition"
    variableNameA=["name"]
    variableNameB=["variable"]
    rowsToPrintB=[]
    rowsToPrintA=[]
    
    num_matches = 1  # Only find 1 match per description
    top_n_match_indices = torch.argsort(similarity_matrix, dim=1, descending=True)[:, :num_matches]
    
    vectorizer = CountVectorizer(binary=True, stop_words="english")
    dfAsparse = vectorizer.fit_transform(dfA[descriptionA])
    dfBsparse = vectorizer.transform(dfB[descriptionB])
    
    cosine_weight = 0.85
    jaccard_weight = 0.15
    
    expanded_rows = []
    
    for row_idx in range(dfAsparse.shape[0]):
        row = dfA.iloc[row_idx]
    
        match_idx = top_n_match_indices[row_idx, 0].item()
    
        combined_score = 0.0
        cosine_score = 0.0
        jaccard_score_value = 0.0
        matched_mdr_definition = None
        matched_row_data = []

        if match_idx < dfB.shape[0]:
            matched_row = dfB.iloc[match_idx]
            matched_mdr_definition = matched_row[descriptionB]
            cosine_score = similarity_matrix[row_idx, match_idx].item()
            
            jaccard_score_value = np.asarray(fast_jaccard_similarity(dfAsparse[row_idx], dfBsparse[match_idx]))[0, 0] # newer version of convert matrix to array
    
            combined_score = (cosine_score * cosine_weight) + (jaccard_score_value * jaccard_weight)
    
            # Collect additional row info from datasetB
            matched_row_data = [matched_row[col] for col in rowsToPrintB]
    
        # Combine all data into a single row
        full_row = [row[col] for col in variableNameA] + [matched_row[col] for col in variableNameB] + [
            row[descriptionA],  # Original description
            matched_mdr_definition,  # Matched description
            round(combined_score * 100, 2),  # Combined score
            round(cosine_score * 100, 2),  # Cosine similarity score
            round(jaccard_score_value * 100, 2)  # Jaccard similarity score
        ]
        full_row.extend([row[col] for col in rowsToPrintA])
        full_row.extend(matched_row_data if matched_row_data else [""] * len(rowsToPrintB))
    
        expanded_rows.append(full_row)
    
    # Debugging prints to check column alignment
    expected_columns = ["VariableNameA", "VariableNameB", "descriptionA", "descriptionB", "similarity_score", "Cosine Score", "Jaccard Score"] + rowsToPrintA + rowsToPrintB
    print(f"Expected columns count: {len(expected_columns)}")
    print(f"Actual row length: {len(expanded_rows[0]) if expanded_rows else 'No data'}")
    print(expanded_rows[0])
    
    df_final = pd.DataFrame(expanded_rows, columns=expected_columns).rename(columns={'VariableNameB':'input_name',
                                                                                      'descriptionB':'input_descr',
                                                                                      'VariableNameA':'mdr_name',
                                                                                      'descriptionA':'mdr_descr'})
    
    # re-arange columns
    cols = ['input_name','input_descr','mdr_name','mdr_descr']
    cols_other = [col for col in df_final.columns if col not in cols]
    df_final = df_final[cols + cols_other ]
    
    # df_final = pd.read_csv(r"C:\Users\hamme040\Documents\work\ECON\sematch\data_for_testing\results\abs_outcome_jaccard_cosine.csv") # for demo, faster display
    
    return df_final


    

def read_clean_model(input_df, mdr_df, model_select):

    input_columns_to_check = ["variable", "description"]
    mdr_columns_to_check = ["name", "definition"]

    # drop missing, drop dups, trim the spaces around the names/vars, that mess up the display
    input_df = input_df.dropna(subset=input_columns_to_check)
    mdr_df = mdr_df.dropna(subset=mdr_columns_to_check)
    
    input_df = input_df.drop_duplicates(subset=['variable'])
    mdr_df = mdr_df.drop_duplicates(subset=['name'])
    
    input_df['variable'] = input_df['variable'].str.strip()
    mdr_df['name'] = mdr_df['name'].str.strip()
    
    # Remove place holders like TBD
    placeholder_vars = ['tbd'] # lower case
    
    for var in placeholder_vars:
        for col in mdr_columns_to_check:
            to_drop = mdr_df[mdr_df[col].str.lower()==var].index
            mdr_df = mdr_df.drop(to_drop)
    
    for var in placeholder_vars:
        for col in input_columns_to_check:
            to_drop = input_df[input_df[col].str.lower()==var].index
            input_df = input_df.drop(to_drop)

    # removed derived
    derived_data_type_list = ['_DVAL', '_DSUM']
    
    mdr_df['derived'] = 'no'
    mask_derived = (mdr_df['name'].str.contains('|'.join(derived_data_type_list), case=False, na=False))
    mdr_df.loc[mask_derived, 'derived'] = 'yes'
    mdr_df = mdr_df[mdr_df['derived'] == 'no']
    
    input_df['derived'] = 'no'
    mask_derived = (input_df['variable'].str.contains('|'.join(derived_data_type_list), case=False, na=False)) 
    input_df.loc[mask_derived, 'derived'] = 'yes'
    input_df = input_df[input_df['derived'] == 'no']

    print(input_df.shape)
    print(mdr_df.shape)

    ## Model learning ...
    # model = SentenceTransformer('all-MiniLM-L6-v2').to(device)
    # mannually load the model due to not able to connect to huggingface.co at running time from census laptop :
    model = SentenceTransformer(f'{model_path}/all-MiniLM-L6-v2').to(device)
    
    if model_select == 'Cosine':
        result_df = cosineSimilarity(input_df, mdr_df, model)
    elif model_select == 'CosJaccard':
        result_df = cosineJaccardSimilarity(input_df, mdr_df, model)
    else:
        result_df = pd.DataFrame(data=None, columns=['input_name','input_descr','mdr_name','mdr_descr'])
    
    sorted_df = result_df.sort_values(by=['similarity_score'], ascending=[False])
    # sorted_df['similarity_score'] = sorted_df['similarity_score'].apply(lambda x: str(x) + '%') not converting to string, otherwise slider function doesn't work
    sorted_df = sorted_df.drop_duplicates(subset=["input_descr", "mdr_descr"]).reset_index(drop=True)

    return sorted_df



if __name__ == '__main__':

    print('something')

    # # Load datasets 
    # file_path_input = "data/raw/ABS-MOPS Variables - December 11 2024.xlsm"
    # file_path_mdr = "data/raw/mdr Variables 1.xlsx"
    
    # input_df = pd.read_excel(file_path_input, sheet_name="Data Sheet", header=12).rename(columns={'Unnamed: 3':'Legacy Variable'})
    # mdr_df = pd.read_excel(file_path_mdr)

    # output_df = read_clean_model(input_df, mdr_df)