Coverage for src/common/analysis.py: 95%

131 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-05-25 13:51 -0400

1# Copyright (c) CloudZero - ALL RIGHTS RESERVED - PROPRIETARY AND CONFIDENTIAL 

2# Unauthorized copying of this file and/or project, via any medium is strictly prohibited. 

3# Direct all questions to legal@cloudzero.com 

4 

5""" 

6Data analysis functions for CloudZero Data Tool. 

7Contains business intelligence and analytical functions for various data insights. 

8""" 

9 

10import polars as pl 

11from rich.panel import Panel 

12from rich import box 

13 

14try: 

15 from ..utils.console_helpers import print_error, print_success, print_warning, print_info, console 

16 from ..utils.table_helpers import create_basic_table, TableColumn 

17except ImportError: 

18 # Fallback for direct execution or test imports 

19 import sys 

20 from pathlib import Path 

21 sys.path.append(str(Path(__file__).parent.parent)) 

22 from utils.console_helpers import print_error, print_success, print_warning, print_info, console 

23 from utils.table_helpers import create_basic_table, TableColumn 

24 

25 

26def analyze_column(df: pl.DataFrame, column: str) -> None: 

27 """Analyze distinct values in the selected column.""" 

28 try: 

29 # Get distinct values and their counts 

30 value_counts = ( 

31 df.group_by(column) 

32 .agg(pl.len().alias("count")) 

33 .sort("count", descending=True) 

34 ) 

35 

36 total_distinct = len(value_counts) 

37 total_rows = len(df) 

38 

39 # Create summary panel 

40 summary_text = f"""📈 [cyan]Total distinct values:[/cyan] [yellow]{total_distinct:,}[/yellow] 

41📊 [cyan]Total rows:[/cyan] [yellow]{total_rows:,}[/yellow] 

42🎯 [cyan]Uniqueness:[/cyan] [yellow]{total_distinct / total_rows:.2%}[/yellow]""" 

43 

44 console.print( 

45 Panel( 

46 summary_text, 

47 title=f"🔍 Analyzing Column: [magenta]{column}[/magenta]", 

48 box=box.ROUNDED, 

49 border_style="blue", 

50 ) 

51 ) 

52 

53 # Create table for top values 

54 columns = [ 

55 TableColumn("Value", "cyan", width=25), 

56 TableColumn("Count", "green", justify="right"), 

57 TableColumn("Percentage", "yellow", justify="right") 

58 ] 

59 table = create_basic_table("🏆 Top 20 Most Frequent Values", columns, box_style=box.ROUNDED) 

60 

61 top_values = value_counts.head(20) 

62 for row in top_values.iter_rows(): 

63 value, count = row 

64 percentage = count / total_rows * 100 

65 

66 # Color code percentages 

67 if percentage >= 50: 

68 pct_style = "red" 

69 elif percentage >= 10: 

70 pct_style = "yellow" 

71 else: 

72 pct_style = "green" 

73 

74 table.add_row( 

75 str(value), 

76 f"{count:,}", 

77 f"[{pct_style}]{percentage:.1f}%[/{pct_style}]", 

78 ) 

79 

80 console.print(table) 

81 

82 if total_distinct > 20: 

83 console.print( 

84 f"\n💡 [dim]... and {total_distinct - 20:,} more distinct values[/dim]" 

85 ) 

86 

87 except Exception as e: 

88 print_error(f"Error analyzing column {column}: {e}") 

89 

90 

91def analyze_timing_patterns(df: pl.DataFrame) -> None: 

92 """Analyze timing patterns by runtime model.""" 

93 print_info("\n" + "=" * 80) 

94 print_info("TIMING PATTERNS BY RUNTIME MODEL") 

95 print_info("=" * 80) 

96 

97 if "RUNTIME_MODEL_NAME" in df.columns and "TOTAL_TIME" in df.columns: 

98 timing_analysis = ( 

99 df.filter(pl.col("TOTAL_TIME").is_not_null()) 

100 .group_by("RUNTIME_MODEL_NAME") 

101 .agg( 

102 [ 

103 pl.len().alias("request_count"), 

104 pl.col("TOTAL_TIME").mean().alias("avg_time"), 

105 pl.col("TOTAL_TIME").median().alias("median_time"), 

106 pl.col("TOTAL_TIME").min().alias("min_time"), 

107 pl.col("TOTAL_TIME").max().alias("max_time"), 

108 ] 

109 ) 

110 .sort("avg_time", descending=True) 

111 .head(15) 

112 ) 

113 

114 for row in timing_analysis.iter_rows(named=True): 

115 print(f"\n🔧 {row['RUNTIME_MODEL_NAME']}") 

116 print(f" 📊 Requests: {row['request_count']:,}") 

117 print(f" ⏱️ Avg Time: {row['avg_time']:.3f}s") 

118 print(f" 📈 Median: {row['median_time']:.3f}s") 

119 print(f" ⬇️ Min: {row['min_time']:.3f}s") 

120 print(f" ⬆️ Max: {row['max_time']:.3f}s") 

121 else: 

122 print_warning("Required columns not found for timing analysis") 

123 

124 

125def analyze_pricing_patterns(df: pl.DataFrame) -> None: 

126 """Analyze pricing patterns by runtime model.""" 

127 print_info("\n" + "=" * 80) 

128 print_info("PRICING PATTERNS BY RUNTIME MODEL") 

129 print_info("=" * 80) 

130 

131 pricing_analysis = ( 

132 df.filter(pl.col("RUNTIME_MODEL_NAME").is_not_null()) 

133 .group_by("RUNTIME_MODEL_NAME") 

134 .agg( 

135 [ 

136 pl.col("PRICE_PER_INPUT_TOKEN").mean().alias("avg_input_price"), 

137 pl.col("PRICE_PER_OUTPUT_TOKEN").mean().alias("avg_output_price"), 

138 pl.col("TOTAL_COST").mean().alias("avg_total_cost"), 

139 pl.len().alias("count"), 

140 ] 

141 ) 

142 .sort("count", descending=True) 

143 ) 

144 

145 print( 

146 f"{'Model':<35} {'Input Price':<12} {'Output Price':<12} {'Avg Cost':<12} {'Count':<10}" 

147 ) 

148 print("-" * 90) 

149 

150 for row in pricing_analysis.iter_rows(): 

151 model, input_price, output_price, avg_cost, count = row 

152 print( 

153 f"{model:<35} ${input_price:<11.6f} ${output_price:<11.6f} ${avg_cost:<11.6f} {count:<10,}" 

154 ) 

155 

156 

157def analyze_customer_patterns(df: pl.DataFrame) -> None: 

158 """Analyze customer usage patterns by API key.""" 

159 print_info("\n" + "=" * 80) 

160 print_info("TOP CUSTOMER USAGE PATTERNS") 

161 print_info("=" * 80) 

162 

163 customer_analysis = ( 

164 df.group_by("API_KEY_SUFFIX") 

165 .agg( 

166 [ 

167 pl.len().alias("total_requests"), 

168 pl.col("TOTAL_COST").sum().alias("total_spend"), 

169 pl.col("MODEL").n_unique().alias("unique_models"), 

170 pl.col("RUNTIME_MODEL_NAME").n_unique().alias("unique_runtime_models"), 

171 ] 

172 ) 

173 .sort("total_requests", descending=True) 

174 .head(15) 

175 ) 

176 

177 print( 

178 f"{'API Key Suffix':<20} {'Requests':<12} {'Total Spend':<12} {'Models':<8} {'Runtime Models':<15}" 

179 ) 

180 print("-" * 80) 

181 

182 for row in customer_analysis.iter_rows(): 

183 suffix, requests, spend, models, runtime_models = row 

184 print( 

185 f"...{suffix:<17} {requests:<12,} ${spend:<11.4f} {models:<8} {runtime_models:<15}" 

186 ) 

187 

188 

189def analyze_search_patterns(df: pl.DataFrame) -> None: 

190 """Analyze search query usage patterns.""" 

191 print_info("\n" + "=" * 80) 

192 print_info("SEARCH QUERY USAGE PATTERNS") 

193 print_info("=" * 80) 

194 

195 search_analysis = ( 

196 df.group_by("NUM_SEARCH_QUERIES") 

197 .agg( 

198 [ 

199 pl.len().alias("request_count"), 

200 pl.col("TOTAL_COST").mean().alias("avg_cost"), 

201 pl.col("TOTAL_TIME").mean().alias("avg_time"), 

202 ] 

203 ) 

204 .sort("NUM_SEARCH_QUERIES") 

205 .head(10) # Show first 10 search query levels 

206 ) 

207 

208 print( 

209 f"{'Search Queries':<15} {'Request Count':<15} {'Avg Cost':<12} {'Avg Time (s)':<12}" 

210 ) 

211 print("-" * 60) 

212 

213 for row in search_analysis.iter_rows(): 

214 num_queries, count, avg_cost, avg_time = row 

215 print(f"{num_queries:<15} {count:<15,} ${avg_cost:<11.4f} {avg_time:<12.3f}") 

216 

217 

218def analyze_internal_external(df: pl.DataFrame) -> None: 

219 """Analyze internal vs external usage patterns.""" 

220 print_info("\n" + "=" * 80) 

221 print_info("INTERNAL VS EXTERNAL USAGE") 

222 print_info("=" * 80) 

223 

224 if "IS_PERPLEXITY_INTERNAL" in df.columns: 

225 internal_external = df.group_by("IS_PERPLEXITY_INTERNAL").agg( 

226 [ 

227 pl.len().alias("request_count"), 

228 pl.col("TOTAL_COST").sum().alias("total_cost") 

229 if "TOTAL_COST" in df.columns 

230 else pl.lit(0).alias("total_cost"), 

231 pl.col("TOTAL_TIME").mean().alias("avg_time") 

232 if "TOTAL_TIME" in df.columns 

233 else pl.lit(0).alias("avg_time"), 

234 ] 

235 ) 

236 

237 for row in internal_external.iter_rows(named=True): 

238 usage_type = ( 

239 "🏢 Internal" if row["IS_PERPLEXITY_INTERNAL"] else "🌍 External" 

240 ) 

241 percentage = (row["request_count"] / len(df)) * 100 

242 

243 print(f"{usage_type} Usage:") 

244 print(f" 📊 Requests: {row['request_count']:,} ({percentage:.1f}%)") 

245 if "TOTAL_COST" in df.columns: 

246 print(f" 💰 Total Cost: ${row['total_cost']:.2f}") 

247 if "TOTAL_TIME" in df.columns: 

248 print(f" ⏱️ Avg Time: {row['avg_time']:.3f}s") 

249 print() 

250 else: 

251 print_warning("IS_PERPLEXITY_INTERNAL column not found") 

252 

253 

254def analyze_business_summary(df: pl.DataFrame) -> None: 

255 """Generate high-level business summary.""" 

256 print_info("\n" + "=" * 80) 

257 print_info("BUSINESS SUMMARY") 

258 print_info("=" * 80) 

259 

260 total_requests = len(df) 

261 total_revenue = df["TOTAL_COST"].sum() 

262 avg_revenue_per_request = df["TOTAL_COST"].mean() 

263 unique_customers = df["API_KEY_SUFFIX"].n_unique() 

264 unique_models = df["MODEL"].n_unique() 

265 unique_runtime_models = df.filter(pl.col("RUNTIME_MODEL_NAME").is_not_null())[ 

266 "RUNTIME_MODEL_NAME" 

267 ].n_unique() 

268 

269 print(f"Total Requests: {total_requests:,}") 

270 print(f"Total Revenue: ${total_revenue:,.2f}") 

271 print(f"Average Revenue per Request: ${avg_revenue_per_request:.6f}") 

272 print(f"Unique Customers (API Keys): {unique_customers:,}") 

273 print(f"Unique Models: {unique_models}") 

274 print(f"Unique Runtime Models: {unique_runtime_models}") 

275 

276 # Search usage breakdown 

277 search_requests = df.filter(pl.col("NUM_SEARCH_QUERIES") > 0) 

278 search_percentage = len(search_requests) / total_requests * 100 

279 print("\nSearch Usage:") 

280 print( 

281 f" Requests with search: {len(search_requests):,} ({search_percentage:.1f}%)" 

282 ) 

283 print( 

284 f" Requests without search: {total_requests - len(search_requests):,} ({100 - search_percentage:.1f}%)" 

285 ) 

286 

287 

288def run_comprehensive_analysis(df: pl.DataFrame) -> None: 

289 """Run a comprehensive analysis of the entire dataset.""" 

290 print_info("\n" + "#" * 80) 

291 print_info("COMPREHENSIVE PERPLEXITY DATA ANALYSIS") 

292 print_info("#" * 80) 

293 

294 analyze_business_summary(df) 

295 analyze_timing_patterns(df) 

296 analyze_pricing_patterns(df) 

297 analyze_customer_patterns(df) 

298 analyze_search_patterns(df) 

299 analyze_internal_external(df) 

300 

301 print_success("\n" + "#" * 80) 

302 print_success("ANALYSIS COMPLETE") 

303 print_success("#" * 80)