# Making a pairwise distance matrix in pandas

Distance matrices are rarely useful in themselves, but are often used as part of workflows involving clustering.

This is a somewhat specialized problem that forms part of a lot of data science and clustering workflows. It starts with a relatively straightforward question: if we have a bunch of measurements for two different things, how do we come up with a single number that represents the difference between the two things?

**Tip:**For a deeper dive into distances and clustering, checkout the chapter on matrix charts in the

*Drawing from Data*book.

An example will make the question clearer. Let's load our olympic medal dataset:

```
import pandas as pd
data = pd.read_csv("https://raw.githubusercontent.com/mojones/binders/master/olympics.csv", sep="\t")
data
```

and measure, for each different country, the number of medals they've won in each different sport:

```
summary = data.groupby(['Country', 'Sport']).size().unstack().fillna(0)
summary
```

Now we'll pick two countries:

```
summary.loc[['Germany', 'Italy']]
```

Each country has 44 columns giving the total number of medals won in each sport. Our job is to come up with a single number that summarizes how different those two lists of numbers are. Mathematicians have figured out lots of different ways of doing that, many of which are implemented in the `scipy.spatial.distance`

module.

If we just import `pdist`

from the module, and pass in our dataframe of two countries, we'll get a measuremnt:

```
from scipy.spatial.distance import pdist
pdist(summary.loc[['Germany', 'Italy']])
```

That's the distance score using the default metric, which is called the *euclidian distance*. Think of it as the straight line distance between the two points in space defined by the two lists of 44 numbers.

Now, what happens if we pass in a dataframe with three countries?

```
pdist(summary.loc[['Germany', 'Italy', 'France']])
```

As we might expect, we have three measurements:

- Germany and Italy
- Germnay and France
- Italy and France

But it's not easy to figure out which belongs to which. Happily, `scipy`

also has a helper function that will take this list of numbers and turn it back into a square matrix:

```
from scipy.spatial.distance import squareform
squareform(pdist(summary.loc[['Germany', 'Italy', 'France']]))
```

In order to make sense of this, we need to re-attach the country names, which we can just do by turning it into a DataFrame:

```
pd.DataFrame(
squareform(pdist(summary.loc[['Germany', 'Italy', 'France']])),
columns = ['Germany', 'Italy', 'France'],
index = ['Germany', 'Italy', 'France']
)
```

Hopefully this agrees with our intuition; the numbers on the diagonal are all zero, because each country is identical to itself, and the numbers above and below are mirror images, because the distance between Germany and France is the same as the distance between France and Germany (remember that we are talking about distance in terms of their medal totals, not geographical distance!)

Finally, to get pairwise measurements for the whole input dataframe, we just pass in the complete object and get the country names from the index:

```
pairwise = pd.DataFrame(
squareform(pdist(summary)),
columns = summary.index,
index = summary.index
)
pairwise
```

A nice way to visualize these is with a heatmap. 137 countries is a bit too much to show on a webpage, so let's restrict it to just the countries that have scored at least 500 medals total:

```
import seaborn as sns
import matplotlib.pyplot as plt
# make summary table for just top countries
top_countries = (
data
.groupby('Country')
.filter(lambda x : len(x) > 500)
.groupby(['Country', 'Sport'])
.size()
.unstack()
.fillna(0)
)
# make pairwise distance matrix
pairwise_top = pd.DataFrame(
squareform(pdist(top_countries)),
columns = top_countries.index,
index = top_countries.index
)
# plot it with seaborn
plt.figure(figsize=(10,10))
sns.heatmap(
pairwise_top,
cmap='OrRd',
linewidth=1
)
```

Now that we have a plot to look at, we can see a problem with the distance metric we're using. The US has won so many more medals than other countries that it distorts the measurement. And if we think about it, what we're really interested in is not the exact number of medals in each category, but the relative number. In other words, we want two contries to be considered similar if they both have about twice as many medals in boxing as athletics, for example, regardless of the exact numbers.

Luckily for us, there is a distance measure already implemented in scipy that has that property - it's called *cosine distance*. Think of it as a measurement that only looks at the relationships between the 44 numbers for each country, not their magnitude. We can switch to cosine distance by specifying the `metric`

keyword argument in `pdist`

:

```
pairwise_top = pd.DataFrame(
squareform(pdist(top_countries, metric='cosine')),
columns = top_countries.index,
index = top_countries.index
)
# plot it with seaborn
plt.figure(figsize=(10,10))
sns.heatmap(
pairwise_top,
cmap='OrRd',
linewidth=1
)
```

And as you can see we spot some much more interstesting patterns. Notice, for example, that Russia and Soviet Union have a very low distance (i.e. their medal distributions are very similar).

When looking at data like this, remember that the shade of each cell is not telling us anything about how many medals a country has won - simply how different or similar each country is to each other. Compare the above heatmap with this one which displays the proportion of medals in each sport per country:

```
plt.figure(figsize=(10,10))
sns.heatmap(
top_countries.apply(lambda x : x / x.sum(), axis=1),
cmap='BuPu',
square=True,
cbar_kws = {'fraction' : 0.02}
)
```

Finally, how might we find pairs of countries that have very similar medal distributions (i.e. very low numbers in the pairwise table)? By far the easiest way is to start of by reshaping the table into long form, so that each comparison is on a separate row:

```
pairwise = pd.DataFrame(
squareform(pdist(summary, metric='cosine')),
columns = summary.index,
index = summary.index
)
# move to long form
long_form = pairwise.unstack()
# rename columns and turn into a dataframe
long_form.index.rename(['Country A', 'Country B'], inplace=True)
long_form = long_form.to_frame('cosine distance').reset_index()
```

Now we can write our filter as normal, remembering to filter out the unintersting rows that tell us a country's distance from itself!

```
long_form[
(long_form['cosine distance'] < 0.05)
& (long_form['Country A'] != long_form['Country B'])
]
```