tablas-python 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- helpers/__init__.py +27 -0
- helpers/display_helper.py +135 -0
- helpers/table_manager.py +418 -0
- tablas_python/__init__.py +68 -0
- tablas_python/cli.py +49 -0
- tablas_python-0.1.0.dist-info/METADATA +375 -0
- tablas_python-0.1.0.dist-info/RECORD +22 -0
- tablas_python-0.1.0.dist-info/WHEEL +5 -0
- tablas_python-0.1.0.dist-info/entry_points.txt +3 -0
- tablas_python-0.1.0.dist-info/licenses/LICENSE +21 -0
- tablas_python-0.1.0.dist-info/top_level.txt +3 -0
- utils/__init__.py +71 -0
- utils/batch_processor.py +111 -0
- utils/data_helpers.py +500 -0
- utils/excel_extractor.py +425 -0
- utils/excel_writer.py +188 -0
- utils/exporter.py +158 -0
- utils/file_utils.py +65 -0
- utils/pdf_extractor.py +129 -0
- utils/sqlite_extractor.py +133 -0
- utils/table_cleaner.py +246 -0
- utils/validator.py +155 -0
utils/table_cleaner.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Módulo para limpiar y estructurar datos tabulares crudos en DataFrames de pandas.
|
|
3
|
+
Permite descartar filas de 'basura' o metadatos superiores indicando la fila exacta
|
|
4
|
+
del encabezado (por ejemplo, fila 4), eliminar filas/columnas vacías y normalizar nombres.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import List, Any, Optional, Union
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TableCleaner:
|
|
13
|
+
"""
|
|
14
|
+
Clase utilitaria para transformar matrices de datos crudos (listas de listas o DataFrames sucios)
|
|
15
|
+
en DataFrames limpios y listos para análisis.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def _normalize_cell(val: Any) -> Any:
|
|
20
|
+
"""Limpia un valor individual: elimina saltos de línea molestos y espacios extra."""
|
|
21
|
+
if val is None:
|
|
22
|
+
return np.nan
|
|
23
|
+
if isinstance(val, str):
|
|
24
|
+
val_str = val.strip().replace("\r\n", " ").replace("\n", " ")
|
|
25
|
+
# Reducir múltiples espacios consecutivos
|
|
26
|
+
val_str = " ".join(val_str.split())
|
|
27
|
+
return val_str if val_str != "" else np.nan
|
|
28
|
+
return val
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def _matrix_to_dataframe(cls, raw_data: Union[List[List[Any]], pd.DataFrame]) -> pd.DataFrame:
|
|
32
|
+
"""Convierte una matriz o DataFrame existente en un DataFrame estandarizado."""
|
|
33
|
+
if isinstance(raw_data, pd.DataFrame):
|
|
34
|
+
df = raw_data.copy()
|
|
35
|
+
elif isinstance(raw_data, list):
|
|
36
|
+
if not raw_data:
|
|
37
|
+
return pd.DataFrame()
|
|
38
|
+
# Asegurar longitud uniforme en todas las filas
|
|
39
|
+
max_cols = max((len(r) for r in raw_data if isinstance(r, (list, tuple))), default=0)
|
|
40
|
+
if max_cols == 0:
|
|
41
|
+
return pd.DataFrame()
|
|
42
|
+
|
|
43
|
+
padded_rows = []
|
|
44
|
+
for row in raw_data:
|
|
45
|
+
if isinstance(row, (list, tuple)):
|
|
46
|
+
r_list = list(row)
|
|
47
|
+
if len(r_list) < max_cols:
|
|
48
|
+
r_list.extend([None] * (max_cols - len(r_list)))
|
|
49
|
+
padded_rows.append(r_list[:max_cols])
|
|
50
|
+
else:
|
|
51
|
+
padded_rows.append([row] + [None] * (max_cols - 1))
|
|
52
|
+
df = pd.DataFrame(padded_rows)
|
|
53
|
+
else:
|
|
54
|
+
raise TypeError(f"Tipo de datos no soportado: {type(raw_data)}. Debe ser list o pd.DataFrame.")
|
|
55
|
+
|
|
56
|
+
# Limpieza básica celda por celda
|
|
57
|
+
return df.map(cls._normalize_cell)
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def auto_detect_header_row(cls, df: pd.DataFrame, max_search_rows: int = 10) -> int:
|
|
61
|
+
"""
|
|
62
|
+
Heurística para detectar automáticamente qué fila contiene los encabezados.
|
|
63
|
+
Busca la primera fila con alta densidad de texto y pocos valores nulos.
|
|
64
|
+
Retorna el número de fila basado en 1 (1-indexed).
|
|
65
|
+
"""
|
|
66
|
+
best_row = 1
|
|
67
|
+
best_score = -1.0
|
|
68
|
+
|
|
69
|
+
limit = min(len(df), max_search_rows)
|
|
70
|
+
for idx in range(limit):
|
|
71
|
+
row_vals = df.iloc[idx].dropna().tolist()
|
|
72
|
+
if not row_vals:
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
# Cantidad de celdas no nulas
|
|
76
|
+
non_null_ratio = len(row_vals) / max(len(df.columns), 1)
|
|
77
|
+
# Celdas que son strings y tienen longitud representativa
|
|
78
|
+
str_count = sum(1 for v in row_vals if isinstance(v, str) and len(v.strip()) > 0 and not v.strip().replace('.', '', 1).isdigit())
|
|
79
|
+
str_ratio = str_count / max(len(row_vals), 1)
|
|
80
|
+
|
|
81
|
+
# Puntuación combinada
|
|
82
|
+
score = (non_null_ratio * 0.6) + (str_ratio * 0.4)
|
|
83
|
+
if score > best_score:
|
|
84
|
+
best_score = score
|
|
85
|
+
best_row = idx + 1 # 1-indexed
|
|
86
|
+
|
|
87
|
+
return best_row
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def clean(
|
|
91
|
+
cls,
|
|
92
|
+
raw_data: Union[List[List[Any]], pd.DataFrame],
|
|
93
|
+
header_row: Optional[Union[int, List[int], str]] = 1,
|
|
94
|
+
skip_footer: int = 0,
|
|
95
|
+
drop_empty_rows: bool = True,
|
|
96
|
+
drop_empty_cols: bool = True,
|
|
97
|
+
auto_clean_types: bool = True,
|
|
98
|
+
) -> pd.DataFrame:
|
|
99
|
+
"""
|
|
100
|
+
Limpia y estructura una tabla cruda.
|
|
101
|
+
|
|
102
|
+
Parámetros:
|
|
103
|
+
-----------
|
|
104
|
+
raw_data : list of lists o pd.DataFrame
|
|
105
|
+
Los datos crudos extraídos del PDF o Excel.
|
|
106
|
+
header_row : int, list of int, 'auto' o None (por defecto 1)
|
|
107
|
+
- int: Fila (1-indexed) que contiene los nombres de columnas. Todo lo que esté
|
|
108
|
+
arriba de esta fila se descarta como metadatos/basura.
|
|
109
|
+
- list[int]: Por ejemplo [3, 4] si los encabezados ocupan múltiples filas continuas.
|
|
110
|
+
- 'auto': Detecta automáticamente la fila más probable de encabezado.
|
|
111
|
+
- None / 0: No usa encabezado, asigna nombres genéricos (Col_1, Col_2, ...).
|
|
112
|
+
skip_footer : int (por defecto 0)
|
|
113
|
+
Cantidad de filas finales a descartar (por ejemplo notas al pie, totales agregados, etc.).
|
|
114
|
+
drop_empty_rows : bool (por defecto True)
|
|
115
|
+
Si elimina filas que estén completamente vacías.
|
|
116
|
+
drop_empty_cols : bool (por defecto True)
|
|
117
|
+
Si elimina columnas que estén completamente vacías.
|
|
118
|
+
auto_clean_types : bool (por defecto True)
|
|
119
|
+
Intenta convertir números y formatos evidentes.
|
|
120
|
+
|
|
121
|
+
Retorna:
|
|
122
|
+
--------
|
|
123
|
+
pd.DataFrame
|
|
124
|
+
DataFrame limpio listo para ser utilizado en Python.
|
|
125
|
+
"""
|
|
126
|
+
df_raw = cls._matrix_to_dataframe(raw_data)
|
|
127
|
+
if df_raw.empty:
|
|
128
|
+
return pd.DataFrame()
|
|
129
|
+
|
|
130
|
+
# Si se solicita detección automática
|
|
131
|
+
if header_row == 'auto':
|
|
132
|
+
header_row = cls.auto_detect_header_row(df_raw)
|
|
133
|
+
|
|
134
|
+
# Tratar footer si aplica
|
|
135
|
+
if skip_footer > 0 and len(df_raw) > skip_footer:
|
|
136
|
+
df_raw = df_raw.iloc[:-skip_footer]
|
|
137
|
+
|
|
138
|
+
if header_row is None or header_row == 0:
|
|
139
|
+
# Sin fila de encabezado: las columnas serán numéricas o genéricas
|
|
140
|
+
clean_df = df_raw.copy()
|
|
141
|
+
clean_df.columns = [f"Col_{i+1}" for i in range(len(clean_df.columns))]
|
|
142
|
+
elif isinstance(header_row, (list, tuple)):
|
|
143
|
+
# Encabezado multi-fila (e.g. [3, 4])
|
|
144
|
+
# Convertir a 0-indexed
|
|
145
|
+
h_indices = [h - 1 for h in header_row if 1 <= h <= len(df_raw)]
|
|
146
|
+
if not h_indices:
|
|
147
|
+
clean_df = df_raw.copy()
|
|
148
|
+
clean_df.columns = [f"Col_{i+1}" for i in range(len(clean_df.columns))]
|
|
149
|
+
else:
|
|
150
|
+
header_parts = []
|
|
151
|
+
for h_idx in h_indices:
|
|
152
|
+
header_parts.append(df_raw.iloc[h_idx].fillna("").astype(str).tolist())
|
|
153
|
+
|
|
154
|
+
# Combinar partes del encabezado
|
|
155
|
+
combined_cols = []
|
|
156
|
+
for col_idx in range(len(df_raw.columns)):
|
|
157
|
+
parts = [header_parts[row_i][col_idx].strip() for row_i in range(len(header_parts))]
|
|
158
|
+
parts = [p for p in parts if p]
|
|
159
|
+
col_name = " _ ".join(parts) if parts else f"Col_{col_idx+1}"
|
|
160
|
+
combined_cols.append(col_name)
|
|
161
|
+
|
|
162
|
+
last_header_idx = max(h_indices)
|
|
163
|
+
clean_df = df_raw.iloc[last_header_idx + 1:].copy()
|
|
164
|
+
clean_df.columns = combined_cols
|
|
165
|
+
else:
|
|
166
|
+
# header_row es un entero (1-indexed)
|
|
167
|
+
h_idx = int(header_row) - 1
|
|
168
|
+
if h_idx < 0 or h_idx >= len(df_raw):
|
|
169
|
+
raise ValueError(
|
|
170
|
+
f"La fila de encabezado {header_row} está fuera de rango. La tabla tiene {len(df_raw)} filas."
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
raw_headers = df_raw.iloc[h_idx].tolist()
|
|
174
|
+
# Las filas de datos empiezan después de header_row
|
|
175
|
+
clean_df = df_raw.iloc[h_idx + 1:].copy()
|
|
176
|
+
|
|
177
|
+
# Formatear nombres de columnas
|
|
178
|
+
cols = []
|
|
179
|
+
for i, h in enumerate(raw_headers):
|
|
180
|
+
if pd.isna(h) or str(h).strip() == "":
|
|
181
|
+
cols.append(f"Col_{i+1}")
|
|
182
|
+
else:
|
|
183
|
+
cols.append(str(h).strip())
|
|
184
|
+
clean_df.columns = cols
|
|
185
|
+
|
|
186
|
+
# Resolver nombres de columnas duplicados añadiendo sufijo
|
|
187
|
+
seen_cols = {}
|
|
188
|
+
unique_cols = []
|
|
189
|
+
for col in clean_df.columns:
|
|
190
|
+
if col in seen_cols:
|
|
191
|
+
seen_cols[col] += 1
|
|
192
|
+
unique_cols.append(f"{col}_{seen_cols[col]}")
|
|
193
|
+
else:
|
|
194
|
+
seen_cols[col] = 0
|
|
195
|
+
unique_cols.append(col)
|
|
196
|
+
clean_df.columns = unique_cols
|
|
197
|
+
|
|
198
|
+
# Eliminar filas completamente vacías
|
|
199
|
+
if drop_empty_rows:
|
|
200
|
+
clean_df = clean_df.dropna(how='all')
|
|
201
|
+
|
|
202
|
+
# Eliminar columnas completamente vacías
|
|
203
|
+
if drop_empty_cols:
|
|
204
|
+
clean_df = clean_df.dropna(axis=1, how='all')
|
|
205
|
+
|
|
206
|
+
# Reiniciar índice
|
|
207
|
+
clean_df = clean_df.reset_index(drop=True)
|
|
208
|
+
|
|
209
|
+
# Conversión automática de tipos numéricos si aplica
|
|
210
|
+
if auto_clean_types:
|
|
211
|
+
clean_df = cls._try_infer_types(clean_df)
|
|
212
|
+
|
|
213
|
+
return clean_df
|
|
214
|
+
|
|
215
|
+
@classmethod
|
|
216
|
+
def _try_infer_types(cls, df: pd.DataFrame) -> pd.DataFrame:
|
|
217
|
+
"""Intenta convertir columnas numéricas limpiando signos de moneda o separadores."""
|
|
218
|
+
df_out = df.copy()
|
|
219
|
+
for col in df_out.columns:
|
|
220
|
+
series = df_out[col]
|
|
221
|
+
if series.dtype == object:
|
|
222
|
+
# Probar si es convertible a numérico tras remover formato común
|
|
223
|
+
try:
|
|
224
|
+
# Limpiar comas/puntos comunes en español/inglés si es puro número
|
|
225
|
+
cleaned_series = series.astype(str).str.replace('$', '', regex=False).str.replace('€', '', regex=False).str.strip()
|
|
226
|
+
# Si tiene formato 1.000,00 cambiar por 1000.00
|
|
227
|
+
# o formato 1,000.00
|
|
228
|
+
sample_non_null = cleaned_series.dropna()
|
|
229
|
+
if not sample_non_null.empty:
|
|
230
|
+
# Intentar conversión directa
|
|
231
|
+
converted = pd.to_numeric(cleaned_series.str.replace(',', ''), errors='coerce')
|
|
232
|
+
if converted.notna().sum() > len(sample_non_null) * 0.7:
|
|
233
|
+
df_out[col] = converted
|
|
234
|
+
except Exception:
|
|
235
|
+
pass
|
|
236
|
+
return df_out
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def clean_raw_table(
|
|
240
|
+
raw_data: Union[List[List[Any]], pd.DataFrame],
|
|
241
|
+
header_row: Optional[Union[int, List[int], str]] = 1,
|
|
242
|
+
skip_footer: int = 0,
|
|
243
|
+
**kwargs
|
|
244
|
+
) -> pd.DataFrame:
|
|
245
|
+
"""Función de acceso directo para TableCleaner.clean()"""
|
|
246
|
+
return TableCleaner.clean(raw_data, header_row=header_row, skip_footer=skip_footer, **kwargs)
|
utils/validator.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Módulo para validación y auditoría de calidad de datos en DataFrames.
|
|
3
|
+
Permite verificar esquemas requeridos, detectar valores nulos, registros duplicados
|
|
4
|
+
y generar reportes de integridad antes de procesar o exportar.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import List, Dict, Any, Optional, Union
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DataValidator:
|
|
13
|
+
"""
|
|
14
|
+
Validador de calidad e integridad de datos para DataFrames.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def validar(
|
|
19
|
+
df: pd.DataFrame,
|
|
20
|
+
columnas_requeridas: Optional[List[str]] = None,
|
|
21
|
+
no_nulos: Optional[List[str]] = None,
|
|
22
|
+
tipos_esperados: Optional[Dict[str, str]] = None,
|
|
23
|
+
min_filas: int = 1
|
|
24
|
+
) -> Dict[str, Any]:
|
|
25
|
+
"""
|
|
26
|
+
Valida que un DataFrame cumpla con una serie de reglas de negocio.
|
|
27
|
+
|
|
28
|
+
Parámetros:
|
|
29
|
+
-----------
|
|
30
|
+
df : pd.DataFrame
|
|
31
|
+
El DataFrame a validar.
|
|
32
|
+
columnas_requeridas : list of str, opcional
|
|
33
|
+
Lista de nombres de columnas que DEBEN existir.
|
|
34
|
+
no_nulos : list of str, opcional
|
|
35
|
+
Lista de columnas que no pueden tener valores vacíos/nulos.
|
|
36
|
+
tipos_esperados : dict, opcional
|
|
37
|
+
Diccionario {'columna': 'numeric'|'string'|'datetime'} para verificar tipos.
|
|
38
|
+
min_filas : int (por defecto 1)
|
|
39
|
+
Cantidad mínima de filas esperadas.
|
|
40
|
+
|
|
41
|
+
Retorna:
|
|
42
|
+
--------
|
|
43
|
+
dict con:
|
|
44
|
+
- 'es_valido': bool (True si pasó todas las pruebas)
|
|
45
|
+
- 'errores': list[str] (detalles de cada fallo)
|
|
46
|
+
- 'alertas': list[str] (advertencias menores)
|
|
47
|
+
"""
|
|
48
|
+
errores = []
|
|
49
|
+
alertas = []
|
|
50
|
+
|
|
51
|
+
if df is None:
|
|
52
|
+
return {"es_valido": False, "errores": ["El DataFrame es None."], "alertas": []}
|
|
53
|
+
|
|
54
|
+
# 1. Cantidad de filas
|
|
55
|
+
if len(df) < min_filas:
|
|
56
|
+
errores.append(f"El DataFrame tiene {len(df)} filas (se esperaban al menos {min_filas}).")
|
|
57
|
+
|
|
58
|
+
# 2. Columnas requeridas
|
|
59
|
+
if columnas_requeridas:
|
|
60
|
+
columnas_actuales = set(df.columns)
|
|
61
|
+
faltantes = [col for col in columnas_requeridas if col not in columnas_actuales]
|
|
62
|
+
if faltantes:
|
|
63
|
+
errores.append(f"Faltan columnas requeridas: {faltantes}")
|
|
64
|
+
|
|
65
|
+
# 3. Columnas sin nulos
|
|
66
|
+
if no_nulos:
|
|
67
|
+
for col in no_nulos:
|
|
68
|
+
if col in df.columns:
|
|
69
|
+
n_nulos = df[col].isna().sum()
|
|
70
|
+
if n_nulos > 0:
|
|
71
|
+
errores.append(f"La columna '{col}' tiene {n_nulos} valores nulos/vacíos.")
|
|
72
|
+
|
|
73
|
+
# 4. Tipos de datos esperados
|
|
74
|
+
if tipos_esperados:
|
|
75
|
+
for col, tipo in tipos_esperados.items():
|
|
76
|
+
if col in df.columns:
|
|
77
|
+
if tipo == 'numeric' and not pd.api.types.is_numeric_dtype(df[col]):
|
|
78
|
+
errores.append(f"La columna '{col}' no es de tipo numérico (tipo actual: {df[col].dtype}).")
|
|
79
|
+
elif tipo == 'datetime' and not pd.api.types.is_datetime64_any_dtype(df[col]):
|
|
80
|
+
errores.append(f"La columna '{col}' no es de tipo fecha/datetime.")
|
|
81
|
+
|
|
82
|
+
es_valido = len(errores) == 0
|
|
83
|
+
return {
|
|
84
|
+
"es_valido": es_valido,
|
|
85
|
+
"errores": errores,
|
|
86
|
+
"alertas": alertas,
|
|
87
|
+
"total_filas": len(df),
|
|
88
|
+
"total_columnas": len(df.columns)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def reporte_calidad(df: pd.DataFrame) -> pd.DataFrame:
|
|
93
|
+
"""
|
|
94
|
+
Genera una tabla de diagnóstico de calidad de datos con conteo de nulos,
|
|
95
|
+
porcentaje de completitud, valores únicos y tipos.
|
|
96
|
+
"""
|
|
97
|
+
if df.empty:
|
|
98
|
+
return pd.DataFrame(columns=["Columna", "Tipo", "No_Nulos", "Nulos", "% Nulos", "Valores_Unicos", "Muestra"])
|
|
99
|
+
|
|
100
|
+
total_filas = len(df)
|
|
101
|
+
reporte = []
|
|
102
|
+
|
|
103
|
+
for col in df.columns:
|
|
104
|
+
serie = df[col]
|
|
105
|
+
n_nulos = serie.isna().sum()
|
|
106
|
+
pct_nulos = round((n_nulos / total_filas) * 100, 1)
|
|
107
|
+
n_unicos = serie.nunique(dropna=True)
|
|
108
|
+
tipo = str(serie.dtype)
|
|
109
|
+
|
|
110
|
+
# Muestra del primer valor no nulo
|
|
111
|
+
muestra_val = serie.dropna().iloc[0] if not serie.dropna().empty else None
|
|
112
|
+
|
|
113
|
+
reporte.append({
|
|
114
|
+
"Columna": col,
|
|
115
|
+
"Tipo": tipo,
|
|
116
|
+
"No_Nulos": total_filas - n_nulos,
|
|
117
|
+
"Nulos": n_nulos,
|
|
118
|
+
"% Nulos": f"{pct_nulos}%",
|
|
119
|
+
"Valores_Unicos": n_unicos,
|
|
120
|
+
"Muestra": str(muestra_val)[:30] if muestra_val is not None else ""
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
return pd.DataFrame(reporte)
|
|
124
|
+
|
|
125
|
+
@staticmethod
|
|
126
|
+
def detectar_duplicados(df: pd.DataFrame, columnas_clave: Optional[List[str]] = None) -> pd.DataFrame:
|
|
127
|
+
"""
|
|
128
|
+
Extrae las filas duplicadas del DataFrame para auditoría.
|
|
129
|
+
"""
|
|
130
|
+
if columnas_clave:
|
|
131
|
+
duplicados = df[df.duplicated(subset=columnas_clave, keep=False)]
|
|
132
|
+
else:
|
|
133
|
+
duplicados = df[df.duplicated(keep=False)]
|
|
134
|
+
return duplicados.sort_values(by=columnas_clave) if columnas_clave else duplicados
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def validar_dataframe(
|
|
138
|
+
df: pd.DataFrame,
|
|
139
|
+
columnas_requeridas: Optional[List[str]] = None,
|
|
140
|
+
no_nulos: Optional[List[str]] = None,
|
|
141
|
+
tipos_esperados: Optional[Dict[str, str]] = None,
|
|
142
|
+
min_filas: int = 1
|
|
143
|
+
) -> Dict[str, Any]:
|
|
144
|
+
"""Acceso directo a DataValidator.validar()"""
|
|
145
|
+
return DataValidator.validar(df, columnas_requeridas, no_nulos, tipos_esperados, min_filas)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def reporte_calidad(df: pd.DataFrame) -> pd.DataFrame:
|
|
149
|
+
"""Acceso directo a DataValidator.reporte_calidad()"""
|
|
150
|
+
return DataValidator.reporte_calidad(df)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def detectar_duplicados(df: pd.DataFrame, columnas_clave: Optional[List[str]] = None) -> pd.DataFrame:
|
|
154
|
+
"""Acceso directo a DataValidator.detectar_duplicados()"""
|
|
155
|
+
return DataValidator.detectar_duplicados(df, columnas_clave)
|