Get reciprocal allocation working

This commit is contained in:
Piv
2023-02-05 10:22:50 +10:30
parent f28dfe22d6
commit a175be4d17

View File

@@ -50,11 +50,13 @@ pub struct OverheadAllocationRule {
to_department_type: DepartmentType, to_department_type: DepartmentType,
} }
#[derive(Debug, PartialEq)]
pub struct TotalDepartmentCost { pub struct TotalDepartmentCost {
department: String, department: String,
value: f64, value: f64,
} }
#[derive(Debug, PartialEq)]
pub struct AccountCost { pub struct AccountCost {
account: String, account: String,
summed_department_costs: Vec<TotalDepartmentCost>, summed_department_costs: Vec<TotalDepartmentCost>,
@@ -77,25 +79,6 @@ 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()
}
pub fn reciprocal_allocation<Lines, Account, AllocationStatistic, Area, CostCentre, Output>( pub fn reciprocal_allocation<Lines, Account, AllocationStatistic, Area, CostCentre, Output>(
lines: csv::Reader<Lines>, lines: csv::Reader<Lines>,
accounts: csv::Reader<Account>, accounts: csv::Reader<Account>,
@@ -103,7 +86,9 @@ pub fn reciprocal_allocation<Lines, Account, AllocationStatistic, Area, CostCent
areas: csv::Reader<Area>, areas: csv::Reader<Area>,
cost_centres: csv::Reader<CostCentre>, cost_centres: csv::Reader<CostCentre>,
output: csv::Writer<Output>, output: csv::Writer<Output>,
) where use_numeric_accounts: bool,
) -> anyhow::Result<()>
where
Lines: Read, Lines: Read,
Account: Read, Account: Read,
AllocationStatistic: Read, AllocationStatistic: Read,
@@ -111,11 +96,28 @@ pub fn reciprocal_allocation<Lines, Account, AllocationStatistic, Area, CostCent
CostCentre: Read, CostCentre: Read,
Output: std::io::Write, Output: std::io::Write,
{ {
let mut accounts_reader = accounts;
let all_accounts_sorted: Result<Vec<CsvAccount>, csv::Error> =
accounts_reader.deserialize::<CsvAccount>().collect();
let mut accounts_sorted = all_accounts_sorted?;
// Sort the accounts, as allocation statistics use account ranges
if use_numeric_accounts {
accounts_sorted.sort_by(|a, b| {
a.code
.parse::<i32>()
.unwrap()
.cmp(&b.code.parse::<i32>().unwrap())
})
} else {
accounts_sorted.sort_by(|a, b| a.code.cmp(&b.code))
};
// Build out the the list of allocation rules from areas/allocation statistics (similar to ppm building 'cost drivers') // Build out the the list of allocation rules from areas/allocation statistics (similar to ppm building 'cost drivers')
// do reciprocal allocation (only for variable portion of accounts), for each account // do reciprocal allocation (only for variable portion of accounts), for each account
// Copy across fixed stuff (if necessary, not sure it is) // Copy across fixed stuff (if necessary, not sure it is)
Ok(())
} }
// Perform the reciprocal allocation (matrix) method to allocate servicing departments (indirect) costs // Perform the reciprocal allocation (matrix) method to allocate servicing departments (indirect) costs
@@ -126,10 +128,7 @@ fn reciprocal_allocation_impl(
account_costs: Vec<AccountCost>, account_costs: Vec<AccountCost>,
// TODO: Throw an appropriate error // TODO: Throw an appropriate error
) -> anyhow::Result<Vec<AccountCost>> { ) -> anyhow::Result<Vec<AccountCost>> {
let overhead_department_mappings: HashMap<String, usize> = let overhead_department_mappings = get_rules_indexes(&allocations, DepartmentType::Overhead);
get_rules_indexes(&allocations, DepartmentType::Overhead);
let operating_department_mappings: HashMap<String, usize> =
get_rules_indexes(&allocations, DepartmentType::Operating);
let mut slice_allocations = let mut slice_allocations =
vec![0.; overhead_department_mappings.len() * overhead_department_mappings.len()]; vec![0.; overhead_department_mappings.len() * overhead_department_mappings.len()];
@@ -138,25 +137,23 @@ fn reciprocal_allocation_impl(
.iter() .iter()
.filter(|allocation| allocation.to_department_type == DepartmentType::Overhead) .filter(|allocation| allocation.to_department_type == DepartmentType::Overhead)
{ {
// TODO: Check if we need to flp this around
let from_index = overhead_department_mappings let from_index = overhead_department_mappings
.get(&allocation.from_overhead_department) .get(&allocation.from_overhead_department)
.unwrap(); .unwrap();
let to_index = operating_department_mappings let to_index = overhead_department_mappings
.get(&allocation.to_department) .get(&allocation.to_department)
.unwrap(); .unwrap();
let elem = &mut slice_allocations // TODO: Check if dmatrix is row or column major order, would need to flip if column major
[(*from_index) + (overhead_department_mappings.len() * (*to_index))]; slice_allocations[from_index * overhead_department_mappings.len() + to_index] =
*elem = allocation.percent; allocation.percent * -1.;
} }
// TODO: Also need ones along the diagonal, and negatives in some places... let mut mat: DMatrix<f64> = DMatrix::from_vec(
let mat: DMatrix<f64> = DMatrix::from_row_slice(
overhead_department_mappings.len(), overhead_department_mappings.len(),
overhead_department_mappings.len(), overhead_department_mappings.len(),
&slice_allocations, slice_allocations,
); );
mat.fill_diagonal(1.);
if mat.determinant() == 0. { if mat.determinant() == 0. {
let pseudo_inverse = mat.svd(true, true).pseudo_inverse(0.000001); let pseudo_inverse = mat.svd(true, true).pseudo_inverse(0.000001);
@@ -176,31 +173,193 @@ fn reciprocal_allocation_impl(
} }
} }
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| {
if department.to_department_type == DepartmentType::Operating {
vec![department.to_department.clone()]
} else {
vec![
department.from_overhead_department.clone(),
department.to_department.clone(),
]
}
})
.unique()
.enumerate()
.map(|(index, department)| (department, index))
.collect()
}
fn do_solve_reciprocal<T: ReciprocalAllocationSolver>( fn do_solve_reciprocal<T: ReciprocalAllocationSolver>(
solver: T, solver: T,
account_costs: Vec<AccountCost>, account_costs: Vec<AccountCost>,
department_mappings: HashMap<String, usize>, overhead_department_mappings: HashMap<String, usize>,
allocations: Vec<OverheadAllocationRule>, allocations: Vec<OverheadAllocationRule>,
) -> anyhow::Result<Vec<AccountCost>> { ) -> anyhow::Result<Vec<AccountCost>> {
// TODO: Could batch the accounts, although probably won't see to big a speed increase, compiler should help us out let operating_department_mappings = get_rules_indexes(&allocations, DepartmentType::Operating);
for total_costs in account_costs { let mut operating_overhead_mappings =
let mut slice_costs = vec![0.; department_mappings.len()]; vec![0.; overhead_department_mappings.len() * operating_department_mappings.len()];
for rule in allocations {
for cost in total_costs.summed_department_costs { if rule.to_department_type == DepartmentType::Operating {
let elem = &mut slice_costs[*department_mappings.get(&cost.department).unwrap()]; let from_index = *overhead_department_mappings
*elem = cost.value; .get(&rule.from_overhead_department)
.unwrap();
let to_index = *operating_department_mappings
.get(&rule.to_department)
.unwrap();
operating_overhead_mappings
[from_index * overhead_department_mappings.len() + to_index] = rule.percent;
} }
}
let operating_overhead_mappings_mat: DMatrix<f64> = DMatrix::from_vec(
operating_department_mappings.len(),
overhead_department_mappings.len(),
operating_overhead_mappings,
);
let mut final_account_costs: Vec<AccountCost> = Vec::with_capacity(account_costs.len());
for total_costs in account_costs {
// TODO: There has to be a cleaner way to do this, perhaps by presorting things?
let mut overhead_slice_costs = vec![0.; overhead_department_mappings.len()];
for cost in total_costs.summed_department_costs.iter() {
if overhead_department_mappings.contains_key(&cost.department) {
overhead_slice_costs[*overhead_department_mappings.get(&cost.department).unwrap()] =
cost.value
}
}
let overhead_costs_vec: DMatrix<f64> =
DMatrix::from_row_slice(overhead_department_mappings.len(), 1, &overhead_slice_costs);
let calculated_overheads = solver.solve(&overhead_costs_vec);
let costs_vec: DMatrix<f64> = let mut operating_slice_costs = vec![0.; operating_department_mappings.len()];
DMatrix::from_row_slice(department_mappings.len(), 1, &slice_costs); for cost in total_costs.summed_department_costs {
if operating_department_mappings.contains_key(&cost.department) {
let elem = &mut operating_slice_costs
[*operating_department_mappings.get(&cost.department).unwrap()];
*elem = cost.value;
}
}
let operating_costs_vec: DMatrix<f64> = DMatrix::from_row_slice(
operating_department_mappings.len(),
1,
&operating_slice_costs,
);
let calculated_overheads = solver.solve(&costs_vec); // Borrow so we don't move between loops
let operating_overhead_mappings = &operating_overhead_mappings_mat;
let calculated_overheads = &calculated_overheads;
// Calculation: operating_overhead_usage . calculated_overheads + initial_totals // Calculation: operating_overhead_usage . calculated_overheads + initial_totals
// 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.
let calculated = operating_overhead_mappings * calculated_overheads + operating_costs_vec;
let converted_result: Vec<TotalDepartmentCost> = operating_department_mappings
.iter()
.map(|(department, index)| TotalDepartmentCost {
department: department.clone(),
value: *calculated.get(*index).unwrap(),
})
.collect();
final_account_costs.push(AccountCost {
account: total_costs.account,
summed_department_costs: converted_result,
});
}
Ok(final_account_costs)
}
#[cfg(test)]
mod tests {
use crate::AccountCost;
use crate::DepartmentType;
use crate::OverheadAllocationRule;
use crate::TotalDepartmentCost;
use super::reciprocal_allocation_impl;
#[test]
fn test_basic() {
let allocation_rules = vec![
OverheadAllocationRule {
from_overhead_department: "Y".to_owned(),
to_department: "Z".to_owned(),
percent: 0.2,
to_department_type: DepartmentType::Overhead,
},
OverheadAllocationRule {
from_overhead_department: "Z".to_owned(),
to_department: "Y".to_owned(),
percent: 0.3,
to_department_type: DepartmentType::Overhead,
},
OverheadAllocationRule {
from_overhead_department: "Y".to_owned(),
to_department: "A".to_owned(),
percent: 0.4,
to_department_type: DepartmentType::Operating,
},
OverheadAllocationRule {
from_overhead_department: "Y".to_owned(),
to_department: "B".to_owned(),
percent: 0.4,
to_department_type: DepartmentType::Operating,
},
OverheadAllocationRule {
from_overhead_department: "Z".to_owned(),
to_department: "A".to_owned(),
percent: 0.2,
to_department_type: DepartmentType::Operating,
},
OverheadAllocationRule {
from_overhead_department: "Z".to_owned(),
to_department: "B".to_owned(),
percent: 0.5,
to_department_type: DepartmentType::Operating,
},
];
let initial_totals = vec![AccountCost {
account: "Default".to_owned(),
summed_department_costs: vec![
TotalDepartmentCost {
department: "Y".to_owned(),
value: 7260.,
},
TotalDepartmentCost {
department: "Z".to_owned(),
value: 4000.,
},
TotalDepartmentCost {
department: "A".to_owned(),
value: 12000.,
},
TotalDepartmentCost {
department: "B".to_owned(),
value: 16000.,
},
],
}];
let expected_final_allocations = vec![AccountCost {
account: "Default".to_owned(),
summed_department_costs: vec![
TotalDepartmentCost {
department: "A".to_owned(),
value: 16760.,
},
TotalDepartmentCost {
department: "B".to_owned(),
value: 22500.,
},
],
}];
let result = reciprocal_allocation_impl(allocation_rules, initial_totals).unwrap();
assert_eq!(expected_final_allocations, result);
} }
// TODO: return something appropriate
Ok(vec![])
} }