Compare Databases Like a Ninja ๐ฅท

We have a database we know inside out. It’s consistent and readable. We have another database, but we can’t exactly call it consistent; its main column is entered manually. The task: analyze both databases together. Sound familiar?
It certainly does to meโI’ve been there. As part of my military service, I compared a standardized database with one written by hand. How did I do it, you ask? With dozens of hours of manual work. The main drawbacks of working this way:
Valuable working time that we’d rather spend on other things. What’s more, all that work can lead to burnout, which we certainly want to avoid.
The time spent working leads to a greater margin of error, potentially making the information even less reliable than it already is.
Repeatability isn’t possible, and we have to classify everything all over again each time. No fun at all.
After reading this article, you’ll know how we can automate this process, step by step:
- First Steps ๐ฃ
- Getting to Know the Databases ๐
- Matching Columns ๐
- Analyzing the Findings ๐ค
- This Is Just the Beginning ๐
First Steps ๐ฃ #
Libraries #
Before I begin, I used Jupyter as my IDE, running locally.
import pandas as pd
pd.set_option('display.float_format', lambda x: '%.3f' % x)
import numpy as np
from fuzzywuzzy import process
import matplotlib.pyplot as plt
import seaborn as sns
pandas(lines 1โ2) - We’ll use it for data analysis inPython. On the second line, I configured it so I could see very large and very small numbers without the development environment rounding them.numpy(line 4) - A library that serves as a foundation for the vast majority of libraries in the field. EvenPandasis built on it.fuzzywuzzy(line 6) - The library we’ll use to compare the columns.matplotlibandseaborn(lines 8โ9) - Libraries for creating charts.
Where the Databases Came From #
This article is based on the government data repository Data Gov. As someone who loves data, I think this site really is every Data Analyst’s dream. You can find data on a variety of topics: Israel’s economy, transportation, health, and more.
I decided to investigate whether there is a relationship between the number of bank branches and the number of residents in Israeli cities.
Getting to Know the Databases ๐ #
I downloaded the databases to my computer manually, then loaded the CSV files using Python. Data Gov actually supports API, but in this case I decided to keep things simple.
Banks #
I had no trouble accessing the bank database. In the following code snippet, I imported the local CSV file and printed the column names.
snifim = pd.read_csv('snifim_he.csv')
snifim.columns
๐๐ผ
Index(['Bank_Code', 'Bank_Name', 'Branch_Code', 'Branch_Name',
'Branch_Address', 'City', 'Zip_Code', 'POB', 'Telephone', 'Fax',
'Free_Tel', 'Handicap_Access', 'day_closed', 'Branch_Type', 'Open_Date',
'Close_Date', 'Merge_Bank', 'Merge_Branch', 'X_Coordinate',
'Y_Coordinate', 'ban_city', 'final_city'],
dtype='object')
In this database, we’ll mainly use the Bank_Name and City columns.
Population #
If we take a look at the CSV containing the population figures for each locality, we can see that the text is unreadable and the columns aren’t laid out properly. We have a problem.

I concluded that both the computer and IDE need to be told explicitly which encoding to use when reading the file. After some research, I determined that the relevant encoding was ISO-8859-8.
population = pd.read_csv('residents_in_israel_by_communities_and_age_groups.csv',encoding='ISO-8859-8')
population = population.applymap(lambda x: x.strip() if isinstance(x, str) else x)
population.columns
๐๐ผ
Index(['ืกืื_ืืฉืื', 'ืฉื_ืืฉืื', 'ืกืื_ื ืคื', 'ื ืคื', 'ืงืื_ืืฉืืช_ืื ื', 'ืืฉืืช_ืื ื',
'ืงืื_ืืืขืฆื_ืืืืจืืช', 'ืืืขืฆื_ืืืืจืืช', 'ืกืื', 'ืืื_0_5', 'ืืื_6_18',
'ืืื_19_45', 'ืืื_46_55', 'ืืื_56_64', 'ืืื_65_ืคืืืก'],
dtype='object')
Reading
population(line 1) - We’ll use the same function we used to read the bank branch database, but this time we’ll set theencodingparameter to theISO-8859-8encoding.Editing
population(line 2) - We’ll remove unnecessary spaces using thestripfunction.
In this database, we’ll mainly use the ืฉื_ืืฉืื' and ืกืื columns.
Initial Exploration ๐ง #
Banks #
Besides running snifim.head(), I thought it would be nice to show the 10 largest banking companies in Israel.
First, I wrote a relatively simple function that sorts the df and returns the num_of_rows by size.
def shorten_df(df, iterate_row, num_of_rows, comment):
sum_df = df[iterate_row].value_counts(ascending=True)
length = len(sum_df)
top_rows = sum_df[num_of_rows:].sort_values(ascending = False)
if num_of_rows < length:
other_rows = sum_df[:length - num_of_rows]
remaining_row = pd.Series(other_rows.sum(), index=[comment])
rows_plot = pd.concat([top_rows, remaining_row])
return rows_plot
return sum_df
Then I could easily see the 10 largest banking companies in Israel and display a chart:
banks = shorten_df(snifim, 'Bank_Name', 10, 'ืื ืงืื ื ืืกืคืื')
ax = sns.barplot(x = banks.index, y = banks.values)
ax.set_xticklabels(ax.get_xticklabels(),rotation = 90)
plt.show()
Although we managed to show the branches in a chart as I wanted, I ran into a problem displaying Hebrew that I still haven’t been able to solve. If anyone knows a solution, I’d love to hear it ๐ง.

Before moving on to the population database, I decided to check how many unique cities there are in this database:
ban_cities = set(snifim['City'])
print("Banks unique cities: {0}".format(len(ban_cities)))
๐๐ผ
"Banks unique cities: 161"
Population #
For the population data, I decided to run a basic groupby to get a sense of the scale and see whether the data made sense:
population.groupby('ืฉื_ืืฉืื')['ืกืื'].sum().sort_values()
๐๐ผ
ืืคืจ ืขืืืื 1
ืืืืื 2
ืืจื ืืฉื 3
ืืืชื ืื 4
ืืืจื ืื 5
...
ืจืืฉืื ืืฆืืื 277915
ืคืชื ืชืงืืื 278645
ืืืคื 331402
ืชื ืืืื - ืืคื 573660
ืืจืืฉืืื 1056097
Name: ืกืื, Length: 1264, dtype: int64
Here, too, I checked how many unique cities there are in the database. Notice the difference!
pop_cities = set(population['ืฉื_ืืฉืื'])
print("Population unique cities: {0}".format(len(pop_cities)))
๐๐ผ
"Population unique cities: 1264"
Matching Columns ๐ #
What’s the Problem? #
Both databases have a column referring to the city. The problem is that the city names aren’t written the same way. For example, we can see that โNahariyaโ is written with two instances of the Hebrew letter yod in the bank database, but with just one in the population database ๐คฌ.
'ื ืืจืืื' in ban_cities ๐๐ผ True
'ื ืืจืื' in pop_cities ๐๐ผ True
The Levenshtein Distance Algorithm #
I first encountered this algorithm while studying the Data Scientist with Python track on Datacamp. In addition to my explanation, I highly recommend learning about the mathematical background in this Medium article.
Given two str, we get a number representing the distance between them. What do I mean by distance? The number of the following operations we need to perform to get from one str to the other:
- Inserting a character ๐งฉ
- Deleting a character โซ
- Replacing a character ๐

So How Do We Calculate the Distance? #
Using the process function, which we imported along with the fuzzywuzzy library, we can calculate the distance between words. In this example (copied from Datacamp), we want to find the element in the strOptions array that is closest to str2Match:
str2Match = "apple inc"
strOptions = ("Apple Inc.", "apple park", "apple incorporated", "iphone")
Ratios = process.extract(str2Match,strOptions)
print(Ratios)
๐๐ผ
[('Apple Inc.', 100), ('apple incorporated', 90), ('apple park', 67), ('iphone', 30)]
We got an array neatly containing the values from the strOptions array and their relative distance from str2Match. That’s it! Now we can return to our task and compare the cities in the databases ๐ฅณ.
Calculating the Distance Between City Names #
Now for the most fun part: applying the distance algorithm to the databases. I decided to match the bank database against the population database based on intuition and the difference in the number of cities.
ban_and_pop_city = pd.DataFrame(columns=['ban_city', 'pop_city', 'ratio'])
for ban_city in ban_cities:
Ratios = process.extract(ban_city,pop_cities)[0]
city_row = {
'ban_city': ban_city,
'pop_city': Ratios[0],
'ratio': Ratios[1]
}
city_row = pd.DataFrame(city_row, index=[0])
ban_and_pop_city = pd.concat([ban_and_pop_city, city_row], ignore_index = True)
ban_and_pop_city(line 1) - Creating an emptydfwhere we’ll put the city names from the bank database and their corresponding cities in the population database.for ban_city(line 3) - A loop that goes through the city names in the bank database.process.extract(line 4) - Finding the city name in the population database that is closest toban_city.city_row(lines 6โ11) - Creating a new row for a city that we can insert into theban_and_pop_cityfrom earlier.pd.concat(line 13) - Insertingcity_rowintoban_and_pop_city.
How Are the Match Percentages Distributed? #
It was important to me to get an overview of how the matches were distributed, especially where they were concentrated.
sns.histplot(data=ban_and_pop_city, x="ratio", bins=20)
plt.show()

We’re in good shape: most cities have a 100 percent match. But our next task is to figure out which cities don’t.
So Which Cities Have Low Match Scores? #
At a glance, we can see that most discrepancies stem from cases like the example at the beginning: cities spelled with either one or two instances of yod. Let’s investigate which city names didn’t contain two instances of yod and weren’t a perfect match:
without_youd = ban_and_pop_city[ban_and_pop_city['ratio'] < 100]
without_youd = without_youd[~without_youd['ban_city'].str.contains('ืื')]
| index | ban_city | pop_city | ratio |
|---|---|---|---|
| 73 | Airport City | Kiryat Ye’arim (institution) | 86 |
| 109 | Ben-Gurion Airport | Kerem Ben Zimra | 86 |
| 131 | Tel Aviv -Yafo | Tel Aviv - Yafo | 96 |
| 139 | Arava Regional Council 54 | Kaukab Abu al-Hija | 86 |
| 140 | Northern Negev Highlands Regional Council 48 | Har Gilo | 86 |
| 153 | Ramla Area (not assigned to a locality) | Ramla | 90 |
Manual Changes #
As we can see in the table above, there are edge cases we’ll need to edit manually. We haven’t eliminated manual work entirely; instead, we’ve moved from a manual workflow to a bird’s-eye review, saving most of the working hours in the process.
city_df = ban_and_pop_city
city_df['final_city'] = city_df['pop_city']
city_df.loc[city_df['ban_city'] == 'ื ืื ืชืขืืคื ืื-ืืืจืืื', 'final_city'] = 'ืืื'
city_df[city_df['pop_city'].str.contains('ื ืื ืชืขืืคื ืื-ืืืจืืื')]
city_df(lines 1โ2) - We created a copy ofban_and_pop_cityto serve as our dictionary. Thefinal_citycolumn will contain the final city names.city_df.loc(line 4) - We’ll manually filter the city names we want to change. For example, we’ll classify a bank at the airport that was initially assigned to Kerem Ben Zimra as being in Lod.contains(line 6) - We’ll check the changes we made.
Merging the Tables ๐ฒ #
Now that we have a dictionary (city_df), we can use it to convert the city names in the snifim table so we can join it to the population table:
In the population Table #
population = population.merge(
city_df[['pop_city','final_city']],
left_on='ืฉื_ืืฉืื',
right_on='pop_city',
how='left'
)
In the snifim Table #
snifim = snifim.merge(
city_df[['ban_city', 'final_city']],
left_on = 'City',
right_on = 'ban_city',
how = 'left'
)
Analyzing the Findings ๐ค #
Which Cities Have No Banks? #
First of all, I decided to find out what percentage of cities have at least one bank:
population['has_bank'] = np.where(population['final_city'].isnull(), False, True)
length = len(population['final_city'])
no_banks = (population['has_bank'] == False).sum()
print("Percentage of cities with banks: {0:.0%}".format((length - no_banks) / length))
๐๐ผ
"Percentage of cities with banks: 13%"
has_bank(line 1) - If the merge operation returnednull, we can infer that there is no bank in that city. The value in this column is Boolean.length(line 2) - Represents the number of cities we have in the population table.no_banks(line 3) - Adds up the number of cities with no banks.
87% of Israeli localities don’t have even a single bank branch. Surprising, right? Let’s show that in a chart:
pop_no_banks = population[population['has_bank'] == False]
sns.histplot(data=pop_no_banks, x="ืกืื")
plt.axvline(x=pop_no_banks['ืกืื'].median(), color='red')
plt.show()
pop_no_banks(line 1) - For simplicity, I added adfto hold the details of cities with no bank branches.sns.histplot(line 3) - I plotted the distribution of population sizes by the number of localities.plt.axvline(line 4) - I displayed the median on the chart.
Distribution of Medium-to-Large Cities Without a Branch #
As we can see from the chart above, it makes a lot of sense that localities with up to 5,000 residents wouldn’t have bank branches. I decided to examine the distribution of cities with no bank branches and more than 5,000 residents.
sns.histplot(
data=pop_no_banks[pop_no_banks['ืกืื'] > 5000],
x="ืกืื",
bins = 20,
kde = True
)

Distribution of the City Groups #
The next step that interested me was comparing the distributions of cities with no bank branches against those with branches.
plt.xlim(0, 1000)
ax = sns.histplot(population, x="ืกืื", hue="has_bank")

We made a simple histogram, just like in the previous section, but ran into a problem: the groups are on different scales.
The Data Binning Process #
What Is It, Anyway? #
To compare and examine the distributions of the groups, we’ll need to divide them up evenly. To do this, we’ll use a process called Binning. This process divides the values into equal ranges and assigns each value to the appropriate range. We’ll consider this a Data Preprocessing step.
Dividing Up the Values #
pop_bind = population.copy()
min_val = 2500
max_val = 75000
num_bins = 30
bins = np.linspace(
min_val,
max_val,
num_bins
)
pop_bind['b_pop_y'] = pd.cut(pop_bind[pop_bind['has_bank']]['ืกืื'], bins=bins, include_lowest=True, precision=0)
pop_bind['b_pop_n'] = pd.cut(pop_bind[~pop_bind['has_bank']]['ืกืื'], bins=bins, include_lowest=True, precision=0)
pop_bind(line 1) - I created a copy ofpopulationto avoid โmessing it upโ and keep the data clean.- Range variables (lines 2โ3) - Variables that help us decide the range of total population sizes per city that we want to divide up.
num_bins(line 4) - Stores the number of bins we want to divide the data into.np.linspace(lines 6โ10) - A function fromNumpythat takes a range of values and the number of bins we want to divide it into. The function returns abinsarray containing the upper value of each range. For example: 2,500, followed by 5,000, and so on.pd.cut(lines 12โ13) - A function fromPandasthat divides values according tobins:- For each row in the table, the function takes the value (
ืกืื) and checks which range inbinsit belongs to. For example, the city in row 1,261, with 33,299 residents, was assigned to the range (32500.0, 35000.0]. pandas._libs.interval.Interval- The type of value we get from thecutfunction. It tells us which range theืกืืbelongs to.- I ran the function separately for cities with banks and cities without, so I could compare them later.
- For each row in the table, the function takes the value (
Combining the Columns #
To compare the distributions, the next step is to combine the b_pop_y column with the b_pop_n column.
pop_bind['b_pop'] = pop_bind['b_pop_y'].fillna(pop_bind['b_pop_n'])
Preparing the Visualization #
The next step was to prepare a summary table, to_plot, that counts how many cities there are in each group.
to_plot = pop_bind.groupby(['b_pop','has_bank'])['ืกืื'].count().reset_index()
| b_pop | has_bank | Total | |
|---|---|---|---|
| 0 | (2499.0, 5000.0] | False | 57 |
| 1 | (2499.0, 5000.0] | True | 6 |
| 2 | (5000.0, 7500.0] | False | 18 |
| 3 | (5000.0, 7500.0] | True | 8 |
| 4 | (7500.0, 10000.0] | False | 12 |
| 5 | (7500.0, 10000.0] | True | 10 |
Visualization #
Now, using a relatively simple barplot function, we can visually compare the concentration of cities with bank branches against those without.
sns.barplot(x = 'b_pop',
y = 'ืกืื',
data = to_plot,
hue = 'has_bank')
plt.xticks(rotation=90)
plt.show()

The Relationship Between the Total Number of Bank Branches and Population #
I was curious to check whether there is a relationship between the total number of bank branches and the number of residents in each locality.
snif = snifim.value_counts('final_city')
pop = population.groupby('ืฉื_ืืฉืื')['ืกืื'].sum()
snif_pop = pd.concat([snif, pop], axis=1)
snif_pop.columns = ['snif', 'pop']
snif_pop = snif_pop[~snif_pop['snif'].isna()]
snif(line 1) - How many branches there are in each city.pop(line 2) - How many residents there are in each locality.snif_pop(lines 4โ5) - A merged table.isna(line 6) - I removed cities with no banks.
plt.xlim(0, 350000)
plt.ylim(0, 75)
sns.regplot(x="pop",
y="snif",
data=snif_pop,
ci=None)
plt.show()
lim(lines 1โ2) - Setting the ranges for the x-axis and y-axis. I excluded the outlier cities, Tel Aviv and Jerusalem.sns.regplot(lines 4โ7) - Creating a scatterplot with a linear trend line showing the relationship.

This Is Just the Beginning ๐ #
Now that we’ve learned about several distinctive technologies, we can take our research even further, expanding the dataset and presenting it in ways we hadn’t considered before.
The most important thing I want to leave you with is to remember, throughout the research, what message we want to convey and what we’re actually investigating. Otherwise, we’ll lose our way and won’t have a clear bottom line.