meteostat 1.7.5__py3-none-any.whl → 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- meteostat/__init__.py +32 -19
- meteostat/api/daily.py +76 -0
- meteostat/api/hourly.py +80 -0
- meteostat/api/interpolate.py +240 -0
- meteostat/api/inventory.py +59 -0
- meteostat/api/merge.py +103 -0
- meteostat/api/monthly.py +73 -0
- meteostat/api/normals.py +144 -0
- meteostat/api/point.py +30 -0
- meteostat/api/stations.py +234 -0
- meteostat/api/timeseries.py +334 -0
- meteostat/core/cache.py +212 -59
- meteostat/core/config.py +158 -0
- meteostat/core/data.py +199 -0
- meteostat/core/logger.py +9 -0
- meteostat/core/network.py +82 -0
- meteostat/core/parameters.py +112 -0
- meteostat/core/providers.py +184 -0
- meteostat/core/schema.py +170 -0
- meteostat/core/validator.py +38 -0
- meteostat/enumerations.py +149 -0
- meteostat/interpolation/idw.py +120 -0
- meteostat/interpolation/lapserate.py +91 -0
- meteostat/interpolation/nearest.py +31 -0
- meteostat/parameters.py +354 -0
- meteostat/providers/dwd/climat.py +166 -0
- meteostat/providers/dwd/daily.py +144 -0
- meteostat/providers/dwd/hourly.py +218 -0
- meteostat/providers/dwd/monthly.py +138 -0
- meteostat/providers/dwd/mosmix.py +351 -0
- meteostat/providers/dwd/poi.py +117 -0
- meteostat/providers/dwd/shared.py +155 -0
- meteostat/providers/eccc/daily.py +87 -0
- meteostat/providers/eccc/hourly.py +104 -0
- meteostat/providers/eccc/monthly.py +66 -0
- meteostat/providers/eccc/shared.py +45 -0
- meteostat/providers/index.py +496 -0
- meteostat/providers/meteostat/daily.py +65 -0
- meteostat/providers/meteostat/daily_derived.py +110 -0
- meteostat/providers/meteostat/hourly.py +66 -0
- meteostat/providers/meteostat/monthly.py +45 -0
- meteostat/providers/meteostat/monthly_derived.py +106 -0
- meteostat/providers/meteostat/shared.py +93 -0
- meteostat/providers/metno/forecast.py +186 -0
- meteostat/providers/noaa/ghcnd.py +228 -0
- meteostat/providers/noaa/isd_lite.py +142 -0
- meteostat/providers/noaa/metar.py +163 -0
- meteostat/typing.py +113 -0
- meteostat/utils/conversions.py +231 -0
- meteostat/utils/data.py +194 -0
- meteostat/utils/geo.py +28 -0
- meteostat/utils/parsers.py +168 -0
- meteostat/utils/types.py +113 -0
- meteostat/utils/validators.py +31 -0
- meteostat-2.0.0.dist-info/METADATA +134 -0
- meteostat-2.0.0.dist-info/RECORD +63 -0
- {meteostat-1.7.5.dist-info → meteostat-2.0.0.dist-info}/WHEEL +1 -2
- meteostat/core/loader.py +0 -103
- meteostat/core/warn.py +0 -34
- meteostat/enumerations/granularity.py +0 -22
- meteostat/interface/base.py +0 -39
- meteostat/interface/daily.py +0 -118
- meteostat/interface/hourly.py +0 -154
- meteostat/interface/meteodata.py +0 -210
- meteostat/interface/monthly.py +0 -109
- meteostat/interface/normals.py +0 -245
- meteostat/interface/point.py +0 -143
- meteostat/interface/stations.py +0 -252
- meteostat/interface/timeseries.py +0 -237
- meteostat/series/aggregate.py +0 -48
- meteostat/series/convert.py +0 -28
- meteostat/series/count.py +0 -17
- meteostat/series/coverage.py +0 -20
- meteostat/series/fetch.py +0 -28
- meteostat/series/interpolate.py +0 -47
- meteostat/series/normalize.py +0 -76
- meteostat/series/stations.py +0 -22
- meteostat/units.py +0 -149
- meteostat/utilities/__init__.py +0 -0
- meteostat/utilities/aggregations.py +0 -37
- meteostat/utilities/endpoint.py +0 -33
- meteostat/utilities/helpers.py +0 -70
- meteostat/utilities/mutations.py +0 -85
- meteostat/utilities/validations.py +0 -30
- meteostat-1.7.5.dist-info/METADATA +0 -112
- meteostat-1.7.5.dist-info/RECORD +0 -39
- meteostat-1.7.5.dist-info/top_level.txt +0 -1
- /meteostat/{core → api}/__init__.py +0 -0
- /meteostat/{enumerations → interpolation}/__init__.py +0 -0
- /meteostat/{interface → providers}/__init__.py +0 -0
- /meteostat/{interface/interpolate.py → py.typed} +0 -0
- /meteostat/{series → utils}/__init__.py +0 -0
- {meteostat-1.7.5.dist-info → meteostat-2.0.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Functions for converting between different
|
|
3
|
+
meteorological data units.
|
|
4
|
+
|
|
5
|
+
The code is licensed under the MIT license.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import math
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from numpy import isnan
|
|
12
|
+
|
|
13
|
+
from meteostat.enumerations import Parameter, Unit, UnitSystem
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def celsius_to_fahrenheit(value):
|
|
17
|
+
"""
|
|
18
|
+
Convert Celsius to Fahrenheit
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
return round((value * 9 / 5) + 32, 1)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def celsius_to_kelvin(value):
|
|
25
|
+
"""
|
|
26
|
+
Convert Celsius to Kelvin
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
return round(value + 273.15, 1)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def millimeters_to_inches(value):
|
|
33
|
+
"""
|
|
34
|
+
Convert millimeters to inches
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
return round(value / 25.4, 3)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def centimeters_to_inches(value):
|
|
41
|
+
"""
|
|
42
|
+
Convert centimeters to inches
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
return round(value / 2.54, 3)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def meters_to_feet(value):
|
|
49
|
+
"""
|
|
50
|
+
Convert meters to feet
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
return round(value / 0.3048, 1)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def kmh_to_ms(value):
|
|
57
|
+
"""
|
|
58
|
+
Convert kilometers per hour to meters per second
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
return round(value / 3.6, 1)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def kmh_to_mph(value):
|
|
65
|
+
"""
|
|
66
|
+
Convert kilometers per hour to miles per hour
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
return round(value * 0.6214, 1)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def kelvin_to_celsius(value):
|
|
73
|
+
"""
|
|
74
|
+
Convert Kelvin to Celsius
|
|
75
|
+
"""
|
|
76
|
+
return value - 273.15 if value is not None and not isnan(value) else None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def ms_to_kmh(value):
|
|
80
|
+
"""
|
|
81
|
+
Convert m/s to km/h
|
|
82
|
+
"""
|
|
83
|
+
return value * 3.6 if value is not None and not isnan(value) else None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def temp_dwpt_to_rhum(row: dict):
|
|
87
|
+
"""
|
|
88
|
+
Get relative humidity from temperature and dew point
|
|
89
|
+
"""
|
|
90
|
+
return (
|
|
91
|
+
100
|
|
92
|
+
* (
|
|
93
|
+
math.exp((17.625 * row["dwpt"]) / (243.04 + row["dwpt"]))
|
|
94
|
+
/ math.exp((17.625 * row["temp"]) / (243.04 + row["temp"]))
|
|
95
|
+
)
|
|
96
|
+
if row["temp"] is not None and row["dwpt"] is not None
|
|
97
|
+
else None
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def pres_to_msl(row: dict, altitude: Optional[int] = None, temp: str = Parameter.TEMP):
|
|
102
|
+
"""
|
|
103
|
+
Convert local air pressure to MSL
|
|
104
|
+
"""
|
|
105
|
+
try:
|
|
106
|
+
return (
|
|
107
|
+
None
|
|
108
|
+
if (
|
|
109
|
+
not row[Parameter.PRES]
|
|
110
|
+
or not row[temp]
|
|
111
|
+
or not altitude
|
|
112
|
+
or row[Parameter.PRES] == -999
|
|
113
|
+
)
|
|
114
|
+
else round(
|
|
115
|
+
row[Parameter.PRES]
|
|
116
|
+
* math.pow(
|
|
117
|
+
(
|
|
118
|
+
1
|
|
119
|
+
- (
|
|
120
|
+
(0.0065 * altitude)
|
|
121
|
+
/ (row[temp] + 0.0065 * altitude + 273.15)
|
|
122
|
+
)
|
|
123
|
+
),
|
|
124
|
+
-5.257,
|
|
125
|
+
),
|
|
126
|
+
1,
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
except Exception:
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def percentage_to_okta(value):
|
|
134
|
+
"""
|
|
135
|
+
Convert cloud cover percentage to oktas
|
|
136
|
+
"""
|
|
137
|
+
return round(value / 12.5) if value is not None and not isnan(value) else None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def jcm2_to_wm2(value):
|
|
141
|
+
"""
|
|
142
|
+
Convert Joule/CM^2 to Watt/M^2
|
|
143
|
+
"""
|
|
144
|
+
return round(value * 2.78) if value is not None and not isnan(value) else None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def to_direction(value):
|
|
148
|
+
"""
|
|
149
|
+
Convert degrees to wind direction
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
wdir = None
|
|
153
|
+
|
|
154
|
+
if (337 <= value <= 360) or value <= 23:
|
|
155
|
+
wdir = "N"
|
|
156
|
+
if 24 <= value <= 68:
|
|
157
|
+
wdir = "NE"
|
|
158
|
+
if 69 <= value <= 113:
|
|
159
|
+
wdir = "E"
|
|
160
|
+
if 114 <= value <= 158:
|
|
161
|
+
wdir = "SE"
|
|
162
|
+
if 159 <= value <= 203:
|
|
163
|
+
wdir = "S"
|
|
164
|
+
if 204 <= value <= 248:
|
|
165
|
+
wdir = "SW"
|
|
166
|
+
if 249 <= value <= 293:
|
|
167
|
+
wdir = "W"
|
|
168
|
+
if 294 <= value <= 336:
|
|
169
|
+
wdir = "NW"
|
|
170
|
+
|
|
171
|
+
return wdir
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def to_condition(value):
|
|
175
|
+
"""
|
|
176
|
+
Convert Meteostat condition code to descriptive string
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
if not value or value < 1 or value > 27:
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
return [
|
|
183
|
+
"Clear",
|
|
184
|
+
"Fair",
|
|
185
|
+
"Cloudy",
|
|
186
|
+
"Overcast",
|
|
187
|
+
"Fog",
|
|
188
|
+
"Freezing Fog",
|
|
189
|
+
"Light Rain",
|
|
190
|
+
"Rain",
|
|
191
|
+
"Heavy Rain",
|
|
192
|
+
"Freezing Rain",
|
|
193
|
+
"Heavy Freezing Rain",
|
|
194
|
+
"Sleet",
|
|
195
|
+
"Heavy Sleet",
|
|
196
|
+
"Light Snowfall",
|
|
197
|
+
"Snowfall",
|
|
198
|
+
"Heavy Snowfall",
|
|
199
|
+
"Rain Shower",
|
|
200
|
+
"Heavy Rain Shower",
|
|
201
|
+
"Sleet Shower",
|
|
202
|
+
"Heavy Sleet Shower",
|
|
203
|
+
"Snow Shower",
|
|
204
|
+
"Heavy Snow Shower",
|
|
205
|
+
"Lightning",
|
|
206
|
+
"Hail",
|
|
207
|
+
"Thunderstorm",
|
|
208
|
+
"Heavy Thunderstorm",
|
|
209
|
+
"Storm",
|
|
210
|
+
][int(value) - 1]
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
CONVERSION_MAPPINGS = {
|
|
214
|
+
Unit.CELSIUS: {
|
|
215
|
+
UnitSystem.IMPERIAL: celsius_to_fahrenheit,
|
|
216
|
+
UnitSystem.SI: celsius_to_kelvin,
|
|
217
|
+
},
|
|
218
|
+
Unit.MILLIMETERS: {
|
|
219
|
+
UnitSystem.IMPERIAL: millimeters_to_inches,
|
|
220
|
+
},
|
|
221
|
+
Unit.CENTIMETERS: {
|
|
222
|
+
UnitSystem.IMPERIAL: centimeters_to_inches,
|
|
223
|
+
},
|
|
224
|
+
Unit.KMH: {
|
|
225
|
+
UnitSystem.SI: kmh_to_ms,
|
|
226
|
+
UnitSystem.IMPERIAL: kmh_to_mph,
|
|
227
|
+
},
|
|
228
|
+
Unit.METERS: {
|
|
229
|
+
UnitSystem.IMPERIAL: meters_to_feet,
|
|
230
|
+
},
|
|
231
|
+
}
|
meteostat/utils/data.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DataFrame mutations
|
|
3
|
+
|
|
4
|
+
Meteorological data provided by Meteostat (https://dev.meteostat.net)
|
|
5
|
+
under the terms of the Creative Commons Attribution-NonCommercial
|
|
6
|
+
4.0 International Public License.
|
|
7
|
+
|
|
8
|
+
The code is licensed under the MIT license.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from collections import Counter
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from itertools import chain
|
|
14
|
+
from typing import List
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
import pandas as pd
|
|
18
|
+
|
|
19
|
+
from meteostat.core.providers import provider_service
|
|
20
|
+
from meteostat.enumerations import Frequency
|
|
21
|
+
from meteostat.typing import Station
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def stations_to_df(stations: List[Station]) -> pd.DataFrame:
|
|
25
|
+
"""
|
|
26
|
+
Convert list of stations to DataFrame
|
|
27
|
+
"""
|
|
28
|
+
assert len(stations) > 0 and all(
|
|
29
|
+
isinstance(station, Station) for station in stations
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
return pd.DataFrame.from_records(
|
|
33
|
+
[
|
|
34
|
+
{
|
|
35
|
+
"id": station.id,
|
|
36
|
+
"name": station.name,
|
|
37
|
+
"country": station.country,
|
|
38
|
+
"latitude": station.latitude,
|
|
39
|
+
"longitude": station.longitude,
|
|
40
|
+
"elevation": station.elevation,
|
|
41
|
+
"timezone": station.timezone,
|
|
42
|
+
}
|
|
43
|
+
for station in stations
|
|
44
|
+
],
|
|
45
|
+
index="id",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def squash_df(df: pd.DataFrame, sources=False) -> pd.DataFrame:
|
|
50
|
+
"""
|
|
51
|
+
Squash a DataFrame based on the source priority
|
|
52
|
+
"""
|
|
53
|
+
# Capture the columns
|
|
54
|
+
columns = df.columns
|
|
55
|
+
|
|
56
|
+
# Add source priority column
|
|
57
|
+
df["source_prio"] = df.index.get_level_values("source").map(
|
|
58
|
+
provider_service.get_source_priority
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Shift source information to columns
|
|
62
|
+
if sources:
|
|
63
|
+
df = df.reset_index(level="source")
|
|
64
|
+
for column in columns:
|
|
65
|
+
df[f"{column}_source"] = np.where(df[column].notna(), df["source"], np.nan)
|
|
66
|
+
df = df.set_index("source", append=True)
|
|
67
|
+
|
|
68
|
+
# Get highest priority value/source for each station and time
|
|
69
|
+
df = (
|
|
70
|
+
df.groupby(level=["station", "time", "source"])
|
|
71
|
+
.last() # In case of duplicate index, the last row will be prefered
|
|
72
|
+
.sort_values(by="source_prio", ascending=False)
|
|
73
|
+
.groupby(["station", "time"])
|
|
74
|
+
.first() # Prefer value with highest priority
|
|
75
|
+
.drop("source_prio", axis=1)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Order columns and return squashed DataFrame
|
|
79
|
+
return df[order_source_columns(df.columns)] if sources else df
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def fill_df(
|
|
83
|
+
df: pd.DataFrame, start: datetime, end: datetime, freq: str
|
|
84
|
+
) -> pd.DataFrame:
|
|
85
|
+
"""
|
|
86
|
+
Fill a DataFrame with a complete date range for each station
|
|
87
|
+
"""
|
|
88
|
+
try:
|
|
89
|
+
iterables = [
|
|
90
|
+
df.index.get_level_values("station").unique(),
|
|
91
|
+
pd.date_range(start=start, end=end, freq=freq),
|
|
92
|
+
]
|
|
93
|
+
names = ["station", "time"]
|
|
94
|
+
if "source" in df.index.names:
|
|
95
|
+
iterables.append(df.index.get_level_values("source").unique())
|
|
96
|
+
names.append("source")
|
|
97
|
+
# Create a new MultiIndex with every hour for each station
|
|
98
|
+
new_index = pd.MultiIndex.from_product(iterables, names=names)
|
|
99
|
+
|
|
100
|
+
# Reindex the DataFrame to add missing rows
|
|
101
|
+
df = df.reindex(index=new_index)
|
|
102
|
+
return df
|
|
103
|
+
except KeyError:
|
|
104
|
+
return df
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def localize(df: pd.DataFrame, timezone: str) -> pd.DataFrame:
|
|
108
|
+
"""
|
|
109
|
+
Convert time data to any time zone
|
|
110
|
+
"""
|
|
111
|
+
return df.tz_localize("UTC", level="time").tz_convert(timezone, level="time")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def reshape_by_source(df: pd.DataFrame) -> pd.DataFrame:
|
|
115
|
+
"""
|
|
116
|
+
Reshape a DataFrame so that the source columns are pivoted into
|
|
117
|
+
their own level of the index
|
|
118
|
+
"""
|
|
119
|
+
# Extract value rows and source rows
|
|
120
|
+
value_rows = df.loc[:, ~df.columns.str.endswith("_source")]
|
|
121
|
+
source_rows = df.loc[:, df.columns.str.endswith("_source")]
|
|
122
|
+
|
|
123
|
+
# Melt both value_rows and source_rows
|
|
124
|
+
value_melted = value_rows.reset_index().melt(id_vars="time")
|
|
125
|
+
source_melted = source_rows.reset_index().melt(id_vars="time")
|
|
126
|
+
|
|
127
|
+
# Remove '_source' from the variable names in source_melted
|
|
128
|
+
source_melted["variable"] = source_melted["variable"].str.replace("_source", "")
|
|
129
|
+
|
|
130
|
+
# Merge the melted DataFrames
|
|
131
|
+
merged_df = pd.merge(
|
|
132
|
+
value_melted, source_melted, on=["time", "variable"], suffixes=("", "_source")
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Drop rows with missing values
|
|
136
|
+
merged_df = merged_df.dropna(subset=["value"])
|
|
137
|
+
|
|
138
|
+
# Pivot the DataFrame
|
|
139
|
+
df_pivoted = merged_df.pivot(
|
|
140
|
+
index=["time", "value_source"], columns="variable", values="value"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Flatten the MultiIndex
|
|
144
|
+
df_pivoted.index = df_pivoted.index.rename(["time", "source"])
|
|
145
|
+
df_pivoted.columns.name = None
|
|
146
|
+
|
|
147
|
+
# Preserve the original parameter column order from value_rows
|
|
148
|
+
# Pivot often produces alphabetical column order; reindex to match input
|
|
149
|
+
ordered_columns = [col for col in value_rows.columns if col in df_pivoted.columns]
|
|
150
|
+
if ordered_columns:
|
|
151
|
+
df_pivoted = df_pivoted.reindex(columns=ordered_columns)
|
|
152
|
+
|
|
153
|
+
return df_pivoted
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def aggregate_sources(series: pd.Series) -> str:
|
|
157
|
+
"""
|
|
158
|
+
Concatenate multiple data sources into a unique source string,
|
|
159
|
+
ordered by the number of occurrences.
|
|
160
|
+
"""
|
|
161
|
+
# Extract sources and flatten them
|
|
162
|
+
sources = [str(item) for item in series if pd.notna(item)]
|
|
163
|
+
flat_sources = list(chain(*[source.split() for source in sources]))
|
|
164
|
+
|
|
165
|
+
# Count occurrences of each source
|
|
166
|
+
source_counts = Counter(flat_sources)
|
|
167
|
+
|
|
168
|
+
# Sort sources by count in descending order
|
|
169
|
+
sorted_sources = sorted(source_counts, key=lambda s: source_counts[s], reverse=True)
|
|
170
|
+
|
|
171
|
+
# Concatenate sorted sources into a unique source string
|
|
172
|
+
return " ".join(sorted_sources)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def enforce_freq(df: pd.DataFrame, freq: Frequency) -> pd.DataFrame:
|
|
176
|
+
"""
|
|
177
|
+
Enforce a specific frequency on a DataFrame by resampling
|
|
178
|
+
"""
|
|
179
|
+
df.index = pd.to_datetime(df.index.get_level_values("time"))
|
|
180
|
+
return df.resample(freq).first()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def order_source_columns(columns: pd.Index) -> List[str]:
|
|
184
|
+
"""
|
|
185
|
+
Order source columns
|
|
186
|
+
"""
|
|
187
|
+
ordered_columns = []
|
|
188
|
+
|
|
189
|
+
for col in columns:
|
|
190
|
+
if not col.endswith("_source"):
|
|
191
|
+
ordered_columns.append(col)
|
|
192
|
+
ordered_columns.append(f"{col}_source")
|
|
193
|
+
|
|
194
|
+
return ordered_columns
|
meteostat/utils/geo.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utilities for geographical calculations.
|
|
3
|
+
|
|
4
|
+
The code is licensed under the MIT license.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_distance(lat1, lon1, lat2, lon2) -> int:
|
|
11
|
+
"""
|
|
12
|
+
Calculate distance between two geographical points using the Haversine formula
|
|
13
|
+
"""
|
|
14
|
+
# Earth radius in meters
|
|
15
|
+
radius = 6371000
|
|
16
|
+
|
|
17
|
+
# Degress to radian
|
|
18
|
+
lat1, lon1, lat2, lon2 = map(np.deg2rad, [lat1, lon1, lat2, lon2])
|
|
19
|
+
|
|
20
|
+
# Deltas
|
|
21
|
+
dlat = lat2 - lat1
|
|
22
|
+
dlon = lon2 - lon1
|
|
23
|
+
|
|
24
|
+
# Calculate distance
|
|
25
|
+
arch = np.sin(dlat / 2) ** 2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon / 2) ** 2
|
|
26
|
+
arch_sin = 2 * np.arcsin(np.sqrt(arch))
|
|
27
|
+
|
|
28
|
+
return round(radius * arch_sin)
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data parsers
|
|
3
|
+
|
|
4
|
+
The code is licensed under the MIT license.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import calendar
|
|
8
|
+
from typing import List
|
|
9
|
+
import datetime
|
|
10
|
+
|
|
11
|
+
import pandas as pd
|
|
12
|
+
import pytz
|
|
13
|
+
|
|
14
|
+
from meteostat.api.stations import stations as stations_service
|
|
15
|
+
from meteostat.api.point import Point
|
|
16
|
+
from meteostat.core.config import config
|
|
17
|
+
from meteostat.typing import Station
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def parse_station(
|
|
21
|
+
station: (str | Station | Point | List[str | Station | Point] | pd.DataFrame),
|
|
22
|
+
) -> Station | List[Station]:
|
|
23
|
+
"""
|
|
24
|
+
Parse one or multiple station(s) or geo point(s)
|
|
25
|
+
|
|
26
|
+
Point objects are converted to virtual stations with IDs like $0001, $0002, etc.
|
|
27
|
+
based on their position in the input list.
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
Station | List[Station]
|
|
32
|
+
- Returns a single Station object for single-station input (str, Station, Point)
|
|
33
|
+
- Returns a list of Station objects for multi-station input (list, pd.Index, etc.)
|
|
34
|
+
"""
|
|
35
|
+
# Return data if it contains station meta data (single station)
|
|
36
|
+
if isinstance(station, Station):
|
|
37
|
+
return station
|
|
38
|
+
|
|
39
|
+
# Handle Point objects (single point)
|
|
40
|
+
if isinstance(station, Point):
|
|
41
|
+
return _point_to_station(station, 1)
|
|
42
|
+
|
|
43
|
+
# Handle string (single station ID)
|
|
44
|
+
if isinstance(station, str):
|
|
45
|
+
meta = stations_service.meta(station)
|
|
46
|
+
if meta is None:
|
|
47
|
+
raise ValueError(f'Weather station with ID "{station}" could not be found')
|
|
48
|
+
return meta
|
|
49
|
+
|
|
50
|
+
# Convert station identifier(s) to list (multi-station)
|
|
51
|
+
if isinstance(station, pd.DataFrame):
|
|
52
|
+
stations = station.index.tolist()
|
|
53
|
+
else:
|
|
54
|
+
# It's a list
|
|
55
|
+
stations = station
|
|
56
|
+
|
|
57
|
+
if config.block_large_requests and len(stations) > 10:
|
|
58
|
+
raise ValueError(
|
|
59
|
+
"Requests with more than 10 stations are blocked by default. "
|
|
60
|
+
"To enable large requests, set `config.block_large_requests = False`."
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Get station meta data
|
|
64
|
+
data = []
|
|
65
|
+
point_counter = 0
|
|
66
|
+
for s in stations:
|
|
67
|
+
# Append data early if it contains station meta data
|
|
68
|
+
if isinstance(s, Station):
|
|
69
|
+
data.append(s)
|
|
70
|
+
continue
|
|
71
|
+
# Handle Point objects
|
|
72
|
+
if isinstance(s, Point):
|
|
73
|
+
point_counter += 1
|
|
74
|
+
data.append(_point_to_station(s, point_counter))
|
|
75
|
+
continue
|
|
76
|
+
# Get station meta data
|
|
77
|
+
meta = stations_service.meta(s)
|
|
78
|
+
# Raise exception if station could not be found
|
|
79
|
+
if meta is None:
|
|
80
|
+
raise ValueError(f'Weather station with ID "{s}" could not be found')
|
|
81
|
+
# Append station meta data
|
|
82
|
+
data.append(meta)
|
|
83
|
+
|
|
84
|
+
# Return list of station meta data
|
|
85
|
+
return data
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _point_to_station(point: Point, index: int) -> Station:
|
|
89
|
+
"""
|
|
90
|
+
Convert a Point object to a virtual Station object
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
point : Point
|
|
95
|
+
The Point object to convert
|
|
96
|
+
index : int
|
|
97
|
+
The position in the list of points (1-indexed)
|
|
98
|
+
|
|
99
|
+
Returns
|
|
100
|
+
-------
|
|
101
|
+
Station
|
|
102
|
+
A virtual Station object with an ID like $0001
|
|
103
|
+
"""
|
|
104
|
+
# Create virtual station ID
|
|
105
|
+
station_id = f"${index:04d}"
|
|
106
|
+
|
|
107
|
+
# Create Station object with extracted coordinates
|
|
108
|
+
return Station(
|
|
109
|
+
id=station_id,
|
|
110
|
+
name=f"Location #{index}",
|
|
111
|
+
latitude=point.latitude,
|
|
112
|
+
longitude=point.longitude,
|
|
113
|
+
elevation=point.elevation,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def parse_time(
|
|
118
|
+
value: datetime.date | datetime.datetime | None,
|
|
119
|
+
timezone: str | None = None,
|
|
120
|
+
is_end: bool = False,
|
|
121
|
+
) -> datetime.datetime | None:
|
|
122
|
+
"""
|
|
123
|
+
Convert a given date/time input to datetime
|
|
124
|
+
|
|
125
|
+
To set the time of a date to 23:59:59, pass is_end=True
|
|
126
|
+
"""
|
|
127
|
+
if not value:
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
if not isinstance(value, datetime.datetime):
|
|
131
|
+
parsed = datetime.datetime.combine(
|
|
132
|
+
value,
|
|
133
|
+
datetime.datetime.max.time() if is_end else datetime.datetime.min.time(),
|
|
134
|
+
)
|
|
135
|
+
else:
|
|
136
|
+
parsed = value
|
|
137
|
+
|
|
138
|
+
if timezone:
|
|
139
|
+
tz = pytz.timezone(timezone)
|
|
140
|
+
# If the datetime is already aware, convert it directly
|
|
141
|
+
# If it's naive, treat it as being in the target timezone
|
|
142
|
+
if parsed.tzinfo is not None:
|
|
143
|
+
parsed = parsed.astimezone(pytz.utc).replace(tzinfo=None)
|
|
144
|
+
else:
|
|
145
|
+
parsed = tz.localize(parsed).astimezone(pytz.utc).replace(tzinfo=None)
|
|
146
|
+
|
|
147
|
+
return parsed
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def parse_month(
|
|
151
|
+
value: datetime.date | datetime.datetime | None, is_end: bool = False
|
|
152
|
+
) -> datetime.date | None:
|
|
153
|
+
"""
|
|
154
|
+
Convert a given date/time input to the first or last day of the month respectively
|
|
155
|
+
"""
|
|
156
|
+
if not value:
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
last_day = calendar.monthrange(value.year, value.month)[1]
|
|
160
|
+
|
|
161
|
+
return datetime.date(value.year, value.month, last_day if is_end else 1)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def parse_year(year: int, is_end: bool = False) -> datetime.date:
|
|
165
|
+
"""
|
|
166
|
+
Parse a year into a date, returning either the first or last day of the year
|
|
167
|
+
"""
|
|
168
|
+
return datetime.date(year, 12, 31) if is_end else datetime.date(year, 1, 1)
|