This is the best solution I could come up with:
#standardSQL
CREATE TEMP FUNCTION distance(lat1 FLOAT64, lat2 FLOAT64, lon1 FLOAT64, lon2 FLOAT64) AS((
WITH data AS(
SELECT POW(SIN((ACOS(-1) / 180 * (lat1 -lat2)) / 2), 2) + COS(ACOS(-1) / 180 * (lat1)) * COS(ACOS(-1) / 180 * (lat2)) * POW(SIN((ACOS(-1) / 180 * (lon1 -lon2)) / 2), 2) a
)
SELECT 6371 * 2 * ATAN2(SQRT((SELECT a FROM data)), SQRT(1 - (SELECT a FROM data)))
));
WITH temperature_data AS(
SELECT
CONCAT(year, mo, da) date,
temp,
b.lat lat,
b.lon lon
FROM `bigquery-public-data.noaa_gsod.gsod2016` a
JOIN `bigquery-public-data.noaa_gsod.stations` b
ON a.stn = b.usaf AND a.wban = b.wban
WHERE concat(year, mo, da) BETWEEN FORMAT_DATE('%Y%m%d', DATE_SUB(PARSE_DATE('%Y%m%d', '20160725'), INTERVAL 7 DAY)) AND '20160725'
)
SELECT
date,
STRUCT(AVG(IF(distance(t.lat, 10.1, t.lon, 10.2) < 20, temp, NULL)) AS avg_temp, STDDEV_SAMP(IF(distance(t.lat, 10.1, t.lon, 10.2) < 20, temp, NULL)) AS std_temp) data_20km,
STRUCT(AVG(IF(distance(t.lat, 10.1, t.lon, 10.2) < 50, temp, NULL)) AS avg_temp, STDDEV_SAMP(IF(distance(t.lat, 10.1, t.lon, 10.2) < 50, temp, NULL)) AS std_temp) data_50km,
STRUCT(AVG(IF(distance(t.lat, 10.1, t.lon, 10.2) < 100, temp, NULL)) AS avg_temp, STDDEV_SAMP(IF(distance(t.lat, 10.1, t.lon, 10.2) < 100, temp, NULL)) AS std_temp) data_100km,
STRUCT(AVG(IF(distance(t.lat, 10.1, t.lon, 10.2) < 200, temp, NULL)) AS avg_temp, STDDEV_SAMP(IF(distance(t.lat, 10.1, t.lon, 10.2) < 200, temp, NULL)) AS std_temp) data_200km,
STRUCT(AVG(IF(distance(t.lat, 10.1, t.lon, 10.2) < 500, temp, NULL)) AS avg_temp, STDDEV_SAMP(IF(distance(t.lat, 10.1, t.lon, 10.2) < 500, temp, NULL)) AS std_temp) data_500km
FROM temperature_data t
WHERE
distance(t.lat, 10.1, t.lon, 10.2) < 2000
GROUP BY date
ORDER BY date
I'll try to explain along with your questions:
How can I get past 7 days of a specific date?
Inside the query temperature_data, notice there WHERE clause has the condition:
WHERE concat(year, mo, da) BETWEEN FORMAT_DATE('%Y%m%d', DATE_SUB(PARSE_DATE('%Y%m%d', '20160725'), INTERVAL 7 DAY)) AND '20160725'
This is where the last 7 days are selected from a given date. You can choose the date you want to analyze just by changing the value '20160725'.
Can max and min lat/lon be calculated directly through the query?
Yes. I imagine you mean by that is if it's possible to select spatial points within a given range (say 20km for instance).
One way of doing so is defining a temporary function to compute distances between a desired point and the station points, which is expressed in the query by:
CREATE TEMP FUNCTION distance(lat1 FLOAT64, lat2 FLOAT64, lon1 FLOAT64, lon2 FLOAT64) AS((
WITH data AS(
SELECT POW(SIN((ACOS(-1) / 180 * (lat1 -lat2)) / 2), 2) + COS(ACOS(-1) / 180 * (lat1)) * COS(ACOS(-1) / 180 * (lat2)) * POW(SIN((ACOS(-1) / 180 * (lon1 -lon2)) / 2), 2) a
)
SELECT 6371 * 2 * ATAN2(SQRT((SELECT a FROM data)), SQRT(1 - (SELECT a FROM data)))
));
You can play around and test this function, for instance:
SELECT distance(50, 60, 30, 10) # result is ~ 1680km
This function is used here:
WHERE
distance(t.lat, 10.1, t.lon, 10.2) < 2000
to filter out points more than 2000km farther away from (10.1°, 10.2°). In your query, you can choose a different input value instead of (10.1°, 10.2°).
Very often it does not find any data because most likely the radius of
20km is too small to find a station. How can I modify the query to
find the closest stations if it cannot find it within 20km radius?
One possible solution is querying for several different distances at once:
SELECT
date,
STRUCT(AVG(IF(distance(t.lat, 10.1, t.lon, 10.2) < 20, temp, NULL)) AS avg_temp, STDDEV_SAMP(IF(distance(t.lat, 10.1, t.lon, 10.2) < 20, temp, NULL)) AS std_temp) data_20km,
STRUCT(AVG(IF(distance(t.lat, 10.1, t.lon, 10.2) < 50, temp, NULL)) AS avg_temp, STDDEV_SAMP(IF(distance(t.lat, 10.1, t.lon, 10.2) < 50, temp, NULL)) AS std_temp) data_50km,
STRUCT(AVG(IF(distance(t.lat, 10.1, t.lon, 10.2) < 100, temp, NULL)) AS avg_temp, STDDEV_SAMP(IF(distance(t.lat, 10.1, t.lon, 10.2) < 100, temp, NULL)) AS std_temp) data_100km,
STRUCT(AVG(IF(distance(t.lat, 10.1, t.lon, 10.2) < 200, temp, NULL)) AS avg_temp, STDDEV_SAMP(IF(distance(t.lat, 10.1, t.lon, 10.2) < 200, temp, NULL)) AS std_temp) data_200km,
STRUCT(AVG(IF(distance(t.lat, 10.1, t.lon, 10.2) < 500, temp, NULL)) AS avg_temp, STDDEV_SAMP(IF(distance(t.lat, 10.1, t.lon, 10.2) < 500, temp, NULL)) AS std_temp) data_500km
FROM temperature_data t
WHERE
distance(t.lat, 10.1, t.lon, 10.2) < 2000
GROUP BY date
Notice that this query is extracting station points ranging from the input point (10.1°, 10.2°) up to 2000km away. And then a filter is applied to select points within the ranges 20km, 50km, 100km, 200km and 500km.
You can change those values as you see fit. If you want to get the average temperature from another point, say (40°, 30°), just change the values (10.1, 10.2) to (40, 30) and you are good to go. Also, if you want different distances from this point, you can change the expressions IF(distance(t.lat, 10.1, t.lon, 10.2) < 200 for instance to a range that better suits your needs.
Notice that the WHERE clause has the condition:
distance(t.lat, 10.1, t.lon, 10.2) < 2000
So this is filtering out all stations farther away from the point (10.1, 10.2) by more than 2000km. You could also change this value as you see fit.
Final note on this one: I also brought the STDDEV_SAMP which is the standard deviation of a sampling. This might be of some value to you as well as it gives you an idea of how much the average is spreading around the mean (corrected by the sampling data size effect). The average by itself is not that valuable if we don't know how close we truly are to the correct value.
Is there better, free, historical weather data I can get?
Don't know. Hopefully this public dataset will turn out to be good enough for you.