winsweb commited on
Commit
5907aab
·
1 Parent(s): df0419a

Optimize for Hugging Face Spaces with Java support

Browse files
.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
- FROM python:3.10-slim
2
-
3
- # Set environment variables
4
- ENV PYTHONDONTWRITEBYTECODE=1 \
5
- PYTHONUNBUFFERED=1 \
6
- PYTHONPATH=/app \
7
- HOME=/home/user
8
-
9
- WORKDIR /app
10
-
11
- # Create user
 
 
 
12
  RUN useradd -m -u 1000 user
13
 
14
- # Copy and install requirements first
 
 
 
 
 
 
 
 
 
 
 
 
15
  COPY --chown=user requirements.txt .
16
- RUN pip install --no-cache-dir --upgrade pip && \
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
- # surgically copy only what is needed
23
- COPY --chown=user telecom_anomaly/ telecom_anomaly/
24
- COPY --chown=user config/ config/
25
- COPY --chown=user main.py .
26
 
27
- # Create empty directories with right permissions
28
- RUN mkdir -p data/input data/output data/save_pdf logs data/raw_csv_temp data/output_process_data data/strictly_cleaned_data && \
29
- chown -R user:user /app
 
30
 
 
31
  USER user
32
 
33
- # Expose HF default port
34
  EXPOSE 7860
35
 
36
- # Run API
37
- CMD ["python3", "-m", "uvicorn", "telecom_anomaly.api:app", "--host", "0.0.0.0", "--port", "7860"]
 
 
 
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==2.0.3
3
- numpy==1.24.3
4
- scikit-learn==1.3.0
5
- pdfplumber==0.10.2
6
 
7
  # API
8
- fastapi==0.103.1
9
- uvicorn==0.23.2
10
- python-multipart==0.0.6
11
- pydantic==1.10.12
12
 
13
  # Export & PDF
14
- fpdf2==2.7.4
 
 
 
 
 
 
 
 
 
 
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 pdfplumber
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, exist_ok=True)
22
  except Exception as e:
23
  print(f"Warning: Could not create folder {folder}: {e}")
24
 
25
- # --- UPDATED EXTRACTION LOGIC (using pdfplumber) ---
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 using pdfplumber."""
65
  setup_folders()
66
  csv_path = os.path.join(RAW_CSV_DIR, f"{base_name}_raw.csv")
67
  try:
68
- lines = extract_pdf_to_lines(pdf_path)
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 saving CSV for {pdf_path}: {e}")
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
- try:
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
- # Intelligently find the date-time column
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(date_regex, val):
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
- # Append fragments to current row
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' and fragment != "":
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
- """Refined strict cleaning to be more resilient to parsing warnings."""
226
- df_temp = df.copy()
227
-
228
- # Ensure duration is numeric
229
- if 'duration' in df_temp.columns:
230
- if df_temp['duration'].dtype == 'object':
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 with robust fallback"""
57
- if not date_str or date_str == 'NULL' or not time_str or time_str == 'NULL':
58
- return None
59
-
60
  try:
61
- # Try standard format
62
- return datetime.strptime(f"{date_str} {time_str}", "%Y-%m-%d %H:%M:%S")
63
- except:
64
- try:
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' or time_str == 'None':
80
  return False
81
- # Handle both "HH:MM:SS" and full "YYYY-MM-DD HH:MM:SS"
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