Before you begin

Under the File tab, choose Save As… and save your own copy of this file.

Then replace Your name here at the top of the file with your name.

This document is your analysis notebook. It combines code, results, and your written interpretations in one place — making your work reproducible. Someone else can read your report, see your code, and understand exactly how you made each figure.


Big idea: data can hide patterns

Large biological datasets are often too big to understand by reading numbers in a table.

In DREAM-High, we will eventually use heatmaps to look for patterns in breast cancer gene expression data from The Cancer Genome Atlas. A heatmap can help us ask:

Today we practice the same idea using a small, familiar dataset.

Main idea: A heatmap turns numbers into colors so that hidden structure becomes easier to see.


What is Python?

Python is a free programming language widely used for data analysis, statistics, and visualization. In this course we will use Python to explore cancer-related datasets.

You do not need to memorize every command. Your goal is to learn how to:


What is RStudio?

RStudio is the application we are using to write and run code. We are working in an R Markdown file, which lets us combine text, code, results, and plots in one report.

When you click Knit, RStudio runs all the code and produces an HTML report.

Note: This notebook runs Python inside RStudio using a package called reticulate. The setup chunk at the top loads it automatically — you do not need to install anything.


R Markdown basics

R Markdown uses simple formatting:

  • # creates a large heading, ## a smaller one
  • **bold** and *italic* for emphasis
  • backticks for inline code
  • gray boxes contain code chunks

The only difference from the R version of this activity is that chunks begin with {python} instead of {r}. You run them the same way — click the green arrow.


The practice dataset: mtcars

R has built-in datasets; Python does not, but mtcars is freely available online. The chunk below loads it and prints its dimensions.

import pandas as pd

# Load mtcars and keep only numeric columns
mtcars = pd.read_csv(
    "https://vincentarelbundock.github.io/Rdatasets/csv/datasets/mtcars.csv",
    index_col=0
)
mtcars = mtcars.select_dtypes(include="number")

# dim(mtcars) in R  →  .shape in Python
print(mtcars.shape)   # (32, 11): 32 cars, 11 features
## (32, 11)

Each row is a car; each column is a feature:

# head(mtcars) in R  →  mtcars.head() in Python
print(mtcars.head())
##                     mpg  cyl   disp   hp  drat  ...   qsec  vs  am  gear  carb
## rownames                                        ...                           
## Mazda RX4          21.0    6  160.0  110  3.90  ...  16.46   0   1     4     4
## Mazda RX4 Wag      21.0    6  160.0  110  3.90  ...  17.02   0   1     4     4
## Datsun 710         22.8    4  108.0   93  3.85  ...  18.61   1   1     4     1
## Hornet 4 Drive     21.4    6  258.0  110  3.08  ...  19.44   1   0     3     1
## Hornet Sportabout  18.7    8  360.0  175  3.15  ...  17.02   0   0     3     2
## 
## [5 rows x 11 columns]

Quick reflection

What do you notice first when you look at this table?

Write your answer here:

The table looks overwhelming at first because every row is packed with numbers, but I also notice that each car has its own “numerical fingerprint.” Instead of reading one value at a time, I’m already wondering whether some cars naturally belong in the same group based on all of their measurements combined.


Learning to Inspect Data

Before making a plot, scientists usually inspect the data.

# dim(mtcars) in R  →  .shape in Python
print(mtcars.shape)
## (32, 11)
print()
# colnames(mtcars) in R  →  .columns in Python
print(mtcars.columns.tolist())
## ['mpg', 'cyl', 'disp', 'hp', 'drat', 'wt', 'qsec', 'vs', 'am', 'gear', 'carb']
print()
# class(mtcars) in R  →  type() in Python
print(type(mtcars))
## <class 'pandas.core.frame.DataFrame'>

Check yourself

View the entire dataset:

View(mtcars)
  • How many cars are in the dataset?
  • How many features are measured for each car?

Write your answer here:

There are 32 cars in the dataset. There are 11 numerical features measured for each car.


Getting help in Python

The easiest way to get help on python functions is to move back to R and use the function browseURL.

browseURL("https://seaborn.pydata.org/generated/seaborn.clustermap.html")

From data frame to matrix

The heatmap() function in R works with a numeric matrix. In Python, seaborn.clustermap works directly with a DataFrame, but we convert to a NumPy array here to mirror the R workflow.

import numpy as np

# as.matrix(mtcars) in R  →  .to_numpy() in Python
data = mtcars.to_numpy()

# class(data) in R  →  type() in Python
print(type(data))
## <class 'numpy.ndarray'>
# typeof(data) in R  →  .dtype in Python
print(data.dtype)   # float64 = R's "double"
## float64

The assignment operator in Python is = (R uses <-):

x = 5   # x <- 5 in R
print(x)
## 5

First heatmap: raw data

import seaborn as sns
import matplotlib.pyplot as plt

g = sns.clustermap(
    mtcars,
    figsize=(10, 12),
    xticklabels=True,
    yticklabels=True,
    cbar=False
)

g.cax.remove()

# Remove the automatic axis titles
g.ax_heatmap.set_xlabel("")
g.ax_heatmap.set_ylabel("")

# Adjust tick label sizes
g.ax_heatmap.tick_params(axis="x", labelsize=9)
g.ax_heatmap.tick_params(axis="y", labelsize=8)

# Give the x-axis labels room
g.fig.subplots_adjust(bottom=0.15)

plt.show()

The heatmap turns numbers into colors and clusters:

The tree diagrams on the sides are called dendrograms.

Interpretation question

Is it easy to see meaningful patterns? Why or why not?

Not really. My eyes are immediately drawn to the darkest colors instead of the actual relationships between cars. It feels like some variables are “shouting” while others are barely visible, making it difficult to tell whether the clusters represent biology—or in this case, cars—or just differences in measurement scales.


Second heatmap: scaled data

Let’s create a heatmap that allows us to find the true underlying patterns.

Why scaling matters

Some columns are measured on very different scales. Compare cyl and disp:

# min(mtcars$cyl) in R  →  mtcars["cyl"].min() in Python
print("cyl: ", mtcars["cyl"].min(), "to", mtcars["cyl"].max())
## cyl:  4 to 8
print("disp:", mtcars["disp"].min(), "to", mtcars["disp"].max())
## disp: 71.1 to 472.0

A value of 8 is high for cyl but tiny for disp. Without scaling, large-number columns dominate the heatmap colors. The same problem appears in gene expression data.

Prediction

What do you think will change after we scale the data?

I think scaling will make every feature “speak at the same volume.” Instead of the heatmap being dominated by variables with larger numbers, each feature should contribute equally, allowing the real similarities between cars to emerge.


Scaling the data

scale() in R standardizes each column to a mean of 0 and standard deviation of 1. We use the same function from the sklearn library in Python.

from sklearn.preprocessing import scale

# scale(data) in R  →  scale(mtcars) from sklearn in Python
data_scaled = pd.DataFrame(
    scale(mtcars),
    index=mtcars.index,
    columns=mtcars.columns
)

Let’s compare a few values before and after scaling.

# data[1:5, 1:5] in R  →  .iloc[0:5, 0:5] in Python (Python counts from 0)
print("Before scaling:")
## Before scaling:
print(mtcars.iloc[0:5, 0:5])
##                     mpg  cyl   disp   hp  drat
## rownames                                      
## Mazda RX4          21.0    6  160.0  110  3.90
## Mazda RX4 Wag      21.0    6  160.0  110  3.90
## Datsun 710         22.8    4  108.0   93  3.85
## Hornet 4 Drive     21.4    6  258.0  110  3.08
## Hornet Sportabout  18.7    8  360.0  175  3.15
print()
print("After scaling:")
## After scaling:
print(data_scaled.iloc[0:5, 0:5])
##                         mpg       cyl      disp        hp      drat
## rownames                                                           
## Mazda RX4          0.153299 -0.106668 -0.579750 -0.543655  0.576594
## Mazda RX4 Wag      0.153299 -0.106668 -0.579750 -0.543655  0.576594
## Datsun 710         0.456737 -1.244457 -1.006026 -0.795570  0.481584
## Hornet 4 Drive     0.220730 -0.106668  0.223615 -0.543655 -0.981576
## Hornet Sportabout -0.234427  1.031121  1.059772  0.419550 -0.848562

Now let’s try the heatmap with data_scaled.

g = sns.clustermap(
    data_scaled,
    figsize=(10, 12),
    xticklabels=True,
    yticklabels=True,
    cbar=False
)

g.cax.remove()

# Remove the automatic axis titles
g.ax_heatmap.set_xlabel("")
g.ax_heatmap.set_ylabel("")

# Adjust tick label sizes
g.ax_heatmap.tick_params(axis="x", labelsize=9)
g.ax_heatmap.tick_params(axis="y", labelsize=8)

# Give the x-axis labels room
g.fig.subplots_adjust(bottom=0.15)

plt.show()

Interpretation questions

  1. Does this heatmap look more informative than the raw-data heatmap?
  2. Which features seem to group together?
  3. Do mpg and wt show similar or opposite patterns?
  4. Do the car groupings make sense based on what you know about cars?
  1. Yes. The clusters seem more balanced, and I can actually compare variables instead of focusing on whichever column has the biggest numbers. It feels more like looking for patterns than looking at random colors.
  2. Weight (wt), horsepower (hp), displacement (disp), and cylinders (cyl) seem to cluster together, which makes intuitive sense because larger engines are usually heavier and produce more power.
  3. They show almost mirror-image patterns. As weight increases, fuel efficiency generally decreases, which matches what I would expect from everyday driving.
  4. Yes. The clusters remind me that cars are designed with trade-offs. Vehicles built for power and performance naturally share characteristics, while smaller, more fuel-efficient cars tend to cluster together for completely different reasons.

Color choices

We can change the color palette to make patterns easier to see. The chunk below shows a selection of available palettes.

# display.brewer.all() in R  →  color swatches in Python
palette_names = [
    "Blues", "Greens", "Oranges", "Purples", "Reds",
    "coolwarm", "rocket", "mako", "YlOrRd", "RdBu"
]

fig, axes = plt.subplots(len(palette_names), 1, figsize=(6, len(palette_names) * 0.6))
for ax, name in zip(axes, palette_names):
    colors = sns.color_palette(name, 10)
    ax.imshow([colors], aspect="auto")
    ax.set_yticks([])
    ax.set_xticks([])
    ax.set_ylabel(name, rotation=0, labelpad=60, va="center", fontsize=9)
plt.tight_layout()
plt.show()

Now use a palette in a heatmap:

# col = brewer.pal(8, "Greens") in R  →  cmap="Greens" in Python
# Try changing "Greens" to another name from the list above

# col = brewer.pal(8, "Greens") in R  →  cmap="Greens" in Python
# Try changing "Greens" to another name from the list above

# Assign clustermap to a variable 'g' to access its axes
g = sns.clustermap(
    data_scaled,
    cmap="Greens",
    figsize=(10, 12),
    xticklabels=True,
    yticklabels=True,
    cbar=False
)

g.cax.remove()

# Remove the automatic axis titles
g.ax_heatmap.set_xlabel("")
g.ax_heatmap.set_ylabel("")

# Adjust tick label sizes
g.ax_heatmap.tick_params(axis="x", labelsize=9)
g.ax_heatmap.tick_params(axis="y", labelsize=8)

# Give the x-axis labels room
g.fig.subplots_adjust(bottom=0.15)

plt.show()

Challenge Question 1:

Customization challenge

Replace "Greens" below with a palette of your choice and run the chunk.

# Change "Greens" in the cmap line to your color scheme
g = sns.clustermap(
    data_scaled,
    cmap="mako",
    figsize=(10, 12),
    xticklabels=True,
    yticklabels=True,
    cbar=False
)

g.cax.remove()

# Remove the automatic axis titles
g.ax_heatmap.set_xlabel("")
g.ax_heatmap.set_ylabel("")

# Adjust tick label sizes
g.ax_heatmap.tick_params(axis="x", labelsize=9)
g.ax_heatmap.tick_params(axis="y", labelsize=8)

# Give the x-axis labels room
g.fig.subplots_adjust(bottom=0.15)

plt.show()

Which palette did you choose, and why?

I chose the mako palette because the gradual transition between shades makes the clusters stand out without being distracting. It also reminds me of the color palettes used in scientific publications, making the visualization look cleaner and more professional.


Challenge Question 2

A more focused question: relationships among features

Sometimes we care less about individual rows and more about how variables relate to one another.

Correlation refers to the statistical relationship between two variables (feature), describing how they change together. For example:

import pandas as pd

# Calculate a single correlation coefficient
correlation = mtcars["mpg"].corr(mtcars["wt"])
print("Correlation:", correlation)
## Correlation: -0.8676593765172279

This means that across cars, mpg decreases as weight increases. This matches our intuition. Consider the scatterplot where each point corresponds to a car:

import matplotlib.pyplot as plt

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

plt.scatter(mtcars["wt"], mtcars["mpg"])
plt.xlabel("Weight")
plt.ylabel("Miles per Gallon")

plt.show()

A correlation matrix shows how strongly pairs of features move together.

  • Positive correlation: both features increase together
  • Negative correlation: one increases as the other decreases
  • Near zero: little relationship
# cor(mtcars) in R  →  .corr() in Python
cor_mtcars = mtcars.corr()

# round(cor_mtcars, 2) in R  →  .round(2) in Python
print(cor_mtcars.round(2))
##        mpg   cyl  disp    hp  drat    wt  qsec    vs    am  gear  carb
## mpg   1.00 -0.85 -0.85 -0.78  0.68 -0.87  0.42  0.66  0.60  0.48 -0.55
## cyl  -0.85  1.00  0.90  0.83 -0.70  0.78 -0.59 -0.81 -0.52 -0.49  0.53
## disp -0.85  0.90  1.00  0.79 -0.71  0.89 -0.43 -0.71 -0.59 -0.56  0.39
## hp   -0.78  0.83  0.79  1.00 -0.45  0.66 -0.71 -0.72 -0.24 -0.13  0.75
## drat  0.68 -0.70 -0.71 -0.45  1.00 -0.71  0.09  0.44  0.71  0.70 -0.09
## wt   -0.87  0.78  0.89  0.66 -0.71  1.00 -0.17 -0.55 -0.69 -0.58  0.43
## qsec  0.42 -0.59 -0.43 -0.71  0.09 -0.17  1.00  0.74 -0.23 -0.21 -0.66
## vs    0.66 -0.81 -0.71 -0.72  0.44 -0.55  0.74  1.00  0.17  0.21 -0.57
## am    0.60 -0.52 -0.59 -0.24  0.71 -0.69 -0.23  0.17  1.00  0.79  0.06
## gear  0.48 -0.49 -0.56 -0.13  0.70 -0.58 -0.21  0.21  0.79  1.00  0.27
## carb -0.55  0.53  0.39  0.75 -0.09  0.43 -0.66 -0.57  0.06  0.27  1.00
# heatmap(cor_mtcars, symm=TRUE) in R  →  sns.clustermap() in Python
# A diverging palette (blue = positive, red = negative) works well here
import seaborn as sns
import matplotlib.pyplot as plt

g = sns.clustermap(
    cor_mtcars,
    cmap="RdBu_r",
    vmin=-1, vmax=1,
    figsize=(5.5, 5.5),      # smaller figure
    dendrogram_ratio=0.12,   # smaller dendrograms
    cbar_pos=(0.02, 0.82, 0.04, 0.15)  # smaller color bar
)

# Make the labels smaller
g.ax_heatmap.tick_params(axis='x', labelsize=9)
g.ax_heatmap.tick_params(axis='y', labelsize=9)

plt.show()

Interpretation question

Find one pair of features with a strong positive relationship and one with a strong negative relationship.

A strong positive relationship is displacement (disp) and horsepower (hp) because larger engines generally produce more power. A strong negative relationship is weight (wt) and miles per gallon (mpg), showing that heavier vehicles usually sacrifice fuel efficiency.


Why this matters for cancer biology

Today we used cars because the dataset is small and easy to practice with. The same workflow applies to biological data:

patients or samples  → rows
genes or features    → columns
expression values    → numbers in the matrix
heatmap              → visual pattern discovery
clustering           → grouping similar samples or genes

In breast cancer data, heatmaps can reveal groups of patients with similar molecular patterns — patterns that sometimes correspond to tumor subtypes with different clinical outcomes.


Final reflection

  1. What did scaling change about the heatmap?
  2. Why might heatmaps be useful for studying cancer data?
  3. What is one Python concept you feel more comfortable with now?
  4. What is one Python concept you still find confusing?
  1. Scaling changed the story the heatmap tells. Instead of highlighting the variables with the biggest numerical values, it highlighted relationships. The visualization shifted from comparing measurements to comparing patterns.
  2. Cancer datasets often contain thousands of genes, making them impossible to understand by simply reading numbers. A heatmap acts like a visual map, helping researchers quickly identify patients or genes that behave similarly. Those hidden patterns could point to different cancer subtypes, potential biomarkers, or even new treatment strategies.
  3. I feel much more comfortable thinking of a DataFrame as something I can explore instead of just storing data. Using methods like .head(), .shape, and .corr() makes it feel less intimidating because I can ask simple questions before diving into more advanced analyses.
  4. I’m still curious about what happens “behind the scenes” during hierarchical clustering. I understand that similar rows are grouped together, but I’d like to learn how the algorithm decides where to split branches in the dendrogram and how different distance metrics affect the final clusters.

Knit your report

Click Knit → Knit to HTML. Your report should include your name, your code, your heatmaps, and your answers to all the reflection questions.


Summary

In this activity you practiced:

Computational tools can help us discover patterns hidden inside large biological datasets.


R vs Python quick reference

Task R Python
Load data built-in mtcars pd.read_csv("url", index_col=0)
First few rows head(mtcars) print(mtcars.head())
Dimensions dim(mtcars) print(mtcars.shape)
Column names colnames(mtcars) print(mtcars.columns.tolist())
Object class class(x) print(type(x))
Data type typeof(x) print(x.dtype)
Convert to matrix as.matrix(mtcars) mtcars.to_numpy()
Assignment x <- 5 x = 5
Index rows & cols data[1:5, 1:5] data.iloc[0:5, 0:5]
Column min / max min(mtcars$cyl) mtcars["cyl"].min()
Scale columns scale(data) scale(mtcars) from sklearn
Correlation matrix cor(mtcars) mtcars.corr()
Round values round(x, 2) x.round(2)
Heatmap heatmap(data) sns.clustermap(data)
Color palette col = brewer.pal(8, "Greens") cmap="Greens"
Comment # text # text
Get help help(heatmap) help(sns.clustermap)