Python 的多层嵌套循环如何优化?

2022-10-31 11:15:57 +08:00
 mmm159357456
result = list()

for x1 in list_a:
    for x2 in list_b:
        for x3 in list_c:
            // 任意层,xn 皆为 f 的必要参数
            _r = f(x1, x2, x3, *args)
            result.append(_r)

众所周知的 python 循环执行慢,如上情形如何优化?

6427 次点击
所在节点    Python
72 条回复
fairless
2022-10-31 17:08:38 +08:00
@mmm159357456 说起来你可能不信,那个函数执行一万次,python 要 26s c 写的只要 0.2 秒不到
fairless
2022-10-31 17:11:02 +08:00
当然我这个不仅仅是 for 循环,里面还涉及到大量的基础运算以及 list 、类型转换(int8 int32)等,c 的话运算速度以及数组都非常快,而且不存在类型转换的步骤。
wxf666
2022-10-31 17:28:29 +08:00
@mmm159357456 你原始数据是 CSV 之类的吗?

能给个 1900 年的数据吗?我想试试用 SQL 能多快 /慢

如果数据比较敏感,可以给个模拟数据。比如:(只要模拟完后,数据量差不多和原来一样即可。如存为 csv 后 1GB )

dateday, geometry, element1, element2, ..., elementN
1900-01-01, geometry1, 11111111, 11111111, ..., 11111111
1900-01-02, geometry2, 22222222, 22222222, ..., 22222222
...
mmm159357456
2022-10-31 17:30:40 +08:00
@wxf666 不好意思,数据给不了,原先数据是存在 csv 里面的,后面我清洗过后转成了 parquet
wxf666
2022-10-31 17:38:56 +08:00
@mmm159357456 给个模拟数据吧

- 列名是像上面那样吗?
- 平均每一年有多少行?
- 有多少种 geometry ?平均有多少个字符? geometry1 ? ggeeoommeettrryy11 ?
- 有多少个 element ?
mmm159357456
2022-10-31 18:05:34 +08:00
@wxf666

dateday, geometry, element1, element2, element3, element4


geometry 大概 300 个,为经纬度的组合,精细到小数点后四位;
dateday19010101-21001331 ,timedelta 为 1d ;
element[1-3]均需要处理成滑动平均,element4 计算滑动窗口的总和,所有 elements 值域在[0, 50]

以上组合完成后需要两个处理水平重复,外加 20 种模式(即 X 2 X 20
mmm159357456
2022-10-31 18:07:07 +08:00
更正:dateday19010101-21001231 ,timedelta 为 1d ;
wxf666
2022-10-31 18:33:02 +08:00
@mmm159357456 你还真跨了 200 年嘛。。😂


假设只有 3 天和 3 个 geometry ,下表符合最终源数据表的格式了嘛?(我把经纬度拆开了)

dateday, geometry_x, geometry_y, element1, element2, element3, element4
2000-01-01, 1.0000, -1.0000, 1, 2, 3, 4
2000-01-01, 2.0000, -2.0000, 5, 6, 7, 8
2000-01-01, 3.0000, -3.0000, 9, 10, 11, 12
2000-01-02, 1.0000, -1.0000, 13, 14, 15, 16
2000-01-02, 2.0000, -2.0000, 17, 18, 19, 20
2000-01-02, 3.0000, -3.0000, 21, 22, 23, 24
2000-01-03, 1.0000, -1.0000, 25, 26, 27, 28
2000-01-03, 2.0000, -2.0000, 29, 30, 31, 32
2000-01-03, 3.0000, -3.0000, 33, 34, 35, 36


下面这俩,是生成上表的规则?还是要求 SQL 计算时要实现的功能?

- 『 element[1-3]均需要处理成滑动平均,element4 计算滑动窗口的总和』
- 『以上组合完成后需要两个处理水平重复,外加 20 种模式(即 X 2 X 20 』


另外:

1. 滑动窗口有多大?要按什么分组和排序吗?(比如,按<年>分组,按<天, 经纬度>排序?)
2. 『两个处理水平重复,外加 20 种模式』是啥意思。。
mmm159357456
2022-10-31 18:41:03 +08:00
@wxf666 第一条即是需要实现的功能,原始数据中只有每天的数据,没有滑动,也没有合计

数据表就和你这个差不多,窗口是在 5 、10 、15 中滑动,分组的话按某一年某一点来算。

两个处理水平重复,外加 20 种模式,为上面的表乘以 2 再乘以 20 ,即 40 个相同结构的表但数据不一样
wxf666
2022-10-31 18:58:10 +08:00
@mmm159357456

你是说:

1. 滑动窗口按 <年, 地区> 分组,按 <时间> 顺序滑动
2. element1/2/3 是窗口大小分别为 5/10/15 的平均值,element4 = 此时这仨平均值之和
3. 每张表都这么算,共计算 40 次

mmm159357456
2022-10-31 19:03:52 +08:00
@wxf666
1.滑动窗口按 <年, 地区> 分组,按 <时间> 顺序滑动,计算 element1/2/3 的滑动平均值,并将值安置在滑动起点的同一行
2.element1/2/3 是窗口大小分别为 5/10/15 的平均值,element4 是对应滑动窗口内的 element4 值的合计
3.对
wxf666
2022-10-31 19:39:52 +08:00
@mmm159357456

假设下表是 2000 年某地区的一张表(省略年份和经纬度),列 1 窗口大小为 2 ,列 2 为 4 ,列 3 为 6:

  日期  列1 列2 列3 列4
—————————————————
01-01 11 21 31 41
01-02 12 22 32 42
01-03 13 23 33 43
01-04 14 24 34 44
01-05 15 25 35 45
01-06 16 26 36 46
01-07 17 27 37 47
01-08 18 28 38 48
01-09 19 29 39 49

1. 1 日、2 日、9 日,新的『列 1 』值应为多少?

a. (11+12)/2 ,(12+13)/2 ,19
b. (11+12)/2 ,不算 2 日,19
c. ……?

2. 新的『列 4 』值怎么算?(像上面那样,随便写两三个日期即可)
mmm159357456
2022-10-31 19:52:07 +08:00
列 1 窗口大小为 2 ,选择 min_period = 1,那么 01-01 对应 11 ,01-02 对应 (11+12)/2 ,01-03 对应(12+13)/2...01-09 对应(18+19)/2

列 4 相对于列 1 来说如果 窗口大小为 2 ,选择 min_period = 1 ,那么 01-01 对应 41 ,01-02 对应 41+42 ,01-03 对应 42+43....01-03 对应 48+49 (简而言之列 4 的窗口是相对于前面三列的窗口来相应调整)

所有的 rolling 均是向前滑动,即滑动的值均是针对已出现的值,计算 01-09 不能用 01-10 的值,因为 01-10 是将来时
wxf666
2022-10-31 20:12:43 +08:00
@mmm159357456

1. 你不是说『将值安置在滑动<起点>的同一行』吗?我就在想,(11+12)/2 要放在 11 那行。。

2. 上面假设了,『列 1 窗口大小为 2 ,列 2 为 4 ,列 3 为 6 』。所以,列 4 的窗口大小是怎么调整的?直接 = 列 1 窗口大小 = 2 ??

3. 所以,最终结果是 40 张表,每张表的表头是 (日期、经纬度、列 1 、2 、3 、4),共计 3KW 行??
mmm159357456
2022-10-31 20:23:17 +08:00
@wxf666
1.所有滑动是向前滑动,参考我上一条最后一句。(11+12)/2 是 01-02 的滑动值
2.对
3.其实说是有 40 张表,其实最开始是 element[1-4]是分别处在不同表里的,我跟你说的表都是合并过后的,具体的行数大概是 300*200*365*2*20
GoodRui
2022-10-31 20:42:12 +08:00
我觉得优化逻辑才是根本,可以参考恋爱循环的嵌套。
mmm159357456
2022-10-31 20:45:06 +08:00
c0xt30a
2022-10-31 22:41:23 +08:00
如果这里是热点,可以考虑 C++ 重写,然后用 Python 调用吧。一般来说能快两个数量级
daweii
2022-11-01 00:33:06 +08:00
@buyan3303 好文章 感谢推荐
wxf666
2022-11-01 09:29:59 +08:00
@mmm159357456 用 SQLite 测试了生成整张表、计算整张表,结果如下:

  项目      结果大小    用时
————————————————————
生成数据库 1.02GB的数据库 2分钟
计算整张表 1.35GB的CSV 7分钟


## 数据库预览( CSV 形式预览。200 年 x 365/366 天 x 360 个经纬度 = 26297640 行):

year,dateday,geometry_x,geometry_y,element1,element2,element3,element4
1900,1900-01-01,0.0,0.0,1,1001,2001,3001
1900,1900-01-02,0.0,0.0,2,1002,2002,3002
1900,1900-01-03,0.0,0.0,3,1003,2003,3003
……
1900,1900-12-29,0.0,0.0,363,1363,2363,3363
1900,1900-12-30,0.0,0.0,364,1364,2364,3364
1900,1900-12-31,0.0,0.0,365,1365,2365,3365
1900,1900-01-01,1.0,-1.0,1,1001,2001,3001
1900,1900-01-02,1.0,-1.0,2,1002,2002,3002
1900,1900-01-03,1.0,-1.0,3,1003,2003,3003
……
1900,1900-12-29,359.0,-359.0,363,1363,2363,3363
1900,1900-12-30,359.0,-359.0,364,1364,2364,3364
1900,1900-12-31,359.0,-359.0,365,1365,2365,3365
1901,1901-01-01,0.0,0.0,1,1001,2001,3001
1901,1901-01-02,0.0,0.0,2,1002,2002,3002
1901,1901-01-03,0.0,0.0,3,1003,2003,3003
……
2099,2099-12-29,359.0,-359.0,363,1363,2363,3363
2099,2099-12-30,359.0,-359.0,364,1364,2364,3364
2099,2099-12-31,359.0,-359.0,365,1365,2365,3365


## 计算结果预览( CSV 文件):

1900,0.0,0.0,1900-01-01,1.0,1001.0,2001.0,3001.0
1900,0.0,0.0,1900-01-02,1.5,1001.5,2001.5,3001.5
1900,0.0,0.0,1900-01-03,2.0,1002.0,2002.0,3002.0
1900,0.0,0.0,1900-01-04,2.5,1002.5,2002.5,3002.5
1900,0.0,0.0,1900-01-05,3.0,1003.0,2003.0,3003.0
1900,0.0,0.0,1900-01-06,4.0,1003.5,2003.5,3004.0
……


## 计算方式预览( CSV 形式。可保存后用 Excel 查看):

1900,0.0,0.0,1900-01-01,(1)/1,(1001)/1,(2001)/1,(3001)/1
1900,0.0,0.0,1900-01-02,(1+2)/2,(1001+1002)/2,(2001+2002)/2,(3001+3002)/2
1900,0.0,0.0,1900-01-03,(1+2+3)/3,(1001+1002+1003)/3,(2001+2002+2003)/3,(3001+3002+3003)/3
1900,0.0,0.0,1900-01-04,(1+2+3+4)/4,(1001+1002+1003+1004)/4,(2001+2002+2003+2004)/4,(3001+3002+3003+3004)/4
1900,0.0,0.0,1900-01-05,(1+2+3+4+5)/5,(1001+1002+1003+1004+1005)/5,(2001+2002+2003+2004+2005)/5,(3001+3002+3003+3004+3005)/5
1900,0.0,0.0,1900-01-06,(2+3+4+5+6)/5,(1001+1002+1003+1004+1005+1006)/6,(2001+2002+2003+2004+2005+2006)/6,(3002+3003+3004+3005+3006)/5
1900,0.0,0.0,1900-01-07,(3+4+5+6+7)/5,(1001+1002+1003+1004+1005+1006+1007)/7,(2001+2002+2003+2004+2005+2006+2007)/7,(3003+3004+3005+3006+3007)/5
1900,0.0,0.0,1900-01-08,(4+5+6+7+8)/5,(1001+1002+1003+1004+1005+1006+1007+1008)/8,(2001+2002+2003+2004+2005+2006+2007+2008)/8,(3004+3005+3006+3007+3008)/5
1900,0.0,0.0,1900-01-09,(5+6+7+8+9)/5,(1001+1002+1003+1004+1005+1006+1007+1008+1009)/9,(2001+2002+2003+2004+2005+2006+2007+2008+2009)/9,(3005+3006+3007+3008+3009)/5
1900,0.0,0.0,1900-01-10,(6+7+8+9+10)/5,(1001+1002+1003+1004+1005+1006+1007+1008+1009+1010)/10,(2001+2002+2003+2004+2005+2006+2007+2008+2009+2010)/10,(3006+3007+3008+3009+3010)/5
1900,0.0,0.0,1900-01-11,(7+8+9+10+11)/5,(1002+1003+1004+1005+1006+1007+1008+1009+1010+1011)/10,(2001+2002+2003+2004+2005+2006+2007+2008+2009+2010+2011)/11,(3007+3008+3009+3010+3011)/5
……
1900,0.0,0.0,1900-12-31,(361+362+363+364+365)/5,(1356+1357+1358+1359+1360+1361+1362+1363+1364+1365)/10,(2351+2352+2353+2354+2355+2356+2357+2358+2359+2360+2361+2362+2363+2364+2365)/15,(3361+3362+3363+3364+3365)/5
1900,1.0,-1.0,1900-01-01,(1)/1,(1001)/1,(2001)/1,(3001)/1
1900,1.0,-1.0,1900-01-02,(1+2)/2,(1001+1002)/2,(2001+2002)/2,(3001+3002)/2
……


## 使用方式:

去 SQLite 官网下载个 1 MB 的 sqlite3.exe ,然后保存下面的 SQLite 代码为 main.sql ,然后命令行运行:

```shell
sqlite3.exe data.db < main.sql
```


## SQLite 代码:

*( V 站排版原因,行首有全角空格)*

```sql
-- PRAGMA journal_mode = off; -- 取消日志记录。但这会输出个 off 。。
PRAGMA synchronous = off; -- 提交写请求给操作系统后,就可继续后续计算
PRAGMA cache_size = -8192; -- 占用 8 MB 内存

-- 建表
CREATE TABLE IF NOT EXISTS data (
   year INT,
   dateday DATE,
   geometry_x REAL,
   geometry_y REAL,
   element1 INT,
   element2 INT,
   element3 INT,
   element4 INT,
   PRIMARY KEY (year, geometry_x, geometry_y, dateday)
) WITHOUT ROWID;

-- 生成数据
INSERT INTO data
SELECT year.value,
    DATE(year.value || '-01-01', day_of_year.value || ' days'),
    area.value * 1.0,
    area.value * -1.0,
    day_of_year.value + 1,
    day_of_year.value + 1001,
    day_of_year.value + 2001,
    day_of_year.value + 3001
  FROM generate_series(1900, 2099) year,
    generate_series(0, STRFTIME('%j', year.value || '-12-31') - 1) day_of_year,
    generate_series(0, 359) area;

-- 计算表
SELECT year,
    geometry_x,
    geometry_y,
    dateday,
    /* -- 下面 4 行用于预览平均值的计算方式对不对
    FORMAT('(%s)/%d', GROUP_CONCAT(element1, '+') OVER win5, COUNT(*) OVER win5),
    FORMAT('(%s)/%d', GROUP_CONCAT(element2, '+') OVER win10, COUNT(*) OVER win10),
    FORMAT('(%s)/%d', GROUP_CONCAT(element3, '+') OVER win15, COUNT(*) OVER win15),
    FORMAT('(%s)/%d', GROUP_CONCAT(element4, '+') OVER win5, COUNT(*) OVER win5)
    */
    AVG(element1) OVER win5,
    AVG(element2) OVER win10,
    AVG(element3) OVER win15,
    AVG(element4) OVER win5
FROM data
WINDOW win AS (PARTITION BY year, geometry_x, geometry_y ORDER BY dateday),
    win5 AS (win ROWS 4 PRECEDING),
    win10 AS (win ROWS 9 PRECEDING),
    win15 AS (win ROWS 14 PRECEDING);
```

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/891370

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX