Skip to content

Commit a5d8129

Browse files
committed
feat: support csv preprocessing
1 parent efe19a5 commit a5d8129

5 files changed

Lines changed: 259 additions & 2 deletions

File tree

cbc.yaml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,50 @@ entrypoints:
8989
normalized_overwritten_file_output_S3_SECRET_KEY: null
9090
description: The File Input Overwritten with the normalized output
9191
type: file
92+
preprocess_csv_file:
93+
description: Entrypoint to preprocess a column of a .csv file
94+
envs:
95+
FILTER_STOPWORDS: true
96+
LANGUAGE: en
97+
NGRAM_MAX: 3
98+
NGRAM_MIN: 2
99+
TXT_DOWNLOAD_PATH: /tmp/input.txt
100+
UNIGRAM_NORMALIZER: lemma
101+
USE_NGRAMS: true
102+
inputs:
103+
csv_input:
104+
config:
105+
csv_file_BUCKET_NAME: null
106+
csv_file_FILE_EXT: txt
107+
csv_file_FILE_NAME: null
108+
csv_file_FILE_PATH: null
109+
csv_file_S3_ACCESS_KEY: null
110+
csv_file_S3_HOST: null
111+
csv_file_S3_PORT: null
112+
csv_file_S3_SECRET_KEY: null
113+
csv_file_SELECTED_ATTRIBUTE: abstract
114+
csv_file_ID_COLUMN: id
115+
description: A .txt file, each line will be treated as a document
116+
type: file
117+
outputs:
118+
normalized_docs_output:
119+
config:
120+
normalized_docs_DB_TABLE: null
121+
normalized_docs_DB_DSN: null
122+
normalized_docs_DB_SCHEMA: null
123+
description: Database Output, containing bib_id aswell as the normalized text
124+
type: database_table
125+
normalized_overwritten_file_output:
126+
config:
127+
normalized_overwritten_file_output_BUCKET_NAME: null
128+
normalized_overwritten_file_output_FILE_EXT: txt
129+
normalized_overwritten_file_output_FILE_NAME: null
130+
normalized_overwritten_file_output_FILE_PATH: null
131+
normalized_overwritten_file_output_S3_ACCESS_KEY: null
132+
normalized_overwritten_file_output_S3_HOST: null
133+
normalized_overwritten_file_output_S3_PORT: null
134+
normalized_overwritten_file_output_S3_SECRET_KEY: null
135+
description: The File Input Overwritten with the normalized output
136+
type: file
137+
92138
name: Language-Preprocessing

main.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
)
1818

1919
from preprocessing.core import Preprocessor
20-
from preprocessing.loader import TxtLoader, BibLoader
20+
from preprocessing.loader import CSVLoader, TxtLoader, BibLoader
2121
from preprocessing.models import DocumentRecord, PreprocessedDocument
2222

2323
logging.basicConfig(
@@ -48,11 +48,24 @@ class BIBFileInput(FileSettings, InputSettings):
4848
SELECTED_ATTRIBUTE: str = "Abstract"
4949

5050

51+
class CSVFileInput(FileSettings, InputSettings):
52+
__identifier__ = "csv_file"
53+
FILE_EXT: str = "csv"
54+
55+
SELECTED_ATTRIBUTE: str = "abstract"
56+
ID_COLUMN: str = "id"
57+
58+
5159
class NormalizedBIBOutput(FileSettings, OutputSettings):
5260
__identifier__ = "normalized_overwritten_file_output"
5361
FILE_EXT: str = "bib"
5462

5563

64+
class NormalizedCSVOutput(FileSettings, OutputSettings):
65+
__identifier__ = "normalized_overwritten_file_output"
66+
FILE_EXT: str = "csv"
67+
68+
5669
class PreprocessTXT(EnvSettings):
5770
LANGUAGE: str = "en"
5871
FILTER_STOPWORDS: bool = True
@@ -83,6 +96,21 @@ class PreprocessBIB(EnvSettings):
8396
normalized_overwritten_file_output: NormalizedBIBOutput
8497

8598

99+
class PreprocessCSV(EnvSettings):
100+
LANGUAGE: str = "en"
101+
FILTER_STOPWORDS: bool = True
102+
UNIGRAM_NORMALIZER: str = "lemma"
103+
USE_NGRAMS: bool = True
104+
NGRAM_MIN: int = 2
105+
NGRAM_MAX: int = 3
106+
107+
CSV_DOWNLOAD_PATH: str = "/tmp/input.csv"
108+
109+
csv_input: CSVFileInput
110+
normalized_docs_output: NormalizedDocsOutput
111+
normalized_overwritten_file_output: NormalizedCSVOutput
112+
113+
86114
def _write_preprocessed_docs_to_postgres(
87115
preprocessed_ouput: list[PreprocessedDocument],
88116
settings: DatabaseSettings,
@@ -172,3 +200,21 @@ def preprocess_bib_file(settings):
172200
overwrite_callback=loader.overwrite_with_results,
173201
settings=settings,
174202
)
203+
204+
205+
@entrypoint(PreprocessCSV)
206+
def preprocess_csv_file(settings):
207+
logger.info("Downloading CSV file...")
208+
S3Operations.download(settings.csv_input, settings.CSV_DOWNLOAD_PATH)
209+
210+
loader = CSVLoader(
211+
file_path=settings.CSV_DOWNLOAD_PATH,
212+
attribute=settings.csv_input.SELECTED_ATTRIBUTE,
213+
id_column=settings.csv_input.ID_COLUMN,
214+
)
215+
216+
_preprocess_and_store(
217+
documents=loader.document_records,
218+
overwrite_callback=loader.overwrite_with_results,
219+
settings=settings,
220+
)

preprocessing/loader.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22
import re
33
import bibtexparser
4+
import pandas as pd
45
from typing import List
56
from pathlib import Path
67

@@ -111,3 +112,86 @@ def overwrite_with_results(
111112
bibtexparser.dump(self.bib_db, f)
112113

113114
logger.info(f"BIB file successfully written to: {output_path}")
115+
116+
117+
class CSVLoader:
118+
def __init__(self, file_path: str, attribute: str, id_column: str = "id"):
119+
logger.info(
120+
f"Loading CSV file (attribute={attribute}, id_column={id_column})."
121+
)
122+
123+
self.file_path = file_path
124+
self.attribute = attribute
125+
self.id_column = id_column
126+
127+
self.df = pd.read_csv(file_path)
128+
129+
if self.attribute not in self.df.columns:
130+
raise ValueError(
131+
f"Column '{self.attribute}' not found in CSV file."
132+
)
133+
134+
if self.id_column not in self.df.columns:
135+
raise ValueError(
136+
f"ID column '{self.id_column}' not found in CSV file."
137+
)
138+
139+
self.document_records = self._build_document_records()
140+
141+
@staticmethod
142+
def _extract_doc_id(row: pd.Series, id_column: str) -> str:
143+
value = row.get(id_column)
144+
145+
if pd.isna(value):
146+
return "UNKNOWN_ID"
147+
148+
return str(value)
149+
150+
def _build_document_records(self) -> List[DocumentRecord]:
151+
records = []
152+
153+
for _, row in self.df.iterrows():
154+
doc_id = self._extract_doc_id(row, self.id_column)
155+
156+
raw_value = row.get(self.attribute, "")
157+
158+
if pd.isna(raw_value):
159+
raw_value = ""
160+
161+
normalized = normalize_text(str(raw_value))
162+
163+
records.append(
164+
DocumentRecord(
165+
doc_id=doc_id,
166+
text=normalized,
167+
)
168+
)
169+
170+
return records
171+
172+
def overwrite_with_results(
173+
self,
174+
preprocessed_docs: List[PreprocessedDocument],
175+
export_path: Path,
176+
) -> None:
177+
logger.info("Overwriting CSV documents with preprocessed text...")
178+
179+
output_path = Path.cwd() / export_path.name
180+
181+
preprocessed_dict = {doc.doc_id: doc for doc in preprocessed_docs}
182+
183+
updated_df = self.df.copy()
184+
185+
for idx, row in updated_df.iterrows():
186+
doc_id = self._extract_doc_id(row, self.id_column)
187+
188+
preprocessed = preprocessed_dict.get(doc_id)
189+
190+
if not preprocessed:
191+
continue
192+
193+
updated_df.at[idx, self.attribute] = " ".join(preprocessed.tokens)
194+
195+
updated_df.to_csv(output_path, index=False)
196+
197+
logger.info(f"CSV file successfully written to: {output_path}")

test/files/input.csv

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
id,title,abstract
2+
1,Neural Network Optimization,"This invention relates to optimizing neural network training using adaptive gradient techniques and distributed computation."
3+
2,Battery Management System,"A system for monitoring lithium ion battery health, temperature regulation, and charge balancing in electric vehicles."
4+
3,Medical Imaging Analysis,"Methods for automated tumor detection in MRI scans using convolutional neural networks and image segmentation."
5+
4,Autonomous Navigation,"A navigation framework for autonomous mobile robots using sensor fusion and probabilistic path planning."
6+
5,Natural Language Processing,"Techniques for multilingual text classification and semantic similarity detection using transformer architectures."

test/test_full.py

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import pandas as pd
55

66
from pathlib import Path
7-
from main import preprocess_bib_file, preprocess_txt_file
7+
from main import preprocess_bib_file, preprocess_txt_file, preprocess_csv_file
88
from botocore.exceptions import ClientError
99
from sqlalchemy import create_engine
1010

@@ -14,6 +14,7 @@
1414

1515
PG_USER = "postgres"
1616
PG_PASS = "postgres"
17+
DB_SCHEMA = ""
1718

1819
INPUT_FILE_NAME = "input"
1920
OUTPUT_FILE_NAME = "output"
@@ -82,6 +83,7 @@ def test_full_bib(s3_minio):
8283
# PostgreSQL output
8384
"normalized_docs_DB_DSN": f"postgresql://{PG_USER}:{PG_PASS}@127.0.0.1:5432/postgres",
8485
"normalized_docs_DB_TABLE": "normalized_docs_bib",
86+
"normalized_docs_DB_SCHEMA": DB_SCHEMA,
8587
"normalized_overwritten_file_output_S3_HOST": "http://127.0.0.1",
8688
"normalized_overwritten_file_output_S3_PORT": "9000",
8789
"normalized_overwritten_file_output_S3_ACCESS_KEY": MINIO_USER,
@@ -145,6 +147,7 @@ def test_full_txt(s3_minio):
145147
# Postgres output
146148
"normalized_docs_DB_DSN": f"postgresql://{PG_USER}:{PG_PASS}@127.0.0.1:5432/postgres",
147149
"normalized_docs_DB_TABLE": "normalized_docs_txt",
150+
"normalized_docs_DB_SCHEMA": DB_SCHEMA,
148151
"normalized_overwritten_file_output_S3_HOST": "http://127.0.0.1",
149152
"normalized_overwritten_file_output_S3_PORT": "9000",
150153
"normalized_overwritten_file_output_S3_ACCESS_KEY": MINIO_USER,
@@ -177,3 +180,75 @@ def test_full_txt(s3_minio):
177180
assert all(isinstance(t, str) for t in df.iloc[0]["tokens"])
178181

179182
# TODO: Test overwritten file upload
183+
184+
185+
def test_full_csv(s3_minio):
186+
csv_path = Path(__file__).parent / "files" / f"{INPUT_FILE_NAME}.csv"
187+
csv_bytes = csv_path.read_bytes()
188+
189+
# Upload input to MinIO
190+
s3_minio.put_object(
191+
Bucket=BUCKET_NAME,
192+
Key=f"{INPUT_FILE_NAME}.csv",
193+
Body=csv_bytes,
194+
)
195+
196+
env = {
197+
"UNIGRAM_NORMALIZER": "porter",
198+
# CSV input S3
199+
"csv_file_S3_HOST": "http://127.0.0.1",
200+
"csv_file_S3_PORT": "9000",
201+
"csv_file_S3_ACCESS_KEY": MINIO_USER,
202+
"csv_file_S3_SECRET_KEY": MINIO_PWD,
203+
"csv_file_BUCKET_NAME": BUCKET_NAME,
204+
"csv_file_FILE_PATH": "",
205+
"csv_file_FILE_NAME": INPUT_FILE_NAME,
206+
"csv_file_SELECTED_ATTRIBUTE": "abstract",
207+
"csv_file_ID_COLUMN": "id",
208+
# PostgreSQL output
209+
"normalized_docs_DB_DSN": (
210+
f"postgresql://{PG_USER}:{PG_PASS}@127.0.0.1:5432/postgres"
211+
),
212+
"normalized_docs_DB_TABLE": "normalized_docs_csv",
213+
"normalized_docs_DB_SCHEMA": DB_SCHEMA,
214+
# overwritten CSV output
215+
"normalized_overwritten_file_output_S3_HOST": "http://127.0.0.1",
216+
"normalized_overwritten_file_output_S3_PORT": "9000",
217+
"normalized_overwritten_file_output_S3_ACCESS_KEY": MINIO_USER,
218+
"normalized_overwritten_file_output_S3_SECRET_KEY": MINIO_PWD,
219+
"normalized_overwritten_file_output_BUCKET_NAME": BUCKET_NAME,
220+
"normalized_overwritten_file_output_FILE_PATH": "",
221+
"normalized_overwritten_file_output_FILE_NAME": OUTPUT_FILE_NAME,
222+
}
223+
224+
for k, v in env.items():
225+
os.environ[k] = v
226+
227+
# Run block
228+
preprocess_csv_file()
229+
230+
# Query PostgreSQL
231+
engine = create_engine(
232+
f"postgresql+psycopg2://{PG_USER}:{PG_PASS}@localhost:5432/"
233+
)
234+
235+
df = pd.read_sql_table("normalized_docs_csv", engine)
236+
237+
# Assertions
238+
assert len(df) > 0
239+
assert "doc_id" in df.columns
240+
assert "tokens" in df.columns
241+
242+
# IDs
243+
assert len(df["doc_id"]) == len(df)
244+
assert df["doc_id"].is_unique
245+
assert all(isinstance(x, str) for x in df["doc_id"])
246+
247+
# Convert PG arrays
248+
df["tokens"] = df["tokens"].apply(parse_pg_array)
249+
250+
# Token structure
251+
assert isinstance(df.iloc[0]["tokens"], list)
252+
assert all(isinstance(t, str) for t in df.iloc[0]["tokens"])
253+
254+
# TODO: Test overwritten file upload

0 commit comments

Comments
 (0)