计算行出现次数并除以总行数 - unix / python
我有一个文本文件 test.in
,内容如下:
english<tab>walawala
foo bar<tab>laa war
foo bar<tab>laa war
hello world<tab>walo lorl
hello world<tab>walo lorl
foo bar<tab>laa war
我想要的输出结果应该是:
english<tab>walawala<tab>0.1666
foo bar<tab>laa war<tab>0.5
hello world<tab>walo lorl<tab>0.3333
新的一列是每行的计数除以总行数。
目前我做的是:
cat test.in | uniq -c | awk '{print $2"\t"$3"\t"$1}' > test.out
但这样只给我行数的计数,而不是概率。而且,我的文件真的很大,大约有10亿行,每列至少有20个字符。
我该如何快速且正确地得到想要的输出结果?
有没有一种快速的Python解决方案?
8 个回答
也许可以通过在Python中使用字典,这种字典自动只能有一个值。
from collections import defaultdict
my_dict_counter = defaultdict(float)
counter = 0
for line in open('test.in'):
my_dict_counter[line] += 1
counter += 1
for line in my_dict_counter:
print line.strip() + "\t" + str(my_dict_counter[line]/counter)
这是一个用Python写的解决方案,不过我不太确定它在处理10亿行数据时的表现如何。
d = {}
s = "english<tab>walawala\nfoo bar<tab>laa war\nfoo bar<tab>laa war\nhello world<tab>walo lorl\nhello world<tab>walo lorl\nfoo bar<tab>laa war"
c = 0
for l in s.split("\n"):
c += 1
if d.has_key(l):
d[l] += 1
else:
d[l] = 1
for k,v in d.items():
print k + " -> " + str(float(v)/float(c))
输出结果:
english<tab>walawala -> 0.166666666667
foo bar<tab>laa war -> 0.5
hello world<tab>walo lorl -> 0.333333333333
补充说明:这个解决方案可以通过使用Python中的Counter对象来改进:https://docs.python.org/2/library/collections.html#collections.Counter
from collections import Counter
with open('data.txt') as infile:
# Counter will treat infile as an iterator and exhaust it
counter = Counter(infile)
# Don't know if you need sorting but this will sort in descending order
counts = ((line.strip(), n) for line, n in counter.most_common())
# Convert to proportional amounts
total = sum(counter.values())
probs = [(line, n / total) for line, n in counts]
print("\n".join("{}{}".format(*p) for p in probs))
这样做有几个好处。它是逐行读取文件,而不是一次性把整个文件都加载进来;它利用了现有的 Counter
功能;它可以进行排序,而且整个过程很清晰,容易理解。
这里有一个纯粹的 AWK 解决方案:
<test.in awk '{a[$0]++} END {for (i in a) {print i, "\t", a[i]/NR}}'
它使用了 AWK 的数组和一个特殊的变量 NR
,这个变量用来记录行数。
我们来拆解一下代码。第一个代码块
{a[$0]++}
会在输入的每一行上执行 一次。在这里,$0
代表每一行的内容,它被用作数组 a
的索引,因此这个数组就 用来统计每一行出现的次数。
第二个代码块
END {for (i in a) {print i, "\t", a[i]/NR}}
是在输入结束时执行的。到这个时候,a
中已经包含了每一行出现的次数,并且这些次数是通过行内容来索引的:所以我们可以通过遍历这个数组来打印出一个行和相对出现次数的表格(我们将次数除以总行数 NR
)。
请注意,uniq 只会计算重复的行,而且它必须在 sort 之后使用,这样才能考虑文件中的所有行。对于 sort | uniq -c
,下面这个使用 collections.Counter 的代码要有效得多,因为它根本不需要排序:
from collections import Counter
with open('test.in') as inf:
counts = sorted(Counter(line.strip('\r\n') for line in inf).items())
total_lines = float(sum(i[1] for i in counts))
for line, freq in counts:
print("{}\t{:.4f}".format(line, freq / total_lines))
这个脚本的输出是
english<tab>walawala<tab>0.1667
foo bar<tab>laa war<tab>0.5000
hello world<tab>walo lorl<tab>0.3333
对于你描述的输入。
不过,如果你只想合并相邻的行,像 uniq -c
那样,请注意,任何使用 Counter
的解决方案都会给出你问题中的输出,但你的 uniq -c
方法不会。uniq -c
的输出将是:
1 english<tab>walawala
2 foo bar<tab>laa war
2 hello world<tab>walo lorl
1 foo bar<tab>laa war
不
1 english<tab>walawala
3 foo bar<tab>laa war
2 hello world<tab>walo lorl
如果这是你想要的结果,你可以使用 itertools.groupby
:
from itertools import groupby
with open('foo.txt') as inf:
grouper = groupby(line.strip('\r\n') for line in inf)
items = [ (k, sum(1 for j in i)) for (k, i) in grouper ]
total_lines = float(sum(i[1] for i in items))
for line, freq in items:
print("{}\t{:.4f}".format(line, freq / total_lines))
不同之处在于,给定一个内容如你所描述的 test.in
,uniq 管道将不会产生你在示例中给出的输出,而是会得到:
english<tab>walawala<tab>0.1667
foo bar<tab>laa war<tab>0.3333
hello world<tab>walo lorl<tab>0.3333
foo bar<tab>laa war<tab>0.1667
由于这与你的输入示例不符,可能你不能在不使用 sort
的情况下解决你的问题——那么你需要回到我第一个示例,Python 的速度肯定会比你的 Unix 命令行快。
顺便说一下,这些在所有版本大于 2.6 的 Python 中效果是一样的。