Spaces:
Sleeping
Sleeping
github-actions[bot]
commited on
Commit
·
bc7640d
1
Parent(s):
eae7723
sync: automatic content update from github
Browse files- README.md +7 -5
- Universal%20Media%20Plan%20-%20Input%20Table (1).csv +298 -0
- app.py +72 -0
- changelog.md +9 -0
- comscore_site_list.py +199 -0
- .gitattributes → gitattributes +0 -0
- index.html +0 -19
- media_plan_templater.py +633 -0
- requirements.txt +10 -0
- style.css +0 -28
README.md
CHANGED
|
@@ -1,10 +1,12 @@
|
|
| 1 |
---
|
| 2 |
title: Sales Toolkit
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk:
|
|
|
|
|
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 1 |
---
|
| 2 |
title: Sales Toolkit
|
| 3 |
+
emoji: 🦀
|
| 4 |
+
colorFrom: purple
|
| 5 |
+
colorTo: pink
|
| 6 |
+
sdk: streamlit
|
| 7 |
+
sdk_version: 1.45.0
|
| 8 |
+
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
---
|
| 11 |
|
| 12 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
Universal%20Media%20Plan%20-%20Input%20Table (1).csv
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Product,Placement,Text,Package Exist,Package Elements,Unit,PMP Rate,PG Rate,Direct IO Rate,Rate Type,Site(s),Ad Size,Start Date,End Date,Rate,Units,Cost,SOV,Notes,Desktop,Tablet,Mobile,Available PG,Available PMP,Third Party Creative
|
| 2 |
+
Creator Content+ - Written,# Pieces of Creator Content+ [Article(s)/Recipes] & # Social Posts_Package,Written,Yes,,,-,-,1.15,CPPV,Raptive,PKG,Insert,Insert,See rate card,,,100%,"No third party tracking on post or social; page views listed; must bill off of first party numbers
|
| 3 |
+
|
| 4 |
+
Non-cancellable",x,x,x,No,No,N
|
| 5 |
+
Creator Content+ - Written,Creator Content+ Sponsorship_Surrounding Newly Created Content_Leaderboard,Written,Yes,Leaderboard,,-,-,-,-,Raptive,728x90,-,-,-,-,-,-,,x,x,,-,-,Y
|
| 6 |
+
Creator Content+ - Written,Creator Content+ Sponsorship_Surrounding Newly Created Content_Medium Rectangle,Written,Yes,Medium Rectangle,,-,-,-,-,Raptive,300x250,-,-,-,-,-,-,,x,x,x,-,-,Y
|
| 7 |
+
Creator Content+ - Written,Creator Content+ Sponsorship_Surrounding Newly Created Content_Mobile Banner,Written,Yes,Mobile Banner,,-,-,-,-,Raptive,320x50,-,-,-,-,-,-,,,,x,-,-,Y
|
| 8 |
+
Creator Content+ - Video,# Pieces of Creator Content+ [Videos] + # Social Posts,Video,No,,# Pieces of Creator Content+ [Videos] + # Social Posts,-,-,0.05,CPV,Raptive,n/a,Insert,Insert,See rate card,,,100%,"No third party tracking; social views listed (3s); must bill off of first party numbers
|
| 9 |
+
|
| 10 |
+
Non-cancellable",x,x,x,No,No,N
|
| 11 |
+
Creator Content+ - In-Feed Social,Creator Content+_# Creators_Each doing # In-feed Social Post(s) [total of # posts],In-Feed Social,No,,Creator Content+_# Creators_Each doing # In-feed Social Post(s) [total of # posts],-,-,8,CPM,Raptive,n/a,Insert,Insert,See rate card,,,100%,"No third party tracking; must bill off of first party numbers
|
| 12 |
+
|
| 13 |
+
Non-cancellable",x,x,x,No,No,N
|
| 14 |
+
Creator Content Social Amplifier+,Creator Content Social Amplifier+ [# Articles]_Package,Articles,Yes,,,-,-,8,CPM,Raptive,PKG,Insert,Insert,See rate card,,,100%,"No third party tracking on social; must bill off of first party numbers
|
| 15 |
+
|
| 16 |
+
On-site roabdlock will appear when the user enters from social
|
| 17 |
+
|
| 18 |
+
Non-cancellable",x,x,x,No,No,N
|
| 19 |
+
Creator Content Social Amplifier+,Creator Content Social Amplifier+ Roadblock_Leaderboard,Articles,Yes,Leaderboard,,-,-,-,-,Raptive,728x90,-,-,-,-,-,-,,x,x,,-,-,Y
|
| 20 |
+
Creator Content Social Amplifier+,Creator Content Social Amplifier+ Roadblock_Medium Rectangle,Articles,Yes,Medium Rectangle,,-,-,-,-,Raptive,300x250,-,-,-,-,-,-,,x,x,x,-,-,Y
|
| 21 |
+
Creator Content Social Amplifier+,Creator Content Social Amplifier+ Roadblock_Mobile Banner,Articles,Yes,Mobile Banner,,-,-,-,-,Raptive,320x50,-,-,-,-,-,-,,,,x,-,-,Y
|
| 22 |
+
Creator Social Video Amplifier+,Creator Social Video Amplifier+ [# Videos],Videos,Yes,,,-,-,8,CPM,Raptive,PKG,Insert,Insert,See rate card,,,100%,"No third party tracking on social; must bill off of first party numbers
|
| 23 |
+
|
| 24 |
+
Non-cancellable",x,x,x,No,No,N
|
| 25 |
+
Newsletters (featuring creators included in Creator Content+),Newsletter Sponsorship Package_# Sponsored Email Newsletters per Creator (with Creator Content + Content Distribution) - Creator with ~#k - #k Subscribers (# Email Newsletters Total),,No,,Newsletter Sponsorship Package_# Sponsored Email Newsletters per Creator (with Creator Content + Content Distribution) - Creator with ~#k - #k Subscribers (# Email Newsletters Total),-,-,25,CPM,Raptive,n/a,Insert,Insert,See rate card,,,100%,"Clicks from Email Newsletter to Creator Content+ Will Not Count Toward CPPV Goal
|
| 26 |
+
|
| 27 |
+
No third party tracking; sends listed; must bill off first party numbers
|
| 28 |
+
|
| 29 |
+
Non-cancellable
|
| 30 |
+
|
| 31 |
+
If the creators selected for this program do not have newsletters, we will shift that budget into other placements.",x,x,x,No,No,N
|
| 32 |
+
Content Sponsorships,[Insert Topic] Creator Content Sponsorship_Package,,Yes,,,-,-,17,CPM,Raptive,PKG,Insert (1 week min),Insert (1 week min),CPM,,,100%,"Guaranteed placement for first 60s of user session
|
| 33 |
+
|
| 34 |
+
Package delivery may include related content impressions
|
| 35 |
+
|
| 36 |
+
Package will go live at 9am eastern",PKG,PKG,PKG,No,No,PKG
|
| 37 |
+
Content Sponsorships,[Insert Topic] Creator Content Sponsorship_Leaderboard,,Yes,Leaderboard,,-,-,-,-,Raptive,728x90,-,-,-,-,-,-,,x,x,,-,-,Y
|
| 38 |
+
Content Sponsorships,[Insert Topic] Creator Content Sponsorship_Medium Rectangle,,Yes,Medium Rectangle,,-,-,-,-,Raptive,300x250,-,-,-,-,-,-,,x,x,x,-,-,Y
|
| 39 |
+
Content Sponsorships,[Insert Topic] Creator Content Sponsorship_Mobile Banner,,Yes,Mobile Banner,,-,-,-,-,Raptive,320x50,-,-,-,-,-,-,,,,x,-,-,Y
|
| 40 |
+
Content Sponsorships,[Insert Topic] Creator Content Sponsorship_Sponsorship Tile,,Yes,Sponsorship Tile,,-,-,-,-,Raptive,Logo,-,-,-,-,-,-,"OPTIONAL
|
| 41 |
+
No third party tracking",x,x,x,-,-,N
|
| 42 |
+
Content Sponsorships,[Insert Topic] Creator Content Sponsorship_Package,Food Product Advertisers Only,Yes,,,-,-,17,CPM,Raptive,PKG,Insert (1 week min),Insert (1 week min),CPM,,,100%,"Guaranteed placement for first 60s of user session (728x90, 300x250, 320x50)
|
| 43 |
+
|
| 44 |
+
Package delivery may include related content impressions
|
| 45 |
+
|
| 46 |
+
Package will go live at 9am eastern",PKG,PKG,PKG,No,No,PKG
|
| 47 |
+
Content Sponsorships,[Insert Topic] Creator Content Sponsorship_Leaderboard,Food Product Advertisers Only,Yes,Leaderboard,,-,-,-,-,Raptive,728x90,-,-,-,-,-,-,,x,x,,-,-,Y
|
| 48 |
+
Content Sponsorships,[Insert Topic] Creator Content Sponsorship_Medium Rectangle,Food Product Advertisers Only,Yes,Medium Rectangle,,-,-,-,-,Raptive,300x250,-,-,-,-,-,-,,x,x,x,-,-,Y
|
| 49 |
+
Content Sponsorships,[Insert Topic] Creator Content Sponsorship_Mobile Banner,Food Product Advertisers Only,Yes,Mobile Banner,,-,-,-,-,Raptive,320x50,-,-,-,-,-,-,,,,x,-,-,Y
|
| 50 |
+
Content Sponsorships,[Insert Topic] Creator Content Sponsorship_In-Recipe Product Recommendation Unit,Food Product Advertisers Only,Yes,In-Recipe Product Recommendation Unit,,-,-,-,-,Raptive,1x1,-,-,-,-,-,-,Persistent,x,x,x,-,-,N
|
| 51 |
+
Content Sponsorships,[Insert Topic] Creator Content Sponsorship_Sponsorship Tile,Food Product Advertisers Only,Yes,Sponsorship Tile,,-,-,-,-,Raptive,Logo,-,-,-,-,-,-,"OPTIONAL
|
| 52 |
+
No third party tracking",x,x,x,-,-,N
|
| 53 |
+
Roadblocks,First Impression Roadblock Targeted to [Insert Channel]_Package,,Yes,,,-,-,16,CPM,Raptive,PKG,Insert,Insert,See rate card,,,<10%,"Impressions are delivered on a first non-sponsored view. Brand does not have permanent roadblocks on qualifying content.
|
| 54 |
+
|
| 55 |
+
Package will go live at 9am eastern",PKG,PKG,PKG,No,No,PKG
|
| 56 |
+
Roadblocks,First Impression Roadblock Targeted to [Insert Channel]_Leaderboard,,Yes,Leaderboard,,-,-,-,-,Raptive,728x90,-,-,-,-,-,-,,x,x,,-,-,Y
|
| 57 |
+
Roadblocks,First Impression Roadblock Targeted to [Insert Channel]_Medium Rectangle,,Yes,Medium Rectangle,,-,-,-,-,Raptive,300x250,-,-,-,-,-,-,,x,x,x,-,-,Y
|
| 58 |
+
Roadblocks,First Impression Roadblock Targeted to [Insert Channel]_Mobile Banner,,Yes,Mobile Banner,,-,-,-,-,Raptive,320x50,-,-,-,-,-,-,,,,x,-,-,Y
|
| 59 |
+
Roadblocks,First Impression Roadblock with Video_Targeted to [Insert Channel]_Package,,Yes,,,-,-,18,CPM,Raptive,PKG,Insert,Insert,See rate card,,,<10%,"Impressions are delivered on a first non-sponsored view. Brand does not have permanent roadblocks on qualifying content.
|
| 60 |
+
|
| 61 |
+
Package will go live at 9am eastern",PKG,PKG,PKG,No,No,PKG
|
| 62 |
+
Roadblocks,First Impression Roadblock with Video_Targeted to [Insert Channel]_Leaderboard,,Yes,Leaderboard,,-,-,-,-,Raptive,728x90,-,-,-,-,-,-,,x,x,,-,-,Y
|
| 63 |
+
Roadblocks,First Impression Roadblock with Video_Targeted to [Insert Channel]_Medium Rectangle,,Yes,Medium Rectangle,,-,-,-,-,Raptive,300x250,-,-,-,-,-,-,,x,x,x,-,-,Y
|
| 64 |
+
Roadblocks,First Impression Roadblock with Video_Targeted to [Insert Channel]_Mobile Banner,,Yes,Mobile Banner,,-,-,-,-,Raptive,320x50,-,-,-,-,-,-,,,,x,-,-,Y
|
| 65 |
+
Roadblocks,First Impression Roadblock with Video_Targeted to [Insert Channel]_Pre-roll,,Yes,Pre-roll,,-,-,-,-,Raptive,"16:9 Aspect Ratio, :06s, :15s, :30s",-,-,-,-,-,-,,x,x,x,-,-,Y
|
| 66 |
+
Roadblocks,Recipe Recommendation First Impression Roadblock [Insert Channel]_Package,???,Yes,,,-,-,16,CPM,Raptive,PKG,Insert,Insert,See rate card,,,<10%,"Impressions are delivered on a first non-sponsored view. Brand does not have permanent roadblocks on qualifying content.
|
| 67 |
+
|
| 68 |
+
Package will go live at 9am eastern",PKG,PKG,PKG,Yes,No,PKG
|
| 69 |
+
Roadblocks,Recipe Recommendation First Impression Roadblock [Insert Channel]_Leaderboard,,Yes,Leaderboard,,-,-,-,-,Raptive,728x90,-,-,-,-,-,-,,x,x,,-,-,Y
|
| 70 |
+
Roadblocks,Recipe Recommendation First Impression Roadblock [Insert Channel]_Medium Rectangle,,Yes,Medium Rectangle,,-,-,-,-,Raptive,300x250,-,-,-,-,-,-,,x,x,x,-,-,Y
|
| 71 |
+
Roadblocks,Recipe Recommendation First Impression Roadblock [Insert Channel]_Mobile Banner,,Yes,Mobile Banner,,-,-,-,-,Raptive,320x50,-,-,-,-,-,-,,,,x,-,-,Y
|
| 72 |
+
Roadblocks,Recipe Recommendation First Impression Roadblock [Insert Channel]_In-Recipe Product Recommendation Unit,,Yes,In-Recipe Product Recommendation Unit,,-,-,-,-,Raptive,1x1,-,-,-,-,-,-,,x,x,x,-,-,N
|
| 73 |
+
Roadblocks,Recipe Box First Impression Roadblock [Insert Channel]_Package,???,Yes,,,-,-,16,CPM,Raptive,PKG,Insert,Insert,See rate card,,,<10%,"Impressions are delivered on a first non-sponsored view. Brand does not have permanent roadblocks on qualifying content.
|
| 74 |
+
|
| 75 |
+
Package will go live at 9am eastern",PKG,PKG,PKG,Yes,No,PKG
|
| 76 |
+
Roadblocks,Recipe Box First Impression Roadblock [Insert Channel]_Medium Rectangle,,Yes,Medium Rectangle,,-,-,-,-,Raptive,300x250,-,-,-,-,-,-,,x,x,x,-,-,Y
|
| 77 |
+
Roadblocks,Recipe Box First Impression Roadblock [Insert Channel]_In-Recipe Product Recommendation Unit,,Yes,In-Recipe Product Recommendation Unit,,-,-,-,-,Raptive,1x1,-,-,-,-,-,-,,x,x,x,-,-,N
|
| 78 |
+
Roadblocks,Beyond Banner First Impression Roadblock Targeted to [Insert Channel]_Package,,Yes,,,-,-,17,CPM,Raptive,PKG,Insert,Insert,See rate card,,,<10%,"Impressions are delivered on a first non-sponsored view. Brand does not have permanent roadblocks on qualifying content.
|
| 79 |
+
|
| 80 |
+
Package will go live at 9am eastern",PKG,PKG,PKG,No,No,PKG
|
| 81 |
+
Roadblocks,Beyond Banner First Impression Roadblock Targeted to [Insert Channel]_Beyond Banner,,Yes,Beyond Banner,,-,-,-,-,Raptive,1x1,-,-,-,-,-,-,,x,x,x,-,-,N
|
| 82 |
+
Roadblocks,Beyond Banner First Impression Roadblock Targeted to [Insert Channel]_Medium Rectangle,,Yes,Medium Rectangle,,-,-,-,-,Raptive,300x250,-,-,-,-,-,-,,x,x,x,-,-,Y
|
| 83 |
+
Roadblocks,Mural First Impression Roadblock Targeted to [Insert Channel]_Package,???,Yes,,,-,-,16,CPM,Raptive,PKG,Insert,Insert,See rate card,,,<10%,"Impressions are delivered on a first non-sponsored view. Brand does not have permanent roadblocks on qualifying content.
|
| 84 |
+
|
| 85 |
+
Package will go live at 9am eastern",PKG,PKG,PKG,Yes,No,PKG
|
| 86 |
+
Roadblocks,Mural First Impression Roadblock Targeted to [Insert Channel]_Leaderboard,,Yes,Leaderboard,,-,-,-,-,Raptive,728x90,-,-,-,-,-,-,,x,x,,-,-,Y
|
| 87 |
+
Roadblocks,Mural First Impression Roadblock Targeted to [Insert Channel]_Medium Rectangle,,Yes,Medium Rectangle,,-,-,-,-,Raptive,300x250,-,-,-,-,-,-,,x,x,x,-,-,Y
|
| 88 |
+
Roadblocks,Mural First Impression Roadblock Targeted to [Insert Channel]_Mural,,Yes,Mural,,-,-,-,-,Raptive,1x1,-,-,-,-,-,-,,x,x,x,-,-,N
|
| 89 |
+
Product Recommendation Units,In-Recipe Product Recommendation Unit Targeted to [Insert Recipe Type],,No,,In-Recipe Product Recommendation Unit Targeted to [Insert Recipe Type],-,8,8,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,Yes,No,N
|
| 90 |
+
Product Recommendation Units,In-Recipe Digital Circular Unit Targeted to [Insert Recipe Type],,No,,In-Recipe Digital Circular Unit Targeted to [Insert Recipe Type],-,8,8,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,Yes,No,N
|
| 91 |
+
Product Recommendation Units,Product Recommendation Unit Targeted to [Insert Content Type],,No,,Product Recommendation Unit Targeted to [Insert Content Type],-,-,9,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,Yes,No,N
|
| 92 |
+
High-Impact Media,Frame Targeted to [Insert Channel],,No,,Frame Targeted to [Insert Channel],7,8,8,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,Yes,Yes,N
|
| 93 |
+
High-Impact Media,Frame 360 View Targeted to [Insert Channel],,No,,Frame 360 View Targeted to [Insert Channel],8,9,9,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 94 |
+
High-Impact Media,Frame Carousel Targeted to [Insert Channel],,No,,Frame Carousel Targeted to [Insert Channel],7,8,8,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 95 |
+
High-Impact Media,Frame Cart Connect Targeted to [Insert Channel],,No,,Frame Cart Connect Targeted to [Insert Channel],-,10,10,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 96 |
+
High-Impact Media,Frame Content Collection Targeted to [Insert Channel],,No,,Frame Content Collection Targeted to [Insert Channel],8,9,9,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 97 |
+
High-Impact Media,Frame Countdown Targeted to [Insert Channel],,No,,Frame Countdown Targeted to [Insert Channel],7,8,8,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 98 |
+
High-Impact Media,Frame Digital Circular Targeted to [Insert Channel],,No,,Frame Digital Circular Targeted to [Insert Channel],-,9,9,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 99 |
+
High-Impact Media,Frame Dynamic Creative Targeted to [Insert Channel],,No,,Frame Dynamic Creative Targeted to [Insert Channel],7,8,8,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 100 |
+
High-Impact Media,Frame Glider Targeted to [Insert Channel],,No,,Frame Glider Targeted to [Insert Channel],8,9,9,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 101 |
+
High-Impact Media,Frame Hotspot Targeted to [Insert Channel],,No,,Frame Hotspot Targeted to [Insert Channel],8,9,9,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 102 |
+
High-Impact Media,Frame Retail Connect Targeted to [Insert Channel],,No,,Frame Retail Connect Targeted to [Insert Channel],7,8,8,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 103 |
+
High-Impact Media,Frame Scratch Off/Wipe Away Targeted to [Insert Channel],,No,,Frame Scratch Off/Wipe Away Targeted to [Insert Channel],8,9,9,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 104 |
+
High-Impact Media,Frame Scroll Reactive Animation Targeted to [Insert Channel],,No,,Frame Scroll Reactive Animation Targeted to [Insert Channel],7,8,8,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 105 |
+
High-Impact Media,Frame Visualizer Targeted to [Insert Channel],,No,,Frame Visualizer Targeted to [Insert Channel],8,9,9,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 106 |
+
High-Impact Media,Panorama Targeted to [Insert Channel],,No,,Panorama Targeted to [Insert Channel],8,11,11,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,Yes,Yes,N
|
| 107 |
+
High-Impact Media,Panorama 360 View Targeted to [Insert Channel],,No,,Panorama 360 View Targeted to [Insert Channel],9,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 108 |
+
High-Impact Media,Panorama Carousel Targeted to [Insert Channel],,No,,Panorama Carousel Targeted to [Insert Channel],8,11,11,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 109 |
+
High-Impact Media,Panorama Cart Connect Targeted to [Insert Channel],,No,,Panorama Cart Connect Targeted to [Insert Channel],-,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 110 |
+
High-Impact Media,Panorama Content Collection Targeted to [Insert Channel],,No,,Panorama Content Collection Targeted to [Insert Channel],9,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 111 |
+
High-Impact Media,Panorama Countdown Targeted to [Insert Channel],,No,,Panorama Countdown Targeted to [Insert Channel],8,11,11,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 112 |
+
High-Impact Media,Panorama Creator Endorsement Targeted to [Insert Channel],,No,,Panorama Creator Endorsement Targeted to [Insert Channel],-,15,15,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,Non-cancellable,x,x,x,,,
|
| 113 |
+
High-Impact Media,Panorama Creator Product Curation Targeted to [Insert Channel],,No,,Panorama Creator Product Curation Targeted to [Insert Channel],-,15,15,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,Non-cancellable,x,x,x,,,
|
| 114 |
+
High-Impact Media,Panorama Digital Circular Targeted to [Insert Channel],,No,,Panorama Digital Circular Targeted to [Insert Channel],-,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 115 |
+
High-Impact Media,Panorama Dynamic Creative Targeted to [Insert Channel],,No,,Panorama Dynamic Creative Targeted to [Insert Channel],8,11,11,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 116 |
+
High-Impact Media,Panorama Glider Targeted to [Insert Channel],,No,,Panorama Glider Targeted to [Insert Channel],9,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 117 |
+
High-Impact Media,Panorama Hotspot Targeted to [Insert Channel],,No,,Panorama Hotspot Targeted to [Insert Channel],9,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 118 |
+
High-Impact Media,Panorama Interactive Shopper Targeted to [Insert Channel],,No,,Panorama Interactive Shopper Targeted to [Insert Channel],9,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 119 |
+
High-Impact Media,Panorama Quiz Targeted to [Insert Channel],,No,,Panorama Quiz Targeted to [Insert Channel],10,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 120 |
+
High-Impact Media,Panorama Retail Connect Targeted to [Insert Channel],,No,,Panorama Retail Connect Targeted to [Insert Channel],8,11,11,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 121 |
+
High-Impact Media,Panorama Scratch Off/Wipe Away Targeted to [Insert Channel],,No,,Panorama Scratch Off/Wipe Away Targeted to [Insert Channel],9,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 122 |
+
High-Impact Media,Panorama Scroll Reactive Animation Targeted to [Insert Channel],,No,,Panorama Scroll Reactive Animation Targeted to [Insert Channel],8,11,11,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 123 |
+
High-Impact Media,Panorama Shoppable Video Targeted to [Insert Channel],,No,,Panorama Shoppable Video Targeted to [Insert Channel],10,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 124 |
+
High-Impact Media,Panorama Video Targeted to [Insert Channel],,No,,Panorama Video Targeted to [Insert Channel],10,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 125 |
+
High-Impact Media,Panorama Visualizer Targeted to [Insert Channel],,No,,Panorama Visualizer Targeted to [Insert Channel],9,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 126 |
+
High-Impact Media,Mural Targeted to [Insert Channel],,No,,Mural Targeted to [Insert Channel],-,14,14,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,Yes,No,N
|
| 127 |
+
High-Impact Media,Mural 360 View Targeted to [Insert Channel],,No,,Mural 360 View Targeted to [Insert Channel],-,15,15,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 128 |
+
High-Impact Media,Mural Carousel Targeted to [Insert Channel],,No,,Mural Carousel Targeted to [Insert Channel],-,14,14,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 129 |
+
High-Impact Media,Mural Cart Connect Targeted to [Insert Channel],,No,,Mural Cart Connect Targeted to [Insert Channel],-,16,16,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 130 |
+
High-Impact Media,Mural Cart View Targeted to [Insert Channel],,No,,Mural Cart View Targeted to [Insert Channel],-,16,16,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 131 |
+
High-Impact Media,Mural Content Collection Targeted to [Insert Channel],,No,,Mural Content Collection Targeted to [Insert Channel],-,15,15,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 132 |
+
High-Impact Media,Mural Countdown Targeted to [Insert Channel],,No,,Mural Countdown Targeted to [Insert Channel],-,14,14,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 133 |
+
High-Impact Media,Mural Creator Endorsement Targeted to [Insert Channel],,No,,Mural Creator Endorsement Targeted to [Insert Channel],-,18,18,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,Non-cancellable,x,x,x,,,
|
| 134 |
+
High-Impact Media,Mural Creator Product Curation Targeted to [Insert Channel],,No,,Mural Creator Product Curation Targeted to [Insert Channel],-,18,18,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,Non-cancellable,x,x,x,,,
|
| 135 |
+
High-Impact Media,Mural Digital Circular Targeted to [Insert Channel],,No,,Mural Digital Circular Targeted to [Insert Channel],-,15,15,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 136 |
+
High-Impact Media,Mural Dynamic Creative Targeted to [Insert Channel],,No,,Mural Dynamic Creative Targeted to [Insert Channel],-,14,14,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 137 |
+
High-Impact Media,Mural Glider Targeted to [Insert Channel],,No,,Mural Glider Targeted to [Insert Channel],-,15,15,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 138 |
+
High-Impact Media,Mural Hotspot Targeted to [Insert Channel],,No,,Mural Hotspot Targeted to [Insert Channel],-,15,15,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 139 |
+
High-Impact Media,Mural Interactive Shopper Targeted to [Insert Channel],,No,,Mural Interactive Shopper Targeted to [Insert Channel],-,15,15,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 140 |
+
High-Impact Media,Mural Quiz Targeted to [Insert Channel],,No,,Mural Quiz Targeted to [Insert Channel],-,16,16,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 141 |
+
High-Impact Media,Mural Retail Connect Targeted to [Insert Channel],,No,,Mural Retail Connect Targeted to [Insert Channel],-,14,14,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 142 |
+
High-Impact Media,Mural Scratch Off/Wipe Away Targeted to [Insert Channel],,No,,Mural Scratch Off/Wipe Away Targeted to [Insert Channel],-,15,15,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 143 |
+
High-Impact Media,Mural Scroll Reactive Animation Targeted to [Insert Channel],,No,,Mural Scroll Reactive Animation Targeted to [Insert Channel],-,14,14,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 144 |
+
High-Impact Media,Mural Shoppable Video Targeted to [Insert Channel],,No,,Mural Shoppable Video Targeted to [Insert Channel],-,16,16,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 145 |
+
High-Impact Media,Mural Video Targeted to [Insert Channel],,No,,Mural Video Targeted to [Insert Channel],-,16,16,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 146 |
+
High-Impact Media,Mural Visualizer Targeted to [Insert Channel],,No,,Mural Visualizer Targeted to [Insert Channel],-,15,15,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 147 |
+
High-Impact Media,Backdrop Targeted to [Insert Channel],,No,,Backdrop Targeted to [Insert Channel],-,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,Yes,No,N
|
| 148 |
+
High-Impact Media,Backdrop Dynamic Creative Targeted to [Insert Channel],,No,,Backdrop Dynamic Creative Targeted to [Insert Channel],-,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,,,
|
| 149 |
+
High-Impact Media,Backdrop Retail Connect Targeted to [Insert Channel],,No,,Backdrop Retail Connect Targeted to [Insert Channel],-,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,,,
|
| 150 |
+
High-Impact Media,Beyond Banner Targeted to [Insert Channel],,No,,Beyond Banner Targeted to [Insert Channel],8,11,11,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,Yes,Yes,N
|
| 151 |
+
High-Impact Media,Beyond Banner Carousel Targeted to [Insert Channel],,No,,Beyond Banner Carousel Targeted to [Insert Channel],8,11,11,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 152 |
+
High-Impact Media,Beyond Banner Cart Connect Targeted to [Insert Channel],,No,,Beyond Banner Cart Connect Targeted to [Insert Channel],-,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 153 |
+
High-Impact Media,Beyond Banner Countdown Targeted to [Insert Channel],,No,,Beyond Banner Countdown Targeted to [Insert Channel],8,11,11,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 154 |
+
High-Impact Media,Beyond Banner Digital Circular Targeted to [Insert Channel],,No,,Beyond Banner Digital Circular Targeted to [Insert Channel],-,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 155 |
+
High-Impact Media,Beyond Banner Dynamic Creative Targeted to [Insert Channel],,No,,Beyond Banner Dynamic Creative Targeted to [Insert Channel],8,11,11,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 156 |
+
High-Impact Media,Beyond Banner Expandable Targeted to [Insert Channel],,No,,Beyond Banner Expandable Targeted to [Insert Channel],-,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 157 |
+
High-Impact Media,Beyond Banner Expandable Content Collection Targeted to [Insert Channel],,No,,Beyond Banner Expandable Content Collection Targeted to [Insert Channel],-,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 158 |
+
High-Impact Media,Beyond Banner Expandable Hotspot Targeted to [Insert Channel],,No,,Beyond Banner Expandable Hotspot Targeted to [Insert Channel],-,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 159 |
+
High-Impact Media,Beyond Banner Expandable Interactive Shopper Targeted to [Insert Channel],,No,,Beyond Banner Expandable Interactive Shopper Targeted to [Insert Channel],-,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 160 |
+
High-Impact Media,Beyond Banner Expandable Video Targeted to [Insert Channel],,No,,Beyond Banner Expandable Video Targeted to [Insert Channel],-,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 161 |
+
High-Impact Media,Beyond Banner Expandable Visualizer Targeted to [Insert Channel],,No,,Beyond Banner Expandable Visualizer Targeted to [Insert Channel],-,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 162 |
+
High-Impact Media,Beyond Banner Retail Connect Targeted to [Insert Channel],,No,,Beyond Banner Retail Connect Targeted to [Insert Channel],-,11,11,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 163 |
+
High-Impact Media,Social Display Ad Targeted to [Insert Channel],,No,,Social Display Ad Targeted to [Insert Channel],-,10,10,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,Yes,No,N
|
| 164 |
+
High-Impact Media,Sticker Targeted to [Insert Channel],,No,,Sticker Targeted to [Insert Channel],-,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,Yes,No,N
|
| 165 |
+
High-Impact Media,Sticker Dynamic Creative Targeted to [Insert Channel],,No,,Sticker Dynamic Creative Targeted to [Insert Channel],-,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 166 |
+
High-Impact Media,Sticker Expandable Targeted to [Insert Channel],,No,,Sticker Expandable Targeted to [Insert Channel],-,14,14,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 167 |
+
High-Impact Media,Sticker Expandable Content Collection Targeted to [Insert Channel],,No,,Sticker Expandable Content Collection Targeted to [Insert Channel],-,14,14,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 168 |
+
High-Impact Media,Sticker Expandable Hotspot Targeted to [Insert Channel],,No,,Sticker Expandable Hotspot Targeted to [Insert Channel],-,14,14,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 169 |
+
High-Impact Media,Sticker Expandable Interactive Shopper Targeted to [Insert Channel],,No,,Sticker Expandable Interactive Shopper Targeted to [Insert Channel],-,14,14,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 170 |
+
High-Impact Media,Sticker Expandable Video Targeted to [Insert Channel],,No,,Sticker Expandable Video Targeted to [Insert Channel],-,14,14,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 171 |
+
High-Impact Media,Sticker Expandable Visualizer Targeted to [Insert Channel],,No,,Sticker Expandable Visualizer Targeted to [Insert Channel],-,14,14,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 172 |
+
High-Impact Media,Sticker Mini Expandable Carousel Targeted to [Insert Channel],,No,,Sticker Mini Expandable Carousel Targeted to [Insert Channel],-,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 173 |
+
High-Impact Media,Sticker Retail Connect Targeted to [Insert Channel],,No,,Sticker Retail Connect Targeted to [Insert Channel],-,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 174 |
+
High-Impact Media,Lens Free Fall Game Targeted to [Insert Channel],,No,,Lens Free Fall Game Targeted to [Insert Channel],-,10,10,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,Yes,No,N
|
| 175 |
+
High-Impact Media,Lens Story Spotlight Targeted to [Insert Channel],,No,,Lens Story Spotlight Targeted to [Insert Channel],-,9,9,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,,,
|
| 176 |
+
High-Impact Media,Lens Swipe Navigator Targeted to [Insert Channel],,No,,Lens Swipe Navigator Targeted to [Insert Channel],8,9,9,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,,,
|
| 177 |
+
High-Impact Media,Tower Banner Targeted to [Insert Channel],,No,,Tower Banner Targeted to [Insert Channel],9,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,Yes,Yes,N
|
| 178 |
+
High-Impact Media,Tower Banner 360 View Targeted to [Insert Channel],,No,,Tower Banner 360 View Targeted to [Insert Channel],10,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,,,
|
| 179 |
+
High-Impact Media,Tower Banner Carousel Targeted to [Insert Channel],,No,,Tower Banner Carousel Targeted to [Insert Channel],9,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,,,
|
| 180 |
+
High-Impact Media,Tower Banner Countdown Targeted to [Insert Channel],,No,,Tower Banner Countdown Targeted to [Insert Channel],9,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,,,
|
| 181 |
+
High-Impact Media,Tower Banner Digital Circular Targeted to [Insert Channel],,No,,Tower Banner Digital Circular Targeted to [Insert Channel],10,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,,,
|
| 182 |
+
High-Impact Media,Tower Banner Dynamic Creative Targeted to [Insert Channel],,No,,Tower Banner Dynamic Creative Targeted to [Insert Channel],9,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,,,
|
| 183 |
+
High-Impact Media,Tower Banner Glider Targeted to [Insert Channel],,No,,Tower Banner Glider Targeted to [Insert Channel],10,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,,,
|
| 184 |
+
High-Impact Media,Tower Banner Hotspot Targeted to [Insert Channel],,No,,Tower Banner Hotspot Targeted to [Insert Channel],10,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,,,
|
| 185 |
+
High-Impact Media,Tower Banner Interactive Shopper Targeted to [Insert Channel],,No,,Tower Banner Interactive Shopper Targeted to [Insert Channel],10,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,,,
|
| 186 |
+
High-Impact Media,Tower Banner Retail Connect Targeted to [Insert Channel],,No,,Tower Banner Retail Connect Targeted to [Insert Channel],9,12,12,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,,,
|
| 187 |
+
High-Impact Media,Tower Banner Scratch Off/Wipe Away Targeted to [Insert Channel],,No,,Tower Banner Scratch Off/Wipe Away Targeted to [Insert Channel],10,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,,,
|
| 188 |
+
High-Impact Media,Tower Banner Visualizer Targeted to [Insert Channel],,No,,Tower Banner Visualizer Targeted to [Insert Channel],10,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,,,
|
| 189 |
+
High-Impact Media,Video Ribbon Targeted to [Insert Channel],,No,,Video Ribbon Targeted to [Insert Channel],11,13,13,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,,,x,Yes,Yes,N
|
| 190 |
+
High-Impact Media,Skyline Targeted to [Insert Channel],,No,,Skyline Targeted to [Insert Channel],-,-,17,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,,,No,No,N
|
| 191 |
+
High-Impact Media,Skyline 360 View Targeted to [Insert Channel],,No,,Skyline 360 View Targeted to [Insert Channel],-,-,18,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,,,,,
|
| 192 |
+
High-Impact Media,Skyline Carousel Targeted to [Insert Channel],,No,,Skyline Carousel Targeted to [Insert Channel],-,-,17,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,,,,,
|
| 193 |
+
High-Impact Media,Skyline Countdown Targeted to [Insert Channel],,No,,Skyline Countdown Targeted to [Insert Channel],-,-,17,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,,,,,
|
| 194 |
+
High-Impact Media,Skyline Dynamic Creative Targeted to [Insert Channel],,No,,Skyline Dynamic Creative Targeted to [Insert Channel],-,-,17,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,,,,,
|
| 195 |
+
High-Impact Media,Skyline Glider Targeted to [Insert Channel],,No,,Skyline Glider Targeted to [Insert Channel],-,-,18,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,,,,,
|
| 196 |
+
High-Impact Media,Skyline Interactive Shopper Targeted to [Insert Channel],,No,,Skyline Interactive Shopper Targeted to [Insert Channel],-,-,18,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,,,,,
|
| 197 |
+
High-Impact Media,Skyline Retail Connect Targeted to [Insert Channel],,No,,Skyline Retail Connect Targeted to [Insert Channel],-,-,17,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,,,,,
|
| 198 |
+
High-Impact Media,Skyline Shoppable Video Targeted to [Insert Channel],,No,,Skyline Shoppable Video Targeted to [Insert Channel],-,-,19,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,,,,,
|
| 199 |
+
High-Impact Media,Skyline Video Targeted to [Insert Channel],,No,,Skyline Video Targeted to [Insert Channel],-,-,19,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,,,,,
|
| 200 |
+
High-Impact Media,Skyline Visualizer Targeted to [Insert Channel],,No,,Skyline Visualizer Targeted to [Insert Channel],-,-,18,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,,,,,
|
| 201 |
+
High-Impact Media Packages,High-Impact Package Targeted to [Insert Channel]_Package,,Yes,,,-,-,12.5,CPM,Raptive,PKG,Insert,Insert,See rate card,,,< 10%,,PKG,PKG,PKG,No,No,PKG
|
| 202 |
+
High-Impact Media Packages,Mural Targeted to [Insert Channel],,Yes,,,-,-,-,CPM,Raptive,1x1,-,-,-,-,-,-,,x,x,x,-,-,N
|
| 203 |
+
High-Impact Media Packages,Beyond Banner Targeted to [Insert Channel],,Yes,,,-,-,-,CPM,Raptive,1x1,-,-,-,-,-,-,,x,x,x,-,-,N
|
| 204 |
+
Native Units,Recipe Recommendation Ad Targeted to [Insert Channel],,No,,Recipe Recommendation Ad Targeted to [Insert Channel],6,9,9,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,Yes,Yes,N
|
| 205 |
+
Native Units,Content Recommendation Ad Targeted to [Insert Channel],,No,,Content Recommendation Ad Targeted to [Insert Channel],6,9,9,CPM,Raptive,1x1,Insert,Insert,See rate card,,,< 10%,,x,x,x,Yes,Yes,N
|
| 206 |
+
Video,Pre-Roll Video (²15 Sec) Targeted to [Insert Targeting Type],,No,,Pre-Roll Video (²15 Sec) Targeted to [Insert Targeting Type],12,14,14,CPM,Raptive,"16:9 Aspect Ratio, :06s, :15s",Insert,Insert,See rate card,,,< 10%,,x,x,x,Yes,Yes,N
|
| 207 |
+
Video,Pre-Roll Video (30 Sec) Targeted to [Insert Targeting Type],,No,,Pre-Roll Video (30 Sec) Targeted to [Insert Targeting Type],12,22,22,CPM,Raptive,"16:9 Aspect Ratio, :30s",Insert,Insert,See rate card,,,< 10%,,x,x,x,Yes,Yes,N
|
| 208 |
+
Video,Pre-Roll Video (Click to Play) Targeted to [Insert Targeting Type],,No,,Pre-Roll Video (Click to Play) Targeted to [Insert Targeting Type],20,22.5,22.5,CPM,Raptive,"16:9 Aspect Ratio, :06s, :15s, :30s",Insert,Insert,See rate card,,,< 10%,,x,x,x,,,
|
| 209 |
+
Video,Outstream Video Targeted to [Insert Targeting Type],,No,,Outstream Video Targeted to [Insert Targeting Type],5,9.5,-,CPM,Raptive,640x480 or 640x360 or 300x250,Insert,Insert,See rate card,,,< 10%,"Ads must be at least 256px in both width and height with the exception of 300x250, 16:9 or 4:3 aspect ratios",x,x,x,No,Yes,N
|
| 210 |
+
Video,YouTube Network Bumper Ad (06 Sec) Targeted to [Insert Targeting Type],,No,,YouTube Network Bumper Ad (06 Sec) Targeted to [Insert Targeting Type],-,12,12,CPM,Raptive,"16:9,9:16, 1:1, 4:3 or 2:3 Aspect Ratio, :06s",Insert,Insert,See rate card,,,< 10%,"Must be purchased with Raptive on-site pre-roll
|
| 211 |
+
|
| 212 |
+
Impressions are estimated. If actual impressions exceed or fall short of the goal, budget will be shifted between this line and the Raptive on-site preroll line.
|
| 213 |
+
|
| 214 |
+
YouTube does not accept VPAID tags. We can accept site-served video and a 1x1 OR 3P VAST tags.
|
| 215 |
+
|
| 216 |
+
Raptive cannot accept 3P tracking tags. Please provide the following IDs for Raptive to implement in order to enable tracking.
|
| 217 |
+
|
| 218 |
+
DoubleVerify:
|
| 219 |
+
+Viewability Client ID
|
| 220 |
+
+Viewability Reporting ID
|
| 221 |
+
|
| 222 |
+
Dynata, Intage (JP only), or Kantar:
|
| 223 |
+
+Publisher Viewability Client ID
|
| 224 |
+
+Publisher Viewability Reporting ID
|
| 225 |
+
|
| 226 |
+
Raptive can also accept the ID's from the following partners:
|
| 227 |
+
|
| 228 |
+
+Viewability: MOAT, IAS
|
| 229 |
+
+Reach: comScore, Nielsen, VideoAmp (US only), iSpotTV (US only), Video Research (JP only), Gemius SA (DE only), AudienceProject (UK)
|
| 230 |
+
+Other vendors integrated with Ads Data Hub: Adform, AdRiver, AudienceProject, C3 Metrics, Extreme Reach, Exactag, Flashtalking, Innovid, Weborama
|
| 231 |
+
|
| 232 |
+
Pixels allowed:
|
| 233 |
+
+Google (including Campaign Manager 360)",x,x,x,"Yes
|
| 234 |
+
|
| 235 |
+
DV360 only",No,N
|
| 236 |
+
Video,YouTube Network Non-Skippable Video (15 Sec) Targeted to [Insert Targeting Type],,No,,YouTube Network Non-Skippable Video (15 Sec) Targeted to [Insert Targeting Type],-,23,23,CPM,Raptive,"16:9,9:16, 1:1, 4:3 or 2:3 Aspect Ratio, :15s",Insert,Insert,See rate card,,,< 10%,"Must be purchased with Raptive on-site pre-roll
|
| 237 |
+
|
| 238 |
+
Impressions are estimated. If actual impressions exceed or fall short of the goal, budget will be shifted between this line and the Raptive on-site preroll line.
|
| 239 |
+
|
| 240 |
+
YouTube does not accept VPAID tags. We can accept site-served video and a 1x1 OR 3P VAST tags.
|
| 241 |
+
|
| 242 |
+
Raptive cannot accept 3P tracking tags. Please provide the following IDs for Raptive to implement in order to enable tracking.
|
| 243 |
+
|
| 244 |
+
DoubleVerify:
|
| 245 |
+
+Viewability Client ID
|
| 246 |
+
+Viewability Reporting ID
|
| 247 |
+
|
| 248 |
+
Dynata, Intage (JP only), or Kantar:
|
| 249 |
+
+Publisher Viewability Client ID
|
| 250 |
+
+Publisher Viewability Reporting ID
|
| 251 |
+
|
| 252 |
+
Raptive can also accept the ID's from the following partners:
|
| 253 |
+
|
| 254 |
+
+Viewability: MOAT, IAS
|
| 255 |
+
+Reach: comScore, Nielsen, VideoAmp (US only), iSpotTV (US only), Video Research (JP only), Gemius SA (DE only), AudienceProject (UK)
|
| 256 |
+
+Other vendors integrated with Ads Data Hub: Adform, AdRiver, AudienceProject, C3 Metrics, Extreme Reach, Exactag, Flashtalking, Innovid, Weborama
|
| 257 |
+
|
| 258 |
+
Pixels allowed:
|
| 259 |
+
+Google (including Campaign Manager 360)",x,x,x,"Yes
|
| 260 |
+
|
| 261 |
+
DV360 only",No,N
|
| 262 |
+
Video,"YouTube Network Skippable Video (30 Sec+, Skippable at 05 Sec) Targeted to [Insert Targeting Type]",,No,,"YouTube Network Skippable Video (30 Sec+, Skippable at 05 Sec) Targeted to [Insert Targeting Type]",-,18,18,CPM,Raptive,"16:9,9:16, 1:1, 4:3 or 2:3 Aspect Ratio, :30s+",Insert,Insert,See rate card,,,< 10%,"Must be purchased with Raptive on-site pre-roll
|
| 263 |
+
|
| 264 |
+
Impressions are estimated. If actual impressions exceed or fall short of the goal, budget will be shifted between this line and the Raptive on-site preroll line.
|
| 265 |
+
|
| 266 |
+
YouTube does not accept VPAID tags. We can accept site-served video and a 1x1 OR 3P VAST tags.
|
| 267 |
+
|
| 268 |
+
Raptive cannot accept 3P tracking tags. Please provide the following IDs for Raptive to implement in order to enable tracking.
|
| 269 |
+
|
| 270 |
+
DoubleVerify:
|
| 271 |
+
+Viewability Client ID
|
| 272 |
+
+Viewability Reporting ID
|
| 273 |
+
|
| 274 |
+
Dynata, Intage (JP only), or Kantar:
|
| 275 |
+
+Publisher Viewability Client ID
|
| 276 |
+
+Publisher Viewability Reporting ID
|
| 277 |
+
|
| 278 |
+
Raptive can also accept the ID's from the following partners:
|
| 279 |
+
|
| 280 |
+
+Viewability: MOAT, IAS
|
| 281 |
+
+Reach: comScore, Nielsen, VideoAmp (US only), iSpotTV (US only), Video Research (JP only), Gemius SA (DE only), AudienceProject (UK)
|
| 282 |
+
+Other vendors integrated with Ads Data Hub: Adform, AdRiver, AudienceProject, C3 Metrics, Extreme Reach, Exactag, Flashtalking, Innovid, Weborama
|
| 283 |
+
|
| 284 |
+
Pixels allowed:
|
| 285 |
+
+Google (including Campaign Manager 360)",x,x,x,"Yes
|
| 286 |
+
|
| 287 |
+
DV360 only",No,N
|
| 288 |
+
Targeted Media,[Insert Targeting Type] Media_Package,,Yes,,,4,7,7,CPM,Raptive,PKG,Insert,Insert,See rate card,,,< 10%,,PKG,PKG,PKG,Yes,Yes,PKG
|
| 289 |
+
Targeted Media,[Insert Targeting Type] Media_Leaderboard,,Yes,Leaderboard,,-,-,-,-,Raptive,728x90,-,-,-,-,-,-,,x,,x,-,-,Y
|
| 290 |
+
Targeted Media,[Insert Targeting Type] Media_Medium Rectangle,,Yes,Medium Rectangle,,-,-,-,-,Raptive,300x250,-,-,-,-,-,-,,x,x,x,-,-,Y
|
| 291 |
+
Targeted Media,[Insert Targeting Type] Media_Mobile Banner,,Yes,Mobile Banner,,-,-,-,-,Raptive,320x50,-,-,-,-,-,-,,,,x,-,-,Y
|
| 292 |
+
Added Value,AV Media [Insert Targeting Type] _Package,,Yes,,,0,0,0,Added Value,Raptive,PKG,Insert,Insert,Added Value,,$0,< 10%,"Value = $x,xxx",PKG,PKG,PKG,No,No,PKG
|
| 293 |
+
Added Value,AV Media [Insert Targeting Type] _Leaderboard,,Yes,Leaderboard,,-,-,-,-,Raptive,728x90,-,-,-,-,-,-,,x,,x,-,-,Y
|
| 294 |
+
Added Value,AV Media [Insert Targeting Type] _Medium Rectangle,,Yes,Medium Rectangle,,-,-,-,-,Raptive,300x250,-,-,-,-,-,-,,x,x,x,-,-,Y
|
| 295 |
+
Added Value,AV Media [Insert Targeting Type] _Mobile Banner,,Yes,Mobile Banner,,-,-,-,-,Raptive,320x50,-,-,-,-,-,-,,,,x,-,-,Y
|
| 296 |
+
Added Value,AV [Insert Name] Study,,No,,AV [Insert Name] Study,0,0,0,Added Value,Raptive,n/a,Insert,Insert,Added Value,n/a,$0,n/a,"Value = $x,xxx
|
| 297 |
+
|
| 298 |
+
Once study starts, entire program becomes non-cancellable",n/a,n/a,n/a,TBD,TBD,n/a
|
app.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import streamlit as st
|
| 5 |
+
import snowflake.connector
|
| 6 |
+
from cryptography.hazmat.primitives import serialization
|
| 7 |
+
|
| 8 |
+
from comscore_site_list import render as render_comscore_site_list
|
| 9 |
+
from media_plan_templater import render as render_media_plan_templater
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
st.set_page_config(layout="wide", page_title="Sales Toolkit", page_icon="📊")
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
# ─── Snowflake Helpers ───────────────────────────────────────
|
| 16 |
+
def get_conn():
|
| 17 |
+
raw_key = os.getenv("snowflake_private_key")
|
| 18 |
+
priv_key = serialization.load_pem_private_key(raw_key.encode(), password=None)
|
| 19 |
+
return snowflake.connector.connect(
|
| 20 |
+
user=os.getenv("snowflake_user"),
|
| 21 |
+
account=os.getenv("snowflake_account_identifier"),
|
| 22 |
+
private_key=priv_key,
|
| 23 |
+
role=os.getenv("snowflake_role"),
|
| 24 |
+
warehouse=os.getenv("snowflake_warehouse"),
|
| 25 |
+
database=os.getenv("snowflake_database"),
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@st.cache_data
|
| 30 |
+
def get_lookup_table():
|
| 31 |
+
conn = get_conn()
|
| 32 |
+
cs = conn.cursor()
|
| 33 |
+
database = os.getenv("snowflake_database") # e.g. ANALYTICS
|
| 34 |
+
schema = "SIGMA_SCRATCH"
|
| 35 |
+
view = "VIEW_NEW_DATASET_FROM_SQL_3B3605DCE9F84DBC8F8508F33C4364FF"
|
| 36 |
+
|
| 37 |
+
cs.execute(
|
| 38 |
+
f"""
|
| 39 |
+
SELECT
|
| 40 |
+
"ENTITY" AS entity,
|
| 41 |
+
"SITE ID" AS site_id,
|
| 42 |
+
"SITE NAME" AS site_name,
|
| 43 |
+
"STATUS" AS status,
|
| 44 |
+
"AD OPTIONS" AS ad_options
|
| 45 |
+
FROM {database}.{schema}.{view}
|
| 46 |
+
"""
|
| 47 |
+
)
|
| 48 |
+
cols = [d[0].lower() for d in cs.description]
|
| 49 |
+
rows = cs.fetchall()
|
| 50 |
+
cs.close()
|
| 51 |
+
conn.close()
|
| 52 |
+
return pd.DataFrame(rows, columns=cols)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def extract_pmp(ad_opts):
|
| 56 |
+
"""Parse the JSON in `ad_options` and return the top-level 'pmp' boolean."""
|
| 57 |
+
try:
|
| 58 |
+
obj = json.loads(ad_opts)
|
| 59 |
+
return obj.get("pmp")
|
| 60 |
+
except Exception:
|
| 61 |
+
return None
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
# ─── App Layout ───────────────────────────────────────────────
|
| 65 |
+
st.title("Sales Toolkit")
|
| 66 |
+
tab1, tab2 = st.tabs(["📊 Comscore Site List", "Media Plan Templater"])
|
| 67 |
+
|
| 68 |
+
with tab1:
|
| 69 |
+
render_comscore_site_list(get_conn, get_lookup_table, extract_pmp)
|
| 70 |
+
|
| 71 |
+
with tab2:
|
| 72 |
+
render_media_plan_templater(get_conn)
|
changelog.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Changelog
|
| 2 |
+
|
| 3 |
+
- 2025-08-07 14:28 UTC: Initialized changelog to track project updates.
|
| 4 |
+
- 2025-08-09 00:00 UTC: Explode bulleted Details in Media Plan Templater for package Products.
|
| 5 |
+
- 2025-08-07 18:44 UTC: Display full placement names in Details column of Media Plan Templater.
|
| 6 |
+
- 2025-08-08 13:31 UTC: Deduplicate package rows in Media Plan Templater.
|
| 7 |
+
- 2025-08-11 17:27 UTC: Split tabs into separate modules for easier maintenance.
|
| 8 |
+
- 2025-08-13 20:35 UTC: Reload Excel data when switching sheets in Media Plan Templater.
|
| 9 |
+
- 2025-08-13 21:10 UTC: Unmerge target columns to prevent 'MergedCell' Excel download errors.
|
comscore_site_list.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import csv
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import streamlit as st
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def render(get_conn, get_lookup_table, extract_pmp):
|
| 7 |
+
st.header("📊 Comscore Site List")
|
| 8 |
+
|
| 9 |
+
# 1) Composition Index Threshold input
|
| 10 |
+
threshold = st.number_input(
|
| 11 |
+
"Composition Index Threshold",
|
| 12 |
+
min_value=0.0,
|
| 13 |
+
step=1.0,
|
| 14 |
+
value=150.0,
|
| 15 |
+
help="Show only rows where Composition Index UV ≥ this value",
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
# 2) Exclude Codes selector
|
| 19 |
+
# ── Fetch all distinct Codes for the dropdown
|
| 20 |
+
conn_codes = get_conn()
|
| 21 |
+
cs_codes = conn_codes.cursor()
|
| 22 |
+
cs_codes.execute(
|
| 23 |
+
"""
|
| 24 |
+
SELECT DISTINCT Codes
|
| 25 |
+
FROM ANALYTICS.SIGMA_SCRATCH.VIEW_GAMLOG_AGG_SITE_CODE_VOLUME_188E5F89A6FD42C7994D0F012A2EECD5
|
| 26 |
+
ORDER BY Codes
|
| 27 |
+
"""
|
| 28 |
+
)
|
| 29 |
+
all_codes = [row[0] for row in cs_codes.fetchall()]
|
| 30 |
+
cs_codes.close()
|
| 31 |
+
conn_codes.close()
|
| 32 |
+
|
| 33 |
+
exclude_codes = st.multiselect(
|
| 34 |
+
"Exclude Codes",
|
| 35 |
+
options=all_codes,
|
| 36 |
+
default=["bsopt_23"] if "bsopt_23" in all_codes else [],
|
| 37 |
+
help="Select Codes whose SITEIDs you want to filter out of the main table",
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
# 3) File uploader
|
| 41 |
+
upload = st.file_uploader(
|
| 42 |
+
"Upload your Comscore CSV", type="csv", key="comscore_uploader"
|
| 43 |
+
)
|
| 44 |
+
if not upload:
|
| 45 |
+
st.info("Please upload your file from Comscore.")
|
| 46 |
+
else:
|
| 47 |
+
try:
|
| 48 |
+
# ─── Parse the raw into rows ───────────────────────────────────────
|
| 49 |
+
raw_text = upload.read().decode("latin-1")
|
| 50 |
+
lines = raw_text.splitlines()
|
| 51 |
+
reader = csv.reader(lines, skipinitialspace=True)
|
| 52 |
+
rows = list(reader)
|
| 53 |
+
|
| 54 |
+
# ─── Locate your three headers anywhere in the first N lines ───────
|
| 55 |
+
required = ["Media", "Target Audience (000)", "Composition Index UV"]
|
| 56 |
+
header_pos = {}
|
| 57 |
+
for i, row in enumerate(rows[:30]):
|
| 58 |
+
cleaned = [cell.strip() for cell in row]
|
| 59 |
+
for col in required:
|
| 60 |
+
if col not in header_pos and col in cleaned:
|
| 61 |
+
header_pos[col] = i
|
| 62 |
+
if len(header_pos) == len(required):
|
| 63 |
+
break
|
| 64 |
+
|
| 65 |
+
missing = [c for c in required if c not in header_pos]
|
| 66 |
+
if missing:
|
| 67 |
+
st.error(f"Missing required column(s): {', '.join(missing)}")
|
| 68 |
+
st.stop()
|
| 69 |
+
|
| 70 |
+
# map header names → column indices
|
| 71 |
+
idx_map = {}
|
| 72 |
+
for col, prow in header_pos.items():
|
| 73 |
+
cleaned = [cell.strip() for cell in rows[prow]]
|
| 74 |
+
idx_map[col] = cleaned.index(col)
|
| 75 |
+
|
| 76 |
+
# data starts right after the bottom header row
|
| 77 |
+
data_start = max(header_pos.values()) + 1
|
| 78 |
+
data_rows = rows[data_start:]
|
| 79 |
+
|
| 80 |
+
# pull out exactly those three columns
|
| 81 |
+
data = []
|
| 82 |
+
for row in data_rows:
|
| 83 |
+
if not row:
|
| 84 |
+
continue
|
| 85 |
+
vals = []
|
| 86 |
+
for col in required:
|
| 87 |
+
idx = idx_map[col]
|
| 88 |
+
vals.append(row[idx].strip() if idx < len(row) else None)
|
| 89 |
+
data.append(vals)
|
| 90 |
+
|
| 91 |
+
# build dataframe & coerce types
|
| 92 |
+
df_clean = pd.DataFrame(data, columns=required)
|
| 93 |
+
df_clean["Target Audience (000)"] = pd.to_numeric(
|
| 94 |
+
df_clean["Target Audience (000)"], errors="coerce"
|
| 95 |
+
)
|
| 96 |
+
df_clean["Composition Index UV"] = pd.to_numeric(
|
| 97 |
+
df_clean["Composition Index UV"], errors="coerce"
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
# ─── ORIGINAL Snowflake lookup & merge ───────────────────────────
|
| 101 |
+
df_lookup = (
|
| 102 |
+
get_lookup_table()
|
| 103 |
+
) # DISCUSSION: this still uses your SIGMA_SCRATCH view
|
| 104 |
+
lookup_small = df_lookup[
|
| 105 |
+
["entity", "site_id", "site_name", "status", "ad_options"]
|
| 106 |
+
]
|
| 107 |
+
df_merged = pd.merge(
|
| 108 |
+
df_clean,
|
| 109 |
+
lookup_small,
|
| 110 |
+
how="left",
|
| 111 |
+
left_on="Media",
|
| 112 |
+
right_on="entity",
|
| 113 |
+
).drop(columns=["entity"])
|
| 114 |
+
df_merged["pmp"] = (
|
| 115 |
+
df_merged["ad_options"]
|
| 116 |
+
.astype(str)
|
| 117 |
+
.apply(extract_pmp)
|
| 118 |
+
.map({True: "Yes", False: "No"})
|
| 119 |
+
.fillna("No")
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
# ─── NEW: fetch SITEIDs to exclude based on Codes ────────────────
|
| 123 |
+
if exclude_codes:
|
| 124 |
+
conn_ex = get_conn()
|
| 125 |
+
cs_ex = conn_ex.cursor()
|
| 126 |
+
quoted = ", ".join(f"'{c}'" for c in exclude_codes)
|
| 127 |
+
cs_ex.execute(
|
| 128 |
+
f"""
|
| 129 |
+
SELECT DISTINCT SITEID
|
| 130 |
+
FROM ANALYTICS.SIGMA_SCRATCH.VIEW_GAMLOG_AGG_SITE_CODE_VOLUME_188E5F89A6FD42C7994D0F012A2EECD5
|
| 131 |
+
WHERE Codes IN ({quoted})
|
| 132 |
+
"""
|
| 133 |
+
)
|
| 134 |
+
excluded_site_ids = {row[0] for row in cs_ex.fetchall()}
|
| 135 |
+
cs_ex.close()
|
| 136 |
+
conn_ex.close()
|
| 137 |
+
else:
|
| 138 |
+
excluded_site_ids = set()
|
| 139 |
+
|
| 140 |
+
# ─── Combined filter ─────────────────────────────────────────────
|
| 141 |
+
df_filtered = df_merged[
|
| 142 |
+
(df_merged["Composition Index UV"] >= threshold)
|
| 143 |
+
& (df_merged["site_name"].notna())
|
| 144 |
+
& (df_merged["status"] != "Dropped")
|
| 145 |
+
& (df_merged["pmp"] == "Yes")
|
| 146 |
+
& (~df_merged["site_id"].isin(excluded_site_ids))
|
| 147 |
+
]
|
| 148 |
+
|
| 149 |
+
# ─── drop extras, sort, display & export ──────────────────────────
|
| 150 |
+
df_display = (
|
| 151 |
+
df_filtered.drop(columns=["ad_options"])
|
| 152 |
+
.sort_values("Target Audience (000)", ascending=False)
|
| 153 |
+
.reset_index(drop=True)
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
if df_display.empty:
|
| 157 |
+
st.warning("No rows meet the criteria.")
|
| 158 |
+
else:
|
| 159 |
+
st.success(f"Showing {len(df_display)} rows after filtering")
|
| 160 |
+
st.dataframe(df_display)
|
| 161 |
+
|
| 162 |
+
# Export – GAM
|
| 163 |
+
site_ids = df_display["site_id"].dropna().astype(str)
|
| 164 |
+
gam_str = ",".join(site_ids)
|
| 165 |
+
gam_df = pd.DataFrame({"site_ids": [gam_str]})
|
| 166 |
+
gam_csv = gam_df.to_csv(index=False, header=False)
|
| 167 |
+
st.download_button(
|
| 168 |
+
label="Export - GAM",
|
| 169 |
+
data=gam_csv,
|
| 170 |
+
file_name="gam_export.csv",
|
| 171 |
+
mime="text/csv",
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
# Export – Client Facing
|
| 175 |
+
client_df = df_display[["site_name", "Media"]].rename(
|
| 176 |
+
columns={"Media": "Domain"}
|
| 177 |
+
)
|
| 178 |
+
client_csv = client_df.to_csv(index=False)
|
| 179 |
+
st.download_button(
|
| 180 |
+
label="Export - Client Facing",
|
| 181 |
+
data=client_csv,
|
| 182 |
+
file_name="client_facing_export.csv",
|
| 183 |
+
mime="text/csv",
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
# Export – Site Names
|
| 187 |
+
site_names = df_display["site_name"].dropna().astype(str)
|
| 188 |
+
names_str = ",".join(site_names)
|
| 189 |
+
names_df = pd.DataFrame({"site_names": [names_str]})
|
| 190 |
+
names_csv = names_df.to_csv(index=False, header=False)
|
| 191 |
+
st.download_button(
|
| 192 |
+
label="Export - Site Names",
|
| 193 |
+
data=names_csv,
|
| 194 |
+
file_name="site_names_export.csv",
|
| 195 |
+
mime="text/csv",
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
except Exception as e:
|
| 199 |
+
st.error(f"Could not parse, merge, or export file: {e}")
|
.gitattributes → gitattributes
RENAMED
|
File without changes
|
index.html
DELETED
|
@@ -1,19 +0,0 @@
|
|
| 1 |
-
<!doctype html>
|
| 2 |
-
<html>
|
| 3 |
-
<head>
|
| 4 |
-
<meta charset="utf-8" />
|
| 5 |
-
<meta name="viewport" content="width=device-width" />
|
| 6 |
-
<title>My static Space</title>
|
| 7 |
-
<link rel="stylesheet" href="style.css" />
|
| 8 |
-
</head>
|
| 9 |
-
<body>
|
| 10 |
-
<div class="card">
|
| 11 |
-
<h1>Welcome to your static Space!</h1>
|
| 12 |
-
<p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
|
| 13 |
-
<p>
|
| 14 |
-
Also don't forget to check the
|
| 15 |
-
<a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
|
| 16 |
-
</p>
|
| 17 |
-
</div>
|
| 18 |
-
</body>
|
| 19 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
media_plan_templater.py
ADDED
|
@@ -0,0 +1,633 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import io
|
| 2 |
+
import os
|
| 3 |
+
import re
|
| 4 |
+
import json
|
| 5 |
+
import hashlib
|
| 6 |
+
import numpy as np
|
| 7 |
+
import pandas as pd
|
| 8 |
+
import streamlit as st
|
| 9 |
+
from docx import Document
|
| 10 |
+
from openpyxl import Workbook, load_workbook
|
| 11 |
+
from openpyxl.styles import numbers
|
| 12 |
+
from datetime import date, timedelta
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def render(get_conn):
|
| 16 |
+
st.header("📊 Media Plan Templater")
|
| 17 |
+
|
| 18 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 19 |
+
# 1) Upload and process Word document
|
| 20 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 21 |
+
upload_doc = st.file_uploader(
|
| 22 |
+
"1. Upload Word document (.docx) with media placements table",
|
| 23 |
+
type=["docx"],
|
| 24 |
+
key="media_plan_docx_uploader",
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
if "df_word" not in st.session_state:
|
| 28 |
+
if not upload_doc:
|
| 29 |
+
st.info(
|
| 30 |
+
"Please upload a Word (.docx) file to extract the media placements table."
|
| 31 |
+
)
|
| 32 |
+
st.stop()
|
| 33 |
+
|
| 34 |
+
# Parse Word tables → DataFrame
|
| 35 |
+
doc = Document(io.BytesIO(upload_doc.getvalue()))
|
| 36 |
+
dfs = []
|
| 37 |
+
for table in doc.tables:
|
| 38 |
+
rows = [[cell.text.strip() for cell in row.cells] for row in table.rows]
|
| 39 |
+
if len(rows) < 2:
|
| 40 |
+
continue
|
| 41 |
+
header, *data = rows
|
| 42 |
+
dfs.append(pd.DataFrame(data, columns=header))
|
| 43 |
+
|
| 44 |
+
if not dfs:
|
| 45 |
+
st.error("No valid tables found in the Word document.")
|
| 46 |
+
st.stop()
|
| 47 |
+
|
| 48 |
+
df_word = pd.concat(dfs, ignore_index=True)
|
| 49 |
+
df_word.columns = df_word.columns.str.strip()
|
| 50 |
+
|
| 51 |
+
# Normalize Details: remove newlines → split bullets → explode
|
| 52 |
+
if "Details" in df_word.columns:
|
| 53 |
+
df_word["Details"] = (
|
| 54 |
+
df_word["Details"].astype(str).str.replace("\n", " ").str.strip()
|
| 55 |
+
)
|
| 56 |
+
df_word["Details"] = (
|
| 57 |
+
df_word["Details"]
|
| 58 |
+
.fillna("")
|
| 59 |
+
.str.split("•")
|
| 60 |
+
.apply(lambda parts: [p.strip() for p in parts if str(p).strip()])
|
| 61 |
+
)
|
| 62 |
+
df_word = df_word.explode("Details")
|
| 63 |
+
else:
|
| 64 |
+
df_word["Details"] = ""
|
| 65 |
+
|
| 66 |
+
# Ensure required columns exist
|
| 67 |
+
for col in [
|
| 68 |
+
"Media Plan",
|
| 69 |
+
"Budget",
|
| 70 |
+
"Product",
|
| 71 |
+
"Targeting",
|
| 72 |
+
"Notes",
|
| 73 |
+
"Rate",
|
| 74 |
+
"Rate_Type",
|
| 75 |
+
"Ad Size",
|
| 76 |
+
]:
|
| 77 |
+
if col not in df_word.columns:
|
| 78 |
+
df_word[col] = ""
|
| 79 |
+
|
| 80 |
+
# ── Pull rate card from Snowflake
|
| 81 |
+
conn = get_conn()
|
| 82 |
+
cs = conn.cursor()
|
| 83 |
+
cs.execute(
|
| 84 |
+
"""
|
| 85 |
+
SELECT
|
| 86 |
+
Package AS Product,
|
| 87 |
+
Placement,
|
| 88 |
+
PLACEMENT_EXTRACTION,
|
| 89 |
+
RATE_CARD_LOOKUP_Concat,
|
| 90 |
+
Rate_Type,
|
| 91 |
+
Ad_Size,
|
| 92 |
+
PMP_Rate,
|
| 93 |
+
PG_Rate,
|
| 94 |
+
Direct_IO_Rate
|
| 95 |
+
FROM ANALYTICS.SIGMA_SCRATCH.UNIVERSAL_MEDIA_PLAN_WITH_RATE_CARD_VALUES
|
| 96 |
+
"""
|
| 97 |
+
)
|
| 98 |
+
rows, cols = cs.fetchall(), [d[0] for d in cs.description]
|
| 99 |
+
cs.close()
|
| 100 |
+
conn.close()
|
| 101 |
+
|
| 102 |
+
df_rate = pd.DataFrame(rows, columns=cols)
|
| 103 |
+
df_rate.columns = [c.strip().upper() for c in df_rate.columns]
|
| 104 |
+
df_rate = df_rate.rename(
|
| 105 |
+
columns={
|
| 106 |
+
"PRODUCT": "Product",
|
| 107 |
+
"PLACEMENT": "Placement",
|
| 108 |
+
"PLACEMENT_EXTRACTION": "Details_key",
|
| 109 |
+
"RATE_CARD_LOOKUP_CONCAT": "LookupKey_sql",
|
| 110 |
+
"RATE_TYPE": "Rate_Type",
|
| 111 |
+
"AD_SIZE": "Ad Size",
|
| 112 |
+
"PMP_RATE": "PMP_Rate",
|
| 113 |
+
"PG_RATE": "PG_Rate",
|
| 114 |
+
"DIRECT_IO_RATE": "Direct_IO_Rate",
|
| 115 |
+
}
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
# Build exact concat from Word doc
|
| 119 |
+
df_word["Details_fixed"] = df_word["Details"].astype(str)
|
| 120 |
+
mask_pkg = df_word["Details_fixed"].str.strip().str.lower().eq("_package")
|
| 121 |
+
df_word.loc[mask_pkg, "Details_fixed"] = "Package"
|
| 122 |
+
if "Product" not in df_word.columns:
|
| 123 |
+
df_word["Product"] = ""
|
| 124 |
+
|
| 125 |
+
df_word["LookupKey_word"] = (
|
| 126 |
+
df_word["Product"].astype(str) + " - " + df_word["Details_fixed"].astype(str)
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
# Merge on LookupKey
|
| 130 |
+
df_word = df_word.merge(
|
| 131 |
+
df_rate,
|
| 132 |
+
left_on="LookupKey_word",
|
| 133 |
+
right_on="LookupKey_sql",
|
| 134 |
+
how="left",
|
| 135 |
+
suffixes=("", "_sql"),
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
# Prefer SQL-standardized fields when present
|
| 139 |
+
def _pick_sql_fallback(df, base_col):
|
| 140 |
+
sql_col = f"{base_col}_sql"
|
| 141 |
+
if sql_col in df.columns:
|
| 142 |
+
df[base_col] = np.where(
|
| 143 |
+
df[sql_col].notna() & (df[sql_col].astype(str).str.strip() != ""),
|
| 144 |
+
df[sql_col],
|
| 145 |
+
df[base_col],
|
| 146 |
+
)
|
| 147 |
+
return df
|
| 148 |
+
|
| 149 |
+
df_word = _pick_sql_fallback(df_word, "Ad Size")
|
| 150 |
+
df_word = _pick_sql_fallback(df_word, "Rate_Type")
|
| 151 |
+
df_word = _pick_sql_fallback(df_word, "Product")
|
| 152 |
+
|
| 153 |
+
# Remove now-redundant columns
|
| 154 |
+
df_word = df_word.drop(
|
| 155 |
+
columns=["Ad Size_sql", "Rate_Type_sql", "Product_sql"], errors=True
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
# Display: prefer SQL concat (Product - PLACEMENT_EXTRACTION) if matched
|
| 159 |
+
df_word["Details"] = np.where(
|
| 160 |
+
df_word["Placement"].notna(),
|
| 161 |
+
df_word["LookupKey_sql"],
|
| 162 |
+
df_word["LookupKey_word"],
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
# Rate_Value chosen ONLY from Word 'Rate' column
|
| 166 |
+
if "Rate" not in df_word.columns:
|
| 167 |
+
df_word["Rate"] = ""
|
| 168 |
+
|
| 169 |
+
def pick_rate(r):
|
| 170 |
+
key = (r.get("Rate") or "").strip().lower()
|
| 171 |
+
if "pmp" in key:
|
| 172 |
+
return r.get("PMP_Rate")
|
| 173 |
+
if "pg" in key or "programmatic guaranteed" in key or "programmatic-guaranteed" in key:
|
| 174 |
+
return r.get("PG_Rate")
|
| 175 |
+
if "direct io" in key or "direct-io" in key or key == "direct" or key.startswith("direct"):
|
| 176 |
+
return r.get("Direct_IO_Rate")
|
| 177 |
+
return None
|
| 178 |
+
|
| 179 |
+
df_word["Rate_Value"] = df_word.apply(pick_rate, axis=1)
|
| 180 |
+
|
| 181 |
+
# Calculate Impressions = (Budget / Rate_Value) * 1000
|
| 182 |
+
budget = pd.to_numeric(df_word["Budget"], errors="coerce")
|
| 183 |
+
rate = pd.to_numeric(df_word["Rate_Value"], errors="coerce")
|
| 184 |
+
df_word["Budget"] = budget.round(2)
|
| 185 |
+
df_word["Rate_Value"] = rate.round(2)
|
| 186 |
+
df_word["Impressions"] = np.where(rate > 0, (budget / rate) * 1000, np.nan)
|
| 187 |
+
df_word["Impressions"] = df_word["Impressions"].round()
|
| 188 |
+
|
| 189 |
+
# Ensure true numeric dtypes
|
| 190 |
+
df_word["Budget"] = pd.to_numeric(df_word["Budget"], errors="coerce").astype(float)
|
| 191 |
+
df_word["Rate_Value"] = pd.to_numeric(df_word["Rate_Value"], errors="coerce").astype(float)
|
| 192 |
+
# keep Impressions as float for editor (avoids %d errors on NaN), cast to int on export
|
| 193 |
+
df_word["Impressions"] = pd.to_numeric(df_word["Impressions"], errors="coerce").astype(float)
|
| 194 |
+
|
| 195 |
+
# Tidy df_word exposed to mapping
|
| 196 |
+
df_word = df_word.drop(
|
| 197 |
+
columns=[
|
| 198 |
+
"PMP_Rate",
|
| 199 |
+
"PG_Rate",
|
| 200 |
+
"Direct_IO_Rate",
|
| 201 |
+
"Details_key",
|
| 202 |
+
"Details_fixed",
|
| 203 |
+
"LookupKey_word",
|
| 204 |
+
"LookupKey_sql",
|
| 205 |
+
"Placement",
|
| 206 |
+
],
|
| 207 |
+
errors=True,
|
| 208 |
+
)
|
| 209 |
+
|
| 210 |
+
final_cols = [
|
| 211 |
+
"Media Plan",
|
| 212 |
+
"Budget",
|
| 213 |
+
"Product",
|
| 214 |
+
"Details",
|
| 215 |
+
"Targeting",
|
| 216 |
+
"Notes",
|
| 217 |
+
"Rate_Value",
|
| 218 |
+
"Rate_Type",
|
| 219 |
+
"Ad Size",
|
| 220 |
+
"Impressions",
|
| 221 |
+
]
|
| 222 |
+
remaining = [c for c in df_word.columns if c not in final_cols]
|
| 223 |
+
df_word = df_word[final_cols + remaining].drop_duplicates().reset_index(drop=True)
|
| 224 |
+
|
| 225 |
+
st.session_state["df_word"] = df_word
|
| 226 |
+
|
| 227 |
+
df_word = st.session_state["df_word"]
|
| 228 |
+
|
| 229 |
+
# Preview (Word doc table) — show $ with commas and no decimals for Budget,
|
| 230 |
+
# $ with 2 decimals for Rate_Value, and comma-separated Impressions.
|
| 231 |
+
st.subheader("Extracted table from Word document")
|
| 232 |
+
|
| 233 |
+
df_word_preview = df_word.copy()
|
| 234 |
+
|
| 235 |
+
def _fmt_money0(x):
|
| 236 |
+
return "" if pd.isna(x) else f"${x:,.0f}"
|
| 237 |
+
|
| 238 |
+
def _fmt_money2(x):
|
| 239 |
+
return "" if pd.isna(x) else f"${x:,.2f}"
|
| 240 |
+
|
| 241 |
+
def _fmt_int(x):
|
| 242 |
+
return "" if pd.isna(x) else f"{int(round(float(x))):,}"
|
| 243 |
+
|
| 244 |
+
df_word_preview["Budget"] = df_word["Budget"].map(_fmt_money0)
|
| 245 |
+
df_word_preview["Rate_Value"] = df_word["Rate_Value"].map(_fmt_money2)
|
| 246 |
+
df_word_preview["Impressions"] = df_word["Impressions"].map(_fmt_int)
|
| 247 |
+
|
| 248 |
+
editor = st.data_editor if hasattr(st, "data_editor") else st.experimental_data_editor
|
| 249 |
+
_ = editor(
|
| 250 |
+
df_word_preview,
|
| 251 |
+
num_rows="dynamic",
|
| 252 |
+
use_container_width=True,
|
| 253 |
+
disabled=True, # read-only preview so numeric df_word stays intact
|
| 254 |
+
)
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 258 |
+
# 2) Upload and load template (Excel/CSV)
|
| 259 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 260 |
+
upload_plan = st.file_uploader(
|
| 261 |
+
"2. Upload Media Plan template (.csv, .xls, .xlsx)",
|
| 262 |
+
type=["csv", "xls", "xlsx"],
|
| 263 |
+
key="media_plan_template_uploader",
|
| 264 |
+
)
|
| 265 |
+
if "plan_bytes" not in st.session_state:
|
| 266 |
+
if not upload_plan:
|
| 267 |
+
st.info("Please upload a CSV or Excel file to load the media plan template.")
|
| 268 |
+
st.stop()
|
| 269 |
+
st.session_state["plan_bytes"] = upload_plan.getvalue()
|
| 270 |
+
st.session_state["plan_ext"] = os.path.splitext(upload_plan.name)[1].lower()
|
| 271 |
+
st.session_state["plan_name"] = upload_plan.name
|
| 272 |
+
|
| 273 |
+
plan_bytes = st.session_state["plan_bytes"]
|
| 274 |
+
plan_ext = st.session_state["plan_ext"]
|
| 275 |
+
plan_name = st.session_state.get("plan_name", "template.xlsx")
|
| 276 |
+
|
| 277 |
+
if plan_ext in [".xls", ".xlsx"]:
|
| 278 |
+
xls = pd.ExcelFile(io.BytesIO(plan_bytes), engine="openpyxl")
|
| 279 |
+
sheet = st.selectbox("Select sheet to load from", xls.sheet_names, key="sheet")
|
| 280 |
+
else:
|
| 281 |
+
sheet = None
|
| 282 |
+
|
| 283 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 284 |
+
# 3) Detect header & keep ALL headered columns
|
| 285 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 286 |
+
if "df_slice" not in st.session_state:
|
| 287 |
+
if plan_ext in [".xls", ".xlsx"]:
|
| 288 |
+
df_all = pd.read_excel(
|
| 289 |
+
io.BytesIO(plan_bytes), sheet_name=sheet, header=None, engine="openpyxl"
|
| 290 |
+
)
|
| 291 |
+
else:
|
| 292 |
+
df_all = pd.read_csv(
|
| 293 |
+
io.BytesIO(plan_bytes), header=None, dtype=str, keep_default_na=False
|
| 294 |
+
)
|
| 295 |
+
|
| 296 |
+
# Heuristic: pick row with most non-blank cells among first 30 rows
|
| 297 |
+
max_cnt, hdr = 0, None
|
| 298 |
+
|
| 299 |
+
def _notblank(v):
|
| 300 |
+
s = "" if pd.isna(v) else str(v).strip()
|
| 301 |
+
return s != ""
|
| 302 |
+
|
| 303 |
+
for idx, row in df_all.head(30).iterrows():
|
| 304 |
+
cnt = sum(1 for v in row if _notblank(v))
|
| 305 |
+
if cnt > max_cnt:
|
| 306 |
+
max_cnt, hdr = cnt, idx
|
| 307 |
+
|
| 308 |
+
if hdr is None or max_cnt < 1:
|
| 309 |
+
st.error("Could not detect header row.")
|
| 310 |
+
st.stop()
|
| 311 |
+
|
| 312 |
+
row = df_all.iloc[hdr]
|
| 313 |
+
keep_idx = [i for i, v in enumerate(row) if _notblank(v)]
|
| 314 |
+
header_cols = [str(row[i]).strip() for i in keep_idx]
|
| 315 |
+
|
| 316 |
+
df_slice = df_all.iloc[hdr + 1 :, keep_idx].reset_index(drop=True)
|
| 317 |
+
df_slice.columns = header_cols
|
| 318 |
+
|
| 319 |
+
st.session_state["hdr_idx"] = hdr
|
| 320 |
+
st.session_state["df_slice"] = df_slice
|
| 321 |
+
st.session_state["header_cols"] = header_cols
|
| 322 |
+
st.session_state["keep_idx"] = keep_idx
|
| 323 |
+
|
| 324 |
+
df_slice = st.session_state["df_slice"]
|
| 325 |
+
header_cols = st.session_state["header_cols"]
|
| 326 |
+
keep_idx = st.session_state.get("keep_idx", list(range(len(header_cols))))
|
| 327 |
+
|
| 328 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 329 |
+
# 4) Mapping UI (NO FUZZY)
|
| 330 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 331 |
+
def canon(s: str) -> str:
|
| 332 |
+
if s is None:
|
| 333 |
+
return ""
|
| 334 |
+
s = re.sub(r"\s+", " ", str(s)).strip().lower()
|
| 335 |
+
s = re.sub(r"[^a-z0-9]+", " ", s)
|
| 336 |
+
return re.sub(r"\s+", " ", s).strip()
|
| 337 |
+
|
| 338 |
+
word_cols = df_word.columns.tolist()
|
| 339 |
+
|
| 340 |
+
ALIASES = {
|
| 341 |
+
"media plan": ["media plan", "plan", "campaign id", "mp", "line id"],
|
| 342 |
+
"budget": ["budget", "cost", "net cost", "amount", "spend"],
|
| 343 |
+
"product": ["product", "package", "pkg name"],
|
| 344 |
+
"details": [
|
| 345 |
+
"details",
|
| 346 |
+
"placement",
|
| 347 |
+
"tactic/placement description",
|
| 348 |
+
"description",
|
| 349 |
+
"line item",
|
| 350 |
+
"line name",
|
| 351 |
+
],
|
| 352 |
+
"targeting": ["targeting", "audience", "segment", "geo", "demo"],
|
| 353 |
+
"notes": ["notes", "comments", "remark"],
|
| 354 |
+
"rate value": ["rate value", "rate", "price", "cpm value", "cppv value"],
|
| 355 |
+
"rate type": ["rate type", "unit", "rate unit", "cpm", "cppv", "cpv"],
|
| 356 |
+
"ad size": ["ad size", "size", "dimensions", "adsize"],
|
| 357 |
+
"impressions": ["impressions", "imps", "imp", "units (impressions)"],
|
| 358 |
+
}
|
| 359 |
+
WORD_COLS = {
|
| 360 |
+
"media plan": "Media Plan",
|
| 361 |
+
"budget": "Budget",
|
| 362 |
+
"product": "Product",
|
| 363 |
+
"details": "Details",
|
| 364 |
+
"targeting": "Targeting",
|
| 365 |
+
"notes": "Notes",
|
| 366 |
+
"rate value": "Rate_Value",
|
| 367 |
+
"rate type": "Rate_Type",
|
| 368 |
+
"ad size": "Ad Size",
|
| 369 |
+
"impressions": "Impressions",
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
alias_index = {}
|
| 373 |
+
for key, examples in ALIASES.items():
|
| 374 |
+
target = WORD_COLS.get(key)
|
| 375 |
+
if not target or target not in word_cols:
|
| 376 |
+
continue
|
| 377 |
+
for ex in examples:
|
| 378 |
+
alias_index[canon(ex)] = target
|
| 379 |
+
|
| 380 |
+
mapping = st.session_state.get("mapping", {})
|
| 381 |
+
for h in header_cols:
|
| 382 |
+
hc = canon(h)
|
| 383 |
+
suggested = alias_index.get(hc, "<leave blank>")
|
| 384 |
+
mapping.setdefault(h, suggested)
|
| 385 |
+
|
| 386 |
+
st.subheader("Map Excel headers to Word columns")
|
| 387 |
+
cols_ui = st.columns(4)
|
| 388 |
+
opts = ["<leave blank>"] + word_cols
|
| 389 |
+
|
| 390 |
+
def status_badge(is_filled: bool) -> str:
|
| 391 |
+
bg = "#198754" if is_filled else "#DC3545"
|
| 392 |
+
text = "mapped" if is_filled else "blank"
|
| 393 |
+
return (
|
| 394 |
+
f'<span style="display:inline-block; background:{bg}; color:white; '
|
| 395 |
+
f"padding:2px 8px; border-radius:999px; font-size:12px; "
|
| 396 |
+
f'line-height:18px; margin-left:6px;">{text}</span>'
|
| 397 |
+
)
|
| 398 |
+
|
| 399 |
+
for i, excel_col in enumerate(header_cols):
|
| 400 |
+
default = mapping.get(excel_col, "<leave blank>")
|
| 401 |
+
idx = opts.index(default) if default in opts else 0
|
| 402 |
+
col = cols_ui[i % 4]
|
| 403 |
+
current_value = st.session_state.get(f"map_{i}", default)
|
| 404 |
+
is_filled = current_value != "<leave blank>"
|
| 405 |
+
col.markdown(
|
| 406 |
+
f'<div style="font-weight:600;">{excel_col}{status_badge(is_filled)}</div>',
|
| 407 |
+
unsafe_allow_html=True,
|
| 408 |
+
)
|
| 409 |
+
sel = col.selectbox(
|
| 410 |
+
label="",
|
| 411 |
+
options=opts,
|
| 412 |
+
index=idx,
|
| 413 |
+
key=f"map_{i}",
|
| 414 |
+
label_visibility="collapsed",
|
| 415 |
+
)
|
| 416 |
+
mapping[excel_col] = sel
|
| 417 |
+
|
| 418 |
+
st.session_state["mapping"] = mapping
|
| 419 |
+
|
| 420 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 421 |
+
# Extra inputs – campaign date range
|
| 422 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 423 |
+
st.subheader("Campaign Dates")
|
| 424 |
+
default_start = date.today()
|
| 425 |
+
default_end = default_start + timedelta(days=30)
|
| 426 |
+
st.date_input(
|
| 427 |
+
"Select campaign start and end dates",
|
| 428 |
+
value=(default_start, default_end),
|
| 429 |
+
key="campaign_dates",
|
| 430 |
+
)
|
| 431 |
+
|
| 432 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 433 |
+
# 5) Fill template columns
|
| 434 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 435 |
+
if st.button("Fill Template Columns", key="fill_btn"):
|
| 436 |
+
combined = df_slice.copy()
|
| 437 |
+
used = set()
|
| 438 |
+
duplicates = []
|
| 439 |
+
|
| 440 |
+
# Apply mapping, enforcing uniqueness
|
| 441 |
+
for excel_col in header_cols:
|
| 442 |
+
word_col = mapping.get(excel_col, "<leave blank>")
|
| 443 |
+
if word_col == "<leave blank>":
|
| 444 |
+
continue
|
| 445 |
+
if word_col in used:
|
| 446 |
+
duplicates.append(word_col)
|
| 447 |
+
continue
|
| 448 |
+
if word_col == "Budget":
|
| 449 |
+
# Only fill Budget on rows that have a Rate_Value
|
| 450 |
+
if excel_col not in combined.columns:
|
| 451 |
+
continue
|
| 452 |
+
|
| 453 |
+
src_budget = pd.to_numeric(df_word["Budget"], errors="coerce")
|
| 454 |
+
rv = pd.to_numeric(df_word["Rate_Value"], errors="coerce")
|
| 455 |
+
|
| 456 |
+
# rows with a non-null, non-zero rate_value
|
| 457 |
+
has_rate_idx = df_word.index[rv.notna()]
|
| 458 |
+
# restrict to rows that exist in the combined DF
|
| 459 |
+
has_rate_idx = [i for i in has_rate_idx if i < len(combined)]
|
| 460 |
+
|
| 461 |
+
# clear the entire target column, then fill only those rows
|
| 462 |
+
combined[excel_col] = np.nan
|
| 463 |
+
if has_rate_idx:
|
| 464 |
+
combined.loc[has_rate_idx, excel_col] = src_budget.loc[has_rate_idx].values
|
| 465 |
+
else:
|
| 466 |
+
if excel_col in combined.columns:
|
| 467 |
+
combined[excel_col] = df_word[word_col].reset_index(drop=True)
|
| 468 |
+
|
| 469 |
+
# mark as used for BOTH branches
|
| 470 |
+
used.add(word_col)
|
| 471 |
+
|
| 472 |
+
|
| 473 |
+
|
| 474 |
+
if duplicates:
|
| 475 |
+
st.warning(
|
| 476 |
+
"The following Word columns were selected more than once and were ignored for duplicates: "
|
| 477 |
+
f"{sorted(set(duplicates))}"
|
| 478 |
+
)
|
| 479 |
+
|
| 480 |
+
# Fill Start/End Date columns if present
|
| 481 |
+
campaign_dates = st.session_state.get("campaign_dates")
|
| 482 |
+
if campaign_dates and len(campaign_dates) == 2:
|
| 483 |
+
start_date, end_date = campaign_dates
|
| 484 |
+
start_aliases = {"start date", "flight start", "flight start date"}
|
| 485 |
+
end_aliases = {"end date", "flight end", "flight end date"}
|
| 486 |
+
start_col = next((c for c in header_cols if canon(c) in start_aliases), None)
|
| 487 |
+
end_col = next((c for c in header_cols if canon(c) in end_aliases), None)
|
| 488 |
+
if start_col and start_col in combined.columns:
|
| 489 |
+
combined[start_col] = start_date
|
| 490 |
+
if end_col and end_col in combined.columns:
|
| 491 |
+
combined[end_col] = end_date
|
| 492 |
+
|
| 493 |
+
# Format numeric columns based on mapping (strict numerics for export)
|
| 494 |
+
for col in header_cols:
|
| 495 |
+
word_col = mapping.get(col)
|
| 496 |
+
if word_col == "Budget" and col in combined.columns:
|
| 497 |
+
combined[col] = pd.to_numeric(combined[col], errors="coerce").round(2).astype(float)
|
| 498 |
+
elif word_col == "Impressions" and col in combined.columns:
|
| 499 |
+
# keep as float for editor (no %d/NaN issue); export will coerce to int
|
| 500 |
+
combined[col] = pd.to_numeric(combined[col], errors="coerce").round(0).astype(float)
|
| 501 |
+
elif word_col == "Rate_Value" and col in combined.columns:
|
| 502 |
+
combined[col] = pd.to_numeric(combined[col], errors="coerce").round(2).astype(float)
|
| 503 |
+
|
| 504 |
+
st.session_state["combined"] = combined
|
| 505 |
+
|
| 506 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 507 |
+
# 6) Show + Download
|
| 508 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 509 |
+
if "combined" in st.session_state:
|
| 510 |
+
# Filled Media Plan Template (formatted preview)
|
| 511 |
+
st.subheader("Filled Media Plan Template")
|
| 512 |
+
|
| 513 |
+
combined = st.session_state["combined"]
|
| 514 |
+
combined_preview = combined.copy()
|
| 515 |
+
mapping = st.session_state.get("mapping", {}) # Excel col name -> Word col name
|
| 516 |
+
|
| 517 |
+
def _fmt_money0(x):
|
| 518 |
+
return "" if pd.isna(x) else f"${float(x):,.0f}"
|
| 519 |
+
|
| 520 |
+
def _fmt_money2(x):
|
| 521 |
+
return "" if pd.isna(x) else f"${float(x):,.2f}"
|
| 522 |
+
|
| 523 |
+
def _fmt_int(x):
|
| 524 |
+
if pd.isna(x):
|
| 525 |
+
return ""
|
| 526 |
+
try:
|
| 527 |
+
return f"{int(round(float(x))):,}"
|
| 528 |
+
except Exception:
|
| 529 |
+
return ""
|
| 530 |
+
|
| 531 |
+
# Helper: find the Excel column that was mapped to a given Word column
|
| 532 |
+
def _excel_col_for(word_col: str):
|
| 533 |
+
for excel_col, mapped in mapping.items():
|
| 534 |
+
if mapped == word_col and excel_col in combined_preview.columns:
|
| 535 |
+
return excel_col
|
| 536 |
+
return None
|
| 537 |
+
|
| 538 |
+
budget_col = _excel_col_for("Budget")
|
| 539 |
+
rate_col = _excel_col_for("Rate_Value")
|
| 540 |
+
imps_col = _excel_col_for("Impressions")
|
| 541 |
+
|
| 542 |
+
# Apply formatting ONLY to columns that exist
|
| 543 |
+
if budget_col:
|
| 544 |
+
combined_preview[budget_col] = combined_preview[budget_col].map(_fmt_money0)
|
| 545 |
+
if rate_col:
|
| 546 |
+
combined_preview[rate_col] = combined_preview[rate_col].map(_fmt_money2)
|
| 547 |
+
if imps_col:
|
| 548 |
+
combined_preview[imps_col] = combined_preview[imps_col].map(_fmt_int)
|
| 549 |
+
|
| 550 |
+
editor = st.data_editor if hasattr(st, "data_editor") else st.experimental_data_editor
|
| 551 |
+
_ = editor(
|
| 552 |
+
combined_preview,
|
| 553 |
+
num_rows="dynamic",
|
| 554 |
+
use_container_width=True,
|
| 555 |
+
disabled=True, # keeps numeric types intact in st.session_state["combined"]
|
| 556 |
+
)
|
| 557 |
+
|
| 558 |
+
|
| 559 |
+
|
| 560 |
+
# Export filled template as Excel, preserving original workbook formatting
|
| 561 |
+
try:
|
| 562 |
+
if plan_ext in [".xls", ".xlsx"]:
|
| 563 |
+
wb = load_workbook(io.BytesIO(plan_bytes))
|
| 564 |
+
ws = wb[sheet]
|
| 565 |
+
else:
|
| 566 |
+
wb = Workbook()
|
| 567 |
+
ws = wb.active
|
| 568 |
+
|
| 569 |
+
hdr_idx = st.session_state.get("hdr_idx", 0)
|
| 570 |
+
keep_idx = st.session_state.get("keep_idx", list(range(len(header_cols))))
|
| 571 |
+
|
| 572 |
+
# Clear existing data below header for mapped columns
|
| 573 |
+
for r in range(hdr_idx + 2, ws.max_row + 1):
|
| 574 |
+
for c in keep_idx:
|
| 575 |
+
ws.cell(row=r, column=c + 1).value = None
|
| 576 |
+
|
| 577 |
+
# Accounting formats: 0-decimals for Budget, 2-decimals for Rate_Value
|
| 578 |
+
acct_usd_0 = '_($* #,##0_);_($* (#,##0);_($* "-"_);_(@_)'
|
| 579 |
+
acct_usd_2 = '_($* #,##0.00_);_($* (#,##0.00);_($* "-"??_);_(@_)'
|
| 580 |
+
|
| 581 |
+
mapping = st.session_state.get("mapping", {})
|
| 582 |
+
cols = st.session_state["combined"].columns.tolist()
|
| 583 |
+
|
| 584 |
+
def _to_excel_numeric(v):
|
| 585 |
+
if pd.isna(v):
|
| 586 |
+
return None
|
| 587 |
+
if isinstance(v, (np.floating,)):
|
| 588 |
+
return float(v)
|
| 589 |
+
if isinstance(v, (np.integer,)):
|
| 590 |
+
return int(v)
|
| 591 |
+
return v
|
| 592 |
+
|
| 593 |
+
# Write data while retaining styles
|
| 594 |
+
for r_offset, row in enumerate(
|
| 595 |
+
st.session_state["combined"].itertuples(index=False), start=0
|
| 596 |
+
):
|
| 597 |
+
for c_offset, val in enumerate(row):
|
| 598 |
+
excel_row = hdr_idx + 2 + r_offset
|
| 599 |
+
excel_col_idx = keep_idx[c_offset] + 1
|
| 600 |
+
excel_col_name = cols[c_offset]
|
| 601 |
+
word_col = mapping.get(excel_col_name)
|
| 602 |
+
|
| 603 |
+
py_val = _to_excel_numeric(val)
|
| 604 |
+
|
| 605 |
+
# Impressions should be integer for "#,##0"
|
| 606 |
+
if word_col == "Impressions" and py_val is not None:
|
| 607 |
+
try:
|
| 608 |
+
py_val = int(round(float(py_val)))
|
| 609 |
+
except Exception:
|
| 610 |
+
pass
|
| 611 |
+
|
| 612 |
+
cell = ws.cell(row=excel_row, column=excel_col_idx, value=py_val)
|
| 613 |
+
|
| 614 |
+
# Number formats
|
| 615 |
+
if word_col == "Impressions":
|
| 616 |
+
cell.number_format = "#,##0"
|
| 617 |
+
elif word_col == "Budget":
|
| 618 |
+
cell.number_format = acct_usd_0 # Accounting, no decimals
|
| 619 |
+
elif word_col == "Rate_Value":
|
| 620 |
+
cell.number_format = acct_usd_2 # Accounting, 2 decimals
|
| 621 |
+
|
| 622 |
+
out = io.BytesIO()
|
| 623 |
+
wb.save(out)
|
| 624 |
+
out.seek(0)
|
| 625 |
+
st.download_button(
|
| 626 |
+
"Download Filled Excel",
|
| 627 |
+
out.read(),
|
| 628 |
+
file_name=f"filled_{os.path.splitext(plan_name)[0]}.xlsx",
|
| 629 |
+
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
| 630 |
+
)
|
| 631 |
+
except Exception as e:
|
| 632 |
+
st.error(f"Excel download error: {e}")
|
| 633 |
+
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
altair
|
| 2 |
+
streamlit
|
| 3 |
+
pandas
|
| 4 |
+
gspread
|
| 5 |
+
google-auth
|
| 6 |
+
openai
|
| 7 |
+
snowflake-connector-python[pandas]
|
| 8 |
+
cryptography
|
| 9 |
+
openpyxl
|
| 10 |
+
python-docx
|
style.css
DELETED
|
@@ -1,28 +0,0 @@
|
|
| 1 |
-
body {
|
| 2 |
-
padding: 2rem;
|
| 3 |
-
font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
|
| 4 |
-
}
|
| 5 |
-
|
| 6 |
-
h1 {
|
| 7 |
-
font-size: 16px;
|
| 8 |
-
margin-top: 0;
|
| 9 |
-
}
|
| 10 |
-
|
| 11 |
-
p {
|
| 12 |
-
color: rgb(107, 114, 128);
|
| 13 |
-
font-size: 15px;
|
| 14 |
-
margin-bottom: 10px;
|
| 15 |
-
margin-top: 5px;
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
.card {
|
| 19 |
-
max-width: 620px;
|
| 20 |
-
margin: 0 auto;
|
| 21 |
-
padding: 16px;
|
| 22 |
-
border: 1px solid lightgray;
|
| 23 |
-
border-radius: 16px;
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
.card p:last-child {
|
| 27 |
-
margin-bottom: 0;
|
| 28 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|