├── tests
├── __init__.py
├── test_cleaner.py
└── test_rnn.py
├── data
├── preprocessing
│ ├── __init__.py
│ ├── gleam_cleaner.py
│ ├── chirps_cleaner.py
│ ├── collect_regrid_data_script.py
│ ├── spei_cleaner.py
│ ├── cleaner.py
│ └── utils.py
├── README.md
├── raw
│ └── README.md
├── processed
│ └── README.md
├── nc_to_pandas.py
├── ee_extract_landcover_by_country.js
├── globmap_lookup.py
├── utils.py
├── drought_masking.py
└── common_grid.py
├── predictor
├── models
│ ├── neural_networks
│ │ ├── __init__.py
│ │ ├── feedforward.py
│ │ ├── nn_base.py
│ │ └── recurrent.py
│ ├── __init__.py
│ ├── baseline.py
│ └── base.py
├── __init__.py
├── analysis
│ ├── __init__.py
│ ├── utils.py
│ ├── plot_shap.py
│ └── plot_results.py
├── engineer.py
└── preprocessing.py
├── ndvi_results.png
├── figs
├── variables_histogram.png
└── ndvi_results_logistic_regression.png
├── .gitignore
├── TODO.md
├── notebooks
├── 10_tl_growing_season.ipynb
├── 05_tl_explore_models.ipynb
├── 02_gt_linear_model.ipynb
└── 01_tl_data_exploration.ipynb
├── run.py
├── environment.yml
└── README.md
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/data/preprocessing/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/predictor/models/neural_networks/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/data/README.md:
--------------------------------------------------------------------------------
1 | # Data
2 |
3 | Placeholder for data
4 |
--------------------------------------------------------------------------------
/data/raw/README.md:
--------------------------------------------------------------------------------
1 | # Raw data
2 |
3 | Placeholder for the raw data file, `tabular_data.csv`.
4 |
--------------------------------------------------------------------------------
/ndvi_results.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommylees112/vegetation_health/HEAD/ndvi_results.png
--------------------------------------------------------------------------------
/predictor/__init__.py:
--------------------------------------------------------------------------------
1 | from .preprocessing import Cleaner
2 | from .engineer import Engineer
3 |
--------------------------------------------------------------------------------
/predictor/analysis/__init__.py:
--------------------------------------------------------------------------------
1 | from .plot_shap import plot_shap_values
2 | from .plot_results import plot_results
3 |
--------------------------------------------------------------------------------
/figs/variables_histogram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommylees112/vegetation_health/HEAD/figs/variables_histogram.png
--------------------------------------------------------------------------------
/figs/ndvi_results_logistic_regression.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tommylees112/vegetation_health/HEAD/figs/ndvi_results_logistic_regression.png
--------------------------------------------------------------------------------
/data/processed/README.md:
--------------------------------------------------------------------------------
1 | # Processed Data
2 |
3 | Placeholder for processed data. Steps in the pipeline will save data here for future steps to use.
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.csv
2 | *.npy
3 | .DS_Store
4 | notebooks/.ipynb_checkpoints
5 | .ipynb_checkpoints
6 | __pycache__
7 | *.pyc
8 | .idea
9 | temp.py
10 | *.nc
11 | test.py
12 | *.json
13 | docs/
14 |
--------------------------------------------------------------------------------
/predictor/models/__init__.py:
--------------------------------------------------------------------------------
1 | from .baseline import LinearModel
2 | from .neural_networks.feedforward import FeedForward as nn_FeedForward
3 | from .neural_networks.recurrent import Recurrent as nn_Recurrent
4 |
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 | Tommy:
2 | - [x] Produce a tabular dataset of precip / temp / vegetation health indices
3 | - [x] upload to github (if < 50mb) (SENT BY WE TRANSFER)
4 | - [x] Mask out the sea values (not sure if already done - check!)
5 |
6 | Gabriel:
7 | - [x] produce project skeleton
8 | - [x] Mask out the LST values == 200
9 |
--------------------------------------------------------------------------------
/data/nc_to_pandas.py:
--------------------------------------------------------------------------------
1 | """ convert netcdf file to tabular dataformat (pandas)
2 | BOILERPLATE
3 | """
4 | import xarray as xr
5 | import pandas as pd
6 |
7 | # change this path to point to the .nc file
8 | data_dir = ''
9 |
10 | ds = xr.open_dataset(data_dir)
11 |
12 | # NOTE
13 | df = ds.to_dataframe()
14 | df.to_csv('path/to/csv/file')
15 |
--------------------------------------------------------------------------------
/predictor/analysis/utils.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from pathlib import Path
3 | from collections import namedtuple
4 |
5 |
6 |
7 | def load_model_data(train_or_test, target='ndvi'):
8 | """ Return a named tuple with the following data attrs
9 | x, y, latlon, years. This is the data fed through the model.
10 | """
11 | if train_or_test == "test":
12 | data_dir = Path('.') / "data" / "processed" / target / "arrays" / "test"
13 | elif train_or_test == "train":
14 | data_dir = Path('.') / "data" / "processed" / target / "arrays" / "train"
15 | else:
16 | assert False, "train_or_test must be either ['train','test']"
17 |
18 | Data = namedtuple('Data',["x","y","latlon","years"])
19 | data = Data(
20 | x=np.load(data_dir/"x.npy"),
21 | y=np.load(data_dir/"y.npy"),
22 | latlon=np.load(data_dir/"latlon.npy"),
23 | years=np.load(data_dir/"years.npy"),
24 | )
25 |
26 | return data
27 |
--------------------------------------------------------------------------------
/tests/test_cleaner.py:
--------------------------------------------------------------------------------
1 | import pandas as pd
2 | from datetime import datetime
3 |
4 | from predictor import Cleaner
5 |
6 |
7 | def test_year_month():
8 |
9 | # lets predict June given a year's data
10 | months_2018 = [datetime(2018, x, 1) for x in range(1, 13)]
11 | months_2019 = [datetime(2019, x, 1) for x in range(1, 13)]
12 |
13 | g1, g2, g3 = [1] * 5, [2] * 12, [3] * 7
14 | test_data = {
15 | 'times': months_2018 + months_2019,
16 | 'group': g1 + g2 + g3,
17 | }
18 | test_df = pd.DataFrame(data=test_data)
19 |
20 | cleaner = Cleaner()
21 | test_df['gp_month'], test_df['gp_year'] = cleaner.update_year_month(test_df['times'],
22 | pred_month=5)
23 |
24 | # all of g2 should be in the same gp_year
25 | group_2 = test_df[test_df['gp_year'] == 2019]
26 |
27 | assert len(group_2) == 12, "Chopped out some 2018 data"
28 | assert (group_2['group'] == 2).all(), "Not all the correct months were grouped!"
29 |
--------------------------------------------------------------------------------
/data/ee_extract_landcover_by_country.js:
--------------------------------------------------------------------------------
1 | // ee_extract_landcover_by_country.js
2 |
3 | // import the landcover dataset & the country shapefiles
4 | var globcover = ee.FeatureCollection('ESA/GLOBCOVER_L4_200901_200912_V2_3')
5 | var world_region = ee.FeatureCollection('ft:1tdSwUL7MVpOauSgRzqVTOwdfy17KDbw-1d9omPw')
6 | var landcover = globcover.select('landcover');
7 |
8 | // create Polygon for Ethiopia and Kenya
9 | var eth = world_region.filterMetadata('Country','equals','Ethiopia').select(['landcover']);
10 | var kenya = world_region.filterMetadata('Country','equals','Kenya');
11 |
12 | // clip the landcover to the countries
13 | var eth_lc = landcover.clip(eth);
14 | var ken_lc = landcover.clip(kenya);
15 |
16 | // export the landcover map
17 | var scale = 500;
18 | var crs='EPSG:4326';
19 |
20 | var task = Export.image.toDrive({
21 | image: eth_lc,
22 | description: 'ethiopia_landcover',
23 | scale: 1000,
24 | region: eth,
25 | folder: 'landcover'
26 | })
27 |
28 | var task_kenya = Export.image.toDrive({
29 | image: ken_lc,
30 | description: 'kenya_landcover',
31 | scale: 1000,
32 | region: kenya,
33 | folder: 'landcover'
34 | })
35 |
--------------------------------------------------------------------------------
/predictor/models/baseline.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from sklearn import linear_model
3 | from sklearn.metrics import mean_squared_error
4 |
5 | from .base import ModelBase
6 |
7 |
8 | class LinearModel(ModelBase):
9 | """A logistic regression, to be used as a baseline
10 | against our more complex models
11 | """
12 |
13 | model_name = 'linear'
14 |
15 | def train(self):
16 |
17 | train_data = self.load_arrays(mode='train')
18 |
19 | x = train_data.x.reshape(train_data.x.shape[0], -1)
20 |
21 | self.model = linear_model.LinearRegression()
22 | self.model.fit(x, train_data.y)
23 |
24 | train_pred_y = self.model.predict(x)
25 | train_rmse = np.sqrt(mean_squared_error(train_data.y, train_pred_y))
26 |
27 | print(f'Train set RMSE: {train_rmse}')
28 |
29 | def predict(self):
30 |
31 | test_data = self.load_arrays(mode='test')
32 | x = test_data.x.reshape(test_data.x.shape[0], -1)
33 | test_pred_y = self.model.predict(x)
34 | return test_data.y, test_pred_y
35 |
36 | def save_model(self):
37 | savedir = self.data_path / self.model_name
38 | if not savedir.exists(): savedir.mkdir()
39 | np.save(savedir / 'model.npy', self.model.coef_)
40 |
--------------------------------------------------------------------------------
/tests/test_rnn.py:
--------------------------------------------------------------------------------
1 | import torch
2 | from torch import nn
3 | import numpy as np
4 |
5 | from predictor.models.neural_networks.recurrent import UnrolledRNN
6 |
7 |
8 | def test_rnn():
9 | """
10 | We implement our own unrolled RNN, so that it can be explained with
11 | shap. This test makes sure it roughly mirrors the behaviour of the pytorch
12 | LSTM.
13 | """
14 |
15 | batch_size, hidden_size, features_per_month = 32, 124, 6
16 |
17 | x = torch.ones(batch_size, 1, features_per_month)
18 |
19 | hidden_state = torch.zeros(1, x.shape[0], hidden_size)
20 | cell_state = torch.zeros(1, x.shape[0], hidden_size)
21 |
22 | torch_rnn = nn.LSTM(input_size=features_per_month,
23 | hidden_size=hidden_size,
24 | batch_first=True,
25 | num_layers=1)
26 |
27 | our_rnn = UnrolledRNN(input_size=features_per_month,
28 | hidden_size=hidden_size,
29 | batch_first=True)
30 |
31 | for parameters in torch_rnn.all_weights:
32 | for pam in parameters:
33 | nn.init.constant_(pam.data, 1)
34 |
35 | for parameters in our_rnn.parameters():
36 | for pam in parameters:
37 | nn.init.constant_(pam.data, 1)
38 |
39 | with torch.no_grad():
40 | o_out, (o_cell, o_hidden) = our_rnn(x, (hidden_state, cell_state))
41 | t_out, (t_cell, t_hidden) = torch_rnn(x, (hidden_state, cell_state))
42 |
43 | assert np.isclose(o_out.numpy(), t_out.numpy(), 0.01).all(), "Difference in hidden state"
44 | assert np.isclose(t_cell.numpy(), o_cell.numpy(), 0.01).all(), "Difference in cell state"
45 |
--------------------------------------------------------------------------------
/notebooks/10_tl_growing_season.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# When is the growing season? (Season with Maximum NDVI?)"
8 | ]
9 | },
10 | {
11 | "cell_type": "code",
12 | "execution_count": 2,
13 | "metadata": {},
14 | "outputs": [],
15 | "source": [
16 | "import xarray as xr\n",
17 | "import pandas as pd\n",
18 | "import numpy as np\n",
19 | "from pathlib import Path\n",
20 | "from collections import namedtuple\n",
21 | "import matplotlib.pyplot as plt\n",
22 | "import seaborn as sns\n",
23 | "%matplotlib inline\n",
24 | "\n",
25 | "import os\n",
26 | "if os.getcwd().split('/')[-1] != \"vegetation_health\":\n",
27 | " os.chdir('..')\n",
28 | " \n",
29 | "assert os.getcwd().split('/')[-1] == \"vegetation_health\", f\"Working directory should be the root (), currently: {os.getcwd()}\"\n",
30 | "\n",
31 | "from predictor.analysis.plot_results import create_dataset_from_vars, plot_results\n",
32 | "from predictor.analysis.utils import load_model_data"
33 | ]
34 | },
35 | {
36 | "cell_type": "code",
37 | "execution_count": null,
38 | "metadata": {},
39 | "outputs": [],
40 | "source": []
41 | },
42 | {
43 | "cell_type": "code",
44 | "execution_count": null,
45 | "metadata": {},
46 | "outputs": [],
47 | "source": []
48 | }
49 | ],
50 | "metadata": {
51 | "kernelspec": {
52 | "display_name": "Python 3",
53 | "language": "python",
54 | "name": "python3"
55 | },
56 | "language_info": {
57 | "codemirror_mode": {
58 | "name": "ipython",
59 | "version": 3
60 | },
61 | "file_extension": ".py",
62 | "mimetype": "text/x-python",
63 | "name": "python",
64 | "nbconvert_exporter": "python",
65 | "pygments_lexer": "ipython3",
66 | "version": "3.7.2"
67 | }
68 | },
69 | "nbformat": 4,
70 | "nbformat_minor": 2
71 | }
72 |
--------------------------------------------------------------------------------
/run.py:
--------------------------------------------------------------------------------
1 | import fire
2 | from pathlib import Path
3 |
4 | from predictor import Cleaner, Engineer
5 | from predictor.models import LinearModel, nn_FeedForward, nn_Recurrent
6 |
7 |
8 | class RunTask:
9 |
10 | @staticmethod
11 | def clean(raw_filepath='data/raw/predict_vegetation_health.nc',
12 | processed_folder='data/processed',
13 | target='ndvi', pred_month=6):
14 |
15 | raw_filepath, processed_folder = Path(raw_filepath), Path(processed_folder)
16 | processed_filepath = processed_folder / target / 'cleaned_data.csv'
17 |
18 | cleaner = Cleaner(raw_filepath, processed_filepath)
19 | cleaner.process(pred_month, target)
20 |
21 | @staticmethod
22 | def engineer(processed_folder='data/processed', target='ndvi',
23 | test_year=2016):
24 |
25 | processed_folder = Path(processed_folder)
26 | cleaned_data = processed_folder / target / 'cleaned_data.csv'
27 | arrays_folder = processed_folder / target / 'arrays'
28 |
29 | engineer = Engineer(cleaned_data, arrays_folder)
30 | engineer.process(test_year)
31 |
32 | @staticmethod
33 | def train_model(model_type='baseline', data_folder='data',
34 | target='ndvi', hide_vegetation=True, save_results=True):
35 |
36 | data_folder = Path(data_folder)
37 | arrays_folder = data_folder / 'processed' / target / 'arrays'
38 |
39 | string2model = {
40 | 'baseline': LinearModel(data_folder, arrays_folder, hide_vegetation),
41 | 'feedforward': nn_FeedForward(data_folder, arrays_folder, hide_vegetation),
42 | 'recurrent': nn_Recurrent(data_folder, arrays_folder, hide_vegetation),
43 | }
44 |
45 | model = string2model[model_type]
46 | model.train()
47 | model.evaluate(save_preds=save_results)
48 | if save_results:
49 | model.save_model()
50 |
51 |
52 | if __name__ == '__main__':
53 | fire.Fire(RunTask)
54 |
--------------------------------------------------------------------------------
/environment.yml:
--------------------------------------------------------------------------------
1 | name: vegetation_health
2 | channels:
3 | - pytorch
4 | - anaconda
5 | - conda-forge
6 | - defaults
7 | dependencies:
8 | - atomicwrites=1.3.0
9 | - attrs=19.1.0
10 | - blas=1.0
11 | - bzip2=1.0.6
12 | - cftime=1.0.3.4
13 | - curl=7.64.0
14 | - hdf4=4.2.13
15 | - hdf5=1.10.4
16 | - intel-openmp=2019.1
17 | - jpeg=9b
18 | - libcurl=7.64.0
19 | - libgfortran=3.0.1
20 | - libnetcdf=4.6.1
21 | - mkl=2019.1
22 | - mkl_fft=1.0.10
23 | - mkl_random=1.0.2
24 | - more-itertools=6.0.0
25 | - netcdf4=1.4.2
26 | - numpy=1.16.2
27 | - numpy-base=1.16.2
28 | - pandas=0.24.1
29 | - pluggy=0.9.0
30 | - py=1.8.0
31 | - pytest=4.3.0
32 | - python-dateutil=2.8.0
33 | - pytz=2018.9
34 | - scikit-learn=0.20.2
35 | - scipy=1.2.1
36 | - six=1.12.0
37 | - xarray=0.11.3
38 | - ca-certificates=2019.3.9
39 | - certifi=2019.3.9
40 | - cycler=0.10.0
41 | - fire=0.1.3
42 | - freetype=2.9.1
43 | - kiwisolver=1.0.1
44 | - libpng=1.6.36
45 | - libssh2=1.8.0
46 | - matplotlib=3.0.3
47 | - matplotlib-base=3.0.3
48 | - openssl=1.1.1b
49 | - pyparsing=2.3.1
50 | - tqdm=4.31.1
51 | - appnope=0.1.0
52 | - backcall=0.1.0
53 | - cffi=1.12.2
54 | - decorator=4.3.2
55 | - ipykernel=5.1.0
56 | - ipython=7.3.0
57 | - ipython_genutils=0.2.0
58 | - jedi=0.13.3
59 | - jupyter_client=5.2.4
60 | - jupyter_core=4.4.0
61 | - krb5=1.16.1
62 | - libcxx=4.0.1
63 | - libcxxabi=4.0.1
64 | - libedit=3.1.20181209
65 | - libffi=3.2.1
66 | - libsodium=1.0.16
67 | - ncurses=6.1
68 | - ninja=1.8.2
69 | - parso=0.3.4
70 | - pexpect=4.6.0
71 | - pickleshare=0.7.5
72 | - pip=19.0.3
73 | - prompt_toolkit=2.0.9
74 | - ptyprocess=0.6.0
75 | - pycparser=2.19
76 | - pygments=2.3.1
77 | - python=3.6.8
78 | - pyzmq=18.0.0
79 | - readline=7.0
80 | - setuptools=40.8.0
81 | - sqlite=3.26.0
82 | - tk=8.6.8
83 | - tornado=6.0.1
84 | - traitlets=4.3.2
85 | - wcwidth=0.1.7
86 | - wheel=0.33.1
87 | - xz=5.2.4
88 | - zeromq=4.3.1
89 | - zlib=1.2.11
90 | - pytorch=1.0.1
91 | - pip:
92 | - torch==1.0.1.post2
93 |
--------------------------------------------------------------------------------
/predictor/analysis/plot_shap.py:
--------------------------------------------------------------------------------
1 | from mpl_toolkits.axes_grid1 import host_subplot
2 | import mpl_toolkits.axisartist as AA
3 | import matplotlib.pyplot as plt
4 |
5 |
6 | def plot_shap_values(x, shap_values, val_list, normalizing_dict, value_to_plot, normalize_shap_plots=True):
7 | """Plots the denormalized values against their shap values, so that
8 | variations in the input features to the model can be compared to their effect
9 | on the model. For example plots, see notebooks/08_gt_recurrent_model.ipynb.
10 |
11 | Parameters:
12 | ----------
13 | x: np.array
14 | The input to a model for a single data instance
15 | shap_values: np.array
16 | The corresponding shap values (to x)
17 | val_list: list
18 | A list of the variable names, for axis labels
19 | normalizing_dict: dict
20 | The normalizing dict saved by the `Cleaner`, so that the x array can be
21 | denormalized
22 | value_to_plot: str
23 | The specific input variable to plot. Must be in val_list
24 | normalize_shap_plots: boolean
25 | If True, then the scale of the shap plots will be uniform across all
26 | variable plots (on an instance specific basis).
27 |
28 | A plot of the variable `value_to_plot` against its shap values will be plotted.
29 | """
30 | # first, lets isolate the lists
31 | idx = val_list.index(value_to_plot)
32 |
33 | x_val = x[:, idx].cpu().numpy()
34 |
35 | # we also want to denormalize
36 | x_val = (x_val * normalizing_dict[value_to_plot]['std']) + \
37 | normalizing_dict[value_to_plot]['mean']
38 |
39 | shap_val = shap_values[:, idx]
40 |
41 | months = list(range(1, 12))
42 |
43 | host = host_subplot(111, axes_class=AA.Axes)
44 | plt.subplots_adjust(right=0.75)
45 |
46 | par1 = host.twinx()
47 | par1.axis["right"].toggle(all=True)
48 |
49 | if normalize_shap_plots:
50 | par1.set_ylim(shap_values.min(), shap_values.max())
51 |
52 | host.set_xlabel("Months")
53 | host.set_ylabel(value_to_plot)
54 | par1.set_ylabel("Shap value")
55 |
56 | p1, = host.plot(months, x_val, label=value_to_plot)
57 | p2, = par1.plot(months, shap_val, label="shap value")
58 |
59 | host.axis["left"].label.set_color(p1.get_color())
60 | par1.axis["right"].label.set_color(p2.get_color())
61 |
62 | host.legend()
63 |
64 | plt.draw()
65 | plt.show()
66 |
--------------------------------------------------------------------------------
/predictor/analysis/plot_results.py:
--------------------------------------------------------------------------------
1 | import xarray as xr
2 | import pandas as pd
3 | import numpy as np
4 | from pathlib import Path
5 | import matplotlib.pyplot as plt
6 |
7 |
8 | def create_dataset_from_vars(vars, latlon, varname, to_xarray=True):
9 | """ Convert the variables from `np.array` to `pd.DataFrame`
10 | and optionally `xr.Dataset`. By default converts to `xr.Dataset`
11 |
12 | Arguments:
13 | ---------
14 | : vars (np.array)
15 | the values of the variable of interest (e.g. Predictions of NDVI from model)
16 |
17 | : latlon (np.array)
18 | the latlon location for each of the values in vars
19 |
20 | : varname (str)
21 | the name of the variable
22 |
23 | TODO:
24 | ----
25 | * Implement a method that works with TIME so that the xarray objects
26 | have a time dimension too
27 | """
28 | assert len(vars) == len(latlon), f"The length of the latlons array should be the same as the legnth of the vars array. Currently latlons: {len(latlon)} vars: {len(vars)}"
29 |
30 |
31 | df = pd.DataFrame(data={varname: vars, 'lat': latlon[:, 0],
32 | 'lon': latlon[:, 1]}).set_index(['lat', 'lon'])
33 | if to_xarray:
34 | return df.to_xarray()
35 | else:
36 | return df
37 |
38 |
39 | def plot_results(processed_data=Path('data/processed'), target='ndvi',
40 | plot_difference=False, savefig=True):
41 | """Plots a landscape of the results (and optionally,
42 | of the ground truth)
43 | """
44 |
45 | preds = np.load(processed_data / target / 'arrays/preds.npy')
46 | true = np.load(processed_data / target / 'arrays/test/y.npy')
47 | latlon = np.load(processed_data / target / 'arrays/test/latlon.npy')
48 |
49 | preds_xr = create_dataset_from_vars(preds, latlon, "preds", to_xarray=True)
50 | true_xr = create_dataset_from_vars(true, latlon, "true", to_xarray=True)
51 |
52 | data_xr = xr.concat((preds_xr['preds'], true_xr['true']),
53 | pd.Index(['predictions', 'ground truth'], name='data'))
54 |
55 | if plot_difference:
56 | # compute the difference and create a difference plot
57 | data = data_xr.data[1] - data_xr.data[0]
58 | da = xr.DataArray(data, coords=[data_xr.lat, data_xr.lon], dims=['lat','lon'])
59 | ds = da.to_dataset('difference')
60 |
61 | ds.difference.plot(x='lon', y='lat', figsize=(12, 8))
62 | else:
63 | data_xr.plot(x='lon', y='lat', col='data', figsize=(15, 6))
64 |
65 | if savefig:
66 | plt.savefig(f'{target}_results.png', dpi=300, bbox_inches='tight')
67 | plt.show()
68 |
--------------------------------------------------------------------------------
/predictor/models/neural_networks/feedforward.py:
--------------------------------------------------------------------------------
1 | from torch import nn
2 |
3 | from pathlib import Path
4 |
5 | from .nn_base import NNBase
6 | from ...preprocessing import VALUE_COLS, VEGETATION_LABELS
7 |
8 |
9 | class FeedForward(NNBase):
10 | """A simple feedforward neural network
11 | """
12 |
13 | def __init__(self, data=Path('data'), arrays=Path('data/processed/arrays'),
14 | hide_vegetation=False):
15 |
16 | features_per_month = len(VALUE_COLS)
17 | if hide_vegetation:
18 | features_per_month -= len(VEGETATION_LABELS)
19 |
20 | num_features = features_per_month * 11
21 |
22 | super().__init__(LinearModel(num_features, [num_features], 0.25),
23 | data, arrays, hide_vegetation)
24 | self.model_name = "feedforward"
25 |
26 | class LinearModel(nn.Module):
27 |
28 | def __init__(self, input_size, layer_sizes, dropout):
29 | super().__init__()
30 | layer_sizes.insert(0, input_size)
31 |
32 | self.dense_layers = nn.ModuleList([
33 | LinearBlock(in_features=layer_sizes[i - 1],
34 | out_features=layer_sizes[i], dropout=dropout) for
35 | i in range(1, len(layer_sizes))
36 | ])
37 |
38 | self.final_dense = nn.Linear(in_features=layer_sizes[-1], out_features=1)
39 |
40 | self.init_weights()
41 |
42 | def init_weights(self):
43 | for dense_layer in self.dense_layers:
44 | nn.init.kaiming_uniform_(dense_layer.linear.weight.data)
45 |
46 | nn.init.kaiming_uniform_(self.final_dense.weight.data)
47 | # http://cs231n.github.io/neural-networks-2/#init
48 | # see: Initializing the biases
49 | nn.init.constant_(self.final_dense.bias.data, 0)
50 |
51 | def forward(self, x):
52 | # flatten
53 | x = x.view(x.shape[0], -1)
54 | for layer in self.dense_layers:
55 | x = layer(x)
56 |
57 | return self.final_dense(x)
58 |
59 |
60 | class LinearBlock(nn.Module):
61 | """
62 | A linear layer followed by batchnorm, a ReLU activation, and dropout
63 | """
64 |
65 | def __init__(self, in_features, out_features, dropout=0.25):
66 | super().__init__()
67 | self.linear = nn.Linear(in_features=in_features, out_features=out_features, bias=False)
68 | self.relu = nn.LeakyReLU(negative_slope=0.1, inplace=True)
69 | self.batchnorm = nn.BatchNorm1d(num_features=out_features)
70 | self.dropout = nn.Dropout(dropout)
71 |
72 | def forward(self, x):
73 | x = self.relu(self.batchnorm(self.linear(x)))
74 | return self.dropout(x)
75 |
--------------------------------------------------------------------------------
/data/preprocessing/gleam_cleaner.py:
--------------------------------------------------------------------------------
1 | """gleam_cleaner.py"""
2 | from pathlib import Path
3 | import xarray as xr
4 | import numpy as np
5 | import pandas as pd
6 |
7 | import ipdb
8 | import warnings
9 | import os
10 |
11 | from preprocessing.utils import (
12 | gdal_reproject,
13 | bands_to_time,
14 | convert_to_same_grid,
15 | select_same_time_slice,
16 | save_netcdf,
17 | get_holaps_mask,
18 | merge_data_arrays,
19 | )
20 |
21 | from preprocessing.cleaner import Cleaner
22 |
23 | class GleamCleaner(Cleaner):
24 | def __init__(self):
25 | self.base_data_path = Path("/soge-home/projects/crop_yield/EGU_compare/")
26 | reference_data_path = self.base_data_path / "holaps_EA_clean.nc"
27 |
28 | # CHANGE THIS PATH:
29 | # ----------------------------------------------------------------------
30 | data_path = self.base_data_path / "EA_chirps_monthly.nc"
31 | # ----------------------------------------------------------------------
32 |
33 | # open the reference dataset
34 | self.reference_data_path = Path(reference_data_path)
35 | self.reference_ds = xr.open_dataset(self.reference_data_path).holaps_evapotranspiration
36 |
37 | # initialise the object using methods from the parent class
38 | super(ChirpsCleaner, self).__init__(data_path=data_path)
39 |
40 | # extract the variable of interest (TO xr.DataArray)
41 | self.update_clean_data(
42 | self.raw_data.precip, msg="Extract Precipitation from CHIRPS xr.Dataset"
43 | )
44 |
45 | # make the mask (FROM REFERENCE_DS) to copy to this dataset too
46 | self.get_mask()
47 | # self.mask = self.mask.drop('units')
48 |
49 | def get_mask(self):
50 | self.mask = get_holaps_mask(self.reference_ds)
51 |
52 | def convert_units(self):
53 | # convert unit label to 'mm day-1'
54 | self.clean_data.attrs["units"] = "mm day-1"
55 |
56 |
57 | def preprocess(self):
58 | # Resample the timesteps to END OF MONTH
59 | self.resample_time(resample_str="M")
60 | # select the correct time slice
61 | self.correct_time_slice()
62 | # update the units
63 | self.convert_units()
64 | # regrid to same as reference data (holaps)
65 | self.regrid_to_reference()
66 | # ipdb.set_trace()
67 | # use the same mask as HOLAPS
68 | self.use_reference_mask() # THIS GOING WRONG
69 | # rename data
70 | self.rename_xr_object("gleam_evapotranspiration")
71 | # save data
72 | save_netcdf(
73 | self.clean_data, filepath=self.base_data_path / "gleam_EA_clean.nc"
74 | )
75 | print("\n\n GLEAM Preprocessed \n\n")
76 | return
77 |
--------------------------------------------------------------------------------
/data/preprocessing/chirps_cleaner.py:
--------------------------------------------------------------------------------
1 | """chirps_cleaner.py"""
2 | from pathlib import Path
3 | import xarray as xr
4 | import numpy as np
5 | import pandas as pd
6 |
7 | import ipdb
8 | import warnings
9 | import os
10 |
11 | from preprocessing.utils import (
12 | gdal_reproject,
13 | bands_to_time,
14 | convert_to_same_grid,
15 | select_same_time_slice,
16 | save_netcdf,
17 | get_holaps_mask,
18 | merge_data_arrays,
19 | )
20 |
21 | from preprocessing.cleaner import Cleaner
22 |
23 | class ChirpsCleaner(Cleaner):
24 | def __init__(self):
25 | self.base_data_path = Path("/soge-home/projects/crop_yield/EGU_compare/")
26 | reference_data_path = self.base_data_path / "holaps_EA_clean.nc"
27 |
28 | # CHANGE THIS PATH:
29 | # ----------------------------------------------------------------------
30 | data_path = self.base_data_path / "EA_chirps_monthly.nc"
31 | # ----------------------------------------------------------------------
32 |
33 | # open the reference dataset
34 | self.reference_data_path = Path(reference_data_path)
35 | self.reference_ds = xr.open_dataset(self.reference_data_path).holaps_evapotranspiration
36 |
37 | # initialise the object using methods from the parent class
38 | super(ChirpsCleaner, self).__init__(data_path=data_path)
39 |
40 | # extract the variable of interest (TO xr.DataArray)
41 | self.update_clean_data(
42 | self.raw_data.precip, msg="Extract Precipitation from CHIRPS xr.Dataset"
43 | )
44 |
45 | # make the mask (FROM REFERENCE_DS) to copy to this dataset too
46 | self.get_mask()
47 | # self.mask = self.mask.drop('units')
48 |
49 |
50 | def convert_units(self):
51 | daily_mm = self.clean_data / 30.417
52 | daily_mm.attrs.units = 'mm day-1'
53 | self.update_clean_data(daily_mm, msg="Change the mm month-1 values to mm day-1")
54 |
55 |
56 | def preprocess(self):
57 | # Resample the timesteps to END OF MONTH
58 | self.resample_time(resample_str="M")
59 | # select the correct time slice
60 | self.correct_time_slice()
61 | # update the units
62 | self.convert_units()
63 | # latitude,longitude => lat,lon
64 | self.rename_lat_lon()
65 | # regrid to same as reference data (holaps)
66 | self.regrid_to_reference(method="bilinear")
67 | # ipdb.set_trace()
68 | # use the same mask as HOLAPS
69 | self.use_reference_mask() # THIS GOING WRONG
70 | # rename data
71 | self.rename_xr_object("chirps_precipitation")
72 | # save data
73 | save_netcdf(
74 | self.clean_data, filepath=self.base_data_path / "chirps_EA_clean.nc"
75 | )
76 | print("\n\n CHIRPS Preprocessed \n\n")
77 | return
78 |
--------------------------------------------------------------------------------
/predictor/models/base.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | import numpy as np
3 | from collections import namedtuple
4 |
5 | from sklearn.metrics import mean_squared_error
6 | from ..preprocessing import VALUE_COLS, VEGETATION_LABELS
7 |
8 | DataTuple = namedtuple('Data', ['x', 'y', 'latlon', 'years'])
9 |
10 |
11 | class ModelBase:
12 | """Base for all machine learning models.
13 |
14 | Attributes:
15 | ----------
16 | arrays: pathlib.Path
17 | The location where the arrays were saved by the `Engineer` class
18 | hide_vegetation: bool, default: False
19 | Whether to hide vegetation-specific information from the training
20 | data. This allows us to better understand how the other factors drive
21 | vegetation health.
22 | """
23 |
24 | model_name = None # to be added by the model classes
25 |
26 | def __init__(self, data=Path('data'), arrays_path=Path('data/processed/arrays'),
27 | hide_vegetation=False):
28 | self.data_path = data
29 | self.arrays_path = arrays_path
30 | self.hide_vegetation = hide_vegetation
31 | self.model = None # to be added by the model classes
32 |
33 | def train(self):
34 | raise NotImplementedError
35 |
36 | def predict(self):
37 | # This method should return the predictions, and
38 | # the corresponding true values, read from the test
39 | # arrays
40 | raise NotImplementedError
41 |
42 | def save_model(self):
43 | # This method should save the model in data / model_name
44 | raise NotImplementedError
45 |
46 | def evaluate(self, return_eval=False, save_preds=False):
47 | """Evaluates the model using root mean squared error.
48 | This ensures evaluation is consistent across different models.
49 |
50 | Parameters:
51 | ----------
52 | return_eval: bool, default: False
53 | Whether to return the calculated root mean squared error
54 | save_preds: bool, default: False
55 | Whether to save the predictions. If True, they will be saved
56 | in self.arrays_path / preds.npy
57 |
58 | Returns:
59 | ----------
60 | (if return_eval) test_rmse: float
61 | The calculated root mean squared error for the test set
62 | """
63 | y_true, y_pred = self.predict()
64 |
65 | test_rmse = np.sqrt(mean_squared_error(y_true, y_pred))
66 |
67 | print(f'Test set RMSE: {test_rmse}')
68 |
69 | if save_preds:
70 | savedir = self.data_path / self.model_name
71 | if not savedir.exists(): savedir.mkdir()
72 | print(f'Saving predictions to {savedir / "preds.npy"}')
73 | np.save(savedir / 'preds.npy', y_pred)
74 |
75 | if return_eval:
76 | return test_rmse
77 |
78 | def load_arrays(self, mode='train'):
79 |
80 | arrays_path = self.arrays_path / mode
81 |
82 | x = np.load(arrays_path / 'x.npy')
83 |
84 | if self.hide_vegetation:
85 | if mode == 'train':
86 | print('Training model without vegetation features')
87 | indices_to_keep = [idx for idx, val in enumerate(VALUE_COLS) if val not in VEGETATION_LABELS]
88 |
89 | x = x[:, :, indices_to_keep]
90 |
91 | return DataTuple(
92 | latlon=np.load(arrays_path / 'latlon.npy'),
93 | years=np.load(arrays_path / 'years.npy'),
94 | x=x,
95 | y=np.load(arrays_path / 'y.npy'))
96 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Vegetation Health
2 |
3 | Predicting vegetation health from precipitation and temperature
4 |
5 | ## Introduction
6 |
7 | This repository experiments with different machine learning models to predict drought indices in East Africa
8 | (specifically the Normalized Difference Vegetation Index) using temperature and precipitation data.
9 |
10 | ## Results
11 |
12 | Models are trained on data before 2016, and evaluated on 2016 data. Vegetation health in June is being predicted.
13 |
14 | In addition, vegetation health can be hidden from the model to better understand the effects of the other features.
15 |
16 | | Model | RMSE | RMSE (no veg) |
17 | |:------------------------:|:----:|:-------------:|
18 | |Linear Regression |0.040 |0.084 |
19 | |Feedforward neural network|0.038 |0.070 |
20 | |Recurrent neural network |0.035 |0.060 |
21 |
22 | The results of the models can also be compared visually with the ground truths (the example below is from the baseline
23 | logistic regression):
24 |
25 |
26 |
27 | In addition, the effects of the inputs on the models' predictions are investigated using [shap values](https://github.com/slundberg/shap)
28 | in Jupyter Notebooks, for both the [feedforward neural network](notebooks/04_gt_feedforward_model.ipynb) and the
29 | [recurrent neural network](notebooks/08_gt_recurrent_model.ipynb).
30 |
31 | ## Pipeline
32 |
33 | [Python Fire](https://github.com/google/python-fire) is used to generate a CLI.
34 |
35 | ### Data cleaning
36 |
37 | Normalize values from the original csv file, remove null values, add a year series.
38 |
39 | ```bash
40 | python run.py clean
41 | ```
42 | A target can be selected by adding the flag `--target`, e.g. `--target=ndvi_anomaly`.
43 | By default, the target is `ndvi`. The selected target must be in
44 | [`predictor.preprocessing.VALUE_COLS`](predictor/preprocessing.py).
45 |
46 | The original data is currently generated using datasets on the Oxford University cluster, using the scripts
47 | in [`data`](data).
48 |
49 | ### Data Processing
50 |
51 | Turn the CSV into `numpy` arrays which can be input into the model.
52 |
53 | ```bash
54 | python run.py engineer
55 | ```
56 |
57 | ### Models
58 |
59 | 3 models have been implemented: a baseline linear regression, a feedforward neural network and a
60 | recurrent neural network. They can be selected using the `--model_type` flag.
61 |
62 | ```bash
63 | python run.py train_model
64 | ```
65 |
66 | ## Setup
67 |
68 | [Anaconda](https://www.anaconda.com/download/#macos) running python 3.7 is used as the package manager. To get set up
69 | with an environment, install Anaconda from the link above, and (from this directory) run
70 |
71 | ```bash
72 | conda env create -f environment.yml
73 | ```
74 | This will create an environment named `vegetation_health` with all the necessary packages to run the code. To
75 | activate this environment, run
76 |
77 | ```bash
78 | conda activate vegetation_health
79 | ```
80 |
81 | ## Additional Notes
82 |
83 | - The following variables are used by the model: `['lst_night', 'lst_day', 'precip', 'sm', 'ndvi', 'evi', 'ndvi_anomaly']`.
84 | They are all from different sources
85 |
86 |
87 | - East Africa is defined here as the area of the original `.nc` file (`spi_spei.nc`)
88 |
89 | lat min, lat max : `-4.9750023`, `15.174995`
90 |
91 | lon min, lon max : `32.524994`, `48.274994`
92 |
93 | This makes the following bounding box: (left, bottom, right, top): `(32.524994, -4.9750023, 15.174995, 48.274994)`
94 |
--------------------------------------------------------------------------------
/data/globmap_lookup.py:
--------------------------------------------------------------------------------
1 | """
2 | The lookup values for the GLOBMAP landcover classes
3 | (defined in ee_extract_landcover.js)
4 | """
5 | globmap_lookup = {
6 | 11 : "Post-flooding or irrigated croplands",
7 | 14 : "Rainfed croplands",
8 | 20 : "Mosaic cropland (50-70%) / vegetation (grassland, shrubland, forest) (20-50%)",
9 | 30 : "Mosaic vegetation (grassland, shrubland, forest) (50-70%) / cropland (20-50%)",
10 | 40 : "Closed to open (>15%) broadleaved evergreen and/or semi-deciduous forest (>5m)",
11 | 50 : "Closed (>40%) broadleaved deciduous forest (>5m)",
12 | 60 : "Open (15-40%) broadleaved deciduous forest (>5m)",
13 | 70 : "Closed (>40%) needleleaved evergreen forest (>5m)",
14 | 90 : "Open (15-40%) needleleaved deciduous or evergreen forest (>5m)",
15 | 100 : "Closed to open (>15%) mixed broadleaved and needleleaved forest (>5m)",
16 | 110 : "Mosaic forest-shrubland (50-70%) / grassland (20-50%)",
17 | 120 : "Mosaic grassland (50-70%) / forest-shrubland (20-50%)",
18 | 130 : "Closed to open (>15%) shrubland (<5m)",
19 | 140 : "Closed to open (>15%) grassland",
20 | 150 : "Sparse (>15%) vegetation (woody vegetation, shrubs, grassland)",
21 | 160 : "Closed (>40%) broadleaved forest regularly flooded - Fresh water",
22 | 170 : "Closed (>40%) broadleaved semi-deciduous and/or evergreen forest regularly flooded - saline water",
23 | 180 : "Closed to open (>15%) vegetation (grassland, shrubland, woody vegetation) on regularly flooded or waterlogged soil",
24 | 190 : "Artificial surfaces and associated areas (urban areas >50%) GLOBCOVER 2009",
25 | 200 : "Bare areas",
26 | 210 : "Water bodies",
27 | 220 : "Permanent snow and ice",
28 | 230 : "Unclassified",
29 | }
30 |
31 | globmap_lookup_tommy = {
32 | 11 : "irrigated croplands",
33 | 14 : "Rainfed croplands",
34 | 20 : "cropland/vegetation",
35 | 30 : "vegetation/cropland",
36 | 40 : "broad-leaved evergreen/semi-deciduous forest",
37 | 50 : "broad-leaved deciduous forest",
38 | 60 : "broad-leaved deciduous forest",
39 | 70 : "needle-leaved evergreen forest",
40 | 90 : "needle-leaved deciduous / evergreen forest",
41 | 100 : "broad-leaved / needleleaved forest",
42 | 110 : "forest-shrubland / grassland",
43 | 120 : "Mosaic grassland / forest-shrubland",
44 | 130 : "shrubland",
45 | 140 : "grassland",
46 | 150 : "Sparse vegetation",
47 | 160 : "forest regularly flooded - Fresh water",
48 | 170 : "forest regularly flooded - saline water",
49 | 180 : "vegetation on flooded soil",
50 | 190 : "Artificial",
51 | 200 : "Bare areas",
52 | 210 : "Water bodies",
53 | 220 : "Permanent snow and ice",
54 | 230 : "Unclassified",
55 | }
56 |
57 |
58 | globmap_lookup2 = {
59 | # np.nan : "NA",
60 | 11 : "cropland",
61 | 14 : "cropland",
62 | 20 : "cropland",
63 | 30 : "cropland",
64 | 40 : "forest",
65 | 50 : "forest",
66 | 60 : "forest",
67 | 70 : "forest",
68 | 90 : "forest",
69 | 100 : "forest",
70 | 110 : "shrubland/grassland",
71 | 120 : "shrubland/grassland",
72 | 130 : "shrubland",
73 | 140 : "grassland",
74 | 150 : "Sparse vegetation",
75 | 160 : "flooded",
76 | 170 : "flooded",
77 | 180 : "flooded",
78 | 190 : "Artificial",
79 | 200 : "Bare areas",
80 | 210 : "Water bodies",
81 | 220 : "Permanent snow and ice",
82 | 230 : "Unclassified",
83 | }
84 |
85 | remap_to_int_dict = {
86 | # "NA": 1,
87 | "cropland": 2,
88 | "forest": 3,
89 | "shrubland/grassland": 4,
90 | "shrubland": 5,
91 | "grassland": 6,
92 | "Sparse vegetation": 7,
93 | "flooded": 8,
94 | "Artificial": 9,
95 | "Bare areas": 10,
96 | "Water bodies": 11,
97 | "Permanent snow and ice": 12,
98 | "Unclassified": 13,
99 | }
100 |
101 | globmap_lookup3 = dict(zip(remap_to_int_dict.values(), remap_to_int_dict.keys()))
102 |
--------------------------------------------------------------------------------
/data/preprocessing/collect_regrid_data_script.py:
--------------------------------------------------------------------------------
1 | """
2 | @tommylees112
3 |
4 | An awful script (sorry Gabi) the data is read in separately for each product.
5 | They are then preprocessed:
6 | resample_time
7 | select_time_slice
8 | regrid_to_reference
9 |
10 | using the precipitation data as reference.
11 | """
12 | # test.py
13 | import xarray as xr
14 | from pathlib import Path
15 | import matplotlib.pyplot as plt
16 |
17 | from preprocessing.utils import (
18 | gdal_reproject,
19 | bands_to_time,
20 | convert_to_same_grid,
21 | select_same_time_slice,
22 | save_netcdf,
23 | get_holaps_mask,
24 | merge_data_arrays,
25 | )
26 |
27 |
28 | def correct_time_slice(ds, reference_ds):
29 | """select the same time slice as the reference data"""
30 | correct_time_slice = select_same_time_slice(reference_ds, ds)
31 |
32 | return correct_time_slice
33 |
34 |
35 | def resample_time(ds, resample_str="M"):
36 | """ should resample to the given timestep """
37 | resampled_time_data = ds.resample(time=resample_str).first()
38 | return resampled_time_data
39 |
40 |
41 | def regrid_to_reference(ds, reference_ds, method="nearest_s2d"):
42 | """ regrid data (spatially) onto the same grid as reference data """
43 |
44 | regrid_data = convert_to_same_grid(
45 | reference_ds, ds, method=method
46 | )
47 |
48 | return regrid_data
49 |
50 |
51 | def use_reference_mask(ds, mask, one_time=False):
52 | # if only one timestep (e.g. landcover) then convert to one time
53 | if one_time:
54 | self.mask = self.mask.isel(time=0)
55 |
56 | masked_d = ds.where(~mask.values)
57 | return masked_d
58 |
59 |
60 | def rename_lat_lon(ds):
61 | rename_latlon = ds.rename({"longitude": "lon", "latitude": "lat"})
62 | return rename_latlon
63 |
64 |
65 | def select_time_slice(ds, timemin, timemax):
66 | return ds.sel(time=slice(timemin, timemax))
67 |
68 |
69 | # ------------------------------------------------------------------------------
70 | # Read the data
71 | # ------------------------------------------------------------------------------
72 | DATA_DIR1 = Path("/soge-home/users/chri4118/EA_data")
73 | DATA_DIR2 = Path("/soge-home/projects/crop_yield/EGU_compare")
74 |
75 | et = xr.open_dataset(DATA_DIR1 / "ET_EastAfrica.nc")
76 | lst = xr.open_dataset(DATA_DIR1 / "LST_EastAfrica.nc")[["lst_day", "lst_night"]]
77 | sm = xr.open_dataset(DATA_DIR1 / "SM_EastAfrica.nc")[['sm','sm_uncertainty']]
78 | ndvi = xr.open_dataset(DATA_DIR1 / "NDVI_EastAfrica.nc")[["ndvi", "evi"]]
79 | precip = xr.open_dataset(DATA_DIR2 / "EA_chirps_monthly.nc")
80 |
81 | # ------------------------------------------------------------------------------
82 | # Clean the data (same timesteps and same gridsizes)
83 | # ------------------------------------------------------------------------------
84 | # RESAMPLE THE REFERENCE DATA
85 | precip = select_time_slice(precip, '2000-02-14','2016-12-01')
86 | precip = resample_time(precip)
87 | precip = rename_lat_lon(precip)
88 | reference_ds = precip
89 |
90 | all_vars = [et,lst,sm,ndvi,precip]
91 | names = ["et","lst","sm","ndvi","precip"]
92 |
93 | # RESAMPLE data (except 'precip')
94 | out = []
95 | for ix, ds in enumerate(all_vars[:-1]):
96 | name = names[ix]
97 | print(f"\n*** working on ds: {name} ***")
98 | # select same time slice
99 | ds = resample_time(ds)
100 | ds = select_time_slice(ds, '2000-02-14','2016-12-01')
101 | # ds = correct_time_slice(ds, reference_ds)
102 | print("selected same time slice")
103 | # convert to same grid
104 | ds = regrid_to_reference(ds, reference_ds)
105 | print("converted to same grid")
106 | out.append(ds)
107 |
108 | # ------------------------------------------------------------------------------
109 | # Merge all of the datasets
110 | # ------------------------------------------------------------------------------
111 | alldata = out + [precip]
112 | OUT = xr.merge(alldata)
113 |
114 | # ------------------------------------------------------------------------------
115 | # Save the data to netcdf format
116 | # ------------------------------------------------------------------------------
117 | OUT.to_netcdf(DATA_DIR1 / "OUT2.nc")
118 | OUT.to_netcdf(DATA_DIR2 / "predict_vegetation_health.nc")
119 |
--------------------------------------------------------------------------------
/data/preprocessing/spei_cleaner.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | import xarray as xr
3 | import numpy as np
4 |
5 | import ipdb
6 | import warnings
7 | import os
8 |
9 | from preprocessing.utils import (
10 | gdal_reproject,
11 | bands_to_time,
12 | convert_to_same_grid,
13 | select_same_time_slice,
14 | save_netcdf,
15 | get_holaps_mask,
16 | merge_data_arrays,
17 | )
18 |
19 | from preprocessing.cleaner import Cleaner
20 |
21 |
22 | # ------------------------------------------------------------------------------
23 | # HOLAPS cleaner
24 | # ------------------------------------------------------------------------------
25 |
26 |
27 | class SpeiCleaner(Cleaner):
28 | """Preprocess the HOLAPS dataset"""
29 | assert False, "This is just boilerplate code from another"
30 | def __init__(self):
31 | # init data paths (should be arguments)
32 | self.base_data_path = Path("/soge-home/projects/crop_yield/EGU_compare/")
33 | data_path = self.base_data_path / "holaps_africa.nc"
34 | reproject_path = self.base_data_path / "holaps_africa_reproject.nc"
35 |
36 | super(HolapsCleaner, self).__init__(data_path=data_path)
37 | self.reproject_path = Path(reproject_path)
38 |
39 |
40 | def chop_EA_region(self, outfile_path):
41 | """ cheeky little bit of bash scripting with string interpolation (kids don't try this at home) """
42 | in_file = self.base_data_path / "holaps_reprojected.nc"
43 | out_file = self.base_data_path / "holaps_EA.nc"
44 | lonmin = 32.6
45 | lonmax = 51.8
46 | latmin = -5.0
47 | latmax = 15.2
48 |
49 | cmd = (
50 | f"cdo sellonlatbox,{lonmin},{lonmax},{latmin},{latmax} {in_file} {out_file}"
51 | )
52 | print(f"Running command: {cmd}")
53 | os.system(cmd)
54 | print("Chopped East Africa from the Reprojected data")
55 | re_chopped_data = xr.open_dataset(out_file)
56 | self.update_clean_data(
57 | re_chopped_data, msg="Opened the reprojected & chopped data"
58 | )
59 | return
60 |
61 |
62 | def reproject(self):
63 | """ reproject to WGS84 / geographic latlon """
64 | if not self.reproject_path.is_file():
65 | gdal_reproject(infile=self.data_path, outfile=self.reproject_path)
66 |
67 | repr_data = xr.open_dataset(self.reproject_path)
68 |
69 | # get the timestamps from the original holaps data
70 | h_times = self.clean_data.time
71 | # each BAND is a time (multiple raster images 1 per time)
72 | repr_data = bands_to_time(repr_data, h_times, var_name="LE_Mean")
73 |
74 | # TODO: ASSUMPTION / PROBLEM
75 | warnings.warn(
76 | "TODO: No idea why but the values appear to be 10* bigger than the pre-reprojected holaps data"
77 | )
78 | repr_data /= 10 # WHY ARE THE VALUES 10* bigger?
79 |
80 | self.update_clean_data(repr_data, "Data Reprojected to WGS84")
81 |
82 | save_netcdf(
83 | self.clean_data, filepath=self.base_data_path / "holaps_reprojected.nc"
84 | )
85 | return
86 |
87 |
88 | def convert_units(self):
89 | # Convert from latent heat (w m-2) to evaporation (mm day-1)
90 | holaps_mm = self.clean_data / 28
91 | holaps_mm = holaps_mm.LE_Mean
92 | holaps_mm.name = "Evapotranspiration"
93 | holaps_mm.attrs["units"] = "mm day-1 [w m-2 / 28]"
94 | self.update_clean_data(
95 | holaps_mm, msg="Transform Latent Heat (w m-2) to Evaporation (mm day-1)"
96 | )
97 |
98 | return
99 |
100 | def preprocess(self):
101 | # reproject the file from sinusoidal to WGS84 / 'ESPG:4326'
102 | self.reproject()
103 | # chop out the correct lat/lon (changes when reprojected)
104 | self.chop_EA_region()
105 | # convert the units
106 | self.convert_units()
107 | # rename data
108 | self.rename_xr_object("holaps_evapotranspiration")
109 | # resample the time units
110 | self.resample_time()
111 | # save the netcdf file (used as reference data for MODIS and GLEAM)
112 | save_netcdf(
113 | self.clean_data, filepath=self.base_data_path / "holaps_EA_clean.nc",
114 | force=True
115 | )
116 | print("\n\n HOLAPS Preprocessed \n\n")
117 | return
118 |
--------------------------------------------------------------------------------
/predictor/engineer.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | import pandas as pd
3 | import numpy as np
4 |
5 | from .preprocessing import VALUE_COLS
6 |
7 |
8 | class Engineer:
9 | """Take the clean csv file and turn it into numpy arrays ready
10 | for training
11 |
12 | Attributes:
13 | ----------
14 | cleaned_data: pathlib.Path
15 | The location of cleaned data, as saved by the `Cleaner` class
16 | arrays: pathlib.Path
17 | The location where the arrays will be saved
18 | """
19 |
20 | def __init__(self, cleaned_data=Path('data/processed/cleaned_data.csv'),
21 | arrays=Path('data/processed/arrays')):
22 |
23 | self.arrays_path = arrays
24 | if not self.arrays_path.exists():
25 | self.arrays_path.mkdir()
26 | self.cleaned_data_path = cleaned_data
27 |
28 | def readfile(self):
29 | return pd.read_csv(self.cleaned_data_path)
30 |
31 | def process(self, test_year=2016):
32 | """Takes the processed data saved by the `preprocessing.Cleaner` class,
33 | and turns it into `np.array`s which can be ingested by the machine learning
34 | models
35 |
36 | Parameters
37 | ----------
38 | test_year: int, default: 2016
39 | Data from this year will be used for testing, and so will be saved in
40 | seperate arrays
41 |
42 | The following are saved, for both the training and test sets:
43 |
44 | {train, test}/latlon.npy:
45 | The locations of each data instance (so that latlon[i] represents the latitude and
46 | longitude of the ith data point
47 | {train, test}/years.npy:
48 | The years of each data point. Specifically, this represents the prediction year, so if
49 | `pred_month` passed to the Cleaner = 6, then if years[i] = 2015, that means y[i] is the
50 | value of the target in June 2015.
51 | {train, test}/x.npy:
52 | The training data; the previous 11 months of data
53 | {train, test}/y.npy:
54 | The test data - the value of the target variable at `pred_month`.
55 | """
56 | data = self.readfile()
57 |
58 | # outputs
59 | latlons, years, vals, targets = [], [], [], []
60 |
61 | skipped = 0
62 | # first, groupby lat, lon, so that we process the same place together
63 | for latlon, group in data.groupby(by=['lat', 'lon']):
64 | latlon_np = np.array(latlon)
65 | for year, subgroup in group.groupby(by='gb_year'):
66 | if len(subgroup) != 12:
67 | # print(f'Ignoring data from {year} at {latlon} due to missing rows')
68 | skipped += 1
69 | continue
70 | subgroup = subgroup.sort_values(by='gb_month', ascending=True)
71 |
72 | # create a np.array of the features (VALUE_COLS) and the target
73 | x = subgroup[:-1][VALUE_COLS].values
74 | y = subgroup.iloc[-1]['target']
75 |
76 | # create lists of np.arrays
77 | latlons.append(latlon_np)
78 | years.append(year)
79 | vals.append(x)
80 | targets.append(y)
81 |
82 | if len(latlons) % 1000 == 0:
83 | print(f'Processed {len(latlons)} examples')
84 |
85 | print(f'Done processing {len(latlons)} pixel-years! Skipped {skipped} pixel-years due to missing rows')
86 |
87 | # turn everything into np arrays for manipulation
88 | latlons, years, vals, targets = np.vstack(latlons), np.array(years), np.stack(vals), np.array(targets)
89 |
90 | # split into train and test sets
91 | test_idx = np.where(years == test_year)[0]
92 | train_idx = np.where(years < test_year)[0]
93 |
94 | test_arrays = self.arrays_path / 'test'
95 | train_arrays = self.arrays_path / 'train'
96 |
97 | test_arrays.mkdir(parents=True, exist_ok=True)
98 | train_arrays.mkdir(exist_ok=True)
99 |
100 | print('Saving data')
101 | np.save(train_arrays / 'latlon.npy', latlons[train_idx])
102 | np.save(train_arrays / 'years.npy', years[train_idx])
103 | np.save(train_arrays / 'x.npy', vals[train_idx])
104 | np.save(train_arrays / 'y.npy', targets[train_idx])
105 |
106 | np.save(test_arrays / 'latlon.npy', latlons[test_idx])
107 | np.save(test_arrays / 'years.npy', years[test_idx])
108 | np.save(test_arrays / 'x.npy', vals[test_idx])
109 | np.save(test_arrays / 'y.npy', targets[test_idx])
110 |
--------------------------------------------------------------------------------
/data/preprocessing/cleaner.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | import xarray as xr
3 | import numpy as np
4 |
5 | import ipdb
6 | import warnings
7 | import os
8 |
9 | from preprocessing.utils import (
10 | gdal_reproject,
11 | bands_to_time,
12 | convert_to_same_grid,
13 | select_same_time_slice,
14 | save_netcdf,
15 | get_holaps_mask,
16 | merge_data_arrays,
17 | )
18 |
19 |
20 | # ------------------------------------------------------------------------------
21 | # Base cleaner
22 | # ------------------------------------------------------------------------------
23 |
24 |
25 | class Cleaner:
26 | """Base class for preprocessing the input data.
27 |
28 | Tasks include:
29 | - Reprojecting
30 | - Putting datasets onto a consistent spatial grid (spatial resolution)
31 | - Converting to equivalent units
32 | - Converting to the same temporal resolution
33 | - Selecting the same time slice
34 |
35 | Design Considerations:
36 | - Have an attribute, clean_data', that is constantly updated
37 | - Keep a copy of the raw data for reference
38 | - Update the 'clean_data' each time a transformation is applied
39 | """
40 |
41 | def __init__(self, data_path):
42 | self.data_path = Path(data_path)
43 |
44 | # open the datasets using xarray
45 | self.raw_data = xr.open_dataset(self.data_path)
46 |
47 | # start with clean data as a copy of the raw data
48 | self.clean_data = self.raw_data.copy()
49 |
50 |
51 | def get_mask(self):
52 | self.mask = get_holaps_mask(self.reference_ds)
53 |
54 |
55 | def update_clean_data(self, clean_data, msg=""):
56 | """ """
57 | self.clean_data = clean_data
58 | print("***** self.clean_data Updated: ", msg, " *****")
59 |
60 | return
61 |
62 | def correct_time_slice(self):
63 | """select the same time slice as the reference data"""
64 | assert (
65 | self.reference_ds is not None
66 | ), "self.reference_ds does not exist! Likely because you're not using the MODIS or GLEAM cleaners / correct data paths"
67 | correct_time_slice = select_same_time_slice(self.reference_ds, self.clean_data)
68 |
69 | self.update_clean_data(
70 | correct_time_slice, msg="Selected the same time slice as reference data"
71 | )
72 | return
73 |
74 | def resample_time(self, resample_str="M"):
75 | """ should resample to the given timestep """
76 | resampled_time_data = self.clean_data.resample(time=resample_str).first()
77 | self.update_clean_data(resampled_time_data, msg="Resampled time ")
78 |
79 | return
80 |
81 | def regrid_to_reference(self, method="nearest_s2d"):
82 | """ regrid data (spatially) onto the same grid as referebce data """
83 | assert (
84 | self.reference_ds is not None
85 | ), "self.reference_ds does not exist! Likely because you're not using the MODIS or GLEAM cleaners / correct data paths"
86 |
87 | regrid_data = convert_to_same_grid(
88 | self.reference_ds, self.clean_data, method=method
89 | )
90 | # UPDATE THE SELF.CLEAN_DATA
91 | self.update_clean_data(regrid_data, msg="Data Regridded to same as HOLAPS")
92 | return
93 |
94 | def use_reference_mask(self, one_time=False):
95 | assert not 'units' in self.mask.coords, "MUST NOT HAVE EXTRA COORDS or you remove ALL values. self.mask has 'units' coord and needs to be dropped:\n self.mask = self.mask.drop('units')"
96 | assert (
97 | self.reference_ds is not None
98 | ), "self.reference_ds does not exist! Likely because you're not using the MODIS or GLEAM cleaners / correct data paths"
99 | assert (
100 | self.mask is not None
101 | ), "self.mask does not exist! Likely because you're not using the MODIS or GLEAM cleaners / correct data paths"
102 |
103 | # if only one timestep (e.g. landcover) then convert to one time
104 | if one_time:
105 | self.mask = self.mask.isel(time=0)
106 |
107 | masked_d = self.clean_data.where(~self.mask.values)
108 | self.update_clean_data(masked_d, msg="Copied the mask from reference to dataset!")
109 | return
110 |
111 | def mask_illegitimate_values(self):
112 | # mask out the missing values (coded as something else)
113 | return NotImplementedError
114 |
115 | def convert_units(self):
116 | """ convert to the equivalent units """
117 | raise NotImplementedError
118 |
119 | def rename_xr_object(self, name):
120 | renamed_data = self.clean_data.rename(name)
121 | self.update_clean_data(renamed_data, msg=f"Data renamed {name}")
122 | return
123 |
124 | def rename_lat_lon(self):
125 | rename_latlon = self.clean_data.rename({"longitude": "lon", "latitude": "lat"})
126 | self.update_clean_data(rename_latlon, msg="Renamed latitude,longitude => lat,lon")
127 | return
128 |
129 | def preprocess(self):
130 | """ The preprocessing steps (relatively unique for each dtype) """
131 | raise NotImplementedError
132 |
--------------------------------------------------------------------------------
/predictor/models/neural_networks/nn_base.py:
--------------------------------------------------------------------------------
1 | import torch
2 | from torch.nn import functional as F
3 | from torch.utils.data import TensorDataset, DataLoader, random_split
4 |
5 | from pathlib import Path
6 | from tqdm import tqdm
7 | import numpy as np
8 |
9 | from ..base import ModelBase, DataTuple
10 |
11 |
12 | class NNBase(ModelBase):
13 | """The base for neural networks models
14 | """
15 |
16 | def __init__(self, model, data=Path('data'), arrays=Path('data/processed/arrays'),
17 | hide_vegetation=False):
18 | super().__init__(data, arrays, hide_vegetation)
19 |
20 | self.device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
21 | # for reproducability
22 | torch.manual_seed(42)
23 | torch.cuda.manual_seed_all(42)
24 |
25 | if torch.cuda.is_available():
26 | model = model.cuda()
27 | self.model = model
28 |
29 | def train(self, num_epochs=100, patience=3, batch_size=32, learning_rate=1e-3):
30 | """Train the neural network
31 |
32 | Parameters
33 | ----------
34 | num_epochs: int
35 | The maximum number of epochs to train the model for
36 | patience: int
37 | If no improvement is seen in the validation set for `patience` epochs,
38 | training is stopped to prevent overfitting
39 | batch_size: int
40 | The batch size to use
41 | learning_rate: float
42 | The learning rate to use when updating model parameters
43 | """
44 | train_data = self.load_tensors(mode='train')
45 |
46 | # split the data into a training and validation set
47 | total_size = train_data.x.shape[0]
48 | val_size = total_size // 10 # 10 % for validation
49 | train_size = total_size - val_size
50 | print(f'After split, training on {train_size} examples, '
51 | f'validating on {val_size} examples')
52 | train_dataset, val_dataset = random_split(TensorDataset(train_data.x, train_data.y.unsqueeze(1)),
53 | (train_size, val_size))
54 |
55 | train_dataloader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
56 | val_dataloader = DataLoader(val_dataset, batch_size=batch_size)
57 |
58 | optimizer = torch.optim.Adam([pam for pam in self.model.parameters()],
59 | lr=learning_rate)
60 |
61 | epochs_without_improvement = 0
62 | best_loss = np.inf
63 |
64 | for epoch in range(num_epochs):
65 | self.model.train()
66 |
67 | epoch_train_loss = 0
68 | num_train_batches = 0
69 |
70 | epoch_val_loss = 0
71 | num_val_batches = 0
72 |
73 | for train_x, train_y in tqdm(train_dataloader):
74 | optimizer.zero_grad()
75 | pred_y = self.model(train_x)
76 |
77 | loss = F.smooth_l1_loss(pred_y, train_y)
78 | loss.backward()
79 | optimizer.step()
80 |
81 | num_train_batches += 1
82 | epoch_train_loss += loss.sqrt().item()
83 |
84 | self.model.eval()
85 | with torch.no_grad():
86 | for val_x, val_y in tqdm(val_dataloader):
87 | val_pred_y = self.model(val_x)
88 | val_loss = F.mse_loss(val_pred_y, val_y)
89 |
90 | num_val_batches += 1
91 | epoch_val_loss += val_loss.sqrt().item()
92 |
93 | epoch_train_loss /= num_train_batches
94 | epoch_val_loss /= num_val_batches
95 |
96 | print(f'Epoch {epoch} - Training RMSE: {epoch_train_loss}, '
97 | f'Validation RMSE: {epoch_val_loss}')
98 |
99 | if epoch_val_loss < best_loss:
100 | best_state = self.model.state_dict()
101 | best_loss = epoch_val_loss
102 |
103 | epochs_without_improvement = 0
104 | else:
105 | epochs_without_improvement += 1
106 | if epochs_without_improvement == patience:
107 | self.model.load_state_dict(best_state)
108 | print('Early stopping!')
109 | return
110 | self.model.load_state_dict(best_state)
111 |
112 | def predict(self, batch_size=64):
113 | test_data = self.load_tensors(mode='test')
114 |
115 | test_dataloader = DataLoader(TensorDataset(test_data.x, test_data.y),
116 | batch_size=batch_size)
117 |
118 | output_preds, output_true = [], []
119 |
120 | self.model.eval()
121 | with torch.no_grad():
122 | for test_x, test_y in tqdm(test_dataloader):
123 | output_preds.append(self.model(test_x).squeeze(1).cpu().numpy())
124 | output_true.append(test_y.cpu().numpy())
125 | return np.concatenate(output_true), np.concatenate(output_preds)
126 |
127 | def load_tensors(self, mode='train'):
128 | data = self.load_arrays(mode)
129 |
130 | return DataTuple(
131 | latlon=data.latlon,
132 | years=data.years,
133 | x=torch.as_tensor(data.x, device=self.device).float(),
134 | y=torch.as_tensor(data.y, device=self.device).float())
135 |
136 | def save_model(self, name="model.pt"):
137 | """save the model's state_dict"""
138 | savedir = self.data_path / self.model_name
139 | if not savedir.exists(): savedir.mkdir()
140 |
141 | # save with .pt extension
142 | if not '.pt' in name: name += '.pt'
143 | print(f'Saving predictions to {savedir / name}')
144 | torch.save(self.model, savedir / name)
145 |
--------------------------------------------------------------------------------
/predictor/models/neural_networks/recurrent.py:
--------------------------------------------------------------------------------
1 | import torch
2 | from torch import nn
3 |
4 | import math
5 | from pathlib import Path
6 |
7 | from .nn_base import NNBase
8 | from ...preprocessing import VALUE_COLS, VEGETATION_LABELS
9 |
10 |
11 | class Recurrent(NNBase):
12 | """A simple feedforward neural network
13 | """
14 |
15 | def __init__(self, data=Path('data'), arrays=Path('data/processed/arrays'),
16 | hide_vegetation=False):
17 |
18 | features_per_month = len(VALUE_COLS)
19 | if hide_vegetation:
20 | features_per_month -= len(VEGETATION_LABELS)
21 |
22 | super().__init__(RNN(features_per_month, [256]),
23 | data, arrays, hide_vegetation)
24 | self.model_name = "recurrent"
25 |
26 | class RNN(nn.Module):
27 | """
28 | A crop yield conv net.
29 | For a description of the parameters, see the RNNModel class.
30 | """
31 | def __init__(self, features_per_month, dense_features, hidden_size=128,
32 | rnn_dropout=0.25):
33 | super().__init__()
34 |
35 | dense_features.insert(0, hidden_size)
36 | if dense_features[-1] != 1:
37 | dense_features.append(1)
38 |
39 | self.dropout = nn.Dropout(rnn_dropout)
40 | self.rnn = UnrolledRNN(input_size=features_per_month,
41 | hidden_size=hidden_size,
42 | batch_first=True)
43 | self.hidden_size = hidden_size
44 |
45 | self.dense_layers = nn.ModuleList([
46 | nn.Linear(in_features=dense_features[i-1],
47 | out_features=dense_features[i])
48 | for i in range(1, len(dense_features))
49 | ])
50 |
51 | self.initialize_weights()
52 |
53 | def initialize_weights(self):
54 |
55 | sqrt_k = math.sqrt(1 / self.hidden_size)
56 | for parameters in self.rnn.parameters():
57 | for pam in parameters:
58 | nn.init.uniform_(pam.data, -sqrt_k, sqrt_k)
59 |
60 | for dense_layer in self.dense_layers:
61 | nn.init.kaiming_uniform_(dense_layer.weight.data)
62 | nn.init.constant_(dense_layer.bias.data, 0)
63 |
64 | def forward(self, x):
65 | """
66 | If return_last_dense is true, the feature vector generated by the second to last
67 | dense layer will also be returned. This is then used to train a Gaussian Process model.
68 | """
69 |
70 | sequence_length = x.shape[1]
71 |
72 | hidden_state = torch.zeros(1, x.shape[0], self.hidden_size)
73 | cell_state = torch.zeros(1, x.shape[0], self.hidden_size)
74 |
75 | if x.is_cuda:
76 | hidden_state = hidden_state.cuda()
77 | cell_state = cell_state.cuda()
78 |
79 | for i in range(sequence_length):
80 | # The reason the RNN is unrolled here is to apply dropout to each timestep;
81 | # The rnn_dropout argument only applies it after each layer.
82 | # https://www.tensorflow.org/api_docs/python/tf/nn/rnn_cell/DropoutWrapper
83 | input_x = x[:, i, :].unsqueeze(1)
84 | _, (hidden_state, cell_state) = self.rnn(input_x,
85 | (hidden_state, cell_state))
86 | hidden_state = self.dropout(hidden_state)
87 |
88 | x = hidden_state.squeeze(0)
89 | for layer_number, dense_layer in enumerate(self.dense_layers):
90 | x = dense_layer(x)
91 | return x
92 |
93 |
94 | class UnrolledRNN(nn.Module):
95 | """An unrolled RNN. The motivation for this is mainly so that we can explain this model using
96 | the shap deep explainer, but also because we unroll the RNN anyway to apply dropout.
97 | """
98 |
99 | def __init__(self, input_size, hidden_size, batch_first=True):
100 | super().__init__()
101 |
102 | self.input_size = input_size
103 | self.hidden_size = hidden_size
104 | self.batch_first = batch_first
105 |
106 | self.forget_gate = nn.Sequential(*[
107 | nn.Linear(in_features=input_size + hidden_size, out_features=hidden_size,
108 | bias=True), nn.Sigmoid()])
109 |
110 | self.update_gate = nn.Sequential(*[
111 | nn.Linear(in_features=input_size + hidden_size, out_features=hidden_size,
112 | bias=True), nn.Sigmoid()
113 | ])
114 |
115 | self.update_candidates = nn.Sequential(*[
116 | nn.Linear(in_features=input_size + hidden_size, out_features=hidden_size,
117 | bias=True), nn.Tanh()
118 | ])
119 |
120 | self.output_gate = nn.Sequential(*[
121 | nn.Linear(in_features=input_size + hidden_size, out_features=hidden_size,
122 | bias=True), nn.Sigmoid()
123 | ])
124 |
125 | self.cell_state_activation = nn.Tanh()
126 |
127 | def forward(self, x, state):
128 | hidden, cell = state
129 |
130 | if self.batch_first:
131 | hidden, cell = torch.transpose(hidden, 0, 1), torch.transpose(cell, 0, 1)
132 |
133 | forget_state = self.forget_gate(torch.cat((x, hidden), dim=-1))
134 | update_state = self.update_gate(torch.cat((x, hidden), dim=-1))
135 | cell_candidates = self.update_candidates(torch.cat((x, hidden), dim=-1))
136 |
137 | updated_cell = (forget_state * cell) + (update_state * cell_candidates)
138 |
139 | output_state = self.output_gate(torch.cat((x, hidden), dim=-1))
140 | updated_hidden = output_state * self.cell_state_activation(updated_cell)
141 |
142 | if self.batch_first:
143 | updated_hidden = torch.transpose(updated_hidden, 0, 1)
144 | updated_cell = torch.transpose(updated_cell, 0, 1)
145 |
146 | return updated_hidden, (updated_hidden, updated_cell)
147 |
--------------------------------------------------------------------------------
/predictor/preprocessing.py:
--------------------------------------------------------------------------------
1 | import pandas as pd
2 | from pathlib import Path
3 | import xarray as xr
4 | import numpy as np
5 | import json
6 |
7 | KEY_COLS = ['lat', 'lon', 'time', 'gb_year', 'gb_month']
8 | VALUE_COLS = ['lst_night', 'lst_day', 'precip', 'sm', 'ndvi', 'evi', 'ndvi_anomaly']
9 | VEGETATION_LABELS = ['ndvi', 'evi', 'ndvi_anomaly']
10 |
11 |
12 | class Cleaner:
13 | """Clean the input data (from the .nc file), by removing nan
14 | (or encoded-as-nan) values, and normalising the non-key values.
15 |
16 | Does some preprocessing on the .nc file using xarray and then converts
17 | it to a dataframe for the other methods
18 |
19 | Attributes:
20 | ----------
21 | raw_filepath: pathlib.Path
22 | The location of the raw .NC data
23 | processed_filepath: pathlib.Path
24 | The location where the processed csv will be saved. The normalizing dict
25 | will be saved in the same directory.
26 | """
27 | def __init__(self, raw_filepath=Path('data/raw/OUT.NC'),
28 | processed_filepath=Path('data/processed/cleaned_data.csv')):
29 |
30 | self.filepath = raw_filepath
31 |
32 | if not processed_filepath.parents[0].exists():
33 | processed_filepath.parents[0].mkdir()
34 |
35 | self.processed_filepath = processed_filepath
36 | self.normalizing_dict = processed_filepath.parents[0] / 'normalizing_dict.json'
37 |
38 | def process(self, pred_month=6, target='ndvi_anomaly'):
39 | """Preprocesses the raw data, and saves it. Specifically, the following
40 | preprocessing happens:
41 | 1. `gb_year` and `gb_month`, which are the dates relative to
42 | `pred_month`, are added.
43 | 2. `ndvi_anomaly` is calculated
44 | 3. NaN values (and missing data) is removed from the dataframe.
45 | 4. Normalizes all values to have mean 0 and std 1
46 |
47 | Parameters
48 | ----------
49 | pred_month: int
50 | The month for which the target value should be predicted. This value will be
51 | predicted using the preceding 11 months of data
52 | target: str
53 | The target variable being predicted
54 |
55 | A processed CSV and a .json object containing the values used to normalize
56 | each variable are saved.
57 | """
58 |
59 | assert target in VALUE_COLS, f'f{target} not in {VALUE_COLS}'
60 |
61 | data = self._readfile(pred_month)
62 |
63 | data['target'] = data[target]
64 |
65 | normalizing_dict = {}
66 | for col in VALUE_COLS:
67 | print(f'Normalizing {col}')
68 |
69 | series = data[col]
70 |
71 | # calculate normalizing values
72 | mean, std = series.mean(), series.std()
73 | # add them to the dictionary
74 | normalizing_dict[col] = {
75 | 'mean': float(mean), 'std': float(std),
76 | }
77 |
78 | data[col] = (series - mean) / std
79 |
80 | print("Saving data")
81 | data.to_csv(self.processed_filepath, index=False)
82 | print(f'Saved {self.processed_filepath}')
83 |
84 | with open(self.normalizing_dict, 'w') as f:
85 | json.dump(normalizing_dict, f)
86 |
87 | print(f'Saved {self.normalizing_dict}')
88 |
89 | def _readfile(self, pred_month):
90 | # drop any Pixel-Times with missing values
91 | data = xr.open_dataset(self.filepath)
92 |
93 | data['gb_month'], data['gb_year'] = self._update_year_month(
94 | pd.to_datetime(data.time.to_series()), pred_month)
95 |
96 | # mask out the invalid temperature values
97 | lst_cols = ['lst_night', 'lst_day']
98 | for var_ in lst_cols:
99 | # for the lst_cols, missing data is coded as 200
100 | valid = (data[var_] < 200) | (np.isnan(data[var_]))
101 | data[var_] = data[var_].fillna(np.nan).where(valid)
102 |
103 | return_cols = KEY_COLS + VALUE_COLS
104 |
105 | # compute the ndvi_anomaly
106 | data['ndvi_anomaly'] = self._compute_anomaly(data.ndvi)
107 |
108 | data = data.to_dataframe().reset_index()
109 | data.dropna(how='any', axis=0, inplace=True)
110 |
111 | print(f'Loaded {len(data)} rows!')
112 | return data[return_cols]
113 |
114 | @staticmethod
115 | def _update_year_month(times, pred_month):
116 | """Given a pred year (e.g. 6), this method will return two new series with
117 | updated years and months so that a "year" of data will be the 11 months preceding the
118 | pred_month, and the pred_month. This makes it easier for the engineer to then make the training
119 | data
120 | """
121 | if pred_month == 12:
122 | return times.dt.month, times.dt.year
123 |
124 | relative_times = times - pd.DateOffset(months=pred_month)
125 |
126 | # we add one year so that the year column the engineer makes will be reflective
127 | # of the pred year, which is shifted because of the data offset we used
128 | return relative_times.dt.month, relative_times.dt.year + 1
129 |
130 | @staticmethod
131 | def _compute_anomaly(da, time_group='time.month'):
132 | """ Return a dataarray where values are an anomaly from the MEAN for that
133 | location at a given timestep. Defaults to finding monthly anomalies.
134 | Notes: http://xarray.pydata.org/en/stable/examples/weather-data.html#calculate-monthly-anomalies
135 |
136 | In addition, since 2016 is being used as the prediction year, data from that year
137 | is not being used to compute the mean.
138 |
139 | Arguments:
140 | ---------
141 | : da (xr.DataArray)
142 | : time_group (str)
143 | time string to group.
144 | """
145 | print('Computing ndvi anomaly')
146 | assert isinstance(da, xr.DataArray), f"`da` should be of type `xr.DataArray`. Currently: {type(da)}"
147 | trimmed_da = da[da['time.year'] < 2016]
148 | mthly_vals = trimmed_da.groupby(time_group).mean('time')
149 | da = da.groupby(time_group) - mthly_vals
150 |
151 | return da
152 |
--------------------------------------------------------------------------------
/data/utils.py:
--------------------------------------------------------------------------------
1 | import xarray as xr
2 | import pandas as pd
3 | import numpy as np
4 | from pathlib import Path
5 | import os
6 | import warnings
7 |
8 | # invert the upside down variables
9 | def invert(da):
10 | """ given a dataarray invert the latitude values so that the plots are the right way up """
11 | da.values = da.values[:,::-1,:]
12 | assert da.name, f"the dataarray passed to function invert() must have a name!"
13 | print(f'inverted the values of {da.name}')
14 | return da
15 |
16 |
17 | def get_lc_mask(ds, mask_var):
18 | assert mask_var in ["spi", "spei", "ndvi"], f"Mask Variable should be one of spi spei ndvi (ADD MORE)"
19 | # create a land cover mask
20 | warnings.warn('Currently get_lc_mask() is working with hardcoded SPI values. May cause problems')
21 | lc_mask = ~ds['spi'].isel(time=1).isnull()
22 | lc_mask.name = "lc_mask"
23 | print("created lc_mask")
24 | return lc_mask
25 |
26 |
27 | def remove_excess_parameters(ds):
28 | """ select only some of the variables (NOTE: poorly programmed because hardcoded)
29 | TODO: remove hardcoded params
30 | """
31 | key_parameters = ["lst_mean","lst_day","lst_night","sm","precip","evaporation","transpiration","spi","spei","ndvi","evi","surface_soil_moisture","rootzone_soil_moisture","baresoil_evaporation","potential_evaporation"]
32 | # check that all the parameters exist in the dataset
33 | for param in key_parameters:
34 | assert param in [var for var in ds.variables.keys()], f"Param {param} not found!"
35 | ds = ds[key_parameters]
36 | print(f"Keeping parameters: {key_parameters}")
37 | return ds
38 |
39 |
40 | def mask_sea(ds, lc_mask):
41 | """ MASK THE SEA VALUES (select only where lc_mask == 1)"""
42 | ds = ds.where(lc_mask)
43 | print(f"Sea values masked for ds with vars {[var for var in ds.data_vars.keys()]}")
44 | return ds
45 |
46 |
47 | def get_boolean_drought_ds(ds):
48 | """ extract the drought indices from the data array """
49 | assert "drought_ndvi" in [var for var in ds.variables.keys()], f"drought_ndvi should be in {[var for var in ds.variables.keys()]}"
50 | assert "drought_spei" in [var for var in ds.variables.keys()], f"drought_spei should be in {[var for var in ds.variables.keys()]}"
51 | assert "drought_spi" in [var for var in ds.variables.keys()], f"drought_spi should be in {[var for var in ds.variables.keys()]}"
52 | drought = ds[['drought_spei','drought_spi','drought_ndvi']]
53 | print("Created a drought index xr.Dataset")
54 | return drought
55 |
56 |
57 | def save_netcdf(output_ds, filename):
58 | """ save the dataset"""
59 | output_ds.to_netcdf(filename)
60 | print(f"{filename} saved!")
61 | return
62 |
63 |
64 | def clean_lst_variables(ds):
65 | """"""
66 | lst_vars = [var_ for var_ in ds.data_vars.keys() if "lst" in var_]
67 | for lst_var in lst_vars:
68 | # filter OUT the lst values of 200
69 | valid = ds[lst_var] < 200
70 | ds[lst_var] = ds[lst_var].fillna(np.nan).where(valid)
71 | return ds
72 |
73 |
74 | def clean_data(ds, lc_mask):
75 | # drop the final timestep
76 | ds = ds.isel(time=slice(0,-1))
77 |
78 | # invert precip
79 | ds['precip'] = invert(ds.precip)
80 |
81 | # get only the important parameters
82 | ds = remove_excess_parameters(ds)
83 |
84 | # mask out the sea
85 | ds = mask_sea(ds, lc_mask)
86 |
87 | # clean lst variables
88 | ds = clean_lst_variables(ds)
89 |
90 | return ds
91 |
92 |
93 | def get_df_of_pixels_to_remove(lc_mask):
94 | """ return a dataframe of the pixels that correspond to SEA points (and therefore that we want to remove)
95 | """
96 | mask_df = lc_mask.to_dataframe()
97 | mask_df = mask_df.reset_index().drop(columns=["time"])
98 | indexes_to_remove = mask_df.where(~mask_df.lc_mask).dropna()
99 |
100 | return indexes_to_remove
101 |
102 |
103 |
104 | def shift_by_time(ds):
105 | """ shift dataset by number of timesteps (so if shift by 3 you get )"""
106 | return ds.shift(ts)
107 |
108 |
109 |
110 | def mask_ds(ds, drought_mask, drought=True):
111 | """set all of the pixels that are SEA or DROUGHT to NAN"""
112 | print("drought pixels masked")
113 |
114 | return ds
115 |
116 |
117 | def make_masks_boolean(mask_ds):
118 | """ convert masks from 0,1 to False,True """
119 | try:
120 | print(f"Convert mask to bool for ds with vars {[var for var in mask_ds.data_vars.keys()]}")
121 | mask_ds = mask_ds.astype(bool)
122 | except:
123 | try:
124 | print(f"UNABLE to convert mask to bool for ds with vars {[var for var in mask_ds.data_vars.keys()]}")
125 | except: # is a data array and it doesn't have .data_vars()
126 | print(f"UNABLE to convert mask to bool for ds with vars {mask_ds.name}")
127 | return mask_ds
128 |
129 |
130 | def read_data(data_path='.', mask_var='spi'):
131 | """ """
132 | assert os.path.isfile(data_path), f"The path provided to read data does not exist! Currently: {data_path}"
133 | # data_path = "/Volumes/Lees_Extend/data/ea_data/all_variables_LC2.nc"
134 | print(f"Reading from file: {data_path}")
135 | ds = xr.open_dataset(data_path)
136 | lc_mask = get_lc_mask(ds, mask_var)
137 | # --------------------------------------------------------------------------
138 | # OFFENDING LINE
139 | warnings.warn('drought_ndvi hardcoded in here. Not at all okay.gst FIX ME')
140 | ds['drought_ndvi'] = ds.ndvi < (ds.ndvi.mean(dim='time') - ds.ndvi.std(dim='time'))
141 | # --------------------------------------------------------------------------
142 | drought_mask = get_boolean_drought_ds(ds)
143 |
144 | # REMOVE THE SEA VALUES from drought mask
145 | drought_mask = mask_sea(drought_mask, lc_mask)
146 | drought_mask = make_masks_boolean(drought_mask)
147 | lc_mask = make_masks_boolean(lc_mask)
148 |
149 | ds = clean_data(ds, lc_mask)
150 |
151 | return ds, lc_mask, drought_mask
152 |
153 |
154 | def print_shift_explanation(ds):
155 | """ print statements to explain the differences with the shift operator """
156 | print("*** UNSHIFTED TIME ***")
157 | print("time=0\n", ds.isel(lat=slice(0,2), lon=slice(0,2),time=0).precip.values)
158 | print("time=1\n", ds.isel(lat=slice(0,2), lon=slice(0,2),time=1).precip.values)
159 | print("time=2\n",ds.isel(lat=slice(0,2), lon=slice(0,2), time=2).precip.values)
160 | print("time=3\n",ds.isel(lat=slice(0,2), lon=slice(0,2), time=3).precip.values)
161 | print()
162 | print("*** SHIFTED TIME (+1) = moving the HISTORICAL TIMESTEPS FORWARD to the PRESENT ***")
163 | print("time=0\n",ds.isel(lat=slice(0,2), lon=slice(0,2)).shift(time=1).isel(time=0).precip.values)
164 | print("time=1\n",ds.isel(lat=slice(0,2), lon=slice(0,2)).shift(time=1).isel(time=1).precip.values)
165 | print("time=2\n",ds.isel(lat=slice(0,2), lon=slice(0,2)).shift(time=1).isel(time=2).precip.values)
166 | print("time=3\n",ds.isel(lat=slice(0,2), lon=slice(0,2)).shift(time=1).isel(time=3).precip.values)
167 | print()
168 | print("*** SHIFTED TIME (-1) = moving the PRESENT TIMESTEPS BACKWARD to the PAST ***")
169 | print("time=0\n",ds.isel(lat=slice(0,2), lon=slice(0,2)).shift(time=-1).isel(time=0).precip.values)
170 | print("time=1\n",ds.isel(lat=slice(0,2), lon=slice(0,2)).shift(time=-1).isel(time=1).precip.values)
171 | print("time=2\n",ds.isel(lat=slice(0,2), lon=slice(0,2)).shift(time=-1).isel(time=2).precip.values)
172 | print("time=3\n",ds.isel(lat=slice(0,2), lon=slice(0,2)).shift(time=-1).isel(time=3).precip.values)
173 | print()
174 |
175 | # ----------------------------------------------------------------------
176 | def create_lc_mask(ds):
177 | """ from the dataset create a landcover mask """
178 | # create a land cover mask
179 | lc_mask = ~ds.spi.isel(time=1).isnull()
180 | lc_mask.name = "lc_mask"
181 |
182 | # create df lc mask
183 | mask_df = lc_mask.to_dataframe()
184 | mask_df = mask_df.reset_index().drop(columns=["time"])
185 |
186 | # get df of SEA pixels (pixels to remove)
187 | indexes_to_remove = mask_df.where(~mask_df.lc_mask).dropna()
188 |
189 | return lc_mask, mask_df, indexes_to_remove
190 |
191 |
192 | def drop_nans_and_flatten(dataArray):
193 | """flatten the array and drop nans from that array. Useful for plotting histograms.
194 |
195 | Arguments:
196 | ---------
197 | : dataArray (xr.DataArray)
198 | the DataArray of your value you want to flatten
199 | """
200 | # drop NaNs and flatten
201 | return dataArray.values[~np.isnan(dataArray.values)]
202 |
--------------------------------------------------------------------------------
/notebooks/05_tl_explore_models.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": 1,
6 | "metadata": {},
7 | "outputs": [],
8 | "source": [
9 | "from pathlib import Path\n",
10 | "from itertools import chain\n",
11 | "import pandas as pd\n",
12 | "import numpy as np\n",
13 | "\n",
14 | "import json\n",
15 | "import sys\n",
16 | "sys.path.insert(0, '..')\n",
17 | "\n",
18 | "from predictor.models import LinearModel\n",
19 | "from predictor.preprocessing import VALUE_COLS, VEGETATION_LABELS\n",
20 | "from predictor.models import nn_FeedForward\n",
21 | "from predictor.analysis import plot_shap_values"
22 | ]
23 | },
24 | {
25 | "cell_type": "code",
26 | "execution_count": 2,
27 | "metadata": {},
28 | "outputs": [],
29 | "source": [
30 | "with open('../data/processed/normalizing_dict.json', 'r') as f:\n",
31 | " normalizing_dict = json.load(f)\n",
32 | " \n",
33 | "path_to_arrays = Path('../data/processed/arrays')"
34 | ]
35 | },
36 | {
37 | "cell_type": "code",
38 | "execution_count": 9,
39 | "metadata": {},
40 | "outputs": [
41 | {
42 | "data": {
43 | "text/plain": [
44 | ""
45 | ]
46 | },
47 | "execution_count": 9,
48 | "metadata": {},
49 | "output_type": "execute_result"
50 | }
51 | ],
52 | "source": [
53 | "model = nn_FeedForward(path_to_arrays, hide_vegetation=True)\n",
54 | "model"
55 | ]
56 | },
57 | {
58 | "cell_type": "code",
59 | "execution_count": 10,
60 | "metadata": {},
61 | "outputs": [
62 | {
63 | "name": "stdout",
64 | "output_type": "stream",
65 | "text": [
66 | "Training model without vegetation features\n"
67 | ]
68 | },
69 | {
70 | "name": "stderr",
71 | "output_type": "stream",
72 | "text": [
73 | " 0%| | 0/8175 [00:00, ?it/s]"
74 | ]
75 | },
76 | {
77 | "name": "stdout",
78 | "output_type": "stream",
79 | "text": [
80 | "After split, training on 261593 examples, validating on 29065 examples\n"
81 | ]
82 | },
83 | {
84 | "name": "stderr",
85 | "output_type": "stream",
86 | "text": [
87 | "100%|██████████| 8175/8175 [00:26<00:00, 305.35it/s]\n",
88 | "100%|██████████| 909/909 [00:01<00:00, 826.49it/s]\n",
89 | " 0%| | 27/8175 [00:00<00:30, 266.30it/s]"
90 | ]
91 | },
92 | {
93 | "name": "stdout",
94 | "output_type": "stream",
95 | "text": [
96 | "Epoch 0 - Training RMSE: 0.06997232270623566, Validation RMSE: 0.06983803885162074\n"
97 | ]
98 | },
99 | {
100 | "name": "stderr",
101 | "output_type": "stream",
102 | "text": [
103 | "100%|██████████| 8175/8175 [00:24<00:00, 331.95it/s]\n",
104 | "100%|██████████| 909/909 [00:01<00:00, 806.22it/s]\n",
105 | " 0%| | 26/8175 [00:00<00:32, 252.17it/s]"
106 | ]
107 | },
108 | {
109 | "name": "stdout",
110 | "output_type": "stream",
111 | "text": [
112 | "Epoch 1 - Training RMSE: 0.05651973409048461, Validation RMSE: 0.06605148216252542\n"
113 | ]
114 | },
115 | {
116 | "name": "stderr",
117 | "output_type": "stream",
118 | "text": [
119 | "100%|██████████| 8175/8175 [00:25<00:00, 319.95it/s]\n",
120 | "100%|██████████| 909/909 [00:01<00:00, 792.90it/s]\n",
121 | " 0%| | 28/8175 [00:00<00:29, 278.07it/s]"
122 | ]
123 | },
124 | {
125 | "name": "stdout",
126 | "output_type": "stream",
127 | "text": [
128 | "Epoch 2 - Training RMSE: 0.05542360727917346, Validation RMSE: 0.0645962596365852\n"
129 | ]
130 | },
131 | {
132 | "name": "stderr",
133 | "output_type": "stream",
134 | "text": [
135 | "100%|██████████| 8175/8175 [00:26<00:00, 304.98it/s]\n",
136 | "100%|██████████| 909/909 [00:01<00:00, 763.35it/s]\n",
137 | " 0%| | 26/8175 [00:00<00:31, 256.86it/s]"
138 | ]
139 | },
140 | {
141 | "name": "stdout",
142 | "output_type": "stream",
143 | "text": [
144 | "Epoch 3 - Training RMSE: 0.054586497671013576, Validation RMSE: 0.06387223563145752\n"
145 | ]
146 | },
147 | {
148 | "name": "stderr",
149 | "output_type": "stream",
150 | "text": [
151 | "100%|██████████| 8175/8175 [00:25<00:00, 326.63it/s]\n",
152 | "100%|██████████| 909/909 [00:01<00:00, 803.46it/s]"
153 | ]
154 | },
155 | {
156 | "name": "stdout",
157 | "output_type": "stream",
158 | "text": [
159 | "Epoch 4 - Training RMSE: 0.054150071823314425, Validation RMSE: 0.06281156092472333\n"
160 | ]
161 | },
162 | {
163 | "name": "stderr",
164 | "output_type": "stream",
165 | "text": [
166 | "\n"
167 | ]
168 | }
169 | ],
170 | "source": [
171 | "model.train(num_epochs=5)"
172 | ]
173 | },
174 | {
175 | "cell_type": "code",
176 | "execution_count": 11,
177 | "metadata": {},
178 | "outputs": [
179 | {
180 | "name": "stderr",
181 | "output_type": "stream",
182 | "text": [
183 | "100%|██████████| 916/916 [00:01<00:00, 619.96it/s]\n"
184 | ]
185 | },
186 | {
187 | "name": "stdout",
188 | "output_type": "stream",
189 | "text": [
190 | "Test set RMSE: 0.08204460144042969\n"
191 | ]
192 | }
193 | ],
194 | "source": [
195 | "model.evaluate()"
196 | ]
197 | },
198 | {
199 | "cell_type": "code",
200 | "execution_count": 12,
201 | "metadata": {},
202 | "outputs": [
203 | {
204 | "name": "stdout",
205 | "output_type": "stream",
206 | "text": [
207 | "Training model without vegetation features\n"
208 | ]
209 | }
210 | ],
211 | "source": [
212 | "background_data = model.load_tensors(mode='train')\n",
213 | "test_data = model.load_tensors(mode='test')"
214 | ]
215 | },
216 | {
217 | "cell_type": "code",
218 | "execution_count": 8,
219 | "metadata": {},
220 | "outputs": [
221 | {
222 | "data": {
223 | "text/plain": [
224 | "[PosixPath('../data/processed/arrays/train/x.npy'),\n",
225 | " PosixPath('../data/processed/arrays/train/y.npy'),\n",
226 | " PosixPath('../data/processed/arrays/train/latlon.npy'),\n",
227 | " PosixPath('../data/processed/arrays/train/years.npy')]"
228 | ]
229 | },
230 | "execution_count": 8,
231 | "metadata": {},
232 | "output_type": "execute_result"
233 | }
234 | ],
235 | "source": [
236 | "[arr for arr in path_to_arrays.glob('test/*')]\n",
237 | "[arr for arr in path_to_arrays.glob('train/*')]"
238 | ]
239 | },
240 | {
241 | "cell_type": "code",
242 | "execution_count": 11,
243 | "metadata": {},
244 | "outputs": [
245 | {
246 | "data": {
247 | "text/plain": [
248 | "(58604, 11, 8)"
249 | ]
250 | },
251 | "execution_count": 11,
252 | "metadata": {},
253 | "output_type": "execute_result"
254 | }
255 | ],
256 | "source": [
257 | "x = np.load(path_to_arrays/'test/x.npy')\n",
258 | "x.shape"
259 | ]
260 | },
261 | {
262 | "cell_type": "code",
263 | "execution_count": 12,
264 | "metadata": {},
265 | "outputs": [
266 | {
267 | "data": {
268 | "text/plain": [
269 | "(58604,)"
270 | ]
271 | },
272 | "execution_count": 12,
273 | "metadata": {},
274 | "output_type": "execute_result"
275 | }
276 | ],
277 | "source": [
278 | "y = np.load(path_to_arrays/'test/y.npy')\n",
279 | "y.shape"
280 | ]
281 | },
282 | {
283 | "cell_type": "code",
284 | "execution_count": 14,
285 | "metadata": {},
286 | "outputs": [
287 | {
288 | "data": {
289 | "text/plain": [
290 | "(58604,)"
291 | ]
292 | },
293 | "execution_count": 14,
294 | "metadata": {},
295 | "output_type": "execute_result"
296 | }
297 | ],
298 | "source": [
299 | "latlon = np.load(path_to_arrays/'test/latlon.npy')\n",
300 | "latlon.shape\n",
301 | "\n",
302 | "years = np.load(path_to_arrays/'test/years.npy')\n",
303 | "years.shape"
304 | ]
305 | },
306 | {
307 | "cell_type": "code",
308 | "execution_count": null,
309 | "metadata": {},
310 | "outputs": [],
311 | "source": []
312 | }
313 | ],
314 | "metadata": {
315 | "kernelspec": {
316 | "display_name": "Python 3",
317 | "language": "python",
318 | "name": "python3"
319 | },
320 | "language_info": {
321 | "codemirror_mode": {
322 | "name": "ipython",
323 | "version": 3
324 | },
325 | "file_extension": ".py",
326 | "mimetype": "text/x-python",
327 | "name": "python",
328 | "nbconvert_exporter": "python",
329 | "pygments_lexer": "ipython3",
330 | "version": "3.7.2"
331 | }
332 | },
333 | "nbformat": 4,
334 | "nbformat_minor": 2
335 | }
336 |
--------------------------------------------------------------------------------
/data/drought_masking.py:
--------------------------------------------------------------------------------
1 | """
2 | @tommylees112
3 |
4 | Code for getting land surface variables for previous timesteps that are IN
5 | drought or OUT of drought.
6 |
7 | For the pixels that are in drought, return variables for that pixel for t=0,
8 | t=-1,t=-2,t=-3.
9 |
10 | This way you have the previous 3 months worth of data
11 | """
12 | import xarray as xr
13 | from joblib import Parallel, delayed
14 | import pandas as pd
15 | import os
16 | from utils import shift_by_time, read_data, print_shift_explanation
17 | from utils import save_netcdf
18 | import warnings
19 | import numpy as np
20 |
21 |
22 | def get_monthly_location_specific_drought_mask(ds, variable):
23 | """ from a dataset, use a variable and calculate 1 STD below
24 | the mean conditioned on:
25 | a) LOCATION (lat/lon)
26 | b) TIME (month)
27 | """
28 | # get month labels
29 | ds = ds.assign_coords(**{'month':ds.time.dt.month})
30 |
31 | outs = []
32 | for month in range(0,13):
33 | # select ONE MONTH for your variable of interest from the dataset
34 | d = ds[variable].sel(time=(ds.time.dt.month == month))
35 |
36 | # calculate avg/std
37 | avg = ds[variable].isel(time=(ds.time.dt.month == month)).mean('time')
38 | std_dev = ds[variable].isel(time=(ds.time.dt.month == month)).std('time')
39 |
40 | # calculate threshold
41 | threshold = avg - std_dev
42 |
43 | # calculate mask
44 | mask = d < threshold
45 |
46 | # create month specific mask for each timestep
47 | outs.append(mask)
48 | drought_ndvi = xr.concat(outs, dim="time")
49 | return drought_ndvi
50 |
51 |
52 |
53 | def create_drought_mask(drought_ds):
54 | """ at the moment just use an SPI / SPEI of <= -1
55 | TODO: implement more flexible functionality
56 | """
57 | assert ("spi" in [_ for _ in drought_ds.var().variables])&("spei" in [_ for _ in drought_ds.var().variables]), f"spi and spei are expected to be in the drought_ds netcdf file. Currently {drought_ds.var().variables}"
58 | assert "ndvi" in [_ for _ in drought_ds.var().variables], f"ndvi should be a variable. currently: {drought_ds.var().variables}"
59 |
60 | # turn the values less than -1 SD into mask (SAME DIMS AS RAW DS)
61 | warnings.warn('NOTE: here the code could do with being more flexible in the determination of drought thresholds and then applying them.')
62 | warnings.warn('Use Ellens functionality already built in Akkadia')
63 | spei_drought = drought_ds.spei.where(drought_ds.spei < -1)
64 | spi_drought = drought_ds.spi.where(drought_ds.spi < -1)
65 |
66 | ndvi_drought = get_monthly_location_specific_drought_mask(drought_ds, 'ndvi')
67 |
68 | # turn into a boolean mask
69 | drought_spei = spei_drought.notnull()
70 | drought_spei = drought_spei.rename('drought_spei')
71 | drought_spi = spi_drought.notnull()
72 | drought_spi = drought_spi.rename('drought_spi')
73 | drought_ndvi = ndvi_drought.notnull()
74 | drought_ndvi = drought_ndvi.rename('drought_ndvi')
75 |
76 | return drought_spei, drought_spi, drought_ndvi
77 |
78 |
79 | def mask_drought_events(ds, drought_mask, ts, index, drought=True):
80 | """ for a given drought mask, at a given time, return the previous 3 months of data
81 | for pixels that were IN (or out of) drought
82 | """
83 | assert ts > 3, "Need timestep to be greater than 3 because want the PREVIOUS 3 months. Otherwise all nans!"
84 | assert index in ['spei','spi', 'ndvi'], f"index must be either ['spei', 'spi', 'ndvi']. Currently: {index}"
85 |
86 | # get the drought mask at that timestep (NOTE have to invert BEFORE this)
87 | if index=='spi':
88 | msk = drought_mask.drought_spi.isel(time=ts)
89 | elif index=='spei':
90 | msk = drought_mask.drought_spei.isel(time=ts)
91 | elif index=='ndvi':
92 | msk = drought_mask.drought_ndvi.isel(time=ts)
93 |
94 | # if want pixels that were IN DROUGHT
95 | if drought:
96 | t0 = ds.where(msk).isel(time=ts)
97 | t1 = ds.where(msk).isel(time=ts-1)
98 | t2 = ds.where(msk).isel(time=ts-2)
99 | t3 = ds.where(msk).isel(time=ts-3)
100 |
101 | # if want pixels that are NOT IN DROUGHT
102 | else:
103 | t0 = ds.where(msk).isel(time=ts)
104 | t1 = ds.where(msk).isel(time=ts-1)
105 | t2 = ds.where(msk).isel(time=ts-2)
106 | t3 = ds.where(msk).isel(time=ts-3)
107 |
108 | return [t0, t1, t2, t3]
109 |
110 |
111 | def merge_all_ts_into_one_ds(ds_arr):
112 | """merge all of the timesteps (t=0, t-1, t-2, t-3) into one dataset object
113 |
114 | input:
115 | : array of all the
116 | returns:
117 | : ds_out (xr.Dataset): one dataset with all of the variables
118 | """
119 | # assert that the minimum time is the first dataset in the array
120 | time = ds_arr[0].time
121 | ds_rnm = []
122 |
123 | for ts, ds_ in enumerate(ds_arr):
124 | # create dict of variables to rename
125 | map_names = dict(zip([var for var in ds_.variables.keys() if var not in ['time','lat','lon']],
126 | [f"{var}_t{ts}" for var in ds_.variables.keys() if var not in ['time','lat','lon']])
127 | )
128 | # rename the variables
129 | ds_ = ds_.rename(map_names)
130 | ds_rnm.append(ds_)
131 |
132 | # drop the 'time' (TO ALLOW THE MERGE)
133 | ds_rnm = [ds_.drop('time') for ds_ in ds_rnm]
134 |
135 | # merge the variables
136 | ds_out = xr.merge(ds_rnm)
137 |
138 | # reassign 'Time' from the first dataset
139 | ds_out = ds_out.assign_coords(**{'time':time})
140 |
141 | return ds_out
142 |
143 |
144 | def calculate_drought_masked_ds(ds, drought_mask, ts):
145 | """ run the above functions on EVERY timestep
146 | so output a dataset with each timestep a calculation of the previous months
147 | """
148 | print(f"Extracting Drought vars from timestep {ts}")
149 | ds_arr = mask_drought_events(ds, drought_mask, ts=ts, index='ndvi', drought=True)
150 | ds_drought = merge_all_ts_into_one_ds(ds_arr)
151 |
152 | return ds_drought
153 |
154 |
155 | def calculate_Ndrought_masked_ds(ds, drought_mask, ts):
156 | """ run the above functions on EVERY timestep
157 | so output a dataset with each timestep a calculation of the previous months
158 | """
159 | print(f"Extracting NON-Drought vars from timestep {ts}")
160 | ds_arr = mask_drought_events(ds, drought_mask, ts=ts, index='ndvi', drought=False)
161 | ds_Ndrought = merge_all_ts_into_one_ds(ds_arr)
162 |
163 | return ds_Ndrought
164 |
165 |
166 | def drought_across_all_timesteps(ds, drought_mask, start_ts=4):
167 | """ run the drought masking process for ALL TIMESTEPS
168 |
169 | Note: the output of Parallel(n_jobs=2)({FNCTN}) is a list of all of the variables
170 | because they are timestamped it doesn't matter if they come back in a different
171 | order. Therefore, we save the reordering to a later date. Future me can deal
172 | with that problem.
173 | """
174 | dr = []
175 | Ndr = []
176 |
177 | # RUN the drought first
178 | print("Extracting the pixels in DROUGHT")
179 | with Parallel(n_jobs=30, verbose=True) as parallel:
180 | dr = parallel(
181 | delayed(calculate_drought_masked_ds)
182 | (ds=ds,drought_mask=drought_mask,ts=ts) for ts in range(start_ts, ds.time.shape[0])
183 | )
184 | print("Pixels in DROUGHT extracted")
185 |
186 | # then run the Ndrought
187 | print("Extracting the pixels NOT in DROUGHT")
188 | # convert to boolean arrays in order to INVERT them
189 | Ndrought_mask = ~(drought_mask.astype(bool))
190 | with Parallel(n_jobs=30, verbose=True) as parallel:
191 | Ndr = parallel(
192 | delayed(calculate_Ndrought_masked_ds)
193 | (ds=ds,drought_mask=Ndrought_mask,ts=ts) for ts in range(start_ts, ds.time.shape[0])
194 | )
195 | print("Pixels NOT in DROUGHT extracted")
196 |
197 | # concatenate the arrays by time into ONE dataset (agree there's lots of duplication of data here)
198 | # TODO: does this really make sense? we are duplicating SOOOOOOO much data and for what?
199 | # definitely a better way.
200 | ds_drought = xr.concat(dr, dim='time')
201 | ds_drought.to_netcdf('ds_drought.nc')
202 | print("DROUGHT Variables extracted. Saving to netcdf ds_drought.nc ...")
203 |
204 | ds_Ndrought = xr.concat(Ndr, dim='time')
205 | ds_Ndrought.to_netcdf('ds_Ndrought.nc')
206 | print("NOT DROUGHT Variables extracted. Saving to netcdf ds_Ndrought.nc ...")
207 |
208 | return ds_drought, ds_Ndrought
209 |
210 |
211 | def run_drought_processing(data_path, mask_var='spi'):
212 | """
213 |
214 | """
215 | print("** Running drought processing for all timesteps! **")
216 | ds, lc_mask, drought_mask = read_data(data_path, mask_var)
217 | print(f"** Data read in - using {mask_var} as the masking variable **")
218 | ds_drought, ds_Ndrought = drought_across_all_timesteps(ds, drought_mask)
219 |
220 | save_netcdf(ds_drought, "ds_drought.nc")
221 | save_netcdf(ds_Ndrought, "ds_Ndrought.nc")
222 | print("** Process finished **")
223 |
224 | return ds_drought, ds_Ndrought
225 |
226 |
227 |
228 |
229 |
230 | def fix_drought_var():
231 | """if drought variable not in dataset then append it!"""
232 | pass
233 |
234 |
235 | if __name__=="__main__":
236 | """ TODO: set up as a CLI """
237 | # data_path = "/soge-home/users/chri4118/EA_data/all_variables_LCMASK.nc"
238 | data_path = "/soge-home/users/chri4118/ea_exploration/OUT.nc"
239 | ds_drought, ds_Ndrought = run_drought_processing(data_path, mask_var='ndvi')
240 |
--------------------------------------------------------------------------------
/data/preprocessing/utils.py:
--------------------------------------------------------------------------------
1 | import xarray as xr
2 | import pandas as pd
3 | import xesmf as xe # for regridding
4 | import numpy as np
5 |
6 | import os
7 | from pathlib import Path
8 | import ipdb
9 | import warnings
10 | import datetime
11 |
12 | import geopandas as gpd
13 | from shapely import geometry
14 |
15 |
16 | def read_csv_point_data(df, lat_col='lat', lon_col='lon', crs='epsg:4326'):
17 | """Read in a csv file with lat,lon values in a column and turn those lat lon
18 | values into geometry.Point objects.
19 | Arguments:
20 | ---------
21 | : df (pd.DataFrame)
22 | : lat_col (str)
23 | the column in the dataframe that has the point latitude information
24 | : lon_col (str)
25 | the column in the dataframe that has the point longitude information
26 | : crs (str)
27 | coordinate reference system (defaults to 'epsg:4326')
28 | Returns:
29 | -------
30 | : gdf (gpd.GeoDataFrame)
31 | a geopandas.GeoDataFrame object
32 | """
33 | df['geometry'] = [geometry.Point(y, x) \
34 | for x, y in zip(df[lat_col],
35 | df[lon_col])
36 | ]
37 | crs = {'init': crs}
38 | gdf = gpd.GeoDataFrame(df, crs=crs, geometry="geometry")
39 | return gdf
40 |
41 |
42 | # ------------------------------------------------------------------------------
43 | # Functions for reprojecting using GDAL and reading resulting .nc file back
44 | # ------------------------------------------------------------------------------
45 |
46 |
47 | def gdal_reproject(infile, outfile, **kwargs):
48 | """Use gdalwarp to reproject one file to another
49 |
50 | Help:
51 | ----
52 | https://www.gdal.org/gdalwarp.html
53 | """
54 | to_proj4_string = "+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs"
55 | resample_method = "near"
56 |
57 | # check options
58 | valid_resample_methods = [
59 | "average",
60 | "near",
61 | "bilinear",
62 | "cubic",
63 | "cubicspline",
64 | "lanczos",
65 | "mode",
66 | "max",
67 | "min",
68 | "med",
69 | "q1",
70 | "q3",
71 | ]
72 | assert (
73 | resample_method in valid_resample_methods
74 | ), f"Resample method not Valid. Must be one of: {valid_resample_methods} Currently: {resample_method}"
75 |
76 | cmd = f'gdalwarp -t_srs "{to_proj4_string}" -of netCDF -r average -dstnodata -9999 -ot Float32 {infile} {outfile}'
77 |
78 | # run command
79 | print(f"\n\n#### Running command: {cmd} ####\n\n")
80 | os.system(cmd)
81 | print(f"\n\n#### Run command {cmd} \n FILE REPROJECTED ####\n\n")
82 |
83 | return
84 |
85 |
86 | def bands_to_time(da, times, var_name):
87 | """ For a dataArray with each timestep saved as a different band, create
88 | a time Coordinate
89 | """
90 | # get a list of all the bands as dataarray objects (for concatenating later)
91 | band_strings = [key for key in da.variables.keys() if "Band" in key]
92 | bands = [da[key] for key in band_strings]
93 | bands = [band.rename(var_name) for band in bands]
94 |
95 | # check the number of bands matches n timesteps
96 | assert len(times) == len(
97 | bands
98 | ), f"The number of bands should match the number of timesteps. n bands: {len(times)} n times: {len(bands)}"
99 | # concatenate into one array
100 | timestamped_da = xr.concat(bands, dim=times)
101 |
102 | return timestamped_da
103 |
104 |
105 | # ------------------------------------------------------------------------------
106 | # Functions for matching resolutions / gridsizes (time and space)
107 | # ------------------------------------------------------------------------------
108 |
109 |
110 | def convert_to_same_grid(reference_ds, ds, method="nearest_s2d"):
111 | """ Use xEMSF package to regrid ds to the same grid as reference_ds """
112 | assert ("lat" in reference_ds.dims) & (
113 | "lon" in reference_ds.dims
114 | ), f"Need (lat,lon) in reference_ds dims Currently: {reference_ds.dims}"
115 | assert ("lat" in ds.dims) & (
116 | "lon" in ds.dims
117 | ), f"Need (lat,lon) in ds dims Currently: {ds.dims}"
118 |
119 | # create the grid you want to convert TO (from reference_ds)
120 | ds_out = xr.Dataset(
121 | {"lat": (["lat"], reference_ds.lat), "lon": (["lon"], reference_ds.lon)}
122 | )
123 |
124 | # create the regridder object
125 | # xe.Regridder(grid_in, grid_out, method='bilinear')
126 | regridder = xe.Regridder(ds, ds_out, method, reuse_weights=True)
127 |
128 | # IF it's a dataarray just do the original transformations
129 | if isinstance(ds, xr.core.dataarray.DataArray):
130 | ds = regridder(ds)
131 | # OTHERWISE loop through each of the variables, regrid the datarray then recombine into dataset
132 | elif isinstance(ds, xr.core.dataset.Dataset):
133 | vars = [i for i in ds.var().variables]
134 | if len(vars) == 1:
135 | ds = regridder(ds)
136 | else:
137 | output_dict = {}
138 | # LOOP over each variable and append to dict
139 | for var in vars:
140 | print(f"- regridding var {var} -")
141 | da = ds[var]
142 | da = regridder(da)
143 | output_dict[var] = da
144 | # REBUILD
145 | ds = xr.Dataset(output_dict)
146 | else:
147 | assert False, "This function only works with xarray dataset / dataarray objects"
148 |
149 | print(
150 | f"Regridded from {(regridder.Ny_in, regridder.Nx_in)} to {(regridder.Ny_out, regridder.Nx_out)}"
151 | )
152 |
153 | return ds
154 |
155 |
156 | def select_same_time_slice(reference_ds, ds):
157 | """ Select the values for the same timestep as the reference ds"""
158 | # CHECK THEY ARE THE SAME FREQUENCY
159 | # get the frequency of the time series from reference_ds
160 | freq = pd.infer_freq(reference_ds.time.values)
161 | if freq == None:
162 | warnings.warn('HARDCODED FOR THIS PROBLEM BUT NO IDEA WHY NOT WORKING')
163 | freq = "M"
164 | # assert False, f"Unable to infer frequency from the reference_ds timestep"
165 |
166 | old_freq = pd.infer_freq(ds.time.values)
167 | warnings.warn(
168 | "Disabled the assert statement. ENSURE FREQUENCIES THE SAME (e.g. monthly)"
169 | )
170 | # assert freq == old_freq, f"The frequencies should be the same! currenlty ref: {freq} vs. old: {old_freq}"
171 |
172 | # get the STARTING time point from the reference_ds
173 | min_time = reference_ds.time.min().values
174 | max_time = reference_ds.time.max().values
175 | orig_time_range = pd.date_range(min_time, max_time, freq=freq)
176 | # EXTEND the original time_range by 1 (so selecting the whole slice)
177 | # because python doesn't select the final in a range
178 | periods = len(orig_time_range) #+ 1
179 | # create new time series going ONE EXTRA PERIOD
180 | new_time_range = pd.date_range(min_time, freq=freq, periods=periods)
181 | new_max = new_time_range.max()
182 |
183 | # select using the NEW MAX as upper limit
184 | # --------------------------------------------------------------------------
185 | # FOR SOME REASON slice is removing the minimum time ...
186 | # something to do with the fact that matplotlib / xarray is working oddly with numpy64datetime object
187 | warnings.warn("L153: HARDCODING THE MIN VALUE OTHERWISE IGNORED ...")
188 | min_time = datetime.datetime(2001, 1, 31)
189 | # --------------------------------------------------------------------------
190 | ds = ds.sel(time=slice(min_time, new_max))
191 | assert reference_ds.time.shape[0] == ds.time.shape[0],f"The time dimensions should match, currently reference_ds.time dims {reference_ds.time.shape[0]} != ds.time dims {ds.time.shape[0]}"
192 |
193 | print_time_min = pd.to_datetime(ds.time.min().values)
194 | print_time_max = pd.to_datetime(ds.time.max().values)
195 | try:
196 | vars = [i for i in ds.var().variables]
197 | except:
198 | vars = ds.name
199 | # ref_vars = [i for i in reference_ds.var().variables]
200 | print(
201 | f"Select same timeslice for ds with vars: {vars}. Min {print_time_min} Max {print_time_max}"
202 | )
203 |
204 | return ds
205 |
206 |
207 | def get_holaps_mask(ds):
208 | """
209 | NOTE:
210 | - assumes that all of the null values from the HOLAPS file are valid null values (e.g. water bodies). Could also be invalid nulls due to poor data processing / lack of satellite input data for a pixel!
211 | """
212 | warnings.warn(
213 | "assumes that all of the null values from the HOLAPS file are valid null values (e.g. water bodies). Could also be invalid nulls due to poor data processing / lack of satellite input data for a pixel!"
214 | )
215 | warnings.warn(
216 | "How to collapse the time dimension in the holaps mask? Here we just select the first time because all of the valid pixels are constant for first, last second last. Need to check this is true for all timesteps"
217 | )
218 |
219 | mask = ds.isnull().isel(time=0).drop("time")
220 | mask.name = "holaps_mask"
221 |
222 | mask = xr.concat([mask for _ in range(len(ds.time))])
223 | mask = mask.rename({"concat_dims": "time"})
224 | mask["time"] = ds.time
225 |
226 | return mask
227 |
228 |
229 |
230 | def select_east_africa(ds):
231 | """ """
232 | lonmin=32.6
233 | lonmax=51.8
234 | latmin=-5.0
235 | latmax=15.2
236 |
237 | if ('x' in ds.dims) & ('y' in ds.dims):
238 | ds = ds.sel(y=slice(latmax,latmin),x=slice(lonmin, lonmax))
239 | elif ('lat' in ds.dims) & ('lon' in ds.dims):
240 | ds = ds.sel(lat=slice(latmax,latmin),lon=slice(lonmin, lonmax))
241 | elif ('latitude' in ds.dims) & ('longitude' in ds.dims):
242 | ds = ds.sel(latitude=slice(latmax,latmin),longitude=slice(lonmin, lonmax))
243 | else:
244 | assert False, "You need one of [(y, x), (lat, lon), (latitude, longitude)] in your dims"
245 |
246 | return
247 |
248 |
249 | # ------------------------------------------------------------------------------
250 | # Functions for working with xarray objects
251 | # ------------------------------------------------------------------------------
252 |
253 |
254 | def merge_data_arrays(*DataArrays):
255 | das = [da.name for da in DataArrays]
256 | print(f"Merging data: {das}")
257 | ds = xr.merge([*DataArrays])
258 | return ds
259 |
260 |
261 | def save_netcdf(xr_obj, filepath, force=False):
262 | """"""
263 | if not Path(filepath).is_file():
264 | xr_obj.to_netcdf(filepath)
265 | print(f"File Saved: {filepath}")
266 | elif force:
267 | print(f"Filepath {filepath} already exists! Overwriting...")
268 | xr_obj.to_netcdf(filepath)
269 | print(f"File Saved: {filepath}")
270 | else:
271 | print(f"Filepath {filepath} already exists!")
272 |
273 | return
274 |
275 |
276 | def get_all_valid(ds, holaps_da, modis_da, gleam_da):
277 | """ Return only values for pixels/times where ALL PRODUCTS ARE VALID """
278 | valid_mask = (
279 | holaps_da.notnull()
280 | & modis_da.notnull()
281 | & gleam_da.notnull()
282 | )
283 | ds_valid = ds.where(valid_mask)
284 |
285 | return ds_valid
286 |
287 |
288 | def drop_nans_and_flatten(dataArray):
289 | """flatten the array and drop nans from that array. Useful for plotting histograms.
290 |
291 | Arguments:
292 | ---------
293 | : dataArray (xr.DataArray)
294 | the DataArray of your value you want to flatten
295 | """
296 | # drop NaNs and flatten
297 | return dataArray.values[~np.isnan(dataArray.values)]
298 |
299 | #
300 |
301 | #
302 | # def create_flattened_dataframe_of_values(h,g,m):
303 | # """ """
304 | # h_ = drop_nans_and_flatten(h)
305 | # g_ = drop_nans_and_flatten(g)
306 | # m_ = drop_nans_and_flatten(m)
307 | # df = pd.DataFrame(dict(
308 | # holaps=h_,
309 | # gleam=g_,
310 | # modis=m_
311 | # ))
312 | # return df
313 |
--------------------------------------------------------------------------------
/notebooks/02_gt_linear_model.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "## Linear model\n",
8 | "\n",
9 | "In this notebook, we train a linear model and investigate the coefficients it learns. These coefficients can be interpreted as the global feature importances of the input features."
10 | ]
11 | },
12 | {
13 | "cell_type": "code",
14 | "execution_count": 1,
15 | "metadata": {},
16 | "outputs": [],
17 | "source": [
18 | "from pathlib import Path\n",
19 | "from itertools import chain\n",
20 | "import pandas as pd\n",
21 | "import numpy as np\n",
22 | "\n",
23 | "import sys\n",
24 | "sys.path.insert(0, '..')\n",
25 | "\n",
26 | "from predictor.models import LinearModel\n",
27 | "from predictor.preprocessing import VALUE_COLS, VEGETATION_LABELS"
28 | ]
29 | },
30 | {
31 | "cell_type": "code",
32 | "execution_count": 2,
33 | "metadata": {},
34 | "outputs": [],
35 | "source": [
36 | "target = 'ndvi'"
37 | ]
38 | },
39 | {
40 | "cell_type": "code",
41 | "execution_count": 3,
42 | "metadata": {},
43 | "outputs": [],
44 | "source": [
45 | "path_to_arrays = Path(f'../data/processed/{target}/arrays')"
46 | ]
47 | },
48 | {
49 | "cell_type": "code",
50 | "execution_count": 4,
51 | "metadata": {},
52 | "outputs": [],
53 | "source": [
54 | "model_with_veg = LinearModel(path_to_arrays)"
55 | ]
56 | },
57 | {
58 | "cell_type": "code",
59 | "execution_count": 5,
60 | "metadata": {},
61 | "outputs": [
62 | {
63 | "name": "stdout",
64 | "output_type": "stream",
65 | "text": [
66 | "Train set RMSE: 0.03977352798000336\n"
67 | ]
68 | }
69 | ],
70 | "source": [
71 | "model_with_veg.train()"
72 | ]
73 | },
74 | {
75 | "cell_type": "markdown",
76 | "metadata": {},
77 | "source": [
78 | "We can isolate the coefficients, as well as the values they correspond to. Note that each label in `value_labels` has the following format: `{value}_{month}` where `month` is relative to the `pred_month` (so if we are predicting June, then `month=11` corresponds to data in May)."
79 | ]
80 | },
81 | {
82 | "cell_type": "code",
83 | "execution_count": 6,
84 | "metadata": {},
85 | "outputs": [],
86 | "source": [
87 | "coefs = model_with_veg.model.coef_"
88 | ]
89 | },
90 | {
91 | "cell_type": "code",
92 | "execution_count": 7,
93 | "metadata": {},
94 | "outputs": [],
95 | "source": [
96 | "value_labels = list(chain(*[[f'{val}_{month}' for val in VALUE_COLS] for month in range(1, 12)]))"
97 | ]
98 | },
99 | {
100 | "cell_type": "code",
101 | "execution_count": 8,
102 | "metadata": {},
103 | "outputs": [],
104 | "source": [
105 | "feature_importances_veg = pd.DataFrame(data={\n",
106 | " 'feature': value_labels,\n",
107 | " 'value': coefs\n",
108 | "})"
109 | ]
110 | },
111 | {
112 | "cell_type": "markdown",
113 | "metadata": {},
114 | "source": [
115 | "Lets investigate the most important features (by absolute value)"
116 | ]
117 | },
118 | {
119 | "cell_type": "code",
120 | "execution_count": 9,
121 | "metadata": {},
122 | "outputs": [
123 | {
124 | "data": {
125 | "text/html": [
126 | "\n",
127 | "\n",
140 | "
\n",
141 | " \n",
142 | " \n",
143 | " | \n",
144 | " feature | \n",
145 | " value | \n",
146 | "
\n",
147 | " \n",
148 | " \n",
149 | " \n",
150 | " | 74 | \n",
151 | " ndvi_11 | \n",
152 | " 0.118459 | \n",
153 | "
\n",
154 | " \n",
155 | " | 4 | \n",
156 | " ndvi_1 | \n",
157 | " 0.110067 | \n",
158 | "
\n",
159 | " \n",
160 | " | 67 | \n",
161 | " ndvi_10 | \n",
162 | " -0.054666 | \n",
163 | "
\n",
164 | " \n",
165 | " | 6 | \n",
166 | " ndvi_anomaly_1 | \n",
167 | " -0.036427 | \n",
168 | "
\n",
169 | " \n",
170 | " | 11 | \n",
171 | " ndvi_2 | \n",
172 | " -0.031033 | \n",
173 | "
\n",
174 | " \n",
175 | " | 75 | \n",
176 | " evi_11 | \n",
177 | " 0.020969 | \n",
178 | "
\n",
179 | " \n",
180 | " | 39 | \n",
181 | " ndvi_6 | \n",
182 | " 0.020805 | \n",
183 | "
\n",
184 | " \n",
185 | " | 71 | \n",
186 | " lst_day_11 | \n",
187 | " -0.016052 | \n",
188 | "
\n",
189 | " \n",
190 | " | 46 | \n",
191 | " ndvi_7 | \n",
192 | " 0.015323 | \n",
193 | "
\n",
194 | " \n",
195 | " | 40 | \n",
196 | " evi_6 | \n",
197 | " -0.014433 | \n",
198 | "
\n",
199 | " \n",
200 | "
\n",
201 | "
"
202 | ],
203 | "text/plain": [
204 | " feature value\n",
205 | "74 ndvi_11 0.118459\n",
206 | "4 ndvi_1 0.110067\n",
207 | "67 ndvi_10 -0.054666\n",
208 | "6 ndvi_anomaly_1 -0.036427\n",
209 | "11 ndvi_2 -0.031033\n",
210 | "75 evi_11 0.020969\n",
211 | "39 ndvi_6 0.020805\n",
212 | "71 lst_day_11 -0.016052\n",
213 | "46 ndvi_7 0.015323\n",
214 | "40 evi_6 -0.014433"
215 | ]
216 | },
217 | "execution_count": 9,
218 | "metadata": {},
219 | "output_type": "execute_result"
220 | }
221 | ],
222 | "source": [
223 | "feature_importances_veg.iloc[(-np.abs(feature_importances_veg['value'].values)).argsort()][:10]"
224 | ]
225 | },
226 | {
227 | "cell_type": "markdown",
228 | "metadata": {},
229 | "source": [
230 | "## Without vegetation\n",
231 | "\n",
232 | "The model above tells us that the vegetation health in May is predictive of the vegetation health in June. What happens if we hide vegetation health from the model?"
233 | ]
234 | },
235 | {
236 | "cell_type": "code",
237 | "execution_count": 10,
238 | "metadata": {},
239 | "outputs": [],
240 | "source": [
241 | "model_no_veg = LinearModel(path_to_arrays, hide_vegetation=True)"
242 | ]
243 | },
244 | {
245 | "cell_type": "code",
246 | "execution_count": 11,
247 | "metadata": {},
248 | "outputs": [
249 | {
250 | "name": "stdout",
251 | "output_type": "stream",
252 | "text": [
253 | "Training model without vegetation features\n",
254 | "Train set RMSE: 0.07832527470825593\n"
255 | ]
256 | }
257 | ],
258 | "source": [
259 | "model_no_veg.train()"
260 | ]
261 | },
262 | {
263 | "cell_type": "code",
264 | "execution_count": 12,
265 | "metadata": {},
266 | "outputs": [],
267 | "source": [
268 | "coefs = model_no_veg.model.coef_\n",
269 | "\n",
270 | "veg_features = ['ndvi', 'evi']\n",
271 | "value_labels = list(chain(*[[f'{val}_{month}' for val in VALUE_COLS if val not in VEGETATION_LABELS] \n",
272 | " for month in range(1, 12)]))"
273 | ]
274 | },
275 | {
276 | "cell_type": "code",
277 | "execution_count": 13,
278 | "metadata": {},
279 | "outputs": [],
280 | "source": [
281 | "feature_importances_no_veg = pd.DataFrame(data={\n",
282 | " 'feature': value_labels,\n",
283 | " 'value': coefs\n",
284 | "})"
285 | ]
286 | },
287 | {
288 | "cell_type": "code",
289 | "execution_count": 14,
290 | "metadata": {},
291 | "outputs": [
292 | {
293 | "data": {
294 | "text/html": [
295 | "\n",
296 | "\n",
309 | "
\n",
310 | " \n",
311 | " \n",
312 | " | \n",
313 | " feature | \n",
314 | " value | \n",
315 | "
\n",
316 | " \n",
317 | " \n",
318 | " \n",
319 | " | 41 | \n",
320 | " lst_day_11 | \n",
321 | " -0.068263 | \n",
322 | "
\n",
323 | " \n",
324 | " | 42 | \n",
325 | " precip_11 | \n",
326 | " 0.048880 | \n",
327 | "
\n",
328 | " \n",
329 | " | 0 | \n",
330 | " lst_night_1 | \n",
331 | " 0.035778 | \n",
332 | "
\n",
333 | " \n",
334 | " | 40 | \n",
335 | " lst_night_11 | \n",
336 | " -0.035483 | \n",
337 | "
\n",
338 | " \n",
339 | " | 30 | \n",
340 | " precip_8 | \n",
341 | " 0.030408 | \n",
342 | "
\n",
343 | " \n",
344 | " | 20 | \n",
345 | " lst_night_6 | \n",
346 | " 0.029277 | \n",
347 | "
\n",
348 | " \n",
349 | " | 21 | \n",
350 | " lst_day_6 | \n",
351 | " -0.026891 | \n",
352 | "
\n",
353 | " \n",
354 | " | 10 | \n",
355 | " precip_3 | \n",
356 | " 0.026077 | \n",
357 | "
\n",
358 | " \n",
359 | " | 33 | \n",
360 | " lst_day_9 | \n",
361 | " 0.025803 | \n",
362 | "
\n",
363 | " \n",
364 | " | 2 | \n",
365 | " precip_1 | \n",
366 | " 0.021489 | \n",
367 | "
\n",
368 | " \n",
369 | "
\n",
370 | "
"
371 | ],
372 | "text/plain": [
373 | " feature value\n",
374 | "41 lst_day_11 -0.068263\n",
375 | "42 precip_11 0.048880\n",
376 | "0 lst_night_1 0.035778\n",
377 | "40 lst_night_11 -0.035483\n",
378 | "30 precip_8 0.030408\n",
379 | "20 lst_night_6 0.029277\n",
380 | "21 lst_day_6 -0.026891\n",
381 | "10 precip_3 0.026077\n",
382 | "33 lst_day_9 0.025803\n",
383 | "2 precip_1 0.021489"
384 | ]
385 | },
386 | "execution_count": 14,
387 | "metadata": {},
388 | "output_type": "execute_result"
389 | }
390 | ],
391 | "source": [
392 | "feature_importances_no_veg.iloc[(-np.abs(feature_importances_no_veg['value'].values)).argsort()][:10]"
393 | ]
394 | }
395 | ],
396 | "metadata": {
397 | "kernelspec": {
398 | "display_name": "Python [conda env:vegetation_health]",
399 | "language": "python",
400 | "name": "conda-env-vegetation_health-py"
401 | },
402 | "language_info": {
403 | "codemirror_mode": {
404 | "name": "ipython",
405 | "version": 3
406 | },
407 | "file_extension": ".py",
408 | "mimetype": "text/x-python",
409 | "name": "python",
410 | "nbconvert_exporter": "python",
411 | "pygments_lexer": "ipython3",
412 | "version": "3.6.8"
413 | }
414 | },
415 | "nbformat": 4,
416 | "nbformat_minor": 2
417 | }
418 |
--------------------------------------------------------------------------------
/data/common_grid.py:
--------------------------------------------------------------------------------
1 | """
2 | Script for putting netcdf (.nc) files onto a common grid. This means that the
3 | location, timesteps and resolution of the data is now the same. This makes
4 | working with the data a lot simpler!
5 | """
6 |
7 | import xarray as xr
8 | import xesmf as xe # for regridding
9 | import numpy as np
10 | import pickle
11 | import matplotlib.pyplot as plt
12 | import pandas as pd
13 | import tqdm # for progress-bars
14 | import click
15 | import warnings
16 |
17 | from drought_masking import create_drought_mask
18 | from utils import save_netcdf
19 |
20 |
21 | def pickle_obj(obj, filename):
22 | """ write to pickle
23 | """
24 | assert (filename.split('.')[-1] == "pickle") or (filename.split('.')[-1] == "pkl"), f"filename should end with ('pickle','pkl') currently: {filename}"
25 | output = open(filename, 'wb')
26 | pickle.dump(obj, output)
27 |
28 | return
29 |
30 |
31 | def read_pickle_df(filepath):
32 | """ read pickled object
33 | """
34 | obj = pd.read_pickle(filepath)
35 | return obj
36 |
37 |
38 | def normalise_coordinate_names(ds):
39 | """ rename latitude/longitude to lat/lon """
40 | if "latitude" in ds.dims:
41 | ds = ds.rename({"latitude":"lat"})
42 | print("Renamed `latitude` to `lat`")
43 | if "longitude" in ds.dims:
44 | ds = ds.rename({"longitude":"lon"})
45 | print("Renamed `longitude` to `lon`")
46 | assert "time" in ds.dims, f"Must have `time` as dimension in object. Currently: {ds.dims}"
47 |
48 | return ds
49 |
50 |
51 | def select_same_time_slice(reference_ds, ds):
52 | """ Select the values for the same timestep as the """
53 | # CHECK THEY ARE THE SAME FREQUENCY
54 | # get the frequency of the time series from reference_ds
55 | freq = pd.infer_freq(reference_ds.time.values)
56 | old_freq = pd.infer_freq(ds.time.values)
57 | assert freq == old_freq, f"The frequencies should be the same! currenlty ref: {freq} vs. old: {old_freq}"
58 |
59 | # get the STARTING time point from the reference_ds
60 | min_time = reference_ds.time.min().values
61 | max_time = reference_ds.time.max().values
62 | orig_time_range = pd.date_range(min_time, max_time, freq=freq)
63 | # EXTEND the original time_range by 1 (so selecting the whole slice)
64 | # because python doesn't select the final in a range
65 | periods = len(orig_time_range) + 1
66 | # create new time series going ONE EXTRA PERIOD
67 | new_time_range = pd.date_range(min_time, freq=freq, periods=periods)
68 | new_max = new_time_range.max()
69 |
70 | # select using the NEW MAX as upper limit
71 | ds = ds.sel(time=slice(min_time, new_max))
72 | # assert reference_ds.time.shape[0] == ds.time.shape[0],"The time dimensions should match, currently reference_ds.time dims {reference_ds.time.shape[0]} != ds.time dims {ds.time.shape[0]}"
73 |
74 | print_time_min = pd.to_datetime(ds.time.min().values)
75 | print_time_max = pd.to_datetime(ds.time.max().values)
76 | try:
77 | vars = [i for i in ds.var().variables]
78 | except:
79 | vars = ds.name
80 | ref_vars = [i for i in reference_ds.var().variables]
81 | print(f"Select same timeslice for ds with vars: {vars}. Min {print_time_min} Max {print_time_max}")
82 |
83 | return ds
84 |
85 |
86 | def select_same_lat_lon_slice(reference_ds, ds):
87 | """
88 | Take a slice of data from the `ds` according to the bounding box from
89 | the reference_ds.
90 | NOTE: - latitude has to be from max() to min() for some reason?
91 | - becuase it's crossing the equator? e.g. -14.:8.
92 | Therefore, have to run an if statement to decide which way round to put the data
93 | """
94 | # lat_bounds = [reference_ds.lat.min(),reference_ds.lat.max()]
95 | # lon_bounds = [reference_ds.lon.min(),reference_ds.lon.max()]
96 | if len(ds.sel(lat=slice(reference_ds.lat.min(), reference_ds.lat.max())).lat) == 0:
97 | ds = ds.sel(lat=slice(reference_ds.lat.max(), reference_ds.lat.min()))
98 | else:
99 | ds = ds.sel(lat=slice(reference_ds.lat.min(), reference_ds.lat.max()))
100 | ds = ds.sel(lon=slice(reference_ds.lon.min(), reference_ds.lon.max()))
101 |
102 | try:
103 | vars = [i for i in ds.var().variables]
104 | except:
105 | vars = ds.name
106 | ref_vars = [i for i in reference_ds.var().variables]
107 | print(f"Select the same bounding box for ds {vars} from reference_ds {ref_vars}")
108 | return ds
109 |
110 |
111 | def open_drought_ds(data_path = 'spei_spi.nc'):
112 | """ returns the raw dataset & 2x boolean masks (SPI/SPEI)"""
113 | # Data path on MONTHLY GRID
114 |
115 | # open the data ()
116 | ds = xr.open_dataset(data_path)
117 | ds = ds.rename({"value":"spi"})
118 | ds = normalise_coordinate_names(ds)
119 |
120 | # turn the values less than -1 into mask
121 | spei_drought = ds.spei.where(ds.spei < -1)
122 | spi_drought = ds.spi.where(ds.spi < -1)
123 |
124 | # turn into a boolean mask
125 | drought_spei = spei_drought.notnull()
126 | drought_spei = drought_spei.rename('drought_spei')
127 | drought_spi = spi_drought.notnull()
128 | drought_spi = drought_spi.rename('drought_spi')
129 |
130 | return ds, drought_spei, drought_spi
131 |
132 |
133 | def ensure_same_time(reference_ds, ds):
134 | """ convert the TIMESTEPS to the same values
135 | e.g. • if Monthly data sometimes its in the middle - 01-16-98
136 | • other times its the start 01-01-98, othertimes 01-31-98
137 | Set them all to the same frequency using the freq from reference_ds
138 | """
139 | freq = pd.infer_freq(reference_ds.time.values)
140 | dr = pd.date_range(reference_ds.time.min().values , periods=len(ds.time.values), freq=freq)
141 | ds['time'] = dr
142 |
143 | return ds
144 |
145 |
146 | def convert_to_same_time_freq(reference_ds,ds):
147 | """ Upscale or downscale data so on the same time frequencies
148 | e.g. convert daily data to monthly ('MS' = month start)
149 | """
150 | freq = pd.infer_freq(reference_ds.time.values)
151 | ds = ds.resample(time='MS').median(dim='time')
152 |
153 | try:
154 | vars = [i for i in ds.var().variables]
155 | except:
156 | vars = ds.name
157 | print(f"Resampled ds ({vars}) to {freq}")
158 | return ds
159 |
160 |
161 | def convert_to_same_grid(reference_ds, ds):
162 | """ Use xEMSF package to regrid ds to the same grid as reference_ds """
163 | assert ("lat" in reference_ds.dims)&("lon" in reference_ds.dims), f"Need (lat,lon) in reference_ds dims Currently: {reference_ds.dims}"
164 | assert ("lat" in ds.dims)&("lon" in ds.dims), f"Need (lat,lon) in ds dims Currently: {ds.dims}"
165 |
166 | # create the grid you want to convert TO (from reference_ds)
167 | ds_out = xr.Dataset({
168 | 'lat': (['lat'], reference_ds.lat),
169 | 'lon': (['lon'], reference_ds.lon),
170 | })
171 |
172 | # create the regridder object
173 | # xe.Regridder(grid_in, grid_out, method='bilinear')
174 | regridder = xe.Regridder(ds, ds_out, 'bilinear', reuse_weights=True)
175 |
176 | # IF it's a dataarray just do the original transformations
177 | if isinstance(ds, xr.core.dataarray.DataArray):
178 | ds = regridder(ds)
179 | # OTHERWISE loop through each of the variables, regrid the datarray then recombine into dataset
180 | elif isinstance(ds, xr.core.dataset.Dataset):
181 | vars = [i for i in ds.var().variables]
182 | if len(vars) ==1 :
183 | ds = regridder(ds)
184 | else:
185 | output_dict = {}
186 | # LOOP over each variable and append to dict
187 | for var in vars:
188 | print(f"- regridding var {var} -")
189 | da = ds[var]
190 | da = regridder(da)
191 | output_dict[var] = da
192 | # REBUILD
193 | ds = xr.Dataset(output_dict)
194 | else:
195 | assert False, "This function only works with xarray dataset / dataarray objects"
196 |
197 | print(f"Regridded from {(regridder.Ny_in, regridder.Nx_in)} to {(regridder.Ny_out, regridder.Nx_out)}")
198 |
199 | return ds
200 |
201 |
202 | def netcdf_to_same_dim_shapes(reference_ds, *other_netcdfs):
203 | """ loop over each of the other_netcdfs and reshape them in TIME, LAT/LON
204 | to be the same as the reference_ds.
205 | This allows them to be stored as a single netcdf file
206 | """
207 | ds_to_merge = []
208 | for ds_ in tqdm.tqdm([*other_netcdfs]):
209 | ds_ = normalise_coordinate_names(ds_)
210 | assert ("lat" in ds_.dims)&("lon" in ds_.dims),f"lat and lon should be in ds.dims. Currently: {ds_.dims}"
211 |
212 | # select the same SLICE of time (reference_ds.min() - reference_ds.min())
213 | ds_ = convert_to_same_time_freq(reference_ds, ds_)
214 | ds_ = select_same_time_slice(reference_ds, ds_)
215 |
216 | # REGRID to same dimensions
217 | ds_ = convert_to_same_grid(reference_ds, ds_)
218 | # select the same lat lon slice
219 | ds_ = select_same_lat_lon_slice(reference_ds, ds_)
220 |
221 | # ENSURE DIMS MATCH
222 | first_var = [i for i in reference_ds.var().variables][0]
223 | np.testing.assert_allclose(reference_ds[first_var].shape[0], ds_.shape[0], atol=1), f"The TIME DIMENSION should be equal. Currently (time, lat, lon) {ds_.shape} should be {reference_ds[first_var].shape}"
224 | assert reference_ds[first_var].shape[1] == ds_.shape[1], f"The LAT DIMENSION should be equal. Currently (time, lat, lon) {ds_.shape} should be {reference_ds[first_var].shape}"
225 | assert reference_ds[first_var].shape[2] == ds_.shape[2], f"The LON DIMENSION should be equal. Currently (time, lat, lon) {ds_.shape} should be {reference_ds[first_var].shape}"
226 |
227 | ds_to_merge.append(ds_)
228 |
229 | return ds_to_merge
230 |
231 |
232 | def get_all_vars_from_ds(ds):
233 | """ return a list of strings for all variables in dataset """
234 | assert isinstance(ds, xr.core.dataset.Dataset), f"Currently only works with xr.Dataset objects. ds = {type(ds)}"
235 | vars = [i for i in ds.var().variables]
236 | return vars
237 |
238 |
239 | def append_reference_ds_vars(reference_ds, ds_to_merge):
240 | """merge in the reference_ds variables to the ds_to_merge"""
241 | for var in reference_ds.var().variables:
242 | ds_to_merge.append(reference_ds[var])
243 |
244 | return ds_to_merge
245 |
246 |
247 | def merge_netcdfs_to_one_file(reference_ds, *other_netcdfs, drought=False):
248 | """ merge the netcdf files with multiple variables into ONE netcdf file.
249 | Use the structure from the reference_ds to get the same TIME & LAT/LON GRID.
250 | """
251 | ds_to_merge = netcdf_to_same_dim_shapes(reference_ds, *other_netcdfs)
252 | ds_to_merge = append_reference_ds_vars(reference_ds, ds_to_merge)
253 |
254 | # join in the drought mask
255 | # --------------------------------------------------------------------------
256 | warnings.warn('This is done once here to put the ndvi mask into the nc file')
257 | output_ds = xr.merge(ds_to_merge)
258 | # --------------------------------------------------------------------------
259 | if drought:
260 | warnings.warn('Should be less focused on hardcoding for the drought variables. need to have a look at this')
261 | # TODO: reference ds might not be the drought mask. this functionality should be elsewhere
262 | spei_mask, spi_mask, ndvi_mask = create_drought_mask(output_ds[['spi','spei','ndvi']])
263 | ds_to_merge.append(spei_mask)
264 | ds_to_merge.append(spi_mask)
265 | ds_to_merge.append(ndvi_mask)
266 |
267 | output_ds = xr.merge(ds_to_merge)
268 |
269 | return output_ds
270 |
271 |
272 | def check_other_netcdfs(*other_netcdfs):
273 | """ if dataarray check that it's named! """
274 | for i, xr_obj in enumerate([*other_netcdfs]):
275 | if isinstance(xr_obj, xr.core.dataarray.DataArray):
276 | assert xr_obj.name != None, f"All dataarrays must be named! Dataarray #{i+1} not named"
277 | return
278 |
279 |
280 | def read_files(files):
281 | """ read in the files to be """
282 | xr_objs = []
283 | for file in files:
284 | xr_obj = xr.open_dataset(file)
285 | xr_objs.append(xr_obj)
286 |
287 | return xr_objs
288 |
289 |
290 | # @click.command()
291 | # @click.argument('reference_ds_path', type=click.Path(exists=True), default="EA_data/spei_spi.nc")
292 | # @click.option('files', '--files', envvar='FILES', multiple=True, type=click.Path())
293 | # @click.argument('output', type=click.File('wb'))
294 | # @click.option('--drought', default=False)
295 | if __name__ == "__main__":
296 | # TODO: IMPLEMENT THIS ALL IN PARALLEL
297 |
298 | reference_ds_path = '/soge-home/users/chri4118/EA_data/spei_spi.nc'
299 | # the reference ds
300 | reference_ds = xr.open_dataset('/soge-home/users/chri4118/EA_data/spei_spi.nc')
301 | reference_ds = reference_ds.rename({"value":'spi'})
302 | reference_ds = normalise_coordinate_names(reference_ds)
303 |
304 | # the output filepath
305 | output = "OUT.nc"
306 |
307 | # --------------------------------------------------------------------------
308 | # TODO: THIS ALL NEEDS TO BE MORE DYNAMICALLY SET UP
309 | # variables to join
310 | TEMP = xr.open_dataset("/soge-home/users/chri4118/EA_data/LST_EastAfrica.nc")
311 | lst_day = TEMP.lst_day
312 | lst_night = TEMP.lst_night
313 | lst_mean = (TEMP.lst_day + TEMP.lst_night) / 2
314 | lst_mean.name = "lst_mean"
315 |
316 | ET = xr.open_dataset("/soge-home/users/chri4118/EA_data/ET_EastAfrica.nc")
317 | evap = ET.evaporation
318 | baresoil_evap = ET.baresoil_evaporation
319 | pet = ET.potential_evaporation
320 | transp = ET.transpiration
321 | surface_sm = ET.surface_soil_moisture
322 | rootzone_sm = ET.rootzone_soil_moisture
323 |
324 | SM = xr.open_dataset('/soge-home/users/chri4118/EA_data/SM_EastAfrica.nc')
325 | sm = SM.sm
326 |
327 | PCP = xr.open_dataset('/soge-home/projects/crop_yield/chirps/EA_precip.nc')
328 | precip = PCP.precip
329 |
330 | VEG = xr.open_dataset('/soge-home/users/chri4118/EA_data/NDVI_EastAfrica.nc')
331 | ndvi = VEG.ndvi
332 | evi = VEG.evi
333 |
334 | # concatenate into one list to pass to the function
335 | vars_list = [lst_day, lst_night, lst_mean, lst_mean, evap, baresoil_evap, pet, transp, surface_sm, rootzone_sm, sm, precip, ndvi, evi]
336 |
337 | print(f"Reading Data from: \n{TEMP}\n{ET}\n{SM}\n{PCP}\n{VEG}")
338 | # --------------------------------------------------------------------------
339 |
340 | check_other_netcdfs(*vars_list)
341 | output_ds = merge_netcdfs_to_one_file(reference_ds, *vars_list, drought=True)
342 | save_netcdf(output_ds, output)
343 |
344 | print("** Process Finished **")
345 |
--------------------------------------------------------------------------------
/notebooks/01_tl_data_exploration.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": 1,
6 | "metadata": {},
7 | "outputs": [],
8 | "source": [
9 | "import xarray as xr\n",
10 | "import pandas as pd\n",
11 | "import numpy as np\n",
12 | "import seaborn as sns\n",
13 | "import matplotlib.pyplot as plt"
14 | ]
15 | },
16 | {
17 | "cell_type": "code",
18 | "execution_count": 2,
19 | "metadata": {},
20 | "outputs": [
21 | {
22 | "data": {
23 | "text/plain": [
24 | "\n",
25 | "Dimensions: (lat: 404, lon: 316, time: 85)\n",
26 | "Coordinates:\n",
27 | " * time (time) datetime64[ns] 2010-01-01 ... 2017-01-01\n",
28 | " * lon (lon) float32 32.524994 32.574997 ... 48.274994\n",
29 | " * lat (lat) float32 -4.9750023 -4.925003 ... 15.174995\n",
30 | " month (time) int64 ...\n",
31 | "Data variables:\n",
32 | " lst_day (time, lat, lon) float64 ...\n",
33 | " lst_night (time, lat, lon) float64 ...\n",
34 | " lst_mean (time, lat, lon) float64 ...\n",
35 | " evaporation (time, lat, lon) float64 ...\n",
36 | " baresoil_evaporation (time, lat, lon) float64 ...\n",
37 | " potential_evaporation (time, lat, lon) float64 ...\n",
38 | " transpiration (time, lat, lon) float64 ...\n",
39 | " surface_soil_moisture (time, lat, lon) float64 ...\n",
40 | " rootzone_soil_moisture (time, lat, lon) float64 ...\n",
41 | " sm (time, lat, lon) float64 ...\n",
42 | " precip (time, lat, lon) float64 ...\n",
43 | " ndvi (time, lat, lon) float64 ...\n",
44 | " evi (time, lat, lon) float64 ...\n",
45 | " spei (time, lat, lon) float32 ...\n",
46 | " spi (time, lat, lon) float64 ...\n",
47 | " drought_spei (time, lat, lon) bool ...\n",
48 | " drought_spi (time, lat, lon) bool ...\n",
49 | " drought_ndvi (time, lat, lon) bool ..."
50 | ]
51 | },
52 | "execution_count": 2,
53 | "metadata": {},
54 | "output_type": "execute_result"
55 | }
56 | ],
57 | "source": [
58 | "ds = xr.open_dataset('/Volumes/Lees_Extend/data/ea_data/OUT.nc')\n",
59 | "ds"
60 | ]
61 | },
62 | {
63 | "cell_type": "code",
64 | "execution_count": 61,
65 | "metadata": {},
66 | "outputs": [
67 | {
68 | "name": "stdout",
69 | "output_type": "stream",
70 | "text": [
71 | "Done for Varible ndvi\n",
72 | "Done for Varible spi\n",
73 | "Done for Varible precip\n",
74 | "Done for Varible sm\n",
75 | "Done for Varible lst_day\n",
76 | "Done for Varible lst_night\n",
77 | "Done for Varible lst_mean\n"
78 | ]
79 | },
80 | {
81 | "ename": "AttributeError",
82 | "evalue": "'Figure' object has no attribute 'title'",
83 | "output_type": "error",
84 | "traceback": [
85 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
86 | "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)",
87 | "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 63\u001b[0m \u001b[0;31m# variables = [\"ndvi\",\"spi\"]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 64\u001b[0m \u001b[0;31m# TODO: why doesn't it work with 2 values?\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 65\u001b[0;31m \u001b[0mfig\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mplot_variables\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mds\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvariables\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 66\u001b[0m \u001b[0mfig\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msavefig\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'../figs/variable_distribution.png'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
88 | "\u001b[0;32m\u001b[0m in \u001b[0;36mplot_variables\u001b[0;34m(Dataset, variables)\u001b[0m\n\u001b[1;32m 56\u001b[0m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34mf'Done for Varible {var}'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 57\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 58\u001b[0;31m \u001b[0mfig\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtitle\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Distribution of variable values'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 59\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 60\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mfig\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
89 | "\u001b[0;31mAttributeError\u001b[0m: 'Figure' object has no attribute 'title'"
90 | ]
91 | },
92 | {
93 | "data": {
94 | "image/png": "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\n",
95 | "text/plain": [
96 | ""
97 | ]
98 | },
99 | "metadata": {
100 | "needs_background": "light"
101 | },
102 | "output_type": "display_data"
103 | }
104 | ],
105 | "source": [
106 | "def drop_nans_and_flatten(dataArray):\n",
107 | " \"\"\"flatten the array and drop nans from that array. Useful for plotting histograms.\n",
108 | " \n",
109 | " Arguments:\n",
110 | " ---------\n",
111 | " : dataArray (xr.DataArray)\n",
112 | " the DataArray of your value you want to flatten\n",
113 | " \"\"\"\n",
114 | " # drop NaNs and flatten\n",
115 | " return dataArray.values[~np.isnan(dataArray.values)]\n",
116 | "\n",
117 | "\n",
118 | "def set_no_axs_rows(variables):\n",
119 | " \"\"\"Dynamically set the number of axes rows\n",
120 | " \n",
121 | " Arguments:\n",
122 | " ---------\n",
123 | " : variables (list)\n",
124 | " the variables that we want to plot distributions of\n",
125 | " \"\"\"\n",
126 | " # Dynamically set the number of ROWS in the subplots\n",
127 | " if len(variables) % 3 > 0:\n",
128 | " n_axs = (len(variables) // 3) + 1\n",
129 | " else:\n",
130 | " n_axs = len(variables) // 3\n",
131 | " \n",
132 | " return n_axs\n",
133 | "\n",
134 | "def plot_variables(Dataset, variables):\n",
135 | " \"\"\" plot histograms of \n",
136 | " \n",
137 | " Arguments:\n",
138 | " ---------\n",
139 | " : Dataset (xr.Dataset)\n",
140 | " the dataset holding the variables of interest\n",
141 | " : variables (list)\n",
142 | " list of str for the labels of the values that you want to plot\n",
143 | " \"\"\"\n",
144 | " assert all([var_ in [var for var in ds.variables.keys()] for var_ in variables]), f\"The variables supplied must be in the xr.Dataset variables. Currently looking for {variables} in dataset variables: {[var for var in ds.variables.keys()]}\\n The variable missing is: {[var_ in [var for var in ds.variables.keys()] for var_ in variables]}\"\n",
145 | " \n",
146 | " n_axs = set_no_axs_rows(variables)\n",
147 | " fig, axs = plt.subplots(n_axs, 3, figsize=(15,8))\n",
148 | " \n",
149 | " # plot each of the variables \n",
150 | " for ix, var in enumerate(variables):\n",
151 | " # get the axes we are plotting on\n",
152 | " ax_ix = np.unravel_index(ix+1,(n_axs,3))\n",
153 | " ax = axs[ax_ix]\n",
154 | " \n",
155 | " # flatten array and drop nans from the variable\n",
156 | " flat_array = drop_nans_and_flatten(ds[var])\n",
157 | " \n",
158 | " # plot the histogram for that variable\n",
159 | " sns.distplot(flat_array, ax=ax)\n",
160 | " ax.set_title(f'{var} Histogram')\n",
161 | " print(f'Done for Varible {var}')\n",
162 | " \n",
163 | " fig.suptitle('Distribution of variable values')\n",
164 | " plt.tight_layout()\n",
165 | " \n",
166 | " return fig\n",
167 | " \n",
168 | "variables = [\"ndvi\",\"spi\",\"precip\",\"sm\",\"lst_day\",\"lst_night\",\"lst_mean\"]\n",
169 | "# variables = [\"ndvi\",\"spi\"]\n",
170 | "# TODO: why doesn't it work with 2 values?\n",
171 | "fig = plot_variables(ds, variables)\n",
172 | "fig.savefig('../figs/variable_distribution.png')"
173 | ]
174 | },
175 | {
176 | "cell_type": "code",
177 | "execution_count": 49,
178 | "metadata": {},
179 | "outputs": [
180 | {
181 | "data": {
182 | "text/plain": [
183 | "(0, 1)"
184 | ]
185 | },
186 | "execution_count": 49,
187 | "metadata": {},
188 | "output_type": "execute_result"
189 | }
190 | ],
191 | "source": [
192 | "assert False, \"The following does not work because the unravel index doesn't work for \"\n",
193 | "\n",
194 | "variables = [\"ndvi\",\"spi\"]\n",
195 | "plot_variables(ds, variables)"
196 | ]
197 | },
198 | {
199 | "cell_type": "code",
200 | "execution_count": null,
201 | "metadata": {},
202 | "outputs": [],
203 | "source": []
204 | }
205 | ],
206 | "metadata": {
207 | "kernelspec": {
208 | "display_name": "Python 3",
209 | "language": "python",
210 | "name": "python3"
211 | },
212 | "language_info": {
213 | "codemirror_mode": {
214 | "name": "ipython",
215 | "version": 3
216 | },
217 | "file_extension": ".py",
218 | "mimetype": "text/x-python",
219 | "name": "python",
220 | "nbconvert_exporter": "python",
221 | "pygments_lexer": "ipython3",
222 | "version": "3.7.2"
223 | }
224 | },
225 | "nbformat": 4,
226 | "nbformat_minor": 2
227 | }
228 |
--------------------------------------------------------------------------------