Tables (pandas)#
Last updated: 2026-02-12 16:03:09
Introduction#
Tables are a fundamental data structure, common to many software interfaces, such as spreadsheet programs, relational databases, statistical software, etc. In general, a table is a two-dimensional structure where:
Columns represent variables
Rows represent observations
Accordingly, the data in a given table column is usually of the same type. For example, a text column may be used to store product names, and a numeric column may be used for storing product prices. Conversely, the data in a given row may comprise mixed types, such as the product name (text) and the price (a number) of a specific product.
In this chapter we introduce methods for working with tables in Python, through a popular third-party package named pandas, introducing two table-related data structures: Series and DataFrame. As we will see, the latter data structures are closely related to numpy, which was covered in the previous chapter (see Arrays (numpy)), since the data in each table column is actually stored as an “extended” one-dimensional numpy array. We are going to cover standard table-related operations, such as:
Creating tables (see Table from scratch and Reading from file)
Examining table properties (see DataFrame properties)
Modifying tables (see Renaming columns, Modifying the index, Sorting, Operators, and Creating new columns)
Subsetting tables (see Subsetting Series, Subsetting DataFrames, and DataFrame filtering)
Conversion to simpler data structures (see Series/DataFrame to ndarray and Conversion to list)
Plotting table data (see Plotting (pandas))
Exporting a table to file (see Writing DataFrame to file)
In the next chapter, we are going to discuss more advanced operations with tables, such as table aggregation and table joins (see Table reshaping and joins). Later on, we are going to learn about the geopandas package for working with vector layers, which is an extension of pandas (see Vector layers (geopandas) and Geometric operations).
What is pandas?#
pandas is a popular Python package which provides a flexible set of tools for working with tabular data. Since tables are fundamental in data analysis, pandas is one of the most important packages for data analysis in Python. Moreover, as we will see later on in the book (see Vector layers (geopandas)), a vector layer is represented by an “extended” table—a table that has a “geometry” column. Accordingly, the package for working with vector layers, named geopandas, is an extension of pandas.
The pandas package defines two fundamental data structures:
Series—A one-dimensional structure, representing a table columnDataFrame—A two-dimensional structure, represent a table
As we will see shortly, pandas is essentially an extension of numpy. A pandas table (a data structure named DataFrame) is a collection of Series representing the table columns, whereas Series are actually nothing more than labelled numpy arrays. To repeat, these two data structures therefore form a hierarchy: each column in a DataFrame is a Series.
Note
For those familiar with the R programming language, the term DataFrame will sound familiar, resembling R’s own data structure for representing tables (the data.frame). Indeed, pandas borrowed many ideas from the R language. A pairwise comparison of pandas vs. R functionality can be found in the pandas documentation Comparison with R / R libraries.
Table from scratch#
Creating a Series#
To understand how a DataFrame is structured, we will experiment with creating one from scratch. First we need to load the pandas package, as well as numpy which we use in some of the examples:
import numpy as np
import pandas as pd
A Series can be created, from a list, or from an ndarray, using pd.Series. For example, the following expression creates a Series from a list containing three strings, the names of three railway stations in southern Israel:
name = pd.Series(['Beer-Sheva Center', 'Beer-Sheva University', 'Dimona'])
name
0 Beer-Sheva Center
1 Beer-Sheva University
2 Dimona
dtype: object
Note
In pandas, strings are always stored as 'object' arrays (Series), unlike numpy which has specialized native data types for strings.
The following expression creates another Series, named lines, containing the number of railway lines going through the latter stations. This time, we create the Series from an ndarray:
lines = pd.Series(np.array([4, 5, 1]))
lines
0 4
1 5
2 1
dtype: int64
A Series object is essentially a numpy array, combined with indices. Here is how we can access each of these components, separately:
lines.to_numpy() ## Series values, as 'ndarray'
array([4, 5, 1])
lines.index ## Series index
RangeIndex(start=0, stop=3, step=1)
As you can see, Series values comprise a numpy array, which we are already familiar with. Series indices, however, comprise a special type of object used for indexing in pandas:
type(name.index)
pandas.core.indexes.range.RangeIndex
By default, the index is just a sequence of consecutive integers (such as in names and lines). When necessary, we can define any other sequence of values as the Series index (see Modifying the index), and then use that index to access specific values (see Subsetting Series).
Index objects are similar to arrays in many ways. For example, they have .shape and .dtype attributes, just like arrays do:
lines.index.shape
(3,)
lines.index.dtype
dtype('int64')
Creating a DataFrame#
While a Series can be thought of as an extended one-dimensional array, a DataFrame can be thought of as a collection of Series comprising table columns. Unlike a two-dimensional numpy array, a DataFrame can be composed of different types of values (among the columns). Consequently:
a
DataFramecolumn (i.e., aSeries) contains values of the same type, usually representing a particular variablea
DataFramerow may contain values of different types, usually representing a specific observation
Since a DataFrame is two-dimensional, it has two sets of indices, row and column indices, just like a two-dimensional array. It is important to note that all Series that comprise the columns of a DataFrame share the same index.
A DataFrame can be created from scratch, using the pd.DataFrame function. To do that, let’s create four more Series which are going to comprise the DataFrame columns. Altogether, the DataFrame is going to represent various properties of three railway stations:
city = pd.Series(['Beer-Sheva', 'Beer-Sheva', 'Dimona'])
piano = pd.Series([False, True, False])
lon = pd.Series([34.798443, 34.812831, 35.011635])
lat = pd.Series([31.243288, 31.260284, 31.068616])
Here is a printout of the four Series we just created:
city
0 Beer-Sheva
1 Beer-Sheva
2 Dimona
dtype: object
piano
0 False
1 True
2 False
dtype: bool
lon
0 34.798443
1 34.812831
2 35.011635
dtype: float64
lat
0 31.243288
1 31.260284
2 31.068616
dtype: float64
Now, the six different Series can be combined into a DataFrame by passing them, as a dict (see Dictionaries (dict)), to the pd.DataFrame function. That way, the dict names will form the DataFrame column names, while the corresponding Series will form the column contents:
d = {
'name': name,
'city': city,
'lines': lines,
'piano': piano,
'lon': lon,
'lat': lat
}
stations = pd.DataFrame(d)
stations
| name | city | lines | piano | lon | lat | |
|---|---|---|---|---|---|---|
| 0 | Beer-Sheva Center | Beer-Sheva | 4 | False | 34.798443 | 31.243288 |
| 1 | Beer-Sheva University | Beer-Sheva | 5 | True | 34.812831 | 31.260284 |
| 2 | Dimona | Dimona | 1 | False | 35.011635 | 31.068616 |
Note
Note that there are other methods to create a DataFrame, passing different types of input to the pd.DataFrame function. For example, another useful method of creating a DataFrame is passing a two-dimensional array to pd.DataFrame.
DataFrame row indices are analogous to Series indices, and accessed exactly the same way:
stations.index
RangeIndex(start=0, stop=3, step=1)
Additionally, a DataFrame has column indices, which function as column names and can be accessed through the .columns property:
stations.columns
Index(['name', 'city', 'lines', 'piano', 'lon', 'lat'], dtype='object')
Reading from file#
In practice, we rarely need to create a DataFrame from scratch. More often, we read a table from a file, e.g., a CSV file.
The pd.read_csv function is used to read a CSV file into a DataFrame in our Python environment. For example, the following expression reads the CSV file named 'ZonAnn.Ts+dSST.csv', which contains global temperature data from NASA’s Goddard Institute for Space Studies (GISS). The temperature values in the table are anomalies, i.e., deviations from the 1951-1980 mean, in the years 1880-2024 ('Year'), both globally ('Glob') and for specific latitudes ('NHem'=Nothern Hemishpere, '24N-90N'=latitudes 24N-90N, etc.):
dat = pd.read_csv('data/ZonAnn.Ts+dSST.csv')
dat
| Year | Glob | NHem | SHem | 24N-90N | ... | EQU-24N | 24S-EQU | 44S-24S | 64S-44S | 90S-64S | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1880 | -0.18 | -0.31 | -0.04 | -0.40 | ... | -0.17 | -0.09 | -0.04 | 0.05 | 0.69 |
| 1 | 1881 | -0.09 | -0.19 | 0.01 | -0.36 | ... | 0.08 | 0.12 | -0.05 | -0.07 | 0.62 |
| 2 | 1882 | -0.11 | -0.23 | -0.01 | -0.32 | ... | -0.07 | -0.04 | 0.01 | 0.04 | 0.65 |
| 3 | 1883 | -0.17 | -0.29 | -0.06 | -0.35 | ... | -0.19 | -0.14 | -0.04 | 0.07 | 0.52 |
| 4 | 1884 | -0.28 | -0.44 | -0.14 | -0.61 | ... | -0.14 | -0.16 | -0.18 | -0.02 | 0.67 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 140 | 2020 | 1.00 | 1.34 | 0.67 | 1.66 | ... | 0.87 | 0.81 | 0.59 | 0.40 | 0.85 |
| 141 | 2021 | 0.84 | 1.14 | 0.55 | 1.42 | ... | 0.72 | 0.57 | 0.73 | 0.33 | 0.29 |
| 142 | 2022 | 0.89 | 1.16 | 0.62 | 1.52 | ... | 0.62 | 0.49 | 0.79 | 0.40 | 1.08 |
| 143 | 2023 | 1.17 | 1.49 | 0.85 | 1.78 | ... | 1.08 | 1.03 | 0.90 | 0.46 | 0.64 |
| 144 | 2024 | 1.28 | 1.65 | 0.91 | 1.95 | ... | 1.21 | 1.15 | 0.88 | 0.55 | 0.68 |
145 rows × 15 columns
Note
pandas has several functions named pd.read_* to read formats other than CSV, such as pd.read_excel, pd.read_sql, pd.read_json, pd.read_spss, and pd.read_stata. Similarly, DataFrame objects have multiple methods (see Writing DataFrame to file) to write to various output formats. See the IO tools section in the pandas user guide for an overview of the input/output capabilities.
Note that rows and columns are labelled using indices, as shown above. In this case:
Rows are labelled with consecutive integers
Columns are labelled with column names, which were imported from the first row of CSV file
DataFrame properties#
Overview#
One of the first things we may want to do with a DataFrame imported from a file is to examine its properties, as shown in the next few sections:
DataFrame dimensions#
DataFrame dimensions are accessible through the .shape property, just like in a numpy array (see Array dimensions). For example, dat (with the temperature data) has 145 rows and 15 columns:
dat.shape
(145, 15)
DataFrame column names#
The DataFrame column names can be obtained through the .columns property. Recall that this is a special data structure representing pandas indices (see Table from scratch):
dat.columns
Index(['Year', 'Glob', 'NHem', 'SHem', '24N-90N', '24S-24N', '90S-24S',
'64N-90N', '44N-64N', '24N-44N', 'EQU-24N', '24S-EQU', '44S-24S',
'64S-44S', '90S-64S'],
dtype='object')
Exercise 05-a
From your experience with Python so far, can you guess how can the columns names be transformed to a plain
list? (You can also try searching for the answer online.)
DataFrame column types#
The data types (see Data types) of the columns are contained in the .dtypes property:
dat.dtypes
Year int64
Glob float64
NHem float64
SHem float64
24N-90N float64
...
EQU-24N float64
24S-EQU float64
44S-24S float64
64S-44S float64
90S-64S float64
Length: 15, dtype: object
Note that this is a Series (of dtype objects). This means we can get a specific value by index using the ordinary Series subsetting methods which we learn about later on (see Subsetting Series). For example, the 'Year' column was imported into a Series of type int64:
dat.dtypes.loc['Year']
dtype('int64')
while the 'Glob' column (global temperature anomaly) was imported into a float64 column:
dat.dtypes.loc['Glob']
dtype('float64')
The basic pandas data types are in agreement with numpy data types (see Data types). For example, the dat table contains int64 and float64 data types. However, pandas and its extensions (such as geopandas, see What is geopandas?) also extend the numpy functionality with new data types (such as GeometryDtype, see GeoSeries (geometry) column).
Renaming columns#
Sometimes it is necessary to rename DataFrame columns. For example, we may wish to use shorter names which are easier to type, or make the columns match another table we are working with. Columns can be renamed using the .rename method, which accepts a columns argument of the form {'old_name':'new_name',...}. For example, here is how we can replace the 'Year' column name with lowercase 'year':
dat.rename(columns={'Year': 'year'})
| year | Glob | NHem | SHem | 24N-90N | ... | EQU-24N | 24S-EQU | 44S-24S | 64S-44S | 90S-64S | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1880 | -0.18 | -0.31 | -0.04 | -0.40 | ... | -0.17 | -0.09 | -0.04 | 0.05 | 0.69 |
| 1 | 1881 | -0.09 | -0.19 | 0.01 | -0.36 | ... | 0.08 | 0.12 | -0.05 | -0.07 | 0.62 |
| 2 | 1882 | -0.11 | -0.23 | -0.01 | -0.32 | ... | -0.07 | -0.04 | 0.01 | 0.04 | 0.65 |
| 3 | 1883 | -0.17 | -0.29 | -0.06 | -0.35 | ... | -0.19 | -0.14 | -0.04 | 0.07 | 0.52 |
| 4 | 1884 | -0.28 | -0.44 | -0.14 | -0.61 | ... | -0.14 | -0.16 | -0.18 | -0.02 | 0.67 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 140 | 2020 | 1.00 | 1.34 | 0.67 | 1.66 | ... | 0.87 | 0.81 | 0.59 | 0.40 | 0.85 |
| 141 | 2021 | 0.84 | 1.14 | 0.55 | 1.42 | ... | 0.72 | 0.57 | 0.73 | 0.33 | 0.29 |
| 142 | 2022 | 0.89 | 1.16 | 0.62 | 1.52 | ... | 0.62 | 0.49 | 0.79 | 0.40 | 1.08 |
| 143 | 2023 | 1.17 | 1.49 | 0.85 | 1.78 | ... | 1.08 | 1.03 | 0.90 | 0.46 | 0.64 |
| 144 | 2024 | 1.28 | 1.65 | 0.91 | 1.95 | ... | 1.21 | 1.15 | 0.88 | 0.55 | 0.68 |
145 rows × 15 columns
Note
Another column renaming technique is to assign a list of strings of the same length as the number of columns to the .columns attribute, as in dat.columns=['col1','col2',...].
Subsetting in pandas#
Overview#
There are numerous methods of subsetting Series and DataFrames in pandas. For example, Series values can be subsetted using the [ operator. However, this can be confusing since both using numeric indices and the index values are permitted (at the time of writing), and it can be unclear which method is actually employed.
In agreement with the Python philosophy, where explicit is better than implicit, the recommended subsetting methods are using .loc and .iloc, which are discussed in the following sub-sections. The principal difference between .loc and .iloc is:
In this book, we are going to use .iloc and .loc methods when subsetting Series (this section) and DataFrame (see Subsetting DataFrames) objects. Nevertheless, for shorter syntax, we are going to use the [ shortcut in the following cases:
Selecting
DataFramecolumns using the[operator and a string, or alistof strings, as in:dat['Year']instead ofdat.loc[:,'Year'](see Selecting DataFrame columns)dat[['Year']]instead ofdat.loc[:,['Year']](see Selecting DataFrame columns)
Filtering
DataFramerows using the[operator and a booleanSeries, as in:dat[dat['Year']>2017]instead ofdat.loc[dat['Year']>2017](see DataFrame filtering)
Subsetting Series#
As mentioned above, the recommended way to subset Series is using the specialized methods named .loc and .iloc:
.loc—For subsetting using theSeriesindex.iloc—For subsetting using the (implicit)numpyindex
What follows after the method are the indices, inside square brackets ([), whereas the indices can be one of:
An individual value, such as
.loc['a']or.iloc[0]A slice, such as
.loc['a':'b']or.iloc[1:2]A
list, such as.loc[['a','c']]or.iloc[[0,2]]
For the next few examples, let’s create a Series object named s, with:
values
11,12,13,14, andindices
'a','b','c','d',
using the index parameter of pd.Series:
s = pd.Series([11, 12, 13, 14], index=['a', 'b', 'c', 'd'])
s
a 11
b 12
c 13
d 14
dtype: int64
Here is how we can use .loc to select values using the three above-mentioned types of indices:
s.loc['a'] ## Individual index
np.int64(11)
s.loc['b':'c'] ## Slice
b 12
c 13
dtype: int64
s.loc[['a', 'c']] ## 'list' of indices
a 11
c 13
dtype: int64
And here is how we can use .iloc to select values using numpy-style numeric indices:
s.iloc[0] ## Individual index
np.int64(11)
s.iloc[1:2] ## Slice
b 12
dtype: int64
s.iloc[[0, 2]] ## 'list' of indices
a 11
c 13
dtype: int64
In the above examples, note how using an individual index returns a standalone value, while using a slice, or a list of indices, return a Series, even if the list or the slice contains just one element:
s.iloc[[0]]
a 11
dtype: int64
Also note that a slice using indices (such as s.loc['a':'b']) is inclusive, while a slice using implicit indices (such as .iloc[0:1]) excludes the last index, similarly to list and ndarray slicing behavior.
Subsetting DataFrames#
Selecting DataFrame columns#
A DataFrame is a two-dimensional object, unlike a Series which is one-dimensional. Accordingly, the .loc and iloc methods of a DataFrame accept two indices, separated by a comma:
The first index refers to rows
The second index refers to columns
When we want to select all rows or columns, we place : in the respective index, similarly to numpy array subsetting (see Subsetting arrays). Using an individual index returns a Series, while using a slice or a list of indices—even if length 1—returns a DataFrame.
For example, this is how we can use .loc to extract one DataFrame column as a Series:
dat.loc[:, 'Glob']
0 -0.18
1 -0.09
2 -0.11
3 -0.17
4 -0.28
...
140 1.00
141 0.84
142 0.89
143 1.17
144 1.28
Name: Glob, Length: 145, dtype: float64
and here is how we can extract a single column as a DataFrame:
dat.loc[:, ['Glob']]
| Glob | |
|---|---|
| 0 | -0.18 |
| 1 | -0.09 |
| 2 | -0.11 |
| 3 | -0.17 |
| 4 | -0.28 |
| ... | ... |
| 140 | 1.00 |
| 141 | 0.84 |
| 142 | 0.89 |
| 143 | 1.17 |
| 144 | 1.28 |
145 rows × 1 columns
As mentioned above (see Overview), we can also use the shortcut [ operator to subset columns(s):
dat['Glob'] ## Shortcut for `dat.loc[:,'Glob']`
0 -0.18
1 -0.09
2 -0.11
3 -0.17
4 -0.28
...
140 1.00
141 0.84
142 0.89
143 1.17
144 1.28
Name: Glob, Length: 145, dtype: float64
dat[['Glob']] ## Shortcut for `dat.loc[:,['Glob']]`
| Glob | |
|---|---|
| 0 | -0.18 |
| 1 | -0.09 |
| 2 | -0.11 |
| 3 | -0.17 |
| 4 | -0.28 |
| ... | ... |
| 140 | 1.00 |
| 141 | 0.84 |
| 142 | 0.89 |
| 143 | 1.17 |
| 144 | 1.28 |
145 rows × 1 columns
Note
Dot notation, as in dat.Glob can also be used to select individual columns of a DataFrame. However, this is not recommended, because it does not work (1) with non-string column names, and (2) with column names that are the same as DataFrame methods, such as .pop.
We can pass a list of column names to select more then one column. For example:
dat[['Year', 'Glob', 'NHem', 'SHem']]
| Year | Glob | NHem | SHem | |
|---|---|---|---|---|
| 0 | 1880 | -0.18 | -0.31 | -0.04 |
| 1 | 1881 | -0.09 | -0.19 | 0.01 |
| 2 | 1882 | -0.11 | -0.23 | -0.01 |
| 3 | 1883 | -0.17 | -0.29 | -0.06 |
| 4 | 1884 | -0.28 | -0.44 | -0.14 |
| ... | ... | ... | ... | ... |
| 140 | 2020 | 1.00 | 1.34 | 0.67 |
| 141 | 2021 | 0.84 | 1.14 | 0.55 |
| 142 | 2022 | 0.89 | 1.16 | 0.62 |
| 143 | 2023 | 1.17 | 1.49 | 0.85 |
| 144 | 2024 | 1.28 | 1.65 | 0.91 |
145 rows × 4 columns
To select all columns except for the specified ones, we can use the .drop method combined with axis=1:
dat.drop(['Year', 'Glob', 'NHem', 'SHem', '24N-90N', '24S-24N', '90S-24S'], axis=1)
| 64N-90N | 44N-64N | 24N-44N | EQU-24N | 24S-EQU | 44S-24S | 64S-44S | 90S-64S | |
|---|---|---|---|---|---|---|---|---|
| 0 | -0.80 | -0.53 | -0.30 | -0.17 | -0.09 | -0.04 | 0.05 | 0.69 |
| 1 | -0.83 | -0.48 | -0.22 | 0.08 | 0.12 | -0.05 | -0.07 | 0.62 |
| 2 | -1.33 | -0.30 | -0.17 | -0.07 | -0.04 | 0.01 | 0.04 | 0.65 |
| 3 | -0.12 | -0.57 | -0.26 | -0.19 | -0.14 | -0.04 | 0.07 | 0.52 |
| 4 | -1.25 | -0.66 | -0.47 | -0.14 | -0.16 | -0.18 | -0.02 | 0.67 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 140 | 2.87 | 1.81 | 1.18 | 0.87 | 0.81 | 0.59 | 0.40 | 0.85 |
| 141 | 2.07 | 1.36 | 1.26 | 0.72 | 0.57 | 0.73 | 0.33 | 0.29 |
| 142 | 2.36 | 1.50 | 1.26 | 0.62 | 0.49 | 0.79 | 0.40 | 1.08 |
| 143 | 2.58 | 1.87 | 1.46 | 1.08 | 1.03 | 0.90 | 0.46 | 0.64 |
| 144 | 2.91 | 1.95 | 1.64 | 1.21 | 1.15 | 0.88 | 0.55 | 0.68 |
145 rows × 8 columns
Another useful technique is to use .loc combined with slices of column names. For example, we can select all columns between two specified ones:
dat.loc[:, 'Year':'SHem']
| Year | Glob | NHem | SHem | |
|---|---|---|---|---|
| 0 | 1880 | -0.18 | -0.31 | -0.04 |
| 1 | 1881 | -0.09 | -0.19 | 0.01 |
| 2 | 1882 | -0.11 | -0.23 | -0.01 |
| 3 | 1883 | -0.17 | -0.29 | -0.06 |
| 4 | 1884 | -0.28 | -0.44 | -0.14 |
| ... | ... | ... | ... | ... |
| 140 | 2020 | 1.00 | 1.34 | 0.67 |
| 141 | 2021 | 0.84 | 1.14 | 0.55 |
| 142 | 2022 | 0.89 | 1.16 | 0.62 |
| 143 | 2023 | 1.17 | 1.49 | 0.85 |
| 144 | 2024 | 1.28 | 1.65 | 0.91 |
145 rows × 4 columns
or all columns before a specified one:
dat.loc[:, :'SHem']
| Year | Glob | NHem | SHem | |
|---|---|---|---|---|
| 0 | 1880 | -0.18 | -0.31 | -0.04 |
| 1 | 1881 | -0.09 | -0.19 | 0.01 |
| 2 | 1882 | -0.11 | -0.23 | -0.01 |
| 3 | 1883 | -0.17 | -0.29 | -0.06 |
| 4 | 1884 | -0.28 | -0.44 | -0.14 |
| ... | ... | ... | ... | ... |
| 140 | 2020 | 1.00 | 1.34 | 0.67 |
| 141 | 2021 | 0.84 | 1.14 | 0.55 |
| 142 | 2022 | 0.89 | 1.16 | 0.62 |
| 143 | 2023 | 1.17 | 1.49 | 0.85 |
| 144 | 2024 | 1.28 | 1.65 | 0.91 |
145 rows × 4 columns
or all columns after a specified one:
dat.loc[:, 'SHem':]
| SHem | 24N-90N | 24S-24N | 90S-24S | 64N-90N | ... | EQU-24N | 24S-EQU | 44S-24S | 64S-44S | 90S-64S | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | -0.04 | -0.40 | -0.13 | -0.01 | -0.80 | ... | -0.17 | -0.09 | -0.04 | 0.05 | 0.69 |
| 1 | 0.01 | -0.36 | 0.10 | -0.07 | -0.83 | ... | 0.08 | 0.12 | -0.05 | -0.07 | 0.62 |
| 2 | -0.01 | -0.32 | -0.05 | 0.01 | -1.33 | ... | -0.07 | -0.04 | 0.01 | 0.04 | 0.65 |
| 3 | -0.06 | -0.35 | -0.17 | -0.01 | -0.12 | ... | -0.19 | -0.14 | -0.04 | 0.07 | 0.52 |
| 4 | -0.14 | -0.61 | -0.15 | -0.13 | -1.25 | ... | -0.14 | -0.16 | -0.18 | -0.02 | 0.67 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 140 | 0.67 | 1.66 | 0.84 | 0.57 | 2.87 | ... | 0.87 | 0.81 | 0.59 | 0.40 | 0.85 |
| 141 | 0.55 | 1.42 | 0.64 | 0.53 | 2.07 | ... | 0.72 | 0.57 | 0.73 | 0.33 | 0.29 |
| 142 | 0.62 | 1.52 | 0.56 | 0.71 | 2.36 | ... | 0.62 | 0.49 | 0.79 | 0.40 | 1.08 |
| 143 | 0.85 | 1.78 | 1.06 | 0.72 | 2.58 | ... | 1.08 | 1.03 | 0.90 | 0.46 | 0.64 |
| 144 | 0.91 | 1.95 | 1.18 | 0.74 | 2.91 | ... | 1.21 | 1.15 | 0.88 | 0.55 | 0.68 |
145 rows × 12 columns
Again, note that in the last examples, the subset is inclusive, i.e., includes both the start and end columns.
Exercise 05-b
How can we select all columns after the specified one excluding itself?
Selecting DataFrame rows#
Rows can be selected using .loc or .iloc, similarly to the way we use those methods to select columns. The difference is that we specify the first index, instead of the second. For example:
dat.iloc[[0], :] ## 1st row
| Year | Glob | NHem | SHem | 24N-90N | ... | EQU-24N | 24S-EQU | 44S-24S | 64S-44S | 90S-64S | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1880 | -0.18 | -0.31 | -0.04 | -0.4 | ... | -0.17 | -0.09 | -0.04 | 0.05 | 0.69 |
1 rows × 15 columns
dat.iloc[0:3, :] ## 1st row (inclusive) to 4th row (exclusive)
| Year | Glob | NHem | SHem | 24N-90N | ... | EQU-24N | 24S-EQU | 44S-24S | 64S-44S | 90S-64S | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1880 | -0.18 | -0.31 | -0.04 | -0.40 | ... | -0.17 | -0.09 | -0.04 | 0.05 | 0.69 |
| 1 | 1881 | -0.09 | -0.19 | 0.01 | -0.36 | ... | 0.08 | 0.12 | -0.05 | -0.07 | 0.62 |
| 2 | 1882 | -0.11 | -0.23 | -0.01 | -0.32 | ... | -0.07 | -0.04 | 0.01 | 0.04 | 0.65 |
3 rows × 15 columns
dat.iloc[[0, 2], :] ## 1st and 3rd rows
| Year | Glob | NHem | SHem | 24N-90N | ... | EQU-24N | 24S-EQU | 44S-24S | 64S-44S | 90S-64S | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1880 | -0.18 | -0.31 | -0.04 | -0.40 | ... | -0.17 | -0.09 | -0.04 | 0.05 | 0.69 |
| 2 | 1882 | -0.11 | -0.23 | -0.01 | -0.32 | ... | -0.07 | -0.04 | 0.01 | 0.04 | 0.65 |
2 rows × 15 columns
Exercise 05-c
Create a subset of
datwith the 1st, 3rd, and 5th rows.
Selecting rows and columns#
Both DataFrame dimensions can be subsetted at once, by passing two indices to .loc or to .iloc. For example, here we select a subset of dat with the first four rows and the first four columns (i.e., the top-left “corner” of the table), using .iloc:
dat.iloc[0:4, 0:4]
| Year | Glob | NHem | SHem | |
|---|---|---|---|---|
| 0 | 1880 | -0.18 | -0.31 | -0.04 |
| 1 | 1881 | -0.09 | -0.19 | 0.01 |
| 2 | 1882 | -0.11 | -0.23 | -0.01 |
| 3 | 1883 | -0.17 | -0.29 | -0.06 |
The same can be acheived using separate steps, for example using .loc to select columns, and then .iloc to select rows:
dat.loc[:, 'Year':'SHem'].iloc[0:4, :]
| Year | Glob | NHem | SHem | |
|---|---|---|---|---|
| 0 | 1880 | -0.18 | -0.31 | -0.04 |
| 1 | 1881 | -0.09 | -0.19 | 0.01 |
| 2 | 1882 | -0.11 | -0.23 | -0.01 |
| 3 | 1883 | -0.17 | -0.29 | -0.06 |
Selecting DataFrame values#
Using the above-mentioned methods, we can access individual values from a DataFrame in several ways. The clearest syntax is probably splitting the operation into two parts: first selecting the column, then selecting the index within the column. For example, here is how we can get the first value in the 'Year' column of dat:
dat['Year'].iloc[0]
np.int64(1880)
Exercise 05-d
How can we get the last value in the
'Year'column ofdat, without explicitly typing its index (e.g.145)?
Series/DataFrame to ndarray#
A Series or a DataFrame is accessible as an ndarray, through the .to_numpy method. For example:
dat['Year'].iloc[0:3].to_numpy() ## 'Series' to array
array([1880, 1881, 1882])
dat.iloc[0:3, 1:3].to_numpy() ## 'DataFrame' to array
array([[-0.18, -0.31],
[-0.09, -0.19],
[-0.11, -0.23]])
Note that a Series is translated to a one-dimensional array, while a DataFrame is translated to a two-dimensional array.
Conversion to list#
Series to list#
In some cases we may prefer to use basic Python methods to work with data contained in a Series or in a DataFrame. For that purpose, the Series or the DataFrame can be converted to a list.
A Series can be converted to a list by directly applying the .to_list method, which is analogous to the .tolist method of ndarray (see ndarray to list). For example, let’s take a small Series of length 5:
s = dat['Glob'].iloc[0:5]
s
0 -0.18
1 -0.09
2 -0.11
3 -0.17
4 -0.28
Name: Glob, dtype: float64
Here is how the Series can be converted to a list, using the .to_list method:
s.to_list()
[-0.18, -0.09, -0.11, -0.17, -0.28]
DataFrame to list#
A DataFrame can also be converted to a list, in which case the list will be nested, with each column represented by an internal list. let’s take a small DataFrame with five rows and three columns as an example:
dat1 = dat[['Year', 'NHem', 'SHem']].iloc[0:5, :]
dat1
| Year | NHem | SHem | |
|---|---|---|---|
| 0 | 1880 | -0.31 | -0.04 |
| 1 | 1881 | -0.19 | 0.01 |
| 2 | 1882 | -0.23 | -0.01 |
| 3 | 1883 | -0.29 | -0.06 |
| 4 | 1884 | -0.44 | -0.14 |
To convert the DataFrame to a list, we can use the .to_records method (possibly with index=False to omit the index), followed by .tolist. The result is a list to tuples, where each tuple represents a row:
dat1.to_records(index=False).tolist()
[(1880, -0.31, -0.04),
(1881, -0.19, 0.01),
(1882, -0.23, -0.01),
(1883, -0.29, -0.06),
(1884, -0.44, -0.14)]
Modifying the index#
Overivew#
In the above examples, the Series and DataFrame we created got the default, consecutive integer, index. As we will see, the index plays an important role in many operations in pandas. Therefore, often we would like to set a more meaningful, custom index. For example, it usually makes sense to set the index of a table representing a time series (such as dat) to the time points (such as 'Year' values). Many pandas operations then utilize the index. For example, when plotting the data, the x-axis will show the time labels (see Line plots (pandas)).
To set a new index, we:
Assign to the
.indexproperty of aSeries(see Setting Series index)Use the
.set_indexmethod of aDataFrame(see Setting DataFrame index)
To reset the index, i.e., get back to the default consecutive integer index, we use the .reset_index method (see Resetting the index).
We demonstrate these three techniques in the following sub-sections.
Setting Series index#
let’s go back to the name series from the beginning of the chapter (Creating a Series):
name
0 Beer-Sheva Center
1 Beer-Sheva University
2 Dimona
dtype: object
The following expression sets the index of the name series as ['a','b','c'], by assigning a list into the .index property:
name.index = ['a', 'b', 'c']
name
a Beer-Sheva Center
b Beer-Sheva University
c Dimona
dtype: object
Setting DataFrame index#
The (row) index of a DataFrame can also be “manually” set by assignment to .index. However, a more useful scenario is where we want one of the columns in the DataFrame to serve as the index, while also removing it from the DataFrame columns to avoid duplication. This can be done using .set_index.
For example, the following expression changes the index of stations to the station names:
stations = stations.set_index('name')
stations
| city | lines | piano | lon | lat | |
|---|---|---|---|---|---|
| name | |||||
| Beer-Sheva Center | Beer-Sheva | 4 | False | 34.798443 | 31.243288 |
| Beer-Sheva University | Beer-Sheva | 5 | True | 34.812831 | 31.260284 |
| Dimona | Dimona | 1 | False | 35.011635 | 31.068616 |
Now, name is no longer a column, but the index of stations.
Resetting the index#
To reset the index of a Series or a DataFrame, we use the .reset_index method. As a result, the index will be replaced with the default consecutive integer sequence. In case we want to remove the information in the index altoghether, we use the drop=True option. Otherwise, the index will be “transferred” into a new column.
For example, here is how we remove the ['a','b','c'] index, to get the original default index in the name Series. Note that the information in the index is lost and we get back to the original Series, due to the drop=True option:
name = name.reset_index(drop=True)
name
0 Beer-Sheva Center
1 Beer-Sheva University
2 Dimona
dtype: object
And here is how we reset the row index in the DataFrame named stations. This time, we don’t want to lose the information, therefore the index “goes back” to the 'name' column:
stations = stations.reset_index()
stations
| name | city | lines | piano | lon | lat | |
|---|---|---|---|---|---|---|
| 0 | Beer-Sheva Center | Beer-Sheva | 4 | False | 34.798443 | 31.243288 |
| 1 | Beer-Sheva University | Beer-Sheva | 5 | True | 34.812831 | 31.260284 |
| 2 | Dimona | Dimona | 1 | False | 35.011635 | 31.068616 |
Sorting#
A DataFrame can be sorted using the .sort_values method. The first parameter (by) accepts a column name, or a list of column names, to sort by.
For example, the following expression sorts the rows of dat according to the global temperature anomaly (in the 'Glob' column), from lowest to highest:
dat.sort_values('Glob')
| Year | Glob | NHem | SHem | 24N-90N | ... | EQU-24N | 24S-EQU | 44S-24S | 64S-44S | 90S-64S | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 29 | 1909 | -0.49 | -0.51 | -0.48 | -0.55 | ... | -0.45 | -0.53 | -0.37 | -0.52 | -0.56 |
| 24 | 1904 | -0.48 | -0.50 | -0.46 | -0.53 | ... | -0.46 | -0.49 | -0.37 | -0.49 | -1.31 |
| 37 | 1917 | -0.47 | -0.59 | -0.36 | -0.51 | ... | -0.71 | -0.59 | -0.23 | -0.09 | 0.07 |
| 31 | 1911 | -0.45 | -0.43 | -0.46 | -0.43 | ... | -0.44 | -0.45 | -0.44 | -0.52 | 0.06 |
| 30 | 1910 | -0.44 | -0.46 | -0.43 | -0.43 | ... | -0.49 | -0.50 | -0.33 | -0.45 | 0.17 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 139 | 2019 | 0.97 | 1.21 | 0.74 | 1.42 | ... | 0.90 | 0.88 | 0.74 | 0.41 | 0.80 |
| 140 | 2020 | 1.00 | 1.34 | 0.67 | 1.66 | ... | 0.87 | 0.81 | 0.59 | 0.40 | 0.85 |
| 136 | 2016 | 1.01 | 1.30 | 0.72 | 1.54 | ... | 0.95 | 1.05 | 0.67 | 0.28 | 0.36 |
| 143 | 2023 | 1.17 | 1.49 | 0.85 | 1.78 | ... | 1.08 | 1.03 | 0.90 | 0.46 | 0.64 |
| 144 | 2024 | 1.28 | 1.65 | 0.91 | 1.95 | ... | 1.21 | 1.15 | 0.88 | 0.55 | 0.68 |
145 rows × 15 columns
The default is to sort in ascending order. If we want the opposite, i.e., a descending order, we need to specify ascending=False:
dat.sort_values('Glob', ascending=False)
| Year | Glob | NHem | SHem | 24N-90N | ... | EQU-24N | 24S-EQU | 44S-24S | 64S-44S | 90S-64S | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 144 | 2024 | 1.28 | 1.65 | 0.91 | 1.95 | ... | 1.21 | 1.15 | 0.88 | 0.55 | 0.68 |
| 143 | 2023 | 1.17 | 1.49 | 0.85 | 1.78 | ... | 1.08 | 1.03 | 0.90 | 0.46 | 0.64 |
| 136 | 2016 | 1.01 | 1.30 | 0.72 | 1.54 | ... | 0.95 | 1.05 | 0.67 | 0.28 | 0.36 |
| 140 | 2020 | 1.00 | 1.34 | 0.67 | 1.66 | ... | 0.87 | 0.81 | 0.59 | 0.40 | 0.85 |
| 139 | 2019 | 0.97 | 1.21 | 0.74 | 1.42 | ... | 0.90 | 0.88 | 0.74 | 0.41 | 0.80 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 30 | 1910 | -0.44 | -0.46 | -0.43 | -0.43 | ... | -0.49 | -0.50 | -0.33 | -0.45 | 0.17 |
| 31 | 1911 | -0.45 | -0.43 | -0.46 | -0.43 | ... | -0.44 | -0.45 | -0.44 | -0.52 | 0.06 |
| 37 | 1917 | -0.47 | -0.59 | -0.36 | -0.51 | ... | -0.71 | -0.59 | -0.23 | -0.09 | 0.07 |
| 24 | 1904 | -0.48 | -0.50 | -0.46 | -0.53 | ... | -0.46 | -0.49 | -0.37 | -0.49 | -1.31 |
| 29 | 1909 | -0.49 | -0.51 | -0.48 | -0.55 | ... | -0.45 | -0.53 | -0.37 | -0.52 | -0.56 |
145 rows × 15 columns
Plotting (pandas)#
Histograms (pandas)#
pandas has several methods to visualize the information in a Series or a DataFrame, which are actually shortcuts to matplotlib functions (see The matplotlib package). These methods are useful for quick visual inspection of the data.
For example, we can draw a histogram of the column(s) in a DataFrame using the .hist method. For example:
dat[['Glob', 'SHem', 'NHem']].hist();
Line plots (pandas)#
The .plot method, given a Series (or a DataFrame with one column), creates a line plot with the indices on the x-axis and the values on the y-axis:
dat['Glob'].plot();
Displaying the individual observations, in addition to the line, can be done as follows:
dat['Glob'].plot(marker='o', markerfacecolor='none');
In case we need to change the values on the x-axis, the simplest way is to set the required values as the index (see Modifying the index). For example, here we set the 'Year' column as the DataFrame index, then plot the 'Glob' column. As a result, the measurement year appears on the x-axis instead of the default consecutive index:
dat.set_index('Year')['Glob'].plot();
Pay attention to the order of operations (from left to right) in the above expression:
dat.set_index('Year')—Setting the index['Glob']—Extracting a column (asSeries).plot();—Plotting
When we plot a DataFrame with more than one column, the columns are plotted as separate series in the same line plot. This is useful to compare the values in different columns. For example, plotting the 'Glob', 'NHem', and 'SHem' columns demonstrates that in recent years Northern Hemisphere ('NHem') temperatures have been relatively higher, while Southern Hemisphere ('SHem') temperatures have been relatively lower, compared to the global average ('Glob'):
dat.set_index('Year')[['Glob', 'NHem', 'SHem']].plot();
Scatterplots (pandas)#
One more useful type of plot is a scatterplot, where we display the association between two series in the form of scattered points. To produce a scatterplot, we use the .plot.scatter method of a DataFrame, specifying the names of the columns to be displayed in the x-axis (x) and y-axis (y).
For example, the following expression shows the relation between the values in the 'NHem' and 'SHem' columns of dat:
dat.plot.scatter(x='NHem', y='SHem');
We can see a strong positive association between Northern and Southern Hemisphere temperatures across years. Namely, in years when the Nothern Hemisphere temperature is high, the Southern Hemisphere temperature also tends to be high, and vice versa.
Operators#
Operators on Series#
Operators and summary functions can be applied to Series objects, similarly to the way they can be applied to numpy arrays (see Vectorized operations and Summarizing array values, respectively). For example, summary methods such as .min, .max, and .mean can be applied on a Series to summarize its respective properties. Here is how we can find out the start and end 'Year' in dat:
dat['Year'].min()
np.int64(1880)
dat['Year'].max()
np.int64(2024)
and the average 'Glob' value:
dat['Glob'].mean()
np.float64(0.07206896551724139)
Other methods, such as .abs (absolute value), are applied on each element of a Series, resulting in a matching Series of results:
dat['Glob'].abs()
0 0.18
1 0.09
2 0.11
3 0.17
4 0.28
...
140 1.00
141 0.84
142 0.89
143 1.17
144 1.28
Name: Glob, Length: 145, dtype: float64
We can also combine Series with individual values, or combine two series, to apply pairwise arithmetic or boolean operators. For example, here is how we can subtract 1 from all 'Year' values:
dat['Year'] - 1
0 1879
1 1880
2 1881
3 1882
4 1883
...
140 2019
141 2020
142 2021
143 2022
144 2023
Name: Year, Length: 145, dtype: int64
and here is how we can calculate the yearly differences between the Northern Hemisphere and Southern Hemisphere temperature anomalies:
dat['NHem'] - dat['SHem']
0 -0.27
1 -0.20
2 -0.22
3 -0.23
4 -0.30
...
140 0.67
141 0.59
142 0.54
143 0.64
144 0.74
Length: 145, dtype: float64
Note that this is an arithmetic operation between two Series, which returns a new series.
Note
When performing operations between pairs of Series or DataFrames, pandas aligns the elements according to the index, rather than the position. When the data come from the same table (such as in the last example), the indices are guaranteed to match. However, when the data come from different tables, you must make sure their indices match.
Operators on DataFrames#
Operating on an entire DataFrame is more complex than operating on a Series (see Operators on Series). Accordingly, operators behave in different ways. For example, arithmetic and boolean operations combined with an individual value are applied per-element, resulting in a new DataFrame (assuming that all columns are numeric):
dat - 10
| Year | Glob | NHem | SHem | 24N-90N | ... | EQU-24N | 24S-EQU | 44S-24S | 64S-44S | 90S-64S | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1870 | -10.18 | -10.31 | -10.04 | -10.40 | ... | -10.17 | -10.09 | -10.04 | -9.95 | -9.31 |
| 1 | 1871 | -10.09 | -10.19 | -9.99 | -10.36 | ... | -9.92 | -9.88 | -10.05 | -10.07 | -9.38 |
| 2 | 1872 | -10.11 | -10.23 | -10.01 | -10.32 | ... | -10.07 | -10.04 | -9.99 | -9.96 | -9.35 |
| 3 | 1873 | -10.17 | -10.29 | -10.06 | -10.35 | ... | -10.19 | -10.14 | -10.04 | -9.93 | -9.48 |
| 4 | 1874 | -10.28 | -10.44 | -10.14 | -10.61 | ... | -10.14 | -10.16 | -10.18 | -10.02 | -9.33 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 140 | 2010 | -9.00 | -8.66 | -9.33 | -8.34 | ... | -9.13 | -9.19 | -9.41 | -9.60 | -9.15 |
| 141 | 2011 | -9.16 | -8.86 | -9.45 | -8.58 | ... | -9.28 | -9.43 | -9.27 | -9.67 | -9.71 |
| 142 | 2012 | -9.11 | -8.84 | -9.38 | -8.48 | ... | -9.38 | -9.51 | -9.21 | -9.60 | -8.92 |
| 143 | 2013 | -8.83 | -8.51 | -9.15 | -8.22 | ... | -8.92 | -8.97 | -9.10 | -9.54 | -9.36 |
| 144 | 2014 | -8.72 | -8.35 | -9.09 | -8.05 | ... | -8.79 | -8.85 | -9.12 | -9.45 | -9.32 |
145 rows × 15 columns
Methods such as .mean, however, are by default applied per column (i.e., axis=0):
dat.mean()
Year 1952.000000
Glob 0.072069
NHem 0.102414
SHem 0.042897
24N-90N 0.131448
...
EQU-24N 0.062483
24S-EQU 0.089034
44S-24S 0.053034
64S-44S -0.047379
90S-64S -0.059448
Length: 15, dtype: float64
We elaborate on this type of row- and column-wise operations later on (see Row/col-wise operations).
Creating new columns#
New DataFrame columns can be created by assignment of a Series to a non-existing column index. For example, the following expression calculates a Series of yearly differences between the Northern Hemisphere and Southern Hemisphere temperatures, as shown above, and assigns it to a new column named diff:
dat['diff'] = dat['NHem'] - dat['SHem']
dat
| Year | Glob | NHem | SHem | 24N-90N | ... | 24S-EQU | 44S-24S | 64S-44S | 90S-64S | diff | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1880 | -0.18 | -0.31 | -0.04 | -0.40 | ... | -0.09 | -0.04 | 0.05 | 0.69 | -0.27 |
| 1 | 1881 | -0.09 | -0.19 | 0.01 | -0.36 | ... | 0.12 | -0.05 | -0.07 | 0.62 | -0.20 |
| 2 | 1882 | -0.11 | -0.23 | -0.01 | -0.32 | ... | -0.04 | 0.01 | 0.04 | 0.65 | -0.22 |
| 3 | 1883 | -0.17 | -0.29 | -0.06 | -0.35 | ... | -0.14 | -0.04 | 0.07 | 0.52 | -0.23 |
| 4 | 1884 | -0.28 | -0.44 | -0.14 | -0.61 | ... | -0.16 | -0.18 | -0.02 | 0.67 | -0.30 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 140 | 2020 | 1.00 | 1.34 | 0.67 | 1.66 | ... | 0.81 | 0.59 | 0.40 | 0.85 | 0.67 |
| 141 | 2021 | 0.84 | 1.14 | 0.55 | 1.42 | ... | 0.57 | 0.73 | 0.33 | 0.29 | 0.59 |
| 142 | 2022 | 0.89 | 1.16 | 0.62 | 1.52 | ... | 0.49 | 0.79 | 0.40 | 1.08 | 0.54 |
| 143 | 2023 | 1.17 | 1.49 | 0.85 | 1.78 | ... | 1.03 | 0.90 | 0.46 | 0.64 | 0.64 |
| 144 | 2024 | 1.28 | 1.65 | 0.91 | 1.95 | ... | 1.15 | 0.88 | 0.55 | 0.68 | 0.74 |
145 rows × 16 columns
Here is a plot (Line plots (pandas)) of the differences we just calulated, as function of time:
dat.set_index('Year')['diff'].plot();
We can also assign an expression that combines series with individual values:
dat['NHem2'] = dat['NHem'] * 2
dat
| Year | Glob | NHem | SHem | 24N-90N | ... | 44S-24S | 64S-44S | 90S-64S | diff | NHem2 | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1880 | -0.18 | -0.31 | -0.04 | -0.40 | ... | -0.04 | 0.05 | 0.69 | -0.27 | -0.62 |
| 1 | 1881 | -0.09 | -0.19 | 0.01 | -0.36 | ... | -0.05 | -0.07 | 0.62 | -0.20 | -0.38 |
| 2 | 1882 | -0.11 | -0.23 | -0.01 | -0.32 | ... | 0.01 | 0.04 | 0.65 | -0.22 | -0.46 |
| 3 | 1883 | -0.17 | -0.29 | -0.06 | -0.35 | ... | -0.04 | 0.07 | 0.52 | -0.23 | -0.58 |
| 4 | 1884 | -0.28 | -0.44 | -0.14 | -0.61 | ... | -0.18 | -0.02 | 0.67 | -0.30 | -0.88 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 140 | 2020 | 1.00 | 1.34 | 0.67 | 1.66 | ... | 0.59 | 0.40 | 0.85 | 0.67 | 2.68 |
| 141 | 2021 | 0.84 | 1.14 | 0.55 | 1.42 | ... | 0.73 | 0.33 | 0.29 | 0.59 | 2.28 |
| 142 | 2022 | 0.89 | 1.16 | 0.62 | 1.52 | ... | 0.79 | 0.40 | 1.08 | 0.54 | 2.32 |
| 143 | 2023 | 1.17 | 1.49 | 0.85 | 1.78 | ... | 0.90 | 0.46 | 0.64 | 0.64 | 2.98 |
| 144 | 2024 | 1.28 | 1.65 | 0.91 | 1.95 | ... | 0.88 | 0.55 | 0.68 | 0.74 | 3.30 |
145 rows × 17 columns
or an individual value on its own (in which case it is duplicated across all rows):
dat['variable'] = 'temperature'
dat
| Year | Glob | NHem | SHem | 24N-90N | ... | 64S-44S | 90S-64S | diff | NHem2 | variable | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1880 | -0.18 | -0.31 | -0.04 | -0.40 | ... | 0.05 | 0.69 | -0.27 | -0.62 | temperature |
| 1 | 1881 | -0.09 | -0.19 | 0.01 | -0.36 | ... | -0.07 | 0.62 | -0.20 | -0.38 | temperature |
| 2 | 1882 | -0.11 | -0.23 | -0.01 | -0.32 | ... | 0.04 | 0.65 | -0.22 | -0.46 | temperature |
| 3 | 1883 | -0.17 | -0.29 | -0.06 | -0.35 | ... | 0.07 | 0.52 | -0.23 | -0.58 | temperature |
| 4 | 1884 | -0.28 | -0.44 | -0.14 | -0.61 | ... | -0.02 | 0.67 | -0.30 | -0.88 | temperature |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 140 | 2020 | 1.00 | 1.34 | 0.67 | 1.66 | ... | 0.40 | 0.85 | 0.67 | 2.68 | temperature |
| 141 | 2021 | 0.84 | 1.14 | 0.55 | 1.42 | ... | 0.33 | 0.29 | 0.59 | 2.28 | temperature |
| 142 | 2022 | 0.89 | 1.16 | 0.62 | 1.52 | ... | 0.40 | 1.08 | 0.54 | 2.32 | temperature |
| 143 | 2023 | 1.17 | 1.49 | 0.85 | 1.78 | ... | 0.46 | 0.64 | 0.64 | 2.98 | temperature |
| 144 | 2024 | 1.28 | 1.65 | 0.91 | 1.95 | ... | 0.55 | 0.68 | 0.74 | 3.30 | temperature |
145 rows × 18 columns
let’s delete the 'diff', 'NHem2', 'variable' columns we just created to get the original version of dat:
dat = dat.drop(['diff', 'NHem2', 'variable'], axis=1)
dat
| Year | Glob | NHem | SHem | 24N-90N | ... | EQU-24N | 24S-EQU | 44S-24S | 64S-44S | 90S-64S | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1880 | -0.18 | -0.31 | -0.04 | -0.40 | ... | -0.17 | -0.09 | -0.04 | 0.05 | 0.69 |
| 1 | 1881 | -0.09 | -0.19 | 0.01 | -0.36 | ... | 0.08 | 0.12 | -0.05 | -0.07 | 0.62 |
| 2 | 1882 | -0.11 | -0.23 | -0.01 | -0.32 | ... | -0.07 | -0.04 | 0.01 | 0.04 | 0.65 |
| 3 | 1883 | -0.17 | -0.29 | -0.06 | -0.35 | ... | -0.19 | -0.14 | -0.04 | 0.07 | 0.52 |
| 4 | 1884 | -0.28 | -0.44 | -0.14 | -0.61 | ... | -0.14 | -0.16 | -0.18 | -0.02 | 0.67 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 140 | 2020 | 1.00 | 1.34 | 0.67 | 1.66 | ... | 0.87 | 0.81 | 0.59 | 0.40 | 0.85 |
| 141 | 2021 | 0.84 | 1.14 | 0.55 | 1.42 | ... | 0.72 | 0.57 | 0.73 | 0.33 | 0.29 |
| 142 | 2022 | 0.89 | 1.16 | 0.62 | 1.52 | ... | 0.62 | 0.49 | 0.79 | 0.40 | 1.08 |
| 143 | 2023 | 1.17 | 1.49 | 0.85 | 1.78 | ... | 1.08 | 1.03 | 0.90 | 0.46 | 0.64 |
| 144 | 2024 | 1.28 | 1.65 | 0.91 | 1.95 | ... | 1.21 | 1.15 | 0.88 | 0.55 | 0.68 |
145 rows × 15 columns
DataFrame filtering#
Using conditional operators#
Most often, instead of selecting rows by index or by position (see Selecting DataFrame rows), we want to filter rows by a condition that we apply on table values. For example, suppose that we want to get a subset of dat with all rows after the year 2020. To do that, we can create a boolean Series, specifying which rows are to be retained:
sel = dat['Year'] > 2020
sel
0 False
1 False
2 False
3 False
4 False
...
140 False
141 True
142 True
143 True
144 True
Name: Year, Length: 145, dtype: bool
Then, the boolean series can be passed as an index inside square brackets ([), to select those rows corresponding to True:
dat[sel]
| Year | Glob | NHem | SHem | 24N-90N | ... | EQU-24N | 24S-EQU | 44S-24S | 64S-44S | 90S-64S | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 141 | 2021 | 0.84 | 1.14 | 0.55 | 1.42 | ... | 0.72 | 0.57 | 0.73 | 0.33 | 0.29 |
| 142 | 2022 | 0.89 | 1.16 | 0.62 | 1.52 | ... | 0.62 | 0.49 | 0.79 | 0.40 | 1.08 |
| 143 | 2023 | 1.17 | 1.49 | 0.85 | 1.78 | ... | 1.08 | 1.03 | 0.90 | 0.46 | 0.64 |
| 144 | 2024 | 1.28 | 1.65 | 0.91 | 1.95 | ... | 1.21 | 1.15 | 0.88 | 0.55 | 0.68 |
4 rows × 15 columns
The same can be acheived in a single step, as follows:
dat[dat['Year'] > 2020]
| Year | Glob | NHem | SHem | 24N-90N | ... | EQU-24N | 24S-EQU | 44S-24S | 64S-44S | 90S-64S | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 141 | 2021 | 0.84 | 1.14 | 0.55 | 1.42 | ... | 0.72 | 0.57 | 0.73 | 0.33 | 0.29 |
| 142 | 2022 | 0.89 | 1.16 | 0.62 | 1.52 | ... | 0.62 | 0.49 | 0.79 | 0.40 | 1.08 |
| 143 | 2023 | 1.17 | 1.49 | 0.85 | 1.78 | ... | 1.08 | 1.03 | 0.90 | 0.46 | 0.64 |
| 144 | 2024 | 1.28 | 1.65 | 0.91 | 1.95 | ... | 1.21 | 1.15 | 0.88 | 0.55 | 0.68 |
4 rows × 15 columns
Here is an example with a more complex condition (note the parantheses!):
dat[(dat['Year'] > 2020) & (dat['Year'] <= 2022)]
| Year | Glob | NHem | SHem | 24N-90N | ... | EQU-24N | 24S-EQU | 44S-24S | 64S-44S | 90S-64S | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 141 | 2021 | 0.84 | 1.14 | 0.55 | 1.42 | ... | 0.72 | 0.57 | 0.73 | 0.33 | 0.29 |
| 142 | 2022 | 0.89 | 1.16 | 0.62 | 1.52 | ... | 0.62 | 0.49 | 0.79 | 0.40 | 1.08 |
2 rows × 15 columns
To get the values of partucular column(s), we can use .loc, as follows:
dat.loc[(dat['Year'] > 2020) & (dat['Year'] <= 2022), 'Glob']
141 0.84
142 0.89
Name: Glob, dtype: float64
Exercise 05-e
Create a subset of
datwith all rows where the global temperature anomaly ('Glob') was above1degree.
Using .isin#
Sometimes, the condition we are interested in is not a comparison against an individual value (such as >2020, see Using conditional operators), but multiple values. For example, suppose we want to extract the rows corresponding to the years 2008, 2012, and 2016, in dat. One way is to compose a combined condition using |, using the numpy syntax we are familiar with (see numpy conditional operators). Again, keep in mind that we must enclose each conditional expression in brackets:
sel = (dat['Year'] == 2008) | (dat['Year'] == 2012) | (dat['Year'] == 2016)
dat[sel]
| Year | Glob | NHem | SHem | 24N-90N | ... | EQU-24N | 24S-EQU | 44S-24S | 64S-44S | 90S-64S | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 128 | 2008 | 0.54 | 0.67 | 0.40 | 0.91 | ... | 0.33 | 0.41 | 0.52 | 0.09 | 0.64 |
| 132 | 2012 | 0.64 | 0.80 | 0.48 | 1.02 | ... | 0.47 | 0.56 | 0.56 | 0.20 | 0.46 |
| 136 | 2016 | 1.01 | 1.30 | 0.72 | 1.54 | ... | 0.95 | 1.05 | 0.67 | 0.28 | 0.36 |
3 rows × 15 columns
As you can imagine, this method quickly becomes unfeasible if the number of items becomes large. Instead, we can use the .isin method, combined with a list of items to compare with the Series values:
sel = dat['Year'].isin([2008, 2012, 2016])
dat[sel]
| Year | Glob | NHem | SHem | 24N-90N | ... | EQU-24N | 24S-EQU | 44S-24S | 64S-44S | 90S-64S | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 128 | 2008 | 0.54 | 0.67 | 0.40 | 0.91 | ... | 0.33 | 0.41 | 0.52 | 0.09 | 0.64 |
| 132 | 2012 | 0.64 | 0.80 | 0.48 | 1.02 | ... | 0.47 | 0.56 | 0.56 | 0.20 | 0.46 |
| 136 | 2016 | 1.01 | 1.30 | 0.72 | 1.54 | ... | 0.95 | 1.05 | 0.67 | 0.28 | 0.36 |
3 rows × 15 columns
Recall the in operator which we learned about earlier (see The in operator). The in operator is analogous to the .isin method, but intended for lists.
Exercise 05-f
What do you think is the meaning of
dat[~sel]? Try it to check your answer.
Assignment to subsets#
When assigning to subsets of a column, we need to use the .loc method. Typically we combine:
A boolean
Seriesin the rows index, to select the observations we want to modifyAn
strin the columns index, to select the variable we want to modify
For example, the following expression sets the 'NHem' column value to 9999 in all years after 2021:
dat.loc[dat['Year'] > 2021, 'NHem'] = 9999
dat
| Year | Glob | NHem | SHem | 24N-90N | ... | EQU-24N | 24S-EQU | 44S-24S | 64S-44S | 90S-64S | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1880 | -0.18 | -0.31 | -0.04 | -0.40 | ... | -0.17 | -0.09 | -0.04 | 0.05 | 0.69 |
| 1 | 1881 | -0.09 | -0.19 | 0.01 | -0.36 | ... | 0.08 | 0.12 | -0.05 | -0.07 | 0.62 |
| 2 | 1882 | -0.11 | -0.23 | -0.01 | -0.32 | ... | -0.07 | -0.04 | 0.01 | 0.04 | 0.65 |
| 3 | 1883 | -0.17 | -0.29 | -0.06 | -0.35 | ... | -0.19 | -0.14 | -0.04 | 0.07 | 0.52 |
| 4 | 1884 | -0.28 | -0.44 | -0.14 | -0.61 | ... | -0.14 | -0.16 | -0.18 | -0.02 | 0.67 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 140 | 2020 | 1.00 | 1.34 | 0.67 | 1.66 | ... | 0.87 | 0.81 | 0.59 | 0.40 | 0.85 |
| 141 | 2021 | 0.84 | 1.14 | 0.55 | 1.42 | ... | 0.72 | 0.57 | 0.73 | 0.33 | 0.29 |
| 142 | 2022 | 0.89 | 9999.00 | 0.62 | 1.52 | ... | 0.62 | 0.49 | 0.79 | 0.40 | 1.08 |
| 143 | 2023 | 1.17 | 9999.00 | 0.85 | 1.78 | ... | 1.08 | 1.03 | 0.90 | 0.46 | 0.64 |
| 144 | 2024 | 1.28 | 9999.00 | 0.91 | 1.95 | ... | 1.21 | 1.15 | 0.88 | 0.55 | 0.68 |
145 rows × 15 columns
To get back to the original values, let’s read the CSV file once again:
dat = pd.read_csv('data/ZonAnn.Ts+dSST.csv')
dat
| Year | Glob | NHem | SHem | 24N-90N | ... | EQU-24N | 24S-EQU | 44S-24S | 64S-44S | 90S-64S | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1880 | -0.18 | -0.31 | -0.04 | -0.40 | ... | -0.17 | -0.09 | -0.04 | 0.05 | 0.69 |
| 1 | 1881 | -0.09 | -0.19 | 0.01 | -0.36 | ... | 0.08 | 0.12 | -0.05 | -0.07 | 0.62 |
| 2 | 1882 | -0.11 | -0.23 | -0.01 | -0.32 | ... | -0.07 | -0.04 | 0.01 | 0.04 | 0.65 |
| 3 | 1883 | -0.17 | -0.29 | -0.06 | -0.35 | ... | -0.19 | -0.14 | -0.04 | 0.07 | 0.52 |
| 4 | 1884 | -0.28 | -0.44 | -0.14 | -0.61 | ... | -0.14 | -0.16 | -0.18 | -0.02 | 0.67 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 140 | 2020 | 1.00 | 1.34 | 0.67 | 1.66 | ... | 0.87 | 0.81 | 0.59 | 0.40 | 0.85 |
| 141 | 2021 | 0.84 | 1.14 | 0.55 | 1.42 | ... | 0.72 | 0.57 | 0.73 | 0.33 | 0.29 |
| 142 | 2022 | 0.89 | 1.16 | 0.62 | 1.52 | ... | 0.62 | 0.49 | 0.79 | 0.40 | 1.08 |
| 143 | 2023 | 1.17 | 1.49 | 0.85 | 1.78 | ... | 1.08 | 1.03 | 0.90 | 0.46 | 0.64 |
| 144 | 2024 | 1.28 | 1.65 | 0.91 | 1.95 | ... | 1.21 | 1.15 | 0.88 | 0.55 | 0.68 |
145 rows × 15 columns
Working with missing data (pandas)#
Missing data in pandas#
Missing values, denoting that the true value is unknown, are an inevitable feature of real-world data. In plain-text formats, such as CSV, missing values are commonly just “missing”, i.e., left blank, or marked with a text label such as 'NA'.
Treatment of “No Data” values in Series and DataFrames is derived from the underlying behavior in numpy (Working with missing data (numpy)), as pandas is based on numpy. Namely, “No Data” values are typically represented using np.nan.
pandas may automatically transform the Series data type, and “No Data” representation, for optimized data storage. For example, a Series of type int which contains one or more np.nan value, is automatically transformed to float, similarly to the behaviour we have seen with numpy arrays (Working with missing data (numpy)):
pd.Series([7, 4, 3])
0 7
1 4
2 3
dtype: int64
pd.Series([7, np.nan, 3])
0 7.0
1 NaN
2 3.0
dtype: float64
Moreover, None values are automatically transformed to np.nan (unlike numpy, where None values are preserved, resulting in an object array):
pd.Series([7, None, 3])
0 7.0
1 NaN
2 3.0
dtype: float64
“No Data” values are automatically assigned where necessary when creating a Series or a DataFrame through other methods too. For example, empty cells in a CSV file are interpreted as np.nan when importing the CSV into a DataFrame (see Reading from file).
The 'education.csv' file contains the locations, and other details, about education facilities in Beer-Sheva:
'Institutio'—Institution name'type'—Institution type'ADDRESS'—Institution address'oldName'—Institution old name'lat'—Latitude'lon'—Longitude
If you open the CSV file in a spreadsheet program (such as Excel), you will see that some of the cells are empty. Those cells are assigned with np.nan when importing the table:
edu = pd.read_csv('data/education.csv')
edu
| Institutio | type | ADDRESS | oldName | lat | lon | |
|---|---|---|---|---|---|---|
| 0 | דוגית | גן ילדים | משה סתוי נ.זאב | NaN | 31.240226 | 34.767153 |
| 1 | שומרון | גן ילדים | שומרון שכ לון | NaN | 31.246547 | 34.765438 |
| 2 | שרת | גן ילדים | משה שרת 13 | NaN | 31.256681 | 34.766891 |
| 3 | חופית | גן ילדים | ניב דוד נ.זאב | NaN | 31.238660 | 34.770356 |
| 4 | רביבים | גן ילדים | מבצע משה נ.זאב | NaN | 31.237117 | 34.779331 |
| ... | ... | ... | ... | ... | ... | ... |
| 484 | ברעם | גן ילדים | נחל משמר | NaN | 31.233890 | 34.818100 |
| 485 | שילה | גן ילדים | נחל יהל 3 | NaN | 31.232486 | 34.821510 |
| 486 | קדם | גן ילדים | נחל יהל 3 | NaN | 31.232557 | 34.821390 |
| 487 | שמים | גן ילדים | וינגיט שכ ג | מיתרים | 31.254752 | 34.803860 |
| 488 | תכלת | גן ילדים | וינגיט שכ ג | מיתרים | 31.254470 | 34.804140 |
489 rows × 6 columns
First, let’s make the column names more convenient:
edu = edu.rename(columns={
'Institutio': 'name',
'ADDRESS': 'address',
'oldName': 'old_name'
})
edu
| name | type | address | old_name | lat | lon | |
|---|---|---|---|---|---|---|
| 0 | דוגית | גן ילדים | משה סתוי נ.זאב | NaN | 31.240226 | 34.767153 |
| 1 | שומרון | גן ילדים | שומרון שכ לון | NaN | 31.246547 | 34.765438 |
| 2 | שרת | גן ילדים | משה שרת 13 | NaN | 31.256681 | 34.766891 |
| 3 | חופית | גן ילדים | ניב דוד נ.זאב | NaN | 31.238660 | 34.770356 |
| 4 | רביבים | גן ילדים | מבצע משה נ.זאב | NaN | 31.237117 | 34.779331 |
| ... | ... | ... | ... | ... | ... | ... |
| 484 | ברעם | גן ילדים | נחל משמר | NaN | 31.233890 | 34.818100 |
| 485 | שילה | גן ילדים | נחל יהל 3 | NaN | 31.232486 | 34.821510 |
| 486 | קדם | גן ילדים | נחל יהל 3 | NaN | 31.232557 | 34.821390 |
| 487 | שמים | גן ילדים | וינגיט שכ ג | מיתרים | 31.254752 | 34.803860 |
| 488 | תכלת | גן ילדים | וינגיט שכ ג | מיתרים | 31.254470 | 34.804140 |
489 rows × 6 columns
For example, the old name of the first entry is unknown:
edu['old_name'].iloc[0]
nan
Detecting with pd.isna#
To detect “No Data” values, the .isna method can be applied on a Series or a DataFrame. The result is a boolean mask that marks the missing values. This is analogous to the np.isnan function for arrays (see Detecting “No Data” (np.isnan)).
For example, edu.isna() returns a boolean DataFrame, of the same shape as edu, with True marking “No Data” values:
edu.isna()
| name | type | address | old_name | lat | lon | |
|---|---|---|---|---|---|---|
| 0 | False | False | False | True | False | False |
| 1 | False | False | False | True | False | False |
| 2 | False | False | False | True | False | False |
| 3 | False | False | False | True | False | False |
| 4 | False | False | False | True | False | False |
| ... | ... | ... | ... | ... | ... | ... |
| 484 | False | False | False | True | False | False |
| 485 | False | False | False | True | False | False |
| 486 | False | False | False | True | False | False |
| 487 | False | False | False | False | False | False |
| 488 | False | False | False | False | False | False |
489 rows × 6 columns
Similarly, the .isna() method applied on a Series returns a new boolean Series where “No Data” values are marked with True:
edu['old_name'].isna()
0 True
1 True
2 True
3 True
4 True
...
484 True
485 True
486 True
487 False
488 False
Name: old_name, Length: 489, dtype: bool
The ~ operator (see numpy conditional operators) reverses a boolean Series. This is very useful in the context of “No Data” masks, since it can reverse the mask so that it points at non-missing values.
For example, here is how we can get the reverse of edu.isna():
~edu.isna()
| name | type | address | old_name | lat | lon | |
|---|---|---|---|---|---|---|
| 0 | True | True | True | False | True | True |
| 1 | True | True | True | False | True | True |
| 2 | True | True | True | False | True | True |
| 3 | True | True | True | False | True | True |
| 4 | True | True | True | False | True | True |
| ... | ... | ... | ... | ... | ... | ... |
| 484 | True | True | True | False | True | True |
| 485 | True | True | True | False | True | True |
| 486 | True | True | True | False | True | True |
| 487 | True | True | True | True | True | True |
| 488 | True | True | True | True | True | True |
489 rows × 6 columns
and here is how we can get the reverse of edu['old_name'].isna():
~edu['old_name'].isna()
0 False
1 False
2 False
3 False
4 False
...
484 False
485 False
486 False
487 True
488 True
Name: old_name, Length: 489, dtype: bool
Another possible use of ~ and .isna is to extract the non-missing values from a series. Here is an example:
edu.loc[~edu['old_name'].isna(), 'old_name']
6 רד"ק
7 השלושה
10 ברנר
13 רקפת
19 מיתרים
...
365 עובדי מדינה
400 גור אריה
415 תבור
487 מיתרים
488 מיתרים
Name: old_name, Length: 67, dtype: object
However, there is a dedicated method called .dropna to do that with shorter code (see Removing with .dropna):
edu['old_name'].dropna()
6 רד"ק
7 השלושה
10 ברנר
13 רקפת
19 מיתרים
...
365 עובדי מדינה
400 גור אריה
415 תבור
487 מיתרים
488 מיתרים
Name: old_name, Length: 67, dtype: object
Similarly, we can extract DataFrame rows where a particular column is non-missing. For example, here are the edu rows that have a non-missing 'old_name' value:
edu[~edu['old_name'].isna()]
| name | type | address | old_name | lat | lon | |
|---|---|---|---|---|---|---|
| 6 | מאדים | גן ילדים | רד"ק שכ יא | רד"ק | 31.256963 | 34.772220 |
| 7 | אדמה | גן ילדים | רח טבריה שכ ט | השלושה | 31.247157 | 34.776405 |
| 10 | חלילית | גן ילדים | ברנר שכ א | ברנר | 31.248934 | 34.793113 |
| 13 | חרצית | גן ילדים | מבצע ארז שכ ו | רקפת | 31.265425 | 34.782446 |
| 19 | איילה | גן ילדים | וינגיט שכ ג | מיתרים | 31.256078 | 34.803588 |
| ... | ... | ... | ... | ... | ... | ... |
| 365 | נבל | גן ילדים | NaN | עובדי מדינה | 31.248385 | 34.786168 |
| 400 | נווה | גן ילדים | המר 3 | גור אריה | 31.266327 | 34.810049 |
| 415 | ארבל | גן ילדים | מודעי | תבור | 31.266514 | 34.809769 |
| 487 | שמים | גן ילדים | וינגיט שכ ג | מיתרים | 31.254752 | 34.803860 |
| 488 | תכלת | גן ילדים | וינגיט שכ ג | מיתרים | 31.254470 | 34.804140 |
67 rows × 6 columns
Note
Instead of reversing the output of .isna with the ~ operator, another option is to use the inverse method .notna, which returns the non-missing mask.
Finally, it is often useful to combine the .isna method with .any to find out if a Series contains at least one missing value:
edu['old_name'].isna().any()
np.True_
edu['name'].isna().any()
np.False_
or to find out which columns contain at least one missing value:
edu.isna().any()
name False
type False
address True
old_name True
lat False
lon False
dtype: bool
We can also use .sum to find out how many missing value are there in each column:
edu.isna().sum()
name 0
type 0
address 86
old_name 422
lat 0
lon 0
dtype: int64
or .mean to find the proportion of missing values in each column:
edu.isna().mean()
name 0.000000
type 0.000000
address 0.175869
old_name 0.862986
lat 0.000000
lon 0.000000
dtype: float64
Operators with missing values#
When applying a method such as .sum, .mean, .min, or .max (or the respective np.* function) on a Series, the “No Data” values are ignored. That is, the result is based only on the non-missing values. For example:
edu.loc[~edu['old_name'].isna(), 'old_name1'] = 1
edu
| name | type | address | old_name | lat | lon | old_name1 | |
|---|---|---|---|---|---|---|---|
| 0 | דוגית | גן ילדים | משה סתוי נ.זאב | NaN | 31.240226 | 34.767153 | NaN |
| 1 | שומרון | גן ילדים | שומרון שכ לון | NaN | 31.246547 | 34.765438 | NaN |
| 2 | שרת | גן ילדים | משה שרת 13 | NaN | 31.256681 | 34.766891 | NaN |
| 3 | חופית | גן ילדים | ניב דוד נ.זאב | NaN | 31.238660 | 34.770356 | NaN |
| 4 | רביבים | גן ילדים | מבצע משה נ.זאב | NaN | 31.237117 | 34.779331 | NaN |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 484 | ברעם | גן ילדים | נחל משמר | NaN | 31.233890 | 34.818100 | NaN |
| 485 | שילה | גן ילדים | נחל יהל 3 | NaN | 31.232486 | 34.821510 | NaN |
| 486 | קדם | גן ילדים | נחל יהל 3 | NaN | 31.232557 | 34.821390 | NaN |
| 487 | שמים | גן ילדים | וינגיט שכ ג | מיתרים | 31.254752 | 34.803860 | 1.0 |
| 488 | תכלת | גן ילדים | וינגיט שכ ג | מיתרים | 31.254470 | 34.804140 | 1.0 |
489 rows × 7 columns
edu['old_name1'].sum()
np.float64(67.0)
Note that this behavior is specific to pandas, and unlike what we have seen with numpy arrays, where these opertions return “No Data” if at least one of the values in the array is “No Data” (see Operations with “No Data”):
edu['old_name1'].to_numpy().sum()
np.float64(nan)
Element-by-element operations, however, expectedly result in “No Data” when applied to “No Data” elements, since the result cannot be computed at all. For example:
edu['old_name1'] * 2
0 NaN
1 NaN
2 NaN
3 NaN
4 NaN
...
484 NaN
485 NaN
486 NaN
487 2.0
488 2.0
Name: old_name1, Length: 489, dtype: float64
Removing with .dropna#
The .dropna method can be used to remove missing values from a DataFrame (or a Series). The default is to drop all rows that have at least one missing value, i.e., keeping only complete observations. For example:
edu.dropna()
| name | type | address | old_name | lat | lon | old_name1 | |
|---|---|---|---|---|---|---|---|
| 6 | מאדים | גן ילדים | רד"ק שכ יא | רד"ק | 31.256963 | 34.772220 | 1.0 |
| 7 | אדמה | גן ילדים | רח טבריה שכ ט | השלושה | 31.247157 | 34.776405 | 1.0 |
| 10 | חלילית | גן ילדים | ברנר שכ א | ברנר | 31.248934 | 34.793113 | 1.0 |
| 13 | חרצית | גן ילדים | מבצע ארז שכ ו | רקפת | 31.265425 | 34.782446 | 1.0 |
| 19 | איילה | גן ילדים | וינגיט שכ ג | מיתרים | 31.256078 | 34.803588 | 1.0 |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 363 | אוריה | גן ילדים | בי"ס מוריה שכ א | נריה | 31.246930 | 34.792426 | 1.0 |
| 400 | נווה | גן ילדים | המר 3 | גור אריה | 31.266327 | 34.810049 | 1.0 |
| 415 | ארבל | גן ילדים | מודעי | תבור | 31.266514 | 34.809769 | 1.0 |
| 487 | שמים | גן ילדים | וינגיט שכ ג | מיתרים | 31.254752 | 34.803860 | 1.0 |
| 488 | תכלת | גן ילדים | וינגיט שכ ג | מיתרים | 31.254470 | 34.804140 | 1.0 |
59 rows × 7 columns
Note
There are other parameters of .dropna for dropping columns instead of rows, or dropping rows or column using a threshold of “acceptable” minimum number of non-missing values. See the documentation for details.
Replacing with .fillna#
Missing values can be replaced using the .fillna method. The parameter specifies which new value should be placed instead of the “No Data” values. For example, here is how we can replace the “No Data” values in the 'old_name1' column of edu with 0:
edu['old_name1'].fillna(0)
0 0.0
1 0.0
2 0.0
3 0.0
4 0.0
...
484 0.0
485 0.0
486 0.0
487 1.0
488 1.0
Name: old_name1, Length: 489, dtype: float64
or assign the result back to the edu table:
edu['old_name1'] = edu['old_name1'].fillna(0)
edu
| name | type | address | old_name | lat | lon | old_name1 | |
|---|---|---|---|---|---|---|---|
| 0 | דוגית | גן ילדים | משה סתוי נ.זאב | NaN | 31.240226 | 34.767153 | 0.0 |
| 1 | שומרון | גן ילדים | שומרון שכ לון | NaN | 31.246547 | 34.765438 | 0.0 |
| 2 | שרת | גן ילדים | משה שרת 13 | NaN | 31.256681 | 34.766891 | 0.0 |
| 3 | חופית | גן ילדים | ניב דוד נ.זאב | NaN | 31.238660 | 34.770356 | 0.0 |
| 4 | רביבים | גן ילדים | מבצע משה נ.זאב | NaN | 31.237117 | 34.779331 | 0.0 |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 484 | ברעם | גן ילדים | נחל משמר | NaN | 31.233890 | 34.818100 | 0.0 |
| 485 | שילה | גן ילדים | נחל יהל 3 | NaN | 31.232486 | 34.821510 | 0.0 |
| 486 | קדם | גן ילדים | נחל יהל 3 | NaN | 31.232557 | 34.821390 | 0.0 |
| 487 | שמים | גן ילדים | וינגיט שכ ג | מיתרים | 31.254752 | 34.803860 | 1.0 |
| 488 | תכלת | גן ילדים | וינגיט שכ ג | מיתרים | 31.254470 | 34.804140 | 1.0 |
489 rows × 7 columns
Views and copies (pandas)#
One more thing we need to be aware when working with pandas is the distinction between views and copies. The pandas package (and geopandas which extends it, see Vector layers (geopandas)) behaves similarly to numpy with respect to copies (see Views and copies (numpy)). We must be aware of whether we are creating a view or a copy of a DataFrame, to avoid unexpected results.
Here is a small demonstration, very similar to the one we did with numpy arrays (see Views and copies (numpy)). Suppose that we create a copy of dat, named dat2:
dat2 = dat
Next, we modify dat2, assigning a new value such as 9999 into a particular cell (e.g., 2nd row, 2nd column):
dat2.iloc[1, 1] = 9999
The printout of the table top-left “corner” demonstrates the value was actually changed:
dat2.iloc[:4, :4]
| Year | Glob | NHem | SHem | |
|---|---|---|---|---|
| 0 | 1880 | -0.18 | -0.31 | -0.04 |
| 1 | 1881 | 9999.00 | -0.19 | 0.01 |
| 2 | 1882 | -0.11 | -0.23 | -0.01 |
| 3 | 1883 | -0.17 | -0.29 | -0.06 |
What may be surprising, again, is that the original table dat has also changed. We thus demonstrated that dat2 is a reference to dat, rather than an independent copy:
dat.iloc[:4, :4]
| Year | Glob | NHem | SHem | |
|---|---|---|---|---|
| 0 | 1880 | -0.18 | -0.31 | -0.04 |
| 1 | 1881 | 9999.00 | -0.19 | 0.01 |
| 2 | 1882 | -0.11 | -0.23 | -0.01 |
| 3 | 1883 | -0.17 | -0.29 | -0.06 |
To create an independent copy, so that its modifications are not reflected in the original, we need to explicitly use the .copy method, as in:
dat2 = dat.copy()
Note
The behavior described in this section can be disabled with pd.options.mode.copy_on_write=True, and will be disabled by default in pandas version 3.0. When disabled, every pandas operation will create an independent copy, and, for example, it would be impossible to update two objects at once as done above.
let’s read the CSV file again to get back to the original values:
dat = pd.read_csv('data/ZonAnn.Ts+dSST.csv')
Writing DataFrame to file#
A DataFrame can be exported to several formats, such as CSV or Excel, using the appropriate method. For example, exporting to CSV is done using the .to_csv method.
Here is how we can export the DataFrame named stations, which we created in the beginning of the chapter (see Creating a DataFrame), to a CSV file named 'stations.csv' in the output directory. The additional index=False argument specifies that we don’t want to export the index values, which is usually the case:
stations.to_csv('output/stations.csv', index=False)
Note that the file is exported to a sub-directory named output (which must already exist!).
The way the resulting file stations.csv is displayed in a spreadsheet program, such as Microsoft Excel or LibreOffice Calc, is shown in Fig. 27.
Fig. 27 The DataFrame named stations when exported to CSV file and opened in LibreOffice Calc#
More exercises#
Exercise 05-g
Import the file
'education.csv'into aDataFrameRename the columns as shown in Missing data in pandas
How many observations with non-missing
'old_name'are there?How many observations with non-missing
'address'and non-missing'old_name'are there?How many unique institution types are there?
Create a
listof the unique types and print it
Exercise solutions#
Exercise 05-d#
import pandas as pd
dat = pd.read_csv('data/ZonAnn.Ts+dSST.csv')
dat['Year'].iloc[dat.shape[0]-1]
np.int64(2024)
Exercise 05-g#
import pandas as pd
edu = pd.read_csv('data/education.csv')
edu = edu.rename(columns={
'Institutio': 'name',
'ADDRESS': 'address',
'oldName': 'old_name'
})
(~edu['old_name'].isna()).sum()
np.int64(67)
(~edu['address'].isna() & ~edu['old_name'].isna()).sum()
np.int64(59)
edu['type'].nunique()
5
edu['type'].unique().tolist()
['גן ילדים', 'מרכז מדעים', 'חווה חקלאי', 'על יסודי', 'בית ספר']