Drug Resistance Frequencies Across Pf7

Introduction

This notebook recreates main Table 2, Supplementary Table 4 from the Pf7 paper.

These tables summarise the number of samples associated with malarial drug resistance (DR) in different populations.

This notebook takes 1 minute to run.

Setup

Install and import the malariagen Python package:

!pip install -q --no-warn-conflicts malariagen_data
import malariagen_data
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 148.5/148.5 kB 3.0 MB/s eta 0:00:00
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3.1/3.1 MB 11.5 MB/s eta 0:00:00
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 52.8/52.8 MB 12.5 MB/s eta 0:00:00
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 10.2/10.2 MB 52.3 MB/s eta 0:00:00
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.0/4.0 MB 45.8 MB/s eta 0:00:00
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 302.5/302.5 kB 21.3 MB/s eta 0:00:00
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 138.7/138.7 kB 13.6 MB/s eta 0:00:00
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 20.9/20.9 MB 14.4 MB/s eta 0:00:00
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 8.1/8.1 MB 24.8 MB/s eta 0:00:00
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 206.9/206.9 kB 12.7 MB/s eta 0:00:00
?25h  Preparing metadata (setup.py) ... ?25l?25hdone
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 7.7/7.7 MB 86.1 MB/s eta 0:00:00
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.6/1.6 MB 68.3 MB/s eta 0:00:00
?25h  Building wheel for asciitree (setup.py) ... ?25l?25hdone

Import required python libraries that are installed at colab by default.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import collections
import re
import scipy

Access Pf7 Data

We use the malariagen data package to load the release data.

release_data = malariagen_data.Pf7()
sample_metadata = release_data.sample_metadata()
# take a glance at the metadata dataframe
sample_metadata.head(3)
Sample Study Country Admin level 1 Country latitude Country longitude Admin level 1 latitude Admin level 1 longitude Year ENA All samples same case Population % callable QC pass Exclusion reason Sample type Sample was in Pf6
0 FP0008-C 1147-PF-MR-CONWAY Mauritania Hodh el Gharbi 20.265149 -10.337093 16.565426 -9.832345 2014.0 ERR1081237 FP0008-C AF-W 82.16 True Analysis_set gDNA True
1 FP0009-C 1147-PF-MR-CONWAY Mauritania Hodh el Gharbi 20.265149 -10.337093 16.565426 -9.832345 2014.0 ERR1081238 FP0009-C AF-W 88.85 True Analysis_set gDNA True
2 FP0010-CW 1147-PF-MR-CONWAY Mauritania Hodh el Gharbi 20.265149 -10.337093 16.565426 -9.832345 2014.0 ERR2889621 FP0010-CW AF-W 86.46 True Analysis_set sWGA False

We will only examine the QC pass samples in the analysis of these notebooks.

# Retain QC pass samples
qc_sample_metadata = sample_metadata.loc[sample_metadata['QC pass']]

Access DR Classification and Genotype Data

We can access inferred resistance status classifications of QC-pass Pf7 samples from the MalariaGEN web resources.

This dataset includes the samples that are resistant to 10 drugs or combinations of drugs and to rapid diagnostic tests (RDT) detection: chloroquine, pyrimethamine, sulfadoxine, mefloquine, artemisinin, piperaquine, sulfadoxine- pyrimethamine for treatment of uncomplicated malaria, sulfadoxine- pyrimethamine for intermittent preventive treatment in pregnancy, artesunate-mefloquine, dihydroartemisinin-piperaquine, hrp2 and hrp3 gene deletions.

# Read the data from the MalariaGEN.net resources website
resistance_classification_fn = pd.read_csv('https://www.malariagen.net/wp-content/uploads/2023/11/Pf7_inferred_resistance_status_classification.txt', sep='\t')

# Print the first rows
resistance_classification_fn.head()
Sample Chloroquine Pyrimethamine Sulfadoxine Mefloquine Artemisinin Piperaquine SP (uncomplicated) SP (IPTp) AS-MQ DHA-PPQ HRP2 HRP3 HRP2 and HRP3
0 FP0008-C Undetermined Undetermined Undetermined Sensitive Sensitive Sensitive Sensitive Sensitive Sensitive Sensitive nodel nodel nodel
1 FP0009-C Resistant Resistant Sensitive Sensitive Sensitive Sensitive Resistant Sensitive Sensitive Sensitive nodel nodel nodel
2 FP0010-CW Undetermined Resistant Resistant Sensitive Sensitive Sensitive Resistant Sensitive Sensitive Sensitive uncallable uncallable uncallable
3 FP0011-CW Undetermined Resistant Undetermined Sensitive Undetermined Sensitive Resistant Sensitive Sensitive Sensitive uncallable uncallable uncallable
4 FP0012-CW Resistant Resistant Sensitive Sensitive Sensitive Sensitive Resistant Sensitive Sensitive Sensitive nodel nodel nodel

We will also use the genotypes utilised in drug resistance status classification containing amino acid and copy number genotypes at six loci: crt, dhfr, dhps, mdr1, kelch13, plasmepsin 2-3. This dataset is also available witin the MalariaGEN web resources.

# Read the data
drm_calls_fn = pd.read_csv('https://www.malariagen.net/wp-content/uploads/2023/11/Pf7_drug_resistance_marker_genotypes.txt', sep='\t', )

# Rename the first column as 'Sample'
drm_calls_fn = drm_calls_fn.rename(columns={drm_calls_fn.columns[0]: 'Sample'})

# Print the first rows
drm_calls_fn.head()
Sample crt_76[K] crt_72-76[CVMNK] dhfr_51[N] dhfr_59[C] dhfr_108[S] dhfr_164[I] dhps_437[G] dhps_540[K] dhps_581[A] dhps_613[A] kelch13_349-726_ns_changes mdr1_dup_call mdr1_breakpoint pm2_dup_call pm2_breakpoint
0 FP0008-C T,K CVIET,CVMNK N C S,N I A,G K A A NaN 0 NaN 0 NaN
1 FP0009-C T CVIET I R N I A K A A NaN 0 NaN 0 NaN
2 FP0010-CW T,K CVIET,CVMNK I R N I G K A A NaN 0 NaN 0 NaN
3 FP0011-CW T,K CVIET,CVMNK I R N I G,A K A A - 0 NaN 0 NaN
4 FP0012-CW T CVIET I R N I A K A A NaN 0 NaN 0 NaN

Load all data into single DataFrame

We can merge these 3 datasets (metadata, drug resistance genotype and classification) to facilitate streamlined analysis.

# Merge the dataframes on "Sample" column
df_all_sample_metadata = pd.merge(pd.merge(qc_sample_metadata,drm_calls_fn,on='Sample'),resistance_classification_fn,on='Sample')

# Print first 3 rows
df_all_sample_metadata.head(3)
Sample Study Country Admin level 1 Country latitude Country longitude Admin level 1 latitude Admin level 1 longitude Year ENA ... Mefloquine Artemisinin Piperaquine SP (uncomplicated) SP (IPTp) AS-MQ DHA-PPQ HRP2 HRP3 HRP2 and HRP3
0 FP0008-C 1147-PF-MR-CONWAY Mauritania Hodh el Gharbi 20.265149 -10.337093 16.565426 -9.832345 2014.0 ERR1081237 ... Sensitive Sensitive Sensitive Sensitive Sensitive Sensitive Sensitive nodel nodel nodel
1 FP0009-C 1147-PF-MR-CONWAY Mauritania Hodh el Gharbi 20.265149 -10.337093 16.565426 -9.832345 2014.0 ERR1081238 ... Sensitive Sensitive Sensitive Resistant Sensitive Sensitive Sensitive nodel nodel nodel
2 FP0010-CW 1147-PF-MR-CONWAY Mauritania Hodh el Gharbi 20.265149 -10.337093 16.565426 -9.832345 2014.0 ERR2889621 ... Sensitive Sensitive Sensitive Resistant Sensitive Sensitive Sensitive uncallable uncallable uncallable

3 rows × 45 columns

Sulfadoxine-Pyrimethamine (SP) is used for the treatment of uncomplicated cases and Intermittent Preventive Treatment in Pregnancy (IPTp). To make these two status clear in the dataset, we rename the columns.

# Define a dictionary of old&new column names
column_name_changes = collections.OrderedDict()
column_name_changes['SP (uncomplicated)'] = 'SP (treatment)'
column_name_changes['SP-IPTp'] = 'SP (IPTp)'

# Rename the columns using the dictionary
df_all_sample_metadata.rename(columns=column_name_changes, inplace=True)

Table setups

We proceed by defining a dictionary where each key represents a drug along with its associated gene, mutation, column name in the dataset, and sensitivity/resistance indicators.

For details on the heuristics employed to map genetic markers to resistance status classification, please refer to here.

drm_dict = collections.OrderedDict()

drm_dict['Chloroquine'] = {
    'gene': 'crt',
    'mutation': '76T',
    'column_name': 'crt_76[K]',
    'sensitive': 'K',
    'resistant': 'T',
}

drm_dict['Pyrimethamine'] = {
    'gene': 'dhfr',
    'mutation': '108N',
    'column_name': 'dhfr_108[S]',
    'sensitive': 'S',
    'resistant': 'N',
}

drm_dict['Sulfadoxine'] = {
    'gene': 'dhps',
    'mutation': '437G',
    'column_name': 'dhps_437[G]',
    'sensitive': 'A',
    'resistant': 'G',
}

drm_dict['Mefloquine'] = {
    'gene': 'mdr1',
    'mutation': '2+ copies',
    'column_name': 'dup_mdr1',
    'sensitive': 0,
    'resistant': 1,
}

drm_dict['Artemisinin'] = {
    'gene': 'kelch13',
    'mutation': 'WHO list',
    'column_name': None,
}

drm_dict['Piperaquine'] = {
    'gene': 'plasmepsin 2-3',
    'mutation': '2+ copies',
    'column_name': 'dup_pm2',
    'sensitive': 0,
    'resistant': 1,
}

drm_dict['SP (treatment)'] = {
    'gene': 'dhfr',
    'mutation': 'triple mutant',
    'column_name': None
}

drm_dict['SP (IPTp)'] = {
    'gene': 'dhfr and dhps',
    'mutation': 'sextuple mutant',
    'column_name': None
}

drm_dict['AS-MQ'] = {
    'gene': 'kelch13 and mdr1',
    'mutation': '',
    'column_name': None
}

drm_dict['DHA-PPQ'] = {
    'gene': 'kelch13 and plasmepsin 2-3',
    'mutation': '',
    'column_name': None
}

To elaborate on population names in the final table, we define a dictionary listing ten populations in Pf7 with the following abbreviations:

populations = collections.OrderedDict()
populations['SA']       = "South America"
populations['AF-W']     = "Africa - West"
populations['AF-C']     = "Africa - Central"
populations['AF-NE']    = "Africa - Northeast"
populations['AF-E']     = "Africa - East"
populations['AS-S-E']  = "Asia - South - East"
populations['AS-S-FE']  = "Asia - South - Far East"
populations['AS-SE-W'] = "Asia - Southeast - West"
populations['AS-SE-E'] = "Asia - Southeast - East"
populations['OC-NG']    = "Oceania - New Guinea"

Summary of drug resistance sample sizes across populations

We want to create a table summarizing the number of samples by population.

To do this, we will write a function that counts number of samples that are resistant and sensitive for a given drug.

def n_agg(x):
    """
    Aggregate function to calculate the count of non-undetermined samples for each drug in a given population.

    Parameters:
    - x: DataFrame

    Returns:
    - pd.Series: Counts of sensitive samples that are resistant to each drug.
    """

    # Initialize an ordered dictionary to store drug names and their corresponding counts
    names = collections.OrderedDict()

    # Iterate through each drug in drm_dict
    for drug in drm_dict:
        # Count the number of non-undetermined samples for the current drug
        n = np.count_nonzero((x[drug] != 'Undetermined'))

        # Store the drug name and its count in the dictionary
        names[drug] = n

    # Return the result as a pandas Series
    return pd.Series(names)

As an addition to table, we would like to include minimum and maximum drug resistance sample size in the population names.

# Calculate counts of non-undetermined samples for each drug in each population
df_drm_n_table = (
    df_all_sample_metadata
    .groupby('Population')
    .apply(n_agg)
    .rename_axis(None)
    .transpose()
    .loc[:, populations.keys()]
    .reset_index()
)

# Calculate the minimum and maximum sample sizes across populations
min_n = df_drm_n_table.min()
max_n = df_drm_n_table.max()

# Customize the index to include gene and mutation information for each drug
df_drm_n_table.index = ["%s %s" % (drm_dict[drug]['gene'], drm_dict[drug]['mutation']) for drug in drm_dict]
df_drm_n_table.index.names = ['Marker']

# Rename the columns to indicate their association with drug resistance and sample counts
df_drm_n_table.rename(columns={'index': 'Associated with resistance to'}, inplace=True)

# Update column names to include population names and corresponding sample size ranges
for population in populations:
    new_column_name = f'{populations[population]} (n={min_n[population]}-{max_n[population]})'
    df_drm_n_table.rename(columns={population: new_column_name}, inplace=True)

# Display the final drug resistance count table
df_drm_n_table
Associated with resistance to South America (n=154-158) Africa - West (n=5234-6233) Africa - Central (n=397-520) Africa - Northeast (n=120-170) Africa - East (n=1373-1532) Asia - South - East (n=164-233) Asia - South - Far East (n=1212-1369) Asia - Southeast - West (n=1657-1876) Asia - Southeast - East (n=2059-3684) Oceania - New Guinea (n=298-341)
Marker
crt 76T Chloroquine 155 5660 397 157 1388 217 1326 1871 3665 333
dhfr 108N Pyrimethamine 154 5589 517 170 1476 201 1369 1876 3684 333
dhps 437G Sulfadoxine 154 5529 501 162 1424 220 1291 1875 3609 333
mdr1 2+ copies Mefloquine 158 5515 478 123 1509 164 1268 1782 3461 314
kelch13 WHO list Artemisinin 158 5595 505 144 1513 189 1341 1768 3475 305
plasmepsin 2-3 2+ copies Piperaquine 158 5464 519 120 1512 172 1272 1790 3428 298
dhfr triple mutant SP (treatment) 158 5234 440 167 1373 230 1212 1833 3619 339
dhfr and dhps sextuple mutant SP (IPTp) 158 6233 510 170 1446 233 1217 1657 2059 341
kelch13 and mdr1 AS-MQ 158 5915 519 150 1532 203 1354 1798 3495 324
kelch13 and plasmepsin 2-3 DHA-PPQ 158 5823 520 148 1526 199 1355 1829 3442 316

Supplementary Table. Numbers of samples used to determine proportions in the main table.

To save the table:

df_drm_n_table.to_excel("DRM_table_sample_numbers.xlsx")

Summary of drug resistance frequencies across populations

Now, we would like to calculate drug resistance proportions in each population.

We can easily adjust the function to count number of samples that are resistant and sensitive for a given drug, and calculate the proportion.

def proportion_agg(x):
    """
    Aggregate function to calculate the proportion of resistance for each drug in a given population.

    Parameters:
    - x: DataFrame

    Returns:
    - pd.Series: Proportions of resistance for each drug.
    """

    # Initialize an ordered dictionary to store drug names and their corresponding proportions
    names = collections.OrderedDict()

    # Iterate through each drug in drm_dict
    for drug in drm_dict:
        # Count the number of non-undetermined samples for the current drug
        n = np.count_nonzero((x[drug] != 'Undetermined'))

        # Check if there are no non-undetermined samples for the drug
        if n == 0:
            proportion = np.nan  # Set proportion to NaN to avoid division by zero
        else:
            # Calculate the proportion of resistant samples for the drug
            proportion = round(np.count_nonzero(
                (x[drug] == 'Resistant')
            ) / np.count_nonzero(
                (x[drug] != 'Undetermined')
            )*100)

        # Store the drug name and its proportion in the dictionary
        names[drug] = proportion

    # Return the result as a pandas Series
    return pd.Series(names)

Let’s apply this function and create a table summarizing drug resistance proportions by population.

# Create a table summarizing drug resistance proportions by population

# Group the DataFrame by 'Population' and apply the 'proportion_agg' function to calculate resistance proportions
df_drm_table = (
    df_all_sample_metadata
    .groupby('Population')
    .apply(proportion_agg)
    .rename_axis(None)
    .transpose()
    .loc[:, populations.keys()]
    .reset_index()
)

# Customize the index to include gene and mutation information for each drug
df_drm_table.index = ["%s %s" % (drm_dict[drug]['gene'], drm_dict[drug]['mutation']) for drug in drm_dict]
df_drm_table.index.names = ['Marker']

# Rename the columns to indicate their association with drug resistance
df_drm_table.rename(columns={'index': 'Associated with resistance to'}, inplace=True)

# Update column names to include population names and corresponding sample size ranges
for population in populations:
    new_column_name = f'{populations[population]} (n={min_n[population]}-{max_n[population]})'
    df_drm_table.rename(columns={population: new_column_name}, inplace=True)

# Add '%' symbol to all values in the table
df_drm_table = df_drm_table.applymap(lambda x: f"{int(x)}%" if isinstance(x, (int, float)) else x)

# Display the final drug resistance table
df_drm_table
Associated with resistance to South America (n=154-158) Africa - West (n=5234-6233) Africa - Central (n=397-520) Africa - Northeast (n=120-170) Africa - East (n=1373-1532) Asia - South - East (n=164-233) Asia - South - Far East (n=1212-1369) Asia - Southeast - West (n=1657-1876) Asia - Southeast - East (n=2059-3684) Oceania - New Guinea (n=298-341)
Marker
crt 76T Chloroquine 100% 29% 61% 40% 24% 31% 94% 99% 95% 96%
dhfr 108N Pyrimethamine 64% 87% 100% 98% 96% 64% 100% 100% 99% 99%
dhps 437G Sulfadoxine 60% 78% 97% 82% 83% 8% 89% 100% 83% 69%
mdr1 2+ copies Mefloquine 0% 0% 0% 0% 0% 0% 0% 29% 5% 1%
kelch13 WHO list Artemisinin 0% 0% 0% 0% 0% 0% 0% 36% 58% 1%
plasmepsin 2-3 2+ copies Piperaquine 0% 0% 0% 0% 0% 0% 0% 0% 37% 0%
dhfr triple mutant SP (treatment) 0% 77% 85% 61% 80% 1% 46% 86% 88% 0%
dhfr and dhps sextuple mutant SP (IPTp) 0% 0% 2% 2% 9% 0% 13% 79% 14% 0%
kelch13 and mdr1 AS-MQ 0% 0% 0% 0% 0% 0% 0% 10% 4% 0%
kelch13 and plasmepsin 2-3 DHA-PPQ 0% 0% 0% 0% 0% 0% 0% 0% 34% 0%

Table Legend. Frequency of different sets of polymorphisms associated with drug resistance in samples from different geographical regions. All samples were classified into different types of drug resistance based on published genetic markers, and represent best attempt based on the available data. Each type of resistance was considered to be either present, absent or unknown for a given sample. For each resistance type, the table reports: the genetic markers considered; the drug they are associated with; the proportion of samples in each major sub-population classified as resistant out of the samples where the type was not unknown. The number of samples classified as either resistant or not resistant varies for each type of resistance considered (e.g. due to different levels of genomic accessibility); numbers in brackets report the minimum and maximum number analysed while the exact numbers considered are reported in Supplementary Table 4. SP: sulfadoxine-pyrimethamine; treatment: SP used for the clinical treatment of uncomplicated malaria; IPTp: SP used for intermittent preventive treatment in pregnancy; AS-MQ: artesunate + mefloquine combination therapy; DHA-PPQ: dihydroartemisinin + piperaquine combination therapy. dhfr triple mutant refers to having all three of 51I, 59R and 108N in dhfr. dhfr and dhps sextuple mutant refers to having all five of 51I, 59R and 108N in dhfr and 437G and 540E in dhps, plus one of dhfr:164L, dhps:581G, dhps:613S or dhps:613T. Full details of the rules used to infer resistance status from genetic markers can be found on the resource page at https://www.malariagen.net/resource/34.

To save the table:

df_drm_table.to_excel("Main_DRM_table.xlsx")

Summary of drug resistance frequencies between time periods across populations

We are interested to compare the drug resistance between periods of 2001-2014 and 2015-2019.

To compare drug resistance frequencies between 2001-2014 and 2015-2019, a new column (Year group) has been added to categorize samples accordingly.

# Create a new column
# Catagorize samples collected in 2001-2014
df_all_sample_metadata.loc[( df_all_sample_metadata['Year'] >= 2001 ) & ( df_all_sample_metadata['Year'] <= 2014 ), 'Year group'] = "2001-2014"

# Catagorize samples collected in 2015-2019
df_all_sample_metadata.loc[( df_all_sample_metadata['Year'] >= 2015 ) & ( df_all_sample_metadata['Year'] <= 2019 ), 'Year group'] = "2015-2019"

# Convert 'Year' column into integer format
df_all_sample_metadata['Year'] = pd.to_numeric(df_all_sample_metadata['Year'], downcast = 'integer')

Next, we adjust the function to count samples within each group. As an addition, the resulting table will provide the proportions and counts for both year groups combined in each cell value.

def my_agg(x):
    """
    Aggregate function to calculate proportions of resistance and counts for each drug in two year groups.

    Parameters:
    - x: DataFrame

    Returns:
    - pd.Series: Series containing proportions and counts of resistance for each drug in two year groups.
    """

    # Initialize an ordered dictionary to store drug names and their corresponding proportions and counts
    names = collections.OrderedDict()

    # Iterate through each drug in drm_dict
    for drug in drm_dict:
        # Calculate counts and proportions for Year Group 1 (2001-2014)
        n_year_group_1 = np.count_nonzero((x[drug] != 'Undetermined') & (x['Year group'] == '2001-2014'))
        proportion_year_group_1 = (
            np.count_nonzero((x[drug] == 'Resistant') & (x['Year group'] == '2001-2014')) /
            np.count_nonzero((x[drug] != 'Undetermined') & (x['Year group'] == '2001-2014'))
        ) if n_year_group_1 != 0 else np.nan

        # Calculate counts and proportions for Year Group 2 (2015-2019)
        n_year_group_2 = np.count_nonzero((x[drug] != 'Undetermined') & (x['Year group'] == '2015-2019'))
        proportion_year_group_2 = (
            np.count_nonzero((x[drug] == 'Resistant') & (x['Year group'] == '2015-2019')) /
            np.count_nonzero((x[drug] != 'Undetermined') & (x['Year group'] == '2015-2019'))
        ) if n_year_group_2 != 0 else np.nan

        # Store drug name, proportions, and counts in the dictionary
        names[drug] = f"{proportion_year_group_1:.2f} (n={n_year_group_1})   {proportion_year_group_2:.2f} (n={n_year_group_2})"

    # Return the result as a pandas Series
    return pd.Series(names)

Let’s create the table in a similar way to before.

# Create a table summarizing proportions and counts of resistance for each drug in two year groups by population

df_drm_table_with_n = (
    df_all_sample_metadata
    .groupby('Population')
    .apply(my_agg)
    .rename_axis(None)
    .transpose()
    .loc[:, populations.keys()]
    .reset_index()
)

# Customize the index to include gene and mutation information for each drug
df_drm_table_with_n.index = ["%s %s" % (drm_dict[drug]['gene'], drm_dict[drug]['mutation']) for drug in drm_dict]
df_drm_table_with_n.index.names = ['Marker']

# Rename the columns to indicate their association with drug resistance and sample counts
df_drm_table_with_n.rename(columns={'index': 'Associated with resistance to'}, inplace=True)

# Update column names to include population names
for population in populations:
    new_column_name = populations[population]
    df_drm_table_with_n.rename(columns={population: new_column_name}, inplace=True)

# Display the final drug resistance table with sample counts
df_drm_table_with_n
Associated with resistance to South America Africa - West Africa - Central Africa - Northeast Africa - East Asia - South - East Asia - South - Far East Asia - Southeast - West Asia - Southeast - East Oceania - New Guinea
Marker
crt 76T Chloroquine 1.00 (n=44) 1.00 (n=111) 0.42 (n=2500) 0.19 (n=3008) 0.65 (n=292) 0.52 (n=105) 0.19 (n=78) 0.61 (n=79) 0.18 (n=1244) 0.05 (n=19) nan (n=0) 0.31 (n=217) 0.93 (n=74) 0.94 (n=1252) 1.00 (n=1158) 0.99 (n=713) 0.94 (n=1427) 0.95 (n=2232) 0.99 (n=195) 0.91 (n=138)
dhfr 108N Pyrimethamine 0.93 (n=44) 0.53 (n=110) 0.85 (n=2547) 0.92 (n=2902) 1.00 (n=383) 0.99 (n=134) 0.99 (n=86) 0.96 (n=84) 0.99 (n=1346) 1.00 (n=19) nan (n=0) 0.64 (n=201) 1.00 (n=77) 1.00 (n=1292) 1.00 (n=1160) 1.00 (n=716) 0.99 (n=1438) 0.99 (n=2240) 0.99 (n=200) 0.98 (n=133)
dhps 437G Sulfadoxine 0.35 (n=43) 0.69 (n=111) 0.75 (n=2513) 0.84 (n=2866) 0.97 (n=371) 0.96 (n=130) 0.99 (n=85) 0.64 (n=77) 0.92 (n=1265) 1.00 (n=19) nan (n=0) 0.08 (n=220) 0.97 (n=63) 0.89 (n=1228) 1.00 (n=1160) 0.99 (n=715) 0.88 (n=1377) 0.81 (n=2226) 0.61 (n=197) 0.81 (n=136)
mdr1 2+ copies Mefloquine 0.00 (n=44) 0.00 (n=114) 0.00 (n=2735) 0.00 (n=2644) 0.00 (n=349) 0.00 (n=129) 0.00 (n=86) 0.00 (n=37) 0.00 (n=1339) 0.00 (n=14) nan (n=0) 0.00 (n=164) 0.00 (n=77) 0.00 (n=1191) 0.43 (n=1128) 0.06 (n=654) 0.12 (n=1334) 0.01 (n=2122) 0.01 (n=198) 0.02 (n=116)
kelch13 WHO list Artemisinin 0.00 (n=44) 0.00 (n=114) 0.00 (n=2781) 0.00 (n=2668) 0.00 (n=374) 0.00 (n=131) 0.00 (n=84) 0.00 (n=60) 0.00 (n=1341) 0.00 (n=20) nan (n=0) 0.00 (n=189) 0.00 (n=78) 0.00 (n=1263) 0.28 (n=1103) 0.49 (n=665) 0.48 (n=1290) 0.64 (n=2179) 0.00 (n=199) 0.02 (n=106)
plasmepsin 2-3 2+ copies Piperaquine 0.00 (n=44) 0.00 (n=114) 0.00 (n=2769) 0.00 (n=2558) 0.00 (n=385) 0.00 (n=134) 0.00 (n=86) 0.00 (n=34) 0.00 (n=1337) 0.00 (n=19) nan (n=0) 0.00 (n=172) 0.00 (n=78) 0.00 (n=1194) 0.00 (n=1139) 0.00 (n=651) 0.19 (n=1296) 0.49 (n=2128) 0.00 (n=201) 0.00 (n=97)
dhfr triple mutant SP (treatment) 0.00 (n=44) 0.00 (n=114) 0.76 (n=2425) 0.83 (n=2657) 0.83 (n=319) 0.89 (n=121) 0.95 (n=82) 0.28 (n=85) 0.87 (n=1235) 1.00 (n=18) nan (n=0) 0.01 (n=230) 0.43 (n=65) 0.46 (n=1147) 0.89 (n=1123) 0.81 (n=710) 0.91 (n=1390) 0.86 (n=2223) 0.00 (n=201) 0.00 (n=138)
dhfr and dhps sextuple mutant SP (IPTp) 0.00 (n=44) 0.00 (n=114) 0.00 (n=2898) 0.00 (n=3183) 0.01 (n=379) 0.02 (n=131) 0.00 (n=84) 0.05 (n=86) 0.10 (n=1271) 0.00 (n=19) nan (n=0) 0.00 (n=233) 0.19 (n=68) 0.13 (n=1149) 0.81 (n=981) 0.78 (n=676) 0.18 (n=1037) 0.09 (n=1016) 0.00 (n=201) 0.00 (n=140)
kelch13 and mdr1 AS-MQ 0.00 (n=44) 0.00 (n=114) 0.00 (n=2848) 0.00 (n=2919) 0.00 (n=385) 0.00 (n=134) 0.00 (n=86) 0.00 (n=64) 0.00 (n=1356) 0.00 (n=20) nan (n=0) 0.00 (n=203) 0.00 (n=78) 0.00 (n=1276) 0.15 (n=1121) 0.03 (n=677) 0.09 (n=1338) 0.01 (n=2151) 0.00 (n=201) 0.00 (n=123)
kelch13 and plasmepsin 2-3 DHA-PPQ 0.00 (n=44) 0.00 (n=114) 0.00 (n=2839) 0.00 (n=2836) 0.00 (n=386) 0.00 (n=134) 0.00 (n=86) 0.00 (n=62) 0.00 (n=1350) 0.00 (n=20) nan (n=0) 0.00 (n=199) 0.00 (n=78) 0.00 (n=1277) 0.00 (n=1150) 0.00 (n=679) 0.16 (n=1302) 0.45 (n=2134) 0.00 (n=201) 0.00 (n=115)

Save table as:

df_drm_table_with_n.to_excel("DRM_before_after_table.xlsx")

CRT 72-76 haplotypes table

The final table will feature crt mutations in codons 72-76 which were associated with a particular resistance profile, with the crt 76T allele a very reliable marker of chloroquine resistance.

Let’s look at the distribution of haplotypes with T and K alleles.

# Rename haplotype column for a better representation
df_all_sample_metadata['crt_72-76'] = df_all_sample_metadata['crt_72-76[CVMNK]']

# Group and count haplotypes
df_all_sample_metadata.groupby(['crt_76[K]', 'crt_72-76']).size()
crt_76[K]  crt_72-76   
-          -                 26
K          -                  3
           CVMNK           5784
           CVMNK,CVMKK        1
           CVMNK,YVMNK        1
K,P        !,CVIEP            1
K,T        CVIET,CVMNK        7
           CVMNK,CVIDT        9
           CVMNK,CVIDT*       1
           CVMNK,CVIET      410
           CVMNK,CVIET*       5
           CVMNK,CVMNT        1
           CVMNK,SVMNT*       4
T          -                  2
           CIIET,CVIET        1
           CVIDT            535
           CVIDT,!            1
           CVIDT,CVIET       33
           CVIDT,CVIET*      16
           CVIET           8252
           CVIET,!            1
           CVIET,CIIET        1
           CVIET,CVI*         2
           CVIET,CVIDT       33
           CVIET,CVIDT*      15
           CVIET,CVMNT        1
           CVIET,SVMNT*       1
           CVIET,YVIET        3
           CVMET            105
           CVMET,CVMNT        2
           CVMNT             33
           CVMNT,CVMET        2
           SVMNT            337
           SVMNT,CVMET*       1
           SVMNT,CVMNT        1
           YVIET              2
T,K        CVIDK,CVIET        1
           CVIDT,CVMNK        7
           CVIET,!            3
           CVIET,CVIDK        1
           CVIET,CVIEK        4
           CVIET,CVMNK      519
           CVIET,CVMNK*      17
           CVMNK,CVIET       12
           CVMNT,CVI*         1
           SVMNT,CVMNK*       4
T,Q*       CVIET,!*           1
dtype: int64

We can see that there are heterozygous and ambiguous calls which we will exclude from the frequency estimation. Only exception is 72-76T haplotypes that contains heterozygous and ambiguous calls, since 76T call is reliable marker and they represent a bigger group.

# Set a group name for 76T haplotypes that contains ambiguous characters
df_all_sample_metadata.loc[
    df_all_sample_metadata['crt_72-76'].str.contains(',|-|X|\?') & ( df_all_sample_metadata['crt_76[K]'] == 'T' ),
    'crt_72-76'
] = 'Other T'

# Exclude het calls and haplotypes that contains ambiguous characters
analysis_samples = (
    ~df_all_sample_metadata['crt_72-76'].astype(str).str.contains(',|-|X|\?'))

# Look at the counts of haplotypes with 76K and 76T alleles
df_all_sample_metadata.loc[analysis_samples].groupby(['crt_76[K]', 'crt_72-76']).size()
crt_76[K]  crt_72-76
K          CVMNK        5784
T          CVIDT         535
           CVIET        8252
           CVMET         105
           CVMNT          33
           Other T       116
           SVMNT         337
           YVIET           2
dtype: int64

Now we can find the distribution of haplotypes in the populations.

# Create a pivot table with haplotypes as index, populations as columns, and counts as values
df_crt = pd.pivot_table(
    df_all_sample_metadata.loc[analysis_samples],
    index='crt_72-76',
    columns='Population',
    values='Study',
    aggfunc=len,
    fill_value=0
).loc[:, populations.keys()]

df_crt
Population SA AF-W AF-C AF-NE AF-E AS-S-E AS-S-FE AS-SE-W AS-SE-E OC-NG
crt_72-76
CVIDT 0 0 0 0 0 0 0 0 535 0
CVIET 0 1622 244 63 329 60 1241 1858 2835 0
CVMET 105 0 0 0 0 0 0 0 0 0
CVMNK 0 4035 152 94 1059 150 83 10 187 14
CVMNT 33 0 0 0 0 0 0 0 0 0
Other T 5 0 0 0 0 0 2 3 105 1
SVMNT 12 0 0 0 0 7 0 0 0 318
YVIET 0 0 0 0 0 0 0 0 2 0

As a last step we want to sort the order of the haplotypes. The wild type haplotype will be shown first, followed by other haplotypes sorted by penalty score and then by total number of haplotypes in descending order.

# Add a row_total column representing the total count for each haplotype
df_crt['row_total'] = df_crt.sum(1)

# Sort the table based on penalty score
df_crt['hamming_dist'] = [10 if x in ['Other T'] else scipy.spatial.distance.hamming(list('CVMNK'), list(str(x))) * len('CVMNK') for x in df_crt.index]
sort_order = df_crt.sort_values(['hamming_dist', 'row_total'], ascending=[True, False]).index

# Get the numbers of samples for each population
numbers_of_samples = df_crt.sum(0)

# Return proportions
df_crt_final = round(df_crt.loc[sort_order].divide(list(numbers_of_samples)),3)
df_crt_final
Population SA AF-W AF-C AF-NE AF-E AS-S-E AS-S-FE AS-SE-W AS-SE-E OC-NG row_total hamming_dist
crt_72-76
CVMNK 0.000 0.713 0.384 0.599 0.763 0.691 0.063 0.005 0.051 0.042 0.381 0.00
CVMNT 0.213 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.002 0.04
SVMNT 0.077 0.000 0.000 0.000 0.000 0.032 0.000 0.000 0.000 0.955 0.022 0.08
CVMET 0.677 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.007 0.08
CVIET 0.000 0.287 0.616 0.401 0.237 0.276 0.936 0.993 0.774 0.000 0.544 0.12
CVIDT 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.146 0.000 0.035 0.12
YVIET 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.001 0.000 0.000 0.16
Other T 0.032 0.000 0.000 0.000 0.000 0.000 0.002 0.002 0.029 0.003 0.008 0.40

We will add the population sizes to the column names and remove the columns used for sorting.

# Create final column names with population names and sample sizes
final_column_names = collections.OrderedDict()
for column_name in list(df_crt_final):
    final_column_names[column_name] = "%s (n=%d)" % (column_name, numbers_of_samples[column_name])

# Rename columns in the DataFrame
df_crt_final = df_crt_final.rename(columns=final_column_names)

# Drop row_total and hamming_dist columns
df_crt_final = df_crt_final.drop(['row_total (n=15164)', 'hamming_dist (n=25)'], axis=1)
df_crt_final
Population SA (n=155) AF-W (n=5657) AF-C (n=396) AF-NE (n=157) AF-E (n=1388) AS-S-E (n=217) AS-S-FE (n=1326) AS-SE-W (n=1871) AS-SE-E (n=3664) OC-NG (n=333)
crt_72-76
CVMNK 0.000 0.713 0.384 0.599 0.763 0.691 0.063 0.005 0.051 0.042
CVMNT 0.213 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000
SVMNT 0.077 0.000 0.000 0.000 0.000 0.032 0.000 0.000 0.000 0.955
CVMET 0.677 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000
CVIET 0.000 0.287 0.616 0.401 0.237 0.276 0.936 0.993 0.774 0.000
CVIDT 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.146 0.000
YVIET 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.001 0.000
Other T 0.032 0.000 0.000 0.000 0.000 0.000 0.002 0.002 0.029 0.003

Save the table as:

df_crt.to_excel("table_crt_haplotypes.xlsx")