Move all command implementations into a single 'commands' directory.
[pspp] / src / language / commands / roc.c
diff --git a/src/language/commands/roc.c b/src/language/commands/roc.c
new file mode 100644 (file)
index 0000000..d73f5da
--- /dev/null
@@ -0,0 +1,1036 @@
+/* PSPP - a program for statistical analysis.
+   Copyright (C) 2009, 2010, 2011 Free Software Foundation, Inc.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>. */
+
+#include <config.h>
+
+#include "language/commands/roc.h"
+
+#include <gsl/gsl_cdf.h>
+
+#include "data/casegrouper.h"
+#include "data/casereader.h"
+#include "data/casewriter.h"
+#include "data/dataset.h"
+#include "data/dictionary.h"
+#include "data/format.h"
+#include "data/subcase.h"
+#include "language/command.h"
+#include "language/lexer/lexer.h"
+#include "language/lexer/value-parser.h"
+#include "language/lexer/variable-parser.h"
+#include "libpspp/misc.h"
+#include "math/sort.h"
+#include "output/charts/roc-chart.h"
+#include "output/pivot-table.h"
+
+#include "gettext.h"
+#define _(msgid) gettext (msgid)
+#define N_(msgid) msgid
+
+struct cmd_roc
+{
+  size_t n_vars;
+  const struct variable **vars;
+  const struct dictionary *dict;
+
+  const struct variable *state_var;
+  union value state_value;
+  size_t state_var_width;
+
+  /* Plot the roc curve */
+  bool curve;
+  /* Plot the reference line */
+  bool reference;
+
+  double ci;
+
+  bool print_coords;
+  bool print_se;
+  bool bi_neg_exp; /* True iff the bi-negative exponential critieria
+                     should be used */
+  enum mv_class exclude;
+
+  bool invert; /* True iff a smaller test result variable indicates
+                  a positive result */
+
+  double pos;
+  double neg;
+  double pos_weighted;
+  double neg_weighted;
+};
+
+static int run_roc (struct dataset *, struct cmd_roc *);
+static void do_roc (struct cmd_roc *, struct casereader *, struct dictionary *);
+
+
+int
+cmd_roc (struct lexer *lexer, struct dataset *ds)
+{
+  const struct dictionary *dict = dataset_dict (ds);
+
+  struct cmd_roc roc = {
+    .exclude = MV_ANY,
+    .curve = true,
+    .ci = 95,
+    .dict = dict,
+    .state_var_width = -1,
+  };
+
+  lex_match (lexer, T_SLASH);
+  if (!parse_variables_const (lexer, dict, &roc.vars, &roc.n_vars,
+                             PV_APPEND | PV_NO_DUPLICATE | PV_NUMERIC))
+    goto error;
+
+  if (!lex_force_match (lexer, T_BY))
+    goto error;
+
+  roc.state_var = parse_variable (lexer, dict);
+  if (!roc.state_var)
+    goto error;
+
+  if (!lex_force_match (lexer, T_LPAREN))
+    goto error;
+
+  roc.state_var_width = var_get_width (roc.state_var);
+  value_init (&roc.state_value, roc.state_var_width);
+  if (!parse_value (lexer, &roc.state_value, roc.state_var)
+      || !lex_force_match (lexer, T_RPAREN))
+    goto error;
+
+  while (lex_token (lexer) != T_ENDCMD)
+    {
+      lex_match (lexer, T_SLASH);
+      if (lex_match_id (lexer, "MISSING"))
+        {
+          lex_match (lexer, T_EQUALS);
+          while (lex_token (lexer) != T_ENDCMD && lex_token (lexer) != T_SLASH)
+            {
+             if (lex_match_id (lexer, "INCLUDE"))
+                roc.exclude = MV_SYSTEM;
+             else if (lex_match_id (lexer, "EXCLUDE"))
+                roc.exclude = MV_ANY;
+             else
+               {
+                  lex_error_expecting (lexer, "INCLUDE", "EXCLUDE");
+                 goto error;
+               }
+           }
+       }
+      else if (lex_match_id (lexer, "PLOT"))
+       {
+         lex_match (lexer, T_EQUALS);
+         if (lex_match_id (lexer, "CURVE"))
+           {
+             roc.curve = true;
+             if (lex_match (lexer, T_LPAREN))
+               {
+                 roc.reference = true;
+                 if (!lex_force_match_id (lexer, "REFERENCE")
+                      || !lex_force_match (lexer, T_RPAREN))
+                   goto error;
+               }
+           }
+         else if (lex_match_id (lexer, "NONE"))
+            roc.curve = false;
+         else
+           {
+             lex_error_expecting (lexer, "CURVE", "NONE");
+             goto error;
+           }
+       }
+      else if (lex_match_id (lexer, "PRINT"))
+       {
+         lex_match (lexer, T_EQUALS);
+          while (lex_token (lexer) != T_ENDCMD && lex_token (lexer) != T_SLASH)
+           {
+             if (lex_match_id (lexer, "SE"))
+                roc.print_se = true;
+             else if (lex_match_id (lexer, "COORDINATES"))
+                roc.print_coords = true;
+             else
+               {
+                 lex_error_expecting (lexer, "SE", "COORDINATES");
+                 goto error;
+               }
+           }
+       }
+      else if (lex_match_id (lexer, "CRITERIA"))
+       {
+         lex_match (lexer, T_EQUALS);
+          while (lex_token (lexer) != T_ENDCMD && lex_token (lexer) != T_SLASH)
+           {
+             if (lex_match_id (lexer, "CUTOFF"))
+               {
+                 if (!lex_force_match (lexer, T_LPAREN))
+                   goto error;
+                 if (lex_match_id (lexer, "INCLUDE"))
+                    roc.exclude = MV_SYSTEM;
+                 else if (lex_match_id (lexer, "EXCLUDE"))
+                    roc.exclude = MV_USER | MV_SYSTEM;
+                 else
+                   {
+                     lex_error_expecting (lexer, "INCLUDE", "EXCLUDE");
+                     goto error;
+                   }
+                 if (!lex_force_match (lexer, T_RPAREN))
+                   goto error;
+               }
+             else if (lex_match_id (lexer, "TESTPOS"))
+               {
+                 if (!lex_force_match (lexer, T_LPAREN))
+                   goto error;
+                 if (lex_match_id (lexer, "LARGE"))
+                    roc.invert = false;
+                 else if (lex_match_id (lexer, "SMALL"))
+                    roc.invert = true;
+                 else
+                   {
+                     lex_error_expecting (lexer, "LARGE", "SMALL");
+                     goto error;
+                   }
+                 if (!lex_force_match (lexer, T_RPAREN))
+                   goto error;
+               }
+             else if (lex_match_id (lexer, "CI"))
+               {
+                 if (!lex_force_match (lexer, T_LPAREN))
+                   goto error;
+                 if (!lex_force_num (lexer))
+                   goto error;
+                 roc.ci = lex_number (lexer);
+                 lex_get (lexer);
+                 if (!lex_force_match (lexer, T_RPAREN))
+                   goto error;
+               }
+             else if (lex_match_id (lexer, "DISTRIBUTION"))
+               {
+                 if (!lex_force_match (lexer, T_LPAREN))
+                   goto error;
+                 if (lex_match_id (lexer, "FREE"))
+                    roc.bi_neg_exp = false;
+                 else if (lex_match_id (lexer, "NEGEXPO"))
+                    roc.bi_neg_exp = true;
+                 else
+                   {
+                     lex_error_expecting (lexer, "FREE", "NEGEXPO");
+                     goto error;
+                   }
+                 if (!lex_force_match (lexer, T_RPAREN))
+                   goto error;
+               }
+             else
+               {
+                 lex_error_expecting (lexer, "CUTOFF", "TESTPOS", "CI",
+                                       "DISTRIBUTION");
+                 goto error;
+               }
+           }
+       }
+      else
+       {
+         lex_error_expecting (lexer, "MISSING", "PLOT", "PRINT", "CRITERIA");
+         goto error;
+       }
+    }
+
+  if (!run_roc (ds, &roc))
+    goto error;
+
+  if (roc.state_var)
+    value_destroy (&roc.state_value, roc.state_var_width);
+  free (roc.vars);
+  return CMD_SUCCESS;
+
+ error:
+  if (roc.state_var)
+    value_destroy (&roc.state_value, roc.state_var_width);
+  free (roc.vars);
+  return CMD_FAILURE;
+}
+
+static int
+run_roc (struct dataset *ds, struct cmd_roc *roc)
+{
+  struct dictionary *dict = dataset_dict (ds);
+  struct casereader *group;
+
+  struct casegrouper *grouper = casegrouper_create_splits (proc_open (ds), dict);
+  while (casegrouper_get_next_group (grouper, &group))
+    do_roc (roc, group, dataset_dict (ds));
+
+  bool ok = casegrouper_destroy (grouper);
+  ok = proc_commit (ds) && ok;
+  return ok;
+}
+
+#if 0
+static void
+dump_casereader (struct casereader *reader)
+{
+  struct ccase *c;
+  struct casereader *r = casereader_clone (reader);
+
+  for (; (c = casereader_read (r)); case_unref (c))
+    {
+      for (size_t i = 0; i < case_get_n_values (c); ++i)
+        printf ("%g ", case_num_idx (c, i));
+      printf ("\n");
+    }
+
+  casereader_destroy (r);
+}
+#endif
+
+
+/*
+   Return true iff the state variable indicates that C has positive actual state.
+
+   As a side effect, this function also accumulates the roc->{pos,neg} and
+   roc->{pos,neg}_weighted counts.
+ */
+static bool
+match_positives (const struct ccase *c, void *aux)
+{
+  struct cmd_roc *roc = aux;
+  const struct variable *wv = dict_get_weight (roc->dict);
+  const double weight = wv ? case_num (c, wv) : 1.0;
+
+  const bool positive =
+  (0 == value_compare_3way (case_data (c, roc->state_var), &roc->state_value,
+    var_get_width (roc->state_var)));
+
+  if (positive)
+    {
+      roc->pos++;
+      roc->pos_weighted += weight;
+    }
+  else
+    {
+      roc->neg++;
+      roc->neg_weighted += weight;
+    }
+
+  return positive;
+}
+
+
+#define VALUE  0
+#define N_EQ   1
+#define N_PRED 2
+
+/* Some intermediate state for calculating the cutpoints and the
+   standard error values */
+struct roc_state
+{
+  double auc;  /* Area under the curve */
+
+  double n1;  /* total weight of positives */
+  double n2;  /* total weight of negatives */
+
+  /* intermediates for standard error */
+  double q1hat;
+  double q2hat;
+
+  /* intermediates for cutpoints */
+  struct casewriter *cutpoint_wtr;
+  struct casereader *cutpoint_rdr;
+  double prev_result;
+  double min;
+  double max;
+};
+
+/*
+   Return a new casereader based upon CUTPOINT_RDR.
+   The number of "positive" cases are placed into
+   the position TRUE_INDEX, and the number of "negative" cases
+   into FALSE_INDEX.
+   POS_COND and RESULT determine the semantics of what is
+   "positive".
+   WEIGHT is the value of a single count.
+ */
+static struct casereader *
+accumulate_counts (struct casereader *input,
+                  double result, double weight,
+                  bool (*pos_cond) (double, double),
+                  int true_index, int false_index)
+{
+  const struct caseproto *proto = casereader_get_proto (input);
+  struct casewriter *w =
+    autopaging_writer_create (proto);
+  struct ccase *cpc;
+  double prev_cp = SYSMIS;
+
+  for (; (cpc = casereader_read (input)); case_unref (cpc))
+    {
+      struct ccase *new_case;
+      const double cp = case_num_idx (cpc, ROC_CUTPOINT);
+
+      assert (cp != SYSMIS);
+
+      /* We don't want duplicates here */
+      if (cp == prev_cp)
+       continue;
+
+      new_case = case_clone (cpc);
+
+      int index = pos_cond (result, cp) ? true_index : false_index;
+      *case_num_rw_idx (new_case, index) += weight;
+
+      prev_cp = cp;
+
+      casewriter_write (w, new_case);
+    }
+  casereader_destroy (input);
+
+  return casewriter_make_reader (w);
+}
+
+
+
+static void output_roc (struct roc_state *rs, const struct cmd_roc *roc);
+
+/*
+  This function does 3 things:
+
+  1. Counts the number of cases which are equal to every other case in READER,
+  and those cases for which the relationship between it and every other case
+  satifies PRED (normally either > or <).  VAR is variable defining a case's value
+  for this purpose.
+
+  2. Counts the number of true and false cases in reader, and populates
+  CUTPOINT_RDR accordingly.  TRUE_INDEX and FALSE_INDEX are the indices
+  which receive these values.  POS_COND is the condition defining true
+  and false.
+
+  3. CC is filled with the cumulative weight of all cases of READER.
+*/
+static struct casereader *
+process_group (const struct variable *var, struct casereader *reader,
+              bool (*pred) (double, double),
+              const struct dictionary *dict,
+              double *cc,
+              struct casereader **cutpoint_rdr,
+              bool (*pos_cond) (double, double),
+              int true_index,
+              int false_index)
+{
+  const struct variable *w = dict_get_weight (dict);
+
+  struct casereader *r1 =
+    casereader_create_distinct (sort_execute_1var (reader, var), var, w);
+
+  const int weight_idx  = w ? var_get_case_index (w) :
+    caseproto_get_n_widths (casereader_get_proto (r1)) - 1;
+
+  struct ccase *c1;
+
+  struct casereader *rclone = casereader_clone (r1);
+  struct casewriter *wtr;
+  struct caseproto *proto = caseproto_create ();
+
+  proto = caseproto_add_width (proto, 0);
+  proto = caseproto_add_width (proto, 0);
+  proto = caseproto_add_width (proto, 0);
+
+  wtr = autopaging_writer_create (proto);
+
+  *cc = 0;
+
+  for (; (c1 = casereader_read (r1)); case_unref (c1))
+    {
+      struct ccase *new_case = case_create (proto);
+      struct ccase *c2;
+      struct casereader *r2 = casereader_clone (rclone);
+
+      const double weight1 = case_num_idx (c1, weight_idx);
+      const double d1 = case_num (c1, var);
+      double n_eq = 0.0;
+      double n_pred = 0.0;
+
+      *cutpoint_rdr = accumulate_counts (*cutpoint_rdr, d1, weight1,
+                                        pos_cond,
+                                        true_index, false_index);
+
+      *cc += weight1;
+
+      for (; (c2 = casereader_read (r2)); case_unref (c2))
+       {
+         const double d2 = case_num (c2, var);
+         const double weight2 = case_num_idx (c2, weight_idx);
+
+         if (d1 == d2)
+           {
+             n_eq += weight2;
+             continue;
+           }
+         else  if (pred (d2, d1))
+           {
+             n_pred += weight2;
+           }
+       }
+
+      *case_num_rw_idx (new_case, VALUE) = d1;
+      *case_num_rw_idx (new_case, N_EQ) = n_eq;
+      *case_num_rw_idx (new_case, N_PRED) = n_pred;
+
+      casewriter_write (wtr, new_case);
+
+      casereader_destroy (r2);
+    }
+
+  casereader_destroy (r1);
+  casereader_destroy (rclone);
+
+  caseproto_unref (proto);
+
+  return casewriter_make_reader (wtr);
+}
+
+/* Some more indeces into case data */
+#define N_POS_EQ 1  /* number of positive cases with values equal to n */
+#define N_POS_GT 2  /* number of positive cases with values greater than n */
+#define N_NEG_EQ 3  /* number of negative cases with values equal to n */
+#define N_NEG_LT 4  /* number of negative cases with values less than n */
+
+static bool
+gt (double d1, double d2)
+{
+  return d1 > d2;
+}
+
+
+static bool
+ge (double d1, double d2)
+{
+  return d1 > d2;
+}
+
+static bool
+lt (double d1, double d2)
+{
+  return d1 < d2;
+}
+
+
+/*
+  Return a casereader with width 3,
+  populated with cases based upon READER.
+  The cases will have the values:
+  (N, number of cases equal to N, number of cases greater than N)
+  As a side effect, update RS->n1 with the number of positive cases.
+*/
+static struct casereader *
+process_positive_group (const struct variable *var, struct casereader *reader,
+                       const struct dictionary *dict,
+                       struct roc_state *rs)
+{
+  return process_group (var, reader, gt, dict, &rs->n1,
+                       &rs->cutpoint_rdr,
+                       ge,
+                       ROC_TP, ROC_FN);
+}
+
+/*
+  Return a casereader with width 3,
+  populated with cases based upon READER.
+  The cases will have the values:
+  (N, number of cases equal to N, number of cases less than N)
+  As a side effect, update RS->n2 with the number of negative cases.
+*/
+static struct casereader *
+process_negative_group (const struct variable *var, struct casereader *reader,
+                       const struct dictionary *dict,
+                       struct roc_state *rs)
+{
+  return process_group (var, reader, lt, dict, &rs->n2,
+                       &rs->cutpoint_rdr,
+                       lt,
+                       ROC_TN, ROC_FP);
+}
+
+
+
+
+static void
+append_cutpoint (struct casewriter *writer, double cutpoint)
+{
+  struct ccase *cc = case_create (casewriter_get_proto (writer));
+
+  *case_num_rw_idx (cc, ROC_CUTPOINT) = cutpoint;
+  *case_num_rw_idx (cc, ROC_TP) = 0;
+  *case_num_rw_idx (cc, ROC_FN) = 0;
+  *case_num_rw_idx (cc, ROC_TN) = 0;
+  *case_num_rw_idx (cc, ROC_FP) = 0;
+
+  casewriter_write (writer, cc);
+}
+
+/*
+   Create and initialise the rs[x].cutpoint_rdr casereaders.  That is, the
+   readers will be created with width 5, ready to take the values (cutpoint,
+   ROC_TP, ROC_FN, ROC_TN, ROC_FP), and the reader will be populated with its
+   final number of cases.  However on exit from this function, only
+   ROC_CUTPOINT entries will be set to their final value.  The other entries
+   will be initialised to zero.
+*/
+static struct roc_state *
+prepare_cutpoints (struct cmd_roc *roc, struct casereader *input)
+{
+  struct casereader *r = casereader_clone (input);
+  struct ccase *c;
+
+  struct subcase ordering;
+  subcase_init (&ordering, ROC_CUTPOINT, 0, SC_ASCEND);
+
+  struct caseproto *proto = caseproto_create ();
+  proto = caseproto_add_width (proto, 0); /* cutpoint */
+  proto = caseproto_add_width (proto, 0); /* ROC_TP */
+  proto = caseproto_add_width (proto, 0); /* ROC_FN */
+  proto = caseproto_add_width (proto, 0); /* ROC_TN */
+  proto = caseproto_add_width (proto, 0); /* ROC_FP */
+
+  struct roc_state *rs = xnmalloc (roc->n_vars, sizeof *rs);
+  for (size_t i = 0; i < roc->n_vars; ++i)
+    rs[i] = (struct roc_state) {
+      .cutpoint_wtr = sort_create_writer (&ordering, proto),
+      .prev_result = SYSMIS,
+      .max = -DBL_MAX,
+      .min = DBL_MAX,
+    };
+
+  caseproto_unref (proto);
+  subcase_uninit (&ordering);
+
+  for (; (c = casereader_read (r)) != NULL; case_unref (c))
+    for (size_t i = 0; i < roc->n_vars; ++i)
+      {
+        const union value *v = case_data (c, roc->vars[i]);
+        const double result = v->f;
+
+        if (mv_is_value_missing (var_get_missing_values (roc->vars[i]), v)
+            & roc->exclude)
+          continue;
+
+        minimize (&rs[i].min, result);
+        maximize (&rs[i].max, result);
+
+        if (rs[i].prev_result != SYSMIS && rs[i].prev_result != result)
+          {
+            const double mean = (result + rs[i].prev_result) / 2.0;
+            append_cutpoint (rs[i].cutpoint_wtr, mean);
+          }
+
+        rs[i].prev_result = result;
+      }
+  casereader_destroy (r);
+
+  /* Append the min and max cutpoints */
+  for (size_t i = 0; i < roc->n_vars; ++i)
+    {
+      append_cutpoint (rs[i].cutpoint_wtr, rs[i].min - 1);
+      append_cutpoint (rs[i].cutpoint_wtr, rs[i].max + 1);
+
+      rs[i].cutpoint_rdr = casewriter_make_reader (rs[i].cutpoint_wtr);
+    }
+
+  return rs;
+}
+
+static void
+do_roc (struct cmd_roc *roc, struct casereader *reader, struct dictionary *dict)
+{
+  struct casereader *input = casereader_create_filter_missing (
+    reader, roc->vars, roc->n_vars, roc->exclude, NULL, NULL);
+  input = casereader_create_filter_missing (
+    input, &roc->state_var, 1, roc->exclude, NULL, NULL);
+
+  struct roc_state *rs = prepare_cutpoints (roc, input);
+
+  /* Separate the positive actual state cases from the negative ones */
+  struct casewriter *neg_wtr
+    = autopaging_writer_create (casereader_get_proto (input));
+  struct casereader *positives = casereader_create_filter_func (
+    input, match_positives, NULL, roc, neg_wtr);
+
+  struct caseproto *n_proto = caseproto_create ();
+  for (size_t i = 0; i < 5; i++)
+    n_proto = caseproto_add_width (n_proto, 0);
+
+  struct subcase up_ordering;
+  struct subcase down_ordering;
+  subcase_init (&up_ordering, VALUE, 0, SC_ASCEND);
+  subcase_init (&down_ordering, VALUE, 0, SC_DESCEND);
+
+  struct casereader *negatives = NULL;
+  for (size_t i = 0; i < roc->n_vars; ++i)
+    {
+      const struct variable *var = roc->vars[i];
+
+      struct casereader *pos = casereader_clone (positives);
+
+      struct casereader *n_pos_reader =
+       process_positive_group (var, pos, dict, &rs[i]);
+
+      if (!negatives)
+        negatives = casewriter_make_reader (neg_wtr);
+
+      struct casereader *neg = casereader_clone (negatives);
+      struct casereader *n_neg_reader
+        = process_negative_group (var, neg, dict, &rs[i]);
+
+      /* Merge the n_pos and n_neg casereaders */
+      struct casewriter *w = sort_create_writer (&up_ordering, n_proto);
+      struct ccase *cpos;
+      for (; (cpos = casereader_read (n_pos_reader)); case_unref (cpos))
+       {
+         struct ccase *pos_case = case_create (n_proto);
+         const double jpos = case_num_idx (cpos, VALUE);
+
+         struct ccase *cneg;
+         while ((cneg = casereader_read (n_neg_reader)))
+           {
+             struct ccase *nc = case_create (n_proto);
+
+             const double jneg = case_num_idx (cneg, VALUE);
+
+             *case_num_rw_idx (nc, VALUE) = jneg;
+             *case_num_rw_idx (nc, N_POS_EQ) = 0;
+
+             *case_num_rw_idx (nc, N_POS_GT) = SYSMIS;
+
+             *case_data_rw_idx (nc, N_NEG_EQ) = *case_data_idx (cneg, N_EQ);
+             *case_data_rw_idx (nc, N_NEG_LT) = *case_data_idx (cneg, N_PRED);
+
+             casewriter_write (w, nc);
+
+             case_unref (cneg);
+             if (jneg > jpos)
+               break;
+           }
+
+         *case_num_rw_idx (pos_case, VALUE) = jpos;
+         *case_data_rw_idx (pos_case, N_POS_EQ) = *case_data_idx (cpos, N_EQ);
+         *case_data_rw_idx (pos_case, N_POS_GT) = *case_data_idx (cpos, N_PRED);
+         *case_num_rw_idx (pos_case, N_NEG_EQ) = 0;
+         *case_num_rw_idx (pos_case, N_NEG_LT) = SYSMIS;
+
+         casewriter_write (w, pos_case);
+       }
+
+      casereader_destroy (n_pos_reader);
+      casereader_destroy (n_neg_reader);
+
+      struct casereader *r = casewriter_make_reader (w);
+
+      /* Propagate the N_POS_GT values from the positive cases
+        to the negative ones */
+      double prev_pos_gt = rs[i].n1;
+      w = sort_create_writer (&down_ordering, n_proto);
+
+      struct ccase *c;
+      for (; (c = casereader_read (r)); case_unref (c))
+        {
+          double n_pos_gt = case_num_idx (c, N_POS_GT);
+          struct ccase *nc = case_clone (c);
+
+          if (n_pos_gt == SYSMIS)
+            {
+              n_pos_gt = prev_pos_gt;
+              *case_num_rw_idx (nc, N_POS_GT) = n_pos_gt;
+            }
+
+          casewriter_write (w, nc);
+          prev_pos_gt = n_pos_gt;
+        }
+      casereader_destroy (r);
+      r = casewriter_make_reader (w);
+
+      /* Propagate the N_NEG_LT values from the negative cases
+        to the positive ones */
+      double prev_neg_lt = rs[i].n2;
+      w = sort_create_writer (&up_ordering, n_proto);
+
+      for (; (c = casereader_read (r)); case_unref (c))
+        {
+          double n_neg_lt = case_num_idx (c, N_NEG_LT);
+          struct ccase *nc = case_clone (c);
+
+          if (n_neg_lt == SYSMIS)
+            {
+              n_neg_lt = prev_neg_lt;
+              *case_num_rw_idx (nc, N_NEG_LT) = n_neg_lt;
+            }
+
+          casewriter_write (w, nc);
+          prev_neg_lt = n_neg_lt;
+        }
+
+      casereader_destroy (r);
+      r = casewriter_make_reader (w);
+
+      struct ccase *prev_case = NULL;
+      for (; (c = casereader_read (r)); case_unref (c))
+        {
+          struct ccase *next_case = casereader_peek (r, 0);
+
+          const double j = case_num_idx (c, VALUE);
+          double n_pos_eq = case_num_idx (c, N_POS_EQ);
+          double n_pos_gt = case_num_idx (c, N_POS_GT);
+          double n_neg_eq = case_num_idx (c, N_NEG_EQ);
+          double n_neg_lt = case_num_idx (c, N_NEG_LT);
+
+          if (prev_case && j == case_num_idx (prev_case, VALUE))
+            {
+              if (0 ==  case_num_idx (c, N_POS_EQ))
+                {
+                  n_pos_eq = case_num_idx (prev_case, N_POS_EQ);
+                  n_pos_gt = case_num_idx (prev_case, N_POS_GT);
+                }
+
+              if (0 ==  case_num_idx (c, N_NEG_EQ))
+                {
+                  n_neg_eq = case_num_idx (prev_case, N_NEG_EQ);
+                  n_neg_lt = case_num_idx (prev_case, N_NEG_LT);
+                }
+            }
+
+          if (NULL == next_case || j != case_num_idx (next_case, VALUE))
+            {
+              rs[i].auc += n_pos_gt * n_neg_eq + (n_pos_eq * n_neg_eq) / 2.0;
+
+              rs[i].q1hat +=
+                n_neg_eq * (pow2 (n_pos_gt) + n_pos_gt * n_pos_eq + pow2 (n_pos_eq) / 3.0);
+              rs[i].q2hat +=
+                n_pos_eq * (pow2 (n_neg_lt) + n_neg_lt * n_neg_eq + pow2 (n_neg_eq) / 3.0);
+
+            }
+
+          case_unref (next_case);
+          case_unref (prev_case);
+          prev_case = case_clone (c);
+        }
+      casereader_destroy (r);
+      case_unref (prev_case);
+
+      rs[i].auc /= rs[i].n1 * rs[i].n2;
+      if (roc->invert)
+        rs[i].auc = 1 - rs[i].auc;
+
+      if (roc->bi_neg_exp)
+        {
+          rs[i].q1hat = rs[i].auc / (2 - rs[i].auc);
+          rs[i].q2hat = 2 * pow2 (rs[i].auc) / (1 + rs[i].auc);
+        }
+      else
+        {
+          rs[i].q1hat /= rs[i].n2 * pow2 (rs[i].n1);
+          rs[i].q2hat /= rs[i].n1 * pow2 (rs[i].n2);
+        }
+    }
+
+  casereader_destroy (positives);
+  casereader_destroy (negatives);
+
+  caseproto_unref (n_proto);
+  subcase_uninit (&up_ordering);
+  subcase_uninit (&down_ordering);
+
+  output_roc (rs, roc);
+
+  for (size_t i = 0; i < roc->n_vars; ++i)
+    casereader_destroy (rs[i].cutpoint_rdr);
+
+  free (rs);
+}
+
+static void
+show_auc (struct roc_state *rs, const struct cmd_roc *roc)
+{
+  struct pivot_table *table = pivot_table_create (N_("Area Under the Curve"));
+
+  struct pivot_dimension *statistics = pivot_dimension_create (
+    table, PIVOT_AXIS_COLUMN, N_("Statistics"),
+    N_("Area"), PIVOT_RC_OTHER);
+  if (roc->print_se)
+    {
+      pivot_category_create_leaves (
+        statistics->root,
+        N_("Std. Error"), PIVOT_RC_OTHER,
+        N_("Asymptotic Sig."), PIVOT_RC_SIGNIFICANCE);
+      struct pivot_category *interval = pivot_category_create_group__ (
+        statistics->root,
+        pivot_value_new_text_format (N_("Asymp. %g%% Confidence Interval"),
+                                     roc->ci));
+      pivot_category_create_leaves (interval,
+                                    N_("Lower Bound"), PIVOT_RC_OTHER,
+                                    N_("Upper Bound"), PIVOT_RC_OTHER);
+    }
+
+  struct pivot_dimension *variables = pivot_dimension_create (
+    table, PIVOT_AXIS_ROW, N_("Variable under test"));
+  variables->root->show_label = true;
+
+  for (size_t i = 0; i < roc->n_vars; ++i)
+    {
+      int var_idx = pivot_category_create_leaf (
+        variables->root, pivot_value_new_variable (roc->vars[i]));
+
+      pivot_table_put2 (table, 0, var_idx, pivot_value_new_number (rs[i].auc));
+
+      if (roc->print_se)
+       {
+         double se = (rs[i].auc * (1 - rs[i].auc)
+                       + (rs[i].n1 - 1) * (rs[i].q1hat - pow2 (rs[i].auc))
+                       + (rs[i].n2 - 1) * (rs[i].q2hat - pow2 (rs[i].auc)));
+         se /= rs[i].n1 * rs[i].n2;
+         se = sqrt (se);
+
+         double ci = 1 - roc->ci / 100.0;
+         double yy = gsl_cdf_gaussian_Qinv (ci, se);
+
+         double sd_0_5 = sqrt ((rs[i].n1 + rs[i].n2 + 1) /
+                                (12 * rs[i].n1 * rs[i].n2));
+          double sig = 2.0 * gsl_cdf_ugaussian_Q (fabs ((rs[i].auc - 0.5)
+                                                        / sd_0_5));
+          double entries[] = { se, sig, rs[i].auc - yy, rs[i].auc + yy };
+          for (size_t i = 0; i < sizeof entries / sizeof *entries; i++)
+            pivot_table_put2 (table, i + 1, var_idx,
+                              pivot_value_new_number (entries[i]));
+       }
+    }
+
+  pivot_table_submit (table);
+}
+
+static void
+show_summary (const struct cmd_roc *roc)
+{
+  struct pivot_table *table = pivot_table_create (N_("Case Summary"));
+
+  struct pivot_dimension *statistics = pivot_dimension_create (
+    table, PIVOT_AXIS_COLUMN, N_("Valid N (listwise)"),
+    N_("Unweighted"), PIVOT_RC_INTEGER,
+    N_("Weighted"), PIVOT_RC_OTHER);
+  statistics->root->show_label = true;
+
+  struct pivot_dimension *cases = pivot_dimension_create__ (
+    table, PIVOT_AXIS_ROW, pivot_value_new_variable (roc->state_var));
+  cases->root->show_label = true;
+  pivot_category_create_leaves (cases->root, N_("Positive"), N_("Negative"));
+
+  struct entry
+    {
+      int stat_idx;
+      int case_idx;
+      double x;
+    }
+  entries[] = {
+    { 0, 0, roc->pos },
+    { 0, 1, roc->neg },
+    { 1, 0, roc->pos_weighted },
+    { 1, 1, roc->neg_weighted },
+  };
+  for (size_t i = 0; i < sizeof entries / sizeof *entries; i++)
+    {
+      const struct entry *e = &entries[i];
+      pivot_table_put2 (table, e->stat_idx, e->case_idx,
+                        pivot_value_new_number (e->x));
+    }
+  pivot_table_submit (table);
+}
+
+static void
+show_coords (struct roc_state *rs, const struct cmd_roc *roc)
+{
+  struct pivot_table *table = pivot_table_create (
+    N_("Coordinates of the Curve"));
+
+  pivot_dimension_create (table, PIVOT_AXIS_COLUMN, N_("Statistics"),
+                          N_("Positive if greater than or equal to"),
+                          N_("Sensitivity"), N_("1 - Specificity"));
+
+  struct pivot_dimension *coordinates = pivot_dimension_create (
+    table, PIVOT_AXIS_ROW, N_("Coordinates"));
+  coordinates->hide_all_labels = true;
+
+  struct pivot_dimension *variables = pivot_dimension_create (
+    table, PIVOT_AXIS_ROW, N_("Test variable"));
+  variables->root->show_label = true;
+
+
+  int n_coords = 0;
+  for (size_t i = 0; i < roc->n_vars; ++i)
+    {
+      struct casereader *r = casereader_clone (rs[i].cutpoint_rdr);
+
+      int var_idx = pivot_category_create_leaf (
+        variables->root, pivot_value_new_variable (roc->vars[i]));
+
+      struct ccase *cc;
+      int coord_idx = 0;
+      for (; (cc = casereader_read (r)) != NULL; case_unref (cc))
+       {
+         const double se = case_num_idx (cc, ROC_TP) /
+           (case_num_idx (cc, ROC_TP) + case_num_idx (cc, ROC_FN));
+
+         const double sp = case_num_idx (cc, ROC_TN) /
+           (case_num_idx (cc, ROC_TN) + case_num_idx (cc, ROC_FP));
+
+          if (coord_idx >= n_coords)
+            {
+              assert (coord_idx == n_coords);
+              pivot_category_create_leaf (
+                coordinates->root, pivot_value_new_integer (++n_coords));
+            }
+
+          pivot_table_put3 (
+            table, 0, coord_idx, var_idx,
+            pivot_value_new_var_value (roc->vars[i],
+                                       case_data_idx (cc, ROC_CUTPOINT)));
+
+          pivot_table_put3 (table, 1, coord_idx, var_idx,
+                            pivot_value_new_number (se));
+          pivot_table_put3 (table, 2, coord_idx, var_idx,
+                            pivot_value_new_number (1 - sp));
+          coord_idx++;
+       }
+
+      casereader_destroy (r);
+    }
+
+  pivot_table_submit (table);
+}
+
+static void
+output_roc (struct roc_state *rs, const struct cmd_roc *roc)
+{
+  show_summary (roc);
+
+  if (roc->curve)
+    {
+      struct roc_chart *rc = roc_chart_create (roc->reference);
+      for (size_t i = 0; i < roc->n_vars; i++)
+        roc_chart_add_var (rc, var_get_name (roc->vars[i]),
+                           rs[i].cutpoint_rdr);
+      roc_chart_submit (rc);
+    }
+
+  show_auc (rs, roc);
+
+  if (roc->print_coords)
+    show_coords (rs, roc);
+}
+