init research

This commit is contained in:
2026-02-08 11:20:43 -10:00
commit bdf064f54d
3041 changed files with 1592200 additions and 0 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+67
View File
@@ -0,0 +1,67 @@
(ns tech.v3.dataset.bzip2
"API for reading/writing a single dataset to/from a bzip2 file. File must
only contain one entry.
You must have apache commons-compress included as part of your project to
load this namespace. Many projects already include commons-compress so manually
including it may not be necessary for your system.
Also note that version before 1.21 have several known vulnerabilities.
```clojure
org.apache.commons/commons-compress {:mvn/version \"1.21\"}
```
### Usage
```clojure
user> (require '[tech.v3.dataset :as ds])
nil
user> (require '[tech.v3.dataset.bzip2])
nil
user> (def stocks (ds/->dataset \"test/data/stocks.csv\"))
#'user/stocks
user> (ds/write! stocks \"test/data/stocks.csv.bz2\")
nil
user> (ds/head (ds/->dataset \"test/data/stocks.csv.bz2\"))
test/data/stocks.csv.bz2 [5 3]:
| symbol | date | price |
|--------|------------|------:|
| MSFT | 2000-01-01 | 39.81 |
| MSFT | 2000-02-01 | 36.35 |
| MSFT | 2000-03-01 | 43.22 |
| MSFT | 2000-04-01 | 28.37 |
| MSFT | 2000-05-01 | 25.45 |
```
"
(:require [tech.v3.dataset.io :as ds-io]
[tech.v3.dataset.io.univocity :as univocity]
[tech.v3.dataset.io.mapseq-colmap :as parse-mapseq-colmap]
[tech.v3.io :as io])
(:import [org.apache.commons.compress.compressors.bzip2 BZip2CompressorInputStream
BZip2CompressorOutputStream]
[java.io InputStream]))
(set! *warn-on-reflection* true)
(defmethod ds-io/data->dataset :bz2
[data options]
(let [ftype (-> (ds-io/str->file-info (.substring (str data) 0
(- (count data) 4)))
(get :file-type))]
(with-open [is (-> (apply io/input-stream data (apply concat (seq options)))
(BZip2CompressorInputStream.))]
(ds-io/data->dataset is (assoc options :file-type ftype)))))
(defmethod ds-io/dataset->data! :bz2
[data output options]
(let [ftype (-> (ds-io/str->file-info (.substring (str output) 0
(- (count output) 4)))
(get :file-type))]
(with-open [os (-> (apply io/output-stream! output (apply concat (seq options)))
(BZip2CompressorOutputStream.))]
(ds-io/dataset->data! data os (assoc options :file-type ftype)))))
+334
View File
@@ -0,0 +1,334 @@
(ns tech.v3.dataset.categorical
"Conversions of categorical values into numbers and back. Two forms of conversions
are supported, a straight value->integer map and one-hot encoding.
The functions in this namespace manipulate the metadata on the columns of the dataset, wich can be inspected via `clojure.core/meta`"
(:require [tech.v3.dataset.base :as ds-base]
[tech.v3.dataset.protocols :as ds-proto]
[tech.v3.dataset.impl.dataset :as ds-impl]
[tech.v3.dataset.impl.column :as col-impl]
[tech.v3.dataset.impl.column-base :as col-base]
[tech.v3.datatype :as dtype]
[tech.v3.datatype.protocols :as dtype-proto]
[tech.v3.datatype.casting :as casting]
[tech.v3.datatype.errors :as errors]
[tech.v3.datatype.bitmap :as bitmap]
[ham-fisted.lazy-noncaching :as lznc]
[ham-fisted.set :as set]
[ham-fisted.defprotocol :refer [extend-type]])
(:refer-clojure :exclude [extend-type]))
(defn- indiscrete-mapping
[[k v]]
(let [v (double v)
vv (double (Math/round (double v)))]
(not (== v vv))))
;;This file uses categorical-map loosely. Really they are lookup tables
;;from categorical object value to integer.
(defn- make-categorical-map-from-table-args
"Make a mapping of value->index from a list of either string values or [valname idx]
pairs.
Returns map of value->index."
[table-value-list]
;; First, any explicit mappings are respected.
(let [[str-table value-list]
(reduce (fn [[str-table value-list] item]
(if (sequential? item)
[(assoc str-table
(first item)
(second item))
value-list]
[str-table (conj value-list item)]))
[{} []]
table-value-list)
;;Finally, auto-generate values for anything not mapped yet.
cat-map (->> value-list
(reduce (fn [str-table item]
(assoc str-table item
(first (remove (set (vals str-table))
(range)))))
str-table))
bad-mappings (->> cat-map
(filter indiscrete-mapping)
seq)]
;;; just to be sure
(assert (= (clojure.set/map-invert (clojure.set/map-invert cat-map))
cat-map)
"The categorical mapping calculated is not bijective")
(when-not (nil? bad-mappings)
(throw (Exception. (str "Categorical map values must be integers.
Non integers found: " (vec bad-mappings)))))
cat-map))
(defrecord ^:private CategoricalMap [lookup-table src-column result-datatype])
(defn ^:no-doc create-categorical-map
[lookup-table src-colname result-datatype]
(map->CategoricalMap
{:lookup-table lookup-table
:src-column src-colname
:result-datatype result-datatype}))
(defn fit-categorical-map
"Given a column, map it into an numeric space via a discrete map of values
to integers. This fits the categorical transformation onto the column and returns
the transformation.
If `table-args` is not given, the distinct column values will be mapped into 0..x without any specific order.
'table-args` allows to specify the precise mapping as a sequence of pairs of [val idx] or as a sorted seq of values.
"
^CategoricalMap [dataset colname & [table-args res-dtype]]
(let [m (make-categorical-map-from-table-args table-args)
closed (atom (set (vals m)))
idx (atom 0)
nxt (fn ^long [n]
(if (@closed n)
(do (swap! closed disj n)
(recur (swap! idx inc)))
(let [res @idx
_ (swap! idx inc)]
(long res))))]
(create-categorical-map
(reduce (fn [categorical-map col-val]
(if (get categorical-map col-val)
categorical-map
(assoc categorical-map col-val (nxt @idx))))
m
(set/unique (ds-base/column dataset colname)))
colname
(or res-dtype :int64))))
(defn transform-categorical-map
"Apply a categorical mapping transformation fit with fit-categorical-map."
[dataset fit-data]
(let [colname (:src-column fit-data)
result-datatype (or (:result-datatype fit-data) :int64)
lookup-table (:lookup-table fit-data)
column (ds-base/column dataset colname)
missing (ds-proto/missing column)
col-meta (meta column)
missing-value (col-base/datatype->missing-value result-datatype)]
(assoc dataset colname
(col-impl/new-column
(:name col-meta)
(dtype/emap (fn [col-val]
(if-not (nil? col-val)
(let [numeric (get lookup-table col-val)]
(errors/when-not-errorf
numeric
"Failed to find label entry for column value %s"
col-val)
numeric)
missing-value))
result-datatype
column)
(assoc col-meta :categorical-map fit-data)
missing))))
(extend-type CategoricalMap
ds-proto/PDatasetTransform
(transform [t dataset]
(transform-categorical-map dataset t)))
(defn dataset->categorical-maps
"Given a dataset, return a sequence of categorical map entries.
```clojure
user> (ds-cat/dataset->categorical-maps catds)
({:lookup-table {:a 0, :b 1, :c 2, :d 3},
:src-column :x,
:result-datatype :float64}
{:lookup-table {:a 0, :b 1, :c 2, :d 3},
:src-column :y,
:result-datatype :float64})
```"
[dataset]
(->> (vals dataset)
(map (comp :categorical-map meta))
(remove nil?)))
(defn invert-categorical-map
"Invert a categorical map returning the column to the original set of values."
[dataset {:keys [src-column lookup-table] :as opts}]
(let [column (ds-base/column dataset src-column)
res-dtype (reduce casting/widest-datatype
(map dtype/datatype (keys lookup-table)))
inv-map (set/map-invert lookup-table)
missing-val (col-base/datatype->missing-value res-dtype)]
(assoc dataset src-column
(col-impl/new-column
(:name src-column)
(dtype/emap (fn [col-val]
(if-not (nil? col-val)
(let [src-val (get inv-map (long col-val))]
(errors/when-not-errorf
src-val
"Unable to find src value for numeric value %s"
col-val)
src-val)
missing-val))
res-dtype
column)
(-> (meta column)
(dissoc :categorical-map)
(assoc :categorical? true))
(ds-proto/missing column)))))
(defn- safe-str
[data]
(if (keyword? data) (name data)
(.replace (str data) " " "-")))
(defrecord OneHotMap [one-hot-table src-column result-datatype])
(defn fit-one-hot
"Fit a one hot transformation to a column. Returns a reusable transformation.
Maps each unique value to a column with 1 every time the value appears in the
original column and 0 otherwise."
^OneHotMap [dataset colname & [table-args res-dtype]]
(let [{:keys [lookup-table result-datatype]}
(fit-categorical-map dataset colname table-args res-dtype)
column (ds-base/column dataset colname)
src-meta (meta column)
src-name (:name src-meta)
name-fn (if (= (dtype/datatype src-name) :keyword)
(let [src-name (name src-name)]
#(keyword (str src-name "-" (safe-str %))))
#(str src-name "-" (safe-str %)))
one-hot-map (->> lookup-table
(map (fn [[k _v]]
[k (name-fn k)]))
(into {}))]
(map->OneHotMap
{:one-hot-table one-hot-map
:src-column src-name
:result-datatype result-datatype})))
(defn transform-one-hot
"Apply a one-hot transformation to a dataset"
[dataset one-hot-fit-data]
(let [{:keys [one-hot-table src-column result-datatype]}
one-hot-fit-data
column (ds-base/column dataset src-column)
missing (ds-proto/missing column)
dataset (dissoc dataset src-column)
n-elems (dtype/ecount column)
op-space (casting/simple-operation-space (dtype-proto/operational-elemwise-datatype column))]
(merge dataset
(->> one-hot-table
(lznc/map
(fn [[k v]]
(col-impl/new-column
v
(case op-space
:int64
(let [buf (dtype/->reader column :int64)
k (long k)]
(reify tech.v3.datatype.LongBuffer
(elemwiseDatatype [this] :int8)
(lsize [this] n-elems)
(readLong [this idx]
(if (== k (.readLong buf idx))
1 0))))
:float64
(let [buf (dtype/->reader column :float64)
k (double k)]
(reify tech.v3.datatype.LongBuffer
(elemwiseDatatype [this] :int8)
(lsize [this] n-elems)
(readLong [this idx]
(if (== k (.readDouble buf idx))
1 0))))
(dtype/emap
#(if (= % k)
1
0)
:int8
column))
(assoc (meta column)
:one-hot-map one-hot-fit-data)
missing)))
(ds-impl/new-dataset)))))
(extend-type OneHotMap
ds-proto/PDatasetTransform
(transform [t dataset]
(transform-one-hot dataset t)))
(defn ^:no-doc dataset->one-hot-maps
"Given a dataset, return a sequence of applied on-hot transformations."
[dataset]
(->> (vals dataset)
(map (comp :one-hot-map meta))
(remove nil?)
(distinct)))
(defn invert-one-hot-map
"Invert a one-hot transformation removing the one-hot columns and adding back the
original column."
[dataset {:keys [one-hot-table src-column] :as opts}]
(let [cast-fn (get @casting/*cast-table* :boolean)
invert-map (set/map-invert one-hot-table)
colnames (vec (keys invert-map))
one-hot-ds (ds-base/select-columns dataset colnames)
rev-mapped-cols (->> invert-map
(map (fn [[colname colval]]
(dtype/emap #(if (cast-fn %)
colval
nil)
:object
(ds-base/column one-hot-ds colname)))))
dataset (apply dissoc dataset colnames)
missing (bitmap/reduce-union (map ds-proto/missing (vals one-hot-ds)))
res-dtype (reduce casting/widest-datatype
(map dtype/datatype (keys one-hot-table)))]
(assoc dataset
src-column
(col-impl/new-column
src-column
(apply dtype/emap (fn [& colvals]
(first (remove nil? colvals)))
res-dtype
rev-mapped-cols)
(-> (meta (first (vals one-hot-ds)))
(dissoc :one-hot-map)
(assoc :categorical? true))
missing))))
(defn reverse-map-categorical-xforms
"Given a dataset where we have converted columns from a categorical representation
to either a numeric reprsentation or a one-hot representation, reverse map
back to the original dataset given the reverse mapping of label->number in
the column's metadata."
[dataset]
(->> (concat (map vector
(dataset->categorical-maps dataset)
(repeat invert-categorical-map))
(map vector
(dataset->one-hot-maps dataset)
(repeat invert-one-hot-map)))
(reduce (fn [dataset [cat-map invert-fn]]
(invert-fn dataset cat-map))
dataset)))
+56
View File
@@ -0,0 +1,56 @@
(ns tech.v3.dataset.clipboard
"Optional namespace that copies a dataset to the clipboard for pasting into
applications such as excel or google sheets.
Reading defaults to 'csv' format while writing defaults to 'tsv' format."
(:require [tech.v3.dataset :as ds]
[tech.v3.datatype.errors :as errors])
(:import [java.io ByteArrayInputStream ByteArrayOutputStream]
[java.awt.datatransfer Clipboard DataFlavor StringSelection]
[java.nio.charset StandardCharsets]
[java.awt Toolkit]))
(set! *warn-on-reflection* true)
(defn clipboard
"Get the system clipboard."
^Clipboard []
(.getSystemClipboard (Toolkit/getDefaultToolkit)))
(defn clipboard->dataset
"Copy from the clipboard to a dataset. Options are passed into ->dataset,
and `:file-type` is defaulted to `:csv`."
[& [{:keys [file-type]
:or {file-type :csv}
:as options}]]
(try
(-> (.getTransferData (.getContents (clipboard) nil)
(DataFlavor/stringFlavor))
(str)
(.getBytes StandardCharsets/UTF_8)
(ByteArrayInputStream.)
(ds/->dataset (assoc options :file-type file-type)))
(catch Exception e
(errors/throwf "Failed to copy dataset from the clipboard,
This can happen with headless JDK installations:
%s" e))))
(defn dataset->clipboard
"Copy a dataset to the clipboard. File type is defaulted to 'tsv' as this appears
to have wider compatibility across various spreadsheet toolkits."
[x & [{:keys [file-type]
:or {file-type :tsv}}]]
(try
(let [os (ByteArrayOutputStream.)
_ (ds/write! x os {:file-type file-type})
selection (StringSelection. (String. (.toByteArray os)
StandardCharsets/UTF_8))]
(.setContents (clipboard) selection selection))
(catch Exception e
(errors/throwf "Failed to copy dataset to the clipboard,
This can happen with headless JDK installations:
%s" e))))
+248
View File
@@ -0,0 +1,248 @@
(ns tech.v3.dataset.column
(:require [tech.v3.dataset.protocols :as ds-proto]
[tech.v3.dataset.impl.column :as col-impl]
[tech.v3.dataset.impl.column-base :as col-base]
[tech.v3.dataset.string-table :as str-table]
[tech.v3.dataset.io.column-parsers :as column-parsers]
[tech.v3.datatype.statistics :as stats]
[tech.v3.datatype :as dtype]
[tech.v3.datatype.argtypes :as argtypes]
[tech.v3.datatype.protocols :as dt-proto]
[tech.v3.datatype.casting :as casting]
[tech.v3.datatype.bitmap :as bitmap]
[ham-fisted.set :as set]
[ham-fisted.api :as hamf]
[ham-fisted.lazy-noncaching :as lznc])
(:import [tech.v3.dataset.impl.column Column]
[org.roaringbitmap RoaringBitmap]
[tech.v3.datatype Buffer LongReader DoubleReader ObjectReader]
[clojure.lang IFn$LL IFn$LLL IFn$LLLL
IFn$DD IFn$DDD IFn$DDDD]))
(declare new-column)
(defn is-column?
"Return true if this item is a column."
[item]
(when item
(ds-proto/is-column? item)))
(defn column-name
[col]
(ds-proto/column-name col))
(defn set-name
"Return a new column."
[col name]
(ds-proto/set-name col name))
(defn supported-stats
"List of available stats for the column"
[col]
stats/all-descriptive-stats-names)
(defn missing
"Indexes of missing values. Both iterable and reader."
^RoaringBitmap [col]
(ds-proto/missing col))
(defn is-missing?
"Return true if this index is missing."
[col ^long idx]
(when-let [^RoaringBitmap bmp (missing col)]
(.contains bmp idx)))
(defn set-missing
"Set the missing indexes for a column. This doesn't change any values in the
underlying data store."
[col idx-seq]
(Column. (-> (when idx-seq
(case (argtypes/arg-type idx-seq)
:scalar [idx-seq]
:iterable (hamf/take (dtype/ecount col) idx-seq)
idx-seq))
(bitmap/->bitmap)
(set/intersection (bitmap/->bitmap 0 (dtype/ecount col))))
(ds-proto/column-buffer col) (meta col) nil))
(defn unique
"Set of all unique values"
[col]
(set/unique col))
(defn stats
"Return a map of stats. Stats set is a set of the desired stats in keyword
form. Guaranteed support across implementations for :mean :variance :median :skew.
Implementations should check their metadata before doing calculations."
[col stats-set]
(stats/descriptive-statistics col stats-set))
(defn correlation
"Correlation coefficient for given 2 columns. Available correlation types
are:
:pearson
:spearman
:kendall
Returns floating point number between [-1 1]"
[lhs rhs correlation-type]
(case correlation-type
:pearson (stats/pearsons-correlation lhs rhs)
:spearman (stats/spearmans-correlation lhs rhs)
:kendall (stats/kendalls-correlation lhs rhs)))
(defn select
"Return a new column with the subset of indexes based on the provided `selection`.
`selection` can be a list of indexes to select or boolean values where the index
position of each true element indicates a index to select. When supplying a list
of indices, duplicates are possible and will select the specified position more
than once."
[col selection]
(ds-proto/select-rows col selection))
(defn clone
"Clone this column not changing anything."
[col]
(dtype/clone col))
(defn string-table-keyset
"Get the string table for this column. Returns nil if this isn't a string column.
This doesn't necessarily tell you the unique set of the column unless you have just
parsed a file. It is, when non-nil, a strict superset of the strings in the
columns."
[col]
(when (and (= :string (dtype/get-datatype col))
(instance? Column col))
(try
(->> (str-table/get-str-table (.data ^Column col))
:str->int
(keys)
(set))
(catch Throwable _e
nil))))
;; TODO - match inference expectations
(defn parse-column
"parse a text or a str column, returning a new column with the same name but with
a different datatype. This method is single-threaded.
parser-fn-or-kwd is nil by default and can the keyword :relaxed? or a function that
must return one of parsed-value, :tech.v3.dataset/missing in which case a
missing value will be added or :tech.v3.dataset/parse-failure in which case the
a missing index will be added and the string value will be recorded in the metadata's
:unparsed-data, :unparsed-indexes entries.
Options:
Same options roughly as ->dataset, specifically of interest may be `:text-temp-file`.
"
([datatype col options]
(let [colname (column-name col)
col-reader (dtype/emap #(when % (str %)) :string col)
col-parser (column-parsers/make-fixed-parser colname datatype options)
n-elems (dtype/ecount col-reader)]
(dotimes [iter n-elems]
(column-parsers/add-value! col-parser iter (col-reader iter)))
(new-column (assoc (column-parsers/finalize! col-parser n-elems)
:tech.v3.dataset/name colname))))
([datatype col]
(parse-column datatype col nil)))
(defn new-column
"Create a new column. Data will scanned for missing values
unless the full 4-argument pathway is used."
([name data]
(col-impl/new-column name data))
([name data metadata]
(col-impl/new-column name data metadata))
([name data metadata missing]
(col-impl/new-column name data metadata missing))
([data-or-data-map]
(col-impl/new-column data-or-data-map)))
(defn extend-column-with-empty
[column n-empty]
(col-impl/extend-column-with-empty column n-empty))
(defn prepend-column-with-empty
[column n-empty]
(col-impl/prepend-column-with-empty column n-empty))
(defn to-double-array
"Convert to a java primitive array of a given datatype. For strings,
an implicit string->double mapping is expected. For booleans, true=1 false=0.
Finally, any missing values should be indicated by a NaN of the expected type."
^doubles [col & [error-on-missing?]]
(when (and error-on-missing? (not (== 0 (dtype/ecount (ds-proto/missing col)))))
(throw (RuntimeException. (str "Missing value detected on column: "
(ds-proto/column-name col)))))
(hamf/double-array (dtype/->reader col :float64)))
(defn column-map
"Map a scalar function across one or more columns.
This is the semi-missing-set aware version of tech.v3.datatype/emap. This function
is never lazy.
If res-dtype is nil then the result is scanned to infer datatype and
missing set. res-dtype may also be a map of options:
Options:
* `:datatype` - Set the dataype of the result column. If not given result is scanned
to infer result datatype and missing set.
* `:missing-fn` - if given, columns are first passed to missing-fn as a sequence and
this dictates the missing set. Else the missing set is by scanning the results
during the inference process. See `tech.v3.dataset.column/union-missing-sets` and
`tech.v3.dataset.column/intersect-missing-sets` for example functions to pass in
here."
[map-fn res-dtype & args]
(let [res-opt-map (if (keyword? res-dtype)
{:datatype res-dtype}
(or res-dtype {}))
;;object readers get scanned to infer datatype
res-dtype (res-opt-map :datatype)
missing-fn (res-opt-map :missing-fn)
^RoaringBitmap missing (when missing-fn
(bitmap/->bitmap (missing-fn args)))
^Buffer data (apply dtype/emap map-fn res-dtype args)
data (if (or (nil? missing) (.isEmpty missing))
;;data will be scanned by the dataset to ascertain datatype and/or missing
(dtype/clone data)
(-> (col-impl/make-column-buffer missing data res-dtype res-dtype)
(dtype/clone)))]
(col-impl/new-column
#:tech.v3.dataset{:name :_unnamed
:data data
:force-datatype (boolean missing)
:missing missing})))
(defn union-missing-sets
"Union the missing sets of the columns returning a roaring bitmap"
[col-seq]
(apply set/reduce-union (lznc/map ds-proto/missing col-seq)))
(defn intersect-missing-sets
"Intersect the missing sets of the columns returning a roaring bitmap"
[col-seq]
(apply set/reduce-intersection (lznc/map ds-proto/missing col-seq)))
+149
View File
@@ -0,0 +1,149 @@
(ns tech.v3.dataset.column-filters
"Queries to select column subsets that have various properites such as all numeric
columns, all feature columns, or columns that have a specific datatype.
Further a few set operations (union, intersection, difference) are provided
to further manipulate subsets of columns.
All functions are transformations from dataset to dataset.
The functions in this namespace use the metadata on the columns of the dataset, wich can be inspected via `clojure.core/meta`"
(:require [tech.v3.datatype.casting :as casting]
[tech.v3.datatype.datetime :as dtype-dt]
[tech.v3.datatype.packing :as packing]
[tech.v3.dataset.base :as ds-base]
[tech.v3.dataset.column :as ds-col])
(:refer-clojure :exclude [boolean update]))
(set! *warn-on-reflection* true)
(defn column-filter
"Return a dataset with only the columns for which the filter function returns a truthy
value."
[dataset filter-fn]
(->> (ds-base/columns dataset)
(filter filter-fn)
(map (comp :name meta))
(ds-base/select-columns dataset)
(ds-base/check-empty)))
(defn metadata-filter
"Return a dataset with only the columns for which, given the column metadata,
the filter function returns a truthy value."
[dataset filter-fn]
(->> (map meta (ds-base/columns dataset))
(filter filter-fn)
(map :name)
(ds-base/select-columns dataset)
(ds-base/check-empty)))
(defn categorical
"Return a dataset containing only the categorical columns."
[dataset]
(metadata-filter dataset :categorical?))
(defn numeric
"Return a dataset containing only the numeric columns."
[dataset]
(metadata-filter dataset (comp casting/numeric-type?
packing/unpack-datatype
:datatype)))
(defn of-datatype
"Return a dataset containing only the columns of a specific datatype."
[dataset datatype]
(metadata-filter dataset #(= datatype (:datatype %))))
(defn boolean
"Return a dataset containing only the boolean columns."
[dataset]
(of-datatype dataset :boolean))
(defn string
"Return a dataset containing only the string columns."
[dataset]
(of-datatype dataset :string))
(defn target
"Return a dataset containing only the columns that have been marked as inference
targets."
[dataset]
(metadata-filter dataset :inference-target?))
(defn probability-distribution
"Return the columns of the dataset that comprise the probability distribution
after classification."
[dataset]
(metadata-filter dataset #(= :probability-distribution (:column-type %))))
(defn prediction
"Return the columns of the dataset marked as predictions."
[dataset]
(metadata-filter dataset #(= :prediction (:column-type %))))
(defn feature
"Return a dataset container only the columns which have not been marked as inference
columns."
[dataset]
(metadata-filter dataset (complement :inference-target?)))
(defn missing
"Return a dataset with only columns have have missing values"
[dataset]
(column-filter dataset #(not (.isEmpty (ds-col/missing %)))))
(defn no-missing
"Return a dataset with only columns that have no missing values."
[dataset]
(column-filter dataset #(.isEmpty (ds-col/missing %))))
(defn datetime
"Return a dataset containing only the datetime columns."
[dataset]
(metadata-filter dataset (comp dtype-dt/datetime-datatype? :datatype)))
(defn intersection
"Return only columns for rhs for which an equivalently named column exists in lhs."
[lhs-ds rhs-ds]
(if (or (nil? lhs-ds)
(nil? rhs-ds))
nil
(let [lhs-names (set (map (comp :name meta) (ds-base/columns lhs-ds)))]
(metadata-filter rhs-ds (comp lhs-names :name)))))
(defn union
"Return all columns of lhs along with any columns in rhs which have names that
do not exist in lhs."
[lhs-ds rhs-ds]
(let [lhs-names (set (map (comp :name meta) (ds-base/columns lhs-ds)))]
(reduce (fn [lhs-ds rhs-col]
(if (lhs-names (:name (meta rhs-col)))
lhs-ds
(ds-base/add-column lhs-ds rhs-col)))
lhs-ds
(ds-base/columns rhs-ds))))
(defn difference
"Return the columns in lhs which do not have an equivalently named column in
rhs."
([lhs-ds rhs-ds]
(apply dissoc lhs-ds (map (comp :name meta) (ds-base/columns rhs-ds))))
([lhs-ds]
lhs-ds))
+131
View File
@@ -0,0 +1,131 @@
(ns tech.v3.dataset.dynamic-int-list
"An int-list implementation that resizes its backing store as it is required to hold
wider data."
(:require [tech.v3.datatype.protocols :as dtype-proto]
[tech.v3.datatype :as dtype]
[tech.v3.datatype.base :as dtype-base]
[tech.v3.datatype.errors :as errors]
[tech.v3.datatype.casting :as casting]
[tech.v3.datatype.array-buffer :as abuf]
[tech.v3.datatype.list :as dtype-list]
[tech.v3.parallel.for :as parallel-for]
[clj-commons.primitive-math :as pmath])
(:import [tech.v3.datatype LongBuffer]
[ham_fisted IMutList Casts]))
(set! *warn-on-reflection* true)
(set! *unchecked-math* :warn-on-boxed)
(defmacro ^:private byte-range?
[number]
`(and (<= ~number Byte/MAX_VALUE)
(>= ~number Byte/MIN_VALUE)))
(defmacro ^:private short-range?
[number]
`(and (<= ~number Short/MAX_VALUE)
(>= ~number Short/MIN_VALUE)))
(deftype DynamicIntList [^:unsynchronized-mutable ^IMutList backing-store
^:unsynchronized-mutable ^long int-width]
dtype-proto/PClone
(clone [_item] (dtype-proto/clone backing-store))
dtype-proto/PToArrayBuffer
(convertible-to-array-buffer? [_item]
(dtype-proto/convertible-to-array-buffer? backing-store))
(->array-buffer [_item]
(dtype-proto/->array-buffer backing-store))
dtype-proto/PToNativeBuffer
(convertible-to-native-buffer? [_item]
(dtype-proto/convertible-to-native-buffer? backing-store))
(->native-buffer [_item]
(dtype-proto/->native-buffer backing-store))
LongBuffer
(elemwiseDatatype [_this] (dtype-proto/elemwise-datatype backing-store))
(clear [this]
(set! backing-store (abuf/as-growable-list (dtype/make-list :int8 1500) 0))
(set! int-width 8))
(lsize [_this] (.size backing-store))
(size [_this] (.size backing-store))
(subBuffer [this sidx eidx]
(dtype/->buffer (.subList backing-store sidx eidx)))
(cloneList [this] (DynamicIntList. (.cloneList backing-store) int-width))
(addLong [_this value]
;;perform container conversion
(cond
(byte-range? value)
nil
(short-range? value)
(when (pmath/< int-width 16)
(set! backing-store (dtype/make-list :int16 backing-store))
(set! int-width 16))
(pmath/< int-width 32)
(do
(set! backing-store (dtype/make-list :int32 backing-store))
(set! int-width 32)))
(.addLong backing-store value))
(add [this idx ct value]
(when-not (== idx (.size this))
(throw (RuntimeException. "Insertion other than at end is not supported.")))
(let [lv (Casts/longCast value)]
(dotimes [c ct]
(.addLong this lv))))
(getLong [_this idx]
(.getLong backing-store idx))
(readLong [_this idx]
(.getLong backing-store idx))
;;Writing is serialized.
(writeLong [this idx value]
(locking this
(cond
(byte-range? value)
nil
(short-range? value)
(when (pmath/< int-width 16)
(set! backing-store (dtype/make-list :int16 backing-store))
(set! int-width 16))
(< int-width 32)
(do
(set! backing-store (dtype/make-list :int32 backing-store))
(set! int-width 32)))
(.setLong backing-store idx value)))
(reduce [this rfn init]
(.reduce backing-store rfn init)))
(defn dynamic-int-list
"Create a dynamic int list from a sequence of numbers or from a
single integer n-elems argument."
(^IMutList [num-or-item-seq]
(if (number? num-or-item-seq)
(DynamicIntList. (dtype/make-list :int8 (long num-or-item-seq))
8)
(let [retval (DynamicIntList. (dtype/make-list :int8 0)
8)]
(.addAllReducible retval num-or-item-seq)
retval)))
(^IMutList [] (dynamic-int-list 0)))
(defn make-from-container
"Make a dynamic int list from something convertible to a byte, short,
or integer list. Shares backing data."
^IMutList [container]
(let [container-datatype (dtype/elemwise-datatype container)]
(errors/when-not-errorf
(and (casting/integer-type? container-datatype)
(<= (casting/int-width container-datatype) 32))
"Container datatype must be integer and 32 bits or less: %s"
container-datatype)
(errors/when-not-errorf
(dtype-base/as-buffer container)
"Container must be convertible to either an array buffer or native buffer: %s"
(type container))
(let [list-data (dtype-base/as-buffer container)]
(DynamicIntList. (abuf/as-growable-list container (dtype/ecount container))
(casting/int-width container-datatype)))))
+123
View File
@@ -0,0 +1,123 @@
(ns tech.v3.dataset.file-backed-text
(:require [tech.v3.datatype :as dtype]
[tech.v3.datatype.errors :as errors]
[tech.v3.datatype.mmap-writer :as mmap-writer]
[tech.v3.datatype.protocols :as dtype-proto])
(:import [java.nio.charset Charset]
[ham_fisted IMutList ChunkedList]
[tech.v3.datatype DataWriter Buffer]
[tech.v3.dataset Text]))
(set! *warn-on-reflection* true)
(set! *unchecked-math* true)
(defn- read-text-object
[^IMutList offsets ^Buffer byte-io ^Charset encoding ^long idx]
(let [start-offset (.getLong offsets idx)
end-offset (if (== idx (unchecked-dec (.size offsets)))
(.lsize byte-io)
(.getLong offsets (unchecked-inc idx)))
byte-data (-> (dtype/sub-buffer byte-io
start-offset
(- end-offset start-offset))
(dtype/->byte-array))
str-data (String. byte-data encoding)]
(Text. str-data)))
(defn- reduce-text-builder
[^IMutList offsets ^Buffer byte-io ^Charset encoding
rfn acc sidx eidx]
(let [sidx (long sidx)
eidx (long eidx)]
(loop [sidx sidx
acc acc]
(if (and (< sidx eidx) (not (reduced? acc)))
(recur (unchecked-inc sidx)
(rfn acc (read-text-object offsets byte-io encoding sidx)))
acc))))
(deftype FileBackedTextBuilder [^Charset encoding
^DataWriter writer
^IMutList offsets
cached-io]
dtype-proto/PClone
(clone [this]
(->> (dtype/make-container :text (.lsize this))
(dtype/copy! this)))
Buffer
(lsize [_this] (.size offsets))
(elemwiseDatatype [_this] :text)
(subBuffer [this sidx eidx]
(ChunkedList/sublistCheck sidx eidx (.size offsets))
(if (and (== 0 sidx) (== eidx (.size offsets)))
this
(let [^Buffer byte-io (swap! cached-io
(fn [old-io]
(if old-io old-io (dtype/->buffer writer))))
^IMutList offsets offsets
^Charset encoding encoding
ne (- eidx sidx)]
(if (== ne 0)
(reify Buffer (elemwiseDatatype [rdr] 0) (lsize [rdr] 0))
(reify Buffer
(elemwiseDatatype [rdr] :text)
(lsize [rdr] ne)
(subBuffer [rdr ssidx seidx]
(ChunkedList/sublistCheck ssidx seidx ne)
(.subBuffer this (+ sidx ssidx) (+ sidx seidx)))
(readObject [rdr idx]
(read-text-object offsets
byte-io
encoding
(+ sidx idx)))
(reduce [rdr rfn init]
(reduce-text-builder offsets byte-io encoding rfn init sidx eidx)))))))
(add [_this data]
(reset! cached-io nil)
(let [data (str data)]
(if (empty? data)
(.addLong offsets (.lsize writer))
(let [bytes (.getBytes ^String data encoding)]
(.addLong offsets (.lsize writer))
(.writeBytes writer bytes))))
true)
(readObject [_this idx]
(read-text-object offsets
(swap! cached-io
(fn [old-io]
(if old-io old-io (dtype/->buffer writer))))
encoding
idx))
(reduce [this rfn init]
(reduce-text-builder offsets (swap! cached-io
(fn [old-io]
(if old-io old-io (dtype/->buffer writer))))
encoding rfn init 0 (.size offsets))))
(defn file-backed-text
"Create a file-backed text store. Texts are written to disk but retrievable
(as Text objects) when ->reader is called on the return value."
(^Buffer [{:keys [mmap-file-path]
:as options}]
(let [mmap-writer (if mmap-file-path
(mmap-writer/mmap-writer mmap-file-path options)
(mmap-writer/temp-mmap-writer options))
encoding (if-let [encoding (:encoding options)]
(cond
(string? encoding)
(Charset/forName encoding)
(instance? Charset encoding)
encoding
:else
(errors/throwf "Unrecognized encoding: %s" encoding))
(Charset/forName "UTF-8"))
offsets (dtype/make-list :int64)
cached-io (atom nil)]
;;Force failure here before we get going - not all platforms support mmap.
(FileBackedTextBuilder. encoding mmap-writer offsets cached-io)))
(^Buffer [] (file-backed-text nil)))
+144
View File
@@ -0,0 +1,144 @@
(ns ^:no-doc tech.v3.dataset.format-sequence
"This code provided initial by genmeblog after careful consideration
of R print code"
(:import [java.text DecimalFormat]))
;;CN - I removed cl-format as a required dependency so that graal native executables would
;;both compile faster and be much smaller.
(set! *warn-on-reflection* true)
(set! *unchecked-math* :warn-on-boxed)
;; maximum double power for precise calculations
(def ^:private ^:const ^long kp-max 22)
;; powers for scientific notation
(def ^:private tbl [1e-1,
1e00, 1e01, 1e02, 1e03, 1e04, 1e05, 1e06, 1e07, 1e08, 1e09,
1e10, 1e11, 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19,
1e20, 1e21, 1e22])
(defn- left
"What is the power of number"
^long [^double x]
(-> x Math/log10 Math/floor unchecked-long inc))
(defn- find-nsig
"Shift decimal places until non-zero value is found"
^long [^double alpha ^long digits]
(loop [a alpha
d digits]
(let [a- (/ a 10.0)]
(if (= a- (Math/floor a-))
(recur a- (dec d))
(max 1 d)))))
(defn- right
"Calculate maximum digits on the right side of the dot."
^long [^double x ^long digits]
(let [alpha (Math/round (* x ^double (tbl (inc digits))))]
(if (zero? alpha)
1
(find-nsig alpha digits))))
(defn- fix-left
"Fix number of digits on the left side. For scientific notations and non-positive exponent (lft) it should be leading digits + sign."
[^double x ^long lft e?]
(let [sgn (if (neg? x) 1 0)]
(if (or e? (not (pos? lft)))
(+ sgn 1)
(+ sgn lft))))
(defn- precision
[^double x ^long digits ^long threshold]
(if (zero? x)
[false 0 1 1] ;; zero is reprezented as 0.0
(let [digits (max 1 (min 10 digits)) ;; constrain digits to 1-10 range
r (Math/abs x)
lft (left r) ;; digits on the left side of dot
alft (Math/abs lft)
e? (>= alft threshold)
r-prec (cond
(< alft threshold) r ;; normal number
(< alft kp-max) (if (neg? lft) ;; scientific number (using table to shift values)
(* r ^double (tbl (inc (- lft))))
(/ r ^double (tbl (inc lft))))
:else (/ r (Math/pow 10.0 (dec lft)))) ;; very big or very small case
rght (right r-prec digits) ;; desired precision on the right side
exp (if (> alft 100) 3 2) ;; size of the exponent
lft (fix-left x lft e?)]
[e? exp lft rght])))
(defn- fit-precision
"Find best matching presision for given sequence."
[xs ^long digits ^long threshold]
(reduce (fn [[ce? ^long cexp ^long clft ^long crght ^long non-finite-len] x]
(let [^double x (if (instance? Float x)
(Double/valueOf (str x))
(or x ##NaN))]
(if (Double/isFinite x)
(let [[e? ^long exp ^long lft ^long rght] (precision x digits threshold)]
(if (and e? (pos? threshold))
(reduced (fit-precision xs digits 0)) ;; switch to scientific notation
[(or e? ce?)
(max exp cexp)
(max lft clft)
(max rght crght)
non-finite-len]))
[ce? cexp clft crght (max non-finite-len (if (= x ##-Inf) 4 3))])))
[false Integer/MIN_VALUE Integer/MIN_VALUE Integer/MIN_VALUE 0] xs))
;; public functions
(defn formatter
"Create formatter for given:
* `xs` - sequence of doubles
* `digits` - maximum precision
* `threshold` - what is absolute power to switch to scientific notation
Returns formatter."
([xs] (formatter xs 8))
([xs ^long digits] (formatter xs digits 8))
([xs ^long digits ^long threshold]
(let [[e? ^long exp ^long lft ^long rght ^long non-finite-len] (fit-precision xs digits threshold)
w (max non-finite-len (if e?
(+ lft rght exp 3) ;; 3 = "." + sign of E + "E"
(+ lft rght 1))) ;; 1 for "."
deci-format-str (apply str (concat ["0."]
(repeat rght "0")
(when e?
["E"])
(when e?
(repeat exp "0"))))
deci-format (DecimalFormat. deci-format-str)
non-finite-format (str "%" w "s")]
(fn [x]
(let [^double x (or x ##NaN)
finite? (Double/isFinite x)
^String unspaced (if (Double/isFinite x)
(.format deci-format x)
(cond
(== ##Inf x) "Inf"
(== ##-Inf x) "-Inf"
:else "NaN"))]
(String/format non-finite-format
(object-array [(if (and e? finite?
(== -1 (.lastIndexOf unspaced "E-")))
;;Insert the + sign
(.replace unspaced "E" "E+")
unspaced)])))))))
(defn format-sequence
"Format sequence of double for given:
* `xs` - sequence of doubles
* `digits` - maximum precision
* `threshold` - what is absolute power to switch to scientific notation
Returns sequence of strings."
([xs] (format-sequence xs 8))
([xs ^long digits] (format-sequence xs digits 8))
([xs ^long digits ^long threshold]
(let [fmt (formatter xs digits threshold)]
(map fmt xs))))
+549
View File
@@ -0,0 +1,549 @@
(ns ^:no-doc tech.v3.dataset.impl.column
(:require [tech.v3.dataset.protocols :as ds-proto]
[tech.v3.datatype.protocols :as dtype-proto]
[tech.v3.datatype :as dtype]
[tech.v3.datatype.unary-pred :as un-pred]
[tech.v3.datatype.casting :as casting]
[tech.v3.datatype.functional :as dfn]
[tech.v3.datatype.statistics :as stats]
[tech.v3.datatype.pprint :as dtype-pp]
[tech.v3.datatype.bitmap :refer [->bitmap] :as bitmap]
[tech.v3.datatype.packing :as packing]
[tech.v3.datatype.argops :as argops]
[tech.v3.tensor :as dtt]
[tech.v3.dataset.impl.column-base :as column-base]
[tech.v3.dataset.impl.column-data-process :as column-data-process]
[ham-fisted.lazy-noncaching :as lznc]
[ham-fisted.api :as hamf]
[ham-fisted.reduce :as hamf-rf]
[ham-fisted.function :as hamf-fn]
[ham-fisted.set :as set]
[ham-fisted.protocols :as hamf-proto])
(:import [java.util Arrays]
[ham_fisted Reductions Casts ChunkedList IMutList]
[org.roaringbitmap RoaringBitmap]
[clojure.lang IPersistentMap Counted IFn IObj Indexed ILookup IFn$OLO IFn$ODO Named]
[tech.v3.datatype Buffer LongBuffer]))
(set! *warn-on-reflection* true)
(set! *unchecked-math* :warn-on-boxed)
(declare new-column construct-column)
(defn ->persistent-map
^IPersistentMap [item]
(if (instance? IPersistentMap item)
item
(into {} item)))
(defn- reduce-column-buffer
[rfn acc src missing primitive-missing-value sidx eidx]
(let [sidx (long sidx)
eidx (long eidx)
^RoaringBitmap missing missing
^Buffer src (dtype-proto/->buffer src)
int-iter (.getIntIterator ^RoaringBitmap
(if (and (== 0 sidx) (== (.lsize src) eidx))
missing
(RoaringBitmap/and missing (bitmap/->bitmap sidx eidx))))
^java.util.function.LongBinaryOperator next-missing
(hamf-fn/long-binary-operator
sidx next-missing-idx
(if (== sidx next-missing-idx)
(long (if (.hasNext int-iter) (Integer/toUnsignedLong (.next int-iter)) -1))
next-missing-idx))
missing-idx (.applyAsLong next-missing 0 0)]
(cond
(instance? IFn$OLO rfn)
(let [primitive-missing-value (Casts/longCast primitive-missing-value)]
(loop [sidx sidx
acc acc
missing-idx missing-idx]
(if (< sidx eidx)
(let [acc (.invokePrim ^IFn$OLO rfn acc (if (== sidx missing-idx)
primitive-missing-value
(.readLong src sidx)))]
(if (reduced? acc)
(deref acc)
(recur (unchecked-inc sidx) acc (.applyAsLong next-missing sidx missing-idx))))
acc)))
(instance? IFn$ODO rfn)
(let [primitive-missing-value (Casts/doubleCast primitive-missing-value)]
(loop [sidx sidx
acc acc
missing-idx missing-idx]
(if (< sidx eidx)
(let [acc (.invokePrim ^IFn$ODO rfn acc (if (== sidx missing-idx)
primitive-missing-value
(.readDouble src sidx)))]
(if (reduced? acc)
(deref acc)
(recur (unchecked-inc sidx) acc (.applyAsLong next-missing sidx missing-idx))))
acc)))
:else
(loop [sidx sidx
acc acc
missing-idx missing-idx]
(if (< sidx eidx)
(let [acc (rfn acc (if (== sidx missing-idx)
nil
(.readObject src sidx)))]
(if (reduced? acc)
(deref acc)
(recur (unchecked-inc sidx) acc (.applyAsLong next-missing sidx missing-idx))))
acc)))))
(defn- kv-reduce-column-buffer
[rfn acc src missing primitive-missing-value sidx eidx]
(let [sidx (long sidx)
eidx (long eidx)
^RoaringBitmap missing missing
^Buffer src (dtype-proto/->buffer src)
int-iter (.getIntIterator ^RoaringBitmap
(if (and (== 0 sidx) (== (.lsize src) eidx))
missing
(RoaringBitmap/and missing (bitmap/->bitmap sidx eidx))))
^java.util.function.LongBinaryOperator next-missing
(hamf-fn/long-binary-operator
sidx next-missing-idx
(if (== sidx next-missing-idx)
(long (if (.hasNext int-iter) (Integer/toUnsignedLong (.next int-iter)) -1))
next-missing-idx))
missing-idx (.applyAsLong next-missing 0 0)]
(loop [sidx sidx
acc acc
missing-idx missing-idx]
(if (< sidx eidx)
(let [acc (if (not (== sidx missing-idx))
(rfn acc sidx (.readObject src sidx))
acc)]
(if (reduced? acc)
(deref acc)
(recur (unchecked-inc sidx) acc (.applyAsLong next-missing sidx missing-idx))))
acc))))
(defn ^:no-doc make-column-buffer
(^Buffer [^RoaringBitmap missing data dtype op-dtype]
(let [^Buffer src (dtype-proto/->buffer data)
missing-value (column-base/datatype->missing-value dtype)
primitive-missing-value (column-base/datatype->packed-missing-value dtype)]
;;Sometimes we can utilize a pure passthrough.
(if (.isEmpty missing)
src
(reify
dtype-proto/POperationalElemwiseDatatype
(operational-elemwise-datatype [this] op-dtype)
dtype-proto/PElemwiseReaderCast
(elemwise-reader-cast [this new-dtype]
(make-column-buffer
missing
(dtype-proto/elemwise-reader-cast data new-dtype)
new-dtype
new-dtype))
Buffer
(elemwiseDatatype [this] dtype)
(lsize [this] (.lsize src))
(allowsRead [this] (.allowsRead src))
(allowsWrite [this] (.allowsWrite src))
(subBuffer [this sidx eidx]
(ChunkedList/sublistCheck sidx eidx (.lsize src))
(cond
(and (== sidx 0) (== eidx (.lsize src)))
this
(not (set/intersects-range? missing sidx eidx))
(.subBuffer src sidx eidx)
:else
(let [ec (- eidx sidx)]
(reify Buffer
(elemwiseDatatype [rdr] dtype)
(lsize [rdr] ec)
(subBuffer [rdr ssidx seidx]
(ChunkedList/sublistCheck ssidx seidx ec)
(.subBuffer this (+ sidx ssidx) (+ sidx seidx)))
(readLong [rdr idx] (.readLong this (+ idx sidx)))
(readDouble [rdr idx] (.readDouble this (+ idx sidx)))
(readObject [rdr idx] (.readObject this (+ idx sidx)))
(reduce [this rfn acc]
(reduce-column-buffer rfn acc src missing
primitive-missing-value sidx eidx))
(kvreduce [this rfn acc]
(kv-reduce-column-buffer rfn acc src missing primitive-missing-value sidx eidx))))))
(readLong [this idx]
(if (.contains missing idx)
(Casts/longCast primitive-missing-value)
(.readLong src idx)))
(readDouble [this idx]
(if (.contains missing idx)
Double/NaN
(.readDouble src idx)))
(readObject [this idx]
(when-not (.contains missing idx)
(.readObject src idx)))
(writeLong [this idx val]
(.remove missing (unchecked-int idx))
(.writeLong src idx val))
(writeDouble [this idx val]
(.remove missing (unchecked-int idx))
(.writeDouble src idx val))
(writeObject [this idx val]
(if val
(do
(.remove missing (unchecked-int idx))
(.writeObject src idx val))
(.add missing (unchecked-int idx))))
(reduce [this rfn acc]
(reduce-column-buffer rfn acc src missing primitive-missing-value
0 (.lsize src)))
(kvreduce [this rfn acc]
(kv-reduce-column-buffer rfn acc src missing primitive-missing-value
0 (.lsize src))))))))
(defmacro cached-buffer!
[]
`(or ~'buffer
(do (set! ~'buffer (make-column-buffer ~'missing ~'data
(.elemwise-datatype ~'this)
(.operational-elemwise-datatype ~'this)))
~'buffer)))
(defn- neg-access-reader
[^long n-elems data]
(let [m (meta data)
data (dtype/->reader data)]
(with-meta
(reify LongBuffer
(lsize [rdr] (.lsize data))
(readLong [rdr idx]
(let [v (.readLong data idx)]
(if (< v 0)
(+ v n-elems)
v)))
(subBuffer [rdr sidx eidx]
(neg-access-reader n-elems (.subBuffer data sidx eidx))))
(assoc m :min 0 :max (unchecked-dec n-elems)))))
(defn- wrap-negative-access
[^long n-elems data]
(if (== 0 (dtype/ecount data))
data
(if (dtype-proto/has-constant-time-min-max? data)
(let [minv (long (dtype-proto/constant-time-min data))]
(if (>= minv 0)
data
(neg-access-reader n-elems data)))
(neg-access-reader n-elems data))))
(defn simplify-row-indexes
[^long n-elems selection]
(if (:simplified? (meta selection))
selection
(do
(vary-meta
(cond
(nil? selection) (hamf/range 0)
;;Cannot possibly be negative
(and (sequential? selection) (boolean? (first selection)))
(un-pred/bool-reader->indexes (dtype/ensure-reader selection))
(set/bitset? selection)
(set/->integer-random-access selection)
(hamf-proto/set? selection)
(->> (set/->integer-random-access selection)
(wrap-negative-access n-elems))
(keyword? selection)
(case selection
:all (hamf/range n-elems)
:lla (hamf/range (dec n-elems) -1 -1))
(number? selection)
(let [selection (long selection)
selection (if (< selection 0)
(+ n-elems selection)
selection)]
(with-meta [selection] {:min selection :max selection :scalar? true}))
:else
;;Ensure the selection object has metadata
(let [minv (long (if (dtype-proto/has-constant-time-min-max? selection)
(long (dtype-proto/constant-time-min selection))
-1))]
(if (< (long minv) 0)
(->> (hamf-rf/preduce-reducer (un-pred/index-reducer n-elems) selection)
(wrap-negative-access n-elems))
;;if the data indicates it has no negative values
(dtype/->reader selection))))
assoc :simplified? true))))
(deftype Column
[^RoaringBitmap missing
data
^IPersistentMap metadata
^:unsynchronized-mutable ^Buffer buffer]
dtype-proto/PToArrayBuffer
(convertible-to-array-buffer? [_this]
(and (.isEmpty missing)
(dtype-proto/convertible-to-array-buffer? data)))
(->array-buffer [_this]
(dtype-proto/->array-buffer data))
dtype-proto/PToNativeBuffer
(convertible-to-native-buffer? [_this]
(and (.isEmpty missing)
(dtype-proto/convertible-to-native-buffer? data)))
(->native-buffer [_this]
(dtype-proto/->native-buffer data))
dtype-proto/PElemwiseDatatype
(elemwise-datatype [_this] (dtype-proto/elemwise-datatype data))
dtype-proto/POperationalElemwiseDatatype
(operational-elemwise-datatype [this]
(if (.isEmpty missing)
(dtype-proto/elemwise-datatype this)
(let [ewise-dt (dtype-proto/elemwise-datatype data)]
(cond
(packing/packed-datatype? ewise-dt)
(packing/unpack-datatype ewise-dt)
(casting/numeric-type? ewise-dt)
(casting/widest-datatype ewise-dt :float64)
(identical? :boolean ewise-dt)
:object
:else
ewise-dt))))
dtype-proto/PElemwiseCast
(elemwise-cast [_this new-dtype]
(let [new-data (dtype-proto/elemwise-cast data new-dtype)]
(Column. missing
new-data
metadata
nil)))
dtype-proto/PElemwiseReaderCast
(elemwise-reader-cast [this new-dtype]
(if (= new-dtype (dtype-proto/elemwise-datatype data))
(cached-buffer!)
(make-column-buffer missing (dtype-proto/elemwise-reader-cast data new-dtype)
new-dtype new-dtype)))
dtype-proto/PECount
(ecount [this] (dtype-proto/ecount data))
dtype-proto/PToBuffer
(convertible-to-buffer? [_this] true)
(->buffer [this]
(cached-buffer!))
dtype-proto/PToReader
(convertible-to-reader? [_this]
(dtype-proto/convertible-to-reader? data))
(->reader [this]
(dtype-proto/->buffer this))
dtype-proto/PToWriter
(convertible-to-writer? [_this]
(dtype-proto/convertible-to-writer? data))
(->writer [this]
(dtype-proto/->buffer this))
dtype-proto/PSubBuffer
(sub-buffer [this offset len]
(let [offset (long offset)
len (long len)
eidx (+ offset len)]
(ChunkedList/sublistCheck offset (+ offset len) (dtype/ecount data))
(if (and (== offset 0) (== len (dtype/ecount this)))
this
;;TODO - use bitmap operations to perform this calculation
(let [new-missing (-> (RoaringBitmap/and
missing
(doto (RoaringBitmap.)
(.add offset (+ offset len))))
(RoaringBitmap/addOffset (- offset)))
new-data (dtype-proto/sub-buffer data offset len)]
(Column. new-missing
new-data
metadata
nil)))))
dtype-proto/PClone
(clone [_col]
(let [new-data (if (or (dtype/writer? data)
(= :string (dtype/get-datatype data))
(= :encoded-text (dtype/get-datatype data)))
(dtype/clone data)
;;It is important that the result of this operation be writeable.
(dtype/make-container :jvm-heap
(dtype/get-datatype data) data))
cloned-missing (dtype/clone missing)]
(Column. cloned-missing
new-data
metadata
nil)))
ds-proto/PRowCount
(row-count [this] (dtype/ecount data))
ds-proto/PMissing
(missing [this] missing)
(num-missing [this] (dtype/ecount missing))
(any-missing? [this] (boolean (not (.isEmpty missing))))
ds-proto/PValidRows
(valid-rows [this] (.xor (->bitmap (hamf/range (dtype/ecount this))) missing))
ds-proto/PSelectRows
(select-rows [this rowidxs]
(let [rowidxs (simplify-row-indexes (dtype/ecount this) rowidxs)
new-missing (if (== 0 (set/cardinality missing))
(bitmap/->bitmap)
(argops/argfilter (set/contains-fn missing)
{:storage-type :bitmap}
rowidxs))
new-data (dtype/indexed-buffer rowidxs data)]
(Column. new-missing
new-data
metadata
nil)))
ds-proto/PColumn
(is-column? [_this] true)
(column-buffer [_this] data)
(empty-column? [this] (== (dtype/ecount this) (dtype/ecount missing)))
(column-data [_this] data)
(with-column-data [this new-data] (construct-column missing new-data metadata))
ds-proto/PColumnName
(column-name [this] (get metadata :name))
IMutList
(size [this] (.size (cached-buffer!)))
(get [this idx] (.get (cached-buffer!) idx))
(set [this idx v] (.set (cached-buffer!) idx v))
(getLong [this idx] (.getLong (cached-buffer!) idx))
(setLong [this idx v] (.setLong (cached-buffer!) idx v))
(getDouble [this idx] (.getDouble (cached-buffer!) idx))
(setDouble [this idx v] (.setDouble (cached-buffer!) idx v))
(valAt [this idx] (.valAt (cached-buffer!) idx))
(valAt [this idx def-val] (.valAt (cached-buffer!) idx def-val))
(invoke [this idx] (.invoke (cached-buffer!) idx))
(invoke [this idx def-val] (.invoke (cached-buffer!) idx def-val))
(meta [this] (assoc metadata
:datatype (dtype-proto/elemwise-datatype this)
:n-elems (dtype-proto/ecount this)))
(withMeta [_this new-meta] (Column. missing
data
new-meta
buffer))
(nth [this idx] (nth (cached-buffer!) idx))
(nth [this idx def-val] (nth (cached-buffer!) idx def-val))
;;should be the same as using hash-unorded-coll, effectively.
(hasheq [this] (.hasheq (cached-buffer!)))
(equiv [this o] (if (identical? this o) true (.equiv (cached-buffer!) o)))
(empty [this]
(Column. (->bitmap)
(column-base/make-container (dtype-proto/elemwise-datatype this) 0)
{}
nil))
(reduce [this rfn init] (.reduce (cached-buffer!) rfn init))
(kvreduce [this rfn init] (.kvreduce (cached-buffer!) rfn init))
(parallelReduction [this init-val-fn rfn merge-fn options]
(.parallelReduction (cached-buffer!) init-val-fn rfn merge-fn options))
Object
(toString [item]
(let [n-elems (dtype/ecount data)
format-str (if (> n-elems 20)
"#tech.v3.dataset.column<%s>%s\n%s\n[%s...]"
"#tech.v3.dataset.column<%s>%s\n%s\n[%s]")]
(format format-str
(name (dtype/elemwise-datatype item))
[n-elems]
(ds-proto/column-name item)
(-> (dtype-proto/sub-buffer item 0 (min 20 n-elems))
(dtype-pp/print-reader-data)))))
;;Delegates to ListPersistentVector, which caches results for us.
(hashCode [this] (.hasheq (cached-buffer!)))
(equals [this o] (.equiv this o)))
(dtype-pp/implement-tostring-print Column)
(defn construct-column
"Low level column construction. No analysis is done of the data to detect
datatype or missing values - the onus is on you. The column name should
be provided as the `:name` member of the metadata. Data must have a
conversion to a buffer - [tech.v3.datatype.protocols/PToBuffer](https://github.com/cnuernber/dtype-next/blob/master/src/tech/v3/datatype/protocols.clj#L131)."
^Column [missing data metadata]
(Column. (bitmap/->bitmap missing) data metadata nil))
(defn new-column
"Given a map of (something convertible to a long reader) missing indexes,
(something convertible to a reader) data
and a (string or keyword) name, return an implementation of enough of the
column and datatype protocols to allow efficient columnwise operations of
the rest of tech.ml.dataset"
([name data metadata missing]
(new-column #:tech.v3.dataset{:name name
:data data
:metadata metadata
:missing missing}))
([name data metadata]
(new-column name data metadata nil))
([name data]
(new-column name data {} nil))
([data-or-column-data-map]
(let [coldata (column-data-process/prepare-column-data data-or-column-data-map)
data (coldata :tech.v3.dataset/data)
metadata (coldata :tech.v3.dataset/metadata)
name (coldata :tech.v3.dataset/name)
;;Unless overidden, we now set the categorical? flag
metadata (if (and (not (contains? metadata :categorical?))
(column-base/column-datatype-categorical?
(dtype/elemwise-datatype data)))
(assoc metadata :categorical? true)
metadata)
missing (bitmap/->bitmap (coldata :tech.v3.dataset/missing))]
;;compress bitmaps
(.runOptimize missing)
(let [new-meta (assoc metadata :name name)]
(Column. missing
data
new-meta
nil)))))
(defn ensure-column-seq
[coldata-seq]
(->> coldata-seq
(column-data-process/prepare-column-data-seq)
;;mapv so we get a real stacktrace when things go south.
(mapv new-column)))
(defn extend-column-with-empty
[column ^long n-empty]
(if (== 0 (long n-empty))
column
(let [^Column column column
col-dtype (packing/unpack-datatype (dtype/elemwise-datatype column))
n-elems (dtype/ecount column)
container (dtype/make-container col-dtype (+ n-elems n-empty))]
(dtype/copy! (.data column) (dtype/sub-buffer container 0 n-elems))
(dtype/set-constant! container n-elems n-empty (get column-base/dtype->missing-val-map
col-dtype))
(Column. (set/union (ds-proto/missing column)
(bitmap/->bitmap (hamf/range n-elems (+ n-elems n-empty))))
container
(meta column)
nil))))
(defn prepend-column-with-empty
[column ^long n-empty]
(if (== 0 (long n-empty))
column
(let [^Column column column
col-dtype (packing/unpack-datatype (dtype/elemwise-datatype column))
n-elems (dtype/ecount column)
container (dtype/make-container col-dtype (+ n-elems n-empty))]
(dtype/copy! (.data column) (dtype/sub-buffer container n-empty n-elems))
(dtype/set-constant! container 0 n-empty (get column-base/dtype->missing-val-map
col-dtype))
(Column. (set/union (bitmap/->bitmap (hamf/range n-empty))
(bitmap/offset (.missing column) n-empty))
container
(meta column)
nil))))
+108
View File
@@ -0,0 +1,108 @@
(ns tech.v3.dataset.impl.column-base
(:require [tech.v3.dataset.string-table :as str-table]
[tech.v3.dataset.file-backed-text :as file-backed-text]
[tech.v3.datatype.datetime :as dtype-dt]
[tech.v3.datatype.packing :as packing]
[tech.v3.datatype.casting :as casting]
[tech.v3.datatype :as dtype]
[clojure.tools.logging :as log])
(:import [java.util Map]
[ham_fisted IMutList]
[tech.v3.dataset Text]))
(defn- pack-nil
^long [unpacked-datatype]
(let [unpacked-container (dtype/make-container unpacked-datatype [nil])
packed-container (packing/pack unpacked-container)]
(-> (dtype/as-buffer packed-container)
(.readLong 0))))
(def ^Map dtype->missing-val-map
{:boolean false
:int8 Byte/MIN_VALUE
:int16 Short/MIN_VALUE
:int32 Integer/MIN_VALUE
:int64 Long/MIN_VALUE
:float32 Float/NaN
:float64 Double/NaN
:packed-instant (pack-nil :instant)
:packed-local-date (pack-nil :local-date)
:packed-duration (pack-nil :duration)
:instant nil
:zoned-date-time nil
:local-date-time nil
:local-date nil
:local-time nil
:duration nil
:string ""
:text nil
:keyword nil
:symbol nil})
(casting/add-object-datatype! :text Text)
(defn datatype->missing-value
[dtype]
(let [packed? (packing/packed-datatype? dtype)
dtype (if packed?
(packing/unpack-datatype dtype)
(casting/un-alias-datatype dtype))]
(get dtype->missing-val-map dtype
(when (casting/numeric-type? dtype)
(casting/cast 0 dtype)))))
(defn datatype->packed-missing-value
[dtype]
(get dtype->missing-val-map dtype
(when (casting/numeric-type? dtype)
(casting/cast 0 dtype))))
(defonce ^:private warn-atom* (atom false))
(defonce file-backed-text-enabled* (atom true))
(defn set-file-backed-text-enabled
[enabled]
(reset! file-backed-text-enabled* enabled)
enabled)
(defn make-container
(^IMutList [dtype options]
(if-let [rv (get-in options [:datatype-parsers dtype])]
(rv dtype options)
(case dtype
:string (str-table/fast-string-container)
:text
(let [^IMutList list-data
(try
(if (and (not= false (get options :text-temp-dir false))
@file-backed-text-enabled*)
(let [tmp-dir (:text-temp-dir options)]
(file-backed-text/file-backed-text (merge
{:suffix ".txt"}
(when tmp-dir
{:temp-dir tmp-dir}))))
(dtype/make-list :text))
(catch Throwable e
(when-not @warn-atom*
(reset! warn-atom* true)
(log/warn e "File backed text failed. Falling back to in-memory"))
(dtype/make-list :text)))]
list-data)
(dtype/make-list dtype))))
(^IMutList [dtype]
(make-container dtype nil)))
(defn column-datatype-categorical?
"Anything where we don't know the conversion to a scalar double or integer
number is considered automatically categorical."
[col-dtype]
(and (not (casting/numeric-type? col-dtype))
(not (identical? col-dtype :boolean))
(not (dtype-dt/datetime-datatype? col-dtype))))
@@ -0,0 +1,136 @@
(ns tech.v3.dataset.impl.column-data-process
(:require [tech.v3.datatype :as dtype]
[tech.v3.datatype.casting :as casting]
[tech.v3.datatype.errors :as errors]
[tech.v3.datatype.bitmap :as bitmap]
[tech.v3.datatype.casting :as casting]
[tech.v3.parallel.for :as pfor]
[tech.v3.dataset.protocols :as ds-proto]
[tech.v3.dataset.io.column-parsers :as column-parsers]
[tech.v3.dataset.impl.column-base :as column-base])
(:import [org.roaringbitmap RoaringBitmap]
[java.util Map Objects]))
(set! *warn-on-reflection* true)
(set! *unchecked-math* :warn-on-boxed)
(defmacro missing-value?
"Is this a missing value given the datatype-specific column missing value"
[missing-value obj]
`(and (not (instance? Boolean ~obj))
(or (nil? ~obj)
(Objects/equals ~missing-value ~obj)
(identical? :tech.v3.dataset/missing ~obj)
(identical? :tech.v3.dataset/parse-failure ~obj))))
(defn scan-missing
"Scan a (potentially primitive) reader for missing values. This simply scans every
value in the reader."
^RoaringBitmap [rdr]
(let [rdr (dtype/->reader rdr)
missing (column-base/datatype->missing-value (dtype/elemwise-datatype rdr))]
(pfor/indexed-map-reduce
(.lsize rdr)
(fn [^long start-idx ^long group-len]
(let [bmp (bitmap/->bitmap)]
(dotimes [group-idx group-len]
(let [idx (+ group-idx start-idx)
obj (.readObject rdr idx)]
(when (missing-value? missing obj)
(.add bmp idx))))
bmp))
(fn [bmps]
(let [^RoaringBitmap bmp (first bmps)]
(doseq [^RoaringBitmap other-bmp (rest bmps)]
(.or bmp other-bmp))
bmp)))))
(defn scan-data
[obj-data missing]
(let [obj-data-datatype (dtype/elemwise-datatype obj-data)]
(if (and (dtype/reader? obj-data)
(not= :object obj-data-datatype))
;;If the user knows the datatype they want, then we just scan for missing if it
;;wasn't provided.
#:tech.v3.dataset{:data obj-data
:force-datatype? true
;;integer types don't have meaningful data indicators of missing.
:missing (or missing
(if (or (casting/integer-type? obj-data-datatype)
(identical? :boolean obj-data-datatype))
(bitmap/->bitmap)
(scan-missing obj-data)))}
(let [obj-meta (meta obj-data)
parser (column-parsers/promotional-object-parser
(:name obj-meta) obj-meta)
;;At this point either you are convertible to a reader or you are
;;iterable.
obj-data (or (dtype/as-reader obj-data) obj-data)
missing (bitmap/->bitmap missing)]
(if-let [rdr (dtype/as-reader obj-data)]
(dotimes [idx (.lsize rdr)]
(column-parsers/add-value! parser idx
(when-not (.contains missing idx)
(.readObject rdr idx))))
;;serially consume the data promoting the container when necessary.
(pfor/indexed-consume!
;;Do not read from a missing entry if missing is provided
#(column-parsers/add-value! parser %1
(when-not (.contains missing (unchecked-int %1))
%2))
obj-data))
(column-parsers/finalize! parser (dtype/ecount parser))))))
(defn prepare-column-data
"Scan data for missing values and to infer storage datatype. Returns
a map of least #tech.v3.dataset{:data :missing :force-datatype?}."
[obj-data]
(cond
(map? obj-data)
(do
(errors/when-not-errorf
(contains? ^Map obj-data :tech.v3.dataset/data)
"Map constructors must contain at least :tech.v3.dataset/data")
;;ensure we do not re-scan this object
(let [{:keys [tech.v3.dataset/data
tech.v3.dataset/missing
tech.v3.dataset/force-datatype?]} obj-data
obj-data (if (nil? missing)
(dissoc obj-data :tech.v3.dataset/missing)
obj-data)]
(merge
(if (and (dtype/reader? data) force-datatype?)
;;skip scan of the data, but potentially still scan for missing
{:tech.v3.dataset/data data
:tech.v3.dataset/missing (if missing missing (scan-missing data))}
(scan-data data missing))
;;allow missing/metadata to make it through so user can override
(dissoc obj-data :tech.v3.dataset/data)
#:tech.v3.dataset{:force-datatype? true})))
(ds-proto/is-column? obj-data)
(let [cm (meta obj-data)]
#:tech.v3.dataset{:name (:name cm)
:missing (ds-proto/missing obj-data)
:force-datatype? true
:data (ds-proto/column-buffer obj-data)
:metadata cm})
:else
(scan-data obj-data nil)))
(defn prepare-column-data-seq
[column-seq]
(pfor/pmap (fn [idx item]
(-> (if (map? item)
(update item :tech.v3.dataset/name
#(if (nil? %)
idx %))
item)
(prepare-column-data)
))
(range) column-seq))
+579
View File
@@ -0,0 +1,579 @@
(ns ^:no-doc tech.v3.dataset.impl.dataset
(:require [tech.v3.dataset.protocols :as ds-proto]
[tech.v3.dataset.impl.column :as col-impl]
[tech.v3.dataset.print :as ds-print]
[tech.v3.datatype.pprint :as dtype-pp]
[tech.v3.datatype :as dtype]
[tech.v3.datatype.argtypes :as argtypes]
[tech.v3.datatype.protocols :as dtype-proto]
[tech.v3.datatype.bitmap :as bitmap]
[tech.v3.datatype.unary-pred :as unary-pred]
[tech.v3.dataset.impl.column-data-process :as column-data-process]
[tech.v3.dataset.impl.column-base :as column-base]
[tech.v3.datatype.graal-native :as graal-native]
[ham-fisted.api :as hamf]
[ham-fisted.function :as hamf-fn]
[ham-fisted.reduce :as hamf-rf]
[ham-fisted.lazy-noncaching :as lznc]
[ham-fisted.protocols :as hamf-proto]
[ham-fisted.set :as set])
(:import [clojure.lang IPersistentMap IObj IFn Counted MapEntry IFn$LO
IFn$LOO]
[java.util Map List LinkedHashSet LinkedHashMap HashSet]
[tech.v3.datatype ObjectReader FastStruct Buffer]
[org.roaringbitmap RoaringBitmap]
[java.util.concurrent ConcurrentHashMap]
[java.util.function BiConsumer]))
(set! *warn-on-reflection* true)
(set! *unchecked-math* true)
(declare new-dataset map-entries empty-dataset)
;;ported from clojure.lang.APersistentMap
(defn- map-equiv [this o]
(cond (not (instance? java.util.Map o)) false
(and (instance? clojure.lang.IPersistentMap o)
(not (instance? clojure.lang.MapEquivalence o))) false
:else (let [^java.util.Map m o]
(and (= (.size m) (count this))
(every? (fn [k]
(and (.containsKey m k)
(= (this k) (.get m k)))) (keys this))))))
(defn- shorten-or-extend
[^long n-rows reader]
(let [reader-dtype (dtype/elemwise-datatype reader)
reader-ecount (dtype/ecount reader)]
(cond
(== reader-ecount n-rows)
reader
(< reader-ecount n-rows)
(dtype/concat-buffers (dtype/elemwise-datatype reader)
[reader (dtype/const-reader (column-base/datatype->missing-value
reader-dtype)
(- n-rows reader-ecount))])
;;Else the number of elements is greater than the number of rows
:else
(dtype/sub-buffer reader 0 n-rows))))
(defn- coldata->column
[^long n-cols ^long n-rows col-name new-col-data]
(let [argtype (argtypes/arg-type new-col-data)
n-rows (if (== 0 n-cols)
(cond
(dtype/reader? new-col-data)
(dtype/ecount new-col-data)
(map? new-col-data)
(if (contains? new-col-data :tech.v3.dataset/data)
(dtype/ecount (new-col-data :tech.v3.dataset/data))
1)
:else
(count (take Integer/MAX_VALUE new-col-data)))
n-rows)]
(cond
(ds-proto/is-column? new-col-data)
(vary-meta new-col-data assoc :name col-name)
;;maps are scalars in dtype-land but you can pass in column data maps
;;so this next check is a bit hairy
(or (identical? argtype :scalar)
;;This isn't a column data map. Maps are
(and (instance? Map new-col-data)
(not (.containsKey ^Map new-col-data :tech.v3.dataset/data))))
(col-impl/new-column col-name (dtype/const-reader new-col-data n-rows))
:else
(let [map-data
(if (or (= argtype :iterable)
(map? new-col-data))
(column-data-process/prepare-column-data
(if (map? new-col-data)
new-col-data
(take n-rows new-col-data)))
;;Else has to be reader or tensor.
(let [data-ecount (dtype/ecount new-col-data)
new-col-data (if (> data-ecount n-rows)
(dtype/sub-buffer new-col-data 0 n-rows)
new-col-data)]
(column-data-process/prepare-column-data new-col-data)))
new-c (col-impl/new-column (assoc map-data :tech.v3.dataset/name col-name))
c-len (dtype/ecount new-c)]
(cond
(< c-len n-rows)
(col-impl/extend-column-with-empty new-c (- n-rows c-len))
(> c-len n-rows)
(dtype/sub-buffer new-c 0 n-rows)
:else
new-c)))))
(defn- map-entries
^List [^List columns]
(reify ObjectReader
(lsize [rdr] (.size columns))
(readObject [rdr idx]
(let [col (.get columns idx)]
(MapEntry. (:name (meta col)) col)))))
(defmacro ^:private row-vec-copying
[n-cols]
`(let ~(->> (range n-cols)
(mapcat (fn [cidx]
[(with-meta (symbol (str "c" cidx))
{:tag 'Buffer}) `(~'readers ~cidx)]))
(vec))
(hamf-fn/long->obj ~'row-idx
(hamf/vector ~@(->> (range n-cols)
(map (fn [cidx]
`(.readObject ~(symbol (str "c" cidx))
~'row-idx))))))))
(deftype Dataset [^List columns
colmap
^IPersistentMap metadata
^{:unsynchronized-mutable true :tag 'int} _hash
^{:unsynchronized-mutable true :tag 'int} _hasheq]
java.util.Map
(size [this] (.count this))
(isEmpty [this] (not (pos? (.count this))))
(containsValue [_this v] (some #(= % v) columns))
(get [this k] (.valAt this k))
(put [_this _k _v] (throw (UnsupportedOperationException.)))
(remove [_this _k] (throw (UnsupportedOperationException.)))
(putAll [_this _m] (throw (UnsupportedOperationException.)))
(clear [_this] (throw (UnsupportedOperationException.)))
(keySet [_this] (.keySet ^Map colmap))
(values [_this] columns)
(entrySet [_this]
(let [retval (LinkedHashSet.)]
(.addAll retval (map #(clojure.lang.MapEntry. (:name (meta %)) %)
columns))
retval))
clojure.lang.ILookup
(valAt [_this k]
(when-let [idx (colmap k)]
(.get columns idx)))
(valAt [this k not-found]
(if-let [res (.valAt this k)]
res
not-found))
clojure.lang.MapEquivalence
clojure.lang.IPersistentMap
(assoc [this k v]
(let [n-cols (ds-proto/column-count this)
c (coldata->column n-cols
(ds-proto/row-count this)
k v)
cidx (get colmap k n-cols)]
(Dataset. (assoc (or columns []) cidx c) (assoc colmap k cidx) metadata 0 0)))
(assocEx [this k v]
(if-not (colmap k)
(.assoc this k v)
(throw (ex-info "Key already present" {:k k}))))
;;without implements (dissoc pm k) behavior
(without [this k]
(if-let [cidx (get colmap k)]
(let [cols (hamf/concatv (hamf/subvec columns 0 cidx) (hamf/subvec columns (inc cidx)))]
(Dataset. cols
(into {} (map-indexed #(vector (:name (meta %2)) %1)) cols)
metadata 0 0))
this))
(entryAt [this k]
(when-let [v (.valAt this k)]
(clojure.lang.MapEntry. k v)))
;;No idea if this is correct behavior....
(empty [_this] (empty-dataset))
;;ported from clojure java impl.
(cons [this e]
(cond (instance? java.util.Map$Entry e)
(.assoc this (key e) (val e))
(vector? e)
(let [^clojure.lang.PersistentVector e e]
(when-not (== (.count e) 2)
(throw (ex-info "Vector arg to map conj must be a pair" {})))
(.assoc this (.nth e 0) (.nth e 1)))
:else
(reduce (fn [^clojure.lang.Associative acc entry]
(.assoc acc (key entry) (val entry))) this e)))
(containsKey [_this k] (.containsKey ^Map colmap k))
;;MAJOR DEVIATION
;;This conforms to clojure's idiom and projects the dataset onto a
;;seq of [column-name column] entries. Legacy implementation defaulted
;;to using iterable, which was a seq of column.
(seq [_this]
;;Do not reorder column data if possible.
(when (pos? (count columns))
(map #(clojure.lang.MapEntry. (:name (meta %)) %) columns)))
;;Equality is likely a rat's nest, although we should be able to do it
;;if we wanted to!
(hashCode [this]
(when (== _hash 0)
(set! _hash (clojure.lang.APersistentMap/mapHash this)))
_hash)
clojure.lang.IHashEq
;;intentionally using seq instead of iterator for now.
(hasheq [this]
(when (== _hasheq 0)
(set! _hasheq (hash-unordered-coll (or (.seq this)
[]))))
_hasheq)
;;DOUBLE CHECK equals/equiv semantics...
(equals [this o] (or (identical? this o)
(clojure.lang.APersistentMap/mapEquals this o)))
(equiv [this o] (or (identical? this o)
(map-equiv this o)))
ds-proto/PRowCount
(row-count [this]
(if (== 0 (.size columns))
0
(dtype/ecount (.get columns 0))))
ds-proto/PColumnCount
(column-count [this]
(.size columns))
ds-proto/PMissing
(missing [this]
(RoaringBitmap/or (.iterator ^Iterable (lznc/map ds-proto/missing (.values this)))))
(num-missing [this]
(dtype/ecount (ds-proto/missing this)))
(any-missing? [this]
(boolean (some ds-proto/any-missing? (.values this))))
ds-proto/PValidRows
(valid-rows [this]
(RoaringBitmap/or (.iterator ^Iterable (lznc/map (comp bitmap/->bitmap ds-proto/valid-rows)
(.values this)))))
ds-proto/PSelectRows
(select-rows [dataset rowidxs]
(let [rowidxs (col-impl/simplify-row-indexes (ds-proto/row-count dataset) rowidxs)]
(when (== 0 (long (ds-proto/row-count dataset)))
(when-not (== 0 (dtype/ecount rowidxs))
(throw (IndexOutOfBoundsException. "Cannot select rows of empty dataset"))))
(->> columns
;;select may be slower if we have to recalculate missing values.
(lznc/map #(ds-proto/select-rows % rowidxs))
(new-dataset (ds-proto/dataset-name dataset)
(if-not (identical? :all (get metadata :print-index-range))
(dissoc metadata :print-index-range)
metadata)))))
ds-proto/PSelectColumns
(select-columns [dataset colnames]
;;Conversion to a reader is expensive in some cases so do it here
;;to avoid each column doing it.
(let [map-selector? (instance? Map colnames)]
(->> (cond
(identical? :all colnames)
columns
map-selector?
(->> colnames
(lznc/map (fn [[old-name new-name]]
(if-let [col-idx (get colmap old-name)]
(vary-meta (.get columns (unchecked-int col-idx))
assoc :name new-name)
(throw (Exception.
(format "Failed to find column %s" old-name)))))))
:else
(let [idx-set (HashSet.)]
(->> colnames
(lznc/map (fn [colname]
(if-let [col-idx (get colmap colname)]
(when-not (.contains idx-set col-idx)
(.add idx-set col-idx)
(.get columns (unchecked-int col-idx)))
(throw (Exception.
(format "Failed to find column %s" colname))))))
(lznc/remove nil?))))
(new-dataset (ds-proto/dataset-name dataset) metadata))))
ds-proto/PDataset
(is-dataset? [item] true)
(column [ds cname]
(if-let [retval (.get ds cname)]
retval
(throw (RuntimeException. (str "Column not found: " cname)))))
(rowvecs [ds options]
(let [readers (hamf/object-array-list
(lznc/map dtype/->reader columns))
n-cols (count readers)
n-rows (long (ds-proto/row-count ds))
copying? (get options :copying? true)
^IFn$LO row-fn
(if copying?
(case n-cols
0 (hamf-fn/long->obj row-idx [])
1 (row-vec-copying 1)
2 (row-vec-copying 2)
3 (row-vec-copying 3)
4 (row-vec-copying 4)
5 (row-vec-copying 5)
6 (row-vec-copying 6)
7 (row-vec-copying 7)
8 (row-vec-copying 8)
(let [crange (hamf/range n-cols)]
(hamf-fn/long->obj
row-idx
(->> crange
(lznc/map (hamf-fn/long->obj
col-idx (.readObject ^Buffer (.get readers col-idx) row-idx)))
(hamf/vec)))))
;;Non-copying in-place reader
(let [crange (hamf/range n-cols)]
(hamf-fn/long->obj
row-idx
(reify ObjectReader
(lsize [this] n-cols)
(readObject [this col-idx]
(.readObject ^Buffer (.get readers col-idx) row-idx))
(reduce [this rfn acc]
(reduce (hamf-rf/long-accumulator
acc col-idx (rfn acc ((.get readers col-idx) row-idx)))
acc
crange))))))]
(reify ObjectReader
(lsize [rdr] n-rows)
(readObject [rdr row-idx] (.invokePrim row-fn row-idx))
(subBuffer [rdr sidx eidx]
(-> (ds-proto/select-rows ds (hamf/range sidx eidx))
(ds-proto/rowvecs options)))
(reduce [rdr rfn acc]
(reduce (hamf-rf/long-accumulator
acc row-idx
(rfn acc (.invokePrim row-fn row-idx)))
acc (hamf/range n-rows))))))
(rows [ds options]
(let [^Buffer rvecs (.rowvecs ds options)
colnamemap (LinkedHashMap.)
_ (reduce (hamf-rf/indexed-accum
acc idx cname
(.put colnamemap cname idx)
colnamemap)
colnamemap
(lznc/map #(ds-proto/column-name %) columns))
n-rows (dtype/ecount rvecs)
nil-missing? (get options :nil-missing?)
^RoaringBitmap ds-missing (when (not nil-missing?) (ds-proto/missing ds))
constant-cmap? (or nil-missing? (.isEmpty ds-missing))
^IFn$LO cmap-fn (if constant-cmap?
(fn [^long v] colnamemap)
(let [col-fns
(->> (vals ds)
(lznc/map (fn [col]
(let [^RoaringBitmap col-missing (ds-proto/missing col)]
(when-not (.isEmpty col-missing)
(let [ckey (ds-proto/column-name col)]
(fn [^long row-idx ^Map colnamemap]
(when (.contains col-missing (unchecked-int row-idx))
(.remove colnamemap ckey))
colnamemap))))))
(lznc/remove nil?)
(hamf/object-array))
n-fns (alength col-fns)]
(fn [^long row-idx]
(if (.contains ds-missing (unchecked-int row-idx))
(let [colnamemap (.clone colnamemap)]
(dotimes [idx n-fns]
(.invokePrim ^IFn$LOO (aget col-fns idx) row-idx colnamemap))
colnamemap)
colnamemap))))]
(reify ObjectReader
(lsize [rdr] n-rows)
(readObject [rdr idx]
(FastStruct. (.invokePrim cmap-fn idx) (.readObject rvecs idx)))
(subBuffer [rdr sidx eidx]
(-> (ds-proto/select-rows ds (hamf/range sidx eidx))
(ds-proto/rows options)))
(reduce [rdr rfn acc]
(reduce (if constant-cmap?
(fn [acc vv]
(rfn acc (FastStruct. colnamemap vv)))
(hamf-rf/indexed-accum
acc row-idx vv
(rfn acc (FastStruct. (.invokePrim cmap-fn row-idx) vv))))
acc
rvecs)))))
dtype-proto/PShape
(shape [ds]
[(count columns) (ds-proto/row-count ds)])
dtype-proto/PCopyRawData
(copy-raw->item! [_raw-data ary-target target-offset options]
(dtype-proto/copy-raw->item! columns ary-target target-offset options))
dtype-proto/PClone
(clone [item]
(new-dataset (ds-proto/dataset-name item)
metadata
(mapv dtype/clone columns)))
Counted
(count [_this] (count columns))
IFn
;;NON-OBVIOUS SEMANTICS
;;Legacy implementation of invoke differs from clojure idioms for maps,
;;and instead of performing a lookup, we have effectively an assoc.
;;Is this necessary, or can we better conform to clojure idioms?
;; (invoke [item col-name new-col]
;; (ds-proto/add-column item (ds-col-proto/set-name new-col col-name)))
(invoke [this k]
(.valAt this k))
(invoke [this k not-found]
(.valAt this k not-found))
(applyTo [this arg-seq]
(case (count arg-seq)
1 (.invoke this (first arg-seq))
2 (.invoke this (first arg-seq) (second arg-seq))))
IObj
(meta [_this] metadata)
(withMeta [_this metadata] (Dataset. columns colmap metadata _hash _hasheq))
Iterable
(iterator [_item]
(.iterator (map-entries columns)))
Object
(toString [item]
(ds-print/dataset->str item)))
(defn construct-dataset
"Low-level construct a new dataset.
* 'columns' - java.util.List of [[tech.v3.dataset.impl.column.Column]] objects.
* 'colmap' - java.util.Map of column name to index in columns.
* 'metadata' - extra metadata for the dataset. Dataset name is store as `:name`."
(^Dataset [columns colmap metadata]
;;quick sanity check
(assert (== (count colmap) (count columns)))
(Dataset. columns colmap metadata 0 0))
(^Dataset [columns metadata]
(Dataset. columns (persistent!
(reduce (hamf-rf/indexed-accum
acc idx col
(.put ^Map acc (or (:name (meta col)) idx) idx)
acc)
(hamf/mut-map)
columns))
metadata 0 0)))
(defn new-dataset
"Create a new dataset from a sequence of columns. Data will be converted
into columns using ds-col-proto/ensure-column-seq. If the column seq is simply a
collection of vectors, for instance, columns will be named ordinally.
options map -
:dataset-name - Name of the dataset. Defaults to \"_unnamed\".
:key-fn - Key function used on all column names before insertion into dataset.
The return value fulfills the dataset protocols."
([options ds-metadata column-seq]
(let [;;Options was dataset-name so have to keep that pathway going.
dataset-name (or (if (map? options)
(:dataset-name options)
options)
(:name ds-metadata)
"_unnamed")
column-seq (hamf/vec column-seq)]
(if-not (seq column-seq)
(Dataset. [] {}
(assoc (col-impl/->persistent-map ds-metadata)
:name
dataset-name) 0 0)
(let [column-seq (hamf/vec column-seq)
sizes (->> column-seq
(lznc/map (fn [data]
(let [data (if (map? data)
(get data :tech.v3.dataset/data data)
data)
argtype (argtypes/arg-type data)]
;;nil return expected
(if (or (identical? argtype :scalar)
(identical? argtype :iterable))
1
(dtype/ecount data)))))
(lznc/remove nil?)
(hamf/immut-set))
n-rows (long (if (== 0 (count sizes))
0
(apply max sizes)))
n-cols (count column-seq)
key-fn (or (when (map? options)
(get options :key-fn identity))
identity)
column-seq (->> column-seq
(lznc/map-indexed
(fn [idx column]
(let [cname (ds-proto/column-name column)
cname (if (or (nil? cname)
(and (string? cname)
(== 0 (.length ^String cname))))
(key-fn idx)
(key-fn cname))]
(coldata->column n-cols n-rows cname column))))
(hamf/vec))]
(Dataset. column-seq
(do
(let [hs (hamf/mut-map)]
(reduce (hamf-rf/indexed-accum
acc idx col
(.put hs (ds-proto/column-name col) idx))
nil
column-seq)
(persistent! hs)))
(assoc (col-impl/->persistent-map ds-metadata)
:name
dataset-name)
0 0)))))
([options column-seq]
(new-dataset options {} column-seq))
([column-seq]
(new-dataset {} {} column-seq)))
(dtype-pp/implement-tostring-print Dataset)
;;pprint and graal native do not play nice with each other.
(graal-native/when-not-defined-graal-native
(require '[clojure.pprint :as pprint])
(defmethod pprint/simple-dispatch
tech.v3.dataset.impl.dataset.Dataset [f] (pr f)))
(defn dataset?
[ds]
(instance? Dataset ds))
(def ^:private EMPTY (new-dataset "_unnamed" nil))
(defn empty-dataset [] EMPTY)
@@ -0,0 +1 @@
(ns tech.v3.dataset.impl.missing-values)
@@ -0,0 +1,419 @@
(ns tech.v3.dataset.impl.sparse-column
(:require [tech.v3.datatype :as dt]
[tech.v3.datatype.bitmap :as bitmap]
[tech.v3.datatype.casting :as casting]
[tech.v3.datatype.packing :as packing]
[tech.v3.datatype.protocols :as dt-proto]
[tech.v3.datatype.pprint :as dtype-pp]
[tech.v3.dataset.protocols :as ds-proto]
[tech.v3.dataset.impl.column :as col-impl]
[tech.v3.dataset.string-table :as str-t]
[tech.v3.dataset.dynamic-int-list :as int-list]
[ham-fisted.set :as set]
[ham-fisted.api :as hamf]
[ham-fisted.reduce :as hamf-rf]
[ham-fisted.function :as hamf-fn]
[ham-fisted.iterator :as hamf-iter])
(:import [ham_fisted IMutList ArrayLists ITypedReduce ChunkedList]
[tech.v3.datatype Buffer ObjectReader ElemwiseDatatype LongReader DoubleReader]
[java.util Arrays Iterator Map List]
[tech.v3.dataset.string_table StringTable]))
(set! *warn-on-reflection* true)
(set! *unchecked-math* :warn-on-boxed)
(declare scol->reader reduce-scol kv-reduce-scol construct-sparse-col)
(defn- index-bin-search
^clojure.lang.IFn$LL [^IMutList indexes]
(let [idx-ary (.toNativeArray indexes)
nrc (- (long (.size indexes)))]
(case (dt/elemwise-datatype idx-ary)
:int8 (hamf-fn/long-unary-operator
idx (Arrays/binarySearch ^bytes idx-ary (unchecked-byte idx)))
:int16 (hamf-fn/long-unary-operator
idx (Arrays/binarySearch ^shorts idx-ary (unchecked-short idx)))
:int32 (hamf-fn/long-unary-operator
idx (Arrays/binarySearch ^ints idx-ary (unchecked-int idx)))
(hamf-fn/long-unary-operator
idx (Arrays/binarySearch ^longs idx-ary (unchecked-long idx))))))
;; Column wrapping csr format.
(deftype SparseCol [^IMutList indexes ;;sorted indexes
^IMutList data ^long rc metadata
^:unsynchronized-mutable msng ;;missing
^:unsynchronized-mutable rdr]
ElemwiseDatatype
(elemwiseDatatype [this] (dt/elemwise-datatype data))
dt-proto/POperationalElemwiseDatatype
(operational-elemwise-datatype [this]
(let [ewise-dt (dt-proto/elemwise-datatype data)]
(cond
(packing/packed-datatype? ewise-dt)
(packing/unpack-datatype ewise-dt)
(casting/numeric-type? ewise-dt)
(casting/widest-datatype ewise-dt :float64)
(identical? :boolean ewise-dt)
:object
:else
ewise-dt)))
tech.v3.datatype.ECount
(lsize [this] rc)
dt-proto/PToBuffer
(convertible-to-buffer? [this] true)
(->buffer [this]
(if rdr
rdr
(let [rv (scol->reader this)]
(set! rdr rv)
rv)))
dt-proto/PSubBuffer
(sub-buffer [this offset len]
(let [offset (long offset)
len (long len)
eidx (+ offset len)]
(ChunkedList/sublistCheck offset (+ offset len) (dt/ecount data))
(if (and (== offset 0) (== len rc))
this
(let [sidx offset
new-indexes (.subList indexes sidx eidx)
new-data (.subList data sidx eidx)
new-rc (- eidx sidx)]
(SparseCol. new-indexes new-data new-rc metadata nil nil)))))
dt-proto/PClone
(clone [_col]
(SparseCol. (dt/clone indexes) (dt/clone data) rc metadata nil nil))
ds-proto/PMissing
(missing [this]
(if (.-msng this)
(.-msng this)
(do
(let [miss-data (set/difference (bitmap/->bitmap (hamf/range rc)) (bitmap/->bitmap indexes))]
(set! (.-msng this) miss-data)
miss-data))))
(num-missing [this] (- rc (.size indexes)))
(any-missing? [this] (not (== rc (.size indexes))))
ds-proto/PValidRows
(valid-rows [this] indexes)
ds-proto/PColumn
(is-column? [this] true)
(column-buffer [this] [indexes data])
(empty-column? [this] (.isEmpty indexes))
(column-data [this] data)
(with-column-data [this new-data] (construct-sparse-col indexes new-data rc metadata))
ds-proto/PColumnName
(column-name [this] (get metadata :name))
ds-proto/PRowCount
(row-count [this] (dt/ecount data))
ds-proto/PSelectRows
(select-rows [this rowidxs]
(let [rowidxs (col-impl/simplify-row-indexes rc rowidxs)
res-rc (dt/ecount rowidxs)
^IMutList res-ixs (cond
(<= res-rc Byte/MAX_VALUE)
(hamf/byte-array-list)
(<= res-rc Short/MAX_VALUE)
(hamf/short-array-list)
(<= res-rc Integer/MAX_VALUE)
(hamf/int-array-list)
:else
(hamf/long-array-list))
^IMutList res-vals (dt/make-list (dt/elemwise-datatype data))
local-indexes indexes
bin-search (index-bin-search indexes)
last-data (long-array [-1 (.getLong local-indexes 0) 0])
dnidxs (dec (.size local-indexes))]
(reduce (hamf-rf/indexed-long-accum
acc rel-idx row-idx
(let [last-row-idx (aget last-data 0)
next-row-idx (aget last-data 1)
v (long (if (> row-idx last-row-idx)
(cond
(== row-idx next-row-idx)
(aget last-data 2)
(< row-idx next-row-idx)
-1
:else
(.invokePrim bin-search row-idx))
(.invokePrim bin-search row-idx)))]
(when (>= v 0)
(let [next-v (inc v)]
(aset last-data 0 row-idx)
(if (<= next-v dnidxs)
(do
(aset last-data 1 (.getLong local-indexes next-v))
(aset last-data 2 next-v))
(do
(aset last-data 1 Long/MAX_VALUE)
(aset last-data 2 -1))))
(.add res-ixs rel-idx)
(.add res-vals (.get data v)))))
nil rowidxs)
(SparseCol. res-ixs res-vals res-rc metadata nil nil)))
IMutList
(size [this] (.size (dt-proto/->buffer this)))
(get [this idx] (.get (dt-proto/->buffer this) idx))
(getLong [this idx] (.getLong (dt-proto/->buffer this) idx))
(getDouble [this idx] (.getDouble (dt-proto/->buffer this) idx))
(valAt [this idx] (.valAt (dt-proto/->buffer this) idx))
(valAt [this idx def-val] (.valAt (dt-proto/->buffer this) idx def-val))
(invoke [this idx] (.invoke (dt-proto/->buffer this) idx))
(invoke [this idx def-val] (.invoke (dt-proto/->buffer this) idx def-val))
(meta [this] (assoc metadata
:datatype (dt-proto/elemwise-datatype data)
:n-elems rc))
(withMeta [_this new-meta] (SparseCol. indexes data rc new-meta msng rdr))
(nth [this idx] (nth (dt-proto/->buffer this) idx))
(nth [this idx def-val] (nth (dt-proto/->buffer this) idx def-val))
;;should be the same as using hash-unorded-coll, effectively.
(hasheq [this] (.hasheq (dt-proto/->buffer this)))
(equiv [this o] (if (identical? this o) true (.equiv (dt-proto/->buffer this) o)))
(empty [this]
(let [ba (ArrayLists/toList (byte-array 0))]
(SparseCol. ba ba 0 {} nil nil)))
ITypedReduce
(reduce [this rfn acc] (reduce-scol this rfn acc 0 rc))
(parallelReduction [this init-val-fn rfn merge-fn options]
(.parallelReduction (dt-proto/->buffer this) init-val-fn rfn merge-fn options))
clojure.lang.IKVReduce
(kvreduce [this rfn acc]
(kv-reduce-scol this rfn acc 0 rc))
Object
(toString [this]
(let [format-str (if (> rc 20)
"#tech.v3.dataset.sparse-column<%s>%s\n%s\n[%s...]"
"#tech.v3.dataset.sparse-column<%s>%s\n%s\n[%s]")]
(format format-str
(name (dt/elemwise-datatype this))
[rc]
(ds-proto/column-name this)
(-> (dt-proto/sub-buffer this 0 (min 20 rc))
(dtype-pp/print-reader-data)))))
(hashCode [this] (.hasheq (dt-proto/->buffer this)))
(equals [this o] (.equiv this o)))
(defn- maybe-next-long
^long [^Iterator iter ^long not-found]
(if (.hasNext iter)
(long (.next iter))
not-found))
(defn- maybe-next
[^Iterator iter]
(when (.hasNext iter) (.next iter)))
(defn scol-iter
^Iterator [^SparseCol scol ^long sidx ^long eidx]
(let [src-idx-iter (.iterator ^Iterable (.-indexes scol))
src-val-iter (.iterator ^Iterable (.-data scol))
idx (long-array [sidx (maybe-next-long src-idx-iter Long/MAX_VALUE)])]
(reify Iterator
(hasNext [this] (< (aget idx 0) eidx))
(next [this]
(let [ii (aget idx 0)]
(aset idx 0 (inc ii))
(if (< ii eidx)
(let [iv (aget idx 1)]
(if (== iv ii)
(do
(aset idx 1 (maybe-next-long src-idx-iter Long/MAX_VALUE))
(maybe-next src-val-iter))
nil))
(throw (java.util.NoSuchElementException.))))))))
(defn- reduce-scol
[^SparseCol scol rfn acc sidx eidx]
(let [^Iterator si (scol-iter scol sidx eidx)]
(if (.hasNext si)
(loop [acc (rfn acc (.next si))]
(cond
(reduced? acc) (deref acc)
(.hasNext si) (recur (rfn acc (.next si)))
:else acc))
acc)))
(defn kv-reduce-scol
[^SparseCol scol rfn acc sidx eidx]
(let [^IMutList idxs (.-indexes scol)
cmp (case (dt/elemwise-datatype idxs)
:int8 it.unimi.dsi.fastutil.bytes.ByteComparators/NATURAL_COMPARATOR
:int16 it.unimi.dsi.fastutil.shorts.ShortComparators/NATURAL_COMPARATOR
:int32 it.unimi.dsi.fastutil.ints.IntComparators/NATURAL_COMPARATOR
it.unimi.dsi.fastutil.longs.LongComparators/NATURAL_COMPARATOR)
local-sidx (.binarySearch idxs sidx cmp)
local-eidx (.binarySearch idxs eidx cmp)
^IMutList idxs (.subList idxs local-sidx local-eidx)
^IMutList data (.subList ^IMutList (.-data scol) local-sidx local-eidx)
src-idx-iter (.iterator ^Iterable idxs)
src-val-iter (.iterator ^Iterable data)]
(if (.hasNext src-idx-iter)
(let [next-i (maybe-next-long src-idx-iter Long/MAX_VALUE)]
(loop [acc (rfn acc next-i (.next src-val-iter))]
(cond
(reduced? acc) (deref acc)
(.hasNext src-val-iter) (let [next-i (maybe-next-long src-idx-iter Long/MAX_VALUE)]
(recur (rfn acc next-i (.next src-val-iter))))
:else acc)))
acc)))
(defn ^:no-doc construct-sparse-col
^SparseCol [indexes data ^long rc metadata]
(SparseCol. indexes data rc metadata nil nil))
(defn- as-string-table
^StringTable [d] (when (and (instance? StringTable d)
(.-str->int ^StringTable d))
d))
(defn ->scol
^SparseCol [col]
(cond
(nil? col) (throw (RuntimeException. "Cannot create scol from nil."))
(instance? SparseCol col) col
:else
(let [rc (dt/ecount col)
valid-indexes (set/difference (bitmap/->bitmap (hamf/range rc)) (ds-proto/missing col))
valid-indexes (ArrayLists/toList
(cond
(<= rc Byte/MAX_VALUE)
(hamf/byte-array valid-indexes)
(<= rc Short/MAX_VALUE)
(hamf/short-array valid-indexes)
(<= rc Integer/MAX_VALUE)
(hamf/int-array valid-indexes)
:else
(hamf/long-array valid-indexes)))
col-dt (dt/elemwise-datatype col)
buf-rdr (dt/->reader (ds-proto/column-data col))
^IMutList data (if (identical? :string col-dt)
(if-let [^StringTable strt (as-string-table (ds-proto/column-data col))]
(StringTable. (.-int->str strt)
(.-str->int strt)
(int-list/dynamic-int-list (dt/ecount valid-indexes)))
(str-t/make-string-table))
(dt/make-list col-dt (.size valid-indexes)))]
(reduce (case (tech.v3.datatype.casting/simple-operation-space col-dt)
:int64 (fn [_acc ^long src-idx]
(.addLong data (.readLong buf-rdr src-idx)))
:float64 (fn [_acc ^long src-idx]
(.addDouble data (.readDouble buf-rdr src-idx)))
(fn [_acc ^long src-idx]
(.add data (.readObject buf-rdr src-idx))))
nil valid-indexes)
(SparseCol. valid-indexes data rc (meta col) nil nil))))
(defn scol->reader
(^Buffer [^SparseCol scol] (scol->reader scol 0 (.-rc scol)))
(^Buffer [^SparseCol scol ^long sidx ^long eidx]
(let [rc (- eidx sidx)
^IMutList data (.-data scol)
^clojure.lang.IFn$LL idx-bin-search (index-bin-search (.-indexes scol))]
(case (casting/simple-operation-space (dt/elemwise-datatype data))
:int64
(reify LongReader
(lsize [this] rc)
(readLong [this idx]
(let [vidx (.invokePrim idx-bin-search (+ idx sidx))]
(if (neg? vidx)
Long/MIN_VALUE
(.getLong data vidx))))
(readObject [this idx]
(let [vidx (.invokePrim idx-bin-search (+ idx sidx))]
(if (neg? vidx)
nil
(.get data vidx))))
(subBuffer [this ssidx seidx] (scol->reader scol (+ ssidx sidx) (+ seidx sidx)))
(reduce [this rfn acc] (reduce-scol scol rfn acc sidx eidx)))
:float64
(reify DoubleReader
(lsize [this] rc)
(readDouble [this idx]
(let [vidx (.invokePrim idx-bin-search (+ idx sidx))]
(if (neg? vidx)
Double/NaN
(.getDouble data vidx))))
(readObject [this idx]
(let [vidx (.invokePrim idx-bin-search (+ idx sidx))]
(if (neg? vidx)
nil
(.get data vidx))))
(subBuffer [this ssidx seidx] (scol->reader scol (+ ssidx sidx) (+ seidx sidx)))
(reduce [this rfn acc] (reduce-scol scol rfn acc sidx eidx)))
(reify ObjectReader
(lsize [this] rc)
(readObject [this idx]
(let [vidx (.invokePrim idx-bin-search (+ idx sidx))]
(if (neg? vidx)
nil
(.get data vidx))))
(subBuffer [this ssidx seidx] (scol->reader scol (+ ssidx sidx) (+ seidx sidx)))
(reduce [this rfn acc] (reduce-scol scol rfn acc sidx eidx)))))))
(defn sparse-rows
"Optimized implementation of ds/rows specifically assuming sparse columns"
(^Buffer [ds] (sparse-rows ds 0 (ds-proto/row-count ds)))
(^Buffer [ds ^long sidx ^long eidx]
(let [rc (- eidx sidx)
col-fns (mapv (fn [col]
(let [cbuf (dt/->buffer col)
cbuf (if (and (== sidx 0) (== eidx rc))
cbuf
(.subBuffer cbuf sidx eidx))
cname (ds-proto/column-name col)]
(fn [res ^long row-idx]
(when-let [rv (.readObject cbuf row-idx)]
(.put ^Map res cname rv))
res)))
(.values ^Map ds))]
(reify ObjectReader
(lsize [this] rc)
(readObject [this idx]
(reduce (fn [acc cfn]
(.invokePrim ^clojure.lang.IFn$OLO cfn acc idx))
(hamf/linked-hashmap) col-fns))
(subBuffer [this ssidx seidx]
(ChunkedList/sublistCheck ssidx seidx rc)
(sparse-rows ds (+ sidx ssidx) (+ sidx seidx)))))))
(defn ->sparse-ds
([ds] (->sparse-ds ds 0.5))
([ds missing-frac]
(let [rc (long (ds-proto/row-count ds))
missing-cutoff (long (* rc (double missing-frac)))]
(reduce (fn [ds col]
(if (>= (dt/ecount (ds-proto/missing col)) missing-cutoff)
(assoc ds (ds-proto/column-name col) (->scol col))
ds))
ds (.values ^java.util.Map ds)))))
(defn is-sparse? [col] (instance? SparseCol col))
(comment
(require '[tech.v3.dataset :as ds])
(require '[tech.v3.libs.arrow])
(def ds (ds/->dataset "../../nubank/dcm/mbrainz-big-final/6823d81f-ad0d-450e-90f9-efb1237575e8.arrow" {:key-fn keyword}))
(def test-col (ds :release/name))
(def test-scol (->scol test-col))
(def sds (ds->sparse-ds ds))
(def rr (ds/rows sds))
(def sr (sparse-rows sds))
(require '[criterium.core :as crit])
(crit/quick-bench (reduce (fn [acc _] (inc acc)) 0 (ds/rows sds))) ;;26ms
(crit/quick-bench (reduce (fn [acc _] (inc acc)) 0 (ds/rows ds))) ;;44ms
(crit/quick-bench (reduce (fn [acc _] (inc acc)) 0 (sparse-rows sds))) ;;19ms
(crit/quick-bench (-> (ds/select-rows ds (ds-proto/valid-rows (ds :release/name)))
(ds/remove-empty-columns)))
(crit/quick-bench (-> (ds/select-rows sds (ds-proto/valid-rows (sds :release/name)))
(ds/remove-empty-columns)))
(require '[clj-async-profiler.core :as prof])
(prof/profile (dotimes [idx 10]
(-> (ds/select-rows sds (ds-proto/valid-rows (sds :release/name)))
(ds/remove-empty-columns))))
(prof/serve-ui 8080)
(dotimes [idx 1000]
)
)
+301
View File
@@ -0,0 +1,301 @@
(ns tech.v3.dataset.io
(:require [tech.v3.io :as io]
[tech.v3.dataset.protocols :as ds-proto]
[tech.v3.datatype.errors :as errors]
[tech.v3.dataset.impl.dataset :as ds-impl]
[charred.api :as charred]
[tech.v3.dataset.io.mapseq-colmap :as parse-mapseq-colmap]
[tech.v3.dataset.readers :as readers])
(:import [java.io InputStream File Reader]
[java.util Map]))
(defn str->file-info
[^String file-str]
(let [file-str (.toLowerCase ^String file-str)
gzipped? (.endsWith file-str ".gz")
file-str (if gzipped?
(.substring file-str 0 (- (count file-str) 3))
file-str)
last-period (.lastIndexOf file-str ".")
file-type (if-not (== -1 last-period)
(keyword (.substring file-str (inc last-period)))
:unknown)]
{:gzipped? gzipped?
:file-type file-type}))
(defn input-stream-or-reader
[input-data]
(if (instance? Reader input-data)
input-data
(io/input-stream input-data)))
(defn wrap-stream-fn
[dataset gzipped? open-fn]
(with-open [^java.lang.AutoCloseable istream (if (instance? InputStream dataset)
(if gzipped?
(io/gzip-input-stream dataset)
dataset)
(if gzipped?
(io/gzip-input-stream dataset)
(input-stream-or-reader dataset)))]
(open-fn istream)))
(defmulti data->dataset
(fn [_data options]
(:file-type options)))
(defmethod data->dataset :default
[_data options]
(errors/throwf "Unrecognized read file type: %s"
(:file-type options)))
(defmethod data->dataset :json
[data options]
(with-open [is (if (get options :gzipped?)
(io/gzip-input-stream data)
(input-stream-or-reader data))]
;;Use mixed json parse profile as we don't care if the input is immutable or mutable and
;;mixed has the best performance.
(let [options (-> (assoc options :profile :mutable)
;; the charred read-json call has another interpretation of parser-fn
;; that is incompatible with the tmd one.
(dissoc :parser-fn))]
(->> (apply charred/read-json is (apply concat (seq options)))
(parse-mapseq-colmap/mapseq->dataset options)))))
(defmethod data->dataset :edn
[data options]
(with-open [is (if (get options :gzipped?)
(io/gzip-input-stream data)
(io/input-stream data))]
(->> (apply io/get-edn is (apply concat (seq options)))
(parse-mapseq-colmap/mapseq->dataset options))))
(defmulti dataset->data!
(fn [_ds _output options]
(:file-type options)))
(defmethod dataset->data! :json
[data output options]
(with-open [os (if (get options :gzipped?)
(io/gzip-output-stream! output)
(io/output-stream! output))]
(apply charred/write-json os
(readers/mapseq-reader data)
(apply concat (seq options)))))
(defmethod dataset->data! :edn
[data output options]
(with-open [os (if (get options :gzipped?)
(io/gzip-output-stream! output)
(io/output-stream! output))]
(apply io/put-edn! os
(readers/mapseq-reader data)
(apply concat (seq options)))))
(defmethod data->dataset :nippy
[data options]
((requiring-resolve 'tech.v3.dataset.io.nippy/load-nippy) data options))
(defmethod dataset->data! :nippy
[dataset output options]
((requiring-resolve 'tech.v3.dataset.io.nippy/write-nippy!) dataset output options))
(defmethod dataset->data! :default
[_ds _output options]
(errors/throwf "Unrecognized write file type: %s"
(:file-type options)))
(defn ->dataset
"Create a dataset from either csv/tsv or a sequence of maps.
* A `String` be interpreted as a file (or gzipped file if it
ends with .gz) of tsv or csv data. The system will attempt to autodetect if this
is csv or tsv and then engineering around detecting datatypes all of which can
be overridden.
* InputStreams have no file type and thus a `file-type` must be provided in the
options.
* A sequence of maps may be passed in in which case the first N maps are scanned in
order to derive the column datatypes before the actual columns are created.
Parquet, xlsx, and xls formats require that you require the appropriate libraries
which are `tech.v3.libs.parquet` for parquet, `tech.v3.libs.fastexcel` for xlsx,
and `tech.v3.libs.poi` for xls.
Arrow support is provided via the tech.v3.libs.Arrow namespace not via a file-type
overload as the Arrow project current has 3 different file types and it is not clear
what their final suffix will be or which of the three file types it will indicate.
Please see documentation in the `tech.v3.libs.arrow` namespace for further information
on Arrow file types.
Options:
- `:dataset-name` - set the name of the dataset.
- `:file-type` - Override filetype discovery mechanism for strings or force a particular
parser for an input stream. Note that parquet must have paths on disk
and cannot currently load from input stream. Acceptible file types are:
#{:csv :tsv :xlsx :xls :parquet}.
- `:gzipped?` - for file formats that support it, override autodetection and force
creation of a gzipped input stream as opposed to a normal input stream.
- `:column-allowlist` - either sequence of string column names or sequence of column
indices of columns to allowlist. This is preferred to `:column-whitelist`
- `:column-blocklist` - either sequence of string column names or sequence of column
indices of columns to blocklist. This is preferred to `:column-blacklist`
- `:num-rows` - Number of rows to read
- `:header-row?` - Defaults to true, indicates the first row is a header.
- `:key-fn` - function to be applied to column names. Typical use is:
`:key-fn keyword`.
- `:separator` - Add a character separator to the list of separators to auto-detect.
- `:csv-parser` - Implementation of univocity's AbstractParser to use. If not
provided a default permissive parser is used. This way you parse anything that
univocity supports (so flat files and such).
- `:bad-row-policy` - One of three options: :skip, :error, :carry-on. Defaults to
:carry-on. Some csv data has ragged rows and in this case we have several
options. If the option is :carry-on then we either create a new column or add
missing values for columns that had no data for that row.
- `:skip-bad-rows?` - Legacy option. Use :bad-row-policy.
- `:disable-comment-skipping?` - As default, the `#` character is recognised as a
line comment when found in the beginning of a line of text in a CSV file,
and the row will be ignored. Set `true` to disable this behavior.
- `:max-chars-per-column` - Defaults to 4096. Columns with more characters that this
will result in an exception.
- `:max-num-columns` - Defaults to 8192. CSV,TSV files with more columns than this
will fail to parse. For more information on this option, please visit:
https://github.com/uniVocity/univocity-parsers/issues/301
- `:text-temp-dir` - The temporary directory to use for file-backed text. Setting
this value to boolean 'false' turns off file backed text which is the default. If a
tech.v3.resource stack context is opened the file will be deleted when the context
closes else it will be deleted when the gc cleans up the dataset. A shutdown hook is
added as a last resort to ensure the file is cleaned up.
- `:n-initial-skip-rows` - Skip N rows initially. This currently may include the
header row. Works across both csv and spreadsheet datasets.
- `:parser-type` - Default parser to use if no parser-fn is specified for that column.
For csv files, the default parser type is `:string` which indicates a promotional
string parser. For sequences of maps, the default parser type is :object. It can
be useful in some contexts to use the `:string` parser with sequences of maps or
maps of columns.
- `:parser-fn` -
v - `keyword?` - all columns parsed to this datatype. For example:
`{:parser-fn :string}`
- `map?` - `{column-name parse-method}` parse each column with specified
`parse-method`.
The `parse-method` can be:
- `keyword?` - parse the specified column to this datatype. For example:
`{:parser-fn {:answer :boolean :id :int32}}`
- tuple - pair of `[datatype parse-data]` in which case container of type
`[datatype]` will be created. `parse-data` can be one of:
- `:relaxed?` - data will be parsed such that parse failures of the standard
parse functions do not stop the parsing process. :unparsed-values and
:unparsed-indexes are available in the metadata of the column that tell
you the values that failed to parse and their respective indexes.
- `fn?` - function from str-> one of `:tech.v3.dataset/missing`,
`:tech.v3.dataset/parse-failure`, or the parsed value.
Exceptions here always kill the parse process. :missing will get marked
in the missing indexes, and :parse-failure will result in the index being
added to missing, the unparsed the column's :unparsed-values and
:unparsed-indexes will be updated.
- `string?` - for datetime types, this will turned into a DateTimeFormatter via
DateTimeFormatter/ofPattern. For `:text` you can specify the backing file
to use.
- `DateTimeFormatter` - use with the appropriate temporal parse static function
to parse the value.
- `map?` - the header-name-or-idx is used to lookup value. If not nil, then
value can be any of the above options. Else the default column parser
is used.
Returns a new dataset"
([dataset
{:keys [table-name dataset-name]
:as options}]
(let [dataset (if (instance? File dataset)
(.toString ^File dataset)
dataset)
;;Unify table-name and dataset name with dataset-name
;;taking precedence.
dataset-name (or dataset-name
table-name
(when (string? dataset)
dataset))
options (if dataset-name
(assoc options :dataset-name dataset-name)
options)
dataset
(cond
(ds-proto/is-dataset? dataset)
dataset
(or (string? dataset)
(instance? InputStream dataset)
(instance? Reader dataset))
(let [options (if (string? dataset)
(merge (str->file-info dataset)
options)
options)]
(data->dataset dataset options))
(instance? Map dataset)
(parse-mapseq-colmap/column-map->dataset options dataset)
:else
(let [rv (parse-mapseq-colmap/mapseq->dataset options dataset)]
(if (tech.v3.dataset.impl.dataset/dataset? rv)
rv
(ds-impl/empty-dataset))))]
(if dataset-name
(vary-meta dataset assoc :name dataset-name)
dataset)))
([dataset]
(->dataset dataset {})))
(defn ->>dataset
"Please see documentation of ->dataset. Options are the same."
([options dataset]
(->dataset dataset options))
([dataset]
(->dataset dataset)))
(defn write!
"Write a dataset out to a file. Supported forms are:
```clojure
(ds/write! test-ds \"test.csv\")
(ds/write! test-ds \"test.tsv\")
(ds/write! test-ds \"test.tsv.gz\")
(ds/write! test-ds \"test.nippy\")
(ds/write! test-ds out-stream)
```
Options:
* `:max-chars-per-column` - csv,tsv specific, defaults to 65536 - values longer than this will
cause an exception during serialization.
* `:max-num-columns` - csv,tsv specific, defaults to 8192 - If the dataset has more than this number of
columns an exception will be thrown during serialization.
* `:quoted-columns` - csv specific - sequence of columns names that you would like to always have quoted.
* `:file-type` - Manually specify the file type. This is usually inferred from the filename but if you
pass in an output stream then you will need to specify the file type.
* `:headers?` - if csv headers are written, defaults to true."
([dataset output-path options]
(let [options (merge (when (string? output-path)
(str->file-info output-path))
options)]
(dataset->data! dataset output-path options)))
([dataset output-path]
(write! dataset output-path nil)))
@@ -0,0 +1,568 @@
(ns tech.v3.dataset.io.column-parsers
"Per-column parsers."
(:require [tech.v3.dataset.io.datetime :as parse-dt]
[tech.v3.dataset.impl.column-base :as column-base]
[tech.v3.dataset.protocols :as ds-proto]
[tech.v3.datatype.packing :as packing]
[tech.v3.datatype :as dtype]
[tech.v3.datatype.casting :as casting]
[tech.v3.datatype.bitmap :as bitmap]
[tech.v3.datatype.errors :as errors]
[tech.v3.datatype.argops :as argops]
[tech.v3.datatype.datetime :as dtype-dt]
[tech.v3.datatype.protocols :as dtype-proto]
[ham-fisted.api :as hamf])
(:import [java.util UUID List]
[tech.v3.dataset Text]
[tech.v3.datatype Buffer]
[ham_fisted IMutList Casts]
[org.roaringbitmap RoaringBitmap]
[clojure.lang IFn Indexed]
[java.time.format DateTimeFormatter]))
(set! *warn-on-reflection* true)
(set! *unchecked-math* :warn-on-boxed)
;;For sequences of maps and spreadsheets
;; 1. No parser specified - do not change datatype, do not change input value.
;; Promote container as necessary; promotion requires a static cast when possible or
;; all the way to object.
;; 2. datatype,parser specified - if input value is correct datatype, add to container.
;; if input is incorrect datatype, call parse fn.
;;For string-based pathways
;; 1. If no parser is specified, try-parse where ::parse-failure means
;; 2. If parse is specified, use parse if
(def parse-failure :tech.v3.dataset/parse-failure)
(def missing :tech.v3.dataset/missing)
(defmacro make-safe-parser
[name parse-code]
`(fn ~name [~'v]
(try
~parse-code
(catch Throwable _e#
parse-failure))))
(def default-coercers
(merge
{:bool #(if (string? %)
(let [^String data %]
(cond
(.equals "true" data) true
(.equals "false" data) false
:else parse-failure))
(boolean %))
:boolean #(if (string? %)
(let [^String data %]
(cond
(or (.equalsIgnoreCase "t" data)
(.equalsIgnoreCase "y" data)
(.equalsIgnoreCase "yes" data)
(.equalsIgnoreCase "True" data)
(.equalsIgnoreCase "positive" data))
true
(or (.equalsIgnoreCase "f" data)
(.equalsIgnoreCase "n" data)
(.equalsIgnoreCase "no" data)
(.equalsIgnoreCase "false" data)
(.equalsIgnoreCase "negative" data))
false
:else
parse-failure))
(boolean %))
:int16 (make-safe-parser int16
(if (string? v)
(Short/parseShort v)
(short v)))
:int32 (make-safe-parser int32 (if (string? v)
(Integer/parseInt v)
(int v)))
:int64 (make-safe-parser int64 (if (string? v)
(Long/parseLong v)
(long v)))
:float32 (make-safe-parser float32 (if (string? v)
(let [fval (Float/parseFloat v)]
(if (Float/isNaN fval)
missing
fval))
(float v)))
:float64 (make-safe-parser float64 (if (string? v)
(let [dval (Double/parseDouble v)]
(if (Double/isNaN dval)
missing
dval))
(double v)))
:uuid (make-safe-parser uuid (if (string? v)
(UUID/fromString v)
(if (instance? UUID v)
v
parse-failure)))
:keyword #(if-let [retval (keyword %)]
retval
parse-failure)
:symbol #(if-let [retval (symbol %)]
retval
parse-failure)
:string #(let [str-val (if (string? %)
%
(str %))]
(if (< (count str-val) 1024)
str-val
parse-failure))
:text #(Text. (str %))}
(->> parse-dt/datatype->general-parse-fn-map
(mapcat (fn [[k vv]]
(let [unpacked-parser (make-safe-parser packed
(if (= k (dtype/elemwise-datatype v))
v
(vv v)))]
;;packing is now done at the container level.
(if (packing/unpacked-datatype? k)
[[k unpacked-parser]
[(packing/pack-datatype k) unpacked-parser]]
[[k unpacked-parser]]))))
(into {}))))
(definterface PParser
(addValue [^long idx value])
(finalize [^long rowcount]))
(defn add-value!
[^PParser p ^long idx value]
(.addValue p idx value))
(defn finalize!
[^PParser p ^long rowcount]
(.finalize p rowcount))
(defn add-missing-values!
^long [^IMutList container ^RoaringBitmap missing
missing-value ^long idx]
(let [n-elems (.size container)]
(when (< n-elems idx)
(.add missing (long n-elems) idx)
(.add container n-elems (- idx n-elems) missing-value))
(- idx n-elems)))
(defn finalize-parser-data!
[container missing failed-values failed-indexes
missing-value rowcount]
(add-missing-values! container missing missing-value rowcount)
(merge
#:tech.v3.dataset{:data (or (dtype/as-array-buffer container)
(dtype/as-native-buffer container)
(if (instance? clojure.lang.IDeref container)
@container
container))
:missing missing
:force-datatype? true}
(when (and failed-values
(not= 0 (dtype/ecount failed-values)))
#:tech.v3.dataset{:metadata {:unparsed-data failed-values
:unparsed-indexes failed-indexes}})))
(defn- missing-value?
"Is this a missing value coming from a CSV file"
[value disable-na-as-missing?]
;;fastpath for numbers
(cond
(or (instance? Double value) (instance? Float value))
(Double/isNaN (Casts/doubleCast value))
(not (instance? Number value))
(or (nil? value)
(identical? value :tech.v3.dataset/missing)
(and (string? value)
(or (.equals "" value)
(and (not disable-na-as-missing?)
(.equalsIgnoreCase ^String value "na")))))))
(deftype FixedTypeParser [^IMutList container
container-dtype
missing-value parse-fn
^RoaringBitmap missing
^IMutList failed-values
^RoaringBitmap failed-indexes
column-name
^:unsynchronized-mutable ^long max-idx
disable-na-as-missing?]
dtype-proto/PECount
(ecount [_this] (inc max-idx))
Indexed
(nth [this idx] (.nth this idx nil))
(nth [this idx dv]
(let [cec (.size container)]
(if (or (>= idx cec)
(.contains missing idx))
nil
(.get container idx))))
PParser
(addValue [_this idx value]
(let [idx (unchecked-long idx)]
(set! max-idx (max idx max-idx))
;;First pass is to potentially parse the value. It could already
;;be in the space of the container or it could require the parse-fn
;;to make it.
(let [parsed-value (cond
(missing-value? value disable-na-as-missing?)
:tech.v3.dataset/missing
(and (identical? (dtype/datatype value) container-dtype)
(not (instance? String value)))
value
:else
(parse-fn value))]
(cond
;;ignore it; we will add missing when we see the first valid value
(identical? :tech.v3.dataset/missing parsed-value)
nil
;;Record the original incoming value if we are parsing in relaxed mode.
(identical? :tech.v3.dataset/parse-failure parsed-value)
(if failed-values
(do
(.add failed-values value)
(.add failed-indexes (unchecked-int idx)))
(errors/throwf "Failed to parse value %s as datatype %s on row %d"
value container-dtype idx))
:else
(do
(add-missing-values! container missing missing-value idx)
(.add container parsed-value))))))
(finalize [_p rowcount]
(finalize-parser-data! container missing failed-values failed-indexes
missing-value rowcount)))
(defn- find-fixed-parser
[kwd]
(if (= kwd :string)
str
(if-let [retval (get default-coercers kwd)]
retval
identity)))
(defn- datetime-formatter-parser-fn
[parser-datatype formatter]
(let [unpacked-datatype (packing/unpack-datatype parser-datatype)
parser-fn (parse-dt/datetime-formatter-parse-str-fn
unpacked-datatype formatter)]
[(make-safe-parser datetime (parser-fn v)) false]))
(defn parser-entry->parser-tuple
[parser-kwd]
(if (vector? parser-kwd)
(do
(assert (= 2 (count parser-kwd)))
(let [[parser-datatype parser-fn] parser-kwd]
(assert (keyword? parser-datatype))
[parser-datatype
(cond
(= :relaxed? parser-fn)
[(find-fixed-parser parser-datatype) true]
(instance? IFn parser-fn)
[parser-fn true]
(and (dtype-dt/datetime-datatype? parser-datatype)
(string? parser-fn))
(datetime-formatter-parser-fn parser-datatype
(DateTimeFormatter/ofPattern parser-fn))
(and (dtype-dt/datetime-datatype? parser-datatype)
(instance? DateTimeFormatter parser-fn))
(datetime-formatter-parser-fn parser-datatype parser-fn)
(= :text parser-datatype)
[(find-fixed-parser parser-datatype)]
:else
(errors/throwf "Unrecoginzed parser fn type: %s" (type parser-fn)))]))
[parser-kwd [(find-fixed-parser parser-kwd) false]]))
(defn make-fixed-parser
^PParser [cname parser-kwd options]
(let [[dtype [parse-fn relaxed?]] (parser-entry->parser-tuple parser-kwd)
[failed-values failed-indexes] (when relaxed?
[(dtype/make-container :list :object 0)
(bitmap/->bitmap)])
container (column-base/make-container dtype options)
missing-value (column-base/datatype->missing-value dtype)
missing (bitmap/->bitmap)]
(FixedTypeParser. container dtype missing-value parse-fn
missing failed-values failed-indexes
cname -1
(get options :disable-na-as-missing?))))
(defn parser-kwd-list->parser-tuples
[kwd-list]
(mapv parser-entry->parser-tuple kwd-list))
(def default-parser-datatype-sequence
[:bool :int16 :int32 :int64 :float64 :uuid
:packed-duration :packed-local-date
:zoned-date-time :string :text :boolean])
(defn- promote-container
^IMutList [old-container ^RoaringBitmap missing new-dtype options]
(let [n-elems (dtype/ecount old-container)
container (column-base/make-container new-dtype options)
missing-value (column-base/datatype->missing-value new-dtype)
;;Ensure we unpack a container if we have to promote it.
old-container (packing/unpack old-container)]
(dotimes [idx n-elems]
(if (.contains missing idx)
(.add container missing-value)
(.add container (casting/cast
(old-container idx)
new-dtype))))
container))
(defn- find-next-parser
^long [value container-dtype ^List promotion-list]
(let [start-idx (argops/index-of (mapv first promotion-list) container-dtype)
n-elems (.size promotion-list)]
(if (== start-idx -1)
-1
(long (loop [idx (inc start-idx)]
(if (< idx n-elems)
(let [[_container-datatype parser-fn]
(.get promotion-list idx)
parsed-value (parser-fn value)]
(if (= parsed-value parse-failure)
(recur (inc idx))
idx))
-1))))))
(defn- resolve-parser-index
"Resolve the next parser index returning a tuple of [parser-datatype new-parser-fn]"
[next-idx ^List container container-dtype missing ^List promotion-list]
(let [next-idx (long next-idx)]
(if (== -1 next-idx)
[:object nil]
(let [n-missing (dtype/ecount missing)
n-valid (- (dtype/ecount container) n-missing)
parser-data (.get promotion-list next-idx)
parser-datatype (first parser-data)]
;;Figure out if our promotion process will result in a valid container.
(cond
(== 0 n-valid)
parser-data
(and (or (identical? :bool container-dtype)
(identical? :boolean container-dtype)
(casting/numeric-type? container-dtype))
(casting/numeric-type? (packing/unpack-datatype parser-datatype)))
parser-data
:else
[:string (default-coercers :string)])))))
(defn- fast-dtype
[value]
(if (string? value)
:string
(dtype/datatype value)))
(deftype PromotionalStringParser [^{:unsynchronized-mutable true
:tag IMutList} container
^{:unsynchronized-mutable true} container-dtype
^{:unsynchronized-mutable true} missing-value
^{:unsynchronized-mutable true} parse-fn
^RoaringBitmap missing
;;List of datatype,parser-fn tuples
^List promotion-list
column-name
^:unsynchronized-mutable ^long last-idx
options
disable-na-as-missing?]
dtype-proto/PECount
(ecount [_this] (inc last-idx))
Indexed
(nth [this idx] (.nth this idx nil))
(nth [this idx dv]
(let [cec (.size container)]
(if (or (>= idx cec)
(.contains missing idx))
nil
(.get container idx))))
PParser
(addValue [_p idx value]
(let [parsed-value
(cond
(missing-value? value disable-na-as-missing?)
:tech.v3.dataset/missing
(identical? (fast-dtype value) container-dtype)
value
;;If we have a function to parse the data
parse-fn
(let [parsed-value (parse-fn value)]
;;If the value parsed successfully
(if (not (identical? :tech.v3.dataset/parse-failure parsed-value))
parsed-value
;;else Perform column promotion
(let [next-idx (find-next-parser value container-dtype promotion-list)
[parser-datatype new-parser-fn]
(resolve-parser-index next-idx container container-dtype
missing promotion-list)
parsed-value (if new-parser-fn
(new-parser-fn value)
value)
new-container (promote-container container missing
parser-datatype
options)
new-missing-value (column-base/datatype->missing-value
parser-datatype)]
;;Update member variables based on new parser
(set! container new-container)
(set! container-dtype parser-datatype)
(set! missing-value new-missing-value)
(set! parse-fn new-parser-fn)
parsed-value)))
;;Else, nothing to parse with, just return string value
:else
value)]
(cond
;;Promotional parsers should not have parse failures.
(identical? :tech.v3.dataset/parse-failure parsed-value)
(errors/throwf "Parse failure detected in promotional parser - Please file issue.")
(identical? :tech.v3.dataset/missing parsed-value)
nil ;;Skip, will add missing on next valid value
:else
(do
(when (> (- idx last-idx) 1)
(add-missing-values! container missing missing-value idx))
(set! last-idx idx)
(try
(.add container parsed-value)
(catch Exception e
(throw (RuntimeException. (str "Parse failure of datatype: "
(dtype/elemwise-datatype container))
e))))))))
(finalize [_p rowcount]
(finalize-parser-data! container missing nil nil missing-value rowcount)))
(defn promotional-string-parser
(^PParser [column-name parser-datatype-sequence options]
(let [first-dtype (first parser-datatype-sequence)]
(PromotionalStringParser. (column-base/make-container
(if (= :bool first-dtype)
:boolean
first-dtype)
options)
first-dtype
false
(default-coercers first-dtype)
(bitmap/->bitmap)
(mapv (juxt identity default-coercers)
parser-datatype-sequence)
column-name
-1
options
(get options :disable-na-as-missing?))))
(^PParser [column-name options]
(promotional-string-parser column-name default-parser-datatype-sequence options)))
(deftype PromotionalObjectParser [^{:unsynchronized-mutable true
:tag IMutList} container
^{:unsynchronized-mutable true} container-dtype
^{:unsynchronized-mutable true} missing-value
^RoaringBitmap missing
column-name
^:unsynchronized-mutable ^long last-idx
^:unsynchronized-mutable ^long max-idx
^:unsynchronized-mutable ^long mc
disable-na-as-missing?
options]
dtype-proto/PECount
(ecount [_this] (inc max-idx))
Indexed
(nth [this idx] (.nth this idx nil))
(nth [this idx dv]
(let [cec (.size container)]
(if (or (>= idx cec)
(.contains missing idx))
nil
(.get container idx))))
ds-proto/PClearable
(ds-clear [this]
(.clear container)
(.clear missing)
(set! last-idx -1)
(set! max-idx -1)
(set! mc 0))
PParser
(addValue [_p idx value]
(set! max-idx idx)
(when-not (missing-value? value disable-na-as-missing?)
(let [val-dtype (fast-dtype value)]
;;setup container for new data
(when-not (identical? container-dtype val-dtype)
(let [;;Avoid the pack call if possible
packed-dtype (packing/pack-datatype val-dtype)
container-ecount (.size container)
logical-ecount (- container-ecount mc)]
;;Setup container
(if (== 0 logical-ecount)
(do
(set! container (column-base/make-container (if (identical? container-dtype :boolean)
packed-dtype
(casting/widest-datatype container-dtype
packed-dtype))
options))
(set! container-dtype val-dtype)
(set! missing-value (column-base/datatype->missing-value packed-dtype)))
;;boolean present a problem here. We generally want to keep them as booleans
;;and not promote them to full numbers.
(let [widest-datatype (if (identical? val-dtype :boolean)
(if (identical? container-dtype :boolean)
:boolean
:object)
(casting/widest-datatype
(packing/unpack-datatype container-dtype)
val-dtype))]
(when-not (= widest-datatype container-dtype)
(let [new-container (promote-container container
missing widest-datatype
options)]
(set! container new-container)
(set! container-dtype widest-datatype)
(set! missing-value (column-base/datatype->missing-value
widest-datatype))))))))
(when (> (- idx last-idx) 1)
(set! mc (+ mc (add-missing-values! container missing missing-value idx))))
(set! last-idx idx)
(.add container value))))
(finalize [_p rowcount]
(finalize-parser-data! container missing nil nil
missing-value rowcount)))
(defn promotional-object-parser
^PParser [column-name options]
(PromotionalObjectParser. (dtype/make-list :boolean)
:boolean
false
(bitmap/->bitmap)
column-name
-1
-1
0
(get options :disable-na-as-missing?)
options))
+151
View File
@@ -0,0 +1,151 @@
(ns tech.v3.dataset.io.context
(:require [clojure.string :as str]
[tech.v3.dataset.io.column-parsers :as column-parsers]
[tech.v3.dataset.protocols :as ds-proto]
[tech.v3.dataset.utils :as utils]
[tech.v3.datatype :as dtype]
[tech.v3.dataset.impl.dataset :as ds-impl]
[tech.v3.dataset.impl.column :as col-impl]
[ham-fisted.api :as hamf])
(:import [java.util.function Function]
[java.util Map Arrays]
[tech.v3.datatype ObjectBuffer ArrayHelpers]
[tech.v3.dataset.impl.dataset Dataset]))
(set! *warn-on-reflection* true)
(defn options->parser-fn
"Given the (beast of an) options map used for parsing, run through it
and created the specific parse context. A parse context is a function
that produces a column parser for a given column name or index.
parse-type is either :string or :object."
[options parse-type]
(let [default-parse-fn (case (get options :parser-type parse-type)
:object column-parsers/promotional-object-parser
:string column-parsers/promotional-string-parser
nil (constantly nil))
key-fn (or (:key-fn options) identity)
parser-descriptor (:parser-fn options)]
(fn [cname-or-index]
(let [cname (if (number? cname-or-index)
(long cname-or-index)
(key-fn cname-or-index))]
(cond
(nil? parser-descriptor)
(default-parse-fn cname options)
(map? parser-descriptor)
(if-let [col-parser-desc (or (get parser-descriptor cname)
(get parser-descriptor cname-or-index))]
(column-parsers/make-fixed-parser cname col-parser-desc options)
(default-parse-fn cname options))
:else
(column-parsers/make-fixed-parser cname parser-descriptor options))))))
(defn- make-colname
[rd]
(if (number? rd)
(str "column-" rd)
rd))
(deftype ObjectArrayList [^{:unsynchronized-mutable true
:tag 'objects} data
^{:unsynchronized-mutable true
:tag 'long} datalen]
ObjectBuffer
(lsize [_this] (alength ^objects data))
(writeObject [_this idx value]
(when (>= idx datalen)
(let [old-len datalen
new-len (* 2 idx)
new-data (Arrays/copyOf ^objects data new-len)]
(set! data new-data)
(set! datalen (alength new-data))))
(ArrayHelpers/aset ^objects data idx value))
(readObject [_this idx]
(when (< idx datalen)
(aget ^objects data idx))))
(defrecord ^:private ParseRecord [^long col-idx
column-name
column-parser])
(defn options->col-idx-parse-context
"Given an option map and a parse type, return a map of parsers
and a function to get a parser from a given column idx.
returns:
{:parsers - parsers
:col-idx->parser - given a column idx, get a parser. Mutates parsers."
[{:keys [ensure-unique-column-names? unique-column-name-fn]
:or {unique-column-name-fn utils/rand-str-column-name-postfix}
:as options}
parse-type col-idx->colname]
(let [parse-context (options->parser-fn options parse-type)
parsers (ObjectArrayList. (object-array 16) 16)
key-fn (:key-fn options identity)
colname->idx (java.util.HashMap.)
colparser-compute-fn (reify Function
(apply [this col-idx]
(let [colname (col-idx->colname col-idx)
colname (cond
(nil? colname) (make-colname col-idx)
(string? colname) (if (empty? colname)
(make-colname col-idx)
(utils/remove-zero-width-spaces colname))
(sequential? colname) (if (empty? colname)
(make-colname colname)
colname)
:else colname)
colname (if (and ensure-unique-column-names?
(get colname->idx colname))
(unique-column-name-fn col-idx colname)
colname)]
(when (.containsKey colname->idx colname)
(let [prev-idx (.get colname->idx colname)]
(throw (RuntimeException. (format "Duplicate colname detected: \"%s\" is used on columns %d, %d"
colname prev-idx col-idx)))))
(.put colname->idx colname col-idx)
(ParseRecord. col-idx (key-fn colname) (parse-context colname)))))
col-idx->parser (fn cidx->parser [^long col-idx]
(if-let [^ParseRecord parser (.readObject parsers col-idx)]
(.-column-parser parser)
(let [^ParseRecord parser (.apply colparser-compute-fn col-idx)]
(.writeObject parsers col-idx parser)
(.-column-parser parser))))]
{:parsers parsers
:col-idx->parser col-idx->parser}))
(defn parsers->dataset
"Parsers are a sequence of maps of :column-name :column-parser; :columns-parser is an object that implement
tech.v3.dataset.io.column-parser.PParser/finalize! which returns the column data in map form
preferrably with :force-datatype? to avoid rescanning any data."
([options parsers row-count]
(let [row-count (long row-count)
columns (hamf/mut-list)
cmap (hamf/mut-map)
dname (or (get options :name)
(get options :dataset-name)
"_unnamed")]
(transduce (remove nil?)
(fn
([] nil)
([acc] acc)
([acc ^Map parse-map]
(let [column-name (.get parse-map :column-name)
column-parser (.get parse-map :column-parser)]
(.put cmap column-name (.size columns))
(.add columns (-> (assoc (column-parsers/finalize! column-parser row-count)
:tech.v3.dataset/name column-name)
(col-impl/new-column))))))
(if (instance? Map parsers)
(vals parsers)
parsers))
(Dataset. (persistent! columns) (persistent! cmap) {:name dname} 0 0)))
([options parsers]
(parsers->dataset options parsers
(apply max 0 (map (comp dtype/ecount :column-parser)
parsers)))))
+333
View File
@@ -0,0 +1,333 @@
(ns tech.v3.dataset.io.csv
"CSV parsing based on [charred.api/read-csv](https://cnuernber.github.io/charred/)."
(:require [charred.api :as charred]
[charred.bulk :as bulk]
[charred.coerce :as coerce]
[tech.v3.dataset.io :as ds-io]
[tech.v3.parallel.for :as pfor]
[tech.v3.datatype :as dtype]
[tech.v3.io :as io]
[tech.v3.datatype.errors :as errors]
[tech.v3.dataset.io.column-parsers :as column-parsers]
[tech.v3.dataset.io.context :as parse-context]
[tech.v3.dataset.impl.dataset :as ds-impl]
[tech.v3.dataset.protocols :as ds-proto]
[ham-fisted.api :as hamf]
[ham-fisted.reduce :as hamf-rf]
[ham-fisted.lazy-noncaching :as lznc])
(:import [tech.v3.datatype ArrayHelpers]
[clojure.lang IReduceInit]
[java.lang AutoCloseable]
[java.util Iterator]))
(set! *warn-on-reflection* true)
(set! *unchecked-math* :warn-on-boxed)
(defn rows->dataset-fn
"Create an efficiently callable function to parse row-batches into datasets.
Returns function from row-iter->dataset. Options passed in here are the
same as ->dataset."
[{:keys [header-row?]
:or {header-row? true}
:as options}]
(fn [row-iter]
(let [row-iter (coerce/->iterator row-iter)
header-row (if (and header-row? (.hasNext row-iter))
(vec (.next row-iter))
[])
n-header-cols (count header-row)
{:keys [parsers col-idx->parser]}
(parse-context/options->col-idx-parse-context
options :string (fn [^long col-idx]
(when (< col-idx n-header-cols)
(header-row col-idx))))
n-records (get options :n-records (get options :num-rows))]
;;initialize parsers so if there are no more rows we get a dataset with
;;at least column names
(dotimes [idx n-header-cols]
(col-idx->parser idx))
(if n-records
(let [n-records (long n-records)]
(loop [continue? (.hasNext row-iter)
row-idx 0]
(if continue?
(do
(reduce (hamf-rf/indexed-accum
acc col-idx field
(-> (col-idx->parser col-idx)
(column-parsers/add-value! row-idx field)))
nil
(.next row-iter))
(recur (and (.hasNext row-iter)
(< (inc row-idx) n-records))
(unchecked-inc row-idx)))
(parse-context/parsers->dataset options parsers))))
(loop [continue? (.hasNext row-iter)
row-idx 0]
(if continue?
(do
(reduce (hamf-rf/indexed-accum
acc col-idx field
(-> (col-idx->parser col-idx)
(column-parsers/add-value! row-idx field)))
nil
(.next row-iter))
(recur (.hasNext row-iter) (unchecked-inc row-idx)))
(parse-context/parsers->dataset options parsers)))))))
(defn csv->dataset-seq
"Read a csv into a lazy sequence of datasets. All options of [[tech.v3.dataset/->dataset]]
are suppored aside from `:n-initial-skip-rows` with an additional option of
`:batch-size` which defaults to 128000.
Options are passed through to
[charred.bulk/batch-csv-rows](https://cnuernber.github.io/charred/charred.bulk.html#var-batch-csv-rows)
renaming where necessary. This method defaults to using a load thread - see above method
for more options. To disable the load thread use `:csv-load-thread-name nil`.
When using multithreaded loading, options are also passed through to
[ham-fisted.api/pmap-opts](https://cnuernber.github.io/ham-fisted/ham-fisted.api.html#var-pmap-opts)
so you can change the amount of `:n-lookahead` the pmap opteration uses when submitting jobs
to the thread pool. By default this is set to 4 to decrease possible OOM sitations.
The input will only be closed once the entire sequence is realized.
Options:
* :load-tfn - dataset->x transformation function to be performed on
in the same thread context just after dataset is loaded. Doing some operations
in this transform function can be considerably more efficient than only loading
the dataset when using multithreaded loading."
[input & [options]]
(let [options (merge {:profile :mutable
:csv-load-thread-name "TMD CSV load thread"}
options)
load-fn (rows->dataset-fn options)
threaded? (boolean (get options :csv-load-thread-name))
batches (->> (charred/read-csv-supplier (ds-io/input-stream-or-reader input) options)
(bulk/batch-csv-rows (get options :batch-size 128000) options))
load-fn (if-let [tfn (get options :load-tfn)]
#(tfn (load-fn %))
load-fn)]
(if threaded?
(hamf/pmap-opts (merge {:n-lookahead (get options :csv-load-queue-size 4)}
options)
load-fn
batches)
(lznc/map load-fn batches))))
(defn csv->dataset
"Read a csv into a dataset. Same options as [[tech.v3.dataset/->dataset]]."
[input & [options]]
(let [s (charred/read-csv-supplier (ds-io/input-stream-or-reader input)
(merge {:profile :mutable} options))
iter (if-let [n-initial-skip-rows (get options :n-initial-skip-rows)]
(let [iter (coerce/->iterator s)]
(dotimes [idx n-initial-skip-rows]
(when (.hasNext iter) (.next iter)))
iter)
s)
retval ((rows->dataset-fn options) iter)]
(when (instance? AutoCloseable s)
(.close ^AutoCloseable s))
retval))
(defn- load-csv
[data options]
(ds-io/wrap-stream-fn
data (:gzipped? options)
#(csv->dataset %1 options)))
(defmethod ds-io/data->dataset :csv
[data options]
(load-csv data options))
(defmethod ds-io/data->dataset :tsv
[data options]
(load-csv data (merge {:separator \tab} options)))
(defmethod ds-io/data->dataset :txt
[data options]
(load-csv data options))
(defn rows->csv!
"Given an something convertible to an output stream, an optional set of headers
as string arrays, and a sequence of string arrows, write a CSV or a TSV file.
Options:
* `:separator` - Defaults to \tab.
* `:quote` - Default \\\"
* `:quote?` A predicate function which determines if a string should be quoted.
Defaults to quoting only when necessary. May also be the the value 'true' in which
case every field is quoted.
* :newline - `:lf` (default) or `:cr+lf`.
* :close-writer? - defaults to true. When true, close writer when finished."
([output headers rows]
(rows->csv! output headers rows {}))
([output headers rows
{:keys [separator]
:or {separator \tab}
:as options}]
(apply charred/write-csv (io/writer! output)
(if headers (lznc/concat [headers] rows) rows)
(apply concat (seq (merge {:close-writer? true
:separator separator}
options))))))
(defn- data->string
^String [data-item]
(when-not (nil? data-item)
(cond
(string? data-item) data-item
(keyword? data-item) (name data-item)
(symbol? data-item) (name data-item)
:else (.toString ^Object data-item))))
(defn- write-csv!
"Write a dataset to a tsv or csv output stream. Closes output if a stream
is passed in. File output format will be inferred if output is a string -
- .csv, .tsv - switches between tsv, csv. Tsv is the default.
- *.gz - write to a gzipped stream.
options:
* `:separator` - in case output isn't a string, you can use either \\, or \\tab to switch
between csv or tsv output respectively.
* `:headers?` - if csv headers are written, defaults to true.
* `:gzipped?` - When true, use a gizpped output stream.
* `:file-type` - `:csv` or `:tsv`."
([ds output options]
(let [{:keys [gzipped? file-type]}
(merge
(when (string? output)
(ds-io/str->file-info output))
options)
headers (when (get options :headers? true)
(map (comp data->string :name meta) (vals ds)))
rows (->> (ds-proto/rowvecs ds nil)
(lznc/map #(lznc/map data->string %)))
tsv? (or (= file-type :tsv) (= \tab (:separator options)))
output (if gzipped?
(io/gzip-output-stream! output)
output)]
(rows->csv! output headers rows (assoc options :separator (if tsv? \tab \,)))))
([ds output]
(write-csv! ds output {})))
(defmethod ds-io/dataset->data! :csv
[dataset output options]
(write-csv! dataset output options))
(defmethod ds-io/dataset->data! :tsv
[dataset output options]
(write-csv! dataset output (assoc options :separator \tab)))
(defmethod ds-io/dataset->data! :txt
[dataset output options]
(write-csv! dataset output (assoc options :separator \tab)))
(comment
(do
(import '[java.util.zip ZipFile ZipInputStream])
(import '[java.util.concurrent ArrayBlockingQueue])
(require '[charred.api :as charred])
(require '[charred.bulk :as bulk])
(defn- abq->iterable
[^ArrayBlockingQueue abq]
(reify
Iterable
(iterator [this]
(println "iterator requested")
(let [nv* (volatile! (.take abq))]
(reify
java.util.Iterator
(hasNext [this] (not= ::finished @nv*))
(next [this]
(let [nv @nv*]
(vreset! nv* (.take abq))
nv)))))
IReduceInit
(reduce [this rfn acc]
(println "reduce requested")
(loop [acc acc
nv (.take abq)]
(if (or (identical? ::finished nv)
(not (reduced? acc)))
(recur (rfn acc nv) (.take abq))
(if (reduced? acc)
@acc
acc))))))
(defn load-zip
[fname]
(let [zf (ZipInputStream. (io/input-stream fname))
fe (.getNextEntry zf)
_ (println (format "Found %s" (.getName fe)))
parse-fn (rows->dataset-fn nil)]
(reduce (fn [rc batch]
(+ rc (ds-proto/row-count (parse-fn batch))))
0
(bulk/batch-csv-rows 10000 (charred/read-csv-supplier zf)))))
(defn load-zip-parallel
[fname]
(let [zf (ZipInputStream. (io/input-stream fname))
fe (.getNextEntry zf)
_ (println (format "Found %s" (.getName fe)))
s (charred/read-csv-supplier zf)
row-batches (bulk/batch-csv-rows 10000 s)
batch-queue (ArrayBlockingQueue. 16)
n-parse-threads 6
csv-thread
(Thread. ^java.lang.Runnable
(fn []
(let [rc
(reduce (fn [rc row-batch]
(let [data (vec row-batch)
rc (+ rc (count data))]
(.put batch-queue data)
rc))
0
row-batches)]
(.put batch-queue ::finished)
(println "csv parse thread finished")))
"CSV parse thread")
_ (.start csv-thread)
sum (->> (abq->iterable batch-queue)
(hamf/pmap #(ds-proto/row-count (first (rows->dataset-seq nil %))))
(reduce + 0))]
(.join csv-thread)
(println "csv thread joined")
sum))
)
(def result (load-zip "/home/chrisn/Downloads/bigcsv/full_data.zip"))
(def result (load-zip-parallel "/home/chrisn/Downloads/bigcsv/full_data.zip"))
)
+179
View File
@@ -0,0 +1,179 @@
(ns tech.v3.dataset.io.datetime
"Helpful and well tested string->datetime pathways."
(:require [clojure.string :as s]
[tech.v3.datatype.datetime :as dtype-dt])
(:import [java.time LocalDate LocalDateTime LocalTime
ZonedDateTime OffsetDateTime Instant Duration]
[java.time.format DateTimeFormatter DateTimeFormatterBuilder]))
(set! *warn-on-reflection* true)
;;Assumping is that the string will have /,-. replaced with /space
(def ^{:doc "Local date parser patterns used to generate the local date
formatter."} local-date-parser-patterns
["yyyy MM dd"
"yyyyMMdd"
"MM dd yyyy"
"dd MMM yyyy"
"M d yyyy"
"M d yy"
"MMM dd yyyy"
"MMM dd yy"
"MMM d yyyy"])
(defn- date-preparse
^String [^String data]
(.replaceAll data "[/,-. ]+" " "))
(def ^{:doc "DateTimeFormatter that runs through a set of options in order to
parse a wide variety of local date formats."
:tag DateTimeFormatter}
local-date-formatter
(let [builder (DateTimeFormatterBuilder.)]
(.parseCaseInsensitive builder)
(doseq [pattern local-date-parser-patterns]
(.appendOptional builder (DateTimeFormatter/ofPattern pattern)))
(.appendOptional builder DateTimeFormatter/ISO_LOCAL_DATE)
(.toFormatter builder)))
(defn parse-local-date
"Convert a string into a local date attempting a wide variety of format types."
^LocalDate [^String str-data]
(LocalDate/parse (date-preparse str-data) local-date-formatter))
(def ^{:doc "Parser patterns to parse a wide variety of time strings"}
time-parser-patterns
["HH:mm:ss:SSS"
"hh:mm:ss a"
"HH:mm:ss"
"h:mm:ss a"
"H:mm:ss"
"hh:mm a"
"HH:mm"
"HHmm"
"h:mm a"
"H:mm"])
(def ^{:doc "DateTimeFormatter built to help parse a wide variety of time strings"
:tag DateTimeFormatter}
local-time-formatter
(let [builder (DateTimeFormatterBuilder.)]
(.parseCaseInsensitive builder)
(doseq [pattern time-parser-patterns]
(.appendOptional builder (DateTimeFormatter/ofPattern pattern)))
(.appendOptional builder DateTimeFormatter/ISO_LOCAL_TIME)
(.toFormatter builder)))
(defn- local-time-preparse
^String [^String data]
(.replaceAll data "[._]" ":"))
(defn parse-local-time
"Convert a string into a local time attempting a wide variety of
possible parser patterns."
^LocalTime [^String str-data]
(LocalTime/parse (local-time-preparse str-data) local-time-formatter))
(defn parse-local-date-time
"Parse a local-date-time by first splitting the string and then separately
parsing the local-date portion and the local-time portions."
^LocalDateTime [^String str-data]
(let [split-data (s/split str-data #"[ T]+")]
(cond
(== 2 (count split-data))
(let [local-date (parse-local-date (first split-data))
local-time (parse-local-time (second split-data))]
(LocalDateTime/of local-date local-time))
(== 3 (count split-data))
(let [local-date (parse-local-date (first split-data))
local-time (parse-local-time (str (split-data 1) " " (split-data 2)))]
(LocalDateTime/of local-date local-time))
:else
(throw (Exception. (format "Failed to parse \"%s\" as a LocalDateTime"
str-data))))))
(defn parse-duration
"Attempt a few ways to parse a duration."
^Duration [^String str-data]
(try
(Duration/parse str-data)
(catch Throwable _e
(let [duration-str (.trim str-data)
[duration-str mult] (if (.startsWith duration-str "-")
[(.substring duration-str 1) -1]
[duration-str 1])
dur-data (s/split duration-str #":")
_ (when-not (> (count dur-data) 1)
(throw (Exception. "Not a valid duration: %s")))
nanos (reduce (fn [nanos [idx next-data]]
(+ (long nanos)
(long
(case (long idx)
0 (* (Integer/parseInt next-data)
(dtype-dt/nanoseconds-in-hour))
1 (* (Integer/parseInt next-data)
(dtype-dt/nanoseconds-in-minute))
2 (* (Integer/parseInt next-data)
(dtype-dt/nanoseconds-in-second))
3 (* (Integer/parseInt next-data)
(dtype-dt/nanoseconds-in-millisecond))))))
0
(map-indexed vector dur-data))]
(Duration/ofNanos (* (long mult) (long nanos)))))))
(def ^{:doc "Map of datetime datatype to generalized parse fn."}
datatype->general-parse-fn-map
{:local-date parse-local-date
:local-date-time parse-local-date-time
:duration parse-duration
:local-time parse-local-time
:instant #(Instant/parse ^String %)
:zoned-date-time #(ZonedDateTime/parse ^String %)})
(defn datetime-formatter-parse-str-fn
"Given a datatype and a formatter return a function that
attempts to parse that specific datatype."
[datatype formatter]
(case datatype
:local-date
#(LocalDate/parse % formatter)
:local-date-time
#(LocalDateTime/parse % formatter)
:local-time
#(LocalTime/parse % formatter)
:zoned-date-time
#(ZonedDateTime/parse % formatter)
:offset-date-time
#(OffsetDateTime/parse % formatter)))
(defn datetime-formatter-or-str->parser-fn
"Given a datatype and one of [fn? string? DateTimeFormatter],
return a function that takes strings and returns datetime objects
of type datatype."
[datatype format-string-or-formatter]
(cond
(instance? DateTimeFormatter format-string-or-formatter)
(datetime-formatter-parse-str-fn datatype format-string-or-formatter)
(string? format-string-or-formatter)
(datetime-formatter-parse-str-fn
datatype
(DateTimeFormatter/ofPattern format-string-or-formatter))
(fn? format-string-or-formatter)
format-string-or-formatter
:else
(throw (Exception. (format "Unrecognized datetime parser type: %s"
format-string-or-formatter)))))
+212
View File
@@ -0,0 +1,212 @@
(ns ^:no-doc tech.v3.dataset.io.mapseq-colmap
"Sequences of maps are maybe the most basic pure datastructure for data.
Converting them into a more structured form (and back) is a key component of
dealing with datasets"
(:require [tech.v3.datatype :as dtype]
[tech.v3.dataset.io.column-parsers :as column-parsers]
[tech.v3.dataset.io.context :as parse-context]
[tech.v3.dataset.protocols :as ds-proto]
[tech.v3.parallel.for :as pfor]
[tech.v3.datatype.argtypes :as argtypes]
[tech.v3.dataset.impl.dataset :as ds-impl]
[ham-fisted.lazy-noncaching :as lznc]
[ham-fisted.api :as hamf]
[ham-fisted.reduce :as hamf-rf]
[ham-fisted.function :as hamf-fn]
[ham-fisted.protocols :as hamf-proto])
(:import [java.util HashMap Map$Entry Map Map$Entry LinkedHashMap Iterator]
[java.util.function Function Consumer]
[tech.v3.dataset.protocols PDatasetParser PClearable]
[clojure.lang IDeref Counted Indexed]
[ham_fisted Reductions$IndexedAccum Reducible Consumers$IncConsumer
ITypedReduce]))
(defrecord ParseRecord [^long col-idx column-name column-parser]
ds-proto/PClearable
(ds-clear [this] (ds-proto/ds-clear column-parser)))
(deftype ^:private MapseqReducer [options parsers consumer ^Consumers$IncConsumer row-idx]
Consumer
(accept [this row]
(hamf-rf/consume! consumer row)
(.inc row-idx))
Counted
(count [this] (.value row-idx))
PDatasetParser
(add-row [this row] (.accept this row))
(add-rows [this rows] (hamf-rf/consume! this rows))
Indexed
(nth [this idx] (nth this idx nil))
(nth [this idx dv]
(let [ec (.value row-idx)
idx (if (< idx 0)
(+ idx ec)
idx)]
(if (or (< idx 0)
(>= idx ec))
dv
(into {} (map (fn [^ParseRecord pr]
[(.-column-name pr) (nth (.-column-parser pr)
(unchecked-int idx))]))
(.values ^Map parsers)))))
ITypedReduce
(reduce [this rfn init]
(let [parser-vals (.values ^Map parsers)]
(reduce (fn [acc ^long idx]
(rfn acc (into {} (map (fn [^ParseRecord pr]
[(.-column-name pr) (nth (.-column-parser pr)
(unchecked-int idx))]))
parser-vals)))
init (hamf/range (.value row-idx)))))
IDeref
(deref [this]
(if (.isEmpty ^Map parsers)
{:tech.v3.dataset/row-count (.value row-idx)
:tech.v3.dataset/missing :all}
(parse-context/parsers->dataset (assoc options :key-fn nil) parsers (.value row-idx))))
PClearable
(ds-clear [this]
(reduce (fn [_ p] (ds-proto/ds-clear p)) nil (.values ^Map parsers))
(.setValue row-idx 0)
this))
(defn mapseq-reducer
"Create a non-parallelized mapseq hamf reducer. If you want a single IFn that follows the
convention of clojure transducers call hamf/reducer->rfn."
[options]
(reify
hamf-proto/Reducer
(->init-val-fn [r]
#(let [parse-context (parse-context/options->parser-fn options :object)
parsers (LinkedHashMap.)
key-fn (:key-fn options identity)
colparser-compute-fn (hamf-fn/function
colname
(ParseRecord. (.size parsers) (key-fn colname) (parse-context colname)))
colname->parser (fn [colname]
(.-column-parser ^ParseRecord
(.computeIfAbsent parsers colname
colparser-compute-fn)))
row-idx (hamf/inc-consumer)
consumer (hamf-fn/consumer
e
(let [^Map$Entry e e
parser (colname->parser (.getKey e))]
(column-parsers/add-value! parser (.value row-idx) (.getValue e))))]
(MapseqReducer. options parsers consumer row-idx)))
(->rfn [r] hamf-rf/consumer-accumulator)
hamf-proto/Finalize
(finalize [r v] @v)))
(defn mapseq->dataset
([options mapseq]
(hamf-rf/reduce-reducer (mapseq-reducer options) mapseq))
([mapseq]
(mapseq->dataset {} mapseq)))
(defn column-map->dataset
([options column-map]
(let [parse-context (parse-context/options->parser-fn options :object)
coltypes (->> column-map
(group-by (fn [e] (argtypes/arg-type (val e)))))
parse-scalar (fn [e ^long n-rows]
#:tech.v3.dataset{:name (key e)
:data (dtype/const-reader (val e) n-rows)})]
;;new rule - anything iterable can only be iterated once. Not all iterables can be re-iterated
;;without kicking off a potentially expensive process again
;;This means that if we haven't decided on n-rows yet the iteration of all
;;the iterables must both create columns *and* take the min length to be the new n-rows.
;;If we have readers those dictate the dataset length
(if-let [rdr-cols (:reader coltypes)]
(let [n-rows (long (apply max (map (comp dtype/ecount val) rdr-cols)))]
(->> column-map
(pfor/pmap
(fn [e]
(let [coldata (val e)
colname (key e)]
(case (argtypes/arg-type coldata)
:scalar (parse-scalar e n-rows)
:reader (if (identical? :object (dtype/elemwise-datatype coldata))
(let [parser (parse-context colname)]
(reduce (hamf-rf/indexed-accum
acc idx v
(column-parsers/add-value! parser idx v))
nil
coldata)
(assoc (column-parsers/finalize! parser n-rows)
:tech.v3.dataset/name colname))
#:tech.v3.dataset{:name colname
:data coldata})
:iterable (let [parser (parse-context colname)]
(reduce (hamf-rf/indexed-accum
acc idx v
(if (< idx n-rows)
(column-parsers/add-value! parser idx v)
(reduced true)))
nil coldata)
(assoc (column-parsers/finalize! parser n-rows)
:tech.v3.dataset/name colname))))))
(ds-impl/new-dataset options)))
;;If we only have iterables the shortest one dictates dataset length
(if-let [iter-cols (:iterable coltypes)]
(let [[n-rows iterable-cols]
(let [iterable-cols iter-cols
parse-contexts (mapv (fn [e]
(let [cname (key e)]
[cname
(parse-context cname)
(.iterator ^Iterable (ham-fisted.protocols/->iterable (val e)))]))
iterable-cols)
val-data (object-array (count parse-contexts))
next-valid?
(fn [^long row-idx]
(let [all-valid?
(reduce (hamf-rf/indexed-accum
acc idx pctx
(let [^Iterator iter (nth pctx 2)]
(if (.hasNext iter)
(do
(aset val-data idx (.next iter))
true)
(reduced false))))
true
parse-contexts)]
(when all-valid?
(reduce (hamf-rf/indexed-accum
acc idx pctx
(column-parsers/add-value! (nth pctx 1) row-idx (aget val-data idx)))
nil
parse-contexts)
true)))
n-rows
(loop [row-idx 0]
(if (next-valid? row-idx)
(recur (unchecked-inc row-idx))
row-idx))]
[n-rows
(into {} (map (fn [val]
[(nth val 0)
(assoc (column-parsers/finalize! (nth val 1) n-rows)
:tech.v3.dataset/name (nth val 0))]))
parse-contexts)])]
(->> column-map
(map
(fn [e]
(let [coldata (val e)
colname (key e)]
(case (argtypes/arg-type coldata)
:scalar (parse-scalar e n-rows)
:iterable (get iterable-cols colname)))))
(ds-impl/new-dataset options)))
;;Finally constants have a dataset length of 1
(->> column-map
(map #(parse-scalar % 1))
(ds-impl/new-dataset options))))))
([column-map]
(column-map->dataset nil column-map)))
+58
View File
@@ -0,0 +1,58 @@
(ns tech.v3.dataset.io.nippy
(:require [tech.v3.dataset.io :as ds-io]
[tech.v3.dataset.base :as ds-base]
[tech.v3.datatype.errors :as errors]
;;Support for tensor/array buffers in nippy
[tech.v3.datatype.nippy]
[tech.v3.io :as io]
[taoensso.nippy :as nippy])
(:import [tech.v3.dataset.impl.dataset Dataset]
[tech.v3.dataset.impl.column Column]))
(nippy/extend-freeze
Dataset :tech.ml/dataset
[ds out]
(nippy/-freeze-without-meta! (ds-base/dataset->data ds) out))
(nippy/extend-thaw
:tech.ml/dataset
[in]
(-> (nippy/thaw-from-in! in)
(ds-base/data->dataset)))
(nippy/extend-freeze
Column :tech.ml.dataset/column
[col out]
(nippy/-freeze-without-meta! (ds-base/column->data col) out))
(nippy/extend-thaw
:tech.ml.dataset/column
[in]
(-> (nippy/thaw-from-in! in)
(ds-base/data->column)))
(defn load-nippy
[data options]
(ds-io/wrap-stream-fn
data (:gzipped? options)
(fn [instream]
(let [retval (io/get-nippy instream)]
(errors/when-not-errorf
(instance? Dataset retval)
"Unthawed data is not a dataset: %s"
(type retval))
retval))))
(defn write-nippy!
[dataset output options]
(if (:gzipped? options)
(with-open [os (io/gzip-output-stream! output)]
(let [^bytes data (nippy/freeze dataset)]
(.write os data)))
(io/put-nippy! output dataset)))
+61
View File
@@ -0,0 +1,61 @@
(ns ^:no-doc tech.v3.dataset.io.spreadsheet
"Spreadsheets in general are stored in a cell-based format. This means that any cell
could have data of any type. Commonalities around parsing spreadsheet-type systems
are captured here."
(:require [tech.v3.dataset.io.context :as parse-context]
[tech.v3.dataset.io.column-parsers :as column-parsers]
[tech.v3.datatype :as dtype])
(:import [tech.v3.dataset Spreadsheet$Sheet Spreadsheet$Row Spreadsheet$Cell]))
(set! *warn-on-reflection* true)
(defn sheet->dataset
[^Spreadsheet$Sheet sheet
{:keys [header-row? n-initial-skip-rows]
:or {header-row? true}
:as options}]
(let [ds-name (or (:dataset-name options)
(.name sheet)
:_unnamed)
options (assoc options :dataset-name ds-name)
rows (iterator-seq (.iterator sheet))
n-initial-skip-rows (long (or n-initial-skip-rows 0))
rows (if-not (== 0 n-initial-skip-rows)
(drop-while #(< (.getRowNum ^Spreadsheet$Row %)
(long n-initial-skip-rows))
rows)
rows)
[header-row rows]
(if header-row?
;;Always have to keep in mind that columns are sparse.
[(->> (first rows)
(map (fn [^Spreadsheet$Cell cell]
(let [column-number (.getColumnNum cell)]
[column-number (.value cell)])))
(into {}))
(rest rows)]
[{} rows])
{:keys [parsers col-idx->parser]}
(parse-context/options->col-idx-parse-context options :object header-row)
row-dec (+ n-initial-skip-rows (if header-row? 1 0))]
(doseq [^Spreadsheet$Row row rows]
(let [row-num (- (.getRowNum row) row-dec)]
(doseq [^Spreadsheet$Cell cell row]
(let [parser (col-idx->parser (.getColumnNum cell))]
(when-not (.missing cell)
(case (dtype/elemwise-datatype cell)
:boolean (column-parsers/add-value! parser row-num (.boolValue cell))
:float64 (column-parsers/add-value! parser row-num (.doubleValue cell))
(column-parsers/add-value! parser row-num (.value cell))))))))
;;Fill out columns that only have missing values
(when-not (== 0 (count parsers))
(let [max-col-idx (->> parsers
(map-indexed (fn [idx parser]
(when parser idx)))
(remove nil?)
(last))]
(dotimes [idx max-col-idx]
(col-idx->parser idx))))
(parse-context/parsers->dataset options parsers)))
@@ -0,0 +1,79 @@
(ns tech.v3.dataset.io.string-row-parser
"Parsing functions based on raw data that is represented by a sequence
of string arrays."
(:require [tech.v3.dataset.io.column-parsers :as column-parsers]
[tech.v3.dataset.io.context :as parse-context]
[tech.v3.datatype :as dtype]
[tech.v3.parallel.for :as pfor]))
(set! *warn-on-reflection* true)
(defn rows->dataset
"Given a sequence of string[] rows, parse into columnar data.
See csv->columns.
This method is useful if you have another way of generating sequences of
string[] row data."
[{:keys [header-row? skip-bad-rows?]
:or {header-row? true}
:as options}
row-seq]
(let [initial-row (first row-seq)
header-row (dtype/->reader (if header-row?
initial-row
[]))
n-header-cols (count header-row)
{:keys [parsers col-idx->parser]}
(parse-context/options->col-idx-parse-context
options :string (fn [^long col-idx]
(when (< col-idx n-header-cols)
(header-row col-idx))))]
;;side effecting loop
(->> (if header-row?
(rest row-seq)
row-seq)
(map-indexed vector)
(pfor/consume!
(fn [[^long row-idx ^"[Ljava.lang.String;" row]]
(when-not (and skip-bad-rows? (not= (alength row) n-header-cols))
(dotimes [col-idx (alength row)]
(let [parser (col-idx->parser col-idx)]
(column-parsers/add-value! parser row-idx (aget row col-idx))))))))
(parse-context/parsers->dataset options parsers)))
(defn partition-all-rows
"Given a sequence of rows, partition into an undefined number of partitions of at most
N rows but keep the header row as the first for all sequences."
([{:keys [header-row?]
:or {header-row? true}} n row-seq]
(let [[header-row row-seq] (if header-row?
[(first row-seq) (rest row-seq)]
[nil row-seq])
row-partitions (partition-all n row-seq)]
(if header-row
(map #(concat [header-row] %) row-partitions)
row-partitions))))
(defn sample-rows
"Sample at most N rows selected randomly from the row sequence. If sequence is
shorter than length N will return less than N rows.
Uses naive reservoir sampling:
https://en.wikipedia.org/wiki/Reservoir_sampling"
[{:keys [header-row?]
:or {header-row? true}} n row-seq]
(let [[header-row row-seq] (if header-row?
[(first row-seq) (rest row-seq)]
[nil row-seq])
n (long n)
^objects row-data (into-array Object (take n row-seq))
row-seq (seq (drop n row-seq))]
;;if we have more data than N
(doseq [[idx row] (map-indexed vector row-seq)]
(let [idx (+ (long idx) n)
replace-idx (rand-int idx)]
(when (< replace-idx n)
(aset row-data replace-idx row))))
(concat [header-row] row-data)))
+65
View File
@@ -0,0 +1,65 @@
(ns tech.v3.dataset.io.svm
(:require [clojure.string :as s]))
(def test-line "1 1:2.617300e+01 2:5.886700e+01 3:-1.894697e-01 4:1.251225e+02")
(defn- parse-line
[line]
(let [parse (s/split line #"\s+")
label (Double/parseDouble (first parse))
idx-val-pairs (->> (rest parse)
(map (fn [item]
(let [[idx val] (s/split item #":")]
[(Long/parseLong idx)
(Double/parseDouble val)]))))]
{:label label
:features (->> idx-val-pairs
(into {}))
:max-idx (apply max (map first idx-val-pairs))
:min-idx (apply min (map first idx-val-pairs))}))
(defn parse-svm-file
"Parse an svm file and generate a dataset of {:features :label}. Always
represents result as a dense matrix; no support for sparse datasets."
[fname & {:keys [label-map]}]
(let [f-data (slurp fname)
[labels features min-idx max-idx]
(->> (s/split f-data #"\n")
(pmap parse-line)
(reduce
(fn [[labels features min-idx max-idx] next-line]
(let [{label :label
line-feature :features
line-max-idx :max-idx
line-min-idx :min-idx} next-line]
[(conj labels (if label-map
(if-let [label-value (get label-map label)]
label-value
(throw (ex-info (format "Failed to find label for value: %s" label)
{:label-map label-map})))
label))
(conj features (->> line-feature
(map (fn [[k v]]
[(str k) v]))
(into {})))
(if (and min-idx
(< min-idx line-min-idx))
min-idx
line-min-idx)
(if (and max-idx
(> max-idx line-max-idx))
max-idx
line-max-idx)]))
[[] [] nil nil]))
min-idx (long min-idx)
max-idx (long max-idx)
_num-items (+ 1 (- max-idx min-idx))]
{:min-idx min-idx
:max-idx max-idx
:feature-ecount (+ 1 (- max-idx min-idx))
:dataset (map (fn [label features]
(assoc features :label label))
labels features)}))
+390
View File
@@ -0,0 +1,390 @@
(ns tech.v3.dataset.io.univocity
"Bindings to univocity. Transforms csv's, tsv's into sequences
of string arrays that are then passed into `tech.v3.dataset.io.string-row-parser`
methods."
(:require [tech.v3.io :as io]
[tech.v3.datatype :as dtype]
[tech.v3.datatype.errors :as errors]
[tech.v3.dataset.io.string-row-parser :as row-parser]
[tech.v3.dataset.io :as ds-io]
[tech.v3.dataset.column :as ds-col]
[tech.v3.dataset.utils :as ds-utils])
(:import [tech.v3.datatype ObjectReader]
[com.univocity.parsers.common AbstractParser AbstractWriter CommonSettings]
[com.univocity.parsers.csv CsvParserSettings CsvParser CsvWriterSettings CsvWriter]
[com.univocity.parsers.tsv
TsvWriterSettings TsvWriter]
[java.io Reader Closeable Writer]
[java.util List]
[java.lang AutoCloseable]))
(set! *warn-on-reflection* true)
(defn- sequence-type
[item-seq]
(cond
(every? string? item-seq)
:string
(every? number? item-seq)
:number
:else
(throw (Exception. "Item seq must of either strings or numbers"))))
(defn- ->character
[item]
(cond
(instance? Character item)
item
(string? item)
(if (== 1 (count item))
(first item)
(throw (Exception.
(format "Multicharacter separators (%s) are not supported." item))))
:else
(throw (Exception. (format "'%s' is not a valid separator" item)))))
(defn- default-csv-options
[options]
(-> options
(update :max-chars-per-column #(or % (* 64 1024)))
(update :max-num-columns #(or % 8192))))
(defn- apply-options-to-format!
^CommonSettings [^CommonSettings settings {:keys [disable-comment-skipping?]}]
(when disable-comment-skipping?
(-> settings .getFormat (.setComment \u0000)))
settings)
(defn- apply-options-to-settings!
^CommonSettings [^CommonSettings settings options]
(let [{:keys [max-chars-per-column
max-num-columns]} (default-csv-options options)]
(doto settings
(.setMaxCharsPerColumn (long max-chars-per-column))
(.setMaxColumns (long max-num-columns))
(apply-options-to-format! options)))
settings)
(defn create-csv-parser
"Create an implementation of univocity csv parser."
^AbstractParser [{:keys [header-row?
num-rows
column-whitelist
column-blacklist
column-allowlist
column-blocklist
separator
n-initial-skip-rows]
:or {header-row? true
;;64K max chars per column. This is a silly thing to have
;;to set...
}
:as options}]
(if-let [csv-parser (:csv-parser options)]
csv-parser
(let [settings (CsvParserSettings.)
num-rows (or num-rows (:n-records options))]
(when separator
(.detectFormatAutomatically settings
(into-array Character/TYPE
[(->character separator)])))
(when num-rows
(.setNumberOfRecordsToRead settings (if header-row?
(inc (int num-rows))
(int num-rows))))
(apply-options-to-settings! settings options)
(doto settings
(.setSkipEmptyLines true)
(.setIgnoreLeadingWhitespaces true)
(.setIgnoreTrailingWhitespaces true))
(when n-initial-skip-rows
(.setNumberOfRowsToSkip settings (int n-initial-skip-rows)))
(when (or (seq column-whitelist)
(seq column-blacklist))
(when (and (seq column-whitelist)
(seq column-blacklist))
(throw (Exception.
"Either whitelist or blacklist can be provided but not both")))
(when (and (seq column-allowlist)
(seq column-blocklist))
(throw (Exception.
"Either allowlist or blocklist can be provided but not both")))
(when (and (seq column-allowlist)
(seq column-whitelist))
(throw (Exception.
"Either allowlist or whitelist can be provided but not both")))
(when (and (seq column-blacklist)
(seq column-blocklist))
(throw (Exception.
"Either blacklist or blocklist can be provided but not both")))
(let [[string-fn! number-fn!]
(if (seq column-whitelist)
[#(.selectFields
settings
^"[Ljava.lang.String;" (into-array String %))
#(.selectIndexes
settings
^"[Ljava.lang.Integer;" (into-array Integer (map int %)))]
[#(.excludeFields
settings
^"[Ljava.lang.String;" (into-array String %))
#(.excludeIndexes
settings
^"[Ljava.lang.Integer;"(into-array Integer (map int %)))])
column-data (if (seq (or column-allowlist column-whitelist))
(or column-allowlist column-whitelist)
(or column-blocklist column-blacklist))
column-type (sequence-type column-data)]
(case column-type
:string (string-fn! column-data)
:number (number-fn! column-data))))
(CsvParser. settings))))
(deftype RawRowIterator [^:unsynchronized-mutable closer
^AbstractParser parser
^:unsynchronized-mutable next-val]
java.util.Iterator
(hasNext [_this] (boolean next-val))
(next [_this]
(let [retval next-val]
(set! next-val (.parseNext parser))
(when-not next-val
(when closer
(cond
(instance? Closeable closer)
(.close ^Closeable closer)
(instance? AutoCloseable closer)
(.close ^AutoCloseable closer))
(set! closer nil)))
retval)))
(defn raw-row-iterable
"Returns an iterable that produces string[]'s"
(^Iterable [input ^AbstractParser parser]
(reify Iterable
(iterator [this]
(let [^Reader reader (io/reader input)]
(.beginParsing parser reader)
(RawRowIterator. reader parser (.parseNext parser))))))
(^Iterable [input]
(raw-row-iterable input (create-csv-parser {}))))
(defn csv->rows
"Given a csv, produces a sequence of rows. The csv options from ->dataset
apply here.
options:
- `:column-allowlist` - either sequence of string column names or sequence of column
indices of columns to whitelist. In preference to `:column-whitelist`.
- `:column-blocklist` - either sequence of string column names or sequence of column
indices of columns to blacklist. In preference to `:column-blacklist`.
- `:num-rows` - Number of rows to read
- `:separator` - Add a character separator to the list of separators to auto-detect.
- `:max-chars-per-column` - Defaults to 4096. Columns with more characters that this
will result in an exception.
- `:max-num-columns` - Defaults to 8192. CSV,TSV files with more columns than this
will fail to parse. For more information on this option, please visit:
https://github.com/uniVocity/univocity-parsers/issues/301"
([input options]
(raw-row-iterable
input
(create-csv-parser options)))
([input]
(csv->rows input {})))
(defn csv->dataset
"Non-lazily and serially parse the columns. Returns a vector of maps of
{
:name column-name
:missing long-reader of in-order missing indexes
:data typed reader/writer of data
:metadata - optional map with unparsed-indexes and unparsed-values
}
Supports a subset of tech.v3.dataset/->dataset options:
:column-allowlist in preference to :column-whitelist
:column-blocklist in preference to :column-blacklist
:n-initial-skip-rows
:num-rows
:header-row?
:separator
:parser-fn
:parser-scan-len"
([input options]
(->> (csv->rows input options)
(seq)
(row-parser/rows->dataset options)))
([input]
(csv->dataset input {})))
(defn- load-csv
[data options]
(ds-io/wrap-stream-fn
data (:gzipped? options)
#(csv->dataset %1 options))
)
#_(defmethod ds-io/data->dataset :csv
[data options]
(load-csv data options))
#_(defmethod ds-io/data->dataset :tsv
[data options]
(load-csv data (merge {:separator \tab} options)))
#_(defmethod ds-io/data->dataset :txt
[data options]
(load-csv data options))
(defprotocol PApplyWriteOptions
(apply-write-options! [settings options]))
(extend-protocol PApplyWriteOptions
CsvWriterSettings
(apply-write-options! [settings options]
(when-let [quoted-fields (seq (:quoted-columns options))]
(let [^"[Ljava.lang.String;" str-ary
(into-array String (map ds-utils/column-safe-name quoted-fields))]
(.quoteFields settings str-ary)))
settings)
TsvWriterSettings
(apply-write-options! [settings options]
settings))
(defn rows->csv!
"Given an something convertible to an output stream, an optional set of headers
as string arrays, and a sequence of string arrows, write a CSV or a TSV file.
Options:
* `:separator` - Defaults to \tab.
* `:quoted-columns` - For csv, specify which columns should always be quoted
regardless of their data."
([output header-string-array row-string-array-seq]
(rows->csv! output header-string-array row-string-array-seq {}))
([output header-string-array row-string-array-seq
{:keys [separator]
:or {separator \tab}
:as options}]
(let [^Writer writer (io/writer! output)
^AbstractWriter csvWriter
(if (:csv-writer options)
(:csv-writer options)
(let [settings (-> (case separator
\,
(CsvWriterSettings.)
\tab
(TsvWriterSettings.))
(apply-options-to-settings! options)
(apply-write-options! options))]
(cond
(instance? CsvWriterSettings settings)
(CsvWriter. writer ^CsvWriterSettings settings)
(instance? TsvWriterSettings settings)
(TsvWriter. writer ^TsvWriterSettings settings)
:else
(errors/throwf "Unrecognized settings type %s" (type settings))
)))]
(when header-string-array
(.writeHeaders csvWriter ^"[Ljava.lang.String;" header-string-array))
(try
(doseq [^"[Ljava.lang.String;" row row-string-array-seq]
(.writeRow csvWriter row))
(finally
(.close csvWriter))))))
(defn- data->string
^String [data-item]
(when-not (nil? data-item)
(cond
(string? data-item) data-item
(keyword? data-item) (name data-item)
(symbol? data-item) (name data-item)
:else (.toString ^Object data-item))))
(defn- write-csv!
"Write a dataset to a tsv or csv output stream. Closes output if a stream
is passed in. File output format will be inferred if output is a string -
- .csv, .tsv - switches between tsv, csv. Tsv is the default.
- *.gz - write to a gzipped stream.
At this time writing to json is not supported.
options -
:separator - in case output isn't a string, you can use either \\, or \\tab to switch
between csv or tsv output respectively.
:headers? - if csv headers are written, defaults to true."
([ds output options]
(let [{:keys [gzipped? file-type]}
(merge
(when (string? output)
(ds-io/str->file-info output))
options)
{:keys [max-chars-per-column
max-num-columns]} (default-csv-options options)
columns (vals ds)
_ (errors/when-not-errorf
(<= (count columns) (long max-num-columns))
"Too many columns detected (%d) for max-num-columns (%d)"
(count columns) max-num-columns)
headers (when (get options :headers? true)
(into-array String
(map (comp data->string :name meta) columns)))
column-names (mapv (comp :name meta) columns)
^List str-readers
(mapv (comp dtype/->reader #(ds-col/column-map data->string :string %)) columns)
tsv? (or (= file-type :tsv) (= \tab (:separator options)))
[n-cols n-rows] (dtype/shape ds)
n-cols (long n-cols)
n-rows (long n-rows)
max-chars-per-column (long max-chars-per-column)
;;Create a reader that produces string arrays of each row.
str-rdr (reify ObjectReader
(lsize [rdr] n-rows)
(readObject [rdr row-idx]
(let [^"[Ljava.lang.String;" str-array (make-array
String n-cols)]
(dotimes [col-idx n-cols]
(let [str-data (.readObject ^ObjectReader
(.get str-readers col-idx)
row-idx)]
(errors/when-not-errorf
(< (count str-data) max-chars-per-column)
"Column %s has value whose length (%d) is greater than max-chars-per-column (%d)."
(column-names col-idx) (count str-data) max-chars-per-column)
(aset str-array col-idx ^String str-data)))
str-array)))
output (if gzipped?
(io/gzip-output-stream! output)
output)
output-options (if tsv?
(merge options
{:separator \tab})
(merge options
{:separator \,}))]
(rows->csv! output headers str-rdr output-options)))
([ds output]
(write-csv! ds output {})))
+799
View File
@@ -0,0 +1,799 @@
(ns tech.v3.dataset.join
"implementation of join algorithms, both exact (hash-join) and near."
(:require [tech.v3.datatype :as dtype]
[tech.v3.datatype.casting :as casting]
[tech.v3.datatype.packing :as packing]
[tech.v3.datatype.argops :as argops]
[tech.v3.datatype.errors :as errors]
[tech.v3.datatype.datetime :as dtype-dt]
[tech.v3.datatype.protocols :as dtype-proto]
[tech.v3.parallel.for :as parallel-for]
[tech.v3.dataset.column :as ds-col]
[tech.v3.dataset.base :as ds-base]
[tech.v3.dataset.readers :as ds-readers]
[tech.v3.dataset.impl.dataset :as ds-impl]
[tech.v3.dataset.utils :as ds-utils]
[ham-fisted.api :as hamf]
[ham-fisted.function :as hamf-fn]
[clj-commons.primitive-math :as pmath]
[clojure.set :as set])
(:import [tech.v3.datatype ObjectReader Buffer
BinaryPredicate BinaryOperator
BinaryOperators$DoubleBinaryOperator]
[ham_fisted IMutList]
[java.util List HashSet Map Set]))
(set! *warn-on-reflection* true)
(defn- colname->str
^String [item]
(ds-utils/column-safe-name item))
(defn- similar-colname-type
[orign-name new-name]
(cond
(string? orign-name) new-name
(keyword? orign-name) (keyword new-name)
(symbol? orign-name) (symbol new-name)
:else
new-name))
(defn- colname->lhs-rhs-colnames
[colname]
(if (instance? java.util.Collection colname)
colname
[colname colname]))
(declare finalize-join-result)
(defn- add-all!
[^List target ^List new-data]
(.addAll target new-data)
target)
(defn- default-table-name
[ds-table default-val]
(if (= (ds-base/dataset-name ds-table) "_unnamed")
default-val
(ds-base/dataset-name ds-table)))
(defn- nice-column-names
[& table-name-column-seq-pairs]
(->>
table-name-column-seq-pairs
(mapcat (fn [[table-name column-seq]]
(map vector
column-seq
(repeat table-name))))
(reduce
(fn [[final-columns name-set] [column prefix]]
(let [original-colname (ds-col/column-name column)
colname
(loop [found? (contains? name-set
original-colname)
new-col-name original-colname
idx 0]
(if found?
(let [new-col-name
(if (== 0 idx)
(str prefix "."
(colname->str original-colname))
(str prefix "." original-colname "-" idx))]
(recur (contains? name-set new-col-name)
new-col-name
(unchecked-inc idx)))
(similar-colname-type original-colname
new-col-name)))]
[(conj final-columns (with-meta column
(assoc (meta column)
:name colname
:src-table-name prefix
:previous-colname original-colname)))
(conj name-set colname)]))
[[] #{}])
(first)))
(defn- update-join-metadata
[result-table lhs-table-name rhs-table-name]
(let [name-data (->> (map meta (vals result-table))
(group-by :src-table-name))]
(with-meta result-table
(assoc (meta result-table)
:left-column-names
(->> (get name-data lhs-table-name)
(map (juxt :previous-colname :name))
(into {}))
:right-column-names
(->> (get name-data rhs-table-name)
(map (juxt :previous-colname :name))
(into {}))))))
(defn- fix-inner-join-metadata
[result-table lhs-join-colname rhs-join-colname]
(with-meta result-table
(assoc-in (meta result-table)
[:right-column-names rhs-join-colname]
lhs-join-colname)))
(defn- append-str
[name-or-kwd data]
(let [rv (str (if (instance? clojure.lang.Named name-or-kwd)
(name name-or-kwd)
name-or-kwd) "-" data)]
(if (keyword? name-or-kwd)
(keyword rv)
rv)))
(defn- finalize-join-result
[lhs-colname rhs-colname lhs rhs lhs-indexes rhs-indexes lhs-missing rhs-missing]
(let [lhs-columns (ds-base/columns (ds-base/remove-column lhs lhs-colname))
rhs-columns (ds-base/columns (ds-base/remove-column rhs rhs-colname))
lhs-table-name (default-table-name lhs "left")
rhs-table-name (default-table-name rhs "right")
;;The table names *must* be different
[lhs-table-name rhs-table-name] (if (= lhs-table-name rhs-table-name)
[(append-str lhs-table-name "left")
(append-str rhs-table-name "right")]
[lhs-table-name rhs-table-name])
lhs-join-column (lhs lhs-colname)
rhs-join-column (rhs rhs-colname)]
(merge
{:inner
(let [lhs-columns (map #(ds-col/select % lhs-indexes) lhs-columns)
rhs-columns (map #(ds-col/select % rhs-indexes) rhs-columns)]
(-> (ds-impl/new-dataset
"inner-join"
(nice-column-names
[lhs-table-name (concat [(ds-col/select lhs-join-column lhs-indexes)]
lhs-columns)]
[rhs-table-name rhs-columns]))
(update-join-metadata lhs-table-name rhs-table-name)
(fix-inner-join-metadata lhs-colname rhs-colname)))
:lhs-indexes lhs-indexes
:rhs-indexes rhs-indexes}
(when rhs-missing
{:rhs-missing rhs-missing
:right-outer
(let [n-empty (count rhs-missing)
rhs-indexes (add-all! (dtype/clone rhs-indexes) rhs-missing)
rhs-columns (map #(ds-col/select % rhs-indexes) rhs-columns)
lhs-columns
(->> (concat [lhs-join-column] lhs-columns)
(map (fn [old-col]
(ds-col/extend-column-with-empty
(ds-col/select old-col lhs-indexes)
n-empty))))]
(-> (ds-impl/new-dataset
"right-outer-join"
(nice-column-names
[lhs-table-name lhs-columns]
[rhs-table-name (concat [(ds-col/select rhs-join-column rhs-indexes)]
rhs-columns)]))
(update-join-metadata lhs-table-name rhs-table-name)))})
(when lhs-missing
{:lhs-missing lhs-missing
:left-outer
(let [n-empty (count lhs-missing)
lhs-indexes (add-all! (dtype/clone lhs-indexes) lhs-missing)
lhs-columns (map #(ds-col/select % lhs-indexes) lhs-columns)
rhs-columns
(->> (concat [rhs-join-column] rhs-columns)
(map (fn [old-col]
(ds-col/extend-column-with-empty
(ds-col/select old-col rhs-indexes)
n-empty))))]
(-> (ds-impl/new-dataset
"left-outer-join"
(nice-column-names
[lhs-table-name (concat [(ds-col/select lhs-join-column lhs-indexes)]
lhs-columns)]
[rhs-table-name rhs-columns]))
(update-join-metadata lhs-table-name rhs-table-name)))}))))
(defn- hash-join-algo
[lhs-col rhs-col lhs-missing? rhs-missing? operation-space]
(let [lhs-dtype (packing/unpack-datatype (dtype/elemwise-datatype lhs-col))
rhs-dtype (packing/unpack-datatype (dtype/elemwise-datatype rhs-col))
op-dtype (casting/simple-operation-space
(casting/widest-datatype lhs-dtype rhs-dtype))
;;We have to group in operation in object space else missing values can
;;cause issues with numeric columns.
^Map idx-groups (argops/arggroup
{:operation-space :object}
(dtype/elemwise-cast lhs-col op-dtype))
rhs-col (dtype/elemwise-cast rhs-col op-dtype)
n-elems (dtype/ecount rhs-col)
{lhs-indexes :lhs-indexes
rhs-indexes :rhs-indexes
rhs-missing :rhs-missing
lhs-found :lhs-found}
;;Directly from talks - parallelize building the maps then apply optimized
;;union operations.
(->> (hamf/pgroups
n-elems
(fn [^long sidx ^long eidx]
(let [lhs-indexes (dtype/make-list operation-space)
rhs-indexes (dtype/make-list operation-space)
rhs-missing (dtype/make-list operation-space)
lhs-found (hamf/mut-set)
ne (- eidx sidx)
;;Sub-buffer here gives us a chance to avoid missing index checks.
rhs-col (dtype/->buffer (dtype/sub-buffer rhs-col sidx ne))]
(dotimes [inner-idx ne]
(let [idx (+ sidx inner-idx)
;;Note the reading is done in object space. This means missing values will be nil
;;in all cases.
rhs-val (.readObject rhs-col inner-idx)]
(if-let [^IMutList item (.get idx-groups rhs-val)]
(do
(when lhs-missing? (.add lhs-found rhs-val))
(when-not (.isEmpty item) (.addAllReducible rhs-indexes (hamf/repeat (.size item) idx)))
(.addAll lhs-indexes item))
(when rhs-missing? (.add rhs-missing idx)))))
(hamf/hash-map :lhs-indexes lhs-indexes
:rhs-indexes rhs-indexes
:rhs-missing rhs-missing
:lhs-found lhs-found))))
(hamf/union-reduce-maps (hamf-fn/bi-function
lhs rhs
(if (instance? Set lhs)
(hamf/union lhs rhs)
(do (.addAll ^List lhs rhs) lhs)))))
;;idx-groups is a hamf linked hashmap with highly optimized reduce-kv implementation
lhs-missing (when lhs-missing?
(reduce-kv (fn [lhs-missing k v]
(when-not (.contains ^Set lhs-found k)
(.addAll ^List lhs-missing v))
lhs-missing)
(dtype/make-list operation-space)
idx-groups))]
{:lhs-indexes lhs-indexes
:rhs-indexes rhs-indexes
:lhs-missing lhs-missing
:rhs-missing rhs-missing}))
(defn hash-join
"Join by column. For efficiency, lhs should be smaller than rhs.
colname - may be a single item or a tuple in which is destructures as:
(let [[lhs-colname rhs-colname]] colname] ...)
An options map can be passed in with optional arguments:
:lhs-missing? Calculate the missing lhs indexes and left outer join table.
:rhs-missing? Calculate the missing rhs indexes and right outer join table.
:operation-space - either :int32 or :int64. Defaults to :int32.
Returns
{:join-table - joined-table
:lhs-indexes - matched lhs indexes
:rhs-indexes - matched rhs indexes
;; -- when rhs-missing? is true --
:rhs-missing - missing indexes of rhs.
:rhs-outer-join - rhs outer join table.
;; -- when lhs-missing? is true --
:lhs-missing - missing indexes of lhs.
:lhs-outer-join - lhs outer join table.
}"
([colname lhs rhs]
(hash-join colname lhs rhs {}))
([colname lhs rhs {:keys [operation-space]
:or {operation-space :int32}
:as options}]
(let [[lhs-colname rhs-colname] (colname->lhs-rhs-colnames colname)
lhs-col (lhs lhs-colname)
rhs-col (rhs rhs-colname)
lhs-missing? (:lhs-missing? options)
rhs-missing? (:rhs-missing? options)
{:keys [lhs-indexes rhs-indexes lhs-missing rhs-missing]}
(hash-join-algo lhs-col rhs-col lhs-missing? rhs-missing? operation-space)]
(finalize-join-result lhs-colname rhs-colname lhs rhs
lhs-indexes rhs-indexes
lhs-missing
rhs-missing))))
(defn inner-join
"Inner join by column. For efficiency, lhs should be smaller than rhs.
colname - may be a single item or a tuple in which is destructures as:
(let [[lhs-colname rhs-colname]] colname] ...)
An options map can be passed in with optional arguments:
:operation-space - either :int32 or :int64. Defaults to :int32.
Returns the joined table"
([colname lhs rhs]
(inner-join colname lhs rhs {}))
([colname lhs rhs options]
(-> (hash-join colname lhs rhs options)
:inner)))
(defn right-join
"Right join by column. For efficiency, lhs should be smaller than rhs.
colname - may be a single item or a tuple in which is destructures as:
(let [[lhs-colname rhs-colname]] colname] ...)
An options map can be passed in with optional arguments:
:operation-space - either :int32 or :int64. Defaults to :int32.
Returns the joined table"
([colname lhs rhs]
(right-join colname lhs rhs {}))
([colname lhs rhs options]
(-> (hash-join colname lhs rhs (assoc options :rhs-missing? true))
:right-outer)))
(defn left-join
"Left join by column. For efficiency, lhs should be smaller than rhs.
colname - may be a single item or a tuple in which is destructures as:
(let [[lhs-colname rhs-colname]] colname] ...)
An options map can be passed in with optional arguments:
:operation-space - either :int32 or :int64. Defaults to :int32.
Returns the joined table"
([colname lhs rhs]
(left-join colname lhs rhs {}))
([colname lhs rhs options]
(-> (hash-join colname lhs rhs (assoc options :lhs-missing? true))
:left-outer)))
(defn- col-or-data->reader
([tuple-data ds])
([tuple-data ds outer?]
;;Else not having the column is an error
(if (and (sequential? tuple-data)
(not= 1 (count tuple-data)))
(-> (ds-base/select-columns ds tuple-data)
(ds-readers/value-reader {:copying? true}))
(let [tuple-data (if (sequential? tuple-data)
(first tuple-data)
tuple-data)]
(if outer?
(get ds tuple-data [])
(ds-base/column ds tuple-data))))))
(defn- ensure-sequential
[colname]
(if-not (sequential? colname) [colname] colname))
(defn- filter-columns
[ds collist outer?]
(when collist
(if outer?
(vec (filter (set (ds-base/column-names ds)) collist))
collist)))
(defn pd-merge
"Pandas-style [merge](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.merge.html).
This is similar to join except it can merge on multiple columns for the left and right
sides.
Options:
* `:on` - column name or list of columns names. Names must be found in both datasets.
* `:left-on` - Column name or list of column names
* `:right-on` - Column name or list of column names
* `:how` - left, right inner, outer, cross. If cross, then no on, left-on, right-on can
be provided.
Examples:
```clojure
user> (require '[tech.v3.dataset :as ds])
nil
user> (require '[tech.v3.dataset.join :as ds-join])
nil
user> (def ds-a (ds/->dataset {:a [:a :b :b :a :c]
:b (range 5)
:c (range 5)}))
#'user/ds-a
user> (def ds-b (ds/->dataset {:a [:a :b :a :b :d]
:b (range 5)
:c (range 6 11)}))
#'user/ds-b
user> ds-a
_unnamed [5 3]:
| :a | :b | :c |
|----|---:|---:|
| :a | 0 | 0 |
| :b | 1 | 1 |
| :b | 2 | 2 |
| :a | 3 | 3 |
| :c | 4 | 4 |
user> ds-b
_unnamed [5 3]:
| :a | :b | :c |
|----|---:|---:|
| :a | 0 | 6 |
| :b | 1 | 7 |
| :a | 2 | 8 |
| :b | 3 | 9 |
| :d | 4 | 10 |
user> (ds-join/pd-merge ds-a ds-b {:on [:a :b] :how :inner})
inner-join [2 4]:
| :a | :b | :c | :right.c |
|----|---:|---:|---------:|
| :a | 0 | 0 | 6 |
| :b | 1 | 1 | 7 |
user> (ds-join/pd-merge ds-a ds-b {:on [:a :b] :how :outer})
outer-join [8 4]:
| :a | :b | :c | :right.c |
|----|---:|---:|---------:|
| :a | 0 | 0 | 6 |
| :b | 1 | 1 | 7 |
| :b | 2 | 2 | |
| :a | 3 | 3 | |
| :c | 4 | 4 | |
| :a | 2 | | 8 |
| :b | 3 | | 9 |
| :d | 4 | | 10 |
```"
([left-ds right-ds options]
(let [lhs-table-name (default-table-name left-ds "left")
rhs-table-name (default-table-name right-ds "right")
how (get options :how :inner)]
(if (identical? how :cross)
(do
(errors/when-not-errorf
(nil? (or (options :on)
(options :left-on)
(options :right-on)))
"Columns must not be specified during a cross merge")
(let [left-rc (ds-base/row-count left-ds)
right-rc (ds-base/row-count right-ds)
n-result (* left-rc right-rc)
[lhs-indexes rhs-indexes]
(if (< n-result Integer/MAX_VALUE)
[(dtype/make-reader :int32 n-result (quot idx right-rc))
(dtype/make-reader :int32 n-result (rem idx right-rc))]
[(dtype/make-reader :int64 n-result (quot idx right-rc))
(dtype/make-reader :int64 n-result (rem idx right-rc))])
lhs-columns (map #(ds-col/select % lhs-indexes) (ds-base/columns left-ds))
rhs-columns (map #(ds-col/select % rhs-indexes) (ds-base/columns right-ds))]
(-> (ds-impl/new-dataset
"cross-join"
(nice-column-names
[lhs-table-name lhs-columns]
[rhs-table-name rhs-columns]))
(update-join-metadata lhs-table-name rhs-table-name))))
(let [left-on (ensure-sequential (get options :left-on (get options :on)))
right-on (ensure-sequential (get options :right-on (get options :on)))
outer? (identical? :outer (get options :how))
on-int (->> (concat left-on right-on)
(filter (set/intersection (set left-on) (set right-on)))
(distinct)
(vec))
_ (errors/when-not-errorf
(== (count left-on) (count right-on))
"Number of left join columns (%d) doesn't equal number of right join columns %d"
(count left-on) (count right-on))
left-on (filter-columns left-ds left-on outer?)
right-on (filter-columns right-ds right-on outer?)
left-join-data (col-or-data->reader left-on left-ds outer?)
right-join-data (col-or-data->reader right-on right-ds outer?)
{:keys [lhs-indexes rhs-indexes lhs-missing rhs-missing]}
(hash-join-algo left-join-data right-join-data true true
(get options :operation-space :int32))
;;Ensure a reasonable column order
left-cols (->> (concat left-on (ds-base/column-names left-ds)) distinct vec)
right-cols (->> (concat right-on (ds-base/column-names right-ds)) distinct vec)
left-ds (ds-base/select-columns left-ds left-cols)
right-ds (ds-base/select-columns right-ds right-cols)]
(case how
:left
(let [n-empty (count lhs-missing)
lhs-indexes (add-all! (dtype/clone lhs-indexes) lhs-missing)
lhs-cols (-> (ds-base/select-rows left-ds lhs-indexes)
(ds-base/columns))
rhs-cols (->> (ds-base/columns (ds-base/remove-columns right-ds on-int))
(map (fn [col]
(ds-col/extend-column-with-empty
(ds-col/select col rhs-indexes)
n-empty))))]
(-> (ds-impl/new-dataset
"left-outer-join"
(nice-column-names
[lhs-table-name lhs-cols]
[rhs-table-name rhs-cols]))
(update-join-metadata lhs-table-name rhs-table-name)))
:right
(let [n-empty (count rhs-missing)
rhs-indexes (add-all! (dtype/clone rhs-indexes) rhs-missing)
rhs-cols (-> (ds-base/select-rows right-ds rhs-indexes)
(ds-base/columns))
lhs-cols (->> (ds-base/columns (ds-base/remove-columns left-ds on-int))
(map (fn [col]
(ds-col/extend-column-with-empty
(ds-col/select col lhs-indexes)
n-empty))))]
(-> (ds-impl/new-dataset
"right-outer-join"
(nice-column-names
[rhs-table-name rhs-cols]
[lhs-table-name lhs-cols]))
(update-join-metadata lhs-table-name rhs-table-name)))
:inner
(let [lhs-cols (-> (ds-base/select-rows left-ds lhs-indexes)
(ds-base/columns))
rhs-cols (->> (-> (ds-base/remove-columns right-ds on-int)
(ds-base/columns))
(mapv #(ds-base/select-rows % rhs-indexes)))]
(-> (ds-impl/new-dataset
"inner-join"
(nice-column-names
[lhs-table-name lhs-cols]
[rhs-table-name rhs-cols]))
(update-join-metadata lhs-table-name rhs-table-name)))
:outer
(cond
(== 0 (ds-base/row-count left-ds))
(vary-meta right-ds assoc :name "outer-join")
(== 0 (ds-base/row-count right-ds))
(vary-meta left-ds assoc :name "outer-join")
:else
(let [n-left-empty (count rhs-missing)
n-right-empty (count lhs-missing)
;;Order is intersection, left-missing, right-missing
lhs-indexes (add-all! (dtype/clone lhs-indexes) lhs-missing)
left-valid (ds-base/select-rows left-ds lhs-indexes)
right-valid (ds-base/select-rows right-ds rhs-indexes)
right-missing (ds-base/select-rows right-ds rhs-missing)
;;For the columns we perhaps joined on
intersection-ds (-> (ds-base/select-columns left-valid on-int)
(ds-base/concat-copying (ds-base/select-columns
right-missing on-int)))
left-full (-> (ds-base/remove-columns left-valid on-int)
(ds-base/extend-with-empty n-left-empty))
right-full (-> (ds-base/remove-columns right-valid on-int)
(ds-base/extend-with-empty n-right-empty)
(ds-base/concat-copying (ds-base/remove-columns
right-missing on-int)))]
(-> (ds-impl/new-dataset
"outer-join"
(nice-column-names
[lhs-table-name (concat (ds-base/columns intersection-ds)
(ds-base/columns left-full))]
[rhs-table-name (ds-base/columns right-full)]))
(update-join-metadata lhs-table-name rhs-table-name)))))))))
([left-ds right-ds]
(pd-merge left-ds right-ds {:on (set/intersection
(set (ds-base/column-names left-ds))
(set (ds-base/column-names right-ds)))})))
(defn- ensure-numeric-reader
"Column is either a float or integer reader."
^Buffer [col]
(let [col-dtype (casting/un-alias-datatype (dtype/get-datatype col))]
(if (dtype-dt/datetime-datatype? col-dtype)
(-> (dtype-dt/datetime->milliseconds col)
(dtype/->reader))
(dtype/->reader col (casting/simple-operation-space col-dtype)))))
(defn- asof-op->binary-pred
^BinaryPredicate [asof-op
^Buffer lhs-rdr
^Buffer rhs-rdr]
(let [comp-op (argops/->binary-predicate asof-op)
op-space (casting/simple-operation-space
(.elemwiseDatatype lhs-rdr)
(.elemwiseDatatype rhs-rdr))]
(case op-space
:int64
(reify BinaryPredicate
(binaryLong [this lhs-idx rhs-idx]
(.binaryLong comp-op
(.readLong lhs-rdr lhs-idx)
(.readLong rhs-rdr rhs-idx))))
:float64
(reify BinaryPredicate
(binaryLong [this lhs-idx rhs-idx]
(.binaryDouble comp-op
(.readDouble lhs-rdr lhs-idx)
(.readDouble rhs-rdr rhs-idx)))))))
(defn- asof-lt
[asof-op lhs rhs]
(let [op-space (casting/simple-operation-space
(dtype/elemwise-datatype lhs)
(dtype/elemwise-datatype rhs))
lhs-rdr (dtype/->reader lhs op-space)
rhs-rdr (dtype/->reader rhs op-space)
n-elems (.lsize lhs-rdr)
n-right (.lsize rhs-rdr)
retval (dtype/make-list :int32)
comp-fn (asof-op->binary-pred asof-op lhs-rdr rhs-rdr)]
(loop [lhs-idx 0
rhs-idx 0]
(when-not (or (== lhs-idx n-elems)
(== rhs-idx n-right))
(let [found? (.binaryLong comp-fn lhs-idx rhs-idx)
new-lhs-idx (if found?
(unchecked-inc lhs-idx)
lhs-idx)
new-rhs-idx (if found?
rhs-idx
(unchecked-inc rhs-idx))]
(when found?
(.addLong retval (unchecked-int rhs-idx)))
(recur new-lhs-idx new-rhs-idx))))
[0 retval]))
(defn- asof-gt
[asof-op lhs rhs]
(let [op-space (casting/simple-operation-space
(dtype/elemwise-datatype lhs)
(dtype/elemwise-datatype rhs))
lhs-rdr (dtype/->reader lhs op-space)
rhs-rdr (dtype/->reader rhs op-space)
n-elems (.lsize lhs-rdr)
n-right (.lsize rhs-rdr)
n-right-dec (dec n-right)
retval (dtype/make-list :int32)
comp-fn (asof-op->binary-pred asof-op lhs-rdr rhs-rdr)]
(loop [lhs-idx (unchecked-dec n-elems)
rhs-idx n-right-dec]
(when-not (or (== lhs-idx -1)
(== rhs-idx -1))
(let [found? (.binaryLong comp-fn lhs-idx rhs-idx)
new-lhs-idx (if found?
(unchecked-dec lhs-idx)
lhs-idx)
new-rhs-idx (if found?
rhs-idx
(unchecked-dec rhs-idx))]
(when found?
(.addLong retval (unchecked-int rhs-idx)))
(recur new-lhs-idx new-rhs-idx))))
;;Now we have to reverse retval
(let [rev-retval (dtype/make-list :int32)
n-retval (.size retval)
n-retval-dec (unchecked-dec n-retval)]
(dotimes [iter n-retval]
(.addLong rev-retval (.getLong retval (- n-retval-dec iter))))
[(- n-elems (.size rev-retval))
rev-retval])))
(def ^{:private true
:tag BinaryOperator} abs-diff-op
(reify BinaryOperators$DoubleBinaryOperator
(binaryLong [this lhs rhs]
(Math/abs (- rhs lhs)))
(binaryDouble [this lhs rhs]
(Math/abs (- rhs lhs)))))
(defn- abs-diff-bin-pred
^BinaryPredicate
[^Buffer lhs-rdr
^Buffer rhs-rdr]
(let [op-space (casting/simple-operation-space
(.elemwiseDatatype lhs-rdr)
(.elemwiseDatatype rhs-rdr))
^BinaryOperator comp-op abs-diff-op]
(case op-space
:int64
(reify BinaryPredicate
(binaryLong [this lhs-idx rhs-idx]
(let [lhs-val (.readLong lhs-rdr lhs-idx)
rhs-val (.readLong rhs-rdr rhs-idx)
rhs-next-val (.readLong rhs-rdr (unchecked-inc rhs-idx))
asof-diff (pmath/- (.binaryLong comp-op lhs-val rhs-val)
(.binaryLong comp-op lhs-val rhs-next-val))]
(< asof-diff 0))))
:float64
(reify BinaryPredicate
(binaryLong [this lhs-idx rhs-idx]
(let [lhs-val (.readLong lhs-rdr lhs-idx)
rhs-val (.readLong rhs-rdr rhs-idx)
rhs-next-val (.readLong rhs-rdr (unchecked-inc rhs-idx))
asof-diff (pmath/- (.binaryDouble comp-op lhs-val rhs-val)
(.binaryDouble comp-op lhs-val rhs-next-val))]
(< asof-diff 0)))))))
(defn- asof-nearest
[lhs rhs]
(let [op-space (casting/simple-operation-space
(dtype/elemwise-datatype lhs)
(dtype/elemwise-datatype rhs))
lhs-rdr (dtype/->reader lhs op-space)
rhs-rdr (dtype/->reader rhs op-space)
n-elems (.lsize lhs-rdr)
n-right (.lsize rhs-rdr)
n-right-dec (dec n-right)
retval (dtype/make-list :int32)
bin-pred (abs-diff-bin-pred lhs-rdr rhs-rdr)]
(loop [lhs-idx 0
rhs-idx 0]
(when-not (or (== lhs-idx n-elems)
(== rhs-idx n-right-dec))
(let [found? (.binaryLong bin-pred lhs-idx rhs-idx)
new-lhs-idx (if found?
(unchecked-inc lhs-idx)
lhs-idx)
new-rhs-idx (if found?
rhs-idx
(unchecked-inc rhs-idx))]
(when found?
(.add retval (unchecked-int rhs-idx)))
(recur new-lhs-idx new-rhs-idx))))
(dotimes [_iter (- n-elems (.size retval))]
(.add retval n-right-dec))
[0 retval]))
(defn left-join-asof
"Perform a left join asof. Similar to left join except this will join on nearest
value. lhs and rhs must be sorted by join-column. join columns must be either
datetime columns in which the join happens in millisecond space or they must be
numeric - integer or floating point datatypes.
Options:
- `asof-op`- may be [:< :<= :nearest :>= :>] - type of join operation. Defaults to
<=."
([colname lhs rhs {:keys [asof-op]
:or {asof-op :<=}}]
(when-not (#{:< :<= :nearest :>= :>} asof-op)
(throw (Exception. (format "Unrecognized asof op: %s" asof-op))))
(let [[lhs-colname rhs-colname] (colname->lhs-rhs-colnames colname)
lhs-col (ds-base/column lhs lhs-colname)
rhs-col (ds-base/column rhs rhs-colname)
lhs-table-name (default-table-name lhs "left")
rhs-table-name (default-table-name rhs "right")
lhs-reader (ensure-numeric-reader lhs-col)
rhs-reader (ensure-numeric-reader rhs-col)
[rhs-offset rhs-indexes]
(cond
(#{:< :<=} asof-op)
(asof-lt (keyword "tech.numerics" (name asof-op)) lhs-reader rhs-reader)
(#{:> :>=} asof-op)
(asof-gt (keyword "tech.numerics" (name asof-op)) lhs-reader rhs-reader)
(= asof-op :nearest)
(asof-nearest lhs-reader rhs-reader)
:else
(throw (Exception. "Unsupported")))
rhs-offset (long rhs-offset)
n-indexes (dtype/ecount rhs-indexes)
n-empty (- (ds-base/row-count lhs) (+ rhs-offset n-indexes))
lhs-columns (ds-base/columns lhs)
rhs-columns
(->> (ds-base/columns rhs)
(map (fn [old-col]
(if (== 0 rhs-offset)
(ds-col/extend-column-with-empty
(ds-col/select old-col rhs-indexes)
n-empty)
(ds-col/prepend-column-with-empty
(ds-col/select old-col rhs-indexes)
rhs-offset)))))]
(-> (ds-impl/new-dataset
(format "asof-%s" (name asof-op))
(nice-column-names
[lhs-table-name lhs-columns]
[rhs-table-name rhs-columns]))
(update-join-metadata lhs-table-name rhs-table-name))))
([colname lhs rhs]
(left-join-asof colname lhs rhs {})))
+344
View File
@@ -0,0 +1,344 @@
(ns tech.v3.dataset.math
"Various mathematic transformations of datasets such as (inefficiently)
building simple tables, pca, and normalizing columns to have mean of 0 and variance of 1.
More in-depth transformations are found at `tech.v3.dataset.neanderthal`."
(:require [tech.v3.datatype :as dtype]
[tech.v3.datatype.protocols :as dtype-proto]
[tech.v3.datatype.functional :as dfn]
[tech.v3.datatype.statistics :as statistics]
[tech.v3.parallel.for :as pfor]
[tech.v3.datatype.casting :as casting]
[tech.v3.datatype.datetime :as dtype-dt]
[tech.v3.datatype.bitmap :as bitmap]
[tech.v3.datatype.errors :as errors]
[tech.v3.dataset.column :as ds-col]
[tech.v3.dataset.base :as ds-base]
[tech.v3.dataset.tensor :as ds-tens]
[tech.v3.dataset.impl.dataset :as ds-impl]
[tech.v3.dataset.utils :as ds-utils]
[tech.v3.dataset.protocols :as ds-proto]
[tech.v3.dataset.missing :as ds-missing]
[ham-fisted.set :as set]
[ham-fisted.api :as hamf]
[ham-fisted.function :as hamf-fn]
[ham-fisted.defprotocol :refer [extend-type]]
[clj-commons.primitive-math :as pmath]
[clojure.tools.logging :as log]
[clojure.set :as c-set])
(:import [org.apache.commons.math3.analysis.interpolation LoessInterpolator]
[tech.v3.datatype DoubleReader]
[org.roaringbitmap RoaringBitmap])
(:refer-clojure :exclude [extend-type]))
(set! *warn-on-reflection* true)
(set! *unchecked-math* true)
(defn correlation-table
"Return a map of colname->list of sorted tuple of [colname, coefficient].
Sort is:
(sort-by (comp #(Math/abs (double %)) second) >)
Thus the first entry is:
[colname, 1.0]
There are three possible correlation types:
:pearson
:spearman
:kendall
:pearson is the default."
[dataset & {:keys [correlation-type
colname-seq]}]
(let [missing-columns (ds-base/columns-with-missing-seq dataset)
_ (when missing-columns
(println "WARNING - excluding columns with missing values:\n"
(mapv :column-name missing-columns)))
non-numeric (->> (ds-base/columns dataset)
(map meta)
(remove #(casting/numeric-type?
(:datatype %)))
(map :name)
seq)
_ (when non-numeric
(println "WARNING - excluding non-numeric columns:\n"
(vec non-numeric)))
_ (when-let [selected-non-numeric
(seq (c-set/intersection (set colname-seq)
(set non-numeric)))]
(throw (ex-info (format "Selected columns are non-numeric: %s"
selected-non-numeric)
{:selected-columns colname-seq
:non-numeric-columns non-numeric})))
dataset (ds-base/select-columns dataset
(->> (ds-base/columns dataset)
(map ds-col/column-name)
(remove (set (concat
(map :column-name missing-columns)
non-numeric)))))
lhs-colseq (->> (if (seq colname-seq)
(map (partial ds-base/column dataset) colname-seq)
(ds-base/columns dataset))
(map (fn [col] [(ds-col/column-name col)
(hamf/double-array (dtype/->reader col :float64))])))
rhs-colseq (->> (ds-base/columns dataset)
(map (fn [col] [(ds-col/column-name col)
(hamf/double-array (dtype/->reader col :float64))])))
correlation-type (or correlation-type :pearson)
corr-fn (case correlation-type
:pearson #(statistics/pearsons-correlation %1 %2 {:nan-strategy :keep})
:spearman #(statistics/spearmans-correlation %1 %2 {:nan-strategy :keep})
:kendall #(statistics/kendalls-correlation %1 %2 {:nan-strategy :keep}))]
(->> (for [[lname ldata] lhs-colseq]
[lname
(->> rhs-colseq
(map (fn [[rname rdata]]
(let [corr (corr-fn ldata rdata)]
(if (Double/isFinite corr)
[rname corr]
(do
(log/warnf "Correlation failed: %s-%s" lname rname)
nil)))))
(remove nil?)
;;Using a type-hinted key-fn allows the sort operator to use a double array
;;which has a very fast indirect sort operation.
(hamf/sort-by (hamf-fn/obj->double
v (-> (second v)
(double)
(Math/abs))) >))])
(into {}))))
(defn interpolate-loess
"Interpolate using the LOESS regression engine. Useful for smoothing out graphs."
([ds x-colname y-colname
{:keys [bandwidth iterations accuracy result-name]
;;Using R defaults, as close as we can get.
:or {bandwidth 0.75
iterations 4
accuracy LoessInterpolator/DEFAULT_ACCURACY}}]
(let [interp (LoessInterpolator. (double bandwidth)
(int iterations)
(double accuracy))
x-col (ds x-colname)
x-orig-datatype (dtype/elemwise-datatype x-col)
x-datetime? (dtype-dt/datetime-datatype? x-orig-datatype)
x-col (if x-datetime?
(dtype-dt/datetime->milliseconds x-col)
x-col)
y-col (ds y-colname)
spline (.interpolate interp
(dtype/->double-array x-col)
(dtype/->double-array y-col))
new-col-name (or result-name
(keyword (str (ds-utils/column-safe-name y-colname) "-loess")))
n-elems (ds-base/row-count ds)
x-rdr (dtype/->buffer x-col)]
(-> (ds-base/add-or-update-column ds new-col-name
(reify DoubleReader
(lsize [rdr] n-elems)
(readDouble [rdr idx]
(.value spline (.readDouble x-rdr idx)))))
(ds-base/update-column new-col-name
#(with-meta % (assoc (meta %)
:interpolator spline))))))
([ds x-colname y-colname]
(interpolate-loess ds x-colname y-colname {})))
(defn- scatter-missing
[index-rdr ^RoaringBitmap missing]
(let [retval (RoaringBitmap.)
iter (.getIntIterator missing)]
(loop [continue? (.hasNext iter)]
(when continue?
(.add retval (int (index-rdr (.next iter))))
(recur (.hasNext iter))))
retval))
(defn fill-range-replace
"Given an in-order column of a numeric or datetime type, fill in spans that are
larger than the given max-span. The source column must not have missing values.
For more documentation on fill-range, see tech.v3.datatype.function.fill-range.
If the column is a datetime type the operation happens in millisecond space and
max-span may be a datetime type convertible to milliseconds.
The result column has the same datatype as the input column.
After the operation, if missing strategy is not nil the newly produced missing
values along with the existing missing values will be replaced using the given
missing strategy for all other columns. See
`tech.v3.dataset.missing/replace-missing` for documentation on missing strategies.
The missing strategy defaults to :down unless explicity set.
Returns a new dataset."
([ds colname max-span]
(fill-range-replace ds colname max-span :down nil))
([ds colname max-span missing-strategy]
(fill-range-replace ds colname max-span missing-strategy nil))
([ds colname max-span missing-strategy missing-value]
(let [target-col (ds colname)
_ (when-not (== 0 (dtype/ecount (ds-col/missing target-col)))
(throw (Exception. "Fill-range column must not have missing values")))
target-dtype (dtype/get-datatype target-col)
target-col (if (dtype-dt/datetime-datatype? target-dtype)
(dtype-dt/datetime->milliseconds target-col)
target-col)
max-span (if (dtype-dt/datetime-datatype? max-span)
(dtype-dt/datetime->milliseconds max-span)
max-span)
{:keys [result missing]} (dfn/fill-range target-col max-span)
;;This is the set of values that have not changed. Iterating through it and
;;and a source vector in parallel allow us to scatter the original data into
;;a new storage container.
original-indexes (-> (doto (bitmap/->bitmap
(range (dtype/ecount result)))
(.andNot ^RoaringBitmap missing))
(bitmap/->random-access))
result (if (dtype-dt/datetime-datatype? target-dtype)
(dtype-dt/milliseconds->datetime target-dtype result)
result)]
(->> (ds-base/columns ds)
(pfor/pmap
(fn [col]
(if (= colname (ds-col/column-name col))
(ds-col/new-column colname result)
(let [new-colname (ds-col/column-name col)
new-data (dtype/make-container
:jvm-heap
(dtype/get-datatype col)
(dtype/ecount result))
^RoaringBitmap col-missing (ds-col/missing col)
any-missing? (not (.isEmpty col-missing))
updated-missing (if any-missing?
(scatter-missing original-indexes col-missing)
(bitmap/->bitmap))
total-missing (set/union updated-missing missing)
;;Scatter original data into new locations
_ (dtype/copy! col
(dtype/indexed-buffer original-indexes new-data))
;;Use col-impl as it skips the scanning of the data and we know
;;that it is dense
new-col (ds-col/new-column new-colname new-data
(meta col)
total-missing)]
(if-not (nil? missing-strategy)
(ds-missing/replace-missing-with-strategy
new-col total-missing missing-strategy missing-value)
new-col)))))
(ds-impl/new-dataset (meta ds))))))
(defrecord StdScaleTransform [])
(defn fit-std-scale
"Calculate nan-aware means, stddev - per-column - of a dataset.
Options are passed through to
tech.v3.datatype.statistics/descriptive-statistics."
([dataset {:keys [mean? stddev?]
:or {mean? true stddev? true}
:as options}]
(let [stats-data (cond-> []
mean? (conj :mean)
stddev? (conj :standard-deviation))]
(errors/when-not-error
(seq stats-data)
"Either mean? or stddev? must be true")
(map->StdScaleTransform
(->> (vals dataset)
(map (fn [col]
[(:name (meta col))
(statistics/descriptive-statistics col stats-data options)]))
(into {})))))
([dataset]
(fit-std-scale dataset nil)))
(defn transform-std-scale
"Given a dataset and a standard scale transform return a new dataset
with the columns "
[dataset std-scale-xform]
(reduce (fn [dataset [colname {:keys [mean standard-deviation]}]]
(let [mean (double (or mean 0.0))
standard-deviation (double (or standard-deviation 1.0))]
(ds-base/update-column
dataset colname
(fn [coldata]
(ds-col/column-map
(fn [val]
(pmath// (pmath/- (double val) mean)
standard-deviation))
:float64
coldata)))))
dataset
std-scale-xform))
(extend-type StdScaleTransform
ds-proto/PDatasetTransform
(transform [t dataset]
(transform-std-scale dataset t)))
(defrecord MinMaxTransform [min max column-data])
(defn fit-minmax
"nan-aware min-max fit of the dataset. Returns an object that can be used
in transform-minmax. target Min-max default to -0.5,0.5"
([dataset {:keys [min max]
:or {min -0.5
max 0.5}
:as options}]
(let [min (double min)
max (double max)]
(map->MinMaxTransform
{:min min :max max
:column-data
(->> (vals dataset)
(map (fn [col]
[(:name (meta col))
(statistics/descriptive-statistics col [:min :max] options)]))
(into {}))})))
([dataset]
(fit-minmax dataset nil)))
(defn transform-minmax
"Scale columns listed in the min-max transform to the mins and maxes dictated
by that transform."
[dataset {:keys [min max column-data]}]
(let [min (double min)
max (double max)
target-range (- max min)]
(reduce (fn [dataset [colname coldata]]
(let [{column-min :min
column-max :max} coldata
column-min (double column-min)
column-max (double column-max)
column-range (- column-max column-min)]
(ds-base/update-column
dataset colname
(fn [coldata]
(ds-col/column-map
(fn [val]
(-> (double val)
(pmath/- column-min)
(pmath// column-range)
(pmath/* target-range)
(pmath/+ min)))
:float64
coldata)))))
dataset
column-data)))
(extend-type MinMaxTransform
ds-proto/PDatasetTransform
(transform [t dataset]
(transform-minmax dataset t)))
File diff suppressed because it is too large Load Diff
+67
View File
@@ -0,0 +1,67 @@
(ns tech.v3.dataset.metamorph-api
"This is an auto-generated api system - it scans the namespaces and changes the first
to be metamorph-compliant which means transforming an argument that is just a dataset into
an argument that is a metamorph context - a map of `{:metamorph/data ds}`. They also return
their result as a metamorph context."
(:require [tech.v3.dataset :as ds]
[tech.v3.dataset.modelling]
[tech.v3.dataset.protocols :as prot])
(:refer-clojure :exclude [filter group-by sort-by concat take-nth shuffle
rand-nth update]))
(defmacro build-pipelined-function
[f m]
(let [args (map (comp vec rest) (:arglists m))
doc-string (:doc m)]
`(defn ~(symbol (name f)) {:doc ~doc-string :orig (symbol (var ~f))}
~@(for [arg args
:let [narg (mapv #(if (map? %) 'options %) arg)
[a & r] (split-with (partial not= '&) narg)]]
(list narg `(fn [ds#]
(let [ctx# (if (ds/dataset? ds#)
{:metamorph/data ds#} ds#)]
(assoc ctx# :metamorph/data (apply ~f (ctx# :metamorph/data) ~@a ~(rest r))))))))))
(def ^:private excludes-dataset
'#{bind-> all-descriptive-stats-names major-version ->dataset ->>dataset column-map-m
mapseq-parser mapseq-rf dataset-parser})
(defmacro ^:private process-all-api-symbols-dataset
[]
(let [ps (ns-publics 'tech.v3.dataset)]
`(do ~@(for [[f v] ps
:when (not (excludes-dataset f))
:let [m (meta v)
f (symbol "tech.v3.dataset" (name f))]]
`(build-pipelined-function ~f ~m)))))
(def ^:private excludes-dataset-modelling
'#{ds-mod/inference-column?})
(defmacro ^:private process-all-api-symbols-dataset-modelling
[]
(let [ps (ns-publics 'tech.v3.dataset.modelling)]
`(do ~@(for [[f v] ps
:when (not (excludes-dataset-modelling f))
:let [m (meta v)
f (symbol "tech.v3.dataset.modelling" (name f))]]
`(build-pipelined-function ~f ~m)))))
(process-all-api-symbols-dataset)
(process-all-api-symbols-dataset-modelling)
(comment
(require '[tech.v3.datatype.export-symbols :as export-symbols])
(export-symbols/write-api! 'tech.v3.dataset.metamorph-api
'tech.v3.dataset.metamorph
"src/tech/v3/dataset/metamorph.clj"
'[filter group-by sort-by concat take-nth shuffle
rand-nth update])
)
+293
View File
@@ -0,0 +1,293 @@
(ns ^:no-doc tech.v3.dataset.missing
"Functions for dealing with missing data. Taken (with permission) nearly verbatim
from https://github.com/scicloj/tablecloth"
(:require [tech.v3.dataset.base :as ds-base]
[tech.v3.dataset.column :as col]
[tech.v3.datatype.update-reader :as update-rdr]
[tech.v3.dataset.protocols :as ds-proto]
[tech.v3.datatype.bitmap :as bitmap]
[tech.v3.datatype :as dtype]
[tech.v3.datatype.packing :as packing]
[tech.v3.datatype.protocols :as dtype-proto]
[tech.v3.datatype.casting :as casting]
[tech.v3.datatype.datetime :as dtype-dt]
[clojure.tools.logging :as log])
(:import [org.roaringbitmap RoaringBitmap]
[clojure.lang IFn]
[tech.v3.datatype ObjectReader]
[tech.v3.dataset.impl.column Column]))
(defn- iterable-sequence?
"Check if object is sequential, is column or maybe a reader (iterable)?"
[xs]
(or (sequential? xs)
(and (not (map? xs))
(instance? Iterable xs))))
(defn select-missing
"Select only rows with missing values"
[ds]
(ds-base/select-rows ds (ds-base/missing ds)))
(defn- remove-from-rbitmap
^RoaringBitmap [^RoaringBitmap rb ks]
(let [rb (.clone rb)]
(reduce (fn [^RoaringBitmap rb ^long k]
(.remove rb k)
rb) rb ks)))
(defn- maybe-column-buffer
[col]
(if (ds-proto/is-column? col)
(ds-proto/column-buffer col)
col))
(defn- replace-missing-with-value
[col missing value]
(let [unpack-data (packing/unpack (maybe-column-buffer col))]
(col/new-column (col/column-name col)
(update-rdr/update-reader
unpack-data
(cond
(map? value) value
(iterable-sequence? value) (zipmap missing (cycle value))
:else (bitmap/bitmap-value->map
missing
(let [col-dt (dtype/elemwise-datatype unpack-data)]
(casting/cast value col-dt)))))
(meta col)
(if (map? value)
(remove-from-rbitmap missing (keys value))
(RoaringBitmap.)))))
(defn- missing-direction-prev
^long [^RoaringBitmap rb ^long idx]
(.previousAbsentValue rb idx))
(defn- missing-direction-next
^long [^RoaringBitmap rb ^long idx]
(.nextAbsentValue rb idx))
(defn- replace-missing-with-direction
[[primary-f secondary-f] col missing value]
(let [cnt (dtype/ecount col)
missing-map-pre-fn (fn [f m v]
(let [vv (f missing v)]
(if (< -1 vv cnt)
(assoc m v (dtype/get-value col vv))
m)))
missing-map-fn #(reduce (partial missing-map-pre-fn %1) {} %2)
col (replace-missing-with-value col missing (missing-map-fn primary-f missing))
missing (col/missing col)]
(cond
(empty? missing)
col
(not (nil? value))
(replace-missing-with-value col missing value)
secondary-f
(replace-missing-with-value col missing (missing-map-fn secondary-f missing))
:else
col)))
(defn- replace-missing-with-abb
[col ^RoaringBitmap missing]
(let [^RoaringBitmap non-missing (doto (RoaringBitmap.)
(.add 0 (dtype/ecount col))
(.andNot missing)) ;; prepare non-missing indices
non-missing-cnt (.getCardinality non-missing) ;; how many non-missing we have
;; bootstrap `non-missing-count` values from a column
samples1 (col/select col (repeatedly non-missing-cnt #(.select non-missing (rand-int non-missing-cnt))))
;; bootstrap `missing-count` values for imputation from first bootstrap round
samples2 (col/select samples1 (repeatedly (.getCardinality missing) #(rand-int (dtype/ecount samples1))))]
(replace-missing-with-value col missing samples2)))
;; mid and range
(defn- find-missing-ranges
[^RoaringBitmap missing]
(when-not (empty? missing)
(loop [current (.first missing)
buff []]
(if (neg? current)
buff
(let [p (.previousAbsentValue missing current)
n (.nextAbsentValue missing current)]
(recur (.nextValue missing n) (conj buff [p n])))))))
(defn- find-lerp-values
[cnt col lerp-fn curr [start end]]
(let [no-left? (neg? start)
no-right? (>= end cnt)]
(when-not (and no-left? no-right?) ;; all values are missing?
(let [s (dtype/get-value col (if no-left? end start))
e (dtype/get-value col (if no-right? start end))
start (inc start)
end (min cnt end)
size (- end start)]
(merge curr (zipmap (range start end) (lerp-fn s e size)))))))
(defn- midpoint-lerp-double
[^double start ^double end ^long n-steps]
(let [mid (+ start (/ (- (double end) (double start)) 2.0))]
(dtype/const-reader mid n-steps)))
(defn- midpoint-lerp-object
[start end ^long n-steps]
(let [mid (+ start (/ (- end start) 2.0))]
(dtype/const-reader mid n-steps)))
(defn- lerp-double
[^double start ^double end ^long n-steps]
(let [steps+ (inc n-steps)
data-range (/ (- end start) (double steps+))]
(dtype/make-reader :float64 n-steps
(+ start (* data-range (inc idx))))))
(defn- lerp-object
[start end ^long n-steps]
(let [steps+ (inc n-steps)
data-range (/ (- end start) steps+)]
(dtype/make-reader :object n-steps
(+ start (* data-range (inc idx))))))
(defn- replace-missing-with-lerp
([lerp-type col missing]
(let [ranges (find-missing-ranges missing)
cnt (dtype/ecount col)
orig-col-dtype (dtype/elemwise-datatype col)
[col-dtype col-rdr]
(cond
(dtype-dt/datetime-datatype? orig-col-dtype)
[:float64 (-> (dtype-dt/datetime->milliseconds col)
(dtype-proto/elemwise-cast :float64))]
(casting/numeric-type? orig-col-dtype)
[:float64 (dtype-proto/elemwise-cast col :float64)]
:else
[:object (dtype-proto/->reader col)])
lerp-fn (condp = [col-dtype lerp-type]
[:float64 :midpoint] midpoint-lerp-double
[:object :midpoint] midpoint-lerp-object
[:float64 :lerp] lerp-double
[:object :lerp] lerp-object)
missing-replace (reduce (partial find-lerp-values cnt col-rdr lerp-fn)
{} ranges)
temp-result-col (replace-missing-with-value col-rdr missing missing-replace)]
(if (dtype-dt/datetime-datatype? orig-col-dtype)
(col/new-column (:name (meta col))
(dtype-dt/milliseconds->datetime orig-col-dtype
temp-result-col))
(vary-meta temp-result-col assoc :name (:name (meta col)))))))
(defn- replace-missing-with-nearest
[col missing]
(let [ranges (find-missing-ranges missing)
cnt (dtype/ecount col)
col-dt (dtype/elemwise-datatype col)
lerp-fn (fn [start end ^long n-steps]
(let [hs (quot (inc n-steps) 2)]
(reify ObjectReader
(elemwiseDatatype [rdr] col-dt)
(lsize [rdr] n-steps)
(readObject [rdr idx]
(if (< idx hs) start end)))))
missing-replace (reduce (partial find-lerp-values cnt col lerp-fn)
{} ranges)]
(replace-missing-with-value col missing missing-replace)))
(defn replace-missing-with-strategy
[col missing strategy value]
(let [value (if (fn? value)
(value col)
value)
col-dtype (dtype/elemwise-datatype col)
;;non-numeric columns default to down from lerp or mid
strategy (cond
(and (#{:lerp :midpoint} strategy)
(not (or (casting/numeric-type? col-dtype)
(dtype-dt/datetime-datatype? col-dtype))))
:down
(= strategy :mid)
:nearest
:else
strategy)]
(condp = strategy
:down (replace-missing-with-direction
[missing-direction-prev nil] col missing value)
:up (replace-missing-with-direction
[missing-direction-next nil] col missing value)
:downup (replace-missing-with-direction
[missing-direction-prev missing-direction-next] col missing value)
:updown (replace-missing-with-direction
[missing-direction-next missing-direction-prev] col missing value)
:abb (replace-missing-with-abb col missing)
:lerp (replace-missing-with-lerp :lerp col missing)
:nearest (replace-missing-with-nearest col missing)
:midpoint (replace-missing-with-lerp :midpoint col missing)
(replace-missing-with-value col missing value))))
(defn replace-missing
"Replace missing values in some columns with a given strategy.
The columns selector may be:
- seq of any legal column names
- or a column filter function, such as `numeric` and `categorical`
Strategies may be:
- `:down` - take value from previous non-missing row if possible else use provided value.
- `:up` - take value from next non-missing row if possible else use provided value.
- `:downup` - take value from previous if possible else use next.
- `:updown` - take value from next if possible else use previous.
- `:nearest` - Use nearest of next or previous values. `:mid` is an alias for `:nearest`.
- `:midpoint` - Use midpoint of averaged values between previous and next nonmissing
rows.
- `:abb` - Impute missing with approximate bayesian bootstrap. See [r's ABB](https://search.r-project.org/CRAN/refmans/LaplacesDemon/html/ABB.html).
- `:lerp` - Linearly interpolate values between previous and next nonmissing rows.
- `:value` - Value will be provided - see below.
value may be provided which will then be used. Value may be a function in which
case it will be called on the column with missing values elided and the return will
be used to as the filler."
([ds] (replace-missing ds :mid))
([ds strategy] (replace-missing ds :all strategy))
([ds columns-selector strategy]
(replace-missing ds columns-selector strategy nil))
([ds columns-selector strategy value]
(let [strategy (or strategy :mid)
row-cnt (ds-base/row-count ds)
selected (if (fn? columns-selector)
(columns-selector ds)
(ds-base/select-columns ds columns-selector))]
(->> selected
(ds-base/columns)
(reduce (fn [ds col]
(let [^RoaringBitmap missing (col/missing col)]
(cond
(.isEmpty missing)
ds
(== row-cnt (.getCardinality missing))
(if (and (= :value strategy)
(not (instance? IFn value)))
(assoc ds (:name (meta col)) (dtype/const-reader value (ds-base/row-count ds)))
(do
(log/warnf "Column has no values and strategy (%s) is dependent upon existing values"
strategy)
ds))
:else
(ds-base/add-or-update-column
ds (replace-missing-with-strategy col missing strategy value)))))
ds)))))
+260
View File
@@ -0,0 +1,260 @@
(ns tech.v3.dataset.modelling
"Methods related specifically to machine learning such as setting the inference
target. This file integrates tightly with tech.v3.dataset.categorical which provides
categorical -> number and one-hot transformation pathways.
The functions in this namespace manipulate the metadata on the columns of the dataset, wich can be inspected via `clojure.core/meta`"
(:require [tech.v3.datatype :as dtype]
[tech.v3.datatype.errors :as errors]
[tech.v3.datatype.argops :as argops]
[tech.v3.dataset.base :as ds-base]
[tech.v3.dataset.readers :as ds-readers]
[tech.v3.dataset.categorical :as categorical]
[tech.v3.dataset.column :as ds-col]
[clojure.set :as set])
(:import [java.util List]))
(set! *warn-on-reflection* true)
(defn inference-column?
[col]
(:inference-target? (meta col)))
(defn set-inference-target
"Set the inference target on the column. This sets the :column-type member
of the column metadata to :inference-target?."
[dataset target-name-or-target-name-seq]
(let [colnames (if (sequential? target-name-or-target-name-seq)
target-name-or-target-name-seq
[target-name-or-target-name-seq])]
(ds-base/update-columns dataset colnames
#(vary-meta % assoc :inference-target? true))))
(defn inference-target-column-names
"Return the names of the columns that are inference targets."
[ds]
(->> (map meta (vals ds))
(filter :inference-target?)
(map :name)
(seq)))
(defn inference-target-label-map
[dataset & [label-columns]]
(let [label-columns (or label-columns (inference-target-column-names dataset))]
(errors/when-not-errorf
(= 1 (count label-columns))
"Multiple or zero label columns found: %s" label-columns)
(-> (ds-base/column dataset (first label-columns))
(meta)
(get-in [:categorical-map :lookup-table]))))
(defn inference-target-label-inverse-map
"Given options generated during ETL operations and annotated with :label-columns
sequence container 1 label column, generate a reverse map that maps from a dataset
value back to the label that generated that value."
[dataset & [label-columns]]
(set/map-invert (inference-target-label-map dataset label-columns)))
(defn dataset->categorical-xforms
"Given a dataset, return a map of column-name->xform information."
[ds]
(->> (concat (categorical/dataset->categorical-maps ds)
(categorical/dataset->one-hot-maps ds))
(map (juxt :src-column identity))
(into {})))
(defn num-inference-classes
"Given a dataset and correctly built options from pipeline operations,
return the number of classes used for the label. Error if not classification
dataset."
^long [dataset]
(count (inference-target-label-map dataset)))
(defn feature-ecount
"Number of feature columns. Feature columns are columns that are not
inference targets."
^long [dataset]
(count (remove #(= :inference-target? (meta %))
(vals dataset))))
(defn model-type
"Check the label column after dataset processing.
Return either
:regression
:classification"
[dataset & [column-name-seq]]
(->> (or column-name-seq
(inference-target-column-names dataset))
(ds-base/select-columns dataset)
(vals)
(map (fn [column]
(let [colmeta (meta column)]
[(:name colmeta) (if (:categorical? colmeta)
:classification
:regression)])))
(into {})))
(defn column-values->categorical
"Given a column encoded via either string->number or one-hot, reverse
map to the a sequence of the original string column values.
In the case of one-hot mappings, src-column must be the original
column name before the one-hot map"
[dataset src-column]
(if-let [cmap (->> (categorical/dataset->categorical-maps dataset)
(filter #(= src-column (:src-column %)))
(first))]
(-> (categorical/invert-categorical-map dataset cmap)
(ds-base/column src-column))
(if-let [one-hot-map (->> (categorical/dataset->one-hot-maps dataset)
(filter #(= src-column (:src-column %)))
(first))]
(-> (categorical/invert-one-hot-map dataset one-hot-map)
(ds-base/column src-column))
(errors/throwf
"Column %s does not appear to have either a categorical or one hot map"
src-column))))
(defn- dataset-indexes
[^long n-rows {:keys [randomize-dataset?]
:or {randomize-dataset? true}
:as options}]
(if randomize-dataset?
(argops/argshuffle n-rows options)
(range n-rows)))
(defn k-fold-datasets
"Given 1 dataset, prepary K datasets using the k-fold algorithm.
Randomize dataset defaults to true which will realize the entire dataset
so use with care if you have large datasets.
Returns a sequence of {:test-ds :train-ds}
Options:
* `:randomize-dataset?` - When true, shuffle the dataset. In that case 'seed' may be
provided. Defaults to true.
* `:seed` - when `:randomize-dataset?` is true then this can either be an
implementation of java.util.Random or an integer seed which will be used to
construct java.util.Random."
([dataset k options]
(let [n-rows (ds-base/row-count dataset)
indexes (dataset-indexes n-rows options)
indexes-per-fold (quot n-rows k)
leftover (rem n-rows k)
folds (mapv (fn [^long idx]
(let [start-idx (-> (* idx indexes-per-fold)
(+ (if (< idx leftover)
idx
0)))
glen (+ indexes-per-fold
(if (< idx leftover)
1
0))]
(dtype/sub-buffer indexes start-idx glen)))
(range k))]
(for [i (range k)]
{:test-ds (ds-base/select-rows dataset (nth folds i))
:train-ds (ds-base/select-rows dataset
(->> (keep-indexed #(when (not= %1 i) %2) folds)
(apply concat)))})))
([dataset k]
(k-fold-datasets dataset k {})))
(defn train-test-split
"Probabilistically split the dataset returning a map of `{:train-ds :test-ds}`.
Options:
* `:randomize-dataset?` - When true, shuffle the dataset. In that case 'seed' may be
provided. Defaults to true.
* `:seed` - when `:randomize-dataset?` is true then this can either be an
implementation of java.util.Random or an integer seed which will be used to
construct java.util.Random.
* `:train-fraction` - Fraction of the dataset to use as training set. Defaults to
0.7."
([dataset {:keys [train-fraction]
:or {train-fraction 0.7}
:as options}]
(let [[_n-cols n-rows] (dtype/shape dataset)
indexes (dataset-indexes n-rows options)
n-elems (long n-rows)
n-training (long (Math/round (* n-elems (double train-fraction))))]
{:train-ds (ds-base/select-rows dataset (take n-training indexes))
:test-ds (ds-base/select-rows dataset (drop n-training indexes))}))
([dataset]
(train-test-split dataset {})))
(defn inference-target-ds
"Given a dataset return reverse-mapped inference target columns or nil
in the case where there are no inference targets."
[dataset]
(when-let [target-cols (inference-target-column-names dataset)]
(-> (ds-base/select-columns dataset target-cols)
(categorical/reverse-map-categorical-xforms))))
(defn labels
"Return the labels. The labels sequence is the reverse mapped inference
column. This returns a single column of data or errors out."
[dataset]
(let [
rev-mapped (inference-target-ds dataset)]
(errors/when-not-errorf
(== 1 (ds-base/column-count rev-mapped))
"Incorrect number of columns (%d) in dataset for labels transformation"
(ds-base/column-count rev-mapped))
rev-mapped))
(defn probability-distributions->label-column
"Given a dataset that has columns in which the column names describe labels and the
rows describe a probability distribution, create a label column by taking the max
value in each row and assign column that row value.
Creates a categorical label column which has a catgeorical map in its meta.
"
([prob-ds dst-colname label-column-datatype]
(let [^List cnames (vec (ds-base/column-names prob-ds))
idx-col (->> (ds-readers/value-reader prob-ds)
(dtype/emap
(fn [valvec]
(let [rdr (dtype/->reader valvec :float64)]
;;scan for missing and throw
(dotimes [idx (.lsize rdr)]
(errors/when-not-errorf
(Double/isFinite (.readDouble rdr idx))
"Nan/infinite values not allowed in probability distributions"))
(argops/argmax rdr)))
label-column-datatype)
(dtype/clone))
cat-map (categorical/create-categorical-map
(->> (map-indexed vector cnames)
(into {})
(set/map-invert))
dst-colname
label-column-datatype)
retval
(assoc prob-ds dst-colname
(ds-col/new-column
dst-colname
idx-col
{:categorical? true
:categorical-map cat-map}
(ds-base/missing prob-ds)))]
retval))
([prob-ds dst-colname] (probability-distributions->label-column prob-ds dst-colname :float64)))
+332
View File
@@ -0,0 +1,332 @@
(ns tech.v3.dataset.neanderthal
"Conversion of a dataset to/from a neanderthal dense matrix as well as various
dataset transformations such as pca, covariance and correlation matrixes.
Please include these additional dependencies in your project:
```clojure
[uncomplicate/neanderthal \"0.45.0\"]
```"
(:require [uncomplicate.neanderthal.core :as n-core]
[uncomplicate.neanderthal.native :as n-native]
[uncomplicate.neanderthal.linalg :as linalg]
[uncomplicate.commons.core :as n-com-core]
[tech.v3.libs.neanderthal :as dt-nean]
[tech.v3.libs.neanderthal]
[tech.v3.parallel.for :as pfor]
[tech.v3.datatype.errors :as errors]
[tech.v3.datatype.argops :as argops]
[tech.v3.dataset.tensor :as ds-tens]
[tech.v3.dataset.base :as ds-base]
[tech.v3.dataset.protocols :as ds-proto]
[tech.v3.dataset.impl.dataset :as ds-impl]
[tech.v3.datatype :as dtype]
[tech.v3.datatype.functional :as dfn]
[tech.v3.tensor :as dtt]
[tech.v3.parallel.for :as pfor]
[ham-fisted.defprotocol :refer [extend-type]]
[clojure.tools.logging :as log])
(:refer-clojure :exclude [extend-type]))
(defn dataset->dense
"Convert a dataset into a dense neanderthal CPU matrix. If the matrix
is column-major, then potentially you can get accelerated copies from the dataset
into neanderthal.
* neanderthal-layout - either :column for a column-major matrix or :row for a row-major
matrix.
* datatype - either :float64 or :float32"
([dataset neanderthal-layout datatype]
(let [[n-cols n-rows] (dtype/shape dataset)
retval (case datatype
:float64
(n-native/dge n-rows n-cols {:layout neanderthal-layout})
:float32
(n-native/fge n-rows n-cols {:layout neanderthal-layout}))
tens (dtt/ensure-tensor retval)
tens-cols (dtt/columns tens)]
;;If possible, these will be accelerated copies
(->> (pfor/pmap (fn [tens-col ds-col]
(dtype/copy! ds-col tens-col))
tens-cols
(vals dataset))
(dorun))
retval))
([dataset neanderthal-layout]
(dataset->dense dataset neanderthal-layout :float64))
([dataset]
(dataset->dense dataset :column :float64)))
(defn dense->dataset
"Given a neanderthal matrix, convert its columns into the columns of a
tech.v3.dataset. This does the conversion in-place. If you would like to copy
the neanderthal matrix into JVM arrays, then after method use dtype/clone."
[matrix]
(->> (n-core/cols matrix)
(map dtt/ensure-tensor)
(ds-impl/new-dataset :neandtheral)))
(defn fit-pca!
"Run Principle Component Analysis on a tensor.
Keep in mind that PCA may be highly influenced by outliers in the dataset
and a probabilistic or some level of auto-encoder dimensionality reduction
more effective for your problem.
Returns a map of:
* :means - vec of means
* :eigenvalues - vec of eigenvalues. These are the variance of columns of the
post-projected tensor if :cov is used. They are in the ballpark if :svd is used.
* :eigenvectors - matrix of eigenvectors
Options:
- method - svd, cov - Either use SVD or covariance based method. SVD is faster
but covariance method means the post-projection variances are accurate. Both
methods produce an identical or extremely similar projection matrix. Defaults
to `:cov`.
- covariance-bias? - When using :cov, divide by n-rows if true and (dec n-rows)
if false. defaults to false."
([tensor {:keys [method
covariance-bias?]
:or {method :cov
covariance-bias? false}
:as _options}]
(let [{:keys [means tensor]} (ds-tens/mean-center-columns! tensor {:nan-strategy :keep})
[n-rows n-cols] (dtype/shape tensor)
n-rows (long n-rows)
n-cols (long n-cols)
matrix (dt-nean/tensor->matrix tensor)]
(case method
:svd
(let [{:keys [sigma u vt]} (linalg/svd matrix false true)
retval {:eigenvalues (dtype/clone (dtt/as-tensor (n-core/dia sigma)))
:means means
:method :svd
:eigenvectors (dtype/clone (dtt/transpose (dtt/as-tensor vt) [1 0]))}]
(n-com-core/release matrix)
(n-com-core/release sigma)
(n-com-core/release u)
(n-com-core/release vt)
retval)
:cov
(let [;;Because we have subtracted out the means above, the covariance matrix
;;is defined by (/ (Xt*X) (- n-rows 1))
tens-dt (dtype/elemwise-datatype tensor)
trans-mat (n-core/trans matrix)
cov-mat (n-core/mm trans-mat matrix)
bias (double (if covariance-bias?
n-rows
(dec n-rows)))
cov-buf (dtype/as-buffer cov-mat)
;; Neanderthal's vecmath space failed us here
inv-bias (/ 1.0 bias)
_ (pfor/parallel-for idx (* n-cols n-cols)
(.writeDouble cov-buf idx
(* inv-bias (.readDouble cov-buf idx))))
fact (dt-nean/datatype->native-factory tens-dt)
w (n-core/ge fact n-cols 1)
vl (n-core/ge fact n-cols n-cols)
sym-cov (n-core/view-sy cov-mat)
_ (linalg/ev! sym-cov w vl nil)
wt (dtt/as-tensor w)
eigvals (dtt/select wt :all 0)
validx (argops/argsort :tech.numerics/> eigvals)
eigvals (dtype/clone (dtt/select eigvals validx))
eigvecs (dtype/clone (dtt/select vl :all validx))]
(n-com-core/release matrix)
(n-com-core/release cov-mat)
(n-com-core/release w)
(n-com-core/release vl)
{:eigenvalues eigvals
:means means
:eigenvectors eigvecs
:method :cov}))))
([tensor]
(fit-pca! tensor nil)))
(defn transform-pca!
"PCA transform the dataset returning a new tensor. Mean-centers
the tensor in-place."
[tensor pca-info n-components]
(let [[_n-row n-cols] (dtype/shape tensor)
eigenvectors (:eigenvectors pca-info)
[_n-eig-rows n-eig-cols] (dtype/shape eigenvectors)
_ (errors/when-not-errorf
(= (long n-cols) (long n-eig-cols))
"Column count of dataset (%d) does not match column count of eigenvectors (%d)"
n-cols n-eig-cols)
_ (errors/when-not-errorf
(<= (long n-components) (long n-cols))
"Num components (%d) must be <= num cols (%d)"
n-components n-cols)
tensor (:tensor (ds-tens/mean-center-columns! tensor (select-keys pca-info [:means])))
project-matrix (dtt/select eigenvectors :all (range n-components))
ntens (dt-nean/tensor->matrix tensor)
nproj (dt-nean/tensor->matrix project-matrix nil (dtype/elemwise-datatype tensor))
resmat (n-core/mm ntens nproj)
result (dtype/clone (dtt/as-tensor resmat))]
(n-com-core/release ntens)
(n-com-core/release nproj)
(n-com-core/release resmat)
result))
(defrecord PCATransform [means eigenvalues eigenvectors n-components result-datatype])
(def ^:private neanderthal-fns*
(delay
(try
{:fit-pca (requiring-resolve 'tech.v3.dataset.neanderthal/fit-pca!)
:transform-pca (requiring-resolve 'tech.v3.dataset.neanderthal/transform-pca!)}
(catch Exception e
(log/debugf "Neanderthal loading failed: %s" (str e))
{}))))
(defn neanderthal-enabled?
[]
(empty? @neanderthal-fns*))
(defn fit-pca
"Run PCA on the dataset. Dataset must not have missing values
or non-numeric string columns.
Keep in mind that PCA may be highly influenced by outliers in the dataset
and a probabilistic or some level of auto-encoder dimensionality reduction
more effective for your problem.
Returns pca-info:
{:means - vec of means
:eigenvalues - vec of eigenvalues
:eigenvectors - matrix of eigenvectors
}
Use transform-pca with a dataset and the the returned value to perform
PCA on a dataset.
Options:
- method - svd, cov - Either use SVD or covariance based method. SVD is faster
but covariance method means the post-projection variances are accurate.
Defaults to cov. Both methods produce similar projection matrixes.
- variance-amount - fractional amount of variance to keep. Defaults to 0.95.
- n-components - If provided overrides variance amount and sets the number of
components to keep. This controls the number of result columns directly as an
integer.
- covariance-bias? - When using :cov, divide by n-rows if true and (dec n-rows)
if false. defaults to false."
(^PCATransform [dataset {:keys [n-components variance-amount]
:or {variance-amount 0.95} :as options}]
(errors/when-not-error
(== 0 (dtype/ecount (ds-base/missing dataset)))
"Cannot pca a dataset with missing entries. See replace-missing.")
(let [result-datatype :float64
{:keys [eigenvalues] :as pca-result}
(fit-pca! (ds-tens/dataset->tensor dataset :float64) options)
;;The eigenvalues are the variance.
variance-amount (double variance-amount)
;;We know the eigenvalues are sorted from greatest to least
used-variance? (nil? n-components)
n-components (long
(or n-components
(let [variance-sum (double (dfn/reduce-+ eigenvalues))
target-variance-sum (* variance-amount variance-sum)
n-variance (dtype/ecount eigenvalues)]
(loop [idx 0
tot-var 0.0]
(if (and (< idx n-variance)
(< tot-var target-variance-sum))
(recur (unchecked-inc idx)
(+ tot-var (double (nth eigenvalues idx))))
idx)))))]
(map->PCATransform (merge (assoc pca-result
:n-components n-components
:result-datatype result-datatype)
(when used-variance?
{:variance-amount variance-amount})))))
(^PCATransform [dataset]
(fit-pca dataset nil)))
(defn transform-pca
"PCA transform the dataset returning a new dataset. The method used to generate the pca information
is indicated in the metadata of the dataset."
[dataset {:keys [n-components result-datatype] :as pca-transform}]
(-> (ds-tens/dataset->tensor dataset result-datatype)
(transform-pca! pca-transform n-components)
(ds-tens/tensor->dataset dataset :pca-result)
(vary-meta assoc :pca-method (:method pca-transform))))
(extend-type PCATransform
ds-proto/PDatasetTransform
(transform [t dataset]
(transform-pca dataset t)))
#_(defn mean-std[rets]
(let [N (nc/mrows rets)
A (nn/fge rets)
fge1s (nc/entry! (nn/fge 1 N) 1)
means (nc/scal! (/ 1.0 N) (nc/mm fge1s A))
-ones (nc/scal -1 (nc/view-vctr fge1s))
-meancols (nc/rk -ones (nc/view-vctr means))
A-mcm (nc/axpy! 1.0 A (nc/copy -meancols))
A-means-sqr-summed (nc/mm fge1s (nm/sqr A-mcm))
A-stdevs (nm/sqrt (nc/scal (/ 1.0 (dec N)) A-means-sqr-summed))
A-sharpes (nc/ax (Math/sqrt 252) (nm/div means A-stdevs))]
{:means means :mcm A-mcm :stdevs A-stdevs :sharpes A-sharpes}))
#_(defn cov-cor[rets]
(let [mstd (mean-std rets)
N (nc/mrows rets)
A-mcm (:mcm mstd)
A-stdevs (:stdevs mstd)
A-cov (nc/scal (/ 1.0 (dec N)) (nc/mm (nc/trans A-mcm) A-mcm))
sigx*sigy (nc/rk (nc/view-vctr A-stdevs) (nc/view-vctr A-stdevs))
A-cor (nm/div A-cov sigx*sigy)]
(merge mstd {:covmatrix A-cov :corrmatrix A-cor})))
#_(defn matrix-desc-stats
"Return a set of information about neanderthal matrix A
- per-column means
- per-column variances
- per-column standard deviations
- covariance matrix
- correlation matrix"
([A] (matrix-desc-stats nil A))
([options A]
(with-release [n-cols (second (nc/dims a))
fge1s (entry! (nm/fge 1 5) 1)
means (scal! (/ 1.0 5) (nc/mm fge1s A))
-meancols (rk (nc/scal -1 (nc/view-vctr fge1s)) (nc/view-vctr means))
A-means (axpy! 1.0 A (copy -meancols))
A-means-sqr (vm/sqr A-means)
A-means-sqr-summed (mm fge1s A-means-sqr)
A-var (nc/scal (/ 1.0 5) A-means-sqr-summed)
A-stdevs (vm/sqrt A-var)
A-cov (nc/scal (/ 1.0 5) (mm (trans A-means) A-means))
sigx*sigy (rk (view-vctr A-stdevs) (view-vctr A-stdevs))
A-cor (vm/div A-cov sigx*sigy)]
(pr-str [A
means -meancols
A-means A-means-sqr
A-means-sqr-summed
A-var
A-stdevs
A-cov
sigx*sigy
A-cor]))))
+376
View File
@@ -0,0 +1,376 @@
(ns tech.v3.dataset.print
(:require [tech.v3.dataset.protocols :as ds-proto]
[tech.v3.dataset.format-sequence :as format-sequence]
[tech.v3.datatype :as dtype]
[tech.v3.datatype.casting :as casting]
[tech.v3.datatype.pprint :as dtype-pp]
[tech.v3.datatype.datetime :as dtype-dt]
[tech.v3.datatype.packing :as packing]
[tech.v3.datatype.graal-native :as graal-native]
[clojure.string :as str]
[ham-fisted.lazy-noncaching :as lznc]
[ham-fisted.api :as hamf]
[ham-fisted.function :as hamf-fn])
(:import [tech.v3.datatype ObjectReader]
[java.util List ArrayList]
[org.roaringbitmap RoaringBitmap]))
(graal-native/when-not-defined-graal-native
(require '[clojure.pprint :as pp]))
(set! *warn-on-reflection* true)
;;The default number of rows to print
(def ^:dynamic ^:no-doc *default-table-row-print-length* 20)
;;The default line policy - see dataset-data->str
(def ^:dynamic ^:no-doc *default-print-line-policy* :repl)
;;The default max width with 'nil' indicating no limit.
(def ^:dynamic ^:no-doc *default-print-column-max-width* nil)
;;The default to show/hide column types
(def ^:dynamic ^:no-doc *default-print-column-types?* false)
(defn- print-stringify
[item]
(-> (if (or (vector? item)
(map? item)
(set? item))
(with-out-str
(graal-native/if-defined-graal-native
(println item)
(do
(require '[clojure.pprint :as pp])
(pp/pprint item))))
(dtype-pp/format-object item))
(str/replace "|" "\\|")))
(defn- reader->string-lines
[reader-data ^RoaringBitmap missing line-policy column-max-width new-number-format?
maximum-precision]
(let [reader-data (if (and new-number-format?
(#{:float32 :float64} (dtype/elemwise-datatype reader-data)))
(vec (format-sequence/format-sequence
reader-data
(long (or maximum-precision 8))))
reader-data)]
(reify ObjectReader
(lsize [rdr] (dtype/ecount reader-data))
(readObject [rdr idx]
(if (.contains missing (int idx))
nil
(let [lines (str/split-lines (print-stringify (reader-data idx)))
lines (if (number? column-max-width)
(let [width (long column-max-width)]
(->> lines (map (fn [^String line]
(if (> (count line) width)
(.substring line 0 width)
line)))))
lines)]
(case line-policy
:single
[(first lines)]
:markdown
[(str/join "<br>" lines)]
:repl
lines)))))))
(defn- append-line!
[^StringBuilder builder line]
(.append builder line)
(.append builder "\n"))
(defn- rpad-str
[col-width line]
(let [n-data (count line)
n-pad (- (long col-width) n-data)
builder (StringBuilder.)]
(.append builder line)
(dotimes [_idx n-pad]
(.append builder " "))
(.toString builder)))
(defn dataset-data->str
"Convert the dataset values to a string.
Options may be provided in the dataset metadata or may be provided
as an options map. The options map overrides the dataset metadata.
* `:print-index-range` - The set of indexes to print. If an integer then
is interpreted according to `:print-style`. Defaults to the integer
`*default-table-row-print-length*`.
* `:print-style` - Defaults to :first-last. Options are #{:first-last :first :last}. In
the case `:print-index-range` is an integer and the dataset has more than that number of
rows prints the first N/2 and last N/2 rows or the first N or last N rows.
* `:print-line-policy` - defaults to `:repl` - one of:
- `:repl` - multiline table - default nice printing for repl
- `:markdown` - lines delimited by <br>
- `:single` - Only print first line
* `:print-column-max-width` - set the max width of a column when printing.
* `:print-column-types?` - show/hide column types.
* `:maximum-precision` - When provided, the maximum double precision as an integer.
* `:elide-header?` - When true, the header such as `test/data/alldtypes.arrow-feather-compressed [1000 15]:` is hidden.
Examples of print styles:
```clojure
user> (require '[tech.v3.dataset :as ds])
nil
user> (require '[tech.v3.libs.arrow :as arrow])
nil
user> (def ds (ds/->dataset \"test/data/alldtypes.arrow-feather-compressed\" {:file-type :arrow}))
08:26:03.156 [tech.resource.gc ref thread] INFO tech.v3.resource.gc - Reference thread starting
#'user/ds
user> (vary-meta ds assoc :print-style :last :print-index-range 10)
test/data/alldtypes.arrow-feather-compressed [1000 15]:
| uints | longs | ubytes | strings | doubles | ushorts | local_times | local_dates | ints | instants | shorts | bytes | boolean | floats | text |
|------:|------:|-------:|---------|--------:|--------:|--------------|-------------|-----:|--------------------:|-------:|------:|---------|-------:|------|
| 990 | 990 | 222 | 990 | 990.0 | 990 | 13:39:59.923 | 2022-02-19 | 990 | 1645303199916000000 | 990 | -34 | | 990.0 | 990 |
| 991 | 991 | 223 | 991 | 991.0 | 991 | 13:39:59.923 | 2022-02-19 | 991 | 1645303199916000000 | 991 | -33 | | 991.0 | 991 |
| 992 | 992 | 224 | 992 | 992.0 | 992 | 13:39:59.923 | 2022-02-19 | 992 | 1645303199916000000 | 992 | -32 | | 992.0 | 992 |
| 993 | 993 | 225 | 993 | 993.0 | 993 | 13:39:59.923 | 2022-02-19 | 993 | 1645303199916000000 | 993 | -31 | | 993.0 | 993 |
| 994 | 994 | 226 | 994 | 994.0 | 994 | 13:39:59.923 | 2022-02-19 | 994 | 1645303199916000000 | 994 | -30 | | 994.0 | 994 |
| 995 | 995 | 227 | 995 | 995.0 | 995 | 13:39:59.923 | 2022-02-19 | 995 | 1645303199916000000 | 995 | -29 | | 995.0 | 995 |
| 996 | 996 | 228 | 996 | 996.0 | 996 | 13:39:59.923 | 2022-02-19 | 996 | 1645303199916000000 | 996 | -28 | | 996.0 | 996 |
| 997 | 997 | 229 | 997 | 997.0 | 997 | 13:39:59.923 | 2022-02-19 | 997 | 1645303199916000000 | 997 | -27 | | 997.0 | 997 |
| 998 | 998 | 230 | 998 | 998.0 | 998 | 13:39:59.923 | 2022-02-19 | 998 | 1645303199916000000 | 998 | -26 | | 998.0 | 998 |
| 999 | 999 | 231 | 999 | 999.0 | 999 | 13:39:59.923 | 2022-02-19 | 999 | 1645303199916000000 | 999 | -25 | | 999.0 | 999 |
user> (vary-meta ds assoc :print-style :first :print-index-range 10)
test/data/alldtypes.arrow-feather-compressed [1000 15]:
| uints | longs | ubytes | strings | doubles | ushorts | local_times | local_dates | ints | instants | shorts | bytes | boolean | floats | text |
|------:|------:|-------:|---------|--------:|--------:|--------------|-------------|-----:|--------------------:|-------:|------:|---------|-------:|------|
| 0 | 0 | 0 | 0 | 0.0 | 0 | 13:39:59.908 | 2022-02-19 | 0 | 1645303199909000000 | 0 | 0 | true | 0.0 | 0 |
| 1 | 1 | 1 | 1 | 1.0 | 1 | 13:39:59.910 | 2022-02-19 | 1 | 1645303199911000000 | 1 | 1 | false | 1.0 | 1 |
| 2 | 2 | 2 | 2 | 2.0 | 2 | 13:39:59.910 | 2022-02-19 | 2 | 1645303199911000000 | 2 | 2 | true | 2.0 | 2 |
| 3 | 3 | 3 | 3 | 3.0 | 3 | 13:39:59.910 | 2022-02-19 | 3 | 1645303199911000000 | 3 | 3 | true | 3.0 | 3 |
| 4 | 4 | 4 | 4 | 4.0 | 4 | 13:39:59.910 | 2022-02-19 | 4 | 1645303199911000000 | 4 | 4 | false | 4.0 | 4 |
| 5 | 5 | 5 | 5 | 5.0 | 5 | 13:39:59.910 | 2022-02-19 | 5 | 1645303199911000000 | 5 | 5 | false | 5.0 | 5 |
| 6 | 6 | 6 | 6 | 6.0 | 6 | 13:39:59.910 | 2022-02-19 | 6 | 1645303199911000000 | 6 | 6 | true | 6.0 | 6 |
| 7 | 7 | 7 | 7 | 7.0 | 7 | 13:39:59.910 | 2022-02-19 | 7 | 1645303199911000000 | 7 | 7 | false | 7.0 | 7 |
| 8 | 8 | 8 | 8 | 8.0 | 8 | 13:39:59.910 | 2022-02-19 | 8 | 1645303199911000000 | 8 | 8 | false | 8.0 | 8 |
| 9 | 9 | 9 | 9 | 9.0 | 9 | 13:39:59.910 | 2022-02-19 | 9 | 1645303199911000000 | 9 | 9 | true | 9.0 | 9 |
;; first-last is default with print-index-range of 20
user> (vary-meta ds assoc :print-style :first-last :print-index-range 10)
test/data/alldtypes.arrow-feather-compressed [1000 15]:
| uints | longs | ubytes | strings | doubles | ushorts | local_times | local_dates | ints | instants | shorts | bytes | boolean | floats | text |
|------:|------:|-------:|---------|--------:|--------:|--------------|-------------|-----:|--------------------:|-------:|------:|---------|-------:|------|
| 0 | 0 | 0 | 0 | 0.0 | 0 | 13:39:59.908 | 2022-02-19 | 0 | 1645303199909000000 | 0 | 0 | true | 0.0 | 0 |
| 1 | 1 | 1 | 1 | 1.0 | 1 | 13:39:59.910 | 2022-02-19 | 1 | 1645303199911000000 | 1 | 1 | false | 1.0 | 1 |
| 2 | 2 | 2 | 2 | 2.0 | 2 | 13:39:59.910 | 2022-02-19 | 2 | 1645303199911000000 | 2 | 2 | true | 2.0 | 2 |
| 3 | 3 | 3 | 3 | 3.0 | 3 | 13:39:59.910 | 2022-02-19 | 3 | 1645303199911000000 | 3 | 3 | true | 3.0 | 3 |
| 4 | 4 | 4 | 4 | 4.0 | 4 | 13:39:59.910 | 2022-02-19 | 4 | 1645303199911000000 | 4 | 4 | false | 4.0 | 4 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 994 | 994 | 226 | 994 | 994.0 | 994 | 13:39:59.923 | 2022-02-19 | 994 | 1645303199916000000 | 994 | -30 | | 994.0 | 994 |
| 995 | 995 | 227 | 995 | 995.0 | 995 | 13:39:59.923 | 2022-02-19 | 995 | 1645303199916000000 | 995 | -29 | | 995.0 | 995 |
| 996 | 996 | 228 | 996 | 996.0 | 996 | 13:39:59.923 | 2022-02-19 | 996 | 1645303199916000000 | 996 | -28 | | 996.0 | 996 |
| 997 | 997 | 229 | 997 | 997.0 | 997 | 13:39:59.923 | 2022-02-19 | 997 | 1645303199916000000 | 997 | -27 | | 997.0 | 997 |
| 998 | 998 | 230 | 998 | 998.0 | 998 | 13:39:59.923 | 2022-02-19 | 998 | 1645303199916000000 | 998 | -26 | | 998.0 | 998 |
| 999 | 999 | 231 | 999 | 999.0 | 999 | 13:39:59.923 | 2022-02-19 | 999 | 1645303199916000000 | 999 | -25 | | 999.0 | 999 |
```
Example of conservative printing:
```clojure
tech.ml.dataset.github-test> (def ds (with-meta ds
(assoc (meta ds)
:print-column-max-width 25
:print-line-policy :single)))
```"
([dataset]
(dataset-data->str dataset {}))
([dataset options]
(let [options (merge (meta dataset) options)
{:keys [print-index-range print-line-policy
print-column-max-width print-column-types?
maximum-precision]} options
n-rows (long (second (dtype/shape dataset)))
index-range (or print-index-range
(min n-rows *default-table-row-print-length*))
print-style (when (number? index-range) (get options :print-style :first-last))
[index-range ellipses?] (if (number? index-range)
(case print-style
:first-last
(if (> n-rows (long index-range))
(let [start-n (quot (long index-range) 2)
end-start (dec (- n-rows start-n))]
[(vec (concat (range start-n)
(range end-start n-rows)))
true])
[(range n-rows) false])
:first
[(range (min index-range n-rows)) false]
:last
[(range (max 0 (- n-rows (long index-range))) n-rows) false])
[index-range false])
line-policy (or print-line-policy *default-print-line-policy*)
column-width (or print-column-max-width *default-print-column-max-width*)
column-types? (or print-column-types? *default-print-column-types?*)
print-ds (if (keyword? index-range)
(if (identical? index-range :all)
dataset
(throw (RuntimeException. (str "Unrecognized index range keyword: "
index-range))))
(ds-proto/select-rows dataset
(lznc/filter
(hamf-fn/long-predicate
idx (and (< idx n-rows)
(>= idx (- n-rows))))
index-range)))
column-names (map #(when (some? %) (.toString ^Object %)) (keys print-ds))
column-types (map #(str (when column-types? (:datatype (meta %))))
(vals print-ds))
string-columns (map #(-> (dtype/->reader %)
(reader->string-lines (ds-proto/missing %)
line-policy
column-width
true
maximum-precision)
;;Do the conversion to string once.
(dtype/clone)
(dtype/->reader))
(vals print-ds))
string-columns (if ellipses?
(let [insert-pos (quot (dtype/ecount index-range) 2)]
(->> string-columns
(map (fn [str-col]
(vec (concat (take insert-pos str-col)
[["..."]]
(drop insert-pos str-col)))))))
string-columns)
n-rows (long (second (dtype/shape print-ds)))
n-rows (long (if ellipses?
(inc n-rows)
n-rows))
row-heights (ArrayList.)
_ (.addAll row-heights (repeat n-rows 1))
column-widths
(->> string-columns
(map (fn [coltype colname coldata]
(->> coldata
(map-indexed
(fn [row-idx lines]
;;Side effecting record row height.
(.set row-heights (int row-idx)
(max (int (.get row-heights row-idx))
(count lines)))
(apply max 0 (map count lines))))
(apply max (count coltype) (count colname))))
column-types
column-names))
spacers (map #(apply str (repeat % "-")) column-widths)
fmts (map #(str "%" % "s") column-widths)
fmt-row (fn [leader divider trailer row]
(str leader
(apply str
(interpose
divider
(map #(format %1 %2) fmts row)))
trailer))
builder (StringBuilder.)]
(append-line! builder (fmt-row "| " " | " " |" column-names))
(when column-types? (append-line! builder (fmt-row "| " " | " " |" column-types)))
(append-line!
builder
(apply str
(concat (mapcat (fn [spacer dtype]
(let [numeric? (and
(casting/numeric-type? dtype)
(not (dtype-dt/datetime-datatype?
dtype)))]
(concat ["|-"]
spacer
(if numeric?
":"
"-"))))
spacers (map dtype/elemwise-datatype
(vals print-ds)))
["|"])))
(dotimes [idx n-rows]
(let [row-height (long (.get row-heights idx))]
(dotimes [inner-idx row-height]
(let [row-data
(->> string-columns
(map (fn [c-width column]
(let [lines (column idx)]
(if (< inner-idx (count lines))
(if (== 1 (count lines))
(.get ^List lines inner-idx)
(->> (.get ^List lines inner-idx)
(rpad-str c-width)))
"")))
column-widths))]
(append-line! builder (fmt-row "| " " | " " |" row-data))))))
(.toString builder))))
(defn dataset->str
"Convert a dataset to a string. Prints a single line header and then calls
dataset-data->str.
For options documentation see dataset-data->str."
([ds options]
(if (= [0 0] (dtype/shape ds))
(format "%s %s"
(ds-proto/dataset-name ds)
;;make row major shape to avoid confusion
(vec (reverse (dtype/shape ds))))
(let [options (merge (meta ds) options)
elide-header? (get options :elide-header?)
header (if elide-header?
""
(format "%s %s:\n\n"
(ds-proto/dataset-name ds)
(vec (reverse (dtype/shape ds)))))]
(str header (dataset-data->str ds options)))))
([ds]
(dataset->str ds {})))
(defn print-range
"Convenience function to set the number of rows to print.\n
Defaults to (range *default-table-row-print-length*) - one of:
- n - prints the first n rows
- range - prints the rows at positions corresponding to the range
- `:all` - prints all the rows in a dataset"
[dataset index-range]
(-> dataset
(vary-meta assoc :print-index-range index-range)))
(defn print-policy
"Convenience function to vary printing behavior.\n
Defaults to `:repl` - one of:
- `:repl` - multiline table - default nice printing for repl
- `:markdown` - lines delimited by <br>
- `:single` - Only print first line"
[dataset line-policy]
(-> dataset
(vary-meta assoc :print-line-policy line-policy)))
(defn print-width
"Convenience function to set the max width of a column when printing."
[dataset column-width]
(-> dataset
(vary-meta assoc :print-column-max-width column-width)))
(defn print-types
"Convenience function to show/hide column types"
[dataset column-types]
(-> dataset
(vary-meta assoc :print-column-types? column-types)))
+145
View File
@@ -0,0 +1,145 @@
(ns tech.v3.dataset.protocols
(:require [tech.v3.datatype.protocols :as dtype-proto]
[ham-fisted.protocols :as hamf-proto]
[ham-fisted.defprotocol :refer [defprotocol extend-type extend-protocol]])
(:import [org.roaringbitmap RoaringBitmap]
[tech.v3.datatype Buffer]
[clojure.lang IDeref])
(:refer-clojure :exclude [merge defprotocol extend-type extend-protocol]))
(defprotocol PRowCount
(^long row-count [this]))
(defprotocol PColumnCount
(^long column-count [this]))
(defprotocol PMissing
(missing [this])
(num-missing [this])
(any-missing? [this]))
(defn missing-count
^long [this] (num-missing this))
(defprotocol PValidRows
(valid-rows [this]))
(defprotocol PSelectRows
(select-rows [this rowidxs]))
(defprotocol PRowFilter
(row-filter [this pred?]))
(defprotocol PSelectColumns
(select-columns [this colnames]))
(defprotocol PColumn
(is-column? [col])
(column-buffer [col]
"Return either the backing dataset for dense columns or tuple of [indexes,data] for sparse columns")
(empty-column? [col])
(column-data [col]
"Return the backing data store. Note for sparse columns this may be much shorter than the column")
(with-column-data [col new-data]
"new data must be same length as old data"))
(defprotocol PDataset
(is-dataset? [item])
;;error on failure
(column [ds colname])
;;indexable object.
(^Buffer rows [ds options])
(^Buffer rowvecs [ds options]))
(defprotocol PDatasetParser
"Protocols for the dataset parser created via (dataset-parser)."
(add-row [p row]
"row needs to reduce to a sequence of objects implementing -key and -val")
(add-rows [p rows]
"rows need only be reducible"))
(defprotocol PClearable
(ds-clear [p]
"Reset to initial state. Avoid conflict with collection/clear"))
(defprotocol PDatasetTransform
(transform [t dataset]))
(extend-type Object
PDataset
(is-dataset? [item] false)
PColumn
(is-column? [item] false)
PRowCount
(row-count [this] (dtype-proto/ecount this))
PColumnCount
(column-count [this] 0)
PMissing
(missing [this] (RoaringBitmap.)))
(extend-type nil
PDataset
(is-dataset? [item] false)
PColumn
(is-column? [item] false)
PRowCount
(row-count [this] 0)
PColumnCount
(column-count [this] 0)
PMissing
(missing [this] (RoaringBitmap.)))
(defprotocol PColumnName
(column-name [col]))
(extend-protocol PColumnName
nil
(column-name [col] nil)
Object
(column-name [col]
(if (map? col)
(get col :tech.v3.dataset/name
(get-in col [:tech.v3.dataset/metadata :name]))
(:name (meta col)))))
(defn dataset-name
[ds]
(:name (meta ds)))
(defn set-name
[item nm]
(vary-meta item assoc :name nm))
(defprotocol PDatasetReducer
"An object that produces a per-dataset reducer and can merge contexts.
It is also expected that this implements hamf-proto/Finalize"
(ds->reducer [this ds])
(merge [this lctx rctx]))
(extend-type Object
PDatasetReducer
(ds->reducer [this ds] this))
;;For large reductions we may want to combine reducers on a single column when
;;possible.
(defprotocol PReducerCombiner
"Some reduces such as ones based on apache can be specified separately
in the output map but actually for efficiency need be represented by 1
concrete reducer."
(reducer-combiner-key [reducer]))
(extend-protocol PReducerCombiner
Object
(reducer-combiner-key [reducer] nil))
+23
View File
@@ -0,0 +1,23 @@
(ns ^:no-doc tech.v3.dataset.readers
(:require [tech.v3.dataset.protocols :as ds-proto]
[ham-fisted.api :as hamf])
(:import [tech.v3.datatype Buffer]))
(defn value-reader
"Return a reader that produces a reader of column values per index.
Options:
:copying? - Default to false - When true row values are copied on read."
(^Buffer [dataset options]
(ds-proto/rowvecs dataset options))
(^Buffer [dataset]
(value-reader dataset nil)))
(defn mapseq-reader
"Return a reader that produces a map of column-name->column-value
upon read."
(^Buffer [dataset options]
(ds-proto/rows dataset options))
(^Buffer [dataset]
(mapseq-reader dataset nil)))
+678
View File
@@ -0,0 +1,678 @@
(ns tech.v3.dataset.reductions
"Specific high performance reductions intended to be performend over a sequence
of datasets. This allows aggregations to be done in situations where the dataset is
larger than what will fit in memory on a normal machine. Due to this fact, summation
is implemented using Kahan algorithm and various statistical methods are done in using
statistical estimation techniques and thus are prefixed with `prob-` which is short
for `probabilistic`.
* `aggregate` - Perform a multi-dataset aggregation. Returns a dataset with row.
* `group-by-column-agg` - Perform a multi-dataset group-by followed by
an aggregation. Returns a dataset with one row per key.
Examples:
```clojure
user> (require '[tech.v3.dataset :as ds])
nil
user> (require '[tech.v3.datatype.datetime :as dtype-dt])
nil
user> (def stocks (-> (ds/->dataset \"test/data/stocks.csv\" {:key-fn keyword})
(ds/update-column :date #(dtype-dt/datetime->epoch :epoch-days %))))
#'user/stocks
user> (require '[tech.v3.dataset.reductions :as ds-reduce])
nil
user> (ds-reduce/group-by-column-agg
:symbol
{:symbol (ds-reduce/first-value :symbol)
:price-avg (ds-reduce/mean :price)
:price-sum (ds-reduce/sum :price)
:price-med (ds-reduce/prob-median :price)}
(repeat 3 stocks))
:symbol-aggregation [5 4]:
| :symbol | :price-avg | :price-sum | :price-med |
|---------|--------------|------------|--------------|
| IBM | 91.26121951 | 33675.39 | 88.70468750 |
| AAPL | 64.73048780 | 23885.55 | 37.05281250 |
| MSFT | 24.73674797 | 9127.86 | 24.07277778 |
| AMZN | 47.98707317 | 17707.23 | 41.35142361 |
| GOOG | 415.87044118 | 84837.57 | 422.69722222 |
```
* [zero-one benchmark winner](https://github.com/zero-one-group/geni-performance-benchmark/blob/da4d02e54de25a72214f72c4864ebd3d307520f8/dataset/src/dataset/optimised_by_chris.clj)"
(:require [tech.v3.datatype :as dtype]
[tech.v3.datatype.bitmap :as bitmap]
[tech.v3.datatype.casting :as casting]
[tech.v3.datatype.sampling :as dt-sample]
[tech.v3.datatype.statistics :as dt-stats]
[tech.v3.datatype.argtypes :as argtypes]
[tech.v3.datatype.export-symbols :refer [export-symbols]]
[tech.v3.dataset.protocols :as ds-proto]
[tech.v3.dataset.base :as ds-base]
[tech.v3.dataset.readers :as ds-readers]
[tech.v3.dataset.io :as ds-io]
[tech.v3.dataset.impl.column :as col-impl]
[tech.v3.dataset.impl.dataset :as ds-impl]
[tech.v3.dataset.column :as ds-col]
[tech.v3.dataset.io.mapseq-colmap :as io-mapseq]
[tech.v3.dataset.reductions.impl :as impl]
[tech.v3.dataset.reductions.apache-data-sketch :as sketch]
[tech.v3.parallel.for :as parallel-for]
[ham-fisted.api :as hamf]
[ham-fisted.reduce :as hamf-rf]
[ham-fisted.function :as hamf-fn]
[ham-fisted.protocols :as hamf-proto]
[ham-fisted.lazy-noncaching :as lznc])
(:import [tech.v3.datatype Buffer]
[java.util List HashSet ArrayList LinkedHashMap Map]
[java.util.concurrent ConcurrentHashMap]
[java.util.concurrent.locks ReentrantLock]
[java.util.function LongConsumer Consumer LongPredicate]
[org.roaringbitmap RoaringBitmap]
[tech.v3.datatype LongReader BooleanReader ObjectReader DoubleReader
FastStruct]
[ham_fisted Sum Reducible Transformables Consumers$IncConsumer]
[clojure.lang IDeref IFn$OLO IFn$ODO])
(:refer-clojure :exclude [distinct]))
(set! *warn-on-reflection* true)
(set! *unchecked-math* :warn-on-boxed)
(defn reducer->column-reducer
"Given a hamf parallel reducer and a column name, return a dataset reducer of one column."
([reducer cname]
(reducer->column-reducer reducer nil cname))
([reducer op-space cname]
(impl/reducer->column-reducer reducer op-space cname)))
(defn first-value
[colname]
(reify ds-proto/PDatasetReducer
(ds->reducer [this ds]
(let [col (ds-base/column ds colname)]
(reify
hamf-proto/Reducer
(->init-val-fn [r] (constantly nil))
(->rfn [r] (fn [acc ^long v] (if acc acc (clojure.lang.Box. (.nth ^clojure.lang.Indexed col v)))))
hamf-proto/ParallelReducer
(->merge-fn [r] (fn [l r] l)))))
(merge [this l r] l)
hamf-proto/Finalize
(finalize [this v] (if v (.-val ^clojure.lang.Box v) v))))
(defn sum
"Create a double consumer which will sum the values."
[colname]
(reducer->column-reducer (hamf-rf/reducer-with-finalize
(Sum.)
#((deref %) :sum))
colname))
(defn mean
"Create a double consumer which will produce a mean of the column."
[colname]
(let [ds-fn (sum colname)]
(reducer->column-reducer (hamf-rf/reducer-with-finalize
(Sum.)
#(let [m (deref %)]
(/ (double (m :sum))
(double (m :n-elems)))))
colname)))
(defn row-count
"Create a simple reducer that returns the number of times reduceIndex was called."
[]
(reify ds-proto/PDatasetReducer
(ds->reducer [this ds]
(hamf-rf/consumer-reducer #(Consumers$IncConsumer.)))
(merge [this l r] (hamf-rf/reducible-merge l r))
hamf-proto/Finalize
(finalize [this v] (deref v))))
(defn maximum-rf
([]
Double/NaN)
([eax v]
(if (Double/isNaN eax)
v
(max (double v) (double eax))))
([eax]
eax))
(defn maximum
[colname]
(reducer->column-reducer maximum-rf colname))
(deftype BitmapConsumer [^RoaringBitmap bitmap]
LongConsumer
(accept [_this lval]
(.add bitmap (unchecked-int lval)))
Reducible
(reduce [this other]
(let [^BitmapConsumer other other]
(.or bitmap (.bitmap other))
this))
IDeref
(deref [_this] bitmap))
(defn distinct-int32
"Get the set of distinct items given you know the space is no larger than int32
space. The optional finalizer allows you to post-process the data."
([colname finalizer]
(let [r (hamf-rf/long-consumer-reducer #(BitmapConsumer. (bitmap/->bitmap)))
r (if finalizer
(hamf-rf/reducer-with-finalize r (fn [b] (finalizer @b)))
r)]
(reducer->column-reducer r colname)))
([colname]
(distinct-int32 colname nil)))
(deftype SetConsumer [^HashSet data]
Consumer
(accept [_this objdata]
(.add data objdata))
Reducible
(reduce [this other]
(let [^SetConsumer other other]
(.addAll ^HashSet data (.data other))
this))
IDeref
(deref [_this] data))
(defn distinct
"Create a reducer that will return a set of values."
([colname finalizer]
(let [r (hamf-rf/consumer-reducer #(SetConsumer. (HashSet.)))
r (if finalizer
(hamf-rf/reducer-with-finalize r (fn [b] (finalizer @b)))
r)]
(reducer->column-reducer r colname)))
([colname]
(distinct colname nil)))
(defn count-distinct
([colname op-space]
(case op-space
:int32 (distinct-int32 colname dtype/ecount)
:object (distinct colname dtype/ecount)))
([colname]
(count-distinct colname :object)))
(defn prob-set-cardinality
"See docs for [[tech.v3.dataset.reductions.apache-data-sketch/prob-set-cardinality]].
Options:
* `:hll-lgk` - defaults to 12, this is log-base2 of k, so k = 4096. lgK can be
from 4 to 21.
* `:hll-type` - One of #{4,6,8}, defaults to 8. The HLL_4, HLL_6 and HLL_8
represent different levels of compression of the final HLL array where the
4, 6 and 8 refer to the number of bits each bucket of the HLL array is
compressed down to. The HLL_4 is the most compressed but generally slightly
slower than the other two, especially during union operations.
* `:datatype` - One of :float64, :int64, :string"
([colname options] (sketch/prob-set-cardinality colname options))
([colname] (sketch/prob-set-cardinality colname)))
(defn prob-cdf
"See docs for [[tech.v3.dataset.reductions.apache-data-sketch/prob-cdf]]
* k - defaults to 128. This produces a normalized rank error of about 1.7%"
([colname cdf] (sketch/prob-cdf colname cdf))
([colname cdf k] (sketch/prob-cdf colname cdf k)))
(defn prob-quantile
"See docs for [[tech.v3.dataset.reductions.apache-data-sketch/prob-quantile]]
* k - defaults to 128. This produces a normalized rank error of about 1.7%"
([colname quantile] (sketch/prob-quantile colname quantile))
([colname quantile k] (sketch/prob-quantile colname quantile k)))
(defn prob-median
"See docs for [[tech.v3.dataset.reductions.apache-data-sketch/prob-median]]
* k - defaults to 128. This produces a normalized rank error of about 1.7%"
([colname] (sketch/prob-median colname))
([colname k] (sketch/prob-median colname k)))
(defn prob-interquartile-range
"See docs for [[tech.v3.dataset.reductions.apache-data-sketch/prob-interquartile-range
* k - defaults to 128. This produces a normalized rank error of about 1.7%"
([colname k] (sketch/prob-interquartile-range colname k))
([colname] (sketch/prob-interquartile-range colname)))
(defn reservoir-desc-stat
"Calculate a descriptive statistic using reservoir sampling. A list of statistic
names are found in `tech.v3.datatype.statistics/all-descriptive-stats-names`.
Options are options used in
[reservoir-sampler](https://cnuernber.github.io/dtype-next/tech.v3.datatype.sampling.html#var-double-reservoir).
Note that this method will *not* convert datetime objects to milliseconds for you as
in descriptive-stats."
([colname reservoir-size stat-name options]
(let [col-reducer (reducer->column-reducer
(dt-sample/reservoir-sampler reservoir-size
(assoc options :datatype :float64))
colname)]
(reify
ds-proto/PDatasetReducer
(ds->reducer [this ds] (ds-proto/ds->reducer col-reducer ds))
(merge [this lhs rhs]
(reduce hamf-rf/double-consumer-accumulator
lhs
@rhs))
hamf-proto/Finalize
(finalize [this ctx]
((dt-stats/descriptive-statistics @ctx [stat-name] options) stat-name))
ds-proto/PReducerCombiner
(reducer-combiner-key [reducer] [colname :reservoir-stats reservoir-size options]))))
([colname reservoir-size stat-name]
(reservoir-desc-stat colname reservoir-size stat-name nil)))
(defn reservoir-dataset
([reservoir-size] (reservoir-dataset reservoir-size nil))
([reservoir-size options]
(let [sampler-reducer (dt-sample/reservoir-sampler reservoir-size
(assoc options :datatype :object))
sampler-rfn (hamf-proto/->rfn sampler-reducer)
merge-fn (hamf-proto/->merge-fn sampler-reducer)]
(reify
ds-proto/PDatasetReducer
(ds->reducer [this ds]
(let [^Buffer rows (ds-proto/rows ds {:copying? true})]
(reify
hamf-proto/Reducer
(->init-val-fn [r] (hamf-proto/->init-val-fn sampler-reducer))
(->rfn [r] (hamf-rf/long-accumulator
acc idx (sampler-rfn acc (.readObject rows idx))))
hamf-proto/ParallelReducer
(->merge-fn [r] merge-fn))))
(merge [this lhs rhs] (merge-fn lhs rhs))
hamf-proto/Finalize
(finalize [this ctx] (ds-io/->dataset @ctx options))))))
(defn reducer
"Make a group-by-agg reducer.
* `column-name` - Single column name or multiple columns.
* `init-val-fn` - Function to produce initial accumulators
* `rfn` - Function that takes the accumulator and each column's data as
as further arguments. For a single-column pathway this looks like a normal clojure
reduction function but for two columns it gets extra arguments.
* `merge-fn` - Function that takes two accumulators and merges them. Merge is not required
for [[group-by-column-agg]] but it *is* required for [[aggregate]].
* `finalize-fn` - finalize the result after aggregation. Optional, will be replaced
with identity of not provided."
([column-name init-val-fn rfn merge-fn finalize-fn]
(let [finalize-fn (or finalize-fn #(rfn %))
init-val-fn (or init-val-fn #(rfn))
column-names (if (= :scalar (argtypes/arg-type column-name))
[column-name]
(vec column-name))
merge-fn (or merge-fn #(rfn %1 %2))]
(reify ds-proto/PDatasetReducer
(ds->reducer [this ds]
(let [rvecs (-> (ds-proto/select-columns ds column-names)
(ds-proto/rowvecs {:copying? true}))]
(reify
hamf-proto/Reducer
(->init-val-fn [r] init-val-fn)
(->rfn [r] (hamf-rf/long-accumulator
acc row-idx
(apply rfn acc (.readObject rvecs row-idx))))
hamf-proto/ParallelReducer
(->merge-fn [r] merge-fn))))
(merge [this lhs rhs] (merge-fn lhs rhs))
hamf-proto/Finalize
(finalize [this ctx] (finalize-fn ctx)))))
([column-name rfn]
(reducer column-name rfn rfn
rfn rfn)))
(defn- combine-reducers
[reducers]
(let [reducer->indexes (->> reducers
(map-indexed vector)
(hamf/group-by (comp ds-proto/reducer-combiner-key second))
(mapcat (fn [[ckey reducer-seq]]
(if (nil? ckey)
(lznc/map (fn [[ridx reducer]]
[reducer [ridx]])
reducer-seq)
(let [reducer (second (first reducer-seq))]
[[reducer (hamf/mapv first reducer-seq)]])))))]
[(mapv first reducer->indexes)
(->> (lznc/map second reducer->indexes)
(lznc/map-indexed (fn [dst-idx src-indexes]
(map #(vector % dst-idx) src-indexes)))
(apply lznc/concat)
(hamf/sort-by first)
(hamf/mapv second))]))
(defn- finalize-combined-reducers
[reducers rev-indexes cnames ^objects reduced-values]
(->> reducers
(lznc/map-indexed
(fn [^long src-idx reducer]
(hamf-proto/finalize
reducer (aget reduced-values (long (.get ^List rev-indexes src-idx))))))
(hamf/vec)
(FastStruct. cnames)))
(defn group-by-column-agg-rf
"Produce a transduce-compatible rf that will perform the group-by-column-agg pathway.
See documentation for [[group-by-column-agg]].
```clojure
tech.v3.dataset.reductions-test> (def stocks (ds/->dataset \"test/data/stocks.csv\" {:key-fn keyword}))
#'tech.v3.dataset.reductions-test/stocks
tech.v3.dataset.reductions-test> (transduce (map identity)
(ds-reduce/group-by-column-agg-rf
:symbol
{:n-elems (ds-reduce/row-count)
:price-avg (ds-reduce/mean :price)
:price-sum (ds-reduce/sum :price)
:symbol (ds-reduce/first-value :symbol)
:n-dates (ds-reduce/count-distinct :date :int32)}
{:index-filter (fn [dataset]
(let [rdr (dtype/->reader (dataset :price))]
(hamf/long-predicate
idx (> (.readDouble rdr idx) 100.0))))})
[stocks stocks stocks])
_unnamed [4 5]:
| :symbol | :n-elems | :price-avg | :price-sum | :n-dates |
|---------|---------:|-------------:|-----------:|---------:|
| AAPL | 93 | 160.19096774 | 14897.76 | 31 |
| IBM | 120 | 111.03775000 | 13324.53 | 40 |
| AMZN | 18 | 126.97833333 | 2285.61 | 6 |
| GOOG | 204 | 415.87044118 | 84837.57 | 68 |
```"
([colname agg-map]
(group-by-column-agg-rf colname agg-map nil))
([colname agg-map options]
(let [;;By default, we include the key-columns in the result.
;;Users can override this by passing in the column in agg-map
agg-map (merge
(->> (if (sequential? colname)
colname
[colname])
(reduce (fn [agg-map cname]
(assoc agg-map
cname
(first-value cname)))
{}))
agg-map)
cnames (->> (hamf/keys agg-map)
(reduce (hamf-rf/indexed-accum
acc idx v (.put ^Map acc v idx) acc)
(LinkedHashMap.)))
;;convert reducers to something with lightning fast reduction.
reducers (hamf/object-array-list (hamf/vals agg-map))
[combined-reducers rev-indexes] (combine-reducers reducers)
[colname ds-map-fn]
(if (sequential? colname)
(let [tmp-colname ::_temp_col]
[tmp-colname
#(assoc % tmp-colname
(ds-col/new-column
#:tech.v3.dataset{:name tmp-colname
:data
(-> (ds-base/select-columns % colname)
(ds-readers/value-reader {:copying? true}))
;;no scanning for missing and no datatype
;;detection
:missing (bitmap/->bitmap)
:force-datatype? true}))])
[colname identity])
finalize-fn #(finalize-combined-reducers reducers rev-indexes cnames %)]
(fn
([] (ConcurrentHashMap. (int (get options :map-initial-capacity 10000))))
([agg-map next-ds]
(let [next-ds (ds-map-fn next-ds)
group-col (dtype/->buffer (ds-base/column next-ds colname))
n-rows (ds-base/row-count next-ds)
^LongPredicate idx-filter (when-let [filter-fn (get options :index-filter)]
(let [idx-filter (filter-fn next-ds)]
(if (instance? LongPredicate idx-filter)
idx-filter
(reify LongPredicate
(test [this v] (boolean (idx-filter v)))))))
agg-reducer (->> combined-reducers
(hamf/mapv #(ds-proto/ds->reducer % next-ds))
(hamf-rf/compose-reducers {:rfn-datatype :int64}))
group-col (-> (ds-base/column next-ds colname)
(dtype/->reader))
agg-init (hamf-proto/->init-val-fn agg-reducer)
agg-rfn (hamf-proto/->rfn agg-reducer)]
(dorun (hamf/pgroups
n-rows
(fn [^long sidx ^long eidx]
(if idx-filter
(loop [sidx sidx]
(when (< sidx eidx)
(when (.test idx-filter sidx)
(.compute ^Map agg-map (.readObject group-col sidx)
(hamf-fn/bi-function
k v
(agg-rfn (or v (agg-init)) sidx))))
(recur (unchecked-inc sidx))))
(loop [sidx sidx]
(when (< sidx eidx)
(.compute ^Map agg-map (.readObject group-col sidx)
(hamf-fn/bi-function
k v
(agg-rfn (or v (agg-init)) sidx)))
(recur (unchecked-inc sidx))))))))
agg-map
#_(->> (if idx-filter
(lznc/filter idx-filter (hamf/range n-rows))
(hamf/range n-rows))
(hamf-rf/preduce (constantly nil)
(fn [acc ^long idx]
)
)
(hamf/group-by-reducer
(hamf-fn/long->obj idx (.readObject group-col idx))
agg-reducer
{:map-fn (constantly agg-map)
:ordered? false
:skip-finalize? true
:min-n 1000}))))
([agg-map]
(if (get options :skip-finalize?)
agg-map
(let [c ((hamf-proto/->init-val-fn (io-mapseq/mapseq-reducer options)))
ll (ReentrantLock.)]
;;Also possible to parse N datasets in parallel and do a concat-copying
;;operation but in my experience this steps takes up nearly no time.
(.forEach ^ConcurrentHashMap agg-map 32
(hamf-fn/bi-consumer
k v
(let [vv (finalize-fn v)]
(.lock ll)
(try
(.accept ^Consumer c vv)
(finally
(.unlock ll))))))
@c)))))))
(defn group-by-column-agg
"Group a sequence of datasets by a column and aggregate down into a new dataset.
* colname - Either a single scalar column name or a vector of column names to group by.
* agg-map - map of result column name to reducer. All values in the agg map must be
functions from dataset to hamf (non-parallel) reducers. Note that transducer-compatible
rf's - such as kixi.mean, are valid hamf reducers.
* ds-seq - Either a single dataset or sequence of datasets.
See also [[group-by-column-agg-rf]].
Options:
* `:map-initial-capacity` - initial hashmap capacity. Resizing hash-maps is expensive so we
would like to set this to something reasonable. Defaults to 10000.
* `:index-filter` - A function that given a dataset produces a function from long index
to boolean, ideally either nil or a java.util.function.LongPredicate. Only indexes for
which the index-filter returns true will be added to the aggregation. For very large
datasets, this is a bit faster than using filter before the aggregation.
Example:
```clojure
user> (require '[tech.v3.dataset :as ds])
nil
user> (require '[tech.v3.dataset.reductions :as ds-reduce])
nil
user> (def ds (ds/->dataset \"https://github.com/techascent/tech.ml.dataset/raw/master/test/data/stocks.csv\"
{:key-fn keyword}))
#'user/ds
user> (ds-reduce/group-by-column-agg
:symbol
{:price-avg (ds-reduce/mean :price)
:price-sum (ds-reduce/sum :price)}
ds)
_unnamed [5 3]:
| :symbol | :price-avg | :price-sum |
|---------|-------------:|-----------:|
| MSFT | 24.73674797 | 3042.62 |
| AAPL | 64.73048780 | 7961.85 |
| IBM | 91.26121951 | 11225.13 |
| AMZN | 47.98707317 | 5902.41 |
| GOOG | 415.87044118 | 28279.19 |
user> (def testds (ds/->dataset {:a [\"a\" \"a\" \"a\" \"b\" \"b\" \"b\" \"c\" \"d\" \"e\"]
:b [22 21 22 44 42 44 77 88 99]}))
#'user/testds
user> (ds-reduce/group-by-column-agg
[:a :b] {:c (ds-reduce/row-count)}
testds)
_unnamed [7 3]:
| :a | :b | :c |
|----|---:|---:|
| e | 99 | 1 |
| a | 21 | 1 |
| c | 77 | 1 |
| d | 88 | 1 |
| b | 44 | 2 |
| b | 42 | 1 |
| a | 22 | 2 |
```"
([colname agg-map options ds-seq]
(hamf-rf/reduce-reducer (group-by-column-agg-rf colname agg-map options)
(if (ds-impl/dataset? ds-seq)
[ds-seq]
ds-seq)))
([colname agg-map ds-seq]
(group-by-column-agg colname agg-map nil ds-seq)))
(defn aggregate
"Create a set of aggregate statistics over a sequence of datasets. Returns a
dataset with a single row and uses the same interface group-by-column-agg.
Example:
```clojure
(ds-reduce/aggregate
{:n-elems (ds-reduce/row-count)
:price-avg (ds-reduce/mean :price)
:price-sum (ds-reduce/sum :price)
:price-med (ds-reduce/prob-median :price)
:price-iqr (ds-reduce/prob-interquartile-range :price)
:n-dates (ds-reduce/count-distinct :date :int32)}
[ds-seq])
```"
([agg-map options ds-seq]
(let [cnames (vec (keys agg-map))
reducers (vals agg-map)
[combined-reducers rev-indexes] (combine-reducers reducers)
context-map (ConcurrentHashMap.)
_ (reduce (fn [acc ds]
(let [rs (->> (map #(ds-proto/ds->reducer % ds) combined-reducers)
(hamf-rf/compose-reducers {:rfn-datatype :int64}))
rs-rfn (hamf-proto/->rfn rs)
rs-init (hamf-proto/->init-val-fn rs)
init-fn (hamf-fn/function _k (rs-init))]
(dorun (hamf/pgroups
(ds-base/row-count ds)
(fn [^long sidx ^long eidx]
(let [tid (.getId (Thread/currentThread))]
(.put context-map tid
(reduce rs-rfn
(.computeIfAbsent context-map tid init-fn)
(hamf/range sidx eidx)))))
{:min-n 100}))))
nil
(if (ds-impl/dataset? ds-seq) [ds-seq] ds-seq))
^objects final-ctx
(reduce (fn [^objects lhs ^objects rhs]
(dotimes [idx (count combined-reducers)]
(aset lhs idx
(ds-proto/merge (combined-reducers idx)
(aget lhs idx)
(aget rhs idx))))
lhs)
(.values context-map))]
(ds-io/->dataset [(zipmap cnames
(->> reducers
(map-indexed
(fn [ridx reducer]
(hamf-proto/finalize
reducer
(aget final-ctx (long (rev-indexes ridx))))))))])))
([agg-map ds-seq]
(aggregate agg-map nil ds-seq)))
(comment
(require '[tech.v3.dataset :as ds])
(require '[tech.v3.datatype.datetime :as dtype-dt])
(def stocks (-> (ds/->dataset "test/data/stocks.csv" {:key-fn keyword})
(ds/update-column :date #(dtype-dt/datetime->epoch :epoch-days %))))
(aggregate
{:n-elems (row-count)
:price-avg (mean :price)
:price-sum (sum :price)
:price-med (prob-median :price)
:price-iqr (prob-interquartile-range :price)
:n-dates (count-distinct :date :int32)
:stddev (reservoir-desc-stat :price 100 :standard-deviation)
:sub-stocks (reservoir-dataset 100)}
[stocks stocks stocks])
(group-by-column-agg
:symbol
{:symbol (first-value :symbol)
:n-elems (row-count)
:price-med (prob-median :price)
:stddev (reservoir-desc-stat :price 100 :standard-deviation)
:sub-stocks (reservoir-dataset 100)}
[stocks stocks stocks])
)
@@ -0,0 +1,326 @@
(ns tech.v3.dataset.reductions.apache-data-sketch
"Reduction reducers based on the apache data sketch family of algorithms.
* [apache data sketches](https://datasketches.apache.org/)
Algorithms included here are:
### Set Cardinality
* [hyper-log-log](https://datasketches.apache.org/docs/HLL/HLL.html)
* [theta](https://datasketches.apache.org/docs/Theta/ThetaSketchFramework.html)
* [cpc](https://datasketches.apache.org/docs/CPC/CPC.html)
### Quantiles
* [doubles](https://datasketches.apache.org/api/java/snapshot/apidocs/index.html)
Example:
```clojure
user> (require '[tech.v3.dataset :as ds])
11:04:44.508 [nREPL-session-e40a19c2-8d41-40a8-8853-abe1293abe20] DEBUG tech.v3.tensor.dimensions.global-to-local - insn custom indexing enabled!
nil
user> (require '[tech.v3.dataset.reductions :as ds-reduce])
nil
user> (require '[tech.v3.dataset.reductions.apache-data-sketch :as ds-sketch])
#'user/stocks
user> (def stocks (ds/->dataset \"test/data/stocks.csv\" {:key-fn keyword}))
#'user/stocks
user> (ds-reduce/group-by-column-agg
:symbol
{:symbol (ds-reduce/first-value :symbol)
:price-quantiles (ds-sketch/prob-quantiles :price [0.25 0.5 0.75])
:price-cdfs (ds-sketch/prob-cdfs :price [25 50 75])}
[stocks stocks stocks])
:symbol-aggregation [5 3]:
| :symbol | :price-quantiles | :price-cdfs |
|---------|-----------------------|--------------------------|
| AAPL | [11.03, 36.81, 105.1] | [0.4065, 0.5528, 0.6423] |
| IBM | [77.26, 88.70, 102.4] | [0.000, 0.000, 0.1382] |
| AMZN | [30.12, 41.50, 67.00] | [0.2249, 0.6396, 0.8103] |
| MSFT | [21.75, 24.11, 27.34] | [0.5772, 1.000, 1.000] |
| GOOG | [338.5, 421.6, 510.0] | [0.000, 0.000, 0.000] |
```"
(:require [tech.v3.datatype :as dtype]
[tech.v3.datatype.casting :as casting]
[tech.v3.dataset.protocols :as ds-proto]
[tech.v3.dataset.reductions.impl :as impl]
[ham-fisted.protocols :as hamf-proto]
[ham-fisted.api :as hamf]
[ham-fisted.reduce :as hamf-rf])
(:import [org.apache.datasketches.hll HllSketch TgtHllType]
[org.apache.datasketches.quantiles DoublesSketch UpdateDoublesSketch
DoublesUnion]
[java.util.function DoubleConsumer]
[ham_fisted Reducible]
[clojure.lang IDeref]))
(set! *warn-on-reflection* true)
(set! *unchecked-math* :warn-on-boxed)
(defprotocol ^:private PSketchResult
(^{:private true
:tag 'double} sketch-estimate [cls]))
(extend-protocol PSketchResult
HllSketch
(sketch-estimate [r] (.getEstimate r))
org.apache.datasketches.hll.Union
(sketch-estimate [r] (.getEstimate r)))
(defn- hll-union?
[val]
(instance? org.apache.datasketches.hll.Union val))
(defn- ->hll-union
^org.apache.datasketches.hll.Union
[item]
(if (instance? org.apache.datasketches.hll.Union item)
item
(doto (org.apache.datasketches.hll.Union.)
(.update ^HllSketch item))))
(defn hll-reducer
"Return a hamf parallel reducer that produces a hyper-log-log-based set cardinality.
At any point you can get an estimate from the reduced value - call sketch-estimate.
Options:
* `:hll-lgk` - defaults to 12, this is log-base2 of k, so k = 4096. lgK can be
from 4 to 21.
* `:hll-type` - One of #{4,6,8}, defaults to 8. The HLL_4, HLL_6 and HLL_8
represent different levels of compression of the final HLL array where the
4, 6 and 8 refer to the number of bits each bucket of the HLL array is
compressed down to. The HLL_4 is the most compressed but generally slightly
slower than the other two, especially during union operations.
* `:datatype` - One of :float64, :int64, :string"
[{:keys [hll-lgk hll-type datatype]
:or {hll-lgk 12 hll-type 8 datatype :float64}}]
(let [cons-fn #(HllSketch. (int hll-lgk)
^TgtHllType (case (long hll-type)
4 TgtHllType/HLL_4
6 TgtHllType/HLL_6
8 TgtHllType/HLL_8))
rfn (case datatype
:float64 (hamf-rf/double-accumulator
acc val (.update ^HllSketch acc val) acc)
:int64 (hamf-rf/long-accumulator
acc val (.update ^HllSketch acc val) acc)
:string (fn [acc val] (.update ^HllSketch acc (str val)) acc))
merge-fn (fn [lhs rhs]
(let [lhs (->hll-union lhs)]
(if (hll-union? rhs)
(.update lhs (.getResult (->hll-union rhs)))
(.update lhs ^HllSketch rhs))
lhs))]
(reify
hamf-proto/Reducer
(->init-val-fn [r] cons-fn)
(->rfn [r] rfn)
hamf-proto/Finalize
(finalize [r v] (sketch-estimate v))
hamf-proto/ParallelReducer
(->merge-fn [r] merge-fn))))
(defn prob-set-cardinality
"Get the probabilistic set cardinality using hyper-log-log. See [[hll-reducer]]."
([colname options]
(impl/reducer->column-reducer (hll-reducer options) colname))
([colname]
(prob-set-cardinality colname nil)))
(defn- ->doubles-union
^DoublesUnion [v]
(if (instance? DoublesUnion v)
v
(doto (.. (DoublesUnion/builder) build)
(.union ^UpdateDoublesSketch v))))
(defn- ->doubles-sketch
^DoublesSketch [item]
(if (instance? DoublesSketch item)
item
(.getResult ^DoublesUnion item)))
(defn doubles-sketch-reducer
"Return a doubles updater. This is the reservoir and k is the reservoir size. From
a reservoir we can then get various different statistical quantities.
A k of 128 results in about 1.7% error in returned quantities."
[k finalize-fn]
(let [cons-fn #(-> (DoublesSketch/builder)
(.setK (long k))
(.build))
rfn (hamf-rf/double-accumulator
acc v (.update ^UpdateDoublesSketch acc v) acc)
merge-fn (fn [lhs rhs]
(let [lhs (->doubles-union lhs)]
(.union lhs (->doubles-sketch rhs))
lhs))]
(reify
hamf-proto/Reducer
(->init-val-fn [r] cons-fn)
(->rfn [r] rfn)
hamf-proto/ParallelReducer
(->merge-fn [r] merge-fn)
hamf-proto/Finalize
(finalize [r v] (finalize-fn v)))))
(defn ^:private doubles-ds-reducer
[colname k finalize-fn]
(let [r (doubles-sketch-reducer k finalize-fn)
col-reducer (impl/reducer->column-reducer r colname)
merge-fn (hamf-proto/->merge-fn r)]
(reify
ds-proto/PDatasetReducer
(ds->reducer [this ds] (ds-proto/ds->reducer col-reducer ds))
(merge [this lhs rhs] (merge-fn lhs rhs))
hamf-proto/Finalize
(finalize [this v] (finalize-fn v))
ds-proto/PReducerCombiner
(reducer-combiner-key [this] [colname :doubles-sketch k]))))
(def ^:private default-doubles-k 128)
(defn prob-quantile
"Probabilistic quantile estimation - see [DoublesSketch](https://datasketches.apache.org/api/java/snapshot/apidocs/index.html).
* k - defaults to 128. This produces a normalized rank error of about 1.7%"
([colname quantile k]
(doubles-ds-reducer colname k #(.getQuantile (->doubles-sketch %) (double quantile))))
([colname quantile]
(prob-quantile colname quantile default-doubles-k)))
(defn prob-quantiles
"Probabilistic quantile estimation - see [DoublesSketch](https://datasketches.apache.org/api/java/snapshot/apidocs/index.html).
* quantiles - sequence of quantiles.
* k - defaults to 128. This produces a normalized rank error of about 1.7%"
([colname quantiles k]
(let [src-data (double-array quantiles)]
(doubles-ds-reducer colname k
#(-> (.getQuantiles (->doubles-sketch %) src-data)
;;fix printing
(dtype/as-array-buffer)
(vary-meta assoc :simple-print? true)))))
([colname quantiles]
(prob-quantiles colname quantiles default-doubles-k)))
(defn prob-median
"Probabilistic median - [DoublesSketch](https://datasketches.apache.org/api/java/snapshot/apidocs/index.html)."
([colname k]
(prob-quantile colname 0.5 k))
([colname]
(prob-quantile colname 0.5)))
(defn prob-interquartile-range
"Probabilistic interquartile range - [DoublesSketch](https://datasketches.apache.org/api/java/snapshot/apidocs/index.html)."
([colname k]
(doubles-ds-reducer colname k
#(let [quantiles (.getQuantiles (->doubles-sketch %)
(double-array [0.25 0.75]))]
(- (aget quantiles 1) (aget quantiles 0)))))
([colname]
(prob-interquartile-range colname default-doubles-k)))
(defn prob-cdfs
"Probabilistic cdfs, one for each value passed in. See [DoublesSketch](https://datasketches.apache.org/api/java/snapshot/apidocs/index.html).
See prob-quantiles for k."
([colname cdfs k]
(doubles-ds-reducer
colname k
#(-> (.getCDF (->doubles-sketch %) (double-array cdfs))
(dtype/as-array-buffer)
(dtype/sub-buffer 0 (count cdfs))
(vary-meta assoc :simple-print? true))))
([colname cdfs]
(prob-cdfs colname cdfs default-doubles-k)))
(defn prob-cdf
"Probabilistic cdfs, one for each value passed in. See [DoublesSketch](https://datasketches.apache.org/api/java/snapshot/apidocs/index.html).
See prob-quantiles for k."
([colname cdf k]
(doubles-ds-reducer
colname k
#(-> (.getCDF (->doubles-sketch %) (double-array [cdf]))
(first))))
([colname cdf]
(prob-cdfs colname cdf default-doubles-k)))
(defn prob-pmfs
"Returns an approximation to the Probability Mass Function (PMF) of the input stream
given a set of splitPoints (values). See [DoublesSketch](https://datasketches.apache.org/api/java/snapshot/apidocs/index.html).
See prog-quantiles for k"
([colname pmfs k]
(doubles-ds-reducer
colname k
#(-> (.getPMF (->doubles-sketch %) (double-array pmfs))
(dtype/as-array-buffer)
(dtype/sub-buffer 0 (count pmfs))
(vary-meta assoc :simple-print? true))))
([colname pmfs]
(prob-pmfs colname pmfs default-doubles-k)))
(comment
(extend-protocol PSketchResult
org.apache.datasketches.theta.Sketch
(sketch-estimate [sketch] (.getEstimate sketch)))
(let [union (.. (org.apache.datasketches.theta.SetOperation/builder)
(buildUnion))]
(.update union ^org.apache.datasketches.theta.Sketch
(.value ^Consumers$StagedConsumer this))
(dotimes [idx (.size other-sketches)]
(.update union ^org.apache.datasketches.theta.Sketch
(.value ^Consumers$StagedConsumer
(.get other-sketches idx))))
(ThetaConsumer. (.getResult union)))
(-> (org.apache.datasketches.theta.UpdateSketchBuilder.)
(.build)
(ThetaConsumer.))
(extend-protocol PSketchResult
org.apache.datasketches.cpc.CpcSketch
(sketch-estimate [sketch] (.getEstimate sketch)))
(combineList [this other-sketches]
(let [union (org.apache.datasketches.cpc.CpcUnion.)]
(.update union ^org.apache.datasketches.cpc.CpcSketch
(.value ^Consumers$StagedConsumer this))
(dotimes [idx (.size other-sketches)]
(.update union ^org.apache.datasketches.cpc.CpcSketch
(.value ^Consumers$StagedConsumer
(.get other-sketches idx))))
(CpcConsumer. (.getResult union))))
(defmethod sketch-build-fn :cpc
[{:keys [cpc-lgk]
:or {cpc-lgk 10}}]
#(-> (org.apache.datasketches.cpc.CpcSketch. cpc-lgk)
(CpcConsumer.))))
+63
View File
@@ -0,0 +1,63 @@
(ns tech.v3.dataset.reductions.impl
"Helper namespace to help make reductions.clj more understandable."
(:require [tech.v3.datatype.casting :as casting]
[tech.v3.datatype :as dtype]
[tech.v3.dataset.protocols :as ds-proto]
[tech.v3.dataset.base :as ds-base]
[ham-fisted.api :as hamf]
[ham-fisted.reduce :as hamf-rf]
[ham-fisted.protocols :as hamf-proto])
(:import [clojure.lang IFn$OLO IFn$ODO]
[ham_fisted Transformables]))
(set! *warn-on-reflection* true)
(set! *unchecked-math* :warn-on-boxed)
(defn reducer->column-reducer
([reducer cname]
(reducer->column-reducer reducer nil cname))
([reducer op-space cname]
(let [merge-fn (hamf-proto/->merge-fn reducer)]
(reify
ds-proto/PDatasetReducer
(ds->reducer [this ds]
(let [rfn (hamf-proto/->rfn reducer)
op-space (or op-space
(cond
(instance? IFn$OLO rfn) :int64
(instance? IFn$ODO rfn) :float64
:else
:object))
col (dtype/->reader (ds-base/column ds cname) op-space)
init-fn (hamf-proto/->init-val-fn reducer)]
(case (casting/simple-operation-space op-space)
:int64
(let [rfn (Transformables/toLongReductionFn rfn)]
(reify
hamf-proto/Reducer
(->init-val-fn [r] init-fn)
(->rfn [r] (hamf-rf/long-accumulator
acc v (.invokePrim rfn acc (.readLong col v))))
hamf-proto/ParallelReducer
(->merge-fn [r] merge-fn)))
:float64
(let [rfn (Transformables/toDoubleReductionFn rfn)]
(reify
hamf-proto/Reducer
(->init-val-fn [r] init-fn)
(->rfn [r] (hamf-rf/long-accumulator
acc v (.invokePrim rfn acc (.readDouble col v))))
hamf-proto/ParallelReducer
(->merge-fn [r] merge-fn)))
(reify
hamf-proto/Reducer
(->init-val-fn [r] init-fn)
(->rfn [r] (hamf-rf/long-accumulator
acc v (rfn acc (.readObject col v))))
hamf-proto/ParallelReducer
(->merge-fn [r] merge-fn)))))
(merge [this lhs rhs] (merge-fn lhs rhs))
hamf-proto/Finalize
(finalize [this v] (hamf-proto/finalize reducer v))))))
+323
View File
@@ -0,0 +1,323 @@
(ns tech.v3.dataset.rolling
"Implement a generalized rolling window including support for time-based variable
width windows."
(:require [tech.v3.datatype :as dtype]
[tech.v3.datatype.rolling :as dt-rolling]
[tech.v3.datatype.argtypes :as argtypes]
[tech.v3.datatype.datetime.operations :as dtype-dt-ops]
[tech.v3.datatype.datetime :as dtype-dt]
[tech.v3.datatype.statistics :as stats]
[tech.v3.datatype.packing :as packing]
[tech.v3.datatype.binary-op :as binary-op]
[tech.v3.dataset.base :as ds-base])
(:import [tech.v3.datatype Buffer])
(:refer-clojure :exclude [min max nth first last]))
(defn mean
[column-name]
{:column-name column-name
:reducer stats/mean
:datatype :float64})
(defn sum
[column-name]
{:column-name column-name
:reducer stats/sum
:datatype :float64})
(defn min
[column-name]
{:column-name column-name
:reducer stats/min})
(defn max
[column-name]
{:column-name column-name
:reducer stats/max})
(defn variance
[column-name]
{:column-name column-name
:reducer stats/variance
:datatype :float64})
(defn standard-deviation
[column-name]
{:column-name column-name
:reducer stats/standard-deviation
:datatype :float64})
(defn nth
"Get the nth window value"
[column-name nth-val]
{:column-name column-name
:reducer (fn [rdr] (rdr nth-val))})
(defn first
[column-name]
{:column-name column-name
:reducer (fn [rdr] (rdr 0))})
(defn last
[column-name]
{:column-name column-name
:reducer (fn [rdr] (rdr -1))})
(defn ^:no-doc apply-window-ranges
[ds windows reducer-map edge-mode]
(->> reducer-map
(map-indexed
(fn [_idx [k red]]
(assoc red :dest-column-name k)))
(group-by :column-name)
(mapv (fn [[colname reducers]]
(let [colname (if (= :scalar (argtypes/arg-type colname))
[colname]
(vec colname))]
{:columns (mapv (partial ds-base/column ds) colname)
:reducers (vec reducers)})))
(mapv
(fn [{:keys [columns reducers]}]
;;common case is 1 column
(if (== 1 (count columns))
(let [win-data (dt-rolling/window-ranges->window-reader
(columns 0) windows edge-mode)]
(mapv (fn [reducer]
{:tech.v3.dataset/name (:dest-column-name reducer)
:tech.v3.dataset/data
(-> (dtype/emap (:reducer reducer) (:datatype reducer :object)
win-data)
(dtype/clone))})
reducers))
(let [win-data (mapv #(dt-rolling/window-ranges->window-reader
% windows edge-mode)
columns)]
(mapv (fn [reducer]
{:tech.v3.dataset/name (:dest-column-name reducer)
:tech.v3.dataset/data
(-> (apply dtype/emap (:reducer reducer) (:datatype reducer :object)
win-data)
(dtype/clone))})
reducers)))))
(apply concat)
(reduce #(ds-base/add-column %1 %2) ds)))
(defn rolling
"Perform a rolling window operation appending columns to the original dataset.
* ds - src dataset.
* window - either an integer for fixed window sizes or a map describing the window
operation containing keys:
- `:window-type` - either `:fixed` or `:variable`. For variable window operations
`:column-name` must be a monotonically increasing column.
- `:window-size` - for fixed window operation must be a positive integer. For
variable window operations must be a double value which is produced via a
comparison function.
- `:relative-window-position` - describes where the window is
positioned. Operations are `:left`, `:center`, `:right` and defaults to
`:center` for fixed and `:right` for relative window types.
- `:edge-mode` - for fixed windows describes what values to fill in at the edges
of the source column. Options are `:zero` which is 0 for numeric types and `nil`
for object types and `:clamp` which fills in the first,last values of the column
respectively. Defaults to `:clamp`.
- `:comp-fn` - if provided must return a double which is the result of comparing
the last value of the range to the first which means `clojure.core/-`
is a reasonable default.
- `:units` - for datetime types, describes the units of `:window-size` and will
dictate the numeric space if `:comp-fn` is not provided.
* reducer-map - A map of result column name to reducer map. The reducer map is a
map which must contain at least `{:column-name :reducer}` where reducer is an ifn
that is passed each window. The result column is scanned to ascertain datatype and
missing value status. Multi-column reducers are supported if column-name is a vector
of column names. In that case each column's window is passed to the reducer. The
reducer can also specify the final datatype if `:datatype` is a key in the map. Beware,
however, that this disables missing value detection for integer datatypes.
**Fixed Window Examples:**
```clojure
user> (def test-ds (ds/->dataset {:a (map #(Math/sin (double %))
(range 0 200 0.1))}))
#'user/test-ds
user> (ds/head (ds-roll/rolling test-ds 10 {:mean (ds-roll/mean :a)
:min (ds-roll/min :a)
:max (ds-roll/max :a)}))
_unnamed [5 4]:
| :a | :mean | :min | :max |
|-----------:|-----------:|-----:|-----------:|
| 0.00000000 | 0.09834413 | 0.0 | 0.38941834 |
| 0.09983342 | 0.14628668 | 0.0 | 0.47942554 |
| 0.19866933 | 0.20275093 | 0.0 | 0.56464247 |
| 0.29552021 | 0.26717270 | 0.0 | 0.64421769 |
| 0.38941834 | 0.33890831 | 0.0 | 0.71735609 |
user> (ds/head (ds-roll/rolling test-ds
{:window-type :fixed
:window-size 10
:relative-window-position :left}
{:mean (ds-roll/mean :a)
:min (ds-roll/min :a)
:max (ds-roll/max :a)}))
_unnamed [5 4]:
| :a | :mean | :min | :max |
|-----------:|-----------:|-----:|-----------:|
| 0.00000000 | 0.00000000 | 0.0 | 0.00000000 |
| 0.09983342 | 0.00998334 | 0.0 | 0.09983342 |
| 0.19866933 | 0.02985027 | 0.0 | 0.19866933 |
| 0.29552021 | 0.05940230 | 0.0 | 0.29552021 |
| 0.38941834 | 0.09834413 | 0.0 | 0.38941834 |
user> (ds/head (ds-roll/rolling test-ds
{:window-type :fixed
:window-size 10
:relative-window-position :right}
{:mean (ds-roll/mean :a)
:min (ds-roll/min :a)
:max (ds-roll/max :a)}))
_unnamed [5 4]:
| :a | :mean | :min | :max |
|-----------:|-----------:|-----------:|-----------:|
| 0.00000000 | 0.41724100 | 0.00000000 | 0.78332691 |
| 0.09983342 | 0.50138810 | 0.09983342 | 0.84147098 |
| 0.19866933 | 0.58052549 | 0.19866933 | 0.89120736 |
| 0.29552021 | 0.65386247 | 0.29552021 | 0.93203909 |
| 0.38941834 | 0.72066627 | 0.38941834 | 0.96355819 |
user> ;;Multi column reducer
user> (ds/head (ds-roll/rolling test-ds 10
{:c {:column-name [:a :a]
:reducer (fn [a b]
(Math/round
(+ (dfn/sum a) (dfn/sum b))))
:datatype :int16}}))
_unnamed [5 2]:
| :a | :c |
|-----------:|---:|
| 0.00000000 | 2 |
| 0.09983342 | 3 |
| 0.19866933 | 4 |
| 0.29552021 | 5 |
| 0.38941834 | 7 |
```
**Variable Window Examples:**
```clojure
user> (def stocks (ds/->dataset \"test/data/stocks.csv\" {:key-fn keyword}))
#'user/stocks
user> ;;variable window column must be monotonically increasing
user> (def stocks (ds/sort-by-column stocks :date))
#'user/stocks
user> (ds/head stocks)
test/data/stocks.csv [5 3]:
| :symbol | :date | :price |
|---------|------------|-------:|
| AAPL | 2000-01-01 | 25.94 |
| IBM | 2000-01-01 | 100.52 |
| MSFT | 2000-01-01 | 39.81 |
| AMZN | 2000-01-01 | 64.56 |
| AAPL | 2000-02-01 | 28.66 |
user> (ds/head (ds-roll/rolling stocks
{:window-type :variable
:column-name :date
:units :days
:window-size 3}
{:price-mean-3d (ds-roll/mean :price)
:price-max-3d (ds-roll/max :price)
:price-min-3d (ds-roll/min :price)}))
test/data/stocks.csv [5 6]:
| :symbol | :date | :price | :price-mean-3d | :price-max-3d | :price-min-3d |
|---------|------------|-------:|---------------:|--------------:|--------------:|
| AAPL | 2000-01-01 | 25.94 | 57.70750000 | 100.52 | 25.94 |
| IBM | 2000-01-01 | 100.52 | 68.29666667 | 100.52 | 39.81 |
| MSFT | 2000-01-01 | 39.81 | 52.18500000 | 64.56 | 39.81 |
| AMZN | 2000-01-01 | 64.56 | 64.56000000 | 64.56 | 64.56 |
| AAPL | 2000-02-01 | 28.66 | 56.49750000 | 92.11 | 28.66 |
user> (ds/head (ds-roll/rolling stocks
{:window-type :variable
:column-name :date
:units :months
:window-size 3}
{:price-mean-3d (ds-roll/mean :price)
:price-max-3d (ds-roll/max :price)
:price-min-3d (ds-roll/min :price)}))
test/data/stocks.csv [5 6]:
| :symbol | :date | :price | :price-mean-3d | :price-max-3d | :price-min-3d |
|---------|------------|-------:|---------------:|--------------:|--------------:|
| AAPL | 2000-01-01 | 25.94 | 58.92500000 | 106.11 | 25.94 |
| IBM | 2000-01-01 | 100.52 | 61.92363636 | 106.11 | 28.66 |
| MSFT | 2000-01-01 | 39.81 | 58.06400000 | 106.11 | 28.66 |
| AMZN | 2000-01-01 | 64.56 | 60.09222222 | 106.11 | 28.66 |
| AAPL | 2000-02-01 | 28.66 | 57.56583333 | 106.11 | 28.37 |
```"
([ds window reducer-map options]
(let [n-rows (ds-base/row-count ds)
window-data (merge (if (integer? window)
{:window-size window
:window-type :fixed}
window)
options)
windows
(case (:window-type window-data :fixed)
:fixed
(dt-rolling/fixed-rolling-window-ranges
n-rows (:window-size window-data)
(:relative-window-position window-data :center))
:variable
(let [_ (when-not (:column-name window-data)
(throw (Exception. (format "Variable rolling windows must have :column-name in the window data"))))
src-col (ds-base/column ds (:column-name window-data))
col-dt (dtype/elemwise-datatype src-col)]
(vec (dt-rolling/variable-rolling-window-ranges
src-col (:window-size window-data)
{:comp-fn
(if-let [comp-fn (:comp-fn window-data)]
comp-fn
(when (dtype-dt/datetime-datatype? (packing/unpack-datatype col-dt))
(dtype-dt-ops/between-op
(dtype/elemwise-datatype src-col)
(:units window-data :milliseconds)
true)))
:relative-window-position (get window-data :relative-window-position)}))))]
(apply-window-ranges ds windows reducer-map (:edge-mode window-data :clamp))))
([ds window reducer-map]
(rolling ds window reducer-map nil)))
(defn expanding
"Run a set of reducers across a dataset with an expanding set of windows. These
will produce a cumsum-type operation."
[ds reducer-map]
(apply-window-ranges ds (dt-rolling/expanding-window-ranges
(ds-base/row-count ds))
reducer-map
:clamp))
(comment
(require '[tech.v3.dataset :as ds])
(def test-ds (ds/->dataset {:a (map #(Math/sin (double %))
(range 0 200 0.1))}))
(rolling test-ds 10 {:mean (mean :a)
:min (min :a)
:max (max :a)})
)
+194
View File
@@ -0,0 +1,194 @@
(ns tech.v3.dataset.set
"Extensions to datasets to do per-row bag-semantics set/union and intersection."
(:require [tech.v3.dataset.impl.dataset :as ds-impl]
[tech.v3.dataset.base :as ds-base]
[tech.v3.dataset.io :as ds-io]
[tech.v3.dataset.protocols :as ds-proto]
[tech.v3.datatype :as dtype]
[ham-fisted.api :as hamf]
[ham-fisted.reduce :as hamf-rf]
[ham-fisted.function :as hamf-fn]
[ham-fisted.lazy-noncaching :as lznc])
(:import [java.util.concurrent ConcurrentHashMap]
[java.util.function BiConsumer]
[java.util Map HashSet]
[tech.v3.datatype Buffer]
[ham_fisted BiFunctions]))
(defn- concurrent-hashmap-frequencies
[data]
(hamf-rf/preduce
(constantly (hamf/java-concurrent-hashmap))
(fn [acc v]
(.compute ^Map acc v BiFunctions/incBiFn)
acc)
(fn [l r] l)
{:min-n 1000}
data))
(defn- concurrent-hashmap-intersection
[bifn ^ConcurrentHashMap l ^ConcurrentHashMap r]
(let [[^ConcurrentHashMap minmap ^ConcurrentHashMap maxmap]
(if (< (.size l) (.size r))
[l r] [r l])
retval (hamf/java-concurrent-hashmap)
bifn (hamf-fn/->bi-function bifn)]
(.forEach minmap 100 (reify java.util.function.BiConsumer
(accept [this k v]
(let [ov (.getOrDefault maxmap k ::not-found)]
(when-not (identical? ov ::not-found)
(.put retval k (.apply bifn v ov)))))))
retval))
(defn- concurrent-hashmap-union
[bifn ^ConcurrentHashMap l ^ConcurrentHashMap r]
(let [retval (hamf/java-concurrent-hashmap l)
bifn (hamf-fn/->bi-function bifn)]
(.forEach r 100 (reify BiConsumer
(accept [this k v]
(let [ov (.getOrDefault l k ::not-found)]
(if-not (identical? ov ::not-found)
(.put retval k (.apply bifn ov v))
(.put retval k v))))))
retval))
(defn- expand-setop-result
[options ds-map]
(if-let [count-attname (get options :count)]
(->> ds-map
(lznc/map #(assoc (key %) count-attname (val %)))
(ds-io/->>dataset))
(->> ds-map
(lznc/map #(hamf/repeat (val %) (key %)))
(apply lznc/concat)
(ds-io/->>dataset))))
(defn reduce-intersection
"Given a sequence of datasets, union the rows such that tuples that exist in all datasets
appear in the final dataset at their mininum repetition amount. Can return either a
dataset with duplicate tuples or a dataset with a :count column.
Options:
* `:count` - Name of count column, if nil then tuples are duplicated and count is implicit.
```clojure
user> (def ds-a (ds/->dataset [{:a 1 :b 2} {:a 1 :b 2} {:a 2 :b 3}]))
#'user/ds-a
user> (def ds-b (ds/->dataset [{:a 1 :b 2} {:a 1 :b 2} {:a 3 :b 3}]))
#'user/ds-b
user> (ds-set/reduce-intersection [ds-a ds-b])
_unnamed [2 2]:
| :a | :b |
|---:|---:|
| 1 | 2 |
| 1 | 2 |
user> (ds-set/reduce-intersection {:count :count} [ds-a ds-b])
_unnamed [1 3]:
| :a | :b | :count |
|---:|---:|-------:|
| 1 | 2 | 2 |
```"
([options datasets]
(->> datasets
(reduce (fn [acc ds]
(let [rows (ds-proto/rows ds {:copying? true})]
(if acc
(->> rows
(lznc/filter (hamf-fn/predicate
v (.containsKey ^java.util.Map acc v)))
(concurrent-hashmap-frequencies)
(concurrent-hashmap-intersection
(hamf-fn/bi-function l r (min (long l) (long r)))
acc))
(concurrent-hashmap-frequencies rows))))
nil)
(expand-setop-result options)))
([datasets] (reduce-intersection nil datasets)))
(defn reduce-union
"Given a sequence of datasets, union the rows such that all tuples appear in the final
dataset at their maximum repetition amount. Can return either a dataset with duplicate
tuples or a dataset with a :count column.
Options:
* `:count` - Name of count column, if nil then tuples are duplicated and count is implicit.
```clojure
user> (def ds-a (ds/->dataset [{:a 1 :b 2} {:a 1 :b 2} {:a 2 :b 3}]))
#'user/ds-a
user> (def ds-b (ds/->dataset [{:a 1 :b 2} {:a 1 :b 2} {:a 3 :b 3}]))
#'user/ds-b
user> (ds-set/reduce-union [ds-a ds-b])
_unnamed [4 2]:
| :a | :b |
|---:|---:|
| 2 | 3 |
| 3 | 3 |
| 1 | 2 |
| 1 | 2 |
user> (ds-set/reduce-union {:count :count} [ds-a ds-b])
_unnamed [3 3]:
| :a | :b | :count |
|---:|---:|-------:|
| 2 | 3 | 1 |
| 3 | 3 | 1 |
| 1 | 2 | 2 |
```"
([options datasets]
(->> datasets
(reduce (fn [acc ds]
(let [rows (ds-proto/rows ds {:copying? true})]
(if acc
(->> rows
(concurrent-hashmap-frequencies)
(concurrent-hashmap-union
(hamf-fn/bi-function l r (min (long l) (long r)))
acc))
(concurrent-hashmap-frequencies rows))))
nil)
(expand-setop-result options)))
([datasets] (reduce-union nil datasets)))
(defn union
"Union two datasets producing a new dataset with the union of tuples. Repeated tuples will
be repeated in final dataset at their maximum per-dataset repetition count."
([a] a)
([a b] (reduce-union [a b]))
([a b & args] (reduce-union (lznc/concat [a b] args))))
(defn intersection
"Intersect two datasets producing a new dataset with the union of tuples.
Tuples repeated across all datasets repeated in final dataset at their minimum
per-dataset repetition count."
([a] a)
([a b] (reduce-intersection [a b]))
([a b & args] (reduce-intersection (lznc/concat [a b] args))))
(defn difference
"Remove tuples from a that also appear in b."
([a] a)
([a b]
(let [b-rows (ds-proto/rows b {:copying? true})
^HashSet s (->> (hamf/upgroups
(dtype/ecount b-rows)
(fn [^long sidx ^long eidx]
(doto (HashSet.)
(.addAll (.subBuffer ^Buffer b-rows sidx eidx)))))
(reduce #(do (.addAll ^HashSet %1 ^HashSet %2) %1)))]
(ds-base/filter a (hamf-fn/predicate r (not (.contains s r)))))))
+239
View File
@@ -0,0 +1,239 @@
(ns tech.v3.dataset.string-table
(:require [tech.v3.datatype :as dtype]
[tech.v3.datatype.protocols :as dtype-proto]
[tech.v3.datatype.base :as dtype-base]
[tech.v3.dataset.dynamic-int-list :as int-list]
[tech.v3.parallel.for :as parallel-for]
[tech.v3.datatype.errors :as errors]
[ham-fisted.api :as hamf]
[ham-fisted.reduce :as hamf-rf]
[ham-fisted.lazy-noncaching :as lznc]
[clojure.tools.logging :as log])
(:import [java.util List HashMap Map ArrayList]
[java.util.function Function]
[tech.v3.datatype ObjectBuffer Buffer]
[ham_fisted IMutList ChunkedList Casts ArrayHelpers ArrayLists]))
(set! *warn-on-reflection* true)
(set! *unchecked-math* :warn-on-boxed)
(defprotocol PStrTable
(get-str-table [item]))
(declare make-string-table)
(deftype StringTable
[^List int->str
^Map str->int
^IMutList data]
dtype-proto/PClone
(clone [this]
;;We do not need to dedup any more; a java array is a more efficient
;;storage mechanism
(let [sz (.size this)
^objects rv (make-array String sz)
local-int->str int->str
local-data data]
(StringTable. (ArrayLists/toList (hamf/object-array int->str)) nil (dtype-proto/clone data))
#_(dorun (hamf/pgroups sz (fn string-table-clone [^long sidx ^long eidx]
(loop [sidx sidx]
(when (< sidx eidx)
(ArrayHelpers/aset rv sidx (.get int->str (.getLong local-data sidx)))
(recur (inc sidx)))))))
#_(ArrayLists/toList rv)))
PStrTable
(get-str-table [_this] {:int->str int->str
:str->int str->int})
ObjectBuffer
(elemwiseDatatype [_this] :string)
(lsize [_this] (.size data))
(clear [this]
(.clear int->str)
(.clear str->int)
(.clear data))
(size [_this] (.size data))
(subBuffer [this sidx eidx]
(ChunkedList/sublistCheck sidx eidx (.lsize this))
(if (and (== sidx 0)
(== eidx (.lsize this)))
this)
(let [^List int->str int->str
^IMutList data (.subList data sidx eidx)]
(reify ObjectBuffer
(elemwiseDatatype [rdr] :string)
(lsize [rdr] (- eidx sidx))
(subBuffer [rdr ssidx seidx]
(ChunkedList/sublistCheck ssidx seidx (.lsize this))
(.subBuffer this (+ sidx ssidx) (+ sidx seidx)))
(readObject [rdr idx] (.get int->str (.getLong data idx)))
(reduce [this rfn acc]
(.reduce data (hamf-rf/long-accumulator
acc v
(rfn acc (.get int->str v)))
acc)))))
(add [this value]
(errors/when-not-errorf
(or (nil? value) (instance? String value))
"Value added to string table is not a string: %s" (type value))
(let [value (or value "")
item-idx (int (.computeIfAbsent
str->int
value
(reify Function
(apply [this keyval]
(let [retval (.size int->str)]
(.add int->str keyval)
retval)))))]
(.addLong data item-idx))
true)
(add [this idx ct value]
(let [value (or value "")
item-idx (int (.computeIfAbsent
str->int
value
(reify Function
(apply [this keyval]
(let [retval (.size int->str)]
(.add int->str keyval)
retval)))))]
(.add data (.size data) ct item-idx)))
(readObject [_this idx]
(.get int->str (.getLong data idx)))
(writeObject [this idx value]
(locking this
(let [item-idx (int (.computeIfAbsent
str->int
value
(reify Function
(apply [this keyval]
(let [retval (.size int->str)]
(.add int->str keyval)
retval)))))]
(.setLong data idx item-idx))))
(reduce [this rfn acc]
(.reduce data (hamf-rf/long-accumulator
acc v
(rfn acc (.get int->str v)))
acc)))
(defn make-string-table
(^Buffer [n-elems missing-val ^List int->str ^HashMap str->int]
(let [^Buffer data (int-list/dynamic-int-list n-elems)
missing-val (str missing-val)]
(.add int->str missing-val)
(.put str->int missing-val (dec (.size int->str)))
(StringTable. int->str str->int data)))
(^Buffer [n-elems missing-val]
(make-string-table n-elems missing-val (hamf/object-array-list n-elems) (HashMap.)))
(^Buffer [n-elems]
(make-string-table n-elems "" (hamf/object-array-list n-elems) (HashMap.)))
(^Buffer []
(make-string-table 0 "" (hamf/object-array-list) (HashMap.))))
(defn compress-indexes
^IMutList [^IMutList indexes ^long max-idx]
(cond
(<= max-idx Byte/MAX_VALUE) (ArrayLists/toList (hamf/byte-array indexes))
(<= max-idx Short/MAX_VALUE) (ArrayLists/toList (hamf/short-array indexes))
(<= max-idx Integer/MAX_VALUE) (ArrayLists/toList (hamf/int-array indexes))
:else (.toLongArray indexes)))
(definterface IDof
(idOf ^long [s]))
(defn fast-str
^String [s]
(cond
(nil? s) ""
(instance? String s) s
:else (.toString ^Object s)))
(deftype FastStringContainer [^IMutList indexes ^List int->str ^HashMap str->int]
IDof
(idOf [this s]
(let [s (fast-str s)
sz (long (.size str->int))
lookup (.putIfAbsent str->int s sz)]
(if lookup
lookup
(do
(.add int->str s)
sz))))
java.util.function.Consumer
(accept [this v] (.add this v))
IMutList
(add [this v]
(.addLong indexes (.idOf this v))
true)
(add [this idx ct v]
(.add indexes (.size indexes) ct (.idOf this v)))
(get [this idx]
(let [rv (.get int->str (.getLong indexes idx))]
(when (nil? rv)
(throw (RuntimeException. (str "Index out of range: " idx))))
rv))
(size [this] (.size indexes))
(clear [this]
(.clear indexes)
(.clear int->str)
(.clear str->int))
tech.v3.datatype.protocols/PElemwiseDatatype
(elemwise-datatype [this] :string)
clojure.lang.IDeref
(deref [this]
(StringTable. (hamf/vec int->str) (.clone str->int)
(compress-indexes indexes (long (.size str->int))))))
(defn fast-string-container
([str->int int->str]
(when-not (instance? java.util.HashMap str->int)
(throw (RuntimeException. "Invalid creation of fast string container")))
(FastStringContainer. (hamf/long-array-list)
int->str str->int))
([]
(let [str->int (hamf/java-hashmap)
int->str (ArrayList.)]
(.put str->int "" 0)
(.add int->str "")
(fast-string-container str->int int->str))))
(defn string-table-from-strings
([str-data] (string-table-from-strings (fast-string-container) str-data))
([fast-string-container str-data]
(hamf-rf/reduce-reducer (hamf-rf/consumer-reducer (constantly fast-string-container)) str-data)))
(defn ->string-table
^StringTable [str-t]
(if (instance? StringTable str-t)
str-t
(string-table-from-strings str-t)))
(defn indices
"Get the string table backing index data."
[^StringTable str-t]
(-> (.data (->string-table str-t))
(dtype-base/as-buffer)))
(defn int->string
"Returns an unmodifiable list of strings from a string table. This serves as the
index->string lookup table. Returned value also implements 'nth' efficiently."
^List [^StringTable str-t]
(-> (->string-table str-t)
(.int->str)))
(defn merge-string-tables
"Return new string table with strings from lhs and rhs. Neither input is changed."
[^StringTable lhs ^StringTable rhs]
(string-table-from-strings (lznc/concat (.-int->str lhs)
(.-int->str rhs))))
+167
View File
@@ -0,0 +1,167 @@
(ns tech.v3.dataset.tensor
"Conversion mechanisms from dataset to tensor and back."
(:require [tech.v3.tensor :as dtt]
[tech.v3.tensor.dimensions :as dims]
[tech.v3.datatype.statistics :as stats]
[tech.v3.datatype.errors :as errors]
[tech.v3.parallel.for :as pfor]
[tech.v3.datatype.functional :as dfn]
[tech.v3.datatype.argops :as argops]
[tech.v3.dataset :as ds]
[tech.v3.dataset.column :as ds-col]
[tech.v3.datatype :as dtype]
[tech.v3.datatype.protocols :as dtype-proto]
[tech.v3.datatype.pprint :as dtype-pp]
[tech.v3.parallel.for :as pfor]
[clj-commons.primitive-math :as pmath])
(:import [tech.v3.datatype DoubleReader Buffer]
[java.util List]
[java.nio DoubleBuffer]))
(set! *warn-on-reflection* true)
(set! *unchecked-math* true)
(defn tensor->dataset
"Convert a tensor to a dataset. Columns of the tensor will be converted to
columns of the dataset."
[dtt & [table-name]]
(when-not (= 2 (count (dtype/shape dtt)))
(throw (ex-info "Tensors must be 2 dimensional to transform to datasets" {})))
(let [[_n-rows n-cols] (dtype/shape dtt)
table-name (or table-name :_unnamed)]
(ds/new-dataset table-name (->> (range n-cols)
(map
#(ds-col/new-column
%
(dtt/select dtt :all %)))))))
(defn dataset->tensor
"Convert a dataset to a tensor. Columns of the dataset will be converted
to columns of the tensor. Default datatype is :float64."
([dataset datatype]
(let [retval (dtt/new-tensor (dtype/shape dataset)
:datatype datatype
:init-value nil)]
(dtype/coalesce! retval
(->> (ds/columns dataset)
(map #(dtype/elemwise-cast % datatype))))
(dtt/transpose retval [1 0])))
([dataset]
(dataset->tensor dataset :float64)))
(defn- externally-safe
"Can we expose this tensor to outside systems - means
no broadcasting and no offsetting (rotation)"
([tens datatype]
(if (and (= datatype (dtype/elemwise-datatype tens))
(dtt/dims-suitable-for-desc? tens))
tens
(dtt/clone tens :datatype datatype)))
([tens]
(externally-safe tens :float64)))
(defn- jvm-matrix-multiply
"**SLOW** fallback for when openblas/mkl aren't available."
[lhs rhs]
(let [;;Slice is lazy, so we make it concrete and perform all the
;;translations necessary up front for rapid reading
lhs-rows ^List (mapv dtype/->buffer (dtt/slice lhs 1))
rhs-trans (dtt/transpose rhs [1 0])
n-rows (long (.size lhs-rows))
rhs-columns ^List (mapv dtype/->buffer (dtt/slice rhs-trans 1))
n-cols (long (.size rhs-columns))
n-elems (* n-rows n-cols)
n-inner (long (count (first lhs-rows)))]
(-> (reify DoubleReader
(lsize [rdr] n-elems)
(readDouble [rdr idx]
(let [row-idx (quot idx n-cols)
col-idx (rem idx n-cols)
^Buffer lhs (.get lhs-rows row-idx)
^Buffer rhs (.get rhs-columns col-idx)]
;;Hardcode operation to avoid dispatch costs. In this case we know
;;we are dealing with readers and we know the result is a double summation.
(loop [idx 0
sum 0.0]
(if (< idx n-inner)
(recur (unchecked-inc idx)
(pmath/+ sum (pmath/* (.readDouble lhs idx)
(.readDouble rhs idx))))
sum)))))
;;Make buffer concrete
(dtype/clone)
;;Reshape into result shape
(dtt/reshape [n-rows n-cols]))))
(defn mean-center-columns!
"in-place nan-aware mean-center the rows of the tensor. If tensor is writeable then this
writes the result directly to the tensor else it clones the tensor.
Return value is `{:means :tensor}`.
Pre-calculated per-column means may be provided."
([tens {:keys [nan-strategy means]
:or {nan-strategy :remove}}]
(errors/when-not-errorf
(== 2 (count (dtype/shape tens)))
"Tensor must have 2 dimensional shape: %s"
(dtype/shape tens))
;;Transpose tensor so our calculation can be in row-major form
(let [tens (dtt/transpose tens [1 0])
;;Note I reversed the shape result to be column-major due to the
;;transpose above.
[_n-cols n-rows] (dtype/shape tens)
n-rows (long n-rows)
;;Remember the tensor is transposed, so the rows are the columns.
^List columns (mapv dtype/->buffer (dtt/slice tens 1))
means (or means
(-> (reify DoubleReader
(lsize [rdr] (long (.size columns)))
(readDouble [rdr idx]
(let [^Buffer data (.get columns idx)]
(stats/mean data {:nan-strategy nan-strategy}))))
(dtype/clone)))
mean-buf (dtype/->buffer means)
tens-buf (dtype/->buffer tens)
^Buffer tens-buf (if-not (.allowsWrite tens-buf)
(dtype/->buffer (dtype/clone tens))
tens-buf)
n-elems (.lsize tens-buf)]
;;Now with the transposed tensor, we can iterate and get full
;;parallelism but still have each thread run through the tensor
;;column by column.
(pfor/parallel-for
idx n-elems
(let [col-idx (quot idx n-rows)
mean (.readDouble mean-buf col-idx)]
(.writeDouble tens-buf idx
(pmath/- (.readDouble tens-buf idx)
mean))))
{:means means
:tensor (dtt/transpose tens [1 0])}))
([tens]
(mean-center-columns! tens nil)))
(comment
(def test-tens (-> (dtt/transpose
(dtt/->tensor [[7 4 6 8 8 7 5 9 7 8]
[4 1 3 6 5 2 3 5 4 2]
[3 8 5 1 7 9 3 8 5 2]] :datatype :float64)
[1 0])))
;; answer
{:means [8.882E-17, 0.000, -4.441E-17],
:method :svd,
:eigenvalues [8.629338515997084, 5.751970392932843, 2.597951765363097],
:eigenvectors [[-0.1376 0.6990 -0.7017]
[-0.2505 0.6609 0.7075]
[ 0.9583 0.2731 0.08416]]}
)
+85
View File
@@ -0,0 +1,85 @@
(ns ^:no-doc tech.v3.dataset.utils
(:require [clojure.string :as str])
(:import [java.util Iterator]))
(defn nanos->millis
^long [^long nanos]
(-> (/ nanos 1000000.0)
(Math/round)))
(defmacro time-section
"Time a section, return
{:retval retval
:milliseconds ms}"
[& body]
`(let [start-time# (System/nanoTime)
retval# (do ~@body)
stop-time# (System/nanoTime)]
{:retval retval#
:milliseconds (-> (- stop-time# start-time#)
nanos->millis)}))
(defn prefix-merge
[prefix src-map merge-map]
(merge src-map
(->> merge-map
(map (fn [[item-k item-v]]
[(keyword (str prefix "-" (name item-k))) item-v]))
(into {}))))
(defn sequence->iterator
"Java ml interfaces sometimes use iterators where they really should
use sequences (iterators have state). In any case, we do what we can."
^Iterator [item-seq]
(.iterator ^Iterable (seq item-seq)))
(defn set-slf4j-log-level
"Set the slf4j log level. Safe to call if slf4j is not in the
classpath. Upon success, returns a keyword. Upon failure, returns
a map with {:exception} pointing to the failure."
[level]
(locking #'sequence->iterator
(try
((requiring-resolve
'tech.v3.dataset.utils.slf4j-log-level/set-log-level) level)
(catch Throwable e
{:exception e}))))
(defn column-safe-name
"Given a generic item (keyword, symbol) create a string that safe to be used
to name columns."
^String [item-name]
(cond
(or (keyword? item-name)
(symbol? item-name))
(if (namespace item-name)
(str (namespace item-name) "/" (name item-name))
(str (name item-name)))
(boolean? item-name)
(if item-name "true" "false")
:else
(str item-name)))
(defn rand-str
"Generates a random string of length `n` composed of alphanumeric characters."
[n]
(apply str (repeatedly n #(rand-nth "abcdefghijklmnopqrstuvwxyz0123456789"))))
(defn remove-zero-width-spaces
"Remove zero-width non-breaking spaces (ZWNBSP) from a string.
These non-breaking spaces often occur in the beginning of CSV-files."
[s]
(str/replace s #"\uFEFF" ""))
(defn rand-str-column-name-postfix
"Generates a random string postfix for a column name."
[col-idx colname]
(str (column-safe-name colname) "_" (rand-str 5)))
@@ -0,0 +1,22 @@
(ns ^:no-doc tech.v3.dataset.utils.slf4j-log-level
(:import [ch.qos.logback.classic Logger]
[ch.qos.logback.classic Level]))
(defn set-log-level
"This has to be a reflection call because no one knows what is returned
by the getLogger fn call. If you are using logback classic, then a logback
classic logger is returned. Else, you are on your own!"
[log-level]
(.setLevel
^Logger (org.slf4j.LoggerFactory/getLogger
(Logger/ROOT_LOGGER_NAME))
(case log-level
:all Level/ALL
:debug Level/DEBUG
:trace Level/TRACE
:info Level/INFO
:warn Level/WARN
:error Level/ERROR
:off Level/OFF))
log-level)
+113
View File
@@ -0,0 +1,113 @@
(ns tech.v3.dataset.zip
"Load zip data. Zip files with a single file entry can be loaded with ->dataset. When
a zip file has multiple entries you have to call zipfile->dataset-seq."
(:require [tech.v3.dataset.io :as ds-io]
[tech.v3.io :as io]
[clojure.tools.logging :as log])
(:import [java.util.zip ZipInputStream ZipOutputStream ZipEntry]
[java.util Set HashSet]
[tech.v3.dataset NoCloseInputStream NoCloseOutputStream]))
(set! *warn-on-reflection* true)
(defn- load-next-entry
[^ZipEntry entry ^ZipInputStream is options]
(let [fdata (ds-io/str->file-info (.getName entry))
ftype (:file-type fdata)]
(when-not (= (:file-type fdata) :unknown)
(try (ds-io/data->dataset (NoCloseInputStream. is)
(merge (assoc options
:dataset-name (.getName entry))
fdata))
(catch Exception e
(log/warnf "Filed to load zip entry: %s" (.getName entry))
nil)))))
(defn- load-zip-entry
[^ZipInputStream is options]
(try
(loop [entry (.getNextEntry is)]
(if entry
(let [nds (load-next-entry entry is options)]
(if-not nds
(recur (.getNextEntry is))
(cons nds (lazy-seq (load-zip-entry is options)))))
(do
(.close is)
nil)))
(catch Exception e
(.close is)
(throw e))))
(defn zipfile->dataset-seq
"Load a zipfile attempting to load each zip entry."
([input options]
(let [is (-> (apply io/input-stream input (apply concat (seq options)))
(ZipInputStream.))]
(load-zip-entry is options)))
([input]
(zipfile->dataset-seq input nil)))
(defmethod ds-io/data->dataset :zip
[data options]
(let [ds-seq (zipfile->dataset-seq data options)
rv (first ds-seq)]
(when (rest ds-seq)
;;forces the input stream to close.
(dorun ds-seq)
(log/warnf "Multiple entries found in zipfile"))
rv))
(defmethod ds-io/dataset->data! :zip
[data output options]
(let [inner-name (.substring (str output) 0 (- (count output) 4))
ftype (-> (ds-io/str->file-info inner-name)
(get :file-type))]
(with-open [os (-> (apply io/output-stream! output (apply concat (seq options)))
(ZipOutputStream.))]
(.putNextEntry os (ZipEntry. inner-name))
(ds-io/dataset->data! data os (assoc options :file-type ftype)))))
(defn- ds-name->string
^String [ds]
(let [nm (:name (meta ds))]
(if (or (symbol? nm) (keyword? nm))
(name nm)
(str nm))))
(defn- unique-name!
^String [ds ^Set used]
(let [nm (ds-name->string ds)
nm (if (.contains used nm )
(loop [idx 0]
(let [idx-nm (str nm "-" idx)]
(if (.contains used idx-nm)
(recur (inc idx))
idx-nm)))
nm)]
(.add used nm)
nm))
(defn dataset-seq->zipfile!
"Write a sequence of datasets to zipfiles. You can control the inner type with the
:file-type option which defaults to .tsv"
([output options ds-seq]
(let [fnames (HashSet.)
ftype (get options :file-type :tsv)
options (assoc options :file-type ftype)]
(with-open [os (-> (apply io/output-stream! output (apply concat (set options)))
(ZipOutputStream.))]
(doseq [ds ds-seq]
(.putNextEntry os (ZipEntry. (str (unique-name! ds fnames) "." (name ftype))))
(ds-io/dataset->data! ds (NoCloseOutputStream. os) options)))))
([output ds-seq]
(dataset-seq->zipfile! output nil ds-seq)))
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+105
View File
@@ -0,0 +1,105 @@
(ns tech.v3.libs.arrow.liblz4
(:require [tech.v3.datatype :as dt]
[tech.v3.datatype.ffi :as dt-ffi]
[tech.v3.datatype.ffi.size-t :as ffi-size-t]
[tech.v3.datatype.struct :as dt-struct]
[tech.v3.resource :as resource])
(:import [tech.v3.datatype.ffi Pointer]))
(def ^{:tag 'long} LZ4F_default 0)
(def ^{:tag 'long} LZ4F_max64KB 4)
(def ^{:tag 'long} LZ4F_max256KB 5)
(def ^{:tag 'long} LZ4F_max1MB 6)
(def ^{:tag 'long} LZ4F_max4MB 7)
(def ^{:tag 'long} LZ4F_VERSION 100)
(def ^{:tag 'long} LZ4F_HEADER_SIZE_MAX 19)
(def ^{:tag 'long} LZ4F_blockLinked 0)
(def ^{:tag 'long} LZ4F_blockIndependent 1)
(def ^{:tag 'long} LZ4F_noContentChecksum 0)
(def ^{:tag 'long} LZ4F_contentChecksumEnabled 1)
(def ^{:tag 'long} LZ4F_noBlockChecksum 0)
(def ^{:tag 'long} LZ4F_blockChecksumEnabled 1)
(def ^{:tag 'long} LZ4F_frame 0)
(def ^{:tag 'long} LZ4F_skippableFrame 1)
(dt-ffi/define-library!
lz4
'{:LZ4F_getVersion {:rettype :int32
:doc "library version"}
:LZ4F_createDecompressionContext {:rettype :size-t
:argtypes [[dctxPtr :pointer] ;;ptrptr
[version :int32]]}
:LZ4F_freeDecompressionContext {:rettype :size-t
:argtypes [[dctx :pointer]]}
:LZ4F_isError {:rettype :int32
:argtypes [[err :size-t]]}
:LZ4F_getErrorName {:rettype :string
:argtypes [[err :size-t]]}
:LZ4F_getFrameInfo {:rettype :size-t
:argtypes [[dctx :pointer] ;;decompression context
[frameInfoPtr :pointer] ;;frameinfo*
[srcBuffer :pointer] ;;void*
[srcSizePtr :pointer]]} ;;size-t*
:LZ4F_decompress {:rettype :size-t
:argtypes [[dctx :pointer]
[dstbuf :pointer]
[dstsize :pointer] ;;size-t*
[srcbuf :pointer]
[srcsize :pointer] ;;size-t*
[decomp-opt :pointer?]]} ;;dOptPtr
}
nil
nil)
;;checked against actual record layout
(def frame-info-def*
(delay (dt-struct/define-datatype! :lz4-frame-info
[{:name :block-size-id :datatype :int32}
{:name :block-mode :datatype :int32}
{:name :content-checksum :datatype :int32}
{:name :frame-type :datatype :int32}
{:name :content-size :datatype :int64}
{:name :dict-id :datatype :int32}
{:name :checksum :datatype :int32}])))
(def decompress-options-def*
(delay (dt-struct/define-datatype! :lz4-decomp-options
[{:name :stable-dst :datatype :int32}
{:name :reserved0 :datatype :int32}
{:name :reserved1 :datatype :int32}
{:name :reserved2 :datatype :int32}])))
;; Used for graal native pathways
(defn set-library-instance!
[lib-instance]
(dt-ffi/library-singleton-set-instance! lz4 lib-instance))
(defn initialize!
[]
(dt-ffi/library-singleton-set! lz4 "lz4"))
(defn create-decomp-ctx
[]
(let [retval
(resource/stack-resource-context
(let [ptrptr (dt-ffi/make-ptr :pointer 0)
ctx-err (LZ4F_createDecompressionContext ptrptr LZ4F_VERSION)]
(when (== 1 (long (LZ4F_isError ctx-err)))
(throw (Exception. (str (LZ4F_getErrorName ctx-err)))))
(Pointer. (ptrptr 0))))
addr (.address retval)]
(resource/track retval {:track-type :auto
:dispose-fn #(LZ4F_freeDecompressionContext (Pointer. addr))})))
(defn free-decomp-ctx
[ctx]
(LZ4F_freeDecompressionContext ctx))
+383
View File
@@ -0,0 +1,383 @@
(ns tech.v3.libs.clj-transit
"Transit bindings for the jvm version of tech.v3.dataset."
(:require [tech.v3.dataset :as ds]
[tech.v3.dataset.impl.dataset :as ds-impl]
[tech.v3.dataset.io :as ds-io]
[tech.v3.dataset.dynamic-int-list :as int-list]
[tech.v3.io :as io]
[tech.v3.datatype :as dtype]
[tech.v3.datatype.errors :as errors]
[tech.v3.datatype.casting :as casting]
[tech.v3.datatype.packing :as packing]
[tech.v3.datatype.datetime :as dtype-dt]
[tech.v3.datatype.array-buffer :as abuf]
[tech.v3.datatype.bitmap :as bitmap]
[clj-commons.primitive-math :as pmath]
[ham-fisted.api :as hamf]
[cognitect.transit :as t])
(:import [tech.v3.dataset.impl.dataset Dataset]
[tech.v3.dataset.impl.column Column]
[tech.v3.dataset Text]
[java.nio ByteBuffer ByteOrder]
[java.nio.charset StandardCharsets]
[java.util Base64 HashMap]
[java.time LocalDate Instant]))
(set! *warn-on-reflection* true)
(defn- le-bbuf
^ByteBuffer [n-elems]
(-> (ByteBuffer/wrap (byte-array n-elems))
(.order ByteOrder/LITTLE_ENDIAN)))
(defn- numeric-data->b64
[col]
(let [src-data (dtype/->reader col)
n-elems (count col)
^ByteBuffer bbuf
(case (casting/host-flatten (dtype/elemwise-datatype col))
:int8
(let [retval (le-bbuf n-elems)
dst-data (dtype/->buffer retval)]
(dotimes [idx (count col)]
(.writeByte dst-data idx (unchecked-byte (.readLong src-data idx))))
retval)
:int16
(let [retval (le-bbuf (* Short/BYTES n-elems))
dst-data (.asShortBuffer retval)]
(dotimes [idx (count col)]
(.put dst-data idx (unchecked-short (.readLong src-data idx))))
retval)
:int32
(let [retval (le-bbuf (* Integer/BYTES n-elems))
dst-data (.asIntBuffer retval)]
(dotimes [idx (count col)]
(.put dst-data idx (unchecked-int (.readLong src-data idx))))
retval)
:int64
(let [retval (le-bbuf (* Long/BYTES n-elems))
dst-data (.asLongBuffer retval)]
(dotimes [idx (count col)]
(.put dst-data idx (.readLong src-data idx)))
retval)
:float32
(let [retval (le-bbuf (* Float/BYTES n-elems))
dst-data (.asFloatBuffer retval)]
(dotimes [idx (count col)]
(.put dst-data idx (.readDouble src-data idx)))
retval)
:float64
(let [retval (le-bbuf (* Double/BYTES n-elems))
dst-data (.asDoubleBuffer retval)]
(dotimes [idx (count col)]
(.put dst-data idx (.readDouble src-data idx)))
retval))]
(String. (.encode (Base64/getEncoder) (.array bbuf)))))
(defn- string-col->data
[col]
(let [strdata (dtype/make-list :string)
indexes (dtype/make-list :int32)
seen (HashMap.)]
(dotimes [idx (count col)]
(let [strval (str (or (col idx) ""))
idx (.computeIfAbsent seen strval (reify java.util.function.Function
(apply [this arg]
(let [retval (count strdata)]
(.add strdata strval)
retval))))]
(.add indexes idx)))
{:strtable (hamf/object-array strdata)
:indexes (numeric-data->b64 indexes)}))
;;The assumption here is that transit has already optimized a vector or strings.
(defn- text-col->data
[col]
(let [offsets (int-list/dynamic-int-list 0)
data (StringBuilder.)
n-elems (dtype/ecount col)]
(dotimes [idx n-elems]
(let [next-data (str (or (col idx) ""))]
(.append data next-data)
(.addLong offsets (.length data))))
(let [offbuf (dtype/as-concrete-buffer offsets)]
{:offset-dtype (dtype/elemwise-datatype offbuf)
:offsets (numeric-data->b64 offbuf)
:buffer (.toString data)})))
(defn- obj-col->numeric-b64
[col dst-dtype convert-fn]
(let [data (dtype/make-list dst-dtype)]
(dotimes [idx (count col)]
(.add data (if-let [colval (col idx)]
(convert-fn colval)
0)))
(numeric-data->b64 data)))
(defn- col->data
[col]
(let [col-dt (packing/unpack-datatype (dtype/elemwise-datatype col))]
{:metadata (assoc (meta col) :datatype col-dt)
:missing (vec (-> (ds/missing col)
(bitmap/->random-access)
(hamf/object-array)
vec))
:data
(cond
(casting/numeric-type? col-dt)
(numeric-data->b64 col)
(= :boolean col-dt)
(numeric-data->b64 (dtype/make-reader :uint8 (count col)
(if (col idx) 1 0)))
(= :string col-dt)
(string-col->data col)
(= :text col-dt)
(text-col->data col)
(#{:packed-local-date :local-date} col-dt)
(obj-col->numeric-b64 col :int32 dtype-dt/local-date->days-since-epoch)
(#{:packed-instant :instant :packed-milli-instant} col-dt)
(obj-col->numeric-b64 col :int64 dtype-dt/instant->milliseconds-since-epoch)
:else ;;Punt!!
(vec col))}))
(defn ^:no-doc dataset->data
"Dataset to transit-safe data"
[ds]
{:metadata (meta ds)
:flavor :transit
:version 1
:columns (mapv col->data (ds/columns ds))})
(defn ^:no-doc b64->numeric-data
[^String data dtype]
(let [byte-data (.decode (Base64/getDecoder) (.getBytes data))
bbuf (-> (ByteBuffer/wrap byte-data)
(.order ByteOrder/LITTLE_ENDIAN))]
(case dtype
:int8 (dtype/->buffer byte-data)
:uint8 (dtype/make-reader :uint8 (alength byte-data)
(pmath/byte->ubyte (aget byte-data idx)))
:int16 (let [sbuf (.asShortBuffer bbuf)]
(dtype/make-reader :int16 (.limit sbuf) (.get sbuf idx)))
:uint16 (let [sbuf (.asShortBuffer bbuf)]
(dtype/make-reader :uint16 (.limit sbuf) (pmath/short->ushort
(.get sbuf idx))))
:int32 (let [ibuf (.asIntBuffer bbuf)]
(dtype/make-reader :int32 (.limit ibuf) (.get ibuf idx)))
:uint32 (let [ibuf (.asIntBuffer bbuf)]
(dtype/make-reader :uint32 (.limit ibuf) (pmath/int->uint (.get ibuf idx))))
:int64 (let [lbuf (.asLongBuffer bbuf)]
(dtype/make-reader :int64 (.limit lbuf) (.get lbuf idx)))
:uint64 (let [lbuf (.asLongBuffer bbuf)]
(dtype/make-reader :uint64 (.limit lbuf) (.get lbuf idx)))
:float32 (let [fbuf (.asFloatBuffer bbuf)]
(dtype/make-reader :float32 (.limit fbuf) (.get fbuf idx)))
:float64 (let [fbuf (.asDoubleBuffer bbuf)]
(dtype/make-reader :float64 (.limit fbuf) (.get fbuf idx))))))
(defn ^:no-doc str-data->coldata
[{:keys [strtable indexes]}]
(let [indexes (dtype/->reader (b64->numeric-data indexes :int32))
strdata (dtype/->reader strtable)]
(dtype/make-reader :string (count indexes) (strdata (indexes idx)))))
(defn ^:no-doc text-data->coldata
[{:keys [offsets buffer offset-dtype]}]
(let [offsets (dtype/->buffer (b64->numeric-data offsets offset-dtype))
n-elems (dtype/ecount offsets)
buffer (str buffer)]
(-> (dtype/make-reader
:text n-elems
(let [prev-idx (unchecked-dec idx)
prev-offset (long (if (> prev-idx -1)
(.readLong offsets prev-idx)
0))]
(Text. (.substring buffer prev-offset (.readLong offsets idx))))))))
(defn ^:no-doc data->dataset
[{:keys [columns] :as ds-data}]
(let [ds-meta (:metadata ds-data)]
(errors/when-not-errorf
(and ds-meta columns)
"Passed in data does not appear to have metadata or columns")
(->> (:columns ds-data)
(map
(fn [{:keys [metadata missing data]}]
(let [dtype (:datatype metadata)]
#:tech.v3.dataset{:metadata metadata
:missing (bitmap/->bitmap missing)
;;do not re-scan data.
:force-datatype? true
:data
(cond
(casting/numeric-type? dtype)
(b64->numeric-data data dtype)
(= :boolean dtype)
(let [ibuf (b64->numeric-data data :int8)]
(dtype/make-reader :boolean (count ibuf)
(if (== 0 (unchecked-long (ibuf idx)))
false true)))
(= :string dtype)
(str-data->coldata data)
(= :text dtype)
(text-data->coldata data)
(= :local-date dtype)
(-> (b64->numeric-data data :int32)
(dtype/->array-buffer)
(abuf/set-datatype :packed-local-date))
(= :instant dtype)
(-> (b64->numeric-data data :int64)
(dtype/->array-buffer)
(abuf/set-datatype :packed-milli-instant))
:else
(dtype/make-container dtype data))
:name (:name metadata)})))
(ds-impl/new-dataset {:dataset-name (:name ds-meta)} ds-meta))))
(comment
;; verify that metadata is preserved
(let [ds_a (ds/->dataset {:a [5]} {:parser-fn {:a :uint8} :dataset-name "keep-this-name"})
ds_b (-> ds_a dataset->data data->dataset)]
(and (= ds_a ds_b)
(= (meta ds_a) (meta ds_b))))
)
(def ^{:doc "Transit handler for writing datasets."}
write-handlers {Dataset (t/write-handler "tech.v3.dataset" dataset->data)})
(def ^{:doc "Transit handler for reading datasets."}
read-handlers {"tech.v3.dataset" (t/read-handler data->dataset)})
(def ^{:doc "Transit write handlers for java.time.LocalDate and java.time.Instant"}
java-time-write-handlers
{LocalDate (t/write-handler "java.time.LocalDate" dtype-dt/local-date->days-since-epoch)
Instant (t/write-handler "java.time.Instant" dtype-dt/instant->milliseconds-since-epoch)})
(def ^{:doc "Transit read handlers for java.time.LocalDate and java.time.Instant"}
java-time-read-handlers
{"java.time.LocalDate" (t/read-handler dtype-dt/days-since-epoch->local-date)
"java.time.Instant" (t/read-handler dtype-dt/milliseconds-since-epoch->instant)})
(defn dataset->transit
"Convert a dataset into a transit encoded writer. See source for details."
[ds out & [format handlers]]
(let [writer (t/writer out (or format :json) {:handlers (merge write-handlers handlers)})]
(t/write writer ds)))
(defn dataset->transit-str
"Convert a dataset to a transit-encoded json string. See [[dataset->transit]]."
[ds & [format handlers]]
(let [out (java.io.ByteArrayOutputStream.)]
(dataset->transit ds out format handlers)
(String. (.toByteArray out))))
(defn transit->dataset
"Decode a transite reader installing the transit handlers for datasets. See source
for details."
[in & [format handlers]]
(let [reader (t/reader in (or format :json) {:handlers (merge read-handlers handlers)})]
(t/read reader)))
(defn transit-str->dataset
"Convert a transit string to a dataset. This simply decodes the string, it isn't
dataset specific. See [[transit->dataset]]"
[ds-str]
(transit->dataset (java.io.ByteArrayInputStream. (.getBytes ^String ds-str))))
(defmethod ds-io/data->dataset :transit-json
[data options]
(with-open [in (io/input-stream data)]
(transit->dataset in (:transit-format options)
(:transit-read-handlers options))))
(defmethod ds-io/dataset->data! :transit-json
[ds output options]
(with-open [outs (io/output-stream! output)]
(dataset->transit ds outs
(:transit-format options)
(:transit-write-handlers options))))
(comment
(defn master-ds
[]
(ds/->dataset (array-map :a (mapv double (range 5))
:b (repeat 5 :a)
:c (repeat 5 "hey")
:d (repeat 5 {:a 1 :b 2})
:e (repeat 4 [1 2 3])
:f (repeat 5 (dtype-dt/local-date))
:g (repeat 5 (dtype-dt/instant))
:h [true false true true false]
:i (repeat 5 "text")
:j [1 nil 2 nil 3])
{:parser-fn {:i :text}}))
(-> (master-ds)
(dataset->transit-str)
(transit-str->dataset))
(def test-data (vec (repeatedly 100000 #(hash-map :time (rand)
:temp (rand)
:valid? (if (> (rand) 0.5)
true
false)))))
(def test-ds (ds/->dataset test-data))
(do
(defn t->file
[ds fname]
(with-open [outs (io/output-stream! fname)]
(dataset->transit ds outs)))
(defn file->t
[fname]
(with-open [ins (io/input-stream fname)]
(transit->dataset ins)))
)
(time (t->file test-data "mapseq.transit-json"))
;; 1027ms, 6.3MB raw, 1.9MB gzipped
(time (t->file test-ds "ds.transit-json"))
;; 16ms, 2.2MB raw, 1.6MB gzipped
(do
(def stocks (ds/->dataset "https://github.com/techascent/tech.ml.dataset/raw/master/test/data/stocks.csv" {:key-fn keyword}))
(io/make-parents "test/data/stocks.transit-json")
(t->file stocks "test/data/stocks.transit-json")
)
(require '[tech.v3.datatype.functional :as dfn])
(->> (ds/group-by-column stocks :symbol)
(map (fn [[k v]]
[k (-> (dfn/mean (v :price))
(* 100)
(Math/round)
(/ 100.0))]))
(into {}))
;;{"MSFT" 24.74, "AMZN" 47.99, "IBM" 91.26, "GOOG" 415.87, "AAPL" 64.73}
)
+170
View File
@@ -0,0 +1,170 @@
(ns tech.v3.libs.fastexcel
"Parse a dataset in xlsx format. This namespace auto-registers a handler for
the 'xlsx' file type so that when using ->dataset, `xlsx` will automatically map to
`(first (workbook->datasets))`.
Note that this namespace does **not** auto-register a handler for the `xls` file type.
`xls` is handled by the poi namespace.
If you have an xlsx file that contains multiple sheets and you want a dataset
out of each sheet you have to use `workbook->datasets` as opposed to the higher level
`->dataset` operator.
This dependency has a known conflict with cognitec's aws library - see [issue 283](https://github.com/techascent/tech.ml.dataset/issues/283).
Required Dependencies:
```clojure
[org.dhatim/fastexcel-reader \"0.12.8\" :exclusions [org.apache.poi/poi-ooxml]]
```"
(:require [tech.v3.io :as io]
[tech.v3.datatype.protocols :as dtype-proto]
[tech.v3.datatype :as dtype]
[tech.v3.datatype.errors :as errors]
[tech.v3.dataset.io.spreadsheet :as parse-spreadsheet]
[tech.v3.dataset.io :as ds-io])
(:import [org.dhatim.fastexcel.reader ReadableWorkbook
Sheet Row Cell CellType]
[tech.v3.dataset Spreadsheet$Workbook Spreadsheet$Sheet
Spreadsheet$Row Spreadsheet$Cell]))
(set! *warn-on-reflection* true)
(defn- cell-type->keyword
[^CellType cell-type]
(condp = cell-type
CellType/EMPTY :none
CellType/NUMBER :float64
CellType/BOOLEAN :boolean
CellType/STRING :string))
(defn- formula-type?
[cell-type]
(or (= cell-type CellType/FORMULA)
(= cell-type CellType/ERROR)))
(defn- try-parse-double
[^Cell cell]
(try
(Double/parseDouble (.getRawValue cell))
(catch Exception _e
(.getRawValue cell))))
(defn- wrap-cell
[^Cell cell]
(reify
dtype-proto/PElemwiseDatatype
(elemwise-datatype [this]
(let [cell-type (.getType cell)]
(if (formula-type? cell-type)
(if (number? (try-parse-double cell))
:float64
:string)
(cell-type->keyword cell-type))))
Spreadsheet$Cell
(getColumnNum [this] (.getColumnIndex cell))
(missing [this] (= :none (dtype/get-datatype this)))
(value [this]
(if (formula-type? (.getType cell))
(try-parse-double cell)
(case (dtype-proto/elemwise-datatype this)
:none nil
:string (.getRawValue cell)
:boolean (.asBoolean cell)
(double (.asNumber cell)))))
(doubleValue [this]
(if (formula-type? (.getType cell))
(Double/parseDouble (.getRawValue cell))
(double (.asNumber cell))))
(boolValue [this] (.asBoolean cell))))
(defn- wrap-row
[^Row row]
(reify
Spreadsheet$Row
(getRowNum [this] (dec (.getRowNum row)))
(iterator [this]
(let [iter (-> (.stream row)
(.filter (reify
java.util.function.Predicate
(test [this val] (not= val nil))))
(.iterator))]
(reify java.util.Iterator
;;In some cases hasNext throws.
(hasNext [this] (try (.hasNext iter)
(catch Exception _e false)))
(next [this] (wrap-cell (.next iter))))))))
(defn- wrap-sheet
[^Sheet sheet]
(reify
Spreadsheet$Sheet
(name [this] (.getName sheet))
(id [this] (.getId sheet))
(stableId [this] (.getStableId sheet))
(iterator [this]
(let [iter (.iterator (.openStream sheet))]
(reify java.util.Iterator
(hasNext [this] (.hasNext iter))
(next [this] (wrap-row (.next iter))))))))
(defn input->workbook
"Given an input data source, return an implementation of
`tech.v3.dataset/Spreadsheet$Workbook`. This interface allows you
to iterate through sheets without necessarily parsing them.
Once you have a spreadsheet, use `tech.v3.dataset.io.spreadsheet/sheet->dataset`
to get a dataset."
(^Spreadsheet$Workbook [input]
(input->workbook input {}))
(^Spreadsheet$Workbook [input _options]
(let [workbook (ReadableWorkbook. (io/input-stream input))]
(reify
Spreadsheet$Workbook
(close [this] (.close workbook))
(iterator [this]
(let [sheet-iter (.iterator (.getSheets workbook))]
(reify java.util.Iterator
(hasNext [this] (.hasNext sheet-iter))
(next [this] (wrap-sheet (.next sheet-iter))))))))))
(defn workbook->datasets
"Given a workbook, an string filename or an input stream return a sequence of
dataset named after the sheets.
Options are a subset of the arguments for tech.v3.dataset/->dataset:
* `:header-row?`
* `:num-rows`
* `:n-initial-skip-rows`
* `:parser-fn`"
([input options]
(let [workbook (input->workbook input options)]
(try
(mapv #(parse-spreadsheet/sheet->dataset % options) workbook)
(finally
(when-not (identical? input workbook)
(.close workbook))))))
([input]
(workbook->datasets input {})))
(defmethod ds-io/data->dataset :xlsx
[data options]
(let [datasets (workbook->datasets data options)
n-datasets (count datasets)]
(errors/when-not-errorf
(== 1 n-datasets)
(if (== 0 n-datasets)
"No (%d) datasets found in file"
"Multiple (%d) datasets found in file")
n-datasets)
(first datasets)))
+63
View File
@@ -0,0 +1,63 @@
(ns tech.v3.libs.guava.cache
"Use a google guava cache to memoize function results. Function must not return
nil values. Exceptions propagate to caller."
(:refer-clojure :exclude [memoize])
(:import [com.google.common.cache
Cache CacheBuilder CacheLoader
LoadingCache CacheStats]
[java.time Duration]))
(set! *warn-on-reflection* true)
(defn memoize
"Create a threadsafe, efficient memoized function using a guavacache backing store."
[f & {:keys [write-ttl-ms
access-ttl-ms
soft-values?
weak-values?
max-size
record-stats?]}]
(let [^CacheBuilder new-builder
(cond-> (CacheBuilder/newBuilder)
access-ttl-ms
(.expireAfterAccess (Duration/ofMillis access-ttl-ms))
write-ttl-ms
(.expireAfterWrite (Duration/ofMillis write-ttl-ms))
soft-values?
(.softValues)
weak-values?
(.weakValues)
max-size
(.maximumSize (long max-size))
record-stats?
(.recordStats))
^LoadingCache cache
(.build new-builder
(proxy [CacheLoader] []
(load [args]
(if-let [retval (apply f args)]
retval
(throw (ex-info
(format "Nil values not allowed in cache: %s" args)
{}))))))]
(-> (fn [& args]
(.get cache args))
(with-meta {:cache cache}))))
(defn memo-stats
"Get cache stats from a memoized object."
[filecache]
(let [^Cache memo-cache (-> (meta filecache)
:cache)
^CacheStats cache-map (.stats memo-cache)]
{:hit-count (.hitCount cache-map)
:hit-rate (.hitRate cache-map)
:miss-count (.missCount cache-map)
:miss-rate (.missRate cache-map)
:load-success-count (.loadSuccessCount cache-map)
:load-exception-count (.loadExceptionCount cache-map)
:average-load-penalty-nanos (.averageLoadPenalty cache-map)
:total-load-time-nanos (.totalLoadTime cache-map)
:eviction-count (.evictionCount cache-map)}))
File diff suppressed because it is too large Load Diff
+156
View File
@@ -0,0 +1,156 @@
(ns tech.v3.libs.parquet-hardwood
(:require [ham-fisted.lazy-caching :as lznc]
[ham-fisted.api :as hamf]
[ham-fisted.iterator :as hamf-iter]
[tech.v3.datatype :as dt]
[tech.v3.datatype.array-buffer :as array-buffer]
[tech.v3.datatype.errors :as errors]
[tech.v3.dataset :as ds]
[tech.v3.dataset.io.column-parsers :as col-parsers]
[tech.v3.dataset.io.context :as parse-context]
[tech.v3.dataset.base :as ds-base]
[clojure.java.io :as io]
[clojure.tools.logging :as log])
(:import [dev.morling.hardwood.reader ParquetFileReader ColumnReader]
[dev.morling.hardwood.schema FileSchema ColumnSchema]
[dev.morling.hardwood.metadata RowGroup ColumnChunk LogicalType
LogicalType$DateType LogicalType$StringType LogicalType$UuidType
LogicalType$EnumType LogicalType$JsonType LogicalType$TimestampType
LogicalType$BsonType LogicalType$IntervalType LogicalType$IntType
LogicalType$DecimalType LogicalType$TimeType LogicalType$MapType
LogicalType$ListType]
[clojure.lang MapEntry]))
(set! *warn-on-reflection* true)
(def ^:private column-finalizers
{LogicalType$StringType (fn [_ str-data] (dt/emap (fn [ss] (String. ^bytes ss)) :string str-data))
LogicalType$DateType (fn [_ int-data] (-> (dt/as-array-buffer int-data)
(array-buffer/set-datatype :packed-local-date)))})
(defn- read-column
[idx ^ColumnSchema col-schema ^ColumnChunk col-def ^ParquetFileReader rdr options]
(let [crdr (.getColumnReader rdr col-schema col-def)
parser (col-parsers/promotional-object-parser (.name col-schema) nil)
lt (.logicalType col-schema)
finalizer (get column-finalizers (type lt) (fn [_ v] v))
key-fn (:key-fn options identity)]
(loop [idx 0]
(if (.hasNext crdr)
(do
(when-let [nv (.readNext crdr)] (col-parsers/add-value! parser idx nv))
(recur (inc idx)))
{:column-name (key-fn (.name col-schema))
:column-parser (reify tech.v3.dataset.io.column_parsers.PParser
(finalize [this n-rows]
(-> (col-parsers/finalize! parser n-rows)
(update :tech.v3.dataset/data #(finalizer lt %)))))
:n-rows idx}))))
(defn- ->path
[data]
(if (instance? java.nio.file.Path data)
data
(java.nio.file.Paths/get (str data) (into-array String []))))
(defn parquet->ds-seq
[path options]
(let [rdr (ParquetFileReader/open (->path path))
schema (.getFileSchema rdr)
schema-columns (->> (range (.getColumnCount schema))
(into [] (map #(.getColumn schema (long %)))))
col-name->idx (into {} (map-indexed (fn [idx ^ColumnSchema col]
[(.name col) idx]))
schema-columns)
col-idx->name (into {} (map-indexed (fn [idx ^ColumnSchema col]
[idx (.name col)]))
schema-columns)
cref->idx (fn [centry] (if (number? centry) (long centry) (col-name->idx centry)))
allowset (into #{} (map cref->idx) (get options :column-allowlist))
blockset (into #{} (map cref->idx) (get options :column-blocklist))
is-allowed? (fn [^long idx]
(if-not (empty? allowset)
(boolean (allowset idx))
(if-not (empty? blockset)
(boolean (not (blockset idx)))
true)))
src-indexes (vec (->> (hamf/range (count schema-columns))
(lznc/filter is-allowed?)))
rv (->> (.getFileMetaData rdr)
(.rowGroups)
(lznc/map (fn [^RowGroup grp]
(let [parsers (->> src-indexes
(hamf/pmap-io 12 (fn [^long idx]
(read-column idx (nth schema-columns idx)
(nth (.columns grp) idx)
rdr
options)))
(vec))
row-count (reduce max 0 (lznc/map :n-rows parsers))]
(parse-context/parsers->dataset options parsers row-count)))))
closer* (delay (.close rdr))
seq* (delay (seq rv))]
(reify
java.lang.AutoCloseable
(close [this] @closer*)
clojure.lang.Seqable
(seq [this] @seq*)
Iterable
(iterator [this]
(if (realized? seq*)
(.iterator ^Iterable @seq*)
(let [src-iter (.iterator ^Iterable rv)]
(reify
java.util.Iterator
(hasNext [this] (let [rv (.hasNext src-iter)]
(when-not rv @closer*)
rv))
(next [this] (.next src-iter))))))
clojure.lang.IReduceInit
(reduce [this rfn acc]
(let [acc
(if (realized? seq*)
(reduce rfn acc @seq*)
(reduce rfn acc rv))]
@closer*
acc)))))
(defn parquet->ds
"Load a parquet file. Input must be a file on disk.
Options are a subset of the options used for loading datasets -
specifically `:column-allowlist` and `:column-blocklist` can be
useful here. The parquet metadata ends up as metadata on the
datasets."
([input options]
(let [data-file (io/file input)
_ (errors/when-not-errorf
(.exists data-file)
"Only on-disk files work with parquet. %s does not resolve to a file"
input)
dataset-seq (vec (parquet->ds-seq (.getCanonicalPath data-file) options))]
(when-not (or (:disable-parquet-warn-on-multiple-datasets options)
(== 1 (count dataset-seq)))
(log/warnf "Concatenating multiple datasets (%d) into one.
To disable this warning use `:disable-parquet-warn-on-multiple-datasets`"
(count dataset-seq)))
(if (== 1 (count dataset-seq))
(first dataset-seq)
(apply ds-base/concat-copying dataset-seq))))
([input]
(parquet->ds input nil)))
;; No writing interface defined yet
;; (defn ds-seq->parquet
;; ([path options ds-seq])
;; ([path ds-seq]))
;; (defn ds->parquet
;; "Write a dataset to a parquet file. Many parquet options are possible;
;; these can also be passed in via ds/->write!
;; Options are the same as ds-seq->parquet."
;; ([ds path options]
;; (ds-seq->parquet path options [ds]))
;; ([ds path]
;; (ds->parquet ds path nil)))
+171
View File
@@ -0,0 +1,171 @@
(ns tech.v3.libs.poi
"Parse a dataset in xls or xlsx format. This namespace auto-registers a handler for
the `xls` file type so that when using ->dataset, `xls` will automatically map to
`(first (workbook->datasets))`.
Note that this namespace does **not** auto-register a handler for the `xlsx` file
type. `xlsx` is handled by the fastexcel namespace.
If you have an `xlsx` or `xls` file that contains multiple sheets and you want a
dataset out of each sheet you have to use `workbook->datasets` as opposed to the
higher level `->dataset` operator.
For serializing datasets to xlsx or xls formats please see
[kixi.large](https://github.com/MastodonC/kixi.large).
Note that poi has many versions and many version conflicts and for instance the
docjure library relies on a much older version of poi."
(:require [tech.v3.io :as io]
[tech.v3.datatype :as dtype]
[tech.v3.datatype.protocols :as dtype-proto]
[tech.v3.datatype.errors :as errors]
[tech.v3.datatype.datetime :as dtype-dt]
[tech.v3.dataset.io.spreadsheet :as parse-spreadsheet]
[tech.v3.dataset.io :as ds-io])
(:import [org.apache.poi.ss.usermodel Workbook Sheet Cell
CellType Row]
[tech.v3.dataset Spreadsheet$Workbook Spreadsheet$Sheet
Spreadsheet$Row Spreadsheet$Cell]
[org.apache.poi.xssf.usermodel XSSFWorkbook]
[org.apache.poi.hssf.usermodel HSSFWorkbook]
[org.apache.poi.ss.usermodel DateUtil]))
(set! *warn-on-reflection* true)
(defn- cell-type->keyword
[^CellType cell-type]
(condp = cell-type
CellType/BLANK :none
CellType/_NONE :none
CellType/NUMERIC :float64
CellType/BOOLEAN :boolean
CellType/STRING :string))
(defn- wrap-cell
[^Cell cell]
(reify
dtype-proto/PElemwiseDatatype
(elemwise-datatype [this]
(let [cell-type (.getCellType cell)]
(if (or (= cell-type CellType/FORMULA)
(= cell-type CellType/ERROR))
(cell-type->keyword (.getCachedFormulaResultType cell))
(let [dtype-kwd (cell-type->keyword (.getCellType cell))]
(if (and (identical? :float64 dtype-kwd)
(DateUtil/isCellDateFormatted cell))
:local-date
dtype-kwd)))))
Spreadsheet$Cell
(getColumnNum [this] (.. cell getAddress getColumn))
(missing [this] (identical? :none (dtype/get-datatype this)))
(value [this]
(case (dtype-proto/elemwise-datatype this)
:none nil
:string (.getStringCellValue cell)
:boolean (.getBooleanCellValue cell)
:local-date (-> (.getLocalDateTimeCellValue cell)
(dtype-dt/local-date-time->local-date))
(.getNumericCellValue cell)))
(doubleValue [this] (.getNumericCellValue cell))
(boolValue [this] (.getBooleanCellValue cell))))
(defn- wrap-row
^Spreadsheet$Row [^Row row]
(reify
Spreadsheet$Row
(getRowNum [this] (.getRowNum row))
(iterator [this]
(let [iter (.iterator row)]
(reify java.util.Iterator
(hasNext [this] (.hasNext iter))
(next [this] (wrap-cell (.next iter))))))))
(defn- wrap-sheet
^Spreadsheet$Sheet [^Sheet sheet]
(reify
Spreadsheet$Sheet
(name [this] (.getSheetName sheet))
(iterator [this]
(let [iter (.iterator sheet)]
(reify java.util.Iterator
(hasNext [this] (.hasNext iter))
(next [this] (wrap-row (.next iter))))))))
(defn- fname->file-type
[^String fname]
(if (.endsWith fname "xls")
:xls
:xlsx))
(defn input->workbook
"Given an input data source, return an implementation of
`tech.v3.dataset/Spreadsheet$Workbook`. This interface allows you
to iterate through sheets without necessarily parsing them.
Once you have a spreadsheet, use `tech.v3.dataset.io.spreadsheet/sheet->dataset`
to get a dataset."
(^Spreadsheet$Workbook [input options]
(if (instance? Spreadsheet$Workbook input)
input
(let [file-type (or (:file-type options)
(when (string? input)
(fname->file-type input))
:xlsx)
^Workbook workbook
(case file-type
:xlsx (XSSFWorkbook. (io/input-stream input))
:xls (HSSFWorkbook. (io/input-stream input)))]
(reify
Spreadsheet$Workbook
(iterator [this]
(let [iter (.iterator workbook)]
(reify java.util.Iterator
(hasNext [this] (.hasNext iter))
(next [this]
(wrap-sheet (.next iter))))))
(close [this] (.close workbook))))))
(^Spreadsheet$Workbook [input]
(input->workbook input {})))
(defn workbook->datasets
"Given a workbook, a string filename or an input stream return a sequence of
datasets named after the sheets.
Options are a subset of the arguments for tech.v3.dataset/->dataset:
* `:file-type` - either `:xls` or `:xlsx` - inferred from the filename when input is
a string. If input is a stream defaults to `:xlsx`.
* `:header-row?`
* `:num-rows`
* `:n-initial-skip-rows`
* `:parser-fn`"
([input options]
(let [workbook (input->workbook input options)]
(try
(mapv #(parse-spreadsheet/sheet->dataset % options) workbook)
(finally
(when-not (identical? input workbook)
(.close workbook))))))
([input]
(workbook->datasets input {})))
(defmethod ds-io/data->dataset :xls
[data options]
(let [datasets (workbook->datasets data options)
n-datasets (count datasets)]
(errors/when-not-errorf
(== 1 n-datasets)
(if (== 0 n-datasets)
"No (%d) datasets found in file"
"Multiple (%d) datasets found in file")
n-datasets)
(first datasets)))
+220
View File
@@ -0,0 +1,220 @@
(ns tech.v3.libs.spark
(:require [tech.v3.dataset.impl.dataset :as ds-impl]
[tech.v3.dataset.readers :as ds-readers]
[tech.v3.dataset.utils :as ds-utils]
[tech.v3.dataset.column :as ds-col]
[tech.v3.dataset.io :as ds-io]
[tech.v3.datatype.base :as dtype-base]
[tech.v3.datatype.emap :as emap]
[tech.v3.datatype.datetime :as dtype-dt]
[tech.v3.datatype.packing :as packing]
[tech.v3.datatype.casting :as casting]
[tech.v3.datatype.errors :as errors]
[clojure.set :as set])
(:import [org.apache.spark.sql Row RowFactory Dataset SparkSession]
[org.apache.spark.sql.types StructType StructField
DataTypes DataType]
[tech.v3.datatype ObjectReader]
[java.time LocalDate Instant]
[java.util List]))
(set! *warn-on-reflection* true)
(def ^:private datatype->sql-map
{:boolean DataTypes/BooleanType
:int8 DataTypes/ByteType
:int16 DataTypes/ShortType
:int32 DataTypes/IntegerType
:int64 DataTypes/LongType
:float32 DataTypes/FloatType
:float64 DataTypes/DoubleType
:sql-date DataTypes/DateType
:string DataTypes/StringType
:text DataTypes/StringType
:sql-timestamp DataTypes/TimestampType})
(defn datatype->sql-type
^DataType [datatype]
(let [datatype (packing/unpack-datatype datatype)
datatype (if-not (= datatype :epoch-days)
(let [temp-dt (casting/un-alias-datatype datatype)]
(if (casting/numeric-type? temp-dt)
(casting/safe-flatten temp-dt)
temp-dt))
datatype)
sql-type (get datatype->sql-map datatype)
_ (errors/when-not-errorf
sql-type
"Failed to convert datatype %s"
datatype)]
sql-type))
(def ^:private sql->datatype-map
(set/map-invert datatype->sql-map))
(defn sql-type->datatype
[sql-type]
(if-let [retval (get sql->datatype-map sql-type)]
retval
(errors/throwf "Failed to find datatype for sql type %s"
sql-type)))
(defn sql-date->local-date
^java.time.LocalDate [^java.sql.Date date]
(dtype-dt/milliseconds-since-epoch->local-date
(.getTime date)))
(defn local-date->sql-date
^java.sql.Date [^LocalDate ld]
(java.sql.Date/valueOf ld))
(casting/add-object-datatype! :sql-date java.sql.Date)
(defn sql-timestamp->instant
^Instant [^java.sql.Timestamp date]
(dtype-dt/milliseconds-since-epoch->instant
(.getTime date)))
(defn instant->sql-timestamp
^java.sql.Timestamp [^Instant inst]
(java.sql.Timestamp.
(dtype-dt/instant->milliseconds-since-epoch inst)))
(casting/add-object-datatype! :sql-timestamp java.sql.Timestamp)
(defn- prepare-ds-for-spark
[ds]
(reduce
(fn [ds col]
(let [cname (ds-utils/column-safe-name (ds-col/column-name col))
col-dtype (packing/unpack-datatype
(dtype-base/elemwise-datatype col))
missing (ds-col/missing col)
epoch-types #{:epoch-milliseconds :epoch-seconds
:epoch-microseconds}
coldata (cond
(epoch-types col-dtype)
(->> (dtype-dt/epoch->datetime :instant col)
(emap/emap instant->sql-timestamp :sql-timestamp))
(= col-dtype :local-date)
(emap/emap local-date->sql-date :sql-date
(packing/unpack col))
(= col-dtype :epoch-days)
(->> (dtype-dt/epoch->datetime :local-date col)
(emap/emap instant->sql-timestamp :sql-date))
(= col-dtype :text)
(emap/emap str :string col)
(and (casting/numeric-type? col-dtype)
(not= col-dtype (casting/safe-flatten col-dtype)))
(dtype-base/elemwise-cast col (casting/safe-flatten col-dtype))
:else
col)]
(assoc ds cname (ds-col/new-column cname coldata (meta col) missing))))
ds
(vals ds)))
(defn ds-schema->spark-schema
^StructType [ds-schema]
(let [retval (StructType.)]
(reduce
(fn [retval col]
(let [{:keys [datatype name]} col
name (ds-utils/column-safe-name name)
nullable? (boolean (not (.isEmpty (ds-col/missing col))))]
(.add ^StructType retval name (datatype->sql-type datatype) nullable?)))
retval
(:columns ds-schema))))
(defn ds-schema
[ds]
(assoc (meta ds)
:columns (mapv meta (vals ds))))
(defn- dataset->row-list
^List [ds]
(let [val-rdr (ds-readers/value-reader ds)
n-elems (.lsize val-rdr)]
(reify ObjectReader
(lsize [rdr] n-elems)
(readObject [rdr idx]
(RowFactory/create
(object-array (.readObject val-rdr idx)))))))
(defn ds->spark-dataset
(^Dataset [ds ^SparkSession spark-session _options]
;;Prepare the dataset datatypes
(let [ds (prepare-ds-for-spark ds)]
(.createDataFrame spark-session (dataset->row-list ds)
(-> (ds-schema ds)
(ds-schema->spark-schema)))))
(^Dataset [ds session]
(ds->spark-dataset ds session nil)))
(defn collect-spark-dataset->ds
[^Dataset dataset]
;;Make sure we are dealing with rows, not drama
;;This system will not be able to deal with missing
;;values without scanning each column.
(let [dataset (.toDF dataset)
schema (.schema dataset)
;;data is an array of rows
^objects data (.collect dataset)
n-rows (alength data)]
(->> (.fields schema)
(map-indexed
(fn [^long col-idx ^StructField field]
(let [colname (.name field)
dtype (sql-type->datatype (.dataType field))]
(ds-col/new-column colname
(reify ObjectReader
(elemwiseDatatype [rdr] dtype)
(lsize [rdr] n-rows)
(readObject [rdr row-idx]
(let [^Row row (aget data row-idx)]
(.get row col-idx))))
nil
[]))))
(ds-impl/new-dataset))))
(comment
;; databricks-connect specific classes
;; should work similar for spark-connect
;;Tested with hese deps
;;org.scala-lang/scala-reflect {:mvn/version "2.12.18"}
;;com.databricks/databricks-connect {:mvn/version "16.1.0"}
(import
'[com.databricks.connect DatabricksSession]
'[com.databricks.sdk.core DatabricksConfig])
(def config (.. (DatabricksConfig.) (setProfile "adb-xxxxx")))
(def spark (.. (DatabricksSession/builder) (sdkConfig config) getOrCreate))
(->
(.sql spark "show catalogs;")
collect-spark-dataset->ds)
)
+326
View File
@@ -0,0 +1,326 @@
(ns tech.v3.libs.tribuo
"Bindings to make working with tribuo more straight forward when using datasets.
```clojure
;; Classification
tech.v3.dataset.tribuo-test> (def ds (classification-example-ds 10000))
#'tech.v3.dataset.tribuo-test/ds
tech.v3.dataset.tribuo-test> (def model (tribuo/train-classification (org.tribuo.classification.xgboost.XGBoostClassificationTrainer. 6) ds :label))
#'tech.v3.dataset.tribuo-test/model
tech.v3.dataset.tribuo-test> (ds/head (tribuo/predict-classification model (ds/remove-columns ds [:label])))
_unnamed [5 3]:
| :prediction | red | green |
|-------------|-----------:|-----------:|
| red | 0.92524981 | 0.07475022 |
| green | 0.07464883 | 0.92535114 |
| green | 0.07464883 | 0.92535114 |
| red | 0.92525917 | 0.07474083 |
| green | 0.07464883 | 0.92535114 |
;; Regression
tech.v3.dataset.tribuo-test> (def ds (ds/->dataset \"test/data/winequality-red.csv\" {:separator \\;}))
#'tech.v3.dataset.tribuo-test/ds
tech.v3.dataset.tribuo-test> (def model (tribuo/train-regression (org.tribuo.regression.xgboost.XGBoostRegressionTrainer. 50) ds \"quality\"))
#'tech.v3.dataset.tribuo-test/model
tech.v3.dataset.tribuo-test> (ds/head (tribuo/predict-regression model (ds/remove-columns ds [\"quality\"])))
_unnamed [5 1]:
| :prediction |
|------------:|
| 5.01974726 |
| 5.02164841 |
| 5.22696543 |
| 5.79519272 |
| 5.01974726 |
```"
(:require [tech.v3.dataset :as ds]
[tech.v3.dataset.modelling :as modelling]
[tech.v3.datatype :as dtype]
[tech.v3.datatype.casting :as casting]
[clojure.set :as set]
[charred.api :as charred])
(:import [tech.v3.datatype Buffer ArrayHelpers]
[java.util HashMap Map List]
[java.nio.file Files]
[java.nio.file.attribute FileAttribute]
[org.tribuo.classification Label LabelFactory]
[org.tribuo DataSource Output OutputFactory Trainer Model MutableDataset
Prediction]
[org.tribuo.impl ArrayExample]
[org.tribuo.provenance SimpleDataSourceProvenance]
[org.tribuo.regression RegressionFactory Regressor]
[org.tribuo.regression.evaluation RegressionEvaluator RegressionEvaluation]
[com.oracle.labs.mlrg.olcut.config ConfigurationManager]
[com.oracle.labs.mlrg.olcut.config.json JsonConfigFactory]))
(set! *warn-on-reflection* true)
(ConfigurationManager/addFileFormatFactory (JsonConfigFactory.)) ; allows json config
(def ^:private str-ary-cls (Class/forName "[Ljava.lang.String;"))
(def ^:private dbl-ary-cls (Class/forName "[D"))
(defn- ->string
^String [obj]
(if (nil? obj)
""
(if (or (keyword? obj)
(symbol? obj))
(name obj)
(str obj))))
(defn- ->string-array
^"[Ljava.lang.String;" [data]
(if (instance? str-ary-cls data)
data
(into-array String (map ->string data))))
(defn- ->double-array
"Slightly faster than (double-array) when the source data implements tech.v3.datatype.Buffer."
^doubles [data]
(if (instance? dbl-ary-cls data)
data
(let [data (dtype/->buffer data)
ndata (.size data)
ddata (double-array ndata)]
;;Clojure's aset function returns a value and thus boxes every item in the array.
(dotimes [idx ndata] (ArrayHelpers/aset ddata idx (.readDouble data idx)))
ddata)))
(defn- ds->regression-outputs
[ds]
(let [fact (RegressionFactory.)]
(with-meta
(if (integer? ds)
(dtype/make-reader :object ds (.getUnknownOutput fact))
(let [rvecs (ds/rowvecs ds)
names (->string-array (ds/column-names ds))]
(dtype/make-reader :object (dtype/ecount rvecs)
(Regressor. names (->double-array (rvecs idx))))))
{:output-factory fact})))
(defn- ds->single-label-outputs
[ds]
(let [label-fact (LabelFactory.)]
(if (integer? ds)
(with-meta
(dtype/make-reader :object ds (.getUnknownOutput label-fact))
{:output-factory label-fact})
(do
(when-not (== 1 (ds/column-count ds))
(throw (Exception. "Single label output expected, got multi-label dataset")))
(let [col (first (vals ds))
colmeta (meta col)
data (dtype/->buffer col)
n-rows (.lsize data)
data (if-let [cat-map (get-in colmeta [:categorical-map :lookup-table])]
(let [rev-map (set/map-invert cat-map)]
(dtype/make-reader :object n-rows (get rev-map (long (data idx)))))
data)]
(with-meta
(dtype/make-reader :object n-rows (Label. (->string (data idx))))
{:output-factory label-fact}))))))
(defn- ensure-numeric-columns
[ds]
(when-not (every? casting/numeric-type? (map (comp :datatype meta) (vals ds)))
(throw (Exception.
(str "All columns must be numeric at this point. Non numeric columns:\n"
(->> (map meta (vals ds))
(filter #(not (casting/numeric-type? (get % :datatype))))
(mapv :name))))))
ds)
(defn- ds->examples
^Buffer [ds ds->outputs]
(let [inf-columns (modelling/inference-target-column-names ds)
inf-ds (when inf-columns (ds/select-columns ds inf-columns))
feat-ds (-> (ds/remove-columns ds inf-columns)
(ensure-numeric-columns))
feat-data (ds/rowvecs feat-ds)
n-rows (ds/row-count ds)
;;column names have to be strings.
cnames (->string-array (ds/column-names feat-ds))
n-rows (ds/row-count ds)
outputs (if inf-ds
(ds->outputs inf-ds)
(ds->outputs n-rows))]
(with-meta
(dtype/make-reader :object n-rows
;;idx is a compile-time variable below
(ArrayExample. ^Output (outputs idx)
cnames (->double-array (feat-data idx))))
(meta outputs))))
(defn- safe-str
[n]
(cond (string? n)
n
(or (keyword? n) (symbol? n))
(if-let [nn (namespace n)]
(str nn "/" (name n))
(str (name n)))))
(defn- ds->datasource
^DataSource [ds ds->outputs]
(let [examples (ds->examples ds ds->outputs)
{:keys [output-factory provenance]} (meta examples)
provenance (or provenance
(SimpleDataSourceProvenance. (safe-str (:name (meta ds))) output-factory))]
(when-not output-factory
(throw (RuntimeException. "Output factory not present in example metadata")))
(reify DataSource
(getProvenance [this] provenance)
(iterator [this] (.iterator examples))
(getOutputFactory [this] output-factory))))
(defn- label-score
^double [^Label label]
(.getScore label))
(defn- label-label
^String [^Label label]
(.getLabel label))
(defn- prediction-label
^String [^Prediction pred]
(-> (.getOutput pred)
(label-label)))
(defn- has-probabilities?
[^Prediction pred]
(.hasProbabilities pred))
(defn- prediction-scores
^Map [^Prediction pred]
(.getOutputScores pred))
(defn- regressor-value
^double [^Regressor reg]
(aget (.getValues reg) 0))
(defn- prediction-regval
[^Prediction pred]
(regressor-value (.getOutput pred)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Begin public API
;; Classification
(defn make-classification-datasource
"Make a single label classification datasource."
(^DataSource [ds]
(make-classification-datasource ds nil))
(^DataSource [ds inf-col-name]
(if inf-col-name
(-> (modelling/set-inference-target ds inf-col-name)
(ds->datasource ds->single-label-outputs))
(ds->datasource ds ds->single-label-outputs))))
(defn train-classification
"Train a single label classification model. Returns the model."
^Model [^Trainer trainer ds & [inf-col-name]]
(.train trainer (-> (make-classification-datasource ds inf-col-name)
(MutableDataset.))))
(defn classification-predictions->dataset
"Given the list of predictions from a classification model return a dataset
that will include probability distributions when possible. The actual prediction
will be in the `:prediction` column."
[^List predictions]
(let [labels (dtype/emap prediction-label :string predictions)
pred (.get predictions 0)]
(-> (ds/->dataset
(merge {:prediction labels}
(when (has-probabilities? pred)
(let [scores (prediction-scores pred)]
(->> (.entrySet scores)
(map (fn [entry]
(let [ekey (key entry)]
[ekey (-> (dtype/emap #(-> (prediction-scores %)
(.get ekey)
(label-score))
:float64
predictions)
(dtype/clone))])))
(into {}))))))
(modelling/set-inference-target :prediction))))
(defn predict-classification
"Use this model to predict every row of this dataset returning a new dataset containing
at least a `:prediction` column. If this classifier is capable of predicting probability
distributions those will be returned as per-label as separate columns."
[^Model model ds]
(when-not (== 0 (ds/row-count ds))
(->> (.predict model (make-classification-datasource ds))
(classification-predictions->dataset))))
;; Regression
(defn make-regression-datasource
"Make a regression datasource from a dataset."
(^DataSource [ds]
(make-regression-datasource ds nil))
(^DataSource [ds inf-col-name]
(if inf-col-name
(-> (modelling/set-inference-target ds inf-col-name)
(ds->datasource ds->regression-outputs))
(ds->datasource ds ds->regression-outputs))))
(defn train-regression
"Train a regression model on a dataset returning the model."
^Model [^Trainer trainer ds & [inf-col-name]]
(.train trainer (-> (make-regression-datasource ds inf-col-name)
(MutableDataset.))))
(defn predict-regression
"Use a regression model to predict each column of the dataset returning a dataset with
at least one column named `:prediction`."
[^Model model ds]
(when-not (== 0 (ds/row-count ds))
(ds/->dataset {:prediction (->> (.predict model (make-regression-datasource ds))
(dtype/emap prediction-regval :float64)
(dtype/clone))})))
(defn evaluate-regression
"Evaluate a regression model against this model. Returns map of
`{:rmse :mae :r2}`"
[^Model model ds inf-col-name]
(let [inf-col-name (->string inf-col-name)
^RegressionEvaluation eval (.evaluate (RegressionEvaluator.) model
(MutableDataset. (make-regression-datasource ds inf-col-name)))
dim (Regressor. inf-col-name Double/NaN)]
{:rmse (.rmse eval dim)
:mae (.mae eval dim)
:r2 (.r2 eval dim)}))
(defn trainer
"Creates a tribuo trainer from a list of config components
follwing OLCUT convention. One of the components should be a trainer,
which is the looked-up by `trainer-name` and returned."
[config-components trainer-name]
(let [config {:config {:components config-components}}
tmp (.toString (Files/createTempFile "config" ".json" (make-array FileAttribute 0)))
_ (charred/write-json tmp config)
config-manager (ConfigurationManager. tmp)]
(.lookup config-manager trainer-name)))