CellTriage / docs /01_dataset_and_eda.md
Sarvarbek13's picture
CellTriage QC operator console - inference only, CPU-bound classical ML
749bffa verified
|
Raw
History Blame Contribute Delete
19.8 kB

01 β€” Dataset provenance, ingestion and exploratory analysis

← Back to README

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):

  • batch group with summary, 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_dQdV are pre-interpolated onto the 1000-point Vdlin grid 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:

  1. 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.
  2. 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 fig04 was 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, because Qdlin derives 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: true would 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 β€”

Gate 1

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.yaml anticipated 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

← README Β· Feature engineering β†’