forked from DrSkippy/Data-Science-45min-Intros
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathgap_stats.py
More file actions
187 lines (143 loc) · 7.37 KB
/
Copy pathgap_stats.py
File metadata and controls
187 lines (143 loc) · 7.37 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
__author__="Josh Montague"
__license__="MIT License"
# Modified from:
# (c) 2014 Reid Johnson
#
# Modified from:
# (c) 2013 Mikael Vejdemo-Johansson
# BSD License
#
#
# The gap statistic is defined by Tibshirani, Walther, Hastie in:
# Estimating the number of clusters in a data set via the gap statistic
# J. R. Statist. Soc. B (2001) 63, Part 2, pp 411-423
import matplotlib.pyplot as plt
import numpy as np
import scipy as sp
import scipy.spatial.distance
import scipy.stats
from sklearn.cluster import KMeans
dst = sp.spatial.distance.euclidean
def gap_statistics(data, refs=None, nrefs=10, ks=range(1,11)):
"""Computes the gap statistics for an nxm dataset.
The gap statistic measures the difference between within-cluster dispersion on an input
dataset and that expected under an appropriate reference null distribution.
Computation of the gap statistic, then, requires a series of reference (null) distributions.
One may either input a precomputed set of reference distributions (via the parameter refs)
or specify the number of reference distributions (via the parameter nrefs) for automatic
generation of uniform distributions within the bounding box of the dataset (data).
Each computation of the gap statistic requires the clustering of the input dataset and of
several reference distributions. To identify the optimal number of clusters k, the gap
statistic is computed over a range of possible values of k (via the parameter ks).
For each value of k, within-cluster dispersion is calculated for the input dataset and each
reference distribution. The calculation of the within-cluster dispersion for the reference
distributions will have a degree of variation, which we measure by standard deviation or
standard error.
The estimated optimal number of clusters, then, is defined as the smallest value k such that
gap_k is greater than or equal to the sum of gap_k+1 minus the expected error err_k+1.
Args:
data ((n,m) SciPy array): The dataset on which to compute the gap statistics.
refs ((n,m,k) SciPy array, optional): A precomputed set of reference distributions.
Defaults to None.
nrefs (int, optional): The number of reference distributions for automatic generation.
Defaults to 20.
ks (list, optional): The list of values k for which to compute the gap statistics.
Defaults to range(1,11), which creates a list of values from 1 to 10.
Returns:
gaps: an array of gap statistics computed for each k.
errs: an array of standard errors (se), with one corresponding to each gap computation.
difs: an array of differences between each gap_k and the sum of gap_k+1 minus err_k+1.
"""
shape = data.shape
if refs==None:
tops = data.max(axis=0) # maxima along the first axis (rows)
bots = data.min(axis=0) # minima along the first axis (rows)
dists = sp.matrix(sp.diag(tops-bots)) # the bounding box of the input dataset
# Generate nrefs uniform distributions each in the half-open interval [0.0, 1.0)
rands = sp.random.random_sample(size=(shape[0],shape[1], nrefs))
# Adjust each of the uniform distributions to the bounding box of the input dataset
for i in range(nrefs):
rands[:,:,i] = rands[:,:,i]*dists+bots
else:
rands = refs
gaps = sp.zeros((len(ks),)) # array for gap statistics (lenth ks)
errs = sp.zeros((len(ks),)) # array for model standard errors (length ks)
difs = sp.zeros((len(ks)-1,)) # array for differences between gaps (length ks-1)
for (i,k) in enumerate(ks): # iterate over the range of k values
# Cluster the input dataset via k-means clustering using the current value of k
kmeans = KMeans(n_clusters=k, n_init=2, n_jobs=-1).fit(data)
(kmc, kml) = kmeans.cluster_centers_, kmeans.labels_
# Generate within-dispersion measure for the clustering of the input dataset
disp = sum([dst(data[m,:],kmc[kml[m],:]) for m in range(shape[0])])
# Generate within-dispersion measures for the clusterings of the reference datasets
refdisps = sp.zeros((rands.shape[2],))
for j in range(rands.shape[2]):
# Cluster the reference dataset via k-means clustering using the current value of k
kmeans = KMeans(n_clusters=k, n_init=2, n_jobs=-1).fit(rands[:,:,j])
(kmc, kml) = kmeans.cluster_centers_, kmeans.labels_
refdisps[j] = sum([dst(rands[m,:,j],kmc[kml[m],:]) for m in range(shape[0])])
# Compute the (estimated) gap statistic for k
gaps[i] = sp.mean(sp.log(refdisps) - sp.log(disp))
# Compute the expected error for k
errs[i] = sp.sqrt(sum(((sp.log(refdisp)-sp.mean(sp.log(refdisps)))**2) \
for refdisp in refdisps)/float(nrefs)) * sp.sqrt(1+1/nrefs)
# Compute the difference between gap_k and the sum of gap_k+1 minus err_k+1
difs = sp.array([gaps[k] - (gaps[k+1]-errs[k+1]) for k in range(len(gaps)-1)])
#print "Gaps: " + str(gaps)
#print "Errs: " + str(errs)
#print "Difs: " + str(difs)
return gaps, errs, difs
def plot_gap_statistics(gaps, errs, difs):
"""Generates and shows plots for the gap statistics.
A figure with two subplots is generated. The first subplot is an errorbar plot of the
estimated gap statistics computed for each value of k. The second subplot is a barplot
of the differences in the computed gap statistics computed.
Args:
gaps (SciPy array): An array of gap statistics, one computed for each k.
errs (SciPy array): An array of standard errors (se), with one corresponding to each gap
computation.
difs (SciPy array): An array of differences between each gap_k and the sum of gap_k+1
minus err_k+1.
"""
# Create a figure
fig = plt.figure(figsize=(8,8))
#plt.subplots_adjust(wspace=0.35) # adjust the distance between figures
# Subplot 1
ax = fig.add_subplot(211)
ind = range(1,len(gaps)+1) # the x values for the gaps
# Create an errorbar plot
rects = ax.errorbar(ind, gaps, yerr=errs, xerr=None, linewidth=1.0)
# Add figure labels and ticks
ax.set_title('Clustering Gap Statistics', fontsize=16)
ax.set_xlabel('Number of clusters k', fontsize=14)
ax.set_ylabel('Gap Statistic', fontsize=14)
ax.set_xticks(ind)
# Add figure bounds
ax.set_ylim(0, max(gaps+errs)*1.1)
ax.set_xlim(0, len(gaps)+1.0)
# space b/w subplots
fig.subplots_adjust(hspace=.5)
# Subplot 2
ax = fig.add_subplot(212)
ind = range(1,len(difs)+1) # the x values for the difs
max_gap = None
if len(np.where(difs > 0)[0]) > 0:
max_gap = np.where(difs > 0)[0][0] + 1 # the k with the first positive dif
# Create a bar plot
ax.bar(ind, difs, alpha=0.5, color='g', align='center')
# Add figure labels and ticks
if max_gap:
ax.set_title('Clustering Gap Differences\n(k=%d Estimated as Optimal)' % (max_gap), \
fontsize=16)
else:
ax.set_title('Clustering Gap Differences\n', fontsize=16)
ax.set_xlabel('Number of clusters k', fontsize=14)
ax.set_ylabel('Gap Difference', fontsize=14)
ax.xaxis.set_ticks(range(1,len(difs)+1))
# Add figure bounds
ax.set_ylim(min(difs)*1.2, max(difs)*1.2)
ax.set_xlim(0, len(difs)+1.0)
# Show the figure
plt.show()