簡単な収益予測シミュレーション
ボトムアップ売上シミュレーション入門
はじめに
「来年の売上はどうなるだろう?」「営業担当者を2名増やしたらどのくらい売上が上がるだろう?」
こうした疑問に、勘や経験だけでなく、数値で答える方法があります。それが売上シミュレーションです。
売上予測をしたかったため、o3に手伝ってもらいながら、簡単なコードを実装しました。
この記事では、シンプルな予測モデルである「ボトムアップモデル」について、解説します。
売上シミュレーションとは
基本的な考え方
売上シミュレーションは、「もしも〜だったら」を数値で検証する手法です。
例えば:
- 「もしもエンジニアを3名増やしたら?」
- 「もしも稼働率が80%から85%に向上したら?」
- 「もしも単価を10%上げることができたら?」
こうした仮定のもとで、売上がどう変化するかを計算で予測します。
なぜ重要なのか
- 計画的な経営: 感覚ではなく数値に基づいた判断ができる
- リスク回避: 実際に投資する前に効果を見積もれる
- 説得力のある提案: 上司や投資家に根拠を示せる
ボトムアップモデルの基本
概要
ボトムアップモデルは、「人月×稼働率×平均単価」を基本とする最もシンプルなモデルです。
基本の計算式
月次売上 = 人数 × 月間稼働時間 × 稼働率 × 時間単価
適用できるビジネス
- IT・システム開発会社
- コンサルティング会社
- デザイン・クリエイティブ会社
- 士業事務所(税理士、弁護士など)
要するに、人の時間を売るビジネスに最適です。
具体例で理解する
IT企業の例
現在の状況:
- エンジニア数:10名
- 月間稼働時間:160時間(1日8時間×20日)
- 平均稼働率:75%(残り25%は営業活動や待機時間)
- 平均時間単価:8,000円
計算:
月次売上 = 10名 × 160時間 × 0.75 × 8,000円
= 9,600,000円(960万円)
年間売上予測:
年間売上 = 960万円 × 12ヶ月 = 1億1,520万円
「もしも」のシナリオ
シナリオ1: エンジニアを3名増やした場合
月次売上 = 13名 × 160時間 × 0.75 × 8,000円
= 12,480,000円(1,248万円)
増加分 = 1,248万円 - 960万円 = 288万円/月
シナリオ2: 稼働率を80%に向上させた場合
月次売上 = 10名 × 160時間 × 0.80 × 8,000円
= 10,240,000円(1,024万円)
増加分 = 1,024万円 - 960万円 = 64万円/月
シナリオ3: 時間単価を10%アップした場合
月次売上 = 10名 × 160時間 × 0.75 × 8,800円
= 10,560,000円(1,056万円)
増加分 = 1,056万円 - 960万円 = 96万円/月
パラメータの意味
1. 人数
- 意味: 実際に案件に携わるスタッフ数
- 注意点: 管理職や営業専任は含めない
2. 月間稼働時間
- 一般的な値: 140〜180時間
- 計算方法: 営業日数 × 1日の労働時間
3. 稼働率
- 意味: 全労働時間のうち、実際に売上に貢献する時間の割合
- 含まれないもの: 営業活動、社内会議、研修、待機時間
- 一般的な値: 60〜80%
4. 時間単価
- 意味: 顧客に請求する1時間あたりの料金
- 設定方法: 過去の案件実績から平均を算出
メリット・デメリット
メリット
- 理解しやすい: 誰でも直感的に理解できる
- 計算が簡単: Excelでも十分実装可能
- 即座に効果測定: 採用や単価変更の効果がすぐ分かる
デメリット
- 営業力を考慮しない: 案件獲得能力の変化を反映できない
- 市場変動に弱い: 経済環境の変化に対応困難
- 顧客継続性を無視: リピート案件の効果を見落としがち
シンプルなモデルなため、概算程度の予測になります。
一方、稼働率などを調整し、売上予測を見ることで、ストック技術の開発やR&Dにどの程度時間を割くことができるか、といったことを見積もることができます。
Python実装の解説
Pythonでシミュレーションする場合の解説をします。
ボトムアップモデルのサンプルコードは以下です。
サンプルコード
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
ボトムアップ売上シミュレーション
このファイルは「人月 × 稼働率 × 平均単価」を月次で積み上げるボトムアップモデルに特化しています。
プロフェッショナルサービス組織の売上予測に最適で、採用計画と直結した予測が可能です。
主な機能:
- ボトムアップ売上シミュレーション
- 年度表示対応(1年目4月から開始)
- 詳細な可視化(売上、ヘッドカウント、累積売上、採用計画)
- CSV出力機能
- 高解像度PNG保存
"""
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import platform
from datetime import datetime
import os
class BottomUpSimulation:
"""
ボトムアップ売上シミュレーション専用クラス
「人月 × 稼働率 × 平均単価」を月次で積み上げる手法で、
プロフェッショナルサービス組織の売上予測を行います。
"""
def __init__(self):
"""初期化とフォント設定"""
self.results = {}
self._setup_japanese_font()
print("✅ ボトムアップシミュレーター初期化完了")
def _setup_japanese_font(self):
"""日本語フォントの設定"""
try:
import matplotlib.font_manager as fm
# 利用可能なフォント一覧を取得
available_fonts = [f.name for f in fm.fontManager.ttflist]
system = platform.system()
if system == "Windows":
japanese_fonts = ['Yu Gothic', 'Meiryo', 'MS Gothic', 'MS PGothic']
elif system == "Darwin": # macOS
japanese_fonts = ['Hiragino Sans', 'Hiragino Kaku Gothic Pro', 'Arial Unicode MS']
else: # Linux
japanese_fonts = ['Noto Sans CJK JP', 'IPAexGothic', 'IPAPGothic', 'VL PGothic']
# 利用可能な日本語フォントを検索
found_font = None
for font in japanese_fonts:
if font in available_fonts:
found_font = font
break
if found_font:
plt.rcParams['font.family'] = found_font
print(f"✅ 日本語フォント設定: {found_font}")
else:
plt.rcParams['font.family'] = 'DejaVu Sans'
print("⚠️ 日本語フォントが見つかりません。英語表示になります。")
plt.rcParams['axes.unicode_minus'] = False
except Exception as e:
print(f"⚠️ フォント設定エラー: {e}")
plt.rcParams['font.family'] = 'DejaVu Sans'
plt.rcParams['axes.unicode_minus'] = False
def _generate_year_month_labels(self, total_months, start_month=4):
"""
年度表示ラベルを生成する関数
Parameters:
-----------
total_months : int
総月数
start_month : int
開始月(デフォルト4月)
Returns:
--------
tuple : (月番号リスト, 年度ラベルリスト, 年度境界位置)
"""
months = []
labels = []
year_boundaries = []
current_month = start_month
current_year = 1
for i in range(total_months):
months.append(i + 1)
# 年度境界の記録(4月の位置)
if current_month == start_month:
year_boundaries.append(i + 1)
year_label = f"{current_year}年目"
else:
year_label = ""
# 月表示
month_label = f"{current_month}月"
# 年度と月を組み合わせ
if year_label:
full_label = f"{year_label}\n{month_label}"
else:
full_label = month_label
labels.append(full_label)
# 次の月に進む
current_month += 1
if current_month > 12:
current_month = 1
# 4月になったら次年度
if current_month == start_month and i > 0:
current_year += 1
return months, labels, year_boundaries
def _apply_year_labels(self, ax, data_length, year_labels, year_boundaries):
"""
グラフに年度表示を適用する関数
Parameters:
-----------
ax : matplotlib.axes
対象のaxesオブジェクト
data_length : int
データの長さ
year_labels : list
年度ラベルのリスト
year_boundaries : list
年度境界の位置リスト
"""
if not year_labels or data_length == 0:
return
# 適切な間隔でラベルを表示(データが長い場合は間引く)
if data_length <= 12:
# 12ヶ月以下の場合は全て表示
tick_positions = list(range(1, data_length + 1))
tick_labels = year_labels[:data_length]
elif data_length <= 36:
# 36ヶ月以下の場合は3ヶ月おきに表示
tick_positions = list(range(1, data_length + 1, 3))
tick_labels = [year_labels[i-1] for i in tick_positions if i-1 < len(year_labels)]
else:
# それ以上の場合は年度境界のみ表示
tick_positions = [pos for pos in year_boundaries if pos <= data_length]
tick_labels = [year_labels[pos-1] for pos in tick_positions if pos-1 < len(year_labels)]
ax.set_xticks(tick_positions)
ax.set_xticklabels(tick_labels, rotation=45, ha='right')
# 年度境界に縦線を追加
for boundary in year_boundaries:
if boundary <= data_length and boundary > 1:
ax.axvline(x=boundary, color='gray', linestyle='--', alpha=0.5, linewidth=1)
def run_simulation(self,
initial_headcount=10,
monthly_hires=[2, 1, 2, 1, 3, 2, 1, 2, 2, 1, 2, 1],
utilization_rate=0.75,
hourly_rate=8000,
working_hours_per_month=160):
"""
ボトムアップ売上シミュレーション実行
このモデルは「人月 × 稼働率 × 平均単価」を月次で積み上げる手法です。
プロフェッショナルサービス組織に最適で、採用計画と直結した予測が可能です。
Parameters:
-----------
initial_headcount : int
初期ヘッドカウント(開始時点の従業員数)
monthly_hires : list
月次採用予定数のリスト
utilization_rate : float
稼働率(0.0-1.0)。billable時間の割合
hourly_rate : float
時間単価(円)
working_hours_per_month : int
月間労働時間(時間)
Returns:
--------
dict : シミュレーション結果
"""
print("=" * 80)
print("🔧 ボトムアップ売上シミュレーション実行中...")
print("=" * 80)
# 期間を採用計画の長さに自動調整
months = len(monthly_hires)
print(f"📊 シミュレーション設定:")
print(f" 初期ヘッドカウント: {initial_headcount}人")
print(f" 採用計画: {monthly_hires}")
print(f" 稼働率: {utilization_rate*100:.1f}%")
print(f" 時間単価: ¥{hourly_rate:,}/時間")
print(f" 月間稼働時間: {working_hours_per_month}時間")
print(f" シミュレーション期間: {months}ヶ月")
# 結果格納用のリスト初期化
results = {
'month': [], # 月
'headcount': [], # ヘッドカウント
'hires': [], # 月次採用数
'billable_hours': [], # 請求可能時間
'revenue': [], # 売上
'cumulative_revenue': [] # 累積売上
}
# 現在のヘッドカウントを初期値に設定
current_headcount = initial_headcount
cumulative_revenue = 0
print(f"\n📈 月次シミュレーション結果:")
print(f"{'月':>3} {'採用':>4} {'HC':>4} {'請求時間':>8} {'月次売上':>12} {'累積売上':>12}")
print("-" * 60)
# 月次シミュレーション実行
for month in range(1, months + 1):
# ステップ1: 新規採用の追加
# 前月末に採用した人員を当月から稼働開始と仮定
if month > 1:
current_headcount += monthly_hires[month - 2]
# 当月の採用数を記録
current_hires = monthly_hires[month - 1]
# ステップ2: 請求可能時間の計算
# 総労働時間 × 稼働率 = 請求可能時間
total_hours = current_headcount * working_hours_per_month
billable_hours = total_hours * utilization_rate
# ステップ3: 月次売上の計算
# 請求可能時間 × 時間単価 = 月次売上
monthly_revenue = billable_hours * hourly_rate
cumulative_revenue += monthly_revenue
# ステップ4: 結果の記録
results['month'].append(month)
results['headcount'].append(current_headcount)
results['hires'].append(current_hires)
results['billable_hours'].append(billable_hours)
results['revenue'].append(monthly_revenue)
results['cumulative_revenue'].append(cumulative_revenue)
# 進捗表示
print(f"{month:3d} {current_hires:4d} {current_headcount:4d} "
f"{billable_hours:8.0f} {monthly_revenue:12,.0f} {cumulative_revenue:12,.0f}")
# 結果をDataFrameに変換
df_results = pd.DataFrame(results)
# 統計情報の計算
stats_info = {
'total_revenue': cumulative_revenue,
'average_monthly_revenue': df_results['revenue'].mean(),
'max_monthly_revenue': df_results['revenue'].max(),
'min_monthly_revenue': df_results['revenue'].min(),
'final_headcount': current_headcount,
'average_headcount': df_results['headcount'].mean(),
'total_hires': sum(monthly_hires),
'average_utilization': utilization_rate,
'effective_hourly_rate': hourly_rate
}
print("\n" + "=" * 80)
print("📊 ボトムアップシミュレーション結果サマリー")
print("=" * 80)
print(f"総売上: {stats_info['total_revenue']:15,.0f} 円")
print(f"月平均売上: {stats_info['average_monthly_revenue']:15,.0f} 円")
print(f"最大月次売上: {stats_info['max_monthly_revenue']:15,.0f} 円")
print(f"最小月次売上: {stats_info['min_monthly_revenue']:15,.0f} 円")
print(f"最終ヘッドカウント: {stats_info['final_headcount']:15.0f} 人")
print(f"平均ヘッドカウント: {stats_info['average_headcount']:15.1f} 人")
print(f"総採用人数: {stats_info['total_hires']:15.0f} 人")
print(f"稼働率: {stats_info['average_utilization']*100:15.1f} %")
print(f"時間単価: {stats_info['effective_hourly_rate']:15,.0f} 円")
# 結果を保存
self.results = {
'data': df_results,
'stats': stats_info,
'parameters': {
'initial_headcount': initial_headcount,
'monthly_hires': monthly_hires,
'utilization_rate': utilization_rate,
'hourly_rate': hourly_rate,
'working_hours_per_month': working_hours_per_month,
'months': months
}
}
return self.results
def visualize_results(self, save_plots=True, output_dir="bottom_up_plots"):
"""
シミュレーション結果の可視化
ボトムアップモデルの結果を4つのグラフで詳細表示します。
Parameters:
-----------
save_plots : bool
グラフをPNGファイルとして保存するかどうか
output_dir : str
保存先ディレクトリ名
"""
if not self.results:
print("⚠️ まずシミュレーションを実行してください。")
return
print("\n" + "=" * 80)
print("📈 結果可視化中...")
print("=" * 80)
# 保存ディレクトリの作成
if save_plots:
if not os.path.exists(output_dir):
os.makedirs(output_dir)
print(f"📁 出力ディレクトリを作成: {output_dir}")
# データ取得
data = self.results['data']
stats = self.results['stats']
# 年度表示ラベルの生成
months_list, year_labels, year_boundaries = self._generate_year_month_labels(len(data))
# 図のサイズとスタイル設定
plt.style.use('default')
fig = plt.figure(figsize=(16, 12))
# 日本語フォントの再設定
try:
system = platform.system()
if system == "Windows":
plt.rcParams['font.family'] = ['Yu Gothic', 'Meiryo', 'MS Gothic']
elif system == "Darwin": # macOS
plt.rcParams['font.family'] = ['Hiragino Sans', 'Hiragino Kaku Gothic Pro']
else: # Linux
plt.rcParams['font.family'] = ['Noto Sans CJK JP', 'IPAexGothic', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
except:
plt.rcParams['font.family'] = 'DejaVu Sans'
plt.rcParams['axes.unicode_minus'] = False
# 1. 月次売上推移
ax1 = plt.subplot(2, 2, 1)
plt.plot(data['month'], data['revenue'] / 1000000,
marker='o', linewidth=3, markersize=8, color='#2E86AB',
markerfacecolor='white', markeredgewidth=2)
plt.title('月次売上推移', fontsize=16, fontweight='bold', pad=20)
plt.xlabel('期間', fontsize=12)
plt.ylabel('売上 (百万円)', fontsize=12)
plt.grid(True, alpha=0.3)
# 年度表示を適用
self._apply_year_labels(ax1, len(data), year_labels, year_boundaries)
# 平均線を追加
plt.axhline(y=stats['average_monthly_revenue']/1000000,
color='red', linestyle='--', alpha=0.7,
label=f'平均: {stats["average_monthly_revenue"]/1000000:.1f}M円')
plt.legend()
# 2. ヘッドカウントと採用計画
ax2 = plt.subplot(2, 2, 2)
# ヘッドカウント(左軸)
line1 = ax2.plot(data['month'], data['headcount'],
marker='s', linewidth=3, markersize=8, color='#A23B72',
markerfacecolor='white', markeredgewidth=2, label='ヘッドカウント')
ax2.set_ylabel('ヘッドカウント (人)', color='#A23B72', fontsize=12)
# 採用数(右軸)
ax2_twin = ax2.twinx()
bars = ax2_twin.bar(data['month'], data['hires'],
alpha=0.6, color='#F18F01', label='月次採用数')
ax2_twin.set_ylabel('採用数 (人)', color='#F18F01', fontsize=12)
plt.title('ヘッドカウント推移と採用計画', fontsize=16, fontweight='bold', pad=20)
ax2.set_xlabel('期間', fontsize=12)
ax2.grid(True, alpha=0.3)
# 年度表示を適用
self._apply_year_labels(ax2, len(data), year_labels, year_boundaries)
# 凡例の統合
lines = line1 + [bars]
labels = [l.get_label() for l in line1] + ['月次採用数']
ax2.legend(lines, labels, loc='upper left')
# 3. 累積売上
ax3 = plt.subplot(2, 2, 3)
plt.plot(data['month'], data['cumulative_revenue'] / 1000000,
marker='D', linewidth=3, markersize=8, color='#F18F01',
markerfacecolor='white', markeredgewidth=2)
plt.title('累積売上推移', fontsize=16, fontweight='bold', pad=20)
plt.xlabel('期間', fontsize=12)
plt.ylabel('累積売上 (百万円)', fontsize=12)
plt.grid(True, alpha=0.3)
# 年度表示を適用
self._apply_year_labels(ax3, len(data), year_labels, year_boundaries)
# 最終値を表示
final_revenue = stats['total_revenue'] / 1000000
plt.text(0.02, 0.98, f'最終累積売上:\n{final_revenue:.1f}百万円',
transform=ax3.transAxes, fontsize=12, fontweight='bold',
verticalalignment='top', bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.8))
# 4. 請求可能時間と効率性分析
ax4 = plt.subplot(2, 2, 4)
# 請求可能時間(左軸)
line2 = ax4.plot(data['month'], data['billable_hours'],
marker='^', linewidth=3, markersize=8, color='#C73E1D',
markerfacecolor='white', markeredgewidth=2, label='請求可能時間')
ax4.set_ylabel('請求可能時間 (時間)', color='#C73E1D', fontsize=12)
# 一人当たり売上(右軸)
ax4_twin = ax4.twinx()
per_person_revenue = data['revenue'] / data['headcount'] / 1000
line3 = ax4_twin.plot(data['month'], per_person_revenue,
marker='o', linewidth=3, markersize=8, color='#2E8B57',
markerfacecolor='white', markeredgewidth=2, label='一人当たり売上')
ax4_twin.set_ylabel('一人当たり売上 (千円)', color='#2E8B57', fontsize=12)
plt.title('請求時間と生産性分析', fontsize=16, fontweight='bold', pad=20)
ax4.set_xlabel('期間', fontsize=12)
ax4.grid(True, alpha=0.3)
# 年度表示を適用
self._apply_year_labels(ax4, len(data), year_labels, year_boundaries)
# 凡例の統合
lines = line2 + line3
labels = [l.get_label() for l in lines]
ax4.legend(lines, labels, loc='upper left')
plt.tight_layout(pad=3.0)
# グラフの保存
if save_plots:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
plot_filename = f"{output_dir}/bottom_up_simulation_{timestamp}.png"
plt.savefig(plot_filename, dpi=300, bbox_inches='tight')
print(f"📊 グラフを保存: {plot_filename}")
plt.show()
print("✅ 可視化完了")
def export_results(self, filename_prefix="bottom_up_results"):
"""
結果をCSVファイルに出力
Parameters:
-----------
filename_prefix : str
出力ファイル名のプレフィックス
"""
if not self.results:
print("⚠️ 出力する結果がありません。")
return
print(f"\n📁 結果をCSVファイルに出力中...")
# タイムスタンプ付きファイル名
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
csv_filename = f"{filename_prefix}_{timestamp}.csv"
# データ出力
self.results['data'].to_csv(csv_filename, index=False, encoding='utf-8-sig')
print(f"✅ シミュレーション結果: {csv_filename}")
# 統計情報も別途保存
stats_filename = f"{filename_prefix}_stats_{timestamp}.txt"
with open(stats_filename, 'w', encoding='utf-8') as f:
f.write("ボトムアップ売上シミュレーション結果サマリー\n")
f.write("=" * 50 + "\n")
f.write(f"実行日時: {datetime.now().strftime('%Y年%m月%d日 %H:%M:%S')}\n\n")
f.write("パラメーター設定:\n")
params = self.results['parameters']
f.write(f"- 初期ヘッドカウント: {params['initial_headcount']}人\n")
f.write(f"- 採用計画: {params['monthly_hires']}\n")
f.write(f"- 稼働率: {params['utilization_rate']*100:.1f}%\n")
f.write(f"- 時間単価: {params['hourly_rate']:,}円\n")
f.write(f"- 月間稼働時間: {params['working_hours_per_month']}時間\n")
f.write(f"- シミュレーション期間: {params['months']}ヶ月\n\n")
f.write("結果サマリー:\n")
stats = self.results['stats']
f.write(f"- 総売上: {stats['total_revenue']:,.0f}円\n")
f.write(f"- 月平均売上: {stats['average_monthly_revenue']:,.0f}円\n")
f.write(f"- 最大月次売上: {stats['max_monthly_revenue']:,.0f}円\n")
f.write(f"- 最小月次売上: {stats['min_monthly_revenue']:,.0f}円\n")
f.write(f"- 最終ヘッドカウント: {stats['final_headcount']:.0f}人\n")
f.write(f"- 平均ヘッドカウント: {stats['average_headcount']:.1f}人\n")
f.write(f"- 総採用人数: {stats['total_hires']:.0f}人\n")
print(f"✅ 統計サマリー: {stats_filename}")
print("📋 出力完了")
def main():
"""
メイン実行関数
ここで設定値を変更してシミュレーションを実行できます。
"""
print("🚀 ボトムアップ売上シミュレーション開始")
print("=" * 80)
# シミュレーターインスタンス作成
simulator = BottomUpSimulation()
# シミュレーション設定
# ※ここの値を変更してください
config = {
# 初期ヘッドカウント(開始時点の従業員数)
'initial_headcount': 20,
# 月次採用予定数(リストの長さがシミュレーション期間になります)
# 例: 3年間の採用計画(各年4月に5人採用)
'monthly_hires': [
5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # 1年目
5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # 2年目
5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # 3年目
],
# 稼働率(0.0-1.0)
# 0.75 = 75%の時間がbillable(請求可能)
# 残りの25%は営業活動やR&D、自己啓発などを想定
'utilization_rate': 0.75,
# 時間単価(円)
'hourly_rate': 10_000,
# 月間稼働時間(時間)
'working_hours_per_month': 160,
}
# シミュレーション実行
results = simulator.run_simulation(**config)
# 結果の可視化
simulator.visualize_results(save_plots=True, output_dir="bottom_up_plots")
# 結果のCSV出力
simulator.export_results()
print("\n🎉 ボトムアップシミュレーション完了!")
print("📊 グラフとCSVファイルが出力されました。")
if __name__ == "__main__":
main()
必要な準備
1. 環境構築
pip install numpy pandas matplotlib seaborn
2. 採用計画の設定
# 3年間の採用計画例
monthly_hires = [
5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # 1年目:4月に5人採用
5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # 2年目:4月に5人採用
5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # 3年目:4月に5人採用
]
3. 年度表示機能
- 1年目4月から開始
- 年度境界に縦線表示
- 長期間でも見やすい表示
4. 出力される4つのグラフ
- 月次売上推移: 売上の変化と平均線
- ヘッドカウント推移: 人員増加と採用タイミング
- 累積売上: 総売上の成長パターン
- 生産性分析: 請求時間と一人当たり売上
設定値の決め方
1. 現実的なパラメータ設定が重要
# 悪い例:根拠のない楽観的な設定
config = {
'utilization_rate': 0.95, # 95%稼働は非現実的
'hourly_rate': 150_000, # 相場より高すぎる単価
}
# 良い例:過去実績に基づく現実的な設定
config = {
'utilization_rate': 0.72, # 過去12ヶ月の平均値
'hourly_rate': 8_500, # 業界相場を参考
}
保守的な見積もりを心がける
# 実績が80%でも、予測では75%に設定
'utilization_rate': 0.75,
# 最高単価12,000円でも、平均的な8,000円で設定
'hourly_rate': 8000,
実際のビジネスでの活用
導入のステップ
Step 1: データの準備
必要なデータを整理します:
- 過去12ヶ月の売上データ
- 月別の人員数
- 案件別の時間単価
- 実際の稼働時間記録
Step 2: 現在の数値を把握
# 実績から平均値を算出
actual_utilization = 総請求時間 / (人数 × 月間稼働時間)
average_hourly_rate = 総売上 / 総請求時間
Step 3: 小さく始める
# 最初は3ヶ月間の短期予測から開始
results = simulator.run_simulation(
monthly_hires=[1, 0, 1], # 3ヶ月分のみ
# その他の設定...
)
Step 4: 実績と比較して改善
毎月、予測と実績を比較してパラメータを調整します。
活用事例
事例1: 採用計画の立案
課題: 来年度の売上目標2億円を達成するために何名採用すべきか?
解決手順:
- 現在の設定でシミュレーション実行
- 目標に足りない分を計算
- 必要な追加人員を逆算
事例2: 目標達成に必要な単価の算出
課題: 売上目標を達成するために必要な時間単価を決めたい
解決: 現状シミュレーションと目標売上の差分から「必要単価」を逆算する
事例3: 投資判断
課題: 新しいオフィスを借りて人員を増強すべきか?
解決: 増員による売上増加と固定費増加を比較
よくある間違いと対策
間違い1: 過度に楽観的な設定
# 悪い例
utilization_rate = 0.90 # 現実的でない高稼働率
monthly_hires = [5, 5, 5, 5, 5, 5] # 採用しすぎ
# 良い例
utilization_rate = 0.70 # 保守的な設定
monthly_hires = [2, 0, 1, 0, 2, 0] # 現実的な採用ペース
間違い2: 一度作って終わり
# 悪い例:作りっぱなし
simulator.run_simulation()
# この後、実績との比較を行わない
# 良い例:継続的な改善
def monthly_review(predicted, actual):
error_rate = abs(predicted - actual) / actual
if error_rate > 0.15: # 15%以上の誤差
print("パラメータの見直しが必要です")
間違い3: 詳細すぎる設定
# 悪い例:複雑すぎる設定
monthly_hires = [2.5, 1.3, 0.7, ...] # 小数点は非現実的
# 良い例:シンプルな設定
monthly_hires = [2, 1, 1, 0, 2, 0] # 整数で十分
成功のコツ
1. 現実的な数値設定
- 過去実績を基にする
- 業界平均を参考にする
- 保守的に見積もる
2. 定期的な見直し
- 月次で実績と比較
- 四半期でパラメータ調整
- 年次でモデル全体を見直し
3. 目的の明確化
- 何のための予測か?
- どの程度の精度が必要か?
- 誰に説明するのか?
まとめ
ボトムアップモデルの特徴
適用場面:
- 人的サービスが主力のビジネス
- 採用計画を立てたい場合
- シンプルで分かりやすい予測が欲しい場合
メリット:
- 誰でも理解できる
- 計算が簡単
- すぐに効果測定できる
注意点:
- 営業力の変化は考慮されない
- 市場環境の変動に弱い
- 保守的な設定が重要
次のステップ
-
まずは試してみる
- 過去3ヶ月のデータで検証
- 実績との差異を確認
-
継続的に改善する
- 月次で見直し
- パラメータを調整
-
組織で共有する
- 経営陣への報告資料として活用
- 採用計画の根拠として使用
-
人材成長要素の組み込み
現在のモデルは全員が同じスキルレベルという前提ですが、実際のビジネスでは以下の要素が重要です:
実装予定の機能:
- スキルレベル別単価設定: 例えば、新人(6,000円/時間)→ 中堅(8,000円/時間)→ 熟練者(12,000円/時間)といったように、スキルレベルに応じて単価を設定する。
- 育成期間の考慮: 採用後1年間は育成期間として低稼働率(50%)・低単価を適用
- OJT影響の反映: 新人指導により指導者の稼働率が一時的に10-15%低下
- 成長曲線の実装: 経験年数に応じた単価とスキルの段階的上昇
- スキル分布の管理: チーム内の新人・中堅・熟練者の比率バランス
期待される効果:
# 従来モデル(全員同一条件) 月次売上 = 人数 × 稼働率(75%) × 単価(8,000円) × 時間(160h) # 改良モデル(スキルレベル考慮) 月次売上 = Σ(各スキルレベル人数 × 個別稼働率 × 個別単価 × 時間) - OJT稼働率低下分
現実的な改善例:
- より正確な採用コスト算出(育成期間中の低収益を考慮)
- 適切なスキル構成の計画(新人ばかりでは売上が上がらない)
- 長期的な人材投資効果の可視化(育成投資→将来の高単価)
この機能により、「採用すれば即戦力」という非現実的な前提から、「人材育成を含めた真の売上予測」へと進化します。
その他のシミュレーションモデル
ボトムアップモデル以外にもモデルがあります。
1. コホートベースモデル
概要: 顧客を獲得月でグループ化し、継続率を追跡するモデル
適用場面:
- SaaSビジネス
- サブスクリプションサービス
- 継続課金型サービス
計算例:
各月の売上 = Σ(コホート別顧客数 × 継続率 × 月額料金)
メリット: 顧客の離脱パターンが分かり、LTV(顧客生涯価値)を算出できる
2. モンテカルロシミュレーション
概要: パラメータを確率分布として扱い、何千回もシミュレーションを実行
適用場面:
- 不確実性の高い新規事業
- リスク分析が必要な投資判断
- 経営会議での報告資料
特徴:
- 「90%の確率で売上は○○円以上」といった予測が可能
- 最悪・標準・最良シナリオを同時に検証
メリット: 不確実性を数値化でき、説得力のある資料を作成できる
3. システム・ダイナミクス(フィードバック型)モデル
概要: 「評判→案件獲得→採用→実績→評判」といった相互作用を数式化
適用場面:
- 長期戦略の検討(2-3年先)
- 複雑な事業モデルの分析
- 政策変更の影響分析
フィードバック例:
高い評判 → 案件増加 → 採用増加 → キャパシティ向上 → 品質向上 → 評判向上
メリット: 実際のビジネスに近い複雑な相互関係を表現できる
どのモデルを選ぶべきか?
ビジネスタイプ | 推奨モデル |
---|---|
IT・コンサル等の人的サービス | ボトムアップ |
SaaS・サブスク | コホート |
新規事業・不確実性が高い・感度分析をしたい | モンテカルロ |
成熟企業・複雑な事業 | システム・ダイナミクス |
Discussion