Entity Matching with Similarity Maps and Deep Learning¶
The example concisely introduces the basic concepts and functionality of the package. Its goals are (i) to bring the reader up to speed with how to use the package to fit entity-matching models and (ii) to familiarize the reader with the basic concepts that are used in the documentation and naming conventions of the package. For simplicity, we use a deep learning matching model for this example. Examples of neural-symbolic matching models are given in the Neural-symbolic Entity Matching and Reasoning vignettes.
Prerequisites¶
Load the libraries we will use and set the seed for reproducibility.
from neer_match.matching_model import DLMatchingModel
from neer_match.similarity_map import SimilarityMap
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import random
import tensorflow as tf
random.seed(42)
np.random.seed(42)
tf.random.set_seed(42)
Preprocessing¶
The preprocessing stage aims to bring the data in a form compatible with
the neermatch package’s calling conventions. The package expects the
data to be in the form of two data frames, left and right, that
contain the entities to be matched. The package also expects a third
data frame, matches, that contains the indices of the matching
entities in the left and right data frames. We illustrate the
calling convention by constructing a toy example.
Left Data Set¶
The data used in this example is constructed from a subset of the
game_reviews dataset shipped with the R version of the package (see
the game_review
documentation).
The subset is created by selecting all the records in game_reviews
having titles starting with either "Metal Slug" or "Metal Gear". The
selection results are hard-coded here to keep the example code
self-contained. Following the package’s naming convention, we name the
resulting data frame left.
left = pd.DataFrame(
{
"title": [
# fmt: off
"Metal Gear Solid 4: Guns of the Patriots", "Metal Gear Solid",
"Metal Gear Solid 3: Snake Eater", "Metal Gear Solid V: The Phantom Pain",
"Metal Gear Solid Mobile", "Metal Gear Solid: Portable Ops",
"Metal Gear Solid 2: Substance", "Metal Gear Solid: The Twin Snakes",
"Metal Gear Solid", "Metal Gear Rising: Revengeance",
"Metal Gear Solid Integral", "Metal Gear Solid: Peace Walker HD Edition",
"Metal Slug 4 Mobile", "Metal Gear Rising: Revengeance",
"Metal Gear Online", "Metal Gear Acid",
"Metal Slug Anthology", "Metal Gear Solid V: Ground Zeroes",
"Metal Slug Mobile", "Metal Gear Rising: Revengeance - Jetstream",
"Metal Slug 2", "Metal Gear Solid V: Ground Zeroes",
"Metal Slug 7", "Metal Gear Solid: VR Missions",
"Metal Slug 4 & 5", "Metal Gear Rising: Revengeance - Blade Wolf",
"Metal Gear Acid 2", "Metal Slug XX",
"Metal Slug Defense", "Metal Slug 1",
"Metal Gear Solid V: Metal Gear Online", "Metal Slug Touch",
"Metal Slug X", "Metal Gear Rising: Revengeance - Jetstream",
"Metal Gear Survive", "Metal Gear",
# fmt: on
],
"platform": [
# fmt: off
"PS3", "PS", "PS2", "XONE", "MOBI", "PSP", "XBOX", "GC", "PC", "PC", "PS", "PS3",
"MOBI", "X360", "PS3", "PSP", "PS4", "PS4", "MOBI", "PS3", "IOS", "XONE", "DS", "PS",
"XBOX", "X360", "MOBI", "PSP", "IOS", "IOS", "PS4", "IOS", "IOS", "X360", "PS4", "MOBI",
# fmt: on
],
"year": [
# fmt: off
2008, 1998, 2004, 2015, 2008, 2006, 2002, 2004, 2000, 2014, 1999, 2012, 2008, 2013,
2008, 2005, 2016, 2014, 2004, 2013, 2013, 2014, 2008, 1999, 2005, 2013, 2009, 2010,
2014, 2012, 2015, 2009, 2013, 2013, 2018, 2004
# fmt: on
],
"scores": [
# fmt: off
93.53, 93.24, 91.77, 90.38, 92.50, 86.95, 86.66, 85.58, 84.22, 83.55, 90.00, 90.00,
90.00, 82.56, 80.37, 76.70, 80.00, 75.23, 75.00, 75.00, 77.00, 72.50, 72.11, 70.64,
70.47, 70.00, 70.00, 68.97, 70.00, 68.33, 67.50, 60.00, 64.00, 60.00, 61.09, 50.00
# fmt: on
],
"reviews": [
# fmt: off
85, 29, 86, 12, 2, 60, 56, 69, 18, 11, 1, 1, 1, 31, 8, 52, 1, 48, 4, 2, 5, 6, 42, 18,
33, 2, 1, 18, 6, 3, 2, 1, 5, 1, 33, 1
# fmt: on
],
"developer": [
# fmt: off
"Kojima Productions/Konami", "KCEJ/Konami", "KCEJ/Konami",
"Kojima Productions/Konami", "Ideaworks3D/Konami", "Kojima Productions/Konami",
"KCEJ/Konami", "Silicon Knights/Konami", "Digital Dialect/Konami",
"PlatinumGames/Konami", "Konami", "Kojima Productions/Konami", "SNK Playmore/I-Play",
"PlatinumGames/Konami", "Kojima Productions/Konami", "Konami",
"Terminal Reality/SNK Playmore", "Kojima Productions/Konami", "SNK Playmore/I-Play",
"PlatinumGames/Konami", "SNK Playmore", "Kojima Productions/Konami",
"SNK Playmore/Ignition Entertainment", "KCEJ/Konami", "BrezzaSoft/SNK Playmore",
"PlatinumGames/Konami", "Konami/Glu Mobile", "SNK Playmore", "SNK Playmore",
"SNK Playmore", "Kojima Productions/Konami", "SNK Playmore", "SNK Playmore",
"PlatinumGames/Konami", "Konami", "Konami Mobile & Online, Inc./Konami"
# fmt: on
],
}
)
Right Data Set¶
What is the right data frame in this example? We construct the right
data frame by copying the left data frame and introducing noise in the
title and developer columns. Up to three characters are randomly
removed from the title column, and up to two characters are randomly
removed from the developer column.
In addition, we create three duplicate matching records on the right
data frame to illustrate that the matching models of the package can be
used to link datasets with records/entities having either one-to-many or
many-to-many relations.
By construction, the left and right data frames in our examples have
the same columns. In practice, this is not always the case. For such
cases, the neermatch package supports specifying different columns of
the left and right data frames. To illustrate how the package can be
used in such cases, we rename the developer column in the right data
frame to dev.
right = left.copy()
no_duplicates = 3
right = pd.concat([right, right.sample(no_duplicates)])
tpos = right.columns.get_loc("title")
dpos = right.columns.get_loc("developer")
for r in range(right.shape[0]):
no_chars = random.randint(1, 3)
title = right.iloc[r, tpos]
right.iloc[r, tpos] = "".join(
[
l
for i, l in enumerate(title)
if i not in random.sample(range(len(title)), no_chars)
]
)
no_chars = random.randint(1, 2)
developer = right.iloc[r, dpos]
right.iloc[r, dpos] = "".join(
[
l
for i, l in enumerate(developer)
if i not in random.sample(range(len(developer)), no_chars)
]
)
right.rename(columns={"developer": "dev"}, inplace=True)
Matches Data Set¶
The matching examples are passed to the matching models as pairs of
indices. We do not need to provide non-matching examples. The models
automatically pick non-matching examples from the Cartesian product of
the left and right data frames as long as their indices are not in
the matches data frame. For this application, the matches are
constructed by the rows of the left and right data frames with the
same index and the 3 duplicate matches we created.
matches = pd.DataFrame(
{
"left": left.index.append(right.iloc[-no_duplicates:,].index),
"right": range(len(right)),
}
)
Matching Model Setup¶
A matching model initialization requires instructions on constructing
the similarity map between the left and right datasets. The
instructions are passed to the model as a named list that specifies
which fields are used from each dataset and how they are associated with each other, and
how to encode them, i.e., how to calculate the similarity values.
If the column names of the left and right are the same, it suffices
to supply the common name to the similarity map instruction list (see,
e.g., the title, platform, and year items below). If two columns
have different names, the instruction should have the form
left_name~right_name, as it is, for instance, the case for the
developing studio in this application (see item developer~dev). Not
all columns of the datasets need to be used in the instructions. We do
not use the scores and reviews columns in this example.
The model can be instructed to calculate one or more similarities for
each column association. For instance, the instructions of this example
specify two similarity calculations for the platform and year
associations and one for the title and developer~dev. The
neermatch provides a set of predefined similarity functions that can
be used in similarity maps. The string similarities and ratios are
calculated using RapidFuzz.
The complete set of predefined functions can be retrieved by calling the
available_similarities()
function.
instructions = {
"title": ["jaro_winkler"],
"platform": ["levenshtein", "discrete"],
"year": ["euclidean", "discrete"],
"developer~dev": ["jaro"]
}
similarity_map = SimilarityMap(instructions)
print(similarity_map)
SimilarityMap[
('title', 'title', 'jaro_winkler')
('platform', 'platform', 'levenshtein')
('platform', 'platform', 'discrete')
('year', 'year', 'euclidean')
('year', 'year', 'discrete')
('developer', 'dev', 'jaro')]
A matching model object is constructed by passing the similarity map instructions. The model prepares encoding operations based on the passed instructions and uses them whenever the model is fitted or evaluated. We skip the construction details of the model in this example and refer the interested reader to the documentation entries of the DLMatchingModel, RecordPairNetwork, and FieldPairNetwork classes.
model = DLMatchingModel(similarity_map)
The model is compiled in the usual (keras) way. The compile function wraps the tensorflow.keras.Model.compile function, so all the functionality and options in the latter can be used here.
model.compile(
loss = tf.keras.losses.BinaryCrossentropy(),
optimizer = tf.keras.optimizers.Adam(learning_rate = 0.001)
)
Matching Model Fit and Evaluation¶
The model is fitted using the fit function (see the fit
documentation
for details). The fit function extends the functionality of
tensorflow.keras.Model.fit
to accommodate the entity matching problem requirements. Firstly,
instead of passing features and label arguments, fit expects the
left, right, and matches data frames.
During training, counterexamples of matching records are automatically
selected from the cross-product of left and right data frames based on
the mismatch_share parameter. Including all the non-matching examples
can lead to a highly unbalanced matching dataset. For each record in the
left data set, appearing in k examples in the matches data set,
there are up to n - k counterexamples, where n is the number of
records in the right data set. The mismatch_share parameter controls
the ratio of counterexamples to matches. For instance, if
mismatch_share = 0.5, the encode function selects 50% of the
possible counterexamples for each match. The counterexamples are
selected randomly from the right dataset.
For instance, the left and right data sets in this example have 36
and 39 records, respectively, and the matches data set has 39 records.
This allows us to construct 1365 non-matching examples. We set
mismatch_share = 0.2, which means that for each matching example
provided in matches, we get 7 non-matching examples (the integer part,
i.e., the floor, of the number of counterexamples used).
The remaining arguments are similar to tensorflow.keras.Model.fit.
model.fit(
left,
right,
matches,
epochs=100,
batch_size=32,
verbose=0
)
The evaluate function overloads the
tensorflow.keras.Model.evaluate
function (see also the evaluate
documentation).
Similar to fitting a matching model, the evaluate call expects left,
right, and matches data sets.
model.evaluate(left, right, matches, verbose = 0)
[............................................................] 1/88[=...........................................................] 2/88[==..........................................................] 3/88[==..........................................................] 4/88[===.........................................................] 5/88[====........................................................] 6/88[====........................................................] 7/88[=====.......................................................] 8/88[======......................................................] 9/88[======......................................................] 10/88[=======.....................................................] 11/88[========....................................................] 12/88[========....................................................] 13/88[=========...................................................] 14/88[==========..................................................] 15/88[==========..................................................] 16/88[===========.................................................] 17/88[============................................................] 18/88[============................................................] 19/88[=============...............................................] 20/88[==============..............................................] 21/88[===============.............................................] 22/88[===============.............................................] 23/88[================............................................] 24/88[=================...........................................] 25/88[=================...........................................] 26/88[==================..........................................] 27/88[===================.........................................] 28/88[===================.........................................] 29/88[====================........................................] 30/88[=====================.......................................] 31/88[=====================.......................................] 32/88[======================......................................] 33/88[=======================.....................................] 34/88[=======================.....................................] 35/88[========================....................................] 36/88[=========================...................................] 37/88[=========================...................................] 38/88[==========================..................................] 39/88[===========================.................................] 40/88[===========================.................................] 41/88[============================................................] 42/88[=============================...............................] 43/88[==============================..............................] 44/88[==============================..............................] 45/88[===============================.............................] 46/88[================================............................] 47/88[================================............................] 48/88[=================================...........................] 49/88[==================================..........................] 50/88[==================================..........................] 51/88[===================================.........................] 52/88[====================================........................] 53/88[====================================........................] 54/88[=====================================.......................] 55/88[======================================......................] 56/88[======================================......................] 57/88[=======================================.....................] 58/88[========================================....................] 59/88[========================================....................] 60/88[=========================================...................] 61/88[==========================================..................] 62/88[==========================================..................] 63/88[===========================================.................] 64/88[============================================................] 65/88[=============================================...............] 66/88[=============================================...............] 67/88[==============================================..............] 68/88[===============================================.............] 69/88[===============================================.............] 70/88[================================================............] 71/88[=================================================...........] 72/88[=================================================...........] 73/88[==================================================..........] 74/88[===================================================.........] 75/88[===================================================.........] 76/88[====================================================........] 77/88[=====================================================.......] 78/88[=====================================================.......] 79/88[======================================================......] 80/88[=======================================================.....] 81/88[=======================================================.....] 82/88[========================================================....] 83/88[=========================================================...] 84/88[=========================================================...] 85/88[==========================================================..] 86/88[===========================================================.] 87/88[============================================================] 88/88
{'TP': 39,
'FP': 19,
'TN': 1346,
'FN': 0,
'Loss': 62.71932601928711,
'Accuracy': 0.9864672422409058,
'Recall': 1.0,
'Precision': 0.6724137663841248,
'F1': 0.8041236996650696,
'MCC': 0.8142813444137573}
Predictions and Suggestions¶
Matching predictions can be obtained in two ways from the fitted model.
Either by calling predict or by calling suggest. The predict
function returns a vector of prediction probabilities for each
combination of left and right records. The prediction probabilities
are stored in row-major order. First, the matching probabilities of the
first row of left with all the rows of right are given. Then, the
probabilities of the second row of left with all the rows of right
are given, and so on. In total, the predict function returns a vector
with rows equal to the product of the number of rows in the left and
right data sets.
predictions = model.predict(left, right, verbose = 0)
fig, ax = plt.subplots()
counts, bins= np.histogram(predictions, bins = 100)
cdf = np.cumsum(counts)/np.sum(counts)
ax.plot(bins[1:], cdf)
ax.set_xlabel("Matching Prediction")
ax.set_ylabel("Cumulative Density")
plt.show()

The suggest function returns the best matching predictions of the
model for each row of the left dataset. The prediction probabilities
of predict are grouped by the indices of the left dataset and sorted
in descending order. The caller can choose the number of returned
suggestions by setting the count argument of suggest.
suggestions = model.suggest(left, right, count = 3, verbose = 0)
suggestions["true_match"] = suggestions.loc[:, ["left", "right"]].apply(
lambda x: any((x.left == matches.left) & (x.right==matches.right)), axis=1
)
suggestions = suggestions.join(
matches.iloc[-no_duplicates:,:].assign(duplicate=True).set_index(['left', 'right']),
on = ["left", "right"],
how = "left"
)
suggestions.duplicate = suggestions.duplicate.apply(
lambda x: False if pd.isna(x) else x
)
suggestions = suggestions.sort_values(
by=["left", "prediction"], ascending=[True, False]
)
suggestions
|
left |
right |
prediction |
true_match |
duplicate |
|
|---|---|---|---|---|---|
|
0 |
0 |
0 |
9.778342e-01 |
True |
False |
|
1 |
0 |
14 |
9.169472e-01 |
False |
False |
|
2 |
0 |
4 |
9.598946e-04 |
False |
False |
|
3 |
1 |
1 |
9.718049e-01 |
True |
False |
|
4 |
1 |
23 |
8.463337e-05 |
False |
False |
|
5 |
1 |
10 |
7.474281e-05 |
False |
False |
|
6 |
2 |
2 |
9.787740e-01 |
True |
False |
|
7 |
2 |
7 |
1.013026e-03 |
False |
False |
|
8 |
2 |
35 |
6.155510e-04 |
False |
False |
|
9 |
3 |
3 |
9.766330e-01 |
True |
False |
|
10 |
3 |
30 |
1.074756e-03 |
False |
False |
|
11 |
3 |
21 |
5.062067e-05 |
False |
False |
|
12 |
4 |
4 |
9.787740e-01 |
True |
False |
|
13 |
4 |
12 |
9.383137e-01 |
False |
False |
|
14 |
4 |
0 |
9.753937e-04 |
False |
False |
|
15 |
5 |
5 |
9.777580e-01 |
True |
False |
|
16 |
5 |
15 |
7.423223e-05 |
False |
False |
|
17 |
5 |
27 |
3.159808e-05 |
False |
False |
|
18 |
6 |
6 |
9.773185e-01 |
True |
False |
|
19 |
6 |
24 |
2.507875e-05 |
False |
False |
|
20 |
6 |
17 |
7.486212e-08 |
False |
False |
|
21 |
7 |
7 |
9.781616e-01 |
True |
False |
|
22 |
7 |
2 |
1.006788e-03 |
False |
False |
|
23 |
7 |
35 |
6.105449e-04 |
False |
False |
|
24 |
8 |
8 |
9.772826e-01 |
True |
False |
|
25 |
8 |
9 |
3.877738e-05 |
False |
False |
|
26 |
8 |
23 |
1.128767e-07 |
False |
False |
|
27 |
9 |
9 |
9.716737e-01 |
True |
False |
|
28 |
9 |
17 |
5.324303e-04 |
False |
False |
|
29 |
9 |
28 |
4.801422e-04 |
False |
False |
|
30 |
10 |
10 |
9.733325e-01 |
True |
False |
|
31 |
10 |
23 |
9.662850e-01 |
False |
False |
|
32 |
10 |
1 |
7.031505e-05 |
False |
False |
|
33 |
11 |
11 |
9.782392e-01 |
True |
False |
|
34 |
11 |
29 |
4.065759e-04 |
False |
False |
|
35 |
11 |
19 |
5.362284e-05 |
False |
False |
|
36 |
12 |
12 |
9.742600e-01 |
True |
False |
|
37 |
12 |
4 |
9.637859e-01 |
False |
False |
|
38 |
12 |
22 |
1.027538e-03 |
False |
False |
|
39 |
13 |
13 |
9.783336e-01 |
True |
False |
|
40 |
13 |
37 |
9.752246e-01 |
True |
True |
|
41 |
13 |
33 |
9.751084e-01 |
False |
False |
|
42 |
14 |
14 |
9.712535e-01 |
True |
False |
|
43 |
14 |
0 |
9.653772e-01 |
False |
False |
|
44 |
14 |
4 |
1.020161e-03 |
False |
False |
|
45 |
15 |
15 |
9.767061e-01 |
True |
False |
|
46 |
15 |
24 |
3.229330e-04 |
False |
False |
|
47 |
15 |
5 |
7.657281e-05 |
False |
False |
|
48 |
16 |
16 |
9.648299e-01 |
True |
False |
|
49 |
16 |
30 |
4.539440e-05 |
False |
False |
|
50 |
16 |
34 |
3.271291e-05 |
False |
False |
|
51 |
17 |
17 |
9.758328e-01 |
True |
False |
|
52 |
17 |
21 |
1.238351e-03 |
False |
False |
|
53 |
17 |
9 |
5.116409e-04 |
False |
False |
|
54 |
18 |
18 |
9.786937e-01 |
True |
False |
|
55 |
18 |
35 |
9.030289e-01 |
False |
False |
|
56 |
18 |
36 |
8.958049e-01 |
False |
False |
|
57 |
19 |
19 |
9.778247e-01 |
True |
False |
|
58 |
19 |
33 |
1.464835e-03 |
False |
False |
|
59 |
19 |
13 |
1.276108e-03 |
False |
False |
|
60 |
20 |
20 |
9.783897e-01 |
True |
False |
|
61 |
20 |
32 |
9.765700e-01 |
False |
False |
|
62 |
20 |
37 |
3.983492e-04 |
False |
False |
|
63 |
21 |
21 |
9.739489e-01 |
True |
False |
|
64 |
21 |
17 |
1.318118e-03 |
False |
False |
|
65 |
21 |
9 |
4.996893e-04 |
False |
False |
|
66 |
22 |
22 |
9.768478e-01 |
True |
False |
|
67 |
22 |
12 |
7.845204e-04 |
False |
False |
|
68 |
22 |
4 |
5.201011e-04 |
False |
False |
|
69 |
23 |
23 |
9.778675e-01 |
True |
False |
|
70 |
23 |
10 |
9.233682e-01 |
False |
False |
|
71 |
23 |
1 |
7.060236e-05 |
False |
False |
|
72 |
24 |
24 |
9.670053e-01 |
True |
False |
|
73 |
24 |
15 |
3.808707e-04 |
False |
False |
|
74 |
24 |
6 |
3.039734e-05 |
False |
False |
|
75 |
25 |
25 |
9.785444e-01 |
True |
False |
|
76 |
25 |
13 |
9.746334e-01 |
False |
False |
|
77 |
25 |
33 |
9.737168e-01 |
False |
False |
|
78 |
26 |
26 |
9.766273e-01 |
True |
False |
|
79 |
26 |
38 |
9.702485e-01 |
True |
True |
|
80 |
26 |
31 |
4.269315e-04 |
False |
False |
|
81 |
27 |
27 |
9.779201e-01 |
True |
False |
|
82 |
27 |
15 |
3.216346e-05 |
False |
False |
|
83 |
27 |
5 |
3.040044e-05 |
False |
False |
|
84 |
28 |
28 |
9.773872e-01 |
True |
False |
|
85 |
28 |
17 |
5.012505e-04 |
False |
False |
|
86 |
28 |
21 |
5.000369e-04 |
False |
False |
|
87 |
29 |
29 |
9.787740e-01 |
True |
False |
|
88 |
29 |
11 |
4.129025e-04 |
False |
False |
|
89 |
29 |
32 |
8.954816e-05 |
False |
False |
|
90 |
30 |
30 |
9.761566e-01 |
True |
False |
|
91 |
30 |
3 |
1.078105e-03 |
False |
False |
|
92 |
30 |
17 |
7.555061e-05 |
False |
False |
|
93 |
31 |
31 |
9.777137e-01 |
True |
False |
|
94 |
31 |
26 |
5.508639e-04 |
False |
False |
|
95 |
31 |
38 |
3.956477e-04 |
False |
False |
|
96 |
32 |
32 |
9.785187e-01 |
True |
False |
|
97 |
32 |
20 |
9.764295e-01 |
False |
False |
|
98 |
32 |
37 |
3.983492e-04 |
False |
False |
|
99 |
33 |
33 |
9.786212e-01 |
True |
False |
|
100 |
33 |
13 |
9.748630e-01 |
False |
False |
|
101 |
33 |
25 |
9.736227e-01 |
False |
False |
|
102 |
34 |
34 |
9.765700e-01 |
True |
False |
|
103 |
34 |
30 |
4.525233e-05 |
False |
False |
|
104 |
34 |
17 |
4.510958e-05 |
False |
False |
|
105 |
35 |
36 |
9.575554e-01 |
True |
True |
|
106 |
35 |
35 |
9.503515e-01 |
True |
False |
|
107 |
35 |
18 |
9.235189e-01 |
False |
False |