Rust Data Analysis Best Practices¶
Objective: Master senior-level Rust data analysis patterns for production systems. When you need to analyze large datasets efficiently, when you want to leverage Rust's performance for data analysis, when you need enterprise-grade data analysisโthese best practices become your weapon of choice.
Core Principles¶
- Performance: Leverage Rust's speed for data analysis
- Memory Efficiency: Optimize memory usage for large datasets
- Parallel Processing: Use multiple cores for data analysis
- Data Structures: Choose appropriate data structures for analysis
- Statistical Computing: Implement statistical analysis patterns
Data Analysis Patterns¶
Data Loading and Processing¶
// rust/01-data-loading.rs
/*
Data loading and processing patterns
*/
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufRead, BufReader};
use serde::{Deserialize, Serialize};
use thiserror::Error;
/// Data loading error types.
#[derive(Error, Debug)]
pub enum DataLoadingError {
#[error("File not found: {0}")]
FileNotFound(String),
#[error("Invalid CSV format: {0}")]
InvalidCsv(String),
#[error("Parsing error: {0}")]
ParsingError(String),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
}
/// Data record structure.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DataRecord {
pub id: String,
pub timestamp: chrono::DateTime<chrono::Utc>,
pub values: HashMap<String, f64>,
pub metadata: HashMap<String, String>,
}
/// CSV data loader.
pub struct CsvDataLoader {
file_path: String,
delimiter: char,
has_header: bool,
}
impl CsvDataLoader {
pub fn new(file_path: String, delimiter: char, has_header: bool) -> Self {
Self {
file_path,
delimiter,
has_header,
}
}
/// Load data from CSV file.
pub async fn load_data(&self) -> Result<Vec<DataRecord>, DataLoadingError> {
let file = File::open(&self.file_path)
.map_err(|_| DataLoadingError::FileNotFound(self.file_path.clone()))?;
let reader = BufReader::new(file);
let mut lines = reader.lines();
// Skip header if present
if self.has_header {
lines.next();
}
let mut records = Vec::new();
let mut record_id = 0;
for line in lines {
let line = line.map_err(DataLoadingError::IoError)?;
let record = self.parse_line(&line, record_id)?;
records.push(record);
record_id += 1;
}
Ok(records)
}
/// Parse a single line of CSV data.
fn parse_line(&self, line: &str, record_id: usize) -> Result<DataRecord, DataLoadingError> {
let fields: Vec<&str> = line.split(self.delimiter).collect();
if fields.len() < 3 {
return Err(DataLoadingError::InvalidCsv(
format!("Expected at least 3 fields, got {}", fields.len())
));
}
let mut values = HashMap::new();
let mut metadata = HashMap::new();
// Parse timestamp (assuming first field is timestamp)
let timestamp = chrono::DateTime::parse_from_rfc3339(fields[0])
.map_err(|e| DataLoadingError::ParsingError(format!("Invalid timestamp: {}", e)))?
.with_timezone(&chrono::Utc);
// Parse numeric values
for (i, field) in fields.iter().enumerate().skip(1) {
if let Ok(value) = field.parse::<f64>() {
values.insert(format!("field_{}", i), value);
} else {
metadata.insert(format!("field_{}", i), field.to_string());
}
}
Ok(DataRecord {
id: format!("record_{}", record_id),
timestamp,
values,
metadata,
})
}
}
/// Data aggregator for analysis.
pub struct DataAggregator {
data: Vec<DataRecord>,
statistics: HashMap<String, StatisticalSummary>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StatisticalSummary {
pub count: usize,
pub mean: f64,
pub median: f64,
pub std_dev: f64,
pub min: f64,
pub max: f64,
pub quartiles: (f64, f64, f64), // Q1, Q2, Q3
}
impl DataAggregator {
pub fn new(data: Vec<DataRecord>) -> Self {
Self {
data,
statistics: HashMap::new(),
}
}
/// Calculate statistics for all numeric fields.
pub async fn calculate_statistics(&mut self) -> Result<(), String> {
if self.data.is_empty() {
return Err("No data to analyze".to_string());
}
// Get all numeric field names
let field_names: Vec<String> = self.data[0]
.values
.keys()
.cloned()
.collect();
// Calculate statistics for each field
for field_name in field_names {
let values: Vec<f64> = self.data
.iter()
.filter_map(|record| record.values.get(&field_name))
.cloned()
.collect();
if !values.is_empty() {
let summary = self.calculate_field_statistics(&values);
self.statistics.insert(field_name, summary);
}
}
Ok(())
}
/// Calculate statistics for a specific field.
fn calculate_field_statistics(&self, values: &[f64]) -> StatisticalSummary {
let mut sorted_values = values.to_vec();
sorted_values.sort_by(|a, b| a.partial_cmp(b).unwrap());
let count = values.len();
let sum: f64 = values.iter().sum();
let mean = sum / count as f64;
let median = if count % 2 == 0 {
(sorted_values[count / 2 - 1] + sorted_values[count / 2]) / 2.0
} else {
sorted_values[count / 2]
};
let variance: f64 = values
.iter()
.map(|x| (x - mean).powi(2))
.sum::<f64>() / count as f64;
let std_dev = variance.sqrt();
let min = sorted_values[0];
let max = sorted_values[count - 1];
let q1 = if count >= 4 {
sorted_values[count / 4]
} else {
min
};
let q3 = if count >= 4 {
sorted_values[3 * count / 4]
} else {
max
};
StatisticalSummary {
count,
mean,
median,
std_dev,
min,
max,
quartiles: (q1, median, q3),
}
}
/// Get statistics for a specific field.
pub fn get_field_statistics(&self, field_name: &str) -> Option<&StatisticalSummary> {
self.statistics.get(field_name)
}
/// Get all statistics.
pub fn get_all_statistics(&self) -> &HashMap<String, StatisticalSummary> {
&self.statistics
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_data_aggregator() {
let mut data = Vec::new();
for i in 0..10 {
let mut values = HashMap::new();
values.insert("temperature".to_string(), 20.0 + i as f64);
values.insert("pressure".to_string(), 101325.0 + i as f64 * 1000.0);
let record = DataRecord {
id: format!("record_{}", i),
timestamp: chrono::Utc::now(),
values,
metadata: HashMap::new(),
};
data.push(record);
}
let mut aggregator = DataAggregator::new(data);
aggregator.calculate_statistics().await.unwrap();
let stats = aggregator.get_field_statistics("temperature");
assert!(stats.is_some());
assert_eq!(stats.unwrap().count, 10);
}
}
Statistical Analysis¶
// rust/02-statistical-analysis.rs
/*
Statistical analysis patterns and best practices
*/
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
/// Statistical analyzer for data analysis.
pub struct StatisticalAnalyzer {
data: Vec<f64>,
statistics: Option<StatisticalSummary>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StatisticalSummary {
pub count: usize,
pub mean: f64,
pub median: f64,
pub mode: Option<f64>,
pub std_dev: f64,
pub variance: f64,
pub min: f64,
pub max: f64,
pub range: f64,
pub quartiles: (f64, f64, f64), // Q1, Q2, Q3
pub iqr: f64, // Interquartile range
pub skewness: f64,
pub kurtosis: f64,
}
impl StatisticalAnalyzer {
pub fn new(data: Vec<f64>) -> Self {
Self {
data,
statistics: None,
}
}
/// Calculate comprehensive statistics.
pub fn calculate_statistics(&mut self) -> Result<&StatisticalSummary, String> {
if self.data.is_empty() {
return Err("No data to analyze".to_string());
}
let mut sorted_data = self.data.clone();
sorted_data.sort_by(|a, b| a.partial_cmp(b).unwrap());
let count = self.data.len();
let sum: f64 = self.data.iter().sum();
let mean = sum / count as f64;
let median = if count % 2 == 0 {
(sorted_data[count / 2 - 1] + sorted_data[count / 2]) / 2.0
} else {
sorted_data[count / 2]
};
let mode = self.calculate_mode(&sorted_data);
let variance: f64 = self.data
.iter()
.map(|x| (x - mean).powi(2))
.sum::<f64>() / count as f64;
let std_dev = variance.sqrt();
let min = sorted_data[0];
let max = sorted_data[count - 1];
let range = max - min;
let q1 = if count >= 4 {
sorted_data[count / 4]
} else {
min
};
let q3 = if count >= 4 {
sorted_data[3 * count / 4]
} else {
max
};
let iqr = q3 - q1;
let skewness = self.calculate_skewness(&self.data, mean, std_dev);
let kurtosis = self.calculate_kurtosis(&self.data, mean, std_dev);
self.statistics = Some(StatisticalSummary {
count,
mean,
median,
mode,
std_dev,
variance,
min,
max,
range,
quartiles: (q1, median, q3),
iqr,
skewness,
kurtosis,
});
Ok(self.statistics.as_ref().unwrap())
}
/// Calculate mode (most frequent value).
fn calculate_mode(&self, sorted_data: &[f64]) -> Option<f64> {
let mut frequency_map = HashMap::new();
for value in sorted_data {
*frequency_map.entry(*value).or_insert(0) += 1;
}
let max_frequency = frequency_map.values().max()?;
let mode = frequency_map
.iter()
.find(|(_, &freq)| freq == *max_frequency)
.map(|(value, _)| *value);
mode
}
/// Calculate skewness.
fn calculate_skewness(&self, data: &[f64], mean: f64, std_dev: f64) -> f64 {
if std_dev == 0.0 {
return 0.0;
}
let n = data.len() as f64;
let sum_cubed_deviations: f64 = data
.iter()
.map(|x| ((x - mean) / std_dev).powi(3))
.sum();
sum_cubed_deviations / n
}
/// Calculate kurtosis.
fn calculate_kurtosis(&self, data: &[f64], mean: f64, std_dev: f64) -> f64 {
if std_dev == 0.0 {
return 0.0;
}
let n = data.len() as f64;
let sum_fourth_deviations: f64 = data
.iter()
.map(|x| ((x - mean) / std_dev).powi(4))
.sum();
(sum_fourth_deviations / n) - 3.0
}
/// Get statistics.
pub fn get_statistics(&self) -> Option<&StatisticalSummary> {
self.statistics.as_ref()
}
/// Get data summary.
pub fn get_summary(&self) -> String {
if let Some(stats) = &self.statistics {
format!(
"Count: {}, Mean: {:.2}, Median: {:.2}, Std Dev: {:.2}, Min: {:.2}, Max: {:.2}",
stats.count, stats.mean, stats.median, stats.std_dev, stats.min, stats.max
)
} else {
"No statistics calculated".to_string()
}
}
}
/// Correlation analyzer.
pub struct CorrelationAnalyzer {
data: Vec<(f64, f64)>,
correlation: Option<f64>,
}
impl CorrelationAnalyzer {
pub fn new(data: Vec<(f64, f64)>) -> Self {
Self {
data,
correlation: None,
}
}
/// Calculate Pearson correlation coefficient.
pub fn calculate_correlation(&mut self) -> Result<f64, String> {
if self.data.len() < 2 {
return Err("Need at least 2 data points for correlation".to_string());
}
let n = self.data.len() as f64;
// Calculate means
let x_mean: f64 = self.data.iter().map(|(x, _)| x).sum::<f64>() / n;
let y_mean: f64 = self.data.iter().map(|(_, y)| y).sum::<f64>() / n;
// Calculate correlation coefficient
let numerator: f64 = self.data
.iter()
.map(|(x, y)| (x - x_mean) * (y - y_mean))
.sum();
let x_variance: f64 = self.data
.iter()
.map(|(x, _)| (x - x_mean).powi(2))
.sum::<f64>();
let y_variance: f64 = self.data
.iter()
.map(|(_, y)| (y - y_mean).powi(2))
.sum::<f64>();
let denominator = (x_variance * y_variance).sqrt();
if denominator == 0.0 {
return Err("Cannot calculate correlation: zero variance".to_string());
}
let correlation = numerator / denominator;
self.correlation = Some(correlation);
Ok(correlation)
}
/// Get correlation coefficient.
pub fn get_correlation(&self) -> Option<f64> {
self.correlation
}
/// Interpret correlation strength.
pub fn interpret_correlation(&self) -> String {
if let Some(corr) = self.correlation {
let abs_corr = corr.abs();
if abs_corr >= 0.9 {
"Very strong correlation".to_string()
} else if abs_corr >= 0.7 {
"Strong correlation".to_string()
} else if abs_corr >= 0.5 {
"Moderate correlation".to_string()
} else if abs_corr >= 0.3 {
"Weak correlation".to_string()
} else {
"Very weak correlation".to_string()
}
} else {
"No correlation calculated".to_string()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_statistical_analyzer() {
let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let mut analyzer = StatisticalAnalyzer::new(data);
let stats = analyzer.calculate_statistics().unwrap();
assert_eq!(stats.count, 5);
assert_eq!(stats.mean, 3.0);
assert_eq!(stats.median, 3.0);
}
#[test]
fn test_correlation_analyzer() {
let data = vec![(1.0, 2.0), (2.0, 4.0), (3.0, 6.0), (4.0, 8.0)];
let mut analyzer = CorrelationAnalyzer::new(data);
let correlation = analyzer.calculate_correlation().unwrap();
assert!((correlation - 1.0).abs() < 0.001); // Perfect positive correlation
}
}
Data Visualization¶
// rust/03-data-visualization.rs
/*
Data visualization patterns and best practices
*/
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
/// Data visualization service.
pub struct DataVisualizationService {
data: Vec<DataPoint>,
charts: Vec<Chart>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DataPoint {
pub x: f64,
pub y: f64,
pub label: Option<String>,
pub color: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Chart {
pub id: String,
pub title: String,
pub chart_type: ChartType,
pub data: Vec<DataPoint>,
pub x_label: String,
pub y_label: String,
pub width: u32,
pub height: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ChartType {
Line,
Bar,
Scatter,
Histogram,
BoxPlot,
}
impl DataVisualizationService {
pub fn new() -> Self {
Self {
data: Vec::new(),
charts: Vec::new(),
}
}
/// Add data points.
pub fn add_data(&mut self, data: Vec<DataPoint>) {
self.data.extend(data);
}
/// Create a line chart.
pub fn create_line_chart(&mut self, id: String, title: String, data: Vec<DataPoint>) -> Result<(), String> {
if data.is_empty() {
return Err("Cannot create chart with empty data".to_string());
}
let chart = Chart {
id,
title,
chart_type: ChartType::Line,
data,
x_label: "X Axis".to_string(),
y_label: "Y Axis".to_string(),
width: 800,
height: 600,
};
self.charts.push(chart);
Ok(())
}
/// Create a bar chart.
pub fn create_bar_chart(&mut self, id: String, title: String, data: Vec<DataPoint>) -> Result<(), String> {
if data.is_empty() {
return Err("Cannot create chart with empty data".to_string());
}
let chart = Chart {
id,
title,
chart_type: ChartType::Bar,
data,
x_label: "Categories".to_string(),
y_label: "Values".to_string(),
width: 800,
height: 600,
};
self.charts.push(chart);
Ok(())
}
/// Create a scatter plot.
pub fn create_scatter_plot(&mut self, id: String, title: String, data: Vec<DataPoint>) -> Result<(), String> {
if data.is_empty() {
return Err("Cannot create chart with empty data".to_string());
}
let chart = Chart {
id,
title,
chart_type: ChartType::Scatter,
data,
x_label: "X Values".to_string(),
y_label: "Y Values".to_string(),
width: 800,
height: 600,
};
self.charts.push(chart);
Ok(())
}
/// Create a histogram.
pub fn create_histogram(&mut self, id: String, title: String, data: Vec<f64>, bins: usize) -> Result<(), String> {
if data.is_empty() {
return Err("Cannot create histogram with empty data".to_string());
}
let histogram_data = self.create_histogram_data(data, bins);
let chart = Chart {
id,
title,
chart_type: ChartType::Histogram,
data: histogram_data,
x_label: "Values".to_string(),
y_label: "Frequency".to_string(),
width: 800,
height: 600,
};
self.charts.push(chart);
Ok(())
}
/// Create histogram data.
fn create_histogram_data(&self, data: Vec<f64>, bins: usize) -> Vec<DataPoint> {
let min = data.iter().cloned().fold(f64::INFINITY, f64::min);
let max = data.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let bin_width = (max - min) / bins as f64;
let mut histogram = vec![0; bins];
for value in data {
let bin_index = ((value - min) / bin_width) as usize;
let bin_index = bin_index.min(bins - 1);
histogram[bin_index] += 1;
}
let mut histogram_data = Vec::new();
for (i, count) in histogram.iter().enumerate() {
let x = min + (i as f64 + 0.5) * bin_width;
histogram_data.push(DataPoint {
x,
y: *count as f64,
label: None,
color: None,
});
}
histogram_data
}
/// Get all charts.
pub fn get_charts(&self) -> &[Chart] {
&self.charts
}
/// Get a specific chart.
pub fn get_chart(&self, id: &str) -> Option<&Chart> {
self.charts.iter().find(|chart| chart.id == id)
}
/// Export chart data as JSON.
pub fn export_chart_json(&self, id: &str) -> Result<String, String> {
let chart = self.get_chart(id)
.ok_or_else(|| "Chart not found".to_string())?;
serde_json::to_string_pretty(chart)
.map_err(|e| format!("Failed to serialize chart: {}", e))
}
/// Export all charts as JSON.
pub fn export_all_charts_json(&self) -> Result<String, String> {
serde_json::to_string_pretty(&self.charts)
.map_err(|e| format!("Failed to serialize charts: {}", e))
}
}
/// Data analysis report generator.
pub struct DataAnalysisReport {
title: String,
sections: Vec<ReportSection>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReportSection {
pub title: String,
pub content: String,
pub charts: Vec<String>, // Chart IDs
}
impl DataAnalysisReport {
pub fn new(title: String) -> Self {
Self {
title,
sections: Vec::new(),
}
}
/// Add a section to the report.
pub fn add_section(&mut self, title: String, content: String, charts: Vec<String>) {
self.sections.push(ReportSection {
title,
content,
charts,
});
}
/// Generate HTML report.
pub fn generate_html(&self) -> String {
let mut html = String::new();
html.push_str(&format!("<html><head><title>{}</title></head><body>", self.title));
html.push_str(&format!("<h1>{}</h1>", self.title));
for section in &self.sections {
html.push_str(&format!("<h2>{}</h2>", section.title));
html.push_str(&format!("<p>{}</p>", section.content));
if !section.charts.is_empty() {
html.push_str("<div class=\"charts\">");
for chart_id in §ion.charts {
html.push_str(&format!("<div class=\"chart\" id=\"{}\"></div>", chart_id));
}
html.push_str("</div>");
}
}
html.push_str("</body></html>");
html
}
/// Generate Markdown report.
pub fn generate_markdown(&self) -> String {
let mut markdown = String::new();
markdown.push_str(&format!("# {}\n\n", self.title));
for section in &self.sections {
markdown.push_str(&format!("## {}\n\n", section.title));
markdown.push_str(&format!("{}\n\n", section.content));
if !section.charts.is_empty() {
markdown.push_str("### Charts\n\n");
for chart_id in §ion.charts {
markdown.push_str(&format!("- Chart: {}\n", chart_id));
}
markdown.push_str("\n");
}
}
markdown
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_data_visualization_service() {
let mut service = DataVisualizationService::new();
let data = vec![
DataPoint { x: 1.0, y: 2.0, label: None, color: None },
DataPoint { x: 2.0, y: 4.0, label: None, color: None },
DataPoint { x: 3.0, y: 6.0, label: None, color: None },
];
service.create_line_chart("chart1".to_string(), "Test Chart".to_string(), data).unwrap();
assert_eq!(service.get_charts().len(), 1);
}
#[test]
fn test_data_analysis_report() {
let mut report = DataAnalysisReport::new("Test Report".to_string());
report.add_section(
"Introduction".to_string(),
"This is a test report.".to_string(),
vec!["chart1".to_string()],
);
let html = report.generate_html();
assert!(html.contains("Test Report"));
assert!(html.contains("Introduction"));
}
}
TL;DR Runbook¶
Quick Start¶
// 1. Data loading
let loader = CsvDataLoader::new("data.csv".to_string(), ',', true);
let data = loader.load_data().await?;
// 2. Statistical analysis
let mut analyzer = StatisticalAnalyzer::new(values);
let stats = analyzer.calculate_statistics()?;
// 3. Data visualization
let mut service = DataVisualizationService::new();
service.create_line_chart("chart1".to_string(), "Title".to_string(), data)?;
Essential Patterns¶
// Complete data analysis setup
pub fn setup_rust_data_analysis() {
// 1. Data loading
// 2. Statistical analysis
// 3. Data visualization
// 4. Report generation
// 5. Performance optimization
// 6. Memory management
// 7. Parallel processing
// 8. Error handling
println!("Rust data analysis setup complete!");
}
This guide provides the complete machinery for Rust data analysis. Each pattern includes implementation examples, analysis strategies, and real-world usage patterns for enterprise data analysis.