Skip to content

Commit

Permalink
Reduce dependency on Makie recipes (#23)
Browse files Browse the repository at this point in the history
* recipe conversion

* CI bump
  • Loading branch information
palday authored Mar 23, 2024
1 parent b7155a3 commit d211202
Show file tree
Hide file tree
Showing 11 changed files with 77 additions and 144 deletions.
8 changes: 5 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
version: [1.6, 1]
version: [1.9, 1]
arch: [x64]
os: [ubuntu-latest]
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Julia Setup
uses: julia-actions/setup-julia@v1
with:
Expand All @@ -45,10 +45,12 @@ jobs:
uses: julia-actions/julia-processcoverage@v1
if: ${{ startsWith(matrix.os, 'ubuntu') && (matrix.version == '1') }}
- name: Coverage Upload
uses: codecov/codecov-action@v2
uses: codecov/codecov-action@v4
if: ${{ startsWith(matrix.os, 'ubuntu') && (matrix.version == '1') }}
with:
file: lcov.info
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
- name: Setup Node for Percy
if: ${{ matrix.version == '1' }}
uses: actions/setup-node@v3
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/documenter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
name: Documentation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- uses: julia-actions/cache@v1
with:
cache-compiled: "true"
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/style.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,19 @@ jobs:
permissions: write-all
strategy:
matrix:
julia-version: [1.6]
julia-version: [1.9]
steps:
- uses: julia-actions/setup-julia@latest
with:
version: ${{ matrix.julia-version }}
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Instantiate `format` environment and format
shell: julia --color=yes -O0 {0}
run: |
julia -e'
using Pkg; Pkg.activate()
Pkg.add("JuliaFormatter")
using JuliaFormatter
format(".", YASStyle())'
format(".", YASStyle())
- uses: reviewdog/action-suggester@v1
if: github.event_name == 'pull_request'
with:
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ docs/site/
# committed for packages, but should be committed for applications that require a static
# environment.
Manifest.toml
Manifest-v*.toml

# Editor files
.vscode/*
*~
coverage/lcov.info
9 changes: 3 additions & 6 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
name = "BoxCox"
uuid = "1248164d-f7a6-4bdb-8e8d-8c4a187b3ce6"
authors = ["Phillip Alday <[email protected]>"]
version = "0.2.4"
version = "0.3.0"

[deps]
Compat = "34da2185-b29b-5c13-b0c7-acf172513d20"
DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a"
Expand Down Expand Up @@ -32,10 +31,9 @@ BoxCoxStatsModelsExt = ["StatsModels", "Tables"]

[compat]
Aqua = "0.6"
Compat = "3.29, 4"
DocStringExtensions = "0.9"
LinearAlgebra = "1"
Makie = "0.19.0, 0.20"
Makie = "0.19, 0.20"
MixedModels = "4"
NLopt = "0.6, 1"
PrecompileTools = "1"
Expand All @@ -47,12 +45,11 @@ StatsBase = "0.33, 0.34"
StatsFuns = "1"
StatsModels = "0.6, 0.7"
Tables = "1"
julia = "1.6"
julia = "1.9"

[extras]
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0"
Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a"
MixedModels = "ff71e718-51f3-5ec2-a782-8ffcbfa3c316"
RDatasets = "ce6b1742-4840-55fa-b093-852dadbb1d8b"
StatsModels = "3eaba693-59b7-5ba5-a881-562e759f1c8d"
Expand Down
2 changes: 1 addition & 1 deletion docs/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
StatsModels = "3eaba693-59b7-5ba5-a881-562e759f1c8d"

[compat]
Documenter = "0.27"
Documenter = "1.3"
1 change: 0 additions & 1 deletion docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ using BoxCox
makedocs(; root=joinpath(dirname(pathof(BoxCox)), "..", "docs"),
sitename="BoxCox",
doctest=true,
strict=true,
pages=["index.md",
"mixed-models.md",
"api.md"])
Expand Down
5 changes: 2 additions & 3 deletions docs/src/mixed-models.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ CurrentModule = BoxCox
```

BoxCox.jl supports finding fitting the Box-Cox transformation to a `LinearMixedModel` from MixedModels.jl.
On Julia 1.9 and above, this support is done via a package extension and so the user pays no dependency or precompilation penalty when this functionality is not used.
On Julia 1.6 to 1.8, this functionality is defined unconditionally (thus incurring the dependency and precompilation penalty), but neither the `@formula` macro nor the `MixedModel` type are re-exported, so MixedModels.jl must still be loaded to use this functionality.
This support is done via a package extension and so the user pays no dependency or precompilation penalty when this functionality is not used.


Let us examine the classic sleepstudy dataset from MixedModels.jl. First, we load the necessary packages.
Expand Down Expand Up @@ -77,7 +76,7 @@ model_bc = fit(MixedModel,
dataset(:sleepstudy))
```

For our original model on the untransformed scale, the intercept was approximately 250, which means that the average response time was about 250 milliseconds.
For our original model on the untransformed scale, the intercept was approximately 250, which means that the average response time was about 250 milliseconds.
For the model on the speed scale, we have an intercept about approximately 4, which means that the average response speed is about 4 responses per second, which implies that the the average response time is 250 milliseconds.
In other words, our new results are compatible with our previous estimates.

Expand Down
138 changes: 38 additions & 100 deletions ext/BoxCoxMakieExt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,17 @@ using Makie

using BoxCox: _loglikelihood_boxcox!,
_loglikelihood_boxcox,
qr, chisqinvcdf, @compat,
qr, chisqinvcdf,
@setup_workload, @compile_workload

# XXX it would be great to have a 1-1 aspect ratio here,
# but this seems like something that should be done upstream
function Makie.convert_arguments(P::Type{<:Makie.QQNorm}, x::BoxCoxTransformation, args...;
qqline=:fitrobust, kwargs...)
return convert_arguments(P, x.(x.y), args...; qqline, kwargs...)
function Makie.qqnorm!(ax::Axis, bc::BoxCoxTransformation, args...; kwargs...)
return qqnorm!(ax, bc.(bc.y), args...; kwargs...)
end

function Makie.convert_arguments(P::Type{<:Union{Makie.Scatter,Makie.Lines}},
bc::BoxCoxTransformation, args...;
λ=nothing, n_steps=21, kwargs...)
if isnothing(λ)
# TODO: cache this somehow so we're not computing it twice
# when also plotting the CI
ci = confint(bc; fast=true)
lower = first(ci) - 0.05 * abs(first(ci))
upper = last(ci) + 0.05 * abs(last(ci))
λ = range(lower, upper; length=n_steps)
end
sort!(collect(λ))

@compat (; X, y) = bc
ll = _loglikelihood_boxcox(X, y, λ)
return convert_arguments(P, λ, ll, args...; kwargs...)
function Makie.qqnorm(bc::BoxCoxTransformation, args...; kwargs...)
return qqnorm(bc.(bc.y), args...; kwargs...)
end

function BoxCox._loglikelihood_boxcox(X::AbstractMatrix{<:Number}, y::Vector{<:Number},
Expand All @@ -54,97 +39,50 @@ function BoxCox._loglikelihood_boxcox(::Nothing, y::Vector{<:Number},
return ll
end

function Makie.convert_arguments(P::Type{<:Makie.VLines}, bc::BoxCoxTransformation, args...;
kwargs...)
return convert_arguments(P, bc.λ, args...; kwargs...)
end

@recipe(BCPlot, boxcox) do scene
s_theme = default_theme(scene, Scatter)
l_theme = default_theme(scene, Lines)
automatic = Makie.automatic
scatline = Attributes(; color=l_theme.color,
colormap=l_theme.colormap,
# colorscale = l_theme.colorscale,
colorrange=get(l_theme.attributes, :colorrange, automatic),
linestyle=l_theme.linestyle,
linewidth=l_theme.linewidth,
markercolor=automatic,
markercolormap=theme(scene, :colormap),
markercolorrange=get(s_theme.attributes, :colorrange, automatic),
markersize=s_theme.markersize,
strokecolor=s_theme.strokecolor,
strokewidth=s_theme.strokewidth,
marker=s_theme.marker,
inspectable=theme(scene, :inspectable),
cycle=[:color])
return merge(scatline, Attributes(; conf_level=0.95, n_steps=10, λ=nothing))
end

function Makie.plot!(p::BCPlot)
# markercolor is the same as linecolor if left automatic
# RGBColors -> union of all colortypes that `to_color` accepts + returns
real_markercolor = Observable{Makie.RGBColors}()
map!(real_markercolor, p.color, p.markercolor) do col, mcol
if mcol === Makie.automatic
return to_color(col)
else
return to_color(mcol)
end
end

bc = p[1][]
n_steps = p.n_steps[]
λ = p.λ[]
# TODO use splines
scatterlines!(p, bc; λ, n_steps,
p.strokecolor, p.strokewidth, p.marker, p.markersize,
color=real_markercolor, p.linestyle, p.linewidth, p.colormap, # p.colorscale,
p.colorrange, p.inspectable)
# seem to be hitting some buggy behavior in Makie
# vlines!(p, bc; λ,
# p.color, linestyle=:dash, p.linewidth, p.colormap, # p.colorscale,
# p.colorrange, p.inspectable)
return plot
function BoxCox.boxcoxplot(bc::BoxCoxTransformation; kwargs...)
fig = Figure()
boxcoxplot!(Axis(fig[1, 1]), bc; kwargs...)
return fig
end

BoxCox.boxcoxplot!(ax, bc::BoxCoxTransformation; kwargs...) = bcplot!(ax, bc; kwargs...)
BoxCox.boxcoxplot(bc::BoxCoxTransformation; kwargs...) = bcplot(bc; kwargs...)
function BoxCox.boxcoxplot!(ax::Axis, bc::BoxCoxTransformation;
xlabel="λ",
ylabel="log likelihood",
n_steps=21,
λ=nothing,
conf_level=nothing,
attributes...)
ax.xlabel = xlabel
ax.ylabel = ylabel

function Makie.plot!(ax::Axis, P::Type{<:BCPlot}, allattrs::Makie.Attributes, bc)
allattrs = merge(default_theme(P), allattrs)
plot = Makie.plot!(ax.scene, P, allattrs, bc)
ci = nothing

if haskey(allattrs, :title)
ax.title = allattrs.title[]
end
if haskey(allattrs, :xlabel)
ax.xlabel = allattrs.xlabel[]
else
ax.xlabel = "λ"
end
if haskey(allattrs, :ylabel)
ax.ylabel = allattrs.ylabel[]
else
ax.ylabel = "log likelihood"
end
# scatterlines!(ax, bc)
# the ylim error doesn't happen if we do this here
vlines!(ax, bc; bc.λ, linestyle=:dash, color=:black)
if haskey(allattrs, :conf_level)
level = allattrs.conf_level[]
lltarget = loglikelihood(bc) - chisqinvcdf(1, level) / 2
if !isnothing(conf_level)
lltarget = loglikelihood(bc) - chisqinvcdf(1, conf_level) / 2
hlines!(ax, lltarget; linestyle=:dash, color=:black)
ci = confint(bc; level)
ci = confint(bc; level=conf_level)
vlines!(ax, ci; linestyle=:dash, color=:black)
text = "$(round(Int, 100 * level))% CI"
text = "$(round(Int, 100 * conf_level))% CI"
text!(ax, first(ci) + 0.05 * abs(first(ci)), lltarget; text)
end

if isnothing(λ)
ci = @something(ci, confint(bc; fast=true))
lower = first(ci) - 0.05 * abs(first(ci))
upper = last(ci) + 0.05 * abs(last(ci))
λ = range(lower, upper; length=n_steps)
end
sort!(collect(λ))

(; X, y) = bc
ll = _loglikelihood_boxcox(X, y, λ)

scatterlines!(ax, λ, ll; attributes...)
vlines!(ax, bc.λ; linestyle=:dash, color=:black)

return plot
end

Makie.plottype(::BoxCoxTransformation) = BCPlot

@setup_workload begin
# Putting some things in `@setup_workload` instead of `@compile_workload` can reduce the size of the
# precompile file and potentially make loading faster.
Expand Down
31 changes: 12 additions & 19 deletions src/BoxCox.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
module BoxCox

using Compat
using DocStringExtensions
using LinearAlgebra
# we use NLopt because that's what MixedModels uses and this was developed
Expand Down Expand Up @@ -140,25 +139,29 @@ end

"""
boxcoxplot(bc::BoxCoxTransformation; kwargs...)
boxcoxplot!(axis, bc::BoxCoxTransformation; λ=nothing, n_steps=21)
boxcoxplot!(axis::Axis, bc::BoxCoxTransformation;
λ=nothing, n_steps=21, xlabel="λ", ylabel="log likelihood",
conf_level=nothing, attributes...)
Create a diagnostic plot for the Box-Cox transformation.
The mutating method for `Axis` returns the (modified) original `Axis`.
The non-mutating method returns a `Figure`.
If λ is `nothing`, the range of possible values for the λ parameter is automatically determined,
with a total of `n_steps`. If `λ` is a vector of numbers, then the λ parameter is evaluated at
each element of that vector.
!!! note
You must load an appropriate Makie backend (e.g., CairoMakie or GLMakie) to actually render a plot.
If `conf_level` is `nothing`, then no confidence interval is displayed.
`attributes` are forwarded to `scatterlines!`.
!!! note
A meaningful plot is only possible when `bc` has not been `empty!`'ed.
!!! compat "Julia 1.6"
The plotting functionality is defined unconditionally.
!!! compat "Julia 1.9"
!!! note
The plotting functionality interface is defined as a package extension and only loaded when Makie is available.
You must load an appropriate Makie backend (e.g., CairoMakie or GLMakie) to actually render a plot.
"""
function boxcoxplot!(::Any, ::PowerTransformation; kwargs...)
# specialize slightly so that they can't just throw Any and get this message
Expand Down Expand Up @@ -227,11 +230,7 @@ If a `LinearMixedModel` is provided, then `X` and `y` are extracted from the mod
The formula interface is only available if StatsModels.jl is loaded either directly or via another package
such GLM.jl or MixedModels.jl.
!!! compat "Julia 1.6"
- The formula interface is defined unconditionally, but `@formula` is not loaded.
- The MixedModels interface is defined unconditionally.
!!! compat "Julia 1.9"
!!! note
- The formula interface is defined as a package extension.
- The MixedModels interface is defined as a package extension.
Expand Down Expand Up @@ -427,12 +426,6 @@ function Base.show(io::IO, t::BoxCoxTransformation)
return nothing
end

if !isdefined(Base, :get_extension)
include("../ext/BoxCoxMakieExt.jl")
include("../ext/BoxCoxMixedModelsExt.jl")
include("../ext/BoxCoxStatsModelsExt.jl")
end

@setup_workload begin
# Putting some things in `@setup_workload` instead of `@compile_workload` can reduce the size of the
# precompile file and potentially make loading faster.
Expand Down
Loading

2 comments on commit d211202

@palday
Copy link
Owner Author

@palday palday commented on d211202 Mar 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator register

Release notes:

While this is technically a breaking release, most users should experience no breakage. Recipe support on Makie 0.20 seems to have some problems, which were foreshadowed in issues like #43.
In order to fix this, the implementation of boxcoxplot was changed to no longer depend on the recipe system. Building on this change, the available methods and precise return types changed slightly, but this should not impact the majority of users.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/103459

Tagging

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.3.0 -m "<description of version>" d211202f716127d68e718f7ec44aa8e2d4d1f5af
git push origin v0.3.0

Please sign in to comment.