From dcaabc84e40226e870bf8a036018ecbd0aa1e5fd Mon Sep 17 00:00:00 2001 From: polochinoc Date: Sat, 21 Sep 2024 14:46:44 +0200 Subject: [PATCH 1/4] Compute linear regression between given year interval ; closes #76 --- app.py | 17 +++++++--- src/core/models/all_rainfall.py | 22 +++++++++++-- src/core/models/yearly_rainfall.py | 31 ++++++++++++++++++ src/core/utils/functions/plotting.py | 38 +++++++++++++---------- tst/core/models/test_yearly_rainfall.py | 7 +++++ tst/core/utils/functions/test_plotting.py | 9 ++++-- 6 files changed, 99 insertions(+), 25 deletions(-) diff --git a/app.py b/app.py index 1f4d96f..7983a49 100644 --- a/app.py +++ b/app.py @@ -345,12 +345,21 @@ def get_rainfall_averages( "/graph/rainfall_linreg_slopes", response_class=StreamingResponse, summary="Retrieve rainfall monthly or seasonal linear regression slopes of data as a PNG.", - description=f"Time mode should be either '{TimeMode.MONTHLY.value}' or '{TimeMode.SEASONAL.value}'.", + description=f"Time mode should be either '{TimeMode.MONTHLY.value}' or '{TimeMode.SEASONAL.value}'.\n" + f"If no ending year is precised, most recent year available is taken: {all_rainfall.get_last_year()}.", tags=["Graph"], operation_id="getRainfallLinregSlopes", ) -def get_rainfall_linreg_slopes(time_mode: TimeMode): - linreg_slopes = all_rainfall.bar_rainfall_linreg_slopes(time_mode.value) +def get_rainfall_linreg_slopes( + time_mode: TimeMode, + begin_year: int, + end_year: int | None = None, +): + end_year = end_year or all_rainfall.get_last_year() + + linreg_slopes = all_rainfall.bar_rainfall_linreg_slopes( + time_mode=time_mode.value, begin_year=begin_year, end_year=end_year + ) if linreg_slopes is None: raise HTTPException( status_code=400, @@ -362,7 +371,7 @@ def get_rainfall_linreg_slopes(time_mode: TimeMode): plt.close() img_buffer.seek(0) - filename = f"rainfall_{time_mode.value}_linreg_slopes_{all_rainfall.starting_year}_{all_rainfall.get_last_year()}.png" + filename = f"rainfall_{time_mode.value}_linreg_slopes_{begin_year}_{end_year}.png" return StreamingResponse( img_buffer, diff --git a/src/core/models/all_rainfall.py b/src/core/models/all_rainfall.py index d81e6df..8c4b916 100644 --- a/src/core/models/all_rainfall.py +++ b/src/core/models/all_rainfall.py @@ -360,21 +360,37 @@ def bar_rainfall_averages( return None - def bar_rainfall_linreg_slopes(self, time_mode: str) -> list | None: + def bar_rainfall_linreg_slopes( + self, + time_mode: str, + begin_year: int, + end_year: int | None = None, + ) -> list | None: """ Plots a bar graphic displaying linear regression slope for each month or each season. :param time_mode: A string setting the time period ['monthly', 'seasonal']. + :param begin_year: An integer representing the year + to start getting our rainfall values. + :param end_year: An integer representing the year + to end getting our rainfall values (optional). + Is set to last year available is None. :return: A list of the Rainfall LinReg slopes for each month or season. None if time_mode is not within {'monthly', 'seasonal'}. """ + end_year = end_year or self.get_last_year() + if time_mode == TimeMode.MONTHLY.value: return plotting.bar_monthly_rainfall_linreg_slopes( - list(self.monthly_rainfalls.values()) + list(self.monthly_rainfalls.values()), + begin_year=begin_year, + end_year=end_year, ) elif time_mode == TimeMode.SEASONAL.value: return plotting.bar_seasonal_rainfall_linreg_slopes( - list(self.seasonal_rainfalls.values()) + list(self.seasonal_rainfalls.values()), + begin_year=begin_year, + end_year=end_year, ) return None diff --git a/src/core/models/yearly_rainfall.py b/src/core/models/yearly_rainfall.py index f32ee17..f8f5ee9 100644 --- a/src/core/models/yearly_rainfall.py +++ b/src/core/models/yearly_rainfall.py @@ -260,6 +260,37 @@ def get_standard_deviation( self.round_precision, ) + def get_linear_regression( + self, begin_year: int, end_year: int | None = None + ) -> tuple[float, float]: + """ + Computes Linear Regression of rainfall according to year for a given time interval. + + :param begin_year: An integer representing the year + to start getting our rainfall values. + :param end_year: An integer representing the year + to end getting our rainfall values (optional). + If not given, defaults to latest year available. + :return: a tuple containing two floats (r2 score, slope). + """ + end_year = end_year or self.get_last_year() + + data = self.get_yearly_rainfall(begin_year, end_year) + + years = data[Label.YEAR.value].values.reshape(-1, 1) # type: ignore + rainfalls = data[Label.RAINFALL.value].values + + lin_reg = LinearRegression() + lin_reg.fit(years, rainfalls) + predicted_rainfalls = [ + round(rainfall_value, self.round_precision) + for rainfall_value in lin_reg.predict(years).tolist() + ] + + return r2_score(rainfalls, predicted_rainfalls), round( + lin_reg.coef_[0], self.round_precision + ) + def add_percentage_of_normal( self, begin_year: int, end_year: int | None = None ) -> None: diff --git a/src/core/utils/functions/plotting.py b/src/core/utils/functions/plotting.py index 00a42ce..ff6d674 100644 --- a/src/core/utils/functions/plotting.py +++ b/src/core/utils/functions/plotting.py @@ -121,25 +121,28 @@ def bar_monthly_rainfall_averages( def bar_monthly_rainfall_linreg_slopes( monthly_rainfalls: list, + begin_year: int, + end_year: int, ) -> list: """ Plots a bar graphic displaying linear regression slope for each month passed through the dict. - If list is empty, does not plot anything and returns an empty list. :param monthly_rainfalls: A list of instances of MonthlyRainfall. To be purposeful, all instances should have the same time frame in years. + :param begin_year: An integer representing the year + to start getting our rainfall values. + :param end_year: An integer representing the year + to end getting our rainfall values. :return: A list of the Rainfall LinReg slopes for each month. """ - if not monthly_rainfalls: - return [] - month_labels, slopes = [], [] for monthly_rainfall in monthly_rainfalls: month_labels.append(monthly_rainfall.month.value[:3]) - slopes.append(monthly_rainfall.add_linear_regression()[1]) - - begin_year = monthly_rainfalls[0].starting_year - end_year = monthly_rainfalls[0].get_last_year() + slopes.append( + monthly_rainfall.get_linear_regression( + begin_year=begin_year, end_year=end_year + )[1] + ) plt.bar( month_labels, @@ -188,25 +191,28 @@ def bar_seasonal_rainfall_averages( def bar_seasonal_rainfall_linreg_slopes( seasonal_rainfalls: list, + begin_year: int, + end_year: int, ) -> list: """ Plots a bar graphic displaying linear regression slope for each season passed through the dict. - If list is empty, does not plot anything and returns an empty list. :param seasonal_rainfalls: A list of instances of SeasonalRainfall. To be purposeful, all instances should have the same time frame in years. + :param begin_year: An integer representing the year + to start getting our rainfall values. + :param end_year: An integer representing the year + to end getting our rainfall values. :return: A list of the Rainfall LinReg slopes for each season. """ - if not seasonal_rainfalls: - return [] - season_labels, slopes = [], [] for seasonal_rainfall in seasonal_rainfalls: season_labels.append(seasonal_rainfall.season.value) - slopes.append(seasonal_rainfall.add_linear_regression()[1]) - - begin_year = seasonal_rainfalls[0].starting_year - end_year = seasonal_rainfalls[0].get_last_year() + slopes.append( + seasonal_rainfall.get_linear_regression( + begin_year=begin_year, end_year=end_year + )[1] + ) plt.bar( season_labels, diff --git a/tst/core/models/test_yearly_rainfall.py b/tst/core/models/test_yearly_rainfall.py index 5ccac06..c271e87 100644 --- a/tst/core/models/test_yearly_rainfall.py +++ b/tst/core/models/test_yearly_rainfall.py @@ -117,6 +117,13 @@ def test_get_standard_deviation(): assert isinstance(std_weighted_by_avg, float) + @staticmethod + def test_get_linear_regression(): + r2_score, slope = YEARLY_RAINFALL.get_linear_regression(begin_year, end_year) + + assert isinstance(r2_score, float) and r2_score <= 1 + assert isinstance(slope, float) + @staticmethod def test_add_percentage_of_normal(): YEARLY_RAINFALL.add_percentage_of_normal(YEARLY_RAINFALL.starting_year) diff --git a/tst/core/utils/functions/test_plotting.py b/tst/core/utils/functions/test_plotting.py index 4d996c9..41ca86e 100644 --- a/tst/core/utils/functions/test_plotting.py +++ b/tst/core/utils/functions/test_plotting.py @@ -1,5 +1,6 @@ from src.core.utils.enums.labels import Label from src.core.utils.functions import plotting +from tst.core.models.test_all_rainfall import begin_year, end_year from tst.core.models.test_yearly_rainfall import YEARLY_RAINFALL, ALL_RAINFALL BEGIN_YEAR = 1970 @@ -48,7 +49,9 @@ def test_bar_monthly_rainfall_averages(): @staticmethod def test_bar_monthly_rainfall_linreg_slopes(): slopes = plotting.bar_monthly_rainfall_linreg_slopes( - list(ALL_RAINFALL.monthly_rainfalls.values()) + list(ALL_RAINFALL.monthly_rainfalls.values()), + begin_year=begin_year, + end_year=end_year, ) assert isinstance(slopes, list) @@ -71,7 +74,9 @@ def test_bar_seasonal_rainfall_averages(): @staticmethod def test_bar_seasonal_rainfall_linreg_slopes(): slopes = plotting.bar_seasonal_rainfall_linreg_slopes( - list(ALL_RAINFALL.seasonal_rainfalls.values()) + list(ALL_RAINFALL.seasonal_rainfalls.values()), + begin_year=begin_year, + end_year=end_year, ) assert isinstance(slopes, list) From d80365784fab2bb88f0ac7841a3009d7c70d0f15 Mon Sep 17 00:00:00 2001 From: polochinoc Date: Sat, 21 Sep 2024 15:38:17 +0200 Subject: [PATCH 2/4] Allow CSV export between specific dates; closes #36 --- app.py | 30 ++++++++++---------- src/core/models/all_rainfall.py | 37 +++++++++++++++++++------ src/core/models/yearly_rainfall.py | 15 ++++++++-- tst/core/models/test_all_rainfall.py | 2 +- tst/core/models/test_yearly_rainfall.py | 2 +- 5 files changed, 59 insertions(+), 27 deletions(-) diff --git a/app.py b/app.py index 7983a49..9b59965 100644 --- a/app.py +++ b/app.py @@ -35,8 +35,7 @@ @app.get( "/rainfall/average", summary="Retrieve rainfall average for Barcelona between two years.", - description=f"If no ending year is precised, " - f"computes average until latest year available: {all_rainfall.get_last_year()}", + description=f"If no ending year is precised, most recent year available is taken: {all_rainfall.get_last_year()}", tags=["Rainfall"], operation_id="getRainfallAverage", ) @@ -106,8 +105,7 @@ async def get_rainfall_normal( "If 100%, all the years are above normal.
" "If -100%, all the years are below normal.
" "If 0%, there are as many years below as years above.
" - "If no ending year is precised, " - f"computes the relative distance until most recent year available: {all_rainfall.get_last_year()}.", + f"If no ending year is precised, most recent year available is taken: {all_rainfall.get_last_year()}.", tags=["Rainfall"], operation_id="getRainfallRelativeDistanceToNormal", ) @@ -145,8 +143,7 @@ async def get_rainfall_relative_distance_to_normal( @app.get( "/rainfall/standard_deviation", summary="Compute the standard deviation of rainfall for Barcelona between two years.", - description="If no ending year is precised, " - f"computes the relative distance until most recent year available: {all_rainfall.get_last_year()}.", + description=f"If no ending year is precised, most recent year available is taken: {all_rainfall.get_last_year()}.", tags=["Rainfall"], operation_id="getRainfallStandardDeviation", ) @@ -185,8 +182,7 @@ async def get_rainfall_standard_deviation( summary="Compute the number of years below normal for a specific year range.", description="Normal is computed as a 30 years average " "starting from the year set via normal_year.
" - "If no ending year is precised, " - f"computes the relative distance until most recent year available: {all_rainfall.get_last_year()}.", + f"If no ending year is precised, most recent year available is taken: {all_rainfall.get_last_year()}.", tags=["Year"], operation_id="getYearsBelowNormal", ) @@ -226,8 +222,7 @@ async def get_years_below_normal( summary="Compute the number of years above normal for a specific year range.", description="Normal is computed as a 30 years average " "starting from the year set via normal_year.
" - "If no ending year is precised, " - f"computes the relative distance until most recent year available: {all_rainfall.get_last_year()}.", + f"If no ending year is precised, most recent year available is taken: {all_rainfall.get_last_year()}.", tags=["Year"], operation_id="getYearsAboveNormal", ) @@ -266,12 +261,15 @@ async def get_years_above_normal( "/csv/minimal_csv", response_class=StreamingResponse, summary="Retrieve minimal CSV of rainfall data [Year, Rainfall].", - description="Could either be for rainfall upon a whole year, a specific month or a given season.", + description="Could either be for rainfall upon a whole year, a specific month or a given season.
" + f"If no ending year is precised, most recent year available is taken: {all_rainfall.get_last_year()}.", tags=["CSV"], operation_id="getMinimalCsv", ) def get_minimal_csv( time_mode: TimeMode, + begin_year: int, + end_year: int | None = None, month: Month | None = None, season: Season | None = None, ): @@ -283,8 +281,10 @@ def get_minimal_csv( csv_str = ( all_rainfall.export_as_csv( time_mode.value, - month_value, - season_value, + begin_year=begin_year, + end_year=end_year, + month=month_value, + season=season_value, ) or "" ) @@ -306,7 +306,7 @@ def get_minimal_csv( "/graph/rainfall_averages", response_class=StreamingResponse, summary="Retrieve rainfall monthly or seasonal averages of data as a PNG.", - description=f"Time mode should be either '{TimeMode.MONTHLY.value}' or '{TimeMode.SEASONAL.value}'.\n" + description=f"Time mode should be either '{TimeMode.MONTHLY.value}' or '{TimeMode.SEASONAL.value}'.
" f"If no ending year is precised, most recent year available is taken: {all_rainfall.get_last_year()}.", tags=["Graph"], operation_id="getRainfallAverages", @@ -345,7 +345,7 @@ def get_rainfall_averages( "/graph/rainfall_linreg_slopes", response_class=StreamingResponse, summary="Retrieve rainfall monthly or seasonal linear regression slopes of data as a PNG.", - description=f"Time mode should be either '{TimeMode.MONTHLY.value}' or '{TimeMode.SEASONAL.value}'.\n" + description=f"Time mode should be either '{TimeMode.MONTHLY.value}' or '{TimeMode.SEASONAL.value}'.
" f"If no ending year is precised, most recent year available is taken: {all_rainfall.get_last_year()}.", tags=["Graph"], operation_id="getRainfallLinregSlopes", diff --git a/src/core/models/all_rainfall.py b/src/core/models/all_rainfall.py index 8c4b916..3936280 100644 --- a/src/core/models/all_rainfall.py +++ b/src/core/models/all_rainfall.py @@ -64,10 +64,16 @@ def from_config(cls, from_file=False): round_precision=cfg.get_rainfall_precision(), ) - def export_all_data_to_csv(self, folder_path="csv_data") -> str: + def export_all_data_to_csv( + self, begin_year: int, end_year: int | None = None, folder_path="csv_data" + ) -> str: """ Export all the different data as CSVs into specified folder path. + :param begin_year: An integer representing the year + to start getting our rainfall values. + :param end_year: An integer representing the year + to end getting our rainfall values (optional). :param folder_path: path to folder where to save our CSV files. If not set, defaults to 'csv_data'. Should not end with '/'. :return: Path to folder that contains CSV files. @@ -75,28 +81,34 @@ def export_all_data_to_csv(self, folder_path="csv_data") -> str: Path(f"{folder_path}/months").mkdir(parents=True, exist_ok=True) Path(f"{folder_path}/seasons").mkdir(parents=True, exist_ok=True) - last_year: int = self.yearly_rainfall.get_last_year() + end_year = end_year or self.yearly_rainfall.get_last_year() self.yearly_rainfall.export_as_csv( - path=Path(folder_path, f"{self.starting_year}_{last_year}_rainfall.csv") + begin_year=begin_year, + end_year=end_year, + path=Path(folder_path, f"{begin_year}_{end_year}_rainfall.csv"), ) for monthly_rainfall in self.monthly_rainfalls.values(): monthly_rainfall.export_as_csv( + begin_year=begin_year, + end_year=end_year, path=Path( folder_path, "months", - f"{self.starting_year}_{last_year}_{monthly_rainfall.month.name.lower()}_rainfall.csv", - ) + f"{begin_year}_{end_year}_{monthly_rainfall.month.value.lower()}_rainfall.csv", + ), ) for season_rainfall in self.seasonal_rainfalls.values(): season_rainfall.export_as_csv( + begin_year=begin_year, + end_year=end_year, path=Path( folder_path, "seasons", - f"{self.starting_year}_{last_year}_{season_rainfall.season.name.lower()}_rainfall.csv", - ) + f"{begin_year}_{end_year}_{season_rainfall.season.value}_rainfall.csv", + ), ) return folder_path @@ -104,6 +116,9 @@ def export_all_data_to_csv(self, folder_path="csv_data") -> str: def export_as_csv( self, time_mode: str, + *, + begin_year: int, + end_year: int | None = None, month: str | None = None, season: str | None = None, path: str | Path | None = None, @@ -113,6 +128,10 @@ def export_as_csv( Could be for a yearly time frame, a specific month or a given season. :param time_mode: A string setting the time period ['yearly', 'monthly', 'seasonal']. + :param begin_year: An integer representing the year + to start getting our rainfall values. + :param end_year: An integer representing the year + to end getting our rainfall values (optional). :param month: A string corresponding to the month name. Set if time_mode is 'monthly' (optional). :param season: A string corresponding to the season name. @@ -123,7 +142,9 @@ def export_as_csv( None otherwise. """ if entity := self.get_entity_for_time_mode(time_mode, month, season): - return entity.export_as_csv(path) + return entity.export_as_csv( + begin_year=begin_year, end_year=end_year, path=path + ) return None diff --git a/src/core/models/yearly_rainfall.py b/src/core/models/yearly_rainfall.py index f8f5ee9..5e22922 100644 --- a/src/core/models/yearly_rainfall.py +++ b/src/core/models/yearly_rainfall.py @@ -98,16 +98,27 @@ def get_yearly_rainfall( self.data, begin_year=begin_year, end_year=end_year ) - def export_as_csv(self, path: str | Path | None = None) -> str | None: + def export_as_csv( + self, + begin_year: int, + end_year: int | None = None, + path: str | Path | None = None, + ) -> str | None: """ Export the actual instance data state as a CSV. + :param begin_year: An integer representing the year + to start getting our rainfall values. + :param end_year: An integer representing the year + to end getting our rainfall values (optional). :param path: path to csv file to save our data (optional). :return: CSV data as a string if no path is set. None otherwise. """ - return self.data.to_csv(path_or_buf=path, index=False) + return self.get_yearly_rainfall(begin_year, end_year).to_csv( + path_or_buf=path, index=False + ) def get_average_yearly_rainfall( self, begin_year: int, end_year: int | None = None diff --git a/tst/core/models/test_all_rainfall.py b/tst/core/models/test_all_rainfall.py index fa34ccf..d084569 100644 --- a/tst/core/models/test_all_rainfall.py +++ b/tst/core/models/test_all_rainfall.py @@ -25,7 +25,7 @@ class TestAllRainfall: def test_export_all_data_to_csv(): folder_path = "" try: - folder_path = ALL_RAINFALL.export_all_data_to_csv() + folder_path = ALL_RAINFALL.export_all_data_to_csv(begin_year) assert isinstance(folder_path, str) assert Path(folder_path).exists() diff --git a/tst/core/models/test_yearly_rainfall.py b/tst/core/models/test_yearly_rainfall.py index c271e87..57a5544 100644 --- a/tst/core/models/test_yearly_rainfall.py +++ b/tst/core/models/test_yearly_rainfall.py @@ -50,7 +50,7 @@ def test_get_yearly_rainfall(): @staticmethod def test_export_as_csv(): - csv_as_str = YEARLY_RAINFALL.export_as_csv() + csv_as_str = YEARLY_RAINFALL.export_as_csv(begin_year, end_year) assert isinstance(csv_as_str, str) From 8c75fc6bd5f81d889b58c0089d8bb1b3e63005fd Mon Sep 17 00:00:00 2001 From: polochinoc Date: Sat, 21 Sep 2024 17:01:31 +0200 Subject: [PATCH 3/4] Use TimeMode Enum in AllRainfall class for time_mode instead of str -> next step, use it in YearlyRainfall class and use it for month/season str --- app.py | 21 ++++---- src/core/models/all_rainfall.py | 54 +++++++++---------- .../utils/functions/dataframe_operations.py | 3 +- tst/core/models/test_all_rainfall.py | 25 ++++----- 4 files changed, 49 insertions(+), 54 deletions(-) diff --git a/app.py b/app.py index 9b59965..f4fb4ed 100644 --- a/app.py +++ b/app.py @@ -28,7 +28,8 @@ debug=True, root_path="/api", title="Barcelona Rainfall API", - summary="An API that provides rainfall-related data of the city of Barcelona", + summary="An API that provides rainfall-related data of the city of Barcelona.", + description=f"Available data is between {all_rainfall.starting_year} and {all_rainfall.get_last_year()}.", ) @@ -53,7 +54,7 @@ async def get_rainfall_average( return RainfallModel( name="rainfall average (mm)", value=all_rainfall.get_average_rainfall( - time_mode.value, + time_mode, begin_year=begin_year, end_year=end_year, month=month.value if month else None, @@ -85,7 +86,7 @@ async def get_rainfall_normal( return RainfallModel( name="rainfall normal (mm)", value=all_rainfall.get_normal( - time_mode.value, + time_mode, begin_year=begin_year, month=month.value if month else None, season=season.value if season else None, @@ -124,7 +125,7 @@ async def get_rainfall_relative_distance_to_normal( return RainfallWithNormalModel( name="relative distance to rainfall normal (%)", value=all_rainfall.get_relative_distance_from_normal( - time_mode.value, + time_mode, normal_year=normal_year, begin_year=begin_year, end_year=end_year, @@ -162,7 +163,7 @@ async def get_rainfall_standard_deviation( return RainfallModel( name=f"rainfall standard deviation {"weighted by average" if weigh_by_average else "(mm)"}", value=all_rainfall.get_rainfall_standard_deviation( - time_mode.value, + time_mode, begin_year=begin_year, end_year=end_year, month=month.value if month else None, @@ -201,7 +202,7 @@ async def get_years_below_normal( return YearWithNormalModel( name="years below rainfall normal", value=all_rainfall.get_years_below_normal( - time_mode.value, + time_mode, normal_year=normal_year, begin_year=begin_year, end_year=end_year, @@ -241,7 +242,7 @@ async def get_years_above_normal( return YearWithNormalModel( name="years above rainfall normal", value=all_rainfall.get_years_above_normal( - time_mode.value, + time_mode, normal_year=normal_year, begin_year=begin_year, end_year=end_year, @@ -280,7 +281,7 @@ def get_minimal_csv( csv_str = ( all_rainfall.export_as_csv( - time_mode.value, + time_mode, begin_year=begin_year, end_year=end_year, month=month_value, @@ -319,7 +320,7 @@ def get_rainfall_averages( end_year = end_year or all_rainfall.get_last_year() averages = all_rainfall.bar_rainfall_averages( - time_mode=time_mode.value, begin_year=begin_year, end_year=end_year + time_mode=time_mode, begin_year=begin_year, end_year=end_year ) if averages is None: raise HTTPException( @@ -358,7 +359,7 @@ def get_rainfall_linreg_slopes( end_year = end_year or all_rainfall.get_last_year() linreg_slopes = all_rainfall.bar_rainfall_linreg_slopes( - time_mode=time_mode.value, begin_year=begin_year, end_year=end_year + time_mode=time_mode, begin_year=begin_year, end_year=end_year ) if linreg_slopes is None: raise HTTPException( diff --git a/src/core/models/all_rainfall.py b/src/core/models/all_rainfall.py index 3936280..51ce685 100644 --- a/src/core/models/all_rainfall.py +++ b/src/core/models/all_rainfall.py @@ -115,7 +115,7 @@ def export_all_data_to_csv( def export_as_csv( self, - time_mode: str, + time_mode: TimeMode, *, begin_year: int, end_year: int | None = None, @@ -127,7 +127,7 @@ def export_as_csv( Export the data state of a specific time mode as a CSV. Could be for a yearly time frame, a specific month or a given season. - :param time_mode: A string setting the time period ['yearly', 'monthly', 'seasonal']. + :param time_mode: A TimeMode Enum: ['yearly', 'monthly', 'seasonal']. :param begin_year: An integer representing the year to start getting our rainfall values. :param end_year: An integer representing the year @@ -150,7 +150,7 @@ def export_as_csv( def get_average_rainfall( self, - time_mode: str, + time_mode: TimeMode, *, begin_year: int, end_year: int | None = None, @@ -160,7 +160,7 @@ def get_average_rainfall( """ Computes Rainfall average for a specific year range and time mode. - :param time_mode: A string setting the time period ['yearly', 'monthly', 'seasonal']. + :param time_mode: A TimeMode Enum: ['yearly', 'monthly', 'seasonal']. :param begin_year: An integer representing the year to start getting our rainfall values. :param end_year: An integer representing the year @@ -179,7 +179,7 @@ def get_average_rainfall( def get_normal( self, - time_mode: str, + time_mode: TimeMode, begin_year: int, month: str | None = None, season: str | None = None, @@ -187,7 +187,7 @@ def get_normal( """ Computes Rainfall normal from a specific year and time mode. - :param time_mode: A string setting the time period ['yearly', 'monthly', 'seasonal']. + :param time_mode: A TimeMode Enum: ['yearly', 'monthly', 'seasonal']. :param begin_year: An integer representing the year to start computing rainfall normal. :param month: A string corresponding to the month name. @@ -205,7 +205,7 @@ def get_normal( def get_relative_distance_from_normal( self, - time_mode: str, + time_mode: TimeMode, normal_year: int, begin_year: int, end_year: int | None = None, @@ -215,7 +215,7 @@ def get_relative_distance_from_normal( """ Computes relative distance to Rainfall normal for a specific year range and time mode. - :param time_mode: A string setting the time period ['yearly', 'monthly', 'seasonal']. + :param time_mode: A TimeMode Enum: ['yearly', 'monthly', 'seasonal']. :param normal_year: An integer representing the year to start computing the 30 years normal of the rainfall. :param begin_year: An integer representing the year @@ -238,7 +238,7 @@ def get_relative_distance_from_normal( def get_rainfall_standard_deviation( self, - time_mode: str, + time_mode: TimeMode, begin_year: int, end_year: int | None = None, month: str | None = None, @@ -250,7 +250,7 @@ def get_rainfall_standard_deviation( for a specific year range and time mode. By default, it uses the 'Rainfall' column. - :param time_mode: A string setting the time period ['yearly', 'monthly', 'seasonal']. + :param time_mode: A TimeMode Enum: ['yearly', 'monthly', 'seasonal']. :param begin_year: An integer representing the year to start getting our rainfall values (optional). :param end_year: An integer representing the year @@ -274,7 +274,7 @@ def get_rainfall_standard_deviation( def get_years_below_normal( self, - time_mode: str, + time_mode: TimeMode, normal_year: int, begin_year: int, end_year: int | None = None, @@ -284,7 +284,7 @@ def get_years_below_normal( """ Computes the number of years below rainfall normal for a specific year range and time mode. - :param time_mode: A string setting the time period ['yearly', 'monthly', 'seasonal']. + :param time_mode: A TimeMode Enum: ['yearly', 'monthly', 'seasonal']. :param normal_year: An integer representing the year to start computing the 30 years normal of the rainfall. :param begin_year: An integer representing the year @@ -305,7 +305,7 @@ def get_years_below_normal( def get_years_above_normal( self, - time_mode: str, + time_mode: TimeMode, normal_year: int, begin_year: int, end_year: int | None = None, @@ -315,7 +315,7 @@ def get_years_above_normal( """ Computes the number of years above rainfall normal for a specific year range and time mode. - :param time_mode: A string setting the time period ['yearly', 'monthly', 'seasonal']. + :param time_mode: A TimeMode Enum: ['yearly', 'monthly', 'seasonal']. :param normal_year: An integer representing the year to start computing the 30 years normal of the rainfall. :param begin_year: An integer representing the year @@ -346,14 +346,14 @@ def get_last_year(self) -> int: def bar_rainfall_averages( self, - time_mode: str, + time_mode: TimeMode, begin_year: int, end_year: int | None = None, ) -> list | None: """ Plots a bar graphic displaying average rainfall for each month or each season. - :param time_mode: A string setting the time period ['monthly', 'seasonal']. + :param time_mode: A TimeMode Enum: ['monthly', 'seasonal']. :param begin_year: An integer representing the year to start getting our rainfall values. :param end_year: An integer representing the year @@ -364,14 +364,14 @@ def bar_rainfall_averages( end_year = end_year or self.get_last_year() label = f"Average rainfall (mm) between {begin_year} and {end_year}" - if time_mode == TimeMode.MONTHLY.value: + if time_mode == TimeMode.MONTHLY: return plotting.bar_monthly_rainfall_averages( list(self.monthly_rainfalls.values()), begin_year=begin_year, end_year=end_year, label=label, ) - elif time_mode == TimeMode.SEASONAL.value: + elif time_mode == TimeMode.SEASONAL: return plotting.bar_seasonal_rainfall_averages( list(self.seasonal_rainfalls.values()), begin_year=begin_year, @@ -383,14 +383,14 @@ def bar_rainfall_averages( def bar_rainfall_linreg_slopes( self, - time_mode: str, + time_mode: TimeMode, begin_year: int, end_year: int | None = None, ) -> list | None: """ Plots a bar graphic displaying linear regression slope for each month or each season. - :param time_mode: A string setting the time period ['monthly', 'seasonal']. + :param time_mode: A TimeMode Enum: ['monthly', 'seasonal']. :param begin_year: An integer representing the year to start getting our rainfall values. :param end_year: An integer representing the year @@ -401,13 +401,13 @@ def bar_rainfall_linreg_slopes( """ end_year = end_year or self.get_last_year() - if time_mode == TimeMode.MONTHLY.value: + if time_mode == TimeMode.MONTHLY: return plotting.bar_monthly_rainfall_linreg_slopes( list(self.monthly_rainfalls.values()), begin_year=begin_year, end_year=end_year, ) - elif time_mode == TimeMode.SEASONAL.value: + elif time_mode == TimeMode.SEASONAL: return plotting.bar_seasonal_rainfall_linreg_slopes( list(self.seasonal_rainfalls.values()), begin_year=begin_year, @@ -417,14 +417,14 @@ def bar_rainfall_linreg_slopes( return None def get_entity_for_time_mode( - self, time_mode: str, month: str | None = None, season: str | None = None + self, time_mode: TimeMode, month: str | None = None, season: str | None = None ) -> YearlyRainfall | MonthlyRainfall | SeasonalRainfall | None: """ Retrieve current entity for specified time mode, amongst instances of YearlyRainfall, MonthlyRainfall or SeasonsalRainfall. Month or Season should be specified according to time mode. - :param time_mode: A string setting the time period ['yearly', 'monthly', 'seasonal']. + :param time_mode: A TimeMode Enum: ['yearly', 'monthly', 'seasonal']. :param month: A string corresponding to the month name. Set if time_mode is 'monthly' (optional). :param season: A string corresponding to the season name. @@ -436,11 +436,11 @@ def get_entity_for_time_mode( """ entity: YearlyRainfall | MonthlyRainfall | SeasonalRainfall | None = None - if time_mode.casefold() == TimeMode.YEARLY.value: + if time_mode == TimeMode.YEARLY: entity = self.yearly_rainfall - elif time_mode.casefold() == TimeMode.MONTHLY.value and month: + elif time_mode == TimeMode.MONTHLY and month: entity = self.monthly_rainfalls[month] - elif time_mode.casefold() == TimeMode.SEASONAL.value and season: + elif time_mode == TimeMode.SEASONAL and season: entity = self.seasonal_rainfalls[season] return entity diff --git a/src/core/utils/functions/dataframe_operations.py b/src/core/utils/functions/dataframe_operations.py index b254a1e..89e26fc 100644 --- a/src/core/utils/functions/dataframe_operations.py +++ b/src/core/utils/functions/dataframe_operations.py @@ -22,8 +22,7 @@ def get_rainfall_within_year_interval( to start getting our rainfall values. :param end_year: An integer representing the year to end getting our rainfall values (optional). - :return: A pandas DataFrame displaying rainfall data (in mm) - according to year. + :return: A pandas DataFrame displaying rainfall data (in mm) according to year. """ if end_year is not None: yearly_rainfall = yearly_rainfall[yearly_rainfall[Label.YEAR.value] <= end_year] diff --git a/tst/core/models/test_all_rainfall.py b/tst/core/models/test_all_rainfall.py index d084569..40a3760 100644 --- a/tst/core/models/test_all_rainfall.py +++ b/tst/core/models/test_all_rainfall.py @@ -8,9 +8,8 @@ from src.core.utils.enums.months import Month from src.core.utils.enums.seasons import Season from src.core.utils.enums.time_modes import TimeMode -from tst.test_config import config -ALL_RAINFALL = AllRainfall(config.get_dataset_url()) +ALL_RAINFALL = AllRainfall.from_config() normal_year = 1971 begin_year = 1991 @@ -35,7 +34,7 @@ def test_export_all_data_to_csv(): @staticmethod def test_get_average_rainfall(): - for t_mode in TimeMode.values(): + for t_mode in TimeMode: avg_rainfall = ALL_RAINFALL.get_average_rainfall( t_mode, begin_year=begin_year, @@ -48,14 +47,14 @@ def test_get_average_rainfall(): @staticmethod def test_get_normal(): - for t_mode in TimeMode.values(): + for t_mode in TimeMode: normal = ALL_RAINFALL.get_normal(t_mode, begin_year, month, season) assert isinstance(normal, float) @staticmethod def test_get_years_below_normal(): - for t_mode in TimeMode.values(): + for t_mode in TimeMode: n_years_below_avg = ALL_RAINFALL.get_years_below_normal( t_mode, normal_year, begin_year, end_year, month, season ) @@ -65,7 +64,7 @@ def test_get_years_below_normal(): @staticmethod def test_get_years_above_normal(): - for t_mode in TimeMode.values(): + for t_mode in TimeMode: n_years_above_avg = ALL_RAINFALL.get_years_above_normal( t_mode, normal_year, begin_year, end_year, month, season ) @@ -79,7 +78,7 @@ def test_get_last_year(): @staticmethod def test_get_relative_distance_from_normal(): - for t_mode in TimeMode.values(): + for t_mode in TimeMode: relative_distance = ALL_RAINFALL.get_relative_distance_from_normal( t_mode, normal_year, begin_year, end_year, month, season ) @@ -89,7 +88,7 @@ def test_get_relative_distance_from_normal(): @staticmethod def test_get_standard_deviation(): - for t_mode in TimeMode.values(): + for t_mode in TimeMode: std = ALL_RAINFALL.get_rainfall_standard_deviation( t_mode, begin_year, end_year, month, season ) @@ -99,18 +98,14 @@ def test_get_standard_deviation(): @staticmethod def test_get_entity_for_time_mode(): assert isinstance( - ALL_RAINFALL.get_entity_for_time_mode(TimeMode.YEARLY.value), YearlyRainfall + ALL_RAINFALL.get_entity_for_time_mode(TimeMode.YEARLY), YearlyRainfall ) assert isinstance( - ALL_RAINFALL.get_entity_for_time_mode( - TimeMode.SEASONAL.value, season=Season.SPRING.value - ), + ALL_RAINFALL.get_entity_for_time_mode(TimeMode.SEASONAL, season=season), SeasonalRainfall, ) assert isinstance( - ALL_RAINFALL.get_entity_for_time_mode( - TimeMode.MONTHLY.value, month=Month.OCTOBER.value - ), + ALL_RAINFALL.get_entity_for_time_mode(TimeMode.MONTHLY, month=month), MonthlyRainfall, ) assert ALL_RAINFALL.get_entity_for_time_mode("unknown_time_mode") is None From 6f4a5890a0a39581c84eb6a9d46dbe8b1a252ffd Mon Sep 17 00:00:00 2001 From: polochinoc Date: Sat, 21 Sep 2024 17:51:02 +0200 Subject: [PATCH 4/4] Label bar plots --- src/core/utils/functions/plotting.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/core/utils/functions/plotting.py b/src/core/utils/functions/plotting.py index ff6d674..0396d32 100644 --- a/src/core/utils/functions/plotting.py +++ b/src/core/utils/functions/plotting.py @@ -75,11 +75,12 @@ def bar_column_according_to_year(yearly_rainfall: pd.DataFrame, label: Label) -> ): return False - plt.bar( + bar_plot = plt.bar( yearly_rainfall[Label.YEAR.value], yearly_rainfall[label.value], label=label.value, ) + plt.bar_label(bar_plot) return True @@ -113,7 +114,8 @@ def bar_monthly_rainfall_averages( ) ) - plt.bar(month_labels, averages, label=label) + bar_plot = plt.bar(month_labels, averages, label=label) + plt.bar_label(bar_plot) plt.legend() return averages @@ -144,11 +146,12 @@ def bar_monthly_rainfall_linreg_slopes( )[1] ) - plt.bar( + bar_plot = plt.bar( month_labels, slopes, label=f"Linear Regression slope (mm/year) between {begin_year} and {end_year}", ) + plt.bar_label(bar_plot) plt.legend() return slopes @@ -183,7 +186,8 @@ def bar_seasonal_rainfall_averages( ) ) - plt.bar(season_labels, averages, label=label) + bar_plot = plt.bar(season_labels, averages, label=label) + plt.bar_label(bar_plot) plt.legend() return averages @@ -214,11 +218,12 @@ def bar_seasonal_rainfall_linreg_slopes( )[1] ) - plt.bar( + bar_plot = plt.bar( season_labels, slopes, label=f"Linear Regression slope (mm/year) between {begin_year} and {end_year}", ) + plt.bar_label(bar_plot) plt.legend() return slopes