Problem Set 7 - Due Thu, Mar 23 at 23:59 EDT
Reading
- Slides and notebooks from Lec 12, Introduction to Recursion, Lec 13, Turtle Recursion
- Think Python, Sec. 5.8: Recursion
About this Problem Set
This problem set is intended to give you practice with recursion, list comprehensions, and data manipulation with lists and dictionaries.
- In Task 1 (Individual Task), you will define a recursive function named
hourglass
that prints an hourglass shape of characters to the screen. - In Task 2 (Individual Task), you will write a recursive function that draws a pattern using Turtle World.
- In Task 3 (Partner Task), you will write a program to generate a path on a Google Map by building a URL, as well as perform some data analysis using list comprehensions and other dict/list/string operations. Use this shared Google Doc to find a pair programming partner. Remember that you can work with the same partner a maximum of TWICE this semester. We will deduct 2 points per day for any students who have not recorded their names on the pair programming google doc by Sunday, Mar 19 at 11:59pm.
- The CS111 Problem Set Guide gives an overview of psets, including a detailed description of individual and partner tasks.
- In Fall 2016, students spent these times:
- Task 1: an average of 1.9 hours (min 0.5 hours, max 7 hours),
- Task 2: an average of 2.2 hours (min 0.5 hours, max 8 hours),
- Task 3: an average of 4.0 hours (min 1 hour, max 8 hours),
All code for this assignment is available in the ps07
folder in the
cs111/download
directory within your cs
server account. This
assignment also uses the Otter Inspector program to help you do a
final check for Task 1, Task 2, Task 3 and your Honor Code form before
you submit. This tool tests only for correct functionality in
specific cases. It does not measure the quality of your program,
which depends not just on correct functionality, but also the
elegance, clarity, efficiency, documentation, and style of the
implementation of this functionality.
Task 1: Hourglasses
This is an individual problem which you must complete on your own, though you may ask for help from the CS111 staff.
In this task, open the provided file named hourglass.py
and define in it a recursive function named hourglass
that prints an hourglass shape of characters to the screen. You must use recursion (no loops are allowed).
The function is invoked as hourglass(indent, width, char1, char2)
.
indent
is the number of spaces printed on the left of each linewidth
is the maximum number of characters printed per line at the topmost and bottommost lines of the hourglass.char1
andchar2
are the two characters in the hourglass. The hourglass should consist of alternating lines of each character, beginning withchar1.
The top line of the hourglass shape consists of width
copies of char1
and begins with no spaces. Each subsequent line has two fewer characters and begins with one more space than the previous line, until a line with just one or two characters is reached. Then the lines start growing again in the opposite order.
If width
is less than or equal to zero, nothing should be printed.
The following invocations of the hourglass
function produce the output shown below.
In [2]: hourglass(0, 11, '&', '%')
&&&&&&&&&&&
%%%%%%%%%
&&&&&&&
%%%%%
&&&
%
&&&
%%%%%
&&&&&&&
%%%%%%%%%
&&&&&&&&&&&
In [3]: hourglass(0, 12, 'x', 'o')
xxxxxxxxxxxx
oooooooooo
xxxxxxxx
oooooo
xxxx
oo
xxxx
oooooo
xxxxxxxx
oooooooooo
xxxxxxxxxxxx
In [4]: hourglass(5, 7, '*', '-')
*******
-----
***
-
***
-----
*******
In [5]: hourglass(0, 1, 'A', 'B')
A
In [6]: hourglass(0, 2, 'C', 'D')
CC
In [7]: hourglass(4, 1, 'E', 'F')
E
In [8]: hourglass(7, 2, 'G', 'H')
GG
In [9]: hourglass(9, 0, 'I', 'J') # Nothing printed for this case
In [10]: hourglass(3, -17, 'K', 'L') # Nothing printed for this case
Note: unlike many of the recursion examples seen in class, hourglass
has more than one base case.
Task 2: Drawing Ls
This is an individual problem which you must complete on your own, though you may ask for help from the CS111 staff.
Inside the ps07
folder, open the file called drawLs.py
. Within drawLs.py
, you will define a new function, drawLs
, so that it causes a turtle to draw a pattern of capital L letters. Your function should take two arguments:
size
indicates the length of the vertical edge of the L shapelevels
indicates the number of levels of recursion.
Each successive L is half the size of its predecessor. Below are example invocations of the drawLs
function with different levels.
![]() drawLs(200,0) |
![]() drawLs(200,1) |
|
![]() drawLs(200,2) |
![]() drawLs(200,3) |
|
![]() drawLs(200,4) |
![]() drawLs(200,5) |
Details on how to draw a single L shape are shown below:

Notes:
- Your
drawLs()
function must be recursive (no loops allowed). - You may (but are not required to) define a helper function to solve this problem. (You should not need more than one.)
- The
drawLs()
function must be invariant with respect to the turtle's heading and position. - The provided
initializeTurtle
function insidedrawLs.py
, sets up the canvas and puts the turtle in a good starting position for drawing all the Ls. The providedrun
function takes a levels argument and invokesinitializeTurtle
before callingdrawLs
with this number of levels:
def run(levels):
initializeTurtle()
drawLs(200,levels)
- The
drawLs.py
file ends with the test invocationrun(3)
that tests the pattern with alevels
argument of 3. To test your program at a different number of levels, either (1) change the3
inrun(3)
to the desired number indrawLs.py
and re-reun the program or (2) invokerun
directly in the Canopy interaction pane with a different number. - There is currently a bug in Canopy that makes it necessary to restart Canopy in order to close the turtle window. However, it is not necessary to close the turtle window to test your program for a different
levels
number. Just invokerun
again. - When testing your function, consider slowing the turtle down using
speed(1)
instead ofspeed(100)
withininitializeTurtle
to get a better sense as to precisely what the turtle is doing.
Task 3: Location Tracker
This task is a partner problem in which you are required to work with a partner as part of a two-person team.
Tracks and Location Entries
Many fitness trackers or GPS devices generate tracks, where each track represents the path you took during some activity (e.g., taking a walk, riding a bike, driving a car) as timestamped sequence of locations that you traveled through. This data can be stored in a file where each line is a location entry with information about one location in the path.
For example, here is a sample track consisting of nine entries that represent a walk from the Science Center to Paramecium Pond:
4,09/25/2015 06:01:07 PM,42.29408,-71.30208
4,09/25/2015 06:01:55 PM,42.29405,-71.30114
4,09/25/2015 06:02:47 PM,42.29405,-71.30029
4,09/25/2015 06:03:42 PM,42.2948,-71.29971
4,09/25/2015 06:04:53 PM,42.2953,-71.29925
4,09/25/2015 06:06:09 PM,42.29547,-71.30099
4,09/25/2015 06:07:13 PM,42.29549,-71.30237
4,09/25/2015 06:08:25 PM,42.29501,-71.30384
4,09/25/2015 06:09:41 PM,42.29493,-71.30498
Each location entry is in so-called CSV (comma-separated value) format, in which fields are separated by commas. In this case, the fields are:
- track ID: an integer identifying the track that the entry is part of (in the above track, the track ID is 4).
- a timestamp that specifies a date and time (entries are in chronological order from earliest to latest)
- a latitude
- a longitude
There are many ways to analyze the data in such a track. For example, we might want to know the total distance traveled between the points of the track, the average speed, or an estimate of the number of calories burned during the walk. We can even use the Google Static Maps API to display the path of this track on a map, by generating a URL that embeds the latitude/longitude data:
https://maps.googleapis.com/maps/api/staticmap?size=600x600&markers=label:S|42.29408,-71.30208&markers=label:E|42.29493,-71.30498&path=42.29408,-71.30208|42.29405,-71.30114|42.29405,-71.30029|42.2948,-71.29971|42.2953,-71.29925|42.29547,-71.30099|42.29549,-71.30237|42.29501,-71.30384|42.29493,-71.30498
Or click here to visit the long URL above
If you click on the above link or copy the long URL to a browser URL bar you should see the following image:
We've given you a sample data file, sampletracks.csv
, containing 8
tracks whose entries are ordered by timestamp:
1,09/25/2015 06:23:01 PM,42.2941,-71.3019
1,09/25/2015 06:23:10 PM,42.29416,-71.30194
1,09/25/2015 06:24:12 PM,42.29404,-71.30106
...
6,09/29/2015 03:50:00 PM,42.20592,-71.2388
6,09/29/2015 03:50:19 PM,42.20449,-71.23774
7,09/29/2015 03:52:24 PM,42.29192,-71.303
7,09/29/2015 03:54:27 PM,42.2918,-71.30425
...
7,09/29/2015 05:31:24 PM,42.29106,-71.30122
8,09/29/2015 05:35:30 PM,42.20403,-71.23852
8,09/29/2015 05:36:53 PM,42.20501,-71.23873
...
8,09/29/2015 05:58:56 PM,42.22341,-71.22407
7,09/29/2015 06:20:39 PM,42.29256,-71.30184
...
7,09/29/2015 06:58:43 PM,42.29391,-71.30196
Entries from different tracks may be interleaved. For example, track 7 begins before track 8, but finishes after track 8.
Your Task
In this problem, you will write a program to read and analyze the tracks from such files. In particular, you'll determine the following statistics for any given track:
- the total distance traveled in the track
- the time that passed from the start to the end of the track
- the average speed during the track
- a visualization of the path of the track, expressed as a URL using the Google Static Maps API (as shown above).
In the file analyzeTracks.py
, we provide a function summarizeTrack
that prints out a summary of the given trackID by calling three helper
functions that you implement. For example, analyzing the track with
ID 4 in sampletracks.csv
should print:
TrackID: 4
Number of locations: 9
Total distance: 0.49 miles
Total time: 0.14 hours
Average speed: 3.46 miles per hour
Path URL: https://maps.googleapis.com/maps/api/staticmap?size=600x600&markers=label:S|42.29408,-71.30208&markers=label:E|42.29493,-71.30498&path=42.29408,-71.30208|42.29405,-71.30114|42.29405,-71.30029|42.2948,-71.29971|42.2953,-71.29925|42.29547,-71.30099|42.29549,-71.30237|42.29501,-71.30384|42.29493,-71.30498
(you can horizontally scroll the above box to view the entire URL) and analyzing the track with ID = 5 should print:
TrackID: 5
Number of locations: 62
Total distance: 4.46 miles
Total time: 0.51 hours
Average speed: 8.69 miles per hour
Path URL: https://maps.googleapis.com/maps/api/staticmap?size=600x600&markers=label:S|42.20417,-71.23804&markers=label:E|42.20414,-71.23802&path=42.20417,-71.23804|42.20457,-71.23657|42.20434,-71.23506|42.20478,-71.23385|42.20568,-71.23345|42.20609,-71.23211|42.20549,-71.23119|42.20455,-71.23127|42.2035,-71.23155|42.20217,-71.23172|42.20139,-71.23238|42.20148,-71.23362|42.20072,-71.23429|42.20028,-71.23536|42.19946,-71.23592|42.19889,-71.23691|42.19863,-71.23813|42.19757,-71.23806|42.19659,-71.23863|42.19549,-71.23887|42.19458,-71.23862|42.19378,-71.23797|42.19343,-71.2368|42.1925,-71.2369|42.19125,-71.23647|42.19008,-71.2371|42.18914,-71.23675|42.18783,-71.2362|42.18655,-71.23632|42.18598,-71.23521|42.18505,-71.23462|42.18396,-71.23465|42.18516,-71.23464|42.1859,-71.23553|42.18578,-71.23689|42.18646,-71.23812|42.18753,-71.23766|42.18846,-71.23836|42.18964,-71.2384|42.19004,-71.23705|42.1908,-71.2363|42.19194,-71.23651|42.19283,-71.23682|42.19365,-71.2379|42.19346,-71.23924|42.19357,-71.24075|42.1945,-71.24153|42.19546,-71.24141|42.19633,-71.24108|42.19728,-71.23995|42.19835,-71.24009|42.19964,-71.23988|42.20097,-71.23974|42.20179,-71.23909|42.20275,-71.23918|42.20347,-71.24039|42.20425,-71.24165|42.20515,-71.24104|42.20607,-71.23975|42.20533,-71.23906|42.20474,-71.23781|42.20414,-71.23802
You will complete three helper functions used by summarizeTrack
and
a fourth function that reads tracks from a file and returns a
dictionary of tracks in the format required by summarizeTrack
. The
following subtasks will guide you through implementing the track
analysis:
- Subtask A steps you through representation of tracks.
- Subtask B describes implementation of three helper functions for
summarizeTask
. - Subtask C describes implementation of
readTracksFromFile
. - Subtask D describes how to put it all together and test the full system.
Subtask A: Familiarize yourself with track entry lists.
Begin by studying analyzeTracks.py
. This contains the function
summarizeTrack
, which has already been implemented for you, plus the
contracts for four functions that you are required to implement.
Several functions must use a list of entries, where each entry is a dictionary that represents one location in the path of a track. Each entry dictionary has two keys:
date
, which is associated with a timestamp of the entry; andlat/lon
, which is associated with a pair of a latitude and longitude (both floating point numbers).
(Here, pair just means a tuple with two elements.)
For example, the track file line:
1,09/25/2015 06:23:01 PM,42.2941,-71.3019
should become the entry dictionary:
{"date": "09/25/2015 06:23:01 PM",
"lat/lon": (42.2941, -71.3019)}
The variable track4Entries
is a predefined list of 9 dictionary
entries that correspond to the 9 locations in the track with trackID
'4'
:
In [3]: track4Entries
Out[3]:
[{'date': '09/25/2015 06:01:07 PM', 'lat/lon': (42.29408, -71.30208)},
{'date': '09/25/2015 06:01:55 PM', 'lat/lon': (42.29405, -71.30114)},
{'date': '09/25/2015 06:02:47 PM', 'lat/lon': (42.29405, -71.30029)},
{'date': '09/25/2015 06:03:42 PM', 'lat/lon': (42.2948, -71.29971)},
{'date': '09/25/2015 06:04:53 PM', 'lat/lon': (42.2953, -71.29925)},
{'date': '09/25/2015 06:06:09 PM', 'lat/lon': (42.29547, -71.30099)},
{'date': '09/25/2015 06:07:13 PM', 'lat/lon': (42.29549, -71.30237)},
{'date': '09/25/2015 06:08:25 PM', 'lat/lon': (42.29501, -71.30384)},
{'date': '09/25/2015 06:09:41 PM', 'lat/lon': (42.29493, -71.30498)}]
There is another predefined variable track5Entries
that holds the 62
dictionary entries in the track with trackID '5'
.
Subtask B: Implement track analysis functions.
Start by implementing the functions trackTime
, trackDistance
, and
trackURL
to inspect tracks. These functions all expect a list of
entry dictionaries as their single argument. Test each of your
function implementations on at least track4Etnries
and
track5Enteries
before continuing.
-
trackTime(entryDicts)
: Returns the total time in hours for the track consisting of the given list of entry dictionaries. For example:In [5]: trackTime(track4Entries) Out[5]: 0.14277777777777778 # The number of hours between 6:01:07 PM and 6:09:41 PM In [6]: trackTime(track5Entries) Out[6]: 0.5133333333333333
-
trackDistance(entryDicts)
: Returns the total distance in miles in the path of the track whose entries are the given list of entry dictionaries. For example:In [7]: trackDistance(track4Entries) Out[7]: 0.4939330869379443 # The total length in miles of the 8 segments between the 9 points of the track In [8]: trackDistance(track5Entries) Out[8]: 4.459767440350546
-
trackURL(entryDicts)
: Returns a Google Static Maps URL that displays the track of this path and marks the locations of the first and last entries. For example:trackURL(track4Entries) Out[9]: 'https://maps.googleapis.com/maps/api/staticmap?size=600x600&markers=label:S|42.29408,-71.30208&markers=label:E|42.29493,-71.30498&path=42.29408,-71.30208|42.29405,-71.30114|42.29405,-71.30029|42.2948,-71.29971|42.2953,-71.29925|42.29547,-71.30099|42.29549,-71.30237|42.29501,-71.30384|42.29493,-71.30498' trackURL(track5Entries) Out[10]: 'https://maps.googleapis.com/maps/api/staticmap?size=600x600&markers=label:S|42.20417,-71.23804&markers=label:E|42.20414,-71.23802&path=42.20417,-71.23804|42.20457,-71.23657|42.20434,-71.23506|42.20478,-71.23385|42.20568,-71.23345|42.20609,-71.23211|42.20549,-71.23119|42.20455,-71.23127|42.2035,-71.23155|42.20217,-71.23172|42.20139,-71.23238|42.20148,-71.23362|42.20072,-71.23429|42.20028,-71.23536|42.19946,-71.23592|42.19889,-71.23691|42.19863,-71.23813|42.19757,-71.23806|42.19659,-71.23863|42.19549,-71.23887|42.19458,-71.23862|42.19378,-71.23797|42.19343,-71.2368|42.1925,-71.2369|42.19125,-71.23647|42.19008,-71.2371|42.18914,-71.23675|42.18783,-71.2362|42.18655,-71.23632|42.18598,-71.23521|42.18505,-71.23462|42.18396,-71.23465|42.18516,-71.23464|42.1859,-71.23553|42.18578,-71.23689|42.18646,-71.23812|42.18753,-71.23766|42.18846,-71.23836|42.18964,-71.2384|42.19004,-71.23705|42.1908,-71.2363|42.19194,-71.23651|42.19283,-71.23682|42.19365,-71.2379|42.19346,-71.23924|42.19357,-71.24075|42.1945,-71.24153|42.19546,-71.24141|42.19633,-71.24108|42.19728,-71.23995|42.19835,-71.24009|42.19964,-71.23988|42.20097,-71.23974|42.20179,-71.23909|42.20275,-71.23918|42.20347,-71.24039|42.20425,-71.24165|42.20515,-71.24104|42.20607,-71.23975|42.20533,-71.23906|42.20474,-71.23781|42.20414,-71.23802'
Notes:
We recommend completing the functions in this order:
-
Define the function
trackTime
first. It does not require any looping or list comprehensions. Use appropriate indexing to access the date values, and then invokes the functiondiffTime
. It should return the time in hours. Test it viatrackTime(track4Entries)
andtrackTime(track5Entries)
. -
Define
trackDistance
. Do not use explicitfor
loops in this function. Instead, use list comprehensions. Also use the built-insum
function. You may wish to use the built-inzip
function as well. Test it viatrackDistance(track4Entries)
andtrackDistance(track5Entries)
. - Define
trackURL
. Do not use explicitfor
loops. Instead, use list comprehensions. Use the built-in string methodjoin
and the built-in functionstr
. Test it viatrackURL(track4Entries)
andtrackURL(track5Entries)
.
Subtask C: Implement readEntriesFromFile
.
Using the provided linesFromFile
helper function,
readEntriesFromFile
should read the contents of the specified file,
and create and return a track dictionary that maps each trackID to a
chronological entry list for the track with that
trackID.
- Each key in this dictionary a trackID. Note that trackIDs are
strings (e.g.,
'4'
), not numbers. - Each value in this dictionary is an entry list (as defined in Subtask A for the track with the corresponding trackID iey.
For example, the reading entries from the track file track4.csv
should create a dictionary where the key '4'
maps to the same entry
list value shown for track4Entries
above:
In [11]: readEntriesFromFile('track4.csv')
Out[11]:
{'4': [{'date': '09/25/2015 06:01:07 PM', 'lat/lon': (42.29408, -71.30208)},
{'date': '09/25/2015 06:01:55 PM', 'lat/lon': (42.29405, -71.30114)},
{'date': '09/25/2015 06:02:47 PM', 'lat/lon': (42.29405, -71.30029)},
{'date': '09/25/2015 06:03:42 PM', 'lat/lon': (42.2948, -71.29971)},
{'date': '09/25/2015 06:04:53 PM', 'lat/lon': (42.2953, -71.29925)},
{'date': '09/25/2015 06:06:09 PM', 'lat/lon': (42.29547, -71.30099)},
{'date': '09/25/2015 06:07:13 PM', 'lat/lon': (42.29549, -71.30237)},
{'date': '09/25/2015 06:08:25 PM', 'lat/lon': (42.29501, -71.30384)},
{'date': '09/25/2015 06:09:41 PM', 'lat/lon': (42.29493, -71.30498)}]}
We have provided several helper functions in the file
tracksHelper.py
that significantly simplify this task. You do
not have to understand how these functions work; you only have to
understand what they do.
-
linesFromFile
takes the name of a file and returns a list of the lines from that file. For example,linesFromFile('sampletracks.csv')
returns a list with 429 elements whose first element is the string1,09/25/2015 06:23:01 PM,42.2941,-71.3019
and whose last element is the string
7,09/29/2015 06:58:43 PM,42.29391,-71.30196
-
twoPointsDistance
takes two latitude/longitude pairs (where each pair is a tuple of two float numbers) and returns the number of miles between their locations. For example:In [36]: twoPointsDistance((42.29408,-71.30208), (42.29405,-71.30114)) Out[36]: 0.04808954510790279
-
diffTime
takes two timestamp strings, and returns the number of seconds from the first to the second. For example:In [37]: diffTime('09/27/2015 05:39:31 PM', '09/27/2015 06:10:19 PM') Out[37]: 1848.0
Notes:
- Your implementation of
readEntriesFromFile
should begin by invokinglinesFromFile
to read all the lines from the specified file. - It should then use a
for
loop to build the track dictionary. - It should also use the string method
split
, the built-in functionfloat
, and the list methodappend
. - First test
readEntriesFromFile
on the filetrack4.csv
(which has just one track with 9 lines). - Once it works on
track4.csv
, test it on the filesampletracks.csv
(which has 429 lines and 9 tracks).
Subtask D: Put it all together.
The provided function summarizeTrack
takes (1) a trackID and (2) a
track dictionary of the form produced by readEntriesFromFile
. It
prints out a summary of the given trackID by calling the three
functions described above. Do not edit the summarizeTrack
function,
but read it so that you understand what your three functions should
do.
Once you have implement all four required functions, you are ready to
test the whole system using summarizeTrack
. Call
test_summarizeTrack('sampletracks.csv')
. This provided testing
function uses readEntriesFromFile
to read the contents of the file
whose name is given as an argument. It then calls summarizeTrack
for each track, where summarizeTrack
itself calls trackDistance
,
trackTime
, and trackURL
. The file sampletracks.csv
has 429
lines and 9 tracks.
Task 4: Honor Code Form and Final Checks
As in the previous problem sets, your honor code submission for this pset will involve
defining entering values for the variables in the honorcode.py
file.
This is a Python file, so your values must be valid Python code (strings or numbers).
If you wrote any function invocations or print statements in your Python files to test your code,
please remove them, comment them out before you submit, or wrap them in a if __name__=='__main__'
block.
Points will be deducted for isolated function invocations or superfluous print statements.
Remember to run otterInspect.py
one final time before you submit to
check that your hourglass.py
and analyzeTracks.py
programs work as
intended, and that your honor code form is complete. While running
your code through otterInspect is not required, it is highly
recommended since it may catch errors in functionality that you
haven't noticed. Remember that this tool tests only for correctness
in specific cases. It does not measure the quality of your program,
which depends not just on correctness, but also elegance, clarity,
efficiency, documentation, and style.
If you have issues running otterInspect.py
,
first check for the following common issues.
- Your code is written in the provided program files in the
ps07
folder, and you haven't deleted or moved any of the existing files, or created new files. - You have selected "Keep directory synced to editor" when running
otterInspect.py
in Canopy - You have followed the specifications here of how your code should be organized into functions.
- If
otterInspect
says you have failed test cases, read the problem set directions carefully to verify exactly what your functions are supposed to do. In particular, be careful about the distinction between print and return, and check that the returned values are of the appropriate types. Precision is important in CS! Points will be deducted for such mistakes.
If you're unable to resolve the issue even after going through this checklist, get in touch with an instructor.
How to turn in this Problem Set
- Save your final
hourglass.py
anddrawLs.py
files in yourps07
folder. - Each team member should save their
analyzeTracks.py
file in theirps07
folder. - Save your completed
honorcode.py
file in theps07
folder as well. - Note: It is critical that the name of the folder you submit is
ps07
, and your submitted files arehourglass.py
,drawLs.py
,analyzeTracks.py
, andhonorcode.py
. In other words, do not rename the folder that you downloaded, do not create new files or folders, and do not delete or rename any of the existing files in this folder. Improperly named files or functions will incur penalties. - Drop your entire
ps07
folder in yourdrop
folder on thecs
server using Cyberduck by 11:59pm on Thursday, Mar 23. - Failure to submit your code before the deadline will result in zero credit for the code portion of PS07.