
Attribution Analysis Modeling
- 29 installs
- 264 repo stars
- Updated May 10, 2026
- liangdabiao/claude-data-analysis-ultra-main
Helps with ai & agent building tasks.
About
attribution-analysis-modeling is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- attribution-analysis-modeling
- AI & Agent Building
- AI-coding skill
Attribution Analysis Modeling by the numbers
- 29 all-time installs (skills.sh)
- Ranked #9,412 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/liangdabiao/claude-data-analysis-ultra-main --skill attribution-analysis-modelingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 29 |
|---|---|
| repo stars | ★ 264 |
| Last updated | May 10, 2026 |
| Repository | liangdabiao/claude-data-analysis-ultra-main ↗ |
What it does
Helps with ai & agent building tasks.
Files
Marketing Attribution Analysis & Modeling
A comprehensive attribution analysis skill that evaluates marketing channel effectiveness using advanced statistical models, helping optimize marketing spend and understand customer journey patterns.
Instructions
1. Data Loading and Preparation
When users provide marketing touchpoint data:
- Load and validate channel interaction data
- Parse customer journey paths and touchpoint sequences
- Handle different data formats (CSV, JSON, Excel)
- Support both user-level and session-level attribution analysis
- Process timestamp data for chronological path analysis
2. Customer Journey Analysis
- Reconstruct customer journey paths from touchpoint data
- Calculate path lengths and conversion patterns
- Identify common conversion paths and bottlenecks
- Analyze channel sequencing and order effects
- Support both online and offline channel attribution
3. Attribution Model Implementation
- Markov Chain Attribution: Build transition probability matrices and calculate removal effects
- Shapley Value Attribution: Calculate fair channel contributions using game theory
- First-Touch Attribution: Assign full credit to the first channel in the path
- Last-Touch Attribution: Assign full credit to the last channel before conversion
- Linear Attribution: Distribute credit equally across all channels
- Time-Decay Attribution: Weight channels based on recency
- Position-Based Attribution: Weight first and last touches more heavily
4. Channel Performance Analysis
- Calculate conversion rates by channel and channel combinations
- Compute ROI and cost-per-acquisition (CPA) for each channel
- Analyze channel synergy and interaction effects
- Identify underperforming and overperforming channels
- Generate channel contribution percentages
5. Visualization and Reporting
- Create attribution weight distribution charts
- Generate customer journey path visualizations
- Build channel transition heatmaps and network graphs
- Produce ROI analysis and budget allocation recommendations
- Generate comprehensive attribution reports
Usage Examples
Marketing Channel Attribution
Analyze the effectiveness of our marketing channels:
[CSV with columns: user_id, timestamp, channel, conversion_status, conversion_value]Digital Campaign Attribution
Calculate attribution for our digital marketing campaigns:
[Marketing touchpoint data with campaign, channel, timestamp, and conversion data]E-commerce Conversion Attribution
Perform attribution analysis for e-commerce customer journeys:
[Customer path data showing touchpoints before purchase]Budget Optimization
Help optimize our marketing budget based on attribution results:
[Channel performance data with spend and conversion metrics]Key Features
Advanced Attribution Models
- Markov Chain Analysis: Probabilistic model for channel transition analysis
- Shapley Values: Game theory-based fair attribution calculation
- Custom Models: Flexible framework for custom attribution logic
- Model Comparison: Compare different attribution models side-by-side
Customer Journey Analysis
- Path Reconstruction: Automatically build conversion paths from raw data
- Touchpoint Sequencing: Analyze order and timing effects
- Conversion Funnels: Identify drop-off points in customer journeys
- Multi-path Analysis: Handle customers with multiple conversion paths
Channel Performance Metrics
- Attribution Weights: Calculate each channel's contribution to conversions
- ROI Analysis: Compute return on investment for each channel
- Synergy Effects: Measure how channels work together
- Incremental Impact: Estimate additional value from channel combinations
Business Intelligence
- Budget Optimization: Recommend optimal budget allocation
- Channel Recommendations: Suggest best channel combinations
- Performance Benchmarks: Compare channel performance against baselines
- Trend Analysis: Track attribution changes over time
File Requirements
Standard Touchpoint Data Format
user_id,timestamp,channel,conversion_status,conversion_value,cost
USER001,2024-01-15T10:30:00Z,paid_search,0,0,50
USER001,2024-01-16T14:20:00Z,social_media,0,0,30
USER001,2024-01-18T09:15:00Z,email,1,1000,10Required Fields:
- user_id: Unique customer identifier
- timestamp: Touchpoint timestamp (ISO format preferred)
- channel: Marketing channel or touchpoint
- conversion_status: Binary indicator of conversion (0/1)
- conversion_value: Monetary value of conversion (optional)
- cost: Marketing cost for touchpoint (optional, for ROI analysis)
Supported Channel Types:
- Digital: paid_search, organic_search, social_media, email, display, video
- Traditional: tv, radio, print, outdoor, direct_mail
- E-commerce: marketplace, affiliate, referral
- Custom: Any channel name can be used
Output Files Generated
- attribution_results.csv: Complete attribution analysis with channel weights
- channel_performance.csv: Channel metrics including ROI and CPA
- customer_paths.csv: Reconstructed customer journey paths
- transition_matrix.csv: Markov chain transition probability matrix
- attribution_dashboard.png: Comprehensive visualization dashboard
- attribution_report.md: Detailed analysis report and recommendations
Dependencies
- Core Analytics: pandas, numpy, scipy
- Visualization: matplotlib, seaborn, networkx (for path graphs)
- Statistical Models: scikit-learn (optional, for advanced models)
- Data Processing: Standard Python libraries for file operations
Attribution Models Explained
Markov Chain Attribution
Uses probability transition matrices to model customer journey behavior:
- Calculates removal effect of each channel
- Considers channel transition probabilities
- Handles complex multi-path customer journeys
- Provides incremental value assessment
Shapley Value Attribution
Applies cooperative game theory for fair attribution:
- Calculates marginal contribution of each channel
- Considers all possible channel combinations
- Provides theoretically optimal attribution
- Handles channel interaction effects
Custom Attribution Models
Flexible framework for business-specific attribution:
- Configurable weighting rules
- Time-based decay functions
- Position-based weighting
- Custom business logic integration
Business Applications
Marketing Budget Optimization
- Allocate budget based on true channel contribution
- Identify underutilized high-performing channels
- Reduce spend on low-impact channels
- Test new channel opportunities
Campaign Performance Analysis
- Evaluate multi-channel campaign effectiveness
- Understand channel synergy effects
- Optimize campaign sequencing and timing
- Measure incremental lift from channel combinations
Customer Journey Optimization
- Identify optimal channel sequences
- Remove friction points in conversion paths
- Enhance high-performing channel combinations
- Personalize channel selection by customer segment
Advanced Features
Real-time Attribution
- Process streaming touchpoint data
- Update attribution weights dynamically
- Provide real-time channel performance insights
- Support live campaign optimization
Multi-Conversion Analysis
- Handle multiple conversion types
- Analyze different conversion values separately
- Compare attribution across conversion types
- Optimize for specific conversion goals
Segmentation Analysis
- Perform attribution by customer segment
- Compare channel effectiveness across segments
- Optimize channel mix by segment
- Personalize marketing strategies
Best Practices
Data Quality
- Ensure consistent user identification across touchpoints
- Maintain accurate timestamp data
- Include cost data for ROI analysis
- Handle data gaps and missing values appropriately
Model Selection
- Choose attribution model based on business goals
- Compare multiple models for validation
- Consider customer journey complexity
- Validate results with business stakeholders
Implementation
- Start with simpler models before advancing to complex ones
- Test attribution results against known business outcomes
- Implement gradual changes based on attribution insights
- Monitor attribution model performance over time
---
This skill transforms complex attribution analysis into actionable marketing insights, helping businesses optimize their marketing spend and understand true channel effectiveness.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
高级归因分析示例
Advanced Attribution Analysis Example
演示如何使用马尔可夫链和Shapley值进行高级归因分析
"""
import pandas as pd
import sys
import os
import time
# 添加父目录到路径以导入技能模块
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from core_attribution import AttributionAnalyzer
from markov_chains import MarkovChainAttributor
from shapley_values import ShapleyValueAttributor
from attribution_visualizer import AttributionVisualizer
def main():
"""高级归因分析示例主函数"""
print("🎮 高级归因分析示例")
print("=" * 60)
# 1. 加载和预处理数据
print("📊 第1步: 数据加载和预处理")
print("-" * 40)
analyzer = AttributionAnalyzer()
data_path = os.path.join(os.path.dirname(__file__), 'sample_channel_data.csv')
df = analyzer.load_and_validate_data(data_path)
if df is None:
print("❌ 数据加载失败")
return
print(f"✅ 数据加载成功: {len(df)} 条记录")
# 构建客户路径
paths_df = analyzer.build_customer_paths(df)
print(f"✅ 客户路径构建完成: {len(paths_df)} 条路径")
# 2. 马尔可夫链归因分析
print(f"\n🔗 第2步: 马尔可夫链归因分析")
print("-" * 40)
start_time = time.time()
try:
markov_attributor = MarkovChainAttributor()
print("🔄 构建马尔可夫链模型...")
# 构建转移矩阵
transition_matrix = markov_attributor.build_transition_matrix(paths_df)
print("✅ 转移矩阵构建完成")
# 计算归因权重
markov_weights = markov_attributor.calculate_attribution_weights()
print("✅ 马尔可夫链归因权重计算完成")
# 分析渠道转换
transition_analysis = markov_attributor.analyze_channel_transitions(transition_matrix)
print("✅ 渠道转换分析完成")
# 构建渠道网络图
channel_graph = markov_attributor.build_channel_graph(transition_matrix)
print("✅ 渠道网络图构建完成")
markov_time = time.time() - start_time
print(f"⏱️ 马尔可夫链分析耗时: {markov_time:.2f} 秒")
# 显示马尔可夫链结果
print(f"\n📊 马尔可夫链归因权重:")
sorted_markov = sorted(markov_weights.items(), key=lambda x: x[1], reverse=True)
for channel, weight in sorted_markov:
print(f" {channel:<15}: {weight:.4f} ({weight*100:.1f}%)")
# 显示关键转换路径
print(f"\n🔀 关键渠道转换路径 (前5个):")
for path in transition_analysis['top_paths'][:5]:
print(f" {' → '.join(path['path'])}: 概率={path['probability']:.4f}")
except Exception as e:
print(f"❌ 马尔可夫链分析失败: {e}")
markov_weights = {}
transition_analysis = {}
channel_graph = None
# 3. Shapley值归因分析
print(f"\n🎮 第3步: Shapley值归因分析")
print("-" * 40)
start_time = time.time()
try:
shapley_attributor = ShapleyValueAttributor()
print("🔄 计算Shapley值...")
# 运行完整的Shapley值分析
shapley_results = shapley_attributor.run_complete_shapley_analysis(paths_df)
print("✅ Shapley值分析完成")
shapley_time = time.time() - start_time
print(f"⏱️ Shapley值分析耗时: {shapley_time:.2f} 秒")
# 显示Shapley值结果
print(f"\n📊 Shapley值归因权重:")
shapley_weights = shapley_results['attribution_weights']
sorted_shapley = sorted(shapley_weights.items(), key=lambda x: x[1], reverse=True)
for channel, weight in sorted_shapley:
print(f" {channel:<15}: {weight:.4f} ({weight*100:.1f}%)")
# 显示渠道协同效应
print(f"\n🤝 渠道协同效应分析 (前3个最佳组合):")
synergy_analysis = shapley_results['channel_synergy']
for i, (pair_key, synergy) in enumerate(list(synergy_analysis.items())[:3]):
synergy_type = synergy['synergy_type']
print(f" {i+1}. {synergy['channel1']} + {synergy['channel2']}: "
f"协同比={synergy['synergy_ratio']:.3f} ({synergy_type})")
# 显示边际贡献分析
print(f"\n📈 边际贡献分析:")
marginal_df = shapley_results['marginal_analysis']
for _, row in marginal_df.iterrows():
tier_icon = "🌟" if row['performance_tier'] == 'top_performer' else \
"⭐" if row['performance_tier'] == 'strong_performer' else \
"✨" if row['performance_tier'] == 'moderate_performer' else "💫"
print(f" {tier_icon} {row['channel']:<15}: "
f"Shapley值={row['shapley_value']:.6f}, "
f"层级={row['performance_tier']}")
# 显示优化建议
print(f"\n🎯 渠道优化建议:")
optimization = shapley_results['optimization']
recommendations = optimization.get('recommendations', [])
for i, rec in enumerate(recommendations[:5]):
action_icon = "📈" if rec['action'] == 'increase' else "📉"
priority_icon = "🔥" if rec['priority'] == 'high' else "⚡"
print(f" {i+1}. {action_icon} {priority_icon} {rec['channel']}: {rec['reason']}")
except Exception as e:
print(f"❌ Shapley值分析失败: {e}")
shapley_results = {}
# 4. 模型对比分析
print(f"\n📊 第4步: 归因模型对比分析")
print("-" * 40)
# 收集所有模型的结果
model_results = {}
# 基础模型
try:
basic_results = analyzer.run_basic_attribution_analysis(paths_df)
model_results.update(basic_results)
except:
pass
# 高级模型
if 'markov_weights' in locals() and markov_weights:
model_results['马尔可夫链归因'] = markov_weights
if 'shapley_weights' in locals() and shapley_weights:
model_results['Shapley值归因'] = shapley_weights
# 创建对比表
if model_results:
print(f"\n📋 归因模型对比表:")
print("-" * 80)
# 获取所有渠道
all_channels = set()
for weights in model_results.values():
all_channels.update(weights.keys())
# 表头
print(f"{'渠道':<15}", end="")
for model_name in model_results.keys():
print(f"{model_name:<12}", end="")
print(f"{'平均权重':<10} {'标准差':<10}")
print("-" * 80)
# 计算每个渠道的统计信息
for channel in sorted(all_channels):
weights = []
print(f"{channel:<15}", end="")
for model_name, model_weights in model_results.items():
weight = model_weights.get(channel, 0)
weights.append(weight)
print(f"{weight*100:>6.1f}%{' '*6}", end="")
avg_weight = sum(weights) / len(weights)
std_weight = (sum((w - avg_weight)**2 for w in weights) / len(weights))**0.5
print(f"{avg_weight*100:>6.1f}%{' '*4} {std_weight*100:>6.1f}%")
# 5. 生成高级可视化
print(f"\n📊 第5步: 生成高级可视化")
print("-" * 40)
try:
visualizer = AttributionVisualizer()
# 创建马尔可夫链可视化
if 'channel_graph' in locals() and channel_graph:
markov_viz_path = visualizer.create_markov_visualization({
'transition_matrix': transition_matrix,
'attribution_weights': markov_weights,
'channel_graph': channel_graph,
'transition_analysis': transition_analysis
})
print(f"✅ 马尔可夫链可视化已保存: {markov_viz_path}")
# 创建Shapley值可视化
if 'shapley_results' in locals() and shapley_results:
shapley_viz_path = visualizer.create_shapley_visualization(shapley_results)
print(f"✅ Shapley值可视化已保存: {shapley_viz_path}")
# 创建综合对比可视化
if model_results:
comparison_viz_path = visualizer.create_attribution_dashboard(model_results)
print(f"✅ 归因模型对比可视化已保存: {comparison_viz_path}")
except Exception as e:
print(f"⚠️ 可视化生成失败: {e}")
# 6. 总结和业务洞察
print(f"\n💡 第6步: 总结和业务洞察")
print("-" * 40)
print(f"🎯 高级归因分析完成!")
print(f" • 分析了 {len(paths_df)} 条客户路径")
print(f" • 评估了 {len(model_results)} 种归因模型")
print(f" • 识别了 {len(df['channel'].unique())} 个营销渠道")
if model_results:
# 找出在不同模型中表现一致的渠道
print(f"\n🏆 稳定表现渠道 (在所有模型中权重排名前3):")
# 计算每个渠道的平均排名
channel_rankings = {}
for model_name, weights in model_results.items():
sorted_channels = sorted(weights.items(), key=lambda x: x[1], reverse=True)
for rank, (channel, weight) in enumerate(sorted_channels):
if channel not in channel_rankings:
channel_rankings[channel] = []
channel_rankings[channel].append(rank + 1)
# 找出平均排名最好的渠道
avg_rankings = {
channel: sum(ranks) / len(ranks)
for channel, ranks in channel_rankings.items()
}
top_channels = sorted(avg_rankings.items(), key=lambda x: x[1])[:3]
for i, (channel, avg_rank) in enumerate(top_channels):
print(f" {i+1}. {channel}: 平均排名 {avg_rank:.1f}")
print(f"\n📈 分析建议:")
print(f" 1. 使用多种归因模型进行交叉验证")
print(f" 2. 关注马尔可夫链识别的关键转换路径")
print(f" 3. 利用Shapley值分析优化渠道组合")
print(f" 4. 基于协同效应设计联合营销策略")
print(f" 5. 定期重新评估归因模型的有效性")
print(f"\n✅ 高级归因分析示例完成!")
print(f"📁 结果文件和可视化图表已保存")
if __name__ == "__main__":
main()#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Attribution Analysis Visualization Tools
Comprehensive visualization for marketing attribution analysis
"""
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import networkx as nx
from matplotlib.gridspec import GridSpec
import warnings
warnings.filterwarnings('ignore')
class AttributionVisualizer:
"""Comprehensive visualization toolkit for attribution analysis"""
def __init__(self, chinese_font='SimHei', style='seaborn-v0_8'):
"""Initialize the visualizer"""
self.chinese_font = chinese_font
self.style = style
# Set up plotting style
plt.rcParams['font.sans-serif'] = [chinese_font]
plt.rcParams['axes.unicode_minus'] = False
sns.set_style("whitegrid")
try:
plt.style.use(style)
except:
plt.style.use('default')
def create_attribution_dashboard(self, attribution_results, save_path='attribution_dashboard.png'):
"""
Create comprehensive attribution analysis dashboard
Args:
attribution_results (dict): Attribution analysis results
save_path (str): Path to save the dashboard
"""
print("创建归因分析综合仪表板...")
# Create figure with custom layout
fig = plt.figure(figsize=(20, 16))
gs = GridSpec(4, 4, figure=fig, hspace=0.3, wspace=0.3)
# 1. Attribution weights comparison (top left)
ax1 = fig.add_subplot(gs[0, 0:2])
self._plot_attribution_weights_comparison(attribution_results, ax1)
# 2. Channel performance metrics (top right)
ax2 = fig.add_subplot(gs[0, 2:4])
self._plot_channel_performance_metrics(attribution_results, ax2)
# 3. Customer journey path analysis (middle left)
ax3 = fig.add_subplot(gs[1, 0:2])
self._plot_journey_path_analysis(attribution_results, ax3)
# 4. Conversion funnel analysis (middle right)
ax4 = fig.add_subplot(gs[1, 2:4])
self._plot_conversion_funnel(attribution_results, ax4)
# 5. ROI vs Attribution weights (bottom left)
ax5 = fig.add_subplot(gs[2, 0:2])
self._plot_roi_attribution_correlation(attribution_results, ax5)
# 6. Channel interaction heatmap (bottom right)
ax6 = fig.add_subplot(gs[2, 2:4])
self._plot_channel_interaction_heatmap(attribution_results, ax6)
# 7. Attribution model comparison (bottom)
ax7 = fig.add_subplot(gs[3, :])
self._plot_model_comparison(attribution_results, ax7)
# Add main title
fig.suptitle('营销归因分析综合仪表板\nMarketing Attribution Analysis Dashboard',
fontsize=20, fontweight='bold', y=0.95)
# Add footer with analysis info
fig.text(0.5, 0.02, f'分析时间: {pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S")} | '
f'渠道数量: {self._get_channel_count(attribution_results)} | '
f'归因模型数: {self._get_model_count(attribution_results)}',
ha='center', fontsize=10, style='italic')
# Save the dashboard
plt.savefig(save_path, dpi=300, bbox_inches='tight', facecolor='white')
plt.close()
print(f"仪表板已保存: {save_path}")
def _plot_attribution_weights_comparison(self, results, ax):
"""Plot attribution weights comparison across models"""
if 'attribution_models' not in results:
self._plot_placeholder(ax, "归因模型数据不可用")
return
models_data = results['attribution_models']
all_channels = set()
for model, channels in models_data.items():
all_channels.update(channels.keys())
# Prepare data for plotting
plot_data = []
for channel in all_channels:
row = {'channel': channel}
for model, channels in models_data.items():
row[model] = channels.get(channel, 0.0)
plot_data.append(row)
df_plot = pd.DataFrame(plot_data)
df_plot = df_plot.set_index('channel')
# Create stacked bar chart
df_plot.T.plot(kind='bar', stacked=True, ax=ax, figsize=(12, 8))
ax.set_title('各归因模型的渠道权重对比', fontweight='bold', fontsize=12)
ax.set_xlabel('归因模型')
ax.set_ylabel('归因权重')
ax.legend(title='归因模型', bbox_to_anchor=(1.05, 1), loc='upper left')
ax.grid(True, alpha=0.3)
ax.tick_params(axis='x', rotation=45)
def _plot_channel_performance_metrics(self, results, ax):
"""Plot channel performance metrics"""
if 'channel_performance' not in results:
self._plot_placeholder(ax, "渠道性能数据不可用")
return
perf_df = pd.DataFrame(results['channel_performance'])
# Select key metrics for visualization
metrics = ['conversion_rate', 'cpa', 'roi']
metric_labels = {
'conversion_rate': '转化率',
'cpa': 'CPA (获客成本)',
'roi': 'ROI (投资回报率)'
}
# Create subplots
n_metrics = len(metrics)
for i, metric in enumerate(metrics):
sub_ax = ax.inset_axes([0.1, 0.8 - i*0.25, 0.8, 0.2])
perf_df_sorted = perf_df.sort_values(metric, ascending=(metric == 'cpa'))
bars = sub_ax.barh(range(len(perf_df_sorted)), perf_df_sorted[metric],
color=plt.cm.RdYlGn_r([0.3, 0.7, 0.9]))
sub_ax.set_yticks(range(len(perf_df_sorted)))
sub_ax.set_yticklabels([ch[:15] for ch in perf_df_sorted['channel']], fontsize=8)
sub_ax.set_xlabel(metric_labels[metric], fontsize=9)
sub_ax.grid(True, alpha=0.3)
# Add value labels
for j, (bar, value) in enumerate(zip(bars, perf_df_sorted[metric])):
if metric == 'roi':
sub_ax.text(bar.get_width() + 0.01, bar.get_y() + bar.get_height()/2,
f'{value:.1f}x', ha='left', va='center', fontsize=7)
else:
sub_ax.text(bar.get_width() + 0.01, bar.get_y() + bar.get_height()/2,
f'{value:.2f}', ha='left', va='center', fontsize=7)
ax.set_title('渠道关键性能指标', fontweight='bold', fontsize=12)
ax.axis('off')
def _plot_journey_path_analysis(self, results, ax):
"""Plot customer journey path analysis"""
if 'customer_paths' not in results:
self._plot_placeholder(ax, "客户路径数据不可用")
return
paths_df = pd.DataFrame(results['customer_paths'])
# Analyze path lengths
path_lengths = [len(path) for path in paths_df['path']]
path_lengths_by_conversion = {
'转化路径': [len(path) for path in paths_df[paths_df['converted'] == 1]['path']],
'未转化路径': [len(path) for path in paths_df[paths_df['converted'] == 0]['path']]
}
# Create histograms
colors = ['green', 'red']
labels = ['转化路径', '未转化路径']
for i, (label, lengths) in enumerate(path_lengths_by_conversion.items()):
ax.hist(lengths, alpha=0.6, bins=15, label=label, color=colors[i])
ax.set_xlabel('路径长度')
ax.set_ylabel('频次')
ax.set_title('客户旅程路径长度分布', fontweight='bold')
ax.legend()
ax.grid(True, alpha=0.3)
# Add statistics
avg_converted_len = np.mean(path_lengths_by_conversion['转化路径'])
avg_non_converted_len = np.mean(path_lengths_by_conversion['未转化路径'])
ax.text(0.7, 0.8, f'转化路径平均长度: {avg_converted_len:.1f}',
transform=ax.transAxes, fontsize=10, bbox=dict(boxstyle="round", facecolor='lightgreen'))
ax.text(0.7, 0.6, f'未转化路径平均长度: {avg_non_converted_len:.1f}',
transform=ax.transAxes, fontsize=10, bbox=dict(boxstyle="round", facecolor='lightcoral'))
def _plot_conversion_funnel(self, results, ax):
"""Plot conversion funnel analysis"""
if 'customer_paths' not in results:
self._plot_placeholder(ax, "漏斗分析数据不可用")
return
paths_df = pd.DataFrame(results['customer_paths'])
# Calculate funnel stages
total_users = len(paths_df)
converted_users = paths_df['converted'].sum()
non_converted_users = total_users - converted_users
# Funnel stages based on path length
funnel_stages = []
stage_labels = []
# Define stages based on path length distribution
max_length = max([len(path) for path in paths_df['path']])
for length in range(2, min(max_length + 1, 7)):
stage_users = len([path for path in paths_df['path'] if len(path) >= length])
funnel_stages.append(stage_users)
stage_labels.append(f'{length}+个触点')
# Create funnel plot
funnel_stages.insert(0, total_users)
stage_labels.insert(0, '总用户数')
# Calculate conversion rates
conversion_rates = [converted_users / stage if stage > 0 else 0 for stage in funnel_stages]
# Create funnel bars
bars = ax.barh(range(len(funnel_stages)), funnel_stages, color=plt.cm.Blues_r(np.linspace(0.3, 0.9, len(funnel_stages))))
ax.set_yticks(range(len(funnel_stages)))
ax.set_yticklabels(stage_labels)
ax.set_xlabel('用户数')
ax.set_title('转化漏斗分析', fontweight='bold')
# Add conversion rate labels
for i, (bar, rate) in enumerate(zip(bars, conversion_rates)):
ax.text(bar.get_width() + total_users * 0.01, bar.get_y() + bar.get_height()/2,
f'{rate:.1%}', ha='left', va='center', fontsize=9)
ax.grid(True, alpha=0.3, axis='x')
def _plot_roi_attribution_correlation(self, results, ax):
"""Plot ROI vs Attribution weights correlation"""
if 'channel_performance' not in results or 'attribution_models' not in results:
self._plot_placeholder(ax, "ROI和归因权重数据不可用")
return
perf_df = pd.DataFrame(results['channel_performance'])
models_data = results['attribution_models']
# Use the average attribution weight across models
channel_weights = {}
for model, channels in models_data.items():
for channel, weight in channels.items():
if channel not in channel_weights:
channel_weights[channel] = []
channel_weights[channel].append(weight)
avg_weights = {ch: np.mean(weights) for ch, weights in channel_weights.items()}
# Create scatter plot
channels = list(set(perf_df['channel']) & set(avg_weights.keys()))
roi_data = []
weight_data = []
for channel in channels:
channel_perf = perf_df[perf_df['channel'] == channel].iloc[0]
if channel_perf['roi'] != float('inf'): # Exclude infinite ROI
roi_data.append(channel_perf['roi'])
weight_data.append(avg_weights[channel])
if len(roi_data) > 0:
scatter = ax.scatter(weight_data, roi_data, alpha=0.6, s=60)
# Add trend line
z = np.polyfit(weight_data, roi_data, 1)
p = np.poly1d(z)
x_trend = np.linspace(min(weight_data), max(weight_data), 100)
y_trend = p(x_trend)
ax.plot(x_trend, y_trend, "r--", alpha=0.8)
# Calculate correlation
correlation = np.corrcoef(weight_data, roi_data)[0, 1]
# Add channel labels
for i, channel in enumerate(channels):
if i < len(channels) // 2: # Label only half of the points to avoid overcrowding
ax.annotate(channel[:10], (weight_data[i], roi_data[i]),
fontsize=8, alpha=0.7)
ax.set_xlabel('平均归因权重')
ax.set_ylabel('ROI')
ax.set_title(f'ROI vs 归因权重相关性 (r={correlation:.3f})', fontweight='bold')
ax.grid(True, alpha=0.3)
# Add correlation interpretation
if correlation > 0.7:
interpretation = "强正相关"
elif correlation > 0.3:
interpretation = "中等正相关"
elif correlation > -0.3:
interpretation = "弱相关"
else:
interpretation = "负相关"
ax.text(0.7, 0.9, f'相关性: {interpretation}',
transform=ax.transAxes, fontsize=10,
bbox=dict(boxstyle="round", facecolor='yellow', alpha=0.7))
def _plot_channel_interaction_heatmap(self, results, ax):
"""Plot channel interaction heatmap"""
if 'attribution_models' not in results or 'shapley_values' not in results:
self._plot_placeholder(ax, "渠道协同数据不可用")
return
# For simplicity, we'll create a correlation matrix of attribution weights across models
models_data = results['attribution_models']
if len(models_data) < 2:
self._plot_placeholder(ax, "需要多个归因模型进行比较")
return
# Create channel x model matrix
all_channels = set()
for model in models_data.values():
all_channels.update(model.keys())
# Create correlation matrix
matrix_data = []
model_names = list(models_data.keys())
for channel in all_channels:
row = []
for model in model_names:
weight = models_data[model].get(channel, 0.0)
row.append(weight)
matrix_data.append(row)
if len(matrix_data) > 0:
df_matrix = pd.DataFrame(matrix_data, index=list(all_channels), columns=model_names)
# Calculate correlation matrix
corr_matrix = df_matrix.corr()
# Create heatmap
sns.heatmap(corr_matrix, annot=True, cmap='RdYlBu_r', center=0,
square=True, linewidths=0.5, ax=ax)
ax.set_title('渠道归因权重相关性热力图', fontweight='bold')
# Rotate labels if needed
if len(model_names) > 5:
plt.setp(ax.get_xticklabels(), rotation=45, ha='right')
def _plot_model_comparison(self, results, ax):
"""Plot comparison of different attribution models"""
if 'attribution_comparison' not in results:
self._plot_placeholder(ax, "模型比较数据不可用")
return
comp_df = pd.DataFrame(results['attribution_comparison'])
if comp_df.empty:
self._plot_placeholder(ax, "模型比较数据为空")
return
# Select key models for comparison
model_columns = ['first_touch', 'last_touch', 'linear', 'markov_chain']
available_models = [col for col in model_columns if col in comp_df.columns]
if not available_models:
self._plot_placeholder(ax, "没有可用的归因模型数据")
return
# Create comparison plot
plot_data = comp_df[['channel'] + available_models].set_index('channel')
# Create horizontal bar plot
plot_data.plot(kind='barh', ax=ax, figsize=(14, 8))
ax.set_xlabel('归因权重')
ax.set_title('多归因模型权重对比', fontweight='bold', fontsize=12)
ax.legend(title='归因模型')
ax.grid(True, alpha=0.3)
ax.tick_params(axis='y')
# Add statistical summary
model_stats = {}
for model in available_models:
weights = comp_df[model].fillna(0)
model_stats[model] = {
'mean': weights.mean(),
'std': weights.std(),
'max': weights.max(),
'min': weights.min()
}
stats_text = "模型统计:\n"
for model, stats in model_stats.items():
stats_text += f"{model}: μ={stats['mean']:.3f}, σ={stats['std']:.3f}\n"
ax.text(0.02, 0.98, stats_text, transform=ax.transAxes, fontsize=9,
verticalalignment='top', family='monospace',
bbox=dict(boxstyle="round", facecolor='lightblue', alpha=0.7))
def _plot_placeholder(self, ax, message):
"""Plot placeholder text when data is not available"""
ax.text(0.5, 0.5, message, ha='center', va='center', fontsize=12,
transform=ax.transAxes, style='italic', color='gray')
ax.set_title('数据不可用', fontweight='bold')
ax.axis('off')
def _get_channel_count(self, results):
"""Get channel count from results"""
if 'attribution_models' in results:
return len(set().union(*[set(model.keys()) for model in results['attribution_models'].values()]))
elif 'channel_performance' in results:
return len(results['channel_performance'])
else:
return 0
def _get_model_count(self, results):
"""Get model count from results"""
if 'attribution_models' in results:
return len(results['attribution_models'])
else:
return 0
def create_markov_visualization(self, markov_results, save_path='markov_analysis.png'):
"""
Create Markov chain analysis visualization
Args:
markov_results (dict): Markov chain analysis results
save_path (str): Path to save the visualization
"""
print("创建马尔可夫链可视化...")
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(16, 12))
fig.suptitle('马尔可夫链归因分析', fontsize=16, fontweight='bold')
# 1. Transition matrix heatmap
if 'transition_matrix' in markov_results:
transition_matrix = markov_results['transition_matrix']
# Focus on channels only (remove start/end states for clarity)
channels = [state for state in transition_matrix.columns
if state not in ['开始', '未转化', '成功转化']]
if channels:
channel_matrix = transition_matrix.loc[channels, channels]
sns.heatmap(channel_matrix, annot=True, cmap='Blues', fmt='.3f', ax=ax1)
ax1.set_title('渠道转移概率矩阵', fontweight='bold')
else:
ax1.text(0.5, 0.5, '渠道数据不可用', ha='center', va='center',
transform=ax1.transAxes, style='italic')
ax1.axis('off')
# 2. Removal effects
if 'removal_effects' in markov_results:
removal_effects = markov_results['removal_effects']
channels = list(removal_effects.keys())
effects = list(removal_effects.values())
bars = ax2.barh(channels, effects)
ax2.set_xlabel('移除效应')
ax2.set_title('渠道移除效应', fontweight='bold')
ax2.grid(True, alpha=0.3)
# Add value labels
for bar, value in zip(bars, effects):
ax2.text(bar.get_width() + 0.01, bar.get_y() + bar.get_height()/2,
f'{value:.3f}', ha='left', va='center', fontsize=9)
# 3. Channel network graph
if 'channel_graph' in markov_results:
graph = markov_results['channel_graph']
if graph and len(graph.nodes()) > 0:
pos = nx.spring_layout(graph, k=1, iterations=50)
# Calculate node sizes based on out-degree
out_degrees = [graph.out_degree(node) for node in graph.nodes()]
node_sizes = [deg * 500 + 100 for deg in out_degrees]
# Calculate edge widths based on weight
edges = graph.edges()
edge_widths = [graph[u][v]['weight'] * 5 for u, v in edges]
nx.draw_networkx_nodes(graph, pos, ax=ax3, node_size=node_sizes,
node_color='lightblue', alpha=0.7)
nx.draw_networkx_edges(graph, pos, ax=ax3, width=edge_widths,
edge_color='gray', alpha=0.5)
nx.draw_networkx_labels(graph, pos, ax=ax3, font_size=8)
ax3.set_title('渠道转换网络图', fontweight='bold')
else:
ax3.text(0.5, 0.5, '网络图数据不可用', ha='center', va='center',
transform=ax3.transAxes, style='italic')
ax3.axis('off')
# 4. Attribution weights comparison
if 'attribution_weights' in markov_results:
attribution_weights = markov_results['attribution_weights']
channels = list(attribution_weights.keys())
weights = list(attribution_weights.values())
bars = ax4.barh(channels, weights)
ax4.set_xlabel('归因权重')
ax4.set_title('马尔可夫链归因权重', fontweight='bold')
ax4.grid(True, alpha=0.3)
# Add percentage labels
for bar, weight in zip(bars, weights):
ax4.text(bar.get_width() + 0.01, bar.get_y() + bar.get_height()/2,
f'{weight:.1%}', ha='left', va='center', fontsize=9)
plt.tight_layout()
plt.savefig(save_path, dpi=300, bbox_inches='tight')
plt.close()
print(f"马尔可夫链可视化已保存: {save_path}")
def create_shapley_visualization(self, shapley_results, save_path='shapley_analysis.png'):
"""
Create Shapley value analysis visualization
Args:
shapley_results (dict): Shapley value analysis results
save_path (str): Path to save the visualization
"""
print("创建Shapley值可视化...")
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(16, 12))
fig.suptitle('Shapley值归因分析', fontsize=16, fontweight='bold')
# 1. Shapley value distribution
if 'attribution_weights' in shapley_results:
attribution_weights = shapley_results['attribution_weights']
channels = list(attribution_weights.keys())
weights = list(attribution_weights.values())
bars = ax1.barh(channels, weights)
ax1.set_xlabel('归因权重')
ax1.set_title('Shapley值归因权重', fontweight='bold')
ax1.grid(True, alpha=0.3)
# Add percentage labels
for bar, weight in zip(bars, weights):
ax1.text(bar.get_width() + 0.01, bar.get_y() + bar.get_height()/2,
f'{weight:.1%}', ha='left', va='center', fontsize=9)
# 2. Marginal contribution analysis
if 'marginal_analysis' in shapley_results:
marginal_df = pd.DataFrame(shapley_results['marginal_analysis'])
# Plot by performance tier
tier_groups = marginal_df.groupby('performance_tier')['shapley_value'].mean()
tier_groups = tier_groups.sort_values(ascending=False)
colors = {'top_performer': '#2ecc71', 'strong_performer': '#3498db',
'moderate_performer': '#f1c40f', 'low_performer': '#e74c3c'}
bars = ax2.bar(range(len(tier_groups)), tier_groups.values(),
color=[colors.get(tier, 'gray') for tier in tier_groups.index])
ax2.set_xticks(range(len(tier_groups)))
ax2.set_xticklabels([tier.replace('_', ' ').title() for tier in tier_groups.index],
rotation=45, ha='right')
ax2.set_ylabel('平均Shapley值')
ax2.set_title('按性能层级的边际贡献', fontweight='bold')
ax2.grid(True, alpha=0.3)
# 3. Channel synergy analysis
if 'channel_synergy' in shapley_results:
synergy_data = shapley_results['channel_synergy']
if synergy_data:
# Select top synergies for visualization
top_synergies = dict(list(synergy_data.items())[:10])
channel_pairs = [f"{data['channel1']} + {data['channel2']}"
for data in top_synergies.values()]
synergy_ratios = [data['synergy_ratio'] for data in top_synergies.values()]
synergy_types = [data['synergy_type'] for data in top_synergies.values()]
bars = ax3.barh(range(len(channel_pairs)), synergy_ratios,
color=['green' if st == 'positive' else 'orange' if st == 'neutral' else 'red'
for st in synergy_types])
ax3.set_yticks(range(len(channel_pairs)))
ax3.set_yticklabels([pair[:20] for pair in channel_pairs], fontsize=8)
ax3.set_xlabel('协同比')
ax3.set_title('渠道协同效应 (前10个)', fontweight='bold')
ax3.grid(True, alpha=0.3)
# Add vertical line at x=1
ax3.axvline(x=1, color='black', linestyle='--', alpha=0.5)
# 4. Optimization recommendations
if 'optimization' in shapley_results:
opt_results = shapley_results['optimization']
if 'recommendations' in opt_results:
recommendations = opt_results['recommendations']
# Group recommendations by action type
actions = {}
for rec in recommendations:
action_type = rec['action']
if action_type not in actions:
actions[action_type] = []
actions[action_type].append(rec['channel'])
# Create pie chart
action_counts = {action: len(channels) for action, channels in actions.items()}
colors = {'increase': '#2ecc71', 'decrease': '#e74c3c', 'optimize': '#f39c12'}
if action_counts:
wedges, texts, autotexts = ax4.pie(
action_counts.values(),
labels=list(action_counts.keys()),
colors=[colors.get(action, 'gray') for action in action_counts.keys()],
autopct='%1.1f%%',
startangle=90
)
ax4.set_title('优化建议分布', fontweight='bold')
else:
ax4.text(0.5, 0.5, '无优化建议数据', ha='center', va='center',
transform=ax4.transAxes, style='italic')
ax4.axis('off')
plt.tight_layout()
plt.savefig(save_path, dpi=300, bbox_inches='tight')
plt.close()
print(f"Shapley值可视化已保存: {save_path}")
def main():
"""Example usage of attribution visualizer"""
visualizer = AttributionVisualizer()
# Create sample attribution results
sample_results = {
'attribution_models': {
'first_touch': {'付费搜索': 0.3, '社交媒体': 0.25, '邮件营销': 0.2, '内容营销': 0.15, '线下广告': 0.1},
'last_touch': {'付费搜索': 0.2, '社交媒体': 0.35, '邮件营销': 0.25, '内容营销': 0.15, '线下广告': 0.05},
'markov_chain': {'付费搜索': 0.28, '社交媒体': 0.30, '邮件营销': 0.22, '内容营销': 0.14, '线下广告': 0.06}
},
'channel_performance': [
{'channel': '付费搜索', 'conversion_rate': 0.025, 'cpa': 50.0, 'roi': 2.5},
{'channel': '社交媒体', 'conversion_rate': 0.035, 'cpa': 40.0, 'roi': 3.0},
{'channel': '邮件营销', 'conversion_rate': 0.020, 'cpa': 30.0, 'roi': 4.0},
{'channel': '内容营销', 'conversion_rate': 0.015, 'cpa': 60.0, 'roi': 1.5},
{'channel': '线下广告', 'conversion_rate': 0.010, 'cpa': 80.0, 'roi': 1.2}
]
}
# Create dashboard
visualizer.create_attribution_dashboard(sample_results)
print("示例归因仪表板已生成: attribution_dashboard.png")
if __name__ == "__main__":
main()#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
基础归因分析示例
Basic Attribution Analysis Example
演示如何使用归因分析技能进行基础的营销渠道归因分析
"""
import pandas as pd
import sys
import os
# Windows环境下控制台编码设置
if sys.platform == 'win32':
import codecs
sys.stdout = codecs.getwriter('utf-8')(sys.stdout.buffer, 'strict')
# 添加父目录到路径以导入技能模块
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from core_attribution import AttributionAnalyzer
from attribution_visualizer import AttributionVisualizer
def main():
"""基础归因分析示例主函数"""
print("🎯 基础归因分析示例")
print("=" * 60)
# 1. 初始化分析器
analyzer = AttributionAnalyzer()
# 2. 加载示例数据
data_path = os.path.join(os.path.dirname(__file__), 'sample_channel_data.csv')
print(f"📊 加载数据: {data_path}")
df = analyzer.load_and_validate_data(data_path)
if df is None:
print("❌ 数据加载失败,请检查文件路径")
return
print(f"✅ 数据加载成功: {len(df)} 条记录")
print(f"📊 数据概览:")
print(f" - 用户数量: {df['user_id'].nunique()}")
print(f" - 渠道数量: {df['channel'].nunique()}")
print(f" - 转化数量: {df['conversion_status'].sum()}")
print(f" - 总转化价值: {df['conversion_value'].sum():,.2f}")
print(f" - 总成本: {df['cost'].sum():,.2f}")
# 3. 构建客户路径
print("\n🛤️ 构建客户路径...")
paths_df = analyzer.build_customer_paths(df)
if paths_df.empty:
print("❌ 客户路径构建失败")
return
print(f"✅ 客户路径构建完成: {len(paths_df)} 条路径")
print(f" - 转化路径: {paths_df['converted'].sum()}")
print(f" - 平均路径长度: {paths_df['path_length'].mean():.2f}")
# 4. 运行基础归因分析
print("\n📈 运行基础归因分析...")
attribution_results = analyzer.run_basic_attribution_analysis(paths_df)
if not attribution_results:
print("❌ 归因分析失败")
return
# 5. 显示归因结果
print("\n📊 归因分析结果:")
print("-" * 40)
for model_name, weights in attribution_results.items():
print(f"\n{model_name}:")
# 按权重排序显示前5个渠道
sorted_weights = sorted(weights.items(), key=lambda x: x[1], reverse=True)[:5]
for channel, weight in sorted_weights:
print(f" {channel:<12}: {weight:.4f} ({weight*100:.1f}%)")
# 6. 计算ROI分析
print("\n💰 ROI分析:")
print("-" * 40)
# 计算各渠道的成本和收益
channel_costs = df.groupby('channel')['cost'].sum()
channel_revenues = df[df['conversion_status'] == 1].groupby('channel')['conversion_value'].sum()
roi_analysis = []
for channel in df['channel'].unique():
cost = channel_costs.get(channel, 0)
revenue = channel_revenues.get(channel, 0)
roi = (revenue - cost) / cost * 100 if cost > 0 else 0
roi_analysis.append({
'channel': channel,
'cost': cost,
'revenue': revenue,
'roi': roi
})
roi_df = pd.DataFrame(roi_analysis)
roi_df = roi_df.sort_values('roi', ascending=False)
for _, row in roi_df.iterrows():
print(f"{row['channel']:<12}: 成本={row['cost']:6.0f}, 收益={row['revenue']:7.0f}, ROI={row['roi']:6.1f}%")
# 7. 生成归因对比表
print("\n📋 归因模型对比:")
print("-" * 60)
# 创建所有渠道的归因权重对比表
all_channels = set()
for weights in attribution_results.values():
all_channels.update(weights.keys())
print(f"{'渠道':<15}", end="")
for model_name in attribution_results.keys():
print(f"{model_name[:8]:<10}", end="")
print()
print("-" * 60)
for channel in sorted(all_channels):
print(f"{channel:<15}", end="")
for model_name, weights in attribution_results.items():
weight = weights.get(channel, 0)
print(f"{weight*100:>6.1f}%{' '*4}", end="")
print()
# 8. 可视化分析
print("\n📊 生成归因可视化...")
try:
visualizer = AttributionVisualizer()
# 保存分析结果
analyzer.save_attribution_results(attribution_results, paths_df, 'examples/')
# 创建可视化
output_path = visualizer.create_attribution_dashboard(attribution_results)
print(f"✅ 可视化已保存至: {output_path}")
except Exception as e:
print(f"⚠️ 可视化生成失败: {e}")
print("提示: 请确保已安装所需的可视化依赖包")
# 9. 业务洞察和建议
print("\n💡 业务洞察和建议:")
print("-" * 40)
# 基于最后接触归因(最常用)给出建议
last_touch_weights = attribution_results.get('最后接触归因', {})
# 找出表现最好和最差的渠道
if last_touch_weights:
sorted_channels = sorted(last_touch_weights.items(), key=lambda x: x[1], reverse=True)
top_channels = sorted_channels[:3]
bottom_channels = sorted_channels[-3:]
print("🏆 表现最佳的渠道 (按最后接触归因):")
for channel, weight in top_channels:
cost = channel_costs.get(channel, 0)
revenue = channel_revenues.get(channel, 0)
roi = (revenue - cost) / cost * 100 if cost > 0 else 0
print(f" • {channel}: 权重={weight*100:.1f}%, ROI={roi:.1f}%")
print("\n⚠️ 需要优化的渠道:")
for channel, weight in bottom_channels:
if weight > 0: # 忽略权重为0的渠道
cost = channel_costs.get(channel, 0)
revenue = channel_revenues.get(channel, 0)
roi = (revenue - cost) / cost * 100 if cost > 0 else 0
print(f" • {channel}: 权重={weight*100:.1f}%, ROI={roi:.1f}%")
# 优化建议
print("\n📈 优化建议:")
print(" 1. 考虑增加对高ROI、高权重渠道的投入")
print(" 2. 分析低权重渠道的原因:定位问题、创意问题还是受众问题")
print(" 3. 测试不同归因模型以获得更全面的视角")
print(" 4. 定期监控和调整渠道策略")
print(f"\n✅ 基础归因分析完成!")
print(f"📁 结果文件已保存至 examples/ 目录")
print(f"📊 可视化图表已生成")
if __name__ == "__main__":
main()#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Core Attribution Analysis Engine
Comprehensive marketing attribution analysis with multiple attribution models
"""
import pandas as pd
import numpy as np
from datetime import datetime
from collections import defaultdict
import warnings
warnings.filterwarnings('ignore')
class AttributionAnalyzer:
"""Core attribution analysis engine with multiple attribution models"""
def __init__(self):
"""Initialize the attribution analyzer"""
self.attribution_models = {}
self.attribution_results = {}
self.channel_performance = {}
self.customer_paths = None
def load_and_validate_data(self, file_path, **kwargs):
"""
Load and validate marketing attribution data
Args:
file_path (str): Path to the CSV file
**kwargs: Additional parameters for pd.read_csv()
Returns:
pd.DataFrame: Validated attribution data
"""
print("=== 数据加载与验证 ===")
# Load data with different encodings
try:
df = pd.read_csv(file_path, encoding='utf-8', **kwargs)
except UnicodeDecodeError:
try:
df = pd.read_csv(file_path, encoding='gbk', **kwargs)
except:
df = pd.read_csv(file_path, encoding='latin-1', **kwargs)
print(f"数据集形状: {df.shape}")
print(f"列名: {list(df.columns)}")
# Validate required columns
required_columns = ['user_id', 'timestamp', 'channel', 'conversion_status']
missing_columns = [col for col in required_columns if col not in df.columns]
if missing_columns:
# Try to find similar column names
column_mapping = {}
for required_col in missing_columns:
possible_matches = [col for col in df.columns
if required_col.lower() in col.lower() or
col.lower() in required_col.lower()]
if possible_matches:
column_mapping[possible_matches[0]] = required_col
print(f"自动识别列: {possible_matches[0]} -> {required_col}")
# Rename columns
df = df.rename(columns=column_mapping)
missing_columns = [col for col in required_columns if col not in df.columns]
if missing_columns:
raise ValueError(f"缺少必需的列: {missing_columns}")
# Data validation
print(f"\n数据质量检查:")
print(f"- 唯一用户数: {df['user_id'].nunique():,}")
print(f"- 唯一渠道数: {df['channel'].nunique()}")
print(f"- 总触点数: {len(df):,}")
# Check conversion rates
conversion_count = df['conversion_status'].sum()
conversion_rate = conversion_count / len(df) if len(df) > 0 else 0
print(f"- 转化触点数: {conversion_count:,}")
print(f"- 转化率: {conversion_rate:.2%}")
# Convert timestamp to datetime
try:
df['timestamp'] = pd.to_datetime(df['timestamp'])
print(f"- 时间范围: {df['timestamp'].min()} 到 {df['timestamp'].max()}")
except:
print("- 警告: 时间戳格式可能不标准")
# Add cost and value columns if not present
if 'cost' not in df.columns:
df['cost'] = 1.0 # Default cost
if 'conversion_value' not in df.columns:
df['conversion_value'] = np.where(df['conversion_status'] == 1, 1.0, 0.0)
return df
def build_customer_paths(self, df):
"""
Build customer journey paths from touchpoint data
Args:
df (pd.DataFrame): Validated attribution data
Returns:
pd.DataFrame: Customer journey paths
"""
print("\n=== 构建客户路径 ===")
# Sort by user and timestamp
df_sorted = df.sort_values(['user_id', 'timestamp'], ascending=[True, True])
# Add visit sequence number
df_sorted['visit_sequence'] = df_sorted.groupby('user_id').cumcount() + 1
# Build paths for each user
user_paths = []
user_conversions = {}
for user_id, user_data in df_sorted.groupby('user_id'):
# Get unique channels in chronological order
channels = user_data['channel'].unique().tolist()
# Determine if user converted
converted = user_data['conversion_status'].max()
conversion_value = user_data.loc[user_data['conversion_status'] == 1, 'conversion_value'].sum() if converted else 0
# Get total cost for this user
total_cost = user_data['cost'].sum()
# Build path with start and end markers
if converted:
path = ['开始'] + channels + ['成功转化']
else:
path = ['开始'] + channels + ['未转化']
user_paths.append({
'user_id': user_id,
'path': path,
'path_length': len(path),
'converted': converted,
'conversion_value': conversion_value,
'total_cost': total_cost,
'touchpoints': len(channels)
})
user_conversions[user_id] = converted
paths_df = pd.DataFrame(user_paths)
self.customer_paths = paths_df
print(f"构建了 {len(paths_df)} 条客户路径")
print(f"转化用户数: {paths_df['converted'].sum():,}")
print(f"转化率: {paths_df['converted'].mean():.2%}")
print(f"平均路径长度: {paths_df['path_length'].mean():.1f}")
return paths_df
def first_touch_attribution(self, paths_df):
"""
First-touch attribution model
Args:
paths_df (pd.DataFrame): Customer journey paths
Returns:
dict: Channel attribution weights
"""
print("\n=== 首次接触归因分析 ===")
attribution = defaultdict(float)
total_conversions = 0
total_value = 0.0
for _, row in paths_df.iterrows():
if row['converted'] and len(row['path']) > 2:
# Get the first real channel (after '开始')
first_channel = row['path'][1]
attribution[first_channel] += row['conversion_value']
total_conversions += 1
total_value += row['conversion_value']
# Normalize
for channel in attribution:
attribution[channel] /= total_value if total_value > 0 else 1
print(f"首次接触归因完成,总转化: {total_conversions:,}, 总价值: {total_value:,.2f}")
self.attribution_models['first_touch'] = dict(attribution)
return dict(attribution)
def last_touch_attribution(self, paths_df):
"""
Last-touch attribution model
Args:
paths_df (pd.DataFrame): Customer journey paths
Returns:
dict: Channel attribution weights
"""
print("\n=== 最后接触归因分析 ===")
attribution = defaultdict(float)
total_conversions = 0
total_value = 0.0
for _, row in paths_df.iterrows():
if row['converted'] and len(row['path']) > 2:
# Get the last real channel (before '成功转化')
last_channel = row['path'][-2]
attribution[last_channel] += row['conversion_value']
total_conversions += 1
total_value += row['conversion_value']
# Normalize
for channel in attribution:
attribution[channel] /= total_value if total_value > 0 else 1
print(f"最后接触归因完成,总转化: {total_conversions:,}, 总价值: {total_value:,.2f}")
self.attribution_models['last_touch'] = dict(attribution)
return dict(attribution)
def linear_attribution(self, paths_df):
"""
Linear attribution model (equal distribution)
Args:
paths_df (pd.DataFrame): Customer journey paths
Returns:
dict: Channel attribution weights
"""
print("\n=== 线性归因分析 ===")
attribution = defaultdict(float)
total_conversions = 0
total_value = 0.0
for _, row in paths_df.iterrows():
if row['converted'] and len(row['path']) > 2:
# Get all real channels (exclude '开始' and '成功转化')
channels = [ch for ch in row['path'] if ch not in ['开始', '成功转化', '未转化']]
if channels:
weight = row['conversion_value'] / len(channels)
for channel in channels:
attribution[channel] += weight
total_conversions += 1
total_value += row['conversion_value']
# Normalize
for channel in attribution:
attribution[channel] /= total_value if total_value > 0 else 1
print(f"线性归因完成,总转化: {total_conversions:,}, 总价值: {total_value:,.2f}")
self.attribution_models['linear'] = dict(attribution)
return dict(attribution)
def time_decay_attribution(self, paths_df, decay_factor=0.5):
"""
Time-decay attribution model
Args:
paths_df (pd.DataFrame): Customer journey paths
decay_factor (float): Decay factor for time weighting
Returns:
dict: Channel attribution weights
"""
print(f"\n=== 时间衰减归因分析 (衰减因子: {decay_factor}) ===")
attribution = defaultdict(float)
total_conversions = 0
total_value = 0.0
for _, row in paths_df.iterrows():
if row['converted'] and len(row['path']) > 2:
# Get all real channels
channels = [ch for ch in row['path'] if ch not in ['开始', '成功转化', '未转化']]
if channels:
# Calculate weights based on position (last channel gets highest weight)
positions = range(len(channels))
weights = [decay_factor ** (len(channels) - 1 - pos) for pos in positions]
total_weight = sum(weights)
# Normalize weights
weights = [w / total_weight for w in weights]
# Distribute conversion value
for channel, weight in zip(channels, weights):
attribution[channel] += weight * row['conversion_value']
total_conversions += 1
total_value += row['conversion_value']
# Normalize
for channel in attribution:
attribution[channel] /= total_value if total_value > 0 else 1
print(f"时间衰减归因完成,总转化: {total_conversions:,}, 总价值: {total_value:,.2f}")
self.attribution_models['time_decay'] = dict(attribution)
return dict(attribution)
def position_based_attribution(self, paths_df, first_weight=0.4, last_weight=0.4):
"""
Position-based attribution model
Args:
paths_df (pd.DataFrame): Customer journey paths
first_weight (float): Weight for first touch
last_weight (float): Weight for last touch
Returns:
dict: Channel attribution weights
"""
print(f"\n=== 位置归因分析 (首次: {first_weight}, 最后: {last_weight}) ===")
attribution = defaultdict(float)
total_conversions = 0
total_value = 0.0
for _, row in paths_df.iterrows():
if row['converted'] and len(row['path']) > 2:
# Get all real channels
channels = [ch for ch in row['path'] if ch not in ['开始', '成功转化', '未转化']]
if channels:
middle_weight = 1 - first_weight - last_weight
num_middle = max(0, len(channels) - 2)
weights = []
for i, channel in enumerate(channels):
if i == 0: # First channel
weights.append(first_weight)
elif i == len(channels) - 1: # Last channel
weights.append(last_weight)
else: # Middle channels
weights.append(middle_weight / num_middle if num_middle > 0 else 0)
# Normalize weights
total_weight = sum(weights)
if total_weight > 0:
weights = [w / total_weight for w in weights]
# Distribute conversion value
for channel, weight in zip(channels, weights):
attribution[channel] += weight * row['conversion_value']
total_conversions += 1
total_value += row['conversion_value']
# Normalize
for channel in attribution:
attribution[channel] /= total_value if total_value > 0 else 1
print(f"位置归因完成,总转化: {total_conversions:,}, 总价值: {total_value:,.2f}")
self.attribution_models['position_based'] = dict(attribution)
return dict(attribution)
def calculate_channel_performance(self, df, paths_df):
"""
Calculate comprehensive channel performance metrics
Args:
df (pd.DataFrame): Original touchpoint data
paths_df (pd.DataFrame): Customer journey paths
Returns:
pd.DataFrame: Channel performance metrics
"""
print("\n=== 渠道性能分析 ===")
# Basic channel metrics
channel_metrics = []
for channel in df['channel'].unique():
# Get all users who touched this channel
channel_users = set(df[df['channel'] == channel]['user_id'])
# Get conversions from this channel
converted_users = set(paths_df[paths_df['converted']]['user_id'])
channel_converted_users = channel_users.intersection(converted_users)
# Calculate metrics
total_touchpoints = len(df[df['channel'] == channel])
total_cost = df[df['channel'] == channel]['cost'].sum()
conversion_value = paths_df[paths_df['user_id'].isin(channel_converted_users)]['conversion_value'].sum()
channel_metrics.append({
'channel': channel,
'total_touchpoints': total_touchpoints,
'unique_users': len(channel_users),
'conversions': len(channel_converted_users),
'conversion_rate': len(channel_converted_users) / len(channel_users) if len(channel_users) > 0 else 0,
'total_cost': total_cost,
'conversion_value': conversion_value,
'cpa': total_cost / len(channel_converted_users) if len(channel_converted_users) > 0 else float('inf'),
'roi': (conversion_value - total_cost) / total_cost if total_cost > 0 else 0,
'avg_touchpoints_per_user': total_touchpoints / len(channel_users) if len(channel_users) > 0 else 0
})
performance_df = pd.DataFrame(channel_metrics)
performance_df = performance_df.sort_values('conversion_value', ascending=False)
self.channel_performance = performance_df
print("渠道性能排名:")
for idx, row in performance_df.iterrows():
print(f"{row['channel']}: 转化率={row['conversion_rate']:.2%}, CPA={row['cpa']:.2f}, ROI={row['roi']:.2f}")
return performance_df
def compare_attribution_models(self, paths_df):
"""
Run and compare multiple attribution models
Args:
paths_df (pd.DataFrame): Customer journey paths
Returns:
pd.DataFrame: Comparison of all attribution models
"""
print("\n=== 多模型归因比较 ===")
# Run all attribution models
self.first_touch_attribution(paths_df)
self.last_touch_attribution(paths_df)
self.linear_attribution(paths_df)
self.time_decay_attribution(paths_df)
self.position_based_attribution(paths_df)
# Create comparison table
all_channels = set()
for model_result in self.attribution_models.values():
all_channels.update(model_result.keys())
comparison_data = []
for channel in all_channels:
row = {'channel': channel}
for model_name, attribution in self.attribution_models.items():
row[model_name] = attribution.get(channel, 0.0)
comparison_data.append(row)
comparison_df = pd.DataFrame(comparison_data)
comparison_df = comparison_df.fillna(0.0)
# Calculate statistics
print("\n各模型归因权重比较:")
print(comparison_df.set_index('channel'))
# Calculate average attribution
model_columns = list(self.attribution_models.keys())
comparison_df['average_attribution'] = comparison_df[model_columns].mean(axis=1)
comparison_df['std_deviation'] = comparison_df[model_columns].std(axis=1)
print(f"\n归因权重标准差 (模型间差异):")
print(comparison_df[['channel', 'average_attribution', 'std_deviation']].sort_values('std_deviation', ascending=False))
self.attribution_results = comparison_df
return comparison_df
def generate_attribution_summary(self, df):
"""
Generate comprehensive attribution analysis summary
Args:
df (pd.DataFrame): Original touchpoint data
Returns:
dict: Comprehensive attribution summary
"""
print("\n=== 生成归因分析摘要 ===")
if self.customer_paths is None:
paths_df = self.build_customer_paths(df)
else:
paths_df = self.customer_paths
if not self.attribution_results.empty:
performance_df = self.calculate_channel_performance(df, paths_df)
else:
performance_df = self.compare_attribution_models(paths_df)
performance_df = self.calculate_channel_performance(df, paths_df)
# Generate summary statistics
total_users = df['user_id'].nunique()
total_conversions = paths_df['converted'].sum()
overall_conversion_rate = total_conversions / total_users if total_users > 0 else 0
total_conversion_value = paths_df[paths_df['converted']]['conversion_value'].sum()
total_cost = df['cost'].sum()
summary = {
'analysis_metadata': {
'total_users': total_users,
'total_conversions': total_conversions,
'overall_conversion_rate': overall_conversion_rate,
'total_conversion_value': total_conversion_value,
'total_cost': total_cost,
'overall_roi': (total_conversion_value - total_cost) / total_cost if total_cost > 0 else 0,
'unique_channels': df['channel'].nunique(),
'analysis_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
},
'attribution_models': self.attribution_models,
'channel_performance': performance_df.to_dict('records'),
'model_comparison': self.attribution_results.to_dict('records') if hasattr(self.attribution_results, 'to_dict') else {},
'recommended_actions': self._generate_recommendations(performance_df)
}
print("\n=== 分析摘要 ===")
print(f"总用户数: {total_users:,}")
print(f"转化用户数: {total_conversions:,}")
print(f"整体转化率: {overall_conversion_rate:.2%}")
print(f"总转化价值: {total_conversion_value:,.2f}")
print(f"总营销成本: {total_cost:,.2f}")
print(f"整体ROI: {(total_conversion_value - total_cost) / total_cost * 100:.1f}%")
return summary
def _generate_recommendations(self, performance_df):
"""
Generate actionable recommendations based on channel performance
Args:
performance_df (pd.DataFrame): Channel performance metrics
Returns:
list: Actionable recommendations
"""
recommendations = []
# High ROI channels (increase investment)
high_roi_channels = performance_df[performance_df['roi'] > 2.0]
if not high_roi_channels.empty:
for _, row in high_roi_channels.iterrows():
recommendations.append({
'type': 'increase_investment',
'channel': row['channel'],
'reason': f"高ROI ({row['roi']:.1f}x), 考虑增加投资",
'priority': 'high' if row['roi'] > 3.0 else 'medium'
})
# Low CPA channels (efficient acquisition)
low_cpa_channels = performance_df[performance_df['cpa'] < performance_df['cpa'].median()]
if not low_cpa_channels.empty:
for _, row in low_cpa_channels.iterrows():
recommendations.append({
'type': 'efficient_acquisition',
'channel': row['channel'],
'reason': f"低CPA ({row['cpa']:.2f}), 获客效率高",
'priority': 'medium'
})
# High conversion rate channels (optimize funnel)
high_conv_channels = performance_df[performance_df['conversion_rate'] > performance_df['conversion_rate'].median()]
if not high_conv_channels.empty:
for _, row in high_conv_channels.iterrows():
recommendations.append({
'type': 'optimize_funnel',
'channel': row['channel'],
'reason': f"高转化率 ({row['conversion_rate']:.2%}), 优化转化漏斗",
'priority': 'medium'
})
# Negative ROI channels (reduce investment)
negative_roi_channels = performance_df[performance_df['roi'] < 0]
if not negative_roi_channels.empty:
for _, row in negative_roi_channels.iterrows():
recommendations.append({
'type': 'reduce_investment',
'channel': row['channel'],
'reason': f"负ROI ({row['roi']:.1f}x), 考虑减少或停止投资",
'priority': 'high'
})
# Underperforming channels (investigate issues)
low_conv_channels = performance_df[performance_df['conversion_rate'] < 0.01]
if not low_conv_channels.empty:
for _, row in low_conv_channels.iterrows():
recommendations.append({
'type': 'investigate_issues',
'channel': row['channel'],
'reason': f"转化率低 ({row['conversion_rate']:.2%}), 调查渠道表现问题",
'priority': 'low'
})
return recommendations
def run_complete_analysis(self, file_path, **kwargs):
"""
Run complete attribution analysis pipeline
Args:
file_path (str): Path to attribution data file
**kwargs: Additional parameters for data loading
Returns:
dict: Complete attribution analysis results
"""
print("🚀 开始完整归因分析")
print("=" * 50)
# 1. Load and validate data
df = self.load_and_validate_data(file_path, **kwargs)
# 2. Build customer paths
paths_df = self.build_customer_paths(df)
# 3. Calculate channel performance
performance_df = self.calculate_channel_performance(df, paths_df)
# 4. Compare attribution models
comparison_df = self.compare_attribution_models(paths_df)
# 5. Generate comprehensive summary
summary = self.generate_attribution_summary(df)
print(f"\n✅ 归因分析完成!")
print(f"分析了 {df['user_id'].nunique():,} 个用户的 {len(df):,} 个触点")
print(f"识别了 {df['channel'].nunique()} 个营销渠道")
print(f"运行了 {len(self.attribution_models)} 种归因模型")
return {
'raw_data': df,
'customer_paths': paths_df,
'channel_performance': performance_df,
'attribution_comparison': comparison_df,
'attribution_models': self.attribution_models,
'summary': summary
}
def main():
"""Example usage of attribution analyzer"""
analyzer = AttributionAnalyzer()
# Example with sample data
file_path = "渠道转化.csv"
if pd.io.common.file_exists(file_path):
results = analyzer.run_complete_analysis(file_path)
print(f"\n归因权重对比:")
for model, attribution in results['attribution_models'].items():
print(f"{model}: {attribution}")
else:
print(f"数据文件未找到: {file_path}")
print("请提供正确的营销触点数据文件")
if __name__ == "__main__":
main()#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Markov Chain Attribution Analysis
Advanced attribution analysis using Markov chain models and removal effects
"""
import pandas as pd
import numpy as np
from scipy.linalg import inv
import networkx as nx
import warnings
warnings.filterwarnings('ignore')
class MarkovChainAttributor:
"""Markov chain-based attribution analysis"""
def __init__(self):
"""Initialize the Markov chain attributor"""
self.transition_matrix = None
self.removal_effects = {}
self.attribution_weights = {}
self.channel_graph = None
def build_transition_matrix(self, paths_df):
"""
Build transition probability matrix from customer paths
Args:
paths_df (pd.DataFrame): Customer journey paths
Returns:
pd.DataFrame: Transition probability matrix
"""
print("\n=== 构建马尔可夫转移矩阵 ===")
# Extract all unique states (channels + start + end states)
all_states = set()
for path in paths_df['path']:
all_states.update(path)
# Initialize transition counts
transition_counts = {}
for state1 in all_states:
for state2 in all_states:
transition_counts[f"{state1}>{state2}"] = 0
# Count transitions
for path in paths_df['path']:
for i in range(len(path) - 1):
current_state = path[i]
next_state = path[i + 1]
transition_counts[f"{current_state}>{next_state}"] += 1
# Calculate transition probabilities
transition_probabilities = {}
state_totals = {}
# Calculate totals for each state (excluding end states)
for transition, count in transition_counts.items():
state = transition.split('>')[0]
if state not in ['成功转化', '未转化']:
state_totals[state] = state_totals.get(state, 0) + count
# Calculate probabilities
for transition, count in transition_counts.items():
state = transition.split('>')[0]
if state in state_totals and state_totals[state] > 0:
transition_probabilities[transition] = count / state_totals[state]
else:
transition_probabilities[transition] = 0
# Create transition matrix
states_list = sorted(list(all_states))
transition_matrix = pd.DataFrame(0.0, index=states_list, columns=states_list)
# Fill transition matrix
for transition, prob in transition_probabilities.items():
from_state, to_state = transition.split('>')
transition_matrix.at[from_state, to_state] = prob
# Set diagonal elements for absorbing states
for state in ['成功转化', '未转化']:
transition_matrix.at[state, state] = 1.0
# Set diagonal elements for other states to ensure rows sum to 1
for state in states_list:
if state not in ['成功转化', '未转化']:
row_sum = transition_matrix.loc[state].sum()
if row_sum < 1.0:
# Add remaining probability to '未转化'
transition_matrix.at[state, '未转化'] = 1.0 - row_sum
self.transition_matrix = transition_matrix
print(f"转移矩阵形状: {transition_matrix.shape}")
print(f"状态数量: {len(states_list)}")
print(f"状态列表: {states_list}")
return transition_matrix
def calculate_removal_effects(self, paths_df, base_conversion_rate):
"""
Calculate removal effects for each channel
Args:
paths_df (pd.DataFrame): Customer journey paths
base_conversion_rate (float): Base conversion rate
Returns:
dict: Removal effects for each channel
"""
print("\n=== 计算移除效应 ===")
transition_matrix = self.transition_matrix.copy()
channels = [state for state in transition_matrix.columns
if state not in ['开始', '未转化', '成功转化']]
removal_effects = {}
base_cvr = base_conversion_rate
for channel in channels:
# Create matrix without the channel
reduced_matrix = transition_matrix.drop(channel, axis=0).drop(channel, axis=1)
# Adjust probabilities for remaining channels
for col in reduced_matrix.columns:
if col not in ['未转化', '成功转化']:
row_sum = reduced_matrix.loc[col].sum()
if row_sum < 1.0:
# Add remaining probability to '未转化'
reduced_matrix.at[col, '未转化'] = 1.0 - row_sum
# Ensure '未转化' state absorbs properly
reduced_matrix.at['未转化', '未转化'] = 1.0
# Calculate conversion rate with channel removed
try:
# Split matrix into transient and absorbing states
transient_states = [state for state in reduced_matrix.index
if state not in ['未转化', '成功转化']]
absorbing_states = ['未转化', '成功转化']
if len(transient_states) > 0:
Q = reduced_matrix.loc[transient_states, transient_states].values
R = reduced_matrix.loc[transient_states, absorbing_states].values
# Calculate fundamental matrix
I = np.identity(len(Q))
N = inv(I - Q)
# Calculate absorption probabilities
B = np.dot(N, R)
# Get probability of successful conversion from start
start_index = transient_states.index('开始') if '开始' in transient_states else 0
success_index = absorbing_states.index('成功转化')
new_cvr = B[start_index, success_index]
else:
new_cvr = 0.0
except:
# Fallback calculation
new_cvr = 0.0
# Calculate removal effect
removal_effect = 1 - (new_cvr / base_cvr) if base_cvr > 0 else 0
removal_effects[channel] = removal_effect
print(f"{channel}: 移除效应 = {removal_effect:.4f}")
self.removal_effects = removal_effects
return removal_effects
def calculate_attribution_weights(self):
"""
Calculate attribution weights based on removal effects
Returns:
dict: Attribution weights for each channel
"""
print("\n=== 计算马尔可夫归因权重 ===")
if not self.removal_effects:
print("错误: 需要先计算移除效应")
return {}
# Normalize removal effects to get attribution weights
total_effect = sum(self.removal_effects.values())
if total_effect == 0:
print("警告: 总移除效应为0,使用均匀权重")
channels = list(self.removal_effects.keys())
attribution_weights = {channel: 1.0/len(channels) for channel in channels}
else:
attribution_weights = {
channel: effect / total_effect
for channel, effect in self.removal_effects.items()
}
# Sort by attribution weight
sorted_weights = dict(sorted(attribution_weights.items(),
key=lambda x: x[1], reverse=True))
print("马尔可夫归因权重:")
for channel, weight in sorted_weights.items():
print(f" {channel}: {weight:.4f} ({weight*100:.1f}%)")
self.attribution_weights = sorted_weights
return sorted_weights
def analyze_channel_transitions(self):
"""
Analyze channel transition patterns
Returns:
dict: Channel transition analysis
"""
print("\n=== 渠道转换分析 ===")
if self.transition_matrix is None:
print("错误: 需要先构建转移矩阵")
return {}
channels = [state for state in self.transition_matrix.columns
if state not in ['开始', '未转化', '成功转化']]
transition_analysis = {}
for channel in channels:
# Outgoing transitions
outgoing = self.transition_matrix.loc[channel].drop(
index=['开始', '未转化', '成功转化', channel], errors='ignore'
)
outgoing_transitions = outgoing[outgoing > 0].sort_values(ascending=False)
# Incoming transitions
incoming = self.transition_matrix[channel].drop(
index=['开始', '未转化', '成功转化', channel], errors='ignore'
)
incoming_transitions = incoming[incoming > 0].sort_values(ascending=False)
transition_analysis[channel] = {
'outgoing_transitions': outgoing_transitions.to_dict(),
'incoming_transitions': incoming_transitions.to_dict(),
'total_outgoing_prob': outgoing_transitions.sum(),
'total_incoming_prob': incoming_transitions.sum()
}
# Find most common transitions
all_transitions = []
for channel in channels:
outgoing = transition_analysis[channel]['outgoing_transitions']
for target, prob in outgoing.items():
all_transitions.append({
'from': channel,
'to': target,
'probability': prob
})
all_transitions.sort(key=lambda x: x['probability'], reverse=True)
print("最常见的渠道转换:")
for i, transition in enumerate(all_transitions[:10]):
print(f" {i+1}. {transition['from']} → {transition['to']}: {transition['probability']:.4f}")
return {
'channel_transitions': transition_analysis,
'top_transitions': all_transitions[:10]
}
def build_channel_graph(self):
"""
Build network graph of channel transitions
Returns:
networkx.DiGraph: Channel transition graph
"""
print("\n=== 构建渠道转换图 ===")
if self.transition_matrix is None:
print("错误: 需要先构建转移矩阵")
return None
channels = [state for state in self.transition_matrix.columns
if state not in ['开始', '未转化', '成功转化']]
# Create directed graph
G = nx.DiGraph()
# Add nodes
for channel in channels:
G.add_node(channel)
# Add edges based on transition probabilities
for channel in channels:
outgoing = self.transition_matrix.loc[channel].drop(
index=['开始', '未转化', '成功转化', channel], errors='ignore'
)
for target, prob in outgoing.items():
if prob > 0.01: # Only include significant transitions
G.add_edge(channel, target, weight=prob)
# Calculate network metrics
network_metrics = {}
for node in G.nodes():
network_metrics[node] = {
'in_degree': G.in_degree(node),
'out_degree': G.out_degree(node),
'clustering': nx.clustering(G, node),
'betweenness': nx.betweenness_centrality(G).get(node, 0)
}
print(f"渠道转换图构建完成:")
print(f" 节点数: {G.number_of_nodes()}")
print(f" 边数: {G.number_of_edges()}")
print(f" 强连通分量: {nx.number_strongly_connected_components(G)}")
self.channel_graph = G
return G
def simulate_attribution_scenarios(self, scenarios):
"""
Simulate different attribution scenarios
Args:
scenarios (dict): Dictionary of scenario configurations
Returns:
dict: Scenario simulation results
"""
print("\n=== 模拟归因场景 ===")
if not self.transition_matrix:
print("错误: 需要先构建转移矩阵")
return {}
results = {}
base_matrix = self.transition_matrix.copy()
for scenario_name, config in scenarios.items():
print(f"\n模拟场景: {scenario_name}")
# Modify transition matrix based on scenario
modified_matrix = base_matrix.copy()
for channel, modifications in config.items():
if channel in modified_matrix.index:
for target_channel, new_prob in modifications.items():
if target_channel in modified_matrix.columns:
modified_matrix.at[channel, target_channel] = new_prob
# Calculate new conversion rate
try:
transient_states = [state for state in modified_matrix.index
if state not in ['未转化', '成功转化']]
absorbing_states = ['未转化', '成功转化']
if len(transient_states) > 0:
Q = modified_matrix.loc[transient_states, transient_states].values
R = modified_matrix.loc[transient_states, absorbing_states].values
I = np.identity(len(Q))
N = inv(I - Q)
B = np.dot(N, R)
start_index = transient_states.index('开始') if '开始' in transient_states else 0
success_index = absorbing_states.index('成功转化')
simulated_cvr = B[start_index, success_index]
else:
simulated_cvr = 0.0
except Exception as e:
print(f"场景计算失败: {str(e)}")
simulated_cvr = 0.0
results[scenario_name] = {
'conversion_rate': simulated_cvr,
'configuration': config
}
return results
def generate_markov_insights(self):
"""
Generate insights from Markov chain analysis
Returns:
dict: Markov chain analysis insights
"""
print("\n=== 生成马尔可夫链洞察 ===")
insights = {}
if self.attribution_weights:
# Identify dominant channels
sorted_channels = sorted(self.attribution_weights.items(),
key=lambda x: x[1], reverse=True)
insights['dominant_channels'] = sorted_channels[:3]
insights['underperforming_channels'] = sorted_channels[-3:]
if self.transition_matrix is not None:
# Analyze conversion bottlenecks
channels = [state for state in self.transition_matrix.columns
if state not in ['开始', '未转化', '成功转化']]
bottlenecks = []
for channel in channels:
direct_to_conversion = self.transition_matrix.at[channel, '成功转化']
if direct_to_conversion > 0:
bottlenecks.append((channel, direct_to_conversion))
bottlenecks.sort(key=lambda x: x[1], reverse=True)
insights['conversion_bottlenecks'] = bottlenecks
if self.channel_graph is not None:
# Network insights
centrality = nx.betweenness_centrality(self.channel_graph)
insights['most_influential_channels'] = sorted(
centrality.items(), key=lambda x: x[1], reverse=True
)[:3]
print("马尔可夫链分析洞察:")
if 'dominant_channels' in insights:
print(" 主导渠道:")
for channel, weight in insights['dominant_channels']:
print(f" {channel}: {weight:.4f}")
return insights
def run_complete_markov_analysis(self, paths_df):
"""
Run complete Markov chain attribution analysis
Args:
paths_df (pd.DataFrame): Customer journey paths
Returns:
dict: Complete Markov chain analysis results
"""
print("🔗 开始马尔可夫链归因分析")
print("=" * 50)
# Calculate base conversion rate
base_conversion_rate = paths_df['converted'].mean()
# 1. Build transition matrix
transition_matrix = self.build_transition_matrix(paths_df)
# 2. Calculate removal effects
removal_effects = self.calculate_removal_effects(paths_df, base_conversion_rate)
# 3. Calculate attribution weights
attribution_weights = self.calculate_attribution_weights()
# 4. Analyze channel transitions
transition_analysis = self.analyze_channel_transitions()
# 5. Build channel graph
channel_graph = self.build_channel_graph()
# 6. Generate insights
insights = self.generate_markov_insights()
results = {
'transition_matrix': transition_matrix,
'removal_effects': removal_effects,
'attribution_weights': attribution_weights,
'transition_analysis': transition_analysis,
'channel_graph': channel_graph,
'insights': insights,
'base_conversion_rate': base_conversion_rate
}
print(f"\n✅ 马可夫链分析完成!")
print(f"基础转化率: {base_conversion_rate:.2%}")
print(f"识别了 {len(attribution_weights)} 个渠道的归因权重")
print(f"分析了 {len(transition_analysis['channel_transitions'])} 个渠道的转换模式")
return results
def main():
"""Example usage of Markov chain attributor"""
attributor = MarkovChainAttributor()
# Create sample data for demonstration
sample_paths = [
['开始', '付费搜索', '社交媒体', '成功转化'],
['开始', '社交媒体', '付费搜索', '成功转化'],
['开始', '邮件营销', '社交媒体', '付费搜索', '成功转化'],
['开始', '付费搜索', '未转化'],
['开始', '社交媒体', '未转化'],
['开始', '邮件营销', '未转化']
]
paths_df = pd.DataFrame({
'user_id': range(len(sample_paths)),
'path': sample_paths,
'converted': [1, 1, 1, 0, 0, 0],
'conversion_value': [100, 150, 80, 0, 0, 0]
})
results = attributor.run_complete_markov_analysis(paths_df)
print(f"\n马尔可夫归因权重:")
for channel, weight in results['attribution_weights'].items():
print(f" {channel}: {weight:.4f}")
if __name__ == "__main__":
main()user_id,timestamp,channel,conversion_status,conversion_value,cost
USER001,2024-01-15T10:30:00Z,paid_search,0,0,50
USER001,2024-01-16T14:20:00Z,social_media,0,0,30
USER001,2024-01-18T09:15:00Z,email,1,1000,10
USER002,2024-01-12T11:45:00Z,paid_search,0,0,45
USER002,2024-01-14T16:30:00Z,social_media,1,800,35
USER002,2024-01-16T10:20:00Z,email,0,0,15
USER002,2024-01-18T13:25:00Z,social_media,0,0,25
USER003,2024-01-10T08:00:00Z,organic_search,0,0,0
USER003,2024-01-12T14:30:00Z,content_marketing,0,0,20
USER003,2024-01-14T11:15:00Z,content_marketing,0,0,15
USER003,2024-01-16T09:45:00Z,paid_search,1,2000,55
USER004,2024-01-08T20:15:00Z,social_media,0,0,25
USER004,2024-01-10T12:30:00Z,email,0,0,8
USER004,2024-01-12T18:45:00Z,social_media,0,0,30
USER004,2024-01-14T15:30:00Z,paid_search,0,0,40
USER004,2024-01-18T11:00:00Z,email,1,500,12
USER005,2024-01-15T13:45:00Z,content_marketing,0,0,18
USER005,2024-01-17T16:20:00Z,social_media,0,0,28
USER005,2024-01-19T10:10:00Z,paid_search,0,0,35
USER005,2024-01-21T14:30:00Z,email,0,0,10
USER005,2024-01-23T11:45:00Z,content_marketing,1,1200,22
USER006,2024-01-06T09:00:00Z,paid_search,0,0,60
USER006,2024-01-08T13:15:00Z,organic_search,0,0,0
USER006,2024-01-10T16:45:00Z,email,1,600,25
USER007,2024-01-11T11:30:00Z,social_media,0,0,20
USER007,2024-01-13T14:20:00Z,content_marketing,0,0,15
USER007,2024-01-15T10:45:00Z,email,0,0,12
USER007,2024-01-17T09:30:00Z,social_media,1,900,28
USER007,2024-01-19T15:00:00Z,email,0,0,8
USER007,2024-01-21T12:15:00Z,content_marketing,0,0,18
USER008,2024-01-09T15:20:00Z,paid_search,0,0,40
USER008,2024-01-11T17:45:00Z,social_media,0,0,30
USER008,2024-01-13T10:30:00Z,email,0,0,15
USER008,2024-01-15T13:20:00Z,content_marketing,0,0,20
USER008,2024-01-17T15:10:00Z,social_media,1,750,32
USER009,2024-01-07T12:00:00Z,organic_search,0,0,0
USER009,2024-01-09T16:30:00Z,paid_search,0,0,50
USER009,2024-01-11T14:15:00Z,email,0,0,18
USER009,2024-01-13T18:45:00Z,content_marketing,0,0,25
USER009,2024-01-15T09:00:00Z,paid_search,1,3000,65
USER010,2024-01-14T08:30:00Z,social_media,0,0,35
USER010,2024-01-16T12:00:00Z,email,1,400,20
USER010,2024-01-18T15:45:00Z,social_media,0,0,28
USER010,2024-01-20T11:15:00Z,content_marketing,0,0,22
USER011,2024-01-05T10:00:00Z,email,0,0,12
USER011,2024-01-07T14:30:00Z,content_marketing,0,0,25
USER011,2024-01-09T11:45:00Z,paid_search,0,0,55
USER011,2024-01-11T16:00:00Z,social_media,0,0,30
USER011,2024-01-13T13:15:00Z,email,0,0,10
USER011,2024-01-15T10:30:00Z,paid_search,0,0,45
USER011,2024-01-17T12:45:00Z,content_marketing,1,1100,35
USER012,2024-01-08T17:00:00Z,affiliate,0,0,40
USER012,2024-01-10T10:30:00Z,paid_search,0,0,50
USER012,2024-01-12T14:45:00Z,social_media,0,0,30
USER012,2024-01-14T11:00:00Z,email,1,800,18
USER013,2024-01-03T09:15:00Z,display,0,0,25
USER013,2024-01-05T13:30:00Z,social_media,0,0,20
USER013,2024-01-07T16:45:00Z,paid_search,0,0,45
USER013,2024-01-09T11:15:00Z,email,1,2200,28
USER014,2024-01-16T19:00:00Z,social_media,0,0,30
USER014,2024-01-18T12:30:00Z,email,0,0,16
USER014,2024-01-20T14:45:00Z,content_marketing,0,0,22
USER014,2024-01-22T11:00:00Z,social_media,1,1600,25
USER015,2024-01-12T13:20:00Z,email,0,0,14
USER015,2024-01-14T17:45:00Z,paid_search,1,2200,48
USER015,2024-01-16T10:10:00Z,content_marketing,0,0,18
USER015,2024-01-18T14:20:00Z,email,0,0,12
USER016,2024-01-04T10:45:00Z,paid_search,0,0,35
USER016,2024-01-06T15:30:00Z,social_media,0,0,25
USER016,2024-01-08T11:00:00Z,email,0,0,15
USER016,2024-01-10T16:15:00Z,content_marketing,0,0,20
USER016,2024-01-12T14:00:00Z,paid_search,1,2800,52
USER017,2024-01-01T08:00:00Z,organic_search,0,0,0
USER017,2024-01-03T12:30:00Z,content_marketing,0,0,15
USER017,2024-01-05T15:45:00Z,social_media,0,0,20
USER017,2024-01-07T10:15:00Z,paid_search,0,0,40
USER017,2024-01-09T13:30:00Z,email,0,0,12
USER017,2024-01-11T16:00:00Z,social_media,1,1400,38
USER018,2024-01-14T15:30:00Z,email,0,0,16
USER018,2024-01-16T18:45:00Z,social_media,0,0,28
USER018,2024-01-18T20:15:00Z,content_marketing,0,0,24
USER018,2024-01-20T11:30:00Z,paid_search,1,1800,42
USER018,2024-01-22T13:45:00Z,email,0,0,14
USER018,2024-01-24T12:00:00Z,social_media,0,0,22
USER019,2024-01-09T17:45:00Z,affiliate,0,0,35
USER019,2024-01-11T10:30:00Z,paid_search,0,0,45
USER019,2024-01-13T14:15:00Z,email,0,0,20
USER019,2024-01-15T19:00:00Z,social_media,0,0,25
USER019,2024-01-17T11:30:00Z,content_marketing,1,1300,33
USER020,2024-01-06T18:30:00Z,email,0,0,12
USER020,2024-01-08T14:45:00Z,social_media,0,0,22
USER020,2024-01-10T12:00:00Z,content_marketing,0,0,18
USER020,2024-01-12T15:30:00Z,paid_search,0,0,38
USER020,2024-01-14T17:45:00Z,email,0,0,10
USER020,2024-01-16T19:30:00Z,social_media,1,2100,36#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Shapley Value Attribution Analysis
Game theory-based fair attribution using Shapley values
"""
import pandas as pd
import numpy as np
from itertools import combinations
import warnings
warnings.filterwarnings('ignore')
class ShapleyValueAttributor:
"""Shapley value-based attribution analysis"""
def __init__(self):
"""Initialize the Shapley value attributor"""
self.shapley_values = {}
self.attribution_weights = {}
self.channel_combinations = {}
self.combination_performance = {}
def calculate_shapley_values(self, paths_df, value_column='conversion_value'):
"""
Calculate Shapley values for channel attribution
Args:
paths_df (pd.DataFrame): Customer journey paths
value_column (str): Column name for conversion values
Returns:
dict: Shapley values for each channel
"""
print("\n=== 计算Shapley值归因 ===")
# Get all unique channels
all_channels = set()
for path in paths_df['path']:
for touchpoint in path:
if touchpoint not in ['开始', '成功转化', '未转化']:
all_channels.add(touchpoint)
channels = sorted(list(all_channels))
print(f"识别了 {len(channels)} 个渠道: {channels}")
# Calculate performance for each channel combination
self._calculate_combination_performance(paths_df, channels, value_column)
# Calculate Shapley values
shapley_values = {}
total_value = paths_df[value_column].sum()
if total_value == 0:
print("警告: 总转化价值为0,使用平均权重")
avg_value = 1.0 / len(channels)
shapley_values = {channel: avg_value for channel in channels}
else:
for channel in channels:
print(f"\n计算 {channel} 的Shapley值...")
shapley_value = self._calculate_channel_shapley_value(
channel, channels, total_value
)
shapley_values[channel] = shapley_value
print(f" {channel}: {shapley_value:.6f}")
# Normalize Shapley values to ensure they sum to 1
total_shapley = sum(shapley_values.values())
if total_shapley > 0:
attribution_weights = {
channel: value / total_shapley
for channel, value in shapley_values.items()
}
else:
attribution_weights = {channel: 1.0/len(channels) for channel in channels}
self.shapley_values = shapley_values
self.attribution_weights = attribution_weights
print(f"\nShapley值归因权重:")
for channel, weight in sorted(attribution_weights.items(),
key=lambda x: x[1], reverse=True):
print(f" {channel}: {weight:.4f} ({weight*100:.1f}%)")
return attribution_weights
def _calculate_combination_performance(self, paths_df, channels, value_column):
"""
Calculate performance metrics for each channel combination
Args:
paths_df (pd.DataFrame): Customer journey paths
channels (list): List of all channels
value_column (str): Column name for conversion values
"""
print("\n=== 计算渠道组合性能 ===")
# Generate all possible channel combinations
channel_combinations = []
for r in range(1, len(channels) + 1):
for combo in combinations(channels, r):
channel_combinations.append(list(combo))
# Add empty combination
channel_combinations.append([])
print(f"需要计算 {len(channel_combinations)} 个组合的性能...")
self.channel_combinations = {i: combo for i, combo in enumerate(channel_combinations)}
self.combination_performance = {}
for i, combination in enumerate(channel_combinations):
# Calculate total value generated by this combination
total_value = 0
total_conversions = 0
for _, row in paths_df.iterrows():
if row[value_column] > 0: # Only consider converting paths
path_channels = set([
touchpoint for touchpoint in row['path']
if touchpoint not in ['开始', '成功转化', '未转化']
])
# Check if this combination contributed to the conversion
if set(combination).issubset(path_channels):
total_value += row[value_column]
total_conversions += 1
self.combination_performance[i] = {
'combination': combination,
'total_value': total_value,
'conversions': total_conversions
}
if i % 100 == 0 and i > 0:
print(f" 已计算 {i}/{len(channel_combinations)} 个组合...")
print(f"渠道组合性能计算完成")
def _calculate_channel_shapley_value(self, channel, all_channels, total_value):
"""
Calculate Shapley value for a specific channel
Args:
channel (str): Target channel
all_channels (list): All channels
total_value (float): Total conversion value
Returns:
float: Shapley value for the channel
"""
n = len(all_channels)
shapley_value = 0.0
# Calculate marginal contributions for all combinations
for r in range(n):
combination_count = 0
# Generate all combinations of size r that don't include the channel
for combo in combinations([c for c in all_channels if c != channel], r):
combination_count += 1
# Performance without the channel
without_channel_performance = self._get_combination_performance(combo)
# Performance with the channel added
with_channel_combo = sorted(list(combo) + [channel])
with_channel_performance = self._get_combination_performance(with_channel_combo)
# Marginal contribution
marginal_contribution = with_channel_performance - without_channel_performance
# Weight in Shapley value calculation
weight = 1.0 / (combination_count * n) if combination_count > 0 else 0
shapley_value += weight * marginal_contribution
return shapley_value
def _get_combination_performance(self, combination):
"""
Get performance value for a specific channel combination
Args:
combination (list): Channel combination
Returns:
float: Performance value (total conversion value)
"""
# Convert combination to set for easier comparison
combo_set = set(combination)
# Find matching performance record
for i, perf in self.combination_performance.items():
if set(perf['combination']) == combo_set:
return perf['total_value']
return 0.0
def calculate_channel_synergy(self):
"""
Calculate channel synergy effects
Returns:
dict: Channel synergy analysis
"""
print("\n=== 计算渠道协同效应 ===")
if not self.combination_performance:
print("错误: 需要先计算组合性能")
return {}
synergy_analysis = {}
# Calculate individual channel performance
individual_performance = {}
for channel, perf in self.combination_performance.items():
if len(perf['combination']) == 1:
individual_performance[perf['combination'][0]] = perf['total_value']
# Calculate synergy for each channel pair
channels = list(individual_performance.keys())
for i in range(len(channels)):
for j in range(i + 1, len(channels)):
channel1, channel2 = channels[i], channels[j]
# Find performance of the pair
pair_performance = 0
for perf in self.combination_performance.values():
if (len(perf['combination']) == 2 and
set(perf['combination']) == {channel1, channel2}):
pair_performance = perf['total_value']
break
# Calculate expected additive performance
expected_additive = (individual_performance[channel1] +
individual_performance[channel2])
# Calculate synergy
if expected_additive > 0:
synergy_ratio = pair_performance / expected_additive
else:
synergy_ratio = 1.0
synergy_analysis[f"{channel1}_{channel2}"] = {
'channel1': channel1,
'channel2': channel2,
'individual1': individual_performance[channel1],
'individual2': individual_performance[channel2],
'combined': pair_performance,
'expected_additive': expected_additive,
'synergy_ratio': synergy_ratio,
'synergy_type': 'positive' if synergy_ratio > 1.1 else 'neutral' if synergy_ratio > 0.9 else 'negative'
}
# Sort by synergy ratio
sorted_synergy = dict(sorted(synergy_analysis.items(),
key=lambda x: x[1]['synergy_ratio'],
reverse=True))
print("渠道协同效应分析:")
for pair_key, analysis in sorted_synergy.items():
synergy_type = analysis['synergy_type']
print(f" {analysis['channel1']} + {analysis['channel2']}: "
f"协同比={analysis['synergy_ratio']:.3f} ({synergy_type})")
return sorted_synergy
def analyze_marginal_contributions(self):
"""
Analyze marginal contributions of channels
Returns:
pd.DataFrame: Marginal contribution analysis
"""
print("\n=== 分析边际贡献 ===")
if not self.shapley_values:
print("错误: 需要先计算Shapley值")
return pd.DataFrame()
# Calculate marginal contribution statistics
marginal_analysis = []
total_shapley = sum(self.shapley_values.values())
for channel, shapley_value in self.shapley_values.items():
marginal_analysis.append({
'channel': channel,
'shapley_value': shapley_value,
'attribution_weight': shapley_value / total_shapley if total_shapley > 0 else 0,
'performance_tier': self._classify_channel_performance(shapley_value / total_shapley if total_shapley > 0 else 0)
})
marginal_df = pd.DataFrame(marginal_analysis)
marginal_df = marginal_df.sort_values('shapley_value', ascending=False)
print("边际贡献排名:")
for idx, row in marginal_df.iterrows():
print(f" {row['channel']}: Shapley值={row['shapley_value']:.6f}, "
f"权重={row['attribution_weight']:.4f}, 层级={row['performance_tier']}")
return marginal_df
def _classify_channel_performance(self, attribution_weight):
"""Classify channel performance tier"""
if attribution_weight >= 0.3:
return 'top_performer'
elif attribution_weight >= 0.15:
return 'strong_performer'
elif attribution_weight >= 0.05:
return 'moderate_performer'
else:
return 'low_performer'
def optimize_channel_mix(self, budget_constraints=None):
"""
Optimize channel mix based on Shapley values
Args:
budget_constraints (dict): Optional budget constraints per channel
Returns:
dict: Channel mix optimization recommendations
"""
print("\n=== 优化渠道组合 ===")
if not self.attribution_weights:
print("错误: 需要先计算归因权重")
return {}
optimization_results = {}
# Sort channels by attribution weight
sorted_channels = sorted(self.attribution_weights.items(),
key=lambda x: x[1], reverse=True)
# Calculate optimal budget allocation
total_budget = 1.0 # Assume normalized budget
optimal_allocation = {}
remaining_budget = total_budget
for channel, weight in sorted_channels:
if budget_constraints and channel in budget_constraints:
max_allocation = budget_constraints[channel]
allocation = min(weight, max_allocation, remaining_budget)
else:
allocation = min(weight, remaining_budget)
optimal_allocation[channel] = allocation
remaining_budget -= allocation
if remaining_budget > 0:
# Distribute remaining budget proportionally
for channel, weight in sorted_channels:
if remaining_budget > 0:
extra = min(remaining_budget * weight / sum(w for w, _ in sorted_channels),
weight - optimal_allocation.get(channel, 0))
optimal_allocation[channel] += extra
remaining_budget -= extra
# Calculate expected improvement
current_efficiency = sum(self.attribution_weights.values())
optimal_efficiency = sum(optimal_allocation.values())
improvement = (optimal_efficiency - current_efficiency) / current_efficiency * 100 if current_efficiency > 0 else 0
optimization_results = {
'current_weights': self.attribution_weights,
'optimal_allocation': optimal_allocation,
'budget_constraints': budget_constraints,
'current_efficiency': current_efficiency,
'optimal_efficiency': optimal_efficiency,
'expected_improvement': improvement,
'recommendations': self._generate_optimization_recommendations(
self.attribution_weights, optimal_allocation
)
}
print("渠道组合优化结果:")
print(f"当前效率: {current_efficiency:.4f}")
print(f"优化效率: {optimal_efficiency:.4f}")
print(f"预期改善: {improvement:.1f}%")
return optimization_results
def _generate_optimization_recommendations(self, current_weights, optimal_allocation):
"""Generate optimization recommendations"""
recommendations = []
for channel in current_weights:
current = current_weights.get(channel, 0)
optimal = optimal_allocation.get(channel, 0)
difference = optimal - current
if abs(difference) > 0.05: # Significant difference
if difference > 0:
recommendations.append({
'channel': channel,
'action': 'increase',
'reason': f"建议增加投资,从 {current:.3f} 增加到 {optimal:.3f}",
'priority': 'high' if difference > 0.1 else 'medium'
})
else:
recommendations.append({
'channel': channel,
'action': 'decrease',
'reason': f"建议减少投资,从 {current:.3f} 减少到 {optimal:.3f}",
'priority': 'high' if abs(difference) > 0.1 else 'medium'
})
return sorted(recommendations, key=lambda x: abs(x['priority']), reverse=True)
def run_complete_shapley_analysis(self, paths_df, value_column='conversion_value'):
"""
Run complete Shapley value attribution analysis
Args:
paths_df (pd.DataFrame): Customer journey paths
value_column (str): Column name for conversion values
Returns:
dict: Complete Shapley value analysis results
"""
print("🎮 开始Shapley值归因分析")
print("=" * 50)
# 1. Calculate Shapley values
attribution_weights = self.calculate_shapley_values(paths_df, value_column)
# 2. Calculate channel synergy
synergy_analysis = self.calculate_channel_synergy()
# 3. Analyze marginal contributions
marginal_analysis = self.analyze_marginal_contributions()
# 4. Optimize channel mix
optimization_results = self.optimize_channel_mix()
results = {
'attribution_weights': attribution_weights,
'shapley_values': self.shapley_values,
'channel_synergy': synergy_analysis,
'marginal_analysis': marginal_analysis,
'optimization': optimization_results,
'total_conversions': paths_df['converted'].sum(),
'total_value': paths_df[value_column].sum()
}
print(f"\n✅ Shapley值分析完成!")
print(f"总转化数: {results['total_conversions']:,}")
print(f"总转化价值: {results['total_value']:,.2f}")
print(f"分析了 {len(attribution_weights)} 个渠道的Shapley值")
return results
def main():
"""Example usage of Shapley value attributor"""
attributor = ShapleyValueAttributor()
# Create sample data for demonstration
sample_paths = [
['开始', '付费搜索', '社交媒体', '邮件营销', '成功转化'],
['开始', '社交媒体', '付费搜索', '成功转化'],
['开始', '邮件营销', '社交媒体', '成功转化'],
['开始', '付费搜索', '未转化'],
['开始', '社交媒体', '未转化'],
['开始', '付费搜索', '社交媒体', '邮件营销', '成功转化']
]
paths_df = pd.DataFrame({
'user_id': range(len(sample_paths)),
'path': sample_paths,
'converted': [1, 1, 1, 0, 0, 1],
'conversion_value': [100, 150, 80, 0, 0, 200]
})
results = attributor.run_complete_shapley_analysis(paths_df)
print(f"\nShapley值归因权重:")
for channel, weight in results['attribution_weights'].items():
print(f" {channel}: {weight:.4f}")
if __name__ == "__main__":
main()#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
归因分析技能基础功能测试
Basic Functionality Test for Attribution Analysis Skill
"""
import pandas as pd
import sys
import os
# 添加父目录到路径以导入技能模块
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from core_attribution import AttributionAnalyzer
from markov_chains import MarkovChainAttributor
from shapley_values import ShapleyValueAttributor
def test_core_functionality():
"""测试核心功能"""
print("=== Attribution Analysis Skill Test ===")
print()
# 1. 测试基础归因分析
print("1. Testing Basic Attribution Analysis...")
try:
analyzer = AttributionAnalyzer()
# 加载示例数据
data_path = os.path.join('examples', 'sample_channel_data.csv')
df = analyzer.load_and_validate_data(data_path)
if df is not None:
print(f" Data loaded successfully: {len(df)} records")
# 构建客户路径
paths_df = analyzer.build_customer_paths(df)
print(f" Customer paths built: {len(paths_df)} paths")
# 运行基础归因分析
results = analyzer.compare_attribution_models(paths_df)
if results is not None and not results.empty:
print(" Basic attribution models executed successfully:")
print(f" Results shape: {results.shape}")
# 获取各模型结果
model_names = ['首次接触归因', '最后接触归因', '线性归因', '时间衰减归因', '位置归因']
for model_name in model_names:
if model_name in results.columns:
weights = results[model_name].to_dict()
if weights:
top_channel = max(weights.items(), key=lambda x: x[1])
print(f" - {model_name}: Top channel is {top_channel[0]} with weight {top_channel[1]:.3f}")
print(" Basic attribution test: PASSED")
else:
print(" Basic attribution test: FAILED")
else:
print(" Data loading test: FAILED")
except Exception as e:
print(f" Basic attribution test: FAILED with error: {e}")
print()
# 2. 测试马尔可夫链分析
print("2. Testing Markov Chain Analysis...")
try:
if 'paths_df' in locals() and not paths_df.empty:
markov_attributor = MarkovChainAttributor()
# 构建转移矩阵
transition_matrix = markov_attributor.build_transition_matrix(paths_df)
print(f" Transition matrix built: {len(transition_matrix)} states")
# 计算基础转化率
base_conversion_rate = paths_df['converted'].sum() / len(paths_df) if len(paths_df) > 0 else 0
# 计算移除效应
removal_effects = markov_attributor.calculate_removal_effects(paths_df, base_conversion_rate)
# 计算归因权重
markov_weights = markov_attributor.calculate_attribution_weights()
print(f" Markov attribution weights calculated for {len(markov_weights)} channels")
if markov_weights:
top_channel = max(markov_weights.items(), key=lambda x: x[1])
print(f" Top Markov channel: {top_channel[0]} with weight {top_channel[1]:.3f}")
print(" Markov chain test: PASSED")
else:
print(" Markov chain test: FAILED")
else:
print(" Markov chain test: SKIPPED (no path data)")
except Exception as e:
print(f" Markov chain test: FAILED with error: {e}")
print()
# 3. 测试Shapley值分析
print("3. Testing Shapley Value Analysis...")
try:
if 'paths_df' in locals() and not paths_df.empty:
shapley_attributor = ShapleyValueAttributor()
# 计算Shapley值
shapley_weights = shapley_attributor.calculate_shapley_values(paths_df)
print(f" Shapley values calculated for {len(shapley_weights)} channels")
if shapley_weights:
top_channel = max(shapley_weights.items(), key=lambda x: x[1])
print(f" Top Shapley channel: {top_channel[0]} with weight {top_channel[1]:.3f}")
print(" Shapley value test: PASSED")
else:
print(" Shapley value test: FAILED")
else:
print(" Shapley value test: SKIPPED (no path data)")
except Exception as e:
print(f" Shapley value test: FAILED with error: {e}")
print()
# 4. 测试模块导入
print("4. Testing Module Imports...")
try:
from attribution_visualizer import AttributionVisualizer
print(" AttributionVisualizer import: PASSED")
except Exception as e:
print(f" AttributionVisualizer import: FAILED with error: {e}")
print()
# 5. 测试数据验证功能
print("5. Testing Data Validation...")
try:
# 创建测试数据
test_data = pd.DataFrame({
'user_id': ['user1', 'user1', 'user2', 'user2'],
'timestamp': ['2024-01-01T10:00:00Z', '2024-01-02T15:00:00Z',
'2024-01-03T09:00:00Z', '2024-01-04T14:00:00Z'],
'channel': ['paid_search', 'email', 'social_media', 'paid_search'],
'conversion_status': [0, 1, 0, 1],
'conversion_value': [0, 100, 0, 200],
'cost': [50, 10, 30, 60]
})
# 保存测试数据
test_data.to_csv('test_data.csv', index=False, encoding='utf-8')
# 测试加载和验证
analyzer_test = AttributionAnalyzer()
validated_data = analyzer_test.load_and_validate_data('test_data.csv')
if validated_data is not None and len(validated_data) == 4:
print(" Data validation test: PASSED")
# 清理测试文件
os.remove('test_data.csv')
else:
print(" Data validation test: FAILED")
except Exception as e:
print(f" Data validation test: FAILED with error: {e}")
print()
print("=== Test Summary ===")
print("Core functionality testing completed.")
print("Check the output above for specific test results.")
if __name__ == "__main__":
test_core_functionality()pandas>=1.3.0
numpy>=1.21.0
matplotlib>=3.5.0
seaborn>=0.11.0
scipy>=1.7.0
networkx>=2.6