Why this activity matters

In this activity, we will explore real clinical data from breast cancer patients in The Cancer Genome Atlas (TCGA).

The goal is not only to learn Python. The goal is to use Python to ask scientific questions:

  • What information is collected from cancer patients?
  • How do we summarize a large clinical dataset?
  • What kinds of missing or uncertain values appear in real biomedical data?
  • How can clinical features help us understand breast cancer subtypes?

Later in DREAM-High, we will connect this type of clinical information to gene expression data. That is where computational biology becomes especially powerful: we can ask how molecular patterns relate to patient and tumor characteristics.


Preliminaries

Create your own copy

Before editing, use:

File → Save As…

Save a copy with your name in the file name.

Example:

Breast_Cancer_Patient_Data_Diana_Python.Rmd

This gives you your own workbook while preserving the original.


About the data files

This activity uses:

  • brca_clin.csv — a simplified TCGA breast cancer clinical dataset
  • duct_cells.jpg — a schematic of breast duct changes
  • metastasis.jpg — a schematic of metastatic spread

The files should be located in the shared data directory.

# Import the Python libraries used in this activity.

import os
import pandas as pd
import matplotlib.pyplot as plt

# Location of the data files on the DREAM-High server.
data_dir = "/shared/dreamhigh/data"

# Make figures a little larger.
plt.rcParams["figure.figsize"] = (8,5)

Breast cancer biology: what are we studying?

Breast cancer often begins in the ducts or lobules of the breast. As cancer develops, cells can grow abnormally, fill spaces where they do not belong, and eventually invade nearby tissue.

The figure below shows a simplified progression from a normal duct to invasive ductal cancer.

## <Figure size 600x400 with 0 Axes>
## <matplotlib.image.AxesImage object at 0x7babaedb23f0>
## (-0.5, 1302.5, 459.5, -0.5)

Think before moving on:

What do you notice as the duct changes from normal tissue to invasive cancer?

Write one observation here:

Your answer: The normal tube has an empty space in the middle. As the cancer grows, that empty space fills up with bad cells. At first, the cells are in a neat ring. Later, the cells become messy, dark, and packed tightly together. In the last step, the bad cells break out of the tube. They start to spread into the areas outside of it.


Metastasis

A major danger of cancer is metastasis: cancer cells can leave the original tumor, move through the body, and grow in distant organs.

## <Figure size 600x400 with 0 Axes>
## <matplotlib.image.AxesImage object at 0x7babaed9d880>
## (-0.5, 1220.5, 778.5, -0.5)

Overview of the metastatic cascade in cancer progression.

Cancer metastasis is a multi-step process in which tumor cells spread from the primary tumor to distant organs.

    1. Tumor cells acquire invasive properties and detach from the primary tumor.
    1. Cancer cells migrate through surrounding tissue and invade nearby blood vessels.
    1. Tumor cells survive in the circulation and travel through the bloodstream.
    1. Circulating tumor cells exit blood vessels (extravasation) at distant sites.
    1. Cancer cells colonize distant organs and form metastatic tumors.

The figure also highlights important biological processes involved in metastasis, including:

  • Motility and invasion — movement through tissue and blood vessels
  • Plasticity — the ability of tumor cells to change cellular states
  • Modulation of the microenvironment — interactions between tumor cells and surrounding stromal or immune cells
  • Colonization — successful growth of tumor cells in distant organs such as lung, liver, bone, or brain

Metastasis is biologically complex and difficult to predict or treat.

In clinical cancer datasets, we often look for features that tell us something about tumor severity, spread, treatment, or patient outcome.


Load the clinical data

We will use a CSV file. CSV stands for comma-separated values. It is a plain-text table format that can be opened in Python, R, Excel, or many other programs.

brca_clin_df = pd.read_csv(os.path.join(data_dir, "brca_clin.csv"))

The object name brca_clin_df is descriptive:

  • brca = breast cancer
  • clin = clinical data
  • df = data frame

A data frame is a table:

  • rows = patients
  • columns = clinical features

First look at the data

Dimensions

brca_clin_df.shape
## (1082, 27)

The first number is the number of rows. The second number is the number of columns.

n_patients = brca_clin_df.shape[0]
n_features = brca_clin_df.shape[1]

print("This dataset contains", n_patients, "patients and", n_features, "clinical features.")
## This dataset contains 1082 patients and 27 clinical features.

Column names

brca_clin_df.columns.tolist()
## ['bcr_patient_barcode', 'gender', 'race', 'ethnicity', 'age_at_diagnosis', 'year_of_initial_pathologic_diagnosis', 'vital_status', 'menopause_status', 'tumor_status', 'margin_status', 'days_to_last_followup', 'prior_dx', 'new_tumor_event_after_initial_treatment', 'radiation_therapy', 'histological_type', 'pathologic_T', 'pathologic_M', 'pathologic_N', 'pathologic_stage_sub', 'pathologic_stage', 'lymph_node_examined_count', 'number_of_lymphnodes_positive', 'initial_diagnosis_method', 'surgical_procedure', 'estrogen_receptor_status', 'progesterone_receptor_status', 'her2_receptor_status']

Question: Which column names look familiar? Which ones are confusing?

Your answer: Columns such as “gender,” “race,” and “age_at_diagnosis” are straightforward demographic variables that most people easily recognize. Other terms such as “vital_status” or “surgical_procedure” are also fairly intuitive, especially if you are used to the terminology used around a clinical environment or an emergency department. On the other hand, several column names are much less clear. For example, “bcr_patient_barcode” sounds more like an inventory tracking number than a patient identifier. Additionally, variables like “pathologic_T” and “pathologic_M” look like abstract codes, making it difficult to understand what clinical details they actually measure without a reference guide.


First few rows

brca_clin_df.head()
##   bcr_patient_barcode  gender  ... progesterone_receptor_status her2_receptor_status
## 0        TCGA-3C-AAAU  FEMALE  ...                     Positive             Negative
## 1        TCGA-3C-AALI  FEMALE  ...                     Positive             Positive
## 2        TCGA-3C-AALJ  FEMALE  ...                     Positive        Indeterminate
## 3        TCGA-3C-AALK  FEMALE  ...                     Positive             Positive
## 4        TCGA-4H-AAAK  FEMALE  ...                     Positive            Equivocal
## 
## [5 rows x 27 columns]

For a larger view, you can run this interactively in the Console:

View(brca_clin_df)

Working with rows and columns

A single value

brca_clin_df.iloc[0, 0]
## 'TCGA-3C-AAAU'

The first number selects the row. The second number selects the column.

Important Python note: Python starts counting at 0. So row 0 is the first row, and column 0 is the first column.


A single patient

brca_clin_df.iloc[[0]].T
##                                                                                          0
## bcr_patient_barcode                                                           TCGA-3C-AAAU
## gender                                                                              FEMALE
## race                                                                                 WHITE
## ethnicity                                                           NOT HISPANIC OR LATINO
## age_at_diagnosis                                                                      55.0
## year_of_initial_pathologic_diagnosis                                                  2004
## vital_status                                                                         Alive
## menopause_status                         Pre (<6 months since LMP AND no prior bilatera...
## tumor_status                                                                    WITH TUMOR
## margin_status                                                                     Negative
## days_to_last_followup                                                                 4047
## prior_dx                                                                                No
## new_tumor_event_after_initial_treatment                                                 NO
## radiation_therapy                                                                       NO
## histological_type                                           Infiltrating Lobular Carcinoma
## pathologic_T                                                                            TX
## pathologic_M                                                                            MX
## pathologic_N                                                                            NX
## pathologic_stage_sub                                                               Stage X
## pathologic_stage                                                                         X
## lymph_node_examined_count                                                               13
## number_of_lymphnodes_positive                                                            4
## initial_diagnosis_method                                                   [Not Available]
## surgical_procedure                                             Modified Radical Mastectomy
## estrogen_receptor_status                                                          Positive
## progesterone_receptor_status                                                      Positive
## her2_receptor_status                                                              Negative

The .T transposes the row so it is easier to read.


A few patients and features

brca_clin_df.iloc[[0, 1, 2], 0:6]
##   bcr_patient_barcode  ... year_of_initial_pathologic_diagnosis
## 0        TCGA-3C-AAAU  ...                                 2004
## 1        TCGA-3C-AALI  ...                                 2003
## 2        TCGA-3C-AALJ  ...                                 2011
## 
## [3 rows x 6 columns]

Prediction question: What do you think this code will show?

brca_clin_df.loc[[9, 19, 29], ["gender", "age_at_diagnosis", "vital_status"]]
##     gender  age_at_diagnosis vital_status
## 9   FEMALE              56.0        Alive
## 19  FEMALE              40.0        Alive
## 29  FEMALE              34.0        Alive

Your answer: I think it will output a table showing information for only the 10th, 20th, and 30th patients in the dataset and it will show the three specific details for those individuals: their gender, their age at diagnosis, and their current vital status.


Real data are messy

Clinical data are not perfectly clean. Some values are missing, unknown, not evaluated, or not available.

Let us look for values that indicate missing or uncertain information.

missing_like_values = ["[Not Available]", "[Not Evaluated]", "[Unknown]", "Indeterminate", "[Discrepancy]", ""]

missing_like_count = []

for column in brca_clin_df.columns:
    count = brca_clin_df[column].isin(missing_like_values).sum() + brca_clin_df[column].isna().sum()
    missing_like_count.append(count)

missing_summary = pd.DataFrame({
    "feature": brca_clin_df.columns,
    "missing_or_uncertain_count": missing_like_count,
    "percent": [round(100 * count / len(brca_clin_df), 1) for count in missing_like_count]
})

missing_summary
##                                     feature  ...  percent
## 0                       bcr_patient_barcode  ...      0.0
## 1                                    gender  ...      0.0
## 2                                      race  ...      8.3
## 3                                 ethnicity  ...     15.6
## 4                          age_at_diagnosis  ...      0.1
## 5      year_of_initial_pathologic_diagnosis  ...      0.2
## 6                              vital_status  ...      0.0
## 7                          menopause_status  ...      8.0
## 8                              tumor_status  ...      3.3
## 9                             margin_status  ...      6.1
## 10                    days_to_last_followup  ...      9.5
## 11                                 prior_dx  ...      0.1
## 12  new_tumor_event_after_initial_treatment  ...     18.3
## 13                        radiation_therapy  ...      9.1
## 14                        histological_type  ...      0.1
## 15                             pathologic_T  ...      0.0
## 16                             pathologic_M  ...      0.0
## 17                             pathologic_N  ...      0.0
## 18                     pathologic_stage_sub  ...      0.5
## 19                         pathologic_stage  ...      0.0
## 20                lymph_node_examined_count  ...     11.2
## 21            number_of_lymphnodes_positive  ...     15.1
## 22                 initial_diagnosis_method  ...      8.6
## 23                       surgical_procedure  ...      5.1
## 24                 estrogen_receptor_status  ...      4.6
## 25             progesterone_receptor_status  ...      4.9
## 26                     her2_receptor_status  ...     17.6
## 
## [27 rows x 3 columns]

Visualize missing or uncertain values

ordered_missing = missing_summary.sort_values("missing_or_uncertain_count", ascending=False)

plt.figure(figsize=(10, 5))
plt.bar(ordered_missing["feature"], ordered_missing["missing_or_uncertain_count"])
plt.title("Missing or uncertain values by clinical feature")
plt.ylabel("Number of patients")
plt.xticks(rotation=90)
## ([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26], [Text(0, 0, 'new_tumor_event_after_initial_treatment'), Text(1, 0, 'her2_receptor_status'), Text(2, 0, 'ethnicity'), Text(3, 0, 'number_of_lymphnodes_positive'), Text(4, 0, 'lymph_node_examined_count'), Text(5, 0, 'days_to_last_followup'), Text(6, 0, 'radiation_therapy'), Text(7, 0, 'initial_diagnosis_method'), Text(8, 0, 'race'), Text(9, 0, 'menopause_status'), Text(10, 0, 'margin_status'), Text(11, 0, 'surgical_procedure'), Text(12, 0, 'progesterone_receptor_status'), Text(13, 0, 'estrogen_receptor_status'), Text(14, 0, 'tumor_status'), Text(15, 0, 'pathologic_stage_sub'), Text(16, 0, 'year_of_initial_pathologic_diagnosis'), Text(17, 0, 'age_at_diagnosis'), Text(18, 0, 'prior_dx'), Text(19, 0, 'histological_type'), Text(20, 0, 'pathologic_N'), Text(21, 0, 'pathologic_stage'), Text(22, 0, 'pathologic_M'), Text(23, 0, 'pathologic_T'), Text(24, 0, 'gender'), Text(25, 0, 'vital_status'), Text(26, 0, 'bcr_patient_barcode')])
plt.tight_layout()
plt.show()

Question: Which clinical features have the most missing or uncertain information?

Your answer: new_tumor_event_after_initial_treatment, ethnicity, number_of_lymphnodes_positive, lymph_node_examined_count, receptor_status (the 2nd column) This is an important lesson: real biomedical datasets are powerful, but they are also imperfect.

This is an important lesson: real biomedical datasets are powerful, but they are also imperfect.


Summarizing categorical variables

A categorical variable describes groups or labels.

Examples:

  • gender
  • race
  • vital status
  • estrogen receptor status
  • tumor histology

In pandas, the function .value_counts() counts how many times each value appears.


Gender

brca_clin_df["gender"].value_counts(dropna=False)
## gender
## FEMALE    1070
## MALE        12
## Name: count, dtype: int64

#Fixed this graph | no longer contains an overlapping on the female’s data

import matplotlib.pyplot as plt

# Count the number of patients in each gender category
gender_counts = brca_clin_df["gender"].value_counts()

# Create the bar plot
plt.figure(figsize=(4, 3))
plt.bar(
    gender_counts.index,
    gender_counts.values,
    width=0.5,
    color="lightgray",    # Gray bars like R
    edgecolor="black",    # Black outlines
    linewidth=1.2         # Thicker outlines
)

plt.title("Gender distribution in the TCGA breast cancer cohort")
plt.xlabel("Gender")
plt.ylabel("Number of patients")

plt.tight_layout()
plt.show()

Question: What does this tell us about the cohort?

Your answer: The majority of patients in this breast cancer dataset were female.


Race

brca_clin_df["race"].value_counts(dropna=False)
## race
## WHITE                               747
## BLACK OR AFRICAN AMERICAN           183
## [Not Available]                      87
## ASIAN                                61
## [Not Evaluated]                       3
## AMERICAN INDIAN OR ALASKA NATIVE      1
## Name: count, dtype: int64
race_counts = brca_clin_df["race"].value_counts(dropna=False)

race_counts.plot(kind="bar")
plt.title("Race distribution")
plt.ylabel("Number of patients")
plt.xlabel("Race")
plt.xticks(rotation=90)
## (array([0, 1, 2, 3, 4, 5]), [Text(0, 0, 'WHITE'), Text(1, 0, 'BLACK OR AFRICAN AMERICAN'), Text(2, 0, '[Not Available]'), Text(3, 0, 'ASIAN'), Text(4, 0, '[Not Evaluated]'), Text(5, 0, 'AMERICAN INDIAN OR ALASKA NATIVE')])
plt.tight_layout()
plt.show()

Discussion question: Why is it important to look at who is represented in a biomedical dataset?

Your answer: It is important to look at who is in the dataset because if the data is mostly from one group such as the White patients in this graph, our results might be biased. If we build medical treatments or computer models using this skewed data, they might not work as well for underrepresented groups. We have to recognize who is missing so we can make sure future healthcare discoveries actually work for everyone.


Summarizing numerical variables

A numerical variable contains numbers.

Example:

  • age at diagnosis

Age at diagnosis

brca_clin_df["age_at_diagnosis"].describe()
## count    1081.000000
## mean       58.394080
## std        13.206204
## min        26.000000
## 25%        49.000000
## 50%        58.000000
## 75%        67.000000
## max        90.000000
## Name: age_at_diagnosis, dtype: float64

Fixed the histrogram to ages (similar to the R version)

import matplotlib.pyplot as plt

plt.figure(figsize=(4, 3))

plt.hist(
    brca_clin_df["age_at_diagnosis"],
    bins=13,
    edgecolor="black",   # Black outlines
    linewidth=0.9        # Thicker outlines
)

plt.title("Age at diagnosis")
plt.xlabel("Age at diagnosis")
plt.ylabel("Number of patients")
plt.xticks([30, 40, 50, 60, 70, 80, 90])
## ([<matplotlib.axis.XTick object at 0x7babae2b8c80>, <matplotlib.axis.XTick object at 0x7babae3568a0>, <matplotlib.axis.XTick object at 0x7babae9de570>, <matplotlib.axis.XTick object at 0x7babaee8f0b0>, <matplotlib.axis.XTick object at 0x7babaeaff530>, <matplotlib.axis.XTick object at 0x7babaeafd220>, <matplotlib.axis.XTick object at 0x7babaeafcb00>], [Text(30, 0, '30'), Text(40, 0, '40'), Text(50, 0, '50'), Text(60, 0, '60'), Text(70, 0, '70'), Text(80, 0, '80'), Text(90, 0, '90')])
plt.tight_layout()
plt.show()

Question: Around what ages are most patients diagnosed in this dataset?

Your answer: 45-65 but it peaks at 60-65


Receptor status

Breast cancer tumors are often tested for receptors that can influence growth and treatment.

Important receptor features include:

  • estrogen receptor status (ER)
  • progesterone receptor status (PR)
  • HER2 receptor status

Estrogen receptor status

brca_clin_df["estrogen_receptor_status"].value_counts(dropna=False)
## estrogen_receptor_status
## Positive           796
## Negative           236
## [Not Evaluated]     48
## Indeterminate        2
## Name: count, dtype: int64
er_counts = brca_clin_df["estrogen_receptor_status"].value_counts(dropna=False)

er_counts.plot(kind="bar")
plt.title("Estrogen receptor status")
plt.ylabel("Number of patients")
plt.xlabel("ER status")
plt.xticks(rotation=90)
## (array([0, 1, 2, 3]), [Text(0, 0, 'Positive'), Text(1, 0, 'Negative'), Text(2, 0, '[Not Evaluated]'), Text(3, 0, 'Indeterminate')])
plt.tight_layout()
plt.show()


Progesterone receptor status

brca_clin_df["progesterone_receptor_status"].value_counts(dropna=False)
## progesterone_receptor_status
## Positive           689
## Negative           340
## [Not Evaluated]     49
## Indeterminate        4
## Name: count, dtype: int64

HER2 receptor status

brca_clin_df["her2_receptor_status"].value_counts(dropna=False)
## her2_receptor_status
## Negative           554
## Equivocal          177
## [Not Evaluated]    170
## Positive           161
## Indeterminate       12
## [Not Available]      8
## Name: count, dtype: int64

Combining conditions

A powerful part of data analysis is selecting patients who meet more than one condition.

For example, how many patients are both ER-positive and PR-positive?

ERpos_PRpos = brca_clin_df[
    (brca_clin_df["estrogen_receptor_status"] == "Positive") &
    (brca_clin_df["progesterone_receptor_status"] == "Positive")
]

len(ERpos_PRpos)
## 672
print("There are", len(ERpos_PRpos), "patients with both ER-positive and PR-positive tumors.")
## There are 672 patients with both ER-positive and PR-positive tumors.

Triple-negative breast cancer

Triple-negative breast cancer means the tumor is:

  • ER-negative
  • PR-negative
  • HER2-negative

These cancers can be more difficult to treat because they do not have these common therapeutic targets.

tnbc = brca_clin_df[
    (brca_clin_df["estrogen_receptor_status"] == "Negative") &
    (brca_clin_df["progesterone_receptor_status"] == "Negative") &
    (brca_clin_df["her2_receptor_status"] == "Negative")
]

len(tnbc)
## 114
tnbc_percent = round(100 * len(tnbc) / len(brca_clin_df), 1)

print("Triple-negative breast cancer cases make up", tnbc_percent, "% of this dataset.")
## Triple-negative breast cancer cases make up 10.5 % of this dataset.

Looking at relationships between variables

So far, we summarized one variable at a time. But clinical questions often involve relationships between variables.

Receptor status and vital status

er_vital_table = pd.crosstab(
    brca_clin_df["estrogen_receptor_status"],
    brca_clin_df["vital_status"]
)

er_vital_table
## vital_status              Alive  Dead
## estrogen_receptor_status             
## Indeterminate                 0     2
## Negative                    195    41
## Positive                    697    99
## [Not Evaluated]              40     8

Add row percentages:

round(100 * er_vital_table.div(er_vital_table.sum(axis=1), axis=0), 1)
## vital_status              Alive   Dead
## estrogen_receptor_status              
## Indeterminate               0.0  100.0
## Negative                   82.6   17.4
## Positive                   87.6   12.4
## [Not Evaluated]            83.3   16.7

This table asks:

Within each ER status group, what percent of patients are alive or dead?


Stacked bar plot

A stacked bar plot is a useful Python visualization for two categorical variables.

er_vital_percent = 100 * er_vital_table.div(er_vital_table.sum(axis=1), axis=0)

er_vital_percent.plot(kind="bar", stacked=True)
plt.title("Estrogen receptor status and vital status")
plt.xlabel("Estrogen receptor status")
plt.ylabel("Percent of patients")
plt.xticks(rotation=45)
## (array([0, 1, 2, 3]), [Text(0, 0, 'Indeterminate'), Text(1, 0, 'Negative'), Text(2, 0, 'Positive'), Text(3, 0, '[Not Evaluated]')])
plt.legend(title="Vital status")
plt.tight_layout()
plt.show()

Question: What can you conclude from this plot? What should you be careful not to overclaim?

Your answer: The plot shows that most patients have ER-positive tumors and that the majority of people in both groups survived. The ER-positive group even has a slightly better survival rate. However, you should not assume that an indeterminate status means a patient will definitely die. That column is so thin it probably only represents one or two people, which is not enough data to prove a real trend. You also should not claim that ER status is the only thing affecting survival, since this graph ignores other important factors like cancer stage and past treatments.


Histological type

Histology describes what the tumor tissue looks like under a microscope.

histology_counts = brca_clin_df["histological_type"].value_counts(dropna=False)
histology_counts
## histological_type
## Infiltrating Ductal Carcinoma       774
## Infiltrating Lobular Carcinoma      201
## Other, specify                       45
## Mixed Histology (please specify)     29
## Mucinous Carcinoma                   17
## Metaplastic Carcinoma                 8
## Medullary Carcinoma                   6
## Infiltrating Carcinoma NOS            1
## [Not Available]                       1
## Name: count, dtype: int64
histology_counts.plot(kind="bar")
plt.title("Histological type")
plt.ylabel("Number of patients")
plt.xlabel("Histological type")
plt.xticks(rotation=90)
## (array([0, 1, 2, 3, 4, 5, 6, 7, 8]), [Text(0, 0, 'Infiltrating Ductal Carcinoma'), Text(1, 0, 'Infiltrating Lobular Carcinoma'), Text(2, 0, 'Other, specify'), Text(3, 0, 'Mixed Histology (please specify)'), Text(4, 0, 'Mucinous Carcinoma'), Text(5, 0, 'Metaplastic Carcinoma'), Text(6, 0, 'Medullary Carcinoma'), Text(7, 0, 'Infiltrating Carcinoma NOS'), Text(8, 0, '[Not Available]')])
plt.tight_layout()
plt.show()

Most tumors in this dataset are infiltrating ductal carcinoma or infiltrating lobular carcinoma.

N_ductal = histology_counts.get("Infiltrating Ductal Carcinoma", 0)
percent_ductal = round(100 * N_ductal / len(brca_clin_df), 1)

print(str(percent_ductal) + "% of patients have infiltrating ductal carcinoma.")
## 71.5% of patients have infiltrating ductal carcinoma.

Pathologic stage challenge

Pathologic stage describes how far the cancer has progressed.

brca_clin_df["pathologic_stage"].value_counts(dropna=False).sort_index()
## pathologic_stage
## I                 179
## II                615
## III               250
## IV                 19
## X                  14
## [Not vailable]      5
## Name: count, dtype: int64

Challenge 1

How many patients have stage II breast cancer?

stage_counts = brca_clin_df["pathologic_stage"].value_counts(dropna=False)
N_stageII = stage_counts.get("II", 0)

N_stageII
## 615
percent_stageII = round(100 * N_stageII / len(brca_clin_df), 1)

print(
    "There are",
    N_stageII,
    "patients with stage II breast cancer, representing",
    percent_stageII,
    "% of the cohort."
)
## There are 615 patients with stage II breast cancer, representing 56.8 % of the cohort.

Challenge 2

Make a bar plot of cancer stage.

#Fixed bar plot (original plot showcased stage I as the highest)

import matplotlib.pyplot as plt

# Count the number of patients in each pathologic stage
stage_counts = brca_clin_df["pathologic_stage"].value_counts().sort_index()

# Create the bar plot
plt.figure(figsize=(5, 4))
plt.bar(
    stage_counts.index,
    stage_counts.values,
    color="lightgray",
    edgecolor="black",
    linewidth=1.2,
    width=0.6
)

plt.title("Pathologic stage")
plt.xlabel("Stage")
plt.ylabel("Number of patients")

plt.tight_layout()
plt.show()

A deeper question: stage and lymph nodes

Cancer staging often depends partly on whether cancer has spread to lymph nodes.

The column number_of_lymphnodes_positive looks numeric, but because it contains values such as [Not Available], Python may read it as text.

We can convert it carefully.

lymph_nodes_positive = pd.to_numeric(
    brca_clin_df["number_of_lymphnodes_positive"],
    errors="coerce"
)

lymph_nodes_positive.describe()
## count    919.000000
## mean       2.381937
## std        4.654389
## min        0.000000
## 25%        0.000000
## 50%        1.000000
## 75%        2.000000
## max       35.000000
## Name: number_of_lymphnodes_positive, dtype: float64

The option errors="coerce" tells pandas to turn non-numeric values like [Not Available] into missing values.

Now we can compare positive lymph node counts by pathologic stage. # Fixed the boxplot (made the figure bigger to be able to see distribution)

boxplot_df = pd.DataFrame({
    "pathologic_stage": brca_clin_df["pathologic_stage"],
    "lymph_nodes_positive": lymph_nodes_positive
}).dropna()

stage_order = sorted(boxplot_df["pathologic_stage"].unique())
boxplot_data = [
    boxplot_df.loc[boxplot_df["pathologic_stage"] == stage, "lymph_nodes_positive"]
    for stage in stage_order
]

plt.figure(figsize=(9, 4))

plt.boxplot(boxplot_data, labels=stage_order)
## {'whiskers': [<matplotlib.lines.Line2D object at 0x7babae9642c0>, <matplotlib.lines.Line2D object at 0x7babae964560>, <matplotlib.lines.Line2D object at 0x7babae965550>, <matplotlib.lines.Line2D object at 0x7babae965850>, <matplotlib.lines.Line2D object at 0x7babae966840>, <matplotlib.lines.Line2D object at 0x7babae966ae0>, <matplotlib.lines.Line2D object at 0x7babae967b30>, <matplotlib.lines.Line2D object at 0x7babae967e30>, <matplotlib.lines.Line2D object at 0x7babae998e00>, <matplotlib.lines.Line2D object at 0x7babae999100>, <matplotlib.lines.Line2D object at 0x7babae99a060>, <matplotlib.lines.Line2D object at 0x7babae99a330>], 'caps': [<matplotlib.lines.Line2D object at 0x7babae964830>, <matplotlib.lines.Line2D object at 0x7babae964b30>, <matplotlib.lines.Line2D object at 0x7babae965b20>, <matplotlib.lines.Line2D object at 0x7babae965dc0>, <matplotlib.lines.Line2D object at 0x7babae966de0>, <matplotlib.lines.Line2D object at 0x7babae9670e0>, <matplotlib.lines.Line2D object at 0x7babae998140>, <matplotlib.lines.Line2D object at 0x7babae9983e0>, <matplotlib.lines.Line2D object at 0x7babae999370>, <matplotlib.lines.Line2D object at 0x7babae999610>, <matplotlib.lines.Line2D object at 0x7babae99a630>, <matplotlib.lines.Line2D object at 0x7babae99a8d0>], 'boxes': [<matplotlib.lines.Line2D object at 0x7babae9640b0>, <matplotlib.lines.Line2D object at 0x7babae9652b0>, <matplotlib.lines.Line2D object at 0x7babae9665d0>, <matplotlib.lines.Line2D object at 0x7babae967860>, <matplotlib.lines.Line2D object at 0x7babae998b90>, <matplotlib.lines.Line2D object at 0x7babae999dc0>], 'medians': [<matplotlib.lines.Line2D object at 0x7babae964e30>, <matplotlib.lines.Line2D object at 0x7babae966090>, <matplotlib.lines.Line2D object at 0x7babae967350>, <matplotlib.lines.Line2D object at 0x7babae998680>, <matplotlib.lines.Line2D object at 0x7babae9998b0>, <matplotlib.lines.Line2D object at 0x7babae99aba0>], 'fliers': [<matplotlib.lines.Line2D object at 0x7babae9650d0>, <matplotlib.lines.Line2D object at 0x7babae966360>, <matplotlib.lines.Line2D object at 0x7babae967620>, <matplotlib.lines.Line2D object at 0x7babae998950>, <matplotlib.lines.Line2D object at 0x7babae999b80>, <matplotlib.lines.Line2D object at 0x7babae99aea0>], 'means': []}
plt.title("Positive lymph nodes by pathologic stage")
plt.xlabel("Pathologic stage")
plt.ylabel("Number of positive lymph nodes")

plt.tight_layout()
plt.show()

Question: What pattern do you see?

Your answer: The plot shows that as the cancer stage increases, the number of positive lymph nodes also goes up. In the early stages, patients usually have zero positive nodes. However, by Stage III and IV, the average number jumps up and the boxes get much taller to show a wider range. This pattern means that more advanced cancer is closely linked to spreading into more lymph nodes.


Final reflection

In this activity, you used Python to:

  • load a real clinical cancer dataset
  • inspect rows and columns
  • summarize categorical variables
  • summarize numerical variables
  • identify missing or uncertain values
  • combine logical conditions
  • define a clinically meaningful subtype
  • compare relationships between variables
  • create plots for communication

Most importantly, you saw that real biomedical data are both powerful and messy.

That is normal. A major part of computational biology is learning how to reason carefully with complex, imperfect data.


Create your report

Save your file. Then click:

Knit → Knit to HTML

Your final HTML report should include:

  • your code
  • your plots
  • your written answers
  • your interpretation of the data

This is one reason R Markdown is powerful: it combines writing, code, results, and figures in one reproducible document.


Next time

In later DREAM-High activities, we will connect these clinical features to gene expression data from the same cancer type.

That will allow us to ask a deeper systems biology question:

Can molecular data reveal patterns that help us understand cancer subtype, treatment, and disease behavior?