ANIMATEDIFF PRO新手指南:从Python爬虫到电影级动画,完整实战流程

最近有个朋友问我,能不能用AI把枯燥的数据报表变成动态视频,比如把股票走势、天气变化这些数字,直接生成一段电影质感的动画。我第一反应是:这想法挺酷,但实现起来会不会很复杂?

试了几个方案后,我发现了一个绝佳组合——Python爬虫抓数据,ANIMATEDIFF PRO做动画。你可能听说过AnimateDiff,但ANIMATEDIFF PRO是它的升级版,专门为追求电影级效果而生。今天我就带你从零开始,完整走一遍这个流程。

1. 项目思路:当数据遇见电影级动画

传统的数据可视化是什么样?折线图、柱状图、热力图,这些静态图表虽然能传达信息,但总觉得少了点生命力。如果能让数据“动”起来,而且动得有电影质感,那会是怎样的体验?

我的想法很简单:用Python爬虫实时抓取数据,把这些数字转换成动画参数,然后用ANIMATEDIFF PRO生成电影级的动态视频。听起来有点抽象?我给你看几个实际场景:

场景一:天气变化可视化 早上6点,城市在晨雾中苏醒,色调偏冷,画面柔和;中午12点,阳光明媚,色彩饱和度高,影子清晰;傍晚6点,夕阳西下,暖色调,长影子。这不是天气预报,这是用真实天气数据生成的24小时城市景观动画。

场景二:股票波动可视化 股价上涨时,镜头缓缓上移,画面充满绿色粒子,节奏轻快;股价下跌时,镜头下沉,红色粒子密集,氛围紧张;成交量放大时,粒子数量激增,形成视觉冲击。这不是K线图,这是用实时交易数据生成的金融艺术动画。

场景三:社交媒体情绪可视化 话题热度上升,画面中心发光,粒子向外扩散;负面情绪聚集,色调变暗,粒子向内收缩;讨论激烈时,画面充满动态线条。这不是词云图,这是用舆情数据生成的情感波动动画。

这套方案的核心价值在于:数据驱动创意,AI实现视觉化。你不用懂3D建模,不用学视频剪辑,只要会写Python代码,就能把任何时序数据变成电影级动画。

2. 环境准备:10分钟快速搭建

在开始写代码之前,我们需要准备好工作环境。如果你已经在星图GPU平台上部署了ANIMATEDIFF PRO镜像,那大部分环境已经配置好了。如果没有,跟着我一步步来。

2.1 基础环境检查

首先确认你的Python环境,我推荐使用Python 3.8以上版本。打开终端,运行以下命令检查:

# 检查Python版本
import sys
print(f"Python版本: {sys.version}")

# 检查关键库
required_libs = ['requests', 'pandas', 'numpy', 'json', 'datetime']
missing_libs = []

for lib in required_libs:
    try:
        __import__(lib)
        print(f"✓ {lib} 已安装")
    except ImportError:
        missing_libs.append(lib)
        print(f"✗ {lib} 未安装")

if missing_libs:
    print(f"\n需要安装的库: {missing_libs}")
    print("运行: pip install " + " ".join(missing_libs))
else:
    print("\n所有依赖库已就绪!")

如果发现有库没安装,用pip安装一下:

pip install requests pandas numpy

2.2 ANIMATEDIFF PRO配置确认

ANIMATEDIFF PRO镜像已经预装了所有必要的组件,但我们还是需要确认几个关键点:

  1. 服务是否启动:在终端运行 bash /root/build/start.sh,看到类似下面的输出就说明启动成功了:

    Starting AnimateDiff PRO service...
    Server running on http://localhost:5000
    Cinema UI ready!
    
  2. 访问Web界面:在浏览器打开 http://localhost:5000,你应该能看到一个深色系的专业界面,这就是ANIMATEDIFF PRO的电影级渲染工作台。

  3. API接口测试:ANIMATEDIFF PRO提供了REST API,我们可以用Python直接调用。先做个简单的测试:

import requests

def test_animatediff_api():
    """测试ANIMATEDIFF PRO API连接"""
    try:
        response = requests.get("http://localhost:5000/api/status", timeout=5)
        if response.status_code == 200:
            print("✓ ANIMATEDIFF PRO API连接正常")
            return True
        else:
            print(f"✗ API返回状态码: {response.status_code}")
            return False
    except Exception as e:
        print(f"✗ 连接失败: {e}")
        print("请确认ANIMATEDIFF PRO服务已启动")
        return False

# 运行测试
if test_animatediff_api():
    print("环境准备完成,可以开始项目了!")
else:
    print("请先启动ANIMATEDIFF PRO服务")

2.3 项目目录结构

建议按下面的结构组织你的项目文件,这样代码清晰,管理方便:

data_driven_animation/
├── data/                    # 数据存储
│   ├── raw/                # 原始数据
│   ├── processed/          # 处理后的数据
│   └── cache/              # 缓存文件
├── scripts/                # Python脚本
│   ├── crawlers/           # 爬虫脚本
│   ├── mappers/            # 数据映射脚本
│   ├── generators/         # 动画生成脚本
│   └── utils/              # 工具函数
├── outputs/                # 输出文件
│   ├── animations/         # 生成的动画
│   ├── configs/            # 配置文件
│   └── logs/               # 日志文件
├── config.yaml             # 项目配置文件
└── main.py                 # 主程序

创建这个目录结构:

import os

project_structure = {
    "data": ["raw", "processed", "cache"],
    "scripts": ["crawlers", "mappers", "generators", "utils"],
    "outputs": ["animations", "configs", "logs"]
}

for main_dir, sub_dirs in project_structure.items():
    os.makedirs(main_dir, exist_ok=True)
    for sub_dir in sub_dirs:
        os.makedirs(os.path.join(main_dir, sub_dir), exist_ok=True)

print("项目目录结构创建完成!")

3. 数据采集:Python爬虫实战

数据是动画的原料,没有好数据,再好的AI也做不出好动画。这一节我带你写几个实用的爬虫,从简单到复杂,覆盖常见的应用场景。

3.1 天气数据爬虫:从中国天气网获取实时数据

天气数据是最适合做动画可视化的,变化有规律,数据容易获取。我们从一个简单的天气爬虫开始:

import requests
import pandas as pd
from datetime import datetime
import time
import json

class WeatherDataCrawler:
    """天气数据爬虫类"""
    
    def __init__(self, city="北京"):
        self.city = city
        self.base_url = "http://t.weather.sojson.com/api/weather/city/"
        self.data_file = f"data/raw/weather_{city}_{datetime.now().strftime('%Y%m%d')}.json"
        
        # 城市代码映射(简化版,实际需要更完整的映射)
        self.city_codes = {
            "北京": "101010100",
            "上海": "101020100",
            "广州": "101280101",
            "深圳": "101280601",
            "杭州": "101210101",
            "成都": "101270101",
            "武汉": "101200101",
            "西安": "101110101"
        }
    
    def get_city_code(self):
        """获取城市代码"""
        return self.city_codes.get(self.city, "101010100")  # 默认北京
    
    def fetch_current_weather(self):
        """获取当前天气数据"""
        city_code = self.get_city_code()
        url = f"{self.base_url}{city_code}"
        
        print(f"正在获取 {self.city} 的天气数据...")
        
        try:
            # 设置请求头,模拟浏览器访问
            headers = {
                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
                "Accept": "application/json"
            }
            
            response = requests.get(url, headers=headers, timeout=10)
            response.raise_for_status()  # 检查HTTP错误
            
            data = response.json()
            
            if data.get("status") == 200:
                weather_info = self._parse_weather_data(data)
                print(f"获取成功: {weather_info['temperature']}°C, {weather_info['weather']}")
                return weather_info
            else:
                print(f"API返回错误: {data.get('message')}")
                return None
                
        except requests.exceptions.RequestException as e:
            print(f"网络请求失败: {e}")
            return None
        except json.JSONDecodeError as e:
            print(f"JSON解析失败: {e}")
            return None
        except Exception as e:
            print(f"未知错误: {e}")
            return None
    
    def _parse_weather_data(self, data):
        """解析天气数据"""
        weather_info = data.get("data", {})
        forecast = weather_info.get("forecast", [{}])[0]  # 今天预报
        
        # 构建完整的数据结构
        result = {
            "timestamp": datetime.now().isoformat(),
            "city": self.city,
            "city_code": data.get("cityInfo", {}).get("cityId", ""),
            # 基础天气信息
            "temperature": int(weather_info.get("wendu", 0)),  # 温度
            "humidity": int(weather_info.get("shidu", "0%").replace("%", "")),  # 湿度
            "pressure": weather_info.get("pressure", 0),  # 气压
            # 天气状况
            "weather": forecast.get("type", "晴"),  # 天气类型
            "high_temp": int(forecast.get("high", "0°").replace("高温 ", "").replace("°", "")),
            "low_temp": int(forecast.get("low", "0°").replace("低温 ", "").replace("°", "")),
            # 风信息
            "wind_direction": forecast.get("fx", "无风"),  # 风向
            "wind_power": forecast.get("fl", "微风"),  # 风力
            # 其他信息
            "sunrise": weather_info.get("sunrise", ""),  # 日出
            "sunset": weather_info.get("sunset", ""),  # 日落
            "aqi": data.get("cityInfo", {}).get("aqi", 0),  # 空气质量
            "quality": data.get("cityInfo", {}).get("quality", ""),  # 空气质量等级
            # 提示信息
            "ganmao": weather_info.get("ganmao", ""),  # 感冒提示
            "tips": forecast.get("notice", "")  # 生活提示
        }
        
        return result
    
    def save_weather_data(self, weather_data):
        """保存天气数据"""
        if not weather_data:
            return False
        
        try:
            # 读取现有数据
            try:
                with open(self.data_file, "r", encoding="utf-8") as f:
                    existing_data = json.load(f)
                    if not isinstance(existing_data, list):
                        existing_data = []
            except (FileNotFoundError, json.JSONDecodeError):
                existing_data = []
            
            # 添加新数据
            existing_data.append(weather_data)
            
            # 保存数据
            with open(self.data_file, "w", encoding="utf-8") as f:
                json.dump(existing_data, f, ensure_ascii=False, indent=2)
            
            print(f"数据已保存到: {self.data_file}")
            return True
            
        except Exception as e:
            print(f"保存数据失败: {e}")
            return False
    
    def collect_daily_data(self, interval_hours=1, days=1):
        """收集多日数据"""
        total_points = days * 24 // interval_hours
        collected_data = []
        
        print(f"开始收集 {self.city} 的天气数据")
        print(f"采集间隔: {interval_hours}小时,持续天数: {days}天,总采集点: {total_points}")
        print("-" * 50)
        
        for i in range(total_points):
            current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            print(f"[{current_time}] 第 {i+1}/{total_points} 次采集")
            
            # 获取数据
            weather_data = self.fetch_current_weather()
            
            if weather_data:
                collected_data.append(weather_data)
                self.save_weather_data(weather_data)
                
                # 显示最新数据
                latest = collected_data[-1]
                print(f"  温度: {latest['temperature']}°C | 湿度: {latest['humidity']}%")
                print(f"  天气: {latest['weather']} | 风力: {latest['wind_power']}")
            
            # 如果不是最后一次,等待指定时间
            if i < total_points - 1:
                print(f"  等待 {interval_hours} 小时...")
                time.sleep(interval_hours * 3600)
        
        print("-" * 50)
        print(f"数据收集完成!共收集 {len(collected_data)} 条记录")
        
        # 转换为DataFrame方便分析
        df = pd.DataFrame(collected_data)
        csv_file = self.data_file.replace(".json", ".csv")
        df.to_csv(csv_file, index=False, encoding="utf-8-sig")
        print(f"数据已导出为CSV: {csv_file}")
        
        return df

# 使用示例
if __name__ == "__main__":
    # 创建爬虫实例
    crawler = WeatherDataCrawler(city="北京")
    
    # 测试单次采集
    print("=== 测试单次采集 ===")
    test_data = crawler.fetch_current_weather()
    if test_data:
        print("测试成功!")
        print(f"城市: {test_data['city']}")
        print(f"温度: {test_data['temperature']}°C")
        print(f"天气: {test_data['weather']}")
        print(f"湿度: {test_data['humidity']}%")
        print(f"风力: {test_data['wind_power']}")
    
    # 开始定时采集(实际使用时取消注释)
    # print("\n=== 开始定时采集 ===")
    # df = crawler.collect_daily_data(interval_hours=2, days=3)
    # print(f"共采集 {len(df)} 条天气数据")

这个爬虫做了几件重要的事:

  1. 数据完整性:不仅获取温度湿度,还包括日出日落、空气质量、生活提示等丰富信息
  2. 错误处理:网络异常、JSON解析错误、API返回错误都有相应处理
  3. 数据持久化:自动保存为JSON和CSV格式,方便后续处理
  4. 定时采集:支持按小时间隔自动采集,构建时间序列数据

3.2 股票数据爬虫:获取实时金融数据

如果你对金融可视化感兴趣,股票数据是绝佳的动画素材。下面是一个简单的股票数据爬虫:

import requests
import pandas as pd
import time
import re
from datetime import datetime, timedelta

class StockDataCrawler:
    """股票数据爬虫类"""
    
    def __init__(self, symbol="sh000001"):  # 默认上证指数
        self.symbol = symbol
        self.base_url = "http://hq.sinajs.cn/list="
        self.data_file = f"data/raw/stock_{symbol}_{datetime.now().strftime('%Y%m%d')}.csv"
        
        # 股票代码映射
        self.stock_names = {
            "sh000001": "上证指数",
            "sz399001": "深证成指",
            "sz399006": "创业板指",
            "sh000300": "沪深300",
            "sh000016": "上证50",
            "sz399005": "中小板指"
        }
    
    def parse_sina_data(self, data_str):
        """解析新浪财经数据格式"""
        # 格式: var hq_str_sh000001="上证指数,3278.1234,3280.5678,...";
        if not data_str or "=" not in data_str:
            return None
        
        try:
            # 提取数据部分
            content = data_str.split("=")[1].strip('";')
            items = content.split(",")
            
            if len(items) < 30:
                return None
            
            # 解析各个字段
            stock_data = {
                "timestamp": datetime.now(),
                "name": items[0],  # 股票名称
                "open": float(items[1]),  # 今日开盘价
                "close": float(items[2]),  # 昨日收盘价
                "price": float(items[3]),  # 当前价格
                "high": float(items[4]),  # 今日最高价
                "low": float(items[5]),  # 今日最低价
                "bid": float(items[6]),  # 竞买价
                "ask": float(items[7]),  # 竞卖价
                "volume": float(items[8]),  # 成交量(手)
                "amount": float(items[9]),  # 成交额(万)
                "buy1_volume": int(items[10]),  # 买一量
                "buy1_price": float(items[11]),  # 买一价
                "buy2_volume": int(items[12]),
                "buy2_price": float(items[13]),
                "buy3_volume": int(items[14]),
                "buy3_price": float(items[15]),
                "buy4_volume": int(items[16]),
                "buy4_price": float(items[17]),
                "buy5_volume": int(items[18]),
                "buy5_price": float(items[19]),
                "sell1_volume": int(items[20]),  # 卖一量
                "sell1_price": float(items[21]),  # 卖一价
                "sell2_volume": int(items[22]),
                "sell2_price": float(items[23]),
                "sell3_volume": int(items[24]),
                "sell3_price": float(items[25]),
                "sell4_volume": int(items[26]),
                "sell4_price": float(items[27]),
                "sell5_volume": int(items[28]),
                "sell5_price": float(items[29])
            }
            
            # 计算衍生指标
            stock_data["change"] = stock_data["price"] - stock_data["close"]  # 涨跌
            stock_data["change_percent"] = (stock_data["change"] / stock_data["close"]) * 100  # 涨跌幅
            
            return stock_data
            
        except Exception as e:
            print(f"解析股票数据失败: {e}")
            return None
    
    def fetch_realtime_data(self):
        """获取实时股票数据"""
        url = f"{self.base_url}{self.symbol}"
        
        headers = {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
            "Referer": "http://finance.sina.com.cn",
            "Accept": "*/*"
        }
        
        try:
            response = requests.get(url, headers=headers, timeout=10)
            response.encoding = "gbk"  # 新浪使用GBK编码
            
            if response.status_code == 200:
                stock_data = self.parse_sina_data(response.text)
                if stock_data:
                    stock_name = self.stock_names.get(self.symbol, stock_data["name"])
                    print(f"{stock_name}: {stock_data['price']:.2f} ({stock_data['change']:+.2f}, {stock_data['change_percent']:+.2f}%)")
                    return stock_data
            else:
                print(f"请求失败,状态码: {response.status_code}")
                
        except requests.exceptions.RequestException as e:
            print(f"网络请求失败: {e}")
        except Exception as e:
            print(f"获取数据失败: {e}")
        
        return None
    
    def collect_intraday_data(self, interval_seconds=30, duration_minutes=60):
        """收集日内交易数据"""
        total_points = (duration_minutes * 60) // interval_seconds
        all_data = []
        
        stock_name = self.stock_names.get(self.symbol, self.symbol)
        print(f"开始收集 {stock_name} 的日内数据")
        print(f"采集间隔: {interval_seconds}秒,持续时间: {duration_minutes}分钟,总采集点: {total_points}")
        print("-" * 60)
        
        start_time = datetime.now()
        end_time = start_time + timedelta(minutes=duration_minutes)
        
        point_count = 0
        while datetime.now() < end_time and point_count < total_points:
            point_count += 1
            current_time = datetime.now().strftime("%H:%M:%S")
            
            print(f"[{current_time}] 第 {point_count}/{total_points} 次采集")
            
            # 获取数据
            stock_data = self.fetch_realtime_data()
            
            if stock_data:
                all_data.append(stock_data)
                
                # 显示最新数据
                latest = all_data[-1]
                print(f"  价格: {latest['price']:.2f} | 涨跌: {latest['change']:+.2f}")
                print(f"  成交量: {latest['volume']/10000:.1f}万手 | 成交额: {latest['amount']:.0f}万")
            
            # 计算等待时间
            if point_count < total_points:
                wait_time = interval_seconds
                print(f"  等待 {wait_time} 秒...")
                time.sleep(wait_time)
        
        print("-" * 60)
        print(f"数据收集完成!共收集 {len(all_data)} 条记录")
        
        # 保存数据
        if all_data:
            df = pd.DataFrame(all_data)
            df.to_csv(self.data_file, index=False, encoding="utf-8-sig")
            print(f"数据已保存到: {self.data_file}")
            
            # 生成简单统计
            self._generate_stats(df)
            
            return df
        
        return None
    
    def _generate_stats(self, df):
        """生成数据统计"""
        if len(df) == 0:
            return
        
        print("\n数据统计:")
        print(f"时间范围: {df['timestamp'].min().strftime('%H:%M:%S')} - {df['timestamp'].max().strftime('%H:%M:%S')}")
        print(f"价格区间: {df['price'].min():.2f} - {df['price'].max():.2f}")
        print(f"平均价格: {df['price'].mean():.2f}")
        print(f"最大涨幅: {df['change_percent'].max():.2f}%")
        print(f"最大跌幅: {df['change_percent'].min():.2f}%")
        print(f"总成交量: {df['volume'].sum()/10000:.1f}万手")
        print(f"总成交额: {df['amount'].sum():.0f}万")

# 使用示例
if __name__ == "__main__":
    # 创建爬虫实例
    crawler = StockDataCrawler(symbol="sh000001")  # 上证指数
    
    # 测试单次采集
    print("=== 测试单次采集 ===")
    test_data = crawler.fetch_realtime_data()
    if test_data:
        print("测试成功!")
        print(f"股票: {test_data['name']}")
        print(f"当前价: {test_data['price']:.2f}")
        print(f"涨跌: {test_data['change']:+.2f}")
        print(f"涨跌幅: {test_data['change_percent']:+.2f}%")
    
    # 开始日内数据收集(实际使用时取消注释)
    # print("\n=== 开始日内数据收集 ===")
    # df = crawler.collect_intraday_data(interval_seconds=60, duration_minutes=10)
    # if df is not None:
    #     print(f"共采集 {len(df)} 条股票数据")

这个股票爬虫的特点:

  1. 实时性:可以按秒级频率采集数据,捕捉市场细微波动
  2. 数据丰富:不仅价格,还包括买卖盘口、成交量等深度信息
  3. 自动统计:采集完成后自动生成基本统计信息
  4. 错误恢复:网络中断后可以继续采集

3.3 数据清洗与预处理

原始数据往往有各种问题,我们需要先清洗再使用:

import pandas as pd
import numpy as np
from datetime import datetime

class DataCleaner:
    """数据清洗器"""
    
    def __init__(self):
        self.cleaning_log = []
    
    def clean_weather_data(self, df):
        """清洗天气数据"""
        if df is None or len(df) == 0:
            return df
        
        original_count = len(df)
        self.cleaning_log.append(f"原始数据: {original_count} 条")
        
        # 1. 处理缺失值
        df_cleaned = df.copy()
        
        # 数值型字段用前向填充
        numeric_cols = ['temperature', 'humidity', 'high_temp', 'low_temp', 'aqi']
        for col in numeric_cols:
            if col in df_cleaned.columns:
                missing_before = df_cleaned[col].isnull().sum()
                df_cleaned[col] = df_cleaned[col].fillna(method='ffill').fillna(method='bfill')
                missing_after = df_cleaned[col].isnull().sum()
                if missing_before > 0:
                    self.cleaning_log.append(f"  列 '{col}': 填充了 {missing_before - missing_after} 个缺失值")
        
        # 文本型字段用众数填充
        text_cols = ['weather', 'wind_direction', 'wind_power']
        for col in text_cols:
            if col in df_cleaned.columns:
                missing_before = df_cleaned[col].isnull().sum()
                mode_value = df_cleaned[col].mode()[0] if not df_cleaned[col].mode().empty else "未知"
                df_cleaned[col] = df_cleaned[col].fillna(mode_value)
                missing_after = df_cleaned[col].isnull().sum()
                if missing_before > 0:
                    self.cleaning_log.append(f"  列 '{col}': 用 '{mode_value}' 填充了 {missing_before - missing_after} 个缺失值")
        
        # 2. 处理异常值(使用IQR方法)
        numeric_cols_for_outlier = ['temperature', 'humidity']
        for col in numeric_cols_for_outlier:
            if col in df_cleaned.columns:
                Q1 = df_cleaned[col].quantile(0.25)
                Q3 = df_cleaned[col].quantile(0.75)
                IQR = Q3 - Q1
                lower_bound = Q1 - 1.5 * IQR
                upper_bound = Q3 + 1.5 * IQR
                
                outliers = df_cleaned[(df_cleaned[col] < lower_bound) | (df_cleaned[col] > upper_bound)]
                if len(outliers) > 0:
                    # 用边界值替换异常值
                    df_cleaned[col] = df_cleaned[col].clip(lower_bound, upper_bound)
                    self.cleaning_log.append(f"  列 '{col}': 修正了 {len(outliers)} 个异常值")
        
        # 3. 数据标准化(可选)
        if 'temperature' in df_cleaned.columns:
            # 将温度标准化到0-1范围,方便后续映射
            temp_min = df_cleaned['temperature'].min()
            temp_max = df_cleaned['temperature'].max()
            if temp_max > temp_min:
                df_cleaned['temperature_normalized'] = (df_cleaned['temperature'] - temp_min) / (temp_max - temp_min)
                self.cleaning_log.append(f"  温度标准化: {temp_min}°C ~ {temp_max}°C → 0 ~ 1")
        
        if 'humidity' in df_cleaned.columns:
            # 湿度已经是百分比,直接除以100
            df_cleaned['humidity_normalized'] = df_cleaned['humidity'] / 100.0
        
        # 4. 时间处理
        if 'timestamp' in df_cleaned.columns:
            # 确保时间列是datetime类型
            df_cleaned['timestamp'] = pd.to_datetime(df_cleaned['timestamp'])
            
            # 提取时间特征
            df_cleaned['hour'] = df_cleaned['timestamp'].dt.hour
            df_cleaned['day_part'] = df_cleaned['hour'].apply(self._categorize_day_part)
            
            self.cleaning_log.append(f"  时间特征提取完成")
        
        cleaned_count = len(df_cleaned)
        self.cleaning_log.append(f"清洗后数据: {cleaned_count} 条")
        self.cleaning_log.append(f"数据保留率: {cleaned_count/original_count*100:.1f}%")
        
        return df_cleaned
    
    def _categorize_day_part(self, hour):
        """将小时分类为时间段"""
        if 5 <= hour < 10:
            return "早晨"
        elif 10 <= hour < 14:
            return "中午"
        elif 14 <= hour < 18:
            return "下午"
        elif 18 <= hour < 22:
            return "晚上"
        else:
            return "深夜"
    
    def clean_stock_data(self, df):
        """清洗股票数据"""
        if df is None or len(df) == 0:
            return df
        
        original_count = len(df)
        self.cleaning_log.append(f"原始股票数据: {original_count} 条")
        
        df_cleaned = df.copy()
        
        # 1. 处理缺失值
        numeric_cols = ['price', 'volume', 'amount', 'change', 'change_percent']
        for col in numeric_cols:
            if col in df_cleaned.columns:
                missing_before = df_cleaned[col].isnull().sum()
                # 股票数据用线性插值,因为连续性较强
                df_cleaned[col] = df_cleaned[col].interpolate(method='linear')
                missing_after = df_cleaned[col].isnull().sum()
                if missing_before > 0:
                    self.cleaning_log.append(f"  列 '{col}': 插值填充了 {missing_before - missing_after} 个缺失值")
        
        # 2. 处理异常值(股票数据波动大,使用更宽松的标准)
        if 'change_percent' in df_cleaned.columns:
            # 涨跌幅超过20%视为异常(极端情况)
            extreme_changes = df_cleaned[abs(df_cleaned['change_percent']) > 20]
            if len(extreme_changes) > 0:
                # 用前后平均值替换
                for idx in extreme_changes.index:
                    if idx > 0 and idx < len(df_cleaned) - 1:
                        prev_val = df_cleaned.loc[idx-1, 'change_percent']
                        next_val = df_cleaned.loc[idx+1, 'change_percent']
                        df_cleaned.loc[idx, 'change_percent'] = (prev_val + next_val) / 2
                
                self.cleaning_log.append(f"  修正了 {len(extreme_changes)} 个极端涨跌幅")
        
        # 3. 计算技术指标
        if 'price' in df_cleaned.columns:
            # 简单移动平均
            df_cleaned['price_ma5'] = df_cleaned['price'].rolling(window=5, min_periods=1).mean()
            df_cleaned['price_ma10'] = df_cleaned['price'].rolling(window=10, min_periods=1).mean()
            
            # 价格变化率
            df_cleaned['price_change'] = df_cleaned['price'].pct_change() * 100
            
            # 成交量变化
            if 'volume' in df_cleaned.columns:
                df_cleaned['volume_change'] = df_cleaned['volume'].pct_change() * 100
            
            self.cleaning_log.append(f"  技术指标计算完成")
        
        # 4. 标准化
        if 'price' in df_cleaned.columns:
            price_min = df_cleaned['price'].min()
            price_max = df_cleaned['price'].max()
            if price_max > price_min:
                df_cleaned['price_normalized'] = (df_cleaned['price'] - price_min) / (price_max - price_min)
                self.cleaning_log.append(f"  价格标准化: {price_min:.2f} ~ {price_max:.2f} → 0 ~ 1")
        
        if 'volume' in df_cleaned.columns:
            volume_min = df_cleaned['volume'].min()
            volume_max = df_cleaned['volume'].max()
            if volume_max > volume_min:
                df_cleaned['volume_normalized'] = (df_cleaned['volume'] - volume_min) / (volume_max - volume_min)
        
        cleaned_count = len(df_cleaned)
        self.cleaning_log.append(f"清洗后股票数据: {cleaned_count} 条")
        
        return df_cleaned
    
    def print_cleaning_log(self):
        """打印清洗日志"""
        print("数据清洗报告:")
        print("=" * 50)
        for log in self.cleaning_log:
            print(log)
        print("=" * 50)

# 使用示例
if __name__ == "__main__":
    # 模拟一些天气数据
    test_weather_data = {
        'timestamp': pd.date_range('2024-01-01', periods=10, freq='H'),
        'temperature': [15, 16, None, 18, 19, 100, 21, 22, 23, 24],  # 包含缺失值和异常值
        'humidity': [60, 65, 70, None, 75, 80, 85, 90, 95, 100],
        'weather': ['晴', '晴', '多云', '阴', '雨', '雨', '雪', '雪', None, '雾']
    }
    
    df_weather = pd.DataFrame(test_weather_data)
    
    print("原始天气数据:")
    print(df_weather)
    print("\n" + "="*50 + "\n")
    
    # 清洗数据
    cleaner = DataCleaner()
    df_cleaned = cleaner.clean_weather_data(df_weather)
    
    print("清洗后的天气数据:")
    print(df_cleaned[['timestamp', 'temperature', 'humidity', 'weather', 'temperature_normalized']])
    print("\n")
    
    cleaner.print_cleaning_log()

数据清洗的关键点:

  1. 缺失值处理:根据数据类型选择填充方法(前向填充、众数填充、插值)
  2. 异常值检测:使用统计方法识别和处理异常数据
  3. 数据标准化:将不同尺度的数据映射到统一范围
  4. 特征工程:从原始数据中提取更有用的特征

4. 数据到动画:参数映射的艺术

这是整个项目的核心创意部分——如何把枯燥的数字变成生动的动画参数。不同的数据需要不同的映射策略,我分享几个实用的映射方法。

4.1 天气数据的动画映射

天气数据有很强的视觉对应关系,温度对应色调,湿度对应雾气,风力对应运动强度:

class WeatherAnimationMapper:
    """天气数据到动画参数的映射器"""
    
    def __init__(self):
        # 颜色映射:温度 -> 色温
        self.temperature_colors = {
            "arctic": {"rgb": [200, 230, 255], "temperature": 9000, "mood": "冰冷"},
            "cold": {"rgb": [180, 220, 255], "temperature": 7000, "mood": "寒冷"},
            "cool": {"rgb": [220, 240, 255], "temperature": 5000, "mood": "凉爽"},
            "neutral": {"rgb": [255, 255, 255], "temperature": 4000, "mood": "舒适"},
            "warm": {"rgb": [255, 240, 200], "temperature": 3000, "mood": "温暖"},
            "hot": {"rgb": [255, 220, 180], "temperature": 2000, "mood": "炎热"},
            "tropical": {"rgb": [255, 200, 150], "temperature": 1000, "mood": "酷热"}
        }
        
        # 天气类型映射
        self.weather_styles = {
            "晴": {
                "lighting": "bright_sunny",
                "clouds": "few_clouds",
                "atmosphere": "clear",
                "special_effects": ["sun_flares", "light_rays"]
            },
            "多云": {
                "lighting": "soft_diffuse",
                "clouds": "scattered_clouds",
                "atmosphere": "hazy",
                "special_effects": ["cloud_shadows"]
            },
            "阴": {
                "lighting": "flat_overcast",
                "clouds": "overcast",
                "atmosphere": "gray",
                "special_effects": ["foggy"]
            },
            "雨": {
                "lighting": "dark_rainy",
                "clouds": "rain_clouds",
                "atmosphere": "wet",
                "special_effects": ["rain_drops", "puddles", "wet_surfaces"]
            },
            "雪": {
                "lighting": "soft_snowy",
                "clouds": "snow_clouds",
                "atmosphere": "white",
                "special_effects": ["snow_flakes", "frost", "snow_covered"]
            },
            "雾": {
                "lighting": "muted_foggy",
                "clouds": "fog",
                "atmosphere": "misty",
                "special_effects": ["depth_fog", "atmospheric_perspective"]
            }
        }
        
        # 风力映射
        self.wind_effects = {
            "微风": {"intensity": 0.2, "particles": 50, "movement": "gentle_breeze"},
            "1级": {"intensity": 0.3, "particles": 100, "movement": "light_breeze"},
            "2-3级": {"intensity": 0.5, "particles": 200, "movement": "moderate_breeze"},
            "4-5级": {"intensity": 0.8, "particles": 400, "movement": "fresh_breeze"},
            "6-7级": {"intensity": 1.2, "particles": 800, "movement": "strong_wind"},
            "8级以上": {"intensity": 2.0, "particles": 1500, "movement": "storm_wind"}
        }
    
    def map_temperature(self, temp_celsius):
        """映射温度到视觉参数"""
        if temp_celsius < -10:
            color_profile = self.temperature_colors["arctic"]
            animation_params = {
                "color_preset": "arctic_blue",
                "brightness": 0.7,
                "contrast": 1.3,
                "saturation": 0.8,
                "hue_shift": -0.1  # 偏蓝
            }
        elif temp_celsius < 0:
            color_profile = self.temperature_colors["cold"]
            animation_params = {
                "color_preset": "cold_blue",
                "brightness": 0.8,
                "contrast": 1.2,
                "saturation": 0.9,
                "hue_shift": -0.05
            }
        elif temp_celsius < 10:
            color_profile = self.temperature_colors["cool"]
            animation_params = {
                "color_preset": "cool_white",
                "brightness": 0.9,
                "contrast": 1.1,
                "saturation": 1.0,
                "hue_shift": 0.0
            }
        elif temp_celsius < 20:
            color_profile = self.temperature_colors["neutral"]
            animation_params = {
                "color_preset": "neutral",
                "brightness": 1.0,
                "contrast": 1.0,
                "saturation": 1.0,
                "hue_shift": 0.0
            }
        elif temp_celsius < 30:
            color_profile = self.temperature_colors["warm"]
            animation_params = {
                "color_preset": "warm_yellow",
                "brightness": 1.1,
                "contrast": 1.1,
                "saturation": 1.2,
                "hue_shift": 0.05  # 偏黄
            }
        elif temp_celsius < 40:
            color_profile = self.temperature_colors["hot"]
            animation_params = {
                "color_preset": "hot_orange",
                "brightness": 1.2,
                "contrast": 1.2,
                "saturation": 1.3,
                "hue_shift": 0.1  # 偏橙
            }
        else:
            color_profile = self.temperature_colors["tropical"]
            animation_params = {
                "color_preset": "tropical_red",
                "brightness": 1.3,
                "contrast": 1.3,
                "saturation": 1.4,
                "hue_shift": 0.15  # 偏红
            }
        
        # 添加温度相关的动画效果
        if temp_celsius > 30:
            animation_params["heat_waves"] = True
            animation_params["heat_distortion"] = min((temp_celsius - 30) / 10, 1.0)
        elif temp_celsius < 0:
            animation_params["frost_effect"] = True
            animation_params["frost_intensity"] = min(abs(temp_celsius) / 10, 1.0)
        
        return {
            "visual_params": animation_params,
            "color_profile": color_profile,
            "description": f"{color_profile['mood']} ({temp_celsius}°C)"
        }
    
    def map_humidity(self, humidity_percent):
        """映射湿度到视觉参数"""
        humidity = float(humidity_percent)
        
        if humidity < 30:
            # 干燥
            return {
                "fog_density": 0.1,
                "atmosphere_density": 0.2,
                "transparency": 0.9,
                "haze": 0.1,
                "particle_type": "dust",
                "particle_count": int(humidity * 2)
            }
        elif humidity < 60:
            # 舒适
            return {
                "fog_density": 0.3,
                "atmosphere_density": 0.4,
                "transparency": 0.8,
                "haze": 0.3,
                "particle_type": "light_mist",
                "particle_count": int(humidity * 3)
            }
        elif humidity < 80:
            # 潮湿
            return {
                "fog_density": 0.6,
                "atmosphere_density": 0.7,
                "transparency": 0.6,
                "haze": 0.6,
                "particle_type": "mist",
                "particle_count": int(humidity * 4)
            }
        else:
            # 非常潮湿
            return {
                "fog_density": 0.9,
                "atmosphere_density": 0.9,
                "transparency": 0.3,
                "haze": 0.9,
                "particle_type": "dense_fog",
                "particle_count": int(humidity * 5)
            }
    
    def map_weather_type(self, weather_type):
        """映射天气类型到场景参数"""
        weather_lower = str(weather_type).lower()
        
        # 查找匹配的天气类型
        for key, style in self.weather_styles.items():
            if key in weather_lower:
                return style
        
        # 默认返回晴天
        return self.weather_styles["晴"]
    
    def map_wind(self, wind_description):
        """映射风力到运动参数"""
        wind_lower = str(wind_description).lower()
        
        # 查找匹配的风力描述
        for key, effect in self.wind_effects.items():
            if key in wind_lower:
                return effect
        
        # 默认返回微风
        return self.wind_effects["微风"]
    
    def map_time_of_day(self, hour):
        """映射时间到光照参数"""
        if 5 <= hour < 8:
            # 清晨
            return {
                "lighting_preset": "sunrise",
                "light_intensity": 0.6,
                "light_color": [255, 200, 150],  # 暖橙色
                "shadow_length": 2.0,
                "shadow_softness": 0.8
            }
        elif 8 <= hour < 11:
            # 上午
            return {
                "lighting_preset": "morning",
                "light_intensity": 0.9,
                "light_color": [255, 240, 220],  # 暖白色
                "shadow_length": 1.0,
                "shadow_softness": 0.6
            }
        elif 11 <= hour < 14:
            # 中午
            return {
                "lighting_preset": "midday",
                "light_intensity": 1.2,
                "light_color": [255, 255, 255],  # 纯白色
                "shadow_length": 0.5,
                "shadow_softness": 0.4
            }
        elif 14 <= hour < 17:
            # 下午
            return {
                "lighting_preset": "afternoon",
                "light_intensity": 0.9,
                "light_color": [255, 230, 200],  # 暖黄色
                "shadow_length": 1.5,
                "shadow_softness": 0.7
            }
        elif 17 <= hour < 19:
            # 黄昏
            return {
                "lighting_preset": "sunset",
                "light_intensity": 0.7,
                "light_color": [255, 150, 100],  # 橙红色
                "shadow_length": 2.5,
                "shadow_softness": 0.9
            }
        else:
            # 夜晚
            return {
                "lighting_preset": "night",
                "light_intensity": 0.3,
                "light_color": [100, 150, 255],  # 冷蓝色
                "shadow_length": 3.0,
                "shadow_softness": 1.0,
                "moonlight": True,
                "stars": True
            }
    
    def create_animation_parameters(self, weather_data):
        """根据天气数据创建完整的动画参数"""
        # 基础参数
        params = {
            "metadata": {
                "timestamp": weather_data.get("timestamp", ""),
                "location": weather_data.get("city", "未知"),
                "data_source": "weather"
            },
            "camera": {
                "movement_speed": 1.0,
                "movement_type": "smooth_pan",
                "fov": 60
            },
            "scene": {
                "base_style": "realistic",
                "quality": "high",
                "resolution": "1024x576"
            },
            "animation": {
                "total_frames": 16,
                "fps": 8,
                "loop": True
            }
        }
        
        # 映射温度
        if "temperature" in weather_data:
            temp_params = self.map_temperature(weather_data["temperature"])
            params["color_grading"] = temp_params["visual_params"]
            params["metadata"]["temperature_description"] = temp_params["description"]
        
        # 映射湿度
        if "humidity" in weather_data:
            humidity_params = self.map_humidity(weather_data["humidity"])
            params["atmosphere"] = humidity_params
        
        # 映射天气类型
        if "weather" in weather_data:
            weather_params = self.map_weather_type(weather_data["weather"])
            params["weather_effects"] = weather_params
            params["metadata"]["weather_type"] = weather_data["weather"]
        
        # 映射风力
        if "wind_power" in weather_data:
            wind_params = self.map_wind(weather_data["wind_power"])
            params["wind_effects"] = wind_params
        
        # 映射时间
        if "hour" in weather_data:
            time_params = self.map_time_of_day(weather_data["hour"])
            params["lighting"] = time_params
        elif "timestamp" in weather_data:
            # 从时间戳提取小时
            try:
                from datetime import datetime
                ts = weather_data["timestamp"]
                if isinstance(ts, str):
                    hour = datetime.fromisoformat(ts).hour
                else:
                    hour = ts.hour
                time_params = self.map_time_of_day(hour)
                params["lighting"] = time_params
            except:
                pass
        
        # 生成提示词
        params["prompt"] = self.generate_prompt(weather_data)
        params["negative_prompt"] = "blurry, distorted, low quality, ugly, deformed, bad anatomy"
        
        return params
    
    def generate_prompt(self, weather_data):
        """根据天气数据生成提示词"""
        parts = []
        
        # 时间描述
        if "hour" in weather_data:
            hour = weather_data["hour"]
            if 5 <= hour < 8:
                parts.append("early morning")
            elif 8 <= hour < 11:
                parts.append("morning")
            elif 11 <= hour < 14:
                parts.append("midday")
            elif 14 <= hour < 17:
                parts.append("afternoon")
            elif 17 <= hour < 19:
                parts.append("sunset, golden hour")
            else:
                parts.append("night time")
        
        # 天气描述
        if "weather" in weather_data:
            weather = weather_data["weather"]
            if "晴" in weather:
                parts.append("sunny day")
            elif "多云" in weather:
                parts.append("cloudy day")
            elif "阴" in weather:
                parts.append("overcast day")
            elif "雨" in weather:
                parts.append("rainy day")
            elif "雪" in weather:
                parts.append("snowy day")
            elif "雾" in weather:
                parts.append("foggy day")
        
        # 温度描述
        if "temperature" in weather_data:
            temp = weather_data["temperature"]
            if temp > 30:
                parts.append("hot weather")
            elif temp > 20:
                parts.append("warm weather")
            elif temp > 10:
                parts.append("cool weather")
            elif temp > 0:
                parts.append("cold weather")
            else:
                parts.append("freezing weather")
        
        # 场景描述
        if "city" in weather_data:
            city = weather_data["city"]
            parts.append(f"{city} cityscape")
        else:
            parts.append("urban cityscape")
        
        # 质量描述
        parts.append("cinematic view")
        parts.append("high quality")
        parts.append("8k resolution")
        parts.append("photorealistic")
        parts.append("detailed")
        
        # 风格描述
        parts.append("film grain")
        parts.append("cinematic lighting")
        parts.append("depth of field")
        
        return ", ".join(parts)

# 使用示例
if __name__ == "__main__":
    # 创建映射器
    mapper = WeatherAnimationMapper()
    
    # 测试不同天气条件的映射
    test_cases = [
        {"temperature": 35, "humidity": 20, "weather": "晴", "wind_power": "微风", "hour": 14},
        {"temperature": 15, "humidity": 60, "weather": "多云", "wind_power": "2-3级", "hour": 9},
        {"temperature": 5, "humidity": 85, "weather": "雨", "wind_power": "4-5级", "hour": 18},
        {"temperature": -5, "humidity": 90, "weather": "雪", "wind_power": "1级", "hour": 22}
    ]
    
    print("天气数据到动画参数映射测试:")
    print("=" * 80)
    
    for i, weather in enumerate(test_cases, 1):
        print(f"\n测试案例 {i}:")
        print(f"  天气条件: {weather['temperature']}°C, {weather['humidity']}%湿度, {weather['weather']}, {weather['wind_power']}, {weather['hour']}:00")
        
        params = mapper.create_animation_parameters(weather)
        
        print(f"  生成的提示词: {params['prompt'][:80]}...")
        print(f"  颜色预设: {params['color_grading']['color_preset']}")
        print(f"  天气效果: {params['weather_effects']['lighting']}")
        print(f"  风力强度: {params['wind_effects']['intensity']}")
        print(f"  光照预设: {params['lighting']['lighting_preset']}")

这个映射器的核心思想是:把数据特征转化为视觉特征。温度决定色调冷暖,湿度决定雾气浓度,天气类型决定场景风格,风力决定运动强度,时间决定光照角度。

4.2 股票数据的动画映射

股票数据的波动性更强,适合做更动态、更有冲击力的映射:

class StockAnimationMapper:
    """股票数据到动画参数的映射器"""
    
    def __init__(self):
        # 价格变化映射
        self.price_change_mapping = {
            "extreme_up": {"threshold": 5.0, "color": [0, 255, 0], "intensity": 2.0, "mood": "狂热上涨"},
            "strong_up": {"threshold": 2.0, "color": [100, 255, 100], "intensity": 1.5, "mood": "强势上涨"},
            "moderate_up": {"threshold": 0.5, "color": [150, 255, 150], "intensity": 1.0, "mood": "温和上涨"},
            "stable": {"threshold": 0.0, "color": [200, 200, 200], "intensity": 0.5, "mood": "横盘整理"},
            "moderate_down": {"threshold": -0.5, "color": [255, 150, 150], "intensity": 1.0, "mood": "温和下跌"},
            "strong_down": {"threshold": -2.0, "color": [255, 100, 100], "intensity": 1.5, "mood": "强势下跌"},
            "extreme_down": {"threshold": -5.0, "color": [255, 0, 0], "intensity": 2.0, "mood": "恐慌下跌"}
        }
        
        # 成交量映射
        self.volume_mapping = {
            "extremely_low": {"ratio": 0.3, "particles": 100, "size": 0.5, "speed": 0.3},
            "low": {"ratio": 0.6, "particles": 300, "size": 0.8, "speed": 0.5},
            "normal": {"ratio": 1.0, "particles": 500, "size": 1.0, "speed": 1.0},
            "high": {"ratio": 2.0, "particles": 1000, "size": 1.5, "speed": 1.5},
            "extremely_high": {"ratio": 5.0, "particles": 2000, "size": 2.0, "speed": 2.0}
        }
        
        # 市场情绪映射
        self.market_sentiment = {
            "bullish": {
                "camera_movement": "rising",
                "particle_direction": "upward",
                "lighting": "bright_optimistic",
                "music_tempo": "fast_upbeat"
            },
            "bearish": {
                "camera_movement": "falling",
                "particle_direction": "downward",
                "lighting": "dark_gloomy",
                "music_tempo": "slow_dark"
            },
            "neutral": {
                "camera_movement": "floating",
                "particle_direction": "swirling",
                "lighting": "neutral_calm",
                "music_tempo": "medium_steady"
            }
        }
    
    def map_price_change(self, change_percent, current_price, avg_price):
        """映射价格变化到视觉参数"""
        # 确定变化级别
        change_level = "stable"
        for level, config in self.price_change_mapping.items():
            if change_percent >= config["threshold"]:
                change_level = level
                break
        
        config = self.price_change_mapping[change_level]
        
        # 计算相对强度
        price_ratio = current_price / avg_price if avg_price > 0 else 1.0
        intensity_multiplier = min(abs(price_ratio - 1.0) * 2 + 1.0, 3.0)
        
        # 构建视觉参数
        visual_params = {
            "color_scheme": {
                "primary_color": config["color"],
                "secondary_color": self._adjust_color(config["color"], -30),
                "intensity": config["intensity"] * intensity_multiplier,
                "bloom_effect": min(config["intensity"] * 0.5, 1.0)
            },
            "movement": {
                "direction": "up" if change_percent > 0 else "down",
                "speed": min(abs(change_percent) * 0.1, 2.0),
                "amplitude": min(abs(change_percent) * 0.05, 1.0),
                "frequency": min(abs(change_percent) * 0.2, 3.0)
            },
            "particles": {
                "enabled": True,
                "count": int(abs(change_percent) * 50),
                "size": min(abs(change_percent) * 0.1, 2.0),
                "lifetime": min(abs(change_percent) * 0.5, 5.0)
            }
        }
        
        # 添加特殊效果
        if abs(change_percent) > 3.0:
            visual_params["special_effects"] = ["energy_pulses", "light_trails"]
        if abs(change_percent) > 7.0:
            visual_params["special_effects"].append("shockwaves")
        
        return {
            "visual_params": visual_params,
            "change_level": change_level,
            "mood": config["mood"],
            "intensity": config["intensity"] * intensity_multiplier
        }
    
    def map_volume(self, current_volume, avg_volume):
        """映射成交量到视觉参数"""
        if avg_volume == 0:
            volume_ratio = 1.0
        else:
            volume_ratio = current_volume / avg_volume
        
        # 确定成交量级别
        volume_level = "normal"
        for level, config in self.volume_mapping.items():
            if volume_ratio <= config["ratio"]:
                volume_level = level
                break
        
        config = self.volume_mapping[volume_level]
        
        # 调整参数基于实际比例
        adjusted_particles = int(config["particles"] * min(volume_ratio, 3.0))
        adjusted_size = config["size"] * min(volume_ratio, 2.0)
        adjusted_speed = config["speed"] * min(volume_ratio, 2.0)
        
        return {
            "particle_system": {
                "count": adjusted_particles,
                "size": adjusted_size,
                "speed": adjusted_speed,
                "density": min(volume_ratio, 2.0),
                "chaos": min(volume_ratio * 0.5, 1.0)
            },
            "volume_level": volume_level,
            "volume_ratio": volume_ratio
        }
    
    def map_market_sentiment(self, price_trend, volume_trend, volatility):
        """映射市场情绪到场景参数"""
        # 简单情绪判断逻辑
        if price_trend > 0.1 and volume_trend > 0:
            sentiment = "bullish"
        elif price_trend < -0.1 and volume_trend > 0:
            sentiment = "bearish"
        else:
            sentiment = "neutral"
        
        config = self.market_sentiment[sentiment]
        
        # 根据波动性调整强度
        volatility_factor = min(volatility * 2, 2.0)
        
        scene_params = {
            "camera": {
                "movement_type": config["camera_movement"],
                "movement_speed": 1.0 * volatility_factor,
                "shake_intensity": min(volatility * 0.5, 1.0)
            },
            "particles": {
                "direction": config["particle_direction"],
                "spread": volatility * 0.5,
                "turbulence": volatility
            },
            "lighting": {
                "preset": config["lighting"],
                "intensity": 1.0 + volatility * 0.5,
                "contrast": 1.0 + volatility * 0.3
            },
            "audio": {
                "tempo": config["music_tempo"],
                "intensity": volatility_factor,
                "pitch_variation": volatility * 0.1
            }
        }
        
        # 高波动性添加特效
        if volatility > 0.5:
            scene_params["special_effects"] = ["glitch_effects", "color_shift"]
        if volatility > 1.0:
            scene_params["special_effects"].append("screen_shake")
        
        return {
            "scene_params": scene_params,
            "sentiment": sentiment,
            "volatility_factor": volatility_factor
        }
    
    def _adjust_color(self, rgb_color, adjustment):
        """调整颜色亮度"""
        return [max(0, min(255, c + adjustment)) for c in rgb_color]
    
    def create_animation_sequence(self, stock_data_frame):
        """根据股票数据序列创建动画序列"""
        if stock_data_frame is None or len(stock_data_frame) == 0:
            return []
        
        # 计算技术指标
        df = stock_data_frame.copy()
        
        # 移动平均线
        if 'price' in df.columns:
            df['price_ma5'] = df['price'].rolling(window=5, min_periods=1).mean()
            df['price_ma20'] = df['price'].rolling(window=20, min_periods=1).mean()
        
        # 成交量平均
        if 'volume' in df.columns:
            df['volume_ma5'] = df['volume'].rolling(window=5, min_periods=1).mean()
        
        # 波动率
        if 'price' in df.columns:
            df['returns'] = df['price'].pct_change()
            df['volatility'] = df['returns'].rolling(window=10, min_periods=1).std()
        
        animation_sequence = []
        
        for i, row in df.iterrows():
            frame_params = {
                "frame_index": len(animation_sequence),
                "timestamp": row.get("timestamp", i),
                "data_point": {
                    "price": row.get("price", 0),
                    "volume": row.get("volume", 0),
                    "change_percent": row.get("change_percent", 0)
                },
                "animation_params": {}
            }
            
            # 映射价格变化
            if 'price' in row and 'price_ma20' in row:
                price_change_params = self.map_price_change(
                    row.get('change_percent', 0),
                    row['price'],
                    row['price_ma20']
                )
                frame_params["animation_params"]["price_effects"] = price_change_params["visual_params"]
                frame_params["mood"] = price_change_params["mood"]
            
            # 映射成交量
            if 'volume' in row and 'volume_ma5' in row:
                volume_params = self.map_volume(
                    row['volume'],
                    row['volume_ma5']
                )
                frame_params["animation_params"]["volume_effects"] = volume_params["particle_system"]
                frame_params["volume_level"] = volume_params["volume_level"]
            
            # 映射市场情绪
            if 'price_ma5' in row and 'volume_ma5' in row and 'volatility' in row:
                price_trend = 1 if row['price'] > row['price_ma5'] else -1
                volume_trend = 1 if row['volume'] > row['volume_ma5'] else -1
                
                sentiment_params = self.map_market_sentiment(
                    price_trend,
                    volume_trend,
                    row.get('volatility', 0)
                )
                frame_params["animation_params"]["scene_settings"] = sentiment_params["scene_params"]
                frame_params["sentiment"] = sentiment_params["sentiment"]
            
            # 生成提示词
            frame_params["prompt"] = self.generate_stock_prompt(row)
            frame_params["negative_prompt"] = "blurry, distorted, low quality, ugly, simple, boring"
            
            animation_sequence.append(frame_params)
        
        return animation_sequence
    
    def generate_stock_prompt(self, stock_data):
        """生成股票数据提示词"""
        parts = []
        
        # 根据价格变化确定基调
        change_percent = stock_data.get('change_percent', 0)
        if change_percent > 5:
            parts.append("explosive growth")
            parts.append("bull market rally")
            parts.append("green energy flowing")
        elif change_percent > 2:
            parts.append("strong upward trend")
            parts.append("optimistic trading")
            parts.append("rising momentum")
        elif change_percent > 0:
            parts.append("moderate gains")
            parts.append("steady growth")
            parts.append("positive movement")
        elif change_percent > -2:
            parts.append("sideways trading")
            parts.append("market consolidation")
            parts.append("balanced flow")
        elif change_percent > -5:
            parts.append("downward pressure")
            parts.append("bearish sentiment")
            parts.append("red waves crashing")
        else:
            parts.append("market crash")
            parts.append("panic selling")
            parts.append("chaotic downturn")
        
        # 根据成交量添加描述
        volume = stock_data.get('volume', 0)
        if volume > 10000000:
            parts.append("high volume trading")
            parts.append("intense activity")
        elif volume > 1000000:
            parts.append("moderate volume")
            parts.append("active market")
        else:
            parts.append("low volume")
            parts.append("quiet trading")
        
        # 添加抽象金融可视化描述
        parts.append("abstract financial visualization")
        parts.append("data flow animation")
        parts.append("digital particles")
        parts.append("market energy")
        
        # 添加质量描述
        parts.append("cinematic")
        parts.append("high quality")
        parts.append("8k resolution")
        parts.append("detailed")
        parts.append("dynamic")
        
        # 添加风格描述
        parts.append("futuristic")
        parts.append("holographic")
        parts.append("neon glow")
        parts.append("cyberpunk aesthetic")
        
        return ", ".join(parts)

# 使用示例
if __name__ == "__main__":
    # 创建映射器
    mapper = StockAnimationMapper()
    
    # 创建测试数据
    import pandas as pd
    import numpy as np
    
    # 生成模拟股票数据
    dates = pd.date_range('2024-01-01 09:30', periods=20, freq='5min')
    prices = 100 + np.cumsum(np.random.randn(20) * 0.5)
    volumes = np.random.randint(1000000, 10000000, 20)
    
    test_data = pd.DataFrame({
        'timestamp': dates,
        'price': prices,
        'volume': volumes,
        'change_percent': np.random.randn(20) * 2
    })
    
    # 计算移动平均
    test_data['price_ma20'] = test_data['price'].rolling(window=5, min_periods=1).mean()
    test_data['volume_ma5'] = test_data['volume'].rolling(window=5, min_periods=1).mean()
    test_data['returns'] = test_data['price'].pct_change()
    test_data['volatility'] = test_data['returns'].rolling(window=5, min_periods=1).std()
    
    print("股票数据到动画参数映射测试:")
    print("=" * 80)
    print(f"数据点数: {len(test_data)}")
    print(f"价格范围: {test_data['price'].min():.2f} - {test_data['price'].max():.2f}")
    print(f"涨跌幅范围: {test_data['change_percent'].min():.2f}% - {test_data['change_percent'].max():.2f}%")
    print("\n" + "=" * 80)
    
    # 生成动画序列
    animation_sequence = mapper.create_animation_sequence(test_data)
    
    print(f"生成的动画序列: {len(animation_sequence)} 帧")
    
    # 显示前3帧的参数
    for i in range(min(3, len(animation_sequence))):
        frame = animation_sequence[i]
        print(f"\n第 {i+1} 帧:")
        print(f"  时间: {frame['timestamp']}")
        print(f"  价格: {frame['data_point']['price']:.2f}")
        print(f"  涨跌幅: {frame['data_point']['change_percent']:.2f}%")
        print

更多推荐