RadGEEToolbox 1.7.0__py3-none-any.whl → 1.7.1__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 +119 -31
- RadGEEToolbox/GetPalette.py +136 -87
- RadGEEToolbox/LandsatCollection.py +461 -36
- RadGEEToolbox/Sentinel1Collection.py +603 -18
- RadGEEToolbox/Sentinel2Collection.py +464 -30
- RadGEEToolbox/VisParams.py +112 -194
- RadGEEToolbox/__init__.py +1 -1
- {radgeetoolbox-1.7.0.dist-info → radgeetoolbox-1.7.1.dist-info}/METADATA +8 -6
- radgeetoolbox-1.7.1.dist-info/RECORD +13 -0
- radgeetoolbox-1.7.0.dist-info/RECORD +0 -13
- {radgeetoolbox-1.7.0.dist-info → radgeetoolbox-1.7.1.dist-info}/WHEEL +0 -0
- {radgeetoolbox-1.7.0.dist-info → radgeetoolbox-1.7.1.dist-info}/licenses/LICENSE.txt +0 -0
- {radgeetoolbox-1.7.0.dist-info → radgeetoolbox-1.7.1.dist-info}/top_level.txt +0 -0
|
@@ -155,6 +155,10 @@ class LandsatCollection:
|
|
|
155
155
|
self._geometry_masked_out_collection = None
|
|
156
156
|
self._median = None
|
|
157
157
|
self._monthly_median = None
|
|
158
|
+
self._monthly_mean = None
|
|
159
|
+
self._monthly_max = None
|
|
160
|
+
self._monthly_min = None
|
|
161
|
+
self._monthly_sum = None
|
|
158
162
|
self._mean = None
|
|
159
163
|
self._max = None
|
|
160
164
|
self._min = None
|
|
@@ -705,7 +709,7 @@ class LandsatCollection:
|
|
|
705
709
|
return nbr
|
|
706
710
|
|
|
707
711
|
@staticmethod
|
|
708
|
-
def anomaly_fn(image, geometry, band_name=None, anomaly_band_name=None, replace=True):
|
|
712
|
+
def anomaly_fn(image, geometry, band_name=None, anomaly_band_name=None, replace=True, scale=30):
|
|
709
713
|
"""
|
|
710
714
|
Calculates the anomaly of a singleband image compared to the mean of the singleband image.
|
|
711
715
|
|
|
@@ -721,6 +725,7 @@ class LandsatCollection:
|
|
|
721
725
|
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.
|
|
722
726
|
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.
|
|
723
727
|
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.
|
|
728
|
+
scale (int, optional): The scale in meters to use for the reduction operation. Default is 30 meters.
|
|
724
729
|
|
|
725
730
|
Returns:
|
|
726
731
|
ee.Image: An ee.Image where each band represents the anomaly (deviation from
|
|
@@ -737,12 +742,16 @@ class LandsatCollection:
|
|
|
737
742
|
mean_image = image_to_process.reduceRegion(
|
|
738
743
|
reducer=ee.Reducer.mean(),
|
|
739
744
|
geometry=geometry,
|
|
740
|
-
scale=
|
|
745
|
+
scale=scale,
|
|
741
746
|
maxPixels=1e13
|
|
742
747
|
).toImage()
|
|
743
748
|
|
|
744
749
|
# Compute the anomaly by subtracting the mean image from the input image.
|
|
745
|
-
|
|
750
|
+
if scale == 30:
|
|
751
|
+
anomaly_image = image_to_process.subtract(mean_image)
|
|
752
|
+
else:
|
|
753
|
+
anomaly_image = image_to_process.reproject(crs=image_to_process.projection(), scale=scale).subtract(mean_image)
|
|
754
|
+
|
|
746
755
|
if anomaly_band_name is None:
|
|
747
756
|
if band_name:
|
|
748
757
|
anomaly_image = anomaly_image.rename(band_name)
|
|
@@ -753,9 +762,9 @@ class LandsatCollection:
|
|
|
753
762
|
anomaly_image = anomaly_image.rename(anomaly_band_name)
|
|
754
763
|
# return anomaly_image
|
|
755
764
|
if replace:
|
|
756
|
-
return anomaly_image.copyProperties(image)
|
|
765
|
+
return anomaly_image.copyProperties(image).set('system:time_start', image.get('system:time_start'))
|
|
757
766
|
else:
|
|
758
|
-
return image.addBands(anomaly_image, overwrite=True)
|
|
767
|
+
return image.addBands(anomaly_image, overwrite=True).copyProperties(image)
|
|
759
768
|
|
|
760
769
|
@staticmethod
|
|
761
770
|
def MaskWaterLandsat(image):
|
|
@@ -1092,7 +1101,7 @@ class LandsatCollection:
|
|
|
1092
1101
|
"downwelling": image.select("downwelling"),
|
|
1093
1102
|
},
|
|
1094
1103
|
).rename("LST")
|
|
1095
|
-
return image.addBands(LST).copyProperties(image) # Outputs temperature in C
|
|
1104
|
+
return image.addBands(LST).copyProperties(image).set('system:time_start', image.get('system:time_start')) # Outputs temperature in C
|
|
1096
1105
|
|
|
1097
1106
|
@staticmethod
|
|
1098
1107
|
def C_to_F_fn(LST_image):
|
|
@@ -1114,7 +1123,7 @@ class LandsatCollection:
|
|
|
1114
1123
|
|
|
1115
1124
|
# Preserve original properties from the input image.
|
|
1116
1125
|
# return fahrenheit_image.rename('LST_F').copyProperties(LST_image)
|
|
1117
|
-
return LST_image.addBands(fahrenheit_image.rename('LST_F'), overwrite=True).copyProperties(LST_image)
|
|
1126
|
+
return LST_image.addBands(fahrenheit_image.rename('LST_F'), overwrite=True).copyProperties(LST_image).set('system:time_start', LST_image.get('system:time_start'))
|
|
1118
1127
|
|
|
1119
1128
|
@staticmethod
|
|
1120
1129
|
def band_rename_fn(image, current_band_name, new_band_name):
|
|
@@ -1214,8 +1223,7 @@ class LandsatCollection:
|
|
|
1214
1223
|
return final_image
|
|
1215
1224
|
|
|
1216
1225
|
def PixelAreaSumCollection(
|
|
1217
|
-
self, band_name, geometry, threshold=-1, scale=30, maxPixels=1e12, output_type='ImageCollection', area_data_export_path=None
|
|
1218
|
-
):
|
|
1226
|
+
self, band_name, geometry, threshold=-1, scale=30, maxPixels=1e12, output_type='ImageCollection', area_data_export_path=None):
|
|
1219
1227
|
"""
|
|
1220
1228
|
Calculates the geodesic summation of area for pixels of interest (above a specific threshold)
|
|
1221
1229
|
within a geometry and stores the value as an image property (matching name of chosen band) for an entire
|
|
@@ -1225,11 +1233,11 @@ class LandsatCollection:
|
|
|
1225
1233
|
|
|
1226
1234
|
Args:
|
|
1227
1235
|
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.
|
|
1228
|
-
geometry (ee.Geometry): ee.Geometry object denoting area to clip to for area calculation
|
|
1236
|
+
geometry (ee.Geometry): ee.Geometry object denoting area to clip to for area calculation.
|
|
1229
1237
|
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.
|
|
1230
|
-
scale (int): integer scale of image resolution (meters) (defaults to 30)
|
|
1231
|
-
maxPixels (int): integer denoting maximum number of pixels for calculations
|
|
1232
|
-
output_type (str): 'ImageCollection' to return an ee.ImageCollection, 'LandsatCollection' to return a LandsatCollection object (defaults to 'ImageCollection')
|
|
1238
|
+
scale (int): integer scale of image resolution (meters) (defaults to 30).
|
|
1239
|
+
maxPixels (int): integer denoting maximum number of pixels for calculations.
|
|
1240
|
+
output_type (str): 'ImageCollection' or 'ee.ImageCollection' to return an ee.ImageCollection, 'LandsatCollection' to return a LandsatCollection object, or 'DataFrame', 'Pandas', 'pd', 'dataframe', 'df' to return a pandas DataFrame (defaults to 'ImageCollection').
|
|
1233
1241
|
area_data_export_path (str, optional): If provided, the function will save the resulting area data to a CSV file at the specified path.
|
|
1234
1242
|
|
|
1235
1243
|
Returns:
|
|
@@ -1254,15 +1262,65 @@ class LandsatCollection:
|
|
|
1254
1262
|
|
|
1255
1263
|
# If an export path is provided, the area data will be exported to a CSV file
|
|
1256
1264
|
if area_data_export_path:
|
|
1257
|
-
LandsatCollection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=band_name, file_path=area_data_export_path+'.csv')
|
|
1265
|
+
LandsatCollection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=[band_name], file_path=area_data_export_path+'.csv')
|
|
1258
1266
|
|
|
1259
1267
|
# Returning the result in the desired format based on output_type argument or raising an error for invalid input
|
|
1260
|
-
if output_type == 'ImageCollection':
|
|
1268
|
+
if output_type == 'ImageCollection' or output_type == 'ee.ImageCollection':
|
|
1261
1269
|
return self._PixelAreaSumCollection
|
|
1262
1270
|
elif output_type == 'LandsatCollection':
|
|
1263
1271
|
return LandsatCollection(collection=self._PixelAreaSumCollection)
|
|
1272
|
+
elif output_type == 'DataFrame' or output_type == 'Pandas' or output_type == 'pd' or output_type == 'dataframe' or output_type == 'df':
|
|
1273
|
+
return LandsatCollection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=[band_name])
|
|
1264
1274
|
else:
|
|
1265
|
-
raise ValueError("output_type must be 'ImageCollection'
|
|
1275
|
+
raise ValueError("Incorrect `output_type`. The `output_type` argument must be one of the following: 'ImageCollection', 'ee.ImageCollection', 'LandsatCollection', 'DataFrame', 'Pandas', 'pd', 'dataframe', or 'df'.")
|
|
1276
|
+
|
|
1277
|
+
@staticmethod
|
|
1278
|
+
def add_month_property_fn(image):
|
|
1279
|
+
"""
|
|
1280
|
+
Adds a numeric 'month' property to the image based on its date.
|
|
1281
|
+
|
|
1282
|
+
Args:
|
|
1283
|
+
image (ee.Image): Input image.
|
|
1284
|
+
|
|
1285
|
+
Returns:
|
|
1286
|
+
ee.Image: Image with the 'month' property added.
|
|
1287
|
+
"""
|
|
1288
|
+
return image.set('month', image.date().get('month'))
|
|
1289
|
+
|
|
1290
|
+
@property
|
|
1291
|
+
def add_month_property(self):
|
|
1292
|
+
"""
|
|
1293
|
+
Adds a numeric 'month' property to each image in the collection.
|
|
1294
|
+
|
|
1295
|
+
Returns:
|
|
1296
|
+
LandsatCollection: A LandsatCollection image collection with the 'month' property added to each image.
|
|
1297
|
+
"""
|
|
1298
|
+
col = self.collection.map(LandsatCollection.add_month_property_fn)
|
|
1299
|
+
return LandsatCollection(collection=col)
|
|
1300
|
+
|
|
1301
|
+
def remove_duplicate_dates(self, sort_by='system:time_start', ascending=True):
|
|
1302
|
+
"""
|
|
1303
|
+
Removes duplicate images that share the same date, keeping only the first one encountered.
|
|
1304
|
+
Useful for handling duplicate acquisitions or overlapping path/rows.
|
|
1305
|
+
|
|
1306
|
+
Args:
|
|
1307
|
+
sort_by (str): Property to sort by before filtering distinct dates. Options: 'system:time_start' or 'CLOUD_COVER'.
|
|
1308
|
+
Defaults to 'system:time_start'.
|
|
1309
|
+
Recommended to use 'CLOUD_COVER' (ascending=True) to keep the clearest image.
|
|
1310
|
+
ascending (bool): Sort order. Defaults to True.
|
|
1311
|
+
|
|
1312
|
+
Returns:
|
|
1313
|
+
LandsatCollection: A new LandsatCollection object with distinct dates.
|
|
1314
|
+
"""
|
|
1315
|
+
if sort_by not in ['system:time_start', 'CLOUD_COVER']:
|
|
1316
|
+
raise ValueError(f"The provided `sort_by` argument is invalid: {sort_by}. The `sort_by` argument must be either 'system:time_start' or 'CLOUD_COVER'.")
|
|
1317
|
+
# Sort the collection to ensure the "best" image comes first (e.g. least cloudy)
|
|
1318
|
+
sorted_col = self.collection.sort(sort_by, ascending)
|
|
1319
|
+
|
|
1320
|
+
# distinct() retains the first image for each unique value of the specified property
|
|
1321
|
+
distinct_col = sorted_col.distinct('Date_Filter')
|
|
1322
|
+
|
|
1323
|
+
return LandsatCollection(collection=distinct_col)
|
|
1266
1324
|
|
|
1267
1325
|
def combine(self, other):
|
|
1268
1326
|
"""
|
|
@@ -1699,6 +1757,338 @@ class LandsatCollection:
|
|
|
1699
1757
|
pass
|
|
1700
1758
|
|
|
1701
1759
|
return self._monthly_median
|
|
1760
|
+
|
|
1761
|
+
@property
|
|
1762
|
+
def monthly_mean_collection(self):
|
|
1763
|
+
"""Creates a monthly mean composite from a LandsatCollection image collection.
|
|
1764
|
+
|
|
1765
|
+
This function computes the mean for each
|
|
1766
|
+
month within the collection's date range, for each band in the collection. It automatically handles the full
|
|
1767
|
+
temporal extent of the input collection.
|
|
1768
|
+
|
|
1769
|
+
The resulting images have a 'system:time_start' property set to the
|
|
1770
|
+
first day of each month and an 'image_count' property indicating how
|
|
1771
|
+
many images were used in the composite. Months with no images are
|
|
1772
|
+
automatically excluded from the final collection.
|
|
1773
|
+
|
|
1774
|
+
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.
|
|
1775
|
+
|
|
1776
|
+
Returns:
|
|
1777
|
+
LandsatCollection: A new LandsatCollection object with monthly mean composites.
|
|
1778
|
+
"""
|
|
1779
|
+
if self._monthly_mean is None:
|
|
1780
|
+
collection = self.collection
|
|
1781
|
+
target_proj = collection.first().projection()
|
|
1782
|
+
# Get the start and end dates of the entire collection.
|
|
1783
|
+
date_range = collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
|
|
1784
|
+
original_start_date = ee.Date(date_range.get('min'))
|
|
1785
|
+
end_date = ee.Date(date_range.get('max'))
|
|
1786
|
+
|
|
1787
|
+
start_year = original_start_date.get('year')
|
|
1788
|
+
start_month = original_start_date.get('month')
|
|
1789
|
+
start_date = ee.Date.fromYMD(start_year, start_month, 1)
|
|
1790
|
+
|
|
1791
|
+
# Calculate the total number of months in the date range.
|
|
1792
|
+
# The .round() is important for ensuring we get an integer.
|
|
1793
|
+
num_months = end_date.difference(start_date, 'month').round()
|
|
1794
|
+
|
|
1795
|
+
# Generate a list of starting dates for each month.
|
|
1796
|
+
# This uses a sequence and advances the start date by 'i' months.
|
|
1797
|
+
def get_month_start(i):
|
|
1798
|
+
return start_date.advance(i, 'month')
|
|
1799
|
+
|
|
1800
|
+
month_starts = ee.List.sequence(0, num_months).map(get_month_start)
|
|
1801
|
+
|
|
1802
|
+
# Define a function to map over the list of month start dates.
|
|
1803
|
+
def create_monthly_composite(date):
|
|
1804
|
+
# Cast the input to an ee.Date object.
|
|
1805
|
+
start_of_month = ee.Date(date)
|
|
1806
|
+
# The end date is exclusive, so we advance by 1 month.
|
|
1807
|
+
end_of_month = start_of_month.advance(1, 'month')
|
|
1808
|
+
|
|
1809
|
+
# Filter the original collection to get images for the current month.
|
|
1810
|
+
monthly_subset = collection.filterDate(start_of_month, end_of_month)
|
|
1811
|
+
|
|
1812
|
+
# Count the number of images in the monthly subset.
|
|
1813
|
+
image_count = monthly_subset.size()
|
|
1814
|
+
|
|
1815
|
+
# Compute the mean. This is robust to outliers like clouds.
|
|
1816
|
+
monthly_mean = monthly_subset.mean()
|
|
1817
|
+
|
|
1818
|
+
# Set essential properties on the resulting composite image.
|
|
1819
|
+
# The timestamp is crucial for time-series analysis and charting.
|
|
1820
|
+
# The image_count is useful metadata for quality assessment.
|
|
1821
|
+
return monthly_mean.set({
|
|
1822
|
+
'system:time_start': start_of_month.millis(),
|
|
1823
|
+
'month': start_of_month.get('month'),
|
|
1824
|
+
'year': start_of_month.get('year'),
|
|
1825
|
+
'Date_Filter': start_of_month.format('YYYY-MM-dd'),
|
|
1826
|
+
'image_count': image_count
|
|
1827
|
+
}).reproject(target_proj)
|
|
1828
|
+
|
|
1829
|
+
# Map the composite function over the list of month start dates.
|
|
1830
|
+
monthly_composites_list = month_starts.map(create_monthly_composite)
|
|
1831
|
+
|
|
1832
|
+
# Convert the list of images into an ee.ImageCollection.
|
|
1833
|
+
monthly_collection = ee.ImageCollection.fromImages(monthly_composites_list)
|
|
1834
|
+
|
|
1835
|
+
# Filter out any composites that were created from zero images.
|
|
1836
|
+
# This prevents empty/masked images from being in the final collection.
|
|
1837
|
+
final_collection = LandsatCollection(collection=monthly_collection.filter(ee.Filter.gt('image_count', 0)))
|
|
1838
|
+
self._monthly_mean = final_collection
|
|
1839
|
+
else:
|
|
1840
|
+
pass
|
|
1841
|
+
|
|
1842
|
+
return self._monthly_mean
|
|
1843
|
+
|
|
1844
|
+
@property
|
|
1845
|
+
def monthly_sum_collection(self):
|
|
1846
|
+
"""Creates a monthly sum composite from a LandsatCollection image collection.
|
|
1847
|
+
|
|
1848
|
+
This function computes the sum for each
|
|
1849
|
+
month within the collection's date range, for each band in the collection. It automatically handles the full
|
|
1850
|
+
temporal extent of the input collection.
|
|
1851
|
+
|
|
1852
|
+
The resulting images have a 'system:time_start' property set to the
|
|
1853
|
+
first day of each month and an 'image_count' property indicating how
|
|
1854
|
+
many images were used in the composite. Months with no images are
|
|
1855
|
+
automatically excluded from the final collection.
|
|
1856
|
+
|
|
1857
|
+
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.
|
|
1858
|
+
|
|
1859
|
+
Returns:
|
|
1860
|
+
LandsatCollection: A new LandsatCollection object with monthly sum composites.
|
|
1861
|
+
"""
|
|
1862
|
+
if self._monthly_sum is None:
|
|
1863
|
+
collection = self.collection
|
|
1864
|
+
target_proj = collection.first().projection()
|
|
1865
|
+
# Get the start and end dates of the entire collection.
|
|
1866
|
+
date_range = collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
|
|
1867
|
+
original_start_date = ee.Date(date_range.get('min'))
|
|
1868
|
+
end_date = ee.Date(date_range.get('max'))
|
|
1869
|
+
|
|
1870
|
+
start_year = original_start_date.get('year')
|
|
1871
|
+
start_month = original_start_date.get('month')
|
|
1872
|
+
start_date = ee.Date.fromYMD(start_year, start_month, 1)
|
|
1873
|
+
|
|
1874
|
+
# Calculate the total number of months in the date range.
|
|
1875
|
+
# The .round() is important for ensuring we get an integer.
|
|
1876
|
+
num_months = end_date.difference(start_date, 'month').round()
|
|
1877
|
+
|
|
1878
|
+
# Generate a list of starting dates for each month.
|
|
1879
|
+
# This uses a sequence and advances the start date by 'i' months.
|
|
1880
|
+
def get_month_start(i):
|
|
1881
|
+
return start_date.advance(i, 'month')
|
|
1882
|
+
|
|
1883
|
+
month_starts = ee.List.sequence(0, num_months).map(get_month_start)
|
|
1884
|
+
|
|
1885
|
+
# Define a function to map over the list of month start dates.
|
|
1886
|
+
def create_monthly_composite(date):
|
|
1887
|
+
# Cast the input to an ee.Date object.
|
|
1888
|
+
start_of_month = ee.Date(date)
|
|
1889
|
+
# The end date is exclusive, so we advance by 1 month.
|
|
1890
|
+
end_of_month = start_of_month.advance(1, 'month')
|
|
1891
|
+
|
|
1892
|
+
# Filter the original collection to get images for the current month.
|
|
1893
|
+
monthly_subset = collection.filterDate(start_of_month, end_of_month)
|
|
1894
|
+
|
|
1895
|
+
# Count the number of images in the monthly subset.
|
|
1896
|
+
image_count = monthly_subset.size()
|
|
1897
|
+
|
|
1898
|
+
# Compute the sum. This is robust to outliers like clouds.
|
|
1899
|
+
monthly_sum = monthly_subset.sum()
|
|
1900
|
+
|
|
1901
|
+
# Set essential properties on the resulting composite image.
|
|
1902
|
+
# The timestamp is crucial for time-series analysis and charting.
|
|
1903
|
+
# The image_count is useful metadata for quality assessment.
|
|
1904
|
+
return monthly_sum.set({
|
|
1905
|
+
'system:time_start': start_of_month.millis(),
|
|
1906
|
+
'month': start_of_month.get('month'),
|
|
1907
|
+
'year': start_of_month.get('year'),
|
|
1908
|
+
'Date_Filter': start_of_month.format('YYYY-MM-dd'),
|
|
1909
|
+
'image_count': image_count
|
|
1910
|
+
}).reproject(target_proj)
|
|
1911
|
+
|
|
1912
|
+
# Map the composite function over the list of month start dates.
|
|
1913
|
+
monthly_composites_list = month_starts.map(create_monthly_composite)
|
|
1914
|
+
|
|
1915
|
+
# Convert the list of images into an ee.ImageCollection.
|
|
1916
|
+
monthly_collection = ee.ImageCollection.fromImages(monthly_composites_list)
|
|
1917
|
+
|
|
1918
|
+
# Filter out any composites that were created from zero images.
|
|
1919
|
+
# This prevents empty/masked images from being in the final collection.
|
|
1920
|
+
final_collection = LandsatCollection(collection=monthly_collection.filter(ee.Filter.gt('image_count', 0)))
|
|
1921
|
+
self._monthly_sum = final_collection
|
|
1922
|
+
else:
|
|
1923
|
+
pass
|
|
1924
|
+
|
|
1925
|
+
return self._monthly_sum
|
|
1926
|
+
|
|
1927
|
+
@property
|
|
1928
|
+
def monthly_max_collection(self):
|
|
1929
|
+
"""Creates a monthly max composite from a LandsatCollection image collection.
|
|
1930
|
+
|
|
1931
|
+
This function computes the max for each
|
|
1932
|
+
month within the collection's date range, for each band in the collection. It automatically handles the full
|
|
1933
|
+
temporal extent of the input collection.
|
|
1934
|
+
|
|
1935
|
+
The resulting images have a 'system:time_start' property set to the
|
|
1936
|
+
first day of each month and an 'image_count' property indicating how
|
|
1937
|
+
many images were used in the composite. Months with no images are
|
|
1938
|
+
automatically excluded from the final collection.
|
|
1939
|
+
|
|
1940
|
+
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.
|
|
1941
|
+
|
|
1942
|
+
Returns:
|
|
1943
|
+
LandsatCollection: A new LandsatCollection object with monthly max composites.
|
|
1944
|
+
"""
|
|
1945
|
+
if self._monthly_max is None:
|
|
1946
|
+
collection = self.collection
|
|
1947
|
+
target_proj = collection.first().projection()
|
|
1948
|
+
# Get the start and end dates of the entire collection.
|
|
1949
|
+
date_range = collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
|
|
1950
|
+
original_start_date = ee.Date(date_range.get('min'))
|
|
1951
|
+
end_date = ee.Date(date_range.get('max'))
|
|
1952
|
+
|
|
1953
|
+
start_year = original_start_date.get('year')
|
|
1954
|
+
start_month = original_start_date.get('month')
|
|
1955
|
+
start_date = ee.Date.fromYMD(start_year, start_month, 1)
|
|
1956
|
+
|
|
1957
|
+
# Calculate the total number of months in the date range.
|
|
1958
|
+
# The .round() is important for ensuring we get an integer.
|
|
1959
|
+
num_months = end_date.difference(start_date, 'month').round()
|
|
1960
|
+
|
|
1961
|
+
# Generate a list of starting dates for each month.
|
|
1962
|
+
# This uses a sequence and advances the start date by 'i' months.
|
|
1963
|
+
def get_month_start(i):
|
|
1964
|
+
return start_date.advance(i, 'month')
|
|
1965
|
+
|
|
1966
|
+
month_starts = ee.List.sequence(0, num_months).map(get_month_start)
|
|
1967
|
+
|
|
1968
|
+
# Define a function to map over the list of month start dates.
|
|
1969
|
+
def create_monthly_composite(date):
|
|
1970
|
+
# Cast the input to an ee.Date object.
|
|
1971
|
+
start_of_month = ee.Date(date)
|
|
1972
|
+
# The end date is exclusive, so we advance by 1 month.
|
|
1973
|
+
end_of_month = start_of_month.advance(1, 'month')
|
|
1974
|
+
|
|
1975
|
+
# Filter the original collection to get images for the current month.
|
|
1976
|
+
monthly_subset = collection.filterDate(start_of_month, end_of_month)
|
|
1977
|
+
|
|
1978
|
+
# Count the number of images in the monthly subset.
|
|
1979
|
+
image_count = monthly_subset.size()
|
|
1980
|
+
|
|
1981
|
+
# Compute the max. This is robust to outliers like clouds.
|
|
1982
|
+
monthly_max = monthly_subset.max()
|
|
1983
|
+
|
|
1984
|
+
# Set essential properties on the resulting composite image.
|
|
1985
|
+
# The timestamp is crucial for time-series analysis and charting.
|
|
1986
|
+
# The image_count is useful metadata for quality assessment.
|
|
1987
|
+
return monthly_max.set({
|
|
1988
|
+
'system:time_start': start_of_month.millis(),
|
|
1989
|
+
'month': start_of_month.get('month'),
|
|
1990
|
+
'year': start_of_month.get('year'),
|
|
1991
|
+
'Date_Filter': start_of_month.format('YYYY-MM-dd'),
|
|
1992
|
+
'image_count': image_count
|
|
1993
|
+
}).reproject(target_proj)
|
|
1994
|
+
|
|
1995
|
+
# Map the composite function over the list of month start dates.
|
|
1996
|
+
monthly_composites_list = month_starts.map(create_monthly_composite)
|
|
1997
|
+
|
|
1998
|
+
# Convert the list of images into an ee.ImageCollection.
|
|
1999
|
+
monthly_collection = ee.ImageCollection.fromImages(monthly_composites_list)
|
|
2000
|
+
|
|
2001
|
+
# Filter out any composites that were created from zero images.
|
|
2002
|
+
# This prevents empty/masked images from being in the final collection.
|
|
2003
|
+
final_collection = LandsatCollection(collection=monthly_collection.filter(ee.Filter.gt('image_count', 0)))
|
|
2004
|
+
self._monthly_max = final_collection
|
|
2005
|
+
else:
|
|
2006
|
+
pass
|
|
2007
|
+
|
|
2008
|
+
return self._monthly_max
|
|
2009
|
+
|
|
2010
|
+
@property
|
|
2011
|
+
def monthly_min_collection(self):
|
|
2012
|
+
"""Creates a monthly min composite from a LandsatCollection image collection.
|
|
2013
|
+
|
|
2014
|
+
This function computes the min for each
|
|
2015
|
+
month within the collection's date range, for each band in the collection. It automatically handles the full
|
|
2016
|
+
temporal extent of the input collection.
|
|
2017
|
+
|
|
2018
|
+
The resulting images have a 'system:time_start' property set to the
|
|
2019
|
+
first day of each month and an 'image_count' property indicating how
|
|
2020
|
+
many images were used in the composite. Months with no images are
|
|
2021
|
+
automatically excluded from the final collection.
|
|
2022
|
+
|
|
2023
|
+
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.
|
|
2024
|
+
|
|
2025
|
+
Returns:
|
|
2026
|
+
LandsatCollection: A new LandsatCollection object with monthly min composites.
|
|
2027
|
+
"""
|
|
2028
|
+
if self._monthly_min is None:
|
|
2029
|
+
collection = self.collection
|
|
2030
|
+
target_proj = collection.first().projection()
|
|
2031
|
+
# Get the start and end dates of the entire collection.
|
|
2032
|
+
date_range = collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
|
|
2033
|
+
original_start_date = ee.Date(date_range.get('min'))
|
|
2034
|
+
end_date = ee.Date(date_range.get('max'))
|
|
2035
|
+
|
|
2036
|
+
start_year = original_start_date.get('year')
|
|
2037
|
+
start_month = original_start_date.get('month')
|
|
2038
|
+
start_date = ee.Date.fromYMD(start_year, start_month, 1)
|
|
2039
|
+
|
|
2040
|
+
# Calculate the total number of months in the date range.
|
|
2041
|
+
# The .round() is important for ensuring we get an integer.
|
|
2042
|
+
num_months = end_date.difference(start_date, 'month').round()
|
|
2043
|
+
|
|
2044
|
+
# Generate a list of starting dates for each month.
|
|
2045
|
+
# This uses a sequence and advances the start date by 'i' months.
|
|
2046
|
+
def get_month_start(i):
|
|
2047
|
+
return start_date.advance(i, 'month')
|
|
2048
|
+
|
|
2049
|
+
month_starts = ee.List.sequence(0, num_months).map(get_month_start)
|
|
2050
|
+
|
|
2051
|
+
# Define a function to map over the list of month start dates.
|
|
2052
|
+
def create_monthly_composite(date):
|
|
2053
|
+
# Cast the input to an ee.Date object.
|
|
2054
|
+
start_of_month = ee.Date(date)
|
|
2055
|
+
# The end date is exclusive, so we advance by 1 month.
|
|
2056
|
+
end_of_month = start_of_month.advance(1, 'month')
|
|
2057
|
+
|
|
2058
|
+
# Filter the original collection to get images for the current month.
|
|
2059
|
+
monthly_subset = collection.filterDate(start_of_month, end_of_month)
|
|
2060
|
+
|
|
2061
|
+
# Count the number of images in the monthly subset.
|
|
2062
|
+
image_count = monthly_subset.size()
|
|
2063
|
+
|
|
2064
|
+
# Compute the min. This is robust to outliers like clouds.
|
|
2065
|
+
monthly_min = monthly_subset.min()
|
|
2066
|
+
|
|
2067
|
+
# Set essential properties on the resulting composite image.
|
|
2068
|
+
# The timestamp is crucial for time-series analysis and charting.
|
|
2069
|
+
# The image_count is useful metadata for quality assessment.
|
|
2070
|
+
return monthly_min.set({
|
|
2071
|
+
'system:time_start': start_of_month.millis(),
|
|
2072
|
+
'month': start_of_month.get('month'),
|
|
2073
|
+
'year': start_of_month.get('year'),
|
|
2074
|
+
'Date_Filter': start_of_month.format('YYYY-MM-dd'),
|
|
2075
|
+
'image_count': image_count
|
|
2076
|
+
}).reproject(target_proj)
|
|
2077
|
+
|
|
2078
|
+
# Map the composite function over the list of month start dates.
|
|
2079
|
+
monthly_composites_list = month_starts.map(create_monthly_composite)
|
|
2080
|
+
|
|
2081
|
+
# Convert the list of images into an ee.ImageCollection.
|
|
2082
|
+
monthly_collection = ee.ImageCollection.fromImages(monthly_composites_list)
|
|
2083
|
+
|
|
2084
|
+
# Filter out any composites that were created from zero images.
|
|
2085
|
+
# This prevents empty/masked images from being in the final collection.
|
|
2086
|
+
final_collection = LandsatCollection(collection=monthly_collection.filter(ee.Filter.gt('image_count', 0)))
|
|
2087
|
+
self._monthly_min = final_collection
|
|
2088
|
+
else:
|
|
2089
|
+
pass
|
|
2090
|
+
|
|
2091
|
+
return self._monthly_min
|
|
1702
2092
|
|
|
1703
2093
|
@property
|
|
1704
2094
|
def ndwi(self):
|
|
@@ -2417,6 +2807,7 @@ class LandsatCollection:
|
|
|
2417
2807
|
)
|
|
2418
2808
|
return LandsatCollection(collection=col)
|
|
2419
2809
|
|
|
2810
|
+
@property
|
|
2420
2811
|
def C_to_F(self):
|
|
2421
2812
|
"""
|
|
2422
2813
|
Function to convert an LST collection from Celcius to Fahrenheit, adding a new band 'LST_F' to each image in the collection.
|
|
@@ -2424,9 +2815,10 @@ class LandsatCollection:
|
|
|
2424
2815
|
Returns:
|
|
2425
2816
|
LandsatCollection: A LandsatCollection image collection with LST in Fahrenheit as band titled 'LST_F'.
|
|
2426
2817
|
"""
|
|
2427
|
-
if self._LST is None:
|
|
2428
|
-
|
|
2429
|
-
col = self._LST.collection.map(LandsatCollection.C_to_F_fn)
|
|
2818
|
+
# if self._LST is None:
|
|
2819
|
+
# raise ValueError("LST has not been calculated yet. Access the LST property first.")
|
|
2820
|
+
# col = self._LST.collection.map(LandsatCollection.C_to_F_fn)
|
|
2821
|
+
col = self.collection.map(LandsatCollection.C_to_F_fn)
|
|
2430
2822
|
return LandsatCollection(collection=col)
|
|
2431
2823
|
|
|
2432
2824
|
def mask_to_polygon(self, polygon):
|
|
@@ -2564,27 +2956,29 @@ class LandsatCollection:
|
|
|
2564
2956
|
if threshold is None:
|
|
2565
2957
|
raise ValueError("Threshold must be specified for binary masking.")
|
|
2566
2958
|
|
|
2959
|
+
|
|
2567
2960
|
if classify_above_threshold:
|
|
2568
|
-
if mask_zeros:
|
|
2961
|
+
if mask_zeros is True:
|
|
2569
2962
|
col = self.collection.map(
|
|
2570
|
-
lambda image: image.select(band_name).gte(threshold).rename(band_name).
|
|
2963
|
+
lambda image: image.select(band_name).gte(threshold).rename(band_name).selfMask().copyProperties(image)
|
|
2571
2964
|
)
|
|
2572
2965
|
else:
|
|
2573
2966
|
col = self.collection.map(
|
|
2574
2967
|
lambda image: image.select(band_name).gte(threshold).rename(band_name).copyProperties(image)
|
|
2575
2968
|
)
|
|
2576
2969
|
else:
|
|
2577
|
-
if mask_zeros:
|
|
2970
|
+
if mask_zeros is True:
|
|
2578
2971
|
col = self.collection.map(
|
|
2579
|
-
lambda image: image.select(band_name).lte(threshold).rename(band_name).
|
|
2972
|
+
lambda image: image.select(band_name).lte(threshold).rename(band_name).selfMask().copyProperties(image)
|
|
2580
2973
|
)
|
|
2581
2974
|
else:
|
|
2582
2975
|
col = self.collection.map(
|
|
2583
2976
|
lambda image: image.select(band_name).lte(threshold).rename(band_name).copyProperties(image)
|
|
2584
2977
|
)
|
|
2978
|
+
|
|
2585
2979
|
return LandsatCollection(collection=col)
|
|
2586
2980
|
|
|
2587
|
-
def anomaly(self, geometry, band_name=None, anomaly_band_name=None, replace=True):
|
|
2981
|
+
def anomaly(self, geometry, band_name=None, anomaly_band_name=None, replace=True, scale=30):
|
|
2588
2982
|
"""
|
|
2589
2983
|
Calculates the anomaly of each image in a collection compared to the mean of each image.
|
|
2590
2984
|
|
|
@@ -2598,6 +2992,7 @@ class LandsatCollection:
|
|
|
2598
2992
|
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.
|
|
2599
2993
|
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.
|
|
2600
2994
|
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.
|
|
2995
|
+
scale (int, optional): The scale (in meters) to be used for image reduction. Default is 30 meters.
|
|
2601
2996
|
|
|
2602
2997
|
Returns:
|
|
2603
2998
|
LandsatCollection: A LandsatCollection where each image represents the anomaly (deviation from
|
|
@@ -2616,7 +3011,7 @@ class LandsatCollection:
|
|
|
2616
3011
|
else:
|
|
2617
3012
|
band_name = band_names.get(0).getInfo()
|
|
2618
3013
|
|
|
2619
|
-
col = self.collection.map(lambda image: LandsatCollection.anomaly_fn(image, geometry=geometry, band_name=band_name, anomaly_band_name=anomaly_band_name, replace=replace))
|
|
3014
|
+
col = self.collection.map(lambda image: LandsatCollection.anomaly_fn(image, geometry=geometry, band_name=band_name, anomaly_band_name=anomaly_band_name, replace=replace, scale=scale))
|
|
2620
3015
|
return LandsatCollection(collection=col)
|
|
2621
3016
|
|
|
2622
3017
|
def mask_via_band(self, band_to_mask, band_for_mask, threshold=-1, mask_above=True, add_band_to_original_image=False):
|
|
@@ -3447,24 +3842,30 @@ class LandsatCollection:
|
|
|
3447
3842
|
ValueError: If input parameters are invalid.
|
|
3448
3843
|
TypeError: If geometries input type is unsupported.
|
|
3449
3844
|
"""
|
|
3845
|
+
# Create a local reference to the collection object to allow for modifications (like band selection) without altering the original instance
|
|
3450
3846
|
img_collection_obj = self
|
|
3847
|
+
|
|
3848
|
+
# If a specific band is requested, select only that band
|
|
3451
3849
|
if band:
|
|
3452
3850
|
img_collection_obj = LandsatCollection(collection=img_collection_obj.collection.select(band))
|
|
3453
3851
|
else:
|
|
3852
|
+
# If no band is specified, default to using the first band of the first image in the collection
|
|
3454
3853
|
first_image = img_collection_obj.image_grab(0)
|
|
3455
3854
|
first_band = first_image.bandNames().get(0)
|
|
3456
3855
|
img_collection_obj = LandsatCollection(collection=img_collection_obj.collection.select([first_band]))
|
|
3457
|
-
|
|
3856
|
+
|
|
3857
|
+
# If a list of dates is provided, filter the collection to include only images matching those dates
|
|
3458
3858
|
if dates:
|
|
3459
3859
|
img_collection_obj = LandsatCollection(
|
|
3460
3860
|
collection=self.collection.filter(ee.Filter.inList('Date_Filter', dates))
|
|
3461
3861
|
)
|
|
3462
3862
|
|
|
3463
|
-
# Initialize variables
|
|
3863
|
+
# Initialize variables to hold the standardized feature collection and coordinates
|
|
3464
3864
|
features = None
|
|
3465
3865
|
validated_coordinates = []
|
|
3466
3866
|
|
|
3467
|
-
#
|
|
3867
|
+
# Define a helper function to ensure every feature has a standardized 'geo_name' property
|
|
3868
|
+
# This handles features that might have different existing name properties or none at all
|
|
3468
3869
|
def set_standard_name(feature):
|
|
3469
3870
|
has_geo_name = feature.get('geo_name')
|
|
3470
3871
|
has_name = feature.get('name')
|
|
@@ -3475,33 +3876,38 @@ class LandsatCollection:
|
|
|
3475
3876
|
ee.Algorithms.If(has_index, has_index, 'unnamed_geometry')))
|
|
3476
3877
|
return feature.set({'geo_name': new_name})
|
|
3477
3878
|
|
|
3879
|
+
# Handle input: FeatureCollection or single Feature
|
|
3478
3880
|
if isinstance(geometries, (ee.FeatureCollection, ee.Feature)):
|
|
3479
3881
|
features = ee.FeatureCollection(geometries)
|
|
3480
3882
|
if geometry_names:
|
|
3481
3883
|
print("Warning: 'geometry_names' are ignored when the input is an ee.Feature or ee.FeatureCollection.")
|
|
3482
3884
|
|
|
3885
|
+
# Handle input: Single ee.Geometry
|
|
3483
3886
|
elif isinstance(geometries, ee.Geometry):
|
|
3484
3887
|
name = geometry_names[0] if (geometry_names and geometry_names[0]) else 'unnamed_geometry'
|
|
3485
3888
|
features = ee.FeatureCollection([ee.Feature(geometries).set('geo_name', name)])
|
|
3486
3889
|
|
|
3890
|
+
# Handle input: List (could be coordinates or ee.Geometry objects)
|
|
3487
3891
|
elif isinstance(geometries, list):
|
|
3488
3892
|
if not geometries: # Handle empty list case
|
|
3489
3893
|
raise ValueError("'geometries' list cannot be empty.")
|
|
3490
3894
|
|
|
3491
|
-
# Case: List of coordinates
|
|
3895
|
+
# Case: List of tuples (coordinates)
|
|
3492
3896
|
if all(isinstance(i, tuple) for i in geometries):
|
|
3493
3897
|
validated_coordinates = geometries
|
|
3898
|
+
# Generate default names if none provided
|
|
3494
3899
|
if geometry_names is None:
|
|
3495
3900
|
geometry_names = [f"Location_{i+1}" for i in range(len(validated_coordinates))]
|
|
3496
3901
|
elif len(geometry_names) != len(validated_coordinates):
|
|
3497
3902
|
raise ValueError("geometry_names must have the same length as the coordinates list.")
|
|
3903
|
+
# Create features with buffers around the coordinates
|
|
3498
3904
|
points = [
|
|
3499
3905
|
ee.Feature(ee.Geometry.Point(coord).buffer(buffer_size), {'geo_name': str(name)})
|
|
3500
3906
|
for coord, name in zip(validated_coordinates, geometry_names)
|
|
3501
3907
|
]
|
|
3502
3908
|
features = ee.FeatureCollection(points)
|
|
3503
3909
|
|
|
3504
|
-
# Case: List of
|
|
3910
|
+
# Case: List of ee.Geometry objects
|
|
3505
3911
|
elif all(isinstance(i, ee.Geometry) for i in geometries):
|
|
3506
3912
|
if geometry_names is None:
|
|
3507
3913
|
geometry_names = [f"Geometry_{i+1}" for i in range(len(geometries))]
|
|
@@ -3516,6 +3922,7 @@ class LandsatCollection:
|
|
|
3516
3922
|
else:
|
|
3517
3923
|
raise TypeError("Input list must be a list of (lon, lat) tuples OR a list of ee.Geometry objects.")
|
|
3518
3924
|
|
|
3925
|
+
# Handle input: Single tuple (coordinate)
|
|
3519
3926
|
elif isinstance(geometries, tuple) and len(geometries) == 2:
|
|
3520
3927
|
name = geometry_names[0] if geometry_names else 'Location_1'
|
|
3521
3928
|
features = ee.FeatureCollection([
|
|
@@ -3524,39 +3931,48 @@ class LandsatCollection:
|
|
|
3524
3931
|
else:
|
|
3525
3932
|
raise TypeError("Unsupported type for 'geometries'.")
|
|
3526
3933
|
|
|
3934
|
+
# Apply the naming standardization to the created FeatureCollection
|
|
3527
3935
|
features = features.map(set_standard_name)
|
|
3528
3936
|
|
|
3937
|
+
# Dynamically retrieve the Earth Engine reducer based on the string name provided
|
|
3529
3938
|
try:
|
|
3530
3939
|
reducer = getattr(ee.Reducer, reducer_type)()
|
|
3531
3940
|
except AttributeError:
|
|
3532
3941
|
raise ValueError(f"Unknown reducer_type: '{reducer_type}'.")
|
|
3533
3942
|
|
|
3943
|
+
# Define the function to map over the image collection
|
|
3534
3944
|
def calculate_stats_for_image(image):
|
|
3535
3945
|
image_date = image.get('Date_Filter')
|
|
3946
|
+
# Calculate statistics for all geometries in 'features' for this specific image
|
|
3536
3947
|
stats_fc = image.reduceRegions(
|
|
3537
3948
|
collection=features, reducer=reducer, scale=scale, tileScale=tileScale
|
|
3538
3949
|
)
|
|
3539
3950
|
|
|
3951
|
+
# Helper to ensure the result has the reducer property, even if masked
|
|
3952
|
+
# If the property is missing (e.g., all pixels masked), set it to a sentinel value (-9999)
|
|
3540
3953
|
def guarantee_reducer_property(f):
|
|
3541
3954
|
has_property = f.propertyNames().contains(reducer_type)
|
|
3542
3955
|
return ee.Algorithms.If(has_property, f, f.set(reducer_type, -9999))
|
|
3956
|
+
|
|
3957
|
+
# Apply the guarantee check
|
|
3543
3958
|
fixed_stats_fc = stats_fc.map(guarantee_reducer_property)
|
|
3544
3959
|
|
|
3960
|
+
# Attach the image date to every feature in the result so we know which image it came from
|
|
3545
3961
|
return fixed_stats_fc.map(lambda f: f.set('image_date', image_date))
|
|
3546
3962
|
|
|
3963
|
+
# Map the calculation over the image collection and flatten the resulting FeatureCollections into one
|
|
3547
3964
|
results_fc = ee.FeatureCollection(img_collection_obj.collection.map(calculate_stats_for_image)).flatten()
|
|
3965
|
+
|
|
3966
|
+
# Convert the Earth Engine FeatureCollection to a pandas DataFrame (client-side operation)
|
|
3548
3967
|
df = LandsatCollection.ee_to_df(results_fc, remove_geom=True)
|
|
3549
3968
|
|
|
3550
|
-
#
|
|
3969
|
+
# Check for empty results or missing columns
|
|
3551
3970
|
if df.empty:
|
|
3552
|
-
# 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.")
|
|
3553
|
-
# return df
|
|
3554
3971
|
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.")
|
|
3555
3972
|
if reducer_type not in df.columns:
|
|
3556
3973
|
print(f"Warning: Reducer '{reducer_type}' not found in results.")
|
|
3557
|
-
# return df
|
|
3558
3974
|
|
|
3559
|
-
#
|
|
3975
|
+
# Filter out the sentinel values (-9999) which indicate failed reductions/masked pixels
|
|
3560
3976
|
initial_rows = len(df)
|
|
3561
3977
|
df.dropna(subset=[reducer_type], inplace=True)
|
|
3562
3978
|
df = df[df[reducer_type] != -9999]
|
|
@@ -3564,9 +3980,18 @@ class LandsatCollection:
|
|
|
3564
3980
|
if dropped_rows > 0:
|
|
3565
3981
|
print(f"Warning: Discarded {dropped_rows} results due to failed reductions (e.g., no valid pixels in geometry).")
|
|
3566
3982
|
|
|
3567
|
-
#
|
|
3983
|
+
# Pivot the DataFrame so that each row represents a date and each column represents a geometry location
|
|
3568
3984
|
pivot_df = df.pivot(index='image_date', columns='geo_name', values=reducer_type)
|
|
3985
|
+
# Rename the column headers (geometry names) to include the reducer type
|
|
3986
|
+
pivot_df.columns = [f"{col}_{reducer_type}" for col in pivot_df.columns]
|
|
3987
|
+
# Rename the index axis to 'Date' so it is correctly labeled when moved to a column later
|
|
3569
3988
|
pivot_df.index.name = 'Date'
|
|
3989
|
+
# 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
|
|
3990
|
+
pivot_df.columns.name = None
|
|
3991
|
+
# Reset the index to move the 'Date' index into a regular column and create a standard numerical index (0, 1, 2...)
|
|
3992
|
+
pivot_df = pivot_df.reset_index(drop=False)
|
|
3993
|
+
|
|
3994
|
+
# If a file path is provided, save the resulting DataFrame to CSV
|
|
3570
3995
|
if file_path:
|
|
3571
3996
|
# Check if file_path ends with .csv and remove it if so for consistency
|
|
3572
3997
|
if file_path.endswith('.csv'):
|