Unit IV · Basics of Python Programming for Pharmaceutical Sciences (BP101T) · As per PCI B.Pharmacy Syllabus, NEP 2020
Loading a dataset into a DataFrame is only the first step — before any real analysis begins, an analyst needs a clear picture of the data’s shape, the data type in each column, and any obvious quality problems. Pandas offers a small set of inspection functions that together give a fast, comprehensive first look. For pharmaceutical data this initial check is not a formality but a scientific necessity: a variable stored with the wrong data type, an unnoticed missing value, or an implausible extreme reading can silently corrupt every statistical conclusion drawn afterward.

head() and tail(): A First Visual Check
The head() method returns the first n rows of a DataFrame, and tail() the last n rows, with n defaulting to 5 for both. These are usually the first commands run after loading a dataset, giving an immediate visual check of its structure and content.
Viewing the First Rows
Calling head() with no argument returns the first five rows of a pharmacokinetic (PK) sampling dataset for a single subject:
pk_data = pd.read_csv("pk_study_data.csv")
print(pk_data.head())
# Output:
# SubjectID Time_h Concentration_ugmL Sex Weight_kg Age_years
# 0 S001 0.0 0.00 F 62.4 28
# 1 S001 0.5 3.21 F 62.4 28
# 2 S001 1.0 7.85 F 62.4 28
# 3 S001 2.0 6.42 F 62.4 28
# 4 S001 4.0 3.10 F 62.4 28
Passing an integer argument limits the rows returned — head(3) would give the same output truncated to indices 0–2. For a PK dataset, head() lets an analyst quickly confirm that column names were parsed correctly, the sampling time series starts at zero, and the first few observations look plausible.
Viewing the Last Rows
The tail() method mirrors head() but reads from the end of the DataFrame. With no argument, it returns the last five rows of the same eight-row PK dataset (index positions 3 through 7):
print(pk_data.tail())
# Output:
# SubjectID Time_h Concentration_ugmL Sex Weight_kg Age_years
# 3 S001 2.0 6.42 F 62.4 28
# 4 S001 4.0 3.10 F 62.4 28
# 5 S001 6.0 1.50 F 62.4 28
# 6 S001 8.0 0.72 F 62.4 28
# 7 S001 12.0 BLQ F 62.4 28
The final observation records the concentration as "BLQ" rather than a number. In a PK context, tail() is the natural way to confirm the sampling series ends where it should and to catch rows accidentally cut off (truncated) during file export.
head() and tail() when called without an argument. The answer is 5 for both — memorize this exactly, since examiners sometimes phrase it as a fill-in-the-blank.info(): A Structural Summary
The info() method reports a compact structural summary of a DataFrame: the total row count, the number of non-null (non-missing) entries in every column, and the data type (dtype) Pandas inferred for each column. This makes it a key tool for a first-pass data quality check.
PK Dataset Example
pk_data = pd.read_csv("pk_study_data.csv")
print(pk_data.info())
# Output:
# <class 'pandas.core.frame.DataFrame'>
# RangeIndex: 8 entries, 0 to 7
# Data columns (total 6 columns):
# # Column Non-Null Count Dtype
# --- ------ -------------- -----
# 0 SubjectID 8 non-null object
# 1 Time_h 8 non-null float64
# 2 Concentration_ugmL 8 non-null object
# 3 Sex 8 non-null object
# 4 Weight_kg 8 non-null float64
# 5 Age_years 8 non-null int64
# dtypes: float64(2), int64(1), object(3)
# memory usage: 516.0+ bytes
The Concentration_ugmL column is classified as object rather than numeric because one record contains the text value “BLQ” — so Pandas treats the entire column as object, even though all 8 entries are technically non-null. That “BLQ” value cannot be used directly in calculations and needs handling — such as conversion to NaN via na_values at load time, covered in the previous topic — before any pharmacokinetic analysis such as computing Cmax or AUC can proceed.
ADR Dataset Example
ADR_data = pd.read_csv("ADR.csv")
print(ADR_data.info())
# Output:
# <class 'pandas.core.frame.DataFrame'>
# RangeIndex: 10 entries, 0 to 9
# Data columns (total 6 columns):
# # Column Non-Null Count Dtype
# --- ------ -------------- -----
# 0 SubjectID 10 non-null object
# 1 Adverse_Event 8 non-null object
# 2 Severity 5 non-null object
# 3 Onset_Time_h 8 non-null float64
# 4 Outcome 10 non-null object
# 5 Relation_to_Drug 10 non-null object
# dtypes: float64(1), object(5)
# memory usage: 612.0+ bytes
The Severity column contains only 5 non-null values out of 10 records, indicating that severity information is unavailable for 5 subjects. Similarly, Onset_Time_h contains only 8 non-null values, because the two subjects who reported no adverse event (S001 and S007) naturally have no onset time to record.
What info() Tells You
| What info() Reports | Why It Matters |
|---|---|
| Total row count | Confirms the full dataset was loaded and nothing was lost during ingestion. |
| Non-null counts | Flags which columns contain missing data and how much. |
| Data types | Exposes type mismatches — for example, a column that should be numeric but was read as object because it contains embedded text. |
| Memory usage | Becomes relevant when working with large pharmacovigilance databases, where dataset size can affect performance. |
describe(): A Statistical Summary
The describe() method produces a statistical summary of every numeric column: count of non-missing values, mean, standard deviation, minimum, the 25th percentile (Q1), median (50th percentile), 75th percentile (Q3), and maximum. This gives an immediate sense of how each variable is distributed and can expose data-entry errors or outliers.
PK Dataset Numeric Summary
pk_data = pd.read_csv("pk_study_data.csv")
print(pk_data.describe())
# Output:
# Time_h Weight_kg Age_years
# count 8.000000 8.0 8.0
# mean 4.187500 62.4 28.0
# std 4.225268 0.0 0.0
# min 0.000000 62.4 28.0
# 25% 0.875000 62.4 28.0
# 50% 3.000000 62.4 28.0
# 75% 6.500000 62.4 28.0
# max 12.000000 62.4 28.0
Because this dataset contains observations from only one subject (S001), Weight_kg and Age_years show zero spread (std of 0), since every row repeats that subject’s weight and age. In a study enrolling many subjects, describe() on demographic columns like these would instead reveal the real range and spread of the patient population — flagging, for instance, whether enrolled ages fall within the trial’s inclusion criteria.
ADR Dataset Numeric Summary
ADR_data = pd.read_csv("ADR.csv")
print(ADR_data.describe())
# Output:
# Onset_Time_h
# count 8.000000
# mean 3.937500
# std 2.930575
# min 0.500000
# 25% 1.375000
# 50% 2.250000
# 75% 4.875000
# max 12.000000
This summary shows that, among the 8 subjects who actually experienced an adverse event, the average onset was about 3.9 hours after dosing. The earliest onset occurred at 0.5 hours (subject S005, abdominal pain) and the latest at 12 hours (subject S008, rash). The median onset time across these subjects was 2.25 hours.
Describing Categorical Columns with describe(include=’object’)
By default, describe() only summarizes numeric columns. Passing include='object' switches it to summarize the non-numeric (categorical/text) columns instead, reporting for each one the count of non-missing entries, the number of unique values, the most frequently occurring value (the “top”), and how many times that top value occurs (its “freq”).
ADR_data = pd.read_csv("ADR.csv")
print(ADR_data.describe(include='object'))
# Output:
# SubjectID Adverse_Event Severity Outcome Relation_to_Drug
# count 10 8 5 10 10
# unique 10 7 3 2 3
# top S001 Headache Mild Resolved Possible
# freq 1 2 2 9 4
| Term | Meaning |
|---|---|
| unique | Number of distinct values present in the column. |
| top | Value that occurs most frequently in the column. |
| freq | Number of times the most frequent value occurs. |
The ADR dataset contains 7 distinct adverse-event categories and 3 distinct categories describing the relationship between the drug and the event. This form of describe() is equally valuable for spotting imbalanced treatment groups or unexpected category levels that were not anticipated during study design.
describe() reports by default, and then to name the parameter that switches it to categorical columns. Remember the full list — count, mean, std, min, 25%, 50%, 75%, max — for the numeric case, and count/unique/top/freq for describe(include='object').Interpreting describe() in a Pharmaceutical Context
The describe() output is especially informative for pharmacokinetic data. For instance, a maximum observed concentration (Cmax) of 22.40 µg/mL in a larger dataset could be checked against published literature values to judge whether it is physiologically plausible. Similarly, an unusually high maximum body weight of 102 kg among enrolled subjects might prompt a review of whether that patient truly met the study’s inclusion criteria. A 50th-percentile (median) time point of 2.0 hours would indicate that most sampling occurred early after dosing, consistent with a drug that reaches peak plasma concentration quickly.
As a general practice, head(), info(), and describe() should be run together as a standard trio immediately after loading any pharmaceutical dataset. Any discrepancies they reveal — incorrect data types, unexpected missing values, or implausible extreme values — should be documented before the dataset is used for further analysis.
Checking Dataset Shape and Column Names
Two further attributes round out a first inspection of any newly loaded DataFrame: shape reports the dimensions of the table as a (rows, columns) pair, while columns.tolist() lists every column name in order. Both are quick sanity checks that a file was parsed the way it was expected to be, especially after a change to how a dataset is exported.
# Number of rows and columns, returned as a (rows, columns) tuple
pk_data = pd.read_csv('pk_study_data.csv')
ADR_data = pd.read_csv("ADR.csv")
print("PK data shape:", pk_data.shape)
print("ADR data shape:", ADR_data.shape)
# List every column name
print("PK columns:", pk_data.columns.tolist())
print("ADR columns:", ADR_data.columns.tolist())
# Output:
# PK data shape: (8, 6)
# ADR data shape: (10, 6)
# PK columns: ['SubjectID', 'Time_h', 'Concentration_ugmL', 'Sex', 'Weight_kg', 'Age_years']
# ADR columns: ['SubjectID', 'Adverse_Event', 'Severity', 'Onset_Time_h', 'Outcome', 'Relation_to_Drug']
A PK dataset shape of (8, 6) confirms all eight sampling rows and six columns were read in; an unexpected row or column count here is often the first sign that a header line was misread or that a delimiter option needs adjusting. Checking columns.tolist() is equally useful for catching stray whitespace or inconsistent capitalisation in column names before they cause a KeyError later in the analysis.
Frequently Asked Questions
Why does describe() sometimes skip a column entirely?
By default, describe() only summarizes numeric (int/float) columns and silently skips object (text) columns. If a column you expect to see is missing from the output, check its dtype with info() first — it may have been read as text (as happens with the “BLQ”-containing concentration column), or you may need describe(include='object') to see it summarized as categorical data instead.
What is the difference between info() and describe()?
info() reports structure — row count, non-null counts, and data types for every column. describe() reports statistics — mean, standard deviation, percentiles, and more, but only for the columns matching the type filter requested (numeric by default). Running both together gives a complete first picture: structure from info(), distribution from describe().
How large an n should I pass to head() or tail() when inspecting a real dataset?
The default of 5 rows is usually enough for a quick sanity check, but for a larger clinical dataset with many subjects, passing a larger value such as head(20) can help confirm that data from multiple subjects or sites is represented correctly, not just the first few rows of a single case.
Summary
Before any pharmaceutical dataset is analysed, it should be inspected using a standard trio of Pandas functions: head() and tail() for a quick visual check of the first and last rows, info() for a structural summary of row counts, non-null counts, and data types, and describe() for a statistical summary of numeric columns (or categorical columns, via include='object'). Together with the shape attribute and columns.tolist(), these tools let a student catch type mismatches, missing values, and implausible readings — such as a text code like “BLQ” silently turning an entire concentration column into object type — before those problems can corrupt any later pharmacokinetic or safety analysis.
References
- Pharmacy Council of India (PCI) — B.Pharm Regulations, NEP 2020, BP101T Syllabus
- Python Software Foundation — Official Python Documentation, docs.python.org
- Pandas Development Team — DataFrame.head, DataFrame.tail, DataFrame.info, and DataFrame.describe API Reference, pandas.pydata.org
