I was trying out the backtracking algorithm with an easy example (sudoku). I first tried another approach where more possibilities are canceled, but after I got the same error I switched to an easier solution.
look for the first unsolved spot
fill in every number between 1 and 9 and backtrack the new field if it is valid
When I run it and output the non-valid fields I can see that when the algorithm goes out of a recursion call the spot that was in that recursion call is still a 9 (so the algorithm couldn't find anything for that spot)
e.g. the first two lines look something like this (it's trying to solve an empty field):
[1, 2, 3, 4, 6, 9, 9, 9, 9]
[9, 9, 9, 9, 9, 9, 9, 0, 0]
I thought it was a reference error and inserted
[e for e in field]
in the backtracking call so that the old field doesn't get altered although that didn't seem to help.
Here is my code:
for i in range(9):
a = [field[i][j] for j in range(9) if field[i][j] != 0]
if len(a) != len(set(a)):
return False
for i in range(9):
a = [field[j][i] for j in range(9) if field[j][i] != 0]
if len(a) != len(set(a)):
return False
for x in range(3):
for y in range(3):
a = []
for addX in range(3):
for addY in range(3):
spot = field[x * 3 + addX][y * 3 + addY]
if spot != 0:
a.append(spot)
if len(a) != len(set(a)):
return False
return True
def findEmpty(field):
for i in range(9):
for j in range(9):
if field[i][j] == 0:
return i, j
def backtracking(field):
find = findEmpty(field)
if not find:
return True, field
else:
x, y = find
for i in range(1, 10):
print(f"Trying {i} at {x} {y}")
field[x][y] = i
if isValid(field):
s = backtracking([e for e in field])
if s[0]:
return s
else:
print("Not valid")
for row in field:
print(row)
return False, None
field = [[0, 0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0]]
solution = backtracking(field)
if solution[0]:
print("There was a solution. The field is:")
for row in solution[1]:
print(row)
else:
print("No solution was found")
Okay, so based on what I can see in the logs, what happens is that when the code gets to 9 and still does not get an answer, it will backtrack, but keeps the value at 9.
So what happens is, every single time the program backtracks, it leaves the value at 9 and then go to the previous value, which might also go to 9, which is invalid as the value we backtracked from is already a 9. This causes a cycle where the program would backtrack straight to the start and make most slots 9, as you can see in your example.
So the solution would be to add a few lines to backtrack() as below. In short, that extra 2 lines checks if the invalid answer is a 9, if it is, it is resetted to a 0 and backtracks to the previous value until it gets a valid answer.
def backtracking(field):
find = findEmpty(field)
if not find:
return True, field
else:
x, y = find
for i in range(1, 10):
print(f"Trying {i} at {x} {y}")
field[x][y] = i
if isValid(field):
s = backtracking(field)
if s[0]:
return s
else:
print("Not valid")
if field[x][y] == 9:
field[x][y] = 0
for row in field:
print(row)
return False, None
Solution it gave:
[2, 3, 4, 5, 1, 6, 7, 8, 9]
[1, 5, 6, 7, 8, 9, 2, 3, 4]
[7, 8, 9, 2, 3, 4, 1, 5, 6]
[3, 1, 2, 4, 5, 7, 6, 9, 8]
[4, 6, 5, 1, 9, 8, 3, 2, 7]
[8, 9, 7, 3, 6, 2, 4, 1, 5]
[5, 2, 8, 6, 4, 1, 9, 7, 3]
[6, 7, 3, 9, 2, 5, 8, 4, 1]
[9, 4, 1, 8, 7, 3, 5, 6, 2]
I did some research and apparently it really is a reference error. For me importing pythons copy library and assigning each new field saying
f = copy.deepcopy(field)
fixes the issue (this also works for the complex example).
1- Create rows, cols, and boxes (3x3 units) array of dictionaries to store which indices of each row, col, and boxes have numbers.
2- Take a screenshot of the board. Run a for-loop and mark which points include numbers.
3- Call the recursive backtrack function.
4- Always in recursive functions define the base case to exit out of the recursion.
5- Start a for-loop to see which coordinate is ".". If you see ".", apply steps:[6,7,8,9]
6- Now we need to insert a valid number here. A valid number is a number that does not exist in the current row, col, and box.
7- If you find a valid number, insert it into the board and update rows, cols, and boxes.
8- After we inserted the valid point, we call backtrack function for the next ".".
9- When calling the backtrack, decide at which point you are. If you are in the last column, your next backtrack function will start from the next row and column 0. But if you are in the middle of row, you just increase the column parameter for next backtrack function.
10- If in step 5 your value is not ".", that means there is already a valid number here so call next backtracking depending on where position is. If you are in the last column, your next backtrack function will start from the next row and column 0. But if you are in the middle of row, you just increase the column parameter for next backtrack function.
class Solution:
def solveSudoku(self, board: List[List[str]]) -> None:
"""
Do not return anything, modify board in-place instead.
"""
n=len(board)
# create state variables,keep track of rows, cols and boxes
rows=[{} for _ in range(n)]
cols=[{} for _ in range(n)]
boxes=[{} for _ in range(n)]
# get the initial state of the grid
for r in range(n):
for c in range(n):
if board[r][c]!='.':
val=board[r][c]
box_id=self.get_box_id(r,c)
boxes[box_id][val]=True
rows[r][val]=True
cols[c][val]=True
# this backtracking just tells if shoul move to the next cell or not
self.backtrack(board,boxes,rows,cols,0,0)
def backtrack(self,board,boxes,rows,cols,r,c):
# base case. If I hit the last row or col, means all digits were correct so far
if r>=len(board) or c>=len(board[0]):
return True
# situation when cell is empty, fill it with correct value
if board[r][c]==".":
for num in range(1,10):
box_id=self.get_box_id(r,c)
box=boxes[box_id]
row=rows[r]
col=cols[c]
str_num=str(num)
# check rows, cols and boxes make sure str_num is not used before
if self.is_valid(box,col,row,str_num):
board[r][c]=str_num
boxes[box_id][str_num]=True
cols[c][str_num]=True
rows[r][str_num]=True
# if I am the last col and I placed the correct val, move to next row. So first col of the next row
if c==len(board)-1:
if self.backtrack(board,boxes,rows,cols,r+1,0):
return True
# if I am in col between 0-8, move to the col+1, in the same row
else:
if self.backtrack(board,boxes,rows,cols,r,c+1):
return True
# If I got a wrong value, then backtrack. So clear the state that you mutated
del box[str_num]
del row[str_num]
del col[str_num]
board[r][c]="."
# if cell is not empty just call the next backtracking
else:
if c==len(board)-1:
if self.backtrack(board,boxes,rows,cols,r+1,0):
return True
else:
if self.backtrack(board,boxes,rows,cols,r,c+1):
return True
return False
def is_valid(self,box,row,col,num):
if num in box or num in row or num in col:
return False
else:
return True
# a helper to get the id of the 3x3 sub grid, given row and column
def get_box_id(self,r,c):
row=(r//3)*3
col=c//3
return row+col
Related
I can't improve the performance of the following Sudoku Solver code. I know there are 3 loops here and they probably cause slow performance but I can't find a better/more efficient way. "board" is mutated with every iteration of recursion - if there are no zeros left, I just need to exit the recursion.
I tried to isolate "board" from mutation but it hasn't changed the performance. I also tried to use list comprehension for the top 2 "for" loops (i.e. only loop through rows and columns with zeros), tried to find coordinates of all zeros, and then use a single loop to go through them - hasn't helped.
I think I'm doing something fundamentally wrong here with recursion - any advice or recommendation on how to make the solution faster?
def box(board,row,column):
start_col = column - (column % 3)
finish_col = start_col + 3
start_row = row - (row % 3)
finish_row = start_row + 3
return [y for x in board[start_row:finish_row] for y in x[start_col:finish_col]]
def possible_values(board,row,column):
values = {1,2,3,4,5,6,7,8,9}
col_values = [v[column] for v in board]
row_values = board[row]
box_values = box(board, row, column)
return (values - set(row_values + col_values + box_values))
def solve(board, i_row = 0, i_col = 0):
for rn in range(i_row,len(board)):
if rn != i_row: i_col = 0
for cn in range(i_col,len(board)):
if board[rn][cn] == 0:
options = possible_values(board, rn, cn)
for board[rn][cn] in options:
if solve(board, rn, cn):
return board
board[rn][cn] = 0
#if no options left for the cell, go to previous cell and try next option
return False
#if no zeros left on the board, problem is solved
return True
problem = [
[9, 0, 0, 0, 8, 0, 0, 0, 1],
[0, 0, 0, 4, 0, 6, 0, 0, 0],
[0, 0, 5, 0, 7, 0, 3, 0, 0],
[0, 6, 0, 0, 0, 0, 0, 4, 0],
[4, 0, 1, 0, 6, 0, 5, 0, 8],
[0, 9, 0, 0, 0, 0, 0, 2, 0],
[0, 0, 7, 0, 3, 0, 2, 0, 0],
[0, 0, 0, 7, 0, 5, 0, 0, 0],
[1, 0, 0, 0, 4, 0, 0, 0, 7]
]
solve(problem)
Three things you can do to speed this up:
Maintain additional state using arrays of integers to keep track of row, col, and box candidates (or equivalently values already used) so that finding possible values is just possible_values = row_candidates[row] & col_candidates[col] & box_candidates[box]. This is a constant factors thing that will change very little in your approach.
As kosciej16 suggested use the min-remaining-values heuristic for selecting which cell to fill next. This will turn your algorithm into crypto-DPLL, giving you early conflict detection (cells with 0 candiates), constraint propagation (cells with 1 candidate), and a lower effective branching factor for the rest.
Add logic to detect hidden singles (like the Norvig solver does). This will make your solver a little slower for the simplest puzzles, but it will make a huge difference for puzzles where hidden singles are important (like 17 clue puzzles).
A result that worked at the end thanks to 53x15 and kosciej16. Not ideal or most optimal but passes the required performance test:
def solve(board, i_row = 0, i_col = 0):
cells_to_solve = [((rn, cn), possible_values(board,rn,cn)) for rn in range(len(board)) for cn in range(len(board)) if board[rn][cn] == 0]
if not cells_to_solve: return True
min_n_of_values = min([len(x[1]) for x in cells_to_solve])
if min_n_of_values == 0: return False
best_cells_to_try = [((rn,cn),options) for ((rn,cn),options) in cells_to_solve if len(options) == min_n_of_values]
for ((rn,cn),options) in best_cells_to_try:
for board[rn][cn] in options:
if solve(board, rn, cn):
return board
board[rn][cn] = 0
return False
I'm trying to returning an array that has been passed through a recursive function. But I'm only getting back None, instead of the bo Numpy array which I expected.
import numpy as np
board = [0, 0, 4, 5, 1, 9, 0, 0, 0, 6, 1, 0, 0, 7, 0, 0, 5, 9, 0, 0, 0, 6, 3, 0, 0, 0, 0, 5,
4, 0, 3, 0, 0, 0, 0, 2, 3, 0, 6, 0, 0, 0, 0, 0, 0, 1, 0, 0, 4, 0, 0, 0, 6, 7, 0, 0, 2, 0,
0, 0, 4, 0, 5, 8, 0, 0, 0, 4, 0, 0, 2, 0, 4, 0, 0, 1, 0, 0, 7, 0, 6]
grid = np.asarray(board).reshape(9, 9)
def possible(bo, y, x, n):
for i in range(0, 9):
if bo[y][i] == n:
return False
for i in range(0, 9):
if bo[i][x] == n:
return False
x0 = (x // 3) * 3
y0 = (y // 3) * 3
for i in range(0, 3):
for j in range(0, 3):
if bo[y0 + i][x0 + j] == n:
return False
return True
def solve(bo):
for y in range(9): # row
for x in range(9): # column
if bo[y][x] == 0:
for n in range(1, 10):
if possible(bo, y, x, n):
bo[y][x] = n
solve(bo)
bo[y][x] = 0
return
return bo
solved = solve(grid)
print(solved)
Why is this?
I can print it out to the console. But the return value is None.
Just to note, the first return statement exits out of the recursive function. Allowing to move onto subsequent items in the array.
(Previous question was marked as duplicate - but the linked question didn't answer my query)
Edit: To make it clear what i'm trying to do. I'm trying to return the modified bo array. The first return statement exits out of the recursive for n in range statement. And then runs through with another # in the range.
I believe your problem is in your return statement in your solve(bo) function.
for n in range(1, 10):
if possible(bo, y, x, n):
bo[y][x] = n
solve(bo)
bo[y][x] = 0
return
Whenever your function finishes this loop, it will always return None because it has a return, but isn't returning anything.
If I change the bare return line to:
return 'foobar'
the result is:
In [5]: grid = np.asarray(board).reshape(9, 9)
In [6]: solve(grid)
Out[6]: 'foobar'
A bare return is the same as return None (as is falling off the end of the function).
Even if the function is recursive, and modifies bo (and grid after running it has the right values), the function returns on that condition, not the final return bo line.
So
return bo
in both places returns the modified grid.
def solve(bo):
for y in range(9): # row
for x in range(9): # column
if bo[y][x] == 0:
for n in range(1, 10):
if possible(bo, y, x, n):
bo[y][x] = n
solve(bo) # doesn't use the return value
bo[y][x] = 0
return bo # final return
return # bo optional
Within the recursion you count on bo being modified, and never use the returned value. That's why with solve(grid), grid is changed. If you want solve to return the modfied grid you have to return it explicitly.
In code like this, you need to pay close attention to all return statements, and be aware of which is returning when.
I have a series of functions that end up giving a list, with the first item containing a number, derived from the dictionaries, and the second and third items are dictionaries.
These dictionaries have been previously randomly generated.
The function I am using generates a given number of these dictionaries, trying to get the highest number possible as the first item. (It's designed to optimise dice rolls).
This all works fine, and I can print the value of the highest first item from all iterations. However, when I try and print the two dictionaries associated with this first number (bearing in mind they're all in a list together), it just seemingly randomly generates the two other dictionaries.
def repeat(type, times):
best = 0
for i in range(0, times):
x = rollForCharacter(type)
if x[0] > best:
print("BEST:", x)
best = x[0]
print("The highest average success is", best)
return best
This works great. The last thing shown is:
BEST: (3.58, [{'strength': 4, 'intelligence': 1, 'charisma': 1, 'stamina': 4, 'willpower': 2, 'dexterity': 2, 'wits': 5, 'luck': 2}, {'agility': 1, 'brawl': 2, 'investigation': 3, 'larceny': 0, 'melee': 1, 'survival': 0, 'alchemy': 3, 'archery': 0, 'crafting': 0, 'drive': 1, 'magic': 0, 'medicine': 0, 'commercial': 0, 'esteem': 5, 'instruction': 2, 'intimidation': 2, 'persuasion': 0, 'seduction': 0}])
The highest average success is 3.58
But if I try something to store the list which gave this number:
def repeat(type, times):
best = 0
bestChar = []
for i in range(0, times):
x = rollForCharacter(type)
if x[0] > best:
print("BEST:", x)
best = x[0]
bestChar = x
print("The highest average success is", best)
print("Therefore the best character is", bestChar)
return best, bestChar
I get this as the last result, which is fine:
BEST: (4.15, [{'strength': 2, 'intelligence': 3, 'charisma': 4, 'stamina': 4, 'willpower': 1, 'dexterity': 2, 'wits': 4, 'luck': 1}, {'agility': 1, 'brawl': 0, 'investigation': 5, 'larceny': 0, 'melee': 0, 'survival': 0, 'alchemy': 7, 'archery': 0, 'crafting': 0, 'drive': 0, 'magic': 0, 'medicine': 0, 'commercial': 1, 'esteem': 0, 'instruction': 3, 'intimidation': 0, 'persuasion': 0, 'seduction': 0}])
The highest average success is 4.15
but the last line is
Therefore the best character is (4.15, [{'strength': 1, 'intelligence': 3, 'charisma': 4, 'stamina': 4, 'willpower': 1, 'dexterity': 2, 'wits': 2, 'luck': 3}, {'agility': 1, 'brawl': 0, 'investigation': 1, 'larceny': 4, 'melee': 2, 'survival': 0, 'alchemy': 2, 'archery': 4, 'crafting': 0, 'drive': 0, 'magic': 0, 'medicine': 0, 'commercial': 1, 'esteem': 0, 'instruction': 0, 'intimidation': 2, 'persuasion': 1, 'seduction': 0}])
As you can see this doesn't match with what I want, and what is printed literally right above it.
Through a little bit of checking, I realised what it gives out as the "Best Character" is just the last one generated, which is not the best, just the most recent. However, it isn't that simple, because the first element IS the highest result that was recorded, just not from the character in the rest of the list. This is really confusing because it means the list is somehow being edited but at no point can I see where that would happen.
Am I doing something stupid whereby the character is randomly generated every time? I wouldn't think so since x[0] gives the correct result and is stored fine, so what changes when it's the whole list?
From the function rollForCharacter() it returns rollResult, character which is just the number and then the two dictionaries.
I would greatly appreciate it if anyone could figure out and explain where I'm going wrong and why it can print the correct answer to the console yet not store it correctly a line below!
EDIT:
Dictionary 1 Code:
attributes = {}
def assignRow(row, p): # p is the number of points you have to assign to each row
rowValues = {}
for i in range(0, len(row)-1):
val = randint(0, p)
rowValues[row[i]] = val + 1
p -= val
rowValues[row[-1]] = p + 1
return attributes.update(rowValues)
def getPoints():
points = [7, 5, 3]
shuffle(points)
row1 = ['strength', 'intelligence', 'charisma']
row2 = ['stamina', 'willpower']
row3 = ['dexterity', 'wits', 'luck']
for i in range(0, len(points)):
row = eval("row" + str(i+1))
assignRow(row, points[i])
Dictionary 2 Code:
skills = {}
def assignRow(row, p): # p is the number of points you have to assign to each row
rowValues = {}
for i in range(0, len(row) - 1):
val = randint(0, p)
rowValues[row[i]] = val
p -= val
rowValues[row[-1]] = p
return skills.update(rowValues)
def getPoints():
points = [11, 7, 4]
shuffle(points)
row1 = ['agility', 'brawl', 'investigation', 'larceny', 'melee', 'survival']
row2 = ['alchemy', 'archery', 'crafting', 'drive', 'magic', 'medicine']
row3 = ['commercial', 'esteem', 'instruction', 'intimidation', 'persuasion', 'seduction']
for i in range(0, len(points)):
row = eval("row" + str(i + 1))
assignRow(row, points[i])
It does look like the dictionary is being re-generated, which could easily happen if the function rollForCharacter returns either a generator or alternatively is overwriting a global variable which is being overwritten by a subsequent cycle of the loop.
A simple-but-hacky way to solve the problem would be to take a deep copy of the dictionary at the time of storing, so that you're sure you're keeping the values at that point:
def repeat(type, times):
best = 0
bestChar = []
for i in range(0, times):
x = rollForCharacter(type)
if x[0] > best:
print("BEST:", x)
best = x[0]
# Create a brand new tuple, containing a copy of the current dict
bestChar = (x[0], x[1].copy())
The correct answer would be however to pass a unique dictionary variable that is not affected by later code.
See this SO answer with a bit more context about how passing a reference to a dictionary can be risky as it's still a mutable object.
I am trying to solve the sudoku problem and I need to add all the tuples that satisfy the constraint.
Here is the problem,
The variables contain non-repeated number from 0 to 9 in sorted order. The domain of each variable can be different.
Eg:
v1 = [1,2,3,4]
v2 = [3,4,6,7]
v3 = [3,5,7,8,9]
...
v9 = [1,2,3,4,5,6,7,8,9]
I want all sets of number where there is no repeated number,
I have come up with the following cumbersome and slow algorithm, but is there a better way to do this?
for d0 in v1:
for d1 in v2:
...
for d9 in v9:
if d0 != d1 and d0 != d2 and d0 != d3 ... d0 != d9
...
and d7 != d8 and d7 != d9:
and d8 != d9:
print(d0,d1,d2,d3,d4,d5,d6,d7,d8,d9)
Is there a more effective way of doing it without using 9 for loops and a long list of and statements??
I can't use the constraint solver like python-constraint since I need to implement it.
I wrote a sudoku solver a while back for fun...
I've posted the code for your reference. It's actually really efficient. I also explain it at the end.
#imports
import sys
import time
#test sudoku puzzle
a = [[0, 6, 0, 1, 3, 0, 0, 0, 8],
[0, 0, 2, 0, 0, 6, 0, 0, 0],
[1, 8, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 7, 0, 4, 2, 0, 0, 0],
[5, 0, 0, 0, 0, 0, 0, 0, 9],
[0, 0, 0, 5, 9, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 9, 2],
[0, 0, 0, 7, 0, 0, 3, 0, 0],
[7, 0, 0, 0, 1, 3, 0, 4, 0]]
'''
a = [[0, 0, 4, 0],
[0, 2, 0, 3],
[2, 0, 0, 0],
[0, 4, 0, 1]]
'''
#store all of the cells that have been solved already
solved = {}
for p in range(len(a)):
for q in range(len(a)):
if a[p][q] != 0:
solved[p, q] = 1
#denotes what direction the code is moving (if it is moving backwards, then we want the already solved cells to return, and if it is moving forwards, we want the solved cells to call next)
goingForward = True
#if the sudoku is solved or not
done = False
# number of iterations
counter = 0
#prints a sudoku neatly
def printSudoku(a):
for i in a:
array = ""
for j in i:
array += str(j) + " "
print array
def next(i, j, a):
global counter
global goingForward
global done
if done:
return
counter += 1
# gets the vertical in which a[i][j] is located
vertical = [z[j] for z in a]
# gets the "box" in which a[i][j] is located
box = []
x = (i/3 + 1)*3
y = (j/3 + 1)*3
for p in range(x-3, x):
for q in range(y-3, y):
box.append(a[p][q])
#if it already solved and it is going forward, call next
#else, return
if solved.has_key((i, j)):
if i == 8 and j == 8:
done = True
printSudoku(a)
return
if goingForward:
if j==8:
next(i+1, 0, a)
else:
next(i, j+1, a)
else:
return
#try every number for cell a[i][j]
#if none work, return so that the previous number may be changed
else:
for k in range(1, 10):
if k not in a[i] and k not in vertical and k not in box:
a[i][j] = k
if i == 8 and j == 8:
done = True
printSudoku(a)
return
if j==8:
goingForward = True
next(i+1, 0, a)
a[i][j] = 0
else:
goingForward = True
next(i, j+1, a)
a[i][j] = 0
goingForward = False
return
start_time = time.time()
#where we actually call the function
next(0, 0, a)
if done == False:
print "no solution!!"
print "It took " + str(time.time()-start_time) + " to solve this sudoku"
print "With " + str(counter) + " iterations"
This uses a technique called recursive backtracking. It starts at the very first cell and tries all possible values. If it finds a possible solution, then it keeps that value and goes on the next cell. For that cell, it employs the same method, trying all values from 1-9 until one works, then moving to the next cell. When it encounters a cell for which no values work, it gives up and goes to the previous cell, where it is determined that that particular value does not work and a new one must be used. The previous cell skips the errant value and goes to the next integer. Through a combination of backtracking and guessing, it eventually arrives at the correct answer.
It's also super speedy since a lot of the cases are skipped due to the error backtracking. For most "guessed" values, it does not get past the third or fourth cell.
I found the solution myself,
So freaking simple, just subtract the current value from the domain of the next list to prevent it from getting selected.
for d0 in v1:
for d1 in list(set(v2) - set(d0)):
...
for d9 in list(set(v9) - set(d1,d2,d3,d4,d5,d6,d7,d8,d9):
print(d0,d1,d2,d3,d4,d5,d6,d7,d8,d9)
I'm creating a class based directional indicator that given a number of days (n_days) and a list of numbers, it gives the (number of numbers out of the most recent n_days on which the number was higher than the previous number minus the n_days out of the previous n_days on which the number went down). So if the number in the list increases I want it to return +1, if it decreases I want it to return -1, otherwise it should be 0 so the first number should always be 0 since you can't compare it to anything. Then based on n_days I basically want to take the sum of the of the recent n_days, so for example in a list of [1,2,2,1,2,1] the change should be [0,+1,0,-1,1,-1] and if I want the sum of the change in the 2 recent numbers for each day so it should be [0,+1,-1,0,+1,0] because on the first day there is only 0, on the second day you take the sum of the most recent two days 0+(+1)=1, the third day (+1)+0=+1, the fourth day 0+(-1)=-1 and so forth. Here is my code that's not working:
class Directionalindicator():
def __init__(self, n_days, list_of_prices):
self.n_days = n_days
self.list_of_prices = list_of_prices
def calculate(self):
change = []
for i in range(len(self.list_of_prices)):
if self.list_of_prices[i-1] < self.list_of_prices[i]:
change.append(1)
elif self.list_of_prices[i-1] > self.list_of_prices[i]:
change.append(-1)
else:
change.append(0)
directional = []
for i in range(len(change)):
directional.append(sum(change[i+1-self.n_days:i+1]))
return directional
testing it with:
y = Directionalindicator(2,[1,2,2,1,2,1])
y.calculate()
should return:
[0,+1,+1,-1,0,0]
and it does.
But testing it with:
y = Directionalindicator(3, [1,2,3,4,5,6,7,8,9,10])
y.calculate()
should return
[0, 0, 2, 3, 3, 3, 3, 3, 3, 3]
but it returns
[0, 0, 1, 3, 3, 3, 3, 3, 3, 3]
I printed change to see what it was doing and the first value is a -1 instead of a 0. Also, the code in one of the answers works using zip, but I don't understand why mine doesn't work for that list from 1-10.
Your comparison
i > i-1
will always be True. You are comparing each price to itself minus one, which will always be smaller. Instead, you should be comparing pairs of prices. zip is useful for this:
change = [0] # first price always zero change
for a, b in zip(self.list_of_prices, self.list_of_prices[1:]):
if a < b: # price went up
change.append(1)
elif a > b: # price went down
change.append(-1)
else: # price stayed the same
change.append(0)
When you plug this into your code and use your example
Directionalindicator(2, [1, 2, 2, 1, 2, 1])
you get:
change == [0, 1, 0, -1, 1, -1]
directional == [0, 1, 1, -1, 0, 0]
This seems to be correct according to your initial statement of the rules, but for some reason doesn't match your "expected output" [0, 1, -1, 0, 1, 0] from the end of your question.
The reason your edit doesn't work is that you are using an index i on the list. When i == 0, i-1 == -1. When used as an index list_of_prices[-1], this gives you the last element in the list. Therefore change contains [-1, 1, 1, 1, 1, 1, 1, 1, 1, 1], as it compares 1 with 10, not [0, 1, 1, 1, 1, 1, 1, 1, 1, 1] as you expected.