Train/test split: private API

PortfolioOptimisers.safe_indexFunction
safe_index(
    lo::Union{Nothing, Number},
    hi::Union{Nothing, Number},
    N::Integer
) -> Tuple{Any, Any}
safe_index(
    lo::Union{Nothing, Number},
    hi::Union{Nothing, Number},
    N::Integer,
    D
) -> Tuple{Any, Any}

Return the (train, test) observation ranges of a holdout split over N time-ordered rows.

Training rows come from the head of the data and test rows from the tail, so the test window is always the most recent one. Each size is a row count (Integer) or a fraction of the observations (AbstractFloat in (0, 1)), resolved by split_count.

  • Neither given: the split falls at D (75 % train, 25 % test).
  • One given: the other side is its complement, so the two windows partition the data.
  • Both given: the head supplies lo training rows, the tail supplies hi test rows, and any rows between them are embargoed — they belong to neither window. This is how a gap between train and test is expressed. The gap is declared by the two sizes and nothing else; a rule that derives one from the label horizon belongs to the purged cross-validators (CombinatorialCrossValidation), not here.

Algorithm

  1. Resolve the two window lengths N_l and N_h, through the branch that lo and hi select:

    1. Neither is given: take n = clamp(floor(Int, D * N), 1, N), then N_l = n and N_h = N - n.
    2. Only lo is given: resolve it with split_count, then N_l = n and N_h = N - n.
    3. Only hi is given: resolve it with split_count, then N_l = N - n and N_h = n.
    4. Both are given: resolve each with split_count on its own. Neither is the complement of the other, so the rows between the two windows are embargoed.
  2. Check that both windows are non-empty, and that the two do not overlap.

  3. Return the two ranges 1:N_l and (N - N_h + 1):N. The training window is the head of the data, and the test window is the tail, so the embargoed rows sit between them.

Arguments

  • lo: Training rows, as a count (Integer) or a fraction (AbstractFloat in (0, 1)); nothing takes the complement of hi.
  • hi: Test rows, likewise; nothing takes the complement of lo.
  • N: Number of observations available.
  • D = 0.75: Training fraction taken when neither size is given.

Validation

  • Both windows are non-empty. A split whose sizes saturate the data on one side (train_size = N) leaves nothing to test on and throws.
  • The windows do not overlap: lo + hi <= N.

Returns

  • (train, test): The training and test row ranges, as two UnitRange{Int}s.

Related

source
PortfolioOptimisers.split_countFunction
split_count(s::Integer, N::Integer, name::Symbol) -> Int64

Resolve one side of a train/test split into a row count.

A size is either an Integer count of observations, or an AbstractFloat fraction of them in (0, 1). Counts saturate at N (asking for more rows than exist takes all of them); the safe_index window guards then reject a split that leaves either side empty.

Algorithm

The method that Julia selects is the algorithm. Each step is one method, and the two mean different things: a count and a fraction.

  1. s is an Integer, so it is a count of rows: check that s > 0, and return min(Int(s), N). A count larger than the data takes every row.
  2. s is an AbstractFloat, so it is a fraction of the rows: check that 0 < s < 1, and return clamp(floor(Int, s * N), 1, N). The fraction rounds down to whole rows, and the clamp keeps a small fraction of a short window from resolving to zero rows.

Arguments

  • s: One side of the split, as a row count (Integer) or a fraction of the observations (AbstractFloat in (0, 1)).
  • N: Number of observations available.
  • name: Symbolic name of the side, displayed in error messages.

Validation

  • s > 0 when s is an Integer.
  • 0 < s < 1 when s is an AbstractFloat.

Returns

  • n::Int: The number of rows the side takes, in 1:N.

Related

source