Repository: jtablesaw/tablesaw
Pull request #621 (2019-08-21 21:25:48)

Clean up Analytic Functions
Thanks for contributing.

- [x] Tick to sign-off your agreement to the [Developer Certificate of Origin (DCO) 1.1](https://developercertificate.org)

## Description

* Added first pass of JavaDoc.
* Made as many of the classes/methods package private as possible.
* Added comments explaining implementation.
* Implemented executeInPlace method.
* cleaned up sliding vs append windows. Do not allow append windows to override removeLeftMost.
* Use consistent naming for referring to left and right side of a window.
* Implemented remaining basic analytic functions min, mean and count

Continues work on #582 

## Testing

Yes added and improved testing.


Patch:
diff --git a/aggregate/pom.xml b/aggregate/pom.xml
index 319185b3..002585e9 100644
--- a/aggregate/pom.xml
+++ b/aggregate/pom.xml
@@ -29,6 +29,7 @@
           <argLine>${argLine} -Xms256m -Xmx2048m</argLine>
           <forkCount>1</forkCount>
           <runOrder>random</runOrder>
+          <printSummary>false</printSummary>
         </configuration>
       </plugin>
       <plugin>
diff --git a/core/pom.xml b/core/pom.xml
index 815f0a6a..f349453a 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -96,6 +96,7 @@
                 <artifactId>maven-surefire-plugin</artifactId>
                 <configuration>
                     <argLine>-Duser.language=en-US -Duser.region=US</argLine>
+                    <printSummary>false</printSummary>
                 </configuration>
             </plugin>
         </plugins>
@@ -132,6 +133,10 @@
             <artifactId>classgraph</artifactId>
             <version>4.8.22</version>
         </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
         <dependency>
             <groupId>com.github.haifengl</groupId>
             <artifactId>smile-data</artifactId>
diff --git a/core/src/main/java/tech/tablesaw/analytic/AggregateFunction.java b/core/src/main/java/tech/tablesaw/analytic/AggregateFunction.java
index 69034a73..a918ddd7 100644
--- a/core/src/main/java/tech/tablesaw/analytic/AggregateFunction.java
+++ b/core/src/main/java/tech/tablesaw/analytic/AggregateFunction.java
@@ -1,16 +1,45 @@
 package tech.tablesaw.analytic;
 
-public interface AggregateFunction<T, R> {
+/**
+ * Analytic Aggregate function.
+ *
+ * <p>See {@link AggregateFunctions} for more background.
+ *
+ * <p>This class allows callers to independently slide the left side, and right side of the window.
+ * Implementers must keep track of the state of the window as it slides.
+ *
+ * @param <T> the type of the values in the input column (E.G. Integer).
+ * @param <R> the type of values in the return column (E.G. Double).
+ */
+interface AggregateFunction<T, R> {
 
+  /**
+   * Slides the left side (aka start) of the window window to the right (aka end) by removing the
+   * first element in the window. E.G. [(1, 2, 3, 4, 5)] -> [1, (2, 3, 4, 5)]
+   */
   void removeLeftMost();
 
+  /**
+   * Slides the right side (aka end) of the window to the right (aka end) by appending a new
+   * non-missing element. E.G. [(1, 2, 3), 4, 5] -> [(1, 2, 3, 4), 5)]
+   */
   void addRightMost(T value);
 
+  /**
+   * Slides the right side (aka end) of the window to the right (aka end) by appending a new missing
+   * element. E.G. [(1, 2, 3), NULL, 5] -> [(1, 2, 3, NULL), 5)]
+   */
   void addRightMostMissing();
 
+  /** Shortcut to appending multiple values to the right (aka end) of a window in one pass. */
   default void addAllRightMost(Iterable<T> newValues) {
     newValues.forEach(this::addRightMost);
   }
 
+  /**
+   * Get the aggregate value for the current window.
+   *
+   * @return the aggregate value for the current window.
+   */
   R getValue();
 }
diff --git a/core/src/main/java/tech/tablesaw/analytic/AggregateFunctions.java b/core/src/main/java/tech/tablesaw/analytic/AggregateFunctions.java
index 57dccf80..359a2133 100644
--- a/core/src/main/java/tech/tablesaw/analytic/AggregateFunctions.java
+++ b/core/src/main/java/tech/tablesaw/analytic/AggregateFunctions.java
@@ -1,43 +1,59 @@
 package tech.tablesaw.analytic;
 
 import java.util.ArrayDeque;
+import java.util.function.Function;
 import tech.tablesaw.analytic.WindowFrame.WindowGrowthType;
 import tech.tablesaw.api.ColumnType;
+import tech.tablesaw.columns.numbers.DoubleColumnType;
 
 /**
  * Analytic Aggregate functions.
  *
- * <p>These require different implementations from regular aggregate functions because they can be
- * called up to n times per table can can take O(n) per call for a total of O(n^2). A table with the
- * window definition ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW would be O(n^2).
+ * <p>Analytic Aggregate functions require different implementations compared to regular aggregate
+ * functions because they are called once per row and must return a value for every row in the
+ * table.
  *
- * <p>If at least one side of the window is unbounded the window is considered fixed. Most analytic
- * functions with fixed windows can be implemented in O(n). For example, calculating a sum over the
- * window UNBOUNDED PRECEDING AND CURRENT ROW can be done by simply summing the numbers. Windows
- * ending in UNBOUNDED FOLLOWING can be converted to a window that starts with UNBOUNDED PRECEDING
- * to take advantage of the more efficient algorithm.
+ * <p>Consider calculating the SUM over a table with a window definition of ROWS BETWEEN UNBOUNDED
+ * PRECEDING AND CURRENT ROW. If a regular aggregate function was used it would be called once for
+ * each row and since each window contains O(n) rows for a total running time of O(n^2). Clearly you
+ * can use a more efficient algorithm that keeps a running sum as rows are added to the window and
+ * runs in O(n). This class allows for those more efficient algorithms to be used.
  *
- * <p>Sliding windows are windows where both sides of the window are following(nrows), *
- * preceding(nrows) or current row. Analytic aggregate algorithms for sliding windows are generally
- * implemented with a Deque so that elements can be added or removed from the widow.
+ * <p>If at least one side of the window is unbounded the window is considered an append window.
+ * With a bit of tweaking windows UNBOUNDED FOLLOWING windows can be converted to UNBOUNDED
+ * PRECEDING windows so they are append only and can use a more efficient algorithm similar to the
+ * one explained above.
  *
- * <p>This class creates two implementations per analytic function. One for fixed windows and one
- * for sliding windows.
+ * <p>Sliding windows are windows where both sides of the window are following, preceding or current
+ * row. Analytic aggregate algorithms for sliding windows are generally implemented with a {@link
+ * java.util.Deque} so that elements can be added or removed from either side of the window as it
+ * slides.
+ *
+ * <p>This class creates two implementations per analytic aggregate function. One for append windows
+ * and one for sliding windows.
  */
 enum AggregateFunctions implements FunctionMetaData {
-  SUM(new Sum<>()),
-  MAX(new Max<>()),
-  MIN(new Min<>()),
-  MEAN(new Mean<>()),
-  COUNT(new Count<>());
+  SUM(new Sum<>(), ColumnType.DOUBLE, AggregateFunctions::isNumericColumn),
+  MAX(new Max<>(), ColumnType.DOUBLE, AggregateFunctions::isNumericColumn),
+  MIN(new Min<>(), ColumnType.DOUBLE, AggregateFunctions::isNumericColumn),
+  MEAN(new Mean<>(), ColumnType.DOUBLE, AggregateFunctions::isNumericColumn),
+  COUNT(new Count<>(), ColumnType.INTEGER, t -> true);
 
   private final WindowDependentAggregateFunction<?> implementation;
+  private final ColumnType outputColumnType;
+  private final Function<ColumnType, Boolean> isCompatibleColumnTestFunc;
 
-  AggregateFunctions(WindowDependentAggregateFunction<?> implementation) {
+  AggregateFunctions(
+      WindowDependentAggregateFunction<?> implementation,
+      ColumnType outputColumnType,
+      Function<ColumnType, Boolean> isCompatibleColumnTestFunc) {
     this.implementation = implementation;
+    this.outputColumnType = outputColumnType;
+    this.isCompatibleColumnTestFunc = isCompatibleColumnTestFunc;
   }
 
-  AggregateFunction<?, Double> getImplementation(WindowGrowthType growthType) {
+  /** Get the right implementation for that window definition. */
+  AggregateFunction<?, ? extends Number> getImplementation(WindowGrowthType growthType) {
     return this.implementation.functionFor(growthType);
   }
 
@@ -48,11 +64,15 @@ enum AggregateFunctions implements FunctionMetaData {
 
   @Override
   public ColumnType returnType() {
-    return ColumnType.DOUBLE;
+    return outputColumnType;
   }
 
   @Override
   public boolean isCompatibleColumn(ColumnType type) {
+    return isCompatibleColumnTestFunc.apply(type);
+  }
+
+  private static boolean isNumericColumn(ColumnType type) {
     return type.equals(ColumnType.DOUBLE)
         || type.equals(ColumnType.FLOAT)
         || type.equals(ColumnType.INTEGER)
@@ -60,20 +80,29 @@ enum AggregateFunctions implements FunctionMetaData {
         || type.equals(ColumnType.LONG);
   }
 
-  private abstract static class WindowDependentAggregateFunction<T extends Number> {
+  private abstract static class WindowDependentAggregateFunction<T> {
 
-    abstract AggregateFunction<T, Double> fixedFunction();
+    /** Sub classes of append windows should never call removeLeft. */
+    abstract static class AppendAggregateFunction<T, R> implements AggregateFunction<T, R> {
+      @Override
+      public final void removeLeftMost() {
+        throw new UnsupportedOperationException(
+            "Implementers of append aggregate functions should never call removeLeftMost");
+      }
+    }
 
-    abstract AggregateFunction<T, Double> slidingFunction();
+    abstract AppendAggregateFunction<T, ? extends Number> functionForAppendWindows();
 
-    AggregateFunction<T, Double> functionFor(WindowGrowthType growthType) {
+    abstract AggregateFunction<T, ? extends Number> functionForSlidingWindows();
+
+    AggregateFunction<T, ? extends Number> functionFor(WindowGrowthType growthType) {
       switch (growthType) {
         case FIXED:
-        case FIXED_START:
-        case FIXED_END:
-          return fixedFunction();
+        case FIXED_LEFT:
+        case FIXED_RIGHT:
+          return functionForAppendWindows();
         case SLIDING:
-          return slidingFunction();
+          return functionForSlidingWindows();
       }
       throw new RuntimeException("Unexpected growthType: " + growthType);
     }
@@ -81,24 +110,21 @@ enum AggregateFunctions implements FunctionMetaData {
 
   static class Sum<T extends Number> extends WindowDependentAggregateFunction<T> {
     @Override
-    AggregateFunction<T, Double> fixedFunction() {
-      return new AggregateFunction<T, Double>() {
-        private double sum = Double.NaN;
+    AppendAggregateFunction<T, Double> functionForAppendWindows() {
+      return new AppendAggregateFunction<T, Double>() {
+        private double sum = DoubleColumnType.missingValueIndicator();
 
         @Override
         public Double getValue() {
           return sum;
         }
 
-        @Override
-        public void removeLeftMost() {}
-
         @Override
         public void addRightMostMissing() {}
 
         @Override
         public void addRightMost(T newValue) {
-          if (Double.isNaN(sum)) {
+          if (DoubleColumnType.isMissingValue(sum)) {
             this.sum = 0.0;
           }
           this.sum += newValue.doubleValue();
@@ -107,7 +133,7 @@ enum AggregateFunctions implements FunctionMetaData {
     }
 
     @Override
-    AggregateFunction<T, Double> slidingFunction() {
+    AggregateFunction<T, Double> functionForSlidingWindows() {
       return new AggregateFunction<T, Double>() {
         private final ArrayDeque<Double> queue = new ArrayDeque<>();
         private Double sum = 0.0;
@@ -116,7 +142,7 @@ enum AggregateFunctions implements FunctionMetaData {
         @Override
         public void removeLeftMost() {
           Double removed = queue.remove();
-          if (Double.isNaN(removed)) {
+          if (DoubleColumnType.isMissingValue(removed)) {
             missingCount--;
           } else {
             this.sum -= removed;
@@ -132,14 +158,14 @@ enum AggregateFunctions implements FunctionMetaData {
 
         @Override
         public void addRightMostMissing() {
-          queue.add(Double.NaN);
+          queue.add(DoubleColumnType.missingValueIndicator());
           missingCount++;
         }
 
         @Override
         public Double getValue() {
           if (queue.size() == 0 || missingCount == queue.size()) {
-            return Double.NaN;
+            return DoubleColumnType.missingValueIndicator();
           }
           return sum;
         }
@@ -150,18 +176,13 @@ enum AggregateFunctions implements FunctionMetaData {
   static class Max<T extends Number> extends WindowDependentAggregateFunction<T> {
 
     @Override
-    AggregateFunction<T, Double> fixedFunction() {
-      return new AggregateFunction<T, Double>() {
-        private Double max = Double.NaN;
-
-        @Override
-        public void removeLeftMost() {
-          throw new IllegalArgumentException("Should never call remove on fixed function");
-        }
+    AppendAggregateFunction<T, Double> functionForAppendWindows() {
+      return new AppendAggregateFunction<T, Double>() {
+        private Double max = DoubleColumnType.missingValueIndicator();
 
         @Override
         public void addRightMost(T newValue) {
-          if (Double.isNaN(max)) {
+          if (DoubleColumnType.isMissingValue(max)) {
             max = newValue.doubleValue();
             return;
           }
@@ -179,7 +200,7 @@ enum AggregateFunctions implements FunctionMetaData {
     }
 
     @Override
-    AggregateFunction<T, Double> slidingFunction() {
+    AggregateFunction<T, Double> functionForSlidingWindows() {
       return new AggregateFunction<T, Double>() {
         private final ArrayDeque<Double> queue = new ArrayDeque<>();
 
@@ -195,7 +216,7 @@ enum AggregateFunctions implements FunctionMetaData {
 
         @Override
         public void addRightMostMissing() {
-          queue.add(Double.NaN);
+          queue.add(DoubleColumnType.missingValueIndicator());
         }
 
         @Override
@@ -203,51 +224,199 @@ enum AggregateFunctions implements FunctionMetaData {
           // This could be faster, but probably does not matter in practice because sliding windows
           // will be small.
           return queue.stream()
-              .filter(d -> !Double.isNaN(d))
+              .filter(d -> !DoubleColumnType.isMissingValue(d))
               .mapToDouble(Number::doubleValue)
               .max()
-              .orElse(Double.NaN);
+              .orElse(DoubleColumnType.missingValueIndicator());
         }
       };
     }
   }
 
   static class Min<T extends Number> extends WindowDependentAggregateFunction<T> {
-
     @Override
-    AggregateFunction<T, Double> fixedFunction() {
-      throw new UnsupportedOperationException("Analytic Function Min Is Not implemented");
+    AppendAggregateFunction<T, Double> functionForAppendWindows() {
+      return new AppendAggregateFunction<T, Double>() {
+        private Double min = DoubleColumnType.missingValueIndicator();
+
+        @Override
+        public void addRightMost(T newValue) {
+          if (DoubleColumnType.isMissingValue(min)) {
+            min = newValue.doubleValue();
+            return;
+          }
+          this.min = Math.min(min, newValue.doubleValue());
+        }
+
+        @Override
+        public void addRightMostMissing() {}
+
+        @Override
+        public Double getValue() {
+          return min;
+        }
+      };
     }
 
     @Override
-    AggregateFunction<T, Double> slidingFunction() {
-      throw new UnsupportedOperationException("Analytic Function Min Is Not implemented");
+    AggregateFunction<T, Double> functionForSlidingWindows() {
+      return new AggregateFunction<T, Double>() {
+        private final ArrayDeque<Double> queue = new ArrayDeque<>();
+
+        @Override
+        public void removeLeftMost() {
+          queue.remove();
+        }
+
+        @Override
+        public void addRightMost(T newValue) {
+          queue.add(newValue.doubleValue());
+        }
+
+        @Override
+        public void addRightMostMissing() {
+          queue.add(DoubleColumnType.missingValueIndicator());
+        }
+
+        @Override
+        public Double getValue() {
+          // This could be faster, but probably does not matter in practice because sliding windows
+          // will be small.
+          return queue.stream()
+              .filter(d -> !DoubleColumnType.isMissingValue(d))
+              .mapToDouble(Number::doubleValue)
+              .min()
+              .orElse(DoubleColumnType.missingValueIndicator());
+        }
+      };
     }
   }
 
   static class Mean<T extends Number> extends WindowDependentAggregateFunction<T> {
 
     @Override
-    AggregateFunction<T, Double> fixedFunction() {
-      throw new UnsupportedOperationException("Analytic Function Mean Is Not implemented");
+    AppendAggregateFunction<T, Double> functionForAppendWindows() {
+      return new AppendAggregateFunction<T, Double>() {
+        private double sum = DoubleColumnType.missingValueIndicator();
+        private double count = 0;
+
+        @Override
+        public Double getValue() {
+          if (count == 0) {
+            return DoubleColumnType.missingValueIndicator();
+          }
+          return sum / count;
+        }
+
+        @Override
+        public void addRightMostMissing() {}
+
+        @Override
+        public void addRightMost(T newValue) {
+          if (DoubleColumnType.isMissingValue(sum)) {
+            this.sum = 0.0;
+          }
+          this.sum += newValue.doubleValue();
+          count++;
+        }
+      };
     }
 
     @Override
-    AggregateFunction<T, Double> slidingFunction() {
-      throw new UnsupportedOperationException("Analytic Function Mean Is Not implemented");
+    AggregateFunction<T, Double> functionForSlidingWindows() {
+      return new AggregateFunction<T, Double>() {
+        private final ArrayDeque<Double> queue = new ArrayDeque<>();
+        private Double sum = 0.0;
+        private int missingCount = 0;
+
+        @Override
+        public void removeLeftMost() {
+          Double removed = queue.remove();
+          if (DoubleColumnType.isMissingValue(removed)) {
+            missingCount--;
+          } else {
+            this.sum -= removed;
+          }
+        }
+
+        @Override
+        public void addRightMost(T newValue) {
+          Double doubleValue = newValue.doubleValue();
+          this.sum += doubleValue;
+          queue.add(doubleValue);
+        }
+
+        @Override
+        public void addRightMostMissing() {
+          queue.add(DoubleColumnType.missingValueIndicator());
+          missingCount++;
+        }
+
+        @Override
+        public Double getValue() {
+          if (queue.size() - missingCount == 0) {
+            return DoubleColumnType.missingValueIndicator();
+          }
+          return sum / (queue.size() - missingCount);
+        }
+      };
     }
   }
 
-  static class Count<T extends Number> extends WindowDependentAggregateFunction<T> {
+  static class Count<T> extends WindowDependentAggregateFunction<T> {
 
     @Override
-    AggregateFunction<T, Double> fixedFunction() {
-      throw new UnsupportedOperationException("Analytic Function Count Is Not implemented");
+    AppendAggregateFunction<T, Integer> functionForAppendWindows() {
+      return new AppendAggregateFunction<T, Integer>() {
+        private int count = 0;
+
+        @Override
+        public Integer getValue() {
+          return count;
+        }
+
+        @Override
+        public void addRightMostMissing() {}
+
+        @Override
+        public void addRightMost(T newValue) {
+          count++;
+        }
+      };
     }
 
     @Override
-    AggregateFunction<T, Double> slidingFunction() {
-      throw new UnsupportedOperationException("Analytic Function Count Is Not implemented");
+    AggregateFunction<T, Integer> functionForSlidingWindows() {
+      return new AggregateFunction<T, Integer>() {
+        // Deque contains a boolean that when true indicates that the value in that position of the
+        // window is missing.
+        private final ArrayDeque<Boolean> queue = new ArrayDeque<>();
+        private int missingCount = 0;
+
+        @Override
+        public void removeLeftMost() {
+          Boolean removedMissingValue = queue.remove();
+          if (removedMissingValue) {
+            missingCount--;
+          }
+        }
+
+        @Override
+        public void addRightMost(T newValue) {
+          queue.add(false);
+        }
+
+        @Override
+        public void addRightMostMissing() {
+          queue.add(true);
+          missingCount++;
+        }
+
+        @Override
+        public Integer getValue() {
+          return queue.size() - missingCount;
+        }
+      };
     }
   }
 }
diff --git a/core/src/main/java/tech/tablesaw/analytic/AnalyticQuery.java b/core/src/main/java/tech/tablesaw/analytic/AnalyticQuery.java
index 9a6fbbcb..1713c4f8 100644
--- a/core/src/main/java/tech/tablesaw/analytic/AnalyticQuery.java
+++ b/core/src/main/java/tech/tablesaw/analytic/AnalyticQuery.java
@@ -2,31 +2,29 @@ package tech.tablesaw.analytic;
 
 import com.google.common.annotations.Beta;
 import com.google.common.base.Preconditions;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.LinkedHashSet;
-import java.util.List;
 import java.util.Optional;
-import java.util.function.Consumer;
 import tech.tablesaw.analytic.AnalyticQuerySteps.AddAggregateFunctions;
 import tech.tablesaw.analytic.AnalyticQuerySteps.AddAggregateFunctionsWithExecute;
 import tech.tablesaw.analytic.AnalyticQuerySteps.AddNumberingFunction;
 import tech.tablesaw.analytic.AnalyticQuerySteps.AddNumberingFunctionWithExecute;
-import tech.tablesaw.analytic.AnalyticQuerySteps.DefineWindow;
+import tech.tablesaw.analytic.AnalyticQuerySteps.DefineWindowFame;
 import tech.tablesaw.analytic.AnalyticQuerySteps.FullAnalyticQuerySteps;
+import tech.tablesaw.analytic.AnalyticQuerySteps.FullAnalyticQuerySteps.OrderByOptionalStep;
 import tech.tablesaw.analytic.AnalyticQuerySteps.NameStepAggregate;
 import tech.tablesaw.analytic.AnalyticQuerySteps.NameStepNumbering;
 import tech.tablesaw.analytic.AnalyticQuerySteps.NumberingQuerySteps;
-import tech.tablesaw.analytic.AnalyticQuerySteps.NumberingQuerySteps.OrderRequiredStep;
-import tech.tablesaw.analytic.AnalyticQuerySteps.NumberingQuerySteps.PartitionStep;
+import tech.tablesaw.analytic.AnalyticQuerySteps.NumberingQuerySteps.OrderByRequiredStep;
+import tech.tablesaw.analytic.AnalyticQuerySteps.NumberingQuerySteps.PartitionByStep;
 import tech.tablesaw.analytic.AnalyticQuerySteps.QuickQuerySteps;
-import tech.tablesaw.analytic.AnalyticQuerySteps.WindowEndOptionOne;
-import tech.tablesaw.analytic.AnalyticQuerySteps.WindowEndOptionTwo;
-import tech.tablesaw.analytic.AnalyticQuerySteps.WindowStart;
-import tech.tablesaw.api.Row;
+import tech.tablesaw.analytic.AnalyticQuerySteps.SetWindowEndOptionOne;
+import tech.tablesaw.analytic.AnalyticQuerySteps.SetWindowEndOptionTwo;
+import tech.tablesaw.analytic.AnalyticQuerySteps.SetWindowStart;
 import tech.tablesaw.api.Table;
 import tech.tablesaw.sorting.Sort;
 
+/** A class representing an analytic query similar to the Over or Window clause in SQL. */
 public final class AnalyticQuery {
 
   private final Table table;
@@ -46,9 +44,21 @@ public final class AnalyticQuery {
   }
 
   /**
-   * Entry point for Full Analytic Query.
+   * Entry point for the fluent analytic query builder. Order By and Partition By are optional.
    *
-   * @return Full Analytic Query Builder.
+   * <p>An AnalyticQuery performs a calculation across a set of table rows that are somehow related
+   * to the current row.
+   *
+   * <pre>
+   * Query includes steps for:
+   * FROM table_name
+   * [ PARTITION BY co1, col2... ]
+   * [ ORDER BY col1, col2...]
+   * window_frame_clause {@link DefineWindowFame}
+   * argument_list {@link AnalyticQuerySteps.AnalyticFunctions}
+   * </pre>
+   *
+   * @return a fluent analytic query builder.
    */
   @Beta
   public static FullAnalyticQuerySteps.FromStep query() {
@@ -56,9 +66,20 @@ public final class AnalyticQuery {
   }
 
   /**
-   * Entry point for Numbering Query.
+   * Entry point for the fluent Numbering Query Builder.
+   *
+   * <p>A numbering assigns integer values to each row based on their position within the specified
+   * partition. Numbering queries require Order By.
    *
-   * @return Numbering Analytic Query Builder.
+   * <pre>
+   * Query includes steps for:
+   * FROM table_name
+   * [ PARTITION BY col1, col2...]
+   * ORDER BY
+   * argument_list {@link AnalyticQuerySteps.NumberingFunctions}
+   * </pre>
+   *
+   * @return a fluent numbering query builder.
    */
   @Beta
   public static NumberingQuerySteps.FromStep numberingQuery() {
@@ -66,40 +87,59 @@ public final class AnalyticQuery {
   }
 
   /**
-   * Entry point for Quick Analytic Query.
+   * Entry point for the fluent Analytic Query Builder. Same as the {@link AnalyticQuery#query()}
+   * but skips the ORDER BY and PARTITION BY steps.
+   *
+   * <pre>
+   * Query includes steps for:
+   * FROM table_name
+   * window_frame_clause {@link DefineWindowFame}
+   * argument_list {@link AnalyticQuerySteps.AnalyticFunctions}
+   * </pre>
    *
-   * @return Quick Analytic Query Builder.
+   * @return a fluent analytic query builder that will skip the PartitionBy and OrderBy steps.
    */
   @Beta
   public static QuickQuerySteps.FromStep quickQuery() {
     return new QuickQueryBuilder();
   }
 
+  /**
+   * The Table behind the query.
+   *
+   * @return the underlying {@link Table} behind this query.
+   */
   public Table getTable() {
     return table;
   }
 
-  public ArgumentList getArgumentList() {
+  ArgumentList getArgumentList() {
     return argumentList;
   }
 
-  public LinkedHashSet<String> getPartitionColumns() {
+  LinkedHashSet<String> getPartitionColumns() {
     return windowSpecification.getPartitionColumns();
   }
 
-  public Optional<Sort> getSort() {
+  Optional<Sort> getSort() {
     return windowSpecification.getSort();
   }
 
-  public WindowSpecification getWindowSpecification() {
+  WindowSpecification getWindowSpecification() {
     return windowSpecification;
   }
 
-  public WindowFrame getWindowFrame() {
+  WindowFrame getWindowFrame() {
     return windowFrame;
   }
 
-  public String toSqlString() {
+  /**
+   * Creates a SQL like string for documentation purposes. The returned SQL is not meant be executed
+   * in SQL database.
+   *
+   * @return a SQL like string explaining the query.
+   */
+  public String toSqlLikeString() {
     StringBuilder sb = new StringBuilder();
     if (!argumentList.getNewColumnNames().isEmpty()) {
       sb.append("SELECT")
@@ -129,44 +169,59 @@ public final class AnalyticQuery {
 
   @Override
   public String toString() {
-    return toSqlString();
+    return toSqlLikeString();
   }
 
+  /**
+   * Executes the query adding all the calculated columns to a new table. The result columns will
+   * have the same order as the from table.
+   *
+   * @return a new table containing only the result columns.
+   */
   public Table execute() {
     return AnalyticQueryEngine.create(this).execute();
   }
 
+  /**
+   * Executes the query and adds all the calculated columns directly to the source table.
+   *
+   * @throws IllegalArgumentException if any of the calculated columns have the same name as one of
+   *     the columns in the FROM table.
+   */
+  public void executeInPlace() {
+    Table result = execute();
+    table.concat(result);
+  }
+
   static class NumberingQueryBuilder
       implements NumberingQuerySteps.FromStep,
-          NumberingQuerySteps.OrderRequiredStep,
-          NumberingQuerySteps.PartitionStep,
+          OrderByRequiredStep,
+          PartitionByStep,
           AnalyticQuerySteps.AddNumberingFunction,
           AddNumberingFunctionWithExecute,
           NameStepNumbering {
     private Table table;
-    private final WindowFrame.Builder frameBuilder = WindowFrame.builder();
     private final WindowSpecification.Builder windowSpecificationBuilder =
         WindowSpecification.builder();
     private final ArgumentList.Builder argumentsListBuilder = ArgumentList.builder();
-    private final List<Consumer<Iterable<Row>>> consumers = new ArrayList<>();
 
     @Override
-    public PartitionStep from(Table table) {
+    public PartitionByStep from(Table table) {
       this.table = table;
       return this;
     }
 
     @Override
-    public OrderRequiredStep partitionBy(String... columnNames) {
+    public OrderByRequiredStep partitionBy(String... columnNames) {
       this.windowSpecificationBuilder.setPartitionColumns(Arrays.asList(columnNames));
       return this;
     }
 
     @Override
-    public AddNumberingFunction orderBy(String columnOne, String... rest) {
-      String[] cols = new String[rest.length + 1];
-      cols[0] = columnOne;
-      System.arraycopy(rest, 0, cols, 1, rest.length);
+    public AddNumberingFunction orderBy(String columnName, String... columnNames) {
+      String[] cols = new String[columnNames.length + 1];
+      cols[0] = columnName;
+      System.arraycopy(columnNames, 0, cols, 1, columnNames.length);
       windowSpecificationBuilder.setSort(Sort.create(this.table, cols));
       return this;
     }
@@ -211,17 +266,17 @@ public final class AnalyticQuery {
 
     @Override
     public void executeInPlace() {
-      throw new UnsupportedOperationException();
+      this.build().executeInPlace();
     }
   }
 
   abstract static class AnalyticBuilder
-      implements FullAnalyticQuerySteps.OrderOptionalStep,
-          FullAnalyticQuerySteps.PartitionStep,
-          DefineWindow,
-          WindowStart,
-          WindowEndOptionOne,
-          WindowEndOptionTwo,
+      implements OrderByOptionalStep,
+          FullAnalyticQuerySteps.PartitionByStep,
+          DefineWindowFame,
+          SetWindowStart,
+          SetWindowEndOptionOne,
+          SetWindowEndOptionTwo,
           NameStepAggregate,
           AddAggregateFunctions,
           AddAggregateFunctionsWithExecute {
@@ -230,7 +285,6 @@ public final class AnalyticQuery {
     private final WindowSpecification.Builder windowSpecificationBuilder =
         WindowSpecification.builder();
     private final ArgumentList.Builder argumentsListBuilder = ArgumentList.builder();
-    private final List<Consumer<Iterable<Row>>> consumers = new ArrayList<>();
 
     @Override
     public NameStepAggregate sum(String columnName) {
@@ -263,61 +317,61 @@ public final class AnalyticQuery {
     }
 
     @Override
-    public FullAnalyticQuerySteps.OrderOptionalStep partitionBy(String... columns) {
+    public OrderByOptionalStep partitionBy(String... columns) {
       this.windowSpecificationBuilder.setPartitionColumns(Arrays.asList(columns));
       return this;
     }
 
     @Override
-    public DefineWindow orderBy(String... columnNames) {
+    public DefineWindowFame orderBy(String... columnNames) {
       windowSpecificationBuilder.setSort(Sort.create(this.table, columnNames));
       return this;
     }
 
     @Override
-    public WindowStart rowsBetween() {
+    public SetWindowStart rowsBetween() {
       return this;
     }
 
     @Override
-    public WindowEndOptionOne unboundedPreceding() {
+    public SetWindowEndOptionOne unboundedPreceding() {
       // default is unbounded preceding.
       return this;
     }
 
     @Override
-    public WindowEndOptionOne preceding(int nRows) {
-      this.frameBuilder.setStartPreceding(nRows);
+    public SetWindowEndOptionOne preceding(int nRows) {
+      this.frameBuilder.setLeftPreceding(nRows);
       return this;
     }
 
     @Override
-    public WindowEndOptionTwo currentRow() {
-      this.frameBuilder.setStartCurrentRow();
+    public SetWindowEndOptionTwo currentRow() {
+      this.frameBuilder.setLeftCurrentRow();
       return this;
     }
 
     @Override
-    public WindowEndOptionTwo following(int nRows) {
-      this.frameBuilder.setStartFollowing(nRows);
+    public SetWindowEndOptionTwo following(int nRows) {
+      this.frameBuilder.setLeftFollowing(nRows);
       return this;
     }
 
     @Override
     public AddAggregateFunctions andPreceding(int nRows) {
-      this.frameBuilder.setEndPreceding(nRows);
+      this.frameBuilder.setRightPreceding(nRows);
       return this;
     }
 
     @Override
     public AddAggregateFunctions andCurrentRow() {
-      this.frameBuilder.setEnndCurrentRow();
+      this.frameBuilder.setRightCurrentRow();
       return this;
     }
 
     @Override
     public AddAggregateFunctions andFollowing(int nRows) {
-      this.frameBuilder.setEndFollowing(nRows);
+      this.frameBuilder.setRightFollowing(nRows);
       return this;
     }
 
@@ -350,14 +404,14 @@ public final class AnalyticQuery {
 
     @Override
     public void executeInPlace() {
-      throw new UnsupportedOperationException();
+      this.build().executeInPlace();
     }
   }
 
   static class FullQueryBuilder extends AnalyticBuilder implements FullAnalyticQuerySteps.FromStep {
 
     @Override
-    public FullAnalyticQuerySteps.PartitionStep from(Table table) {
+    public FullAnalyticQuerySteps.PartitionByStep from(Table table) {
       super.table = table;
       return this;
     }
@@ -366,7 +420,7 @@ public final class AnalyticQuery {
   static class QuickQueryBuilder extends AnalyticBuilder implements QuickQuerySteps.FromStep {
 
     @Override
-    public DefineWindow from(Table table) {
+    public DefineWindowFame from(Table table) {
       super.table = table;
       return this;
     }
diff --git a/core/src/main/java/tech/tablesaw/analytic/AnalyticQueryEngine.java b/core/src/main/java/tech/tablesaw/analytic/AnalyticQueryEngine.java
index 1a8e1f01..113ef7c9 100644
--- a/core/src/main/java/tech/tablesaw/analytic/AnalyticQueryEngine.java
+++ b/core/src/main/java/tech/tablesaw/analytic/AnalyticQueryEngine.java
@@ -9,8 +9,13 @@ import tech.tablesaw.sorting.SortUtils;
 import tech.tablesaw.sorting.comparators.IntComparatorChain;
 import tech.tablesaw.table.TableSlice;
 
-/** Executes AnalyticQueries. */
-public final class AnalyticQueryEngine {
+/**
+ * Executes analytic queries.
+ *
+ * <p>Makes no changes to the underlying table. The order of the rows in "result" Table will match
+ * the order of the rows in underlying source table.
+ */
+final class AnalyticQueryEngine {
   private final AnalyticQuery query;
   private final Table destination;
   private final IntComparatorChain rowComparator;
@@ -29,6 +34,12 @@ public final class AnalyticQueryEngine {
     return new AnalyticQueryEngine(query);
   }
 
+  /**
+   * Execute the given analytic Query.
+   *
+   * @return a table with the result of the query. Rows in the result table match the order of rows
+   *     in the source table.
+   */
   public Table execute() {
     addColumns();
     partition().forEach(this::processSlice);
@@ -41,6 +52,10 @@ public final class AnalyticQueryEngine {
     processNumberingFunctions(slice);
   }
 
+  /**
+   * Execute all aggregate functions for the given slice setting values in the appropriate
+   * destination column.
+   */
   private void processAggregateFunctions(TableSlice slice) {
     for (String toColumn : query.getArgumentList().getAggregateFunctions().keySet()) {
       FunctionCall<AggregateFunctions> functionCall =
@@ -53,15 +68,19 @@ public final class AnalyticQueryEngine {
       Column<?> destinationColumn = destination.column(functionCall.getDestinationColumnName());
       new WindowSlider(
               query.getWindowFrame(), aggregateFunction, slice, sourceColumn, destinationColumn)
-          .process();
+          .execute();
     }
   }
 
+  /**
+   * Execute all numbering functions for the given slice setting values in the appropriate
+   * destination column.
+   */
   @SuppressWarnings({"unchecked", "rawtypes"})
   private void processNumberingFunctions(TableSlice slice) {
     for (String toColumn : query.getArgumentList().getNumberingFunctions().keySet()) {
       if (rowComparator == null) {
-        throw new IllegalArgumentException("Cannot use Numbering Function without" + " OrderBy");
+        throw new IllegalArgumentException("Cannot use Numbering Function without OrderBy");
       }
       FunctionCall<NumberingFunctions> functionCall =
           query.getArgumentList().getNumberingFunctions().get(toColumn);
@@ -71,24 +90,32 @@ public final class AnalyticQueryEngine {
           (Column<Integer>) destination.column(functionCall.getDestinationColumnName());
 
       int prevRowNumber = -1;
+      // Slice has already been ordered.
       for (Row row : slice) {
         if (row.getRowNumber() == 0) {
           function.addNextRow();
         } else {
+          // Consecutive rows are equal.
           if (rowComparator.compare(
                   slice.mappedRowNumber(prevRowNumber), slice.mappedRowNumber(row.getRowNumber()))
               == 0) {
             function.addEqualRow();
           } else {
+            // Consecutive rows are not equal.
             function.addNextRow();
           }
         }
         prevRowNumber = row.getRowNumber();
+        // Set the row number in the destination that corresponds to the row in the view.
         destinationColumn.set(slice.mappedRowNumber(row.getRowNumber()), function.getValue());
       }
     }
   }
 
+  /**
+   * Checks to make sure the given aggregate function is compatible with the type of the source
+   * column.
+   */
   private void validateColumn(FunctionMetaData function, Column<?> sourceColumn) {
     if (!function.isCompatibleColumn(sourceColumn.type())) {
       throw new IllegalArgumentException(
@@ -99,6 +126,10 @@ public final class AnalyticQueryEngine {
     }
   }
 
+  /**
+   * Creates empty columns that will be filled in when the analytic aggregate or numbering functions
+   * are executed.
+   */
   private void addColumns() {
     this.destination.addColumns(
         query
@@ -107,6 +138,9 @@ public final class AnalyticQueryEngine {
             .toArray(new Column<?>[0]));
   }
 
+  /**
+   * Partition the source table into a series of table slices. Does not modify the underlying table.
+   */
   private Iterable<TableSlice> partition() {
     if (query.getPartitionColumns().isEmpty()) {
       return ImmutableList.of(new TableSlice(query.getTable()));
@@ -114,6 +148,7 @@ public final class AnalyticQueryEngine {
     return query.getTable().splitOn(query.getPartitionColumns().toArray(new String[0]));
   }
 
+  /** Order the tableSlice in place. Does not modify the underlying table. */
   private void orderBy(TableSlice tableSlice) {
     if (query.getSort().isPresent()) {
       tableSlice.sortOn(query.getSort().get());
diff --git a/core/src/main/java/tech/tablesaw/analytic/AnalyticQuerySteps.java b/core/src/main/java/tech/tablesaw/analytic/AnalyticQuerySteps.java
index 472052c9..2c6d3549 100644
--- a/core/src/main/java/tech/tablesaw/analytic/AnalyticQuerySteps.java
+++ b/core/src/main/java/tech/tablesaw/analytic/AnalyticQuerySteps.java
@@ -5,42 +5,104 @@ import tech.tablesaw.api.Table;
 public interface AnalyticQuerySteps {
 
   interface FullAnalyticQuerySteps {
-    interface FromStep extends From<FullAnalyticQuerySteps.PartitionStep> {}
 
-    interface PartitionStep extends Partition<FullAnalyticQuerySteps.OrderOptionalStep> {}
-
-    interface OrderOptionalStep extends OrderOptional<DefineWindow> {}
+    interface FromStep {
+      /**
+       * Set the From/Source Table name to use in the query.
+       *
+       * @param table to query.
+       * @return the Partition By step in the fluent analytic query builder.
+       */
+      PartitionByStep from(Table table);
+    }
+
+    interface PartitionByStep {
+      /**
+       * Set the partition columns. The query will partition the table into slices using the given
+       * column names.
+       *
+       * @param columnNames the column names to partition the table by. If no column names are
+       *     supplied the query will skip this step.
+       * @return the Order By step in the fluent query builder.
+       */
+      OrderByOptionalStep partitionBy(String... columnNames);
+    }
+
+    interface OrderByOptionalStep {
+      /**
+       * Set the Order by Columns. The query will order each partition by the given column names,
+       * applied in order.
+       *
+       * <p>if column name starts with - then sort that column descending otherwise sort ascending
+       *
+       * @param columnNames column names to order by. If no column names are supplied the query will
+       *     skip this step.
+       * @return the define window frame step in the fluent analytic query builder.
+       */
+      DefineWindowFame orderBy(String... columnNames);
+    }
   }
 
   interface NumberingQuerySteps {
-    interface FromStep extends From<NumberingQuerySteps.PartitionStep> {}
-
-    interface PartitionStep extends Partition<NumberingQuerySteps.OrderRequiredStep> {}
 
-    interface OrderRequiredStep
-        extends AnalyticQuerySteps.OrderRequiredStep<AddNumberingFunction> {}
+    interface FromStep {
+      /**
+       * Set the From/Source Table name to use in the query.
+       *
+       * @param table to query.
+       * @return the Partition By Step in the fluent analytic query builder.
+       */
+      PartitionByStep from(Table table);
+    }
+
+    interface PartitionByStep {
+      /**
+       * Set the partition columns. The query will partition the table into slices using the given
+       * column names.
+       *
+       * @param columnNames the column names to partition the table by. If no column names are
+       *     supplied the query will skip this step.
+       * @return the Order By step in the fluent query builder.
+       */
+      OrderByRequiredStep partitionBy(String... columnNames);
+    }
+
+    interface OrderByRequiredStep {
+
+      /**
+       * Set the Order by Columns. The query will order each partition by the given column names,
+       * applied in order.
+       *
+       * <p>if column name starts with - then sort that column descending otherwise sort ascending
+       *
+       * @param columnName the first column name to sort by. Required.
+       * @param columnNames other column names to sort by. Optional.
+       * @return the add numbering functions step in the fluent query builder.
+       */
+      AddNumberingFunction orderBy(String columnName, String... columnNames);
+    }
   }
 
   interface QuickQuerySteps {
-    interface FromStep extends From<DefineWindow> {}
-  }
-
-  interface From<Next> {
-    Next from(Table table);
-  }
-
-  interface Partition<Next> {
-    Next partitionBy(String... columnNames);
-  }
-
-  interface OrderOptional<Next> {
-    Next orderBy(String... columnNames);
-  }
-
-  interface OrderRequiredStep<Next> {
-    Next orderBy(String columnOne, String... rest);
+    interface FromStep {
+      /**
+       * Set the From/Source Table name to use in the query.
+       *
+       * @param table to query.
+       * @return the define window frame step in the fluent analytic query builder.
+       */
+      DefineWindowFame from(Table table);
+    }
   }
 
+  /**
+   * An AnalyticFunction performs a calculation across a set of table rows that are somehow related
+   * to the current row. The related rows are defined by the window frame clause. See {@link
+   * DefineWindowFame} .
+   *
+   * <p>Tablesaw only supports a subset of the analytic functions you might find in a SQL database.
+   * If you would like to see a new numbering function implemented please file an issue on GitHub.
+   */
   interface AnalyticFunctions {
     NameStepAggregate sum(String columnName);
 
@@ -53,67 +115,199 @@ public interface AnalyticQuerySteps {
     NameStepAggregate count(String columnName);
   }
 
+  /**
+   * Numbering functions assign integer values to each row based on their position within the
+   * specified partition. Numbering functions require OrderBy.
+   *
+   * <p>Tablesaw only supports a subset of the numbering functions you might find in a SQL database.
+   * If you would like to see a new numbering function implemented please file an issue on GitHub.
+   */
   interface NumberingFunctions {
-    // Require Order.
+    /**
+     * Calculates the sequential row ordinal (1-based) of each row for each ordered partition.
+     *
+     * @return the name step in the fluent analytic query builder.
+     */
     NameStepNumbering rowNumber();
 
+    /**
+     * Calculates the ordinal (1-based) rank of each row within the ordered partition. All peer rows
+     * receive the same rank value. The next row or set of peer rows receives a rank value which
+     * increments by the number of peers with the previous rank value, instead of {@link
+     * NumberingFunctions#denseRank()} , which always increments by 1.
+     *
+     * @return the name step in the fluent analytic query builder.
+     */
     NameStepNumbering rank();
 
+    /**
+     * Calculates the ordinal (1-based) rank of each row within the window partition. All peer rows
+     * receive the same rank value, and the subsequent rank value is incremented by one.
+     *
+     * @return the name step in the fluent analytic query builder.
+     */
     NameStepNumbering denseRank();
   }
 
-  interface DefineWindow {
-    WindowStart rowsBetween();
+  /**
+   * First step in defining a window frame. The window frame clause here matches a window frame
+   * clause in SQL.
+   *
+   * <p>SQL Grammar for the window frame clause:
+   *
+   * <pre>
+   * { ROWS }
+   * {
+   *   { UNBOUNDED PRECEDING | numeric_expression PRECEDING | CURRENT ROW }
+   *   |
+   *   { BETWEEN window_frame_boundary_start AND window_frame_boundary_end }
+   * }
+   *
+   * window_frame_boundary_start:
+   * { UNBOUNDED PRECEDING | numeric_expression { PRECEDING | FOLLOWING } | CURRENT ROW }
+   *
+   * window_frame_boundary_end:
+   * { UNBOUNDED FOLLOWING | numeric_expression { PRECEDING | FOLLOWING } | CURRENT ROW }
+   * </pre>
+   */
+  interface DefineWindowFame {
+    /**
+     * The first step in defining a window frame.
+     *
+     * @return the set window start step in the fluent query builder.
+     */
+    SetWindowStart rowsBetween();
   }
 
-  interface WindowStart {
-    WindowEndOptionOne unboundedPreceding();
-
-    WindowEndOptionOne preceding(int nRows);
-
-    WindowEndOptionTwo currentRow();
-
-    WindowEndOptionTwo following(int nRows);
+  /** Set the window frame boundary start. */
+  interface SetWindowStart {
+    /**
+     * Set the bound to the first partition row.
+     *
+     * @return the set window frame end step in the fluent analytic query builder.
+     */
+    SetWindowEndOptionOne unboundedPreceding();
+
+    /**
+     * Set the bound to a number of rows preceding the current row.
+     *
+     * @param nRows number of rows before the current row to include in the window.
+     * @return the set window frame end step in the fluent analytic query builder.
+     */
+    SetWindowEndOptionOne preceding(int nRows);
+
+    /**
+     * Set the bound to the current row.
+     *
+     * @return the set window frame end step in the fluent analytic query builder.
+     */
+    SetWindowEndOptionTwo currentRow();
+
+    /**
+     * Set the bound to a number of rows following the current row.
+     *
+     * @param nRows number of rows after the current row to include in the window.
+     * @return the set window frame end step in the fluent analytic query builder.
+     */
+    SetWindowEndOptionTwo following(int nRows);
   }
 
-  interface WindowEndOptionOne {
+  /** Set the window frame boundary end. */
+  interface SetWindowEndOptionOne {
+    /**
+     * Set the bound to a number of rows preceding the current row.
+     *
+     * @param nRows number of rows before the current row to include in the window.
+     * @return the add aggregate functions step in the fluent analytic query builder.
+     */
     AddAggregateFunctions andPreceding(int nRows);
 
+    /**
+     * Set the bound to the current row.
+     *
+     * @return the add aggregate functions step in the fluent analytic query builder.
+     */
     AddAggregateFunctions andCurrentRow();
 
+    /**
+     * Set the bound to a number of rows following the current row.
+     *
+     * @param nRows number of rows after the current row to include in the window.
+     * @return the add aggregate functions step in the fluent analytic query builder.
+     */
     AddAggregateFunctions andFollowing(int nRows);
 
+    /**
+     * Set the bound to the last partition row.
+     *
+     * @return the add aggregate functions step in the fluent analytic query builder.
+     */
     AddAggregateFunctions andUnBoundedFollowing();
   }
 
-  interface WindowEndOptionTwo {
+  /** Set the window frame boundary end. */
+  interface SetWindowEndOptionTwo {
     AddAggregateFunctions andFollowing(int nRows);
 
     AddAggregateFunctions andUnBoundedFollowing();
   }
 
   interface NameStepAggregate {
+
+    /**
+     * Add alias/name to the calculated column.
+     *
+     * @param columnName the name to give the calculated column.
+     * @return the next step in the fluent query builder.
+     */
     AddAggregateFunctionsWithExecute as(String columnName);
   }
 
   interface NameStepNumbering {
+    /**
+     * Add alias/name to the calculated column.
+     *
+     * @param columnName the name to give the calculated column.
+     * @return the next step in the fluent query builder.
+     */
     AddNumberingFunctionWithExecute as(String columnName);
   }
 
   interface Execute {
+
+    /**
+     * Build the Query object without executing it.
+     *
+     * @return a query object that can be executed.
+     */
     AnalyticQuery build();
-    // Creates a new table.
+
+    /**
+     * Executes the query adding all the calculated columns to a new table. The result columns will
+     * have the same order as the from table.
+     *
+     * @return a new table containing only the result columns.
+     */
     Table execute();
 
-    // Adds Columns to existing table.
+    /**
+     * Executes the query and adds all the calculated columns directly to the source table.
+     *
+     * @throws IllegalArgumentException if any of the calculated columns have the same name as one
+     *     of the columns in the FROM table.
+     */
     void executeInPlace();
   }
 
+  /** Step to add the first Analytic Aggregate Function. Cannot execute the query yet. */
   interface AddAggregateFunctions extends AnalyticFunctions {}
 
+  /** Step to add the another Analytic Aggregate Function. Can execute the query. */
   interface AddAggregateFunctionsWithExecute extends AnalyticFunctions, Execute {}
 
+  /** Step to add the first Numbering Function. Cannot execute the query yet. */
   interface AddNumberingFunction extends NumberingFunctions {}
 
+  /** Step to add the another Numbering Function. Can execute the query. */
   interface AddNumberingFunctionWithExecute extends NumberingFunctions, Execute {}
 }
diff --git a/core/src/main/java/tech/tablesaw/analytic/ArgumentList.java b/core/src/main/java/tech/tablesaw/analytic/ArgumentList.java
index e1ca886e..0720d2df 100644
--- a/core/src/main/java/tech/tablesaw/analytic/ArgumentList.java
+++ b/core/src/main/java/tech/tablesaw/analytic/ArgumentList.java
@@ -13,7 +13,8 @@ import java.util.stream.Stream;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.columns.Column;
 
-public final class ArgumentList {
+/** This class holds data on what aggregate and numbering functions to execute in the query. */
+final class ArgumentList {
   // Throws if a column with the same name is registered
   private final Map<String, FunctionCall<AggregateFunctions>> aggregateFunctions;
   private final Map<String, FunctionCall<NumberingFunctions>> numberingFunctions;
@@ -154,16 +155,18 @@ public final class ArgumentList {
 
   static class Builder {
 
-    // maps new column to aggregate function.
+    // Maps the destination column name to aggregate function.
     private final Map<String, FunctionCall<AggregateFunctions>> aggregateFunctions =
         new HashMap<>();
+    // Maps the destination column name to aggregate function.
     private final Map<String, FunctionCall<NumberingFunctions>> numberingFunctions =
         new HashMap<>();
 
     // Throws if a column with the same name is registered twice.
     private final LinkedHashSet<String> newColumnNames = new LinkedHashSet<>();
 
-    // Temporarily store analytic function data until the user calls as to give the new coumn a name
+    // Temporarily store analytic function data until the user calls 'as' to give the new column a
+    // name
     // and save all the metadata.
     private String stagedFromColumn;
     private AggregateFunctions stagedAggregateFunction;
@@ -183,6 +186,7 @@ public final class ArgumentList {
     Builder stageFunction(NumberingFunctions function) {
       checkNothingStaged();
       Preconditions.checkNotNull(function);
+      // Numbering functions do not have a from column. Use a placeholder instead.
       this.stagedFromColumn = "NUMBERING_FUNCTION_PLACEHOLDER";
       this.stagedNumberingFunction = function;
       return this;
@@ -224,7 +228,7 @@ public final class ArgumentList {
 
     ArgumentList build() {
       if (this.stagedFromColumn != null) {
-        throw new IllegalArgumentException("Cannot build when a column is staged");
+        throw new IllegalStateException("Cannot build when a column is staged");
       }
       return new ArgumentList(aggregateFunctions, numberingFunctions, newColumnNames);
     }
diff --git a/core/src/main/java/tech/tablesaw/analytic/NumberingFunction.java b/core/src/main/java/tech/tablesaw/analytic/NumberingFunction.java
index b4e2116b..a6927b39 100644
--- a/core/src/main/java/tech/tablesaw/analytic/NumberingFunction.java
+++ b/core/src/main/java/tech/tablesaw/analytic/NumberingFunction.java
@@ -1,25 +1,35 @@
 package tech.tablesaw.analytic;
 
 /**
- * Analytic numbering functions give every value in the window a number based on some ordering
- * rules. // TODO make sure partition is ordered. They require the partition to be ordered.
+ * Analytic numbering function.
  *
- * <p>A analytic Aggregate functions accepts a window of rows and returns a single value which is
- * used to set a single row in the output column. In contrast an Analytic Mapping function accepts a
- * window and returns a window of rows (of the same size) which is used to a set a window of rows in
- * the output column.
+ * <p>See {@link NumberingFunctions} for more background.
  *
- * <p>Rank denseRank and rowNumber are examples of analytic mapping functions.
- *
- * <p>Analytic mapping functions are called exactly once per window.
- *
- * <p>TODO be explicit about the use case. I am not sure what else they are used for outside of *
- * numbering functions.
+ * <p>When iterating over a partition this allows callers indicate whether the next row in the
+ * partition is equal to or not equal to the previous row. Implementers can use this information to
+ * rank rows.
  */
 abstract class NumberingFunction {
+
+  /**
+   * Called when the next row in the partition is equal to the previous row.
+   *
+   * <p>E.G. [(1, 1, 1), 1] -> [(1, 1, 1, 1)]
+   */
   abstract void addEqualRow();
 
+  /**
+   * Called when the next row in the partition is not equal to the previous row.
+   *
+   * <p>E.G. [(1, 1, 1), 3] -> [(1, 1, 1, 4)]
+   */
   abstract void addNextRow();
 
+  /**
+   * Returns the numbering value (E.G. Rank) for the current row. Is be called once for every row in
+   * the partition while iterating through the partition.
+   *
+   * @return the numbering value for the current row.
+   */
   abstract int getValue();
 }
diff --git a/core/src/main/java/tech/tablesaw/analytic/NumberingFunctions.java b/core/src/main/java/tech/tablesaw/analytic/NumberingFunctions.java
index 872ab075..daa39177 100644
--- a/core/src/main/java/tech/tablesaw/analytic/NumberingFunctions.java
+++ b/core/src/main/java/tech/tablesaw/analytic/NumberingFunctions.java
@@ -3,6 +3,11 @@ package tech.tablesaw.analytic;
 import java.util.function.Supplier;
 import tech.tablesaw.api.ColumnType;
 
+/**
+ * Analytic numbering functions give every value in the window a number based on some ordering.
+ *
+ * <p>Rank denseRank and rowNumber are examples of analytic mapping functions.
+ */
 enum NumberingFunctions implements FunctionMetaData {
   ROW_NUMBER(Implementations::rowNumber),
   RANK(Implementations::rank),
@@ -34,14 +39,14 @@ enum NumberingFunctions implements FunctionMetaData {
 
   @Override
   public boolean isCompatibleColumn(ColumnType type) {
-    // TODO. Hard code this list to functions that implement comparable and equals.
+    // True for call tables that can be ordered.
     return true;
   }
 
   /** Implementations. */
   static class Implementations {
 
-    static final NumberingFunction rowNumber() {
+    static NumberingFunction rowNumber() {
 
       return new NumberingFunction() {
         private int count = 0;
@@ -63,7 +68,7 @@ enum NumberingFunctions implements FunctionMetaData {
       };
     }
 
-    static final NumberingFunction denseRank() {
+    static NumberingFunction denseRank() {
       return new NumberingFunction() {
         private int rank = 0;
 
@@ -82,7 +87,7 @@ enum NumberingFunctions implements FunctionMetaData {
       };
     }
 
-    static final NumberingFunction rank() {
+    static NumberingFunction rank() {
 
       return new NumberingFunction() {
         private int rank = 0;
diff --git a/core/src/main/java/tech/tablesaw/analytic/WindowFrame.java b/core/src/main/java/tech/tablesaw/analytic/WindowFrame.java
index ca72e2dd..cd921405 100644
--- a/core/src/main/java/tech/tablesaw/analytic/WindowFrame.java
+++ b/core/src/main/java/tech/tablesaw/analytic/WindowFrame.java
@@ -3,8 +3,18 @@ package tech.tablesaw.analytic;
 import com.google.common.base.Objects;
 import com.google.common.base.Preconditions;
 
-/** Simple Data class capturing the start and end of the window. */
-public final class WindowFrame {
+/**
+ * This class holds data on the WindowFrame clause of an analytic query.
+ *
+ * <p>Each Window is viewed as an array of values/rows and has a let bound and right bound.
+ *
+ * <p>For example in the window [1, 2, (3, 4, 5), 6, 7] The left most element in the window is 3 and
+ * the rightmost element is 5.
+ *
+ * <p>For more information on the window frame clause in SQL see {@link
+ * AnalyticQuerySteps.DefineWindowFame}
+ */
+final class WindowFrame {
 
   enum WindowBoundTypes {
     UNBOUNDED_PRECEDING(0),
@@ -20,28 +30,32 @@ public final class WindowFrame {
     }
   }
 
-  // TODO make this package private
   enum WindowGrowthType {
+    // UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
     FIXED,
-    FIXED_START,
-    FIXED_END,
+    // UNBOUNDED PRECEDING AND NOT UNBOUNDED FOLLOWING
+    FIXED_LEFT,
+    // NOT UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
+    FIXED_RIGHT,
+    // NOT UNBOUNDED PRECEDING AND NOT UNBOUNDED FOLLOWING
     SLIDING;
   }
 
-  private final WindowBoundTypes frameStart;
-  private final int frameStartShift;
-  private final WindowBoundTypes frameEnd;
-  private final int frameEndShift;
+  private final WindowBoundTypes leftBoundType;
+  private final int initialLeftBound;
+  private final WindowBoundTypes rightBoundType;
+  // Set to zero for UNBOUNDED FOLLOWING windows.
+  private final int initialRightBound;
 
   private WindowFrame(
-      WindowBoundTypes frameStart,
-      int frameStartShift,
-      WindowBoundTypes frameEnd,
-      int frameEndShift) {
-    this.frameStart = frameStart;
-    this.frameStartShift = frameStartShift;
-    this.frameEnd = frameEnd;
-    this.frameEndShift = frameEndShift;
+      WindowBoundTypes leftBoundType,
+      int initialLeftBound,
+      WindowBoundTypes rightBoundType,
+      int initialRightBound) {
+    this.leftBoundType = leftBoundType;
+    this.initialLeftBound = initialLeftBound;
+    this.rightBoundType = rightBoundType;
+    this.initialRightBound = initialRightBound;
     validateWindow();
   }
 
@@ -49,72 +63,79 @@ public final class WindowFrame {
     return new Builder();
   }
 
-  public WindowBoundTypes getFrameStart() {
-    return frameStart;
+  WindowBoundTypes getLeftBoundType() {
+    return leftBoundType;
   }
 
-  public int getFrameStartShift() {
-    return frameStartShift;
+  int getInitialLeftBound() {
+    return initialLeftBound;
   }
 
-  public WindowBoundTypes getFrameEnd() {
-    return frameEnd;
+  WindowBoundTypes getRightBoundType() {
+    return rightBoundType;
   }
 
-  public int getFrameEndShift() {
-    return frameEndShift;
+  int getInitialRightBound() {
+    return initialRightBound;
   }
 
-  /** Throw if invalid combination */
+  /**
+   * Throw if invalid window frame. For example ROWS BETWEEN FOLLOWING AND UNBOUNDED PRECEDING is
+   * invalid.
+   */
   private void validateWindow() {
     String errorMsg = "Invalid Window: " + this.toString() + '.';
     // If bounds are the same they both must either be preceding or following.
-    if (this.frameEnd == this.frameStart) {
+    if (this.rightBoundType == this.leftBoundType) {
       Preconditions.checkArgument(
-          frameStart == WindowBoundTypes.PRECEDING || frameStart == WindowBoundTypes.FOLLOWING,
+          leftBoundType == WindowBoundTypes.PRECEDING
+              || leftBoundType == WindowBoundTypes.FOLLOWING,
           errorMsg);
       // When the bounds are both preceding the lef bound should be greater than
-      if (this.frameStart == WindowBoundTypes.PRECEDING) {
+      if (this.leftBoundType == WindowBoundTypes.PRECEDING) {
         Preconditions.checkArgument(
-            frameStartShift < frameEndShift,
+            initialLeftBound < initialRightBound,
             errorMsg
                 + " The number preceding at start of the window '"
-                + Math.abs(frameStartShift)
+                + Math.abs(initialLeftBound)
                 + "' must be greater than the number preceding at the end of the window '"
-                + Math.abs(frameEndShift)
+                + Math.abs(initialRightBound)
                 + "'");
       } else {
         Preconditions.checkArgument(
-            frameEndShift > frameStartShift,
+            initialRightBound > initialLeftBound,
             errorMsg
                 + " The number following at start of the window '"
-                + frameStartShift
+                + initialLeftBound
                 + "' must be less than the number following at the end of the window '"
-                + frameEndShift
+                + initialRightBound
                 + "'");
       }
     }
     Preconditions.checkArgument(
-        frameEnd.order >= frameStart.order,
-        errorMsg + ". " + frameStart + " cannot come before " + frameEnd);
+        rightBoundType.order >= leftBoundType.order,
+        errorMsg + ". " + leftBoundType + " cannot come before " + rightBoundType);
   }
 
+  /**
+   * Calculate the window growth type. Knowing the growth type simplifies the executing the query.
+   */
   WindowGrowthType windowGrowthType() {
-    if (frameStart == WindowBoundTypes.UNBOUNDED_PRECEDING
-        && frameEnd == WindowBoundTypes.UNBOUNDED_FOLLOWING) {
+    if (leftBoundType == WindowBoundTypes.UNBOUNDED_PRECEDING
+        && rightBoundType == WindowBoundTypes.UNBOUNDED_FOLLOWING) {
       return WindowGrowthType.FIXED;
-    } else if ((frameStart == WindowBoundTypes.PRECEDING
-            || frameStart == WindowBoundTypes.FOLLOWING
-            || frameStart == WindowBoundTypes.CURRENT_ROW)
-        && (frameEnd == WindowBoundTypes.PRECEDING
-            || frameEnd == WindowBoundTypes.FOLLOWING
-            || frameEnd == WindowBoundTypes.CURRENT_ROW)) {
+    } else if ((leftBoundType == WindowBoundTypes.PRECEDING
+            || leftBoundType == WindowBoundTypes.FOLLOWING
+            || leftBoundType == WindowBoundTypes.CURRENT_ROW)
+        && (rightBoundType == WindowBoundTypes.PRECEDING
+            || rightBoundType == WindowBoundTypes.FOLLOWING
+            || rightBoundType == WindowBoundTypes.CURRENT_ROW)) {
       return WindowGrowthType.SLIDING;
     }
-    if (frameStart == WindowBoundTypes.UNBOUNDED_PRECEDING) {
-      return WindowGrowthType.FIXED_START;
+    if (leftBoundType == WindowBoundTypes.UNBOUNDED_PRECEDING) {
+      return WindowGrowthType.FIXED_LEFT;
     }
-    return WindowGrowthType.FIXED_END;
+    return WindowGrowthType.FIXED_RIGHT;
   }
 
   @Override
@@ -122,26 +143,28 @@ public final class WindowFrame {
     if (this == o) return true;
     if (o == null || getClass() != o.getClass()) return false;
     WindowFrame that = (WindowFrame) o;
-    return frameStartShift == that.frameStartShift
-        && frameEndShift == that.frameEndShift
-        && frameStart == that.frameStart
-        && frameEnd == that.frameEnd;
+    return initialLeftBound == that.initialLeftBound
+        && initialRightBound == that.initialRightBound
+        && leftBoundType == that.leftBoundType
+        && rightBoundType == that.rightBoundType;
   }
 
   @Override
   public int hashCode() {
-    return Objects.hashCode(frameStart, frameStartShift, frameEnd, frameEndShift);
+    return Objects.hashCode(leftBoundType, initialLeftBound, rightBoundType, initialRightBound);
   }
 
-  public String toSqlString() {
-    String formatedStart = frameStart.toString();
-    if (frameStart == WindowBoundTypes.PRECEDING || frameStart == WindowBoundTypes.FOLLOWING) {
-      formatedStart = Math.abs(frameStartShift) + " " + formatedStart;
+  String toSqlString() {
+    String formatedStart = leftBoundType.toString();
+    if (leftBoundType == WindowBoundTypes.PRECEDING
+        || leftBoundType == WindowBoundTypes.FOLLOWING) {
+      formatedStart = Math.abs(initialLeftBound) + " " + formatedStart;
     }
 
-    String formattedRightBound = frameEnd.toString();
-    if (frameEnd == WindowBoundTypes.PRECEDING || frameEnd == WindowBoundTypes.FOLLOWING) {
-      formattedRightBound = Math.abs(frameEndShift) + " " + formattedRightBound;
+    String formattedRightBound = rightBoundType.toString();
+    if (rightBoundType == WindowBoundTypes.PRECEDING
+        || rightBoundType == WindowBoundTypes.FOLLOWING) {
+      formattedRightBound = Math.abs(initialRightBound) + " " + formattedRightBound;
     }
 
     return "ROWS BETWEEN " + formatedStart + " AND " + formattedRightBound;
@@ -152,55 +175,62 @@ public final class WindowFrame {
     return toSqlString();
   }
 
-  static class Builder {
+  /**
+   * Builder for a {@link WindowFrame}. Defaults to UNBOUNDED PRECEDING UNBOUNDED FOLLOWING.
+   *
+   * <p>The shift is the number of rows to extend the window left or right from the current row.
+   * Negative includes rows to the left, positive includes rows to the right.
+   */
+  static final class Builder {
 
-    private WindowBoundTypes frameStart = WindowBoundTypes.UNBOUNDED_PRECEDING;
-    private int frameStartShift = 0;
-    private WindowBoundTypes frameEnd = WindowBoundTypes.UNBOUNDED_FOLLOWING;
-    private int frameEndShift = 0;
+    private WindowBoundTypes leftBoundType = WindowBoundTypes.UNBOUNDED_PRECEDING;
+    private int initialLeftBound = 0;
+    private WindowBoundTypes rightBoundType = WindowBoundTypes.UNBOUNDED_FOLLOWING;
+    // Set to zero for UNBOUNDED FOLLOWING windows
+    private int initialRightBound = 0;
 
     private Builder() {}
 
-    Builder setStartPreceding(int nRows) {
+    Builder setLeftPreceding(int nRows) {
       Preconditions.checkArgument(nRows > 0);
-      this.frameStart = WindowBoundTypes.PRECEDING;
-      this.frameStartShift = nRows * -1;
+      this.leftBoundType = WindowBoundTypes.PRECEDING;
+      this.initialLeftBound = nRows * -1;
       return this;
     }
 
-    Builder setStartCurrentRow() {
-      this.frameStart = WindowBoundTypes.CURRENT_ROW;
+    Builder setLeftCurrentRow() {
+      this.leftBoundType = WindowBoundTypes.CURRENT_ROW;
       return this;
     }
 
-    Builder setStartFollowing(int nRows) {
+    Builder setLeftFollowing(int nRows) {
       Preconditions.checkArgument(nRows > 0);
-      this.frameStart = WindowBoundTypes.FOLLOWING;
-      this.frameStartShift = nRows;
+      this.leftBoundType = WindowBoundTypes.FOLLOWING;
+      this.initialLeftBound = nRows;
       return this;
     }
 
-    Builder setEndPreceding(int nRows) {
+    Builder setRightPreceding(int nRows) {
       Preconditions.checkArgument(nRows > 0);
-      this.frameEnd = WindowBoundTypes.PRECEDING;
-      this.frameEndShift = nRows * -1;
+      this.rightBoundType = WindowBoundTypes.PRECEDING;
+      this.initialRightBound = nRows * -1;
       return this;
     }
 
-    Builder setEnndCurrentRow() {
-      this.frameEnd = WindowBoundTypes.CURRENT_ROW;
+    Builder setRightCurrentRow() {
+      this.rightBoundType = WindowBoundTypes.CURRENT_ROW;
       return this;
     }
 
-    Builder setEndFollowing(int nRows) {
+    Builder setRightFollowing(int nRows) {
       Preconditions.checkArgument(nRows > 0);
-      this.frameEnd = WindowBoundTypes.FOLLOWING;
-      this.frameEndShift = nRows;
+      this.rightBoundType = WindowBoundTypes.FOLLOWING;
+      this.initialRightBound = nRows;
       return this;
     }
 
     public WindowFrame build() {
-      return new WindowFrame(frameStart, frameStartShift, frameEnd, frameEndShift);
+      return new WindowFrame(leftBoundType, initialLeftBound, rightBoundType, initialRightBound);
     }
   }
 }
diff --git a/core/src/main/java/tech/tablesaw/analytic/WindowSlider.java b/core/src/main/java/tech/tablesaw/analytic/WindowSlider.java
index 11b5430f..f096487d 100644
--- a/core/src/main/java/tech/tablesaw/analytic/WindowSlider.java
+++ b/core/src/main/java/tech/tablesaw/analytic/WindowSlider.java
@@ -6,20 +6,21 @@ import tech.tablesaw.columns.Column;
 import tech.tablesaw.table.TableSlice;
 
 /**
- * Execute the aggregate function over the correct windows.
+ * Execute the aggregate function once for every row in the slice.
  *
  * <p>Any window with a Fixed end (UNBOUNDED FOLLOWING) is converted ("mirrored") into the
- * equivalent UNBOUNDED PRECEDING widow so that a faster algorithm can be used.
+ * equivalent UNBOUNDED PRECEDING widow so that it is an append window and a faster algorithm can be
+ * used.
  */
 class WindowSlider {
   private final boolean mirrored;
   private final WindowGrowthType windowGrowthType;
-  private final int frameStartShift;
-  private final int frameEndShift;
+  private final int initialLeftBound;
+  private final int initialRightBound;
 
   @SuppressWarnings({"unchecked", "rawtypes"})
   private final AggregateFunction function;
-  // TODO change to table slice
+
   private final TableSlice slice;
   private final Column<?> sourceColumn;
 
@@ -37,41 +38,45 @@ class WindowSlider {
     this.sourceColumn = sourceColumn;
     this.function = func.getImplementation(windowFrame.windowGrowthType());
 
-    // Can convert UNBOUNDED FOLLOWING to an equivalent UNBOUNDED PRECEDING window by mirroring
-    // everything.
-    if (windowFrame.windowGrowthType() == WindowGrowthType.FIXED_END) {
-      this.windowGrowthType = WindowGrowthType.FIXED_START;
+    // Convert UNBOUNDED FOLLOWING to an equivalent UNBOUNDED PRECEDING window.
+    if (windowFrame.windowGrowthType() == WindowGrowthType.FIXED_RIGHT) {
+      this.windowGrowthType = WindowGrowthType.FIXED_LEFT;
       this.mirrored = true;
-      this.frameStartShift = windowFrame.getFrameEndShift() * -1;
-      this.frameEndShift = windowFrame.getFrameStartShift() * -1;
+      this.initialLeftBound = windowFrame.getInitialRightBound() * -1;
+      this.initialRightBound = windowFrame.getInitialLeftBound() * -1;
     } else {
       this.mirrored = false;
-      this.frameStartShift = windowFrame.getFrameStartShift();
-      this.frameEndShift = windowFrame.getFrameEndShift();
+      this.initialLeftBound = windowFrame.getInitialLeftBound();
+      this.initialRightBound = windowFrame.getInitialRightBound();
       this.windowGrowthType = windowFrame.windowGrowthType();
     }
   }
 
-  /**
-   * Slide over the partition setting a value for all the relevant windows in the destination
-   * column.
-   */
+  /** Slide the window over the slice calculating an aggregate value for every row in the slice. */
   @SuppressWarnings({"unchecked", "rawtypes"})
-  void process() {
-    initWindow(sourceColumn);
-    int leftBound = getInitialStartIndex() - 1;
-    int rightBound = getInitialEndIndex();
+  void execute() {
+    initWindow();
+    // Initial window bounds can be outside the current slice. This allows for windows like 20
+    // PRECEDING 10 PRECEDING
+    // to slide into the slice. Rows outside the slide will be ignored.
+    int leftBound = getInitialLeftBound() - 1;
+    int rightBound = getInitialRightBound();
     for (int i = 0; i < slice.rowCount(); i++) {
       this.set(i, function.getValue());
 
+      // Slide the left side of the window if applicable for the window definition.
       int newLeftBound = slideLeftStrategy().apply(leftBound);
-      if (newLeftBound > leftBound && inTableRange(newLeftBound)) {
+      if (newLeftBound > leftBound && isRowNumberInSlice(newLeftBound)) {
+        // If the left side of the window changed remove the left most value from the aggregate
+        // function.
         function.removeLeftMost();
       }
       leftBound = newLeftBound;
 
+      // Slide the right side of the window if applicable for the window definition.
       int newRightBound = slideRightStrategy().apply(rightBound);
-      if (newRightBound > rightBound && inTableRange(newRightBound)) {
+      if (newRightBound > rightBound && isRowNumberInSlice(newRightBound)) {
+        // If the right side of the window changed add the next value to the aggregate function.
         if (isMissing(newRightBound)) {
           function.addRightMostMissing();
         } else {
@@ -82,7 +87,10 @@ class WindowSlider {
     }
   }
 
-  // Get the mirrored index about the center of the window.
+  /**
+   * Returns the mirrored index about the center of the window. Used to convert UNBOUNDED FOLLOWING
+   * windows to UNBOUNDED PRECEDING windows.
+   */
   int mirror(int rowNumber) {
     if (this.mirrored) {
       return slice.rowCount() - rowNumber - 1;
@@ -90,10 +98,14 @@ class WindowSlider {
     return rowNumber;
   }
 
+  /**
+   * Adds initial values to the aggregate function for the first window. E.G. ROWS BETWEEN CURRENT
+   * ROW AND 3 FOLLOWING would add the first four rows in the slice to the function.
+   */
   @SuppressWarnings({"unchecked", "rawtypes"})
-  private void initWindow(Column<?> sourceColumn) {
-    int leftBound = Math.max(getInitialStartIndex(), 0);
-    int rightBound = Math.min(getInitialEndIndex(), slice.rowCount() - 1);
+  private void initWindow() {
+    int leftBound = Math.max(getInitialLeftBound(), 0);
+    int rightBound = Math.min(getInitialRightBound(), slice.rowCount() - 1);
     for (int i = leftBound; i <= rightBound; i++) {
       if (isMissing(i)) {
         function.addRightMostMissing();
@@ -103,30 +115,33 @@ class WindowSlider {
     }
   }
 
-  // Set value in the destination column.
+  /** Set the value in the destination column that corresponds to the row in the view. */
   @SuppressWarnings({"unchecked", "rawtypes"})
   private void set(int rowNumberInSlice, Object value) {
     destinationColumn.set(slice.mappedRowNumber(mirror(rowNumberInSlice)), value);
   }
 
-  // Get value from the source column. Pulling from the source table behind the slide.
+  /** Get a value from the source column that corresponds to the row in the view. */
   private Object get(int rowNumberInSlice) {
     return sourceColumn.get(slice.mappedRowNumber(mirror(rowNumberInSlice)));
   }
 
-  // Determine if the value in the source column is missing.
+  /**
+   * Determine if the value in the source column that corresponds to the row in the view is missing.
+   */
   private boolean isMissing(int rowNumberInSlice) {
     return sourceColumn.isMissing(slice.mappedRowNumber(mirror(rowNumberInSlice)));
   }
 
-  private boolean inTableRange(int rowNumber) {
+  /** Returns true of the rowNumber exists in the slice. */
+  private boolean isRowNumberInSlice(int rowNumber) {
     return rowNumber >= 0 && rowNumber < slice.rowCount();
   }
 
   private Function<Integer, Integer> slideLeftStrategy() {
     switch (this.windowGrowthType) {
       case FIXED:
-      case FIXED_START:
+      case FIXED_LEFT:
         return i -> i;
       case SLIDING:
         return i -> i + 1;
@@ -138,31 +153,25 @@ class WindowSlider {
     switch (this.windowGrowthType) {
       case FIXED:
         return i -> i;
-      case FIXED_START:
+      case FIXED_LEFT:
       case SLIDING:
         return i -> i + 1;
     }
     throw new RuntimeException("Unexpected growthType: " + this.windowGrowthType);
   }
 
-  private int getInitialStartIndex() {
-    switch (this.windowGrowthType) {
-      case FIXED:
-      case FIXED_START:
-        return 0;
-      case SLIDING:
-        return this.frameStartShift;
-    }
-    throw new RuntimeException("Unexpected growthType: " + this.windowGrowthType);
+  private int getInitialLeftBound() {
+    // is zero for FIXED and FIXED_LEFT windows.
+    return this.initialLeftBound;
   }
 
-  private int getInitialEndIndex() {
+  private int getInitialRightBound() {
     switch (this.windowGrowthType) {
       case FIXED:
         return slice.rowCount() - 1;
-      case FIXED_START:
+      case FIXED_LEFT:
       case SLIDING:
-        return this.frameEndShift;
+        return this.initialRightBound;
     }
     throw new RuntimeException("Unexpected growthType: " + this.windowGrowthType);
   }
diff --git a/core/src/main/java/tech/tablesaw/analytic/WindowSpecification.java b/core/src/main/java/tech/tablesaw/analytic/WindowSpecification.java
index 4f10ae12..9eb7d98a 100644
--- a/core/src/main/java/tech/tablesaw/analytic/WindowSpecification.java
+++ b/core/src/main/java/tech/tablesaw/analytic/WindowSpecification.java
@@ -10,7 +10,8 @@ import java.util.Optional;
 import java.util.stream.Collectors;
 import tech.tablesaw.sorting.Sort;
 
-public final class WindowSpecification {
+/** This class holds data on the Partition By and Order By clauses of an an analytic query. */
+final class WindowSpecification {
 
   private final String windowName;
   private final LinkedHashSet<String> partitionColumns;
@@ -103,7 +104,6 @@ public final class WindowSpecification {
     Builder setPartitionColumns(List<String> columns) {
       this.partitioning.clear();
       this.partitioning.addAll(columns);
-      // TODO add actual duplicate columns to the error message.
       Preconditions.checkArgument(
           partitioning.size() == columns.size(),
           "Partition by Columns cannot contain duplicate columns");
diff --git a/core/src/main/java/tech/tablesaw/api/BooleanColumn.java b/core/src/main/java/tech/tablesaw/api/BooleanColumn.java
index 741f9c81..6703940a 100644
--- a/core/src/main/java/tech/tablesaw/api/BooleanColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/BooleanColumn.java
@@ -79,8 +79,6 @@ public class BooleanColumn extends AbstractColumn<Boolean>
     data = values;
   }
 
-  /** @deprecated Use BooleanColumnType.isMissingValue(byte) instead */
-  @Deprecated
   public static boolean valueIsMissing(byte b) {
     return BooleanColumnType.isMissingValue(b);
   }
diff --git a/core/src/main/java/tech/tablesaw/api/DateColumn.java b/core/src/main/java/tech/tablesaw/api/DateColumn.java
index ab951e80..d9b811b7 100644
--- a/core/src/main/java/tech/tablesaw/api/DateColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/DateColumn.java
@@ -410,8 +410,6 @@ public class DateColumn extends AbstractColumn<LocalDate>
     return table;
   }
 
-  /** @deprecated Use DateColumnType.isMissingValue(int) instead */
-  @Deprecated
   public static boolean valueIsMissing(int i) {
     return DateColumnType.isMissingValue(i);
   }
diff --git a/core/src/main/java/tech/tablesaw/api/DateTimeColumn.java b/core/src/main/java/tech/tablesaw/api/DateTimeColumn.java
index 4061ac03..2a0f5fcd 100644
--- a/core/src/main/java/tech/tablesaw/api/DateTimeColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/DateTimeColumn.java
@@ -123,8 +123,6 @@ public class DateTimeColumn extends AbstractColumn<LocalDateTime>
     return column;
   }
 
-  /** @deprecated Use DateTimeColumnType.isMissingValue(int) instead */
-  @Deprecated
   public static boolean valueIsMissing(long value) {
     return DateTimeColumnType.isMissingValue(value);
   }
diff --git a/core/src/main/java/tech/tablesaw/api/DoubleColumn.java b/core/src/main/java/tech/tablesaw/api/DoubleColumn.java
index cd9fbc71..c25e69bf 100644
--- a/core/src/main/java/tech/tablesaw/api/DoubleColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/DoubleColumn.java
@@ -42,6 +42,10 @@ public class DoubleColumn extends NumberColumn<Double> implements NumberFillers<
     this.data = data;
   }
 
+  public static boolean valueIsMissing(double value) {
+    return DoubleColumnType.isMissingValue(value);
+  }
+
   @Override
   public String getString(final int row) {
     final double value = getDouble(row);
diff --git a/core/src/main/java/tech/tablesaw/api/FloatColumn.java b/core/src/main/java/tech/tablesaw/api/FloatColumn.java
index a238fb8e..900e3da1 100644
--- a/core/src/main/java/tech/tablesaw/api/FloatColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/FloatColumn.java
@@ -74,6 +74,10 @@ public class FloatColumn extends NumberColumn<Float> {
     return data.getFloat(index);
   }
 
+  public static boolean valueIsMissing(float value) {
+    return FloatColumnType.isMissingValue(value);
+  }
+
   @Override
   public FloatColumn subset(final int[] rows) {
     final FloatColumn c = this.emptyCopy();
diff --git a/core/src/main/java/tech/tablesaw/api/InstantColumn.java b/core/src/main/java/tech/tablesaw/api/InstantColumn.java
index 98d83f17..0c7d391b 100644
--- a/core/src/main/java/tech/tablesaw/api/InstantColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/InstantColumn.java
@@ -108,8 +108,6 @@ public class InstantColumn extends AbstractColumn<Instant>
     return column;
   }
 
-  /** @deprecated Use InstantColumnType.isMissingValue(long) instead */
-  @Deprecated
   public static boolean valueIsMissing(long value) {
     return InstantColumnType.isMissingValue(value);
   }
diff --git a/core/src/main/java/tech/tablesaw/api/IntColumn.java b/core/src/main/java/tech/tablesaw/api/IntColumn.java
index 3c26cad9..7e0b6650 100644
--- a/core/src/main/java/tech/tablesaw/api/IntColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/IntColumn.java
@@ -92,8 +92,6 @@ public class IntColumn extends NumberColumn<Integer> implements CategoricalColum
     data.clear();
   }
 
-  /** @deprecated use IntColumnType.isMissingValue(value) instead */
-  @Deprecated
   public static boolean valueIsMissing(int value) {
     return IntColumnType.isMissingValue(value);
   }
diff --git a/core/src/main/java/tech/tablesaw/api/LongColumn.java b/core/src/main/java/tech/tablesaw/api/LongColumn.java
index 58d57d40..f2e1e26e 100644
--- a/core/src/main/java/tech/tablesaw/api/LongColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/LongColumn.java
@@ -85,8 +85,6 @@ public class LongColumn extends NumberColumn<Long> implements CategoricalColumn<
     return String.valueOf(getPrintFormatter().format(value));
   }
 
-  /** @deprecated Use LongColumnType.isMissingValue(value) instead */
-  @Deprecated
   public static boolean valueIsMissing(long value) {
     return LongColumnType.isMissingValue(value);
   }
diff --git a/core/src/main/java/tech/tablesaw/api/Row.java b/core/src/main/java/tech/tablesaw/api/Row.java
index 10b75749..edee4c32 100644
--- a/core/src/main/java/tech/tablesaw/api/Row.java
+++ b/core/src/main/java/tech/tablesaw/api/Row.java
@@ -4,39 +4,91 @@ import java.time.Instant;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
-import java.util.TreeMap;
 import tech.tablesaw.columns.Column;
 import tech.tablesaw.table.TableSlice;
 
 public class Row implements Iterator<Row> {
 
+  /**
+   * Wrap Map of column name to Column map and provide helpful error messages to the user when a
+   * column name cannot be found.
+   */
+  private class ColumnMap<T> {
+    private final Map<String, T> columnMap = new HashMap<>();
+    private final ColumnType columnType;
+
+    public ColumnMap(ColumnType columnType) {
+      this.columnType = columnType;
+    }
+
+    ColumnMap() {
+      this.columnType = null;
+    }
+
+    T get(String columnName) {
+      T column = columnMap.get(columnName.toLowerCase());
+      if (column == null) {
+        throwWrongTypeError(columnName);
+        throwColumnNotPresentError(columnName);
+      }
+      return column;
+    }
+
+    void put(String columnName, T column) {
+      columnMap.put(columnName.toLowerCase(), column);
+    }
+
+    /**
+     * Will get thrown when column name is correct, but used the wrong method get/set is called.
+     * E.G. the user called .getLong on an IntColumn.
+     */
+    private void throwWrongTypeError(String columnName) {
+      for (int i = 0; i < columnNames.length; i++) {
+        if (columnName.equals(columnNames[i])) {
+          String actualType = tableSlice.getTable().columns().get(i).type().name();
+          String proposedType = columnTypeName();
+          throw new IllegalArgumentException(
+                  String.format(
+                          "Column %s is of type %s and cannot be cast to %s. Use the method for %s.",
+                          columnName, actualType, proposedType, actualType));
+        }
+      }
+    }
+
+    private void throwColumnNotPresentError(String columnName) {
+      throw new IllegalStateException(
+              String.format(
+                      "Column %s is not present in table %s", columnName, tableSlice.getTable().name()));
+    }
+
+    private String columnTypeName() {
+      if (this.columnType != null) {
+        return this.columnType.name();
+      }
+      return "Object";
+    }
+  }
+
   private final TableSlice tableSlice;
   private final String[] columnNames;
-  private final Map<String, DateColumn> dateColumnMap =
-      new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-  private final Map<String, DoubleColumn> doubleColumnMap =
-      new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-  private final Map<String, IntColumn> intColumnMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-  private final Map<String, LongColumn> longColumnMap =
-      new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-  private final Map<String, ShortColumn> shortColumnMap =
-      new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-  private final Map<String, FloatColumn> floatColumnMap =
-      new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-  private final Map<String, Column<String>> stringColumnMap =
-      new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-  private final Map<String, BooleanColumn> booleanColumnMap =
-      new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-  private final Map<String, DateTimeColumn> dateTimeColumnMap =
-      new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-  private final Map<String, InstantColumn> instantColumnMap =
-      new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-  private final Map<String, TimeColumn> timeColumnMap =
-      new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-  private final Map<String, Column<?>> columnMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+  private final ColumnMap<DateColumn> dateColumnMap = new ColumnMap<>();
+  private final ColumnMap<DoubleColumn> doubleColumnMap = new ColumnMap<>(ColumnType.DOUBLE);
+  private final ColumnMap<IntColumn> intColumnMap = new ColumnMap<>(ColumnType.INTEGER);
+  private final ColumnMap<LongColumn> longColumnMap = new ColumnMap<>(ColumnType.LONG);
+  private final ColumnMap<ShortColumn> shortColumnMap = new ColumnMap<>(ColumnType.SHORT);
+  private final ColumnMap<FloatColumn> floatColumnMap = new ColumnMap<>(ColumnType.FLOAT);
+  private final ColumnMap<Column<String>> stringColumnMap = new ColumnMap<>(ColumnType.STRING);
+  private final ColumnMap<BooleanColumn> booleanColumnMap = new ColumnMap<>(ColumnType.BOOLEAN);
+  private final ColumnMap<DateTimeColumn> dateTimeColumnMap =
+          new ColumnMap<>(ColumnType.LOCAL_DATE_TIME);
+  private final ColumnMap<InstantColumn> instantColumnMap = new ColumnMap<>(ColumnType.INSTANT);
+  private final ColumnMap<TimeColumn> timeColumnMap = new ColumnMap<>(ColumnType.LOCAL_TIME);
+  private final ColumnMap<Column<?>> columnMap = new ColumnMap<>();
+  private final ColumnMap<NumericColumn<? extends Number>> numericColumnMap = new ColumnMap<>();
   private int rowNumber;
 
   public Row(Table table) {
@@ -58,18 +110,23 @@ public class Row implements Iterator<Row> {
     for (Column<?> column : tableSlice.getTable().columns()) {
       if (column instanceof DoubleColumn) {
         doubleColumnMap.put(column.name(), (DoubleColumn) column);
+        numericColumnMap.put(column.name(), (DoubleColumn) column);
       }
       if (column instanceof IntColumn) {
         intColumnMap.put(column.name(), (IntColumn) column);
+        numericColumnMap.put(column.name(), (IntColumn) column);
       }
       if (column instanceof ShortColumn) {
         shortColumnMap.put(column.name(), (ShortColumn) column);
+        numericColumnMap.put(column.name(), (ShortColumn) column);
       }
       if (column instanceof LongColumn) {
         longColumnMap.put(column.name(), (LongColumn) column);
+        numericColumnMap.put(column.name(), (LongColumn) column);
       }
       if (column instanceof FloatColumn) {
         floatColumnMap.put(column.name(), (FloatColumn) column);
+        numericColumnMap.put(column.name(), (FloatColumn) column);
       }
       if (column instanceof BooleanColumn) {
         booleanColumnMap.put(column.name(), (BooleanColumn) column);
@@ -241,6 +298,12 @@ public class Row implements Iterator<Row> {
     return stringColumnMap.get(columnName).get(getIndex(rowNumber));
   }
 
+  public boolean isMissing(String columnName) {
+    Column<?> x = columnMap.get(columnName);
+    int i = getIndex(rowNumber);
+    return x.isMissing(i);
+  }
+
   @Override
   public boolean hasNext() {
     return rowNumber < this.tableSlice.rowCount() - 1;
@@ -252,6 +315,14 @@ public class Row implements Iterator<Row> {
     return this;
   }
 
+  public void setMissing(int columnIndex) {
+    setMissing(columnNames[columnIndex]);
+  }
+
+  public void setMissing(String columnName) {
+    columnMap.get(columnName).setMissing(getIndex(rowNumber));
+  }
+
   public void setBoolean(int columnIndex, boolean value) {
     setBoolean(columnNames[columnIndex], value);
   }
@@ -344,10 +415,24 @@ public class Row implements Iterator<Row> {
     setTime(columnNames[columnIndex], value);
   }
 
+  /**
+   * Returns the row number for this row, relative to the backing column
+   *
+   * @param rowNumber the rowNumber in the TableSlice backing this (row)
+   * @return the matching row number in the underlying column.
+   */
   private int getIndex(int rowNumber) {
     return tableSlice.mappedRowNumber(rowNumber);
   }
 
+  public double getNumber(String columnName) {
+    return numericColumnMap.get(columnName).getDouble(rowNumber);
+  }
+
+  public ColumnType getColumnType(String columnName) {
+    return columnMap.get(columnName).type();
+  }
+
   @Override
   public String toString() {
     Table t = tableSlice.getTable().emptyCopy();
@@ -361,4 +446,4 @@ public class Row implements Iterator<Row> {
   public void setTime(String columnName, LocalTime value) {
     timeColumnMap.get(columnName).set(rowNumber, value);
   }
-}
+}
\ No newline at end of file
diff --git a/core/src/main/java/tech/tablesaw/api/ShortColumn.java b/core/src/main/java/tech/tablesaw/api/ShortColumn.java
index 18884324..60805874 100644
--- a/core/src/main/java/tech/tablesaw/api/ShortColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/ShortColumn.java
@@ -59,8 +59,6 @@ public class ShortColumn extends NumberColumn<Short> implements CategoricalColum
     return create(name);
   }
 
-  /** @deprecated Use ShortColumnType.isMissingValue(int value) instead */
-  @Deprecated
   public static boolean valueIsMissing(int value) {
     return ShortColumnType.isMissingValue(value);
   }
diff --git a/core/src/main/java/tech/tablesaw/api/StringColumn.java b/core/src/main/java/tech/tablesaw/api/StringColumn.java
index 2148673d..a36b672c 100644
--- a/core/src/main/java/tech/tablesaw/api/StringColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/StringColumn.java
@@ -55,8 +55,6 @@ public class StringColumn extends AbstractStringColumn {
         return f1.compareTo(f2);
       };
 
-  /** @deprecated Use StringColumnType.isMissingValue(int value) instead */
-  @Deprecated
   public static boolean valueIsMissing(String string) {
     return StringColumnType.isMissingValue(string);
   }
diff --git a/core/src/main/java/tech/tablesaw/api/TextColumn.java b/core/src/main/java/tech/tablesaw/api/TextColumn.java
index 52b3ba4b..359033fb 100644
--- a/core/src/main/java/tech/tablesaw/api/TextColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/TextColumn.java
@@ -75,8 +75,6 @@ public class TextColumn extends AbstractStringColumn {
     }
   }
 
-  /** @deprecated Use TextColumnType.isMissingValue(string) instead */
-  @Deprecated
   public static boolean valueIsMissing(String string) {
     return TextColumnType.isMissingValue(string);
   }
diff --git a/core/src/main/java/tech/tablesaw/api/TimeColumn.java b/core/src/main/java/tech/tablesaw/api/TimeColumn.java
index 63f881f3..afb60d46 100644
--- a/core/src/main/java/tech/tablesaw/api/TimeColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/TimeColumn.java
@@ -78,8 +78,6 @@ public class TimeColumn extends AbstractColumn<LocalTime>
     data = new IntArrayList(DEFAULT_ARRAY_SIZE);
   }
 
-  /** @deprecated Use TimeColumnType.isMissingValue() instead */
-  @Deprecated
   public static boolean valueIsMissing(int i) {
     return TimeColumnType.isMissingValue(i);
   }
diff --git a/core/src/main/java/tech/tablesaw/columns/AbstractColumnParser.java b/core/src/main/java/tech/tablesaw/columns/AbstractColumnParser.java
index dc8c72ca..07e5e152 100644
--- a/core/src/main/java/tech/tablesaw/columns/AbstractColumnParser.java
+++ b/core/src/main/java/tech/tablesaw/columns/AbstractColumnParser.java
@@ -33,7 +33,7 @@ public abstract class AbstractColumnParser<T> {
     return columnType;
   }
 
-  protected boolean isMissing(String s) {
+  public boolean isMissing(String s) {
     if (s == null) {
       return true;
     }
diff --git a/core/src/main/java/tech/tablesaw/io/ColumnTypeDetector.java b/core/src/main/java/tech/tablesaw/io/ColumnTypeDetector.java
index 4dfee66d..bf20be51 100644
--- a/core/src/main/java/tech/tablesaw/io/ColumnTypeDetector.java
+++ b/core/src/main/java/tech/tablesaw/io/ColumnTypeDetector.java
@@ -145,15 +145,25 @@ public class ColumnTypeDetector {
 
     CopyOnWriteArrayList<ColumnType> typeCandidates = new CopyOnWriteArrayList<>(typeArray);
 
+    boolean hasNonMissingValues = false;
+
     for (String s : valuesList) {
       for (AbstractColumnParser<?> parser : parsers) {
-        if (!parser.canParse(s)) {
-          typeCandidates.remove(parser.columnType());
-          parsers.remove(parser);
+        if (!parser.isMissing(s)) {
+          hasNonMissingValues = true;
+          if (!parser.canParse(s)) { // we can skip this test if we know the value is missing
+            typeCandidates.remove(parser.columnType());
+            parsers.remove(parser);
+          }
         }
       }
     }
-    return selectType(typeCandidates);
+    if (hasNonMissingValues) {
+      return selectType(typeCandidates);
+    } else {
+      // the last type in the typeArray is the default
+      return typeArray.get(typeArray.size() - 1);
+    }
   }
 
   /**
diff --git a/docs/README.md b/docs/README.md
index 978ba5a3..795781cb 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,9 +1,10 @@
 # Tablesaw Documentation
 
-* [Introductory Tutorial](https://jtablesaw.github.io/tablesaw/gettingstarted) If you haven't used Tablesaw, or are having trouble 'getting it', you should definitely start here. It covers most everything you need to begin using it productively. 
-* [User Guide](https://jtablesaw.github.io/tablesaw/userguide/toc)
+* [The Tornado Tutorial](https://jtablesaw.github.io/tablesaw/tutorial) A short tutorial using Tablesaw to analyze a Tornado dataset.
+* [Getting Started](https://jtablesaw.github.io/tablesaw/gettingstarted)  If you haven't used Tablesaw, or are having trouble 'getting it', you should start here. It covers most everything you need to begin using it productively. 
+* [User Guide](https://jtablesaw.github.io/tablesaw/userguide/toc) An in-depth guide to much of Tablesaw's functionality.
 * [Javadoc](http://www.javadoc.io/doc/tech.tablesaw/tablesaw-core/)
-* [Contributor's Guide](https://jtablesaw.github.io/tablesaw/contributing)
+* [Contributor's Guide](https://jtablesaw.github.io/tablesaw/contributing) We welcome contributions! Here's how. 
 * [Release Notes](https://github.com/jtablesaw/tablesaw/releases)
 
 Can't find what you're looking for?
diff --git a/docs/gettingstarted.md b/docs/gettingstarted.md
index 9000fa45..e38b7025 100644
--- a/docs/gettingstarted.md
+++ b/docs/gettingstarted.md
@@ -2,7 +2,7 @@
 
 Java is a great language, but it wasn't designed for data analysis. Tablesaw makes it easy to do data analysis in Java. 
 
-Once you're up and running, this tutorial will tell you what you need to know to use Tablesaw.
+This tutorial will help you get up and running, and introduce some of Tablesaw's basic functionality.
 
 ## Setup
 
@@ -28,7 +28,7 @@ As you would expect, Tablesaw is all about tables, and tables are made of column
 
 A column is a named, one-dimensional collection of data. It may or may not be part of a table. All data in a column must be of the same type. 
 
-Tablesaw supports columns for Strings, doubles, booleans, LocalDates, LocalTimes, and LocalDateTimes. The date and time columns are comparable with the java.time classes introduced in Java 8.
+Tablesaw supports columns for Strings, floats, doubles, ints, shorts, longs, booleans, LocalDates, LocalTimes, Instants, and LocalDateTimes. The date and time columns are comparable with the java.time classes introduced in Java 8. 
 
 To create a column you can use one of its static *create()* methods:
 
@@ -47,7 +47,7 @@ Column: Test
 ```
 Each column has an associated 0-based index. To get an individual value call *get()* with the index. 
 ```java
-nc.get(2);
+double three = nc.get(2);
 ```
 which returns 3.0.
 
@@ -74,7 +74,7 @@ There are so many columnar operations in Tablesaw that, as a general rule, if yo
 
 #### Objects and Primitives
 
-Many Java programs and programmers work exclusively with Objects, rather than primitives. In Tablesaw, we use primitives whenever possible because they use  *much* less memory than their boxed alternatives.  A Byte object, for example, uses as much memory as a primitive double, even though bytes have a range of only 256 values. 
+Many Java programs and programmers work exclusively with Objects, rather than primitives. In Tablesaw, we often use primitives because they use *much* less memory than their boxed alternatives.  A Byte *object*, for example, uses as much memory as a primitive double, even though bytes have a range of only 256 values. 
 
 There is a price for this frugality. When you work with primitives, you forgo some common java capabilities, like the use of standard Java 8 predicates. While Java thoughtfully provides some specialized predicate interfaces (e.g. *IntPredicate*), they don't provide any primitive *BiPredicate* implementations, nor do their primitive interfaces cover all primitive types. Without an IntBiPredicate, we can't implement operations like a < b. So we were left to roll our own. You can find them in the package *tech.tablesaw.filtering.predicates*. They work like the standard objects. 
 
@@ -112,10 +112,10 @@ If the methods returned columns directly, they couldn't be combined this way.  I
 These examples show how to select using predicates. You can also use a selection to retrieve the value at a specific index, or indexes. All of the following are supported:
 
 ```java
-nc.where(Selection.with(0, 2));  	// returns 2 rows with the given indexes
-nc.where(Selection.selectNRowsAtRandom(2)); // returns 2 randomly selected rows
-nc.where(Selection.withRange(1, 3));		// returns rows 1-3 inclusive
-nc.where(Selection.withoutRange(1, 3));		// returns row 0
+nc.where(Selection.with(0, 2));  							// returns 2 rows with the given indexes
+nc.where(Selection.selectNRowsAtRandom(2)); 	// returns 2 randomly selected rows
+nc.where(Selection.withRange(1, 3));					// returns rows 1-3 inclusive
+nc.where(Selection.withoutRange(1, 3));				// returns row 0
 ```
 
 If you have several columns of the same length as you would in a table of data, you can make a selection with one column and use it to filter another:
@@ -143,7 +143,7 @@ StringColumn s;
 s = aStringColumn.upperCase();
 s = s.replaceFirst("foo", "bar")
 s = s.substring(3, 10);
-s = s.padEnd(4, 'x');				// put 4 x chars at the end of each string
+s = s.padEnd(4, 'x');					// put 4 x chars at the end of each string
 
 // this returns the common prefix of each row in two columns
 y = s.commonPrefix(anotherStringColumn);
@@ -202,7 +202,7 @@ Tablesaw does a pretty good job at guessing the column types for many data sets,
 
 #### Exploring Tables
 
-Because Tablesaw excels at manipulating tables, we use them whenever we can.  When you ask tablesaw for the structure of a table, the answer comes back in the form of another table where one column contains the column names, etc.  The structure() method is one of several that help you to know a new data set. Here are some examples.
+Because Tablesaw excels at manipulating tables, we use them whenever we can.  When you ask tablesaw for the structure of a table, the answer comes back in the form of another table where one column contains the column names, etc.  The methods ` structure()`, `shape()`, `first(n)`, and `last(n)` can help you get to know a new data set. Here are some examples.
 
 ```java
 Table structure = bushTable.structure();
@@ -225,8 +225,7 @@ Table head = bushTable.first(3);
 	 2004-01-21  |      53.0  |  fox  |
 	 2004-01-07  |      58.0  |  fox  |
                      
-Table tail = myTable.last(3);
-> etc.
+Table tail = myTable.last(3);  // etc.
 ```
 
 Table's *toString()* method returns a String representation like those shown above. It returns a limited number of rows by default, but you can also use *table.printAll()*, or *table.print(n)* to get the output you want.
@@ -238,12 +237,12 @@ Of course, this is just the beginning of exploratory data analysis. You can also
 Often you'll work with specific columns in a table. Here are some useful methods:
 
 ```java
-table.columnNames();  			// returns all column names
-List<Column> = table.columns(); // returns all the columns in the table
+table.columnNames();  			              // returns all column names
+List<Column> = table.columns();           // returns all the columns in the table
 
 // removing columns
-table.removeColumns("Foo");			// keep everything but "foo"
-table.retainColumns("Foo", "Bar");  // only keep foo and bar
+table.removeColumns("Foo");			         // keep everything but "foo"
+table.retainColumns("Foo", "Bar");       // only keep foo and bar
 table.removeColumnsWithMissingValues();
 
 // adding columns
@@ -265,23 +264,26 @@ remembering column names is enough of a burden without having to remember exactl
 Columns can be retrieved from tables by name or position. The simplest method *column()* returns a object of type Column. This may be good enough, but often you want to get a column of a specific type. For example, you would need to cast the value returned to a NumberColumn to use its values in a scatter plot. 
 
 ```java
-Column nc = table.column("Foo"); // returns the column named 'Foo' if it's in the table.
+Column column = table.column("Foo"); // returns the column named 'Foo' if it's in the table.
 // or 
-Column nc = table.column(0);  // returns the first column
+Column column = table.column(0);     // returns the first column
+```
+
+When a variable type is "Column" it only provides methods that are available on *all* columns. You can't perform math or do a string replace directly on a Column type. If you need a StringColumn you could cast the column, for example: 
 
-// To work with a StringColumn you can cast the return value
-StringColumn nc = (StringColumn) table.column(0); 
-// Now you can do some numeric stuff with the column nc
+```java
+StringColumn sc = (StringColumn) table.column(0); 
 ```
 
 Table also supports methods that return columns of the desired type directly:
 
 ```Java
-StringColumn nc = table.stringColumn(0); 
-DateColumn dc = table.dateColumn("start date");
+StringColumn strings = table.stringColumn(0); 
+DateColumn dates = table.dateColumn("start date");
+DoubleColumn doubles = table.doubleColumn("doubles");
 ```
 
-If you want all the columns of specific type, you can get those as well. The method columnsOfType(aColumnType) returns them as a List, but you still have to cast the results. For example, to format all columns as ints when you print them, you could do this. 
+If you want all the columns of specific type, you can get those as well. The method columnsOfType(aColumnType) returns them as a List, but you still have to cast the results. For example, to format all DoubleColumns as ints when you print them, you could do this: 
 
 ```Java
 table.columnOfType(ColumnType.DOUBLE).forEach(x ->                                               		((DoubleColumn)x).setPrintFormatter(NumberColumnFormatter.ints()));
@@ -301,7 +303,7 @@ result = table.dropRowsWithMissingValues();
 result = table.dropWhere(table.numberColumn(0).isLessThan(100));
 
 // add rows
-table.addRow(43, sourceTable);	// adds row 43 from sourceTable to the receiver
+destinationTable.addRow(43, sourceTable);	// adds row 43 from sourceTable to the receiver
 
 // sampling
 table.sample(200);				// select 200 rows at random from table 
@@ -315,7 +317,7 @@ for (Row row : table) {
 }
 ```
 
-There are better ways, however. Another approach lets you skip the iteration and just provide a Consumer for each row.
+Another approach lets you skip the iteration and just provide a Consumer for each row.
 
 ```java
 table.stream().forEach(row -> {
@@ -353,8 +355,8 @@ Table sorted = table.sort("foo", "bar", "bam");
 The above code sorts in ascending order by default.  Other options are shown below:
 
 ```java
+sorted = table.sortAscending("bar"); 	 // just like sort(), but makes the order explicit.
 sorted = table.sortDescending("foo");
-sorted = table.sortAscending("bar"); 	// just like sort(), but makes order explicit.
 
 /* sort on foo ascending, then bar descending. Note the minus sign preceding the name of column bar. */
 sorted = table.sort("foo", "-bar");		 
@@ -364,7 +366,7 @@ See [Sorting](https://jtablesaw.github.io/tablesaw/userguide/sorting) for more i
 
 #### Filtering tables with selections
 
-Tables also use selections to perform filtering. The basic approach is similar.
+We showed an example above of how to use the where() method to perform filtering. The basic approach is similar.
 
 ```java
 Table t = Table.create("test").addColumns(nc1, nc2);
@@ -379,8 +381,8 @@ Query filters can be combined using the logical operations *and*, *or*, and *not
 
 ```java
 Table result = t.where(
-	t.and(nc1.isGreaterThan(4),
-         t.or(t.not(nc2.isLessThanOrEqualTo(5)),
+	and(nc1.isGreaterThan(4),
+         or(t.not(nc2.isLessThanOrEqualTo(5)),
          		nc2.isEven()));
 ```
 
diff --git a/docs/tutorial.md b/docs/tutorial.md
index 66c21b1c..9f481efd 100644
--- a/docs/tutorial.md
+++ b/docs/tutorial.md
@@ -1,9 +1,8 @@
 
 
-# Introducing Tablesaw: A brief tutorial
+# Tablesaw: A brief tutorial
 
-Tablesaw is a fairly large library. In this section, we touch on the most common operations. If you haven't already done
-so, we strongly recommend that you read the Getting Started guide, before continuing here. 
+Tablesaw is a large library. We'll use Tablesaw to look at data about Tornadoes. If you haven't already done so, we strongly recommend that you read the Getting Started guide, before continuing here. 
 
 ## Exploring Tornadoes 
 
@@ -25,9 +24,11 @@ All the data is in the Tablesaw *data* folder. The code is taken from the Tornad
 
 Here we read a csv file of tornado data. Tablesaw infers the column types by sampling the data.
 
-    Table tornadoes = Table.read().csv("../data/tornadoes_1950-2014.csv");
+```java
+Table tornadoes = Table.read().csv("../data/tornadoes_1950-2014.csv");
+```
 
-Note that the file is addressed relative to the current working directory. You may have to change it for your code. 
+**Note:** that the file is addressed relative to the current working directory. You may have to change it for your code. 
 
 ### Viewing table metadata
 
@@ -46,21 +47,23 @@ The *shape()* method displays the row and column counts:
 
 *structure()* shows the index, name and type of each column
 
-    tornadoes.structure();
-    
-    >>  Structure of data/tornadoes_1950-2014.csv
-            Index Column Names Column Type 
-            0     Date         LOCAL_DATE  
-            1     Time         LOCAL_TIME  
-            2     State        CATEGORY    
-            3     State No     DOUBLE     
-            4     Scale        DOUBLE     
-            5     Injuries     DOUBLE     
-            6     Fatalities   DOUBLE     
-            7     Start Lat    DOUBLE       
-            8     Start Lon    DOUBLE       
-            9     Length       DOUBLE       
-            10    Width        DOUBLE       
+```java
+tornadoes.structure().printAll();
+
+>>  Structure of data/tornadoes_1950-2014.csv
+        Index Column Names Column Type 
+        0     Date         LOCAL_DATE  
+        1     Time         LOCAL_TIME  
+        2     State        STRING    
+        3     State No     DOUBLE     
+        4     Scale        DOUBLE     
+        5     Injuries     INTEGER     
+        6     Fatalities   INTEGER     
+        7     Start Lat    DOUBLE       
+        8     Start Lon    DOUBLE       
+        9     Length       DOUBLE       
+        10    Width        DOUBLE       
+```
 
 Like many Tablesaw methods, *structure()* returns a table.  You can then produce a string representation for display. For convenience, calling *toString()* on a table invokes *print()*, which produces a string representation of the table table. To display the table then, you can simply call. 
 
@@ -98,7 +101,7 @@ Mapping operations in Tablesaw take one or more columns as inputs and produce a
 StringColumn month = tornadoes.dateColumn("Date").month();
 ```
 
-Now that you have a new column, you can add it to a table:
+Now that you have a new column, you can add it to the table:
 
 ```java
 tornadoes.addColumn(month);
@@ -139,13 +142,13 @@ table.column("Fatalities").summary().print();
 You can write your own methods to filter rows, but it’s easier to use the built-in filter classes as shown below:
 
 ```java 
-result = tornadoes.selectWhere(DOUBLEColumn("Fatalities").isGreaterThan(0));
+result = tornadoes.selectWhere(doubleColumn("Fatalities").isGreaterThan(0));
 
 result = tornadoes.selectWhere(dateColumn("Date").isInApril());
 
 result = tornadoes.selectWhere(either
-           (DOUBLEColumn("Width").isGreaterThan(300)),   // 300 yards
-           (DOUBLEColumn("Length").isGreaterThan(10)));  // 10 miles
+           (doubleColumn("Width").isGreaterThan(300)),   // 300 yards
+           (doubleColumn("Length").isGreaterThan(10)));  // 10 miles
 
 result = tornadoes.select("State", "Date").where(column("Date").isInQ2());
 ```
@@ -223,7 +226,7 @@ summer.addColumn(summer.dateColumn("Date").lag(1));
 DateColumn summerDate = summer.dateColumn("Date");
 DateColumn laggedDate = summer.dateColumn("Date lag(1)");
 
-DOUBLEColumn delta = laggedDate.daysUntil(summerDate);
+DoubleColumn delta = laggedDate.daysUntil(summerDate);
 summer.addColumn(delta);
 ```
 
@@ -252,8 +255,6 @@ summary.nCol(1).mean();
 >>	Average days between tornadoes in the summer: 1.761269943549373
 ```
 
-
-
 ### Saving your data
 
 To save a table, you can write it as a CSV file:
@@ -262,5 +263,5 @@ To save a table, you can write it as a CSV file:
 tornadoes.write().csv("data/rev_tornadoes_1950-2014.csv");
 ```
 
-And that’s it for the introduction. Stay tuned for more info about advanced features.
+And that’s it for the introduction. Please see the User Guide for more information.
 
diff --git a/docs/userguide/importing_data.md b/docs/userguide/importing_data.md
index f186fc36..50515d5c 100644
--- a/docs/userguide/importing_data.md
+++ b/docs/userguide/importing_data.md
@@ -1,13 +1,32 @@
 [Contents](https://jtablesaw.github.io/tablesaw/userguide/toc)
 
-Importing data
-==============
+# Importing & exporting data
 
-To minimize the size of the core library, the readers are packaged in separate modules except for the CSV reader. You will need to include them in your project to use the `Table.read()` methods.
+## Table of contents
+
+[TOC]
+
+## Supported Formats
+
+Tablesaw supports importing and exporting data to and from a variety of data types and sources. 
+
+| Format                         | Import | Export |
+| :----------------------------- | :----- | :----- |
+| CSV (and other delimited text) | Yes    | Yes    |
+| JSON                           | Yes    | Yes    |
+| RDBMS (via JDBC)               | Yes    |        |
+| Fixed Width Text               | Yes    | Yes    |
+| Excel                          | Yes    | Yes    |
+
+## Importing data
+
+**Note**: To minimize the size of the core library, some of the readers (currently JSON, HTML tables, and Excel files) are packaged in separate modules. You will need to include them in your project to use the `Table.read()` methods.
 
 See the Javadoc for [DataFrameReader](http://static.javadoc.io/tech.tablesaw/tablesaw-core/0.31.0/tech/tablesaw/io/DataFrameReader.html) for a listing of all the `Table.read()` methods that are available.
 
-## Reading CSV files
+### Text files (CSV, Tab-delimited, Fixed-width field, etc.)
+
+Most text formats are treated similarly. This section covers rectangular text formats like CSV, but much of the information is also relevant for JSON, Excel, and HTML tables. 
 
 The easiest way to load data from a CSV file on disk is to use:
 
@@ -22,9 +41,9 @@ You can create an options object with a builder:
 ```Java
 CsvReadOptionsBuilder builder = 
 	CsvReadOptions.builder("myFile.csv")
-		.separator('\t')			// table is tab-delimited
-		.header(false)				// no header
-		.dateFormat("yyyy.MM.dd");  // the date format to use. 
+		.separator('\t')										// table is tab-delimited
+		.header(false)											// no header
+		.dateFormat("yyyy.MM.dd");  				// the date format to use. 
 
 CsvReadOptions options = builder.build();
 
@@ -39,13 +58,13 @@ The *dateFormat* lets you provide a format for reading dates. All dates in the f
 
 When the table is created, it is given a default name based on the name of the file it was loaded from. You can change the name at any time using ```table.setName(aString);```. The table name is used in printing the table and information about it.
 
-### Column types
+#### Column types
 
 With all these methods, Tablesaw looks at the data in each column in the file and takes a wild guess at the type. Actually, it looks at a *sample* of the data and applies some heuristics. Of course, it’s possible that the data set includes rare values that are missed in the type inference sample. If that happens, you can set the option ```sample(false)``` to consider all the data when performing type inference. 
 
 If nothing else seems to fit, the column is read as a StringColumn. Usually, Tablesaw gets it right, but sometimes it needs a little help. 
 
-#### Specifying the datatypes for each column
+##### Specifying the datatypes for each column
 
 By a little help, we mean you could specify the types explicitly, by passing an array of ColumnType objects to the read().csv() method. For example:
 
@@ -58,13 +77,13 @@ Table t = Table.read().usingOptions(CsvReadOptions
 
 If that seems like a lot of work, it does have some advantages.
 
-First, it reduces the loading time as the system does not need to infer the column types, which takes some time. The amount of time saved can be noticeable if the file is large.
+First, it reduces the loading time as the system does not need to infer the column types. The time saved can be significant if the file is large.
 
 Second, it gives you complete control over the types for your columns. 
 
 In some cases, you *must* specify the column type, because Tablesaw can’t guess correctly. For example, if a file has times encoded as HHmm so that noon appears as ‘1200’, it’s impossible to infer that this means 12:00 noon, and not the integer 1,200.  
 
-#### A shortcut: Getting the guessed column types
+##### A shortcut: Getting the guessed column types
 
 If the table has many columns, it can be tedious to build the column type array by hand. To help, CsvReader has methods that return the inferred ColumnTypes in the form of an array, or as a String. The String is formatted so that it resembles a Java array literal. This method can be used even if reading the file fails.
 
@@ -80,7 +99,7 @@ System.out.println(types);
 
 Note that the returned String is a legal array literal you can paste into Java code: the types are comma separated, and the index position and the column name are provided such that they would be interpreted as comments. You can paste it into your code and then edit it to fix whatever column types are incorrect.
 
-#### Skipping columns during import
+##### Skipping columns during import
 
 Another advantage to specifying the column types is that you can skip some if you don't need them. You can prevent those columns from being imported by using the special “SKIP” column type as shown below:
 
@@ -93,7 +112,7 @@ Table t = Table.read().usingOptions(CsvReadOptions
 
 In this example, the first and last columns are not loaded.
 
-### Handling Missing data
+#### Handling Missing data
 
 Tablesaw has a predefined set of strings that it interprets as missing data when reading from a CSV file. These are: “NaN”,  “*”, “NA”, “null” and, of course, the empty string “”.
 
@@ -107,7 +126,7 @@ Table t = Table.read().usingOptions(CsvReadOptions
     .missingValueIndicator("-"));
 ```
 
-### Dealing with Dates and Times
+#### Dealing with Dates and Times
 
 Importing dates and times can be tricky because of Locales and the wide variety of possible formats. As with other Column types, Tablesaw does its best to determine what type is represented and import it correctly. When this fails, two things can help. The first is to specify a locale.  A locale can also help with number formats. 
 
@@ -122,7 +141,7 @@ Table t = Table.read().usingOptions(CsvReadOptions
     .dateTimeFormat("yyyy.MM.dd::HH:mm:ss");
 ```
 
-## Using the Stream API
+### Using the Stream API
 
 All the examples above attempt to streamline the loading process when you have a CSV file stored on your file system. A more flexible way to load a CSV is using the Stream interface, which takes a java.io.InputStream as a parameter.
 
@@ -132,7 +151,7 @@ Table.read().csv(InputStream stream, String tableName);
 
 It can be used to read local files, but also files read across the net, in S3, etc. Here’s some examples of how it can be used.
 
-### Loading a CSV from a Website:
+#### Loading a CSV from a Website:
 
 ```java
 ColumnType[] types = {SHORT_INT, FLOAT, SHORT_INT};
@@ -143,7 +162,7 @@ Table table = Table.read().usingOptions(CsvReadOptions.builder(new URL(location)
   	.columnTypes(types)));
 ```
 
-### Loading a CSV from S3:
+#### Loading a CSV from S3:
 
 ```Java
 ColumnTypes[] types = {SHORT_INT, FLOAT, SHORT_INT};
@@ -169,7 +188,7 @@ Table restaurants = Table.read()
 		.usingOptions(CsvReadOptions.builder(reader, "restaurants"));
 ```
 
-## Importing from a Database
+### Importing from a Database
 
 It's equally easy to create a table from the results of a database query. In this case, you never need to specify the column types, because they are inferred from the database column types. 
 
@@ -190,7 +209,7 @@ try (Statement stmt = conn.createStatement()) {
 }
 ```
 
-## Importing from HTML, JSON, Excel
+### Importing from HTML, JSON, Excel
 
 Tablesaw supports importing data from HTML, JSON, and Excel. See the Javadoc for the [Table.read()](http://static.javadoc.io/tech.tablesaw/tablesaw-core/0.31.0/tech/tablesaw/io/DataFrameReader.html) methods for more info. You will need to add the corresponding optional dependency:
 
@@ -214,3 +233,9 @@ Tablesaw supports importing data from HTML, JSON, and Excel. See the Javadoc for
   <artifactId>tablesaw-excel</artifactId>
 </dependency>
 ```
+
+## Exporting data
+
+
+
+TODO
\ No newline at end of file
diff --git a/docs/userguide/toc.md b/docs/userguide/toc.md
index fc1518ad..66c05a29 100644
--- a/docs/userguide/toc.md
+++ b/docs/userguide/toc.md
@@ -11,7 +11,7 @@ Tablesaw User Guide
     * [Sorting](https://jtablesaw.github.io/tablesaw/userguide/sorting)
     * [Filtering (Queries)](https://jtablesaw.github.io/tablesaw/userguide/filters)
     * [Mapping](https://jtablesaw.github.io/tablesaw/userguide/mapping)
-    * [Summarizing (Aggregation/Reducing)](https://jtablesaw.github.io/tablesaw/userguide/reducing)
+    * [Summarizing (a.k.a. "Aggregation", or "Reducing")](https://jtablesaw.github.io/tablesaw/userguide/reducing)
   * [Working with Rows](https://jtablesaw.github.io/tablesaw/userguide/rows)
 * Plotting and Visualization
   * [Introduction](https://jtablesaw.github.io/tablesaw/userguide/Introduction_to_Plotting)
@@ -21,11 +21,11 @@ Tablesaw User Guide
   * [Simple plotting API: Time Series, Line Charts and Area charts](https://jtablesaw.github.io/tablesaw/userguide/TimeSeries)
   * [Writing Custom Visualizations](https://jtablesaw.github.io/tablesaw/userguide/Visualization_custom)
 * Data Science
-  * Introduction
+  * Introduction (*Not yet available*)
   * [Linear Regression with Smile and Tablesaw: Moneyball Tutorial](https://jtablesaw.github.io/tablesaw/userguide/ml/Moneyball Linear regression)
-  * [Creating cross-tabs](https://jtablesaw.github.io/tablesaw/userguide/crosstabs)
-  * Non-hierarchical clustering (with Smile)
-  * Hierarchical clustering (with Smile)
-  * Classification using Logistic Regression (with Smile)
-  * Classification using Random Forests (with Smile)
-* Using Tablesaw in Jupyter
+  * [Creating cross-tabs](https://jtablesaw.github.io/tablesaw/userguide/crosstabs) 
+  * Non-hierarchical clustering (with Smile) (*Not yet available*)
+  * Hierarchical clustering (with Smile) (*Not yet available*)
+  * Classification using Logistic Regression (with Smile) (*Not yet available*)
+  * Classification using Random Forests (with Smile) (*Not yet available*)
+* Using Tablesaw in Jupyter () (*Not yet available*)
diff --git a/pom.xml b/pom.xml
index 95595679..cfbf7e5a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -85,6 +85,9 @@
                     <configuration>
                         <detectOfflineLinks>false</detectOfflineLinks>
                         <failOnError>false</failOnError>
+                        <quiet>true</quiet>
+                        <additionalOptions>-Xdoclint:none</additionalOptions>
+                        <additionalJOption>-Xdoclint:none</additionalJOption>
                     </configuration>
                     <executions>
                         <execution>
@@ -238,6 +241,11 @@
 
     <dependencyManagement>
         <dependencies>
+            <dependency>
+                <groupId>org.slf4j</groupId>
+                <artifactId>slf4j-api</artifactId>
+                <version>1.7.28</version>
+            </dependency>
             <dependency>
                 <groupId>org.junit.jupiter</groupId>
                 <artifactId>junit-jupiter-engine</artifactId>


Test patch:
diff --git a/core/src/test/java/tech/tablesaw/analytic/AnalyticAggregateFunctionsTest.java b/core/src/test/java/tech/tablesaw/analytic/AnalyticAggregateFunctionsTest.java
index 1a2be5e0..572f873e 100644
--- a/core/src/test/java/tech/tablesaw/analytic/AnalyticAggregateFunctionsTest.java
+++ b/core/src/test/java/tech/tablesaw/analytic/AnalyticAggregateFunctionsTest.java
@@ -3,32 +3,22 @@ package tech.tablesaw.analytic;
 import static org.junit.jupiter.api.Assertions.*;
 
 import com.google.common.collect.ImmutableList;
-import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import tech.tablesaw.analytic.AggregateFunctions.Max;
 import tech.tablesaw.analytic.AggregateFunctions.Sum;
-import tech.tablesaw.api.Table;
-import tech.tablesaw.io.csv.CsvReadOptions;
 
 class AnalyticAggregateFunctionsTest {
 
-  private Table table;
-
-  @BeforeEach
-  public void setUp() throws Exception {
-    table = Table.read().csv(CsvReadOptions.builder("../data/bush.csv"));
-  }
-
   @Test
   public void testSumFixed() {
-    AggregateFunction<Integer, Double> function = new Sum<Integer>().fixedFunction();
+    AggregateFunction<Integer, Double> function = new Sum<Integer>().functionForAppendWindows();
     function.addAllRightMost(ImmutableList.of(10, 10, 10));
     assertEquals(30, function.getValue());
   }
 
   @Test
   public void testSumSliding() {
-    AggregateFunction<Integer, Double> function = new Sum<Integer>().slidingFunction();
+    AggregateFunction<Integer, Double> function = new Sum<Integer>().functionForSlidingWindows();
     function.addAllRightMost(ImmutableList.of(10, 10, 10));
     function.removeLeftMost();
     function.addRightMost(100);
@@ -37,14 +27,14 @@ class AnalyticAggregateFunctionsTest {
 
   @Test
   public void testMaxFixed() {
-    AggregateFunction<Integer, Double> function = new Max<Integer>().fixedFunction();
+    AggregateFunction<Integer, Double> function = new Max<Integer>().functionForAppendWindows();
     function.addAllRightMost(ImmutableList.of(11, 10, 9));
     assertEquals(11, function.getValue());
   }
 
   @Test
   public void testMaxSliding() {
-    AggregateFunction<Integer, Double> function = new Max<Integer>().slidingFunction();
+    AggregateFunction<Integer, Double> function = new Max<Integer>().functionForSlidingWindows();
     function.addAllRightMost(ImmutableList.of(100, 10, 9));
     function.removeLeftMost();
     function.addRightMost(3);
diff --git a/core/src/test/java/tech/tablesaw/analytic/AnalyticQueryEngineTest.java b/core/src/test/java/tech/tablesaw/analytic/AnalyticQueryEngineTest.java
index d6ff6a24..6a623518 100644
--- a/core/src/test/java/tech/tablesaw/analytic/AnalyticQueryEngineTest.java
+++ b/core/src/test/java/tech/tablesaw/analytic/AnalyticQueryEngineTest.java
@@ -1,9 +1,11 @@
 package tech.tablesaw.analytic;
 
 import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import com.google.common.collect.ImmutableList;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 import tech.tablesaw.api.DoubleColumn;
@@ -12,17 +14,22 @@ import tech.tablesaw.api.Table;
 
 class AnalyticQueryEngineTest {
 
-  private static Table source;
+  private static Table referenceImplementation;
 
-  // Before all is a few seconds faster.
+  // Before runs tests a few seconds faster.
   @BeforeAll
   public static void setUp() throws Exception {
     // Reference implementation generated from BigQuery.
-    source = Table.read().csv("../data/bush_analytic_reference_implementation.csv");
+    referenceImplementation =
+        Table.read().csv("../data/bush_analytic_reference_implementation.csv");
   }
 
-  private double[] sourceColumnAsDouble(String columnName) {
-    return source.intColumn(columnName).asDoubleArray();
+  private double[] intSourceColumnAsDoubleArray(String columnName) {
+    return referenceImplementation.intColumn(columnName).asDoubleArray();
+  }
+
+  private double[] doubleSourceColumnAsDoubleArray(String columnName) {
+    return referenceImplementation.doubleColumn(columnName).asDoubleArray();
   }
 
   @Test
@@ -106,7 +113,7 @@ class AnalyticQueryEngineTest {
   public void unoundedPrecedingAnd5Preceding() {
     AnalyticQuery query =
         AnalyticQuery.query()
-            .from(source)
+            .from(referenceImplementation)
             .partitionBy("who")
             .orderBy("date")
             .rowsBetween()
@@ -116,25 +123,43 @@ class AnalyticQueryEngineTest {
             .as("sum")
             .max("approval")
             .as("max")
+            .min("approval")
+            .as("min")
+            .mean("approval")
+            .as("mean")
+            .count("approval")
+            .as("count")
             .build();
 
     AnalyticQueryEngine queryEngine = AnalyticQueryEngine.create(query);
     Table result = queryEngine.execute();
 
-    double[] expected = sourceColumnAsDouble("sum_unboundedpreceding_and_5preceding");
+    double[] expected = intSourceColumnAsDoubleArray("sum_unboundedpreceding_and_5preceding");
     double[] actual = result.doubleColumn("sum").asDoubleArray();
     assertArrayEquals(expected, actual);
 
-    expected = sourceColumnAsDouble("max_unboundedpreceding_and_5preceding");
+    expected = intSourceColumnAsDoubleArray("max_unboundedpreceding_and_5preceding");
     actual = result.doubleColumn("max").asDoubleArray();
     assertArrayEquals(expected, actual);
+
+    expected = intSourceColumnAsDoubleArray("min_unboundedpreceding_and_5preceding");
+    actual = result.doubleColumn("min").asDoubleArray();
+    assertArrayEquals(expected, actual);
+
+    expected = doubleSourceColumnAsDoubleArray("mean_unboundedpreceding_and_5preceding");
+    actual = result.doubleColumn("mean").asDoubleArray();
+    assertArrayEquals(expected, actual);
+
+    expected = intSourceColumnAsDoubleArray("count_unboundedpreceding_and_5preceding");
+    actual = result.intColumn("count").asDoubleArray();
+    assertArrayEquals(expected, actual);
   }
 
   @Test
   public void unboundedPrecedingAndCurrentRow() {
     AnalyticQuery query =
         AnalyticQuery.query()
-            .from(source)
+            .from(referenceImplementation)
             .partitionBy("who")
             .orderBy("date")
             .rowsBetween()
@@ -144,25 +169,43 @@ class AnalyticQueryEngineTest {
             .as("sum")
             .max("approval")
             .as("max")
+            .min("approval")
+            .as("min")
+            .mean("approval")
+            .as("mean")
+            .count("approval")
+            .as("count")
             .build();
 
     AnalyticQueryEngine queryEngine = AnalyticQueryEngine.create(query);
     Table result = queryEngine.execute();
 
-    double[] expected = sourceColumnAsDouble("sum_unboundedpreceding_and_currentrow");
+    double[] expected = intSourceColumnAsDoubleArray("sum_unboundedpreceding_and_currentrow");
     double[] actual = result.doubleColumn("sum").asDoubleArray();
     assertArrayEquals(expected, actual);
 
-    expected = sourceColumnAsDouble("max_unboundedpreceding_and_currentrow");
+    expected = intSourceColumnAsDoubleArray("max_unboundedpreceding_and_currentrow");
     actual = result.doubleColumn("max").asDoubleArray();
     assertArrayEquals(expected, actual);
+
+    expected = intSourceColumnAsDoubleArray("min_unboundedpreceding_and_currentrow");
+    actual = result.doubleColumn("min").asDoubleArray();
+    assertArrayEquals(expected, actual);
+
+    expected = doubleSourceColumnAsDoubleArray("mean_unboundedpreceding_and_currentrow");
+    actual = result.doubleColumn("mean").asDoubleArray();
+    assertArrayEquals(expected, actual);
+
+    expected = intSourceColumnAsDoubleArray("count_unboundedpreceding_and_currentrow");
+    actual = result.intColumn("count").asDoubleArray();
+    assertArrayEquals(expected, actual);
   }
 
   @Test
   public void unboundedPrecedingAnd5Following() {
     AnalyticQuery query =
         AnalyticQuery.query()
-            .from(source)
+            .from(referenceImplementation)
             .partitionBy("who")
             .orderBy("date")
             .rowsBetween()
@@ -172,25 +215,43 @@ class AnalyticQueryEngineTest {
             .as("sum")
             .max("approval")
             .as("max")
+            .min("approval")
+            .as("min")
+            .mean("approval")
+            .as("mean")
+            .count("approval")
+            .as("count")
             .build();
 
     AnalyticQueryEngine queryEngine = AnalyticQueryEngine.create(query);
     Table result = queryEngine.execute();
 
-    double[] expected = sourceColumnAsDouble("sum_unboundedpreceding_and_5following");
+    double[] expected = intSourceColumnAsDoubleArray("sum_unboundedpreceding_and_5following");
     double[] actual = result.doubleColumn("sum").asDoubleArray();
     assertArrayEquals(expected, actual);
 
-    expected = sourceColumnAsDouble("max_unboundedpreceding_and_5following");
+    expected = intSourceColumnAsDoubleArray("max_unboundedpreceding_and_5following");
     actual = result.doubleColumn("max").asDoubleArray();
     assertArrayEquals(expected, actual);
+
+    expected = intSourceColumnAsDoubleArray("min_unboundedpreceding_and_5following");
+    actual = result.doubleColumn("min").asDoubleArray();
+    assertArrayEquals(expected, actual);
+
+    expected = doubleSourceColumnAsDoubleArray("mean_unboundedpreceding_and_5following");
+    actual = result.doubleColumn("mean").asDoubleArray();
+    assertArrayEquals(expected, actual);
+
+    expected = intSourceColumnAsDoubleArray("count_unboundedpreceding_and_5following");
+    actual = result.intColumn("count").asDoubleArray();
+    assertArrayEquals(expected, actual);
   }
 
   @Test
   public void unboundedPrecedingAndUnboundedFollowing() {
     AnalyticQuery query =
         AnalyticQuery.query()
-            .from(source)
+            .from(referenceImplementation)
             .partitionBy("who")
             .orderBy("date")
             .rowsBetween()
@@ -200,25 +261,44 @@ class AnalyticQueryEngineTest {
             .as("sum")
             .max("approval")
             .as("max")
+            .min("approval")
+            .as("min")
+            .mean("approval")
+            .as("mean")
+            .count("approval")
+            .as("count")
             .build();
 
     AnalyticQueryEngine queryEngine = AnalyticQueryEngine.create(query);
     Table result = queryEngine.execute();
 
-    double[] expected = sourceColumnAsDouble("sum_unboundedpreceding_and_unboundedfollowing");
+    double[] expected =
+        intSourceColumnAsDoubleArray("sum_unboundedpreceding_and_unboundedfollowing");
     double[] actual = result.doubleColumn("sum").asDoubleArray();
     assertArrayEquals(expected, actual);
 
-    expected = sourceColumnAsDouble("max_unboundedpreceding_and_unboundedfollowing");
+    expected = intSourceColumnAsDoubleArray("max_unboundedpreceding_and_unboundedfollowing");
     actual = result.doubleColumn("max").asDoubleArray();
     assertArrayEquals(expected, actual);
+
+    expected = intSourceColumnAsDoubleArray("min_unboundedpreceding_and_unboundedfollowing");
+    actual = result.doubleColumn("min").asDoubleArray();
+    assertArrayEquals(expected, actual);
+
+    expected = doubleSourceColumnAsDoubleArray("mean_unboundedpreceding_and_unboundedfollowing");
+    actual = result.doubleColumn("mean").asDoubleArray();
+    assertArrayEquals(expected, actual);
+
+    expected = intSourceColumnAsDoubleArray("count_unboundedpreceding_and_unboundedfollowing");
+    actual = result.intColumn("count").asDoubleArray();
+    assertArrayEquals(expected, actual);
   }
 
   @Test
   public void fivePrecedingAnd3Preceding() {
     AnalyticQuery query =
         AnalyticQuery.query()
-            .from(source)
+            .from(referenceImplementation)
             .partitionBy("who")
             .orderBy("date")
             .rowsBetween()
@@ -228,25 +308,43 @@ class AnalyticQueryEngineTest {
             .as("sum")
             .max("approval")
             .as("max")
+            .min("approval")
+            .as("min")
+            .mean("approval")
+            .as("mean")
+            .count("approval")
+            .as("count")
             .build();
 
     AnalyticQueryEngine queryEngine = AnalyticQueryEngine.create(query);
     Table result = queryEngine.execute();
 
-    double[] expected = sourceColumnAsDouble("sum_5preceding_and_3preceding");
+    double[] expected = intSourceColumnAsDoubleArray("sum_5preceding_and_3preceding");
     double[] actual = result.doubleColumn("sum").asDoubleArray();
     assertArrayEquals(expected, actual);
 
-    expected = sourceColumnAsDouble("max_5preceding_and_3preceding");
+    expected = intSourceColumnAsDoubleArray("max_5preceding_and_3preceding");
     actual = result.doubleColumn("max").asDoubleArray();
     assertArrayEquals(expected, actual);
+
+    expected = intSourceColumnAsDoubleArray("min_5preceding_and_3preceding");
+    actual = result.doubleColumn("min").asDoubleArray();
+    assertArrayEquals(expected, actual);
+
+    expected = doubleSourceColumnAsDoubleArray("mean_5preceding_and_3preceding");
+    actual = result.doubleColumn("mean").asDoubleArray();
+    assertArrayEquals(expected, actual);
+
+    expected = intSourceColumnAsDoubleArray("count_5preceding_and_3preceding");
+    actual = result.intColumn("count").asDoubleArray();
+    assertArrayEquals(expected, actual);
   }
 
   @Test
   public void fivePrecedingAndCurrentRow() {
     AnalyticQuery query =
         AnalyticQuery.query()
-            .from(source)
+            .from(referenceImplementation)
             .partitionBy("who")
             .orderBy("date")
             .rowsBetween()
@@ -256,25 +354,43 @@ class AnalyticQueryEngineTest {
             .as("sum")
             .max("approval")
             .as("max")
+            .min("approval")
+            .as("min")
+            .mean("approval")
+            .as("mean")
+            .count("approval")
+            .as("count")
             .build();
 
     AnalyticQueryEngine queryEngine = AnalyticQueryEngine.create(query);
     Table result = queryEngine.execute();
 
-    double[] expected = sourceColumnAsDouble("sum_5preceding_and_currentrow");
+    double[] expected = intSourceColumnAsDoubleArray("sum_5preceding_and_currentrow");
     double[] actual = result.doubleColumn("sum").asDoubleArray();
     assertArrayEquals(expected, actual);
 
-    expected = sourceColumnAsDouble("max_5preceding_and_currentrow");
+    expected = intSourceColumnAsDoubleArray("max_5preceding_and_currentrow");
     actual = result.doubleColumn("max").asDoubleArray();
     assertArrayEquals(expected, actual);
+
+    expected = intSourceColumnAsDoubleArray("min_5preceding_and_currentrow");
+    actual = result.doubleColumn("min").asDoubleArray();
+    assertArrayEquals(expected, actual);
+
+    expected = doubleSourceColumnAsDoubleArray("mean_5preceding_and_currentrow");
+    actual = result.doubleColumn("mean").asDoubleArray();
+    assertArrayEquals(expected, actual);
+
+    expected = intSourceColumnAsDoubleArray("count_5preceding_and_currentrow");
+    actual = result.intColumn("count").asDoubleArray();
+    assertArrayEquals(expected, actual);
   }
 
   @Test
   public void fivePrecedingAnd5Following() {
     AnalyticQuery query =
         AnalyticQuery.query()
-            .from(source)
+            .from(referenceImplementation)
             .partitionBy("who")
             .orderBy("date")
             .rowsBetween()
@@ -284,25 +400,43 @@ class AnalyticQueryEngineTest {
             .as("sum")
             .max("approval")
             .as("max")
+            .min("approval")
+            .as("min")
+            .mean("approval")
+            .as("mean")
+            .count("approval")
+            .as("count")
             .build();
 
     AnalyticQueryEngine queryEngine = AnalyticQueryEngine.create(query);
     Table result = queryEngine.execute();
 
-    double[] expected = sourceColumnAsDouble("sum_5preceding_and_5following");
+    double[] expected = intSourceColumnAsDoubleArray("sum_5preceding_and_5following");
     double[] actual = result.doubleColumn("sum").asDoubleArray();
     assertArrayEquals(expected, actual);
 
-    expected = sourceColumnAsDouble("max_5preceding_and_5following");
+    expected = intSourceColumnAsDoubleArray("max_5preceding_and_5following");
     actual = result.doubleColumn("max").asDoubleArray();
     assertArrayEquals(expected, actual);
+
+    expected = intSourceColumnAsDoubleArray("min_5preceding_and_5following");
+    actual = result.doubleColumn("min").asDoubleArray();
+    assertArrayEquals(expected, actual);
+
+    expected = doubleSourceColumnAsDoubleArray("mean_5preceding_and_5following");
+    actual = result.doubleColumn("mean").asDoubleArray();
+    assertArrayEquals(expected, actual);
+
+    expected = intSourceColumnAsDoubleArray("count_5preceding_and_5following");
+    actual = result.intColumn("count").asDoubleArray();
+    assertArrayEquals(expected, actual);
   }
 
   @Test
   public void fivePrecedingAndUnboundedFollowing() {
     AnalyticQuery query =
         AnalyticQuery.query()
-            .from(source)
+            .from(referenceImplementation)
             .partitionBy("who")
             .orderBy("date")
             .rowsBetween()
@@ -312,25 +446,43 @@ class AnalyticQueryEngineTest {
             .as("sum")
             .max("approval")
             .as("max")
+            .min("approval")
+            .as("min")
+            .mean("approval")
+            .as("mean")
+            .count("approval")
+            .as("count")
             .build();
 
     AnalyticQueryEngine queryEngine = AnalyticQueryEngine.create(query);
     Table result = queryEngine.execute();
 
-    double[] expected = sourceColumnAsDouble("sum_5preceding_and_unboundedfollowing");
+    double[] expected = intSourceColumnAsDoubleArray("sum_5preceding_and_unboundedfollowing");
     double[] actual = result.doubleColumn("sum").asDoubleArray();
     assertArrayEquals(expected, actual);
 
-    expected = sourceColumnAsDouble("max_5preceding_and_unboundedfollowing");
+    expected = intSourceColumnAsDoubleArray("max_5preceding_and_unboundedfollowing");
     actual = result.doubleColumn("max").asDoubleArray();
     assertArrayEquals(expected, actual);
+
+    expected = intSourceColumnAsDoubleArray("min_5preceding_and_unboundedfollowing");
+    actual = result.doubleColumn("min").asDoubleArray();
+    assertArrayEquals(expected, actual);
+
+    expected = doubleSourceColumnAsDoubleArray("mean_5preceding_and_unboundedfollowing");
+    actual = result.doubleColumn("mean").asDoubleArray();
+    assertArrayEquals(expected, actual);
+
+    expected = intSourceColumnAsDoubleArray("count_5preceding_and_unboundedfollowing");
+    actual = result.intColumn("count").asDoubleArray();
+    assertArrayEquals(expected, actual);
   }
 
   @Test
   public void currentRowAndUnboundedFollowing() {
     AnalyticQuery query =
         AnalyticQuery.query()
-            .from(source)
+            .from(referenceImplementation)
             .partitionBy("who")
             .orderBy("date")
             .rowsBetween()
@@ -340,25 +492,43 @@ class AnalyticQueryEngineTest {
             .as("sum")
             .max("approval")
             .as("max")
+            .min("approval")
+            .as("min")
+            .mean("approval")
+            .as("mean")
+            .count("approval")
+            .as("count")
             .build();
 
     AnalyticQueryEngine queryEngine = AnalyticQueryEngine.create(query);
     Table result = queryEngine.execute();
 
-    double[] expected = sourceColumnAsDouble("sum_currentrow_and_unboundedfollowing");
+    double[] expected = intSourceColumnAsDoubleArray("sum_currentrow_and_unboundedfollowing");
     double[] actual = result.doubleColumn("sum").asDoubleArray();
     assertArrayEquals(expected, actual);
 
-    expected = sourceColumnAsDouble("max_currentrow_and_unboundedfollowing");
+    expected = intSourceColumnAsDoubleArray("max_currentrow_and_unboundedfollowing");
     actual = result.doubleColumn("max").asDoubleArray();
     assertArrayEquals(expected, actual);
+
+    expected = intSourceColumnAsDoubleArray("min_currentrow_and_unboundedfollowing");
+    actual = result.doubleColumn("min").asDoubleArray();
+    assertArrayEquals(expected, actual);
+
+    expected = doubleSourceColumnAsDoubleArray("mean_currentrow_and_unboundedfollowing");
+    actual = result.doubleColumn("mean").asDoubleArray();
+    assertArrayEquals(expected, actual);
+
+    expected = intSourceColumnAsDoubleArray("count_currentrow_and_unboundedfollowing");
+    actual = result.intColumn("count").asDoubleArray();
+    assertArrayEquals(expected, actual);
   }
 
   @Test
   public void fiveFollowingAnd8Following() {
     AnalyticQuery query =
         AnalyticQuery.query()
-            .from(source)
+            .from(referenceImplementation)
             .partitionBy("who")
             .orderBy("date")
             .rowsBetween()
@@ -368,18 +538,35 @@ class AnalyticQueryEngineTest {
             .as("sum")
             .max("approval")
             .as("max")
+            .min("approval")
+            .as("min")
+            .mean("approval")
+            .as("mean")
+            .count("approval")
+            .as("count")
             .build();
 
     AnalyticQueryEngine queryEngine = AnalyticQueryEngine.create(query);
     Table result = queryEngine.execute();
 
-    double[] expected = sourceColumnAsDouble("sum_5following_and_8following");
+    double[] expected = intSourceColumnAsDoubleArray("sum_5following_and_8following");
     double[] actual = result.doubleColumn("sum").asDoubleArray();
     assertArrayEquals(expected, actual);
 
-    expected = sourceColumnAsDouble("max_5following_and_8following");
+    expected = intSourceColumnAsDoubleArray("max_5following_and_8following");
     actual = result.doubleColumn("max").asDoubleArray();
+    assertArrayEquals(expected, actual);
+
+    expected = intSourceColumnAsDoubleArray("min_5following_and_8following");
+    actual = result.doubleColumn("min").asDoubleArray();
+    assertArrayEquals(expected, actual);
+
+    expected = doubleSourceColumnAsDoubleArray("mean_5following_and_8following");
+    actual = result.doubleColumn("mean").asDoubleArray();
+    assertArrayEquals(expected, actual);
 
+    expected = intSourceColumnAsDoubleArray("count_5following_and_8following");
+    actual = result.intColumn("count").asDoubleArray();
     assertArrayEquals(expected, actual);
   }
 
@@ -387,7 +574,7 @@ class AnalyticQueryEngineTest {
   public void fiveFollowingAndUnboundedFollowing() {
     AnalyticQuery query =
         AnalyticQuery.query()
-            .from(source)
+            .from(referenceImplementation)
             .partitionBy("who")
             .orderBy("date")
             .rowsBetween()
@@ -397,25 +584,66 @@ class AnalyticQueryEngineTest {
             .as("sum")
             .max("approval")
             .as("max")
+            .min("approval")
+            .as("min")
+            .mean("approval")
+            .as("mean")
+            .count("approval")
+            .as("count")
             .build();
 
     AnalyticQueryEngine queryEngine = AnalyticQueryEngine.create(query);
     Table result = queryEngine.execute();
 
-    double[] expected = sourceColumnAsDouble("sum_5following_and_unboundedfollowing");
+    double[] expected = intSourceColumnAsDoubleArray("sum_5following_and_unboundedfollowing");
     double[] actual = result.doubleColumn("sum").asDoubleArray();
     assertArrayEquals(expected, actual);
 
-    expected = sourceColumnAsDouble("max_5following_and_unboundedfollowing");
+    expected = intSourceColumnAsDoubleArray("max_5following_and_unboundedfollowing");
     actual = result.doubleColumn("max").asDoubleArray();
     assertArrayEquals(expected, actual);
+
+    expected = intSourceColumnAsDoubleArray("min_5following_and_unboundedfollowing");
+    actual = result.doubleColumn("min").asDoubleArray();
+    assertArrayEquals(expected, actual);
+
+    expected = doubleSourceColumnAsDoubleArray("mean_5following_and_unboundedfollowing");
+    actual = result.doubleColumn("mean").asDoubleArray();
+    assertArrayEquals(expected, actual);
+
+    expected = intSourceColumnAsDoubleArray("count_5following_and_unboundedfollowing");
+    actual = result.intColumn("count").asDoubleArray();
+    assertArrayEquals(expected, actual);
+  }
+
+  @Test
+  public void countWithStrings() {
+    Table table =
+        Table.create(
+            "table",
+            StringColumn.create("col1", new String[] {"A", "B", null, "C", "C", "C", "D"}));
+
+    AnalyticQuery query =
+        AnalyticQuery.quickQuery()
+            .from(table)
+            .rowsBetween()
+            .unboundedPreceding()
+            .andCurrentRow()
+            .count("col1")
+            .as("count")
+            .build();
+
+    AnalyticQueryEngine queryEngine = AnalyticQueryEngine.create(query);
+    Table result = queryEngine.execute();
+
+    assertEquals(ImmutableList.of(1, 2, 2, 3, 4, 5, 6), result.intColumn("count").asList());
   }
 
   @Test
   public void numberingFunctionReferenceImplementation() {
     AnalyticQuery query =
         AnalyticQuery.numberingQuery()
-            .from(source)
+            .from(referenceImplementation)
             .partitionBy("who")
             .orderBy("date")
             .rowNumber()
@@ -430,10 +658,11 @@ class AnalyticQueryEngineTest {
     Table result = queryEngine.execute();
 
     assertArrayEquals(
-        sourceColumnAsDouble("row_number"), result.intColumn("rowNumber").asDoubleArray());
-    assertArrayEquals(sourceColumnAsDouble("rank"), result.intColumn("rank").asDoubleArray());
+        intSourceColumnAsDoubleArray("row_number"), result.intColumn("rowNumber").asDoubleArray());
+    assertArrayEquals(
+        intSourceColumnAsDoubleArray("rank"), result.intColumn("rank").asDoubleArray());
     assertArrayEquals(
-        sourceColumnAsDouble("dense_rank"), result.intColumn("denseRank").asDoubleArray());
+        intSourceColumnAsDoubleArray("dense_rank"), result.intColumn("denseRank").asDoubleArray());
   }
 
   @Test
@@ -467,4 +696,27 @@ class AnalyticQueryEngineTest {
         new double[] {1.0, 2.0, 2.0, 3.0, 3.0, 3.0, 4.0},
         result.intColumn("denseRank").asDoubleArray());
   }
+
+  @Test
+  public void resultColumnOrderSameAsSpecifiedInQuery() {
+    Table table = Table.create("table", StringColumn.create("col1", new String[] {}));
+
+    AnalyticQuery query =
+        AnalyticQuery.numberingQuery()
+            .from(table)
+            .partitionBy()
+            .orderBy("col1")
+            .rowNumber()
+            .as("rowNumber")
+            .rank()
+            .as("rank")
+            .denseRank()
+            .as("denseRank")
+            .build();
+
+    AnalyticQueryEngine queryEngine = AnalyticQueryEngine.create(query);
+    Table result = queryEngine.execute();
+
+    assertEquals(ImmutableList.of("rowNumber", "rank", "denseRank"), result.columnNames());
+  }
 }
diff --git a/core/src/test/java/tech/tablesaw/analytic/AnalyticQueryTest.java b/core/src/test/java/tech/tablesaw/analytic/AnalyticQueryTest.java
index 947debfb..993f4408 100644
--- a/core/src/test/java/tech/tablesaw/analytic/AnalyticQueryTest.java
+++ b/core/src/test/java/tech/tablesaw/analytic/AnalyticQueryTest.java
@@ -1,11 +1,16 @@
 package tech.tablesaw.analytic;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import com.google.common.collect.ImmutableList;
 import java.util.function.Consumer;
 import org.junit.jupiter.api.Test;
+import tech.tablesaw.api.DoubleColumn;
 import tech.tablesaw.api.IntColumn;
 import tech.tablesaw.api.Row;
+import tech.tablesaw.api.StringColumn;
 import tech.tablesaw.api.Table;
 
 class AnalyticQueryTest {
@@ -43,7 +48,7 @@ class AnalyticQueryTest {
             + System.lineSeparator()
             + "ROWS BETWEEN UNBOUNDED_PRECEDING AND UNBOUNDED_FOLLOWING);";
 
-    assertEquals(expected, query.toSqlString());
+    assertEquals(expected, query.toSqlLikeString());
   }
 
   @Test
@@ -65,16 +70,16 @@ class AnalyticQueryTest {
             + "Window w1 AS (\n"
             + "ROWS BETWEEN CURRENT_ROW AND 1 FOLLOWING);";
 
-    assertEquals(expectd, query.toSqlString());
+    assertEquals(expectd, query.toSqlLikeString());
   }
 
   @Test
   public void toSqlStringNumbering() {
     AnalyticQuery query =
         AnalyticQuery.numberingQuery()
-            .from(Table.create("myTable", IntColumn.create("date")))
+            .from(Table.create("myTable", IntColumn.create("date"), IntColumn.create("region")))
             .partitionBy()
-            .orderBy("date")
+            .orderBy("date", "region")
             .rank()
             .as("myRank")
             .build();
@@ -84,8 +89,69 @@ class AnalyticQueryTest {
             + "RANK() OVER w1 AS myRank\n"
             + "FROM myTable\n"
             + "Window w1 AS (\n"
-            + "ORDER BY date ASC);";
+            + "ORDER BY date ASC, region ASC);";
 
-    assertEquals(expectd, query.toSqlString());
+    assertEquals(expectd, query.toSqlLikeString());
+  }
+
+  @Test
+  public void executeInPlaceNumbering() {
+    Table table = Table.create("table", StringColumn.create("col1", new String[] {}));
+
+    AnalyticQuery.numberingQuery()
+        .from(table)
+        .partitionBy()
+        .orderBy("col1")
+        .rowNumber()
+        .as("rowNumber")
+        .rank()
+        .as("rank")
+        .denseRank()
+        .as("denseRank")
+        .executeInPlace();
+
+    assertEquals(ImmutableList.of("col1", "rowNumber", "rank", "denseRank"), table.columnNames());
+  }
+
+  @Test
+  public void executeInPlaceAnalytic() {
+    Table table = Table.create("table", DoubleColumn.create("col1", new Double[] {}));
+
+    AnalyticQuery.query()
+        .from(table)
+        .partitionBy()
+        .orderBy("col1")
+        .rowsBetween()
+        .unboundedPreceding()
+        .andUnBoundedFollowing()
+        .sum("col1")
+        .as("sum")
+        .max("col1")
+        .as("max")
+        .executeInPlace();
+
+    assertEquals(ImmutableList.of("col1", "sum", "max"), table.columnNames());
+  }
+
+  @Test
+  public void executeInPlaceWithDuplicateColumnsThrows() {
+    Table table = Table.create("myTable", DoubleColumn.create("col1", new Double[] {}));
+
+    Throwable thrown =
+        assertThrows(
+            IllegalArgumentException.class,
+            () ->
+                AnalyticQuery.query()
+                    .from(table)
+                    .partitionBy()
+                    .orderBy("col1")
+                    .rowsBetween()
+                    .unboundedPreceding()
+                    .andUnBoundedFollowing()
+                    .sum("col1")
+                    .as("col1")
+                    .executeInPlace());
+
+    assertTrue(thrown.getMessage().contains("Cannot add column with duplicate name"));
   }
 }
diff --git a/core/src/test/java/tech/tablesaw/analytic/ArgumentListTest.java b/core/src/test/java/tech/tablesaw/analytic/ArgumentListTest.java
index eedabbab..20c9e2bc 100644
--- a/core/src/test/java/tech/tablesaw/analytic/ArgumentListTest.java
+++ b/core/src/test/java/tech/tablesaw/analytic/ArgumentListTest.java
@@ -110,7 +110,7 @@ class ArgumentListTest {
   public void buildWithStagedThrows() {
     Throwable thrown =
         assertThrows(
-            IllegalArgumentException.class,
+            IllegalStateException.class,
             () -> ArgumentList.builder().stageFunction("col1", AggregateFunctions.MAX).build());
 
     assertTrue(thrown.getMessage().contains("Cannot build when a column is staged"));
diff --git a/core/src/test/java/tech/tablesaw/analytic/WindowFrameTest.java b/core/src/test/java/tech/tablesaw/analytic/WindowFrameTest.java
index 95509ee6..c41d4a69 100644
--- a/core/src/test/java/tech/tablesaw/analytic/WindowFrameTest.java
+++ b/core/src/test/java/tech/tablesaw/analytic/WindowFrameTest.java
@@ -16,45 +16,45 @@ class WindowFrameTest {
 
     String expectedString = "ROWS BETWEEN UNBOUNDED_PRECEDING AND UNBOUNDED_FOLLOWING";
 
-    assertEquals(WindowBoundTypes.UNBOUNDED_PRECEDING, frame.getFrameStart());
-    assertEquals(WindowBoundTypes.UNBOUNDED_FOLLOWING, frame.getFrameEnd());
+    assertEquals(WindowBoundTypes.UNBOUNDED_PRECEDING, frame.getLeftBoundType());
+    assertEquals(WindowBoundTypes.UNBOUNDED_FOLLOWING, frame.getRightBoundType());
     assertEquals(expectedString, frame.toSqlString());
   }
 
   @Test
   public void testPreceding() {
-    WindowFrame frame = WindowFrame.builder().setStartPreceding(5).setEndPreceding(2).build();
+    WindowFrame frame = WindowFrame.builder().setLeftPreceding(5).setRightPreceding(2).build();
     String expectedString = "ROWS BETWEEN 5 PRECEDING AND 2 PRECEDING";
 
-    assertEquals(WindowBoundTypes.PRECEDING, frame.getFrameStart());
-    assertEquals(-5, frame.getFrameStartShift());
-    assertEquals(WindowBoundTypes.PRECEDING, frame.getFrameEnd());
-    assertEquals(-2, frame.getFrameEndShift());
+    assertEquals(WindowBoundTypes.PRECEDING, frame.getLeftBoundType());
+    assertEquals(-5, frame.getInitialLeftBound());
+    assertEquals(WindowBoundTypes.PRECEDING, frame.getRightBoundType());
+    assertEquals(-2, frame.getInitialRightBound());
     assertEquals(expectedString, frame.toSqlString());
   }
 
   @Test
   public void testCurrentRowToUnbounded() {
-    WindowFrame frame = WindowFrame.builder().setStartCurrentRow().build();
+    WindowFrame frame = WindowFrame.builder().setLeftCurrentRow().build();
 
     String expectedString = "ROWS BETWEEN CURRENT_ROW AND UNBOUNDED_FOLLOWING";
 
-    assertEquals(WindowBoundTypes.CURRENT_ROW, frame.getFrameStart());
-    assertEquals(0, frame.getFrameStartShift());
-    assertEquals(WindowBoundTypes.UNBOUNDED_FOLLOWING, frame.getFrameEnd());
-    assertEquals(0, frame.getFrameEndShift());
+    assertEquals(WindowBoundTypes.CURRENT_ROW, frame.getLeftBoundType());
+    assertEquals(0, frame.getInitialLeftBound());
+    assertEquals(WindowBoundTypes.UNBOUNDED_FOLLOWING, frame.getRightBoundType());
+    assertEquals(0, frame.getInitialRightBound());
     assertEquals(expectedString, frame.toSqlString());
   }
 
   @Test
   public void testFollowing() {
-    WindowFrame frame = WindowFrame.builder().setStartFollowing(2).setEndFollowing(5).build();
+    WindowFrame frame = WindowFrame.builder().setLeftFollowing(2).setRightFollowing(5).build();
     String expectedString = "ROWS BETWEEN 2 FOLLOWING AND 5 FOLLOWING";
 
-    assertEquals(WindowBoundTypes.FOLLOWING, frame.getFrameStart());
-    assertEquals(2, frame.getFrameStartShift());
-    assertEquals(WindowBoundTypes.FOLLOWING, frame.getFrameEnd());
-    assertEquals(5, frame.getFrameEndShift());
+    assertEquals(WindowBoundTypes.FOLLOWING, frame.getLeftBoundType());
+    assertEquals(2, frame.getInitialLeftBound());
+    assertEquals(WindowBoundTypes.FOLLOWING, frame.getRightBoundType());
+    assertEquals(5, frame.getInitialRightBound());
     assertEquals(expectedString, frame.toSqlString());
   }
 
@@ -63,7 +63,7 @@ class WindowFrameTest {
     Throwable thrown =
         assertThrows(
             IllegalArgumentException.class,
-            () -> WindowFrame.builder().setStartFollowing(10).setEndPreceding(10).build());
+            () -> WindowFrame.builder().setLeftFollowing(10).setRightPreceding(10).build());
 
     assertTrue(thrown.getMessage().contains("FOLLOWING cannot come before PRECEDING"));
   }
@@ -73,7 +73,7 @@ class WindowFrameTest {
     Throwable thrown =
         assertThrows(
             IllegalArgumentException.class,
-            () -> WindowFrame.builder().setStartFollowing(10).setEnndCurrentRow().build());
+            () -> WindowFrame.builder().setLeftFollowing(10).setRightCurrentRow().build());
 
     assertTrue(thrown.getMessage().contains("FOLLOWING cannot come before CURRENT_ROW"));
   }
@@ -83,7 +83,7 @@ class WindowFrameTest {
     Throwable thrown =
         assertThrows(
             IllegalArgumentException.class,
-            () -> WindowFrame.builder().setStartPreceding(5).setEndPreceding(10).build());
+            () -> WindowFrame.builder().setLeftPreceding(5).setRightPreceding(10).build());
     assertTrue(
         thrown
             .getMessage()
@@ -95,7 +95,7 @@ class WindowFrameTest {
     Throwable thrown =
         assertThrows(
             IllegalArgumentException.class,
-            () -> WindowFrame.builder().setStartFollowing(10).setEndFollowing(5).build());
+            () -> WindowFrame.builder().setLeftFollowing(10).setRightFollowing(5).build());
     assertTrue(
         thrown
             .getMessage()
@@ -106,7 +106,7 @@ class WindowFrameTest {
   public void rightShiftEqualsThanLeftShift() {
     assertThrows(
         IllegalArgumentException.class,
-        () -> WindowFrame.builder().setStartPreceding(5).setEndPreceding(5).build());
+        () -> WindowFrame.builder().setLeftPreceding(5).setRightPreceding(5).build());
   }
 
   @Test
@@ -118,28 +118,28 @@ class WindowFrameTest {
   @Test
   public void windowGrowthTypeFixedStart() {
     WindowGrowthType growthType =
-        WindowFrame.builder().setEndFollowing(10).build().windowGrowthType();
-    assertEquals(growthType, WindowGrowthType.FIXED_START);
+        WindowFrame.builder().setRightFollowing(10).build().windowGrowthType();
+    assertEquals(growthType, WindowGrowthType.FIXED_LEFT);
   }
 
   @Test
   public void windowGrothTypeFixedEnd() {
     WindowGrowthType growthType =
-        WindowFrame.builder().setStartFollowing(10).build().windowGrowthType();
-    assertEquals(growthType, WindowGrowthType.FIXED_END);
+        WindowFrame.builder().setLeftFollowing(10).build().windowGrowthType();
+    assertEquals(growthType, WindowGrowthType.FIXED_RIGHT);
   }
 
   @Test
   public void windowGrowthTypeSliding() {
     WindowGrowthType growthType =
-        WindowFrame.builder().setStartPreceding(5).setEndFollowing(5).build().windowGrowthType();
+        WindowFrame.builder().setLeftPreceding(5).setRightFollowing(5).build().windowGrowthType();
     assertEquals(growthType, WindowGrowthType.SLIDING);
   }
 
   @Test
   public void windowGrowthTypeSlidingWithCurrentRow() {
     WindowGrowthType growthType =
-        WindowFrame.builder().setStartPreceding(5).setEnndCurrentRow().build().windowGrowthType();
+        WindowFrame.builder().setLeftPreceding(5).setRightCurrentRow().build().windowGrowthType();
     assertEquals(growthType, WindowGrowthType.SLIDING);
   }
 }
diff --git a/core/src/test/java/tech/tablesaw/api/RowTest.java b/core/src/test/java/tech/tablesaw/api/RowTest.java
index ac8e4197..c3c4a9a3 100644
--- a/core/src/test/java/tech/tablesaw/api/RowTest.java
+++ b/core/src/test/java/tech/tablesaw/api/RowTest.java
@@ -2,6 +2,9 @@ package tech.tablesaw.api;
 
 import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static tech.tablesaw.api.ColumnType.BOOLEAN;
 import static tech.tablesaw.api.ColumnType.DOUBLE;
 import static tech.tablesaw.api.ColumnType.FLOAT;
@@ -627,4 +630,42 @@ public class RowTest {
 
     assertArrayEquals(expected, actual);
   }
+
+  @Test
+  public void columnDoesNotExistOnRow() {
+    Table table = Table.create("myTable", IntColumn.create("col1", new int[] {1}));
+
+    Throwable thrown =
+        assertThrows(IllegalStateException.class, () -> table.forEach(r -> r.getInt("col2")));
+
+    assertEquals("Column col2 is not present in table myTable", thrown.getMessage());
+  }
+
+  @Test
+  public void columnExistsButWrongType() {
+    Table table = Table.create("myTale", DateColumn.create("col1", new LocalDate[] {null}));
+
+    Throwable thrown =
+        assertThrows(
+            IllegalArgumentException.class, () -> table.forEach(r -> r.setTime("col1", null)));
+
+    assertEquals(
+        "Column col1 is of type LOCAL_DATE and cannot be cast to LOCAL_TIME."
+            + " Use the method for LOCAL_DATE.",
+        thrown.getMessage());
+  }
+
+  @Test
+  void testGetNumber() throws IOException {
+    Table table = Table.read().csv("../data/bush.csv");
+    assertEquals(53.0, table.row(0).getNumber("approval"));
+  }
+
+  @Test
+  void testIsMissing() throws IOException {
+    Table table = Table.read().csv("../data/bush.csv");
+    assertFalse(table.row(0).isMissing("approval"));
+    table.row(0).setMissing("approval");
+    assertTrue(table.row(0).isMissing("approval"));
+  }
 }
diff --git a/core/src/test/java/tech/tablesaw/io/ColumnTypeDetectorTest.java b/core/src/test/java/tech/tablesaw/io/ColumnTypeDetectorTest.java
new file mode 100644
index 00000000..fabff033
--- /dev/null
+++ b/core/src/test/java/tech/tablesaw/io/ColumnTypeDetectorTest.java
@@ -0,0 +1,54 @@
+package tech.tablesaw.io;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static tech.tablesaw.api.ColumnType.BOOLEAN;
+import static tech.tablesaw.api.ColumnType.DOUBLE;
+import static tech.tablesaw.api.ColumnType.FLOAT;
+import static tech.tablesaw.api.ColumnType.INTEGER;
+import static tech.tablesaw.api.ColumnType.LOCAL_DATE;
+import static tech.tablesaw.api.ColumnType.LOCAL_DATE_TIME;
+import static tech.tablesaw.api.ColumnType.LOCAL_TIME;
+import static tech.tablesaw.api.ColumnType.LONG;
+import static tech.tablesaw.api.ColumnType.SHORT;
+import static tech.tablesaw.api.ColumnType.STRING;
+import static tech.tablesaw.api.ColumnType.TEXT;
+
+import com.google.common.collect.Lists;
+import java.util.ArrayList;
+import org.junit.jupiter.api.Test;
+import tech.tablesaw.api.ColumnType;
+import tech.tablesaw.columns.dates.DateColumnType;
+import tech.tablesaw.columns.strings.StringColumnType;
+import tech.tablesaw.columns.strings.TextColumnType;
+
+class ColumnTypeDetectorTest {
+
+  @Test
+  void detectColumnTypes() {
+
+    String[][] val = {{"", "2010-05-03", "x"}, {"", "", ""}};
+
+    ArrayList<String[]> dates = Lists.newArrayList(val);
+
+    ColumnTypeDetector detector =
+        new ColumnTypeDetector(
+            Lists.newArrayList(
+                LOCAL_DATE_TIME,
+                LOCAL_TIME,
+                LOCAL_DATE,
+                BOOLEAN,
+                SHORT,
+                INTEGER,
+                LONG,
+                FLOAT,
+                DOUBLE,
+                STRING,
+                TEXT));
+
+    ColumnType[] types =
+        detector.detectColumnTypes(dates.iterator(), new ReadOptions.Builder().build());
+    assertEquals(TextColumnType.instance(), types[0]);
+    assertEquals(DateColumnType.instance(), types[1]);
+    assertEquals(StringColumnType.instance(), types[2]);
+  }
+}