GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | ||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | ||||||||
GeezerVison is showing devnich.github.io — Programming in Python
Fundamentals (Week 1)OrientationWhat programming language should I use?
Python is pretty good at lots of things
Literate programming and notebooks
Jupyter commandsHow to start Jupyter Lab
Navigation
Writing code
Variables and AssignmentUse variables to store valuesVariables are names for values. Rules for naming things
Use
|
| Shape of data selection | Pandas return type |
|---|---|
| 2D | DataFrame |
| 1D | Series |
| 0D | single value |
Use method chaining
print(subset.max().max())
# Alternatively
print(subset.max(axis=None))
Doing it the hard way
# Which values match the criterion?
subset == subset.max().max()
# Return value at position, code all other cells as NA
val = subset[subset == subset.max().max()]
# Drop rows where all values are NA, then drop columns where values are NA
val.dropna(how="all").dropna(axis=1)
# Putting it all together: Get index label and column label for last remaining cell
names = val.dropna(how="all").dropna(axis=1)
print(names.index[0])
print(names.columns[0])
Frequently there will be a built-in method that solves the same problem
df.idxmax()
df.unique()
df.nunique()
.filter() always returns the same type as the original item, whereas .loc and .iloc might return a data frame or a series.
italy = data.filter(items=["Italy"], axis="index")
print(italy)
print(type(italy))
.filter() is a general-purpose, flexible method
help(data.filter)
data.filter(like="200", axis="columns")
data.filter(like="200", axis="columns").filter(items=["Italy"], axis="index")
Show which data frame elements match a criterion.
# Which GDPs are greater than 10,000?
subset > 10000
Use the criterion match to filter the data frame’s contents. This uses index notation:
df = subset[subset > 10000]
print(df)
subset > 10000 returns a data frame of True/False valuessubset[subset > 10000] filters its contents based on that True/False data frame. All True values are returned, element-wise.NaN.(Optional) Use .where() method to find elements that match the criterion:
df = subset.where(subset > 10000)
print(df)
For example, get the GDP for all countries greater than the median.
# Get the overall median
subset.median() # Returns Series
subset.median(axis=None) # Returns single value
# Which data points are above the median
subset > subset.median(axis=None)
# Return the masked data set
subset[subset > subset.median(axis=None)]
subset (Hint: consult the Pandas documentation to figure out how to get the mean of the rows).# Get the means
subset.mean(axis=1)
# Sort the means. We are illustrating method chaining.
subset.mean(axis=1).sort_values()
# Sort in descending and ranky
subset.mean(axis=1).sort_values(ascending=False).rank()
# Calculate mean GDP per cap for each country
mean_gdp = data.mean(axis=1)
# Calculate the median score
median_score = mean_gdp.median()
# Classify countries as high or low GDP
high_gdp = mean_gdp > median_score
print(mean_gdp)
print(high_gdp)
Data frames are dictionaries of Series:
data["mean_gdp"] = mean_gdp
data["wealthy"] = high_gdp
Group data by category and get group stats
# Get descriptive statistics for the group
data.groupby("wealthy").mean()
data.groupby("wealthy").describe()
(Optional) Sort data by new column
data.sort_values(by="mean_gdp")
Capture the results of your filter in a new file, rather than overwriting your original data.
Create a new subdirectory called “processed” using the editor’s file interface.
Save your data as a CSV in the subdirectory
# Save to a new CSV, preserving your original data
data.to_csv('gapminder_gdp_europe_grouped.csv')
# If you don't want to preserve row names:
data.to_csv('gapminder_gdp_europe_grouped.csv', index=False)
Examples include min, max, mean, std, etc.
Missing values ignored by default
print("Column means")
print(df.mean())
print("Row means")
print(df.mean(axis=1))
Force inclusions with the skipna argument
print("Column means")
print(df.mean(skipna=False))
print("Row means")
print(df.mean(axis=1, skipna=False))
Show which items are missing. “NA” includes NaN and None. It doesn’t include empty strings or numpy.inf.
# Show which items are NA
df.isna()
Count missing values
# Missing by row
print(df.isna().sum())
# Missing by column
print(df.isna().sum(axis=1))
# Aggregate sum
df.isna().sum().sum()
Are any values missing?
df.isna().any(axis=None)
(Optional) Are all of the values missing?
df.isna().all(axis=None)
Drop all rows with missing values
df_drop = df.dropna()
Replace with a fixed value
df_fixed = df.fillna(99)
print(df_fixed)
Replace specific values with replace()
Replace values that don’t meet a criterion with an alternate value
subset_fixed = subset.where(subset > 10000, 99)
print(subset_fixed)
Impute missing values. Read the docs, this may or may not be sufficient for your needs.
df_imputed = df.interpolate()
Recode specific values as missing when importing data (e.g., using read_csv()) by passing in the `na_values` parameter.
A DataFrame is a dictionary of Series columns. With this in mind, experiment with the following code and try to explain what each line is doing. What operation is it performing, and what is being returned?
Feel free to use print(), help(), type(), etc as you investigate.
df["1962"]
df["1962"].notna()
df[df["1962"].notna()]
surveys = pd.read_csv('data/surveys.csv', index_col="record_id")
print(surveys.shape)
df1 = surveys.head(10)
df2 = surveys.tail(10)
df3 = pd.concat([df1, df2])
print(df3.shape)
Import species data
species = pd.read_csv('data/species.csv', index_col="species_id")
print(species.shape)
Join tables on common column. The “left” join is a strategy for augmenting the first table (surveys) with information from the second table (species).
df_join = surveys.merge(species, on="species_id", how="left")
print(df_join.shape)
print(df_join.head())
Use the additional information in the joined data set to inform your analyses:
# Get mean weight for each taxa group
df_join.groupby("taxa")["weight"].mean()
# Get group means for multiple variables
df_join.groupby("taxa")[["weight", "hindfoot_length"]].mean()
# Get nested group means
df_join.groupby(["taxa", "genus"])[["weight", "hindfoot_length"]].mean()
Verify that weight data is missing for non-rodents. This works for str but not obj; if text data has been imported as obj, re-cast it to str.
# Convert column to type "str" if necessary
df_join["taxa"] = df_join["taxa"].astype("str")
# Verify that there aren't any "weight" values for Birds
df_bird = df_join[df_join["taxa"].str.startswith("Bird")]
print(df_bird["weight"].isna().sum())
print(df_bird["weight"].isna().all())
(Optional) The resulting table loses its index because surveys.record_id is not being used in the join. To keep record_id as the index for the final table, we need to retain it as an explicit column.
# Don't set record_id as index during initial import
surveys = pd.read_csv('data/surveys.csv')
df_join = surveys.merge(species, on="species_id", how="left").set_index("record_id")
df_join.head()
Aside: Method chaining formatting options
# Python allows free line breaks inside parens
df_join = (surveys
.merge(species, on="species_id", how="left")
.set_index("record_id")
)
# Use parens as nested break points
df_join = surveys.merge(species, on="species_id", how="left"
).set_index("record_id")
# Explicit line continuation
df_join = surveys.merge(species, on="species_id", how="left") \
.set_index("record_id")
Get the subset of species that match a criterion, and join on that subset. The “inner” join only includes rows where both tables match on the key column; it’s a strategy for filtering the first table by the second table.
# Get the taxa column, masking the rows based on which values match "Bird"
birds = species[species["taxa"] == "Bird"]
df_inner = surveys.merge(birds, on="species_id", how="inner").set_index("record_id")
print("Inner join")
print(df_inner.shape)
print(df_inner.head())
Compare with the results of the left join
df_surveys_left = surveys.merge(birds, on="species_id", how="left").set_index("record_id")
print("Surveys left join")
print(df_surveys_left.shape)
print(df_surveys_left.head())
cf. https://pandas.pydata.org/docs/user_guide/text.html
Import tabular data that contains strings
species = pd.read_csv('data/species.csv', index_col='species_id')
# You can explicitly set all of the columns to type string
# species = pd.read_csv('data/species.csv', index_col='species_id', dtype='string')
# ...or specify the type of individual columns
# species = pd.read_csv('data/species.csv', index_col='species_id',
# dtype = {"genus": "string",
# "species": "string",
# "taxa": "string"})
print(species.head())
print(species.info())
print(species.describe())
A Pandas Series has string methods that operate on the entire Series at once
# Two ways of getting an individual column
print(type(species.genus))
print(type(species["genus"]))
# Inspect the available string methods
print(dir(species["genus"].str))
Use string methods for filtering. This works for str but not obj; if text data has been imported as obj, re-cast it to str.
# Which species are in the taxa "Bird"?
print(species["taxa"].str.startswith("Bird"))
# Filter the dataset to only look at Birds
print(species[species["taxa"].str.startswith("Bird")])
Use string methods to transform and combine data
binomial_name = species["genus"].str.cat(species["species"].str.title(), " ")
species["binomial"] = binomial_name
print(species.head())
A row is a view onto the nth item of each of the column Series. Appending rows is a performance bottleneck because it requires a separate append operation for each Series. You should concatenate data frames instead.
Create a single row as a data frame and concatenate it.
row = pd.DataFrame({"1962": 5000, "1967": 5000, "1972": 5000}, index=["Latveria"])
pd.concat([subset, row])
If you have individual rows as Series, pd.concat() will produce a data frame.
# Get each row as a Series
italy = data.loc["Italy", :]
poland = data.loc["Poland", :]
# Omitting axis argument (or axis=0) concatenates the 2 series end-to-end
# axis=1 creates a 2D data frame
# Transpose recovers original orientation
# Column labels come from Series index
# Row labels come from Series name
pd.concat([italy, poland], axis=1).T
Scikit-Learn documentation: https://scikit-learn.org/stable/
Motivating example: Ordinary least squares regression
from sklearn import linear_model
# Create some random data
x_train = np.random.rand(20)
y = np.random.rand(20)
# Fit a linear model
reg = linear_model.LinearRegression()
reg.fit(x_train.reshape(-1,1), y)
print("Regression slope:", reg.coef_)
Estimate model fit
from sklearn.metrics import r2_score
# Test model fit with new data
x_test = np.random.rand(20)
y_prediction = reg.predict(x_test.reshape(-1,1))
# Get model stats
mse = mean_squared_error(y, y_prediction)
r2 = r2_score(y, y_prediction)
print("R squared:", "{:.3f}".format(r2))
(Optional) Inspect our prediction
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax.scatter(x_train, y, color="black")
ax.plot(x_test, y_prediction, color="blue")
# `fig` in Jupyter Lab
fig.show()
(Optional) Compare with Statsmodels
# Load modules and data
import statsmodels.api as sm
# Fit and summarize OLS model (center data to get accurate model fit
mod = sm.OLS(y - y.mean(), x_train - x_train.mean())
res = mod.fit()
print(res.summary())
Import data
data = pd.read_csv('surveys.csv')
# Check for NaN
print("Valid weights:", data['weight'].count())
print("NaN weights:", data['weight'].isna().sum())
print("Valid lengths:", data['hindfoot_length'].count())
print("NaN lengths:", data['hindfoot_length'].isna().sum())
Fit OLS regression model
from statsmodels.formula.api import ols
model = ols("weight ~ hindfoot_length", data, missing='drop').fit()
print(model.summary())
Generic parameters for all models
import statsmodels
help(statsmodels.base.model.Model)
DataFrames have a huge number of fields and methods, so dir() is not very useful
print(dir(data))
Create a new list that filters out internal attributes
df_joinpublic = [item for item in dir(data) if not item.startswith('_')]
print(df_public)
(Optional) Pretty-print the new list
importort pprint
pp = pprint.PrettyPrinter(width=100, compact=True, indent=2)
pp.pprint(df_public)
Objects have fields (i.e. data/variables) and methods (i.e. functions/procedures). The difference between a method and a function is that methods are attached to objects, whereas functions are free-floating (“first-class citizens”). Methods and functions are “callable”:
# GeneratorExitenerate a list of public methods and a list of public fields. We do this
# by testing each attribute to determine whether it is "callable".
# NB: Because Python allows you to override any attribute at runtime,
# testing with `callable` is not always reliable.
# List of methods (callable attributes)
df_methods = [item for item in dir(data) if not item.startswith('_')
and callable(getattr(data, item))]
# List of fields (non-callable attributes)
df_attr = [item for item in dir(data) if not item.startswith('_')
and not callable(getattr(data, item))]
pp.pprint(df_methods)
pp.pprint(df_attr)
for loop to process files given a list of their namesimport pandas as pd
file_list = ['data/gapminder_gdp_africa.csv', 'data/gapminder_gdp_asia.csv']
for filename in file_list:
data = pd.read_csv(filename, index_col='country')
print(filename)
print(data.head(1))
Get a list of all the CSV files
import glob
glob.glob('data/*.csv')
In Unix, the term “globbing” means “matching a set of files with a pattern”. It uses shell expansion rules, not regular expressions, so there’s an upper limit to how flexible it can be. The most common patterns are:
(Optional) Get a list of all CSV or TSV files
glob.glob('data/*.?sv')
Get a list of all the Gapminder CSV files
glob.glob('data/gapminder_gdp_*.csv')
(Optional) Exclude the “all” CSV file
glob.glob('data/gapminder_[!all]*.csv')
for loop to process batches of filesdata_frames = []
for filename in glob.glob('data/gapminder_gdp_*.csv'):
print(filename)
data = pd.read_csv(filename)
data_frames.append(data)
# Concatenate all columns, adding missing data for columns that aren't shared
all_data = pd.concat(data_frames)
print(all_data.shape)
print(all_data.columns)
# Concatenate only matching columns
common_data = pd.concat(data_frames, join="inner")
print(common_data.shape)
print(common_data.columns)
Value of a variable
mass = 3
print(mass == 3)
print(mass > 5)
print(mass < 4)
Does a file end in "long.csv"?
for filename in glob.glob('data/gapminder*.csv'):
print("Current file:", filename)
print(filename.endswith("long.csv")
Membership in a collection
primes = [2, 3, 5]
print(2 in primes)
print(7 in primes)
Missing values
surveys = pd.read_csv('data/surveys.csv', index_col="record_id")
# Are any values missing?
surveys.isna().any(axis=None)
# Investigate missing values
surveys.isna().sum()
(Optional) Truth of a collection Note that any() and all() evaluate each item using .__bool__() or .__len()__, which tells you whether an item is “truthy” or “falsey” (i.e. interpreted as being true or false).
my_list = [2.75, "green", 0]
print(any(my_list))
print(all(my_list))
(Optional) Understanding “truthy” and “falsey” values in Python (cf. https://stackoverflow.com/a/53198991) Every value in Python, regardless of type, is interpreted as being True except for the following values (which are interpreted as False). “Truthy” values satisfy if or while statements; “Falsey” values do not.
None and False.0, 0.0, 0j, Decimal(0), Fraction(0, 1)'', (), [], {}, set(), range(0)if statements to control whether or not a block of code is executedAn if statement (more properly called a conditional statement) controls whether some block of code is executed or not.
mass = 3.5
if mass > 3.0:
print(mass, 'is large')
mass = 2.0
if mass > 3.0:
print (mass, 'is large')
Structure is similar to a for statement:
if and ends with a colonUse conditionals to decide which files to process
for filename in glob.glob('data/gapminder*.csv'):
if not filename.endswith("long.csv"):
print("Passes test:", filename)
else can be used following an if. This allows us to specify an alternative to execute when the if branch isn’t taken.
if m > 3.0:
print(m, 'is large')
else:
print(m, 'is small')
This lets you explicitly handle the base case
for filename in glob.glob('data/gapminder*.csv'):
if filename.endswith("long.csv"):
print("I don't want any of that")
else:
print("Passes test:", filename)
Expand your reach with compound relations (and, or)
for filename in glob.glob('data/gapminder*.csv'):
if filename.endswith("long.csv") or filename.endswith("wide.csv"):
print("I don't want any of that")
else:
print("Passes test:", filename)
Iterate through all of the CSV files in the data directory. Determine whether the data set has missing data. If it has missing data, print the file name and the missing data counts for each variable.
for filename in glob.glob('data/*.csv'):
df = pd.read_csv(filename)
if df.isna().any(axis=None):
print(filename)
print(df.isna().sum())
elif to specify additional testsMay want to provide several alternative choices, each with its own test; use elif (short for “else if”) and a condition to specify these.
if m > 9.0:
print(m, 'is huge')
elif m > 3.0:
print(m, 'is large')
else:
print(m, 'is small')
if.else (which is the “catch all”).What does this code do? Is it correct? Why or why not?
grade = 85
if grade >= 70:
print('grade is C')
elif grade >= 80:
print('grade is B')
elif grade >= 90:
print('grade is A')
Python steps through the branches of the conditional in order, testing each in turn. Order matters!
and, or, and ParenthesesOften, you want some combination of things to be true. You can combine relations within a conditional using and and or.
mass = [1, 2, 3, 4]
velocity = [4, 3, 2, 1]
for m, v in zip(mass, velocity):
print(m, v)
if m < 2 or v > 2:
print("At least one of our critera is true")
zip()for count, filename in enumerate(glob.glob('data/gapminder_*.csv')):
# Print every other filename
if count % 2 == 0:
print(count, filename)
def with a name, parameters, and a block of codedef print_greeting():
print('Hello!')
def, followed by the name of the function.print_greeting()
Positional arguments
def format_date(year, month, day):
"""Print the formatted date. This works with strings or integers."""
formatted_date = f"{year}/{month}/{day}"
print(formatted_date)
format_date(1871, 3, 19)
(Optional) Keyword arguments
format_date(month=3, day=19, year=1871)
returnUse return ... to give a value back to the caller. return ends the function’s execution and returns you to the code that originally called the function.
def format_date(year, month, day):
"""Print the formatted date. This works with strings or integers."""
formatted_date = f"{year}/{month}/{day}"
return formatted_date
d = format_date(1871, 3, 19)
print(d)
You should explicitly handle common problems. Start with a simple base function.
def average(values):
"""Return average of values."""
return sum(values) / len(values)
Test your function on inputs that should work.
a = average([1, 2, 3, 4])
print(a)
Test against edge cases and erroneous input.
test1 = []
test2 = ["a", "b", "c", "d"]
Revise based on test results.
av1 = average(test1)
def average(values):
"""Return average of values, or None if no values are supplied."""
if len(values) == 0:
return None
else:
return sum(values) / len(values)
You can choose to handle errors within the function, or by validating input ahead of time. Either choice is defensible, just be consistent.
av2 = average(test2)
def average(values):
"""Return average of values, or None if no values are supplied."""
if len(values) == 0:
return None
elif all([type(x) == int for x in values]):
return sum(values) / len(values)
else:
return None
Notes:
return can occur anywhere in the function, but functions are easier to understand if return occurs:
Write a function that takes line as an input and returns the information required by writer.writerow().
Write a function that calculates the mean GDP per capita for a Gapminder dataset. It should do the following:
NB: Test on “Americas” data to show the need for dropping the continent column.
def calc_mean_gdp(filename):
"""Add a mean GDP column to a data frame."""
df = pd.read_csv(filename, index_col = "country")
# If you need to drop the continent column
if "continent" in df.columns:
df.drop("continent", axis=1)
# Get the mean GDP score for each country
mean_gdp = data.mean(axis=1)
df["mean_gdp"] = mean_gdp
return df
df = calc_mean_gdp("data/gapminder_gdp_europe.csv")
Process all of the continent files using your new function. Concatenate the files into a new data frame and save that data frame in a new CSV file.
data_frames = []
for filename in glob.glob('data/gapminder_*.csv'):
# Print a status message
print("Current file:", filename)
df = calc_mean_gdp(filename)
data_frames.append(df)
all_data = pd.concat(data_frames)
all_data.to_csv("gapminder_mean_all.csv", index=False)
https://matplotlib.org/stable/gallery/mplot3d/lorenz_attractor.html
Broadly, a trade-off between managing big code bases and making it easy to experiment. See: https://github.com/elliewix/Ways-Of-Installing-Python/blob/master/ways-of-installing.md#why-do-you-need-a-specific-tool
Python is an interactive interpreter (REPL)
python
Python is a command line program
# hello.py
print("Hello!")
python hello.py
(Optional) Python programs can accept command line arguments as inputs
sys.argv (https://docs.python.org/3/library/sys.html#sys.argv)argparse (https://docs.python.org/3/library/argparse.html)Pathlib provides cross-platform path objects
from pathlib import Path
# Create Path objects
raw_path = Path("data")
processed_path = Path("data/processed")
print("Relative path:", processed_path)
print("Absolute path:", processed_path.absolute())
The file objects have methods that provide much better information about files and directories.
#Note the careful testing at each level of the code.
data_frames = []
if raw_path.exists():
for filename in raw_path.glob('gapminder_gdp_*.csv'):
if filename.is_file():
data = pd.read_csv(filename)
print(filename)
data_frames.append(data)
all_data = pd.concat(data_frames)
# Check for destination folder and create if it doesn't exist
if not processed_path.exists():
processed_path.mkdir()
all_data.to_csv(processed_path.joinpath("combined_data.csv"))
Pandas understands specific file types, but what if you need to work with a generic file?
with open("data/bouldercreek_09_2013.txt", "r") as infile:
lines = infile.readlines()
"bouldercreek_09_2013.txt" is the name of the fileinfile is a variable that refers to the file on disk.readlines() produces the file contents as a list of lines; each line is a string.
print(len(text))
print(type(text))
# View the first 10 lines
print(text[:10])
Compare the following:
# This displays the nicely-formatted document
print(lines[0])
# This shows the true nature of the string; you can see newlines (/n),
# tabs (/t), and other hidden characters
lines[0]
The file contains front matter that we can discard
tabular_lines = []
for line in lines:
if not line.startswith("#"):
tabular_lines.append(line)
Now the first line is tab-separated data. Note that the print statement prints the tabs instead of showing us the \t character.
tabular_lines[0]
outfile_name = "data/tabular_data.txt"
with open(outfile_name, "w") as outfile:
outfile.writelines(tabular_lines)
Strip trailing whitespace
stripped_line = tabular_lines[0].strip()
stripped_line
Split each line into a list based using the tabs.
split_line = stripped_line.split("\t")
split_line
Use a special-purpose library to create a correctly-formatted CSV file
import csv
outfile_name = "data/csv_data.csv"
with open(outfile_name, "w") as outfile:
writer = csv.writer(outfile)
for line in tabular_lines:
csv_line = line.strip().split("\t")
writer.writerow(csv_line)
You can initialize csv.reader and csv.writer with different “dialects” or with custom delimiters and quotechars; see https://docs.python.org/3/library/csv.html
infile_name = "data/bouldercreek_09_2013.txt"
outfile_name = "data/csv_data.csv"
with open(infile_name, "r") as infile, open(outfile_name, "w") as outfile:
writer = csv.writer(outfile)
for line in infile:
if not line.startswith("#"):
writer.writerow(line.strip().split("\t"))
Pandas has utilities for reading fixed-width files: https://pandas.pydata.org/docs/reference/api/pandas.read_fwf.html
Saving datasets with new-style string formatting
for i in datasets_list:
do_something(f'{i}.png'
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
time = [0, 1, 2, 3]
position = [0, 100, 200, 300]
ax.plot(time, position)
type(fig)
print(type(fig))
print(type(ax))
Show figure inline (Jupyter Lab default)
fig
Show figure in a separate window (command line default)
fig.show()
Show figure in a separate window from Jupyter Lab. You may need to specify a different “backend” parameter for matplotlib.use() depending on your exact setup: https://matplotlib.org/stable/tutorials/introductory/usage.html#the-builtin-backends
import matplotlib
matplotlib.use('TkAgg')
fig.show()
Create mock data
import numpy as np
y = np.random.random(10) # outputs an array of 10 random numbers between 0 and 1
x = np.arange(1980,1990,1) # generates an ordered array of numbers from 1980 to 1989
# Check that x and y contain the same number of values
assert len(x) == len(y)
Inspect our data
print("x:", x)
print("y:", y)
Create the basic plot
# Convert y axis into a percentage
y = y * 100
# Draw plot
fig, ax = plt.subplots()
ax.plot(x, y)
Show available styles
# What are the global styles?
plt.style.available
# Set a global figure style
plt.style.use("dark_background")
# The style is only applied to new figures, not pre-existing figures
fig
# Re-creating the figure applies the new style
fig, ax = plt.subplots()
ax.plot(x, y)
Customize the graph In principle, nearly every element on a Matplotlib figure is independently modifiable.
# Set figure size
fig, ax = plt.subplots(figsize=(8,6))
# Set line attributes
ax.plot(x, y, color='darkorange', linewidth=2, marker='o')
# Add title and labels
ax.set_title("Percent Change in Stock X", fontsize=22, fontweight='bold')
ax.set_xlabel(" Years ", fontsize=20, fontweight='bold')
ax.set_ylabel(" % change ", fontsize=20, fontweight='bold')
# Adjust the tick labels
ax.tick_params(axis='both', which='major', labelsize=18)
# Add a grid
ax.grid(True)
Save your figure
fig.savefig("mygraph_dark.png", dpi=300)
In this example, plot GDP over time for multiple countries.
Import data
import pandas as pd
data = pd.read_csv('data/gapminder_gdp_europe.csv', index_col='country')
# Inspect our data
data.head(3)
Transform column headers into an ordinal scale
(Optional) Original column names are object (i.e. string) data
data.columns
Strip off non-numeric portion of each column title
years = data.columns.str.strip('gdpPercap_')
years
Convert years strings into integers and replace original data frame column headers
data.columns = years.astype(int)
Extract rows from the DataFrame
x_years = data.columns
y_austria = data.loc['Austria']
y_bulgaria = data.loc['Bulgaria']
Create the plot object
# Change global background back to default
plt.style.use("default")
# Create GDP figure
fig, ax = plt.subplots(figsize=(8,6))
# Create GDP plot
ax.plot(x_years, y_austria, label='Austria', color='darkgreen', linewidth=2, marker='x')
ax.plot(x_years, y_bulgaria, label='Bulgaria', color='maroon', linewidth=2, marker='o')
# Decorate the plot
ax.legend(fontsize=16, loc='upper center') #automatically uses labels
ax.set_title("GDP of Austria vs Bulgaria", fontsize=22, fontweight='bold')
ax.set_xlabel("Years", fontsize=20, fontweight='bold')
ax.set_ylabel("GDP", fontsize=20, fontweight='bold')
# Fix the display of years in scientific notation
ax.xaxis.get_major_formatter().set_useOffset(False)
Don’t do this.
The basic plot syntax
ax = data.loc['Austria'].plot()
fig = ax.get_figure()
fig
Decorate your Pandas plot
ax = data.loc['Austria'].plot(figsize=(8,6), color='darkgreen', linewidth=2, marker='*')
ax.set_title("GDP of Austria", fontsize=22, fontweight='bold')
ax.set_xlabel("Years",fontsize=20, fontweight='bold' )
ax.set_ylabel("GDP",fontsize=20, fontweight='bold' )
fig = ax.get_figure()
fig
Overlaying multiple plots on the same figure with Pandas. This is super unintuitive.
# Create an Axes object with the Austria data
ax = data.loc['Austria'].plot(figsize=(8,6), color='darkgreen', linewidth=2, marker='*')
print("Austria graph", id(ax))
# Overlay the Bulgaria data on the same Axes object
ax = data.loc['Bulgaria'].plot(color='maroon', linewidth=2, marker='o')
print("Bulgaria graph", id(ax))
The equivalent Matplotlib plot (optional)
# extract the x and y values from dataframe
x_years = data.columns
y_gdp = data.loc['Austria']
# Create the plot
fig, ax = plt.subplots(figsize=(8,6))
ax.plot(x_years, y_gdp, color='darkgreen', linewidth=2, marker='x')
# etc.
## Visualize the same data using a scatterplot
plt.style.use('ggplot')
# Create a scatter plot
fig, ax = plt.subplots(figsize=(8,6))
ax.scatter(y_austria, y_bulgaria, color='blue', linewidth=2, marker='o')
# Decorate the plot
ax.set_title("GDP of Austria vs Bulgaria", fontsize=22, fontweight='bold')
ax.set_xlabel("GDP of Austria",fontsize=20, fontweight='bold' )
ax.set_ylabel("GDP of Bulgaria",fontsize=20, fontweight='bold' )
Seaborn is a set of high-level pre-sets for Matplotlib.
# Import the Seaborn library
import seaborn as sns
ax = sns.lineplot(data=data.T, legend=False, dashes=False)
Let’s make a poster!
Import Iris data set https://gist.githubusercontent.com/curran/a08a1080b88344b0c8a7/raw/0e7a9b0a5d22642a06d3d5b9bcbad9890c8ee534/iris.csv
iris = pd.read_csv("../data/iris.csv")
iris.head()
Create a basic scatter plot
ax = sns.scatterplot(data=iris, x='sepal_length',y='petal_length')
Change plotting theme
# plt.style.use("dark_background")
# Fix grid if necessary
#plt.rcParams["axes.grid"] = False
# Make everything visible at a distance
sns.set_context('poster')
# Color by species
ax = sns.scatterplot(data=iris, x='sepal_length', y='petal_length', hue='species',
palette='colorblind', size='petal_width')
# Place legend
ax.legend(bbox_to_anchor=(2,1))
loc vs. bbox_to_anchor in the legend documentation: https://matplotlib.org/stable/api/legend_api.htmlThe Seaborn plot uses Matplotlib under the hood
# Set the figure size
fig = ax.get_figure()
fig.set_size_inches(8,6)
fig
Box plot
plt.style.use("default")
ax = sns.boxplot(data=iris, x='species', y='petal_length')
Swarm plot
ax = sns.swarmplot(data=iris,x='species', y='petal_length', hue='species', palette='Set1')
ax.legend(loc='upper left', fontsize=16)
ax.tick_params(axis='x', labelrotation = 45)
This gives us a format warning.
Strip plot
ax = sns.stripplot(data=iris,x='species', y='petal_length', hue='species', palette='Set1')
ax.legend(loc='upper left', fontsize=16)
ax.tick_params(axis='x', labelrotation = 45)
Overlapping plots
ax = sns.boxplot(data=iris, x='species', y='petal_length')
sns.stripplot(data=iris, x='species', y='petal_length', ax=ax, palette='Set1', hue='species')
penguins = sns.load_dataset("penguins")
penguin
# Clear previous chart settings
#plt.clf()
sns.set_theme(style="whitegrid")
# Draw a nested barplot by species and sex
fig2 = sns.catplot(
data=penguins, kind="bar",
x="species", y="body_mass_g", hue="sex",
palette="dark", alpha=.6, height=6,
errorbar='sd'
#ci='sd' # This is deprecated in newer versions of Seaborn
)
fig2.despine(left=True)
fig2.set_axis_labels("", "Body mass (g)")
fig2.legend.set_title("")
# Re-display:
#fig2.figure
plt.clf()
sns.set_theme(style="whitegrid", palette="muted")
# Draw a categorical scatterplot to show each observation
fig3 = sns.swarmplot(data=penguins, x="body_mass_g", y="sex", hue="species")
fig3.set(ylabel="")
#fig3.figure
plt.clf()
sns.set_theme()
# Plot bill length and bill depth by species
fig4 = sns.lmplot(
data=penguins,
x="bill_length_mm", y="bill_depth_mm", hue="species",
height=5
)
# Use more informative axis labels than are provided by default
fig4.set_axis_labels("Snoot length (mm)", "Snoot depth (mm)")
#fig4.figure
plt.clf()
sns.set_theme(style="ticks")
fig5 = sns.pairplot(penguins, hue="species")
#fig5.figure
# Save our figure
fig5.savefig("scatterplot_matrix.png", dpi=300)
plt.clf()
sns.set_theme(style="darkgrid")
fig6 = sns.displot(
penguins, x="bill_length_mm", col="species",
binwidth=3, height=3, facet_kws=dict(margin_titles=True),
)
#fig6.figure
#
# (i.e. smoothed histograms)
plt.clf()
sns.set_theme(style="ticks")
# Show the joint distribution using kernel density estimation
fig7 = sns.jointplot(
data=penguins,
x="bill_length_mm", y="bill_depth_mm", hue="species",
kind="kde",
)
#fig7.figure
plt.clf()
sns.set_theme(style="white")
fig8 = sns.PairGrid(penguins, diag_sharey=False)
fig8.map_upper(sns.scatterplot, s=15)
fig8.map_lower(sns.kdeplot)
fig8.map_diag(sns.kdeplot, lw=2)
#fig8.figure
Add styling to individual points
ax = sns.scatterplot(data=iris, x='sepal_length', y='petal_length', hue='species', palette='colorblind', style='species')
Prettify column names
words = [' '.join(i) for i in iris.columns.str.split('_')]
iris.columns = words
Make a regression plot
# Color by species, size by petal width
ax = sns.regplot(data=iris, x='sepal_length', y='petal_length', scatter=True,
scatter_kws={'color':'white'})
Bar Plot
ax = sns.barplot(data=iris, x='species', y='sepal_width', palette='colorblind')
Add custom parameters
# Error bars show standard deviation
ax = sns.barplot(data=iris, x='species', y='sepal_width', ci='sd', edgecolor='black')
(Optional) count plot counts the records in each category
ax = sns.countplot(data=iris, x='species', palette='colorblind')
Histogram of overall data set
ax = sns.histplot(data=iris, x='petal_length', kde=True)
Histogram of data decomposed by category
ax = sns.histplot(data=iris, x='petal_length', hue='species', palette='Set2')
Create multiple subplots to compare binning strategies
# This generates 3 subplots (ncols=3) on the same figure
fig, axes = plt.subplots(figsize=(12,4), nrows=1, ncols=3)
# Note that we can use Seaborn to draw on our Matplotlib figure
sns.histplot(data=iris,x='petal_length', bins=5, ax=axes[0], color='#f5a142')
sns.histplot(data=iris,x='petal_length', bins=10, ax=axes[1], color='maroon')
sns.histplot(data=iris,x='petal_length', bins=15, ax=axes[2], color='darkmagenta')
plt vs axeslegend() vs get legend handles and patchesobject.set_field(value) usage is taken from Java, which was popular in 2003 when Matplotlib was developing its object-oriented syntaxobject.get_field(value)object.field = value. However, the Matplotlib getters and setters do a lot of internal bookkeeping, so if you try to set field values directly you will get errors. For example, compare ax.get_ylabel() with ax.yaxis.label.with open('pettigrew_letters_ORIGINAL.txt', 'r') as file_in:
text = file_in.read()
print(len(text))
Compare the following:
# This displays the nicely-formatted document
print(text[:300])
# This shows the true nature of the string; you can see newlines (/n),
# tabs (/t), and other hidden characters
text[:300]
.read() produces the file contents as one stringtype(text)
.readlines() produces the file contents as a list of lines; each line is a stringwith open('pettigrew_letters_ORIGINAL.txt', 'r') as file_in:
text = file_in.readlines()
print(len(text))
print(type(text))
# View the first 10 lines
text[:10]
manifest_list = text[14:159]
Demonstrate string tests with manifest_list:
# Raw text
for location in manifest_list[:10]:
print(location)
# Remove extra whitespace
for location in manifest_list[:10]:
print(location.strip())
# Test whether the cleaned line starts with 'Box '
for location in manifest_list[:10]:
stripped_line = location.strip()
print(stripped_line.startswith('Box '))
# Test whether the cleaned line starts with 'box '
for location in manifest_list[:10]:
stripped_line = location.strip()
print(stripped_line.startswith('box '))
letters = text[162:]
for line in letters[:25]:
# Create a variables to hold current line and truth value of is_box
stripped_line = line.strip()
is_box = stripped_line.startswith('Box ')
if is_box == True:
print(stripped_line)
# If the line is empty, don't print anything
elif stripped_line == '\n':
continue
# Indent non-Box lines
else:
print('---', stripped_line)
print() statements so that we can see what’s happeningletter_locations = []
for line in letters:
stripped_line = line.strip()
is_box = stripped_line.startswith("Box ")
if is_box == True:
letter_locations.append(stripped_line)
print('Items in manifest:', len(manifest_list))
print('Letters:', len(letter_locations))
Explicitly handle common errors, rather than waiting for your code to blow up.
def average(values):
"Return average of values, or None if no values are supplied."
if len(values) == 0:
return None
return sum(values) / len(values)
print(average([3, 4, 5])) # Prints expected output
print(average([])) # Explicitly handles possible divide-by-zero error
print(average(4)) # Unhandled exception
def average(values):
"Return average of values, or an informative error if bad values are supplied."
try:
return sum(values) / len(values)
except ZeroDivisionError as err:
return err
except TypeError as err:
return err
print(average([3, 4, 5]))
print(average(4))
print(average([]))
from timeit import time
import cProfile
import pstats
def my_fun(val):
# Get 1st timestamp
t1 = time.time()
# do work
# Get 2nd timestamp
t2 = time.time()
print(round(t2 - t1, 3))
# Run the function with the profiler and collect stats
cProfile.run('my_fun(val)', 'dumpstats')
s = pstats.Stats('dumpstats')
with open('pettigrew_letters_ORIGINAL.txt', 'r') as file_in:
for line in file_in:
# Do stuff to current line
pass
import sqlite3
conn = sqlite3.connect('my_database_name.db')
with conn:
c = conn.execute("SELECT column_name FROM table_name WHERE criterion")
results = c.fetchall()
c.close
# Do stuff with `results`