Finding max value using reduce - clojure

New to clojure with a java background. I have the following table and need to transform the table to a hash-map that maps products to the city that has the highest sale. For example, the output should look like:
{"Pencil": "Toronto"
"Bread": "Ottawa"}
(def table [
{:product "Pencil"
:city "Toronto"
:year "2010"
:sales "2653.00"}
{:product "Pencil"
:city "Oshawa"
:year "2010"
:sales "525.00"}
{:product "Bread"
:city "Toronto"
:year "2010"
:sales "136,264.00"}
{:product "Bread"
:city "Oshawa"
:year "nil"
:sales "242,634.00"}
{:product "Bread"
:city "Ottawa"
:year "2011"
:sales "426,164.00"}])
This is what I have so far:
(reduce (fn [product-cities {:keys [product sales]}]
(update-in product-cities [product] (fnil conj []) sales))
{}
table)
This produces the outcome:
{"Bread"
["136,264.00"
"242,634.00"
"426,164.00"],
"Pencil" ["2653.00" "525.00"]}
How can i compare the sales of each city and and only keep the name of the city with the highest sales? Having a really tough time with this. Thanks

there is a handy function max-key in clojure.core, that is perfectly suitable for this case:
(defn process [table]
(let [parseDouble #(Double/parseDouble (clojure.string/replace % #"," ""))]
(->> table
(group-by :product)
(map (comp (juxt :product :city)
(partial apply max-key (comp parseDouble :sales))
val))
(into {}))))
user> (process table)
;;=> {"Pencil" "Toronto", "Bread" "Ottawa"}
the key is that (partial apply max-key (comp parseDouble :sales)) part looks for the record in a group, having maximum parsed sales value.

You need some function that will convert the sales values from strings to numbers. For now assuming the sales numbers are indeed numbers, this should do the trick:
(->> table
(group-by :product)
(map (fn [[k v]]
[k (first (sort-by (comp - identity :sales) v))]))
(into {})
vals
(map (comp #(apply vector %)
vals
#(select-keys % [:product :city])))
(into {}))
Replace identity with your string->number function.
No doubt this function can be improved...

You can use something like:
(into {} (map (fn [[k {:keys [city sales]}]] [k city])
(reduce (fn [product-cities {:keys [product sales city]}]
(let [sales (Double/parseDouble (clojure.string/replace sales "," ""))
prev-sales (get-in product-cities [product :sales] 0)]
(if (> sales prev-sales)
(assoc product-cities product {:sales sales :city city})
product-cities)))
{}
table)))
P.S. Although previous answer could be more readable...

Here is a pretty fast version that avoids intermediate data structures:
(let [parse #(Double/parseDouble (clojure.string/replace % "," ""))]
(reduce (fn [m {:keys [product sales city] :as cand}]
(let [sales-d (parse sales)]
(update m product (fn [prev]
(if (or (nil? prev) (< (:sales prev) sales-d))
(assoc cand :sales sales-d)
prev)))))
{} products))

A spinoff idea based on #leetwinski answer.
Idea is to use sort-by on sales' values as it's a bit more basic in the language.
(defn process [table]
(let [parseDouble #(Double/parseDouble (clojure.string/replace % #"," ""))
parsedTable (for [a table] (update a :sales parseDouble))]
(->> parsedTable
(sort-by :sales)
(group-by :product)
vals
(map (comp (juxt :product :city) last))
(into {}))))
(process table)
=>{"Bread" "Ottawa" "Pencil" "Toronto"}

Here is how I would do it. I used spyx-pretty from the Tupelo library to make visualizing the intermediate steps easier (API docs can be found here). The code:
(ns tst.demo.core
(:use demo.core
tupelo.test)
(:require [tupelo.core :as t]
[clojure.string :as str] ))
(t/refer-tupelo)
(def table
[{:product "Pencil" :city "Toronto" :year "2010" :sales "2653.00"}
{:product "Pencil" :city "Oshawa" :year "2010" :sales "525.00"}
{:product "Bread" :city "Toronto" :year "2010" :sales "136,264.00"}
{:product "Bread" :city "Oshawa" :year "nil" :sales "242,634.00"}
{:product "Bread" :city "Ottawa" :year "2011" :sales "426,164.00"}])
(defn str->double
"Convert a string like '2,123.97' to a double like 2123.97 "
[str-val]
(let [no-commas (str/replace str-val #"," "")
dbl-val (Double/parseDouble no-commas)]
dbl-val))
(dotest
(let [table-num (forv [item table]
(update item :sales str->double))
grouped (group-by :product table-num)
>> (spyx-pretty grouped)
group-max (forv [group grouped]
(do
(spyx-pretty group)
(let [records (xsecond group)
>> (spyx-pretty records)
records-sorted (sort-by :sales > records)
>> (spyx-pretty records-sorted)
max-rec (xfirst records-sorted)
]
(spyx max-rec))))]
(spyx-pretty group-max)))
The results are:
---------------------------------------
Clojure 1.9.0-beta1 Java 9.0.1
---------------------------------------
Testing tst.demo.core
grouped =>
{"Pencil"
[{:product "Pencil", :city "Toronto", :year "2010", :sales 2653.0}
{:product "Pencil", :city "Oshawa", :year "2010", :sales 525.0}],
"Bread"
[{:product "Bread", :city "Toronto", :year "2010", :sales 136264.0}
{:product "Bread", :city "Oshawa", :year "nil", :sales 242634.0}
{:product "Bread", :city "Ottawa", :year "2011", :sales 426164.0}]}
group =>
["Pencil"
[{:product "Pencil", :city "Toronto", :year "2010", :sales 2653.0}
{:product "Pencil", :city "Oshawa", :year "2010", :sales 525.0}]]
records =>
[{:product "Pencil", :city "Toronto", :year "2010", :sales 2653.0}
{:product "Pencil", :city "Oshawa", :year "2010", :sales 525.0}]
records-sorted =>
({:product "Pencil", :city "Toronto", :year "2010", :sales 2653.0}
{:product "Pencil", :city "Oshawa", :year "2010", :sales 525.0})
max-rec => {:product "Pencil", :city "Toronto", :year "2010", :sales 2653.0}
group =>
["Bread"
[{:product "Bread", :city "Toronto", :year "2010", :sales 136264.0}
{:product "Bread", :city "Oshawa", :year "nil", :sales 242634.0}
{:product "Bread", :city "Ottawa", :year "2011", :sales 426164.0}]]
records =>
[{:product "Bread", :city "Toronto", :year "2010", :sales 136264.0}
{:product "Bread", :city "Oshawa", :year "nil", :sales 242634.0}
{:product "Bread", :city "Ottawa", :year "2011", :sales 426164.0}]
records-sorted =>
({:product "Bread", :city "Ottawa", :year "2011", :sales 426164.0}
{:product "Bread", :city "Oshawa", :year "nil", :sales 242634.0}
{:product "Bread", :city "Toronto", :year "2010", :sales 136264.0})
max-rec => {:product "Bread", :city "Ottawa", :year "2011", :sales 426164.0}
group-max =>
[{:product "Pencil", :city "Toronto", :year "2010", :sales 2653.0}
{:product "Bread", :city "Ottawa", :year "2011", :sales 426164.0}]
Note that the 1st step is to convert all string sales values to floating point. Then, it is easiest if we use the built-in function group-by to separate out pencils from Bread, etc. I like to keep each step separate for ease of thinking, and also so I can put debug printouts at each step.
IMHO this is more straightforward than using the REPL, as I can stay in my favorite IDE/editor and the stuff I type is saved in a file rather than disappearing as soon as I hit .

Related

Get list of values from vector of hashmap using filter or for

I'm quite new to clojure and have been struggling to understand how things work exactly. I have a vector of hashmaps as such, titled authors:
------ Authors -----------
[{:id 100, :name "Albert Einstein", :interest "Physics"}
{:id 200, :name "Alan Turing", :interest "Computer Science"}
{:id 300, :name "Jeff Dean", :interest "Programming"}]
I want to write a function that takes the id, and returns a list of the corresponding author names. I have two options for doing so: using filter or using for loop.
When using filter, I have a predicate function already that returns true if the author has matching id:
(defn check-by-id [author id]
(if (= id (:id author)) true false))
But I'm not sure how to use this in order to get the list of author names when passing the id.
Three other ways via keep, for and reduce:
(keep (fn [{:keys [id name]}] (when (= id 100) name)) authors)
;; => ("Albert Einstein")
(for [{:keys [id name]} authors
:when (= id 100)]
name)
;; => ("Albert Einstein")
(reduce (fn [v {:keys [id name]}]
(if (= id 100) (conj v name) v))
[]
authors)
;; => ["Albert Einstein"]
I prefer for (with :when) since it's shortest and in my eyes most clear. reduce I find best when you want to build a specific type of collection, this case a vector.
Filter will filter the list of maps. But the result is still a sequence of maps. You can map or reduce the result to get the list of authors.
(def authors [{:id 100, :name "Albert Einstein", :interest "Physics"}
{:id 100, :name "Richard Fynmann", :interest "Physics"}
{:id 200, :name "Alan Turing", :interest "Computer Science"}
{:id 300, :name "Jeff Dean", :interest "Programming"}])
(defn check-by-id [author id] (= id (:id author)))
(defn filter-ids [id col] (filter #(check-by-id % id) col))
(filter-ids 100 authors)
;; ↪ ({:id 100, :name "Albert Einstein", :interest "Physics"}
;; {:id 100, :name "Richard Fynmann", :interest "Physics"})
(map :name (filter-ids 100 authors))
;; ↪ ("Albert Einstein" "Richard Fynmann")
You can also use group-by for this task:
(def list-of-maps
[{:id 100, :name "Albert Einstein", :interest "Physics"}
{:id 200, :name "Alan Turing", :interest "Computer Science"}
{:id 300, :name "Jeff Dean", :interest "Programming"}])
(map :name (get (group-by :id list-of-maps) 100))
;; => ("Albert Einstein")

clojure - presence of a given set of keys in a clojure map

There is a map like this
{:buyers [{:name "James" :city "Berlin"} {:name "Jane" :city "Milan"}]
:sellers [{:name "Dustin" :city "Turin" :age "21"} {:name "Mark" :city "Milan"}]}
and I need to check only for :sellers that all the keys :name, :city and :age are present and if one is missing drop
that map all together and have a new structure as below:
{:buyers [{:name "James" :city "Berlin"} {:name "Jane" :city "Milan"}]
:sellers [{:name "Dustin" :city "Turin" :age "21"}]}
I came across validateur and I am trying to use it like:
(:require [validateur.validation :as v])
(def my-map {:buyers [{:name "James" :city "Berlin"} {:name "Jane" :city "Milan"}]
:sellers [{:name "Dustin" :city "Turin" :age "21"} {:name "Dustin" :city "Milan" :age "" } {:city "Rome" :age "22"}]})
(defn valid-seller? [mp]
(let [v-set (v/validation-set
(v/presence-of #{:name :city :age}))]
(fmap vec (v-set mp))))
(map valid-seller? (:sellers my-map))
=> ({} {:age ["can't be blank"]} {:name ["can't be blank"]})
But I do not know how to update my map so missing keys or nil values be dropped
To make the code more readable, I created a new predicate, valid-seller?, and put validation there. You can use any of these versions:
Pure Clojure:
(defn valid-seller? [m]
(every? #(contains? m %) [:name :city :age]))
Spec:
[org.clojure/spec.alpha "0.3.218"], require [clojure.spec.alpha :as spec]
(defn valid-seller? [m]
(spec/valid? (spec/keys :req-un [::name ::city ::age]) m))
Malli (if you also want to test type of values):
[metosin/malli "0.8.9"], require [malli.core :as malli]
(defn valid-seller? [m]
(malli/validate [:map
[:name :string]
[:city :string]
[:age :string]] m))
Then I used this predicate:
(update {:buyers [{:name "James" :city "Berlin"} {:name "Jane" :city "Milan"}]
:sellers [{:name "Dustin" :city "Turin" :age "21"} {:name "Mark" :city "Milan"}]}
:sellers
#(filter valid-seller? %))
=>
{:buyers [{:name "James", :city "Berlin"} {:name "Jane", :city "Milan"}],
:sellers ({:name "Dustin", :city "Turin", :age "21"})}
After your answer, I think you should use Malli, as it also checks the type of values. You can use some? for any non-nil value:
(defn valid-seller? [m]
(malli/validate [:map
[:name some?]
[:city some?]
[:age some?]] m))
(let [data {:buyers [{:name "James" :city "Berlin"} {:name "Jane" :city "Milan"}]
:sellers [{:name "Dustin" :city "Turin" :age "21"} {:name "Mark" :city "Milan"}]}]
(update data :sellers #(filter (every-pred :name :city :age) %)))

Clojure: Is there a way to remove a key but keep its values

I need some help with a pickle. I have the below map
(def my-list
{:data-0
{:a1 ;;this is the value for :ward-room
{:type "Specialist"
:name "Dr Spongebob"
:illness "dehydration"
:ward-room "a1"}}
:data-1
{:b5
{:type "GP"
:name "Dr Patrick"
:illness "fishy eyes"}
:ward-room "b5"}})
I want to transform this into a map such as below
(def my-new-list
{:data-0
{:type "Specialist"
:name "Dr Spongebob"
:illness "dehydration"
:ward-room "a1"}
:data-1
{:type "GP"
:name "Dr Patrick"
:illness "fishy eyes"}
:ward-room "b5"}})
I couldn't figure out how to do this so I've tried using seq and then flatten but have not had success. Im not too sure how to get started Solving this problem. I would really appreciate a little help
considering corrected structure, with :ward-room having the same nesting level for both entries:
(def my-list
{:data-0
{:a1 ;;this is the value for :ward-room
{:type "Specialist"
:name "Dr Spongebob"
:illness "dehydration"
:ward-room "a1"}}
:data-1
{:b5
{:type "GP"
:name "Dr Patrick"
:illness "fishy eyes"
:ward-room "b5"}}})
there are some options:
first of all you could use reduce-kv, remapping to new vals:
(reduce-kv
(fn [acc k v] (assoc acc k (val (first v))))
{}
my-list)
;; {:data-0
;; {:type "Specialist",
;; :name "Dr Spongebob",
;; :illness "dehydration",
;; :ward-room "a1"},
;; :data-1
;; {:type "GP",
;; :name "Dr Patrick",
;; :illness "fishy eyes",
;; :ward-room "b5"}}
or you could use zipmap to zip keys with new vals:
(zipmap (keys my-list)
(map (comp val first val) my-list))
also there is a nice way to do it with functional composition:
(into {} (map (juxt key (comp val first val))) my-list)
Another approach is to splice the entries for every internal map at the upper level. This would also work for 'malformed' data, like yours:
(def my-list
{:data-0
{:a1 ;;this is the value for :ward-room
{:type "Specialist"
:name "Dr Spongebob"
:illness "dehydration"
:ward-room "a1"}}
:data-1
{:b5
{:type "GP"
:name "Dr Patrick"
:illness "fishy eyes"}
:ward-room "b5"}})
(defn lift-entries [old-entries]
(into {} (mapcat #(if (map? (val %))
(val %)
[%]))
old-entries))
(zipmap (keys my-list) (map lift-entries (vals my-list)))
;; {:data-0
;; {:type "Specialist",
;; :name "Dr Spongebob",
;; :illness "dehydration",
;; :ward-room "a1"},
;; :data-1
;; {:type "GP",
;; :name "Dr Patrick",
;; :illness "fishy eyes",
;; :ward-room "b5"}}
I think your data sample had a cut/paste error. Here is a simple answer:
(def my-data
{:data-0 {:a1 {:type "Specialist"
:name "Dr Spongebob"
:illness "dehydration"
:ward-room "a1"}}
:data-1 {:b5 {:type "GP"
:name "Dr Patrick"
:illness "fishy eyes"
:ward-room "b5"
}}})
(defn delete-intermediate-level
[data]
(into {}
(for [[k1 v1] data]
[k1 (into {}
(for [[k2 v2] v1]
v2))])))
with result:
(delete-intermediate-level my-data) =>
{:data-0
{:type "Specialist",
:name "Dr Spongebob",
:illness "dehydration",
:ward-room "a1"},
:data-1
{:type "GP",
:name "Dr Patrick",
:illness "fishy eyes",
:ward-room "b5"}}
If my-list is supposed to be defined as
(def my-list
{:data-0
{:a1 ;;this is the value for :ward-room
{:type "Specialist"
:name "Dr Spongebob"
:illness "dehydration"
:ward-room "a1"}}
:data-1
{:b5
{:type "GP"
:name "Dr Patrick"
:illness "fishy eyes"
:ward-room "b5"}}})
(that is, if we include the :ward-room key and value in the :b5 map rather than having it floating around on the loose) then the quickest way I can see to do this is:
(zipmap (keys my-list) (map #(first (vals %)) (vals my-list)))
It we wrap the above in a (pprint...) form it returns
{:data-0
{:type "Specialist",
:name "Dr Spongebob",
:illness "dehydration",
:ward-room "a1"},
:data-1
{:type "GP",
:name "Dr Patrick",
:illness "fishy eyes",
:ward-room "b5"}}
EDIT
Or if you prefer it in thread-last form you can use
(->> my-list
(vals)
(map #(first (vals %)))
(zipmap (keys my-list)))

Merge list of maps by UUID

I have two list of maps
(def map1 ({:book/public-id #uuid "555b6f35-4e8c-42c5-bb80-b4d9147394c0", :book/name "AAA"}
{:book/public-id #uuid "555b6f35-4e8c-42c5-bb80-b4d9147394c3", :book/name "CCC"}))
and
(def map2 ({:book/public-id #uuid "555b6f35-4e8c-42c5-bb80-b4d9147394c3", :book/author "John"}
{:book/public-id #uuid "555b6f35-4e8c-42c5-bb80-b4d9147394c0", :book/author "Alan"}))
and I want to merge this maps by UUID to get following
({:book/public-id #uuid "555b6f35-4e8c-42c5-bb80-b4d9147394c0", :book/name "AAA", :book/author "Alan"}
{:book/public-id #uuid "555b6f35-4e8c-42c5-bb80-b4d9147394c3", :book/name "CCC", :book/author "John"})
What way can I do it?
(defn group-by-id [m]
(->> m
(map (juxt :book/public-id identity))
(into {})))
(vals (merge-with merge (group-by-id map1) (group-by-id map2)))
If you had vectors instead of lists, you can use join to merge these sets on matching values:
user=> (def map1 [{:book/public-id #uuid "555b6f35-4e8c-42c5-bb80-b4d9147394c0", :book/name "AAA"}
#_=> {:book/public-id #uuid "555b6f35-4e8c-42c5-bb80-b4d9147394c3", :book/name "CCC"}])
#'user/map1
user=> (def map2 [{:book/public-id #uuid "555b6f35-4e8c-42c5-bb80-b4d9147394c3", :book/author "John"}
#_=> {:book/public-id #uuid "555b6f35-4e8c-42c5-bb80-b4d9147394c0", :book/author "Alan"}])
#'user/map2
user=> (clojure.set/join map1 map2)
#{{:book/public-id #uuid "555b6f35-4e8c-42c5-bb80-b4d9147394c3", :book/name "CCC", :book/author "John"} {:book/public-id #uuid "555b6f35-4e8c-42c5-bb80-b4d9147394c0", :book/name "AAA", :book/author "Alan"}}
You can use merge to do that. I renamed the vars to make it a little more clear. Also, I used vectors to represent the collection as that is more idiomatic:
(def titles
[{:book/public-id #uuid "555b6f35-4e8c-42c5-bb80-b4d9147394c0"
:book/name "AAA"}
{:book/public-id #uuid "555b6f35-4e8c-42c5-bb80-b4d9147394c3"
:book/name "CCC"}])
(def authors
[{:book/public-id #uuid "555b6f35-4e8c-42c5-bb80-b4d9147394c3"
:book/author "John"}
{:book/public-id #uuid "555b6f35-4e8c-42c5-bb80-b4d9147394c0"
:book/author "Alan"}])
(def prices
[{:book/public-id #uuid "555b6f35-4e8c-42c5-bb80-b4d9147394c3"
:book/price 25}])
I created a prices var in order to show that the vectors don't need to be the same size. The first step would be to group each book info in one structure, and then we can use merge to get one map per book. To do that, we can use group-by:
(def book-info-by-uuid
(group-by :book/public-id (concat titles authors prices)))
Which will give us a map with uuid's as keys and a vector with all the info of each book as values:
{#uuid "555b6f35-4e8c-42c5-bb80-b4d9147394c0"
[{:book/public-id #uuid "555b6f35-4e8c-42c5-bb80-b4d9147394c0"
:book/name "AAA"}
{:book/public-id #uuid "555b6f35-4e8c-42c5-bb80-b4d9147394c0"
:book/author "Alan"}]
#uuid "555b6f35-4e8c-42c5-bb80-b4d9147394c3"
[{:book/public-id #uuid "555b6f35-4e8c-42c5-bb80-b4d9147394c3"
:book/name "CCC"}
{:book/public-id #uuid "555b6f35-4e8c-42c5-bb80-b4d9147394c3"
:book/author "John"}
{:book/public-id #uuid "555b6f35-4e8c-42c5-bb80-b4d9147394c3"
:book/price 25}]}
Finally, we use merge to obtain the result:
(map #(apply merge %)
(vals books-by-uuid))
({:book/public-id #uuid "555b6f35-4e8c-42c5-bb80-b4d9147394c0"
:book/name "AAA"
:book/author "Alan"}
{:book/public-id #uuid "555b6f35-4e8c-42c5-bb80-b4d9147394c3"
:book/name "CCC"
:book/author "John"
:book/price 25})

How best to update this tree?

I've got the following tree:
{:start_date "2014-12-07"
:data {
:people [
{:id 1
:projects [{:id 1} {:id 2}]}
{:id 2
:projects [{:id 1} {:id 3}]}
]
}
}
I want to update the people and projects subtrees by adding a :name key-value pair.
Assuming I have these maps to perform the lookup:
(def people {1 "Susan" 2 "John")
(def projects {1 "Foo" 2 "Bar" 3 "Qux")
How could I update the original tree so that I end up with the following?
{:start_date "2014-12-07"
:data {
:people [
{:id 1
:name "Susan"
:projects [{:id 1 :name "Foo"} {:id 2 :name "Bar"}]}
{:id 2
:name "John"
:projects [{:id 1 :name "Foo"} {:id 3 :name "Qux"}]}
]
}
}
I've tried multiple combinations of assoc-in, update-in, get-in and map calls, but haven't been able to figure this out.
I have used letfn to break down the update into easier to understand units.
user> (def tree {:start_date "2014-12-07"
:data {:people [{:id 1
:projects [{:id 1} {:id 2}]}
{:id 2
:projects [{:id 1} {:id 3}]}]}})
#'user/tree
user> (def people {1 "Susan" 2 "John"})
#'user/people
user> (def projects {1 "Foo" 2 "Bar" 3 "Qux"})
#'user/projects
user>
(defn integrate-tree
[tree people projects]
;; letfn is like let, but it creates fn, and allows forward references
(letfn [(update-person [person]
;; -> is the "thread first" macro, the result of each expression
;; becomes the first arg to the next
(-> person
(assoc :name (people (:id person)))
(update-in [:projects] update-projects)))
(update-projects [all-projects]
(mapv
#(assoc % :name (projects (:id %)))
all-projects))]
(update-in tree [:data :people] #(mapv update-person %))))
#'user/integrate-tree
user> (pprint (integrate-tree tree people projects))
{:start_date "2014-12-07",
:data
{:people
[{:projects [{:name "Foo", :id 1} {:name "Bar", :id 2}],
:name "Susan",
:id 1}
{:projects [{:name "Foo", :id 1} {:name "Qux", :id 3}],
:name "John",
:id 2}]}}
nil
Not sure if entirely the best approach:
(defn update-names
[tree people projects]
(reduce
(fn [t [id name]]
(let [person-idx (ffirst (filter #(= (:id (second %)) id)
(map-indexed vector (:people (:data t)))))
temp (assoc-in t [:data :people person-idx :name] name)]
(reduce
(fn [t [id name]]
(let [project-idx (ffirst (filter #(= (:id (second %)) id)
(map-indexed vector (get-in t [:data :people person-idx :projects]))))]
(if project-idx
(assoc-in t [:data :people person-idx :projects project-idx :name] name)
t)))
temp
projects)))
tree
people))
Just call it with your parameters:
(clojure.pprint/pprint (update-names tree people projects))
{:start_date "2014-12-07",
:data
{:people
[{:projects [{:name "Foo", :id 1} {:name "Bar", :id 2}],
:name "Susan",
:id 1}
{:projects [{:name "Foo", :id 1} {:name "Qux", :id 3}],
:name "John",
:id 2}]}}
With nested reduces
Reduce over the people to update corresponding names
For each people, reduce over projects to update corresponding names
The noisesmith solution looks better since doesn't need to find person index or project index for each step.
Naturally you tried to assoc-in or update-in but the problem lies in your tree structure, since the key path to update John name is [:data :people 1 :name], so your assoc-in code would look like:
(assoc-in tree [:data :people 1 :name] "John")
But you need to find John's index in the people vector before you can update it, same things happens with projects inside.