簡體   English   中英

Simpy:呼叫中心模擬 - 非活動呼叫超時

[英]Simpy: Callcenter simulation - inactive call timeout

目前我正在研究或多或少復雜的呼叫中心模擬。 我對 Simpy 很陌生,如果沒有座席可以接聽電話,我會遇到超時呼叫的問題。

在我的模擬中,我在 4 個不同的隊列中生成呼叫。 他們每個人都應該有自己的呼叫產生率。 此外,我還有 4 個活躍的特工。 他們每個人都在處理多個隊列,但沒有人處理所有 4 個隊列。 並且他們每個人在某個隊列中工作時都有單獨的處理時間分布。

此外,一個可用的座席不僅應該能夠接聽隊列中的第一個可用呼叫,而且我想稍后添加關於選擇哪個呼叫的更詳細的邏輯。 因此,我將生成調用添加到存儲(后來是過濾器存儲)並在 consume_calls function 中將它們拉出。

由於呼叫中心可能在人手不足的情況下運行(代理人太少),我也希望 model 客戶耐心等待。 所以我也定義了 MIN_PATIENCE 和 MAX_PATIENCE ,如果代理不能在這個時間范圍內接受它,讓他們退出呼叫。

我的腳本看起來像這樣。

代碼已更新 (2023-02-02):似乎現在可以工作

from typing import Callable, Generator, List, Union

import numpy as np
import pandas as pd
import simpy

RANDOM_SEED = 42
NUM_AGENTS = 4  # Number of agents in the callcenter
NUM_QUEUES = 2

MIN_PATIENCE = 2 * 60
MAX_PATIENCE = 5 * 60
SIM_DURATION = 8 * 60 * 60

RNG = np.random.default_rng(RANDOM_SEED)

i = 0

# Parse config files for agent performance and queue arrivial times
agents_config = pd.read_csv("queue_agent_mapping.csv")
agents_config["lambda"] = agents_config["lambda"] * 60
agents_config_idx = agents_config.set_index(["agent_id", "queue_id"])

queue_config = pd.read_csv("call_freq.csv")
queue_config["lambda"] = queue_config["lambda"] * 60
queue_config_idx = queue_config.set_index("queue_id")

callcenter_logging = pd.DataFrame(
    {
        "call_id": [],
        "queue_id": [],
        "received_time": [],
        "agent_id": [],
        "start_time": [],
        "end_time": [],
        "status": [],
    }
)
callcenter_logging = callcenter_logging.set_index("call_id")

open_transactions = pd.DataFrame({"call_id": [], "queue_id": [], "received_time": []})
open_transactions = open_transactions.set_index("call_id")

# Get number of agents in simulation from config file
num_agents = agents_config.drop_duplicates(subset="agent_id")

# Get agent-queue mapping from config file
agents_config_grouped = (
    agents_config.groupby("agent_id").agg({"queue_id": lambda x: list(x)}).reset_index()
)


class Callcenter:
    """Representation of the call center.

    Entry point of the simulation as it starts processes to generate calls and their processing.
    """

    # Variable used to generate unique ids
    call_id = 0

    def __init__(self, env: simpy.Environment):

        self.env = env

    def get_next_call_id(self):
        self.call_id += 1
        return self.call_id

    def run_simulation(self, agents: simpy.FilterStore):
        self.agents = agents
        self.queues = [Queue(env, queue_id=qq) for qq in range(4)]
        self.call_generator = [env.process(queue.generate_calls(self)) for queue in self.queues]
        self.call_accept_consumer = [env.process(queue.consume_calls()) for queue in self.queues]


class Agent:
    """Representation of agents and their global attributes."""

    def __init__(self, agent_id, queue_id):
        self.agent_id = agent_id
        self.allowed_queue_ids = queue_id


class Queue:
    """Representation of call center queues.

    Holds methods to generate and consume calls. Calls are stored in a store.
    """

    def __init__(self, env, queue_id):
        self.env = env

        # Defines the store for calls. Later, a FilterStore should be used. This will enable us to
        # draw calls by special attribute to fine tune the routing.
        self.store = simpy.FilterStore(env)

        self.queue_id = queue_id

        # Get the arrivial distribution of calls in the queue from the initial config.
        self.lam = self._get_customer_arrival_distribution()

    def generate_calls(self, callcenter_instance: Callcenter) -> Generator:
        """Generate the calls.

        Calls are then put to the queues store.
        """
        while True:
            yield self.env.timeout(RNG.poisson(self.lam))

            # Initialize and fill the call object.
            new_call_id = callcenter_instance.get_next_call_id()
            call = Call(queue_id=self.queue_id, call_id=new_call_id, env=env)
            call = call.update_history()
            call.add_open_transaction(status="active")

            # Write call to logging table
            callcenter_logging.loc[call.call_id, ["queue_id", "received_time"]] = [
                call.queue_id,
                self.env.now,
            ]

            # Put call to the queue store.
            self.put_call(call)

    def consume_calls(self) -> Generator:
        """Draw call from queue store and let agents work on them.

        If no agent is found within MIN_PATIENCE and MAX_PATIENCE, drop the call.
        """
        while True:

            # Wait for available agent or drop the call as customer ran out of patience.
            agent = yield agents.get(lambda ag: self.queue_id in ag.allowed_queue_ids)

            # call = yield self.get_call(lambda ca: ca.status == "active")
            call = yield self.get_call(lambda ca: ca.status == "active")
            if call.received_at + call.max_waiting <= call.env.now:
                print(
                    f"customer hung up call {call.call_id} after waiting {call.max_waiting / 60} minutes."
                )

                # The call did not receive an agent in time and ran out of patience.
                call.status = "dropped"

                callcenter_logging.loc[call.call_id, ["end_time", "status"]] = [
                    call.env.now,
                    call.status,
                ]
                print(
                    f"no agent for {call.call_id} after waiting {(call.env.now - call.received_at) / 60} minutes"
                )

            else:
                # Do some logging that an agent took the call.
                print(
                    f"Agent {agent.agent_id} takes {call.call_id} in queue {call.queue_id} at {call.env.now}."
                )
                callcenter_logging.loc[call.call_id, ["queue_id", "agent_id", "start_time"]] = [
                    call.queue_id,
                    agent.agent_id,
                    call.env.now,
                ]

                # Get average handling time from config dataframe. Change this to an Agent class
                # attribute, later.
                ag_lambda = agents_config_idx.loc[agent.agent_id, call.queue_id]["lambda"]

                yield call.env.timeout(RNG.poisson(ag_lambda))  # Let the agent work on the call.

                call.status = "finished"

                # Do some logging
                callcenter_logging.loc[call.call_id, ["end_time", "status"]] = [
                    call.env.now,
                    call.status,
                ]
                print(
                    f"Agent {agent.agent_id} finishes {call.call_id} in queue {call.queue_id} at {call.env.now}."
                )

                # Put the agent back to the agents store. -> Why is this needed? Shouldn't the
                # context manager handle this? But it did not work without this line.
            yield agents.put(agent)

    def _get_customer_arrival_distribution(self) -> float:
        """Returns the mean call arrivial time in a given queue_id."""
        return queue_config.loc[self.queue_id, "lambda"]

    def get_call(self, filter_func: Callable):
        """Helper function to get calls by complex filters from the queue store."""
        return self.store.get(filter=filter_func)

    def put_call(self, call):
        """Helper function to put calls to the queue store."""
        self.store.put(call)
        print(f"Call {call.call_id} added to queue {call.queue_id} at {call.env.now}")


class Call(Callcenter):
    """Representation of a call with all its."""

    def __init__(self, env: simpy.Environment, call_id: int, queue_id: int):
        self.env = env
        self.call_id = call_id
        self.queue_id = queue_id
        self.agent_id = None
        self.status = "active"
        self.received_at = env.now
        self.max_waiting = RNG.integers(MIN_PATIENCE, MAX_PATIENCE)
        self.history: List[Union[int, str]] = []

    def update_history(self):
        """Helper to update the call history.

        Needed for tracking calls, which changes are transferred from one queue to another.
        Currently this is not in use.
        """
        self.history.append([self.call_id, self.queue_id, self.agent_id, self.status])
        return self

    def add_open_transaction(self, status="active"):
        """Helper to add a call to the open transactions log.

        Needed for rebalancing logics. Currently this is not in use.
        """
        open_transactions.loc[self.call_id, ["queue_id", "received_time", "status"]] = [
            self.queue_id,
            self.env.now,
            status,
        ]


env = simpy.Environment()

agents = simpy.FilterStore(env, capacity=len(num_agents))
agents.items = [Agent(row.agent_id, row.queue_id) for row in agents_config_grouped.itertuples()]

callcenter = Callcenter(env)
callcenter.run_simulation(agents)
env.run(until=SIM_DURATION)

這些是配置文件。

# call_freq
queue_name,queue_id,lambda
A,0,2
B,1,4
C,2,3.5
D,3,3
# queue_agent_mapping.csv
queue_id,agent_id,lambda
0,abc11,2
0,abc13,5
0,abc14,2
1,abc11,5
1,abc12,3
1,abc14,12
2,abc12,2
2,abc13,3
3,abc14,3

我將客戶耐心設置在 3 到 6 分鍾以內。 N. 盡管如此,如果我檢查一個呼叫的等待時間,直到它被服務,我發現他們中的很多人等待的時間遠遠超過 3-6 分鍾。 更糟糕的是,我還發現一些呼叫已按需掛斷。

callcenter_logging["wait_time"] = callcenter_logging["start_time"] - callcenter_logging["received_time"] 
callcenter_logging["serving_wait"] = callcenter_logging["end_time"] - callcenter_logging["start_time"]
callcenter_logging.head(20)

dropped = (
    callcenter_logging.loc[callcenter_logging["status2"] == "dropped", ["queue_id", "status2", "end_time"]]
)
dropped = (
    dropped.set_index("end_time")
    .groupby(["queue_id"])
    .expanding()["status2"]
    .agg({"cnt_dropped": 'count'})
    .reset_index()
)

fig_waiting = px.line(callcenter_logging, "received_time", "wait_time", color="queue_id")
fig_waiting = fig_waiting.update_traces(connectgaps=True)
fig_waiting.show()

fig_dropped = px.line(dropped, x="end_time", y="cnt_dropped", color="queue_id")
fig_dropped.show()

等待時間 plot

掉線 plot

我想我的電話沒有在正確的時間從隊列存儲中拉出。 但是我無法理解代碼的確切問題。

有沒有人有想法?

您的耐心等待時間不會影響呼叫已經排在隊列中的時間。 因此,如果在循環開始時呼叫已在隊列中等待 10 分鍾並且沒有代理可用,則在耐心超時發生時呼叫的總等待時間將為 10 分鍾加上 env.timeout(RNG.integers(MIN_PATIENCE,最大耐心))。

您需要耐心超時才能進入呼叫 object 並讓呼叫 object 從隊列中刪除自身或在耐心超時到期時設置耐心超時標志。

我會改變你的循環,首先得到一個代理,然后做一個內部循環來接聽電話,忽略帶有過期標志的電話。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM