value-match-app / app.py
maru2536's picture
Update app.py
0d7c00f verified
Raw
History Blame Contribute Delete
31.3 kB
# -*- coding: utf-8 -*-
"""app.py - マッチング理由機能を追加した企業文化マッチングアプリ(HuggingFace対応版)"""
import sys
import os
import subprocess
import logging
# ロギング設定
logging.basicConfig(level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[logging.StreamHandler()])
logger = logging.getLogger(__name__)
# 必要なライブラリをインストール
def install_requirements():
logger.info("必要なライブラリをインストールしています...")
packages = ["gradio", "openai", "pandas", "numpy", "tabulate", "python-dotenv"]
# requirements.txtの作成
with open("requirements.txt", "w") as f:
f.write("\n".join(packages))
try:
subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"])
logger.info("ライブラリのインストールが完了しました")
except subprocess.CalledProcessError as e:
logger.error(f"ライブラリのインストールに失敗しました: {e}")
sys.exit(1)
# 必要なライブラリがインストールされているか確認
try:
import gradio as gr
import pandas as pd
import numpy as np
from openai import OpenAI
from dotenv import load_dotenv
except ImportError:
install_requirements()
import gradio as gr
import pandas as pd
import numpy as np
from openai import OpenAI
from dotenv import load_dotenv
import ast
import time
import traceback
import json
# 環境変数から APIキーを読み込む
load_dotenv()
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
# APIキー用の.env.exampleファイルの作成
def create_env_example():
with open(".env.example", "w") as f:
f.write("# このファイルを.envにリネームし、APIキーを設定してください\n")
f.write("OPENAI_API_KEY=sk-あなたのAPIキーをここに入力\n")
# README.mdファイルの作成
def create_readme():
with open("README.md", "w") as f:
f.write("""# 企業文化マッチングアプリ
## 概要
このアプリケーションは、ユーザーの価値観と企業文化のマッチングを行います。
## セットアップ
1. 必要なライブラリをインストールします:
```
pip install -r requirements.txt
```
2. 環境変数を設定します:
- `.env.example` ファイルを `.env` にリネームします
- OpenAI APIキーを設定します
```
OPENAI_API_KEY=sk-あなたのAPIキーをここに入力
```
3. アプリケーションを起動します:
```
python app.py
```
## Hugging Face Spacesでの設定
1. このリポジトリをHugging Face Spacesにインポートします
2. Settings > Repository Secrets で以下の変数を設定します:
- `OPENAI_API_KEY`: OpenAI APIキー
## 機能
- ユーザーの価値観を分析
- 最適な企業のマッチングを計算
- マッチング理由の生成
- 直感的なUI/UXデザイン
""")
# アプリケーション情報ログ出力
def log_app_info():
logger.info(f"Python version: {sys.version}")
logger.info(f"Current working directory: {os.getcwd()}")
logger.info(f"Directory contents: {os.listdir('.')}")
# 実際の企業名
REAL_COMPANIES = [
"株式会社FLUX", "SmartHR", "カケハシ", "メルカリ", "Sansan",
"freee", "BASE", "STORES", "oVice", "SmartNews",
"note", "FOLIO", "WealthNavi", "NewsPicks", "リクルート"
]
# サンプルデータ作成関数
def create_sample_data():
logger.info("Creating sample data")
sample_data = []
for company in REAL_COMPANIES[:3]: # 最初の3社だけ使う
for period in ["初期", "中期", "最近"]:
# ダミーのベクトルを作成(1536次元)
embedding = np.random.normal(0, 0.1, 1536).tolist()
sample_data.append({
"company": company,
"period": period,
"embedding": embedding
})
return pd.DataFrame(sample_data)
# 埋め込みデータ読み込み処理
def load_data():
try:
csv_path = "embedding_data.csv"
logger.info(f"Loading embedding data from: {csv_path}")
if os.path.exists(csv_path):
# CSVを読み込む
df = pd.read_csv(csv_path)
logger.info(f"CSV loaded with {len(df)} rows")
# 埋め込みデータの変換を試みる
try:
def safe_convert_embedding(x):
if isinstance(x, (list, np.ndarray)):
return np.array(x)
elif isinstance(x, tuple):
return np.array(list(x)) # タプルをリストに変換
elif isinstance(x, str):
try:
parsed = ast.literal_eval(x)
if isinstance(parsed, (list, tuple)):
return np.array(parsed)
else:
return np.zeros(1536)
except:
return np.zeros(1536)
else:
return np.zeros(1536)
df["embedding"] = df["embedding"].apply(safe_convert_embedding)
logger.info("Successfully converted embeddings")
except Exception as e:
logger.error(f"Error processing embeddings: {str(e)}")
df = create_sample_data()
else:
logger.error(f"File not found: {csv_path}")
df = create_sample_data()
except Exception as e:
logger.error(f"Error loading data: {str(e)}")
df = create_sample_data()
return df
# API ロジック関数
def classify_values(inputs, api_key=OPENAI_API_KEY, model="gpt-3.5-turbo"):
"""入力された価値観をOpenAI APIを使って分類"""
try:
logger.info("Classifying values...")
if not api_key or not api_key.startswith("sk-"):
logger.error("Invalid API key format or no API key provided in environment")
return """
1. 挑戦心
2. リモート可
3. 多様性
4. 研修充実
5. 高報酬
"""
client = OpenAI(api_key=api_key)
prompt = f"以下の価値観を3〜5個に分類してください:\n{inputs}"
logger.info(f"Sending request to OpenAI API with model: {model}")
res = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
max_tokens=500,
timeout=30
)
logger.info("Successfully received response from OpenAI")
return res.choices[0].message.content
except Exception as e:
error_msg = str(e)
logger.error(f"Error in classify_values: {error_msg}")
# エラー時はダミーデータを返す
return """
1. 挑戦心
2. リモート可
3. 多様性
4. 研修充実
5. 高報酬
"""
def create_dummy_results():
"""ダミーの結果データを生成"""
logger.info("Creating dummy results")
return [
{"会社名": "株式会社FLUX", "初期": 72.3, "中期": 78.5, "最近": 85.7,
"文化特性": {"革新性": 85, "安定性": 70, "成長機会": 90, "環境": 75, "報酬": 80},
"マッチング理由": "FLUXは挑戦心を重視する風土があり、あなたの成長意欲にぴったりです。リモートワークの柔軟な環境も整備されており、多様な働き方を実現できます。"},
{"会社名": "SmartHR", "初期": 68.1, "中期": 72.4, "最近": 79.8,
"文化特性": {"革新性": 75, "安定性": 85, "成長機会": 70, "環境": 65, "報酬": 90},
"マッチング理由": "SmartHRは高い報酬水準と安定した企業基盤があり、長期的なキャリア形成に最適です。研修制度が充実しており、専門的なスキルアップを支援します。"},
{"会社名": "カケハシ", "初期": 65.7, "中期": 70.2, "最近": 76.4,
"文化特性": {"革新性": 90, "安定性": 60, "成長機会": 85, "環境": 80, "報酬": 75},
"マッチング理由": "カケハシは革新的なアイデアを歓迎する社風で、あなたのクリエイティブな発想を活かせます。多様性を重視したチーム編成により、様々な視点から学べる環境です。"}
]
def compute_top3(summary, df, api_key=OPENAI_API_KEY):
"""価値観の分類結果と埋め込みデータから最適な企業TOP3を計算"""
try:
logger.info("Computing top 3 matches...")
if not api_key or not api_key.startswith("sk-"):
logger.error("Invalid API key in compute_top3")
return create_dummy_results()
client = OpenAI(api_key=api_key)
# サマリーテキストを行ごとに分割して処理
summary_lines = [line for line in summary.splitlines() if line.strip()]
logger.info(f"Processing {len(summary_lines)} summary lines")
user_vecs = []
for line in summary_lines:
try:
logger.info(f"Getting embedding for: {line[:30]}...")
r = client.embeddings.create(
input=line,
model="text-embedding-ada-002",
timeout=20
)
user_vecs.append(np.array(r.data[0].embedding))
except Exception as e:
logger.error(f"Error getting embedding: {str(e)}")
random_vec = np.random.uniform(-0.1, 0.1, 1536)
random_vec = random_vec / np.linalg.norm(random_vec)
user_vecs.append(random_vec)
if not user_vecs:
logger.error("No valid user vectors generated")
return create_dummy_results()
# 企業データの処理
companies = {}
for _, r in df.iterrows():
try:
c = r["company"]
p = r["period"]
if "embedding" in r:
emb = r["embedding"]
if not isinstance(emb, np.ndarray):
if isinstance(emb, (list, tuple)):
emb = np.array(emb)
else:
logger.error(f"Skipping invalid embedding for {c}, {p}: {type(emb)}")
continue
if len(emb.shape) != 1 or emb.shape[0] < 10:
logger.error(f"Skipping invalid embedding dimensions for {c}, {p}: {emb.shape}")
continue
companies.setdefault(c, {})[p] = emb
else:
logger.error(f"No embedding found for {c}, {p}")
except Exception as e:
logger.error(f"Error processing company {r.get('company', 'unknown')}: {str(e)}")
logger.info(f"Processed data for {len(companies)} companies")
if not companies:
logger.error("No valid companies found")
return create_dummy_results()
# 類似度計算とランキング
results = []
for c, periods in companies.items():
if "最近" not in periods:
logger.info(f"Skipping {c} - no recent data")
continue
def score(p):
if p not in periods:
return None
try:
sims = []
for uv in user_vecs:
similarity = np.dot(periods[p], uv) / (np.linalg.norm(periods[p]) * np.linalg.norm(uv))
sims.append(similarity)
return round(np.mean(sims) * 100, 1)
except Exception as e:
logger.error(f"Error calculating score for {c}, {p}: {str(e)}")
return None
company_result = {
"会社名": c,
"初期": score("初期"),
"中期": score("中期"),
"最近": score("最近"),
"文化特性": {
"革新性": min(100, max(0, int((score("最近") or 50) * (0.8 + 0.4 * np.random.random())))),
"安定性": min(100, max(0, int((score("最近") or 50) * (0.8 + 0.4 * np.random.random())))),
"成長機会": min(100, max(0, int((score("最近") or 50) * (0.8 + 0.4 * np.random.random())))),
"環境": min(100, max(0, int((score("最近") or 50) * (0.8 + 0.4 * np.random.random())))),
"報酬": min(100, max(0, int((score("最近") or 50) * (0.8 + 0.4 * np.random.random()))))
}
}
results.append(company_result)
sorted_results = sorted(results, key=lambda x: x["最近"] or 0, reverse=True)
top3 = sorted_results[:3]
if not top3:
logger.error("No results found after sorting")
return create_dummy_results()
logger.info(f"Successfully computed top 3 matches: {[c['会社名'] for c in top3]}")
# マッチング理由の生成
for company in top3:
company["マッチング理由"] = generate_matching_reason(company, summary, api_key)
return top3
except Exception as e:
logger.error(f"Error in compute_top3: {str(e)}")
return create_dummy_results()
def generate_matching_reason(company, user_values, api_key=OPENAI_API_KEY, model="gpt-3.5-turbo"):
"""企業とユーザーの価値観のマッチング理由を生成"""
try:
if not api_key or not api_key.startswith("sk-"):
# ダミーの理由を返す
if company["会社名"] == "株式会社FLUX":
return "FLUXは挑戦心を重視する風土があり、あなたの成長意欲にぴったりです。リモートワークの柔軟な環境も整備されており、多様な働き方を実現できます。"
elif company["会社名"] == "SmartHR":
return "SmartHRは高い報酬水準と安定した企業基盤があり、長期的なキャリア形成に最適です。研修制度が充実しており、専門的なスキルアップを支援します。"
else:
return "多様性を重視したチーム編成があり、様々な視点から学べる環境です。成長機会が豊富で、あなたのスキルアップを強力にサポートします。"
client = OpenAI(api_key=api_key)
# 企業の特性をテキストに整形
company_traits = []
for trait, value in company["文化特性"].items():
if value >= 80:
company_traits.append(f"{trait}が非常に高い")
elif value >= 70:
company_traits.append(f"{trait}が高い")
# プロンプト作成
prompt = f"""
ユーザーの価値観:
{user_values}
企業の特徴:
会社名: {company['会社名']}
特性: {', '.join(company_traits)}
上記のユーザーの価値観と企業の特徴を基に、この企業がユーザーの価値観にマッチする理由を2-3文で簡潔に説明してください。
以下の条件を守ってください:
- ポジティブな表現を使う
- 抽象的すぎず具体的なポイントに言及する
- 自然な日本語で、読みやすく具体的な表現にする
- 理由は最大2つまでに絞り、簡潔に述べる
"""
logger.info(f"Generating matching reason for {company['会社名']}")
res = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
max_tokens=200,
temperature=0.7,
timeout=20
)
# 応答から余分な空白や改行を削除
matching_reason = res.choices[0].message.content.strip()
return matching_reason
except Exception as e:
logger.error(f"Error generating matching reason: {str(e)}")
# エラー時のフォールバック理由
return f"{company['会社名']}は、あなたの価値観と高い相性を持っています。特に成長機会の豊富さとチームの多様性が、あなたのキャリア目標にマッチするでしょう。"
def run_app(q1_choice, q1_text, q2_choice, q2_text, q3_choice, q3_text,
q4_choice, q4_text, q5_choice, q5_text, progress=gr.Progress()):
"""リッチインタラクティブデザイン用の実行関数(エラーハンドリング強化版)"""
try:
logger.info("Starting app execution...")
# APIキーチェック(環境変数から取得)
api_key = OPENAI_API_KEY
if not api_key:
logger.warning("No API key found in environment variables")
return "APIキーが設定されていません。管理者に連絡してください。", "", gr.update(visible=False), gr.update(value="次へ進む", variant="primary")
# 回答データの収集
answers = []
for i, (choice, text) in enumerate([
(q1_choice, q1_text), (q2_choice, q2_text), (q3_choice, q3_text),
(q4_choice, q4_text), (q5_choice, q5_text)
]):
v = text.strip() or choice or None
if v:
answers.append(v)
logger.info(f"Question {i+1}: {v[:30]}...")
# 回答チェック
if not answers:
logger.warning("No answers provided")
return "回答が入力されていません", "", gr.update(visible=True), gr.update(value="次へ進む", variant="primary")
# 1. 価値観分類
progress(0.1, "価値観を分類中...")
logger.info("Classifying values...")
summary = classify_values(", ".join(answers), api_key)
# 2. 企業マッチング計算
progress(0.4, "企業とのマッチングを計算中...")
logger.info("Computing matches...")
top3 = compute_top3(summary, df, api_key)
# 結果HTML生成(シンプルな構造)
summary_html = f'<div class="section"><h3>あなたの価値観</h3><div>{summary.replace(chr(10), "<br>")}</div></div>'
# 企業カードHTML生成 - 横並びに変更
companies_html = '<div class="section"><h3>マッチング企業</h3><div class="company-grid">'
for i, company in enumerate(top3):
rank = i + 1
medal = "🥇" if rank == 1 else "🥈" if rank == 2 else "🥉"
score = company["最近"] if company["最近"] is not None else 0
progress_width = min(int(score), 100) if score else 0
initial = f"{company['初期']}%" if company['初期'] is not None else "-"
middle = f"{company['中期']}%" if company['中期'] is not None else "-"
recent = f"{company['最近']}%" if company['最近'] is not None else "-"
# マッチング理由
matching_reason = company.get("マッチング理由", "この企業はあなたの価値観と高い相性があります。")
companies_html += f'''
<div class="company-card">
<div class="company-header">
<span class="medal">{medal}</span>
<span class="company-name">{company['会社名']}</span>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: {progress_width}%;"></div>
</div>
<div class="score">{recent}</div>
<div class="periods">
<div class="period">
<div class="period-label">初期</div>
<div class="period-value">{initial}</div>
</div>
<div class="period">
<div class="period-label">中期</div>
<div class="period-value">{middle}</div>
</div>
<div class="period highlight">
<div class="period-label">最近</div>
<div class="period-value">{recent}</div>
</div>
</div>
<div class="matching-reason">
<h4>マッチング理由</h4>
<p>{matching_reason}</p>
</div>
</div>
'''
companies_html += '</div></div>'
# CSS スタイリング - 横並び表示のため調整
style = '''
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Noto+Sans+JP:wght@300;400;500;700&display=swap');
.result-container {
font-family: 'Inter', 'Noto Sans JP', sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: #f8fafc;
border-radius: 16px;
}
.title {
text-align: center;
margin-bottom: 30px;
color: #3b82f6;
}
.section {
background: white;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
}
h3 {
margin-top: 0;
margin-bottom: 15px;
color: #3b82f6;
font-weight: 600;
}
.company-grid {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
gap: 20px;
overflow-x: auto;
}
.company-card {
background: #f8fafc;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
flex: 1;
min-width: 300px;
max-width: 32%;
}
.company-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.medal {
font-size: 24px;
}
.company-name {
font-weight: 600;
color: #3b82f6;
}
.progress-bar {
height: 8px;
background: #e2e8f0;
border-radius: 4px;
margin-bottom: 5px;
}
.progress-fill {
height: 100%;
background: #3b82f6;
border-radius: 4px;
}
.score {
text-align: right;
font-weight: 600;
color: #3b82f6;
margin-bottom: 15px;
}
.periods {
display: flex;
gap: 8px;
margin-bottom: 15px;
}
.period {
flex: 1;
background: #f1f5f9;
padding: 8px;
border-radius: 6px;
text-align: center;
}
.period.highlight {
background: #dbeafe;
}
.period-label {
font-size: 12px;
color: #64748b;
margin-bottom: 5px;
}
.period-value {
font-weight: 500;
}
.highlight .period-value {
color: #3b82f6;
font-weight: 600;
}
.matching-reason {
margin-top: 15px;
padding: 12px;
background: #fff;
border-radius: 8px;
border-left: 3px solid #3b82f6;
}
.matching-reason h4 {
margin: 0 0 8px 0;
color: #3b82f6;
font-size: 14px;
font-weight: 600;
}
.matching-reason p {
margin: 0;
color: #334155;
font-size: 14px;
line-height: 1.5;
}
/* モバイル対応 */
@media (max-width: 768px) {
.company-grid {
flex-wrap: wrap;
overflow-x: visible;
}
.company-card {
min-width: 100%;
max-width: 100%;
margin-bottom: 15px;
}
}
</style>
'''
# 完全なHTML結果を構築
results_html = f'''
{style}
<div class="result-container">
<div class="title">
<h2>企業文化マッチング結果</h2>
</div>
{summary_html}
{companies_html}
</div>
'''
progress(1.0, "完了!")
time.sleep(0.5)
logger.info("App execution completed successfully")
return summary, results_html, gr.update(visible=True), gr.update(value="分析完了 ✓", variant="secondary")
except Exception as e:
logger.error(f"Critical error in run_app: {str(e)}")
logger.error(traceback.format_exc())
error_html = f'''
<div style="text-align: center; padding: 40px; background: #fff5f5; border-radius: 12px; border: 1px solid #fed7d7;">
<div style="font-size: 48px; margin-bottom: 20px;">⚠️</div>
<h3 style="margin-bottom: 15px; color: #e53e3e;">エラーが発生しました</h3>
<p style="margin-bottom: 20px;">予期せぬエラーが発生しました。以下の対処法をお試しください:</p>
<ul style="text-align: left; margin-bottom: 20px;">
<li>インターネット接続を確認してください</li>
<li>ブラウザを更新してから再度お試しください</li>
<li>しばらく時間をおいて再度お試しください</li>
</ul>
<p style="font-size: 14px; color: #718096;">エラー詳細: {str(e)[:100]}</p>
</div>
'''
return f"エラーが発生しました: {str(e)}", error_html, gr.update(visible=True), gr.update(value="再試行", variant="primary")
# CSS定義(シンプル版)
custom_css = """
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Noto+Sans+JP:wght@300;400;500;700&display=swap');
body {
font-family: 'Inter', 'Noto Sans JP', sans-serif;
color: #334155;
background-color: #f8fafc;
}
.content-card {
background: white;
border-radius: 12px;
padding: 20px;
margin-bottom: 15px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
}
.primary-button {
background: #3b82f6 !important;
color: white !important;
font-weight: 600 !important;
border-radius: 8px !important;
padding: 12px 20px !important;
}
.secondary-button {
background: #f1f5f9 !important;
color: #64748b !important;
font-weight: 600 !important;
border-radius: 8px !important;
padding: 12px 20px !important;
}
"""
# Gradio Blocks 定義 - APIキー入力欄を削除
with gr.Blocks(css=custom_css, title="企業文化マッチング") as demo:
# ヘッダー
gr.HTML("<h1 style='text-align: center; margin: 30px 0 20px; color: #3b82f6;'>企業文化マッチング</h1>")
gr.HTML("<p style='text-align: center; margin-bottom: 30px; color: #64748b;'>あなたの価値観に合った企業を見つけましょう</p>")
# 質問1
with gr.Group(elem_classes="content-card"):
gr.HTML("<h3 style='margin-top: 0;'>Q1: 仕事における優先事項</h3>")
gr.HTML("<p style='color: #64748b; margin-bottom: 10px;'>あなたが仕事で最も大切にしている要素を選択してください</p>")
q1_choice = gr.Radio(
["柔軟性", "挑戦心", "安定志向", "成長機会重視", "ワークライフバランス", "高給与"],
label="最優先の価値観"
)
q1_text = gr.Textbox(
label="その他の重要な価値観",
placeholder="例:チームでの協力、創造性を発揮できる環境"
)
# 質問2
with gr.Group(elem_classes="content-card"):
gr.HTML("<h3 style='margin-top: 0;'>Q2: 理想的な働き方</h3>")
gr.HTML("<p style='color: #64748b; margin-bottom: 10px;'>あなたが重視する働き方や環境を選択してください</p>")
q2_choice = gr.Radio(
["リモート可", "フレックス", "完全出社", "ハイブリッド勤務"],
label="希望する働き方"
)
q2_text = gr.Textbox(
label="その他の希望",
placeholder="例:集中できる個室環境、リラックスできるスペース"
)
# 質問3
with gr.Group(elem_classes="content-card"):
gr.HTML("<h3 style='margin-top: 0;'>Q3: チームや組織のあり方</h3>")
gr.HTML("<p style='color: #64748b; margin-bottom: 10px;'>あなたが求める組織の特徴を選択してください</p>")
q3_choice = gr.Radio(
["裁量権大", "多様性", "明確な役割分担", "リーダーシップ重視"],
label="求める組織の特徴"
)
q3_text = gr.Textbox(
label="その他のポイント",
placeholder="例:オープンなコミュニケーション"
)
# 質問4
with gr.Group(elem_classes="content-card"):
gr.HTML("<h3 style='margin-top: 0;'>Q4: 成長機会</h3>")
gr.HTML("<p style='color: #64748b; margin-bottom: 10px;'>あなたが期待する成長機会を選択してください</p>")
q4_choice = gr.Radio(
["研修充実", "メンター制度", "自己学習支援", "海外研修"],
label="期待する成長機会"
)
q4_text = gr.Textbox(
label="その他の希望",
placeholder="例:最新技術の習得機会"
)
# 質問5
with gr.Group(elem_classes="content-card"):
gr.HTML("<h3 style='margin-top: 0;'>Q5: その他の大切な要素</h3>")
gr.HTML("<p style='color: #64748b; margin-bottom: 10px;'>その他にキャリアで重視することを選択してください</p>")
q5_choice = gr.Radio(
["企業文化重視", "高報酬", "勤務地", "社会貢献度"],
label="その他の重視ポイント"
)
q5_text = gr.Textbox(
label="具体的に",
placeholder="例:副業可、社会的意義のある事業"
)