From b4e1e775345c35c53d54a5b851d0ca36b85b63cc Mon Sep 17 00:00:00 2001 From: Vincent M Date: Wed, 2 Oct 2024 12:11:20 +0200 Subject: [PATCH] DOC Replace the MovieLens dataset with the fraud dataset (#1053) --- CHANGES.rst | 4 + doc/_static/08_example_aggjoiner.png | Bin 0 -> 108510 bytes doc/_static/08_example_data.png | Bin 0 -> 39012 bytes doc/conf.py | 1 + doc/reference/downloading_a_dataset.rst | 1 + examples/08_join_aggregation.py | 532 ++++++++++--------- examples/FIXME/08_join_aggregation_full.py | 548 +++++++++++++++++++ pixi.lock | 585 +++++++++++++++++---- pyproject.toml | 3 +- skrub/datasets/__init__.py | 2 + skrub/datasets/_fetching.py | 43 ++ skrub/datasets/tests/test_fetching.py | 38 ++ 12 files changed, 1401 insertions(+), 356 deletions(-) create mode 100644 doc/_static/08_example_aggjoiner.png create mode 100644 doc/_static/08_example_data.png create mode 100644 examples/FIXME/08_join_aggregation_full.py diff --git a/CHANGES.rst b/CHANGES.rst index 865dec22d..31a3f63b0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -44,6 +44,10 @@ Minor changes ``pandas.info()`` ) in a table. It can be sorted by each column. :pr:`1056` and :pr:`1068` by :user:`Jérôme Dockès `. +* The credit fraud dataset is now available with the + :func:`fetch_credit_fraud function`. + :pr:`1053` by :user:`Vincent Maladiere `. + * Added zero padding for column names in :class:`MinHashEncoder` to improve column ordering consistency. :pr:`1069` by :user:`Shreekant Nandiyawar `. diff --git a/doc/_static/08_example_aggjoiner.png b/doc/_static/08_example_aggjoiner.png new file mode 100644 index 0000000000000000000000000000000000000000..e064c08293d2c9bee0bdace6beeb814ab0f0231b GIT binary patch literal 108510 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%0AZ!#J&Fa+hXe+ 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 literal 0 HcmV?d00001 diff --git a/doc/_static/08_example_data.png b/doc/_static/08_example_data.png new file mode 100644 index 0000000000000000000000000000000000000000..0bea0f8769d7090ae99844c0c103128eb22278f6 GIT binary patch literal 39012 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 Om(q&Q@}C;M`@aAj<7#pM literal 0 HcmV?d00001 diff --git a/doc/conf.py b/doc/conf.py index 6b7fb208b..b71d38be0 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -512,6 +512,7 @@ def notebook_modification_function(notebook_content, notebook_filename): "Series": "pandas.Series", "pandas.Index": "pandas.Index", "read_csv": "pandas.read_csv", + "pandas.melt": "pandas.melt", "pandas.merge": "pandas.merge", # Skrub "fetch_ken_table_aliases": "skrub.datasets.fetch_ken_table_aliases", diff --git a/doc/reference/downloading_a_dataset.rst b/doc/reference/downloading_a_dataset.rst index f471ef70c..3bbb423f5 100644 --- a/doc/reference/downloading_a_dataset.rst +++ b/doc/reference/downloading_a_dataset.rst @@ -18,6 +18,7 @@ Downloading a dataset fetch_drug_directory fetch_world_bank_indicator fetch_movielens + fetch_credit_fraud fetch_ken_table_aliases fetch_ken_types fetch_ken_embeddings diff --git a/examples/08_join_aggregation.py b/examples/08_join_aggregation.py index e26148928..eac307376 100644 --- a/examples/08_join_aggregation.py +++ b/examples/08_join_aggregation.py @@ -1,302 +1,342 @@ """ -Self-aggregation on MovieLens -============================= +AggJoiner on a credit fraud dataset +=================================== -MovieLens is a famous movie dataset used for both explicit -and implicit recommender systems. It provides a main table, -"ratings", that can be viewed as logs or transactions, comprised -of only 4 columns: ``userId``, ``movieId``, ``rating`` and ``timestamp``. -MovieLens also gives a contextual table "movies", including -``movieId``, ``title`` and ``types``, to enable content-based feature extraction. +Many problems involve tables whose entities have a one-to-many relationship. +To simplify aggregate-then-join operations for machine learning, we can include +the |AggJoiner| in our pipeline. -From the perspective of machine-learning pipelines, one challenge is to -transform the transaction log into features that can be fed to supervised learning. +In this example, we are tackling a fraudulent loan detection use case. +Because fraud is rare, this dataset is extremely imbalanced, with a prevalence of around +1.4%. -In this notebook, we only deal with the main table "ratings". -Our objective is **not to achieve state-of-the-art performance** on -the explicit regression task, but rather to illustrate how to perform -feature engineering in a simple way using |AggJoiner| and |AggTarget|. -Note that our performance is higher than the baseline of using the mean -rating per movies. +The data consists of two distinct entities: e-commerce "baskets", and "products". +Baskets can be tagged fraudulent (1) or not (0), and are essentially a list of products +of variable size. Each basket is linked to at least one products, e.g. basket 1 can have +product 1 and 2. -The benefit of using |AggJoiner| and |AggTarget| is that they readily -provide a full pipeline, from the original tables to the prediction, that can -be cross-validated or applied to new data to serve prediction. At the end of -this example, we showcase hyper-parameter optimization on the whole pipeline. +.. image:: ../../_static/08_example_data.png + :width: 450 px +| + +Our aim is to predict which baskets are fraudulent. + +The products dataframe can be joined on the baskets dataframe using the ``basket_ID`` +column. + +Each product has several attributes: + +- a category (marked by the column ``"item"``), +- a model (``"model"``), +- a brand (``"make"``), +- a merchant code (``"goods_code"``), +- a price per unit (``"cash_price"``), +- a quantity selected in the basket (``"Nbr_of_prod_purchas"``) .. |AggJoiner| replace:: :class:`~skrub.AggJoiner` -.. |AggTarget| replace:: - :class:`~skrub.AggTarget` +.. |Joiner| replace:: + :class:`~skrub.Joiner` + +.. |DropCols| replace:: + :class:`~skrub.DropCols` .. |TableVectorizer| replace:: :class:`~skrub.TableVectorizer` -.. |DatetimeEncoder| replace:: - :class:`~skrub.DatetimeEncoder` +.. |TableReport| replace:: + :class:`~skrub.TableReport` + +.. |MinHashEncoder| replace:: + :class:`~skrub.MinHashEncoder` .. |TargetEncoder| replace:: :class:`~sklearn.preprocessing.TargetEncoder` .. |make_pipeline| replace:: - :class:`~sklearn.pipeline.make_pipeline` + :func:`~sklearn.pipeline.make_pipeline` .. |Pipeline| replace:: :class:`~sklearn.pipeline.Pipeline` -.. |GridSearchCV| replace:: - :class:`~sklearn.model_selection.GridSearchCV` - -.. |TimeSeriesSplit| replace:: - :class:`~sklearn.model_selection.TimeSeriesSplit` - -.. |HGBR| replace:: - :class:`~sklearn.ensemble.HistGradientBoostingRegressor` -""" - -############################################################################### -# The data -# -------- -# -# We begin with loading the ratings table from MovieLens. -# Note that we use the light version (100k rows). -import pandas as pd +.. |HGBC| replace:: + :class:`~sklearn.ensemble.HistGradientBoostingClassifier` -from skrub.datasets import fetch_movielens +.. |OrdinalEncoder| replace:: + :class:`~sklearn.preprocessing.OrdinalEncoder` -ratings = fetch_movielens(dataset_id="ratings") -ratings = ratings.X.sort_values("timestamp").reset_index(drop=True) -ratings["timestamp"] = pd.to_datetime(ratings["timestamp"], unit="s") +.. |TunedThresholdClassifierCV| replace:: + :class:`~sklearn.model_selection.TunedThresholdClassifierCV` -X = ratings[["userId", "movieId", "timestamp"]] -y = ratings["rating"] -X.shape, y.shape -############################################################################### -X.head() +.. |CalibrationDisplay| replace:: + :class:`~sklearn.calibration.CalibrationDisplay` -############################################################################### -# Encoding the timestamp with a TableVectorizer -# --------------------------------------------- -# -# Our first step is to extract features from the timestamp, using the -# |TableVectorizer|. Natively, it uses the |DatetimeEncoder| on datetime -# columns, and doesn't interact with numerical columns. -from skrub import DatetimeEncoder, TableVectorizer - -table_vectorizer = TableVectorizer(datetime=DatetimeEncoder(add_weekday=True)) -X_date_encoded = table_vectorizer.fit_transform(X) -X_date_encoded.head() +.. |pandas.melt| replace:: + :func:`~pandas.melt` -############################################################################### -# We can now make a couple of plots and gain some insight on our dataset. -import seaborn as sns -from matplotlib import pyplot as plt - -sns.set_style("darkgrid") - - -def make_barplot(x, y, title): - fig, ax = plt.subplots(layout="constrained") - norm = plt.Normalize(y.min(), y.max()) - cmap = plt.get_cmap("magma") - - sns.barplot(x=x, y=y, palette=cmap(norm(y)), ax=ax) - ax.set_title(title) - ax.set_xticks(ax.get_xticks(), labels=ax.get_xticklabels(), rotation=30) - ax.set_ylabel(None) - - -# O is Monday, 6 is Sunday +""" +# %% +from skrub import TableReport +from skrub.datasets import fetch_credit_fraud -daily_volume = X_date_encoded["timestamp_weekday"].value_counts().sort_index() +bunch = fetch_credit_fraud() +products, baskets = bunch.products, bunch.baskets +TableReport(products) -make_barplot( - x=daily_volume.index, - y=daily_volume.values, - title="Daily volume of ratings", -) +# %% +TableReport(baskets) -############################################################################### -# We also display the distribution of our target ``y``. -rating_count = y.value_counts().sort_index() - -make_barplot( - x=rating_count.index, - y=rating_count.values, - title="Distribution of ratings given to movies", -) +# %% +# Naive aggregation +# ----------------- +# +# Let's explore a naive solution first. +# +# .. note:: +# +# Click :ref:`here` to skip this section and see the AggJoiner +# in action! +# +# +# The first idea that comes to mind to merge these two tables is to aggregate the +# products attributes into lists, using their basket IDs. +products_grouped = products.groupby("basket_ID").agg(list) +TableReport(products_grouped) + +# %% +# Then, we can expand all lists into columns, as if we were "flattening" the dataframe. +# We end up with a products dataframe ready to be joined on the baskets dataframe, using +# ``"basket_ID"`` as the join key. +import pandas as pd +products_flatten = [] +for col in products_grouped.columns: + cols = [f"{col}{idx}" for idx in range(24)] + products_flatten.append(pd.DataFrame(products_grouped[col].to_list(), columns=cols)) +products_flatten = pd.concat(products_flatten, axis=1) +products_flatten.insert(0, "basket_ID", products_grouped.index) +TableReport(products_flatten) -############################################################################### -# AggTarget: aggregate y, then join -# --------------------------------- +# %% +# Look at the "Stats" section of the |TableReport| above. Does anything strike you? +# +# Not only did we create 144 columns, but most of these columns are filled with NaN, +# which is very inefficient for learning! # -# We have just extracted datetime features from timestamps. +# This is because each basket contains a variable number of products, up to 24, and we +# created one column for each product attribute, for each position (up to 24) in +# the dataframe. # -# Let's now perform an expansion for the target ``y``, by aggregating it before -# joining it back on the main table. The biggest risk of doing target expansion -# with multiple dataframe operations yourself is to end up leaking the target. +# Moreover, if we wanted to replace text columns with encodings, we would create +# :math:`d \times 24 \times 2` columns (encoding of dimensionality :math:`d`, for +# 24 products, for the ``"item"`` and ``"make"`` columns), which would explode the +# memory usage. # -# To solve this, the |AggTarget| transformer allows you to -# aggregate the target ``y`` before joining it on the main table, without -# risk of leaking. Note that to perform aggregation then joining on the features -# ``X``, you need to use |AggJoiner| instead. +# .. _agg-joiner-anchor: # -# You can also think of it as a generalization of the |TargetEncoder|, which -# encodes categorical features based on the target. +# AggJoiner +# --------- +# Let's now see how the |AggJoiner| can help us solve this. We begin with splitting our +# basket dataset in a training and testing set. +from sklearn.model_selection import train_test_split + +X, y = baskets[["ID"]], baskets["fraud_flag"] +X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y, test_size=0.1) +X_train.shape, y_train.shape + +# %% +# Before aggregating our product dataframe, we need to vectorize our categorical +# columns. To do so, we use: # -# We only focus on aggregating the target by **users**, but later we will -# also consider aggregating by **movies**. Here, we compute the histogram of the -# target with 3 bins, before joining it back on the initial table. +# - |MinHashEncoder| on "item" and "model" columns, because they both expose typos +# and text similarities. +# - |OrdinalEncoder| on "make" and "goods_code" columns, because they consist in +# orthogonal categories. # -# This feature answer questions like -# *"How many times has this user given a bad, medium or good rate to movies?"*. -from skrub import AggTarget - -agg_target_user = AggTarget( - main_key="userId", - suffix="_user", - operation="hist(3)", +# We bring this logic into a |TableVectorizer| to vectorize these columns in a +# single step. +# See `this example `_ +# for more details about these encoding choices. +from sklearn.preprocessing import OrdinalEncoder + +from skrub import MinHashEncoder, TableVectorizer + +vectorizer = TableVectorizer( + high_cardinality=MinHashEncoder(), # encode ["item", "model"] + specific_transformers=[ + (OrdinalEncoder(), ["make", "goods_code"]), + ], ) -X_transformed = agg_target_user.fit_transform(X, y) +products_transformed = vectorizer.fit_transform(products) +TableReport(products_transformed) -X_transformed.shape -############################################################################### -X_transformed.head() - -############################################################################### -# Similarly, we join on ``movieId`` instead of ``userId``. +# %% +# Our objective is now to aggregate this vectorized product dataframe by +# ``"basket_ID"``, then to merge it on the baskets dataframe, still on +# the ``"basket_ID"``. # -# This feature answer questions like -# *"How many times has this movie received a bad, medium or good rate from users?"*. -agg_target_movie = AggTarget( - main_key="movieId", - suffix="_movie", - operation="hist(3)", -) -X_transformed = agg_target_movie.fit_transform(X, y) -X_transformed.shape -############################################################################### -X_transformed.head() - -############################################################################### -# Chaining everything together in a pipeline -# ------------------------------------------ +# .. image:: ../../_static/08_example_aggjoiner.png +# :width: 900 # -# To perform cross-validation and enable hyper-parameter tuning, we gather -# all elements into a scikit-learn |Pipeline| by using |make_pipeline|, -# and define a scikit-learn |HGBR|. -from sklearn.ensemble import HistGradientBoostingRegressor -from sklearn.pipeline import make_pipeline - -pipeline = make_pipeline( - table_vectorizer, - agg_target_user, - agg_target_movie, - HistGradientBoostingRegressor(learning_rate=0.1, max_depth=4, max_iter=40), -) - -pipeline - -############################################################################### -# Hyper-parameters tuning and cross validation -# -------------------------------------------- +# | +# +# |AggJoiner| can help us achieve exactly this. We need to pass the product dataframe as +# an auxiliary table argument to |AggJoiner| in ``__init__``. The ``aux_key`` argument +# represent both the columns used to groupby on, and the columns used to join on. # -# We can finally create our hyper-parameter search space, and use a -# |GridSearchCV|. We select the cross validation splitter to be -# the |TimeSeriesSplit| to prevent leakage, since our data are timestamped -# logs. +# The basket dataframe is our main table, and we indicate the columns to join on with +# ``main_key``. Note that we pass the main table during ``fit``, and we discuss the +# limitations of this design in the conclusion at the bottom of this notebook. # -# Note that you need the name of the pipeline elements to assign them -# hyper-parameters search. +# The minimum ("min") is the most appropriate operation to aggregate encodings from +# |MinHashEncoder|, for reasons that are out of the scope of this notebook. # -# You can lookup the name of the pipeline elements by doing: -list(pipeline.named_steps) +from skrub import AggJoiner +from skrub import _selectors as s + +# Skrub selectors allow us to select columns using regexes, which reduces +# the boilerplate. +minhash_cols_query = s.glob("item*") | s.glob("model*") +minhash_cols = s.select(products_transformed, minhash_cols_query).columns + +agg_joiner = AggJoiner( + aux_table=products_transformed, + aux_key="basket_ID", + main_key="ID", + cols=minhash_cols, + operations=["min"], +) +baskets_products = agg_joiner.fit_transform(baskets) +TableReport(baskets_products) -############################################################################### -# Alternatively, you can use scikit-learn |Pipeline| to name your transformers: -# ``Pipeline([("agg_target_user", agg_target_user), ...])`` +# %% +# Now that we understand how to use the |AggJoiner|, we can now assemble our pipeline by +# chaining two |AggJoiner| together: # -# We now perform the grid search over the ``AggTarget`` transformers to find the -# operation maximizing our validation score. -from sklearn.model_selection import GridSearchCV, TimeSeriesSplit - -operations = ["mean", "hist(3)", "hist(5)", "hist(7)", "value_counts"] -param_grid = [ - { - "aggtarget-2__operation": [op], - } - for op in operations -] - -cv = GridSearchCV(pipeline, param_grid, cv=TimeSeriesSplit(n_splits=10)) -cv.fit(X, y) - -results = pd.DataFrame(cv.cv_results_) - -cols = [f"split{idx}_test_score" for idx in range(10)] -results = results.set_index("param_aggtarget-2__operation")[cols].T -results - -############################################################################### -# The score used in this regression task is the R2. Remember that the R2 -# evaluates the relative performance compared to the naive baseline consisting -# in always predicting the mean value of ``y_test``. -# Therefore, the R2 is 0 when ``y_pred = y_true.mean()`` and is upper bounded -# to 1 when ``y_pred = y_true``. +# - the first one to deal with the |MinHashEncoder| vectors as we just saw +# - the second one to deal with the all the other columns # -# To get a better sense of the learning performances of our simple pipeline, -# we also compute the average rating of each movie in the training set, -# and uses this average to predict the ratings in the test set. -from sklearn.metrics import r2_score - - -def baseline_r2(X, y, train_idx, test_idx): - """Compute the average rating for all movies in the train set, - and map these averages to the test set as a prediction. - - If a movie in the test set is not present in the training set, - we simply predict the global average rating of the training set. - """ - X_train, y_train = X.iloc[train_idx].copy(), y.iloc[train_idx] - X_test, y_test = X.iloc[test_idx], y.iloc[test_idx] - - X_train["y"] = y_train - - movie_avg_rating = X_train.groupby("movieId")["y"].mean().to_frame().reset_index() - - y_pred = X_test.merge(movie_avg_rating, on="movieId", how="left")["y"] - y_pred = y_pred.fillna(y_pred.mean()) - - return r2_score(y_true=y_test, y_pred=y_pred) +# For the second |AggJoiner|, we use the mean, standard deviation, minimum and maximum +# operations to extract a representative summary of each distribution. +# +# |DropCols| is another skrub transformer which removes the "ID" column, which doesn't +# bring any information after the joining operation. +from scipy.stats import loguniform, randint +from sklearn.ensemble import HistGradientBoostingClassifier +from sklearn.pipeline import make_pipeline +from skrub import DropCols + +model = make_pipeline( + AggJoiner( + aux_table=products_transformed, + aux_key="basket_ID", + main_key="ID", + cols=minhash_cols, + operations=["min"], + ), + AggJoiner( + aux_table=products_transformed, + aux_key="basket_ID", + main_key="ID", + cols=["make", "goods_code", "cash_price", "Nbr_of_prod_purchas"], + operations=["sum", "mean", "std", "min", "max"], + ), + DropCols(["ID"]), + HistGradientBoostingClassifier(), +) +model -all_baseline_r2 = [] -for train_idx, test_idx in TimeSeriesSplit(n_splits=10).split(X, y): - all_baseline_r2.append(baseline_r2(X, y, train_idx, test_idx)) +# %% +# We tune the hyper-parameters of the |HGBC| to get a good performance. +from time import time -results.insert(0, "naive mean estimator", all_baseline_r2) +from sklearn.model_selection import RandomizedSearchCV -# we only keep the 5 out of 10 last results -# because the initial size of the train set is rather small -fig, ax = plt.subplots(layout="constrained") -sns.boxplot(results.tail(5), palette="magma", ax=ax) -ax.set_ylabel("R2 score") -ax.set_title("Hyper parameters grid-search results") -plt.tight_layout() +param_distributions = dict( + histgradientboostingclassifier__learning_rate=loguniform(1e-3, 1), + histgradientboostingclassifier__max_depth=randint(3, 9), + histgradientboostingclassifier__max_leaf_nodes=[None, 10, 30, 60, 90], + histgradientboostingclassifier__max_iter=randint(50, 500), +) -############################################################################### -# The naive estimator has a lower performance than our pipeline, which means -# that our extracted features brought some predictive power. +tic = time() +search = RandomizedSearchCV( + model, + param_distributions, + scoring="neg_log_loss", + refit=False, + n_iter=10, + cv=3, + verbose=1, +).fit(X_train, y_train) +print(f"This operation took {time() - tic:.1f}s") +# %% +# The best hyper parameters are: + +pd.Series(search.best_params_) + +# %% +# To benchmark our performance, we plot the log loss of our model on the test set +# against the log loss of a dummy model that always output the observed probability of +# the two classes. +# +# As this dataset is extremely imbalanced, this dummy model should be a good baseline. +# +# The vertical bar represents one standard deviation around the mean of the cross +# validation log-loss. +import seaborn as sns +from matplotlib import pyplot as plt +from sklearn.dummy import DummyClassifier +from sklearn.metrics import log_loss + +results = search.cv_results_ +best_idx = search.best_index_ +log_loss_model_mean = -results["mean_test_score"][best_idx] +log_loss_model_std = results["std_test_score"][best_idx] + +dummy = DummyClassifier(strategy="prior").fit(X_train, y_train) +y_proba_dummy = dummy.predict_proba(X_test) +log_loss_dummy = log_loss(y_true=y_test, y_pred=y_proba_dummy) + +fig, ax = plt.subplots() +ax.bar( + height=[log_loss_model_mean, log_loss_dummy], + x=["AggJoiner model", "Dummy"], + color=["C0", "C4"], +) +for container in ax.containers: + ax.bar_label(container, padding=4) + +ax.vlines( + x="AggJoiner model", + ymin=log_loss_model_mean - log_loss_model_std, + ymax=log_loss_model_mean + log_loss_model_std, + linestyle="-", + linewidth=1, + color="k", +) +sns.despine() +ax.set_title("Log loss (lower is better)") + +# %% +# Conclusion +# ---------- +# With |AggJoiner|, you can bring the aggregation and joining operations within a +# sklearn pipeline, and train models more efficiently. +# +# One known limitation of both the |AggJoiner| and |Joiner| is that the auxiliary data +# to join is passed during the ``__init__`` method instead of the ``fit`` method, and +# is therefore fixed once the model has been trained. +# This limitation causes two main issues: +# +# 1. **Bigger model serialization:** Since the dataset has to be pickled along with +# the model, it can result in a massive file size on disk. # -# It seems that using the ``"value_counts"`` as an aggregation operator for -# |AggTarget| yields better performances than using the mean (which is -# equivalent to using the |TargetEncoder|). +# 2. **Inflexibility with new, unseen data in a production environment:** To use new +# auxiliary data, you would need to replace the auxiliary table in the |AggJoiner| that +# was used during ``fit`` with the updated data, which is a rather hacky approach. # -# Here, the number of bins encoding the target is proportional to the -# performance: computing the mean yields a single statistic, whereas histograms -# yield a density over a reduced set of bins, and ``"value_counts"`` yields an -# exhaustive histogram over all the possible values of ratings -# (here 10 different values, from 0.5 to 5). +# These limitations will be addressed later in skrub. diff --git a/examples/FIXME/08_join_aggregation_full.py b/examples/FIXME/08_join_aggregation_full.py new file mode 100644 index 000000000..e1e363e6f --- /dev/null +++ b/examples/FIXME/08_join_aggregation_full.py @@ -0,0 +1,548 @@ +""" +AggJoiner on a credit fraud dataset +=================================== + +In this example, we are tackling a fraudulent loan detection use case. +Because fraud is rare, this dataset is extremely imbalanced, with a prevalence of around +1.4%. + +Instead of focusing on arbitrary metrics like accuracy, we will derive a cost function +based on (questionable) assumptions about the data. In a real-world scenario, we would +need to consult with a domain expert within the company to develop a realistic utility +function. + +The data consists of two distinct concepts: a "basket," which can be tagged as fraud (1) +or not (0), and a list of "products." Each product has several attributes: + +- a category (marked by the column ``"item"``), +- a model (``"model"``), +- a brand (``"make"``), +- a merchant code (``"goods_code"``), +- a price per unit (``"cash_price"``), +- a quantity selected in the basket (``"Nbr_of_prod_purchas"``) + +Since the number of products in each basket varies, the creators of this dataset have +chosen to join all products and their attributes with their respective basket. They have +arbitrarily decided to cut off the basket at the 24th product. However, since most +baskets contain only one or two products, a large proportion of the columns are empty. +Therefore, the dataset is very sparse, which is challenging from a machine learning +perspective and also inefficient in terms of memory usage. + +.. |AggJoiner| replace:: + :class:`~skrub.AggJoiner` + +.. |Joiner| replace:: + :class:`~skrub.Joiner` + +.. |TableVectorizer| replace:: + :class:`~skrub.TableVectorizer` + +.. |MinHashEncoder| replace:: + :class:`~skrub.MinHashEncoder` + +.. |TargetEncoder| replace:: + :class:`~sklearn.preprocessing.TargetEncoder` + +.. |make_pipeline| replace:: + :func:`~sklearn.pipeline.make_pipeline` + +.. |Pipeline| replace:: + :class:`~sklearn.pipeline.Pipeline` + +.. |HGBC| replace:: + :class:`~sklearn.ensemble.HistGradientBoostingClassifier` + +.. |TunedThresholdClassifierCV| replace:: + :class:`~sklearn.model_selection.TunedThresholdClassifierCV` + +.. |CalibrationDisplay| replace:: + :class:`~sklearn.calibration.CalibrationDisplay` + +.. |pandas.melt| replace:: + :func:`~pandas.melt` + +""" + +# %% +# The data +# -------- +# +# We begin with loading the table from figshare. It has around 100k rows. +from skrub.datasets import fetch_figshare + +X = fetch_figshare("48931237").X + +# %% +# The total price is the sum of the price per unit of each product in the basket, +# multiplied by their quantity. This will also allow us to define a utility function +# later, in addition of being a useful feature for the learner. +import numpy as np +import pandas as pd + +from skrub import TableReport + + +def total_price(X): + total_price = pd.Series(np.zeros(X.shape[0]), index=X.index, name="total_price") + max_item = 24 + for idx in range(1, max_item + 1): + total_price += X[f"cash_price{idx}"].fillna(0) * X[ + f"Nbr_of_prod_purchas{idx}" + ].fillna(0) + + return total_price + + +X["total_price"] = total_price(X) +TableReport(X) + +# %% +# Metrics +# ------- +# +# To consider the problem from a business perspective, we define our utility function +# by the cost matrix in the function ``credit_gain_score``. False positive and false +# negative predictions incur a negative gain. +# +# Ultimately, we want to maximize this metric. To do so, we can train our learner to +# minimize a proper scoring rule like the log loss. +import sklearn +from sklearn.metrics import log_loss, make_scorer + + +def credit_gain_score(y_true, y_pred, amount): + """Define our utility function. + + These numbers are entirely made-up, don't try this at home! + """ + mask_tn = (y_true == 0) & (y_pred == 0) + mask_fp = (y_true == 0) & (y_pred == 1) + mask_fn = (y_true == 1) & (y_pred == 0) + + # Refusing a fraud yields 0 € + fraudulent_refuse = 0 + + # Accepting a fraud costs its whole amount + fraudulent_accept = -amount[mask_fn].sum() + + # Refusing a legitimate basket transactions cost 5 € + legitimate_refuse = mask_fp.sum() * -5 + + # Accepting a legitimate basket transaction yields 7% of its amount + legitimate_accept = (amount[mask_tn] * 0.07).sum() + + return fraudulent_refuse + fraudulent_accept + legitimate_refuse + legitimate_accept + + +def get_results(model, X_test, y_test, threshold, amount, time_to_fit): + y_proba = model.predict_proba(X_test)[:, 1] + return { + "log_loss": log_loss(y_test, y_proba), + "gain_score": credit_gain_score(y_test, y_proba > threshold, amount), + "y_proba": y_proba, + "y_test": y_test, + "time_to_fit": time_to_fit, + } + + +sklearn.set_config(enable_metadata_routing=True) +gain_score = make_scorer(credit_gain_score).set_score_request(amount=True) + +results = dict() + +# %% +# Dummy model +# ----------- +# +# We first evaluate the performance of a dummy model that always predict the negative +# class (i.e. all transactions are legit). +# This is a good sanity check to make sure our model actually learns something useful. +from time import time + +from sklearn.dummy import DummyClassifier +from sklearn.model_selection import train_test_split + +target_col = "fraud_flag" +X_ = X.drop(columns=[target_col]) +y_ = X[target_col] + +X_train, X_test, y_train, y_test = train_test_split( + X_, + y_, + test_size=0.1, + stratify=y_, + random_state=0, +) + +tic = time() +dummy_negative = DummyClassifier(strategy="constant", constant=0).fit(X_train, y_train) +time_to_fit = time() - tic + +results["Dummy Negative"] = get_results( + dummy_negative, + X_test, + y_test, + threshold=0.5, + amount=X_test["total_price"], + time_to_fit=time_to_fit, +) + +# %% +# Low effort estimator +# -------------------- +# +# Next, we use the |TableVectorizer| and a |HGBC| to create a very simple baseline model +# that uses the sparse dataset directly. Note that due to the large number of high +# cardinality columns, we can't use an multi-dimensional encoder like the +# |MinHashEncoder|, because the number of columns would then explode. +# +# Instead, we encode our categories with a |TargetEncoder|. +# +# We also further split the training set into a training and validation set for +# post-training tuning in the post-training phase below. +from sklearn.ensemble import HistGradientBoostingClassifier +from sklearn.pipeline import make_pipeline +from sklearn.preprocessing import TargetEncoder + +from skrub import TableVectorizer + +X_train_, X_val, y_train_, y_val = train_test_split( + X_train, y_train, test_size=0.1, stratify=y_train, random_state=0 +) + +low_effort = make_pipeline( + TableVectorizer( + high_cardinality=TargetEncoder(), + ), + HistGradientBoostingClassifier(), +) + +tic = time() +low_effort.fit(X_train_, y_train_) +time_to_fit = time() - tic + +# %% +# To maximise our utility function, we have to find the best classification threshold to +# replace the default at 0.5. |TunedThresholdClassifierCV| is a scikit-learn +# meta-estimator that is designed for this exact purpose. +# More details in this `example from scikit-learn `_. +# +# We give it our trained model, and fit it on the validation dataset instead of the +# training dataset to avoid overfitting. Notice that the scoring method is the utility +# function, to which we pass the amount in ``fit`` +from sklearn.model_selection import TunedThresholdClassifierCV + +low_effort_tuned = TunedThresholdClassifierCV( + low_effort, cv="prefit", scoring=gain_score, refit=False +).fit(X_val, y_val, amount=X_val["total_price"]) + +results["Low effort"] = get_results( + low_effort, + X_test, + y_test, + threshold=low_effort_tuned.best_threshold_, + amount=X_test["total_price"], + time_to_fit=time_to_fit, +) + +# %% +# We define some plotting functions to display our results. +import seaborn as sns +from matplotlib import pyplot as plt +from sklearn.calibration import CalibrationDisplay + + +def plot_gain_tradeoff(results): + """Scatter plot of the score gain (y) vs the fit time (x) for each model.""" + + rows = [] + for estimator_name, result in results.items(): + result["estimator_name"] = estimator_name + rows.append(result) + df = pd.DataFrame(rows) + + names = df["estimator_name"].values + palette = dict(zip(names, sns.color_palette("colorblind", n_colors=len(names)))) + + fig, ax = plt.subplots(figsize=(5, 4), dpi=100) + sns.scatterplot( + df, + x="time_to_fit", + y="gain_score", + hue="estimator_name", + style="estimator_name", + ax=ax, + palette=palette, + s=200, + ) + ax.grid() + + ticks = df["time_to_fit"].round(3).tolist() + labels = [f"{tick}s" for tick in ticks] + ax.set_xticks(ticks, labels) + + ticks = df["gain_score"].round().tolist() + ticks.insert(1, 650_000) + labels = [f"{tick:,} €" for tick in ticks] + + ax.set_yticks(ticks, labels) + ax.set_ylabel("Gain score") + ax.set_xlabel("Time to fit") + ax.set_title("Gain score vs Time to fit") + plt.tight_layout() + + +def plot_calibration_curve(results): + """Plot a calibration curve and the log-loss.""" + + estimator_names = list(results) + palette = dict( + zip( + estimator_names, + sns.color_palette("colorblind", n_colors=len(estimator_names)), + ) + ) + fig, ax = plt.subplots(figsize=(6, 4), dpi=100) + for name, result in results.items(): + log_loss = str(round(result["log_loss"], 4)) + label = f"{name}, {'log_loss: ' + log_loss}" + CalibrationDisplay.from_predictions( + y_true=result["y_test"], + y_prob=result["y_proba"], + strategy="quantile", + label=label, + ax=ax, + color=palette[name], + n_bins=15, + ) + ax.set_xlim([-0.001, 0.13]) + ax.set_ylim([-0.001, 0.13]) + ax.set_title("Calibration curve") + + +# %% +# We see below that the low effort classifier significantly improves our gains compared +# to the dummy baseline. The former is of course slower to train than the latter. + +plot_gain_tradeoff(results) + + +# %% +# We also evaluate the calibration of both models. As very few classes are +# positive, we can expect all probabilities to be close to 0. We have to +# zoom on it, and use the "quantile" strategy of |CalibrationDisplay| in order to create +# bins containing an equal number of samples. + +plot_calibration_curve(results) + + +# %% +# Agg-Joiner based estimator +# -------------------------- +# +# We first need to split the dataframe between a dataframe representing baskets and a +# dataframe representing products. In other words, we need to revert the join operation +# performed by the creator of this dataset. Conceptually, this is close to a +# |pandas.melt| operation +# +# Note that we don't keep the product ordering information, which is probably not an +# important feature here. + + +def get_columns_at(idx, cols_2_idx): + """Small helper that give the position of each of the columns of the idx-th \ + product.""" + cols = [ + "ID", + target_col, + f"item{idx}", + f"cash_price{idx}", + f"make{idx}", + f"model{idx}", + f"goods_code{idx}", + f"Nbr_of_prod_purchas{idx}", + ] + return [cols_2_idx[col] for col in cols] + + +def melt_multi_columns(X): + """Create a dataframe where each product is a row.""" + products = [] + cols_2_idx = dict(zip(X.columns, range(X.shape[1]))) + for row in X.values: + n_products = min(row[cols_2_idx["Nb_of_items"]], 24) + for idx in range(1, n_products + 1): + cols = get_columns_at(idx, cols_2_idx) + products.append(row[cols]) + + cols = [ + "ID", + target_col, + "item", + "cash_price", + "make", + "model", + "goods_code", + "Nbr_of_prod_purchas", + ] + + products = pd.DataFrame(products, columns=cols) + + for col in ["make", "model"]: + products[col] = products[col].fillna("None") + + return products + + +X_train_[target_col] = y_train_ +X_val[target_col] = y_val +X_test[target_col] = y_test + +baskets_train = X_train_[["ID", "total_price", target_col]] +baskets_val = X_val[["ID", "total_price", target_col]] +baskets_test = X_test[["ID", "total_price", target_col]] + +products = melt_multi_columns(X) + +TableReport(products) + +# %% +# We have to aggregate the products dataframe before joining it back to the basket +# dataframe. Prior to that, we need to apply some preprocessing to deal with +# the high cardinality columns. Since these columns have some morphological variations +# and typos, we use the |MinHashEncoder|. +# +# ``goods_code`` is slightly different, as it represents some merchant IDs, which +# co-occurs for different products. Therefore, we encode it with a |TargetEncoder| as +# we previously did. +# +# To later perform the joiner operation, we must keep the basket ``ID`` with +# ``"passthrough"``. +from skrub import MinHashEncoder + + +def get_X_y(data): + return data.drop(columns=[target_col]), data[target_col] + + +tic = time() +vectorizer = TableVectorizer( + high_cardinality=MinHashEncoder(), # applied on ["item", "model", "make"] + specific_transformers=[ + (TargetEncoder(), ["goods_code"]), + ("passthrough", ["ID"]), + ], +) + +products_transformed = vectorizer.fit_transform(*get_X_y(products)) +time_to_fit = time() - tic + +TableReport(products_transformed) + +# %% +# Let's now detail how to leverage |AggJoiner| here. We have just encoded each product +# attributes, and now we need to somehow aggregate these product encodings into their +# respective baskets. +# +# By aggregating instead of concatenating, we obtain an invariant number of columns, +# and we remove the sparsity of the dataset. +# +# But which aggregation operation should we choose? Since the |MinHashEncoder| hashes +# ngrams with different hashing functions and return their minimum, it makes sense to +# aggregate different product encodings using their **minimum** for each dimension. +# You can view MinHash minimums as activations. +# +# For numeric columns and columns encoded with the |TargetEncoder|, we take the mean, +# standard deviation, minimum and maximum to extract a representative summary of each +# distribution. +# +# We can apply these two sets of operations by chaining together two |AggJoiner| in +# a |Pipeline| using |make_pipeline|. We also make use of skrub selectors to select +# columns with the ``glob`` syntax. +# +# We need to pass the product dataframe as an auxiliary table argument to AggJoiner +# in ``__init__``. The basket dataframe is our main table, and we pass it during +# ``fit``. We discuss the limitations of this design in the conclusion at the bottom +# of this notebook. +# +# Let's display the output of this preprocessing pipeline. + +from sklearn.pipeline import make_pipeline + +from skrub import AggJoiner +from skrub import _selectors as s + +minhash_cols = "ID" | s.glob("item_*") | s.glob("model_*") | s.glob("make_*") +single_cols = ["ID", "goods_code", "Nbr_of_prod_purchas", "cash_price"] + +pipe_agg_joiner = make_pipeline( + AggJoiner( + aux_table=s.select(products_transformed, minhash_cols), + key="ID", + operations=["min"], + ), + AggJoiner( + aux_table=s.select(products_transformed, single_cols), + key="ID", + operations=["mean", "sum", "std", "min", "max"], + ), +) +basket_train_transformed = pipe_agg_joiner.fit_transform(baskets_train) + +TableReport(basket_train_transformed) + +# %% +# Now that we get a sense of how the |AggJoiner| can help us, we complete this pipeline +# with a |HGBC| and evaluate our final model. + +tic = time() +agg_join_estimator = make_pipeline( + pipe_agg_joiner, + HistGradientBoostingClassifier(), +).fit(*get_X_y(baskets_train)) +time_to_fit += time() - tic + +agg_join_tuned = TunedThresholdClassifierCV( + agg_join_estimator, cv="prefit", scoring=gain_score, refit=False +).fit(*get_X_y(baskets_val), amount=baskets_val["total_price"]) + +results["Agg Joiner"] = get_results( + agg_join_tuned, + *get_X_y(baskets_test), + threshold=agg_join_tuned.best_threshold_, + amount=baskets_test["total_price"], + time_to_fit=time_to_fit, +) +# %% +# Not only did we improve the gains, but this operation is also much faster than the +# naive low effort! + +plot_gain_tradeoff(results) + +# %% +# We see that the agg-joiner model is slighly more calibrated, with a lower (better) +# log loss. + +plot_calibration_curve(results) + +# %% +# Conclusion +# ---------- +# +# Many problems involve tables where IDs have a one-to-many relationship. To simplify +# aggregate-then-join operations for machine learning, we can include the |AggJoiner| +# in our pipeline. +# +# One known limitation of both the |AggJoiner| and |Joiner| is that the auxiliary data +# to join is passed during the ``__init__`` method instead of the ``fit`` method, and +# is therefore fixed once the model has been trained. +# This limitation causes two main issues: +# +# 1. **Inefficient model serialization:** Since the dataset has to be pickled along with +# the model, it can result in a massive file size on disk. +# +# 2. **Inflexibility with new, unseen data in a production environment:** To use new +# auxiliary data, you would need to replace the auxiliary table in the AggJoiner that +# was used during ``fit`` with the updated data, which is a rather hacky approach. +# +# These limitations will be addresssed later in skrub. diff --git a/pixi.lock b/pixi.lock index 423522acd..fcb8f57e1 100644 --- a/pixi.lock +++ b/pixi.lock @@ -1873,11 +1873,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.16-hb7c19ff_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h27087fc_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20240116.2-cxx17_he02047a_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-17.0.0-had3b6fe_16_cpu.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-17.0.0-h5888daf_16_cpu.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-17.0.0-h5888daf_16_cpu.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-17.0.0-hf54134d_16_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20240722.0-cxx17_h5888daf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-17.0.0-hef0f6b3_17_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-17.0.0-h5888daf_17_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-17.0.0-h5888daf_17_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-17.0.0-he882d9a_17_cpu.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-24_linux64_openblas.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.1.0-hb9d3cd8_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.1.0-hb9d3cd8_2.conda @@ -1906,9 +1906,9 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libglvnd-1.7.0-ha4b6fd6_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-1.7.0-ha4b6fd6_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.1.0-h77fa898_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-2.29.0-h435de7b_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-storage-2.29.0-h0121fbd_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.62.2-h15f2491_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-2.29.0-h438788a_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-storage-2.29.0-h0121fbd_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.65.5-hf5c653b_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.17-hd590300_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.0.0-hd590300_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-24_linux64_openblas.conda @@ -1917,12 +1917,12 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.27-pthreads_hac2b453_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libopengl-1.7.0-ha4b6fd6_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libparquet-17.0.0-h39682fd_16_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libparquet-17.0.0-h39682fd_17_cpu.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libpciaccess-0.18-hd590300_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.44-hadc24fc_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libpq-16.4-h2d7952a_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-4.25.3-hd5b35b9_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libre2-11-2023.09.01-h5a48ba9_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-5.27.5-h5b01275_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libre2-11-2023.09.01-hbbce691_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.46.1-hadc24fc_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.0-h0841786_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-14.1.0-hc0a3c3a_1.conda @@ -1950,7 +1950,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/numpydoc-1.8.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.2-h488ebb8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.3.2-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/orc-2.0.2-h669347b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/orc-2.0.2-h690cf93_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.3-py312hf9745cd_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.44-hba22ea6_2.conda @@ -1976,7 +1976,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2024.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/qhull-2020.2-h434a139_5.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/qt6-main-6.7.2-hadfd74e_5.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/re2-2023.09.01-h7f4b329_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/re2-2023.09.01-h77b4e00_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.5.3-h7b32b05_0.conda @@ -3038,11 +3038,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.16-hb7c19ff_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h27087fc_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20240116.2-cxx17_he02047a_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-17.0.0-had3b6fe_16_cpu.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-17.0.0-h5888daf_16_cpu.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-17.0.0-h5888daf_16_cpu.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-17.0.0-hf54134d_16_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20240722.0-cxx17_h5888daf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-17.0.0-hef0f6b3_17_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-17.0.0-h5888daf_17_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-17.0.0-h5888daf_17_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-17.0.0-he882d9a_17_cpu.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-24_linux64_openblas.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.1.0-hb9d3cd8_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.1.0-hb9d3cd8_2.conda @@ -3071,9 +3071,9 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libglvnd-1.7.0-ha4b6fd6_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-1.7.0-ha4b6fd6_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.1.0-h77fa898_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-2.29.0-h435de7b_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-storage-2.29.0-h0121fbd_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.62.2-h15f2491_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-2.29.0-h438788a_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-storage-2.29.0-h0121fbd_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.65.5-hf5c653b_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.17-hd590300_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.0.0-hd590300_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-24_linux64_openblas.conda @@ -3082,12 +3082,12 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.27-pthreads_hac2b453_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libopengl-1.7.0-ha4b6fd6_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libparquet-17.0.0-h39682fd_16_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libparquet-17.0.0-h39682fd_17_cpu.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libpciaccess-0.18-hd590300_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.44-hadc24fc_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libpq-16.4-h2d7952a_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-4.25.3-hd5b35b9_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libre2-11-2023.09.01-h5a48ba9_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-5.27.5-h5b01275_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libre2-11-2023.09.01-hbbce691_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.20-h4ab18f5_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.46.1-hadc24fc_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.0-h0841786_0.conda @@ -3125,7 +3125,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/numpydoc-1.8.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.2-h488ebb8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.3.2-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/orc-2.0.2-h669347b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/orc-2.0.2-h690cf93_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.3-py311h7db5c69_1.conda @@ -3172,7 +3172,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-26.2.0-py311h7deb3e3_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/qhull-2020.2-h434a139_5.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/qt6-main-6.7.2-hadfd74e_5.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/re2-2023.09.01-h7f4b329_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/re2-2023.09.01-h77b4e00_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.35.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_0.conda @@ -4217,11 +4217,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.16-hb7c19ff_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h27087fc_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20240116.2-cxx17_he02047a_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-17.0.0-had3b6fe_16_cpu.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-17.0.0-h5888daf_16_cpu.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-17.0.0-h5888daf_16_cpu.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-17.0.0-hf54134d_16_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20240722.0-cxx17_h5888daf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-17.0.0-hef0f6b3_17_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-17.0.0-h5888daf_17_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-17.0.0-h5888daf_17_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-17.0.0-he882d9a_17_cpu.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-24_linux64_openblas.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.1.0-hb9d3cd8_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.1.0-hb9d3cd8_2.conda @@ -4250,9 +4250,9 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libglvnd-1.7.0-ha4b6fd6_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-1.7.0-ha4b6fd6_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.1.0-h77fa898_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-2.29.0-h435de7b_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-storage-2.29.0-h0121fbd_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.62.2-h15f2491_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-2.29.0-h438788a_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-storage-2.29.0-h0121fbd_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.65.5-hf5c653b_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.17-hd590300_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.0.0-hd590300_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-24_linux64_openblas.conda @@ -4261,12 +4261,12 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.27-pthreads_hac2b453_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libopengl-1.7.0-ha4b6fd6_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libparquet-17.0.0-h39682fd_16_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libparquet-17.0.0-h39682fd_17_cpu.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libpciaccess-0.18-hd590300_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.44-hadc24fc_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libpq-16.4-h2d7952a_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-4.25.3-hd5b35b9_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libre2-11-2023.09.01-h5a48ba9_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-5.27.5-h5b01275_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libre2-11-2023.09.01-hbbce691_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.20-h4ab18f5_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.46.1-hadc24fc_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.0-h0841786_0.conda @@ -4299,7 +4299,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/numpydoc-1.8.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.2-h488ebb8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.3.2-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/orc-2.0.2-h669347b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/orc-2.0.2-h690cf93_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.3-py312hf9745cd_1.conda @@ -4334,7 +4334,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-26.2.0-py312hbf22597_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/qhull-2020.2-h434a139_5.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/qt6-main-6.7.2-hadfd74e_5.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/re2-2023.09.01-h7f4b329_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/re2-2023.09.01-h77b4e00_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.35.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_0.conda @@ -5591,11 +5591,30 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.12-h4ab18f5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.7.31-h57bd9a3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.7.4-hfd43aa1_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.9.28-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.2.19-h756ea98_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.4.3-h29ce20c_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.8.10-h5e77a74_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.14.18-h33ff4e5_10.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.10.6-h02abb05_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.6.6-h834ce55_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.1.19-h756ea98_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.1.20-h756ea98_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.28.3-h469002c_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.407-h9f1560d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-core-cpp-1.13.0-h935415a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-identity-cpp-1.8.0-hd126650_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.12.0-hd2e3451_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-common-cpp-12.7.0-h10ac4d7_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-files-datalake-cpp-12.11.0-h325d260_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.14.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-1.1.0-hb9d3cd8_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.1.0-hb9d3cd8_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py312h2ec8cdc_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.33.1-heb4867d_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2024.8.30-hbcca054_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.0-hebfffa5_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2024.8.30-pyhd8ed1ab_0.conda @@ -5620,6 +5639,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.54.1-py312h66e93f0_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.12.1-h267a509_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gflags-2.2.2-h5888daf_1005.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/glog-0.7.1-hbabe93e_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.13-h59595ed_1003.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.1.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-9.0.0-hda332d3_1.conda @@ -5637,6 +5658,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.16-hb7c19ff_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h27087fc_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20240722.0-cxx17_h5888daf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-17.0.0-hef0f6b3_17_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-17.0.0-h5888daf_17_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-17.0.0-h5888daf_17_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-17.0.0-he882d9a_17_cpu.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-24_linux64_openblas.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.1.0-hb9d3cd8_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.1.0-hb9d3cd8_2.conda @@ -5644,11 +5670,15 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-24_linux64_openblas.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libclang-cpp19.1-19.1.0-default_hb5137d0_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libclang13-19.1.0-default_h9c6a7e4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcrc32c-1.1.2-h9c3ff4c_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-h4637d8d_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.10.1-hbbe4b11_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.21-h4bc722e_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libdrm-2.4.123-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20191231-he28a2e2_2.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-1.7.0-ha4b6fd6_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.3-h5888daf_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.2-h7f98852_5.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-14.1.0-h77fa898_1.conda @@ -5661,20 +5691,30 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libglvnd-1.7.0-ha4b6fd6_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-1.7.0-ha4b6fd6_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.1.0-h77fa898_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-2.29.0-h438788a_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-storage-2.29.0-h0121fbd_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.65.5-hf5c653b_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.17-hd590300_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.0.0-hd590300_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-24_linux64_openblas.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm19-19.1.0-ha7bfdaf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.58.0-h47da74e_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.27-pthreads_hac2b453_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libopengl-1.7.0-ha4b6fd6_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libparquet-17.0.0-h39682fd_17_cpu.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libpciaccess-0.18-hd590300_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.44-hadc24fc_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libpq-16.4-h2d7952a_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-5.27.5-h5b01275_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libre2-11-2023.09.01-hbbce691_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.46.1-hadc24fc_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.0-h0841786_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-14.1.0-hc0a3c3a_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-14.1.0-h4852527_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libthrift-0.20.0-h0e7cc3e_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.0-h6565414_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libutf8proc-2.8.0-h166bdaf_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.4.0-hd590300_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda @@ -5683,6 +5723,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.12.7-he7c6b58_4.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libxslt-1.1.39-h76b75d6_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-h4ab18f5_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.9.4-hcb278e6_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-2.1.5-py312h66e93f0_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/matplotlib-3.9.2-py312h7900ff3_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.9.2-py312hd3ec401_1.conda @@ -5694,13 +5735,17 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/numpydoc-1.8.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.2-h488ebb8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.3.2-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/orc-2.0.2-h690cf93_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.3-py312hf9745cd_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.44-hba22ea6_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pillow-10.4.0-py312h56024de_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.43.2-h59595ed_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/polars-1.8.2-py312hfe7c9be_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-17.0.0-py312h9cebb41_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-17.0.0-py312h9cafe31_1_cpu.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.18.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.1.4-pyhd8ed1ab_0.conda @@ -5716,12 +5761,15 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2024.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/qhull-2020.2-h434a139_5.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/qt6-main-6.7.2-hadfd74e_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/re2-2023.09.01-h77b4e00_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.5.3-h7b32b05_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/scikit-learn-1.5.2-py312h7a48858_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.14.1-py312h7d485d2_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-75.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.1-ha2e4443_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-8.0.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_0.conda @@ -5766,11 +5814,30 @@ environments: - pypi: . osx-64: - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-auth-0.7.31-hb28a666_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-cal-0.7.4-h8128ea2_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-common-0.9.28-h00291cd_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-compression-0.2.19-h8128ea2_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-event-stream-0.4.3-hcd1ed9e_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-http-0.8.10-h2f86973_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-io-0.14.18-hf9a0f1c_10.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-mqtt-0.10.6-h9d7d61c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-s3-0.6.6-hd01826e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-sdkutils-0.1.19-h8128ea2_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-checksums-0.1.20-h8128ea2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-crt-cpp-0.28.3-h21c617a_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-sdk-cpp-1.11.407-h2e282c2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-core-cpp-1.13.0-hf8dbe3c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-identity-cpp-1.8.0-h60298e3_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-storage-blobs-cpp-12.12.0-h646f05d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-storage-common-cpp-12.7.0-hf91904f_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-storage-files-datalake-cpp-12.11.0-h14965f0_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.14.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-1.1.0-h00291cd_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-bin-1.1.0-h00291cd_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.1.0-py312h5861a67_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-hfdf4475_7.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/c-ares-1.33.1-h44e7173_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/ca-certificates-2024.8.30-h8857fd0_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2024.8.30-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/cffi-1.17.1-py312hf857d28_0.conda @@ -5784,38 +5851,65 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/fonttools-4.54.1-py312hb553811_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/freetype-2.12.1-h60636b9_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/gflags-2.2.2-hac325c4_1005.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/glog-0.7.1-h2790a97_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.1.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.0.0-pyh9f0ad1d_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.0.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/kiwisolver-1.4.7-py312hc5c4d5f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/krb5-1.21.3-h37d8d59_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/lcms2-2.16-ha2f27b4_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/lerc-4.0.0-hb486fe8_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-64/libabseil-20240116.2-cxx17_hf036a51_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-17.0.0-h74c41f6_16_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-acero-17.0.0-hac325c4_16_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-dataset-17.0.0-hac325c4_16_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-substrait-17.0.0-hba007a9_16_cpu.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libblas-3.9.0-22_osx64_openblas.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlicommon-1.1.0-h00291cd_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlidec-1.1.0-h00291cd_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libbrotlienc-1.1.0-h00291cd_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libcblas-3.9.0-22_osx64_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libcrc32c-1.1.2-he49afe7_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-64/libcurl-8.10.1-h58e7537_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-19.1.0-hf95d169_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libdeflate-1.21-hfdf4475_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libedit-3.1.20191231-h0678c8f_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-64/libev-4.33-h10d778d_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libevent-2.1.12-ha90c15b_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.6.3-hac325c4_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.4.2-h0d85af4_5.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/osx-64/libgfortran-5.0.0-13_2_0_h97931a8_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libgfortran5-13.2.0-h2873a65_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libgoogle-cloud-2.29.0-hecd3d69_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libgoogle-cloud-storage-2.29.0-h8126ed0_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libgrpc-1.62.2-h384b2fc_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libiconv-1.17-hd75f5a5_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libjpeg-turbo-3.0.0-h0dc2134_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/liblapack-3.9.0-22_osx64_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libnghttp2-1.58.0-h64cf6d3_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libopenblas-0.3.27-openmp_h8869122_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libparquet-17.0.0-hf1b0f52_16_cpu.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libpng-1.6.44-h4b8f8c9_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libprotobuf-4.25.3-hd4aba4c_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libre2-11-2023.09.01-h81f5012_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.46.1-h4b8f8c9_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libssh2-1.11.0-hd019ec5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libthrift-0.20.0-h75589b3_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libtiff-4.7.0-h5f227bf_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libutf8proc-2.8.0-hb7f2c08_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/osx-64/libwebp-base-1.4.0-h10d778d_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libxcb-1.17.0-hf1f96e2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libxml2-2.12.7-heaf3512_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.1-h87427d6_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/llvm-openmp-18.1.8-h15ab845_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/lz4-c-1.9.4-hf0c8a7f_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/markupsafe-2.1.5-py312hb553811_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/matplotlib-3.9.2-py312hb401068_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/matplotlib-base-3.9.2-py312h30cc4df_1.conda @@ -5825,11 +5919,15 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/numpydoc-1.8.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/openjpeg-2.5.2-h7310d3a_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.3.2-hd23fc13_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/orc-2.0.2-h22b2039_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/pandas-2.2.3-py312h98e817e_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/pillow-10.4.0-py312h683ea77_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/polars-1.8.2-py312h088783b_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/pthread-stubs-0.4-h00291cd_1002.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pyarrow-17.0.0-py312h0be7463_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pyarrow-core-17.0.0-py312h63b501a_1_cpu.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.18.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.1.4-pyhd8ed1ab_0.conda @@ -5843,12 +5941,14 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/python_abi-3.12-5_cp312.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2024.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/qhull-2020.2-h3c5361c_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/re2-2023.09.01-hb168e87_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h9e318b2_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/scikit-learn-1.5.2-py312h9d777eb_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/scipy-1.14.1-py312he82a568_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-75.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-64/snappy-1.2.1-he1e6707_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-8.0.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_0.conda @@ -5873,11 +5973,30 @@ environments: - pypi: . osx-arm64: - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-auth-0.7.31-hc27b277_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-cal-0.7.4-h41dd001_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-common-0.9.28-hd74edd7_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-compression-0.2.19-h41dd001_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-event-stream-0.4.3-h40a8fc1_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-http-0.8.10-hf5a2c8c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-io-0.14.18-hc3cb426_10.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-mqtt-0.10.6-h3acc7b9_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-s3-0.6.6-hd16c091_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-sdkutils-0.1.19-h41dd001_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-checksums-0.1.20-h41dd001_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-crt-cpp-0.28.3-hdde83a9_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-sdk-cpp-1.11.407-h0455a66_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-core-cpp-1.13.0-hd01fc5c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-identity-cpp-1.8.0-h13ea094_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-blobs-cpp-12.12.0-hfde595f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-common-cpp-12.7.0-hcf3b6fd_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-files-datalake-cpp-12.11.0-h082e32e_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.14.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-1.1.0-hd74edd7_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-bin-1.1.0-hd74edd7_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.1.0-py312hde4cb15_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h99b78c6_7.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.33.1-hd74edd7_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ca-certificates-2024.8.30-hf0a4a13_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2024.8.30-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-1.17.1-py312h0fad829_0.conda @@ -5891,38 +6010,65 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fonttools-4.54.1-py312h024a12e_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/freetype-2.12.1-hadb7bae_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gflags-2.2.2-hf9b8971_1005.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/glog-0.7.1-heb240a5_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.1.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.0.0-pyh9f0ad1d_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.0.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/joblib-1.4.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/kiwisolver-1.4.7-py312h6142ec9_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.21.3-h237132a_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lcms2-2.16-ha0e7c42_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.0.0-h9a09cb3_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20240116.2-cxx17_h00cdb27_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-17.0.0-hc6a7651_16_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-acero-17.0.0-hf9b8971_16_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-dataset-17.0.0-hf9b8971_16_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-substrait-17.0.0-hbf8b706_16_cpu.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.9.0-24_osxarm64_openblas.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.1.0-hd74edd7_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.1.0-hd74edd7_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.1.0-hd74edd7_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.9.0-24_osxarm64_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcrc32c-1.1.2-hbdafb3b_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.10.1-h13a7ad3_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-19.1.0-ha82da77_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libdeflate-1.21-h99b78c6_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20191231-hc8eb9b7_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libevent-2.1.12-h2757513_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.6.3-hf9b8971_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.2-h3422bc3_5.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-5.0.0-13_2_0_hd922786_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-13.2.0-hf226fd6_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgoogle-cloud-2.29.0-hfa33a2f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgoogle-cloud-storage-2.29.0-h90fd6fa_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgrpc-1.62.2-h9c18a4f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.17-h0d3ecfb_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.0.0-hb547adb_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblapack-3.9.0-24_osxarm64_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.58.0-ha4dd798_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.27-openmp_h517c56d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libparquet-17.0.0-hf0ba9ef_16_cpu.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.44-hc14010f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libprotobuf-4.25.3-hc39d83c_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libre2-11-2023.09.01-h7b2c953_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.46.1-hc14010f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libssh2-1.11.0-h7a5bd25_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libthrift-0.20.0-h64651cc_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libtiff-4.7.0-h9c1d414_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libutf8proc-2.8.0-h1a8c8d9_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libwebp-base-1.4.0-h93a5062_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxcb-1.17.0-hdb1d25a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.12.7-h01dff8b_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-hfb2fe0b_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-18.1.8-hde57baf_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lz4-c-1.9.4-hb7217d7_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-2.1.5-py312h024a12e_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/matplotlib-3.9.2-py312h1f38498_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/matplotlib-base-3.9.2-py312h9bd0bc6_1.conda @@ -5932,11 +6078,15 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/numpydoc-1.8.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openjpeg-2.5.2-h9f1df11_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.3.2-h8359307_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/orc-2.0.2-h75dedd0_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pandas-2.2.3-py312hcd31e36_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pillow-10.4.0-py312h8609ca0_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/polars-1.8.2-py312hcc4db84_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pthread-stubs-0.4-hd74edd7_1002.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyarrow-17.0.0-py312ha814d7c_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyarrow-core-17.0.0-py312he20ac61_1_cpu.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.18.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.1.4-pyhd8ed1ab_0.conda @@ -5950,12 +6100,14 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python_abi-3.12-5_cp312.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2024.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/qhull-2020.2-h420ef59_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/re2-2023.09.01-h4cba328_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h92ec313_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/scikit-learn-1.5.2-py312h387f99c_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/scipy-1.14.1-py312heb3a901_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-75.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/snappy-1.2.1-hd02b534_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-8.0.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_0.conda @@ -5981,11 +6133,25 @@ environments: win-64: - conda: https://conda.anaconda.org/conda-forge/win-64/_openmp_mutex-4.5-2_gnu.conda - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-auth-0.7.31-hce3b56f_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-cal-0.7.4-hf1fc857_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-common-0.9.28-h2466b09_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-compression-0.2.19-hf1fc857_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-event-stream-0.4.3-hd0ca3c1_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-http-0.8.10-heca9ddf_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-io-0.14.18-h3831a8d_10.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-mqtt-0.10.6-hf27581b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-s3-0.6.6-h56e9fbd_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-sdkutils-0.1.19-hf1fc857_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-checksums-0.1.20-hf1fc857_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-crt-cpp-0.28.3-hd65be8e_5.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-sdk-cpp-1.11.407-h25dd3c2_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.14.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-1.1.0-h2466b09_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-bin-1.1.0-h2466b09_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.1.0-py312h275cf98_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h2466b09_7.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/c-ares-1.33.1-h2466b09_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ca-certificates-2024.8.30-h56e8100_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cairo-1.18.0-h32b962e_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2024.8.30-pyhd8ed1ab_0.conda @@ -6025,31 +6191,49 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.21.3-hdf4eb48_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lcms2-2.16-h67d730c_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lerc-4.0.0-h63175ca_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/libabseil-20240116.2-cxx17_he0c23c2_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-17.0.0-h5bcb882_16_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-acero-17.0.0-he0c23c2_16_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-dataset-17.0.0-he0c23c2_16_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-substrait-17.0.0-h1f0e801_16_cpu.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.9.0-24_win64_mkl.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlicommon-1.1.0-h2466b09_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlidec-1.1.0-h2466b09_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlienc-1.1.0-h2466b09_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.9.0-24_win64_mkl.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libclang13-19.1.0-default_ha5278ca_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcrc32c-1.1.2-h0e60522_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.10.1-h1ee3ff0_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.21-h2466b09_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libevent-2.1.12-h3671451_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.6.3-he0c23c2_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.2-h8ffe710_5.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/win-64/libgcc-14.1.0-h1383e82_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libglib-2.82.1-h7025463_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libgomp-14.1.0-h1383e82_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libgoogle-cloud-2.29.0-h5e7cea3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libgoogle-cloud-storage-2.29.0-he5eb982_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libgrpc-1.62.2-h5273850_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.17-hcfcfb64_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.0.0-hcfcfb64_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.9.0-24_win64_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libparquet-17.0.0-ha915800_16_cpu.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.44-h3ca93ac_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libprotobuf-4.25.3-h47a098d_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libre2-11-2023.09.01-hf8d8778_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.46.1-h2466b09_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libssh2-1.11.0-h7dfc565_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libthrift-0.20.0-hbe90ef8_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libtiff-4.7.0-hb151862_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libutf8proc-2.8.0-h82a8f57_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/win-64/libwebp-base-1.4.0-hcfcfb64_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libwinpthread-12.0.0.r4.gg4f2fc60ca-h57928b3_8.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libxcb-1.17.0-h0e4246c_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.12.7-h0f24e4e_4.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libxslt-1.1.39-h3df6e99_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/lz4-c-1.9.4-hcfcfb64_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-2.1.5-py312h4389bb4_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/matplotlib-3.9.2-py312h2e8e312_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/matplotlib-base-3.9.2-py312h90004f6_1.conda @@ -6059,13 +6243,17 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/numpydoc-1.8.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openjpeg-2.5.2-h3d672ee_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.3.2-h2466b09_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/orc-2.0.2-h784c2ca_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pandas-2.2.3-py312h72972c8_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pcre2-10.44-h3d7b363_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pillow-10.4.0-py312h381445a_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pixman-0.43.4-h63175ca_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/polars-1.8.2-py312ha0f2741_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pthread-stubs-0.4-h0e40799_1002.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyarrow-17.0.0-py312h7e22eef_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyarrow-core-17.0.0-py312h6a9c419_1_cpu.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.18.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.1.4-pyhd8ed1ab_0.conda @@ -6081,11 +6269,13 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2024.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/qhull-2020.2-hc790b64_5.conda - conda: https://conda.anaconda.org/conda-forge/win-64/qt6-main-6.7.2-h2fedb45_5.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/re2-2023.09.01-hd3b24a8_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/scikit-learn-1.5.2-py312h816cc57_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/scipy-1.14.1-py312h1f4e10d_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-75.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/snappy-1.2.1-h23299a8_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-8.0.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_0.conda @@ -12374,6 +12564,27 @@ packages: purls: [] size: 1124364 timestamp: 1720857589333 +- kind: conda + name: libabseil + version: '20240722.0' + build: cxx17_h5888daf_1 + build_number: 1 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20240722.0-cxx17_h5888daf_1.conda + sha256: 8f91429091183c26950f1e7ffa730e8632f0627ba35d2fccd71df31628c9b4e5 + md5: e1f604644fe8d78e22660e2fec6756bc + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libstdcxx >=13 + constrains: + - libabseil-static =20240722.0=cxx17* + - abseil-cpp =20240722.0 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 1310521 + timestamp: 1727295454064 - kind: conda name: libarrow version: 15.0.2 @@ -12622,14 +12833,14 @@ packages: - kind: conda name: libarrow version: 17.0.0 - build: had3b6fe_16_cpu + build: hc6a7651_16_cpu build_number: 16 - subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/libarrow-17.0.0-had3b6fe_16_cpu.conda - sha256: 9aa5598878cccc29de744ebc4b501c4a5a43332973edfdf0a19ddc521bd7248f - md5: c899e532e16be21570d32bc74ea3d34f + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-17.0.0-hc6a7651_16_cpu.conda + sha256: 1facd5aa7140031be0f68733ab5e413ea1505da40548e27a173b2407046f36b5 + md5: 05fecc4ae5930dc548327980a4bc7a83 depends: - - __glibc >=2.17,<3.0.a0 + - __osx >=11.0 - aws-crt-cpp >=0.28.3,<0.28.4.0a0 - aws-sdk-cpp >=1.11.407,<1.11.408.0a0 - azure-core-cpp >=1.13.0,<1.13.1.0a0 @@ -12637,17 +12848,15 @@ packages: - azure-storage-blobs-cpp >=12.12.0,<12.12.1.0a0 - azure-storage-files-datalake-cpp >=12.11.0,<12.11.1.0a0 - bzip2 >=1.0.8,<2.0a0 - - gflags >=2.2.2,<2.3.0a0 - glog >=0.7.1,<0.8.0a0 - libabseil * cxx17* - libabseil >=20240116.2,<20240117.0a0 - libbrotlidec >=1.1.0,<1.2.0a0 - libbrotlienc >=1.1.0,<1.2.0a0 - - libgcc >=13 + - libcxx >=17 - libgoogle-cloud >=2.29.0,<2.30.0a0 - libgoogle-cloud-storage >=2.29.0,<2.30.0a0 - libre2-11 >=2023.9.1,<2024.0a0 - - libstdcxx >=13 - libutf8proc >=2.8.0,<3.0a0 - libzlib >=1.3.1,<2.0a0 - lz4-c >=1.9.3,<1.10.0a0 @@ -12656,25 +12865,25 @@ packages: - snappy >=1.2.1,<1.3.0a0 - zstd >=1.5.6,<1.6.0a0 constrains: - - parquet-cpp <0.0a0 - - arrow-cpp <0.0a0 - apache-arrow-proc =*=cpu + - arrow-cpp <0.0a0 + - parquet-cpp <0.0a0 license: Apache-2.0 license_family: APACHE purls: [] - size: 8495428 - timestamp: 1726669963852 + size: 5318871 + timestamp: 1726669928492 - kind: conda name: libarrow version: 17.0.0 - build: hc6a7651_16_cpu - build_number: 16 - subdir: osx-arm64 - url: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-17.0.0-hc6a7651_16_cpu.conda - sha256: 1facd5aa7140031be0f68733ab5e413ea1505da40548e27a173b2407046f36b5 - md5: 05fecc4ae5930dc548327980a4bc7a83 + build: hef0f6b3_17_cpu + build_number: 17 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/libarrow-17.0.0-hef0f6b3_17_cpu.conda + sha256: 923c17caa8d6b56d2bd0bf53640601959b00647a48debd73164b2301715181bf + md5: ae0b3234c958c6071f6523fcde7afe99 depends: - - __osx >=11.0 + - __glibc >=2.17,<3.0.a0 - aws-crt-cpp >=0.28.3,<0.28.4.0a0 - aws-sdk-cpp >=1.11.407,<1.11.408.0a0 - azure-core-cpp >=1.13.0,<1.13.1.0a0 @@ -12682,15 +12891,17 @@ packages: - azure-storage-blobs-cpp >=12.12.0,<12.12.1.0a0 - azure-storage-files-datalake-cpp >=12.11.0,<12.11.1.0a0 - bzip2 >=1.0.8,<2.0a0 + - gflags >=2.2.2,<2.3.0a0 - glog >=0.7.1,<0.8.0a0 - libabseil * cxx17* - - libabseil >=20240116.2,<20240117.0a0 + - libabseil >=20240722.0,<20240723.0a0 - libbrotlidec >=1.1.0,<1.2.0a0 - libbrotlienc >=1.1.0,<1.2.0a0 - - libcxx >=17 + - libgcc >=13 - libgoogle-cloud >=2.29.0,<2.30.0a0 - libgoogle-cloud-storage >=2.29.0,<2.30.0a0 - libre2-11 >=2023.9.1,<2024.0a0 + - libstdcxx >=13 - libutf8proc >=2.8.0,<3.0a0 - libzlib >=1.3.1,<2.0a0 - lz4-c >=1.9.3,<1.10.0a0 @@ -12699,14 +12910,13 @@ packages: - snappy >=1.2.1,<1.3.0a0 - zstd >=1.5.6,<1.6.0a0 constrains: + - parquet-cpp <0.0a0 - apache-arrow-proc =*=cpu - arrow-cpp <0.0a0 - - parquet-cpp <0.0a0 license: Apache-2.0 - license_family: APACHE purls: [] - size: 5318871 - timestamp: 1726669928492 + size: 8530218 + timestamp: 1727706025186 - kind: conda name: libarrow-acero version: 15.0.2 @@ -12785,22 +12995,21 @@ packages: - kind: conda name: libarrow-acero version: 17.0.0 - build: h5888daf_16_cpu - build_number: 16 + build: h5888daf_17_cpu + build_number: 17 subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-17.0.0-h5888daf_16_cpu.conda - sha256: 0ff4c712c7c61e60708c6ef4f8158200059e0f63c25d0a54c8e4cca7bd153d86 - md5: 18f796aae018a26a20ac51d19de69115 + url: https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-17.0.0-h5888daf_17_cpu.conda + sha256: 6c95ff9332ccade12f839d44f370bed74c0b144fcbad10dab0f1791a4f2a27a8 + md5: bc27c1f44562481b579f5ceae1a3e51e depends: - __glibc >=2.17,<3.0.a0 - - libarrow 17.0.0 had3b6fe_16_cpu + - libarrow 17.0.0 hef0f6b3_17_cpu - libgcc >=13 - libstdcxx >=13 license: Apache-2.0 - license_family: APACHE purls: [] - size: 608267 - timestamp: 1726669999941 + size: 608175 + timestamp: 1727706066011 - kind: conda name: libarrow-acero version: 17.0.0 @@ -12942,24 +13151,23 @@ packages: - kind: conda name: libarrow-dataset version: 17.0.0 - build: h5888daf_16_cpu - build_number: 16 + build: h5888daf_17_cpu + build_number: 17 subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-17.0.0-h5888daf_16_cpu.conda - sha256: e500e0154cf3ebb41bed3bdf41bd0ff5e0a6b7527a46ba755c05e59c8036e442 - md5: 5400efd6bf101674e0ce170906a0f7cb + url: https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-17.0.0-h5888daf_17_cpu.conda + sha256: 0da4757d75ad0cd838810ab8af0ff52a0edd5d78e55aafa218f001413b93eb11 + md5: b45dc76a8a9a23a421b8f166d7e16684 depends: - __glibc >=2.17,<3.0.a0 - - libarrow 17.0.0 had3b6fe_16_cpu - - libarrow-acero 17.0.0 h5888daf_16_cpu + - libarrow 17.0.0 hef0f6b3_17_cpu + - libarrow-acero 17.0.0 h5888daf_17_cpu - libgcc >=13 - - libparquet 17.0.0 h39682fd_16_cpu + - libparquet 17.0.0 h39682fd_17_cpu - libstdcxx >=13 license: Apache-2.0 - license_family: APACHE purls: [] - size: 585061 - timestamp: 1726670063965 + size: 583849 + timestamp: 1727706140203 - kind: conda name: libarrow-dataset version: 17.0.0 @@ -13454,27 +13662,26 @@ packages: - kind: conda name: libarrow-substrait version: 17.0.0 - build: hf54134d_16_cpu - build_number: 16 + build: he882d9a_17_cpu + build_number: 17 subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-17.0.0-hf54134d_16_cpu.conda - sha256: 53f3d5f12c9ea557f33a4e1cf9067ce2dbb4211eff0a095574eeb7f0528bc044 - md5: 1cbc3fb1ee28c99e5f8c52920a7717a3 + url: https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-17.0.0-he882d9a_17_cpu.conda + sha256: 3346fb0097fb9c5e2509487ab950cddb388d0458e3a7594ba9989bfdea8868c1 + md5: 3c3084245ba8384c07eb4504c1961497 depends: - __glibc >=2.17,<3.0.a0 - libabseil * cxx17* - - libabseil >=20240116.2,<20240117.0a0 - - libarrow 17.0.0 had3b6fe_16_cpu - - libarrow-acero 17.0.0 h5888daf_16_cpu - - libarrow-dataset 17.0.0 h5888daf_16_cpu + - libabseil >=20240722.0,<20240723.0a0 + - libarrow 17.0.0 hef0f6b3_17_cpu + - libarrow-acero 17.0.0 h5888daf_17_cpu + - libarrow-dataset 17.0.0 h5888daf_17_cpu - libgcc >=13 - - libprotobuf >=4.25.3,<4.25.4.0a0 + - libprotobuf >=5.27.5,<5.27.6.0a0 - libstdcxx >=13 license: Apache-2.0 - license_family: APACHE purls: [] - size: 550960 - timestamp: 1726670093831 + size: 515946 + timestamp: 1727706175064 - kind: conda name: libasprintf version: 0.22.5 @@ -15080,6 +15287,32 @@ packages: purls: [] size: 1241649 timestamp: 1725640926284 +- kind: conda + name: libgoogle-cloud + version: 2.29.0 + build: h438788a_1 + build_number: 1 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-2.29.0-h438788a_1.conda + sha256: cf5c97fb1a270a072faae6decd7e74681e7ead99a1cec6325c8d7a7213bcb2d1 + md5: 3d27459264de681a74c0aebbbd3ecd8f + depends: + - __glibc >=2.17,<3.0.a0 + - libabseil * cxx17* + - libabseil >=20240722.0,<20240723.0a0 + - libcurl >=8.10.1,<9.0a0 + - libgcc >=13 + - libgrpc >=1.65.5,<1.66.0a0 + - libprotobuf >=5.27.5,<5.27.6.0a0 + - libstdcxx >=13 + - openssl >=3.3.2,<4.0a0 + constrains: + - libgoogle-cloud 2.29.0 *_1 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 1200532 + timestamp: 1727245497586 - kind: conda name: libgoogle-cloud version: 2.29.0 @@ -15175,6 +15408,30 @@ packages: purls: [] size: 781655 timestamp: 1725641060970 +- kind: conda + name: libgoogle-cloud-storage + version: 2.29.0 + build: h0121fbd_1 + build_number: 1 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-storage-2.29.0-h0121fbd_1.conda + sha256: 78e22048ab9bb554c4269f5e2a4ab9baae2c0f490418e0cdecd04e5c59130805 + md5: ea93fded95ddff7798e28954c446e22f + depends: + - __glibc >=2.17,<3.0.a0 + - libabseil + - libcrc32c >=1.1.2,<1.2.0a0 + - libcurl + - libgcc >=13 + - libgoogle-cloud 2.29.0 h438788a_1 + - libstdcxx >=13 + - libzlib >=1.3.1,<2.0a0 + - openssl + license: Apache-2.0 + license_family: Apache + purls: [] + size: 781418 + timestamp: 1727245657213 - kind: conda name: libgoogle-cloud-storage version: 2.29.0 @@ -15364,6 +15621,33 @@ packages: purls: [] size: 5016525 timestamp: 1713392846329 +- kind: conda + name: libgrpc + version: 1.65.5 + build: hf5c653b_0 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.65.5-hf5c653b_0.conda + sha256: d279abd46262e817c7a00aeb4df9b5ed4de38130130b248e2c50875e982f30fa + md5: 3b0048cabc6815a4d8874a0240519d32 + depends: + - __glibc >=2.17,<3.0.a0 + - c-ares >=1.32.3,<2.0a0 + - libabseil * cxx17* + - libabseil >=20240722.0,<20240723.0a0 + - libgcc >=13 + - libprotobuf >=5.27.5,<5.27.6.0a0 + - libre2-11 >=2023.9.1,<2024.0a0 + - libstdcxx >=13 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.3.2,<4.0a0 + - re2 + constrains: + - grpc-cpp =1.65.5 + license: Apache-2.0 + license_family: APACHE + purls: [] + size: 7229891 + timestamp: 1727200905306 - kind: conda name: libhwloc version: 2.11.1 @@ -16142,24 +16426,23 @@ packages: - kind: conda name: libparquet version: 17.0.0 - build: h39682fd_16_cpu - build_number: 16 + build: h39682fd_17_cpu + build_number: 17 subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/libparquet-17.0.0-h39682fd_16_cpu.conda - sha256: 09bc64111e5e1e9f5fee78efdd62592e01c681943fe6e91b369f6580dc8726c4 - md5: dd1fee2da0659103080fdd74004656df + url: https://conda.anaconda.org/conda-forge/linux-64/libparquet-17.0.0-h39682fd_17_cpu.conda + sha256: c94d49b69a3023cf37cff4e0ede610c45576c0dea5876f0550b210e6d354b092 + md5: dee1563caf816cb8f8f7e84622876487 depends: - __glibc >=2.17,<3.0.a0 - - libarrow 17.0.0 had3b6fe_16_cpu + - libarrow 17.0.0 hef0f6b3_17_cpu - libgcc >=13 - libstdcxx >=13 - libthrift >=0.20.0,<0.20.1.0a0 - openssl >=3.3.2,<4.0a0 license: Apache-2.0 - license_family: APACHE purls: [] - size: 1186069 - timestamp: 1726670048098 + size: 1188999 + timestamp: 1727706121710 - kind: conda name: libparquet version: 17.0.0 @@ -16399,6 +16682,27 @@ packages: purls: [] size: 2883090 timestamp: 1727161327039 +- kind: conda + name: libprotobuf + version: 5.27.5 + build: h5b01275_2 + build_number: 2 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-5.27.5-h5b01275_2.conda + sha256: 79ac9726cd0a1cb1ba335f7fc7ccac5f679a66d71d9553ca88a805b8787d55ce + md5: 66ed3107adbdfc25ba70454ba11e6d1e + depends: + - __glibc >=2.17,<3.0.a0 + - libabseil * cxx17* + - libabseil >=20240722.0,<20240723.0a0 + - libgcc >=13 + - libstdcxx >=13 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 2940269 + timestamp: 1727424395109 - kind: conda name: libre2-11 version: 2023.09.01 @@ -16461,6 +16765,28 @@ packages: purls: [] size: 184017 timestamp: 1708947106275 +- kind: conda + name: libre2-11 + version: 2023.09.01 + build: hbbce691_3 + build_number: 3 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/libre2-11-2023.09.01-hbbce691_3.conda + sha256: 239ca2319645308633ed773bda7ff1f153390ac84ee4e94955e0ed5be7e78967 + md5: f7f3ff4fff310fcac18769ce3f46e40a + depends: + - __glibc >=2.17,<3.0.a0 + - libabseil * cxx17* + - libabseil >=20240722.0,<20240723.0a0 + - libgcc >=13 + - libstdcxx >=13 + constrains: + - re2 2023.09.01.* + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 241063 + timestamp: 1727157471101 - kind: conda name: libre2-11 version: 2023.09.01 @@ -19362,6 +19688,30 @@ packages: purls: [] size: 1066349 timestamp: 1723760593232 +- kind: conda + name: orc + version: 2.0.2 + build: h690cf93_1 + build_number: 1 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/orc-2.0.2-h690cf93_1.conda + sha256: ce023f259ffd93b4678cc582fc4b15a8a991a7b8edd9def8b6838bf7e7962bec + md5: 0044701dd48af57d3d5467a704ef9ebd + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libprotobuf >=5.27.5,<5.27.6.0a0 + - libstdcxx >=13 + - libzlib >=1.3.1,<2.0a0 + - lz4-c >=1.9.3,<1.10.0a0 + - snappy >=1.2.1,<1.3.0a0 + - tzdata + - zstd >=1.5.6,<1.6.0a0 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 1184634 + timestamp: 1727242386732 - kind: conda name: orc version: 2.0.2 @@ -19711,7 +20061,7 @@ packages: license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/pandas?source=compressed-mapping + - pkg:pypi/pandas?source=hash-mapping size: 14470437 timestamp: 1726878887799 - kind: conda @@ -23354,6 +23704,22 @@ packages: purls: [] size: 26770 timestamp: 1708947220914 +- kind: conda + name: re2 + version: 2023.09.01 + build: h77b4e00_3 + build_number: 3 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/re2-2023.09.01-h77b4e00_3.conda + sha256: f3cd9d8c39b2b39da67bbf6630c807e5019dce496b21aea104f97b2264b5474a + md5: 173a62ebf031d6d53462f8f657c800bb + depends: + - libre2-11 2023.09.01 hbbce691_3 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 26605 + timestamp: 1727157480972 - kind: conda name: re2 version: 2023.09.01 @@ -24615,7 +24981,7 @@ packages: name: skrub version: 0.4.dev0 path: . - sha256: b7c67ca1782219d797b575bcda5068fa98117f9e47ffa9f926dc09a4ae4e026b + sha256: b05795442e9930d7741094828e9df85834daa7798eea5a23ba0d3b5316312389 requires_dist: - numpy>=1.23.5 - packaging>=23.1 @@ -24637,6 +25003,7 @@ packages: - sphinx-gallery ; extra == 'doc' - sphinxext-opengraph ; extra == 'doc' - statsmodels ; extra == 'doc' + - pyarrow ; extra == 'doc' - black==23.3.0 ; extra == 'lint' - ruff==0.4.8 ; extra == 'lint' - pre-commit ; extra == 'lint' diff --git a/pyproject.toml b/pyproject.toml index 28fc95e2c..340cacc06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ doc = [ "sphinx-gallery", "sphinxext-opengraph", "statsmodels", + "pyarrow", ] lint = [ "black==23.3.0", @@ -166,7 +167,7 @@ test = { cmd = "pytest -vsl --cov=skrub --cov-report=xml skrub" } [tool.pixi.environments] lint = ["lint"] doc = ["optional", "doc"] -test = ["test"] +test = ["optional", "test"] dev = ["dev", "optional", "doc", "lint", "test"] ci-py309-min-deps = ["py309", "min-dependencies", "test"] ci-py309-min-optional-deps = ["py309", "min-dependencies", "min-optional-dependencies", "test"] diff --git a/skrub/datasets/__init__.py b/skrub/datasets/__init__.py index d64775d3d..ea393dad3 100644 --- a/skrub/datasets/__init__.py +++ b/skrub/datasets/__init__.py @@ -1,6 +1,7 @@ from ._fetching import ( DatasetAll, DatasetInfoOnly, + fetch_credit_fraud, fetch_drug_directory, fetch_employee_salaries, fetch_figshare, @@ -32,6 +33,7 @@ "fetch_traffic_violations", "fetch_world_bank_indicator", "fetch_figshare", + "fetch_credit_fraud", "fetch_movielens", "get_data_dir", "make_deduplication_data", diff --git a/skrub/datasets/_fetching.py b/skrub/datasets/_fetching.py index 8e4b18691..cd8667c4d 100644 --- a/skrub/datasets/_fetching.py +++ b/skrub/datasets/_fetching.py @@ -25,6 +25,7 @@ from sklearn import __version__ as sklearn_version from sklearn.datasets import fetch_openml from sklearn.datasets._base import _sha256 +from sklearn.utils import Bunch from sklearn.utils.fixes import parse_version from skrub._utils import import_optional_dependency @@ -1096,3 +1097,45 @@ def fetch_movielens( load_dataframe=load_dataframe, data_directory=data_directory, ) + + +def fetch_credit_fraud(load_dataframe=True, data_directory=None): + """Fetch the credit fraud dataset from figshare. + + This is an imbalanced binary classification use-case. This dataset consists in + two tables: + + - baskets, containing the binary fraud target label + - products + + Baskets contain at least one product each, so aggregation then joining operations + are required to build a design matrix. + + More details on \ + `Figshare `_ + + Parameters + ---------- + load_dataframe : bool, default=True + Whether or not to load the dataset in memory after download. + + data_directory : str, default=None + The directory to which the dataset will be written during the download. + If None, the directory is set to ~/skrub_data. + """ + dataset_name_to_id = { + "products": "49176205", + "baskets": "49176202", + } + bunch = Bunch() + for dataset_name, figshare_id in dataset_name_to_id.items(): + dataset = fetch_figshare( + figshare_id, + load_dataframe=load_dataframe, + data_directory=data_directory, + ) + bunch[dataset_name] = dataset.X + bunch[f"source_{dataset_name}"] = dataset.source + bunch[f"path_{dataset_name}"] = dataset.path + + return bunch diff --git a/skrub/datasets/tests/test_fetching.py b/skrub/datasets/tests/test_fetching.py index 6539ca2db..8cfac5661 100644 --- a/skrub/datasets/tests/test_fetching.py +++ b/skrub/datasets/tests/test_fetching.py @@ -5,6 +5,7 @@ import pandas as pd import pytest +from pandas.testing import assert_frame_equal from skrub.datasets import _fetching @@ -219,3 +220,40 @@ def test_fetch_movielens(): ) mock_urlretrieve.assert_not_called() assert disk_loaded_info == returned_info + + +def test_fetch_credit_fraud(): + pytest.importorskip("pyarrow") + with TemporaryDirectory() as temp_dir: + try: + # Valid call + bunch = _fetching.fetch_credit_fraud( + data_directory=temp_dir, + ) + + except (ConnectionError, URLError): + pytest.skip( + "Exception: Skipping this test because we encountered an " + "issue probably related to an Internet connection problem. " + ) + return + + assert ( + bunch.source_products == "https://ndownloader.figshare.com/files/49176205" + ) + assert bunch.source_baskets == "https://ndownloader.figshare.com/files/49176202" + + assert_frame_equal(bunch.products, pd.read_parquet(bunch.path_products)) + assert_frame_equal(bunch.baskets, pd.read_parquet(bunch.path_baskets)) + + # Now that we have verified the file is on disk, we want to test + # whether calling the function again reads it from disk (it should) + # or queries the network again (it shouldn't). + with mock.patch("urllib.request.urlretrieve") as mock_urlretrieve: + # Same valid call as above + disk_bunch = _fetching.fetch_credit_fraud( + data_directory=temp_dir, + ) + mock_urlretrieve.assert_not_called() + assert_frame_equal(bunch.products, disk_bunch.products) + assert_frame_equal(bunch.baskets, disk_bunch.baskets)