Explicit error terms production code#1064
Conversation
…ple to test loc sampling with MC
addresses code review, updates to latest main, removes ARC test as discussed
|
@dhensle target is now main as discussed earlier. I cannot resolve the copilot suggestions, but they are all fixed. |
dhensle
left a comment
There was a problem hiding this comment.
Looks good! Documentation is thorough. Just a couple of things for you to review.
| # and do not choose others. Let's use 3.0 as large value here. | ||
| probs_or_utils[choice_col] = np.where( | ||
| probs_or_utils[choice_col] > logit.UTIL_MIN, | ||
| 3.0, |
There was a problem hiding this comment.
Suggest we parameterize this 3.0 value in logit.py (named like UTIL_LARGE_ENOUGH or something). Shows up in interaction_sample_simulate too in the same context.
There was a problem hiding this comment.
Done. Replaced all three occurrences and added a comment on usage too.
| ) in chunk.adaptive_chunked_choosers(state, indirect_tours, trace_label): | ||
| # Sort the choosers and get the schedule alternatives | ||
| choosers = choosers.sort_index() | ||
| # FIXME-EET: For explicit error term choices, we need a stable alternative ID. Currently, we use |
There was a problem hiding this comment.
Is this fix something that should get just get implemented right now? Seem better than just leaving this fixme in here for a who-knows amount of time. (The proposed fix make sense to me)
There was a problem hiding this comment.
Ideally yes, but it is not as simple as the idea suggests and therefore not within budget. Something to discuss in the next engineering meeting.
| Sharrow settings for a component. | ||
| """ | ||
|
|
||
| sample_method: None | Literal["monte_carlo", "eet", "poisson"] = None |
There was a problem hiding this comment.
Do we need to be careful about the sampling method for estimation? The actual choice method (eet vs monte-carlo) shouldn't matter because the choice just gets overwritten by the survey choice anyways. But is the sampling methodology important?
I don't think it should matter, but would like to double check.
There was a problem hiding this comment.
Currently, destination choice logsums are different for Poisson because of the different meaning of prob (Poisson reports inclusion probabilities). That could be worked around, and in general I don't think the method matters theoretically, only in the sense that some additional things would need to be implemented. I do not see any advantage to this currently, and this is already a very large PR, so I would suggest just requiring MC for estimation. Looking at Jeff's update of the EET branch at the start of this phase, it looks like he added a "estimation needs MC sampling" explicitly (https://github.com/ActivitySim/activitysim/pull/1042/changes#diff-9ec5e393569d30759b9ced82065107dd053d6f1901a4ad20df23825f19fe5f22). @jpn-- anything I missed here?
| sample_compute_settings = sample_compute_settings.subcomponent_settings( | ||
| "sample" | ||
| ) | ||
| return _resolve_sample_method(state, sample_compute_settings) |
There was a problem hiding this comment.
Combining this into a single resolve_sample_method seems cleaner. I don't think we need another function here.
| sampling_method = _resolve_sample_method(state, compute_settings) | ||
|
|
||
| # Estimation requires MC sampling and MC choice for now | ||
| if estimation.manager.enabled and sampling_method != "monte_carlo": |
There was a problem hiding this comment.
If this is true, then we should enforce it in the settings. But I am not sure why monte_carlo should be required for estimation.
There was a problem hiding this comment.
Regarding if monte_carlo is required, see my answer in base.py. On the validation I agree in principle, but the sample_method can be set globally and per-component in compute_settings, and it looks like enabling estimation comes from a separate config, which is only read on first use in a model.
|
|
||
| chunk_sizer.log_df(trace_label, "choices", choices) | ||
| # resulting pandas Int64Index has one element per chooser row and is in same order as choosers | ||
| choices = alternatives[choice_column].take(positions + first_row_offsets) |
| n = 0 | ||
| active_row_positions = np.arange(len(probs), dtype=np.int64) | ||
|
|
||
| while active_row_positions.size > 0: |
There was a problem hiding this comment.
I am not sure I really like how these retries are being applied. I see two issues:
First, if an alternative gets selected in a re-try, I think the inclusion probability correction np.log(pick_count / prob) is slightly off:
The current correction is
np.log(1/
where
For n > 1, the prob should be corrected to include the probability of an empty sample
as the probability of selection would be
I think this correction would be generally quite small, but the rows that actually require retries are the rows that would comparatively have a large
Second, setting the prob = 1 for the selected alternatives that pass the fallback method again isn't quite right as the correction factor therefore becomes np.log(1/prob) = log(1/1) = 0. But, the probability of being selected isn't 1 -- it's whatever the selection probability is in the fallback method.
Can we just avoid retrying and if we do not return anything in the first iteration, just select all alternatives that have a non-zero probability? Then their selection prob is 1 and the correction factor becomes moot and we don't waste time and complexity iterating?
I am also wondering whether we should be checking for a "zero probability" error here before we start this looping (or choosing) to catch cases where no alternative is available.
It's difficult for me to say how much any of this matters in practice....
There was a problem hiding this comment.
Yes, these are good points and I agree on the math. The variable-number retries also potentially could mess up error term alignment. In practice, these issues did not show up for sample sizes of 30 or above and "peaky-enough" alternative sets, which is what is currently used in all models I know of, but I agree that it's good to fix this. I am a bit hesitant to add all alternatives here because this would slow down things considerably due to mode choice logsums being very slow. However, there is a solution: Choose the sample_size highest-utility alternatives as a fallback set when Poisson sampling came up empty. Those two events are mutually exclusive and the total inclusion probability becomes
There was a problem hiding this comment.
Regarding checking for zero probabilities, that's handled in utils to probs
There was a problem hiding this comment.
On another note, Poisson sampling could also be replaced with probabilistic top-k sampling, there is a known closed-form expression for inclusion probabilities and this would be easy to implement and some preliminary tests suggested that it has the same level of stability as Poisson sampling. However, with the above changes I do not think this is necessary.
| chunk_sizer.log_df(trace_label, "nested_exp_utilities", nested_exp_utilities) | ||
| if state.settings.use_explicit_error_terms: | ||
| raw_utilities = logit.validate_utils( | ||
| state, raw_utilities, allow_zero_probs=True, trace_label=trace_label |
There was a problem hiding this comment.
should this allow_zero_probs be True? Shouldn't this be passed in from our global/model-level settings?
There was a problem hiding this comment.
It should not be, good catch. Fixed.
| row_mask = masked[row_num] | ||
| candidate_values = np.full(n_alts, -np.inf, dtype=np.float64) | ||
| active = ~row_mask | ||
| if active.any(): |
There was a problem hiding this comment.
if not active.any() (i.e. there are no available choices), then the argmax below just selects the first one? I am concerned that we aren't correctly catching the "zero probability" case in this function. We should be reporting zero probabilities and skipping their choices if that setting is enabled.
There was a problem hiding this comment.
I thought we can just rely on validate_utils always being called upstream in simulate, sample_simulate, etc. On second thought, this is not a good pattern. For MC there is a utils_to_probs step outside make_sample_choices (the equivalent to validate_utils), and then there is an argument called allow_bad_probs in make_choices. I've added a corresponding argument to make_choices_explicit_error_term that mirrors the allow_bad_probs functionality. There's also more documentation now to make things clearer.
| positions = np.empty(n_rows, dtype=np.int64) | ||
|
|
||
| if alt_nrs_df is not None: | ||
| assert alt_nrs_df.shape == utilities.shape |
There was a problem hiding this comment.
Suggest we check the actual index (and columns?) here instead of just the shape.
…MC in make_choices
Code review changes
|
Thanks for the thorough review @dhensle, some good suggestions there. Your sample correction factor for Poisson retries led to another idea which does not require any retries and removes potential error-term misalignment between scenarios, let me know what you think. |
This PR brings the explicit error term (EET) work to production standard. It contains the following major changes compared to the PoC implementation:
1. It adds testing and documentation for EET
2. It decouples the sampling method from the simulation method and adds Poisson sampling (based on @m-richards implementation in #1065), including tests and documentation. It also addresses runtime and memory issues with the sampling method "eet".
3. It removes several inconsistencies in the EET simulation branch where edge cases could have led to unexpected changes in choices for individual choosers due to non-alignment of error terms.
4. It consistently implements EET for nested logit models
Note that the default simulation method remains Monte Carlo, with all existing unit and integration tests unchanged and passing. Users should therefore not see any differences in their model runs unless explicitly opting in to EET. This is done by adding
use_explicit_error_terms: Trueto the settings. This is a drop-in replacement, with default settings leading to a 3-10% runtime increase of a single demand model run for the models we have tested so far.This is a large PR and I will add details in comments below so we can keep discussions focused.