diff --git a/conflowgen/analyses/abstract_analysis.py b/conflowgen/analyses/abstract_analysis.py index e8e466eb..c5fbad4d 100644 --- a/conflowgen/analyses/abstract_analysis.py +++ b/conflowgen/analyses/abstract_analysis.py @@ -98,35 +98,39 @@ def _restrict_storage_requirement(selected_containers: ModelSelect, storage_requ @staticmethod def _restrict_container_delivered_by_vehicle_type( selected_containers: ModelSelect, container_delivered_by_vehicle_type: typing.Any - ) -> ModelSelect: + ) -> (ModelSelect, list[ModeOfTransport]): if hashable(container_delivered_by_vehicle_type) \ and container_delivered_by_vehicle_type in set(ModeOfTransport): selected_containers = selected_containers.where( Container.delivered_by == container_delivered_by_vehicle_type ) + list_of_vehicle_types = [container_delivered_by_vehicle_type] else: # assume it is some kind of collection (list, set, ...) selected_containers = selected_containers.where( Container.delivered_by << container_delivered_by_vehicle_type ) - return selected_containers + list_of_vehicle_types = list(container_delivered_by_vehicle_type) + return selected_containers, list_of_vehicle_types @staticmethod def _restrict_container_picked_up_by_vehicle_type( selected_containers: ModelSelect, container_picked_up_by_vehicle_type: typing.Any - ) -> ModelSelect: + ) -> (ModelSelect, list[ModeOfTransport]): if container_picked_up_by_vehicle_type == "scheduled vehicles": container_picked_up_by_vehicle_type = ModeOfTransport.get_scheduled_vehicles() - + list_of_vehicle_types = container_picked_up_by_vehicle_type if hashable(container_picked_up_by_vehicle_type) \ and container_picked_up_by_vehicle_type in set(ModeOfTransport): selected_containers = selected_containers.where( Container.picked_up_by == container_picked_up_by_vehicle_type ) + list_of_vehicle_types = [container_picked_up_by_vehicle_type] else: # assume it is some kind of collection (list, set, ...) selected_containers = selected_containers.where( Container.picked_up_by << container_picked_up_by_vehicle_type ) - return selected_containers + list_of_vehicle_types = list(container_picked_up_by_vehicle_type) + return selected_containers, list_of_vehicle_types @staticmethod def _restrict_container_picked_up_by_initial_vehicle_type( diff --git a/conflowgen/analyses/container_dwell_time_analysis.py b/conflowgen/analyses/container_dwell_time_analysis.py index 822e42b7..8a30db70 100644 --- a/conflowgen/analyses/container_dwell_time_analysis.py +++ b/conflowgen/analyses/container_dwell_time_analysis.py @@ -64,12 +64,12 @@ def get_container_dwell_times( ) if container_delivered_by_vehicle_type != "all": - selected_containers = self._restrict_container_delivered_by_vehicle_type( + selected_containers, vehicle_types = self._restrict_container_delivered_by_vehicle_type( selected_containers, container_delivered_by_vehicle_type ) if container_picked_up_by_vehicle_type != "all": - selected_containers = self._restrict_container_picked_up_by_vehicle_type( + selected_containers, vehicle_types = self._restrict_container_picked_up_by_vehicle_type( selected_containers, container_picked_up_by_vehicle_type ) diff --git a/conflowgen/analyses/container_flow_by_vehicle_instance_analysis.py b/conflowgen/analyses/container_flow_by_vehicle_instance_analysis.py index 780bf87b..4e92e4d6 100644 --- a/conflowgen/analyses/container_flow_by_vehicle_instance_analysis.py +++ b/conflowgen/analyses/container_flow_by_vehicle_instance_analysis.py @@ -3,6 +3,8 @@ import datetime import typing +from peewee import ModelSelect + from conflowgen.data_summaries.data_summaries_cache import DataSummariesCache from conflowgen.domain_models.container import Container from conflowgen.domain_models.data_types.mode_of_transport import ModeOfTransport @@ -17,27 +19,34 @@ class ContainerFlowByVehicleInstanceAnalysis(AbstractAnalysis): as it is the case with :class:`.ContainerFlowByVehicleInstanceAnalysisReport`. """ - @staticmethod @DataSummariesCache.cache_result def get_container_flow_by_vehicle( + self, + vehicle_types: typing.Collection[ModeOfTransport] = ( + ModeOfTransport.train, + ModeOfTransport.feeder, + ModeOfTransport.deep_sea_vessel, + ModeOfTransport.barge + ), start_date: typing.Optional[datetime.datetime] = None, - end_date: typing.Optional[datetime.datetime] = None - ) -> typing.Dict[ + end_date: typing.Optional[datetime.datetime] = None, + ) -> [ ModeOfTransport, typing.Dict[VehicleIdentifier, typing.Dict[FlowDirection, (int, int)]] ]: """ - This shows for each of the vehicles - Args: + vehicle_types: A collection of vehicle types, e.g., passed as a :class:`list` or :class:`set`. + Only the vehicles that correspond to the provided vehicle type(s) are considered in the analysis. start_date: The earliest arriving container that is included. Consider all containers if :obj:`None`. end_date: The latest departing container that is included. Consider all containers if :obj:`None`. Returns: - Grouped by vehicle type and vehicle instance, how much import, export, and transshipment is unloaded and - loaded. + Grouped by vehicle type and vehicle instance, how many import, export, and transshipment containers are + unloaded and loaded (measured in TEU). """ + container_flow_by_vehicle: typing.Dict[ ModeOfTransport, typing.Dict[VehicleIdentifier, typing.Dict[FlowDirection, typing.Dict[str, int]]]] = { @@ -47,8 +56,12 @@ def get_container_flow_by_vehicle( vehicle_identifier_cache = {} + selected_containers: ModelSelect = Container.select().where( + (Container.delivered_by.in_(vehicle_types) | Container.picked_up_by.in_(vehicle_types)) + ) + container: Container - for container in Container.select(): + for container in selected_containers: if start_date and container.get_arrival_time() < start_date: continue if end_date and container.get_departure_time() > end_date: @@ -77,7 +90,8 @@ def get_container_flow_by_vehicle( for flow_direction in FlowDirection } container_flow_by_vehicle[ - container.delivered_by][vehicle_id_inbound][container.flow_direction]["inbound"] += 1 + container.delivered_by + ][vehicle_id_inbound][container.flow_direction]["inbound"] += container.occupied_teu if container.picked_up_by_large_scheduled_vehicle is not None: # if not transported by truck @@ -104,4 +118,8 @@ def get_container_flow_by_vehicle( container_flow_by_vehicle[ container.picked_up_by][vehicle_id_outbound][container.flow_direction]["outbound"] += 1 + for skipped_vehicle_type in set(ModeOfTransport) - set(vehicle_types): + assert len(container_flow_by_vehicle[skipped_vehicle_type]) == 0 + del container_flow_by_vehicle[skipped_vehicle_type] + return container_flow_by_vehicle diff --git a/conflowgen/analyses/container_flow_by_vehicle_instance_analysis_report.py b/conflowgen/analyses/container_flow_by_vehicle_instance_analysis_report.py index 10e59360..a6327e61 100644 --- a/conflowgen/analyses/container_flow_by_vehicle_instance_analysis_report.py +++ b/conflowgen/analyses/container_flow_by_vehicle_instance_analysis_report.py @@ -5,10 +5,10 @@ import matplotlib.figure import matplotlib.pyplot as plt +import numpy as np import pandas as pd from conflowgen.analyses.container_flow_by_vehicle_instance_analysis import ContainerFlowByVehicleInstanceAnalysis -from conflowgen.descriptive_datatypes import VehicleIdentifier, FlowDirection from conflowgen.domain_models.data_types.mode_of_transport import ModeOfTransport from conflowgen.reporting import AbstractReportWithMatplotlib from conflowgen.reporting.no_data_plot import no_data_graph @@ -35,10 +35,15 @@ def __init__(self): self.analysis = ContainerFlowByVehicleInstanceAnalysis() def get_report_as_text( - self, **kwargs + self, + vehicle_types: ModeOfTransport | str | typing.Collection = "scheduled vehicles", + **kwargs ) -> str: """ Keyword Args: + vehicle_types (typing.Collection[ModeOfTransport]): A collection of vehicle types, e.g., passed as a + :class:`list` or :class:`set`. + Only the vehicles that correspond to the provided vehicle type(s) are considered in the analysis. start_date (datetime.datetime): The earliest arriving container that is included. Consider all containers if :obj:`None`. end_date (datetime.datetime): The latest departing container that is included. @@ -47,38 +52,39 @@ def get_report_as_text( Returns: The report in human-readable text format """ - container_flow = self._get_analysis(kwargs) + plain_table, vehicle_types = self._get_analysis(kwargs) - report = "(pending)" + if len(plain_table) > 0: + df = self._get_dataframe_from_plain_table(plain_table) + report = str(df) + else: + report = "(no report feasible because no data is available)" - # create string representation return report - def _get_analysis(self, kwargs: dict) -> typing.Dict[ - ModeOfTransport, typing.Dict[VehicleIdentifier, typing.Dict[FlowDirection, (int, int)]] - ]: + def _get_analysis(self, kwargs: dict) -> (list, list): start_date = kwargs.pop("start_date", None) end_date = kwargs.pop("end_date", None) + vehicle_types = kwargs.pop("vehicle_types", ( + ModeOfTransport.train, + ModeOfTransport.feeder, + ModeOfTransport.deep_sea_vessel, + ModeOfTransport.barge + )) + assert len(kwargs) == 0, f"Keyword(s) {kwargs.keys()} have not been processed" container_flow = self.analysis.get_container_flow_by_vehicle( + vehicle_types=vehicle_types, start_date=start_date, - end_date=end_date + end_date=end_date, ) - return container_flow - - def get_report_as_graph(self, **kwargs) -> matplotlib.figure.Figure: - """ - Visualize the container flows (import, export, transshipment) over time. - Returns: - The diagram. - """ - container_flow = self._get_analysis(kwargs) + vehicle_types = list(container_flow.keys()) plain_table = [] - for mode_of_transport in (set(container_flow.keys()) - set([ModeOfTransport.truck])): + for mode_of_transport in vehicle_types: for vehicle_identifier in container_flow[mode_of_transport].keys(): for flow_direction in container_flow[mode_of_transport][vehicle_identifier]: for journey_direction in container_flow[mode_of_transport][vehicle_identifier][flow_direction]: @@ -91,6 +97,25 @@ def get_report_as_graph(self, **kwargs) -> matplotlib.figure.Figure: str(flow_direction), journey_direction, handled_volume) ) + return plain_table, vehicle_types + + def get_report_as_graph(self, **kwargs) -> matplotlib.figure.Figure: + """ + Keyword Args: + vehicle_types (typing.Collection[ModeOfTransport]): A collection of vehicle types, e.g., passed as a + :class:`list` or :class:`set`. + Only the vehicles that correspond to the provided vehicle type(s) are considered in the analysis. + start_date (datetime.datetime): + The earliest arriving container that is included. Consider all containers if :obj:`None`. + end_date (datetime.datetime): + The latest departing container that is included. Consider all containers if :obj:`None`. + + Returns: + Grouped by vehicle type and vehicle instance, how many import, export, and transshipment containers are + unloaded and loaded (measured in TEU). + """ + plain_table, vehicle_types = self._get_analysis(kwargs) + plot_title = "Container Flow By Vehicle Instance Analysis Report" if len(plain_table) == 0: @@ -98,28 +123,33 @@ def get_report_as_graph(self, **kwargs) -> matplotlib.figure.Figure: ax.set_title(plot_title) return fig - column_names = ("mode_of_transport", "vehicle_id", "vehicle_name", "service_name", "vehicle_arrival_time", - "flow_direction", "journey_direction", "handled_volume") - - df = pd.DataFrame(plain_table) - df.columns = column_names - df.set_index("vehicle_arrival_time", inplace=True) - - self._df = df + df = self._get_dataframe_from_plain_table(plain_table) - fig, axes = plt.subplots(nrows=2 * (len(ModeOfTransport) - 1), figsize=(7, 20)) + number_subplots = 2 * len(vehicle_types) + fig, axes = plt.subplots(nrows=number_subplots, figsize=(7, 4 * number_subplots)) i = 0 - for mode_of_transport in (set(ModeOfTransport) - set([ModeOfTransport.truck])): + for mode_of_transport in vehicle_types: for journey_direction in ["inbound", "outbound"]: ax = axes[i] i += 1 - ax.set_title(f"{mode_of_transport} - {journey_direction}") + ax.set_title(f"{str(mode_of_transport).replace('_', ' ').capitalize()} - {journey_direction}") df[(df["mode_of_transport"] == mode_of_transport) & (df["journey_direction"] == journey_direction) ].groupby("flow_direction")["handled_volume"].plot(ax=ax, linestyle=":", marker=".") + ax.set_ylabel("") plt.legend(loc='center left', bbox_to_anchor=(1, 0.5)) plt.tight_layout() return fig + + def _get_dataframe_from_plain_table(self, plain_table): + column_names = ("mode_of_transport", "vehicle_id", "vehicle_name", "service_name", "vehicle_arrival_time", + "flow_direction", "journey_direction", "handled_volume") + df = pd.DataFrame(plain_table) + df.columns = column_names + df.set_index("vehicle_arrival_time", inplace=True) + df.replace(0, np.nan, inplace=True) + self._df = df + return df diff --git a/conflowgen/analyses/container_flow_vehicle_type_adjustment_per_vehicle_analysis.py b/conflowgen/analyses/container_flow_vehicle_type_adjustment_per_vehicle_analysis.py index a2672a1c..f77ea071 100644 --- a/conflowgen/analyses/container_flow_vehicle_type_adjustment_per_vehicle_analysis.py +++ b/conflowgen/analyses/container_flow_vehicle_type_adjustment_per_vehicle_analysis.py @@ -61,7 +61,7 @@ def get_vehicle_type_adjustments_per_vehicle( ) if adjusted_vehicle_type is not None and adjusted_vehicle_type != "all": - selected_containers = self._restrict_container_picked_up_by_vehicle_type( + selected_containers, list_of_vehicle_types = self._restrict_container_picked_up_by_vehicle_type( selected_containers, adjusted_vehicle_type ) diff --git a/conflowgen/analyses/modal_split_analysis_report.py b/conflowgen/analyses/modal_split_analysis_report.py index 441e3bbd..1f149d9e 100644 --- a/conflowgen/analyses/modal_split_analysis_report.py +++ b/conflowgen/analyses/modal_split_analysis_report.py @@ -18,8 +18,8 @@ class ModalSplitAnalysisReport(AbstractReportWithMatplotlib): """ report_description = """ - Analyze the amount of containers dedicated for or coming from the hinterland compared to the amount of containers - that are transshipment. + Analyze how many containers are delivered and picked up by which type of vehicle. + This counts the containers in the yard, i.e., transshipment containers are *not* counted twice. """ def __init__(self):