2024年5月31日金曜日

当直拘束表Excel出力

 以下のように出力しました。


各日、次のような出力です。

(短時間宿直)

当直

内科拘束

整形拘束


短時間宿直シフトは、暦日通りに出力しています。スケジュールナースのシフトから補正しています。

プロジェクトの全Pythonソースです。mainのPython部(制約)が最初、post部のPythonのmain def post_main() が最後に配置することが注意点です。制約のpythonとpostのpythonは、別プロセスであることにも注意してください。通信はできません。

post処理では、shift_solutionがシフト解配列として利用可能ですので、ひたすら解を読んで上記フォーマットとなるように整形しています。タスク解(診療科)も利用可能ですが、上記フォーマット出力するには、シフト解のみで十分です。

本プロジェクトは、大変に複雑な仕様です。本コードは、そのための開発デバッグ検証用も兼ねており、開発当初から、改変を積み重ねてきました。冗長なコードになっているのはそのためです。

ユーザには、スケジュール解ではなく、開発当初より、本Excel解を提出してデバッグにご協力いただきました。元々の顧客フォーマットが上のようなものなので、違和感なく検証いただけたと思います。Excel解の提出のフェーズが完了しました。以降、スケジュールナースプロジェクトを使用した、デバッグフェーズに移行します。


import sc3
import sys
import os
import csv
import re
import win32gui, win32con
import pywin.dialogs.list
import calendar
from collections import namedtuple

for person in 非常勤:
    for day in 今月:
            if shift_schedules[person][day][0]=='':
                v=sc3.GetShiftVar(person,day,'その他')
                s='非常勤は予定入力がないときはその他 '+staffdef[person]+' '+daydef[day]
                sc3.AddHard(v,s)

def get_open_file_name(title):
    filter='xlsx\0*.xlsx\0'
    customfilter='Other file types\0*.*\0'
    fname, customfilter, flags=win32gui.GetOpenFileNameW(
    InitialDir=project_file_path, 
    Flags=win32con.OFN_ALLOWMULTISELECT|win32con.OFN_EXPLORER,
    File='', DefExt='csv',
    Title=title,#
    Filter=filter,
    CustomFilter=customfilter,
    FilterIndex=0)
    return str(fname)


def draw拘束(ws,c,person,str):   
        if person in 内科:
            ws.Cells(c.Row+3,c.Column).Value=str
        elif person in 整形外科:
            ws.Cells(c.Row+4,c.Column).Value=str

def day_solution_analysis(ws,c,n,day):

    #clear 
   
    
    宿日直person=-1
    日直person=-1
    宿直person=-1
    
    for person in 非常勤:
        if shift_solution[person][day]=='その他':
            continue
        
        if '宿日直' in shift_solution[person][day]:
            if 宿日直person!=-1:
                print("fatal error 宿日直は、 most one work per a day")
                exit()
            宿日直person=person
            continue

        if '日直(日中のみ)' == shift_solution[person][day]:
            if 日直person!=-1:

                print("fatal error 日直は、 most one work per a day")
                exit()
            日直person=person
            continue

        if '宿直'in shift_solution[person][day]:
            if 宿直person!=-1:
                print("fatal error 宿直は、 at most one work per a day")
                exit()
            宿直person=person
            continue

        print("fatal error ")
        exit()
        
    list=[]
    
    短宿person=-1
    拘束宿list=[]
    拘束日直list=[]
    日直拘束宿日直person=-1
    宿直拘束宿日直person=-1

    for person in 常勤:
        if shift_solution[person][day]=='その他':
             continue
        if shift_solution[person][day]=='宿日直':
            if 宿日直person!=-1:
                print("fatal error 宿日直は、 most one work per a day")
                exit()
            宿日直person=person
            continue

        if '30分'in shift_solution[person][day] or '60分' in shift_solution[person][day] or '90分' in shift_solution[person][day]:
            
            if 短宿person!=-1:
                print(staffdef[person],daydef[day])
                print("fatal error 短宿は、 at most one work per a day")
                exit()
            短宿person=person
            print(staffdef[短宿person],daydef[day],shift_solution[短宿person][day])
            continue

        if '日直(日中のみ)' in shift_solution[person][day] :
            if 日直person!=-1:
                print(staffdef[person],daydef[day])
                print("fatal error 日直は、 at most one work per a day")
                exit()
            日直person=person
            continue

        if '宿直'==shift_solution[person][day] :
            if 宿直person!=-1:
                print("fatal error 宿直は、 at most one work per a day")
                exit()
            宿直person=person
            continue

        if '日直拘束宿日直'==shift_solution[person][day]:
            if 日直拘束宿日直person!=-1:
                print("fatal error 日直拘束宿日直は、 at most one work per a day")
                exit()
            日直拘束宿日直person=person
            continue

        if '宿直拘束宿日直'==shift_solution[person][day]:
            if 宿直拘束宿日直person!=-1:
                print("fatal error 宿直拘束宿日直は、 at most one work per a day")
                exit()
            宿直拘束宿日直person=person
            continue

        if '拘束宿'==shift_solution[person][day]:
            拘束宿list.append(person)
            continue

        if '拘束日直'==shift_solution[person][day]:
            拘束日直list.append(person)
            continue


        #print("list add",staffdef[person])
        list.append((person,shift_solution[person][day]))

    if 短宿person!=-1:#Draw 短縮
        print(短宿person,staffdef[短宿person],daydef[day],n,shift_solution[短宿person][day])
        
        str=staffdef[短宿person]+' '
        if '拘束' in shift_solution[短宿person][day]:
            draw拘束(ws,c,短宿person,staffdef[短宿person])

        if shift_solution[短宿person][day]=='夜30分宿直' or shift_solution[短宿person][day]=='夜30分宿直拘束付':
            str +='17:00~17:30'
            ws.Cells(c.Row+1,c.Column+1).Value=str
            
        elif shift_solution[短宿person][day]=='夜60分宿直' or shift_solution[短宿person][day]=='夜60分宿直拘束付':
            str +='17:00~18:00'
            ws.Cells(c.Row+1,c.Column+1).Value=str
        elif shift_solution[短宿person][day]=='朝30分宿直' or shift_solution[短宿person][day]=='朝30分宿日直拘束付':
            str +='8:00~8:30'
            print("!!朝30分",staffdef[person],daydef[day],n.Row,n.Column)
            if n!=None:
                print(staffdef[短宿person],daydef[day])
                ws.Cells(n.Row+1,n.Column+1).Value=str
        elif shift_solution[短宿person][day]=='朝60分宿直' or shift_solution[短宿person][day]=='朝60分宿日直拘束付':
            str +='7:30~8:30'
            if n!=None:
                ws.Cells(n.Row+1,n.Column+1).Value=str
                print(staffdef[短宿person],daydef[day],n.Row+1,n.Column,ws.Cells(n.Row+1,n.Column).Value)
        elif shift_solution[短宿person][day]=='朝90分宿直' or shift_solution[短宿person][day]=='朝90分宿日直拘束付':
            str +='7:00~8:30'
            if n!=None:
                ws.Cells(n.Row+1,n.Column+1).Value=str

    if 日直person!=-1 or 日直拘束宿日直person!=-1:
        if 宿直person==-1 and  宿直拘束宿日直person==-1:
            print("fatal error 日直があるなら宿直もないとだめ")
            exit()
        if 宿直person!=-1:
            strlast=staffdef[宿直person]
        else:
            strlast=staffdef[宿直拘束宿日直person]
        if 日直person!=-1:
            str=staffdef[日直person]+'/'+strlast
            ws.Cells(c.Row+2,c.Column).Value=str
            if 宿直拘束宿日直person!=-1:
                draw拘束(ws,c,宿直拘束宿日直person,strlast)
            else:
                draw拘束(ws,c,宿直person,strlast)
        elif 日直拘束宿日直person!=-1:
            str=staffdef[日直拘束宿日直person]+'/'+strlast
            ws.Cells(c.Row+2,c.Column).Value=str
            draw拘束(ws,c,日直拘束宿日直person,str)

    elif 宿日直person!=-1:
        str=staffdef[宿日直person]
        ws.Cells(c.Row+2,c.Column).Value=str
        draw拘束(ws,c,宿日直person,str)

    elif 宿直person!=-1:
        str=staffdef[宿直person]
        ws.Cells(c.Row+2,c.Column).Value=str
        draw拘束(ws,c,宿直person,str)
        
    else:
        print("fatal error ")
        exit()

    
    if len(拘束日直list)==1:
        str=staffdef[拘束日直list[0]]
        if 宿直person==-1 and len(拘束宿list)!=1:

            print("fatal error 拘束日直があるなら、拘束宿日直もしくは宿直もないとだめ")
            exit()
        if 拘束日直list[0] in 内科:
            if 宿直person not in 内科 and 拘束宿list[0] not in 内科:
                print("fatal error")
                exit()
        elif 拘束日直list[0] in 整形外科:
            if 宿直person not in 整形外科 and 拘束宿list[0] not in 整形外科:
                print("fatal error")
                exit()
        if 宿直person !=-1:
            str=staffdef[拘束日直list[0]]+'/'+staffdef[宿直person]
        elif len(拘束宿list)==1:
            str=staffdef[拘束日直list[0]]+'/'+staffdef[拘束宿list[0]]
        print("拘束日直list[0]",str)
        draw拘束(ws,c,拘束日直list[0],str)
    elif len(拘束日直list)==2:
        if 宿直person==-1 or len(拘束宿list)!=1:
            print("fatal error 拘束日直があるなら、拘束宿直および宿直どちらもないとだめ")
            exit()
        if 拘束日直list[0] in 内科:
            if 宿直person in 内科:
                str=staffdef[拘束日直list[0]]+'/'+staffdef[宿直person]
                draw拘束(ws,c,拘束日直list[0],str)
                str=staffdef[拘束日直list[1]]+'/'+staffdef[拘束宿list[0]]
                draw拘束(ws,c,拘束日直list[1],str)

            elif 拘束宿list[0] in 内科:
                str=staffdef[拘束日直list[0]]+'/'+staffdef[拘束宿list[0]]
                draw拘束(ws,c,拘束日直list[0],str)
                str=staffdef[拘束日直list[1]]+'/'+staffdef[宿直person]
                draw拘束(ws,c,拘束日直list[1],str)
            else:
                print("fatal error 拘束日直が内科なら、拘束宿直および宿直どちらは内科")
                exit()
        elif 拘束日直list[0] in 整形外科:
            if 宿直person in 整形外科:
                str=staffdef[拘束日直list[0]]+'/'+staffdef[宿直person]
                draw拘束(ws,c,拘束日直list[0],str)
                str=staffdef[拘束日直list[1]]+'/'+staffdef[拘束宿list[0]]
                draw拘束(ws,c,拘束日直list[1],str)

            elif 拘束宿list[0] in 整形外科:
                str=staffdef[拘束日直list[0]]+'/'+staffdef[拘束宿list[0]]
                draw拘束(ws,c,拘束日直list[0],str)
                str=staffdef[拘束日直list[1]]+'/'+staffdef[宿直person]
                draw拘束(ws,c,拘束日直list[1],str)
            else:
                print("fatal error 拘束日直が整形外科なら、拘束宿直および宿直どちらは整形外科")
                exit()
 
    elif len(拘束宿list) >=1:
        for person in 拘束宿list:
            str=staffdef[person]
            draw拘束(ws,c,person,str)

        #draw拘束(ws,c,拘束宿person,str)
        
    if len(list) <=2:
        for tuple in list:
            if '拘束宿日直' not in tuple[1]:
                print(staffdef[tuple[0]],tuple[1])
                print("fatal error ")
                exit()
            str=staffdef[tuple[0]]
            print('拘束宿日直',str)
            draw拘束(ws,c,tuple[0],str)
        
    else:
        print("len(list)",len(list),len(拘束宿list),len(拘束日直list))
        print("fatal error programming error")
        exit()


def get_sheet_name():
    s=daydef[制約開始日]

    year_str=s[0:4]
    month=int(s[5:7])
    sheet_name=year_str+'年'+str(month)+'月'
    return sheet_name

def get_year():
    s=daydef[制約開始日]
    year_str=s[0:4]
    return int(year_str)

def get_month():
    s=daydef[制約開始日]
    month=int(s[5:7])
    return month

def get_day():
    s=daydef[制約開始日]
    day=int(s[8:10])
    return day


def find_address(moncal,D):
    row=0
    for r in moncal:
        if D not in r:
            row+=1
            continue
        return (row,r.index(D))
    #次月最初の日:最終行の最終列+1を出す
    row=len(moncal)-1
    col=len(moncal[row])
    return (row,col)
    raise IndexError

def init_row_columns():
    global start_row
    global start_column
    global row_interval
    global column_interval

    start_row=6
    start_column=2
    row_interval=6
    column_interval=2

def findcell(ws,moncal,D):
    tuple=find_address(moncal,D)
    init_row_columns()
    row=start_row+row_interval*tuple[0]
    column=start_column+column_interval*tuple[1]
    c=namedtuple("Row","Column")
    c.Row=row
    c.Column=column
    return c

def clear_cells(ws):
    init_row_columns()
    for r in range(6):
        for d in range(7):
            row=start_row+r*row_interval
            column=start_column+d*column_interval
            print(row,column)
            ws.Cells(row  ,column+1).Value=""
            ws.Cells(row  ,column).Value=""
            ws.Cells(row+1,column+1).Value=""
            ws.Cells(row+1,column).Value=""
            ws.Cells(row+2,column+1).Value=""
            ws.Cells(row+2,column).Value=""
            ws.Cells(row+3,column).Value=""
            ws.Cells(row+4,column).Value=""





def post_main():
    
    print('\n\n*********ポスト処理を実行中です。*************\n')
    import win32com.client#pywin32をインポート

#すでにExcelが起動されている場合はそのタスクが使われる
#エラー終了するとタスクは残ります
    try :
        xl = win32com.client.Dispatch("Excel.Application")
    except:
        print("can not invoke excel")
        exit()
    #動いている様子を見てみる
    xl.Visible = False
    #file=get_open_file_name("Open Excel File")
    #print(file)
    os.chdir(project_file_path)
    filename="2024当直拘束表.xlsx"
   
    file=os.path.join(project_file_path,filename)
    file.replace("/","\\") #なぜか逆スラッシュでないと動かない
    wb = xl.Workbooks.Open(file)
    # Excelシートオブジェクト
    ws = wb.Worksheets(get_sheet_name())

    # 指定したシートを選択
    # Select()の使用前にシートのActivate()が必要
    ws.Activate()

    calendar.setfirstweekday(6)#日曜日を最初に
    mon_cal=calendar.monthcalendar(get_year(),get_month())#行列でリスト

    print(findcell(ws,mon_cal,1))

    clear_cells(ws)


    for day in 今月区間:
        D=day-制約開始日+1
        print(D)
        c=findcell(ws,mon_cal,D)
        n=findcell(ws,mon_cal,D+1)
        #print(c.Row,c.Column)
        day_solution_analysis(ws,c,n,day)

    wb.Close(True)# Trueで保存。False=Defaultでブックを保存せずにクローズ
    # Excel終了
    xl.Quit()
    print('\n\n*********ポスト処理を実行終了しました。*************\n')

2024年5月30日木曜日

Excelユーザ特殊フォーマットで解出力

 適当なテンプレートを探します。

今回は、以下のExcelブックを有難く使わせていただきます。ありがとうございます。


エクセルカレンダーテンプレート | アラクネ (arachne.jp)

ユーザフォーマットとしては、3行に渡るものが必要でした。上記フォーマットは、最大6行まで可能です。


これに、短時間宿直もあるので、出来れば4行以上のものが必要でした。

まずは、単独でExcelファイルを操作するものを作ります。プロジェクト設定は以下です。

<python ポスト処理設定>



<シート名の作成>
「2024年6月」のような名前になっているので、プロジェクト処理月に応じてシート名を作成する必要があります。年月日情報は、ソース全体の最初の方、daydefでリストとして作成されています。制約開始日でインデックスすると、当該年月日がフォーマット文字列として取り出すことが出来ます。


<ポスト処理>
def post_mainがポスト処理のメインになります。

python ソースは以下です。以下を参考にしました。このテンプレートでは、Dayが数値として定義されていません。検索がうまく動かないので、日曜日基準のカレンダを内部で持って、それをExcelシートフォーマットに展開する方法を取りました。このプロジェクトは、クリアするだけの機能しかありませんが、後々これを基に解を出力していきます。




import sc3
import sys
import os
import csv
import re
import win32gui, win32con
import datetime
import calendar


def get_sheet_name():
    s=daydef[制約開始日]

    year_str=s[0:4]
    month=int(s[5:7])
    sheet_name=year_str+'年'+str(month)+'月'
    return sheet_name

def get_year():
    s=daydef[制約開始日]
    year_str=s[0:4]
    return int(year_str)

def get_month():
    s=daydef[制約開始日]
    month=int(s[5:7])
    return month

def get_day():
    s=daydef[制約開始日]
    day=int(s[8:10])
    return day


def find_address(moncal,D):
    row=0
    for r in moncal:
        if D not in r:
            row+=1
            continue
        return (row,r.index(D))
    raise IndexError

def init_row_columns():
    global start_row
    global start_column
    global row_interval
    global column_interval

    start_row=5
    start_column=2
    row_interval=6
    column_interval=2

def findcell(ws,moncal,D):
    tuple=find_address(moncal,D)
    init_row_columns()
    row=start_row+row_interval*tuple[0]
    column=start_column+column_interval*tuple[1]
    return (row,column)

def clear_cells(ws):
    init_row_columns()
    for r in range(6):
        for d in range(7):
            row=start_row+r*row_interval
            column=start_column+d*column_interval
            print(row,column)
            ws.Cells(row  ,column+1).Value=""
            ws.Cells(row+1,column+1).Value=""
            ws.Cells(row+2,column+1).Value=""
            ws.Cells(row+3,column).Value=""
            ws.Cells(row+4,column).Value=""
            ws.Cells(row+5,column).Value=""


def post_main():
    
    print('\n\n*********ポスト処理を実行中です。*************\n')
    import win32com.client#pywin32をインポート

#すでにExcelが起動されている場合はそのタスクが使われる
#エラー終了するとタスクは残ります
    try :
        excel = win32com.client.Dispatch("Excel.Application")
    except:
        print("can not invoke excel")
        exit()
    #動いている様子を見てみる
    excel.Visible = True
    os.chdir(project_file_path)
    filename="2024勤務表.xlsx"
    file=os.path.join(project_file_path,filename)
    file.replace("/","\\") 
    sheet_name=get_sheet_name()
    try:
        wb = excel.Workbooks.Open(file)
        # Excelシートオブジェクト
        ws = wb.Worksheets(sheet_name)
    except:
        print("Can not open the excel file.")
        wb.Close()
        excel.Quit()
    

    
    # 指定したシートを選択
    # Select()の使用前にシートのActivate()が必要
    ws.Activate()
    
    calendar.setfirstweekday(6)#日曜日を最初に
    mon_cal=calendar.monthcalendar(get_year(),get_month())#行列でリスト

    print(findcell(ws,mon_cal,1))

    clear_cells(ws)

    wb.Close(True)# Trueで保存。False=Defaultでブックを保存せずにクローズ
    # Excel終了
    excel.Quit()
    print('\n\n*********ポスト処理を実行終了しました。*************\n')
             

2024年5月29日水曜日

会計年度単位での集計・平準化

 Q.

毎月の「当直予定表及び拘束表」の常勤医師ごとの担当実績は、当直及び拘束もそれぞれ休日または平日に分類し、実績を各月・会計年ごとに集計したい。

常勤医師の当直、宿直又は宿日直(当直)及び拘束の担当回数は、各月及び会計年度でできれば平準化したい。

Ans.

会計年の集計・平準化については、スケジュールナース仕事の対象外とさせてください。

仮に、Excelのマクロ等を用いて自動化したとしても、希望休み属性やスキル等により、どうしても不均衡が生じてしまう例はたくさんあります。そこで、敢えて自動化しない、俯瞰的公平の観点により、累計結果を見ながら、下記設定を毎月管理者が行う、という方法をお勧めしています。

具体的には、以下のスタッフプロパティシートにより、各医師の各目標回数を毎月設定します。


各月は、勿論割り切れない場合も存在するので、別にExcel累計集計によりフィードバック・フィードフォワードしながらの設定が可能になります。しかし、前述のように、各医師の希望休み志向やスキル等により、どうしてもフラットにはならないケースが出てきます。(例えば、前回、休日の拘束出勤2連勤は、若手医師がやらざるを得ないケースがありましたが、そのような希望休み数が少ない方が居て初めて解が存在する場合があります。)

何を持って平等とするか、あるいは、経年数による傾斜配分等、人事・人間関係に及ぶので、複雑で難しい問題が待っています。例えば、以下のような俯瞰的に判断するケースが出てくると思います。

人生初の勤務表作成 公平性と家庭の事情のハザマ (nurse-scheduling-software.com)

単純なプログラムで対応可能なケースはむしろまれで、管理者が月毎に判断設定した方が実際的である、と思います。

なお、お客さまのExcelフォーマットに解を出力させるには、PythonPost処理で行うことが可能です。次に説明します。



2024年5月28日火曜日

Q x医師が、拘束のみで休日の25日、 26日と2日連続していま すが

 Q.

拘束のみで休日の25日、26日と2日連続している点が気になります。ただし、こちらも、実際の予定表にあるケースで、さらに、今回は、実際の予定表がこれと同じになっていますが、休日は拘束のみであれば連続していない方が望ましいです。


Ans.

実装しましたが、今回の例では、回避することが出来ず同じになります。

証明は、以下に示しますが、結論的には、予定をハード制約と考えると、物理的必然となるからです。

<実装>

以下が追加した制約になります。


<解>

依然として休日2連勤拘束のままです。

<ハード制約化>

言うことを聞かないときの最終手段は、ハード制約化です。ソフト制約のレベルを空白にするとハード制約となります。



<解>

すると次のようなエラーとなりました。この状態は、解がないことを示しています。解がない理由は、ハード制約同士に矛盾があるからです。矛盾要因は通常、複数あります。要因がヒントとして列挙されています。



特に、茶色枠部に着目します。クリックすると当該予定にマークが付き飛びます。


<解析>

以下の予定を見ると、X大医師の当直があり、なおかつ他の整形の先生方が両日共休みになっています。



他の整形の先生方は、全てハード予定休みであり、かつX大医師が2日共、宿・当直ですので、希望を出していないX先生が2日共、拘束勤務を行うしかない状況です。つまり、

■X先生が2日共、拘束をやる

選択一択しかありません。しかし、ハード制約で2連拘束休日禁止にしたので、矛盾となり解が存在しません。

以上、ハード制約化することにより、要因が列挙され原因が特定できました。

予定をセル毎に任意のレベルのソフト制約化すれば、回避できますが、予定はハード制約のまま、制約もソフト制約にしておいて、必要がある場合のみ上記のような解析を行うのが通常です。いずれにせよ、このシステムを使いこなすには、制約に対する深い理解が必要である、ということです。GUI操作自体は、誰でも出来ますが、エラーメッセージを理解して原因を理解するには、ハード制約とソフト制約を理解する必要があります。

また、今回追加実装した程度の制約追加は、造作ないことですので、ユーザ様ご自身が必要に応じて追加することも可能です。


2024年5月27日月曜日

Q.x医師の拘束が25日及び26日の連続となっている。24日の宿直も含めれば 3日連続となっている。

 Q.x医師の拘束が25日及び26日の連続となっている。24日の宿直も含めれば

 3日連続となっている。

  対策案

  ソフト制約として、「拘束は、当直、宿直又は宿日直による拘束の兼務と

  当直、宿直又は宿日直の終了日が休日の場合の拘束の連続以外は、連続しな

  いのが望ましい。」を追加する。


Ans.  

既に、拘束2連勤禁止は、ソフト制約の高い重みで入っていたのですが、組み合わせ最適化の結果としてそういう解になっていました。ご要望の仕様も実装が可能と思いますが、簡単な実装という意味で、当直・拘束3連勤禁止として、追加実装しております。


常Anyラベル3連勤を禁止します。

常Anyラベルは、以下のシフト集合です。短時間夜勤とその他以外の全ての勤務となりますから、一般の宿直関係と拘束を全て含みます。



2024年5月26日日曜日

Q.発注の仕方について

   こちらは自治体病院ということもあり、発注の仕方も限られてきてご迷惑を

  おかけしますが、もう少し具体的に教えて下さい。

  こちらの都合では、例えば年間の契約料を委託料としてお支払するには、作業項目

  毎の金額の入った見積書をいただき、それに基づいて契約を取り交わして、

  委託料をお支払するのが良いような気がいたしますが、これまでのことなども

  あると思いますので、どうすれば良いのか教えてください。

Ans.

既に、プロジェクトサービスをご利用中ですので、以下のストア買い切り版+プロジェクト作成サービス一択になります。

https://www.nurse-scheduling-software.com/japanese/service2/onetime_purchase/

国公立大学病院、地方自治体で、ご利用頂いておりますが、全てこの形態です。

基本的には、1年の間に使いこなして頂いて、後はサポート不要という形まで持って行く、のが望ましい姿になります。なお、物理的には、同時使用10台可能ですが、ライセンスは、一部署一ライセンスとなります。(Ex.看護部で使用するには別ライセンスが必要になります。)

今まで上記のようなご要求を頂いたことはなく、ご容赦頂きたいと思います。

もしも将来、「プロジェクトを大変更したいので作業を依頼したい」、という場合は、その都度お見積りしますが、最悪、今回サービスの再発注になるとお考え頂ければよいかと思います。




2024年5月25日土曜日

Q.下図がどのようなものか理解できませんでした。もう少し具体的に教えてください。

 (C)予定入力画面です。人力解を予定として入力したものです。


これは、以下のように作成しました。

今月ブランク予定(A)から出発します。


次のように希望勤務、可能勤務、禁止勤務を入力した予定を作りました。以降、変更することがないので、ロック(黄色枠)しています。(B)

この予定状態で、解を求めたのが自動解になります。

これに対して、さらに人力解を加えたのが、最初の予定です。(人力解部の予定は、ロックしていないので、全クリアすれば、本画面となり、いつでも本状態に戻ることが出来ます。)

人力の場合、Excel等で、A)状態から出発し、B)状態まで入力、さらにC)状態まで、考えながら行くと思います。全く同じように入力しても構わないというです。


大きな違いは、

■人力だけの場合、このまま行って解が存在するという保証がない/破綻しない保証がない、のが

■スケジュールナースで時々求解すれば、解が存在し組み合わせ最適化の最適解を常に示せるという安心感があります。


逆に、組み合わせ最適化の最適であるので、人力でこれから入力していってもこれを下回る数値(目的関数値)になることはあり得ません、これから如何に入力しようとも、これより良くなることはありません、ということを示しています。

そういう解の提示を常にできるというところが、大きく違います。自動解が気にくわなければ、人力解のようにして入力することもできます。また、予定そのものも、一つ一つのセルをソフト制約にすることも出来ます。どうしても解がない場合は、予定の「最小」変更で解が存在するようにもできる、ということです。

つまり、必要なら、従来と同じように、管理者が好きなように入力できるということになります。


2024年5月24日金曜日

赤マークの意味

 質問

1 赤色マーカ部分で「当直3連続の絶対禁止」に1がカウントされている件

   絶対禁止と言う表現からは、当直3連続はあってはならない気がしますが、

   解として許されてしまうのでしょうか?どのような状態なのでしょうか?


2 赤色マーカ部で「休日の拘束2連勤禁止」に1がカウントされて

  いる件

    上記(1)と同じように赤色マーカ部として

    「休日の拘束2連勤禁止」に1がカウントされています。

    現実的には、先生方の希望を考慮するとこうならざる負えないことは、

    先日のやり取りでわかりましたが、このように赤色マーカ部の該当する

    ものがあっても構わないものなのでしょうか。


Ans.1)

これは、制約名であって、そのように制約することになることを意味しません。具体的には、以下のようにソフト制約レベル7になっておりソフト制約です。ですので絶対ではありません。その名前の通り絶対にしたい場合は、7をブランクにすればハード制約となり、絶対になります。



Ans.2)

赤になるのは、レベル7として入力したソフト制約です。以下においてレベル7のソフト制約を違反した場合、赤マークとなります。




レベル7の実際の重みは、以下の求解画面で設定しますが、ユーザが自由に設定して構いません。



通常、レベル7は、ソフト制約でも破るのは禁止という意味で使います。注意喚起のためだけで実際の求解結果には影響しません。たまたま、今回、そのように設定してあるだけで深い意味はありません。重みを調整するのは、お客さまにプロジェクトファイルが届いたあと、お客さまご自身で行っていただきます。


2024年5月23日木曜日

cuPDLP-C GPUベースのLPソルバ

https://github.com/COPT-Public/COPT-Release


Firstオーダを評価したことがあるのですが、今回の論文を見てもやはり精度に難があり使えそうにはない、という風に見ます。精度が要求されないなら、例えばPageRankクラスのとんでもない規模や、機械学習には、その用途があるかもしれないのですが、NSPでは難しいと見ます。ジュリアンホール教授のHighsソースにも実装されていました。Highsソースを読んでいで、cuPDLPに気づきました。



商用ソルバとの性能差を指摘されるとすぐにお金の話が出てくるのは、いつもの事です。ジュリアンホール教授は、将来的には、有償のプレミアムバージョンを考えているようです。

HIGHSのIPMパラレル実装が今年度末までに予定されているので、そちらの期待の方が未だ高いです。

余談ですが、Highsチームには、MIPソルバ開発者が現在いないはずなので、今後の進捗にはあまり期待できないのですが、最近のソースを見た限り一応の形には、整いつつある印象です。ノードTreeのパラレル化を考えているようですが、これはさほど効果がないと思います。商用MIPソルバの最大差は、恐らくHeuristicsにあります。これは問題ごとに多くの最適化手法が存在するので、物量がものを言います。NSPもそのうちの一つです。しかし、これだけの少ない行数で、ここまで性能が出るとは、私も思っていなかったで、大したものだと思います。CBCのソースに比べれば圧倒的に読みやすいです。その中身については、何故かMatlabで解説がありました。


ともあれ、現在実装中のアルゴリズム5にHighsを入れようか迷っています。殆どのNSPでは不要なのですが、オールランドに構えるには、入れた方が良い結果になるかもしれません。とりあえず、入れ込んでみて評価してみようと計画中です。

既にLPソルバでは、GurobiをしのぎCOPT(中国)が世界を席巻しています。Gurobiがこのままでいるはずはないとは思いますが、LSIツールにせよ、最適化ツールにせよ、今まで米国が世界をリードしてきた構図が変わろうとしている時代の変革期だと思います。産業基盤の核になる技術は、そのまま国力に比例すると思うので、日本の若手研究者に頑張ってもらいたいと思います。

で、上記が何の役に立つかというと、実際的なNSPには関係しません。しかし、世界記録を更新するうえで、上記のLPソルバの向上は、至上命題です。今までCOPTのライセンスを一時的に借用しようと思っていたのですが、やはりそうなることになりそうです。


2024年5月22日水曜日

Q.解はでました。

Q. 解はでました。

助言もなんとか理解し、進めました。解の「入りの回数」のみ、黄色か赤ででますが、どうすればいいのか

ここのみわかりません。


Ans.

https://schedule-nurse.blogspot.com/2024/04/blog-post_17.html

の5)を思い出します。


入りの回数がおかしいのですから、制約がまともに動いていません。

マウス中ボタンを押してグループ集合とDay集合を確認します。

Day集合をみると、あれれ、1日しか集合定義されていませんね。この制約では、対象が1日しかないので、解としては、0か1しかないのです。当然の動きです。制約が思い通りに動いていないのなら、制約を見直しましょう。

次のように、正しく修正します。

これで、一応はまともな入り回数になりました。
レベル7のエラーが赤になります。これは、慣用的にソフトエラーであってもあってはいけない、という重大違反を意図していますが、その重みはユーザが自由に設定できます。レベル7は、注意喚起のためと思ってください。求解動作には、なんら影響しません。

次のような解となりました。



医師当直表・拘束表のペア制約

 このプロジェクトでは、多くのペア制約があります。次の仕様を実現するためです。

1)日直・宿直・宿日直は、平日の勤務を除く時間帯であり、切れ目なく常に一人が勤

    務していること。30分以下の空き・重複も許されない。

2)拘束は、平日の勤務を除く時間帯であり、内科及び整形外科の常勤医師一人が自宅

    待機等していること。  30分以下の空き・重複も許されない。

「切れ目なく」を保証するには、非常勤医師の早遅に起因する短時間宿直を常勤医師の誰かが行う必要があります。さらに条件として、非常勤医師の出身医局に応じて、常勤医師の誰が行うかが決まっています。そのために、次のような枠組みで制約しています。

非常勤医師が医局内科1の場合の常勤医師の対応です。「AならばB」で制約します。


医局内科2も全く同様です。茶色枠部だけが異なり、上と全く同じ制約です。
内科3、整形外科、泌尿器科、腫瘍内科も全く同様です。

次は、タスクの制約です。泌尿器科、腫瘍内科、外科等の診療科は、常勤医師では存在しません。そこで、院長等でそれらをタスクとして出来るようにしています。そうすると、制約しないと、浮遊状態となり、不用意に出現してしまうことがあります。出現しないようにするための制約です。タスクとしてあり得るのは、非常勤医師がそのタスクを行う場合に限るのですから、それ以外は、禁止にします。

常勤医師の短時間宿直が、出現しないようにするための制約です。短時間勤務があり得るのは、非常勤医師がそのような勤務をした場合に限るので、それ以外を禁止にします。

これは、休日宿日直が、通常、分割されないようにするための制約です。宿日直が、日直と宿直に分割されるのは、非常勤医師が分割勤務のみの場合です。例外として、前日の長時間手術等で負荷が大きい場合、意図して分割する場合もあるので、弱めのレベル2としています。

同様に、拘束宿日直が、通常は分割しないようにします。

以上、ペア制約について記述しました。整然と記述しているように見えますが、どちらかというと、出てきた不具合に対応するため、追加・変更を繰り返してきた結果です。


そのようなやり方でも、方法論として間違いではありません。しかし、追加変更する際に、より一般的な制約の仕方はないか?常に整理統合を意識しているところが大事な姿勢だと思います。そのようにして、仕様と制約を明確化する、文書に残しておくことがメンテナンス上必要なことです。


大変に複雑なシステムの場合、ユーザさま自身の開発協力は不可欠です。

1)出てきた解に対して、どこがいけないかを指摘すること

2)1)は、特殊ケースですが、それを一般仕様化、制約化する能力

がユーザさま側に必要になるということです。いわば、日本語のキャッチボールが出来ることが開発での必要要件となります。言葉として表現されてこなかった仕様(隠れ仕様)が顕在化され、必要な仕様が明確化される過程でもあります。お客さま自身が意識していなかった仕様が、実は重要な制約のために必要だった、ということでもあります。

そうした過程で大事だと思うのは、人の話をきちんと聞いて、それに対する適切なレスポンスをする、他人に対して、知らない前提で話をする、当たり前のことです。こうした開発でユーザ側の必要スキルは、プログラミング経験や、集合に対する理解ですらありません。日本語のキャッチボールがきちんと出来る、これに尽きると思う次第です。








2024年5月18日土曜日

解が出ないのですが。。

 https://schedule-nurse.blogspot.com/2024/04/blog-post_17.html

の4)を思い出します。

しばらく触っていないと皆さん、やり方を忘れますが、忘れないくらいに繰り返し練習を積むことが肝要です。


次のような赤で表示されているのところをダブルクリックすると要因ヒントに飛びます。



一つは、列制約です。これはソフトレベルが空欄なのでハード制約です。
この制約を読むと、「看護師の明けは、一人よ。」と言っています。

一方、Scheduled.名前 日付の方をクリックすると、明けが二人います。解がないのは、ハード制約の矛盾です。列制約で一人と言っているにも関わらず、予定として二人が入力されており矛盾です。これは、恐らく先月からの流れでそうなっているのだと思います。
過ぎ去った過去は変えることが出来ないので、列制約の方をソフト化します。具体的には、レベル7を入力し、求解します。

すると、今度は、次のようなエラーとなりました。これも、先月からの流れのようですので、該当する行制約をソフト化します。


以下のようにレベル7を入力しソフト化しました。

求解すると、次のようなエラーとなりました。

これは、面倒なので、ソフト化して逃げることにします。

今度は、次のエラーとなりました。
これも先月からの必然のようですので、ソフト化して逃げます。

ようやく解が出ました。未だ、沢山の問題(赤部)が出ているようですが、
解が出れば、何が起きているかは、推定できると思います。


頑張ってください。復習と練習が大事です。


特に、2)3)の基本に立ち返って冷静に、何が起きているかを考えることをお勧めします。

https://schedule-nurse.blogspot.com/2024/04/blog-post_17.html





2024年5月15日水曜日

医師当直表・拘束表 出身診療科属性

 非常勤医師の勤務形態により常勤医師は、30分ー90分の短時間宿直勤務がありえます。その短縮勤務を誰が行うかについて、常勤医師と非常勤医師とで出身医局が同じが望ましい、という制約があります。

具体的に決まっていますが、個人名で制約するのではなく、一旦グループ属性で表現して抽象化することをお勧めします。


このようにしておけば、制約は、個人名ではなく、グループ集合名になります。
上のスタッフプロパティシートを変更するだけでメンテ完了する可能性も高まります。


2024年5月14日火曜日

医師当直表・拘束表 列制約の実装

 このプロジェクトの列制約は、全てハード制約です。

まずは、簡単な制約から。休日のみに存在するシフトは、平日禁止にします。


次に、次のコア部シフト制約について制約します。

1)日直・宿直・宿日直は、平日の勤務を除く時間帯であり、切れ目なく常に一人が勤

    務していること。30分以下の空き・重複も許されない。

2)拘束は、平日の勤務を除く時間帯であり、内科及び整形外科の常勤医師一人が自宅

    待機等していること。  30分以下の空き・重複も許されない。

3)日直、宿直又は宿日直を常勤医師が行った場合は、担当科の拘束医を兼ねる。


次に各フェーズについて制約します。平日は、PH0が存在しません。PH1/PH2のみになります。休日は、PH0/PH1/PH2が存在します。

このプロジェクトは、スケジュールナース史上、最も複雑なプロジェクトですが、シフト、フェーズ、タスクを駆使することで、ほぼPythonで記述することなく出来ています。

2024年5月11日土曜日

医師当直表・拘束表、当直・拘束の平準化

 重要視しているのが、下記の4項目らしいです。

休日宿直数

平日宿直数

休日拘束数

平日拘束数

これらが、フラットになるように平準化します。いつものスタッフプロパティで記述します。基本的には、最大と最小で記述します。


各カウント対象のシフトをシフト集合で指定します。


休日の値については、整数制約で記述しています。
これは、ユーザ指定で、宿日直24時間勤務を1として、日直や、宿直等、24時間に満たない勤務は、0.5とカウントする、という指定に基づくものです。

なので、シフトでは、対象シフトにカウント数を設定しています。当直関係と拘束関係では、カウントが異なります。

例えば、宿日直シフトは、実際には、拘束も24時間行っていますが、拘束のみを行ってフェーズはありません。(●部分は、全て〇があります。)この場合、拘束カウントは0です。拘束カウントは、○のみの部分があるところのみカウント対象となります。

イメージ的には、24時間勤務である宿日直を3として、日直は、8時間なので、1、宿直は、16時間なので、2とした方が、勤務時間と比例するカウントとなるので合うような気がしますが、ユーザ指定の通りとしています。

マッピング、1→2,0.5→1は、必要になりますが、ご理解頂けるでしょう。


2024年5月10日金曜日

医師当直表・拘束表問題 状態定義

 改めて、定義とルールを書き出します。

定義


1)日直は、休日の昼間時間帯の勤務である

2)宿直は、夜間帯の勤務である

3)宿日直は、日直と宿直を合わせた勤務である。

4)拘束は、日直、宿直又は宿日直の時間帯の自宅待機等である。


ルール


1)日直・宿直・宿日直は、平日の勤務を除く時間帯であり、切れ目なく常に一人が勤務していること。

  30分以下の空き・重複も許されない。

2)拘束は、平日の勤務を除く時間帯であり、内科及び整形外科の常勤医師一人が自宅待機等していること。

  30分以下の空き・重複も許されない。

3)日直、宿直又は宿日直を常勤医師が行った場合は、担当科の拘束医を兼ねる。


どのように状態を割り付けるか?について考えてみます。今状態としては、当直状態・拘束状態・診療科状態の3つがあります。スケジュールナースの状態ダイレクトに表現可能なのは、シフトとタスクの2状態のみです。今、診療科をタスクとすると、シフトは、当直状態と拘束状態を兼ねたものにするしかありません。従い、ありえる当直状態と拘束状態を全て列挙しそれをシフト名とする、ということが必要になります。

とりわけ難しいのは、3)ルールです。

非常勤医の勤務は、予定のみでしかありえないので、特段に制約は必要ありません。制約が必要になるのは、常勤医のみです。どのシフトがどの状態を指すかは、スケジュールナース上では、陽には分かりません。つまり、下の定義表を失ってしまうと、後で制約を見ても全く理解できない恐れがあるということになります。設計の根幹をなす表ですので、失くさないようにしてください。これから制約を書いていきますが、常にこの表を参照します。



なお、宿直カウントと拘束カウントは、平準化するための制約で使用します。

勤務表制約で難しいのは、定義とルールを表により整理せずに、いきなり制約を書くことはできない、ということです。これらは、AIは決してやってはくれません。人間がやるべき作業になります。


2024年5月9日木曜日

医師当直表・拘束表 非常勤医は、予定以外自動アサインしない

 シフトの自動アサインは、全てのスタッフが対象です。チェックを外すと全てのスタッフが自動アサインがオフになってしまいます。今回は、非常勤医のみ、予定だけの勤務となるようにしたい、ということです。

実装としては、以下のようなPythonになります。非常勤医師で、予定に何も入っていなかったら、「その他」にしてしまう、という単純な実装になります。簡単ですので、Pythonが分からなくとも、コピペして利用可能と思います。

import sc3

for person in 非常勤:
    for day in 今月:
            if shift_schedules[person][day][0]=='':
                v=sc3.GetShiftVar(person,day,'その他')
                s='非常勤は予定入力がないときはその他 '+staffdef[person]+' '+daydef[day]
                sc3.AddHard(v,s)
                

2024年5月8日水曜日

医師の当直表・拘束表問題のタスク設定

 タスクは、診療科にします。これで、休日のフェーズ0/1/2,平日のフェーズ1/2に対して縦の列を制約します。診療科の設定は、自由ですので、将来の追加・変更は容易であり拡張性を有しています。これをシフトのみで行おうとすると破綻すると思います。

もう一つのポイントは、NoTaskVarを使用しない、ということです。これまでのフェーズモードを有する勤務表では、これを用いる例が殆どだったと思いますが今回は用いません。

この特殊タスクの説明は、難しいので割愛しますが、これを用いないということは、定義されている全てのフェーズでタスクがFillINされている必要がある、ということになります。例えば、宿日直というシフトのフェーズを見ると24時間、フェーズ0/1/2でチェックがされています。このときフェーズ0/1/2のいずれでも、以下のタスクのどれかが必ず割り当てられるという意味になります。空きや重複はありえません。必ず、定義したタスクのどれかが割り当てられる、という仕様になります。

シフトは、以下のように最終的になりました。拘束勤務をしつつ、日直や宿直、宿日直、短時間宿直があり得るので、どうしても状態数が増えてしまいますが、これ以下にはできません。通常の勤務や休みは全てまとめて、「その他」シフトにしています。このシフトは別な意味で便利で、当直したくない日には、予定を「その他」で埋めてしまえばよい訳です。「30分以下の空きや重複がない、切れ目なく」の実装は、シフトで保証します。縦の列はタスクで保証します。

以上のフレームワークで記述した解(人力解)の例が以下です。宿日直は、3フェーズ24時間、宿直は、2フェーズ16時間が割り当てられていることが分かります。連休中も、宿日直や、拘束が連続してある、という過酷な勤務になっていることが分かると思います。宿日直のあとオンコール(呼び出し)があったなら、何時間寝ないことになってしまうか想像してみてください。恐らくは日本だけの特殊勤務形態ではあります。しかし、日本の医療は、こうした過酷な勤務をこなしている勤務医によって支えられている、ということではないでしょうか?