数据科学有氧运动 1 - 天气
我打算毫不客气地借用Wes Bos 的 JavaScript30 课程中“编程有氧”的概念。我想,如果我提出一个简短的数据科学示例问题,然后和你们一起解决,你们可能会喜欢。我现在正在辅导一个学习 Python 的学生(我的第一个学生!太棒了!),这就是她的一个问题。这个例子似乎涵盖了很多基础知识。这个例子将使用 Python (3)。我会尽量在合适的库和文档出现时提供链接,这样你们就可以进一步探索,而不是轻信我的空话。
那么,事不宜迟,让我们开始吧!
0. 问题
我们被要求调查各种天气现象如何随纬度变化。具体来说,我们需要收集至少500个天气数据样本,随机分布在全球各地。获得这些数据后,我们应该绘制图表,并分析温度、湿度、云量和风速方面观察到的任何模式。我将把它们转换成美国常用单位制。你觉得怎样做就怎样做吧。
注意:有很多不同的方法可以解决这个问题。我将向您展示一种方法。您可以自由探索您自己的解决方法,看看结果是否相似。
第二点说明:我使用了一些不属于标准库的库,但它们可以在 Python 包索引 (PyPI) 中找到。如果遇到No module named 'whatever'
错误,您需要打开一个终端窗口并输入pip install <packagename>
,其中<packagename>
是您缺少的包的名称,然后按 Enter。如果您使用的是 Jupyter Notebook,也可以输入! pip install <packagename>
一个单元格并运行它。感叹号 ( !
) 可以让 Notebook 运行一行系统调用。
我最初使用Jupyter Notebook完成了这项分析。我强烈推荐它。如果你有点焦躁,想提前了解一下,可以在这里找到源代码仓库。
1. 500 个随机坐标
我们首先需要的是 500 个随机坐标。我们需要这些数字涵盖所有可能的纬度范围(-90 度到 90 度),以及所有可能的经度范围(-180 度到 180 度)。注意,负纬度表示南纬,负经度表示西纬。
import numpy as np
import pandas as pd
np.random.seed(125) # So that other scientists can duplicate our work!
lats = np.random.randint(-90, 90, size=500)
longs = np.random.randint(-180, 180, size=500)
coords = pd.DataFrame({
"latitude": lats,
"longitude": longs
})
# Let's take a look at how our coordinates look
coords.head()
纬度 | 经度 | |
---|---|---|
0 | 67 | -117 |
1 | -3 | 11 |
2 | -23 | -146 |
3 | 20 | -19 |
4 | -47 | 6 |
为了理智起见,让我们确保我们的坐标是合理随机的。
from matplotlib import pyplot as plt
# And, we're going to give our plots a bit of pizazz.
# Feel free to skip these two lines
import seaborn
seaborn.set()
plt.hist(coords['latitude'])
plt.show()
plt.hist(coords['longitude'])
plt.show()
虽然有一些峰值,但总体来说,对于我们目前的做法来说,这似乎是合理的。如果您对数据的随机性不满意,请更改随机种子并重新运行上面的单元格。
2. 设置天气 API
这部分会更侧重于管理,编程则没那么有趣。不过没关系!为了获取这些天气数据,我们需要访问一个 Web API 并请求它提供数据。具体来说,我们将使用OpenWeatherData API。您需要创建一个帐户(免费!),然后会获得一个 API 密钥,您可以在帐户页面的“API 密钥”选项卡中找到它。
一定要保密这个密钥(我会给你一些建议,教你如何做好这件事)。你肯定不希望某个不法之徒攻击天气 API,让所有人都以为是你干的。想想你作为天气 API 的良好公民的声誉吧!想想孩子们!
正如页面所示,您的密钥可能需要一些时间才能生效。幸运的是,在使用它之前,我们还需要进行一些设置。现在,我们先来看看要使用的端点。请查看坐标天气端点文档。
我们可以通过几种不同的方式请求我们需要的数据,但由于我们已经创建了一堆漂亮的(纬度,经度)对,我认为这可能是最简单的方法。
http://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&APPID={api_key}
你会注意到,即使在线文档没有直接讨论,我们也需要添加APPID
包含 API 密钥的参数。如果你觉得很酷,还可以添加units=imperial
获取华氏温度和英里/小时风速的参数。你也可以保留默认值,稍后再进行转换。我也会向你演示这个过程。好了,管理工作就到此为止!让我们回到代码!
3. 设置以获取数据
在我们打开分析代码之前,我建议您在同一目录中打开一个名为 的新文件secrets.py
。
# secrets.py
API_KEY = "copy your api key here"
如果您使用 git 存储库跟踪此项目,请将此文件添加到您的.gitignore
文件中。
__pycache__/
.ipynb_checkpoints
secrets.py
haters
现在我们准备重新回到笔记本中。
from secrets import API_KEY
import requests
import time
def get_weather_data(coords, time_between=1):
"""Queries the OpenWeatherAPI for data.
Args:
coords: A Pandas DataFrame with rows containing 'latitude'
and 'longitude' columns.
time_between: An integer specifying the sleep time in seconds
between each API ping. Defaults to the OpenWeatherAPI's
recommended limit of 1 request per second.
Returns:
A list of nested dicts (loaded JSON results).
"""
results = []
for ind, row in coords.iterrows():
lat, lon = row['latitude'], row['longitude']
query = f"http://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&APPID={API_KEY}"
response = requests.get(query)
results.append(response.json())
time.sleep(time_between)
return results
这段代码有两个关键特性。第一个是“f-string”,它是 Python 3 用于字符串插值的 shwoopy 语法。优点是这些“f-string”超级快!至少相对而言是这样。而且,我们可以直接从 DataFrame 行插入经纬度值,以及 API 密钥。
另一个关键特性是,我们使用requests
来发起get
请求,然后使用json
函数立即将响应处理成我们可以处理的 Python 字典。如果你不确定如何从 API 获取数据,你可能会失望,因为它并没有比这更复杂。只要你知道正确的 URL,requests
我们的工作就会非常轻松。
3a. 记录我们的请求
我要快速说两句题外话,再练习一下。如果你想直接跳到第四步,别担心。你不会伤害我的。
我接下来要做的第一件事是设置一些日志记录到文件中。在笔记本顶部,添加以下代码。
import logging
logger = logging.getLogger('weather')
logger.setLevel(logging.INFO)
fh = logging.FileHandler('api_calls.log')
formatter = logging.Formatter('%(asctime)s - %(message)')
fh.setFormatter(formatter)
logger.addHandler(fh)
然后在你的get_weather_data
函数内部:
def get_weather_data(coords, time_between=1):
# ...
results = []
for ind, row in coords.iterrows():
lat, long = row['latitude'], row['longitude']
query = f"http://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&APPID={API_KEY}"
# Here's the new stuff
clean_url = query.rpartition("&")[0] # Don't log your api key!
logger.info(f"Call {ind}: ({lat}, {lon}) - {clean_url}")
response = requests.get(query)
results.append(response.json())
time.sleep(time_between)
return results
现在我们可以保存所有访问过的 URL 的日志!
3b. 获取最接近的城市名称
你知道什么会很棒吗?用我们的日志输出最近的城市名称。有一个简洁的小库citipy
可以做到这一点!让我们get_weather_data
再次更新我们的函数。
from secrets import API_KEY
from citipy import citipy # Make sure to import it once you've installed it
def get_weather_data(coords, time_between=1):
# ...
results = []
for ind, row in coords.iterrows():
lat, lon = row['latitude'], row['longitude']
query = f"http://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&APPID={API_KEY}"
clean_url = query.rpartition("&")[0]
# Here's the new stuff
city = citipy.nearest_city(lat, lon)
logger.info(f"Call {ind}: {city.city_name} {clean_url})")
result = requests.get(query)
results.append(result.json())
time.sleep(time_between)
return results
这太棒了!回到手头的问题。
4. 实际获取数据
首先,让我们通过测试调用来测试我们的功能。
test_coords = pd.DataFrame({"latitude": [37], "longitude": [-122]})
test_results = get_weather_data(test_coords)
test_results
[{'base': 'stations',
'clouds': {'all': 1},
'cod': 200,
'coord': {'lat': 37, 'lon': -122},
'dt': 1522341300,
'id': 5381421,
'main': {'humidity': 76,
'pressure': 1021,
'temp': 287.78,
'temp_max': 289.15,
'temp_min': 286.15},
'name': 'Pasatiempo',
'sys': {'country': 'US',
'id': 399,
'message': 0.004,
'sunrise': 1522331815,
'sunset': 1522376913,
'type': 1},
'visibility': 16093,
'weather': [{'description': 'clear sky',
'icon': '01d',
'id': 800,
'main': 'Clear'}],
'wind': {'deg': 331.003, 'speed': 1.32}}]
如果您的结果与我的一样,那么看起来我们可以进行完整的数据收集。
full_results = get_weather_data(coords)
full_results[:3] # Let's peek at the first 3 datapoints
这大约需要8分30秒(这是成为好公民的代价)。去喝杯咖啡或吃点零食,犒劳一下自己所有的辛勤工作吧。
5.保存数据
第一件事是,先把数据保存起来,万一发生爆炸,我们还能用。
import json
with open("weather.json", "w") as outfile:
json.dump(full_results, outfile)
这将在你的项目目录中创建一个新文件weather.json
。接下来是另一个可选的辅助步骤:单位转换。
5a. 单位换算
如果您没有units=imperial
在 API 调用中使用该参数并且想要美国习惯单位,那么您将需要一些辅助函数。
def k_to_f(temp):
"""Converts a Kelvin temperature to Fahrenheit"""
return temp * 9/5 - 459.67
def mps_to_mph(speed):
"""Converts a meters/s speed to miles/hour"""
return speed * 2.23694
6. 数据处理
是的,这是一个单词。查一下。随便。我们需要构建一个可以转换成 DataFrame 的数据结构,并且希望将范围缩小到我们关心的数据。再看看上面的示例输出,并深入研究 JSON 数据。
important_json_data = []
for point in full_results:
lat = point['coord']['lat']
lon = point['coord']['lon']
temp = k_to_f(point['main']['temp'])
humidity = point['main']['humidity']
cloudiness = point['clouds']['all']
wind = mps_to_mph(point['wind']['speed'])
row = [lat, lon, temp, humidity, cloudiness, wind]
important_json_data.append(row)
weather_df = pd.DataFrame(important_json_data)
weather_df.columns = [
"latitude",
"longitude",
"temperature",
"humidity",
"clouds",
"wind",
]
weather_df.head()
纬度 | 经度 | 温度 | 湿度 | 云 | 风 | |
---|---|---|---|---|---|---|
0 | 67 | -117 | -16.15 | 69 | 8 | 4.29 |
1 | -3 | 11 | 74.93 | 96 | 68 | 2.17 |
2 | -23 | -146 | 80.96 | 100 | 88 | 12.91 |
3 | 20 | -19 | 67.37 | 100 | 8 | 9.78 |
4 | -47 | 6 | 44.78 | 97 | 8 | 13.35 |
再次,为了以防万一,我们还是保存一下数据吧。
weather_df.to_csv("weather.csv")
恭喜!最难的部分完成了。让我们看看数据,看看能得出什么结论。
7.绘制数据
还记得我们的目标吗?
- 比较温度和纬度。
- 比较湿度和纬度。
- 比较云量和纬度。
- 比较风速和纬度。
- 得出一些结论。
我打算把纬度放在 Y 轴上,因为我觉得这样绘图会更直观。我们通常认为纬度是从北向南,也就是从上向下。如果你想坚持把自变量(纬度)放在 X 轴,把因变量(温度)放在 Y 轴上,那就随心所欲吧。
温度
plt.scatter(weather_df.temperature, weather_df.latitude)
plt.xlabel("Temperature (F)")
plt.ylabel("Latitude (degrees)")
plt.title("Temperature vs. Latitude")
plt.show()
哇哦!这趋势真强劲!正如你可能预料的那样,温度随着接近赤道而上升,随着接近两极而下降。加油,科学!
湿度
plt.scatter(weather_df.humidity, weather_df.latitude)
plt.xlabel("Humidity (%)")
plt.ylabel("Latitude (degrees)")
plt.title("Humidity vs. Latitude")
plt.show()
这些结果很奇怪。看起来,除了少数几个下降点外,很多数据点的湿度都是100%。我觉得这难以置信。我在谷歌上找到了一些结果,让我怀疑他们测量湿度的方式是不是有什么问题。如果有人有其他想法,我很想听听。请告诉我你的想法。
云量
plt.scatter(weather_df.clouds, weather_df.lat)
plt.xlabel("Cloudiness (%)")
plt.ylabel("Latitude (degrees)")
plt.title("Cloudiness vs. Latitude")
plt.show()
我在这里也看不出什么趋势。不过,数据的条纹(整齐的行)让我感觉好像有某种规律。我们看看是否存在经度关系。
plt.scatter(weather_df.long, weather_df.clouds)
plt.xlabel("Longitude (degrees)")
plt.ylabel("Cloudiness (%)")
plt.title("Longitude vs. Cloudiness")
plt.show()
嗯……我还是没看出什么关系。再说一次,如果有人有什么想法,请告诉我!
风速
plt.scatter(weather_df.wind, weather_df.lat)
plt.xlabel("Wind Speed (mph (abs))")
plt.ylabel("Latitude (degrees)")
plt.title("Wind Speed vs. Latitude")
plt.show()
这是一个有趣的图。我们看到的是一片混乱,但在-50度和50度左右有一些明显的峰值。它似乎在两极和赤道附近逐渐下降到零度。一开始我有点困惑,但后来我想起了我八年级的科学课。
有一股被称为“西风”的风,吹拂在南北纬40度至50度之间。这些风有时也被称为“咆哮西风带”。由于南半球海域广阔(没有陆地或树木阻挡风),它们被用来加快航行速度。在南半球夏季,它们往往会向赤道方向移动,而在冬季,它们则会向极地方向移动。
相反,赤道周围的区域被称为“热带辐合带”,也称为“赤道无风带”。根据季节的不同,这个区域会出现静风和雷暴。
我可以相当肯定地说,我们的数据似乎支持这一趋势。所以,再次为科学欢呼!
包起来
就这样!希望你喜欢这次练习。如果你从我们的数据中发现了其他有趣的发现,请务必与我分享。
祝您愉快!
最初发布于assert_not magic?