├── .github ├── FUNDING.yml └── workflows │ ├── assign-project.yml │ ├── linter.yml │ ├── publish.yml │ └── tests.yml ├── .gitignore ├── .pylintrc ├── CITATION.cff ├── LICENSE ├── README.md ├── examples ├── README.md ├── daily │ ├── README.md │ ├── aggregate.py │ ├── aggregate_regional.py │ ├── chart.py │ ├── compare.py │ ├── compare_aggregate.py │ ├── point.py │ └── source_flags.py ├── hourly │ ├── README.md │ ├── aggregate.py │ ├── chart.py │ ├── convert.py │ ├── interpolate.py │ ├── performance.py │ └── point.py ├── monthly │ ├── README.md │ └── aggregate.py ├── normals │ ├── README.md │ ├── point.py │ └── simple.py └── stations │ ├── README.md │ ├── bounds.py │ ├── nearby.py │ └── region.py ├── meteostat ├── __init__.py ├── core │ ├── __init__.py │ ├── cache.py │ ├── loader.py │ └── warn.py ├── enumerations │ ├── __init__.py │ └── granularity.py ├── interface │ ├── __init__.py │ ├── base.py │ ├── daily.py │ ├── hourly.py │ ├── interpolate.py │ ├── meteodata.py │ ├── monthly.py │ ├── normals.py │ ├── point.py │ ├── stations.py │ └── timeseries.py ├── series │ ├── __init__.py │ ├── aggregate.py │ ├── convert.py │ ├── count.py │ ├── coverage.py │ ├── fetch.py │ ├── interpolate.py │ ├── normalize.py │ └── stations.py ├── units.py └── utilities │ ├── __init__.py │ ├── aggregations.py │ ├── endpoint.py │ ├── helpers.py │ ├── mutations.py │ └── validations.py ├── requirements.txt ├── setup.py └── tests ├── e2e ├── test_daily.py ├── test_hourly.py ├── test_monthly.py ├── test_normals.py ├── test_point.py └── test_stations.py ├── manual ├── manual_test_aggregation.py └── manual_test_spatial_interpolation.py └── unit ├── core ├── __init__.py └── test_cache.py └── utilities ├── __init__.py └── test_endpoint.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: clampr 2 | patreon: meteostat 3 | custom: ["https://www.paypal.com/donate?hosted_button_id=MQ67WRDC8EW38"] 4 | -------------------------------------------------------------------------------- /.github/workflows/assign-project.yml: -------------------------------------------------------------------------------- 1 | on: 2 | issues: 3 | types: [opened] 4 | jobs: 5 | createCard: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Create or Update Project Card 9 | uses: peter-evans/create-or-update-project-card@v1 10 | with: 11 | token: ${{ secrets.PAT }} 12 | project-location: meteostat 13 | project-name: Meteostat 14 | column-name: Triage 15 | -------------------------------------------------------------------------------- /.github/workflows/linter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ########################### 3 | ########################### 4 | ## Linter GitHub Actions ## 5 | ########################### 6 | ########################### 7 | name: Lint Code Base 8 | 9 | # 10 | # Documentation: 11 | # https://help.github.com/en/articles/workflow-syntax-for-github-actions 12 | # 13 | 14 | ############################# 15 | # Start the job on all push # 16 | ############################# 17 | on: 18 | push: 19 | branches-ignore: [master] 20 | # Remove the line above to run when pushing to master 21 | pull_request: 22 | branches: [master] 23 | 24 | ############### 25 | # Set the Job # 26 | ############### 27 | jobs: 28 | build: 29 | # Name the Job 30 | name: Lint Code Base 31 | # Set the agent to run on 32 | runs-on: ubuntu-latest 33 | 34 | ################## 35 | # Load all steps # 36 | ################## 37 | steps: 38 | ########################## 39 | # Checkout the code base # 40 | ########################## 41 | - name: Checkout Code 42 | uses: actions/checkout@v2 43 | with: 44 | # Full git history is needed to get a proper list of changed files within `super-linter` 45 | fetch-depth: 0 46 | 47 | ################################ 48 | # Run Linter against code base # 49 | ################################ 50 | - name: Lint Code Base 51 | uses: github/super-linter@v3.17.0 52 | env: 53 | VALIDATE_ALL_CODEBASE: false 54 | DEFAULT_BRANCH: master 55 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 56 | LINTER_RULES_PATH: / 57 | PYTHON_PYLINT_CONFIG_FILE: .pylintrc 58 | VALIDATE_PYTHON_BLACK: false 59 | VALIDATE_PYTHON_FLAKE8: false 60 | VALIDATE_PYTHON_ISORT: false 61 | VALIDATE_PYTHON_MYPY: false 62 | VALIDATE_JSCPD: false 63 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Install Python 3 13 | uses: actions/setup-python@v4 14 | with: 15 | python-version: 3.11 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install -r requirements.txt 20 | - name: Build the package 21 | run: python setup.py sdist bdist_wheel 22 | - name: Check using Twine 23 | run: twine check dist/* 24 | - name: Upload to PyPI 25 | run: twine upload -u ${{ secrets.PYPI_USER }} -p ${{ secrets.PYPI_PASSWORD }} dist/* 26 | 27 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Unit & E2E Tests 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Install Python 3 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: 3.11 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install -r requirements.txt 23 | - name: Install Meteostat 24 | run: python -m pip install . -U 25 | - name: Run unit tests 26 | run: cd tests/unit && python -m pytest 27 | - name: Run E2E tests 28 | run: cd tests/e2e && python -m pytest 29 | 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | .DS_Store 131 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MESSAGES CONTROL] 2 | 3 | disable=no-member, 4 | too-few-public-methods, 5 | protected-access, 6 | import-outside-toplevel, 7 | duplicate-code, 8 | too-many-positional-arguments, 9 | import-error 10 | 11 | [BASIC] 12 | 13 | good-names=i, 14 | j, 15 | k, 16 | ex, 17 | Run, 18 | _, 19 | id, 20 | ms, 21 | df, 22 | x, 23 | y 24 | 25 | [DESIGN] 26 | 27 | max-args=10 28 | 29 | [CLASSES] 30 | 31 | exclude-protected=_asdict, 32 | _fields, 33 | _replace, 34 | _sour, 35 | _columns, 36 | _stations, 37 | _start, 38 | _end, 39 | _data, 40 | _aggregations 41 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | title: Meteostat Python 3 | message: >- 4 | If you use this software, please cite it using the 5 | metadata from this file. 6 | type: software 7 | authors: 8 | - given-names: Christian Sebastian 9 | family-names: Lamprecht 10 | email: cla@meteostat.net 11 | orcid: 'https://orcid.org/0000-0003-3301-2852' 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Meteostat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Meteostat Python Package 2 | 3 | The Meteostat Python library provides a simple API for accessing open weather and climate data. The historical observations and statistics are collected by [Meteostat](https://meteostat.net) from different public interfaces, most of which are governmental. 4 | 5 | Among the data sources are national weather services like the National Oceanic and Atmospheric Administration (NOAA) and Germany's national weather service (DWD). 6 | 7 | Are you looking for a **hosted solution**? Try our [JSON API](https://rapidapi.com/meteostat/api/meteostat/). 8 | 9 | ## Installation 10 | 11 | The Meteostat Python package is available through [PyPI](https://pypi.org/project/meteostat/): 12 | 13 | ```sh 14 | pip install meteostat 15 | ``` 16 | 17 | Meteostat **requires Python 3.6** or higher. If you want to visualize data, please install Matplotlib, too. 18 | 19 | ## Documentation 20 | 21 | The Meteostat Python library is divided into multiple classes which provide access to the actual data. The [documentation](https://dev.meteostat.net/python/) covers all aspects of the library: 22 | 23 | * **Selecting Locations** 24 | * [Geographical Point](https://dev.meteostat.net/python/point.html) 25 | * [Weather Stations](https://dev.meteostat.net/python/stations.html) 26 | * **Time Series** 27 | * [Hourly Data](https://dev.meteostat.net/python/hourly.html) 28 | * [Daily Data](https://dev.meteostat.net/python/daily.html) 29 | * [Monthly Data](https://dev.meteostat.net/python/monthly.html) 30 | * **Miscellaneous Data** 31 | * [Climate Normals](https://dev.meteostat.net/python/normals.html) 32 | * **Library** 33 | * [Contributing](https://dev.meteostat.net/python/contributing.html) 34 | * [Formats & Units](https://dev.meteostat.net/formats.html) 35 | * [Data Sources](https://dev.meteostat.net/sources.html) 36 | * [Terms & License](https://dev.meteostat.net/terms.html) 37 | 38 | ## Example 39 | 40 | Let's plot 2018 temperature data for Vancouver, BC: 41 | 42 | ```python 43 | # Import Meteostat library and dependencies 44 | from datetime import datetime 45 | import matplotlib.pyplot as plt 46 | from meteostat import Point, Daily 47 | 48 | # Set time period 49 | start = datetime(2018, 1, 1) 50 | end = datetime(2018, 12, 31) 51 | 52 | # Create Point for Vancouver, BC 53 | location = Point(49.2497, -123.1193, 70) 54 | 55 | # Get daily data for 2018 56 | data = Daily(location, start, end) 57 | data = data.fetch() 58 | 59 | # Plot line chart including average, minimum and maximum temperature 60 | data.plot(y=['tavg', 'tmin', 'tmax']) 61 | plt.show() 62 | ``` 63 | 64 | Take a look at the expected output: 65 | 66 | ![2018 temperature data for Vancouver, BC](https://dev.meteostat.net/assets/img/py-example-chart.046f8b8e.png) 67 | 68 | ## Contributing 69 | 70 | Instructions on building and testing the Meteostat Python package can be found in the [documentation](https://dev.meteostat.net/python/contributing.html). More information about the Meteostat bulk data interface is available [here](https://dev.meteostat.net/bulk/). 71 | 72 | ## Donating 73 | 74 | If you want to support the project financially, you can make a donation using one of the following services: 75 | 76 | * [GitHub](https://github.com/sponsors/clampr) 77 | * [Patreon](https://www.patreon.com/meteostat) 78 | * [PayPal](https://www.paypal.com/donate?hosted_button_id=MQ67WRDC8EW38) 79 | 80 | ## Data License 81 | 82 | Meteorological data is provided under the terms of the [Creative Commons Attribution-NonCommercial 4.0 International Public License (CC BY-NC 4.0)](https://creativecommons.org/licenses/by-nc/4.0/legalcode). You may build upon the material 83 | for any purpose, even commercially. However, you are not allowed to redistribute Meteostat data "as-is" for commercial purposes. 84 | 85 | By using the Meteostat Python library you agree to our [terms of service](https://dev.meteostat.net/terms.html). All meteorological data sources used by the Meteostat project are listed [here](https://dev.meteostat.net/sources.html). 86 | 87 | ## Code License 88 | 89 | The code of this library is available under the [MIT license](https://opensource.org/licenses/MIT). 90 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | This directory contains some examples which showcase the usage of the different data interfaces provided by Meteostat Python. 4 | 5 | ## Table of Contents 6 | 7 | * [Hourly Data](hourly) 8 | * [Daily Data](daily) 9 | * [Monthly Data](monthly) 10 | * [Climate Normals](normals) 11 | * [Weather Stations](stations) 12 | -------------------------------------------------------------------------------- /examples/daily/README.md: -------------------------------------------------------------------------------- 1 | # Daily Data Examples 2 | 3 | This directory contains examples which illustrate the usage of Meteostat's `Daily` data interface. 4 | 5 | ## Examples 6 | 7 | * [Point Data](point.py): Query daily weather data by geo coordinates 8 | * [Plotting Charts](chart.py): Plot a chart of daily data for closest weather station to geo coordinates 9 | * [Aggregating Data](aggregate.py): Grouping and aggregation of daily data 10 | * [Spatial Aggregation](aggregate_regional.py): Perform spatial aggregation of multiple weather stations 11 | * [Multiple Stations](compare.py): Plot time series of multiple weather stations in a single chart 12 | * [Aggregating Multiple Stations](compare_aggregate.py): Aggregate data for multiple weather stations 13 | -------------------------------------------------------------------------------- /examples/daily/aggregate.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example: Aggregation 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 datetime import datetime 12 | import matplotlib.pyplot as plt 13 | from meteostat import Daily 14 | 15 | # Time period 16 | start = datetime(2018, 1, 1) 17 | end = datetime(2018, 12, 31) 18 | 19 | # Get daily data 20 | data = Daily("10637", start, end) 21 | 22 | # Group & aggregate weekly 23 | data = data.normalize().aggregate(freq="1W").fetch() 24 | 25 | # Plot chart 26 | data.plot(y=["tavg", "tmin", "tmax"]) 27 | plt.show() 28 | -------------------------------------------------------------------------------- /examples/daily/aggregate_regional.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example: Spatial aggregation 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 | if __name__ == "__main__": 12 | 13 | from datetime import datetime 14 | import matplotlib.pyplot as plt 15 | from meteostat import Stations, Daily 16 | 17 | # Configuration 18 | Daily.cores = 12 19 | 20 | # Time period 21 | start = datetime(1980, 1, 1) 22 | end = datetime(2019, 12, 31) 23 | 24 | # Get random weather stations in the US 25 | stations = Stations() 26 | stations = stations.region("US") 27 | stations = stations.inventory("daily", (start, end)) 28 | stations = stations.fetch(limit=150, sample=True) 29 | 30 | # Get daily data 31 | data = Daily(stations, start, end) 32 | 33 | # Normalize & aggregate 34 | data = data.normalize().aggregate("1Y", spatial=True).fetch() 35 | 36 | # Chart title 37 | TITLE = "Average US Annual Temperature from 1980 to 2019" 38 | 39 | # Plot chart 40 | data.plot(y=["tavg"], title=TITLE) 41 | plt.show() 42 | -------------------------------------------------------------------------------- /examples/daily/chart.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example: Simple chart 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 datetime import datetime 12 | import matplotlib.pyplot as plt 13 | from meteostat import Stations, Daily 14 | 15 | # Time period 16 | start = datetime(2018, 1, 1) 17 | end = datetime(2018, 12, 31) 18 | 19 | # Get closest weather station 20 | stations = Stations() 21 | stations = stations.nearby(49.2497, -123.1193) 22 | stations = stations.inventory("daily", (start, end)) 23 | station = stations.fetch(1) 24 | 25 | # Get daily data 26 | data = Daily(station, start, end) 27 | data = data.fetch() 28 | 29 | # Plot chart 30 | data.plot(y=["tavg", "tmin", "tmax", "prcp"], subplots=True) 31 | plt.show() 32 | -------------------------------------------------------------------------------- /examples/daily/compare.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example: Comparing multiple weather stations 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 datetime import datetime 12 | import matplotlib.pyplot as plt 13 | from meteostat import Daily 14 | 15 | # Time period 16 | start = datetime(2019, 1, 1) 17 | end = datetime(2019, 12, 31) 18 | 19 | # Get daily data 20 | data = Daily(["71624", "72295", "68816", "94767"], start, end) 21 | data = data.fetch() 22 | 23 | # Plot chart 24 | data.unstack("station")["tavg"].plot( 25 | legend=True, 26 | ylabel="Avg. Daily Temperature °C", 27 | title="Average Temperature Report for 2019", 28 | ) 29 | plt.show() 30 | -------------------------------------------------------------------------------- /examples/daily/compare_aggregate.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example: Comparing aggregated data 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 datetime import datetime 12 | import matplotlib.pyplot as plt 13 | from meteostat import Stations, Daily 14 | 15 | # Get weather stations by Meteostat ID 16 | stations = Stations() 17 | stations = stations.fetch() 18 | stations = stations[stations.index.isin(("D1424", "10729", "10803", "10513"))] 19 | 20 | # Get names of weather stations 21 | names = stations["name"].to_list() 22 | 23 | # Time period 24 | start = datetime(2000, 1, 1) 25 | end = datetime(2019, 12, 31) 26 | 27 | # Get daily data 28 | data = Daily(stations, start, end) 29 | 30 | # Aggregate annually 31 | data = data.aggregate(freq="1YE").fetch() 32 | 33 | # Plot chart 34 | fig, ax = plt.subplots(figsize=(8, 6)) 35 | data.unstack("station")["tmax"].plot( 36 | legend=True, 37 | ax=ax, 38 | style=".-", 39 | ylabel="Max. Annual Temperature (°C)", 40 | title="Max. Temperature Report", 41 | ) 42 | plt.legend(names) 43 | plt.show() 44 | -------------------------------------------------------------------------------- /examples/daily/point.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example: Daily point data access 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 datetime import datetime 12 | import matplotlib.pyplot as plt 13 | from meteostat import Point, Daily 14 | 15 | # Set time period 16 | start = datetime(2018, 1, 1) 17 | end = datetime(2018, 12, 31) 18 | 19 | # Create Point for Vancouver, BC 20 | vancouver = Point(49.2497, -123.1193, 70) 21 | 22 | # Get daily data for 2018 23 | data = Daily(vancouver, start, end) 24 | data = data.fetch() 25 | 26 | # Plot line chart including average, minimum and maximum temperature 27 | data.plot(y=["tavg", "tmin", "tmax"]) 28 | plt.show() 29 | -------------------------------------------------------------------------------- /examples/daily/source_flags.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example: Request with source flags 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 datetime import datetime 12 | from meteostat import Daily 13 | 14 | # Time period 15 | start = datetime(2018, 1, 1) 16 | end = datetime(2018, 12, 31) 17 | 18 | data = Daily("10637", start, end, flags=True) 19 | df = data.fetch() 20 | 21 | # Print DataFrame 22 | print(df) 23 | -------------------------------------------------------------------------------- /examples/hourly/README.md: -------------------------------------------------------------------------------- 1 | # Hourly Data Examples 2 | 3 | This directory contains examples which illustrate the usage of Meteostat's `Hourly` data interface. 4 | 5 | ## Examples 6 | 7 | * [Point Data](point.py): Query hourly weather data by geo coordinates 8 | * [Plotting Charts](chart.py): Plot a chart of hourly data for closest weather station to geo coordinates 9 | * [Aggregating Data](aggregate.py): Grouping and aggregation of hourly data 10 | * [Converting Units](convert.py): Convert measurements to a different unit 11 | * [Interpolation](interpolate.py): Close gaps in time series using interpolation 12 | -------------------------------------------------------------------------------- /examples/hourly/aggregate.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example: Aggregation 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 datetime import datetime 12 | from meteostat import Hourly 13 | 14 | # Time period 15 | start = datetime(2018, 1, 1) 16 | end = datetime(2018, 1, 1, 23, 59) 17 | 18 | # Get hourly data & aggregate daily 19 | data = Hourly("10637", start, end) 20 | data = data.aggregate("1D") 21 | data = data.fetch() 22 | 23 | # Print 24 | print(data) 25 | -------------------------------------------------------------------------------- /examples/hourly/chart.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example: Simple chart 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 datetime import datetime 12 | import matplotlib.pyplot as plt 13 | from meteostat import Stations, Hourly 14 | 15 | # Get closest weather station 16 | stations = Stations() 17 | stations = stations.nearby(50, 8) 18 | station = stations.fetch(1) 19 | 20 | # Time period 21 | start = datetime(2017, 1, 1) 22 | end = datetime(2017, 1, 1, 23, 59) 23 | 24 | # Get hourly data 25 | data = Hourly(station, start, end) 26 | data = data.fetch() 27 | 28 | # Plot chart 29 | data.plot(y="temp") 30 | plt.show() 31 | -------------------------------------------------------------------------------- /examples/hourly/convert.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example: Simple data access 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 datetime import datetime 12 | from meteostat import Stations, Hourly 13 | from meteostat.units import fahrenheit, direction, condition 14 | 15 | # Time period 16 | start = datetime(2018, 1, 1) 17 | end = datetime(2018, 1, 1, 23, 59) 18 | 19 | # Get nearby weather station 20 | stations = Stations() 21 | stations = stations.nearby(50, 8) 22 | station = stations.fetch(1) 23 | 24 | # Get hourly data 25 | data = Hourly(station, start, end, timezone="Europe/Berlin") 26 | 27 | # Convert data units 28 | data = data.convert({"temp": fahrenheit, "wdir": direction, "coco": condition}) 29 | 30 | # Print to console 31 | data = data.fetch() 32 | print(data) 33 | -------------------------------------------------------------------------------- /examples/hourly/interpolate.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example: Interpolation 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 datetime import datetime 12 | import matplotlib.pyplot as plt 13 | from meteostat import Hourly 14 | 15 | # Time period 16 | start = datetime(2018, 8, 1) 17 | end = datetime(2018, 8, 4, 23, 59) 18 | 19 | # Get hourly data 20 | data = Hourly("10730", start, end) 21 | 22 | # Normalize data & interpolate up to 6 missing consecutive records 23 | data = data.normalize() 24 | data = data.interpolate(6) 25 | 26 | # Fetch data 27 | data = data.fetch() 28 | 29 | # Plot chart 30 | data.plot(y="temp") 31 | plt.show() 32 | -------------------------------------------------------------------------------- /examples/hourly/performance.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example: Hourly point data performance 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 | if __name__ == "__main__": 12 | 13 | from timeit import default_timer as timer 14 | 15 | # Get start time 16 | s = timer() 17 | 18 | # Run script 19 | from datetime import datetime 20 | from meteostat import Hourly 21 | 22 | Hourly.cores = 12 23 | 24 | start = datetime(1960, 1, 1) 25 | end = datetime(2021, 1, 1, 23, 59) 26 | 27 | data = Hourly("10637", start, end, timezone="Europe/Berlin") 28 | data = data.fetch() 29 | 30 | # Get end time 31 | e = timer() 32 | 33 | # Print performance 34 | print(e - s) 35 | -------------------------------------------------------------------------------- /examples/hourly/point.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example: Hourly point data access 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 datetime import datetime 12 | from meteostat import Point, Hourly 13 | 14 | # Time period 15 | start = datetime(2021, 1, 1) 16 | end = datetime(2021, 1, 1, 23, 59) 17 | 18 | # The point 19 | point = Point(50.3167, 8.5, 320) 20 | 21 | # Get hourly data 22 | data = Hourly(point, start, end, timezone="Europe/Berlin") 23 | 24 | # Print to console 25 | data = data.fetch() 26 | print(data) 27 | -------------------------------------------------------------------------------- /examples/monthly/README.md: -------------------------------------------------------------------------------- 1 | # Monthly Data Examples 2 | 3 | This directory contains examples which illustrate the usage of Meteostat's `Monthly` data interface. 4 | 5 | ## Examples 6 | 7 | * [Aggregating Data](aggregate.py): Grouping and aggregation of monthly data 8 | -------------------------------------------------------------------------------- /examples/monthly/aggregate.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example: Aggregation 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 datetime import datetime 12 | import matplotlib.pyplot as plt 13 | from meteostat import Monthly 14 | 15 | # Time period 16 | start = datetime(2000, 1, 1) 17 | end = datetime(2018, 12, 31) 18 | 19 | # Get monthly data 20 | # Then, aggregate annually 21 | data = Monthly("72202", start, end) 22 | data = data.normalize().aggregate(freq="1YE").fetch() 23 | 24 | # Plot chart 25 | data.plot(y="tavg") 26 | plt.show() 27 | -------------------------------------------------------------------------------- /examples/normals/README.md: -------------------------------------------------------------------------------- 1 | # Climate Normals Examples 2 | 3 | This directory contains examples which illustrate the usage of Meteostat's climate `Normals` interface. 4 | 5 | ## Examples 6 | 7 | * [By Point](point.py): Query climate normals by geo coordinates 8 | * [By Weather Station](simple.py): Query climate normals by weather station 9 | -------------------------------------------------------------------------------- /examples/normals/point.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example: Climate normals by geo point 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 | import matplotlib.pyplot as plt 12 | from meteostat import Normals, Point 13 | 14 | # Create Point for Vancouver, BC 15 | vancouver = Point(49.2497, -123.1193, 70) 16 | 17 | # Get normals 18 | data = Normals(vancouver, 1961, 1990) 19 | data = data.normalize().fetch() 20 | 21 | # Plot chart 22 | data.plot(y=["tavg", "tmin", "tmax"]) 23 | plt.show() 24 | -------------------------------------------------------------------------------- /examples/normals/simple.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example: Simple climate data access 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 | import matplotlib.pyplot as plt 12 | from meteostat import Normals 13 | 14 | # Get normals 15 | data = Normals("10637") 16 | data = data.normalize().fetch() 17 | 18 | # Plot chart 19 | data.plot(y=["tavg", "tmin", "tmax"]) 20 | plt.show() 21 | -------------------------------------------------------------------------------- /examples/stations/README.md: -------------------------------------------------------------------------------- 1 | # Weather Stations Examples 2 | 3 | This directory contains examples which illustrate the usage of Meteostat's `Stations` interface. 4 | 5 | ## Examples 6 | 7 | * [Nearby Stations](nearby.py): Get nearby weather stations based on a geo location 8 | * [Stations By Country & Region](region.py): Get weather stations located in a certain country (& state) 9 | * [Stations By Geographic Boundaries](bounds.py): Get weather stations within rectangular boundaries 10 | -------------------------------------------------------------------------------- /examples/stations/bounds.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example: Get weather stations by geographical area 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 meteostat import Stations 12 | 13 | # Get all stations 14 | stations = Stations() 15 | 16 | # Get number of stations in northern hemisphere 17 | northern = stations.bounds((90, -180), (0, 180)) 18 | print("Stations in northern hemisphere:", northern.count()) 19 | 20 | # Get number of stations in southern hemisphere 21 | southern = stations.bounds((0, -180), (-90, 180)) 22 | print("Stations in southern hemisphere:", southern.count()) 23 | -------------------------------------------------------------------------------- /examples/stations/nearby.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example: Closest weather station by coordinates 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 meteostat import Stations 12 | 13 | # Get weather station 14 | stations = Stations() 15 | stations = stations.nearby(50, 8) 16 | stations = stations.inventory("hourly") 17 | station = stations.fetch(1).to_dict("records")[0] 18 | 19 | # Print name 20 | print("Closest weather station at coordinates 50, 8:", station["name"]) 21 | -------------------------------------------------------------------------------- /examples/stations/region.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example: Select weather stations by country & state 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 meteostat import Stations 12 | 13 | # Get stations in Ontario 14 | stations = Stations() 15 | stations = stations.region("CA", "ON") 16 | 17 | # Print count to console 18 | print("Stations in Ontario:", stations.count()) 19 | -------------------------------------------------------------------------------- /meteostat/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | █▀▄▀█ █▀▀ ▀█▀ █▀▀ █▀█ █▀ ▀█▀ ▄▀█ ▀█▀ 3 | █░▀░█ ██▄ ░█░ ██▄ █▄█ ▄█ ░█░ █▀█ ░█░ 4 | 5 | A Python library for accessing open weather and climate data 6 | 7 | Meteorological data provided by Meteostat (https://dev.meteostat.net) 8 | under the terms of the Creative Commons Attribution-NonCommercial 9 | 4.0 International Public License. 10 | 11 | The code is licensed under the MIT license. 12 | """ 13 | 14 | __appname__ = "meteostat" 15 | __version__ = "1.7.0" 16 | 17 | from .interface.base import Base 18 | from .interface.timeseries import TimeSeries 19 | from .interface.stations import Stations 20 | from .interface.point import Point 21 | from .interface.hourly import Hourly 22 | from .interface.daily import Daily 23 | from .interface.monthly import Monthly 24 | from .interface.normals import Normals 25 | 26 | __all__ = [ 27 | "Base", 28 | "TimeSeries", 29 | "Stations", 30 | "Point", 31 | "Hourly", 32 | "Daily", 33 | "Monthly", 34 | "Normals", 35 | ] 36 | -------------------------------------------------------------------------------- /meteostat/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteostat/meteostat-python/ba9653a4244c2cd72e7d926c29c738d18fa61258/meteostat/core/__init__.py -------------------------------------------------------------------------------- /meteostat/core/cache.py: -------------------------------------------------------------------------------- 1 | """ 2 | Core Class - Cache 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 | import os 12 | import time 13 | import hashlib 14 | 15 | 16 | def get_local_file_path(cache_dir: str, cache_subdir: str, path: str) -> str: 17 | """ 18 | Get the local file path 19 | """ 20 | 21 | # Get file ID 22 | file = hashlib.md5(path.encode("utf-8")).hexdigest() 23 | 24 | return f"{cache_dir}/{cache_subdir}/{file}" 25 | 26 | 27 | def file_in_cache(path: str, max_age: int = 0) -> bool: 28 | """ 29 | Check if a file exists in the local cache 30 | """ 31 | 32 | # Get directory 33 | directory = os.path.dirname(path) 34 | 35 | # Make sure the cache directory exists 36 | if not os.path.exists(directory): 37 | try: 38 | os.makedirs(directory) 39 | except FileExistsError: 40 | pass 41 | 42 | # Return the file path if it exists 43 | if os.path.isfile(path) and time.time() - os.path.getmtime(path) <= max_age: 44 | return True 45 | 46 | return False 47 | 48 | 49 | @classmethod 50 | def clear_cache(cls, max_age: int = None) -> None: 51 | """ 52 | Clear the cache 53 | """ 54 | 55 | if os.path.exists(cls.cache_dir + os.sep + cls.cache_subdir): 56 | # Set max_age 57 | if max_age is None: 58 | max_age = cls.max_age 59 | 60 | # Get current time 61 | now = time.time() 62 | 63 | # Go through all files 64 | for file in os.listdir(cls.cache_dir + os.sep + cls.cache_subdir): 65 | # Get full path 66 | path = os.path.join(cls.cache_dir + os.sep + cls.cache_subdir, file) 67 | 68 | # Check if file is older than max_age 69 | if now - os.path.getmtime(path) > max_age and os.path.isfile(path): 70 | # Delete file 71 | os.remove(path) 72 | -------------------------------------------------------------------------------- /meteostat/core/loader.py: -------------------------------------------------------------------------------- 1 | """ 2 | Core Class - Data Loader 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 io import BytesIO 12 | from gzip import GzipFile 13 | from urllib.request import Request, ProxyHandler, build_opener 14 | from urllib.error import HTTPError 15 | from multiprocessing import Pool 16 | from multiprocessing.pool import ThreadPool 17 | from typing import Callable, List, Optional 18 | import pandas as pd 19 | from meteostat.core.warn import warn 20 | 21 | 22 | def processing_handler( 23 | datasets: List, load: Callable[[dict], None], cores: int, threads: int 24 | ) -> None: 25 | """ 26 | Load multiple datasets (simultaneously) 27 | """ 28 | 29 | # Data output 30 | output = [] 31 | 32 | # Multi-core processing 33 | if cores > 1 and len(datasets) > 1: 34 | # Create process pool 35 | with Pool(cores) as pool: 36 | # Process datasets in pool 37 | output = pool.starmap(load, datasets) 38 | 39 | # Wait for Pool to finish 40 | pool.close() 41 | pool.join() 42 | 43 | # Multi-thread processing 44 | elif threads > 1 and len(datasets) > 1: 45 | # Create process pool 46 | with ThreadPool(threads) as pool: 47 | # Process datasets in pool 48 | output = pool.starmap(load, datasets) 49 | 50 | # Wait for Pool to finish 51 | pool.close() 52 | pool.join() 53 | 54 | # Single-thread processing 55 | else: 56 | for dataset in datasets: 57 | output.append(load(*dataset)) 58 | 59 | # Remove empty DataFrames 60 | filtered = list(filter(lambda df: not df.empty, output)) 61 | 62 | return pd.concat(filtered) if len(filtered) > 0 else output[0] 63 | 64 | 65 | def load_handler( 66 | endpoint: str, 67 | path: str, 68 | proxy: Optional[str] = None, 69 | names: Optional[List] = None, 70 | dtype: Optional[dict] = None, 71 | parse_dates: Optional[List] = None, 72 | default_df: Optional[pd.DataFrame] = None, 73 | ) -> pd.DataFrame: 74 | """ 75 | Load a single CSV file into a DataFrame 76 | """ 77 | 78 | try: 79 | handlers = [] 80 | 81 | # Set a proxy 82 | if proxy: 83 | handlers.append(ProxyHandler({"http": proxy, "https": proxy})) 84 | 85 | # Read CSV file from Meteostat endpoint 86 | with build_opener(*handlers).open(Request(endpoint + path)) as response: 87 | # Decompress the content 88 | with GzipFile(fileobj=BytesIO(response.read()), mode="rb") as file: 89 | df = pd.read_csv( 90 | file, 91 | names=names, 92 | dtype=dtype, 93 | parse_dates=parse_dates, 94 | ) 95 | 96 | except (FileNotFoundError, HTTPError): 97 | df = default_df if default_df is not None else pd.DataFrame(columns=names) 98 | 99 | # Display warning 100 | warn(f"Cannot load {path} from {endpoint}") 101 | 102 | # Return DataFrame 103 | return df 104 | -------------------------------------------------------------------------------- /meteostat/core/warn.py: -------------------------------------------------------------------------------- 1 | """ 2 | Core Class - Warnings 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 | import warnings 12 | 13 | 14 | def _format(message, category, _filename, _lineno, _line=None) -> str: 15 | """ 16 | Print warning on a single line 17 | """ 18 | 19 | return f"{category.__name__}: {message}\n" 20 | 21 | 22 | # Set warning format 23 | warnings.formatwarning = _format 24 | 25 | 26 | def warn(message: str) -> None: 27 | """ 28 | Create a warning 29 | """ 30 | 31 | try: 32 | warnings.warn(message, Warning) 33 | except TypeError: 34 | pass 35 | -------------------------------------------------------------------------------- /meteostat/enumerations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteostat/meteostat-python/ba9653a4244c2cd72e7d926c29c738d18fa61258/meteostat/enumerations/__init__.py -------------------------------------------------------------------------------- /meteostat/enumerations/granularity.py: -------------------------------------------------------------------------------- 1 | """ 2 | Granularity Enumeration 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 enum import Enum 12 | 13 | 14 | class Granularity(Enum): 15 | """ 16 | The different levels of time series granularity 17 | """ 18 | 19 | HOURLY = "hourly" 20 | DAILY = "daily" 21 | MONTHLY = "monthly" 22 | NORMALS = "normals" 23 | -------------------------------------------------------------------------------- /meteostat/interface/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteostat/meteostat-python/ba9653a4244c2cd72e7d926c29c738d18fa61258/meteostat/interface/__init__.py -------------------------------------------------------------------------------- /meteostat/interface/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Base Interface Class 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 | import os 12 | from typing import Optional 13 | 14 | 15 | class Base: 16 | """ 17 | Base class that provides features which are used across the package 18 | """ 19 | 20 | # Base URL of the Meteostat bulk data interface 21 | endpoint = "https://bulk.meteostat.net/v2/" 22 | 23 | # Proxy URL for the Meteostat (bulk) data interface 24 | proxy: Optional[str] = None 25 | 26 | # Location of the cache directory 27 | cache_dir = os.path.expanduser("~") + os.sep + ".meteostat" + os.sep + "cache" 28 | 29 | # Auto clean cache directories? 30 | autoclean = True 31 | 32 | # Maximum age of a cached file in seconds 33 | max_age = 24 * 60 * 60 34 | 35 | # Number of processes used for processing files 36 | processes = 1 37 | 38 | # Number of threads used for processing files 39 | threads = 1 40 | -------------------------------------------------------------------------------- /meteostat/interface/daily.py: -------------------------------------------------------------------------------- 1 | """ 2 | Daily Class 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 datetime import datetime, timedelta 12 | from typing import Union 13 | import pandas as pd 14 | from meteostat.enumerations.granularity import Granularity 15 | from meteostat.utilities.aggregations import degree_mean 16 | from meteostat.interface.timeseries import TimeSeries 17 | from meteostat.interface.point import Point 18 | 19 | 20 | class Daily(TimeSeries): 21 | """ 22 | Retrieve daily weather observations for one or multiple weather stations or 23 | a single geographical point 24 | """ 25 | 26 | # The cache subdirectory 27 | cache_subdir = "daily" 28 | 29 | # Granularity 30 | granularity = Granularity.DAILY 31 | 32 | # Download data as annual chunks 33 | # This cannot be changed and is only kept for backward compatibility 34 | chunked = True 35 | 36 | # Default frequency 37 | _freq = "1D" 38 | 39 | # Source mappings 40 | _source_mappings = { 41 | "dwd_daily": "A", 42 | "eccc_daily": "A", 43 | "ghcnd": "B", 44 | "dwd_hourly": "C", 45 | "eccc_hourly": "C", 46 | "isd_lite": "D", 47 | "synop": "E", 48 | "dwd_poi": "E", 49 | "metar": "F", 50 | "model": "G", 51 | "dwd_mosmix": "G", 52 | "metno_forecast": "G", 53 | } 54 | 55 | # Flag which represents model data 56 | _model_flag = "G" 57 | 58 | # Columns 59 | _columns = [ 60 | "year", 61 | "month", 62 | "day", 63 | {"tavg": "temp"}, 64 | "tmin", 65 | "tmax", 66 | "prcp", 67 | {"snow": "snwd"}, 68 | {"wdir": None}, 69 | "wspd", 70 | "wpgt", 71 | "pres", 72 | "tsun", 73 | ] 74 | 75 | # Index of first meteorological column 76 | _first_met_col = 3 77 | 78 | # Columns for date parsing 79 | _parse_dates = ["year", "month", "day"] 80 | 81 | # Default aggregation functions 82 | aggregations = { 83 | "tavg": "mean", 84 | "tmin": "min", 85 | "tmax": "max", 86 | "prcp": "sum", 87 | "snow": "max", 88 | "wdir": degree_mean, 89 | "wspd": "mean", 90 | "wpgt": "max", 91 | "pres": "mean", 92 | "tsun": "sum", 93 | } 94 | 95 | def __init__( 96 | self, 97 | loc: Union[pd.DataFrame, Point, list, str], # Station(s) or geo point 98 | start=datetime(1781, 1, 1, 0, 0, 0), 99 | end=datetime.combine( 100 | datetime.today().date() + timedelta(days=10), datetime.max.time() 101 | ), 102 | model=True, # Include model data? 103 | flags=False, # Load source flags? 104 | ) -> None: 105 | # Extract relevant years 106 | if self.chunked: 107 | self._annual_steps = [ 108 | start.year + i for i in range(end.year - start.year + 1) 109 | ] 110 | # Initialize time series 111 | self._init_time_series(loc, start, end, model, flags) 112 | 113 | def expected_rows(self) -> int: 114 | """ 115 | Return the number of rows expected for the defined date range 116 | """ 117 | 118 | return (self._end - self._start).days + 1 119 | -------------------------------------------------------------------------------- /meteostat/interface/hourly.py: -------------------------------------------------------------------------------- 1 | """ 2 | Hourly Class 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 math import floor 12 | from datetime import datetime, timedelta 13 | from typing import Optional, Union 14 | import pytz 15 | import pandas as pd 16 | from meteostat.enumerations.granularity import Granularity 17 | from meteostat.utilities.aggregations import degree_mean 18 | from meteostat.interface.timeseries import TimeSeries 19 | from meteostat.interface.point import Point 20 | from meteostat.utilities.mutations import calculate_dwpt 21 | 22 | 23 | class Hourly(TimeSeries): 24 | """ 25 | Retrieve hourly weather observations for one or multiple weather stations or 26 | a single geographical point 27 | """ 28 | 29 | # The cache subdirectory 30 | cache_subdir = "hourly" 31 | 32 | # Granularity 33 | granularity = Granularity.HOURLY 34 | 35 | # Download data as annual chunks 36 | # This cannot be changed and is only kept for backward compatibility 37 | chunked = True 38 | 39 | # The time zone 40 | _timezone: Optional[str] = None 41 | 42 | # Default frequency 43 | _freq = "1h" 44 | 45 | # Source mappings 46 | _source_mappings = { 47 | "metar": "D", 48 | "model": "E", 49 | "isd_lite": "B", 50 | "synop": "C", 51 | "dwd_poi": "C", 52 | "dwd_hourly": "A", 53 | "dwd_mosmix": "E", 54 | "metno_forecast": "E", 55 | "eccc_hourly": "A", 56 | } 57 | 58 | # Flag which represents model data 59 | _model_flag = "E" 60 | 61 | # Raw data columns 62 | _columns = [ 63 | "year", 64 | "month", 65 | "day", 66 | "hour", 67 | "temp", 68 | {"dwpt": calculate_dwpt}, 69 | "rhum", 70 | "prcp", 71 | {"snow": "snwd"}, 72 | "wdir", 73 | "wspd", 74 | "wpgt", 75 | "pres", 76 | "tsun", 77 | "coco", 78 | ] 79 | 80 | # Index of first meteorological column 81 | _first_met_col = 4 82 | 83 | # Columns for date parsing 84 | _parse_dates = ["year", "month", "day", "hour"] 85 | 86 | # Default aggregation functions 87 | aggregations = { 88 | "temp": "mean", 89 | "dwpt": "mean", 90 | "rhum": "mean", 91 | "prcp": "sum", 92 | "snow": "max", 93 | "wdir": degree_mean, 94 | "wspd": "mean", 95 | "wpgt": "max", 96 | "pres": "mean", 97 | "tsun": "sum", 98 | "coco": "max", 99 | } 100 | 101 | def _set_time( 102 | self, 103 | start: Optional[datetime] = None, 104 | end: Optional[datetime] = None, 105 | timezone: Optional[str] = None, 106 | ) -> None: 107 | """ 108 | Set & adapt the period's time zone 109 | """ 110 | if timezone: 111 | # Save timezone 112 | self._timezone = timezone 113 | 114 | if start and end: 115 | # Initialize time zone 116 | timezone = pytz.timezone(self._timezone) 117 | 118 | # Set start date 119 | start = timezone.localize(start, is_dst=None).astimezone(pytz.utc) 120 | 121 | # Set end date 122 | end = timezone.localize(end, is_dst=None).astimezone(pytz.utc) 123 | 124 | if self.chunked: 125 | self._annual_steps = [ 126 | start.year + i for i in range(end.year - start.year + 1) 127 | ] 128 | 129 | self._start = start 130 | self._end = end 131 | 132 | def __init__( 133 | self, 134 | loc: Union[pd.DataFrame, Point, list, str], # Station(s) or geo point 135 | start=datetime(1890, 1, 1, 0, 0, 0), 136 | end=datetime.combine( 137 | datetime.today().date() + timedelta(days=10), datetime.max.time() 138 | ), 139 | timezone: Optional[str] = None, 140 | model=True, # Include model data? 141 | flags=False, # Load source flags? 142 | ) -> None: 143 | # Set time zone and adapt period 144 | self._set_time(start, end, timezone) 145 | 146 | # Initialize time series 147 | self._init_time_series(loc, start, end, model, flags) 148 | 149 | def expected_rows(self) -> int: 150 | """ 151 | Return the number of rows expected for the defined date range 152 | """ 153 | 154 | return floor((self._end - self._start).total_seconds() / 3600) + 1 155 | -------------------------------------------------------------------------------- /meteostat/interface/interpolate.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteostat/meteostat-python/ba9653a4244c2cd72e7d926c29c738d18fa61258/meteostat/interface/interpolate.py -------------------------------------------------------------------------------- /meteostat/interface/meteodata.py: -------------------------------------------------------------------------------- 1 | """ 2 | Meteorological Data Class 3 | 4 | A parent class for both time series and 5 | climate normals data 6 | 7 | Meteorological data provided by Meteostat (https://dev.meteostat.net) 8 | under the terms of the Creative Commons Attribution-NonCommercial 9 | 4.0 International Public License. 10 | 11 | The code is licensed under the MIT license. 12 | """ 13 | 14 | from collections.abc import Callable 15 | from typing import Dict, List, Union 16 | import pandas as pd 17 | from meteostat.enumerations.granularity import Granularity 18 | from meteostat.core.loader import processing_handler 19 | from meteostat.utilities.mutations import adjust_temp 20 | from meteostat.utilities.aggregations import weighted_average 21 | from meteostat.interface.base import Base 22 | 23 | 24 | class MeteoData(Base): 25 | """ 26 | A parent class for both time series and 27 | climate normals data 28 | """ 29 | 30 | # The list of weather Stations 31 | _stations: Union[pd.Index, None] = None 32 | 33 | # The data frame 34 | _data: pd.DataFrame = pd.DataFrame() 35 | 36 | @property 37 | def _raw_columns(self) -> List[str]: 38 | """ 39 | Get the list of raw data columns, excluding any dicts with callable values 40 | """ 41 | return [ 42 | list(col.values())[0] if isinstance(col, dict) else col 43 | for col in self._columns 44 | if not ( 45 | isinstance(col, dict) 46 | and ( 47 | isinstance(list(col.values())[0], Callable) 48 | or list(col.values())[0] is None 49 | ) 50 | ) 51 | ] 52 | 53 | @property 54 | def _processed_columns(self) -> List[str]: 55 | """ 56 | Get the list of processed data columns, excluding any dicts with callable values 57 | """ 58 | return [ 59 | list(col.keys())[0] if isinstance(col, dict) else col 60 | for col in self._columns[self._first_met_col :] 61 | ] 62 | 63 | @property 64 | def _renamed_columns(self) -> Dict[str, str]: 65 | """ 66 | Get the dict of renamed data columns, including `_source` suffixes 67 | """ 68 | return { 69 | new_key: new_val 70 | for d in self._columns 71 | if isinstance(d, dict) 72 | for k, v in d.items() 73 | if not isinstance(v, Callable) 74 | for new_key, new_val in ((v, k), (f"{v}_source", f"{k}_source")) 75 | } 76 | 77 | @property 78 | def _virtual_columns(self) -> Dict[str, str]: 79 | """ 80 | Get the dict of virtual data columns 81 | """ 82 | return { 83 | k: v 84 | for d in self._columns 85 | if isinstance(d, dict) 86 | for k, v in d.items() 87 | if isinstance(v, Callable) 88 | } 89 | 90 | def _get_datasets(self) -> list: 91 | """ 92 | Get list of datasets 93 | """ 94 | 95 | if self.granularity in (Granularity.HOURLY, Granularity.DAILY): 96 | datasets = [ 97 | (str(station), year) 98 | for station in self._stations 99 | for year in self._annual_steps 100 | ] 101 | else: 102 | datasets = [(str(station),) for station in self._stations] 103 | 104 | return datasets 105 | 106 | def _get_data(self) -> None: 107 | """ 108 | Get all required data dumps 109 | """ 110 | 111 | if len(self._stations) > 0: 112 | # Get list of datasets 113 | datasets = self._get_datasets() 114 | 115 | # Data Processings 116 | return processing_handler( 117 | datasets, self._load_data, self.processes, self.threads 118 | ) 119 | 120 | # Empty DataFrame 121 | return pd.DataFrame(columns=[*self._types]) 122 | 123 | # pylint: disable=too-many-branches 124 | def _resolve_point( 125 | self, method: str, stations: pd.DataFrame, alt: int, adapt_temp: bool 126 | ) -> None: 127 | """ 128 | Project weather station data onto a single point 129 | """ 130 | 131 | if self._stations.size == 0 or self._data.size == 0: 132 | return 133 | 134 | if method == "nearest": 135 | if adapt_temp: 136 | # Join elevation of involved weather stations 137 | data = self._data.join(stations["elevation"], on="station") 138 | 139 | # Adapt temperature-like data based on altitude 140 | data = adjust_temp(data, alt) 141 | 142 | # Drop elevation & round 143 | data = data.drop("elevation", axis=1).round(1) 144 | 145 | else: 146 | data = self._data 147 | 148 | if self.granularity == Granularity.NORMALS: 149 | self._data = data.groupby(level=["start", "end", "month"]).agg("first") 150 | 151 | else: 152 | self._data = data.groupby( 153 | pd.Grouper(level="time", freq=self._freq) 154 | ).agg("first") 155 | 156 | else: 157 | # Join score and elevation of involved weather stations 158 | data = self._data.join(stations[["score", "elevation"]], on="station") 159 | 160 | # Adapt temperature-like data based on altitude 161 | if adapt_temp: 162 | data = adjust_temp(data, alt) 163 | 164 | # Exclude non-mean data & perform aggregation 165 | if not self.granularity == Granularity.NORMALS: 166 | excluded = data["wdir"] 167 | excluded = excluded.groupby( 168 | pd.Grouper(level="time", freq=self._freq) 169 | ).agg("first") 170 | 171 | # Aggregate mean data 172 | if self.granularity == Granularity.NORMALS: 173 | data = data.groupby(level=["start", "end", "month"]).apply( 174 | weighted_average 175 | ) 176 | 177 | # Remove obsolete index column 178 | try: 179 | data = data.reset_index(level=3, drop=True) 180 | except IndexError: 181 | pass 182 | 183 | else: 184 | data = data.groupby(pd.Grouper(level="time", freq=self._freq)).apply( 185 | weighted_average 186 | ) 187 | 188 | # Drop RangeIndex 189 | data.index = data.index.droplevel(1) 190 | 191 | # Merge excluded fields 192 | data["wdir"] = excluded 193 | 194 | # Drop score and elevation 195 | self._data = data.drop(["score", "elevation"], axis=1).round(1) 196 | 197 | # Set placeholder station ID 198 | self._data["station"] = "XXXXX" 199 | 200 | # Set index 201 | if self.granularity == Granularity.NORMALS: 202 | self._data = self._data.set_index("station", append=True) 203 | self._data = self._data.reorder_levels(["station", "start", "end", "month"]) 204 | else: 205 | self._data = self._data.set_index( 206 | ["station", self._data.index.get_level_values("time")] 207 | ) 208 | 209 | # Set station index 210 | self._stations = pd.Index(["XXXXX"]) 211 | -------------------------------------------------------------------------------- /meteostat/interface/monthly.py: -------------------------------------------------------------------------------- 1 | """ 2 | Monthly Class 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 datetime import datetime 12 | from typing import Union 13 | import pandas as pd 14 | from meteostat.enumerations.granularity import Granularity 15 | from meteostat.interface.timeseries import TimeSeries 16 | from meteostat.interface.point import Point 17 | 18 | 19 | class Monthly(TimeSeries): 20 | """ 21 | Retrieve monthly weather data for one or multiple weather stations or 22 | a single geographical point 23 | """ 24 | 25 | # The cache subdirectory 26 | cache_subdir = "monthly" 27 | 28 | # Granularity 29 | granularity = Granularity.MONTHLY 30 | 31 | # Default frequency 32 | _freq = "1MS" 33 | 34 | # Source mappings 35 | _source_mappings = { 36 | "dwd_monthly": "A", 37 | "eccc_monthly": "A", 38 | "dwd_daily": "C", 39 | "eccc_daily": "C", 40 | "ghcnd": "D", 41 | "dwd_hourly": "E", 42 | "eccc_hourly": "E", 43 | "isd_lite": "F", 44 | "synop": "G", 45 | "dwd_poi": "G", 46 | "metar": "H", 47 | "model": "I", 48 | "dwd_mosmix": "I", 49 | "metno_forecast": "I", 50 | } 51 | 52 | # Flag which represents model data 53 | _model_flag = "I" 54 | 55 | # Columns 56 | _columns = [ 57 | "year", 58 | "month", 59 | {"tavg": "temp"}, 60 | "tmin", 61 | "tmax", 62 | "prcp", 63 | "wspd", 64 | "pres", 65 | "tsun", 66 | ] 67 | 68 | # Index of first meteorological column 69 | _first_met_col = 2 70 | 71 | # Columns for date parsing 72 | _parse_dates = ["year", "month"] 73 | 74 | # Default aggregation functions 75 | aggregations = { 76 | "tavg": "mean", 77 | "tmin": "mean", 78 | "tmax": "mean", 79 | "prcp": "sum", 80 | "wspd": "mean", 81 | "pres": "mean", 82 | "tsun": "sum", 83 | } 84 | 85 | def __init__( 86 | self, 87 | loc: Union[pd.DataFrame, Point, list, str], # Station(s) or geo point 88 | start: datetime = None, 89 | end: datetime = None, 90 | model: bool = True, # Include model data? 91 | flags: bool = False, # Load source flags? 92 | ) -> None: 93 | # Set start date 94 | if start is not None: 95 | start = start.replace(day=1) 96 | 97 | # Initialize time series 98 | self._init_time_series(loc, start, end, model, flags) 99 | 100 | def expected_rows(self) -> int: 101 | """ 102 | Return the number of rows expected for the defined date range 103 | """ 104 | 105 | return ( 106 | (self._end.year - self._start.year) * 12 107 | + self._end.month 108 | - self._start.month 109 | ) + 1 110 | -------------------------------------------------------------------------------- /meteostat/interface/normals.py: -------------------------------------------------------------------------------- 1 | """ 2 | Normals Interface Class 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 copy import copy 12 | from typing import Optional, Union 13 | from datetime import datetime 14 | import numpy as np 15 | import pandas as pd 16 | from meteostat.core.cache import file_in_cache, get_local_file_path 17 | from meteostat.core.loader import load_handler 18 | from meteostat.utilities.endpoint import generate_endpoint_path 19 | from meteostat.enumerations.granularity import Granularity 20 | from meteostat.core.warn import warn 21 | from meteostat.interface.meteodata import MeteoData 22 | from meteostat.interface.point import Point 23 | 24 | 25 | class Normals(MeteoData): 26 | """ 27 | Retrieve climate normals for one or multiple weather stations or 28 | a single geographical point 29 | """ 30 | 31 | # The cache subdirectory 32 | cache_subdir = "normals" 33 | 34 | # Granularity 35 | granularity = Granularity.NORMALS 36 | 37 | # The list of weather Stations 38 | _stations: Optional[pd.Index] = None 39 | 40 | # The first year of the period 41 | _start: Optional[int] = None 42 | 43 | # The last year of the period 44 | _end: Optional[int] = None 45 | 46 | # The data frame 47 | _data: pd.DataFrame = pd.DataFrame() 48 | 49 | # Columns 50 | _columns = [ 51 | "start", 52 | "end", 53 | "month", 54 | "tmin", 55 | "tmax", 56 | "prcp", 57 | "wspd", 58 | "pres", 59 | "tsun", 60 | ] 61 | 62 | # Index of first meteorological column 63 | _first_met_col = 3 64 | 65 | # Which columns should be parsed as dates? 66 | _parse_dates = None 67 | 68 | def _load_data(self, station: str, year: Optional[int] = None) -> None: 69 | """ 70 | Load file for a single station from Meteostat 71 | """ 72 | 73 | # File name 74 | file = generate_endpoint_path(self.granularity, station, year) 75 | 76 | # Get local file path 77 | path = get_local_file_path(self.cache_dir, self.cache_subdir, file) 78 | 79 | # Check if file in cache 80 | if self.max_age > 0 and file_in_cache(path, self.max_age): 81 | # Read cached data 82 | df = pd.read_pickle(path) 83 | 84 | else: 85 | # Get data from Meteostat 86 | df = load_handler( 87 | self.endpoint, 88 | file, 89 | self.proxy, 90 | self._columns, 91 | ) 92 | 93 | # Validate and prepare data for further processing 94 | if not df.empty: 95 | # Add weather station ID 96 | df["station"] = station 97 | 98 | # Set index 99 | df = df.set_index(["station", "start", "end", "month"]) 100 | 101 | # Save as Pickle 102 | if self.max_age > 0: 103 | df.to_pickle(path) 104 | 105 | # Filter time period and append to DataFrame 106 | if self.granularity == Granularity.NORMALS and not df.empty and self._end: 107 | # Get time index 108 | end = df.index.get_level_values("end") 109 | # Filter & return 110 | return df.loc[end == self._end] 111 | 112 | # Return 113 | return df 114 | 115 | def __init__( 116 | self, 117 | loc: Union[pd.DataFrame, Point, list, str], 118 | start: int = None, 119 | end: int = None, 120 | ) -> None: 121 | # Set list of weather stations 122 | if isinstance(loc, pd.DataFrame): 123 | self._stations = loc.index 124 | 125 | elif isinstance(loc, Point): 126 | if start and end: 127 | stations = loc.get_stations( 128 | "monthly", datetime(start, 1, 1), datetime(end, 12, 31) 129 | ) 130 | else: 131 | stations = loc.get_stations() 132 | 133 | self._stations = stations.index 134 | 135 | else: 136 | if not isinstance(loc, list): 137 | loc = [loc] 138 | 139 | self._stations = pd.Index(loc) 140 | 141 | # Check period 142 | if (start and end) and ( 143 | end - start != 29 or end % 10 != 0 or end >= datetime.now().year 144 | ): 145 | raise ValueError("Invalid reference period") 146 | 147 | # Set period 148 | self._start = start 149 | self._end = end 150 | 151 | # Get data for all weather stations 152 | self._data = self._get_data() 153 | 154 | # Interpolate data 155 | if isinstance(loc, Point): 156 | self._resolve_point(loc.method, stations, loc.alt, loc.adapt_temp) 157 | 158 | # Clear cache 159 | if self.max_age > 0 and self.autoclean: 160 | self.clear_cache() 161 | 162 | def normalize(self): 163 | """ 164 | Normalize the DataFrame 165 | """ 166 | 167 | # Create temporal instance 168 | temp = copy(self) 169 | 170 | if self.count() == 0: 171 | warn("Pointless normalization of empty DataFrame") 172 | 173 | # Go through list of weather stations 174 | for station in temp._stations: 175 | # The list of periods 176 | periods: pd.Index = pd.Index([]) 177 | # Get periods 178 | if self.count() > 0: 179 | periods = temp._data[ 180 | temp._data.index.get_level_values("station") == station 181 | ].index.unique("end") 182 | elif periods.size == 0 and self._end: 183 | periods = pd.Index([self._end]) 184 | # Go through all periods 185 | for period in periods: 186 | # Create DataFrame 187 | df = pd.DataFrame( 188 | columns=temp._columns[temp._first_met_col :], dtype="float64" 189 | ) 190 | # Populate index columns 191 | df["month"] = range(1, 13) 192 | df["station"] = station 193 | df["start"] = period - 29 194 | df["end"] = period 195 | # Set index 196 | df.set_index(["station", "start", "end", "month"], inplace=True) 197 | # Merge data 198 | temp._data = ( 199 | pd.concat([temp._data, df], axis=0) 200 | .groupby(["station", "start", "end", "month"], as_index=True) 201 | .first() 202 | if temp._data.index.size > 0 203 | else df 204 | ) 205 | 206 | # None -> nan 207 | temp._data = temp._data.fillna(np.nan) 208 | 209 | # Return class instance 210 | return temp 211 | 212 | def fetch(self) -> pd.DataFrame: 213 | """ 214 | Fetch DataFrame 215 | """ 216 | 217 | # Copy DataFrame 218 | temp = copy(self._data) 219 | 220 | # Add avg. temperature column 221 | temp.insert( 222 | 0, "tavg", temp[["tmin", "tmax"]].dropna(how="any").mean(axis=1).round(1) 223 | ) 224 | 225 | # Remove station index if it's a single station 226 | if len(self._stations) == 1 and "station" in temp.index.names: 227 | temp = temp.reset_index(level="station", drop=True) 228 | 229 | # Remove start & end year if period is set 230 | if self._start and self._end and self.count() > 0: 231 | temp = temp.reset_index(level="start", drop=True) 232 | temp = temp.reset_index(level="end", drop=True) 233 | 234 | # Return data frame 235 | return temp 236 | 237 | # Import methods 238 | from meteostat.series.convert import convert 239 | from meteostat.series.count import count 240 | from meteostat.core.cache import clear_cache 241 | -------------------------------------------------------------------------------- /meteostat/interface/point.py: -------------------------------------------------------------------------------- 1 | """ 2 | Point Class 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 datetime import datetime 12 | import pandas as pd 13 | from meteostat.interface.stations import Stations 14 | 15 | 16 | class Point: 17 | """ 18 | Automatically select weather stations by geographic location 19 | """ 20 | 21 | # The interpolation method (weighted or nearest) 22 | method: str = "nearest" 23 | 24 | # Maximum radius for nearby stations 25 | radius: int = 35000 26 | 27 | # Maximum difference in altitude 28 | alt_range: int = 350 29 | 30 | # Maximum number of stations 31 | max_count: int = 4 32 | 33 | # Adapt temperature data based on altitude 34 | adapt_temp: bool = True 35 | 36 | # Distance Weight 37 | weight_dist: float = 0.6 38 | 39 | # Altitude Weight 40 | weight_alt: float = 0.4 41 | 42 | # The list of weather stations 43 | _stations: pd.Index = None 44 | 45 | # The latitude 46 | _lat: float = None 47 | 48 | # The longitude 49 | _lon: float = None 50 | 51 | # The altitude 52 | _alt: int = None 53 | 54 | def __init__(self, lat: float, lon: float, alt: int = None) -> None: 55 | self._lat = lat 56 | self._lon = lon 57 | self._alt = alt 58 | 59 | if alt is None: 60 | self.adapt_temp = False 61 | 62 | def get_stations( 63 | self, 64 | freq: str = None, 65 | start: datetime = None, 66 | end: datetime = None, 67 | model: bool = True, 68 | ) -> pd.DataFrame: 69 | """ 70 | Get list of nearby weather stations 71 | """ 72 | 73 | # Get nearby weather stations 74 | stations = Stations() 75 | stations = stations.nearby(self._lat, self._lon, self.radius) 76 | 77 | # Guess altitude if not set 78 | if self._alt is None: 79 | self._alt = stations.fetch().head(self.max_count)["elevation"].mean() 80 | 81 | # Captue unfiltered weather stations 82 | unfiltered = stations.fetch() 83 | if self.alt_range: 84 | unfiltered = unfiltered[ 85 | abs(self._alt - unfiltered["elevation"]) <= self.alt_range 86 | ] 87 | 88 | # Apply inventory filter 89 | if freq and start and end: 90 | age = (datetime.now() - end).days 91 | if model is False or age > 180: 92 | stations = stations.inventory(freq, (start, end)) 93 | 94 | # Apply altitude filter 95 | stations = stations.fetch() 96 | if self.alt_range: 97 | stations = stations[ 98 | abs(self._alt - stations["elevation"]) <= self.alt_range 99 | ] 100 | 101 | # Fill up stations 102 | selected: int = len(stations.index) 103 | if selected < self.max_count: 104 | # Remove already included stations from unfiltered 105 | unfiltered = unfiltered.loc[~unfiltered.index.isin(stations.index)] 106 | # Append to existing DataFrame 107 | stations = pd.concat((stations, unfiltered.head(self.max_count - selected))) 108 | 109 | # Score values 110 | if self.radius: 111 | # Calculate score values 112 | stations["score"] = ( 113 | (1 - (stations["distance"] / self.radius)) * self.weight_dist 114 | ) + ( 115 | (1 - (abs(self._alt - stations["elevation"]) / self.alt_range)) 116 | * self.weight_alt 117 | ) 118 | 119 | # Sort by score (descending) 120 | stations = stations.sort_values("score", ascending=False) 121 | 122 | # Capture result 123 | self._stations = stations.index[: self.max_count] 124 | 125 | return stations.head(self.max_count) 126 | 127 | @property 128 | def alt(self) -> int: 129 | """ 130 | Returns the point's altitude 131 | """ 132 | 133 | # Return altitude 134 | return self._alt 135 | 136 | @property 137 | def stations(self) -> pd.Index: 138 | """ 139 | Returns the point's weather stations 140 | """ 141 | 142 | # Return weather stations 143 | return self._stations 144 | -------------------------------------------------------------------------------- /meteostat/interface/stations.py: -------------------------------------------------------------------------------- 1 | """ 2 | Stations Class 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 copy import copy 12 | from datetime import datetime, timedelta 13 | from typing import Union 14 | import pandas as pd 15 | from meteostat.core.cache import get_local_file_path, file_in_cache 16 | from meteostat.core.loader import load_handler 17 | from meteostat.interface.base import Base 18 | from meteostat.utilities.helpers import get_distance 19 | 20 | 21 | class Stations(Base): 22 | """ 23 | Select weather stations from the full list of stations 24 | """ 25 | 26 | # The cache subdirectory 27 | cache_subdir: str = "stations" 28 | 29 | # The list of selected weather Stations 30 | _data: pd.DataFrame = None 31 | 32 | # Raw data columns 33 | _columns: list = [ 34 | "id", 35 | "name", 36 | "country", 37 | "region", 38 | "wmo", 39 | "icao", 40 | "latitude", 41 | "longitude", 42 | "elevation", 43 | "timezone", 44 | "hourly_start", 45 | "hourly_end", 46 | "daily_start", 47 | "daily_end", 48 | "monthly_start", 49 | "monthly_end", 50 | ] 51 | 52 | # Processed data columns with types 53 | _types: dict = { 54 | "id": "string", 55 | "name": "object", 56 | "country": "string", 57 | "region": "string", 58 | "wmo": "string", 59 | "icao": "string", 60 | "latitude": "float64", 61 | "longitude": "float64", 62 | "elevation": "float64", 63 | "timezone": "string", 64 | } 65 | 66 | # Columns for date parsing 67 | _parse_dates: list = [10, 11, 12, 13, 14, 15] 68 | 69 | def _load(self) -> None: 70 | """ 71 | Load file from Meteostat 72 | """ 73 | 74 | # File name 75 | file = "stations/slim.csv.gz" 76 | 77 | # Get local file path 78 | path = get_local_file_path(self.cache_dir, self.cache_subdir, file) 79 | 80 | # Check if file in cache 81 | if self.max_age > 0 and file_in_cache(path, self.max_age): 82 | # Read cached data 83 | df = pd.read_pickle(path) 84 | 85 | else: 86 | # Get data from Meteostat 87 | df = load_handler( 88 | self.endpoint, 89 | file, 90 | self.proxy, 91 | self._columns, 92 | self._types, 93 | self._parse_dates, 94 | ) 95 | 96 | # Add index 97 | df = df.set_index("id") 98 | 99 | # Save as Pickle 100 | if self.max_age > 0: 101 | df.to_pickle(path) 102 | 103 | # Set data 104 | self._data = df 105 | 106 | def __init__(self) -> None: 107 | # Get all weather stations 108 | self._load() 109 | 110 | def nearby(self, lat: float, lon: float, radius: int = None) -> "Stations": 111 | """ 112 | Sort/filter weather stations by physical distance 113 | """ 114 | 115 | # Create temporal instance 116 | temp = copy(self) 117 | 118 | # Get distance for each station 119 | temp._data["distance"] = get_distance( 120 | lat, lon, temp._data["latitude"], temp._data["longitude"] 121 | ) 122 | 123 | # Filter by radius 124 | if radius: 125 | temp._data = temp._data[temp._data["distance"] <= radius] 126 | 127 | # Sort stations by distance 128 | temp._data.columns.str.strip() 129 | temp._data = temp._data.sort_values("distance") 130 | 131 | # Return self 132 | return temp 133 | 134 | def region(self, country: str, state: str = None) -> "Stations": 135 | """ 136 | Filter weather stations by country/region code 137 | """ 138 | 139 | # Create temporal instance 140 | temp = copy(self) 141 | 142 | # Country code 143 | temp._data = temp._data[temp._data["country"] == country] 144 | 145 | # State code 146 | if state is not None: 147 | temp._data = temp._data[temp._data["region"] == state] 148 | 149 | # Return self 150 | return temp 151 | 152 | def bounds(self, top_left: tuple, bottom_right: tuple) -> "Stations": 153 | """ 154 | Filter weather stations by geographical bounds 155 | """ 156 | 157 | # Create temporal instance 158 | temp = copy(self) 159 | 160 | # Return stations in boundaries 161 | temp._data = temp._data[ 162 | (temp._data["latitude"] <= top_left[0]) 163 | & (temp._data["latitude"] >= bottom_right[0]) 164 | & (temp._data["longitude"] <= bottom_right[1]) 165 | & (temp._data["longitude"] >= top_left[1]) 166 | ] 167 | 168 | # Return self 169 | return temp 170 | 171 | def inventory( 172 | self, freq: str, required: Union[datetime, tuple, bool] = True 173 | ) -> "Stations": 174 | """ 175 | Filter weather stations by inventory data 176 | """ 177 | 178 | # Create temporal instance 179 | temp = copy(self) 180 | 181 | if required is True: 182 | # Make sure data exists at all 183 | temp._data = temp._data[~pd.isna(temp._data[f"{freq}_start"])] 184 | 185 | elif isinstance(required, tuple): 186 | # Make sure data exists across period 187 | temp._data = temp._data[ 188 | (~pd.isna(temp._data[f"{freq}_start"])) 189 | & (temp._data[freq + "_start"] <= required[0]) 190 | & ( 191 | temp._data[freq + "_end"] + timedelta(seconds=temp.max_age) 192 | >= required[1] 193 | ) 194 | ] 195 | 196 | else: 197 | # Make sure data exists on a certain day 198 | temp._data = temp._data[ 199 | (~pd.isna(temp._data[f"{freq}_start"])) 200 | & (temp._data[freq + "_start"] <= required) 201 | & ( 202 | temp._data[freq + "_end"] + timedelta(seconds=temp.max_age) 203 | >= required 204 | ) 205 | ] 206 | 207 | return temp 208 | 209 | def convert(self, units: dict) -> "Stations": 210 | """ 211 | Convert columns to a different unit 212 | """ 213 | 214 | # Create temporal instance 215 | temp = copy(self) 216 | 217 | # Change data units 218 | for parameter, unit in units.items(): 219 | if parameter in temp._data.columns.values: 220 | temp._data[parameter] = temp._data[parameter].apply(unit) 221 | 222 | # Return class instance 223 | return temp 224 | 225 | def count(self) -> int: 226 | """ 227 | Return number of weather stations in current selection 228 | """ 229 | 230 | return len(self._data.index) 231 | 232 | def fetch(self, limit: int = None, sample: bool = False) -> pd.DataFrame: 233 | """ 234 | Fetch all weather stations or a (sampled) subset 235 | """ 236 | 237 | # Copy DataFrame 238 | temp = copy(self._data) 239 | 240 | # Return limited number of sampled entries 241 | if sample and limit: 242 | return temp.sample(limit) 243 | 244 | # Return limited number of entries 245 | if limit: 246 | return temp.head(limit) 247 | 248 | # Return all entries 249 | return temp 250 | 251 | # Import additional methods 252 | from meteostat.core.cache import clear_cache 253 | -------------------------------------------------------------------------------- /meteostat/interface/timeseries.py: -------------------------------------------------------------------------------- 1 | """ 2 | TimeSeries Class 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 datetime import datetime 12 | from typing import Optional, Union 13 | import pandas as pd 14 | from meteostat.core.cache import file_in_cache, get_local_file_path 15 | from meteostat.core.loader import load_handler 16 | from meteostat.enumerations.granularity import Granularity 17 | from meteostat.utilities.endpoint import generate_endpoint_path 18 | from meteostat.utilities.mutations import filter_time, localize 19 | from meteostat.utilities.validations import validate_series 20 | from meteostat.utilities.helpers import get_flag_from_source_factory, with_suffix 21 | from meteostat.interface.point import Point 22 | from meteostat.interface.meteodata import MeteoData 23 | 24 | 25 | class TimeSeries(MeteoData): 26 | """ 27 | TimeSeries class which provides features which are 28 | used across all time series classes 29 | """ 30 | 31 | # Base URL of the Meteostat bulk data interface 32 | endpoint = "https://data.meteostat.net/" 33 | 34 | # The list of origin weather Stations 35 | _origin_stations: Optional[pd.Index] = None 36 | 37 | # The start date 38 | _start: Optional[datetime] = None 39 | 40 | # The end date 41 | _end: Optional[datetime] = None 42 | 43 | # Include model data? 44 | _model = True 45 | 46 | # Fetch source flags? 47 | _flags = False 48 | 49 | def _load_data(self, station: str, year: Optional[int] = None) -> None: 50 | """ 51 | Load file for a single station from Meteostat 52 | """ 53 | # File name 54 | file = generate_endpoint_path(self.granularity, station, year) 55 | 56 | # Get local file path 57 | path = get_local_file_path(self.cache_dir, self.cache_subdir, file) 58 | 59 | # Check if file in cache 60 | if self.max_age > 0 and file_in_cache(path, self.max_age): 61 | # Read cached data 62 | df = pd.read_pickle(path) 63 | 64 | else: 65 | # Get data from Meteostat 66 | df = load_handler( 67 | self.endpoint, 68 | file, 69 | self.proxy, 70 | default_df=pd.DataFrame( 71 | columns=self._raw_columns 72 | + with_suffix(self._raw_columns, "_source") 73 | ), 74 | ) 75 | 76 | # Add time column and drop original columns 77 | if len(self._parse_dates) < 3: 78 | df["day"] = 1 79 | 80 | df["time"] = pd.to_datetime( 81 | df[ 82 | ( 83 | self._parse_dates 84 | if len(self._parse_dates) > 2 85 | else self._parse_dates + ["day"] 86 | ) 87 | ] 88 | ) 89 | df = df.drop(self._parse_dates, axis=1) 90 | 91 | # Validate and prepare data for further processing 92 | df = validate_series(df, station) 93 | 94 | # Rename columns 95 | df = df.rename(columns=self._renamed_columns, errors="ignore") 96 | 97 | # Convert sources to flags 98 | for col in df.columns: 99 | basecol = col[:-7] if col.endswith("_source") else col 100 | 101 | if basecol not in self._processed_columns: 102 | df.drop(col, axis=1, inplace=True) 103 | continue 104 | 105 | if basecol == col: 106 | df[col] = df[col].astype("Float64") 107 | 108 | if col.endswith("_source"): 109 | flagcol = f"{basecol}_flag" 110 | df[flagcol] = pd.NA 111 | df[flagcol] = df[flagcol].astype("string") 112 | mask = df[col].notna() 113 | df.loc[mask, flagcol] = df.loc[mask, col].apply( 114 | get_flag_from_source_factory( 115 | self._source_mappings, self._model_flag 116 | ) 117 | ) 118 | df.drop(col, axis=1, inplace=True) 119 | 120 | # Process virtual columns 121 | for key, value in self._virtual_columns.items(): 122 | df = value(df, key) 123 | 124 | # Save as Pickle 125 | if self.max_age > 0: 126 | df.to_pickle(path) 127 | 128 | # Localize time column 129 | if ( 130 | self.granularity == Granularity.HOURLY 131 | and self._timezone is not None 132 | and len(df.index) > 0 133 | ): 134 | df = localize(df, self._timezone) 135 | 136 | # Filter time period and append to DataFrame 137 | df = filter_time(df, self._start, self._end) 138 | 139 | # Return 140 | return df 141 | 142 | def _filter_model(self) -> None: 143 | """ 144 | Remove model data from time series 145 | """ 146 | 147 | for col_name in self._processed_columns: 148 | self._data.loc[ 149 | (pd.isna(self._data[f"{col_name}_flag"])) 150 | | (self._data[f"{col_name}_flag"].str.contains(self._model_flag)), 151 | col_name, 152 | ] = pd.NA 153 | 154 | # Drop nan-only rows 155 | self._data.dropna(how="all", subset=self._processed_columns, inplace=True) 156 | 157 | def _init_time_series( 158 | self, 159 | loc: Union[pd.DataFrame, Point, list, str], # Station(s) or geo point 160 | start: datetime = None, 161 | end: datetime = None, 162 | model=True, # Include model data? 163 | flags=False, # Load source flags? 164 | ) -> None: 165 | """ 166 | Common initialization for all time series, regardless 167 | of its granularity 168 | """ 169 | 170 | # Set list of weather stations based on user 171 | # input or retrieve list of stations programatically 172 | # if location is a geographical point 173 | if isinstance(loc, pd.DataFrame): 174 | self._stations = loc.index 175 | elif isinstance(loc, Point): 176 | stations = loc.get_stations("daily", start, end, model) 177 | self._stations = stations.index 178 | else: 179 | if not isinstance(loc, list): 180 | loc = [loc] 181 | self._stations = pd.Index(loc) 182 | 183 | # Preserve settings 184 | self._start = start if self._start is None else self._start 185 | self._end = end if self._end is None else self._end 186 | self._model = model 187 | self._flags = flags 188 | 189 | # Get data for all weather stations 190 | self._data = self._get_data() 191 | 192 | # Fill columns if they don't exist 193 | for col in self._processed_columns: 194 | if col not in self._data.columns: 195 | self._data[col] = pd.NA 196 | self._data[col] = self._data[col].astype("Float64") 197 | self._data[f"{col}_flag"] = pd.NA 198 | self._data[f"{col}_flag"] = self._data[f"{col}_flag"].astype("string") 199 | 200 | # Reorder the DataFrame 201 | self._data = self._data[ 202 | self._processed_columns + with_suffix(self._processed_columns, "_flag") 203 | ] 204 | 205 | # Remove model data from DataFrame 206 | if not model: 207 | self._filter_model() 208 | 209 | # Conditionally, remove flags from DataFrame 210 | if not self._flags: 211 | self._data.drop( 212 | with_suffix(self._processed_columns, "_flag"), 213 | axis=1, 214 | errors="ignore", 215 | inplace=True, 216 | ) 217 | 218 | # Interpolate data spatially if requested 219 | # location is a geographical point 220 | if isinstance(loc, Point): 221 | self._resolve_point(loc.method, stations, loc.alt, loc.adapt_temp) 222 | 223 | # Clear cache if auto cleaning is enabled 224 | if self.max_age > 0 and self.autoclean: 225 | self.clear_cache() 226 | 227 | # Import methods 228 | from meteostat.series.normalize import normalize 229 | from meteostat.series.interpolate import interpolate 230 | from meteostat.series.aggregate import aggregate 231 | from meteostat.series.convert import convert 232 | from meteostat.series.coverage import coverage 233 | from meteostat.series.count import count 234 | from meteostat.series.fetch import fetch 235 | from meteostat.series.stations import stations 236 | from meteostat.core.cache import clear_cache 237 | -------------------------------------------------------------------------------- /meteostat/series/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteostat/meteostat-python/ba9653a4244c2cd72e7d926c29c738d18fa61258/meteostat/series/__init__.py -------------------------------------------------------------------------------- /meteostat/series/aggregate.py: -------------------------------------------------------------------------------- 1 | """ 2 | Aggregate Data 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 copy import copy 12 | import pandas as pd 13 | from meteostat.core.warn import warn 14 | 15 | 16 | def aggregate(self, freq: str = None, spatial: bool = False): 17 | """ 18 | Aggregate observations 19 | """ 20 | 21 | if self.count() > 0 and not self._data.isnull().values.all(): 22 | # Create temporal instance 23 | temp = copy(self) 24 | 25 | # Set default frequency if not set 26 | if freq is None: 27 | freq = self._freq 28 | 29 | # Time aggregation 30 | temp._data = temp._data.groupby( 31 | ["station", pd.Grouper(level="time", freq=freq)] 32 | ).agg(temp.aggregations) 33 | 34 | # Spatial aggregation 35 | if spatial: 36 | temp._data = temp._data.groupby( 37 | [pd.Grouper(level="time", freq=freq)] 38 | ).mean() 39 | 40 | # Round 41 | temp._data = temp._data.round(1) 42 | 43 | # Return class instance 44 | return temp 45 | 46 | # Show warning & return self 47 | warn("Skipping aggregation on empty DataFrame") 48 | return self 49 | -------------------------------------------------------------------------------- /meteostat/series/convert.py: -------------------------------------------------------------------------------- 1 | """ 2 | Convert Data Units 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 copy import copy 12 | 13 | 14 | def convert(self, units: dict): 15 | """ 16 | Convert columns to a different unit 17 | """ 18 | 19 | # Create temporal instance 20 | temp = copy(self) 21 | 22 | # Change data units 23 | for parameter, unit in units.items(): 24 | if parameter in temp._columns: 25 | temp._data[parameter] = temp._data[parameter].apply(unit) 26 | 27 | # Return class instance 28 | return temp 29 | -------------------------------------------------------------------------------- /meteostat/series/count.py: -------------------------------------------------------------------------------- 1 | """ 2 | Get Number Of Rows 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 | 12 | def count(self) -> int: 13 | """ 14 | Return number of rows in DataFrame 15 | """ 16 | 17 | return len(self._data.index) 18 | -------------------------------------------------------------------------------- /meteostat/series/coverage.py: -------------------------------------------------------------------------------- 1 | """ 2 | Calculate Data Coverage 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 | 12 | def coverage(self, parameter: str = None) -> float: 13 | """ 14 | Calculate data coverage (overall or by parameter) 15 | """ 16 | 17 | if parameter is None: 18 | return len(self._data.index) / self.expected_rows() 19 | 20 | return round(self._data[parameter].count() / self.expected_rows(), 2) 21 | -------------------------------------------------------------------------------- /meteostat/series/fetch.py: -------------------------------------------------------------------------------- 1 | """ 2 | Fetch Data 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 copy import copy 12 | import pandas as pd 13 | 14 | 15 | def fetch(self) -> pd.DataFrame: 16 | """ 17 | Fetch DataFrame 18 | """ 19 | 20 | # Copy DataFrame 21 | temp = copy(self._data) 22 | 23 | # Remove station index if it's a single station 24 | if len(self._stations) == 1 and "station" in temp.index.names: 25 | temp = temp.reset_index(level="station", drop=True) 26 | 27 | # Return data frame 28 | return temp 29 | -------------------------------------------------------------------------------- /meteostat/series/interpolate.py: -------------------------------------------------------------------------------- 1 | """ 2 | Interpolate Data 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 copy import copy 12 | import numpy as np 13 | from meteostat.core.warn import warn 14 | 15 | 16 | def interpolate(self, limit: int = 3): 17 | """ 18 | Interpolate NULL values 19 | """ 20 | 21 | if self.count() > 0 and not self._data.isnull().values.all(): 22 | # Create temporal instance 23 | temp = copy(self) 24 | 25 | # Convert to float64 26 | temp._data = temp._data.astype("float64") 27 | 28 | # Apply interpolation 29 | temp._data = temp._data.groupby("station", group_keys=False).apply( 30 | lambda group: group.interpolate( 31 | method="linear", 32 | limit=limit, 33 | limit_direction="both", 34 | axis=0, 35 | fill_value=np.nan, 36 | ) 37 | ) 38 | 39 | # Convert to original type 40 | temp._data = temp._data.astype("Float64") 41 | 42 | # Return class instance 43 | return temp 44 | 45 | # Show warning & return self 46 | warn("Skipping interpolation on empty DataFrame") 47 | return self 48 | -------------------------------------------------------------------------------- /meteostat/series/normalize.py: -------------------------------------------------------------------------------- 1 | """ 2 | Normalize Data 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 copy import copy 12 | from numpy import nan 13 | import pandas as pd 14 | import pytz 15 | from meteostat.core.warn import warn 16 | 17 | 18 | def normalize(self): 19 | """ 20 | Normalize the DataFrame 21 | """ 22 | 23 | if self.count() == 0: 24 | warn("Pointless normalization of empty DataFrame") 25 | 26 | # Create temporal instance 27 | temp = copy(self) 28 | 29 | if temp._start and temp._end and temp.coverage() < 1: 30 | # Create result DataFrame 31 | result = pd.DataFrame(columns=temp._processed_columns, dtype="Float64") 32 | 33 | # Handle tz-aware date ranges 34 | if hasattr(temp, "_timezone") and temp._timezone is not None: 35 | timezone = pytz.timezone(temp._timezone) 36 | start = temp._start.astimezone(timezone) 37 | end = temp._end.astimezone(timezone) 38 | else: 39 | start = temp._start 40 | end = temp._end 41 | 42 | # Go through list of weather stations 43 | for station in temp._stations: 44 | # Create data frame 45 | df = pd.DataFrame(columns=temp._processed_columns, dtype="Float64") 46 | # Add time series 47 | df["time"] = pd.date_range( 48 | start, 49 | end, 50 | freq=self._freq, 51 | tz=temp._timezone if hasattr(temp, "_timezone") else None, 52 | ) 53 | # Add station ID 54 | df["station"] = station 55 | # Add columns 56 | for column in temp._processed_columns: 57 | # Add column to DataFrame 58 | df[column] = nan 59 | 60 | result = pd.concat([result, df], axis=0) 61 | 62 | # Set index 63 | result = result.set_index(["station", "time"]) 64 | 65 | # Merge data 66 | temp._data = ( 67 | pd.concat([temp._data, result], axis=0) 68 | .groupby(["station", "time"], as_index=True) 69 | .first() 70 | ) 71 | 72 | # None -> nan 73 | temp._data = temp._data.fillna(pd.NA) 74 | 75 | # Return class instance 76 | return temp 77 | -------------------------------------------------------------------------------- /meteostat/series/stations.py: -------------------------------------------------------------------------------- 1 | """ 2 | Get Weather Stations 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 copy import copy 12 | import pandas as pd 13 | 14 | 15 | @property 16 | def stations(self) -> pd.Index: 17 | """ 18 | Fetch Weather Stations 19 | """ 20 | 21 | # Return index of weather stations 22 | return copy(self._stations) 23 | -------------------------------------------------------------------------------- /meteostat/units.py: -------------------------------------------------------------------------------- 1 | """ 2 | Meteorological Data Units 3 | 4 | Convert a Pandas Series to any meteorological data unit 5 | 6 | The code is licensed under the MIT license. 7 | """ 8 | 9 | from numpy import nan, isnan 10 | 11 | 12 | def fahrenheit(value): 13 | """ 14 | Convert Celsius to Fahrenheit 15 | """ 16 | 17 | return round((value * 9 / 5) + 32, 1) 18 | 19 | 20 | def kelvin(value): 21 | """ 22 | Convert Celsius to Kelvin 23 | """ 24 | 25 | return round(value + 273.15, 1) 26 | 27 | 28 | def inches(value): 29 | """ 30 | Convert millimeters to inches 31 | """ 32 | 33 | return round(value / 25.4, 3) 34 | 35 | 36 | def feet(value): 37 | """ 38 | Convert meters to feet 39 | """ 40 | 41 | return round(value / 0.3048, 1) 42 | 43 | 44 | def ms(value): 45 | """ 46 | Convert kilometers per hour to meters per second 47 | """ 48 | 49 | return round(value / 3.6, 1) 50 | 51 | 52 | def mph(value): 53 | """ 54 | Convert kilometers per hour to miles per hour 55 | """ 56 | 57 | return round(value * 0.6214, 1) 58 | 59 | 60 | def direction(value): 61 | """ 62 | Convert degrees to wind direction 63 | """ 64 | 65 | wdir = nan 66 | 67 | if (337 <= value <= 360) or value <= 23: 68 | wdir = "N" 69 | if 24 <= value <= 68: 70 | wdir = "NE" 71 | if 69 <= value <= 113: 72 | wdir = "E" 73 | if 114 <= value <= 158: 74 | wdir = "SE" 75 | if 159 <= value <= 203: 76 | wdir = "S" 77 | if 204 <= value <= 248: 78 | wdir = "SW" 79 | if 249 <= value <= 293: 80 | wdir = "W" 81 | if 294 <= value <= 336: 82 | wdir = "NW" 83 | 84 | return wdir 85 | 86 | 87 | def condition(value): 88 | """ 89 | Convert Meteostat condition code to descriptive string 90 | """ 91 | 92 | if isnan(value) or value < 1 or value > 27: 93 | return nan 94 | 95 | return [ 96 | "Clear", 97 | "Fair", 98 | "Cloudy", 99 | "Overcast", 100 | "Fog", 101 | "Freezing Fog", 102 | "Light Rain", 103 | "Rain", 104 | "Heavy Rain", 105 | "Freezing Rain", 106 | "Heavy Freezing Rain", 107 | "Sleet", 108 | "Heavy Sleet", 109 | "Light Snowfall", 110 | "Snowfall", 111 | "Heavy Snowfall", 112 | "Rain Shower", 113 | "Heavy Rain Shower", 114 | "Sleet Shower", 115 | "Heavy Sleet Shower", 116 | "Snow Shower", 117 | "Heavy Snow Shower", 118 | "Lightning", 119 | "Hail", 120 | "Thunderstorm", 121 | "Heavy Thunderstorm", 122 | "Storm", 123 | ][int(value) - 1] 124 | 125 | 126 | # Imperial units 127 | imperial = { 128 | "temp": fahrenheit, 129 | "tavg": fahrenheit, 130 | "tmin": fahrenheit, 131 | "tmax": fahrenheit, 132 | "dwpt": fahrenheit, 133 | "prcp": inches, 134 | "snow": inches, 135 | "wspd": mph, 136 | "wpgt": mph, 137 | "distance": feet, 138 | } 139 | 140 | # Scientific units 141 | scientific = { 142 | "temp": kelvin, 143 | "tavg": kelvin, 144 | "tmin": kelvin, 145 | "tmax": kelvin, 146 | "dwpt": kelvin, 147 | "wspd": ms, 148 | "wpgt": ms, 149 | } 150 | -------------------------------------------------------------------------------- /meteostat/utilities/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteostat/meteostat-python/ba9653a4244c2cd72e7d926c29c738d18fa61258/meteostat/utilities/__init__.py -------------------------------------------------------------------------------- /meteostat/utilities/aggregations.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utilities - Aggregation Methods 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 | import numpy as np 12 | import pandas as pd 13 | 14 | 15 | def weighted_average(step: pd.DataFrame) -> pd.DataFrame: 16 | """ 17 | Calculate weighted average from grouped data 18 | """ 19 | 20 | data = np.ma.masked_array(step, np.isnan(step)) 21 | data = np.ma.average(data, axis=0, weights=data[:, -2]) 22 | data = data.filled(np.nan) 23 | 24 | return pd.DataFrame(data=[data], columns=step.columns) 25 | 26 | 27 | def degree_mean(data: pd.Series) -> float: 28 | """ 29 | Return the mean of a list of degrees 30 | """ 31 | 32 | if data.isnull().all(): 33 | return np.nan 34 | 35 | rads = np.deg2rad(data) 36 | sums = np.arctan2(np.sum(np.sin(rads)), np.sum(np.cos(rads))) 37 | return (np.rad2deg(sums) + 360) % 360 38 | -------------------------------------------------------------------------------- /meteostat/utilities/endpoint.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utilities - Endpoint Helpers 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 typing import Union 12 | from meteostat.enumerations.granularity import Granularity 13 | 14 | 15 | def generate_endpoint_path( 16 | granularity: Granularity, 17 | station: str, 18 | year: Union[int, None] = None, 19 | map_file: bool = False, # Is a source map file? 20 | ) -> str: 21 | """ 22 | Generate Meteostat Bulk path 23 | """ 24 | 25 | # Base path 26 | path = f"{granularity.value}/" 27 | 28 | if granularity in (Granularity.HOURLY, Granularity.DAILY) and year: 29 | path += f"{year}/" 30 | 31 | appendix = ".map" if map_file else "" 32 | 33 | return f"{path}{station}{appendix}.csv.gz" 34 | -------------------------------------------------------------------------------- /meteostat/utilities/helpers.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utilities - Helpers 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 typing import Optional 12 | import numpy as np 13 | 14 | 15 | def get_distance(lat1, lon1, lat2, lon2) -> float: 16 | """ 17 | Calculate distance between weather station and geo point 18 | """ 19 | # Earth radius in meters 20 | radius = 6371000 21 | 22 | # Degress to radian 23 | lat1, lon1, lat2, lon2 = map(np.deg2rad, [lat1, lon1, lat2, lon2]) 24 | 25 | # Deltas 26 | dlat = lat2 - lat1 27 | dlon = lon2 - lon1 28 | 29 | # Calculate distance 30 | arch = np.sin(dlat / 2) ** 2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon / 2) ** 2 31 | arch_sin = 2 * np.arcsin(np.sqrt(arch)) 32 | 33 | return radius * arch_sin 34 | 35 | 36 | def _get_flag_from_single_source( 37 | source: str, source_mappings: dict, model_flag: str 38 | ) -> str: 39 | """ 40 | Get flag from single source 41 | """ 42 | if source in source_mappings: 43 | return source_mappings[source] 44 | return model_flag 45 | 46 | 47 | def get_flag_from_source_factory(source_mappings: dict, model_flag: str) -> str: 48 | """ 49 | Get flag from source 50 | """ 51 | 52 | def _get_flag_from_source(source: Optional[str]) -> str: 53 | sources = source.split(" ") 54 | 55 | flags = [ 56 | _get_flag_from_single_source(src, source_mappings, model_flag) 57 | for src in sources 58 | ] 59 | 60 | return "".join(flags) 61 | 62 | return _get_flag_from_source 63 | 64 | 65 | def with_suffix(items, suffix): 66 | """ 67 | Takes a list of strings and a suffix, returns a new list containing 68 | the same items with the suffix added. 69 | """ 70 | return [item + suffix for item in items] 71 | -------------------------------------------------------------------------------- /meteostat/utilities/mutations.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utilities - 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 datetime import datetime 12 | from typing import Union 13 | import numpy as np 14 | import pandas as pd 15 | 16 | 17 | def localize(df: pd.DataFrame, timezone: str) -> pd.DataFrame: 18 | """ 19 | Convert time data to any time zone 20 | """ 21 | 22 | return df.tz_localize("UTC", level="time").tz_convert(timezone, level="time") 23 | 24 | 25 | def filter_time( 26 | df: pd.DataFrame, 27 | start: Union[datetime, None] = None, 28 | end: Union[datetime, None] = None, 29 | ) -> pd.DataFrame: 30 | """ 31 | Filter time series data based on start and end date 32 | """ 33 | 34 | # Get time index 35 | time = df.index.get_level_values("time") 36 | 37 | # Filter & return 38 | return df.loc[(time >= start) & (time <= end)] if start and end else df 39 | 40 | 41 | def adjust_temp(df: pd.DataFrame, alt: int): 42 | """ 43 | Adjust temperature-like data based on altitude 44 | """ 45 | 46 | # Default temperature difference by 100 meters 47 | temp_diff = 0.6 48 | 49 | # Temperature-like columns 50 | temp_like = ("temp", "dwpt", "tavg", "tmin", "tmax") 51 | 52 | # Adjust values for all temperature-like data 53 | for col_name in temp_like: 54 | if col_name in df.columns: 55 | df.loc[df[col_name] != np.nan, col_name] = df[col_name] + ( 56 | temp_diff * ((df["elevation"] - alt) / 100) 57 | ) 58 | 59 | return df 60 | 61 | 62 | def calculate_dwpt(df: pd.DataFrame, col: str) -> pd.DataFrame: 63 | """ 64 | Calculate dew point temperature 65 | """ 66 | df[col] = df["temp"] - ((100 - df["rhum"]) / 5) 67 | df[f"{col}_flag"] = df[["temp_flag", "rhum_flag"]].max(axis=1, skipna=True) 68 | 69 | return df 70 | -------------------------------------------------------------------------------- /meteostat/utilities/validations.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utilities - Validations 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 | import pandas as pd 12 | 13 | 14 | def validate_series(df: pd.DataFrame, station: str) -> pd.DataFrame: 15 | """ 16 | Make sure a series is formatted correctly 17 | """ 18 | 19 | # Add missing column(s) 20 | if "time" not in df.columns: 21 | df["time"] = None 22 | 23 | # Add weather station ID 24 | df["station"] = station 25 | 26 | # Set index 27 | df = df.set_index(["station", "time"]) 28 | 29 | # Return DataFrame 30 | return df 31 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pandas>=2 2 | pytz 3 | numpy 4 | matplotlib 5 | pylint 6 | pytest 7 | black 8 | wheel 9 | twine -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Setup file 3 | 4 | The code is licensed under the MIT license. 5 | """ 6 | 7 | from os import path 8 | from setuptools import setup, find_packages 9 | 10 | # Content of the README file 11 | here = path.abspath(path.dirname(__file__)) 12 | with open(path.join(here, "README.md")) as f: 13 | long_description = f.read() 14 | 15 | # Setup 16 | setup( 17 | name="meteostat", 18 | version="1.7.0", 19 | author="Meteostat", 20 | author_email="info@meteostat.net", 21 | description="Access and analyze historical weather and climate data with Python.", 22 | long_description=long_description, 23 | long_description_content_type="text/markdown", 24 | url="https://github.com/meteostat/meteostat-python", 25 | keywords=["weather", "climate", "data", "timeseries", "meteorology"], 26 | python_requires=">=3.8.0", 27 | packages=find_packages(), 28 | include_package_data=True, 29 | install_requires=["pandas>=2", "pytz", "numpy"], 30 | license="MIT", 31 | classifiers=[ 32 | "Programming Language :: Python :: 3", 33 | "License :: OSI Approved :: MIT License", 34 | "Operating System :: OS Independent", 35 | "Topic :: Database", 36 | "Topic :: Scientific/Engineering :: Atmospheric Science", 37 | "Topic :: Scientific/Engineering :: Information Analysis", 38 | "Topic :: Scientific/Engineering :: Visualization", 39 | ], 40 | ) 41 | -------------------------------------------------------------------------------- /tests/e2e/test_daily.py: -------------------------------------------------------------------------------- 1 | """ 2 | E2E Test - Daily Class 3 | 4 | The code is licensed under the MIT license. 5 | """ 6 | 7 | from datetime import datetime 8 | from meteostat import Daily 9 | 10 | 11 | def test_flags(): 12 | """ 13 | Test request with flags 14 | """ 15 | 16 | # Get 2018 daily data for Frankfurt Airport 17 | df = Daily( 18 | ["10637"], start=datetime(2018, 1, 1), end=datetime(2018, 12, 31), flags=True 19 | ).fetch() 20 | 21 | assert len(df.columns) == 20 22 | 23 | 24 | def test_normalize(): 25 | """ 26 | Test: normalize() method 27 | """ 28 | 29 | # Get 2018 daily data for Frankfurt Airport 30 | data = Daily(["10637"], start=datetime(2018, 1, 1), end=datetime(2018, 12, 31)) 31 | count = data.normalize().count() 32 | 33 | # Check if count matches 365 34 | assert count == 365 35 | 36 | 37 | def test_aggregate(): 38 | """ 39 | Test: aggregate() method 40 | """ 41 | 42 | # Get 2018 daily data for Frankfurt Airport 43 | data = Daily(["10637"], start=datetime(2018, 1, 1), end=datetime(2018, 12, 31)) 44 | count = data.normalize().aggregate("1W").count() 45 | 46 | # Check if count matches 53 47 | assert count == 53 48 | 49 | 50 | def test_coverage(): 51 | """ 52 | Test: coverage() method 53 | """ 54 | 55 | # Get 2018 daily data for Frankfurt Airport 56 | data = Daily(["10637"], start=datetime(2018, 1, 1), end=datetime(2018, 12, 31)) 57 | coverage = data.normalize().coverage() 58 | 59 | # Check if coverage is 100% 60 | assert coverage == 1 61 | -------------------------------------------------------------------------------- /tests/e2e/test_hourly.py: -------------------------------------------------------------------------------- 1 | """ 2 | E2E Test - Hourly Class 3 | 4 | The code is licensed under the MIT license. 5 | """ 6 | 7 | from datetime import datetime, timedelta 8 | from meteostat import Hourly 9 | 10 | 11 | def test_model_disabled(): 12 | """ 13 | Test request with disabled model data 14 | """ 15 | 16 | today = datetime.today() 17 | start = today + timedelta(days=2) 18 | end = today + timedelta(days=3) 19 | data = Hourly("10637", start, end, model=False) 20 | 21 | assert data.count() == 0 22 | 23 | 24 | def test_normalize(): 25 | """ 26 | Test: normalize() method 27 | """ 28 | 29 | # Get data for some day at Frankfurt Airport 30 | data = Hourly( 31 | ["10637"], start=datetime(2018, 1, 1), end=datetime(2018, 1, 1, 23, 59) 32 | ) 33 | count = data.normalize().count() 34 | 35 | # Check if count matches 24 36 | assert count == 24 37 | 38 | 39 | def test_aggregate(): 40 | """ 41 | Test: aggregate() method 42 | """ 43 | 44 | # Get data for some days at Frankfurt Airport 45 | data = Hourly( 46 | ["10637"], start=datetime(2018, 1, 1), end=datetime(2018, 1, 3, 23, 59) 47 | ) 48 | count = data.normalize().aggregate("1D").count() 49 | 50 | # Check if count matches 3 51 | assert count == 3 52 | 53 | 54 | def test_interpolate(): 55 | """ 56 | Test: interpolate() method 57 | """ 58 | 59 | # Get data for one day at Frankfurt Airport 60 | data = Hourly( 61 | ["10637"], start=datetime(2018, 1, 1), end=datetime(2018, 1, 1, 23, 59) 62 | ) 63 | count = data.normalize().interpolate().count() 64 | 65 | # Check if count matches 24 66 | assert count == 24 67 | 68 | 69 | def test_coverage(): 70 | """ 71 | Test: coverage() method 72 | """ 73 | 74 | # Get data for some day at Frankfurt Airport 75 | data = Hourly( 76 | ["10637"], start=datetime(2018, 1, 1), end=datetime(2018, 1, 1, 23, 59) 77 | ) 78 | coverage = data.normalize().coverage() 79 | 80 | # Check if coverage is 100% 81 | assert coverage == 1 82 | -------------------------------------------------------------------------------- /tests/e2e/test_monthly.py: -------------------------------------------------------------------------------- 1 | """ 2 | E2E Test - Monthly Class 3 | 4 | The code is licensed under the MIT license. 5 | """ 6 | 7 | from datetime import datetime 8 | from meteostat import Monthly 9 | 10 | 11 | def test_normalize(): 12 | """ 13 | Test: normalize() method 14 | """ 15 | 16 | # Get 2018 monthly data for Frankfurt Airport 17 | data = Monthly("72202", start=datetime(2018, 1, 1), end=datetime(2018, 12, 31)) 18 | count = data.normalize().count() 19 | 20 | # Check if count matches 12 21 | assert count == 12 22 | 23 | 24 | def test_aggregate(): 25 | """ 26 | Test: aggregate() method 27 | """ 28 | 29 | # Get 2018 monthly data for Frankfurt Airport 30 | data = Monthly("72202", start=datetime(2018, 1, 1), end=datetime(2018, 12, 31)) 31 | count = data.normalize().aggregate("1Y").count() 32 | 33 | # Check if count matches 1 34 | assert count == 1 35 | 36 | 37 | def test_coverage(): 38 | """ 39 | Test: coverage() method 40 | """ 41 | 42 | # Get 2018 monthly data for Frankfurt Airport 43 | data = Monthly("72202", start=datetime(2018, 1, 1), end=datetime(2018, 12, 31)) 44 | coverage = data.normalize().coverage() 45 | 46 | # Check if coverage is 100% 47 | assert coverage == 1 48 | -------------------------------------------------------------------------------- /tests/e2e/test_normals.py: -------------------------------------------------------------------------------- 1 | """ 2 | E2E Test - Normals Class 3 | 4 | The code is licensed under the MIT license. 5 | """ 6 | 7 | from meteostat import Normals 8 | 9 | 10 | def test_normals(): 11 | """ 12 | Test: Fetch climate normals 13 | """ 14 | 15 | # Get normals for Frankfurt Airport 16 | data = Normals("10637", 1961, 1990) 17 | 18 | # Count rows 19 | count = data.count() 20 | 21 | # Check if count matches 12 22 | assert count == 12 23 | -------------------------------------------------------------------------------- /tests/e2e/test_point.py: -------------------------------------------------------------------------------- 1 | """ 2 | E2E Test - Point Class 3 | 4 | The code is licensed under the MIT license. 5 | """ 6 | 7 | from datetime import datetime 8 | from meteostat import Point 9 | 10 | 11 | def test_point(): 12 | """ 13 | Test: Point Data 14 | """ 15 | 16 | # Create Point for Vancouver, BC 17 | point = Point(49.2497, -123.1193, 70) 18 | 19 | # Get count of weather stations 20 | stations = point.get_stations("daily", datetime(2020, 1, 1), datetime(2020, 1, 31)) 21 | 22 | # Check if three stations are returned 23 | assert len(stations.index) == 4 24 | -------------------------------------------------------------------------------- /tests/e2e/test_stations.py: -------------------------------------------------------------------------------- 1 | """ 2 | E2E Test - Stations Class 3 | 4 | The code is licensed under the MIT license. 5 | """ 6 | 7 | from datetime import datetime 8 | from meteostat import Stations, units 9 | 10 | 11 | def test_nearby(): 12 | """ 13 | Test: Nearby stations 14 | """ 15 | 16 | # Selecting closest weather station to Frankfurt Airport 17 | station = Stations().nearby(50.05, 8.6).fetch(1).to_dict("records")[0] 18 | 19 | # Check if country code matches Germany 20 | assert station["country"] == "DE" 21 | 22 | 23 | def test_region(): 24 | """ 25 | Test: Stations by country/region code 26 | """ 27 | 28 | # Select a weather station in Ontario, Canada 29 | station = Stations().region("CA", "ON").fetch(1).to_dict("records")[0] 30 | 31 | # Check if country code matches Canada 32 | assert station["country"] == "CA" 33 | 34 | # Check if region code matches Ontario 35 | assert station["region"] == "ON" 36 | 37 | 38 | def test_bounds(): 39 | """ 40 | Test: Stations by geographical area 41 | """ 42 | 43 | # Select weather stations in southern hemisphere 44 | station = Stations().bounds((0, -180), (-90, 180)).fetch(1).to_dict("records")[0] 45 | 46 | # Check if -90 <= latitude <= 0 47 | assert -90 <= station["latitude"] <= 0 48 | 49 | 50 | def test_inventory(): 51 | """ 52 | Test: Filter stations by inventory 53 | """ 54 | 55 | # Select weather stations in Germany 56 | stations = Stations().region("DE") 57 | 58 | # Apply inventory filter 59 | stations = stations.inventory("daily", datetime(2020, 1, 1)) 60 | 61 | # Get count 62 | count = stations.count() 63 | 64 | # Check if at least one station remains 65 | assert count > 0 66 | 67 | 68 | def test_convert(): 69 | """ 70 | Test: Convert distance to feet 71 | """ 72 | 73 | # Get closest weather stations to Seattle, WA 74 | stations = Stations().nearby(47.6062, -122.3321) 75 | 76 | # Convert distance to feet 77 | stations = stations.convert({"distance": units.feet}) 78 | 79 | # Get three closest weather stations 80 | stations = stations.fetch(3) 81 | 82 | # Check if three stations are returned 83 | assert len(stations.index) == 3 84 | -------------------------------------------------------------------------------- /tests/manual/manual_test_aggregation.py: -------------------------------------------------------------------------------- 1 | """ 2 | Compare hourly aggregations with daily data 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 datetime import datetime 12 | import matplotlib.pyplot as plt 13 | from meteostat import Hourly, Stations, Daily 14 | 15 | start = datetime(2007, 12, 30) 16 | end = datetime(2021, 12, 31) 17 | lat, lon = 6.25, -75.5 18 | 19 | stations = Stations() 20 | station = stations.nearby(lat, lon).fetch(1) 21 | data_daily = Daily(station, start, end, model=True).fetch() 22 | data_agg = Hourly(station, start, end, timezone="America/Bogota", model=True) 23 | data_agg = data_agg.normalize().aggregate("1D").fetch() 24 | 25 | fig = plt.figure() 26 | plt.plot( 27 | data_daily.index.date, data_daily["tavg"].values, label="Daily mean by Meteostat" 28 | ) 29 | plt.plot( 30 | data_agg.index.date, 31 | data_agg["temp"].values, 32 | color="black", 33 | label="Daily mean calculated with aggregate", 34 | linewidth=0.25, 35 | ) 36 | 37 | 38 | plt.legend() 39 | plt.show() 40 | -------------------------------------------------------------------------------- /tests/manual/manual_test_spatial_interpolation.py: -------------------------------------------------------------------------------- 1 | """ 2 | Compare Interpolation Methods 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 datetime import datetime 12 | import matplotlib.pyplot as plt 13 | from meteostat import Point, Hourly 14 | 15 | # Start & end datetime 16 | start = datetime(2022, 3, 6, 0) 17 | end = datetime(2022, 3, 13, 23) 18 | 19 | # Schmitten (465m) 20 | p1 = Point(50.2667, 8.45, 465) 21 | # Neu-Anspach (320m) 22 | p2 = Point(50.3167, 8.5, 320) 23 | # Bad Homburg (186m) 24 | p3 = Point(50.2268, 8.6182, 186) 25 | 26 | # Fetch data 27 | df1 = Hourly(p1, start, end).normalize().fetch() 28 | df2 = Hourly(p2, start, end).fetch() 29 | df3 = Hourly(p3, start, end).fetch() 30 | df4 = Hourly("10635", start, end).fetch() 31 | df5 = Hourly("D1424", start, end).fetch() 32 | 33 | # Plot 34 | fig, ax = plt.subplots(figsize=(8, 6)) 35 | df1.plot(y=["temp"], ax=ax) 36 | df2.plot(y=["temp"], ax=ax) 37 | df3.plot(y=["temp"], ax=ax) 38 | df4.plot(y=["temp"], ax=ax) 39 | df5.plot(y=["temp"], ax=ax) 40 | 41 | # Show plot 42 | plt.legend( 43 | [ 44 | f'Schmitten ({df1["temp"].mean()})', 45 | f'Neu-Anspach ({df2["temp"].mean()})', 46 | f'Bad Homburg ({df3["temp"].mean()})', 47 | f'Kleiner Feldberg ({df4["temp"].mean()})', 48 | f'Frankfurt Westend ({df5["temp"].mean()})', 49 | ] 50 | ) 51 | plt.show() 52 | -------------------------------------------------------------------------------- /tests/unit/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteostat/meteostat-python/ba9653a4244c2cd72e7d926c29c738d18fa61258/tests/unit/core/__init__.py -------------------------------------------------------------------------------- /tests/unit/core/test_cache.py: -------------------------------------------------------------------------------- 1 | """ 2 | Cache Tests 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 meteostat.core.cache import get_local_file_path 12 | 13 | 14 | EXPECTED_FILE_PATH = "cache/hourly/6dfc35c47756e962ef055d1049f1f8ec" 15 | 16 | 17 | def test_get_local_file_path(): 18 | """ 19 | Test local file path 20 | """ 21 | 22 | assert get_local_file_path("cache", "hourly", "10101") == EXPECTED_FILE_PATH 23 | 24 | 25 | def test_get_local_file_path_chunked(): 26 | """ 27 | Test local file path II 28 | """ 29 | 30 | assert get_local_file_path("cache", "hourly", "10101_2022") != EXPECTED_FILE_PATH 31 | -------------------------------------------------------------------------------- /tests/unit/utilities/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteostat/meteostat-python/ba9653a4244c2cd72e7d926c29c738d18fa61258/tests/unit/utilities/__init__.py -------------------------------------------------------------------------------- /tests/unit/utilities/test_endpoint.py: -------------------------------------------------------------------------------- 1 | """ 2 | Endpoint Utility Tests 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 meteostat.utilities.endpoint import generate_endpoint_path 12 | from meteostat.enumerations.granularity import Granularity 13 | 14 | 15 | def test_generate_endpoint_path_normals(): 16 | """ 17 | Generate endpoint path for climate normals 18 | """ 19 | 20 | assert ( 21 | generate_endpoint_path(Granularity.NORMALS, "10286") == "normals/10286.csv.gz" 22 | ) 23 | 24 | 25 | def test_generate_endpoint_path_hourly(): 26 | """ 27 | Generate endpoint path for hourly data 28 | """ 29 | 30 | assert generate_endpoint_path(Granularity.HOURLY, "10286") == "hourly/10286.csv.gz" 31 | 32 | 33 | def test_generate_endpoint_path_hourly_map(): 34 | """ 35 | Generate endpoint path for hourly observation data 36 | """ 37 | 38 | assert ( 39 | generate_endpoint_path(Granularity.HOURLY, "10286", None, True) 40 | == "hourly/10286.map.csv.gz" 41 | ) 42 | 43 | 44 | def test_generate_endpoint_path_hourly_chunk(): 45 | """ 46 | Generate endpoint path for hourly chunk 47 | """ 48 | 49 | assert ( 50 | generate_endpoint_path(Granularity.HOURLY, "10286", 2021) 51 | == "hourly/2021/10286.csv.gz" 52 | ) 53 | 54 | 55 | def test_generate_endpoint_path_daily(): 56 | """ 57 | Generate endpoint path for daily data 58 | """ 59 | 60 | assert generate_endpoint_path(Granularity.DAILY, "10286") == "daily/10286.csv.gz" 61 | 62 | 63 | def test_generate_endpoint_path_monthly(): 64 | """ 65 | Generate endpoint path for monthly data 66 | """ 67 | 68 | assert ( 69 | generate_endpoint_path(Granularity.MONTHLY, "10286") == "monthly/10286.csv.gz" 70 | ) 71 | --------------------------------------------------------------------------------