Skip to content

Commit 265ff4e

Browse files
authored
Add MatrixTable type to wrap a Matrix and provide Tables.jl interface. (#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
1 parent 6ac138d commit 265ff4e

File tree

4 files changed

+101
-1
lines changed

4 files changed

+101
-1
lines changed

Project.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ authors = ["quinnj <[email protected]>"]
44
version = "0.1.14"
55

66
[deps]
7+
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
78
Requires = "ae029012-a4dd-5104-9daa-d747884805df"
89
TableTraits = "3783bdb8-4a98-5b6b-af9a-565f29a5fe9c"
910
IteratorInterfaceExtensions = "82899510-4779-5014-852e-03e436cf321d"

src/Tables.jl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module Tables
22

3-
using Requires
3+
using Requires, LinearAlgebra
44

55
using TableTraits, IteratorInterfaceExtensions
66

@@ -191,4 +191,7 @@ include("iteratorwrapper.jl")
191191
# simple table operations on table inputs
192192
include("operations.jl")
193193

194+
# matrix integration
195+
include("matrix.jl")
196+
194197
end # module

src/matrix.jl

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
istable(::Type{<:AbstractMatrix}) = false
2+
3+
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"))
4+
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"))
5+
6+
struct MatrixTable{T <: AbstractMatrix}
7+
names::Vector{Symbol}
8+
lookup::Dict{Symbol, Int}
9+
matrix::T
10+
end
11+
12+
istable(::Type{<:MatrixTable}) = true
13+
names(m::MatrixTable) = getfield(m, :names)
14+
15+
# row interface
16+
struct MatrixRow{T}
17+
row::Int
18+
source::MatrixTable{T}
19+
end
20+
21+
Base.getproperty(m::MatrixRow, ::Type, col::Int, nm::Symbol) =
22+
getfield(getfield(m, :source), :matrix)[getfield(m, :row), col]
23+
Base.getproperty(m::MatrixRow, nm::Symbol) =
24+
getfield(getfield(m, :source), :matrix)[getfield(m, :row), getfield(getfield(m, :source), :lookup)[nm]]
25+
Base.propertynames(m::MatrixRow) = names(getfield(m, :source))
26+
27+
rowaccess(::Type{<:MatrixTable}) = true
28+
schema(m::MatrixTable{T}) where {T} = Schema(Tuple(names(m)), NTuple{size(getfield(m, :matrix), 2), eltype(T)})
29+
rows(m::MatrixTable) = m
30+
Base.eltype(m::MatrixTable{T}) where {T} = MatrixRow{T}
31+
Base.length(m::MatrixTable) = size(getfield(m, :matrix), 1)
32+
33+
function Base.iterate(m::MatrixTable, st=1)
34+
st > length(m) && return nothing
35+
return MatrixRow(st, m), st + 1
36+
end
37+
38+
# column interface
39+
columnaccess(::Type{<:MatrixTable}) = true
40+
columns(m::MatrixTable) = m
41+
Base.getproperty(m::MatrixTable, ::Type{T}, col::Int, nm::Symbol) where {T} = getfield(m, :matrix)[:, col]
42+
Base.getproperty(m::MatrixTable, nm::Symbol) = getfield(m, :matrix)[:, getfield(m, :lookup)[nm]]
43+
Base.propertynames(m::MatrixTable) = names(m)
44+
45+
"""
46+
Tables.table(m::AbstractMatrix; [header::Vector{Symbol}])
47+
48+
Wrap an `AbstractMatrix` (`Matrix`, `Adjoint`, etc.) in a `MatrixTable`, which satisfies
49+
the Tables.jl interface. This allows accesing the matrix via `Tables.rows` and
50+
`Tables.columns`. An optional keyword argument `header` can be passed as a `Vector{Symbol}`
51+
to be used as the column names. Note that no copy of the `AbstractMatrix` is made.
52+
"""
53+
function table(m::AbstractMatrix; header::Vector{Symbol}=[Symbol("Column$i") for i = 1:size(m, 2)])
54+
length(header) == size(m, 2) || throw(ArgumentError("provided column names `header` length must match number of columns in matrix ($(size(m, 2))"))
55+
lookup = Dict(nm=>i for (i, nm) in enumerate(header))
56+
return MatrixTable(header, lookup, m)
57+
end
58+
59+
"""
60+
Tables.matrix(table)
61+
62+
Materialize any table source input as a `Matrix`. If the table column types are not homogenous,
63+
they will be promoted to a common type in the materialized `Matrix`. Note that column names are
64+
ignored in the conversion.
65+
"""
66+
function matrix(table)
67+
cols = Tables.columns(table)
68+
types = schema(cols).types
69+
T = reduce(promote_type, types)
70+
n, p = rowcount(cols), length(types)
71+
mat = Matrix{T}(undef, n, p)
72+
for (i, col) in enumerate(Tables.eachcolumn(cols))
73+
copyto!(mat, n * (i - 1) + 1, col)
74+
end
75+
return mat
76+
end

test/runtests.jl

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,26 @@ end
129129
@test select(rt, :a) == [(a=1,), (a=2,), (a=3,)]
130130
end
131131

132+
@testset "Matrix integration" begin
133+
rt = [(a=1, b=4.0, c="7"), (a=2, b=5.0, c="8"), (a=3, b=6.0, c="9")]
134+
nt = (a=[1,2,3], b=[4.0, 5.0, 6.0])
135+
136+
mat = Tables.matrix(rt)
137+
@test nt.a == mat[:, 1]
138+
@test size(mat) == (3, 3)
139+
@test eltype(mat) == Any
140+
mat2 = Tables.matrix(nt)
141+
@test eltype(mat2) == Float64
142+
@test mat2[:, 1] == nt.a
143+
144+
tbl = Tables.table(mat) |> columntable
145+
@test keys(tbl) == (:Column1, :Column2, :Column3)
146+
@test tbl.Column1 == [1, 2, 3]
147+
tbl2 = Tables.table(mat2) |> rowtable
148+
@test length(tbl2) == 3
149+
@test map(x->x.Column1, tbl2) == [1.0, 2.0, 3.0]
150+
end
151+
132152
import Base: ==
133153
struct GenericRow
134154
a::Int

0 commit comments

Comments
 (0)