Mathew K Analytics

Lesson 1 · Full-Length Courses

Pandas for Absolute Beginners (with Real Data)

Pandas is the most popular Python library for working with tables of data, similar to a spreadsheet or a database table. This lesson runs entirely inside a…

📓 Full notebook

Download .ipynb

Pandas for Absolute Beginners (with Real Data)#

  • Pandas is the most popular Python library for working with tables of data, similar to a spreadsheet or a database table.
  • This lesson runs entirely inside a Jupyter Notebook in VS Code, one cell at a time.
  • We start from the very basics, so no prior pandas experience is required.
  • We then build up to intermediate skills like grouping, merging, and cleaning messy data, using a real dataset of cars called mtcars.csv.
  • By the end, you will be comfortable loading, exploring, filtering, grouping, and summarizing real-world data with pandas.

Before You Start#

  • Make sure Python, VS Code, and the Python and Jupyter extensions are already installed.
  • Open a new Jupyter Notebook in VS Code and select your Python interpreter as the kernel.
  • Place mtcars.csv in the same folder as your notebook so pandas can find it with just its file name.
  • If pandas is not installed yet, open a terminal in VS Code and run: pip install pandas
import pandas as pd
import numpy as np
print(pd.__version__)
2.3.0

Beginner: The Series A Single Labelled Column#

  • A Series is a one-dimensional, labelled array think of it as a single column in a spreadsheet.
  • Every value in a Series has a label, called its index, even if you never set one yourself.
  • Series are the building block that DataFrames (full tables) are made of.
obj = pd.Series([4, 7, -5, 3])
obj
0    4
1    7
2   -5
3    3
dtype: int64
obj.values
obj.index
RangeIndex(start=0, stop=4, step=1)
obj2 = pd.Series([4, 7, -5, 3], index=['d', 'b', 'a', 'c'])
obj2
d    4
b    7
a   -5
c    3
dtype: int64
obj2['a']
np.int64(-5)
obj2['d'] = 6
obj2
d    6
b    7
a   -5
c    3
dtype: int64
obj2[obj2 > 0]
d    6
b    7
c    3
dtype: int64
obj2 * 2
d    12
b    14
a   -10
c     6
dtype: int64
print('b' in obj2)
print('e' in obj2)
True
False

Building a Series From a Dictionary#

  • Python dictionaries map naturally onto a Series: keys become the index, and values become the data.
sdata = {'Ohio': 35000, 'Texas': 71000, 'Oregon': 16000, 'Utah': 5000}
obj3 = pd.Series(sdata)
obj3
Ohio      35000
Texas     71000
Oregon    16000
Utah       5000
dtype: int64
obj3['Kansas'] = 18000
obj3
Ohio      35000
Texas     71000
Oregon    16000
Utah       5000
Kansas    18000
dtype: int64
del obj3['Kansas']
obj3
Ohio      35000
Texas     71000
Oregon    16000
Utah       5000
dtype: int64

Automatic Index Alignment#

  • When you combine two Series with math operators, pandas lines up the values by their labels first, not by position.
  • Labels that only exist in one of the two Series become missing values, shown as NaN (Not a Number).
data = {'California': None, 'Ohio': 35000, 'Oregon': 16000, 'Texas': 71000}
obj4 = pd.Series(data)
obj4
California        NaN
Ohio          35000.0
Oregon        16000.0
Texas         71000.0
dtype: float64
obj3 + obj4
California         NaN
Ohio           70000.0
Oregon         32000.0
Texas         142000.0
Utah               NaN
dtype: float64
result = obj4.add(obj3, fill_value=0)
result
California         NaN
Ohio           70000.0
Oregon         32000.0
Texas         142000.0
Utah            5000.0
dtype: float64

Beginner: The DataFrame A Full Table#

  • A DataFrame is a two-dimensional table made up of rows and columns, exactly like a spreadsheet.
  • Every column in a DataFrame is really a Series, and all the columns share the same row index.
  • Most real-world pandas work happens on DataFrames rather than single Series.
data = {'state': ['Ohio', 'Ohio', 'Ohio', 'Nevada', 'Nevada'],
        'year': [2000, 2001, 2002, 2001, 2002],
        'pop': [1.5, 1.7, 3.6, 2.4, 2.9]}
frame = pd.DataFrame(data)
frame
state year pop
0 Ohio 2000 1.5
1 Ohio 2001 1.7
2 Ohio 2002 3.6
3 Nevada 2001 2.4
4 Nevada 2002 2.9
frame2 = pd.DataFrame(data, columns=['year', 'state', 'pop', 'debt'],
                       index=['one', 'two', 'three', 'four', 'five'])
frame2
year state pop debt
one 2000 Ohio 1.5 NaN
two 2001 Ohio 1.7 NaN
three 2002 Ohio 3.6 NaN
four 2001 Nevada 2.4 NaN
five 2002 Nevada 2.9 NaN

Selecting Columns#

frame2['state']
one        Ohio
two        Ohio
three      Ohio
four     Nevada
five     Nevada
Name: state, dtype: object
frame2.year
one      2000
two      2001
three    2002
four     2001
five     2002
Name: year, dtype: int64

Selecting Rows With loc and iloc#

frame2.loc['three']
year     2002
state    Ohio
pop       3.6
debt      NaN
Name: three, dtype: object
frame2.iloc[2]
year     2002
state    Ohio
pop       3.6
debt      NaN
Name: three, dtype: object

Adding, Changing, and Removing Columns#

frame2['debt'] = np.arange(5.)
frame2
year state pop debt
one 2000 Ohio 1.5 0.0
two 2001 Ohio 1.7 1.0
three 2002 Ohio 3.6 2.0
four 2001 Nevada 2.4 3.0
five 2002 Nevada 2.9 4.0
val = pd.Series([-1.2, -1.5, -1.7], index=['two', 'four', 'five'])
frame2['debt'] = val
frame2
year state pop debt
one 2000 Ohio 1.5 NaN
two 2001 Ohio 1.7 -1.2
three 2002 Ohio 3.6 NaN
four 2001 Nevada 2.4 -1.5
five 2002 Nevada 2.9 -1.7
frame2['eastern'] = frame2.state == 'Ohio'
frame2
year state pop debt eastern
one 2000 Ohio 1.5 NaN True
two 2001 Ohio 1.7 -1.2 True
three 2002 Ohio 3.6 NaN True
four 2001 Nevada 2.4 -1.5 False
five 2002 Nevada 2.9 -1.7 False
del frame2['state']
frame2.columns
Index(['year', 'pop', 'debt', 'eastern'], dtype='object')

Dropping Rows and Columns With .drop()#

data2 = pd.DataFrame(np.arange(16).reshape((4, 4)),
                     index=['Ohio', 'Colorado', 'Utah', 'New York'],
                     columns=['one', 'two', 'three', 'four'])
data2
one two three four
Ohio 0 1 2 3
Colorado 4 5 6 7
Utah 8 9 10 11
New York 12 13 14 15
data2.drop(['Colorado', 'Ohio'])
one two three four
Utah 8 9 10 11
New York 12 13 14 15
data2.drop(['two', 'four'], axis=1)
one three
Ohio 0 2
Colorado 4 6
Utah 8 10
New York 12 14

Beginner: Building a DataFrame From a Nested Dictionary#

pop = {'Nevada': {2001: 2.4, 2002: 2.9},
       'Ohio': {2000: 1.5, 2001: 1.7, 2002: 3.6}}
frame3 = pd.DataFrame(pop)
frame3
Nevada Ohio
2001 2.4 1.7
2002 2.9 3.6
2000 NaN 1.5
frame3.index.name = 'year'
frame3.columns.name = 'state'
frame3
state Nevada Ohio
year
2001 2.4 1.7
2002 2.9 3.6
2000 NaN 1.5

Intermediate: Combining Tables With merge()#

  • pd.merge() joins two DataFrames together based on a shared column, exactly like a SQL JOIN.
  • The 'how' argument controls which rows survive when keys do not perfectly match on both sides.
left_frame = pd.DataFrame({'key': range(5), 'left_value': ['a', 'b', 'c', 'd', 'e']})
right_frame = pd.DataFrame({'key': range(2, 7), 'right_value': ['f', 'g', 'h', 'i', 'j']})
print(left_frame)
print(right_frame)
   key left_value
0    0          a
1    1          b
2    2          c
3    3          d
4    4          e
   key right_value
0    2           f
1    3           g
2    4           h
3    5           i
4    6           j
pd.merge(left_frame, right_frame, on='key', how='inner')
key left_value right_value
0 2 c f
1 3 d g
2 4 e h
pd.merge(left_frame, right_frame, on='key', how='left')
key left_value right_value
0 0 a NaN
1 1 b NaN
2 2 c f
3 3 d g
4 4 e h
pd.merge(left_frame, right_frame, on='key', how='right')
key left_value right_value
0 2 c f
1 3 d g
2 4 e h
3 5 NaN i
4 6 NaN j
pd.merge(left_frame, right_frame, on='key', how='outer')
key left_value right_value
0 0 a NaN
1 1 b NaN
2 2 c f
3 3 d g
4 4 e h
5 5 NaN i
6 6 NaN j

Intermediate: Stacking Tables With concat()#

  • pd.concat() stacks DataFrames together, either on top of each other or side by side.
  • Unlike merge, concat does not match on a key column it simply lines things up by position or by index.
pd.concat([left_frame, right_frame])
key left_value right_value
0 0 a NaN
1 1 b NaN
2 2 c NaN
3 3 d NaN
4 4 e NaN
0 2 NaN f
1 3 NaN g
2 4 NaN h
3 5 NaN i
4 6 NaN j
pd.concat([left_frame, right_frame], axis=1)
key left_value key right_value
0 0 a 2 f
1 1 b 3 g
2 2 c 4 h
3 3 d 5 i
4 4 e 6 j

Beginner: Working With a Real Dataset mtcars.csv#

  • mtcars is a classic dataset listing specifications for 32 different car models.
  • mpg: Miles per US gallon (fuel efficiency)
  • cyl: Number of cylinders in the engine
  • disp: Engine displacement in cubic inches
  • hp: Gross horsepower
  • drat: Rear axle ratio
  • wt: Weight, in thousands of pounds
  • qsec: Quarter-mile time in seconds
  • vs: Engine shape, 0 means V-shaped, 1 means straight
  • am: Transmission, 0 means automatic, 1 means manual
  • gear: Number of forward gears
  • carb: Number of carburetors
mtcars = pd.read_csv('mtcars.csv')
mtcars.head()
model mpg cyl disp hp drat wt qsec vs am gear carb
0 Mazda RX4 21.0 6 160.0 110 3.90 2.620 16.46 0 1 4 4
1 Mazda RX4 Wag 21.0 6 160.0 110 3.90 2.875 17.02 0 1 4 4
2 Datsun 710 22.8 4 108.0 93 3.85 2.320 18.61 1 1 4 1
3 Hornet 4 Drive 21.4 6 258.0 110 3.08 3.215 19.44 1 0 3 1
4 Hornet Sportabout 18.7 8 360.0 175 3.15 3.440 17.02 0 0 3 2
mtcars.shape
(32, 12)
mtcars.describe()
mpg cyl disp hp drat wt qsec vs am gear carb
count 32.000000 32.000000 32.000000 32.000000 32.000000 32.000000 32.000000 32.000000 32.000000 32.000000 32.0000
mean 20.090625 6.187500 230.721875 146.687500 3.596563 3.217250 17.848750 0.437500 0.406250 3.687500 2.8125
std 6.026948 1.785922 123.938694 68.562868 0.534679 0.978457 1.786943 0.504016 0.498991 0.737804 1.6152
min 10.400000 4.000000 71.100000 52.000000 2.760000 1.513000 14.500000 0.000000 0.000000 3.000000 1.0000
25% 15.425000 4.000000 120.825000 96.500000 3.080000 2.581250 16.892500 0.000000 0.000000 3.000000 2.0000
50% 19.200000 6.000000 196.300000 123.000000 3.695000 3.325000 17.710000 0.000000 0.000000 4.000000 2.0000
75% 22.800000 8.000000 326.000000 180.000000 3.920000 3.610000 18.900000 1.000000 1.000000 4.000000 4.0000
max 33.900000 8.000000 472.000000 335.000000 4.930000 5.424000 22.900000 1.000000 1.000000 5.000000 8.0000
mtcars.mean(numeric_only=True)
mpg      20.090625
cyl       6.187500
disp    230.721875
hp      146.687500
drat      3.596563
wt        3.217250
qsec     17.848750
vs        0.437500
am        0.406250
gear      3.687500
carb      2.812500
dtype: float64
mtcars[mtcars['am'] == 0]
model mpg cyl disp hp drat wt qsec vs am gear carb
3 Hornet 4 Drive 21.4 6 258.0 110 3.08 3.215 19.44 1 0 3 1
4 Hornet Sportabout 18.7 8 360.0 175 3.15 3.440 17.02 0 0 3 2
5 Valiant 18.1 6 225.0 105 2.76 3.460 20.22 1 0 3 1
6 Duster 360 14.3 8 360.0 245 3.21 3.570 15.84 0 0 3 4
7 Merc 240D 24.4 4 146.7 62 3.69 3.190 20.00 1 0 4 2
8 Merc 230 22.8 4 140.8 95 3.92 3.150 22.90 1 0 4 2
9 Merc 280 19.2 6 167.6 123 3.92 3.440 18.30 1 0 4 4
10 Merc 280C 17.8 6 167.6 123 3.92 3.440 18.90 1 0 4 4
11 Merc 450SE 16.4 8 275.8 180 3.07 4.070 17.40 0 0 3 3
12 Merc 450SL 17.3 8 275.8 180 3.07 3.730 17.60 0 0 3 3
13 Merc 450SLC 15.2 8 275.8 180 3.07 3.780 18.00 0 0 3 3
14 Cadillac Fleetwood 10.4 8 472.0 205 2.93 5.250 17.98 0 0 3 4
15 Lincoln Continental 10.4 8 460.0 215 3.00 5.424 17.82 0 0 3 4
16 Chrysler Imperial 14.7 8 440.0 230 3.23 5.345 17.42 0 0 3 4
20 Toyota Corona 21.5 4 120.1 97 3.70 2.465 20.01 1 0 3 1
21 Dodge Challenger 15.5 8 318.0 150 2.76 3.520 16.87 0 0 3 2
22 AMC Javelin 15.2 8 304.0 150 3.15 3.435 17.30 0 0 3 2
23 Camaro Z28 13.3 8 350.0 245 3.73 3.840 15.41 0 0 3 4
24 Pontiac Firebird 19.2 8 400.0 175 3.08 3.845 17.05 0 0 3 2
mtcars['mpg'].hist()
<Axes: >
No description has been provided for this image

Intermediate: Grouping Data With groupby()#

  • .groupby() splits a DataFrame into groups based on the values in one or more columns, then lets you summarize each group separately.
  • This is one of the single most useful tools in all of pandas, often described as 'split, apply, combine'.
grouped_by_carb = mtcars.groupby('carb')
grouped_by_carb.mean(numeric_only=True)
mpg cyl disp hp drat wt qsec vs am gear
carb
1 25.342857 4.571429 134.271429 86.0 3.681429 2.4900 19.507143 1.0 0.571429 3.571429
2 22.400000 5.600000 208.160000 117.2 3.699000 2.8628 18.186000 0.5 0.400000 3.800000
3 16.300000 8.000000 275.800000 180.0 3.070000 3.8600 17.666667 0.0 0.000000 3.000000
4 15.790000 7.200000 308.820000 187.0 3.596000 3.8974 16.965000 0.2 0.300000 3.600000
6 19.700000 6.000000 145.000000 175.0 3.620000 2.7700 15.500000 0.0 1.000000 5.000000
8 15.000000 8.000000 301.000000 335.0 3.540000 3.5700 14.600000 0.0 1.000000 5.000000
grouped_by_carb_am = mtcars.groupby(['carb', 'am'])
grouped_by_carb_am.mean(numeric_only=True)
mpg cyl disp hp drat wt qsec vs gear
carb am
1 0 20.333333 5.333333 201.033333 104.000000 3.180000 3.046667 19.890000 1.000000 3.000000
1 29.100000 4.000000 84.200000 72.500000 4.057500 2.072500 19.220000 1.000000 4.000000
2 0 19.300000 6.666667 278.250000 134.500000 3.291667 3.430000 18.523333 0.333333 3.333333
1 27.050000 4.000000 103.025000 91.250000 4.310000 2.012000 17.680000 0.750000 4.500000
3 0 16.300000 8.000000 275.800000 180.000000 3.070000 3.860000 17.666667 0.000000 3.000000
4 0 14.300000 7.428571 345.314286 198.000000 3.420000 4.329857 17.381429 0.285714 3.285714
1 19.266667 6.666667 223.666667 161.333333 4.006667 2.888333 15.993333 0.000000 4.333333
6 1 19.700000 6.000000 145.000000 175.000000 3.620000 2.770000 15.500000 0.000000 5.000000
8 1 15.000000 8.000000 301.000000 335.000000 3.540000 3.570000 14.600000 0.000000 5.000000
counts = grouped_by_carb_am['carb'].count()
counts
carb  am
1     0     3
      1     4
2     0     6
      1     4
3     0     3
4     0     7
      1     3
6     1     1
8     1     1
Name: carb, dtype: int64
grouped_stats = mtcars.groupby('carb')[['mpg', 'hp']].agg(['mean', 'std'])
grouped_stats
mpg hp
mean std mean std
carb
1 25.342857 6.001349 86.0 19.782147
2 22.400000 5.472152 117.2 43.964127
3 16.300000 1.053565 180.0 0.000000
4 15.790000 3.911081 187.0 62.949715
6 19.700000 NaN 175.0 NaN
8 15.000000 NaN 335.0 NaN
import matplotlib.pyplot as plt
df = counts.unstack()
ax = df.plot(kind='bar', stacked=True, figsize=(10, 5), colormap='viridis')
ax.set_ylabel('Count')
plt.show()
No description has been provided for this image

Try It Yourself: Search by Car Model#

  • We can combine string methods with boolean filtering to search for cars by name.
search_term = input('Enter part of a car model name to search for: ')
matches = mtcars[mtcars['model'].str.contains(search_term, case=False)]
matches[['model', 'mpg', 'hp']]
model mpg hp
19 Toyota Corolla 33.9 65
20 Toyota Corona 21.5 97

Intermediate: Arithmetic Between DataFrames and Series (Broadcasting)#

  • When you subtract a Series from a DataFrame, pandas 'broadcasts' the Series across every row automatically.
  • This avoids writing manual loops to repeat an operation across many rows or columns.
frame = pd.DataFrame(np.arange(12.).reshape((4, 3)),
                     columns=list('bde'),
                     index=['Utah', 'Ohio', 'Texas', 'Oregon'])
frame
b d e
Utah 0.0 1.0 2.0
Ohio 3.0 4.0 5.0
Texas 6.0 7.0 8.0
Oregon 9.0 10.0 11.0
series = frame.iloc[0]
frame - series
b d e
Utah 0.0 0.0 0.0
Ohio 3.0 3.0 3.0
Texas 6.0 6.0 6.0
Oregon 9.0 9.0 9.0
series3 = frame['d']
frame.sub(series3, axis=0)
b d e
Utah -1.0 0.0 1.0
Ohio -1.0 0.0 1.0
Texas -1.0 0.0 1.0
Oregon -1.0 0.0 1.0

Intermediate: A Closer Look at Indexing#

  • We have already used square brackets, .loc, and .iloc a little now let's understand exactly how each one behaves.
obj = pd.Series([4.5, 7.2, -5.3, 3.6], index=['d', 'b', 'a', 'c'])
obj
d    4.5
b    7.2
a   -5.3
c    3.6
dtype: float64
obj.reindex(list('abcde'))
a   -5.3
b    7.2
c    3.6
d    4.5
e    NaN
dtype: float64
obj.reindex(list('abcde'), fill_value=0)
a   -5.3
b    7.2
c    3.6
d    4.5
e    0.0
dtype: float64
obj3 = pd.Series(['blue', 'purple', 'yellow'], index=[0, 2, 4])
obj3.reindex(range(6), method='ffill')
0      blue
1      blue
2    purple
3    purple
4    yellow
5    yellow
dtype: object

Series Indexing and Slicing#

obj = pd.Series(np.arange(4.), index=['a', 'b', 'c', 'd'])
print(obj.iloc[1])
print(obj['b'])
1.0
1.0
obj['b':'c']
b    1.0
c    2.0
dtype: float64

DataFrame Indexing and Boolean Filtering#

data2['two']
Ohio         1
Colorado     5
Utah         9
New York    13
Name: two, dtype: int64
data2[['two', 'four']]
two four
Ohio 1 3
Colorado 5 7
Utah 9 11
New York 13 15
data2['Colorado':'Utah']
one two three four
Colorado 4 5 6 7
Utah 8 9 10 11
data2[:2]
one two three four
Ohio 0 1 2 3
Colorado 4 5 6 7
data2[data2['three'] > 5]
one two three four
Colorado 4 5 6 7
Utah 8 9 10 11
New York 12 13 14 15
data2[data2 < 5] = 0
data2
one two three four
Ohio 0 0 0 0
Colorado 0 5 6 7
Utah 8 9 10 11
New York 12 13 14 15

loc and iloc, Side by Side#

  • .loc[label] selects by label; .loc[[label1, label2]] selects a list of labels; .loc[start:end] selects an inclusive label slice.
  • .iloc[index] selects by integer position; .iloc[[i, j]] selects a list of positions; .iloc[start:end] selects an exclusive position slice, just like normal Python.
  • Both .loc and .iloc also accept a boolean condition to filter rows.
data2.loc['Colorado', ['two', 'three']]
two      5
three    6
Name: Colorado, dtype: int64
data2.iloc[2]
one       8
two       9
three    10
four     11
Name: Utah, dtype: int64
data2.loc[data2.three > 5, data2.columns[:3]]
one two three
Colorado 0 5 6
Utah 8 9 10
New York 12 13 14

Advanced (Optional): Hierarchical Indexing#

  • Hierarchical indexing lets a Series or DataFrame have more than one level of row or column labels at once.
  • This section is optional for absolute beginners feel free to skip ahead to Missing Data if you prefer, and come back later.
data = pd.Series(np.random.randn(10),
                 index=[['a', 'a', 'a', 'b', 'b', 'b', 'c', 'c', 'd', 'd'],
                        [1, 2, 3, 1, 2, 3, 1, 2, 2, 3]])
data
a  1    0.838915
   2    0.524399
   3    0.817581
b  1   -0.702521
   2    0.092964
   3   -0.423044
c  1    1.224144
   2   -0.167005
d  2    0.282914
   3   -0.573901
dtype: float64
data['b']
1   -0.702521
2    0.092964
3   -0.423044
dtype: float64
data[:, 2]
a    0.524399
b    0.092964
c   -0.167005
d    0.282914
dtype: float64
data.unstack()
1 2 3
a 0.838915 0.524399 0.817581
b -0.702521 0.092964 -0.423044
c 1.224144 -0.167005 NaN
d NaN 0.282914 -0.573901
frame = pd.DataFrame(np.arange(12).reshape((4, 3)), index=[['a', 'a', 'b', 'b'], [1, 2, 1, 2]],
                     columns=[['Ohio', 'Ohio', 'Colorado'], ['Green', 'Red', 'Green']])
frame.index.names = ['key1', 'key2']
frame.columns.names = ['state', 'colour']
frame
state Ohio Colorado
colour Green Red Green
key1 key2
a 1 0 1 2
2 3 4 5
b 1 6 7 8
2 9 10 11
frame['Ohio']
colour Green Red
key1 key2
a 1 0 1
2 3 4
b 1 6 7
2 9 10

Intermediate: Applying Your Own Functions#

  • Built-in pandas methods cover a lot, but sooner or later you will need a calculation pandas does not already provide.
  • .apply() runs your own function once per column (or per row), while .map() runs it on every individual value.
frame = pd.DataFrame(np.random.randn(4, 3), columns=list('bde'), index=['Utah', 'Ohio', 'Texas', 'Oregon'])
frame
b d e
Utah 1.025693 -2.067057 1.633511
Ohio -0.830899 1.129180 -0.503058
Texas -1.174636 -1.372666 -1.077913
Oregon 0.707075 -2.159143 -0.468626
f = lambda x: x.max() - x.min()
frame.apply(f)
b    2.200329
d    3.288324
e    2.711424
dtype: float64
format_fn = lambda x: '%.2f' % x
frame.map(format_fn)
b d e
Utah 1.03 -2.07 1.63
Ohio -0.83 1.13 -0.50
Texas -1.17 -1.37 -1.08
Oregon 0.71 -2.16 -0.47

Beginner: Handling Missing Data#

  • Real datasets almost always have gaps. Pandas represents a missing value as NaN.
  • Knowing how to detect, fill, and drop missing values is an essential, everyday pandas skill.
data = pd.Series([1, np.nan, 3.5, np.nan, 7])
data
0    1.0
1    NaN
2    3.5
3    NaN
4    7.0
dtype: float64
data.isnull()
0    False
1     True
2    False
3     True
4    False
dtype: bool
data.dropna()
0    1.0
2    3.5
4    7.0
dtype: float64
data.fillna(0)
0    1.0
1    0.0
2    3.5
3    0.0
4    7.0
dtype: float64
data.fillna(data.mean())
0    1.000000
1    3.833333
2    3.500000
3    3.833333
4    7.000000
dtype: float64
data.ffill()
0    1.0
1    1.0
2    3.5
3    3.5
4    7.0
dtype: float64
data.bfill()
0    1.0
1    3.5
2    3.5
3    7.0
4    7.0
dtype: float64

Missing Data in a Full DataFrame#

missing_df = pd.DataFrame([[1., 6.5, 3.], [1., np.nan, np.nan], [np.nan, np.nan, np.nan], [np.nan, 6.5, 3.]])
missing_df
0 1 2
0 1.0 6.5 3.0
1 1.0 NaN NaN
2 NaN NaN NaN
3 NaN 6.5 3.0
missing_df.dropna()
0 1 2
0 1.0 6.5 3.0
missing_df.dropna(how='all')
0 1 2
0 1.0 6.5 3.0
1 1.0 NaN NaN
3 NaN 6.5 3.0
df = pd.DataFrame(np.random.randn(7, 3))
df.iloc[:4, 1] = np.nan
df.iloc[:2, 2] = np.nan
df
0 1 2
0 1.254066 NaN NaN
1 1.455803 NaN NaN
2 -1.579892 NaN -0.359382
3 -0.189369 NaN 1.817148
4 -1.428140 -0.898397 -0.046185
5 -1.183165 -0.347626 0.746513
6 0.148022 -0.062821 -1.436010
df.dropna(thresh=2)
0 1 2
2 -1.579892 NaN -0.359382
3 -0.189369 NaN 1.817148
4 -1.428140 -0.898397 -0.046185
5 -1.183165 -0.347626 0.746513
6 0.148022 -0.062821 -1.436010

A Common Beginner Mistake: Index Objects Cannot Be Edited#

  • Unlike a list, a pandas Index is immutable once created, individual labels inside it cannot be changed directly.
  • The next cell deliberately triggers this error on purpose, so you recognize it immediately if you ever see it in your own code.
obj = pd.Series(range(3), index=list('abc'))
obj.index[1] = 'd'
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[89], line 2
      1 obj = pd.Series(range(3), index=list('abc'))
----> 2 obj.index[1] = 'd'

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\base.py:5383, in Index.__setitem__(self, key, value)
   5381 @final
   5382 def __setitem__(self, key, value) -> None:
-> 5383     raise TypeError("Index does not support mutable operations")

TypeError: Index does not support mutable operations

Intermediate: Catching Errors Gracefully#

  • Sometimes you want to detect a problem and handle it in your code, rather than letting the whole notebook stop.
  • Python's try/except lets you catch a specific error type and respond to it instead of crashing.
try:
    invalid = mtcars['not_a_real_column']
except KeyError as e:
    print('KeyError:', e)
KeyError: 'not_a_real_column'
try:
    bad_value = float('not a number')
except ValueError as e:
    print('ValueError:', e)
ValueError: could not convert string to float: 'not a number'

Best Practice: Wrapping Repeated Work in a Function#

  • If you find yourself writing the same few lines of pandas code repeatedly, turn them into a function.
  • This avoids copy-paste mistakes and makes your analysis easy to reuse on a different column or dataset.
def summarize_by_group(df, group_col, value_col):
    grouped = df.groupby(group_col)[value_col]
    summary = grouped.agg(['mean', 'std', 'count'])
    return summary.sort_values('mean', ascending=False)

summarize_by_group(mtcars, 'cyl', 'mpg')
mean std count
cyl
4 26.663636 4.509828 11
6 19.742857 1.453567 7
8 15.100000 2.560048 14

End-to-End Mini Project: Which Cars Are Most Fuel-Efficient?#

  • Let's combine everything from this lesson loading, filtering, grouping, and sorting into one small real analysis.
  • Question: is a manual or automatic transmission more fuel-efficient in this dataset, and how does cylinder count affect the answer?
mtcars['transmission'] = mtcars['am'].map({0: 'automatic', 1: 'manual'})
mtcars[['model', 'am', 'transmission']].head()
model am transmission
0 Mazda RX4 1 manual
1 Mazda RX4 Wag 1 manual
2 Datsun 710 1 manual
3 Hornet 4 Drive 0 automatic
4 Hornet Sportabout 0 automatic
efficiency_summary = mtcars.groupby('transmission')['mpg'].agg(['mean', 'min', 'max', 'count'])
efficiency_summary
mean min max count
transmission
automatic 17.147368 10.4 24.4 19
manual 24.392308 15.0 33.9 13
cyl_transmission_summary = mtcars.groupby(['cyl', 'transmission'])['mpg'].mean().round(1)
cyl_transmission_summary
cyl  transmission
4    automatic       22.9
     manual          28.1
6    automatic       19.1
     manual          20.6
8    automatic       15.0
     manual          15.4
Name: mpg, dtype: float64
most_efficient = mtcars.sort_values('mpg', ascending=False)[['model', 'mpg', 'cyl', 'transmission']].head(5)
most_efficient
model mpg cyl transmission
19 Toyota Corolla 33.9 4 manual
17 Fiat 128 32.4 4 manual
27 Lotus Europa 30.4 4 manual
18 Honda Civic 30.4 4 manual
25 Fiat X1-9 27.3 4 manual
output_path = 'mtcars_efficiency_summary.csv'
efficiency_summary.to_csv(output_path)
print(f'Summary saved to {output_path}')
Summary saved to mtcars_efficiency_summary.csv

Wrap-Up: What You Learned#

  • Series and DataFrames, pandas' core building blocks for labelled, one- and two-dimensional data.
  • Selecting, filtering, adding, and dropping rows and columns using square brackets, .loc, and .iloc.
  • Combining tables with merge() and concat(), and grouping and summarizing data with groupby() and .agg().
  • Reshaping data with hierarchical indexes, unstack(), and stack().
  • Detecting, filling, and dropping missing data with isnull(), fillna(), ffill(), bfill(), and dropna().
  • Applying your own custom functions with .apply() and .map().
  • Loading and saving real data with read_csv() and to_csv(), applied to the real mtcars dataset.
  • Practice prompt: pick a dataset of your own even a simple CSV export from a spreadsheet you already have and try repeating the mini project's question-and-answer approach on it.

Enjoyed This Lesson?#

  • If this pandas walkthrough helped you, consider subscribing it is free, and it is the best way to support more real-data tutorials like this one.
  • Drop a comment below with what you would like to see covered next: NumPy, data visualization, or something else entirely.
  • If you made it all the way to the end, give the video a like you just leveled up your pandas skills!

Found this useful?

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