{"id":8,"date":"2025-08-07T21:37:59","date_gmt":"2025-08-07T11:37:59","guid":{"rendered":"https:\/\/maxcoding.imaxim.org\/?p=8"},"modified":"2025-08-07T21:58:06","modified_gmt":"2025-08-07T11:58:06","slug":"create-a-weather-map-using-openweather-api-in-python","status":"publish","type":"post","link":"https:\/\/maxcoding.imaxim.org\/?p=8","title":{"rendered":"Create a weather map using OpenWeather API in\u00a0Python"},"content":{"rendered":"\n<p><a href=\"https:\/\/openweathermap.org\/current\" target=\"_blank\" rel=\"noreferrer noopener\">Current weather data<\/a> provided by Open Weather API allows you to get weather information for free for any location on our planet. All you need is to register and get an API key to make requests.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"645\" src=\"https:\/\/maxcoding.imaxim.org\/wp-content\/uploads\/2025\/08\/image-1024x645.png\" alt=\"\" class=\"wp-image-9\" srcset=\"https:\/\/maxcoding.imaxim.org\/wp-content\/uploads\/2025\/08\/image-1024x645.png 1024w, https:\/\/maxcoding.imaxim.org\/wp-content\/uploads\/2025\/08\/image-300x189.png 300w, https:\/\/maxcoding.imaxim.org\/wp-content\/uploads\/2025\/08\/image-768x484.png 768w, https:\/\/maxcoding.imaxim.org\/wp-content\/uploads\/2025\/08\/image.png 1260w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>In this post, I would like to show you how to generate a weather map for a list of cities showing the city name, temperature and weather type icon.<\/p>\n\n\n\n<p>Following libraries will be used to load and process data:<\/p>\n\n\n\n<p>For example, let\u2019s use the following locations:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: python; title: ; notranslate\" title=\"\">\nimport requests\nimport pandas as pd\nfrom geopy.geocoders import Nominatim\nfrom geopy.extra.rate_limiter import RateLimiter\n<\/pre><\/div>\n\n\n<p>For example, let\u2019s use the following locations:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\ncities = &#x5B;\n    &#x5B;&quot;Kyiv&quot;, &quot;Ukraine&quot;],\n    &#x5B;&quot;Warsaw&quot;, &quot;Poland&quot;],\n    &#x5B;&quot;Berlin&quot;, &quot;Germany&quot;],\n    &#x5B;&quot;London&quot;, &quot;UK&quot;],\n    &#x5B;&quot;Madrid&quot;, &quot;Spain&quot;],\n    &#x5B;&quot;Paris&quot;, &quot;France&quot;],\n    &#x5B;&quot;Rome&quot;, &quot;Italy&quot;],\n    &#x5B;&quot;Prague&quot;, &quot;Czechia&quot;],\n    &#x5B;&quot;Istanbul&quot;, &quot;Turkey&quot;],\n    &#x5B;&quot;Stockholm&quot;, &quot;Sweden&quot;],\n    &#x5B;&quot;Sofia&quot;, &quot;Bulgaria&quot;],\n    &#x5B;&quot;Bucharest&quot;, &quot;Romania&quot;],\n    &#x5B;&quot;Zurich&quot;, &quot;Switzerland&quot;],\n]\ndf = pd.DataFrame(cities, columns=&#x5B;&quot;city&quot;, &quot;country&quot;])\n\n<\/pre><\/div>\n\n\n<p><\/p>\n\n\n\n<p>Since the OpenWeather API endpoint requires geographical coordinates such as latitude and longitude, we need to perform geocoding for each city. I would like to use the <a href=\"https:\/\/nominatim.org\/\" target=\"_blank\" rel=\"noreferrer noopener\">Nominatim<\/a> geocoder provided by OpenStreetMap.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>locator = Nominatim(user_agent=\"myGeocoder\")\ngeocode = RateLimiter(locator.geocode, min_delay_seconds=.1)\n\ndef get_coordinates(city, country):\n  response = geocode(query={\"city\": city, \"country\": country})\n  return {\n    \"latitude\": response.latitude,\n    \"longitude\": response.longitude\n  }\n\ndf_coordinates = df.apply(lambda x: get_coordinates(x.city, x.country), axis=1)\ndf = pd.concat(&#91;df, pd.json_normalize(df_coordinates)], axis=1)<\/code><\/pre>\n\n\n\n<p>After running this code. Additional columns with coordinates will be added to the pandas DataFrame:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"852\" height=\"978\" src=\"https:\/\/maxcoding.imaxim.org\/wp-content\/uploads\/2025\/08\/image-1.png\" alt=\"\" class=\"wp-image-10\" srcset=\"https:\/\/maxcoding.imaxim.org\/wp-content\/uploads\/2025\/08\/image-1.png 852w, https:\/\/maxcoding.imaxim.org\/wp-content\/uploads\/2025\/08\/image-1-261x300.png 261w, https:\/\/maxcoding.imaxim.org\/wp-content\/uploads\/2025\/08\/image-1-768x882.png 768w\" sizes=\"auto, (max-width: 852px) 100vw, 852px\" \/><\/figure>\n\n\n\n<p>We should store the open Weather API key in <strong>openweathermap_api_key<\/strong> variable. It will prompt you to enter the API key while running the code.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>from getpass import getpass\nopenweathermap_api_key = getpass('Enter Openweathermap API key: ')<\/code><\/pre>\n\n\n\n<p>Now we can get weather information for each location<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import datetime\n\ndef get_weather(row):\n\n  owm_url = f\"https:\/\/api.openweathermap.org\/data\/2.5\/weather?lat={row.latitude}&amp;lon={row.longitude}&amp;appid={openweathermap_api_key}\"\n  owm_response = requests.get(owm_url)\n  owm_response_json = owm_response.json()\n  sunset_utc = datetime.datetime.fromtimestamp(owm_response_json&#91;\"sys\"]&#91;\"sunset\"])\n  return {\n      \"temp\": owm_response_json&#91;\"main\"]&#91;\"temp\"] - 273.15,\n      \"description\": owm_response_json&#91;\"weather\"]&#91;0]&#91;\"description\"],\n      \"icon\": owm_response_json&#91;\"weather\"]&#91;0]&#91;\"icon\"],\n      \"sunset_utc\": sunset_utc,\n      \"sunset_local\": sunset_utc + datetime.timedelta(seconds=owm_response_json&#91;\"timezone\"])\n  }\n\ndf_weather = df.apply(lambda x: get_weather(x), axis=1)\ndf = pd.concat(&#91;df, pd.json_normalize(df_weather)], axis=1)<\/code><\/pre>\n\n\n\n<p>As you can see in the code, we do the following processing after weather information loads:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>I convert temperature from Kelvin into centigrade<\/li>\n\n\n\n<li>I converted sunset time from Unix Epoch into UTC datetime and the local time zone<\/li>\n<\/ul>\n\n\n\n<p>The response also contains other weather information, so you can API response.<\/p>\n\n\n\n<p>The result for today, 28 June 2023, looks like this:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"558\" src=\"https:\/\/maxcoding.imaxim.org\/wp-content\/uploads\/2025\/08\/image-2-1024x558.png\" alt=\"\" class=\"wp-image-11\" srcset=\"https:\/\/maxcoding.imaxim.org\/wp-content\/uploads\/2025\/08\/image-2-1024x558.png 1024w, https:\/\/maxcoding.imaxim.org\/wp-content\/uploads\/2025\/08\/image-2-300x164.png 300w, https:\/\/maxcoding.imaxim.org\/wp-content\/uploads\/2025\/08\/image-2-768x419.png 768w, https:\/\/maxcoding.imaxim.org\/wp-content\/uploads\/2025\/08\/image-2-1536x837.png 1536w, https:\/\/maxcoding.imaxim.org\/wp-content\/uploads\/2025\/08\/image-2.png 1600w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>To visualize results, Geopandas with contextily will be used to render the map with a base layer. I am running my example using Google Colab, and you can install these additional libraries by running:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>try:\n  import geopandas as gpd\nexcept ModuleNotFoundError:\n  if 'google.colab' in str(get_ipython()):\n    %pip install geopandas\n  import geopandas as gpd\n\ntry:\n  import contextily as ctx\nexcept ModuleNotFoundError:\n  if 'google.colab' in str(get_ipython()):\n    %pip install contextily\n  import contextily as ctx<\/code><\/pre>\n\n\n\n<p>Convert pandas DataFrame into GeoDataFrame and create a geometry column with coordinates. Note that CRS is set to 4326 which sets the coordinate system to WGS84\u200a\u2014\u200aWorld Geodetic System 1984. Which is latitude and longitude in degrees.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>gdf = gpd.GeoDataFrame(df, geometry=gpd.points_from_xy(df.longitude, df.latitude), crs=4326)<\/code><\/pre>\n\n\n\n<p>Now the map can be rendered using Geopandas and Matplotlib.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>from skimage import io\nimport matplotlib.pyplot as plt\nfrom matplotlib.offsetbox import AnnotationBbox, OffsetImage  \n\n# plot city location marker\nax = gdf.to_crs(epsg=3857).plot(figsize=(12,8), color=\"black\")\n\n# add weather icon\ndef add_icon(row):\n  img = io.imread(f\"https:\/\/openweathermap.org\/img\/wn\/{row.icon}@2x.png\")\n  img_offset = OffsetImage(img, zoom=.4, alpha=1, )\n  ab = AnnotationBbox(img_offset, &#91;row.geometry.x+150000, row.geometry.y-110000], frameon=False)\n  ax.add_artist(ab)\n\ngdf.to_crs(epsg=3857).apply(add_icon, axis=1)\n\n# add city name label\ngdf.to_crs(epsg=3857).apply(lambda x: ax.annotate(text=f\"{x.city}  \", fontsize=10, color=\"black\", xy=x.geometry.centroid.coords&#91;0], ha='right'), axis=1);\n\n# add temperature\ngdf.to_crs(epsg=3857).apply(lambda x: ax.annotate(text=f\" {round(x.temp)}\u00b0\", fontsize=15, color=\"black\", xy=x.geometry.centroid.coords&#91;0], ha='left'), axis=1);\n\n# add margins\nxmin, ymin, xmax, ymax = gdf.to_crs(epsg=3857).total_bounds\nmargin_y = .2\nmargin_x = .2\ny_margin = (ymax - ymin) * margin_y\nx_margin = (xmax - xmin) * margin_x\n\nax.set_xlim(xmin - x_margin, xmax + x_margin)\nax.set_ylim(ymin - y_margin, ymax + y_margin)\n\n# add basemap\nctx.add_basemap(ax, source=ctx.providers.Stamen.Watercolor)\n\nax.set_axis_off()<\/code><\/pre>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/cdn-images-1.medium.com\/max\/1600\/1*-BWYiZolzPM3bLKOdfkQHA.png\" alt=\"\"\/><figcaption class=\"wp-element-caption\">This weather map for 28 June 2023 was rendered using OpenWeather API and geopandas<\/figcaption><\/figure>\n\n\n\n<p>To ensure that labels do not go outside map boundaries, I have added horizontal and vertical margins. There is also a code that loads and displays the weather map icon in PNG format.<\/p>\n\n\n\n<p>Feel free to modify my example by <a href=\"https:\/\/colab.research.google.com\/drive\/1P2n4LdriXp6Imm9g5fE7Rvm5hc0ySXif?usp=sharing\" rel=\"noreferrer noopener\" target=\"_blank\">running the complete code example in the Google Colab notebook<\/a>, please note that you will need to provide your own Open Weather API key.<\/p>\n\n\n\n<p>OpenWeather API paid access allows you to get additional details and historical data. Please check <a href=\"https:\/\/openweathermap.org\/api\" rel=\"noreferrer noopener\" target=\"_blank\">OpenWeather API documentation<\/a>.<\/p>\n\n\n\n<p>I hope you found this example useful.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Current weather data provided by Open Weather API allows you to get weather information for free for any location on our planet. All you need is to register and get an API key to make requests. In this post, I would like to show you how to generate a weather map for a list of [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[7,5,4],"class_list":["post-8","post","type-post","status-publish","format-standard","hentry","category-python","tag-geopandas","tag-openweather","tag-python"],"_links":{"self":[{"href":"https:\/\/maxcoding.imaxim.org\/index.php?rest_route=\/wp\/v2\/posts\/8","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/maxcoding.imaxim.org\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/maxcoding.imaxim.org\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/maxcoding.imaxim.org\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/maxcoding.imaxim.org\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=8"}],"version-history":[{"count":4,"href":"https:\/\/maxcoding.imaxim.org\/index.php?rest_route=\/wp\/v2\/posts\/8\/revisions"}],"predecessor-version":[{"id":19,"href":"https:\/\/maxcoding.imaxim.org\/index.php?rest_route=\/wp\/v2\/posts\/8\/revisions\/19"}],"wp:attachment":[{"href":"https:\/\/maxcoding.imaxim.org\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=8"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/maxcoding.imaxim.org\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=8"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/maxcoding.imaxim.org\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=8"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}