무엇을 정하나요
입원 일정마다 병상을 정합니다. 병상을 주지 못하면 중간 수준의 벌점을 받습니다.
최소 실행 예제
from datetime import date
from optimaru import bed_allocation as ba
from optimaru.bed_allocation.models import BedModel, BedPlanModel, DepartmentModel, RoomModel, StayModel
plan = BedPlanModel(
departments=[DepartmentModel(id="D1", specialty_to_priority={"cardiology": 1})],
rooms=[
RoomModel(id="R1", department_id="D1", capacity=2, gender_limitation="SAME_GENDER",
equipment=["oxygen"]),
RoomModel(id="R2", department_id="D1", capacity=1),
],
beds=[
BedModel(id="B1", room_id="R1"),
BedModel(id="B2", room_id="R1", index_in_room=1),
BedModel(id="B3", room_id="R2"),
],
stays=[
StayModel(id="S1", patient_gender="MALE", patient_age=64, specialty="cardiology",
patient_required_equipment=["oxygen"],
arrival_date=date(2026, 1, 5), departure_date=date(2026, 1, 8)),
StayModel(id="S2", patient_gender="FEMALE", patient_age=71, specialty="cardiology",
arrival_date=date(2026, 1, 6), departure_date=date(2026, 1, 9)),
],
)
result = ba.solve(plan, time_limit_seconds=2.0, random_seed=42)
print(result.score_hard, result.score_medium, result.score_soft)
print({stay.id: stay.bed_id for stay in result.stays}) 같은 유스케이스를 그림과 실행 결과가 담긴 Jupyter 노트북으로도 볼 수 있습니다. examples/bed-allocation.ipynb ↗
입력 파라미터
DepartmentModel
| 필드 | 의미 |
|---|
id 필수 입력 specialty_ to_ priority minimum_ age maximum_ age RoomModel
| 필드 | 의미 |
|---|
id 필수 입력 department_ id 필수 입력 capacity 필수 입력 gender_ limitation ANY_GENDER, MALE_ONLY, FEMALE_ONLY, SAME_GENDER 중 하나입니다.equipment BedModel
| 필드 | 의미 |
|---|
id 필수 입력 room_ id 필수 입력 index_ in_ room StayModel
| 필드 | 의미 |
|---|
id 필수 입력 patient_ gender MALE입니다.patient_ age 필수 입력 arrival_ date 필수 입력 departure_ date 필수 입력 specialty patient_ required_ equipment patient_ preferred_ equipment patient_ preferred_ maximum_ room_ capacity BedPlanModel
| 필드 | 의미 |
|---|
departments 필수 입력 rooms 필수 입력 beds 필수 입력 stays 필수 입력 풀이 파라미터
풀이를 호출할 때 넘기는 값입니다. 시간 제한, 시드, 병렬 수가 여기에 있습니다.
| 파라미터 | 기본값 | 의미 |
|---|---|---|
time_ | None | 탐색 시간(초)이며, 0부터 1e9까지 줄 수 있습니다. 지정하지 않으면 설정의 spent_limit_seconds를 쓰며, 기본값은 30입니다. |
random_ | None | 탐색에 쓰는 난수 시드입니다. 지정하지 않으면 설정값을 쓰며, 기본값은 0입니다. |
parallel_ | None | 독립적으로 동시에 돌리는 탐색 수입니다. 탐색마다 시드가 다르고 시간은 똑같이 쓰며, 가장 좋은 결과를 고릅니다. 지정하지 않으면 설정값을 쓰며, 기본값은 1입니다. |
stop_ | False | 필수 점수가 0이 되는 즉시 멈춥니다. |
full_ | False | 증분 점수를 전체 재계산과 매번 비교합니다. 느리므로 디버깅할 때만 씁니다. |
on_ | None | 새 최고 해를 찾을 때마다 부르는 콜백입니다. 콜백이 예외를 내면 풀이를 멈추고 그 예외를 그대로 냅니다. |
config | None | 기본으로 쓸 SolverConfig입니다. 여기서 준 키워드 인자는 이번 풀이에만 이 값을 덮어쓰며, 넘긴 객체 자체는 바뀌지 않습니다. |
constraints | None | 점수에 넣을 제약과 그 설정입니다. 지정하지 않으면 기본 제약을 씁니다. |
탐색 옵션 전체 (22)
SolverConfig에 값을 넣어 config=로 넘기세요.
| 파라미터 | 기본값 | 의미 |
|---|---|---|
best_ | — | 최고 점수가 이 값에 닿으면 일찍 멈춥니다. 기본값으로는 작동하지 않습니다. |
change_ | 0.75 | 변경 이동의 상대적 비중입니다. 0이면 쓰지 않습니다. |
full_ | False | 증분 점수를 전체 재계산과 매번 비교합니다. 느리므로 디버깅할 때만 씁니다. |
late_ | 0 | 늦은 수락(late acceptance) 기록의 길이입니다. 0이면 문제 크기에 맞춰 정합니다. |
nearby_ | 0.4 | 이동 상대를 가까운 항목에서 고를 확률입니다. 0이면 쓰지 않습니다. |
nearby_ | 20 | 항목마다 유지할 이웃 수입니다. 0의 의미는 유스케이스마다 달라 자동 선택이거나 끄는 값입니다. |
on_ | None | 새 최고 해를 찾을 때마다 부르는 콜백입니다. 콜백이 예외를 내면 풀이를 멈추고 그 예외를 그대로 냅니다. |
parallel_ | 1 | 독립적으로 동시에 돌리는 탐색 수입니다. 탐색마다 시드가 다르고 시간은 똑같이 쓰며, 가장 좋은 결과를 고릅니다. |
random_ | 0 | 탐색에 쓰는 난수 시드입니다. |
ruin_ | True | 탐색이 정체되면 적응형 파괴·재구성을 실행합니다. |
ruin_ | 60 | 한 번에 제거하는 항목 수의 절대 상한입니다. |
ruin_ | 0.3 | 한 번에 제거하는 해의 최대 비율입니다. |
ruin_ | 0.1 | 한 번에 제거하는 해의 최소 비율입니다. |
ruin_ | 0.8 | 연산자 가중치가 적응하는 속도입니다. |
ruin_ | 5000 | 실행 후 다듬는 언덕 오르기 이동 수입니다. step_count_limit에는 세지 않습니다. |
ruin_ | [33.0, 9.0, 3.0, 0.0] | 새 최고, 개선, 수락, 거절 결과에 주는 연산자 보상입니다. |
ruin_ | 5000 | 파괴·재구성을 실행하기까지 새 최고 해 없이 지나는 걸음 수입니다. |
spent_ | 30.0 | 탐색 시간(초)이며, 0부터 1e9까지 줄 수 있습니다. |
step_ | 0 | 이 걸음 수에 도달하면 멈춥니다. 0이면 제한이 없습니다. |
swap_ | 0.25 | 교환 이동의 상대적 비중입니다. 0이면 쓰지 않습니다. |
unimproved_ | 0.0 | 새 최고 해 없이 이 시간(초)이 지나면 멈춥니다. 0이면 제한이 없습니다. |
unimproved_ | 0 | 새 최고 해 없이 이 걸음 수가 지나면 멈춥니다. 0이면 제한이 없습니다. |
돌아오는 결과
solve는 모델을 받아 풀린 모델을 돌려줍니다.
StayModel
| 필드 | 의미 |
|---|
bed_ id None이면 병상을 못 받은 경우를 포함해 미배정입니다.BedPlanModel
| 필드 | 의미 |
|---|
score_ hard score_ medium score_ soft 제약
optimaru.bed_allocation.constraints에서 클래스를 가져와 BedConflictCost()처럼 호출해서 넘깁니다. 넘긴 제약만 점수에 들어가고, 아무것도 넘기지 않으면 기본 제약을 씁니다. 목록에 같은 제약을 여러 번 넣을 수 있으며, 넣은 것마다 따로 점수에 들어갑니다. 제약마다 다음 파라미터를 받습니다.
| 파라미터 | 기본값 | 의미 |
|---|---|---|
weight | None | 제약 비용에 곱하는 값이며, 적용되는 수준의 단위로 셉니다. 지정하지 않으면 아래 기본 가중치를 씁니다. 기본 가중치가 0인 제약은 가중치를 줘야 작동합니다. |
level | None | "hard", "medium", "soft"를 주면 제약을 그 수준으로 옮깁니다. Level.HARD, Level.MEDIUM, Level.SOFT도 됩니다. 지정하지 않으면 아래 수준을 그대로 씁니다. 이 유스케이스에는 중간 수준이 있어서 어느 제약이든 중간으로 옮길 수 있습니다. |
exponent | 1.0 | 부호를 유지한 채 비용에 거듭제곱을 적용하며, 0보다 커야 합니다. offset을 반영한 뒤에 적용합니다. 2.0이면 제곱이라 한곳에 몰린 위반의 비용이 크게 늘어납니다. 차량이나 날짜 같은 단위마다 적용하고, 합계 하나로 계산하는 제약에는 그 합계에 적용합니다. |
offset | 0.0 | 거듭제곱 전에 비용 x에서 빼는 값입니다. exponent처럼 단위마다, 합계로 계산하는 제약은 그 합계에 적용하며, 비용과 같은 척도(초, 분, 개수 등)로 셉니다. 부호와 상관없이 유한한 수면 되고, 키워드로만 줄 수 있습니다. 이 값 아래에서 어떻게 계산할지는 clip이 정합니다. |
clip | True | True면 offset을 문턱값으로 씁니다. 넘은 부분만 계산하므로(max(0, x - offset) ** exponent) 그 이하의 비용은 아무것도 더하지 않습니다. False면 부호를 유지한 채 (x - offset) ** exponent를 그대로 계산하므로, 오프셋보다 작은 단위는 비용이 0이어도 음수, 곧 보상으로 들어갑니다. 하드 점수에 들어가는 수준에서는 clip=False에 0이 아닌 오프셋을 주면 ValueError가 납니다. 하드 0이 위반 없음이라는 뜻을 지키기 위해서입니다. 키워드로만 줄 수 있습니다. |
중간 제약의 기본 가중치는 중간 수준 단위로 1입니다. 탐색은 중간 값에 1e9을 곱해 선호 값과 합친 점수로 해를 비교하므로, 선호 합계가 1e9보다 작은 동안에는 중간 수준이 선호보다 우선합니다. level로 어느 제약이든 세 수준 가운데 하나로 옮길 수 있습니다. level="medium"이면 중간으로 옮기고, "hard"나 "soft"면 중간 제약을 필수나 선호로 옮깁니다. 가중치는 적용되는 수준의 단위로 셉니다.
위의 최소 실행 예제에 이어서 실행합니다.
from optimaru.bed_allocation.constraints import (
BedConflictCost, PreferredPatientEquipmentCost, RequiredPatientEquipmentCost,
UnassignedStaysCost, get_constraints,
)
picked = get_constraints([
BedConflictCost(), RequiredPatientEquipmentCost(), UnassignedStaysCost(),
PreferredPatientEquipmentCost(weight=100),
])
custom = ba.solve(plan, time_limit_seconds=2.0, random_seed=42, constraints=picked)
print(custom.score_hard, custom.score_medium, custom.score_soft) | 이름 | 수준 | 기본 가중치 | 하는 일 |
|---|---|---|---|
bed_ | 필수 | 1000 | 두 입원 일정이 같은 병상을 쓰는 박수에 벌점을 줍니다. |
female_ | 필수 | 50 | 여성 환자가 MALE_ONLY 병실에 있는 박수에 벌점을 줍니다. |
male_ | 필수 | 50 | 남성 환자가 FEMALE_ONLY 병실에 있는 박수에 벌점을 줍니다. |
department_ | 필수 | 100 | 진료과 최저 나이보다 어린 환자가 있는 박수에 벌점을 줍니다. |
department_ | 필수 | 100 | 진료과 최고 나이보다 많은 환자가 있는 박수에 벌점을 줍니다. |
required_ | 필수 | 50 | 필요한 장비가 하나라도 없는 병실 배정에 벌점을 줍니다. 벌점은 병실에 있는 필요 장비 수로 매깁니다. 아래 제한 사항을 참고하세요. |
unassigned_ | 중간 | 1 | 병상 없이 보내는 박수에 벌점을 줍니다. |
preferred_ | 선호 | 8 | 환자가 원하는 크기보다 큰 병실에 있는 박수에 벌점을 줍니다. |
department_ | 선호 | 10 | 필요한 진료 분야가 없는 진료과에 있는 박수에 벌점을 줍니다. |
department_ | 선호 | 10 | 진료 분야의 우선순위가 1이 아닌 진료과에 있는 박수에 벌점을 줍니다. 벌점은 우선순위에서 1을 뺀 값으로 매깁니다. |
preferred_ | 선호 | 50 | 원하는 장비가 없는 병실에 있는 박수에 벌점을 줍니다. 벌점은 없는 장비 수로 매깁니다. |
mixed_ | 필수 | 1000 | SAME_GENDER 병실에 남녀가 함께 있는 박수에 벌점을 줍니다. |
기존 해에서 시작하기
최신 버전이 필요합니다. pip install -U optimaru로 올려 주세요.
with_initial_solution(problem, solution)은 solution에서 탐색을 시작하는 problem의 복사본을 돌려줍니다. solution은 solve가 돌려준 모델이나 그 JSON 딕셔너리입니다.
위의 최소 실행 예제에 이어서 실행합니다.
seeded = ba.with_initial_solution(plan, result.model_dump(mode="json"))
again = ba.solve(seeded, time_limit_seconds=2.0, random_seed=43)
print(again.score_hard, again.score_medium, again.score_soft) - 입원 일정은 id로 맞춥니다.
- 완전한 해를 넣으면 구성 단계를 건너뛰고 그 해에서 바로 탐색을 시작합니다. 일부만 넣으면 넣은 부분은 그대로 두고, 빠진 입원 일정만 새로 구성합니다.
time_limit_seconds=0을 주면 바로 멈추므로 완전한 해는 넣은 그대로 돌아옵니다.step_count_limit = 0은 제한이 없다는 뜻이므로, 바로 멈추려면 시간 제한을 쓰세요.- 병상을 못 받은 일정은 넘길 병상 id가 없으므로 미배정으로 돌아오고, 구성 단계에서 다시 배치합니다.
- 모르는 id가 있으면
ValueError가 납니다.
예외와 제한
required_patient_equipment_cost는 필요한 장비가 하나라도 없을 때 작동하고, 벌점은 병실에 있는 필요 장비 수로 매깁니다. 필요한 장비가 하나도 없는 병실은 벌점이 0이므로, 장비가 중요하다면 결과를 꼭 확인하세요.- 날짜는 일수로 바뀌어 계산됩니다. 박수는 퇴원일에서 입원일을 뺀 값에 1을 더합니다.