meteostat 1.7.6__py3-none-any.whl → 2.0.1__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 +38 -19
- meteostat/api/config.py +158 -0
- meteostat/api/daily.py +76 -0
- meteostat/api/hourly.py +80 -0
- meteostat/api/interpolate.py +378 -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/data.py +203 -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/guards.py +51 -0
- meteostat/utils/parsers.py +161 -0
- meteostat/utils/types.py +113 -0
- meteostat/utils/validators.py +31 -0
- meteostat-2.0.1.dist-info/METADATA +130 -0
- meteostat-2.0.1.dist-info/RECORD +64 -0
- {meteostat-1.7.6.dist-info → meteostat-2.0.1.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 -89
- meteostat/utilities/validations.py +0 -30
- meteostat-1.7.6.dist-info/METADATA +0 -112
- meteostat-1.7.6.dist-info/RECORD +0 -39
- meteostat-1.7.6.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.6.dist-info → meteostat-2.0.1.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interpolation Module
|
|
3
|
+
|
|
4
|
+
Provides spatial interpolation functions for meteorological data.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional, Union
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
from meteostat.api.point import Point
|
|
13
|
+
from meteostat.api.timeseries import TimeSeries
|
|
14
|
+
from meteostat.typing import Station
|
|
15
|
+
from meteostat.enumerations import Parameter
|
|
16
|
+
from meteostat.interpolation.lapserate import apply_lapse_rate
|
|
17
|
+
from meteostat.interpolation.nearest import nearest_neighbor
|
|
18
|
+
from meteostat.interpolation.idw import inverse_distance_weighting
|
|
19
|
+
from meteostat.utils.data import aggregate_sources, reshape_by_source, stations_to_df
|
|
20
|
+
from meteostat.utils.geo import get_distance
|
|
21
|
+
from meteostat.utils.parsers import parse_station
|
|
22
|
+
from meteostat.core.schema import schema_service
|
|
23
|
+
from meteostat.core.logger import logger
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Parameters that are categorical and should not use IDW interpolation
|
|
27
|
+
CATEGORICAL_PARAMETERS = {Parameter.WDIR, Parameter.CLDC, Parameter.COCO}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _create_timeseries(
|
|
31
|
+
ts: TimeSeries, point: Point, df: Optional[pd.DataFrame] = None
|
|
32
|
+
) -> TimeSeries:
|
|
33
|
+
"""
|
|
34
|
+
Create a TimeSeries object from interpolated DataFrame
|
|
35
|
+
"""
|
|
36
|
+
parsed = parse_station(point)
|
|
37
|
+
stations_list = [parsed] if isinstance(parsed, Station) else parsed
|
|
38
|
+
|
|
39
|
+
# Convert stations to DataFrame
|
|
40
|
+
stations_df = stations_to_df(stations_list)
|
|
41
|
+
|
|
42
|
+
return TimeSeries(
|
|
43
|
+
ts.granularity,
|
|
44
|
+
stations_df,
|
|
45
|
+
df=df,
|
|
46
|
+
start=ts.start,
|
|
47
|
+
end=ts.end,
|
|
48
|
+
timezone=ts.timezone,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _add_source_columns(
|
|
53
|
+
result: pd.DataFrame,
|
|
54
|
+
df: pd.DataFrame,
|
|
55
|
+
) -> pd.DataFrame:
|
|
56
|
+
"""
|
|
57
|
+
Add source columns to the result DataFrame
|
|
58
|
+
"""
|
|
59
|
+
source_cols = [c for c in df.columns if c.endswith("_source")]
|
|
60
|
+
if source_cols:
|
|
61
|
+
grouped = df.groupby("time")[source_cols].agg(aggregate_sources)
|
|
62
|
+
if isinstance(grouped, pd.Series):
|
|
63
|
+
grouped = grouped.to_frame(name=source_cols[0])
|
|
64
|
+
grouped.index.name = "time"
|
|
65
|
+
|
|
66
|
+
# Safely align on time and add/fill source columns without causing overlaps
|
|
67
|
+
result_has_time_col = "time" in result.columns
|
|
68
|
+
if result_has_time_col:
|
|
69
|
+
result = result.set_index("time")
|
|
70
|
+
|
|
71
|
+
# Ensure both frames align on the same index (time)
|
|
72
|
+
# For each source column, add it if missing or fill NaNs if present
|
|
73
|
+
for col in source_cols:
|
|
74
|
+
if col in grouped.columns:
|
|
75
|
+
if col in result.columns:
|
|
76
|
+
# Fill missing values in result using aggregated sources
|
|
77
|
+
result[col] = result[col].where(result[col].notna(), grouped[col])
|
|
78
|
+
else:
|
|
79
|
+
# Add aggregated source column
|
|
80
|
+
result[col] = grouped[col]
|
|
81
|
+
|
|
82
|
+
if result_has_time_col:
|
|
83
|
+
result = result.reset_index()
|
|
84
|
+
return result
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _prepare_data_with_distances(
|
|
88
|
+
df: pd.DataFrame, point: Point, elevation_weight: float
|
|
89
|
+
) -> pd.DataFrame:
|
|
90
|
+
"""
|
|
91
|
+
Add distance and elevation calculations to the DataFrame
|
|
92
|
+
"""
|
|
93
|
+
# Add distance column
|
|
94
|
+
df["distance"] = get_distance(
|
|
95
|
+
point.latitude, point.longitude, df["latitude"], df["longitude"]
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Add effective distance column if elevation is available
|
|
99
|
+
if point.elevation is not None and "elevation" in df.columns:
|
|
100
|
+
elev_diff = np.abs(df["elevation"] - point.elevation)
|
|
101
|
+
df["effective_distance"] = np.sqrt(
|
|
102
|
+
df["distance"] ** 2 + (elev_diff * elevation_weight) ** 2
|
|
103
|
+
)
|
|
104
|
+
else:
|
|
105
|
+
df["effective_distance"] = df["distance"]
|
|
106
|
+
|
|
107
|
+
# Add elevation difference column
|
|
108
|
+
if "elevation" in df.columns and point.elevation is not None:
|
|
109
|
+
df["elevation_diff"] = np.abs(df["elevation"] - point.elevation)
|
|
110
|
+
else:
|
|
111
|
+
df["elevation_diff"] = np.nan
|
|
112
|
+
|
|
113
|
+
return df
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _should_use_nearest_neighbor(
|
|
117
|
+
df: pd.DataFrame,
|
|
118
|
+
point: Point,
|
|
119
|
+
distance_threshold: Union[int, None],
|
|
120
|
+
elevation_threshold: Union[int, None],
|
|
121
|
+
) -> bool:
|
|
122
|
+
"""
|
|
123
|
+
Determine if nearest neighbor should be used based on thresholds
|
|
124
|
+
"""
|
|
125
|
+
min_distance = df["distance"].min()
|
|
126
|
+
use_nearest = distance_threshold is None or min_distance <= distance_threshold
|
|
127
|
+
|
|
128
|
+
if use_nearest and point.elevation is not None and "elevation" in df.columns:
|
|
129
|
+
min_elev_diff = np.abs(df["elevation"] - point.elevation).min()
|
|
130
|
+
use_nearest = (
|
|
131
|
+
elevation_threshold is None or min_elev_diff <= elevation_threshold
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return use_nearest
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _get_categorical_columns(df: pd.DataFrame) -> list:
|
|
138
|
+
"""
|
|
139
|
+
Identify categorical columns in the data (excluding source columns)
|
|
140
|
+
"""
|
|
141
|
+
data_cols = [c for c in df.columns if not c.endswith("_source")]
|
|
142
|
+
return [c for c in data_cols if c in CATEGORICAL_PARAMETERS]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _interpolate_with_nearest_neighbor(
|
|
146
|
+
df: pd.DataFrame,
|
|
147
|
+
ts: TimeSeries,
|
|
148
|
+
point: Point,
|
|
149
|
+
distance_threshold: Union[int, None],
|
|
150
|
+
elevation_threshold: Union[int, None],
|
|
151
|
+
) -> Optional[pd.DataFrame]:
|
|
152
|
+
"""
|
|
153
|
+
Perform nearest neighbor interpolation with threshold filtering
|
|
154
|
+
"""
|
|
155
|
+
distance_filter = (
|
|
156
|
+
pd.Series([True] * len(df), index=df.index)
|
|
157
|
+
if distance_threshold is None
|
|
158
|
+
else (df["distance"] <= distance_threshold)
|
|
159
|
+
)
|
|
160
|
+
elevation_filter = (
|
|
161
|
+
pd.Series([True] * len(df), index=df.index)
|
|
162
|
+
if elevation_threshold is None
|
|
163
|
+
else (np.abs(df["elevation"] - point.elevation) <= elevation_threshold)
|
|
164
|
+
)
|
|
165
|
+
df_filtered = df[distance_filter & elevation_filter]
|
|
166
|
+
return nearest_neighbor(df_filtered, ts, point)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _interpolate_with_idw_and_categorical(
|
|
170
|
+
df: pd.DataFrame,
|
|
171
|
+
ts: TimeSeries,
|
|
172
|
+
point: Point,
|
|
173
|
+
categorical_cols: list,
|
|
174
|
+
power: float,
|
|
175
|
+
) -> Optional[pd.DataFrame]:
|
|
176
|
+
"""
|
|
177
|
+
Perform IDW interpolation for non-categorical parameters and nearest neighbor for categorical
|
|
178
|
+
"""
|
|
179
|
+
# For categorical parameters, always use nearest neighbor
|
|
180
|
+
if categorical_cols:
|
|
181
|
+
df_categorical = nearest_neighbor(df, ts, point)
|
|
182
|
+
# Keep only categorical columns that exist in the result
|
|
183
|
+
existing_categorical = [
|
|
184
|
+
c for c in categorical_cols if c in df_categorical.columns
|
|
185
|
+
]
|
|
186
|
+
df_categorical = (
|
|
187
|
+
df_categorical[existing_categorical]
|
|
188
|
+
if existing_categorical
|
|
189
|
+
else pd.DataFrame()
|
|
190
|
+
)
|
|
191
|
+
else:
|
|
192
|
+
df_categorical = pd.DataFrame()
|
|
193
|
+
|
|
194
|
+
# Perform IDW interpolation for all parameters
|
|
195
|
+
idw_func = inverse_distance_weighting(power=power)
|
|
196
|
+
df_idw = idw_func(df, ts, point)
|
|
197
|
+
|
|
198
|
+
# Remove categorical columns from IDW result if they exist
|
|
199
|
+
if not df_categorical.empty and df_idw is not None:
|
|
200
|
+
# Drop categorical columns from IDW result
|
|
201
|
+
idw_cols_to_keep = [c for c in df_idw.columns if c not in categorical_cols]
|
|
202
|
+
df_idw = df_idw[idw_cols_to_keep] if idw_cols_to_keep else pd.DataFrame()
|
|
203
|
+
|
|
204
|
+
# Combine categorical (nearest) and non-categorical (IDW) results
|
|
205
|
+
if not df_categorical.empty and not df_idw.empty:
|
|
206
|
+
return pd.concat([df_idw, df_categorical], axis=1)
|
|
207
|
+
elif not df_categorical.empty:
|
|
208
|
+
return df_categorical
|
|
209
|
+
else:
|
|
210
|
+
return df_idw
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _merge_interpolation_results(
|
|
214
|
+
df_nearest: Optional[pd.DataFrame],
|
|
215
|
+
df_idw: Optional[pd.DataFrame],
|
|
216
|
+
use_nearest: bool,
|
|
217
|
+
) -> Optional[pd.DataFrame]:
|
|
218
|
+
"""
|
|
219
|
+
Merge nearest neighbor and IDW results with appropriate priority
|
|
220
|
+
"""
|
|
221
|
+
if use_nearest and df_nearest is not None and len(df_nearest) > 0:
|
|
222
|
+
if df_idw is not None:
|
|
223
|
+
# Combine nearest and IDW results, prioritizing nearest values
|
|
224
|
+
return df_nearest.combine_first(df_idw)
|
|
225
|
+
else:
|
|
226
|
+
return df_nearest
|
|
227
|
+
else:
|
|
228
|
+
return df_idw
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _postprocess_result(
|
|
232
|
+
result: pd.DataFrame, df: pd.DataFrame, ts: TimeSeries
|
|
233
|
+
) -> pd.DataFrame:
|
|
234
|
+
"""
|
|
235
|
+
Post-process the interpolation result: drop location columns, add sources, format, reshape
|
|
236
|
+
"""
|
|
237
|
+
# Drop location-related columns
|
|
238
|
+
result = result.drop(
|
|
239
|
+
[
|
|
240
|
+
"latitude",
|
|
241
|
+
"longitude",
|
|
242
|
+
"elevation",
|
|
243
|
+
"distance",
|
|
244
|
+
"effective_distance",
|
|
245
|
+
"elevation_diff",
|
|
246
|
+
],
|
|
247
|
+
axis=1,
|
|
248
|
+
errors="ignore",
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# Add source columns
|
|
252
|
+
result = _add_source_columns(result, df)
|
|
253
|
+
|
|
254
|
+
# Reshape by source
|
|
255
|
+
result = reshape_by_source(result)
|
|
256
|
+
|
|
257
|
+
# Add station index
|
|
258
|
+
result["station"] = "$0001"
|
|
259
|
+
result = result.set_index("station", append=True).reorder_levels(
|
|
260
|
+
["station", "time", "source"]
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# Reorder columns to match the canonical schema order
|
|
264
|
+
result = schema_service.purge(result, ts.parameters)
|
|
265
|
+
|
|
266
|
+
# Format the result using schema_service to apply proper rounding
|
|
267
|
+
result = schema_service.format(result, ts.granularity)
|
|
268
|
+
|
|
269
|
+
return result
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def interpolate(
|
|
273
|
+
ts: TimeSeries,
|
|
274
|
+
point: Point,
|
|
275
|
+
distance_threshold: Union[int, None] = 5000,
|
|
276
|
+
elevation_threshold: Union[int, None] = 50,
|
|
277
|
+
elevation_weight: float = 10,
|
|
278
|
+
power: float = 2.0,
|
|
279
|
+
lapse_rate: Union[float, None] = 6.5,
|
|
280
|
+
lapse_rate_threshold: int = 50,
|
|
281
|
+
) -> TimeSeries:
|
|
282
|
+
"""
|
|
283
|
+
Interpolate time series data spatially to a specific point.
|
|
284
|
+
|
|
285
|
+
Parameters
|
|
286
|
+
----------
|
|
287
|
+
ts : TimeSeries
|
|
288
|
+
The time series to interpolate.
|
|
289
|
+
point : Point
|
|
290
|
+
The point to interpolate the data for.
|
|
291
|
+
distance_threshold : int, optional
|
|
292
|
+
Maximum distance (in meters) to use nearest neighbor (default: 5000).
|
|
293
|
+
Beyond this, IDW is used.
|
|
294
|
+
elevation_threshold : int, optional
|
|
295
|
+
Maximum elevation difference (in meters) to use nearest neighbor (default: 50).
|
|
296
|
+
Beyond this, IDW is used even if distance is within threshold.
|
|
297
|
+
elevation_weight : float, optional
|
|
298
|
+
Weight for elevation difference in distance calculation (default: 0.1).
|
|
299
|
+
The effective distance is calculated as:
|
|
300
|
+
sqrt(horizontal_distance^2 + (elevation_diff * elevation_weight)^2)
|
|
301
|
+
power : float, optional
|
|
302
|
+
Power parameter for IDW (default: 2.0). Higher values give more
|
|
303
|
+
weight to closer stations.
|
|
304
|
+
lapse_rate : float, optional
|
|
305
|
+
Apply lapse rate correction based on elevation difference (default: 6.5).
|
|
306
|
+
lapse_rate_threshold : int, optional
|
|
307
|
+
Elevation difference threshold (in meters) to apply lapse rate correction
|
|
308
|
+
(default: 50). If the elevation difference between the point and stations
|
|
309
|
+
is less than this, no correction is applied.
|
|
310
|
+
|
|
311
|
+
Returns
|
|
312
|
+
-------
|
|
313
|
+
pd.DataFrame or None
|
|
314
|
+
A DataFrame containing the interpolated data for the specified point,
|
|
315
|
+
or None if no data is available.
|
|
316
|
+
"""
|
|
317
|
+
# Fetch DataFrame, filling missing values and adding location data
|
|
318
|
+
df = ts.fetch(fill=True, location=True, sources=True)
|
|
319
|
+
|
|
320
|
+
# If no data is returned, return None
|
|
321
|
+
if df is None:
|
|
322
|
+
logger.debug("No data available for interpolation. Returning empty TimeSeries.")
|
|
323
|
+
return _create_timeseries(ts, point)
|
|
324
|
+
|
|
325
|
+
# Prepare data with distance and elevation calculations
|
|
326
|
+
df = _prepare_data_with_distances(df, point, elevation_weight)
|
|
327
|
+
|
|
328
|
+
# Apply lapse rate if specified and elevation is available
|
|
329
|
+
if (
|
|
330
|
+
lapse_rate
|
|
331
|
+
and point.elevation
|
|
332
|
+
and df["elevation_diff"].max() >= lapse_rate_threshold
|
|
333
|
+
):
|
|
334
|
+
logger.debug("Applying lapse rate correction.")
|
|
335
|
+
df = apply_lapse_rate(df, point.elevation, lapse_rate)
|
|
336
|
+
|
|
337
|
+
# Determine if nearest neighbor should be used
|
|
338
|
+
use_nearest = _should_use_nearest_neighbor(
|
|
339
|
+
df, point, distance_threshold, elevation_threshold
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# Identify categorical columns
|
|
343
|
+
categorical_cols = _get_categorical_columns(df)
|
|
344
|
+
logger.debug(f"Categorical columns identified: {categorical_cols}")
|
|
345
|
+
|
|
346
|
+
# Perform interpolation
|
|
347
|
+
df_nearest = None
|
|
348
|
+
df_idw = None
|
|
349
|
+
|
|
350
|
+
if use_nearest:
|
|
351
|
+
logger.debug("Using nearest neighbor interpolation.")
|
|
352
|
+
df_nearest = _interpolate_with_nearest_neighbor(
|
|
353
|
+
df, ts, point, distance_threshold, elevation_threshold
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
# Use IDW if nearest neighbor doesn't provide complete data
|
|
357
|
+
if (
|
|
358
|
+
not use_nearest
|
|
359
|
+
or df_nearest is None
|
|
360
|
+
or len(df_nearest) == 0
|
|
361
|
+
or df_nearest.isna().any().any()
|
|
362
|
+
):
|
|
363
|
+
logger.debug("Using IDW interpolation.")
|
|
364
|
+
df_idw = _interpolate_with_idw_and_categorical(
|
|
365
|
+
df, ts, point, categorical_cols, power
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
# Merge results
|
|
369
|
+
result = _merge_interpolation_results(df_nearest, df_idw, use_nearest)
|
|
370
|
+
|
|
371
|
+
# If no data is returned, return None
|
|
372
|
+
if result is None or result.empty:
|
|
373
|
+
return _create_timeseries(ts, point)
|
|
374
|
+
|
|
375
|
+
# Post-process result
|
|
376
|
+
result = _postprocess_result(result, df, ts)
|
|
377
|
+
|
|
378
|
+
return _create_timeseries(ts, point, result)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Inventory Module
|
|
3
|
+
|
|
4
|
+
Provides classes for working with weather station data inventories.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import date, datetime
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
from meteostat.enumerations import Parameter
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Inventory:
|
|
16
|
+
"""
|
|
17
|
+
A weather station's data inventory
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
df: Optional[pd.DataFrame] = None
|
|
21
|
+
|
|
22
|
+
def __init__(self, df: Optional[pd.DataFrame] = None):
|
|
23
|
+
if df is not None and not df.empty:
|
|
24
|
+
self.df = df
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def start(self) -> Optional[date]:
|
|
28
|
+
"""
|
|
29
|
+
Get the earliest start date from the inventory
|
|
30
|
+
"""
|
|
31
|
+
return (
|
|
32
|
+
datetime.strptime(self.df["start"].min(), "%Y-%m-%d").date()
|
|
33
|
+
if self.df is not None
|
|
34
|
+
else None
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def end(self) -> Optional[date]:
|
|
39
|
+
"""
|
|
40
|
+
Get the latest end date from the inventory
|
|
41
|
+
"""
|
|
42
|
+
return (
|
|
43
|
+
datetime.strptime(self.df["end"].max(), "%Y-%m-%d").date()
|
|
44
|
+
if self.df is not None
|
|
45
|
+
else None
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def parameters(self) -> Optional[List[Parameter]]:
|
|
50
|
+
"""
|
|
51
|
+
Get the list of available parameters from the inventory
|
|
52
|
+
"""
|
|
53
|
+
if self.df is None:
|
|
54
|
+
return []
|
|
55
|
+
|
|
56
|
+
return [
|
|
57
|
+
Parameter[parameter.upper()]
|
|
58
|
+
for parameter in self.df.index.get_level_values("parameter").unique()
|
|
59
|
+
]
|
meteostat/api/merge.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Concatenation Module
|
|
3
|
+
|
|
4
|
+
Provides functions to concatenate multiple time series objects into one.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from copy import copy
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import List, Optional
|
|
10
|
+
|
|
11
|
+
import pandas as pd
|
|
12
|
+
|
|
13
|
+
from meteostat.core.data import data_service
|
|
14
|
+
from meteostat.core.schema import schema_service
|
|
15
|
+
from meteostat.api.timeseries import TimeSeries
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _get_dt(
|
|
19
|
+
dt_a: Optional[datetime], dt_b: Optional[datetime], start=True
|
|
20
|
+
) -> Optional[datetime]:
|
|
21
|
+
"""
|
|
22
|
+
Return the earlier or later (depending on "start" argument) of two datetimes,
|
|
23
|
+
considering None as 'no value'.
|
|
24
|
+
|
|
25
|
+
If both are None, return None.
|
|
26
|
+
"""
|
|
27
|
+
if dt_a is None:
|
|
28
|
+
return dt_b
|
|
29
|
+
if dt_b is None:
|
|
30
|
+
return dt_a
|
|
31
|
+
return min(dt_a, dt_b) if start else max(dt_a, dt_b)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def merge(objs: List[TimeSeries]) -> TimeSeries:
|
|
35
|
+
"""
|
|
36
|
+
Merge one or multiple Meteostat time series into a common one
|
|
37
|
+
|
|
38
|
+
In case of duplicate index, the last row will be prefered.
|
|
39
|
+
Hence, please pass newest data last.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
objs : List[TimeSeries]
|
|
44
|
+
List of time series objects to concatenate
|
|
45
|
+
|
|
46
|
+
Returns
|
|
47
|
+
-------
|
|
48
|
+
TimeSeries
|
|
49
|
+
Concatenated time series object
|
|
50
|
+
|
|
51
|
+
Raises
|
|
52
|
+
------
|
|
53
|
+
ValueError
|
|
54
|
+
If the time series objects have divergent granularity or time zone
|
|
55
|
+
"""
|
|
56
|
+
ts = objs[0]
|
|
57
|
+
|
|
58
|
+
if not all(
|
|
59
|
+
obj.granularity == ts.granularity and obj.timezone == ts.timezone
|
|
60
|
+
for obj in objs[1:]
|
|
61
|
+
):
|
|
62
|
+
raise ValueError(
|
|
63
|
+
"Can't concatenate time series objects with divergent granularity or time zone"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
stations = copy(ts.stations)
|
|
67
|
+
start = copy(ts.start)
|
|
68
|
+
end = copy(ts.end)
|
|
69
|
+
parameters = ts.parameters
|
|
70
|
+
multi_station = ts._multi_station
|
|
71
|
+
|
|
72
|
+
for obj in objs[1:]:
|
|
73
|
+
stations = (
|
|
74
|
+
pd.concat([stations, obj.stations])
|
|
75
|
+
.reset_index()
|
|
76
|
+
.drop_duplicates(subset=["id"])
|
|
77
|
+
.set_index("id")
|
|
78
|
+
)
|
|
79
|
+
start = _get_dt(start, obj.start)
|
|
80
|
+
end = _get_dt(end, obj.end, False)
|
|
81
|
+
parameters.extend(obj.parameters)
|
|
82
|
+
if (
|
|
83
|
+
obj._multi_station
|
|
84
|
+
or stations.index.get_level_values("id")[0]
|
|
85
|
+
!= obj.stations.index.get_level_values("id")[0]
|
|
86
|
+
):
|
|
87
|
+
multi_station = True
|
|
88
|
+
|
|
89
|
+
df = data_service.concat_fragments(
|
|
90
|
+
[obj._df for obj in objs if obj._df is not None],
|
|
91
|
+
list(dict.fromkeys(parameters)),
|
|
92
|
+
)
|
|
93
|
+
df = schema_service.format(df, ts.granularity)
|
|
94
|
+
|
|
95
|
+
return TimeSeries(
|
|
96
|
+
ts.granularity,
|
|
97
|
+
stations,
|
|
98
|
+
df,
|
|
99
|
+
start,
|
|
100
|
+
end,
|
|
101
|
+
ts.timezone,
|
|
102
|
+
multi_station=multi_station,
|
|
103
|
+
)
|
meteostat/api/monthly.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Monthly Time Series Data
|
|
3
|
+
|
|
4
|
+
Access monthly time series data for one or multiple weather stations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
from datetime import datetime, date
|
|
9
|
+
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
from meteostat.core.data import data_service
|
|
13
|
+
from meteostat.enumerations import Parameter, Provider, Granularity
|
|
14
|
+
from meteostat.typing import Station, Request
|
|
15
|
+
from meteostat.api.point import Point
|
|
16
|
+
from meteostat.utils.parsers import parse_station, parse_time
|
|
17
|
+
|
|
18
|
+
DEFAULT_PARAMETERS = [
|
|
19
|
+
Parameter.TEMP,
|
|
20
|
+
Parameter.TMIN,
|
|
21
|
+
Parameter.TMAX,
|
|
22
|
+
Parameter.TXMN,
|
|
23
|
+
Parameter.TXMX,
|
|
24
|
+
Parameter.PRCP,
|
|
25
|
+
Parameter.PRES,
|
|
26
|
+
Parameter.TSUN,
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def monthly(
|
|
31
|
+
station: str | Station | Point | List[str | Station | Point] | pd.DataFrame,
|
|
32
|
+
start: Optional[datetime | date],
|
|
33
|
+
end: Optional[datetime | date],
|
|
34
|
+
parameters: Optional[List[Parameter]] = None,
|
|
35
|
+
providers: Optional[List[Provider]] = None,
|
|
36
|
+
):
|
|
37
|
+
"""
|
|
38
|
+
Access monthly time series data.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
station : str, Station, Point, List[str | Station | Point], pd.Index, pd.Series
|
|
43
|
+
Weather station(s) or Point(s) to query data for. Can be a single station/point or a list.
|
|
44
|
+
Points are converted to virtual stations with IDs like $0001, $0002, etc.
|
|
45
|
+
start : datetime, date, optional
|
|
46
|
+
Start date for the data query. If None, the earliest available date will be used.
|
|
47
|
+
end : datetime, date, optional
|
|
48
|
+
End date for the data query. If None, the latest available date will be used.
|
|
49
|
+
parameters : List[Parameter], optional
|
|
50
|
+
List of parameters to include in the data query. Defaults to a set of common parameters.
|
|
51
|
+
providers : List[Provider], optional
|
|
52
|
+
List of data providers to use for the query. Defaults to the monthly provider.
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
TimeSeries
|
|
57
|
+
A TimeSeries object containing the monthly data for the specified stations and parameters.
|
|
58
|
+
"""
|
|
59
|
+
if parameters is None:
|
|
60
|
+
parameters = DEFAULT_PARAMETERS
|
|
61
|
+
if providers is None:
|
|
62
|
+
providers = [Provider.MONTHLY]
|
|
63
|
+
|
|
64
|
+
req = Request(
|
|
65
|
+
granularity=Granularity.MONTHLY,
|
|
66
|
+
providers=providers,
|
|
67
|
+
parameters=parameters,
|
|
68
|
+
station=parse_station(station),
|
|
69
|
+
start=parse_time(start),
|
|
70
|
+
end=parse_time(end, is_end=True),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return data_service.fetch(req)
|