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
« 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
5"""
6Data analysis functions for CloudZero Data Tool.
7Contains business intelligence and analytical functions for various data insights.
8"""
10import polars as pl
11from rich.panel import Panel
12from rich import box
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
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 )
36 total_distinct = len(value_counts)
37 total_rows = len(df)
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]"""
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 )
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)
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
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"
74 table.add_row(
75 str(value),
76 f"{count:,}",
77 f"[{pct_style}]{percentage:.1f}%[/{pct_style}]",
78 )
80 console.print(table)
82 if total_distinct > 20:
83 console.print(
84 f"\n💡 [dim]... and {total_distinct - 20:,} more distinct values[/dim]"
85 )
87 except Exception as e:
88 print_error(f"Error analyzing column {column}: {e}")
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)
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 )
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")
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)
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 )
145 print(
146 f"{'Model':<35} {'Input Price':<12} {'Output Price':<12} {'Avg Cost':<12} {'Count':<10}"
147 )
148 print("-" * 90)
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 )
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)
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 )
177 print(
178 f"{'API Key Suffix':<20} {'Requests':<12} {'Total Spend':<12} {'Models':<8} {'Runtime Models':<15}"
179 )
180 print("-" * 80)
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 )
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)
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 )
208 print(
209 f"{'Search Queries':<15} {'Request Count':<15} {'Avg Cost':<12} {'Avg Time (s)':<12}"
210 )
211 print("-" * 60)
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}")
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)
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 )
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
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")
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)
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()
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}")
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 )
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)
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)
301 print_success("\n" + "#" * 80)
302 print_success("ANALYSIS COMPLETE")
303 print_success("#" * 80)