Aryan Mishra commited on
Commit
2f9be58
·
1 Parent(s): db13628

feat: Phase 2 - preprocessing and EDA pipeline

Browse files
data/.gitignore CHANGED
@@ -1,2 +1 @@
1
  /raw
2
- /processed
 
1
  /raw
 
data/processed.dvc DELETED
@@ -1,6 +0,0 @@
1
- outs:
2
- - md5: d751713988987e9331980363e24189ce.dir
3
- size: 0
4
- nfiles: 0
5
- hash: md5
6
- path: processed
 
 
 
 
 
 
 
data/raw.dvc CHANGED
@@ -1,6 +1,6 @@
1
  outs:
2
- - md5: 0219de7f981995ce70939659481c9757.dir
3
- size: 5750191
4
- nfiles: 6
5
  hash: md5
6
  path: raw
 
1
  outs:
2
+ - md5: 0d10b4997c93bd1bce4a9d53a6e4748e.dir
3
+ size: 7943297
4
+ nfiles: 21
5
  hash: md5
6
  path: raw
dvc.lock ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ schema: '2.0'
2
+ stages:
3
+ preprocess_semeval:
4
+ cmd: PYTHONPATH=. python src/data/dataset.py
5
+ deps:
6
+ - path: data/raw/semeval_laptops
7
+ hash: md5
8
+ md5: 834bf3cc7b3d9216331adae54c9239c6.dir
9
+ size: 1104115
10
+ nfiles: 9
11
+ - path: data/raw/semeval_restaurants
12
+ hash: md5
13
+ md5: 07f540b34a0561f20908aaa70012defb.dir
14
+ size: 1625908
15
+ nfiles: 9
16
+ - path: src/data/dataset.py
17
+ hash: md5
18
+ md5: 4199a5c7d112040180779168f7da1a4a
19
+ size: 2761
20
+ - path: src/data/lang_detect.py
21
+ hash: md5
22
+ md5: ca10825874c1eeb68e199e0e178d8f94
23
+ size: 1053
24
+ - path: src/data/preprocess.py
25
+ hash: md5
26
+ md5: c01401432dfeb6e32e928d47570caa21
27
+ size: 855
28
+ - path: src/data/transliterate.py
29
+ hash: md5
30
+ md5: 2ab5e443eb6123188986e975586f42b8
31
+ size: 1404
32
+ outs:
33
+ - path: data/processed/semeval_test.jsonl
34
+ hash: md5
35
+ md5: 2c4581091b4aeb29a0734e44fd3a5656
36
+ size: 263576
37
+ - path: data/processed/semeval_train.jsonl
38
+ hash: md5
39
+ md5: 04b5486dc6a7c62d1289804bd373eeaa
40
+ size: 971123
41
+ preprocess_hindi:
42
+ cmd: PYTHONPATH=. python src/data/hindi_loader.py
43
+ deps:
44
+ - path: data/raw/amazon_hindi/hindi_sentiment.jsonl
45
+ hash: md5
46
+ md5: 152317207b7015c46f6260a93c2cd756
47
+ size: 1140222
48
+ - path: src/data/hindi_loader.py
49
+ hash: md5
50
+ md5: 6ffae4a0c61907d396b4e693beb6a23a
51
+ size: 1826
52
+ - path: src/data/lang_detect.py
53
+ hash: md5
54
+ md5: ca10825874c1eeb68e199e0e178d8f94
55
+ size: 1053
56
+ - path: src/data/preprocess.py
57
+ hash: md5
58
+ md5: c01401432dfeb6e32e928d47570caa21
59
+ size: 855
60
+ - path: src/data/transliterate.py
61
+ hash: md5
62
+ md5: 2ab5e443eb6123188986e975586f42b8
63
+ size: 1404
64
+ outs:
65
+ - path: data/processed/amazon_hindi.jsonl
66
+ hash: md5
67
+ md5: 29825aafaee80993c9fcb89669bb27ce
68
+ size: 304642
dvc.yaml ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ stages:
2
+ preprocess_semeval:
3
+ cmd: PYTHONPATH=. python src/data/dataset.py
4
+ deps:
5
+ - src/data/dataset.py
6
+ - src/data/preprocess.py
7
+ - src/data/lang_detect.py
8
+ - src/data/transliterate.py
9
+ - data/raw/semeval_restaurants
10
+ - data/raw/semeval_laptops
11
+ outs:
12
+ - data/processed/semeval_train.jsonl
13
+ - data/processed/semeval_test.jsonl
14
+
15
+ preprocess_hindi:
16
+ cmd: PYTHONPATH=. python src/data/hindi_loader.py
17
+ deps:
18
+ - src/data/hindi_loader.py
19
+ - src/data/preprocess.py
20
+ - src/data/lang_detect.py
21
+ - src/data/transliterate.py
22
+ - data/raw/amazon_hindi/hindi_sentiment.jsonl
23
+ outs:
24
+ - data/processed/amazon_hindi.jsonl
notebooks/01_data_exploration.ipynb CHANGED
The diff for this file is too large to render. See raw diff
 
src/data/dataset.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from pathlib import Path
3
+ from datasets import load_from_disk
4
+ from collections import defaultdict
5
+ from src.config import RAW_DIR, SEMEVAL_TRAIN_PATH, SEMEVAL_TEST_PATH
6
+ from src.data.preprocess import clean
7
+ from src.data.lang_detect import detect_language
8
+
9
+ def process_semeval():
10
+ rest_path = RAW_DIR / "semeval_restaurants"
11
+ lap_path = RAW_DIR / "semeval_laptops"
12
+
13
+ rest_data = load_from_disk(str(rest_path))
14
+ lap_data = load_from_disk(str(lap_path))
15
+
16
+ train_samples = defaultdict(list)
17
+ test_samples = defaultdict(list)
18
+
19
+ for ds_name, ds, source_name in [("train", rest_data["train"], "restaurants"),
20
+ ("test", rest_data["test"], "restaurants"),
21
+ ("train", lap_data["train"], "laptops"),
22
+ ("test", lap_data["test"], "laptops")]:
23
+ target = train_samples if ds_name == "train" else test_samples
24
+ for row in ds:
25
+ text = row["text"]
26
+ span = row["span"]
27
+ label = row["label"]
28
+
29
+ target[(text, source_name)].append({
30
+ "term": span,
31
+ "polarity": label
32
+ })
33
+
34
+ SEMEVAL_TRAIN_PATH.parent.mkdir(parents=True, exist_ok=True)
35
+
36
+ for path, data_dict in [(SEMEVAL_TRAIN_PATH, train_samples), (SEMEVAL_TEST_PATH, test_samples)]:
37
+ total = 0
38
+ lang_counts = defaultdict(int)
39
+ with open(path, "w", encoding="utf-8") as f:
40
+ for (text, source), aspects in data_dict.items():
41
+ lang = detect_language(text)
42
+ cleaned_text = clean(text, lang)
43
+ lang_counts[lang] += 1
44
+
45
+ final_aspects = []
46
+ for aspect in aspects:
47
+ term_clean = clean(aspect["term"], lang)
48
+ from_idx = cleaned_text.find(term_clean)
49
+ to_idx = from_idx + len(term_clean) if from_idx != -1 else -1
50
+ final_aspects.append({
51
+ "term": term_clean,
52
+ "polarity": aspect["polarity"],
53
+ "from": from_idx,
54
+ "to": to_idx
55
+ })
56
+
57
+ sample = {
58
+ "text": cleaned_text,
59
+ "language": lang,
60
+ "aspect_terms": final_aspects,
61
+ "source": source
62
+ }
63
+ f.write(json.dumps(sample, ensure_ascii=False) + "\n")
64
+ total += 1
65
+
66
+ print(f"SemEval {path.stem} samples: {total}")
67
+ print(f"SemEval {path.stem} languages: {dict(lang_counts)}")
68
+
69
+ if __name__ == "__main__":
70
+ process_semeval()
src/data/hindi_loader.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from pathlib import Path
3
+ from src.config import RAW_DIR, AMAZON_HINDI_PATH
4
+ from src.data.preprocess import clean
5
+ from src.data.lang_detect import detect_language
6
+
7
+ def process_hindi():
8
+ raw_path = RAW_DIR / "amazon_hindi" / "hindi_sentiment.jsonl"
9
+ if not raw_path.exists():
10
+ print(f"File not found: {raw_path}")
11
+ return
12
+
13
+ AMAZON_HINDI_PATH.parent.mkdir(parents=True, exist_ok=True)
14
+
15
+ total = 0
16
+ lang_counts = {"hi": 0, "hinglish": 0, "en": 0, "other": 0}
17
+
18
+ with open(raw_path, "r", encoding="utf-8") as fin, \
19
+ open(AMAZON_HINDI_PATH, "w", encoding="utf-8") as fout:
20
+ for line in fin:
21
+ row = json.loads(line)
22
+ text = row.get("INDIC REVIEW", row.get("text", ""))
23
+ if not text:
24
+ continue
25
+
26
+ label = str(row.get("LABEL", row.get("label", ""))).lower()
27
+ if label == "0" or label == "negative":
28
+ label = "negative"
29
+ elif label == "1" or label == "positive":
30
+ label = "positive"
31
+ else:
32
+ label = "neutral"
33
+
34
+ lang = detect_language(text)
35
+ if lang in lang_counts:
36
+ lang_counts[lang] += 1
37
+ else:
38
+ lang_counts[lang] = 1
39
+
40
+ cleaned_text = clean(text, lang)
41
+
42
+ sample = {
43
+ "text": cleaned_text,
44
+ "language": lang,
45
+ "label": label,
46
+ "source": "amazon_hindi"
47
+ }
48
+ fout.write(json.dumps(sample, ensure_ascii=False) + "\n")
49
+ total += 1
50
+
51
+ print(f"Hindi samples processed: {total}")
52
+ print(f"Hindi language distribution: {lang_counts}")
53
+
54
+ if __name__ == "__main__":
55
+ process_hindi()
src/data/lang_detect.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import fasttext
3
+ from src.config import FASTTEXT_MODEL_PATH
4
+ from pathlib import Path
5
+
6
+ _model = None
7
+
8
+ def get_model():
9
+ global _model
10
+ if _model is None:
11
+ _model = fasttext.load_model(str(FASTTEXT_MODEL_PATH))
12
+ return _model
13
+
14
+ def detect_language(text: str) -> str:
15
+ """Detects if text is en, hi, hinglish, or other."""
16
+ if not text or not text.strip():
17
+ return "other"
18
+
19
+ has_alpha = bool(re.search(r'[^\W\d_]', text))
20
+ if not has_alpha:
21
+ return "other"
22
+
23
+ text = text.replace('\n', ' ')
24
+ model = get_model()
25
+ predictions = model.predict(text, k=1)
26
+ label = predictions[0][0].replace("__label__", "")
27
+
28
+ has_latin = bool(re.search(r'[a-zA-Z]', text))
29
+ has_devanagari = bool(re.search(r'[\u0900-\u097F]', text))
30
+
31
+ is_hinglish = has_latin and has_devanagari
32
+
33
+ if is_hinglish and label in ["en", "hi"]:
34
+ return "hinglish"
35
+
36
+ if label == "en":
37
+ return "en"
38
+ elif label == "hi":
39
+ return "hi"
40
+ else:
41
+ return "other"
src/data/preprocess.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import unicodedata
3
+ from src.data.transliterate import transliterate
4
+
5
+ def clean(text: str, language: str) -> str:
6
+ """Clean text by lowercasing, removing URLs/mentions/hashtags, normalizing unicode, stripping whitespace."""
7
+ if not text:
8
+ return ""
9
+
10
+ # lowercase
11
+ text = text.lower()
12
+
13
+ # remove URLs
14
+ text = re.sub(r'http\S+|www\.\S+', '', text)
15
+
16
+ # remove mentions
17
+ text = re.sub(r'@\w+', '', text)
18
+
19
+ # remove hashtags
20
+ text = re.sub(r'#\w+', '', text)
21
+
22
+ # Apply transliteration only for hi/hinglish inputs
23
+ if language in ["hi", "hinglish"]:
24
+ text = transliterate(text, language)
25
+
26
+ # normalize unicode
27
+ text = unicodedata.normalize("NFKC", text)
28
+
29
+ # strip whitespace
30
+ text = text.strip()
31
+ text = re.sub(r'\s+', ' ', text)
32
+
33
+ return text
src/data/transliterate.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import unicodedata
3
+ import re
4
+ from typing import Optional
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ try:
9
+ from indicnlp.transliterate.unicode_transliterate import ItransTransliterator
10
+ HAS_INDIC_NLP = True
11
+ except ImportError:
12
+ HAS_INDIC_NLP = False
13
+ logger.warning("indic-nlp-library not found. Transliteration will fallback to basic unicode handling.")
14
+
15
+ def transliterate(text: str, src_lang: str) -> str:
16
+ """Romanize Devanagari text."""
17
+ if src_lang not in ["hi", "hinglish"]:
18
+ return text
19
+
20
+ if HAS_INDIC_NLP:
21
+ try:
22
+ # We will process word by word if needed, but itrans translates string.
23
+ # actually to_itrans takes devanagari and romanizes it.
24
+ roman_text = ItransTransliterator.to_itrans(text, "hi")
25
+ except Exception as e:
26
+ logger.warning(f"indicnlp transliteration failed: {e}")
27
+ roman_text = text
28
+ else:
29
+ # Fallback to basic unicode normalization
30
+ roman_text = unicodedata.normalize("NFKD", text).encode("ascii", "ignore").decode("utf-8")
31
+
32
+ if not roman_text:
33
+ roman_text = text
34
+
35
+ # Normalize common Hinglish spellings
36
+ # replace acha / accha -> achha
37
+ roman_text = re.sub(r'\baccha\b', 'achha', roman_text, flags=re.IGNORECASE)
38
+ roman_text = re.sub(r'\bacha\b', 'achha', roman_text, flags=re.IGNORECASE)
39
+
40
+ return roman_text
tests/test_lang_detect.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from src.data.lang_detect import detect_language
2
+
3
+ def test_detect_language():
4
+ samples = [
5
+ ("This is a simple English sentence.", "en"),
6
+ ("The food was amazing!", "en"),
7
+ ("यह एक हिंदी वाक्य है।", "hi"),
8
+ ("मुझे यह उत्पाद बहुत पसंद आया।", "hi"),
9
+ ("The phone is great but battery life kharab hai.", "en"), # no devanagari -> en
10
+ ("Phone bahut badhiya hai, लेकिन battery is bad.", "hinglish"),
11
+ ("I love this! मुझे यह पसंद है", "hinglish"),
12
+ ("Bonjour tout le monde", "other"),
13
+ ("12345 67890 !@#", "other"),
14
+ ("Just english text with 123", "en"),
15
+ ("सिर्फ हिंदी 123", "hi")
16
+ ]
17
+ for text, expected in samples:
18
+ assert detect_language(text) == expected, f"Failed on '{text}', expected {expected}"