"""data-ana ClickHouse 种子数据脚本. 生成 mock 数据并写入 ClickHouse 5 张宽表: - student_dashboard_view: 30 学生 × 5 考试 × 3 学科 + 30 学生 × 10 作业 - student_errors: 每生 2~5 条错题 - mastery_snapshot: 30 学生 × 6 知识点 × 3 历史快照 - ai_usage_log: 80 条 AI 用量记录 - attendance_logs: 30 学生 × 30 天(跳过周末) 使用方式:python scripts/seed_clickhouse.py 环境变量:DATA_ANA_CLICKHOUSE_HOST / DATA_ANA_CLICKHOUSE_PORT / DATA_ANA_CLICKHOUSE_DATABASE """ from __future__ import annotations import contextlib import os import random from datetime import UTC, datetime, timedelta from typing import Any import clickhouse_connect DATABASE = os.environ.get("DATA_ANA_CLICKHOUSE_DATABASE", "edu_analytics") HOST = os.environ.get("DATA_ANA_CLICKHOUSE_HOST", "localhost") PORT = int(os.environ.get("DATA_ANA_CLICKHOUSE_PORT", "8123")) USERNAME = os.environ.get("DATA_ANA_CLICKHOUSE_USERNAME", "default") PASSWORD = os.environ.get("DATA_ANA_CLICKHOUSE_PASSWORD", "") SUBJECTS = ["math", "chinese", "english"] CLASSES = ["class-001", "class-002"] KNOWLEDGE_POINTS = { "math": ["kp-math-001", "kp-math-002", "kp-math-003"], "chinese": ["kp-cn-001", "kp-cn-002"], "english": ["kp-en-001", "kp-en-002"], } PROVIDERS = ["openai", "anthropic", "baichuan", "local"] MODELS = ["gpt-4o", "claude-3-sonnet", "baichuan-2", "local-llama"] BATCH_SIZE = 500 SEED = 42 def generate_students() -> list[dict[str, str]]: students: list[dict[str, str]] = [] for i in range(30): class_id = CLASSES[i % 2] students.append({ "student_id": f"stu-{i + 1:03d}", "class_id": class_id, }) return students def generate_dashboard_rows( students: list[dict[str, str]], rng: random.Random ) -> list[list[Any]]: rows: list[list[Any]] = [] base_time = datetime.now(UTC) - timedelta(days=30) for s in students: for exam_idx in range(5): exam_id = f"exam-{exam_idx + 1:03d}" for subject in SUBJECTS: score = round(rng.uniform(55, 98), 1) rank = rng.randint(1, 30) for kp in KNOWLEDGE_POINTS[subject]: mastery = round(rng.uniform(0.2, 0.95), 3) error_count = rng.randint(0, 5) ts = base_time + timedelta( days=exam_idx * 6, hours=rng.randint(0, 23) ) rows.append([ s["student_id"], s["class_id"], exam_id, subject, score, rank, kp, mastery, error_count, ts, ]) for hw_idx in range(10): hw_id = f"hw-{hw_idx + 1:03d}" for subject in SUBJECTS: score = round(rng.uniform(60, 100), 1) ts = base_time + timedelta(days=hw_idx * 3, hours=rng.randint(0, 23)) rows.append([ s["student_id"], s["class_id"], hw_id, subject, score, rng.randint(1, 30), "kp-hw", 0.0, 0, ts, ]) return rows def generate_error_rows( students: list[dict[str, str]], rng: random.Random ) -> list[list[Any]]: rows: list[list[Any]] = [] for s in students: count = rng.randint(2, 5) for _ in range(count): subject = rng.choice(SUBJECTS) kp = rng.choice(KNOWLEDGE_POINTS[subject]) qid = f"q-{rng.randint(1, 200):03d}" ts = datetime.now(UTC) - timedelta(days=rng.randint(1, 30)) rows.append([ s["student_id"], qid, kp, rng.randint(1, 4), ts, f"{subject} error", ]) return rows def generate_mastery_rows( students: list[dict[str, str]], rng: random.Random ) -> list[list[Any]]: rows: list[list[Any]] = [] base_time = datetime.now(UTC) - timedelta(days=30) for s in students: for subject in SUBJECTS: for kp in KNOWLEDGE_POINTS[subject]: for snap in range(3): mastery = round(rng.uniform(0.3, 0.9), 3) ts = base_time + timedelta(days=snap * 10) rows.append([ s["student_id"], kp, subject, mastery, ts, "weighted_moving_avg", ]) return rows def generate_attendance_rows( students: list[dict[str, str]], rng: random.Random ) -> list[list[Any]]: rows: list[list[Any]] = [] start_date = datetime.now(UTC).date() - timedelta(days=30) for s in students: for day_offset in range(30): d = start_date + timedelta(days=day_offset) if d.weekday() >= 5: continue status = rng.choices( ["present", "absent", "late", "leave"], weights=[85, 5, 7, 3], )[0] ts = datetime.combine(d, datetime.min.time()).replace(tzinfo=UTC) rows.append([ s["student_id"], s["class_id"], d, status, "teacher-001", "", ts, ]) return rows def generate_ai_usage_rows(rng: random.Random) -> list[list[Any]]: rows: list[list[Any]] = [] for i in range(80): ts = datetime.now(UTC) - timedelta( days=rng.randint(0, 30), hours=rng.randint(0, 23) ) prompt_t = rng.randint(50, 2000) completion_t = rng.randint(20, 1500) rows.append([ f"req-{i + 1:04d}", f"stu-{rng.randint(1, 30):03d}", rng.choice(PROVIDERS), rng.choice(MODELS), prompt_t, completion_t, prompt_t + completion_t, rng.randint(100, 5000), rng.random() > 0.05, rng.randint(1, 50), ts, ]) return rows def batch_insert( client: Any, table: str, columns: list[str], rows: list[list[Any]] ) -> None: if not rows: return for i in range(0, len(rows), BATCH_SIZE): chunk = rows[i : i + BATCH_SIZE] client.insert(f"{DATABASE}.{table}", chunk, column_names=columns) print(f" {table}: {len(rows)} rows inserted") def main() -> None: rng = random.Random(SEED) print("=" * 70) print("data-ana ClickHouse 种子数据脚本") print(f" Host: {HOST}:{PORT} Database: {DATABASE}") print("=" * 70) client = clickhouse_connect.get_client( host=HOST, port=PORT, username=USERNAME, password=PASSWORD, database=DATABASE, ) students = generate_students() print(f"\n生成 {len(students)} 名学生(2 个班级)") print("\n[1/5] student_dashboard_view...") dashboard_rows = generate_dashboard_rows(students, rng) batch_insert(client, "student_dashboard_view", [ "student_id", "class_id", "exam_id", "subject_id", "score", "rank_in_class", "knowledge_point_id", "mastery_level", "error_count", "last_updated", ], dashboard_rows) print("\n[2/5] student_errors...") error_rows = generate_error_rows(students, rng) batch_insert(client, "student_errors", [ "student_id", "question_id", "knowledge_point_id", "error_count", "last_error_time", "content", ], error_rows) print("\n[3/5] mastery_snapshot...") mastery_rows = generate_mastery_rows(students, rng) batch_insert(client, "mastery_snapshot", [ "student_id", "knowledge_point_id", "subject_id", "mastery_level", "calculated_at", "calculation_method", ], mastery_rows) print("\n[4/5] attendance_logs...") attendance_rows = generate_attendance_rows(students, rng) batch_insert(client, "attendance_logs", [ "student_id", "class_id", "attendance_date", "status", "recorded_by", "remark", "occurred_at", ], attendance_rows) print("\n[5/5] ai_usage_log...") ai_rows = generate_ai_usage_rows(rng) batch_insert(client, "ai_usage_log", [ "request_id", "user_id", "provider", "model", "prompt_tokens", "completion_tokens", "total_tokens", "latency_ms", "success", "cost_cents", "occurred_at", ], ai_rows) print("\n" + "=" * 70) print("种子数据写入完成") print(f" student_dashboard_view: {len(dashboard_rows)} rows") print(f" student_errors: {len(error_rows)} rows") print(f" mastery_snapshot: {len(mastery_rows)} rows") print(f" attendance_logs: {len(attendance_rows)} rows") print(f" ai_usage_log: {len(ai_rows)} rows") print(f" Total: {len(dashboard_rows) + len(error_rows) + len(mastery_rows) + len(attendance_rows) + len(ai_rows)} rows") print("=" * 70) with contextlib.suppress(Exception): client.close() print("完成。") if __name__ == "__main__": main()