<jupyter_start><jupyter_text>Chapter 1 - Importing Data in Python (Part 1)  In this chapter, you'll learn how to import data into Python from all types of flat files, a simple and prevalent form of data storage. You've previously learned how to use NumPy and pandas - you will learn how to use these packages to import flat files, as well as how to customize your imports.   Exploring your working directoryIn order to import data into Python, you should first have an idea of what files are in your working directory.IPython, has a bunch of cool commands, including its magic commands. For example, starting a line with ! gives you complete system shell access. This means that the IPython magic command ! ls will display the contents of your current directory. Your task is to use the IPython magic command ! ls to check out the contents of your current director<jupyter_code>!ls<jupyter_output>Chinook.sqlite
L-L1_LOSC_4_V1-1126259446-32.hdf5
Untitled.ipynb
battledeath.xlsx
disarea.dta
ja_data2.mat
mnist_kaggle_some_rows.csv
sales.sas7bdat
seaslug.txt
titanic_sub.csv<jupyter_text>Why we like flat files and the Zen of PythonIn PythonLand, there are currently hundreds of Python Enhancement Proposals, commonly referred to as PEPs. PEP8, for example, is a standard style guide for Python, written by our sensei __Guido van Rossum__ himself.Another one is PEP20, commonly called the Zen of Python. Its abstract is as follows:>>>Long time Pythoneer Tim Peters succinctly channels the BDFL's guiding principles for Python's design into 20 aphorisms, only 19 of which have been written down.If you don't know what the acronym BDFL stands for, I suggest that you look [here](https://docs.python.org/3.3/glossary.htmlterm-bdfl). You can print the Zen of Python in your shell by typing `import this` into it! You're going to do this now and the 5th aphorism (line) will say something of particular interest.The question you need to answer is: what is the 5th aphorism of the Zen of Python?<jupyter_code>import this<jupyter_output>The Zen of Python, by <NAME>

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!<jupyter_text>__Flat is better than nested.__  Using NumPy to import flat filesIn this exercise, you're now going to load the MNIST digit recognition dataset using the numpy function `loadtxt()` and see just how easy it can be:- The first argument will be the filename.- The second will be the delimiter which, in this case, is a comma.  You can find more information about the MNIST dataset [here](Using NumPy to import flat filesIn this exercise, you're now going to load the MNIST digit recognition dataset using the numpy function loadtxt() and see just how easy it can be:The first argument will be the filename.The second will be the delimiter which, in this case, is a comma.You can find more information about the MNIST dataset [here](http://yann.lecun.com/exdb/mnist/) on the webpage of Yann LeCun, who is currently Director of AI Research at Facebook and Founding Director of the NYU Center for Data Science, among many other things.<jupyter_code>import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# Assign filename to variable: file
file = 'mnist_kaggle_some_rows.csv'

# Load file as array: digits
digits = np.loadtxt(file, delimiter=',')

# Print datatype of digits
print(type(digits))

# Select and reshape a row
im = digits[21, 1:]
im_sq = np.reshape(im, (28, 28))

# Plot reshaped data (matplotlib.pyplot already loaded as plt)
plt.imshow(im_sq, cmap='Greys', interpolation='nearest')<jupyter_output><class 'numpy.ndarray'><jupyter_text>Customizing your NumPy importWhat if there are rows, such as a header, that you don't want to import? What if your file has a delimiter other than a comma? What if you only wish to import particular columns?There are a number of arguments that `np.loadtxt()` takes that you'll find useful:- delimiter changes the delimiter that loadtxt() is expecting, for example, you can use ',' and '\t' for comma-delimited and tab-delimited respectively;- skiprows allows you to specify how many rows (not indices) you wish to skip;- usecols takes a list of the indices of the columns you wish to keep.The file that you'll be importing, digits_header.txt,- has a header-is tab-delimited.  Importing different datatypesThe file seaslug.txt- has a text header, consisting of strings- is tab-delimited.  These data consists of percentage of sea slug larvae that had metamorphosed in a given time period.Due to the header, if you tried to import it as-is using `np.loadtxt()`, Python would throw you a `ValueError` and tell you that it could not convert string to float. There are two ways to deal with this: firstly, you can set the data type argument dtype equal to str (for string).Alternatively, you can skip the first row , using the skiprows argument.<jupyter_code># Assign filename: file
file = 'seaslug.txt'

# Import file: data
data = np.loadtxt(file, delimiter='\t', dtype=str)

# Print the first element of data
print(data[0])

# Import data as floats and skip the first row: data_float
data_float = np.loadtxt(file, delimiter='\t', dtype=float, skiprows=1)

# Print the 10th element of data_float
print(data_float[9])

# Plot a scatterplot of the data
plt.scatter(data_float[:, 0], data_float[:, 1])
plt.xlabel('time (min.)')
plt.ylabel('percentage of larvae')<jupyter_output>['Time' 'Percent']
[0.    0.357]<jupyter_text>Working with mixed datatypes (1)Much of the time you will need to import datasets which have different datatypes in different columns; one column may contain strings and another floats, for example. The function `np.loadtxt()` will freak at this. There is another function, `np.genfromtxt()`, which can handle such structures. If we pass `dtype=None` to it, it will figure out what types each column should be.Import 'titanic.csv' using the function np.genfromtxt() as follows:```pythondata = np.genfromtxt('titanic.csv', delimiter=',', names=True, dtype=None)```  Here, the first argument is the filename, the second specifies the delimiter , and the third argument names tells us there is a header. Because the data are of different types, data is an object called a structured array. Because numpy arrays have to contain elements that are all the same type, the structured array solves this by being a 1D array, where each element of the array is a row of the flat file imported. You can test this by checking out the array's shape in the shell by executing np.shape(data).Accessing rows and columns of structured arrays is super-intuitive: to get the ith row, merely execute data[i] and to get the column with name 'Fare', execute data['Fare'].Print the entire column with name Survived to the shell.<jupyter_code>import warnings
warnings.filterwarnings('ignore')
data = np.genfromtxt('titanic_sub.csv', delimiter=',', names=True, dtype=None)
data['Survived']<jupyter_output><empty_output><jupyter_text>Working with mixed datatypes (2)You have just used np.genfromtxt() to import data containing mixed datatypes. There is also another function `np.recfromcsv()` that behaves similarly to np.genfromtxt(), except that its default dtype is `None`. In this exercise, you'll practice using this to achieve the same result.<jupyter_code># Assign the filename: file
file = 'titanic_sub.csv'

# Import file using np.recfromcsv: d
d = np.recfromcsv(file)

# Print out first three entries of d
print(d[:3])<jupyter_output>[(1, 0, 3, b'male', 22., 1, 0, b'A/5 21171',  7.25  , b'', b'S')
 (2, 1, 1, b'female', 38., 1, 0, b'PC 17599', 71.2833, b'C85', b'C')
 (3, 1, 3, b'female', 26., 0, 0, b'STON/O2. 3101282',  7.925 , b'', b'S')]<jupyter_text>Using pandas to import flat files as DataFrames (1)In the last exercise, you were able to import flat files containing columns with different datatypes as numpy arrays. However, the DataFrame object in pandas is a more appropriate structure in which to store such data and, thankfully, we can easily import files of mixed data types as DataFrames using the pandas functions `read_csv()` and `read_table()`.<jupyter_code># Assign the filename: file
file = 'titanic_sub.csv'

# Read the file into a DataFrame: df
df = pd.read_csv(file)

# View the head of the DataFrame
df.head()<jupyter_output><empty_output><jupyter_text>Using pandas to import flat files as DataFrames (2)In the last exercise, you were able to import flat files into a pandas DataFrame. As a bonus, it is then straightforward to retrieve the corresponding numpy array using the attribute values. You'll now have a chance to do this using the MNIST dataset<jupyter_code># Assign the filename: file
file = 'mnist_kaggle_some_rows.csv'

# Read the first 5 rows of the file into a DataFrame: data
data = pd.read_csv(file, nrows = 5, header = None)

# Build a numpy array from the DataFrame: data_array
data_array = data.values

# Print the datatype of data_array to the shell
print(type(data_array))
data_array<jupyter_output><class 'numpy.ndarray'><jupyter_text>Customizing your pandas importThe pandas package is also great at dealing with many of the issues you will encounter when importing data as a data scientist, such as comments occurring in flat files, empty lines and missing values. Note that missing values are also commonly referred to as `NA` or `NaN`. ---  Chapter 2 - Importing data from other file types   You've learned how to import flat files, but there are many other file types you will potentially have to work with as a data scientist. In this chapter, you'll learn how to import data into Python from a wide array of important file types. You will be importing file types such as pickled files, Excel spreadsheets, SAS and Stata files, HDF5 files, a file type for storing large quantities of numerical data, and MATLAB files.  Not so flat any moreIn Chapter 1, you learned how to use the IPython magic command ! ls to explore your current working directory. You can also do this natively in Python using the library `os`, which consists of miscellaneous operating system interfaces.The first line of the following code imports the library os, the second line stores the name of the current directory in a string called wd and the third outputs the contents of the directory in a list to the shell.```pythonimport oswd = os.getcwd()os.listdir(wd)```<jupyter_code>import os
wd = os.getcwd()
os.listdir(wd)<jupyter_output><empty_output><jupyter_text>Loading a pickled fileThere are a number of datatypes that cannot be saved easily to flat files, such as lists and dictionaries. If you want your files to be human readable, you may want to save them as text files in a clever manner. `JSONs`, which you will see in a later chapter, are appropriate for Python dictionaries.However, if you merely want to be able to import them into Python, you can serialize them. All this means is converting the object into a sequence of bytes, or a bytestream.In this exercise, you'll import the pickle package, open a previously pickled data structure from a file and load it.<jupyter_code># Import pickle package
import pickle

# Open pickle file and load data: d
with open('data.pkl', 'rb') as file:
    d = pickle.load(file)

# Print d
print(d)

# Print datatype of d
print(type(d))<jupyter_output><empty_output><jupyter_text>Listing sheets in Excel filesWhether you like it or not, any working data scientist will need to deal with Excel spreadsheets at some point in time. You won't always want to do so in Excel, however!Here, you'll learn how to use pandas to import Excel spreadsheets and how to list the names of the sheets in any loaded .xlsx file.Given an Excel file imported into a variable spreadsheet, you can retrieve a list of the sheet names using the attribute spreadsheet.sheet_names.Specifically, you'll be loading and checking out the spreadsheet 'battledeath.xlsx', modified from the Peace Research Institute Oslo's (PRIO) dataset. This data contains age-adjusted mortality rates due to war in various countries over several years.<jupyter_code># Import pandas
import pandas as pd

# Assign spreadsheet filename: file
file = 'battledeath.xlsx'

# Load spreadsheet: xl
xl = pd.ExcelFile(file)

# Print sheet names
print(xl.sheet_names)<jupyter_output>['2002', '2004']<jupyter_text>Importing sheets from Excel filesIn the previous exercises, you saw that the Excel file contains two sheets, '2002' and '2004'. The next step is to import these.In this exercise, you'll learn how to import any given sheet of your loaded .xlsx file as a DataFrame. You'll be able to do so by specifying either the sheet's name or its index.<jupyter_code># Load a sheet into a DataFrame by name: df1
df1 = xl.parse('2004')

# Print the head of the DataFrame df1
print(df1.head())

# Load a sheet into a DataFrame by index: df2
df2 = xl.parse(0)

# Print the head of the DataFrame df2
print(df2.head())<jupyter_output>War(country)      2004
0  Afghanistan  9.451028
1      Albania  0.130354
2      Algeria  3.407277
3      Andorra  0.000000
4       Angola  2.597931
  War, age-adjusted mortality due to       2002
0                        Afghanistan  36.083990
1                            Albania   0.128908
2                            Algeria  18.314120
3                            Andorra   0.000000
4                             Angola  18.964560<jupyter_text>Awesome! You'll typically find yourself referring to the Excel sheet by name, but it's good to know you can also use indexes.<jupyter_code>### Customizing your spreadsheet import
Here, you'll parse your spreadsheets and use additional arguments to skip rows, rename columns and select only particular columns.

The spreadsheet 'battledeath.xlsx' is already loaded as xl.

As before, you'll use the method parse(). This time, however, you'll add the additional arguments skiprows, names and parse_cols. These skip rows, name the columns and designate which columns to parse, respectively. All these arguments can be assigned to lists containing the specific row numbers, strings and column numbers, as appropriate.<jupyter_output><empty_output><jupyter_text>Parse the first sheet and rename the columns: df1df1 = xl.parse(0, skiprows=[1], names=['Country', 'AAM due to War (2002)']) Print the head of the DataFrame df1print(df1.head()) Parse the first column of the second sheet and rename the column: df2df2 = xl.parse(1, parse_cols=[0], skiprows=[0], names=['Country', 'AAM due to War (2002)']) Print the head of the DataFrame df2print(df2.head())<jupyter_code>### Importing SAS files
In this exercise, you'll figure out how to import a SAS file as a DataFrame using SAS7BDAT and pandas. The file 'sales.sas7bdat' is already in your working directory

The data are adapted from the website of the undergraduate text book [Principles of Econometrics](http://www.principlesofeconometrics.com/sas/) by <NAME> and Lim.<jupyter_output><empty_output><jupyter_text>!pip install sas7bdat<jupyter_code># Import sas7bdat package
from sas7bdat import SAS7BDAT

# Save file to a DataFrame: df_sas
with SAS7BDAT('sales.sas7bdat') as file:
    df_sas = file.to_data_frame()

# Print head of DataFrame
print(df_sas.head())

# Plot histogram of DataFrame features (pandas and pyplot already imported)
pd.DataFrame.hist(df_sas[['P']])
plt.ylabel('count')<jupyter_output>YEAR     P           S
0  1950.0  12.9  181.899994
1  1951.0  11.9  245.000000
2  1952.0  10.7  250.199997
3  1953.0  11.3  265.899994
4  1954.0  11.2  248.500000<jupyter_text>Importing Stata filesHere, you'll gain expertise in importing Stata files as DataFrames using the pd.read_stata() function from pandas<jupyter_code>from IPython import InteractiveShell
InteractiveShell.ast_node_interactivity = 'all'
# Load Stata file into a pandas DataFrame: df
df = pd.read_stata('disarea.dta')

# Print the head of the DataFrame df
df.head()

# Plot histogram of one column of the DataFrame
pd.DataFrame.hist(df[['disa10']])
plt.xlabel('Extent of disease')
plt.ylabel('Number of countries')<jupyter_output><empty_output><jupyter_text>Using h5py to import HDF5 filesThe file 'LIGO_data.hdf5' is already in your working directory. In this exercise, you'll import it using the h5py library. You'll also print out its datatype to confirm you have imported it correctly. You'll then study the structure of the file in order to see precisely what HDF groups it contains.You can find the LIGO data plus loads of documentation and tutorials [here](https://www.gw-openscience.org/events/GW150914/). There is also a great tutorial on Signal Processing with the data [here](https://www.gw-openscience.org/s/events/GW150914/GW150914_tutorial.html).Using h5py to import HDF5 files<jupyter_code>import h5py

# Assign filename: file
file = 'LIGO_data.hdf5'

# Load file: data
data = h5py.File(file, 'r')

# Print the datatype of the loaded file
print(type(data))

# Print the keys of the file
for key in data.keys():
    print(key)<jupyter_output><class 'h5py._hl.files.File'>
meta
quality
strain<jupyter_text>Extracting data from your HDF5 fileIn this exercise, you'll extract some of the LIGO experiment's actual data from the HDF5 file and you'll visualize it.To do so, you'll need to first explore the HDF5 group 'strain'.<jupyter_code># Get the HDF5 group: group
group = data['strain']

# Check out keys of group
for key in group.keys():
    print(key)

# Set variable equal to time series data: strain
strain = data['strain']['Strain'].value

# Set number of time points to sample: num_samples
num_samples = 10000

# Set time vector
time = np.arange(0, 1, 1/num_samples)

# Plot data
plt.plot(time, strain[:num_samples])
plt.xlabel('GPS Time (s)')
plt.ylabel('strain')<jupyter_output>Strain<jupyter_text>Loading .mat filesIn this exercise, you'll figure out how to load a MATLAB file using `scipy.io.loadmat()` and you'll discover what Python datatype it yields.The file `'albeck_gene_expression.mat'` is in your working directory. This file contains gene expression data from the Albeck Lab at UC Davis.<jupyter_code># Import package
import scipy.io

# Load MATLAB file: mat
mat = scipy.io.loadmat('albeck_gene_expression.mat')

# Print the datatype type of mat
print(type(mat))<jupyter_output><class 'dict'><jupyter_text>The structure of .mat in PythonHere, you'll discover what is in the MATLAB dictionary that you loaded in the previous exercise.<jupyter_code># Print the keys of the MATLAB dictionary
print(mat.keys())

# Print the type of the value corresponding to the key 'CYratioCyt'
print(type(mat['CYratioCyt']))

# Print the shape of the value corresponding to the key 'CYratioCyt'
print(np.shape(mat['CYratioCyt']))

# Subset the array and plot it
data = mat['CYratioCyt'][25, 5:]
fig = plt.figure()
plt.plot(data)
plt.xlabel('time (min.)')
plt.ylabel('normalized fluorescence (measure of expression)')<jupyter_output>dict_keys(['__header__', '__version__', '__globals__', 'rfpCyt', 'rfpNuc', 'cfpNuc', 'cfpCyt', 'yfpNuc', 'yfpCyt', 'CYratioCyt'])
<class 'numpy.ndarray'>
(200, 137)<jupyter_text>---  Chapter 3 - Working with relational databases in Python  In this chapter, you'll learn how to extract meaningful data from relational databases, an essential element of any data scientist's toolkit. You will be learning about the relational model, creating SQL queries, filtering and ordering your SQL records, and advanced querying by JOINing database tables.  Creating a database engineHere, you're going to fire up your very first SQL engine. You'll create an engine to connect to the SQLite database `'Chinook.sqlite'`, which is in your working directory. Remember that to create an engine to connect use the commandengine = create_engine('sqlite:///Chinook.sqlite')Here, 'sqlite:///Chinook.sqlite' is called the connection string to the SQLite database Chinook.sqlite. A little bit of background on the Chinook database: the Chinook database contains information about a semi-fictional digital media store in which media data is real and customer, employee and sales data has been manually created.Why the name Chinook, you ask? According to their website,>>>The name of this sample database was based on the Northwind database. Chinooks are winds in the interior West of North America, where the Canadian Prairies and Great Plains meet various mountain ranges. Chinooks are most prevalent over southern Alberta in Canada. Chinook is a good name choice for a database that intends to be an alternative to Northwind.<jupyter_code># Import necessary module
from  sqlalchemy import create_engine

# Create engine: engine
engine = create_engine('sqlite:///Chinook.sqlite')<jupyter_output><empty_output><jupyter_text>What are the tables in the database?In this exercise, you'll once again create an engine to connect to 'Chinook.sqlite'. Before you can get any data out of the database, however, you'll need to know what tables it contains!To this end, you'll save the table names to a list using the method `table_names()` on the engine and then you will print the list.<jupyter_code># Import necessary module
from sqlalchemy import create_engine

# Create engine: engine
engine = create_engine('sqlite:///Chinook.sqlite')

# Save the table names to a list: table_names
table_names = engine.table_names()

# Print the table names to the shell
print(table_names)<jupyter_output>['Album', 'Artist', 'Customer', 'Employee', 'Genre', 'Invoice', 'InvoiceLine', 'MediaType', 'Playlist', 'PlaylistTrack', 'Track']<jupyter_text>The Hello World of SQL Queries!Now, it's time for liftoff! In this exercise, you'll perform the Hello World of SQL queries, SELECT, in order to retrieve all columns of the table Album in the Chinook database. Recall that the query `SELECT * `selects all columns.<jupyter_code># Open engine connection: con
con = engine.connect()

# Perform query: rs
rs = con.execute('SELECT * from Album')

# Save results of the query to DataFrame: df
df = pd.DataFrame(rs.fetchall())

# Close connection
con.close()

# Print head of DataFrame df
df.head()<jupyter_output><empty_output><jupyter_text>Customizing the Hello World of SQL QueriesCongratulations on executing your first SQL query! Now you're going to figure out how to customize your query in order to:- Select specified columns from a table;- Select a specified number of rows;- Import column names from the database table.<jupyter_code># Open engine in context manager
# Perform query and save results to DataFrame: df
with engine.connect() as con:
    rs = con.execute('SELECT LastName, Title FROM Employee')
    df = pd.DataFrame(rs.fetchmany(size=3))
    df.columns = rs.keys()

# Print the length of the DataFrame df
len(df)

# Print the head of the DataFrame df
df.head()<jupyter_output><empty_output><jupyter_text>Filtering your database records using SQL's WHEREYou can now execute a basic SQL query to select records from any table in your database and you can also perform simple query customizations to select particular columns and numbers of rows.There are a couple more standard SQL query chops that will aid you in your journey to becoming an SQL ninja.Let's say, for example that you wanted to get all records from the Customer table of the Chinook database for which the Country is 'Canada'. You can do this very easily in SQL using a SELECT statement followed by a WHERE clause as follows:```SQLSELECT * FROM Customer WHERE Country = 'Canada'```  In fact, you can filter any SELECT statement by any condition using a WHERE clause. This is called filtering your records.In this interactive exercise, you'll select all records of the Employee table for which 'EmployeeId' is greater than or equal to 6.<jupyter_code># Create engine: engine
engine = create_engine('sqlite:///Chinook.sqlite')

# Open engine in context manager
# Perform query and save results to DataFrame: df
with engine.connect() as con:
    rs = con.execute('SELECT * FROM Employee WHERE EmployeeId >= 6')
    df = pd.DataFrame(rs.fetchall())
    df.columns = rs.keys()

# Print the head of the DataFrame df
df.head()<jupyter_output><empty_output><jupyter_text>Ordering your SQL records with ORDER BYYou can also order your SQL query results. For example, if you wanted to get all records from the Customer table of the Chinook database and order them in increasing order by the column SupportRepId, you could do so with the following query:```SQLSELECT * FROM Customer ORDER BY SupportRepId```  In fact, you can order any SELECT statement by any column.In this interactive exercise, you'll select all records of the Employee table and order them in increasing order by the column BirthDate.<jupyter_code># Open engine in context manager
with engine.connect() as con:
    rs = con.execute('SELECT * FROM Employee ORDER BY BirthDate')
    df = pd.DataFrame(rs.fetchall())

    # Set the DataFrame's column names
df.columns = rs.keys()

# Print head of DataFrame
df.head()<jupyter_output><empty_output><jupyter_text>Pandas and The Hello World of SQL Queries!Here, you'll take advantage of the power of pandas to write the results of your SQL query to a DataFrame in one swift line of Python code!You'll first import pandas and create the SQLite 'Chinook.sqlite' engine. Then you'll query the database to select all records from the Album table.<jupyter_code># Create engine: engine
engine = create_engine('sqlite:///Chinook.sqlite')

# Execute query and store records in DataFrame: df
df = pd.read_sql_query('SELECT * FROM Album', engine)

# Print head of DataFrame
df.head()

# Open engine in context manager and store query result in df1
with engine.connect() as con:
    rs = con.execute("SELECT * FROM Album")
    df1 = pd.DataFrame(rs.fetchall())
    df1.columns = rs.keys()

# Confirm that both methods yield the same result
df.equals(df1)<jupyter_output><empty_output><jupyter_text>Pandas for more complex queryingHere, you'll become more familiar with the pandas function read_sql_query() by using it to execute a more complex query: a SELECT statement followed by both a WHERE clause AND an ORDER BY clause.You'll build a DataFrame that contains the rows of the Employee table for which the EmployeeId is greater than or equal to 6 and you'll order these entries by BirthDate.<jupyter_code># Execute query and store records in DataFrame: df
df = pd.read_sql_query(
    "SELECT * FROM Employee WHERE EmployeeId >= 6 ORDER BY BirthDate",
    engine
)

# Print head of DataFrame
df.head()<jupyter_output><empty_output><jupyter_text>The power of SQL lies in relationships between tables: INNER JOINHere, you'll perform your first `INNER JOIN`! You'll be working with your favourite SQLite database, Chinook.sqlite. For each record in the Album table, you'll extract the Title along with the Name of the Artist. The latter will come from the Artist table and so you will need to INNER JOIN these two tables on the ArtistID column of both.<jupyter_code># Open engine in context manager
# Perform query and save results to DataFrame: df
with engine.connect() as con:
    rs = con.execute('SELECT Title, Name FROM Album INNER JOIN Artist on Album.ArtistID = Artist.ArtistID')
    df = pd.DataFrame(rs.fetchall())
    df.columns = rs.keys()

# Print head of DataFrame df
df.head()<jupyter_output><empty_output><jupyter_text>Filtering your INNER JOINCongrats on performing your first INNER JOIN! You're now going to finish this chapter with one final exercise in which you perform an INNER JOIN and filter the result using a WHERE clause.<jupyter_code># Execute query and store records in DataFrame: df
df = pd.read_sql_query('SELECT * FROM PlaylistTrack INNER JOIN Track on PlaylistTrack.TrackId = Track.TrackId WHERE Milliseconds < 250000', engine)

# Print head of DataFrame
df.head()<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.07261959318926922, 'fraction_numerical': 0.014876440934889133, 'mean_word_length': 4.843048403707518, 'pattern_counts': {'":': 0, '<': 115, '<?xml version=': 0, '>': 122, 'https://': 3, 'lorem ipsum': 0, 'www.': 3, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 1, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '30', 'dir': 'jupyter-structured-clean-dedup', 'id': '285236', 'license': 'MIT', 'n_tokens_mistral': 8520, 'n_tokens_neox': 7966, 'n_words': 3845, 'path': 'Skill Tracks/Importing & Cleaning Data with Python/Importing Data in Python (Part 1)/Importing Data in Python (Part 1).ipynb', 'repo_name': 'indmitDS/DataCamp-DataScience', 'size': '28380'}