In this coursework exercise, you will download data from the provided link and read it in as a CSV
file using the Pandas data analysis package for Python.
The data we will use contains a variety of information about cities from around the world.
DataFrame
s¶To complete these tasks you will need to access and filter a DataFrame
.
The DataFrame
data structure has many convenient features for extracting and ordering information. Although conceptually it can be thought of as a comptuational
represention of a table, it is quite a complex data structure and takes a while
to master. The following questions can be done with only a small but powerful
set of DataFrame
operations; and the following examples of typical forms of programming with DataFrame
s should be useful
for coding your answers.
DataFrame
¶DataFrame
s are specifically designed to handle data organised in a tabular format.
Hence, as we would expect, since CSV
is the standard format for tabular data, it is very easy to
create a DataFrame
by loading data from a CSV
file.
pandas.read_csv(source)
---
The read_csv function can accept a filename or URL as an argument.Download the data file worldcities.csv
from the
module's data repository, and put
the file in the same directory as this Jupyter notebook file.
Then, by running the following cell, we can set the
global variable WC_DF
to a DataFrame containing the information from worldcities.csv
.
## WC_DF Initialisation
import pandas ## This is the module for creating and manipulating DataFrames
WC_DF = pandas.read_csv("worldcities.csv")
Be sure to keep the same variable name WC_DF
for this global variable, otherwise most of the
following code will not work and you will break the autograder.
Pandas provides the following useful methods that enable you to quickly check the contents of a DataFrame
:
df.head()
---
For a DataFrame object, df
, this method extracts the first 5 rows of data, so you can easily check what the data looks like.
df.describe()
--- for a DataFrame, df
, this method provides a table giving and overview of some basic statistical properties of the DataFrame.
Note that the head()
and describe()
methods are actually operations that
return a new DataFrame object. If this value is returned by the last line of a cell
it will be displayed as a table, but if it is generated elsewhere in the code
you will not see any output unless you use the display
function from the
IPython.display
module.
Each column of a DataFrame
is a list-like object called a
Series
. Elements, and slices of a Series
can then be accessed in similar
fashion to a list. The following illustrates how get the Series
containing
the first 5 elements of the city
column of WC_DF
:
top_5_cities = WC_DF["city"][:5] ## selects the first 5 items of the "city" column.
top_5_cities
In the above output, the left hand column of the displayed value of top_5_cities
shows the index label of each element. One of the differences between a Series
and an ordinary list is that, whereas a list always has integers for its index labels, a Series
can have different kinds of values for these. For instance (though there
is no reason to do this for the current assignment) we could set the index values to alphabetic letters, as follows:
top_5_cities.index = list("abcde")
top_5_cities
top_5_cities.index
You can also use .values
to return an array
of the column values without
the index:
WC_DF["city"][:5].values
An array
is also a list-like datastructure. It does not have an index. The main difference between a list and an array
is that the list is optimised for
storing large amounts of information and for efficiently applying numerical and
other operations to all elements of the array. Hence, array
s are usually preferred
to lists when handling large amounts of information, or when storing numerical
vectors.
You can also easily find the column names of the DataFrame using .columns
, for example:
WC_DF.columns
Note: The Index
returned here is yet another type of list-like, object. It is similar to an array,
except that it is used for indexing a Series
or DataFrame
. You do not usually
need to create or deal with Index
objects directly, since this is done automatically when you create and minipulate DataFrame
s. So you will normally only see one, when
you want to look at the columns or rows of a DataFrame
. But what you should be
aware of, when dealing with DataFrames
, is that the word index can refer to
several different types of thing.
In many cases you can treat Series
, array
and Index
objects like lists and if you want to change them to an ordinary list you can just use the list
operator,
as in the following:
list(WC_DF.columns)
We can refer to rows of a DataFrame
either by the expression DF.loc[label]
, where label
is the index label of the row we want, or by DF.iloc[n]
,
where n
is an int
giving the position of the row in the DataFrame
.
In the case of WC_DF
, the labels are integers, so we would get the same result using either. You could test this. You could also see the difference if you try finding a row of top_5_cities
DataFrame
defined above, after its index labels have been replaced by letters. In this case you could access rows either using letters, using loc
, or by int
s, using iloc
.
WC_DF.iloc[1]
A convenient way of going through the rows of a DataFrame
to perform some operation i by using the iterrows
method in a for
loop. This enables you to get both the index label and the row itself, for each successive row of the DataFrame
. The following code is a simple example:
for i, row in WC_DF.iterrows():
print(i, row['city_ascii'], row['lat'], row['lng'])
if i >3: break
It is easy, and often very useful, to sort the DataFrame by column values using .sort_values
, for example:
WC_DF.sort_values(by=["country"], ascending=True)[:10] # Sorts countries by alphabet
there are two columns that hold the city name. The first column name is 'city'
and
the second is city_ascii
. There are various different ways in which textual
information can be encoded into bytes. These days Unicode characters
encoded using UTF-8 are pretty standard.
But the older ASCII code, which uses
a single byte per character is still commonly used. Unicode provides a huge
variaty of text characters and other symbols, whereas ASCII is quite
limited (mainly to characters and symbols found in standard English).
But ASCII and is simpler and in
some ways easier to deal with than UTF-8. In the following questions you
will be asked to use the ASCII version of the city name (from the city_ascii
column). This mainly just to make you aware that there are different encodings
of text strings, but it will also prevent cerain problems that could occur in the
Autograder, if different people used different encodings.
By filtering we mean keeping some parts that we want and throwing away others.
Typically, we look for rows that match some condition; and the filter condition
is often some constraint involving the values for that row in one or more
columns.
pandas
DataFrame
s can be filtered according values of a column by using a boolean expression, for example:
filtered_DF = WC_DF[ WC_DF['capital'] == 'admin'] # This keeps only administrative capitals
filtered_DF.head()
This way of filtering is a very powerful and useful aspect of DataFrames
.
However, the
syntax of the filter operation is rather unusual and a bit difficult to understand.
What is happening can be explained by these steps in the way a filter expression is evaluated:
DF['label']
(where DF
is any DataFrame
), gives a Series
corresponding
to the 'label'
column of `DF.
a_series == val
is a special use of ==
. When a Boolean operator
(such ==
, <
etc.) is applied to a Series
object
the result is actually a Series
of Boolean values (not a single Boolean).
The new Series
obtained will have the value True
for each element where
the original Series
satisfies element == val
, and False
for the rest.
DF[ bool_series ]
, is a special kind of slice-like operation, where a boolean
series is given as a selection argument to the DataFrame
. It will return
a new DF, containing all the rows of DF
for which bool_series
has the
value True
. (These rows can be quickly found because the DataFrame
and
the Boolean series both have the same Index
.)
You do not necessarily need to follow all of that precise desciption of filtering but it will be extremely helpful if you are able to construct filtering operations similar to the above example. You will see another example below, in relation to the earthquake data you will be processing.
This question requires you to write functions to carry out the following specific tasks.
Question 1a --- Find all city names that include non-ASCII characters. [1 Mark]
Question 1b --- Find city names that occur multiple times in the dataset. [2 Marks]
Question 1c --- Create a dictionary of the number of cities in each country that are included in the dataset. [1 Mark]
Question 1d --- Create a DataFrame
of the largest cities (by population) in the world. [2 Marks]
Quesiton 1e --- Find all cities in a given country whose population is above a given number. [2 Marks]
Question 1f --- Find the total population of people living in cities in a given country. [2 Marks]
Full details for each task are explained below.
As we have seen, the names of some cities include accents or symbols that are not represented in the standard ASCII character set. Write a function non_ascii_cities
, which returns a Set
containing all the cities that occur in the world_cities.csv
dataset whose name cannot be properly represented using only ASCII characters.
Your function should not have any arguments. You should assume that the global variable WC_DF has already been initialised by running the first code cell in this notebook (see above).
# Question 1a answer cell
def non_ascii_cities():
pass
# Modify to return a set of all non-ascii city names in the world_cities data
One issue that you will discover if you investigate the worldcities.csv
data is that there are many different cities that have the same name. You may find it interesting to discover which are the most common city names. But for this question you need to write a function num_cities_occurring_n_times(n)
, such that:
n
is an integer;n
times in the worldcities
dataset. city
column. In other words the name in the form that may contain non-ASCII characters.# Question 1b answer cell
def num_cities_occurring_n_times(n):
pass
# Modify to return a value according to the specification given above
Write a function that returns a dictionary (a dict
object), whose keys are all the country name strings that occur in the worldcities
data and whose values are int
s giving the number of cities of that country that are included in the dataset.
def country_num_cities_dict():
pass
Write a function largest_cities_dataframe
that takes an int
argument n
and uses the
pandas DataFrame WC_DF
to return a new DataFrame
containing n
rows corresponding to
the n
largest cities in terms of population size, in order of decreasing population size.
You should return a dataframe such that it has the same columns as the WC_DF
and each
row has the same values as a corresponding row of WC_DF
. It does not matter if the row
indexes are the same. (They may or may not be the same depending on the specific way
that you create the new DataFrame.)
# Question 1d answer cell
def largest_cities_dataframe(n):
pass
# Modify to return a list of the n cities with the largest population
NOTE: In answering 1d you may assume that no two cities have exactly the same population, which is almost but not quite certain, when dealing with large numbers like this. But, of course, when dealing with quantites where multiple data records could have the same value, we need to be careful, because this may not be the case. For example, if we are interested in what equipement students own, we might think it would be informative to find 'the top 10 students owning the most laptops'. In this case there could be: 1 student with 3 laptops, 23 students with 2 laptops, 160 with 1 laptop and 3 who do not own a laptop. In such a case it is not meaningful to pick the 'top 10' in terms of laptop ownership. A similar problem could potentiall occur with the earthquake data that we will look at later, because the earthquake magnitudes are only recorded to 1 decimal place.
Define a function big_cities_in_country( country, population)
that takes as arguments a string corresponding to the name of a country and an integer, which will referes to a population number.
The function should return a list
of the form
where each pair
("cityN", popN)
is a tuple
consisting of the name, in ASCII form, of a city in the given country
, followed by an int
, which is the population of that city (according to the worldcities data). The list should include all and only those cities in the country whose population is greater than or equal to the given popuplation
argument. The list should be ordered so that the ("cityN", popN)
items occur in increasing order of the population size popN
.
# Complete question 1d in this cell
def big_cities_in_country(country, population): # country is a string argument
pass # Edit this function to return a list, as specified above
Create a function that given a country name, returns an int
which is the total population of
people liveing in all the cities of that country, as given in WC_DF
.
Hints:
WC_DF
. You will need to be able to deal with NaN
values. A distinctive feature of NaN
values is that they have been defined so that x != x
has the value True
if x
has a NaN
value. ## Question 1e Answer Code Cell
def country_total_cities_population(country):
pass
In this coursework exercise, you will learn how to download live information from the web and procress it using the Pandas data analysis package for Python.
The data we will use as an example is from the United States Geological Survey (USGS), which provides a wide range of geographic and geological information and data. We shall be using their data relating to seismological events (i.e. Earthquakes) from around the world, which is published in the form of continually updated CSV files. Information about these feeds can be found here. The URL for the particular feed we shall be using is given below.
Questions Overview
Read earthquake data from the USGS live feed CSV all_day.csv
into a Pandas DataFrame.
The data can be obtained directly from http://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.csv and read into a Pandas DataFrame.
Note: For this question you do not need to download and save the file all_day.csv
. It should
be loaded directly from the web feed. However, while testing, if you have no internet connection or
a bad connection you could download a copy of the file. But remember to put it back to downloading
the current one before you submit. Note also that all_day.csv
is a live file, which lists
quakes recorded during the past 24 hours, and is updated every minute, so of course,
you will not always get the same file or the same results. More information about this and other
earthquake feeds provided by USGS can be found here.
# Q2a answer code cell
import pandas ## This is the module for creating and manupulating DataFrames
# Here we have assigned the url of the quake datasource to the global variable
# 'QUAKE_SOURCE' for your convenience.
QUAKE_SOURCE = ( "http://earthquake.usgs.gov/" +
"earthquakes/feed/v1.0/summary/all_day.csv" )
QUAKE_DF = None ## Modify this line to import the data using Pandas
QUAKE_DF
¶## If QUAKE_DF is a DataFrame, show the first 5 rows
try:
if type(QUAKE_DF) == pandas.DataFrame:
display(QUAKE_DF.head())
else:
print("QUAKE_DF is not a DataFrame")
except:
print("QUAKE_DF has not been assigned a value")
The columns containing latitude and longitude values are labelled differently in the worldcities.csv
and the earthquake data from USGS. This is a minor but very typical form of incompatibility between data formats that you will often need to deal with when working with real data.
pandas
functions¶Here we show you some more pandas functions that you may find useful in this exercise.
As we have seen, versatile filtering and sorting capabilities are provided by pandas. To get more understanding of these, you should look at tutorials of using Pandas DataFrames. But the following example illustrates how you can find and display quakes whose depth is greater than or equal to a given threshold:
def show_deep_quakes( depth ):
# make deep_quakes DataFrame by selecting rows from QUAKE_DF
deep_quakes = QUAKE_DF[ QUAKE_DF["depth"] >= depth ] ## This is how you select rows by a condition
## on one of the column values.
print("Number of quakes of depth {} or deeper:".format(depth),
len(deep_quakes.index)) ## This finds the number of rows of the deep_quakes DataFrame
display(deep_quakes.sort_values("depth", ascending=False)) ## Sort by descending depth value
Note:
The QUAKES_DF
global variable needs to be set before these examples will work, so I am using a try
, except
construct to avoid getting an error.
try:
show_deep_quakes(100)
except:
print("Probably QUAKE_DF not correctly set")
You can also find max
and min
values in a column. Eg:
try:
QUAKE_DF["depth"].max()
except:
print("Probably QUAKE_DF not correctly set")
try:
QUAKE_DF["depth"].min()
except:
print("Probably QUAKE_DF not correctly set")
Write a function powerful_quakes
that takes a numerical argument and returns a DataFrame
including
all the quakes in QUAKE_DF
that have a magnitude greater than or equal to the given argument.
# Complete question 2b answer cell
def powerful_quakes(mag):
## This is just returning an empty DataFrame you need to code it to return
## a DataFrame with all quakes of magnitude greater than or equal to mag
return pandas.DataFrame()
n+
most powerful earthquakes¶Produce a DataFrame
with rows represent the n
(or maybe more) most powerful quakes
in descending order of magnitude. The returned DataFrame
should show at least n
quakes and may sometimes show more since we do not want to leave out any quake that is equally
powerful as the last quake listed in the DataFrame
,
More specificially, we want the function to return a DataFrame
that:
QUAKES_DF
,QUAKES_DF
(but it does not matter whether the row indices in the returned DataFrame
are the same or different from corresponding rows in QUAKES_DF
),DataFrame
are ordered in descending order of their magnitude column value (rows of equal magnitude can appear in any order),QUAKES_DF
, such that there are fewer than n
other rows in
QUAKES_DF
that have a higher magnitude.The above definition of the requirements is clear and precise. Though you may ask for help and advice regarding implementation, you will not be given help with understanding the specification.
# Question 2c answer cell
def most_powerful_n_quakes(n):
pass
# Edit this function to make it return a DataFrame of
# the 'top n' quakes of the all_day.csv file
Clearly, when dealing with data pertaining to locations in space, the distince between such locations is often of great significance when interpreting or extracting further information from the data.
To help answer the following questions you are provided with the function haversine_distance
,
which implements the Haversine formula to find the surface distance in kilometres between two locations, that are specified in terms of
latitude and longitude values. When finding distances betwen points on the surface of the
Earth We need to use this formula, rather than the simpler Pythagorean distance formula,
because the Earth's surface is a sphere.
## Function to compute distance between locations (kilometres)
# Returns the surface distance in meters, according to the Haversine formula,
# between two locations given as (latitude, longitude) coordinate pairs.
import math
def haversine_distance( loc1 , loc2 ):
'''finds the distance (m) between 2 locations, where locations are defined by
longitudes and latitudes'''
lat1, lon1 = loc1
lat2, lon2 = loc2
radius = 6371 # kilometers
dlat = math.radians(lat2 - lat1)
dlon = math.radians(lon2 - lon1)
a = (math.sin(dlat / 2) * math.sin(dlat / 2) +
math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) *
math.sin(dlon / 2) * math.sin(dlon / 2))
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
d = radius * c
return d
Write a function quake_distance_from_loc_dataframe(loc)
satisfying the following requirements:
It takes a location argument (latitude,longitude)
consisting of a pair of float
s
(Note: this is a single argument but consists of a pair of values represented as Python tuple
.)
It returns a DataFrame
object derived from QUAKE_DF
but with one extra column distance_from_loc
, giving the distance of each quake from the given location.
The rows of the returned DataFrame
should be the same as those in QUAKE_DF
except for the aditional distance_from_loc
column. (However, it is not necessary to preserve the index of the DataFrame
, this will be ignored when your solution is tested.)
The rows of the returned DataFrame
should be sorted in order of increasing values of distance_from_loc
.
The original DataFrame QUAKE_DF
should not be altered by the execution of
quake_distance_from_loc_dataframe(loc)
.
You will need to do some research to find out how to create a new column and set its values.
## 2d Answer Code Cell
def quake_distance_from_loc_dataframe(loc):
## Replace with code so that the function returns a DataFrame in accord with the
## specification given above
pass
The idea of this question is to identify possible emergency situations by finding cities that are likely to suffer from the effects of an earthquake.
The effect of an earthquake on a city or person will depend on their distance from the source of the quake. The effect of an earthquake will depend on many factors and even the dependence on distance to source is very complex. However, after a bit of background research, Brandon has come up with a simple formula which hopefully at least gives a very crude estimate of relative effect of a quake with a particular magnitude and depth on a surface location at a known surface distance from the quake's epicenter. The calculated effective magnitude of an earthquake will be less that the source magnitude, for instance a magnitude 9 quake at a depth of 100km (which is likely to be extremely destructive, would have an effective magnitude of 5 at its epicentre (directly above the source) and 3.585 at a point on the earth surface 500km away from the epicenter.
def effective_magnitude( magnitude, depth, surface_distance ):
energy = 10**magnitude # convert logarithmic magnitude to a linear energy value
if depth < 1: # Crude fix for small or negative depths (can occur where land is above sea level)
depth = 1
## Calculate distance to source by Pythagorus (ignoring curvature of surface)
dist_to_source_squared = depth**2 + surface_distance**2
## Apply inverse square distance multiplier to get energy density at distance from source
## (Ignores damping effects)
attenuated_energy = energy/dist_to_source_squared
attenuated_magnitude = math.log10(attenuated_energy) ## Convert back to a log base 10 scale
return attenuated_magnitude
# Some test cases.
#effective_magnitude(9,100,500)
#effective_magnitude(6,50, 100)
The epicenter of an earthquake is the point on the earth that is directly above its source. Thus the effective magnitude at the epicenter is just the effective magnitude of the quake at surface distance zero:
def epicenter_magnitude( magnitude, depth ):
return effective_magnitude( magnitude, depth, 0)
Note: For any given quake, its effective_magnitude
at any point on Earth is always less than or equal to its epicenter_magnitude
.
endangered_cities
function¶Now we get to the specification of the function.
Write a function engangered_cities( minimum_population, minimum_effective_magnitude)
that takes
two numerical arguments: an int
(minimum_population
) and a float
(minimum_effective_magnitude
)
and returns a list
specifying all those cities listed in
the WC_DF
such that:
the city has a population greater than or equal to the given minimum_population
;
for at least one of the quakes recorded in QUAKE_DF
, the effective_magnitude
(as determined by the
function defined above) of that quake
at the location of the city (as given in worldcities.csv
) is equal to or greater than the minimum_effective_magnitude
.
each city in the list should be represented by a tuple (city_ascii, country, (lat, lng))
, giving
the city name in ASCII form, the country and the location (as a latitude, longitude pair).
the returned list should be ordered aphabetically, primarily in terms of country
and secondarily
cities of the same country should be ordered in terms of city_ascii
.
This ordering is illustrated by the sample output given below.
A final condition is that the function should run in a reasonable time frame such that it can be expected to give correct and complete results after no more that 5 minutes excecution time. To ensure that this is feasible, the example cases you will be tested on, will be ones for which Brandon's solution ran in less than 2 minues on the Gradescope autograder platform. (All submissions will be tested on the same saved copy of all_day.csv
previously downloaded from the USGS feed.)
in [165] %time endangered_cities(200000, 0.5) out[165] CPU times: user 1min 58s, sys: 768 µs, total: 1min 58s Wall time: 1min 58s [('Baghlan', 'Afghanistan', (36.1393, 68.6993)), ('Kunduz', 'Afghanistan', (36.728, 68.8725)), ('Mazar-e Sharif', 'Afghanistan', (36.7, 67.1)), ('Ambon', 'Indonesia', (-3.7167, 128.2)), ('Denov', 'Uzbekistan', (38.2772, 67.8872))]
Your code should make use of the functions defined above: haversine_distance
, effective_magnitude
and epicenter_magnitude
. You are advised not to change these otherwise you may get different results from what the Autograder is expecting.
The calculation of this function is computationally intensive. For prelimiary testing you might use smaller data sets by, say only using cities in one country.
There are many optimisations that can be done to reduce the computational cost of finding the endangered cities. For example, calculating epicenter_magnitude
enables weaker earthquakes to be ignifed withought the need to determine their distance from every city on earth. It may also be useful to not that when calcuating the effective magnitude of a given quake (with a specific depth) at different locations, the value always decreases as surface distance from the quake increases.
Remember that the autograder will run your actual code, so will probably take a while to grade this function. However, it only runs the function once and then performs 3 different tests on the value that is returned.
The autograder will run on a presaved set of quakes (so the test is the same for all submissions). However, your code should run on any version of all_day.csv
that you download from the USGS live feed.
## 2e Answer Code Cell
def endangered_cities(min_population, min_effective_magnitude):
## Replace with code that fulfils the specification
pass
Having got this far, you may find it interesting and informative to do some more processing of the city and earthquake information. Since the previous exercises were designed so they can be quickly and reliably assessed by the autograding software, they involve coding particular functions with very specific requirements. But the following exercises are more open ended and give suggestions for interactive and visual use of the city and earthquake data.
To ensure that your assignment submission works with the autograding software when submitted, it is recommended that you now save this file and make a new copy with a different name, such as Earthquakes_optional.ipynb
. Then use the new file to continue with the optional exercises.
DataFrame
¶A government or other organisation may want to monitor a certain list of cities with regard to whether
they may be at risk of earthquake damage. To answer this question you should create a function
that uses the endangered_cities
function you have defined above to create such a DataFrame
.
Your function city_risk_alert
should return a pandas DataFrame that includes the status of 'ENDANGERED'
or 'SAFE'
for a certain city. The dataframe should also contain the city name, country and status for each city input. You could also extend this to add more columns showing
things like the distance and magnitude of the nearest earthquake. And you could perhaps make it
so any endangered cities were put at the top of the list.
For example:
display( city_risk_alert( ['Rome', 'Milan', 'Pisa'] )
might give the following output:
city | country | status |
---|---|---|
Pisa | Italy | ENDANGERED |
Rome | Italy | SAFE |
Milan | Italy | SAFE |
The code below creates a Map object using the ipyleaflet
module and uses this to
display powerful quakes on the map. If you have coded the powerful_quakes
function for
Question 2b above, the code in the cell below the map should draw the detected powerful
quakes onto the map at their correct locations.
To install the ipyleaflet
module use pip3 install ipyleaflet
. If the map does not display after installation be sure to restart the kernel, and close and reopen this file. We provide the draw_circle_on_map
function, this add circles to a specified location on the map, where the location is defined by longitudes and latitudes.
from ipyleaflet import Map, basemaps, basemap_to_tiles, Circle, Polyline
from ipywidgets import Layout
LEEDS_LOC = ( 53.8008, -1.5491 ) # Here we define the longitude and latitude of Leeds
WORLD_MAP = Map(basemap=basemaps.OpenTopoMap, center=LEEDS_LOC, zoom=1.5,
layout=Layout(height="500px")) # Here we create a map object centred on Leeds
WORLD_MAP
def draw_circle_on_map( a_map, location, radius = 1000, color="red", fill_color=None ):
if not fill_color:
fill_color = color
circle = Circle()
circle.location = location
circle.radius = radius
circle.color = color
circle.fill_color = fill_color
a_map.add_layer(circle)
# This will edit your previous map rather than produce a new one
draw_circle_on_map(WORLD_MAP, LEEDS_LOC, color="green" )
def display_powerful_quakes_on_map(mag):
powerful = powerful_quakes(3)
for i, quake in powerful.iterrows():
draw_circle_on_map( WORLD_MAP,
(quake["latitude"],quake["longitude"]),
radius= 20000*int(quake["mag"]) )
display_powerful_quakes_on_map(3)
It would be nice to also see the endangered cities on the map. For an ambitious exercise you could see if you can draw lines on the map running from from the locations of powerful earthquakes to the cities that are endangered by them.