Mathew K Analytics

Lesson 4 · Pandas Full Course

Create and Inspect a Pandas DataFrame Like a Pro: info, describe, dtypes | Pandas Course #4

In this lesson you'll be able to: Build a DataFrame from a dictionary, a list of rows or an array Load a real CSV file and take a first look at it Read a…

What you'll learn

Datasets used in this lesson

Save these next to the notebook. In Google Colab, upload them with the 📁 icon on the left first.

📓 Full notebook

Download .ipynb

Pandas 04: Creating and Inspecting DataFrames#

In this lesson you'll be able to:

  • Build a DataFrame from a dictionary, a list of rows or an array
  • Load a real CSV file and take a first look at it
  • Read a table's structure with info, dtypes, columns and index
  • Summarise numbers with describe and categories with value_counts
  • Audit a dataset for gaps before you trust it Dataset: Palmer penguins (data/pandas/penguins.csv)

Warm-up: a Series is one labelled column#

Lesson 03 met Series and DataFrames. Load the file, build a Series.

import pandas as pd

df = pd.read_csv("data/pandas/penguins.csv")
weights = df["body_mass_g"].head(3).to_list()
mass = pd.Series(weights, name="body_mass_g")
print(mass)
print(mass.mean())
0    3750.0
1    3800.0
2    3250.0
Name: body_mass_g, dtype: float64
3600.0
print(mass.index)
print(mass.dtype)
RangeIndex(start=0, stop=3, step=1)
float64

Build a DataFrame from a dictionary#

Problem: turn a few values you already have into a proper table.

pick = df.iloc[[0, 1, 277, 152]]
data = {
    "species": pick["species"].to_list(),
    "island": pick["island"].to_list(),
    "body_mass_g": pick["body_mass_g"].to_list(),
}
small = pd.DataFrame(data)
small
species island body_mass_g
0 Adelie Torgersen 3750.0
1 Adelie Torgersen 3800.0
2 Gentoo Biscoe 5000.0
3 Chinstrap Dream 3500.0
tags = ["p1", "p2", "p3", "p4"]
tagged = pd.DataFrame(data, index=tags)
tagged
species island body_mass_g
p1 Adelie Torgersen 3750.0
p2 Adelie Torgersen 3800.0
p3 Gentoo Biscoe 5000.0
p4 Chinstrap Dream 3500.0

Common mistake: lists of different lengths

broken = {"species": df["species"].head(2).to_list(),
          "body_mass_g": df["body_mass_g"].head(3).to_list()}
pd.DataFrame(broken)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[6], line 3
      1 broken = {"species": df["species"].head(2).to_list(),
      2           "body_mass_g": df["body_mass_g"].head(3).to_list()}
----> 3 pd.DataFrame(broken)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py:769, in DataFrame.__init__(self, data, index, columns, dtype, copy)
    763     mgr = self._init_mgr(
    764         data, axes={"index": index, "columns": columns}, dtype=dtype, copy=copy
    765     )
    767 elif isinstance(data, dict):
    768     # GH#38939 de facto copy defaults to False only in non-dict cases
--> 769     mgr = dict_to_mgr(data, index, columns, dtype=dtype, copy=copy)
    770 elif isinstance(data, ma.MaskedArray):
    771     from numpy.ma import mrecords

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\internals\construction.py:460, in dict_to_mgr(data, index, columns, dtype, copy)
    441 if copy:
    442     # We only need to copy arrays that will not get consolidated, i.e.
    443     #  only EA arrays
    444     arrays = [
    445         (
    446             x.copy()
   (...)
    457         for x in arrays
    458     ]
--> 460 return arrays_to_mgr(arrays, columns, index, dtype=dtype, consolidate=copy)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\internals\construction.py:113, in arrays_to_mgr(arrays, columns, index, dtype, verify_integrity, consolidate)
    110 if verify_integrity:
    111     # figure out the index, if necessary
    112     if index is None:
--> 113         index = _extract_index(arrays)
    114     else:
    115         index = ensure_index(index)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\internals\construction.py:643, in _extract_index(data)
    641 if have_raw_arrays:
    642     if len(raw_lengths) > 1:
--> 643         raise ValueError("All arrays must be of the same length")
    645     if have_dicts:
    646         raise ValueError(
    647             "Mixing dicts with non-Series may lead to ambiguous ordering."
    648         )

ValueError: All arrays must be of the same length
fixed = {"species": df["species"].head(3).to_list(),
         "body_mass_g": df["body_mass_g"].head(3).to_list()}
pd.DataFrame(fixed)
species body_mass_g
0 Adelie 3750.0
1 Adelie 3800.0
2 Adelie 3250.0

Quick check: how many rows and columns does small have?

print(small.shape)
print(len(small))
(4, 3)
4

Other ways in: rows, arrays and chosen columns#

Problem: your data arrives as rows or as a NumPy array, not as columns.

bits = df[["species", "bill_length_mm"]].iloc[2:4]
rows = bits.to_dict("records")
print(rows)
pd.DataFrame(rows)
[{'species': 'Adelie', 'bill_length_mm': 40.3}, {'species': 'Adelie', 'bill_length_mm': nan}]
species bill_length_mm
0 Adelie 40.3
1 Adelie NaN
cols = ["bill_length_mm", "bill_depth_mm"]
arr = df[cols].head(3).to_numpy()
print(arr)
bills = pd.DataFrame(arr, columns=cols)
bills
[[39.1 18.7]
 [39.5 17.4]
 [40.3 18. ]]
bill_length_mm bill_depth_mm
0 39.1 18.7
1 39.5 17.4
2 40.3 18.0
pd.DataFrame(data, columns=["body_mass_g", "species"])
body_mass_g species
0 3750.0 Adelie
1 3800.0 Adelie
2 5000.0 Gentoo
3 3500.0 Chinstrap

Your turn#

Build a two-column table of islands and penguin counts from df["island"].value_counts().

counts = df["island"].value_counts()
islands = pd.DataFrame({
    "island": counts.index.to_list(),
    "penguins": counts.to_list(),
})
islands
island penguins
0 Biscoe 168
1 Dream 124
2 Torgersen 52
print(islands["penguins"].sum())
344

Take a first look at a real file#

Problem: open a file of hundreds of rows without drowning in it.

print(type(df))
print(df.shape)
<class 'pandas.DataFrame'>
(344, 7)
df.head()
species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex
0 Adelie Torgersen 39.1 18.7 181.0 3750.0 MALE
1 Adelie Torgersen 39.5 17.4 186.0 3800.0 FEMALE
2 Adelie Torgersen 40.3 18.0 195.0 3250.0 FEMALE
3 Adelie Torgersen NaN NaN NaN NaN NaN
4 Adelie Torgersen 36.7 19.3 193.0 3450.0 FEMALE
df.tail(3)
species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex
341 Gentoo Biscoe 50.4 15.7 222.0 5750.0 MALE
342 Gentoo Biscoe 45.2 14.8 212.0 5200.0 FEMALE
343 Gentoo Biscoe 49.9 16.1 213.0 5400.0 MALE
df.sample(4, random_state=42)
species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex
194 Chinstrap Dream 50.9 19.1 196.0 3550.0 MALE
157 Chinstrap Dream 45.2 17.8 198.0 3950.0 FEMALE
225 Gentoo Biscoe 46.5 13.5 210.0 4550.0 FEMALE
208 Chinstrap Dream 45.2 16.6 191.0 3250.0 FEMALE
cols = ["species", "island", "body_mass_g", "sex"]
df[cols].head()
species island body_mass_g sex
0 Adelie Torgersen 3750.0 MALE
1 Adelie Torgersen 3800.0 FEMALE
2 Adelie Torgersen 3250.0 FEMALE
3 Adelie Torgersen NaN NaN
4 Adelie Torgersen 3450.0 FEMALE

Common mistake: the wrong file path

path = "data/penguins.csv"
pd.read_csv(path)
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
Cell In[19], line 2
      1 path = "data/penguins.csv"
----> 2 pd.read_csv(path)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\io\parsers\readers.py:872, in read_csv(filepath_or_buffer, sep, delimiter, header, names, index_col, usecols, dtype, engine, converters, true_values, false_values, skipinitialspace, skiprows, skipfooter, nrows, na_values, keep_default_na, na_filter, skip_blank_lines, parse_dates, date_format, dayfirst, cache_dates, iterator, chunksize, compression, thousands, decimal, lineterminator, quotechar, quoting, doublequote, escapechar, comment, encoding, encoding_errors, dialect, on_bad_lines, low_memory, memory_map, float_precision, storage_options, dtype_backend)
    860 kwds_defaults = _refine_defaults_read(
    861     dialect,
    862     delimiter,
   (...)
    868     dtype_backend=dtype_backend,
    869 )
    870 kwds.update(kwds_defaults)
--> 872 return _read(filepath_or_buffer, kwds)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\io\parsers\readers.py:300, in _read(filepath_or_buffer, kwds)
    297 _validate_names(kwds.get("names", None))
    299 # Create the parser.
--> 300 parser = TextFileReader(filepath_or_buffer, **kwds)
    302 if chunksize or iterator:
    303     return parser

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\io\parsers\readers.py:1643, in TextFileReader.__init__(self, f, engine, **kwds)
   1640     self.options["has_index_names"] = kwds["has_index_names"]
   1642 self.handles: IOHandles | None = None
-> 1643 self._engine = self._make_engine(f, self.engine)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\io\parsers\readers.py:1907, in TextFileReader._make_engine(self, f, engine)
   1905     if "b" not in mode:
   1906         mode += "b"
-> 1907 self.handles = get_handle(
   1908     f,
   1909     mode,
   1910     encoding=self.options.get("encoding", None),
   1911     compression=self.options.get("compression", None),
   1912     memory_map=self.options.get("memory_map", False),
   1913     is_text=is_text,
   1914     errors=self.options.get("encoding_errors", "strict"),
   1915     storage_options=self.options.get("storage_options", None),
   1916 )
   1917 assert self.handles is not None
   1918 f = self.handles.handle

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\io\common.py:930, in get_handle(path_or_buf, mode, encoding, compression, memory_map, is_text, errors, storage_options)
    925 elif isinstance(handle, str):
    926     # Check whether the filename is to be opened in binary mode.
    927     # Binary mode does not support 'encoding' and 'newline'.
    928     if ioargs.encoding and "b" not in ioargs.mode:
    929         # Encoding
--> 930         handle = open(
    931             handle,
    932             ioargs.mode,
    933             encoding=ioargs.encoding,
    934             errors=errors,
    935             newline="",
    936         )
    937     else:
    938         # Binary mode
    939         handle = open(handle, ioargs.mode)

FileNotFoundError: [Errno 2] No such file or directory: 'data/penguins.csv'
import os
print(sorted(os.listdir("data/pandas"))[:6])
print(os.path.exists("data/pandas/penguins.csv"))
['aapl.csv', 'diamonds.csv', 'flights.csv', 'gapminder.csv', 'housing.csv', 'iris.csv']
True

Read the structure: info, dtypes, columns and index#

Problem: know every column's type and how many values are missing.

df.info()
<class 'pandas.DataFrame'>
RangeIndex: 344 entries, 0 to 343
Data columns (total 7 columns):
 #   Column             Non-Null Count  Dtype  
---  ------             --------------  -----  
 0   species            344 non-null    str    
 1   island             344 non-null    str    
 2   bill_length_mm     342 non-null    float64
 3   bill_depth_mm      342 non-null    float64
 4   flipper_length_mm  342 non-null    float64
 5   body_mass_g        342 non-null    float64
 6   sex                333 non-null    str    
dtypes: float64(4), str(3)
memory usage: 24.9 KB
df.dtypes
species                  str
island                   str
bill_length_mm       float64
bill_depth_mm        float64
flipper_length_mm    float64
body_mass_g          float64
sex                      str
dtype: object
print(df.columns.tolist())
print(df.index)
['species', 'island', 'bill_length_mm', 'bill_depth_mm', 'flipper_length_mm', 'body_mass_g', 'sex']
RangeIndex(start=0, stop=344, step=1)

Quick check: which column has the most missing values?

df.isna().sum()
species               0
island                0
bill_length_mm        2
bill_depth_mm         2
flipper_length_mm     2
body_mass_g           2
sex                  11
dtype: int64
missing_pct = df.isna().mean() * 100
missing_pct.round(1)
species              0.0
island               0.0
bill_length_mm       0.6
bill_depth_mm        0.6
flipper_length_mm    0.6
body_mass_g          0.6
sex                  3.2
dtype: float64
mem = df.memory_usage(deep=True)
print(mem.sum())
25465

Your turn#

List only the number columns. Hint: select_dtypes.

nums = df.select_dtypes(include="number")
print(nums.columns.tolist())
print(nums.shape)
['bill_length_mm', 'bill_depth_mm', 'flipper_length_mm', 'body_mass_g']
(344, 4)

Common mistake: calling an attribute like a method

df.shape()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[28], line 1
----> 1 df.shape()

TypeError: 'tuple' object is not callable
print(df.shape)
print(df.ndim, df.size)
(344, 7)
2 2408

Summarise numbers and categories#

Problem: describe a whole table in two or three lines of code.

df.describe().round(1)
bill_length_mm bill_depth_mm flipper_length_mm body_mass_g
count 342.0 342.0 342.0 342.0
mean 43.9 17.2 200.9 4201.8
std 5.5 2.0 14.1 802.0
min 32.1 13.1 172.0 2700.0
25% 39.2 15.6 190.0 3550.0
50% 44.4 17.3 197.0 4050.0
75% 48.5 18.7 213.0 4750.0
max 59.6 21.5 231.0 6300.0
df.describe(exclude="number")
species island sex
count 344 344 333
unique 3 3 2
top Adelie Biscoe MALE
freq 152 168 168
df["species"].value_counts()
species
Adelie       152
Gentoo       124
Chinstrap     68
Name: count, dtype: int64
df["island"].value_counts(normalize=True).round(3)
island
Biscoe       0.488
Dream        0.360
Torgersen    0.151
Name: proportion, dtype: float64
df["sex"].value_counts(dropna=False)
sex
MALE      168
FEMALE    165
NaN        11
Name: count, dtype: int64
df.nunique()
species                3
island                 3
bill_length_mm       164
bill_depth_mm         80
flipper_length_mm     55
body_mass_g           94
sex                    2
dtype: int64

Quick check: which three penguins are the heaviest, and what species are they?

heavy = df.nlargest(3, "body_mass_g")
heavy[cols]
species island body_mass_g sex
237 Gentoo Biscoe 6300.0 MALE
253 Gentoo Biscoe 6050.0 MALE
297 Gentoo Biscoe 6000.0 MALE

Your turn#

Find the shortest and longest flipper, and the average, rounded to 1 place.

flip = df["flipper_length_mm"]
print(flip.min(), flip.max())
print(round(flip.mean(), 1))
172.0 231.0
200.9
import matplotlib.pyplot as plt
ax = flip.plot(kind="hist", bins=20, figsize=(7, 4))
ax.set_title("Flipper length of 342 penguins")
ax.set_xlabel("Flipper length (mm)")
ax.set_ylabel("Number of penguins")
plt.show()
No description has been provided for this image

Mini-project: is this dataset ready to analyse?#

Plan: find incomplete rows, see who they are, drop them, compare.

gaps = df[df.isna().any(axis=1)]
print(len(gaps))
gaps[cols]
11
species island body_mass_g sex
3 Adelie Torgersen NaN NaN
8 Adelie Torgersen 3475.0 NaN
9 Adelie Torgersen 4250.0 NaN
10 Adelie Torgersen 3300.0 NaN
11 Adelie Torgersen 3700.0 NaN
47 Adelie Dream 2975.0 NaN
246 Gentoo Biscoe 4100.0 NaN
286 Gentoo Biscoe 4650.0 NaN
324 Gentoo Biscoe 4725.0 NaN
336 Gentoo Biscoe 4875.0 NaN
339 Gentoo Biscoe NaN NaN
gaps["species"].value_counts()
species
Adelie    6
Gentoo    5
Name: count, dtype: int64
complete = df.dropna()
print(df.shape, complete.shape)
print(round(len(complete) / len(df) * 100, 1))
(344, 7) (333, 7)
96.8
print(complete.isna().sum().sum())
print(complete.index[:12].tolist())
0
[0, 1, 2, 4, 5, 6, 7, 12, 13, 14, 15, 16]
audit = pd.DataFrame({
    "before": df["species"].value_counts(),
    "after": complete["species"].value_counts(),
})
audit["kept_pct"] = (audit["after"] / audit["before"] * 100)
audit.round(1)
before after kept_pct
species
Adelie 152 146 96.1
Gentoo 124 119 96.0
Chinstrap 68 68 100.0
ax = audit[["before", "after"]].plot(kind="bar", rot=0,
                                     figsize=(7, 4))
ax.set_title("Penguins per species, before and after dropna")
ax.set_xlabel("Species")
ax.set_ylabel("Number of penguins")
plt.show()
No description has been provided for this image
lost = len(df) - len(complete)
worst = audit["kept_pct"].idxmin()
print(f"Dropped {lost} rows; {worst} lost the most.")
Dropped 11 rows; Gentoo lost the most.

Recap#

  • pd.DataFrame builds tables from dicts, lists of rows or arrays
  • Every column list must have the same length
  • head, tail and sample peek; info and dtypes show structure
  • isna().sum() counts gaps; shape and size need no brackets
  • describe and value_counts summarise numbers and categories

Challenges#

  1. Build a DataFrame of 3 penguins from a list of dicts, one with a missing sex.
  2. Use value_counts to count penguins per island and sex together.
  3. Load data/pandas/tips.csv and audit it the same way.

What's next#

Lesson 05: Basic DataFrame Operations and Attributes Add, remove and calculate columns, and use every key attribute.

Found this useful?

All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.