Quick Cluster: Reimplement clustering algorithm
authorJohn Darrington <john@darrington.wattle.id.au>
Sat, 7 Nov 2015 11:16:13 +0000 (12:16 +0100)
committerJohn Darrington <john@darrington.wattle.id.au>
Tue, 10 Nov 2015 17:20:51 +0000 (18:20 +0100)
This change replaces the algorithm to be closer to that described by
SPSS Statistical Algorithms.  It results in better classification of
"real" data, and better seperated cluster centers.

It also provides a few subcommands which were previously not
implemented.

AUTHORS
NEWS
doc/statistics.texi
src/language/stats/quick-cluster.c
tests/language/stats/quick-cluster.at

diff --git a/AUTHORS b/AUTHORS
index 97c179e894f5628abd7bc14cddfe4ae05cdb6425..5c0808287f2653b09b67c76d6520c1300dff329c 100644 (file)
--- a/AUTHORS
+++ b/AUTHORS
@@ -14,8 +14,8 @@ revisions to other modules.
 including lib/gslextras and the linear regression features. Jason 
 is also an important contributor to GSL, which is used by PSPP. 
 
-* Mehmet Hakan Satman wrote the QUICK CLUSTER command and Alan Mead
-  contributed improvements including the cluster membership subcommands.
+* Mehmet Hakan Satman provided the initial implementation of the QUICK CLUSTER command.
+  Alan Mead and others contributed later improvements including the cluster membership subcommands.
 
 * Friedrich Beckmann wrote the GRAPH command.
 
diff --git a/NEWS b/NEWS
index 2999c5e62c2191ee57f2eb9f6c6c8955c77394af..14000fe95802b4832ef928c29ae5d7afa513cf98 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -8,7 +8,8 @@ Changes from 0.8.5 to 0.9.0:
 
  * The QUICK CLUSTER command has a  /PRINT subcommand which shows
    the initial cluster centres and the final cluster membership of
-   each case.
+   each case.  The clustering algorithm has also been updated, so
+   as to produce better separated clusters.
 
  * A Russian localisation has been contributed.
 
index ac3f0b5c06f64e97df1f27417a0ccb67c4ff6881..85217cd636fef532832114c5182e1aac4d30fee9 100644 (file)
@@ -1647,7 +1647,7 @@ The default is 0.05.
 
 @display
 QUICK CLUSTER @var{var_list}
-      [/CRITERIA=CLUSTERS(@var{k}) [MXITER(@var{max_iter})]]
+      [/CRITERIA=CLUSTERS(@var{k}) [MXITER(@var{max_iter})] CONVERGE(@var{epsilon}) [NOINITIAL]]
       [/MISSING=@{EXCLUDE,INCLUDE@} @{LISTWISE, PAIRWISE@}]
       [/PRINT=@{INITIAL@} @{CLUSTERS@}]
 @end display
@@ -1659,11 +1659,29 @@ of similar values and you already know the number of clusters.
 The minimum specification is @samp{QUICK CLUSTER} followed by the names
 of the variables which contain the cluster data.  Normally you will also
 want to specify @subcmd{/CRITERIA=CLUSTERS(@var{k})} where @var{k} is the
-number of clusters.  If this is not given, then @var{k} defaults to 2.
+number of clusters.  If this is not specified, then @var{k} defaults to 2.
 
-The command uses an iterative algorithm to determine the clusters for
-each case.  It will continue iterating until convergence, or until @var{max_iter}
-iterations have been done.  The default value of @var{max_iter} is 2.
+If you use @subcmd{/CRITERIA=NOINITIAL} then a naive algorithm to select
+the initial clusters is used.   This will provide for faster execution but
+less well separated initial clusters and hence possibly an inferior final
+result.
+
+
+@cmd{QUICK CLUSTER} uses an iterative algorithm to select the clusters centers.
+The subcommand  @subcmd{/CRITERIA=MXITER(@var{max_iter})} sets the maximum number of iterations.
+During classification, @pspp{} will continue iterating until until @var{max_iter}
+iterations have been done or the convergence criterion (see below) is fulfilled.
+The default value of @var{max_iter} is 2.
+
+If however, you specify @subcmd{/CRITERIA=NOUPDATE} then after selecting the initial centers,
+no further update to the cluster centers is done.  In this case, @var{max_iter}, if specified.
+is ignored.
+
+The subcommand  @subcmd{/CRITERIA=CONVERGE(@var{epsilon})} is used
+to set the convergence criterion.  The value of convergence criterion is  @var{epsilon}
+times the minimum distance between the @emph{initial} cluster centers.  Iteration stops when
+the  mean cluster distance between  one iteration and the next  
+is less than the convergence criterion.  The default value of @var{epsilon} is zero.
 
 The @subcmd{MISSING} subcommand determines the handling of missing variables.  
 If @subcmd{INCLUDE} is set, then user-missing values are considered at their face
index 28ceea3e733329173c03bcbbd54cdeadead2cb35..4f34e4f0a8ef0eabcb955d4eb1588d634264f33e 100644 (file)
@@ -37,6 +37,7 @@
 #include "language/lexer/variable-parser.h"
 #include "libpspp/message.h"
 #include "libpspp/misc.h"
+#include "libpspp/assertion.h"
 #include "libpspp/str.h"
 #include "math/random.h"
 #include "output/tab.h"
@@ -58,10 +59,14 @@ struct qc
   const struct variable **vars;
   size_t n_vars;
 
+  double epsilon;               /* The convergence criterium */
+
   int ngroups;                 /* Number of group. (Given by the user) */
   int maxiter;                 /* Maximum iterations (Given by the user) */
-  int print_cluster_membership; /* true => print membership */
-  int print_initial_clusters;   /* true => print initial cluster */
+  bool print_cluster_membership; /* true => print membership */
+  bool print_initial_clusters;   /* true => print initial cluster */
+  bool no_initial;              /* true => simplified initial cluster selection */
+  bool no_update;               /* true => do not iterate  */
 
   const struct variable *wv;   /* Weighting variable. */
 
@@ -75,30 +80,19 @@ struct qc
 struct Kmeans
 {
   gsl_matrix *centers;         /* Centers for groups. */
-  gsl_vector_long *num_elements_groups;
+  gsl_matrix *updated_centers;
+  casenumber n;
 
-  casenumber n;                        /* Number of observations (default -1). */
+  gsl_vector_long *num_elements_groups;
 
-  int lastiter;                        /* Iteration where it found the solution. */
-  int trials;                  /* If not convergence, how many times has
-                                   clustering done. */
   gsl_matrix *initial_centers; /* Initial random centers. */
-
+  double convergence_criteria;
   gsl_permutation *group_order;        /* Group order for reporting. */
-  struct caseproto *proto;
-  struct casereader *index_rdr;        /* Group ids for each case. */
 };
 
 static struct Kmeans *kmeans_create (const struct qc *qc);
 
-static void kmeans_randomize_centers (struct Kmeans *kmeans, const struct casereader *reader, const struct qc *qc);
-
-static int kmeans_get_nearest_group (struct Kmeans *kmeans, struct ccase *c, const struct qc *);
-
-static void kmeans_recalculate_centers (struct Kmeans *kmeans, const struct casereader *reader, const struct qc *);
-
-static int
-kmeans_calculate_indexes_and_check_convergence (struct Kmeans *kmeans, const struct casereader *reader, const struct qc *);
+static void kmeans_get_nearest_group (const struct Kmeans *kmeans, struct ccase *c, const struct qc *, int *, double *, int *, double *);
 
 static void kmeans_order_groups (struct Kmeans *kmeans, const struct qc *);
 
@@ -124,16 +118,11 @@ kmeans_create (const struct qc *qc)
 {
   struct Kmeans *kmeans = xmalloc (sizeof (struct Kmeans));
   kmeans->centers = gsl_matrix_alloc (qc->ngroups, qc->n_vars);
+  kmeans->updated_centers = gsl_matrix_alloc (qc->ngroups, qc->n_vars);
   kmeans->num_elements_groups = gsl_vector_long_alloc (qc->ngroups);
-  kmeans->n = 0;
-  kmeans->lastiter = 0;
-  kmeans->trials = 0;
   kmeans->group_order = gsl_permutation_alloc (kmeans->centers->size1);
   kmeans->initial_centers = NULL;
 
-  kmeans->proto = caseproto_create ();
-  kmeans->proto = caseproto_add_width (kmeans->proto, 0);
-  kmeans->index_rdr = NULL;
   return (kmeans);
 }
 
@@ -141,197 +130,258 @@ static void
 kmeans_destroy (struct Kmeans *kmeans)
 {
   gsl_matrix_free (kmeans->centers);
+  gsl_matrix_free (kmeans->updated_centers);
   gsl_matrix_free (kmeans->initial_centers);
 
   gsl_vector_long_free (kmeans->num_elements_groups);
 
   gsl_permutation_free (kmeans->group_order);
 
-  caseproto_unref (kmeans->proto);
+  free (kmeans);
+}
 
-  casereader_destroy (kmeans->index_rdr);
+static double
+diff_matrix (const gsl_matrix *m1, const gsl_matrix *m2)
+{
+  int i,j;
+  double max_diff = -INFINITY;
+  for (i = 0; i < m1->size1; ++i)
+    {
+      double diff = 0;
+      for (j = 0; j < m1->size2; ++j)
+       {
+         diff += pow2 (gsl_matrix_get (m1,i,j) - gsl_matrix_get (m2,i,j) );
+       }
+      if (diff > max_diff)
+       max_diff = diff;
+    }
 
-  free (kmeans);
+  return max_diff;
 }
 
-/* Creates random centers using randomly selected cases from the data. */
-static void
-kmeans_randomize_centers (struct Kmeans *kmeans, const struct casereader *reader UNUSED, const struct qc *qc)
+
+
+static double 
+matrix_mindist (const gsl_matrix *m, int *mn, int *mm)
 {
   int i, j;
-  for (i = 0; i < qc->ngroups; i++)
+  double mindist = INFINITY;
+  for (i = 0; i < m->size1 - 1; ++i)
     {
-      for (j = 0; j < qc->n_vars; j++)
+      for (j = i + 1; j < m->size1; ++j)
        {
-         if (i == j)
+         int k;
+         double diff_sq = 0;
+         for (k = 0; k < m->size2; ++k)
            {
-             gsl_matrix_set (kmeans->centers, i, j, 1);
+             diff_sq += pow2 (gsl_matrix_get (m, j, k) - gsl_matrix_get (m, i, k));
            }
-         else
+         if (diff_sq < mindist)
            {
-             gsl_matrix_set (kmeans->centers, i, j, 0);
+             mindist = diff_sq;
+             if (mn)
+               *mn = i;
+             if (mm)
+               *mm = j;
            }
        }
     }
-  /* If it is the first iteration, the variable kmeans->initial_centers is NULL
-     and it is created once for reporting issues. In SPSS, initial centers are
-     shown in the reports but in PSPP it is not shown now. I am leaving it
-     here. */
-  if (!kmeans->initial_centers)
+
+  return sqrt (mindist);
+}
+
+
+static void
+dump_matrix (const gsl_matrix *m)
+{
+  size_t i, j;
+
+  for (i = 0 ; i < m->size1; ++i)
+    {
+      for (j = 0 ; j < m->size2; ++j)
+       printf ("%02f ", gsl_matrix_get (m, i, j));
+      printf ("\n");
+    }
+}
+
+
+/* Return the distance of C from the group whose index is WHICH */
+static double
+dist_from_case (const struct Kmeans *kmeans, const struct ccase *c, const struct qc *qc, int which)
+{
+  int j;
+  double dist = 0;
+  for (j = 0; j < qc->n_vars; j++)
     {
-      kmeans->initial_centers = gsl_matrix_alloc (qc->ngroups, qc->n_vars);
-      gsl_matrix_memcpy (kmeans->initial_centers, kmeans->centers);
+      const union value *val = case_data (c, qc->vars[j]);
+      if ( var_is_value_missing (qc->vars[j], val, qc->exclude))
+       NOT_REACHED ();
+      
+      dist += pow2 (gsl_matrix_get (kmeans->centers, which, j) - val->f);
     }
+  return sqrt (dist);
 }
 
-static int
-kmeans_get_nearest_group (struct Kmeans *kmeans, struct ccase *c, const struct qc *qc)
+/* Return the minimum distance of the group WHICH and all other groups */
+static double
+min_dist_from (const struct Kmeans *kmeans, const struct qc *qc, int which)
 {
-  int result = -1;
-  int i, j;
+  int j, i;
+
   double mindist = INFINITY;
   for (i = 0; i < qc->ngroups; i++)
     {
+      if (i == which)
+       continue;
+
       double dist = 0;
       for (j = 0; j < qc->n_vars; j++)
        {
-         const union value *val = case_data (c, qc->vars[j]);
-         if ( var_is_value_missing (qc->vars[j], val, qc->exclude))
-           continue;
-
-         dist += pow2 (gsl_matrix_get (kmeans->centers, i, j) - val->f);
+         dist += pow2 (gsl_matrix_get (kmeans->centers, i, j) - gsl_matrix_get (kmeans->centers, which, j));
        }
+      
       if (dist < mindist)
        {
          mindist = dist;
-         result = i;
        }
     }
-  return (result);
+
+  return sqrt (mindist);
 }
 
-/* Re-calculate the cluster centers. */
+
+
+/* Calculate the intial cluster centers. */
 static void
-kmeans_recalculate_centers (struct Kmeans *kmeans, const struct casereader *reader, const struct qc *qc)
+kmeans_initial_centers (struct Kmeans *kmeans, const struct casereader *reader, const struct qc *qc)
 {
-  casenumber i = 0;
-  int v, j;
   struct ccase *c;
+  int nc = 0, j;
 
   struct casereader *cs = casereader_clone (reader);
-  struct casereader *cs_index = casereader_clone (kmeans->index_rdr);
-
-  gsl_matrix_set_all (kmeans->centers, 0.0);
   for (; (c = casereader_read (cs)) != NULL; case_unref (c))
     {
-      double weight = qc->wv ? case_data (c, qc->wv)->f : 1.0;
-      struct ccase *c_index = casereader_read (cs_index);
-      int index = case_data_idx (c_index, 0)->f;
-      for (v = 0; v < qc->n_vars; ++v)
+      bool missing = false;
+      for (j = 0; j < qc->n_vars; ++j)
        {
-         const union value *val = case_data (c, qc->vars[v]);
-         double x = val->f * weight;
-         double curval;
-
-         if ( var_is_value_missing (qc->vars[v], val, qc->exclude))
-           continue;
+         const union value *val = case_data (c, qc->vars[j]);
+         if ( var_is_value_missing (qc->vars[j], val, qc->exclude))
+           {
+             missing = true;
+             break;
+           }
 
-         curval = gsl_matrix_get (kmeans->centers, index, v);
-         gsl_matrix_set (kmeans->centers, index, v, curval + x);
+         if (nc < qc->ngroups)
+           gsl_matrix_set (kmeans->centers, nc, j, val->f);
        }
-      i++;
-      case_unref (c_index);
-    }
-  casereader_destroy (cs);
-  casereader_destroy (cs_index);
 
-  /* Getting number of cases */
-  if (kmeans->n == 0)
-    kmeans->n = i;
+      if (missing)
+       continue;
 
-  /* We got sum of each center but we need averages.
-     We are dividing centers to numobs. This may be inefficient and
-     we should check it again. */
-  for (i = 0; i < qc->ngroups; i++)
-    {
-      casenumber numobs = kmeans->num_elements_groups->data[i];
-      for (j = 0; j < qc->n_vars; j++)
+      if (nc++ < qc->ngroups)
+       continue;
+
+      if (!qc->no_initial)
        {
-         if (numobs > 0)
+         int mq, mp;
+         double delta;
+
+         int mn, mm;
+         double m = matrix_mindist (kmeans->centers, &mn, &mm);
+
+         kmeans_get_nearest_group (kmeans, c, qc, &mq, &delta, &mp, NULL);
+         if (delta > m)
+           /* If the distance between C and the nearest group, is greater than the distance
+              between the two  groups which are clostest to each other, then one group must be replaced */
            {
-             double *x = gsl_matrix_ptr (kmeans->centers, i, j);
-             *x /= numobs;
+             /* Out of mn and mm, which is the clostest of the two groups to C ? */
+             int which = (dist_from_case (kmeans, c, qc, mn) > dist_from_case (kmeans, c, qc, mm)) ? mm : mn;
+
+             for (j = 0; j < qc->n_vars; ++j)
+               {
+                 const union value *val = case_data (c, qc->vars[j]);
+                 gsl_matrix_set (kmeans->centers, which, j, val->f);
+               }
            }
-         else
+         else if (dist_from_case (kmeans, c, qc, mp) > min_dist_from (kmeans, qc, mq))
+           /* If the distance between C and the second nearest group (MP) is greater than the 
+              smallest distance between the nearest group (MQ) and any other group, then replace
+              MQ with C */
            {
-             gsl_matrix_set (kmeans->centers, i, j, 0);
+             for (j = 0; j < qc->n_vars; ++j)
+               {
+                 const union value *val = case_data (c, qc->vars[j]);
+                 gsl_matrix_set (kmeans->centers, mq, j, val->f);
+               }
            }
        }
     }
-}
 
-/* The variable index in struct Kmeans holds integer values that represents the
-   current groups of cases.  index[n]=a shows the nth case is belong to ath
-   cluster.  This function calculates these indexes and returns the number of
-   different cases of the new and old index variables.  If last two index
-   variables are equal, there is no any enhancement of clustering. */
-static int
-kmeans_calculate_indexes_and_check_convergence (struct Kmeans *kmeans, const struct casereader *reader, const struct qc *qc)
-{
-  int totaldiff = 0;
-  struct ccase *c;
-  struct casereader *cs = casereader_clone (reader);
+  casereader_destroy (cs);
 
-  /* A casewriter into which we will write the indexes. */
-  struct casewriter *index_wtr = autopaging_writer_create (kmeans->proto);
+  kmeans->convergence_criteria = qc->epsilon * matrix_mindist (kmeans->centers, NULL, NULL);
 
-  gsl_vector_long_set_all (kmeans->num_elements_groups, 0);
+  /* As it is the first iteration, the variable kmeans->initial_centers is NULL
+     and it is created once for reporting issues. */
+  kmeans->initial_centers = gsl_matrix_alloc (qc->ngroups, qc->n_vars);
+  gsl_matrix_memcpy (kmeans->initial_centers, kmeans->centers);
+}
 
-  for (; (c = casereader_read (cs)) != NULL; case_unref (c))
+
+/* Return the index of the group which is nearest to the case C */
+static void
+kmeans_get_nearest_group (const struct Kmeans *kmeans, struct ccase *c, const struct qc *qc, int *g_q, double *delta_q, int *g_p, double *delta_p)
+{
+  int result0 = -1;
+  int result1 = -1;
+  int i, j;
+  double mindist0 = INFINITY;
+  double mindist1 = INFINITY;
+  for (i = 0; i < qc->ngroups; i++)
     {
-      /* A case to hold the new index. */
-      struct ccase *index_case_new = case_create (kmeans->proto);
-      int bestindex = kmeans_get_nearest_group (kmeans, c, qc);
-      double weight = qc->wv ? case_data (c, qc->wv)->f : 1.0;
-      assert (bestindex < kmeans->num_elements_groups->size);
-      kmeans->num_elements_groups->data[bestindex] += weight;
-      if (kmeans->index_rdr)
+      double dist = 0;
+      for (j = 0; j < qc->n_vars; j++)
        {
-         /* A case from which the old index will be read. */
-         struct ccase *index_case_old = NULL;
+         const union value *val = case_data (c, qc->vars[j]);
+         if ( var_is_value_missing (qc->vars[j], val, qc->exclude))
+           continue;
 
-         /* Read the case from the index casereader. */
-         index_case_old = casereader_read (kmeans->index_rdr);
+         dist += pow2 (gsl_matrix_get (kmeans->centers, i, j) - val->f);
+       }
+      dist = sqrt (dist);
 
-         /* Set totaldiff, using the old_index. */
-         totaldiff += abs (case_data_idx (index_case_old, 0)->f - bestindex);
+      if (dist < mindist0)
+       {
+         mindist1 = mindist0;
+         result1 = result0;
 
-         /* We have no use for the old case anymore, so unref it. */
-         case_unref (index_case_old);
+         mindist0 = dist;
+         result0 = i;
        }
-      else
+      else if (dist < mindist1)
        {
-         /* If this is the first run, then assume index is zero. */
-         totaldiff += bestindex;
+         mindist1 = dist;
+         result1 = i;
        }
+    }
 
-      /* Set the value of the new inde.x */
-      case_data_rw_idx (index_case_new, 0)->f = bestindex;
+  if (delta_q)
+    *delta_q = mindist0;
+
+  if (g_q)
+    *g_q = result0;
 
-      /* and write the new index to the casewriter */
-      casewriter_write (index_wtr, index_case_new);
-    }
-  casereader_destroy (cs);
-  /* We have now read through the entire index_rdr, so it's of no use
-     anymore. */
-  casereader_destroy (kmeans->index_rdr);
 
-  /* Convert the writer into a reader, ready for the next iteration to read */
-  kmeans->index_rdr = casewriter_make_reader (index_wtr);
+  if (delta_p)
+    *delta_p = mindist1;
 
-  return (totaldiff);
+  if (g_p)
+    *g_p = result1;
 }
 
+
+
 static void
 kmeans_order_groups (struct Kmeans *kmeans, const struct qc *qc)
 {
@@ -346,50 +396,134 @@ kmeans_order_groups (struct Kmeans *kmeans, const struct qc *qc)
 static void
 kmeans_cluster (struct Kmeans *kmeans, struct casereader *reader, const struct qc *qc)
 {
-  int i;
-  bool redo;
-  int diffs;
-  bool show_warning1;
-  int redo_count = 0;
-
-  show_warning1 = true;
- cluster:
-  redo = false;
-  kmeans_randomize_centers (kmeans, reader, qc);
-  for (kmeans->lastiter = 0; kmeans->lastiter < qc->maxiter;
-       kmeans->lastiter++)
+  int j;
+
+  kmeans_initial_centers (kmeans, reader, qc);
+
+  gsl_matrix_memcpy (kmeans->updated_centers, kmeans->centers);
+
+
+  for (int xx = 0 ; xx < qc->maxiter ; ++xx)
     {
-      diffs = kmeans_calculate_indexes_and_check_convergence (kmeans, reader, qc);
-      kmeans_recalculate_centers (kmeans, reader, qc);
-      if (show_warning1 && qc->ngroups > kmeans->n)
+      gsl_vector_long_set_all (kmeans->num_elements_groups, 0.0);
+
+      kmeans->n = 0;
+      if (!qc->no_update)
        {
-         msg (MW, _("Number of clusters may not be larger than the number "
-                     "of cases."));
-         show_warning1 = false;
+         struct casereader *r = casereader_clone (reader);
+         struct ccase *c;
+         for (; (c = casereader_read (r)) != NULL; case_unref (c))
+           {
+             int group = -1;
+             int g;
+             bool missing = false;
+
+             for (j = 0; j < qc->n_vars; j++)
+               {
+                 const union value *val = case_data (c, qc->vars[j]);
+                 if ( var_is_value_missing (qc->vars[j], val, qc->exclude))
+                   missing = true;
+               }
+       
+             if (missing)
+               continue;
+
+             double mindist = INFINITY;
+             for (g = 0; g < qc->ngroups; ++g)
+               {
+                 double d = dist_from_case (kmeans, c, qc, g);
+
+                 if (d < mindist)
+                   {
+                     mindist = d;
+                     group = g;
+                   }
+               }
+
+             long *n = gsl_vector_long_ptr (kmeans->num_elements_groups, group);
+             *n += qc->wv ? case_data (c, qc->wv)->f : 1.0;
+             kmeans->n++;
+
+             for (j = 0; j < qc->n_vars; ++j)
+               {
+                 const union value *val = case_data (c, qc->vars[j]);
+                 if ( var_is_value_missing (qc->vars[j], val, qc->exclude))
+                   continue;
+                 double *x = gsl_matrix_ptr (kmeans->updated_centers, group, j);
+                 *x += val->f * (qc->wv ? case_data (c, qc->wv)->f : 1.0);
+               }
+           }    
+
+         casereader_destroy (r);
        }
-      if (diffs == 0)
-       break;
-    }
 
-  for (i = 0; i < qc->ngroups; i++)
-    {
-      if (kmeans->num_elements_groups->data[i] == 0)
+      int g;
+
+      /* Divide the cluster sums by the number of items in each cluster */
+      for (g = 0; g < qc->ngroups; ++g)
        {
-         kmeans->trials++;
-         if (kmeans->trials >= 3)
-           break;
-         redo = true;
-         break;
+         for (j = 0; j < qc->n_vars; ++j)
+           {
+             long n = gsl_vector_long_get (kmeans->num_elements_groups, g);
+             double *x = gsl_matrix_ptr (kmeans->updated_centers, g, j);
+             *x /= n + 1;  // Plus 1 for the initial centers
+           }
        }
-    }
+  
 
-  if (redo)
-    {
-      redo_count++;
-      assert (redo_count < 10);
-      goto cluster;
-    }
+      gsl_matrix_memcpy (kmeans->centers, kmeans->updated_centers);
+
+      {
+       kmeans->n = 0;
+       int i;
+       /* Step 3 */
+       gsl_vector_long_set_all (kmeans->num_elements_groups, 0.0);
+       gsl_matrix_set_all (kmeans->updated_centers, 0.0);
+       struct ccase *c;
+       struct casereader *cs = casereader_clone (reader);
+       for (; (c = casereader_read (cs)) != NULL; i++, case_unref (c))
+         {
+           int group = -1; 
+           kmeans_get_nearest_group (kmeans, c, qc, &group, NULL, NULL, NULL);
+
+           for (j = 0; j < qc->n_vars; ++j)
+             {
+               const union value *val = case_data (c, qc->vars[j]);
+               if ( var_is_value_missing (qc->vars[j], val, qc->exclude))
+                 continue;
+
+               double *x = gsl_matrix_ptr (kmeans->updated_centers, group, j);
+               *x += val->f;
+             }
 
+           long *n = gsl_vector_long_ptr (kmeans->num_elements_groups, group);
+           *n += qc->wv ? case_data (c, qc->wv)->f : 1.0;
+           kmeans->n++;
+
+
+         }
+       casereader_destroy (cs);
+
+
+       /* Divide the cluster sums by the number of items in each cluster */
+       for (g = 0; g < qc->ngroups; ++g)
+         {
+           for (j = 0; j < qc->n_vars; ++j)
+             {
+               long n = gsl_vector_long_get (kmeans->num_elements_groups, g);
+               double *x = gsl_matrix_ptr (kmeans->updated_centers, g, j);
+               *x /= n ;
+             }
+         }
+
+       double d = diff_matrix (kmeans->updated_centers, kmeans->centers);
+       if (d < kmeans->convergence_criteria)
+         break;
+      }
+
+      if (qc->no_update)
+       break;
+    }
 }
 
 /* Reports centers of clusters.
@@ -461,8 +595,8 @@ static void
 quick_cluster_show_membership (struct Kmeans *kmeans, const struct casereader *reader, const struct qc *qc)
 {
   struct tab_table *t;
-  int nc, nr;
-  int i, clust; 
+  int nc, nr, i;
+
   struct ccase *c;
   struct casereader *cs = casereader_clone (reader);
   nc = 2;
@@ -475,15 +609,19 @@ quick_cluster_show_membership (struct Kmeans *kmeans, const struct casereader *r
   tab_box (t, TAL_2, TAL_2, TAL_0, TAL_1, 0, 0, nc - 1, nr - 1);
   tab_hline (t, TAL_1, 0, nc - 1, 1);
 
+  gsl_permutation *ip = gsl_permutation_alloc (qc->ngroups);
+  gsl_permutation_inverse (ip, kmeans->group_order);
 
   for (i = 0; (c = casereader_read (cs)) != NULL; i++, case_unref (c))
     {
+      int clust = -1; 
       assert (i < kmeans->n);
-      clust = kmeans_get_nearest_group (kmeans, c, qc);
-      clust = kmeans->group_order->data[clust];
+      kmeans_get_nearest_group (kmeans, c, qc, &clust, NULL, NULL, NULL);
+      clust = ip->data[clust];
       tab_text_format (t, 0, i+1, TAB_CENTER, "%d", (i + 1));
       tab_text_format (t, 1, i+1, TAB_CENTER, "%d", (clust + 1));
     }
+  gsl_permutation_free (ip);
   assert (i == kmeans->n);
   tab_submit (t);
   casereader_destroy (cs);
@@ -526,6 +664,7 @@ static void
 quick_cluster_show_results (struct Kmeans *kmeans, const struct casereader *reader, const struct qc *qc)
 {
   kmeans_order_groups (kmeans, qc); /* what does this do? */
+  
   if( qc->print_initial_clusters )
     quick_cluster_show_centers (kmeans, true, qc);
   quick_cluster_show_centers (kmeans, false, qc);
@@ -542,11 +681,14 @@ cmd_quick_cluster (struct lexer *lexer, struct dataset *ds)
   bool ok;
   const struct dictionary *dict = dataset_dict (ds);
   qc.ngroups = 2;
-  qc.maxiter = 2;
+  qc.maxiter = 10;
+  qc.epsilon = DBL_EPSILON;
   qc.missing_type = MISS_LISTWISE;
   qc.exclude = MV_ANY;
   qc.print_cluster_membership = false; /* default = do not output case cluster membership */
   qc.print_initial_clusters = false;   /* default = do not print initial clusters */
+  qc.no_initial = false;               /* default = use well separated initial clusters */
+  qc.no_update = false;               /* default = iterate until convergence or max iterations */
 
   if (!parse_variables_const (lexer, dict, &qc.vars, &qc.n_vars,
                              PV_NO_DUPLICATE | PV_NUMERIC))
@@ -625,6 +767,21 @@ cmd_quick_cluster (struct lexer *lexer, struct dataset *ds)
                      lex_force_match (lexer, T_RPAREN);
                    }
                }
+             else if (lex_match_id (lexer, "CONVERGE"))
+               {
+                 if (lex_force_match (lexer, T_LPAREN))
+                   {
+                     lex_force_num (lexer);
+                     qc.epsilon = lex_number (lexer);
+                     if (qc.epsilon <= 0)
+                       {
+                         lex_error (lexer, _("The convergence criterium must be positive"));
+                         goto error;
+                       }
+                     lex_get (lexer);
+                     lex_force_match (lexer, T_RPAREN);
+                   }
+               }
              else if (lex_match_id (lexer, "MXITER"))
                {
                  if (lex_force_match (lexer, T_LPAREN))
@@ -640,6 +797,14 @@ cmd_quick_cluster (struct lexer *lexer, struct dataset *ds)
                      lex_force_match (lexer, T_RPAREN);
                    }
                }
+             else if (lex_match_id (lexer, "NOINITIAL"))
+               {
+                 qc.no_initial = true;
+               }
+             else if (lex_match_id (lexer, "NOUPDATE"))
+               {
+                 qc.no_update = true;
+               }
              else
                {
                  lex_error (lexer, NULL);
index 75dd52cb19f89a0a584ab3281bc9c24334ae7953..ee64902387a70eb3d3e771129873a42d117c7e4b 100644 (file)
@@ -46,13 +46,13 @@ AT_CLEANUP
 AT_SETUP([QUICK CLUSTER with large data set])
 AT_DATA([quick-cluster.sps], [dnl
 input program.
-loop #i = 1 to 500000.
+loop #i = 1 to 50000.
 compute x = 3.
 end case.
 end loop.
 end file.
 end input program.
-QUICK CLUSTER x /CRITERIA = CLUSTER(4) MXITER (100).
+QUICK CLUSTER x /CRITERIA = CLUSTER(4) NOINITIAL.
 ])
 AT_CHECK([pspp -o pspp.csv quick-cluster.sps])
 AT_CHECK([cat pspp.csv], [0], [dnl
@@ -61,14 +61,14 @@ Table: Final Cluster Centers
 ,,,,
 ,1,2,3,4
 ,,,,
-x,.00,.00,.00,3.00
+x,NaN,NaN,NaN,3.00
 
 Table: Number of Cases in each Cluster
 Cluster,1,0
 ,2,0
 ,3,0
-,4,500000
-Valid,,500000
+,4,50000
+Valid,,50000
 ])
 AT_CLEANUP
 
@@ -91,7 +91,7 @@ end input program.
 
 weight by w.
 
-QUICK CLUSTER x /CRITERIA = CLUSTER(4) MXITER (100).
+QUICK CLUSTER x /CRITERIA = CLUSTER(4) MXITER (10).
 ])
 
 AT_CHECK([pspp -o pspp-w.csv qc-weighted.sps])
@@ -106,7 +106,7 @@ end loop.
 end file.
 end input program.
 
-QUICK CLUSTER x /CRITERIA = CLUSTER(4) MXITER (100).
+QUICK CLUSTER x /CRITERIA = CLUSTER(4) MXITER (10).
 ])
 
 AT_CHECK([pspp -o pspp-unw.csv qc-unweighted.sps])
@@ -128,7 +128,7 @@ begin data.
 2
 end data.
 
-QUICK CLUSTER x /CRITERIA = CLUSTER(4) MXITER (100).
+QUICK CLUSTER x /CRITERIA = CLUSTER(4) MXITER (10).
 ])
 
 AT_CHECK([pspp -o pspp-m.csv quick-miss.sps])
@@ -144,7 +144,7 @@ begin data.
 2
 end data.
 
-QUICK CLUSTER x /CRITERIA = CLUSTER(4) MXITER (100).
+QUICK CLUSTER x /CRITERIA = CLUSTER(4) MXITER (10).
 ])
 
 AT_CHECK([pspp -o pspp-nm.csv quick-nmiss.sps])
@@ -155,6 +155,12 @@ AT_CLEANUP
 
 
 AT_SETUP([QUICK CLUSTER with pairwise missing])
+
+dnl This test runs two programs, which are identical except that one
+dnl has an extra case with one missing value. Becuase the syntax uses
+dnl NOINITIAL and NOUPDATE, the results should be identical except for
+dnl the final classification.
+
 AT_DATA([quick-s.sps], [dnl
 data list notable list /x * y *.
 begin data.
@@ -169,16 +175,15 @@ begin data.
 3.4 3
 3.5 2.5
 3.1 2.0
-3.9 2.5
-3.8 2.0
 end data.
 
 QUICK CLUSTER x y 
-       /CRITERIA = CLUSTER(3) MXITER (100)
+       /PRINT = INITIAL
+       /CRITERIA = CLUSTER(3) NOINITIAL NOUPDATE
        .
 ])
 
-AT_CHECK([pspp -O format=csv quick-s.sps | tail -5 > pspp-s.csv])
+AT_CHECK([pspp -O format=csv quick-s.sps  > pspp-s.csv])
 
 AT_DATA([quick-pw.sps], [dnl
 data list notable list /x * y *.
@@ -194,19 +199,28 @@ begin data.
 3.4 3
 3.5 2.5
 3.1 2.0
-3.9 .
-3.8 .
+.   2.3
 end data.
 
 QUICK CLUSTER x y 
-       /CRITERIA = CLUSTER(3) MXITER (100)
+       /CRITERIA = CLUSTER(3) NOINITIAL NOUPDATE
+       /PRINT = INITIAL
        /MISSING = PAIRWISE
        .
 ])
 
-AT_CHECK([pspp -O format=csv quick-pw.sps | tail -5 > pspp-pw.csv])
+AT_CHECK([pspp -O format=csv quick-pw.sps  > pspp-pw.csv])
+
+AT_CHECK([head -n -3  pspp-s.csv > top-s.csv])
+AT_CHECK([head -n -3  pspp-pw.csv > top-pw.csv])
+AT_CHECK([diff top-s.csv top-pw.csv])
+
 
-AT_CHECK([diff pspp-s.csv pspp-pw.csv], [0])
+AT_CHECK([grep Valid pspp-s.csv], [0], [Valid,,11
+])
+
+AT_CHECK([grep Valid pspp-pw.csv], [0], [Valid,,12
+])
 
 
 AT_CLEANUP
@@ -321,8 +335,8 @@ Table: Initial Cluster Centers
 ,,,
 ,1,2,3
 ,,,
-x,0,0,1
-y,0,1,0
+x,-11,-12,11
+y,-12,11,11
 
 Table: Final Cluster Centers
 ,Cluster,,