hbaltuntas commited on
Commit
c5beb5d
·
verified ·
1 Parent(s): 26e9310

Upload 4 files

Browse files
Files changed (4) hide show
  1. README.md +22 -8
  2. app_hierarchical.py +95 -51
  3. classification_utils.py +50 -0
  4. requirements-dev.txt +1 -0
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: Automatic Assignment of Project Responsibilities to Stakeholders
3
  emoji: 🏗️
4
  colorFrom: blue
5
  colorTo: green
@@ -8,12 +8,11 @@ sdk_version: 1.31.0
8
  app_file: app_hierarchical.py
9
  pinned: false
10
  license: mit
11
- short_description: Automatic Assignment of Project Responsibilities
12
  ---
13
 
14
  # FIDIC Contract Hierarchical Responsibility Classifier
15
 
16
- This application performs **hierarchical classification** of construction contract clauses from construction responsible matrices. It uses an ensemble of machine learning models to:
17
 
18
  1. **Level 1 - Party Classification:** Identifies the main responsible party (e.g., Employer, Contractor, Engineer)
19
  2. **Level 2 - Stakeholder Classification:** Determines the specific stakeholder role within that party
@@ -22,7 +21,7 @@ This application performs **hierarchical classification** of construction contra
22
 
23
  - 🎯 **Two-level hierarchical classification** for precise responsibility allocation
24
  - 🤖 **Ensemble learning** using multiple ML algorithms (Naive Bayes, Logistic Regression, SVM, Random Forest, XGBoost, LightGBM, CatBoost)
25
- - 📊 **Probability scores** for both party and stakeholder predictions
26
  - 📥 **Excel/CSV export** of results
27
  - 🚀 **Fast processing** with optimized pipelines
28
 
@@ -45,6 +44,9 @@ Your file should contain at least:
45
  - A **text column** with contract clause content
46
  - A **category column** with risk/type classification
47
 
 
 
 
48
  Example:
49
  ```
50
  texts,category
@@ -57,15 +59,27 @@ texts,category
57
  This application is based on research from:
58
  - **Institution:** Department of Civil Engineering, Karadeniz Technical University, Trabzon, Turkey
59
  - **Authors:** Hayri Burak Altuntaş, Hasan Basri Başağa
60
- - **Topic:** Hierarchical Classification of Responsibility Allocation
61
 
62
  ## Technical Details
63
 
64
  - **NLP Pipeline:** Text cleaning, stopword removal, lemmatization
65
- - **Feature Engineering:** TF-IDF vectorization, categorical encoding
66
  - **Models:** 7 different ML algorithms in soft voting ensemble
67
- - **Optimization:** Trained on Responsibility Matrices
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
  ## License
70
 
71
- MIT License
 
1
  ---
2
+ title: FIDIC Hierarchical Responsibility Classifier
3
  emoji: 🏗️
4
  colorFrom: blue
5
  colorTo: green
 
8
  app_file: app_hierarchical.py
9
  pinned: false
10
  license: mit
 
11
  ---
12
 
13
  # FIDIC Contract Hierarchical Responsibility Classifier
14
 
15
+ This application performs **hierarchical classification** of construction contract clauses from FIDIC Red Book contracts. It uses an ensemble of machine learning models to:
16
 
17
  1. **Level 1 - Party Classification:** Identifies the main responsible party (e.g., Employer, Contractor, Engineer)
18
  2. **Level 2 - Stakeholder Classification:** Determines the specific stakeholder role within that party
 
21
 
22
  - 🎯 **Two-level hierarchical classification** for precise responsibility allocation
23
  - 🤖 **Ensemble learning** using multiple ML algorithms (Naive Bayes, Logistic Regression, SVM, Random Forest, XGBoost, LightGBM, CatBoost)
24
+ - 📊 **Joint confidence score** based on party and stakeholder probabilities
25
  - 📥 **Excel/CSV export** of results
26
  - 🚀 **Fast processing** with optimized pipelines
27
 
 
44
  - A **text column** with contract clause content
45
  - A **category column** with risk/type classification
46
 
47
+ Categories may be numeric ids (`0`–`15`) or free-text labels. Missing text cells are
48
+ treated as empty strings and reported in the interface.
49
+
50
  Example:
51
  ```
52
  texts,category
 
59
  This application is based on research from:
60
  - **Institution:** Department of Civil Engineering, Karadeniz Technical University, Trabzon, Turkey
61
  - **Authors:** Hayri Burak Altuntaş, Hasan Basri Başağa
62
+ - **Topic:** Hierarchical Classification of Responsibility Allocation in FIDIC Red Book Contracts
63
 
64
  ## Technical Details
65
 
66
  - **NLP Pipeline:** Text cleaning, stopword removal, lemmatization
67
+ - **Feature Engineering:** Count vectorization and categorical encoding
68
  - **Models:** 7 different ML algorithms in soft voting ensemble
69
+ - **Optimization:** Trained on FIDIC Red Book contract dataset
70
+
71
+ The upload limit is 50 MB and 100,000 rows to keep memory usage predictable in a
72
+ Streamlit deployment. Text and category must be selected from different columns.
73
+
74
+ ## Development
75
+
76
+ Install development dependencies and run the tests:
77
+
78
+ ```bash
79
+ pip install -r requirements-dev.txt
80
+ python -m unittest discover -s tests -v
81
+ ```
82
 
83
  ## License
84
 
85
+ MIT License
app_hierarchical.py CHANGED
@@ -1,4 +1,5 @@
1
  import os
 
2
  import time
3
  import streamlit as st
4
  import pandas as pd
@@ -9,13 +10,30 @@ import re
9
  import string
10
  from collections import Counter
11
  from typing import Dict
 
 
 
12
 
13
  # ------------------------------------------------------------
14
  # NLP Setup
15
  # ------------------------------------------------------------
16
  import nltk
17
- nltk.download("stopwords", quiet=True)
18
- nltk.download("wordnet", quiet=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  from nltk.corpus import stopwords
20
  from nltk.stem import WordNetLemmatizer
21
 
@@ -81,28 +99,7 @@ def map_prediction_labels(party_value, stakeholder_value):
81
 
82
  def map_category_labels(category_value):
83
  """Convert category ids to English category names."""
84
- category_map = {
85
- 0: "Contract",
86
- 1: "General",
87
- 2: "Taxes and Permits",
88
- 3: "Personnel and Facilities",
89
- 4: "Site, Office and Administrative Affairs",
90
- 5: "Transportation and Storage",
91
- 6: "Occupational Health and Safety",
92
- 7: "Auxiliary Equipment and Machinery",
93
- 8: "Documentation and Reporting",
94
- 9: "Insurance and Guarantees",
95
- 10: "Water and Energy Supply and Lighting",
96
- 11: "Labor",
97
- 12: "Inspection and Testing",
98
- 13: "Quality Control and Assurance",
99
- 14: "Design and Engineering",
100
- 15: "Construction Works",
101
- }
102
-
103
- if pd.isna(category_value):
104
- return ""
105
- return category_map.get(int(category_value), str(category_value))
106
 
107
 
108
  def style_results_table(df: pd.DataFrame):
@@ -199,7 +196,10 @@ class HierarchicalClassifier(BaseEstimator, ClassifierMixin):
199
  for p_val, sub_clf in self.child_clfs_.items():
200
  child_p = sub_clf.predict_proba(X.loc[:, "texts"])
201
  cols = [np.where(self.classes_ == c)[0][0] for c in sub_clf.classes_]
202
- proba[:, cols] += p_proba[:, p_val][:, None] * child_p
 
 
 
203
  return proba
204
 
205
  def predict_party(self, X: pd.DataFrame):
@@ -208,21 +208,28 @@ class HierarchicalClassifier(BaseEstimator, ClassifierMixin):
208
  def predict_proba_party(self, X: pd.DataFrame):
209
  return self.party_clf_.predict_proba(X)
210
 
211
- def predict_stakeholder(self, X: pd.DataFrame):
212
- p_pred = self.party_clf_.predict(X)
 
 
213
  final = np.empty(X.shape[0], dtype=self.classes_.dtype)
214
  for p_val in np.unique(p_pred):
215
  idx = p_pred == p_val
216
  final[idx] = self.child_clfs_[p_val].predict(X.loc[idx, "texts"])
217
  return final
218
 
219
- def predict_proba_stakeholder(self, X: pd.DataFrame):
220
- p_pred = self.party_clf_.predict(X)
221
- results = []
222
- for i, p_val in enumerate(p_pred):
223
- row_X = X.iloc[[i]]
224
- proba = self.child_clfs_[p_val].predict_proba(row_X["texts"])
225
- results.append(proba[0])
 
 
 
 
 
226
  return results
227
 
228
 
@@ -267,11 +274,16 @@ if 'analysis_stats' not in st.session_state:
267
  st.session_state.analysis_stats = None
268
  if 'compact_results_df' not in st.session_state:
269
  st.session_state.compact_results_df = None
 
 
270
 
271
  # ------------------------------------------------------------
272
  # Model path
273
  # ------------------------------------------------------------
274
- MODEL_PATH = "hierarchical_model.pkl"
 
 
 
275
 
276
  # ------------------------------------------------------------
277
  # Load model
@@ -336,12 +348,15 @@ uploaded = st.file_uploader(
336
 
337
  # Reset classification state when new file is uploaded
338
  if uploaded is not None:
339
- file_key = f"{uploaded.name}_{uploaded.size}"
 
340
  if 'current_file_key' not in st.session_state or st.session_state.current_file_key != file_key:
341
  st.session_state.current_file_key = file_key
342
  st.session_state.classification_done = False
343
  st.session_state.classified_df = None
344
  st.session_state.analysis_stats = None
 
 
345
 
346
  # ------------------------------------------------------------
347
  # File processing
@@ -350,8 +365,8 @@ if uploaded is not None:
350
  try:
351
  # Check file size
352
  file_size = uploaded.size / (1024 * 1024)
353
- if file_size > 200:
354
- st.error(f"❌ File size ({file_size:.2f} MB) exceeds the 200MB limit.")
355
  st.stop()
356
 
357
  filename = uploaded.name.lower()
@@ -376,6 +391,12 @@ if uploaded is not None:
376
  if df.empty:
377
  st.warning("⚠️ The uploaded file is empty.")
378
  st.stop()
 
 
 
 
 
 
379
 
380
  st.subheader("📄 Uploaded Data")
381
  st.caption(f"File size: {file_size:.2f} MB | Total rows: {len(df):,} | Total columns: {len(df.columns)}")
@@ -425,6 +446,10 @@ if uploaded is not None:
425
  )
426
 
427
  # ------------------------------------------------------------
 
 
 
 
428
 
429
  # Classification button
430
  if st.button("🚀 Run Hierarchical Classification", type="secondary", use_container_width=True):
@@ -440,7 +465,13 @@ if uploaded is not None:
440
  # Prepare input dataframe
441
  input_df = df[[text_col, cat_col]].copy()
442
  input_df.columns = ["texts", "category"]
443
- input_df["texts"] = input_df["texts"].astype(str).apply(clean_text)
 
 
 
 
 
 
444
  input_df["keywords"] = input_df["texts"].apply(extract_keywords)
445
  input_df = input_df.reset_index(drop=True)
446
 
@@ -454,7 +485,9 @@ if uploaded is not None:
454
  status_placeholder.info("🔄 Level 2: Classifying stakeholders...")
455
  progress_bar.progress(70)
456
 
457
- stakeholder_predictions = hclf.predict_stakeholder(input_df)
 
 
458
 
459
  progress_bar.progress(90)
460
  elapsed_time = time.time() - start_time
@@ -477,7 +510,9 @@ if uploaded is not None:
477
  if hasattr(hclf, "predict_proba_stakeholder"):
478
  try:
479
  status_placeholder.info("🔄 Calculating stakeholder probabilities...")
480
- stake_probs = hclf.predict_proba_stakeholder(input_df)
 
 
481
 
482
  all_stake_classes = set()
483
  for clf in hclf.child_clfs_.values():
@@ -513,7 +548,9 @@ if uploaded is not None:
513
  else:
514
  df["stakeholder_confidence"] = 0.0
515
 
516
- df["overall_confidence"] = df[["party_confidence", "stakeholder_confidence"]].max(axis=1)
 
 
517
  df["confidence_level"] = df["overall_confidence"].apply(confidence_label)
518
  df["needs_review"] = df["overall_confidence"] < 0.6
519
  df["keywords"] = input_df["keywords"].tolist()
@@ -526,8 +563,8 @@ if uploaded is not None:
526
  df["predicted_party_name"] = [label[0] for label in mapped_labels]
527
  df["predicted_stakeholder_name"] = [label[1] for label in mapped_labels]
528
 
529
- compact_df = df[[text_col, cat_col, "predicted_party_name", "predicted_stakeholder_name", "confidence_level", "overall_confidence", "needs_review", "keywords"]].copy()
530
- compact_df = compact_df.rename(columns={
531
  text_col: "Text",
532
  cat_col: "Category",
533
  "predicted_party_name": "Predicted Party",
@@ -537,16 +574,20 @@ if uploaded is not None:
537
  "needs_review": "Needs Review",
538
  "keywords": "Keywords",
539
  })
540
- if "Category" in compact_df.columns:
541
- compact_df["Category"] = compact_df["Category"].apply(map_category_labels)
 
 
 
 
 
 
542
  compact_df["Text"] = compact_df["Text"].astype(str).str.slice(0, 140)
543
  compact_df["Keywords"] = compact_df["Keywords"].astype(str).str.slice(0, 80)
544
- compact_df["Overall Confidence"] = compact_df["Overall Confidence"].round(3)
545
- compact_df["Needs Review"] = compact_df["Needs Review"].map({True: "⚠️ Yes", False: "No"})
546
- compact_df = compact_df.reset_index(drop=True)
547
 
548
  st.session_state.classified_df = df.copy()
549
  st.session_state.compact_results_df = compact_df.copy()
 
550
  st.session_state.classification_done = True
551
 
552
  num_clauses = len(df)
@@ -644,7 +685,10 @@ if uploaded is not None:
644
  with col1:
645
  try:
646
  buffer = BytesIO()
647
- st.session_state.classified_df.to_excel(buffer, index=False, engine="openpyxl")
 
 
 
648
  buffer.seek(0)
649
 
650
  st.download_button(
@@ -659,7 +703,7 @@ if uploaded is not None:
659
  st.error(f"❌ Error creating Excel file: {str(e)}")
660
  st.info("Trying alternative CSV download...")
661
 
662
- csv_data = st.session_state.classified_df.to_csv(index=False)
663
  st.download_button(
664
  "📥 Download results as CSV",
665
  data=csv_data,
 
1
  import os
2
+ import hashlib
3
  import time
4
  import streamlit as st
5
  import pandas as pd
 
10
  import string
11
  from collections import Counter
12
  from typing import Dict
13
+ from pathlib import Path
14
+
15
+ from classification_utils import class_position, combined_confidence, map_category_label
16
 
17
  # ------------------------------------------------------------
18
  # NLP Setup
19
  # ------------------------------------------------------------
20
  import nltk
21
+
22
+
23
+ def ensure_nltk_resource(resource_path: str, package_name: str) -> None:
24
+ """Download an NLTK resource only when it is not already installed."""
25
+ try:
26
+ nltk.data.find(resource_path)
27
+ except LookupError:
28
+ if not nltk.download(package_name, quiet=True):
29
+ raise RuntimeError(
30
+ f"Required NLTK resource '{package_name}' is unavailable. "
31
+ f"Install it with: python -m nltk.downloader {package_name}"
32
+ )
33
+
34
+
35
+ ensure_nltk_resource("corpora/stopwords", "stopwords")
36
+ ensure_nltk_resource("corpora/wordnet", "wordnet")
37
  from nltk.corpus import stopwords
38
  from nltk.stem import WordNetLemmatizer
39
 
 
99
 
100
  def map_category_labels(category_value):
101
  """Convert category ids to English category names."""
102
+ return map_category_label(category_value)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
 
105
  def style_results_table(df: pd.DataFrame):
 
196
  for p_val, sub_clf in self.child_clfs_.items():
197
  child_p = sub_clf.predict_proba(X.loc[:, "texts"])
198
  cols = [np.where(self.classes_ == c)[0][0] for c in sub_clf.classes_]
199
+ party_col = class_position(self.party_clf_.classes_, p_val)
200
+ if party_col is None:
201
+ continue
202
+ proba[:, cols] += p_proba[:, party_col][:, None] * child_p
203
  return proba
204
 
205
  def predict_party(self, X: pd.DataFrame):
 
208
  def predict_proba_party(self, X: pd.DataFrame):
209
  return self.party_clf_.predict_proba(X)
210
 
211
+ def predict_stakeholder(self, X: pd.DataFrame, party_predictions=None):
212
+ p_pred = party_predictions
213
+ if p_pred is None:
214
+ p_pred = self.party_clf_.predict(X)
215
  final = np.empty(X.shape[0], dtype=self.classes_.dtype)
216
  for p_val in np.unique(p_pred):
217
  idx = p_pred == p_val
218
  final[idx] = self.child_clfs_[p_val].predict(X.loc[idx, "texts"])
219
  return final
220
 
221
+ def predict_proba_stakeholder(self, X: pd.DataFrame, party_predictions=None):
222
+ p_pred = party_predictions
223
+ if p_pred is None:
224
+ p_pred = self.party_clf_.predict(X)
225
+ results = [None] * X.shape[0]
226
+ for p_val in np.unique(p_pred):
227
+ positions = np.flatnonzero(p_pred == p_val)
228
+ probabilities = self.child_clfs_[p_val].predict_proba(
229
+ X.iloc[positions]["texts"]
230
+ )
231
+ for position, probability in zip(positions, probabilities):
232
+ results[position] = probability
233
  return results
234
 
235
 
 
274
  st.session_state.analysis_stats = None
275
  if 'compact_results_df' not in st.session_state:
276
  st.session_state.compact_results_df = None
277
+ if 'export_results_df' not in st.session_state:
278
+ st.session_state.export_results_df = None
279
 
280
  # ------------------------------------------------------------
281
  # Model path
282
  # ------------------------------------------------------------
283
+ APP_DIR = Path(__file__).resolve().parent
284
+ MODEL_PATH = APP_DIR / "hierarchical_model.pkl"
285
+ MAX_UPLOAD_MB = 50
286
+ MAX_UPLOAD_ROWS = 100_000
287
 
288
  # ------------------------------------------------------------
289
  # Load model
 
348
 
349
  # Reset classification state when new file is uploaded
350
  if uploaded is not None:
351
+ file_digest = hashlib.sha256(uploaded.getbuffer()).hexdigest()
352
+ file_key = f"{uploaded.name}_{file_digest}"
353
  if 'current_file_key' not in st.session_state or st.session_state.current_file_key != file_key:
354
  st.session_state.current_file_key = file_key
355
  st.session_state.classification_done = False
356
  st.session_state.classified_df = None
357
  st.session_state.analysis_stats = None
358
+ st.session_state.compact_results_df = None
359
+ st.session_state.export_results_df = None
360
 
361
  # ------------------------------------------------------------
362
  # File processing
 
365
  try:
366
  # Check file size
367
  file_size = uploaded.size / (1024 * 1024)
368
+ if file_size > MAX_UPLOAD_MB:
369
+ st.error(f"❌ File size ({file_size:.2f} MB) exceeds the {MAX_UPLOAD_MB}MB limit.")
370
  st.stop()
371
 
372
  filename = uploaded.name.lower()
 
391
  if df.empty:
392
  st.warning("⚠️ The uploaded file is empty.")
393
  st.stop()
394
+ if len(df) > MAX_UPLOAD_ROWS:
395
+ st.error(
396
+ f"❌ The file contains {len(df):,} rows; the limit is "
397
+ f"{MAX_UPLOAD_ROWS:,} rows. Split the file and try again."
398
+ )
399
+ st.stop()
400
 
401
  st.subheader("📄 Uploaded Data")
402
  st.caption(f"File size: {file_size:.2f} MB | Total rows: {len(df):,} | Total columns: {len(df.columns)}")
 
446
  )
447
 
448
  # ------------------------------------------------------------
449
+
450
+ if text_col == cat_col:
451
+ st.error("❌ Text and category columns must be different.")
452
+ st.stop()
453
 
454
  # Classification button
455
  if st.button("🚀 Run Hierarchical Classification", type="secondary", use_container_width=True):
 
465
  # Prepare input dataframe
466
  input_df = df[[text_col, cat_col]].copy()
467
  input_df.columns = ["texts", "category"]
468
+ empty_text_mask = input_df["texts"].isna() | input_df["texts"].astype(str).str.strip().eq("")
469
+ if empty_text_mask.any():
470
+ st.warning(
471
+ f"⚠️ {int(empty_text_mask.sum()):,} rows contain empty text and were "
472
+ "classified using an empty string."
473
+ )
474
+ input_df["texts"] = input_df["texts"].fillna("").astype(str).apply(clean_text)
475
  input_df["keywords"] = input_df["texts"].apply(extract_keywords)
476
  input_df = input_df.reset_index(drop=True)
477
 
 
485
  status_placeholder.info("🔄 Level 2: Classifying stakeholders...")
486
  progress_bar.progress(70)
487
 
488
+ stakeholder_predictions = hclf.predict_stakeholder(
489
+ input_df, party_predictions=party_predictions
490
+ )
491
 
492
  progress_bar.progress(90)
493
  elapsed_time = time.time() - start_time
 
510
  if hasattr(hclf, "predict_proba_stakeholder"):
511
  try:
512
  status_placeholder.info("🔄 Calculating stakeholder probabilities...")
513
+ stake_probs = hclf.predict_proba_stakeholder(
514
+ input_df, party_predictions=party_predictions
515
+ )
516
 
517
  all_stake_classes = set()
518
  for clf in hclf.child_clfs_.values():
 
548
  else:
549
  df["stakeholder_confidence"] = 0.0
550
 
551
+ df["overall_confidence"] = combined_confidence(
552
+ df["party_confidence"], df["stakeholder_confidence"]
553
+ )
554
  df["confidence_level"] = df["overall_confidence"].apply(confidence_label)
555
  df["needs_review"] = df["overall_confidence"] < 0.6
556
  df["keywords"] = input_df["keywords"].tolist()
 
563
  df["predicted_party_name"] = [label[0] for label in mapped_labels]
564
  df["predicted_stakeholder_name"] = [label[1] for label in mapped_labels]
565
 
566
+ export_df = df[[text_col, cat_col, "predicted_party_name", "predicted_stakeholder_name", "confidence_level", "overall_confidence", "needs_review", "keywords"]].copy()
567
+ export_df = export_df.rename(columns={
568
  text_col: "Text",
569
  cat_col: "Category",
570
  "predicted_party_name": "Predicted Party",
 
574
  "needs_review": "Needs Review",
575
  "keywords": "Keywords",
576
  })
577
+ if "Category" in export_df.columns:
578
+ export_df["Category"] = export_df["Category"].apply(map_category_labels)
579
+ export_df["Overall Confidence"] = export_df["Overall Confidence"].round(3)
580
+ export_df["Needs Review"] = export_df["Needs Review"].map({True: "⚠️ Yes", False: "No"})
581
+ export_df = export_df.reset_index(drop=True)
582
+
583
+ # Preserve complete values in downloads and truncate only the UI preview.
584
+ compact_df = export_df.copy()
585
  compact_df["Text"] = compact_df["Text"].astype(str).str.slice(0, 140)
586
  compact_df["Keywords"] = compact_df["Keywords"].astype(str).str.slice(0, 80)
 
 
 
587
 
588
  st.session_state.classified_df = df.copy()
589
  st.session_state.compact_results_df = compact_df.copy()
590
+ st.session_state.export_results_df = export_df.copy()
591
  st.session_state.classification_done = True
592
 
593
  num_clauses = len(df)
 
685
  with col1:
686
  try:
687
  buffer = BytesIO()
688
+ export_df = st.session_state.export_results_df
689
+ if export_df is None:
690
+ export_df = st.session_state.compact_results_df
691
+ export_df.to_excel(buffer, index=False, engine="openpyxl")
692
  buffer.seek(0)
693
 
694
  st.download_button(
 
703
  st.error(f"❌ Error creating Excel file: {str(e)}")
704
  st.info("Trying alternative CSV download...")
705
 
706
+ csv_data = export_df.to_csv(index=False)
707
  st.download_button(
708
  "📥 Download results as CSV",
709
  data=csv_data,
classification_utils.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+
4
+
5
+ CATEGORY_MAP = {
6
+ 0: "Contract",
7
+ 1: "General",
8
+ 2: "Taxes and Permits",
9
+ 3: "Personnel and Facilities",
10
+ 4: "Site, Office and Administrative Affairs",
11
+ 5: "Transportation and Storage",
12
+ 6: "Occupational Health and Safety",
13
+ 7: "Auxiliary Equipment and Machinery",
14
+ 8: "Documentation and Reporting",
15
+ 9: "Insurance and Guarantees",
16
+ 10: "Water and Energy Supply and Lighting",
17
+ 11: "Labor",
18
+ 12: "Inspection and Testing",
19
+ 13: "Quality Control and Assurance",
20
+ 14: "Design and Engineering",
21
+ 15: "Construction Works",
22
+ }
23
+
24
+
25
+ def map_category_label(category_value):
26
+ """Map known numeric category ids while preserving free-text categories."""
27
+ if pd.isna(category_value):
28
+ return ""
29
+
30
+ try:
31
+ numeric_value = float(category_value)
32
+ if numeric_value.is_integer():
33
+ return CATEGORY_MAP.get(int(numeric_value), str(category_value))
34
+ except (TypeError, ValueError):
35
+ pass
36
+
37
+ return str(category_value)
38
+
39
+
40
+ def combined_confidence(party_confidence, stakeholder_confidence):
41
+ """Return joint confidence for the two dependent classification stages."""
42
+ party = pd.to_numeric(party_confidence, errors="coerce").fillna(0.0).clip(0.0, 1.0)
43
+ stakeholder = pd.to_numeric(stakeholder_confidence, errors="coerce").fillna(0.0).clip(0.0, 1.0)
44
+ return party * stakeholder
45
+
46
+
47
+ def class_position(classes, label):
48
+ """Return a label's probability-column position without assuming numeric ids."""
49
+ matches = np.flatnonzero(np.asarray(classes) == label)
50
+ return int(matches[0]) if matches.size else None
requirements-dev.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ -r requirements.txt