BP101T · Semester 1 · B.Pharm

Unit IV — Data Handling with Pandas

Basics of Python Programming for Pharmaceutical Sciences

This study page introduces Data Handling with Pandas in Basics of Python Programming for Pharmaceutical Sciences (BP101T), B.Pharm Semester 1. Read the full notes below, or download the unit PDF for offline study.

Topics in this unit

  • Pandas library, Series, and DataFrame structures
  • CSV and Excel practice datasets for PK studies and ADR reports
  • head(), tail(), info(), and describe() inspection
  • Cleaning missing values, filtering, selecting, grouping, and aggregation

Full notes for this unit

Read the notes here, or download the PDF below for offline study. Plots and diagrams are in the PDF. All data in the examples is synthetic teaching data.

Why Pandas? Meet the Series

Lists vs Series - the ladder

Python list: values only, position access doses[2]. NumPy: fast math, still no labels. Series: values + index labels - doses["P03"] reads as patient P03. That label is what makes PK time-courses and ADR severity counts self-documenting.

series_dose.py
# Series: one labelled column of doses (mg)
import pandas as pd
doses = pd.Series([100, 250, 500, 500, 250],
    index=["P01", "P02", "P03", "P04", "P05"], name="dose_mg")
print(doses.mean(), doses.max())

Result: 320.0 500

DataFrame - columns, index, values

Synthetic teaching data - generated with seeded numpy for classroom practice.

frame_mini.py
# DataFrame from a dict: each key becomes a column
adr_mini = pd.DataFrame({
    "patient": ["P01", "P02", "P03"],
    "drug": ["PCM", "IBU", "AMX"],
    "severity": ["mild", "severe", "moderate"]})
print(adr_mini.shape, list(adr_mini.columns))

Result: (3, 3) ['patient', 'drug', 'severity']

PartWhat it isExampleAccess
indexrow labels0, 1, 2df.index
columnscolumn labelsdrug, severitydf.columns
valuesraw 2-D array3 x 3 blockdf.values
shape(rows, cols)(3, 3)df.shape

Dataset 1 - plasma concentration vs time

Design: 5 patients x 4 timepoints

Columns: patient time_hr conc_ng_ml. Seeded numpy (seed 42) so every run reproduces the same table. Exponential decay + small noise mimics a real single-dose PK curve.

build_pk.py
# Seeded synthetic PK study (20 rows)
import numpy as np, pandas as pd
np.random.seed(42)
rows = []
for p in ["P01", "P02", "P03", "P04", "P05"]:
    cmax = np.random.uniform(700, 950)
    for t in [0.5, 1.0, 2.0, 4.0]:
        conc = round(cmax*np.exp(-0.35*t)+np.random.normal(0,18), 1)
        rows.append((p, t, conc))
pk = pd.DataFrame(rows, columns=["patient", "time_hr", "conc_ng_ml"])

Result: 20 rows | P01 0.5h 646.2 P01 1.0h 565.0 P02 0.5h 727.8 ... | conc falls with time

Why Seeded? Without seed(42) every student gets different numbers and answers stop matching. Seeded data = one answer key for the whole class.

Dataset 2 - ADR reports with missing outcomes

build_adr.py
# 12 ADR reports; 2 outcomes left blank (NaN) on purpose
adr = pd.DataFrame({
    "patient": ["P01".."P12"], # 12 IDs
    "drug": ["Paracetamol", "Ibuprofen", ...],
    "severity": ["mild", "moderate", "severe", ...],
    "outcome": ["recovered", ...,None, ...,None]}) # 2 NaN
pk.to_csv("u4_pk.csv", index=False)
adr.to_csv("u4_adr.csv", index=False)
with pd.ExcelWriter("u4_data.xlsx") as w:
    pk.to_excel(w, sheet_name="PK", index=False)
    adr.to_excel(w, sheet_name="ADR", index=False)

Result: u4_pk.csv (20 rows) u4_adr.csv (12 rows, 2 NaN) u4_data.xlsx sheets PK + ADR

File layout

u4_pk.csv holds the concentration table. u4_adr.csv holds ADR reports. u4_data.xlsx bundles both as sheets PK and ADR via openpyxl.

Reading CSV and Excel

read_files.py
# Read the files we just saved
pk2 = pd.read_csv("u4_pk.csv")
adr2 = pd.read_excel("u4_data.xlsx", sheet_name="ADR")
print(pk2.shape, adr2.shape)

Result: (20, 3) (12, 4) | PK: patient, time_hr, conc_ng_ml ADR: + drug, severity, outcome

The two readers side by side

read_csv(path) parses comma text - fastest for PK exports from instruments. read_excel(path, sheet_name=...) opens workbook sheets - standard for ADR registers. Both return a DataFrame; check .shape immediately.

Preview rows, check structure

first_look.py
# Peek at the top, bottom and structure
print(pk.head(3)) # first 3 rows
print(pk.tail(3)) # last 3 rows
pk.info() # dtype + non-null counts

Result: head: P01 0.5h 646.2, 1.0h 565.0, 2.0h 399.1 | tail: P05 1.0h 535.6, 2.0h 374.6, 4.0h 175.6

Output info PK: 20 entries, 3 cols, float64 x2 + object x1, 612 bytes | ADR: 12 entries, outcome 10 non-null

Reading info

Non-Null Count below the total = missing values. outcome shows 10 of 12 - the 2 NaN we planted. dtypes tell you which columns can do math.

describe - one-line summary statistics

describe_pk.py
print(pk.describe().round(2))
print(adr["severity"].value_counts())

Result: conc_ng_ml: count 20, mean 448.51, std 183.63, min 163.90, 50% 459.80, max 727.80

Output severity value_counts: mild 4, moderate 4, severe 4 (balanced teaching set)

How to read describe

mean centres the curve, std spreads it, min / max bound it, 25% / 50% / 75% split it in quartiles. count below 20 would itself flag missing concentrations.

Finding Missing Values

find_na.py
# BEFORE: count the gaps
print(adr.isna().sum())

Result: patient 0 drug 0 severity 0 outcome 2 | only outcome has gaps

Two fixes - drop rows or fill values

fix_na.py
# AFTER option A: drop gappy rows (12 -> 10 rows)
clean_drop = adr.dropna()
# AFTER option B: keep rows, label the gap
clean_fill = adr.copy()
clean_fill["outcome"] = clean_fill["outcome"].fillna("under review")

Result: dropna: 10 rows survive (P07, P12 removed) | fillna: 12 rows, P07 + P12 outcome = under review

Which fix when? dropna when gaps are few and rows expendable - simplest for analysis. fillna when every report counts - ADR registers must not lose cases. Never fill concentrations with 0; it fakes a measurement.

Filtering with Conditions

filter_pharma.py
# Boolean mask keeps only matching rows
severe = adr[adr.severity == "severe"] # 4 rows
high = pk[pk.conc_ng_ml > 500] # 9 rows above threshold

Result: severe: P03 PCM, P05 IBU, P07 AMX, P12 PCM (4 rows)

Output conc > 500: 9 rows, all at 0.5h-1.0h (early timepoints only)

Mask anatomy

adr.severity == 'severe' builds a True/False mask; adr[mask] keeps True rows. Combine with & (and) / | (or), each test in brackets: (a) & (b).

Groupby: Split-Apply-Combine

group_pharma.py
# Mean conc per timepoint + ADR counts per drug
print(pk.groupby("time_hr")["conc_ng_ml"].mean().round(1))
print(adr.groupby("drug").size())

Result: mean conc: 0.5h 650.9, 1.0h 557.0, 2.0h 398.5, 4.0h 187.7 (decay curve)

Output ADR counts: Amoxicillin 3, Ibuprofen 4, Paracetamol 5

Split - Apply - Combine

Split rows by key (time, drug). Apply one function (mean, size, count). Combine results into a small summary frame. Two keys give two-level summaries: groupby(["drug","severity"]).

ADR summary pipeline - one function, full workflow

adr_pipeline.py
def adr_summary(df):
    # clean -> group -> sort, all inside one function
    df = df.copy()
    df["outcome"] = df["outcome"].fillna("under review")
    out = df.groupby(["drug", "severity"]).size().reset_index(name="n")
    return out.sort_values(["drug", "n"], ascending=[True, False])
print(adr_summary(adr).to_string(index=False))

Result: IBU mild 2 on top of its group PCM moderate 2, severe 2 | 9-row ranked summary

Trace the pipeline

1 copy protects the original. 2 fillna labels 2 gaps. 3 groupby + size counts each drug-severity cell. 4 reset_index turns the result back into a table. 5 sort lists the most frequent severity first within each drug.

Exam Gold

This function chains every Unit-IV skill: clean, filter-ready, group, sort. Reproduce it from memory and the 5-mark pipeline question is yours.

Two Errors, Zero Panic

err_key.py
# KeyError: wrong column name
try:
     adr["dosage"]
except KeyError as e:
     print("KeyError:", e) # KeyError: 'dosage'

Result: KeyError: 'dosage' | fix: print(df.columns) first, copy the exact name

KeyError - the typo tax

Column names are case- and spelling-sensitive: "Severity" is not "severity". Diagnosis: print(adr.columns.tolist()) and copy-paste. Prevention: never retype a column name from memory.

Every Unit-IV method on one card

MethodWhat it doesPharma example
Series(list, index)one labelled columnSeries(doses, P-ids)
DataFrame(dict)dict of lists to tableDataFrame(adr_dict)
read_csv / read_excelload file to frameread_csv('u4_pk.csv')
head(n) / tail(n)preview endspk.head(3)
info() / describe()structure / statspk.describe()
isna().sum()count gaps per coladr.isna().sum()
dropna / fillna(v)drop / fill gapsfillna('under review')
df[mask]filter rowsadr[sev=='severe']
groupby + aggsplit-apply-combinegroupby('drug').size()

How to Memorise

Read each row as verb-object-target: Series labels, DataFrame tables, readers load, head previews, info diagnoses, cleaners fix, masks filter, groupby summarises.

* Three Demos, Real Output

D1 · describe row guide

dive_describe.py
# D1: what each describe() row tells - real PK frame
print(pk["conc_ng_ml"].describe().round(2))

Result: count 20 = no gaps | mean 448.51 centre | std 183.63 spread | 50% 459.80 median | min 163.90 max 727.80 bounds

D2 · median vs mean fill

dive_fillna.py
# D2: knock out P03 2.0h (was 400.1), then choose the fill
pk2 = pk.copy(); pk2.loc[10, "conc_ng_ml"] = float("nan")
med = pk2.conc_ng_ml.median() # 492.4
avg = pk2.conc_ng_ml.mean() # 451.06
clean = pk2.conc_ng_ml.fillna(med) # 0 gaps left

Result: BEFORE isna 1 (P03 2.0h knocked out) | median fill 492.4 | mean fill 451.06 | AFTER 0 gaps

D3 · multi-aggregation

dive_multiagg.py
# D3: centre AND peak per timepoint in one pass
print(pk.groupby("time_hr")["conc_ng_ml"].agg(["mean", "max"]).round(1))

Result: 0.5h mean 650.9 max 727.8 | 1.0h 557.0 627.8 | 2.0h 398.5 427.2 | 4.0h 187.7 213.9

Dive Rules

count = coverage, mean = centre, 50% = median, min/max = bounds. Low 4.0h troughs drag the mean (451.06) below the median (492.4) - skewed tails mean fill with median, never 0. agg(['mean','max']) gives centre + peak together.

Exam Points

Q1 - Series vs DataFrame (2m)

Series is 1-D labelled pd.Series(doses); DataFrame is 2-D pd.DataFrame(d). Quote dose list vs ADR table.

Q2 - read_csv vs read_excel (2m)

read_csv('u4_pk.csv') for text CSV; read_excel(x, sheet_name='ADR') for workbooks. Always verify with .shape.

Q3 - head/info/describe (3m)

head() previews, info() shows dtypes + non-null counts (outcome 10/12), describe() gives mean 448.51, max 727.80.

Q4 - isna/dropna/fillna (3m)

isna().sum() found outcome 2; dropna() 12->10 rows; fillna('under review') keeps all 12.

Q5 - filter + groupby pipeline (5m)

Filter adr[sev=='severe'] (4 rows); group groupby('time_hr').mean() decay 650.9->187.7; reproduce adr_summary().

Five questions - honest answers only

Try each question first, then check the answer after the dash.

  • Series([100,250]) with index [P01,P02]: what does s['P02'] return? - 250 - labels select values.
  • adr has 12 rows, outcome 2 NaN: what does dropna leave? What does fillna keep? - dropna: 10 rows; fillna: all 12 with labels.
  • Write the one-liner that keeps only severe ADR rows. - adr[adr.severity == 'severe'] - mask then select.
  • groupby('time_hr').mean() on PK gives 650.9, 557.0, 398.5, 187.7: rising or falling? - Falling: a classic decay curve.
  • adr['dosage'] raises KeyError: what is the first debugging command? - print(adr.columns.tolist()) - copy the exact name.

Unit-IV in One Line

Pandas turns PK tables and ADR reports into labelled DataFrames you read, explore, clean, filter and group - one reproducible pipeline from raw file to safety summary.

! Pinpoints - Must Remember

#HookRecall in one line
1head/info/describepreview rows, diagnose types + gaps, summarise stats
2isna().sum()count gaps per column BEFORE any fix - outcome 2
3dropna vs fillnadrop 12->10 rows; fill keeps 12 as 'under review'
4boolean maskdf[mask] keeps True rows; combine with (a) & (b)
5loc vs ilocloc takes labels like P03; iloc takes positions 0,1,2
6groupbysplit-apply-combine: mean per time, size per drug
7read_csv/excelCSV is fastest; Excel needs openpyxl + sheet_name
8dtypesnumeric columns do math; object columns do not
9.shape checkverify every load: PK (20,3), ADR (12,4)
10value_countsseverity audit 4-4-4: a balanced teaching set

3 exam predictions

P1 - 5-mark pipeline: reproduce adr_summary() from memory - clean, group, sort. P2 - 3-mark describe: quote mean 448.51, median 459.80, max 727.80; say what 50% means. P3 - 2-mark debug: KeyError on 'dosage' - first command print(adr.columns.tolist()).

Mnemonic - the 5 L's

Load, Look, Lose-gaps, Limit, Lump: Load files, Look with head/info/describe, Lose gaps via dropna/fillna, Limit rows with a mask, Lump rows with groupby. Five L's, five calls, full unit.

Download notes

Save the PDF for offline reading. On some browsers, the file opens first; use the PDF viewer’s download button to save it.

For education and exam preparation only, not medical advice or current regulatory guidance.

Other units in this subject

Back to BP101T notes