From 5bf04c55a0b565cb12039b3955c4b832ede53433 Mon Sep 17 00:00:00 2001 From: Damonamajor Date: Wed, 18 Feb 2026 20:59:25 +0000 Subject: [PATCH 01/28] 4 working functions --- R/helpers.R | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++- params.yaml | 3 +++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/R/helpers.R b/R/helpers.R index 8aefbb277..9453f2245 100644 --- a/R/helpers.R +++ b/R/helpers.R @@ -199,13 +199,63 @@ extract_num_iterations <- function(x) { # model: Lightgbm model # leaf_idx: integer matrix [training data x trees] of leaf indices # init_score: mean value of sale prices in the training data +# algorithm: type of algorithm to use. Set in params.yaml. Possible types +# unweighted, unweighted_with_error_reduction, error_reduction, +# and prediction_variance # outcome: Predicted FMV values for each observation in the training data # Returns: # weights: numeric matrix [n_obs x n_trees] where each row sums to 1 -extract_tree_weights <- function(model, leaf_idx, init_score, outcome) { +extract_tree_weights <- function(model, + leaf_idx, + init_score = NULL, + algorithm = "unweighted", + outcome = NULL) { n_obs <- nrow(leaf_idx) n_trees <- ncol(leaf_idx) + # --------------------------------------------------------- + # unweighted (vector with 1/n_trees for each tree) + # --------------------------------------------------------- + if (algorithm == "unweighted") { + weights <- rep(1 / n_trees, n_trees) + + return(weights) + } + + # --------------------------------------------------------- + # prediction_variance: + # vector for tree weights based on variance of + # leaf values across data + # --------------------------------------------------------- + if (algorithm == "prediction_variance") { + tree_dt <- lgb.model.dt.tree(model) + leaf_lookup <- tree_dt[ + !is.na(leaf_index), + c("tree_index", "leaf_index", "leaf_value") + ] + + var_per_tree <- numeric(n_trees) + + for (t in seq_len(n_trees)) { + # LightGBM is 0-indexed + this_tree <- subset(leaf_lookup, tree_index == (t - 1L)) + m <- match(leaf_idx[, t], this_tree$leaf_index) + # incremental outputs for this tree across training rows + incr <- this_tree$leaf_value[m] + var_per_tree[t] <- stats::var(incr, na.rm = TRUE) + } + + var_per_tree[is.na(var_per_tree)] <- 0 + summed_variance <- sum(var_per_tree) + weights <- var_per_tree / summed_variance + + return(weights) + } + + # --------------------------------------------------------- + # Remaining algorithms require tree-based improvements to the predicted values + # --------------------------------------------------------- + init_vec <- rep_len(as.numeric(init_score), n_obs) # Lookup: leaf_index -> leaf_value for each tree @@ -236,11 +286,31 @@ extract_tree_weights <- function(model, leaf_idx, init_score, outcome) { colnames(tree_predictions) <- NULL # Absolute errors vs outcome for each prefix + tree_errors <- abs(outcome - tree_predictions) # Improvement per tree = previous error - next error prev_err <- tree_errors[, 1:n_trees, drop = FALSE] next_err <- tree_errors[, 2:(n_trees + 1L), drop = FALSE] + + # --------------------------------------------------------- + # unweighted_with_error_reduction + # (weights are 1/n_improving trees for trees which reduce errors, 0 otherwise) + # --------------------------------------------------------- + if (algorithm == "unweighted_with_error_reduction") { + improving <- prev_err > next_err + n_improving <- rowSums(improving) + + weights <- improving / n_improving + + return(weights) + } + + # --------------------------------------------------------- + # proportional error reduction: + # weights are proportional to the reduction in error (prev_err - next_err) for + # improving trees, 0 otherwise + # --------------------------------------------------------- diff_in_errors <- pmax(0, prev_err - next_err) dim(diff_in_errors) <- dim(prev_err) diff --git a/params.yaml b/params.yaml index 2b295d26d..76ce3ca85 100644 --- a/params.yaml +++ b/params.yaml @@ -412,6 +412,9 @@ ratio_study: comp: # Number of comps to generate for each PIN/card num_comps: 5 + # Type of comp run to use. Possible types are unweighted, + # unweighted_with_error_reduction, error_reduction, and prediction_variance + algorithm: unweighted # Export ----------------------------------------------------------------------- From 22e38e58cd094e99a634e108c1125e80c1dadd33 Mon Sep 17 00:00:00 2001 From: Michael Wagner Date: Wed, 18 Feb 2026 21:46:50 +0000 Subject: [PATCH 02/28] Update comps docs --- params.yaml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/params.yaml b/params.yaml index 76ce3ca85..5cd8af4e0 100644 --- a/params.yaml +++ b/params.yaml @@ -412,8 +412,17 @@ ratio_study: comp: # Number of comps to generate for each PIN/card num_comps: 5 - # Type of comp run to use. Possible types are unweighted, - # unweighted_with_error_reduction, error_reduction, and prediction_variance + # Algorithm used to weight trees for the comps similarity score. + # Valid options: + # - "unweighted": Equal weight (1/n_trees) for every tree. No + # observation-level variation. Returns a vector of weights. + # - "unweighted_with_error_reduction": Binary 1/0 per tree per observation + # (1 if tree reduces the training sale's prediction error, 0 otherwise), + # then row-normalized. Returns a matrix of weights. + # - "error_reduction": Proportional error reduction for training data sale per + # tree, row-normalized. Returns a matrix of weights. + # - "prediction_variance": Variance of each tree's leaf values across + # training observations, normalized to sum to 1. Returns a vector of weights. algorithm: unweighted # Export ----------------------------------------------------------------------- From 5b655cf2f4fa1d60b7b1e9c21bf0123f6aa25279 Mon Sep 17 00:00:00 2001 From: Michael Wagner Date: Wed, 18 Feb 2026 21:50:11 +0000 Subject: [PATCH 03/28] Add algorithm param for extract_tree_weights --- pipeline/04-interpret.R | 1 + 1 file changed, 1 insertion(+) diff --git a/pipeline/04-interpret.R b/pipeline/04-interpret.R index 9ae810b83..aa65ddce5 100644 --- a/pipeline/04-interpret.R +++ b/pipeline/04-interpret.R @@ -267,6 +267,7 @@ if (comp_enable) { model = lgbm_final_full_fit$fit, leaf_idx = as.matrix(training_leaf_nodes), init_score = mean(training_data$meta_sale_price, na.rm = TRUE), + algorithm = params$comp$algorithm, outcome = training_data$meta_sale_price ) From 6f0ece521e83ae9efa1c8c51716e7f1e756e8189 Mon Sep 17 00:00:00 2001 From: Michael Wagner Date: Wed, 18 Feb 2026 22:00:50 +0000 Subject: [PATCH 04/28] Add input checking --- R/helpers.R | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/R/helpers.R b/R/helpers.R index 9453f2245..b9b76caa1 100644 --- a/R/helpers.R +++ b/R/helpers.R @@ -213,6 +213,19 @@ extract_tree_weights <- function(model, n_obs <- nrow(leaf_idx) n_trees <- ncol(leaf_idx) + # Validate algorithm arg + valid_algorithms <- c( + "unweighted", + "prediction_variance", + "unweighted_with_error_reduction", + "proportional_error_reduction" + ) + + algorithm <- rlang::arg_match( + algorithm, + values = valid_algorithms + ) + # --------------------------------------------------------- # unweighted (vector with 1/n_trees for each tree) # --------------------------------------------------------- From 363b108605f6e2b67a47d04a40651bd27be3857e Mon Sep 17 00:00:00 2001 From: Michael Wagner Date: Wed, 18 Feb 2026 22:06:47 +0000 Subject: [PATCH 05/28] Adjust tree_weights shape checking --- pipeline/04-interpret.R | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/pipeline/04-interpret.R b/pipeline/04-interpret.R index aa65ddce5..a67096990 100644 --- a/pipeline/04-interpret.R +++ b/pipeline/04-interpret.R @@ -274,13 +274,19 @@ if (comp_enable) { if (length(tree_weights) == 0) { message("Warning: tree_weights are empty") } - if (all(rowSums(tree_weights) %in% c(0, 1))) { - message("Warning: tree_weights do not sum to 1 or 0 for each row") - message("First 5 weights:") - print(head(tree_weights, 5)) + if (is.matrix(tree_weights)) { + if (all(rowSums(tree_weights) %in% c(0, 1))) { + message("Warning: tree_weights do not sum to 1 or 0 for each row") + message("First 5 weights:") + print(head(tree_weights, 5)) + } + } else { + message( + "Tree weights are a vector of length ", length(tree_weights), + " (same weights for all training observations)" + ) } - # Make sure that the leaf node tibbles are all integers, which is what # the comps algorithm expects leaf_nodes <- leaf_nodes %>% From 123dd817fc8a013fd9480b77bcdfc0abce131d42 Mon Sep 17 00:00:00 2001 From: Michael Wagner Date: Wed, 18 Feb 2026 22:11:44 +0000 Subject: [PATCH 06/28] Test vector inclusion --- python/comps.py | 164 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 122 insertions(+), 42 deletions(-) diff --git a/python/comps.py b/python/comps.py index aa05148d8..cde640197 100644 --- a/python/comps.py +++ b/python/comps.py @@ -17,11 +17,18 @@ def get_comps( lightgbm leaf node assignments (`observation_df`) compared to a second dataframe of leaf node assignments (`comparison_df`). - Leaf nodes are weighted according to a tree importance matrix `weights` - and used to generate a similarity score. The function returns two - dataframes: One containing the indices of the most similar compararables + Leaf nodes are weighted according to a tree importance vector or matrix + `weights` and used to generate a similarity score. The function returns two + dataframes: One containing the indices of the most similar comparables and the other containing their corresponding similarity scores. + Weights can be: + - A 1-D array of shape (n_trees,) for algorithms that produce a single + weight per tree (e.g. "unweighted", "prediction_variance"). + - A 2-D matrix of shape (n_training_obs, n_trees) for algorithms that + produce per-observation weights (e.g. "error_reduction", + "unweighted_with_error_reduction"). + More details on the underlying algorithm can be found here: https://ccao-data.github.io/lightsnip/articles/finding-comps.html @@ -33,7 +40,7 @@ def get_comps( comparables. weights (numpy.ndarray): Importance weights for leaf nodes, used to compute similarity - scores. + scores. Either 1-D (n_trees,) or 2-D (n_comparisons, n_trees). num_comps (int, optional): Number of top comparables to return for each observation. Default is 5. @@ -59,16 +66,38 @@ def get_comps( f"({observation_df.shape[1]}) " f"must match `comparison_df` ({comparison_df.shape[1]})" ) - if comparison_df.shape != weights.shape: + + if not isinstance(weights, np.ndarray): + weights = np.asarray(weights) + + # Determine whether weights are per-tree (1-D vector) or + # per-observation (2-D matrix) and validate accordingly + if weights.ndim == 1: + # Vector weights: one weight per tree, shared across all comparisons. + # Used by "unweighted" and "prediction_variance" algorithms. + if weights.shape[0] != comparison_df.shape[1]: + raise ValueError( + f"`weights` length {weights.shape[0]} must equal number of " + f"trees {comparison_df.shape[1]}" + ) + weights_arr = weights.astype(np.float32, copy=False) + use_matrix_weights = False + elif weights.ndim == 2: + # Matrix weights: one weight per comparison observation per tree. + # Used by "error_reduction" and "unweighted_with_error_reduction". + if comparison_df.shape != weights.shape: + raise ValueError( + f"`comparison_df.shape` {comparison_df.shape} must match " + f"`weights.shape` {weights.shape}" + ) + weights_arr = weights.astype(np.float32, copy=False) + use_matrix_weights = True + else: raise ValueError( - f"`comparison_df.shape` {comparison_df.shape} must match " - f"`weights.shape` {weights.shape}" + "`weights` must be a 1-D vector (n_trees,) or 2-D matrix " + f"(n_comparisons, n_trees), got {weights.ndim}-D" ) - # Convert the weights to a numpy array so that we can take advantage of - # numba acceleration later on - weights_matrix = np.asarray(weights, dtype=np.float32) - # Chunk the observations so that the script can periodically report progress observation_df["chunk"] = pd.cut( observation_df.index, bins=num_chunks, labels=False @@ -98,10 +127,22 @@ def get_comps( flush=True, ) - # Compute comps for each observation - comp_ids, comp_scores = _get_top_n_comps( - observation_matrix, possible_comp_matrix, weights_matrix, num_comps - ) + # Compute comps for each observation, dispatching to the appropriate + # numba kernel based on the shape of the weights + if use_matrix_weights: + comp_ids, comp_scores = _get_top_n_comps_matrix_weights( + observation_matrix, + possible_comp_matrix, + weights_arr, + num_comps, + ) + else: + comp_ids, comp_scores = _get_top_n_comps_vector_weights( + observation_matrix, + possible_comp_matrix, + weights_arr, + num_comps, + ) observation_ids = observations.index.values for obs_idx, comp_idx, comp_score in zip( @@ -127,46 +168,77 @@ def get_comps( @nb.njit(fastmath=True, parallel=True) -def _get_top_n_comps( +def _get_top_n_comps_vector_weights( + leaf_node_matrix: np.ndarray, + comparison_leaf_node_matrix: np.ndarray, + weights_vector: np.ndarray, + num_comps: int, +) -> typing.Tuple[np.ndarray, np.ndarray]: + """Compute top-N comps using a shared per-tree weight vector. + + Used by algorithms like "unweighted" and "prediction_variance" where + the weight for a tree is the same regardless of which training + observation is being compared.""" + num_observations = len(leaf_node_matrix) + num_possible_comparisons = len(comparison_leaf_node_matrix) + idx_dtype = np.int32 + score_dtype = np.float32 + + all_top_n_idxs = np.full((num_observations, num_comps), -1, dtype=idx_dtype) + all_top_n_scores = np.zeros((num_observations, num_comps), dtype=score_dtype) + + for x_i in nb.prange(num_observations): + for y_i in range(num_possible_comparisons): + similarity_score = 0.0 + for tree_idx in range(leaf_node_matrix.shape[1]): + if ( + leaf_node_matrix[x_i, tree_idx] + == comparison_leaf_node_matrix[y_i, tree_idx] + ): + similarity_score += weights_vector[tree_idx] + + if similarity_score > all_top_n_scores[x_i][-1]: + for idx, score in enumerate(all_top_n_scores[x_i]): + if similarity_score > score: + insert_at_idx_and_shift(all_top_n_idxs[x_i], y_i, idx) + insert_at_idx_and_shift( + all_top_n_scores[x_i], similarity_score, idx + ) + break + + return all_top_n_idxs, all_top_n_scores + + +@nb.njit(fastmath=True, parallel=True) +def _get_top_n_comps_matrix_weights( leaf_node_matrix: np.ndarray, comparison_leaf_node_matrix: np.ndarray, weights_matrix: np.ndarray, num_comps: int, ) -> typing.Tuple[np.ndarray, np.ndarray]: - """Helper function that takes matrices of leaf node assignments for - observations in a tree model, a matrix of weights for each obs/tree, and an - integer `num_comps`, and returns a matrix where each observation is scored - by similarity to observations in the comparison matrix and the top N scores - are returned along with the indexes of the comparison observations.""" + """Compute top-N comps using a per-observation weight matrix. + + Used by algorithms like "error_reduction" and + "unweighted_with_error_reduction" where the weight for a tree + depends on which training observation (y_i) is being compared.""" num_observations = len(leaf_node_matrix) num_possible_comparisons = len(comparison_leaf_node_matrix) idx_dtype = np.int32 score_dtype = np.float32 - # Store scores and indexes in two separate arrays rather than a 3d matrix - # for simplicity (array of tuples does not convert to pandas properly). - # Indexes default to -1, which is an impossible index and so is a signal - # that no comp was found all_top_n_idxs = np.full((num_observations, num_comps), -1, dtype=idx_dtype) all_top_n_scores = np.zeros((num_observations, num_comps), dtype=score_dtype) for x_i in nb.prange(num_observations): - # TODO: We could probably speed this up by skipping comparisons we've - # already made; we just need to do it in a way that will have a - # low memory footprint for y_i in range(num_possible_comparisons): similarity_score = 0.0 - for tree_idx in range(len(leaf_node_matrix[x_i])): + for tree_idx in range(leaf_node_matrix.shape[1]): if ( - leaf_node_matrix[x_i][tree_idx] - == comparison_leaf_node_matrix[y_i][tree_idx] + leaf_node_matrix[x_i, tree_idx] + == comparison_leaf_node_matrix[y_i, tree_idx] ): - similarity_score += weights_matrix[y_i][tree_idx] + similarity_score += weights_matrix[y_i, tree_idx] - # See if the score is higher than any of the top N - # comps, and store it in the sorted comps array if it is. - # First check if the score is higher than the lowest score, - # since otherwise we don't need to bother iterating the scores if similarity_score > all_top_n_scores[x_i][-1]: for idx, score in enumerate(all_top_n_scores[x_i]): if similarity_score > score: @@ -198,18 +270,26 @@ def insert_at_idx_and_shift( num_trees = 500 num_obs = 20001 num_comparisons = 10000 - mean_sale_price = 350000 - std_deviation = 110000 - leaf_nodes = pd.DataFrame(np.random.randint(0, num_obs, size=[num_obs, num_trees])) + leaf_nodes = pd.DataFrame( + np.random.randint(0, num_obs, size=[num_obs, num_trees]) + ) training_leaf_nodes = pd.DataFrame( np.random.randint(0, num_comparisons, size=[num_comparisons, num_trees]) ) - tree_weights = np.asarray( + + # Test with matrix weights (error_reduction style) + tree_weights_matrix = np.asarray( [np.random.dirichlet(np.ones(num_trees)) for _ in range(num_comparisons)] ) + start = time.time() + get_comps(leaf_nodes, training_leaf_nodes, tree_weights_matrix) + end = time.time() + print(f"get_comps (matrix weights) runtime: {end - start}s") + # Test with vector weights (unweighted / prediction_variance style) + tree_weights_vector = np.random.dirichlet(np.ones(num_trees)) start = time.time() - get_comps(leaf_nodes, training_leaf_nodes, tree_weights) + get_comps(leaf_nodes, training_leaf_nodes, tree_weights_vector) end = time.time() - print(f"get_comps runtime: {end - start}s") + print(f"get_comps (vector weights) runtime: {end - start}s") \ No newline at end of file From 4c0d8cceb2e01d1f793f0bec76215dc71b14b0f3 Mon Sep 17 00:00:00 2001 From: Michael Wagner Date: Wed, 18 Feb 2026 22:19:30 +0000 Subject: [PATCH 07/28] Attempt using only 1 get_top_comps function --- python/comps.py | 116 +++++++++++++++++------------------------------- 1 file changed, 40 insertions(+), 76 deletions(-) diff --git a/python/comps.py b/python/comps.py index cde640197..9995f3da8 100644 --- a/python/comps.py +++ b/python/comps.py @@ -24,7 +24,8 @@ def get_comps( Weights can be: - A 1-D array of shape (n_trees,) for algorithms that produce a single - weight per tree (e.g. "unweighted", "prediction_variance"). + weight per tree (e.g. "unweighted", "prediction_variance"). Will be + reshaped to (1, n_trees) before being passed to the numba kernel. - A 2-D matrix of shape (n_training_obs, n_trees) for algorithms that produce per-observation weights (e.g. "error_reduction", "unweighted_with_error_reduction"). @@ -70,28 +71,24 @@ def get_comps( if not isinstance(weights, np.ndarray): weights = np.asarray(weights) - # Determine whether weights are per-tree (1-D vector) or - # per-observation (2-D matrix) and validate accordingly + # Normalize weights to a 2-D matrix so that we can use a single numba + # kernel for both vector and matrix weights. A 1-D vector of per-tree + # weights is reshaped to (1, n_trees); the numba kernel detects this + # shape and broadcasts the single row to all comparison observations. if weights.ndim == 1: - # Vector weights: one weight per tree, shared across all comparisons. - # Used by "unweighted" and "prediction_variance" algorithms. if weights.shape[0] != comparison_df.shape[1]: raise ValueError( f"`weights` length {weights.shape[0]} must equal number of " f"trees {comparison_df.shape[1]}" ) - weights_arr = weights.astype(np.float32, copy=False) - use_matrix_weights = False + weights_matrix = weights.reshape(1, -1).astype(np.float32, copy=False) elif weights.ndim == 2: - # Matrix weights: one weight per comparison observation per tree. - # Used by "error_reduction" and "unweighted_with_error_reduction". if comparison_df.shape != weights.shape: raise ValueError( f"`comparison_df.shape` {comparison_df.shape} must match " f"`weights.shape` {weights.shape}" ) - weights_arr = weights.astype(np.float32, copy=False) - use_matrix_weights = True + weights_matrix = weights.astype(np.float32, copy=False) else: raise ValueError( "`weights` must be a 1-D vector (n_trees,) or 2-D matrix " @@ -127,22 +124,10 @@ def get_comps( flush=True, ) - # Compute comps for each observation, dispatching to the appropriate - # numba kernel based on the shape of the weights - if use_matrix_weights: - comp_ids, comp_scores = _get_top_n_comps_matrix_weights( - observation_matrix, - possible_comp_matrix, - weights_arr, - num_comps, - ) - else: - comp_ids, comp_scores = _get_top_n_comps_vector_weights( - observation_matrix, - possible_comp_matrix, - weights_arr, - num_comps, - ) + # Compute comps for each observation + comp_ids, comp_scores = _get_top_n_comps( + observation_matrix, possible_comp_matrix, weights_matrix, num_comps + ) observation_ids = observations.index.values for obs_idx, comp_idx, comp_score in zip( @@ -168,77 +153,56 @@ def get_comps( @nb.njit(fastmath=True, parallel=True) -def _get_top_n_comps_vector_weights( - leaf_node_matrix: np.ndarray, - comparison_leaf_node_matrix: np.ndarray, - weights_vector: np.ndarray, - num_comps: int, -) -> typing.Tuple[np.ndarray, np.ndarray]: - """Compute top-N comps using a shared per-tree weight vector. - - Used by algorithms like "unweighted" and "prediction_variance" where - the weight for a tree is the same regardless of which training - observation is being compared.""" - num_observations = len(leaf_node_matrix) - num_possible_comparisons = len(comparison_leaf_node_matrix) - idx_dtype = np.int32 - score_dtype = np.float32 - - all_top_n_idxs = np.full((num_observations, num_comps), -1, dtype=idx_dtype) - all_top_n_scores = np.zeros((num_observations, num_comps), dtype=score_dtype) - - for x_i in nb.prange(num_observations): - for y_i in range(num_possible_comparisons): - similarity_score = 0.0 - for tree_idx in range(leaf_node_matrix.shape[1]): - if ( - leaf_node_matrix[x_i, tree_idx] - == comparison_leaf_node_matrix[y_i, tree_idx] - ): - similarity_score += weights_vector[tree_idx] - - if similarity_score > all_top_n_scores[x_i][-1]: - for idx, score in enumerate(all_top_n_scores[x_i]): - if similarity_score > score: - insert_at_idx_and_shift(all_top_n_idxs[x_i], y_i, idx) - insert_at_idx_and_shift( - all_top_n_scores[x_i], similarity_score, idx - ) - break - - return all_top_n_idxs, all_top_n_scores - - -@nb.njit(fastmath=True, parallel=True) -def _get_top_n_comps_matrix_weights( +def _get_top_n_comps( leaf_node_matrix: np.ndarray, comparison_leaf_node_matrix: np.ndarray, weights_matrix: np.ndarray, num_comps: int, ) -> typing.Tuple[np.ndarray, np.ndarray]: - """Compute top-N comps using a per-observation weight matrix. - - Used by algorithms like "error_reduction" and - "unweighted_with_error_reduction" where the weight for a tree - depends on which training observation (y_i) is being compared.""" + """Helper function that takes matrices of leaf node assignments for + observations in a tree model, a matrix of weights for each obs/tree, and an + integer `num_comps`, and returns a matrix where each observation is scored + by similarity to observations in the comparison matrix and the top N scores + are returned along with the indexes of the comparison observations. + + The weights_matrix is always 2-D. If its first dimension is 1, the single + row of weights is broadcast to all comparison observations (i.e. tree-level + weights shared across all comparisons). Otherwise, each comparison + observation y_i uses its own row of weights.""" num_observations = len(leaf_node_matrix) num_possible_comparisons = len(comparison_leaf_node_matrix) idx_dtype = np.int32 score_dtype = np.float32 + # Detect whether we have shared (vector-style) or per-observation weights + shared_weights = weights_matrix.shape[0] == 1 + + # Store scores and indexes in two separate arrays rather than a 3d matrix + # for simplicity (array of tuples does not convert to pandas properly). + # Indexes default to -1, which is an impossible index and so is a signal + # that no comp was found all_top_n_idxs = np.full((num_observations, num_comps), -1, dtype=idx_dtype) all_top_n_scores = np.zeros((num_observations, num_comps), dtype=score_dtype) for x_i in nb.prange(num_observations): + # TODO: We could probably speed this up by skipping comparisons we've + # already made; we just need to do it in a way that will have a + # low memory footprint for y_i in range(num_possible_comparisons): similarity_score = 0.0 + # Use row 0 for shared weights, row y_i for per-obs weights + w_i = 0 if shared_weights else y_i for tree_idx in range(leaf_node_matrix.shape[1]): if ( leaf_node_matrix[x_i, tree_idx] == comparison_leaf_node_matrix[y_i, tree_idx] ): - similarity_score += weights_matrix[y_i, tree_idx] + similarity_score += weights_matrix[w_i, tree_idx] + # See if the score is higher than any of the top N + # comps, and store it in the sorted comps array if it is. + # First check if the score is higher than the lowest score, + # since otherwise we don't need to bother iterating the scores if similarity_score > all_top_n_scores[x_i][-1]: for idx, score in enumerate(all_top_n_scores[x_i]): if similarity_score > score: From b52fbd88dd76b869d5105ea010bee7cdf89d6b1a Mon Sep 17 00:00:00 2001 From: Michael Wagner Date: Fri, 20 Feb 2026 17:24:01 +0000 Subject: [PATCH 08/28] Fix message --- pipeline/04-interpret.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pipeline/04-interpret.R b/pipeline/04-interpret.R index a67096990..dfb57366f 100644 --- a/pipeline/04-interpret.R +++ b/pipeline/04-interpret.R @@ -26,7 +26,7 @@ purrr::walk(list.files("R/", "\\.R$", full.names = TRUE), source) -#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +?#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # 2. Load Data ----------------------------------------------------------------- #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - message("Loading model fit and recipe") @@ -276,7 +276,7 @@ if (comp_enable) { } if (is.matrix(tree_weights)) { if (all(rowSums(tree_weights) %in% c(0, 1))) { - message("Warning: tree_weights do not sum to 1 or 0 for each row") + message("Warning: tree_weights do sum to 1 or 0 for each row") message("First 5 weights:") print(head(tree_weights, 5)) } From 1923e195a20af4beaf2c23c3cd2671731d9d05cf Mon Sep 17 00:00:00 2001 From: Michael Wagner Date: Fri, 20 Feb 2026 17:29:02 +0000 Subject: [PATCH 09/28] Switch boolean check --- pipeline/04-interpret.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pipeline/04-interpret.R b/pipeline/04-interpret.R index dfb57366f..4465b1c5b 100644 --- a/pipeline/04-interpret.R +++ b/pipeline/04-interpret.R @@ -275,8 +275,8 @@ if (comp_enable) { message("Warning: tree_weights are empty") } if (is.matrix(tree_weights)) { - if (all(rowSums(tree_weights) %in% c(0, 1))) { - message("Warning: tree_weights do sum to 1 or 0 for each row") + if (!all(rowSums(tree_weights) %in% c(0, 1))) { + message("Warning: tree_weights do not sum to 1 or 0 for each row") message("First 5 weights:") print(head(tree_weights, 5)) } From 972772ceec3040a6dd2e4724e2af807b3405a16e Mon Sep 17 00:00:00 2001 From: Michael Wagner Date: Fri, 20 Feb 2026 19:58:58 +0000 Subject: [PATCH 10/28] Style --- pipeline/04-interpret.R | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/pipeline/04-interpret.R b/pipeline/04-interpret.R index 4465b1c5b..e09b7c026 100644 --- a/pipeline/04-interpret.R +++ b/pipeline/04-interpret.R @@ -24,9 +24,7 @@ reticulate::py_require( purrr::walk(list.files("R/", "\\.R$", full.names = TRUE), source) - - -?#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +? #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # 2. Load Data ----------------------------------------------------------------- #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - message("Loading model fit and recipe") @@ -84,8 +82,6 @@ if (comp_enable) { } - - #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # 3. Calculate SHAP Values ----------------------------------------------------- #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -151,8 +147,6 @@ if (shap_enable) { } - - #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # 4. Calculate Feature Importance ---------------------------------------------- #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -172,8 +166,6 @@ lightgbm::lgb.importance(lgbm_final_full_fit$fit) %>% write_parquet(paths$output$feature_importance$local) - - #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # 5. Find Comparables --------------------------------------------------------- #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 1f6bf1943021618cd8ba2a689de1100fd7a4ab9f Mon Sep 17 00:00:00 2001 From: Michael Wagner Date: Fri, 20 Feb 2026 20:03:49 +0000 Subject: [PATCH 11/28] Format --- python/comps.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/python/comps.py b/python/comps.py index 9995f3da8..34b508741 100644 --- a/python/comps.py +++ b/python/comps.py @@ -25,10 +25,10 @@ def get_comps( Weights can be: - A 1-D array of shape (n_trees,) for algorithms that produce a single weight per tree (e.g. "unweighted", "prediction_variance"). Will be - reshaped to (1, n_trees) before being passed to the numba kernel. + reshaped to (1, n_trees) before being passed to numba - A 2-D matrix of shape (n_training_obs, n_trees) for algorithms that produce per-observation weights (e.g. "error_reduction", - "unweighted_with_error_reduction"). + "unweighted_with_error_reduction") More details on the underlying algorithm can be found here: https://ccao-data.github.io/lightsnip/articles/finding-comps.html @@ -239,7 +239,11 @@ def insert_at_idx_and_shift( np.random.randint(0, num_obs, size=[num_obs, num_trees]) ) training_leaf_nodes = pd.DataFrame( - np.random.randint(0, num_comparisons, size=[num_comparisons, num_trees]) + np.random.randint( + 0, + num_comparisons, + size=[num_comparisons, num_trees] + ) ) # Test with matrix weights (error_reduction style) @@ -256,4 +260,4 @@ def insert_at_idx_and_shift( start = time.time() get_comps(leaf_nodes, training_leaf_nodes, tree_weights_vector) end = time.time() - print(f"get_comps (vector weights) runtime: {end - start}s") \ No newline at end of file + print(f"get_comps (vector weights) runtime: {end - start}s") From 365dee4d8b2ebb2d0193e6a08dd6a577b68deb9d Mon Sep 17 00:00:00 2001 From: Michael Wagner Date: Fri, 20 Feb 2026 20:34:36 +0000 Subject: [PATCH 12/28] Lint --- python/comps.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/python/comps.py b/python/comps.py index 34b508741..5bd972b00 100644 --- a/python/comps.py +++ b/python/comps.py @@ -239,11 +239,7 @@ def insert_at_idx_and_shift( np.random.randint(0, num_obs, size=[num_obs, num_trees]) ) training_leaf_nodes = pd.DataFrame( - np.random.randint( - 0, - num_comparisons, - size=[num_comparisons, num_trees] - ) + np.random.randint(0, num_comparisons, size=[num_comparisons, num_trees]) ) # Test with matrix weights (error_reduction style) From fa8ce67356bb368a5f8f21319b61131a280d3fb0 Mon Sep 17 00:00:00 2001 From: Michael Wagner Date: Fri, 20 Feb 2026 20:39:29 +0000 Subject: [PATCH 13/28] Lint --- pipeline/04-interpret.R | 4 +++- python/comps.py | 6 ++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pipeline/04-interpret.R b/pipeline/04-interpret.R index e09b7c026..148603878 100644 --- a/pipeline/04-interpret.R +++ b/pipeline/04-interpret.R @@ -24,7 +24,7 @@ reticulate::py_require( purrr::walk(list.files("R/", "\\.R$", full.names = TRUE), source) -? #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # 2. Load Data ----------------------------------------------------------------- #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - message("Loading model fit and recipe") @@ -82,6 +82,8 @@ if (comp_enable) { } + + #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # 3. Calculate SHAP Values ----------------------------------------------------- #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/python/comps.py b/python/comps.py index 5bd972b00..20eefd515 100644 --- a/python/comps.py +++ b/python/comps.py @@ -235,11 +235,9 @@ def insert_at_idx_and_shift( num_obs = 20001 num_comparisons = 10000 - leaf_nodes = pd.DataFrame( - np.random.randint(0, num_obs, size=[num_obs, num_trees]) - ) + leaf_nodes = pd.DataFrame(np.random.randint(0, num_obs, size=[num_obs, num_trees])) training_leaf_nodes = pd.DataFrame( - np.random.randint(0, num_comparisons, size=[num_comparisons, num_trees]) + np.random.randint(0, num_comparisons, size=[num_comparisons, num_trees]) ) # Test with matrix weights (error_reduction style) From d32eb89bdaac80c0d92d7c77ea069e84a218d4bb Mon Sep 17 00:00:00 2001 From: wagnerlmichael <93889413+wagnerlmichael@users.noreply.github.com> Date: Mon, 23 Feb 2026 09:01:17 -0600 Subject: [PATCH 14/28] Update R/helpers.R Co-authored-by: Jean Cochrane --- R/helpers.R | 1 - 1 file changed, 1 deletion(-) diff --git a/R/helpers.R b/R/helpers.R index b9b76caa1..8774f6676 100644 --- a/R/helpers.R +++ b/R/helpers.R @@ -299,7 +299,6 @@ extract_tree_weights <- function(model, colnames(tree_predictions) <- NULL # Absolute errors vs outcome for each prefix - tree_errors <- abs(outcome - tree_predictions) # Improvement per tree = previous error - next error From 441c4cded9d7b8cbfdab06ae74c3744e0861b5c6 Mon Sep 17 00:00:00 2001 From: wagnerlmichael <93889413+wagnerlmichael@users.noreply.github.com> Date: Mon, 23 Feb 2026 09:01:50 -0600 Subject: [PATCH 15/28] Update R/helpers.R Co-authored-by: Jean Cochrane --- R/helpers.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/helpers.R b/R/helpers.R index 8774f6676..7ef7c9f0b 100644 --- a/R/helpers.R +++ b/R/helpers.R @@ -218,7 +218,7 @@ extract_tree_weights <- function(model, "unweighted", "prediction_variance", "unweighted_with_error_reduction", - "proportional_error_reduction" + "error_reduction" ) algorithm <- rlang::arg_match( From 5ca1ed7df4021477fbd79c003ee09f295249b522 Mon Sep 17 00:00:00 2001 From: wagnerlmichael <93889413+wagnerlmichael@users.noreply.github.com> Date: Mon, 23 Feb 2026 11:47:22 -0600 Subject: [PATCH 16/28] Update R/helpers.R Co-authored-by: Jean Cochrane --- R/helpers.R | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/R/helpers.R b/R/helpers.R index 7ef7c9f0b..ee810c00b 100644 --- a/R/helpers.R +++ b/R/helpers.R @@ -221,10 +221,12 @@ extract_tree_weights <- function(model, "error_reduction" ) - algorithm <- rlang::arg_match( - algorithm, - values = valid_algorithms - ) + if (!algorithm %in% valid_algorithms) { + stop( + "Invalid algorithm '", algorithm, "'. Must be one of: ", + paste0(valid_algorithms, collapse = ", ") + ) + } # --------------------------------------------------------- # unweighted (vector with 1/n_trees for each tree) From 1e32a952d62c7a755f2cef6c6edb18ab01b973d4 Mon Sep 17 00:00:00 2001 From: Michael Wagner Date: Mon, 23 Feb 2026 17:55:05 +0000 Subject: [PATCH 17/28] Update tests to accomdate vector weight support --- python/tests/test_comps.py | 126 ++++++++++++++++++++++++++++++++++++- 1 file changed, 125 insertions(+), 1 deletion(-) diff --git a/python/tests/test_comps.py b/python/tests/test_comps.py index fc61e1ef1..17f239cce 100644 --- a/python/tests/test_comps.py +++ b/python/tests/test_comps.py @@ -8,6 +8,8 @@ @pt.mark.parametrize( "leaf_nodes,training_leaf_nodes,tree_weights,num_comps,num_chunks,expected_comps,expected_scores", [ + # Matrix weights tests: + # - - - - - - - - - - - # Simple example that tests one input observation with uniform weights # to make sure that the algorithm correctly prioritizes comparison # observations with the highest number of leaf node matches @@ -203,6 +205,118 @@ ), id="more_possible_comps_than_num_comps", ), + # Vector weights tests: + # - - - - - - - - - - - + # Test one observation with uniform 1-D weights (vector). With a 1-D + # weight vector, every tree gets the same weight and that weight is + # broadcast identically to every comparison observation. This should + # produce the same results as the matrix case with identical rows. + pt.param( + pd.DataFrame([[1, 1, 1]]), + pd.DataFrame([[1, 1, 0], [1, 0, 0], [0, 0, 0]]), + np.array([0.333, 0.333, 0.333]), + 3, + 1, + pd.DataFrame( + { + "comp_idx_1": [0], + "comp_idx_2": [1], + "comp_idx_3": [-1], + }, + dtype=np.int32, + ), + pd.DataFrame( + { + "comp_score_1": [0.333 * 2], + "comp_score_2": [0.333 * 1], + "comp_score_3": [0.333 * 0], + }, + dtype=np.float32, + ), + id="one_observation_1d_weights", + ), + # Test two observations with uniform 1-D weights. The 1-D vector is + # used for all comparison rows, so every comparison uses the same + # per-tree weight. Results should mirror the 2-D uniform-weight case. + pt.param( + pd.DataFrame([[1, 1, 1], [2, 2, 2]]), + pd.DataFrame([[1, 1, 1], [1, 1, 2], [1, 2, 2], [2, 2, 2]]), + np.array([0.333, 0.333, 0.333]), + 4, + 1, + pd.DataFrame( + { + "comp_idx_1": [0, 3], + "comp_idx_2": [1, 2], + "comp_idx_3": [2, 1], + "comp_idx_4": [-1, -1], + }, + dtype=np.int32, + ), + pd.DataFrame( + { + "comp_score_1": [0.333 * 3] * 2, + "comp_score_2": [0.333 * 2] * 2, + "comp_score_3": [0.333 * 1] * 2, + "comp_score_4": [0.333 * 0] * 2, + }, + dtype=np.float32, + ), + id="two_observations_1d_weights", + ), + # Test 1-D weights with non-uniform per-tree weights. With weights + pt.param( + pd.DataFrame([[1, 1, 1]]), + pd.DataFrame([[1, 1, 0], [1, 0, 0], [0, 0, 0]]), + np.array([0.5, 0.3, 0.2]), + 3, + 1, + pd.DataFrame( + {"comp_idx_1": [0], "comp_idx_2": [1], "comp_idx_3": [-1]}, + dtype=np.int32, + ), + pd.DataFrame( + { + "comp_score_1": [0.5 + 0.3], + "comp_score_2": [0.5], + "comp_score_3": [0.0], + }, + dtype=np.float32, + ), + id="non_uniform_1d_weights", + ), + # Test that 1-D vector weights produce the same result as an + # equivalent 2-D matrix where every row is the same weight vector. + # This confirms the logic is correct: a (3,) vector + # reshaped to (1, 3) should behave identically to a (3, 3) matrix + # with all rows equal to [0.5, 0.3, 0.2]. + pt.param( + pd.DataFrame([[1, 1, 1]]), + pd.DataFrame([[1, 1, 0], [1, 1, 0], [1, 1, 0]]), + np.array([0.5, 0.3, 0.2]), + 3, + 1, + pd.DataFrame( + { + # All comparison obs match on trees 1 & 2 with the same + # weight, so they all tie and tiebreak by index order + "comp_idx_1": [0], + "comp_idx_2": [1], + "comp_idx_3": [2], + }, + dtype=np.int32, + ), + pd.DataFrame( + { + # All comparison obs get the same score: 0.5 + 0.3 = 0.8 + "comp_score_1": [0.5 + 0.3], + "comp_score_2": [0.5 + 0.3], + "comp_score_3": [0.5 + 0.3], + }, + dtype=np.float32, + ), + id="1d_weights_tiebreak_same_as_uniform_matrix", + ), ], ) def test_get_comps( @@ -244,6 +358,16 @@ def test_get_comps( "`comparison_df.shape` (3, 3) must match `weights.shape` (2, 2)", id="comparison_weights_shapes_match", ), + # Vector weights validation tests: + # 1-D weights with wrong length: 2 weights for 3 trees + pt.param( + pd.DataFrame([[1, 1, 1]]), + pd.DataFrame([[1, 1, 1], [2, 2, 2], [3, 3, 3]]), + np.array([0.333, 0.333]), + ValueError, + "`weights` length 2 must equal number of trees 3", + id="1d_weights_wrong_length", + ), ], ) def test_get_comps_raises_on_invalid_inputs( @@ -271,4 +395,4 @@ def test_get_comps_raises_on_invalid_inputs( ) def test_insert_at_idx_and_shift(arr, elem, idx, expected): result = comps_module.insert_at_idx_and_shift(np.array(arr), elem, idx) - np.testing.assert_array_equal(result, np.array(expected)) + np.testing.assert_array_equal(result, np.array(expected)) \ No newline at end of file From 899450c6ab6a002277b656fa89a5e14e8580bcf7 Mon Sep 17 00:00:00 2001 From: Michael Wagner Date: Mon, 23 Feb 2026 21:06:06 +0000 Subject: [PATCH 18/28] Remove redundant test --- python/tests/test_comps.py | 35 ++--------------------------------- 1 file changed, 2 insertions(+), 33 deletions(-) diff --git a/python/tests/test_comps.py b/python/tests/test_comps.py index 17f239cce..faeb8802a 100644 --- a/python/tests/test_comps.py +++ b/python/tests/test_comps.py @@ -264,7 +264,7 @@ ), id="two_observations_1d_weights", ), - # Test 1-D weights with non-uniform per-tree weights. With weights + # Test 1-D weights with non-uniform per-tree weights. pt.param( pd.DataFrame([[1, 1, 1]]), pd.DataFrame([[1, 1, 0], [1, 0, 0], [0, 0, 0]]), @@ -285,38 +285,6 @@ ), id="non_uniform_1d_weights", ), - # Test that 1-D vector weights produce the same result as an - # equivalent 2-D matrix where every row is the same weight vector. - # This confirms the logic is correct: a (3,) vector - # reshaped to (1, 3) should behave identically to a (3, 3) matrix - # with all rows equal to [0.5, 0.3, 0.2]. - pt.param( - pd.DataFrame([[1, 1, 1]]), - pd.DataFrame([[1, 1, 0], [1, 1, 0], [1, 1, 0]]), - np.array([0.5, 0.3, 0.2]), - 3, - 1, - pd.DataFrame( - { - # All comparison obs match on trees 1 & 2 with the same - # weight, so they all tie and tiebreak by index order - "comp_idx_1": [0], - "comp_idx_2": [1], - "comp_idx_3": [2], - }, - dtype=np.int32, - ), - pd.DataFrame( - { - # All comparison obs get the same score: 0.5 + 0.3 = 0.8 - "comp_score_1": [0.5 + 0.3], - "comp_score_2": [0.5 + 0.3], - "comp_score_3": [0.5 + 0.3], - }, - dtype=np.float32, - ), - id="1d_weights_tiebreak_same_as_uniform_matrix", - ), ], ) def test_get_comps( @@ -352,6 +320,7 @@ def test_get_comps( ), pt.param( pd.DataFrame([[1, 1, 1]]), + pd.DataFrame([[1, 1, 1], [2, 2, 2], [3, 3, 3]]), np.array([0.333] * 4).reshape(2, 2), ValueError, From 4e269fc0ccba5c3759bad63fede001af10df83bc Mon Sep 17 00:00:00 2001 From: Jean Cochrane Date: Mon, 23 Feb 2026 15:20:09 -0600 Subject: [PATCH 19/28] Fix failing Python test infrastructure on CI --- .github/workflows/test.yaml | 9 +- .gitignore | 3 - pipeline/04-interpret.R | 6 +- python/.gitignore | 7 +- python/pyproject.toml | 13 ++ python/uv.lock | 382 ++++++++++++++++++++++++++++++++++++ 6 files changed, 409 insertions(+), 11 deletions(-) create mode 100644 python/uv.lock diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 6e2947deb..868eeb2cf 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -20,8 +20,10 @@ jobs: uses: astral-sh/setup-uv@v4 with: enable-cache: true - cache-dependency-glob: requirements.txt cache-suffix: pytest + cache-dependency-glob: | + python/pyproject.toml + python/uv.lock - name: Setup Python uses: actions/setup-python@v5 @@ -29,10 +31,9 @@ jobs: python-version: 3.12 - name: Install dependencies + working-directory: python shell: bash - run: | - uv pip install -r requirements.txt - uv pip install pytest~=8.3.5 + run: uv pip install . - name: Run Python tests shell: bash diff --git a/.gitignore b/.gitignore index 9de0fa307..033b11087 100644 --- a/.gitignore +++ b/.gitignore @@ -30,7 +30,4 @@ cache/ # Ignore scratch documents scratch*.* -# Python files -__pycache__ - /.quarto/ diff --git a/pipeline/04-interpret.R b/pipeline/04-interpret.R index 9ae810b83..f5b59e519 100644 --- a/pipeline/04-interpret.R +++ b/pipeline/04-interpret.R @@ -14,7 +14,11 @@ tictoc::tic("Interpret") # configures reticulate to use uv to install those dependencies. Reticulate # will install the dependencies when we import them. In this particular script, # we don't import Python dependencies directly, but rather we import the -# comps module which then imports these dependencies +# comps module which then imports these dependencies. +# +# Because the reticulate uv integration is not very sophisticated, this +# dependency list is duplicated in `pipeline/04-interpret.R`. If you add or +# change any dependencies in this list, make sure to change them there too reticulate::py_require( packages = c("numpy==2.2.*", "numba==0.62.*", "pandas==2.3.*"), python_version = "3.10" diff --git a/python/.gitignore b/python/.gitignore index d04bdcda0..659d1195d 100644 --- a/python/.gitignore +++ b/python/.gitignore @@ -1,3 +1,4 @@ -# Ignore uv lockfile because we use requirements.txt for this project, in order -# to make it compatible with reticulate -uv.lock +# Python artifacts +__pycache__ +build/ +*.egg-info diff --git a/python/pyproject.toml b/python/pyproject.toml index 1f643ed16..f0f3154e2 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -3,6 +3,19 @@ name = "model-res-avm-python" version = "0.0.1" description = "Python code for the CCAO residential model AVM" requires-python = ">=3.10" +# Because the reticulate uv integration is not very sophisticated, this +# dependency list is duplicated in `pipeline/04-interpret.R`. If you add or +# change any dependencies in this list, make sure to change them there too +dependencies = [ + "numpy==2.2.*", + "numba==0.62.*", + "pandas==2.3.*" +] + +[project.optional-dependencies] +tests = [ + "pytest==8.3.*" +] [tool.pytest.ini_options] minversion = "7.0.0" diff --git a/python/uv.lock b/python/uv.lock new file mode 100644 index 000000000..8973d9bfd --- /dev/null +++ b/python/uv.lock @@ -0,0 +1,382 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version < '3.11'", +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "llvmlite" +version = "0.45.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/99/8d/5baf1cef7f9c084fb35a8afbde88074f0d6a727bc63ef764fe0e7543ba40/llvmlite-0.45.1.tar.gz", hash = "sha256:09430bb9d0bb58fc45a45a57c7eae912850bedc095cd0810a57de109c69e1c32", size = 185600, upload-time = "2025-10-01T17:59:52.046Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/6d/585c84ddd9d2a539a3c3487792b3cf3f988e28ec4fa281bf8b0e055e1166/llvmlite-0.45.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:1b1af0c910af0978aa55fa4f60bbb3e9f39b41e97c2a6d94d199897be62ba07a", size = 43043523, upload-time = "2025-10-01T18:02:58.621Z" }, + { url = "https://files.pythonhosted.org/packages/ae/34/992bd12d3ff245e0801bcf6013961daa8c19c9b9c2e61cb4b8bce94566f9/llvmlite-0.45.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02a164db2d79088bbd6e0d9633b4fe4021d6379d7e4ac7cc85ed5f44b06a30c5", size = 37253122, upload-time = "2025-10-01T18:03:55.159Z" }, + { url = "https://files.pythonhosted.org/packages/a6/7b/6d7585998a5991fa74dc925aae57913ba8c7c2efff909de9d34cc1cd3c27/llvmlite-0.45.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f2d47f34e4029e6df3395de34cc1c66440a8d72712993a6e6168db228686711b", size = 56288210, upload-time = "2025-10-01T18:00:41.978Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e2/a4abea058633bfc82eb08fd69ce242c118fdb9b0abad1fdcbe0bc6aedab5/llvmlite-0.45.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7319e5f9f90720578a7f56fbc805bdfb4bc071b507c7611f170d631c3c0f1e0", size = 55140958, upload-time = "2025-10-01T18:01:55.694Z" }, + { url = "https://files.pythonhosted.org/packages/74/c0/233468e96ed287b953239c3b24b1d69df47c6ba9262bfdca98eda7e83a04/llvmlite-0.45.1-cp310-cp310-win_amd64.whl", hash = "sha256:4edb62e685867799e336723cb9787ec6598d51d0b1ed9af0f38e692aa757e898", size = 38132232, upload-time = "2025-10-01T18:04:41.538Z" }, + { url = "https://files.pythonhosted.org/packages/04/ad/9bdc87b2eb34642c1cfe6bcb4f5db64c21f91f26b010f263e7467e7536a3/llvmlite-0.45.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:60f92868d5d3af30b4239b50e1717cb4e4e54f6ac1c361a27903b318d0f07f42", size = 43043526, upload-time = "2025-10-01T18:03:15.051Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ea/c25c6382f452a943b4082da5e8c1665ce29a62884e2ec80608533e8e82d5/llvmlite-0.45.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98baab513e19beb210f1ef39066288784839a44cd504e24fff5d17f1b3cf0860", size = 37253118, upload-time = "2025-10-01T18:04:06.783Z" }, + { url = "https://files.pythonhosted.org/packages/fe/af/85fc237de98b181dbbe8647324331238d6c52a3554327ccdc83ced28efba/llvmlite-0.45.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3adc2355694d6a6fbcc024d59bb756677e7de506037c878022d7b877e7613a36", size = 56288209, upload-time = "2025-10-01T18:01:00.168Z" }, + { url = "https://files.pythonhosted.org/packages/0a/df/3daf95302ff49beff4230065e3178cd40e71294968e8d55baf4a9e560814/llvmlite-0.45.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f3377a6db40f563058c9515dedcc8a3e562d8693a106a28f2ddccf2c8fcf6ca", size = 55140958, upload-time = "2025-10-01T18:02:11.199Z" }, + { url = "https://files.pythonhosted.org/packages/a4/56/4c0d503fe03bac820ecdeb14590cf9a248e120f483bcd5c009f2534f23f0/llvmlite-0.45.1-cp311-cp311-win_amd64.whl", hash = "sha256:f9c272682d91e0d57f2a76c6d9ebdfccc603a01828cdbe3d15273bdca0c3363a", size = 38132232, upload-time = "2025-10-01T18:04:52.181Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7c/82cbd5c656e8991bcc110c69d05913be2229302a92acb96109e166ae31fb/llvmlite-0.45.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:28e763aba92fe9c72296911e040231d486447c01d4f90027c8e893d89d49b20e", size = 43043524, upload-time = "2025-10-01T18:03:30.666Z" }, + { url = "https://files.pythonhosted.org/packages/9d/bc/5314005bb2c7ee9f33102c6456c18cc81745d7055155d1218f1624463774/llvmlite-0.45.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1a53f4b74ee9fd30cb3d27d904dadece67a7575198bd80e687ee76474620735f", size = 37253123, upload-time = "2025-10-01T18:04:18.177Z" }, + { url = "https://files.pythonhosted.org/packages/96/76/0f7154952f037cb320b83e1c952ec4a19d5d689cf7d27cb8a26887d7bbc1/llvmlite-0.45.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b3796b1b1e1c14dcae34285d2f4ea488402fbd2c400ccf7137603ca3800864f", size = 56288211, upload-time = "2025-10-01T18:01:24.079Z" }, + { url = "https://files.pythonhosted.org/packages/00/b1/0b581942be2683ceb6862d558979e87387e14ad65a1e4db0e7dd671fa315/llvmlite-0.45.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:779e2f2ceefef0f4368548685f0b4adde34e5f4b457e90391f570a10b348d433", size = 55140958, upload-time = "2025-10-01T18:02:30.482Z" }, + { url = "https://files.pythonhosted.org/packages/33/94/9ba4ebcf4d541a325fd8098ddc073b663af75cc8b065b6059848f7d4dce7/llvmlite-0.45.1-cp312-cp312-win_amd64.whl", hash = "sha256:9e6c9949baf25d9aa9cd7cf0f6d011b9ca660dd17f5ba2b23bdbdb77cc86b116", size = 38132231, upload-time = "2025-10-01T18:05:03.664Z" }, + { url = "https://files.pythonhosted.org/packages/1d/e2/c185bb7e88514d5025f93c6c4092f6120c6cea8fe938974ec9860fb03bbb/llvmlite-0.45.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:d9ea9e6f17569a4253515cc01dade70aba536476e3d750b2e18d81d7e670eb15", size = 43043524, upload-time = "2025-10-01T18:03:43.249Z" }, + { url = "https://files.pythonhosted.org/packages/09/b8/b5437b9ecb2064e89ccf67dccae0d02cd38911705112dd0dcbfa9cd9a9de/llvmlite-0.45.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:c9f3cadee1630ce4ac18ea38adebf2a4f57a89bd2740ce83746876797f6e0bfb", size = 37253121, upload-time = "2025-10-01T18:04:30.557Z" }, + { url = "https://files.pythonhosted.org/packages/f7/97/ad1a907c0173a90dd4df7228f24a3ec61058bc1a9ff8a0caec20a0cc622e/llvmlite-0.45.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:57c48bf2e1083eedbc9406fb83c4e6483017879714916fe8be8a72a9672c995a", size = 56288210, upload-time = "2025-10-01T18:01:40.26Z" }, + { url = "https://files.pythonhosted.org/packages/32/d8/c99c8ac7a326e9735401ead3116f7685a7ec652691aeb2615aa732b1fc4a/llvmlite-0.45.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3aa3dfceda4219ae39cf18806c60eeb518c1680ff834b8b311bd784160b9ce40", size = 55140957, upload-time = "2025-10-01T18:02:46.244Z" }, + { url = "https://files.pythonhosted.org/packages/09/56/ed35668130e32dbfad2eb37356793b0a95f23494ab5be7d9bf5cb75850ee/llvmlite-0.45.1-cp313-cp313-win_amd64.whl", hash = "sha256:080e6f8d0778a8239cd47686d402cb66eb165e421efa9391366a9b7e5810a38b", size = 38132232, upload-time = "2025-10-01T18:05:14.477Z" }, +] + +[[package]] +name = "model-res-avm-python" +version = "0.0.1" +source = { virtual = "." } +dependencies = [ + { name = "numba" }, + { name = "numpy" }, + { name = "pandas" }, +] + +[package.optional-dependencies] +tests = [ + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "numba", specifier = "==0.62.*" }, + { name = "numpy", specifier = "==2.2.*" }, + { name = "pandas", specifier = "==2.3.*" }, + { name = "pytest", marker = "extra == 'tests'", specifier = "==8.3.*" }, +] +provides-extras = ["tests"] + +[[package]] +name = "numba" +version = "0.62.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llvmlite" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/20/33dbdbfe60e5fd8e3dbfde299d106279a33d9f8308346022316781368591/numba-0.62.1.tar.gz", hash = "sha256:7b774242aa890e34c21200a1fc62e5b5757d5286267e71103257f4e2af0d5161", size = 2749817, upload-time = "2025-09-29T10:46:31.551Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/27/a5a9a58f267ec3b72f609789b2a8eefd6156bd7117e41cc9b7cf5de30490/numba-0.62.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:a323df9d36a0da1ca9c592a6baaddd0176d9f417ef49a65bb81951dce69d941a", size = 2684281, upload-time = "2025-09-29T10:43:31.863Z" }, + { url = "https://files.pythonhosted.org/packages/3a/9d/ffc091c0bfd7b80f66df3887a7061b6af80c8c2649902444026ee1454391/numba-0.62.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1e1f4781d3f9f7c23f16eb04e76ca10b5a3516e959634bd226fc48d5d8e7a0a", size = 2687311, upload-time = "2025-09-29T10:43:54.441Z" }, + { url = "https://files.pythonhosted.org/packages/a1/13/9a27bcd0baeea236116070c7df458414336f25e9dd5a872b066cf36b74bf/numba-0.62.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:14432af305ea68627a084cd702124fd5d0c1f5b8a413b05f4e14757202d1cf6c", size = 3734548, upload-time = "2025-09-29T10:42:38.232Z" }, + { url = "https://files.pythonhosted.org/packages/a7/00/17a1ac4a60253c784ce59549375e047da98330b82de7df6ac7f4ecc90902/numba-0.62.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f180922adf159ae36c2fe79fb94ffaa74cf5cb3688cb72dba0a904b91e978507", size = 3441277, upload-time = "2025-09-29T10:43:06.124Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/20ae0ff78612c4697eaf942a639db01dd4e2d90f634ac41fa3e015c961fc/numba-0.62.1-cp310-cp310-win_amd64.whl", hash = "sha256:f41834909d411b4b8d1c68f745144136f21416547009c1e860cc2098754b4ca7", size = 2745647, upload-time = "2025-09-29T10:44:15.282Z" }, + { url = "https://files.pythonhosted.org/packages/dd/5f/8b3491dd849474f55e33c16ef55678ace1455c490555337899c35826836c/numba-0.62.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:f43e24b057714e480fe44bc6031de499e7cf8150c63eb461192caa6cc8530bc8", size = 2684279, upload-time = "2025-09-29T10:43:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/bf/18/71969149bfeb65a629e652b752b80167fe8a6a6f6e084f1f2060801f7f31/numba-0.62.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:57cbddc53b9ee02830b828a8428757f5c218831ccc96490a314ef569d8342b7b", size = 2687330, upload-time = "2025-09-29T10:43:59.601Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7d/403be3fecae33088027bc8a95dc80a2fda1e3beff3e0e5fc4374ada3afbe/numba-0.62.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:604059730c637c7885386521bb1b0ddcbc91fd56131a6dcc54163d6f1804c872", size = 3739727, upload-time = "2025-09-29T10:42:45.922Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c3/3d910d08b659a6d4c62ab3cd8cd93c4d8b7709f55afa0d79a87413027ff6/numba-0.62.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6c540880170bee817011757dc9049dba5a29db0c09b4d2349295991fe3ee55f", size = 3445490, upload-time = "2025-09-29T10:43:12.692Z" }, + { url = "https://files.pythonhosted.org/packages/5b/82/9d425c2f20d9f0a37f7cb955945a553a00fa06a2b025856c3550227c5543/numba-0.62.1-cp311-cp311-win_amd64.whl", hash = "sha256:03de6d691d6b6e2b76660ba0f38f37b81ece8b2cc524a62f2a0cfae2bfb6f9da", size = 2745550, upload-time = "2025-09-29T10:44:20.571Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fa/30fa6873e9f821c0ae755915a3ca444e6ff8d6a7b6860b669a3d33377ac7/numba-0.62.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:1b743b32f8fa5fff22e19c2e906db2f0a340782caf024477b97801b918cf0494", size = 2685346, upload-time = "2025-09-29T10:43:43.677Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d5/504ce8dc46e0dba2790c77e6b878ee65b60fe3e7d6d0006483ef6fde5a97/numba-0.62.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:90fa21b0142bcf08ad8e32a97d25d0b84b1e921bc9423f8dda07d3652860eef6", size = 2688139, upload-time = "2025-09-29T10:44:04.894Z" }, + { url = "https://files.pythonhosted.org/packages/50/5f/6a802741176c93f2ebe97ad90751894c7b0c922b52ba99a4395e79492205/numba-0.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6ef84d0ac19f1bf80431347b6f4ce3c39b7ec13f48f233a48c01e2ec06ecbc59", size = 3796453, upload-time = "2025-09-29T10:42:52.771Z" }, + { url = "https://files.pythonhosted.org/packages/7e/df/efd21527d25150c4544eccc9d0b7260a5dec4b7e98b5a581990e05a133c0/numba-0.62.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9315cc5e441300e0ca07c828a627d92a6802bcbf27c5487f31ae73783c58da53", size = 3496451, upload-time = "2025-09-29T10:43:19.279Z" }, + { url = "https://files.pythonhosted.org/packages/80/44/79bfdab12a02796bf4f1841630355c82b5a69933b1d50eb15c7fa37dabe8/numba-0.62.1-cp312-cp312-win_amd64.whl", hash = "sha256:44e3aa6228039992f058f5ebfcfd372c83798e9464297bdad8cc79febcf7891e", size = 2745552, upload-time = "2025-09-29T10:44:26.399Z" }, + { url = "https://files.pythonhosted.org/packages/22/76/501ea2c07c089ef1386868f33dff2978f43f51b854e34397b20fc55e0a58/numba-0.62.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:b72489ba8411cc9fdcaa2458d8f7677751e94f0109eeb53e5becfdc818c64afb", size = 2685766, upload-time = "2025-09-29T10:43:49.161Z" }, + { url = "https://files.pythonhosted.org/packages/80/68/444986ed95350c0611d5c7b46828411c222ce41a0c76707c36425d27ce29/numba-0.62.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:44a1412095534a26fb5da2717bc755b57da5f3053965128fe3dc286652cc6a92", size = 2688741, upload-time = "2025-09-29T10:44:10.07Z" }, + { url = "https://files.pythonhosted.org/packages/78/7e/bf2e3634993d57f95305c7cee4c9c6cb3c9c78404ee7b49569a0dfecfe33/numba-0.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8c9460b9e936c5bd2f0570e20a0a5909ee6e8b694fd958b210e3bde3a6dba2d7", size = 3804576, upload-time = "2025-09-29T10:42:59.53Z" }, + { url = "https://files.pythonhosted.org/packages/e8/b6/8a1723fff71f63bbb1354bdc60a1513a068acc0f5322f58da6f022d20247/numba-0.62.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:728f91a874192df22d74e3fd42c12900b7ce7190b1aad3574c6c61b08313e4c5", size = 3503367, upload-time = "2025-09-29T10:43:26.326Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ec/9d414e7a80d6d1dc4af0e07c6bfe293ce0b04ea4d0ed6c45dad9bd6e72eb/numba-0.62.1-cp313-cp313-win_amd64.whl", hash = "sha256:bbf3f88b461514287df66bc8d0307e949b09f2b6f67da92265094e8fa1282dd8", size = 2745529, upload-time = "2025-09-29T10:44:31.738Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763, upload-time = "2025-09-29T23:16:53.287Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217, upload-time = "2025-09-29T23:17:04.522Z" }, + { url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791, upload-time = "2025-09-29T23:17:18.444Z" }, + { url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373, upload-time = "2025-09-29T23:17:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444, upload-time = "2025-09-29T23:17:49.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459, upload-time = "2025-09-29T23:18:03.722Z" }, + { url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086, upload-time = "2025-09-29T23:18:18.505Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, + { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, + { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, + { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, + { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, + { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, + { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, + { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] From 3396b13e80a4ba48bcb315a1d3217275af693587 Mon Sep 17 00:00:00 2001 From: Damonamajor Date: Mon, 23 Feb 2026 21:21:06 +0000 Subject: [PATCH 20/28] improve commenting --- R/helpers.R | 35 +++++++++++++++++------------------ python/tests/test_comps.py | 3 +-- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/R/helpers.R b/R/helpers.R index ee810c00b..12fa6be48 100644 --- a/R/helpers.R +++ b/R/helpers.R @@ -184,16 +184,11 @@ extract_num_iterations <- function(x) { } # Helper function to return weights for comps -# Computes per-tree weights from cumulative leaf node values. -# Basic Steps -# For every observation, map its assigned leaf index in -# each tree to the corresponding leaf value. -# Compute the row-wise cumulative sums of these -# leaf values (stand-in for training data predictions). -# Calculate the absolute prediction error. -# Compute the reduction in error. -# Normalize these improvements so that row-weights sum to 1. +# The `extract_tree_weights` function allows the user to return weights +# for each tree in a LightGBM model. Based on internal testing, we currently +# default to an unweighted value of 1 / n_trees for each tree. This returns +# a single vector with a length of the number of trees. # Inputs: # model: Lightgbm model @@ -229,7 +224,9 @@ extract_tree_weights <- function(model, } # --------------------------------------------------------- - # unweighted (vector with 1/n_trees for each tree) + # Unweighted: + # Vector with 1/n_trees for each tree. This is the default input. + # This returns a vector with the length of the number of trees. # --------------------------------------------------------- if (algorithm == "unweighted") { weights <- rep(1 / n_trees, n_trees) @@ -238,9 +235,9 @@ extract_tree_weights <- function(model, } # --------------------------------------------------------- - # prediction_variance: - # vector for tree weights based on variance of - # leaf values across data + # Prediction_variance: + # Vector for tree weights based on variance of leaf values across data. + # This returns a vector with the length of the number of trees. # --------------------------------------------------------- if (algorithm == "prediction_variance") { tree_dt <- lgb.model.dt.tree(model) @@ -308,8 +305,9 @@ extract_tree_weights <- function(model, next_err <- tree_errors[, 2:(n_trees + 1L), drop = FALSE] # --------------------------------------------------------- - # unweighted_with_error_reduction - # (weights are 1/n_improving trees for trees which reduce errors, 0 otherwise) + # Unweighted_with_error_reduction: + # Weights are 1/n_improving trees for trees which reduce errors, 0 otherwise. + # This returns a matrix with dimensions of observations x trees. # --------------------------------------------------------- if (algorithm == "unweighted_with_error_reduction") { improving <- prev_err > next_err @@ -321,9 +319,10 @@ extract_tree_weights <- function(model, } # --------------------------------------------------------- - # proportional error reduction: - # weights are proportional to the reduction in error (prev_err - next_err) for - # improving trees, 0 otherwise + # Proportional error reduction: + # Weights are proportional to the reduction in error (prev_err - next_err) for + # improving trees, 0 otherwise. This returns a matrix with dimensions of + # observations x trees. # --------------------------------------------------------- diff_in_errors <- pmax(0, prev_err - next_err) dim(diff_in_errors) <- dim(prev_err) diff --git a/python/tests/test_comps.py b/python/tests/test_comps.py index faeb8802a..ab4427e75 100644 --- a/python/tests/test_comps.py +++ b/python/tests/test_comps.py @@ -320,7 +320,6 @@ def test_get_comps( ), pt.param( pd.DataFrame([[1, 1, 1]]), - pd.DataFrame([[1, 1, 1], [2, 2, 2], [3, 3, 3]]), np.array([0.333] * 4).reshape(2, 2), ValueError, @@ -364,4 +363,4 @@ def test_get_comps_raises_on_invalid_inputs( ) def test_insert_at_idx_and_shift(arr, elem, idx, expected): result = comps_module.insert_at_idx_and_shift(np.array(arr), elem, idx) - np.testing.assert_array_equal(result, np.array(expected)) \ No newline at end of file + np.testing.assert_array_equal(result, np.array(expected)) From 71368088683fdce9a8d9b23cc597a97b2726f342 Mon Sep 17 00:00:00 2001 From: Jean Cochrane Date: Mon, 23 Feb 2026 15:28:21 -0600 Subject: [PATCH 21/28] Make sure to install test dependencies in `test` workflow --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 868eeb2cf..34ec0dfe7 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -33,7 +33,7 @@ jobs: - name: Install dependencies working-directory: python shell: bash - run: uv pip install . + run: uv pip install .[tests] - name: Run Python tests shell: bash From 82605f85ee4ef530b2a25db545efd7c846d47ec5 Mon Sep 17 00:00:00 2001 From: Jean Cochrane Date: Mon, 23 Feb 2026 15:33:35 -0600 Subject: [PATCH 22/28] Fix incorrect path reference in interpret stage --- pipeline/04-interpret.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pipeline/04-interpret.R b/pipeline/04-interpret.R index f5b59e519..cc1036295 100644 --- a/pipeline/04-interpret.R +++ b/pipeline/04-interpret.R @@ -17,7 +17,7 @@ tictoc::tic("Interpret") # comps module which then imports these dependencies. # # Because the reticulate uv integration is not very sophisticated, this -# dependency list is duplicated in `pipeline/04-interpret.R`. If you add or +# dependency list is duplicated in `python/pyproject.toml`. If you add or # change any dependencies in this list, make sure to change them there too reticulate::py_require( packages = c("numpy==2.2.*", "numba==0.62.*", "pandas==2.3.*"), From 1200ea3d3913afd7f9c5048145a0c9eb225bdfc1 Mon Sep 17 00:00:00 2001 From: Michael Wagner Date: Mon, 23 Feb 2026 21:35:41 +0000 Subject: [PATCH 23/28] CHeck to make sure weights sum to 1 --- pipeline/04-interpret.R | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pipeline/04-interpret.R b/pipeline/04-interpret.R index 148603878..491f03299 100644 --- a/pipeline/04-interpret.R +++ b/pipeline/04-interpret.R @@ -275,6 +275,13 @@ if (comp_enable) { print(head(tree_weights, 5)) } } else { + tree_weights_sum <- sum(tree_weights) + if (!isTRUE(all.equal(tree_weights_sum, 1))) { + stop( + "Tree weights vector does not sum to 1 (got ", tree_weights_sum, "). ", + "All sales would have a score of 0 if weights sum to 0." + ) + } message( "Tree weights are a vector of length ", length(tree_weights), " (same weights for all training observations)" From 1cfb285d52847fc90c8639d6659ce3ce4f6949a4 Mon Sep 17 00:00:00 2001 From: Damonamajor <56321109+Damonamajor@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:54:55 -0600 Subject: [PATCH 24/28] Update helpers.R --- R/helpers.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/helpers.R b/R/helpers.R index 12fa6be48..7766ec237 100644 --- a/R/helpers.R +++ b/R/helpers.R @@ -319,7 +319,7 @@ extract_tree_weights <- function(model, } # --------------------------------------------------------- - # Proportional error reduction: + # Proportional_error_reduction: # Weights are proportional to the reduction in error (prev_err - next_err) for # improving trees, 0 otherwise. This returns a matrix with dimensions of # observations x trees. From dc091472df60798e0012442a8dbd01507de53c9e Mon Sep 17 00:00:00 2001 From: Damonamajor <56321109+Damonamajor@users.noreply.github.com> Date: Tue, 24 Feb 2026 15:34:23 -0600 Subject: [PATCH 25/28] Add param to finalize.R --- pipeline/05-finalize.R | 1 + 1 file changed, 1 insertion(+) diff --git a/pipeline/05-finalize.R b/pipeline/05-finalize.R index 369434cd1..c57898605 100644 --- a/pipeline/05-finalize.R +++ b/pipeline/05-finalize.R @@ -93,6 +93,7 @@ metadata <- tibble::tibble( shap_enable = shap_enable, comp_enable = comp_enable, comp_num_comps = params$comp$num_comps, + comp_algorithm = params$comp$algorithm, cv_enable = cv_enable, cv_num_folds = params$cv$num_folds, cv_fold_overlap = params$cv$fold_overlap, From 2215b3d69fbfe62baac38f5de525ac41ecd3834c Mon Sep 17 00:00:00 2001 From: Damonamajor <56321109+Damonamajor@users.noreply.github.com> Date: Tue, 24 Feb 2026 15:35:39 -0600 Subject: [PATCH 26/28] alphabetize --- pipeline/05-finalize.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pipeline/05-finalize.R b/pipeline/05-finalize.R index c57898605..1341b004f 100644 --- a/pipeline/05-finalize.R +++ b/pipeline/05-finalize.R @@ -91,9 +91,9 @@ metadata <- tibble::tibble( ratio_study_near_column = params$ratio_study$near_column, ratio_study_num_quantile = list(params$ratio_study$num_quantile), shap_enable = shap_enable, + comp_algorithm = params$comp$algorithm, comp_enable = comp_enable, comp_num_comps = params$comp$num_comps, - comp_algorithm = params$comp$algorithm, cv_enable = cv_enable, cv_num_folds = params$cv$num_folds, cv_fold_overlap = params$cv$fold_overlap, From 0d55dc1f72a9829ca7b0e6a338aafcc69ed720fd Mon Sep 17 00:00:00 2001 From: Michael Wagner Date: Wed, 25 Feb 2026 15:35:53 +0000 Subject: [PATCH 27/28] Add in-place adustment --- python/comps.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/python/comps.py b/python/comps.py index 20eefd515..6891ed64f 100644 --- a/python/comps.py +++ b/python/comps.py @@ -95,6 +95,9 @@ def get_comps( f"(n_comparisons, n_trees), got {weights.ndim}-D" ) + # Avoid editing the df in-place + observation_df = observation_df.copy() + # Chunk the observations so that the script can periodically report progress observation_df["chunk"] = pd.cut( observation_df.index, bins=num_chunks, labels=False From ffa4f1bdc4e089c7760fee6d55b293c977b2a04c Mon Sep 17 00:00:00 2001 From: Michael Wagner Date: Wed, 25 Feb 2026 15:43:52 +0000 Subject: [PATCH 28/28] Remove space --- python/comps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/comps.py b/python/comps.py index 6891ed64f..9cc93e1a9 100644 --- a/python/comps.py +++ b/python/comps.py @@ -97,7 +97,7 @@ def get_comps( # Avoid editing the df in-place observation_df = observation_df.copy() - + # Chunk the observations so that the script can periodically report progress observation_df["chunk"] = pd.cut( observation_df.index, bins=num_chunks, labels=False