More progress towards reciprocal allocation algorithm

This commit is contained in:
piv
2022-06-20 12:22:07 +09:30
parent 08433d6ea6
commit 28e3c87b98
3 changed files with 75 additions and 28 deletions

16
Cargo.lock generated
View File

@@ -97,6 +97,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"clap", "clap",
"csv", "csv",
"itertools",
"nalgebra", "nalgebra",
] ]
@@ -122,6 +123,12 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "either"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.12.1" version = "0.12.1"
@@ -153,6 +160,15 @@ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]]
name = "itertools"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "0.4.8" version = "0.4.8"

View File

@@ -14,4 +14,6 @@ csv = "1.1"
# simba = { version = "0.7.1", features = ["partial_fixed_point_support"] } # simba = { version = "0.7.1", features = ["partial_fixed_point_support"] }
# num = "0.4" # num = "0.4"
clap = { version = "3.1.18", features = ["derive"] } clap = { version = "3.1.18", features = ["derive"] }
itertools = "0.10.3"

View File

@@ -1,8 +1,8 @@
extern crate nalgebra as na; extern crate nalgebra as na;
use core::slice; use itertools::Itertools;
use na::{DMatrix, Dynamic, LU}; use na::{DMatrix, Dynamic, LU};
use std::{collections::HashMap, ops::Mul, error::Error}; use std::{collections::HashMap, error::Error, ops::Mul};
// TODO: Look into serde for serialisation, can also use it to serialise/deserialise // TODO: Look into serde for serialisation, can also use it to serialise/deserialise
// records from a csv file using the csv crate // records from a csv file using the csv crate
@@ -117,15 +117,22 @@ pub fn move_money_2(
running_total running_total
} }
#[derive(Debug, PartialEq, Eq)]
pub enum DepartmentType {
Operating,
Overhead,
}
// TODO: Could also look at BigDecimal rather than f64 for higher precision (even i64 might be fine if we don't need to divide...) // TODO: Could also look at BigDecimal rather than f64 for higher precision (even i64 might be fine if we don't need to divide...)
// Note: remember these are overhead departments only when calculating the lu decomposition or pseudoinverse, and for each department, // Note: remember these are overhead departments only when calculating the lu decomposition or pseudoinverse, and for each department,
// you either need -1 or rest negative for a row to subtract the initial amounts so we end up effectively 0 (simultaneous equations end // you either need -1 or rest negative for a row to subtract the initial amounts so we end up effectively 0 (simultaneous equations end
// up with negative there so yes this is expected) // up with negative there so yes this is expected)
// Also, we could potentially use this same struct for non-overhead departments when mapping from overhead to // Also, we could potentially use this same struct for non-overhead departments when mapping from overhead to
pub struct OverheadAllocationRule { pub struct OverheadAllocationRule {
from_department: String, from_overhead_department: String,
to_department: String, to_department: String,
percent: f64, percent: f64,
to_department_type: DepartmentType,
} }
pub struct TotalDepartmentCost { pub struct TotalDepartmentCost {
@@ -155,6 +162,24 @@ impl ReciprocalAllocationSolver for DMatrix<f64> {
} }
} }
fn get_rules_indexes(
allocations: &Vec<OverheadAllocationRule>,
department_type: DepartmentType,
) -> HashMap<String, usize> {
allocations
.iter()
.filter(|allocation| allocation.to_department_type == department_type)
.flat_map(|department| {
[
department.from_overhead_department.clone(),
department.to_department.clone(),
]
})
.unique()
.enumerate()
.map(|(index, department)| (department, index))
.collect()
}
// Perform the reciprocal allocation (matrix) method to allocate servicing departments (indirect) costs // Perform the reciprocal allocation (matrix) method to allocate servicing departments (indirect) costs
// to functional departments. Basically just a matrix solve, uses regression (moore-penrose pseudoinverse) when // to functional departments. Basically just a matrix solve, uses regression (moore-penrose pseudoinverse) when
@@ -164,23 +189,35 @@ pub fn reciprocal_allocation(
account_costs: Vec<AccountCost>, account_costs: Vec<AccountCost>,
// TODO: Throw an appropriate error // TODO: Throw an appropriate error
) -> Result<Vec<AccountCost>, Box<dyn Error>> { ) -> Result<Vec<AccountCost>, Box<dyn Error>> {
// TODO: Need to split up the rules so that we only pass overhead departments into the getreciprocal matrix method, let overhead_department_mappings: HashMap<String, usize> =
// and get_rules_indexes(&allocations, DepartmentType::Overhead);
let mut department_mappings: HashMap<String, usize> = HashMap::new(); let operating_department_mappings: HashMap<String, usize> =
for allocation in allocations.iter() { get_rules_indexes(&allocations, DepartmentType::Operating);
let map_size = department_mappings.len();
department_mappings let mut slice_allocations =
.entry(allocation.from_department.clone()) vec![0.; overhead_department_mappings.len() * overhead_department_mappings.len()];
.or_insert(map_size);
let map_size = department_mappings.len(); for allocation in allocations
department_mappings .iter()
.entry(allocation.to_department.clone()) .filter(|allocation| allocation.to_department_type == DepartmentType::Overhead)
.or_insert(map_size); {
// TODO: Check if we need to flp this around
let from_index = overhead_department_mappings
.get(&allocation.from_overhead_department)
.unwrap();
let to_index = operating_department_mappings
.get(&allocation.to_department)
.unwrap();
let elem = &mut slice_allocations
[(*from_index) + (overhead_department_mappings.len() * (*to_index))];
*elem = allocation.percent;
} }
// TODO: Also need ones along the diagonal, and negatives in some places...
let mat: DMatrix<f64> = DMatrix::from_row_slice( let mat: DMatrix<f64> = DMatrix::from_row_slice(
department_mappings.len(), overhead_department_mappings.len(),
department_mappings.len(), overhead_department_mappings.len(),
&slice_allocations, &slice_allocations,
); );
@@ -189,14 +226,14 @@ pub fn reciprocal_allocation(
do_solve_reciprocal( do_solve_reciprocal(
pseudo_inverse.unwrap(), pseudo_inverse.unwrap(),
account_costs, account_costs,
department_mappings, overhead_department_mappings,
allocations, allocations,
) )
} else { } else {
do_solve_reciprocal( do_solve_reciprocal(
mat.lu(), mat.lu(),
account_costs, account_costs,
department_mappings, overhead_department_mappings,
allocations, allocations,
) )
} }
@@ -210,14 +247,7 @@ fn do_solve_reciprocal<T: ReciprocalAllocationSolver>(
) -> Result<Vec<AccountCost>, Box<dyn Error>> { ) -> Result<Vec<AccountCost>, Box<dyn Error>> {
// TODO: Could batch the accounts, although probably won't see to big a speed increase, compiler should help us out // TODO: Could batch the accounts, although probably won't see to big a speed increase, compiler should help us out
for total_costs in account_costs { for total_costs in account_costs {
let mut slice_allocations = vec![0.; department_mappings.len()];
let mut slice_costs = vec![0.; department_mappings.len()]; let mut slice_costs = vec![0.; department_mappings.len()];
for allocation in allocations {
let elem = &mut slice_allocations[*department_mappings
.get(&allocation.from_department)
.unwrap()];
*elem = allocation.percent;
}
for cost in total_costs.summed_department_costs { for cost in total_costs.summed_department_costs {
let elem = &mut slice_costs[*department_mappings.get(&cost.department).unwrap()]; let elem = &mut slice_costs[*department_mappings.get(&cost.department).unwrap()];
@@ -233,7 +263,6 @@ fn do_solve_reciprocal<T: ReciprocalAllocationSolver>(
// Where operating_overhead_usage is the direct mapping from overhead -> operating department, calculated overheads is the // Where operating_overhead_usage is the direct mapping from overhead -> operating department, calculated overheads is the
// solved overheads usages after taking into account usage between departments, and initial_totals is the initial values // solved overheads usages after taking into account usage between departments, and initial_totals is the initial values
// for the operating departments. // for the operating departments.
} }
// TODO: return something appropriate // TODO: return something appropriate
Ok(vec![]) Ok(vec![])