Exemplo n.º 1
0
    def table_client_demand(self, start_date=None, end_date=None, freq="AS-APR"):
        start_date, end_date, freq = self.get_time_parameters(
            start_date, end_date, freq
        )

        project_ids = self.wim.project_confirmed.columns

        clients = []
        for project in project_ids:
            client_id = self.wim.projects.loc[project, "client"]

            if (
                not np.isnan(client_id)
                and self.wim.clients.loc[client_id, "name"] != "UNAVAILABLE"
            ):

                clients.append(self.wim.clients.loc[client_id, "name"])

            else:
                clients.append("NaN")

        client_meanfte = self.wim.project_confirmed.copy()

        client_meanfte = select_date_range(
            client_meanfte, start_date, end_date, drop_zero_cols=False
        )

        client_meanfte = client_meanfte.groupby(clients, axis=1).sum()
        client_meanfte = client_meanfte.resample(freq).mean()

        client_meanfte = client_meanfte.loc[:, client_meanfte.sum() > 0]

        client_meanfte = client_meanfte.T
        client_meanfte.drop("NaN", inplace=True)

        # strip time from column names for prettier printing
        client_meanfte.columns = client_meanfte.columns.date

        return client_meanfte
Exemplo n.º 2
0
 def _get_association_capacities(self, start_date, end_date, freq):
     capacity = self.wim.people_capacities.copy()
     # change name indices to association indices
     capacity.columns = self.wim.people.association.loc[
         self.wim.people_capacities.columns
     ]
     # groupby association (transpose then revert below so groupby can apply to rows)
     capacity = capacity.T.groupby("association").sum().T
     # replace
     capacity.columns = self.wim.associations.loc[capacity.columns, "name"]
     # order columns
     capacity = capacity[
         [
             "REG Director",
             "REG Principal",
             "REG Senior",
             "REG Standard",
             "REG Junior",
             "REG Associate",
             "University Partner",
         ]
     ]
     capacity = capacity.resample(freq).mean()
     return select_date_range(capacity, start_date, end_date, drop_zero_cols=False)
Exemplo n.º 3
0
    def plot_forecast_harvest(
        self,
        project_id,
        start_date=None,
        end_date=None,
        freq="W-MON",
        err_bar=True,
        err_size=0.2,
        stack=False,
        units="Hours",
    ):

        """compare planned time for a project in forecast to tracked time in harvest.
        If stack is True: Area plot for harvest data split into each person's
        contribution.
        If err_bar is True: Add lines representing forecast projection
        +/- err_size*100 %"""
        start_date, end_date, freq = self.get_time_parameters(
            start_date, end_date, freq
        )

        # NB scale forecast fte by using harvest hours per day property (default 6.4)
        fc_totals = (
            self.wim.proj_hrs_per_day * self.wim.project_confirmed[project_id].copy()
        )
        fc_totals = fc_totals.resample(freq).sum().cumsum()
        fc_totals = select_date_range(
            fc_totals, start_date, end_date, drop_zero_cols=False
        )

        if stack:
            hv_totals = self.wim.tracked_project_people[project_id].copy()
            hv_totals = hv_totals.resample(freq).sum().cumsum()
            hv_totals = select_date_range(
                hv_totals, start_date, end_date, drop_zero_cols=True
            )
            hv_totals.columns = [
                self.wim.get_person_name(idx) for idx in hv_totals.columns
            ]
        else:
            hv_totals = self.wim.tracked_project_totals[project_id].copy()
            hv_totals = hv_totals.resample(freq).sum().cumsum()
            hv_totals = select_date_range(
                hv_totals, start_date, end_date, drop_zero_cols=False
            )

        if (fc_totals == 0).all(axis=None) & (hv_totals == 0).all(axis=None):
            raise ValueError("forecast_id " + str(project_id) + " no data to plot.")

        if units == "FTE days":
            fc_totals = fc_totals / self.wim.work_hrs_per_day
            hv_totals = hv_totals / self.wim.work_hrs_per_day
        elif units == "FTE weeks":
            fc_totals = fc_totals / (self.wim.work_hrs_per_day * 5)
            hv_totals = hv_totals / (self.wim.work_hrs_per_day * 5)
        elif units == "FTE months":
            fc_totals = fc_totals / (self.wim.work_hrs_per_day * 5 * 52 / 12)
            hv_totals = hv_totals / (self.wim.work_hrs_per_day * 5 * 52 / 12)
        else:
            units = "Hours"

        try:
            fig = plt.figure(figsize=(10, 10))
            ax = fig.gca()

            fc_totals.plot(ax=ax, label="Forecast", linewidth=3, color="k")

            if err_bar:
                ((1 + err_size) * fc_totals).plot(
                    linestyle="--",
                    linewidth=1,
                    color="k",
                    label="Forecast +/- {:.0%}".format(err_size),
                )
                ((1 - err_size) * fc_totals).plot(
                    linestyle="--", linewidth=1, color="k", label=""
                )

            if stack:
                hv_totals.plot.area(ax=ax)
            else:
                hv_totals.plot(ax=ax, label="Harvest", linewidth=3, color="r")

            plt.xlim([start_date, end_date])
            plt.ylabel(units)
            plt.legend()
            plt.title(self.wim.get_project_name(project_id))

            return fig

        except ValueError:
            plt.close(fig)
            raise ValueError("project " + str(project_id) + " plot failed")
        except TypeError:
            plt.close(fig)
            raise TypeError("project " + str(project_id) + " plot failed")
Exemplo n.º 4
0
    def _get_grouped_demand(self, start_date, end_date, freq):
        # Get totals for REG management, development and support clients
        corp_duties_idx = self.wim.get_client_id("Corporate Duties")
        reg_service_idx = self.wim.get_client_id("REG Service Areas")
        reg_management_idx = self.wim.get_client_id("REG Management")
        reg_dev_idx = self.wim.get_client_id("REG Development Work")
        turing_service_idx = self.wim.get_client_id("Turing Service Areas")
        turing_prog_idx = self.wim.get_client_id("Turing Programme Support")

        corp_duties_projs = self.wim.projects[
            self.wim.projects.client == corp_duties_idx
        ].index
        reg_service_projs = self.wim.projects[
            self.wim.projects.client == reg_service_idx
        ].index
        reg_management_projs = self.wim.projects[
            self.wim.projects.client == reg_management_idx
        ].index
        reg_dev_projs = self.wim.projects[self.wim.projects.client == reg_dev_idx].index
        turing_service_projs = self.wim.projects[
            self.wim.projects.client == turing_service_idx
        ].index
        turing_prog_projs = self.wim.projects[
            self.wim.projects.client == turing_prog_idx
        ].index

        corp_duties_reqs = self.wim.project_confirmed[corp_duties_projs].sum(axis=1)
        reg_service_reqs = self.wim.project_confirmed[reg_service_projs].sum(axis=1)
        reg_management_reqs = self.wim.project_confirmed[reg_management_projs].sum(
            axis=1
        )
        reg_dev_reqs = self.wim.project_confirmed[reg_dev_projs].sum(axis=1)
        turing_service_reqs = self.wim.project_confirmed[turing_service_projs].sum(
            axis=1
        )
        turing_prog_reqs = self.wim.project_confirmed[turing_prog_projs].sum(axis=1)

        # Get overall totals
        unavail_client = self.wim.get_client_id("UNAVAILABLE")
        unavail_project_ids = self.wim.get_client_projects(unavail_client)
        project_confirmed = self.wim.project_confirmed.drop(
            [
                proj
                for proj in self.wim.project_confirmed.columns
                if proj in unavail_project_ids
            ],
            axis=1,
        )
        project_confirmed = project_confirmed.sum(axis=1)

        # project_confirmed = total for all non-research support, REG management or
        # REG development projects
        project_confirmed = (
            project_confirmed
            - corp_duties_reqs
            - reg_management_reqs
            - reg_dev_reqs
            - reg_service_reqs
            - turing_service_reqs
            - turing_prog_reqs
        )

        unconfirmed = self.wim.project_unconfirmed.sum(axis=1)
        deferred = self.wim.project_deferred.sum(axis=1)
        notfunded = self.wim.project_notfunded.sum(axis=1)

        demand = pd.DataFrame(
            {
                "Corporate Duties": corp_duties_reqs,
                "REG Management": reg_management_reqs,
                "REG Development": reg_dev_reqs,
                "REG Service Areas": reg_service_reqs,
                "Turing Service Areas": turing_service_reqs,
                "Turing Programme Support": turing_prog_reqs,
                "Confirmed projects": project_confirmed,
                "Projects with funder": unconfirmed,
                "Deferred projects": deferred,
                "Not Funded projects": notfunded,
            }
        )
        demand = select_date_range(demand, start_date, end_date, drop_zero_cols=False)
        return demand.resample(freq).mean()
Exemplo n.º 5
0
    def plot_allocations(
        self, id_value, id_type, start_date=None, end_date=None, freq="W-MON"
    ):
        """Make a stacked area plot of a person's project allocations between
        a start date and an end date."""

        start_date, end_date, freq = self.get_time_parameters(
            start_date, end_date, freq
        )

        try:
            df = self.get_allocations(id_value, id_type, start_date, end_date, freq)

            if id_type == "person":
                unavail_client = self.wim.get_client_id("UNAVAILABLE")
                unavail_project_names = [
                    self.wim.get_project_name(idx)
                    for idx in self.wim.get_client_projects(unavail_client)
                ]
                df.drop(
                    [proj for proj in df.columns if proj in unavail_project_names],
                    inplace=True,
                )
                # people nominally allocated 100%
                nominal_allocation = self.wim.people_capacities[id_value]
                time_label = "Time Capacity"

            elif id_type == "project":
                # get the project's person allocations
                nominal_allocation = self.wim.project_confirmed[id_value]
                time_label = "Time Requirement"

            else:
                raise ValueError("id_type must be person or project")

            nominal_allocation = select_date_range(
                nominal_allocation, start_date, end_date, drop_zero_cols=False
            )

            nominal_allocation = nominal_allocation.resample(freq).mean()

            # plot the data
            fig = plt.figure(figsize=(15, 5))
            ax = fig.gca()

            df.plot.area(ax=ax, linewidth=0)

            ax.set_title(df.columns.name)
            ax.set_ylabel("Total FTE @ " + str(self.wim.work_hrs_per_day) + " hrs/day")

            nominal_allocation.plot(
                ax=ax, color="k", linewidth=3, linestyle="--", label=time_label
            )
            ax.set_ylim(
                [0, 1.1 * max([nominal_allocation.max(), df.sum(axis=1).max()])]
            )

            ax.legend(title="", loc="best")
            ax.set_xlim([start_date, end_date])

            return fig

        except ValueError:
            return None
Exemplo n.º 6
0
    def get_allocations(self, id_value, id_type, start_date, end_date, freq):

        if id_type == "person":
            if id_value == "ALL":
                # initialise df
                df = self.wim.people_totals.copy()

                # slice the given date range from the dataframe
                df = select_date_range(df, start_date, end_date, drop_zero_cols=False)

                # replace person ids with names
                df.columns = [
                    self.wim.get_person_name(person_id) for person_id in df.columns
                ]

            else:
                # extract the person's allocations, and replace ids with names
                df = deepcopy(self.wim.people_allocations[id_value])

                # slice the given date range from the dataframe
                df = select_date_range(df, start_date, end_date, drop_zero_cols=True)

                df.columns = [
                    self.wim.get_project_name(project_id) for project_id in df.columns
                ]
                df.columns.name = self.wim.get_person_name(id_value)

        elif id_type == "project":

            if id_value == "CONFIRMED":
                # initialise df
                df = self.wim.project_confirmed.copy()

                # slice the given date range from the dataframe
                df = select_date_range(df, start_date, end_date, drop_zero_cols=True)

                # replace person ids with names
                df.columns = [
                    self.wim.get_project_name(project_id) for project_id in df.columns
                ]

            elif id_value == "PEOPLE_REQ":
                # initialise df
                df = self.wim.project_peoplereq.copy()

                # slice the given date range from the dataframe
                df = select_date_range(df, start_date, end_date, drop_zero_cols=True)

                # replace person ids with names
                df.columns = [
                    self.wim.get_project_name(project_id) for project_id in df.columns
                ]

            elif id_value == "ALLOCATED":
                # initialise df
                df = (
                    self.wim.project_confirmed.copy()
                    - self.wim.project_peoplereq.copy()
                )

                # slice the given date range from the dataframe
                df = select_date_range(df, start_date, end_date, drop_zero_cols=True)

                # replace person ids with names
                df.columns = [
                    self.wim.get_project_name(project_id) for project_id in df.columns
                ]

            else:
                # extract the project's people allocations, and replace ids with names
                df = deepcopy(self.wim.project_allocations[id_value])

                # slice the given date range from the dataframe
                df = select_date_range(df, start_date, end_date, drop_zero_cols=True)

                df.columns = [
                    self.wim.get_name(person_id, "person") for person_id in df.columns
                ]
                df.columns.name = self.wim.get_project_name(id_value)

        elif id_type == "placeholder":
            if id_value == "ALL":
                # initialise df
                df = self.wim.people_totals.copy()
                df = df[
                    [
                        self.wim.get_association_name(idx) == "Placeholder"
                        for idx in df.columns
                    ]
                ]

                # slice the given date range from the dataframe
                df = select_date_range(df, start_date, end_date, drop_zero_cols=False)

                # replace ids with names
                df.columns = [self.wim.get_person_name(idx) for idx in df.columns]

                # remove people required placeholders
                cols = [
                    col for col in df.columns if "people required" not in col.lower()
                ]
                df = df[cols]

            else:
                # extract the person's allocations, and replace ids with names
                df = self.wim.people_allocations[id_value].copy()

                # slice the given date range from the dataframe
                df = select_date_range(df, start_date, end_date, drop_zero_cols=True)

                df.columns = [
                    self.wim.get_project_name(project_id) for project_id in df.columns
                ]
                df.columns.name = self.wim.get_person_name(id_value)

        else:
            raise ValueError("id_type must be person, project or placeholder")

        # check whether there's anything to plot
        rows, cols = df.shape
        if rows <= 0 or cols <= 0:
            raise ValueError(
                "No {:s} data to plot for id {:s} between {:s} and {:s}".format(
                    id_type, str(id_value), str(start_date.date()), str(end_date.date())
                )
            )

        if freq != "D":
            df = df.resample(freq).mean()

        return df
Exemplo n.º 7
0
    def plot_harvest(
        self,
        id_type,
        group_type,
        id_value=None,
        start_date=None,
        end_date=None,
        freq="MS",
        plot_type="bar",
        units="Hours",
    ):
        """Bar charts of Harvest time tracking."""

        start_date, end_date, freq = self.get_time_parameters(
            start_date, end_date, freq
        )

        e = ValueError(
            "Invalid id_type, group_type combination. Valid options are: "
            "person-project, person-client, person-task, person-TOTAL, project-person, "
            "project-task, project-TOTAL, client-TOTAL and task-TOTAL"
        )

        if id_type == "person":
            if id_value is not None and group_type != "TOTAL":
                id_name = self.wim.get_person_name(id_value)
            else:
                id_name = ""

            if group_type == "project":
                df = self.wim.tracked_person_projects[id_value].copy()
                df.columns = [self.wim.get_project_name(idx) for idx in df.columns]
                type_name = "Project"
            elif group_type == "client":
                df = self.wim.tracked_person_clients[id_value].copy()
                df.columns = [self.wim.get_client_name(idx) for idx in df.columns]
                type_name = "Client"
            elif group_type == "task":
                df = self.wim.tracked_person_tasks[id_value].copy()
                df.columns = [self.wim.get_task_name(idx) for idx in df.columns]
                type_name = "Task"
            elif group_type == "TOTAL":
                df = self.wim.tracked_person_totals.copy()
                df.columns = [self.wim.get_person_name(idx) for idx in df.columns]
                type_name = "People"
            else:
                raise e

        elif id_type == "project":
            if id_value is not None and group_type != "TOTAL":
                id_name = self.wim.get_project_name(id_value)
            else:
                id_name = ""

            if group_type == "person":
                df = self.wim.tracked_project_people[id_value].copy()
                df.columns = [self.wim.get_person_name(idx) for idx in df.columns]
                type_name = "People"
            elif group_type == "task":
                df = self.wim.tracked_project_tasks[id_value].copy()
                df.columns = [self.wim.get_task_name(idx) for idx in df.columns]
                type_name = "Task"
            elif group_type == "TOTAL":
                df = self.wim.tracked_project_totals.copy()
                df.columns = [self.wim.get_project_name(idx) for idx in df.columns]
                type_name = "Project"
            else:
                raise e

        elif id_type == "client":
            id_name = ""

            if group_type == "TOTAL":
                df = self.wim.tracked_client_totals.copy()
                df.columns = [self.wim.get_client_name(idx) for idx in df.columns]
                type_name = "Client"
            else:
                raise e

        elif id_type == "task":
            id_name = ""

            if group_type == "TOTAL":
                df = self.wim.tracked_task_totals.copy()
                df.columns = [self.wim.get_task_name(idx) for idx in df.columns]
                type_name = "Task"
            else:
                raise e

        else:
            raise e

        df = select_date_range(df, start_date, end_date)

        if units == "FTE days":
            df = df / self.wim.work_hrs_per_day
        elif units == "FTE weeks":
            df = df / (self.wim.work_hrs_per_day * 5)
        elif units == "FTE months":
            df = df / (self.wim.work_hrs_per_day * 5 * 52 / 12)
        else:
            units = "Hours"

        if plot_type == "bar":
            fig = plt.figure(figsize=(10, df.shape[1]))
            ax = fig.gca()

            df.sum().sort_values().plot.barh(ax=ax)
            ax.set_xlabel(units)

        elif plot_type == "pie":
            fig = plt.figure(figsize=(10, 10))
            ax = fig.gca()

            df.sum().sort_values().plot.pie(ax=ax)
            ax.set_ylabel("")
            ax.set_xlabel("")

        elif plot_type == "heatmap":
            fig = plt.figure(figsize=(10, df.shape[1]))
            ax = fig.gca()

            df = df.resample(freq).sum()
            df = self.format_date_index(df, freq)

            if units == "Hours":
                fmt = ".0f"
            else:
                fmt = ".1f"

            sns.heatmap(
                df.T.sort_values(by=[col for col in df.T.columns], ascending=False),
                ax=ax,
                cmap="Reds",
                annot=True,
                cbar=False,
                fmt=fmt,
            )

        else:
            raise ValueError("plot_type must be bar or heatmap.")

        title = "{:s} {:s} from {:s} to {:s}".format(
            type_name,
            units,
            start_date.strftime("%d %b %y"),
            end_date.strftime("%d %b %y"),
        )
        if id_name != "":
            title = id_name + "\n" + title

        ax.set_title(title)

        return fig