← return to practice.dsc10.com
Instructor(s): Janine Tiefenbruck
This exam was administered in-person. Students were allowed one page of double-sided handwritten notes. No calculators were allowed. The exam consisted of 8 questions worth 100 points total.
Unidentified flying objects (or UFOs) are objects seen in the sky that cannot be explained by the observer. These objects seem to be something other than planes, birds, etc. Some people believe that UFOs are actually extraterrestrial spacecrafts that suggest the possibility of alien life.
UFO sightings are reports describing where and when an individual observed a UFO. The DataFrame ufo has one row for every reported UFO sighting in the state of Nevada. The columns are:
"zipcode" (str): The zip code of the town where the sighting was reported."shape" (str): The reported shape of the UFO, either "cylinder", "sphere", or "disk"."time" (str): The time at which the sighting began, formatted as 24-hour time as "HH:MM"."duration" (str): How long the sighting lasted, formatted using a combination of hours (h), minutes (m), and seconds (s), e.g. "2m" or "1h 45m 3s".The first few rows of ufo are shown below, though ufo has many more rows than pictured.

Throughout this exam, we will refer to ufo repeatedly. Assume that we have already run import babypandas as bpd and import numpy as np.
Complete the implementation of the function minutes which takes as input a single duration as a string and returns that duration in minutes, as an int. Example inputs and outputs are given below.
>>> minutes("22m")
22
>>> minutes("4m 45s") # seconds do not contribute to the output
4
>>> minutes("1h 31m 2s")
91
Fill in blanks (i), (ii), and (iii).
def minutes(d):
total = 0
parts = __(i)__
for part in parts:
if part[-1] == "h":
total = __(ii)__
if part[-1] == "m":
total = __(iii)__
return total
Answer (i): d.split(" ")
Answer (ii): total + int(part.strip("h")) * 60
Answer (iii): total + int(part.strip("m"))
The pieces of a duration are separated by spaces, so d.split(" ") breaks the string into a list with one element per piece. For example, "1h 31m 2s" becomes ["1h", "31m", "2s"].
The for loop then looks at the last character of each piece to decide what unit it is measured in.
"h", it’s a number of hours. Strip off the "h", convert what’s left to an int, and multiply by 60 to convert hours to minutes. Add that to the running total."m", it’s already a number of minutes, so just strip the "m", convert to an int, and add it to the running total.Notice there is no if statement for pieces ending in "s". That’s intentional: seconds do not contribute to the output, so those pieces are simply ignored.
Tracing through minutes("1h 31m 2s"): the piece "1h" contributes 1 \cdot 60 = 60, the piece "31m" contributes 31, and the piece "2s" contributes nothing, for a total of 91.
The average score on this problem was 55%.
Complete the line of code to replace the "duration" column in the ufo DataFrame so that ufo.get(["duration"]) results in the DataFrame shown below (only the first few rows are shown). Utilize the minutes function you defined in part (a).

ufo = ufo.assign(__(i)__)
Answer (i): duration=ufo.get("duration").apply(minutes)
Assigning to a column name that already exists replaces that column, which is what we want here.
ufo.get("duration") is a Series of duration strings. Calling .apply(minutes) on that Series runs the minutes function on every element and returns a new Series of the corresponding integers. Note that we pass minutes without parentheses — we want to hand .apply the function itself, not call it.
Since the new column is named duration, we write it as the keyword argument duration=... inside .assign.
The average score on this problem was 76%.
For the rest of the exam, we’ll assume that ufo contains the updated "duration" column.
The following four expressions are each intended to find the duration of the longest sighting in minutes. Select all that are correct.
ufo.take(np.arange(5)).get("duration").iloc[0]
ufo.get("duration").max()
ufo.sort_values("duration").get("duration").iloc[-1]
ufo.sort_values("duration", ascending=False).get("duration").loc[0]
Answer: Options 2 and 3.
ufo.take(np.arange(5)) keeps only the first five rows, and .iloc[0] then grabs the duration of the very first row. There’s no reason the first row would hold the largest duration, so this is incorrect..max() on the "duration" Series returns the largest duration directly. Correct..sort_values("duration") sorts in ascending order by default, so the largest duration ends up in the last position, and .iloc[-1] retrieves it. Correct..loc[0] looks up the element with index label 0, not the element in position 0. Sorting rearranges the rows but carries the original index labels along, so .loc[0] still returns the duration of the row that was originally row 0. To make this work we’d need .iloc[0] instead.
The average score on this problem was 86%.
Define the variables once and twice as follows.
once = ufo.groupby("zipcode").count()
twice = once.groupby("shape").count()
Determine the value resulting from the following expression. You may indicate that the value cannot be determined or the expression produces an error.
((once.get("duration") < 2) & (once.get("duration") >= 2)).sum()
Answer: 0
The two conditions being combined are exact opposites of one another: a number cannot be both strictly less than 2 and greater than or equal to 2 at the same time. Combining them with & therefore produces a Series containing False in every position, no matter what’s in once.
Summing a Boolean Series counts the number of True values, and since there are none, the result is 0. We don’t need to know anything about the contents of ufo to answer this.
The average score on this problem was 54%.
What can we conclude using only the twice DataFrame? Select only one of the options and in no more than three sentences, explain your answer.
The zip codes where exactly 3 UFO sightings occurred.
The number of sightings for each UFO shape where exactly 3 distinct zip codes were reported.
The number of zip codes where exactly 3 distinct UFO shapes were reported.
The number of zip codes where exactly 3 UFO sightings occurred.
The number of sightings where the UFO shape was spherical.
Answer: The number of zip codes where exactly 3 UFO sightings occurred.
Work from the inside out. once = ufo.groupby("zipcode").count() has one row per zip code, and every column of once holds the same number: the count of sightings in that zip code. In particular, the "shape" column of once is a number of sightings, not a shape.
So twice = once.groupby("shape").count() groups the zip codes by their number of sightings. Its index is a number of sightings and each entry counts how many zip codes had that many sightings. Looking up the row labeled 3 in twice tells us the number of zip codes where exactly 3 sightings occurred.
Here is the code that inspired the other options:
ufo.groupby("zipcode").count()ufo.groupby(["zipcode", "shape"]).count()ufo.groupby(["zipcode", "shape"]).count()ufo.groupby("shape").count()
The average score on this problem was 25%.
The "time" column in ufo describes the time at which a UFO was initially sighted. The times are formatted in 24-hour time which uses hours 00 through 23. For example, 13:51 is the same as 1:51 PM.
We want to create a function named time_of_day which takes in a time and returns the corresponding category, defined by the table below.
| hour range | category |
|---|---|
| 00:00–04:59 | "night" |
| 05:00–11:59 | "morning" |
| 12:00–16:59 | "afternoon" |
| 17:00–23:59 | "evening" |
Example inputs and outputs are given below.
>>> time_of_day("00:00")
"night"
>>> time_of_day("13:37")
"afternoon"
>>> time_of_day("23:59")
"evening"
Consider the following implementations for time_of_day. Which of the following implementations are correct? Select all that apply.
Implementation 1:
def time_of_day(time):
hour_part = int(time.split(":")[0])
if hour_part >= 17 or hour_part < 24:
return "evening"
elif hour_part >= 12 or hour_part < 17:
return "afternoon"
elif hour_part >= 5 or hour_part < 12:
return "morning"
else:
return "night"
Implementation 2:
def time_of_day(time):
hour_part = int(time.split(":")[0])
if hour_part < 5:
return "night"
if hour_part < 12:
return "morning"
if hour_part < 17:
return "afternoon"
if hour_part >= 0:
return "evening"
Implementation 3:
def time_of_day(time):
hour_part = int(time.split(":")[0])
if hour_part >= 17:
return "morning"
elif hour_part >= 12:
return "night"
elif hour_part >= 5:
return "afternoon"
else:
return "evening"
Implementation 1
Implementation 2
Implementation 3
Answer: Implementation 2 only.
All three implementations extract the hour correctly with int(time.split(":")[0]); the difference is in the conditions.
or where they should use and. Every valid hour satisfies hour_part < 24, so the very first condition hour_part >= 17 or hour_part < 24 is True for every time, and the function always returns "evening".if returns immediately, reaching the second if already guarantees hour_part >= 5, reaching the third guarantees hour_part >= 12, and reaching the fourth guarantees hour_part >= 17. So the one-sided comparisons are enough, and the categories come out exactly as specified. (The final if hour_part >= 0 is always True at that point, so it behaves like an else.)"evening", not "morning".
The average score on this problem was 71%.
Suppose we use a correct implementation of time_of_day to add another column called "time_of_day" to the ufo DataFrame. Every entry in the "time" column has a converted value in the "time_of_day" column.

Which type of data visualization should be used to compare average duration to the original "time" column? Select only one of the options.
line plot
scatter plot
bar chart
histogram
Answer: Line plot.
The "time" column is a sequence of times running from 00:00 to 23:59, so it behaves like a numerical variable with a natural order. Here we’re plotting one average duration per time, so we have one variable (average duration) measured across a sequence — exactly the situation a line plot is designed for. Line plots are the standard choice for showing how a quantity changes over time.
A scatter plot would be used for two numerical variables measured on each individual, a bar chart for a categorical variable, and a histogram for the distribution of a single numerical variable.
The average score on this problem was 45%.
Which type of data visualization should be used to compare average duration to the new "time_of_day" column? Select only one of the options.
line plot
scatter plot
bar chart
histogram
Answer: Bar chart.
Unlike "time", the "time_of_day" column is categorical — it takes only the four values "night", "morning", "afternoon", and "evening". Bar charts are used to show a numerical quantity (here, average duration) for each category of a categorical variable.
A line plot would incorrectly suggest a continuous trend between categories, while scatter plots and histograms both require numerical data.
The average score on this problem was 88%.
Janine was in Las Vegas last year when she spotted a UFO. She remembers that the UFO was either disk-shaped or cylinder-shaped, but she does not remember which.
Write a Python expression that evaluates to the time at which this sighting could have occurred. If there are multiple possible sightings matching these conditions, your expression should evaluate to the time of the earliest one.
The zip code of Las Vegas is "89109". The earliest time is the one with the smallest hour value; ties are broken by the smallest minute value (i.e. "00:10" is earlier than "23:57", "04:10" is earlier than "04:11").
Hint: Since "time" is a string in "HH:MM" format, comparing these strings alphabetically gives the same result as comparing them chronologically.
You may write on multiple lines, but your code should represent a single expression in Python.
Answer:
ufo[(ufo.get("zipcode") == "89109") &
((ufo.get("shape") == "disk") |
(ufo.get("shape") == "cylinder"))].get("time").min()
We build this up one piece at a time.
ufo.get("zipcode") == "89109" is a Boolean Series that is True for sightings in Las Vegas.(ufo.get("shape") == "disk") | (ufo.get("shape") == "cylinder") is True for sightings of either shape Janine might have seen. We use | (or) here, not &, since a single sighting can’t be both shapes at once — requiring both would leave us with no rows at all.& keeps only the rows that satisfy both, and querying ufo with that Series gives the DataFrame of candidate sightings.Note the parentheses around each individual comparison. They’re required, because & and | have higher precedence than == in Python.
Finally, .get("time") pulls out the times of those sightings and .min() finds the smallest one. Thanks to the hint, the alphabetically smallest "HH:MM" string is also the chronologically earliest time, so .min() is exactly what we want.
An equivalent approach sorts instead of using .min():
ufo[(ufo.get("zipcode") == "89109") &
((ufo.get("shape") == "disk") |
(ufo.get("shape") == "cylinder"))].sort_values("time").get("time").iloc[0]
The average score on this problem was 61%.
The density histogram below was created using data in the "duration" column of ufo.

Use the histogram above to determine the result of the following expression. You do not have to simplify your answer. Show your work.
ufo[(ufo.get("duration") >= 50) & (ufo.get("duration") < 150)].shape[0] / ufo.shape[0]
Answer: (0.004 \cdot 50) + (0.002 \cdot 50) = 0.30
The expression counts the sightings whose duration is at least 50 minutes and less than 150 minutes, then divides by the total number of sightings. That’s the proportion of sightings in the interval [50, 150).
In a density histogram, the area of a bar equals the proportion of values that fall in that bin. The interval [50, 150) is covered exactly by two bins:
Adding the two areas gives 0.2 + 0.1 = 0.30, so 30\% of sightings fall in this range.
The average score on this problem was 67%.
We want a better look at shorter duration sightings. Consider a DataFrame short which only includes sightings that lasted less than an hour. We plot short with the following command:
short.plot(kind="hist", y="duration", bins=np.arange(0, 70, 10))
What can we conclude using only the new histogram and the number of rows in short? Select all that apply.
The duration in minutes of the shortest recorded sighting.
The number of sightings that were exactly 20 minutes long.
The number of sightings that were strictly less than 30 minutes long.
The number of sightings that were at least 40 minutes long and strictly less than 50 minutes long.
The number of sightings that were over an hour.
Answer: Options 3 and 4.
The bins are np.arange(0, 70, 10), which gives the edges 0, 10, 20, 30, 40, 50, 60 — that is, the bins [0, 10), [10, 20), [20, 30), [30, 40), [40, 50), and [50, 60]. A histogram tells us the proportion of values in each bin, and nothing about how the values are spread out within a bin. Multiplying a proportion by the number of rows in short converts it into a count.
short.short gives the count.short only contains sightings that lasted less than an hour, so sightings over an hour aren’t in this histogram at all. We’d need information about the full ufo DataFrame to answer this.The general rule: a histogram can answer a question only when the interval in the question lines up exactly with bin boundaries.
The average score on this problem was 89%.
The DataFrame farms contains information about eight Nevada farms. Each row includes the name of the farm as the index as well as the "zipcode", "town", and "state" of the farm. The DataFrame farms is shown below in its entirety.

The DataFrame rural only contains zip codes which appear both in ufo and farms. Each row in rural contains the number of "sightings" that occurred in ufo at a given zip code. The DataFrame rural is shown below in its entirety.

What does mystery1 evaluate to? Give your answer as an integer.
mystery1 = rural.merge(farms, left_index=True, right_on="zipcode").shape[0]
Answer: 5
This merge matches the index of rural (the zip codes 89101, 89301, and 89801) against the "zipcode" column of farms. A merge produces one row for every pair of matching rows, so we count the farms sitting in each of rural’s zip codes:
89101: one farm (First Fruits) → 1 row89301: two farms (Rolling Horse, Ana’s Village) → 2 rows89801: two farms (Nevada Hay, Blue Lizard) → 2 rowsThat gives 1 + 2 + 2 = 5 rows in total. The farms in 89406, 89446, and 89808 don’t appear because those zip codes aren’t in rural.
The average score on this problem was 55%.
What does mystery2 evaluate to? Give your answer as an integer.
Hint: You do not need to know all of the rows in the ufo DataFrame.
mystery2 = ufo.merge(farms, on="zipcode").shape[0]
Answer: 17
This merge matches the "zipcode" column of ufo against the "zipcode" column of farms. Each sighting in a given zip code pairs with each farm in that same zip code, so the number of resulting rows is
\text{(sightings in that zip code)} \times \text{(farms in that zip code)}
summed over all zip codes that appear in both DataFrames.
This is exactly where rural comes in: it lists the zip codes appearing in both ufo and farms, along with the number of sightings in each. That’s why we don’t need to see all of ufo.
89101: 1 sighting \times 1 farm = 189301: 5 sightings \times 2 farms = 1089801: 3 sightings \times 2 farms = 6Altogether, 1 + 10 + 6 = 17 rows.
The average score on this problem was 33%.
Ray defines the probability of events regarding a single UFO sighting:
Recall a UFO may either be "cylinder", "sphere", or "disk".
S is the event in which a UFO is sphere-shaped. What is the maximum possible value of P(S)? Show your work.
Answer: 0.1
Every UFO is exactly one of the three shapes, so the three events are mutually exclusive and together cover every possibility:
P(C) + P(D) + P(S) = 1
Substituting P(C) = 0.3 and rearranging,
P(S) = 1 - 0.3 - P(D) = 0.7 - P(D)
To make P(S) as large as possible, we want P(D) as small as possible. We’re told D is at least twice as likely as C, so
P(D) \geq 2 \cdot 0.3 = 0.6
The smallest allowed value is P(D) = 0.6, which gives
P(S) = 0.7 - 0.6 = 0.1
The average score on this problem was 80%.
Marta learns the following about her rural town:
What is the probability that a UFO is sighted and at least one crop circle is created on any given night? You do not have to simplify your answer. Show your work.
Answer: 0.2 \cdot (1 - 0.7^5)
We need both things to happen, so we multiply the probability of a sighting by the probability of at least one crop circle given that a sighting occurred.
The probability of a sighting is 0.2.
For the second piece, “at least one” is easiest to handle through its complement, which is “no crop circles at all.” Each farm independently avoids a crop circle with probability 1 - 0.3 = 0.7, and since the five farms are independent, the probability that none of them gets a crop circle is
0.7^5
So the probability that at least one crop circle is created, on a night when a UFO is sighted, is 1 - 0.7^5.
Multiplying the two pieces together gives
0.2 \cdot (1 - 0.7^5)
The average score on this problem was 57%.
Lauren is a rancher in a small town. She observes that every time a UFO is sighted, 5 different cows go missing (however, they always eventually reappear). There are 100 cows in town, and Lauren owns 23 of the 100 cows.
Complete the simulation below to estimate the probability that exactly two of Lauren’s cows are abducted after a UFO sighting. Assume the missing cows are selected at random from the set of all cows.
Fill in blanks (i), (ii), (iii), and (iv).
all_cows = np.arange(100)
repetitions = 10000
count_event = 0
for i in np.arange(repetitions):
abducted = np.random.choice(__(i)__)
laurens_cows = abducted <= __(ii)__
n = np.count_nonzero(laurens_cows)
if __(iii)__:
count_event = count_event + 1
prob = __(iv)__
Answer (i): all_cows, 5, replace=False
Answer (ii): 22
Answer (iii): n == 2
Answer (iv): count_event / repetitions
all_cows. Since the problem says 5 different cows go missing, we must sample without replacement, hence replace=False.all_cows is np.arange(100), i.e. the numbers 0 through 99. Lauren owns 23 of them, and the natural way to label her cows is as the first 23 numbers, 0 through 22. So a cow belongs to Lauren exactly when its number is less than or equal to 22. The comparison abducted <= 22 produces a Boolean array, and np.count_nonzero counts the True values, so n is the number of Lauren’s cows that were abducted. Note that writing 23 here would incorrectly give Lauren 24 cows.n == 2. Writing n >= 2 would answer a different question.
The average score on this problem was 53%.