Repository: JuliaML/MLUtils.jl
Pull request #5 (2021-12-27 16:34:54)

port splitobs, shuffleobs, obsview
Changes:
- In `shuffleobs` now `rng` is in the first position
- In `splitobs` now `at` is a mandatory keyword arg
- In `splitobs` now `at` can take integers or tuple of integers
- Now `getobs(a::SubArray)` materializes the array
- modernized the implementations (e.g. removed `@generated`)


Patch:
diff --git a/Project.toml b/Project.toml
index 7a6c826..76c0881 100644
--- a/Project.toml
+++ b/Project.toml
@@ -4,15 +4,15 @@ authors = ["Carlo Lucibello <carlo.lucibello@gmail.com> and contributors"]
 version = "0.1.0"
 
 [deps]
+Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
+Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
 
 [compat]
 julia = "1.6"
 
 [extras]
-Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
 SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
-Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
 Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
 
 [targets]
-test = ["Random", "Statistics", "SparseArrays", "Test"]
+test = ["SparseArrays", "Test"]
diff --git a/src/MLUtils.jl b/src/MLUtils.jl
index d9d424b..1527922 100644
--- a/src/MLUtils.jl
+++ b/src/MLUtils.jl
@@ -1,4 +1,5 @@
 module MLUtils
+using Random
 
 include("utils.jl")
 
@@ -11,5 +12,14 @@ export randobs
 include("datasubset.jl")
 export datasubset, DataSubset
 
+include("shuffleobs.jl")
+export shuffleobs
+
+include("splitobs.jl")
+export splitobs
+
+include("dataview.jl")
+export DataView, ObsView,
+       obsview
 
 end
diff --git a/src/datasubset.jl b/src/datasubset.jl
index e101d21..4326d92 100644
--- a/src/datasubset.jl
+++ b/src/datasubset.jl
@@ -234,6 +234,8 @@ function datasubset(A::AbstractArray{T,N}, idx) where {T,N}
     return view(A, I..., idx)
 end
 
+getobs(a::SubArray) = getobs(a.parent, last(a.indices))
+
 getobs!(buffer, subset::SubArray) = getobs(subset)
 getobs!(buffer::AbstractArray, subset::SubArray) = buffer .= subset
 
diff --git a/src/dataview.jl b/src/dataview.jl
new file mode 100644
index 0000000..1fef418
--- /dev/null
+++ b/src/dataview.jl
@@ -0,0 +1,152 @@
+"""
+    abstract DataView{TElem, TData} <: AbstractVector{TElem}
+
+Baseclass for all vector-like views of some data structure.
+This allow for example to see some design matrix as a vector of
+individual observation-vectors instead of one matrix.
+see `ObsView` and `BatchView` for examples.
+"""
+abstract type DataView{TElem, TData} <: AbstractVector{TElem} end
+
+Base.IndexStyle(::Type{T}) where {T<:DataView} = IndexLinear()
+Base.size(A::DataView) = (length(A),)
+Base.lastindex(A::DataView) = length(A)
+getobs(A::DataView) = map(getobs, A)
+getobs(A::DataView, i) = getobs(A[i])
+
+
+# --------------------------------------------------------------------
+
+"""
+    ObsView(data)
+
+Description
+============
+
+Create a view of the given `data` in the form of a vector of
+individual observations. Any data access is delayed until
+`getindex` is called, and even `getindex` returns the result of
+[`datasubset`](@ref) which in general avoids data movement until
+[`getobs`](@ref) is called.
+
+If used as an iterator, the view will iterate over the dataset
+once, effectively denoting an epoch. Each iteration will return a
+lazy subset to the current observation.
+
+Arguments
+==========
+
+- **`data`** : The object describing the dataset. Can be of any
+    type as long as it implements [`getobs`](@ref) and
+    [`numobs`](@ref) (see Details for more information).
+
+
+Methods
+========
+
+Aside from the `AbstractArray` interface following additional
+methods are provided:
+
+- **`getobs(data::ObsView, indices::AbstractVector)`** :
+    Returns a `Vector` of indivdual observations specified by
+    `indices`.
+
+- **`numobs(data::ObsView)`** :
+    Returns the number of observations in `data` that the
+    iterator will go over.
+
+Details
+========
+
+For `ObsView` to work on some data structure, the type of the
+given variable `data` must implement the data container
+interface. See `?DataSubset` for more info.
+
+Examples
+=========
+
+```julia
+X, Y = MLDataUtils.load_iris()
+
+A = obsview(X)
+@assert typeof(A) <: ObsView <: AbstractVector
+@assert eltype(A) <: SubArray{Float64,1}
+@assert length(A) == 150 # Iris has 150 observations
+@assert size(A[1]) == (4,) # Iris has 4 features
+
+for x in obsview(X)
+    @assert typeof(x) <: SubArray{Float64,1}
+end
+
+# iterate over each individual labeled observation
+for (x,y) in obsview((X,Y))
+    @assert typeof(x) <: SubArray{Float64,1}
+    @assert typeof(y) <: String
+end
+
+# same but in random order
+for (x,y) in obsview(shuffleobs((X,Y)))
+    @assert typeof(x) <: SubArray{Float64,1}
+    @assert typeof(y) <: String
+end
+```
+
+see also
+=========
+
+[`eachobs`](@ref), [`BatchView`](@ref), [`shuffleobs`](@ref),
+[`getobs`](@ref), [`numobs`](@ref), [`DataSubset`](@ref)
+"""
+struct ObsView{TElem,TData} <: DataView{TElem,TData}
+    data::TData
+end
+
+const obsview = ObsView
+
+function ObsView(data::T) where {T}
+    E = typeof(datasubset(data, 1))
+    ObsView{E,T}(data)
+end
+
+function ObsView(A::T) where T<:DataView
+    @warn string("Trying to nest a ", T.name, " into an ObsView, which is not supported. Returning ObsView(parent(_)) instead")
+    ObsView(parent(A))
+end
+
+ObsView(A::ObsView) = A
+
+numobs(A::ObsView) = numobs(A.data)
+Base.parent(A::ObsView) = A.data
+Base.length(A::ObsView) = numobs(A)
+Base.getindex(A::ObsView, i::Int) = datasubset(A.data, i)
+Base.getindex(A::ObsView, i::AbstractVector) = ObsView(datasubset(A.data, i))
+
+function Base.showarg(io::IO, A::ObsView, toplevel)
+    print(io, "obsview(")
+    Base.showarg(io, parent(A), false)
+    print(io, ')')
+    toplevel && print(io, " with eltype ", nameof(eltype(A))) # simplify
+end
+
+# ------------------------------------------------------------
+
+
+# --------------------------------------------------------------------
+
+# # if subsetting a DataView, then DataView the subset instead.
+# for fun in (:DataSubset, :datasubset)
+#     @eval @generated function ($fun)(A::T, i, obsdim::$O) where T<:AbstractObsView
+#         quote
+#             @assert obsdim == A.obsdim
+#             ($(T.name.name))(($($fun))(parent(A), i, obsdim), obsdim)
+#         end
+#     end
+#     @eval function ($fun)(A::BatchView, i, obsdim::$O)
+#         @assert obsdim == A.obsdim
+#         length(i) < A.size && throw(ArgumentError("The chosen batch-size ($(A.size)) is greater than the number of observations ($(length(i)))"))
+#         BatchView(($fun)(parent(A), i, obsdim), A.size, -1, obsdim)
+#     end
+# end
+
+DataSubset(A::ObsView, i) = ObsView(DataSubset(parent(A), i))
+datasubset(A::ObsView, i) = ObsView(datasubset(parent(A), i))
diff --git a/src/observation.jl b/src/observation.jl
index 3af6360..0317a52 100644
--- a/src/observation.jl
+++ b/src/observation.jl
@@ -90,6 +90,11 @@ end
 
 numobs(data::Union{Tuple, NamedTuple}) = _check_numobs(data)
 
+
+function getobs(tup::Union{Tuple, NamedTuple})
+    return map(x -> getobs(x), tup)
+end
+
 function getobs(tup::Union{Tuple, NamedTuple}, indices)
     _check_numobs(tup)
     return map(x -> getobs(x, indices), tup)
@@ -113,6 +118,10 @@ function getobs(data::Dict, i)
     Dict(k => getobs(v, i) for (k, v) in pairs(data))
 end
 
+function getobs(data::Dict)
+    Dict(k => getobs(v) for (k, v) in pairs(data))
+end
+
 function getobs!(buffers, data::Dict, i)
     for (k, v) in pairs(data)
         getobs!(buffers[k], v, i)
diff --git a/src/shuffleobs.jl b/src/shuffleobs.jl
new file mode 100644
index 0000000..86c6ba0
--- /dev/null
+++ b/src/shuffleobs.jl
@@ -0,0 +1,35 @@
+"""
+    shuffleobs([rng], data)
+
+Return a "subset" of `data` that spans all observations, but
+has the order of the observations shuffled.
+
+The values of `data` itself are not copied. Instead only the
+indices are shuffled. This function calls [`datasubset`](@ref) to
+accomplish that, which means that the return value is likely of a
+different type than `data`.
+
+```julia
+# For Arrays the subset will be of type SubArray
+@assert typeof(shuffleobs(rand(4,10))) <: SubArray
+
+# Iterate through all observations in random order
+for x in eachobs(shuffleobs(X))
+    ...
+end
+```
+
+The optional parameter `rng` allows one to specify the
+random number generator used for shuffling. This is useful when
+reproducible results are desired. By default, uses the global RNG.
+See `Random` in Julia's standard library for more info.
+
+For this function to work, the type of `data` must implement
+[`numobs`](@ref) and [`getobs`](@ref). See [`DataSubset`](@ref)
+for more information.
+"""
+shuffleobs(data) = shuffleobs(Random.GLOBAL_RNG, data)
+
+function shuffleobs(rng::AbstractRNG, data)
+    datasubset(data, randperm(rng, numobs(data)))
+end
diff --git a/src/splitobs.jl b/src/splitobs.jl
new file mode 100644
index 0000000..fc5aabb
--- /dev/null
+++ b/src/splitobs.jl
@@ -0,0 +1,99 @@
+"""
+    splitobs(n::Int; at) -> Tuple
+
+Pre-compute the indices for two disjoint subsets and return them
+as a tuple of two ranges. The first range will span the first
+`at` fraction of possible indices, while the second range will
+cover the rest. These indices are applicable to any data
+container of size `n`.
+
+```julia
+julia> splitobs(100, at=0.7)
+(1:70, 71:100)
+```
+
+A tuple `at` can be passed for multiple splits:
+
+```julia    
+julia> splitobs(100, at=(0.1, 0.4))
+(1:10, 11:50, 51:100)
+```
+"""
+splitobs(n::Int; at) = _splitobs(n, at)
+
+_splitobs(n::Int, at::Integer) = _splitobs(n::Int, at / n) 
+_splitobs(n::Int, at::NTuple{N, <:Integer}) where {N} = _splitobs(n::Int, at ./ n) 
+
+_splitobs(n::Int, at::Tuple{}) = (1:n,)
+
+function _splitobs(n::Int, at::AbstractFloat)
+    0 <= at <= 1 || throw(ArgumentError("the parameter \"at\" must be in interval (0, 1)"))
+    n1 = clamp(round(Int, at*n), 0, n)
+    (1:n1, n1+1:n)
+end
+
+function _splitobs(n::Int, at::NTuple{N,<:AbstractFloat}) where N
+    at1 = first(at)
+    a, b = _splitobs(n::Int, at1)
+    n1 = a.stop 
+    n2 = b.stop
+    at2 = Base.tail(at) .* n ./ (n2 - n1)
+    rest = map(x -> n1 .+ x, _splitobs(n2-n1, at2))
+    return (a, rest...)
+end
+
+"""
+    splitobs(data; at) -> Tuple
+
+Split the `data` into multiple subsets proportional to the
+value(s) of `at`.
+
+Note that this function will perform the splits statically and
+thus not perform any randomization. The function creates a
+`NTuple` of data subsets in which the first N-1 elements/subsets
+contain the fraction of observations of `data` that is specified
+by `at`.
+
+For example, if `at` is a `Float64` then the return-value will be
+a tuple with two elements (i.e. subsets), in which the first
+element contains the fracion of observations specified by `at`
+and the second element contains the rest. In the following code
+the first subset `train` will contain the first 70% of the
+observations and the second subset `test` the rest.
+
+```julia
+train, test = splitobs(X, at=0.7)
+```
+
+If `at` is a tuple of `Float64` then additional subsets will be
+created. In this example `train` will have the first 50% of the
+observations, `val` will have next 30%, and `test` the last 20%
+
+```julia
+train, val, test = splitobs(X, at=(0.5, 0.3))
+```
+
+It is also possible to call `splitobs` with multiple data
+arguments as tuple, which all must have the same number of total
+observations. This is useful for labeled data.
+
+```julia
+train, test = splitobs((X, y), at=0.7)
+(x_train,y_train), (x_test,y_test) = splitobs((X, y), at=0.7)
+```
+
+If the observations should be randomly assigned to a subset,
+then you can combine the function with `shuffleobs`
+
+```julia
+# This time observations are randomly assigned.
+train, test = splitobs(shuffleobs((X, y)), at=0.7)
+```
+
+See [`stratifiedobs`](@ref) for a related function that preserves
+the target distribution.
+"""
+function splitobs(data; at)
+    n = numobs(data)
+    map(idx -> datasubset(data, idx), splitobs(n; at))
+end


Test patch:
diff --git a/test/datasubset.jl b/test/datasubset.jl
index 0afba83..da7adee 100644
--- a/test/datasubset.jl
+++ b/test/datasubset.jl
@@ -1,37 +1,3 @@
-
-# --------------------------------------------------------------------
-# create some test data
-
-
-Random.seed!(1335)
-X = rand(4, 150)
-y = repeat(["setosa","versicolor","virginica"], inner = 50)
-Y = permutedims(hcat(y,y), [2,1])
-Yt = hcat(y,y)
-yt = Y[1:1,:]
-Xv = view(X,:,:)
-yv = view(y,:)
-XX = rand(20,30,150)
-XXX = rand(3,20,30,150)
-vars = (X, Xv, yv, XX, XXX, y)
-tuples = ((X,y), (X,Y), (XX,X,y), (XXX,XX,X,y))
-Xs = sprand(10, 150, 0.5)
-ys = sprand(150, 0.5)
-# to compare if obs match
-X1 = hcat((1:150 for i = 1:10)...)'
-Y1 = collect(1:150)
-
-struct EmptyType end
-
-struct CustomType end
-MLUtils.numobs(::CustomType) = 100
-MLUtils.getobs(::CustomType, i::Int) = i
-MLUtils.getobs(::CustomType, i::AbstractVector) = collect(i)
-# MLUtils.gettargets(::CustomType, i::Int) = "obs $i"
-# MLUtils.gettargets(::CustomType, i::AbstractVector) = "batch $i"
-
-
-
 @testset "DataSubset constructor" begin
     @test_throws DimensionMismatch DataSubset((rand(2,10), rand(9)))
     @test_throws DimensionMismatch DataSubset((rand(2,10), rand(9)), 1:2)
@@ -40,16 +6,16 @@ MLUtils.getobs(::CustomType, i::AbstractVector) = collect(i)
     @testset "bounds check" begin
         for var in (vars..., tuples..., CustomType())
             @test_throws BoundsError DataSubset(var, -1:100)
-            @test_throws BoundsError DataSubset(var, 1:151)
+            @test_throws BoundsError DataSubset(var, 1:16)
             @test_throws BoundsError DataSubset(var, [1, 10, 0, 3])
             @test_throws BoundsError DataSubset(var, [1, 10, -10, 3])
-            @test_throws BoundsError DataSubset(var, [1, 10, 180, 3])
+            @test_throws BoundsError DataSubset(var, [1, 10, 18, 3])
         end
     end
 
     @testset "Tuples" begin
         @test typeof(@inferred(DataSubset((X,X)))) <: DataSubset
-        @test typeof(@inferred(DataSubset((X,X), 1:150))) <: DataSubset
+        @test typeof(@inferred(DataSubset((X,X), 1:15))) <: DataSubset
         d = DataSubset((X, y), 5:10)
         @test @inferred(getobs(d , 2)) == getobs((X, y), 6)
         @test numobs(d) == 6
@@ -59,16 +25,16 @@ MLUtils.getobs(::CustomType, i::AbstractVector) = collect(i)
         for var in (Xs, ys, vars...)
             subset = @inferred(DataSubset(var))
             @test subset.data === var
-            @test subset.indices === 1:150
+            @test subset.indices === 1:15
             @test typeof(subset) <: DataSubset
             @test @inferred(numobs(subset)) === numobs(var)
             @test @inferred(getobs(subset)) == getobs(var)
             @test @inferred(DataSubset(subset)) === subset
-            @test @inferred(DataSubset(subset, 1:150)) === subset
-            @test subset[end] == DataSubset(var, 150)
-            @test @inferred(subset[150]) == DataSubset(var, 150)
-            @test @inferred(subset[20:25]) == DataSubset(var, 20:25)
-            for idx in (1:100, [1,10,150,3], [2])
+            @test @inferred(DataSubset(subset, 1:15)) === subset
+            @test subset[end] == DataSubset(var, 15)
+            @test @inferred(subset[15]) == DataSubset(var, 15)
+            @test @inferred(subset[2:5]) == DataSubset(var, 2:5)
+            for idx in (1:10, [1,10,15,3], [2])
                 @test DataSubset(var)[idx] == DataSubset(var, idx)
                 @test DataSubset(var)[idx] == DataSubset(var, collect(idx))
                 subset = @inferred(DataSubset(var, idx))
@@ -95,140 +61,140 @@ MLUtils.getobs(::CustomType, i::AbstractVector) = collect(i)
         @test_throws MethodError DataSubset(EmptyType(), 1:10)
         @test_throws BoundsError getobs(DataSubset(CustomType(), 11:20), 11)
         @test typeof(@inferred(DataSubset(CustomType()))) <: DataSubset
-        @test numobs(DataSubset(CustomType())) === 100
-        @test numobs(DataSubset(CustomType(), 11:20)) === 10
-        @test getobs(DataSubset(CustomType())) == collect(1:100)
-        @test getobs(DataSubset(CustomType(),11:20),10) == 20
-        @test getobs(DataSubset(CustomType(),11:20),[3,5]) == [13,15]
+        @test numobs(DataSubset(CustomType())) === 15
+        @test numobs(DataSubset(CustomType(), 1:10)) === 10
+        @test getobs(DataSubset(CustomType())) == collect(1:15)
+        @test getobs(DataSubset(CustomType(),1:10),10) == 10
+        @test getobs(DataSubset(CustomType(),1:10),[3,5]) == [3,5]
     end
 end
 
 @testset "DataSubset getindex and getobs" begin
     @testset "Matrix and SubArray{T,2}" begin
         for var in (X, Xv)
-            subset = @inferred(DataSubset(var, 101:150))
+            subset = @inferred(DataSubset(var, 5:12))
             @test typeof(@inferred(getobs(subset))) <: Array{Float64,2}
-            @test @inferred(numobs(subset)) == length(subset) == 50
-            @test @inferred(subset[10:20]) == DataSubset(X, 110:120)
-            @test @inferred(subset[11:21]) != DataSubset(X, 110:120)
-            @test @inferred(getobs(subset, 10:20)) == X[:, 110:120]
-            @test @inferred(getobs(subset, [11,10,14])) == X[:, [111,110,114]]
-            @test typeof(subset[10:20]) <: DataSubset
-            @test @inferred(subset[collect(10:20)]) == DataSubset(X, collect(110:120))
-            @test typeof(subset[collect(10:20)]) <: DataSubset
-            @test @inferred(getobs(subset)) == getobs(subset[1:end]) == X[:, 101:150]
+            @test @inferred(numobs(subset)) == length(subset) == 8
+            @test @inferred(subset[2:5]) == DataSubset(X, 6:9)
+            @test @inferred(subset[3:6]) != DataSubset(X, 6:9)
+            @test @inferred(getobs(subset, 2:5)) == X[:, 6:9]
+            @test @inferred(getobs(subset, [3,1,4])) == X[:, [7,5,8]]
+            @test typeof(subset[2:5]) <: DataSubset
+            @test @inferred(subset[collect(2:5)]) == DataSubset(X, collect(6:9))
+            @test typeof(subset[collect(2:5)]) <: DataSubset
+            @test @inferred(getobs(subset)) == getobs(subset[1:end]) == X[:, 5:12]
         end
     end
 
     @testset "Vector and SubArray{T,1}" begin
         for var in (y, yv)
-            subset = @inferred(DataSubset(var, 101:150))
+            subset = @inferred(DataSubset(var, 6:10))
             @test typeof(getobs(subset)) <: Array{String,1}
-            @test @inferred(numobs(subset)) == length(subset) == 50
-            @test @inferred(subset[10:20]) == DataSubset(y, 110:120)
-            @test @inferred(getobs(subset, 10:20)) == y[110:120]
-            @test @inferred(getobs(subset, [11,10,14])) == y[[111,110,114]]
-            @test typeof(subset[10:20]) <: DataSubset
-            @test @inferred(subset[collect(10:20)]) == DataSubset(y, collect(110:120))
-            @test typeof(subset[collect(10:20)]) <: DataSubset
-            @test @inferred(getobs(subset)) == getobs(subset[1:end]) == y[101:150]
+            @test @inferred(numobs(subset)) == length(subset) == 5
+            @test @inferred(subset[2:3]) == DataSubset(y, 7:8)
+            @test @inferred(getobs(subset, 2:3)) == y[7:8]
+            @test @inferred(getobs(subset, [2,1,4])) == y[[7,6,9]]
+            @test typeof(subset[2:3]) <: DataSubset
+            @test @inferred(subset[collect(2:3)]) == DataSubset(y, collect(7:8))
+            @test typeof(subset[collect(2:3)]) <: DataSubset
+            @test @inferred(getobs(subset)) == getobs(subset[1:end]) == y[6:10]
         end
     end
 end
 
-@testset "datasubset" begin
-    @testset "Array and SubArray" begin
-        @test @inferred(datasubset(X)) == Xv
-        @test @inferred(datasubset(X)) == Xv
-        @test @inferred(datasubset(X)) == Xv
-        @test typeof(datasubset(X)) <: SubArray
-        @test @inferred(datasubset(Xv)) === Xv
-        @test @inferred(datasubset(XX)) == XX
-        @test @inferred(datasubset(XXX)) == XXX
-        @test typeof(datasubset(XXX)) <: SubArray
-        @test @inferred(datasubset(y)) == y
-        @test typeof(datasubset(y)) <: SubArray
-        @test @inferred(datasubset(yv)) === yv
-        for i in (2, 1:150, 2:10, [2,5,7], [2,1])
-            @test @inferred(datasubset(X,i))   === view(X,:,i)
-            @test @inferred(datasubset(Xv,i))  === view(X,:,i)
-            @test @inferred(datasubset(Xv,i))  === view(Xv,:,i)
-            @test @inferred(datasubset(XX,i))  === view(XX,:,:,i)
-            @test @inferred(datasubset(XXX,i)) === view(XXX,:,:,:,i)
-            @test @inferred(datasubset(y,i))   === view(y,i)
-            @test @inferred(datasubset(yv,i))  === view(y,i)
-            @test @inferred(datasubset(yv,i))  === view(yv,i)
-            @test @inferred(datasubset(Y,i))   === view(Y,:,i)
-        end
-    end
-
-    @testset "Tuple of Array and Subarray" begin
-        @test @inferred(datasubset((X,y)))   == (X,y)
-        @test @inferred(datasubset((X,yv)))  == (X,yv)
-        @test @inferred(datasubset((Xv,y)))  == (Xv,y)
-        @test @inferred(datasubset((Xv,yv))) == (Xv,yv)
-        @test @inferred(datasubset((X,Y)))   == (X,Y)
-        @test @inferred(datasubset((XX,X,y))) == (XX,X,y)
-        for i in (1:150, 2:10, [2,5,7], [2,1])
-            @test @inferred(datasubset((X,y),i))   === (view(X,:,i), view(y,i))
-            @test @inferred(datasubset((Xv,y),i))  === (view(X,:,i), view(y,i))
-            @test @inferred(datasubset((X,yv),i))  === (view(X,:,i), view(y,i))
-            @test @inferred(datasubset((Xv,yv),i)) === (view(X,:,i), view(y,i))
-            @test @inferred(datasubset((XX,X,y),i)) === (view(XX,:,:,i), view(X,:,i),view(y,i))
-            # compare if obs match in tuple
-            x1, y1 = getobs(datasubset((X1,Y1), i))
-            @test all(x1' .== y1)
-            x1, y1, z1 = getobs(datasubset((X1,Y1,X1), i))
-            @test all(x1' .== y1)
-            @test all(x1 .== z1)
-        end
-    end
-
-    @testset "custom types" begin
-        @test_throws MethodError datasubset(EmptyType())
-        @test_throws MethodError datasubset(EmptyType(), 1:10)
-        @test_throws BoundsError getobs(datasubset(CustomType(), 11:20), 11)
-        @test typeof(@inferred(datasubset(CustomType()))) <: DataSubset
-        @test datasubset(CustomType()) == DataSubset(CustomType())
-        @test datasubset(CustomType(), 11:20) == DataSubset(CustomType(), 11:20)
-        @test numobs(datasubset(CustomType())) === 100
-        @test numobs(datasubset(CustomType(), 11:20)) === 10
-        @test getobs(datasubset(CustomType())) == collect(1:100)
-        @test getobs(datasubset(CustomType(), 11:20), 10) == 20
-        @test getobs(datasubset(CustomType(), 11:20), [3,5]) == [13,15]
-    end
-end
-
-@testset "getobs!" begin
-    @test getobs!(nothing, datasubset(y, 1))[1] == "setosa"
-
-    @testset "DataSubset" begin
-        xbuf1 = zeros(4,8)
-        s1 = DataSubset(X, 2:9)
-        @test @inferred(getobs!(xbuf1,s1)) === xbuf1
-        @test xbuf1 == getobs(s1)
-        xbuf1 = zeros(4,5)
-        s1 = DataSubset(X, 10:17)
-        @test @inferred(getobs!(xbuf1,s1,2:6)) === xbuf1
-        @test xbuf1 == getobs(s1,2:6) == getobs(X,11:15)
-
-        s3 = DataSubset(Xs, 11:15)
-        @test @inferred(getobs!(nothing,s3)) == getobs(Xs,11:15)
-
-        s4 = DataSubset(CustomType(), 6:10)
-        @test @inferred(getobs!(nothing,s4)) == getobs(s4)
-        s5 = DataSubset(CustomType(), 9:20)
-        @test @inferred(getobs!(nothing,s5,2:6)) == getobs(s5,2:6)
-    end
-
-    @testset "Tuple with DataSubset" begin
-        xbuf = zeros(4,2)
-        ybuf = ["foo", "bar"]
-        s1 = DataSubset(Xs, 5:9)
-        s2 = DataSubset(X, 5:9)
-        @test getobs!((nothing,xbuf),(s1,s2), 2:3) == (getobs(Xs,6:7),xbuf)
-        @test xbuf == getobs(X,6:7)
-        @test getobs!((nothing,xbuf),(s1,s2), 2:3) == (getobs(Xs,6:7),xbuf)
-        @test getobs!((nothing,xbuf),(s1,s2), 2:3) == (getobs(Xs,6:7),xbuf)
-    end
-end
+# @testset "datasubset" begin
+#     @testset "Array and SubArray" begin
+#         @test @inferred(datasubset(X)) == Xv
+#         @test @inferred(datasubset(X)) == Xv
+#         @test @inferred(datasubset(X)) == Xv
+#         @test typeof(datasubset(X)) <: SubArray
+#         @test @inferred(datasubset(Xv)) === Xv
+#         @test @inferred(datasubset(XX)) == XX
+#         @test @inferred(datasubset(XXX)) == XXX
+#         @test typeof(datasubset(XXX)) <: SubArray
+#         @test @inferred(datasubset(y)) == y
+#         @test typeof(datasubset(y)) <: SubArray
+#         @test @inferred(datasubset(yv)) === yv
+#         for i in (2, 1:15, 2:10, [2,5,7], [2,1])
+#             @test @inferred(datasubset(X,i))   === view(X,:,i)
+#             @test @inferred(datasubset(Xv,i))  === view(X,:,i)
+#             @test @inferred(datasubset(Xv,i))  === view(Xv,:,i)
+#             @test @inferred(datasubset(XX,i))  === view(XX,:,:,i)
+#             @test @inferred(datasubset(XXX,i)) === view(XXX,:,:,:,i)
+#             @test @inferred(datasubset(y,i))   === view(y,i)
+#             @test @inferred(datasubset(yv,i))  === view(y,i)
+#             @test @inferred(datasubset(yv,i))  === view(yv,i)
+#             @test @inferred(datasubset(Y,i))   === view(Y,:,i)
+#         end
+#     end
+
+#     @testset "Tuple of Array and Subarray" begin
+#         @test @inferred(datasubset((X,y)))   == (X,y)
+#         @test @inferred(datasubset((X,yv)))  == (X,yv)
+#         @test @inferred(datasubset((Xv,y)))  == (Xv,y)
+#         @test @inferred(datasubset((Xv,yv))) == (Xv,yv)
+#         @test @inferred(datasubset((X,Y)))   == (X,Y)
+#         @test @inferred(datasubset((XX,X,y))) == (XX,X,y)
+#         for i in (1:15, 2:10, [2,5,7], [2,1])
+#             @test @inferred(datasubset((X,y),i))   === (view(X,:,i), view(y,i))
+#             @test @inferred(datasubset((Xv,y),i))  === (view(X,:,i), view(y,i))
+#             @test @inferred(datasubset((X,yv),i))  === (view(X,:,i), view(y,i))
+#             @test @inferred(datasubset((Xv,yv),i)) === (view(X,:,i), view(y,i))
+#             @test @inferred(datasubset((XX,X,y),i)) === (view(XX,:,:,i), view(X,:,i),view(y,i))
+#             # compare if obs match in tuple
+#             x1, y1 = getobs(datasubset((X1,Y1), i))
+#             @test all(x1' .== y1)
+#             x1, y1, z1 = getobs(datasubset((X1,Y1,X1), i))
+#             @test all(x1' .== y1)
+#             @test all(x1 .== z1)
+#         end
+#     end
+
+#     @testset "custom types" begin
+#         @test_throws MethodError datasubset(EmptyType())
+#         @test_throws MethodError datasubset(EmptyType(), 1:10)
+#         @test_throws BoundsError getobs(datasubset(CustomType(), 11:20), 11)
+#         @test typeof(@inferred(datasubset(CustomType()))) <: DataSubset
+#         @test datasubset(CustomType()) == DataSubset(CustomType())
+#         @test datasubset(CustomType(), 11:20) == DataSubset(CustomType(), 11:20)
+#         @test numobs(datasubset(CustomType())) === 100
+#         @test numobs(datasubset(CustomType(), 11:20)) === 10
+#         @test getobs(datasubset(CustomType())) == collect(1:100)
+#         @test getobs(datasubset(CustomType(), 11:20), 10) == 20
+#         @test getobs(datasubset(CustomType(), 11:20), [3,5]) == [13,15]
+#     end
+# end
+
+# @testset "getobs!" begin
+#     @test getobs!(nothing, datasubset(y, 1)) == "setosa"
+
+#     @testset "DataSubset" begin
+#         xbuf1 = zeros(4,8)
+#         s1 = DataSubset(X, 2:9)
+#         @test @inferred(getobs!(xbuf1,s1)) === xbuf1
+#         @test xbuf1 == getobs(s1)
+#         xbuf1 = zeros(4,5)
+#         s1 = DataSubset(X, 10:17)
+#         @test @inferred(getobs!(xbuf1,s1,2:6)) === xbuf1
+#         @test xbuf1 == getobs(s1,2:6) == getobs(X,11:15)
+
+#         s3 = DataSubset(Xs, 11:15)
+#         @test @inferred(getobs!(nothing,s3)) == getobs(Xs,11:15)
+
+#         s4 = DataSubset(CustomType(), 6:10)
+#         @test @inferred(getobs!(nothing,s4)) == getobs(s4)
+#         s5 = DataSubset(CustomType(), 9:20)
+#         @test @inferred(getobs!(nothing,s5,2:6)) == getobs(s5,2:6)
+#     end
+
+#     @testset "Tuple with DataSubset" begin
+#         xbuf = zeros(4,2)
+#         ybuf = ["foo", "bar"]
+#         s1 = DataSubset(Xs, 5:9)
+#         s2 = DataSubset(X, 5:9)
+#         @test getobs!((nothing,xbuf),(s1,s2), 2:3) == (getobs(Xs,6:7),xbuf)
+#         @test xbuf == getobs(X,6:7)
+#         @test getobs!((nothing,xbuf),(s1,s2), 2:3) == (getobs(Xs,6:7),xbuf)
+#         @test getobs!((nothing,xbuf),(s1,s2), 2:3) == (getobs(Xs,6:7),xbuf)
+#     end
+# end
diff --git a/test/dataview.jl b/test/dataview.jl
new file mode 100644
index 0000000..18ab887
--- /dev/null
+++ b/test/dataview.jl
@@ -0,0 +1,253 @@
+@testset "ObsView" begin
+    @test ObsView <: AbstractVector
+    @test ObsView <: DataView
+    # @test ObsView <: AbstractObsView
+    # @test ObsView <: AbstractObsIterator
+    # @test ObsView <: AbstractDataIterator
+    @test obsview === ObsView
+
+    @testset "constructor" begin
+        # @test_throws DimensionMismatch ObsView((rand(2,10),rand(9)))
+        # @test_throws DimensionMismatch ObsView((rand(2,10),rand(4,9,10),rand(9)))
+        @test_throws MethodError ObsView(EmptyType())
+        @test_throws MethodError ObsView((EmptyType(),EmptyType()))
+        @test_throws MethodError ObsView((EmptyType(),EmptyType()))
+        for var in (vars..., Xs, ys)
+            A = @inferred(ObsView(var))
+        end
+        for var in (vars..., tuples..., Xs, ys)
+            A = ObsView(var)
+            @test @inferred(parent(A)) === var
+            @test @inferred(ObsView(A)) == A
+            @test @inferred(ObsView(var)) == A
+        end
+    end
+
+    @testset "typestability" begin
+        for var in (vars..., tuples..., Xs, ys)
+            @test typeof(@inferred(ObsView(var))) <: ObsView
+            @test typeof(@inferred(ObsView(var))) <: ObsView
+        end
+        for tup in tuples
+            @test typeof(@inferred(ObsView(tup))) <: ObsView
+        end
+        @test typeof(@inferred(ObsView(CustomType()))) <: ObsView
+    end
+
+    @testset "AbstractArray interface" begin
+        for var in (vars..., tuples..., Xs, ys)
+            A = ObsView(var)
+            @test_throws BoundsError A[-1]
+            @test_throws BoundsError A[16]
+            @test @inferred(numobs(A)) == 15
+            @test @inferred(length(A)) == 15
+            @test @inferred(size(A)) == (15,)
+            @test @inferred(A[2:3]) == ObsView(datasubset(var, 2:3))
+            @test @inferred(A[[1,3]]) == ObsView(datasubset(var, [1,3]))
+            @test @inferred(A[1]) == datasubset(var, 1)
+            @test @inferred(A[11]) == datasubset(var, 11)
+            @test @inferred(A[15]) == datasubset(var, 15)
+            @test A[end] == A[15]
+            @test @inferred(getobs(A,1)) == getobs(var, 1)
+            @test @inferred(getobs(A,11)) == getobs(var, 11)
+            @test @inferred(getobs(A,15)) == getobs(var, 15)
+            @test typeof(@inferred(collect(A))) <: Vector
+        end
+        for var in vars
+            A = ObsView(var)
+            @test @inferred(getobs(A)) == [getobs(var,i) for i in 1:numobs(var)]
+        end
+        for var in tuples
+            A = ObsView(var)
+            @test @inferred(getobs(A)) == [map(x->getobs(x,i), var) for i in 1:numobs(var)]
+        end
+    end
+
+    @testset "subsetting" begin
+        for var_raw in (vars..., tuples..., Xs, ys)
+        # for var_raw in (vars[1], )
+            for var in (var_raw, DataSubset(var_raw))
+            # for var in (var_raw,)
+                A = ObsView(var)
+                @test getobs(@inferred(datasubset(A))) == @inferred(getobs(A))
+                S = @inferred(datasubset(A, 1:5))
+                @test typeof(S) <: ObsView
+                @test @inferred(length(S)) == 5
+                @test @inferred(size(S)) == (5,)
+                @test @inferred(A[1:5]) == S
+                @test @inferred(getobs(A,1:5)) == getobs(S)
+                @test @inferred(getobs(S)) == getobs(ObsView(datasubset(var,1:5)))
+                S = @inferred(DataSubset(A, 1:5))
+                @test typeof(S) <: ObsView
+                @test typeof(S.data) <: Union{DataSubset,Tuple}
+                @test @inferred(length(S)) == 5
+                @test @inferred(size(S)) == (5,)
+                @test @inferred(getobs(S)) == getobs(ObsView(DataSubset(var,1:5)))
+            end
+        end
+        A = ObsView(X)
+        @test typeof(A.data) <: Array
+        S = @inferred(datasubset(A))
+        @test typeof(S) <: ObsView
+        @test @inferred(length(S)) == 15
+        @test typeof(S.data) <: SubArray
+    end
+
+    @testset "iteration" begin
+        count = 0
+        for (i,x) in enumerate(ObsView(X1))
+            @test all(i .== x)
+            count += 1
+        end
+        @test count == 15
+    end
+end
+
+# @testset "_compute_batch_settings" begin
+#     @test MLDataPattern._compute_batch_settings(X) === (30,5)
+#     @test MLDataPattern._compute_batch_settings(Xv) === (30,5)
+#     @test MLDataPattern._compute_batch_settings(Xs) === (30,5)
+#     @test MLDataPattern._compute_batch_settings(DataSubset(X)) === (30,5)
+#     @test MLDataPattern._compute_batch_settings((X,y)) === (30,5)
+#     @test MLDataPattern._compute_batch_settings((Xv,yv)) === (30,5)
+
+#     @test_throws ArgumentError MLDataPattern._compute_batch_settings(X, 160)
+#     @test_throws ArgumentError MLDataPattern._compute_batch_settings(X, 1, 160)
+#     @test_throws ArgumentError MLDataPattern._compute_batch_settings(X, 10, 20)
+
+#     for inner in (Xs, ys, vars...), var in (inner, DataSubset(inner))
+#         @test MLDataPattern._compute_batch_settings(var,10) === (10,15)
+#         @test MLDataPattern._compute_batch_settings(var,0,10) === (15,10)
+#         @test MLDataPattern._compute_batch_settings(var,-1,10) === (15,10)
+#         @test MLDataPattern._compute_batch_settings(var,10,10) === (10,15)
+#         @test MLDataPattern._compute_batch_settings(var,15,1) === (15,1)
+#         @test MLDataPattern._compute_batch_settings(var,15) === (15,1)
+#         @test MLDataPattern._compute_batch_settings(var,0,15) === (1,15)
+#         @test MLDataPattern._compute_batch_settings(var,-1,15) === (1,15)
+#     end
+# end
+
+# @testset "BatchView" begin
+#     @test BatchView <: AbstractVector
+#     @test BatchView <: LearnBase.DataView
+#     @test BatchView <: LearnBase.AbstractBatchView
+#     @test BatchView <: LearnBase.AbstractBatchIterator
+#     @test BatchView <: LearnBase.AbstractDataIterator
+#     @test batchview == BatchView
+#     @test_throws MethodError oversample(BatchView(X))
+#     @test_throws MethodError undersample(BatchView(X))
+#     @test_throws MethodError stratifiedobs(BatchView(X))
+
+#     @testset "constructor" begin
+#         @test_throws DimensionMismatch BatchView((rand(2,10),rand(9)))
+#         @test_throws DimensionMismatch BatchView((rand(2,10),rand(9)))
+#         @test_throws DimensionMismatch BatchView((rand(2,10),rand(4,9,10),rand(9)))
+#         @test_throws MethodError BatchView(EmptyType())
+#         for var in (vars..., tuples..., Xs, ys)
+#             @test_throws MethodError BatchView(var...)
+#             @test_throws ArgumentError BatchView(var, 151)
+#             A = BatchView(var, maxsize=151)
+#             @test length(A) == 1
+#             @test batchsize(A) == 15
+#             @test numobs(A) == numobs(var)
+#             @test @inferred(parent(BatchView(var))) === var
+#             A = @inferred(BatchView(var))
+#             @test @inferred(BatchView(A)) == A
+#             @test typeof(BatchView(A)) <: typeof(A)
+#             @test @inferred(BatchView(var)) == A
+#             @test numobs(A) == numobs(var)
+#             @test batchsize(A) == 30
+#         end
+#     end
+
+
+#     @testset "typestability" begin
+#         for var in (vars..., tuples..., Xs, ys)
+#             @test typeof(@inferred(BatchView(var))) <: BatchView
+#             @test typeof(@inferred(BatchView(var, 10))) <: BatchView
+#             @test typeof(@inferred(BatchView(var, 10, 15))) <: BatchView
+#             @test typeof(@inferred(BatchView(var, -1, 10))) <: BatchView
+#             @test typeof(@inferred(BatchView(var))) <: BatchView
+#         end
+#         for tup in tuples
+#             @test typeof(@inferred(BatchView(tup))) <: BatchView
+#             @test typeof(@inferred(BatchView(tup, 5))) <: BatchView
+#             @test typeof(@inferred(BatchView(tup, -1, 10))) <: BatchView
+#         end
+#         @test typeof(@inferred(BatchView(CustomType()))) <: BatchView
+#     end
+
+
+#     @testset "AbstractArray interface" begin
+#         for var in (vars..., tuples..., Xs, ys)
+#             A = BatchView(var, 15)
+#             @test_throws BoundsError A[-1]
+#             @test_throws BoundsError A[11]
+#             @test @inferred(numobs(A)) == 15
+#             @test @inferred(length(A)) == 10
+#             @test @inferred(batchsize(A)) == 15
+#             @test @inferred(size(A)) == (10,)
+#             @test @inferred(getobs(A[2:3])) == getobs(BatchView(datasubset(var, 16:45), 15))
+#             @test @inferred(getobs(A[[1,3]])) == getobs(BatchView(datasubset(var, [1:15..., 31:45...]), 15))
+#             @test @inferred(A[1]) == datasubset(var, 1:15)
+#             @test @inferred(A[2]) == datasubset(var, 16:30)
+#             @test @inferred(A[3]) == datasubset(var, 31:45)
+#             @test A[end] == A[10]
+#             @test @inferred(getobs(A,1)) == getobs(var, 1:15)
+#             @test @inferred(getobs(A,2)) == getobs(var, 16:30)
+#             @test @inferred(getobs(A,3)) == getobs(var, 31:45)
+#             @test typeof(@inferred(collect(A))) <: Vector
+#         end
+#         for var in (vars..., tuples...)
+#             A = BatchView(var, 15)
+#             @test @inferred(getobs(A)) == A
+#             @test @inferred(A[2:3]) == BatchView(datasubset(var, 16:45), 15)
+#             @test @inferred(A[[1,3]]) == BatchView(datasubset(var, [1:15..., 31:45...]), 15)
+#         end
+#     end
+
+
+#     @testset "subsetting" begin
+#         for var in (vars..., tuples..., Xs, ys)
+#             A = BatchView(var)
+#             @test getobs(@inferred(datasubset(A))) == @inferred(getobs(A))
+#             @test_throws ArgumentError datasubset(A,1:5)
+#             S = @inferred(datasubset(A, 1:60))
+#             @test typeof(S) <: BatchView
+#             @test @inferred(numobs(S)) == 60
+#             @test @inferred(length(S)) == 2
+#             @test @inferred(size(S)) == (2,)
+#             @test getobs(@inferred(A[1:2])) == getobs(S)
+#             @test @inferred(getobs(A,1:2)) == getobs(S)
+#             @test @inferred(getobs(S)) == getobs(BatchView(datasubset(var,1:60),30))
+#             S = @inferred(DataSubset(A, 1:60))
+#             @test typeof(S) <: BatchView
+#             @test typeof(S.data) <: Union{DataSubset,Tuple}
+#             @test @inferred(length(S)) == 2
+#             @test @inferred(size(S)) == (2,)
+#             @test @inferred(getobs(S)) == getobs(BatchView(DataSubset(var,1:60),30))
+#         end
+#         A = BatchView(X)
+#         @test typeof(A.data) <: Array
+#         S = @inferred(datasubset(A))
+#         @test typeof(S) <: BatchView
+#         @test @inferred(length(S)) == 5
+#         @test typeof(S.data) <: SubArray
+#     end
+
+#     @testset "nesting with ObsView" begin
+#         for var in vars
+#             @test eltype(@inferred(BatchView(ObsView(var)))[1]) <: Union{SubArray,String}
+#             @test @inferred(BatchView(BatchView(var))) == BatchView(var)
+#         end
+#         for var in tuples
+#             @test eltype(@inferred(BatchView(ObsView(var)))[1]) <: Tuple
+#             @test @inferred(BatchView(BatchView(var))) == BatchView(var)
+#         end
+#         for var in (Xs, ys)
+#             @test eltype(@inferred(BatchView(ObsView(var)))[1]) <: DataSubset
+#             @test @inferred(BatchView(BatchView(var))) == BatchView(var)
+#         end
+#         @test ObsView(BatchView(X)) == ObsView(X)
+#     end
+# end
diff --git a/test/observation.jl b/test/observation.jl
index 255a962..ef622db 100644
--- a/test/observation.jl
+++ b/test/observation.jl
@@ -1,18 +1,3 @@
-X = rand(4, 150)
-y = repeat(["setosa","versicolor","virginica"], inner = 50)
-Y = permutedims(hcat(y,y), [2,1])
-Xs = sprand(10,150,.5)
-ys = sprand(150,.5)
-Yt = hcat(y,y)
-yt = Y[1:1,:]
-Xv = view(X,:,:)
-yv = view(y,:)
-XX = rand(20,30,150)
-XXX = rand(3,20,30,150)
-X1 = hcat((1:150 for i = 1:10)...)'
-Y1 = collect(1:150)
-vars = (X, Xv, yv, XX, XXX, y)
-
 @testset "array" begin
     a = rand(2,3)
     @test numobs(a) == 3
@@ -44,18 +29,17 @@ end
 end
 
 @testset "dict" begin
-    X, Y = rand(2, 3), rand(3)
-    dataset = Dict("X" => X, "Y" => Y) 
-    @test numobs(dataset) == 3
+    dataset = Dict("X" => X, "y" => y) 
+    @test numobs(dataset) == 15
 
     @test_broken @inferred getobs(dataset, 2) # not inferred
     o = getobs(dataset, 2)
     @test o["X"] == X[:,2]
-    @test o["Y"] == Y[2]
+    @test o["y"] == y[2]
 
     o = getobs(dataset, 1:2)
     @test o["X"] == X[:,1:2]
-    @test o["Y"] == Y[1:2]
+    @test o["y"] == y[1:2]
 end
 
 @testset "numobs" begin
@@ -66,18 +50,18 @@ end
         @test_throws DimensionMismatch numobs((X,XX,rand(100)))
         @test_throws DimensionMismatch numobs((X,X'))
         for var in (Xs, ys, vars...)
-            @test @inferred(numobs(var)) === 150
+            @test @inferred(numobs(var)) === 15
         end
         @test @inferred(numobs(())) === 0
     end
 
     @testset "SubArray" begin
-        @test @inferred(numobs(view(X,:,:))) === 150
-        @test @inferred(numobs(view(X,:,:))) === 150
-        @test @inferred(numobs(view(XX,:,:,:))) === 150
-        @test @inferred(numobs(view(XXX,:,:,:,:))) === 150
-        @test @inferred(numobs(view(y,:))) === 150
-        @test @inferred(numobs(view(Y,:,:))) === 150
+        @test @inferred(numobs(view(X,:,:))) === 15
+        @test @inferred(numobs(view(X,:,:))) === 15
+        @test @inferred(numobs(view(XX,:,:,:))) === 15
+        @test @inferred(numobs(view(XXX,:,:,:,:))) === 15
+        @test @inferred(numobs(view(y,:))) === 15
+        @test @inferred(numobs(view(Y,:,:))) === 15
     end
 end
 
@@ -86,11 +70,11 @@ end
         # access outside numobs bounds
         @test_throws BoundsError getobs(X, -1)
         @test_throws BoundsError getobs(X, 0)
-        @test_throws BoundsError getobs(X, 151)
-        for i in (2, 2:20, [2,1,4])
+        @test_throws BoundsError getobs(X, 16)
+        for i in (2, 2:10, [2,1,4])
             @test getobs(XX, i) == XX[:, :, i]
         end
-        for i in (2, 1:150, 2:10, [2,5,7], [2,1])
+        for i in (2, 1:15, 2:10, [2,5,7], [2,1])
             @test typeof(getobs(Xv, i)) <: Array
             @test typeof(getobs(yv, i)) <: ((i isa Int) ? String : Array)
             @test all(getobs(Xv, i) .== X[:, i])
@@ -109,7 +93,7 @@ end
         @test typeof(getobs(Xs,1:5)) <: SparseMatrixCSC
         @test typeof(getobs(ys,2)) <: Float64
         @test typeof(getobs(ys,1:5)) <: SparseVector
-        for i in (2, 1:150, 2:10, [2,5,7], [2,1])
+        for i in (2, 1:15, 2:10, [2,5,7], [2,1])
             @test getobs(Xs,i) == Xs[:,i]
             @test getobs(ys,i) == ys[i]
         end
@@ -117,13 +101,13 @@ end
 
     @testset "Tuple" begin
         # bounds checking correctly
-        @test_throws BoundsError getobs((X,y), 151)
+        @test_throws BoundsError getobs((X,y), 16)
         # special case empty tuple
         @test @inferred(getobs((), 10)) === ()
-        tx, ty = getobs((Xv, yv), 10:50)
+        tx, ty = getobs((Xv, yv), 2:5)
         @test typeof(tx) <: Array
         @test typeof(ty) <: Array
-        for i in (1:150, 2:10, [2,5,7], [2,1])
+        for i in (1:15, 2:10, [2,5,7], [2,1])
             @test_throws DimensionMismatch getobs((X', y), i)
             @test @inferred(getobs((X,y), i))  == (X[:,i], y[i])
             @test @inferred(getobs((X,yv), i)) == (X[:,i], y[i])
@@ -155,7 +139,7 @@ end
         @test_throws BoundsError getobs!(Xbuf, X, 151)
         
         buff = zeros(4)
-        @test @inferred(getobs!(buff, X, 45)) == getobs(X, 45)
+        @test @inferred(getobs!(buff, X, 5)) == getobs(X, 5)
         
         buff = zeros(4, 8)
         @test @inferred(getobs!(buff, X, 3:10)) == getobs(X, 3:10)
@@ -192,9 +176,9 @@ end
         @test @inferred(getobs!((xbuf,ybuf),(X,y), 2:3)) === (xbuf,ybuf)
         @test xbuf == getobs(X, 2:3)
         @test ybuf == getobs(y, 2:3)
-        @test @inferred(getobs!((xbuf,ybuf),(X,y), [50,150])) === (xbuf,ybuf)
-        @test xbuf == getobs(X, [50,150])
-        @test ybuf == getobs(y, [50,150])
+        @test @inferred(getobs!((xbuf,ybuf),(X,y), [14,5])) === (xbuf,ybuf)
+        @test xbuf == getobs(X, [14,5])
+        @test ybuf == getobs(y, [14,5])
 
         @test getobs!((nothing,xbuf),(Xs,X), 3:4) == (getobs(Xs,3:4),xbuf)
         @test xbuf == getobs(X,3:4)
diff --git a/test/randobs.jl b/test/randobs.jl
index dc6e916..96c4dd3 100644
--- a/test/randobs.jl
+++ b/test/randobs.jl
@@ -1,9 +1,8 @@
-X = rand(3, 4)
-x = randobs(X)
-@test size(x) == (3,)
+x = randobs(X[:,1:4])
+@test size(x) == (4,)
 @test any(X[:,i] == x for i in 1:4) 
 
-x = randobs(X, 2) 
-@test size(x) == (3, 2)
+x = randobs(X[:,1:4], 2) 
+@test size(x) == (4, 2)
 @test any(X[:,i] == x[:,1] for i in 1:4) 
 @test any(X[:,i] == x[:,2] for i in 1:4)
diff --git a/test/runtests.jl b/test/runtests.jl
index 7e68149..3442235 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -5,8 +5,41 @@ using Test
 
 showcompact(io, x) = show(IOContext(io, :compact => true), x)
 
-@testset "MLUtils.jl" begin
-    @testset "observation" begin; include("observation.jl"); end
-    @testset "randobs" begin; include("randobs.jl"); end
-    @testset "datasubset" begin; include("datasubset.jl"); end
-end
+
+# --------------------------------------------------------------------
+# create some test data
+Random.seed!(1335)
+const X = rand(4, 15)
+const y = repeat(["setosa","versicolor","virginica"], inner = 5)
+const Y = permutedims(hcat(y,y), [2,1])
+const Yt = hcat(y,y)
+const yt = Y[1:1,:]
+const Xv = view(X,:,:)
+const yv = view(y,:)
+const XX = rand(20,30,15)
+const XXX = rand(3,20,30,15)
+const vars = (X, Xv, yv, XX, XXX, y)
+const tuples = ((X,y), (X,Y), (XX,X,y), (XXX,XX,X,y))
+const Xs = sprand(10, 15, 0.5)
+const ys = sprand(15, 0.5)
+const X1 = hcat((1:15 for i = 1:10)...)'
+const Y1 = collect(1:15)
+
+struct EmptyType end
+
+struct CustomType end
+MLUtils.numobs(::CustomType) = 15
+MLUtils.getobs(::CustomType, i::Int) = i
+MLUtils.getobs(::CustomType, i::AbstractVector) = collect(i)
+# MLUtils.gettargets(::CustomType, i::Int) = "obs $i"
+# MLUtils.gettargets(::CustomType, i::AbstractVector) = "batch $i"
+# --------------------------------------------------------------------
+
+# @testset "MLUtils.jl" begin
+@testset "observation" begin; include("observation.jl"); end
+@testset "randobs" begin; include("randobs.jl"); end
+@testset "datasubset" begin; include("datasubset.jl"); end
+@testset "splitobs" begin; include("splitobs.jl"); end
+@testset "shuffleobs" begin; include("shuffleobs.jl"); end
+@testset "dataview" begin; include("dataview.jl"); end
+# end
diff --git a/test/shuffleobs.jl b/test/shuffleobs.jl
new file mode 100644
index 0000000..875428f
--- /dev/null
+++ b/test/shuffleobs.jl
@@ -0,0 +1,92 @@
+@test_throws DimensionMismatch shuffleobs((X, rand(149)))
+
+@testset "typestability" begin
+    for var in vars
+        @test typeof(@inferred(shuffleobs(var))) <: SubArray
+    end
+    for tup in tuples
+        @test typeof(@inferred(shuffleobs(tup))) <: Tuple
+    end
+end
+
+@testset "Array and SubArray" begin
+    for var in vars
+        @test size(shuffleobs(var)) == size(var)
+    end
+    # tests if all obs are still present and none duplicated
+    @test sum(shuffleobs(Y1)) == 120
+end
+
+@testset "Tuple of Array and SubArray" begin
+    for var in ((X,yv), (Xv,y), tuples...)
+        @test_throws MethodError shuffleobs(var...)
+        @test typeof(shuffleobs(var)) <: Tuple
+        @test all(map(x->(typeof(x)<:SubArray), shuffleobs(var)))
+        @test all(map(x->(numobs(x)===15), shuffleobs(var)))
+    end
+    # tests if all obs are still present and none duplicated
+    # also tests that both paramter are shuffled identically
+    x1, y1, z1 = shuffleobs((X1,Y1,X1))
+    @test vec(sum(x1,dims=2)) == fill(120,10)
+    @test vec(sum(z1,dims=2)) == fill(120,10)
+    @test sum(y1) == 120
+    @test all(x1' .== y1)
+    @test all(z1' .== y1)
+end
+
+@testset "SparseArray" begin
+    for var in (Xs, ys)
+        @test typeof(shuffleobs(var)) <: SubArray
+        @test numobs(shuffleobs(var)) == numobs(var)
+    end
+    # tests if all obs are still present and none duplicated
+    @test vec(sum(getobs(shuffleobs(sparse(X1))),dims=2)) == fill(120,10)
+    @test sum(getobs(shuffleobs(sparse(Y1)))) == 120
+end
+
+@testset "Tuple of SparseArray" begin
+    for var in ((Xs,ys), (X,ys), (Xs,y), (Xs,Xs), (XX,X,ys))
+        @test_throws MethodError shuffleobs(var...)
+        @test typeof(shuffleobs(var)) <: Tuple
+        @test numobs(shuffleobs(var)) == numobs(var)
+    end
+    # tests if all obs are still present and none duplicated
+    # also tests that both paramter are shuffled identically
+    x1, y1 = getobs(shuffleobs((sparse(X1),sparse(Y1))))
+    @test vec(sum(x1,dims=2)) == fill(120,10)
+    @test sum(y1) == 120
+    @test all(x1' .== y1)
+end
+
+# @testset "ObsView" begin
+#     # tests if all obs are still present and none duplicated
+#     ov = @inferred shuffleobs(obsview(X1))
+#     @test ov isa ObsView
+#     x1 = getobs(ov)
+#     @test sum(x1) == fill(120,10)
+#     # also tests that both paramter are shuffled identically
+#     ov = @inferred shuffleobs(obsview((X1,X1)))
+#     @test ov isa ObsView
+#     x1 = getobs(ov)
+#     for i = 1:length(x1)
+#         @test x1[i][1] == x1[i][2]
+#     end
+#     for i = 1:2
+#         @test sum(getindex.(x1,i)) == fill(120,10)
+#     end
+# end
+
+# @testset "BatchView" begin
+#     # tests if all obs are still present and none duplicated
+#     bv1 = batchview(X1, 30)
+#     bv = @inferred shuffleobs(bv1)
+#     @test length(bv) == length(bv1)
+#     @test bv isa BatchView{<:SubArray,<:SubArray}
+#     @test vec(sum(sum(bv),dims=2)) == fill(120,10)
+# end
+
+@testset "RNG" begin
+    # tests reproducibility
+    explicit_shuffle = shuffleobs(MersenneTwister(42), (X, y))
+    @test explicit_shuffle == shuffleobs(MersenneTwister(42), (X, y))
+end
diff --git a/test/splitobs.jl b/test/splitobs.jl
new file mode 100644
index 0000000..1db9c7d
--- /dev/null
+++ b/test/splitobs.jl
@@ -0,0 +1,91 @@
+@test_throws DimensionMismatch splitobs((X, rand(149)), at=0.7)
+
+@testset "typestability" begin
+    @testset "Int" begin
+        @test typeof(@inferred(splitobs(10, at=0.))) <: NTuple{2}
+        @test eltype(@inferred(splitobs(10, at=0.))) <: UnitRange
+        @test typeof(@inferred(splitobs(10, at=1.))) <: NTuple{2}
+        @test eltype(@inferred(splitobs(10, at=1.))) <: UnitRange
+        @test typeof(@inferred(splitobs(10, at=0.7))) <: NTuple{2}
+        @test eltype(@inferred(splitobs(10, at=0.7))) <: UnitRange
+        @test typeof(@inferred(splitobs(10, at=0.5))) <: NTuple{2}
+        @test typeof(@inferred(splitobs(10, at=(0.5,0.2)))) <: NTuple{3}
+        @test eltype(@inferred(splitobs(10, at=0.5))) <: UnitRange
+        @test eltype(@inferred(splitobs(10, at=(0.5,0.2)))) <: UnitRange
+        @test eltype(@inferred(splitobs(10, at=0.5))) <: UnitRange
+        @test eltype(@inferred(splitobs(10, at=(0.,0.2)))) <: UnitRange
+    end
+    for var in vars
+        @test typeof(@inferred(splitobs(var, at=0.7))) <: NTuple{2}
+        @test eltype(@inferred(splitobs(var, at=0.7))) <: SubArray
+        @test typeof(@inferred(splitobs(var, at=0.5))) <: NTuple{2}
+        @test typeof(@inferred(splitobs(var, at=(0.5,0.2)))) <: NTuple{3}
+        @test eltype(@inferred(splitobs(var, at=0.5))) <: SubArray
+        @test eltype(@inferred(splitobs(var, at=(0.5,0.2)))) <: SubArray
+    end
+    for tup in tuples
+        @test typeof(@inferred(splitobs(tup, at=0.5))) <: NTuple{2}
+        @test typeof(@inferred(splitobs(tup, at=(0.5,0.2)))) <: NTuple{3}
+        @test eltype(@inferred(splitobs(tup, at=0.5))) <: Tuple
+        @test eltype(@inferred(splitobs(tup, at=(0.5,0.2)))) <: Tuple
+    end
+end
+
+@testset "Int" begin
+    @test splitobs(10, at=0.7) == (1:7,8:10)
+    @test splitobs(10, at=0.5) == (1:5,6:10)
+    @test splitobs(10, at=(0.5,0.3)) == (1:5,6:8,9:10)
+    @test splitobs(150, at=0.7) == splitobs(150, at=0.7)
+    @test splitobs(150, at=0.5) == splitobs(150, at=0.5)
+    @test splitobs(150, at=(0.5,0.2)) == splitobs(150, at=(0.5,0.2))
+    @test numobs.(splitobs(150, at=0.7)) == (105,45)
+    @test numobs.(splitobs(150, at=(.2,.3))) == (30,45,75)
+    @test numobs.(splitobs(150, at=(.1,.2,.3))) == (15,30,45,60)
+    # tests if all obs are still present and none duplicated
+    @test sum(sum.(getobs.(splitobs(150, at=0.7)))) == 11325
+    @test sum(sum.(splitobs(150,at=.1))) == 11325
+    @test sum(sum.(splitobs(150,at=(.2,.1)))) == 11325
+    @test sum(sum.(splitobs(150,at=(.1,.4,.2)))) == 11325
+    @test sum.(splitobs(150, at=0.7)) == (5565, 5760)
+end
+
+@testset "Array, SparseArray, and SubArray" begin
+    for var in (Xs, ys, vars...)
+        @test numobs.(splitobs(var, at=10)) == (10,5)
+        @test numobs.(splitobs(var, at=0.7)) == (10, 5)
+        @test numobs.(splitobs(var, at=(.2,.3))) == (3,4, 8)
+        @test numobs.(splitobs(var, at=(.11,.2,.3))) == (2,3,4,6)
+        @test numobs.(splitobs(var, at=(2,3,4))) == (2,3,4,6)
+    end
+    # tests if all obs are still present and none duplicated
+    @test sum(sum.(splitobs(X1,at=.1))) == 1200
+    @test sum(sum.(splitobs(X1,at=(.2,.1)))) == 1200
+    @test sum(sum.(splitobs(X1,at=(.1,.4,.2)))) == 1200
+end
+
+@testset "Tuple of Array, SparseArray, and SubArray" begin
+    for tup in ((Xs,ys), (X,ys), (Xs,y), (Xs,Xs), (XX,X,ys), (X,yv), (Xv,y), tuples...)
+        @test_throws MethodError splitobs(tup..., at=0.5)
+        @test all(map(x->(typeof(x)<:Tuple), splitobs(tup,at=0.5)))
+        @test numobs.(splitobs(tup, at=0.6)) == (9, 6)
+        @test numobs.(splitobs(tup, at=(.2,.3))) == (3,4,8)
+    end
+end
+
+# @testset "ObsView" begin
+#     A = ObsView(X)
+#     b, c = @inferred splitobs(A, .7)
+#     @test b isa ObsView
+#     @test c isa ObsView
+#     @test b == A[1:105]
+#     @test c == A[106:end]
+# end
+
+# @testset "BatchView" begin
+#     A = BatchView(X, 5)
+#     b, c = @inferred splitobs(A, .6)
+#     @test b isa BatchView
+#     @test c isa BatchView
+#     @test b == A[1:18]
+#     @test c == A[19:end]
+# end