Una de las grandes ventajas de los sistemas informáticos es la facilidad con la que se puede copiar y modificar los archivos. Cuando tenemos que repetir un análisis que ya hemos realizado previamente, sea este en una hoja de cálculo, un Jupyter Notebook o con cualquier otra herramienta, podemos partir de este y modificar adecuadamente los datos. Esto que nos no reinventar los procesos cada vez, facilita actividades no deseadas como puede ser la copia. Por eso vamos a ver como crear un método para medir la similitud de archivos con Python. Para lo que nos basaremos en la clase SequenceMatcher que hemos visto recientemente.
Medir la similitud de dos secuencias con SequenceMatcher
La clase SequenceMatcher que hemos visto reciéntemente permite crear objetos que identifican las subsecuencias comunes dentro de secuencias más grandes. Ofreciendo una métrica de la similitud entre ellas. Los archivos no son más que secuencias. Por ejemplo, los archivos Jupyter Notebook no son más que un documento con formato JSON. Lo que se puede comprobar al abrir un archivo de estos en cualquier editor texto.
Así, si importamos dos archivos podemos tener una idea de los similares que son simplemente con la línea
SequenceMatcher(None, file_A, file_A).ratio()
Lo que nos devolverá un valor entre 0 y 1. Indiciado 1 que ambos son el mismo archivo. Un valor que tiene en cuenta posible rotaciones del contenido de la secuencia.
Método para detectar documentos similares
Así podemos crear un método que nos permita identificar archivos que son similares. Método al que le indicaremos una ruta de búsqueda en la que localizara todos los archivos y los con la clase SequenceMatcher. Una vez terminada la comparación puede ordenar los archivos en base a los similares que son.
Una posible implementación es:
from glob import glob from difflib import SequenceMatcher from tqdm import tqdm def get_similarity_in_folder(path='./', ext='*', progress=False): # Obtención de los archivos files = glob(path + ext) num_files = len(files) results = dict() # Barra de progreso if progress: pbar = tqdm(total= num_files * (num_files - 1) / 2) for i in range(num_files-1): # Primer fichero file_i = open(files[i]).read() name_i = files[i].split('/')[2] for j in range(i+1, num_files): # Segudo fichero file_j = open(files[j]).read() name_j = files[j].split('/')[2] # Obtención de ratio results[(name_i, name_j)] = SequenceMatcher(None, file_i, file_j).ratio() # Actualiza barra de progreso if progress: pbar.update(1) results = sorted(results.items(), key=lambda x: x[1], reverse=True) # Finaliza barra de progreso if progress: pbar.close() return results
En donde se ha utilizado glob
para obtener el listado de archivos. Una vez obtenidos estos se puede compara cada par de archivos con dos bucles. Dado que el valor de la ratio es simétrico, es decir, la ratio de A y B es el mismo que el de B y A el segundo bucle no tiene porqué recorrer todos los archivos, solo los que no se han analizado previamente.
Los resultados se guardan en un diccionario, donde la clave es una tupla con los nombres de los dos archivos. Finalmente se ordenan con el método sorted()
en orden inverso, de más a menos similares.
Como la tarea puede ser lenta se ha incluido la opción de una barra de progreso, utilizando el módulo tqdm visto anteriormente.
Comparativa
Para ver si este método se puede usar para detectar cuadernos de Jupyter con ligeras modificaciones hemos creado un cuaderno con 3 celdas. Hemos copiado el archivo y ejecutado las celdas. Posteriormente hemos rotado el orden de las celdas, el cual también hemos copiado y ejecutado el archivo. Por lo que tenemos cuatro documentos que son básicamente el mismo con ligeras modificaciones. El método nos ha dado los siguientes resultados:
Archivo 1 | Archivo 2 | Ratio |
base | base ejecutado | 0,84 |
rotado ejecutado | rotado | 0,84 |
rotado ejecutado | base ejecutado | 0,76 |
base | rotado | 0,75 |
base ejecutado | rotado | 0,64 |
base | rotado ejecutado | 0,63 |
Podemos ver que ejecutar el Notebook baja el valor de la ratio a 0,84 solo con cuatro celdas. Rotarlo lo baja aún más a 0,76. Rotarlo y ejecutarlo lo baja a 0,63. Hay que tener en cuenta que es un documento pequeño, por lo que en documentos más grandes posiblemente los valores sean mayores.
Conclusiones
Hemos visto un método para medir la similitud de archivos con Python que se puede utilizar para detectar si los cuadernos Jupyter son similares entre sí de una forma automática. Posiblemente el documento proceso pueda ser optimizado eliminado adecuadamente subcadenas que se pueden considerar “basura”.
Fernanda dice
Como es que se debe escribir el path para utilizar esta funcion?
Daniel Rodríguez dice
Se puede usar tanto la ruta relativa, por ejemplo, para una carpeta test que se encuentra junto al archivo Python con el código
get_similarity_in_folder('./test/')
o la ruta absoluta a la carpeta
get_similarity_in_folder('/Users/analyticslane/test/')
Daniel Rodríguez dice
El texto encontrado es el que pasa el filtro aplicado, por lo que lo tendrás en la columna correspondiente del DataFrame.