メインコンテンツへスキップ
見出し画像

「効いた気がする」を卒業する。AI効果シミュレータを5ファイルでつくり、社内で使える形にするまで

    AIを導入したあと、会議でこんな会話になっていないでしょうか。

    「作業がかなり楽になりました」

    「前より速くなった気がします」

    「評判はいいので、ほかの部署にも広げたいです」

    どれも前向きな報告です。ただし、経営判断に使うには情報が足りません。どの業務で、何人が、何時間使い、導入前と比べてどれだけ短くなったのか。利用料や教育費を差し引いたら、投資はいつ回収できるのか。そこまで見えなければ、導入継続も横展開も「なんとなく良さそう」という空気に支えられたままです。

    そこで今回は、自社の条件を入力すると、月間の削減時間、時間換算額、AI費用、参考回収月数をその場で確認できる「AI効果シミュレータ」をつくります。構成はわずか5ファイル。画面、見た目、計算処理、Cloudflare Workersの入口、配信設定だけです。計算は利用者のブラウザ内で完結し、入力した業務データをサーバーへ送信する設計にはしません。worker側は稼働確認の応答と静的ファイルの配信を担当します。

    狙いは、立派な業務システムを新しく開発することではありません。効果の話を感想から数字へ移し、現場と経営者が同じ前提で会話できる小さな道具を持つことです。この記事では、数式の決め方、短縮率の測り方、5ファイルの役割、ローカルでの検証、共有時の注意点まで順番に説明します。

    画像

    1.なぜ「効いた気がする」では次の投資を決められないのか

    画像

    AI導入の初期には、体感が大切です。使う人が「便利だ」と感じなければ定着しません。ところが、体感だけを成果として扱うと、投資判断の段階で話が止まります。便利という言葉が、作業時間の短縮を指すのか、心理的負担の軽減を指すのか、品質向上を指すのかが分からないからです。

    たとえば、文章の下書きをAIに任せて作成時間が30分から10分になったとしても、確認と修正に25分かかっていれば、合計時間は35分です。下書き工程だけを見れば20分短縮ですが、業務全体では5分増えています。「生成が速い」と「仕事が終わるまでが速い」は同じではありません。

    また、同じツールでも効果は業務によって変わります。定型メールの下書きには大きく効いても、判断材料が社内に散らばる企画書では、情報収集に時間がかかるかもしれません。熟練者はプロンプト作成に手間取り、若手は確認に時間をかけることもあります。ひとつの成功例から全社一律の短縮率を置くと、期待値だけが膨らみます。

    だから、最初に問いを変えます。「AIは役に立ったか」ではなく、「どの業務の、どの範囲が、同じ品質条件で何時間から何時間になったか」と聞きます。この問いなら測定できます。そして、測った数字を人数や頻度と組み合わせれば、月間効果の概算へ進めます。

    ここで大切なのは、シミュレータの数字を絶対的な答えにしないことです。前提が雑なら、結果も雑になります。目的は数字で権威づけすることではなく、前提を見える場所に置き、関係者が修正できるようにすることです。

    2.効果説明が感想で止まる三つの構造

    画像

    効果説明が感想で止まる会社には、よく似た構造があります。第一に、計算式が定義されていません。「削減時間」ひとつを取っても、AIが文章を生成した時間だけを数える人と、確認・修正・承認まで含める人が混在します。週と月の換算方法も人によって違い、4週間で計算する資料と、年間52週を12か月で割った約4.3週で計算する資料が並びます。条件が違う数字を比べても、意思決定には使えません。

    第二に、計算が担当者のExcelで止まります。Excel自体が悪いわけではありません。柔軟で、試算にはとても便利です。ただ、作成者しか計算根拠を知らない、最新版がどれか分からない、数式セルが上書きされる、といった状態になると、数字を説明できる人が限られます。会議のたびに担当者が資料を更新する運用では、検討速度も上がりません。

    第三に、経営者や部門責任者が自分で条件を変えられません。「短縮率を保守的に20%にしたらどうなるか」「対象を5人から30人へ広げたらどうなるか」「月額費用が増えたら回収できるか」といった質問に、その場で答えられないのです。数字は提示されても、前提を動かせなければ、経営者にとっては中身の見えない箱になります。

    この三つは連動しています。式が決まっていないから担当者ごとに計算し、担当者ごとの計算だから共有しにくく、共有しにくいから意思決定者が触れません。逆に、式を決め、入力項目を絞り、ブラウザで触れる形にすれば、一度にほどけます。

    もちろん、すべてを一つの画面で管理する必要はありません。最初の目的は、精密な会計システムをつくることではなく、AI導入の仮説を比較可能にすることです。入力項目が多すぎると、使い始める前に疲れてしまいます。業務名、対象人数、一人あたりの週時間、短縮率、時給換算という最小限の項目から始め、費用を加えて回収の目安を見ます。

    3.最初に固定する入力項目と、数字の意味

    【画像04】

    画像

    管理する項目は、業務名、対象人数、一人あたり週時間、短縮率、時給換算額を基本にし、費用側にAI月額費用と初期費用を置きます。今回の画面へ入力するのは業務名を除く六つの数値です。業務名は記録シート側で保持し、シミュレータは一業務ずつ試算します。まず、それぞれが何を表すかをチームで揃えます。

    業務名は「資料作成」のような大分類より、「週次営業会議の報告文作成」「問い合わせ返信の初稿作成」のように、開始と終了が分かる単位にします。範囲が広いと、短縮された工程と増えた工程が混ざり、測定できません。入力、生成、確認、修正、社内承認までのどこを対象にするかも記録します。

    対象人数は、アカウントを持つ人数ではなく、その業務を実際に行う人数です。一部の人だけが使うなら、その人数を入れます。全社契約だから全社員を入力する、といった置き方は効果を過大にします。逆に、代表者だけの実測結果を全員へ適用するときは、習熟度や業務量の違いを考慮し、まず小さく見積もります。

    一人あたり週時間は、AI導入前に対象業務へ使っていた時間です。ここにも、確認や手戻りを含めます。月末だけ発生する業務なら、月の所要時間を4.3で割って週換算する方法もありますが、季節変動の大きい業務は数か月平均を使ったほうが安定します。繁忙月だけを基準にすると、通常月の効果を大きく見せてしまいます。

    短縮率は、短縮前時間に対して何割減ったかを表します。50%なら入力値は50、計算では0.5として扱います。導入前が10時間、導入後が6時間なら、短縮は4時間なので40%です。導入後が12時間なら短縮率はマイナス20%です。立ち上がり期には、プロンプト作成や学習で時間が増えることがあります。マイナスを入力できる設計にしておくと、不都合な事実も隠さず記録できます。

    時給換算額は、給与そのものに限定しません。会社が業務時間を評価するための統一単価です。人件費、法定福利費、間接費をどこまで含めるかは会社によって異なります。比較に使うなら、部署ごとに都合のよい単価を置かず、同じ基準を使います。ただし、そこで出る金額は「時間の換算価値」です。削減時間分の給与が自動で減るわけでも、同額の利益が生まれるわけでもありません。

    AI月額費用には、ツールの利用料だけでなく、対象業務にひもづけられる継続費用を含めます。運用担当の定常作業、継続教育、追加の監査費用が見込まれるなら、別項目で把握したうえで月額側に含めます。初期費用には、導入支援、初回研修、テンプレート整備、初期設定など、一度だけ発生する費用を置きます。教育費を初期と継続に分けるのは、回収計算を正しくするためです。

    4.計算式を公開し、手計算できる状態にする

    【画像05】

    基本の計算式は三つです。

    月間削減時間は「対象人数×一人あたり週時間×短縮率×4.3」で求めます。4.3は、1年を52週として12か月で割った月平均の概算です。月によって営業日数は違うため、厳密な勤怠計算には向きません。複数案を同じ条件で比べるための係数として使います。

    月間の時間換算額は「月間削減時間×時給換算額」です。そして参考回収月数は「初期費用÷(月間時間換算額−AI月額費用)」です。分母は、毎月得られる時間換算価値から毎月の費用を引いた「月間純効果」に当たります。

    仮の例で計算します。5人が、一人あたり週3時間かけている業務を、AIで40%短縮できるとします。月間削減時間は、5人×3時間×0.4×4.3で25.8時間です。時給換算額を3,000円とすると、月間の時間換算額は25.8時間×3,000円で77,400円になります。AI月額費用が20,000円なら、月間純効果は57,400円です。初期費用が100,000円なら、参考回収月数は100,000円÷57,400円で約1.74か月です。

    この1.74か月は、現金がその期間で口座に戻ることを意味しません。削減した25.8時間が、残業削減、外注削減、処理件数の増加、営業活動への再配分などに結びついて初めて、事業上の成果になります。空いた時間がそのまま未活用なら、時間換算価値はあっても、現金削減や売上増加は起きません。したがって、試算の横に「削減時間の使い道」を書いておくと、実行計画へつながります。

    また、月間純効果がゼロ以下なら、回収月数は出せません。この場合は「回収不能」と表示します。月間純効果がプラスで、かつ初期費用が0の場合だけ、回収月数を0か月とします。この判定順を決めておけば、意味のない負の月数を防げます。

    複数業務を登録する場合は、二重計上にも注意が必要です。同じ30分を「議事録作成」と「会議後の共有」の両方で削減したことにすると、全体効果が膨らみます。業務工程を分けるなら、各工程の開始・終了を決めます。あるいは、重なりやすい作業を一つの業務として測ります。足し算できる数字かどうかを確かめてから合計してください。

    5.短縮率は予想で置かず、三つの業務を一週間測る

    シミュレータで最も結果を動かすのは短縮率です。それだけに、「たぶん半分になる」「生成AIだから70%くらい」と感覚で置くと、見栄えのよい数字だけができます。まず三つの業務を選び、一週間、導入前と導入後の所要時間を測ります。

    選ぶ三業務は、頻度が高い、手順が比較的明確、成果物の品質を比べやすい、という条件から選ぶと測りやすくなります。たとえば、定型メールの初稿、会議メモの整理、既存資料からの要点抽出です。個人情報や機密情報の扱い、利用中のAIサービスの規約は別途確認し、入力可能な情報だけで試します。

    測定では、開始時刻と終了時刻だけでなく、何を含めたかを残します。AIへの指示作成、資料のアップロード準備、出力待ち、事実確認、表現修正、上司確認、やり直しまで含めて、一つの業務が利用可能な状態になるまでを測ります。AIが数秒で回答しても、確認に20分かかれば、その20分は導入後時間です。

    品質条件も揃えます。導入前は顧客へ送れる完成稿、導入後は粗い下書き、という比較では短縮率が大きく見えます。誤字、事実の正確さ、社内書式、必要項目の充足など、最低限の合格条件を決め、同じ水準に達するまでの時間を比べます。品質が上がったこと自体を評価したい場合は、時間効果と混ぜず、別の指標にします。

    一週間の実測値は、平均だけでなく、ばらつきも見ます。初日は操作に慣れず遅いが、五日目には速くなるかもしれません。逆に、簡単な案件は速いものの、例外案件で大きく手戻りすることもあります。件数が少なければ、その数字は確定値ではなく暫定値です。「実測」と「見込み」を画面や記録で区別し、サンプル数と測定期間を添えます。

    短縮率は業務ごとに計算します。三業務の率を単純平均して全業務へ適用しないでください。週10時間の業務が20%短縮される効果と、週1時間の業務が80%短縮される効果は、会社全体では同じ重みではありません。各業務の削減時間を計算してから合計するのが安全です。

    測定開始直後にマイナスが出ても失敗と決めつける必要はありません。初期学習による一時的な増加なのか、業務との相性が悪いのかを分けます。ただし、将来速くなるはずだと都合よく書き換えず、現時点の実測はマイナスのまま残し、改善後に再測定します。この正直さが、次の投資判断への信頼をつくります。

    6.5ファイルに役割を分け、AIと一緒につくる

    【画像06】

    今回のシミュレータは、public/index.html、public/style.css、public/app.js、worker.js、wrangler.tomlの5ファイルで構成します。小規模な道具では、役割が見える構成にすると、AIへ修正を依頼するときも説明しやすくなります。

    public/index.htmlは画面の骨組みです。人数など六つの数値を入力する欄、計算ボタン、結果表示、注意書きを置きます。短縮率なら「%」、週時間なら「時間」、費用なら「円」と単位を示します。結果だけでなく、入力した前提も同じ画面で確認できる設計にします。

    public/style.cssは見た目を整えます。経営者がスマートフォンやパソコンから触る可能性を考え、狭い画面でも入力欄がつぶれないようにします。色だけで黒字・赤字を区別せず、文言でも状態を伝えます。回収不能を赤くするだけでは、色の見え方が異なる人に伝わらないためです。

    public/app.jsは入力値の読み取り、計算、検証、結果表示を担当します。短縮率の50%を0.5へ変換し、空欄や不正な値を検知します。短縮率のマイナスを許す一方、人数のマイナスは受け付けない、といった項目別のルールが必要です。計算はブラウザ内で行い、入力データをサーバーへ送る処理は持たせません。

    worker.jsはWorkers側の入口です。稼働確認用のhealth応答に加え、それ以外の要求では静的アセット配信へ渡すfallbackを担います。入力内容の保存や共有データの同期は行いません。誰かの試算を別の人が同時に見られるわけではなく、ページを閉じれば入力は残りません。保存が必要になったら、データ分類、認証、保管期間、削除方法を決めて別機能として検討します。

    wrangler.tomlは、ローカル開発やWorkersへの配信に必要な設定をまとめるファイルです。設定項目や推奨構成はサービス側の更新で変わる可能性があるため、実際に作成する時点の公式情報と、利用しているWranglerのバージョンに合わせて確認します。この記事では、特定の細かな設定を将来まで不変なものとして扱いません。

    AIへ依頼するときは、5ファイルを一度に「いい感じにつくって」と頼むより、要件を明示します。入力項目、数式、エラー条件、保存しないこと、サーバー送信しないこと、稼働確認と静的配信に処理を絞ることを箇条書きにします。生成後は、ファイルごとの役割と計算箇所を人が読み、要件と一致するか確認します。AIが書いたコードは完成品ではなく、検証対象です。

    7.ローカルで動かし、手計算との一致を確認する

    5ファイルがそろったら、いきなり公開せず、npx wrangler devでローカル起動します。表示されたローカルのURLへアクセスし、画面が開くこと、入力できること、結果が表示されることを確認します。コマンドや設定は利用時点の公式案内に沿ってください。

    最初の検証には、先ほどの仮例を使えます。人数5、一人あたり週3時間、短縮率40%、時給換算額3,000円、AI月額費用20,000円、初期費用100,000円を入力します。期待値は、月間削減時間25.8時間、時間換算額77,400円、月間純効果57,400円、参考回収月数約1.74か月です。画面の値が手計算と一致するかを確認します。

    正常な例だけでは足りません。短縮率0%なら削減時間は0、短縮率がマイナスなら削減時間と換算額もマイナスになります。AI月額費用が時間換算額と同額なら回収不能です。人数0、空欄、小数、非常に大きな値、全角数字、文字列なども試し、意図しない結果や画面崩れがないか見ます。

    端数処理も決めます。内部計算の途中で丸めると、最終結果が手計算とずれることがあります。計算は十分な精度を保ち、表示時に時間は小数第一位、金額は整数、回収月数は小数第二位など、読みやすい桁へ整える方法があります。どの段階で丸めるかを統一し、画面に概算であることを示します。

    ブラウザの開発者向け画面を使える人は、入力時に外部通信が発生していないことも確かめます。今回の本体計算はブラウザ内で完結するため、計算ボタンを押すたびに入力内容が送信される必要はありません。一方で、静的ファイルの取得やhealth確認など、ページ表示に必要な通信までゼロになるわけではありません。「サーバー送信なし」が何を指すのかを正確に説明します。

    検証結果は短い表で残すと、修正後の再確認が容易です。入力値、期待値、実際の表示、合否、確認日だけでも十分です。式を変更した場合は、正常系と回収不能の境界をもう一度試します。小さな道具でも、数字を経営判断に使うなら、再現できる確認手順を持つことが欠かせません。

    8.共有は簡単でも、公開範囲は慎重に決める

    【画像07】

    ローカル検証が終わったら、利用するサービスへログインし、デプロイしてURLを共有します。実際のログイン方法やデプロイ手順は環境やサービスの更新によって変わるため、利用時点の公式案内を確認します。大切なのは、URLが発行できたことと、安全に社内共有できることを別々に考えることです。

    入力を保存しない設計でも、無制限に一般公開してよいとは限りません。画面には自社の時給換算基準や投資額を入力する可能性があります。肩越しに見られる、画面を共有する、ブラウザ拡張機能が動くなど、サーバー保存以外の経路もあります。また、将来の修正でログ送信や分析機能が追加されれば、当初の説明と実態が変わることもあります。

    社内利用では、Cloudflare Accessなどのアクセス制限を使い、許可した利用者だけが開ける状態を検討します。会社の認証基盤、対象者、退職・異動時の権限削除、管理者を決めます。単にURLを知っている人だけが使える状態は、認証ではありません。URLが転送されても、許可のない人は開けない構成が望まれます。

    このシミュレータには共有データの同期機能がありません。経営者が数字を変更しても、担当者の画面へ反映されません。結果を残す必要があるなら、入力条件と計算結果を会議資料へ転記する、所定の記録欄へ保存するなど、当面の運用を決めます。自動保存を急いで追加すると、個人情報や機密情報の保管責任が生まれます。必要性が確認できてから設計するほうが安全です。

    公開後には、管理者と更新日を画面か運用台帳へ明記します。計算式、費用、時給換算基準が変わったときに誰が直すのか。修正内容を誰が確認するのか。利用を終了するとき誰が停止するのか。小さな社内ツールほど、作った人の異動後に放置されがちです。共有の完了はデプロイ時ではなく、管理方法まで決まった時点です。

    9.数字を誤らせる五つの落とし穴と、感度分析

    【画像08】

    第一の落とし穴は、短縮率を感覚で置くことです。50%という切りのよい数字には根拠があるように見えますが、実測していなければ予想です。予想を使う場合は「見込み」と表示し、実測値と同じ欄に混ぜません。運用開始後に実測へ置き換える日を決めます。

    第二は、全部の業務を一度に対象にすることです。範囲が広いほど、測定条件がばらばらになり、二重計上も起きます。最初は三業務に絞り、それぞれの時間、品質、頻度を確認します。小さな結果から式と運用を直し、それから対象を増やします。

    第三は、費用を忘れることです。AIの月額利用料だけでなく、初期設定、研修、テンプレート作成、利用ルール整備、継続的な教育や確認の工数があります。一度だけの費用と毎月続く費用を分けないと、回収月数が歪みます。無料試用期間の結果を、その後の有料運用へそのまま当てはめるのも避けます。

    第四は、試算を更新しないことです。導入前の見込み値が、半年後も経営会議に載り続けることがあります。導入前見込み、試行期間の実測、本運用後の実測を分け、測定日を残します。AIの性能だけでなく、社員の習熟、業務手順、料金、対象人数も変わります。数字は一度つくる資料ではなく、意思決定の節目で更新する前提です。

    第五は、アクセス制限なしで公開することです。入力データを保存しないから安全、と単純には言えません。社内限定の目的なら、社内限定の入口を用意します。画面に「機密情報を入力しない」と書くだけでなく、認証と権限管理で利用範囲を制御します。

    さらに、単一の結果だけで判断せず、感度分析を行います。感度分析とは、重要な前提を変えたときに結果がどれだけ動くかを見ることです。たとえば短縮率を20%、40%、60%の三段階に変えます。対象人数も5人、10人、30人、AI月額費用も現在値と増額後で比べます。すると、どの前提が回収月数を大きく左右するかが見えます。

    仮例では短縮率40%で参考回収月数約1.74か月でした。しかし、同じ条件で短縮率が20%なら月間削減時間は12.9時間、時間換算額は38,700円です。月額費用20,000円を引くと月間純効果は18,700円で、回収には約5.35か月かかります。40%から20%へ半減しただけで、回収期間は約3倍です。これは、固定の月額費用があるためです。

    反対に、短縮率が高くても、その時間を再配分できなければ事業効果は限定されます。感度分析では計算値だけでなく、「削減時間の何割を残業削減へつなげられるか」「増えた時間で何件多く対応できるか」といった実行条件も検討します。数字の幅を示すことは、弱気な説明ではありません。不確実性を見える形にして、追加測定の優先順位を決める方法です。

    10.三つのステップで始め、数字を次の行動へ変える

    【画像09】

    ここまでを三つのステップにまとめます。

    ステップ1は、最初の三業務を選ぶことです。頻度が高く、範囲が明確で、品質を比較しやすい業務を選びます。一週間測り、AIへの指示、確認、修正、承認まで含めた導入前後の時間を記録します。短縮がマイナスでも、そのまま残します。見込みと実測を区別し、測定条件を揃えます。

    ステップ2は、共通の式で試算することです。月間削減時間を「人数×一人週時間×短縮率×4.3」、時間換算額を「削減時間×時給換算額」、参考回収月数を「初期費用÷(換算額−AI月額費用)」で計算します。教育費は初期と継続に分け、月間純効果が0以下なら回収不能とします。換算価値を現金削減や利益と呼ばず、空いた時間の使い道まで決めます。

    ステップ3は、5ファイルのシミュレータをローカルで検証し、アクセス制限を付けて共有することです。手計算と一致するか、境界値で壊れないか、入力がサーバーへ送られないかを確認します。公開後は管理者、更新日、見直し時期を決めます。保存や同期が必要になったときは、要件とデータ管理を別途設計します。

    この道具の価値は、現場の「速くなった」を、経営者が検討できる共通言語へ変えることにあります。数字が小さければ対象業務や使い方を変える。大きければ再現条件を確かめて横展開する。どちらの結果でも、次の行動が具体的になります。

    全社のROIを完璧に出そうとすると、データ収集だけで止まります。まずは三業務、一週間の測定から始めてください。測ってみると、想像より短縮できる作業も、確認負担が増える作業も見つかります。その差こそが、自社に合うAI活用を選ぶ材料です。

    もし、どの業務を測ればよいか、費用をどこまで含めるか、経営者へどう説明すればよいかで迷うなら、「AI導入前診断」を一つの入口にできます。オンライン60分で、対象業務の切り分け、測定項目、試算の前提を整理し、最初の三業務を決める場です。ツール選定の前に効果の測り方を決めておくと、導入後の評価がぶれにくくなります。

    【画像10】

    「効いた気がする」を否定する必要はありません。その感覚は、改善の芽です。ただし、次の投資を決めるときは、感覚を測定可能な問いへ変えます。小さく測り、同じ式で比べ、前提を更新する。その習慣が、AIを一時的な話題ではなく、会社の業務改善として育てていきます。

    すぐ使えるコード10点 付録

    01〜05は「5ファイル効果シミュレータ」を構成する一組です。HTML、CSS、JavaScript、Worker、Wrangler設定を同じ構成で使います。CSSや設定ファイルを単独実行する意味ではありません。06〜10は、それぞれ単独で実行できるPython標準ライブラリ製CLIです。プロンプトは10点に含みません。

    01 code/public/index.html

    • 言語: HTML

    • 用途: 入力フォームと結果領域を提供する画面。

    • 前提: モダンブラウザ。app.js と style.css を同じ public 配下に置く。

    • 変更箇所: 初期値や見出し、説明文を自社の前提に合わせて変更する。

    • 実行方法: Web本体5ファイルを同じ構成で置き、ローカルHTTPサーバーまたはWranglerで開く。

    • 期待値: 初期表示で25.8時間、77,400円、57,400円、1.74か月と表示される。

    <!doctype html>
    <html lang="ja">
    <head>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <meta name="description" content="AI導入による月間の時間効果と換算価値を試算します。">
      <title>5ファイル効果シミュレータ</title>
      <link rel="stylesheet" href="./style.css">
      <script src="./app.js" defer></script>
    </head>
    <body>
      <main class="shell">
        <header>
          <p class="eyebrow">AI導入の効果を数字で概算</p>
          <h1>5ファイル効果シミュレータ</h1>
          <p class="lead">入力値はブラウザ内だけで計算され、保存・送信されません。</p>
        </header>
    
        <form id="effect-form" novalidate>
          <section class="panel inputs" aria-labelledby="input-heading">
            <h2 id="input-heading">入力</h2>
            <div class="field-grid">
              <label>対象人数<input id="people" name="people" type="number" min="0" step="1" value="5" required inputmode="numeric"><span>人</span></label>
              <label>1人あたり週時間<input id="weeklyHours" name="weeklyHours" type="number" min="0" step="0.1" value="3" required inputmode="decimal"><span>時間</span></label>
              <label>短縮率<input id="reductionRate" name="reductionRate" type="number" step="0.1" value="40" required inputmode="decimal"><span>%</span></label>
              <label>時給<input id="hourlyRate" name="hourlyRate" type="number" min="0" step="1" value="3000" required inputmode="numeric"><span>円</span></label>
              <label>AI月額費<input id="monthlyAiCost" name="monthlyAiCost" type="number" min="0" step="1" value="20000" required inputmode="numeric"><span>円</span></label>
              <label>初期費用<input id="initialCost" name="initialCost" type="number" min="0" step="1" value="100000" required inputmode="numeric"><span>円</span></label>
            </div>
            <p id="form-error" class="error" role="alert" aria-live="assertive"></p>
            <button type="submit">計算する</button>
          </section>
    
          <section class="panel" aria-labelledby="result-heading" aria-live="polite">
            <h2 id="result-heading">月間の試算</h2>
            <dl class="results">
              <div><dt>削減時間</dt><dd id="savedHours">25.8時間</dd></div>
              <div><dt>換算価値</dt><dd id="timeValue">77,400円</dd></div>
              <div><dt>AI月額費を差し引いた換算価値</dt><dd id="netValue">57,400円</dd></div>
              <div><dt>参考回収期間</dt><dd id="paybackMonths">1.74か月</dd></div>
            </dl>
            <p class="note">換算価値は、短縮できた時間を時給で換算した参考値です。現金支出の削減額ではありません。</p>
          </section>
        </form>
      </main>
    </body>
    </html>

    02 code/public/style.css

    • 言語: CSS

    • 用途: 画面をモバイル対応・キーボード操作対応の見た目にする。

    • 前提: 01のHTMLが同じクラス名を使う。

    • 変更箇所: :root の色、ブレークポイント、余白を変更する。

    • 実行方法: 単独実行はしない。01〜05のWeb本体の一部としてブラウザが読み込む。

    • 期待値: 720px以下で1列になり、フォーカス枠とエラー色が視認できる。

    :root {
      color-scheme: light;
      --ink: #132238;
      --muted: #516071;
      --paper: #ffffff;
      --wash: #eef4f8;
      --accent: #075985;
      --accent-dark: #0c4a6e;
      --danger: #b42318;
      --line: #c9d7e2;
      font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
    }
    
    * { box-sizing: border-box; }
    body { margin: 0; color: var(--ink); background: linear-gradient(145deg, #e9f5fb, var(--wash)); }
    .shell { width: min(100% - 2rem, 62rem); margin-inline: auto; padding: 3rem 0 4rem; }
    header { max-width: 44rem; margin-bottom: 1.5rem; }
    h1 { margin: .25rem 0 .75rem; font-size: clamp(2rem, 5vw, 3.6rem); line-height: 1.08; }
    h2 { margin-top: 0; }
    .eyebrow { margin: 0; color: var(--accent); font-weight: 800; letter-spacing: .08em; text-transform: uppercase; }
    .lead, .note { color: var(--muted); line-height: 1.7; }
    form { display: grid; grid-template-columns: 1.1fr .9fr; gap: 1rem; align-items: start; }
    .panel { background: var(--paper); border: 1px solid var(--line); border-radius: 1.2rem; padding: clamp(1.2rem, 3vw, 2rem); box-shadow: 0 1rem 2.5rem rgb(19 34 56 / 8%); }
    .field-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; }
    label { display: grid; grid-template-columns: 1fr auto; gap: .35rem .5rem; font-weight: 700; }
    label input { grid-column: 1; width: 100%; margin-top: .35rem; border: 1px solid #8094a5; border-radius: .65rem; padding: .8rem; font: inherit; }
    label span { grid-column: 2; align-self: end; padding-bottom: .8rem; color: var(--muted); }
    input:focus-visible, button:focus-visible { outline: 3px solid #38bdf8; outline-offset: 2px; }
    input[aria-invalid="true"] { border-color: var(--danger); }
    button { width: 100%; margin-top: 1rem; border: 0; border-radius: .75rem; padding: .9rem 1rem; background: var(--accent); color: white; font: inherit; font-weight: 800; cursor: pointer; }
    button:hover { background: var(--accent-dark); }
    .error { min-height: 1.5em; margin: .8rem 0 0; color: var(--danger); font-weight: 700; }
    .results { margin: 0; }
    .results div { display: flex; justify-content: space-between; gap: 1rem; padding: 1rem 0; border-bottom: 1px solid var(--line); }
    .results dt { color: var(--muted); }
    .results dd { margin: 0; font-size: 1.15rem; font-weight: 850; text-align: right; }
    
    @media (max-width: 720px) {
      .shell { width: min(100% - 1rem, 40rem); padding-top: 1.5rem; }
      form, .field-grid { grid-template-columns: 1fr; }
      .panel { border-radius: .9rem; }
    }
    
    @media (prefers-reduced-motion: reduce) { * { scroll-behavior: auto !important; } }

    03 code/public/app.js

    • 言語: JavaScript

    • 用途: 入力検証、効果計算、表示更新をブラウザ内だけで行う。

    • 前提: 保存・通信はしない。短縮率は負値を許可し100超を拒否する。

    • 変更箇所: FIELD_LABELSや表示桁数を必要に応じて変更する。

    • 実行方法: 単独実行はしない。01〜05のWeb本体の一部としてブラウザで使う。Nodeからrequireして関数検証もできる。

    • 期待値: 既知例を表示し、空欄・NaN・無限値・オーバーフローはエラーとなり結果欄を—へ戻す。

    (function (root) {
      "use strict";
    
      const FIELD_LABELS = {
        people: "対象人数", weeklyHours: "1人あたり週時間", reductionRate: "短縮率",
        hourlyRate: "時給", monthlyAiCost: "AI月額費", initialCost: "初期費用"
      };
      const NON_NEGATIVE = new Set(["people", "weeklyHours", "hourlyRate", "monthlyAiCost", "initialCost"]);
    
      function parseInputs(raw) {
        const values = {};
        for (const key of Object.keys(FIELD_LABELS)) {
          const text = String(raw[key] ?? "").trim();
          if (text === "") throw new Error(`${FIELD_LABELS[key]}を入力してください。`);
          const value = Number(text);
          if (!Number.isFinite(value)) throw new Error(`${FIELD_LABELS[key]}は有限の数値で入力してください。`);
          if (NON_NEGATIVE.has(key) && value < 0) throw new Error(`${FIELD_LABELS[key]}は0以上で入力してください。`);
          if (key === "reductionRate" && value > 100) throw new Error("短縮率は100以下で入力してください。");
          values[key] = value;
        }
        return values;
      }
    
      function calculateEffect(input) {
        const monthlyHours = input.people * input.weeklyHours * (input.reductionRate / 100) * 4.3;
        const timeValue = monthlyHours * input.hourlyRate;
        const netValue = timeValue - input.monthlyAiCost;
        const result = {
          monthlyHours,
          timeValue,
          netValue,
          paybackMonths: netValue > 0 ? input.initialCost / netValue : null
        };
        if (![result.monthlyHours, result.timeValue, result.netValue].every(Number.isFinite) ||
            (result.paybackMonths !== null && !Number.isFinite(result.paybackMonths))) {
          throw new Error("計算結果が扱える範囲を超えました。入力値を小さくしてください。");
        }
        return result;
      }
    
      function formatNumber(value, digits) {
        return new Intl.NumberFormat("ja-JP", { maximumFractionDigits: digits }).format(value);
      }
    
      function mount(doc) {
        const form = doc.getElementById("effect-form");
        if (!form) return false;
        const error = doc.getElementById("form-error");
    
        function render(event) {
          if (event) event.preventDefault();
          for (const input of form.querySelectorAll("input")) input.removeAttribute("aria-invalid");
          try {
            const raw = Object.fromEntries(Object.keys(FIELD_LABELS).map((key) => [key, doc.getElementById(key).value]));
            const result = calculateEffect(parseInputs(raw));
            doc.getElementById("savedHours").textContent = `${formatNumber(result.monthlyHours, 2)}時間`;
            doc.getElementById("timeValue").textContent = `${formatNumber(result.timeValue, 0)}円`;
            doc.getElementById("netValue").textContent = `${formatNumber(result.netValue, 0)}円`;
            doc.getElementById("paybackMonths").textContent = result.paybackMonths === null ? "回収見込みなし" : `${formatNumber(result.paybackMonths, 2)}か月`;
            error.textContent = "";
          } catch (problem) {
            for (const id of ["savedHours", "timeValue", "netValue", "paybackMonths"]) doc.getElementById(id).textContent = "—";
            error.textContent = problem.message;
            const key = Object.keys(FIELD_LABELS).find((name) => problem.message.startsWith(FIELD_LABELS[name]));
            if (key) doc.getElementById(key).setAttribute("aria-invalid", "true");
          }
        }
    
        form.addEventListener("submit", render);
        render();
        return true;
      }
    
      const api = { parseInputs, calculateEffect, formatNumber, mount };
      if (typeof module !== "undefined" && module.exports) module.exports = api;
      if (root) root.EffectSimulator = api;
      if (typeof document !== "undefined") {
        if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", () => mount(document));
        else mount(document);
      }
    })(typeof globalThis !== "undefined" ? globalThis : this);

    04 code/worker.js

    • 言語: JavaScript

    • 用途: ヘルスチェックJSONを返し、それ以外を静的アセットへ委譲するWorker。

    • 前提: Cloudflare WorkersのASSETS binding。

    • 変更箇所: サービス名や追加APIルートを変更する。

    • 実行方法: 単独の画面ではない。01〜05のWeb本体としてWranglerから起動する。

    • 期待値: /api/health がJSONを返し、他の要求は env.ASSETS.fetch(request) へ流れる。

    export default {
      async fetch(request, env) {
        const url = new URL(request.url);
        if (url.pathname === "/api/health") {
          return Response.json({ ok: true, service: "effect-simulator" }, {
            headers: { "cache-control": "no-store" }
          });
        }
        if (env && env.ASSETS && typeof env.ASSETS.fetch === "function") {
          return env.ASSETS.fetch(request);
        }
        return Response.json({ ok: false, error: "assets_binding_unavailable" }, { status: 503 });
      }
    };

    05 code/wrangler.toml

    • 言語: TOML

    • 用途: Worker、静的ファイル、API優先ルートを結ぶ設定。

    • 前提: Wrangler 4.20.0以上。

    • 変更箇所: name と compatibility_date を環境に合わせる。assets.directory とbindingは構成を変えない限り維持する。

    • 実行方法: 単独実行はしない。01〜05のWeb本体の設定として npx wrangler dev で読む。

    • 期待値: ./publicがASSETSに結び付き、/api/*だけWorkerが先に実行される。

    name = "five-file-effect-simulator"
    main = "worker.js"
    compatibility_date = "2026-09-18"
    
    [assets]
    directory = "./public"
    binding = "ASSETS"
    run_worker_first = ["/api/*"]

    06 code/verify_fixture.py

    • 言語: Python

    • 用途: 既知例を自動検算する独立CLI。

    • 前提: Python 3標準ライブラリのみ。

    • 変更箇所: calculateへの入力とexpectedを自社ケースへ変更する。

    • 実行方法: python verify_fixture.py

    • 期待値: 4項目がOKとなり、回収期間は約1.7421602787か月になる。

    #!/usr/bin/env python3
    """既知の入力値を式へ代入し、期待値との差を検算する。"""
    import argparse
    import math
    
    
    def calculate(people, weekly_hours, reduction_rate, hourly_rate, monthly_ai_cost, initial_cost):
        values = (people, weekly_hours, reduction_rate, hourly_rate, monthly_ai_cost, initial_cost)
        if not all(math.isfinite(value) for value in values):
            raise ValueError("入力は有限の数値にしてください")
        monthly_hours = people * weekly_hours * reduction_rate / 100 * 4.3
        time_value = monthly_hours * hourly_rate
        net_value = time_value - monthly_ai_cost
        payback = initial_cost / net_value if net_value > 0 else None
        return monthly_hours, time_value, net_value, payback
    
    
    def main():
        parser = argparse.ArgumentParser()
        parser.add_argument("--tolerance", type=float, default=1e-9)
        args = parser.parse_args()
        actual = calculate(5, 3, 40, 3000, 20000, 100000)
        expected = (25.8, 77400.0, 57400.0, 100000 / 57400)
        for label, got, want in zip(("時間", "換算価値", "差引換算", "回収月"), actual, expected):
            if not math.isclose(got, want, rel_tol=args.tolerance, abs_tol=args.tolerance):
                raise SystemExit(f"NG {label}: got={got}, expected={want}")
            print(f"OK {label}: {got}")
    
    
    if __name__ == "__main__":
        main()

    07 code/aggregate_csv.py

    • 言語: Python

    • 用途: 条件を揃えた導入前と導入後の実測CSVを月別集計する独立CLI。

    • 前提: date,baseline_minutes,actual_minutes列を持つUTF-8 CSV。baselineは同条件で測った導入前値で、予定値を実測値として扱わない。

    • 変更箇所: 必須列や集計軸を運用に合わせて変更する。

    • 実行方法: python aggregate_csv.py actual.csv

    • 期待値: 月別の時間と削減率を出力する。baseline合計0なら削減率は空欄(算出不可)になる。

    #!/usr/bin/env python3
    """条件を揃えた導入前 baseline_minutes と導入後 actual_minutes を月別集計する。"""
    import argparse
    import csv
    import math
    from collections import defaultdict
    from datetime import date
    
    
    def number(row, key, line):
        text = (row.get(key) or "").strip()
        try:
            value = float(text)
        except ValueError as exc:
            raise ValueError(f"{line}行目の{key}が数値ではありません") from exc
        if not math.isfinite(value) or value < 0:
            raise ValueError(f"{line}行目の{key}は0以上の有限値にしてください")
        return value
    
    
    def aggregate(path):
        totals = defaultdict(lambda: [0, 0.0, 0.0])
        with open(path, newline="", encoding="utf-8-sig") as handle:
            reader = csv.DictReader(handle)
            required = {"date", "baseline_minutes", "actual_minutes"}
            if not required.issubset(reader.fieldnames or []):
                raise ValueError("CSV列は date,baseline_minutes,actual_minutes が必須です")
            for line, row in enumerate(reader, 2):
                day = date.fromisoformat((row.get("date") or "").strip())
                planned = number(row, "baseline_minutes", line)
                actual = number(row, "actual_minutes", line)
                bucket = totals[day.strftime("%Y-%m")]
                bucket[0] += 1
                bucket[1] += planned
                bucket[2] += actual
        return totals
    
    
    def main():
        parser = argparse.ArgumentParser()
        parser.add_argument("csv_file")
        args = parser.parse_args()
        print("month,count,baseline_hours,actual_hours,saved_hours,saved_rate_percent")
        for month, (count, planned, actual) in sorted(aggregate(args.csv_file).items()):
            saved = planned - actual
            rate = f"{saved / planned * 100:.2f}" if planned else ""
            print(f"{month},{count},{planned/60:.2f},{actual/60:.2f},{saved/60:.2f},{rate}")
    
    
    if __name__ == "__main__":
        main()

    08 code/sensitivity.py

    • 言語: Python

    • 用途: 複数条件の感度分析をCSV出力する独立CLI。

    • 前提: Python 3標準ライブラリのみ。短縮率は負値を許可し100超を拒否する。

    • 変更箇所: 既定の人数、週時間、短縮率、単価、AI月額費を変更する。

    • 実行方法: python sensitivity.py --people 3,5,10 --weekly-hours 1,3,5 --rates 20,40,60

    • 期待値: 全組合せについて月間時間、換算価値、差引換算を出力する。

    #!/usr/bin/env python3
    """人数・週時間・短縮率の組合せ別に月間効果をCSV出力する。"""
    import argparse
    import csv
    import itertools
    import math
    import sys
    
    
    def csv_numbers(text):
        values = [float(part.strip()) for part in text.split(",") if part.strip()]
        if not values or not all(math.isfinite(value) for value in values):
            raise argparse.ArgumentTypeError("カンマ区切りの有限数値を指定してください")
        return values
    
    
    def main():
        parser = argparse.ArgumentParser()
        parser.add_argument("--people", type=csv_numbers, default=[3, 5, 10])
        parser.add_argument("--weekly-hours", type=csv_numbers, default=[1, 3, 5])
        parser.add_argument("--rates", type=csv_numbers, default=[20, 40, 60])
        parser.add_argument("--hourly-rate", type=float, default=3000)
        parser.add_argument("--monthly-ai-cost", type=float, default=20000)
        args = parser.parse_args()
        if not math.isfinite(args.hourly_rate) or not math.isfinite(args.monthly_ai_cost):
            parser.error("単価と月額費は有限数値にしてください")
        if any(value < 0 for value in args.people + args.weekly_hours):
            parser.error("人数と週時間は0以上にしてください")
        if any(value > 100 for value in args.rates):
            parser.error("短縮率は100以下にしてください(負値は許容)")
        if args.hourly_rate < 0 or args.monthly_ai_cost < 0:
            parser.error("単価と月額費は0以上にしてください")
        writer = csv.writer(sys.stdout, lineterminator="\n")
        writer.writerow(["people", "weekly_hours", "reduction_rate", "monthly_hours", "time_value", "net_value"])
        for people, hours, rate in itertools.product(args.people, args.weekly_hours, args.rates):
            monthly = people * hours * rate / 100 * 4.3
            value = monthly * args.hourly_rate
            writer.writerow([people, hours, rate, round(monthly, 2), round(value), round(value - args.monthly_ai_cost)])
    
    
    if __name__ == "__main__":
        main()

    09 code/weekly_template.py

    • 言語: Python

    • 用途: 7日分の導入前・導入後実測記録テンプレートを生成する独立CLI。

    • 前提: Python 3標準ライブラリのみ。baseline_minutesには条件を揃えて実測した導入前値を記録する。

    • 変更箇所: 列名や日数を運用に合わせて変更する。

    • 実行方法: python weekly_template.py --start 2026-09-14 --output weekly-log.csv

    • 期待値: 指定日から7行のUTF-8 BOM付きCSVを生成する。

    #!/usr/bin/env python3
    """指定週の実測記録CSVテンプレートを生成する。"""
    import argparse
    import csv
    from datetime import date, timedelta
    from pathlib import Path
    
    
    def main():
        parser = argparse.ArgumentParser()
        parser.add_argument("--start", default=date.today().isoformat(), help="週の開始日 YYYY-MM-DD")
        parser.add_argument("--output", default="weekly-log.csv")
        args = parser.parse_args()
        start = date.fromisoformat(args.start)
        output = Path(args.output)
        with output.open("w", newline="", encoding="utf-8-sig") as handle:
            writer = csv.writer(handle)
            writer.writerow(["date", "task", "baseline_minutes", "actual_minutes", "note"])
            for offset in range(7):
                writer.writerow([(start + timedelta(days=offset)).isoformat(), "", "", "", ""])
        print(output.resolve())
    
    
    if __name__ == "__main__":
        main()

    10 code/monthly_report.py

    • 言語: Python

    • 用途: 実測CSVからMarkdown月報を生成する独立CLI。

    • 前提: 07と同じ必須列を持つCSV。baselineは導入前実測値。

    • 変更箇所: 月報項目や時給を変更する。

    • 実行方法: python monthly_report.py actual.csv --hourly-rate 3000 --output monthly-report.md

    • 期待値: 月別の件数、削減時間、換算価値を含む月報を生成する。

    #!/usr/bin/env python3
    """実測CSVから人が読める月報Markdownを生成する。"""
    import argparse
    import csv
    import math
    from collections import defaultdict
    from datetime import date
    from pathlib import Path
    
    
    def read_rows(path):
        months = defaultdict(lambda: {"count": 0, "planned": 0.0, "actual": 0.0})
        with open(path, newline="", encoding="utf-8-sig") as handle:
            reader = csv.DictReader(handle)
            required = {"date", "baseline_minutes", "actual_minutes"}
            if not required.issubset(reader.fieldnames or []):
                raise ValueError("必須列: date,baseline_minutes,actual_minutes")
            for line, row in enumerate(reader, 2):
                month = date.fromisoformat((row["date"] or "").strip()).strftime("%Y-%m")
                try:
                    planned, actual = float(row["baseline_minutes"]), float(row["actual_minutes"])
                except ValueError as exc:
                    raise ValueError(f"{line}行目の時間が数値ではありません") from exc
                if not all(math.isfinite(v) and v >= 0 for v in (planned, actual)):
                    raise ValueError(f"{line}行目の時間は0以上の有限値にしてください")
                months[month]["count"] += 1
                months[month]["planned"] += planned
                months[month]["actual"] += actual
        return months
    
    
    def main():
        parser = argparse.ArgumentParser()
        parser.add_argument("csv_file")
        parser.add_argument("--hourly-rate", type=float, default=3000)
        parser.add_argument("--output", default="monthly-report.md")
        args = parser.parse_args()
        if not math.isfinite(args.hourly_rate) or args.hourly_rate < 0:
            parser.error("時給は0以上の有限値にしてください")
        lines = ["# AI活用 実測月報", "", "換算価値は現金削減額ではなく、時間を時給換算した参考値です。", ""]
        for month, data in sorted(read_rows(args.csv_file).items()):
            saved_hours = (data["planned"] - data["actual"]) / 60
            lines += [f"## {month}", "", f"- 記録件数: {data['count']}件", f"- 削減時間: {saved_hours:.2f}時間", f"- 換算価値: {saved_hours * args.hourly_rate:,.0f}円", ""]
        output = Path(args.output)
        output.write_text("\n".join(lines), encoding="utf-8")
        print(output.resolve())
    
    
    if __name__ == "__main__":
        main()

    Cloudflare設定の根拠

    Cloudflare公式ドキュメントでは、assets.directoryで静的ファイルのフォルダを指定し、assets.bindingでWorkerから利用するbinding名を指定できます。また、assets.run_worker_firstは文字列配列を受け取り、/api/*のようなパターンに一致する要求だけWorkerを先に実行できます。Worker側はenv.ASSETS.fetch(request)で静的アセットへ委譲できます。

    本付録のコードはローカル検証用です。外部公開、deploy、Cloudflareへのログインは行っていません。

    出典・参考資料

    本記事は、ミニ教材『「効いた気がする」を卒業する。効果は、5ファイルで見える。』全10ページをもとに、測定方法と実装例を加筆したものです。図解10点は同PDFから抽出しています。コードと数値例は本記事用の実装・説明例です。

    Cloudflareの設定・操作については、2026年9月18日に以下の公式資料を確認しました。

    あわせて読みたい関連記事

    現場で効果を記録するところから始めたい方へ。

    一度きりの試算を、継続的な測定につなげたい方へ。

    出てきた数字を、次の経営判断につなげたい方へ。


    鈴木章裕
    株式会社コミクス 代表取締役

     
     
     
    AI活用のご相談→ https://www.comix.co.jp/contact/ 株式会社コミクス代表取締役。生成AI活用支援実績304社(2026年9月末現在)。営業・資料作成の効率化、AIエージェント導入を支援。何から始めるか迷っている段階でもご相談ください。

    あなたへのおすすめ