Aller au contenu

Séparation et regroupement

Traduction assistée par IA - en savoir plus et suggérer des améliorations

Nextflow fournit des outils puissants pour travailler avec les données de manière flexible. Une capacité clé est la séparation des données en différents flux, puis le regroupement des éléments liés. C'est particulièrement utile dans les workflows bioinformatiques où vous devez traiter différents types d'échantillons séparément avant de combiner les résultats pour l'analyse.

Pensez-y comme au tri du courrier : vous séparez les lettres par destination, traitez chaque pile différemment, puis recombinez les éléments allant à la même personne. Nextflow utilise des opérateurs spéciaux pour accomplir cela avec des données scientifiques. Cette approche est également communément connue sous le nom de pattern scatter/gather dans le calcul distribué et les workflows bioinformatiques.

Le système de canaux de Nextflow est au cœur de cette flexibilité. Les canaux connectent différentes parties de votre workflow, permettant aux données de circuler à travers votre analyse. Vous pouvez créer plusieurs canaux à partir d'une seule source de données, traiter chaque canal différemment, puis fusionner les canaux lorsque nécessaire. Cette approche vous permet de concevoir des workflows qui reflètent naturellement les chemins ramifiés et convergents des analyses bioinformatiques complexes.

Objectifs d'apprentissage

Dans cette quête secondaire, vous apprendrez à séparer et regrouper des données en utilisant les opérateurs de canaux de Nextflow. Nous commencerons avec un fichier CSV contenant des informations sur les échantillons et les fichiers de données associés, puis nous manipulerons et réorganiserons ces données.

À la fin de cette quête secondaire, vous serez capable de séparer et combiner efficacement des flux de données, en utilisant les techniques suivantes :

  • Lire des données depuis des fichiers avec splitCsv
  • Filtrer et transformer des données avec filter et map
  • Combiner des données liées avec join et groupTuple
  • Créer des combinaisons de données avec combine pour le traitement parallèle
  • Optimiser la structure des données avec subMap et des stratégies de déduplication
  • Construire des fonctions réutilisables avec des closures nommées pour manipuler les structures de canaux

Ces compétences vous aideront à construire des workflows capables de gérer plusieurs fichiers d'entrée et différents types de données efficacement, tout en maintenant une structure de code propre et maintenable.

Prérequis

Avant de vous lancer dans cette quête secondaire, vous devriez :

  • Avoir complété le tutoriel Hello Nextflow ou un cours équivalent pour débutant·es.
  • Être à l'aise avec les concepts et mécanismes de base de Nextflow (processus, canaux, opérateurs, travail avec des fichiers, métadonnées)

Optionnel : Nous recommandons de compléter d'abord la quête secondaire Métadonnées dans les workflows. Celle-ci couvre les fondamentaux de la lecture de fichiers CSV avec splitCsv et la création de meta maps, que nous utiliserons abondamment ici.


0. Premiers pas

Ouvrir l'environnement de formation

Si vous ne l'avez pas encore fait, assurez-vous d'ouvrir l'environnement de formation comme décrit dans la Configuration de l'environnement.

Open in GitHub Codespaces

Se déplacer dans le répertoire du projet

Déplaçons-nous dans le répertoire où se trouvent les fichiers de ce tutoriel.

cd side-quests/splitting_and_grouping

Vous pouvez configurer VSCode pour se concentrer sur ce répertoire :

code .

Examiner les fichiers

Vous trouverez un fichier de workflow principal et un répertoire data contenant un samplesheet nommé samplesheet.csv.

Directory contents
.
├── data
│   └── samplesheet.csv
└── main.nf

Le samplesheet contient des informations sur des échantillons provenant de différents patients, notamment l'identifiant du patient, le numéro de répétition de l'échantillon, le type (normal ou tumoral), et les chemins vers des fichiers de données hypothétiques (qui n'existent pas réellement, mais nous ferons semblant qu'ils existent).

samplesheet.csv
id,repeat,type,bam
patientA,1,normal,patientA_rep1_normal.bam
patientA,1,tumor,patientA_rep1_tumor.bam
patientA,2,normal,patientA_rep2_normal.bam
patientA,2,tumor,patientA_rep2_tumor.bam
patientB,1,normal,patientB_rep1_normal.bam
patientB,1,tumor,patientB_rep1_tumor.bam
patientC,1,normal,patientC_rep1_normal.bam
patientC,1,tumor,patientC_rep1_tumor.bam

Ce samplesheet liste huit échantillons provenant de trois patients (A, B, C).

Pour chaque patient, nous avons des échantillons de type tumor (provenant généralement de biopsies tumorales) ou normal (prélevés sur des tissus sains ou du sang). Si vous n'êtes pas familier·ère avec l'analyse du cancer, sachez simplement que cela correspond à un modèle expérimental qui utilise des paires d'échantillons tumeur/normal pour effectuer des analyses contrastives.

Pour le patient A spécifiquement, nous avons deux ensembles de réplicats techniques (répétitions).

Note

Ne vous inquiétez pas si vous n'êtes pas familier·ère avec ce design expérimental, ce n'est pas essentiel pour comprendre ce tutoriel.

Examiner l'objectif

Votre défi est d'écrire un workflow Nextflow qui va :

  1. Lire les données d'échantillons depuis un fichier CSV et les structurer avec des meta maps
  2. Séparer les échantillons dans différents canaux selon leur type (normal vs tumoral)
  3. Joindre les paires tumeur/normal correspondantes par identifiant de patient et numéro de réplicat
  4. Distribuer les échantillons sur des intervalles génomiques pour le traitement parallèle
  5. Regrouper les échantillons liés pour l'analyse en aval

Cela représente un pattern bioinformatique courant où vous devez séparer les données pour un traitement indépendant, puis recombiner les éléments liés pour une analyse comparative.

Liste de vérification

Vous pensez être prêt·e à vous lancer ?

  • Je comprends l'objectif de ce cours et ses prérequis
  • Mon codespace est opérationnel
  • J'ai défini mon répertoire de travail de manière appropriée
  • Je comprends l'objectif

Si vous pouvez cocher toutes les cases, vous êtes prêt·e à commencer.


1. Lire les données d'échantillons

1.1. Lire les données d'échantillons avec splitCsv et créer des meta maps

Commençons par lire les données d'échantillons avec splitCsv et les organiser selon le pattern de meta map. Dans main.nf, vous verrez que nous avons déjà commencé le workflow.

main.nf
1
2
3
workflow {
    ch_samplesheet = channel.fromPath("./data/samplesheet.csv")
}

Note

Tout au long de ce tutoriel, nous utiliserons le préfixe ch_ pour toutes les variables de canal afin d'indiquer clairement qu'il s'agit de canaux Nextflow.

Si vous avez complété la quête secondaire Métadonnées dans les workflows, vous reconnaîtrez ce pattern. Nous utiliserons splitCsv pour lire le CSV et structurer immédiatement les données avec une meta map pour séparer les métadonnées des chemins de fichiers.

Info

Nous rencontrerons deux concepts différents appelés map dans cette formation :

  • Structure de données : La map Groovy (équivalente aux dictionnaires/hashes dans d'autres langages) qui stocke des paires clé-valeur
  • Opérateur de canal : L'opérateur .map() qui transforme les éléments dans un canal

Nous préciserons lequel nous voulons dire selon le contexte, mais cette distinction est importante à comprendre lorsque vous travaillez avec Nextflow.

Appliquez ces modifications à main.nf :

main.nf
2
3
4
5
6
7
    ch_samples = channel.fromPath("./data/samplesheet.csv")
        .splitCsv(header: true)
        .map{ row ->
          [[id:row.id, repeat:row.repeat, type:row.type], row.bam]
        }
        .view()
main.nf
    ch_samplesheet = channel.fromPath("./data/samplesheet.csv")

Cela combine l'opération splitCsv (lecture du CSV avec les en-têtes) et l'opération map (structuration des données en tuples [meta, fichier]) en une seule étape. Appliquez ce changement et exécutez le pipeline :

nextflow run main.nf
Sortie de la commande
N E X T F L O W   ~  version 25.10.2

Launching `main.nf` [deadly_mercator] DSL2 - revision: bd6b0224e9

[[id:patientA, repeat:1, type:normal], patientA_rep1_normal.bam]
[[id:patientA, repeat:1, type:tumor], patientA_rep1_tumor.bam]
[[id:patientA, repeat:2, type:normal], patientA_rep2_normal.bam]
[[id:patientA, repeat:2, type:tumor], patientA_rep2_tumor.bam]
[[id:patientB, repeat:1, type:normal], patientB_rep1_normal.bam]
[[id:patientB, repeat:1, type:tumor], patientB_rep1_tumor.bam]
[[id:patientC, repeat:1, type:normal], patientC_rep1_normal.bam]
[[id:patientC, repeat:1, type:tumor], patientC_rep1_tumor.bam]

Nous avons maintenant un canal où chaque élément est un tuple [meta, fichier] — les métadonnées sont séparées des chemins de fichiers. Cette structure nous permet de séparer et regrouper notre charge de travail en fonction des champs de métadonnées.


2. Filtrer et transformer les données

2.1. Filtrer les données avec filter

Nous pouvons utiliser l'opérateur filter pour filtrer les données selon une condition. Supposons que nous voulions uniquement traiter les échantillons normaux. Nous pouvons le faire en filtrant les données sur le champ type. Insérons cela avant l'opérateur view.

main.nf
2
3
4
5
6
7
8
    ch_samples = channel.fromPath("./data/samplesheet.csv")
        .splitCsv(header: true)
        .map{ row ->
          [[id:row.id, repeat:row.repeat, type:row.type], row.bam]
        }
        .filter { meta, file -> meta.type == 'normal' }
        .view()
main.nf
2
3
4
5
6
7
    ch_samples = channel.fromPath("./data/samplesheet.csv")
        .splitCsv(header: true)
        .map{ row ->
          [[id:row.id, repeat:row.repeat, type:row.type], row.bam]
        }
        .view()

Exécutez à nouveau le workflow pour voir le résultat filtré :

nextflow run main.nf
Sortie de la commande
N E X T F L O W   ~  version 25.10.2

Launching `main.nf` [admiring_brown] DSL2 - revision: 194d61704d

[[id:patientA, repeat:1, type:normal], patientA_rep1_normal.bam]
[[id:patientA, repeat:2, type:normal], patientA_rep2_normal.bam]
[[id:patientB, repeat:1, type:normal], patientB_rep1_normal.bam]
[[id:patientC, repeat:1, type:normal], patientC_rep1_normal.bam]

Nous avons filtré avec succès les données pour n'inclure que les échantillons normaux. Récapitulons comment cela fonctionne.

L'opérateur filter prend une closure qui est appliquée à chaque élément du canal. Si la closure retourne true, l'élément est inclus ; si elle retourne false, l'élément est exclu.

Dans notre cas, nous voulons conserver uniquement les échantillons où meta.type == 'normal'. La closure utilise le tuple meta,file pour faire référence à chaque échantillon, accède au type d'échantillon avec meta.type, et vérifie s'il est égal à 'normal'.

Cela est accompli avec la closure unique que nous avons introduite ci-dessus :

main.nf
    .filter { meta, file -> meta.type == 'normal' }

2.2. Créer des canaux filtrés séparés

Actuellement, nous appliquons le filtre au canal créé directement depuis le CSV, mais nous voulons filtrer de plusieurs façons, alors réécrivons la logique pour créer un canal filtré séparé pour les échantillons normaux :

main.nf
    ch_samples = channel.fromPath("./data/samplesheet.csv")
        .splitCsv(header: true)
        .map{ row ->
            [[id:row.id, repeat:row.repeat, type:row.type], row.bam]
        }
    ch_normal_samples = ch_samples
        .filter { meta, file -> meta.type == 'normal' }
    ch_normal_samples
        .view()
main.nf
2
3
4
5
6
7
8
    ch_samples = channel.fromPath("./data/samplesheet.csv")
        .splitCsv(header: true)
        .map{ row ->
          [[id:row.id, repeat:row.repeat, type:row.type], row.bam]
        }
        .filter { meta, file -> meta.type == 'normal' }
        .view()

Exécutez le pipeline pour voir les résultats :

nextflow run main.nf
Sortie de la commande
N E X T F L O W   ~  version 25.10.2

Launching `main.nf` [trusting_poisson] DSL2 - revision: 639186ee74

[[id:patientA, repeat:1, type:normal], patientA_rep1_normal.bam]
[[id:patientA, repeat:2, type:normal], patientA_rep2_normal.bam]
[[id:patientB, repeat:1, type:normal], patientB_rep1_normal.bam]
[[id:patientC, repeat:1, type:normal], patientC_rep1_normal.bam]

Nous avons filtré avec succès les données et créé un canal séparé pour les échantillons normaux.

Créons également un canal filtré pour les échantillons tumoraux :

main.nf
    ch_normal_samples = ch_samples
        .filter { meta, file -> meta.type == 'normal' }
    ch_tumor_samples = ch_samples
        .filter { meta, file -> meta.type == 'tumor' }
    ch_normal_samples
        .view{'Normal sample: ' + it}
    ch_tumor_samples
        .view{'Tumor sample: ' + it}
main.nf
    ch_normal_samples = ch_samples
        .filter { meta, file -> meta.type == 'normal' }
    ch_normal_samples
        .view()
nextflow run main.nf
Sortie de la commande
N E X T F L O W   ~  version 25.10.2

Launching `main.nf` [maniac_boltzmann] DSL2 - revision: 3636b6576b

Tumor sample: [[id:patientA, repeat:1, type:tumor], patientA_rep1_tumor.bam]
Tumor sample: [[id:patientA, repeat:2, type:tumor], patientA_rep2_tumor.bam]
Normal sample: [[id:patientA, repeat:1, type:normal], patientA_rep1_normal.bam]
Normal sample: [[id:patientA, repeat:2, type:normal], patientA_rep2_normal.bam]
Normal sample: [[id:patientB, repeat:1, type:normal], patientB_rep1_normal.bam]
Normal sample: [[id:patientC, repeat:1, type:normal], patientC_rep1_normal.bam]
Tumor sample: [[id:patientB, repeat:1, type:tumor], patientB_rep1_tumor.bam]
Tumor sample: [[id:patientC, repeat:1, type:tumor], patientC_rep1_tumor.bam]

Nous avons séparé les échantillons normaux et tumoraux dans deux canaux différents, et utilisé une closure fournie à view() pour les étiqueter différemment dans la sortie : ch_tumor_samples.view{'Tumor sample: ' + it}.

À retenir

Dans cette section, vous avez appris :

  • Filtrer les données : Comment filtrer les données avec filter
  • Séparer les données : Comment séparer les données dans différents canaux selon une condition
  • Visualiser les données : Comment utiliser view pour afficher les données et étiqueter la sortie de différents canaux

Nous avons maintenant séparé les échantillons normaux et tumoraux dans deux canaux différents. Ensuite, nous joindrons les échantillons normaux et tumoraux sur le champ id.


3. Joindre des canaux par identifiants

Dans la section précédente, nous avons séparé les échantillons normaux et tumoraux dans deux canaux différents. Ceux-ci pourraient être traités indépendamment en utilisant des processus ou des workflows spécifiques selon leur type. Mais que se passe-t-il lorsque nous voulons comparer les échantillons normaux et tumoraux du même patient ? À ce stade, nous devons les rejoindre en nous assurant de faire correspondre les échantillons sur la base de leur champ id.

Nextflow inclut de nombreuses méthodes pour combiner des canaux, mais dans ce cas l'opérateur le plus approprié est join. Si vous êtes familier·ère avec SQL, il agit comme l'opération JOIN, où nous spécifions la clé sur laquelle joindre et le type de jointure à effectuer.

3.1. Utiliser map et join pour combiner sur la base de l'identifiant patient

Si nous consultons la documentation de join, nous pouvons voir que par défaut il joint deux canaux sur la base du premier élément de chaque tuple.

3.1.1. Vérifier la structure des données

Si vous n'avez plus la sortie console disponible, exécutons le pipeline pour vérifier notre structure de données et voir comment nous devons la modifier pour joindre sur le champ id.

nextflow run main.nf
Sortie de la commande
N E X T F L O W   ~  version 25.10.2

Launching `main.nf` [maniac_boltzmann] DSL2 - revision: 3636b6576b

Tumor sample: [[id:patientA, repeat:1, type:tumor], patientA_rep1_tumor.bam]
Tumor sample: [[id:patientA, repeat:2, type:tumor], patientA_rep2_tumor.bam]
Normal sample: [[id:patientA, repeat:1, type:normal], patientA_rep1_normal.bam]
Normal sample: [[id:patientA, repeat:2, type:normal], patientA_rep2_normal.bam]
Normal sample: [[id:patientB, repeat:1, type:normal], patientB_rep1_normal.bam]
Normal sample: [[id:patientC, repeat:1, type:normal], patientC_rep1_normal.bam]
Tumor sample: [[id:patientB, repeat:1, type:tumor], patientB_rep1_tumor.bam]
Tumor sample: [[id:patientC, repeat:1, type:tumor], patientC_rep1_tumor.bam]

Nous pouvons voir que le champ id est le premier élément de chaque meta map. Pour que join fonctionne, nous devons isoler le champ id dans chaque tuple. Après cela, nous pouvons simplement utiliser l'opérateur join pour combiner les deux canaux.

3.1.2. Isoler le champ id

Pour isoler le champ id, nous pouvons utiliser l'opérateur map pour créer un nouveau tuple avec le champ id comme premier élément.

main.nf
    ch_normal_samples = ch_samples
        .filter { meta, file -> meta.type == 'normal' }
        .map { meta, file -> [meta.id, meta, file] }
    ch_tumor_samples = ch_samples
        .filter { meta, file -> meta.type == 'tumor' }
        .map { meta, file -> [meta.id, meta, file] }
    ch_normal_samples
        .view{'Normal sample: ' + it}
    ch_tumor_samples
        .view{'Tumor sample: ' + it}
main.nf
    ch_normal_samples = ch_samples
        .filter { meta, file -> meta.type == 'normal' }
    ch_tumor_samples = ch_samples
        .filter { meta, file -> meta.type == 'tumor' }
    ch_normal_samples
        .view{'Normal sample: ' + it}
    ch_tumor_samples
        .view{'Tumor sample: ' + it}
nextflow run main.nf
Sortie de la commande
N E X T F L O W   ~  version 25.10.2

Launching `main.nf` [mad_lagrange] DSL2 - revision: 9940b3f23d

Tumor sample: [patientA, [id:patientA, repeat:1, type:tumor], patientA_rep1_tumor.bam]
Tumor sample: [patientA, [id:patientA, repeat:2, type:tumor], patientA_rep2_tumor.bam]
Normal sample: [patientA, [id:patientA, repeat:1, type:normal], patientA_rep1_normal.bam]
Normal sample: [patientA, [id:patientA, repeat:2, type:normal], patientA_rep2_normal.bam]
Tumor sample: [patientB, [id:patientB, repeat:1, type:tumor], patientB_rep1_tumor.bam]
Tumor sample: [patientC, [id:patientC, repeat:1, type:tumor], patientC_rep1_tumor.bam]
Normal sample: [patientB, [id:patientB, repeat:1, type:normal], patientB_rep1_normal.bam]
Normal sample: [patientC, [id:patientC, repeat:1, type:normal], patientC_rep1_normal.bam]

C'est peut-être subtil, mais vous devriez pouvoir voir que le premier élément de chaque tuple est le champ id.

3.1.3. Combiner les deux canaux

Nous pouvons maintenant utiliser l'opérateur join pour combiner les deux canaux sur la base du champ id.

Une fois encore, nous utiliserons view pour afficher les sorties jointes.

main.nf
    ch_normal_samples = ch_samples
        .filter { meta, file -> meta.type == 'normal' }
        .map { meta, file -> [meta.id, meta, file] }
    ch_tumor_samples = ch_samples
        .filter { meta, file -> meta.type == 'tumor' }
        .map { meta, file -> [meta.id, meta, file] }
    ch_joined_samples = ch_normal_samples
        .join(ch_tumor_samples)
    ch_joined_samples.view()
main.nf
    ch_normal_samples = ch_samples
        .filter { meta, file -> meta.type == 'normal' }
        .map { meta, file -> [meta.id, meta, file] }
    ch_tumor_samples = ch_samples
        .filter { meta, file -> meta.type == 'tumor' }
        .map { meta, file -> [meta.id, meta, file] }
    ch_normal_samples
        .view{'Normal sample: ' + it}
    ch_tumor_samples
        .view{'Tumor sample: ' + it}
nextflow run main.nf
Sortie de la commande
N E X T F L O W   ~  version 25.10.2

Launching `main.nf` [soggy_wiles] DSL2 - revision: 3bc1979889

[patientA, [id:patientA, repeat:1, type:normal], patientA_rep1_normal.bam, [id:patientA, repeat:1, type:tumor], patientA_rep1_tumor.bam]
[patientA, [id:patientA, repeat:2, type:normal], patientA_rep2_normal.bam, [id:patientA, repeat:2, type:tumor], patientA_rep2_tumor.bam]
[patientB, [id:patientB, repeat:1, type:normal], patientB_rep1_normal.bam, [id:patientB, repeat:1, type:tumor], patientB_rep1_tumor.bam]
[patientC, [id:patientC, repeat:1, type:normal], patientC_rep1_normal.bam, [id:patientC, repeat:1, type:tumor], patientC_rep1_tumor.bam]

C'est un peu difficile à lire car c'est très large, mais vous devriez pouvoir voir que les échantillons ont été joints sur le champ id. Chaque tuple a maintenant le format suivant :

  • id : L'identifiant de l'échantillon
  • normal_meta_map : Les métadonnées de l'échantillon normal incluant le type, le réplicat et le chemin vers le fichier BAM
  • normal_sample_file : Le fichier de l'échantillon normal
  • tumor_meta_map : Les métadonnées de l'échantillon tumoral incluant le type, le réplicat et le chemin vers le fichier BAM
  • tumor_sample : L'échantillon tumoral incluant le type, le réplicat et le chemin vers le fichier BAM

Avertissement

L'opérateur join supprimera tous les tuples non appariés. Dans cet exemple, nous nous sommes assurés que tous les échantillons étaient appariés pour les types tumoral et normal, mais si ce n'est pas le cas, vous devez utiliser le paramètre remainder: true pour conserver les tuples non appariés. Consultez la documentation pour plus de détails.

Vous savez maintenant comment utiliser map pour isoler un champ dans un tuple, et comment utiliser join pour combiner des tuples sur la base du premier champ. Avec ces connaissances, nous pouvons combiner avec succès des canaux sur la base d'un champ partagé.

Ensuite, nous examinerons la situation où vous souhaitez joindre sur plusieurs champs.

3.2. Joindre sur plusieurs champs

Nous avons 2 réplicats pour sampleA, mais seulement 1 pour sampleB et sampleC. Dans ce cas, nous avons pu les joindre efficacement en utilisant le champ id, mais que se passerait-il s'ils étaient désynchronisés ? Nous pourrions mélanger les échantillons normaux et tumoraux de différents réplicats !

Pour éviter cela, nous pouvons joindre sur plusieurs champs. Il existe en réalité plusieurs façons d'y parvenir, mais nous allons nous concentrer sur la création d'une nouvelle clé de jointure qui inclut à la fois l'id de l'échantillon et le numéro de replicate.

Commençons par créer une nouvelle clé de jointure. Nous pouvons le faire de la même manière qu'avant en utilisant l'opérateur map pour créer un nouveau tuple avec les champs id et repeat comme premier élément.

main.nf
    ch_normal_samples = ch_samples
        .filter { meta, file -> meta.type == 'normal' }
        .map { meta, file -> [[meta.id, meta.repeat], meta, file] }
    ch_tumor_samples = ch_samples
        .filter { meta, file -> meta.type == 'tumor' }
        .map { meta, file -> [[meta.id, meta.repeat], meta, file] }
main.nf
    ch_normal_samples = ch_samples
        .filter { meta, file -> meta.type == 'normal' }
        .map { meta, file -> [meta.id, meta, file] }
    ch_tumor_samples = ch_samples
        .filter { meta, file -> meta.type == 'tumor' }
        .map { meta, file -> [meta.id, meta, file] }

Nous devrions maintenant voir que la jointure s'effectue en utilisant à la fois les champs id et repeat. Exécutez le workflow :

nextflow run main.nf
Sortie de la commande
N E X T F L O W   ~  version 25.10.2

Launching `main.nf` [prickly_wing] DSL2 - revision: 3bebf22dee

[[patientA, 1], [id:patientA, repeat:1, type:normal], patientA_rep1_normal.bam, [id:patientA, repeat:1, type:tumor], patientA_rep1_tumor.bam]
[[patientA, 2], [id:patientA, repeat:2, type:normal], patientA_rep2_normal.bam, [id:patientA, repeat:2, type:tumor], patientA_rep2_tumor.bam]
[[patientB, 1], [id:patientB, repeat:1, type:normal], patientB_rep1_normal.bam, [id:patientB, repeat:1, type:tumor], patientB_rep1_tumor.bam]
[[patientC, 1], [id:patientC, repeat:1, type:normal], patientC_rep1_normal.bam, [id:patientC, repeat:1, type:tumor], patientC_rep1_tumor.bam]

Notez que nous avons un tuple de deux éléments (les champs id et repeat) comme premier élément de chaque résultat joint. Cela démontre comment des éléments complexes peuvent être utilisés comme clé de jointure, permettant une correspondance assez précise entre des échantillons provenant des mêmes conditions.

Si vous souhaitez explorer d'autres façons de joindre sur différentes clés, consultez la documentation de l'opérateur join pour des options et des exemples supplémentaires.

3.3. Utiliser subMap pour créer une nouvelle clé de jointure

L'approche précédente perd les noms de champs de notre clé de jointure — les champs id et repeat deviennent simplement une liste de valeurs. Pour conserver les noms de champs pour un accès ultérieur, nous pouvons utiliser la méthode subMap.

La méthode subMap extrait uniquement les paires clé-valeur spécifiées d'une map. Ici, nous extrairons uniquement les champs id et repeat pour créer notre clé de jointure.

main.nf
    ch_normal_samples = ch_samples
        .filter { meta, file -> meta.type == 'normal' }
        .map { meta, file -> [meta.subMap(['id', 'repeat']), meta, file] }
    ch_tumor_samples = ch_samples
        .filter { meta, file -> meta.type == 'tumor' }
        .map { meta, file -> [meta.subMap(['id', 'repeat']), meta, file] }
main.nf
    ch_normal_samples = ch_samples
        .filter { meta, file -> meta.type == 'normal' }
        .map { meta, file -> [[meta.id, meta.repeat], meta, file] }
    ch_tumor_samples = ch_samples
        .filter { meta, file -> meta.type == 'tumor' }
        .map { meta, file -> [[meta.id, meta.repeat], meta, file] }
nextflow run main.nf
Sortie de la commande
N E X T F L O W   ~  version 25.10.2

Launching `main.nf` [reverent_wing] DSL2 - revision: 847016c3b7

[[id:patientA, repeat:1], [id:patientA, repeat:1, type:normal], patientA_rep1_normal.bam, [id:patientA, repeat:1, type:tumor], patientA_rep1_tumor.bam]
[[id:patientA, repeat:2], [id:patientA, repeat:2, type:normal], patientA_rep2_normal.bam, [id:patientA, repeat:2, type:tumor], patientA_rep2_tumor.bam]
[[id:patientB, repeat:1], [id:patientB, repeat:1, type:normal], patientB_rep1_normal.bam, [id:patientB, repeat:1, type:tumor], patientB_rep1_tumor.bam]
[[id:patientC, repeat:1], [id:patientC, repeat:1, type:normal], patientC_rep1_normal.bam, [id:patientC, repeat:1, type:tumor], patientC_rep1_tumor.bam]

Nous avons maintenant une nouvelle clé de jointure qui inclut non seulement les champs id et repeat, mais conserve également les noms de champs pour pouvoir y accéder ultérieurement par leur nom, par exemple meta.id et meta.repeat.

3.4. Utiliser une closure nommée dans map

Pour éviter la duplication et réduire les erreurs, nous pouvons utiliser une closure nommée. Une closure nommée nous permet de créer une fonction réutilisable que nous pouvons appeler à plusieurs endroits.

Pour ce faire, nous définissons d'abord la closure comme une nouvelle variable :

main.nf
    ch_samples = channel.fromPath("./data/samplesheet.csv")
        .splitCsv(header: true)
        .map{ row ->
            [[id:row.id, repeat:row.repeat, type:row.type], row.bam]
        }

    getSampleIdAndReplicate = { meta, bam -> [ meta.subMap(['id', 'repeat']), meta, file(bam) ] }

    ch_normal_samples = ch_samples
        .filter { meta, file -> meta.type == 'normal' }
main.nf
2
3
4
5
6
7
8
    ch_samples = channel.fromPath("./data/samplesheet.csv")
        .splitCsv(header: true)
        .map{ row ->
            [[id:row.id, repeat:row.repeat, type:row.type], row.bam]
        }
    ch_normal_samples = ch_samples
        .filter { meta, file -> meta.type == 'normal' }

Nous avons défini la transformation map comme une variable nommée que nous pouvons réutiliser.

Notez que nous convertissons également le chemin du fichier en objet Path en utilisant file() afin que tout processus recevant ce canal puisse gérer le fichier correctement (pour plus d'informations, voir Travailler avec des fichiers).

Implémentons la closure dans notre workflow :

main.nf
    ch_normal_samples = ch_samples
        .filter { meta, file -> meta.type == 'normal' }
         .map ( getSampleIdAndReplicate )
    ch_tumor_samples = ch_samples
        .filter { meta, file -> meta.type == 'tumor' }
         .map ( getSampleIdAndReplicate )
main.nf
    ch_normal_samples = ch_samples
        .filter { meta, file -> meta.type == 'normal' }
        .map { meta, file -> [meta.subMap(['id', 'repeat']), meta, file] }
    ch_tumor_samples = ch_samples
        .filter { meta, file -> meta.type == 'tumor' }
        .map { meta, file -> [meta.subMap(['id', 'repeat']), meta, file] }

Note

L'opérateur map est passé de l'utilisation de { } à l'utilisation de ( ) pour passer la closure comme argument. C'est parce que l'opérateur map attend une closure comme argument et { } est utilisé pour définir une closure anonyme. Lors de l'appel d'une closure nommée, utilisez la syntaxe ( ).

Exécutez à nouveau le workflow pour vérifier que tout fonctionne toujours :

nextflow run main.nf
Sortie de la commande
N E X T F L O W   ~  version 25.10.2

Launching `main.nf` [angry_meninsky] DSL2 - revision: 2edc226b1d

[[id:patientA, repeat:1], [id:patientA, repeat:1, type:normal], patientA_rep1_normal.bam, [id:patientA, repeat:1, type:tumor], patientA_rep1_tumor.bam]
[[id:patientA, repeat:2], [id:patientA, repeat:2, type:normal], patientA_rep2_normal.bam, [id:patientA, repeat:2, type:tumor], patientA_rep2_tumor.bam]
[[id:patientB, repeat:1], [id:patientB, repeat:1, type:normal], patientB_rep1_normal.bam, [id:patientB, repeat:1, type:tumor], patientB_rep1_tumor.bam]
[[id:patientC, repeat:1], [id:patientC, repeat:1, type:normal], patientC_rep1_normal.bam, [id:patientC, repeat:1, type:tumor], patientC_rep1_tumor.bam]

L'utilisation d'une closure nommée nous permet de réutiliser la même transformation à plusieurs endroits, réduisant le risque d'erreurs et rendant le code plus lisible et maintenable.

3.5. Réduire la duplication des données

Nous avons beaucoup de données dupliquées dans notre workflow. Chaque élément des échantillons joints répète les champs id et repeat. Puisque cette information est déjà disponible dans la clé de regroupement, nous pouvons éviter cette redondance. Pour rappel, notre structure de données actuelle ressemble à ceci :

[
  [
    "id": "sampleC",
    "repeat": "1",
  ],
  [
    "id": "sampleC",
    "repeat": "1",
    "type": "normal",
  ],
  "sampleC_rep1_normal.bam"
  [
    "id": "sampleC",
    "repeat": "1",
    "type": "tumor",
  ],
  "sampleC_rep1_tumor.bam"
]

Puisque les champs id et repeat sont disponibles dans la clé de regroupement, supprimons-les du reste de chaque élément du canal pour éviter la duplication. Nous pouvons le faire en utilisant la méthode subMap pour créer une nouvelle map avec uniquement le champ type. Cette approche nous permet de maintenir toutes les informations nécessaires tout en éliminant la redondance dans notre structure de données.

main.nf
    getSampleIdAndReplicate = { meta, bam -> [ meta.subMap(['id', 'repeat']), meta.subMap(['type']), file(bam) ] }
main.nf
    getSampleIdAndReplicate = { meta, bam -> [ meta.subMap(['id', 'repeat']), meta, file(bam) ] }

Maintenant, la closure retourne un tuple où le premier élément contient les champs id et repeat, et le second élément contient uniquement le champ type. Cela élimine la redondance en stockant les informations id et repeat une seule fois dans la clé de regroupement, tout en maintenant toutes les informations nécessaires.

Exécutez le workflow pour voir à quoi cela ressemble :

nextflow run main.nf
Sortie de la commande
[[id:patientA, repeat:1], [type:normal], /workspaces/training/side-quests/splitting_and_grouping/patientA_rep1_normal.bam, [type:tumor], /workspaces/training/side-quests/splitting_and_grouping/patientA_rep1_tumor.bam]
[[id:patientA, repeat:2], [type:normal], /workspaces/training/side-quests/splitting_and_grouping/patientA_rep2_normal.bam, [type:tumor], /workspaces/training/side-quests/splitting_and_grouping/patientA_rep2_tumor.bam]
[[id:patientB, repeat:1], [type:normal], /workspaces/training/side-quests/splitting_and_grouping/patientB_rep1_normal.bam, [type:tumor], /workspaces/training/side-quests/splitting_and_grouping/patientB_rep1_tumor.bam]
[[id:patientC, repeat:1], [type:normal], /workspaces/training/side-quests/splitting_and_grouping/patientC_rep1_normal.bam, [type:tumor], /workspaces/training/side-quests/splitting_and_grouping/patientC_rep1_tumor.bam]

Nous pouvons voir que nous ne mentionnons les champs id et repeat qu'une seule fois dans la clé de regroupement et que nous avons le champ type dans les données d'échantillon. Nous n'avons perdu aucune information mais nous avons réussi à rendre le contenu de notre canal plus concis.

3.6. Supprimer les informations redondantes

Nous avons supprimé les informations dupliquées ci-dessus, mais il reste encore d'autres informations redondantes dans nos canaux.

Au début, nous avons séparé les échantillons normaux et tumoraux avec filter, puis nous les avons joints sur les clés id et repeat. L'opérateur join préserve l'ordre dans lequel les tuples sont fusionnés, donc dans notre cas, avec les échantillons normaux à gauche et les échantillons tumoraux à droite, le canal résultant maintient cette structure : id, <éléments normaux>, <éléments tumoraux>.

Puisque nous connaissons la position de chaque élément dans notre canal, nous pouvons simplifier davantage la structure en supprimant les métadonnées [type:normal] et [type:tumor].

main.nf
    getSampleIdAndReplicate = { meta, file -> [ meta.subMap(['id', 'repeat']), file ] }
main.nf
    getSampleIdAndReplicate = { meta, file -> [ meta.subMap(['id', 'repeat']), meta.subMap(['type']), file ] }

Exécutez à nouveau pour voir le résultat :

nextflow run main.nf
Sortie de la commande
N E X T F L O W   ~  version 25.10.2

Launching `main.nf` [confident_leavitt] DSL2 - revision: a2303895bd

[[id:patientA, repeat:1], patientA_rep1_normal.bam, patientA_rep1_tumor.bam]
[[id:patientA, repeat:2], patientA_rep2_normal.bam, patientA_rep2_tumor.bam]
[[id:patientB, repeat:1], patientB_rep1_normal.bam, patientB_rep1_tumor.bam]
[[id:patientC, repeat:1], patientC_rep1_normal.bam, patientC_rep1_tumor.bam]

À retenir

Dans cette section, vous avez appris :

  • Manipuler des tuples : Comment utiliser map pour isoler un champ dans un tuple
  • Joindre des tuples : Comment utiliser join pour combiner des tuples sur la base du premier champ
  • Créer des clés de jointure : Comment utiliser subMap pour créer une nouvelle clé de jointure
  • Closures nommées : Comment utiliser une closure nommée dans map
  • Jointure sur plusieurs champs : Comment joindre sur plusieurs champs pour une correspondance plus précise
  • Optimisation de la structure des données : Comment simplifier la structure des canaux en supprimant les informations redondantes

Vous disposez maintenant d'un workflow capable de séparer un samplesheet, de filtrer les échantillons normaux et tumoraux, de les joindre par identifiant d'échantillon et numéro de réplicat, puis d'afficher les résultats.

C'est un pattern courant dans les workflows bioinformatiques où vous devez faire correspondre des échantillons ou d'autres types de données après un traitement indépendant, c'est donc une compétence utile. Ensuite, nous verrons comment répéter un échantillon plusieurs fois.

4. Distribuer les échantillons sur des intervalles

Un pattern clé dans les workflows bioinformatiques est la distribution de l'analyse sur des régions génomiques. Par exemple, l'appel de variants peut être parallélisé en divisant le génome en intervalles (comme des chromosomes ou des régions plus petites). Cette stratégie de parallélisation améliore significativement l'efficacité du pipeline en distribuant la charge de calcul sur plusieurs cœurs ou nœuds, réduisant ainsi le temps d'exécution global.

Dans la section suivante, nous allons démontrer comment distribuer nos données d'échantillons sur plusieurs intervalles génomiques. Nous associerons chaque échantillon à chaque intervalle, permettant le traitement parallèle de différentes régions génomiques. Cela multipliera la taille de notre jeu de données par le nombre d'intervalles, créant plusieurs unités d'analyse indépendantes qui pourront être regroupées ultérieurement.

4.1. Distribuer les échantillons sur des intervalles avec combine

Commençons par créer un canal d'intervalles. Pour simplifier, nous utiliserons simplement 3 intervalles que nous définirons manuellement. Dans un workflow réel, vous pourriez les lire depuis un fichier d'entrée ou même créer un canal avec de nombreux fichiers d'intervalles.

main.nf
        .join(ch_tumor_samples)
    ch_intervals = channel.of('chr1', 'chr2', 'chr3')
main.nf
        .join(ch_tumor_samples)
    ch_joined_samples.view()

Rappelons-nous que nous voulons répéter chaque échantillon pour chaque intervalle. C'est parfois appelé le produit cartésien des échantillons et des intervalles. Nous pouvons y parvenir en utilisant l'opérateur combine. Cela prendra chaque élément du canal 1 et le répétera pour chaque élément du canal 2. Ajoutons un opérateur combine à notre workflow :

main.nf
    ch_intervals = channel.of('chr1', 'chr2', 'chr3')

    ch_combined_samples = ch_joined_samples
        .combine(ch_intervals)
        .view()
main.nf
    ch_intervals = channel.of('chr1', 'chr2', 'chr3')

Exécutons maintenant et voyons ce qui se passe :

nextflow run main.nf
Sortie de la commande
N E X T F L O W   ~  version 25.10.2

Launching `main.nf` [mighty_tesla] DSL2 - revision: ae013ab70b

[[id:patientA, repeat:1], patientA_rep1_normal.bam, patientA_rep1_tumor.bam, chr1]
[[id:patientA, repeat:1], patientA_rep1_normal.bam, patientA_rep1_tumor.bam, chr2]
[[id:patientA, repeat:1], patientA_rep1_normal.bam, patientA_rep1_tumor.bam, chr3]
[[id:patientA, repeat:2], patientA_rep2_normal.bam, patientA_rep2_tumor.bam, chr1]
[[id:patientA, repeat:2], patientA_rep2_normal.bam, patientA_rep2_tumor.bam, chr2]
[[id:patientA, repeat:2], patientA_rep2_normal.bam, patientA_rep2_tumor.bam, chr3]
[[id:patientB, repeat:1], patientB_rep1_normal.bam, patientB_rep1_tumor.bam, chr1]
[[id:patientB, repeat:1], patientB_rep1_normal.bam, patientB_rep1_tumor.bam, chr2]
[[id:patientB, repeat:1], patientB_rep1_normal.bam, patientB_rep1_tumor.bam, chr3]
[[id:patientC, repeat:1], patientC_rep1_normal.bam, patientC_rep1_tumor.bam, chr1]
[[id:patientC, repeat:1], patientC_rep1_normal.bam, patientC_rep1_tumor.bam, chr2]
[[id:patientC, repeat:1], patientC_rep1_normal.bam, patientC_rep1_tumor.bam, chr3]

Succès ! Nous avons répété chaque échantillon pour chaque intervalle de notre liste de 3 intervalles. Nous avons effectivement triplé le nombre d'éléments dans notre canal.

C'est un peu difficile à lire, donc dans la section suivante nous allons l'organiser.

4.2. Organiser le canal

Nous pouvons utiliser l'opérateur map pour organiser et refactoriser nos données d'échantillons afin qu'elles soient plus faciles à comprendre. Déplaçons la chaîne d'intervalles vers la map de jointure en premier élément.

main.nf
    ch_combined_samples = ch_joined_samples
        .combine(ch_intervals)
        .map { grouping_key, normal, tumor, interval ->
            [
                grouping_key + [interval: interval],
                normal,
                tumor
            ]
        }
        .view()
main.nf
    ch_combined_samples = ch_joined_samples
        .combine(ch_intervals)
        .view()

Décomposons ce que fait cette opération map étape par étape.

Premièrement, nous utilisons des paramètres nommés pour rendre le code plus lisible. En utilisant les noms grouping_key, normal, tumor et interval, nous pouvons faire référence aux éléments du tuple par leur nom plutôt que par leur index :

        .map { grouping_key, normal, tumor, interval ->

Ensuite, nous combinons le grouping_key avec le champ interval. Le grouping_key est une map contenant les champs id et repeat. Nous créons une nouvelle map avec l'interval et les fusionnons en utilisant l'addition de maps Groovy (+) :

                grouping_key + [interval: interval],

Enfin, nous retournons cela comme un tuple de trois éléments : la map de métadonnées combinée, le fichier de l'échantillon normal et le fichier de l'échantillon tumoral :

            [
                grouping_key + [interval: interval],
                normal,
                tumor
            ]

Exécutons à nouveau et vérifions le contenu du canal :

nextflow run main.nf
Sortie de la commande
N E X T F L O W   ~  version 25.10.2

Launching `main.nf` [sad_hawking] DSL2 - revision: 1f6f6250cd

[[id:patientA, repeat:1, interval:chr1], patientA_rep1_normal.bam, patientA_rep1_tumor.bam]
[[id:patientA, repeat:1, interval:chr2], patientA_rep1_normal.bam, patientA_rep1_tumor.bam]
[[id:patientA, repeat:1, interval:chr3], patientA_rep1_normal.bam, patientA_rep1_tumor.bam]
[[id:patientA, repeat:2, interval:chr1], patientA_rep2_normal.bam, patientA_rep2_tumor.bam]
[[id:patientA, repeat:2, interval:chr2], patientA_rep2_normal.bam, patientA_rep2_tumor.bam]
[[id:patientA, repeat:2, interval:chr3], patientA_rep2_normal.bam, patientA_rep2_tumor.bam]
[[id:patientB, repeat:1, interval:chr1], patientB_rep1_normal.bam, patientB_rep1_tumor.bam]
[[id:patientB, repeat:1, interval:chr2], patientB_rep1_normal.bam, patientB_rep1_tumor.bam]
[[id:patientB, repeat:1, interval:chr3], patientB_rep1_normal.bam, patientB_rep1_tumor.bam]
[[id:patientC, repeat:1, interval:chr1], patientC_rep1_normal.bam, patientC_rep1_tumor.bam]
[[id:patientC, repeat:1, interval:chr2], patientC_rep1_normal.bam, patientC_rep1_tumor.bam]
[[id:patientC, repeat:1, interval:chr3], patientC_rep1_normal.bam, patientC_rep1_tumor.bam]

Utiliser map pour contraindre vos données dans la structure correcte peut être délicat, mais c'est crucial pour une manipulation efficace des données.

Nous avons maintenant chaque échantillon répété sur tous les intervalles génomiques, créant plusieurs unités d'analyse indépendantes qui peuvent être traitées en parallèle. Mais que faire si nous voulons regrouper des échantillons liés ? Dans la section suivante, nous apprendrons comment regrouper des échantillons qui partagent des attributs communs.

À retenir

Dans cette section, vous avez appris :

  • Distribuer les échantillons sur des intervalles : Comment utiliser combine pour répéter des échantillons sur des intervalles
  • Créer des produits cartésiens : Comment générer toutes les combinaisons d'échantillons et d'intervalles
  • Organiser la structure des canaux : Comment utiliser map pour restructurer les données pour une meilleure lisibilité
  • Préparation au traitement parallèle : Comment configurer les données pour une analyse distribuée

5. Agréger des échantillons avec groupTuple

Dans les sections précédentes, nous avons appris à séparer les données d'un fichier d'entrée et à filtrer par champs spécifiques (dans notre cas les échantillons normaux et tumoraux). Mais cela ne couvre qu'un seul type de jointure. Que faire si nous voulons regrouper des échantillons par un attribut spécifique ? Par exemple, au lieu de joindre des paires normal-tumeur correspondantes, nous pourrions vouloir traiter tous les échantillons de "sampleA" ensemble indépendamment de leur type. Ce pattern est courant dans les workflows bioinformatiques où vous pouvez vouloir traiter des échantillons liés séparément pour des raisons d'efficacité avant de comparer ou combiner les résultats à la fin.

Nextflow inclut des méthodes intégrées pour faire cela, la principale que nous examinerons est groupTuple.

Commençons par regrouper tous nos échantillons qui ont les mêmes champs id et interval, ce qui serait typique d'une analyse où nous voulions regrouper des réplicats techniques mais garder des échantillons significativement différents séparés.

Pour ce faire, nous devons séparer nos variables de regroupement afin de pouvoir les utiliser de manière isolée.

La première étape est similaire à ce que nous avons fait dans la section précédente. Nous devons isoler notre variable de regroupement comme premier élément du tuple. Rappelons-nous que notre premier élément est actuellement une map des champs id, repeat et interval :

main.nf
1
2
3
4
5
{
  "id": "sampleA",
  "repeat": "1",
  "interval": "chr1"
}

Nous pouvons réutiliser la méthode subMap de tout à l'heure pour isoler nos champs id et interval de la map. Comme avant, nous utiliserons l'opérateur map pour appliquer la méthode subMap au premier élément du tuple pour chaque échantillon.

main.nf
    ch_combined_samples = ch_joined_samples
        .combine(ch_intervals)
        .map { grouping_key, normal, tumor, interval ->
            [
                grouping_key + [interval: interval],
                normal,
                tumor
            ]
        }

    ch_grouped_samples = ch_combined_samples
        .map { grouping_key, normal, tumor ->
            [
                grouping_key.subMap('id', 'interval'),
                normal,
                tumor
            ]
          }
          .view()
main.nf
    ch_combined_samples = ch_joined_samples
        .combine(ch_intervals)
        .map { grouping_key, normal, tumor, interval ->
            [
                grouping_key + [interval: interval],
                normal,
                tumor
            ]
        }
        .view()

Exécutons à nouveau et vérifions le contenu du canal :

nextflow run main.nf
Sortie de la commande
N E X T F L O W   ~  version 25.10.2

Launching `main.nf` [hopeful_brenner] DSL2 - revision: 7f4f7fea76

[[id:patientA, interval:chr1], patientA_rep1_normal.bam, patientA_rep1_tumor.bam]
[[id:patientA, interval:chr2], patientA_rep1_normal.bam, patientA_rep1_tumor.bam]
[[id:patientA, interval:chr3], patientA_rep1_normal.bam, patientA_rep1_tumor.bam]
[[id:patientA, interval:chr1], patientA_rep2_normal.bam, patientA_rep2_tumor.bam]
[[id:patientA, interval:chr2], patientA_rep2_normal.bam, patientA_rep2_tumor.bam]
[[id:patientA, interval:chr3], patientA_rep2_normal.bam, patientA_rep2_tumor.bam]
[[id:patientB, interval:chr1], patientB_rep1_normal.bam, patientB_rep1_tumor.bam]
[[id:patientB, interval:chr2], patientB_rep1_normal.bam, patientB_rep1_tumor.bam]
[[id:patientB, interval:chr3], patientB_rep1_normal.bam, patientB_rep1_tumor.bam]
[[id:patientC, interval:chr1], patientC_rep1_normal.bam, patientC_rep1_tumor.bam]
[[id:patientC, interval:chr2], patientC_rep1_normal.bam, patientC_rep1_tumor.bam]
[[id:patientC, interval:chr3], patientC_rep1_normal.bam, patientC_rep1_tumor.bam]

Nous pouvons voir que nous avons isolé avec succès les champs id et interval, mais que les échantillons ne sont pas encore regroupés.

Note

Nous supprimons ici le champ replicate. C'est parce que nous n'en avons pas besoin pour le traitement en aval. Après avoir terminé ce tutoriel, voyez si vous pouvez l'inclure sans affecter le regroupement ultérieur !

Regroupons maintenant les échantillons par ce nouvel élément de regroupement, en utilisant l'opérateur groupTuple.

main.nf
    ch_grouped_samples = ch_combined_samples
        .map { grouping_key, normal, tumor ->
            [
                grouping_key.subMap('id', 'interval'),
                normal,
                tumor
            ]
          }
          .groupTuple()
          .view()
main.nf
    ch_grouped_samples = ch_combined_samples
        .map { grouping_key, normal, tumor ->
            [
                grouping_key.subMap('id', 'interval'),
                normal,
                tumor
            ]
          }
          .view()

C'est tout ce qu'il y a à faire ! Nous avons simplement ajouté une seule ligne de code. Voyons ce qui se passe lorsque nous l'exécutons :

nextflow run main.nf
Sortie de la commande
N E X T F L O W   ~  version 25.10.2

Launching `main.nf` [friendly_jang] DSL2 - revision: a1bee1c55d

[[id:patientA, interval:chr1], [patientA_rep1_normal.bam, patientA_rep2_normal.bam], [patientA_rep1_tumor.bam, patientA_rep2_tumor.bam]]
[[id:patientA, interval:chr2], [patientA_rep1_normal.bam, patientA_rep2_normal.bam], [patientA_rep1_tumor.bam, patientA_rep2_tumor.bam]]
[[id:patientA, interval:chr3], [patientA_rep1_normal.bam, patientA_rep2_normal.bam], [patientA_rep1_tumor.bam, patientA_rep2_tumor.bam]]
[[id:patientB, interval:chr1], [patientB_rep1_normal.bam], [patientB_rep1_tumor.bam]]
[[id:patientB, interval:chr2], [patientB_rep1_normal.bam], [patientB_rep1_tumor.bam]]
[[id:patientB, interval:chr3], [patientB_rep1_normal.bam], [patientB_rep1_tumor.bam]]
[[id:patientC, interval:chr1], [patientC_rep1_normal.bam], [patientC_rep1_tumor.bam]]
[[id:patientC, interval:chr2], [patientC_rep1_normal.bam], [patientC_rep1_tumor.bam]]
[[id:patientC, interval:chr3], [patientC_rep1_normal.bam], [patientC_rep1_tumor.bam]]

Notez que notre structure de données a changé et que dans chaque élément du canal, les fichiers sont maintenant contenus dans des tuples comme [patientA_rep1_normal.bam, patientA_rep2_normal.bam]. C'est parce que lorsque nous utilisons groupTuple, Nextflow combine les fichiers individuels pour chaque échantillon d'un groupe. C'est important à retenir lorsque vous essayez de gérer les données en aval.

Note

transpose est l'opposé de groupTuple. Il décompresse les éléments d'un canal et les aplatit. Essayez d'ajouter transpose et d'annuler le regroupement que nous avons effectué ci-dessus !

À retenir

Dans cette section, vous avez appris :

  • Regrouper des échantillons liés : Comment utiliser groupTuple pour agréger des échantillons par attributs communs
  • Isoler les clés de regroupement : Comment utiliser subMap pour extraire des champs spécifiques pour le regroupement
  • Gérer les structures de données regroupées : Comment travailler avec la structure imbriquée créée par groupTuple
  • Gestion des réplicats techniques : Comment regrouper des échantillons qui partagent les mêmes conditions expérimentales

Résumé

Dans cette quête secondaire, vous avez appris à séparer et regrouper des données en utilisant des canaux.

En modifiant les données au fur et à mesure qu'elles circulent dans le pipeline, vous pouvez construire un pipeline évolutif sans utiliser de boucles ou d'instructions while, offrant plusieurs avantages par rapport aux approches plus traditionnelles :

  • Nous pouvons passer à autant ou aussi peu d'entrées que nous le souhaitons sans code supplémentaire
  • Nous nous concentrons sur la gestion du flux de données à travers le pipeline, plutôt que sur l'itération
  • Nous pouvons être aussi complexes ou simples que nécessaire
  • Le pipeline devient plus déclaratif, se concentrant sur ce qui doit se passer plutôt que sur comment cela doit se passer
  • Nextflow optimisera l'exécution pour nous en exécutant des opérations indépendantes en parallèle

Maîtriser ces opérations de canaux vous permettra de construire des pipelines flexibles et évolutifs qui gèrent des relations de données complexes sans recourir à des boucles ou à une programmation itérative, permettant à Nextflow d'optimiser l'exécution et de paralléliser automatiquement les opérations indépendantes.

Patterns clés

  1. Créer des données d'entrée structurées : En partant d'un fichier CSV avec des meta maps (en s'appuyant sur les patterns de Métadonnées dans les workflows)

    ch_samples = channel.fromPath("./data/samplesheet.csv")
        .splitCsv(header: true)
        .map{ row ->
          [[id:row.id, repeat:row.repeat, type:row.type], row.bam]
        }
    
  2. Séparer les données en canaux distincts : Nous avons utilisé filter pour diviser les données en flux indépendants sur la base du champ type

    channel.filter { it.type == 'tumor' }
    
  3. Joindre des échantillons correspondants : Nous avons utilisé join pour recombiner des échantillons liés sur la base des champs id et repeat

    • Joindre deux canaux par clé (premier élément du tuple)
    tumor_ch.join(normal_ch)
    
    • Extraire la clé de jointure et joindre par cette valeur
    tumor_ch.map { meta, file -> [meta.id, meta, file] }
        .join(
          normal_ch.map { meta, file -> [meta.id, meta, file] }
        )
    
    • Joindre sur plusieurs champs avec subMap
    tumor_ch.map { meta, file -> [meta.subMap(['id', 'repeat']), meta, file] }
        .join(
          normal_ch.map { meta, file -> [meta.subMap(['id', 'repeat']), meta, file] }
        )
    
  4. Distribuer sur des intervalles : Nous avons utilisé combine pour créer des produits cartésiens d'échantillons avec des intervalles génomiques pour le traitement parallèle.

    samples_ch.combine(intervals_ch)
    
  5. Agréger par clés de regroupement : Nous avons utilisé groupTuple pour regrouper par le premier élément de chaque tuple, collectant ainsi les échantillons partageant les champs id et interval et fusionnant les réplicats techniques.

    channel.groupTuple()
    
  6. Optimiser la structure des données : Nous avons utilisé subMap pour extraire des champs spécifiques et créé une closure nommée pour rendre les transformations réutilisables.

    • Extraire des champs spécifiques d'une map
    meta.subMap(['id', 'repeat'])
    
    • Utiliser une closure nommée pour des transformations réutilisables
    getSampleIdAndReplicate = { meta, file -> [meta.subMap(['id', 'repeat']), file] }
    channel.map(getSampleIdAndReplicate)
    

Ressources supplémentaires


Et ensuite ?

Retournez au menu des Quêtes secondaires ou cliquez sur le bouton en bas à droite de la page pour passer au sujet suivant dans la liste.