Skip to content

ADD: Latent-Class model on preference cars dataset - #327

Open
VincentAuriau wants to merge 3 commits into
mainfrom
lc-cars
Open

ADD: Latent-Class model on preference cars dataset#327
VincentAuriau wants to merge 3 commits into
mainfrom
lc-cars

Conversation

@VincentAuriau

Copy link
Copy Markdown
Collaborator
  • small improvements

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces features to load car preferences with one-hot encoded categorical features or specific preprocessing in choice_learn/datasets/base.py, adds seed-based initialization to latent class and simple MNL models, and refactors the training and EM fit logic in latent_class_base_model.py. However, the review highlights several critical issues in the model changes: the new probability and weight calculations in latent_class_base_model.py are mathematically incorrect, contain redundant/dead code, and omit latent class weights from trainable_weights. Additionally, initializing self.weights in fit causes collisions and shape mismatches, clipping probabilities to 0.0 introduces numerical instability, and commenting out EM initialization degrades convergence. Finally, the review recommends vectorizing the DataFrame operations in the dataset loader and avoiding global seed modification inside model instantiation methods.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +563 to +588
sample_class_weight = tf.stack(probabilities, axis=1) / tf.reduce_sum(
tf.stack(probabilities, axis=1), axis=1, keepdims=True
)
sample_class_weight = tf.stack(
[
tf.gather_nd(prob, tf.stack([np.arange(0, len(choices)), choices], axis=1))
for prob in probabilities
],
axis=1,
)
# sample_class_weight = tf.expand_dims(tf.reduce_sum(sample_class_weight,
# axis=0, keepdims=True), axis=-1)
self._weights = sample_class_weight
self.__weights = tf.stack(
[
tf.gather_nd(prob, tf.stack([np.arange(0, len(choices)), choices], axis=1))
for prob in probabilities
],
axis=1,
)
# Summing over the latent classes
probabilities = tf.reduce_sum(probabilities, axis=0)

probabilities = tf.reduce_sum(
tf.stack(probabilities, axis=1) * tf.expand_dims(sample_class_weight, axis=-1),
axis=1,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

critical

There are several critical issues in this block:

  1. sample_class_weight is calculated on lines 563-565 and then immediately overwritten on lines 566-572, rendering the first calculation dead code.
  2. self.__weights is computed on lines 576-582 using the exact same expression as sample_class_weight, which is redundant.
  3. The calculation of probabilities on lines 585-588 uses sample_class_weight (the probability of the chosen item in each class) as the mixture weights. This is mathematically incorrect for computing the probability of choosing any item. It violates the latent class model formulation where the mixture weights must be the prior class probabilities (derived from self.latent_logits), not the posterior/chosen item probabilities. This also results in probabilities not summing to 1 across items, which violates the requirements of the Categorical Cross-Entropy loss.

Comment on lines +95 to 99
weights = []
# weights = [self.weights]
for model in self.models:
weights += model.trainable_weights
return weights

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The latent class weights (either self.latent_logits or self.weights) are omitted from trainable_weights. This prevents the optimizer from updating the mixture weights during gradient descent (_fit_with_gd), meaning the class probabilities will remain static at their initial random values.

Comment on lines +266 to +271
self.weights = tf.Variable(
tf.random_normal_initializer(0.0, 0.08)(
shape=(self.n_latent_classes - 1, len(choice_dataset))
),
name="Latent-Logits",
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Initializing self.weights as a tf.Variable of shape (self.n_latent_classes - 1, len(choice_dataset)) in fit causes several issues:

  1. It collides with self.weights used in EM fit (_em_fit), where self.weights is a tensor of shape (len(choice_dataset), n_latent_classes) representing the posterior class probabilities.
  2. In L-BFGS training (_lbfgs_train_step), self.weights is appended to trainable_weights, but in _fit_with_lbfgs, self.latent_logits (shape (n_latent_classes - 1,)) is appended to init. This shape mismatch will cause tf.dynamic_stitch to crash.
  3. Having observation-specific latent parameters of shape (n_latent_classes - 1, len(choice_dataset)) without any sharing or regularization is extremely prone to overfitting and makes out-of-sample prediction impossible.

Comment on lines +980 to +989
for i in range(1, 7):
for car_type in ["regcar", "sportcar", "sportuv", "stwagon", "truck", "van"][:-1]:
cars_df[f"{car_type}{i}"] = cars_df.apply(
lambda row: row[f"type{i}"] == car_type, axis=1
).astype(int)
for i in range(1, 7):
for fuel_type in ["cng", "electric", "gasoline", "methanol"][:-1]:
cars_df[f"{fuel_type}{i}"] = cars_df.apply(
lambda row: row[f"fuel{i}"] == fuel_type, axis=1
).astype(int)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Using .apply(lambda row: ...) row-by-row on a pandas DataFrame is highly inefficient and slow. Since the operations are simple equality checks, they can be fully vectorized using pandas' native element-wise comparison, which is significantly faster.

Suggested change
for i in range(1, 7):
for car_type in ["regcar", "sportcar", "sportuv", "stwagon", "truck", "van"][:-1]:
cars_df[f"{car_type}{i}"] = cars_df.apply(
lambda row: row[f"type{i}"] == car_type, axis=1
).astype(int)
for i in range(1, 7):
for fuel_type in ["cng", "electric", "gasoline", "methanol"][:-1]:
cars_df[f"{fuel_type}{i}"] = cars_df.apply(
lambda row: row[f"fuel{i}"] == fuel_type, axis=1
).astype(int)
for i in range(1, 7):
for car_type in ["regcar", "sportcar", "sportuv", "stwagon", "truck", "van"][:-1]:
cars_df[f"{car_type}{i}"] = (cars_df[f"type{i}"] == car_type).astype(int)
for i in range(1, 7):
for fuel_type in ["cng", "electric", "gasoline", "methanol"][:-1]:
cars_df[f"{fuel_type}{i}"] = (cars_df[f"fuel{i}"] == fuel_type).astype(int)

Comment on lines 832 to 834
return tf.clip_by_value(
predicted_probas / np.sum(predicted_probas, axis=1, keepdims=True), 1e-6, 1
predicted_probas / np.sum(predicted_probas, axis=1, keepdims=True), 0.0, 1.0
), loss

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Clipping the predicted probabilities to 0.0 instead of a small positive epsilon (like 1e-6 or 1e-15) can lead to numerical instability. Specifically, in the M-step (_maximization), tf.math.log is computed on the latent probabilities. If any class probability becomes exactly 0.0 due to this clipping, tf.math.log will receive 0.0 and return -inf or NaN, causing the training to fail.

Suggested change
return tf.clip_by_value(
predicted_probas / np.sum(predicted_probas, axis=1, keepdims=True), 1e-6, 1
predicted_probas / np.sum(predicted_probas, axis=1, keepdims=True), 0.0, 1.0
), loss
return tf.clip_by_value(
predicted_probas / np.sum(predicted_probas, axis=1, keepdims=True), 1e-15, 1.0
), loss

Comment on lines +885 to +891
# init_sample_weight = np.random.rand(self.n_latent_classes, len(choice_dataset))
# init_sample_weight = np.clip(
# init_sample_weight / np.sum(init_sample_weight, axis=0, keepdims=True), 1e-6, 1
# )
# for i, model in enumerate(self.models):
# # model.instantiate()
# model.fit(choice_dataset, sample_weight=init_sample_weight[i], verbose=verbose)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Commenting out the initialization of the latent models in EM fit means that the models will start with raw random weights instead of being pre-fitted on a random partition of the dataset. This can significantly degrade the convergence speed and the quality of the local optimum found by the EM algorithm. If this was intentional, please provide an alternative initialization scheme or document why it was removed.

Comment on lines +111 to 115
tf.random.set_seed(base_seed)
self.latent_logits = tf.Variable(
tf.random_normal_initializer(0.0, 0.02, seed=42)(shape=(self.n_latent_classes - 1,)),
tf.random_normal_initializer(0.0, 0.02)(shape=(self.n_latent_classes - 1,)),
name="Latent-Logits",
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Calling tf.random.set_seed(base_seed) inside a model's instantiation method is a bad practice in library code because it modifies the global TensorFlow random number generator state. This can have unintended side effects on other parts of the user's program or other models being instantiated concurrently/subsequently. It is better to pass the seed directly to the initializer (e.g., tf.random_normal_initializer(0.0, 0.02, seed=base_seed)) or use a stateful tf.random.Generator.

list of tf.Tensor
List of the weights created coresponding to the specification.
"""
tf.random.set_seed(base_seed)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Calling tf.random.set_seed(base_seed) inside a model's instantiation method is a bad practice in library code because it modifies the global TensorFlow random number generator state. This can have unintended side effects on other parts of the user's program or other models being instantiated concurrently/subsequently. It is better to pass the seed directly to the initializer (e.g., tf.random_normal_initializer(0.0, 0.02, seed=base_seed)) or use a stateful tf.random.Generator.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Coverage

Coverage Report for Python 3.10
FileStmtsMissCoverMissing
choice_learn
   __init__.py20100% 
   tf_ops.py64198%286
choice_learn/basket_models
   __init__.py40100% 
   alea_carta.py155398%111, 136, 326
   base_basket_model.py2482988%115–116, 127, 145, 189, 259, 381, 489, 589–591, 680, 785, 793, 803, 851, 854–864, 925–928, 967–968
   basic_attention_model.py89496%424, 427, 433, 440
   self_attention_model.py133993%71, 73, 75, 450–454, 651
   shopper.py184995%130, 159, 325, 345, 360, 363, 377, 489, 618
choice_learn/basket_models/data
   __init__.py20100% 
   basket_dataset.py1956268%74–77, 223–231, 299–301, 411, 544–580, 608–648, 671, 678–683, 753–764, 812
   preprocessing.py947817%43–45, 128–364
choice_learn/basket_models/datasets
   __init__.py30100% 
   badminton.py81693%62, 194–199, 247
   bakery.py38392%47, 51, 61
choice_learn/basket_models/utils
   __init__.py00100% 
   permutation.py22195%37
choice_learn/data
   __init__.py30100% 
   choice_dataset.py6493395%198, 250, 283, 421, 463–464, 589, 724, 738, 840, 842, 937, 957–961, 1140, 1159–1161, 1179–1181, 1209, 1214, 1223, 1240, 1281, 1293, 1307, 1346, 1361, 1366, 1395, 1408, 1443–1444
   indexer.py2412390%20, 31, 45, 60–67, 202–204, 219–230, 265, 291, 582
   storage.py161696%22, 33, 51, 56, 61, 71
   store.py72720%3–275
choice_learn/datasets
   __init__.py40100% 
   base.py4172095%42–43, 153–154, 714, 979–1006, 1017–1027
   expedia.py1028319%37–301
   tafeng.py490100% 
choice_learn/datasets/data
   __init__.py00100% 
choice_learn/models
   __init__.py14286%15–16
   base_model.py3353590%145, 187, 289, 297, 303, 312, 352, 356–357, 362, 391, 395–396, 413, 426, 434, 475–476, 485–486, 587, 589, 605, 609, 611, 734–735, 935, 939–953
   baseline_models.py490100% 
   conditional_logit.py2692690%49, 52, 54, 85, 88, 91–95, 98–102, 136, 206, 212–216, 351, 388, 445, 520–526, 651, 685, 822, 826
   halo_mnl.py124298%186, 374
   latent_class_base_model.py2877972%55–61, 280–286, 294, 328–329, 331–336, 395–402, 424–441, 489–507, 643, 662, 702–738, 752, 757, 788–789, 809–810, 898–899, 917–932, 951–969, 995–1004
   latent_class_mnl.py63690%263–267, 302
   learning_mnl.py67396%157, 182, 188
   nested_logit.py2911296%55, 77, 160, 269, 351, 484, 530, 600, 679, 848, 900, 904
   reslogit.py132695%285, 360, 369, 374, 382, 432
   rumnet.py236399%748–751, 982
   simple_mnl.py140696%170, 278, 350, 358, 360, 362
   tastenet.py94397%142, 180, 188
choice_learn/toolbox
   __init__.py00100% 
   assortment_optimizer.py27678%28–30, 93–95, 160–162
   gurobi_opt.py2382380%3–675
   or_tools_opt.py2301195%103, 107, 296–305, 315, 319, 607, 611
choice_learn/utils
   metrics.py116794%73, 151–153, 219–221, 287–289
TOTAL572488785% 

Tests Skipped Failures Errors Time
228 0 💤 3 ❌ 0 🔥 5m 6s ⏱️

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Coverage

Coverage Report for Python 3.9
FileStmtsMissCoverMissing
choice_learn
   __init__.py20100% 
   tf_ops.py64198%286
choice_learn/basket_models
   __init__.py40100% 
   alea_carta.py155398%111, 136, 326
   base_basket_model.py2482988%115–116, 127, 145, 189, 259, 381, 489, 589–591, 680, 785, 793, 803, 851, 854–864, 925–928, 967–968
   basic_attention_model.py89496%424, 427, 433, 440
   self_attention_model.py133993%71, 73, 75, 450–454, 651
   shopper.py184995%130, 159, 325, 345, 360, 363, 377, 489, 618
choice_learn/basket_models/data
   __init__.py20100% 
   basket_dataset.py1956268%74–77, 223–231, 299–301, 411, 544–580, 608–648, 671, 678–683, 753–764, 812
   preprocessing.py947817%43–45, 128–364
choice_learn/basket_models/datasets
   __init__.py30100% 
   badminton.py81693%62, 194–199, 247
   bakery.py38392%47, 51, 61
choice_learn/basket_models/utils
   __init__.py00100% 
   permutation.py22195%37
choice_learn/data
   __init__.py30100% 
   choice_dataset.py6493395%198, 250, 283, 421, 463–464, 589, 724, 738, 840, 842, 937, 957–961, 1140, 1159–1161, 1179–1181, 1209, 1214, 1223, 1240, 1281, 1293, 1307, 1346, 1361, 1366, 1395, 1408, 1443–1444
   indexer.py2412390%20, 31, 45, 60–67, 202–204, 219–230, 265, 291, 582
   storage.py161696%22, 33, 51, 56, 61, 71
   store.py72720%3–275
choice_learn/datasets
   __init__.py40100% 
   base.py4172095%42–43, 153–154, 714, 979–1006, 1017–1027
   expedia.py1028319%37–301
   tafeng.py490100% 
choice_learn/datasets/data
   __init__.py00100% 
choice_learn/models
   __init__.py14286%15–16
   base_model.py3353490%145, 187, 289, 297, 303, 312, 352, 356–357, 362, 391, 395–396, 413, 426, 434, 475–476, 485–486, 587, 589, 605, 609, 734–735, 935, 939–953
   baseline_models.py490100% 
   conditional_logit.py2692690%49, 52, 54, 85, 88, 91–95, 98–102, 136, 206, 212–216, 351, 388, 445, 520–526, 651, 685, 822, 826
   halo_mnl.py124298%186, 374
   latent_class_base_model.py2877972%55–61, 280–286, 294, 328–329, 331–336, 395–402, 424–441, 489–507, 643, 662, 702–738, 752, 757, 788–789, 809–810, 898–899, 917–932, 951–969, 995–1004
   latent_class_mnl.py63690%263–267, 302
   learning_mnl.py67396%157, 182, 188
   nested_logit.py2911296%55, 77, 160, 269, 351, 484, 530, 600, 679, 848, 900, 904
   reslogit.py132795%122, 285, 360, 369, 374, 382, 432
   rumnet.py236399%748–751, 982
   simple_mnl.py140696%170, 278, 350, 358, 360, 362
   tastenet.py94397%142, 180, 188
choice_learn/toolbox
   __init__.py00100% 
   assortment_optimizer.py27678%28–30, 93–95, 160–162
   gurobi_opt.py2362360%3–675
   or_tools_opt.py2301195%103, 107, 296–305, 315, 319, 607, 611
choice_learn/utils
   metrics.py1167337%71–105, 115, 149–173, 183, 217–240, 250, 285–309, 319
TOTAL572295183% 

Tests Skipped Failures Errors Time
228 0 💤 3 ❌ 0 🔥 6m 0s ⏱️

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Coverage

Coverage Report for Python 3.11
FileStmtsMissCoverMissing
choice_learn
   __init__.py20100% 
   tf_ops.py64198%286
choice_learn/basket_models
   __init__.py40100% 
   alea_carta.py155398%111, 136, 326
   base_basket_model.py2482988%115–116, 127, 145, 189, 259, 381, 489, 589–591, 680, 785, 793, 803, 851, 854–864, 925–928, 967–968
   basic_attention_model.py89496%424, 427, 433, 440
   self_attention_model.py133993%71, 73, 75, 450–454, 651
   shopper.py184995%130, 159, 325, 345, 360, 363, 377, 489, 618
choice_learn/basket_models/data
   __init__.py20100% 
   basket_dataset.py1956268%74–77, 223–231, 299–301, 411, 544–580, 608–648, 671, 678–683, 753–764, 812
   preprocessing.py947817%43–45, 128–364
choice_learn/basket_models/datasets
   __init__.py30100% 
   badminton.py81693%62, 194–199, 247
   bakery.py38392%47, 51, 61
choice_learn/basket_models/utils
   __init__.py00100% 
   permutation.py22195%37
choice_learn/data
   __init__.py30100% 
   choice_dataset.py6493395%198, 250, 283, 421, 463–464, 589, 724, 738, 840, 842, 937, 957–961, 1140, 1159–1161, 1179–1181, 1209, 1214, 1223, 1240, 1281, 1293, 1307, 1346, 1361, 1366, 1395, 1408, 1443–1444
   indexer.py2412390%20, 31, 45, 60–67, 202–204, 219–230, 265, 291, 582
   storage.py161696%22, 33, 51, 56, 61, 71
   store.py72720%3–275
choice_learn/datasets
   __init__.py40100% 
   base.py4172095%42–43, 153–154, 714, 979–1006, 1017–1027
   expedia.py1028319%37–301
   tafeng.py490100% 
choice_learn/datasets/data
   __init__.py00100% 
choice_learn/models
   __init__.py14286%15–16
   base_model.py3353689%145, 187, 289, 297, 303, 312, 352, 356–357, 362, 391, 395–396, 413, 426, 434, 475–476, 485–486, 587, 589, 605, 609, 611, 734–735, 908, 935, 939–953
   baseline_models.py490100% 
   conditional_logit.py2692690%49, 52, 54, 85, 88, 91–95, 98–102, 136, 206, 212–216, 351, 388, 445, 520–526, 651, 685, 822, 826
   halo_mnl.py124298%186, 374
   latent_class_base_model.py2877972%55–61, 280–286, 294, 328–329, 331–336, 395–402, 424–441, 489–507, 643, 662, 702–738, 752, 757, 788–789, 809–810, 898–899, 917–932, 951–969, 995–1004
   latent_class_mnl.py63690%263–267, 302
   learning_mnl.py67396%157, 182, 188
   nested_logit.py2911296%55, 77, 160, 269, 351, 484, 530, 600, 679, 848, 900, 904
   reslogit.py132695%285, 360, 369, 374, 382, 432
   rumnet.py236399%748–751, 982
   simple_mnl.py140696%170, 278, 350, 358, 360, 362
   tastenet.py94397%142, 180, 188
choice_learn/toolbox
   __init__.py00100% 
   assortment_optimizer.py27678%28–30, 93–95, 160–162
   gurobi_opt.py2382380%3–675
   or_tools_opt.py2301195%103, 107, 296–305, 315, 319, 607, 611
choice_learn/utils
   metrics.py116794%73, 151–153, 219–221, 287–289
TOTAL572488884% 

Tests Skipped Failures Errors Time
228 0 💤 3 ❌ 0 🔥 6m 27s ⏱️

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Coverage

Coverage Report for Python 3.12
FileStmtsMissCoverMissing
choice_learn
   __init__.py20100% 
   tf_ops.py64198%286
choice_learn/basket_models
   __init__.py40100% 
   alea_carta.py155398%111, 136, 326
   base_basket_model.py2482988%115–116, 127, 145, 189, 259, 381, 489, 589–591, 680, 785, 793, 803, 851, 854–864, 925–928, 967–968
   basic_attention_model.py89496%424, 427, 433, 440
   self_attention_model.py133993%71, 73, 75, 450–454, 651
   shopper.py184995%130, 159, 325, 345, 360, 363, 377, 489, 618
choice_learn/basket_models/data
   __init__.py20100% 
   basket_dataset.py1956268%74–77, 223–231, 299–301, 411, 544–580, 608–648, 671, 678–683, 753–764, 812
   preprocessing.py947817%43–45, 128–364
choice_learn/basket_models/datasets
   __init__.py30100% 
   badminton.py81693%62, 194–199, 247
   bakery.py38392%47, 53, 61
choice_learn/basket_models/utils
   __init__.py00100% 
   permutation.py22195%37
choice_learn/data
   __init__.py30100% 
   choice_dataset.py6493395%198, 250, 283, 421, 463–464, 589, 724, 738, 840, 842, 937, 957–961, 1140, 1159–1161, 1179–1181, 1209, 1214, 1223, 1240, 1281, 1293, 1307, 1346, 1361, 1366, 1395, 1408, 1443–1444
   indexer.py2412390%20, 31, 45, 60–67, 202–204, 219–230, 265, 291, 582
   storage.py161696%22, 33, 51, 56, 61, 71
   store.py72720%3–275
choice_learn/datasets
   __init__.py40100% 
   base.py4172095%42–43, 153–154, 714, 979–1006, 1017–1027
   expedia.py1028319%37–301
   tafeng.py490100% 
choice_learn/datasets/data
   __init__.py00100% 
choice_learn/models
   __init__.py14286%15–16
   base_model.py3353689%145, 187, 289, 297, 303, 312, 352, 356–357, 362, 391, 395–396, 413, 426, 434, 475–476, 485–486, 587, 589, 605, 609, 611, 734–735, 908, 935, 939–953
   baseline_models.py490100% 
   conditional_logit.py2692690%49, 52, 54, 85, 88, 91–95, 98–102, 136, 206, 212–216, 351, 388, 445, 520–526, 651, 685, 822, 826
   halo_mnl.py124298%186, 374
   latent_class_base_model.py2877972%55–61, 280–286, 294, 328–329, 331–336, 395–402, 424–441, 489–507, 643, 662, 702–738, 752, 757, 788–789, 809–810, 898–899, 917–932, 951–969, 995–1004
   latent_class_mnl.py63690%263–267, 302
   learning_mnl.py67396%157, 182, 188
   nested_logit.py2911296%55, 77, 160, 269, 351, 484, 530, 600, 679, 848, 900, 904
   reslogit.py132695%285, 360, 369, 374, 382, 432
   rumnet.py236399%748–751, 982
   simple_mnl.py140696%170, 278, 350, 358, 360, 362
   tastenet.py94397%142, 180, 188
choice_learn/toolbox
   __init__.py00100% 
   assortment_optimizer.py27678%28–30, 93–95, 160–162
   gurobi_opt.py2382380%3–675
   or_tools_opt.py2301195%103, 107, 296–305, 315, 319, 607, 611
choice_learn/utils
   metrics.py116794%73, 151–153, 219–221, 287–289
TOTAL572488884% 

Tests Skipped Failures Errors Time
228 0 💤 3 ❌ 0 🔥 6m 59s ⏱️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant