Compare commits

...

30 Commits

Author SHA1 Message Date
Andy Eschbacher
6e540361af remove unneeded commented code 2017-02-27 12:14:48 -05:00
Andy Eschbacher
84d33d841f tests for new class 2016-11-15 12:03:54 +01:00
Andy Eschbacher
ded26dc46b adding class for database response 2016-11-15 12:03:24 +01:00
Andy Eschbacher
0d40080f6c move back to colnames 2016-11-15 12:02:42 +01:00
Andy Eschbacher
0867e69d1f replace plpy method colnames 2016-11-15 11:19:15 +01:00
Andy Eschbacher
cbe8571546 fixes argument in not-standardize 2016-11-15 10:10:07 +01:00
Andy Eschbacher
af536757fe adds silhouettes to output 2016-11-14 23:29:38 +00:00
Andy Eschbacher
b6dae5e380 adding silhouette 2016-11-15 00:15:23 +01:00
Andy Eschbacher
64c4b6611c changes cluster centers to json 2016-11-10 16:56:04 +00:00
Andy Eschbacher
a188b2e104 adds missing arguments 2016-10-21 15:51:54 -06:00
Andy Eschbacher
4389c9538d small updates for readability 2016-10-21 10:13:21 -06:00
Andy Eschbacher
3c6d73b7e2 Merge branch 'adds-nonspatial-kmeans' of https://github.com/CartoDB/crankshaft into adds-nonspatial-kmeans 2016-10-18 21:14:09 -06:00
Andy Eschbacher
3e0dba3522 update comments 2016-10-18 21:13:34 -06:00
Andy Eschbacher
5d8641732f change string formatting 2016-10-18 19:30:09 +00:00
Andy Eschbacher
f0c6cca766 fix key name 2016-10-18 13:05:56 -06:00
Andy Eschbacher
f800a35fd1 new format for input data 2016-10-18 13:01:31 -06:00
Andy Eschbacher
54bbd18b02 remove unneeded modules from test script 2016-10-18 12:12:38 -06:00
Andy Eschbacher
da23b002cf rename to match submodule name 2016-10-18 11:51:53 -06:00
Andy Eschbacher
a370a2da52 pep8 updates of test file 2016-10-18 11:50:59 -06:00
Andy Eschbacher
5404589058 Merge branch 'adds-nonspatial-kmeans' of https://github.com/CartoDB/crankshaft into adds-nonspatial-kmeans 2016-10-13 12:52:07 -04:00
Andy Eschbacher
b255fd3e06 make private functions more explictly private 2016-10-13 12:50:46 -04:00
Andy Eschbacher
0feaf36cf6 outputting consistent labels and centers 2016-10-13 15:52:00 +00:00
Andy Eschbacher
5d2a1881b1 make numpy with global scope in module 2016-10-13 15:00:28 +00:00
Andy Eschbacher
a95423174c adds back alias for kmeans removed by accident 2016-10-13 10:50:48 -04:00
Andy Eschbacher
4314f0f066 adds more robust data processing 2016-10-13 10:28:29 -04:00
Andy Eschbacher
c2e2359e65 addes minmax scaling for variables 2016-10-12 17:16:52 -04:00
Andy Eschbacher
361505fca9 fixes syntax errors 2016-10-12 21:13:51 +00:00
Andy Eschbacher
c47116571f properly close plpgsql function 2016-10-12 14:19:19 -04:00
Andy Eschbacher
3e1cef9958 fix output signature 2016-10-11 16:48:22 -04:00
Andy Eschbacher
947d6ba798 first add 2016-10-11 16:38:18 -04:00
7 changed files with 237 additions and 73 deletions

View File

@@ -1,21 +1,40 @@
CREATE OR REPLACE FUNCTION CDB_KMeans(query text, no_clusters integer,no_init integer default 20)
RETURNS table (cartodb_id integer, cluster_no integer) as $$
from crankshaft.clustering import kmeans
return kmeans(query,no_clusters,no_init)
-- Spatial k-means clustering
$$ language plpythonu;
CREATE OR REPLACE FUNCTION CDB_KMeans(query text, no_clusters integer, no_init integer default 20)
RETURNS table (cartodb_id integer, cluster_no integer) as $$
from crankshaft.clustering import kmeans
return kmeans(query, no_clusters, no_init)
$$ LANGUAGE plpythonu;
-- Non-spatial k-means clustering
-- query: sql query to retrieve all the needed data
CREATE OR REPLACE FUNCTION CDB_KMeansNonspatial(
query TEXT,
colnames TEXT[],
num_clusters INTEGER,
id_colname TEXT DEFAULT 'cartodb_id',
standarize BOOLEAN DEFAULT true
)
RETURNS TABLE(cluster_label text, cluster_center json, silhouettes numeric, rowid bigint) AS $$
from crankshaft.clustering import kmeans_nonspatial
return kmeans_nonspatial(query, colnames, num_clusters,
id_colname, standarize)
$$ LANGUAGE plpythonu;
CREATE OR REPLACE FUNCTION CDB_WeightedMeanS(state Numeric[],the_geom GEOMETRY(Point, 4326), weight NUMERIC)
RETURNS Numeric[] AS
RETURNS Numeric[] AS
$$
DECLARE
DECLARE
newX NUMERIC;
newY NUMERIC;
newW NUMERIC;
BEGIN
IF weight IS NULL OR the_geom IS NULL THEN
IF weight IS NULL OR the_geom IS NULL THEN
newX = state[1];
newY = state[2];
newW = state[3];
@@ -30,12 +49,12 @@ END
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION CDB_WeightedMeanF(state Numeric[])
RETURNS GEOMETRY AS
RETURNS GEOMETRY AS
$$
BEGIN
IF state[3] = 0 THEN
IF state[3] = 0 THEN
RETURN ST_SetSRID(ST_MakePoint(state[1],state[2]), 4326);
ELSE
ELSE
RETURN ST_SETSRID(ST_MakePoint(state[1]/state[3], state[2]/state[3]),4326);
END IF;
END
@@ -56,7 +75,7 @@ BEGIN
SFUNC = CDB_WeightedMeanS,
FINALFUNC = CDB_WeightedMeanF,
STYPE = Numeric[],
INITCOND = "{0.0,0.0,0.0}"
INITCOND = "{0.0,0.0,0.0}"
);
END IF;
END

View File

@@ -1,18 +1,109 @@
from sklearn.cluster import KMeans
import plpy
import numpy as np
def kmeans(query, no_clusters, no_init=20):
data = plpy.execute('''select array_agg(cartodb_id order by cartodb_id) as ids,
array_agg(ST_X(the_geom) order by cartodb_id) xs,
array_agg(ST_Y(the_geom) order by cartodb_id) ys from ({query}) a
where the_geom is not null
'''.format(query=query))
"""
find centers based on clusteres of latitude/longitude pairs
query: SQL query that has a WGS84 geometry (the_geom)
"""
full_query = ("SELECT array_agg(cartodb_id ORDER BY cartodb_id) as ids,"
"array_agg(ST_X(the_geom) ORDER BY cartodb_id) xs,"
"array_agg(ST_Y(the_geom) ORDER BY cartodb_id) ys "
"FROM ({query}) As a "
"WHERE the_geom IS NOT NULL").format(query=query)
try:
data = plpy.execute(full_query)
except plpy.SPIError, err:
plpy.error("k-means (spatial) cluster analysis failed: %s" % err)
xs = data[0]['xs']
ys = data[0]['ys']
# Unpack query response
xs = data[0]['xs']
ys = data[0]['ys']
ids = data[0]['ids']
km = KMeans(n_clusters= no_clusters, n_init=no_init)
labels = km.fit_predict(zip(xs,ys))
return zip(ids,labels)
km = KMeans(n_clusters=no_clusters, n_init=no_init)
labels = km.fit_predict(zip(xs, ys))
return zip(ids, labels)
def kmeans_nonspatial(query, colnames, num_clusters=5,
id_col='cartodb_id', standarize=True):
"""
query (string): A SQL query to retrieve the data required to do the
k-means clustering analysis, like so:
SELECT * FROM iris_flower_data
colnames (list): a list of the column names which contain the data of
interest, like so: ["sepal_width", "petal_width",
"sepal_length", "petal_length"]
num_clusters (int): number of clusters (greater than zero)
id_col (string): name of the input id_column
"""
import json
from sklearn import metrics
out_id_colname = 'rowids'
# TODO: need a random seed?
full_query = '''
SELECT {cols}, array_agg({id_col}) As {out_id_colname}
FROM ({query}) As a
'''.format(query=query,
id_col=id_col,
out_id_colname=out_id_colname,
cols=', '.join(['array_agg({0}) As col{1}'.format(val, idx)
for idx, val in enumerate(colnames)]))
try:
db_resp = plpy.execute(full_query)
except plpy.SPIError, err:
plpy.error("k-means (non-spatial) cluster analysis failed: %s" % err)
# fill array with values for k-means clustering
if standarize:
cluster_columns = _scale_data(
_extract_columns(db_resp, out_id_colname))
else:
cluster_columns = _extract_columns(db_resp, out_id_colname)
# TODO: decide on optimal parameters for most cases
# Are there ways of deciding parameters based on inputs?
kmeans = KMeans(n_clusters=num_clusters,
random_state=0).fit(cluster_columns)
centers = [json.dumps(dict(zip(colnames, c)))
for c in kmeans.cluster_centers_[kmeans.labels_]]
silhouettes = metrics.silhouette_samples(cluster_columns,
kmeans.labels_,
metric='sqeuclidean')
return zip(kmeans.labels_,
centers,
silhouettes,
db_resp[0][out_id_colname])
def _extract_columns(db_resp, id_col_name):
"""
Extract the features from the query and pack them into a NumPy array
db_resp (plpy data object): result of the kmeans request
id_col_name (string): name of column which has the row id (not a
feature of the analysis)
"""
return np.array([db_resp[0][c] for c in db_resp.colnames()
if c != id_col_name],
dtype=float).T
# -- Preprocessing steps
def _scale_data(features):
"""
Scale all input columns to center on 0 with a standard devation of 1
features (numpy matrix): features of dimension (n_features, n_samples)
"""
from sklearn.preprocessing import StandardScaler
return StandardScaler().fit_transform(features)

View File

@@ -2,6 +2,7 @@ import unittest
from mock_plpy import MockPlPy
plpy = MockPlPy()
from mock_plpy import MockDBResponse
import sys
sys.modules['plpy'] = plpy

View File

@@ -1,12 +1,13 @@
import re
class MockCursor:
def __init__(self, data):
self.cursor_pos = 0
self.data = data
def fetch(self, batch_size):
batch = self.data[self.cursor_pos : self.cursor_pos + batch_size]
batch = self.data[self.cursor_pos:self.cursor_pos + batch_size]
self.cursor_pos += batch_size
return batch
@@ -45,8 +46,22 @@ class MockPlPy:
data = self.execute(query)
return MockCursor(data)
def execute(self, query): # TODO: additional arguments
for result in self.results:
if result[0].match(query):
return result[1]
return []
# TODO: additional arguments
def execute(self, query):
for result in self.results:
if result[0].match(query):
return result[1]
return []
class MockDBResponse:
def __init__(self, data, colnames=None):
self.data = data
if colnames is None:
self.colnames = data[0].keys()
else:
self.colnames = colnames
def colnames(self):
return self.colnames

View File

@@ -1,38 +0,0 @@
import unittest
import numpy as np
# from mock_plpy import MockPlPy
# plpy = MockPlPy()
#
# import sys
# sys.modules['plpy'] = plpy
from helper import plpy, fixture_file
import numpy as np
import crankshaft.clustering as cc
import crankshaft.pysal_utils as pu
from crankshaft import random_seeds
import json
class KMeansTest(unittest.TestCase):
"""Testing class for Moran's I functions"""
def setUp(self):
plpy._reset()
self.cluster_data = json.loads(open(fixture_file('kmeans.json')).read())
self.params = {"subquery": "select * from table",
"no_clusters": "10"
}
def test_kmeans(self):
data = self.cluster_data
plpy._define_result('select' ,data)
clusters = cc.kmeans('subquery', 2)
labels = [a[1] for a in clusters]
c1 = [a for a in clusters if a[1]==0]
c2 = [a for a in clusters if a[1]==1]
self.assertEqual(len(np.unique(labels)),2)
self.assertEqual(len(c1),20)
self.assertEqual(len(c2),20)

View File

@@ -0,0 +1,73 @@
import unittest
import numpy as np
# from mock_plpy import MockPlPy
# plpy = MockPlPy()
#
# import sys
# sys.modules['plpy'] = plpy
from helper import plpy, fixture_file, MockDBResponse
import crankshaft.clustering as cc
import json
from collections import OrderedDict
class KMeansTest(unittest.TestCase):
"""Testing class for k-means spatial"""
def setUp(self):
plpy._reset()
self.cluster_data = json.loads(
open(fixture_file('kmeans.json')).read())
self.params = {"subquery": "select * from table",
"no_clusters": "10"}
def test_kmeans(self):
"""
"""
data = [{'xs': d['xs'],
'ys': d['ys'],
'ids': d['ids']} for d in self.cluster_data]
plpy._define_result('select', data)
clusters = cc.kmeans('subquery', 2)
labels = [a[1] for a in clusters]
c1 = [a for a in clusters if a[1] == 0]
c2 = [a for a in clusters if a[1] == 1]
self.assertEqual(len(np.unique(labels)), 2)
self.assertEqual(len(c1), 20)
self.assertEqual(len(c2), 20)
class KMeansNonspatialTest(unittest.TestCase):
"""Testing class for k-means non-spatial"""
def setUp(self):
plpy._reset()
self.params = {"subquery": "SELECT * FROM TABLE",
"n_clusters": 5}
def test_kmeans_nonspatial(self):
"""
test for k-means non-spatial
"""
data_raw = [OrderedDict([("col1", [1, 1, 1, 4, 4, 4]),
("col2", [2, 4, 0, 2, 4, 0]),
("rowids", [1, 2, 3, 4, 5, 6])])]
data_obj = MockDBResponse(data_raw, [k for k in data_raw[0]
if k != 'rowids'])
plpy._define_result('select', data_obj)
clusters = cc.kmeans_nonspatial('subquery', ['col1', 'col2'], 4)
cl1 = clusters[0][1]
cl2 = clusters[3][1]
for idx, val in enumerate(clusters):
if idx < 3:
self.assertEqual(val[1], cl1)
else:
self.assertEqual(val[1], cl2)

View File

@@ -7,13 +7,13 @@ import numpy as np
#
# import sys
# sys.modules['plpy'] = plpy
from helper import plpy, fixture_file
from helper import plpy, fixture_file, MockDBResponse
import crankshaft.clustering as cc
import crankshaft.pysal_utils as pu
from crankshaft import random_seeds
import json
from collections import OrderedDict
class MoranTest(unittest.TestCase):
"""Testing class for Moran's I functions"""
@@ -58,11 +58,14 @@ class MoranTest(unittest.TestCase):
def test_moran_local(self):
"""Test Moran's I local"""
data = [{'id': d['id'],
'attr1': d['value'],
'neighbors': d['neighbors']} for d in self.neighbors_data]
data = [OrderedDict([('id', d['id']),
('attr1', d['value']),
('neighbors', d['neighbors'])])
for d in self.neighbors_data]
plpy._define_result('select', data)
db_resp = MockDBResponse(data)
plpy._define_result('select', db_resp)
random_seeds.set_random_seeds(1234)
result = cc.moran_local('subquery', 'value',
'knn', 5, 99, 'the_geom', 'cartodb_id')