2023年10月3日火曜日

リニアペア制約追加

 リニアペア制約による記述


AならばBによる記述との対比


演算子は、==、>=、<=の3種類、オフセットは、+-1、+-2...をサポートしています。

係数は、A用とB用で独立してあり、各々ACoff ,BCoffとすると

ΣACoff*A[i] 演算子 ΣBCoff*B[i] +offset

となるように制約します。

用途は、数理的表現が必要な場合用です。殆ど場合、上のように、現状のペア制約で事足りますが、数理的表現の方が、簡潔に記述できる場合があります。

例えば、新人二人につき一人の指導者がつく制約を考えます。

新人数 指導者数

-------------------

0     0

1    禁止

2             1

3           禁止

4    2

.....

このような場合、論理的制約では、多数の制約記述が必要となります。数理的記述が可能ならば、Aを新人、Bを指導者として

ΣA[i]==2*B[i]

という風に、一つの制約で、記述できます。

どちらかというと、数理ソルバーでは、Nativeな記述ですが、論理的ソルバーでは、苦手な記述となります。

2023年10月2日月曜日

スタッフ毎のシフトタスク設定でフィルタを追加

 フィルタには、なっていませんが、フィルタという名前にしてしまいました。

新しいスタイルでは、スタッフ毎のシフト・タスクのチェックを割りに頻繁に変えることがあるかもしれません。そうなると、各スタッフのプロパティを参照したくなるので、追加しました。(未だリリースしていません。今月10日頃の配信となります。)


2023年10月1日日曜日

変則2交代の希望シフト統計を基に実装に関する一考察

次のような体感統計となりました。

1位:休み

2位:明休

3位:入(休入、日入)

4位:日

1位の休みは、当然として、意外に多いのが、明けの後の休み、これは、明けの後の休みが当然ではない、の裏返しではないでしょうか?

3位の入りは、休入、日入まで、指定したものがあります。これは、長入明パターンを阻害します。

「明けの後の2連休を出来る限り多く」、というのが多くの職場で要求されるパターンですが、それに関して、最も効率がよいのは、長入明パターンだけで構成される職場です。

仮に、入り回数が4回以下という条件であれば、

長入明休休..長入明休休..長入明休休..長入明休休

という状態となり、4週8休の条件下で、全ての明けを2連休とできる可能性が高まります。

入り回数が5回以上なら、その条件は物理的に満たせません。

理想的には、

■入り回数4回以下

■全員、長入明パターンのみ

■勤怠システムは、長入り差を+-1まで許容する

であることです。長入明パターンのみで構成されれば、長入差は、自動的に+-1以内となり、重い==制約を使わなくてもよくなります。

しかし、現実には、

■入り回数5回以上がある。(職場によっては、殆ど6回のところもある)

■上記のように個人の希望により、長入明パターンだけには、できない。

■勤怠システムのなかには、長入差は、+-0を強制される場合がある

等により、上記理想条件をキープ出来ずに、2連休達成は、物理的に不能な場合が存在します。また、長入パターンだけに出来なければ、長勤務者数、入り勤務者数を揃えるために、また、新たな勤務、単独長(長休),遅出 等が必要になってきます。そうすると、長入差を埋めるための、非番や、半休といったものが必要となり、複雑な条件を達成するため、求解性能の劣化が懸念されます。

上記希望シフトに着目します。それは、長日希望が一つもないことです。数ある希望シフトで、長日が一つもないということは、出来れば長日はやりたくない、ということが伺えます。それに対して、入り希望はあり、長入明パターンを阻害する希望シフトが入り込むことがありえます。

以上の知見から、次のような二つのグループに分ける発想を得ました。

■A 長入明パターンだけのグループ

■B  長だけ、単長あり、休入、日入り等、イレギュラーパターンを含むグループ

Aグループだけで、見れば、長入明勤務者数列は、常に自動的に一緒になります。また、長入差は、自動的に各スタッフで+-1を保証できます。

Bグループだけで見ると、長入明勤務者数は、一緒になりません、また、長入差も存在することになります。長だけの人は、半休もしくは非番とセットにする必要があります。Aグループの長入明勤務者数は、常に一緒であるので、Bグループの長入明勤務者数も常に一緒であることが必要です。つまり、Aグループ、Bグループ単位で長入明勤務者数が一緒である必要があります。具体的には、Bグループに長だけ行う人だけが場合は、長入明勤務者数一緒列が常に成立しないので、長を行わないことがあるスタッフを組み入れる必要があります。(月毎にグループ人員を入れ替える案もあります。)

このようにする背景は、Aグループが大半とし、少数のBグループとすることで、煩雑な部分を限定することができ、結果として、処理速度の向上、結果的に、明け後2連休の達成度を上げることにつながる、ということがあります。

本来は、全スタッフをBグループとしても、同じ求解品質となることが理想ですが、残念ながらそうではありません。Aグループは、シンプルパターンのみ構成され、一見自由度がないように見えますが、行列の要件に対して達成しやすいパターンであるとも言えます。解空間は減少しているように思えますが、真の解に近いパターンのみが生き残り易いとも考えられます。

それに対して、Bグループは、煩雑なシフトに対して、行列要件を満たすことが必要となります。特に==処理は、AL1にとって重い処理となります。

Bグループでは、Bグループパターン用の特入りを追加します。(Aグループ内では、特入りは、不可です。)

これは、日特入、休特入を可能にする入りです。希望シフトでこれを記入することが可能となります。また、Bグループでは、各スタッフ、

■長日数<=2*非番数+半休+特入り

■長日数>=特入り-1

に制約します。これにより、長の余り数は+0-1に制約されます。

この処理が重いのは、多数のシフトがこの等式に拘わっていて、なおかつ、>=と<=により両サイドから制約されることにあります。なので、Bグループはなるべく小さく、しかし、十分にBグループ内で、長入明勤務者数列が一緒に出来るようなスタッフ数としてください。

通常は、ソルバの性質にユーザは深く立ち入る必要はないのですが、こと長日数差が制約として必要となる場合は、以上のような観点での制約設計とすることが、皆の幸せにつながる(希望の通り易さ)と思います。

なお、長日数差が勤怠システムの中で制約の必要なければ、非番、半休は必要ないし、==処理も不要となるので、上記のような面倒なことは、必要ありません。



2023年9月30日土曜日

ユーザ勤務表からスケジュールナース予定へのインポート

 顧客仕様を実装するにあたって、最初に行うのは、仕様の検討です。具体的な実装の目安がついたら、次に行うのは、顧客勤務表Excelから、予定へのインポートです。つまり、人力解(Excel)から、予定へインポートする作業です。それには、一部署でしたら、手入力してもよいのですが、多数の部署の場合は、インポートするスクリプトを書きます。

インポートの目的は、

1)初期状態の把握(人力解の達成度把握)

2)実装の検討、確認用

です。

で、インポートしてみると、多数のハード制約違反が見つかるのが普通です。私の理解の誤りも見つかりますが、大半は、お聞きしていた仕様そのものが達成されていないことにあります。

実装にあたっては、本当に達成そのものが難しいものがあります。当然そのような制約は、ソフト制約としますが、例えば、6連勤務の禁止や、明けの後の休み、等、これは当然ハード制約だろう、と思える制約も、時々達成出来ていないことが把握できます。

しかし、多くの達成できていない中でも、達成できている項目があるのも事実です。そういう風に人力勤務表を眺めていると、作成者が何を最重要視しているかが、判るときもあります。

お伝えしたいのは、多くの労力を要した勤務表作りの経験は、スケジュールナースにおいて無用ということではありません。むしろ、必要なことなのではないか? とも思います。何が大変か?という経験は、ほぼそのままスケジュールナースのエンジンでも、同じように大変なことなのです。

<インポートスクリプト例>


import os
import ctypes
import win32gui, win32con


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='xlsx',
    Title=title,#'Excel予定シートを開く',
    Filter=filter,
    CustomFilter=customfilter,
    FilterIndex=0)
    print ('読み込みファイル名:', repr(fname))
    print(fname,flags)
    return str(fname)

def output_question():
    MessageBox = ctypes.windll.user32.MessageBoxW
    res=MessageBox(HWND, 'スタッフ希望を入力しますか?', '人力解全体を出力しますか?', 3)
    print(res)
    if res==7 or res==2:##いいえ、キャンセル
        return False
    return True

def post_main():
    
    #import pdb
    #pdb.set_trace()
    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 = True
    os.chdir(project_file_path)
    #file=os.path.join(project_file_path,"ex1.xlsx")
    #file.replace("/","\\")
    file=get_open_file_name("人力解を開く")
    wb=xl.workbooks.Open(file)

# Excelシートオブジェクト
    ws = wb.Worksheets(1)
 
    # 指定したシートを選択
    # Select()の使用前にシートのActivate()が必要
    ws.Activate()
    #v=ws.Range("A3").Value
    #print(v)
    wk = ws.UsedRange 		# 使用範囲を取得
    columns = wk.Columns.Count	# 行数
    rows = wk.Rows.Count		# 列数
    cells = wk.Value
    days=0
    day_list=[]
    for col in range(0,columns):
        v=cells[0][col]#,colws.Cells(1,col).Value
        if v!=None:
            days+=1
            day_list.append(v)
            #print(v,v.year,v.month, v.day)
    
    print('Days',days)
    name_list=[]
    data_list=[]
    preferred_list=[]
    for row in range(3,1000,3):
        v=ws.Cells(row,1).Value
        if v !=None:
            print(v)
            name_list.append(v)
            list=[]
            plist=[]
            for col in range(2,2+days):
                v=ws.Cells(row+2,col).Value
                list.append(v)
                v=ws.Cells(row+1,col).Interior.ColorIndex
                plist.append(v)
            data_list.append(list)
            preferred_list.append(plist)
        else:
            break
    wb.Close()
    print("persons=",len(name_list),len(data_list))
    print(data_list)
    print(preferred_list)
    output_preferred_schedule=output_question()
    file=get_open_file_name("書き込みする予定ファイルを選択")
    wb=xl.workbooks.Open(file)

# Excelシートオブジェクト
    ws = xl.Worksheets(1)
 
    # 指定したシートを選択
    # Select()の使用前にシートのActivate()が必要
    ws.Activate()
    start_col=0
    for col in range(2,100):
        v=ws.Cells(2,col).Text
        #print(v)
        if v!=None and v=="1":
            start_col=col
            break
    print(start_col)
    start_row=4
    wk = ws.UsedRange 		# 使用範囲を取得
    columns = wk.Columns.Count	# 行数
    rows = wk.Rows.Count	
    for r in range(rows+1):#len(name_list)):
        name=ws.Cells(r+start_row,1).Value
        processed=False
        for k in range(len(name_list)):
            if name==name_list[k]:
                processed=True
                for c in range(days):
                #print(r,c)
                    if output_preferred_schedule:
                        if preferred_list[r][c]>0:
                            ws.Cells(r+start_row,c+start_col).Value=data_list[k][c]
                    else:
                        ws.Cells(r+start_row,c+start_col).Value=data_list[k][c]
        if not processed:
            for c in range(days):
                ws.Cells(r+start_row,c+start_col).Value=''#clear

    wb.Save()
    wb.Close()
    xl.Quit()

post_main()

2023年9月28日木曜日

スケジュールナースでの制約設計の考え方

制約設計に携わる方々のためのガイドです。 

1)能力差を知る

対決! 勤務表作成名人 vs 最強エンジン (nurse-scheduling-software.com)

素の能力が違います。どんなに能力に長けていても、最後は、トレードオフとなります。しかし、人のそれと、スケジュールナースのそれとは、全くレベルの異なるものです。スケジュールナースユーザは、物凄く精緻な制約でのトレードオフを行いがちなのですが、初期の能力差を思い出してください。人間には、出来ない次元でのトレードオフになっていると思います。つまり、人間より良い勤務表という意味では、とっくに達成されています。そこに時間をかける意味があるかどうか?自問自答する必要があると思います。

スケジュールナース適用前の人力解の実力を把握しておくことは、以降機会がないと思うので、特に適用前に測定しておくことをお勧めします。

2)稼動時と制約設計時では、考え方が異なる

制約を設計するときは、しっかりと原因解析を行います。ハード制約違反が、ルールの設定ミスを教えてくれることもあるからです。しかし、稼動時は、エラー指摘に従い、入力の修正だけを行い、エラー解析はしません。解析自体が面倒でありスキルを要するので、一般の方が行うには、ハードルが高すぎます。

希望休み数が多い職場が良いとは言えない (nurse-scheduling-software.com)

予定ブランクでは、エラーがない、もしくは殆どない、ということが目指すべき状態です。スケジュールナースの能力を最大に発揮するためです。エラーがない、ということは、未だ余裕があることを意味します。つまり、解空間がまだ広い状態です。どれだけ広いかは、分かりませんが、余裕があることだけは確かです。で、ブランクでも既に余裕がない状態、つまり、エラーがある状態からのスタートは、解空間に余裕がないのですから、残された選択肢もあまりない、という状態からのスタートになってしまいます。

3)制約することは、解空間を狭める。効率の良い制約設計

制約することは、解空間を狭めます。全員の負荷をフラットにすることも解空間を狭めます。

制約は出来るだけ少ない方が良いのです。

例えば、多くの職場で共通にある要望は、夜勤後の2連休を出来るだけ増やす、ということです。これに関連してのユーザから要求を多々頂くのですが、却ってその達成を難しくする制約も含まれていることがあります。

https://schedule-nurse.blogspot.com/2023/09/blog-post_5.html

一般に、

■行パターンの長い制約

■==制約

■プリセプタプリセプティ・会議

は、重い制約となります。(計算リソース負荷が重い制約と、解空間を極端に狭める制約と2種類あります。計算リソースを食う制約を実装すると解空間はそれほど狭めていないはずなのに、求解性能が落ちます。ソフト制約も広くはこの類になります。上二つは、計算リソースを食う制約、最後の一つは、明らかに解空間を狭める制約です。)

また、ギチギチに負荷分散すれば、まともに解が出なくなります。負荷分散を適度に緩めつつ、2連休達成を阻害する制約を、実装しないようにします。制約が多くなれば、難しくなるからです。多くの細かい制約を実装し、達成することは、Primary制約、夜勤後の2連休の達成を難しくします。Primary要件を達成した上での「もしも可能ならば」制約なのか? それとも、Primary要件よりも優先すべきなのか、を意識するようにしましょう。(往々にしてあるのは、多くの細かな制約を実現しようとして、Primary制約の方の達成度が落ちるという事象です。)

ユーザ要求の全てを満たせなくても良しとしましょう。1)で見たように、既にスケジュールナースの解は、人のそれを遥かに上回る精度と品質を同時に達成しています。




2023年9月23日土曜日

放射線技師の勤務表

 放射線技師の勤務表 (nurse-scheduling-software.com)

ユーザさまのプロジェクトを最近のスタイルに変更し一般化の処置を施したプロジェクトになります。手順は、スタッフ名を変更し、スタッフ毎のシフトと、予定を元のプロジェクトからコピペします。

前は、Pythonで書くしかなかったものも今は、GUIでかなり書けます。看護師、介護士に限らず、今は、殆どのプロジェクトがGUIだけで書けると思います。

IVR,CT,MRI,RI等の専門用語だらけのプロジェクトですが、見る人が見ると判ると思います。

2023年9月22日金曜日

書店のシフト最適化(パート・アルバイトのシフト最適化)

 書店のシフト最適化 (nurse-scheduling-software.com)

南山大学の学生の卒論を題材にしたプロジェクトです。実用性まで今一歩という感じですが、

似たようなプロジェクト作成の参考にはなろうかと思います。

スケジュールナースは、最も難しいナーススケジューリング問題を専門にしているので、こうした分野のプロジェクトは、あまりないのですが、それでもこうした要求は時折あります。いずれにせよ、この種の設定で、他のソフトが出来て、スケジュールナースが出来ない事例は考えられません。逆の事例は、沢山あると思います。


書店のシフト?と思いきや、意外にこうしたシフトは多いので、対応ソフトは激戦区です。

ところで、書店のシフトは奥深いようです。

超書店員によるシフトの作成方法【Part1】 │ 超書店員 (bookseller.work)