Sample weights and uniqueness
len(y) is not your sample size. Advances in Financial Machine Learning (Lopez de Prado
2018), chapter 4. Label a 20-bar forward outcome at every bar and 1,980 rows arrive at fit(),
each sharing 95 % of its outcome with its neighbours. Every model, every standard error and every
confidence interval computed downstream is scaled by that 1,980. The shapes are right, the code
runs, and the reported significance is fiction.
Every number below is printed by scripts/sample_weights.py (numpy only, seed 0, 0.4 s).
⚠️ Snippet and page numbers throughout are as cited in mlfinpy 0.1.2's own docstrings; what
this skill verified is the code — this skill did not have the book.
1. Concurrency and average uniqueness
For each bar, count the labels whose span covers it; for each label, average 1 / concurrency
over its own span. ✅ Source-verified in mlfinpy 0.1.2 (mlfinpy/sampling/concurrent.py):
num_concurrent_events (Snippet 4.1, page 60): count.loc[t_in:t_out] += 1 — pandas label
slicing is inclusive at both ends, so a "20-bar" label occupies 21 bars.
_get_average_uniqueness (Snippet 4.2, page 62):
wght.loc[t_in] = (1.0 / num_conc_events.loc[t_in:t_out]).mean().
✅ Measured, 2,000 bars, labels spanning 20 bars, one event every step bars — and it matches the
analytic min(1, step / (span + 1)) exactly away from the ends:
step |
events |
mean concurrency |
mean uniqueness |
min(1, step/(span+1)) |
| 1 |
1,980 |
21.000 |
0.0476 |
0.0476 |
| 2 |
990 |
10.500 |
0.0952 |
0.0952 |
| 5 |
396 |
4.200 |
0.2381 |
0.2381 |
| 10 |
198 |
2.100 |
0.4762 |
0.4762 |
| 20 |
99 |
1.050 |
0.9524 |
0.9524 |
| 40 |
50 |
0.525 |
1.0000 |
1.0000 |
🚨 step = span is not unique. Sampling every 20 bars with a 20-bar label still leaves
uniqueness at 0.9524, because the inclusive endpoints make adjacent labels share one bar. Sample
at span + 1 if you want independence from spacing alone.
2. 🚨 The effective sample size, and the metric that does not measure it
✅ Measured at step = 1, span = 20, n = 1,980:
| quantity |
value |
| nominal n |
1,980 |
| sum of average uniqueness |
95.2 (4.8 % of n) |
| mean average uniqueness |
0.0481 |
| Kish effective size of uniform weights |
1,980.0 |
| Kish effective size of the uniqueness weights |
1,953.4 |
🚨 Kish's (sum w)^2 / sum(w^2) is the wrong tool here and it will reassure you. It measures
the dispersion of the weights, not the overlap. Every uniqueness weight in this design is
approximately 0.0476, so the weights are nearly uniform and Kish reports 1,953 of 1,980 — a 1.3 %
haircut on a sample that is 95 % redundant. The number that carries the information is
sum(uniqueness) = 95.2.
3. 🚨 What unweighted fitting does to your evidence
The claim to test: with overlapping labels, an ordinary t-test is not a 5 % test. ✅ Measured over
400 seeded paths, driftless log prices, the label being the next 20 bars and the feature being
the prior 5-bar return — independent of the label by construction, so the true slope is
exactly zero:
| fit |
sd of the reported t-statistic |
rejection rate of a nominal 5 % two-sided test |
| unweighted OLS |
2.19 (a correct one is 1.00) |
39.2 % |
| uniqueness-weighted OLS |
2.20 |
38.2 % |
🚨 A nominal 5 % test rejects on 39 % of null paths, and weighting does not fix it. Divide the
reported t by 2.19: a headline t = 3.0 is really 1.37, and the finding evaporates.
Two things worth reading carefully:
- ✅ Weights are not a variance correction. They change what each observation counts for;
they do not change the correlation between overlapping residuals. The size barely moved
(39.2 % → 38.2 %). The fixes are purged, embargoed cross-validation
(
../../../fin-libraries/skills/lib-purgedcv/SKILL.md) and an overlap-aware standard error
(Newey-West / Hansen-Hodrick), not sample_weight=.
- ⚠️ Summed uniqueness is the right direction and the wrong magnitude here. The measured
inflation implies an effective size of 412 of 1,980 (20.8 %), while summed uniqueness
predicts 95 (4.8 %) — 4.3x more pessimistic. The feature is autocorrelated too, and the
two dependencies do not simply multiply. Use
sum(uniqueness) as an alarm, and measure the
actual inflation by simulation before quoting a corrected t.
4. Return-attribution and time-decay weights
✅ Source-verified in mlfinpy 0.1.2 (mlfinpy/sample_weights/attribution.py):
_apply_weight_by_return (Snippet 4.10, page 69):
weights.loc[t_in] = (ret.loc[t_in:t_out] / num_conc_events.loc[t_in:t_out]).sum(), then
.abs(), with ret = np.log(close_series).diff() — log returns, so the attributions are
additive. get_weights_by_return then rescales with weights *= weights.shape[0] / weights.sum(),
so the weights sum to n, not to 1.
get_weights_by_time_decay (Snippet 4.11, page 70): the decay is linear in
av_uniqueness["tW"].sort_index().cumsum() — cumulative uniqueness, not calendar time. A
crowded stretch of history ages more slowly than a sparse one. decay = 1 is no decay,
decay = 0 sends the oldest observation to zero, decay < 0 erases the oldest fraction
outright.
✅ Measured on the same 1,980 events:
| weighting |
result |
| by |return attribution| |
min 0.0016, median 0.8478, max 4.9821, sum 1,980.0 |
| its concentration |
top 10 % of events carry 25.9 % of the weight; the bottom half carry 21.1 % |
| its Kish effective size |
1,264.7 of 1,980 (63.9 %) |
decay |
oldest weight |
newest |
at exactly zero |
Kish |
| +1.00 |
1.0000 |
1.0000 |
0.0 % |
1,980.0 |
| +0.50 |
0.5009 |
1.0000 |
0.0 % |
1,910.7 |
| 0.00 |
0.0018 |
1.0000 |
0.0 % |
1,492.7 |
| -0.50 |
0.0000 |
1.0000 |
50.0 % |
742.8 |
🚨 decay = -0.5 deletes half your training set, exactly and silently. That is the documented
behaviour, not a bug — but "erased from memory" in a docstring is easy to read past, and the
resulting Kish size (743) is the only place it shows up.
5. Sequential bootstrap
✅ Source-verified: seq_bootstrap (mlfinpy mlfinpy/sampling/bootstrapping.py, cited to
Snippets 4.5-4.6, page 65) draws each label with probability proportional to its average
uniqueness given the labels already drawn, recomputed at every pick. get_ind_matrix builds
the bars x labels indicator, and get_ind_mat_average_uniqueness reports
uniqueness[uniqueness > 0].mean().
✅ Measured, 80 labels of span 10 at random starts over 389 bars (whole-set uniqueness 0.3898),
5 draws each:
| draw |
average uniqueness of the drawn sample |
| standard bootstrap |
0.3323 (sd 0.0089) |
| sequential bootstrap |
0.3584 (sd 0.0143) |
| ratio |
1.08x |
⚠️ The gain is real but small on this design, and it is zero when the labels are regularly
spaced with identical spans — there is then no less-overlapping subset to find. Sequential
bootstrap pays when the spans vary (triple-barrier touch times do vary). Measure it on your own
t1 before paying its cost: it is O(n) uniqueness recomputations per draw.
6. 🚨 The extra bar in the reference return attribution
ret = np.log(close).diff() puts the return into bar t at index t, and
ret.loc[t_in:t_out] therefore starts with a return the label did not earn — the move from
t0 - 1 to t0. ✅ Measured: dropping that bar moves each weight by a median 21.6 % of its
own value (16.8 % of the mean weight), 72.6 % of events move by more than 10 %, and the rank
correlation between the two weight vectors is 0.9090. The ordering of the weights survives;
the individual weights do not. Decide which convention you want and be consistent, because the
two are not interchangeable at the level of a single sample's weight.
7. Traps
- 🚨 Reporting
len(y) as the sample size. Section 2. Report sum(uniqueness) next to it,
every time.
- 🚨 Believing a t-statistic from overlapping labels. Section 3, 39 % size at a nominal 5 %.
This is the same failure as overlapping forward returns in a factor regression — see
../../../fin-models/skills/factor-models/SKILL.md.
- 🚨 Thinking
sample_weight= fixes dependence. It does not (section 3). Weights fix
attribution; purging fixes leakage; a HAC standard error fixes inference. Three different
problems.
- 🚨 Kish's effective sample size on near-uniform weights. Section 2. It reports 1,953 of
1,980 on a sample that is 95 % redundant.
- 🚨
t1 must be the actual touch time. Concurrency, uniqueness and every weight here are
computed from [t0, t1]. Feed the vertical barrier instead of the barrier that was touched and
every number on this page is wrong in the safe direction; feed t0 + 1 and they are wrong in
the dangerous one. ../triple-barrier-labeling/SKILL.md produces the touch times.
- 🚨 Inclusive endpoints. Section 1: a "20-bar" label spans 21 bars in the reference
implementation. It matters at the margin where you choose the sampling step.
- ⚠️ Weights change what a metric means. A weighted accuracy is not an accuracy; a
return-attributed weight makes the model care about the large moves, which is usually what you
want and is never what
accuracy_score reports. State which weights produced which number.
- 🚨 Do not re-implement purged cross-validation here.
../../../fin-libraries/skills/lib-purgedcv/SKILL.md owns it, including the
evaluation_times argument that these same t1 values feed.
- 🔴
pip install mlfinlab cannot work — ✅ checked 2026-09-09: the PyPI JSON API returns 404
and the simple index lists zero files. The maintained fork is mlfinpy 0.1.2 (MIT), which
⚠️ pins numpy<1.27. The snippets above were read from its sdist.
8. Scripts
scripts/sample_weights.py — concurrency by prefix sum, average uniqueness, return-attribution
and time-decay weights, Kish effective size, the indicator matrix, sequential bootstrap, and
the 400-path Monte Carlo that measures the true size of a nominal 5 % t-test on overlapping
labels. numpy only. Seed 0, 0.4 s.
9. Where this sits
../triple-barrier-labeling/SKILL.md produces the t0/t1 spans everything here consumes.
../../../fin-libraries/skills/lib-purgedcv/SKILL.md owns purged and embargoed cross-validation
of exactly these labels — this skill is the weights, that one is the folds.
../feature-importance-financial/SKILL.md needs both: MDA scored on unpurged folds with
unweighted samples is the same error twice.
../../../fin-core/skills/backtest-validation/SKILL.md owns the multiple-testing deflation that
comes after the standard error is right. ../../../fin-models/skills/factor-models/SKILL.md owns
the overlapping-return problem in its regression form.
1---2name: sample-weights-and-uniqueness3description: Overlapping labels are not independent observations - compute concurrency, average uniqueness and return-attributed weights, and divide your t-statistics by the overlap factor before believing any of them. TRIGGER - sample weights, average uniqueness, concurrency, overlapping labels, numCoEvents, num_concurrent_events, getAvgUniqueness, tW, sample_weight in fit(), getWeightsByReturn, get_weights_by_return, time decay weights, getTimeDecay, sequential bootstrap, seq_bootstrap, indicator matrix, effective sample size, "my labels overlap", "my t-stat is 4 but it does not hold up", "overlapping forward returns", Lopez de Prado chapter 4, AFML sample weights. SKIP for purged and embargoed cross-validation of the same labels (lib-purgedcv owns it, do not re-implement), for producing the labels and their t1 touch times (triple-barrier-labeling), for feature importance under overlap (feature-importance-financial), and for Sharpe deflation (backtest-validation).4license: MIT5---67# Sample weights and uniqueness89**`len(y)` is not your sample size.** Advances in Financial Machine Learning (Lopez de Prado102018), chapter 4. Label a 20-bar forward outcome at every bar and 1,980 rows arrive at `fit()`,11each sharing 95 % of its outcome with its neighbours. Every model, every standard error and every12confidence interval computed downstream is scaled by that 1,980. The shapes are right, the code13runs, and the reported significance is fiction.1415Every number below is printed by `scripts/sample_weights.py` (numpy only, seed 0, **0.4 s**).16⚠️ Snippet and page numbers throughout are as cited in **mlfinpy 0.1.2's own docstrings**; what17this skill verified is the *code* — this skill did not have the book.1819## 1. Concurrency and average uniqueness2021For each bar, count the labels whose span covers it; for each label, average `1 / concurrency`22over its own span. ✅ Source-verified in mlfinpy 0.1.2 (`mlfinpy/sampling/concurrent.py`):2324- `num_concurrent_events` (Snippet 4.1, page 60): `count.loc[t_in:t_out] += 1` — pandas label25 slicing is **inclusive at both ends**, so a "20-bar" label occupies **21** bars.26- `_get_average_uniqueness` (Snippet 4.2, page 62):27 `wght.loc[t_in] = (1.0 / num_conc_events.loc[t_in:t_out]).mean()`.2829✅ Measured, 2,000 bars, labels spanning 20 bars, one event every `step` bars — and it matches the30analytic `min(1, step / (span + 1))` exactly away from the ends:3132| `step` | events | mean concurrency | mean uniqueness | `min(1, step/(span+1))` |33|---|---|---|---|---|34| **1** | 1,980 | **21.000** | **0.0476** | 0.0476 |35| 2 | 990 | 10.500 | 0.0952 | 0.0952 |36| 5 | 396 | 4.200 | 0.2381 | 0.2381 |37| 10 | 198 | 2.100 | 0.4762 | 0.4762 |38| 20 | 99 | 1.050 | **0.9524** | 0.9524 |39| 40 | 50 | 0.525 | 1.0000 | 1.0000 |4041🚨 **`step = span` is not unique.** Sampling every 20 bars with a 20-bar label still leaves42uniqueness at 0.9524, because the inclusive endpoints make adjacent labels share one bar. Sample43at `span + 1` if you want independence from spacing alone.4445## 2. 🚨 The effective sample size, and the metric that does not measure it4647✅ Measured at `step = 1`, `span = 20`, n = 1,980:4849| quantity | value |50|---|---|51| nominal n | 1,980 |52| **sum of average uniqueness** | **95.2** (4.8 % of n) |53| mean average uniqueness | 0.0481 |54| Kish effective size of **uniform** weights | 1,980.0 |55| Kish effective size of the **uniqueness** weights | **1,953.4** |5657🚨 **Kish's `(sum w)^2 / sum(w^2)` is the wrong tool here and it will reassure you.** It measures58the *dispersion* of the weights, not the overlap. Every uniqueness weight in this design is59approximately 0.0476, so the weights are nearly uniform and Kish reports 1,953 of 1,980 — a 1.3 %60haircut on a sample that is 95 % redundant. The number that carries the information is61`sum(uniqueness) = 95.2`.6263## 3. 🚨 What unweighted fitting does to your evidence6465The claim to test: with overlapping labels, an ordinary t-test is not a 5 % test. ✅ Measured over66**400 seeded paths**, driftless log prices, the label being the next 20 bars and the feature being67the *prior* 5-bar return — **independent of the label by construction**, so the true slope is68exactly zero:6970| fit | sd of the reported t-statistic | rejection rate of a nominal 5 % two-sided test |71|---|---|---|72| **unweighted OLS** | **2.19** (a correct one is 1.00) | **39.2 %** |73| uniqueness-weighted OLS | 2.20 | 38.2 % |7475🚨 **A nominal 5 % test rejects on 39 % of null paths, and weighting does not fix it.** Divide the76reported t by 2.19: a headline `t = 3.0` is really **1.37**, and the finding evaporates.7778Two things worth reading carefully:7980- ✅ **Weights are not a variance correction.** They change what each observation counts *for*;81 they do not change the correlation between overlapping residuals. The size barely moved82 (39.2 % → 38.2 %). The fixes are purged, embargoed cross-validation83 (`../../../fin-libraries/skills/lib-purgedcv/SKILL.md`) and an overlap-aware standard error84 (Newey-West / Hansen-Hodrick), not `sample_weight=`.85- ⚠️ **Summed uniqueness is the right direction and the wrong magnitude here.** The measured86 inflation implies an effective size of **412 of 1,980 (20.8 %)**, while summed uniqueness87 predicts **95 (4.8 %)** — **4.3x more pessimistic**. The feature is autocorrelated too, and the88 two dependencies do not simply multiply. Use `sum(uniqueness)` as an alarm, and measure the89 actual inflation by simulation before quoting a corrected t.9091## 4. Return-attribution and time-decay weights9293✅ Source-verified in mlfinpy 0.1.2 (`mlfinpy/sample_weights/attribution.py`):9495- `_apply_weight_by_return` (Snippet 4.10, page 69):96 `weights.loc[t_in] = (ret.loc[t_in:t_out] / num_conc_events.loc[t_in:t_out]).sum()`, then97 `.abs()`, with `ret = np.log(close_series).diff()` — **log** returns, so the attributions are98 additive. `get_weights_by_return` then rescales with `weights *= weights.shape[0] / weights.sum()`,99 so the weights **sum to n**, not to 1.100- `get_weights_by_time_decay` (Snippet 4.11, page 70): the decay is linear in101 `av_uniqueness["tW"].sort_index().cumsum()` — **cumulative uniqueness**, not calendar time. A102 crowded stretch of history ages more slowly than a sparse one. `decay = 1` is no decay,103 `decay = 0` sends the oldest observation to zero, `decay < 0` erases the oldest fraction104 outright.105106✅ Measured on the same 1,980 events:107108| weighting | result |109|---|---|110| by \|return attribution\| | min 0.0016, median 0.8478, max 4.9821, sum 1,980.0 |111| its concentration | top 10 % of events carry **25.9 %** of the weight; the bottom half carry 21.1 % |112| its Kish effective size | **1,264.7** of 1,980 (63.9 %) |113114| `decay` | oldest weight | newest | at exactly zero | Kish |115|---|---|---|---|---|116| +1.00 | 1.0000 | 1.0000 | 0.0 % | 1,980.0 |117| +0.50 | 0.5009 | 1.0000 | 0.0 % | 1,910.7 |118| 0.00 | 0.0018 | 1.0000 | 0.0 % | 1,492.7 |119| **-0.50** | 0.0000 | 1.0000 | **50.0 %** | **742.8** |120121🚨 `decay = -0.5` **deletes half your training set**, exactly and silently. That is the documented122behaviour, not a bug — but "erased from memory" in a docstring is easy to read past, and the123resulting Kish size (743) is the only place it shows up.124125## 5. Sequential bootstrap126127✅ Source-verified: `seq_bootstrap` (mlfinpy `mlfinpy/sampling/bootstrapping.py`, cited to128Snippets 4.5-4.6, page 65) draws each label with probability proportional to its average129uniqueness **given the labels already drawn**, recomputed at every pick. `get_ind_matrix` builds130the bars x labels indicator, and `get_ind_mat_average_uniqueness` reports131`uniqueness[uniqueness > 0].mean()`.132133✅ Measured, 80 labels of span 10 at random starts over 389 bars (whole-set uniqueness 0.3898),1345 draws each:135136| draw | average uniqueness of the drawn sample |137|---|---|138| standard bootstrap | 0.3323 (sd 0.0089) |139| sequential bootstrap | **0.3584** (sd 0.0143) |140| ratio | **1.08x** |141142⚠️ **The gain is real but small on this design**, and it is *zero* when the labels are regularly143spaced with identical spans — there is then no less-overlapping subset to find. Sequential144bootstrap pays when the spans vary (triple-barrier touch times do vary). Measure it on your own145`t1` before paying its cost: it is O(n) uniqueness recomputations per draw.146147## 6. 🚨 The extra bar in the reference return attribution148149`ret = np.log(close).diff()` puts the return *into* bar `t` at index `t`, and150`ret.loc[t_in:t_out]` therefore starts with a return the label did not earn — the move from151`t0 - 1` to `t0`. ✅ Measured: dropping that bar moves each weight by a **median 21.6 %** of its152own value (16.8 % of the mean weight), **72.6 %** of events move by more than 10 %, and the rank153correlation between the two weight vectors is **0.9090**. The *ordering* of the weights survives;154the individual weights do not. Decide which convention you want and be consistent, because the155two are not interchangeable at the level of a single sample's weight.156157## 7. Traps158159- 🚨 **Reporting `len(y)` as the sample size.** Section 2. Report `sum(uniqueness)` next to it,160 every time.161- 🚨 **Believing a t-statistic from overlapping labels.** Section 3, 39 % size at a nominal 5 %.162 This is the same failure as overlapping forward returns in a factor regression — see163 `../../../fin-models/skills/factor-models/SKILL.md`.164- 🚨 **Thinking `sample_weight=` fixes dependence.** It does not (section 3). Weights fix165 *attribution*; purging fixes *leakage*; a HAC standard error fixes *inference*. Three different166 problems.167- 🚨 **Kish's effective sample size on near-uniform weights.** Section 2. It reports 1,953 of168 1,980 on a sample that is 95 % redundant.169- 🚨 **`t1` must be the actual touch time.** Concurrency, uniqueness and every weight here are170 computed from `[t0, t1]`. Feed the vertical barrier instead of the barrier that was touched and171 every number on this page is wrong in the safe direction; feed `t0 + 1` and they are wrong in172 the dangerous one. `../triple-barrier-labeling/SKILL.md` produces the touch times.173- 🚨 **Inclusive endpoints.** Section 1: a "20-bar" label spans 21 bars in the reference174 implementation. It matters at the margin where you choose the sampling step.175- ⚠️ **Weights change what a metric means.** A weighted accuracy is not an accuracy; a176 return-attributed weight makes the model care about the large moves, which is usually what you177 want and is never what `accuracy_score` reports. State which weights produced which number.178- 🚨 **Do not re-implement purged cross-validation here.**179 `../../../fin-libraries/skills/lib-purgedcv/SKILL.md` owns it, including the180 `evaluation_times` argument that these same `t1` values feed.181- 🔴 **`pip install mlfinlab` cannot work** — ✅ checked 2026-09-09: the PyPI JSON API returns 404182 and the simple index lists zero files. The maintained fork is `mlfinpy` 0.1.2 (MIT), which183 ⚠️ pins `numpy<1.27`. The snippets above were read from its sdist.184185## 8. Scripts186187- `scripts/sample_weights.py` — concurrency by prefix sum, average uniqueness, return-attribution188 and time-decay weights, Kish effective size, the indicator matrix, sequential bootstrap, and189 the 400-path Monte Carlo that measures the true size of a nominal 5 % t-test on overlapping190 labels. numpy only. Seed 0, **0.4 s**.191192## 9. Where this sits193194`../triple-barrier-labeling/SKILL.md` produces the `t0`/`t1` spans everything here consumes.195`../../../fin-libraries/skills/lib-purgedcv/SKILL.md` owns purged and embargoed cross-validation196of exactly these labels — this skill is the weights, that one is the folds.197`../feature-importance-financial/SKILL.md` needs both: MDA scored on unpurged folds with198unweighted samples is the same error twice.199`../../../fin-core/skills/backtest-validation/SKILL.md` owns the multiple-testing deflation that200comes after the standard error is right. `../../../fin-models/skills/factor-models/SKILL.md` owns201the overlapping-return problem in its regression form.