diff --git a/Project.toml b/Project.toml index 427e8aa..b49deec 100644 --- a/Project.toml +++ b/Project.toml @@ -7,6 +7,7 @@ authors = ["Eton Tackett ", "Vivak Patel 1 #Drop the first column of the indicator matrix to avoid multicollinearity if drop_first is true and there are multiple levels. + ind = ind[:, 2:end] + end + + for j in 1:size(ind, 2) + push!(cols, Float64.(ind[:, j])) #Convert the boolean indicator columns to Float64 and add them to the list of columns. + end + else + eltype(col) <: Real || + throw(ArgumentError("Column $name must be numeric unless it is listed in cols_to_encode")) + + push!(cols, Float64.(col)) + end + end + + p = length(cols) + X = Matrix{Float64}(undef, n, p) + for j in 1:p + X[:, j] = cols[j] + end + + return Matrix{Float64}(X) + +end +""" + load_csv_dataset(path_or_url; target_col, name="csv_dataset") + +Load a dataset from a CSV file or URL and removes rows with missing values. + +# Arguments +- `path_or_url::String`: Local file path or web URL containing CSV data. + +# Keyword Arguments +- `cols_to_encode=Symbol[]`: Column names or indices in the feature data to one-hot encode. +- `target_col`: Column index or column name containing the response variable. +- `name::String="csv_dataset"`: Dataset name. +- `λ::Real=1.0`: Regularization parameter for ridge regression. + +# Returns +- `Unit`: A unit containing the encoded feature matrix `X`, response vector `y`, + regularization parameter `λ`, and dimensions `n` and `p`. +""" +function load_csv_dataset(path_or_url::String; cols_to_encode=Symbol[], target_col, name::String = "csv_dataset", λ::Real=1.0) + + filepath = + startswith(path_or_url, "http") ? + Downloads.download(path_or_url) : + path_or_url + + df = DataFrame(CSV.File(filepath)) #Read CSV file into a DataFrame. + df = dropmissing(df) #Remove rows with missing values. + Xdf = select(df, DataFrames.Not(target_col)) #Select all columns except the target column for features. + + y = target_col isa Int ? + df[:, target_col] : #If target_col is an integer, use it as a column index to extract the target variable from the DataFrame. + df[:, Symbol(target_col)] #Extract the target variable based on whether target_col is an index or a name. + + + feature_names = names(Xdf) + encode_cols = [c isa Int ? Symbol(names(Xdf)[c]) : Symbol(c) for c in cols_to_encode] + X = one_hot_encode(Xdf; cols_to_encode=encode_cols, drop_first = true) + + + return Unit(name, X, collect(Float64, y), λ) +end diff --git a/test/Project.toml b/test/Project.toml index 73141b0..118565d 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -2,4 +2,9 @@ CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" \ No newline at end of file +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +RidgeRegression = "739161c8-60e1-4c49-8f89-ff30998444b1" + +[compat] +CSV = "0.10" +DataFrames = "1" diff --git a/test/runtests.jl b/test/runtests.jl index dbbe06f..11b4788 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,20 @@ using RidgeRegression using Test +using DataFrames +using LinearAlgebra +using CSV @testset "RidgeRegression.jl" begin - # Write your tests here. + @testset "Dataset Tests" begin + include("src/units/units_dataset_tests.jl") + end + + @testset "One-Hot Encoding Tests" begin + include("src/units/units_encoding_tests.jl") + end + + @testset "Load CSV Dataset Tests" begin + include("src/units/units_load_csv_dataset_tests.jl") + end + end diff --git a/test/src/units/units_dataset_tests.jl b/test/src/units/units_dataset_tests.jl new file mode 100644 index 0000000..d2332c1 --- /dev/null +++ b/test/src/units/units_dataset_tests.jl @@ -0,0 +1,23 @@ +@testset "Unit constructor stores fields correctly" begin + X = [1 2; 3 4] + y = [10, 20] + λ = 0.1 + d = RidgeRegression.Unit("toy", X, y, λ) + + @test "toy" == d.name + @test X == d.X + @test y == d.y + @test λ == d.λ + @test 2 == d.n + @test 2 == d.p + @test (2, 2) == size(d.X) + @test 2 == length(d.y) + @test 1.0 == d.X[1, 1] + @test 20.0 == d.y[2] +end + +@testset "Unit constructor throws error for mismatched dimensions" begin + X = [1 2; 3 4] + λ = 0.1 + @test_throws ArgumentError RidgeRegression.Unit("bad", X, [1, 2, 3], λ) +end diff --git a/test/src/units/units_encoding_tests.jl b/test/src/units/units_encoding_tests.jl new file mode 100644 index 0000000..5b57c35 --- /dev/null +++ b/test/src/units/units_encoding_tests.jl @@ -0,0 +1,40 @@ +@testset "one_hot_encode encodes specified categorical columns and keeps numeric columns" begin + df = DataFrame( + A = ["red", "blue", "red", "green"], + B = [1, 2, 3, 4], + C = ["small", "large", "medium", "small"] + ) + + X = one_hot_encode(df; cols_to_encode=[:A, :C], drop_first=true) + + @test (4, 6) == size(X) + @test all(X[:, 1] .== 1.0) + @test [1.0, 2.0, 3.0, 4.0] == X[:, 4] + @test all(x -> x == 0.0 || x == 1.0, X[:, [2, 3, 5, 6]]) + @test all(vec(sum(X[:, 2:3]; dims=2)) .<= 1) + @test all(vec(sum(X[:, 5:6]; dims=2)) .<= 1) +end + +@testset "one_hot_encode throws error for invalid column specifications" begin + df = DataFrame( + A = ["red", "blue", "red", "green"], + B = [1, 2, 3, 4], + C = ["small", "large", "medium", "small"] + ) + + @test_throws ArgumentError one_hot_encode(df; cols_to_encode=[:A], drop_first=true) +end + +@testset "one_hot_encode supports integer-coded categorical columns when specified" begin + df = DataFrame( + group = [1, 2, 1, 3], + x = [10.0, 20.0, 30.0, 40.0] + ) + + X = one_hot_encode(df; cols_to_encode=[:group], drop_first=true) + + @test (4, 4) == size(X) + @test all(X[:, 1] .== 1.0) + @test [10.0, 20.0, 30.0, 40.0] == X[:, 4] + @test all(x -> x == 0.0 || x == 1.0, X[:, 2:3]) +end \ No newline at end of file diff --git a/test/src/units/units_load_csv_dataset_tests.jl b/test/src/units/units_load_csv_dataset_tests.jl new file mode 100644 index 0000000..208a7dd --- /dev/null +++ b/test/src/units/units_load_csv_dataset_tests.jl @@ -0,0 +1,48 @@ +@testset "load_csv_dataset drops missing rows and uses target column" begin + tmp = tempname() * ".csv" + + df = DataFrame( + a = [1.0, 2.0, missing, 4.0], + b = ["x", "y", "y", "x"], + y = [10.0, 20.0, 30.0, 40.0] + ) + + CSV.write(tmp, df) + + λ = 0.1 + d = load_csv_dataset(tmp; target_col=:y, cols_to_encode=[:b], name="tmp", λ=λ) + + @test "tmp" == d.name + @test λ == d.λ + @test 3 == d.n + @test 3 == d.p + @test 3 == length(d.y) + @test 3 == size(d.X, 1) + @test all(d.X[:, 1] .== 1.0) + @test [10.0, 20.0, 40.0] == d.y + @test (3, 3) == size(d.X) +end + +@testset "load_csv_dataset drops missing rows and uses target column by index" begin + tmp = tempname() * ".csv" + + df = DataFrame( + a = [1.0, 2.0, missing, 4.0], + b = ["x", "y", "y", "x"], + y = [10.0, 20.0, 30.0, 40.0] + ) + + CSV.write(tmp, df) + + λ = 0.5 + d = load_csv_dataset(tmp; target_col=3, cols_to_encode=[:b], name="tmp2", λ=λ) + + @test "tmp2" == d.name + @test λ == d.λ + @test 3 == d.n + @test 3 == d.p + @test all(d.X[:, 1] .== 1.0) + @test [10.0, 20.0, 40.0] == d.y + @test 3 == size(d.X, 1) + @test (3, 3) == size(d.X) +end \ No newline at end of file