ranklens.offpolicy¶
ranklens.offpolicy
¶
Off-policy evaluation: estimate a new ranking from feedback logged under an old one.
ESS_WARNING_SHARE
module-attribute
¶
ESS_WARNING_SHARE = 0.1
ESS below this share of the weights means a few weights dominate the estimate.
Discount
module-attribute
¶
Discount = Callable[[Positions], npt.NDArray[np.float64]]
Weight λ(r) of a 1-based position r in the metric being estimated.
EventColumns
dataclass
¶
Column names of the show and feedback tables.
Shows hold either an items array per request (position = index + 1) or one row
per shown document with doc_id and position. Feedback holds request_id,
doc_id and event; ts is needed in both only when a window is set.
Source code in src/ranklens/offpolicy/events.py
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | |
EventLog
dataclass
¶
Impressions built from events and what did not make it into them.
Source code in src/ranklens/offpolicy/events.py
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | |
Propensity
dataclass
¶
Examination probability of each position: examination[r - 1] is p_r.
Source code in src/ranklens/offpolicy/propensity.py
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | |
power
classmethod
¶
power(depth: int, eta: float = 1.0) -> Propensity
p_r = (1 / r)^eta for positions 1..depth; eta = 0 means everything is examined.
Source code in src/ranklens/offpolicy/propensity.py
37 38 39 40 41 42 43 44 | |
at
¶
at(
positions: Sequence[int] | Positions,
) -> npt.NDArray[np.float64]
p_r for 1-based positions; a position deeper than known is an error, not 0.
Source code in src/ranklens/offpolicy/propensity.py
54 55 56 57 58 59 60 61 62 | |
ClickWorld
dataclass
¶
Queries with known relevance; builds policies, logs and true policy values.
The relevance is copied into read-only mappings: the truth a log was simulated from must not change under it. Worlds compare by value and are not hashable.
Source code in src/ranklens/offpolicy/simulator.py
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | |
attractiveness
¶
attractiveness(query: QueryId, doc: DocId) -> float
Probability of a click on doc once it is examined.
Source code in src/ranklens/offpolicy/simulator.py
64 65 66 | |
policy
¶
policy(
noise: float, seed: int = 0
) -> dict[QueryId, RankedList]
Rank every query by relevance plus N(0, noise²): 0 is the ideal ranking.
Source code in src/ranklens/offpolicy/simulator.py
68 69 70 71 72 73 74 75 76 77 78 79 | |
simulate
¶
simulate(
logging: Mapping[QueryId, RankedList],
propensity: Propensity,
n_impressions: int,
seed: int = 0,
) -> tuple[Impression, ...]
A log of logging: queries drawn uniformly, top propensity.depth shown.
Source code in src/ranklens/offpolicy/simulator.py
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | |
value
¶
value(
policy: Mapping[QueryId, RankedList], discount: Discount
) -> float
True value of policy: mean over queries of Σ_d λ(rank(d)) · a(d).
This is the expected discounted number of clicks if every position were examined — what an unbiased estimator recovers from a log of another policy.
Source code in src/ranklens/offpolicy/simulator.py
119 120 121 122 123 124 125 126 127 128 129 130 | |
weight_diagnostics
¶
weight_diagnostics(
log: Iterable[Impression],
propensity: Propensity,
*,
clip: float | None = None,
) -> WeightDiagnostics
Distribution of the weights 1/p of the rewarded documents, after clip.
Keeps one number per rewarded document: rewards are rare, so this is far less than the log itself.
Source code in src/ranklens/offpolicy/diagnostics.py
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | |
estimate
¶
estimate(
log: Iterable[Impression],
policy: Mapping[QueryId, RankedList],
propensity: Propensity,
discount: Discount,
*,
estimators: Sequence[Estimator] = ("ips", "snips"),
clip: float | None = None,
alpha: float = 0.05,
) -> tuple[OffPolicyEstimate, ...]
Several estimators from one pass over the log, in the order of estimators.
Warns with DegenerateWeightsWarning when the effective sample size of the weights
falls below 10% of their number: then a handful of rewards at deep positions
decide the estimate, and its interval is not to be trusted.
Source code in src/ranklens/offpolicy/estimators.py
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | |
ips
¶
ips(
log: Iterable[Impression],
policy: Mapping[QueryId, RankedList],
propensity: Propensity,
discount: Discount,
*,
clip: float | None = None,
alpha: float = 0.05,
) -> OffPolicyEstimate
Expected discounted reward of policy: mean over impressions of Σ c·λ/p.
Source code in src/ranklens/offpolicy/estimators.py
58 59 60 61 62 63 64 65 66 67 68 69 | |
snips
¶
snips(
log: Iterable[Impression],
policy: Mapping[QueryId, RankedList],
propensity: Propensity,
discount: Discount,
*,
clip: float | None = None,
alpha: float = 0.05,
) -> OffPolicyEstimate
Share of the reweighted reward of the shown documents that policy collects.
Source code in src/ranklens/offpolicy/estimators.py
72 73 74 75 76 77 78 79 80 81 82 83 | |
impressions_from_events
¶
impressions_from_events(
shows: DataFrame,
feedback: DataFrame,
scale: FeedbackScale,
*,
window: Timedelta | str | None = None,
columns: EventColumns | None = None,
) -> EventLog
Join show and feedback events into one Impression per request.
window (e.g. "1h") counts from the show: a purchase three days later is
not credited to this ranking. Without it, time is not looked at.
Impressions come in the order of the first show of each request.
Source code in src/ranklens/offpolicy/events.py
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | |
dcg_discount
¶
dcg_discount(k: int) -> Discount
λ(r) = 1[r ≤ k] / log2(r + 1): the estimated metric is DCG@k with the reward as gain.
Source code in src/ranklens/offpolicy/propensity.py
65 66 67 68 69 70 71 72 73 | |
topk_discount
¶
topk_discount(k: int) -> Discount
λ(r) = 1[r ≤ k]: the estimated metric is the reward collected in the top k.
Source code in src/ranklens/offpolicy/propensity.py
76 77 78 79 80 81 82 83 | |