Spaces:
Sleeping
Sleeping
File size: 34,251 Bytes
993e6a6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 | import json
import os
import sys
import random
import re
import concurrent.futures
from typing import List, Dict, Tuple
from datetime import datetime
import threading
from tqdm import tqdm
import requests
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from config import api_key, base_url
# OpenAI API configuration
API_KEY = api_key
API_PROVIDER = base_url
# Column combinations
COLUMN_COMBINATIONS = [
"categorical + numerical",
"categorical + numerical + categorical",
"categorical + numerical + numerical",
"categorical + numerical + numerical + categorical",
"temporal + numerical",
"temporal + numerical + categorical",
"categorical + numerical + temporal"
]
# Thread-safe print function
print_lock = threading.Lock()
def thread_safe_print(*args, **kwargs):
with print_lock:
print(*args, **kwargs)
# LLM 响应时间统计
import time
llm_stats_lock = threading.Lock()
llm_response_times = []
llm_stats_running = True
def get_llm_stats():
"""获取 LLM 统计信息"""
with llm_stats_lock:
if not llm_response_times:
return None
total_calls = len(llm_response_times)
avg_time = sum(llm_response_times) / total_calls
min_time = min(llm_response_times)
max_time = max(llm_response_times)
# 最近 10 次调用的平均时间
recent_times = llm_response_times[-10:]
recent_avg = sum(recent_times) / len(recent_times)
return {
'total_calls': total_calls,
'avg_time': avg_time,
'min_time': min_time,
'max_time': max_time,
'recent_avg': recent_avg
}
def llm_stats_reporter():
"""每 30 秒报告一次 LLM 统计信息"""
global llm_stats_running
while llm_stats_running:
time.sleep(30)
if not llm_stats_running:
break
stats = get_llm_stats()
if stats:
thread_safe_print(f"\n📊 [LLM Stats] 总调用: {stats['total_calls']} | "
f"平均: {stats['avg_time']:.2f}s | "
f"最近10次: {stats['recent_avg']:.2f}s | "
f"最小: {stats['min_time']:.2f}s | "
f"最大: {stats['max_time']:.2f}s")
def query_llm(prompt: str) -> str:
"""
Query LLM API with a prompt
Args:
prompt: The prompt to send to LLM
Returns:
str: The response from LLM
"""
headers = {
'Authorization': f'Bearer {API_KEY}',
'Content-Type': 'application/json'
}
data = {
'model': 'deepseek-v3.2',
'messages': [
{
'role': 'system',
'content': 'You are a senior data analyst and visualization expert with deep knowledge of real-world statistics, industry benchmarks, and data patterns. Generate realistic, diverse data that reflects authentic patterns found in published reports and research. Always return valid JSON format only when requested.'
},
{
'role': 'user',
'content': prompt
}
],
'temperature': 0.85
}
start_time = time.time()
try:
response = requests.post(
f'{API_PROVIDER}/chat/completions',
headers=headers,
json=data,
timeout=120
)
response.raise_for_status()
result = response.json()
content = result['choices'][0]['message']['content'].strip()
# 记录响应时间
elapsed = time.time() - start_time
with llm_stats_lock:
llm_response_times.append(elapsed)
return content
except requests.exceptions.Timeout:
elapsed = time.time() - start_time
with llm_stats_lock:
llm_response_times.append(elapsed)
thread_safe_print("❌ LLM API 超时(120秒)")
return None
except requests.exceptions.HTTPError as e:
elapsed = time.time() - start_time
with llm_stats_lock:
llm_response_times.append(elapsed)
thread_safe_print(f"❌ LLM API HTTP 错误: {e}")
if hasattr(e.response, 'text'):
thread_safe_print(f" 响应: {e.response.text[:200]}")
return None
except requests.exceptions.RequestException as e:
thread_safe_print(f"❌ LLM API 请求错误: {e}")
return None
except KeyError as e:
thread_safe_print(f"❌ LLM API 响应格式错误: {e}")
return None
except Exception as e:
thread_safe_print(f"❌ 查询 LLM 时出错: {e}")
return None
# Data facts dictionary
datafacts = {
"trend": {
"increase": "Increasing trend",
"decrease": "Decreasing trend",
"stable": "Stable trend",
"increase_then_decrease": "Increase then decrease",
"decrease_then_increase": "Decrease then increase",
"fluctuation": "Fluctuating trend"
},
"proportion": {
"majority": "Majority",
"minority": "Minority"
},
"value": {
"total": "Total",
"average": "Average",
"maximum": "Maximum",
"minimum": "Minimum"
},
"comparison": {
"average_higher": "Higher average value compared to others",
"average_lower": "Lower average value compared to others",
"significant_difference": "Significant difference compared to other categories"
},
"change": {
"sudden_increase": "Significantly higher value compared to the previous value",
"sudden_decrease": "Significantly lower value compared to the previous value",
},
"correlation": {
"positive": "Positive correlation",
"negative": "Negative correlation"
},
"rank": {
"first": "First",
"second": "Second",
"third": "Third",
"last": "Last"
}
}
def load_themes(file_path: str) -> List[Dict]:
"""Load themes from JSON file"""
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
def generate_scenarios_for_theme(theme: Dict, num_scenarios: int = 5) -> List[str]:
"""Step 1: Generate specific scenarios for a given theme
Args:
theme: 主题字典
num_scenarios: 要生成的场景数量,默认为5个
"""
prompt = f"""As a senior data journalist creating infographics for major publications (Bloomberg, The Economist, NYT), generate {num_scenarios} compelling visualization scenarios for: "{theme['theme']}" ({theme['description']})
**SCENARIO DIVERSITY MATRIX** (each scenario should hit different cells):
| Dimension | Options to Vary |
|-----------|-----------------|
| Geographic Scope | Global comparison, Regional (EU/Asia/Americas), National, City-level, Local |
| Time Frame | Historical (10+ years), Recent trends (2-5 years), Current snapshot, Future projection |
| Subject Type | Countries, Companies, Industries, Demographics, Products, Behaviors |
| Analysis Angle | Ranking, Comparison, Distribution, Change over time, Correlation, Breakdown |
| Audience | Executives, Policymakers, Consumers, Researchers, General public |
| Data Source Type | Government stats, Industry reports, Surveys, Academic research, Financial data |
**SCENARIO QUALITY REQUIREMENTS:**
1. Each scenario must have a clear "story hook" - what makes this data interesting or surprising?
2. Specify concrete analysis subjects (e.g., "Fortune 500 companies" not just "companies")
3. Include realistic data sources or contexts (e.g., "based on WHO 2023 data")
4. Make scenarios timely - reference recent events, emerging trends, or evergreen insights
5. Each scenario should be 20-35 words with specific details
**AVOID:**
- Generic scenarios without specific subjects
- Repetitive geographic or temporal scopes
- Abstract or theoretical framings
- Scenarios that couldn't be backed by real data
**EXAMPLE GOOD SCENARIOS:**
- "Comparing semiconductor manufacturing capacity across Taiwan, South Korea, US, and China from 2018-2024, showing the impact of CHIPS Act investments"
- "How Gen Z vs Millennials allocate monthly entertainment budgets across streaming, gaming, concerts, and dining based on 2023 consumer spending surveys"
- "European cities ranked by cost of living vs quality of life index, highlighting affordable livable alternatives to London and Paris"
FORMAT: Return ONLY a numbered list (1-{num_scenarios}), one scenario per line. No explanations.
"""
response = query_llm(prompt)
if not response:
return []
scenarios = []
for line in response.split('\n'):
line = line.strip()
if line and (line[0].isdigit() or line.lower().startswith('- ')):
scenario = line.lstrip('0123456789.- ').strip()
if scenario:
scenarios.append(scenario)
return scenarios[:num_scenarios]
def select_relevant_datafacts(theme: Dict, scenario: str) -> List[Dict]:
"""Step 2: Select relevant datafacts for the theme and scenario"""
# Convert datafacts to a flat list for easier processing
flat_datafacts = []
for category, facts in datafacts.items():
for key, description in facts.items():
flat_datafacts.append({
"category": category,
"key": key,
"description": description
})
prompt = f"""As a data storytelling expert, select the 5 most compelling data facts to highlight in this visualization:
**CONTEXT:**
- THEME: {theme['theme']}
- SCENARIO: {scenario}
**TASK:**
Select exactly 5 data facts that would create the most impactful and insightful visualization. Consider:
1. **Story Arc**: Choose facts that together tell a coherent narrative
2. **Diversity**: Mix different types (trends, comparisons, values, rankings)
3. **Relevance**: Facts should directly support the scenario's key message
4. **Visual Impact**: Facts that translate well into compelling visuals
5. **Insight Value**: Prioritize facts that reveal non-obvious patterns
**SELECTION STRATEGY by Scenario Type:**
- Time-based analysis → Prioritize: trend, change, value facts
- Comparison analysis → Prioritize: comparison, rank, proportion facts
- Distribution analysis → Prioritize: proportion, value, comparison facts
- Correlation analysis → Prioritize: correlation, trend, change facts
**AVAILABLE FACTS:**
{json.dumps(flat_datafacts, indent=2)}
**FORMAT:**
Return ONLY a numbered list (1-5):
1. [Category]: [Description]
No explanations.
"""
response = query_llm(prompt)
if not response:
return []
selected_facts = []
for line in response.split('\n'):
line = line.strip()
if line:
for fact in flat_datafacts:
if fact['description'].lower() in line.lower():
selected_facts.append(fact)
break
return selected_facts[:3]
def extract_json_from_response(response: str) -> str:
"""Extract JSON from LLM response using regex"""
if not response:
return "{}"
# Try to find JSON content between triple backticks
json_match = re.search(r'```(?:json)?\s*([\s\S]*?)\s*```', response)
if json_match:
extracted = json_match.group(1).strip()
return json.loads(extracted)
# Try to find content that looks like a JSON object
json_match = re.search(r'(\{[\s\S]*\})', response)
if json_match:
extracted = json_match.group(1).strip()
return json.loads(extracted)
# Return the original response if no JSON pattern found
return response.strip()
def parse_json_safely(text: str) -> Dict:
"""Parse JSON from text safely with error handling"""
if not text:
thread_safe_print(f"Warning: Empty text to parse as JSON")
return {}
return extract_json_from_response(text)
def validate_generated_data(generated_data, column_recommendation):
"""验证生成的数据是否有效"""
validation = {
"is_valid": True,
"issues": []
}
# 检查数据是否为空
if not generated_data or "data" not in generated_data or not generated_data["data"]:
validation["is_valid"] = False
validation["issues"].append("Empty data array")
return validation
# 获取期望的列
expected_columns = [col["name"] for col in column_recommendation.get("columns", [])]
# 检查每行数据
for i, row in enumerate(generated_data["data"]):
# 检查是否包含所有列
for col in expected_columns:
if col not in row:
validation["is_valid"] = False
validation["issues"].append(f"Row {i} missing column '{col}'")
# 检查数值列的数据类型
for col in column_recommendation.get("columns", []):
if col["name"] in row and col["data_type"] == "numerical":
if not isinstance(row[col["name"]], (int, float)):
validation["is_valid"] = False
validation["issues"].append(f"Row {i}, column '{col['name']}' has non-numeric value: {row[col['name']]}")
return validation
def recommend_columns(theme: Dict, scenario: str, selected_fact: Dict) -> Dict:
"""Step 3: Recommend column structure based on theme, scenario and selected facts"""
facts_str = f"- {selected_fact['category']}: {selected_fact['description']}"
prompt = f"""As a data architect designing schemas for business intelligence dashboards, recommend the optimal column structure:
**CONTEXT:**
- THEME: {theme['theme']}
- SCENARIO: {scenario}
- KEY DATA FACT: {facts_str}
**COLUMN COMBINATION OPTIONS:**
| Combination | Best For | Example |
|-------------|----------|---------|
| categorical + numerical | Rankings, comparisons | Countries by GDP |
| categorical + numerical + numerical | Multi-metric comparisons | Companies: Revenue vs Profit |
| categorical + numerical + categorical | Grouped comparisons | Products by Revenue by Category |
| temporal + numerical | Time series, trends | Monthly sales 2020-2024 |
| temporal + numerical + categorical | Multi-series trends | Quarterly revenue by region |
| categorical + numerical + temporal | Snapshot comparisons | Country metrics across periods |
**DECISION LOGIC:**
1. Does the scenario involve change over TIME? → Use temporal column
2. Does it compare CATEGORIES at a single point? → categorical + numerical
3. Does it need MULTIPLE METRICS per item? → Add second numerical column
4. Does it need to SEGMENT by groups? → Add categorical column
5. PREFER SIMPLER combinations - only add columns if truly needed
**COLUMN NAMING RULES - KEEP IT SHORT:**
✓ GOOD column names (short, clear):
- "Country", "Company", "Region", "City", "Industry"
- "Revenue", "Growth Rate", "Market Share", "GDP", "Population"
- "Corruption Index", "Democracy Index", "Happiness Score"
- "Year", "Quarter", "Month"
✗ BAD column names (too long, avoid):
- "Corruption Perceptions Index 2023 (Transparency International, 0-100)" → Use "Corruption Index"
- "EIU Democracy Index 2023 (score 0-10)" → Use "Democracy Index"
- "Annual Revenue in USD Millions" → Use "Revenue"
**DESCRIPTION RULES - ONE SHORT SENTENCE:**
- Keep descriptions brief and informative (5-15 words max)
- Example: "Annual company revenue" not "Annual revenue figures for Fortune 500 companies based on fiscal year 2023 reports"
- Example: "Corruption perception score by country" not "Transparency International's Corruption Perceptions Index measuring public sector corruption levels"
**UNIT RULES - SYMBOL ONLY:**
- Use ONLY unit symbols: $, €, £, ¥, %, K, M, B, km, kg, °C, kWh, ms, etc.
- For dimensionless metrics (index, score, ratio): leave unit as empty string ""
- ✗ WRONG: "index", "score", "points", "0-100", "0-10", "USD millions"
- ✓ CORRECT: "", "%", "$", "M", "B"
**FORMAT:**
Return ONLY valid JSON (no markdown):
{{
"selected_combination": "combination_name",
"columns": [
{{
"name": "ShortName",
"description": "Brief one-sentence description",
"data_type": "categorical/numerical/temporal",
"unit": "$ or % or M or empty string"
}}
]
}}
**EXAMPLE OUTPUT:**
{{
"selected_combination": "categorical + numerical",
"columns": [
{{"name": "Country", "description": "G20 member countries", "data_type": "categorical", "unit": ""}},
{{"name": "GDP", "description": "Gross domestic product", "data_type": "numerical", "unit": "B"}}
]
}}
"""
response = query_llm(prompt)
if not response:
return {"selected_combination": COLUMN_COMBINATIONS[0], "columns": []}
result = parse_json_safely(response)
try:
# Validate the structure to ensure it has required keys
if "selected_combination" not in result:
result["selected_combination"] = COLUMN_COMBINATIONS[0]
if "columns" not in result:
result["columns"] = []
# Add unit field if missing
for col in result["columns"]:
if "unit" not in col and col["data_type"] == "numerical":
# Extract unit from description if possible
desc = col["description"]
if "%" in desc:
col["unit"] = "%"
elif "$" in desc:
col["unit"] = "$"
elif "£" in desc:
col["unit"] = "£"
elif "€" in desc:
col["unit"] = "€"
else:
col["unit"] = ""
elif "unit" not in col:
col["unit"] = ""
return result
except Exception as e:
thread_safe_print(f"Error parsing column recommendation: {e}")
print("response: ", result)
# Return a default structure
return {
"selected_combination": COLUMN_COMBINATIONS[0],
"columns": [
{
"name": "Category",
"description": "Main category for the data",
"data_type": "categorical",
"unit": ""
},
{
"name": "Value",
"description": "Numerical value",
"data_type": "numerical",
"unit": ""
}
]
}
def generate_data(theme: Dict, scenario: str, selected_facts: List[Dict], column_recommendation: Dict, times = 1) -> List[Dict]:
"""生成数据"""
results = []
for _ in range(times):
facts_str = "\n".join([f"- {fact['category']}: {fact['description']}" for fact in selected_facts])
columns_str = "\n".join([f"- {col['name']} ({col['data_type']}): {col['description']}" for col in column_recommendation['columns']])
# Determine data size constraints based on column combination using ranges
combination = column_recommendation['selected_combination']
constraints = []
# Generate range constraints for different combinations
if combination == "categorical + numerical" or combination == "categorical + numerical + numerical":
constraints.append("First categorical column should have between 8-20 unique values")
elif combination == "categorical + numerical + categorical" or combination == "categorical + numerical + numerical + categorical":
constraints.append("First categorical column should have between 8-20 unique values")
constraints.append("Second categorical column should have between 2-6 unique values")
constraints.append("Total unique combinations should not exceed 60")
elif combination == "temporal + numerical":
constraints.append("Number of time points should be between 8-20")
elif combination == "temporal + numerical + categorical":
constraints.append("Number of time points should be between 5-20")
constraints.append("Number of categories should be between 2-7")
elif combination == "categorical + numerical + temporal":
constraints.append("First categorical column should have between 5-20 unique values")
constraints.append("Number of time points should be between 2-4")
constraints_str = "\n".join([f"- {constraint}" for constraint in constraints])
# 在提示中强调组合完整性和真实性
prompt = f"""You are a statistician at a major research institution creating synthetic data that mirrors real-world patterns for this visualization:
**VISUALIZATION CONTEXT:**
- THEME: {theme['theme']}
- SCENARIO: {scenario}
- KEY DATA FACTS TO HIGHLIGHT: {facts_str}
**COLUMN STRUCTURE:**
{columns_str}
**DATA SIZE REQUIREMENTS:**
{constraints_str}
**REALISM REQUIREMENTS - CRITICAL:**
1. **Categorical Values - Use REAL names:**
- Countries: Use actual country names (USA, Germany, Japan, Brazil, etc.)
- Companies: Use real company names (Apple, Toyota, Samsung, Nestlé, etc.)
- Cities: Use real city names (Tokyo, New York, London, Shanghai, etc.)
- Industries: Use standard industry names (Healthcare, Technology, Finance, etc.)
- Products: Use realistic product categories or actual brands
- Demographics: Use realistic age groups, income brackets, education levels
2. **Numerical Values - Match real-world magnitudes:**
- GDP: Trillions for large countries, billions for smaller ones
- Population: Match actual country/city scales
- Percentages: Realistic ranges (market share 1-40%, growth rates -5% to 15%)
- Prices: Match real-world price points for the category
- Revenue: Match industry benchmarks (tech companies in billions, local businesses in millions)
- Include natural variance - avoid round numbers for most values (use 47.3 not 50)
3. **Temporal Values:**
- Use YYYY, YYYY-MM, or YYYY-MM-DD format strictly
- Choose appropriate time spans (economic trends: 2015-2024, recent events: 2022-2024)
- Ensure chronological consistency in trends
4. **Data Patterns - Reflect reality:**
- Include outliers naturally (one market leader, one laggard)
- Show regional/cultural patterns (Asian countries often cluster, Nordic countries cluster)
- Respect known facts (USA/China usually top GDP, Nordic countries top happiness indices)
- Add natural noise - real data isn't perfectly smooth
**COMBINATION REQUIREMENTS:**
- When both temporal and categorical columns exist, generate data for ALL combinations
- Example: Years=[2020,2021,2022] × Countries=[USA,China] = 6 data points
**TITLE REQUIREMENTS:**
- main_title: Compelling headline that could appear in The Economist or Bloomberg (8-15 words)
- sub_title: Contextual detail with time period, data source style, or key finding
**FORMAT:**
Return ONLY valid JSON (no markdown, no explanation):
{{
"data": [
{{"column_name1": "value1", "column_name2": value2, ...}}
],
"main_insight": "One clear sentence stating the single most important finding from this data",
"description": "A comprehensive paragraph (80-150 words) that tells the complete data story. Include: (1) What the data shows - the key patterns and findings, (2) Why it matters - the significance and implications, (3) Notable outliers or surprises in the data, (4) Context that helps interpret the numbers. Write as if explaining the visualization to someone who hasn't seen it. This should read like a data journalism paragraph that could accompany the chart in a publication.",
"titles": {{
"main_title": "Publication-quality headline",
"sub_title": "Contextual subtitle with specifics"
}}
}}
"""
response = query_llm(prompt)
if not response:
continue
try:
result = parse_json_safely(response)
results.append(result)
except Exception as e:
thread_safe_print(f"Error parsing response: {e}")
continue
return results
def process_theme(theme: Dict, syn_data_dir: str) -> Dict:
"""处理单个主题、生成场景并保存数据"""
thread_safe_print(f"Processing theme: '{theme['theme']}'")
# 步骤1:生成场景
scenarios = generate_scenarios_for_theme(theme)
result = {
'theme': theme['theme'],
'base_description': theme['description'],
'main_category': theme.get('main_category', ''), # 添加main_category
'scenarios': []
}
# 处理每个场景
for i, scenario in enumerate(scenarios):
scenario_num = i + 1
thread_safe_print(f" Scenario {scenario_num}/{len(scenarios)}: '{scenario[:50]}...'")
selected_facts = select_relevant_datafacts(theme, scenario)
for fact in selected_facts:
try:
column_recommendation = recommend_columns(theme, scenario, fact)
# 如果没有有效的列结构,跳过
if not column_recommendation or "columns" not in column_recommendation or not column_recommendation["columns"]:
thread_safe_print(f" No valid column structure, skipping")
continue
# 步骤4:生成数据
generated_datas = generate_data(theme, scenario, selected_facts, column_recommendation)
# 如果没有数据,跳过
for generated_data in generated_datas:
if not generated_data or "data" not in generated_data or not generated_data["data"]:
thread_safe_print(f" No data generated, skipping")
continue
# 准备场景结果
scenario_result = {
'description': generated_data.get('description', scenario),
'data': {
'data': generated_data.get('data', []),
'columns': column_recommendation.get('columns', []),
'type_combination': column_recommendation.get('selected_combination', '')
},
'metadata': {
'main_insight': generated_data.get('main_insight', ''),
'scenario': scenario,
'datafact': selected_facts
},
'titles': generated_data.get('titles', {'main_title': '', 'sub_title': ''})
}
save_individual_data(theme['theme'], scenario_result, scenario_num, syn_data_dir, theme.get('main_category', None))
thread_safe_print(f" ✓ Completed")
except Exception as e:
thread_safe_print(f" ✗ Error: {str(e)}")
continue
return result
def process_theme_wrapper(args):
"""Wrapper for process_theme to be used with ProcessPoolExecutor"""
theme, syn_data_dir, theme_idx, total_themes = args
thread_safe_print(f"\nProcessing theme {theme_idx+1}/{total_themes}: '{theme['theme']}'")
try:
theme_result = process_theme(theme, syn_data_dir)
thread_safe_print(f"✓ Completed processing for theme {theme_idx+1}/{total_themes}: '{theme['theme']}'")
return theme['theme'], theme_result
except Exception as e:
thread_safe_print(f"✗ Error processing theme '{theme['theme']}': {e}")
import traceback
thread_safe_print(f"Stack trace: {traceback.format_exc()}")
return theme['theme'], {
'theme': theme['theme'],
'base_description': theme['description'],
'main_category': theme.get('main_category', ''), # 添加main_category
'scenarios': []
}
def save_results(results: Dict, output_file: str):
"""Save generated results to a JSON file"""
with print_lock: # Use lock to prevent file corruption from multiple threads
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(results, f, indent=2, ensure_ascii=False)
def save_individual_data(theme_name: str, scenario_data: Dict, index: int, syn_data_dir: str, main_category: str = None):
"""Save individual scenario data to separate JSON files in syn_data directory"""
# Create safe filename from main_category (if available) or theme
if main_category:
prefix = "".join(c for c in main_category if c.isalnum() or c in [' ', '_']).strip().replace(' ', '_')
else:
prefix = "".join(c for c in theme_name if c.isalnum() or c in [' ', '_']).strip().replace(' ', '_')
timestamp = datetime.now().strftime("%H%M%S")
filename = f"{prefix}_scenario_{index}_{timestamp}_{random.randint(10000, 99999)}.json"
filepath = os.path.join(syn_data_dir, filename)
try:
with print_lock: # Use lock to prevent file corruption from multiple threads
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(scenario_data, f, indent=2, ensure_ascii=False)
return True
except Exception as e:
thread_safe_print(f"Error saving individual data file {filepath}: {e}")
return False
def main():
global llm_stats_running
# File paths
current_dir = os.path.dirname(os.path.abspath(__file__))
theme_file = os.path.join(current_dir, 'theme_new.json')
output_file = os.path.join(current_dir, 'theme_analysis.json')
thread_safe_print(f"Starting data generation process")
thread_safe_print(f"Theme file: {theme_file}")
thread_safe_print(f"Output file: {output_file}")
# 启动 LLM 统计报告线程
stats_thread = threading.Thread(target=llm_stats_reporter, daemon=True)
stats_thread.start()
thread_safe_print("📊 LLM 响应时间监控已启动(每30秒报告一次)")
# Create syn_data directory if it doesn't exist
syn_data_dir = os.path.join(current_dir, 'syn_data')
print('Creating syn_data directory...', syn_data_dir)
if not os.path.exists(syn_data_dir):
os.makedirs(syn_data_dir)
thread_safe_print(f"Created directory: {syn_data_dir}")
# Load themes
try:
thread_safe_print(f"Loading themes from {theme_file}...")
themes = load_themes(theme_file)
thread_safe_print(f"Loaded {len(themes)} themes")
except Exception as e:
thread_safe_print(f"Error loading themes: {e}")
return
# Prepare for parallel processing
results = {}
NUM_WORKERS = 20 # Number of concurrent threads
thread_safe_print(f"Using {NUM_WORKERS} concurrent workers for processing")
# Create arguments for each theme processing task
theme_args = [(theme, syn_data_dir, idx, len(themes)) for idx, theme in enumerate(themes)]
# Process themes in parallel
with concurrent.futures.ThreadPoolExecutor(max_workers=NUM_WORKERS) as executor:
# Submit all tasks and get futures
future_to_theme = {executor.submit(process_theme_wrapper, args): args[0]['theme'] for args in theme_args}
# Process results as they complete
for future in tqdm(concurrent.futures.as_completed(future_to_theme), total=len(themes), desc="Processing themes"):
theme_name = future_to_theme[future]
try:
theme_name, theme_result = future.result()
results[theme_name] = theme_result
# Save overall progress after each theme
save_results(results, output_file)
thread_safe_print(f"Updated overall progress file with theme '{theme_name}'")
except Exception as e:
thread_safe_print(f"Error processing theme '{theme_name}': {e}")
# 停止 LLM 统计报告线程
llm_stats_running = False
# 输出最终 LLM 统计
final_stats = get_llm_stats()
thread_safe_print(f"\n========== SUMMARY ==========")
thread_safe_print(f"Processed {len(results)} themes")
total_scenarios = sum(len(theme_data['scenarios']) for theme_data in results.values())
thread_safe_print(f"Generated {total_scenarios} scenarios in total")
thread_safe_print(f"Full analysis saved to: {output_file}")
thread_safe_print(f"Individual scenario data saved to: {syn_data_dir}")
if final_stats:
thread_safe_print(f"\n📊 LLM 调用统计:")
thread_safe_print(f" 总调用次数: {final_stats['total_calls']}")
thread_safe_print(f" 平均响应时间: {final_stats['avg_time']:.2f}s")
thread_safe_print(f" 最小响应时间: {final_stats['min_time']:.2f}s")
thread_safe_print(f" 最大响应时间: {final_stats['max_time']:.2f}s")
thread_safe_print(f"============================\n")
if __name__ == "__main__":
main() |