DataLens

Inspecting data (head, info, describe, dtypes)

The five-minute health check you run on every new dataset: its size, columns, types, missing values, ranges and categories.

Level: Foundations · 11 min read

Practise this module

The questions for this lesson run in your browser and check your answer straight away.

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:

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:

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.

Key idea

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

Practise this module

The questions for this lesson run in your browser and check your answer straight away.

Next: Selecting columns and rows →