Visualizing Bio Data with Python
After Completing This Topic
You'll be able to draw three graphs commonly used in bio research with matplotlib: survival curves, volcano plots, and heatmaps.
matplotlib Basics: Your First Graph
matplotlib is Python's go-to visualization library. You can draw a graph with a single plt.plot().
import matplotlibmatplotlib.use("Agg")import matplotlib.pyplot as pltimport numpy as np
# Cell growth curve (exponential growth)hours = np.array([0, 2, 4, 6, 8, 10, 12, 24])cell_count = np.array([1000, 1200, 1800, 3200, 6000, 11000, 22000, 500000])
fig, ax = plt.subplots(figsize=(8, 5))ax.plot(hours, cell_count, "o-", color="#2E86AB", linewidth=2, markersize=6)ax.set_xlabel("Time (hours)", fontsize=12)ax.set_ylabel("Cell Count", fontsize=12)ax.set_title("Cell Growth Curve", fontsize=14, fontweight="bold")ax.set_yscale("log")ax.grid(True, alpha=0.3)fig.tight_layout()fig.savefig("growth_curve.png", dpi=150)plt.close(fig)
assert len(hours) == 8assert cell_count[-1] == 500000print("growth_curve.png saved")Bar Chart: Comparing GC Content Across Genes
import matplotlibmatplotlib.use("Agg")import matplotlib.pyplot as pltimport numpy as np
def calculate_gc(seq: str) -> float: seq = seq.upper() return (seq.count("G") + seq.count("C")) / len(seq) * 100
genes = { "BRCA1": "ATGGATTTATCTGCTCTTCGCGTTGAAGAAGTACAAAATGTC", "TP53": "ATGGAGGAGCCGCAGTCAGATCCTAGCGTGAGTTTGCTGTGA", "EGFR": "ATGCGACCCTCCGGGACGGCCGGGGCAGCGCTCCTGGCGCTG", "MYC": "ATGCCCCTCAACGTTAGCTTCACCAACAGGAACTATGACCTCG", "KRAS": "ATGACTGAATATAAACTTGTGGTAGTTGGAGCTGGTGGCGTAG",}
names = list(genes.keys())gc_values = [calculate_gc(seq) for seq in genes.values()]colors = ["#E74C3C" if gc > 60 else "#2E86AB" for gc in gc_values]
fig, ax = plt.subplots(figsize=(8, 5))bars = ax.bar(names, gc_values, color=colors, edgecolor="white", linewidth=0.5)ax.axhline(y=60, color="#E74C3C", linestyle="--", alpha=0.5, label="GC > 60% threshold")ax.set_ylabel("GC Content (%)", fontsize=12)ax.set_title("GC Content by Gene", fontsize=14, fontweight="bold")ax.legend()ax.set_ylim(0, 100)fig.tight_layout()fig.savefig("gc_bar_chart.png", dpi=150)plt.close(fig)
assert len(gc_values) == 5assert all(0 <= gc <= 100 for gc in gc_values)print(f"GC values: {[f'{gc:.1f}%' for gc in gc_values]}")Kaplan-Meier Survival Curve
Survival analysis is the most important visualization in clinical trials. It compares survival rates between two groups (treatment vs control).
import matplotlibmatplotlib.use("Agg")import matplotlib.pyplot as pltimport numpy as np
np.random.seed(42)
# Generate simulated clinical datan_patients = 50treatment_times = np.sort(np.random.exponential(scale=24, size=n_patients))control_times = np.sort(np.random.exponential(scale=16, size=n_patients))
def kaplan_meier(times, max_time=48): times = times[times <= max_time] n = len(times) km_times = [0] km_survival = [1.0] at_risk = n for t in sorted(set(times)): events = np.sum(times == t) survival = km_survival[-1] * (1 - events / at_risk) km_times.append(t) km_survival.append(survival) at_risk -= events return np.array(km_times), np.array(km_survival)
t_times, t_surv = kaplan_meier(treatment_times)c_times, c_surv = kaplan_meier(control_times)
fig, ax = plt.subplots(figsize=(8, 5))ax.step(t_times, t_surv, where="post", color="#2E86AB", linewidth=2, label="Treatment (n=50)")ax.step(c_times, c_surv, where="post", color="#E74C3C", linewidth=2, label="Control (n=50)")ax.fill_between(t_times, t_surv, step="post", alpha=0.1, color="#2E86AB")ax.fill_between(c_times, c_surv, step="post", alpha=0.1, color="#E74C3C")ax.set_xlabel("Time (months)", fontsize=12)ax.set_ylabel("Survival Probability", fontsize=12)ax.set_title("Kaplan-Meier Survival Curve", fontsize=14, fontweight="bold")ax.legend(fontsize=11, loc="lower left")ax.set_xlim(0, 48)ax.set_ylim(0, 1.05)ax.grid(True, alpha=0.3)fig.tight_layout()fig.savefig("kaplan_meier.png", dpi=150)plt.close(fig)
assert t_surv[0] == 1.0assert c_surv[0] == 1.0assert len(t_times) > 1print("kaplan_meier.png saved")Volcano Plot: Visualizing Differentially Expressed Genes
A volcano plot shows genes with statistically significant expression changes in RNA-seq data at a glance. The X-axis is the magnitude of change (log2 fold change), and the Y-axis is statistical significance (-log10 p-value).
import matplotlibmatplotlib.use("Agg")import matplotlib.pyplot as pltimport numpy as np
np.random.seed(42)n_genes = 500
log2fc = np.random.normal(0, 1.5, n_genes)pvalues = 10 ** (-np.abs(log2fc) * np.random.uniform(0.5, 3, n_genes))
fc_threshold = 1.0p_threshold = 0.05
colors = []for fc, p in zip(log2fc, pvalues): if p < p_threshold and fc > fc_threshold: colors.append("#E74C3C") # Upregulated elif p < p_threshold and fc < -fc_threshold: colors.append("#2E86AB") # Downregulated else: colors.append("#CCCCCC") # Not significant
neg_log_p = -np.log10(pvalues)
fig, ax = plt.subplots(figsize=(8, 6))ax.scatter(log2fc, neg_log_p, c=colors, s=10, alpha=0.7, edgecolors="none")ax.axhline(-np.log10(p_threshold), color="gray", linestyle="--", alpha=0.5)ax.axvline(fc_threshold, color="gray", linestyle="--", alpha=0.5)ax.axvline(-fc_threshold, color="gray", linestyle="--", alpha=0.5)
n_up = sum(1 for c in colors if c == "#E74C3C")n_down = sum(1 for c in colors if c == "#2E86AB")ax.set_xlabel("logโ Fold Change", fontsize=12)ax.set_ylabel("-logโโ p-value", fontsize=12)ax.set_title(f"Volcano Plot (โ{n_up} โ{n_down} DEGs)", fontsize=14, fontweight="bold")ax.grid(True, alpha=0.2)fig.tight_layout()fig.savefig("volcano_plot.png", dpi=150)plt.close(fig)
assert n_up > 0assert n_down > 0print(f"Upregulated: {n_up}, Downregulated: {n_down}")Heatmap: Gene Expression Patterns
A heatmap compares gene expression levels across multiple samples using colors.
import matplotlibmatplotlib.use("Agg")import matplotlib.pyplot as pltimport numpy as np
np.random.seed(42)
gene_names = ["BRCA1", "TP53", "EGFR", "MYC", "KRAS", "PTEN", "RB1", "APC"]sample_names = ["Normal_1", "Normal_2", "Tumor_1", "Tumor_2", "Tumor_3"]
expression = np.random.randn(len(gene_names), len(sample_names))expression[:, 2:] += np.random.uniform(0.5, 2.0, (len(gene_names), 3))
fig, ax = plt.subplots(figsize=(8, 6))im = ax.imshow(expression, cmap="RdBu_r", aspect="auto", vmin=-3, vmax=3)ax.set_xticks(range(len(sample_names)))ax.set_xticklabels(sample_names, rotation=45, ha="right", fontsize=10)ax.set_yticks(range(len(gene_names)))ax.set_yticklabels(gene_names, fontsize=10)ax.set_title("Gene Expression Heatmap", fontsize=14, fontweight="bold")fig.colorbar(im, ax=ax, label="Z-score", shrink=0.8)fig.tight_layout()fig.savefig("heatmap.png", dpi=150)plt.close(fig)
assert expression.shape == (8, 5)print(f"Heatmap: {expression.shape[0]} genes ร {expression.shape[1]} samples")Try It Yourself (Faded Example)
Fill in the blanks to complete a scatter plot.
import matplotlibmatplotlib.use("Agg")import matplotlib.pyplot as pltx = [1, 2, 3, 4, 5]y = [2.1, 3.9, 6.2, 7.8, 10.1]fig, ax = plt.subplots()ax.(x, y, color="blue")ax.set_xlabel("Concentration")ax.set_ylabel("")fig.savefig("scatter.png")plt.close(fig)
Common Errors & Solutions
Q: Graph doesn't show, UserWarning: Matplotlib is currently using agg
In server environments (Colab, SSH, etc.), call matplotlib.use("Agg") before importing plt, and use fig.savefig() to save to file instead of plt.show().
Q: Non-ASCII title characters are garbled
Add plt.rcParams['font.family'] = 'NanumGothic'. In Colab, install fonts first with !apt-get install -y fonts-nanum and restart the runtime.
Q: Labels are cut off at the edges
Call fig.tight_layout() before savefig(). This solves most clipping issues.
In the next article, we'll learn to classify gene expression data with machine learning.