打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
使用整数规划进行多条件数据分配

之前遇到了这样一个问题:

总结就是,将一批名单分成两组使得产品个数和销售额尽可能相等。

单纯的平分销售额或者平分产品个数会非常简单,但是要求两个维度同时进行就比较复杂,观察可以看到销售额的数字远远高于产品个数,我们可以先控制要求产品个数相等的情况下,找出销售额相差最小的方案,如果销售额相差较大,可以降低产品个数相差要求再看。慢慢提高相差范围,最终凭感性找出一个比较平衡的值。

首先读取数据(复制下面的表格后再执行下面的代码即可得到相同的数据):

import pandas as pd
import numpy as np

df = pd.read_clipboard()
df
产品经理产品个数销量销售额
0黄腾飞381403375680156.5
1陈心悦64942984905685.2
2赵音诺54723343717523.2
3张馨文26640693101085.6
4夏佳倩23496992669572.1
5熊雪君17390312555844.4
6徐安娜18491172269450.4
7刘航33430662084240.0
8张羚钰2382441227615.6
9银素蝶14184861193600.5
10严诚68271937001.5
11唐敏1313372590821.6
12刘婷97072536381.8
13宗兴武75416376375.0
14张诗蕊28515336892.5
15赵嘉成12344299836.0
16王霞44104209033.0
17毕隆滔83549208318.8
18王彬彬53599207989.9
19但伊静43476153416.8
20张丹62614131388.8
21马一丹61450128639.6
22郭哲达72590124674.6
23黄怡4229375829.7
24何虹275171925.1
25仲倩影394936375.1
26秦文君142020958.0
27关贞卓147820218.2
28黄蕊18619994.0
29许国露135717374.3

pulp求解

下面我们使用pulp进行整数规划进行求解,首先尝试设置约束要求划分的两个数据集产品个数相等,其次要求两个数据集销售额的差值尽可能的小。

可惜pulp默认的cbc求解器并不支持绝对值约束,我的方案是先限制a大于b之后,再求a-b的最小值。

from pulp import *
import numpy as np

prob = LpProblem('目标函数和约束', sense=LpMinimize)
# 第一份数据集被选中的位置
x = np.array([LpVariable(f"x{i}", cat=LpBinary) for i in range(df.shape[0])])
# 剩余被选中的位置
y = 1-x

# 首先尝试产品个数相等
prob += (x*df.产品个数).sum() == (y*df.产品个数).sum()
prob += (x*df.销售额).sum() >= (y*df.销售额).sum()
# 目标
prob += (x*df.销售额).sum() - (y*df.销售额).sum()
status = prob.solve()
print("求解状态:", LpStatus[prob.status])
r1 = df[[bool(value(i)) for i in x]]
r2 = df[[bool(value(i)) for i in y]]
print(r1.sum(numeric_only=True))
print(r2.sum(numeric_only=True))
print(r1.sum(numeric_only=True)-r2.sum(numeric_only=True))
display(r1)
display(r2)

executed in 10.3s, finished

产品个数         190.0
销量        326201.0
销售额     16954109.8
dtype: float64
产品个数         190.0
销量        354186.0
销售额     16954108.0
dtype: float64
产品个数        0.0
销量     -27985.0
销售额         1.8
dtype: float64

可以看到,销售额差值仅1.8,可以说差距已经非常小了,很可能已经就是最佳答案。我们再求解一下产品个数差距在2范围内时,销售额最低差值是多少:

from pulp import *
import numpy as np

prob = LpProblem('目标函数和约束', sense=LpMinimize)
# 第一份数据集被选中的位置
x = np.array([LpVariable(f"x{i}", cat=LpBinary) for i in range(df.shape[0])])
# 剩余被选中的位置
y = 1-x

# 产品个数差距小于3
prob += (x*df.产品个数).sum()-3 <= (y*df.产品个数).sum()
prob += (x*df.产品个数).sum()+3 >= (y*df.产品个数).sum()
# 两者的销售额的差值的绝对值最小
prob += (x*df.销售额).sum() >= (y*df.销售额).sum()
prob += (x*df.销售额).sum() - (y*df.销售额).sum()
status = prob.solve()
print("求解状态:", LpStatus[prob.status])
r1 = df[[bool(value(i)) for i in x]]
r2 = df[[bool(value(i)) for i in y]]
print(r1.sum(numeric_only=True))
print(r2.sum(numeric_only=True))
print(r1.sum(numeric_only=True)-r2.sum(numeric_only=True))
display(r1)
display(r2)

executed in 1.21s, finished

求解状态: Optimal
产品个数         189.0
销量        354048.0
销售额     16954110.2
dtype: float64
产品个数         191.0
销量        326339.0
销售额     16954107.6
dtype: float64
产品个数       -2.0
销量      27709.0
销售额         2.6
dtype: float64

相反计算出来的结果比之前的差距更大了,说明产品个数相同时就可以找到最佳答案。

MIP 求解器

ortools的SCIP求解器可以使用相同的思路求解这个问题:

from ortools.linear_solver import pywraplp

# 创建一个mip求解器
solver = pywraplp.Solver.CreateSolver('SCIP')
x = np.array([solver.BoolVar(f'x_{i}') for i in range(df.shape[0])])
y = 1-x
solver.Add((x*df.产品个数).sum() == (y*df.产品个数).sum())
solver.Add((y*df.销售额).sum() >= (x*df.销售额).sum())
solver.Minimize((y*df.销售额).sum() - (x*df.销售额).sum())
# 求解结果
status = solver.Solve()
status = {solver.OPTIMAL: "最优解", solver.FEASIBLE: "可行解"}.get(status)
print(status)
r1 = df[[bool(i.solution_value()) for i in x]]
r2 = df[[bool(i.solution_value()) for i in y]]
print(r1.sum(numeric_only=True))
print(r2.sum(numeric_only=True))
print(r1.sum(numeric_only=True)-r2.sum(numeric_only=True))
display(r1)
display(r2)

executed in 6.16s, finished

最优解
产品个数         190.0
销量        354186.0
销售额     16954108.0
dtype: float64
产品个数         190.0
销量        326201.0
销售额     16954109.8
dtype: float64
产品个数        0.0
销量      27985.0
销售额        -1.8
dtype: float64

cp_model求解

ortools的cp_model可以使用相同的思路求解这个问题,但是仅支持整数:

from ortools.sat.python import cp_model

model = cp_model.CpModel()
x = np.array([model.NewBoolVar(f'x_{i}') for i in range(df.shape[0])])
y = 1-x
model.Add((x*df.产品个数).sum() == (y*df.产品个数).sum())
v = (df.销售额*10).astype("int")
model.Add((x*v).sum() >= (y*v).sum())
model.Minimize((x*v).sum() - (y*v).sum())
# 求解结果
solver = cp_model.CpSolver()
status = solver.Solve(model)
status = {cp_model.OPTIMAL: "最优解", cp_model.FEASIBLE: "可行解"}.get(status)
print(status)
r1 = df[[bool(solver.Value(i)) for i in x]]
r2 = df[[bool(solver.Value(i)) for i in y]]
print(r1.sum(numeric_only=True))
print(r2.sum(numeric_only=True))
print(r1.sum(numeric_only=True)-r2.sum(numeric_only=True))
display(r1)
display(r2)

executed in 1.66s, finished

最优解
产品个数         190.0
销量        326201.0
销售额     16954109.8
dtype: float64
产品个数         190.0
销量        354186.0
销售额     16954108.0
dtype: float64
产品个数        0.0
销量     -27985.0
销售额         1.8
dtype: float64

这个求解器的优点是速度极快,2秒之内已经计算出结果。

回顾

最终划分结果为:

ortools的cp_model求解器虽然只支持整数操作,但速度最快。

参考:

  • OR-Tools官档中文用法大全
  • 使用Python进行线性规划求解
本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
计算各类别产品销售额的三种占比
lingo使用教程
华为机试HJ84:统计大写字母个数
(python3)pandas做数据分析:统计相关函数
Dataset之Rotten Tomatoes:Rotten Tomatoes影评数据集简介、下载、使用方法之详细攻略
Pandas分组统计函数:groupby、pivot
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服