Skip to content

Commit

Permalink
Add MatrixTable type to wrap a Matrix and provide Tables.jl interface. (
Browse files Browse the repository at this point in the history
#61)

* Add MatrixTable type to wrap a Matrix and provide Tables.jl interface.

* Implement Tables.asmatrix sink function to turn any table into a Matrix

* Cleanups

* Cleanup
  • Loading branch information
quinnj authored Feb 4, 2019
1 parent 6ac138d commit 265ff4e
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 1 deletion.
1 change: 1 addition & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ authors = ["quinnj <[email protected]>"]
version = "0.1.14"

[deps]
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
Requires = "ae029012-a4dd-5104-9daa-d747884805df"
TableTraits = "3783bdb8-4a98-5b6b-af9a-565f29a5fe9c"
IteratorInterfaceExtensions = "82899510-4779-5014-852e-03e436cf321d"
Expand Down
5 changes: 4 additions & 1 deletion src/Tables.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module Tables

using Requires
using Requires, LinearAlgebra

using TableTraits, IteratorInterfaceExtensions

Expand Down Expand Up @@ -191,4 +191,7 @@ include("iteratorwrapper.jl")
# simple table operations on table inputs
include("operations.jl")

# matrix integration
include("matrix.jl")

end # module
76 changes: 76 additions & 0 deletions src/matrix.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
istable(::Type{<:AbstractMatrix}) = false

rows(m::T) where {T <: AbstractMatrix} = throw(ArgumentError("a '$T' is not a table; see `?Tables.table` for ways to treat an AbstractMatrix as a table"))
columns(m::T) where {T <: AbstractMatrix} = throw(ArgumentError("a '$T' is not a table; see `?Tables.table` for ways to treat an AbstractMatrix as a table"))

struct MatrixTable{T <: AbstractMatrix}
names::Vector{Symbol}
lookup::Dict{Symbol, Int}
matrix::T
end

istable(::Type{<:MatrixTable}) = true
names(m::MatrixTable) = getfield(m, :names)

# row interface
struct MatrixRow{T}
row::Int
source::MatrixTable{T}
end

Base.getproperty(m::MatrixRow, ::Type, col::Int, nm::Symbol) =
getfield(getfield(m, :source), :matrix)[getfield(m, :row), col]
Base.getproperty(m::MatrixRow, nm::Symbol) =
getfield(getfield(m, :source), :matrix)[getfield(m, :row), getfield(getfield(m, :source), :lookup)[nm]]
Base.propertynames(m::MatrixRow) = names(getfield(m, :source))

rowaccess(::Type{<:MatrixTable}) = true
schema(m::MatrixTable{T}) where {T} = Schema(Tuple(names(m)), NTuple{size(getfield(m, :matrix), 2), eltype(T)})
rows(m::MatrixTable) = m
Base.eltype(m::MatrixTable{T}) where {T} = MatrixRow{T}
Base.length(m::MatrixTable) = size(getfield(m, :matrix), 1)

function Base.iterate(m::MatrixTable, st=1)
st > length(m) && return nothing
return MatrixRow(st, m), st + 1
end

# column interface
columnaccess(::Type{<:MatrixTable}) = true
columns(m::MatrixTable) = m
Base.getproperty(m::MatrixTable, ::Type{T}, col::Int, nm::Symbol) where {T} = getfield(m, :matrix)[:, col]
Base.getproperty(m::MatrixTable, nm::Symbol) = getfield(m, :matrix)[:, getfield(m, :lookup)[nm]]
Base.propertynames(m::MatrixTable) = names(m)

"""
Tables.table(m::AbstractMatrix; [header::Vector{Symbol}])
Wrap an `AbstractMatrix` (`Matrix`, `Adjoint`, etc.) in a `MatrixTable`, which satisfies
the Tables.jl interface. This allows accesing the matrix via `Tables.rows` and
`Tables.columns`. An optional keyword argument `header` can be passed as a `Vector{Symbol}`
to be used as the column names. Note that no copy of the `AbstractMatrix` is made.
"""
function table(m::AbstractMatrix; header::Vector{Symbol}=[Symbol("Column$i") for i = 1:size(m, 2)])
length(header) == size(m, 2) || throw(ArgumentError("provided column names `header` length must match number of columns in matrix ($(size(m, 2))"))
lookup = Dict(nm=>i for (i, nm) in enumerate(header))
return MatrixTable(header, lookup, m)
end

"""
Tables.matrix(table)
Materialize any table source input as a `Matrix`. If the table column types are not homogenous,
they will be promoted to a common type in the materialized `Matrix`. Note that column names are
ignored in the conversion.
"""
function matrix(table)
cols = Tables.columns(table)
types = schema(cols).types
T = reduce(promote_type, types)
n, p = rowcount(cols), length(types)
mat = Matrix{T}(undef, n, p)
for (i, col) in enumerate(Tables.eachcolumn(cols))
copyto!(mat, n * (i - 1) + 1, col)
end
return mat
end
20 changes: 20 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,26 @@ end
@test select(rt, :a) == [(a=1,), (a=2,), (a=3,)]
end

@testset "Matrix integration" begin
rt = [(a=1, b=4.0, c="7"), (a=2, b=5.0, c="8"), (a=3, b=6.0, c="9")]
nt = (a=[1,2,3], b=[4.0, 5.0, 6.0])

mat = Tables.matrix(rt)
@test nt.a == mat[:, 1]
@test size(mat) == (3, 3)
@test eltype(mat) == Any
mat2 = Tables.matrix(nt)
@test eltype(mat2) == Float64
@test mat2[:, 1] == nt.a

tbl = Tables.table(mat) |> columntable
@test keys(tbl) == (:Column1, :Column2, :Column3)
@test tbl.Column1 == [1, 2, 3]
tbl2 = Tables.table(mat2) |> rowtable
@test length(tbl2) == 3
@test map(x->x.Column1, tbl2) == [1.0, 2.0, 3.0]
end

import Base: ==
struct GenericRow
a::Int
Expand Down

0 comments on commit 265ff4e

Please sign in to comment.