DZone>大数据专区>用Java和Python进行数据统计和分析
用Java和Python进行数据统计和分析
如何使用Java Streams和Python Pandas来分析表格数据。以及比较它们对大量数据的性能和规模。
通过
-
6月14日,22日 - 大数据区 -教程
喜欢 (1)
评论
保存
鸣叫
132次浏览
加入DZone社区,获得完整的会员体验。
Java和Python是当今最流行的两种计算机语言。两者都非常成熟,并提供了工具和技术生态系统,以支持开发解决数据科学世界中出现的挑战性问题。每种语言都有其特异之处。重要的是要了解它们在处理不同问题时如何比较,它们是否闪耀或缺乏处理指定任务所需的灵活性。 什么时候一个比另一个好,或者什么时候它们串联起来相互补充。
Python是一种动态类型的语言,使用起来非常简单,如果我们不用担心复杂的程序流程,它当然是进行复杂计算的首选语言。它提供了优秀的库(Pandas, NumPy, Matplotlib, ScyPy, PyTorch, TensorFlow等)来支持数据结构或数组的逻辑、数学和科学操作。
Java是一种非常健壮的语言,强类型化,因此有更严格的语法规则,使其不容易出现程序性错误。像Python提供了大量的库来处理数据结构、线性代数、机器学习和数据处理(ND4J、Mahout、Spark、Deeplearning4J,等等)。
在这篇文章中,我们将重点研究如何使用Java和Python对大量的表格数据进行简单的数据分析并计算一些统计数据。我们将看到如何在每个平台上进行数据分析的不同技术,比较它们如何扩展,以及应用并行计算来提高其性能的可能性。
问题布局
我们将对不同州的一大批城市的价格做一个简单的分析。 为了简单起见,我们假设有一个包含这些信息的CSV文件。我们读取该文件,并着手过滤掉一些州,将剩下的按城市-州对分组,做一些基本的统计。我们感兴趣的是找到能够有效执行的解决方案,并且随着输入数据规模的增长而有良好的扩展。
数据的一个样本是。
| 城市 | 州 | 基准价格 | 实际价格 |
| 拉荷西 | 宾夕法尼亚州 | 34.17 | 33.19 |
| 传教士沼泽地 | WA | 27,46 | 90.17 |
| 杜南角 | 纽约州 | 92.0 | 162.46 |
| 窦南角 | 纽约州 | 97.45 | 159.46 |
| 城堡岩 | 华盛顿州 | 162.16 | 943.21 |
| 大理石岩 | IA | 97.13 | 391.49 |
| 矿物 | 加州 | 99.13 | 289.37 |
| 辽宁省 | 共和國 | 92.50 | 557.66 |
| 辽宁省 | IN | 122.50 | 557.66 |
| 科伊 | 共和國 | 187.85 | 943.98 |
| 塞西莉亚 | KY | 92.85 | 273.61 |
目的是展示我们将如何使用Java和Python来解决这些类型的问题。正如我们所看到的,这个例子非常简单,范围也很有限,但它将很容易被推广到更具挑战性的问题。
Java的方法
我们开始定义一个封装了数据条目的Java记录。
Java
record InputEntry(String city, String state, double basePrice, double actualPrice) {}
记录是JDK 14中引入的一种新的类型声明。它是一种定义不可变类的简明方式,提供了构造函数、访问函数、等价物和哈希实现。
接下来,我们读取CVS文件并将它们累积到一个列表中。
Java
List<InputEntry> inputEntries = readRecordEntriesFromCSVFile(recordEntries.csv);
为了按城市和州对输入的条目进行分组,我们定义了。
Java
record CityState(String city, String state) {};
我们用下面的类来封装属于一个组的所有条目的统计信息。
Java
record StatsAggregation(StatsAccumulator basePrice, StatsAccumulator actualPrice) {}
StatsAccumulator 是Guava库的一部分。你可以向该类添加一组双倍的值,它可以计算基本的统计数据,如计数、平均值、方差或标准差。我们使用 ,以获得 和 的统计数据 。StatsAccumulator basePrice actualPrice InputEntry
现在我们有了解决我们问题的所有材料。Java Streams提供了一个强大的框架来实现数据操作和分析。它的声明式编程风格,对选择、过滤、分组和聚合的支持,简化了数据操作和统计分析。它的框架还提供了一个强大的实现,可以处理大量的(甚至是无限的流),并通过使用并行性、懒惰性和短路操作来非常有效地运行。所有这些特性使Java Streams成为解决这些类型问题的绝佳选择。其实现非常简单。
Java
Map<CityState, StatsAggregation> stats = inputEntries.stream().
filter(i -> !(i.state().equals("MN") || i.state().equals("CA"))).collect(
groupingBy(entry -> new CityState(entry.city(), entry.state()),
collectingAndThen(Collectors.toList(),
list -> {StatsAccumulator sac = new StatsAccumulator();
sac.addAll(list.stream().mapToDouble(InputEntry::basePrice));
StatsAccumulator sas = new StatsAccumulator();
sas.addAll(list.stream().mapToDouble(InputEntry::actualPrice));
return new StatsAggregation(sac, sas);}
)));
在代码的第2行,我们使用Stream::filter 。这是一个布尔值 函数来测试列表中的元素。我们实现了一个lambda表达式来删除任何包含 "MN "或 "CA "状态的条目。
然后我们继续收集列表中的元素并调用Collectors::groupingBy() (第3行),它需要两个参数。
- 一个分类函数,我们使用我们的
CityState记录来做按城市和州的分组(第3行)。 - 下游的收集器,其中包含属于同一城邦的项目。我们使用
Collectors::collectingAndThen(第4行),它需要两个参数,分两步进行还原。- 我们使用
Collectors::toList(第4行),它返回一个收集器,将属于同一城市-州的所有元素累积成一个列表。 - 我们对这个列表进行整理转换。我们使用一个lambda函数(第5行至第9行)来定义两个
StatsAccumulator(s),在这里我们分别计算前一个列表中的basePrice和actualPrice条目的统计数据。最后,我们返回一个新创建的包含这些条目的StatsAggregation记录。
- 我们使用
总而言之,我们返回一个Map<CityState, StatsAggregation> ,其中键代表分组的城市-国家对,其值是一个StatsAggregation ,其中包含每个键的basePrice 和actualPrice 的统计数据。
正如我们之前提到的,使用Java Streams的一个关键优势是,它提供了一个简单的机制,可以使用多线程进行并行处理。这允许利用CPU的多核资源同时执行多个线程。只需在流中添加一个 "并行",如图所示。
Java
Map<CityState, StatsAggregation> stats = inputEntries.stream().parallel().
导致流框架将条目列表细分为若干部分,并在不同的线程中同时运行。当所有不同的线程完成它们的计算时,框架会将它们串行地添加到生成的Map中。
还有一个额外的优化是在第4行使用Collectors::groupingByConcurrent ,而不是Collectors:groupingBy 。在这种情况下,框架使用一个并发的Map,允许将不同线程的元素直接插入这个Map中,而不是必须串行地组合。
有了这三种可能性,我们可以检查它们在做之前的统计计算时的表现(不包括从CSV文件加载数据的时间),因为负载从五百万条增加到了两千万条。
| 串行 | 并行 | 并行和GroupByConcurrent | |
| 五百万条数据 | 3.045秒 | 1.941秒 | 1.436秒 |
| 1000万个条目 | 6.405秒 | 2.876秒 | 2.785秒 |
| 两千万条记录 | 8.507秒 | 4.956秒 | 4.537秒 |
我们看到,并行运行大大改善了性能;随着负载的增加,它的时间几乎减半。使用GroupByConcurrent也有10%的额外收益。
最后,获得结果是很简单的;例如,要获得印第安纳州Blountsville的统计数字,我们只需要。
爪哇
StatsAggregation aggreg = stateAggr.get(new CityState("Blountsville ", "IN"));
System.out.println("Blountsville, IN");
System.out.println("basePrice.mean: " + aggreg.basePrice().mean());
System.out.println("basePrice.populationVariance: " + aggreg.basePrice().populationVariance());
System.out.println("basePrice.populationStandardDeviation: " + aggreg.basePrice().populationStandardDeviation());
System.out.println("actualPrice.mean: " + aggreg.basePrice().mean());
System.out.println("actualPrice.populationVariance: " + aggreg.actualPrice().populationVariance());
System.out.println("actualPrice.populationStandardDeviation: " + aggreg.actualPrice().populationStandardDeviation());
我们获得的结果。
纯文本
Blountsville : IN
basePrice.mean: 50.302588996763795
basePrice.sampleVariance: 830.7527439246837
basePrice.sampleStandardDeviation: 28.822781682632293
basePrice.count: 309
basePrice.min: 0.56
basePrice.max: 99.59
actualPrice.mean: 508.8927831715211
actualPrice.sampleVariance: 78883.35878833274
actualPrice.sampleStandardDeviation: 280.86181440048546
actualPrice.count: 309
actualPrice.min: 0.49
actualPrice.max: 999.33
Python的方法
在Python中,我们有几个库可以处理数据统计和分析。然而,我们发现Pandas库非常适用于处理大量的表格数据,并提供非常有效的过滤、分组和统计分析方法。
让我们回顾一下我们将如何使用Python来分析前面的数据。
Python
import pandas as pd
def group_aggregations(df_group_by):
df_result = df_group_by.agg(
{'basePrice': ['count', 'min', 'max', 'mean', 'std', 'var'],
'actualPrice': ['count', 'min', 'max', 'mean', 'std', 'var']}
)
return df_result
if __name__ == '__main__':
df = pd.read_csv("recordEntries.csv")
excluded_states = ['MN', 'CA']
df_st = df.loc[~ df['state'].isin(excluded_states)]
group_by = df_st.groupby(['city', 'state'], sort=False)
aggregated_results = group_aggregations(group_by)
在主体部分,我们首先调用pandas.read_csv() (第11行),将文件中的逗号分隔的数值加载到PandasDataFrame 。
在第13行,我们使用~df['state'].isin(excluded_states) ,得到一个PandasSeries 的Booleans ,其中有False ,用于排除州(MN和CA)。最后,我们对这个系列使用pandas.loc() ,把它们过滤掉。
接下来,我们在第14行使用DataFrame.groupby() ,按城市和州分组。结果由group_aggregations() 来处理,得到basePrice 和actualPrice 的每一组的统计数据。
我们看到,在Python中的实现是非常简单的。要打印印第安纳州Blountsville的结果。
Python
print(aggregated_results.loc['Blountsville', 'IN']['basePrice'])
print(aggregated_results.loc['Blountsville', 'IN']['actualPrice'])
这样我们就得到了统计结果。
Python
base_price:
Name: (Blountsville, IN), dtype: float64
count 309.000000
min 0.560000
max 99.590000
mean 50.302589
std 28.822782
var 830.752744
actual_price:
Name: (Blountsville, IN), dtype: float64
count 309.000000
min 0.490000
max 999.330000
mean 508.892783
std 280.861814
var 78883.358788
要并行运行前面的代码,我们必须记住,Python并不像Java那样支持细粒度的锁机制。我们必须与全局解释器锁(GIL)作斗争,无论你有多少个CPU多核或线程,一次只允许一个线程执行。我们不会去讨论这些细节。
为了支持并发性,我们必须考虑到我们有一个CPU密集型的进程,因此,最好的方法是使用多进程。在这种情况下,我们必须修改我们的实现。
Python
from multiprocessing import Pool
import pandas as pd
def aggreg_basePrice(df_group):
ct_st, grp = df_group
return ct_st, grp.basePrice.agg(['count', 'min', 'max', 'mean', 'std', 'var'])
if __name__ == '__main__':
df = pd.read_csv("recordEntries.csv")
start = time.perf_counter()
excluded_states = ['MN', 'CA']
filtr = ~ df['state'].isin(excluded_states)
df_st = df.loc[filtr]
grouped_by_ct_st = df_st.groupby(['city', 'state'], sort=False)
with Pool() as p:
list_parallel = p.map(aggreg_basePrice, [(ct_st, grouped) for ct_st, grouped in grouped_by_ct_st])
print(f'Time elapsed parallel: {round(finish - start, 2)} sec')
正如我们之前所做的,我们使用Pandasgroupby() 来获得按城市和州分组的数据(第14行)。在下一行,我们使用多处理库提供的Pool() 来映射分组数据,使用aggreg_basePrice 来计算每组的统计数据。Pool() 对数据进行分割,并在几个并行的独立进程中进行统计计算。
我们将不详细回顾前面的代码,因为正如我们在下面的表格中所显示的,多进程比串行运行的进程慢得多。因此,对于这些类型的问题,不值得使用这种方法。
另一种并发运行代码的可能性是使用Modin。Modin提供了一种无缝的方式来并行化你的代码,当你必须处理大量的数据时,它非常有用。将导入语句从import pandas as pd 改为import modin.pandas as pd ,可以并行地运行你的代码,并利用你的环境中可能存在的核心集群来加快代码的执行速度。关于它如何工作的更多细节,请阅读文档。
正如我们对Java所做的那样,我们提供了下表,其中包含了我们刚刚涉及的不同场景的运行时间(和以前一样,我们不包括从CSV文件中读取数据的时间)。
| 串行 | 多进程 | Modin Proc | |
| 五百万条信息 | 1.94秒 | 20.25秒 | 6.99秒 |
| 1000万个条目 | 4.07秒 | 25.1秒 | 12.88秒 |
| 两千万条 | 7.62秒 | 36.2秒 | 25.94秒 |
我们看到,在Python中串行运行代码甚至比在Java中略快。然而,使用多进程会大大降低性能。使用Moding可以改善结果,但仍然是串行运行进程更有优势。
值得一提的是,和以前一样,我们在计算时间时不包括从CSV文件中读取数据的时间。
我们看到,对于Pandas中的CPU密集型进程,代码的并行化并无优势。从某种意义上说,这也反映了Pandas最初的架构方式。然而,令人印象深刻的是,Pandas在串行模式下的运行速度非常快,而且即使对于大量的数据,也有很好的扩展性。
需要指出的是,Python中统计数字的计算速度取决于它们的执行方式。为了得到快速的计算,在应用需要的统计函数时需要谨慎。例如,做一个简单的pandas.DataFrame.describe() 来得到统计数字,运行速度会非常慢。
我们已经看到,Java的Streams或Python的Pandas是对大量数据进行分析和统计的两个优秀选择。两者都有非常坚实的框架,有很多支持,可以实现很好的性能和扩展性。
Java提供了一个非常强大的基础设施,是处理复杂程序流的理想选择。它的性能非常好,可以有效地并行运行各种进程。这使得它在需要快速获得结果的时候成为理想的选择。
Python很适合做数学和统计学。它非常直截了当,速度相当快,很适合做复杂的计算。
数据科学 统计学 Java(编程语言) Python(语言)
DZone贡献者所表达的观点属于他们自己。
在DZone上很受欢迎
评论