Skip to content

Commit 47fabe6

Browse files
authored
Merge pull request cfpb#1156 from nickgrippin/n52
N52
2 parents 0e7585b + 3f50fda commit 47fabe6

File tree

14 files changed

+339
-14
lines changed

14 files changed

+339
-14
lines changed

census/src/main/resources/jul_2015_cbsa.csv

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

census/src/main/scala/hmda/census/model/CbsaLookup.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import com.github.tototoshi.csv.CSVParser.parse
1010
object CbsaLookup extends CbsaResourceUtils {
1111
val values: Seq[Cbsa] = {
1212
val lines = resourceLines("/jul_2015_cbsa.csv", "ISO-8859-1")
13-
lines.map { line =>
13+
lines.drop(3).map { line =>
1414
val values = parse(line, '\\', ',', '"').getOrElse(List())
1515
val cbsaCode = values(0)
1616
val metroDivCode = values(1)

model/shared/src/main/scala/hmda/model/publication/reports/BorrowerCharacteristic.scala

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,26 @@ package hmda.model.publication.reports
22

33
sealed trait BorrowerCharacteristic
44

5-
case class RaceBorrowerCharacteristic(races: List[RaceCharacteristic]) extends BorrowerCharacteristic
5+
case class RaceBorrowerCharacteristic(races: List[RaceCharacteristic]) extends BorrowerCharacteristic {
6+
def +(rbc: RaceBorrowerCharacteristic) = {
7+
val combined = races.map(r =>
8+
r + rbc.races.find(_.race == r.race).get)
9+
RaceBorrowerCharacteristic(combined)
10+
}
11+
}
612

7-
case class EthnicityBorrowerCharacteristic(ethnicities: List[EthnicityCharacteristic]) extends BorrowerCharacteristic
13+
case class EthnicityBorrowerCharacteristic(ethnicities: List[EthnicityCharacteristic]) extends BorrowerCharacteristic {
14+
def +(ebc: EthnicityBorrowerCharacteristic) = {
15+
val combined = ethnicities.map(e =>
16+
e + ebc.ethnicities.find(_.ethnicity == e.ethnicity).get)
17+
EthnicityBorrowerCharacteristic(combined)
18+
}
19+
}
820

9-
case class MinorityStatusBorrowerCharacteristic(minoritystatus: List[MinorityStatusCharacteristic]) extends BorrowerCharacteristic
21+
case class MinorityStatusBorrowerCharacteristic(minoritystatus: List[MinorityStatusCharacteristic]) extends BorrowerCharacteristic {
22+
def +(msbc: MinorityStatusBorrowerCharacteristic) = {
23+
val combined = minoritystatus.map(m =>
24+
m + msbc.minoritystatus.find(_.minorityStatus == m.minorityStatus).get)
25+
MinorityStatusBorrowerCharacteristic(combined)
26+
}
27+
}
Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,29 @@
11
package hmda.model.publication.reports
22

3-
sealed trait Characteristic
3+
sealed trait Characteristic {
4+
def dispositions: List[Disposition]
5+
}
46

5-
case class RaceCharacteristic(race: RaceEnum, dispositions: List[Disposition]) extends Characteristic
7+
case class RaceCharacteristic(race: RaceEnum, dispositions: List[Disposition]) extends Characteristic {
8+
def +(rc: RaceCharacteristic) = {
9+
val combined = dispositions.map(d =>
10+
d + rc.dispositions.find(_.disposition == d.disposition).get)
11+
RaceCharacteristic(race, combined)
12+
}
13+
}
614

7-
case class EthnicityCharacteristic(ethnicity: EthnicityEnum, dispositions: List[Disposition]) extends Characteristic
15+
case class EthnicityCharacteristic(ethnicity: EthnicityEnum, dispositions: List[Disposition]) extends Characteristic {
16+
def +(ec: EthnicityCharacteristic) = {
17+
val combined = dispositions.map(d =>
18+
d + ec.dispositions.find(_.disposition == d.disposition).get)
19+
EthnicityCharacteristic(ethnicity, combined)
20+
}
21+
}
822

9-
case class MinorityStatusCharacteristic(minorityStatus: MinorityStatusEnum, dispositions: List[Disposition]) extends Characteristic
23+
case class MinorityStatusCharacteristic(minorityStatus: MinorityStatusEnum, dispositions: List[Disposition]) extends Characteristic {
24+
def +(msc: MinorityStatusCharacteristic) = {
25+
val combined = dispositions.map(d =>
26+
d + msc.dispositions.find(_.disposition == d.disposition).get)
27+
MinorityStatusCharacteristic(minorityStatus, combined)
28+
}
29+
}
Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package hmda.model.publication.reports
22

33
case class Disposition(
4-
disposition: ActionTakenTypeEnum,
5-
count: Int,
6-
value: Int
7-
)
4+
disposition: ActionTakenTypeEnum,
5+
count: Int,
6+
value: Int
7+
) {
8+
def +(disp: Disposition): Disposition = {
9+
Disposition(disposition, count + disp.count, value + disp.value)
10+
}
11+
}

publication/src/main/resources/reports-metadata.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ ReportId,Type,ReportTable,Dispositions,Description
22
A52,Aggregate,5-2,Received;Originated;ApprovedButNotAccepted;Denied;Withdrawn;Closed,"Disposition of Applications for Conventional Home-Purchase Loans, 1-to-4 Family and Manufactured Home Dwellings, by Income, Race, and Ethnicity of Applicant"
33
D51,Disclosure,5-1,Received;Originated;ApprovedButNotAccepted;Denied;Withdrawn;Closed,"Disposition of applications for FHA, FSA/RHS, and VA home-purchase loans, 1- to 4-family and manufactured home dwellings, by income, race and ethnicity of applicant"
44
D53,Disclosure,5-3,Received;Originated;ApprovedButNotAccepted;Denied;Withdrawn;Closed,"Disposition of Applications to Refinance Loans on 1-to-4 Family and Manufactured Home Dwellings, by Income, Race, and Ethnicity of Applicant"
5+
N52,National Aggregate,5-2,Received;Originated;ApprovedButNotAccepted;Denied;Withdrawn;Closed,"Disposition of Applications for Conventional Home-Purchase Loans, 1-to-4 Family and Manufactured Home Dwellings, by Income, Race, and Ethnicity of Applicant"

publication/src/main/scala/hmda/publication/reports/aggregate/A52.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ object A52 {
5656
year <- yearF
5757
total <- totalF
5858
} yield {
59-
6059
A52(
6160
year,
6261
msa,
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package hmda.publication.reports.national
2+
3+
import akka.NotUsed
4+
import akka.stream.scaladsl.Source
5+
import hmda.census.model.MsaIncomeLookup
6+
import hmda.model.publication.reports._
7+
import hmda.publication.reports._
8+
import hmda.publication.reports.aggregate.A52
9+
import hmda.publication.reports.util.ReportUtil._
10+
import hmda.publication.reports.util.ReportsMetaDataLookup
11+
import hmda.query.model.filing.LoanApplicationRegisterQuery
12+
13+
import scala.concurrent.Future
14+
15+
case class N52(
16+
year: Int = 0,
17+
reportDate: String = formattedCurrentDate,
18+
applicantIncomes: List[ApplicantIncome] = List(),
19+
total: List[Disposition] = List(),
20+
table: String = N52.metaData.reportTable,
21+
description: String = N52.metaData.description
22+
) extends NationalAggregateReport {
23+
def +(a52: A52): N52 = {
24+
if (applicantIncomes.isEmpty) {
25+
N52(a52.year, a52.reportDate, a52.applicantIncomes, a52.total)
26+
} else {
27+
val combinedIncomes = a52.applicantIncomes.map(income => {
28+
val newC = applicantIncomes.find(i => i.applicantIncome == income.applicantIncome).get.borrowerCharacteristics
29+
val originalC = income.borrowerCharacteristics
30+
31+
val nR = newC.find(_.isInstanceOf[RaceBorrowerCharacteristic]).get.asInstanceOf[RaceBorrowerCharacteristic]
32+
val oR = originalC.find(_.isInstanceOf[RaceBorrowerCharacteristic]).get.asInstanceOf[RaceBorrowerCharacteristic]
33+
val cR = oR + nR
34+
35+
val nE = newC.find(_.isInstanceOf[EthnicityBorrowerCharacteristic]).get.asInstanceOf[EthnicityBorrowerCharacteristic]
36+
val oE = originalC.find(_.isInstanceOf[EthnicityBorrowerCharacteristic]).get.asInstanceOf[EthnicityBorrowerCharacteristic]
37+
val cE = oE + nE
38+
39+
val nM = newC.find(_.isInstanceOf[MinorityStatusBorrowerCharacteristic]).get.asInstanceOf[MinorityStatusBorrowerCharacteristic]
40+
val oM = originalC.find(_.isInstanceOf[MinorityStatusBorrowerCharacteristic]).get.asInstanceOf[MinorityStatusBorrowerCharacteristic]
41+
val cM = oM + nM
42+
43+
ApplicantIncome(income.applicantIncome, List(cR, cE, cM))
44+
})
45+
46+
val combinedDispositions = a52.total.map(disposition => {
47+
val originalDisposition = total.find(d => d.disposition == disposition.disposition).get
48+
disposition + originalDisposition
49+
})
50+
51+
N52(year, reportDate, combinedIncomes, combinedDispositions)
52+
}
53+
}
54+
}
55+
56+
object N52 {
57+
val metaData = ReportsMetaDataLookup.values("N52")
58+
val dispositions = metaData.dispositions
59+
60+
// Table filters:
61+
// Loan Type 1
62+
// Property Type 1,2
63+
// Purpose of Loan 1
64+
def generate[ec: EC, mat: MAT, as: AS](larSource: Source[LoanApplicationRegisterQuery, NotUsed]): Future[N52] = {
65+
val fipsList = MsaIncomeLookup.values.map(_.fips).filterNot(_ == 99999)
66+
67+
val a52List = fipsList.map(fipsCode => A52.generate(larSource, fipsCode))
68+
val n52f = Future.sequence(a52List).map(seq => {
69+
seq.foldLeft(N52())((n52, a52) => n52 + a52)
70+
})
71+
72+
for {
73+
n <- n52f
74+
y <- calculateYear(larSource)
75+
} yield N52(y, n.reportDate, n.applicantIncomes, n.total)
76+
}
77+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package hmda.publication.reports.national
2+
3+
import hmda.model.publication.reports.ReportTypeEnum.NationalAggregate
4+
import hmda.model.publication.reports.ReportTypeEnum
5+
6+
trait NationalAggregateReport {
7+
8+
val description: String
9+
val table: String
10+
val year: Int
11+
12+
val reportType: ReportTypeEnum = NationalAggregate
13+
14+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package hmda.publication.reports.national
2+
3+
import akka.actor.ActorSystem
4+
import akka.stream.ActorMaterializer
5+
import akka.util.Timeout
6+
import com.typesafe.config.ConfigFactory
7+
import hmda.query.repository.filing.FilingCassandraRepository
8+
import hmda.publication.reports.protocol.national.N52Protocol._
9+
10+
import scala.concurrent.Future
11+
import scala.concurrent.duration._
12+
import spray.json._
13+
14+
class NationalAggregateReports(val sys: ActorSystem, val mat: ActorMaterializer) extends FilingCassandraRepository {
15+
override implicit def system: ActorSystem = sys
16+
override implicit def materializer: ActorMaterializer = mat
17+
val config = ConfigFactory.load()
18+
val duration = config.getInt("hmda.actor-lookup-timeout")
19+
implicit val timeout = Timeout(duration.seconds)
20+
21+
val larSource = readData(1000)
22+
23+
def generateReports(fipsCode: Int, respId: String): Future[Unit] = {
24+
25+
val n52F = N52.generate(larSource)
26+
n52F.map { n52 =>
27+
println(n52.toJson.prettyPrint)
28+
}
29+
}
30+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package hmda.publication.reports.protocol.national
2+
3+
import hmda.model.publication.reports.ReportTypeEnum._
4+
import hmda.model.publication.reports.{ ApplicantIncome, Disposition }
5+
import hmda.publication.reports.national.N52
6+
import hmda.publication.reports.protocol.{ ApplicantIncomeProtocol, MSAReportProtocol, ReportTypeEnumProtocol }
7+
import spray.json._
8+
9+
object N52Protocol
10+
extends DefaultJsonProtocol
11+
with ReportTypeEnumProtocol
12+
with MSAReportProtocol
13+
with ApplicantIncomeProtocol {
14+
15+
implicit object N52Format extends RootJsonFormat[N52] {
16+
override def write(obj: N52): JsValue = {
17+
JsObject(
18+
"table" -> JsString("5-2"),
19+
"type" -> JsString(NationalAggregate.toString),
20+
"desc" -> JsString(obj.description),
21+
"year" -> JsNumber(obj.year),
22+
"reportDate" -> JsString(obj.reportDate),
23+
"applicantIncomes" -> obj.applicantIncomes.toJson,
24+
"total" -> obj.total.toJson
25+
)
26+
}
27+
28+
override def read(json: JsValue): N52 = json.asJsObject.getFields(
29+
"table",
30+
"type",
31+
"desc",
32+
"year",
33+
"reportDate",
34+
"applicantIncomes",
35+
"total"
36+
) match {
37+
case Seq(table, reportType, description, year, reportDate, applicantIncomes, total) =>
38+
N52(
39+
year.convertTo[Int],
40+
reportDate.convertTo[String],
41+
applicantIncomes.convertTo[List[ApplicantIncome]],
42+
total.convertTo[List[Disposition]]
43+
)
44+
}
45+
}
46+
47+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package hmda.publication.reports.national
2+
3+
import org.scalatest.prop.PropertyChecks
4+
import org.scalatest.{ MustMatchers, PropSpec }
5+
import spray.json._
6+
import NationalAggregateReportGenerators._
7+
import hmda.publication.reports.protocol.national.N52Protocol._
8+
9+
class N52ProtocolSpec extends PropSpec with PropertyChecks with MustMatchers {
10+
11+
property("N52 Report must convert to and from JSON") {
12+
forAll(n52Gen) { n52 =>
13+
n52.toJson.convertTo[N52] mustBe n52
14+
}
15+
}
16+
17+
property("N52 Report must serialize to the correct JSON format") {
18+
forAll(n52Gen) { n52 =>
19+
n52.toJson mustBe JsObject(
20+
"table" -> JsString("5-2"),
21+
"type" -> JsString("NationalAggregate"),
22+
"desc" -> JsString("Disposition of Applications for Conventional Home-Purchase Loans, 1-to-4 Family and Manufactured Home Dwellings, by Income, Race, and Ethnicity of Applicant"),
23+
"year" -> JsNumber(n52.year),
24+
"reportDate" -> JsString(n52.reportDate),
25+
"applicantIncomes" -> n52.applicantIncomes.toJson,
26+
"total" -> n52.total.toJson
27+
)
28+
}
29+
}
30+
}
31+
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package hmda.publication.reports.national
2+
3+
import akka.NotUsed
4+
import akka.actor.ActorSystem
5+
import akka.stream.ActorMaterializer
6+
import akka.stream.scaladsl.Source
7+
import hmda.census.model.CbsaLookup
8+
import hmda.model.fi.lar.{ LarGenerators, LoanApplicationRegister }
9+
import hmda.model.publication.reports.ActionTakenTypeEnum._
10+
import hmda.model.publication.reports.ApplicantIncomeEnum.LessThan50PercentOfMSAMedian
11+
import hmda.model.publication.reports.{ EthnicityBorrowerCharacteristic, MinorityStatusBorrowerCharacteristic, RaceBorrowerCharacteristic }
12+
import hmda.query.model.filing.LoanApplicationRegisterQuery
13+
import hmda.query.repository.filing.LarConverter._
14+
import org.scalacheck.Gen
15+
import org.scalatest.{ AsyncWordSpec, BeforeAndAfterAll, MustMatchers }
16+
17+
class N52Spec extends AsyncWordSpec with MustMatchers with LarGenerators with BeforeAndAfterAll {
18+
19+
implicit val system = ActorSystem()
20+
implicit val ec = system.dispatcher
21+
implicit val materializer = ActorMaterializer()
22+
23+
override def afterAll(): Unit = {
24+
super.afterAll()
25+
system.terminate()
26+
}
27+
28+
val fips = CbsaLookup.values.map(_.cbsa).filterNot(f => f.isEmpty || f == "99999")
29+
val fipsGen = Gen.oneOf(fips)
30+
def propType = Gen.oneOf(1, 2).sample.get
31+
32+
val lars = larListGen.sample.get.map { lar: LoanApplicationRegister =>
33+
val geo = lar.geography.copy(msa = fipsGen.sample.get)
34+
val loan = lar.loan.copy(loanType = 1, propertyType = propType, purpose = 1)
35+
lar.copy(geography = geo, loan = loan)
36+
}
37+
38+
val source: Source[LoanApplicationRegisterQuery, NotUsed] = Source
39+
.fromIterator(() => lars.toIterator)
40+
.map(lar => toLoanApplicationRegisterQuery(lar))
41+
42+
val expectedDispositions = List(ApplicationReceived, LoansOriginated, ApprovedButNotAccepted, ApplicationsDenied, ApplicationsWithdrawn, ClosedForIncompleteness)
43+
44+
"Generate an National Aggregate 5-2 report" in {
45+
N52.generate(source).map { result =>
46+
47+
result.table mustBe "5-2"
48+
result.applicantIncomes.size mustBe 5
49+
50+
val lowestIncome = result.applicantIncomes.head
51+
lowestIncome.applicantIncome mustBe LessThan50PercentOfMSAMedian
52+
53+
val races = lowestIncome.borrowerCharacteristics.head.asInstanceOf[RaceBorrowerCharacteristic].races
54+
races.size mustBe 8
55+
56+
val ethnicities = lowestIncome.borrowerCharacteristics(1).asInstanceOf[EthnicityBorrowerCharacteristic].ethnicities
57+
ethnicities.size mustBe 4
58+
59+
val minorityStatuses = lowestIncome.borrowerCharacteristics(2).asInstanceOf[MinorityStatusBorrowerCharacteristic].minoritystatus
60+
minorityStatuses.size mustBe 2
61+
62+
races.head.dispositions.map(_.disposition) mustBe expectedDispositions
63+
}
64+
}
65+
66+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package hmda.publication.reports.national
2+
3+
import java.util.Calendar
4+
5+
import hmda.publication.reports.ReportGenerators._
6+
import hmda.publication.reports.util.DateUtil._
7+
import org.scalacheck.Gen
8+
9+
object NationalAggregateReportGenerators {
10+
implicit def n52Gen: Gen[N52] = {
11+
for {
12+
total <- totalDispositionGen
13+
year = Calendar.getInstance().get(Calendar.YEAR)
14+
reportDate = formatDate(Calendar.getInstance().toInstant)
15+
applicantIncomes <- Gen.listOfN(5, applicantIncomeGen)
16+
} yield N52(year, reportDate, applicantIncomes, total)
17+
}
18+
}

0 commit comments

Comments
 (0)