Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Extend KNN neighbor search beyond coincident sites #287

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 117 additions & 13 deletions libpysal/weights/distance.py
Original file line number Diff line number Diff line change
@@ -12,6 +12,49 @@
import scipy.sparse as sp
import numpy as np

def duplicated(array):
"""Identify duplicate rows in an array
Parameters
----------
array : np.ndarray
(n,k)
Returns
-------
duplicate : np.ndarray
(n, 3)
First column indicates if the row is a duplicate
Second column indicates if the row is a duplicate of a row with
a lower index
Third column contains the index of the first row that
duplicates current row
Examples
---------
>>> a = np.array([[1,1,1],[2,2,2],[3,3,3],[4,4,4],[5,5,5],[1,1,1],
[2,2,2], [1,1,1]])
>>> duplicated(a)
array([[1, 0, 0],
[1, 0, 0],
[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
[1, 1, 0],
[1, 1, 1],
[1, 1, 0]])
>>> duplicated(a)[:,0].any()
True
"""
array = np.asarray(array)
n = array.shape[0]
duplicate = np.zeros((n,3), dtype=int)
unq, count = np.unique(array, axis=0, return_counts=True)
repeated_groups = unq[count > 1]
for repeated_group in repeated_groups:
repeated_idx = np.argwhere(np.all(array == repeated_group, axis=1))
duplicate[repeated_idx, 0] = 1
duplicate[repeated_idx[1:], 1] = 1
duplicate[repeated_idx[1:], 2] = repeated_idx[0]

return duplicate

def knnW(data, k=2, p=2, ids=None, radius=None, distance_metric='euclidean'):
"""
@@ -21,6 +64,7 @@ def knnW(data, k=2, p=2, ids=None, radius=None, distance_metric='euclidean'):
return KNN(data, k=k, p=p, ids=ids, radius=radius,
distance_metric=distance_metric)


class KNN(W):
"""
Creates nearest neighbor weights matrix based on k nearest
@@ -50,6 +94,27 @@ class KNN(W):
instance
Weights object with binary weights

See Also
--------
:class:`libpysal.weights.weights.W`

Notes
-----

Ties between neighbors of equal distance are arbitrarily broken.

Coincident points can cause challenges for distance based weights since the
distance separating a pair of coincident points is 0 by definition. We
handle this situation as follows. Define `P` as the set of indices for all
points in a data set. The first record in a set of duplicates (i.e., points
with same coordinates) is defined as the coincident seed and the remaining
points that are coincident with the seed are coincident duplicates. Define
`D` as the set of indices for the coincident duplicates. Initial neighbors
are identified using the set `S = P\D` (i.e., the coincident duplicates are
not included initially). Then, each coincident duplicate has its neighbors
set equal to that of its coincident seed.


Examples
--------
>>> import libpysal
@@ -75,17 +140,28 @@ class KNN(W):
>>> 0 in wnn2.neighbors
False

Notes
-----
coincident points
>>> points = [(10, 10), (20, 10), (10,10), (20,10), (40, 10),
(15, 20), (30, 20), (30, 30)]
>>> wknn2 = KNN.from_array(points, 2)
>>> wknn2.neighbors
{0: [1, 5],
1: [0, 5],
4: [6, 1],
5: [1, 0],
6: [7, 1],
7: [6, 5],
2: [1, 5],
3: [0, 5]}


Ties between neighbors of equal distance are arbitrarily broken.

See Also
--------
:class:`libpysal.weights.weights.W`
"""
def __init__(self, data, k=2, p=2, ids=None, radius=None,
distance_metric='euclidean', **kwargs):


if radius is not None:
distance_metric='arc'
if isKDTree(data):
@@ -94,21 +170,46 @@ def __init__(self, data, k=2, p=2, ids=None, radius=None,
else:
self.kdtree = KDTree(data, radius=radius, distance_metric=distance_metric)
self.data = self.kdtree.data

duplicates = duplicated(self.data)
coincident = duplicates[:,1].any()

self.duplicates = duplicates
Copy link
Member

@ljwolf ljwolf May 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean that we'd want all distance-style weights objects gain a duplicates attribute?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thinking was to let the user know, somehow, that they have coincident points - duplicates is a bad choice in this regard as that might be taken to mean the records are identical in all attributes, whereas I think by coincident implies spatial duplicates.

self.k = k
self.p = p
this_nnq = self.kdtree.query(self.data, k=k+1, p=p)
data = self.data
if coincident:
duplicate_ids = np.nonzero(duplicates[:,1])
data = self.data[np.nonzero(duplicates[:,1]==0)]
self.kdtree = KDTree(data, radius=radius, distance_metric=distance_metric)

this_nnq = self.kdtree.query(data, k=k+1, p=p)

to_weight = this_nnq[1]
if ids is None:
ids = list(range(to_weight.shape[0]))

neighbors = {}
for i,row in enumerate(to_weight):
row = row.tolist()
row.remove(i)
row = [ids[j] for j in row]
focal = ids[i]
neighbors[focal] = row
if coincident:
unique_ids = np.nonzero(duplicates[:,1]==0)[0]
for i, row in enumerate(to_weight):
row = row.tolist()
row.remove(i)
row = [unique_ids[j] for j in row]
focal = unique_ids[i]
neighbors[focal] = row
for row in duplicate_ids[0]:
neighbors[row] = neighbors[duplicates[row, 2]]
n = self.data.shape[0]
ids = list(range(n))
Copy link
Member

@ljwolf ljwolf May 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This forces ids to be 0,n-1. The other part of this if statement, where we keep coincident points, does not do this.... regardless, it means that this fails when we use KNN.from_dataframe on something with an index...

else:
for i, row in enumerate(to_weight):
row = row.tolist()
row.remove(i)
row = [ids[j] for j in row]
focal = ids[i]
neighbors[focal] = row

W.__init__(self, neighbors, id_order=ids, **kwargs)

@classmethod
@@ -249,15 +350,18 @@ def from_dataframe(cls, df, geom_col='geometry', ids=None, *args, **kwargs):
if iterable, a list of ids to use for the W
if None, df.index is used.


See Also
--------
:class:`libpysal.weights.weights.W`
"""
pts = get_points_array(df[geom_col])

if ids is None:
ids = df.index.tolist()
elif isinstance(ids, str):
ids = df[ids].tolist()

return cls(pts, *args, ids=ids, **kwargs)

def reweight(self, k=None, p=None, new_data=None, new_ids=None, inplace=True):
15 changes: 15 additions & 0 deletions libpysal/weights/tests/test_distance.py
Original file line number Diff line number Diff line change
@@ -20,6 +20,9 @@ class Distance_Mixin(object):
arc_path = pysal_examples.get_path('stl_hom.shp')
points = [(10, 10), (20, 10), (40, 10),
(15, 20), (30, 20), (30, 30)]
coincident_points = [(10, 10), (20, 10), (10,10),
(20,10), (40, 10), (15, 20),
(30, 20), (30, 30)]
euclidean_kdt = KDTree(points, distance_metric='euclidean')

polygon_f = psopen(polygon_path) # our file handler
@@ -74,6 +77,10 @@ def setUp(self):
self.known_w2 = [1, 3, 9, 12]
self.known_wi3 = 40
self.known_w3 = [31, 38, 45, 49]
self.known_coincident_neighbors = {0: [1, 5], 1: [0, 5],
4: [6, 1], 5: [1, 0],
6: [7, 1], 7: [6, 5],
2: [1, 5], 3: [0, 5]}

##########################
# Classmethod tests #
@@ -94,16 +101,24 @@ def test_from_array(self):
w = d.KNN.from_array(self.poly_centroids, k=4)
self.assertEqual(w.neighbors[self.known_wi0], self.known_w0)
self.assertEqual(w.neighbors[self.known_wi1], self.known_w1)
w = d.KNN.from_array(self.coincident_points)
self.assertEqual(w.neighbors, self.known_coincident_neighbors)

def test_from_shapefile(self):
w = d.KNN.from_shapefile(self.polygon_path, k=4)
self.assertEqual(w.neighbors[self.known_wi0], self.known_w0)
self.assertEqual(w.neighbors[self.known_wi1], self.known_w1)


##########################
# Function/User tests #
##########################

def test_duplicated(self):
p = self.coincident_points
self.assertTrue(d.duplicated(p)[:,0].any())


def test_reweight(self):
w = d.KNN(self.points, k=2)
new_point = [(21,21)]
795 changes: 795 additions & 0 deletions notebooks/knn_coincident.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,795 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 27,
"metadata": {},
"outputs": [],
"source": [
"import libpysal\n",
"import geopandas\n",
"import pandas\n",
"import copy"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": 28,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"6"
]
},
"execution_count": 28,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"points = [(10, 10), (20, 10), (40, 10), (15, 20), (30, 20), (30, 30)]\n",
"\n",
"from libpysal.weights import KNN\n",
"\n",
"wknn2 = KNN.from_array(points, 2)\n",
"wknn2.n"
]
},
{
"cell_type": "code",
"execution_count": 29,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{0: [1, 3], 1: [0, 3], 2: [4, 1], 3: [1, 0], 4: [5, 1], 5: [4, 3]}"
]
},
"execution_count": 29,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"wknn2.neighbors"
]
},
{
"cell_type": "code",
"execution_count": 30,
"metadata": {},
"outputs": [],
"source": [
"# create duplicate points\n",
"points = [(10, 10), (20, 10), (10,10), (20,10), (40, 10), (15, 20), (30, 20), (30, 30)]\n",
"\n",
"wknn2 = KNN.from_array(points, 2)"
]
},
{
"cell_type": "code",
"execution_count": 31,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{0: [1, 5],\n",
" 1: [0, 5],\n",
" 4: [6, 1],\n",
" 5: [1, 0],\n",
" 6: [7, 1],\n",
" 7: [6, 5],\n",
" 2: [1, 5],\n",
" 3: [0, 5]}"
]
},
"execution_count": 31,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"wknn2.neighbors"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": 32,
"metadata": {},
"outputs": [],
"source": [
"balt = libpysal.examples.load_example('Baltimore')"
]
},
{
"cell_type": "code",
"execution_count": 33,
"metadata": {},
"outputs": [],
"source": [
"import geopandas"
]
},
{
"cell_type": "code",
"execution_count": 34,
"metadata": {},
"outputs": [],
"source": [
"gdf = geopandas.read_file(balt.get_path(\"baltim.shp\"))"
]
},
{
"cell_type": "code",
"execution_count": 35,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"(211, 18)"
]
},
"execution_count": 35,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"gdf.shape"
]
},
{
"cell_type": "code",
"execution_count": 36,
"metadata": {},
"outputs": [],
"source": [
"w1 = libpysal.weights.KNN.from_dataframe(gdf, k=2)"
]
},
{
"cell_type": "code",
"execution_count": 37,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>STATION</th>\n",
" <th>PRICE</th>\n",
" <th>NROOM</th>\n",
" <th>DWELL</th>\n",
" <th>NBATH</th>\n",
" <th>PATIO</th>\n",
" <th>FIREPL</th>\n",
" <th>AC</th>\n",
" <th>BMENT</th>\n",
" <th>NSTOR</th>\n",
" <th>GAR</th>\n",
" <th>AGE</th>\n",
" <th>CITCOU</th>\n",
" <th>LOTSZ</th>\n",
" <th>SQFT</th>\n",
" <th>X</th>\n",
" <th>Y</th>\n",
" <th>geometry</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>1</td>\n",
" <td>47.0</td>\n",
" <td>4.0</td>\n",
" <td>0.0</td>\n",
" <td>1.0</td>\n",
" <td>0.0</td>\n",
" <td>0.0</td>\n",
" <td>0.0</td>\n",
" <td>2.0</td>\n",
" <td>3.0</td>\n",
" <td>0.0</td>\n",
" <td>148.0</td>\n",
" <td>0.0</td>\n",
" <td>5.70</td>\n",
" <td>11.25</td>\n",
" <td>907.0</td>\n",
" <td>534.0</td>\n",
" <td>POINT (907.000 534.000)</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>2</td>\n",
" <td>113.0</td>\n",
" <td>7.0</td>\n",
" <td>1.0</td>\n",
" <td>2.5</td>\n",
" <td>1.0</td>\n",
" <td>1.0</td>\n",
" <td>1.0</td>\n",
" <td>2.0</td>\n",
" <td>2.0</td>\n",
" <td>2.0</td>\n",
" <td>9.0</td>\n",
" <td>1.0</td>\n",
" <td>279.51</td>\n",
" <td>28.92</td>\n",
" <td>922.0</td>\n",
" <td>574.0</td>\n",
" <td>POINT (922.000 574.000)</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>3</td>\n",
" <td>165.0</td>\n",
" <td>7.0</td>\n",
" <td>1.0</td>\n",
" <td>2.5</td>\n",
" <td>1.0</td>\n",
" <td>1.0</td>\n",
" <td>0.0</td>\n",
" <td>3.0</td>\n",
" <td>2.0</td>\n",
" <td>2.0</td>\n",
" <td>23.0</td>\n",
" <td>1.0</td>\n",
" <td>70.64</td>\n",
" <td>30.62</td>\n",
" <td>920.0</td>\n",
" <td>581.0</td>\n",
" <td>POINT (920.000 581.000)</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>4</td>\n",
" <td>104.3</td>\n",
" <td>7.0</td>\n",
" <td>1.0</td>\n",
" <td>2.5</td>\n",
" <td>1.0</td>\n",
" <td>1.0</td>\n",
" <td>1.0</td>\n",
" <td>2.0</td>\n",
" <td>2.0</td>\n",
" <td>2.0</td>\n",
" <td>5.0</td>\n",
" <td>1.0</td>\n",
" <td>174.63</td>\n",
" <td>26.12</td>\n",
" <td>923.0</td>\n",
" <td>578.0</td>\n",
" <td>POINT (923.000 578.000)</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>5</td>\n",
" <td>62.5</td>\n",
" <td>7.0</td>\n",
" <td>1.0</td>\n",
" <td>1.5</td>\n",
" <td>1.0</td>\n",
" <td>1.0</td>\n",
" <td>0.0</td>\n",
" <td>2.0</td>\n",
" <td>2.0</td>\n",
" <td>0.0</td>\n",
" <td>19.0</td>\n",
" <td>1.0</td>\n",
" <td>107.80</td>\n",
" <td>22.04</td>\n",
" <td>918.0</td>\n",
" <td>574.0</td>\n",
" <td>POINT (918.000 574.000)</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" STATION PRICE NROOM DWELL NBATH PATIO FIREPL AC BMENT NSTOR GAR \\\n",
"0 1 47.0 4.0 0.0 1.0 0.0 0.0 0.0 2.0 3.0 0.0 \n",
"1 2 113.0 7.0 1.0 2.5 1.0 1.0 1.0 2.0 2.0 2.0 \n",
"2 3 165.0 7.0 1.0 2.5 1.0 1.0 0.0 3.0 2.0 2.0 \n",
"3 4 104.3 7.0 1.0 2.5 1.0 1.0 1.0 2.0 2.0 2.0 \n",
"4 5 62.5 7.0 1.0 1.5 1.0 1.0 0.0 2.0 2.0 0.0 \n",
"\n",
" AGE CITCOU LOTSZ SQFT X Y geometry \n",
"0 148.0 0.0 5.70 11.25 907.0 534.0 POINT (907.000 534.000) \n",
"1 9.0 1.0 279.51 28.92 922.0 574.0 POINT (922.000 574.000) \n",
"2 23.0 1.0 70.64 30.62 920.0 581.0 POINT (920.000 581.000) \n",
"3 5.0 1.0 174.63 26.12 923.0 578.0 POINT (923.000 578.000) \n",
"4 19.0 1.0 107.80 22.04 918.0 574.0 POINT (918.000 574.000) "
]
},
"execution_count": 37,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"gdf.head()"
]
},
{
"cell_type": "code",
"execution_count": 38,
"metadata": {},
"outputs": [],
"source": [
"gdf1 = gdf.iloc[[0,1,2,3,4]]"
]
},
{
"cell_type": "code",
"execution_count": 39,
"metadata": {},
"outputs": [],
"source": [
"gdf2 = pandas.concat([gdf, gdf1, gdf1, gdf1])"
]
},
{
"cell_type": "code",
"execution_count": 40,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"0 False\n",
"1 False\n",
"2 False\n",
"3 False\n",
"4 False\n",
" ... \n",
"0 True\n",
"1 True\n",
"2 True\n",
"3 True\n",
"4 True\n",
"Length: 226, dtype: bool"
]
},
"execution_count": 40,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"gdf2.duplicated()"
]
},
{
"cell_type": "code",
"execution_count": 41,
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"/home/serge/Dropbox/p/pysal/src/subpackages/libpysal/libpysal/weights/weights.py:172: UserWarning: The weights matrix is not fully connected: \n",
" There are 10 disconnected components.\n",
" warnings.warn(message)\n"
]
}
],
"source": [
"w2 = libpysal.weights.KNN.from_dataframe(gdf2, k=2)"
]
},
{
"cell_type": "code",
"execution_count": 42,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"226"
]
},
"execution_count": 42,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"w2.n"
]
},
{
"cell_type": "code",
"execution_count": 43,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 43,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"w1.neighbors[0] == w2.neighbors[0]"
]
},
{
"cell_type": "code",
"execution_count": 44,
"metadata": {},
"outputs": [],
"source": [
"W1 = w1.full()[0]\n",
"W2 = w2.full()[0]"
]
},
{
"cell_type": "code",
"execution_count": 45,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 45,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"(W1 == W2[:w1.n,:w1.n]).all()"
]
},
{
"cell_type": "code",
"execution_count": 46,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"(226, 226)"
]
},
"execution_count": 46,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"W2.shape"
]
},
{
"cell_type": "code",
"execution_count": 47,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"(211, 211)"
]
},
"execution_count": 47,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"W1.shape"
]
},
{
"cell_type": "code",
"execution_count": 48,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{0: 2,\n",
" 1: 2,\n",
" 2: 2,\n",
" 3: 2,\n",
" 4: 2,\n",
" 5: 2,\n",
" 6: 2,\n",
" 7: 2,\n",
" 8: 2,\n",
" 9: 2,\n",
" 10: 2,\n",
" 11: 2,\n",
" 12: 2,\n",
" 13: 2,\n",
" 14: 2,\n",
" 15: 2,\n",
" 16: 2,\n",
" 17: 2,\n",
" 18: 2,\n",
" 19: 2,\n",
" 20: 2,\n",
" 21: 2,\n",
" 22: 2,\n",
" 23: 2,\n",
" 24: 2,\n",
" 25: 2,\n",
" 26: 2,\n",
" 27: 2,\n",
" 28: 2,\n",
" 29: 2,\n",
" 30: 2,\n",
" 31: 2,\n",
" 32: 2,\n",
" 33: 2,\n",
" 34: 2,\n",
" 35: 2,\n",
" 36: 2,\n",
" 37: 2,\n",
" 38: 2,\n",
" 39: 2,\n",
" 40: 2,\n",
" 41: 2,\n",
" 42: 2,\n",
" 43: 2,\n",
" 44: 2,\n",
" 45: 2,\n",
" 46: 2,\n",
" 47: 2,\n",
" 48: 2,\n",
" 49: 2,\n",
" 50: 2,\n",
" 51: 2,\n",
" 52: 2,\n",
" 53: 2,\n",
" 54: 2,\n",
" 55: 2,\n",
" 56: 2,\n",
" 57: 2,\n",
" 58: 2,\n",
" 59: 2,\n",
" 60: 2,\n",
" 61: 2,\n",
" 62: 2,\n",
" 63: 2,\n",
" 64: 2,\n",
" 65: 2,\n",
" 66: 2,\n",
" 67: 2,\n",
" 68: 2,\n",
" 69: 2,\n",
" 70: 2,\n",
" 71: 2,\n",
" 72: 2,\n",
" 73: 2,\n",
" 74: 2,\n",
" 75: 2,\n",
" 76: 2,\n",
" 77: 2,\n",
" 78: 2,\n",
" 79: 2,\n",
" 80: 2,\n",
" 81: 2,\n",
" 82: 2,\n",
" 83: 2,\n",
" 84: 2,\n",
" 85: 2,\n",
" 86: 2,\n",
" 87: 2,\n",
" 88: 2,\n",
" 89: 2,\n",
" 90: 2,\n",
" 91: 2,\n",
" 92: 2,\n",
" 93: 2,\n",
" 94: 2,\n",
" 95: 2,\n",
" 96: 2,\n",
" 97: 2,\n",
" 98: 2,\n",
" 99: 2,\n",
" 100: 2,\n",
" 101: 2,\n",
" 102: 2,\n",
" 103: 2,\n",
" 104: 2,\n",
" 105: 2,\n",
" 106: 2,\n",
" 107: 2,\n",
" 108: 2,\n",
" 109: 2,\n",
" 110: 2,\n",
" 111: 2,\n",
" 112: 2,\n",
" 113: 2,\n",
" 114: 2,\n",
" 115: 2,\n",
" 116: 2,\n",
" 117: 2,\n",
" 118: 2,\n",
" 119: 2,\n",
" 120: 2,\n",
" 121: 2,\n",
" 122: 2,\n",
" 123: 2,\n",
" 124: 2,\n",
" 125: 2,\n",
" 126: 2,\n",
" 127: 2,\n",
" 128: 2,\n",
" 129: 2,\n",
" 130: 2,\n",
" 131: 2,\n",
" 132: 2,\n",
" 133: 2,\n",
" 134: 2,\n",
" 135: 2,\n",
" 136: 2,\n",
" 137: 2,\n",
" 138: 2,\n",
" 139: 2,\n",
" 140: 2,\n",
" 141: 2,\n",
" 142: 2,\n",
" 143: 2,\n",
" 144: 2,\n",
" 145: 2,\n",
" 146: 2,\n",
" 147: 2,\n",
" 148: 2,\n",
" 149: 2,\n",
" 150: 2,\n",
" 151: 2,\n",
" 152: 2,\n",
" 153: 2,\n",
" 154: 2,\n",
" 155: 2,\n",
" 156: 2,\n",
" 157: 2,\n",
" 158: 2,\n",
" 159: 2,\n",
" 160: 2,\n",
" 161: 2,\n",
" 162: 2,\n",
" 163: 2,\n",
" 164: 2,\n",
" 165: 2,\n",
" 166: 2,\n",
" 167: 2,\n",
" 168: 2,\n",
" 169: 2,\n",
" 170: 2,\n",
" 171: 2,\n",
" 172: 2,\n",
" 173: 2,\n",
" 174: 2,\n",
" 175: 2,\n",
" 176: 2,\n",
" 177: 2,\n",
" 178: 2,\n",
" 179: 2,\n",
" 180: 2,\n",
" 181: 2,\n",
" 182: 2,\n",
" 183: 2,\n",
" 184: 2,\n",
" 185: 2,\n",
" 186: 2,\n",
" 187: 2,\n",
" 188: 2,\n",
" 189: 2,\n",
" 190: 2,\n",
" 191: 2,\n",
" 192: 2,\n",
" 193: 2,\n",
" 194: 2,\n",
" 195: 2,\n",
" 196: 2,\n",
" 197: 2,\n",
" 198: 2,\n",
" 199: 2,\n",
" 200: 2,\n",
" 201: 2,\n",
" 202: 2,\n",
" 203: 2,\n",
" 204: 2,\n",
" 205: 2,\n",
" 206: 2,\n",
" 207: 2,\n",
" 208: 2,\n",
" 209: 2,\n",
" 210: 2,\n",
" 211: 2,\n",
" 212: 2,\n",
" 213: 2,\n",
" 214: 2,\n",
" 215: 2,\n",
" 216: 2,\n",
" 217: 2,\n",
" 218: 2,\n",
" 219: 2,\n",
" 220: 2,\n",
" 221: 2,\n",
" 222: 2,\n",
" 223: 2,\n",
" 224: 2,\n",
" 225: 2}"
]
},
"execution_count": 48,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"w2.cardinalities"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.6"
}
},
"nbformat": 4,
"nbformat_minor": 4
}