Eliminador de duplicados en bibliotecas de cebadores — Uniendo 3 conceptos para crear una herramienta
Al finalizar este tema
Podrás construir una herramienta práctica que encuentra casi instantáneamente las duplicidades en bibliotecas de cebadores de decenas de miles de entradas, uniendo los tres conceptos aprendidos por separado en el libro de texto (DryBench): tablas hash, diccionarios y expresiones regulares. No se trata simplemente de "eliminar duplicados", sino de comprender por qué el método ingenuo es lento y entender cómo cada concepto constituye una pieza clave de la herramienta, adquiriendo esta comprensión a través de la práctica.
Este texto es un ejemplo educativo general. No utiliza la biblioteca real de un laboratorio específico, sino que toma como tema el problema recurrente en biología molecular: "los problemas que surgen cuando aumenta el stock de oligonucleótidos".
"¿Eh, no es esto lo que pedí la última vez?" — El problema que enfrentamos
Al realizar PCR, clonación, qPCR y secuenciación en el laboratorio, terminamos pidiendo cebadores (oligonucleótidos cortos de ADN, normalmente de 18 a 25 nt) una y otra vez. Al principio, una sola hoja de cálculo era suficiente: nombre, secuencia, uso, fecha de pedido y temperatura de fusión ().
Sin embargo, al pasar uno o dos años, esta hoja se expande a miles o decenas de miles de filas. Cuando varias personas utilizan la misma hoja, ocurren cosas como las siguientes:
- El profesor Kim registra una secuencia bajo
hGAPDH_F, y más tarde el profesor Lee la registra exactamente igual pero con un nombre diferenteGAPDH_forward. - Alguien introduce
ATGCGT...en mayúsculas, mientras que otro lo introduce comoatgcgt...en minúsculas. - Al copiar y pegar, se adjuntan espacios o tabulaciones invisibles al principio o al final de la secuencia.
- Algunas secuencias se introducen invertidas como complemento inverso, pero en realidad ocupan la misma posición que un cebador ya existente.
- Debido a errores tipográficos, se mezclan caracteres que no son ACGT, como
ATGXGT.
¿Cuál es el resultado? Se terminan pidiendo los mismos oligonucleótidos dos o tres veces, se desperdicia dinero y en el laboratorio surge la confusión "¿es este cebador realmente ese cebador?", lo que provoca errores.
Por eso, decidimos "ordenar las duplicidades" y escribimos un código ingenuo: comparando cada secuencia con todas las demás una por una.
# Método ingenuo: compara todos los pares (versión deliberadamente lenta)def find_duplicates_naive(sequences): duplicates = [] for i in range(len(sequences)): for j in range(i + 1, len(sequences)): if sequences[i] == sequences[j]: duplicates.append((i, j)) return duplicatesSi hay 100 muestras, es instantáneo. Pero, ¿qué pasa si hay 10,000? El número de comparaciones es aproximadamente 10000 × 10000 / 2 = 50 millones de veces. En un portátil, toma desde unas pocas decenas de segundos hasta unos minutos. Si son 100,000, es 100 veces eso. No terminará ni siquiera después de tomar una taza de café.
No nos detengamos aquí y planteemos una pregunta. ¿Por qué es lento? ¿Y qué herramientas se necesitan para convertir esto en algo instantáneo? Este artículo desarrollará la respuesta a través de código.
Primero veamos el producto terminado (ejecutando la caja negra primero)
Si empezamos desde un editor vacío, nos perderemos. Por lo tanto, primero ejecutaremos la herramienta que crearemos finalmente y, después, abriremos la tapa para analizar su interior. (Este es el método de aprendizaje "top-down" que se utiliza en lugares como fast.ai: primero juegas una partida de un juego terminado y luego aprendes las reglas).
Así es como se utiliza el dedup_primers.py terminado.
report = dedup_primer_library("primers.csv")
print(report.summary())Entonces, se genera el siguiente informe.
=== Informe de duplicados de la biblioteca de primers ===
Elementos totales : 12.480
Elementos válidos : 12.451 (se excluyen 29 errores de formato)
Secuencias únicas : 9.832
Duplicados exactos : 2.619 (misma secuencia, nombre distinto)
Duplicados reverso-complem.: 174 (misma secuencia al invertir)
Tiempo de procesamiento : 0,31 segundos
─────────────────────────────────
Las tres secuencias más duplicadas
1) ATGGCACCACAGTCCATGCC ×7 (GAPDH_F, hGAPDH_forward, ...)
2) GTCGACCTGCAGGCATGCAA ×5
3) CCTGCAGGTCGACTCTAGAG ×4La clave reside en dos aspectos: primero, 0.31 segundos, que permite completar en un instante una tarea que la versión anterior tardaba varios minutos; segundo, no se limita a indicar si hay "duplicados" o no, sino que también informa cuántas veces y con qué nombres se ha duplicado cada secuencia. Este artículo se centra en cómo lograr estos dos puntos.
¿De qué componentes se compone esta herramienta (despiece)?
El "motor de eliminación de duplicados" que vamos a ensamblar es, en realidad, una máquina en la que tres conceptos fundamentales están interconectados. Vamos a indicar el origen de cada componente, como si fuera un modelo de plástico.
Motor de deduplicación de primers
┌─────────────────────────────────────────────┐
│ │
│ [Entrada] Leer CSV ───── pieza: entrada/salida │ ← Se proporciona completa
│ │ │
│ ▼ │
│ [Limpieza] Normalizar y validar ─ regex │ ← La construyes tú ★
│ │ │
│ ▼ │
│ [Decisión] Detectar duplicados ─ conjunto hash │ ← La construyes tú ★
│ │ │
│ ▼ │
│ [Agregación] Agrupar nombres ─ diccionario │ ← La construyes tú ★
│ │ │
│ ▼ │
│ [Salida] Informe │
└─────────────────────────────────────────────┘| Component | Where you learned it (DryBench) | What this tool does |
|---|---|---|
| File I/O | string-and-file-io | Read names and sequences from CSV |
| Regular expressions | regex-pattern-matching | Validate and refine sequences to ensure they contain only ACGT |
| Hash set | hash-table | Determine if a sequence has been seen before in O(1) time |
| Dictionary | list-and-dictionary | Group names associated with each sequence |
📌 If you're new to these concepts (entry links at the top) The following three are core components of this tutorial. If they're unfamiliar, spend 30 seconds reviewing them in the side drawer (mini cards on the right) before proceeding. We'll revisit them here when needed.
Now let's build them one by one, from top to bottom. We'll provide the file reading component (tool) already completed, so you only need to focus on the three components: cleaning, validation, and aggregation. Just three, right? We deliberately split it this way because humans can typically grasp only three new concepts at a time.
Step 0 — See what the data looks like
Before writing code, let's look at the ingredients. If you export the primer sheet as CSV, it will roughly look like this:
name,sequence,purpose,date
GAPDH_F,ATGGCACCACAGTCCATGCC,qPCR,2025-01-12
GAPDH_R,GGCATGGACTGTGGTCATGAG,qPCR,2025-01-12
hGAPDH_forward, atggcaccacagtccatgcc ,qPCR,2025-06-03
cloning_F,GTCGACCTGCAGGCATGCAA,cloning,2025-02-20
bad_entry,ATGXGTNN,cloning,2025-03-01A simple vista se aprecian los problemas.
GAPDH_FyhGAPDH_forwardtienen nombres diferentes, pero secuencias idénticas. Además, la segunda incluye letras minúsculas y espacios en blanco al principio y al final.- La secuencia de
bad_entry,ATGXGTNN, contieneX, por lo que no es una secuencia de ADN válida (es un error tipográfico o una abreviatura).
Por lo tanto, para contar correctamente los "duplicados", debe normalizar las secuencias (hacerlas uniformes) antes de compararlas y filtrar los elementos que no son secuencias (validación). De lo contrario, tratará erróneamente ATGGCACCACAGTCCATGCC y atggcaccacagtccatgcc como secuencias diferentes, lo que hará que omita los duplicados.
Esta es la idea clave. La mitad de la eliminación de duplicados no es "comparar bien", sino "normalizar antes de comparar".
Paso 1: leer el archivo (esta parte se proporciona completa)
Leer archivos no es el objetivo de aprendizaje de este tutorial. Dado que su espacio de trabajo mental es valioso, le proporcionaré este código "genérico" como un producto terminado y pasaré a la siguiente etapa. (Sigue exactamente lo que aprendió en la tarjeta string-and-file-io. Si le resulta desconocido, consulte el glosario).
import csv
def load_primers(filepath): """Lee del CSV una lista de tuplas (nombre, secuencia original).""" records = [] with open(filepath, "r", encoding="utf-8") as f: reader = csv.DictReader(f) for row in reader: name = row["name"].strip() seq = row["sequence"] # Se deja sin limpiar para procesarla en el siguiente paso records.append((name, seq)) return records
# En el ejercicio se simula con una lista para poder ejecutarlo directamente en el navegador.raw_records = [ ("GAPDH_F", "ATGGCACCACAGTCCATGCC"), ("GAPDH_R", "GGCATGGACTGTGGTCATGAG"), ("hGAPDH_forward", " atggcaccacagtccatgcc "), ("cloning_F", "GTCGACCTGCAGGCATGCAA"), ("bad_entry", "ATGXGTNN"),]
assert len(raw_records) == 5assert raw_records[2][1] == " atggcaccacagtccatgcc " # Conserva espacios y minúsculasraw_records todavía está en un estado inicial y sin procesar. Contiene minúsculas, espacios en blanco y secuencias incorrectas. Esto se corregirá en la siguiente etapa.
Paso 2: creación — Normalización y validación de secuencias ★ (comience aquí)
✍️ Esta es la sección que deben completar. Componente = expresión regular. Objetivo: transformar la secuencia a un "formato estándar" y filtrar cualquier elemento que no sea ADN.
Primero, definamos el "formato estándar". Sigamos estas reglas:
- Eliminar los espacios en blanco al principio y al final.
- Convertir todo a mayúsculas.
- Verificar que la secuencia esté compuesta únicamente por
A,C,GyT. De lo contrario, descartarla.
Aquí es donde las expresiones regulares demuestran su utilidad. La pregunta "¿la cadena completa es una repetición de ACGT?" se puede resolver con un único patrón de expresión regular: ^[ACGT]+$.
🔎 ¿Por qué usar una expresión regular? (Diagrama: coincidencia de patrones de expresiones regulares)
^representa el inicio de la cadena,$el final,[ACGT]significa "uno de estos cuatro caracteres" y+significa "una o más repeticiones". Al combinarlos, se obtiene el significado "solo hay ACGT desde el principio hasta el final". Aunque también se podría verificar cada carácter con un bucleif, la expresión regular expresa esta intención en una sola línea.
import re
# Patrón que comprueba que toda la secuencia contiene solo A/C/G/T; se compila para reutilizarloDNA_PATTERN = re.compile(r"^[ACGT]+$")
def normalize(seq): """Elimina espacios y convierte a mayúsculas; todavía sin validar.""" return seq.strip().upper()
def is_valid_dna(seq): """Comprueba que la secuencia normalizada sea ADN válido.""" return DNA_PATTERN.match(seq) is not NoneAhora, vamos a verificarlo.
assert normalize(" atggcaccacagtccatgcc ") == "ATGGCACCACAGTCCATGCC"assert is_valid_dna("ATGGCACCACAGTCCATGCC") is Trueassert is_valid_dna("ATGXGTNN") is False # Contiene X y N → se descartaassert is_valid_dna("") is False # La cadena vacía también se descartaObserve carefully the last assert. The empty string is filtered because the pattern uses + (one or more). If it had used * (zero or more), the empty string would have passed as "valid DNA". A single regular expression symbol can make or break data quality.
🤔 Self-explanation prompt Why is it correct to perform
normalizefirst andis_valid_dnalater? What would happen if validation were done first, followed by normalization? Explain this to yourself usingatgc(lowercase) as an example. (Hint: Would the lowercaseatgcpass the^[ACGT]+$check?)
Now, complete the function that transforms raw records into a list of "clean sequence + name". Invalid items will be collected separately and reported in the summary as "X items excluded".
def clean_records(raw_records): cleaned = [] # (nombre, secuencia normalizada) rejected = [] # (nombre, original): error de formato for name, seq in raw_records: norm = normalize(seq) if is_valid_dna(norm): cleaned.append((name, norm)) else: rejected.append((name, seq)) return cleaned, rejected
cleaned, rejected = clean_records(raw_records)
assert len(cleaned) == 4 # Solo se excluye bad_entryassert len(rejected) == 1assert rejected[0][0] == "bad_entry"# Clave: aunque los nombres difieran, las secuencias ahora son idénticasassert cleaned[0][1] == cleaned[2][1] == "ATGGCACCACAGTCCATGCC"La última línea marca el éxito de esta etapa. GAPDH_F y hGAPDH_forward ahora tienen exactamente la misma secuencia. Antes de la normalización, el ordenador las reconocía como cadenas diferentes. Ahora, podemos contar los "duplicados".
Paso 3: Creación — Identificación instantánea de duplicados ★ (conjunto hash)
✍️ Sección para completar directamente. Componente = conjunto hash. Objetivo: responder en O(1) a la pregunta: "¿Ya he visto esta secuencia?".
Recordemos por qué la versión inicial era tan lenta. Para verificar una secuencia, la comparaba una por una con todas las secuencias anteriores. Si procesamos n elementos, el número de comparaciones crece proporcionalmente a n².
🔎 O(n²) vs. O(n) (Diagrama — notación Big O) El operador
inaplicado a una lista la recorre desde el principio: O(n) por elemento, lo que da un total de O(n²). En cambio, el operadorinen un conjunto calcula directamente la posición mediante una función hash y responde en O(1): O(n) en total. Si los datos se multiplican por 10, el método de lista se vuelve 100 veces más lento, mientras que el método del conjunto solo se vuelve 10 veces más lento.
La clave del conjunto hash es que, al aplicar una función hash a una secuencia, se obtiene un "número de celda (dirección)", y basta con consultar directamente esa celda. Por lo tanto, el tiempo de verificación es prácticamente constante, independientemente de la cantidad de datos. Es como encontrar un libro directamente por su número de clasificación en una biblioteca. En Python, set es precisamente esta tabla hash.
El principio que aprendimos en la tarjeta hash-table se aplica aquí en solo tres líneas.
def find_exact_duplicates(cleaned): """Encuentra nombres con secuencias exactamente duplicadas en O(n).""" seen = set() # Secuencias vistas hasta ahora (conjunto hash) unique = [] # Primera aparición de (nombre, secuencia) duplicates = [] # (nombre, secuencia) duplicados for name, seq in cleaned: if seq in seen: # ← Decisión O(1): el núcleo del algoritmo duplicates.append((name, seq)) else: seen.add(seq) unique.append((name, seq)) return unique, duplicates
unique, dups = find_exact_duplicates(cleaned)
assert len(unique) == 3 # GAPDH_F, GAPDH_R, cloning_Fassert len(dups) == 1 # hGAPDH_forward tiene la misma secuencia que GAPDH_Fassert dups[0][0] == "hGAPDH_forward"assert dups[0][1] == "ATGGCACCACAGTCCATGCC"if seq in seen: — Este es el protagonista que redujo el tiempo de procesamiento de varios minutos a 0,3 segundos. Si se hubiera utilizado una lista, la prueba se habría vuelto progresivamente más lenta a medida que seen aumentara, pero al utilizar un conjunto, la velocidad de la prueba se mantiene constante incluso con 100.000 elementos.
🤔 Indicación para la autoexplicación Si se reemplaza
seenpor[](lista) en lugar deset(), el código seguirá "funcionando" y los resultados serán idénticos. Sin embargo, ¿por qué no se debería hacer esto? Explica qué sucede con 10.000 o 100.000 elementos, relacionándolo con la complejidad O(n²).
Paso 4: Construcción — Agrupar las repeticiones por cantidad y nombre ★ (diccionario)
✍️ Sección para completar manualmente. Componente = diccionario. Objetivo: agrupar "los nombres utilizados para cada secuencia".
Después de los pasos 1 a 3, se puede determinar si existen duplicados. Sin embargo, en la práctica, lo que realmente interesa es "¿cuántos nombres y cuántas veces se ha registrado esta secuencia?". GAPDH_F, hGAPDH_forward, GAPDH_qF... Es necesario agrupar estos elementos dispersos bajo una única secuencia para obtener una estructura organizada.
Esta es la función principal del diccionario. Clave = secuencia, Valor = lista de nombres utilizados para esa secuencia.
🔎 ¿Por qué un diccionario? (Diagrama — lista y diccionario) Internamente, un diccionario también es una tabla hash (es un tipo de estructura de datos relacionada con el conjunto del paso 3). Mientras que un conjunto solo recuerda si un elemento "existe o no", un diccionario también recuerda "el valor asociado a la clave". Es ideal para relaciones de uno a muchos, como "secuencia → lista de nombres".
from collections import defaultdict
def group_by_sequence(cleaned): """Agrupa por secuencia como clave y guarda como valor la lista de nombres que la usan.""" groups = defaultdict(list) for name, seq in cleaned: groups[seq].append(name) # Si no existe, crea automáticamente una lista vacía return groups
groups = group_by_sequence(cleaned)
# La secuencia GAPDH debe tener dos nombres asociadosassert groups["ATGGCACCACAGTCCATGCC"] == ["GAPDH_F", "hGAPDH_forward"]assert len(groups["GGCATGGACTGTGGTCATGAG"]) == 1 # GAPDH_R es únicaassert len(groups) == 3 # Tres secuencias únicasGracias a defaultdict(list), logré implementar la lógica de "agregar si la clave existe, crear una nueva si no existe" en una sola línea con groups[seq].append(name), evitando la necesidad de una bifurcación con if. Ahora, al extraer solo los valores que tienen dos o más elementos de este groups, se obtiene la "lista de secuencias duplicadas".
def duplicated_groups(groups): """Selecciona secuencias con dos o más nombres y las ordena por número de duplicados.""" dups = {seq: names for seq, names in groups.items() if len(names) > 1} # Ordena de mayor a menor número de duplicados return sorted(dups.items(), key=lambda kv: len(kv[1]), reverse=True)
ranked = duplicated_groups(groups)
assert len(ranked) == 1assert ranked[0][0] == "ATGGCACCACAGTCCATGCC"assert ranked[0][1] == ["GAPDH_F", "hGAPDH_forward"]El “TOP 3 de secuencias más repetidas” del informe proviene exactamente de ranked. Aquí, tres componentes (expresión regular, conjunto hash y diccionario) se integran en un solo proceso.
Paso 5 de creación — Un paso más: duplicados por complementariedad inversa (avanzado)
Hasta ahora, solo hemos detectado duplicados con caracteres idénticos. Sin embargo, existe otra particularidad en la biología: la complementariedad inversa.
El ADN es de doble cadena; la cadena opuesta a 5'-ATGC-3' es 3'-TACG-5', que al leerse en sentido inverso se convierte en 5'-GCAT-3'. Si alguien anota un cebador basándose en la cadena opuesta, aunque los caracteres parezcan completamente diferentes, podría ser el mismo oligonucleótido uniéndose en el mismo sitio.
Para detectar esto, podemos tomar como valor representativo (forma canónica) “la secuencia o su complementariedad inversa que aparezca primero en orden alfabético”. Así, tanto la secuencia original como su complementaria se agrupan bajo el mismo valor representativo. Esta es una extensión de la “normalización” que realizamos en el paso 2. La utilidad de la normalización se manifiesta nuevamente aquí.
COMPLEMENT = str.maketrans("ACGT", "TGCA")
def reverse_complement(seq): return seq.translate(COMPLEMENT)[::-1]
def canonical(seq): """Usa como representante el menor lexicográficamente entre la secuencia y su reverso complementario.""" rc = reverse_complement(seq) return min(seq, rc)
# Verificación: dos secuencias reverso-complementarias deben tener el mismo representanteassert reverse_complement("ATGC") == "GCAT"assert canonical("ATGC") == canonical("GCAT") # Ambas convergen en la menor lexicográficamenteassert canonical("AAAA") == "AAAA" # Es menor que su reverso complementario TTTTAhora, en los pasos 3 y 4, solo hay que colocar canonical(seq) en la posición de seq, lo que permite detectar y corregir las duplicaciones por retrocomplementariedad en un solo paso. No se ha creado una pieza nueva; simplemente se ha añadido una "lente" llamada "valor representativo" a las piezas de evaluación y agregación existentes. Así es como se amplían las funcionalidades de una buena herramienta.
🤔 Indicación para la autoexplicación Si se utiliza
canonical, ¿cómo se puede diferenciar el número exacto de duplicaciones del número de duplicaciones por retrocomplementariedad? (La clave está en determinar si se trata deseq == canonical(seq)o no).
Unificar las piezas: el motor completo
Al ensamblar los cinco pasos, se obtiene la herramienta que vimos al principio.
def dedup_primer_library(raw_records, use_canonical=False): cleaned, rejected = clean_records(raw_records)
key = canonical if use_canonical else (lambda s: s)
seen = set() groups = defaultdict(list) exact_dups = 0 for name, seq in cleaned: k = key(seq) if k in seen: exact_dups += 1 else: seen.add(k) groups[k].append(name)
ranked = sorted( ((k, names) for k, names in groups.items() if len(names) > 1), key=lambda kv: len(kv[1]), reverse=True, ) return { "total": len(raw_records), "valid": len(cleaned), "rejected": len(rejected), "unique": len(groups), "exact_duplicates": exact_dups, "top": ranked[:3], }
report = dedup_primer_library(raw_records)
assert report["total"] == 5assert report["valid"] == 4assert report["rejected"] == 1assert report["unique"] == 3assert report["exact_duplicates"] == 1assert report["top"][0][1] == ["GAPDH_F", "hGAPDH_forward"]Si todos estos assert se completan, habrán creado una herramienta funcional al integrar tres conceptos del libro de texto. ¡Felicitaciones!
¿Qué tan rápido es ahora? (Análisis detallado del rendimiento)
No basta con decir que es "rápido". Vamos a medirlo directamente. Generaremos 10 000 secuencias (incluyendo duplicados) y compararemos la versión básica con la versión que utiliza hash.
import random, time
random.seed(42)bases = "ACGT"pool = ["".join(random.choice(bases) for _ in range(20)) for _ in range(2000)]# Repite 2.000 tipos cinco veces → 10.000 elementos con muchos duplicadosbig = [(f"p{i}", random.choice(pool)) for i in range(10000)]
# Versión ingenua O(n²): incluso los primeros 1.500 ya son suficientemente lentossample = [s for _, s in big[:1500]]t0 = time.time()naive_pairs = find_duplicates_naive(sample)naive_time = time.time() - t0
# Versión hash O(n): los 10.000 completost0 = time.time()report = dedup_primer_library(big)hash_time = time.time() - t0
print(f"Versión ingenua (1.500): {naive_time:.3f} segundos")print(f"Versión hash (10.000) : {hash_time:.3f} segundos")
# Aunque procesa más de seis veces más datos, la versión hash es mucho más rápidaassert hash_time < naive_timeLos números varían según la máquina, pero la dirección es siempre la misma. Incluso con pocos datos, la versión ingenua es más lenta, y al aumentar los datos, la brecha explota. Este es el momento en que puedes tocar con tus propias manos la diferencia entre O(n²) y O(n). big-o-notation Las dos curvas que antes solo veías en un gráfico, ahora se están separando en el cronómetro de tu pantalla.
Hay otros caminos (Reflexión multipista)
Acabas de resolver el problema usando Hash Set + Diccionario. Excelente. Sin embargo, no hay un solo camino para resolver el mismo problema. La verdadera habilidad proviene de saber "qué otros métodos existen además del mío y cuándo es mejor usar cada uno".
- Pandas
drop_duplicates(): Si los datos ya están en un DataFrame, se resuelve con una sola líneadf.drop_duplicates(subset="seq"). Internamente también utiliza hashes. La librería hace por nosotros lo que nosotros hicimos manualmente. ¿Cuándo es mejor nuestro método? Cuando necesitemos extraer de forma personalizada incluso la frecuencia de duplicados y la lista de nombres.drop_duplicatessimplemente los elimina, pero no genera un informe de quién se duplicó cuántas veces. - Biopython: Ya existen herramientas especializadas en el parsing y la complementariedad reversa de secuencias. Procesa la complementariedad reversa de forma segura con
Seq(...).reverse_complement(). En el mundo profesional, lo correcto es usar estas librerías probadas. Sin embargo, si las usas sin entender los principios, no podrás depurar por qué son lentas o por qué los resultados son extraños. Por eso las hemos construido desde cero. - Ordenamiento y comparación de vecinos: Existe un método que consiste en ordenar las secuencias (O(n log n)) y luego comparar solo los elementos adyacentes. Puede ahorrar más memoria que un set, lo cual es ventajoso cuando los datos son tan grandes que no caben en la RAM.
Clave: Nuestra herramienta no es la "respuesta definitiva"; la respuesta definitiva es la persona que comprende el trade-off de cada método. El hash es rápido pero consume memoria, el ordenamiento ahorra memoria pero es un poco más lento, y las librerías son convenientes pero su funcionamiento interno es una caja negra.
Siguientes pasos (Enlaces de salida inferiores)
Si deseas expandir esta herramienta, los siguientes conceptos son el paso natural a seguir.
- Ahora hemos cargado todas las secuencias en la memoria. ¿Qué pasa si el archivo es más grande que la RAM? → Aprende a "buscar rápidamente en el disco" con Búsqueda Binaria e Índices de DB, lo que te llevará a la aplicación avanzada de Indexación de DB de secuencias.
- ¿Detectar duplicados en tiempo real en la web? → Con la aplicación práctica que integra WebSocket.
- ¿Por qué el conjunto es O(1)? → Vuelvan a la tarjeta Tabla hash y dominen también la resolución de colisiones.
Pruébenlo ustedes mismos (problema independiente)
Esta vez, sin el esqueleto, apliquen sus herramientas a una situación completamente nueva. La estructura es la misma: normalización → decisión → agregación.
- Añadan un filtro de longitud: Los cebadores suelen tener entre 18 y 25 nt. Amplíen
clean_recordspara enviar las secuencias que se salgan de este rango arejected. (Verifiquen conassertsi se filtran correctamente las de 30 nt). - Advertencia de contenido GC: Calculen el contenido GC de cada secuencia única y añadan una advertencia al informe si está fuera del rango del 40-60%. (Esto es un adelanto del próximo capítulo de aplicaciones, Calculadora de contenido GC).
- Informe del complemento inverso: Al usar
use_canonical=True, cuenten por separado las "duplicaciones exactas" y las "duplicaciones del complemento inverso", mostrándolas en dos líneas distintas en el informe. (Utilicen la pista del prompt de autoexplicación de 5 pasos). - Desafío: ¿Cómo deberían modificar la expresión regular del paso 2 para clasificar por separado los casos que contienen caracteres abreviados IUPAC (
N,R,Y, etc.) como "secuencia abreviada" en lugar de "inválida"?
Autoevalúense cada tarea con assert. Si la superan, ahora son los dueños de esta herramienta.
Resumen
Partimos de un problema aparentemente trivial: "limpiemos las duplicaciones", y vimos cómo tres conceptos de libro de texto se combinan para formar una herramienta práctica.
- Las expresiones regulares ajustaron la forma de la secuencia antes de compararla y filtraron el ruido (purificación).
- El conjunto hash respondió "¿lo he visto antes?" en O(1), reduciendo minutos a 0,3 segundos (decisión).
- El diccionario agrupó los nombres por secuencia, indicando quién se superpuso y cuántas veces (agregación).
Y estos tres elementos, al aprenderlos por separado, podían parecer "¿y para qué sirve esto?". Pero cuando se conectan, resuelven problemas reales del laboratorio. Este es el poder de la aplicación: aprendamos los conceptos por separado y ensamblémoslos como herramientas.
Reitero que este texto es un ejemplo educativo general. Sus bibliotecas reales tendrán muchas más columnas, reglas y excepciones. La versión detallada es lo que ustedes deben añadir a este esqueleto. El esqueleto siempre será el mismo: normalización, decisión, agregación.