rainfallqc 0.2.0__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. {rainfallqc-0.2.0 → rainfallqc-0.2.2}/PKG-INFO +4 -1
  2. {rainfallqc-0.2.0 → rainfallqc-0.2.2}/pyproject.toml +9 -4
  3. {rainfallqc-0.2.0 → rainfallqc-0.2.2}/src/rainfallqc/__init__.py +4 -1
  4. {rainfallqc-0.2.0 → rainfallqc-0.2.2}/src/rainfallqc/checks/comparison_checks.py +14 -3
  5. {rainfallqc-0.2.0 → rainfallqc-0.2.2}/src/rainfallqc/checks/gauge_checks.py +24 -13
  6. {rainfallqc-0.2.0 → rainfallqc-0.2.2}/src/rainfallqc/checks/neighbourhood_checks.py +9 -0
  7. rainfallqc-0.2.2/src/rainfallqc/checks/pypwsqc_filters.py +475 -0
  8. {rainfallqc-0.2.0 → rainfallqc-0.2.2}/src/rainfallqc/checks/timeseries_checks.py +11 -2
  9. rainfallqc-0.2.2/src/rainfallqc/core/all_qc_checks.py +98 -0
  10. {rainfallqc-0.2.0 → rainfallqc-0.2.2}/src/rainfallqc/qc_frameworks/apply_qc_framework.py +0 -1
  11. {rainfallqc-0.2.0 → rainfallqc-0.2.2}/src/rainfallqc/qc_frameworks/inbuilt_qc_frameworks.py +12 -2
  12. {rainfallqc-0.2.0 → rainfallqc-0.2.2}/src/rainfallqc/utils/data_readers.py +34 -8
  13. {rainfallqc-0.2.0 → rainfallqc-0.2.2}/src/rainfallqc/utils/data_utils.py +24 -1
  14. {rainfallqc-0.2.0 → rainfallqc-0.2.2}/src/rainfallqc/utils/neighbourhood_utils.py +87 -24
  15. {rainfallqc-0.2.0 → rainfallqc-0.2.2}/src/rainfallqc/utils/spatial_utils.py +32 -0
  16. {rainfallqc-0.2.0 → rainfallqc-0.2.2}/LICENSE +0 -0
  17. {rainfallqc-0.2.0 → rainfallqc-0.2.2}/README.rst +0 -0
  18. {rainfallqc-0.2.0 → rainfallqc-0.2.2}/src/rainfallqc/checks/__init__.py +0 -0
  19. {rainfallqc-0.2.0 → rainfallqc-0.2.2}/src/rainfallqc/core/__init__.py +0 -0
  20. {rainfallqc-0.2.0 → rainfallqc-0.2.2}/src/rainfallqc/data/ETCCDI/RawData_HADEX2_CDD_1951-2010_ANN_from-90to90_from-180to180.nc +0 -0
  21. {rainfallqc-0.2.0 → rainfallqc-0.2.2}/src/rainfallqc/data/ETCCDI/RawData_HADEX2_CWD_1951-2010_ANN_from-90to90_from-180to180.nc +0 -0
  22. {rainfallqc-0.2.0 → rainfallqc-0.2.2}/src/rainfallqc/data/ETCCDI/RawData_HADEX2_PRCPTOT_1951-2010_ANN_from-90to90_from-180to180.nc +0 -0
  23. {rainfallqc-0.2.0 → rainfallqc-0.2.2}/src/rainfallqc/data/ETCCDI/RawData_HADEX2_R99p_1951-2010_ANN_from-90to90_from-180to180.nc +0 -0
  24. {rainfallqc-0.2.0 → rainfallqc-0.2.2}/src/rainfallqc/data/ETCCDI/RawData_HADEX2_Rx1day_1951-2010_ANN_from-90to90_from-180to180.nc +0 -0
  25. {rainfallqc-0.2.0 → rainfallqc-0.2.2}/src/rainfallqc/data/ETCCDI/RawData_HADEX2_SDII_1951-2010_ANN_from-90to90_from-180to180.nc +0 -0
  26. {rainfallqc-0.2.0 → rainfallqc-0.2.2}/src/rainfallqc/data/ETCCDI/__init__.py +0 -0
  27. {rainfallqc-0.2.0 → rainfallqc-0.2.2}/src/rainfallqc/data/__init__.py +0 -0
  28. {rainfallqc-0.2.0 → rainfallqc-0.2.2}/src/rainfallqc/qc_frameworks/__init__.py +0 -0
  29. {rainfallqc-0.2.0 → rainfallqc-0.2.2}/src/rainfallqc/utils/__init__.py +0 -0
  30. {rainfallqc-0.2.0 → rainfallqc-0.2.2}/src/rainfallqc/utils/stats.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rainfallqc
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Quality control for rainfall data
5
5
  Author-email: Tom Keel <tomkee@ceh.ac.uk>
6
6
  Maintainer-email: Tom Keel <tomkee@ceh.ac.uk>
@@ -23,10 +23,13 @@ Requires-Dist: scipy
23
23
  Requires-Dist: numpy
24
24
  Requires-Dist: polars>=1.30
25
25
  Requires-Dist: pandas
26
+ Requires-Dist: pyarrow
26
27
  Requires-Dist: xarray
27
28
  Requires-Dist: netCDF4
28
29
  Requires-Dist: bottleneck
29
30
  Requires-Dist: geopy
31
+ Requires-Dist: poligrain
32
+ Requires-Dist: pypwsqc
30
33
  Requires-Dist: pip ; extra == "dev"
31
34
  Requires-Dist: numpy ; extra == "dev"
32
35
  Requires-Dist: bump-my-version ; extra == "dev"
@@ -30,10 +30,13 @@ dependencies = [
30
30
  "numpy",
31
31
  "polars>=1.30",
32
32
  "pandas",
33
+ "pyarrow",
33
34
  "xarray",
34
35
  "netCDF4",
35
36
  "bottleneck",
36
- "geopy"
37
+ "geopy",
38
+ "poligrain",
39
+ "pypwsqc",
37
40
  ]
38
41
 
39
42
  [project.optional-dependencies]
@@ -65,7 +68,7 @@ homepage = "https://github.com/thomasjkeel/RainfallQC"
65
68
  target-version = ["py310", "py311", "py312", "py313"]
66
69
 
67
70
  [tool.bumpversion]
68
- current_version = "0.2.0"
71
+ current_version = "0.2.2"
69
72
  commit = true
70
73
  message = "Bump version: {current_version} → {new_version}"
71
74
  commit_args = ""
@@ -146,8 +149,10 @@ case-sensitive = true
146
149
 
147
150
  [tool.ruff.lint.per-file-ignores]
148
151
  "tests/*.py" = [
149
- "ANN001", "ANN201", "ANN202", # Missing type annotation
150
- "D103" # Missing docstring in public function
152
+ "ANN001",
153
+ "ANN201",
154
+ "ANN202", # Missing type annotation
155
+ "D103", # Missing docstring in public function
151
156
  ]
152
157
 
153
158
  [tool.mypy]
@@ -2,8 +2,9 @@
2
2
 
3
3
  __author__ = """Tom Keel"""
4
4
  __email__ = "tomkee@ceh.ac.uk"
5
- __version__ = "0.2.0"
5
+ __version__ = "0.2.2"
6
6
 
7
+ from rainfallqc import core, utils
7
8
  from rainfallqc.checks import (
8
9
  comparison_checks,
9
10
  gauge_checks,
@@ -18,4 +19,6 @@ __all__ = [
18
19
  "gauge_checks",
19
20
  "neighbourhood_checks",
20
21
  "timeseries_checks",
22
+ "core",
23
+ "utils",
21
24
  ]
@@ -11,9 +11,11 @@ import numpy as np
11
11
  import polars as pl
12
12
  import xarray as xr
13
13
 
14
+ from rainfallqc.core.all_qc_checks import qc_check
14
15
  from rainfallqc.utils import data_readers, data_utils, neighbourhood_utils, stats
15
16
 
16
17
 
18
+ @qc_check("check_annual_exceedance_etccdi_r99p", require_non_negative=True)
17
19
  def check_annual_exceedance_etccdi_r99p(
18
20
  data: pl.DataFrame, target_gauge_col: str, gauge_lat: int | float, gauge_lon: int | float
19
21
  ) -> list:
@@ -43,7 +45,9 @@ def check_annual_exceedance_etccdi_r99p(
43
45
  etccdi_r99p = data_readers.load_etccdi_data(etccdi_var="R99p")
44
46
 
45
47
  # 2. Get nearest local R99p value to the gauge coordinates
46
- nearby_etccdi_r99p = neighbourhood_utils.get_nearest_etccdi_val_to_gauge(etccdi_r99p, gauge_lat, gauge_lon)
48
+ nearby_etccdi_r99p = neighbourhood_utils.get_nearest_non_nan_etccdi_val_to_gauge(
49
+ etccdi_r99p, etccdi_name="R99p", gauge_lat=gauge_lat, gauge_lon=gauge_lon
50
+ )
47
51
 
48
52
  # 3. Get sum of rainfall above the 99th percentile per year
49
53
  sum_rainfall_above_99percentile_per_year = get_sum_rainfall_above_percentile_per_year(
@@ -58,6 +62,7 @@ def check_annual_exceedance_etccdi_r99p(
58
62
  return exceedance_flags
59
63
 
60
64
 
65
+ @qc_check("check_annual_exceedance_etccdi_prcptot", require_non_negative=True)
61
66
  def check_annual_exceedance_etccdi_prcptot(
62
67
  data: pl.DataFrame, target_gauge_col: str, gauge_lat: int | float, gauge_lon: int | float
63
68
  ) -> list:
@@ -87,7 +92,9 @@ def check_annual_exceedance_etccdi_prcptot(
87
92
  etccdi_prcptot = data_readers.load_etccdi_data(etccdi_var="PRCPTOT")
88
93
 
89
94
  # 2. Get nearest local PRCPTOT value to the gauge coordinates
90
- nearby_etccdi_prcptot = neighbourhood_utils.get_nearest_etccdi_val_to_gauge(etccdi_prcptot, gauge_lat, gauge_lon)
95
+ nearby_etccdi_prcptot = neighbourhood_utils.get_nearest_non_nan_etccdi_val_to_gauge(
96
+ etccdi_prcptot, etccdi_name="PRCPTOT", gauge_lat=gauge_lat, gauge_lon=gauge_lon
97
+ )
91
98
 
92
99
  # 3. Get sum of rainfall above the 99th percentile per year
93
100
  sum_rainfall_above_99percentile_per_year = get_sum_rainfall_above_percentile_per_year(
@@ -102,6 +109,7 @@ def check_annual_exceedance_etccdi_prcptot(
102
109
  return exceedance_flags
103
110
 
104
111
 
112
+ @qc_check("check_exceedance_of_rainfall_world_record", require_non_negative=True)
105
113
  def check_exceedance_of_rainfall_world_record(data: pl.DataFrame, target_gauge_col: str, time_res: str) -> pl.DataFrame:
106
114
  """
107
115
  Check exceedance of rainfall world record.
@@ -135,6 +143,7 @@ def check_exceedance_of_rainfall_world_record(data: pl.DataFrame, target_gauge_c
135
143
  return data_w_flags.select(["time", "world_record_check"])
136
144
 
137
145
 
146
+ @qc_check("check_hourly_exceedance_etccdi_rx1day", require_non_negative=True)
138
147
  def check_hourly_exceedance_etccdi_rx1day(
139
148
  data: pl.DataFrame, target_gauge_col: str, gauge_lat: int | float, gauge_lon: int | float
140
149
  ) -> pl.DataFrame:
@@ -173,7 +182,9 @@ def check_hourly_exceedance_etccdi_rx1day(
173
182
  etccdi_rx1day = data_readers.load_etccdi_data(etccdi_var="Rx1day")
174
183
 
175
184
  # 3. Get nearest local Rx1day value to the gauge coordinates
176
- nearby_etccdi_rx1day = neighbourhood_utils.get_nearest_etccdi_val_to_gauge(etccdi_rx1day, gauge_lat, gauge_lon)
185
+ nearby_etccdi_rx1day = neighbourhood_utils.get_nearest_non_nan_etccdi_val_to_gauge(
186
+ etccdi_rx1day, etccdi_name="Rx1day", gauge_lat=gauge_lat, gauge_lon=gauge_lon
187
+ )
177
188
 
178
189
  # 4. Get local maximum ETCCDI value
179
190
  max_nearby_etccdi_rx1day = np.max(nearby_etccdi_rx1day["Rx1day"])
@@ -10,9 +10,11 @@ Classes and functions ordered by appearance in IntenseQC framework.
10
10
  import polars as pl
11
11
  import scipy.stats
12
12
 
13
+ from rainfallqc.core.all_qc_checks import qc_check
13
14
  from rainfallqc.utils import data_utils, stats
14
15
 
15
16
 
17
+ @qc_check("check_years_where_nth_percentile_is_zero", require_non_negative=True)
16
18
  def check_years_where_nth_percentile_is_zero(data: pl.DataFrame, target_gauge_col: str, quantile: float) -> list:
17
19
  """
18
20
  Return years where the n-th percentiles is zero.
@@ -38,6 +40,7 @@ def check_years_where_nth_percentile_is_zero(data: pl.DataFrame, target_gauge_co
38
40
  return nth_perc.filter(pl.col(target_gauge_col) == 0)["time"].dt.year().to_list()
39
41
 
40
42
 
43
+ @qc_check("check_years_where_annual_mean_k_top_rows_are_zero", require_non_negative=True)
41
44
  def check_years_where_annual_mean_k_top_rows_are_zero(data: pl.DataFrame, target_gauge_col: str, k: int) -> list:
42
45
  """
43
46
  Return year list where the annual mean top-K rows are zero.
@@ -63,6 +66,7 @@ def check_years_where_annual_mean_k_top_rows_are_zero(data: pl.DataFrame, target
63
66
  return data_top_k.filter(pl.col(target_gauge_col) == 0)["time"].dt.year().to_list()
64
67
 
65
68
 
69
+ @qc_check("check_temporal_bias", require_non_negative=True)
66
70
  def check_temporal_bias(
67
71
  data: pl.DataFrame,
68
72
  target_gauge_col: str,
@@ -109,6 +113,7 @@ def check_temporal_bias(
109
113
  return int(p_val < p_threshold)
110
114
 
111
115
 
116
+ @qc_check("check_intermittency", require_non_negative=True)
112
117
  def check_intermittency(
113
118
  data: pl.DataFrame, target_gauge_col: str, no_data_threshold: int = 2, annual_count_threshold: int = 5
114
119
  ) -> list:
@@ -135,20 +140,24 @@ def check_intermittency(
135
140
  List of years with intermittency issues.
136
141
 
137
142
  """
138
- # 1. Replace missing values with NaN
143
+ # 1. Check data has consistent time step
144
+ data_utils.check_data_has_consistent_time_step(data)
145
+
146
+ # 2. Replace missing values with NaN
139
147
  data = data_utils.replace_missing_vals_with_nan(data, target_gauge_col)
140
148
 
141
- # --- Step 2: Mark missing values ---
149
+ # 3. Mark missing values
142
150
  data = data.with_columns(pl.col(target_gauge_col).is_nan().alias("is_missing"))
143
151
 
144
- # 3. Assign group numbers to consecutive missing values
152
+ # 4. Assign group numbers to consecutive missing values
145
153
  grouped = data.with_columns(
146
154
  pl.when(pl.col("is_missing"))
147
155
  .then((~pl.col("is_missing")).cum_sum()) # Only number missing stretches
148
156
  .otherwise(None)
149
157
  .alias("group")
150
158
  )
151
- # 4. Count size of each missing group
159
+
160
+ # 5. Count size of each missing group
152
161
  group_counts = (
153
162
  grouped.filter(pl.col("is_missing"))
154
163
  .group_by("group")
@@ -156,21 +165,21 @@ def check_intermittency(
156
165
  .filter(pl.col("count") >= no_data_threshold)
157
166
  )
158
167
 
159
- # 5. Keep only valid groups
168
+ # 6. Keep only valid groups
160
169
  valid_missing_groups = group_counts["group"].to_list()
161
170
  valid_missing = grouped.filter(pl.col("group").is_in(valid_missing_groups))
162
171
 
163
- # 6. Get first and last time for each group
172
+ # 7. Get first and last time for each group
164
173
  first_last = (
165
174
  valid_missing.sort("time")
166
175
  .group_by("group")
167
176
  .agg([pl.first("time").alias("start_time"), pl.last("time").alias("end_time")])
168
177
  )
169
178
 
170
- # 7. Add stable row index to full sorted data
179
+ # 8. Add stable row index to full sorted data
171
180
  df_sorted = data.sort("time").with_row_index("row_idx")
172
181
 
173
- # 8. Lookup row indices of group bounds
182
+ # 9. Lookup row indices of group bounds
174
183
  first_last_with_idx = (
175
184
  first_last.join(df_sorted.select(["time", "row_idx"]), left_on="start_time", right_on="time")
176
185
  .rename({"row_idx": "start_idx"})
@@ -178,7 +187,7 @@ def check_intermittency(
178
187
  .rename({"row_idx": "end_idx"})
179
188
  )
180
189
 
181
- # 9. Compute prev/next indices and fetch values
190
+ # 10. Compute prev/next indices and fetch values
182
191
  row_lookup = df_sorted.select([pl.col("row_idx"), pl.col(target_gauge_col)])
183
192
 
184
193
  first_last_with_vals = (
@@ -189,21 +198,22 @@ def check_intermittency(
189
198
  .join(row_lookup.rename({target_gauge_col: "next_val"}), left_on="next_idx", right_on="row_idx")
190
199
  )
191
200
 
192
- # 10. Filter to groups bounded by zero
201
+ # 11. Filter to groups bounded by zero
193
202
  bounded_by_zero = first_last_with_vals.filter((pl.col("prev_val") == 0) & (pl.col("next_val") == 0))
194
203
 
195
- # 11. Extract year from each bounded group's start time
204
+ # 12. Extract year from each bounded group's start time
196
205
  bounded_years = bounded_by_zero.select(pl.col("start_time").dt.year().alias("year"))
197
206
 
198
- # 12. Count how many bounded groups occur per year
207
+ # 13. Count how many bounded groups occur per year
199
208
  year_counts = bounded_years.group_by("year").len()
200
209
 
201
- # 13. Get years exceeding threshold
210
+ # 14. Get years exceeding threshold
202
211
  years_w_intermittency = year_counts.filter(pl.col("len") >= annual_count_threshold)["year"].to_list()
203
212
 
204
213
  return years_w_intermittency
205
214
 
206
215
 
216
+ @qc_check("check_breakpoints", require_non_negative=True)
207
217
  def check_breakpoints(
208
218
  data: pl.DataFrame,
209
219
  target_gauge_col: str,
@@ -240,6 +250,7 @@ def check_breakpoints(
240
250
  return 0
241
251
 
242
252
 
253
+ @qc_check("check_min_val_change", require_non_negative=True)
243
254
  def check_min_val_change(data: pl.DataFrame, target_gauge_col: str, expected_min_val: float) -> list:
244
255
  """
245
256
  Return years when the minimum recorded value changes.
@@ -12,9 +12,11 @@ from typing import Iterable, List
12
12
  import numpy as np
13
13
  import polars as pl
14
14
 
15
+ from rainfallqc.core.all_qc_checks import qc_check
15
16
  from rainfallqc.utils import data_readers, data_utils, neighbourhood_utils, stats
16
17
 
17
18
 
19
+ @qc_check("check_wet_neighbours", require_non_negative=True)
18
20
  def check_wet_neighbours(
19
21
  neighbour_data: pl.DataFrame,
20
22
  target_gauge_col: str,
@@ -129,6 +131,7 @@ def check_wet_neighbours(
129
131
  return neighbour_data_w_wet_flags
130
132
 
131
133
 
134
+ @qc_check("check_dry_neighbours", require_non_negative=True)
132
135
  def check_dry_neighbours(
133
136
  neighbour_data: pl.DataFrame,
134
137
  target_gauge_col: str,
@@ -261,6 +264,7 @@ def check_dry_neighbours(
261
264
  return neighbour_data_w_dry_flags
262
265
 
263
266
 
267
+ @qc_check("check_monthly_neighbours", require_non_negative=True)
264
268
  def check_monthly_neighbours(
265
269
  monthly_neighbour_data: pl.DataFrame,
266
270
  target_gauge_col: str,
@@ -355,6 +359,7 @@ def check_monthly_neighbours(
355
359
  return monthly_neighbour_data_w_flags.select(["time", "majority_monthly_flag"])
356
360
 
357
361
 
362
+ @qc_check("check_timing_offset", require_non_negative=True)
358
363
  def check_timing_offset(
359
364
  neighbour_data: pl.DataFrame,
360
365
  target_gauge_col: str,
@@ -436,6 +441,7 @@ def check_timing_offset(
436
441
  return offset_flag
437
442
 
438
443
 
444
+ @qc_check("check_neighbour_affinity_index", require_non_negative=True)
439
445
  def check_neighbour_affinity_index(
440
446
  neighbour_data: pl.DataFrame, target_gauge_col: str, neighbouring_gauge_col: str
441
447
  ) -> float:
@@ -471,6 +477,7 @@ def check_neighbour_affinity_index(
471
477
  return stats.affinity_index(neighbour_data, binary_col="rain_not_minima")
472
478
 
473
479
 
480
+ @qc_check("check_neighbour_correlation", require_non_negative=True)
474
481
  def check_neighbour_correlation(
475
482
  neighbour_data: pl.DataFrame, target_gauge_col: str, neighbouring_gauge_col: str
476
483
  ) -> float:
@@ -501,6 +508,7 @@ def check_neighbour_correlation(
501
508
  return stats.gauge_correlation(neighbour_data, target_col=target_gauge_col, other_col=neighbouring_gauge_col)
502
509
 
503
510
 
511
+ @qc_check("check_daily_factor", require_non_negative=True)
504
512
  def check_daily_factor(
505
513
  neighbour_data: pl.DataFrame, target_gauge_col: str, neighbouring_gauge_col: str, averaging_method: str = "mean"
506
514
  ) -> float:
@@ -553,6 +561,7 @@ def check_daily_factor(
553
561
  raise ValueError(f"{averaging_method} not recognised, please use 'mean' or 'median'")
554
562
 
555
563
 
564
+ @qc_check("check_monthly_factor", require_non_negative=True)
556
565
  def check_monthly_factor(
557
566
  neighbour_data: pl.DataFrame, target_gauge_col: str, neighbouring_gauge_col: str
558
567
  ) -> pl.DataFrame: