Spaces:
Running on Zero
01 β Dataset provenance, ingestion and exploratory analysis
Covers Phase 2 (ingestion, validation, cleaning, reconciliation) and Phase 3 (exploratory analysis and Gate 1).
Every number on this page is generated by
src/data/ and written to outputs/reports/.
Nothing here is hand-typed.
1. Source provenance (Task 2.0)
URLs were verified by request, not recalled. The server's own
Content-Disposition header supplied each filename and Content-Length the
exact size; a Range request read the first 19 bytes of each file and
confirmed the ASCII string MATLAB 7.3 MAT-file. That is why h5py is
required and scipy.io cannot be used β it handles v7.0βv7.2 only.
| Batch | File | Bytes | SHA-256 (first 16) |
|---|---|---|---|
| batch1 | 2017-05-12_batchdata_updated_struct_errorcorrect.mat |
3,025,320,241 | 9d928ab978f0e3c7 |
| batch2 | 2017-06-30_batchdata_updated_struct_errorcorrect.mat |
2,007,331,155 | 63ab200d09ecb237 |
| batch3 | 2018-04-12_batchdata_updated_struct_errorcorrect.mat |
3,236,690,412 | 62c30e413b63e614 |
Total 7.70 GiB. Full digests in configs/data.yaml
and outputs/reports/dataset_manifest.json.
Landing page: https://data.matr.io/1/projects/5c48dd2bc625d700019f3204.
Resume behaviour was measured, not assumed. data.matr.io sends no
Accept-Ranges header but does honour Range, answering HTTP 206 with a
correct Content-Range. downloader.py implements
resume on that confirmed behaviour and refuses to append a full body to a
partial file if the server ever ignores the header.
Attia extension not located. The 45-cell extension (β 169 cells) lives in a different matr.io project; candidate file IDs returned zero-length responses. No URL is recorded rather than a guessed one. This project uses the 124-cell primary corpus.
2. Manufacturing interpretation, and its limits
| Dataset concept | QC analogue |
|---|---|
| One cell | One unit arriving at end-of-line QC |
| Charging policy | Process recipe |
| Batch | Production campaign |
| Cycle life to 80% nominal | The quantity a grade decision depends on |
| Cycles observed before deciding | Aging-chamber occupancy (the scarce resource) |
Stated plainly: this is a research cycling dataset, not a factory dataset. Cells were cycled under controlled laboratory conditions at 30 Β°C, not produced and screened on a line. There is no true process-variation signal (no electrode coating weight, no formation-line drift, no supplier lot). The QC framing is a faithful analogue, not a literal production log, and every conclusion in this project should be read that way.
3. Parsing
MATLAB v7.3 files are HDF5 containers in which nearly everything is an object
reference dereferenced through the file handle. Structure was inspected
before parsing rather than assumed (mat_parser.py --inspect):
batchgroup withsummary,cycles,Vdlin,barcode,channel_id,policy,policy_readable,cycle_life- Summary fields are direct datasets, shape
(1, n_cycles) - Cycle fields are object-reference arrays, shape
(n_cycles, 1) Qdlin/Tdlin/discharge_dQdVare pre-interpolated onto the 1000-pointVdlingrid running 3.5 β 2.0 V
Storage decision
Summary series are kept for every cycle. Within-cycle traces are kept only
to cycle 120 (parsing.within_cycle_max_cycle). This is not a shortcut: the
leakage rule caps every feature at budget N β€ 100, so no feature may
legitimately read a trace beyond that, and the Phase 3 figures using
within-cycle data also stop at 100. Keeping all ~96,700 cycles of raw traces
would cost several GiB to store data the project's own rules forbid using.
Result: 964 MB of parquet.
The placeholder row β a real structural difference between batches
| Batch | Cells with an all-zero first row |
|---|---|
| batch1 | 46 / 46 |
| batch2 | 0 / 43 |
| batch3 | 0 / 46 |
Every summary field is exactly zero in that row. It is a structural artefact, not a measured cycle β and it is why the source literature takes "discharge capacity at cycle 2" as the first usable value.
Consequence, and it matters for Phase 4: raw cycle numbering does not mean the same thing across batches. Slicing "cycles 1..N" on the raw index would give batch-1 cells one fewer measured cycle at every budget β a systematic, batch-correlated bias in exactly the variable RQ4 tests for.
Resolution: the placeholder row is retained (so the data stays faithful to
source and comparable with published work), and a separate cycle_measured
column numbers only measured cycles from 1, consistently across batches. All
feature budgets slice on cycle_measured.
4. The continuation join
Five batch-1 cells continued cycling as batch-2 records. Joining them is not bookkeeping: an unjoined record has its cycle life truncated, which would inject artificially short-lived cells into training and corrupt the target.
| Primary | Continuation | Primary cycles | Added | Joined | Config expected | Match |
|---|---|---|---|---|---|---|
| b1c0 | b2c7 | 1189 | 662 | 1851 | 662 | β |
| b1c1 | b2c8 | 1178 | 981 | 2159 | 981 | β |
| b1c2 | b2c9 | 1176 | 1060 | 2236 | 1060 | β |
| b1c3 | b2c15 | 1225 | 208 | 1433 | 208 | β |
| b1c4 | b2c16 | 1226 | 482 | 1708 | 482 | β |
All five matched their configured extra-cycle counts exactly.
Independent confirmation: these are precisely the five cells whose batch-1 record never reaches end of life (minimum capacity 0.971β1.043 Ah against a 0.88 Ah threshold). The join is confirmed both correct and necessary β without it those five labels would be 40β90% too short.
5. Cycle life is recomputed, never inherited
The naive rule "first cycle strictly below 80% of nominal" classifies every cell as never reaching end of life. The reason is experimental, not numerical: these cells were cycled until they reached 80% of nominal, at which point the experiment stopped. Capacity never crosses below the threshold β the record simply ends just above it.
Measured across all 46 batch-1 cells, the minima separate cleanly:
| Group | n | Minimum discharge capacity |
|---|---|---|
| Reached end of life | 36 | 0.8801 β 0.8834 Ah |
| Did not (5 continuation + 5 excluded) | 10 | 0.9131 β 1.0433 Ah |
A ~0.029 Ah gap, stable at every smoothing window tested. So two distinct questions are answered separately, which matters:
- Did the cell reach end of life? Decided with a 2% tolerance (effective threshold 0.8976 Ah = 81.6% of nominal), placing it mid-gap.
- When? Decided against the true 0.88 Ah threshold; if the series ends before strictly crossing it, the final recorded cycle is the end-of-life cycle.
Conflating them biased the label ~14 cycles early. Separated, the recomputed
value agrees with the file's precomputed cycle_life field to exactly 1 cycle
for all 36 batch-1 EOL cells at every smoothing window β the constant offset
being the n vs n+1 convention. Censoring classification matches expectation
exactly at all three windows, so the result does not depend on the smoothing
choice.
The only large disagreements are the five continuation cells (207β1059 cycles), which is the join working as intended.
6. Validation β 0 FAIL
validator.py checks cycle-index monotonicity and
uniqueness, cycle_measured uniqueness and contiguity, capacity within
0 < Q β€ 1.2 Γ nominal, voltage and temperature bounds, and the cycle-life
recomputation. Result: 135 cells, 0 FAIL, 144 WARN.
Distinguishing a parser bug from an instrument artefact
These are different failures and only one invalidates the pipeline:
- A parser bug is systematic across cells β misreading the layout corrupts every cell identically.
- An instrument artefact is localised to a cell.
So per-cell implausibility is a WARN, and the run FAILs only if the affected fraction of cells exceeds 20%. Measured: capacity 4/135 cells (3.0%), voltage 1/135 (0.7%). Both well below threshold.
Two calibrations were corrected against measurement reality:
- Voltage. Protocol setpoints are 2.0/3.6 V, but the cycler samples at a finite rate so the last sample of a discharge overshoots β observed span 1.919β3.614 V. The validation bound was widened to [1.85, 3.70]; the schema now rejects a validation bound tighter than the protocol range, since that would flag correctly parsed data as impossible.
- Temperature. Observed 27.3β40.4 Β°C, comfortably inside the original [20, 50] bound. Unchanged.
Data-quality flags carried forward
Four cells are flagged and retained β dropping them would break comparability with the standard 124-cell corpus, and ignoring them would be worse:
| Cell | Finding |
|---|---|
| b1c18 | q_charge ramps monotonically 1.335 β 1.968 Ah over cycles 16β40 while q_discharge stays flat at 1.070 Ah β a coulomb-counter reset failure. Voltage spans 0.736β6.606 V, physically impossible for a 3.3 V LFP cell. Not in the canonical exclusion list. Phase 4 found a third corrupted channel on this cell: its median cycle duration reads 2236 minutes (37 hours) against a cohort median of 51.8 min. |
| b1c0, b2c12, b2c44 | One isolated implausible capacity cycle each. |
Correction (Phase 4). The single vertical spike near 3.09 V in
fig04was initially attributed to b1c18 in this document. Checking directly, it is b1c41 β the only cell in the cohort whose ΞQ(V) contains a localized spike (peak 8.7Ο from its own mean, at V β 3.09). b1c18's ΞQ(V) is clean, becauseQdlinderives from the discharge segment while b1c18's corruption is in the charge phase. b1c41's raw channels are otherwise entirely healthy.
b1c18 is the most significant: cycles 16β40 fall inside every diagnostic budget, so it will affect its features. Phase 4 must handle it deliberately.
7. Reconciliation β exactly 124 cells
| Batch | Found | Consumed by join | Physical cells | Excluded | Used | Expected | Match |
|---|---|---|---|---|---|---|---|
| batch1 | 46 | 0 | 46 | 5 | 41 | 41 | β |
| batch2 | 48 | 5 | 43 | 0 | 43 | 43 | β |
| batch3 | 46 | 0 | 46 | 6 | 40 | 40 | β |
| TOTAL | 140 | 5 | 135 | 11 | 124 | 124 | β |
Exclusion audit β every dropped cell justified individually
Rather than applying the inherited list on authority, each stated reason was tested against the data.
| Cells | Verdict | Evidence |
|---|---|---|
| b1c8, b1c10, b1c12, b1c13, b1c22 | SUPPORTED | Right-censored: capacity never reaches end of life. Directly confirmed. |
| b3c23 | SUPPORTED | Roughest trajectory of all 46 batch-3 cells, and right-censored. |
| b3c2 | SUPPORTED | Trajectory roughness ranks 3/46, above the 90th percentile of peers. |
| b3c32 | SUPPORTED (different reason) | Stated noisy-channel claim not confirmed (roughness ranks only 15/46), but the cell is right-censored, so it cannot carry a label regardless. |
| b3c37, b3c42, b3c43 | NOT SUPPORTED | Roughness ranks 20, 22, 23 of 46 β unremarkable β and each yields a valid cycle life (1389, 1641, 1045). Excluded on the source release's authority only. |
"Noisy channel" was tested comparatively via median absolute second difference of the capacity trajectory, which removes level and fade slope leaving cycle-to-cycle jitter.
Honest limitation: 3 of 11 exclusions could not be independently justified.
Decision on b3c37, b3c42, b3c43 (reviewed and accepted)
These three cells remain excluded, and their config entries remain
verified: false. The reasoning, recorded explicitly because it is a
judgement call rather than a derivation:
- What the evidence says. Each yields a valid cycle life (1389, 1641, 1045) and each is unremarkable on the roughness metric (ranks 20, 22, 23 of 46, against a peer 90th percentile none of them exceeds). This project found no independent support for the stated noisy-channel reason.
- Why they are excluded anyway. The 124-cell cohort is the corpus the literature reports against, and Gate 2 (Phase 6) is a reproduction of Severson et al. at budget 100. Silently analysing a different 127-cell cohort would make that reproduction non-comparable and the discrepancy would be invisible to a reader.
- Why the flag stays false. Marking them
verified: truewould assert evidence this project does not have. The flag is the mechanism that keeps inherited assumptions visible, so it must not be flipped for tidiness.
The honest summary is therefore: these three cells are excluded on the source
release's authority, not on evidence reproduced here. Retaining them is a
legitimate alternative that would give a 127-cell corpus; anyone re-running this
work can do so by deleting the three entries from
configs/data.yaml, and the reconciliation will follow
automatically. The exclusion is unlikely to be material β three mid-distribution
cells out of 127 β but that is an expectation, not a measurement.
Outstanding assumptions: 3 of the original 21 (all three above). The other 18 β source provenance, all batch counts, all five continuation joins, eight exclusions, and the final reconciliation β are closed out against the data.
8. GATE 1 β the canary β PASSED
"If you cannot approximately reproduce this relationship, STOP and debug the parser. This is the canary for the entire pipeline."
logββ(var(ΞQββββββ(V))) versus logββ(cycle life), n = 124:
| Quantity | This project | Severson et al. 2019 |
|---|---|---|
| Pearson r | β0.9267 | Ο = β0.93 |
| RΒ² | 0.8588 | β 0.86 implied |
| Spearman | β0.9015 | β |
| Slope / intercept | β0.4157 / 1.2680 | β |
Per batch: batch1 RΒ² = 0.901, batch2 RΒ² = 0.665, batch3 RΒ² = 0.647. The relationship is materially weaker in the later campaigns β an early and relevant signal for RQ4, since batch 3 is the OOD split.
The canary earned its place
An intermediate run gave RΒ² = 0.7186 with batch-1 RΒ² collapsing to 0.4249. The
cause was a genuine bug: cycle_measured was computed per record before the
continuation join, so after concatenation it restarted at 1 and contained
duplicates. A lookup keyed on it resolved to the last match, silently
reading late-life curves as early-life ones for the five longest-lived cells.
Nothing else in the pipeline detected this β validation passed, reconciliation
was exact, cycle lives were correct. Only the canary caught it. The join now
recomputes cycle_measured across the merged series, the validator asserts
uniqueness and contiguity, and the lookup keeps the first occurrence as
defence in depth.
9. Exploratory analysis
All eleven figures regenerate headlessly via python -m src.data.eda into
outputs/figures/.
Realised grade balance β recomputed, not assumed
| Grade | Boundary | Cells | Share | Tier |
|---|---|---|---|---|
| A | β₯ 1200 | 11 | 8.9% | automotive traction pack |
| B | β₯ 550 | 70 | 56.5% | stationary ESS / second tier |
| C | β₯ 0 | 43 | 34.7% | scrap or non-critical |
Cycle life spans 150 β 2236 (median 736), consistent with the published 150β2300 range.
β οΈ Grade A holds only 11 cells.
configs/grading.yamlanticipated this risk explicitly. Eleven examples is thin for a three-class classifier and very thin for per-fold estimates under 5-fold grouped CV (~2 cells per fold). This is a decision for Phase 4/6 review, not something to be worked around silently. Options: lower the A boundary, treat grading as ordinal regression from predicted cycle life, or report grade-A metrics with explicit caveats.
Batch 3 is genuinely shifted
Two-sample KS between batches 1β2 and batch 3: D = 0.651, p = 1.7 Γ 10β»ΒΉΒΉ. Median cycle life 547.5 vs 963.5. Batch 3 is a real distribution shift, so RQ4 tests something meaningful rather than a relabelling.
Feature signal (Spearman vs log cycle life)
| Feature | Ο |
|---|---|
| log var ΞQ(V) | β0.902 |
| min ΞQ(V) | +0.868 |
| internal resistance at cycle N | β0.594 |
| minimum internal resistance | β0.588 |
| maximum temperature | β0.252 |
| mean temperature | +0.222 |
ΞQ(V)-derived features dominate, consistent with the literature. Thermal signal is weak (|Ο| β 0.22β0.25) in this corpus β worth stating now, since Phase 9 hypothesis H3 predicts monotonic thermal SHAP attributions. In a 30 Β°C-controlled chamber there is little thermal variation to exploit, so a weak result there would be a property of the dataset, not a modelling failure.
Process-recipe space
69 unique charging policies across 124 cells, step-1 C-rates spanning 1.0β8.0 C. Recipe families are heavily confounded with cycle life by design, which is exactly why Group F must be ablated for RQ4.
Why a naive rule is not enough β the motivation
Single-feature threshold on log var ΞQ(V), at matched yield (shipping exactly the 81 truly capable cells' worth), tuned in-sample:
| Budget | Escapes | Escape rate | Overkill rate |
|---|---|---|---|
| 5 | 14 | 17.3% | 17.3% |
| 10 | 22 | 27.2% | 27.2% |
| 20 | 23 | 28.4% | 28.4% |
| 50 | 9 | 11.1% | 11.1% |
| 100 | 7 | 8.6% | 8.6% |
Even at budget 100, tuned in-sample with a perfectly calibrated yield, a naive rule leaves an 8.6% escape rate β and it offers no way to bound that rate at a chosen level. This is the gap the conformal risk control of Phase 7 and the cost-optimal triage of Phase 8 exist to close.
Two honest caveats: these are in-sample numbers, so they are an optimistic bound rather than held-out performance; and the non-monotonicity at budgets 10β20 reflects both a changing ΞQ baseline cycle and genuine small-sample noise at n = 124.
Artifacts
| File | Contents |
|---|---|
outputs/reports/dataset_manifest.json |
URLs, sizes, SHA-256 digests |
outputs/reports/reconciliation.csv |
The found/joined/excluded/used table |
outputs/reports/exclusion_audit.json |
Per-cell exclusion verdicts and evidence |
outputs/reports/validation_report.csv |
Per-cell validation record |
outputs/reports/validation_issues.json |
Every FAIL and WARN |
outputs/reports/eda_results.json |
All Phase 3 quantitative results |
outputs/reports/eda_delta_q_statistics.csv |
Per-cell ΞQ(V) statistics |
outputs/reports/cohort.csv |
Final 124-cell labelled cohort |
data/processed/cohort.parquet |
Cohort with labels, grades and quality flags |
Reproduce end to end:
python -m src.data.downloader # 7.70 GiB, resumable, checksum-verified
python -m src.data.mat_parser # -> data/interim/cells/
python -m src.data.validator # must report 0 FAIL
python -m src.data.cleaner # reconciliation + cohort
python -m src.data.eda # 11 figures + Gate 1
