constructelligence commited on
Commit
e42cda0
·
verified ·
1 Parent(s): eec49a5

Opt-in --bid-items mode: TF-IDF also trained on 2,842 real TxDOT bid items (item-held-out .489 -> .608)

Browse files
IMPROVEMENT-PLAN.md CHANGED
@@ -124,3 +124,62 @@ full sections; add an `other/<division>` fallback and score it honestly; revisit
124
  - More synthetic line-item augmentation (tested: `mf-0.3` — no line-item transfer, manual regression).
125
  - More TF-IDF feature engineering (already saturated; gains below the CI).
126
  - Reporting sub-CI improvements as wins on the model card.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  - More synthetic line-item augmentation (tested: `mf-0.3` — no line-item transfer, manual regression).
125
  - More TF-IDF feature engineering (already saturated; gains below the CI).
126
  - Reporting sub-CI improvements as wins on the model card.
127
+
128
+ ---
129
+
130
+ ## Progress — P0.1 acquisition started (2026-10-08)
131
+
132
+ **Tooling added (this stage).**
133
+ - `scripts/compare.py` — the gate the plan was missing: **paired bootstrap CI of the difference + exact
134
+ McNemar**, per set, with a `LIFT / DROP / no detectable change` verdict. `cloud/stats.py` had Wilson CIs
135
+ but no significance test, which is how sub-CI changes kept reading as wins.
136
+ - `scripts/build_manual_eval_v2.py` — section eval over **every** local manual, with a train/val
137
+ leakage guard (drops any chunk whose text appears in the corpus).
138
+ - `scripts/import_bid_tabulation.py` — pulls real line items from public bid tabulations.
139
+
140
+ **What the sources actually hold (the hard part).**
141
+ - The local AEC-Bench / ConstructBench cache has 18 documents, but **only 2 are project manuals**
142
+ (`spec.pdf` ×2 → Wenatchee 651 pp, Macon 807 pp); the rest are drawings, sheets and submittals. Building
143
+ over all 18 → **153 sections / 6,890 chunks, 0 leaked** — so the section eval is capped at ±8 pts by the
144
+ *source*, not by code. **≥400 sections needs more public project manuals.**
145
+ - No public MasterFormat-labelled dataset exists: Hugging Face dataset search returns **0** for
146
+ `masterformat` / `construction specifications` / `CSI division`; Kaggle has none either.
147
+ - Public bid tabulations are real but are labelled with **agency** codes, not CSI: `aniket0712/bid-tabulations`
148
+ (TxDOT, 960,791 rows) has `BID ITEM DESCRIPTION` + `SPECIFICATION CODE` (TxDOT's own numbering). Useful as
149
+ weak-label **training** text (P0.2) and as a **candidate pool** for labelling — not as a true-label eval.
150
+
151
+ **Produced.** `eval/line_items_v2_candidates.tsv` — **2,000 unique real line items** (deduped, cleaned) with
152
+ their source spec code and a blank `true_group` for a reviewer to fill. That is the automated half of P0.1's
153
+ "≥1,000 real line items"; the labels are the human half.
154
+
155
+ **Conclusion.** Both halves of P0.1 are blocked on **data and labelling, not modelling**: more public
156
+ project manuals for sections, and human labels (or a licensed source) for line items. The mf-0.5 Kaggle run
157
+ already confirmed the point — `val` rose (+2.3) while line items (−1.6) and sections (−1.3) fell, i.e. the
158
+ auxiliary coarse-to-fine loss bought nothing on the target distribution.
159
+
160
+ ---
161
+
162
+ ## Result 2026-10-13 — P0.2 first pass: real bid items in the TF-IDF scorer (`runs/bidtab/`, NOT published)
163
+
164
+ Data: full TxDOT bid tabulation (960k rows -> 13,239 unique item texts, `data/raw/txdot_bid_items_unique.jsonl`)
165
+ labelled by the eval crosswalk + 45 training-only codes (`data/txdot_crosswalk_train.json`, titles taken from
166
+ TxDOT's own SPECIFICATION DESCRIPTION column). After dropping every eval text and every eval text up to digits:
167
+ 2,842 training items, 19 groups. Weight x3 chosen on a dev split of training-only codes. Script:
168
+ `scripts/bidtab_experiment.py`, results `runs/bidtab-exp.json`. Transformer unchanged (mf-0.2, blend w 0.25).
169
+
170
+ | eval | published ensemble | + bid items | paired |
171
+ |---|---:|---:|---|
172
+ | TxDOT, **item numbers held out** (5-fold grouped CV), n 790 | 0.489 [.454,.523] | **0.608** [.573,.641] | 104 vs 10, p < 0.001 |
173
+ | TxDOT capped <= 20/item, n 529 | 0.369 [.329,.411] | **0.492** [.449,.534] | 73 vs 8, p < 0.001 |
174
+ | hand-labelled building line items, n 194 | 0.691 | 0.701 | 3 vs 1, p 0.63 |
175
+ | manual chunks, n 6,890 | 0.326 | 0.329 | 65 vs 47, p 0.11 |
176
+ | whole sections top-1 / division, n 153 | 0.620 / 0.804 | 0.613 / 0.791 | -1 / -2 sections |
177
+ | UFGS val, n 11,598 | 0.5875 | 0.5836 | 61 vs 106, p < 0.001 |
178
+
179
+ TxDOT with item numbers *seen* (texts deduped) is 0.953 — memorisation of item types, not a headline number.
180
+
181
+ Gate check (§3): lift on real line items is large and significant on the agency set but not measurable on the
182
+ 194-item building set; **section division -1.3 pts misses the "within 1 pt" gate by 2 sections** (noise-level,
183
+ but the gate was set in advance) and UFGS val drops 0.4 pts (significant). Decision left to the owner:
184
+ ship as default, ship as an opt-in "bid-item" TF-IDF, or first enlarge the section eval (P0.1) to tell a
185
+ 2-section move from noise.
README.md CHANGED
@@ -240,6 +240,36 @@ non-overlapping intervals. It is still well below the 70 % on hand-written build
240
  bid items are out of domain for a UFGS-trained model, and these labels come from an item-number crosswalk
241
  (reviewed, not per-item), covering civil work only.
242
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  ## Calibration and abstention
244
 
245
  The ensemble is already close to calibrated; a single **temperature** `T = 0.90` (fit on `val`) lowers the
@@ -342,9 +372,10 @@ manual-chunk and whole-section accuracy. Constructelligence's proprietary models
342
  - `predict.py` — CLI example: batching, `--top-k`, `--divisions`, `--json`, stdin/file input, `--onnx`.
343
  - `onnx/` — ONNX fp32 and int8 exports.
344
  - `ensemble/` — `tfidf.joblib` (word + char n-grams, ~112 MB), `blend.json`, `predict_ensemble.py`: the
345
- transformer + TF-IDF line-item ensemble.
346
  - `abbrev.py` — bid-item abbreviation expansion used by both predictors (`--no-expand` to disable).
347
  - `txdot_crosswalk.json` — TxDOT item number → MasterFormat group table behind the bid-item evaluation.
 
348
  - `IMPROVEMENT-PLAN.md` — the prioritised roadmap (enlarged eval, data, domain-adaptive pretraining, distillation).
349
  - `CITATION.cff`, `requirements.txt`.
350
 
 
240
  bid items are out of domain for a UFGS-trained model, and these labels come from an item-number crosswalk
241
  (reviewed, not per-item), covering civil work only.
242
 
243
+ ### Bid-item mode (`--bid-items`, opt-in)
244
+
245
+ `ensemble/tfidf_bid.joblib` is the same TF-IDF scorer **also trained on 2,842 real TxDOT bid items**: the full
246
+ public bid tabulation (960k rows → 13,239 unique descriptions), labelled through the crosswalk above plus 45
247
+ training-only item codes (`txdot_crosswalk_train.json`, mapped from TxDOT's own specification titles). Every
248
+ evaluation description — and every description equal to one after removing numbers ("TREE REMOVAL (4" - 12" DIA)"
249
+ vs "(12" - 18" DIA)") — was removed from training. Bid items are weighted ×3, chosen on a dev split of
250
+ training-only codes.
251
+
252
+ ```bash
253
+ python ensemble/predict_ensemble.py --bid-items "CL C CONC (RAIL FOUNDATION)" "SET (TY II) (24 IN) (RCP) (6: 1) (P)"
254
+ ```
255
+
256
+ The honest number is **item-held-out**: 5-fold cross-validation grouped by TxDOT item number, so each evaluation
257
+ row is scored by a model that saw no bid item of its type (new item types, not paraphrases). Paired against the
258
+ default on the same rows:
259
+
260
+ | Ensemble on 790 TxDOT bid items | default | `--bid-items` | changed |
261
+ |---|---:|---:|---|
262
+ | top-1, item types held out | 0.489 [0.454, 0.523] | **0.608 [0.573, 0.641]** | 104 fixed, 10 broken (McNemar p < 0.001) |
263
+ | top-1 capped ≤ 20/item (n = 529) | 0.369 [0.329, 0.411] | **0.492 [0.449, 0.534]** | 73 fixed, 8 broken |
264
+
265
+ The shipped `tfidf_bid.joblib` has seen these item *types* (not these descriptions); on them it reaches 0.95 —
266
+ that is memorised item types, not a measure of quality on new agencies' items.
267
+
268
+ **Why it is opt-in.** On building text it is a wash or slightly worse: 194 hand-labelled line items 0.691 → 0.701
269
+ (3 vs 1 changed), manual chunks 0.326 → 0.329, whole sections 0.620 → 0.613 top-1 and 0.804 → 0.791 division
270
+ (2 of 153 sections), UFGS val 0.588 → 0.584. Use it for agency / DOT bid tabulations, keep the default for
271
+ specifications and building estimates.
272
+
273
  ## Calibration and abstention
274
 
275
  The ensemble is already close to calibrated; a single **temperature** `T = 0.90` (fit on `val`) lowers the
 
372
  - `predict.py` — CLI example: batching, `--top-k`, `--divisions`, `--json`, stdin/file input, `--onnx`.
373
  - `onnx/` — ONNX fp32 and int8 exports.
374
  - `ensemble/` — `tfidf.joblib` (word + char n-grams, ~112 MB), `blend.json`, `predict_ensemble.py`: the
375
+ transformer + TF-IDF line-item ensemble; `tfidf_bid.joblib` (~115 MB) is the opt-in `--bid-items` scorer.
376
  - `abbrev.py` — bid-item abbreviation expansion used by both predictors (`--no-expand` to disable).
377
  - `txdot_crosswalk.json` — TxDOT item number → MasterFormat group table behind the bid-item evaluation.
378
+ - `txdot_crosswalk_train.json` — 45 further TxDOT codes used only to label `--bid-items` training data.
379
  - `IMPROVEMENT-PLAN.md` — the prioritised roadmap (enlarged eval, data, domain-adaptive pretraining, distillation).
380
  - `CITATION.cff`, `requirements.txt`.
381
 
ensemble/predict_ensemble.py CHANGED
@@ -10,6 +10,7 @@ manual-chunk accuracy). This script reproduces the blend. The weight is chosen o
10
  python predict_ensemble.py "EPDM membrane roofing" "4000 psi concrete slab on grade"
11
  python predict_ensemble.py --top-k 5 --divisions "12\" RCP storm drain pipe"
12
  python predict_ensemble.py --json --file items.txt > out.json
 
13
 
14
  # default TF-IDF/results live beside this script; point --model at the Hub or a local run
15
  python predict_ensemble.py --model constructelligence/masterformat-classifier \
@@ -98,10 +99,14 @@ def main():
98
  ap.add_argument("--file")
99
  ap.add_argument("--json", action="store_true")
100
  ap.add_argument("--no-expand", action="store_true", help="do not expand bid-item abbreviations first")
 
 
101
  a = ap.parse_args()
 
 
102
 
103
  if a.weight is None:
104
- bf = Path(a.tfidf).with_name("blend.json")
105
  a.weight = json.loads(bf.read_text())["weight"] if bf.exists() else 0.2
106
  w = a.weight
107
 
 
10
  python predict_ensemble.py "EPDM membrane roofing" "4000 psi concrete slab on grade"
11
  python predict_ensemble.py --top-k 5 --divisions "12\" RCP storm drain pipe"
12
  python predict_ensemble.py --json --file items.txt > out.json
13
+ python predict_ensemble.py --bid-items "CL C CONC (RAIL FOUNDATION)" "RE PM W/RET REQ TY I (W)6\"(SLD)" # agency bid tabs
14
 
15
  # default TF-IDF/results live beside this script; point --model at the Hub or a local run
16
  python predict_ensemble.py --model constructelligence/masterformat-classifier \
 
99
  ap.add_argument("--file")
100
  ap.add_argument("--json", action="store_true")
101
  ap.add_argument("--no-expand", action="store_true", help="do not expand bid-item abbreviations first")
102
+ ap.add_argument("--bid-items", action="store_true",
103
+ help="agency bid-tab mode: use tfidf_bid.joblib (also trained on 2,842 real TxDOT bid items)")
104
  a = ap.parse_args()
105
+ if a.bid_items and a.tfidf == str(HERE / "tfidf.joblib"):
106
+ a.tfidf = str(HERE / "tfidf_bid.joblib") # same blend weight (blend.json) as the default scorer
107
 
108
  if a.weight is None:
109
+ bf = HERE / "blend.json" if a.bid_items else Path(a.tfidf).with_name("blend.json")
110
  a.weight = json.loads(bf.read_text())["weight"] if bf.exists() else 0.2
111
  w = a.weight
112
 
ensemble/tfidf_bid.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ac597727d5baf9e563c1f2d5ecc771de8f437ba65fa1fa7d9b1e25e967c5a4f6
3
+ size 115060552
metrics.json CHANGED
@@ -70,5 +70,71 @@
70
  "division": 0.57
71
  }
72
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  }
74
- }
 
70
  "division": 0.57
71
  }
72
  }
73
+ },
74
+ "txdot_bid_items_bidmode": {
75
+ "_doc": "ensemble/tfidf_bid.joblib (opt-in --bid-items); item-held-out = 5-fold CV grouped by TxDOT item number; paired vs default ensemble; building-set regression with the shipped model",
76
+ "item_held_out": {
77
+ "n": 790,
78
+ "top1": 0.6076,
79
+ "ci": [
80
+ 0.573,
81
+ 0.641
82
+ ],
83
+ "vs_published": {
84
+ "new_right_old_wrong": 104,
85
+ "old_right_new_wrong": 10,
86
+ "p": 0.0
87
+ }
88
+ },
89
+ "item_held_out_capped": {
90
+ "n": 529,
91
+ "top1": 0.4915,
92
+ "ci": [
93
+ 0.449,
94
+ 0.534
95
+ ],
96
+ "vs_published": {
97
+ "new_right_old_wrong": 73,
98
+ "old_right_new_wrong": 8,
99
+ "p": 0.0
100
+ }
101
+ },
102
+ "default_same_rows": {
103
+ "n": 790,
104
+ "top1": 0.4886,
105
+ "ci": [
106
+ 0.454,
107
+ 0.523
108
+ ]
109
+ },
110
+ "items_seen_optimistic": 0.9532,
111
+ "building_regression": {
112
+ "val": {
113
+ "default": 0.5875,
114
+ "bid_mode": 0.5836
115
+ },
116
+ "line_items": {
117
+ "default": 0.6907,
118
+ "bid_mode": 0.701
119
+ },
120
+ "manuals": {
121
+ "default": 0.3261,
122
+ "bid_mode": 0.3287
123
+ },
124
+ "manual_sections": {
125
+ "default": {
126
+ "sections": 153,
127
+ "top1": 0.62,
128
+ "division": 0.803921568627451
129
+ },
130
+ "bid_mode": {
131
+ "sections": 153,
132
+ "top1": 0.6133333333333333,
133
+ "division": 0.7908496732026143
134
+ }
135
+ }
136
+ },
137
+ "train_items": 2842,
138
+ "bid_weight": 3.0
139
  }
140
+ }
txdot_crosswalk_train.json ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_doc": "TRAINING-ONLY additions to eval/txdot_crosswalk.json: TxDOT specification codes -> MasterFormat level-2 group, for labelling bid items from the full TxDOT bid tabulation (Kaggle aniket0712/bid-tabulations). Titles are TxDOT's own SPECIFICATION DESCRIPTION strings from that file, not recalled from memory. Same rules as the eval crosswalk: only titles that map to one of the 171 groups without doubt; maintenance (7xx), temporary work-zone, signs and signals (34 40 00 is not a class), mixed scopes (400 structure excavation, 423 retaining walls, 450 railing, 434 bearings, 476 jacking/boring, 533 rumble strips, 305 RAP, 401 flowable fill) and all 4-digit special specifications are left out. Grouping follows the eval crosswalk's own choices (erosion control -> 31 20 00 as item 506; removal of existing work -> 02 40 00 as items 104/496; vehicle barriers -> 34 70 00 as item 540). The eval file's 790 rows keep their original labels. Built 2026-10-13.",
3
+ "items": {
4
+ "112": ["SUBGRADE WIDENING", "31 20 00"],
5
+ "134": ["BACKFILLING PAVEMENT EDGES", "31 20 00"],
6
+ "158": ["SPECIALIZED EXCAVATION WORK", "31 20 00"],
7
+ "169": ["SOIL RETENTION BLANKETS", "31 20 00"],
8
+ "160": ["TOPSOIL", "32 90 00"],
9
+ "161": ["COMPOST", "32 90 00"],
10
+ "162": ["SODDING FOR EROSION CONTROL", "32 90 00"],
11
+ "166": ["FERTILIZER", "32 90 00"],
12
+ "180": ["WILDFLOWER SEEDING", "32 90 00"],
13
+ "192": ["LANDSCAPE PLANTING", "32 90 00"],
14
+ "170": ["IRRIGATION SYSTEM", "32 80 00"],
15
+ "251": ["REWORKING BASE COURSES", "32 10 00"],
16
+ "276": ["CEMENT TREATMENT (PLANT-MIXED)", "31 30 00"],
17
+ "310": ["PRIME COAT", "32 10 00"],
18
+ "315": ["FOG SEAL", "32 10 00"],
19
+ "334": ["HOT-MIX COLD-LAID ASPHALT CONCRETE PAVEMENT", "32 10 00"],
20
+ "340": ["DENSE-GRADED HOT-MIX ASPHALT (SMALL QUANTITY)", "32 10 00"],
21
+ "341": ["DENSE-GRADED HOT-MIX ASPHALT", "32 10 00"],
22
+ "342": ["PERMEABLE FRICTION COURSE", "32 10 00"],
23
+ "344": ["SUPERPAVE MIXTURES", "32 10 00"],
24
+ "346": ["STONE-MATRIX ASPHALT", "32 10 00"],
25
+ "347": ["THIN OVERLAY MIXTURES (TOM)", "32 10 00"],
26
+ "348": ["THIN BONDED FRICTION COURSES", "32 10 00"],
27
+ "407": ["STEEL PILING", "31 60 00"],
28
+ "409": ["PRESTRESSED CONCRETE PILING", "31 60 00"],
29
+ "427": ["SURFACE FINISHES FOR CONCRETE", "03 30 00"],
30
+ "442": ["METAL FOR STRUCTURES", "05 10 00"],
31
+ "459": ["GABIONS AND GABION MATTRESSES", "31 30 00"],
32
+ "460": ["CORRUGATED METAL PIPE", "33 40 00"],
33
+ "467": ["SAFETY END TREATMENT", "33 40 00"],
34
+ "468": ["THERMOPLASTIC PIPE CULVERTS AND DRAINS", "33 40 00"],
35
+ "471": ["FRAMES, GRATES, RINGS AND COVERS", "33 40 00"],
36
+ "474": ["LINEAR DRAINS", "33 40 00"],
37
+ "481": ["PIPE OR DRAINS", "33 40 00"],
38
+ "556": ["PIPE UNDERDRAINS", "33 40 00"],
39
+ "514": ["PERMANENT CONCRETE TRAFFIC BARRIER", "34 70 00"],
40
+ "543": ["CABLE BARRIER SYSTEM", "34 70 00"],
41
+ "542": ["REMOVING METAL BEAM GUARD FENCE", "02 40 00"],
42
+ "528": ["COLORED TEXTURED CONCRETE AND LANDSCAPE PAVERS", "32 10 00"],
43
+ "536": ["CONCRETE MEDIANS AND DIRECTIONAL ISLANDS", "32 10 00"],
44
+ "552": ["WIRE FENCE", "32 30 00"],
45
+ "613": ["HIGH MAST ILLUMINATION POLES", "26 50 00"],
46
+ "614": ["HIGH MAST ILLUMINATION ASSEMBLIES", "26 50 00"],
47
+ "621": ["TRAY CABLE", "26 00 00"],
48
+ "628": ["ELECTRICAL SERVICES", "26 00 00"]
49
+ }
50
+ }