Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

CPSC 330 Python notes

About this document

This document contains some Python lecture materials from the 1st offering of CPSC 330. We have decided to stop allocated lecture time to this topic and instead have this as reference material.

Plotting with matplotlib

  • We will use matplotlib as our plotting library.

  • For those familiar with MATLAB, this package is based on MATLAB plotting.

  • To use matplotlib, we first import it:

  • We can now use functions in plt to plot things:

<Figure size 640x480 with 1 Axes>
  • You will often see me put a semicolon at the end of a line.

  • This is only relevant to Jupyter; it suppresses the line of “output”..

<Figure size 640x480 with 1 Axes>
  • In your homework assignments, at a minimum, you should have axis labels for every figure that you submit.

<Figure size 640x480 with 1 Axes>
  • If you are plotting multiple curves, make sure you include a legend!

<Figure size 640x480 with 1 Axes>
  • You will likely need to visit the matplotlib.pyplot documentation when trying to do other things.

  • When you save an .ipynb file, the output, including plots, is stored in the file.

    • This is a hassle for git.

    • But it’s also convenient.

    • This is how you will submit plots.

Numpy arrays

Basic numpy is covered in the posted videos, you are expected to have a basic knowledge of numpy.

array([0., 0., 0., 0.])
array([1., 1., 1., 1.])
array([[0.17393037, 0.16991815, 0.18317085], [0.56521734, 0.37050406, 0.33888659]])
0.1699181533555938

Numpy array shapes

One of the most confusing things about numpy: what I call a “1-D array” can have 3 possible shapes:

[1. 1. 1. 1. 1.]
size: 5
ndim: 1
shape: (5,)
[[1. 1. 1. 1. 1.]]
size: 5
ndim: 2
shape: (1, 5)
[[1.]
 [1.]
 [1.]
 [1.]
 [1.]]
size: 5
ndim: 2
shape: (5, 1)
False
False
False

Broadcasting in numpy

  • Arrays with different sizes cannot be directly used in arithmetic operations.

  • Broadcasting describes how numpy treats arrays with different shapes during arithmetic operations.

  • The idea is to vectorize operations to avoid loops and speed up the code.

  • Example: I sell pies on the weekends.

  • I sell 3 types of pies at different prices, and I sold the following number of each pie last weekend.

  • I want to know how much money I made per pie type per day.

Pie cost:
[[20]
 [15]
 [25]]

Pie sales (#):
[[2 3 1]
 [6 3 3]
 [5 3 5]]
  • How can we multiply these two arrays together?

Slowest method: nested loop

array([[ 40., 60., 20.], [ 90., 45., 45.], [125., 75., 125.]])

Faster method: vectorize the loop over rows

array([[ 40., 60., 20.], [ 90., 45., 45.], [125., 75., 125.]])

No-loop method: make them the same size, and multiply element-wise

array([[20, 20, 20], [15, 15, 15], [25, 25, 25]])
array([[ 40, 60, 20], [ 90, 45, 45], [125, 75, 125]])
  • What is np.newaxis?

  • It changes the shape:

(3,)
(3, 1)
(3, 1)
(1, 3)

Fastest method: broadcasting

array([[ 40, 60, 20], [ 90, 45, 45], [125, 75, 125]])
  • numpy does the equivalent of np.repeat() for you - no need to do it explicitly

  • It is debatable whether this code is more readable, but it is definitely faster.

When can we use broadcasting?

Say we want to broadcast the following two arrays:

array([0, 1, 2])
array([1., 1., 1., 1., 1.])
(3,)
(5,)
  • The broadcast will fail because the arrays are not compatible...

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[33], line 1
----> 1 arr1 + arr2

ValueError: operands could not be broadcast together with shapes (3,) (5,) 
  • We can facilitate this broadcast by adding a dimension using np.newaxis.

  • np.newaxis increases the dimension of an array by one dimension.

  • the opposite, reducing a dimension, can be achieved by np.squeeze()

The rules of broadcasting:

  • NumPy compares arrays one dimension at a time. It starts with the trailing dimensions, and works its way to the first dimensions.

  • dimensions are compatible if:

    • they are equal, or

    • one of them is 1.

  • Use the code below to test out array compatibitlity

Introduction to pandas

  • The most popular Python library for tabular data structures

Pandas Series

  • A Series is like a NumPy array but with labels

  • 1-dimensional

  • Can be created from a list, ndarray or dictionary using pd.Series()

  • Labels may be integers or strings

Here are two series of gold medal counts for the 2012 and 2016 Olympics:

  • Like ndarrays we use square brackets [] to index a series

  • BUT, Series can be indexed by an integer location OR a label

Do we expect these two series to be compatible for broadcasting?

  • Unlike ndarrays operations between Series (+, -, /, *) align values based on their LABELS

  • The result index will be the sorted union of the two indexes

Pandas DataFrames

  • The primary Pandas data structure

  • Really just a bunch of Series (with the same index labels) stuck together

  • Made using pd.DataFrame()

Creating a DataFrame with a numpy array

(optional) Creating a DataFrame with a dictionary

Indexing Dataframes

  • There are three main ways to index a DataFrame:

    1. [] (slice for rows, label for columns)

    2. .loc[]

    3. .iloc[]

[] notation
  • you can index columns by single labels or lists of labels

(optional) you can also index rows with [], but you can only index rows with slices

  • this is a little unintuitive, so pandas created two other ways to index a dataframe:

  • for indexing with integers: df.iloc[]

  • for indexing with labels: df.loc[]

Indexing cheatsheet

  • [] accepts slices for row indexing or labels (single or list) for column indexing

  • .iloc[] accepts integers for row/column indexing, and can be single values or lists

  • .loc[] accepts labels for row/column indexing, and can be single values or lists

  • for integer row/named column: df.loc[df.index[#], 'labels']

  • for named row/integer column: df.loc['labels', df.columns[#]]

Break (5 min)

Reading from .csv

  • Most of the time you will be loading .csv files for use in pandas using pd.read_csv()

  • Example dataset: a colleague’s cycling commute to/from UBC everyday

Reading from url

  • you may also want to read directly from an url at times

  • pd.read_csv() accepts urls as input

Reading from other formats

  • pd.read_excel()

  • pd.read_html()

  • pd.read_json()

  • etc

Dataframe summaries

Renaming columns with df.rename()

  • we can rename specific columns using df.rename()

  • there are two options for making permanent dataframe changes:

      1. set the argument inplace=True, e.g., df.rename(..., inplace=True)

      1. re-assign, e.g., df = df.rename(...)

NOTE:

  • the pandas team discourages the use of inplace for a few reasons

  • mostly because not all functions have the argument, hides memory copying, leads to hard-to-find bugs

  • it is recommend to re-assign (method 2 above)

  • we can also change all columns at once using a list

Adding/removing columns with [] and drop()

  • adding a single column

  • dropping a column

  • we can also add/drop multiple columns at a time

Adding/removing rows with [] and drop()

(optional) We can add the row to the end of the dataframe using df.append()

  • but now we have the index label 32 occurring twice (that can be bad! Why?)

  • we need can set ignore_index=True to avoid duplicate index labels

Sorting a dataframe with df.sort_values()

  • use the ascending argument to specify sort order as ascending or descending

(optional) we can sort by multiple columns in succession by passing in lists

  • we can sort a dataframe back to it’s orginal state (based on index) using df.sort_index()

Filtering a dataframe with [] and df.query()

  • we’ve already seen how to filter a dataframe using [], .loc and .iloc notation

  • but what if we want more control?

  • df.query() is a powerful tool for filtering data

  • df.query() accepts a string expression to evaluate, using it’s own syntax

  • we can refer to variables in the environment by prefixing them with an @

Applying functions to a dataframe with df.apply() and df.applymap()

  • many common functions are built into Pandas as dataframe methods

  • e.g., df.mean(), df.round(), df.min(), df.max(), df.sum(), etc.

  • however there will be times when you want to apply a non-built in function

  • df.apply() applies a function column-wise or row-wise

  • the function must be able to operate over an entire row or column at a time

  • you may use functions from other packages, such as numpy

  • or make your own custom function

  • use df.applymap() for functions that accept and return a scalar

  • however, if you’re applying an in-built function, there’s often another (vectorized) way...

  • from Pandas docs “Note that a vectorized version of func often exists, which will be much faster.”