meteostat 2.0.0__tar.gz → 2.0.1__tar.gz
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-2.0.0 → meteostat-2.0.1}/PKG-INFO +14 -18
- {meteostat-2.0.0 → meteostat-2.0.1}/README.md +13 -16
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/__init__.py +10 -4
- {meteostat-2.0.0/meteostat/core → meteostat-2.0.1/meteostat/api}/config.py +5 -5
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/api/interpolate.py +217 -79
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/api/stations.py +1 -1
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/core/cache.py +1 -1
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/core/data.py +4 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/core/network.py +1 -1
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/interpolation/lapserate.py +1 -1
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/providers/dwd/climat.py +1 -1
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/providers/dwd/daily.py +1 -1
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/providers/dwd/hourly.py +1 -1
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/providers/dwd/shared.py +1 -1
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/providers/meteostat/daily.py +1 -1
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/providers/meteostat/hourly.py +1 -1
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/providers/meteostat/monthly.py +1 -1
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/providers/meteostat/shared.py +1 -1
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/providers/metno/forecast.py +1 -1
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/providers/noaa/metar.py +1 -1
- meteostat-2.0.1/meteostat/utils/guards.py +51 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/utils/parsers.py +0 -7
- {meteostat-2.0.0 → meteostat-2.0.1}/pyproject.toml +1 -1
- {meteostat-2.0.0 → meteostat-2.0.1}/LICENSE +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/api/__init__.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/api/daily.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/api/hourly.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/api/inventory.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/api/merge.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/api/monthly.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/api/normals.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/api/point.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/api/timeseries.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/core/logger.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/core/parameters.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/core/providers.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/core/schema.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/core/validator.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/enumerations.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/interpolation/__init__.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/interpolation/idw.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/interpolation/nearest.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/parameters.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/providers/__init__.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/providers/dwd/monthly.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/providers/dwd/mosmix.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/providers/dwd/poi.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/providers/eccc/daily.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/providers/eccc/hourly.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/providers/eccc/monthly.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/providers/eccc/shared.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/providers/index.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/providers/meteostat/daily_derived.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/providers/meteostat/monthly_derived.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/providers/noaa/ghcnd.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/providers/noaa/isd_lite.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/py.typed +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/typing.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/utils/__init__.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/utils/conversions.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/utils/data.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/utils/geo.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/utils/types.py +0 -0
- {meteostat-2.0.0 → meteostat-2.0.1}/meteostat/utils/validators.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meteostat
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.1
|
|
4
4
|
Summary: Access and analyze historical weather and climate data with Python.
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Author: Meteostat
|
|
@@ -18,12 +18,11 @@ Description-Content-Type: text/markdown
|
|
|
18
18
|
<!-- PROJECT SHIELDS -->
|
|
19
19
|
<div align="center">
|
|
20
20
|
|
|
21
|
-
[![
|
|
22
|
-
[![
|
|
23
|
-
[![Stargazers][stars-shield]][stars-url]
|
|
21
|
+
[![Downloads][downloads-shield]][downloads-url]
|
|
22
|
+
[![Provider Tests][provider-tests-shield]][provider-tests-url]
|
|
24
23
|
[![Issues][issues-shield]][issues-url]
|
|
25
|
-
[![
|
|
26
|
-
[![
|
|
24
|
+
[![MIT License][license-shield]][license-url]
|
|
25
|
+
[![Stargazers][stars-shield]][stars-url]
|
|
27
26
|
|
|
28
27
|
</div>
|
|
29
28
|
|
|
@@ -118,17 +117,14 @@ Meteostat is licensed under the [**MIT License**](https://github.com/meteostat/m
|
|
|
118
117
|
|
|
119
118
|
<!-- MARKDOWN LINKS & IMAGES -->
|
|
120
119
|
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
|
|
121
|
-
[
|
|
122
|
-
[
|
|
123
|
-
[
|
|
124
|
-
[
|
|
125
|
-
[
|
|
126
|
-
[stars-url]: https://github.com/meteostat/meteostat/stargazers
|
|
127
|
-
[issues-shield]: https://img.shields.io/github/issues/meteostat/meteostat.svg?style=for-the-badge
|
|
120
|
+
[downloads-shield]: https://img.shields.io/pypi/dm/meteostat
|
|
121
|
+
[downloads-url]: https://pypi.org/project/meteostat/
|
|
122
|
+
[provider-tests-shield]: https://github.com/meteostat/meteostat/actions/workflows/provider-tests.yml/badge.svg
|
|
123
|
+
[provider-tests-url]: https://github.com/meteostat/meteostat/actions/workflows/provider-tests.yml
|
|
124
|
+
[issues-shield]: https://img.shields.io/github/issues/meteostat/meteostat.svg
|
|
128
125
|
[issues-url]: https://github.com/meteostat/meteostat/issues
|
|
129
|
-
[license-shield]: https://img.shields.io/github/license/meteostat/meteostat.svg
|
|
130
|
-
[license-url]: https://github.com/meteostat/meteostat
|
|
131
|
-
[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555
|
|
132
|
-
[linkedin-url]: https://www.linkedin.com/company/meteostat
|
|
126
|
+
[license-shield]: https://img.shields.io/github/license/meteostat/meteostat.svg
|
|
127
|
+
[license-url]: https://github.com/meteostat/meteostat?tab=readme-ov-file#-license
|
|
133
128
|
[product-screenshot]: https://dev.meteostat.net/assets/images/example-8b6cf2a3fe2efa285bc72d7dc72c4865.png
|
|
134
|
-
|
|
129
|
+
[stars-shield]: https://img.shields.io/github/stars/meteostat/meteostat.svg
|
|
130
|
+
[stars-url]: https://github.com/meteostat/meteostat/stargazers
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
<!-- PROJECT SHIELDS -->
|
|
2
2
|
<div align="center">
|
|
3
3
|
|
|
4
|
-
[![
|
|
5
|
-
[![
|
|
6
|
-
[![Stargazers][stars-shield]][stars-url]
|
|
4
|
+
[![Downloads][downloads-shield]][downloads-url]
|
|
5
|
+
[![Provider Tests][provider-tests-shield]][provider-tests-url]
|
|
7
6
|
[![Issues][issues-shield]][issues-url]
|
|
8
|
-
[![
|
|
9
|
-
[![
|
|
7
|
+
[![MIT License][license-shield]][license-url]
|
|
8
|
+
[![Stargazers][stars-shield]][stars-url]
|
|
10
9
|
|
|
11
10
|
</div>
|
|
12
11
|
|
|
@@ -101,16 +100,14 @@ Meteostat is licensed under the [**MIT License**](https://github.com/meteostat/m
|
|
|
101
100
|
|
|
102
101
|
<!-- MARKDOWN LINKS & IMAGES -->
|
|
103
102
|
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
|
|
104
|
-
[
|
|
105
|
-
[
|
|
106
|
-
[
|
|
107
|
-
[
|
|
108
|
-
[
|
|
109
|
-
[stars-url]: https://github.com/meteostat/meteostat/stargazers
|
|
110
|
-
[issues-shield]: https://img.shields.io/github/issues/meteostat/meteostat.svg?style=for-the-badge
|
|
103
|
+
[downloads-shield]: https://img.shields.io/pypi/dm/meteostat
|
|
104
|
+
[downloads-url]: https://pypi.org/project/meteostat/
|
|
105
|
+
[provider-tests-shield]: https://github.com/meteostat/meteostat/actions/workflows/provider-tests.yml/badge.svg
|
|
106
|
+
[provider-tests-url]: https://github.com/meteostat/meteostat/actions/workflows/provider-tests.yml
|
|
107
|
+
[issues-shield]: https://img.shields.io/github/issues/meteostat/meteostat.svg
|
|
111
108
|
[issues-url]: https://github.com/meteostat/meteostat/issues
|
|
112
|
-
[license-shield]: https://img.shields.io/github/license/meteostat/meteostat.svg
|
|
113
|
-
[license-url]: https://github.com/meteostat/meteostat
|
|
114
|
-
[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555
|
|
115
|
-
[linkedin-url]: https://www.linkedin.com/company/meteostat
|
|
109
|
+
[license-shield]: https://img.shields.io/github/license/meteostat/meteostat.svg
|
|
110
|
+
[license-url]: https://github.com/meteostat/meteostat?tab=readme-ov-file#-license
|
|
116
111
|
[product-screenshot]: https://dev.meteostat.net/assets/images/example-8b6cf2a3fe2efa285bc72d7dc72c4865.png
|
|
112
|
+
[stars-shield]: https://img.shields.io/github/stars/meteostat/meteostat.svg
|
|
113
|
+
[stars-url]: https://github.com/meteostat/meteostat/stargazers
|
|
@@ -12,29 +12,34 @@ The code is licensed under the MIT license.
|
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
14
|
__appname__ = "meteostat"
|
|
15
|
-
__version__ = "2.0.
|
|
15
|
+
__version__ = "2.0.1"
|
|
16
16
|
|
|
17
17
|
from meteostat.api.daily import daily
|
|
18
18
|
from meteostat.api.hourly import hourly
|
|
19
19
|
from meteostat.api.interpolate import interpolate
|
|
20
|
+
from meteostat.api.inventory import Inventory
|
|
20
21
|
from meteostat.api.merge import merge
|
|
21
22
|
from meteostat.api.monthly import monthly
|
|
22
23
|
from meteostat.api.normals import normals
|
|
23
24
|
from meteostat.api.point import Point
|
|
24
25
|
from meteostat.api.stations import stations
|
|
26
|
+
from meteostat.api.timeseries import TimeSeries
|
|
25
27
|
from meteostat.core.cache import purge
|
|
26
|
-
from meteostat.
|
|
27
|
-
from meteostat.enumerations import Parameter, Provider, UnitSystem
|
|
28
|
+
from meteostat.api.config import config
|
|
29
|
+
from meteostat.enumerations import Granularity, Parameter, Provider, UnitSystem
|
|
28
30
|
from meteostat.interpolation.lapserate import lapse_rate
|
|
29
|
-
from meteostat.typing import Station
|
|
31
|
+
from meteostat.typing import Station, License
|
|
30
32
|
|
|
31
33
|
# Export public API
|
|
32
34
|
__all__ = [
|
|
33
35
|
"config",
|
|
34
36
|
"daily",
|
|
35
37
|
"hourly",
|
|
38
|
+
"Granularity",
|
|
36
39
|
"interpolate",
|
|
40
|
+
"Inventory",
|
|
37
41
|
"lapse_rate",
|
|
42
|
+
"License",
|
|
38
43
|
"merge",
|
|
39
44
|
"monthly",
|
|
40
45
|
"normals",
|
|
@@ -44,5 +49,6 @@ __all__ = [
|
|
|
44
49
|
"purge",
|
|
45
50
|
"Station",
|
|
46
51
|
"stations",
|
|
52
|
+
"TimeSeries",
|
|
47
53
|
"UnitSystem",
|
|
48
54
|
]
|
|
@@ -15,9 +15,9 @@ from meteostat.enumerations import TTL, Parameter
|
|
|
15
15
|
from meteostat.utils.types import extract_property_type, validate_parsed_value
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
class
|
|
18
|
+
class ConfigService:
|
|
19
19
|
"""
|
|
20
|
-
Configuration
|
|
20
|
+
Configuration Service for Meteostat
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
23
|
prefix: str
|
|
@@ -92,9 +92,9 @@ class Config:
|
|
|
92
92
|
self._set_env_value(key, value)
|
|
93
93
|
|
|
94
94
|
|
|
95
|
-
class ConfigService
|
|
95
|
+
class Config(ConfigService):
|
|
96
96
|
"""
|
|
97
|
-
Configuration
|
|
97
|
+
Meteostat Configuration
|
|
98
98
|
|
|
99
99
|
Manages all configuration settings including cache, network, stations,
|
|
100
100
|
interpolation, and provider-specific settings. Supports loading configuration
|
|
@@ -155,4 +155,4 @@ class ConfigService(Config):
|
|
|
155
155
|
metno_user_agent: Optional[str] = None
|
|
156
156
|
|
|
157
157
|
|
|
158
|
-
config =
|
|
158
|
+
config = Config("MS")
|
|
@@ -12,12 +12,19 @@ import pandas as pd
|
|
|
12
12
|
from meteostat.api.point import Point
|
|
13
13
|
from meteostat.api.timeseries import TimeSeries
|
|
14
14
|
from meteostat.typing import Station
|
|
15
|
+
from meteostat.enumerations import Parameter
|
|
15
16
|
from meteostat.interpolation.lapserate import apply_lapse_rate
|
|
16
17
|
from meteostat.interpolation.nearest import nearest_neighbor
|
|
17
18
|
from meteostat.interpolation.idw import inverse_distance_weighting
|
|
18
19
|
from meteostat.utils.data import aggregate_sources, reshape_by_source, stations_to_df
|
|
19
20
|
from meteostat.utils.geo import get_distance
|
|
20
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}
|
|
21
28
|
|
|
22
29
|
|
|
23
30
|
def _create_timeseries(
|
|
@@ -77,6 +84,191 @@ def _add_source_columns(
|
|
|
77
84
|
return result
|
|
78
85
|
|
|
79
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
|
+
|
|
80
272
|
def interpolate(
|
|
81
273
|
ts: TimeSeries,
|
|
82
274
|
point: Point,
|
|
@@ -127,27 +319,11 @@ def interpolate(
|
|
|
127
319
|
|
|
128
320
|
# If no data is returned, return None
|
|
129
321
|
if df is None:
|
|
322
|
+
logger.debug("No data available for interpolation. Returning empty TimeSeries.")
|
|
130
323
|
return _create_timeseries(ts, point)
|
|
131
324
|
|
|
132
|
-
#
|
|
133
|
-
df
|
|
134
|
-
point.latitude, point.longitude, df["latitude"], df["longitude"]
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
# Add effective distance column if elevation is available
|
|
138
|
-
if point.elevation is not None and "elevation" in df.columns:
|
|
139
|
-
elev_diff = np.abs(df["elevation"] - point.elevation)
|
|
140
|
-
df["effective_distance"] = np.sqrt(
|
|
141
|
-
df["distance"] ** 2 + (elev_diff * elevation_weight) ** 2
|
|
142
|
-
)
|
|
143
|
-
else:
|
|
144
|
-
df["effective_distance"] = df["distance"]
|
|
145
|
-
|
|
146
|
-
# Add elevation difference column
|
|
147
|
-
if "elevation" in df.columns and point.elevation is not None:
|
|
148
|
-
df["elevation_diff"] = np.abs(df["elevation"] - point.elevation)
|
|
149
|
-
else:
|
|
150
|
-
df["elevation_diff"] = np.nan
|
|
325
|
+
# Prepare data with distance and elevation calculations
|
|
326
|
+
df = _prepare_data_with_distances(df, point, elevation_weight)
|
|
151
327
|
|
|
152
328
|
# Apply lapse rate if specified and elevation is available
|
|
153
329
|
if (
|
|
@@ -155,86 +331,48 @@ def interpolate(
|
|
|
155
331
|
and point.elevation
|
|
156
332
|
and df["elevation_diff"].max() >= lapse_rate_threshold
|
|
157
333
|
):
|
|
334
|
+
logger.debug("Applying lapse rate correction.")
|
|
158
335
|
df = apply_lapse_rate(df, point.elevation, lapse_rate)
|
|
159
336
|
|
|
160
|
-
#
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
# Calculate minimum elevation difference
|
|
165
|
-
min_elev_diff = np.abs(df["elevation"] - point.elevation).min()
|
|
166
|
-
use_nearest = (
|
|
167
|
-
elevation_threshold is None or min_elev_diff <= elevation_threshold
|
|
168
|
-
)
|
|
337
|
+
# Determine if nearest neighbor should be used
|
|
338
|
+
use_nearest = _should_use_nearest_neighbor(
|
|
339
|
+
df, point, distance_threshold, elevation_threshold
|
|
340
|
+
)
|
|
169
341
|
|
|
170
|
-
#
|
|
342
|
+
# Identify categorical columns
|
|
343
|
+
categorical_cols = _get_categorical_columns(df)
|
|
344
|
+
logger.debug(f"Categorical columns identified: {categorical_cols}")
|
|
345
|
+
|
|
346
|
+
# Perform interpolation
|
|
171
347
|
df_nearest = None
|
|
172
348
|
df_idw = None
|
|
173
349
|
|
|
174
|
-
# Perform nearest neighbor if applicable
|
|
175
350
|
if use_nearest:
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if distance_threshold is None
|
|
180
|
-
else (df["distance"] <= distance_threshold)
|
|
181
|
-
)
|
|
182
|
-
elevation_filter = (
|
|
183
|
-
pd.Series([True] * len(df), index=df.index)
|
|
184
|
-
if elevation_threshold is None
|
|
185
|
-
else (np.abs(df["elevation"] - point.elevation) <= elevation_threshold)
|
|
351
|
+
logger.debug("Using nearest neighbor interpolation.")
|
|
352
|
+
df_nearest = _interpolate_with_nearest_neighbor(
|
|
353
|
+
df, ts, point, distance_threshold, elevation_threshold
|
|
186
354
|
)
|
|
187
|
-
df_filtered = df[distance_filter & elevation_filter]
|
|
188
|
-
df_nearest = nearest_neighbor(df_filtered, ts, point)
|
|
189
355
|
|
|
190
|
-
#
|
|
356
|
+
# Use IDW if nearest neighbor doesn't provide complete data
|
|
191
357
|
if (
|
|
192
358
|
not use_nearest
|
|
193
359
|
or df_nearest is None
|
|
194
360
|
or len(df_nearest) == 0
|
|
195
361
|
or df_nearest.isna().any().any()
|
|
196
362
|
):
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
363
|
+
logger.debug("Using IDW interpolation.")
|
|
364
|
+
df_idw = _interpolate_with_idw_and_categorical(
|
|
365
|
+
df, ts, point, categorical_cols, power
|
|
366
|
+
)
|
|
200
367
|
|
|
201
|
-
# Merge
|
|
202
|
-
|
|
203
|
-
if df_idw is not None:
|
|
204
|
-
# Combine nearest and IDW results, prioritizing nearest values
|
|
205
|
-
result = df_nearest.combine_first(df_idw)
|
|
206
|
-
else:
|
|
207
|
-
result = df_nearest
|
|
208
|
-
else:
|
|
209
|
-
result = df_idw
|
|
368
|
+
# Merge results
|
|
369
|
+
result = _merge_interpolation_results(df_nearest, df_idw, use_nearest)
|
|
210
370
|
|
|
211
371
|
# If no data is returned, return None
|
|
212
372
|
if result is None or result.empty:
|
|
213
373
|
return _create_timeseries(ts, point)
|
|
214
374
|
|
|
215
|
-
#
|
|
216
|
-
result = result
|
|
217
|
-
[
|
|
218
|
-
"latitude",
|
|
219
|
-
"longitude",
|
|
220
|
-
"elevation",
|
|
221
|
-
"distance",
|
|
222
|
-
"effective_distance",
|
|
223
|
-
"elevation_diff",
|
|
224
|
-
],
|
|
225
|
-
axis=1,
|
|
226
|
-
)
|
|
227
|
-
|
|
228
|
-
# Add source columns: aggregate all columns that end with "_source"
|
|
229
|
-
result = _add_source_columns(result, df)
|
|
230
|
-
|
|
231
|
-
# Reshape by source
|
|
232
|
-
result = reshape_by_source(result)
|
|
233
|
-
|
|
234
|
-
# Add station index
|
|
235
|
-
result["station"] = "$0001"
|
|
236
|
-
result = result.set_index("station", append=True).reorder_levels(
|
|
237
|
-
["station", "time", "source"]
|
|
238
|
-
)
|
|
375
|
+
# Post-process result
|
|
376
|
+
result = _postprocess_result(result, df, ts)
|
|
239
377
|
|
|
240
378
|
return _create_timeseries(ts, point, result)
|
|
@@ -15,7 +15,7 @@ from requests import Response
|
|
|
15
15
|
from meteostat.api.inventory import Inventory
|
|
16
16
|
from meteostat.api.point import Point
|
|
17
17
|
from meteostat.core.cache import cache_service
|
|
18
|
-
from meteostat.
|
|
18
|
+
from meteostat.api.config import config
|
|
19
19
|
from meteostat.core.logger import logger
|
|
20
20
|
from meteostat.core.network import network_service
|
|
21
21
|
from meteostat.enumerations import Provider
|
|
@@ -18,6 +18,7 @@ from meteostat.core.schema import schema_service
|
|
|
18
18
|
from meteostat.enumerations import Parameter, Provider
|
|
19
19
|
from meteostat.typing import Station, Request
|
|
20
20
|
from meteostat.utils.data import stations_to_df
|
|
21
|
+
from meteostat.utils.guards import request_size_guard
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
class DataService:
|
|
@@ -147,6 +148,9 @@ class DataService:
|
|
|
147
148
|
"""
|
|
148
149
|
Load meteorological time series data from different providers
|
|
149
150
|
"""
|
|
151
|
+
# Guard request
|
|
152
|
+
request_size_guard(req)
|
|
153
|
+
|
|
150
154
|
# Convert stations to list if single Station
|
|
151
155
|
stations: List[Station] = (
|
|
152
156
|
cast(List[Station], req.station)
|
|
@@ -10,7 +10,7 @@ from typing import List, Optional
|
|
|
10
10
|
import pandas as pd
|
|
11
11
|
|
|
12
12
|
from meteostat.core.logger import logger
|
|
13
|
-
from meteostat.
|
|
13
|
+
from meteostat.api.config import config
|
|
14
14
|
from meteostat.enumerations import TTL, Parameter
|
|
15
15
|
from meteostat.typing import ProviderRequest, Station
|
|
16
16
|
from meteostat.core.cache import cache_service
|
|
@@ -14,7 +14,7 @@ from zipfile import ZipFile
|
|
|
14
14
|
|
|
15
15
|
import pandas as pd
|
|
16
16
|
|
|
17
|
-
from meteostat.
|
|
17
|
+
from meteostat.api.config import config
|
|
18
18
|
from meteostat.enumerations import TTL, Parameter
|
|
19
19
|
from meteostat.typing import ProviderRequest
|
|
20
20
|
from meteostat.core.cache import cache_service
|
|
@@ -18,7 +18,7 @@ from meteostat.enumerations import TTL, Parameter
|
|
|
18
18
|
from meteostat.core.logger import logger
|
|
19
19
|
from meteostat.typing import ProviderRequest, Station
|
|
20
20
|
from meteostat.core.cache import cache_service
|
|
21
|
-
from meteostat.
|
|
21
|
+
from meteostat.api.config import config
|
|
22
22
|
from meteostat.utils.conversions import ms_to_kmh
|
|
23
23
|
from meteostat.providers.dwd.shared import get_condicode
|
|
24
24
|
from meteostat.providers.dwd.shared import get_ftp_connection
|
|
@@ -10,7 +10,7 @@ import pandas as pd
|
|
|
10
10
|
from meteostat.providers.meteostat.shared import filter_model_data, handle_exceptions
|
|
11
11
|
from meteostat.typing import ProviderRequest
|
|
12
12
|
from meteostat.core.cache import cache_service
|
|
13
|
-
from meteostat.
|
|
13
|
+
from meteostat.api.config import config
|
|
14
14
|
from meteostat.utils.data import reshape_by_source
|
|
15
15
|
|
|
16
16
|
ENDPOINT = config.daily_endpoint
|
|
@@ -9,7 +9,7 @@ import pandas as pd
|
|
|
9
9
|
|
|
10
10
|
from meteostat.providers.meteostat.shared import filter_model_data, handle_exceptions
|
|
11
11
|
from meteostat.typing import ProviderRequest
|
|
12
|
-
from meteostat.
|
|
12
|
+
from meteostat.api.config import config
|
|
13
13
|
from meteostat.core.cache import cache_service
|
|
14
14
|
from meteostat.utils.data import reshape_by_source
|
|
15
15
|
|
|
@@ -6,7 +6,7 @@ from typing import Optional
|
|
|
6
6
|
|
|
7
7
|
import pandas as pd
|
|
8
8
|
|
|
9
|
-
from meteostat.
|
|
9
|
+
from meteostat.api.config import config
|
|
10
10
|
from meteostat.enumerations import TTL
|
|
11
11
|
from meteostat.providers.meteostat.shared import filter_model_data, handle_exceptions
|
|
12
12
|
from meteostat.typing import ProviderRequest
|
|
@@ -4,7 +4,7 @@ from typing import Optional, Callable, TypeVar
|
|
|
4
4
|
|
|
5
5
|
import pandas as pd
|
|
6
6
|
|
|
7
|
-
from meteostat.
|
|
7
|
+
from meteostat.api.config import config
|
|
8
8
|
from meteostat.core.logger import logger
|
|
9
9
|
from meteostat.core.providers import provider_service
|
|
10
10
|
from meteostat.enumerations import Grade
|
|
@@ -3,7 +3,7 @@ from urllib.error import HTTPError
|
|
|
3
3
|
|
|
4
4
|
import pandas as pd
|
|
5
5
|
|
|
6
|
-
from meteostat.
|
|
6
|
+
from meteostat.api.config import config
|
|
7
7
|
from meteostat.enumerations import TTL, Parameter
|
|
8
8
|
from meteostat.core.logger import logger
|
|
9
9
|
from meteostat.core.network import network_service
|
|
@@ -8,7 +8,7 @@ import pandas as pd
|
|
|
8
8
|
from metar import Metar
|
|
9
9
|
|
|
10
10
|
from meteostat.core.logger import logger
|
|
11
|
-
from meteostat.
|
|
11
|
+
from meteostat.api.config import config
|
|
12
12
|
from meteostat.enumerations import TTL, Frequency, Parameter
|
|
13
13
|
from meteostat.typing import ProviderRequest
|
|
14
14
|
from meteostat.utils.conversions import temp_dwpt_to_rhum
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Guard functions for Meteostat.
|
|
3
|
+
|
|
4
|
+
The code is licensed under the MIT license.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from meteostat.api.config import config
|
|
9
|
+
from meteostat.core.logger import logger
|
|
10
|
+
from meteostat.enumerations import Granularity
|
|
11
|
+
from meteostat.typing import Request
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def request_size_guard(req: Request) -> None:
|
|
15
|
+
"""
|
|
16
|
+
Guard to block large requests
|
|
17
|
+
"""
|
|
18
|
+
if not config.block_large_requests:
|
|
19
|
+
logger.debug("Large request blocking is disabled.")
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
if isinstance(req.station, list) and len(req.station) > 10:
|
|
23
|
+
raise ValueError(
|
|
24
|
+
"Requests with more than 10 stations are blocked by default. "
|
|
25
|
+
"To enable large requests, set `config.block_large_requests = False`."
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
if req.granularity not in [Granularity.HOURLY, Granularity.DAILY]:
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
if req.start is None:
|
|
32
|
+
raise ValueError(
|
|
33
|
+
"Hourly and daily requests without a start date are blocked by default. "
|
|
34
|
+
"To enable large requests, set `config.block_large_requests = False`."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
time_diff_years = abs((req.end or datetime.now()).year - req.start.year)
|
|
38
|
+
|
|
39
|
+
logger.debug(f"Request time range: {time_diff_years} years.")
|
|
40
|
+
|
|
41
|
+
if req.granularity is Granularity.HOURLY and time_diff_years > 3:
|
|
42
|
+
raise ValueError(
|
|
43
|
+
"Hourly requests longer than 3 years are blocked by default. "
|
|
44
|
+
"To enable large requests, set `config.block_large_requests = False`."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if req.granularity is Granularity.DAILY and time_diff_years > 30:
|
|
48
|
+
raise ValueError(
|
|
49
|
+
"Daily requests longer than 30 years are blocked by default. "
|
|
50
|
+
"To enable large requests, set `config.block_large_requests = False`."
|
|
51
|
+
)
|
|
@@ -13,7 +13,6 @@ import pytz
|
|
|
13
13
|
|
|
14
14
|
from meteostat.api.stations import stations as stations_service
|
|
15
15
|
from meteostat.api.point import Point
|
|
16
|
-
from meteostat.core.config import config
|
|
17
16
|
from meteostat.typing import Station
|
|
18
17
|
|
|
19
18
|
|
|
@@ -54,12 +53,6 @@ def parse_station(
|
|
|
54
53
|
# It's a list
|
|
55
54
|
stations = station
|
|
56
55
|
|
|
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
56
|
# Get station meta data
|
|
64
57
|
data = []
|
|
65
58
|
point_counter = 0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|