Module 10 Python
Module 10 Python
Data analysis
According to International Data Corporation, an IT industry analyst, the total amount of data
created, captured, copied, and consumed in the world in 2018 was 33 zettabytes (ZB), the
equivalent of 33 trillion gigabytes. The amount of data grew to 59ZB in 2020 and is predicted to
reach a mind-boggling 175ZB by 2025. This growth has created both an opportunity and a
challenge. The opportunity is to convert this data into information that can be used to inform
decision-making. The challenge is finding the data relevant to solving a particular problem and
transforming it into a format that supports the decision-making process. Roughly speaking, the
process for meeting this challenge is:
1. Set your goals. Determine what type of questions you are trying to answer or what
problems you are trying to solve. If you are doing this work for a customer, often this
includes understanding the customer's business needs and how the information will be
used to implement change in the business.
2. Define your target audience. If the user doesn't understand the information you're
presenting, then they won't be able to act on that information in an effective way. For
example, is the end user of the information an IT person, someone in Marketing,
someone who works on a production line, or some other role in a company? Each of
these roles will require different types of information to be presented in different ways to
be as effective as possible.
3. Get the data. A vast amount of raw data is available on the internet and in private
spreadsheets, databases, and other systems that are owned by companies. Your job as
a data analyst is to first locate and identify the data that will support solving the problem,
and then get the data into a form that can be loaded into analysis tools. Often, this
requires finding multiple sources containing parts of the data you need and finding ways
to stitch this data together.
4. Clean the data. Often, the available data is in a raw format and is not exactly what you
need to solve the problem. Therefore, the data must be cleaned before processing can
continue. Some examples of cleaning the data include:
○ Removing unnecessary rows and columns.
○ Sanitizing the data to remove sensitive information that isn't needed for analysis
(like customer or employee identifying information, salaries, etc.). The purpose is
to prevent negative consequences if the data is "leaked."
○ Handling invalid or missing values.
○ Changing data types to more appropriate types.
○ Canonicalizing values by dealing with inconsistencies in the way data is
represented. For example, the same Social Security Number might be
represented as 123-45-6789 in one data source and 123456789 in another data
source. Or, a person might be named John Smith, J. Smith, John Robert Smith,
J. R. Smith, Doctor John Smith, or John Smith, Jr. in different data sources.
Detecting that all of these different strings should (or shouldn't) map to the same
person's name is a difficult problem but will improve the quality of the resulting
information.
5. Prepare the data. Once the data has been cleaned, we can begin massaging it into a
format that supports analysis. Some steps in data preparation include:
○ Adding columns that are derived from other columns.
○ Shaping the data into the forms needed for analysis (such as the format required
for plotting the data).
○ Reviewing the content of the data and the distribution of values within the data.
This often includes creating preliminary visualizations of the data to better
understand it.
○ Checking the data for bias (for example, making sure that all demographic
groups are equally represented in the sample).
6. Analyze the data. In this step, the actual data exploration occurs. This might involve
grouping or aggregating the data in different ways, looking for relationships between
different attributes, and creating simple visualizations to look for relevant factors. In
some cases, the analysis will also include "data science" tasks, such as building models
for classification or prediction using Machine Learning or other statistical modeling
techniques.
7. Visualize the data. Finally, the relevant factors determined during analysis are prepared
for presentation to the end-user. Often, this will include enhancing the simple
visualizations prepared in the previous step and tailoring the visualizations to meet the
target audience’s needs.
Lesson 2: Tools for Data Analysis
Tools for data analysis
Three popular Python packages that are often used for data analysis and visualization are:
These packages can be installed globally using pip, or into a PyCharm project's Virtual
Environment using the PyCharm package installer. To install globally, use the following pip
command:
[Link]
After installing the packages, you can import them into your Python programs using:
import numpy as np
import pandas as pd
import [Link] as plt
Here's an example program you can run to make sure everything is installed and working
correctly:
import pymssql
import pandas as pd
import [Link] as plt
connection = [Link](
server='[Link]',
user='275student',
password='275student',
database='IMDB'
)
sql = """
SELECT startYear, COUNT(*) AS count
FROM title_basics
WHERE titleType = 'tvSeries'
AND startYear BETWEEN 1920 AND 2016
GROUP BY startYear
ORDER BY startYear;
"""
df = pd.read_sql(sql, connection)
[Link](df['startYear'], df['count'])
[Link]("Year the Series Started")
[Link]("Number of Series")
[Link]("Number of TV Series that Started in Each Year")
[Link]()
Click on the accordion title below to expand each step for more information.
Get the data: What the client wants to know is how much money they will
make from their next film. So, ideally, we'd like to find a dataset that
includes both genre and total box office for a large number of movies
and derive average earnings for movies in each genre. Unfortunately,
most of the published data show the total box office for each genre, not
for each movie. For example, the box office data in the graph here:
Statista: North American Box Office Data shows the total box office for
Adventure movies from 1995-2021 was $64 billion. However, what
doesn’t show was the average box office for each Adventure movie or
the total number of Adventure movies produced. So it's hard to estimate
how much a new Adventure movie is likely to earn on average or to
compare the average earnings of an Adventure movie with an Action or
Drama movie.
We also want trend information for each genre on a year-by-year basis
to find genres that are currently trending upward. Thus, we increase the
chances the genre will be hot when the client completes their new film.
Unfortunately, we don't have box office data in precisely the format we
need; however, we have access to IMDB data. It doesn't include box
office data for each movie, but it does have over 670 million movie
ratings covering 478 thousand movies. We know from running the
following code:
import pymssql
import pandas as pd
# This SQL query sums up all the values in the numVotes column in
title_ratings
# for all the movies. That displays the total number of times an IMDB user
rated a movie.
sql = """
SELECT SUM(numVotes) AS totalVotes
FROM title_ratings
JOIN title_basics ON title_ratings.tconst = title_basics.tconst
WHERE titleType = 'movie';
"""
# Read the results of the query and store them in a pandas dataframe.
df = pd.read_sql(sql, connection)
print(df)
count
0 478024
Using the IMDB data represents a compromise we can discuss with the
client. Instead of choosing a genre to maximize earnings, we'll choose a
genre to maximize viewer ratings, which we expect will correlate with
earnings to some degree (movies that are highly rated by moviegoers
tend to do well at the box office). We can also determine the degree of
correlation if we can find a smaller set of movies with their box office
numbers. We can then calculate the statistical correlation between the
ratings data from the IMDB title_ratings table and the box office data for
the smaller sample. This can also be used to create a simple formula for
estimating box office based on the average IMDB rating.
As shown in the sample code above, we can use pyodbc to open a
connection to the database and the Pandas read_sql method to send a
SQL command to the database and wrap the results up in a dataframe.
A dataframe is a Pandas data structure that holds a table worth of data,
including an index, a set of columns, and a set of rows. Here, the queries
we are executing return very simple dataframes - they only include one
column of information (the aggregate number of total votes across all
movies in the title_ratings table for the first query, and the total number
of movies in the title_basics table for the second query), and one row for
each dataframe which includes the aggregate statistics across the entire
database.
Note when the data is already in a SQL database, we can rely on
complex SQL queries to fetch exactly what we need, which reduces the
amount of work we need to do in Pandas to clean and prepare the data.
However, that’s not true when we're reading data from a flat text file
(such as a Comma-Separated Values or CSV text file). When reading a
flat text file, we have to read the entire file into a dataframe and then rely
on Pandas commands to filter exactly what we need.
Clear the data: The first step in cleaning the data is to examine the
values in the dataset. For this problem, we're going to be focusing on the
year a movie was released (the startYear column in the title_basics table
of the IMDB), the genres of each movie (the genre column of the
title_genres table), and the average rating of each movie (the
averageRating column of the title_ratings table). The value that links
these tables together is the unique ID assigned to each movie, which is
found in the tconst column of each table.
We start by looking at the values in the startYear column, as follows:
# Imports and connection to the database are as given above, and
# will not be repeated to keep the code examples as short as possible.
Looking at the plot in Figure 3.1, we see a small number of movies were
released as far back as the late 1800s, but there was a surge in the
number of movies from about 1910-1920. We'll want to skip years before
1920 because there's not enough data to make reliable conclusions.
Towards the middle of the graph, we see a big jump in the number of
movies starting slightly after 2000. Perhaps this is due to the wide
availability of digital recording devices and the rise of the worldwide web,
which allowed streaming services to get their start. These new means of
production and distribution contributed to a reduction in the cost of
making an independent movie and made movies accessible to a wider
audience, which increased the commercial opportunities for creating new
movies. The data sharply declines starting in 2018, which is expected
because the database was built using data from 2017. So, we'll want to
skip all data from 2018 on.
Here is the same SQL query, but we've added a filter to the dataset:
sql = """
SELECT startYear, COUNT(*) AS Count
FROM title_basics
WHERE titleType = 'movie'
GROUP BY startYear;
"""
df = pd.read_sql(sql, connection)
# this line takes the dataframe, filters out all rows where the startYear
is null, or the year is
# less than 1920, or the year is greater than 2017. Then, it sorts the
rows in the dataframe
# in ascending order (the default) by the startYear.
df = df[(df['startYear'].notnull()) & (df['startYear'] >= 1920) &
(df['startYear'] <= 2017)].sort_values("startYear")
# Plot the data. This prot is the same as the one above, but it's running
on a filtered dataframe,
# and we've also added a command to show tick labels every 10 years from
1920 through 2020.
[Link](figsize=(12, 8), dpi=72)
[Link]("startYear", "Count", data=df, marker='o')
[Link](range(1920, 2020, 10))
[Link]()
The query above produces the following output:
startYear Count
12 1920.0 2630
13 1921.0 2590
79 1922.0 2172
80 1923.0 1893
81 1924.0 1928
.. ... ...
60 2013.0 14620
124 2014.0 15389
125 2015.0 16036
126 2016.0 17216
127 2017.0 18969
The plot above in Figure 3.2 looks good and no other cleaning on the
startYear data seems necessary.
Next, we look at the averageRating column in title_ratings. Here's a
histogram showing the distribution of ratings:
# Get all the ratings for all the movies between 1920 and 2017. This takes
a while.
sql = """
SELECT startYear, averageRating
FROM title_basics
JOIN title_ratings ON title_basics.tconst = title_ratings.tconst
WHERE startYear BETWEEN 1920 AND 2017
AND titleType = 'movie';
"""
df = pd.read_sql(sql, connection)
[Link](figsize=(12, 8), dpi=72)
# The matplotlib hist method produces a histogram that shows the frequency
of values
# in different "bins." The IMDB ratings range from 1.0 up to 10.0 by
tenths, so it makes
# sense to use bins from 1-2, from 2-3, ..., from 9-10. That's 9 bins
total.
[Link](df['averageRating'], bins=9)
[Link](range(0, 11, 1))
[Link]()
Figure 3.3 shows the histogram:
Figure 3.3: Histogram of distribution of ratings
This is about what one would expect. The most frequent movie rating is
between 6 and 7, and the ratings look like a standard bell curve. Next,
let's look at a breakdown of the averageRating by year:
# GROUP BY means combine all the data for each individual year into one
row of results.
# That will result in one row each for 1920, 1921, 1922, ..., 2017. Each
row will have a rating
# column, and the value of rating will be the average of all the
individual averageRatings for
# each of the movies in that year.
sql = """
SELECT startYear, AVG(averageRating) AS rating
FROM title_basics
JOIN title_ratings ON title_basics.tconst = title_ratings.tconst
WHERE startYear BETWEEN 1920 AND 2017
AND titleType = 'movie'
GROUP BY startYear
ORDER BY startYear;
"""
df = pd.read_sql(sql, connection)
# Create a simple plot of the results.
[Link](figsize=(12, 8), dpi=72)
[Link]("startYear", "rating", data=df)
[Link]()
Figure 3.4 below shows the resulting graph:
The results look pretty noisy but pay close attention to the Y-axis labels,
which range from 6.0 up to 6.8. That means the average rating only
varied from 6.4 ± 0.4 (6.4 plus or minus 0.4). That's a pretty narrow
range for almost 100 years of movie data and indicates the data is
probably reasonably clean. There may be some additional factors in play
between 1970-2010 that explain why the ratings were slightly depressed
over that time period, but we have no direct evidence for the cause.
Finally, let's look at the genre values in title_genres (we need to JOIN
with title_basics to select only the shows that are movies, with startYear
between 1920 and 2017):
# For each genre, merge all the movies for that genre into a single row.
# The Count column will include the number of movies from that genre.
# Return the results with the most common genres first.
sql = """
SELECT genre, COUNT(*) AS Count
FROM title_genre
JOIN title_basics ON title_genre.tconst = title_basics.tconst
WHERE startYear BETWEEN 1920 AND 2017
AND titleType = 'movie'
GROUP BY genre
ORDER BY Count DESC
"""
df = pd.read_sql(sql, connection)
[Link](figsize=(12, 8), dpi=72)
# Create a bar chart this time, because we are dealing with categories of
# information (different genres) rather than a range of quantities (like
years)
# There will be one bar for each genre.
[Link](df['genre'], df['Count'])
# Rotate the x labels by 90 degrees (vertical orientation) so that they
# don't overlap.
[Link](rotation=90)
# When you rotate the labels, they take up move space than they normally
would.
# The next line tells matplotlib to recalculate the layout so that the
labels
# aren't cut off.
plt.tight_layout()
Below is the resulting bar chart in Figure 3.5:
Figure 3.5: Number of movies per year
Prepare and analyze the data: We already began preparing the data in
the previous step, when we began to create simple visualizations. In this
step, we will continue to answer the "big question," which genre should
we recommend to the client. Let's begin by creating a list of the genres
we want to include in the analysis:
# This is the same query as we saw in the previous example.
# It calculates the number of movies for each genre.
sql = """
SELECT genre, COUNT(*) AS Count
FROM title_genre
JOIN title_basics ON title_genre.tconst = title_basics.tconst
WHERE startYear BETWEEN 1920 AND 2017
AND titleType = 'movie'
GROUP BY genre
ORDER BY Count DESC
"""
df = pd.read_sql(sql, connection)
# Let's filter out all of the genres with less than 20,000 movies.
MIN_MOVIES = 20000
# Select only the rows where the number of movies is greater than
MIN_MOVIES,
# and then return the genres column for only those movies.
genres = df[df['Count'] > MIN_MOVIES]['genre'].values
# This creates a string of comma separated values with single
# quotes around them that we can use as a filter in a SQL query.
# Basically, the code creates a list of each genre in genres, and adds
single
# quotes around each genre in the list. Then, it joins all the strings in
the
# list together using a comma as a separator between each quoted genre.
GENRES = ",".join([ "'" + x + "'" for x in genres])
print(GENRES)
Here's the GENRES string:
'Drama','Documentary','Comedy','Romance','Action','Crime','Thriller'
There ended up being seven genres with more than 20,000 movies. We
could adjust MIN_MOVIES in the code above if we wanted to include
more or fewer genres in the list.
Now let's look at the relationship between genre and overall rating for all
of the genres:
# For each genre, create one row of results by merging all
# the movies in that genre into a single row. Calculate the average
# of the average ratings for all of those movies. Order the results
# in descending order by the average of the average ratings (more highly
# rated genres come before less highly rated genres.
sql = """
SELECT genre, AVG(averageRating) AS rating
FROM title_genre
JOIN title_basics ON title_genre.tconst = title_basics.tconst
JOIN title_ratings ON title_genre.tconst = title_ratings.tconst
WHERE startYear BETWEEN 1920 AND 2017
AND titleType = 'movie'
GROUP BY genre
ORDER BY AVG(averageRating) DESC;
"""
df = pd.read_sql(sql, connection)
We see that some of the highly rated genres have very few movies (for
example, News movies and Talk-Show movies). Let's add our filter on
GENRES:
# The SQL condition:
# fruit in ('Apple','Cherry','Banana')
# returns true for rows where the value in the fruit column is in the list
of strings.
# We're using this syntax to find genres in our list of GENRES.
sql = F"""
SELECT genre, AVG(averageRating) AS rating
FROM title_genre
JOIN title_basics ON title_genre.tconst = title_basics.tconst
JOIN title_ratings ON title_genre.tconst = title_ratings.tconst
WHERE startYear BETWEEN 1920 AND 2017
AND titleType = 'movie'
AND genre IN ({GENRES})
GROUP BY genre
ORDER BY AVG(averageRating) DESC;
"""
df = pd.read_sql(sql, connection)
[Link](figsize=(12, 8), dpi=72)
[Link](df['genre'], df['rating'])
[Link](rotation=90)
plt.tight_layout()
Figure 3.7 below shows the graph:
df = pd.read_sql(sql, connection)
print(df)
Figure 3.8: Average movie ratings for each genre per year
The graph is a bit of a mess! There are a lot of jagged lines, and they
overlap, and it's hard to make out exactly what's going on. Fortunately,
we can smooth the data a bit to show the overall trends without all the
visual clutter.
[Link](figsize=(12, 8), dpi=72)
for genre in genres:
# This is using an "exponential weighted average" to smooth the data
in the forward direction.
smooth = df[df['genre'] == genre]["rating"].ewm(span=20).mean()
# Next we reverse the data.
smooth = [Link](index=[Link][::-1])
# Now we smooth it again in the opposite direction. That just makes
sure that the peaks
# and valleys land in the right places.
smooth = [Link](span=20).mean()
# Finally, we reverse the data again to restore the original order.
smooth = [Link](index=[Link][::-1])
[Link](df[df['genre'] == genre]["startYear"], smooth)
[Link](genres)
[Link]()
Figure 3.9 shows the smoothed graph:
Figure 3.10: Popular documentary movies over the last five years by rating
Though for this one, given the length of the labels, a better use of the
space would probably be a horizontal bar graph (Figure 3.11) using
[Link]():
Figure 3.11: Popular documentary movies over the last five years by rating
sql = F"""
SELECT genre, startYear, AVG(averageRating) AS rating
FROM title_genre
JOIN title_basics ON title_genre.tconst = title_basics.tconst
JOIN title_ratings ON title_genre.tconst = title_ratings.tconst
WHERE genre IN ({GENRES})
AND startYear BETWEEN 1920 AND 2017
AND titleType = 'movie'
GROUP BY genre, startYear
ORDER BY startYear;
"""
df = pd.read_sql(sql, connection)
I've taken the smoothed graph from before but pruned it down to five
popular genres to make it a bit less complicated. I've also added a
descriptive title and labels for the x and y axes.
For the second visualization, I'd probably just produce a table showing
some popular documentaries:
GENRE = "Documentary"
sql = """
SELECT primaryTitle AS Title, startYear AS "Release Year",
averageRating AS "Rating on IMDB", numVotes AS "Number of Votes"
FROM title_genre
JOIN title_basics ON title_genre.tconst = title_basics.tconst
JOIN title_ratings ON title_genre.tconst = title_ratings.tconst
WHERE genre = ?
AND titleType = 'movie'
AND startYear BETWEEN 2012 AND 2017
AND numVotes > 10000
AND averageRating >= 7.5
ORDER BY averageRating DESC
"""
TPB AFK: The Pirate Bay Away from 2013 7.6 12732
Keyboard
# Reads a flat file of comma separated values (CSV) and stores the data in
a dataframe.
my_dataframe = pd.read_csv("my_csv_file.csv")
# Instead of a filename, you can also supply a URL that fetches CSV data.
url = "[Link]
accessType=DOWNLOAD"
mortality_data = pd.read_csv(url)
# As we saw in the case study, Pandas can also read data directly from a
SQL database.
connection = ... open a database connection ...
SQL = ... A string holding a SQL command ...
dataframe_from_database = pd.read_sql(SQL, connection)
Examining a dataframe:
When you print a dataframe in pandas, it looks like this:
startYear count
0 1921 2
1 1922 1
2 1923 2
3 1924 2
4 1927 1
.. ... ...
88 2012 7474
89 2013 7629
90 2014 7699
91 2015 8050
92 2016 7651
Component Description
Column labels The names at the tops of the columns (startYear and count in
the dataframe above).
Column data The data in the columns. All of the data in a column typically has
the same data type with one entry in each row.
Column data types Each column has a defined data type. If all of the elements in a
column don’t have the same data type, the elements are stored
with the object data type.
There are also some handy functions that will give you more insight into
the data:
url = "[Link]
accessType=DOWNLOAD"
mortality_data = pd.read_csv(url)
# Shows a list of the columns along with the number of values and the
types
print("info()")
print("=====================")
mortality_data.info()
nunique()
=====================
Year 119
Age Group 4
Death Rate 430
dtype: int64
describe()
=====================
Year Death Rate
count 476.000000 476.000000
mean 1959.000000 192.924160
std 34.387268 293.224216
min 1900.000000 11.400000
25% 1929.000000 40.575000
50% 1959.000000 89.500000
75% 1989.000000 222.575000
max 2018.000000 1983.800000
Accessing data:
You can select columns in a dataframe using square brackets along with
the column name:
# Produces a column of data along with an index.
# The actual type is <class '[Link]'>
print(mortality_data['Death Rate'])
print("=========================\n")
Death Rate
0 1983.8
1 1695.0
2 1655.7
3 1542.1
4 1591.5
.. ...
471 45.5
472 48.3
473 51.2
474 51.5
475 49.2
# The above line can be more easily understood as two separate lines:
# selected_rows will be a sequence of true/false values
selected_rows = mortality_data['Age Group'] == '1-4 Years'
# When you put a sequence of true/false values inside the square brackets,
# it selects the rows where the value is true.
one_to_four_years = mortality_data[selected_rows]
# Multiple selection criteria can also be combined using & (and) and |
(or).
# It's important to use the parenthesies when you do this because of
operator precedence.
# This will select the rows where the Age Group is 1-4 Years and also the
Year is between 2000 and 2015.
selected = mortality_data[(mortality_data['Age Group'] == '1-4 Years')
& (mortality_data['Year'] >= 2000)
& (mortality_data['Year'] <= 2015)]
# You can combine row selection and column selection. Here's only the
Death Rate series for the above selection:
death_rate_one_to_four_years_from_2000_through_2015 = selected['Death
Rate']
Sorting data:
If you're working with data from a database, you can use the SQL
ORDER BY clause of a SELECT statement to return the rows in a
particular data. However, sometimes, you'll want to specify the order
after the data is already in a dataframe. The sort_values method of
dataframes can be used to do that:
# sort_values doesn't change the order in mortality_data, it returns a
dataframe
# in the right order. So, we have to save that return value in a variable
if
# we want to use it later.
sorted = mortality_data.sort_values('Death Rate', ascending=False)
Running the code (except the first line, which causes an error) produces
this output:
Year Age Group Death Rate Death Percent
0 1900 1-4 Years 1983.8 1.9838
1 1901 1-4 Years 1695.0 1.6950
2 1902 1-4 Years 1655.7 1.6557
3 1903 1-4 Years 1542.1 1.5421
4 1904 1-4 Years 1591.5 1.5915
.. ... ... ... ...
471 2014 15-19 Years 45.5 0.0455
472 2015 15-19 Years 48.3 0.0483
473 2016 15-19 Years 51.2 0.0512
474 2017 15-19 Years 51.5 0.0515
475 2018 15-19 Years 49.2 0.0492
Year Name F M
Year Name
import pandas as pd
import pymssql
import [Link] as plt
name_connection = [Link](
server='[Link]',
database='NAMES',
user='275student',
password='275student'
)
sql = """
SELECT Year, Name, Gender, NameCount
FROM all_data
WHERE Name = 'Lynn';
"""
[Link]()
[Link]("Popularity of the Name Lynn")
[Link]("Year")
[Link]("Number of Babies Named Lynn")
[Link](['Female Babies', 'Male Babies'])
# For a second graph, we've already imported our packages and fetched the
data.
# We just need to do a little more preparation this time.
[Link]()
[Link]("Is Lynn a Female Name or a Male Name?")
[Link]("Year")
[Link]("Percent of Babies Named Lynn Who Were Female")
[Link]()
When you run this in PyCharm, the plots will appear on the SciView tab
(underneath the Database tab on the far right of PyCharm). Click on the
tab to open the SciView pane, and then you should see two plots. Click
on each of the thumbnails to view the corresponding plot. You can also
resize the pane by grabbing an edge and dragging, and zoom in or out
using the buttons above the plot. Figure 5.1 shows a picture of the
SciView pane:
Types of plots
Matplotlib supports many different types of plots, but most are fairly
specific to particular types of problems or problem domains. Rather than
present an exhaustive list, we'll instead focus on the most common types
of plots you're likely to need. These include:
● Line plots - probably, the single most common type of plot, and
where we'll focus the bulk of our attention. Line plots are good for
showing the relationship between two numerical quantities, such
as the number of people with a given name on a year-by-year
basis, or the number of people who come down with COVID-19 on
a given day.
● Bar charts - also, very common. These are good when one of the
values is a category rather than a quantity. For example, if you
want to show the number of babies with a particular name who
were male vs. the number of babies with the same name who were
female, you'd want to use bars to show that instead of a line plot.
Or, if you want to show the average rating for each movie genre,
you'd want to use one bar for each genre, and then the length of
the bar could be used to show the rating.
● Pie plots - similar to bar charts, they show how a quantity is related
to a category. But pie plots are more suitable when the number of
categories is small, and when the relationship shows the
percentage each category represents of a total. For example, a pie
chart would ideally represent the percentage of Americans who
belong to each political party or who are independent.
● Histograms - when you have many numerical data points, and
want to understand the distribution of those values, a histogram is
the right choice. For example, you might want to see the
distribution of ratings on IMDB.
● Scatter plots - good for understanding the relationship between two
numerical quantities, especially when the numerical quantities are
not strongly enough related for a line plot to make sense. For
example, you might want to look at a scatter plot that shows the
popularity of Lynn as a male baby name vs. the popularity of Lynn
as a male baby name on a year-by-year basis. Would you guess
that when Lynn is a popular male name, it is also a popular female
name at the same time, or would you guess when Lynn is a
popular male name, it's an unpopular female name, and vice
versa? A scatter plot can tell you.
Line plots: Line plots are suitable when you have a numeric quantity that
varies in relationship to some other numeric quantity. For example, if you
want to look at the number of movies each year, a line plot would suit
that. Or, if you wanted to look at the popularity of a particular name each
year, the popularity would be a numeric quantity, and the year would be
a numeric quantity, so a line plot would be appropriate for that as well.
To create a line plot, you need a series of data points containing an X
value and a Y value (X values are plotted against a horizontal axis and Y
values are plotted against a vertical axis). Line plots can be created with
the plot method of pyplot. The first two parameters to plot are a set of X
values (which could be a list or a pandas Series) and a set of Y values
(which could also be a list or a pandas Series). The simplest form of a
line plot would be something like this:
import [Link] as plt
import pandas as pd
import pymssql
connection = [Link](
server='[Link]',
database='IMDB',
user='275student',
password='275student'
sql = """
FROM title_basics
GROUP BY startYear
ORDER BY startYear;
"""
df = pd.read_sql(sql, connection)
[Link](df['startYear'], df['count'])
[Link]()
Here, we pull out the series of data containing startYear, pass that as the
x values, and pull out the series of data that includes the count of the
number of movies released in the year and pass that as the y values.
When we show the plot, it looks like this:
Figure 5.2: Number of movies released per year
A slightly way to create a plot with the same data is:
df = pd.read_sql(sql, connection)
[Link]()
In Figure 5.3, we're using green for the line color, showing circular
markers for each data point, and using a dotted line. It looks like this:
Figure 5.3: Number of movies released per year
You'll notice the markers are crowded together pretty closely. We
probably want either a larger chart, or smaller markers, or both:
[Link](figsize=(12, 8), dpi=72)
In Figure 5.4, we've set the size of the current figure (where the current
plot lives) to be bigger and set the marker size to be as small as we
could get it. So now the markers are less crowded, and we can also see
that the line is dotted a little better:
Figure 5.4: Number of movies released per year
You can also set the linewidth to be thicker or thinner.
There are a bunch of different ways to specify the line color, as
documented on the [Link] page, but this list of colors works
and should be helpful:
Here's a list of different marker names you can use:
character description
'x' x marker
'X' x (filled) marker
Linestyle Description
There are other style properties of your line and marker you can change,
but these are the most common ones. For future reference, here's a list
of all the line style properties you can set:
alpha float
xdata [Link]
ydata [Link]
# So shorter values get padded out with spaces (which we don't want).
sql = """
FROM title_basics
OR titleType = 'tvSeries')
ORDER BY startYear;
"""
df = pd.read_sql(sql, connection)
df_wide = df_wide.reset_index()
[Link](df_wide['startYear'], df_wide['movie'])
[Link](df_wide['startYear'], df_wide['tvSeries'])
[Link]()
Here, we're adding separate lines for the number of movies (dark blue)
and the number of tvSeries (orange). Here's the plot:
Figure 5.5: Number of movies and TV series released per year
When you don't specify colors, matplotlib runs through a pre-defined
cycle of colors (you can change the default "cycler," or just include the
color parameter to plot). But either way, there's nothing in the above
diagram that tells the user which line is for movie and which line is for
tvSeries. We need to add a legend to the graph to clear that up. We'll
see how to do that in the next section. However, since we added movies
first and tvSeries second, the default color cycle means the blue line is
movies and the orange line is TV series. You'll notice the TV series line
is a little broken. That's because there were no TV series for those
years, and we ended up with NaN (Not a Number) values in those rows.
If you want to fix that as part of data cleansing, you can replace the NaN
values with 0s.
Another way to plot multiple lines is to supply additional pairs of X and Y
series to the call to plot:
[Link](df_wide['startYear'], df_wide['movie'], df_wide['startYear'],
df_wide['tvSeries'])
This produces the same plot, but I think it's messier and harder to read
than just calling plot twice.
As a side note, you might be wondering why there were any TV series at
all in the 1920s and 1930s when TV hadn't been invented yet. Well, here
are all of the TV series listed before 1930:
0 tt5987 tvSer Palm Beach Palm Beach Fals 1897 NaN NaN
568 ies Daily News Daily News e
1 tt4993 tvSer Urban Legends Urban Legends Fals 1905 NaN NaN
824 ies e
2 tt4046 tvSer British Pathé British Pathé Fals 1910 1970 NaN
852 ies News News e .0
3 tt6964 tvSer A Paul Terry A Paul Terry Fals 1914 NaN 7.0
140 ies Cartoon Cartoon e
5 tt7431 tvSer Aesop's fables Aesop's fables Fals 1921 1933 7.0
484 ies e .0
6 tt6352 tvSer Aesop's Fables Aesop's Fables Fals 1921 1933 7.0
818 ies e .0
7 tt5983 tvSer WSB News WSB News Fals 1922 NaN NaN
102 ies e
8 tt6352 tvSer Alice Comedies Alice Comedies Fals 1923 1927 6.0
816 ies e .0
9 tt7431 tvSer Alice comedies Alice comedies Fals 1923 1927 6.0
464 ies e .0
1 tt6289 tvSer Castle Film Castle Film Fals 1924 1924 9.0
0 264 ies e .0
1 tt0230 tvSer From Sparks: Iz iskry plamya Fals 1924 NaN NaN
1 344 ies Flames e
1 tt6964 tvSer Oswald the Oswald the Fals 1927 NaN 6.0
2 138 ies Lucky Rabbit Lucky Rabbit e
Most of these seem to be short serial films that were shown alongside
films in movie theaters. I'm guessing that rather than create a category
for movieSerials, the data engineers at IMDB just decided to go with
tvSeries as the nearest available category. The more you work with data,
the more you appreciate these weird little exceptional cases.
Bar charts:
Line charts are good for plotting series of datapoints with two numerical
values that can be translated into an X coordinate and a Y coordinate.
But sometimes, you want to plot a numerical value against a non-
numerical value. For example, if you want to know the number of records
in the title_basics table for each type of show. Type of show is not a
numeric quantity. Rather, it's a category. Bar charts are ideally suited for
working with different categories of data. For example, here's the code to
produce a bar chart with the number of shows for each title type:
sql = """
SELECT RTRIM(titleType) AS titleType, COUNT(*) AS count
FROM title_basics
GROUP BY titleType
"""
df = pd.read_sql(sql, connection)
[Link](df['titleType'], df['count'])
[Link]()
The bar method takes an X series and a Y series, just like the plot
method, though some of the optional parameters are different because
we're dealing with bars instead of lines. Here's the resulting graph:
[Link](df['titleType'], df['count'])
# This sets the y-axis labels to "plain" format, which prevents it from
using scientific notation.
plt.ticklabel_format(style='plain', axis='y')
[Link](rotation=45)
[Link]()
FROM title_basics
GROUP BY titleType
"""
df = pd.read_sql(sql, connection)
[Link](df['titleType'], df['count'])
plt.ticklabel_format(style='plain', axis='x')
[Link]()
Figure 5.8: Number of records for each type of show - horizontal bars
Note that the first parameter to barh is still the titleType, and the second
parameter is still the count, even though titleType is now on the Y axis
and count is now on the X axis.
Generally, if I'm ordering the results by count, popularity, cost, or
something similar, I prefer to have the longest bar at the top rather than
the bottom. To clean that up, I can add a call to sort_values:
sql = """
FROM title_basics
GROUP BY titleType
"""
df = pd.read_sql(sql, connection)
# Reverse the order of the rows. We could also change the ORDER BY in the
# SQL statement, but that's only because we're not using TOP. If we were
using TOP,
# we'd end up with the titleTypes with the smallest counts instead of the
largest counts,
df = df.sort_values('count', ascending=True)
[Link](df['titleType'], df['count'])
plt.ticklabel_format(style='plain', axis='x')
[Link]()
sql = """
FROM title_basics
GROUP BY titleType
UNION ALL
FROM title_basics
GROUP BY titleType
"""
df = pd.read_sql(sql, connection)
df = df.reset_index()
df = df.sort_values('after', ascending=False)
[Link](df['titleType'], df['after'])
[Link](df['titleType'], df['before'])
# This sets the y-axis labels to "plain" format, which prevents it from
using scientific notation.
plt.ticklabel_format(style='plain', axis='y')
[Link](rotation=45)
[Link]()
Figure 5.10: Number of shows per title type before 1980 and after 1980
This almost worked, but it drew the bars on top of each other. I ordered
them to draw the "on or after 1980" bars first, since those are taller than
the "before 1980" bars, because otherwise, the "before 1980" bars would
be completely covered up. What we need to do to fix this is make the
bars about half as wide and then offset their positions a little bit so
they're drawn next to each other. This is easier than it sounds:
# Collect the data in long form
sql = """
FROM title_basics
WHERE startYear < 1980
GROUP BY titleType
UNION ALL
FROM title_basics
GROUP BY titleType
"""
df = pd.read_sql(sql, connection)
df = df.reset_index()
df = df.sort_values('after', ascending=False)
# Draw each bar in about half the width, and offset them so that they're
# This sets the y-axis labels to "plain" format, which prevents it from
using scientific notation.
plt.ticklabel_format(style='plain', axis='y')
# Now we can put the tick labels and positions here because we had to use
[Link]()
Figure 5.11: Number of shows per title type before 1980 and after 1980
Pine Plots:
A pie plot might be a good alternative to a bar chart when you have a
small number of categories. For some people, the size of a wedge of pie
is easier to compare visually than two bars. Here's the distribution of
titleTypes for the five most common titleTypes (any more, and the labels
start to jam together):
sql = """
FROM title_basics
GROUP BY titleType
"""
df = pd.read_sql(sql, connection)
# Reverse the order of the rows. We could also change the ORDER BY in the
# SQL statement, but that's only because we're not using TOP. If we were
using TOP,
# we'd end up with the titleTypes with the smallest counts instead of the
largest counts,
df = df.sort_values('count', ascending=True)
[Link](df['count'], labels=df['titleType'])
[Link]()
Note that the counts are supplied as a single dimension, and then the
categories are filled in as an optional label parameter. And here's the
plot itself:
Histograms:
If you're interested in the distribution of values in a numeric column, it's very easy to
create a histogram:
sql = """
SELECT startYear
FROM title_basics
WHERE startYear BETWEEN 1920 AND 2017;
"""
df = pd.read_sql(sql, connection)
[Link](df['startYear'])
plt.ticklabel_format(style='plain', axis='y')
[Link]()
Here's the resulting histogram:
Figure 5.14: Distribution of categories per year in ten bins
The values (startYear in this case) are split into ranges, called "bins." By default, you
get ten equally spaced bins, so if the data ranges from 1920 through 2017, you will end
up with about 9.3 years per bin. However, you can adjust this by specifying a bin’s
parameter. If bins=<an integer>, then that will be the number of bins. If bins=<a list of
numbers>, the numbers specify the cut points between bins. For example:
[Link](df['startYear'], bins=[1920, 1960, 2000, 2010, 2017])
creates 5 bins, from 1920 up to but not including 1960, from 1960 up to but not including
2000, from 2000 up to but not including 2010, and from 2010 up to and including 2017.
Here's the result:
Scatter plots:
Scatter plots are like line plots in that both types of graphs show the relationship
between two numerical values. However, line plots are best suited for data where the X
values are in numerical order, and there is only one value of Y for each distinct value of
X. On the other hand, scatter plots are better for showing weaker relationships between
two numeric quantities. Like line plots, scatter plots can be created by specifying a
series of X values and a series of Y values. But scatter plots only show a marker for
each point, and the markers are not connected in any particular order. For example,
here’s the code to generate a scatter plot with two separate relationships - the
relationship between the number of Sci-Fi shows and the number of Westerns for each
year from 1920-2017, and the relationship between the number of Sci-Fi shows and the
number of Fantasy shows over the same period:
# Read the data in long form
sql = """
SELECT startYear, genre, COUNT(*) AS count
FROM title_basics
JOIN title_genre ON title_basics.tconst = title_genre.tconst
WHERE startYear BETWEEN 1920 AND 2017
GROUP BY startYear, genre;
"""
df = pd.read_sql(sql, connection)
On the other hand, the orange data shows the relationship between Sci-Fi and Fantasy.
When Sci-Fi is popular, Fantasy is as well, and when Sci-Fi is unpopular, then Fantasy
is as well. This shows a high degree of correlation between the popularity of the two
genres.
df = pd.read_sql(sql, connection)
df = df[(df['startYear'].notnull()) & (df['startYear'] >= 1920) &
(df['startYear'] <= 2017)].sort_values("startYear")
[Link](figsize=(12, 8), dpi=72)
[Link]("startYear", "Count", data=df, marker='o')
[Link](range(1920, 2020, 10))
[Link]("Movies Released per Year", fontsize=16)
[Link]("Year", fontsize=14)
[Link]("Number of Movies", fontsize=14)
[Link]("2003, WWW starting\n to take off.", xytext=(1980,
7000), xy=(2003, 5500),
arrowprops={'facecolor': 'black', 'width': 1, 'headwidth': 7,
'shrink': 0.05})
[Link]("1995, Digital Video\n standard set.", xytext=(2000,
1500), xy=(1995, 3500),
arrowprops={'facecolor': 'black', 'width': 1, 'headwidth': 7,
'shrink': 0.05})
[Link]()
Adding gridlines
Adding gridlines to your plot can often help the viewer understand the
data, particularly when reading specific quantities from the axes.
Gridlines can be enabled by calling [Link](True). By default, you'll get
gridlines in both X and Y directions, and you'll get one gridline for each of
the ticks on the X and Y axes. For example:
Figure 5.18: Movies released per year with text on chart and gridlines
As with lines on line plots, you can adjust the line width, line color, line
style, and so on. You can also choose to have grid lines in only the X
direction, only the Y direction, or both. In particular, you probably don't
want to have X gridlines for bar plots, but Y gridlines are handy:
[Link](True, axis='y', linestyle='dotted', color='green')
Results in:
Figure 5.19: Bar chart with horizontal green dotted lines for axis
xkcd
If you haven't read xkcd, check it out. You might like it! Now, matplotlib
has the ability to style your plots to look like xkcd. Why? That's the wrong
question. Do or do not, there is no why.
# Step 1: import our packages
import pandas as pd
import pymssql
import [Link] as plt
name_connection = [Link](
server='[Link]',
database='NAMES',
user='275student',
password='275student'
)
sql = """
SELECT Year, Name, Gender, NameCount
FROM all_data
WHERE Name = 'Lynn';
"""
with [Link]():
[Link](figsize=(12, 8), dpi=72)
[Link]('Year', 'F', data=lynn_wide)
[Link]('Year', 'M', data=lynn_wide)
[Link]()
[Link]("Popularity of the Name Lynn")
[Link]("Year")
[Link]("Number of Babies Named Lynn")
[Link](['Female Babies', 'Male Babies'])
# For a second graph, we've already imported our packages and fetched
the data.
# We just need to do a little more preparation this time.
[Link]()
[Link]("Is Lynn a Female Name or a Male Name?")
[Link]("Year")
[Link]("Percent of Babies Named Lynn Who Were Female")