Skip to content

Commit 9c126ff

Browse files
productivity indicators
1 parent d403303 commit 9c126ff

6 files changed

Lines changed: 419 additions & 70 deletions

File tree

cybench/config.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -150,11 +150,14 @@
150150
# Soil moisture indicators: surface moisture, root zone moisture
151151
SOIL_MOISTURE_INDICATORS = ["ssm"] # , "rsm"]
152152

153+
PRODUCTIVITY_INDICATORS = ["twso"]
154+
153155
TIME_SERIES_INPUTS = {
154-
"meteo": METEO_INDICATORS,
155-
"fpar": [RS_FPAR],
156-
"ndvi": [RS_NDVI],
157-
"soil_moisture": SOIL_MOISTURE_INDICATORS,
156+
# "meteo": METEO_INDICATORS,
157+
# "fpar": [RS_FPAR],
158+
# "ndvi": [RS_NDVI],
159+
# "soil_moisture": SOIL_MOISTURE_INDICATORS,
160+
"twso": PRODUCTIVITY_INDICATORS,
158161
}
159162

160163
# Time series predictors
@@ -171,6 +174,7 @@
171174
RS_FPAR: "mean",
172175
RS_NDVI: "mean",
173176
"ssm": "mean",
177+
"twso": "max",
174178
}
175179

176180
# All predictors. Add more when available
@@ -198,10 +202,11 @@
198202
# Lead time for forecasting
199203
# Choices: "middle-of-season", "quarter-of-season",
200204
# "n-day(s)" where n is an integer
201-
FORECAST_LEAD_TIME = "middle-of-season"
205+
# FORECAST_LEAD_TIME = "middle-of-season"
206+
FORECAST_LEAD_TIME = "0-days"
202207

203208
# Buffer period before the start of season
204-
SPINUP_DAYS = 90
209+
SPINUP_DAYS = 0
205210

206211
# Logging
207212
PATH_LOGS_DIR = os.path.join(PATH_OUTPUT_DIR, "logs")

cybench/datasets/alignment.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ def align_to_crop_season_window_numpy(
271271
valid_start = np.zeros_like(valid_mask, dtype=bool)
272272
valid_end = np.zeros_like(valid_mask, dtype=bool)
273273

274-
tolerance = np.timedelta64(10, "D") # indicators may not come in daily timesteps
274+
tolerance = np.timedelta64(15, "D") # indicators may not come in daily timesteps
275275
valid_start[valid_mask] = date_min[valid_mask] - tolerance < (
276276
cutoff_dates[crop_indices[valid_mask]]
277277
- season_window_lengths[crop_indices[valid_mask]].astype("timedelta64[D]")

cybench/models/naive_models.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,65 @@ def load(cls, model_name):
100100
saved_model = pickle.load(f)
101101

102102
return saved_model
103+
104+
105+
class MaxPredictorModel(BaseModel):
106+
"""A naive model that predicts the max value of a predefined feature column
107+
for each test item. No training; prediction uses only test data.
108+
"""
109+
110+
def __init__(self, feature_column):
111+
self.feature_column = feature_column
112+
self._logger = logging.getLogger(__name__)
113+
114+
def fit(self, dataset, **fit_params):
115+
"""No training needed; return self."""
116+
self._logger.info("MaxFromItemModel: nothing to fit.")
117+
return self, {}
118+
119+
def predict_items(self, X: list):
120+
"""Predict the max value contained in each test item.
121+
122+
Assumes each item is a dict where item[self.feature_column]
123+
is a list, array, or iterable of numeric values.
124+
"""
125+
predictions = np.zeros(len(X))
126+
for i, item in enumerate(X):
127+
values = item.get(self.feature_column, None)
128+
129+
if values is None:
130+
raise ValueError(
131+
f"Item {i} does not contain the feature column '{self.feature_column}'."
132+
)
133+
134+
try:
135+
max_val = (
136+
0.001 * np.nanmax(values) if np.any(~np.isnan(values)) else np.NAN
137+
)
138+
predictions[i] = max_val
139+
except Exception as e:
140+
raise ValueError(f"Could not compute max for item {i}: {e}")
141+
return predictions, {}
142+
143+
def save(self, model_name):
144+
"""Save model, e.g. using pickle.
145+
146+
Args:
147+
model_name: Filename that will be used to save the model.
148+
"""
149+
with open(model_name, "wb") as f:
150+
pickle.dump(self, f)
151+
152+
def load(cls, model_name):
153+
"""Deserialize a saved model.
154+
155+
Args:
156+
model_name: Filename that was used to save the model.
157+
158+
Returns:
159+
The deserialized model.
160+
"""
161+
with open(model_name, "rb") as f:
162+
saved_model = pickle.load(f)
163+
164+
return saved_model

cybench/runs/run_benchmark.py

Lines changed: 98 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
get_default_metrics,
2222
prepare_targets_preds,
2323
)
24-
from cybench.models.naive_models import AverageYieldModel
24+
from cybench.models.naive_models import AverageYieldModel, MaxPredictorModel
2525
from cybench.models.trend_models import TrendModel
2626
from cybench.models.sklearn_models import SklearnRidge, SklearnRandomForest
2727
from cybench.models.xgboost_model import XGBoostModel
@@ -43,6 +43,7 @@
4343

4444
_BASELINE_MODEL_CONSTRUCTORS = {
4545
"AverageYieldModel": AverageYieldModel,
46+
"MaxPredictorModel": MaxPredictorModel,
4647
"LinearTrend": TrendModel,
4748
"SklearnRidge": SklearnRidge,
4849
"RidgeRes": RidgeRes,
@@ -60,6 +61,8 @@
6061

6162
BASELINE_MODELS = list(_BASELINE_MODEL_CONSTRUCTORS.keys())
6263

64+
BASELINE_MODELS = ["AverageYieldModel", "MaxPredictorModel"]
65+
6366
_BASELINE_MODEL_INIT_KWARGS = defaultdict(dict)
6467

6568
NN_MODELS_EPOCHS = 50
@@ -90,6 +93,45 @@
9093
"epochs": NN_MODELS_EPOCHS,
9194
"device": "cuda" if torch.cuda.is_available() else "cpu",
9295
}
96+
_BASELINE_MODEL_INIT_KWARGS["MaxPredictorModel"] = {
97+
"feature_column": "twso",
98+
}
99+
100+
_BASELINE_MODEL_FIT_KWARGS["MaxPredictorModel"] = {
101+
"feature_column": "twso",
102+
}
103+
104+
105+
def discover_datasets_from_disk(path_data_dir: str):
106+
"""
107+
Discover datasets by checking for:
108+
PATH_DATA_DIR/<crop>/<region>/twso_<crop>_<region>.csv
109+
Returns list of dataset_name strings like "maize_FR".
110+
"""
111+
found = []
112+
113+
# iterate crops as folders under PATH_DATA_DIR
114+
if not os.path.isdir(path_data_dir):
115+
raise FileNotFoundError(
116+
f"PATH_DATA_DIR does not exist or is not a directory: {path_data_dir}"
117+
)
118+
119+
for crop in sorted(os.listdir(path_data_dir)):
120+
crop_dir = os.path.join(path_data_dir, crop)
121+
if not os.path.isdir(crop_dir):
122+
continue
123+
124+
# iterate regions as folders under crop
125+
for region in sorted(os.listdir(crop_dir)):
126+
region_dir = os.path.join(crop_dir, region)
127+
if not os.path.isdir(region_dir):
128+
continue
129+
130+
twso_file = os.path.join(region_dir, f"twso_{crop}_{region}.csv")
131+
if os.path.exists(twso_file):
132+
found.append(f"{crop}_{region}")
133+
134+
return found
93135

94136

95137
def run_benchmark(
@@ -164,6 +206,7 @@ def run_benchmark(
164206
else:
165207
sel_years = all_years
166208

209+
all_results = []
167210
for test_year in sel_years:
168211
train_years = [y for y in all_years if y != test_year]
169212
test_years = [test_year]
@@ -194,13 +237,14 @@ def run_benchmark(
194237
df = pd.DataFrame.from_dict(model_output)
195238
df[KEY_COUNTRY] = df[KEY_LOC].str[:2]
196239
df.set_index([KEY_COUNTRY, KEY_LOC, KEY_YEAR], inplace=True)
197-
df.to_csv(os.path.join(path_results, f"{dataset_name}_year_{test_year}.csv"))
240+
all_results.append(df)
198241

199-
df_metrics = compute_metrics(run_name, list(model_constructors.keys()))
242+
df_all = pd.concat(all_results).sort_index()
200243

201-
return {
202-
"df_metrics": df_metrics,
203-
}
244+
results_file = os.path.join(path_results, f"{dataset_name}.csv")
245+
print(f"write results to {results_file}")
246+
df_all.to_csv(results_file)
247+
return
204248

205249

206250
def load_results(
@@ -228,6 +272,8 @@ def load_results(
228272

229273
df_all = pd.DataFrame()
230274
for file in files:
275+
if not file.lower().endswith(".csv"):
276+
continue
231277
path = os.path.join(path_results, file)
232278
df = pd.read_csv(path)
233279
df_all = pd.concat([df_all, df], axis=0)
@@ -339,7 +385,12 @@ def run_benchmark_on_all_data():
339385
if __name__ == "__main__":
340386
parser = argparse.ArgumentParser(prog="run_benchmark.py", description="Run cybench")
341387
parser.add_argument("-r", "--run-name")
342-
parser.add_argument("-d", "--dataset-name")
388+
parser.add_argument(
389+
"-d",
390+
"--dataset-name",
391+
default=None,
392+
help="Dataset name (e.g. maize_FR). If omitted or 'all'/'none', run all datasets.",
393+
)
343394
parser.add_argument("-m", "--mode")
344395
parser.add_argument(
345396
"-y",
@@ -350,48 +401,45 @@ def run_benchmark_on_all_data():
350401
help="Test year(s)",
351402
)
352403
args = parser.parse_args()
353-
dataset_name = args.dataset_name
354-
assert dataset_name is not None
355-
356-
if args.run_name is not None:
357-
run_name = args.run_name
358-
else:
359-
run_name = dataset_name
360404

361-
if args.years is None or [y.lower() for y in args.years] in (["none"], ["all"]):
362-
args.years = None
405+
if args.dataset_name is None or str(args.dataset_name).lower() in ("none", "all"):
406+
dataset_names = discover_datasets_from_disk(PATH_DATA_DIR)
407+
if not dataset_names:
408+
raise FileNotFoundError(
409+
f"No datasets found. Expected files like "
410+
f"{PATH_DATA_DIR}/<crop>/<region>/twso_<crop>_<region>.csv"
411+
)
363412
else:
364-
args.years = [int(y) for y in args.years]
365-
sel_years = args.years
366-
367-
if (args.mode is not None) and args.mode == "test":
368-
# skipping some models
369-
baseline_models = [
370-
"AverageYieldModel",
371-
"LinearTrend",
372-
"SklearnRidge",
373-
"RidgeRes",
374-
"LSTM",
375-
"LSTMRes",
376-
]
377-
# override epochs for nn-models
378-
nn_models_epochs = 5
379-
results = run_benchmark(
380-
run_name=run_name,
381-
dataset_name=dataset_name,
382-
baseline_models=baseline_models,
383-
nn_models_epochs=nn_models_epochs,
384-
)
385-
else:
386-
results = run_benchmark(
387-
run_name=run_name, dataset_name=dataset_name, sel_years=sel_years
388-
)
389-
390-
index_cols = results["df_metrics"].index.names
391-
df_metrics = results["df_metrics"].reset_index()
392-
393-
metric_cols = [c for c in df_metrics.columns if c not in index_cols]
394-
395-
# Group and average all available metrics
396-
agg_df = df_metrics.groupby("model")[metric_cols].mean().round(3)
397-
print(agg_df)
413+
dataset_names = [args.dataset_name]
414+
415+
for dataset_name in dataset_names:
416+
run_name = args.run_name if args.run_name is not None else dataset_name
417+
418+
if args.years is None or [y.lower() for y in args.years] in (["none"], ["all"]):
419+
args.years = None
420+
else:
421+
args.years = [int(y) for y in args.years]
422+
sel_years = args.years
423+
424+
if (args.mode is not None) and args.mode == "test":
425+
# skipping some models
426+
baseline_models = [
427+
"AverageYieldModel",
428+
"LinearTrend",
429+
"SklearnRidge",
430+
"RidgeRes",
431+
"LSTM",
432+
"LSTMRes",
433+
]
434+
# override epochs for nn-models
435+
nn_models_epochs = 5
436+
results = run_benchmark(
437+
run_name=run_name,
438+
dataset_name=dataset_name,
439+
baseline_models=baseline_models,
440+
nn_models_epochs=nn_models_epochs,
441+
)
442+
else:
443+
run_benchmark(
444+
run_name=run_name, dataset_name=dataset_name, sel_years=sel_years
445+
)

0 commit comments

Comments
 (0)