The idea
Before you analyse anything, look at the data. Every experienced analyst runs the same quick health check on a new dataset, because data is almost never what the email said it would be: a column of numbers stored as text, a city missing for some customers, a status value nobody mentioned, a duplicated header row halfway down.
pandas gives you a small set of methods for this check. Together they take five minutes and save hours of confused analysis later. You'll use Toko Kita's sales.csv, one row per product in an order.
import pandas as pd
sales = pd.read_csv("sales.csv")
head and tail: eyeball some rows
The very first question about any dataset is "what does one row actually represent?". Is it one customer, one order, one product in an order? The quickest way to find out is to look at a few rows from the top and the bottom:
print(sales[["order_id", "customer", "product", "status"]].head())
print(sales[["order_id", "customer", "product", "status"]].tail(3))
order_id customer product status
0 1 Kartika Rahma Desk Organizer completed
1 2 Citra Dewi Arabica Gayo 250g completed
2 3 Eka Putri Pour-over Kettle completed
3 4 Citra Dewi Scented Candle completed
4 4 Citra Dewi Throw Pillow completed
order_id customer product status
68 39 Citra Dewi Toraja Kalosi 250g cancelled
69 40 Joko Susilo Arabica Gayo 250g cancelled
70 40 Joko Susilo Robusta Lampung 250g cancelled
.head() shows the first 5 rows (or .head(n)), .tail() the last ones. Already you learn something: order 4 appears twice, so each row is a product line, not an order. Misreading what one row represents is the most common analysis mistake of all.
.sample(5) shows random rows, which is useful because problems often hide in the middle of a file.
shape, columns and dtypes
Next: how big is it, what columns does it have, and what kind of data did pandas think each column holds? These three attributes answer that:
print(sales.shape)
print(list(sales.columns))
print(sales.dtypes)
(71, 12)
['order_id', 'order_date', 'customer', 'city', 'segment', 'product', 'category', 'quantity', 'unit_price', 'revenue', 'channel', 'status']
order_id int64
order_date object
customer object
city object
segment object
product object
category object
quantity int64
unit_price int64
revenue int64
channel object
status object
dtype: object
dtypes are the most important line of the health check:
int64: whole numbers.float64: decimals (and any numeric column with missing values).object: text, or a mix of things pandas couldn't type more precisely.bool,datetime64[ns]andcategoryappear once you convert columns.
Notice order_date is object, plain text, not a date. You can't filter by month or compute durations until it's converted (module 10). And if a column you expect to be numeric shows as object, some value in it isn't a number, maybe "N/A" or "1.250.000", and sums will fail or, worse, concatenate strings.
info: the one-screen summary
There's one method that covers most of the health check in a single call. It's usually the first thing analysts run after loading a file:
sales.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 71 entries, 0 to 70
Data columns (total 12 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 order_id 71 non-null int64
1 order_date 71 non-null object
2 customer 71 non-null object
3 city 67 non-null object
4 segment 71 non-null object
5 product 71 non-null object
6 category 71 non-null object
7 quantity 71 non-null int64
8 unit_price 71 non-null int64
9 revenue 71 non-null int64
10 channel 71 non-null object
11 status 71 non-null object
dtypes: int64(4), object(8)
...
.info() combines shape, dtypes and the non-null count per column. 71 rows, but city has only 67 non-null values, so 4 rows are missing a city. That one number tells you the column needs a decision before you group by city. (.info() prints directly, so you call it without print.) The last line, memory usage, depends on your computer, so it's left out above.
Counting missing values
To get missing counts as data you can use, combine .isna() (True where a value is missing) with .sum() (True counts as 1):
print(sales.isna().sum()[lambda counts: counts > 0])
city 4
dtype: int64
The [lambda counts: counts > 0] part keeps only columns that have missing values; you'll learn this filtering style in module 5. On its own, sales.isna().sum() lists every column.
describe: numbers at a glance
For the number columns, you want to know what range the values fall in, and whether anything looks impossible. .describe() gives you a statistical summary of every numeric column at once:
print(sales[["quantity", "unit_price", "revenue"]].describe())
quantity unit_price revenue
count 71.000000 71.000000 71.000000
mean 1.647887 140901.408451 246507.042254
std 0.794599 92952.699942 227977.935852
min 1.000000 38000.000000 38000.000000
25% 1.000000 65000.000000 89500.000000
50% 1.000000 110000.000000 150000.000000
75% 2.000000 185000.000000 320000.000000
max 3.000000 320000.000000 960000.000000
For each numeric column: the count, mean, standard deviation (spread), minimum, the 25th/50th/75th percentiles and the maximum. Read it like a detective:
- min and max catch impossible values: a negative quantity, a price of 0, an age of 200.
- mean vs 50% (the median): revenue's mean (246,507) is much higher than its median (150,000), so a few big orders pull the average up. That's a hint to report medians, too.
- count lower than the row count means missing values.
For text columns, .describe(include="object") shows the count, number of unique values, the most common value and how often it appears.
Categories: unique and value_counts
Text columns like status and category have a limited set of values, and you need to know what they are before you filter or group by them. Which statuses exist? How common is each one? Are there any surprises?
print(sales["status"].unique())
print(sales["status"].value_counts())
print(sales["category"].nunique())
['completed' 'cancelled' 'shipped' 'pending']
status
completed 45
cancelled 19
shipped 6
pending 1
Name: count, dtype: int64
5
.value_counts() is the analyst's favourite method: it counts each distinct value, sorted from most to least common. Here it reveals that 19 of 71 lines were cancelled, which matters enormously: any revenue total that doesn't filter out cancelled orders will be badly overstated. Add normalize=True to get proportions instead of counts.
Run value_counts() on every text column you plan to group or filter by. It surfaces typos ("Jakarta" vs "jakarta"), unexpected categories, and how much each one matters.
A health-check routine
Put it together and you have a routine for every new file:
df.shape # how big?
df.head() # what does a row mean?
df.info() # types and missing values
df.describe() # numeric ranges, outliers
df["col"].value_counts() # categories, typos
df.duplicated().sum() # exact duplicate rows
df.duplicated() marks rows that are exact copies of an earlier row; summing it counts them.
Why this matters for clean code
Analysis built on unexamined data is guesswork. Writing your health check as code (rather than scrolling through a spreadsheet) makes it repeatable: when next month's file arrives, you rerun the same checks in seconds and spot what changed. Many teams turn these checks into assertions at the top of their pipelines, like assert sales["revenue"].min() > 0, so bad data stops the job instead of flowing into a report.
Recap
.head(),.tail(),.sample()show rows. First, understand what one row represents..shape,.columns,.dtypesdescribe the structure. Text-typed numbers and dates need converting..info()shows types and non-null counts in one view;.isna().sum()counts missing values..describe()summarises numeric columns: watch min/max, and compare mean with median..unique(),.nunique(),.value_counts()explore categories..duplicated().sum()counts duplicate rows. Turn the routine into code you can rerun.