I am writing a python GUI application that plots weather data on a map for a specific region. It plots temperature, dewpoint, wind data, and icons that represent the sky conditions. It extracts the data for longitude/latitude from text files. I timed out how long it takes for the program to plot each type of weather data and I got the following results:
temperature: 10 seconds
dewpoint: 10 seconds
wind: 15 seconds
sky conditions: 90 seconds
So, it doesn't take too long for it to plot temperature, dewpoint, and wind. However, it takes a very long time for it to plot the sky conditions, which are icon images. My goal is for my application to display any kind of weather data in a few seconds. I am wondering what I can do to my python code to make it more efficient and run faster, so it meets that goal.
I use global variables, which I know is not good programming practice. This is likely one thing I am doing in my code that is slowing things down. I also use numerous function calls, which could also be slowing things down as well. I would like to rewrite my code, so that I don't use so many global variables and function calls.
The biggest problem I think is when it displays the icons, which are images as stated above. I am new at working with images in python, so I don't know if python is just slow with dealing with images or not. Any analysis of my code/suggestions to make it better would be greatly appreciated.
import tkinter as tk
from tkinter import ttk
import tkinter.font as tkFont
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
import matplotlib.image as mpimg
from mpl_toolkits.basemap import Basemap
from PIL import Image
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
import matplotlib.pyplot as plt
import numpy as np
import requests
def retrieve_filedata():
file = area.get() + '.txt'
f = open(file, 'r')
line = f.readline()
global minlon
minlon = float(line.split(' ')[0])
global minlat
minlat = float(line.split(' ')[1])
global maxlon
maxlon = float(line.split(' ')[2])
global maxlat
maxlat = float(line.split()[3])
global centerlat
centerlat = float(line.split()[4])
global centerlon
centerlon = float(line.split()[5])
lines = f.readlines()[1:] #put longitude and latitudes into array
global lat
lat = []
global lon
lon = []
for x in lines:
lat.append(float(x.split(' ')[0]))
lon.append(float(x.split()[1]))
f.close()
def current_weather():
retrieve_filedata()
key = '4a7a419e4e16e2629a4cedc37cbf7e50'
url = 'https://api.openweathermap.org/data/2.5/weather'
global observation
observation = []
global u
u = []
global v
v = []
global icon
icon = []
for i in range(0,len(lon)):
params = {'lat': lat[i], 'lon': lon[i], 'appid': key, 'units': 'imperial'}
response = requests.get(url, params = params)
weather = response.json()
airtemp = round(weather['main']['temp'])
humidity = weather['main']['humidity'] #relative humidity
kelvin = ((airtemp-32)/1.8)+273 #convert temperature from F to Kelvin
#E = 0.611 * np.exp(5423 * ((1/273) - (1/dewpoint))) vapor pressure
Es = 0.611 * np.exp(5423 * ((1/273) - (1/kelvin))) #saturation vapor pressure
kelvindew = 5423/((5423/273)-np.log(((humidity/100)*Es)/0.611)) #use vapor pressure equation to find dewpoint in Kelvin
dewpoint = round(((kelvindew-273)*1.8)+32) #convert dewpoint from Kelvin to F
windspeed = round(weather['wind']['speed'])
winddirection = weather['wind']['deg']
u.append(-(windspeed)*np.sin(((np.pi)/180)*winddirection))
v.append(-(windspeed)*np.cos(((np.pi)/180)*winddirection))
icon.append(weather['weather'][0]['icon'])
if data.get() == 'Temperature':
observation.append(airtemp)
if data.get() == 'Dewpoint':
observation.append(dewpoint)
def display_winds():
current_weather()
map = Basemap(llcrnrlon= minlon, llcrnrlat= minlat, urcrnrlon= maxlon, urcrnrlat= maxlat ,resolution='i',
lat_0 = centerlat, lon_0 = centerlon)
for i in range(0, len(lon)):
x,y = map(lon[i], lat[i])
map.barbs(x, y, u[i], v[i], length = 5, pivot = 'middle', zorder = 100)
plt.show()
def display_icons():
map = Basemap(llcrnrlon= minlon, llcrnrlat= minlat, urcrnrlon= maxlon, urcrnrlat= maxlat ,resolution='i',
lat_0 = centerlat, lon_0 = centerlon)
image = []
im = []
ab = []
x,y = map(lon, lat)
for i in range(0, len(lon)):
image.append(Image.open(requests.get('http://openweathermap.org/img/wn/' + icon[i] + '@2x.png', stream=True).raw))
im.append(OffsetImage(image[i], zoom = 0.25))
ab.append(AnnotationBbox(im[i], (x[i],y[i]), frameon=False))
map._check_ax().add_artist(ab[i])
plt.show()
def plot_data():
current_weather()
map = Basemap(llcrnrlon= minlon, llcrnrlat= minlat, urcrnrlon= maxlon, urcrnrlat= maxlat ,resolution='i',
lat_0 = centerlat, lon_0 = centerlon)
map.drawmapboundary(fill_color='#A6CAE0')
map.drawcounties(zorder = 20)
map.drawstates()
map.fillcontinents(color='#e6b800',lake_color='#A6CAE0')
for i in range(0, len(lon)):
x,y = map(lon[i], lat[i])
if data.get() == 'Temperature' or data.get() == 'Dewpoint':
plt.text(x, y, observation[i], fontsize = 7)
if data.get() == 'Wind':
display_winds()
if data.get() == 'Sky Conditions':
display_icons()
plt.show()
root = tk.Tk()
canvas = tk.Canvas(root, height = '300', width = '400')
canvas.pack()
#create widgets
titlefont = tkFont.Font(size = 20)
title = tk.Label(text = "Current Weather Map Data", font = titlefont)
arealabel = tk.Label(text = "Choose Region: ")
areavalue = tk.StringVar()
area = ttk.Combobox(root, width = '13', textvariable = areavalue)
datalabel = tk.Label(text = "Choose Data: ")
datavalue = tk.StringVar()
data = ttk.Combobox(root, width = '12', textvariable = datavalue)
button = tk.Button(root, text = "Plot Data", command = plot_data)
#organize widgets
title.place(relx = 0.1, rely = 0.05)
arealabel.place(relx = 0.05, rely = 0.25)
area.place(relx = 0.35, rely = 0.25)
datalabel.place(relx = 0.05, rely = 0.40)
data.place(relx = 0.35, rely = 0.40)
button.place(relx = 0.40, rely = 0.65)
#Insert Values into Area Combobox
area['values'] = ('Pennsylvania', 'Maryland', 'Delaware', 'New Jersey', 'Virginia', 'West Virginia', 'Ohio', 'New York',
'Rhode Island', 'Massachusetts', 'Connecticut', 'Vermont', 'New Hampshire', 'Maine', 'Mid Atlantic', 'New England')
area.current(0)
#Insert Values into Data Combobox
data['values'] = ('Temperature', 'Dewpoint', 'Wind', 'Sky Conditions')
data.current(0)
root.mainloop()
1 Answer 1
There are a couple of problems with your code we'll have to clear up before looking at performance. I'd like to write a comprehensive review about it, but I'd have to make too many assumptions. So instead it's going to be a more general one and one of the problems with that is that I can't touch the performance issue. You have not specified your Python version, how your input is structured (we know there'll be at least 8 values, but that's it), what your output looks like (at least one plot) and one of the libraries you're using is out-of-date (no longer readily available).
I'll look at your imports and your functions to see if we can make it readable. Once it's better readable either the performance issues will be much easier to profile for yourself so you can see what's going wrong, or you'll have to post a revised version in a new question for further review.
Imports
The following imports are unused and can be removed:
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
import matplotlib.image as mpimg
A more glaring problem though is that people keep using Basemap. Yes, it's incredibly powerful. It's also very old and no longer supported since 2020. This means no updates and more hassle getting it installed on newer systems.
Luckily there's Cartopy. I know full well it's not always easy to migrate a project, but with the current code size it's still doable. If you wait till the project has grown further it will be much harder.
You won't be the first to migrate so there are plenty of people who've gone before and figured most of it out (examples: [1], [2]) already.
Functions
You say you've timed temperature, dewpoint, wind and sky conditions. However, there is no indication in the code that this is timed by the software or bound to a specific function. There's a display_winds
function, but the rest is labelled wrong or inconsistently.
for i in range(0, len(lon)):
x,y = map(lon[i], lat[i])
if data.get() == 'Temperature' or data.get() == 'Dewpoint':
plt.text(x, y, observation[i], fontsize = 7)
if data.get() == 'Wind':
display_winds()
if data.get() == 'Sky Conditions':
display_icons()
Why not make a display_temperature()
and display_dewpoint
as well? Should they even give the same output like they do now?
I won't overwhelm you with new terms, but I'd like to introduce you to the Single Responsibility Principle (SRP). Every class or function should have responsibility over a single part of the program. All of your functions are doing too much, making it unclear what's happening where and making it nearly impossible to re-use some of the code except by copy-pasting it around. Which you've done, since there are plenty of places where you're doing the same thing in multiple places, either exactly the same or slightly different.
Some of your actions happen in the global scope. It's customary to wrap all this in a main
function. While Python doesn't require it unless some other languages, it does help with testing, debugging, profiling en re-use. Put everything that's not in its own function in def main()
and end your code with:
if __name__ == "__name__":
main()
This is particularly useful if you want to include functions from this file into a new project, since everything in main
won't be automatically executed unless explicitly told to.
Remainder
There are a lot of other things that can be improved. Your for
loops are ugly, you define map
twice exactly the same way in different functions (re-use it, don't do the same work twice). All your globals can be removed, how you read your file is dangerous (use with open(area.get() + ".txt") as f:
instead to have the file close automatically) and you can probably redo your file reading entirely if you show us how it's formatted. Currently you're using split
all over the place (inconsistently too, sometimes with and sometimes without argument), if you know how the file is formatted you can do that all in one go. Your code is bordering on the edge of whether it makes sense to create a new class, another thing I can't get into details about without being able to reproduce the output.
plt.show()
under the definition ofdisplay_icons()
is incorrectly indented. Ordinarily I'd say it's a copy-paste mistake, but this is a really odd place for such a mistake. Please clarify. \$\endgroup\$