docs and dataframe-listoflist structures, and use of new listoflist dispatched methods.
[CommonLispStat.git] / src / data / dataframe.lisp
blobee9407c266e56f58d866a83ff806f23bbaebf623
1 ;;; -*- mode: lisp -*-
3 ;;; Time-stamp: <2009-07-14 14:21:00 tony>
4 ;;; Creation: <2008-03-12 17:18:42 blindglobe@gmail.com>
5 ;;; File: dataframe.lisp
6 ;;; Author: AJ Rossini <blindglobe@gmail.com>
7 ;;; Copyright: (c)2008, AJ Rossini. BSD, LLGPL, or GPLv2, depending
8 ;;; on how it arrives.
10 ;;; Purpose: Data packaging and access for Common Lisp Statistics.
11 ;;; This redoes dataframe structures in a CLOS based
12 ;;; framework. Currently contains the virtual class
13 ;;; DATAFRAME-LIKE as well as the actual classes
14 ;;; DATAFRAME-ARRAY and DATAFRAME-MATRIXLIKE
16 ;;; What is this talk of 'release'? Klingons do not make software
17 ;;; 'releases'. Our software 'escapes', leaving a bloody trail of
18 ;;; designers and quality assurance people in its wake.
20 (in-package :cls-dataframe)
22 ;;; No real basis for work, there is a bit of new-ness and R-ness to
23 ;;; this work. In particular, the notion of relation is key and
24 ;;; integral to the analysis. Tables are related and matched vectors,
25 ;;; for example. "column" vectors are related observations (by
26 ;;; measure/recording) while "row" vectors are related readings (by
27 ;;; case, independence). This does mean that we are placing
28 ;;; statistical semantics into the computational data object -- and
29 ;;; that it is a violation of use to consider rows which are not at
30 ;;; the least conditionally independent (though the conditioning
31 ;;; should be outside the data set, not internally specified).
33 ;;; So we want a verb-driven API for data collection construction. We
34 ;;; should encode independence or lack of, as possible.
36 ;;; Need to figure out statistically-typed vectors. We then map a
37 ;;; series of typed vectors over to tables where columns are equal
38 ;;; typed. In a sense, this is a relation (1-1) of equal-typed
39 ;;; arrays. For the most part, this ends up making the R data.frame
40 ;;; into a relational building block (considering 1-1 mappings using
41 ;;; row ID as a relation). Is this a worthwhile generalization or
42 ;;; communicable analogy?
44 ;;; verbs vs semantics for DF construction -- consider the possibily
45 ;;; of how adverbs and verbs relate, where to put which semantically
46 ;;; to allow for general approach.
48 ;;; Need to consider modification APIs
49 ;;; actions are:
50 ;;; - import
51 ;;; - get/set row names (case names)
52 ;;; - column names (variable names)
53 ;;; - dataset values
54 ;;; - annotation/metadata
55 ;;; - make sure that we do coherency checking in the exported
56 ;;; - functions.
57 ;;; - ...
58 ;;; - reshapeData/reformat/reshapr a reformed version of the dataset (no
59 ;;; additional input).
60 ;;; - either overwriting or not, i.e. with or without copy.
61 ;;; - check consistency of resulting data with metadata and related
62 ;;; data information.
65 ;;; Misc Functions (to move into a lisp data manipulation support package)
67 ;; the next two should be merged into a general replicator pattern.
68 (defun gen-seq (n &optional (start 1))
69 "Generates an integer sequence of length N starting at START. Used
70 for indexing."
71 (if (>= n start)
72 (append (gen-seq (- n 1) start) (list n))))
74 (defun repeat-seq (n item)
75 "FIXME: There has to be a better way -- I'm sure of it!
76 (repeat-seq 3 \"d\") ; => (\"d\" \"d\" \"d\")
77 (repeat-seq 3 'd) ; => ('d 'd 'd)
78 (repeat-seq 3 (list 1 2))"
79 (if (>= n 1)
80 (append (repeat-seq (1- n) item) (list item))))
83 (defun strsym->indexnum (df strsym)
84 "Returns a number indicating the DF column labelled by STRSYM.
85 Probably should be a method dispatching on DATAFRAME-LIKE type."
86 (position strsym (varlabels df)))
88 (defun string->number (str)
89 "Convert a string <str> representing a number to a number. A second value is
90 returned indicating the success of the conversion.
91 Examples:
92 (string->number \"123\") ; => 123 t
93 (string->number \"1.23\") ; => 1.23 t"
94 (let ((*read-eval* nil))
95 (let ((num (read-from-string str)))
96 (values num (numberp num)))))
99 (equal 'testme 'testme)
100 (defparameter *test-pos* 'testme)
101 (position *test-pos* (list 'a 'b 'testme 'c))
102 (position #'(lambda (x) (equal x "testme")) (list "a" "b" "testme" "c"))
103 (position #'(lambda (x) (equal x 1)) (list 2 1 3 4))
106 ;;; abstract dataframe class
108 (defclass dataframe-like (matrix-like)
109 ((case-labels :initform nil
110 :initarg :case-labels
111 :type list
112 :accessor case-labels
113 :documentation "labels used for describing cases (doc
114 metadata), possibly used for merging.")
115 (var-labels :initform nil
116 :initarg :var-labels
117 :type list
118 :accessor var-labels
119 :documentation "Variable names.")
120 (var-types :initform nil
121 :initarg :var-types
122 :type list
123 :accessor var-types
124 :documentation "variable types to ensure fit")
125 (doc-string :initform nil
126 :initarg :doc
127 :accessor doc-string
128 :documentation "additional information, potentially
129 uncomputable, possibly metadata, about
130 dataframe-like instance."))
131 (:documentation "Abstract class for standard statistical analysis
132 dataset for independent data. Rows are considered
133 to be independent, matching observations. Columns
134 are considered to be type-consistent, match a
135 variable with distribution. inherits from
136 lisp-matrix base MATRIX-LIKE class.
137 MATRIX-LIKE (from lisp-matrix) is basically a
138 rectangular table without storage. We emulate
139 that, and add storage, row/column labels, and
140 within-column-typing.
142 DATAFRAME-LIKE is the basic cases by variables
143 framework. Need to embed this within other
144 structures which allow for generalized relations.
145 Goal is to ensure that relations imply and drive
146 the potential for statistical relativeness such as
147 correlation, interference, and similar concepts.
149 STORE is the storage component. We ignore this in
150 the DATAFRAME-LIKE class, as it is the primary
151 differentiator, spec'ing the structure used for
152 storing the actual data. We create methods which
153 depend on STORE for access. See DATAFRAME-ARRAY
154 and DATAFRAME-MATRIXLIKE for examples. The rest of
155 this is metadata."))
157 ;;; Generics specialized above matrix-like, particularly for
158 ;;; dataframe-like objects. Need implementation of methods which
159 ;;; depend on storage form.
161 (defgeneric dataframe-dimensions (df)
162 (:documentation "")
163 (:method ((df dataframe-like))
164 (error "Dispatch on virtual class, Method needed for
165 DATAFRAME-DIMENSIONS with class ~A." (find-class df))))
167 (defgeneric dataframe-dimension (df index)
168 (:documentation "")
169 (:method ((df dataframe-like) index)
170 (elt (dataframe-dimensions df) index)))
172 (defgeneric dfref (df index1 index2)
173 (:documentation "Scalar access to entries in dataframe.")
174 (:method ((df dataframe-like) index1 index2)
175 (error "Dispatch on virtual class, Method needed for DFREF with
176 class ~A." (find-class df))))
178 (defgeneric (setf dfref) (df index1 index2 val)
179 (:documentation "setter for dfref")
180 (:method ((df dataframe-like) index1 index2 val)
181 (error "Dispatch on virtual class, Method needed for SET-DFREF
182 with class ~A." (find-class df))))
184 (defgeneric dfselect (df &optional cases vars indices)
185 (:documentation "access to sub-dataframes. Always returns a dataframe.")
186 (:method ((df dataframe-like) &optional cases vars indices)
187 (declare (ignorable cases vars))
188 (if indices (error "Indicies not used yet"))
189 (error "Dispatch on virtual class, Method needed for DFSELECT with
190 class ~A." (find-class df))))
192 ;;; Specializing on superclasses...
194 ;;; Access and Extraction: implementations needed for any storage
195 ;;; type. But here, just to point out that we've got a specializing
196 ;;; virtual subclass (DATAFRAME-LIKE specializing MATRIX-LIKE).
198 (defmethod nrows ((df dataframe-like))
199 "specializes on inheritance from matrix-like in lisp-matrix."
200 (error "Need implementation; can't dispatch on virtual class DATAFRAME-LIKE."))
202 (defmethod ncols ((df dataframe-like))
203 "specializes on inheritance from matrix-like in lisp-matrix."
204 (error "Need implementation; can't dispatch on virtual class DATAFRAME-LIKE."))
206 ;; Testing consistency/coherency.
208 (defgeneric consistent-dataframe-p (df)
209 (:documentation "methods to check for consistency.")
210 (:method ((df dataframe-like))
211 (and
212 ;; ensure dimensionality
213 (= (length (var-labels df)) (ncols df)) ; array-dimensions (dataset df))
214 (= (length (case-labels df)) (nrows df))
215 ;; when dims sane, check-type for each variable
216 (progn
217 (dotimes (i (nrows df))
218 (dotimes (j (ncols df))
219 ;; dfref bombs if not a df-like subclass so we don't worry
220 ;; about specialization.
221 ;; (check-type (aref dt i j) (elt lot j)))))) ???
222 (typep (dfref df i j) (nth j (var-types df)))))
223 t))))
226 ;;; FUNCTIONS WHICH DISPATCH ON INTERNAL METHODS OR ARGS
228 ;;; Q: change the following to generic functions and dispatch on
229 ;;; array, matrix, and dataframe? Others?
230 (defun make-labels (initstr num)
231 "generate a list of strings which can be used as labels, i.e. something like
232 (make-labels \"a\" 3) => '(\"a1\" \"a2\" \"a3\")."
233 (check-type initstr string)
234 (mapcar #'(lambda (x y) (concatenate 'string x y))
235 (repeat-seq num initstr)
236 (mapcar #'(lambda (x) (format nil "~A" x)) (gen-seq num))))
238 (defun ncase-store (store)
239 "Return number of cases (rows) in dataframe storage. Doesn't test
240 that that list is a valid listoflist dataframe structure."
241 (etypecase store
242 (array (array-dimension store 0))
243 (matrix-like (nrows store))
244 (list (length store))))
246 (defun nvars-store (store)
247 "Return number of variables (columns) in dataframe storage. Doesn't
248 test that that list is a valid listoflist dataframe structure."
249 (etypecase store
250 (array (array-dimension store 1))
251 (matrix-like (ncols store))
252 (list (length (elt store 0)))))
255 (defun make-dataframe (newdata
256 &key (vartypes nil)
257 (caselabels nil) (varlabels nil)
258 (doc "no docs"))
259 "Helper function to use instead of make-instance to assure
260 construction of proper DF-array."
261 (check-type newdata (or matrix-like array list))
262 (check-type caselabels sequence)
263 (check-type varlabels sequence)
264 (check-type doc string)
265 (let ((ncases (ncase-store newdata))
266 (nvars (nvars-store newdata)))
267 (if caselabels (assert (= ncases (length caselabels))))
268 (if varlabels (assert (= nvars (length varlabels))))
269 (let ((newcaselabels (if caselabels
270 caselabels
271 (make-labels "C" ncases)))
272 (newvarlabels (if varlabels
273 varlabels
274 (make-labels "V" nvars))))
275 (etypecase newdata
276 (list
277 (make-instance 'dataframe-listoflist
278 :storage newdata
279 :nrows (length newcaselabels)
280 :ncols (length newvarlabels)
281 :case-labels newcaselabels
282 :var-labels newvarlabels
283 :var-types vartypes))
284 (array
285 (make-instance 'dataframe-array
286 :storage newdata
287 :nrows (length newcaselabels)
288 :ncols (length newvarlabels)
289 :case-labels newcaselabels
290 :var-labels newvarlabels
291 :var-types vartypes))
292 (matrix-like
293 (make-instance 'dataframe-matrixlike
294 :storage newdata
295 :nrows (length newcaselabels)
296 :ncols (length newvarlabels)
297 :case-labels newcaselabels
298 :var-labels newvarlabels
299 :var-types vartypes))
301 ))))
304 (make-dataframe #2A((1.2d0 1.3d0) (2.0d0 4.0d0)))
305 (make-dataframe #2A(('a 1) ('b 2)))
306 (dfref (make-dataframe #2A(('a 1) ('b 2))) 0 1)
307 (dfref (make-dataframe #2A(('a 1) ('b 2))) 1 0)
308 (make-dataframe 4) ; ERROR, should we allow?
309 (make-dataframe #2A((4)))
310 (make-dataframe (rand 10 5)) ;; ERROR, but should work!
314 (defun row-order-as-list (ary)
315 "Pull out data in row order into a list."
316 (let ((result (list))
317 (nrows (nth 0 (array-dimensions ary)))
318 (ncols (nth 1 (array-dimensions ary))))
319 (dotimes (i ncols)
320 (dotimes (j nrows)
321 (append result (aref ary i j))))))
323 (defun col-order-as-list (ary)
324 "Pull out data in row order into a list."
325 (let ((result (list))
326 (nrows (nth 0 (array-dimensions ary)))
327 (ncols (nth 1 (array-dimensions ary))))
328 (dotimes (i nrows)
329 (dotimes (j ncols)
330 (append result (aref ary i j))))))
332 (defun transpose-array (ary)
333 "map NxM to MxN."
334 (make-array (reverse (array-dimensions ary))
335 :initial-contents (col-order-as-list ary)))
337 ;;; THE FOLLOWING 2 dual-sets done to provide error checking
338 ;;; possibilities on top of the generic function structure. Not
339 ;;; intended as make-work!
341 (defun varlabels (df)
342 "Variable-name handling for DATAFRAME-LIKE. Needs error checking."
343 (var-labels df))
345 (defun set-varlabels (df vl)
346 "Variable-name handling for DATAFRAME-LIKE. Needs error checking."
347 (if (= (length (var-labels df))
348 (length vl))
349 (setf (var-labels df) vl)
350 (error "wrong size.")))
352 (defsetf varlabels set-varlabels)
354 ;;; Case-name handling for Tables. Needs error checking.
355 (defun caselabels (df)
356 "Case-name handling for DATAFRAME-LIKE. Needs error checking."
357 (case-labels df))
359 (defun set-caselabels (df cl)
360 "Case-name handling for DATAFRAME-LIKE. Needs error checking."
361 (if (= (length (case-labels df))
362 (length cl))
363 (setf (case-labels df) cl)
364 (error "wrong size.")))
366 (defsetf caselabels set-caselabels)
368 ;;;;;;;;;;;; IMPLEMENTATIONS, with appropriate methods.
369 ;; See also:
370 ;; (documentation 'dataframe-like 'type)
372 ;;;;; DATAFRAME-ARRAY
374 (defclass dataframe-array (dataframe-like)
375 ((store :initform nil
376 :initarg :storage
377 :type (array * *)
378 :accessor dataset
379 :documentation "Data storage: typed as array."))
380 (:documentation "example implementation of dataframe-like using storage
381 based on lisp arrays. An obvious alternative could be a
382 dataframe-matrix-like which uses the lisp-matrix classes."))
384 (defmethod nrows ((df dataframe-array))
385 "specializes on inheritance from matrix-like in lisp-matrix."
386 (array-dimension (dataset df) 0))
388 (defmethod ncols ((df dataframe-array))
389 "specializes on inheritance from matrix-like in lisp-matrix."
390 (array-dimension (dataset df) 1))
392 (defmethod dfref ((df dataframe-array)
393 (index1 number) (index2 number))
394 "Returns a scalar in array, in the same vein as aref, mref, vref, etc.
395 idx1/2 is row/col or case/var."
396 (aref (dataset df) index1 index2))
398 (defmethod set-dfref ((df dataframe-array) (index1 number) (index2 number) val)
399 "set value for df-ar."
400 ;; (check-type val (elt (var-type df) index2))
401 (setf (aref (dataset df) index1 index2) val))
403 (defparameter *default-dataframe-class* 'dataframe-array)
405 (defmethod dfselect ((df dataframe-array)
406 &optional cases vars indices)
407 "Extract the OR of cases, vars, or have a list of indices to extract"
408 (if indices (error "Indicies not used yet"))
409 (let ((newdf (make-instance *default-dataframe-class*
410 :storage (make-array (list (length cases) (length vars)))
411 :nrows (length cases)
412 :ncols (length vars)
414 :case-labels (select-list caselist (case-labels df))
415 :var-labels (select-list varlist (var-labels df))
416 :var-types (select-list varlist (vartypes df))
419 (dotimes (i (length cases))
420 (dotimes (j (length vars))
421 (setf (dfref newdf i j)
422 (dfref df
423 (position (elt cases i) (case-labels df))
424 (position (elt vars j) (var-labels df))))))))
426 ;;; DATAFRAME-MATRIXLIKE
427 ;;;
428 ;;; example/implementatin of using lisp-matrix datastructures for
429 ;;; dataframe storage.
431 (defclass dataframe-matrixlike (dataframe-like)
432 ((store :initform nil
433 :initarg :storage
434 :type matrix-like
435 :accessor dataset
436 :documentation "Data storage: typed as matrix-like
437 (numerical only)."))
438 (:documentation "example implementation of dataframe-like using storage
439 based on lisp-matrix structures."))
441 (defmethod nrows ((df dataframe-matrixlike))
442 "specializes on inheritance from matrix-like in lisp-matrix."
443 (matrix-dimension (dataset df) 0))
445 (defmethod ncols ((df dataframe-matrixlike))
446 "specializes on inheritance from matrix-like in lisp-matrix."
447 (matrix-dimension (dataset df) 1))
449 (defmethod dfref ((df dataframe-matrixlike)
450 (index1 number) (index2 number))
451 "Returns a scalar in array, in the same vein as aref, mref, vref, etc.
452 idx1/2 is row/col or case/var."
453 (mref (dataset df) index1 index2))
455 (defmethod set-dfref ((df dataframe-matrixlike)
456 (index1 number) (index2 number) val)
457 "Sets a value for df-ml."
458 ;; NEED TO CHECK TYPE!
459 ;; (check-type val (elt (vartype df) index2))
460 (setf (mref (dataset df) index1 index2) val))
465 ;;; DATAFRAME-LISTOFLIST
466 ;;;
467 ;;; example/implementatin of using lisp-matrix datastructures for
468 ;;; dataframe storage.
470 (defclass dataframe-listoflist (dataframe-like)
471 ((store :initform nil
472 :initarg :storage
473 :type list
474 :accessor dataset
475 :documentation "Data storage: typed as matrix-like
476 (numerical only)."))
477 (:documentation "example implementation of dataframe-like using storage
478 based on lisp-matrix structures."))
480 (defmethod nrows ((df dataframe-listoflist))
481 "specializes on inheritance from listoflist in lisp-matrix."
482 (length (dataset df)))
484 (defmethod ncols ((df dataframe-listoflist))
485 "specializes on inheritance from matrix-like in lisp-matrix."
486 (length (elt (dataset df) 0)))
488 (defmethod dfref ((df dataframe-listoflist)
489 (index1 number) (index2 number))
490 "Returns a scalar in array, in the same vein as aref, mref, vref, etc.
491 idx1/2 is row/col or case/var."
492 (elt (elt (dataset df) index1) index2)) ;; ??
494 (defmethod set-dfref ((df dataframe-listoflist)
495 (index1 number) (index2 number) val)
496 "Sets a value for df-ml."
497 ;; NEED TO CHECK TYPE!
498 ;; (check-type val (elt (vartype df) index2))
499 (setf (elt (elt (dataset df) index2) index1) val))
503 ;;;;;; IMPLEMENTATION INDEPENDENT FUNCTIONS AND METHODS
504 ;;;;;; (use only dfref, nrows, ncols and similar dataframe-like
505 ;;;;;; components as core).
507 (defun dfref-var (df index return-type)
508 "Returns the data in a single variable as type.
509 type = sequence, vector, vector-like (if valid numeric type) or dataframe."
510 (ecase return-type
511 (('list)
512 (map 'list
513 #'(lambda (x) (dfref df index x))
514 (gen-seq (nth 2 (array-dimensions (dataset df))))))
515 (('vector) t)
516 (:vector-like t)
517 (:matrix-like t)
518 (:dataframe t)))
520 (defun dfref-case (df index return-type)
521 "Returns row as sequence."
522 (ecase return-type
523 (:list
524 (map 'list
525 #'(lambda (x) (dfref df x index))
526 (gen-seq (nth 1 (array-dimensions (dataset df))))))
527 (:vector t)
528 (:vector-like t)
529 (:matrix-like t)
530 (:dataframe t)))
532 ;; FIXME
533 (defun dfref-2indexlist (df indexlist1 indexlist2 &key (return-type :array))
534 "return an array, row X col dims. FIXME TESTME"
535 (case return-type
536 (:array
537 (let ((my-pre-array (list)))
538 (dolist (x indexlist1)
539 (dolist (y indexlist2)
540 (append my-pre-array (dfref df x y))))
541 (make-array (list (length indexlist1)
542 (length indexlist2))
543 :initial-contents my-pre-array)))
544 (:dataframe
545 (make-instance 'dataframe-array
546 :storage (make-array
547 (list (length indexlist1)
548 (length indexlist2))
549 :initial-contents (dataset df))
550 ;; ensure copy for this and following
551 :doc (doc-string df)
552 ;; the following 2 need to be subseted based on
553 ;; the values of indexlist1 and indexlist2
554 :case-labels (case-labels df)
555 :var-labels (var-labels df)))))
557 ;;; Do we establish methods for dataframe-like, which specialize to
558 ;;; particular instances of storage?
560 (defmethod print-object ((object dataframe-like) stream)
561 (print-unreadable-object (object stream :type t)
562 (format stream " ~d x ~d" (nrows object) (ncols object))
563 (terpri stream)
564 ;; (format stream "~T ~{~S ~T~}" (var-labels object))
565 (dotimes (j (ncols object)) ; print labels
566 (write-char #\tab stream)
567 (write-char #\tab stream)
568 (format stream "~T~A~T" (nth j (var-labels object))))
569 (dotimes (i (nrows object)) ; print obs row
570 (terpri stream)
571 (format stream "~A:~T" (nth i (case-labels object)))
572 (dotimes (j (ncols object))
573 (write-char #\tab stream) ; (write-char #\space stream)
574 ;; (write (dfref object i j) :stream stream)
575 (format stream "~7,3E" (dfref object i j)) ; if works, need to include a general output mechanism control
576 ))))
579 (defun print-structure-relational (ds)
580 "example of what we want the methods to look like. Should be sort
581 of like a graph of spreadsheets if the storage is a relational
582 structure."
583 (dolist (k (relations ds))
584 (let ((currentRelationSet (getRelation ds k)))
585 (print-as-row (var-labels currentRelationSet))
586 (let ((j -1))
587 (dolist (i (case-labels currentRelationSet))
588 (print-as-row
589 (append (list i)
590 (dfref-obsn (dataset currentRelationSet)
591 (incf j)))))))))
593 (defun testecase (s)
594 (ecase s
595 ((scalar) 1)
596 ((asd asdf) 2)))
598 (testecase 'scalar)
599 (testecase 'asd)
600 (testecase 'asdf)
601 (testecase 'as)
605 ;;; Vector-like generalizations: we consider observation-like and
606 ;;; variable-like to be abstract classes which provide row and column
607 ;;; access to dataframe structures. These will be specialized, in
608 ;;; that rows correspond to an observation (or case?) which are
609 ;;; multitype, while columns correspond to a variable, which must be
610 ;;; singularly typed.
612 (defclass observation-like (dataframe-like)
614 (:documentation "dataframe-like with only 1 row, is an observation-like."))
616 (defclass variable-like (dataframe-like)
618 (:documentation "dataframe-like with only 1 column is a variable-like."))
620 ;;; Need to implement views, i.e. dataframe-view-like,
621 ;;; observation-view-like, variable-view-like.
623 ;;; Need to consider read-only variants, leveraging the xref
624 ;;; strategy.