【问题标题】:First, Last, Next ordering in exporting CSV, Python导出 CSV、Python 中的 First、Last、Next 排序
【发布时间】:2014-03-07 00:36:14
【问题描述】:

我有一个包含项目和值的 CSV,它的表示形式如下所示:

foo, 569
bar, 9842
asdasd, 98
poiqweu, 7840
oiasd, 4
poeri, 145
sacodiw, 55
aosdwr, 855
9523, 60
a52sd, 5500
sdcw, 415
0932, 317

我想导出到三个 CSV,以便它们按以下顺序从主 CSV 接收项目:最高、最低、次高、次低等。

CSV1 应该是:

bar, 9842
oiasd, 4
poiqweu, 7840
sacodiw, 55

其他两个 CSV 依此类推。

作为奖励,我真正想要做的是从 270 个主文件中创建三个 90 个项目的 CSV,这样三个中的每一个都接近相同的值总和。其他尽可能。我认为有比我的简单(且高度假设)的方法更好的方法。

我将如何在我已经在使用的 python 脚本中处理这个问题(包括 CSV 和 pandas,如果后者有帮助的话)?

【问题讨论】:

  • 1) 创建一个元组列表。 2)按元组中的第二个条目(数字)排序 3)从排序的元组列表中写出 i、-i、i+1、-i-1、i+2、-i-2 等
  • 这听起来像是分区问题(但 3 路而不是 2 路)。这是 NP 完全的(获得保证的最佳解决方案的唯一方法是尝试所有组合),但是有各种启发式方法应该给出一个很好的近似值。编辑:见en.wikipedia.org/wiki/Partition_problem
  • 是否要求三个子集大小相同?
  • 你可能会发现this page对分区问题很有启发。
  • 谢谢你,jme 和 Hugh:这让我在几分钟内使用贪心算法得到了自己的答案。

标签: python python-2.7 csv pandas


【解决方案1】:

您可以使用以下构建块来解决问题(从这里开始应该不难):

使用pandas进行加载和排序:

import pandas as pd
original = pd.read_csv('test.csv', names=['name','count'])
df_highest_first  = df.sort(columns=['count'])
df_smallest_first = df.sort(columns=['count'], ascending=False)

largest_1 = df_largest['count'][0:-1:2].values
largest_2 = df_largest['count'][1:-2:2].values

smallest_1 = df_smallest['count'][0:-1:2].values
smallest_2 = df_smallest['count'][1:-2:2].values

然后izip 在列表对之间交错元素:

result = list(chain.from_iterable(izip(list_a, list_b)))

【讨论】:

  • 当然你只需要排序一次,只需向后一步。
【解决方案2】:

这里是部分解决方案;

reorder 是函数式的,但由于我对 pandas 不是很熟悉,所以我只是使用了 Python 的内置数据结构。

编辑:我已经用贪婪的实现替换了partition_by_sum;它试图找到相等的总和,但不注意每个 bin 的项目数。 对更好算法的建议?

这应该会给你一个很好的开端。

from collections import defaultdict
import csv

VALUE_COL = 1
NUM_BINS = 3

inp = [
    ["foo",      569],
    ["bar",     9842],
    ["asdasd",    98],
    ["poiqweu", 7840],
    ["oiasd",      4],
    ["poeri",    145],
    ["sacodiw",   55],
    ["aosdwr",   855],
    ["9523",      60],
    ["a52sd",   5500],
    ["sdcw",     415],
    ["0932",     317]
]

def load_csv(fname, **kwargs):
    with open(fname, "rb") as inf:
        for row in csv.reader(inf, **kwargs):
            yield row

def save_csv(fname, rows, **kwargs):
    with open(fname, "wb") as outf:
        csv.writer(outf, **kwargs).writerows(rows)

def make_index(lst, col):
    """
    Index a table by column;
    return list of column-values and dict of lists of rows having that value
    """
    values, index = [], defaultdict(list)
    for row in lst:
        val = row[col]
        values.append(val)
        index[val].append(row)
    return values, index

def min_index(lst):
    """
    Return index of min item in lst
    """
    return lst.index(min(lst))

def partition_by_sum(values, num_bins, key=None):
    """
    Try to partition values into lists having equal sum

    Greedy algorithm, per http://en.wikipedia.org/wiki/Partition_problem#Approximation_algorithm_approaches
    """
    values.sort(key=key, reverse=True)   # sort descending
    bins = [[] for i in xrange(num_bins)]
    sums = [0] * num_bins
    for value in values:
        index = min_index(sums)
        bins[index].append(value)
        sums[index] += value
    return bins

def reorder(lst, key=None):
    """
    Return [highest, lowest, second-highest, second-lowest, ...]
    """
    lst.sort(key=key, reverse=True)    # sort in descending order
    halflen = (len(lst) + 1) // 2      # find midpoint
    highs, lows = lst[:halflen], lst[halflen:][::-1]   # grab [high half descending], [low half ascending]
    lst[0::2], lst[1::2] = highs, lows                 # reassemble
    return lst

def main():
    # load data
    data = inp    # load_csv("input_file.csv")

    # solve partitioning    
    values, index = make_index(data, VALUE_COL)
    bins = partition_by_sum(values, NUM_BINS)

    # rearrange for output
    bins = [[index[val].pop() for val in reorder(bin)] for bin in bins]

    # write output
    for i,bin in enumerate(bins, 1):
        save_csv("output_file_{}.csv".format(i), bin)

if __name__=="__main__":
    main()

【讨论】:

    【解决方案3】:

    如果数据有 N 行,我会采用这种方法:

    • 对输入数据进行降序排序。
    • 创建 3 个空列表
    • 遍历已排序的数据,并将当前行添加到总和最低的列表中,除非此列表已经有 N/3 或更多条目

    在阅读了维基百科上关于the partition problem 的页面后,我看到这个算法是the greedy algorithm 的改编版,唯一的例外是我要求所有子集的长度相同(如果 N % 3 == 0) .

    我编写了一个简单的代码 sn-p 为您演示它。我认为这比您提出的解决方案更好地解决您的问题。从下面的输出可以看出,第一个数据集包含最大值和 3 个最小值。您提出的解决方案会导致总和的差异更大。

    import csv
    
    class DataSet:
        def __init__(self, filename):
            self.total = 0
            self.data = []
            self.filename = filename
    
        def add(self, row):
            self.total += int(row[1])
            self.data.append(row)
    
        def write(self):
            with open(self.filename, 'wb') as ofile:
                writer = csv.writer(ofile)
                writer.writerows(self.data)
    
    with open('my_data.csv') as ifile:
        data = sorted(csv.reader(ifile), key=lambda l: -int(l[1]))
    
    subsets = DataSet('data_1.csv'), DataSet('data_2.csv'), DataSet('data_3.csv')
    
    for row in data:
        sets = [k for k in subsets if len(k.data) < 4]
        min(sets, key=lambda x: x.total).add(row)
    
    for k in subsets:
        print k.data, k.total
        k.write()
    

    输出:

    [['bar', ' 9842'], ['9523', ' 60'], ['sacodiw', ' 55'], ['oiasd', ' 4']] 9961
    [['poiqweu', ' 7840'], ['0932', ' 317'], ['poeri', ' 145'], ['asdasd', ' 98']] 8400
    [['a52sd', ' 5500'], ['aosdwr', ' 855'], ['foo', ' 569'], ['sdcw', ' 415']] 7339
    

    【讨论】:

    • 我喜欢在没有编码知识的情况下看到相同的基本概念,但编写正确。这是你不能在课堂上得到的那种东西!最好的部分是我可以更轻松地调整此代码,并且可以将集合中的“4”替换为可以拆分我需要的任何内容的变量。
    • @Xodarap777 很高兴听到这个消息,您的编码练习只会通过练习(并且通过阅读文档!)变得更好。如果它回答了您的问题,请考虑接受此答案:)
    【解决方案4】:

    jme 和 Hugh Bothwell 将我与分区问题联系起来,在那里我可以找到贪婪算法,我很快在 Python-2.7 中针对 CS101 风格的代码进行了调整:

    import csv
    
    inf = csv.reader(open('ACslist.csv', 'r'))
    out1 = csv.writer(open('ACs1.csv', 'wb'))
    out2 = csv.writer(open('ACs2.csv', 'wb'))
    out3 = csv.writer(open('ACs3.csv', 'wb'))
    
    firstrow = inf.next()
    out1.writerow(firstrow)
    out2.writerow(firstrow)
    out3.writerow(firstrow)
    
    sum1 = 0
    sum2 = 0
    sum3 = 0
    
    count1 = 0
    count2 = 0
    count3 = 0
    
    
    for row in inf:
        row[1] = int(row[1])
        if sum1 == 0:
            out1.writerow(row)
            count1 += 1
            sum1 += row[1]
        elif sum2 == 0:
            out2.writerow(row)
            count2 += 1
            sum2 += row[1]
        elif sum1 < sum2 and sum1 < sum3 and count1 < 90:
            out1.writerow(row)
            count1 += 1
            sum1 += row[1]
        elif sum2 < sum1 and sum2 < sum3 and count2 < 90:
            out2.writerow(row)
            count2 += 1
            sum2 += row[1]
        elif sum3 < sum2 and sum3 < sum1 and count3 < 90:
            out3.writerow(row)
            count3 += 1
            sum3 += row[1]
        elif count1 < 90:
            out1.writerow(row)
            count1 += 1
            sum1 += row[1]
        elif count2 < 90:
            out2.writerow(row)
            count2 += 1
            sum2 += row[1]
    
    print sum1
    print sum2
    print sum3
    

    我的打印输出是这样的:

    122413
    122397
    122399
    

    如果我自己这么说,那就太接近了!

    在我非常业余的眼中,这似乎是一个更简单的解决方案。我确信我可以更有效地编写它。如果有人想指出我在风格上的缺陷,我很乐意提供帮助。

    【讨论】:

    • 我已经适应了相同的算法,请看看我的答案:)
    • "如果有人想指出我的风格缺陷" -- 有一个网站:codereview.stackexchange.com,您可以在其中发布有效的代码(就您而言知道)但您想获得反馈。
    • 嘿,这就是他们教你在编程课程介绍中使用 python 的方式。我必须从那里适应... :) 它可能会让你哭泣,但结果是完美的 - 3 个 90 项的列表几乎完全相同。
    • 哦,拜托,真的吗?作为一个 python 新手尝试自己做这件事而投反对票?它适用于它的目的 - 考虑到我对正式的 python 一无所知,这有点整洁,至少......
    • @Xodarap777 这当然取决于你,但正如你在我的帖子下方的评论中所说,“我喜欢在没有编码知识的情况下看到相同的基本概念 - 但是写得对。”,你确实更喜欢我的版本。你为什么要把我接受的答案改成你的?
    猜你喜欢
    • 1970-01-01
    • 2014-08-21
    • 2017-11-02
    • 2011-01-22
    • 2020-10-11
    • 1970-01-01
    • 2018-09-15
    • 2021-10-28
    • 1970-01-01
    相关资源
    最近更新 更多