Módulo: data_operations.py#

Este es el módulo que se encarga de hacer el procesamiento de los datos, para ello utiliza diferentes métodos como lo son: eliminación de datos nulos, eliminar datos duplicados, normalizar datos y rellenar datos nulos con la media.

class src.data_operations.DataOperations#

Bases: object

Clase para realizar operaciones de manipulación y procesamiento de datos con pandas. Esta clase proporciona funcionalidades para cargar, procesar y exportar datos, manteniendo un registro de las transformaciones aplicadas y los datos originales.

Las operaciones a realizar en dichas transformaciones son: - Eliminar valores nulos: Elimina las filas con valores nulos del DataFrame. - Eliminar duplicados: Elimina las filas duplicadas del DataFrame. - Normalizar datos: Normaliza los datos numéricos del DataFrame. - Rellenar valores nulos con la media: Rellena los valores nulos con la media de cada columna.

Como se mencionó, la clase también proporciona una función para obtener un resumen de las transformaciones realizadas, que devuelve un string con el formato:

Resumen de transformaciones:

  1. Eliminar valores nulos Fecha: 2024-01-01 12:00:00 Detalles: None Filas afectadas: 100

  2. Normalizar datos Fecha: 2024-01-01 12:00:00 Detalles: None Filas afectadas: 100

  3. Rellenar valores nulos con la media Fecha: 2024-01-01 12:00:00 Detalles: None Filas afectadas: 100

Attributos:

data (pd.DataFrame): DataFrame actual con los datos procesados. original_data (pd.DataFrame): Copia de los datos originales sin procesar. transformation_history (list): Lista de diccionarios con el historial de transformaciones.

Cada entrada contiene: - operation: nombre de la operación - timestamp: momento de la ejecución - details: detalles específicos - rows_affected: número de filas afectadas

__init__()#

Inicializa la clase DataOperations con un DataFrame vacío y una lista para registrar transformaciones. Aquí no realiza ninguna operación en los datos iniciales, sino que almacena los datos originales para poder realizar transformaciones posteriores.

Parámetros:

None

def __init__(self):

    self.data = None
    self.original_data = None
    self.transformation_history = []
load_file()#

Permite al usuario seleccionar y cargar un archivo de datos, desde un archivo CSV, TXT o Excel.

Parámetros:

file_path (str) – La ruta al archivo que se desea cargar.

Devuelve:

DataFrame que contiene los datos cargados.

Tipo del valor devuelto:

pd.DataFrame

Muestra:

ValueError – Si el archivo no tiene extensión .csv .xlsx o .txt.

def load_file(self):

    file = filedialog.askopenfilename(filetypes=[
        ("Archivos CSV", "*.csv"),
        ("Archivos TXT", "*.txt"),
        ("Archivos Excel", "*.xlsx *.xls")
    ])

    if file:
        try:
            if file.endswith('.csv'):
                self.data = pd.read_csv(file)
            elif file.endswith('.txt'):
                self.data = pd.read_csv(file, delimiter='\t')
            else:
                self.data = pd.read_excel(file)

            self.original_data = self.data.copy()
            self.transformation_history = []

            messagebox.showinfo("Éxito", "Archivo cargado correctamente")
            return True
        except Exception as e:
            messagebox.showerror("Error", f"No se pudo cargar el archivo. Detalles: {e}")
            return False
    return False
remove_null_values()#

Elimina las filas que contienen valores nulos del DataFrame.

La operación se realiza in-place y se registra en el historial de transformaciones. Muestra un mensaje con el número de filas eliminadas.

Requires:

Datos cargados previamente (self.data no None).

def remove_null_values(self):

    if self.data is not None:
        rows_before = len(self.data)
        self.data.dropna(inplace=True)
        rows_removed = rows_before - len(self.data)

        self._add_to_history('remove_null_values', 
                           f'Eliminadas {rows_removed} filas con valores nulos')

        messagebox.showinfo("Éxito", 
                          f"Se han eliminado {rows_removed} filas con valores nulos")
    else:
        messagebox.showwarning("Advertencia", "Primero debes cargar los datos")
remove_duplicates()#

Elimina las filas duplicadas del DataFrame, manteniendo la primera ocurrencia.

Este método: - Identifica y elimina filas completamente duplicadas - Mantiene la primera ocurrencia de cada fila duplicada - Registra la operación en el historial - Muestra un mensaje con el número de filas eliminadas

Devuelve:

None

Muestra:
  • No lanza excepciones directamente, pero muestra un messagebox de advertencia

  • si self.data es None

Notas:
  • La comparación de duplicados considera todas las columnas

  • La operación es irreversible

def remove_duplicates(self):

    if self.data is not None:
        rows_before = len(self.data)
        self.data.drop_duplicates(inplace=True)
        rows_removed = rows_before - len(self.data)

        self._add_to_history('remove_duplicates',
                           f'Eliminadas {rows_removed} filas duplicadas')

        messagebox.showinfo("Éxito", 
                          f"Se han eliminado {rows_removed} filas duplicadas")
    else:
        messagebox.showwarning("Advertencia", "Primero debes cargar los datos")
normalize_data(selected_columns, method='Min-Max Scaling')#

Normaliza las columnas seleccionadas en el conjunto de datos utilizando el método especificado.

Parámetros:
  • selected_columns (list) – Lista de nombres de columnas a normalizar.

  • method (str) – Método de normalización a utilizar. Opciones: - «Min-Max Scaling»: Escala los valores al rango [0, 1]. - «Z-Score Scaling»: Centraliza los datos eliminando la media y escalando a varianza unitaria. - «Max Abs Scaling»: Escala los valores al rango [-1, 1] dividiendo por el valor absoluto máximo.

Muestra:

ValueError – Si no se seleccionan columnas o si no hay datos cargados.

Devuelve:

Número de filas afectadas por la normalización.

Tipo del valor devuelto:

int

Efectos secundarios:
  • Actualiza el atributo data con los valores normalizados.

  • Registra la transformación en el historial de operaciones.

def normalize_data(self, selected_columns, method="Min-Max Scaling"):

    if not selected_columns:
        raise ValueError("Debe seleccionar al menos una columna para normalizar.")

    if self.data is None:
        raise ValueError("No hay datos cargados para normalizar.")

    original_data = self.data[selected_columns].copy()

    for col in selected_columns:
        if self.data[col].notnull().any():  # Solo aplica si la columna no está completamente vacía
            if method == "Min-Max Scaling":
                min_val = self.data[col].min()
                max_val = self.data[col].max()
                if max_val != min_val:
                    self.data[col] = (self.data[col] - min_val) / (max_val - min_val)
                else:
                    self.data[col] = 0

            elif method == "Z-Score Scaling":
                mean_val = self.data[col].mean()
                std_val = self.data[col].std()
                if std_val != 0:
                    self.data[col] = (self.data[col] - mean_val) / std_val
                else:
                    self.data[col] = 0

            elif method == "Max Abs Scaling":
                max_abs_val = self.data[col].abs().max()
                if max_abs_val != 0:
                    self.data[col] = self.data[col] / max_abs_val
                else:
                    self.data[col] = 0

    affected_rows = (self.data[selected_columns] != original_data).any(axis=1).sum()
    self._add_to_history('normalize_data', f'Normalizadas las columnas {", ".join(selected_columns)} usando {method}')

    return affected_rows
fill_null_values(method='mean', degree=None, columns=None, n_neighbors=5)#

Llena valores nulos en columnas seleccionadas utilizando diferentes métodos de imputación.

Este método permite rellenar valores faltantes en un DataFrame utilizando diversas técnicas, como imputación por media, interpolación lineal, interpolación polinómica, interpolación basada en tiempo o imputación mediante K-Nearest Neighbors (KNN).

Parámetros:
  • method (str, opcional) – El método de imputación a utilizar. Por defecto es “mean”. Métodos compatibles: - “mean”: Reemplaza valores nulos con la media de la columna. - “linear”: Usa interpolación lineal para llenar valores faltantes. - “polynomial”: Usa interpolación polinómica para llenar valores faltantes. - “time”: Usa interpolación basada en tiempo (como indica el nombre). - “knn”: Usa imputación por K-Nearest Neighbors para columnas numéricas.

  • degree (int, opcional) – El grado de la interpolación polinómica. Requerido cuando el método es “polynomial”. Por defecto es None.

  • columns (list, opcional) – Lista de nombres de columnas a procesar. Si es None, se procesarán todas las columnas del DataFrame. Por defecto es None.

  • n_neighbors (int, opcional) – Número de vecinos a usar para la imputación KNN. Relevante solo cuando el método es “knn”. Por defecto es 5.

Muestra:
  • tkinter.messagebox.showwarning – Si no se cargan datos.

  • tkinter.messagebox.showerror – Si no se cumplen los requisitos para la imputación KNN.

Efectos secundarios:
  • Modifica el DataFrame subyacente in-place.

  • Muestra cuadros de mensaje con detalles de la imputación y éxito/error.

  • Registra el historial de imputación utilizando el método self._add_to_history.

Notas:
  • Para la imputación KNN, solo se consideran columnas numéricas.

  • El método KNN normaliza los datos antes de la imputación para manejar diferentes escalas.

  • El método proporciona interacción con el usuario para seleccionar la cantidad de vecinos en KNN.

Ejemplos:

# Llenar valores nulos con la media df.fill_null_values(method=”mean”)

# Llenar valores nulos con interpolación polinómica de grado 2 df.fill_null_values(method=”polynomial”, degree=2)

# Llenar valores nulos en columnas específicas usando KNN df.fill_null_values(method=”knn”, columns=[“column1”, “column2”])

def fill_null_values(self, method='mean', degree=None, columns=None, n_neighbors=5):


    if self.data is None:
        messagebox.showwarning("Advertencia", "Primero debes cargar los datos")
        return

    if columns is None:
        columns = self.data.columns  # Si no se pasan columnas, usar todas las columnas

    affected_rows = 0  # Contador de filas afectadas

    for column in columns:
        if column not in self.data.columns:
            continue  # Si la columna no existe en los datos, continuar con la siguiente

        if self.data[column].isnull().any():
            initial_null_count = self.data[column].isnull().sum()  # Contar los valores nulos antes

            if method == 'mean':
                self.data[column] = self.data[column].fillna(self.data[column].mean())
                nulls_filled = initial_null_count - self.data[column].isnull().sum()  # Calcular los nulos rellenados
                affected_rows += nulls_filled  # Sumar al total de filas afectadas
                detail = f"rellenados con la media en {column}"

            elif method == 'linear':
                # Realizar la interpolación lineal y asignar el resultado a la columna
                self.data[column] = self.data[column].interpolate(method='linear')

                nulls_filled = initial_null_count - self.data[column].isnull().sum()
                affected_rows += nulls_filled
                detail = f"rellenados con interpolación lineal en {column}"

                # Registrar el detalle y mostrar mensaje
                self._add_to_history('fill_null_values', detail)
                messagebox.showinfo("Éxito", f"{detail}. Se imputaron {nulls_filled} valores nulos.")

            elif method == 'polynomial' and degree is not None:
                # Realizar la interpolación polinomial y asignar el resultado a la columna
                self.data[column] = self.data[column].interpolate(method='polynomial', order=degree)

                nulls_filled = initial_null_count - self.data[column].isnull().sum()
                affected_rows += nulls_filled
                detail = f"rellenados con interpolación polinomial en {column} de grado {degree}"

                # Registrar el detalle y mostrar mensaje
                self._add_to_history('fill_null_values', detail)
                messagebox.showinfo("Éxito", f"{detail}. Se imputaron {nulls_filled} valores nulos.")

            elif method == 'knn':
                from sklearn.impute import KNNImputer
                import pandas as pd
                from tkinter.simpledialog import askstring

                # Pedir al usuario el número de vecinos cercanos
                neighbors_input = askstring("Número de Vecinos", "Ingrese el número de vecinos cercanos (default: 5):")

                # Validar entrada del usuario
                try:
                    n_neighbors = int(neighbors_input) if neighbors_input else 5
                    if n_neighbors <= 0:
                        raise ValueError("El número de vecinos debe ser mayor a 0.")
                except ValueError:
                    messagebox.showerror("Error", "Entrada inválida. Usando el valor predeterminado de 5 vecinos.")
                    n_neighbors = 5

                # Selección de columnas relevantes para KNN
                numeric_cols = [col for col in columns if pd.api.types.is_numeric_dtype(self.data[col])]
                if len(numeric_cols) < 2:
                    messagebox.showerror(
                        "Error", "KNN requiere al menos 2 columnas numéricas correlacionadas para funcionar."
                    )
                    return

                # Normalizar datos para evitar problemas de escala
                normalized_data = self.data[numeric_cols].copy()
                min_vals = normalized_data.min()
                max_vals = normalized_data.max()

                normalized_data = (normalized_data - min_vals) / (max_vals - min_vals)

                # Contar valores nulos antes
                nulls_before = self.data[numeric_cols].isnull().sum().sum()

                # Aplicar KNNImputer
                imputer = KNNImputer(n_neighbors=n_neighbors)
                imputed_normalized_data = imputer.fit_transform(normalized_data)

                # Desnormalizar los datos imputados
                imputed_data = pd.DataFrame(
                    imputed_normalized_data, columns=numeric_cols
                )
                imputed_data = imputed_data * (max_vals - min_vals) + min_vals

                # Actualizar el DataFrame con los valores imputados
                for col in numeric_cols:
                    self.data[col] = imputed_data[col]

                # Contar valores nulos después
                nulls_after = self.data[numeric_cols].isnull().sum().sum()
                nulls_filled = nulls_before - nulls_after
                affected_rows += nulls_filled  # Sumar al total de filas afectadas

                # Registrar el detalle y mostrar mensaje
                detail = f"KNN aplicado en columnas: {', '.join(numeric_cols)} con {n_neighbors} vecinos"
                self._add_to_history('fill_null_with_knn', detail)

                messagebox.showinfo("Éxito", f"{detail}. Se imputaron {nulls_filled} valores nulos.")

    # Mostrar el número total de filas afectadas
    messagebox.showinfo("Éxito", f"Se afectaron {affected_rows} valores nulos en total.")
export_results()#

Exporta los datos procesados y el historial de transformaciones.

Formatos soportados: - Excel (.xlsx): Crea múltiples hojas para datos transformados, originales e historial - CSV (.csv): Crea archivos separados para cada tipo de dato - TXT (.txt): Similar a CSV pero con delimitador de tabulación

Devuelve:

True si la exportación fue exitosa, False en otro caso.

Tipo del valor devuelto:

bool

Notas

Para CSV y TXT, se crean archivos adicionales con sufijos “_original” y “_transformaciones” para los datos originales y el historial.

def export_results(self):

    if self.data is None:
        messagebox.showwarning("Advertencia", "No hay datos para exportar")
        return False

    # Crear un DataFrame con el resumen de transformaciones
    transformation_summary = pd.DataFrame(self.transformation_history)

    file_path = filedialog.asksaveasfilename(
        filetypes=[
            ("Excel files", "*.xlsx"),
            ("CSV files", "*.csv"),
            ("Text files", "*.txt"),
            ("All files", "*.*")
        ]
    )

    if not file_path:
        return False

    try:
        # Exportar a Excel
        if file_path.endswith('.xlsx'):
            with pd.ExcelWriter(file_path, engine='openpyxl') as writer:
                self.data.to_excel(writer, sheet_name='Datos Transformados', index=False)
                if self.original_data is not None:
                    self.original_data.to_excel(writer, sheet_name='Datos Originales', index=False)
                if self.transformation_history:
                    transformation_summary.to_excel(writer, 
                                                 sheet_name='Historial de Transformaciones',
                                                 index=False)

        # Exportar a CSV
        elif file_path.endswith('.csv'):
            # Exportar datos transformados
            self.data.to_csv(file_path, index=False)

            # Exportar datos originales y transformaciones en archivos separados
            base_path = file_path[:-4]  # Remover la extensión .csv
            if self.original_data is not None:
                self.original_data.to_csv(f"{base_path}_original.csv", index=False)
            if self.transformation_history:
                transformation_summary.to_csv(f"{base_path}_transformaciones.csv", index=False)

        # Exportar a TXT
        elif file_path.endswith('.txt'):
            # Exportar datos transformados
            self.data.to_csv(file_path, sep='\t', index=False)

            # Exportar datos originales y transformaciones en archivos separados
            base_path = file_path[:-4]  # Remover la extensión .txt
            if self.original_data is not None:
                self.original_data.to_csv(f"{base_path}_original.txt", sep='\t', index=False)
            if self.transformation_history:
                transformation_summary.to_csv(f"{base_path}_transformaciones.txt", sep='\t', index=False)

        mensaje = "Los resultados se han exportado correctamente"
        if file_path.endswith(('.csv', '.txt')) and (self.original_data is not None or self.transformation_history):
            mensaje += "\nSe han creado archivos adicionales para los datos originales y el historial de transformaciones"

        messagebox.showinfo("Éxito", mensaje)
        return True

    except Exception as e:
        messagebox.showerror("Error", f"Error al exportar los resultados: {str(e)}")
        return False
get_transformation_summary()#

Genera un resumen textual de todas las transformaciones aplicadas.

Devuelve:

Resumen formateado del historial de transformaciones, incluyendo:
  • Número de operación

  • Tipo de operación

  • Fecha y hora

  • Detalles específicos

  • Número de filas afectadas

Si no hay transformaciones, retorna un mensaje indicándolo.

Tipo del valor devuelto:

str

def get_transformation_summary(self):

    if not self.transformation_history:
        return "No se han realizado transformaciones"

    summary = "Resumen de transformaciones:\n\n"
    for i, trans in enumerate(self.transformation_history, 1):
        summary += f"{i}. {trans['operation']}\n"
        summary += f"   Fecha: {trans['timestamp']}\n"
        summary += f"   Detalles: {trans['details']}\n"
        summary += f"   Filas afectadas: {trans['rows_affected']}\n\n"

    return summary