RadGEEToolbox 1.7.0__py3-none-any.whl → 1.7.2__py3-none-any.whl
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.
- RadGEEToolbox/GenericCollection.py +123 -32
- RadGEEToolbox/GetPalette.py +136 -87
- RadGEEToolbox/LandsatCollection.py +465 -37
- RadGEEToolbox/Sentinel1Collection.py +607 -19
- RadGEEToolbox/Sentinel2Collection.py +468 -31
- RadGEEToolbox/VisParams.py +112 -194
- RadGEEToolbox/__init__.py +1 -1
- {radgeetoolbox-1.7.0.dist-info → radgeetoolbox-1.7.2.dist-info}/METADATA +8 -6
- radgeetoolbox-1.7.2.dist-info/RECORD +13 -0
- radgeetoolbox-1.7.0.dist-info/RECORD +0 -13
- {radgeetoolbox-1.7.0.dist-info → radgeetoolbox-1.7.2.dist-info}/WHEEL +0 -0
- {radgeetoolbox-1.7.0.dist-info → radgeetoolbox-1.7.2.dist-info}/licenses/LICENSE.txt +0 -0
- {radgeetoolbox-1.7.0.dist-info → radgeetoolbox-1.7.2.dist-info}/top_level.txt +0 -0
|
@@ -166,6 +166,10 @@ class Sentinel2Collection:
|
|
|
166
166
|
self._masked_water_collection = None
|
|
167
167
|
self._median = None
|
|
168
168
|
self._monthly_median = None
|
|
169
|
+
self._monthly_mean = None
|
|
170
|
+
self._monthly_max = None
|
|
171
|
+
self._monthly_min = None
|
|
172
|
+
self._monthly_sum = None
|
|
169
173
|
self._mean = None
|
|
170
174
|
self._max = None
|
|
171
175
|
self._min = None
|
|
@@ -482,7 +486,7 @@ class Sentinel2Collection:
|
|
|
482
486
|
return nbr
|
|
483
487
|
|
|
484
488
|
@staticmethod
|
|
485
|
-
def anomaly_fn(image, geometry, band_name=None, anomaly_band_name=None, replace=True):
|
|
489
|
+
def anomaly_fn(image, geometry, band_name=None, anomaly_band_name=None, replace=True, scale=10):
|
|
486
490
|
"""
|
|
487
491
|
Calculates the anomaly of a singleband image compared to the mean of the singleband image.
|
|
488
492
|
|
|
@@ -498,6 +502,7 @@ class Sentinel2Collection:
|
|
|
498
502
|
band_name (str, optional): A string representing the band name to be used for the output anomaly image. If not provided, the band name of the first band of the input image will be used.
|
|
499
503
|
anomaly_band_name (str, optional): A string representing the band name to be used for the output anomaly image. If not provided, the band name of the first band of the input image will be used.
|
|
500
504
|
replace (bool, optional): A boolean indicating whether to replace the original band with the anomaly band in the output image. If True, the output image will contain only the anomaly band. If False, the output image will contain both the original band and the anomaly band. Default is True.
|
|
505
|
+
scale (int, optional): The scale in meters to use for the reduction operation. Default is 10 meters.
|
|
501
506
|
|
|
502
507
|
Returns:
|
|
503
508
|
ee.Image: An ee.Image where each band represents the anomaly (deviation from
|
|
@@ -514,12 +519,16 @@ class Sentinel2Collection:
|
|
|
514
519
|
mean_image = image_to_process.reduceRegion(
|
|
515
520
|
reducer=ee.Reducer.mean(),
|
|
516
521
|
geometry=geometry,
|
|
517
|
-
scale=
|
|
522
|
+
scale=scale,
|
|
518
523
|
maxPixels=1e13
|
|
519
524
|
).toImage()
|
|
520
525
|
|
|
521
526
|
# Compute the anomaly by subtracting the mean image from the input image.
|
|
522
|
-
|
|
527
|
+
if scale == 10:
|
|
528
|
+
anomaly_image = image_to_process.subtract(mean_image)
|
|
529
|
+
else:
|
|
530
|
+
anomaly_image = image_to_process.reproject(crs=image_to_process.projection(), scale=scale).subtract(mean_image)
|
|
531
|
+
|
|
523
532
|
if anomaly_band_name is None:
|
|
524
533
|
if band_name:
|
|
525
534
|
anomaly_image = anomaly_image.rename(band_name)
|
|
@@ -530,7 +539,7 @@ class Sentinel2Collection:
|
|
|
530
539
|
anomaly_image = anomaly_image.rename(anomaly_band_name)
|
|
531
540
|
# return anomaly_image
|
|
532
541
|
if replace:
|
|
533
|
-
return anomaly_image.copyProperties(image)
|
|
542
|
+
return anomaly_image.copyProperties(image).set('system:time_start', image.get('system:time_start'))
|
|
534
543
|
else:
|
|
535
544
|
return image.addBands(anomaly_image, overwrite=True)
|
|
536
545
|
|
|
@@ -808,11 +817,11 @@ class Sentinel2Collection:
|
|
|
808
817
|
|
|
809
818
|
Args:
|
|
810
819
|
band_name (string or list of strings): name of band(s) (string) for calculating area. If providing multiple band names, pass as a list of strings.
|
|
811
|
-
geometry (ee.Geometry): ee.Geometry object denoting area to clip to for area calculation
|
|
820
|
+
geometry (ee.Geometry): ee.Geometry object denoting area to clip to for area calculation.
|
|
812
821
|
threshold (float): integer threshold to specify masking of pixels below threshold (defaults to -1). If providing multiple band names, the same threshold will be applied to all bands. Best practice in this case is to mask the bands prior to passing to this function and leave threshold at default of -1.
|
|
813
|
-
scale (int): integer scale of image resolution (meters) (defaults to
|
|
814
|
-
maxPixels (int): integer denoting maximum number of pixels for calculations
|
|
815
|
-
output_type (str): 'ImageCollection' to return an ee.ImageCollection, 'Sentinel2Collection' to return a Sentinel2Collection object (defaults to 'ImageCollection')
|
|
822
|
+
scale (int): integer scale of image resolution (meters) (defaults to 30).
|
|
823
|
+
maxPixels (int): integer denoting maximum number of pixels for calculations.
|
|
824
|
+
output_type (str): 'ImageCollection' or 'ee.ImageCollection' to return an ee.ImageCollection, 'Sentinel2Collection' to return a Sentinel2Collection object, or 'DataFrame', 'Pandas', 'pd', 'dataframe', 'df' to return a pandas DataFrame (defaults to 'ImageCollection').
|
|
816
825
|
area_data_export_path (str, optional): If provided, the function will save the resulting area data to a CSV file at the specified path.
|
|
817
826
|
|
|
818
827
|
Returns:
|
|
@@ -835,17 +844,45 @@ class Sentinel2Collection:
|
|
|
835
844
|
# Storing the result in the instance variable to avoid redundant calculations
|
|
836
845
|
self._PixelAreaSumCollection = AreaCollection
|
|
837
846
|
|
|
847
|
+
prop_names = band_name if isinstance(band_name, list) else [band_name]
|
|
848
|
+
|
|
838
849
|
# If an export path is provided, the area data will be exported to a CSV file
|
|
839
850
|
if area_data_export_path:
|
|
840
|
-
Sentinel2Collection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=
|
|
841
|
-
|
|
851
|
+
Sentinel2Collection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=prop_names, file_path=area_data_export_path+'.csv')
|
|
842
852
|
# Returning the result in the desired format based on output_type argument or raising an error for invalid input
|
|
843
|
-
if output_type == 'ImageCollection':
|
|
853
|
+
if output_type == 'ImageCollection' or output_type == 'ee.ImageCollection':
|
|
844
854
|
return self._PixelAreaSumCollection
|
|
845
855
|
elif output_type == 'Sentinel2Collection':
|
|
846
856
|
return Sentinel2Collection(collection=self._PixelAreaSumCollection)
|
|
857
|
+
elif output_type == 'DataFrame' or output_type == 'Pandas' or output_type == 'pd' or output_type == 'dataframe' or output_type == 'df':
|
|
858
|
+
return Sentinel2Collection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=prop_names)
|
|
847
859
|
else:
|
|
848
|
-
raise ValueError("output_type must be 'ImageCollection'
|
|
860
|
+
raise ValueError("Incorrect `output_type`. The `output_type` argument must be one of the following: 'ImageCollection', 'ee.ImageCollection', 'Sentinel2Collection', 'DataFrame', 'Pandas', 'pd', 'dataframe', or 'df'.")
|
|
861
|
+
|
|
862
|
+
@staticmethod
|
|
863
|
+
def add_month_property_fn(image):
|
|
864
|
+
"""
|
|
865
|
+
Adds a numeric 'month' property to the image based on its date.
|
|
866
|
+
|
|
867
|
+
Args:
|
|
868
|
+
image (ee.Image): Input image.
|
|
869
|
+
|
|
870
|
+
Returns:
|
|
871
|
+
ee.Image: Image with the 'month' property added.
|
|
872
|
+
"""
|
|
873
|
+
return image.set('month', image.date().get('month'))
|
|
874
|
+
|
|
875
|
+
@property
|
|
876
|
+
def add_month_property(self):
|
|
877
|
+
"""
|
|
878
|
+
Adds a numeric 'month' property to each image in the collection.
|
|
879
|
+
|
|
880
|
+
Returns:
|
|
881
|
+
Sentinel2Collection: A Sentinel2Collection image collection with the 'month' property added to each image.
|
|
882
|
+
"""
|
|
883
|
+
col = self.collection.map(Sentinel2Collection.add_month_property_fn)
|
|
884
|
+
return Sentinel2Collection(collection=col)
|
|
885
|
+
|
|
849
886
|
|
|
850
887
|
def combine(self, other):
|
|
851
888
|
"""
|
|
@@ -974,6 +1011,8 @@ class Sentinel2Collection:
|
|
|
974
1011
|
# Ensure property_names is a list for consistent processing
|
|
975
1012
|
if isinstance(property_names, str):
|
|
976
1013
|
property_names = [property_names]
|
|
1014
|
+
elif isinstance(property_names, list):
|
|
1015
|
+
property_names = property_names
|
|
977
1016
|
|
|
978
1017
|
# Ensure properties are included without duplication, including 'Date_Filter'
|
|
979
1018
|
all_properties_to_fetch = list(set(['Date_Filter'] + property_names))
|
|
@@ -1209,6 +1248,338 @@ class Sentinel2Collection:
|
|
|
1209
1248
|
pass
|
|
1210
1249
|
|
|
1211
1250
|
return self._monthly_median
|
|
1251
|
+
|
|
1252
|
+
@property
|
|
1253
|
+
def monthly_mean_collection(self):
|
|
1254
|
+
"""Creates a monthly mean composite from a Sentinel2Collection image collection.
|
|
1255
|
+
|
|
1256
|
+
This function computes the mean for each
|
|
1257
|
+
month within the collection's date range, for each band in the collection. It automatically handles the full
|
|
1258
|
+
temporal extent of the input collection.
|
|
1259
|
+
|
|
1260
|
+
The resulting images have a 'system:time_start' property set to the
|
|
1261
|
+
first day of each month and an 'image_count' property indicating how
|
|
1262
|
+
many images were used in the composite. Months with no images are
|
|
1263
|
+
automatically excluded from the final collection.
|
|
1264
|
+
|
|
1265
|
+
NOTE: the day of month for the 'system:time_start' property is set to the earliest date of the first month observed and may not be the first day of the month.
|
|
1266
|
+
|
|
1267
|
+
Returns:
|
|
1268
|
+
Sentinel2Collection: A new Sentinel2Collection object with monthly mean composites.
|
|
1269
|
+
"""
|
|
1270
|
+
if self._monthly_mean is None:
|
|
1271
|
+
collection = self.collection
|
|
1272
|
+
target_proj = collection.first().projection()
|
|
1273
|
+
# Get the start and end dates of the entire collection.
|
|
1274
|
+
date_range = collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
|
|
1275
|
+
original_start_date = ee.Date(date_range.get('min'))
|
|
1276
|
+
end_date = ee.Date(date_range.get('max'))
|
|
1277
|
+
|
|
1278
|
+
start_year = original_start_date.get('year')
|
|
1279
|
+
start_month = original_start_date.get('month')
|
|
1280
|
+
start_date = ee.Date.fromYMD(start_year, start_month, 1)
|
|
1281
|
+
|
|
1282
|
+
# Calculate the total number of months in the date range.
|
|
1283
|
+
# The .round() is important for ensuring we get an integer.
|
|
1284
|
+
num_months = end_date.difference(start_date, 'month').round()
|
|
1285
|
+
|
|
1286
|
+
# Generate a list of starting dates for each month.
|
|
1287
|
+
# This uses a sequence and advances the start date by 'i' months.
|
|
1288
|
+
def get_month_start(i):
|
|
1289
|
+
return start_date.advance(i, 'month')
|
|
1290
|
+
|
|
1291
|
+
month_starts = ee.List.sequence(0, num_months).map(get_month_start)
|
|
1292
|
+
|
|
1293
|
+
# Define a function to map over the list of month start dates.
|
|
1294
|
+
def create_monthly_composite(date):
|
|
1295
|
+
# Cast the input to an ee.Date object.
|
|
1296
|
+
start_of_month = ee.Date(date)
|
|
1297
|
+
# The end date is exclusive, so we advance by 1 month.
|
|
1298
|
+
end_of_month = start_of_month.advance(1, 'month')
|
|
1299
|
+
|
|
1300
|
+
# Filter the original collection to get images for the current month.
|
|
1301
|
+
monthly_subset = collection.filterDate(start_of_month, end_of_month)
|
|
1302
|
+
|
|
1303
|
+
# Count the number of images in the monthly subset.
|
|
1304
|
+
image_count = monthly_subset.size()
|
|
1305
|
+
|
|
1306
|
+
# Compute the mean. This is robust to outliers like clouds.
|
|
1307
|
+
monthly_mean = monthly_subset.mean()
|
|
1308
|
+
|
|
1309
|
+
# Set essential properties on the resulting composite image.
|
|
1310
|
+
# The timestamp is crucial for time-series analysis and charting.
|
|
1311
|
+
# The image_count is useful metadata for quality assessment.
|
|
1312
|
+
return monthly_mean.set({
|
|
1313
|
+
'system:time_start': start_of_month.millis(),
|
|
1314
|
+
'month': start_of_month.get('month'),
|
|
1315
|
+
'year': start_of_month.get('year'),
|
|
1316
|
+
'Date_Filter': start_of_month.format('YYYY-MM-dd'),
|
|
1317
|
+
'image_count': image_count
|
|
1318
|
+
}).reproject(target_proj)
|
|
1319
|
+
|
|
1320
|
+
# Map the composite function over the list of month start dates.
|
|
1321
|
+
monthly_composites_list = month_starts.map(create_monthly_composite)
|
|
1322
|
+
|
|
1323
|
+
# Convert the list of images into an ee.ImageCollection.
|
|
1324
|
+
monthly_collection = ee.ImageCollection.fromImages(monthly_composites_list)
|
|
1325
|
+
|
|
1326
|
+
# Filter out any composites that were created from zero images.
|
|
1327
|
+
# This prevents empty/masked images from being in the final collection.
|
|
1328
|
+
final_collection = Sentinel2Collection(collection=monthly_collection.filter(ee.Filter.gt('image_count', 0)))
|
|
1329
|
+
self._monthly_mean = final_collection
|
|
1330
|
+
else:
|
|
1331
|
+
pass
|
|
1332
|
+
|
|
1333
|
+
return self._monthly_mean
|
|
1334
|
+
|
|
1335
|
+
@property
|
|
1336
|
+
def monthly_sum_collection(self):
|
|
1337
|
+
"""Creates a monthly sum composite from a Sentinel2Collection image collection.
|
|
1338
|
+
|
|
1339
|
+
This function computes the sum for each
|
|
1340
|
+
month within the collection's date range, for each band in the collection. It automatically handles the full
|
|
1341
|
+
temporal extent of the input collection.
|
|
1342
|
+
|
|
1343
|
+
The resulting images have a 'system:time_start' property set to the
|
|
1344
|
+
first day of each month and an 'image_count' property indicating how
|
|
1345
|
+
many images were used in the composite. Months with no images are
|
|
1346
|
+
automatically excluded from the final collection.
|
|
1347
|
+
|
|
1348
|
+
NOTE: the day of month for the 'system:time_start' property is set to the earliest date of the first month observed and may not be the first day of the month.
|
|
1349
|
+
|
|
1350
|
+
Returns:
|
|
1351
|
+
Sentinel2Collection: A new Sentinel2Collection object with monthly sum composites.
|
|
1352
|
+
"""
|
|
1353
|
+
if self._monthly_sum is None:
|
|
1354
|
+
collection = self.collection
|
|
1355
|
+
target_proj = collection.first().projection()
|
|
1356
|
+
# Get the start and end dates of the entire collection.
|
|
1357
|
+
date_range = collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
|
|
1358
|
+
original_start_date = ee.Date(date_range.get('min'))
|
|
1359
|
+
end_date = ee.Date(date_range.get('max'))
|
|
1360
|
+
|
|
1361
|
+
start_year = original_start_date.get('year')
|
|
1362
|
+
start_month = original_start_date.get('month')
|
|
1363
|
+
start_date = ee.Date.fromYMD(start_year, start_month, 1)
|
|
1364
|
+
|
|
1365
|
+
# Calculate the total number of months in the date range.
|
|
1366
|
+
# The .round() is important for ensuring we get an integer.
|
|
1367
|
+
num_months = end_date.difference(start_date, 'month').round()
|
|
1368
|
+
|
|
1369
|
+
# Generate a list of starting dates for each month.
|
|
1370
|
+
# This uses a sequence and advances the start date by 'i' months.
|
|
1371
|
+
def get_month_start(i):
|
|
1372
|
+
return start_date.advance(i, 'month')
|
|
1373
|
+
|
|
1374
|
+
month_starts = ee.List.sequence(0, num_months).map(get_month_start)
|
|
1375
|
+
|
|
1376
|
+
# Define a function to map over the list of month start dates.
|
|
1377
|
+
def create_monthly_composite(date):
|
|
1378
|
+
# Cast the input to an ee.Date object.
|
|
1379
|
+
start_of_month = ee.Date(date)
|
|
1380
|
+
# The end date is exclusive, so we advance by 1 month.
|
|
1381
|
+
end_of_month = start_of_month.advance(1, 'month')
|
|
1382
|
+
|
|
1383
|
+
# Filter the original collection to get images for the current month.
|
|
1384
|
+
monthly_subset = collection.filterDate(start_of_month, end_of_month)
|
|
1385
|
+
|
|
1386
|
+
# Count the number of images in the monthly subset.
|
|
1387
|
+
image_count = monthly_subset.size()
|
|
1388
|
+
|
|
1389
|
+
# Compute the sum. This is robust to outliers like clouds.
|
|
1390
|
+
monthly_sum = monthly_subset.sum()
|
|
1391
|
+
|
|
1392
|
+
# Set essential properties on the resulting composite image.
|
|
1393
|
+
# The timestamp is crucial for time-series analysis and charting.
|
|
1394
|
+
# The image_count is useful metadata for quality assessment.
|
|
1395
|
+
return monthly_sum.set({
|
|
1396
|
+
'system:time_start': start_of_month.millis(),
|
|
1397
|
+
'month': start_of_month.get('month'),
|
|
1398
|
+
'year': start_of_month.get('year'),
|
|
1399
|
+
'Date_Filter': start_of_month.format('YYYY-MM-dd'),
|
|
1400
|
+
'image_count': image_count
|
|
1401
|
+
}).reproject(target_proj)
|
|
1402
|
+
|
|
1403
|
+
# Map the composite function over the list of month start dates.
|
|
1404
|
+
monthly_composites_list = month_starts.map(create_monthly_composite)
|
|
1405
|
+
|
|
1406
|
+
# Convert the list of images into an ee.ImageCollection.
|
|
1407
|
+
monthly_collection = ee.ImageCollection.fromImages(monthly_composites_list)
|
|
1408
|
+
|
|
1409
|
+
# Filter out any composites that were created from zero images.
|
|
1410
|
+
# This prevents empty/masked images from being in the final collection.
|
|
1411
|
+
final_collection = Sentinel2Collection(collection=monthly_collection.filter(ee.Filter.gt('image_count', 0)))
|
|
1412
|
+
self._monthly_sum = final_collection
|
|
1413
|
+
else:
|
|
1414
|
+
pass
|
|
1415
|
+
|
|
1416
|
+
return self._monthly_sum
|
|
1417
|
+
|
|
1418
|
+
@property
|
|
1419
|
+
def monthly_max_collection(self):
|
|
1420
|
+
"""Creates a monthly max composite from a Sentinel2Collection image collection.
|
|
1421
|
+
|
|
1422
|
+
This function computes the max for each
|
|
1423
|
+
month within the collection's date range, for each band in the collection. It automatically handles the full
|
|
1424
|
+
temporal extent of the input collection.
|
|
1425
|
+
|
|
1426
|
+
The resulting images have a 'system:time_start' property set to the
|
|
1427
|
+
first day of each month and an 'image_count' property indicating how
|
|
1428
|
+
many images were used in the composite. Months with no images are
|
|
1429
|
+
automatically excluded from the final collection.
|
|
1430
|
+
|
|
1431
|
+
NOTE: the day of month for the 'system:time_start' property is set to the earliest date of the first month observed and may not be the first day of the month.
|
|
1432
|
+
|
|
1433
|
+
Returns:
|
|
1434
|
+
Sentinel2Collection: A new Sentinel2Collection object with monthly max composites.
|
|
1435
|
+
"""
|
|
1436
|
+
if self._monthly_max is None:
|
|
1437
|
+
collection = self.collection
|
|
1438
|
+
target_proj = collection.first().projection()
|
|
1439
|
+
# Get the start and end dates of the entire collection.
|
|
1440
|
+
date_range = collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
|
|
1441
|
+
original_start_date = ee.Date(date_range.get('min'))
|
|
1442
|
+
end_date = ee.Date(date_range.get('max'))
|
|
1443
|
+
|
|
1444
|
+
start_year = original_start_date.get('year')
|
|
1445
|
+
start_month = original_start_date.get('month')
|
|
1446
|
+
start_date = ee.Date.fromYMD(start_year, start_month, 1)
|
|
1447
|
+
|
|
1448
|
+
# Calculate the total number of months in the date range.
|
|
1449
|
+
# The .round() is important for ensuring we get an integer.
|
|
1450
|
+
num_months = end_date.difference(start_date, 'month').round()
|
|
1451
|
+
|
|
1452
|
+
# Generate a list of starting dates for each month.
|
|
1453
|
+
# This uses a sequence and advances the start date by 'i' months.
|
|
1454
|
+
def get_month_start(i):
|
|
1455
|
+
return start_date.advance(i, 'month')
|
|
1456
|
+
|
|
1457
|
+
month_starts = ee.List.sequence(0, num_months).map(get_month_start)
|
|
1458
|
+
|
|
1459
|
+
# Define a function to map over the list of month start dates.
|
|
1460
|
+
def create_monthly_composite(date):
|
|
1461
|
+
# Cast the input to an ee.Date object.
|
|
1462
|
+
start_of_month = ee.Date(date)
|
|
1463
|
+
# The end date is exclusive, so we advance by 1 month.
|
|
1464
|
+
end_of_month = start_of_month.advance(1, 'month')
|
|
1465
|
+
|
|
1466
|
+
# Filter the original collection to get images for the current month.
|
|
1467
|
+
monthly_subset = collection.filterDate(start_of_month, end_of_month)
|
|
1468
|
+
|
|
1469
|
+
# Count the number of images in the monthly subset.
|
|
1470
|
+
image_count = monthly_subset.size()
|
|
1471
|
+
|
|
1472
|
+
# Compute the max. This is robust to outliers like clouds.
|
|
1473
|
+
monthly_max = monthly_subset.max()
|
|
1474
|
+
|
|
1475
|
+
# Set essential properties on the resulting composite image.
|
|
1476
|
+
# The timestamp is crucial for time-series analysis and charting.
|
|
1477
|
+
# The image_count is useful metadata for quality assessment.
|
|
1478
|
+
return monthly_max.set({
|
|
1479
|
+
'system:time_start': start_of_month.millis(),
|
|
1480
|
+
'month': start_of_month.get('month'),
|
|
1481
|
+
'year': start_of_month.get('year'),
|
|
1482
|
+
'Date_Filter': start_of_month.format('YYYY-MM-dd'),
|
|
1483
|
+
'image_count': image_count
|
|
1484
|
+
}).reproject(target_proj)
|
|
1485
|
+
|
|
1486
|
+
# Map the composite function over the list of month start dates.
|
|
1487
|
+
monthly_composites_list = month_starts.map(create_monthly_composite)
|
|
1488
|
+
|
|
1489
|
+
# Convert the list of images into an ee.ImageCollection.
|
|
1490
|
+
monthly_collection = ee.ImageCollection.fromImages(monthly_composites_list)
|
|
1491
|
+
|
|
1492
|
+
# Filter out any composites that were created from zero images.
|
|
1493
|
+
# This prevents empty/masked images from being in the final collection.
|
|
1494
|
+
final_collection = Sentinel2Collection(collection=monthly_collection.filter(ee.Filter.gt('image_count', 0)))
|
|
1495
|
+
self._monthly_max = final_collection
|
|
1496
|
+
else:
|
|
1497
|
+
pass
|
|
1498
|
+
|
|
1499
|
+
return self._monthly_max
|
|
1500
|
+
|
|
1501
|
+
@property
|
|
1502
|
+
def monthly_min_collection(self):
|
|
1503
|
+
"""Creates a monthly min composite from a Sentinel2Collection image collection.
|
|
1504
|
+
|
|
1505
|
+
This function computes the min for each
|
|
1506
|
+
month within the collection's date range, for each band in the collection. It automatically handles the full
|
|
1507
|
+
temporal extent of the input collection.
|
|
1508
|
+
|
|
1509
|
+
The resulting images have a 'system:time_start' property set to the
|
|
1510
|
+
first day of each month and an 'image_count' property indicating how
|
|
1511
|
+
many images were used in the composite. Months with no images are
|
|
1512
|
+
automatically excluded from the final collection.
|
|
1513
|
+
|
|
1514
|
+
NOTE: the day of month for the 'system:time_start' property is set to the earliest date of the first month observed and may not be the first day of the month.
|
|
1515
|
+
|
|
1516
|
+
Returns:
|
|
1517
|
+
Sentinel2Collection: A new Sentinel2Collection object with monthly min composites.
|
|
1518
|
+
"""
|
|
1519
|
+
if self._monthly_min is None:
|
|
1520
|
+
collection = self.collection
|
|
1521
|
+
target_proj = collection.first().projection()
|
|
1522
|
+
# Get the start and end dates of the entire collection.
|
|
1523
|
+
date_range = collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
|
|
1524
|
+
original_start_date = ee.Date(date_range.get('min'))
|
|
1525
|
+
end_date = ee.Date(date_range.get('max'))
|
|
1526
|
+
|
|
1527
|
+
start_year = original_start_date.get('year')
|
|
1528
|
+
start_month = original_start_date.get('month')
|
|
1529
|
+
start_date = ee.Date.fromYMD(start_year, start_month, 1)
|
|
1530
|
+
|
|
1531
|
+
# Calculate the total number of months in the date range.
|
|
1532
|
+
# The .round() is important for ensuring we get an integer.
|
|
1533
|
+
num_months = end_date.difference(start_date, 'month').round()
|
|
1534
|
+
|
|
1535
|
+
# Generate a list of starting dates for each month.
|
|
1536
|
+
# This uses a sequence and advances the start date by 'i' months.
|
|
1537
|
+
def get_month_start(i):
|
|
1538
|
+
return start_date.advance(i, 'month')
|
|
1539
|
+
|
|
1540
|
+
month_starts = ee.List.sequence(0, num_months).map(get_month_start)
|
|
1541
|
+
|
|
1542
|
+
# Define a function to map over the list of month start dates.
|
|
1543
|
+
def create_monthly_composite(date):
|
|
1544
|
+
# Cast the input to an ee.Date object.
|
|
1545
|
+
start_of_month = ee.Date(date)
|
|
1546
|
+
# The end date is exclusive, so we advance by 1 month.
|
|
1547
|
+
end_of_month = start_of_month.advance(1, 'month')
|
|
1548
|
+
|
|
1549
|
+
# Filter the original collection to get images for the current month.
|
|
1550
|
+
monthly_subset = collection.filterDate(start_of_month, end_of_month)
|
|
1551
|
+
|
|
1552
|
+
# Count the number of images in the monthly subset.
|
|
1553
|
+
image_count = monthly_subset.size()
|
|
1554
|
+
|
|
1555
|
+
# Compute the min. This is robust to outliers like clouds.
|
|
1556
|
+
monthly_min = monthly_subset.min()
|
|
1557
|
+
|
|
1558
|
+
# Set essential properties on the resulting composite image.
|
|
1559
|
+
# The timestamp is crucial for time-series analysis and charting.
|
|
1560
|
+
# The image_count is useful metadata for quality assessment.
|
|
1561
|
+
return monthly_min.set({
|
|
1562
|
+
'system:time_start': start_of_month.millis(),
|
|
1563
|
+
'month': start_of_month.get('month'),
|
|
1564
|
+
'year': start_of_month.get('year'),
|
|
1565
|
+
'Date_Filter': start_of_month.format('YYYY-MM-dd'),
|
|
1566
|
+
'image_count': image_count
|
|
1567
|
+
}).reproject(target_proj)
|
|
1568
|
+
|
|
1569
|
+
# Map the composite function over the list of month start dates.
|
|
1570
|
+
monthly_composites_list = month_starts.map(create_monthly_composite)
|
|
1571
|
+
|
|
1572
|
+
# Convert the list of images into an ee.ImageCollection.
|
|
1573
|
+
monthly_collection = ee.ImageCollection.fromImages(monthly_composites_list)
|
|
1574
|
+
|
|
1575
|
+
# Filter out any composites that were created from zero images.
|
|
1576
|
+
# This prevents empty/masked images from being in the final collection.
|
|
1577
|
+
final_collection = Sentinel2Collection(collection=monthly_collection.filter(ee.Filter.gt('image_count', 0)))
|
|
1578
|
+
self._monthly_min = final_collection
|
|
1579
|
+
else:
|
|
1580
|
+
pass
|
|
1581
|
+
|
|
1582
|
+
return self._monthly_min
|
|
1212
1583
|
|
|
1213
1584
|
@property
|
|
1214
1585
|
def mean(self):
|
|
@@ -1868,6 +2239,30 @@ class Sentinel2Collection:
|
|
|
1868
2239
|
)
|
|
1869
2240
|
return Sentinel2Collection(collection=col)
|
|
1870
2241
|
|
|
2242
|
+
def remove_duplicate_dates(self, sort_by='system:time_start', ascending=True):
|
|
2243
|
+
"""
|
|
2244
|
+
Removes duplicate images that share the same date, keeping only the first one encountered.
|
|
2245
|
+
Useful for handling duplicate Sentinel-2A/2B acquisitions or overlapping path/rows.
|
|
2246
|
+
|
|
2247
|
+
Args:
|
|
2248
|
+
sort_by (str): Property to sort by before filtering distinct dates.
|
|
2249
|
+
Defaults to 'system:time_start'.
|
|
2250
|
+
Recommended to use 'CLOUDY_PIXEL_PERCENTAGE' (ascending=True) to keep the clearest image.
|
|
2251
|
+
ascending (bool): Sort order. Defaults to True.
|
|
2252
|
+
|
|
2253
|
+
Returns:
|
|
2254
|
+
Sentinel2Collection: A new Sentinel2Collection object with distinct dates.
|
|
2255
|
+
"""
|
|
2256
|
+
if sort_by not in ['system:time_start', 'CLOUDY_PIXEL_PERCENTAGE']:
|
|
2257
|
+
raise ValueError(f"The provided `sort_by` argument is invalid: {sort_by}. The `sort_by` argument must be either 'system:time_start' or 'CLOUDY_PIXEL_PERCENTAGE'.")
|
|
2258
|
+
# Sort the collection to ensure the "best" image comes first (e.g. least cloudy)
|
|
2259
|
+
sorted_col = self.collection.sort(sort_by, ascending)
|
|
2260
|
+
|
|
2261
|
+
# distinct() retains the first image for each unique value of the specified property
|
|
2262
|
+
distinct_col = sorted_col.distinct('Date_Filter')
|
|
2263
|
+
|
|
2264
|
+
return Sentinel2Collection(collection=distinct_col)
|
|
2265
|
+
|
|
1871
2266
|
@property
|
|
1872
2267
|
def masked_water_collection(self):
|
|
1873
2268
|
"""
|
|
@@ -2068,26 +2463,27 @@ class Sentinel2Collection:
|
|
|
2068
2463
|
raise ValueError("Threshold must be specified for binary masking.")
|
|
2069
2464
|
|
|
2070
2465
|
if classify_above_threshold:
|
|
2071
|
-
if mask_zeros:
|
|
2466
|
+
if mask_zeros is True:
|
|
2072
2467
|
col = self.collection.map(
|
|
2073
|
-
lambda image: image.select(band_name).gte(threshold).rename(band_name).
|
|
2468
|
+
lambda image: image.select(band_name).gte(threshold).rename(band_name).selfMask().copyProperties(image)
|
|
2074
2469
|
)
|
|
2075
2470
|
else:
|
|
2076
2471
|
col = self.collection.map(
|
|
2077
2472
|
lambda image: image.select(band_name).gte(threshold).rename(band_name).copyProperties(image)
|
|
2078
2473
|
)
|
|
2079
2474
|
else:
|
|
2080
|
-
if mask_zeros:
|
|
2475
|
+
if mask_zeros is True:
|
|
2081
2476
|
col = self.collection.map(
|
|
2082
|
-
lambda image: image.select(band_name).lte(threshold).rename(band_name).
|
|
2477
|
+
lambda image: image.select(band_name).lte(threshold).rename(band_name).selfMask().copyProperties(image)
|
|
2083
2478
|
)
|
|
2084
2479
|
else:
|
|
2085
2480
|
col = self.collection.map(
|
|
2086
2481
|
lambda image: image.select(band_name).lte(threshold).rename(band_name).copyProperties(image)
|
|
2087
2482
|
)
|
|
2483
|
+
|
|
2088
2484
|
return Sentinel2Collection(collection=col)
|
|
2089
2485
|
|
|
2090
|
-
def anomaly(self, geometry, band_name=None, anomaly_band_name=None, replace=True):
|
|
2486
|
+
def anomaly(self, geometry, band_name=None, anomaly_band_name=None, replace=True, scale=10):
|
|
2091
2487
|
"""
|
|
2092
2488
|
Calculates the anomaly of each image in a collection compared to the mean of each image.
|
|
2093
2489
|
|
|
@@ -2101,6 +2497,7 @@ class Sentinel2Collection:
|
|
|
2101
2497
|
band_name (str, optional): A string representing the band name to be used for the output anomaly image. If not provided, the band name of the first band of the input image will be used.
|
|
2102
2498
|
anomaly_band_name (str, optional): A string representing the band name to be used for the output anomaly image. If not provided, the band name of the first band of the input image will be used.
|
|
2103
2499
|
replace (bool, optional): A boolean indicating whether to replace the original band with the anomaly band. If True, the output image will only contain the anomaly band. If False, the output image will retain all original bands and add the anomaly band. Default is True.
|
|
2500
|
+
scale (int, optional): The scale (in meters) to use for the image reduction. Default is 10 meters.
|
|
2104
2501
|
|
|
2105
2502
|
Returns:
|
|
2106
2503
|
Sentinel2Collection: A Sentinel2Collection where each image represents the anomaly (deviation from
|
|
@@ -2119,7 +2516,7 @@ class Sentinel2Collection:
|
|
|
2119
2516
|
else:
|
|
2120
2517
|
band_name = band_names.get(0).getInfo()
|
|
2121
2518
|
|
|
2122
|
-
col = self.collection.map(lambda image: Sentinel2Collection.anomaly_fn(image, geometry=geometry, band_name=band_name, anomaly_band_name=anomaly_band_name, replace=replace))
|
|
2519
|
+
col = self.collection.map(lambda image: Sentinel2Collection.anomaly_fn(image, geometry=geometry, band_name=band_name, anomaly_band_name=anomaly_band_name, replace=replace, scale=scale))
|
|
2123
2520
|
return Sentinel2Collection(collection=col)
|
|
2124
2521
|
|
|
2125
2522
|
def mask_via_band(self, band_to_mask, band_for_mask, threshold=-1, mask_above=True, add_band_to_original_image=False):
|
|
@@ -2892,8 +3289,9 @@ class Sentinel2Collection:
|
|
|
2892
3289
|
def iterate_zonal_stats(
|
|
2893
3290
|
self,
|
|
2894
3291
|
geometries,
|
|
3292
|
+
band=None,
|
|
2895
3293
|
reducer_type="mean",
|
|
2896
|
-
scale=
|
|
3294
|
+
scale=30,
|
|
2897
3295
|
geometry_names=None,
|
|
2898
3296
|
buffer_size=1,
|
|
2899
3297
|
tileScale=1,
|
|
@@ -2905,10 +3303,13 @@ class Sentinel2Collection:
|
|
|
2905
3303
|
When coordinates are provided, a radial buffer is applied around each coordinate to extract the statistics, where the size of the buffer is determined by the buffer_size argument (defaults to 1 meter).
|
|
2906
3304
|
The function returns a pandas DataFrame with the statistics for each coordinate and date, or optionally exports the data to a table in .csv format.
|
|
2907
3305
|
|
|
3306
|
+
NOTE: The expected input is a singleband image. If a multiband image is provided, the first band will be used for zonal statistic extraction, unless `band` is specified.
|
|
3307
|
+
|
|
2908
3308
|
Args:
|
|
2909
3309
|
geometries (ee.Geometry, ee.Feature, ee.FeatureCollection, list, or tuple): Input geometries for which to extract statistics. Can be a single ee.Geometry, an ee.Feature, an ee.FeatureCollection, a list of (lon, lat) tuples, or a list of ee.Geometry objects. Be careful to NOT provide coordinates as (lat, lon)!
|
|
3310
|
+
band (str, optional): The name of the band to use for statistics. If None, the first band is used. Defaults to None.
|
|
2910
3311
|
reducer_type (str, optional): The ee.Reducer to use, e.g., 'mean', 'median', 'max', 'sum'. Defaults to 'mean'. Any ee.Reducer method can be used.
|
|
2911
|
-
scale (int, optional): Pixel scale in meters for the reduction. Defaults to
|
|
3312
|
+
scale (int, optional): Pixel scale in meters for the reduction. Defaults to 30.
|
|
2912
3313
|
geometry_names (list, optional): A list of string names for the geometries. If provided, must match the number of geometries. Defaults to None.
|
|
2913
3314
|
buffer_size (int, optional): Radial buffer in meters around coordinates. Defaults to 1.
|
|
2914
3315
|
tileScale (int, optional): A scaling factor to reduce aggregation tile size. Defaults to 1.
|
|
@@ -2923,18 +3324,30 @@ class Sentinel2Collection:
|
|
|
2923
3324
|
ValueError: If input parameters are invalid.
|
|
2924
3325
|
TypeError: If geometries input type is unsupported.
|
|
2925
3326
|
"""
|
|
3327
|
+
# Create a local reference to the collection object to allow for modifications (like band selection) without altering the original instance
|
|
2926
3328
|
img_collection_obj = self
|
|
2927
|
-
|
|
3329
|
+
|
|
3330
|
+
# If a specific band is requested, select only that band
|
|
3331
|
+
if band:
|
|
3332
|
+
img_collection_obj = Sentinel2Collection(collection=img_collection_obj.collection.select(band))
|
|
3333
|
+
else:
|
|
3334
|
+
# If no band is specified, default to using the first band of the first image in the collection
|
|
3335
|
+
first_image = img_collection_obj.image_grab(0)
|
|
3336
|
+
first_band = first_image.bandNames().get(0)
|
|
3337
|
+
img_collection_obj = Sentinel2Collection(collection=img_collection_obj.collection.select([first_band]))
|
|
3338
|
+
|
|
3339
|
+
# If a list of dates is provided, filter the collection to include only images matching those dates
|
|
2928
3340
|
if dates:
|
|
2929
3341
|
img_collection_obj = Sentinel2Collection(
|
|
2930
3342
|
collection=self.collection.filter(ee.Filter.inList('Date_Filter', dates))
|
|
2931
3343
|
)
|
|
2932
3344
|
|
|
2933
|
-
# Initialize variables
|
|
3345
|
+
# Initialize variables to hold the standardized feature collection and coordinates
|
|
2934
3346
|
features = None
|
|
2935
3347
|
validated_coordinates = []
|
|
2936
3348
|
|
|
2937
|
-
#
|
|
3349
|
+
# Define a helper function to ensure every feature has a standardized 'geo_name' property
|
|
3350
|
+
# This handles features that might have different existing name properties or none at all
|
|
2938
3351
|
def set_standard_name(feature):
|
|
2939
3352
|
has_geo_name = feature.get('geo_name')
|
|
2940
3353
|
has_name = feature.get('name')
|
|
@@ -2945,33 +3358,38 @@ class Sentinel2Collection:
|
|
|
2945
3358
|
ee.Algorithms.If(has_index, has_index, 'unnamed_geometry')))
|
|
2946
3359
|
return feature.set({'geo_name': new_name})
|
|
2947
3360
|
|
|
3361
|
+
# Handle input: FeatureCollection or single Feature
|
|
2948
3362
|
if isinstance(geometries, (ee.FeatureCollection, ee.Feature)):
|
|
2949
3363
|
features = ee.FeatureCollection(geometries)
|
|
2950
3364
|
if geometry_names:
|
|
2951
3365
|
print("Warning: 'geometry_names' are ignored when the input is an ee.Feature or ee.FeatureCollection.")
|
|
2952
3366
|
|
|
3367
|
+
# Handle input: Single ee.Geometry
|
|
2953
3368
|
elif isinstance(geometries, ee.Geometry):
|
|
2954
3369
|
name = geometry_names[0] if (geometry_names and geometry_names[0]) else 'unnamed_geometry'
|
|
2955
3370
|
features = ee.FeatureCollection([ee.Feature(geometries).set('geo_name', name)])
|
|
2956
3371
|
|
|
3372
|
+
# Handle input: List (could be coordinates or ee.Geometry objects)
|
|
2957
3373
|
elif isinstance(geometries, list):
|
|
2958
3374
|
if not geometries: # Handle empty list case
|
|
2959
3375
|
raise ValueError("'geometries' list cannot be empty.")
|
|
2960
3376
|
|
|
2961
|
-
# Case: List of coordinates
|
|
3377
|
+
# Case: List of tuples (coordinates)
|
|
2962
3378
|
if all(isinstance(i, tuple) for i in geometries):
|
|
2963
3379
|
validated_coordinates = geometries
|
|
3380
|
+
# Generate default names if none provided
|
|
2964
3381
|
if geometry_names is None:
|
|
2965
3382
|
geometry_names = [f"Location_{i+1}" for i in range(len(validated_coordinates))]
|
|
2966
3383
|
elif len(geometry_names) != len(validated_coordinates):
|
|
2967
3384
|
raise ValueError("geometry_names must have the same length as the coordinates list.")
|
|
3385
|
+
# Create features with buffers around the coordinates
|
|
2968
3386
|
points = [
|
|
2969
3387
|
ee.Feature(ee.Geometry.Point(coord).buffer(buffer_size), {'geo_name': str(name)})
|
|
2970
3388
|
for coord, name in zip(validated_coordinates, geometry_names)
|
|
2971
3389
|
]
|
|
2972
3390
|
features = ee.FeatureCollection(points)
|
|
2973
3391
|
|
|
2974
|
-
# Case: List of
|
|
3392
|
+
# Case: List of ee.Geometry objects
|
|
2975
3393
|
elif all(isinstance(i, ee.Geometry) for i in geometries):
|
|
2976
3394
|
if geometry_names is None:
|
|
2977
3395
|
geometry_names = [f"Geometry_{i+1}" for i in range(len(geometries))]
|
|
@@ -2986,6 +3404,7 @@ class Sentinel2Collection:
|
|
|
2986
3404
|
else:
|
|
2987
3405
|
raise TypeError("Input list must be a list of (lon, lat) tuples OR a list of ee.Geometry objects.")
|
|
2988
3406
|
|
|
3407
|
+
# Handle input: Single tuple (coordinate)
|
|
2989
3408
|
elif isinstance(geometries, tuple) and len(geometries) == 2:
|
|
2990
3409
|
name = geometry_names[0] if geometry_names else 'Location_1'
|
|
2991
3410
|
features = ee.FeatureCollection([
|
|
@@ -2994,39 +3413,48 @@ class Sentinel2Collection:
|
|
|
2994
3413
|
else:
|
|
2995
3414
|
raise TypeError("Unsupported type for 'geometries'.")
|
|
2996
3415
|
|
|
3416
|
+
# Apply the naming standardization to the created FeatureCollection
|
|
2997
3417
|
features = features.map(set_standard_name)
|
|
2998
3418
|
|
|
3419
|
+
# Dynamically retrieve the Earth Engine reducer based on the string name provided
|
|
2999
3420
|
try:
|
|
3000
3421
|
reducer = getattr(ee.Reducer, reducer_type)()
|
|
3001
3422
|
except AttributeError:
|
|
3002
3423
|
raise ValueError(f"Unknown reducer_type: '{reducer_type}'.")
|
|
3003
3424
|
|
|
3425
|
+
# Define the function to map over the image collection
|
|
3004
3426
|
def calculate_stats_for_image(image):
|
|
3005
3427
|
image_date = image.get('Date_Filter')
|
|
3428
|
+
# Calculate statistics for all geometries in 'features' for this specific image
|
|
3006
3429
|
stats_fc = image.reduceRegions(
|
|
3007
3430
|
collection=features, reducer=reducer, scale=scale, tileScale=tileScale
|
|
3008
3431
|
)
|
|
3009
3432
|
|
|
3433
|
+
# Helper to ensure the result has the reducer property, even if masked
|
|
3434
|
+
# If the property is missing (e.g., all pixels masked), set it to a sentinel value (-9999)
|
|
3010
3435
|
def guarantee_reducer_property(f):
|
|
3011
3436
|
has_property = f.propertyNames().contains(reducer_type)
|
|
3012
3437
|
return ee.Algorithms.If(has_property, f, f.set(reducer_type, -9999))
|
|
3438
|
+
|
|
3439
|
+
# Apply the guarantee check
|
|
3013
3440
|
fixed_stats_fc = stats_fc.map(guarantee_reducer_property)
|
|
3014
3441
|
|
|
3442
|
+
# Attach the image date to every feature in the result so we know which image it came from
|
|
3015
3443
|
return fixed_stats_fc.map(lambda f: f.set('image_date', image_date))
|
|
3016
3444
|
|
|
3445
|
+
# Map the calculation over the image collection and flatten the resulting FeatureCollections into one
|
|
3017
3446
|
results_fc = ee.FeatureCollection(img_collection_obj.collection.map(calculate_stats_for_image)).flatten()
|
|
3447
|
+
|
|
3448
|
+
# Convert the Earth Engine FeatureCollection to a pandas DataFrame (client-side operation)
|
|
3018
3449
|
df = Sentinel2Collection.ee_to_df(results_fc, remove_geom=True)
|
|
3019
3450
|
|
|
3020
|
-
#
|
|
3451
|
+
# Check for empty results or missing columns
|
|
3021
3452
|
if df.empty:
|
|
3022
|
-
# print("No results found for the given parameters. Check if the geometries intersect with the images, if the dates filter is too restrictive, or if the provided bands are empty.")
|
|
3023
|
-
# return df
|
|
3024
3453
|
raise ValueError("No results found for the given parameters. Check if the geometries intersect with the images, if the dates filter is too restrictive, or if the provided bands are empty.")
|
|
3025
3454
|
if reducer_type not in df.columns:
|
|
3026
3455
|
print(f"Warning: Reducer '{reducer_type}' not found in results.")
|
|
3027
|
-
# return df
|
|
3028
3456
|
|
|
3029
|
-
#
|
|
3457
|
+
# Filter out the sentinel values (-9999) which indicate failed reductions/masked pixels
|
|
3030
3458
|
initial_rows = len(df)
|
|
3031
3459
|
df.dropna(subset=[reducer_type], inplace=True)
|
|
3032
3460
|
df = df[df[reducer_type] != -9999]
|
|
@@ -3034,9 +3462,18 @@ class Sentinel2Collection:
|
|
|
3034
3462
|
if dropped_rows > 0:
|
|
3035
3463
|
print(f"Warning: Discarded {dropped_rows} results due to failed reductions (e.g., no valid pixels in geometry).")
|
|
3036
3464
|
|
|
3037
|
-
#
|
|
3465
|
+
# Pivot the DataFrame so that each row represents a date and each column represents a geometry location
|
|
3038
3466
|
pivot_df = df.pivot(index='image_date', columns='geo_name', values=reducer_type)
|
|
3467
|
+
# Rename the column headers (geometry names) to include the reducer type
|
|
3468
|
+
pivot_df.columns = [f"{col}_{reducer_type}" for col in pivot_df.columns]
|
|
3469
|
+
# Rename the index axis to 'Date' so it is correctly labeled when moved to a column later
|
|
3039
3470
|
pivot_df.index.name = 'Date'
|
|
3471
|
+
# Remove the name of the columns axis (which defaults to 'geo_name') so it doesn't appear as a confusing label in the final output
|
|
3472
|
+
pivot_df.columns.name = None
|
|
3473
|
+
# Reset the index to move the 'Date' index into a regular column and create a standard numerical index (0, 1, 2...)
|
|
3474
|
+
pivot_df = pivot_df.reset_index(drop=False)
|
|
3475
|
+
|
|
3476
|
+
# If a file path is provided, save the resulting DataFrame to CSV
|
|
3040
3477
|
if file_path:
|
|
3041
3478
|
# Check if file_path ends with .csv and remove it if so for consistency
|
|
3042
3479
|
if file_path.endswith('.csv'):
|