Spaces:
Sleeping
Sleeping
Optimize for Hugging Face Spaces with Java support
Browse files- .dockerignore +0 -11
- Dockerfile +40 -27
- error.txt +24 -0
- requirements.txt +18 -9
- telecom_anomaly/utils/pdf_processor.py +50 -131
- telecom_anomaly/validation/validator.py +8 -24
.dockerignore
CHANGED
|
@@ -17,14 +17,3 @@ Task2.txt
|
|
| 17 |
Task3.txt
|
| 18 |
error.txt
|
| 19 |
tests/
|
| 20 |
-
.ipynb_checkpoints/
|
| 21 |
-
.cache/
|
| 22 |
-
node_modules/
|
| 23 |
-
*.exe
|
| 24 |
-
*.dll
|
| 25 |
-
*.so
|
| 26 |
-
*.bak
|
| 27 |
-
*.tmp
|
| 28 |
-
*.log
|
| 29 |
-
.DS_Store
|
| 30 |
-
Thumbs.db
|
|
|
|
| 17 |
Task3.txt
|
| 18 |
error.txt
|
| 19 |
tests/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Dockerfile
CHANGED
|
@@ -1,37 +1,50 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
| 12 |
RUN useradd -m -u 1000 user
|
| 13 |
|
| 14 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
COPY --chown=user requirements.txt .
|
| 16 |
-
RUN pip install --no-cache-dir --
|
| 17 |
-
pip install --no-cache-dir --user -r requirements.txt
|
| 18 |
-
|
| 19 |
-
# Add user local bin to path
|
| 20 |
-
ENV PATH="/home/user/.local/bin:${PATH}"
|
| 21 |
|
| 22 |
-
#
|
| 23 |
-
COPY --chown=user
|
| 24 |
-
COPY --chown=user config/ config/
|
| 25 |
-
COPY --chown=user main.py .
|
| 26 |
|
| 27 |
-
# Create
|
| 28 |
-
|
| 29 |
-
|
|
|
|
| 30 |
|
|
|
|
| 31 |
USER user
|
| 32 |
|
| 33 |
-
# Expose
|
| 34 |
EXPOSE 7860
|
| 35 |
|
| 36 |
-
#
|
| 37 |
-
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use official Python slim image
|
| 2 |
+
FROM python:3.9-slim
|
| 3 |
+
|
| 4 |
+
# Install Java Runtime Environment (REQUIRED for tabula-py to process PDFs)
|
| 5 |
+
# Also install other potentially useful system dependencies
|
| 6 |
+
RUN apt-get update && \
|
| 7 |
+
apt-get install -y --no-install-recommends \
|
| 8 |
+
default-jre \
|
| 9 |
+
gcc \
|
| 10 |
+
python3-dev \
|
| 11 |
+
&& apt-get clean && \
|
| 12 |
+
rm -rf /var/lib/apt/lists/*
|
| 13 |
+
|
| 14 |
+
# Set up a new user named "user" with user ID 1000
|
| 15 |
RUN useradd -m -u 1000 user
|
| 16 |
|
| 17 |
+
# Set environment variables
|
| 18 |
+
# HF Spaces needs the port to be 7860
|
| 19 |
+
# _JAVA_OPTIONS limits Java's heap size to avoid crashing the free tier (16GB RAM)
|
| 20 |
+
ENV HOME=/home/user \
|
| 21 |
+
PATH=/home/user/.local/bin:$PATH \
|
| 22 |
+
PYTHONPATH=/home/user/app \
|
| 23 |
+
JAVA_HOME=/usr/lib/jvm/default-java \
|
| 24 |
+
_JAVA_OPTIONS="-Xmx4g"
|
| 25 |
+
|
| 26 |
+
# Set the working directory to the user's home directory
|
| 27 |
+
WORKDIR $HOME/app
|
| 28 |
+
|
| 29 |
+
# Copy the requirements file and install dependencies as the "user"
|
| 30 |
COPY --chown=user requirements.txt .
|
| 31 |
+
RUN pip install --no-cache-dir --user -r requirements.txt
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
+
# Copy the rest of the application and set ownership
|
| 34 |
+
COPY --chown=user . .
|
|
|
|
|
|
|
| 35 |
|
| 36 |
+
# Create necessary directories and ensure they are writable
|
| 37 |
+
# We create them under the app directory to ensure they are within the user's scope
|
| 38 |
+
RUN mkdir -p data/input data/output data/save_pdf data/raw_csv_temp data/output_process_data data/strictly_cleaned_data logs && \
|
| 39 |
+
chmod -R 777 data logs
|
| 40 |
|
| 41 |
+
# Switch to the "user"
|
| 42 |
USER user
|
| 43 |
|
| 44 |
+
# Expose port 7860 (Hugging Face default)
|
| 45 |
EXPOSE 7860
|
| 46 |
|
| 47 |
+
# Command to run the API
|
| 48 |
+
# We use --workers 1 to save memory on free tier
|
| 49 |
+
# We use --timeout-keep-alive 60 to handle potentially slow PDF processing
|
| 50 |
+
CMD ["uvicorn", "telecom_anomaly.api:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "1", "--timeout-keep-alive", "60"]
|
error.txt
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
INFO:telecom_anomaly.core.detector:Initialized TelecomAnomalyDetector with 5 detectors
|
| 2 |
+
INFO:telecom_anomaly.io.reader:Reading data from C:\Users\winsweb\AppData\Local\Temp\tmpdwrf9kgk.csv
|
| 3 |
+
INFO:telecom_anomaly.io.reader:Read 551 valid records from 551 total rows
|
| 4 |
+
INFO:telecom_anomaly.core.detector:Loaded 551 valid records
|
| 5 |
+
INFO:telecom_anomaly.core.detector:Starting anomaly detection
|
| 6 |
+
INFO:telecom_anomaly.core.detector:DurationDetector: 21 anomalies
|
| 7 |
+
INFO:telecom_anomaly.core.detector:FrequencyDetector: 0 anomalies
|
| 8 |
+
INFO:telecom_anomaly.core.detector:SMSDetector: 0 anomalies
|
| 9 |
+
INFO:telecom_anomaly.core.detector:LocationDetector: 0 anomalies
|
| 10 |
+
INFO:telecom_anomaly.core.detector:TimePatternDetector: 9 anomalies
|
| 11 |
+
INFO:telecom_anomaly.core.detector:Detection completed in 0.10s
|
| 12 |
+
INFO:telecom_anomaly.core.detector:Rows with anomalies: 30/551
|
| 13 |
+
INFO:telecom_anomaly.io.exporter:Generating trackable data: C:\my_project\work\GH_Police\Backend\telecom_anomaly_detection\data\output\telecom_data_with_anomalies_20260324_161213.csv
|
| 14 |
+
INFO:telecom_anomaly.io.exporter:Exported 551 rows, 30 with anomalies
|
| 15 |
+
INFO:telecom_anomaly.io.exporter:Results exported to C:\my_project\work\GH_Police\Backend\telecom_anomaly_detection\data\output\telecom_anomaly_results_20260324_161213.json
|
| 16 |
+
INFO:telecom_anomaly.io.exporter:Results exported to C:\my_project\work\GH_Police\Backend\telecom_anomaly_detection\data\output\telecom_anomaly_results_20260324_161213.csv
|
| 17 |
+
INFO:telecom_anomaly.io.exporter:Report generated: C:\my_project\work\GH_Police\Backend\telecom_anomaly_detection\data\output\telecom_anomaly_report_20260324_161213.txt
|
| 18 |
+
INFO:telecom_anomaly.io.exporter:Suspicious subscribers exported to C:\my_project\work\GH_Police\Backend\telecom_anomaly_detection\data\output\suspicious_subscribers_20260324_161213.csv
|
| 19 |
+
ERROR:telecom_anomaly.api:Analysis failed: name 'Path' is not defined
|
| 20 |
+
Traceback (most recent call last):
|
| 21 |
+
File "C:\my_project\work\GH_Police\Backend\telecom_anomaly_detection\telecom_anomaly\api.py", line 72, in analyze_cdr
|
| 22 |
+
"saved_files": [Path(p).name for p in outputs.values()] if save_results else []
|
| 23 |
+
^^^^
|
| 24 |
+
NameError: name 'Path' is not defined
|
requirements.txt
CHANGED
|
@@ -1,14 +1,23 @@
|
|
| 1 |
# Core Analysis
|
| 2 |
-
pandas=
|
| 3 |
-
numpy=
|
| 4 |
-
scikit-learn=
|
| 5 |
-
|
| 6 |
|
| 7 |
# API
|
| 8 |
-
fastapi=
|
| 9 |
-
uvicorn=
|
| 10 |
-
python-multipart=
|
| 11 |
-
pydantic=
|
| 12 |
|
| 13 |
# Export & PDF
|
| 14 |
-
fpdf2=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# Core Analysis
|
| 2 |
+
pandas>=1.3.0
|
| 3 |
+
numpy>=1.21.0
|
| 4 |
+
scikit-learn>=1.0.0
|
| 5 |
+
tabula-py>=2.4.0
|
| 6 |
|
| 7 |
# API
|
| 8 |
+
fastapi>=0.95.0
|
| 9 |
+
uvicorn>=0.21.0
|
| 10 |
+
python-multipart>=0.0.6
|
| 11 |
+
pydantic>=1.10.0
|
| 12 |
|
| 13 |
# Export & PDF
|
| 14 |
+
fpdf2>=2.7.0
|
| 15 |
+
|
| 16 |
+
# Visualization (if needed for exports)
|
| 17 |
+
matplotlib>=3.4.0
|
| 18 |
+
seaborn>=0.11.0
|
| 19 |
+
|
| 20 |
+
# Development
|
| 21 |
+
pytest>=6.2.0
|
| 22 |
+
black>=21.0.0
|
| 23 |
+
flake8>=3.9.0
|
telecom_anomaly/utils/pdf_processor.py
CHANGED
|
@@ -1,13 +1,12 @@
|
|
| 1 |
-
import
|
| 2 |
import pandas as pd
|
| 3 |
import io
|
| 4 |
import re
|
| 5 |
import os
|
| 6 |
import tempfile
|
| 7 |
-
import csv
|
| 8 |
from pathlib import Path
|
| 9 |
|
| 10 |
-
# --- CONFIGURATION ---
|
| 11 |
DATA_BASE = Path("data")
|
| 12 |
RAW_CSV_DIR = DATA_BASE / "raw_csv_temp"
|
| 13 |
PROCESS_OUTPUT_DIR = DATA_BASE / "output_process_data"
|
|
@@ -18,63 +17,23 @@ def setup_folders():
|
|
| 18 |
for folder in [RAW_CSV_DIR, PROCESS_OUTPUT_DIR, STRICT_CLEAN_DIR]:
|
| 19 |
try:
|
| 20 |
if not os.path.exists(folder):
|
| 21 |
-
os.makedirs(folder
|
| 22 |
except Exception as e:
|
| 23 |
print(f"Warning: Could not create folder {folder}: {e}")
|
| 24 |
|
| 25 |
-
# ---
|
| 26 |
-
|
| 27 |
-
def extract_pdf_to_lines(pdf_file_path):
|
| 28 |
-
"""Stage 1: Returns raw CSV lines using pdfplumber (no Java required)."""
|
| 29 |
-
setup_folders()
|
| 30 |
-
output_lines = []
|
| 31 |
-
|
| 32 |
-
try:
|
| 33 |
-
with pdfplumber.open(pdf_file_path) as pdf:
|
| 34 |
-
for page in pdf.pages:
|
| 35 |
-
tables = page.extract_tables()
|
| 36 |
-
if not tables:
|
| 37 |
-
# If no tables found, try extracting text directly as a fallback
|
| 38 |
-
text = page.extract_text()
|
| 39 |
-
if text:
|
| 40 |
-
output_lines.extend([line + "\n" for line in text.splitlines()])
|
| 41 |
-
continue
|
| 42 |
-
|
| 43 |
-
for table in tables:
|
| 44 |
-
# Write each table to a CSV-formatted string
|
| 45 |
-
si = io.StringIO()
|
| 46 |
-
cw = csv.writer(si)
|
| 47 |
-
# Clean the data: remove None and replace newlines with spaces
|
| 48 |
-
cleaned_table = []
|
| 49 |
-
for row in table:
|
| 50 |
-
if row:
|
| 51 |
-
cleaned_row = [str(cell).replace('\n', ' ').strip() if cell is not None else "" for cell in row]
|
| 52 |
-
cleaned_table.append(cleaned_row)
|
| 53 |
-
|
| 54 |
-
cw.writerows(cleaned_table)
|
| 55 |
-
output_lines.extend(si.getvalue().splitlines(True))
|
| 56 |
-
si.close()
|
| 57 |
-
|
| 58 |
-
return output_lines
|
| 59 |
-
except Exception as e:
|
| 60 |
-
print(f"Error extracting {pdf_file_path} with pdfplumber: {e}")
|
| 61 |
-
return []
|
| 62 |
|
| 63 |
def extract_pdf_to_csv(pdf_path, base_name):
|
| 64 |
-
"""Stage 1: Convert PDF to a raw CSV file
|
| 65 |
setup_folders()
|
| 66 |
csv_path = os.path.join(RAW_CSV_DIR, f"{base_name}_raw.csv")
|
| 67 |
try:
|
| 68 |
-
|
| 69 |
-
with open(csv_path, 'w', encoding='utf-8') as f:
|
| 70 |
-
f.writelines(lines)
|
| 71 |
return csv_path
|
| 72 |
except Exception as e:
|
| 73 |
-
print(f"Error
|
| 74 |
return None
|
| 75 |
|
| 76 |
-
# --- REMAINING LOGIC (Exactly same as before) ---
|
| 77 |
-
|
| 78 |
def process_telecom_data_complete(input_path, base_name):
|
| 79 |
"""Stage 2: Parse raw CSV and repair fragmented rows."""
|
| 80 |
try:
|
|
@@ -95,6 +54,21 @@ def strict_clean_data(file_path, base_name):
|
|
| 95 |
df_cleaned.to_csv(final_path, index=False)
|
| 96 |
return final_path
|
| 97 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
def process_telecom_data_df(lines, base_name):
|
| 99 |
"""Stage 2 (In-Memory): Returns DataFrame."""
|
| 100 |
return _process_lines_logic(lines, base_name, save_files=False)
|
|
@@ -103,6 +77,8 @@ def strict_clean_df(df):
|
|
| 103 |
"""Stage 3 (In-Memory): Returns Cleaned DataFrame."""
|
| 104 |
return _strict_clean_logic(df)
|
| 105 |
|
|
|
|
|
|
|
| 106 |
def _process_lines_logic(lines, base_name, save_files=False):
|
| 107 |
header_idx = -1
|
| 108 |
for i, line in enumerate(lines):
|
|
@@ -111,99 +87,48 @@ def _process_lines_logic(lines, base_name, save_files=False):
|
|
| 111 |
break
|
| 112 |
|
| 113 |
if header_idx == -1:
|
| 114 |
-
print(f"DEBUG: Header 'event_date_time' not found in lines")
|
| 115 |
return None
|
| 116 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
call_text = "".join(lines[header_idx:])
|
| 118 |
-
|
| 119 |
-
df_raw = pd.read_csv(io.StringIO(call_text))
|
| 120 |
-
except Exception as e:
|
| 121 |
-
print(f"DEBUG: Failed to read CSV from lines: {e}")
|
| 122 |
-
return None
|
| 123 |
-
|
| 124 |
df_raw.columns = [c.strip() for c in df_raw.columns]
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
main_col = None
|
| 128 |
-
for col in df_raw.columns:
|
| 129 |
-
if "event_date_time" in col.lower():
|
| 130 |
-
main_col = col
|
| 131 |
-
break
|
| 132 |
-
|
| 133 |
-
if main_col is None:
|
| 134 |
-
main_col = df_raw.columns[0]
|
| 135 |
-
print(f"DEBUG: 'event_date_time' column not found, falling back to first column: {main_col}")
|
| 136 |
-
|
| 137 |
-
# Remove header repetitions that can happen with pdfplumber page-by-page extraction
|
| 138 |
-
df_raw = df_raw[df_raw[main_col].astype(str).str.contains("event_date_time|date_time") == False]
|
| 139 |
|
| 140 |
fixed_rows = []
|
| 141 |
current_row = None
|
| 142 |
-
# Support multiple date formats (YYYY-MM-DD or DD-MM-YYYY or DD/MM/YYYY)
|
| 143 |
-
date_regex = r'^(\d{4}-\d{2}-\d{2}|\d{2}-\d{2}-\d{4}|\d{2}/\d{2}/\d{4})'
|
| 144 |
-
|
| 145 |
for _, row in df_raw.iterrows():
|
| 146 |
val = str(row[main_col]).strip()
|
| 147 |
-
if re.match(
|
| 148 |
if current_row is not None:
|
| 149 |
fixed_rows.append(current_row)
|
| 150 |
current_row = row.copy()
|
| 151 |
elif current_row is not None:
|
| 152 |
-
|
| 153 |
-
# CRITICAL FIX: If the current value ends in a colon, don't add a space when joining
|
| 154 |
-
if str(current_row[main_col]).strip().endswith(':'):
|
| 155 |
-
current_row[main_col] = (str(current_row[main_col]).strip() + val).replace('\r', '').replace('\n', '')
|
| 156 |
-
else:
|
| 157 |
-
current_row[main_col] = (str(current_row[main_col]) + " " + val).replace('\r', '').replace('\n', '')
|
| 158 |
-
|
| 159 |
for col in df_raw.columns:
|
| 160 |
if col != main_col and pd.api.types.is_string_dtype(df_raw[col]):
|
| 161 |
fragment = str(row[col]).strip()
|
| 162 |
-
if fragment.lower() != 'nan'
|
| 163 |
current_row[col] = (str(current_row[col]) + " " + fragment).replace('\r', '').replace('\n', ' ')
|
| 164 |
|
| 165 |
if current_row is not None:
|
| 166 |
fixed_rows.append(current_row)
|
| 167 |
|
| 168 |
-
if not fixed_rows:
|
| 169 |
-
print(f"DEBUG: No rows matched date regex {date_regex}")
|
| 170 |
-
return None
|
| 171 |
-
|
| 172 |
df_calls = pd.DataFrame(fixed_rows)
|
| 173 |
-
# Clean up the joined column: remove any "01: 52:44" -> "01:52:44"
|
| 174 |
-
df_calls[main_col] = df_calls[main_col].str.replace(r'(\d{2}:)\s+(\d{2})', r'\1\2', regex=True)
|
| 175 |
df_calls[main_col] = df_calls[main_col].str.replace(r'[\r\n]+', '', regex=True).str.replace(r'\s+', ' ', regex=True).str.strip()
|
| 176 |
-
|
| 177 |
-
# Robust date parsing
|
| 178 |
-
try:
|
| 179 |
-
dt_series = pd.to_datetime(df_calls[main_col], errors='coerce')
|
| 180 |
-
# If too many NaT, try with dayfirst
|
| 181 |
-
if dt_series.isna().sum() > len(dt_series) * 0.5:
|
| 182 |
-
dt_series = pd.to_datetime(df_calls[main_col], errors='coerce', dayfirst=True)
|
| 183 |
-
except:
|
| 184 |
-
dt_series = pd.Series([None] * len(df_calls))
|
| 185 |
-
|
| 186 |
df_calls['event_date'] = dt_series.dt.date
|
| 187 |
df_calls['event_time'] = dt_series.dt.time
|
| 188 |
-
|
| 189 |
-
# Manual fallback for the specific "2024-01-01 01:52:44" format if pandas fails
|
| 190 |
-
mask = df_calls['event_date'].isna()
|
| 191 |
-
if mask.any():
|
| 192 |
-
for i in df_calls[mask].index:
|
| 193 |
-
try:
|
| 194 |
-
# Try simple split
|
| 195 |
-
parts = str(df_calls.loc[i, main_col]).split(' ')
|
| 196 |
-
if len(parts) >= 2:
|
| 197 |
-
df_calls.loc[i, 'event_date'] = pd.to_datetime(parts[0]).date()
|
| 198 |
-
df_calls.loc[i, 'event_time'] = pd.to_datetime(parts[1]).time()
|
| 199 |
-
except:
|
| 200 |
-
pass
|
| 201 |
-
|
| 202 |
-
# If the main column was NOT the first one, we should keep it for debugging or drop it if it's already split
|
| 203 |
-
if main_col in df_calls.columns:
|
| 204 |
-
df_calls.drop(columns=[main_col], inplace=True)
|
| 205 |
|
| 206 |
-
# Standardize column types and casing
|
| 207 |
uppercase_cols = ['district', 'city', 'usage_type', 'usage_sub_type', 'calling_no', 'called_no', 'imei', 'location_id', 'msisdn']
|
| 208 |
for col in df_calls.columns:
|
| 209 |
c_low = col.lower()
|
|
@@ -219,22 +144,16 @@ def _process_lines_logic(lines, base_name, save_files=False):
|
|
| 219 |
ordered_cols = ['event_date', 'event_time'] + [c for c in df_calls.columns if c not in ['event_date', 'event_time']]
|
| 220 |
df_calls = df_calls[ordered_cols]
|
| 221 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
return df_calls
|
| 223 |
|
| 224 |
def _strict_clean_logic(df):
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
df_temp['duration'] = df_temp['duration'].astype(str).str.replace(',', '.').str.extract('(\d+\.?\d*)')[0].astype(float)
|
| 232 |
-
|
| 233 |
-
# Drop rows where we absolutely don't have a date or time
|
| 234 |
-
# But be careful not to drop everything if just the time format check is strict
|
| 235 |
-
df_temp = df_temp.dropna(subset=['event_date', 'event_time'])
|
| 236 |
-
|
| 237 |
-
if df_temp.empty:
|
| 238 |
-
return df_temp
|
| 239 |
-
|
| 240 |
-
return df_temp
|
|
|
|
| 1 |
+
import tabula
|
| 2 |
import pandas as pd
|
| 3 |
import io
|
| 4 |
import re
|
| 5 |
import os
|
| 6 |
import tempfile
|
|
|
|
| 7 |
from pathlib import Path
|
| 8 |
|
| 9 |
+
# --- CONFIGURATION (Restored) ---
|
| 10 |
DATA_BASE = Path("data")
|
| 11 |
RAW_CSV_DIR = DATA_BASE / "raw_csv_temp"
|
| 12 |
PROCESS_OUTPUT_DIR = DATA_BASE / "output_process_data"
|
|
|
|
| 17 |
for folder in [RAW_CSV_DIR, PROCESS_OUTPUT_DIR, STRICT_CLEAN_DIR]:
|
| 18 |
try:
|
| 19 |
if not os.path.exists(folder):
|
| 20 |
+
os.makedirs(folder)
|
| 21 |
except Exception as e:
|
| 22 |
print(f"Warning: Could not create folder {folder}: {e}")
|
| 23 |
|
| 24 |
+
# --- ORIGINAL FUNCTIONS (Restored) ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
def extract_pdf_to_csv(pdf_path, base_name):
|
| 27 |
+
"""Stage 1: Convert PDF tables to a raw CSV file."""
|
| 28 |
setup_folders()
|
| 29 |
csv_path = os.path.join(RAW_CSV_DIR, f"{base_name}_raw.csv")
|
| 30 |
try:
|
| 31 |
+
tabula.convert_into(pdf_path, csv_path, output_format="csv", pages="all", lattice=True)
|
|
|
|
|
|
|
| 32 |
return csv_path
|
| 33 |
except Exception as e:
|
| 34 |
+
print(f"Error extracting {pdf_path}: {e}")
|
| 35 |
return None
|
| 36 |
|
|
|
|
|
|
|
| 37 |
def process_telecom_data_complete(input_path, base_name):
|
| 38 |
"""Stage 2: Parse raw CSV and repair fragmented rows."""
|
| 39 |
try:
|
|
|
|
| 54 |
df_cleaned.to_csv(final_path, index=False)
|
| 55 |
return final_path
|
| 56 |
|
| 57 |
+
# --- IN-MEMORY VERSIONS (Exact same logic) ---
|
| 58 |
+
|
| 59 |
+
def extract_pdf_to_lines(pdf_file_path):
|
| 60 |
+
"""Stage 1 (In-Memory): Returns raw CSV lines."""
|
| 61 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".csv") as tmp:
|
| 62 |
+
tmp_path = tmp.name
|
| 63 |
+
try:
|
| 64 |
+
tabula.convert_into(pdf_file_path, tmp_path, output_format="csv", pages="all", lattice=True)
|
| 65 |
+
with open(tmp_path, 'r', encoding='utf-8') as f:
|
| 66 |
+
lines = f.readlines()
|
| 67 |
+
return lines
|
| 68 |
+
finally:
|
| 69 |
+
if os.path.exists(tmp_path):
|
| 70 |
+
os.remove(tmp_path)
|
| 71 |
+
|
| 72 |
def process_telecom_data_df(lines, base_name):
|
| 73 |
"""Stage 2 (In-Memory): Returns DataFrame."""
|
| 74 |
return _process_lines_logic(lines, base_name, save_files=False)
|
|
|
|
| 77 |
"""Stage 3 (In-Memory): Returns Cleaned DataFrame."""
|
| 78 |
return _strict_clean_logic(df)
|
| 79 |
|
| 80 |
+
# --- SHARED LOGIC (To ensure 100% parity) ---
|
| 81 |
+
|
| 82 |
def _process_lines_logic(lines, base_name, save_files=False):
|
| 83 |
header_idx = -1
|
| 84 |
for i, line in enumerate(lines):
|
|
|
|
| 87 |
break
|
| 88 |
|
| 89 |
if header_idx == -1:
|
|
|
|
| 90 |
return None
|
| 91 |
|
| 92 |
+
if save_files:
|
| 93 |
+
setup_folders()
|
| 94 |
+
customer_text = "".join(lines[:header_idx])
|
| 95 |
+
df_customer = pd.read_csv(io.StringIO(customer_text))
|
| 96 |
+
df_customer.columns = [re.sub(r'[\r\n\s]+', ' ', c).strip() for c in df_customer.columns]
|
| 97 |
+
cust_path = os.path.join(PROCESS_OUTPUT_DIR, f'{base_name}_customer_info.csv')
|
| 98 |
+
df_customer.to_csv(cust_path, index=False)
|
| 99 |
+
|
| 100 |
call_text = "".join(lines[header_idx:])
|
| 101 |
+
df_raw = pd.read_csv(io.StringIO(call_text))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
df_raw.columns = [c.strip() for c in df_raw.columns]
|
| 103 |
+
main_col = df_raw.columns[0]
|
| 104 |
+
df_raw = df_raw[df_raw[main_col].astype(str).str.contains("event_date_time") == False]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
|
| 106 |
fixed_rows = []
|
| 107 |
current_row = None
|
|
|
|
|
|
|
|
|
|
| 108 |
for _, row in df_raw.iterrows():
|
| 109 |
val = str(row[main_col]).strip()
|
| 110 |
+
if re.match(r'^\d{4}-\d{2}-\d{2}', val):
|
| 111 |
if current_row is not None:
|
| 112 |
fixed_rows.append(current_row)
|
| 113 |
current_row = row.copy()
|
| 114 |
elif current_row is not None:
|
| 115 |
+
current_row[main_col] = (str(current_row[main_col]) + " " + val).replace('\r', '').replace('\n', '')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
for col in df_raw.columns:
|
| 117 |
if col != main_col and pd.api.types.is_string_dtype(df_raw[col]):
|
| 118 |
fragment = str(row[col]).strip()
|
| 119 |
+
if fragment.lower() != 'nan':
|
| 120 |
current_row[col] = (str(current_row[col]) + " " + fragment).replace('\r', '').replace('\n', ' ')
|
| 121 |
|
| 122 |
if current_row is not None:
|
| 123 |
fixed_rows.append(current_row)
|
| 124 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
df_calls = pd.DataFrame(fixed_rows)
|
|
|
|
|
|
|
| 126 |
df_calls[main_col] = df_calls[main_col].str.replace(r'[\r\n]+', '', regex=True).str.replace(r'\s+', ' ', regex=True).str.strip()
|
| 127 |
+
dt_series = pd.to_datetime(df_calls[main_col], errors='coerce')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
df_calls['event_date'] = dt_series.dt.date
|
| 129 |
df_calls['event_time'] = dt_series.dt.time
|
| 130 |
+
df_calls.drop(columns=[main_col], inplace=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
|
|
|
|
| 132 |
uppercase_cols = ['district', 'city', 'usage_type', 'usage_sub_type', 'calling_no', 'called_no', 'imei', 'location_id', 'msisdn']
|
| 133 |
for col in df_calls.columns:
|
| 134 |
c_low = col.lower()
|
|
|
|
| 144 |
ordered_cols = ['event_date', 'event_time'] + [c for c in df_calls.columns if c not in ['event_date', 'event_time']]
|
| 145 |
df_calls = df_calls[ordered_cols]
|
| 146 |
|
| 147 |
+
if save_files:
|
| 148 |
+
interim_path = os.path.join(PROCESS_OUTPUT_DIR, f'{base_name}_call_records_final.csv')
|
| 149 |
+
df_calls.to_csv(interim_path, index=False)
|
| 150 |
+
return interim_path
|
| 151 |
return df_calls
|
| 152 |
|
| 153 |
def _strict_clean_logic(df):
|
| 154 |
+
if 'duration' in df.columns and df['duration'].dtype == 'object':
|
| 155 |
+
df['duration'] = df['duration'].str.replace(',', '.').astype(float)
|
| 156 |
+
df['event_date_copy'] = pd.to_datetime(df['event_date'], errors='coerce')
|
| 157 |
+
df['event_time_check'] = pd.to_datetime(df['event_time'].astype(str), format='%H:%M:%S', errors='coerce')
|
| 158 |
+
df_cleaned = df.dropna().copy()
|
| 159 |
+
return df_cleaned.drop(columns=['event_date_copy', 'event_time_check'])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
telecom_anomaly/validation/validator.py
CHANGED
|
@@ -53,39 +53,23 @@ class DataValidator:
|
|
| 53 |
|
| 54 |
@staticmethod
|
| 55 |
def parse_datetime(date_str: str, time_str: str) -> Optional[datetime]:
|
| 56 |
-
"""Parse datetime from date and time strings
|
| 57 |
-
if not date_str or date_str == 'NULL' or not time_str or time_str == 'NULL':
|
| 58 |
-
return None
|
| 59 |
-
|
| 60 |
try:
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
except:
|
| 64 |
-
|
| 65 |
-
# Fallback to pandas for flexible parsing
|
| 66 |
-
import pandas as pd
|
| 67 |
-
combined = f"{date_str} {time_str}"
|
| 68 |
-
dt = pd.to_datetime(combined, errors='coerce')
|
| 69 |
-
if pd.notna(dt):
|
| 70 |
-
return dt.to_pydatetime()
|
| 71 |
-
except:
|
| 72 |
-
pass
|
| 73 |
return None
|
| 74 |
|
| 75 |
@staticmethod
|
| 76 |
def is_night_call(time_str: str, night_start: int = 23, night_end: int = 5) -> bool:
|
| 77 |
"""Check if call occurred at night"""
|
| 78 |
try:
|
| 79 |
-
if not time_str or time_str == 'NULL'
|
| 80 |
return False
|
| 81 |
-
|
| 82 |
-
ts = str(time_str).strip()
|
| 83 |
-
if ' ' in ts:
|
| 84 |
-
ts = ts.split(' ')[1]
|
| 85 |
-
|
| 86 |
-
hour = int(ts.split(':')[0])
|
| 87 |
return hour >= night_start or hour < night_end
|
| 88 |
-
except:
|
| 89 |
return False
|
| 90 |
|
| 91 |
@staticmethod
|
|
|
|
| 53 |
|
| 54 |
@staticmethod
|
| 55 |
def parse_datetime(date_str: str, time_str: str) -> Optional[datetime]:
|
| 56 |
+
"""Parse datetime from date and time strings"""
|
|
|
|
|
|
|
|
|
|
| 57 |
try:
|
| 58 |
+
if date_str and time_str and date_str != 'NULL' and time_str != 'NULL':
|
| 59 |
+
return datetime.strptime(f"{date_str} {time_str}", "%Y-%m-%d %H:%M:%S")
|
| 60 |
+
except (ValueError, TypeError) as e:
|
| 61 |
+
logger.debug(f"Failed to parse datetime: {date_str} {time_str} - {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
return None
|
| 63 |
|
| 64 |
@staticmethod
|
| 65 |
def is_night_call(time_str: str, night_start: int = 23, night_end: int = 5) -> bool:
|
| 66 |
"""Check if call occurred at night"""
|
| 67 |
try:
|
| 68 |
+
if not time_str or time_str == 'NULL':
|
| 69 |
return False
|
| 70 |
+
hour = int(time_str.split(':')[0])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
return hour >= night_start or hour < night_end
|
| 72 |
+
except (ValueError, IndexError):
|
| 73 |
return False
|
| 74 |
|
| 75 |
@staticmethod
|