用Java和Python进行数据统计和分析

741 阅读11分钟

DZone>大数据专区>用Java和Python进行数据统计和分析

用Java和Python进行数据统计和分析

如何使用Java Streams和Python Pandas来分析表格数据。以及比较它们对大量数据的性能和规模。

Manu Barriola user avatar 通过

马努-巴里奥拉

-

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.1733.19
传教士沼泽地WA27,4690.17
杜南角纽约州92.0162.46
窦南角纽约州97.45159.46
城堡岩华盛顿州162.16943.21
大理石岩IA97.13391.49
矿物加州99.13289.37
辽宁省共和國92.50557.66
辽宁省IN122.50557.66
科伊共和國187.85943.98
塞西莉亚KY92.85273.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) {}

StatsAccumulatorGuava库的一部分。你可以向该类添加一组双倍的值,它可以计算基本的统计数据,如计数、平均值、方差或标准差。我们使用 ,以获得 和 的统计数据 。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),在这里我们分别计算前一个列表中的basePriceactualPrice 条目的统计数据。最后,我们返回一个新创建的包含这些条目的StatsAggregation 记录。

总而言之,我们返回一个Map<CityState, StatsAggregation> ,其中键代表分组的城市-国家对,其值是一个StatsAggregation ,其中包含每个键的basePriceactualPrice 的统计数据。

正如我们之前提到的,使用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) ,得到一个PandasSeriesBooleans ,其中有False ,用于排除州(MN和CA)。最后,我们对这个系列使用pandas.loc() ,把它们过滤掉。

接下来,我们在第14行使用DataFrame.groupby() ,按城市和州分组。结果由group_aggregations() 来处理,得到basePriceactualPrice 的每一组的统计数据。

我们看到,在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上很受欢迎


评论

大数据 合作伙伴资源