Integer Programming - If then statement - constraints

I am new to linear/integer programming and I am having a hard time formulating constraints for a specific if-then statement in a fixed charge problem. Suppose that there are five manufacturers of t-shirts, and a customer wishes to purchase 400 t-shirts while minimizing costs.
Producer
Variable cost/t-shirt
Delivery
Availability
A
3
40
200
B
3.5
30
100
C
4.10
Free delivery
100
D
4.1
30
200
E
3.2
30
First 100 t-shirts
E
2.90
20
101st-150th t-shirt
Producer E has an availability of 150 t-shirts. The first 100 t-shirts bought from producer E have a variable cost of $3.20 and a delivery fee of $30. If the customer orders more than 100 t-shirts from producer E, she can buy them at a variable cost of $2.90 and an additional fee of $20.
How can I create constraints from this if-then statement:
Xe1 = number of expensive t-shirts bought at producer E
Xe2 = number of cheap t-shirts bought at producer E
I want constraint Xe2 <= 0 to exist when Xe1 < 100.
Thanks in advance!

Since Xe1 and Xe2 are both non-negative integer variables, your constraint is the same as Xe1 <= 99 ā‡’ Xe2 = 0. This can be formulated as follows:
b1 <= b2
99*b1 + 100*(1-b1) <= Xe1 <= 99*b1 + M*(1-b1)
1-b2 <= Xe2 <= M'*(1-b2)
where b1,b2 are binary helper variables and M and M' are upper bounds for Xe1 and Xe2, i.e. M = 101 and M' = 400.

This is a pretty standard "discount pricing model" where a discount price is awarded at a certain quantity. You can introduce 1 binary variable to the model to do this. This sounds like a H/W assignment.... :)
Let:
Xe1 be the qty bought from E at price 1
Xe2 be the qty bought from E at price 2
Ye be the decision to buy at the second price point y āˆˆ {0, 1}
Me2 be the qty available at the 2nd price (or a reasonable upper bound)
Then:
Xe2 <= Ye * Me2 # purchase at 2nd price point held to zero, unless Ye==1
Xe1 >= 100 * Ye # Ye can only be 1 if 100 are purchased at Xe1
There are some nuances here with the shipping. It isn't clear from the problem description if you bought 101 shirts from E if the shipping cost would be 30 + 20 (which would be odd) or the whole cost of shipping drops to 20. You could use the same indicator variable Ye to work something out for that as well.

Related

Get the previous calculated record divided by group - SQL

I'm strugging to build two calculated columns (named balance and avg). My original SQLite base is:
name seq side price qnt
groupA 1 B 30 100
groupA 2 B 36 200
groupA 3 S 23 300
groupA 4 B 30 100
groupA 5 B 54 400
groupB 1 B 70 300
groupB 2 B 84 300
groupB 3 B 74 600
groupB 4 S 90 100
Rational for the 2 calculated new columns:
balance: the first line of each group (seq = 1), must have the same value of qnt. The next records follow the below formula (Excel-based scheme):
if(side="B"; `previous balance record` + `qnt`; `previous balance record` - `qnt`)
avg: the first line of each group (seq = 1), must have the same value of price. The next records follow the below formula (Excel-based scheme):
if(side="B"; ((`price` \* `qnt`) + (`previous balance record` \* `previous avg record`)) / (`qnt` + `previous balance record`); `previous avg record`)
Example with numbers (the second row of groupA is calculated below):
--> balance: 100 + 200 = 300
--> avg: ((36 * 200) + (100 * 30)) / (200 + 100) = 34
I think this problem must be solved with CTE because I need the previous record, which is in being calculated every time.
I wouldn't like to aggregate groups - my goal is to display every record.
Finally, this is what I expect as the output:
name seq side price qnt balance avg
groupA 1 B 30 100 100 30
groupA 2 B 36 200 300 34
groupA 3 S 23 300 0 34
groupA 4 B 30 100 100 30
groupA 5 B 54 400 500 49,2
groupB 1 B 70 300 300 70
groupB 2 B 84 300 600 77
groupB 3 B 74 600 1200 75,5
groupB 4 S 90 100 1100 75,5
Thank you in advance!
Here is my dbfiddle test: https://dbfiddle.uk/TSarc3Nl
I tried to explain part of the coding (commented) to make things easier.
The balance can be derived from a cumulative sum (using a case expression for when to deduct instead of add).
Then the recursive part just needs a case expression of its own.
WITH
adjust_table AS
(
SELECT
*,
SUM(
CASE WHEN side='B'
THEN qnt
ELSE -qnt
END
)
OVER (
PARTITION BY name
ORDER BY seq
)
AS balance
FROM
mytable
),
recurse AS
(
SELECT adjust_table.*, price AS avg FROM adjust_table WHERE seq = 1
UNION ALL
SELECT
n.*,
CASE WHEN n.side='B'
THEN ((n.price * n.qnt * 1.0) + (s.balance * s.avg)) / (n.qnt + s.balance)
ELSE s.avg
END
AS avg
FROM
adjust_table n
INNER JOIN
recurse s
ON n.seq = s.seq + 1
AND n.name = s.name
)
SELECT
*
FROM
recurse
ORDER BY
name,
seq
https://dbfiddle.uk/mWz945pG
Though I'm not sure what the avg is meant to be doing, so it's possible I got that wrong and/or it could possibly be simplified to not need recursion.
NOTE: Never use , to join tables.
EDIT: Recursion-less version
Use window functions to accumulate the balance, and also the total spent.
Then, use that to a enable the use of another window function to accumulate how much 'spend' is being 'recouped' by sales.
Your avg is then the adjusted spend divided by the current balance.
WITH
accumulate AS
(
SELECT
*,
SUM(
CASE WHEN side='B' THEN qnt ELSE -qnt END
)
OVER (
PARTITION BY name
ORDER BY seq
)
AS balance,
1.0
*
SUM(
CASE WHEN side='B' THEN price * qnt END
)
OVER (
PARTITION BY name
ORDER BY seq
)
AS total_spend
FROM
mytable
)
SELECT
*,
(
total_spend
-
SUM(
CASE WHEN side='S'
THEN qnt * total_spend / (balance+qnt)
ELSE 0
END
)
OVER (
PARTITION BY name
ORDER BY seq
)
-- cumulative sum for amount 'recouped' by sales
)
/ NULLIF(balance, 0)
AS avg
FROM
accumulate
https://dbfiddle.uk/O0HEr556
Note: I still don't understand why you're calculating the avg price this way, but it matched your desired results/formulae, without recursion.

Convert answer to percentage with two decimals place SQL

Iā€™m trying to figure out how to convert the Male Percentage column to a percentage with decimals to the hundredths
select Top 20 pa.State,
Sum(case when p.gender='M' then 1 else 0 end) as [Male Count],
Sum(case when p.gender='F' then 1 else 0 end) as [ Female Count],
100*sum(case when gender='m' then 1 else 0 end )/count(*) as [Male Percentage]
From [dbo].[Patients] as p
Join PatientAddresses as pa
on p.mrn=pa.MRN
group by pa.State
The results I got.
State
Male Count
Female Count
Male Percentage
UT
105
120
46
NC
1152
1123
50
WI
700
669
51
MA
1486
1424
51
SQL Server by default will report integer division as integer numbers. If you want to force two decimal places, use ROUND(x, 2), where x is a float. One way to make x a float here is to multiply the percentage by the float 100.0 instead of the integer 100.
SELECT TOP 20
pa.State,
COUNT(CASE WHEN p.gender = 'M' THEN 1 END) AS [Male Count],
COUNT(CASE WHEN p.gender = 'F' THEN 1 END) AS [Female Count],
ROUND(100.0*COUNT(CASE WHEN gender = 'm' THEN 1 END) / COUNT(*), 2) AS [Male Percentage]
FROM [dbo].[Patients] AS p
INNER JOIN PatientAddresses AS pa
ON p.mrn = pa.MRN
GROUP BY
pa.State;
Side note: Using TOP without ORDER BY does not make much sense, because it is not clear which 20 records you want to see. So, adding an ORDER BY clause here is probably what you want, unless you are OK with getting back 20 random states.
Edit:
If you want to view the output in SSMS with only two decimal places, and not just with a precision of 2 decimal places, then use CONVERT:
CONVERT(DECIMAL(10,2), 100.0*COUNT(CASE WHEN gender = 'm' THEN 1 END) / COUNT(*))

Count ID based on start date

I have a source table that looks like this
I start counting ID of the Pd based on the first date then go to the 2nd date and check if it is Pd the add the ID, the go the 3rd date and check if Pd from the previous date are change or not if the change the count them to new group. Please see the desired output. Could you please help?
Thank you
In a single pass solution you will need to track each ids prior inv. When this tracking is in place you will
decrement an invs count based on ids prior inv
increment an invs count based on ids current inv
in the tracker replace the ids prior inv with the current inv
The number of ids is dynamic and not known apriori, and ids prior inv value lookup is keyed on id. The best DATA Step feature for dynamic lookup is HASH
Also, because the counts output is a pivot based on inv values, you will need to either
have a series of if/then or select/when statements to increment/decrement the invs counts
output data as date inv count and Proc TRANSPOSE
Data
data have;
format id 4. date yymmdd10. inv $2.;
input id date yymmdd10. event $ e_seq inv ; datalines;
100 2018-01-01 In 1 Pd
101 2018-01-01 In 1 Pd
102 2018-02-04 In 1 Pd
100 2018-02-07 N 2 NG
101 2018-02-14 P 2 G
101 2018-02-18 A 3 Pd
100 2018-03-15 A 3 Pd
102 2018-05-01 P 2 G
103 2018-06-03 In 1 Pd
run;
Sample code
Nested DOW loops are used to test for end of input data and ensure one row output for each date (the group)
data want(keep=date G NG Pd);
if 0 then set have; * prep pdv for hash;
* ids is the 'tracker';
declare hash ids();
ids.defineKey('id');
ids.defineData('id', 'lastinv');
ids.defineDone();
lastinv = inv; * prep lastinv in pdv;
do until (end);
do until (last.date);
set have end=end;
where inv in ('Pd' 'G' 'NG');
by date;
if ids.find() = 0 then do; * decrement count based on ids prior inv;
select (lastinv);
when ('G') G + -1;
when ('NG') NG + -1;
when ('Pd') Pd + -1;
otherwise ;
end;
end;
* update ids prior inv;
lastinv = inv;
ids.replace();
* increment count based on ids prior inv;
select (lastinv);
when ('G') G + 1;
when ('NG') NG + 1;
when ('Pd') Pd + 1;
otherwise ;
end;
end;
OUTPUT; * <------------ output one row of counts per date;
end;
run;

SQLite Query to group continuous range of numbers into different grouping sets

I want get the islands of this table below:
Group MemberNo
A 100
A 101
A 200
A 201
A 202
A 203
X 100
X 101
A 204
X 301
X 302
A 500
A 600
I want get this results using SQL (the islands):
Group FromMemberNo ToMemberNo
A 100 101
A 200 204
X 100 101
X 301 302
A 500 500
A 600 600
I have seen a lot of codes/forums for this but not working with SQLite because SQLite doesn't have CTEs.
100-101 is continuous so that it will be group into one.
Does anyone know how to do it in SQLite?
The fastest way to do this would be to go through the ordered records of this table in a loop and collect the islands manually.
In pure SQL (as a set-oriented language), this is not so easy.
First, we find out which records are the first in an island. The first record does not have a previous record, i.e., a record with the same group but with a MemberNo one smaller:
SELECT "Group",
MemberNo AS FromMemberNo
FROM ThisTable AS t1
WHERE NOT EXISTS (SELECT 1
FROM ThisTable AS t2
WHERE t2."Group" = t1."Group"
AND t2.MemberNo = t1.MemberNo - 1)
To find the last record of an island, we have to find the record with the largest MemberNo that still belongs to the same island, i.e., has the same group, and where all MemberNos in the island are continuous.
We detect continuous MemberNos by computing the difference between their values in the first and last records.
The last MemberNo of the island with group G and first MemberNo M can be computed like this:
SELECT MAX(MemberNo) AS LastMemberNo
FROM ThisTable AS t3
WHERE t3."Group" = G
AND t3.MemberNo - M + 1 = (SELECT COUNT(*)
FROM ThisTable AS t4
WHERE t4."Group" = G
AND t4.MemberNo BETWEEN M AND t3.MemberNo)
Finally, plug this into the first query:
SELECT "Group",
MemberNo AS FromMemberNo,
(SELECT MAX(MemberNo)
FROM ThisTable AS t3
WHERE t3."Group" = t1."Group"
AND t3.MemberNo - t1.MemberNo + 1 = (SELECT COUNT(*)
FROM ThisTable AS t4
WHERE t4."Group" = t1."Group"
AND t4.MemberNo BETWEEN t1.MemberNo AND t3.MemberNo)
) AS LastMemberNo
FROM ThisTable AS t1
WHERE NOT EXISTS (SELECT 1
FROM ThisTable AS t2
WHERE t2."Group" = t1."Group"
AND t2.MemberNo = t1.MemberNo - 1)

calculate sum for values in SQL for display per month name

i have a table with the following layout.
Email Blast Table
EmailBlastId | FrequencyId | UserId
---------------------------------
1 | 5 | 1
2 | 2 | 1
3 | 4 | 1
Frequency Table
Id | Frequency
------------
1 | Daily
2 | Weekly
3 | Monthly
4 | Quarterly
5 | Bi-weekly
I need to come up with a grid display on my asp.net page as follows.
Email blasts per month.
UserId | Jan | Feb | Mar | Apr |..... Dec | Cumulative
-----------------------------------------------------
1 7 6 6 7 6 #xx
The only way I can think of doing this is as below, for each month have a case statement.
select SUM(
CASE WHEN FrequencyId = 1 THEN 31
WHEN FrequencyId = 2 THEN 4
WHEN FrequencyId = 3 THEN 1
WHEN FrequencyId = 4 THEN 1
WHEN FrequencyId = 5 THEN 2 END) AS Jan,
SUM(
CASE WHEN FrequencyId = 1 THEN 28 (29 - leap year)
WHEN FrequencyId = 2 THEN 4
WHEN FrequencyId = 3 THEN 1
WHEN FrequencyId = 4 THEN 0
WHEN FrequencyId = 5 THEN 2 END) AS Feb, etc etc
FROM EmailBlast
Group BY UserId
Any other better way of achieving the same?
Is this for any given year? I'm going to assume you want the schedule for the current year. If you want a future year you can always change the DECLARE #now to specify any future date.
"Once in 2 weeks" (usually known as "bi-weekly") doesn't fit well into monthly buckets (except for February in a non-leap year). Should that possibly be changed to "Twice a month"?
Also, why not store the coefficient in the Frequency table, adding a column called "PerMonth"? Then you only have to deal with the Daily and Quarterly cases (and is it an arbitrary choice that this will happen only in January, April, and so on?).
Assuming that some of this is flexible, here is what I would suggest, assuming this very minor change to the table schema:
USE tempdb;
GO
CREATE TABLE dbo.Frequency
(
Id INT PRIMARY KEY,
Frequency VARCHAR(32),
PerMonth TINYINT
);
CREATE TABLE dbo.EmailBlast
(
Id INT,
FrequencyId INT,
UserId INT
);
And this sample data:
INSERT dbo.Frequency(Id, Frequency, PerMonth)
SELECT 1, 'Daily', NULL
UNION ALL SELECT 2, 'Weekly', 4
UNION ALL SELECT 3, 'Monthly', 1
UNION ALL SELECT 4, 'Quarterly', NULL
UNION ALL SELECT 5, 'Twice a month', 2;
INSERT dbo.EmailBlast(Id, FrequencyId, UserId)
SELECT 1, 5, 1
UNION ALL SELECT 2, 2, 1
UNION ALL SELECT 3, 4, 1;
We can accomplish this using a very complex query (but we don't have to hard-code those month numbers):
DECLARE #now DATE = CURRENT_TIMESTAMP;
DECLARE #Jan1 DATE = DATEADD(MONTH, 1-MONTH(#now), DATEADD(DAY, 1-DAY(#now), #now));
WITH n(m) AS
(
SELECT TOP 12 m = number
FROM master.dbo.spt_values
WHERE number > 0 GROUP BY number
),
months(MNum, MName, StartDate, NumDays) AS
( SELECT m, mn = CONVERT(CHAR(3), DATENAME(MONTH, DATEADD(MONTH, m-1, #Jan1))),
DATEADD(MONTH, m-1, #Jan1),
DATEDIFF(DAY, DATEADD(MONTH, m-1, #Jan1), DATEADD(MONTH, m, #Jan1))
FROM n
),
grp AS
(
SELECT UserId, MName, c = SUM (
CASE x.Id WHEN 1 THEN NumDays
WHEN 4 THEN CASE WHEN MNum % 3 = 1 THEN 1 ELSE 0 END
ELSE x.PerMonth END )
FROM months CROSS JOIN (SELECT e.UserId, f.*
FROM EmailBlast AS e
INNER JOIN Frequency AS f
ON e.FrequencyId = f.Id) AS x
GROUP BY UserId, MName
),
cumulative(UserId, total) AS
(
SELECT UserId, SUM(c)
FROM grp GROUP BY UserID
),
pivoted AS
(
SELECT * FROM (SELECT UserId, c, MName FROM grp) AS grp
PIVOT(MAX(c) FOR MName IN (
[Jan],[Feb],[Mar],[Apr],[May],[Jun],[Jul],[Aug],[Sep],[Oct],[Nov],[Dec])
) AS pvt
)
SELECT p.*, c.total
FROM pivoted AS p
LEFT OUTER JOIN cumulative AS c
ON p.UserId = c.UserId;
Results:
UserId Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec total
1 7 6 6 7 6 6 7 6 6 7 6 6 76
Clean up:
DROP TABLE dbo.EmailBlast, dbo.Frequency;
GO
In fact the schema change I suggested doesn't really buy you much, it just saves you two additional CASE branches inside the grp CTE. Peanuts, overall.
I think you're going to end up with a lot more complicated logic. Sure Jan has 31 days.. but Feb doesn't... and Feb changes depending on the year. Next, are email blasts sent even on weekends and holidays or are certain days skipped for various reasons... If that's the case then the number of business days for a given month changes each year.
Next the number of full weeks in a given month also changes year by year. What happens to those extra 4 half weeks? Do they go on the current or next month? What method are you using to determine that? For an example of how complicated this gets read: http://en.wikipedia.org/wiki/ISO_week_date Specifically the part where it talks about the first week, which actually has 9 different definitions.
I'm usually not one to say this, but you might be better off writing this with regular code instead of a sql query. Just issue a 'select * from emailblast where userid = xxx' and transform it using a variety of code methods.
Depends on what you're looking for. Suggestion 1 would be to track your actual email blasts (with a date :-).
Without actual dates, whatever you come-up with for one month will be the same for every month.
Anyway, If you're going to generalize, then I'd suggest using something other than ints -- like maybe floats or decimals. Since your output based on the tables listed in your post can only ever approximate what actually happens (e.g., January actually has 4-1/2 weeks, not 4), you'll have a compounding error-bounds over any range of months -- getting worse, the further out you extrapolate. If you output an entire 12 months, for example, your extrapolation will under-estimate by over 4 weeks.
If you use floats or decimals, then you'll be able to come much closer to what actually happens. For starters: find a common unit of measure (I'd suggest using a "day") E.g., 1 month = 365/12 days; 1 quarter = 365/4 days; 1 2week = 14 days; etc.
If you do that -- then your user who had one 1 per quarter actually had 1 per 91.25 days; 1 per week turns into 1 per 7 days; 1 per BiWeek turns into 1 per 14 days.
**EDIT** -- Incidentally, you could store the per-day value in your reference table, so you didn't have to calculate it each time. For example:
Frequency Table
Id | Frequency | Value
-------------------------------
1 | Daily | 1.0
2 | Weekly | .14286
3 | Monthly | .03288
4 | Quarterly | .01096
5 | Once in 2 weeks | .07143
Now do math -- (1/91.25 + 1/7 + 1/14) needs a common denom (like maybe 91.25 * 14), so it becomes (14/1277.5 + 182.5/1277.5 + 91.25/1277.5).
That adds-up to 287.75/1277.5, or .225 emails per day.
Since there are 365/12 days per month, multiple .225 * (365/12) to get 6.85 emails per month.
Your output would then look something like this:
Email blasts per month.
UserId | Jan | Feb | Mar | Apr |..... Dec | Cumulative
-----------------------------------------------------
1 6.85 6.85 6.85 6.85 6.85 #xx
The math may seem a little tedious, but once you step it out on your code, you'll never have to do it again. Your results will be more accurate (I rounded to 2 decimal places, but you could go further out if you wanted to). And if your company is using this data to determine budgets / potential income for the upcoming year, that might be worth it.
Also worth mentioning is that after YOU get done extrapolating (and the error bounds that entails), your consumers of this output will do THEIR OWN extrapolating, not on the raw data, but on your output. So it's kind of a double-whammy of error bounds. The more accurate you can be early-on, the more reliable these numbers will be at each subsequent levels.
You might want to consider adding a 3rd table called something like Schedule.
You could structure it like this:
MONTH_NAME
DAILY_COUNT
WEEKLY_COUNT
MONTHLY_COUNT
QUARTERLY_COUNT
BIWEEKLY_COUNT
The record for JAN would be
JAN
31
4
1
1
2
Or you could structure it like this:
MONTH_NAME
FREQUENCY_ID
EMAIL_COUNT
and have multiple records for each month:
JAN 1 31
JAN 2 4
JAN 3 1
JAN 4 1
JAN 5 2
I let you figure out if the logic to retrieve this is better than your CASE structure.

Resources